| | |
| | | <template> |
| | | <div class="app-container metric-binding"> |
| | | <!-- 左侧:检测标准列表(只读) --> |
| | | <div class="left-panel"> |
| | | <el-row :gutter="16" class="metric-binding-row"> |
| | | <!-- 左侧:检测标准列表 --> |
| | | <el-col :xs="24" :sm="24" :md="12" :lg="14" :xl="14" class="left-col"> |
| | | <div class="panel left-panel"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="standardColumns" |
| | |
| | | :isSelection="false" |
| | | :rowClassName="rowClassNameCenter" |
| | | :tableLoading="tableLoading" |
| | | :rowClick="handleTableRowClick" |
| | | @pagination="handlePagination" |
| | | :total="page.total" |
| | | > |
| | |
| | | </el-select> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | |
| | | <!-- 右侧:绑定列表 --> |
| | | <div class="right-panel"> |
| | | <!-- 右侧:绑定列表 --> |
| | | <el-col :xs="24" :sm="24" :md="12" :lg="10" :xl="10" class="right-col"> |
| | | <div class="panel right-panel"> |
| | | <div class="right-header"> |
| | | <div class="title">绑定关系</div> |
| | | <div class="desc" v-if="currentStandard"> |
| | |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 添加绑定弹框 --> |
| | | <el-dialog |
| | | v-model="bindingDialogVisible" |
| | | title="添加绑定" |
| | | width="520px" |
| | | width="640px" |
| | | @close="closeBindingDialog" |
| | | > |
| | | <el-form label-width="100px"> |
| | | <el-form-item label="产品"> |
| | | <el-tree-select |
| | | v-model="selectedProductIds" |
| | | multiple |
| | | collapse-tags |
| | | collapse-tags-tooltip |
| | | placeholder="请选择产品(可多选)" |
| | | clearable |
| | | check-strictly |
| | | :data="productOptions" |
| | | :render-after-expand="false" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="binding-dialog"> |
| | | <el-input |
| | | v-model="productSearchKeyword" |
| | | placeholder="搜索产品" |
| | | clearable |
| | | prefix-icon="Search" |
| | | class="binding-search" |
| | | /> |
| | | <el-tree |
| | | ref="productTreeRef" |
| | | :key="productTreeKey" |
| | | v-loading="productTreeLoading" |
| | | :data="productTreeData" |
| | | show-checkbox |
| | | check-strictly |
| | | node-key="id" |
| | | :props="{ label: 'label', children: 'children', disabled: 'disabled' }" |
| | | :filter-node-method="filterProductNode" |
| | | class="product-binding-tree" |
| | | @check="handleProductTreeCheck" |
| | | /> |
| | | <div v-if="selectedProductLabels.length" class="selected-products"> |
| | | <div class="selected-title">已选择({{ selectedProductLabels.length }})</div> |
| | | <div class="selected-tags"> |
| | | <el-tag |
| | | v-for="item in selectedProductLabels" |
| | | :key="item.id" |
| | | closable |
| | | type="info" |
| | | @close="removeSelectedProduct(item.id)" |
| | | > |
| | | {{ item.label }} |
| | | </el-tag> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="closeBindingDialog">取消</el-button> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search } from '@element-plus/icons-vue' |
| | | import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue' |
| | | import { ref, reactive, toRefs, computed, watch, nextTick, onMounted, getCurrentInstance } from 'vue' |
| | | import { ElMessageBox } from 'element-plus' |
| | | import PIMTable from '@/components/PIMTable/PIMTable.vue' |
| | | import { productTreeList } from '@/api/basicData/product.js' |
| | | import { |
| | | qualityTestStandardListPage |
| | | } from '@/api/qualityManagement/metricMaintenance.js' |
| | | import { productProcessListPage } from '@/api/basicData/productProcess.js' |
| | | import { |
| | | qualityTestStandardBindingList, |
| | | qualityTestStandardBindingAdd, |
| | |
| | | const tableLoading = ref(false) |
| | | const page = reactive({ current: 1, size: 10, total: 0 }) |
| | | |
| | | // 工序下拉(用于列表回显) |
| | | const processOptions = ref([]) |
| | | |
| | | const getProcessList = async () => { |
| | | try { |
| | | const res = await productProcessListPage({ current: 1, size: 1000 }) |
| | | if (res?.code === 200) { |
| | | const records = res?.data?.records || [] |
| | | processOptions.value = records.map((item) => ({ |
| | | label: item.processName || item.name || item.label, |
| | | value: item.id || item.processId || item.value |
| | | })) |
| | | } |
| | | } catch (error) { |
| | | console.error('获取工序列表失败:', error) |
| | | } |
| | | } |
| | | |
| | | const standardColumns = ref([ |
| | | { label: '标准编号', prop: 'standardNo', dataType: 'slot', slot: 'standardNoCell', minWidth: 160, headerSlot: 'standardNoHeader' }, |
| | | { label: '标准名称', prop: 'standardName', minWidth: 180, headerSlot: 'standardNameHeader' }, |
| | | { label: '标准编号', prop: 'standardNo', dataType: 'slot', slot: 'standardNoCell', minWidth: 160, align: 'center', headerSlot: 'standardNoHeader' }, |
| | | { label: '标准名称', prop: 'standardName', minWidth: 180, align: 'center', headerSlot: 'standardNameHeader' }, |
| | | { |
| | | label: '检测类型', |
| | | label: '类别', |
| | | prop: 'inspectType', |
| | | headerSlot: 'inspectTypeHeader', |
| | | align: 'center', |
| | | dataType: 'tag', |
| | | formatData: (val) => { |
| | | const map = { 0: '原材料检验', 1: '过程检验', 2: '出厂检验' } |
| | | return map[val] || val |
| | | } |
| | | }, |
| | | { |
| | | label: '工序', |
| | | prop: 'processId', |
| | | align: 'center', |
| | | dataType: 'tag', |
| | | formatData: (val) => { |
| | | const target = processOptions.value.find( |
| | | (item) => String(item.value) === String(val) |
| | | ) |
| | | return target?.label || val |
| | | } |
| | | }, |
| | | { |
| | | label: '备注', |
| | | prop: 'remark', |
| | | minWidth: 160, |
| | | align: 'center' |
| | | } |
| | | // { |
| | | // label: '状态', |
| | | // prop: 'state', |
| | |
| | | const bindingDialogVisible = ref(false) |
| | | |
| | | // 产品树(用于绑定选择) |
| | | const productOptions = ref([]) |
| | | const productTreeData = ref([]) |
| | | const productTreeLoading = ref(false) |
| | | const productTreeRef = ref(null) |
| | | const productTreeKey = ref(0) |
| | | const productSearchKeyword = ref('') |
| | | const selectedProductIds = ref([]) |
| | | |
| | | const getProductOptions = async () => { |
| | | // 避免重复请求 |
| | | if (productOptions.value?.length) return |
| | | const res = await productTreeList() |
| | | productOptions.value = convertIdToValue(Array.isArray(res) ? res : []) |
| | | const markParentNodesDisabled = (nodes) => { |
| | | return (nodes || []).map((node) => { |
| | | const children = node.children?.length ? markParentNodesDisabled(node.children) : [] |
| | | return { |
| | | ...node, |
| | | children, |
| | | disabled: children.length > 0 |
| | | } |
| | | }) |
| | | } |
| | | |
| | | function convertIdToValue(data) { |
| | | return (data || []).map((item) => { |
| | | const { id, children, ...rest } = item |
| | | const newItem = { |
| | | ...rest, |
| | | value: id |
| | | const buildProductLabelMap = (nodes, map = {}) => { |
| | | ;(nodes || []).forEach((node) => { |
| | | if (node.id != null) { |
| | | map[node.id] = node.label |
| | | } |
| | | if (children && children.length > 0) { |
| | | newItem.children = convertIdToValue(children) |
| | | if (node.children?.length) { |
| | | buildProductLabelMap(node.children, map) |
| | | } |
| | | return newItem |
| | | }) |
| | | return map |
| | | } |
| | | |
| | | const productLabelMap = computed(() => buildProductLabelMap(productTreeData.value)) |
| | | |
| | | const selectedProductLabels = computed(() => |
| | | selectedProductIds.value.map((id) => ({ |
| | | id, |
| | | label: productLabelMap.value[id] || id |
| | | })) |
| | | ) |
| | | |
| | | const filterProductNode = (value, data) => { |
| | | if (!value) return true |
| | | return String(data.label || '').includes(value) |
| | | } |
| | | |
| | | watch(productSearchKeyword, (val) => { |
| | | productTreeRef.value?.filter(val) |
| | | }) |
| | | |
| | | const getProductTreeData = async () => { |
| | | if (productTreeData.value?.length) return |
| | | productTreeLoading.value = true |
| | | try { |
| | | const res = await productTreeList() |
| | | productTreeData.value = markParentNodesDisabled(Array.isArray(res) ? res : []) |
| | | } catch (error) { |
| | | console.error('获取产品树失败:', error) |
| | | } finally { |
| | | productTreeLoading.value = false |
| | | } |
| | | } |
| | | |
| | | const handleProductTreeCheck = () => { |
| | | selectedProductIds.value = productTreeRef.value?.getCheckedKeys(true) || [] |
| | | } |
| | | |
| | | const removeSelectedProduct = (id) => { |
| | | selectedProductIds.value = selectedProductIds.value.filter((item) => item !== id) |
| | | productTreeRef.value?.setChecked(id, false, false) |
| | | } |
| | | |
| | | const handleQuery = () => { |
| | |
| | | }) |
| | | } |
| | | |
| | | // 表格行点击,加载右侧绑定列表 |
| | | const handleTableRowClick = (row) => { |
| | | currentStandard.value = row |
| | | loadBindingList() |
| | | } |
| | | |
| | | // 左侧行点击,加载右侧绑定列表(保留用于标准编号列的点击) |
| | | const handleStandardRowClick = (row) => { |
| | | currentStandard.value = row |
| | | loadBindingList() |
| | |
| | | bindingLoading.value = true |
| | | qualityTestStandardBindingList({ testStandardId: currentStandard.value.id }) |
| | | .then((res) => { |
| | | bindingTableData.value = res?.data || [] |
| | | const base = res?.data || [] |
| | | // 将当前标准的工序和备注带到绑定列表中展示 |
| | | bindingTableData.value = base.map((item) => ({ |
| | | ...item, |
| | | processId: currentStandard.value?.processId, |
| | | remark: currentStandard.value?.remark |
| | | })) |
| | | }) |
| | | .finally(() => { |
| | | bindingLoading.value = false |
| | |
| | | const openBindingDialog = () => { |
| | | if (!currentStandard.value?.id) return |
| | | selectedProductIds.value = [] |
| | | getProductOptions() |
| | | productSearchKeyword.value = '' |
| | | productTreeKey.value += 1 |
| | | bindingDialogVisible.value = true |
| | | nextTick(() => { |
| | | productTreeRef.value?.setCheckedKeys([]) |
| | | productTreeRef.value?.filter('') |
| | | }) |
| | | } |
| | | |
| | | const closeBindingDialog = () => { |
| | | bindingDialogVisible.value = false |
| | | selectedProductIds.value = [] |
| | | productSearchKeyword.value = '' |
| | | productTreeRef.value?.setCheckedKeys([]) |
| | | } |
| | | |
| | | const submitBinding = async () => { |
| | |
| | | } |
| | | |
| | | const handleUnbind = async (row) => { |
| | | if (!row?.id) return |
| | | const id = row?.id ?? row?.qualityTestStandardBindingId |
| | | if (id == null || id === '') return |
| | | try { |
| | | await ElMessageBox.confirm('确认删除该绑定?', '提示', { type: 'warning' }) |
| | | } catch { |
| | | return |
| | | } |
| | | await qualityTestStandardBindingDel([row.qualityTestStandardBindingId]) |
| | | proxy.$message.success('删除成功') |
| | | loadBindingList() |
| | | try { |
| | | await qualityTestStandardBindingDel([id]) |
| | | proxy.$message.success('删除成功') |
| | | loadBindingList() |
| | | } catch (err) { |
| | | console.error('删除绑定失败:', err) |
| | | proxy.$message?.error(err?.message || '删除失败') |
| | | } |
| | | } |
| | | |
| | | const handleBatchUnbind = async () => { |
| | |
| | | proxy.$message.warning('请选择数据') |
| | | return |
| | | } |
| | | const ids = bindingSelectedRows.value.map((i) => i.qualityTestStandardBindingId) |
| | | const ids = bindingSelectedRows.value |
| | | .map((i) => i?.id ?? i?.qualityTestStandardBindingId) |
| | | .filter((id) => id != null && id !== '') |
| | | if (!ids.length) { |
| | | proxy.$message.warning('选中数据缺少有效 id') |
| | | return |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('选中的内容将被删除,是否确认删除?', '删除提示', { type: 'warning' }) |
| | | } catch { |
| | | return |
| | | } |
| | | await qualityTestStandardBindingDel(ids) |
| | | proxy.$message.success('删除成功') |
| | | loadBindingList() |
| | | try { |
| | | await qualityTestStandardBindingDel(ids) |
| | | proxy.$message.success('删除成功') |
| | | loadBindingList() |
| | | } catch (err) { |
| | | console.error('批量删除绑定失败:', err) |
| | | proxy.$message?.error(err?.message || '删除失败') |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getStandardList() |
| | | getProcessList() |
| | | getProductTreeData() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .metric-binding { |
| | | display: flex; |
| | | gap: 16px; |
| | | .metric-binding-row { |
| | | width: 100%; |
| | | } |
| | | |
| | | .metric-binding-row .left-col, |
| | | .metric-binding-row .right-col { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .metric-binding-row .panel { |
| | | background: #ffffff; |
| | | padding: 16px; |
| | | box-sizing: border-box; |
| | | height: 100%; |
| | | min-height: 400px; |
| | | } |
| | | |
| | | .left-panel, |
| | | .right-panel { |
| | | flex: 1; |
| | | background: #ffffff; |
| | | padding: 16px; |
| | | box-sizing: border-box; |
| | | height: 100%; |
| | | } |
| | | |
| | | .toolbar { |
| | |
| | | :deep(.center-table .el-table__body-wrapper td .cell) { |
| | | text-align: center !important; |
| | | } |
| | | |
| | | /* PIMTable 表头居中 */ |
| | | :deep(.lims-table .pim-table-header-cell) { |
| | | text-align: center; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | :deep(.lims-table .pim-table-header-title) { |
| | | text-align: center; |
| | | width: 100%; |
| | | } |
| | | |
| | | :deep(.lims-table .pim-table-header-extra) { |
| | | width: 100%; |
| | | margin-top: 4px; |
| | | } |
| | | |
| | | .binding-dialog { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .binding-search { |
| | | width: 100%; |
| | | } |
| | | |
| | | .product-binding-tree { |
| | | max-height: 360px; |
| | | overflow-y: auto; |
| | | border: 1px solid #ebeef5; |
| | | border-radius: 4px; |
| | | padding: 8px; |
| | | } |
| | | |
| | | .selected-products { |
| | | border-top: 1px solid #ebeef5; |
| | | padding-top: 12px; |
| | | } |
| | | |
| | | .selected-title { |
| | | font-size: 13px; |
| | | color: #606266; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .selected-tags { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | </style> |