feat(inspection): 更新检测类型选项并移除湿度字段
- 将"委托试验"改为"委托检验",增加委托检测、实验室间比对、能力验证等检测类型
- 移除所有表单中的湿度相关字段及验证逻辑
- 在报告准备界面添加印章选择功能,支持MA章、报告章、骑缝章的位置选择
- 优化报告结果流程界面,增加报告下载功能和领报告相关信息字段
- 新增印章管理系统,支持印章的增删改查和图片上传功能
已修改8个文件
已添加1个文件
690 ■■■■■ 文件已修改
src/api/cnas/process/reportResults.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/process/reportResults/index.vue 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/inspectionReview/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/inspectionTask/components/InspectionWord.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/inspectionTask/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/inspectionTask/inspection.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/inspectionView/index.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/reportPreparation/index.vue 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/structural/sealManage/index.vue 314 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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",
  });
}
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: "确定",
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: '季度检验', value: 'Quarterly inspection', type: ''},
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) {
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: "",
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] : [],
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}」的检测地点`);
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: '季度检验', 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>
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>