gaoluyang
7 小时以前 b93657ca98a9f1ced3aacc255c3fa09b7d39cca4
feat: 质量监控/人员培训/体系文件控制功能与界面调整

- 质量监控:质控方式多选、记录名称统一、导入模板与提示更新
- 人员培训:支持外聘讲师,结果明细排版、列表与查询优化
- 体系文件控制:附件格式放开,修复下载与盖章预览
- 通用:导入提示不换行、表格序号兜底

Co-Authored-By: Claude Code <noreply@anthropic.com>
已修改16个文件
300 ■■■■■ 文件已修改
public/import-templates/人员培训年度计划导入模板.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
public/import-templates/质量监控计划导入模板.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ImportDialog/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/lims-table.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/UpPdfStamp/index.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/personnel/personnelInfo/components/AddInDetail.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/personnel/personnelInfo/components/Edit.vue 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/personnel/personnelInfo/tabs/personnelTraining.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/process/ensureResults/ensureResultsValidity/components/detailFormDialog.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/process/ensureResults/ensureResultsValidity/components/evaluateDialog.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/process/ensureResults/ensureResultsValidity/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/process/ensureResults/qualityControlPlan/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/systemManagement/documentControl/components/ControlledFileApplication.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/systemManagement/documentControl/components/DistributionCollectionRecord.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/systemManagement/documentControl/components/FileChangeRequest.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/systemManagement/documentControl/components/FileList.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/import-templates/ÈËÔ±ÅàѵÄê¶È¼Æ»®µ¼ÈëÄ£°å.xlsx
Binary files differ
public/import-templates/ÖÊÁ¿¼à¿Ø¼Æ»®µ¼ÈëÄ£°å.xlsx
Binary files differ
src/components/ImportDialog/index.vue
@@ -186,7 +186,8 @@
.template-tip {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: nowrap;
  gap: 4px;
  margin-bottom: 12px;
  padding: 8px 12px;
  border-radius: 4px;
@@ -195,6 +196,15 @@
  font-size: 13px;
}
.template-tip > span {
  white-space: nowrap;
}
.template-tip .el-button {
  flex-shrink: 0;
  margin-left: 0;
}
.upload-tips {
  margin-top: 10px;
  color: var(--el-text-color-secondary);
src/components/Table/lims-table.vue
@@ -231,7 +231,10 @@
}
function indexMethod(index) {
  return (props.page.current - 1) * props.page.size + index + 1
  // æœªä¼  page æ—¶é»˜è®¤ current ä¸º 0,会算出 -9、-8 è¿™ç±»è´Ÿåºå·ï¼›è¿™é‡Œå…œåº•为第一页
  const current = props.page && props.page.current > 0 ? props.page.current : 1
  const size = props.page && props.page.size > 0 ? props.page.size : 10
  return (current - 1) * size + index + 1
}
function onPagination({ page, limit }) {
src/components/UpPdfStamp/index.vue
@@ -1,7 +1,10 @@
<template>
  <div class="up-pdf-stamp">
    <div v-if="isUpFile" class="work" style="display: flex;justify-content: space-between;align-items: center;margin-bottom: 10px;">
      <input ref="fileInput" type="file" accept="application/pdf" @change="handleFileUpload" />
      <input ref="fileInput" type="file" accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.wps,.et,.dps,.txt,.jpg,.jpeg,.png" @change="handleFileUpload" />
      <span v-if="fileName !== '文件名'" style="margin-left: 10px;font-size: 12px;color: var(--el-text-color-secondary);">
        å·²é€‰æ‹©ï¼š{{ fileName }}<template v-if="!isPdfFile">(非 PDF æ–‡ä»¶æŒ‰åŽŸæ–‡ä»¶ä¸Šä¼ ï¼Œä¸æ”¯æŒç›–ç« ï¼‰</template>
      </span>
    </div>
    <div v-if="canvasNumPages > 0" style="color: red;font-size: 12px;margin: 16px 0;">提示:在文件范围内,单击鼠标盖章,双击鼠标已盖好章处可删除当前章</div>
    <canvas
@@ -35,7 +38,9 @@
      stampHeight: 80, // ç›–章高度
      stampsName: '',
      stampsList: ['主任', '质量负责人', '技术负责人'],
      fileName: '文件名'
      fileName: '文件名',
      selectedFile: null, // éž PDF æ–‡ä»¶ï¼šä¿ç•™åŽŸå§‹ File,直接上传
      isPdfFile: false
    }
  },
  methods: {
@@ -49,6 +54,9 @@
      const file = event.target.files[0]
      if (file.size > 20 * 1024 * 1024) {
        this.$refs.fileInput.value = '' // æ¸…空文件输入框内容
        this.selectedFile = null
        this.isPdfFile = false
        this.fileName = '文件名'
        return this.$message.error('文件大小不能超过20M')
      }
      this.lookFile(file)
@@ -67,14 +75,25 @@
          this.stampsName = '综合室'
        }
      }
      if (file && file.type === 'application/pdf') {
      this.isPdfFile = !!file && (file.type === 'application/pdf' || /\.pdf$/i.test(file.name || ''))
      if (this.isPdfFile) {
        this.selectedFile = file
        const reader = new FileReader()
        reader.onload = e => {
          const typedArray = new Uint8Array(e.target.result)
          this.loadPDF(typedArray)
        }
        reader.readAsArrayBuffer(file)
      } else if (this.isUpFile) {
        // æ–‡ä»¶æ¸…单上传支持 Word/Excel/图片等格式,不做盖章,选中后按原文件上传
        this.selectedFile = file
        this.pdfDoc = null
        this.canvasNumPages = 0
        this.stamps = []
        this.contextList = []
      } else {
        // å…¶ä»–流程(文件申请、变更申请等)仍只处理 PDF
        this.selectedFile = null
        this.$message.error('请选择 PDF æ–‡ä»¶')
      }
    },
@@ -167,8 +186,12 @@
    },
    // ç”Ÿæˆ PDF çš„函数
    async generatePDF() {
      // éž PDF:不盖章,原文件直接上传
      if (this.selectedFile && !this.isPdfFile) {
        return this.$emit('uploadPDF', this.selectedFile, this.fileName)
      }
      if (this.contextList.length === 0) {
        this.$message({ message: '请先上传PDF文件', type: 'error' })
        this.$message({ message: '请先选择文件', type: 'error' })
        this.$emit('uploadPDFErr')
        return false
      }
src/views/CNAS/personnel/personnelInfo/components/AddInDetail.vue
@@ -33,12 +33,20 @@
            </el-col>
            <el-col :span="12">
              <el-form-item label="培训讲师:" prop="trainingLecturerId">
                <el-select v-model="trainingPlan.trainingLecturerId"
                           clearable filterable
                           placeholder="请选择" size="small" style="width: 100%;">
                  <el-option v-for="item in responsibleOptions" :key="item.id" :label="item.name" :value="item.id">
                  </el-option>
                </el-select>
                <div style="display: flex; align-items: center; gap: 8px; width: 100%;">
                  <el-radio-group v-model="lecturerType" size="small" @change="handleLecturerTypeChange">
                    <el-radio-button label="internal">内部人员</el-radio-button>
                    <el-radio-button label="external">外聘讲师</el-radio-button>
                  </el-radio-group>
                  <el-select v-if="lecturerType === 'internal'" v-model="trainingPlan.trainingLecturerId"
                             clearable filterable
                             placeholder="请选择" size="small" style="flex: 1;">
                    <el-option v-for="item in responsibleOptions" :key="item.id" :label="item.name" :value="item.id">
                    </el-option>
                  </el-select>
                  <el-input v-else v-model="trainingPlan.trainingLecturerName" clearable
                            placeholder="请输入外聘讲师姓名" size="small" style="flex: 1;"></el-input>
                </div>
              </el-form-item>
            </el-col>
            <el-col :span="12">
@@ -88,6 +96,7 @@
    return {
      addTrainingPlanDia: false,
      submitAddLoading: false,
      lecturerType: 'internal',
      trainingPlan: {
        planId: '',
        id: '',
@@ -97,6 +106,7 @@
        participants: '',
        trainingContent: '',
        trainingLecturerId: '',
        trainingLecturerName: '',
        trainingMode: '',
        holdingDepartment: '',
        remarks: '',
@@ -104,7 +114,6 @@
      trainingPlanRules: {
        trainingDate: [{ required: true, message: '请选择培训日期', trigger: 'change' }],
        trainingContent: [{ required: true, message: '请输入培训内容', trigger: 'blur' }],
        trainingLecturerId: [{ required: true, message: '请选择培训讲师', trigger: 'blur' }],
      },
      responsibleOptions: [],
      operationType: ''
@@ -117,7 +126,9 @@
      this.operationType = type
      if (this.operationType === 'edit') {
        this.trainingPlan = this.HaveJson(row)
        this.lecturerType = row.trainingLecturerId ? 'internal' : (row.trainingLecturerName ? 'external' : 'internal')
      } else {
        this.lecturerType = 'internal'
        this.trainingPlan = {
          id: '',
          trainingDate: '',
@@ -134,10 +145,32 @@
      this.trainingPlan.planId = id
      this.getUserList()
    },
    handleLecturerTypeChange(type) {
      if (type === 'internal') {
        this.trainingPlan.trainingLecturerName = ''
      } else {
        this.trainingPlan.trainingLecturerId = null
      }
    },
    // æäº¤æ–°å¢ž
    submitAdd() {
      this.$refs.trainingPlanForm.validate((valid) => {
        if (valid) {
          if (this.lecturerType === 'internal' && !this.trainingPlan.trainingLecturerId) {
            this.$message.warning('请选择培训讲师')
            return
          }
          if (this.lecturerType === 'external') {
            const lecturerName = (this.trainingPlan.trainingLecturerName || '').trim()
            if (!lecturerName) {
              this.$message.warning('请输入外聘讲师姓名')
              return
            }
            this.trainingPlan.trainingLecturerId = null
            this.trainingPlan.trainingLecturerName = lecturerName
          } else {
            this.trainingPlan.trainingLecturerName = null
          }
          this.submitAddLoading = true
          this.trainingPlan.planId = this.currentChangeRow.id
          const personTrainingDetailed = this.trainingPlan
src/views/CNAS/personnel/personnelInfo/components/Edit.vue
@@ -10,40 +10,44 @@
    </div>
    <div class="form_title">
      <el-row>
        <el-col :span="7">
        <el-col :span="8">
          <span class="form_label">培训内容:</span>
          <span> {{ trainingForm.trainingContent }} </span>
        </el-col>
        <el-col :span="5">
        <el-col :span="4">
          <span class="form_label">状态:</span>
          <el-tag v-if="trainingForm.state === 1" type="success">已完成</el-tag>
          <el-tag v-if="trainingForm.state === 2" type="warning">待评价</el-tag>
          <el-tag v-if="trainingForm.state === 3" type="primary">未开始</el-tag>
        </el-col>
        <el-col :span="4">
        <el-col :span="5">
          <span class="form_label">培训讲师:</span>
          <span> {{ trainingForm.trainingLecturerName }} </span>
        </el-col>
        <el-col :span="7">
          <span class="form_label">培训日期:</span>
          <span> {{ trainingForm.trainingDate || '-' }} </span>
        </el-col>
      </el-row>
      <el-row style="margin: 15px 0">
        <el-form>
        <el-form label-width="130px">
          <el-row>
          <el-col :span="6">
            <el-form-item label="培训日期:">
            <el-form-item label="开始时间:">
              <el-date-picker v-model="trainingForm.openingTime" format="YYYY-MM-DD" :disabled="currentRow.state !== 3"
                              placeholder="选择日期" size="small" value-format="YYYY-MM-DD"
                              type="date" style="width: 60%"></el-date-picker>
                              placeholder="选择实际开始日期" size="small" value-format="YYYY-MM-DD"
                              type="date" style="width: 100%"></el-date-picker>
            </el-form-item>
          </el-col>
            <el-col :span="7">
            <el-col :span="9">
              <el-form-item label="培训地点">
                <el-input v-model="trainingForm.placeTraining" :disabled="currentRow.state !== 3" :rows="2" placeholder="请输入"
                          size="small" style="width: 60%" type="text"></el-input>
                          size="small" style="width: 100%" type="text"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="6">
            <el-col :span="9">
              <el-form-item label="评价人">
                <el-select v-model="trainingForm.assessmentUserId" :disabled="currentRow.state !== 3" placeholder="请选择" size="small" style="width: 50%">
                <el-select v-model="trainingForm.assessmentUserId" :disabled="currentRow.state !== 3" placeholder="请选择" size="small" style="width: 100%">
                  <el-option v-for="item in userList" :key="item.id" :label="item.name" :value="item.id"></el-option>
                </el-select>
              </el-form-item>
@@ -51,7 +55,7 @@
            <el-col :span="12">
              <el-form-item label="考核方式">
                <el-input v-model="trainingForm.assessmentMethod" :disabled="currentRow.state !== 3" :rows="2" placeholder="请输入"
                          size="small" style="width: 79%" type="textarea"></el-input>
                          size="small" style="width: 100%" type="textarea"></el-input>
              </el-form-item>
            </el-col>
          </el-row>
@@ -60,14 +64,14 @@
              <el-form-item label="评价时间">
                <el-date-picker v-model="trainingForm.assessmentDate" :disabled="currentRow.state !== 2 || isDisabled"
                                type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD" size="small"
                                placeholder="选择日期">
                                placeholder="选择日期" style="width: 100%">
                </el-date-picker>
              </el-form-item>
            </el-col>
          <el-col :span="12">
            <el-form-item label="本次培训综合评价">
              <el-input v-model="trainingForm.comprehensiveAssessment" :disabled="currentRow.state !== 2 || isDisabled" :rows="2" placeholder="请输入"
                        size="small" style="width: 68%" type="textarea"></el-input>
                        size="small" style="width: 100%" type="textarea"></el-input>
            </el-form-item>
          </el-col>
          </el-row>
@@ -76,15 +80,17 @@
    </div>
    <el-divider>人员详情</el-divider>
    <div>
      <div class="items_center">
        <span>姓名</span>
        <el-input v-model="userName" class="search" placeholder="请输入"
                  size="small"></el-input>
        <el-button size="small" type="primary" @click="getInfo">查询</el-button>
      </div>
      <div class="items_btn">
        <el-button :disabled="currentRow.state === 1" size="small" type="primary" @click="addPerson">新增人员</el-button>
        <el-button :disabled="currentRow.state === 1" size="small" @click="batchDelete">批量删除</el-button>
      <div class="items_header">
        <div class="items_center">
          <span>姓名</span>
          <el-input v-model="userName" class="person-search-input" placeholder="请输入"
                    size="small" clearable @keyup.enter="getInfo"></el-input>
          <el-button size="small" type="primary" @click="getInfo">查询</el-button>
        </div>
        <div class="items_btn">
          <el-button :disabled="currentRow.state === 1" size="small" type="primary" @click="addPerson">新增人员</el-button>
          <el-button :disabled="currentRow.state === 1" size="small" @click="batchDelete">批量删除</el-button>
        </div>
      </div>
      <el-table :data="trainingTableData" height="calc(100vh - 30em)" stripe style="width: 100%"
 border
@@ -117,7 +123,7 @@
        </el-form>
      </div>
      <div v-if="selectUserDia" class="body" style="height: 60vh;">
        <lims-table :tableData="tableData1" :column="column1"
        <lims-table :tableData="tableData1" :column="column1" :page="page1"
                    :isSelection="true" :handleSelectionChange="selectMethod"
                    height="500" :tableLoading="tableLoading1"></lims-table>
      </div>
@@ -402,15 +408,27 @@
}
.items_header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}
.items_center {
  float: left;
  width: 50%;
  text-align: left;
  display: flex;
  align-items: center;
  gap: 8px;
}
.items_center > span {
  line-height: 24px;
  white-space: nowrap;
}
.items_center .person-search-input {
  width: 200px;
}
.items_btn {
  text-align: right;
  width: 50%;
  float: right;
  margin-bottom: 1em;
  display: flex;
  align-items: center;
  gap: 8px;
}
</style>
src/views/CNAS/personnel/personnelInfo/tabs/personnelTraining.vue
@@ -95,9 +95,9 @@
          </el-option>
        </el-select>
      </div>
      <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px; padding: 8px 12px; border-radius: 4px; background: var(--el-color-primary-light-9); font-size: 13px;">
        <span>第一次导入请先下载模板,按模板格式填写后再上传。</span>
        <el-button link type="primary" @click="downloadTemplate">下载导入模板</el-button>
      <div style="display: flex; align-items: center; flex-wrap: nowrap; gap: 4px; margin-bottom: 10px; padding: 8px 12px; border-radius: 4px; background: var(--el-color-primary-light-9); font-size: 13px;">
        <span style="white-space: nowrap;">第一次导入请先下载模板,按模板格式填写后再上传。</span>
        <el-button link type="primary" style="flex-shrink: 0; margin-left: 0;" @click="downloadTemplate">下载导入模板</el-button>
      </div>
      <div style="margin: 0 auto;">
        <el-upload ref="upload"
@@ -110,7 +110,7 @@
      </div>
      <div style="margin-top: 10px; color: var(--el-text-color-secondary); font-size: 12px; line-height: 20px;">
        <div>支持格式:.xls、.xlsx;单个文件不超过10MB。</div>
        <div>培训讲师必须填写系统中已存在的人员姓名,否则整批导入失败。</div>
        <div>培训讲师填写系统内人员姓名时自动关联;外聘讲师直接填写姓名,按文本保存。</div>
        <div>培训时间建议填写 YYYY-MM-DD,课时填写整数。</div>
        <div>导入只读取模板的第一个工作表。</div>
      </div>
@@ -325,23 +325,23 @@
        {
          label: '培训大类',
          prop: 'trainingCategory',
          width: '100px',
          minWidth: '100px',
        },{
          label: '培训目标',
          prop: 'trainingObjectives',
          width: '100px',
          minWidth: '100px',
        }, {
          label: '培训内容',
          prop: 'trainingContent',
          width: '100px',
          minWidth: '100px',
        }, {
          label: '培训方式',
          prop: 'trainingMode',
          width: '100px',
          minWidth: '100px',
        }, {
          dataType: 'tag',
          label: '课程状态',
          width: '100px',
          minWidth: '100px',
          prop: 'state',
          formatData: (params) => {
            if (params == 1) {
@@ -370,34 +370,34 @@
        }, {
          label: '参加对象',
          prop: 'participants',
          width: '100px',
          minWidth: '100px',
        }, {
          label: '举办部门',
          prop: 'holdingDepartment',
          width: '100px',
          minWidth: '100px',
        }, {
          label: '培训讲师',
          prop: 'trainingLecturerName',
          width: '100px',
          minWidth: '100px',
        }, {
          label: '培训日期',
          prop: 'trainingDate',
          width: '100px',
          minWidth: '100px',
        }, {
          label: '开始时间',
          prop: 'openingTime',
          width: '100px',
          minWidth: '100px',
        }, {
          label: '课时',
          prop: 'classHour',
          width: '100px',
          minWidth: '100px',
        }, {
          label: '备注',
          prop: 'remarks',
          width: '100px',
          minWidth: '100px',
        }, {
          dataType: 'action',
          width: '200',
          minWidth: '200px',
          label: '操作',
          fixed: 'right',
          operation: [
@@ -645,7 +645,7 @@
    // ä¸‹è½½å¹´åº¦è®¡åˆ’导入模板
    downloadTemplate() {
      const link = document.createElement('a')
      link.href = '/import-templates/人员培训年度计划导入模板.xlsx?v=2'
      link.href = '/import-templates/人员培训年度计划导入模板.xlsx?v=3'
      link.download = '人员培训年度计划导入模板.xlsx'
      document.body.appendChild(link)
      link.click()
src/views/CNAS/process/ensureResults/ensureResultsValidity/components/detailFormDialog.vue
@@ -21,8 +21,11 @@
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="质控方式" prop="monitorWay">
            <el-input v-model="form.monitorWay" size="small" clearable placeholder="如盲样检测、人员比对、监督检查" />
          <el-form-item label="质控方式" prop="monitorWayList">
            <el-select v-model="form.monitorWayList" multiple filterable allow-create default-first-option
                       size="small" style="width: 100%" placeholder="可多选,也可输入其他方式">
              <el-option v-for="item in monitorWayOptions" :key="item" :label="item" :value="item" />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
@@ -103,6 +106,11 @@
    // è¿™é‡Œå­˜æ”¾æ•°æ®
    return {
      formDia: false,
      // LMSY-CX30-2024《质量控制程序》4.1 è§„定的常用质量控制手段,可多选
      monitorWayOptions: [
        '人员比对', '设备比对', '方法比对', '盲样检测',
        '留样再测', '标准物质监控', '实验室间比对', '能力验证',
      ],
      form: {
        qualityMonitorDetailsId: '',
        sortNo: null,
@@ -116,6 +124,7 @@
        budget: '',
        organization: '',
        monitorWay: '',
        monitorWayList: [],
        itemKind: 'OTHER',
        standardMethodId: null,
        templateId: null,
@@ -134,7 +143,7 @@
      rules: {
        monitorProject: [{ required: true, message: '请输入质控项目', trigger: 'blur' }],
        technicalStandardText: [{ required: true, message: '请输入技术标准', trigger: 'blur' }],
        monitorWay: [{ required: true, message: '请输入质控方式', trigger: 'blur' }],
        monitorWayList: [{ required: true, type: 'array', min: 1, message: '请选择质控方式', trigger: 'change' }],
        completionMonth: [{ required: true, message: '请选择完成时限', trigger: 'change' }],
        responsibleUserId: [{ required: true, message: '请选择负责人', trigger: 'change' }],
        templateIds: [{ required: true, type: 'array', min: 1, message: '请至少选择一个原始记录模板', trigger: 'change' }],
@@ -148,6 +157,7 @@
    openDia(type, row) {
      this.formDia = true
      this.operationType = type
      this.form.monitorWayList = []
      this.templateOptions = []
      this.allTemplateOptions = []
      this.templateBindingLoading = false
@@ -165,7 +175,12 @@
      const templateIds = row.templateIds
        ? String(row.templateIds).split(',').filter(Boolean).map(id => Number(id))
        : (row.templateId ? [Number(row.templateId)] : [])
      this.form = {...row, itemKind: row.itemKind || 'OTHER', templateIds}
      this.form = {
        ...row,
        itemKind: row.itemKind || 'OTHER',
        templateIds,
        monitorWayList: row.monitorWay ? String(row.monitorWay).split(/[、,,;;]/).map(item => item.trim()).filter(Boolean) : [],
      }
    },
    handleStandardChange(id) {
      const standard = this.standardOptions.find(item => Number(item.id) === Number(id))
@@ -211,6 +226,8 @@
        : []
      entity.templateIds = templateIds.join(',') || null
      entity.templateId = templateIds.length ? templateIds[0] : null
      entity.monitorWay = (this.form.monitorWayList || []).map(item => item.trim()).filter(Boolean).join('、')
      delete entity.monitorWayList
      entity.reviewerUserId = null
      return entity
    },
src/views/CNAS/process/ensureResults/ensureResultsValidity/components/evaluateDialog.vue
@@ -2,8 +2,10 @@
  <div>
    <el-dialog :close-on-click-modal="false" :close-on-press-escape="false"
               v-model="formDia"
               title="质量监控评价"
               title="质量控制评价记录"
               width="60%" @close="closeEvaDia">
      <el-alert type="info" :closable="false" show-icon style="margin-bottom: 12px"
        title="检测室主任组织评价并填写本记录,技术负责人审核;评审结论如需采取措施,按《纠正措施程序》执行并留存记录。" />
      <el-steps :active="currentStep" align-center finish-status="success">
        <el-step title="监控计划实施情况" @click="setStep(0)"></el-step>
        <el-step title="监控结果评价" @click="setStep(1)"></el-step>
src/views/CNAS/process/ensureResults/ensureResultsValidity/index.vue
@@ -227,9 +227,9 @@
          </el-select>
        </el-form-item>
      </el-form>
      <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px; padding: 8px 12px; border-radius: 4px; background: var(--el-color-primary-light-9); font-size: 13px;">
        <span>第一次导入请先下载模板,按模板格式填写后再上传。</span>
        <el-button link type="primary" @click="downloadTemplate">下载导入模板</el-button>
      <div style="display: flex; align-items: center; flex-wrap: nowrap; gap: 4px; margin-bottom: 10px; padding: 8px 12px; border-radius: 4px; background: var(--el-color-primary-light-9); font-size: 13px;">
        <span style="white-space: nowrap;">第一次导入请先下载模板,按模板格式填写后再上传。</span>
        <el-button link type="primary" style="flex-shrink: 0; margin-left: 0;" @click="downloadTemplate">下载导入模板</el-button>
      </div>
      <div style="margin: 0 auto;">
        <el-upload ref="upload" :action="action" :auto-upload="false" v-model:file-list="fileList" :headers="uploadHeader"
@@ -594,7 +594,7 @@
    downloadTemplate() {
      const link = document.createElement('a')
      // å¸¦ç‰ˆæœ¬å‚数避免浏览器继续使用缓存里的旧模板
      link.href = '/import-templates/质量监控计划导入模板.xlsx?v=2'
      link.href = '/import-templates/质量监控计划导入模板.xlsx?v=3'
      link.download = '质量监控计划导入模板.xlsx'
      document.body.appendChild(link)
      link.click()
@@ -956,7 +956,7 @@
      exportQualityMonitorRatify({ qualityMonitorDetailsId: this.download.qualityMonitorDetailsId }).then(res => {
        this.outLoading = false
        const blob = new Blob([res], { type: 'application/msword' });
        this.$download.saveAs(blob, '质量监控实施计划.docx')
        this.$download.saveAs(blob, '质量控制措施.docx')
      })
    },
    // å®ŒæˆæŠ¥å‘Šå¯¼å‡º
@@ -973,7 +973,7 @@
      exportQualityMonitorEvaluate({ qualityMonitorDetailsId: this.download.qualityMonitorDetailsId }).then(res => {
        this.outLoading = false
        const blob = new Blob([res], { type: 'application/msword' });
        this.$download.saveAs(blob, '质量监控评价.docx')
        this.$download.saveAs(blob, '质量控制评价记录.docx')
      })
    },
    delYearPlanDetail(row) {
src/views/CNAS/process/ensureResults/qualityControlPlan/index.vue
@@ -131,9 +131,9 @@
          </el-select>
        </el-form-item>
      </el-form>
      <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px; padding: 8px 12px; border-radius: 4px; background: var(--el-color-primary-light-9); font-size: 13px;">
        <span>第一次导入请先下载模板,按模板格式填写后再上传。</span>
        <el-button link type="primary" @click="downloadTemplate">下载导入模板</el-button>
      <div style="display: flex; align-items: center; flex-wrap: nowrap; gap: 4px; margin-bottom: 10px; padding: 8px 12px; border-radius: 4px; background: var(--el-color-primary-light-9); font-size: 13px;">
        <span style="white-space: nowrap;">第一次导入请先下载模板,按模板格式填写后再上传。</span>
        <el-button link type="primary" style="flex-shrink: 0; margin-left: 0;" @click="downloadTemplate">下载导入模板</el-button>
      </div>
      <div style="margin: 0 auto;">
        <el-upload ref="upload1" :action="action" :auto-upload="false" :file-list="fileList" :headers="uploadHeader"
src/views/CNAS/systemManagement/documentControl/components/ControlledFileApplication.vue
@@ -71,7 +71,7 @@
          <div class="search_thing">
            <div class="search_label">上传附件:</div>
            <div class="search_input"><el-upload style="margin: 8px 0 0px 50px;" action="#" :auto-upload="false"
                :multiple="false" accept='.pdf' :on-change="handleChangeUpload" v-if="addDialogVisible">
                :multiple="false" accept='.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.wps,.et,.dps,.txt,.jpg,.jpeg,.png' :on-change="handleChangeUpload" v-if="addDialogVisible">
                <el-button size="small" type="primary">上传附件</el-button>
              </el-upload></div>
          </div>
@@ -437,21 +437,30 @@
    handleCheck0(row) {
      this.checkStampDialogVisible = false
      this.checkDialogVisible = true
      checkManageDocumentControlledPdf({ id: row.id }).then(res => {
        const blob = new Blob([res]);
      checkManageDocumentControlledPdf({ id: row.id }).then(async res => {
        // åŽç«¯è½¬ PDF å¤±è´¥æ—¶ä¼šè¿”回 JSON é”™è¯¯ï¼Œblob é‡Œå°±ä¸æ˜¯ PDF
        if (res.type && res.type.includes('json')) {
          const data = JSON.parse(await res.text())
          this.$message.error(data.msg || '附件转换预览失败')
          this.checkDialogVisible = false
          return
        }
        const blob = new Blob([res], { type: 'application/pdf' });
        const file = new File([blob], row.name, { type: 'application/pdf' })
        this.$refs.UpPdfStamp.lookFile(file, this.currentStamp)
      }).catch(err => {
        this.checkDialogVisible = false
        console.log(err)
      })
    },
    handleDown(row) {
      if (!row.url) return this.$message.warning('文件未上传')
      this.$download.saveAs(row.url, row.url);
      const fileName = row.url.replace(/^[0-9a-fA-F-]{36}_/, '')
      this.$download.saveAs(this.javaApi + '/word/' + row.url, fileName);
    },
    async uploadPDF(pdfBlob) {
      const formData = new FormData();
      formData.append('file', pdfBlob, this.fileName + '.pdf'); // æ–‡ä»¶å­—段
      formData.append('file', pdfBlob, (this.currentInfo.name || 'controlled') + '.pdf'); // æ–‡ä»¶å­—段
      formData.append('id', this.currentInfo.id); // æ–‡ä»¶åå­—段
      formData.append('state', this.type); // æ–‡ä»¶åå­—段
      formData.append('writer', this.currentInfo.writer); // æ–‡ä»¶åå­—段
src/views/CNAS/systemManagement/documentControl/components/DistributionCollectionRecord.vue
@@ -154,7 +154,7 @@
          <div class="search_thing">
            <div class="search_label">上传附件:</div>
            <div class="search_input"><el-upload :auto-upload="false" :multiple="false" :on-change="handleChangeUpload"
                accept='.pdf,.doc,.docx,.xls,.xlsx' action="#" style="margin: 8px 0 0px 50px;">
                accept='.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.wps,.et,.dps,.txt,.jpg,.jpeg,.png' action="#" style="margin: 8px 0 0px 50px;">
                <el-button size="small" type="primary">上传附件</el-button>
              </el-upload></div>
          </div>
src/views/CNAS/systemManagement/documentControl/components/FileChangeRequest.vue
@@ -184,7 +184,7 @@
            <div class="search_thing" style="width: 100%;">
              <div class="search_label">相关附件:</div>
              <div class="search_input"><el-upload v-if="addDialogVisible" :auto-upload="false" :multiple="false"
                                                   :on-change="handleChangeUpload" accept='.pdf' action="#" style="margin: 8px 0 0px 50px;">
                                                   :on-change="handleChangeUpload" accept='.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.wps,.et,.dps,.txt,.jpg,.jpeg,.png' action="#" style="margin: 8px 0 0px 50px;">
                <el-button size="small" type="primary">上传附件</el-button>
              </el-upload></div>
            </div>
@@ -536,15 +536,23 @@
      this.getCurrentFile(alterBeforeCode)
      this.addDialogVisible = true
      this.diaLoading = true
      checkManageDocumentAlterPdf({ id: row.id }).then(res => {
      checkManageDocumentAlterPdf({ id: row.id }).then(async res => {
        //
        this.diaLoading = false
        const blob = new Blob([res]);
        // åŽç«¯è½¬ PDF å¤±è´¥æ—¶ä¼šè¿”回 JSON é”™è¯¯ï¼Œblob é‡Œå°±ä¸æ˜¯ PDF
        if (res.type && res.type.includes('json')) {
          const data = JSON.parse(await res.text())
          this.$message.error(data.msg || '附件转换预览失败')
          this.addDialogVisible = false
          return
        }
        const blob = new Blob([res], { type: 'application/pdf' });
        const file = new File([blob], row.name, { type: 'application/pdf' })
        this.$refs.UpPdfStamp.lookFile(file)
        this.currentInfo = row
      }).catch(err => {
        this.diaLoading = false
        this.addDialogVisible = false
        console.log(err)
      })
    },
src/views/CNAS/systemManagement/documentControl/components/FileList.vue
@@ -29,6 +29,9 @@
        :height="'calc(100vh - 290px)'" @pagination="pagination"></lims-table>
    </div>
    <el-dialog title="上传" v-model="addDialogVisible" width="1000px" top="3vh">
      <div style="margin-bottom: 10px; padding: 8px 12px; border-radius: 4px; background: var(--el-color-primary-light-9); font-size: 13px; color: var(--el-text-color-regular);">
        æ”¯æŒ PDF、Word(.doc/.docx)、Excel(.xls/.xlsx)、PPT、图片等常见格式,单个文件不超过 20MB。PDF å¯é¢„览并盖章;其他格式按原文件上传,不盖章。
      </div>
      <UpPdfStamp ref="UpPdfStamp" v-if="addDialogVisible" @uploadPDF="uploadPDF" :isUpFile="true"
        @uploadPDFErr="uploadPDFErr"></UpPdfStamp>
      <template #footer><span  class="dialog-footer">
@@ -277,7 +280,9 @@
    // ä¸‹è½½æ–‡ä»¶
    handleDown(row) {
      if (!row.url) return this.$message.warning('文件未上传')
      this.$download.saveAs(row.url, row.url);
      // åŽç«¯ä¿å­˜çš„æ˜¯ word ç›®å½•下的 uuid_原文件名,下载时走静态资源地址并去掉 uuid å‰ç¼€
      const fileName = row.url.replace(/^[0-9a-fA-F-]{36}_/, '')
      this.$download.saveAs(this.javaApi + '/word/' + row.url, fileName);
    },
    // æŸ¥çœ‹æ–‡ä»¶
    handleLook(row) {