吴羡
8 小时以前 a52ac50ffc5f3b369eb689dd734bfb8a4bb988a4
Merge remote-tracking branch 'origin/master'

# Conflicts:
# src/views/CNAS/process/method/methodVerification/component/viewWorkPermitDia.vue
已修改5个文件
已添加1个文件
442 ■■■■■ 文件已修改
src/api/cnas/process/method/methodVerification.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/personnel/personnelInfo/components/mandateAdd.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/process/method/methodVerification/component/viewWorkPermitDia.vue 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/process/method/methodVerification/component/wordFormDIa.vue 297 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/process/method/methodVerification/index.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/cnas/process/method/methodVerification.js
@@ -38,6 +38,15 @@
  });
}
// æŒ‰å½“前所选检测人员实时读取任职授权证书
export function getCurrentWorkFiles(query) {
  return request({
    url: '/processMethodVerify/getCurrentWorkFiles',
    method: 'get',
    params: query,
  })
}
// ä¿®æ”¹æ ‡å‡†æ–¹æ³•验证
export function updateMethodVerify(data) {
  return request({
src/views/CNAS/personnel/personnelInfo/components/mandateAdd.vue
@@ -17,10 +17,10 @@
          </el-input>
        </el-form-item >
        <el-form-item label="被任职人员" prop="userId">
          <el-select v-model="form.userId" placeholder="请选择">
          <el-select v-model="form.userId" filterable placeholder="请选择">
            <el-option
              v-for="(item, index) in userList"
              :key="index"
              v-for="item in userList"
              :key="item.id"
              :label="item.name"
              :value="item.id"
            ></el-option>
@@ -141,9 +141,14 @@
     * @desc èŽ·å–ç”¨æˆ·ä¿¡æ¯
     */
    async getUserList() {
      selectUserCondition({type: 2}).then((res) => {
        this.userList = res.data;
      })
      try {
        const res = await selectUserCondition()
        if (res.code !== 200) throw new Error('人员列表接口返回失败')
        this.userList = Array.isArray(res.data) ? res.data : []
      } catch (error) {
        this.userList = []
        this.$message.error('人员列表加载失败,请稍后重试')
      }
    },
    /**
     * @desc æäº¤è¡¨å•
src/views/CNAS/process/method/methodVerification/component/viewWorkPermitDia.vue
@@ -2,23 +2,25 @@
  <div>
    <el-dialog v-model="filesDialogVisible" title="查看上岗证" width="80%" @closed="closeFilesLook">
      <div>
        <el-alert v-if="!tableLoading && tableData.length === 0" type="info" :closable="false"
                  title="所选检测人员暂无任职授权记录,请核对被任职人员与检测人员是否为同一人。"/>
        <limsTable
          ref="yearTable"
          :column="columnData"
          :height="'calc(100vh - 400px)'"
          :highlightCurrentRow="true"
          :table-data="tableData"
          :table-loading="tableLoading"
          style="margin-top: 0.5em;">
            ref="yearTable"
            :column="columnData"
            :height="'calc(100vh - 400px)'"
            :highlightCurrentRow="true"
            :table-data="tableData"
            :table-loading="tableLoading"
            style="margin-top: 0.5em;">
        </limsTable>
      </div>
    </el-dialog>
    <el-dialog
      v-model="lookDialogVisible"
      fullscreen
      title="查看附件" top="5vh" width="800px">
        v-model="lookDialogVisible"
        fullscreen
        title="查看附件" top="5vh" width="800px">
      <filePreview v-if="lookDialogVisible" :currentFile="{}"
                   :fileUrl="javaApi+'/img/'+currentInfo.fileUrl" style="max-height: 90vh;overflow-y: auto;"/>
                   :fileUrl="fileUrl(currentInfo)" style="max-height: 90vh;overflow-y: auto;"/>
    </el-dialog>
  </div>
</template>
@@ -27,11 +29,12 @@
import file from '@/utils/file';
import filePreview from '@/components/Preview/filePreview.vue';
import limsTable from '@/components/Table/lims-table.vue'
import {getCurrentWorkFiles} from '@/api/cnas/process/method/methodVerification'
export default {
  name: 'viewWorkPermitDia',
  // import å¼•入的组件需要注入到对象中才能使用
  components: { filePreview, limsTable },
  components: {filePreview, limsTable},
  data() {
    // è¿™é‡Œå­˜æ”¾æ•°æ®
    return {
@@ -74,7 +77,7 @@
      ],
      tableData: [],
      info: {},
      currentInfo:{},
      currentInfo: {},
      lookDialogVisible: false,
    };
  },
@@ -83,27 +86,60 @@
  },
  // æ–¹æ³•集合
  methods: {
    openDia(row) {
      this.filesDialogVisible = true
      this.info = row
      this.tableData = this.info.workFileList
    fileUrl(row) {
      const image = /\.(png|jpe?g|gif|bmp|webp)$/i.test(row.fileUrl || '')
      return this.javaApi + (image ? '/img/' : '/word/') + row.fileUrl
    },
    closeFilesLook () {
    // ä¸¤ç§ç”¨æ³•:数组=检测人员id(实时查询);行对象=优先展示已保存的上岗证,其次按已选检测人员实时查询
    openDia(payload = {}) {
      this.filesDialogVisible = true
      this.tableData = []
      if (Array.isArray(payload)) {
        this.loadCurrentFiles(payload)
        return
      }
      this.info = payload || {}
      if (Array.isArray(payload.workFileList) && payload.workFileList.length) {
        this.tableData = payload.workFileList
        return
      }
      this.loadCurrentFiles(Array.isArray(payload.confirmUser) ? payload.confirmUser : [])
    },
    loadCurrentFiles(userIds) {
      if (!userIds.length) return
      this.tableLoading = true
      getCurrentWorkFiles({userIds: userIds.join(',')}).then(res => {
        if (res.code === 200) this.tableData = res.data || []
      }).catch(() => {
        this.$message.error('上岗证读取失败,请稍后重试')
      }).finally(() => {
        this.tableLoading = false
      })
    },
    closeFilesLook() {
      this.filesDialogVisible = false
    },
    // æŸ¥çœ‹æ–‡ä»¶
    handleLook(row){
    handleLook(row) {
      if (!row.fileUrl) {
        this.$message.warning('该人员已有任职授权记录,但尚未上传上岗证书文件')
        return
      }
      this.currentInfo = row
      this.lookDialogVisible = true
    },
    // ä¸‹è½½
    upload (row) {
    upload(row) {
      if (!row.fileUrl) {
        this.$message.warning('该人员已有任职授权记录,但尚未上传上岗证书文件')
        return
      }
      let url = '';
      if(row.type==1){
        url = this.javaApi+'/img/'+row.fileUrl
        file.downloadIamge(url,row.fileName)
      }else{
        url = this.javaApi+'/word/'+row.fileUrl
      if (/\.(png|jpe?g|gif|bmp|webp)$/i.test(row.fileUrl || '')) {
        url = this.javaApi + '/img/' + row.fileUrl
        file.downloadIamge(url, row.fileName)
      } else {
        url = this.javaApi + '/word/' + row.fileUrl
        const link = document.createElement('a');
        link.href = url;
        link.download = row.fileName;
src/views/CNAS/process/method/methodVerification/component/wordFormDIa.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,297 @@
<template>
  <div>
    <el-dialog v-model="visible" :title="dialogTitle"
               width="90%" :close-on-click-modal="false" @close="closeDia">
      <div class="form-body">
        <table class="verify-table">
          <colgroup>
            <col style="width: 16%"/>
            <col style="width: 36%"/>
            <col style="width: 16%"/>
            <col style="width: 32%"/>
          </colgroup>
          <tbody>
          <tr>
            <th>标准名称</th>
            <td>
              <el-input v-model="form.standardName"/>
            </td>
            <th>标准编号</th>
            <td>
              <el-input v-model="form.standardCode"/>
            </td>
          </tr>
          <tr>
            <th>验证项目</th>
            <td colspan="3">
              <el-input v-model="form.verifyItem"/>
            </td>
          </tr>
          </tbody>
        </table>
        <table class="verify-table content-table">
          <colgroup>
            <col style="width: 16%"/>
            <col style="width: 36%"/>
            <col style="width: 36%"/>
            <col style="width: 12%"/>
          </colgroup>
          <thead>
          <tr>
            <th>验证内容</th>
            <th>方法要求</th>
            <th>实验室能力</th>
            <th>是否满足</th>
          </tr>
          </thead>
          <tbody>
          <tr v-for="(item, index) in content.checks" :key="item.key">
            <th>{{ index < 7 ? `${index + 1}. ${item.label}` : item.label }}</th>
            <td>
              <el-input v-model="item.requirement" type="textarea" :rows="3"/>
            </td>
            <td>
              <el-input v-model="item.capability" type="textarea" :rows="3"/>
              <div v-if="item.key === 'personnel'" class="row-extra">
                <span>检测人员</span>
                <el-select v-model="form.confirmUser" multiple :multiple-limit="5" placeholder="请选择检测人员">
                  <el-option v-for="user in userList" :key="user.id" :label="user.name" :value="user.id"/>
                </el-select>
                <el-link v-if="operationType === 'edit'" type="primary" @click="viewWorkPermit">查看上岗证</el-link>
              </div>
              <div v-if="item.key === 'equipment'" class="row-extra">
                <el-link type="primary" @click="viewDevice">查看设备</el-link>
                <el-link v-if="operationType === 'edit'" type="primary" @click="viewCalibrationsFileDia">查看校准证书
                </el-link>
              </div>
            </td>
            <td class="choice-cell">
              <el-radio-group v-model="item.satisfied">
                <el-radio :value="1">是</el-radio>
                <el-radio :value="0">否</el-radio>
              </el-radio-group>
            </td>
          </tr>
          <tr>
            <th>验证结论</th>
            <td colspan="3">
              <el-input v-model="content.conclusion" type="textarea" :rows="4"/>
            </td>
          </tr>
          <tr v-for="decision in decisions" :key="decision.key">
            <th>{{ decision.label }}</th>
            <td colspan="3">
              <div class="decision-row">
                <el-radio-group v-model="content[decision.key]">
                  <el-radio value="同意使用">同意使用</el-radio>
                  <el-radio value="不能使用">不能使用</el-radio>
                  <el-radio value="限制使用">限制使用</el-radio>
                </el-radio-group>
                <span>{{ decision.personLabel }}</span>
                <el-input v-model="content[decision.personKey]" class="person-input"/>
                <span>日期</span>
                <el-date-picker v-model="content[decision.dateKey]" type="date" value-format="YYYY-MM-DD"
                                placeholder="选择日期"/>
              </div>
            </td>
          </tr>
          </tbody>
        </table>
        <div v-if="operationType === 'edit'" class="attachments">
          <span>附件1 åŽŸå§‹è®°å½•ã€é™„ä»¶2 éªŒè¯ç»“æžœ</span>
          <el-button size="small" @click="viewTestRecord">查看及上传附件</el-button>
        </div>
      </div>
      <template #footer>
        <el-button @click="closeDia">取消</el-button>
        <el-button v-if="operationType !== 'view'" :loading="loading" type="primary" @click="submit">提交</el-button>
      </template>
    </el-dialog>
    <ViewDeviceDialog v-if="deviceVisible" ref="device" @closDeviceDia="deviceVisible = false"
                      @handleDeviceInfo="setDevices"/>
    <ViewTestRecord v-if="recordVisible" ref="record"/>
    <ViewWorkPermitDia v-if="permitVisible" ref="permit"/>
    <CalibrationsFileDia v-if="calibrationVisible" ref="calibration"/>
  </div>
</template>
<script>
import ViewDeviceDialog from './ViewDeviceDialog.vue'
import ViewTestRecord from './ViewTestRecord.vue'
import ViewWorkPermitDia from './viewWorkPermitDia.vue'
import CalibrationsFileDia from './calibrationsFileDia.vue'
import {addMethodVerify, getMethodVerifyOne, updateMethodVerify} from '@/api/cnas/process/method/methodVerification'
import {selectUserCondition} from '@/api/cnas/process/method/methodVerification'
const labels = [
  ['personnel', '人员能力'], ['equipment', '检测设备'], ['reagents', '试剂要求'],
  ['conditions', '试验条件'], ['sample', '样品处置'], ['steps', '试验步骤'],
  ['processing', '数据处理'], ['qualityControl', '质量控制方法'], ['result', '方法验证结果']
]
const decisions = [
  {key: 'reviewDecision', label: '评审意见', personKey: 'reviewer', personLabel: '评审人员', dateKey: 'reviewDate'},
  {
    key: 'approvalDecision',
    label: '审批意见',
    personKey: 'approver',
    personLabel: '技术负责人',
    dateKey: 'approvalDate'
  }
]
function emptyContent() {
  return {
    checks: labels.map(([key, label]) => ({key, label, requirement: '', capability: '', satisfied: null})),
    conclusion: '',
    reviewDecision: '',
    reviewer: '',
    reviewDate: '',
    approvalDecision: '',
    approver: '',
    approvalDate: ''
  }
}
export default {
  name: 'MethodVerificationWordForm',
  components: {ViewDeviceDialog, ViewTestRecord, ViewWorkPermitDia, CalibrationsFileDia},
  props: {operationType: {type: String, default: 'add'}},
  computed: {
    dialogTitle() {
      if (this.operationType === 'view') return '查看方法验证'
      return this.operationType === 'edit' ? '编辑方法验证' : '新增方法验证'
    }
  },
  data() {
    return {
      visible: false, loading: false, decisions,
      form: {standardName: '', standardCode: '', verifyItem: '', confirmUser: [], machineAttachmentList: []},
      content: emptyContent(), info: {}, userList: [],
      deviceVisible: false, recordVisible: false, permitVisible: false, calibrationVisible: false
    }
  },
  methods: {
    openDia(row = {}) {
      this.visible = true
      this.info = row || {}
      selectUserCondition().then(res => {
        if (res.code === 200) this.userList = res.data || []
      })
      if (this.operationType !== 'add') this.load(row)
    },
    load(row) {
      getMethodVerifyOne({methodVerifyId: row.methodVerifyId}).then(res => {
        if (res.code !== 200) return
        this.form = {...res.data, confirmUser: res.data.confirmUser ? res.data.confirmUser.split(',').map(Number) : []}
        try {
          const saved = JSON.parse(res.data.verificationContent || '{}')
          const defaults = emptyContent()
          this.content = {
            ...defaults, ...saved, checks: defaults.checks.map(item => ({
              ...item, ...(saved.checks || []).find(check => check.key === item.key)
            }))
          }
        } catch (_) {
          this.content = emptyContent()
          this.$message.error('验证内容格式错误,请联系管理员')
        }
      })
    },
    submit() {
      if (!this.form.standardName?.trim() || !this.form.standardCode?.trim() || !this.form.verifyItem?.trim()) {
        this.$message.warning('请填写标准名称、标准编号和验证项目')
        return
      }
      this.loading = true
      const payload = {
        ...this.form, operationType: 1,
        confirmUser: (this.form.confirmUser || []).join(','),
        verificationContent: JSON.stringify(this.content)
      }
      const request = this.operationType === 'edit' ? updateMethodVerify : addMethodVerify
      request(payload).then(res => {
        if (res.code === 200) {
          this.$message.success('操作成功');
          this.closeDia()
        }
      }).finally(() => {
        this.loading = false
      })
    },
    openChild(flag, refName, data) {
      this[flag] = true
      this.$nextTick(() => this.$refs[refName]?.openDia(data))
    },
    viewWorkPermit() {
      if (!this.form.confirmUser?.length) {
        this.$message.warning('请先选择检测人员')
        return
      }
      this.openChild('permitVisible', 'permit', this.form.confirmUser)
    },
    viewDevice() {
      this.openChild('deviceVisible', 'device', this.form)
    },
    viewCalibrationsFileDia() {
      this.openChild('calibrationVisible', 'calibration', this.form)
    },
    viewTestRecord() {
      this.openChild('recordVisible', 'record', this.info)
    },
    setDevices(list) {
      this.deviceVisible = false;
      this.form.machineAttachmentList = list
    },
    closeDia() {
      this.visible = false;
      this.$emit('closeDia')
    }
  }
}
</script>
<style scoped>
.form-body {
  max-height: 70vh;
  overflow: auto;
}
.verify-table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 14px;
  table-layout: fixed;
}
.verify-table th, .verify-table td {
  border: 1px solid #c5cbd0;
  padding: 10px;
  vertical-align: middle;
}
.verify-table th {
  background: #f5f7f8;
  font-weight: 500;
  text-align: center;
}
.choice-cell {
  text-align: center;
}
.row-extra, .decision-row, .attachments {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 8px;
}
.person-input {
  width: 140px;
}
.attachments {
  margin: 16px 0;
}
</style>
src/views/CNAS/process/method/methodVerification/index.vue
@@ -3,8 +3,8 @@
    <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: var(--lims-text-2);">标准方法</span>
          <el-input size="small" placeholder="请输入" clearable v-model="searchForm.methodName"
          <span style="width: 88px;font-size: 14px;font-weight: 700;color: var(--lims-text-2);">标准名称</span>
          <el-input size="small" placeholder="请输入" clearable v-model="searchForm.standardName"
                    @keyup.enter="searchList"></el-input>
        </div>
        <div style="line-height: 30px;">
@@ -22,24 +22,26 @@
                 @pagination="pagination">
      </limsTable>
    </div>
    <formDIa v-if="formDIa" ref="formDIa" :operationType="operationType" @closeDia="closeDia"></formDIa>
    <component :is="isWordForm ? 'wordFormDIa' : 'formDIa'" v-if="formDIa" ref="formDIa"
               :operationType="operationType" @closeDia="closeDia" />
  </div>
</template>
<script>
import limsTable from '@/components/Table/lims-table.vue'
import formDIa from './component/formDIa.vue';
import wordFormDIa from './component/wordFormDIa.vue';
import { delMethodVerify, exportMethodVerify, pagesMethodVerify } from '@/api/cnas/process/method/methodVerification'
export default {
  name: 'MethodVerification',
  // import å¼•入的组件需要注入到对象中才能使用
  components: { limsTable, formDIa },
  components: { limsTable, formDIa, wordFormDIa },
  data() {
    // è¿™é‡Œå­˜æ”¾æ•°æ®
    return {
      searchForm: {
        methodName: '',
        standardName: '',
        operationType: 1,
      },
      options: [
@@ -48,18 +50,18 @@
      ],
      tableColumn: [
        {
          label: '标准方法',
          prop: 'methodName',
          label: '标准名称',
          prop: 'standardName',
          minWidth: '100'
        },
        {
          label: '验证原因',
          prop: 'verifyReason',
          label: '标准编号',
          prop: 'standardCode',
          minWidth: '100'
        },
        {
          label: '主要技术变化',
          prop: 'technologyChange',
          label: '验证项目',
          prop: 'verifyItem',
          minWidth: '100'
        },
        {
@@ -110,6 +112,7 @@
      total: 0,
      formDIa: false,
      operationType: '',
      isWordForm: true,
    };
  },
  mounted() {
@@ -120,7 +123,7 @@
    // æŸ¥è¯¢åˆ—表
    searchList() {
      const entity = {
        methodName: this.searchForm.methodName,
        standardName: this.searchForm.standardName,
        operationType: this.searchForm.operationType,
      }
      const page = this.page
@@ -158,12 +161,13 @@
    },
    // é‡ç½®æŸ¥è¯¢æ¡ä»¶
    resetSearchForm() {
      this.searchForm.methodName = '';
      this.searchForm.standardName = '';
      this.searchList()
    },
    openFormDia(type, row) {
      this.formDIa = true
      this.operationType = type
      this.isWordForm = type === 'add' || !!row?.standardCode
      this.$nextTick(() => {
        this.$refs.formDIa.openDia(row)
      })
vite.config.js
@@ -7,7 +7,7 @@
  const env = loadEnv(mode, process.cwd())
  const { VITE_APP_ENV } = env
  // åŽç«¯æŽ¥å£ï¼šé»˜è®¤æ‰“内网共享服务,本地起后端时用 VITE_APP_PROXY_TARGET è¦†ç›–
  const baseUrl = env.VITE_APP_PROXY_TARGET || 'http://192.168.0.24:9509/lims'
  const baseUrl = env.VITE_APP_PROXY_TARGET || 'http://127.0.0.1:9509'
  return {
    // éƒ¨ç½²ç”Ÿäº§çŽ¯å¢ƒå’Œå¼€å‘çŽ¯å¢ƒä¸‹çš„URL。
    // é»˜è®¤æƒ…况下,vite ä¼šå‡è®¾ä½ çš„应用是被部署在一个域名的根路径上
@@ -61,6 +61,7 @@
    server: {
      port: 80,
      host: true,
      allowedHosts: ['.trycloudflare.com'],
      open: true,
      proxy: {
        // https://cn.vitejs.dev/config/#server-proxy