From a6fdbed4f31530c080b591d78c141e561db78967 Mon Sep 17 00:00:00 2001
From: liding <756868258@qq.com>
Date: 星期一, 28 九月 2026 09:35:01 +0800
Subject: [PATCH] feat(inspection): 更新检测类型选项并移除湿度字段 - 将"委托试验"改为"委托检验",增加委托检测、实验室间比对、能力验证等检测类型 - 移除所有表单中的湿度相关字段及验证逻辑 - 在报告准备界面添加印章选择功能,支持MA章、报告章、骑缝章的位置选择 - 优化报告结果流程界面,增加报告下载功能和领报告相关信息字段 - 新增印章管理系统,支持印章的增删改查和图片上传功能

---
 src/views/business/inspectionTask/components/InspectionWord.vue |   17 -
 src/views/structural/sealManage/index.vue                       |  314 ++++++++++++++++++++++++++
 src/views/business/inspectionReview/index.vue                   |    5 
 src/views/business/reportPreparation/index.vue                  |  173 ++++++++++++++
 src/api/cnas/process/reportResults.js                           |   10 
 src/views/CNAS/process/reportResults/index.vue                  |  130 ++++++++--
 src/views/business/inspectionView/index.vue                     |    9 
 src/views/business/inspectionTask/inspection.vue                |   12 
 src/views/business/inspectionTask/index.vue                     |   20 +
 9 files changed, 623 insertions(+), 67 deletions(-)

diff --git a/src/api/cnas/process/reportResults.js b/src/api/cnas/process/reportResults.js
index 1b1011f..4902f6d 100644
--- a/src/api/cnas/process/reportResults.js
+++ b/src/api/cnas/process/reportResults.js
@@ -46,3 +46,13 @@
     params: query,
   });
 }
+
+// 涓嬭浇鎵瑰噯鍚庣敓鎴愮殑鏈夌珷鎶ュ憡
+export function downloadStampedReport(id, format) {
+  return request({
+    url: "/processReport/downloadStampedReport",
+    method: "get",
+    params: { id, format },
+    responseType: "blob",
+  });
+}
diff --git a/src/views/CNAS/process/reportResults/index.vue b/src/views/CNAS/process/reportResults/index.vue
index 88b1c7c..1752de6 100644
--- a/src/views/CNAS/process/reportResults/index.vue
+++ b/src/views/CNAS/process/reportResults/index.vue
@@ -21,27 +21,13 @@
       <lims-table :tableData="tableData" :column="column" :tableLoading="tableLoading" :height="'calc(100vh - 270px)'"
         :page="page" @pagination="pagination"></lims-table>
     </div>
-    <el-dialog :title="title" :visible.sync="addDialogVisible" width="400px" top="6vh">
+    <el-dialog :title="title" :visible.sync="addDialogVisible" width="520px" top="6vh" :close-on-click-modal="false">
       <el-row>
         <el-col :span="24" style="margin-bottom: 16px;">
           <div class="search_thing">
             <div class="search_label">妫�楠屾姤鍛婄紪鍙凤細</div>
             <div class="search_input"><el-input size="small" placeholder="璇疯緭鍏�" clearable
                 v-model="addInfo.insReportCode"></el-input></div>
-          </div>
-        </el-col>
-        <el-col :span="24" style="margin-bottom: 16px;">
-          <div class="search_thing">
-            <div class="search_label">椤垫暟锛�</div>
-            <div class="search_input"><el-input size="small" placeholder="璇疯緭鍏�" clearable
-                v-model="addInfo.pages"></el-input></div>
-          </div>
-        </el-col>
-        <el-col :span="24" style="margin-bottom: 16px;">
-          <div class="search_thing">
-            <div class="search_label">鍙戦�佷唤鏁帮細</div>
-            <div class="search_input"><el-input size="small" placeholder="璇疯緭鍏�" clearable
-                v-model="addInfo.number"></el-input></div>
           </div>
         </el-col>
         <el-col :span="24" style="margin-bottom: 16px;">
@@ -55,7 +41,7 @@
           <div class="search_thing">
             <div class="search_label">鍙戦�佹柟寮忥細</div>
             <div class="search_input"><el-input size="small" placeholder="璇疯緭鍏�" clearable
-                v-model="addInfo.method"></el-input></div>
+                v-model="addInfo.method" :disabled="!!addInfo.inspectionOrderId"></el-input></div>
           </div>
         </el-col>
         <el-col :span="24" style="margin-bottom: 16px;">
@@ -66,6 +52,23 @@
                 value-format="yyyy-MM-dd" style="width: 100%;">
               </el-date-picker>
             </div>
+          </div>
+        </el-col>
+        <el-col :span="24" style="margin-bottom: 16px;">
+          <div class="search_thing">
+            <div class="search_label">棰嗘姤鍛婃棩鏈燂細</div>
+            <div class="search_input">
+              <el-date-picker v-model="addInfo.receiveDate" type="date" size="small" placeholder="閫夋嫨鏃ユ湡"
+                format="yyyy-MM-dd" value-format="yyyy-MM-dd" style="width: 100%;">
+              </el-date-picker>
+            </div>
+          </div>
+        </el-col>
+        <el-col :span="24" style="margin-bottom: 16px;">
+          <div class="search_thing">
+            <div class="search_label">棰嗘姤鍛婁汉锛�</div>
+            <div class="search_input"><el-input size="small" placeholder="璇疯緭鍏�" clearable
+                v-model="addInfo.receiveUser"></el-input></div>
           </div>
         </el-col>
         <el-col :span="24" style="margin-bottom: 16px;">
@@ -99,6 +102,22 @@
         <el-button type="primary" @click="handleAdd" :loading="addLoading">纭� 瀹�</el-button>
       </span>
     </el-dialog>
+    <el-dialog title="涓嬭浇鏈夌珷鎶ュ憡" :visible.sync="downloadDialogVisible" width="420px"
+               :close-on-click-modal="false" @closed="closeDownloadDialog">
+      <el-form label-width="90px">
+        <el-form-item label="鏂囦欢鏍煎紡锛�">
+          <el-radio-group v-model="selectedDownloadFormat">
+            <el-radio label="pdf">PDF锛堝惈楠戠紳绔狅級</el-radio>
+            <el-radio label="word">Word</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="downloadDialogVisible = false">鍙� 娑�</el-button>
+        <el-button type="primary" :loading="downloadingReportId !== null"
+                   @click="downloadSelectedReport">涓� 杞�</el-button>
+      </span>
+    </el-dialog>
   </div>
 </template>
 
@@ -109,7 +128,8 @@
   doProcessReport,
   addProcessReport,
   pageProcessReport,
-  delProcessReport
+  delProcessReport,
+  downloadStampedReport
 } from '@/api/cnas/process/reportResults.js'
 import {
   selectUserCondition,
@@ -129,20 +149,21 @@
       addDialogVisible: false,
       addInfo: {},
       addLoading: false,
+      downloadingReportId: null,
+      downloadDialogVisible: false,
+      downloadRow: null,
+      selectedDownloadFormat: 'pdf',
       queryParams: {
         insReportCode: ''
       },
       tableData: [],
       column: [
         { label: "妫�楠屾姤鍛婄紪鍙�", prop: "insReportCode" },
-        { label: "椤垫暟", prop: "pages", width: "120px" },
-        {
-          label: "鍙戦�佷唤鏁�",
-          prop: "number",
-        },
         { label: "鍙戝線浣曞", prop: "send" },
         { label: "鍙戦�佹柟寮�", prop: "method" },
         { label: "鍙戦�佹棩鏈�", prop: "sendTime" },
+        { label: "棰嗘姤鍛婃棩鏈�", prop: "receiveDate", width: "120px" },
+        { label: "棰嗘姤鍛婁汉", prop: "receiveUser", width: "110px" },
         { label: "鍙戦�佷汉", prop: "sendUserName" },
         { label: "绛炬敹浜�", prop: "signatory" },
         { label: "澶囨敞", prop: "remark" },
@@ -150,7 +171,16 @@
           dataType: "action",
           fixed: "right",
           label: "鎿嶄綔",
+          width: "180px",
           operation: [
+            {
+              name: "涓嬭浇",
+              type: "text",
+              disabled: (row) => this.downloadingReportId === row.id,
+              clickFun: (row) => {
+                this.handleDownload(row);
+              },
+            },
             {
               name: "缂栬緫",
               type: "text",
@@ -242,7 +272,7 @@
       delete this.addInfo.createTime
       delete this.addInfo.createUser
       delete this.addInfo.updateTime
-      delete this.addInfo.updateUserer
+      delete this.addInfo.updateUser
       delete this.addInfo.sendUserName
       if (this.title == '鏂板') {
         addProcessReport(this.addInfo).then(res => {
@@ -253,7 +283,9 @@
             message: '鎿嶄綔鎴愬姛!'
           });
           this.refreshTable()
-        }).catch(err => { })
+        }).catch(() => {
+          this.addLoading = false
+        })
       } else {
         doProcessReport(this.addInfo).then(res => {
           this.addLoading = false
@@ -263,15 +295,59 @@
             message: '鎿嶄綔鎴愬姛!'
           });
           this.refreshTable()
-        }).catch(err => { })
+        }).catch(() => {
+          this.addLoading = false
+        })
       }
     },
     handleWork(row) {
       this.title = '淇敼'
-      this.addInfo = row
-      console.log(this.addInfo)
+      this.addInfo = { ...row }
       this.addDialogVisible = true;
     },
+    handleDownload(row) {
+      this.downloadRow = row
+      this.selectedDownloadFormat = 'pdf'
+      this.downloadDialogVisible = true
+    },
+    closeDownloadDialog() {
+      if (this.downloadingReportId === null) {
+        this.downloadRow = null
+        this.selectedDownloadFormat = 'pdf'
+      }
+    },
+    downloadSelectedReport() {
+      if (!this.downloadRow) {
+        return
+      }
+      const row = this.downloadRow
+      const format = this.selectedDownloadFormat
+      this.downloadingReportId = row.id
+      downloadStampedReport(row.id, format).then(res => {
+        if (res.type && res.type.includes('application/json')) {
+          const reader = new FileReader()
+          reader.onload = () => {
+            try {
+              this.$message.error(JSON.parse(reader.result).msg || '鏈夌珷鎶ュ憡涓嬭浇澶辫触')
+            } catch (e) {
+              this.$message.error('鏈夌珷鎶ュ憡涓嬭浇澶辫触')
+            }
+          }
+          reader.readAsText(res)
+          return
+        }
+        const extension = format === 'word' ? 'docx' : 'pdf'
+        this.$download.saveAs(res, `${row.insReportCode || '鏈夌珷鎶ュ憡'}.${extension}`)
+        this.downloadDialogVisible = false
+      }).catch(() => {
+        this.$message.error('鏈夌珷鎶ュ憡涓嬭浇澶辫触')
+      }).finally(() => {
+        this.downloadingReportId = null
+        if (!this.downloadDialogVisible) {
+          this.downloadRow = null
+        }
+      })
+    },
     handleDelete(row) {
       this.$confirm("鏄惁鍒犻櫎璇ユ潯鏁版嵁?", "鎻愮ず", {
         confirmButtonText: "纭畾",
diff --git a/src/views/business/inspectionReview/index.vue b/src/views/business/inspectionReview/index.vue
index a054dbc..ccc5887 100644
--- a/src/views/business/inspectionReview/index.vue
+++ b/src/views/business/inspectionReview/index.vue
@@ -273,7 +273,10 @@
       fullscreen: false,
       option: null,
       orderTypeList: [
-        {label: '濮旀墭璇曢獙', value: 'Customer-ordered test', type: 'success'},
+        {label: '濮旀墭妫�娴�', value: 'Commissioned testing', type: 'success'},
+        {label: '瀹為獙瀹ら棿姣斿', value: 'Inter-laboratory comparison', type: 'warning'},
+        {label: '鑳藉姏楠岃瘉', value: 'Proficiency testing', type: 'primary'},
+        {label: '濮旀墭妫�楠�', value: 'Customer-ordered test', type: 'success'},
         {label: '鎶芥', value: '鎶芥', type: 'danger'},
         {label: '杩涘巶妫�楠�', value: '杩涘巶妫�楠�', type: 'info'},
         {label: '瀛e害妫�楠�', value: 'Quarterly inspection', type: ''},
diff --git a/src/views/business/inspectionTask/components/InspectionWord.vue b/src/views/business/inspectionTask/components/InspectionWord.vue
index d9ff99f..f8dbf8e 100644
--- a/src/views/business/inspectionTask/components/InspectionWord.vue
+++ b/src/views/business/inspectionTask/components/InspectionWord.vue
@@ -33,11 +33,6 @@
                 style="width: 90px" @change="m => subOtherForm(m, 'temperature')"></el-input>
               <span class="template-panel__unit">鈩�</span>
             </el-form-item>
-            <el-form-item label="婀垮害:">
-              <el-input v-model="currentOtherForm.humidity" :disabled="state > 1" placeholder="" size="small"
-                style="width: 90px" @change="m => subOtherForm(m, 'humidity')"></el-input>
-              <span class="template-panel__unit">%RH</span>
-            </el-form-item>
             <el-form-item label="妫�娴嬫椂闂�:">
               <el-date-picker v-model="currentOtherForm.detectionTime" :disabled="state > 1" type="date"
                 value-format="yyyy-MM-dd" placeholder="璇烽�夋嫨鏃ユ湡" size="small" style="width: 160px"
@@ -383,7 +378,7 @@
       numOptions: [],
       temperatureOptions: [],
       wareTableData: [],
-      // 妫�娴嬫潯浠舵寜妯℃澘 id 鍒嗙粍锛歿 [templateId]: { temperature, humidity, detectionTime, detectionPlace, deviceIds } }
+      // 妫�娴嬫潯浠舵寜妯℃澘 id 鍒嗙粍锛歿 [templateId]: { temperature, detectionTime, detectionPlace, deviceIds } }
       otherForm: {},
       deviceOptions: [],
       equipForm: {
@@ -465,7 +460,6 @@
     currentOtherForm() {
       return this.otherForm[this.currentTable] || {
         temperature: null,
-        humidity: null,
         detectionTime: null,
         detectionPlace: null,
         deviceIds: [],
@@ -512,7 +506,6 @@
         conditionList.forEach(item => {
           this.$set(this.otherForm, item.templateId, {
             temperature: item.temperature != null ? item.temperature : null,
-            humidity: item.humidity != null ? item.humidity : null,
             detectionTime: item.detectionTime != null ? item.detectionTime : null,
             detectionPlace: item.detectionPlace != null ? item.detectionPlace : null,
             deviceIds: Array.isArray(item.deviceIds) ? [...item.deviceIds] : [],
@@ -577,7 +570,7 @@
             if (this.tableLists && this.tableLists.length > 0) {
               this.tableList = null;
               this.tableList = this.tableLists.filter(m => m.templateId == val1)
-              // 妯℃澘闆嗗悎鍙兘鍙樺寲锛岃ˉ榻愭俯搴︺�佹箍搴︽Ы浣�
+              // 妯℃澘闆嗗悎鍙兘鍙樺寲锛岃ˉ榻愭俯搴︽Ы浣�
               this.tableLists.forEach(m => this.ensureOtherForm(m.templateId))
               // 瀵规ā鏉胯繘琛屽鐞�
               this.handleTableData()
@@ -910,7 +903,7 @@
         this.tableList = null;
         this.tableList = [this.tableLists[0]]
         this.currentTable = this.tableLists[0].templateId;
-        // 琛ラ綈姣忎釜妯℃澘鐨勬俯搴︺�佹箍搴︽Ы浣�
+        // 琛ラ綈姣忎釜妯℃澘鐨勬俯搴︽Ы浣�
         this.tableLists.forEach(m => this.ensureOtherForm(m.templateId))
         // 澶勭悊椤甸潰鍒楄〃鏁版嵁
         this.handleTableData()
@@ -941,7 +934,7 @@
         this.tableList = [this.tableLists[0]]
         this.currentTable = this.tableLists[0].templateId;
         this.currentSample.insProduct = this.HaveJson(list)
-        // 琛ラ綈姣忎釜妯℃澘鐨勬俯搴︺�佹箍搴︽Ы浣�
+        // 琛ラ綈姣忎釜妯℃澘鐨勬俯搴︽Ы浣�
         this.tableLists.forEach(m => this.ensureOtherForm(m.templateId))
         // 澶勭悊椤甸潰鍒楄〃鏁版嵁
         this.handleTableData()
@@ -1428,7 +1421,6 @@
       if (!this.otherForm[templateId]) {
         this.$set(this.otherForm, templateId, {
           temperature: null,
-          humidity: null,
           detectionTime: null,
           detectionPlace: null,
           deviceIds: [],
@@ -1462,7 +1454,6 @@
       this.tableLists.forEach(m => {
         const form = this.otherForm[m.templateId] || {}
         if (!form.temperature) missing.push(`璇峰~鍐欍��${m.templateName}銆嶇殑娓╁害`)
-        if (!form.humidity) missing.push(`璇峰~鍐欍��${m.templateName}銆嶇殑婀垮害`)
         if (!form.detectionTime) missing.push(`璇烽�夋嫨銆�${m.templateName}銆嶇殑妫�娴嬫椂闂碻)
         if (!form.detectionPlace) missing.push(`璇峰~鍐欍��${m.templateName}銆嶇殑妫�娴嬪湴鐐筦)
         if (!form.deviceIds || form.deviceIds.length === 0) {
diff --git a/src/views/business/inspectionTask/index.vue b/src/views/business/inspectionTask/index.vue
index 5cd38be..68b91cf 100644
--- a/src/views/business/inspectionTask/index.vue
+++ b/src/views/business/inspectionTask/index.vue
@@ -304,11 +304,29 @@
       orderTypeList: [
         {
           value: "Customer-ordered test",
-          label: "濮旀墭璇曢獙",
+          label: "濮旀墭妫�楠�",
           type: "success",
           effect: "plain",
         },
         {
+          value: "Commissioned testing",
+          label: "濮旀墭妫�娴�",
+          type: "success",
+          effect: "plain",
+        },
+        {
+          value: "Inter-laboratory comparison",
+          label: "瀹為獙瀹ら棿姣斿",
+          type: "warning",
+          effect: "plain",
+        },
+        {
+          value: "Proficiency testing",
+          label: "鑳藉姏楠岃瘉",
+          type: "primary",
+          effect: "plain",
+        },
+        {
           value: "鎶芥",
           label: "鎶芥",
           type: "",
diff --git a/src/views/business/inspectionTask/inspection.vue b/src/views/business/inspectionTask/inspection.vue
index 64d1911..a660bcd 100644
--- a/src/views/business/inspectionTask/inspection.vue
+++ b/src/views/business/inspectionTask/inspection.vue
@@ -184,11 +184,6 @@
               style="width: 90px" @change="(m) => subOtherForm(m, 'temperature')"></el-input>
             <span class="template-panel__unit">鈩�</span>
           </el-form-item>
-          <el-form-item v-if="insOrder.ifsOrderType!=='02wg'" label="婀垮害:">
-            <el-input v-model="currentOtherForm.humidity" :disabled="state > 1" placeholder="" size="small"
-              style="width: 90px" @change="(m) => subOtherForm(m, 'humidity')"></el-input>
-            <span class="template-panel__unit">%RH</span>
-          </el-form-item>
           <el-form-item label="妫�娴嬫椂闂�:">
             <el-date-picker v-model="currentOtherForm.detectionTime" :disabled="state > 1" type="date"
               value-format="yyyy-MM-dd" placeholder="璇烽�夋嫨鏃ユ湡" size="small" style="width: 160px"
@@ -702,7 +697,7 @@
         inspectionItem: 1,
         inspectionItemSubclass: "20(甯告俯)",
       },
-      // 妫�娴嬫潯浠朵笌妫�楠岀粨璁烘寜妯℃澘 id 鍒嗙粍锛歿 [templateId]: { temperature, humidity, detectionTime, detectionPlace, deviceIds, insResult } }
+      // 妫�娴嬫潯浠朵笌妫�楠岀粨璁烘寜妯℃澘 id 鍒嗙粍锛歿 [templateId]: { temperature, detectionTime, detectionPlace, deviceIds, insResult } }
       otherForm: {},
       equipForm: {
         value0: null,
@@ -861,7 +856,6 @@
       return (
         this.otherForm[this.currentTable] || {
           temperature: null,
-          humidity: null,
           detectionTime: null,
           detectionPlace: null,
           deviceIds: [],
@@ -1152,7 +1146,6 @@
         (this.insOrder.templateConditions || []).forEach((item) => {
           this.$set(this.otherForm, item.templateId, {
             temperature: item.temperature != null ? item.temperature : null,
-            humidity: item.humidity != null ? item.humidity : null,
             detectionTime: item.detectionTime != null ? item.detectionTime : null,
             detectionPlace: item.detectionPlace != null ? item.detectionPlace : null,
             deviceIds: Array.isArray(item.deviceIds) ? [...item.deviceIds] : [],
@@ -1669,7 +1662,6 @@
       if (!this.otherForm[templateId]) {
         this.$set(this.otherForm, templateId, {
           temperature: null,
-          humidity: null,
           detectionTime: null,
           detectionPlace: null,
           deviceIds: [],
@@ -2265,7 +2257,6 @@
         const form = this.otherForm[m.templateId] || {};
         if (this.insOrder.ifsOrderType !== "02wg") {
           if (!form.temperature) missing.push(`璇峰~鍐欍��${m.templateName}銆嶇殑娓╁害`);
-          if (!form.humidity) missing.push(`璇峰~鍐欍��${m.templateName}銆嶇殑婀垮害`);
         }
         if (!form.detectionTime) missing.push(`璇烽�夋嫨銆�${m.templateName}銆嶇殑妫�娴嬫椂闂碻);
         if (!form.detectionPlace) missing.push(`璇峰~鍐欍��${m.templateName}銆嶇殑妫�娴嬪湴鐐筦);
@@ -2393,7 +2384,6 @@
             id: this.insOrder.id,
             templateId: template.templateId,
             temperature: form.temperature == null ? null : form.temperature,
-            humidity: form.humidity == null ? null : form.humidity,
             detectionTime: form.detectionTime || null,
             detectionPlace: form.detectionPlace || null,
             deviceIds: Array.isArray(form.deviceIds) ? [...form.deviceIds] : [],
diff --git a/src/views/business/inspectionView/index.vue b/src/views/business/inspectionView/index.vue
index ca8bc0e..259c9ea 100644
--- a/src/views/business/inspectionView/index.vue
+++ b/src/views/business/inspectionView/index.vue
@@ -144,11 +144,6 @@
                       style="width: 90px" @change="m => subOtherForm(m, 'temperature')" />
             <span class="template-panel__unit">鈩�</span>
           </el-form-item>
-          <el-form-item v-if="insOrder.ifsOrderType !== '02wg'" label="婀垮害:">
-            <el-input v-model="currentOtherForm.humidity" :disabled="state > 1" size="small"
-                      style="width: 90px" @change="m => subOtherForm(m, 'humidity')" />
-            <span class="template-panel__unit">%RH</span>
-          </el-form-item>
           <el-form-item label="妫�娴嬫椂闂�:">
             <el-date-picker v-model="currentOtherForm.detectionTime" :disabled="state > 1" type="date"
                             value-format="yyyy-MM-dd" placeholder="璇烽�夋嫨鏃ユ湡" size="small" style="width: 160px"
@@ -753,7 +748,6 @@
     currentOtherForm() {
       return this.otherForm[this.currentTable] || {
         temperature: null,
-        humidity: null,
         detectionTime: null,
         detectionPlace: null,
         deviceIds: [],
@@ -950,7 +944,6 @@
         (this.insOrder.templateConditions || []).forEach((item) => {
           this.$set(this.otherForm, item.templateId, {
             temperature: item.temperature != null ? item.temperature : null,
-            humidity: item.humidity != null ? item.humidity : null,
             detectionTime: item.detectionTime != null ? item.detectionTime : null,
             detectionPlace: item.detectionPlace != null ? item.detectionPlace : null,
             deviceIds: Array.isArray(item.deviceIds) ? [...item.deviceIds] : [],
@@ -1464,7 +1457,6 @@
       if (templateId == null || this.otherForm[templateId]) return;
       this.$set(this.otherForm, templateId, {
         temperature: null,
-        humidity: null,
         detectionTime: null,
         detectionPlace: null,
         deviceIds: [],
@@ -2116,7 +2108,6 @@
         const form = this.otherForm[item.templateId] || {};
         if (this.insOrder.ifsOrderType !== "02wg") {
           if (!form.temperature) missing.push(`璇峰~鍐欍��${item.templateName}銆嶇殑娓╁害`);
-          if (!form.humidity) missing.push(`璇峰~鍐欍��${item.templateName}銆嶇殑婀垮害`);
         }
         if (!form.detectionTime) missing.push(`璇烽�夋嫨銆�${item.templateName}銆嶇殑妫�娴嬫椂闂碻);
         if (!form.detectionPlace) missing.push(`璇峰~鍐欍��${item.templateName}銆嶇殑妫�娴嬪湴鐐筦);
diff --git a/src/views/business/reportPreparation/index.vue b/src/views/business/reportPreparation/index.vue
index ccce7c9..d7b3314 100644
--- a/src/views/business/reportPreparation/index.vue
+++ b/src/views/business/reportPreparation/index.vue
@@ -130,8 +130,42 @@
            @click="fullscreen = true;"></i>
         <img v-else alt="" src="@/assets/images/no-full.svg" style="cursor: pointer;" @click="fullscreen = false;">
       </div>
-      <div v-if="approveVisible" style="height: 70vh;">
+      <div v-if="approveVisible" class="approve-preview">
         <onlyoffice ref="onlyoffice" :options="option" style="width: 100%;height: 100%;"/>
+      </div>
+      <div class="seal-select-block" v-loading="sealLoading">
+        <div class="seal-select-title">
+          <span>鎸変綅缃�夋嫨鍗扮珷</span>
+          <span class="seal-select-tip">鑷姩鍖归厤 MA绔� / 1鍙风珷 / 楠戠紳绔狅紝姣忛」鍙崟鐙皟鏁存垨娓呯┖</span>
+        </div>
+        <div v-if="sealOptions.length" class="seal-position-grid">
+          <div v-for="position in sealPositions" :key="position.key" class="seal-position-card">
+            <div class="seal-position-name">{{ position.label }}</div>
+            <div class="seal-position-desc">{{ position.description }}</div>
+            <el-select
+              v-model="selectedSealPositions[position.key]"
+              clearable
+              placeholder="涓嶇洊绔�"
+              style="width: 100%"
+            >
+              <el-option
+                v-for="seal in sealOptions"
+                :key="seal.id"
+                :label="seal.name"
+                :value="Number(seal.id)"
+              />
+            </el-select>
+            <div v-if="getSelectedSeal(position.key)" class="seal-position-preview">
+              <img
+                :src="javaApi + '/img/' + getSelectedSeal(position.key).address"
+                :alt="getSelectedSeal(position.key).name"
+              >
+              <span>{{ getSelectedSeal(position.key).name }}</span>
+            </div>
+            <div v-else class="seal-position-empty">璇ヤ綅缃笉鐩栫珷</div>
+          </div>
+        </div>
+        <el-empty v-if="!sealLoading && sealOptions.length === 0" description="鏆傛棤鍚敤鍗扮珷锛岃鍏堝湪鍗扮珷绠$悊涓淮鎶�" :image-size="54" />
       </div>
       <span slot="footer" class="dialog-footer">
         <el-button :disabled="loadingApprove" @click="approveReasonVisible = true">涓嶆壒鍑�</el-button>
@@ -249,6 +283,7 @@
 import {selectUserCondition} from "@/api/business/inspectionTask";
 import limsTable from "@/components/Table/lims-table.vue";
 import {delFile, downFile, getFileList} from "@/api/business/rawMaterialOrder";
+import {selectEnabledSeals} from "@/api/structural/sealConfig";
 
 export default {
   name: 'ReportPreparation',
@@ -276,6 +311,18 @@
       issuedReasonVisible: false,
       approveVisible: false,
       approveReasonVisible: false,
+      sealOptions: [],
+      sealPositions: [
+        { key: 'maSealId', label: 'MA鏍囧織绔�', description: 'PDF 绗�1椤靛乏涓婅' },
+        { key: 'reportSealId', label: '鎶ュ憡绔�', description: 'Word 妯℃澘鍚勯〉 seal1 / seal2 棰勭暀浣嶇疆' },
+        { key: 'seamSealId', label: '楠戠紳绔�', description: 'PDF 姣忛〉鍙充晶涓棿浣嶇疆' }
+      ],
+      selectedSealPositions: {
+        maSealId: null,
+        reportSealId: null,
+        seamSealId: null
+      },
+      sealLoading: false,
       fullscreen: false,
       loadingApproveReason: false,
       loadingApprove: false,
@@ -307,7 +354,10 @@
         {label: '鍘熸潗鏂欎笅鍗�', value: 1},
       ],
       orderTypeList: [
-        {label: '濮旀墭璇曢獙', value: 'Customer-ordered test'},
+        {label: '濮旀墭妫�娴�', value: 'Commissioned testing'},
+        {label: '瀹為獙瀹ら棿姣斿', value: 'Inter-laboratory comparison'},
+        {label: '鑳藉姏楠岃瘉', value: 'Proficiency testing'},
+        {label: '濮旀墭妫�楠�', value: 'Customer-ordered test'},
         {label: '鎶芥', value: '鎶芥'},
         // { label: '杩涘巶妫�楠�', value: '杩涘巶妫�楠�' },
         // { label: '瀛e害妫�楠�', value: 'Quarterly inspection' },
@@ -986,6 +1036,7 @@
     // 鎵瑰噯鎸夐挳
     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 === '') {
@@ -1009,10 +1060,16 @@
     },
     // 鎵瑰噯閫氳繃
     subApprove() {
+      const selectedIds = Object.values(this.selectedSealPositions).filter(id => id != null)
+      if (!selectedIds.length) {
+        return this.$message.error('璇疯嚦灏戜负涓�涓綅缃�夋嫨鍗扮珷')
+      }
       this.loadingApprove = true;
       ratifyReport({
         id: this.currentInfo.id,
-        isRatify: 1
+        isRatify: 1,
+        ...this.selectedSealPositions,
+        sealIds: [...new Set(selectedIds)].join(',')
       }).then(res => {
         this.loadingApprove = false;
         this.$message.success('宸叉壒鍑�')
@@ -1022,6 +1079,39 @@
       }).catch(() => {
         this.loadingApprove = false;
       })
+    },
+    loadEnabledSeals(row) {
+      this.sealLoading = true
+      this.sealOptions = []
+      this.selectedSealPositions = { maSealId: null, reportSealId: null, seamSealId: null }
+      selectEnabledSeals().then(res => {
+        this.sealOptions = res.data || []
+        const availableIds = this.sealOptions.map(item => Number(item.id))
+        const legacyIds = String((row && row.sealIds) || '')
+          .split(',')
+          .map(item => Number(item.trim()))
+          .filter(id => availableIds.includes(id))
+        const legacyId = legacyIds[0] || null
+        const maSeal = this.sealOptions.find(item => String(item.name).toUpperCase().includes('MA'))
+        const reportSeal = this.sealOptions.find(item => item.name === '1鍙风珷')
+          || this.sealOptions.find(item => !String(item.name).toUpperCase().includes('MA') && !String(item.name).includes('楠戠紳'))
+          || this.sealOptions[0]
+        const seamSeal = this.sealOptions.find(item => String(item.name).includes('楠戠紳'))
+        this.selectedSealPositions = {
+          maSealId: availableIds.includes(Number(row && row.maSealId))
+            ? Number(row.maSealId) : (maSeal ? Number(maSeal.id) : null),
+          reportSealId: availableIds.includes(Number(row && row.reportSealId))
+            ? Number(row.reportSealId) : (legacyId || (reportSeal ? Number(reportSeal.id) : null)),
+          seamSealId: availableIds.includes(Number(row && row.seamSealId))
+            ? Number(row.seamSealId) : (seamSeal ? Number(seamSeal.id) : legacyId)
+        }
+      }).finally(() => {
+        this.sealLoading = false
+      })
+    },
+    getSelectedSeal(positionKey) {
+      const sealId = this.selectedSealPositions[positionKey]
+      return this.sealOptions.find(item => Number(item.id) === Number(sealId))
     },
     handle(row) {
       this.getAuthorizedPerson()
@@ -1080,7 +1170,7 @@
       ratifyReport({
         id: this.currentInfo.id,
         isRatify: 0,
-        examineTell: this.reason
+        ratifyTell: this.reason
       }).then(res => {
         this.loadingApproveReason = false
         this.$message.success('鎿嶄綔鎴愬姛')
@@ -1091,7 +1181,7 @@
         this.approveReasonVisible = false;
       }).catch(e => {
         this.$message.error('鎿嶄綔澶辫触')
-        this.loadingIssuedReason = false;
+        this.loadingApproveReason = false;
       })
     }
   }
@@ -1156,4 +1246,77 @@
 .fullscreen {
   height: 82vh
 }
+
+.approve-preview {
+  height: 52vh;
+}
+
+.seal-select-block {
+  margin-top: 16px;
+  padding: 14px 16px;
+  border: 1px solid #ebeef5;
+  border-radius: 6px;
+  background: #fafafa;
+}
+
+.seal-select-title {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+  color: #303133;
+  font-weight: 600;
+}
+
+.seal-select-tip {
+  color: #909399;
+  font-size: 12px;
+  font-weight: normal;
+}
+
+.seal-position-grid {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 14px;
+}
+
+.seal-position-card {
+  padding: 14px;
+  border: 1px solid #dcdfe6;
+  border-radius: 6px;
+}
+
+.seal-position-name {
+  color: #303133;
+  font-weight: 600;
+}
+
+.seal-position-desc {
+  margin: 4px 0 10px;
+  color: #909399;
+  font-size: 12px;
+}
+
+.seal-position-preview {
+  display: flex;
+  align-items: center;
+  margin-top: 12px;
+  color: #606266;
+}
+
+.seal-position-preview img {
+  width: 70px;
+  height: 70px;
+  margin-right: 10px;
+  object-fit: contain;
+}
+
+.seal-position-empty {
+  height: 70px;
+  margin-top: 12px;
+  color: #c0c4cc;
+  font-size: 12px;
+  line-height: 70px;
+  text-align: center;
+}
 </style>
diff --git a/src/views/structural/sealManage/index.vue b/src/views/structural/sealManage/index.vue
new file mode 100644
index 0000000..e9e3e2b
--- /dev/null
+++ b/src/views/structural/sealManage/index.vue
@@ -0,0 +1,314 @@
+<template>
+  <div class="app-container seal-manage">
+    <div class="search-bar">
+      <el-form ref="queryForm" :inline="true" :model="queryParams" size="small">
+        <el-form-item label="鍗扮珷鍚嶇О" prop="name">
+          <el-input v-model="queryParams.name" clearable placeholder="璇疯緭鍏�" @keyup.enter.native="refreshTable" />
+        </el-form-item>
+        <el-form-item label="鐘舵��" prop="status">
+          <el-select v-model="queryParams.status" clearable placeholder="鍏ㄩ儴">
+            <el-option label="鍚敤" :value="1" />
+            <el-option label="鍋滅敤" :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" size="mini" @click="refreshTable">鏌ヨ</el-button>
+          <el-button size="mini" @click="resetQuery">閲嶇疆</el-button>
+        </el-form-item>
+      </el-form>
+      <div class="toolbar">
+        <el-button type="primary" size="small" icon="el-icon-plus" @click="openForm()">鏂板</el-button>
+        <el-button type="primary" size="small" icon="el-icon-download" :loading="exportLoading" @click="handleExport">
+          瀵煎嚭
+        </el-button>
+      </div>
+    </div>
+
+    <lims-table
+      :table-data="tableData"
+      :column="columns"
+      :page="page"
+      :table-loading="tableLoading"
+      height="calc(100vh - 245px)"
+      @pagination="pagination"
+    />
+
+    <el-dialog
+      :title="formTitle"
+      :visible.sync="formVisible"
+      width="620px"
+      :close-on-click-modal="false"
+      @closed="resetSealForm"
+    >
+      <el-form ref="sealForm" :model="form" :rules="rules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="鍗扮珷鍚嶇О" prop="name">
+              <el-input v-model="form.name" maxlength="64" placeholder="璇疯緭鍏ュ嵃绔犲悕绉�" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="鍗扮珷鍥剧墖" prop="address">
+          <el-upload
+            ref="sealUpload"
+            class="seal-uploader"
+            :action="uploadAction"
+            :headers="uploadHeader"
+            accept="image/jpeg,image/png"
+            :show-file-list="false"
+            :before-upload="beforeUpload"
+            :on-success="handleUploadSuccess"
+            :on-error="handleUploadError"
+          >
+            <img v-if="form.address" :src="sealImageUrl(form.address)" class="seal-preview" alt="鍗扮珷鍥剧墖">
+            <div v-else class="seal-upload-placeholder">
+              <i class="el-icon-upload" />
+              <span>鐐瑰嚮涓婁紶鍗扮珷鍥剧墖</span>
+            </div>
+          </el-upload>
+          <div class="upload-tip">浠呮敮鎸� jpg / png锛屽ぇ灏忎笉瓒呰繃 2MB</div>
+        </el-form-item>
+        <el-form-item label="浣跨敤鑼冨洿" prop="useScope">
+          <el-input v-model="form.useScope" maxlength="255" placeholder="璇疯緭鍏ヤ娇鐢ㄨ寖鍥�" />
+        </el-form-item>
+        <el-form-item label="鐘舵��" prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio :label="1">鍚敤</el-radio>
+            <el-radio :label="0">鍋滅敤</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="澶囨敞" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="3" maxlength="255" show-word-limit />
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="formVisible = false">鍙� 娑�</el-button>
+        <el-button type="primary" :loading="submitLoading" @click="submitForm">纭� 瀹�</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import limsTable from '@/components/Table/lims-table.vue'
+import { addSeal, delSeal, exportSeal, pageSeal, updateSeal } from '@/api/structural/sealConfig'
+
+function emptyForm() {
+  return {
+    id: null,
+    name: '',
+    address: '',
+    useScope: '',
+    status: 1,
+    remark: ''
+  }
+}
+
+export default {
+  name: 'SealManage',
+  components: { limsTable },
+  data() {
+    return {
+      queryParams: {
+        name: '',
+        status: null
+      },
+      page: {
+        total: 0,
+        size: 20,
+        current: 1
+      },
+      tableData: [],
+      tableLoading: false,
+      exportLoading: false,
+      formVisible: false,
+      submitLoading: false,
+      form: emptyForm(),
+      rules: {
+        name: [{ required: true, message: '璇疯緭鍏ュ嵃绔犲悕绉�', trigger: 'blur' }],
+        address: [{ required: true, message: '璇蜂笂浼犲嵃绔犲浘鐗�', trigger: 'change' }]
+      },
+      columns: [
+        { label: '鍗扮珷缂栧彿', prop: 'code', width: '110px' },
+        { label: '鍗扮珷鍚嶇О', prop: 'name', width: '130px' },
+        { label: '鍗扮珷鍥剧墖', prop: 'address', dataType: 'image', width: '100px' },
+        { label: '浣跨敤鑼冨洿', prop: 'useScope' },
+        {
+          label: '鐘舵��',
+          prop: 'status',
+          width: '90px',
+          dataType: 'tag',
+          formatData: value => Number(value) === 1 ? '鍚敤' : '鍋滅敤',
+          formatType: value => Number(value) === 1 ? 'success' : 'info'
+        },
+        { label: '澶囨敞', prop: 'remark' },
+        { label: '鍒涘缓鏃堕棿', prop: 'createTime', width: '165px' },
+        {
+          label: '鎿嶄綔',
+          dataType: 'action',
+          fixed: 'right',
+          width: '140px',
+          operation: [
+            { name: '缂栬緫', type: 'text', clickFun: row => this.openForm(row) },
+            { name: '鍒犻櫎', type: 'text', clickFun: row => this.handleDelete(row) }
+          ]
+        }
+      ]
+    }
+  },
+  computed: {
+    formTitle() {
+      return this.form.id ? '缂栬緫鍗扮珷' : '鏂板鍗扮珷'
+    },
+    uploadAction() {
+      return this.javaApi + '/sealConfig/upload'
+    }
+  },
+  mounted() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.tableLoading = true
+      const params = { ...this.queryParams, current: this.page.current, size: this.page.size }
+      pageSeal(params).then(res => {
+        this.tableData = res.data.records || []
+        this.page.total = res.data.total || 0
+      }).finally(() => {
+        this.tableLoading = false
+      })
+    },
+    refreshTable() {
+      this.page.current = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.page.current = 1
+      this.getList()
+    },
+    pagination({ page, limit }) {
+      this.page.current = page
+      this.page.size = limit
+      this.getList()
+    },
+    openForm(row) {
+      this.form = row ? { ...row } : emptyForm()
+      this.formVisible = true
+      this.$nextTick(() => this.$refs.sealForm && this.$refs.sealForm.clearValidate())
+    },
+    resetSealForm() {
+      this.form = emptyForm()
+      if (this.$refs.sealUpload) this.$refs.sealUpload.clearFiles()
+      if (this.$refs.sealForm) this.$refs.sealForm.clearValidate()
+    },
+    submitForm() {
+      this.$refs.sealForm.validate(valid => {
+        if (!valid) return
+        this.submitLoading = true
+        const request = this.form.id ? updateSeal : addSeal
+        request(this.form).then(() => {
+          this.$message.success(this.form.id ? '淇敼鎴愬姛' : '鏂板鎴愬姛')
+          this.formVisible = false
+          this.getList()
+        }).finally(() => {
+          this.submitLoading = false
+        })
+      })
+    },
+    handleDelete(row) {
+      this.$confirm(`纭鍒犻櫎鍗扮珷鈥�${row.name}鈥濆悧锛焋, '鎻愮ず', {
+        confirmButtonText: '纭畾',
+        cancelButtonText: '鍙栨秷',
+        type: 'warning'
+      }).then(() => delSeal(row.id)).then(() => {
+        this.$message.success('鍒犻櫎鎴愬姛')
+        if (this.tableData.length === 1 && this.page.current > 1) this.page.current -= 1
+        this.getList()
+      }).catch(() => {})
+    },
+    handleExport() {
+      this.exportLoading = true
+      exportSeal(this.queryParams).then(data => {
+        this.$download.saveAs(new Blob([data]), '鍗扮珷绠$悊.xlsx')
+      }).finally(() => {
+        this.exportLoading = false
+      })
+    },
+    beforeUpload(file) {
+      const validType = ['image/jpeg', 'image/png'].includes(file.type)
+      const validSize = file.size / 1024 / 1024 <= 2
+      if (!validType) this.$message.error('浠呮敮鎸� jpg / png 鍥剧墖')
+      if (validType && !validSize) this.$message.error('鍥剧墖澶у皬涓嶈兘瓒呰繃 2MB')
+      return validType && validSize
+    },
+    handleUploadSuccess(response) {
+      if (response.code !== 200) {
+        this.$message.error(response.msg || '涓婁紶澶辫触')
+        return
+      }
+      this.form.address = typeof response.data === 'string' ? response.data : response.data.url
+      this.$nextTick(() => this.$refs.sealForm.validateField('address'))
+    },
+    handleUploadError() {
+      this.$message.error('鍗扮珷鍥剧墖涓婁紶澶辫触')
+    },
+    sealImageUrl(address) {
+      return this.javaApi + '/img/' + address
+    }
+  }
+}
+</script>
+
+<style scoped>
+.search-bar {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+}
+
+.toolbar {
+  flex: none;
+  margin-left: 16px;
+}
+
+.seal-uploader ::v-deep .el-upload {
+  overflow: hidden;
+  width: 150px;
+  height: 150px;
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+}
+
+.seal-uploader ::v-deep .el-upload:hover {
+  border-color: #409eff;
+}
+
+.seal-preview {
+  display: block;
+  width: 150px;
+  height: 150px;
+  object-fit: contain;
+}
+
+.seal-upload-placeholder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  color: #909399;
+}
+
+.seal-upload-placeholder i {
+  margin-bottom: 8px;
+  font-size: 30px;
+}
+
+.upload-tip {
+  color: #909399;
+  font-size: 12px;
+  line-height: 20px;
+}
+</style>

--
Gitblit v1.9.3