Detail.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898
  1. <template>
  2. <div v-loading="loading">
  3. <div class="btn-group">
  4. <div class="left">
  5. <div v-if="checkRecordList.length" class="left-content">
  6. <span class="title">检验记录列表:</span>
  7. <el-button
  8. :type="index === activeButtonIndex ? 'primary' : 'default'"
  9. v-for="(report, index) in checkRecordList"
  10. plain
  11. :key="report.id"
  12. @click="() => handleBtnClick(index, report.id)"
  13. >
  14. {{ report.label }}
  15. </el-button>
  16. </div>
  17. <div v-if="isTypeTestFlag" class="left-content">
  18. <span class="title">报告内容:</span>
  19. <el-button
  20. :type="activeContentButtonFlag == '1' ? 'primary' : 'default'"
  21. @click="handleContentBtnClick('1')"
  22. >
  23. 检验报告
  24. </el-button>
  25. <el-button
  26. :type="activeContentButtonFlag == '2' ? 'primary' : 'default'"
  27. @click="handleContentBtnClick('2')"
  28. >
  29. 型式试验报告
  30. </el-button>
  31. </div>
  32. </div>
  33. <div class="right">
  34. <el-button v-if="route.query.status === '0'" type="primary" @click="handleSubmitVerify">
  35. <el-icon><Check /></el-icon>&nbsp;提交
  36. </el-button>
  37. <el-button v-if="route.query.status === '0'" type="warning" @click="handleRevertReport">
  38. 回退
  39. </el-button>
  40. <el-button type="success" @click="handleDownloadFn"><el-icon><Download /></el-icon>&nbsp;下载报告内容</el-button>
  41. <el-button type="danger" @click="handleClose">退出</el-button>
  42. </div>
  43. </div>
  44. <el-row class="task-detail-layout" :gutter="16">
  45. <el-col :span="12" class="wrapperContainer">
  46. <ContentWrap title="报告内容" v-loading="reportSourceLoading">
  47. <el-scrollbar ref="reportDetailscrollbarRef" always>
  48. <div
  49. v-for="(report, index) in lazyRenderReportFileList"
  50. :key="report.key"
  51. class="file-viewer-container"
  52. >
  53. <!-- PDF预览 -->
  54. <VuePdfEmbed
  55. v-if="report.type === 'pdf'"
  56. class="reportPDFViewer"
  57. :height="wrapperContainerHeight"
  58. :width="pdfContentWidth"
  59. :source="report.url"
  60. :text-layer="false"
  61. :annotation-layer="false"
  62. @rendered="() => handleReportFileRendered(report.url, index, 'pdf')"
  63. @rendering-failed="() => handleFileRenderError(report.url, index, 'pdf')"
  64. />
  65. </div>
  66. </el-scrollbar>
  67. </ContentWrap>
  68. </el-col>
  69. <el-col :span="12" ref="col24HalfRef" class="wrapperContainer">
  70. <ContentWrap title="检验记录" v-loading="recordSourceLoading">
  71. <el-scrollbar
  72. ref="recordDetailscrollbarRef"
  73. always
  74. @scroll="handleRecordScroll"
  75. v-if="checkRecordList.length"
  76. >
  77. <div
  78. v-for="(record, index) in lazyRenderRecordFileList"
  79. :key="record.key"
  80. class="file-viewer-container"
  81. :data-record-id="record.reportId"
  82. :data-record-index="index"
  83. >
  84. <!-- PDF预览 -->
  85. <VuePdfEmbed
  86. v-if="record.type === 'pdf'"
  87. class="recordPDFViewer"
  88. :height="wrapperContainerHeight"
  89. :width="pdfContentWidth"
  90. :source="record.url"
  91. :text-layer="false"
  92. :annotation-layer="false"
  93. @rendered="() => handleRecordFileRendered(record.url, index, 'pdf')"
  94. @rendering-failed="() => handleFileRenderError(record.url, index, 'pdf')"
  95. />
  96. <!-- 图片预览 -->
  97. <div v-else-if="record.type === 'image'" class="image-viewer">
  98. <img
  99. :src="record.url"
  100. :style="{ maxWidth: pdfContentWidth + 'px', height: 'auto' }"
  101. @load="() => handleRecordFileRendered(record.url, index, 'image')"
  102. @error="() => handleFileRenderError(record.url, index, 'image')"
  103. alt="图片预览"
  104. />
  105. </div>
  106. <!-- Word/Excel等Office文档预览 -->
  107. <div v-else-if="['word', 'excel', 'ppt'].includes(record.type)" class="office-viewer">
  108. <div class="office-preview-container">
  109. <iframe
  110. v-if="record.previewUrl"
  111. :src="record.previewUrl"
  112. :width="pdfContentWidth"
  113. :height="wrapperContainerHeight"
  114. frameborder="0"
  115. @load="() => handleRecordFileRendered(record.url, index, record.type)"
  116. ></iframe>
  117. <div v-else class="office-fallback">
  118. <el-icon size="48"><Document /></el-icon>
  119. <p>{{ record.fileName || '文档预览' }}</p>
  120. <el-button type="primary" @click="downloadFile(record.url, record.fileName)">
  121. 下载查看
  122. </el-button>
  123. </div>
  124. </div>
  125. </div>
  126. <!-- 其他格式文件 -->
  127. <div v-else class="unsupported-file">
  128. <el-icon size="48"><Document /></el-icon>
  129. <p>不支持的文件格式: {{ record.type }}</p>
  130. <p>{{ record.fileName }}</p>
  131. <el-button type="primary" @click="downloadFile(record.url, record.fileName)">
  132. 下载文件
  133. </el-button>
  134. </div>
  135. </div>
  136. </el-scrollbar>
  137. <div v-else class="no-data">暂无检验记录</div>
  138. </ContentWrap>
  139. </el-col>
  140. </el-row>
  141. </div>
  142. <AuditUserDialog
  143. v-if="isShowApproveByDialog"
  144. v-model="isShowApproveByDialog"
  145. :apiFn="getAuditList"
  146. :apiParams="{ roleCode: 'sysbgpzr' }"
  147. title="请选择报告批准人"
  148. selectedAlertText="已选择报告批准人"
  149. :columns="auditDialogColumns"
  150. :deptIdDefaultFlag="true"
  151. :searchFormProps="labelWidthDefault"
  152. @confirm="handleApproveBySelectConfirm"
  153. />
  154. <RejectDialog
  155. v-if="rejectDialogVisible"
  156. v-model:modelValue="rejectDialogVisible"
  157. title="回退"
  158. :apiParams="rejectParams"
  159. :apiFn="returnReportRatify"
  160. reasonLabel="回退原因"
  161. reasonProp="ratifyRollbackReason"
  162. @success="handleClose"
  163. />
  164. </template>
  165. <script setup lang="ts">
  166. import { Document, Download, Check} from '@element-plus/icons-vue'
  167. const AuditUserDialog = defineAsyncComponent(
  168. () => import('@/views/Functional/components/AuditUserDialog.vue')
  169. )
  170. const RejectDialog = defineAsyncComponent(
  171. () => import('@/views/pressure/components/RejectDialog.vue')
  172. )
  173. import VuePdfEmbed from 'vue-pdf-embed'
  174. import 'vue-pdf-embed/dist/styles/annotationLayer.css'
  175. import 'vue-pdf-embed/dist/styles/textLayer.css'
  176. import { debounce, throttle } from 'lodash-es'
  177. import {
  178. getReportCheckRecordList,
  179. getReportCheckPdf,
  180. getCheckRecordPdf,
  181. getReportPDFPdf,
  182. submitReportCheck,
  183. submitReportRatify,
  184. returnReportRatify,
  185. reviewFallbackReportCheck
  186. } from '@/api/laboratory/functional/report'
  187. import { useRoute, useRouter } from 'vue-router'
  188. import { getAuditList } from '@/api/laboratory/functional'
  189. import { useTagsViewStore } from '@/store/modules/tagsView'
  190. import dayjs from 'dayjs'
  191. import { useEmitt } from '@/hooks/web/useEmitt'
  192. const { emitter } = useEmitt()
  193. const tagsViewStore = useTagsViewStore()
  194. const route = useRoute()
  195. const router = useRouter()
  196. const getTaskId = computed(() => route.query?.id || '')
  197. const getReportType = computed(() => route.query?.reportType || '')
  198. const loading = ref(true)
  199. const recordLoading = ref(false)
  200. const recordContentRef = ref()
  201. const contentWidth = ref(0)
  202. const getContentWidth = computed(() => contentWidth.value)
  203. const activeReportIndex = ref<number>(0)
  204. const activeRecordIndex = ref<number>(0)
  205. const btnClickRender = ref<number>(0)
  206. const reportIdMaps = ref(new Map())
  207. const recordSourceLoading = ref(false)
  208. const reportSourceLoading = ref(false)
  209. const lazyRenderRecordFileList = ref<any[]>([])
  210. const lazyRenderReportFileList = ref<any[]>([])
  211. const pdfViewerHeight = ref(0)
  212. const pdfContentWidth = ref(0)
  213. const activeRecordUrl = ref('')
  214. const activeReportUrl = ref('')
  215. const recordDetailscrollbarRef = ref()
  216. const reportDetailscrollbarRef = ref()
  217. const recordLastScrollTop = ref(0)
  218. const reportLastScrollTop = ref(0)
  219. const wrapperContainerHeight = ref(0)
  220. const recordFileUrlMaps = ref(new Map()) // 修复:用于存储检验记录文件的DOM映射
  221. const activeContentButtonFlag = ref('1')
  222. // 按钮高亮相关变量
  223. const activeButtonIndex = ref<number>(0)
  224. const isManualClick = ref(false)
  225. const isTypeTestFlag = computed(() => route.query.isTypeTest === '1')
  226. const checkRecordList = ref([
  227. {
  228. label: '测试的',
  229. value: 'https://yudao-admin.hofo.co/dexdev/b05b962cb88589e006c50d4f548f4f9a51597ae45b923a2677f31561d55d9205'
  230. }
  231. ])
  232. const labelWidthDefault = ref({labelWidth: 'auto'})
  233. const auditDialogColumns = computed(() => [
  234. {
  235. type: 'selection',
  236. fieldProps: {
  237. reserveSelection: true
  238. }
  239. },
  240. {
  241. label: '姓名',
  242. prop: 'nickName',
  243. search: {
  244. type: 'input',
  245. span: 12,
  246. placeholder: '请输入姓名'
  247. },
  248. render: (row) => {
  249. return row?.nickname || '-'
  250. }
  251. },
  252. {
  253. label: '部门',
  254. prop: 'deptId',
  255. search: {
  256. type: 'DeptSelect',
  257. span: 12,
  258. placeholder: '请选择部门'
  259. },
  260. render: (row) => {
  261. return row?.deptName || '-'
  262. }
  263. }
  264. ])
  265. // 文件类型检测函数
  266. const detectFileType = async (blob: Blob, fileName?: string): Promise<string> => {
  267. const fileExtension = fileName?.split('.').pop()?.toLowerCase()
  268. const mimeType = blob.type.toLowerCase()
  269. if (mimeType.includes('pdf')) return 'pdf'
  270. if (mimeType.includes('image')) return 'image'
  271. if (mimeType.includes('word') || mimeType.includes('msword') || mimeType.includes('openxmlformats-officedocument.wordprocessingml')) return 'word'
  272. if (mimeType.includes('excel') || mimeType.includes('spreadsheet') || mimeType.includes('openxmlformats-officedocument.spreadsheetml')) return 'excel'
  273. if (mimeType.includes('powerpoint') || mimeType.includes('presentation') || mimeType.includes('openxmlformats-officedocument.presentationml')) return 'ppt'
  274. if (fileExtension) {
  275. const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
  276. const pdfExts = ['pdf']
  277. const wordExts = ['doc', 'docx']
  278. const excelExts = ['xls', 'xlsx', 'csv']
  279. const pptExts = ['ppt', 'pptx']
  280. if (imageExts.includes(fileExtension)) return 'image'
  281. if (pdfExts.includes(fileExtension)) return 'pdf'
  282. if (wordExts.includes(fileExtension)) return 'word'
  283. if (excelExts.includes(fileExtension)) return 'excel'
  284. if (pptExts.includes(fileExtension)) return 'ppt'
  285. }
  286. try {
  287. const arrayBuffer = await blob.slice(0, 20).arrayBuffer()
  288. const uint8Array = new Uint8Array(arrayBuffer)
  289. const header = Array.from(uint8Array).map(b => b.toString(16).padStart(2, '0')).join('')
  290. if (header.startsWith('25504446')) return 'pdf'
  291. if (header.startsWith('ffd8ff')) return 'image'
  292. if (header.startsWith('89504e47')) return 'image'
  293. if (header.startsWith('47494638')) return 'image'
  294. if (header.startsWith('424d')) return 'image'
  295. if (header.startsWith('504b0304')) {
  296. if (fileName?.includes('word') || fileName?.includes('.docx')) return 'word'
  297. if (fileName?.includes('excel') || fileName?.includes('.xlsx')) return 'excel'
  298. if (fileName?.includes('powerpoint') || fileName?.includes('.pptx')) return 'ppt'
  299. }
  300. } catch (error) {
  301. console.warn('文件类型检测失败:', error)
  302. }
  303. return 'unknown'
  304. }
  305. // 生成Office文档预览链接(如果需要)
  306. const generateOfficePreviewUrl = (fileUrl: string, fileType: string): string | null => {
  307. return null
  308. }
  309. // 下载文件
  310. const downloadFile = (url: string, fileName?: string) => {
  311. const link = document.createElement('a')
  312. link.href = url
  313. link.download = fileName || '文件'
  314. document.body.appendChild(link)
  315. link.click()
  316. document.body.removeChild(link)
  317. }
  318. // 获取检验项目填写记录
  319. const getCheckRecordList = async () => {
  320. const result = await getReportCheckRecordList({
  321. taskId: getTaskId.value
  322. })
  323. checkRecordList.value = result
  324. .filter((x) => x.itemUrl || x.attachmentUrl)
  325. .map((x) => ({ label: x.name, value: x.id, id: x.id }))
  326. loading.value = false
  327. }
  328. // 选择检验项目
  329. const checkedRecord = ref('')
  330. // 提交审批
  331. const isShowApproveByDialog = ref(false)
  332. const handleSubmitVerify = async () => {
  333. const result = await submitReportRatify({
  334. id: route.query.id
  335. })
  336. if (result) {
  337. ElMessage.success('提交成功')
  338. handleClose()
  339. }
  340. }
  341. const handleApproveBySelectConfirm = async (res) => {
  342. const result = await submitReportCheck({
  343. id: route.query.id,
  344. reportRatifyBy: res[0]
  345. })
  346. if (result) {
  347. ElMessage.success('提交成功')
  348. handleClose()
  349. }
  350. }
  351. // 回退报告
  352. const rejectDialogVisible = ref(false)
  353. const rejectParams = ref({})
  354. const handleRevertReport = () => {
  355. rejectDialogVisible.value = true
  356. rejectParams.value = {
  357. ids: [route.query.id]
  358. }
  359. }
  360. // 退出详情页
  361. const handleClose = () => {
  362. tagsViewStore.closeSelectedTag(route)
  363. emitter.emit('refresh-ReportPreparationIndex-list')
  364. router.push({
  365. name: 'ReportApproveList'
  366. })
  367. }
  368. // 修复:文件渲染成功处理 - 更新检验记录文件的DOM映射
  369. const handleRecordFileRendered = (url: string, index: number, fileType: string) => {
  370. console.log(`检验记录文件渲染完成: ${fileType}`)
  371. recordSourceLoading.value = false
  372. // 等待DOM更新后再获取元素
  373. nextTick(() => {
  374. const containerElements = document.querySelectorAll('.file-viewer-container[data-record-index]')
  375. containerElements.forEach((element, domIndex) => {
  376. const recordIndex = parseInt(element.getAttribute('data-record-index') || '0')
  377. const recordId = element.getAttribute('data-record-id')
  378. const fileItem = lazyRenderRecordFileList.value[recordIndex]
  379. if (fileItem) {
  380. recordFileUrlMaps.value.set(fileItem.url, {
  381. index: recordIndex,
  382. dom: element as HTMLElement,
  383. recordId: recordId
  384. })
  385. }
  386. })
  387. })
  388. }
  389. // 报告文件渲染成功处理
  390. const handleReportFileRendered = (url: string, index: number, fileType: string) => {
  391. console.log(`报告文件渲染完成: ${fileType}`)
  392. reportSourceLoading.value = false
  393. }
  394. // 文件渲染失败处理
  395. const handleFileRenderError = (url: string, index: number, fileType: string) => {
  396. console.error(`文件渲染失败: ${fileType}, URL: ${url}`)
  397. recordSourceLoading.value = false
  398. reportSourceLoading.value = false
  399. ElMessage.error(`${fileType}文件加载失败`)
  400. }
  401. // 处理文件数据
  402. const processFileData = async (blob: Blob, reportId: string, index: number, fileName?: string) => {
  403. const fileType = await detectFileType(blob, fileName)
  404. const fileUrl = URL.createObjectURL(blob)
  405. const fileData = {
  406. url: fileUrl,
  407. type: fileType,
  408. fileName: fileName,
  409. key: `${dayjs().valueOf()}-${index}`,
  410. index,
  411. reportId
  412. }
  413. // 如果是Office文档,尝试生成预览链接
  414. if (['word', 'excel', 'ppt'].includes(fileType)) {
  415. fileData.previewUrl = generateOfficePreviewUrl(fileUrl, fileType)
  416. }
  417. return fileData
  418. }
  419. // 下载报告内容功能(复用已加载的文件)
  420. const handleDownloadFn = async () => {
  421. try {
  422. // 检查是否已经有加载的报告文件
  423. const reportFile = lazyRenderReportFileList.value[0]
  424. if (reportFile && reportFile.url) {
  425. // 如果已经有加载的文件,直接下载
  426. const response = await fetch(reportFile.url)
  427. const blob = await response.blob()
  428. const link = document.createElement('a')
  429. link.href = URL.createObjectURL(blob)
  430. link.download = `报告内容_${getTaskId.value}_${dayjs().format('YYYY-MM-DD')}.pdf`
  431. document.body.appendChild(link)
  432. link.click()
  433. document.body.removeChild(link)
  434. URL.revokeObjectURL(link.href)
  435. ElMessage.success('下载成功')
  436. } else {
  437. // 如果还没有加载,重新获取
  438. loading.value = true
  439. const fileBlob = await getReportPDFPdf({ id: getTaskId.value })
  440. activeContentButtonFlag.value = '1'
  441. const url = URL.createObjectURL(fileBlob)
  442. const link = document.createElement('a')
  443. link.href = url
  444. link.download = `报告内容_${getTaskId.value}_${dayjs().format('YYYY-MM-DD')}.pdf`
  445. document.body.appendChild(link)
  446. link.click()
  447. document.body.removeChild(link)
  448. URL.revokeObjectURL(url)
  449. ElMessage.success('下载成功')
  450. }
  451. } catch (error) {
  452. console.error('下载报告失败:', error)
  453. ElMessage.error('下载失败,请重试')
  454. } finally {
  455. loading.value = false
  456. }
  457. }
  458. // 修复:按钮点击处理 - 点击按钮时滚动到对应文件
  459. const handleBtnClick = async (index: number, reportId: string) => {
  460. isManualClick.value = true
  461. activeButtonIndex.value = index
  462. // 查找对应的文件
  463. const targetFile = lazyRenderRecordFileList.value.find(file => file.reportId === reportId)
  464. if (targetFile) {
  465. // 如果文件已经加载,直接滚动到对应位置
  466. const fileMapping = recordFileUrlMaps.value.get(targetFile.url)
  467. if (fileMapping?.dom) {
  468. recordDetailscrollbarRef.value?.setScrollTop(fileMapping.dom.offsetTop)
  469. }
  470. } else {
  471. // 如果文件还没有加载,先加载文件
  472. try {
  473. recordSourceLoading.value = true
  474. const fileBlob = await getCheckRecordPdf({ itemId: reportId })
  475. const fileData = await processFileData(fileBlob, reportId, index)
  476. // 插入到正确的位置
  477. const recordUrllist = [...lazyRenderRecordFileList.value]
  478. recordUrllist.push(fileData)
  479. lazyRenderRecordFileList.value = recordUrllist.sort((a, b) => a.index - b.index)
  480. // 等待文件渲染完成后滚动
  481. await nextTick()
  482. // 设置一个延时等待DOM完全更新
  483. setTimeout(() => {
  484. const fileMapping = recordFileUrlMaps.value.get(fileData.url)
  485. if (fileMapping?.dom) {
  486. recordDetailscrollbarRef.value?.setScrollTop(fileMapping.dom.offsetTop)
  487. }
  488. }, 300)
  489. } catch (error) {
  490. console.error('动态加载文件失败:', error)
  491. recordSourceLoading.value = false
  492. ElMessage.error('加载文件失败')
  493. }
  494. }
  495. setTimeout(() => {
  496. isManualClick.value = false
  497. }, 500)
  498. }
  499. // 修复:处理检验记录滚动事件 - 根据滚动位置高亮对应按钮
  500. const handleRecordScroll = throttle((event) => {
  501. if (isManualClick.value) return
  502. const scrollTop = event.scrollTop
  503. const viewportHeight = recordDetailscrollbarRef.value?.$refs?.wrap?.clientHeight || 0
  504. const scrollCenter = scrollTop + viewportHeight / 2
  505. let targetButtonIndex = 0
  506. let minDistance = Infinity
  507. // 遍历所有文件容器,找到距离滚动中心最近的文件
  508. recordFileUrlMaps.value.forEach((mapping, url) => {
  509. const { dom, index } = mapping
  510. if (dom) {
  511. const elementTop = dom.offsetTop
  512. const elementBottom = elementTop + dom.offsetHeight
  513. const elementCenter = elementTop + dom.offsetHeight / 2
  514. // 计算元素中心与滚动中心的距离
  515. const distance = Math.abs(elementCenter - scrollCenter)
  516. // 如果元素在视口内或距离最近,更新目标按钮索引
  517. if (distance < minDistance || (scrollCenter >= elementTop && scrollCenter <= elementBottom)) {
  518. minDistance = distance
  519. targetButtonIndex = index
  520. }
  521. }
  522. })
  523. // 更新按钮高亮
  524. if (activeButtonIndex.value !== targetButtonIndex) {
  525. activeButtonIndex.value = targetButtonIndex
  526. }
  527. }, 100)
  528. // 获取PDF宽度
  529. const col24HalfRef = ref(null)
  530. const handleWindowResize = debounce(() => {
  531. pdfContentWidth.value = col24HalfRef.value?.$el.clientWidth - 28
  532. }, 100)
  533. const handleContentBtnClick = async (flag: string) => {
  534. if (activeContentButtonFlag.value === flag) return
  535. activeContentButtonFlag.value = flag
  536. reportSourceLoading.value = true
  537. if (flag === '2') {
  538. lazyRenderReportFileList.value = []
  539. // 并行获取报告内容
  540. const reportFilePromise = getReportPDFPdf({ id: getTaskId.value, type: '1' })
  541. .then(async (fileBlob) => {
  542. const fileData = await processFileData(fileBlob, getTaskId.value, 0, '报告内容')
  543. reportIdMaps.value.set('record-' + getTaskId.value, { index: 0, url: fileData.url })
  544. lazyRenderReportFileList.value.push(fileData)
  545. activeRecordUrl.value = fileData.url
  546. return fileData.url
  547. })
  548. .catch((error) => {
  549. console.error('获取报告文件失败:', error)
  550. reportSourceLoading.value = false
  551. throw error
  552. })
  553. await reportFilePromise
  554. }
  555. if (flag === '1') {
  556. lazyRenderReportFileList.value = []
  557. // 并行获取报告内容
  558. const reportFilePromise = getReportPDFPdf({ id: getTaskId.value })
  559. .then(async (fileBlob) => {
  560. const fileData = await processFileData(fileBlob, getTaskId.value, 0, '报告内容')
  561. reportIdMaps.value.set('record-' + getTaskId.value, { index: 0, url: fileData.url })
  562. lazyRenderReportFileList.value.push(fileData)
  563. activeRecordUrl.value = fileData.url
  564. return fileData.url
  565. })
  566. .catch((error) => {
  567. console.error('获取报告文件失败:', error)
  568. reportSourceLoading.value = false
  569. throw error
  570. })
  571. await reportFilePromise
  572. }
  573. }
  574. // 优化后的 watch 函数
  575. watch(
  576. () => [getTaskId.value],
  577. async ([taskId]: [string]) => {
  578. if (!taskId) return
  579. try {
  580. await getCheckRecordList()
  581. loading.value = true
  582. lazyRenderRecordFileList.value = []
  583. lazyRenderReportFileList.value = []
  584. reportIdMaps.value.clear()
  585. recordFileUrlMaps.value.clear() // 清理检验记录文件映射
  586. // 并行获取报告内容
  587. const reportFilePromise = getReportPDFPdf({ id: getTaskId.value })
  588. .then(async (fileBlob) => {
  589. const fileData = await processFileData(fileBlob, taskId, 0, '报告内容')
  590. reportIdMaps.value.set('record-' + taskId, { index: 0, url: fileData.url })
  591. lazyRenderReportFileList.value.push(fileData)
  592. activeRecordUrl.value = fileData.url
  593. return fileData.url
  594. })
  595. .catch((error) => {
  596. console.error('获取报告文件失败:', error)
  597. reportSourceLoading.value = false
  598. throw error
  599. })
  600. if (checkRecordList.value.length) recordSourceLoading.value = true
  601. // 并行获取检验记录文件
  602. const recordFilePromises = checkRecordList.value.map(async (report, index) => {
  603. try {
  604. const fileBlob = await getCheckRecordPdf({ itemId: report.value })
  605. const fileData = await processFileData(fileBlob, report.id, index, report.label)
  606. reportIdMaps.value.set('report-' + report.id, {
  607. index,
  608. url: fileData.url
  609. })
  610. return fileData
  611. } catch (error) {
  612. console.error(`获取检验记录文件失败 (${report.label}):`, error)
  613. return null
  614. }
  615. })
  616. // 等待所有检验记录文件加载完成
  617. const recordFileResults = await Promise.allSettled(recordFilePromises)
  618. // 过滤成功的结果并按索引排序
  619. const successfulRecords = recordFileResults
  620. .filter((result) => result.status === 'fulfilled' && result.value !== null)
  621. .map((result) => result.value)
  622. .sort((a, b) => a.index - b.index)
  623. // 设置检验记录文件列表
  624. lazyRenderRecordFileList.value = successfulRecords
  625. // 设置活动的记录URL和按钮高亮
  626. if (successfulRecords.length > 0) {
  627. activeReportUrl.value = successfulRecords[0].url
  628. activeButtonIndex.value = 0
  629. }
  630. // 等待右侧报告文件完成
  631. await reportFilePromise
  632. loading.value = false
  633. } catch (error) {
  634. console.error('初始化文件加载失败:', error)
  635. loading.value = false
  636. recordSourceLoading.value = false
  637. reportSourceLoading.value = false
  638. ElMessage.error('文件加载失败,请重试')
  639. }
  640. },
  641. {
  642. immediate: true
  643. }
  644. )
  645. onMounted(() => {
  646. wrapperContainerHeight.value = col24HalfRef.value?.$el.clientHeight - 42
  647. handleWindowResize()
  648. window.addEventListener('resize', handleWindowResize)
  649. })
  650. onUnmounted(() => {
  651. window.removeEventListener('resize', handleWindowResize)
  652. // 清理创建的URL对象
  653. lazyRenderRecordFileList.value.forEach(file => {
  654. if (file.url) URL.revokeObjectURL(file.url)
  655. })
  656. lazyRenderReportFileList.value.forEach(file => {
  657. if (file.url) URL.revokeObjectURL(file.url)
  658. })
  659. })
  660. </script>
  661. <style lang="scss" scoped>
  662. .task-detail-layout {
  663. display: flex;
  664. height: calc(100vh - 210px);
  665. margin: 0 !important;
  666. gap: 16px;
  667. flex-wrap: nowrap;
  668. .no-data {
  669. width: 100%;
  670. height: 100%;
  671. display: flex;
  672. justify-content: center;
  673. align-items: center;
  674. font-size: 16px;
  675. color: #999;
  676. background-color: #f5f5f5;
  677. }
  678. .el-col-12 {
  679. padding: 0 !important;
  680. max-width: 100% !important;
  681. overflow-y: auto;
  682. }
  683. .wrapperContainer {
  684. position: relative;
  685. overflow: hidden;
  686. }
  687. :deep(.v-content-wrap) {
  688. position: relative;
  689. width: 100%;
  690. height: 100%;
  691. background: #8e8e9d;
  692. .el-card__header {
  693. position: sticky;
  694. left: 0;
  695. top: 0;
  696. padding: 10px !important;
  697. background-color: #fff;
  698. }
  699. .el-card__body {
  700. position: relative;
  701. width: 100%;
  702. height: calc(100% - 62px);
  703. // margin: 10px;
  704. overflow-y: auto;
  705. }
  706. }
  707. }
  708. .btn-group {
  709. display: flex;
  710. justify-content: space-between;
  711. padding-bottom: 12px;
  712. max-width: 100%;
  713. overflow: hidden;
  714. // gap: 16px;
  715. .left {
  716. display: flex;
  717. flex-direction: column;
  718. gap: 10px;
  719. flex: 1;
  720. min-width: 0; // 重要:允许flex子项收缩
  721. padding-right: 20px;
  722. overflow: hidden;
  723. margin-top: 10px;
  724. cursor: pointer;
  725. > div {
  726. display: flex;
  727. align-items: center;
  728. gap: 8px;
  729. overflow-x: auto;
  730. overflow-y: hidden;
  731. padding-bottom: 4px; // 给滚动条留空间
  732. // 自定义滚动条样式(可选)
  733. &::-webkit-scrollbar {
  734. height: 6px;
  735. }
  736. &::-webkit-scrollbar-thumb {
  737. background-color: #dcdfe6;
  738. border-radius: 3px;
  739. &:hover {
  740. background-color: #c0c4cc;
  741. }
  742. }
  743. &::-webkit-scrollbar-track {
  744. background-color: #f5f7fa;
  745. border-radius: 3px;
  746. }
  747. .title {
  748. white-space: nowrap;
  749. flex-shrink: 0; // 标题不收缩
  750. font-weight: 500;
  751. margin-bottom: 0;
  752. }
  753. .el-button {
  754. flex-shrink: 0; // 按钮不收缩
  755. white-space: nowrap;
  756. }
  757. }
  758. .left-content {
  759. display: flex;
  760. align-items: center;
  761. gap: 8px;
  762. }
  763. }
  764. .right {
  765. display: flex;
  766. align-items: center;
  767. gap: 8px;
  768. flex-shrink: 0; // 右侧按钮组不收缩,始终可见
  769. .el-button {
  770. white-space: nowrap;
  771. }
  772. }
  773. }
  774. .file-viewer-container {
  775. margin-bottom: 20px;
  776. // border: 1px solid #e4e7ed;
  777. border-radius: 4px;
  778. overflow: hidden;
  779. background: #fff;
  780. }
  781. // 为PDF的每一页canvas添加间距
  782. .reportPDFViewer {
  783. :deep(canvas) {
  784. display: block !important;
  785. width: 100% !important;
  786. box-sizing: border-box !important;
  787. // position: relative;
  788. border-bottom: 20px solid #8e8e9d;
  789. overflow: hidden;
  790. // &::after {
  791. // position: absolute;
  792. // }
  793. &:last-child {
  794. margin-bottom: 0;
  795. }
  796. }
  797. }
  798. .image-viewer {
  799. display: flex;
  800. justify-content: center;
  801. align-items: center;
  802. padding: 20px;
  803. background: #f5f7fa;
  804. img {
  805. max-width: 100%;
  806. height: auto;
  807. border-radius: 4px;
  808. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  809. }
  810. }
  811. </style>