feat(order): 添加产品检验项选择功能并增加导入导出功能
- 实现产品检验项的选择状态管理和计数显示功能
- 添加标准物质和设备管理模块的导入导出功能
- 新增导入模板下载和上传验证机制
- 调整表单验证规则,移除生产单位必填限制
已修改7个文件
291 ■■■■■ 文件已修改
src/api/cnas/resourceDemand/device.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/cnas/resourceDemand/standardMaterial/standardMaterial.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/device/component/deviceOverview.vue 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/device/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/standardMaterial/index.vue 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/productOrder/components/addOrder.vue 123 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/productOrder/components/addView.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/cnas/resourceDemand/device.js
@@ -38,6 +38,15 @@
  });
}
// 下载设备导入模板
export function downloadDeviceImportTemplate() {
  return request({
    url: "/deviceScope/importTemplate",
    method: "get",
    responseType: "blob",
  });
}
//设备里面选择检验项目(树形结构)
export function getInsProduction(query) {
  return request({
src/api/cnas/resourceDemand/standardMaterial/standardMaterial.js
@@ -37,6 +37,15 @@
  });
}
// 下载标准物质导入模板
export function downloadStandardSubstanceImportTemplate() {
  return request({
    url: '/feStandardSubstance/importTemplate',
    method: 'get',
    responseType: 'blob'
  })
}
//标准物质清单借用
export function borrowSubstance(query) {
  return request({
src/views/CNAS/resourceDemand/device/component/deviceOverview.vue
@@ -20,6 +20,7 @@
        <div style="line-height: 30px;">
          <el-button size="mini" type="primary" @click="currentPage = 1, keyMap = {}, list = [], finishLoding = false, refreshTable()">查询</el-button>
          <el-button size="mini" @click="refresh()">重置</el-button>
          <el-button size="mini" type="primary" @click="importVisible = true">导入</el-button>
        </div>
      </div>
    </div>
@@ -73,6 +74,22 @@
        暂无数据
      </div>
    </div>
    <el-dialog title="设备导入" :visible.sync="importVisible" width="500px" @closed="resetImport">
      <div style="margin-bottom: 12px;">
        请使用设备导入模板填写数据,单个文件不超过10MB。
        <el-button type="text" @click="downloadTemplate">下载导入模板</el-button>
      </div>
      <el-upload ref="deviceUpload" drag name="file" accept=".xlsx" :action="importAction"
                 :headers="uploadHeader" :auto-upload="false" :limit="1" :file-list="importFiles"
                 :before-upload="beforeImport" :on-success="importSuccess" :on-error="importError">
        <i class="el-icon-upload"></i>
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
      </el-upload>
      <span slot="footer" class="dialog-footer">
        <el-button @click="importVisible = false">取消</el-button>
        <el-button type="primary" :loading="importing" @click="submitImport">导入</el-button>
      </span>
    </el-dialog>
  </div>
</template>
@@ -80,6 +97,7 @@
import ScrollPagination from '@/components/index/scroll-paging.vue'
import {
  selectDeviceParameter,
  downloadDeviceImportTemplate,
} from '@/api/cnas/resourceDemand/device.js'
export default {
  props: {
@@ -108,6 +126,14 @@
      finishLoding: false, // 加载完成,显示已经没有更多了
      keyMap: {},
      laboratoryNameIsNull: false,
      importVisible: false,
      importing: false,
      importFiles: [],
    }
  },
  computed: {
    importAction() {
      return this.javaApi + '/deviceScope/importDevice'
    }
  },
  created() {
@@ -118,6 +144,52 @@
    this.clickSidebar(this.clickNodeVal)
  },
  methods: {
    downloadTemplate() {
      downloadDeviceImportTemplate().then(res => {
        this.$download.saveAs(new Blob([res]), '设备导入模板.xlsx')
      })
    },
    submitImport() {
      if (!this.$refs.deviceUpload.uploadFiles.length) {
        this.$message.warning('请选择需要导入的Excel文件')
        return
      }
      this.$refs.deviceUpload.submit()
    },
    beforeImport(file) {
      if (!file.name.toLowerCase().endsWith('.xlsx')) {
        this.$message.error('仅支持.xlsx格式文件')
        return false
      }
      if (file.size > 10 * 1024 * 1024) {
        this.$message.error('导入文件不能超过10MB')
        return false
      }
      this.importing = true
      return true
    },
    importSuccess(response) {
      this.importing = false
      if (response.code !== 200) {
        this.$message.error(response.message || response.msg || '导入失败')
        return
      }
      this.$message.success(`成功导入${response.data}条设备`)
      this.importVisible = false
      this.refresh()
      this.$emit('refresh-tree')
    },
    importError() {
      this.importing = false
      this.$message.error('导入失败')
    },
    resetImport() {
      this.importing = false
      this.importFiles = []
      if (this.$refs.deviceUpload) {
        this.$refs.deviceUpload.clearFiles()
      }
    },
    refreshTable() {
      const key = `_${this.currentPage}`
      const value = this.keyMap[key]
src/views/CNAS/resourceDemand/device/index.vue
@@ -38,7 +38,7 @@
      <div v-if="isShowAll" style="height: 100%;">
        <el-tabs v-model="menuListActiveName" class="main_right" type="border-card">
          <el-tab-pane label="设备总览" name="设备总览">
            <overview v-if="menuListActiveName == '设备总览'" :clickNodeVal="clickNodeVal" />
            <overview v-if="menuListActiveName == '设备总览'" :clickNodeVal="clickNodeVal" @refresh-tree="geList" />
          </el-tab-pane>
          <el-tab-pane label="设备工具明细" name="设备工具明细">
            <management v-if="menuListActiveName == '设备工具明细'" :clickNodeVal="clickNodeVal" />
src/views/CNAS/resourceDemand/standardMaterial/index.vue
@@ -19,6 +19,7 @@
      </div>
      <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>
@@ -29,6 +30,22 @@
    <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>
    <el-dialog title="标准物质导入" :visible.sync="importVisible" width="500px" @closed="resetImport">
      <div style="margin-bottom: 12px;">
        请使用标准物质导入模板填写数据,单个文件不超过10MB。
        <el-button type="text" @click="downloadTemplate">下载导入模板</el-button>
      </div>
      <el-upload ref="standardSubstanceUpload" drag name="file" accept=".xlsx" :action="importAction"
                 :headers="uploadHeader" :auto-upload="false" :limit="1" :file-list="importFiles"
                 :before-upload="beforeImport" :on-success="importSuccess" :on-error="importError">
        <i class="el-icon-upload"></i>
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
      </el-upload>
      <span slot="footer" class="dialog-footer">
        <el-button @click="importVisible = false">取消</el-button>
        <el-button type="primary" :loading="importing" @click="submitImport">导入</el-button>
      </span>
    </el-dialog>
  </div>
</template>
@@ -39,7 +56,8 @@
import {
  getPageStandardSubstance,
  removeStandardSubstance,
  exportOfStandardSubstanceList
  exportOfStandardSubstanceList,
  downloadStandardSubstanceImportTemplate
} from '@/api/cnas/resourceDemand/standardMaterial/standardMaterial'
import FormDia from '../standardMaterial/component/formDia.vue';
import BorrowDia from '../standardMaterial/component/borrowDia.vue';
@@ -173,12 +191,65 @@
      formDia: false,
      borrowDia: false,
      returnDia: false,
      importVisible: false,
      importing: false,
      importFiles: [],
    }
  },
  computed: {
    importAction() {
      return this.javaApi + '/feStandardSubstance/importStandardSubstance'
    }
  },
  mounted() {
    this.searchList()
  },
  methods: {
    downloadTemplate() {
      downloadStandardSubstanceImportTemplate().then(res => {
        this.$download.saveAs(new Blob([res]), '标准物质导入模板.xlsx')
      })
    },
    submitImport() {
      if (!this.$refs.standardSubstanceUpload.uploadFiles.length) {
        this.$message.warning('请选择需要导入的Excel文件')
        return
      }
      this.$refs.standardSubstanceUpload.submit()
    },
    beforeImport(file) {
      if (!file.name.toLowerCase().endsWith('.xlsx')) {
        this.$message.error('仅支持.xlsx格式文件')
        return false
      }
      if (file.size > 10 * 1024 * 1024) {
        this.$message.error('导入文件不能超过10MB')
        return false
      }
      this.importing = true
      return true
    },
    importSuccess(response) {
      this.importing = false
      if (response.code !== 200) {
        this.$message.error(response.message || response.msg || '导入失败')
        return
      }
      this.$message.success(`成功导入${response.data}条标准物质`)
      this.importVisible = false
      this.searchList()
    },
    importError() {
      this.importing = false
      this.$message.error('导入失败')
    },
    resetImport() {
      this.importing = false
      this.importFiles = []
      if (this.$refs.standardSubstanceUpload) {
        this.$refs.standardSubstanceUpload.clearFiles()
      }
    },
    exportFun() {
      this.outLoading = true
      exportOfStandardSubstanceList(this.searchForm).then(res => {
src/views/business/productOrder/components/addOrder.vue
@@ -300,19 +300,27 @@
            </template>
          </el-table-column>
        </el-table>
        <div v-if="canSelectProduct" style="display: flex; align-items: center; gap: 12px; padding: 10px 0;">
          <span>检测范围:</span>
          <el-radio-group v-model="inspectionScope" size="small" @change="changeInspectionScope">
            <el-radio-button label="all">全部检验</el-radio-button>
            <el-radio-button label="specified">指定检验项</el-radio-button>
          </el-radio-group>
          <span>已选 {{ selectedProductCount }} / 共 {{ productList.length }} 项</span>
        </div>
        <el-table ref="productTable" v-loading="getProductLoad" :data="productList"
                  :row-class-name="tableRowClassName" border class="el-table"
                  :header-cell-style="{ background: '#f8f8f9', color: '#515a6e' }"
          max-height="400px" style="margin-bottom: 10px;" tooltip-effect="dark"
          @select="selectOne" @selection-change="selectProduct" @select-all="handleAll">
          <el-table-column v-if="active==1" :selectable="selectable0" type="selection" width="65"></el-table-column>
          <el-table-column v-if="canSelectProduct" :selectable="selectable0" type="selection" width="65"></el-table-column>
          <el-table-column label="检验项分类" min-width="140" prop="inspectionItemClass" show-overflow-tooltip></el-table-column>
          <el-table-column label="检验项" min-width="140" prop="inspectionItem" show-overflow-tooltip>
            <template slot="header" slot-scope="scope">
              <div style="display: flex;align-items: center;flex-direction: column;font-size: 14px">
                <span>检验项</span>
                <el-input
                  v-if="active==1"
                  v-if="canSelectProduct"
                v-model="inspectionItem"
                placeholder="请输入"
                size="mini"
@@ -326,7 +334,7 @@
              <div style="display: flex;align-items: center;flex-direction: column;font-size: 14px">
                <span>检验项子项</span>
                <el-input
                  v-if="active==1"
                  v-if="canSelectProduct"
                  v-model="inspectionItemSubclass"
                  placeholder="请输入"
                  size="mini"
@@ -368,7 +376,7 @@
              <div style="display: flex;align-items: center;flex-direction: column;font-size: 14px">
                <span>试验方法</span>
                <el-input
                  v-if="active==1"
                  v-if="canSelectProduct"
                  v-model="methodS"
                  placeholder="请输入"
                  size="mini"
@@ -614,6 +622,12 @@
  },
  computed:{
    ...mapGetters(["nickName", 'nameEn', 'userName']),
    canSelectProduct() {
      return this.active == 1 || (this.tabIndex == 4 && this.active == 2)
    },
    selectedProductCount() {
      return this.productList.filter(item => item.state == 1).length
    },
    newJuLaboratoryList() {
      return (this.dict.type.sys_sub_lab || []).filter(item => item.label === '新聚')
    },
@@ -691,9 +705,6 @@
        ],
        testQuantity: [
          { required: true, message: '请填写抽检数量', trigger: 'blur' }
        ],
        production: [
          { required: true, message: '请填写生产单位', trigger: 'blur' }
        ],
      },
      sample: {
@@ -796,6 +807,7 @@
      quarterItemOptions: [], // 查询季度信息
      specialStandardMethod: '',
      isSpecial: false,
      inspectionScope: 'all',
    }
  },
  watch: {
@@ -906,7 +918,12 @@
            ...res.data.insOrder
          };
          this.addObj.type = String(this.addObj.type)
          this.sampleList = this.HaveJson(res.data.sampleProduct);
          const samples = this.HaveJson(res.data.sampleProduct)
          const loadSamples = this.canSelectProduct
            ? Promise.all(samples.map(sample => this.loadSelectableProducts(sample)))
            : Promise.resolve(samples)
          loadSamples.then(sampleList => {
            this.sampleList = sampleList
          this.specialStandardMethod = this.sampleList[0].specialStandardMethod
          this.getProNum()
          this.addObj.sampleNum = this.sampleList.length
@@ -916,6 +933,7 @@
              this.$refs.sampleTable.setCurrentRow(this.sampleList[0], true)
              this.rowClick(this.sampleList[0])
            }
            })
          })
        })
      }
@@ -1166,22 +1184,23 @@
                  if (this.tabIndex != 4) {
                    delete c.id
                  }
                  if (c.isNew) {
                    c.id = null
                    delete c.isNew
                  }
                })
              }
              if (a.endModels) {
                a.model = a.endModels
              }
              if (!this.canSelectProduct || this.active == 1) {
              a.insProduct = a.insProduct.filter(b=>b.state === 1)
              }
            })
            let projectNum = this.totalArr.filter(a => a.state == 1).length
            if(projectNum==0){
              this.$confirm('检验项目为空,是否确认提交?', "提示", {
                confirmButtonText: "确定",
                cancelButtonText: "取消",
                type: "warning"
              }).then(() => {
                this.saveMethod(sampleList)
              }).catch(() => {})
              this.$message.warning('请至少选择一个检验项')
              return
            }else{
              let isRTS = this.totalArr.find(a => a.ask != null && this.symbolList.find(b=>a.ask.includes(b)) && a.state == 1)
              if (isRTS) {
@@ -1206,12 +1225,18 @@
                if (this.tabIndex != 4) {
                  delete c.id
                }
                if (c.isNew) {
                  c.id = null
                  delete c.isNew
                }
              })
            }
            if (a.endModels) {
              a.model = a.endModels
            }
            if (!this.canSelectProduct || this.active == 1) {
            a.insProduct = a.insProduct.filter(b=>b.state === 1)
            }
          })
          this.saveMethod(sampleList)
        }else{
@@ -1625,6 +1650,9 @@
        }
        this.productList = row.insProduct
        this.productList0 = JSON.parse(JSON.stringify(this.productList))
        this.inspectionScope = this.productList.length === 0
          ? (this.active == 1 ? 'all' : 'specified')
          : (this.productList.every(item => item.state == 1) ? 'all' : 'specified')
        setTimeout(() => {
          this.productList.forEach(a => {
            if (a.state == 1) {
@@ -1694,6 +1722,9 @@
        })
        this.changeProductList0()
        this.currentMethod.insProduct = this.productList0
        this.inspectionScope = this.productList.length > 0 && this.productList.every(item => item.state == 1)
          ? 'all'
          : 'specified'
        this.getProNum()
      },
      permute(nums) {
@@ -1814,7 +1845,7 @@
          conductorType: row.conductorType,
        }).then(res => {
          res.data.forEach(a => {
            a.state = 0
            a.state = this.inspectionScope === 'all' ? 1 : 0
          })
          row.insProduct = this.HaveJson(res.data)
          this.getProductLoad = false
@@ -1829,6 +1860,9 @@
            this.productList.forEach(a => {
              if (a.state == 1) this.toggleSelection(a)
            })
            if (this.inspectionScope === 'all') {
              this.handleAll(this.productList.filter(item => this.selectable0(item)))
            }
          }, 200)
        })
      },
@@ -1876,7 +1910,7 @@
          factory: selectTreeList.join(" - "),
        }).then(res => {
          res.data.forEach(a => {
            a.state = 0
            a.state = this.inspectionScope === 'all' ? 1 : 0
          })
          row.insProduct = this.HaveJson(res.data)
          this.getProductLoad = false
@@ -1887,6 +1921,9 @@
            this.productList.forEach(a => {
              if (a.state == 1) this.toggleSelection(a)
            })
            if (this.inspectionScope === 'all') {
              this.handleAll(this.productList.filter(item => this.selectable0(item)))
            }
          }, 200)
        })
      },
@@ -1955,7 +1992,7 @@
        }
      },
      selectable0(row,index) {
        if (this.active > 1||row.repetitionTag) {
        if (!this.canSelectProduct||row.repetitionTag) {
          return false
        } else {
          return true
@@ -2026,18 +2063,58 @@
            return item
          })
        }
        this.productList.forEach(item => {
          if (item.id == row.id) {
            item.state = row.state;
          }
        })
        this.changeProductList0()
        this.currentMethod.insProduct = this.productList0
        this.inspectionScope = e.length > 0 ? 'all' : 'specified'
        this.getProNum()
        this.$nextTick(() => {
          this.$refs.productTable.doLayout()
        })
      },
      changeInspectionScope(scope) {
        if (scope !== 'all' || this.productList.length === 0) return
        this.$refs.productTable.clearSelection()
        this.productList.forEach(item => {
          if (this.selectable0(item)) this.$refs.productTable.toggleRowSelection(item, true)
        })
        this.handleAll(this.productList.filter(item => this.selectable0(item)))
      },
      inspectionProductKey(product) {
        return [
          product.structureItemParameterId,
          product.inspectionItemClass,
          product.inspectionItem,
          product.inspectionItemSubclass
        ].join('|')
      },
      loadSelectableProducts(sample) {
        const selectedProducts = sample.insProduct || []
        if (!sample.standardMethodListId) return Promise.resolve(sample)
        const product = selectedProducts[0] || {}
        const factory = [
          product.factory || this.addObj.factory,
          product.laboratory || this.addObj.laboratory,
          product.sampleType || this.addObj.sampleType,
          product.sample || sample.sample,
          product.model || sample.model
        ].join(' - ')
        return selectStandardProductList({
          model: sample.model,
          modelNum: sample.modelNum,
          standardMethodListId: sample.standardMethodListId,
          factory,
          cores: sample.cores,
          conductorMaterial: sample.conductorMaterial,
          conductorType: sample.conductorType
        }).then(res => {
          const selectedKeys = new Set(selectedProducts.map(item => this.inspectionProductKey(item)))
          const availableProducts = (res.data || [])
            .filter(item => !selectedKeys.has(this.inspectionProductKey(item)))
            .map((item, index) => ({...item, id: `new-${sample.id}-${index}`, state: 0, isNew: true}))
          sample.insProduct = selectedProducts.concat(availableProducts)
          return sample
        }).catch(() => sample)
      },
      submitTell() {
        if (!this.tell) {
          this.$message.error('请输入不通过原因')
src/views/business/productOrder/components/addView.vue
@@ -686,9 +686,6 @@
        testQuantity: [
          { required: true, message: '请填写抽检数量', trigger: 'blur' }
        ],
        production: [
          { required: true, message: '请填写生产单位', trigger: 'blur' }
        ],
      },
      sample: {
        sampleCode: null,