From 89fd9ab312c0aef9fcab89fbbaee4add5baa7c57 Mon Sep 17 00:00:00 2001
From: 吴羡 <16976746+Xian-TT3@user.noreply.gitee.com>
Date: 星期二, 27 十月 2026 09:43:22 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master'

---
 src/views/business/inspectionTask/components/InspectionWord.vue                      |    2 
 src/components/Attachment/lims-attachment.vue                                        |    6 
 src/views/CNAS/resourceDemand/device/component/check.vue                             |   13 -
 src/views/business/reportPreparation/index.vue                                       |    2 
 src/views/CNAS/process/method/methodVerification/component/calibrationsFileDia.vue   |   14 -
 src/utils/reportDocxLayout.js                                                        |   16 +
 src/views/business/unpass/components/unPassDialog.vue                                |    2 
 src/views/CNAS/personnel/personnelInfo/components/ViewRecord.vue                     |   12 -
 src/views/business/materialOrderComponents/materialOrder/filesLookVisible.vue        |    6 
 src/views/CNAS/personnel/personnelInfo/tabs/personnel-information.vue                |   27 --
 src/views/business/inspectionTask/inspection.vue                                     |    4 
 src/views/business/productOrder/index.vue                                            |   23 +
 src/views/CNAS/resourceDemand/device/component/operationInstruction.vue              |   15 -
 src/utils/attachment.js                                                              |   69 +++++++
 tests/attachment-download.test.mjs                                                   |  172 +++++++++++++++++++
 src/views/business/inspectionView/index.vue                                          |    4 
 src/plugins/download.js                                                              |   48 +++++
 src/views/CNAS/resourceDemand/device/component/checkRecord.vue                       |    3 
 src/components/FileUpload/index.vue                                                  |    3 
 src/views/CNAS/process/method/methodVerification/component/viewWorkPermitDia.vue     |   14 -
 src/views/CNAS/resourceDemand/device/component/equipmentVerificationPlan.vue         |    3 
 src/views/CNAS/process/ensureResults/ensureResultsValidity/components/ViewRecord.vue |   12 -
 src/views/CNAS/process/method/methodVerification/component/ViewTestRecord.vue        |   12 -
 23 files changed, 347 insertions(+), 135 deletions(-)

diff --git a/src/components/Attachment/lims-attachment.vue b/src/components/Attachment/lims-attachment.vue
index 62ae50d..ae6a8b0 100644
--- a/src/components/Attachment/lims-attachment.vue
+++ b/src/components/Attachment/lims-attachment.vue
@@ -60,6 +60,7 @@
 
 <script>
 import request from '@/utils/request'
+import { resolveAttachmentUrl } from '@/utils/attachment'
 import filePreview from '@/components/Preview/filePreview.vue'
 
 /**
@@ -298,7 +299,7 @@
       this.previewVisible = true
     },
     downloadFile(row) {
-      this.$download.saveAs(this.fullUrl(row.fileUrl), row.fileName)
+      return this.$download.attachment(row, row.fileName)
     },
     removeFile(row) {
       this.$confirm('鏄惁鍒犻櫎璇ラ檮浠讹紵', '鎻愮ず', { type: 'warning' }).then(() => {
@@ -314,8 +315,7 @@
     },
     // 闈欐�佸湴鍧�鎷兼帴锛屼笌鍚庣涓婁紶缁熶竴璺敱淇濇寔涓�鑷达細鍥剧墖鍦� /img锛屽叾瀹冮兘鍦� /word
     fullUrl(fileUrl) {
-      const base = /\.(jpg|jpeg|png|gif)$/i.test(fileUrl) ? '/img/' : '/word/'
-      return this.javaApi + base + fileUrl
+      return resolveAttachmentUrl(fileUrl)
     }
   }
 }
diff --git a/src/components/FileUpload/index.vue b/src/components/FileUpload/index.vue
index b3bed45..f2410d1 100644
--- a/src/components/FileUpload/index.vue
+++ b/src/components/FileUpload/index.vue
@@ -29,7 +29,7 @@
     <!-- 鏂囦欢鍒楄〃 -->
     <transition-group ref="uploadFileList" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
       <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
-        <el-link :href="`${baseUrl}${file.url}`" underline="never" target="_blank">
+        <el-link @click="proxy.$download.saveAs(file.url, getFileName(file.name))" underline="never">
           <lims-icon name="el-icon-document" /> {{ getFileName(file.name) }}
         </el-link>
         <div class="ele-upload-list__item-content-action">
@@ -91,7 +91,6 @@
 const emit = defineEmits()
 const number = ref(0)
 const uploadList = ref([])
-const baseUrl = import.meta.env.VITE_APP_BASE_API
 const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 涓婁紶鏂囦欢鏈嶅姟鍣ㄥ湴鍧�
 const headers = ref({ Authorization: "Bearer " + getToken() })
 const fileList = ref([])
diff --git a/src/plugins/download.js b/src/plugins/download.js
index e83e43c..f2903dc 100644
--- a/src/plugins/download.js
+++ b/src/plugins/download.js
@@ -4,6 +4,8 @@
 import { getToken } from '@/utils/auth'
 import errorCode from '@/utils/errorCode'
 import { blobValidate } from '@/utils/ruoyi'
+import { getJavaApi } from '@/utils/java-api'
+import { resolveAttachmentUrl, attachmentFileName, alternateImageAttachmentUrl, validateAttachmentBlob } from '@/utils/attachment'
 
 const baseURL = import.meta.env.VITE_APP_BASE_API
 let downloadLoadingInstance
@@ -67,7 +69,51 @@
     })
   },
   saveAs(text, name, opts) {
-    saveAs(text, name, opts)
+    if (typeof text === 'string') return this.url(text, name, opts)
+    return saveAs(text, name, opts)
+  },
+  attachment(file, name) {
+    try {
+      return this.url(resolveAttachmentUrl(file?.fileUrl, file?.type), name || file?.fileName, undefined,
+        ![1, 2].includes(Number(file?.type)))
+    } catch (error) {
+      ElMessage.error(error.message)
+      return Promise.resolve(false)
+    }
+  },
+  async url(path, name, opts, allowAlternative = true) {
+    try {
+      const url = resolveAttachmentUrl(path)
+      const backend = new URL(getJavaApi(), window.location.origin)
+      const target = new URL(url, window.location.origin)
+      const isBackend = target.origin === backend.origin
+        && (target.pathname === backend.pathname || target.pathname.startsWith(backend.pathname.replace(/\/$/, '') + '/'))
+      const filename = attachmentFileName(url, name)
+      // 澶栭儴闄勪欢娌跨敤娴忚鍣ㄤ笅杞斤紝鍏煎涓嶅厑璁歌法鍩熻鍙栫殑鏂囦欢鏈嶅姟銆�
+      if (!isBackend && /^https?:$/.test(target.protocol)) {
+        saveAs(url, filename, opts)
+        return true
+      }
+      const headers = isBackend && getToken() ? { Authorization: 'Bearer ' + getToken() } : {}
+      let response
+      try {
+        response = await axios.get(url, { responseType: 'blob', headers })
+      } catch (error) {
+        const alternative = isBackend && allowAlternative && error.response?.status === 404
+          ? alternateImageAttachmentUrl(url) : null
+        if (!alternative) throw error
+        response = await axios.get(alternative, { responseType: 'blob', headers })
+      }
+      const { data } = response
+      await validateAttachmentBlob(data, filename)
+      saveAs(data, filename, opts)
+      return true
+    } catch (error) {
+      const status = error.response?.status
+      ElMessage.error(status === 404 ? '闄勪欢涓嶅瓨鍦ㄦ垨宸茶鍒犻櫎' : status === 401 || status === 403
+        ? '鐧诲綍宸茶繃鏈熸垨娌℃湁闄勪欢涓嬭浇鏉冮檺' : error.message || '闄勪欢涓嬭浇澶辫触锛岃绋嶅悗閲嶈瘯')
+      return false
+    }
   },
   async printErrMsg(data) {
     const resText = await data.text()
diff --git a/src/utils/attachment.js b/src/utils/attachment.js
new file mode 100644
index 0000000..21223b5
--- /dev/null
+++ b/src/utils/attachment.js
@@ -0,0 +1,69 @@
+import { getJavaApi } from '@/utils/java-api'
+
+const imageExtension = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tiff?|avif|heic)$/i
+
+function decodePart(value) {
+  try {
+    return decodeURIComponent(value)
+  } catch {
+    return value
+  }
+}
+
+// 鏁版嵁搴撳彲鑳戒繚瀛樻枃浠跺悕銆侀潤鎬佽矾寰勬垨瀹屾暣鍦板潃锛涘彧涓烘枃浠跺悕琛ュ厖瀛樺偍鐩綍銆�
+export function resolveAttachmentUrl(value, type, base = getJavaApi()) {
+  if (typeof value !== 'string' || !value.trim()) throw new Error('闄勪欢鍦板潃涓虹┖')
+  let path = value.replace(/\\/g, '/')
+  base = base.replace(/\/+$/, '')
+  if (/^(blob:|data:)/i.test(path)) return path
+  if (path.startsWith(base + '/')) path = path.slice(base.length)
+  else if (/^(https?:)?\/\//i.test(path)) return new URL(path, window.location.origin).href
+  else if (/^[a-z][a-z\d+.-]*:/i.test(path)) throw new Error('闄勪欢鍦板潃鏍煎紡涓嶆纭�')
+
+  // 鍏煎宸茬粡甯︽湁 /dev-api 绛変唬鐞嗗墠缂�鐨勫湴鍧�锛岄伩鍏嶉噸澶嶆嫾鎺ャ��
+  const basePath = new URL(base, window.location.origin).pathname.replace(/\/$/, '')
+  if (basePath && path.startsWith(basePath + '/')) path = path.slice(basePath.length)
+  path = path.replace(/^\/+/, '')
+  if (!/^(img|word|profile)\//i.test(path)) {
+    const isImage = Number(type) === 1 || (Number(type) !== 2 && imageExtension.test(decodePart(path)))
+    path = (isImage ? 'img/' : 'word/') + path
+  }
+  // 閫愭缂栫爜锛屼繚鐣欑洰褰曞拰宸叉湁缂栫爜锛屾枃浠跺悕閲岀殑 #銆�?銆佺┖鏍间笌涓枃鍧囦綔涓烘枃浠跺悕澶勭悊銆�
+  const encodedPath = path.split('/').map(part => encodeURIComponent(decodePart(part))).join('/')
+  return base + '/' + encodedPath
+}
+
+export function attachmentFileName(url, name) {
+  const path = new URL(url, window.location.origin).pathname
+  const storedName = decodePart(path.split('/').pop() || '')
+  const filename = (name || storedName || '闄勪欢').replace(/\\/g, '/').split('/').pop()
+  const extension = storedName.match(/\.[a-z\d]{1,10}$/i)?.[0] || ''
+  return extension && !/\.[a-z\d]{1,10}$/i.test(filename) ? filename + extension : filename
+}
+
+// 鍘嗗彶涓婁紶鎺ュ彛瀵� BMP/WebP 绛夋牸寮忕殑鐩綍鍒ゆ柇涓嶄竴鑷达紝404 鏃跺吋瀹瑰彟涓�瀛樺偍鐩綍銆�
+export function alternateImageAttachmentUrl(url) {
+  const parsed = new URL(url, window.location.origin)
+  if (!imageExtension.test(decodePart(parsed.pathname)) || !/\/(img|word)\//.test(parsed.pathname)) return null
+  parsed.pathname = parsed.pathname.replace(/\/(img|word)\//, (_, directory) => directory === 'img' ? '/word/' : '/img/')
+  return parsed.href
+}
+
+// 涓嬭浇澶辫触鐨� JSON/HTML 鍝嶅簲涓嶈兘淇濆瓨鎴� Word銆丳DF 绛夆�滄崯鍧忕殑闄勪欢鈥濄��
+export async function validateAttachmentBlob(blob, filename) {
+  const head = (await blob.slice(0, 512).text()).trim()
+  if (head.startsWith('{') && blob.size < 1024 * 1024) {
+    let result
+    try {
+      result = JSON.parse(await blob.text())
+    } catch {
+      return
+    }
+    if (result && ((result.code != null && Number(result.code) !== 200) || result.success === false)) {
+      throw new Error(result.msg || result.message || '闄勪欢涓嬭浇澶辫触')
+    }
+  }
+  if (/^(<!doctype html|<html)/i.test(head) && !/\.html?$/i.test(filename)) {
+    throw new Error('鏈幏鍙栧埌闄勪欢鍐呭锛岃纭鏂囦欢瀛樺湪鎴栭噸鏂扮櫥褰曞悗閲嶈瘯')
+  }
+}
diff --git a/src/utils/reportDocxLayout.js b/src/utils/reportDocxLayout.js
index ff2ae3f..8b081a6 100644
--- a/src/utils/reportDocxLayout.js
+++ b/src/utils/reportDocxLayout.js
@@ -164,6 +164,21 @@
   return changed ? documentXml : null
 }
 
+function clearDefaultSequenceUnderlines(xml) {
+  return xml.replace(resultTablePattern, table => {
+    if (!table.includes('搴忓彿') || !/(妫�娴嬬粨鏋渱妫�楠岀粨鏋�)/.test(table)) return table
+    return table.replace(tableRowPattern, row => row.replace(tableCellPattern, (cell, offset) => {
+      if (offset !== row.indexOf('<w:tc')) return cell
+      const text = [...cell.matchAll(/<w:t\b(?![^>]*\/>)[^>]*>([\s\S]*?)<\/w:t>/g)]
+        .map(match => match[1]).join('').trim()
+      if (!/^\d+$/.test(text)) return cell
+      // 鏃ф姤鍛婁腑鐨� <w:u w:color="auto"/> 榛樿浼氳瑙i噴鎴愬崟涓嬪垝绾裤��
+      // 鏄惧紡璁剧疆浜嗙被鍨嬬殑涓嬪垝绾夸繚鐣欙紝鍏佽鐢ㄦ埛鑷淇敼搴忓彿鏍煎紡銆�
+      return cell.replace(/<w:u\b[^>]*\/>/g, tag => /\bw:val=/.test(tag) ? tag : '')
+    }))
+  })
+}
+
 export async function normalizeReportDocxForEditor(blob, reportCode) {
   const zip = await JSZip.loadAsync(blob)
   const documentPart = zip.file('word/document.xml')
@@ -173,6 +188,7 @@
   const themeXml = themePart ? await themePart.async('string') : ''
   const themeFonts = { major: chineseThemeFont(themeXml, 'major'), minor: chineseThemeFont(themeXml, 'minor') }
   let normalizedXml = alignResultTable(documentXml) || documentXml
+  normalizedXml = clearDefaultSequenceUnderlines(normalizedXml)
   normalizedXml = fitCoverCustomer(normalizedXml) || normalizedXml
   normalizedXml = materializeChineseThemeFonts(normalizedXml, themeFonts) || normalizedXml
   let changed = normalizedXml !== documentXml
diff --git a/src/views/CNAS/personnel/personnelInfo/components/ViewRecord.vue b/src/views/CNAS/personnel/personnelInfo/components/ViewRecord.vue
index cc56bda..80af54d 100644
--- a/src/views/CNAS/personnel/personnelInfo/components/ViewRecord.vue
+++ b/src/views/CNAS/personnel/personnelInfo/components/ViewRecord.vue
@@ -113,17 +113,7 @@
     },
     // 涓嬭浇
     upload (row) {
-      let url = '';
-      if(row.type==1){
-        url = this.javaApi+'/img/'+row.fileUrl
-        file.downloadIamge(url,row.fileName)
-      }else{
-        url = this.javaApi+'/word/'+row.fileUrl
-        const link = document.createElement('a');
-        link.href = url;
-        link.download = row.fileName;
-        link.click();
-      }
+      return this.$download.attachment(row, row.fileName)
     },
     // 鍒犻櫎
     delete (row) {
diff --git a/src/views/CNAS/personnel/personnelInfo/tabs/personnel-information.vue b/src/views/CNAS/personnel/personnelInfo/tabs/personnel-information.vue
index c8169d2..a4fddee 100644
--- a/src/views/CNAS/personnel/personnelInfo/tabs/personnel-information.vue
+++ b/src/views/CNAS/personnel/personnelInfo/tabs/personnel-information.vue
@@ -470,7 +470,7 @@
 </template>
 
 <script>
-import fileDownload from '@/utils/file'
+
 import {
   addAnnex,
   addBasicInfoWork,
@@ -734,17 +734,7 @@
     },
     // 涓嬭浇
     upload(row) {
-      let url = '';
-      if (row.type == 1) {
-        url = this.javaApi + '/img/' + row.fileUrl
-        fileDownload.downloadIamge(url, row.fileName)
-      } else {
-        url = this.javaApi + '/word/' + row.fileUrl
-        const link = document.createElement('a');
-        link.href = url;
-        link.download = row.fileName;
-        link.click();
-      }
+      return this.$download.attachment(row, row.fileName)
     },
     // 鍒犻櫎
     delete(row) {
@@ -932,18 +922,7 @@
       }
     },
     downloadFile(fileName) {
-      let state = /\.(jpg|jpeg|png|gif)$/i.test(fileName)
-      if (state) {
-        let url = this.javaApi + '/img/' + fileName;
-        fileDownload.downloadIamge(url, fileName)
-      } else {
-        const url = this.javaApi + '/word/' + fileName
-        const link = document.createElement('a');
-        link.href = url;
-        link.download = fileName;
-        link.click();
-        this.$message.success('涓嬭浇鎴愬姛')
-      }
+      return this.$download.saveAs(fileName, fileName)
     },
     async onSuccess(response, file, fileList, entityVal) {
       if (entityVal == 'fileName') {
diff --git a/src/views/CNAS/process/ensureResults/ensureResultsValidity/components/ViewRecord.vue b/src/views/CNAS/process/ensureResults/ensureResultsValidity/components/ViewRecord.vue
index 20ad699..7e1899b 100644
--- a/src/views/CNAS/process/ensureResults/ensureResultsValidity/components/ViewRecord.vue
+++ b/src/views/CNAS/process/ensureResults/ensureResultsValidity/components/ViewRecord.vue
@@ -116,17 +116,7 @@
     },
     // 涓嬭浇
     upload (row) {
-      let url = '';
-      if(row.type==1){
-        url = this.javaApi+'/img/'+row.fileUrl
-        file.downloadIamge(url,row.fileName)
-      }else{
-        url = this.javaApi+'/word/'+row.fileUrl
-        const link = document.createElement('a');
-        link.href = url;
-        link.download = row.fileName;
-        link.click();
-      }
+      return this.$download.attachment(row, row.fileName)
     },
     // 鍒犻櫎
     delete (row) {
diff --git a/src/views/CNAS/process/method/methodVerification/component/ViewTestRecord.vue b/src/views/CNAS/process/method/methodVerification/component/ViewTestRecord.vue
index 02b4ad2..f5f52ed 100644
--- a/src/views/CNAS/process/method/methodVerification/component/ViewTestRecord.vue
+++ b/src/views/CNAS/process/method/methodVerification/component/ViewTestRecord.vue
@@ -116,17 +116,7 @@
     },
     // 涓嬭浇
     upload (row) {
-      let url = '';
-      if(row.type==1){
-        url = this.javaApi+'/img/'+row.fileUrl
-        file.downloadIamge(url,row.fileName)
-      }else{
-        url = this.javaApi+'/word/'+row.fileUrl
-        const link = document.createElement('a');
-        link.href = url;
-        link.download = row.fileName;
-        link.click();
-      }
+      return this.$download.attachment(row, row.fileName)
     },
     // 鍒犻櫎
     delete (row) {
diff --git a/src/views/CNAS/process/method/methodVerification/component/calibrationsFileDia.vue b/src/views/CNAS/process/method/methodVerification/component/calibrationsFileDia.vue
index 04ca9c0..0730d60 100644
--- a/src/views/CNAS/process/method/methodVerification/component/calibrationsFileDia.vue
+++ b/src/views/CNAS/process/method/methodVerification/component/calibrationsFileDia.vue
@@ -28,7 +28,7 @@
 <script>
 import filePreview from '@/components/Preview/filePreview.vue';
 import limsTable from '@/components/Table/lims-table.vue'
-import file from '@/utils/file';
+
 import { getCurrentCalibrationFiles } from '@/api/cnas/process/method/methodVerification'
 
 export default {
@@ -120,17 +120,7 @@
     },
     // 涓嬭浇
     upload (row) {
-      let url = '';
-      if (/\.(png|jpe?g|gif|bmp|webp)$/i.test(row.fileUrl || '')) {
-        url = this.javaApi+'/img/'+row.fileUrl
-        file.downloadIamge(url,row.fileName)
-      }else{
-        url = this.javaApi+'/word/'+row.fileUrl
-        const link = document.createElement('a');
-        link.href = url;
-        link.download = row.fileName;
-        link.click();
-      }
+      return this.$download.attachment(row, row.fileName)
     },
   }
 };
diff --git a/src/views/CNAS/process/method/methodVerification/component/viewWorkPermitDia.vue b/src/views/CNAS/process/method/methodVerification/component/viewWorkPermitDia.vue
index cbe7ee6..ce2f5dd 100644
--- a/src/views/CNAS/process/method/methodVerification/component/viewWorkPermitDia.vue
+++ b/src/views/CNAS/process/method/methodVerification/component/viewWorkPermitDia.vue
@@ -26,7 +26,7 @@
 </template>
 
 <script>
-import file from '@/utils/file';
+
 import filePreview from '@/components/Preview/filePreview.vue';
 import limsTable from '@/components/Table/lims-table.vue'
 import {getCurrentWorkFiles} from '@/api/cnas/process/method/methodVerification'
@@ -134,17 +134,7 @@
         this.$message.warning('璇ヤ汉鍛樺凡鏈変换鑱屾巿鏉冭褰曪紝浣嗗皻鏈笂浼犱笂宀楄瘉涔︽枃浠�')
         return
       }
-      let url = '';
-      if (/\.(png|jpe?g|gif|bmp|webp)$/i.test(row.fileUrl || '')) {
-        url = this.javaApi + '/img/' + row.fileUrl
-        file.downloadIamge(url, row.fileName)
-      } else {
-        url = this.javaApi + '/word/' + row.fileUrl
-        const link = document.createElement('a');
-        link.href = url;
-        link.download = row.fileName;
-        link.click();
-      }
+      return this.$download.attachment({ fileUrl: row.fileUrl }, row.fileName)
     },
   },
 };
diff --git a/src/views/CNAS/resourceDemand/device/component/check.vue b/src/views/CNAS/resourceDemand/device/component/check.vue
index b6257eb..e0038cc 100644
--- a/src/views/CNAS/resourceDemand/device/component/check.vue
+++ b/src/views/CNAS/resourceDemand/device/component/check.vue
@@ -516,10 +516,7 @@
     },
     // end
     handleAttachmentClick(row) {
-      // 妯℃嫙涓嬭浇闄勪欢
-      const imageUrl = this.javaApi + '/img/' + row.systemFileName; // 鍥剧墖 URL
-      file.downloadIamge(imageUrl, row.fileName)
-      // downloadImage(imageUrl, row.fileName);
+      return this.$download.attachment({ fileUrl: row.systemFileName }, row.fileName)
     },
     // 娣诲姞鏍告煡璁板綍
     add(type) {
@@ -583,14 +580,6 @@
   }
 }
 
-function downloadImage(url, name) {
-  const link = document.createElement('a');
-  link.href = url;
-  link.download = name; // 鏂囦欢鍚�
-  document.body.appendChild(link);
-  link.click();
-  document.body.removeChild(link);
-}
 </script>
 <style scoped>
 .dialog-content {
diff --git a/src/views/CNAS/resourceDemand/device/component/checkRecord.vue b/src/views/CNAS/resourceDemand/device/component/checkRecord.vue
index f4ea5a5..bbd9d3c 100644
--- a/src/views/CNAS/resourceDemand/device/component/checkRecord.vue
+++ b/src/views/CNAS/resourceDemand/device/component/checkRecord.vue
@@ -451,8 +451,7 @@
       if (!this.form.fileUrl) {
         return
       }
-      const isImage = /\.(jpg|jpeg|png|gif)$/i.test(this.form.fileUrl)
-      window.open(this.javaApi + (isImage ? '/img/' : '/word/') + this.form.fileUrl, '_blank')
+      return this.$download.saveAs(this.form.fileUrl, this.form.fileName)
     },
   },
   computed: {
diff --git a/src/views/CNAS/resourceDemand/device/component/equipmentVerificationPlan.vue b/src/views/CNAS/resourceDemand/device/component/equipmentVerificationPlan.vue
index 6c5e8d8..975d0d1 100644
--- a/src/views/CNAS/resourceDemand/device/component/equipmentVerificationPlan.vue
+++ b/src/views/CNAS/resourceDemand/device/component/equipmentVerificationPlan.vue
@@ -568,8 +568,7 @@
     },
     // 鎵撳紑闄勪欢
     openAttachment(row) {
-      const isImage = /\.(jpg|jpeg|png|gif)$/i.test(row.fileUrl)
-      window.open(this.javaApi + (isImage ? '/img/' : '/word/') + row.fileUrl, '_blank')
+      return this.$download.saveAs(row.fileUrl, row.fileName)
     },
     // 鍒楄〃闄勪欢鍒楀彧璇绘煡鐪嬶紙涓婁紶/鍒犻櫎鍏ュ彛鍙湪缂栬緫銆佹柊澧炲脊绐楀唴锛�
     handleAttachment(row) {
diff --git a/src/views/CNAS/resourceDemand/device/component/operationInstruction.vue b/src/views/CNAS/resourceDemand/device/component/operationInstruction.vue
index fbf1483..d66538b 100644
--- a/src/views/CNAS/resourceDemand/device/component/operationInstruction.vue
+++ b/src/views/CNAS/resourceDemand/device/component/operationInstruction.vue
@@ -182,7 +182,7 @@
 </template>
 
 <script>
-import fileDownload from "@/utils/file";
+
 import {
   approvalOfHomeworkInstructionManual,
   homeworkGuidebook,
@@ -322,18 +322,7 @@
       });
     },
     downloadFile(fileName) {
-      let state = /\.(jpg|jpeg|png|gif)$/i.test(fileName)
-      if (state) {
-        let url = this.javaApi + '/img/' + fileName;
-        fileDownload.downloadIamge(url, fileName)
-      } else {
-        const url = this.javaApi + '/word/' + fileName
-        const link = document.createElement('a');
-        link.href = url;
-        link.download = fileName;
-        link.click();
-        this.$message.success('涓嬭浇鎴愬姛')
-      }
+      return this.$download.saveAs(fileName, fileName)
     },
     instructionEditFun(row) {
       this.dialogVisible = true
diff --git a/src/views/business/inspectionTask/components/InspectionWord.vue b/src/views/business/inspectionTask/components/InspectionWord.vue
index 97a0b2a..9c05062 100644
--- a/src/views/business/inspectionTask/components/InspectionWord.vue
+++ b/src/views/business/inspectionTask/components/InspectionWord.vue
@@ -1729,7 +1729,7 @@
       downFile({
         id: row.id,
       }).then(res => {
-        this.$download.saveAs(res.data.fileUrl, row.fileName);
+        return this.$download.attachment(res.data, row.fileName);
       }).catch(error => {
 
       })
diff --git a/src/views/business/inspectionTask/inspection.vue b/src/views/business/inspectionTask/inspection.vue
index 5e92482..0a6299a 100644
--- a/src/views/business/inspectionTask/inspection.vue
+++ b/src/views/business/inspectionTask/inspection.vue
@@ -2242,9 +2242,7 @@
     },
     handleDown(row) {
       downFile({ id: row.id }).then((res) => {
-        const resourcePath = Number(res.data.type) === 1 ? "/img/" : "/word/";
-        const fileUrl = this.javaApi + resourcePath + encodeURIComponent(res.data.fileUrl);
-        this.$download.saveAs(fileUrl, row.fileName);
+        return this.$download.attachment(res.data, row.fileName);
       })
         .catch((error) => { });
     },
diff --git a/src/views/business/inspectionView/index.vue b/src/views/business/inspectionView/index.vue
index 8abe80f..7396b58 100644
--- a/src/views/business/inspectionView/index.vue
+++ b/src/views/business/inspectionView/index.vue
@@ -2470,9 +2470,7 @@
     },
     handleDown(row) {
       downFile({ id: row.id }).then((res) => {
-        const resourcePath = Number(res.data.type) === 1 ? "/img/" : "/word/";
-        const fileUrl = this.javaApi + resourcePath + encodeURIComponent(res.data.fileUrl);
-        this.$download.saveAs(fileUrl, row.fileName);
+        return this.$download.attachment(res.data, row.fileName);
       })
         .catch((error) => { });
     },
diff --git a/src/views/business/materialOrderComponents/materialOrder/filesLookVisible.vue b/src/views/business/materialOrderComponents/materialOrder/filesLookVisible.vue
index 043938a..6a07f31 100644
--- a/src/views/business/materialOrderComponents/materialOrder/filesLookVisible.vue
+++ b/src/views/business/materialOrderComponents/materialOrder/filesLookVisible.vue
@@ -124,6 +124,7 @@
     // 鍒囨崲鏁版嵁鏌ョ湅tab鏍�
     handleDataVisibleTab(m, i) {
       this.dataVisibleIndex = i
+      this.pageFile.current = 1
       this.getFileList()
     },
     // 鏌ヨ鍥炶皟
@@ -134,7 +135,7 @@
         this.entity.insOrderId = this.filesLookInfo.quarterOrderId
       }
       this.tableLoadingFile = true
-      const params = { ...this.entity }
+      const params = { ...this.entity, current: this.pageFile.current, size: this.pageFile.size }
       getFileList(params).then(res => {
         this.tableLoadingFile = false
         if (res.code === 200) {
@@ -146,13 +147,14 @@
       })
     },
     paginationFile(page) {
+      this.pageFile.current = page.page
       this.pageFile.size = page.limit
       this.getFileList()
     },
     // 涓嬭浇
     handleDown(row) {
       downFile({ id: row.id, }).then(res => {
-        this.$download.saveAs(res.data.fileUrl, row.fileName);
+        return this.$download.attachment(res.data, row.fileName);
       }).catch(error => {
 
       })
diff --git a/src/views/business/productOrder/index.vue b/src/views/business/productOrder/index.vue
index 72952d9..49965e2 100644
--- a/src/views/business/productOrder/index.vue
+++ b/src/views/business/productOrder/index.vue
@@ -15,10 +15,10 @@
           <el-input v-model="entity.sampleModel" clearable placeholder="璇疯緭鍏�" size="small"
             @keyup.enter="refreshTable()"></el-input>
         </el-form-item>
-        <el-form-item label="涓嬪崟鏃堕棿" prop="createTime" v-if="more">
-          <el-date-picker v-model="entity.createTime" clearable format="YYYY-MM-DD" placeholder="閫夋嫨鏃ユ湡" size="small"
+        <el-form-item label="閫佹牱鏃堕棿" prop="sampleDate" v-if="more">
+          <el-date-picker v-model="entity.sampleDate" clearable format="YYYY-MM-DD" placeholder="閫夋嫨鏃ユ湡" size="small"
                           @change="refreshTable"
-            style="width:100%" type="date" value-format="YYYY-MM-DD HH:mm:ss">
+            style="width:100%" type="date" value-format="YYYY-MM-DD">
           </el-date-picker>
         </el-form-item>
         <el-form-item label="濮旀墭浜�" prop="prepareUser" v-if="more">
@@ -298,6 +298,7 @@
       entity: {
         orderType: '',
         state: '',
+        sampleDate: null,
       },
       upIndex: 0,
       tableData: [],
@@ -332,7 +333,7 @@
         { label: '澶囨敞', prop: 'remark' },
         { label: '妫�楠岃繘搴�%', prop: 'insProgress' },
         { label: '绾﹀畾鏃堕棿', prop: 'appointed' },
-        { label: '涓嬪崟鏃堕棿', prop: 'createTime' },
+        { label: '閫佹牱鏃堕棿', prop: 'sampleDate', minWidth: 110 },
         { label: '涓嬪彂鏃堕棿', prop: 'sendTime' },
         { label: '閫�鍥炵悊鐢�', prop: 'tell' },
         { label: '鎷掓敹鍘熷洜', prop: 'sampleRejectRemark' },
@@ -471,6 +472,7 @@
       verifyDialogVisible: false,
       quashDialogVisible: false,
       filesDialogVisible: false, // 闄勪欢鏌ョ湅寮规
+      attachmentOrderId: null,
       tableDataFile: [],
       tableLoadingFile: false,
       columnFile: [
@@ -652,6 +654,7 @@
     },
     refresh() {
       this.resetForm('entity')
+      this.entity.sampleDate = null
       this.refreshTable()
     },
     pagination(page) {
@@ -842,12 +845,15 @@
     },
     // 闄勪欢鏌ョ湅
     handleFileLook(row) {
+      this.attachmentOrderId = row.id
+      this.pageFile.current = 1
+      this.tableDataFile = []
       this.filesDialogVisible = true
-      this.getFileList(row)
+      this.getFileList()
     },
-    getFileList(row) {
+    getFileList() {
       this.tableLoadingFile = true
-      getFileList({ insOrderId: row.id }).then(res => {
+      getFileList({ insOrderId: this.attachmentOrderId, current: this.pageFile.current, size: this.pageFile.size }).then(res => {
         this.tableLoadingFile = false
         if (res.code === 200) {
           this.tableDataFile = res.data.records
@@ -858,6 +864,7 @@
       })
     },
     paginationFile(page) {
+      this.pageFile.current = page.page
       this.pageFile.size = page.limit
       this.getFileList()
     },
@@ -866,7 +873,7 @@
       downFile({
         id: row.id,
       }).then(res => {
-        this.$download.saveAs(res.data.fileUrl, row.fileName);
+        return this.$download.attachment(res.data, row.fileName);
       }).catch(error => {
 
       })
diff --git a/src/views/business/reportPreparation/index.vue b/src/views/business/reportPreparation/index.vue
index c0669fc..2cb25ad 100644
--- a/src/views/business/reportPreparation/index.vue
+++ b/src/views/business/reportPreparation/index.vue
@@ -728,7 +728,7 @@
       downFile({
         id: row.id,
       }).then(res => {
-        this.$download.saveAs(res.data.fileUrl, row.fileName);
+        return this.$download.attachment(res.data, row.fileName);
       }).catch(error => {
 
       })
diff --git a/src/views/business/unpass/components/unPassDialog.vue b/src/views/business/unpass/components/unPassDialog.vue
index 23b6d4c..9551bb5 100644
--- a/src/views/business/unpass/components/unPassDialog.vue
+++ b/src/views/business/unpass/components/unPassDialog.vue
@@ -180,7 +180,7 @@
       downFile({
         id: row.id,
       }).then(res => {
-        this.$download.saveAs(res.data.fileUrl, row.fileName);
+        return this.$download.attachment(res.data, row.fileName);
       }).catch(error => {
 
       })
diff --git a/tests/attachment-download.test.mjs b/tests/attachment-download.test.mjs
new file mode 100644
index 0000000..48928e2
--- /dev/null
+++ b/tests/attachment-download.test.mjs
@@ -0,0 +1,172 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import fs from 'node:fs'
+import vm from 'node:vm'
+import { parse as parseVue } from '@vue/compiler-sfc'
+import { parse as parseJs } from 'acorn'
+
+const backend = 'https://lims.example/dev-api'
+const read = path => fs.readFileSync(new URL(path, import.meta.url), 'utf8')
+
+function withoutImports(source) {
+  const imports = parseJs(source, { sourceType: 'module', ecmaVersion: 'latest' }).body
+    .filter(node => node.type === 'ImportDeclaration')
+  for (const node of imports.reverse()) source = source.slice(0, node.start) + source.slice(node.end)
+  return source
+}
+
+function downloader() {
+  const saved = [], requests = [], errors = []
+  const state = { data: new Blob(['original bytes']), error: null, missingUrls: [] }
+  const context = vm.createContext({
+    URL, Blob, window: { location: { origin: 'https://lims.example' } },
+    getJavaApi: () => backend, getToken: () => 'test-token',
+    saveAs: (...args) => saved.push(args),
+    ElMessage: { error: message => errors.push(message) },
+    axios: { get: async (url, options) => {
+      requests.push({ url, ...options })
+      if (state.missingUrls.includes(url)) throw { response: { status: 404 } }
+      if (state.error) throw state.error
+      return { data: state.data }
+    } },
+  })
+  const helper = withoutImports(read('../src/utils/attachment.js')).replace(/^export /mg, '')
+  vm.runInContext(helper, context)
+  const plugin = withoutImports(read('../src/plugins/download.js'))
+    .replace('import.meta.env.VITE_APP_BASE_API', "'/dev-api'")
+    .replace('export default', 'globalThis.download =')
+  vm.runInContext(plugin, context)
+  return { context, saved, requests, errors, state, download: context.download }
+}
+
+test('鏂囦欢鍚嶈ˉ鍏呮枃妗�/鍥剧墖鐩綍锛屽苟浼樺厛閬靛惊闄勪欢鐨勫瓨鍌ㄧ被鍨�', () => {
+  const { context: c } = downloader()
+  for (const [file, type, directory] of [
+    ['report.docx', undefined, 'word'], ['report.pdf', 2, 'word'],
+    ['photo.PNG', undefined, 'img'], ['photo.webp', undefined, 'img'],
+    ['scan.bin', 1, 'img'], ['photo.png', 2, 'word'], ['photo.bmp', 0, 'img'],
+  ]) assert.equal(c.resolveAttachmentUrl(file, type), `${backend}/${directory}/${file}`)
+})
+
+test('宸叉湁闈欐�佽矾寰勪笌浠g悊鍓嶇紑涓嶄細閲嶅鎷兼帴', () => {
+  const { context: c } = downloader()
+  for (const path of ['word/a.docx', '/word/a.docx', '/dev-api/word/a.docx', `${backend}/word/a.docx`]) {
+    assert.equal(c.resolveAttachmentUrl(path), `${backend}/word/a.docx`)
+  }
+  assert.equal(c.resolveAttachmentUrl('/profile/upload/2026/a.pdf'), `${backend}/profile/upload/2026/a.pdf`)
+  assert.equal(c.resolveAttachmentUrl('word\\folder\\a.pdf'), `${backend}/word/folder/a.pdf`)
+})
+
+test('涓枃銆佺┖鏍笺��#銆�?銆佺櫨鍒嗗彿鍜屽凡鏈夌紪鐮佸潎鎸夌湡瀹炴枃浠跺悕涓嬭浇', () => {
+  const { context: c } = downloader()
+  for (const name of ['濮旀墭闄勪欢 #1?.docx', '娴撳害100%.pdf', 'a+b&c.pdf']) {
+    const expected = `${backend}/word/${encodeURIComponent(name)}`
+    assert.equal(c.resolveAttachmentUrl(name), expected)
+    assert.equal(c.resolveAttachmentUrl(expected), expected)
+  }
+  assert.equal(c.resolveAttachmentUrl('/word/folder/a%20b.pdf'), `${backend}/word/folder/a%20b.pdf`)
+})
+
+test('淇濈暀澶栭儴鍦板潃鏌ヨ鍙傛暟锛屾嫆缁濈┖鍦板潃鍙婃棤鏁堝崗璁�', () => {
+  const { context: c } = downloader()
+  const external = 'https://files.example/a.pdf?signature=abc#page=1'
+  assert.equal(c.resolveAttachmentUrl(external), external)
+  for (const value of [null, '', ' ', 'javascript:alert(1)']) assert.throws(() => c.resolveAttachmentUrl(value))
+})
+
+test('涓嬭浇鍚嶄繚鐣欏師濮嬫墿灞曞悕锛屾棤鍚嶇О鏃朵娇鐢ㄥ瓨鍌ㄦ枃浠跺悕', () => {
+  const { context: c } = downloader()
+  assert.equal(c.attachmentFileName(`${backend}/word/test.pdf`, '妫�楠岄檮浠�'), '妫�楠岄檮浠�.pdf')
+  assert.equal(c.attachmentFileName(`${backend}/word/test.docx`, '鍘熷璁板綍.docx'), '鍘熷璁板綍.docx')
+  assert.equal(c.attachmentFileName(`${backend}/word/a%20b.pdf`), 'a b.pdf')
+})
+
+test('鍥剧墖鍜屾枃妗d笅杞藉師濮� Blob锛屽苟浠呭悜涓氬姟鏈嶅姟鍣ㄥ彂閫佺櫥褰曞嚟鎹�', async () => {
+  const { download, saved, requests, state } = downloader()
+  assert.equal(await download.attachment({ fileUrl: '鍘熷浘.jpg', type: 1 }, '鍘熷浘.jpg'), true)
+  assert.equal(saved[0][0], state.data)
+  assert.equal(saved[0][1], '鍘熷浘.jpg')
+  assert.equal(requests[0].url, `${backend}/img/${encodeURIComponent('鍘熷浘.jpg')}`)
+  assert.equal(requests[0].responseType, 'blob')
+  assert.equal(requests[0].headers.Authorization, 'Bearer test-token')
+  await download.saveAs('https://files.example/a.docx?signature=abc', '闄勪欢.docx')
+  assert.equal(requests.length, 1)
+  assert.equal(saved[1][0], 'https://files.example/a.docx?signature=abc')
+})
+
+test('鎺ュ彛閿欒涓庨敊璇綉椤典笉淇濆瓨鎴愰檮浠讹紝鍚堟硶 JSON/HTML 闄勪欢鍙甯镐笅杞�', async () => {
+  const { download, saved, errors, state } = downloader()
+  state.data = new Blob(['{"code":500,"msg":"闄勪欢涓嶅瓨鍦�"}'], { type: 'application/json' })
+  assert.equal(await download.saveAs('missing.docx', 'missing.docx'), false)
+  assert.equal(errors.pop(), '闄勪欢涓嶅瓨鍦�')
+  state.data = new Blob(['<!doctype html><html>Not found</html>'], { type: 'text/html' })
+  assert.equal(await download.saveAs('missing.pdf'), false)
+  assert.equal(saved.length, 0)
+  state.data = new Blob(['{"hello":"world"}'], { type: 'application/json' })
+  assert.equal(await download.saveAs('data.json'), true)
+  state.data = new Blob(['<!doctype html><html>Document</html>'], { type: 'text/html' })
+  assert.equal(await download.saveAs('document.html'), true)
+})
+
+test('404銆佺櫥褰曞け鏁堝強绌烘枃浠跺湴鍧�鎻愮ず澶辫触锛屼笉鐢熸垚涓嬭浇鏂囦欢', async () => {
+  const { download, saved, errors, requests, state } = downloader()
+  for (const status of [404, 401, 403]) {
+    state.error = { response: { status } }
+    assert.equal(await download.saveAs('a.pdf'), false)
+  }
+  const count = requests.length
+  assert.equal(await download.attachment({ fileUrl: null }), false)
+  assert.equal(requests.length, count)
+  assert.equal(saved.length, 0)
+  assert.equal(errors.length, 4)
+})
+
+test('鐜版湁瀵煎嚭鎺ュ彛浼犲叆 Blob 鏃剁洿鎺ヤ繚瀛橈紝涓嶉澶栧彂璧疯姹�', () => {
+  const { download, saved, requests } = downloader()
+  const data = new Blob(['exported bytes'])
+  download.saveAs(data, '瀵煎嚭.xlsx')
+  assert.equal(saved[0][0], data)
+  assert.equal(saved[0][1], '瀵煎嚭.xlsx')
+  assert.equal(requests.length, 0)
+})
+
+test('鍏煎鍘嗗彶鍥剧墖淇濆瓨鐩綍宸紓锛屾槑纭寚瀹氬瓨鍌ㄧ被鍨嬬殑闄勪欢涓嶇寽娴嬪叾浠栫洰褰�', async () => {
+  const { download, saved, requests, state } = downloader()
+  state.missingUrls = [`${backend}/img/scan.bmp`]
+  assert.equal(await download.saveAs('scan.bmp', '鍘熸壂鎻忎欢.bmp'), true)
+  assert.equal(requests[1].url, `${backend}/word/scan.bmp`)
+  assert.equal(saved[0][1], '鍘熸壂鎻忎欢.bmp')
+  const count = requests.length
+  assert.equal(await download.attachment({ fileUrl: 'scan.bmp', type: 1 }), false)
+  assert.equal(requests.length, count + 1)
+})
+
+function orderComponent(getFileList) {
+  const { descriptor } = parseVue(read('../src/views/business/productOrder/index.vue'))
+  let source = descriptor.script.content
+  const ast = parseJs(source, { sourceType: 'module', ecmaVersion: 'latest' })
+  const edits = ast.body.filter(n => n.type === 'ImportDeclaration' || n.type === 'ExportDefaultDeclaration')
+    .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 })
+  vm.runInContext(source, context)
+  return { ...context.component.data(), ...context.component.methods }
+}
+
+test('涓嬪崟闄勪欢缈婚〉淇濈暀濮旀墭ID锛屽垏鎹㈠鎵樻椂閲嶇疆椤电爜鍜屾棫鍒楄〃', async () => {
+  const requests = []
+  const instance = orderComponent(params => {
+    requests.push(params)
+    return Promise.resolve({ code: 200, data: { records: [], total: 30 } })
+  })
+  instance.handleFileLook({ id: 123 })
+  instance.paginationFile({ page: 2, limit: 10 })
+  assert.equal(requests[1].insOrderId, 123)
+  assert.equal(requests[1].current, 2)
+  assert.equal(requests[1].size, 10)
+  instance.handleFileLook({ id: 456 })
+  assert.equal(requests[2].insOrderId, 456)
+  assert.equal(requests[2].current, 1)
+  assert.equal(instance.tableDataFile.length, 0)
+})

--
Gitblit v1.9.3