吴羡
9 小时以前 d2eed0eb7c2f9537d5eaab7ccc3b6ab258959227
feat(device): 设备核查计划附件上传与运行记录导出调整

- 核查计划表单接入附件上传组件
- 附件组件支持只读、内联展示与延迟上传
- 仪器设备运行记录导出支持按月份筛选并校验无记录情况
已修改6个文件
1248 ■■■■■ 文件已修改
src/api/cnas/resourceDemand/device.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Attachment/lims-attachment.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/device/component/checkRecord.vue 317 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/device/component/equipmentVerificationPlan.vue 391 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/device/component/record.vue 385 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/device/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/cnas/resourceDemand/device.js
@@ -379,6 +379,15 @@
  });
}
// 查找可复用的核查计划明细(只查询,不新增)
export function findExaminePlanDetail(query) {
  return request({
    url: "/deviceExaminePlan/findExaminePlanDetail",
    method: "get",
    params: query,
  });
}
// 新增设备核查计划详情
export function addDeviceExaminePlanDetail(data) {
  return request({
src/components/Attachment/lims-attachment.vue
@@ -1,10 +1,39 @@
<template>
  <div>
    <el-dialog :title="dialogTitle" v-model="visible" width="760px">
  <div class="lims-attachment">
    <div v-if="inline" class="attachment-inline">
      <div class="attachment-header">
        <div class="el-upload__tip">{{ tip }}</div>
        <el-upload :action="uploadAction" :headers="uploadHeader" multiple :show-file-list="false"
          :on-success="handleUploadSuccess" :on-error="handleUploadError">
        <div class="el-upload__tip">
          {{ readonly ? '仅可预览、下载附件' : ((currentId || deferUpload) ? tip : emptyTip) }}
        </div>
        <el-upload v-if="!readonly" :action="uploadAction" :headers="uploadHeader" multiple
          :show-file-list="false" :disabled="!canUpload" :auto-upload="!deferUpload"
          :on-change="handleFileChange" :on-success="handleUploadSuccess"
          :on-error="handleUploadError">
          <el-button size="small" type="primary" :disabled="!canUpload">上传附件</el-button>
        </el-upload>
      </div>
      <el-table :data="displayList" border size="small" max-height="180" style="margin-top:8px">
        <el-table-column label="文件名称" prop="fileName" min-width="220" show-overflow-tooltip />
        <el-table-column label="上传时间" prop="createTime" width="160" />
        <el-table-column label="操作" width="170">
          <template #default="scope">
            <template v-if="scope.row.pending">
              <el-button link type="danger" @click="removePending(scope.row)">删除</el-button>
            </template>
            <template v-else>
              <el-button link type="primary" @click="previewFile(scope.row)">预览</el-button>
              <el-button link type="primary" @click="downloadFile(scope.row)">下载</el-button>
              <el-button v-if="!readonly" link type="danger" @click="removeFile(scope.row)">删除</el-button>
            </template>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <el-dialog v-else :title="dialogTitle" v-model="visible" width="760px" append-to-body @close="$emit('closed')">
      <div class="attachment-header">
        <div class="el-upload__tip">{{ readonly ? '仅可预览、下载附件' : tip }}</div>
        <el-upload v-if="!readonly" :action="uploadAction" :headers="uploadHeader" multiple
          :show-file-list="false" :on-success="handleUploadSuccess" :on-error="handleUploadError">
          <el-button size="small" type="primary">上传附件</el-button>
        </el-upload>
      </div>
@@ -15,7 +44,7 @@
          <template #default="scope">
            <el-button link type="primary" @click="previewFile(scope.row)">预览</el-button>
            <el-button link type="primary" @click="downloadFile(scope.row)">下载</el-button>
            <el-button link type="danger" @click="removeFile(scope.row)">删除</el-button>
            <el-button v-if="!readonly" link type="danger" @click="removeFile(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -51,6 +80,7 @@
  components: {
    filePreview
  },
  emits: ['closed'],
  props: {
    // 上传接口地址,如 /supplierManagement/uploadAttachment
    uploadUrl: {
@@ -91,12 +121,54 @@
    tip: {
      type: String,
      default: '支持任意格式文件,单个文件不超过200MB'
    },
    // 只读模式:仅预览/下载,不显示上传和删除
    readonly: {
      type: Boolean,
      default: false
    },
    // 内嵌模式:直接渲染上传区和附件列表,不弹管理弹窗
    inline: {
      type: Boolean,
      default: false
    },
    // 内嵌模式关联主表ID,变化时自动加载附件
    mainId: {
      type: [String, Number],
      default: ''
    },
    // 内嵌模式未关联主表ID时的提示
    emptyTip: {
      type: String,
      default: '请先选择数据后再上传附件'
    },
    // 内嵌模式:没有主表ID时也允许选择文件,先暂存在页面,提交时再统一上传
    deferUpload: {
      type: Boolean,
      default: false
    }
  },
  watch: {
    mainId: {
      immediate: true,
      handler(val) {
        if (!this.inline) {
          return
        }
        const field = this.idField || this.idParam
        this.currentRow = { [field]: val }
        this.attachmentList = []
        if (val !== null && val !== undefined && val !== '') {
          this.loadList()
        }
      }
    }
  },
  data() {
    return {
      visible: false,
      attachmentList: [],
      pendingFiles: [],
      currentRow: {},
      previewVisible: false,
      previewRow: {},
@@ -107,6 +179,19 @@
    currentId() {
      const field = this.idField || this.idParam
      return this.currentRow ? this.currentRow[field] : undefined
    },
    canUpload() {
      return !this.readonly && (!!this.currentId || this.deferUpload)
    },
    displayList() {
      const pending = this.pendingFiles.map(item => ({
        id: 'pending_' + item.uid,
        uid: item.uid,
        fileName: item.fileName,
        createTime: '待上传',
        pending: true
      }))
      return pending.concat(this.attachmentList)
    },
    dialogTitle() {
      const name = this.nameField && this.currentRow ? this.currentRow[this.nameField] : ''
@@ -171,6 +256,42 @@
    handleUploadError() {
      this.$message.error('上传失败,文件可能超过200MB')
    },
    // 延迟上传模式:文件先暂存在页面,提交保存时再统一上传
    handleFileChange(file) {
      if (!this.deferUpload || !file || !file.raw) {
        return
      }
      this.pendingFiles.push({ uid: file.uid, fileName: file.name, raw: file.raw })
    },
    removePending(row) {
      this.pendingFiles = this.pendingFiles.filter(item => item.uid !== row.uid)
    },
    // 提交时调用:把暂存的附件上传到指定主表ID,全部成功才算完成
    flushPending(planDetailsId) {
      if (!this.pendingFiles.length) {
        return Promise.resolve()
      }
      const field = this.idField || this.idParam
      this.currentRow = { [field]: planDetailsId }
      return Promise.all(this.pendingFiles.slice().map(item => {
        const formData = new FormData()
        formData.append('file', item.raw)
        return request({
          url: this.uploadUrl,
          method: 'post',
          params: { [this.idParam]: planDetailsId },
          data: formData,
          headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false }
        }).then(res => {
          if (!res || res.code !== 200) {
            throw new Error((res && res.msg) || ('附件“' + item.fileName + '”上传失败'))
          }
          this.pendingFiles = this.pendingFiles.filter(pending => pending.uid !== item.uid)
        })
      })).then(() => {
        this.loadList()
      })
    },
    previewFile(row) {
      this.previewRow = row
      this.previewUrl = this.fullUrl(row.fileUrl)
@@ -201,9 +322,17 @@
</script>
<style scoped>
.lims-attachment {
  width: 100%;
}
.attachment-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.attachment-inline {
  width: 100%;
}
</style>
src/views/CNAS/resourceDemand/device/component/checkRecord.vue
@@ -1,61 +1,125 @@
<template>
  <div>
    <el-dialog :close-on-click-modal="false" :close-on-press-escape="false" v-model="dialogVisible"
      :title="currentStep === 0 ? '新增核查记录' : '设备核查记录'" width="50%" @close="resetForm">
      <el-steps v-if="currentStep === 1" :active="1" align-center finish-status="success">
      :title="currentStep === 0 ? '新增核查记录' : '设备核查记录'" top="5vh" width="960px" @close="resetForm">
      <el-steps v-if="currentStep === 1" class="record-steps" :active="1" align-center finish-status="success">
        <el-step title="填写核查记录"></el-step>
        <el-step title="审核"></el-step>
      </el-steps>
      <el-form ref="recordForm" :model="form" :rules="rules" label-width="110px">
        <el-form-item label="设备编号:" prop="deviceNumber">
          <el-input v-model="form.deviceNumber" disabled size="small"></el-input>
        </el-form-item>
        <el-form-item label="设备名称:" prop="deviceName">
          <el-select v-if="currentStep === 0" v-model="selectedDeviceId" clearable filterable
            placeholder="请选择设备" size="small" style="width: 100%" @change="changeMachineName">
            <el-option v-for="item in equipOptions" :key="item.id" :label="item.label + '-' + item.value"
              :value="item.id"></el-option>
          </el-select>
          <el-input v-else v-model="form.deviceName" disabled size="small"></el-input>
        </el-form-item>
        <el-form-item label="核查时间:" prop="checkTime">
          <el-date-picker v-model="form.checkTime" :disabled="currentStep !== 0" clearable format="YYYY-MM"
            placeholder="选择日期" size="small" style="width: 100%" type="month" value-format="YYYY-MM">
          </el-date-picker>
        </el-form-item>
        <el-form-item label="核查指标:" prop="checkIndex">
          <el-input v-model="form.checkIndex" :disabled="currentStep !== 0" clearable size="small"></el-input>
        </el-form-item>
        <el-form-item label="核查方法:" prop="checkMethod">
          <el-input v-model="form.checkMethod" :disabled="currentStep !== 0" clearable size="small"
            type="textarea"></el-input>
        </el-form-item>
        <el-form-item label="结果如何判定:" prop="howResults">
          <el-input v-model="form.howResults" :disabled="currentStep !== 0" clearable size="small"
            type="textarea"></el-input>
        </el-form-item>
        <el-form-item label="核查责任人:" prop="checkChargerUserId">
          <el-select v-if="currentStep === 0" v-model="form.checkChargerUserId" clearable filterable
            placeholder="请选择" size="small" style="width: 100%" @change="setCheckChargerUser">
            <el-option v-for="item in chargerOptions" :key="item.id" :label="item.name"
              :value="item.id"></el-option>
          </el-select>
          <el-input v-else v-model="form.checkChargerUser" disabled size="small"></el-input>
        </el-form-item>
        <el-form-item label="备注:" prop="remark">
          <el-input v-model="form.remark" :disabled="currentStep !== 0" clearable size="small"></el-input>
        </el-form-item>
        <el-form-item label="附件:">
          <el-upload v-if="currentStep === 0" :action="javaApi + '/deviceScope/uploadFile'" :headers="uploadHeader"
            :limit="1" :on-exceed="handleUploadExceed" :before-upload="beforeAttachmentUpload"
            :on-success="handleAttachmentSuccess" :on-remove="handleAttachmentRemove">
            <el-button size="small" type="primary">选择附件</el-button>
          </el-upload>
          <el-link v-if="form.fileUrl" type="primary" @click="openAttachment">
            {{ form.fileName || '查看附件' }}
          </el-link>
          <span v-else-if="currentStep !== 0">—</span>
        </el-form-item>
      <el-form ref="recordForm" :model="form" :rules="rules" label-width="104px">
        <div class="form-section">
          <div class="form-section-title">基本信息</div>
          <el-row :gutter="12">
            <el-col :span="12">
              <el-form-item label="仪器名称:" prop="deviceName">
                <el-select v-if="currentStep === 0" v-model="selectedDeviceId" clearable filterable
                  placeholder="请选择设备" size="small" style="width: 100%" @change="changeMachineName">
                  <el-option v-for="item in equipOptions" :key="item.id" :label="item.label + '-' + item.value"
                    :value="item.id"></el-option>
                </el-select>
                <el-input v-else v-model="form.deviceName" disabled size="small"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="检查日期:" prop="checkTime">
                <el-date-picker v-if="currentStep === 0" v-model="form.checkTime" clearable format="YYYY-MM-DD"
                  placeholder="选择检查日期" size="small" style="width: 100%" type="date" value-format="YYYY-MM-DD"
                  @change="resolveAttachmentPlan">
                </el-date-picker>
                <el-input v-else v-model="form.checkTime" disabled size="small"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="管理编号:" prop="deviceNumber">
                <el-input v-model="form.deviceNumber" disabled size="small"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="设备型号:" prop="deviceModel">
                <el-input v-model="form.deviceModel" disabled size="small"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="核查人:" prop="checkChargerUserId">
                <el-select v-if="currentStep === 0" v-model="form.checkChargerUserId" clearable filterable
                  placeholder="请选择" size="small" style="width: 100%" @change="setCheckChargerUser">
                  <el-option v-for="item in chargerOptions" :key="item.id" :label="item.name"
                    :value="item.id"></el-option>
                </el-select>
                <el-input v-else v-model="form.checkChargerUser" disabled size="small"></el-input>
              </el-form-item>
            </el-col>
          </el-row>
        </div>
        <div class="form-section">
          <div class="form-section-title">核查内容</div>
          <el-row :gutter="12">
            <el-col :span="8">
              <el-form-item label="环境温湿度:" prop="environment">
                <el-input v-model="form.environment" :disabled="currentStep !== 0" clearable size="small"
                  placeholder="如:25℃ / 50%RH"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="核查依据:" prop="checkBasis">
                <el-input v-model="form.checkBasis" :disabled="currentStep !== 0" clearable size="small"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="核查指标:" prop="checkIndex">
                <el-input v-model="form.checkIndex" :disabled="currentStep !== 0" clearable size="small"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="核查方式:" prop="checkMethod">
                <el-input v-model="form.checkMethod" :disabled="currentStep !== 0" clearable :rows="2" size="small"
                  type="textarea"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="结果如何判定:" prop="howResults">
                <el-input v-model="form.howResults" :disabled="currentStep !== 0" clearable :rows="2" size="small"
                  type="textarea"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="外观情况:">
                <el-radio-group v-model="form.appearanceStatus" :disabled="currentStep !== 0">
                  <el-radio value="良好">良好</el-radio>
                  <el-radio value="一般">一般</el-radio>
                  <el-radio value="损坏">损坏</el-radio>
                </el-radio-group>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="运行状态:">
                <el-radio-group v-model="form.runningStatus" :disabled="currentStep !== 0">
                  <el-radio value="正常">正常</el-radio>
                  <el-radio value="异常">异常</el-radio>
                  <el-radio value="停机">停机</el-radio>
                </el-radio-group>
              </el-form-item>
            </el-col>
            <el-col :span="24">
              <el-form-item label="备注:" prop="remark">
                <el-input v-model="form.remark" :disabled="currentStep !== 0" clearable size="small"></el-input>
              </el-form-item>
            </el-col>
          </el-row>
        </div>
        <div class="form-section">
          <div class="form-section-title">附件资料</div>
          <el-form-item label-width="0">
            <lims-attachment ref="recordAttachment" inline upload-url="/deviceExaminePlan/uploadAttachment"
              select-url="/deviceExaminePlan/selectAttachments" delete-url="/deviceExaminePlan/deleteAttachment"
              id-param="planDetailsId" name-field="deviceName" title="核查记录附件"
              :main-id="form.planDetailsId" :readonly="currentStep === 1"
              :defer-upload="currentStep === 0" :tip="attachmentTip"></lims-attachment>
            <el-link v-if="form.fileUrl" type="primary" @click="openAttachment">
              {{ form.fileName || '查看原附件' }}
            </el-link>
          </el-form-item>
        </div>
      </el-form>
      <template #footer><span class="dialog-footer">
        <el-button @click="resetForm">取 消</el-button>
@@ -74,22 +138,28 @@
  addExamineRecord,
  reviewExamineRecord,
  deviceScopeSearch,
  findExaminePlanDetail,
  resolveExaminePlanDetail,
  updateDeviceExaminePlanDetail,
} from '@/api/cnas/resourceDemand/device.js'
import { selectUserCondition } from "@/api/performance/class";
import draftSave from '@/mixins/draftSave'
import LimsAttachment from '@/components/Attachment/lims-attachment.vue'
const createEmptyForm = () => ({
  deviceId: '',
  deviceName: '',
  deviceNumber: '',
  deviceModel: '',
  checkTime: '',
  checkIndex: '',
  checkMethod: '',
  howResults: '',
  checkChargerUserId: '',
  checkChargerUser: '',
  environment: '',
  checkBasis: '',
  appearanceStatus: '',
  runningStatus: '',
  remark: '',
  fileName: '',
  fileUrl: '',
@@ -97,7 +167,7 @@
})
export default {
  name: "check-record",
  mixins: [draftSave],
  components: { LimsAttachment },
  data() {
    // 这里存放数据
    return {
@@ -109,7 +179,7 @@
      submitFormLoading: false,
      userList: [],
      rules: {
        checkMethod: [{ required: true, message: '请填写核查方法', trigger: 'blur' }],
        checkMethod: [{ required: true, message: '请填写核查方式', trigger: 'blur' }],
      },
    }
  },
@@ -124,12 +194,17 @@
        deviceId: row.deviceId || '',
        deviceName: row.deviceName || '',
        deviceNumber: row.deviceNumber || '',
        checkTime: (row.checkTime || '').substring(0, 7),
        deviceModel: row.deviceModel || '',
        checkTime: row.checkTime || '',
        checkIndex: row.checkIndex || '',
        checkMethod: row.checkMethod || '',
        howResults: row.howResults || '',
        checkChargerUserId: row.checkChargerUserId || '',
        checkChargerUser: row.checkChargerUser || '',
        environment: row.environment || '',
        checkBasis: row.checkBasis || '',
        appearanceStatus: row.appearanceStatus || '',
        runningStatus: row.runningStatus || '',
        remark: row.remark || '',
        fileName: row.fileName || '',
        fileUrl: row.fileUrl || '',
@@ -143,17 +218,14 @@
      this.form = createEmptyForm()
      this.selectedDeviceId = ''
      const now = new Date()
      this.form.checkTime = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0')
      this.form.checkTime = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0')
      this.getEquipOptions()
      this.getUserList()
      this.$nextTick(() => {
        if (this.$refs.recordForm) {
          this.$refs.recordForm.clearValidate()
        }
        this.draftRestore('checkRecordForm', 'form', 'new', 'dialogVisible')
        if (this.form.deviceId) {
          this.selectedDeviceId = this.form.deviceId
        }
        this.resolveAttachmentPlan()
      })
    },
    // 按设备 id 回填名称和编号,避免同名设备选错
@@ -163,11 +235,14 @@
        this.form.deviceId = item.id
        this.form.deviceName = item.deviceName
        this.form.deviceNumber = item.managementNumber
        this.form.deviceModel = item.specificationModel || ''
      } else {
        this.form.deviceId = ''
        this.form.deviceName = ''
        this.form.deviceNumber = ''
        this.form.deviceModel = ''
      }
      this.resolveAttachmentPlan()
    },
    setCheckChargerUser(value) {
      const item = this.userList.find(user => user.id === value)
@@ -214,28 +289,40 @@
      if (this.form.howResults) params.howResults = this.form.howResults
      if (this.form.checkChargerUserId) params.checkChargerUserId = this.form.checkChargerUserId
      if (this.form.checkChargerUser) params.checkChargerUser = this.form.checkChargerUser
      if (this.form.deviceModel) params.deviceModel = this.form.deviceModel
      params.environment = this.form.environment || ''
      params.checkBasis = this.form.checkBasis || ''
      params.appearanceStatus = this.form.appearanceStatus || ''
      params.runningStatus = this.form.runningStatus || ''
      if (this.form.remark) params.remark = this.form.remark
      if (this.form.fileName) params.fileName = this.form.fileName
      if (this.form.fileUrl) params.fileUrl = this.form.fileUrl
      updateDeviceExaminePlanDetail(params).then(res => {
        if (res.code !== 200) {
          this.$message.error(res.msg || '保存核查记录失败')
          this.submitFormLoading = false
          return
        }
        addExamineRecord({ planDetailsId }).then(recordRes => {
          if (recordRes.code == 200) {
            this.$message.success('核查记录已提交,等待审核')
            this.draftClear('checkRecordForm')
            this.resetForm()
          } else {
            this.$message.error(recordRes.msg || '提交核查记录失败')
      // 附件先暂存在页面上,确认提交时再连同核查记录一起保存,取消则什么都不留
      const attachmentRef = this.$refs.recordAttachment
      const flushPending = attachmentRef ? attachmentRef.flushPending(planDetailsId) : Promise.resolve()
      flushPending.then(() => {
        updateDeviceExaminePlanDetail(params).then(res => {
          if (res.code !== 200) {
            this.$message.error(res.msg || '保存核查记录失败')
            this.submitFormLoading = false
            return
          }
          this.submitFormLoading = false
          addExamineRecord({ planDetailsId }).then(recordRes => {
            if (recordRes.code == 200) {
              this.$message.success('核查记录已提交,等待审核')
              this.resetForm()
            } else {
              this.$message.error(recordRes.msg || '提交核查记录失败')
            }
            this.submitFormLoading = false
          }).catch(() => {
            this.submitFormLoading = false
          })
        }).catch(() => {
          this.submitFormLoading = false
        })
      }).catch(() => {
        // 上传失败的具体原因由 request 拦截器统一提示
        this.submitFormLoading = false
      })
    },
@@ -249,7 +336,6 @@
      reviewExamineRecord(params).then(res => {
        if (res.code == 200) {
          this.$message.success(statue === 1 ? '审核通过' : '已退回')
          this.draftClear('checkRecordForm')
          this.resetForm()
        }
        this.submitFormLoading = false
@@ -258,7 +344,6 @@
      })
    },
    resetForm() {
      this.draftStop('checkRecordForm')
      this.dialogVisible = false
      this.$emit('closeDialog1')
    },
@@ -283,30 +368,27 @@
        this.userList = res.data
      })
    },
    // 附件
    beforeAttachmentUpload(file) {
      if (file.size > 1024 * 1024 * 10) {
        this.$message.error('上传文件不超过10M')
        return false
    // 附件内嵌在表单里:设备+日期确定后只查可复用明细,不提前建档,避免取消后留下空记录
    resolveAttachmentPlan() {
      if (this.currentStep !== 0 || !this.selectedDeviceId || !this.form.checkTime) {
        this.form.planDetailsId = ''
        return
      }
      return true
      const deviceId = this.selectedDeviceId
      const checkTime = this.form.checkTime
      this.form.planDetailsId = ''
      findExaminePlanDetail({ deviceId, checkTime }).then(res => {
        if (res.code !== 200 || !res.data) {
          return
        }
        // 请求期间用户可能已切换设备或日期,避免回填过期明细
        if (this.selectedDeviceId === deviceId && this.form.checkTime === checkTime) {
          this.form.planDetailsId = res.data.planDetailsId
        }
      }).catch(() => {
      })
    },
    handleUploadExceed() {
      this.$message.warning('仅支持一个附件,请先移除已上传的附件')
    },
    handleAttachmentSuccess(response) {
      if (response.code == 200) {
        this.form.fileName = response.data.name
        this.form.fileUrl = response.data.url
        this.$message.success('附件上传成功')
      } else {
        this.$message.error(response.msg || '附件上传失败')
      }
    },
    handleAttachmentRemove() {
      this.form.fileName = ''
      this.form.fileUrl = ''
    },
    // 兼容历史单附件
    openAttachment() {
      if (!this.form.fileUrl) {
        return
@@ -316,6 +398,10 @@
    },
  },
  computed: {
    // 附件不提前上传,选好后随核查记录一起提交保存
    attachmentTip() {
      return '可直接选择附件,点“确认”时随核查记录一起保存'
    },
    // 责任人下拉:已保存的责任人不在候选列表时补一条,避免回显成用户ID
    chargerOptions() {
      const list = this.userList || []
@@ -328,3 +414,36 @@
  },
}
</script>
<style scoped>
.record-steps {
  margin-bottom: 14px;
}
.form-section {
  padding: 12px 14px 0;
  border: 1px solid #ebeef5;
  border-radius: 6px;
  margin-bottom: 10px;
}
.form-section:last-child {
  margin-bottom: 0;
}
.form-section-title {
  display: flex;
  align-items: center;
  padding-left: 8px;
  border-left: 3px solid var(--el-color-primary);
  margin-bottom: 12px;
  font-size: 14px;
  font-weight: 600;
  line-height: 1;
  color: #303133;
}
.form-section :deep(.el-form-item) {
  margin-bottom: 12px;
}
</style>
src/views/CNAS/resourceDemand/device/component/equipmentVerificationPlan.vue
@@ -4,14 +4,14 @@
    <div style="margin: 10px 0 14px;">
      <div class="plan-top">
        <div class="plan-top-left">
          <span class="title" style="margin-bottom: 0;">{{ planInfo.planName || (planYear + '年度设备核查计划') }}</span>
          <span class="title" style="margin-bottom: 0;">{{ planInfo.planName || (planYear ? planYear + '年度设备核查计划' : '设备核查计划') }}</span>
          <el-select v-model="planYear" size="small" style="width: 110px; margin-left: 16px;" @change="handleYearChange">
            <el-option v-for="y in yearOptions" :key="y" :label="y" :value="y"></el-option>
          </el-select>
        </div>
        <div>
          <el-button size="small" type="primary" @click="handleAddRecord">新增核查记录</el-button>
          <el-button size="small" type="primary" @click="loadPlan">刷新</el-button>
          <el-button size="small" type="primary" @click="loadYearOptions">刷新</el-button>
          <el-button size="small" type="primary" @click="downLoadPost">导出</el-button>
        </div>
      </div>
@@ -57,7 +57,7 @@
        <el-table-column label="核查方法" min-width="140" prop="checkMethod" show-overflow-tooltip></el-table-column>
        <el-table-column label="结果如何判定" min-width="140" prop="howResults" show-overflow-tooltip></el-table-column>
        <el-table-column label="核查负责人" min-width="120" prop="checkChargerUser"></el-table-column>
        <el-table-column label="记录状态" min-width="100" prop="recordStatus">
        <el-table-column label="审核状态" min-width="100" prop="recordStatus">
          <template #default="scope">
            <el-tag type="" v-if="scope.row.recordStatus === 0" disable-transitions>未开始</el-tag>
            <el-tag type="warning" v-if="scope.row.recordStatus === 1" disable-transitions>待批准</el-tag>
@@ -65,18 +65,13 @@
            <el-tag type="danger" v-if="scope.row.recordStatus === 3" disable-transitions>不通过</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="对比状态" min-width="100" prop="recordContrastStatus">
          <template #default="scope">
            <el-tag type="" v-if="scope.row.recordContrastStatus === 0" disable-transitions>未开始</el-tag>
            <el-tag type="warning" v-if="scope.row.recordContrastStatus === 1" disable-transitions>待批准</el-tag>
            <el-tag type="success" v-if="scope.row.recordContrastStatus === 2" disable-transitions>通过</el-tag>
            <el-tag type="danger" v-if="scope.row.recordContrastStatus === 3" disable-transitions>不通过</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="备注" min-width="130" prop="remark"></el-table-column>
        <el-table-column label="附件" min-width="140">
          <template #default="scope">
            <el-link v-if="scope.row.fileUrl" type="primary" @click="openAttachment(scope.row)">
            <el-link v-if="scope.row.attachmentCount > 0" type="primary" @click="handleAttachment(scope.row)">
              查看附件({{ scope.row.attachmentCount }})
            </el-link>
            <el-link v-else-if="scope.row.fileUrl" type="primary" @click="openAttachment(scope.row)">
              {{ scope.row.fileName || '查看附件' }}
            </el-link>
            <span v-else>—</span>
@@ -86,17 +81,14 @@
        <el-table-column align="center" fixed="right" label="操作" min-width="320">
          <template #default="scope">
            <el-button size="small" link type="primary"
              :disabled="scope.row.recordStatus === 2 || scope.row.recordStatus === 3"
              @click="handleForm('edit', scope.row)">编辑</el-button>
            <el-button size="small" link type="primary" @click="handleForm('view', scope.row)">查看</el-button>
            <el-button v-if="scope.row.recordStatus === 1" size="small" link type="primary"
            <el-button v-if="scope.row.recordStatus === 0 || scope.row.recordStatus === 1"
              size="small" link type="primary" :disabled="!scope.row.checkChargerUser"
              @click="handleRecord(scope.row)">审核</el-button>
            <el-upload class="row-upload" :action="javaApi + '/deviceScope/uploadFile'"
              :headers="uploadHeader" :show-file-list="false" :before-upload="beforeAttachmentUpload"
              :on-success="(res) => handleAttachmentSuccess(res, scope.row)"
              :on-error="handleAttachmentError">
              <el-button size="small" link type="primary">上传附件</el-button>
            </el-upload>
            <el-button size="small" link type="primary" @click="handleDown(scope.row)">导出</el-button>
            <el-button v-if="scope.row.recordStatus === 2 || scope.row.recordStatus === 3"
              size="small" link type="primary" @click="handleDown(scope.row)">导出</el-button>
            <el-button v-if="scope.row.recordStatus === 0 && scope.row.recordContrastStatus === 0"
              size="small" link type="danger" @click="deleteFun1(scope.row.planDetailsId)">删除</el-button>
          </template>
@@ -108,44 +100,116 @@
        @size-change="handleSizeChange1" @current-change="handleCurrentChange1">
      </el-pagination>
    </div>
    <el-dialog :close-on-click-modal="false" :close-on-press-escape="false" v-model="dialogVisible" title="设备核查计划"
      width="50%" @close="resetForm">
      <el-form ref="modelForm" :model="form" :rules="rules" label-width="110px">
        <el-form-item label="设备编号:" prop="deviceNumber">
          <el-input v-model="form.deviceNumber" :disabled="operationType === 'view'" clearable
            size="small"></el-input>
        </el-form-item>
        <el-form-item label="设备名称:" prop="deviceName">
          <el-input v-model="form.deviceName" :disabled="operationType === 'view'" clearable
            size="small"></el-input>
        </el-form-item>
        <el-form-item label="核查时间:" prop="checkTime">
          <el-date-picker v-model="form.checkTime" :disabled="operationType === 'view'" clearable format="YYYY-MM"
            placeholder="选择日期" size="small" style="width:100%" type="month" value-format="YYYY-MM">
          </el-date-picker>
        </el-form-item>
        <el-form-item label="核查指标:" prop="checkIndex">
          <el-input v-model="form.checkIndex" :disabled="operationType === 'view'" clearable
            size="small"></el-input>
        </el-form-item>
        <el-form-item label="核查方法:" prop="checkMethod">
          <el-input v-model="form.checkMethod" :disabled="operationType === 'view'" clearable size="small"
            type="textarea"></el-input>
        </el-form-item>
        <el-form-item label="结果如何判定:" prop="howResults">
          <el-input v-model="form.howResults" :disabled="operationType === 'view'" clearable size="small"
            type="textarea"></el-input>
        </el-form-item>
        <el-form-item label="核查责任人:" prop="checkChargerUserId">
          <el-select v-model="form.checkChargerUserId" @change="setCheckChargerUser"
            :disabled="operationType === 'view'" placeholder="请选择" size="small" style="width: 100%">
            <el-option v-for="item in chargerOptions" :key="item.id" :label="item.name"
              :value="item.id"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="备注:" prop="remark">
          <el-input v-model="form.remark" :disabled="operationType === 'view'" clearable size="small"></el-input>
        </el-form-item>
    <el-dialog :close-on-click-modal="false" :close-on-press-escape="false" v-model="dialogVisible"
      :title="operationType === 'view' ? '设备核查记录' : '编辑核查记录'" top="5vh" width="960px" @close="resetForm">
      <el-form ref="modelForm" :model="form" :rules="rules" label-width="104px">
        <div class="form-section">
          <div class="form-section-title">基本信息</div>
          <el-row :gutter="12">
            <el-col :span="12">
              <el-form-item label="仪器名称:" prop="deviceName">
                <el-input v-model="form.deviceName" disabled size="small"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="检查日期:" prop="checkTime">
                <el-date-picker v-model="form.checkTime" :disabled="operationType === 'view'" clearable
                  format="YYYY-MM" placeholder="选择检查日期" size="small" style="width:100%" type="month"
                  value-format="YYYY-MM">
                </el-date-picker>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="管理编号:" prop="deviceNumber">
                <el-input v-model="form.deviceNumber" disabled size="small"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="设备型号:" prop="deviceModel">
                <el-input v-model="form.deviceModel" disabled size="small"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="核查人:" prop="checkChargerUserId">
                <el-select v-model="form.checkChargerUserId" @change="setCheckChargerUser" clearable filterable
                  :disabled="operationType === 'view'" placeholder="请选择" size="small" style="width: 100%">
                  <el-option v-for="item in chargerOptions" :key="item.id" :label="item.name"
                    :value="item.id"></el-option>
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
        </div>
        <div class="form-section">
          <div class="form-section-title">核查内容</div>
          <el-row :gutter="12">
            <el-col :span="8">
              <el-form-item label="环境温湿度:" prop="environment">
                <el-input v-model="form.environment" :disabled="operationType === 'view'" clearable size="small"
                  placeholder="如:25℃ / 50%RH"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="核查依据:" prop="checkBasis">
                <el-input v-model="form.checkBasis" :disabled="operationType === 'view'" clearable
                  size="small"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="核查指标:" prop="checkIndex">
                <el-input v-model="form.checkIndex" :disabled="operationType === 'view'" clearable
                  size="small"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="核查方式:" prop="checkMethod">
                <el-input v-model="form.checkMethod" :disabled="operationType === 'view'" clearable :rows="2"
                  size="small" type="textarea"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="结果如何判定:" prop="howResults">
                <el-input v-model="form.howResults" :disabled="operationType === 'view'" clearable :rows="2"
                  size="small" type="textarea"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="外观情况:">
                <el-radio-group v-model="form.appearanceStatus" :disabled="operationType === 'view'">
                  <el-radio value="良好">良好</el-radio>
                  <el-radio value="一般">一般</el-radio>
                  <el-radio value="损坏">损坏</el-radio>
                </el-radio-group>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="运行状态:">
                <el-radio-group v-model="form.runningStatus" :disabled="operationType === 'view'">
                  <el-radio value="正常">正常</el-radio>
                  <el-radio value="异常">异常</el-radio>
                  <el-radio value="停机">停机</el-radio>
                </el-radio-group>
              </el-form-item>
            </el-col>
            <el-col :span="24">
              <el-form-item label="备注:" prop="remark">
                <el-input v-model="form.remark" :disabled="operationType === 'view'" clearable size="small"></el-input>
              </el-form-item>
            </el-col>
          </el-row>
        </div>
        <div class="form-section">
          <div class="form-section-title">附件资料</div>
          <el-form-item label-width="0">
            <lims-attachment inline upload-url="/deviceExaminePlan/uploadAttachment"
              select-url="/deviceExaminePlan/selectAttachments" delete-url="/deviceExaminePlan/deleteAttachment"
              id-param="planDetailsId" name-field="deviceName" title="核查记录附件"
              :main-id="form.planDetailsId" :readonly="operationType === 'view'"></lims-attachment>
            <el-link v-if="form.fileUrl" type="primary" @click="openAttachment(form)">
              {{ form.fileName || '查看原附件' }}
            </el-link>
          </el-form-item>
        </div>
      </el-form>
      <template #footer><span class="dialog-footer">
        <el-button @click="resetForm">取 消</el-button>
@@ -153,23 +217,47 @@
          v-if="operationType !== 'view'">确 认</el-button>
      </span></template>
    </el-dialog>
    <el-dialog v-model="downloadDialog" title="导出" width="600px">
      <span>
        <el-button :loading="outLoading" type="primary" :disabled="!downRow.recordStatus"
          @click="controlDown">核查记录</el-button>
        <el-button :loading="outLoading" type="primary" :disabled="!downRow.recordContrastStatus"
          @click="processingDown">核查比对记录</el-button>
      </span>
    <el-dialog v-model="downloadDialog" title="导出" width="480px">
      <div class="export-panel">
        <div class="export-panel-head">
          <span class="export-panel-name">仪器设备期间核查记录表</span>
          <el-tag v-if="downRow.recordStatus === 2" type="success" disable-transitions>通过</el-tag>
          <el-tag v-else-if="downRow.recordStatus === 1" type="warning" disable-transitions>待批准</el-tag>
          <el-tag v-else-if="downRow.recordStatus === 3" type="danger" disable-transitions>不通过</el-tag>
          <el-tag v-else type="info" disable-transitions>未开始</el-tag>
        </div>
        <div class="export-panel-meta">
          <div class="export-panel-item">
            <span class="export-panel-label">设备编号</span>{{ downRow.deviceNumber || '—' }}
          </div>
          <div class="export-panel-item">
            <span class="export-panel-label">设备名称</span>{{ downRow.deviceName || '—' }}
          </div>
          <div class="export-panel-item">
            <span class="export-panel-label">核查时间</span>{{ formatCheckMonth(null, null, downRow.checkTime) || '—' }}
          </div>
        </div>
        <div v-if="!canExport" class="export-panel-tip">
          {{ downRow.recordStatus === 1 ? '该计划行尚未审核完成,审核通过或不通过后才能导出。' : '该计划行还没有核查记录,录入并提交审核后才能导出。' }}
        </div>
      </div>
      <template #footer><span class="dialog-footer">
        <el-button @click="downloadDialog = false">取 消</el-button>
        <el-button :loading="outLoading" type="primary" :disabled="!canExport"
          @click="controlDown">导出记录表</el-button>
      </span></template>
    </el-dialog>
    <check-record v-if="checkRecord" ref="checkRecord" @closeDialog1="closeDialog1"></check-record>
    <lims-attachment ref="rowAttachment" upload-url="/deviceExaminePlan/uploadAttachment"
      select-url="/deviceExaminePlan/selectAttachments" delete-url="/deviceExaminePlan/deleteAttachment"
      id-param="planDetailsId" name-field="deviceName" title="核查记录附件" :readonly="attachmentReadonly"
      @closed="getYearTableDetailData"></lims-attachment>
  </div>
</template>
<script>
import CheckRecord from "./checkRecord.vue";
import LimsAttachment from '@/components/Attachment/lims-attachment.vue'
import {
  pageDeviceExaminePlan,
  getExaminePlanByYear,
@@ -186,7 +274,7 @@
export default {
  name: "equipment-verification-plan",
  // import 引入的组件需要注入到对象中才能使用
  components: { CheckRecord },
  components: { CheckRecord, LimsAttachment },
  mixins: [draftSave],
  props: {
    clickNodeVal: {
@@ -216,24 +304,31 @@
      form: {
        deviceNumber: '',
        deviceName: '',
        deviceModel: '',
        checkTime: '',
        checkIndex: '',
        checkMethod: '',
        howResults: '',
        checkChargerUserId: '',
        checkChargerUser: '',
        environment: '',
        checkBasis: '',
        appearanceStatus: '',
        runningStatus: '',
        remark: '',
        fileName: '',
        fileUrl: '',
        planId: '',
        planDetailsId: '',
      },
      operationType: '',
      rules: {
        deviceNumber: [{ required: true, message: '请填写设备编号', trigger: 'blur' }],
        checkMethod: [{ required: true, message: '请填写核查方法', trigger: 'blur' }],
        checkMethod: [{ required: true, message: '请填写核查方式', trigger: 'blur' }],
      },
      yearTableDetailDataLoading: false,
      submitFormLoading: false,
      userList: [],
      attachmentReadonly: false,
      checkRecord: false,
      downloadDialog: false,
      outLoading: false,
@@ -245,7 +340,7 @@
  },
  // 方法集合
  methods: {
    // 年份下拉选项:已有计划年份 + 当前年份
    // 年份下拉只保留有计划明细的年份,空年份自动消失
    loadYearOptions() {
      pageDeviceExaminePlan({
        current: 1,
@@ -255,12 +350,19 @@
        if (res.code == 200) {
          years = (res.data.records || []).map(r => r.planYear).filter(Boolean)
        }
        const currentYear = String(new Date().getFullYear())
        if (years.indexOf(currentYear) === -1) {
          years.push(currentYear)
        const options = [...new Set(years)].sort((a, b) => b.localeCompare(a))
        this.yearOptions = options
        if (options.length === 0) {
          this.planYear = ''
          this.planInfo = {}
          this.yearTableDetailData = []
          this.pagination1.total = 0
          return
        }
        this.yearOptions = [...new Set(years)].sort((a, b) => b.localeCompare(a))
        this.planYear = currentYear
        if (options.indexOf(this.planYear) === -1) {
          const currentYear = String(new Date().getFullYear())
          this.planYear = options.indexOf(currentYear) > -1 ? currentYear : options[0]
        }
        this.loadPlan()
      })
    },
@@ -271,6 +373,12 @@
    },
    // 查询年份计划(不存在自动创建并补全到点明细),随后刷新明细
    loadPlan() {
      if (!this.planYear) {
        this.planInfo = {}
        this.yearTableDetailData = []
        this.pagination1.total = 0
        return
      }
      getExaminePlanByYear({ planYear: this.planYear }).then(res => {
        if (res.code == 200) {
          this.planInfo = res.data || {}
@@ -311,19 +419,27 @@
      })
    },
    // 到点需核查的行标红
    // 核查时间按月份显示(兼容历史数据中的完整日期)
    // 手动录入的核查日期显示到日,系统登记的月份数据仍按年月显示
    formatCheckMonth(row, column, cellValue) {
      if (cellValue === null || cellValue === undefined || cellValue === '') {
        return cellValue
      }
      const str = String(cellValue).trim().replace(/[./]/g, '-')
      const dateMatched = str.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/)
      if (dateMatched) {
        return `${dateMatched[1]}-${dateMatched[2].padStart(2, '0')}-${dateMatched[3].padStart(2, '0')}`
      }
      const matched = str.match(/^(\d{4})-(\d{1,2})/)
      if (matched) {
        return `${matched[1]}-${matched[2].padStart(2, '0')}`
      }
      const formatted = formatListDate(str)
      const fallback = String(formatted).match(/^(\d{4})-(\d{1,2})/)
      return fallback ? `${fallback[1]}-${fallback[2].padStart(2, '0')}` : cellValue
      const fallback = String(formatted).match(/^(\d{4})-(\d{1,2})-(\d{1,2})/)
      if (fallback) {
        return `${fallback[1]}-${fallback[2].padStart(2, '0')}-${fallback[3].padStart(2, '0')}`
      }
      const monthFallback = String(formatted).match(/^(\d{4})-(\d{1,2})/)
      return monthFallback ? `${monthFallback[1]}-${monthFallback[2].padStart(2, '0')}` : cellValue
    },
    tableRowClassName({ row }) {
      return row.redFlag === 1 ? 'red-row' : ''
@@ -380,14 +496,14 @@
    },
    // 明细-删除
    deleteFun1(id) {
      this.$confirm('此操作将永久删除该条明细, 是否继续?', '提示', {
      this.$confirm('删除后,若该设备还有审核通过的核查记录,档案的最近核查日期将回退到最近一次完成记录;若没有,则清空档案的核查日期和核查周期。是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        delDeviceExaminePlanDetail({ planDetailsId: id }).then(res => {
          this.$message.success('删除成功!');
          this.getYearTableDetailData()
          this.loadYearOptions()
        });
      }).catch(() => {
        this.$message({
@@ -426,39 +542,15 @@
      const isImage = /\.(jpg|jpeg|png|gif)$/i.test(row.fileUrl)
      window.open(this.javaApi + (isImage ? '/img/' : '/word/') + row.fileUrl, '_blank')
    },
    beforeAttachmentUpload(file) {
      if (file.size > 1024 * 1024 * 10) {
        this.$message.error('上传文件不超过10M')
        return false
      }
      return true
    },
    // 附件上传成功后保存到计划明细
    handleAttachmentSuccess(response, row) {
      if (response.code == 200) {
        updateDeviceExaminePlanDetail({
          planDetailsId: row.planDetailsId,
          fileName: response.data.name,
          fileUrl: response.data.url
        }).then(res => {
          if (res.code == 200) {
            this.$message.success('附件上传成功')
            this.getYearTableDetailData()
          } else {
            this.$message.error(res.msg || '附件保存失败')
          }
        })
      } else {
        this.$message.error(response.msg || '附件上传失败')
      }
    },
    handleAttachmentError() {
      this.$message.error('附件上传失败')
    // 列表附件列只读查看(上传/删除入口只在编辑、新增弹窗内)
    handleAttachment(row) {
      this.attachmentReadonly = true
      this.$refs.rowAttachment.open(row)
    },
    // 关闭记录弹框
    closeDialog1() {
      this.checkRecord = false
      this.getYearTableDetailData()
      this.loadYearOptions()
    },
    // 打开导出弹框
    handleDown(row) {
@@ -519,6 +611,10 @@
    // 是否已完成审批(批准/不批准)
    isRatified() {
      return this.planInfo.ratifyStatus === 0 || this.planInfo.ratifyStatus === 1;
    },
    // 记录状态为通过或不通过(审核已结束)才允许导出
    canExport() {
      return this.downRow.recordStatus === 2 || this.downRow.recordStatus === 3;
    },
    // 责任人下拉:已保存的责任人不在候选列表时补一条,避免回显成用户ID
    chargerOptions() {
@@ -589,9 +685,80 @@
  background: #ffcccc !important;
}
.row-upload {
  display: inline-block;
  margin-left: 10px;
  vertical-align: middle;
.form-section {
  padding: 12px 14px 0;
  border: 1px solid #ebeef5;
  border-radius: 6px;
  margin-bottom: 10px;
}
.form-section:last-child {
  margin-bottom: 0;
}
.form-section-title {
  display: flex;
  align-items: center;
  padding-left: 8px;
  border-left: 3px solid var(--el-color-primary);
  margin-bottom: 12px;
  font-size: 14px;
  font-weight: 600;
  line-height: 1;
  color: #303133;
}
.form-section :deep(.el-form-item) {
  margin-bottom: 12px;
}
.export-panel {
  border: 1px solid #e4e7ed;
  border-radius: 6px;
  padding: 18px 20px;
  background: #fafbfc;
}
.export-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
}
.export-panel-name {
  font-size: 15px;
  font-weight: 600;
  color: var(--lims-text);
}
.export-panel-meta {
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: 13px;
  color: #606266;
}
.export-panel-item {
  display: flex;
  align-items: center;
}
.export-panel-label {
  width: 72px;
  flex-shrink: 0;
  color: #909399;
}
.export-panel-tip {
  margin-top: 14px;
  padding: 8px 12px;
  border-radius: 4px;
  background: #fdf6ec;
  color: #e6a23c;
  font-size: 13px;
  line-height: 1.5;
}
</style>
src/views/CNAS/resourceDemand/device/component/record.vue
@@ -5,51 +5,67 @@
      <el-form :model="search" ref="search" size="small" :inline="true">
        <el-form-item label="样品编号">
          <el-input v-model="search.sampleCode" clearable placeholder="请输入" size="small"
                    @keyup.enter="getTableList(clickNodeVal.value)"></el-input>
                    @keyup.enter="getTableList()"></el-input>
        </el-form-item>
        <el-form-item label="管理编号">
        <el-form-item label="设备编号">
          <el-input v-model="search.managementNumber" clearable placeholder="请输入" size="small"
                    @keyup.enter="getTableList(clickNodeVal.value)"></el-input>
                    @keyup.enter="getTableList()"></el-input>
        </el-form-item>
        <el-form-item>
          <el-button size="small" type="primary" @click="getTableList(clickNodeVal.value)">查询</el-button>
          <el-button size="small" type="primary" @click="getTableList()">查询</el-button>
          <el-button size="small" @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
      <div>
        <el-button v-if="isMenuList !== 1" size="small" type="primary" @click="dialogVisible = true, openAdd()">新 建</el-button>
        <el-button :loading="outLoading" size="small" type="primary" @click="openHandleOut">导 出</el-button>
      </div>
    </div>
    <div>
      <el-table ref="Recordtable" :data="formParamList" height="calc(100vh - 20em)"
                v-loading="tableLoading"
 border>
        <!-- 表格列 -->
        <el-table-column label="序号" type="index" width="70" align="center">
          <template v-slot="scope">
      <el-table ref="Recordtable" :data="formParamList" height="calc(100vh - 25em)"
                v-loading="tableLoading" border>
        <el-table-column label="序号" type="index" width="60" align="center">
          <template #default="scope">
            <span>{{ (search.current - 1) * search.size + scope.$index + 1 }}</span>
          </template>
        </el-table-column>
        <el-table-column label="样品编号" min-width="180" prop="sampleCode" />
        <el-table-column label="设备名称" min-width="150" prop="deviceName" />
        <el-table-column label="管理编号" min-width="150" prop="managementNumber" />
        <el-table-column label="使用前" min-width="120" prop="useBefore">
          <template v-slot="scope">
            {{ scope.row.useBefore === 1 ? '良好' : '异常' }}
        <el-table-column label="设备名称" min-width="140" prop="deviceName" show-overflow-tooltip />
        <el-table-column label="设备型号" min-width="120" prop="specificationModel" show-overflow-tooltip />
        <el-table-column label="设备编号" min-width="130" prop="managementNumber" />
        <el-table-column label="使用日期" width="110" align="center">
          <template #default="scope">
            {{ formatDateCell(scope.row.useStartDate) }}
          </template>
        </el-table-column>
        <el-table-column label="使用后" min-width="120" prop="useAfter">
          <template v-slot="scope">
            {{ scope.row.useAfter === 1 ? '良好' : '异常' }}
        <el-table-column label="样品编号" min-width="150" prop="sampleCode">
          <template #default="scope">
            <span>{{ scope.row.sampleCode }}</span>
            <el-tooltip v-if="scope.row.orderCode" :content="'关联原始记录:' + scope.row.orderCode" placement="top">
              <lims-icon class="link-icon" name="el-icon-link"></lims-icon>
            </el-tooltip>
          </template>
        </el-table-column>
        <!--        <el-table-column label="异常情况" min-width="120" prop="abnormal" />-->
        <el-table-column label="使用开始日期" min-width="170" prop="useStartDate" :formatter="formatDateCell" />
        <el-table-column label="使用结束日期" min-width="170" prop="useEndDate" :formatter="formatDateCell" />
        <el-table-column label="使用人" min-width="120" prop="usePerson" />
        <el-table-column label="备注" min-width="120" prop="remark" />
        <!-- 操作按钮 -->
        <el-table-column label="时间(24小时制)" align="center">
          <el-table-column label="开始" width="90" align="center">
            <template #default="scope">{{ formatTimeCell(scope.row.useStartDate) }}</template>
          </el-table-column>
          <el-table-column label="结束" width="90" align="center">
            <template #default="scope">{{ formatTimeCell(scope.row.useEndDate) }}</template>
          </el-table-column>
        </el-table-column>
        <el-table-column label="设备状况" align="center">
          <el-table-column label="使用前" width="80" align="center">
            <template #default="scope">{{ formatStatus(scope.row.useBefore) }}</template>
          </el-table-column>
          <el-table-column label="使用后" width="80" align="center">
            <template #default="scope">{{ formatStatus(scope.row.useAfter) }}</template>
          </el-table-column>
        </el-table-column>
        <el-table-column label="环境条件" align="center">
          <el-table-column label="℃" width="80" align="center" prop="temperature" />
          <el-table-column label="%RH" width="80" align="center" prop="humidity" />
        </el-table-column>
        <el-table-column label="使用人" width="100" prop="usePerson" />
        <el-table-column label="备注" min-width="140" prop="remark" show-overflow-tooltip />
        <el-table-column fixed="right" label="操作" width="150" align="center">
          <template #default="scope">
            <el-button size="small" link type="primary" @click="showDetailsDialog('edit', scope.row)">编辑</el-button>
@@ -63,30 +79,57 @@
        @current-change="handleCurrentChange" background>
      </el-pagination>
    </div>
    <el-dialog v-model="dialogVisible" title="仪器设备使用记录表" top="5vh" width="55%">
      <el-form ref="form" :model="form" label-width="130px">
        <el-row>
          <el-col :span="24">
            <el-form-item :rules="[{ required: true, message: '样品编号', trigger: 'blur' }]" label="样品编号:"
    <el-dialog v-model="dialogVisible" title="仪器设备运行记录" top="5vh" width="70%">
      <el-form ref="form" :model="form" label-width="105px">
        <el-row :gutter="16">
          <el-col :span="8">
            <el-form-item label="设备名称:">
              <el-input v-model="form.deviceName" disabled size="small"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="设备型号:">
              <el-input v-model="form.specificationModel" disabled size="small"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="设备编号:">
              <el-input v-model="form.managementNumber" disabled size="small"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item :rules="[{ required: true, message: '请选择使用日期', trigger: 'change' }]" label="使用日期:"
              prop="useDate">
              <el-date-picker v-model="form.useDate" :disabled="operationType === 'view'" format="YYYY-MM-DD"
                placeholder="选择使用日期" size="small" style="width: 100%" type="date" value-format="YYYY-MM-DD"
                :disabled-date="disabledDate">
              </el-date-picker>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item :rules="[{ required: true, message: '请输入样品编号', trigger: 'blur' }]" label="样品编号:"
              prop="sampleCode">
              <el-input v-model="form.sampleCode" :disabled="operationType === 'view'" size="small"
                style="width: 100%;"></el-input>
              <el-input v-model="form.sampleCode" :disabled="operationType === 'view'" size="small"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="设备名称:" required>
              <el-input v-model="form.deviceName" :disabled="operationType === 'view'" size="small"
                style="width: 100%;"></el-input>
            <el-form-item :rules="[{ required: true, message: '请选择开始时间', trigger: 'change' }]" label="开始时间:"
              prop="useStartTime">
              <el-time-picker v-model="form.useStartTime" :disabled="operationType === 'view'" format="HH:mm"
                placeholder="选择开始时间" size="small" style="width: 100%" value-format="HH:mm:ss">
              </el-time-picker>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="管理编号:" required>
              <el-input v-model="form.managementNumber" :disabled="operationType === 'view'" size="small"
                style="width: 100%;"></el-input>
            <el-form-item :rules="[{ required: true, message: '请选择结束时间', trigger: 'change' }]" label="结束时间:"
              prop="useEndTime">
              <el-time-picker v-model="form.useEndTime" :disabled="operationType === 'view'" format="HH:mm"
                placeholder="选择结束时间" size="small" style="width: 100%" value-format="HH:mm:ss">
              </el-time-picker>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item :rules="[{ required: true, message: '请选择使用前', trigger: 'change' }]" label="使用前:"
            <el-form-item :rules="[{ required: true, message: '请选择使用前状态', trigger: 'change' }]" label="使用前:"
              prop="useBefore">
              <el-radio-group v-model="form.useBefore" :disabled="operationType === 'view'">
                <el-radio :value="1">良好</el-radio>
@@ -95,7 +138,7 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item :rules="[{ required: true, message: '请选择使用后', trigger: 'change' }]" label="使用后:"
            <el-form-item :rules="[{ required: true, message: '请选择使用后状态', trigger: 'change' }]" label="使用后:"
              prop="useAfter">
              <el-radio-group v-model="form.useAfter" :disabled="operationType === 'view'">
                <el-radio :value="1">良好</el-radio>
@@ -104,22 +147,31 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item :rules="[{ required: true, message: '请输入使用日期', trigger: 'blur' }]" label="使用日期:"
              prop="useDateList">
              <el-date-picker v-model="form.useDateList" :disabled="operationType === 'view'" end-placeholder="结束日期"
                format="YYYY-MM-DD HH:mm:ss" size="small" start-placeholder="开始日期" style="width:100%"
                type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss"
                              :disabled-date="disabledDate">
              </el-date-picker>
            <el-form-item label="环境温度:">
              <el-input v-model="form.temperature" :disabled="operationType === 'view'" placeholder="请输入温度" size="small">
                <template #append>℃</template>
              </el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item :rules="[{ required: true, message: '请输入使用人', trigger: 'change' }]" label="使用人:"
            <el-form-item label="环境湿度:">
              <el-input v-model="form.humidity" :disabled="operationType === 'view'" placeholder="请输入湿度" size="small">
                <template #append>%RH</template>
              </el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item :rules="[{ required: true, message: '请选择使用人', trigger: 'change' }]" label="使用人:"
              prop="usePersonId">
              <el-select @change="usePersonName" v-model="form.usePersonId" :disabled="operationType === 'view'"
                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>
          </el-col>
          <el-col :span="12" v-if="form.orderCode">
            <el-form-item label="关联原始记录:">
              <el-input v-model="form.orderCode" disabled size="small"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="24">
@@ -129,7 +181,7 @@
          </el-col>
        </el-row>
      </el-form>
      <template #footer><span  class="dialog-footer">
      <template #footer><span class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button v-if="operationType !== 'view'" type="primary" @click="saveRecord">确 定</el-button>
      </span></template>
@@ -140,7 +192,7 @@
          type="month" value-format="YYYY-MM">
        </el-date-picker>
      </span>
      <template #footer><span  class="dialog-footer">
      <template #footer><span class="dialog-footer">
        <el-button @click="ratifyDialog = false, exportDate = ''">取 消</el-button>
        <el-button :loading="outLoading" type="primary" @click="handleDown">导 出</el-button>
      </span></template>
@@ -151,28 +203,41 @@
import {
  deviceRecordPage,
  exportUseRecord,
  selectDeviceByCode,
  saveDeviceRecord,
  updateDeviceRecord,
  deleteDeviceRecord,
} from '@/api/cnas/resourceDemand/device.js'
import { selectUserCondition } from "@/api/business/inspectionTask";
import { formatListDate } from './dateFormat';
import { formatDateOnly as toDateOnly } from './dateFormat';
import { saveBlobOrShowError } from '@/utils/saveBlob'
import dayjs from 'dayjs'
const today = () => {
  const now = new Date()
  return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
}
const defaultForm = () => ({
  id: null,
  deviceId: '',
  deviceName: '',
  specificationModel: '',
  managementNumber: '',
  insOrderId: null,
  orderCode: '',
  sampleCode: '',
  useDate: today(),
  useStartTime: '',
  useEndTime: '',
  temperature: '',
  humidity: '',
  useBefore: 1,
  useAfter: 1,
  abnormal: '',
  usePersonId: '',
  usePerson: '',
  remark: '',
})
export default {
  props: {
    clickNodeVal: {
      type: Object,
      default: () => {
        return {};
      }
    },
    // 判断是否是外部分离
    isMenuList: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      search: {
@@ -182,37 +247,11 @@
        sampleCode: '',
        managementNumber: ''
      },
      responsiblePersonList: {},
      isShow: false,
      outLoading: false,
      ratifyDialog: false,
      exportDate: '',
      userList: [],
      form: {
        // deviceId:'',
        processNumber: '',
        deviceName: '',
        managementNumber: '',
        //温度
        temperature: '',
        //湿度
        humidity: '',
        //使用前
        useBefore: 1,
        //使用后
        useAfter: 1,
        //异常情况
        abnormal: '',
        useDateList: [],
        //使用日期
        useStartDate: null,
        useEndDate: null,
        //使用人
        usePerson: '',
        usePersonId: '',
        //备注
        remark: '',
      },
      form: defaultForm(),
      dialogVisible: false,
      operationType: '',
      formParamList: [],
@@ -220,11 +259,21 @@
    }
  },
  mounted() {
    this.getTableList(this.clickNodeVal.value);
    this.getTableList();
  },
  methods: {
    formatDateCell(row, column, cellValue) {
      return formatListDate(cellValue)
    formatDateCell(value) {
      return toDateOnly(value)
    },
    formatTimeCell(value) {
      if (!value) return ''
      const date = dayjs(String(value).replace('T', ' '))
      return date.isValid() ? date.format('HH:mm') : ''
    },
    formatStatus(value) {
      if (value === 1) return '良好'
      if (value === 0) return '异常'
      return ''
    },
    // element-plus 的 el-date-picker 没有 picker-options,改成 :disabled-date
    disabledDate(time) {
@@ -232,11 +281,11 @@
    },
    handleSizeChange(val) {
      this.search.size = val
      this.getTableList(this.clickNodeVal.value);
      this.getTableList();
    },
    handleCurrentChange(val) {
      this.search.current = val
      this.getTableList(this.clickNodeVal.value);
      this.getTableList();
    },
    resetSearch() {
      this.search = {
@@ -246,15 +295,11 @@
        sampleCode: '',
        managementNumber: ''
      }
      this.getTableList(this.clickNodeVal.value);
      this.getTableList();
    },
    async getTableList(deviceId) {
      if (deviceId === undefined) {
        deviceId = ''
      }
    async getTableList() {
      this.tableLoading = true
      await deviceRecordPage({
        deviceId: deviceId,
        ...this.search
      }).then(res => {
        this.tableLoading = false
@@ -274,28 +319,18 @@
    handleDown() {
      this.outLoading = true
      exportUseRecord({
        deviceId: this.clickNodeVal.value,
        sampleCode: this.search.sampleCode,
        managementNumber: this.search.managementNumber,
        exportDate: this.exportDate
      }).then(res => {
        this.outLoading = false
        saveBlobOrShowError(res, '设备使用记录.doc', 'application/msword')
        saveBlobOrShowError(res, '仪器设备运行记录.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')
      }).catch(() => {
        this.outLoading = false
      })
    },
    //新建
    openAdd() {
      this.operationType = 'add'
      this.getUserList()
      selectDeviceByCode({ id: this.clickNodeVal.value }).then(res => {
        this.form.deviceName = res.data.deviceName
        this.form.managementNumber = res.data.managementNumber
        this.$nextTick(() => {
          this.$refs['form'].clearValidate()
        })
      });
    resetForm() {
      this.form = defaultForm()
    },
    usePersonName(value) {
      const index = this.userList.findIndex(item => item.id === value)
@@ -303,31 +338,39 @@
        this.form.usePerson = this.userList[index].name
      }
    },
    //确认添加记录
    //保存修改
    saveRecord() {
      this.$refs.form.validate((valid) => {
        if (valid) {
          this.form.useStartDate = this.form.useDateList[0]
          this.form.useEndDate = this.form.useDateList[1]
          this.form.deviceId = this.clickNodeVal.value
          if (this.operationType === 'add') {
            saveDeviceRecord(this.form).then(res => {
              if (res.code == 200) {
                this.$message.success('新增成功')
                this.getTableList(this.clickNodeVal.value)
                this.dialogVisible = false
              }
            })
          } else {
            updateDeviceRecord(this.form).then(res => {
              if (res.code == 200) {
                this.$message.success('修改成功')
                this.getTableList(this.clickNodeVal.value)
                this.dialogVisible = false
              }
            })
          }
        if (!valid) {
          return
        }
        if (this.form.useStartTime >= this.form.useEndTime) {
          this.$message.error('结束时间必须晚于开始时间')
          return
        }
        const payload = {
          id: this.form.id || undefined,
          deviceId: this.form.deviceId,
          insOrderId: this.form.insOrderId || undefined,
          sampleCode: this.form.sampleCode,
          temperature: this.form.temperature,
          humidity: this.form.humidity,
          useBefore: this.form.useBefore,
          useAfter: this.form.useAfter,
          abnormal: this.form.abnormal,
          usePersonId: this.form.usePersonId,
          usePerson: this.form.usePerson,
          remark: this.form.remark,
          useStartDate: `${this.form.useDate} ${this.form.useStartTime}`,
          useEndDate: `${this.form.useDate} ${this.form.useEndTime}`,
        }
        updateDeviceRecord(payload).then(res => {
          if (res.code == 200) {
            this.$message.success('修改成功')
            this.getTableList()
            this.dialogVisible = false
          }
        })
      });
    },
    //查看详情
@@ -336,13 +379,23 @@
      this.dialogVisible = true;
      this.getUserList()
      // 复制一份再编辑,避免直接改动表格里的数据
      const useStartDate = row.useStartDate || ''
      const useEndDate = row.useEndDate || ''
      const start = row.useStartDate ? String(row.useStartDate).replace('T', ' ') : ''
      const end = row.useEndDate ? String(row.useEndDate).replace('T', ' ') : ''
      this.form = {
        ...defaultForm(),
        ...row,
        useStartDate,
        useEndDate,
        useDateList: useStartDate && useEndDate ? [useStartDate, useEndDate] : []
        deviceId: row.deviceId,
        deviceName: row.deviceName || '',
        specificationModel: row.specificationModel || '',
        managementNumber: row.managementNumber || '',
        orderCode: row.orderCode || '',
        useDate: start ? start.slice(0, 10) : '',
        useStartTime: start.length >= 19 ? start.slice(11, 19) : '',
        useEndTime: end.length >= 19 ? end.slice(11, 19) : '',
        temperature: row.temperature || '',
        humidity: row.humidity || '',
        usePersonId: row.usePersonId || '',
        usePerson: row.usePerson || '',
      }
    },
    handleDeleteClick(index, row) {
@@ -372,37 +425,12 @@
    },
  },
  watch: {
    // 监听点击el-tree的数据,进行数据刷新
    clickNodeVal(newVal) {
      if (newVal.value) {
        this.getTableList(newVal.value);
      }
    },
    dialogVisible(newVal) {
      if (newVal === false) {
        this.form = {
          // deviceId:'',
          sampleCode: '',
          deviceName: '',
          managementNumber: '',
          //温度
          temperature: '',
          //湿度
          humidity: '',
          //使用前
          useBefore: 1,
          //使用后
          useAfter: 1,
          //异常情况
          abnormal: '',
          //使用日期
          useDateList: [],
          //使用人
          usePersonId: '',
          //备注
          remark: '',
        }
        this.$refs.form.clearValidate()
        this.resetForm()
        this.$nextTick(() => {
          this.$refs.form && this.$refs.form.clearValidate()
        })
      }
    }
  }
@@ -414,6 +442,13 @@
  height: 46px;
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
  margin-top: 4px;
}
.link-icon {
  margin-left: 4px;
  color: #409eff;
  font-size: 14px;
  vertical-align: middle;
}
</style>
src/views/CNAS/resourceDemand/device/index.vue
@@ -62,7 +62,7 @@
            <using-external-instruments v-if="menuListActiveName == '利用外部仪器设备'" :clickNodeVal="clickNodeVal"></using-external-instruments>
          </el-tab-pane>
          <el-tab-pane label="使用记录" name="使用记录">
            <record v-if="menuListActiveName == '使用记录'" :isMenuList="1" />
            <record v-if="menuListActiveName == '使用记录'" />
          </el-tab-pane>
        </el-tabs>
      </div>
@@ -83,9 +83,6 @@
          </el-tab-pane>
          <el-tab-pane label="设备借用" name="设备借用">
            <borrow v-if="tabListActiveName == '设备借用'" :clickNodeVal="clickNodeVal" />
          </el-tab-pane>
          <el-tab-pane label="使用记录" name="使用记录">
            <record v-if="tabListActiveName == '使用记录'" :clickNodeVal="clickNodeVal" />
          </el-tab-pane>
          <el-tab-pane label="设备停用/启用" name="设备停用/启用">
            <state v-if="tabListActiveName == '设备停用/启用'" :clickNodeVal="clickNodeVal" />