From 6eeb82e56306d645a290a0f5ba202efe3ef2385d Mon Sep 17 00:00:00 2001
From: liding <756868258@qq.com>
Date: 星期二, 27 十月 2026 17:49:09 +0800
Subject: [PATCH] feat(report): 新增PDF报告预览功能并重构报告下载流程

---
 src/permission.js                                                           |    6 
 src/views/business/materialOrderComponents/materialOrder/downFileDialog.vue |   99 ++----
 src/views/business/reportPreparation/index.vue                              |  183 +++++++------
 src/api/cnas/process/reportResults.js                                       |    3 
 src/views/CNAS/process/reportResults/index.vue                              |   21 +
 src/views/business/productOrder/index.vue                                   |   22 +
 src/api/business/insReport.js                                               |   13 
 src/views/business/inspectionTask/index.vue                                 |   14 +
 src/views/business/materialOrder/index.vue                                  |    4 
 src/utils/request.js                                                        |    2 
 tests/attachment-download.test.mjs                                          |    2 
 package.json                                                                |    1 
 src/components/ReportPreview/PdfViewer.vue                                  |  193 ++++++++++++++
 src/components/ReportPreview/index.vue                                      |  123 +++++++++
 vite.config.js                                                              |    3 
 src/utils/reportPreview.js                                                  |   40 +++
 16 files changed, 573 insertions(+), 156 deletions(-)

diff --git a/package.json b/package.json
index d678fa9..da5a5e5 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
     "mammoth": "^1.13.0",
     "nprogress": "0.2.0",
     "pako": "^3.0.2",
+    "pdfjs-dist": "5.7.284",
     "pinia": "3.0.4",
     "print-js": "^1.6.0",
     "qrcode.vue": "^3.11.0",
diff --git a/src/api/business/insReport.js b/src/api/business/insReport.js
index d704bd2..96b869b 100644
--- a/src/api/business/insReport.js
+++ b/src/api/business/insReport.js
@@ -27,12 +27,21 @@
 }
 
 // 鍗曚唤鎶ュ憡涓嬭浇
-export function downloadReport(id, reportType) {
+export function downloadReport(id, reportType, signal) {
   return request({
     url: '/insReport/download',
     method: 'get',
     params: { id, reportType },
+    signal,
     responseType: 'blob'
+  })
+}
+
+// 鎸� Word 鐗堝紡鐢熸垚鍒嗛〉 PDF锛屾部鐢ㄦ姤鍛婁笅杞界殑鏁版嵁鑼冨洿銆�
+export function previewReportPdf(id, reportType, signal) {
+  return request({
+    url: '/insReport/preview', method: 'get',
+    params: { id, reportType }, signal, responseType: 'blob'
   })
 }
 
@@ -78,7 +87,7 @@
     data: { id }
   })
 }
-// 鎶ュ憡閫�鍥炲埌妫�楠屼换鍔�
+// 鎶ュ憡閫�鍥炲埌妫�楠屼换鍔℃垨鎶ュ憡瀹℃牳
 export function sendBackTask(query) {
   return request({
     url: '/insReport/sendBackTask',
diff --git a/src/api/cnas/process/reportResults.js b/src/api/cnas/process/reportResults.js
index 4902f6d..64ceb98 100644
--- a/src/api/cnas/process/reportResults.js
+++ b/src/api/cnas/process/reportResults.js
@@ -48,11 +48,12 @@
 }
 
 // 涓嬭浇鎵瑰噯鍚庣敓鎴愮殑鏈夌珷鎶ュ憡
-export function downloadStampedReport(id, format) {
+export function downloadStampedReport(id, format, signal) {
   return request({
     url: "/processReport/downloadStampedReport",
     method: "get",
     params: { id, format },
+    signal,
     responseType: "blob",
   });
 }
diff --git a/src/components/ReportPreview/PdfViewer.vue b/src/components/ReportPreview/PdfViewer.vue
new file mode 100644
index 0000000..4c64a9d
--- /dev/null
+++ b/src/components/ReportPreview/PdfViewer.vue
@@ -0,0 +1,193 @@
+<template>
+  <div class="report-pdf-viewer">
+    <div class="pdf-toolbar">
+      <el-button size="small" :disabled="currentPage <= 1" @click="goToPage(currentPage - 1)">涓婁竴椤�</el-button>
+      <span>{{ currentPage }} / {{ pages.length || '鈥�' }}</span>
+      <el-button size="small" :disabled="currentPage >= pages.length" @click="goToPage(currentPage + 1)">涓嬩竴椤�</el-button>
+      <span class="toolbar-spacer" />
+      <el-button size="small" :disabled="zoom <= 0.5" @click="zoom = Math.max(0.5, zoom - 0.25)">缂╁皬</el-button>
+      <el-button size="small" @click="zoom = 1">{{ Math.round(zoom * 100) }}%</el-button>
+      <el-button size="small" :disabled="zoom >= 2" @click="zoom = Math.min(2, zoom + 0.25)">鏀惧ぇ</el-button>
+    </div>
+    <div ref="viewport" v-loading="loading" element-loading-text="姝e湪鍔犺浇鎶ュ憡鈥�" class="pdf-viewport" @scroll="updatePage">
+      <el-result v-if="error" icon="warning" title="鎶ュ憡棰勮澶辫触" :sub-title="error">
+        <template #extra><el-button @click="openDocument">閲嶈瘯</el-button></template>
+      </el-result>
+      <div v-for="page in pages" :key="page.number" class="pdf-paper" :data-page-number="page.number"
+           :style="{ width: pageWidth(page) + 'px', height: pageWidth(page) * page.height / page.width + 'px' }">
+        <canvas :ref="element => setCanvas(element, page.number)" :data-page-number="page.number"
+                role="img" :aria-label="'鎶ュ憡绗� ' + page.number + ' 椤�'" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, nextTick, watch, onMounted, onBeforeUnmount } from 'vue'
+import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?worker&url'
+import { loadReportFile, validateReportPreview } from '@/utils/reportPreview'
+
+const props = defineProps({ source: { default: null }, load: Function })
+const emit = defineEmits(['ready', 'error'])
+const viewport = ref(null)
+const pages = ref([])
+const loading = ref(false)
+const error = ref('')
+const zoom = ref(1)
+const availableWidth = ref(800)
+const currentPage = ref(1)
+const canvases = new Map()
+const rendering = new Map()
+const completed = new Set()
+let generation = 0, renderGeneration = 0, pdf = null, task = null, controller = null
+let observer = null, resizeObserver = null, resizeTimer = null
+
+// PDF 鐨� 72pt 杞负灞忓箷 96px锛欰4 绾搁粯璁ょ害 794px锛岀獎绐楀彛鍙缉灏忥紝涓嶈嚜鍔ㄦ斁澶с��
+function pageWidth(page) { return Math.min(page.width * zoom.value, Math.max(1, availableWidth.value - 32)) }
+function setCanvas(element, number) { if (element) canvases.set(number, element); else canvases.delete(number) }
+function cancelRendering() {
+  renderGeneration++
+  observer?.disconnect()
+  for (const item of rendering.values()) item.cancel()
+  rendering.clear()
+  completed.clear()
+}
+function dispose() {
+  generation++
+  controller?.abort()
+  cancelRendering()
+  task?.destroy().catch(() => {})
+  task = null
+  pdf = null
+  for (const canvas of canvases.values()) { canvas.width = 0; canvas.height = 0 }
+  canvases.clear()
+}
+async function renderPage(number, documentGeneration, viewGeneration) {
+  if (!pdf || completed.has(number) || rendering.has(number)) return
+  completed.add(number)
+  try {
+    const page = await pdf.getPage(number)
+    if (documentGeneration !== generation || viewGeneration !== renderGeneration) return
+    const canvas = canvases.get(number)
+    if (!canvas) return
+    delete canvas.dataset.rendered
+    const spec = pages.value[number - 1]
+    const scale = pageWidth(spec) / spec.width * 96 / 72
+    const density = Math.min(window.devicePixelRatio || 1, 2)
+    const size = page.getViewport({ scale })
+    canvas.width = Math.ceil(size.width * density)
+    canvas.height = Math.ceil(size.height * density)
+    const render = page.render({ canvasContext: canvas.getContext('2d'), viewport: size,
+      transform: [density, 0, 0, density, 0, 0], background: '#ffffff' })
+    rendering.set(number, render)
+    await render.promise
+    if (documentGeneration === generation && viewGeneration === renderGeneration) {
+      canvas.dataset.rendered = 'true'
+      if (number === 1) { loading.value = false; emit('ready') }
+    }
+  } catch (cause) {
+    if (documentGeneration !== generation || viewGeneration !== renderGeneration) return
+    if (cause.name !== 'RenderingCancelledException') {
+      error.value = '鎶ュ憡椤甸潰鍔犺浇澶辫触锛岃閲嶈瘯'
+      loading.value = false
+      emit('error', error.value)
+    }
+  } finally {
+    if (documentGeneration === generation && viewGeneration === renderGeneration) rendering.delete(number)
+  }
+}
+async function renderVisiblePages() {
+  if (!pdf || !viewport.value) return
+  const cancelled = [...rendering.values()].map(item => item.promise.catch(() => {}))
+  cancelRendering()
+  const documentGeneration = generation, viewGeneration = renderGeneration
+  await Promise.all(cancelled)
+  await nextTick()
+  if (documentGeneration !== generation || viewGeneration !== renderGeneration) return
+  observer = new IntersectionObserver(entries => {
+    for (const entry of entries) if (entry.isIntersecting) {
+      renderPage(Number(entry.target.dataset.pageNumber), documentGeneration, viewGeneration)
+    }
+  }, { root: viewport.value, rootMargin: '300px' })
+  for (const canvas of canvases.values()) observer.observe(canvas)
+  if (loading.value) renderPage(1, documentGeneration, viewGeneration)
+}
+async function openDocument() {
+  dispose()
+  const version = generation
+  controller = new AbortController()
+  pages.value = []
+  currentPage.value = 1
+  zoom.value = 1
+  error.value = ''
+  loading.value = true
+  try {
+    const content = props.load ? await props.load(controller.signal)
+      : props.source instanceof Blob ? props.source : await loadReportFile(props.source, controller.signal)
+    const blob = await validateReportPreview(content, 'pdf')
+    const library = await import('pdfjs-dist/build/pdf.mjs')
+    const bytes = new Uint8Array(await blob.arrayBuffer())
+    if (version !== generation) return
+    library.GlobalWorkerOptions.workerSrc = workerUrl
+    task = library.getDocument({ data: bytes, useWasm: false, useSystemFonts: true, isEvalSupported: false })
+    const document = await task.promise
+    if (version !== generation) return
+    pdf = document
+    const sizes = []
+    for (let number = 1; number <= document.numPages; number++) {
+      const page = await document.getPage(number)
+      if (version !== generation) return
+      const size = page.getViewport({ scale: 96 / 72 })
+      sizes.push({ number, width: size.width, height: size.height })
+    }
+    pages.value = sizes
+    await nextTick()
+    viewport.value.scrollTop = 0
+    await renderVisiblePages()
+  } catch (cause) {
+    if (version !== generation) return
+    error.value = cause.message || '鎶ュ憡鍔犺浇澶辫触锛岃閲嶈瘯'
+    loading.value = false
+    emit('error', error.value)
+  }
+}
+function goToPage(number) {
+  const target = viewport.value?.querySelector(`[data-page-number="${number}"]`)
+  if (target) { viewport.value.scrollTop = target.offsetTop - 16; currentPage.value = number }
+}
+function updatePage() {
+  const top = viewport.value.getBoundingClientRect().top
+  for (const paper of viewport.value.querySelectorAll('.pdf-paper')) {
+    if (paper.getBoundingClientRect().bottom > top + 32) {
+      currentPage.value = Number(paper.dataset.pageNumber)
+      break
+    }
+  }
+}
+watch(() => [props.source, props.load], () => { if (viewport.value) openDocument() })
+watch(zoom, renderVisiblePages)
+onMounted(() => {
+  availableWidth.value = viewport.value.clientWidth
+  resizeObserver = new ResizeObserver(() => {
+    const width = viewport.value?.clientWidth
+    if (!width || width === availableWidth.value) return
+    const previousWidths = pages.value.map(pageWidth)
+    availableWidth.value = width
+    if (pages.value.every((page, index) => pageWidth(page) === previousWidths[index])) return
+    clearTimeout(resizeTimer)
+    resizeTimer = setTimeout(renderVisiblePages, 100)
+  })
+  resizeObserver.observe(viewport.value)
+  openDocument()
+})
+onBeforeUnmount(() => { clearTimeout(resizeTimer); resizeObserver?.disconnect(); dispose() })
+</script>
+
+<style scoped>
+.report-pdf-viewer { display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0; background: #fff; color: #303133; color-scheme: light; }
+.pdf-toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; flex: 0 0 auto; padding: 8px 12px; background: #fff; border-bottom: 1px solid #ebeef5; }
+.toolbar-spacer { flex: 1; }
+.pdf-viewport { flex: 1; min-height: 0; overflow: auto; position: relative; background: #fff; }
+.pdf-paper { position: relative; margin: 16px auto; background: #fff; box-shadow: 0 0 0 1px #e4e7ed; }
+.pdf-paper canvas { display: block; width: 100%; height: 100%; background: #fff; }
+</style>
diff --git a/src/components/ReportPreview/index.vue b/src/components/ReportPreview/index.vue
new file mode 100644
index 0000000..1559a9a
--- /dev/null
+++ b/src/components/ReportPreview/index.vue
@@ -0,0 +1,123 @@
+<template>
+  <el-dialog v-model="visible" title="鎶ュ憡棰勮" width="90vw" :fullscreen="fullscreen"
+             append-to-body destroy-on-close :close-on-click-modal="false" @close="reset">
+    <div class="report-preview-toolbar">
+      <el-radio-group v-if="variants.length > 1" :model-value="selectedKey" @change="selectReport">
+        <el-radio-button v-for="item in variants" :key="item.key" :value="item.key">{{ item.label }}</el-radio-button>
+      </el-radio-group>
+      <span class="report-preview-name">{{ current?.filename }}</span>
+      <el-button @click="fullscreen = !fullscreen">{{ fullscreen ? '閫�鍑哄叏灞�' : '鍏ㄥ睆鏌ョ湅' }}</el-button>
+      <el-button type="primary" :disabled="!blob || loading" :loading="downloading" @click="download">涓嬭浇褰撳墠鎶ュ憡</el-button>
+    </div>
+    <div v-loading="loading" element-loading-text="姝e湪鍔犺浇鎶ュ憡鈥�" class="report-preview-content"
+         :class="{ 'is-fullscreen': fullscreen }">
+      <el-result v-if="error" icon="warning" title="鎶ュ憡棰勮澶辫触" :sub-title="error">
+        <template #extra><el-button @click="selectReport(selectedKey, true)">閲嶈瘯</el-button></template>
+      </el-result>
+      <pdf-viewer v-else-if="blob" :source="blob" />
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { validateReportPreview } from '@/utils/reportPreview'
+import PdfViewer from './PdfViewer.vue'
+
+export default {
+  name: 'ReportPreview',
+  components: { PdfViewer },
+  data() {
+    return {
+      visible: false, fullscreen: false, variants: [], selectedKey: null,
+      loading: false, downloading: false, error: '', blob: null, objectUrl: '',
+      generation: 0, controller: null, downloadController: null, cache: new Map()
+    }
+  },
+  computed: {
+    current() { return this.variants.find(item => item.key === this.selectedKey) }
+  },
+  beforeUnmount() { this.reset() },
+  methods: {
+    open(variants, selectedKey = variants[0].key) {
+      this.reset()
+      this.variants = variants
+      this.fullscreen = false
+      this.visible = true
+      return this.selectReport(selectedKey)
+    },
+    releaseUrl() {
+      if (this.objectUrl) URL.revokeObjectURL(this.objectUrl)
+      this.objectUrl = ''
+      this.blob = null
+    },
+    reset() {
+      this.generation++
+      this.controller?.abort()
+      this.downloadController?.abort()
+      this.controller = null
+      this.downloadController = null
+      this.releaseUrl()
+      this.cache.clear()
+      this.loading = false
+      this.downloading = false
+      this.error = ''
+    },
+    async selectReport(key, retry = false) {
+      this.controller?.abort()
+      this.downloadController?.abort()
+      this.downloading = false
+      const generation = ++this.generation
+      this.selectedKey = key
+      this.releaseUrl()
+      this.error = ''
+      this.loading = true
+      const controller = new AbortController()
+      this.controller = controller
+      const item = this.current
+      try {
+        let blob = retry ? null : this.cache.get(key)
+        if (!blob) blob = await validateReportPreview(await item.load(controller.signal), 'pdf')
+        // 鍏抽棴寮圭獥銆佸揩閫熷垏鎹㈡姤鍛婃垨鎵撳紑鍙︿竴琛屽悗锛屾棫璇锋眰涓嶅緱瑕嗙洊褰撳墠鍐呭銆�
+        if (generation !== this.generation) return
+        this.cache.set(key, blob)
+        this.blob = blob
+        this.objectUrl = URL.createObjectURL(blob)
+      } catch (error) {
+        if (generation !== this.generation) return
+        const status = error.response?.status
+        this.error = status === 404 ? '鎶ュ憡鏂囦欢涓嶅瓨鍦紝璇疯仈绯荤鐞嗗憳'
+          : status === 401 || status === 403 ? '鐧诲綍宸茶繃鏈熸垨娌℃湁鎶ュ憡鏌ョ湅鏉冮檺'
+          : error.message || '鎶ュ憡鍔犺浇澶辫触锛岃绋嶅悗閲嶈瘯'
+      } finally {
+        if (generation === this.generation) this.loading = false
+      }
+    },
+    async download() {
+      if (!this.blob || this.loading || this.downloading) return
+      const generation = this.generation
+      const item = this.current
+      this.downloading = true
+      const controller = new AbortController()
+      this.downloadController = controller
+      try {
+        // 棰勮缁熶竴鏄剧ず PDF锛涙姤鍛婄紪鍒朵粛涓嬭浇鍘� Word锛屽叾浠栧叆鍙e鐢ㄥ凡鍔犺浇鐨� PDF銆�
+        const blob = item.download
+          ? await validateReportPreview(await item.download(controller.signal), item.downloadFormat || 'docx')
+          : this.blob
+        if (generation === this.generation) this.$download.saveAs(blob, item.filename)
+      } catch (error) {
+        if (generation === this.generation) this.$message.error(error.message || '鎶ュ憡涓嬭浇澶辫触')
+      } finally {
+        if (generation === this.generation) this.downloading = false
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.report-preview-toolbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
+.report-preview-name { flex: 1; min-width: 120px; overflow-wrap: anywhere; color: var(--el-text-color-secondary); }
+.report-preview-content { height: 75vh; min-height: 300px; overflow: auto; background: #fff; }
+.report-preview-content.is-fullscreen { height: calc(100vh - 160px); }
+</style>
diff --git a/src/permission.js b/src/permission.js
index c66a9d3..983a66e 100644
--- a/src/permission.js
+++ b/src/permission.js
@@ -75,3 +75,9 @@
 router.afterEach(() => {
   NProgress.done()
 })
+
+router.onError(error => {
+  NProgress.done()
+  console.error('[router] 椤甸潰鍔犺浇澶辫触锛�', error)
+  ElMessage.error('椤甸潰鍔犺浇澶辫触锛岃鍒锋柊椤甸潰鍚庨噸璇�')
+})
diff --git a/src/utils/reportPreview.js b/src/utils/reportPreview.js
new file mode 100644
index 0000000..90eef92
--- /dev/null
+++ b/src/utils/reportPreview.js
@@ -0,0 +1,40 @@
+import axios from 'axios'
+import { getToken } from '@/utils/auth'
+import { getJavaApi } from '@/utils/java-api'
+import { resolveAttachmentUrl, validateAttachmentBlob } from '@/utils/attachment'
+
+export function reportFilename(value, extension) {
+  const name = String(value || '鎶ュ憡').replace(/[\\/:*?"<>|\x00-\x1f]/g, '-').trim() || '鎶ュ憡'
+  return name.replace(/\.(docx|pdf)$/i, '') + '.' + extension
+}
+
+// 鍙浛鎹㈡湯灏炬墿灞曞悕锛屽吋瀹圭洰褰曞悕銆佹姤鍛婄紪鍙蜂腑鍚皬鏁扮偣鐨勫巻鍙叉枃浠躲��
+export function reportPdfPath(path) {
+  return path.replace(/\.docx?$/i, '.pdf')
+}
+
+// 涓嬪崟椤垫部鐢ㄥ凡鏈夋姤鍛婃枃浠跺叆鍙o紝閴存潈淇℃伅鍙彂閫佺粰鏈郴缁熴��
+export async function loadReportFile(path, signal) {
+  const url = resolveAttachmentUrl(path, 2)
+  const backend = new URL(getJavaApi(), window.location.origin)
+  const target = new URL(url, window.location.origin)
+  const isBackend = target.origin === backend.origin
+    && target.pathname.startsWith(backend.pathname.replace(/\/$/, '') + '/')
+  const { data } = await axios.get(url, {
+    responseType: 'blob', signal,
+    headers: isBackend && getToken() ? { Authorization: 'Bearer ' + getToken() } : {}
+  })
+  return data
+}
+
+export async function validateReportPreview(blob, format) {
+  if (!(blob instanceof Blob) || !blob.size) throw new Error('鎶ュ憡鍐呭涓虹┖锛岃閲嶈瘯鎴栬仈绯荤鐞嗗憳')
+  await validateAttachmentBlob(blob, '鎶ュ憡.' + format)
+  const header = new Uint8Array(await blob.slice(0, 5).arrayBuffer())
+  const isPdf = String.fromCharCode(...header) === '%PDF-'
+  const isDocx = header[0] === 0x50 && header[1] === 0x4b
+  if (format === 'pdf' ? !isPdf : !isDocx) throw new Error('鏈幏鍙栧埌鏈夋晥鐨勬姤鍛婃枃浠讹紝璇烽噸璇曟垨鑱旂郴绠$悊鍛�')
+  // 闈欐�佹枃浠舵湇鍔″櫒鍙兘杩斿洖 octet-stream锛孭DF 闃呰鍣ㄩ渶瑕佹槑纭殑 MIME 绫诲瀷銆�
+  return new Blob([blob], { type: format === 'pdf' ? 'application/pdf'
+    : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' })
+}
diff --git a/src/utils/request.js b/src/utils/request.js
index f93bf2e..6a89da6 100644
--- a/src/utils/request.js
+++ b/src/utils/request.js
@@ -115,6 +115,8 @@
     }
   },
   error => {
+    // 棰勮绐楀彛鍏抽棴鎴栧垏鎹㈡姤鍛婁細涓诲姩鍙栨秷璇诲彇锛屼笉鎻愮ず涓烘帴鍙e紓甯搞��
+    if (axios.isCancel(error)) return Promise.reject(error)
     console.log('err' + error)
     let { message } = error
     if (message == "Network Error") {
diff --git a/src/views/CNAS/process/reportResults/index.vue b/src/views/CNAS/process/reportResults/index.vue
index b594601..91094c0 100644
--- a/src/views/CNAS/process/reportResults/index.vue
+++ b/src/views/CNAS/process/reportResults/index.vue
@@ -114,14 +114,18 @@
       </el-form>
       <template #footer><span  class="dialog-footer">
         <el-button @click="downloadDialogVisible = false">鍙� 娑�</el-button>
+        <el-button :disabled="!downloadRow" @click="previewReport(downloadRow)">棰勮鏈夌珷鎶ュ憡</el-button>
         <el-button type="primary" :loading="downloadingReportId !== null"
                    @click="downloadSelectedReport">涓� 杞�</el-button>
       </span></template>
     </el-dialog>
+    <report-preview ref="reportPreview" />
   </div>
 </template>
 
 <script>
+import ReportPreview from '@/components/ReportPreview/index.vue'
+import { reportFilename } from '@/utils/reportPreview'
 import limsTable from "@/components/Table/lims-table.vue";
 import {
   exportProcessReport,
@@ -137,7 +141,8 @@
 export default {
   name: 'ReportResults',
   components: {
-    limsTable
+    limsTable,
+    ReportPreview
   },
   data() {
     return {
@@ -171,8 +176,13 @@
           dataType: "action",
           fixed: "right",
           label: "鎿嶄綔",
-          width: "180px",
+          width: "230px",
           operation: [
+            {
+              name: "棰勮",
+              type: "text",
+              clickFun: row => this.previewReport(row),
+            },
             {
               name: "涓嬭浇",
               type: "text",
@@ -305,6 +315,13 @@
       this.addInfo = { ...row }
       this.addDialogVisible = true;
     },
+    previewReport(row) {
+      this.$refs.reportPreview.open([{
+        key: 'stamped', label: '鏈夌珷鎶ュ憡', format: 'pdf',
+        filename: reportFilename(row.insReportCode || '鏈夌珷鎶ュ憡', 'pdf'),
+        load: signal => downloadStampedReport(row.id, 'pdf', signal)
+      }])
+    },
     handleDownload(row) {
       this.downloadRow = row
       this.selectedDownloadFormat = 'pdf'
diff --git a/src/views/business/inspectionTask/index.vue b/src/views/business/inspectionTask/index.vue
index 9131551..28e4def 100644
--- a/src/views/business/inspectionTask/index.vue
+++ b/src/views/business/inspectionTask/index.vue
@@ -417,7 +417,7 @@
           },
         },
         {
-          label: "鐘舵��",
+          label: "妫�楠岀姸鎬�",
           prop: "insState",
           dataType: "tag",
           formatData: (params) => {
@@ -429,6 +429,18 @@
             return item ? item.type : "info";
           },
         },
+        {
+          label: "鎶ュ憡閫�鍥炵姸鎬�",
+          prop: "reportReturnSource",
+          width: "140px",
+          dataType: "tag",
+          formatData: (source) => {
+            if (source === "APPROVED") return "鎶ュ憡鎵瑰噯閫�鍥�";
+            if (source === "REPORT_REVIEW") return "鎶ュ憡瀹℃牳閫�鍥�";
+            return null;
+          },
+          formatType: () => "warning",
+        },
         { label: "妫�楠屼汉", prop: "userName" },
         { label: "澶嶆牳浜�", prop: "checkName" },
         { label: "绾﹀畾鏃堕棿", prop: "appointed" },
diff --git a/src/views/business/materialOrder/index.vue b/src/views/business/materialOrder/index.vue
index 3695671..017a0ba 100644
--- a/src/views/business/materialOrder/index.vue
+++ b/src/views/business/materialOrder/index.vue
@@ -552,7 +552,7 @@
               }
             },
             {
-              name: '鎶ュ憡涓嬭浇',
+              name: '鎶ュ憡棰勮/涓嬭浇',
               type: 'text',
               clickFun: (row) => {
                 this.download(row);
@@ -887,7 +887,7 @@
               },
             },
             {
-              name: '鎶ュ憡涓嬭浇',
+              name: '鎶ュ憡棰勮/涓嬭浇',
               type: 'text',
               clickFun: (row) => {
                 this.download(row);
diff --git a/src/views/business/materialOrderComponents/materialOrder/downFileDialog.vue b/src/views/business/materialOrderComponents/materialOrder/downFileDialog.vue
index 37bc1d8..76f465c 100644
--- a/src/views/business/materialOrderComponents/materialOrder/downFileDialog.vue
+++ b/src/views/business/materialOrderComponents/materialOrder/downFileDialog.vue
@@ -1,85 +1,58 @@
 <template>
   <div>
-    <el-dialog title="鎶ュ憡鏌ョ湅涓嬭浇" v-model="isShow" width="500px" @closed="$emit('closeDownFileDialog')">
-      <div style="margin-bottom: 18px">
-        <span style="font-size: 16px;">杩涘巶妫�楠屾姤鍛婁笅杞�</span>
-        <lims-icon name="el-icon-paperclip" style="color:var(--el-color-primary);margin-left: 6px"></lims-icon>
-        <el-link :underline="false" style="vertical-align: bottom;font-size: 16px;"
-                 :disabled="!downLoadInfo.enterUrlS && !downLoadInfo.enterUrl"
-                 type="primary" @click="downLoad0">鏌ョ湅</el-link>
-      </div>
-      <div>
-        <span  style="font-size: 16px;">瀛e害妫�楠屾姤鍛婁笅杞�</span>
-        <lims-icon name="el-icon-paperclip" style="color:var(--el-color-primary);margin-left: 6px"></lims-icon>
-        <el-link :underline="false" style="vertical-align: bottom;font-size: 16px;"
-                 :disabled="!downLoadInfo.quarterUrlS && !downLoadInfo.quarterUrl"
-                 type="primary" @click="downLoad1">鏌ョ湅</el-link>
+    <el-dialog title="鎶ュ憡棰勮/涓嬭浇" v-model="isShow" width="520px" @closed="$emit('closeDownFileDialog')">
+      <div v-for="item in reports" :key="item.key" class="report-choice">
+        <span>{{ item.label }}</span>
+        <el-button link type="primary" :disabled="!canView(item)" @click="preview(item)">棰勮</el-button>
+        <el-button link type="primary" :disabled="!canView(item)" @click="download(item)">涓嬭浇</el-button>
+        <span v-if="!canView(item)" class="report-unavailable">鏈敓鎴愭垨灏氭湭鎵瑰噯</span>
       </div>
     </el-dialog>
+    <report-preview ref="reportPreview" />
   </div>
 </template>
 
 <script>
+import ReportPreview from '@/components/ReportPreview/index.vue'
+import { loadReportFile, reportFilename, reportPdfPath } from '@/utils/reportPreview'
+
 export default {
-  name: "downFileDialog",
-  // import 寮曞叆鐨勭粍浠堕渶瑕佹敞鍏ュ埌瀵硅薄涓墠鑳戒娇鐢�
-  components: {},
+  name: 'downFileDialog',
+  components: { ReportPreview },
   props: {
-    downFileDialogVisible: {
-      type: Boolean,
-      default: () => false
-    },
-    downLoadInfo: {
-      type: Object,
-      default: () => {}
-    },
+    downFileDialogVisible: { type: Boolean, default: false },
+    downLoadInfo: { type: Object, default: () => ({}) }
   },
   data() {
-    // 杩欓噷瀛樻斁鏁版嵁
-    return {
-      isShow: this.downFileDialogVisible,
+    return { isShow: this.downFileDialogVisible }
+  },
+  computed: {
+    reports() {
+      const info = this.downLoadInfo
+      return [
+        { key: 'enter', label: '杩涘巶妫�楠屾姤鍛�', url: info.enterUrlS || info.enterUrl, status: info.enterRatifyStatus },
+        { key: 'quarter', label: '瀛e害妫�楠屾姤鍛�', url: info.quarterUrlS || info.quarterUrl, status: info.quarterRatifyStatus }
+      ]
     }
   },
-  // 鏂规硶闆嗗悎
   methods: {
-    // 杩涘巶妫�楠屾姤鍛婁笅杞�
-    downLoad0 () {
-      if(this.downLoadInfo.enterRatifyStatus !== 1){
-        this.$message.error("鎶ュ憡鏈鎵规垨瀹℃壒鏈�氳繃锛屾棤娉曚笅杞�")
-        return
-      }
-      let url = this.downLoadInfo.enterUrlS ? this.downLoadInfo.enterUrlS : this.downLoadInfo.enterUrl
-      if(url){
-        url = url.split('.')[0]+'.pdf'
-        const link = document.createElement('a');
-        link.href = this.javaApi + url;
-        link.target = '_blank';
-        document.body.appendChild(link);
-        link.click();
-      }
+    canView(item) { return !!item.url && Number(item.status) === 1 },
+    preview(item) {
+      if (!this.canView(item)) return
+      this.$refs.reportPreview.open([{
+        key: item.key, label: item.label, format: 'pdf',
+        filename: reportFilename(item.label, 'pdf'),
+        load: signal => loadReportFile(reportPdfPath(item.url), signal)
+      }])
     },
-    // 瀛e害妫�楠屾姤鍛婁笅杞�
-    downLoad1 () {
-      if(this.downLoadInfo.quarterRatifyStatus !== 1){
-        this.$message.error("鎶ュ憡鏈鎵规垨瀹℃壒鏈�氳繃锛屾棤娉曚笅杞�")
-        return
-      }
-      let url = this.downLoadInfo.quarterUrlS ? this.downLoadInfo.quarterUrlS : this.downLoadInfo.quarterUrl
-      if(url){
-        url = url.split('.')[0]+'.pdf'
-        const link = document.createElement('a');
-        link.href = this.javaApi + url;
-        link.target = '_blank';
-        document.body.appendChild(link);
-        link.click();
-      }
+    download(item) {
+      if (this.canView(item)) this.$download.url(reportPdfPath(item.url), item.label + '.pdf')
     }
-  },
+  }
 }
 </script>
 
 <style scoped>
-:deep(.el-dialog) {
-  height: 160px;
-}
+.report-choice { display: flex; align-items: center; gap: 12px; padding: 12px 0; flex-wrap: wrap; }
+.report-unavailable { color: var(--el-text-color-secondary); font-size: 12px; }
 </style>
diff --git a/src/views/business/productOrder/index.vue b/src/views/business/productOrder/index.vue
index 49965e2..8bdd92c 100644
--- a/src/views/business/productOrder/index.vue
+++ b/src/views/business/productOrder/index.vue
@@ -265,10 +265,13 @@
         <el-button :loading="submitListLoad" type="primary" @click="submitList">纭� 瀹�</el-button>
       </span></template>
     </el-dialog>
+    <report-preview ref="reportPreview" />
   </div>
 </template>
 
 <script>
+import ReportPreview from '@/components/ReportPreview/index.vue'
+import { loadReportFile, reportFilename, reportPdfPath } from '@/utils/reportPreview'
 // vue-qr 鍙敮鎸� Vue2锛屾崲鎴愬悓 props 鐨勬湰鍦扮粍浠讹紙鍐呴儴鐢� Vue3 鐨� qrcode.vue锛�
 import vueQr from '@/components/QrCode'
 import PrintJS from 'print-js'
@@ -290,6 +293,7 @@
   components: {
     limsTable,
     PrintDialog,
+    ReportPreview,
     vueQr
   },
   data() {
@@ -373,6 +377,13 @@
               showHide: (row) => {
                 return this.tabIndex === 1 || this.tabIndex === 2 || this.tabIndex === 3 || this.tabIndex === 6 || this.tabIndex === 7
               }
+            },
+            {
+              name: '鎶ュ憡棰勮',
+              type: 'text',
+              clickFun: (row) => this.previewReport(row),
+              disabled: (row) => !this.canDownloadReport(row),
+              showHide: () => [2, 3, 7].includes(this.tabIndex)
             },
             {
               name: '鎶ュ憡涓嬭浇',
@@ -882,12 +893,21 @@
     canDownloadReport(row) {
       return !!(row && row.reportId && (row.urlS || row.url))
     },
+    previewReport(row) {
+      if (!this.canDownloadReport(row)) return
+      const path = reportPdfPath(row.urlS || row.url)
+      this.$refs.reportPreview.open([{
+        key: 'approved', label: '鏈夌珷鎶ュ憡', format: 'pdf',
+        filename: reportFilename(row.reportCode || row.entrustCode || row.code, 'pdf'),
+        load: signal => loadReportFile(path, signal)
+      }])
+    },
     download(row) {
       if (!this.canDownloadReport(row)) {
         return
       }
       let url = row.urlS ? row.urlS : row.url;
-      url = url.split('.')[0] + '.pdf'
+      url = reportPdfPath(url)
       const link = document.createElement('a');
       link.href = this.javaApi + url;
       link.target = '_blank';
diff --git a/src/views/business/reportPreparation/index.vue b/src/views/business/reportPreparation/index.vue
index 2cb25ad..09de219 100644
--- a/src/views/business/reportPreparation/index.vue
+++ b/src/views/business/reportPreparation/index.vue
@@ -48,7 +48,8 @@
                   :isSelection="true" :handleSelectionChange="handleChange" :height="'calc(100vh - 290px)'"
                   @pagination="pagination" key="valueTableData">
         <template #action="scope"><div>
-          <!--          <el-button size="small" type="text" @click="viewIssued(scope.row)">鏌ョ湅鎶ュ憡</el-button>-->
+          <el-button link type="primary" size="small" :disabled="!scope.row.url && !scope.row.urlS"
+                     @click="previewReport(scope.row)">棰勮</el-button>
           <el-upload ref='upload1' style="display: inline;margin: 0 6px" :action="fileAction1 + '?id=' + scope.row.id"
                      :auto-upload="true" :before-upload="fileBeforeUpload1" :headers="uploadHeader" :on-error="onError1"
                      :on-success="handleSuccessUp1" :show-file-list="false" accept='.doc,.docx'>
@@ -59,7 +60,8 @@
           <el-button v-if="scope.row.urlS && !isBrowserEditedReportUrl(scope.row.urlS)" link type="primary" size="small"
                      :disabled="scope.row.state != 0 || nickName !== scope.row.writeUserName || !scope.row.url"
                      @click="handleRestore(scope.row)">杩樺師绯荤粺鎶ュ憡</el-button>
-          <el-button link type="primary" size="small" @click="sendBackTask(scope.row)">閫�鍥炰换鍔�</el-button>
+          <el-button v-if="scope.row.state == 1" link type="primary" size="small"
+                     @click="openSendBack(scope.row)">閫�鍥炰换鍔�</el-button>
           <el-button link type="primary" size="small" :disabled="scope.row.state != 0 || nickName !== scope.row.writeUserName"
                      @click="handle(scope.row)">鎻愪氦
           </el-button>
@@ -68,7 +70,7 @@
                      @click="handleIssued(scope.row)">瀹℃牳
           </el-button>
           <el-button link type="primary" size="small"
-                     :disabled="scope.row.state == null || scope.row.state == 0 || scope.row.isExamine == 0 || scope.row.isExamine == null || scope.row.isRatify == 1 || nickName !== scope.row.ratifyUser"
+                     :disabled="scope.row.state == null || scope.row.state == 0 || scope.row.isExamine != 1 || scope.row.isRatify == 1 || nickName !== scope.row.ratifyUser"
                      @click="handleApprove(scope.row)">鎵瑰噯
           </el-button>
           <el-button style="margin-left: 10px" link type="primary" size="small"
@@ -84,6 +86,25 @@
         </div></template>
       </lims-table>
     </div>
+    <el-dialog v-model="sendBackVisible" title="閫�鍥炰换鍔�" width="460px" :close-on-click-modal="false">
+      <el-form label-width="90px">
+        <el-form-item label="閫�鍥炰綅缃�">
+          <el-radio-group v-if="sendBackRow && sendBackRow.isRatify == 1" v-model="sendBackForm.target">
+            <el-radio value="INSPECTION_TASK">閫�鍥炲埌妫�楠屼换鍔�</el-radio>
+            <el-radio value="REPORT_REVIEW">閫�鍥炲埌鎶ュ憡瀹℃牳</el-radio>
+          </el-radio-group>
+          <span v-else>閫�鍥炲埌妫�楠屼换鍔�</span>
+        </el-form-item>
+        <el-form-item label="閫�鍥炲師鍥�">
+          <el-input v-model="sendBackForm.reason" type="textarea" :rows="4" maxlength="1000"
+                    show-word-limit placeholder="璇疯緭鍏ラ��鍥炲師鍥�" />
+        </el-form-item>
+      </el-form>
+      <template #footer><span class="dialog-footer">
+        <el-button :disabled="sendBackLoading" @click="sendBackVisible = false">鍙栨秷</el-button>
+        <el-button type="primary" :loading="sendBackLoading" @click="submitSendBack">纭畾閫�鍥�</el-button>
+      </span></template>
+    </el-dialog>
     <el-dialog :fullscreen="fullscreen" :modal-append-to-body="false" v-model="claimVisible" title="鍦ㄧ嚎缂栧埗"
                width="22cm">
       <div class="full-screen">
@@ -160,7 +181,7 @@
         <img v-else alt="" src="@/assets/images/no-full.svg" style="cursor: pointer;" @click="fullscreen = false;">
       </div>
       <div v-if="approveVisible" class="approve-preview">
-        <onlyoffice ref="onlyoffice" :options="option" style="width: 100%;height: 100%;"/>
+        <pdf-viewer :key="currentInfo.id" :load="loadApprovalPdf" />
       </div>
       <div class="seal-select-block" v-loading="sealLoading">
         <div class="seal-select-title">
@@ -247,18 +268,7 @@
     </el-dialog>
     <!--浜т笟閾句俊鎭煡鐪�-->
     <!--    <ShowInfo v-if="showInfoDialog" ref="showInfoDialog" :showInfoDialog="showInfoDialog"></ShowInfo>-->
-    <!--鎶ュ憡鏌ョ湅-->
-    <el-dialog :fullscreen="fullscreen" :modal-append-to-body="false" v-model="viewIssuedVisible" title="鎶ュ憡鏌ョ湅"
-               width="80vw">
-      <div class="full-screen">
-        <lims-icon name="el-icon-full-screen" v-if="!fullscreen" style="cursor: pointer;font-size: 18px"
-           @click="fullscreen = true;"></lims-icon>
-        <img v-else alt="" src="@/assets/images/no-full.svg" style="cursor: pointer;" @click="fullscreen = false;">
-      </div>
-      <div v-if="viewIssuedVisible" style="height: 80vh;">
-        <onlyoffice ref="onlyoffice" :options="option" style="width: 100%;height: 100%;"/>
-      </div>
-    </el-dialog>
+    <report-preview ref="reportPreview" />
     <!--闄勪欢鏌ョ湅-->
     <el-dialog v-model="filesDialogVisible" title="闄勪欢鏌ョ湅" width="80%" @closed="closeFilesLook">
       <div style="margin-bottom: 10px">
@@ -285,6 +295,7 @@
       </el-form>
       <template #footer><span  class="dialog-footer">
         <el-button @click="downloadDialogVisible = false">鍙� 娑�</el-button>
+        <el-button :disabled="!downloadRow" @click="previewReport(downloadRow, selectedReportType)">棰� 瑙�</el-button>
         <el-button type="primary" :loading="downloadingReportType !== null"
                    :disabled="selectedReportType === null || downloadingReportType !== null"
                    @click="downloadSelectedReport(selectedReportType)">涓� 杞�</el-button>
@@ -294,12 +305,15 @@
 </template>
 
 <script>
-import onlyoffice from "@/components/Onlyoffice/onlyoffice.vue";
+import PdfViewer from '@/components/ReportPreview/PdfViewer.vue'
 import ReportWordEditor from "@/components/ReportWordEditor/index.vue";
+import ReportPreview from '@/components/ReportPreview/index.vue'
+import { reportFilename } from '@/utils/reportPreview'
 // import ShowInfo from "../do/b1-material-ins-order/showInfo.vue";
 import {
   downAll,
   downloadReport,
+  previewReportPdf,
   examineReport,
   getReportEditDocument,
   saveReportEditDocument,
@@ -321,10 +335,11 @@
 
 export default {
   name: 'ReportPreparation',
-  components: {limsTable, onlyoffice, ReportWordEditor},
+  components: {limsTable, PdfViewer, ReportWordEditor, ReportPreview},
   dicts: ['material_prop_type'],
   data() {
     return {
+      hasActivated: false,
       entity: {
         queryStatus: null,
         code: null,
@@ -345,6 +360,13 @@
       issuedReasonVisible: false,
       approveVisible: false,
       approveReasonVisible: false,
+      sendBackVisible: false,
+      sendBackLoading: false,
+      sendBackRow: null,
+      sendBackForm: {
+        target: null,
+        reason: ''
+      },
       sealOptions: [],
       sealPositions: [
         { key: 'maSealId', label: 'MA鏍囧織绔�', description: 'PDF 绗�1椤靛乏涓婅' },
@@ -418,11 +440,11 @@
       customsInspection: {},
       currentId: null,
       examine: null,
-      viewIssuedVisible: false,
       queryStatusList: [
         {label: '寰呮彁浜�', value: 0},
         {label: '寰呭鏍�', value: 1},
         {label: '寰呮壒鍑�', value: 2},
+        {label: '宸查��鍥�', value: 3},
       ],
       state: 0,
       orderId: 0,
@@ -580,6 +602,8 @@
               return "涓嶉�氳繃";
             } else if (params == 1) {
               return "閫氳繃";
+            } else if (params == 2) {
+              return "宸查��鍥�";
             } else {
               return null
             }
@@ -589,6 +613,8 @@
               return "danger";
             } else if (params == 1) {
               return "success";
+            } else if (params == 2) {
+              return "warning";
             } else {
               return undefined
             }
@@ -607,6 +633,8 @@
               return "涓嶆壒鍑�";
             } else if (params == 1) {
               return "鎵瑰噯";
+            } else if (params == 2) {
+              return "宸查��鍥�";
             } else {
               return null
             }
@@ -616,6 +644,8 @@
               return "danger";
             } else if (params == 1) {
               return "success";
+            } else if (params == 2) {
+              return "warning";
             } else {
               return undefined
             }
@@ -647,6 +677,13 @@
   },
   mounted() {
     this.refreshTable()
+  },
+  activated() {
+    // 棣栨杩涘叆鐢� mounted 鍔犺浇锛岀紦瀛橀〉闈㈠啀娆℃縺娲绘椂鍒锋柊鍒楄〃銆�
+    if (this.hasActivated) {
+      this.refreshTable()
+    }
+    this.hasActivated = true
   },
   methods: {
     refreshTable() {
@@ -1103,52 +1140,47 @@
       this.auditDocument = null
       done()
     },
-    // 鏌ョ湅鎶ュ憡
-    viewIssued(row) {
-      console.log('this.javaApi', this.javaApi)
-      this.currentInfo = row;
-      let fileName = (row.urlS === null || row.urlS === '') ? row.url : row.urlS
-      let fileType = "docx"
-      if (row.tempUrlPdf != null || row.tempUrlPdf === '') {
-        fileName = row.tempUrlPdf
-        fileType = "pdf"
-      }
-      fileName = fileName.replace('/word/', '')
-      const userName = this.nickName
-      this.option = {
-        url: this.javaApi + "/word/" + fileName,
-        isEdit: false,
-        fileType: fileType,
-        title: fileName,
-        lang: 'zh-CN',
-        isPrint: false,
-        user_id: 1,
-        user_name: userName,
-        editUrl: this.javaApi + "/insReport/onlyOffice/save?fileName=" + fileName
-      }
-      this.viewIssuedVisible = true;
+    // 浣跨敤鏈嶅姟绔寜 Word 鐗堝紡鐢熸垚鐨� PDF锛屽鏉傝〃鏍间笉鍦ㄦ祻瑙堝櫒涓噸鏂版帓鐗堛��
+    previewReport(row, reportType = 0) {
+      const variants = [{ key: 0, label: '妫�娴嬫姤鍛�' }]
+      if (row.inspectionUrl) variants.push({ key: 1, label: '妫�楠屾姤鍛�' })
+      this.$refs.reportPreview.open(variants.map(item => ({
+        ...item, format: 'pdf',
+        filename: reportFilename((row.code || '鎶ュ憡') + (row.inspectionUrl ? '-' + item.label : ''), 'docx'),
+        load: signal => previewReportPdf(row.id, item.key, signal),
+        download: signal => downloadReport(row.id, item.key, signal), downloadFormat: 'docx'
+      })), reportType)
     },
-    // 閫�鍥炲埌浠诲姟
-    sendBackTask(row) {
-      this.$confirm('纭閫�鍥炲埌妫�楠屼换鍔�?閫�鍥炲悗闇�閲嶆柊鎻愪氦澶嶆牳', '鎻愮ず', {
-        confirmButtonText: '纭畾',
-        cancelButtonText: '鍙栨秷',
-        type: 'warning'
+    openSendBack(row) {
+      this.sendBackRow = row
+      this.sendBackForm = {
+        target: row.isRatify == 1 ? null : 'INSPECTION_TASK',
+        reason: ''
+      }
+      this.sendBackVisible = true
+    },
+    submitSendBack() {
+      if (!this.sendBackForm.target) {
+        return this.$message.error('璇烽�夋嫨閫�鍥炰綅缃�')
+      }
+      if (!this.sendBackForm.reason || !this.sendBackForm.reason.trim()) {
+        return this.$message.error('璇疯緭鍏ラ��鍥炲師鍥�')
+      }
+      if (this.sendBackLoading || !this.sendBackRow) return
+      this.sendBackLoading = true
+      const targetName = this.sendBackForm.target === 'REPORT_REVIEW' ? '鎶ュ憡瀹℃牳' : '妫�楠屼换鍔�'
+      sendBackTask({
+        id: this.sendBackRow.id,
+        target: this.sendBackForm.target,
+        reason: this.sendBackForm.reason.trim()
       }).then(() => {
-        this.upLoad = true
-        sendBackTask({id: row.id}).then(res => {
-          this.upLoad = false
-          this.$message.success('閫�鍥炴垚鍔燂紒')
-          this.refreshTable('page')
-        }).catch(err => {
-          this.upLoad = false
-        })
-      }).catch(() => {
-        this.$message({
-          type: 'info',
-          message: '宸插彇娑堝垹闄�'
-        });
-      });
+        this.$message.success(`宸查��鍥炲埌${targetName}`)
+        this.sendBackVisible = false
+        this.sendBackRow = null
+        this.refreshTable('page')
+      }).finally(() => {
+        this.sendBackLoading = false
+      })
     },
     // 瀹℃牳閫氳繃
     async submitAddApprover() {
@@ -1228,26 +1260,11 @@
     handleApprove(row) {
       this.currentInfo = row;
       this.loadEnabledSeals(row);
-      let fileName = (row.urlS === null || row.urlS === '') ? row.url : row.urlS
-      let fileType = "docx"
-      if (row.tempUrlPdf != null || row.tempUrlPdf === '') {
-        fileName = row.tempUrlPdf
-        fileType = "pdf"
-      }
-      fileName = fileName.replace('/word/', '')
-      const userName = this.nickName
-      this.option = {
-        url: this.javaApi + "/word/" + fileName,
-        isEdit: false,
-        fileType: fileType,
-        title: fileName,
-        lang: 'zh-CN',
-        isPrint: false,
-        user_id: 1,
-        user_name: userName,
-        editUrl: this.javaApi + "/insReport/onlyOffice/save?fileName=" + fileName
-      }
+      this.fullscreen = false;
       this.approveVisible = true;
+    },
+    loadApprovalPdf(signal) {
+      return previewReportPdf(this.currentInfo.id, 0, signal)
     },
     // 鎵瑰噯閫氳繃
     subApprove() {
@@ -1368,7 +1385,7 @@
         ratifyTell: this.reason
       }).then(res => {
         this.loadingApproveReason = false
-        this.$message.success('鎿嶄綔鎴愬姛')
+        this.$message.success('宸查��鍥炴姤鍛婂鏍�')
         this.refreshTable('page')
         this.currentInfo = null;
         this.reason = '';
diff --git a/tests/attachment-download.test.mjs b/tests/attachment-download.test.mjs
index 48928e2..3e856a8 100644
--- a/tests/attachment-download.test.mjs
+++ b/tests/attachment-download.test.mjs
@@ -149,7 +149,7 @@
     .map(n => n.type === 'ImportDeclaration' ? { start: n.start, end: n.end, text: '' }
       : { start: n.start, end: n.declaration.start, text: 'globalThis.component = ' })
   for (const edit of edits.sort((a, b) => b.start - a.start)) source = source.slice(0, edit.start) + edit.text + source.slice(edit.end)
-  const context = vm.createContext({ vueQr: {}, PrintDialog: {}, limsTable: {}, mapGetters: () => ({}), getFileList })
+  const context = vm.createContext({ vueQr: {}, PrintDialog: {}, ReportPreview: {}, limsTable: {}, mapGetters: () => ({}), getFileList })
   vm.runInContext(source, context)
   return { ...context.component.data(), ...context.component.methods }
 }
diff --git a/vite.config.js b/vite.config.js
index 4f1b5a0..2611d74 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -13,6 +13,9 @@
     // 榛樿鎯呭喌涓嬶紝vite 浼氬亣璁句綘鐨勫簲鐢ㄦ槸琚儴缃插湪涓�涓煙鍚嶇殑鏍硅矾寰勪笂
     // 渚嬪 https://www.ruoyi.vip/銆傚鏋滃簲鐢ㄨ閮ㄧ讲鍦ㄤ竴涓瓙璺緞涓婏紝浣犲氨闇�瑕佺敤杩欎釜閫夐」鎸囧畾杩欎釜瀛愯矾寰勩�備緥濡傦紝濡傛灉浣犵殑搴旂敤琚儴缃插湪 https://www.ruoyi.vip/admin/锛屽垯璁剧疆 baseUrl 涓� /admin/銆�
     base: VITE_APP_ENV === 'production' ? '/' : '/',
+    // 涓庝复鏃堕瑙堛�佹牎楠岃剼鏈娇鐢ㄧ殑榛樿 .vite/deps 缂撳瓨闅旂锛岄伩鍏嶈繍琛屼腑鐨�
+    // 鏈嶅姟缁х画寮曠敤宸茶鍙︿竴瀹炰緥鏇挎崲鐨勯鏋勫缓渚濊禆锛�504 Outdated Optimize Dep锛夈��
+    cacheDir: `node_modules/.vite/lims-${command}-${mode}`,
     plugins: createVitePlugins(env, command === 'build'),
     resolve: {
       // https://cn.vitejs.dev/config/#resolve-alias

--
Gitblit v1.9.3