资源需求(标准物质相关)模块调整;新增统一附件组件并接入供应商管理与客户满意度;客户满意度表单改客户卡片下拉并关联已完成委托单;预览、图表、导航栏等组件调整
已修改14个文件
已添加1个文件
已删除1个文件
1441 ■■■■■ 文件已修改
src/api/business/productOrder.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/cnas/resourceDemand/standardMaterial/standardMaterial.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Attachment/lims-attachment.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Preview/filePreview.vue 265 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/echarts/echarts.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Navbar.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/externalService/supplierManage/index.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/standardMaterailRequistion/index.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/standardMaterial/component/borrowDia.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/standardMaterial/component/formDia.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/standardMaterial/component/returnDia.vue 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/standardMaterial/index.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/standardMaterialAccept/component/AddRecord.vue 375 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/standardMaterialAccept/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/systemManagement/customerSatisfaction/components/formDialog.vue 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/systemManagement/customerSatisfaction/index.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/business/productOrder.js
@@ -146,3 +146,10 @@
    data: query
  })
}
// æŸ¥è¯¢å·²å®Œæˆå§”托单(客户满意度下拉用,仅已检验)
export function selectCompletedOrders() {
  return request({
    url: '/insOrder/selectCompletedOrders',
    method: 'get'
  })
}
src/api/cnas/resourceDemand/standardMaterial/standardMaterial.js
@@ -55,14 +55,12 @@
  })
}
//标准物质清单归还
export function returnSubstance(query) {
// æŸ¥è¯¢æ ‡å‡†ç‰©è´¨å‰©ä½™å¯é¢†ç”¨æ•°é‡
export function getRemainingQuantity(substanceId) {
  return request({
    url: '/feStandardSubstanceRecord/returnSubstance',
    method: 'post',
    data: query
    url: `/feStandardSubstanceRecord/getRemainingQuantity/${substanceId}`,
    method: 'get'
  })
}
src/components/Attachment/lims-attachment.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,209 @@
<template>
  <div>
    <el-dialog :title="dialogTitle" v-model="visible" width="760px">
      <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">
          <el-button size="small" type="primary">上传附件</el-button>
        </el-upload>
      </div>
      <el-table :data="attachmentList" border size="small" style="margin-top:12px">
        <el-table-column label="文件名称" prop="fileName" min-width="280" show-overflow-tooltip />
        <el-table-column label="上传时间" prop="createTime" width="170" />
        <el-table-column label="操作" width="180">
          <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>
          </template>
        </el-table-column>
      </el-table>
      <template #footer>
        <el-button @click="visible = false">关 é—­</el-button>
      </template>
    </el-dialog>
    <el-dialog title="预览附件" v-model="previewVisible" fullscreen append-to-body>
      <file-preview v-if="previewVisible" :file-url="previewUrl" :current-file="previewRow" />
    </el-dialog>
  </div>
</template>
<script>
import request from '@/utils/request'
import filePreview from '@/components/Preview/filePreview.vue'
/**
 * é€šç”¨é™„件管理组件(上传 / é¢„览 / ä¸‹è½½ / åˆ é™¤ï¼‰
 *
 * ç”¨æ³•:页面操作列放三个入口,分别调用本组件的方法:
 *   ä¸Šä¼ é™„ä»¶ -> this.$refs.xxx.open(row)                 æ‰“开附件弹窗(弹窗内可上传)
 *   ä¸‹è½½é™„ä»¶ -> this.$refs.xxx.downloadAttachments(row)  åªæœ‰ä¸€ä¸ªé™„件直接下载,多个打开弹窗
 *   é¢„览附件 -> this.$refs.xxx.previewAttachments(row)   åªæœ‰ä¸€ä¸ªé™„件直接预览,多个打开弹窗
 *
 * åŽç«¯æŽ¥å£éœ€æŒ‰ç»Ÿä¸€çº¦å®šæä¾›ï¼ˆå‚ç…§ /supplierManagement çš„附件接口):
 *   ä¸Šä¼ ï¼šPOST {uploadUrl}?{idParam}=主表ID  è¡¨å•参数 file
 *   åˆ—表:GET  {selectUrl}?{idParam}=主表ID  è¿”回 [{id, fileName, fileUrl, createTime, ...}]
 *   åˆ é™¤ï¼šDELETE {deleteUrl}?id=附件ID
 */
export default {
  name: 'LimsAttachment',
  components: {
    filePreview
  },
  props: {
    // ä¸Šä¼ æŽ¥å£åœ°å€ï¼Œå¦‚ /supplierManagement/uploadAttachment
    uploadUrl: {
      type: String,
      required: true
    },
    // æŸ¥è¯¢é™„件列表接口地址
    selectUrl: {
      type: String,
      required: true
    },
    // åˆ é™¤é™„件接口地址
    deleteUrl: {
      type: String,
      required: true
    },
    // å…³è”主表ID在接口里的参数名,如 supplierManagementId
    idParam: {
      type: String,
      default: 'id'
    },
    // å…³è”主表ID在行数据里的字段名,不传时默认与 idParam ç›¸åŒ
    idField: {
      type: String,
      default: ''
    },
    // å¼¹çª—标题上要显示名称的行字段,如 supplierName
    nameField: {
      type: String,
      default: ''
    },
    // å¼¹çª—标题前缀
    title: {
      type: String,
      default: '附件'
    },
    // å¼¹çª—左上角提示文字
    tip: {
      type: String,
      default: '支持任意格式文件,单个文件不超过200MB'
    }
  },
  data() {
    return {
      visible: false,
      attachmentList: [],
      currentRow: {},
      previewVisible: false,
      previewRow: {},
      previewUrl: ''
    }
  },
  computed: {
    currentId() {
      const field = this.idField || this.idParam
      return this.currentRow ? this.currentRow[field] : undefined
    },
    dialogTitle() {
      const name = this.nameField && this.currentRow ? this.currentRow[this.nameField] : ''
      return this.title + (name ? ' - ' + name : '')
    },
    uploadAction() {
      return this.javaApi + this.uploadUrl + '?' + this.idParam + '=' + this.currentId
    }
  },
  methods: {
    // æ‰“开附件管理弹窗
    open(row) {
      this.currentRow = row
      this.visible = true
      this.loadList()
    },
    // é¢„览附件入口:只有一个附件直接预览,多个打开弹窗
    previewAttachments(row) {
      this.queryList(row).then(list => {
        if (!list.length) {
          this.$message.warning('暂无附件')
        } else if (list.length === 1) {
          this.previewFile(list[0])
        } else {
          this.open(row)
        }
      })
    },
    // ä¸‹è½½é™„件入口:只有一个附件直接下载,多个打开弹窗
    downloadAttachments(row) {
      this.queryList(row).then(list => {
        if (!list.length) {
          this.$message.warning('暂无附件')
        } else if (list.length === 1) {
          this.downloadFile(list[0])
        } else {
          this.open(row)
        }
      })
    },
    queryList(row) {
      const field = this.idField || this.idParam
      return request({
        url: this.selectUrl,
        method: 'get',
        params: { [this.idParam]: row[field] }
      }).then(res => (res && res.data) || [])
    },
    loadList() {
      this.queryList(this.currentRow).then(list => {
        this.attachmentList = list
      })
    },
    handleUploadSuccess(res) {
      if (res.code === 200) {
        this.$message.success('上传成功')
        this.loadList()
      } else {
        this.$message.error(res.msg || '上传失败')
      }
    },
    handleUploadError() {
      this.$message.error('上传失败,文件可能超过200MB')
    },
    previewFile(row) {
      this.previewRow = row
      this.previewUrl = this.fullUrl(row.fileUrl)
      this.previewVisible = true
    },
    downloadFile(row) {
      this.$download.saveAs(this.fullUrl(row.fileUrl), row.fileName)
    },
    removeFile(row) {
      this.$confirm('是否删除该附件?', '提示', { type: 'warning' }).then(() => {
        request({
          url: this.deleteUrl,
          method: 'delete',
          params: { id: row.id }
        }).then(() => {
          this.$message.success('删除成功')
          this.loadList()
        })
      }).catch(() => {})
    },
    // é™æ€åœ°å€æ‹¼æŽ¥ï¼Œä¸ŽåŽç«¯ä¸Šä¼ ç»Ÿä¸€è·¯ç”±ä¿æŒä¸€è‡´ï¼šå›¾ç‰‡åœ¨ /img,其它都在 /word
    fullUrl(fileUrl) {
      const base = /\.(jpg|jpeg|png|gif)$/i.test(fileUrl) ? '/img/' : '/word/'
      return this.javaApi + base + fileUrl
    }
  }
}
</script>
<style scoped>
.attachment-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
</style>
src/components/Preview/filePreview.vue
@@ -1,53 +1,67 @@
<template>
  <div>
    <div v-if="isImage">
      <img :src="imgUrl" alt="Image Preview" />
    <!-- å›¾ç‰‡ -->
    <div v-if="isImage" style="text-align: center">
      <img :src="imgUrl" alt="图片预览" />
    </div>
    <div v-if="isPdf" style="height: 80vh;">
    <!-- è§†é¢‘ -->
    <div v-else-if="isVideo" style="text-align: center">
      <video :src="fileUrl" controls style="max-width: 100%; max-height: 85vh"></video>
    </div>
    <!-- éŸ³é¢‘ -->
    <div v-else-if="isAudio" style="text-align: center; padding-top: 10vh">
      <audio :src="fileUrl" controls style="width: 60%"></audio>
    </div>
    <!-- PDF:传了 OnlyOffice é…ç½®èµ°æ–‡æ¡£æœåŠ¡ï¼Œå¦åˆ™ç”¨æµè§ˆå™¨å†…ç½®é˜…è¯»å™¨ -->
    <div v-else-if="isPdf" style="height: 80vh;">
      <onlyoffice v-if="useOnlyOffice" ref="onlyoffice" :options="option" style="width: 100%;height: 100%;" />
      <iframe v-else-if="pdfUrl" :src="pdfUrl" title="PDF é¢„览" style="width: 100%;height: 100%;border: none;"></iframe>
      <p v-else-if="pdfError">PDF é¢„览失败,<a :href="fileUrl" style="color: var(--el-color-primary);" target="_blank">点此下载查看</a></p>
      <p v-else>正在加载 PDF…</p>
    </div>
    <div v-if="isDoc">
      <p v-if="!isDocShow">文档无法直接预览,请下载查看。</p>
      <a :href="fileUrl" v-if="!isDocShow">下载文件</a>
      <vue-office-docx v-else :src="fileUrl" style="height: 100vh;" @rendered="renderedHandler" @error="errorHandler" />
    </div>
    <div v-if="isXls">
      <p v-if="!isDocShow">文档无法直接预览,请下载查看。</p>
      <a :href="fileUrl" v-if="!isDocShow">下载文件</a>
      <vue-office-excel v-else :src="fileUrl" :options="excelOptions" style="height: 100vh;" @rendered="renderedHandler"
    <!-- Word -->
    <div v-else-if="isDoc">
      <vue-office-docx v-if="isDocShow" :key="fileUrl" :src="fileUrl" style="height: 100vh;" @rendered="renderedHandler"
        @error="errorHandler" />
      <div v-else class="preview-fallback">
        <p>Word æ–‡æ¡£æ— æ³•直接预览,请下载查看。</p>
        <el-button type="primary" @click="downloadFile">下载文件</el-button>
    </div>
    <div v-if="isZipOrRar">
      <p>压缩文件无法直接预览,请下载查看。</p>
      <a :href="fileUrl">下载文件</a>
    </div>
    <div v-if="isCsv">
      <p v-if="csvList.length == 0">CSV æ–‡ä»¶æ— æ³•直接预览,请下载查看。</p>
      <a :href="fileUrl" v-if="csvList.length == 0">下载文件</a>
      <el-tabs type="border-card" v-if="csvList.length > 0" tab-position="bottom">
        <el-tab-pane :label="item.sheetName" v-for="(item, index) in csvList" :key="index">
          <el-table :data="item.tableData" height="75vh">
            <el-table-column :label="m.label" :prop="m.prop" v-for="(m, i) in item.column" :key="i" min-width="120px"
              show-overflow-tooltip>
              <template #header="scope">
                <div>
                  <el-tooltip :content="m.label" placement="top">
                    <div class="oneLine">
                      <span>{{ m.label }}</span>
    <!-- Excel -->
    <div v-else-if="isXls">
      <vue-office-excel v-if="isDocShow" :key="fileUrl" :src="fileUrl" :options="excelOptions" style="height: 100vh;"
        @rendered="renderedHandler" @error="errorHandler" />
      <div v-else class="preview-fallback">
        <p>Excel æ–‡æ¡£æ— æ³•直接预览,请下载查看。</p>
        <el-button type="primary" @click="downloadFile">下载文件</el-button>
                    </div>
                  </el-tooltip>
                </div>
              </template>
            </el-table-column>
    <!-- CSV è¡¨æ ¼é¢„览 -->
    <div v-else-if="isCsv">
      <el-table v-if="csvRows.length" :data="csvRows" height="75vh">
        <el-table-column :label="col" :prop="col" v-for="(col, i) in csvColumns" :key="i" min-width="120px"
          show-overflow-tooltip />
          </el-table>
        </el-tab-pane>
      </el-tabs>
      <div v-else-if="csvError" class="preview-fallback">
        <p>CSV æ–‡ä»¶è§£æžå¤±è´¥ï¼Œè¯·ä¸‹è½½æŸ¥çœ‹ã€‚</p>
        <el-button type="primary" @click="downloadFile">下载文件</el-button>
    </div>
    <div v-if="!isSupported">
      <p>不支持的文件格式</p>
      <p v-else>正在加载…</p>
    </div>
    <!-- æ–‡æœ¬æ–‡ä»¶ -->
    <div v-else-if="isText">
      <pre v-if="textLoaded" class="text-preview">{{ textContent }}</pre>
      <div v-else-if="textError" class="preview-fallback">
        <p>文本文件读取失败,请下载查看。</p>
        <el-button type="primary" @click="downloadFile">下载文件</el-button>
      </div>
      <p v-else>正在加载…</p>
    </div>
    <!-- å…œåº•:压缩包、程序文件等浏览器无法直接显示的格式,保证也可下载 -->
    <div v-else class="preview-fallback">
      <p>该类型文件({{ fileExt }})浏览器无法在线显示,请下载后查看。</p>
      <el-button type="primary" @click="downloadFile">下载文件</el-button>
    </div>
  </div>
</template>
@@ -85,22 +99,29 @@
  data() {
    return {
      isDocShow: true,
      csvList: [], //csv文件数据
      csvColumns: [],
      csvRows: [],
      csvError: false,
      textContent: '',
      textLoaded: false,
      textError: false,
      pdfUrl: '', //pdf本地预览地址
      pdfError: false
    }
  },
  computed: {
    // åŽŸå®žçŽ°æŠŠ isImage / isXls / isCsv å†™æˆäº†ã€Œè¯»æ–‡ä»¶åŽç¼€ + é¡ºæ‰‹æ”¹åˆ«çš„响应式状态」的 computed:
    // isImage é‡Œå†™ imgUrl、isXls é‡Œå†™ options.xls、isCsv é‡Œç›´æŽ¥å‘请求。
    // computed é‡Œå†™çŠ¶æ€å±žäºŽå‰¯ä½œç”¨ï¼Œåœ¨ Vue3 ä¸­ä¼šåœ¨æ¸²æŸ“期触发状态变更,很容易演变成递归更新。
    // è¿™é‡Œæ‹†æˆçº¯è®¡ç®—属性,副作用统一交给下面那个 watch。
    isImage() {
      return /\.(jpg|jpeg|png|gif)$/i.test(this.fileUrl)
    },
    imgUrl() {
      // åŽŸ isImage æ— è®ºçœŸå‡éƒ½ä¼šå†™ imgUrl = fileUrl(命中图片时把 word æ¢æˆ img)
      // å›¾ç‰‡å­˜åœ¨ /img é™æ€è·¯å¾„下,调用方传的可能是 /word åœ°å€ï¼Œè¿™é‡Œç»Ÿä¸€æ¢æˆ /img
      return this.isImage ? this.fileUrl.replaceAll('word', 'img') : ''
    },
    isVideo() {
      return /\.(mp4|webm|ogv|ogg)$/i.test(this.fileUrl)
    },
    isAudio() {
      return /\.(mp3|wav|m4a|aac|flac|oga)$/i.test(this.fileUrl)
    },
    isPdf() {
      return /\.pdf$/i.test(this.fileUrl)
@@ -110,6 +131,12 @@
    },
    isXls() {
      return /\.(xls|xlsx)$/i.test(this.fileUrl)
    },
    isCsv() {
      return /\.csv$/i.test(this.fileUrl)
    },
    isText() {
      return /\.(txt|log|json|xml|md|ini|conf|cfg|yml|yaml|sql|properties)$/i.test(this.fileUrl)
    },
    // vue-office çš„ excel ç»„件用 xls æ ‡å¿—区分 xlsx / xls
    excelOptions() {
@@ -121,20 +148,15 @@
        heightOffset: 10, //在默认渲染的列表高度上再加 Npx高
        beforeTransformData: workbookData => {
          return workbookData
        }, //底层通过exceljs获取excel文件内容,通过该钩子函数,可以对获取的excel文件内容进行修改,比如某个单元格的数据显示不正确,可以在此自行修改每个单元格的value值。
        }, //底层通过exceljs获取excel文件内容,通过该钩子函数,可以对获取到的excel文件内容进行修改,比如某个单元格的数据显示不正确,可以在此自行修改每个单元格的value值。
        transformData: workbookData => {
          return workbookData
        } //将获取到的excel数据进行处理之后且渲染到页面之前,可通过transformData对即将渲染的数据及样式进行修改,此时每个单元格的text值就是即将渲染到页面上的内容
      }
    },
    isZipOrRar() {
      return /\.(zip|rar)$/i.test(this.fileUrl)
    },
    isCsv() {
      return /\.csv$/i.test(this.fileUrl)
    },
    isSupported() {
      return this.isImage || this.isPdf || this.isDoc || this.isZipOrRar || this.isCsv || this.isXls
    fileExt() {
      const match = this.fileUrl.match(/\.([a-zA-Z0-9]+)$/)
      return match ? match[1].toUpperCase() : '未知'
    },
    // è°ƒç”¨æ–¹ä¼ äº† OnlyOffice çš„配置才走文档服务,否则用浏览器内置阅读器
    useOnlyOffice() {
@@ -144,10 +166,9 @@
  watch: {
    fileUrl: {
      handler() {
        this.resetState()
        this.loadPdf()
        if (this.isCsv) {
          this.loadCSVData()
        }
        this.loadTextContent()
      },
      immediate: true
    }
@@ -156,6 +177,16 @@
    this.releasePdfUrl()
  },
  methods: {
    // åˆ‡æ¢æ–‡ä»¶æ—¶æ¸…掉上一个文件的预览状态,避免沿用旧结果(比如上次 Word æ¸²æŸ“失败,这次还被卡在下载兜底页)
    resetState() {
      this.isDocShow = true
      this.csvColumns = []
      this.csvRows = []
      this.csvError = false
      this.textContent = ''
      this.textLoaded = false
      this.textError = false
    },
    releasePdfUrl() {
      if (this.pdfUrl) {
        URL.revokeObjectURL(this.pdfUrl)
@@ -178,51 +209,44 @@
        this.pdfError = true
      }
    },
    renderedHandler() {
      console.log('渲染完成')
      this.isDocShow = true
      this.resetStyle()
    },
    errorHandler() {
      console.log('渲染失败')
      this.isDocShow = false
    },
    async loadCSVData() {
      // åŽŸå®žçŽ°ä¾èµ– this.$axios / this.$api,而这两个全局变量在项目里从未注册过
      // ï¼ˆmain.js æ²¡æœ‰æŒ‚载,也没有 api èšåˆå¯¹è±¡ï¼‰ï¼Œä¸€æ—¦è¿›å…¥ CSV åˆ†æ”¯å°±æŠ› TypeError。
      // åœ¨æ‹¿åˆ°çœŸå®žæŽ¥å£ä¹‹å‰å…ˆæ˜¾å¼é™çº§ä¸ºã€Œä¸‹è½½æŸ¥çœ‹ã€ï¼Œä¸Žç”¨æˆ·å®žé™…看到的结果一致。
      if (!this.$axios || !this.$api?.insOrderPlan?.preview) {
        console.warn('[filePreview] æœªé…ç½® $axios/$api,CSV è½¬è¡¨æ ¼ä¸å¯ç”¨ï¼Œå·²é™çº§ä¸ºä¸‹è½½æŸ¥çœ‹')
        this.csvList = []
    // CSV è§£æžæˆè¡¨æ ¼ã€æ–‡æœ¬æ–‡ä»¶ç›´æŽ¥å±•示;内容常见为 GBK ç¼–码,UTF-8 è§£å‡ºä¹±ç æ—¶æ¢ GBK å†è§£ä¸€æ¬¡
    async loadTextContent() {
      if (!this.isCsv && !this.isText) {
        return
      }
      this.$axios
        .post(this.$api.insOrderPlan.preview, {
          id: this.currentFile.id
        })
        .then(res => {
          let arr = res.data
          arr = arr.map(m => {
            const obj = {
              sheetName: m.sheetName,
              tableData: [],
              column: []
      try {
        const { data } = await axios.get(this.fileUrl, { responseType: 'arraybuffer' })
        let text = new TextDecoder('utf-8').decode(data)
        // è§£ç ç»“果里出现 U+FFFD æ›¿æ¢ç¬¦ï¼Œè¯´æ˜ŽåŽŸæ–‡ä¸æ˜¯ UTF-8,常见是 GBK ç¼–码的中文文件
        if (text.indexOf(String.fromCharCode(0xFFFD)) !== -1) {
          try {
            text = new TextDecoder('gbk').decode(data)
          } catch (e) {
            // ä¸ªåˆ«æµè§ˆå™¨ä¸æ”¯æŒ gbk è§£ç ï¼Œä¿ç•™ UTF-8 ç»“æžœ
            }
            obj.tableData = this.formatCSVToTable(m.content.replaceAll('null', ' '))
            // .replaceAll('MIN','=MIN').replaceAll('MAX','=MAX').replaceAll('AVERAGE','=AVERAGE')
            for (const item in obj.tableData[0]) {
              obj.column.push({
                label: item,
                prop: item
              })
            }
            return obj
          })
          this.csvList = arr
        })
        .catch(err => {
          console.log(err)
        })
        if (this.isCsv) {
          const rows = this.formatCSVToTable(text.replaceAll('\r\n', '\n').replaceAll('\r', '\n'))
          if (rows.length) {
            this.csvColumns = Object.keys(rows[0])
            this.csvRows = rows.slice(0, 5000)
          } else {
            this.csvError = true
          }
        } else {
          this.textContent = text.length > 1000000
            ? text.slice(0, 1000000) + '\n\n……(文件过大,仅预览前一百万字,完整内容请下载查看)'
            : (text || '(文件内容为空)')
          this.textLoaded = true
        }
      } catch (error) {
        console.error(error)
        if (this.isCsv) {
          this.csvError = true
        } else {
          this.textError = true
        }
      }
    },
    formatCSVToTable(str) {
      const result = []
@@ -239,19 +263,10 @@
            } else {
              const obj = {}
              for (let index = 0; index < arrHeader.length; index++) {
                // ç»„装表格数据
                const name = String(arrHeader[index])
                const name = String(arrHeader[index]).trim()
                if (!arrHeader[index]) continue
                if (!obj[name]) {
                  try {
                    if (kv[index]) {
                      obj[name] = String(kv[index])
                    } else {
                      obj[name] = ''
                    }
                  } catch (err) {
                    obj[name] = ''
                  }
                  obj[name] = kv[index] ? String(kv[index]) : ''
                }
              }
              result.push(obj)
@@ -260,6 +275,21 @@
        }
      }
      return result
    },
    downloadFile() {
      const name = this.currentFile && this.currentFile.fileName
        ? this.currentFile.fileName
        : this.fileUrl.split('/').pop()
      this.$download.saveAs(this.fileUrl, name)
    },
    renderedHandler() {
      console.log('渲染完成')
      this.isDocShow = true
      this.resetStyle()
    },
    errorHandler() {
      console.log('渲染失败')
      this.isDocShow = false
    },
    resetStyle() {
      // æ³¨ï¼šåŽŸå®žçŽ°æ˜¯ style.replace(/pt/g, 'px'),会把样式里任意位置的 pt éƒ½æ¢æŽ‰
@@ -282,9 +312,26 @@
  max-width: 100%;
}
.oneLine {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
.text-preview {
  white-space: pre-wrap;
  word-break: break-all;
  max-height: 85vh;
  overflow: auto;
  padding: 12px;
  margin: 0;
  background: #f8f9fa;
  font-family: Consolas, Monaco, monospace;
  font-size: 13px;
  line-height: 1.6;
}
.preview-fallback {
  text-align: center;
  padding-top: 15vh;
  color: #606266;
}
.preview-fallback p {
  margin-bottom: 16px;
}
</style>
src/components/echarts/echarts.vue
@@ -210,21 +210,18 @@
    },
    generateChart(option) {
      // é…ç½®æŠ˜çº¿å›¾å’ŒæŸ±çŠ¶å›¾çš„æ ·å¼
      // æ ·å¼åªåŠ åœ¨å‰¯æœ¬ä¸Šï¼Œä¸èƒ½å›žå†™ this.series:series æ˜¯çˆ¶é¡µé¢çš„响应式数据,
      // å›žå†™ä¼šè§¦å‘本组件 series çš„ deep watcher å†æ¬¡ renderChart,每次生成新对象永不收敛,页面死循环卡死
      if (option.series && option.series.length > 0) {
        option.series.forEach((s, index) => {
        option.series = option.series.map((s, index) => {
          if (s.type === 'line') {
            s.itemStyle = {
              color: this.lineColors[index] || this.lineColors[0]
            }
            s.lineStyle = {
              color: this.lineColors[index] || this.lineColors[0]
            }
            const color = this.lineColors[index] || this.lineColors[0]
            return { ...s, itemStyle: { color }, lineStyle: { color } }
          } else if (s.type === 'bar') {
            s.itemStyle = {
              color: this.barColors[index] || this.barColors[0]
            const color = this.barColors[index] || this.barColors[0]
            return { ...s, itemStyle: { color } }
            }
          }
          return s
        })
      }
      this.chartInstance.clear()
src/layout/components/Navbar.vue
@@ -5,6 +5,28 @@
      <span class="label">LIMS实验室管理系统</span>
    </div>
    <div class="right-menu">
      <div class="menu-search">
        <el-select
          v-model="selectedMenu"
          placeholder="搜索菜单"
          filterable
          clearable
          :fit-input-width="false"
          :filter-method="filterMenus"
          @change="handleMenuSelect"
          @clear="resetMenuFilter"
        >
          <template #prefix>
            <lims-icon name="el-icon-search" />
          </template>
          <el-option
            v-for="item in filteredMenus"
            :key="item.path"
            :label="item.breadcrumb"
            :value="item.path"
          />
        </el-select>
      </div>
      <div class="avatar-wrapper">
        <el-avatar shape="square" :size="30">{{ avatarText }}</el-avatar>
        <span class="userName">{{ userStore.nickName }}</span>
@@ -31,11 +53,61 @@
import Hamburger from '@/components/Hamburger'
import useAppStore from '@/store/modules/app'
import useUserStore from '@/store/modules/user'
import usePermissionStore from '@/store/modules/permission'
import { getNormalPath } from '@/utils/ruoyi'
const appStore = useAppStore()
const userStore = useUserStore()
const permissionStore = usePermissionStore()
const router = useRouter()
const avatarText = computed(() => (userStore.nickName || '').substring(0, 1))
const selectedMenu = ref(undefined)
const filteredMenus = ref([])
// ä¾§è¾¹æ èœå•树拍平成可搜索列表(标题 + å®Œæ•´è·¯å¾„面包屑)
const menuList = computed(() => {
  const list = []
  const walk = (routes, basePath, parents) => {
    ;(routes || []).forEach(route => {
      if (!route || route.hidden || typeof route.path !== 'string') return
      const fullPath = getNormalPath(`${basePath}/${route.path}`)
      const title = route.meta && route.meta.title
      const children = Array.isArray(route.children) ? route.children : []
      if (children.length) {
        walk(children, fullPath, title ? parents.concat(title) : parents)
      } else if (title && fullPath) {
        list.push({ title, path: fullPath, breadcrumb: parents.concat(title).join(' / ') })
      }
    })
  }
  walk(permissionStore.sidebarRouters, '', [])
  return list
})
function filterMenus(query) {
  const q = (query || '').trim().toLowerCase()
  filteredMenus.value = q
    ? menuList.value.filter(item =>
        item.title.toLowerCase().includes(q) || item.breadcrumb.toLowerCase().includes(q)
      )
    : menuList.value
}
function resetMenuFilter() {
  filteredMenus.value = menuList.value
}
function handleMenuSelect(path) {
  if (path) {
    router.push(path).catch(() => {})
  }
  selectedMenu.value = undefined
  resetMenuFilter()
}
watch(menuList, resetMenuFilter, { immediate: true })
function toggleSideBar() {
  appStore.toggleSideBar()
@@ -123,6 +195,18 @@
      outline: none;
    }
    .menu-search {
      margin-right: 20px;
      :deep(.el-select) {
        width: 210px;
      }
      :deep(.el-select__wrapper) {
        border-radius: 16px;
      }
    }
    .avatar-wrapper {
      display: flex;
      align-items: center;
src/views/CNAS/externalService/supplierManage/index.vue
@@ -22,12 +22,16 @@
        :page="page" :tableLoading="tableLoading"></lims-table>
    </div>
    <form-dia ref="formDia" v-if="formDia" @closeDia="closeDia"></form-dia>
    <lims-attachment ref="attachmentDialog" title="供应商附件" name-field="supplierName" id-param="supplierManagementId"
      upload-url="/supplierManagement/uploadAttachment" select-url="/supplierManagement/selectAttachments"
      delete-url="/supplierManagement/deleteAttachment" />
  </div>
</template>
<script>
import FormDia from "../supplierManage/component/formDia.vue";
import limsTable from '@/components/Table/lims-table.vue'
import limsAttachment from '@/components/Attachment/lims-attachment.vue'
import {
  selectQualifiedSupplierManagementPage,
  exportSupplierManagement,
@@ -39,7 +43,8 @@
  // import å¼•入的组件需要注入到对象中才能使用
  components: {
    limsTable,
    FormDia
    FormDia,
    limsAttachment
  },
  data() {
    // è¿™é‡Œå­˜æ”¾æ•°æ®
@@ -83,6 +88,13 @@
              },
            },
            {
              name: '附件',
              type: 'text',
              clickFun: (row) => {
                this.$refs.attachmentDialog.open(row);
              },
            },
            {
              name: '删除',
              type: 'text',
              color: 'var(--el-color-danger)',
src/views/CNAS/resourceDemand/standardMaterailRequistion/index.vue
@@ -59,9 +59,24 @@
          minWidth: '100'
        },
        {
          label: '数量',
          prop: 'factoryNum',
          label: '原始数量',
          prop: 'quantity',
          minWidth: '100'
        },
        {
          label: '单位',
          prop: 'unit',
          minWidth: '80'
        },
        {
          label: '本次领用数量',
          prop: 'borrowQuantity',
          minWidth: '110'
        },
        {
          label: '余量',
          prop: 'remainingQuantity',
          minWidth: '80'
        },
        {
          label: '领用人',
@@ -71,21 +86,6 @@
        {
          label: '领用日期',
          prop: 'borrowDate',
          minWidth: '100'
        },
        {
          label: '归还人',
          prop: 'returnIntegrity',
          minWidth: '100'
        },
        {
          label: '归还日期',
          prop: 'returnDate',
          minWidth: '100'
        },
        {
          label: '归还时状态',
          prop: 'returnIntegrity',
          minWidth: '100'
        }
      ],
@@ -97,9 +97,6 @@
        current: 1
      },
      total: 0,
      formDia: false,
      borrowDia: false,
      returnDia: false,
    }
  },
  mounted() {
src/views/CNAS/resourceDemand/standardMaterial/component/borrowDia.vue
@@ -3,7 +3,7 @@
    <el-dialog v-loading="diaLoading" :close-on-click-modal="false"
               :close-on-press-escape="false"
               v-model="formDia"
               title="标准物质借用"
               title="标准物质领用"
               width="80%" @close="closeBorrowDia">
      <el-form ref="form" :model="form" :rules="rules" label-position="right" label-width="auto">
        <el-row :gutter="20">
@@ -18,12 +18,22 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="剩余可领用">
              <el-input v-model="remaining" disabled size="small"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="领用数量" prop="borrowQuantity">
              <el-input-number v-model="form.borrowQuantity" :min="1" :max="remaining > 0 ? remaining : 1" size="small" style="width: 100%"></el-input-number>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="完好性" prop="integrity">
              <el-input v-model="form.integrity" clearable size="small"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="借用人" prop="borrowUser">
            <el-form-item label="领用人" prop="borrowUser">
              <el-input v-model="form.borrowUser" clearable size="small"></el-input>
            </el-form-item>
          </el-col>
@@ -33,7 +43,7 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="借出日期" prop="borrowDate">
            <el-form-item label="领用日期" prop="borrowDate">
              <el-date-picker
                v-model="form.borrowDate"
                clearable
@@ -44,18 +54,7 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="约定归还日期" prop="borrowReturnDate">
              <el-date-picker
                v-model="form.borrowReturnDate"
                clearable
                format="YYYY-MM-DD HH:mm:ss" placeholder="选择日期"
                size="small" style="width: 100%"
                type="datetime" value-format="YYYY-MM-DD HH:mm:ss">
              </el-date-picker>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="借出人" prop="lender">
            <el-form-item label="发放人" prop="lender">
              <el-input v-model="form.lender" clearable size="small"></el-input>
            </el-form-item>
          </el-col>
@@ -71,7 +70,8 @@
<script>
import {
  borrowSubstance
  borrowSubstance,
  getRemainingQuantity
} from "@/api/cnas/resourceDemand/standardMaterial/standardMaterial";
export default {
@@ -91,17 +91,34 @@
        borrowUser: '',
        phone: '',
        borrowDate: '',
        borrowReturnDate: '',
        lender: '',
        substanceId: ''
        substanceId: '',
        borrowQuantity: null
      },
      remaining: 0,
      rules: {
        integrity: [{required: true, message: '请填写完好性',trigger: 'blur'}],
        borrowUser: [{required: true, message: '请填写借用人',trigger: 'blur'}],
        borrowUser: [{required: true, message: '请填写领用人',trigger: 'blur'}],
        phone: [{required: true, message: '请填写联系方式',trigger: 'blur'}],
        borrowDate: [{required: true, message: '请选择借出日期',trigger: 'change'}],
        borrowReturnDate: [{required: true, message: '请选择约定归还日期',trigger: 'change'}],
        lender: [{required: true, message: '请选择lender',trigger: 'blur'}],
        borrowDate: [{required: true, message: '请选择领用日期',trigger: 'change'}],
        lender: [{required: true, message: '请填写发放人',trigger: 'blur'}],
        borrowQuantity: [
          {required: true, message: '请填写领用数量', trigger: 'blur'},
          {
            validator: (rule, value, callback) => {
              if (value === null || value === undefined || value === '') {
                callback(new Error('请填写领用数量'))
              } else if (Number(value) <= 0) {
                callback(new Error('领用数量必须大于0'))
              } else if (Number(value) > this.remaining) {
                callback(new Error('领用数量不能超过剩余可领用数量'))
              } else {
                callback()
              }
            },
            trigger: 'blur'
          },
        ],
      },
    };
  },
@@ -116,6 +133,12 @@
      this.form.name = row.name
      this.form.model = row.model
      this.form.substanceId = row.id
      this.remaining = 0
      getRemainingQuantity(row.id).then(res => {
        if (res.code === 200) {
          this.remaining = res.data
        }
      })
    },
    // æäº¤å¼¹æ¡†æ•°æ®
    handleEdit () {
src/views/CNAS/resourceDemand/standardMaterial/component/formDia.vue
@@ -23,6 +23,13 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="供应商" prop="supplier">
              <el-select v-model="form.supplier" clearable filterable placeholder="请选择" size="small" style="width: 100%">
                <el-option v-for="item in supplierOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="出厂编号" prop="factoryNum">
              <el-input v-model="form.factoryNum" clearable size="small"></el-input>
            </el-form-item>
@@ -32,14 +39,19 @@
              <el-input v-model="form.manageNum" clearable size="small"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
          <el-col :span="8">
            <el-form-item label="不确定度" prop="uncertainty">
              <el-input v-model="form.uncertainty" clearable size="small"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
          <el-col :span="8">
            <el-form-item label="数量" prop="quantity">
              <el-input v-model="form.quantity" clearable size="small"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="单位" prop="unit">
              <el-input v-model="form.unit" clearable size="small"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -69,7 +81,7 @@
              <el-input v-model="form.fileNum" clearable size="small"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="24">
          <el-col :span="12">
            <el-form-item label="存放位置" prop="position">
              <el-input v-model="form.position" clearable size="small"></el-input>
            </el-form-item>
@@ -94,6 +106,7 @@
import {
  addStandardSubstance
} from "@/api/cnas/resourceDemand/standardMaterial/standardMaterial";
import { selectQualifiedSupplierManagementPage } from "@/api/cnas/externalService/supplierManage/supplierManage";
export default {
  name: 'formDia',
@@ -109,10 +122,12 @@
        name: '',
        model: '',
        factoryManufacturer: '',
        supplier: '',
        factoryNum: '',
        manageNum: '',
        uncertainty: '',
        quantity: '',
        unit: '',
        acquisitionDate: '',
        effectiveDate: '',
        fileNum: '',
@@ -123,7 +138,6 @@
      rules: {
        name: [{required: true, message: '请填写标准物质名称',trigger: 'blur'}],
        model: [{required: true, message: '请填写规格型号',trigger: 'blur'}],
        factoryManufacturer: [{required: true, message: '请填写生产厂家',trigger: 'blur'}],
        factoryNum: [{required: true, message: '请填写出厂编号',trigger: 'blur'}],
        manageNum: [{required: true, message: '请填写管理编号',trigger: 'blur'}],
        uncertainty: [{required: true, message: '请填写不确定度',trigger: 'blur'}],
@@ -134,6 +148,7 @@
        position: [{required: true, message: '请填写存放位置',trigger: 'blur'}],
      },
      operationType: '',
      supplierOptions: [],
    };
  },
  mounted() {
@@ -143,12 +158,23 @@
  methods: {
    // æ‰“开弹框
    openDia (type, row) {
      this.loadSuppliers()
      this.formDia = true
      this.operationType = type
      if (type !== 'add') {
        this.form = row
      }
    },
    loadSuppliers () {
      selectQualifiedSupplierManagementPage({ current: 1, size: 999 }).then(res => {
        if (res.code === 200) {
          this.supplierOptions = (res.data.records || []).map(item => ({
            label: item.supplierName,
            value: item.supplierName
          }))
        }
      })
    },
    // æäº¤å¼¹æ¡†æ•°æ®
    handleEdit () {
      this.$refs['form'].validate((valid) => {
src/views/CNAS/resourceDemand/standardMaterial/component/returnDia.vue
ÎļþÒÑɾ³ý
src/views/CNAS/resourceDemand/standardMaterial/index.vue
@@ -20,7 +20,6 @@
      <div>
        <el-button size="small" @click="exportFun">导 å‡º</el-button>
        <el-button size="small" type="primary" @click="importVisible = true">导 å…¥</el-button>
        <el-button size="small" type="primary" @click="openFormDia('add')">新 å¢ž</el-button>
      </div>
    </div>
    <div class="table">
@@ -28,8 +27,7 @@
        :page="page" :tableLoading="tableLoading"></lims-table>
    </div>
    <form-dia v-if="formDia" ref="formDia" @closeYearDia="closeYearDia"></form-dia>
    <borrow-dia v-if="borrowDia" ref="borrowDia" @closeYearDia="closeBorrowDia"></borrow-dia>
    <return-dia v-if="returnDia" ref="returnDia" @closeYearDia="closeReturnDia"></return-dia>
    <borrow-dia v-if="borrowDia" ref="borrowDia" @closeBorrowDia="closeBorrowDia"></borrow-dia>
    <el-dialog title="标准物质导入" v-model="importVisible" width="500px" @closed="resetImport">
      <div style="margin-bottom: 12px;">
        è¯·ä½¿ç”¨æ ‡å‡†ç‰©è´¨å¯¼å…¥æ¨¡æ¿å¡«å†™æ•°æ®ï¼Œå•个文件不超过10MB。
@@ -61,14 +59,12 @@
} from '@/api/cnas/resourceDemand/standardMaterial/standardMaterial'
import FormDia from '../standardMaterial/component/formDia.vue';
import BorrowDia from '../standardMaterial/component/borrowDia.vue';
import ReturnDia from '../standardMaterial/component/returnDia.vue';
export default {
  name: 'StandardMaterial',
  components: {
    limsTable,
    FormDia,
    BorrowDia,
    ReturnDia
    BorrowDia
  },
  data() {
    return {
@@ -93,6 +89,11 @@
          minWidth: '100'
        },
        {
          label: '供应商',
          prop: 'supplier',
          minWidth: '120'
        },
        {
          label: '出厂编号',
          prop: 'factoryNum',
          minWidth: '100'
@@ -111,6 +112,11 @@
          label: '数量',
          prop: 'quantity',
          minWidth: '100'
        },
        {
          label: '单位',
          prop: 'unit',
          minWidth: '80'
        },
        {
          label: '购置日期',
@@ -156,18 +162,11 @@
              },
            },
            {
              name: '借用',
              name: '领用',
              type: 'text',
              clickFun: (row) => {
                this.borrow(row);
              },
            },
            {
              name: '归还',
              type: 'text',
              clickFun: (row) => {
                this.return(row);
              }
            },
            {
              name: '删除',
@@ -190,7 +189,6 @@
      total: 0,
      formDia: false,
      borrowDia: false,
      returnDia: false,
      importVisible: false,
      importing: false,
      importFiles: [],
@@ -316,17 +314,6 @@
    },
    closeBorrowDia() {
      this.borrowDia = false
      this.searchList()
    },
    // å½’还
    return(row) {
      this.returnDia = true
      this.$nextTick(() => {
        this.$refs.returnDia.openDia(row)
      })
    },
    closeReturnDia() {
      this.returnDia = false
      this.searchList()
    },
    // é‡ç½®æŸ¥è¯¢æ¡ä»¶
src/views/CNAS/resourceDemand/standardMaterialAccept/component/AddRecord.vue
@@ -1,15 +1,146 @@
<template>
  <el-dialog v-model="dialogVisible" title="标准物质验收记录" width="72%" v-loading="loading">
    <el-form :model="acceptance" ref="acceptance" label-width="140px" size="small">
    <el-form ref="form" :model="form" :rules="rules" label-width="140px" size="small">
      <div class="section-title">标准物质信息</div>
      <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="厂家代表" prop="producer">
          <el-input v-model="acceptance.producer"></el-input>
          <el-form-item label="标准物质名称" prop="substance.name">
            <el-input v-model="form.substance.name" clearable></el-input>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="相关附件" prop="file">
          <el-form-item label="规格型号" prop="substance.model">
            <el-input v-model="form.substance.model" clearable></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="生产厂家" prop="substance.factoryManufacturer">
            <el-input v-model="form.substance.factoryManufacturer" clearable></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="供应商" prop="substance.supplier">
            <el-select v-model="form.substance.supplier" clearable filterable placeholder="请选择" style="width: 100%" @change="onSupplierChange">
              <el-option v-for="item in supplierOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="出厂编号" prop="substance.factoryNum">
            <el-input v-model="form.substance.factoryNum" clearable></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="管理编号" prop="substance.manageNum">
            <el-input v-model="form.substance.manageNum" clearable></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="不确定度" prop="substance.uncertainty">
            <el-input v-model="form.substance.uncertainty" clearable></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="数量" prop="substance.quantity">
            <el-input v-model="form.substance.quantity" clearable></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="单位" prop="substance.unit">
            <el-input v-model="form.substance.unit" clearable></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="购置日期" prop="substance.acquisitionDate">
            <el-date-picker
              v-model="form.substance.acquisitionDate"
              clearable
              type="datetime"
              format="YYYY-MM-DD HH:mm:ss"
              value-format="YYYY-MM-DD HH:mm:ss"
              placeholder="选择日期"
              style="width: 100%">
            </el-date-picker>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="有效期" prop="substance.effectiveDate">
            <el-date-picker
              v-model="form.substance.effectiveDate"
              clearable
              type="datetime"
              format="YYYY-MM-DD HH:mm:ss"
              value-format="YYYY-MM-DD HH:mm:ss"
              placeholder="选择日期"
              style="width: 100%">
            </el-date-picker>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="档案编号" prop="substance.fileNum">
            <el-input v-model="form.substance.fileNum" clearable></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="存放位置" prop="substance.position">
            <el-input v-model="form.substance.position" clearable></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="24">
          <el-form-item label="备注" prop="substance.remark">
            <el-input v-model="form.substance.remark" clearable></el-input>
          </el-form-item>
        </el-col>
      </el-row>
      <div class="section-title">验收信息</div>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="到货日期" prop="acceptance.arriveDate">
            <el-date-picker
              v-model="form.acceptance.arriveDate"
              type="date"
              placeholder="选择日期"
              style="width: 100%"
              value-format="YYYY-MM-DD"
            >
            </el-date-picker>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="厂家代表" prop="acceptance.producer">
            <el-input v-model="form.acceptance.producer"></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="接收人" prop="acceptance.recipient">
            <el-input v-model="form.acceptance.recipient"></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="接受签字" prop="acceptance.signature">
            <el-input v-model="form.acceptance.signature"></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="验收情况" prop="acceptance.situation">
            <el-input v-model="form.acceptance.situation"></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="安装调试情况" prop="acceptance.installation">
            <el-input v-model="form.acceptance.installation"></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="维修单位" prop="acceptance.maintenanceUnit">
            <el-input v-model="form.acceptance.maintenanceUnit"></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="相关附件" prop="acceptance.file">
          <div class="table-between">
            <el-input v-model="acceptance.file" style="width: 80%;" disabled></el-input>
              <el-input v-model="form.acceptance.file" style="width: 80%;" disabled></el-input>
            <el-upload
              ref="upload"
              style="float: right;"
@@ -25,61 +156,12 @@
          </div>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="接收人" prop="recipient">
          <el-input v-model="acceptance.recipient"></el-input>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="接受签字" prop="signature">
          <el-input v-model="acceptance.signature"></el-input>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="验收情况" prop="situation">
          <el-input v-model="acceptance.situation"></el-input>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="安装调试情况" prop="installation">
          <el-input v-model="acceptance.installation"></el-input>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="清单" prop="substanceId"
                      :rule="[{ required: true, message: '请选择清单', trigger: 'change' }]">
          <el-select v-model="acceptance.substanceId" placeholder="请选择" style="width: 100%">
            <el-option
              v-for="item in options"
              :key="item.value"
              :label="item.name"
              :value="item.id">
            </el-option>
          </el-select>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="到货日期" prop="arriveDate">
          <el-date-picker
            v-model="acceptance.arriveDate"
            align="right"
            type="date"
            placeholder="选择日期"
            style="width: 100%"
            value-format="YYYY-MM-DD"
          >
          </el-date-picker>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="维修单位" prop="maintenanceUnit">
          <el-input v-model="acceptance.maintenanceUnit"></el-input>
        </el-form-item>
      </el-col>
      <el-col :span="24">
        <el-form-item label="备件确认">
          <el-button type="primary" style="float: right;" @click="addSpareTable">增加行</el-button>
        </el-form-item>
      </el-row>
      <div class="section-title">备件确认</div>
      <el-row :gutter="20">
        <el-col :span="24" class="spare-toolbar">
          <el-button type="primary" size="small" @click="addSpareTable">增加行</el-button>
      </el-col>
      <el-col :span="24">
        <lims-table :tableData="list" :column="spareColumns" height="300">
@@ -102,6 +184,7 @@
          </template>
        </lims-table>
      </el-col>
      </el-row>
    </el-form>
    <template #footer><span  class="dialog-footer">
      <el-button @click="closeDialog">取 æ¶ˆ</el-button>
@@ -114,19 +197,32 @@
import limsTable from "@/components/Table/lims-table.vue";
import {
  addAcceptance, getAcceptanceDetails,
  getStandardSubstanceAll,
  updateAcceptance
} from "@/api/cnas/resourceDemand/standardMaterialAccept/standardMaterialAccept";
export default {
  components: {
    limsTable,
  },
  data() {
import { selectQualifiedSupplierManagementPage } from "@/api/cnas/externalService/supplierManage/supplierManage";
function defaultSubstance() {
    return {
      dialogVisible: false,
      loading: false,
      submitLoading: false,
      acceptance: {
    id: undefined,
    name: undefined,
    model: undefined,
    factoryManufacturer: undefined,
    supplier: undefined,
    factoryNum: undefined,
    manageNum: undefined,
    uncertainty: undefined,
    quantity: undefined,
    unit: undefined,
    acquisitionDate: undefined,
    effectiveDate: undefined,
    fileNum: undefined,
    position: undefined,
    remark: undefined,
  }
}
function defaultAcceptance() {
  return {
        id: undefined,
        producer: undefined, // åŽ‚å®¶ä»£è¡¨
        file: undefined,   // ç›¸å…³é™„ä»¶
@@ -137,8 +233,35 @@
        substanceId: undefined, // æ¸…单
        arriveDate: undefined,   // åˆ°è´§æ—¥æœŸ
        maintenanceUnit: undefined,   // ç»´ä¿®å•位
        list: [],
  }
}
export default {
  components: {
    limsTable,
      },
  data() {
    return {
      dialogVisible: false,
      loading: false,
      submitLoading: false,
      form: {
        substance: defaultSubstance(),
        acceptance: defaultAcceptance(),
      },
      rules: {
        'substance.name': [{required: true, message: '请填写标准物质名称', trigger: 'blur'}],
        'substance.model': [{required: true, message: '请填写规格型号', trigger: 'blur'}],
        'substance.factoryNum': [{required: true, message: '请填写出厂编号', trigger: 'blur'}],
        'substance.manageNum': [{required: true, message: '请填写管理编号', trigger: 'blur'}],
        'substance.uncertainty': [{required: true, message: '请填写不确定度', trigger: 'blur'}],
        'substance.quantity': [{required: true, message: '请填写数量', trigger: 'blur'}],
        'substance.acquisitionDate': [{required: true, message: '请选择购置日期', trigger: 'change'}],
        'substance.effectiveDate': [{required: true, message: '请选择有效期', trigger: 'change'}],
        'substance.fileNum': [{required: true, message: '请填写档案编号', trigger: 'blur'}],
        'substance.position': [{required: true, message: '请填写存放位置', trigger: 'blur'}],
      },
      supplierOptions: [],
      list: [],
      spareColumns: [
        {
@@ -161,14 +284,11 @@
          width: 150
        }
      ],
      options: []
    }
  },
  mounted() {
    this.getStand()
  },
  methods: {
    openDialog(id) {
      this.loadSuppliers()
      if(id) {
        this.getDetail(id)
      } else {
@@ -176,31 +296,40 @@
      }
      this.dialogVisible = true
    },
    loadSuppliers() {
      selectQualifiedSupplierManagementPage({ current: 1, size: 999 }).then(res => {
        if (res.code === 200) {
          this.supplierOptions = (res.data.records || []).map(item => ({
            label: item.supplierName,
            value: item.supplierName
          }))
        }
      })
    },
    onSupplierChange(val) {
      this.form.acceptance.producer = val || ''
    },
    getDetail(id) {
      this.loading = true
      getAcceptanceDetails({id: id}).then(res => {
        this.acceptance = res.data.acceptance
        this.list = res.data.list
        this.form.acceptance = res.data.acceptance || defaultAcceptance()
        this.form.substance = res.data.substance || defaultSubstance()
        this.list = res.data.list || []
        this.loading = false
      }).catch(err => {
        this.loading = false
      })
    },
    getStand() {
      getStandardSubstanceAll().then(res => {
        this.options = res.data
      })
    },
    addSpareTable() {
      this.list.push({
        acceptanceId: this.acceptance.id,
        acceptanceId: this.form.acceptance.id,
        name: '',
        number: '',
        isEdit: true
      })
    },
    onSuccess(response) {
      this.acceptance.file = response.data
      this.form.acceptance.file = response.data
    },
    save(index) {
      this.list[index]['isEdit'] = false
@@ -209,39 +338,41 @@
      this.list[index]['isEdit'] = true
    },
    clearForm() {
      this.form.substance = defaultSubstance()
      this.form.acceptance = defaultAcceptance()
      this.list = []
      this.resetForm('acceptance')
      this.$nextTick(() => {
        if (this.$refs.form) {
          this.$refs.form.clearValidate()
        }
      })
    },
    closeDialog() {
      this.clearForm()
      this.resetForm('acceptance')
      this.dialogVisible = false
    },
    submit() {
      if (!this.acceptance.substanceId) {
        this.$message.warning('请选择清单')
        return
      this.$refs.form.validate((valid) => {
        if (!valid) {
          return false
      }
      this.acceptance.list = this.HaveJson(this.list)
        const isEdit = !!this.form.acceptance.id
      this.submitLoading = true
      if(this.acceptance.id) {
        updateAcceptance({acceptance: this.acceptance, list: this.list}).then(res => {
          this.$message.success('编辑成功')
          this.submitLoading = false
          this.closeDialog()
          this.$emit('submit')
        }).catch((e) => {
          this.submitLoading = false
        })
      } else {
        addAcceptance({acceptance: this.acceptance, list: this.list}).then(res => {
          this.$message.success('新增成功')
          this.closeDialog()
          this.$emit('submit')
        }).catch((e) => {
          this.submitLoading = false
        })
        const payload = {
          acceptance: this.form.acceptance,
          substance: this.form.substance,
          list: this.HaveJson(this.list)
      }
        const request = isEdit ? updateAcceptance : addAcceptance
        request(payload).then(res => {
          this.submitLoading = false
          this.$message.success(isEdit ? '编辑成功' : '新增成功')
          this.closeDialog()
          this.$emit('submit')
        }).catch((e) => {
          this.submitLoading = false
        })
      })
    },
  },
  computed: {
@@ -258,4 +389,34 @@
  height: 580px;
  overflow-y: auto;
}
.section-title {
  display: flex;
  align-items: center;
  font-size: 14px;
  font-weight: 600;
  color: var(--lims-text);
  padding-bottom: 8px;
  border-bottom: 1px solid var(--lims-divider);
  margin: 8px 0 16px;
}
.section-title:not(:first-child) {
  margin-top: 24px;
}
.section-title::before {
  content: '';
  display: inline-block;
  width: 3px;
  height: 14px;
  border-radius: 2px;
  background: var(--el-color-primary);
  margin-right: 8px;
}
.spare-toolbar {
  text-align: right;
  margin-bottom: 8px;
}
</style>
src/views/CNAS/resourceDemand/standardMaterialAccept/index.vue
@@ -48,7 +48,7 @@
      addRecordRef: false,
      columns: [
        {
          label: "出场编号",
          label: "出厂编号",
          prop: "factoryNum"
        },
        {
@@ -60,7 +60,7 @@
          prop: "factoryManufacturer"
        },
        {
          label: "文档编号",
          label: "档案编号",
          prop: "fileNum"
        },
        {
@@ -85,13 +85,17 @@
        // },
        {
          label: "提交日期",
          prop: "acquisitionDate"
          prop: "createTime"
        },
        {
          label: "数量",
          prop: "quantity"
        },
        {
          label: "单位",
          prop: "unit"
        },
        {
          fixed: "right",
          label: "操作",
          align: "center",
src/views/CNAS/systemManagement/customerSatisfaction/components/formDialog.vue
@@ -22,11 +22,19 @@
        </tr>
        <tr>
          <td class="td-title">
            <p>姓名:</p>
            <p>客户:</p>
          </td>
          <td class="td-info">
            <el-input v-model="form.userName" placeholder="请输入内容" size="small">
            </el-input>
            <el-select v-model="form.orderId" filterable clearable size="small" style="width: 100%"
              popper-class="customer-select-popper" placeholder="请选择客户(已完成委托单)" @change="onCustomerChange">
              <el-option v-for="opt in customerOptions" :key="opt.id" :label="opt.company" :value="opt.id">
                <div class="customer-card">
                  <div>客户:{{ opt.company }}</div>
                  <div class="customer-card-sub">委托单编号:{{ opt.entrustCode || '--' }}</div>
                  <div class="customer-card-sub">样品名称:{{ opt.sampleName || '--' }}</div>
                </div>
              </el-option>
            </el-select>
          </td>
          <td class="td-title">
            <p>部门:</p>
@@ -142,6 +150,7 @@
  addClientSatisfaction,
  updateClientSatisfaction
} from '@/api/cnas/systemManagement/customerSatisfaction.js'
import { selectCompletedOrders } from '@/api/business/productOrder'
export default {
  name: 'formDialog',
  // import å¼•入的组件需要注入到对象中才能使用
@@ -157,6 +166,7 @@
        department: '',
        contactNumber: '',
        serviceAttitude: '',
        serviceAttitudeSuggestion: '',
        technicalCompetence: '',
        technicalCompetenceSuggestion: '',
        inspectionWork: '',
@@ -165,7 +175,12 @@
        reasonableFeesSuggestion: '',
        remark: '',
        clientSatisfactionId: '',
        orderId: '',
        entrustCode: '',
        sampleName: ''
      },
      // å®¢æˆ·ä¸‹æ‹‰é€‰é¡¹ï¼šåªåŒ…含"已下过委托单且已完成"的数据
      customerOptions: [],
      operationType: '',
      editLoad: false,
@@ -173,14 +188,89 @@
  },
  // æ–¹æ³•集合
  methods: {
    openDia(type, row) {
    async openDia(type, row) {
      this.formDia = true;
      this.operationType = type
      await this.loadCustomerOptions()
      if (this.operationType === 'edit') {
        this.form = { ...row }
        // è€æ•°æ®æ²¡æœ‰å…³è”委托单:合成一个选项把原来的值回显出来,不强制重选
        if (row.orderId == null && row.userName) {
          this.customerOptions.unshift({
            id: 0,
            company: row.userName,
            entrustCode: row.entrustCode || '--',
            sampleName: row.sampleName || '--'
          })
          this.form.orderId = 0
        } else if (row.orderId && !this.customerOptions.some(o => o.id === row.orderId)) {
          // å…³è”的委托单已不是"已完成"状态,用记录里的信息兜底回显
          this.customerOptions.unshift({
            id: row.orderId,
            company: row.userName || '未知客户',
            entrustCode: row.entrustCode || '--',
            sampleName: row.sampleName || '--'
          })
        }
      } else {
        this.resetForm()
      }
    },
    resetForm() {
      this.form = {
        unitName: '',
        fillDate: '',
        userName: '',
        department: '',
        contactNumber: '',
        serviceAttitude: '',
        serviceAttitudeSuggestion: '',
        technicalCompetence: '',
        technicalCompetenceSuggestion: '',
        inspectionWork: '',
        inspectionWorkSuggestion: '',
        reasonableFees: '',
        reasonableFeesSuggestion: '',
        remark: '',
        clientSatisfactionId: '',
        orderId: '',
        entrustCode: '',
        sampleName: ''
      }
    },
    // ä¸‹æ‹‰æ•°æ®ï¼šåŽç«¯åªè¿”回"已检验"的委托单(state=4)
    loadCustomerOptions() {
      this.customerOptions = []
      return selectCompletedOrders().then(res => {
        if (res.code === 200 && res.data) {
          this.customerOptions = res.data.map(o => ({
            id: o.id,
            company: o.company,
            entrustCode: o.entrustCode,
            sampleName: o.sampleView
          }))
        }
      }).catch(err => {
        console.log('err---', err)
      })
    },
    // é€‰ä¸­å®¢æˆ·åŽï¼šå›žå¡«å®¢æˆ·åå¹¶è‡ªåŠ¨å¸¦å‡ºå•ä½åç§°ã€å§”æ‰˜å•ç¼–å·ã€æ ·å“åç§°
    onCustomerChange(val) {
      const opt = this.customerOptions.find(o => o.id === val)
      if (!opt || opt.id === 0) {
        return
      }
      this.form.userName = opt.company
      this.form.entrustCode = opt.entrustCode
      this.form.sampleName = opt.sampleName
      this.form.unitName = opt.company
    },
    handleEdit() {
      const orderIdEmpty = this.form.orderId === '' || this.form.orderId === null || this.form.orderId === undefined
      if (orderIdEmpty) {
        this.$message.warning('请选择客户')
        return
      }
      if (!this.form.unitName) {
        this.$message.warning('请填写单位名称')
        return
@@ -190,8 +280,13 @@
        return
      }
      this.editLoad = true
      const payload = { ...this.form }
      // è€æ•°æ®åˆæˆçš„回显选项(id=0)不算真实关联,保存时仍保持未关联
      if (payload.orderId === 0) {
        payload.orderId = null
      }
      if (this.operationType === 'add') {
        addClientSatisfaction(this.form).then(res => {
        addClientSatisfaction(payload).then(res => {
          this.editLoad = false
          this.$message.success('提交成功')
          this.closeFormDia()
@@ -200,7 +295,7 @@
          this.editLoad = false
        })
      } else {
        updateClientSatisfaction(this.form).then(res => {
        updateClientSatisfaction(payload).then(res => {
          this.editLoad = false
          this.$message.success('提交成功')
          this.closeFormDia()
@@ -251,4 +346,18 @@
  word-wrap: break-word;
  white-space: normal;
}
.customer-card-sub {
  color: #909399;
  font-size: 12px;
}
</style>
<style>
/* ä¸‹æ‹‰å¡ç‰‡ä¸‰è¡Œéœ€è¦è‡ªé€‚应高度,弹层 teleport åˆ° body,用自定义 popper-class å‘½ä¸­ */
.customer-select-popper .el-select-dropdown__item {
  height: auto;
  line-height: 1.7;
  padding: 6px 12px;
}
</style>
src/views/CNAS/systemManagement/customerSatisfaction/index.vue
@@ -50,6 +50,9 @@
        style="height: 90vh;overflow-y: auto;" />
    </el-dialog>
    <FormDialog v-if="formDialog" ref="formDialog" @closeFormDia="closeFormDia"></FormDialog>
    <lims-attachment ref="attachmentDialog" title="满意度附件" name-field="unitName" id-param="clientSatisfactionId"
      upload-url="/clientSatisfaction/uploadAttachment" select-url="/clientSatisfaction/selectAttachments"
      delete-url="/clientSatisfaction/deleteAttachment" />
  </div>
</template>
@@ -57,6 +60,7 @@
import limsTable from "@/components/Table/lims-table.vue";
import FormDialog from './components/formDialog.vue';
import filePreview from '@/components/Preview/filePreview.vue'
import limsAttachment from '@/components/Attachment/lims-attachment.vue'
import {
  pageClientSatisfaction,
  pageAnalyseFile,
@@ -67,7 +71,7 @@
export default {
  name: 'CustomerSatisfaction',
  // import å¼•入的组件需要注入到对象中才能使用
  components: { filePreview, FormDialog, limsTable },
  components: { filePreview, FormDialog, limsTable, limsAttachment },
  data() {
    // è¿™é‡Œå­˜æ”¾æ•°æ®
    return {
@@ -90,8 +94,18 @@
          minWidth: '100'
        },
        {
          label: '姓名',
          label: '客户',
          prop: 'userName',
          minWidth: '100'
        },
        {
          label: '委托单编号',
          prop: 'entrustCode',
          minWidth: '100'
        },
        {
          label: '样品名称',
          prop: 'sampleName',
          minWidth: '100'
        },
        {
@@ -122,6 +136,13 @@
              },
            },
            {
              name: '附件',
              type: 'text',
              clickFun: (row) => {
                this.$refs.attachmentDialog.open(row);
              },
            },
            {
              name: '删除',
              type: 'text',
              color: 'var(--el-color-danger)',