gaoluyang
6 小时以前 29be8639dc16b5d82bddc6099b1fa5cdca75f77a
src/views/CNAS/process/ensureResults/qualityControlPlan/index.vue
@@ -5,7 +5,7 @@
      <div style="display: flex;justify-content: space-between">
        <div style="display: flex;">
          <div style="margin-bottom: 18px;margin-right: 10px;display: flex;align-items: center;line-height: 32px;">
            <span style="width: 88px;font-size: 14px;font-weight: 700;color: #606266;">计划名称</span>
            <span style="width: 88px;font-size: 14px;font-weight: 700;color: var(--lims-text-2);">计划名称</span>
            <el-input size="small" placeholder="请输入" clearable v-model="yearForm.superviseName"
                      @keyup.enter="getYearPlanList"></el-input>
          </div>
@@ -25,18 +25,12 @@
      <div style="display: flex;justify-content: space-between">
        <div style="display: flex;">
          <div style="margin-bottom: 18px;margin-right: 10px;display: flex;align-items: center;line-height: 32px;">
            <el-radio-group v-model="yearDetailForm.causeType" @change="getYearDetailPlan" size="small">
              <el-radio-button :value="1">定期监督</el-radio-button>
              <el-radio-button :value="2">动态监督</el-radio-button>
            </el-radio-group>
          </div>
          <div style="margin-bottom: 18px;margin-right: 10px;display: flex;align-items: center;line-height: 32px;">
            <span style="width: 88px;font-size: 14px;font-weight: 700;color: #606266;">监控目的</span>
            <span style="width: 88px;font-size: 14px;font-weight: 700;color: var(--lims-text-2);">监控目的</span>
            <el-input v-model="yearDetailForm.supervisePurpose" placeholder="请输入" size="small"
                      @keyup.enter="getYearDetailPlan"></el-input>
          </div>
          <div style="margin-bottom: 18px;margin-right: 10px;display: flex;align-items: center;line-height: 32px;">
            <span style="width: 88px;font-size: 14px;font-weight: 700;color: #606266;">监控项目</span>
            <span style="width: 88px;font-size: 14px;font-weight: 700;color: var(--lims-text-2);">监控项目</span>
            <el-input v-model="yearDetailForm.superviseProject" placeholder="请输入" size="small"
                      @keyup.enter="getYearDetailPlan"></el-input>
          </div>
@@ -74,12 +68,43 @@
        <el-button :loading="ratifyLoading" type="primary" @click="handleRatify(1)">批 准</el-button>
      </span></template>
    </el-dialog>
    <el-dialog v-model="downloadDialog" title="导出" width="600px">
      <span>
        <el-button plain type="primary" :disabled="download.recordStatus !== 3" @click="controlDown">记录单导出</el-button>
        <el-button plain type="primary" :disabled="download.accordingStatus !== 3" @click="processingDown">处理单导出</el-button>
        <el-button plain type="primary" :disabled="download.correctStatus !== 3" @click="supervisoryDown">纠正单导出</el-button>
      </span>
    <el-dialog v-model="downloadDialog" title="导出" width="640px">
      <div class="doc-card">
        <div class="doc-card-head">
          <span class="doc-card-name">日常监督检查记录表(记录单)</span>
          <el-tag :type="download.recordStatus === 3 ? 'success' : 'info'" size="small">
            {{ download.recordStatus === 3 ? '已批准' : '未批准' }}
          </el-tag>
        </div>
        <div class="doc-card-foot">
          <el-button type="primary" :disabled="download.recordStatus !== 3" @click="controlDown">导 出</el-button>
        </div>
      </div>
      <div class="doc-card">
        <div class="doc-card-head">
          <span class="doc-card-name">不符合工作控制单(处理单)</span>
          <el-tag :type="download.accordingStatus === 3 ? 'success' : 'info'" size="small">
            {{ download.accordingStatus === 3 ? '已批准' : '未批准' }}
          </el-tag>
        </div>
        <div class="doc-card-foot">
          <el-button type="primary" :disabled="download.accordingStatus !== 3" @click="processingDown">导 出</el-button>
        </div>
      </div>
      <div class="doc-card">
        <div class="doc-card-head">
          <span class="doc-card-name">纠正措施处理单(纠正单)</span>
          <el-tag :type="download.correctStatus === 3 ? 'success' : 'info'" size="small">
            {{ download.correctStatus === 3 ? '已批准' : '未批准' }}
          </el-tag>
        </div>
        <div class="doc-card-foot">
          <el-button type="primary" :disabled="download.correctStatus !== 3" @click="supervisoryDown">导 出</el-button>
        </div>
      </div>
      <div class="down-tip">
        仅"已批准"的表单可导出,灰色表示对应表单尚未批准或暂无数据
      </div>
      <template #footer><span  class="dialog-footer">
        <el-button @click="downloadDialog = false">取 消</el-button>
      </span></template>
@@ -87,33 +112,46 @@
    <!--导入计划-->
    <el-dialog v-model="uploadDia" title="数据导入" width="500px" :close-on-click-modal="false"
      :close-on-press-escape="false">
      <div style="display: flex; align-items: center;">
        <span style="width: 80px">年份:</span>
        <el-date-picker v-model="superviseYear" type="year" value-format="YYYY" clearable size="small" format="YYYY"
          placeholder="选择年">
        </el-date-picker>
        <span style="width: 110px">监督员:</span>
        <el-select v-model="recordUserIds" placeholder="请选择" size="small" @change="splitList" :multiple-limit="2"
          filterable multiple style="width: 100%">
          <el-option v-for="item in userList" :key="item.id" :label="item.name" :value="item.id"></el-option>
        </el-select>
      </div>
      <div style="display: flex;align-items: center;margin: 10px 0">
        <div style="width: 60px">批准人:</div>
        <el-select v-model="ratifyUserId" clearable filterable size="small" style="width: 50%;">
          <el-option v-for="item in userList" :key="item.id" :label="item.name" :value="item.id">
          </el-option>
        </el-select>
      <el-form label-width="70px" size="small">
        <el-form-item label="年份">
          <el-date-picker v-model="superviseYear" type="year" value-format="YYYY" clearable format="YYYY"
            placeholder="选择年" style="width: 100%">
          </el-date-picker>
        </el-form-item>
        <el-form-item label="监督员">
          <el-select v-model="recordUserIds" placeholder="请选择" @change="splitList" :multiple-limit="2"
            filterable multiple 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>
        <el-form-item label="批准人">
          <el-select v-model="ratifyUserId" clearable filterable 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>
      </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>
      <div style="margin: 0 auto;">
        <el-upload ref="upload1" :action="action" :auto-upload="false" :file-list="fileList" :headers="uploadHeader"
          :limit="1" accept='.jpg,.jpeg,.png,.gif,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.zip,.rar'
          :limit="1" accept=".xls,.xlsx"
          :on-change="beforeUpload" :on-error="onError" :on-success="handleSuccessUp" drag
          :data="{ recordUserIds: recordUserIds1, superviseYear: superviseYear, ratifyUserId: ratifyUserId }"
          name="file">
          <lims-icon name="el-icon-upload"></lims-icon>
          <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
        </el-upload>
      </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>监督日期建议填“年.月”(如 2026.3);填成 2026-03-15 也可以,系统会自动换算为 2026.3。</div>
        <div>被监督人员必须填写系统中已存在的人员姓名,否则整批导入失败。</div>
        <div>导入只读取模板的第一个工作表。</div>
        <div style="color: var(--el-color-warning);">导入后计划为“待批准”:需使用所选批准人账号登录并点击“批准”,批准后才能下发和导出。</div>
      </div>
      <template #footer><span  class="dialog-footer">
        <el-button @click="closeUploadDia">取 消</el-button>
@@ -131,7 +169,7 @@
import limsTable from "@/components/Table/lims-table.vue";
import {
  delQualitySupervise, delQualitySuperviseDetail,
  exportQualitySupervise, exportSuperviseDetailRecord,
  exportQualitySupervise, exportSuperviseDetailRecord, issueQualitySupervise,
  pageQualitySupervise, pageQualitySuperviseDetail,
  ratifyQualitySupervise, superviseDetailAccordingExport,
  exportSuperviseDetaillCorrect
@@ -177,7 +215,7 @@
            } else if (params === 1) {
              return '批准';
            } else {
              return null;
              return '待批准';
            }
          },
          formatType: (params) => {
@@ -186,7 +224,7 @@
            } else if (params === 1) {
              return 'success';
            } else {
              return null;
              return 'warning';
            }
          }
        }, {
@@ -199,6 +237,25 @@
          label: '批准日期',
          prop: 'ratifyTime',
          width: '160'
        }, {
          dataType: 'tag',
          label: '下发状态',
          prop: 'issueStatus',
          width: '100',
          formatData: (params) => {
            if (params === 1) {
              return '已下发';
            } else {
              return '未下发';
            }
          },
          formatType: (params) => {
            if (params === 1) {
              return 'success';
            } else {
              return 'info';
            }
          }
        }, {
          dataType: 'action',
          label: '操作',
@@ -215,6 +272,20 @@
              },
              clickFun: (row) => {
                this.approvalPlan(row)
              }
            },
            {
              name: '下发',
              type: 'text',
              disabled: (row) => {
                if (row.ratifyStatus !== 1 || row.issueStatus === 1) {
                  return true
                } else {
                  return false
                }
              },
              clickFun: (row) => {
                this.issuePlan(row)
              }
            },
            {
@@ -252,8 +323,7 @@
      yearLoading: false,
      yearDetailForm: {
        supervisePurpose: '',
        superviseProject: '',
        causeType: 1
        superviseProject: ''
      },
      yearDetailColumnData: [
        {
@@ -389,6 +459,13 @@
              type: 'text',
              clickFun: (row) => {
                this.records(row)
              },
              disabled: () => {
                if (this.currentPlan.issueStatus !== 1) {
                  return true
                } else {
                  return false
                }
              }
            },
            {
@@ -446,6 +523,7 @@
      },
      currentScrollPosition: 0,
      superviseId: '',
      currentPlan: {},
      formDia: false,
      recordsDia: false,
      processingDia: false,
@@ -498,13 +576,29 @@
      this.yearPage.size = limit;
      this.getYearPlanList();
    },
    // 下载导入模板
    downloadTemplate() {
      const link = document.createElement('a')
      // 带版本参数避免浏览器继续使用缓存里的旧模板
      link.href = '/import-templates/质量监督计划导入模板.xlsx?v=3'
      link.download = '质量监督计划导入模板.xlsx'
      document.body.appendChild(link)
      link.click()
      document.body.removeChild(link)
    },
    // 导入流程
    beforeUpload(file) {
      if (!/\.(xls|xlsx)$/i.test(file.name || '')) {
        this.$message.error('请选择 .xls 或 .xlsx 格式的文件');
        this.$refs.upload1.clearFiles()
        return false;
      }
      if (file.size > 1024 * 1024 * 10) {
        this.$message.error('上传文件不超过10M');
        this.$refs.upload1.clearFiles()
        return false;
      }
      return true;
    },
    onError(err, file, fileList) {
      this.$message.error('上传失败')
@@ -513,7 +607,13 @@
    handleSuccessUp(response) {
      this.uploading = false;
      if (response.code == 200) {
        this.$message.success('上传成功');
        const approver = this.userList.find(item => Number(item.id) === Number(this.ratifyUserId))
        const approverName = approver ? approver.name : '所选批准人'
        if (Number(this.ratifyUserId) === Number(this.userId)) {
          this.$message.success('导入成功,请在列表中点击“批准”完成批准,批准后才能下发和导出')
        } else {
          this.$message.success(`导入成功,计划已提交给${approverName}批准;请用该账号登录批准,批准后才能下发和导出`)
        }
        this.recordUserIds = []
        this.superviseYear = ''
        this.fileList = []
@@ -562,8 +662,14 @@
      this.getUserList()
    },
    getUserList() {
      selectUserCondition({ type: 2 }).then((res) => {
        this.userList = res.data;
      // 质量监督面向全实验室, 人员列表不按部门筛选
      selectUserCondition({ type: 1 }).then((res) => {
        const list = res.data || []
        // 当前登录用户可能因角色(如 admin)被接口过滤,仍允许选自己作为批准人,避免导入后无人可批准
        if (this.userId && !list.some(item => Number(item.id) === Number(this.userId))) {
          list.unshift({ id: this.userId, name: this.nickName })
        }
        this.userList = list
      })
    },
    // 批准
@@ -592,14 +698,29 @@
    // 导出
    handleDown(row) {
      exportQualitySupervise({ superviseId: row.superviseId }).then(res => {
        try {
          this.outLoading = false
          const blob = new Blob([res], { type: 'application/msword' });
          this.$download.saveAs(blob, row.superviseName + '.docx')
        } catch (error) {
          console.error('创建Blob对象时出错:', error);
        }
        this.outLoading = false
        this.saveDocx(res, row.superviseName + '.docx')
      }).catch(err => {
        console.log('err---', err);
      })
    },
    // 保存导出文件: 后端导出失败时会返回错误JSON, 此时弹提示而不是存成损坏的docx
    saveDocx(res, filename) {
      const reader = new FileReader();
      reader.readAsText(res, 'utf-8');
      reader.onload = () => {
        const text = (reader.result || '').trim();
        if (text.startsWith('{') || text.startsWith('[')) {
          let msg = '导出失败';
          try { msg = JSON.parse(text).msg || msg; } catch (e) { }
          this.$message.error(msg);
        } else {
          this.$download.saveAs(new Blob([res], { type: 'application/msword' }), filename);
        }
      };
      reader.onerror = () => {
        this.$download.saveAs(new Blob([res], { type: 'application/msword' }), filename);
      };
    },
    // 删除进度计划表
    delPlan(row) {
@@ -627,7 +748,31 @@
    // 年度计划表格,点击行数据后刷新详情
    rowClick(row) {
      this.superviseId = row.superviseId
      this.currentPlan = row
      this.getYearDetailPlan()
    },
    // 下发计划
    issuePlan(row) {
      this.$confirm('下发后监督员与相关检测人员将收到通知, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.yearLoading = true
        issueQualitySupervise({ superviseId: row.superviseId }).then(res => {
          this.yearLoading = false
          this.$message.success('下发成功')
          this.getYearPlanList()
        }).catch(err => {
          this.yearLoading = false
          console.log('err---', err);
        })
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消下发'
        });
      });
    },
    getYearDetailPlan () {
      this.yearDetailPage.current = 1
@@ -635,18 +780,30 @@
      this.getYearDetailPlanList()
    },
    // 获取年度明细表
    // 监督日期统一成 "YYYY.M":兼容 2026-03-15 / 2026/3/15 / 2026年3月15日 等导入写法
    normalizeSuperviseMonth(value) {
      if (!value) return value
      const text = String(value).trim()
      const matched = text.match(/^(\d{4})\s*[./年-]\s*(\d{1,2})(?:\s*[./月-]\s*\d{1,2}\s*日?)?$/)
      if (!matched) return text
      const month = Number(matched[2])
      return month >= 1 && month <= 12 ? `${matched[1]}.${month}` : text
    },
    getYearDetailPlanList() {
      const entity = {
        superviseId: this.superviseId,
        supervisePurpose: this.yearDetailForm.supervisePurpose,
        superviseProject: this.yearDetailForm.superviseProject,
        causeType: this.yearDetailForm.causeType,
      }
      const page = this.yearDetailPage
      this.yearDetailLoading = true
      pageQualitySuperviseDetail({ ...entity, ...page }).then(res => {
        this.yearDetailLoading = false
        this.yearDetailTableData = res.data.records
        // 历史导入的监督日期可能是 2026/3/15、2026-03-15 等完整日期,统一显示成 2026.3
        this.yearDetailTableData = (res.data.records || []).map(item => ({
          ...item,
          superviseTime: this.normalizeSuperviseMonth(item.superviseTime)
        }))
        this.yearDetailPage.total = res.data.total
      }).catch(err => {
        console.log('err---', err);
@@ -667,7 +824,7 @@
    showDialog(type, row) {
      this.formDia = true
      this.$nextTick(() => {
        this.$refs.formDia.openDia(type, row, this.yearDetailForm.causeType)
        this.$refs.formDia.openDia(type, row)
      })
    },
    closeDia() {
@@ -715,8 +872,7 @@
    // 导出记录
    controlDown() {
      exportSuperviseDetailRecord({ superviseDetailsId: this.download.superviseDetailsId }).then(res => {
        const blob = new Blob([res], { type: 'application/msword' });
        this.$download.saveAs(blob, '记录导出.docx')
        this.saveDocx(res, '记录导出.docx')
      }).catch(err => {
        console.log('err---', err);
      })
@@ -724,8 +880,7 @@
    // 处理单导出
    processingDown() {
      superviseDetailAccordingExport({ superviseDetailsId: this.download.superviseDetailsId }).then(res => {
        const blob = new Blob([res], { type: 'application/msword' });
        this.$download.saveAs(blob, '不符合项导出.docx')
        this.saveDocx(res, '不符合项导出.docx')
      }).catch(err => {
        console.log('err---', err);
      })
@@ -733,8 +888,9 @@
    // 纠正单导出
    supervisoryDown() {
      exportSuperviseDetaillCorrect({ superviseDetailsCorrectId: this.download.superviseDetailsCorrectId }).then(res => {
        const blob = new Blob([res], { type: 'application/msword' });
        this.$download.saveAs(blob, '监督纠正措施.docx')
        this.saveDocx(res, '监督纠正措施.docx')
      }).catch(err => {
        console.log('err---', err);
      })
    },
    // 删除年度详情列表
@@ -761,7 +917,7 @@
    action() {
      return this.javaApi + '/qualitySupervise/importQualitySupervise'
    },
    ...mapGetters(["userId"]),
    ...mapGetters(["userId", "nickName"]),
  },
};
</script>
@@ -793,6 +949,37 @@
  margin: 0 5px;
}
.doc-card {
  padding: 12px 16px;
  margin-bottom: 12px;
  background-color: #f7f8fa;
  border: 1px solid #ebeef5;
  border-radius: 6px;
}
.doc-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.doc-card-name {
  font-size: 14px;
  color: var(--lims-text-2, #303133);
}
.doc-card-foot {
  display: flex;
  justify-content: flex-end;
  margin-top: 10px;
}
.down-tip {
  margin-top: 10px;
  color: #909399;
  font-size: 12px;
}
.search {
  width: 150px;
  padding: 0 16px;