| | |
| | | border |
| | | :preserve-expanded-content="false" |
| | | :default-expand-all="true" |
| | | style="width: 100%"> |
| | | style="width: 100%" |
| | | @expand-change="handleOuterExpandChange"> |
| | | <el-table-column type="expand"> |
| | | <template #default="props"> |
| | | <el-form ref="form" |
| | | :model="dataValue"> |
| | | <el-table :data="dataValue.dataList" |
| | | <div v-if="dataValue.isEdit" |
| | | class="drag-tip"> |
| | | <el-icon><Rank /></el-icon> |
| | | <span>æå¨ææè°æ´ç»æ:åå±çº§ç´æ¥æå¨æåº;æå°æè¡æ£ä¸æ¹æä¸ºå
¶å级;æå°å表æåæä¸ºä¸çº§</span> |
| | | </div> |
| | | <el-table ref="tableRef" |
| | | :data="dataValue.dataList" |
| | | row-key="tempId" |
| | | default-expand-all |
| | | :tree-props="{children: 'children', hasChildren: 'hasChildren'}" |
| | | @expand-change="handleTreeExpandChange" |
| | | style="width: 100%"> |
| | | <el-table-column v-if="dataValue.isEdit" |
| | | width="44" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | <div class="drag-handle" |
| | | title="æå¨è°æ´é¡ºåº:æå°æè¡æ£ä¸æ¹æä¸ºå
¶å级,æå°å表æåæä¸ºä¸çº§"> |
| | | <el-icon><Rank /></el-icon> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="productName" |
| | | label="产å" /> |
| | | <el-table-column prop="model" |
| | |
| | | <el-form-item v-if="dataValue.isEdit" |
| | | :rules="dataValue.dataList.some(item => (item as any).tempId === row.tempId) ? [] : [{ required: true, message: 'è¯·éæ©æ¶èå·¥åº', trigger: 'change' }]" |
| | | style="margin: 0"> |
| | | <el-select v-model="row.processId" |
| | | placeholder="è¯·éæ©" |
| | | filterable |
| | | clearable |
| | | style="width: 100%" |
| | | @change="value => handleProcessChange(row, value)" |
| | | :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)"> |
| | | <el-option v-for="item in dataValue.processOptions" |
| | | :key="item.id" |
| | | :label="item.name" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | <el-input v-model="row.processName" |
| | | readonly |
| | | placeholder="è¯·éæ©æ¶èå·¥åº" |
| | | clearable |
| | | style="width: 100%" |
| | | :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" |
| | | @click="openProcessDialog(row, $event)" |
| | | @clear="clearProcess(row)"> |
| | | <template #append> |
| | | <el-button icon="Search" |
| | | :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" |
| | | @click="openProcessDialog(row)" /> |
| | | </template> |
| | | </el-input> |
| | | </el-form-item> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | v-model:model-value="dataValue.showProductDialog" |
| | | :single="true" |
| | | @confirm="handleProduct" /> |
| | | <process-select-dialog v-if="dataValue.showProcessDialog" |
| | | v-model:model-value="dataValue.showProcessDialog" |
| | | @confirm="handleProcessConfirm" /> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | computed, |
| | | defineAsyncComponent, |
| | | defineComponent, |
| | | nextTick, |
| | | onBeforeUnmount, |
| | | onMounted, |
| | | reactive, |
| | | ref, |
| | | watch, |
| | | } from "vue"; |
| | | import { |
| | | queryList, |
| | | addBomDetail, |
| | | } from "@/api/productionManagement/productStructure.js"; |
| | | import { listProcessBom } from "@/api/productionManagement/productionOrder.js"; |
| | | import { list } from "@/api/productionManagement/productionProcess"; |
| | | import { listAll } from "@/api/productionManagement/productionProcess"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { useRoute, useRouter } from "vue-router"; |
| | | import Sortable from "sortablejs"; |
| | | import { Rank } from "@element-plus/icons-vue"; |
| | | |
| | | defineComponent({ |
| | | name: "StructureEdit", |
| | |
| | | const ProductSelectDialog = defineAsyncComponent( |
| | | () => import("@/views/basicData/product/ProductSelectDialog.vue") |
| | | ); |
| | | const ProcessSelectDialog = defineAsyncComponent( |
| | | () => |
| | | import( |
| | | "@/views/productionManagement/productionProcess/ProcessSelectDialog.vue" |
| | | ) |
| | | ); |
| | | const emit = defineEmits(["update:router"]); |
| | | const form = ref(); |
| | | const tableRef = ref(); |
| | | let tableSortable = null; |
| | | |
| | | const route = useRoute(); |
| | | const router = useRouter(); |
| | |
| | | productOptions: [], |
| | | processOptions: [], |
| | | showProductDialog: false, |
| | | showProcessDialog: false, |
| | | currentRowIndex: null, |
| | | currentRowName: null, |
| | | currentProcessRow: null, |
| | | loading: false, |
| | | isEdit: false, |
| | | }); |
| | |
| | | syncDemandedQuantityTree(dataValue.dataList); |
| | | }; |
| | | |
| | | // ===== ææ½æåº ===== |
| | | // æå½å DOM è¡é¡ºåºå±å¼ææå¹³å表(ç¼è¾æ¨¡å¼ä¸å¼ºå¶å
¨é¨å±å¼,ä¿è¯è¡é¡ºåºä¸æ°æ®ä¸è´) |
| | | const buildFlatTree = () => { |
| | | const flat = []; |
| | | const walk = (items, depth, parentNode) => { |
| | | items.forEach(node => { |
| | | flat.push({ node, depth, parentArray: items, parentNode }); |
| | | if (Array.isArray(node.children) && node.children.length > 0) { |
| | | walk(node.children, depth + 1, node); |
| | | } |
| | | }); |
| | | }; |
| | | walk(dataValue.dataList, 0, null); |
| | | return flat; |
| | | }; |
| | | |
| | | const removeNodeByTempId = (items, tempId) => { |
| | | for (let i = 0; i < items.length; i++) { |
| | | if (String(items[i].tempId) === String(tempId)) { |
| | | items.splice(i, 1); |
| | | return true; |
| | | } |
| | | if ( |
| | | Array.isArray(items[i].children) && |
| | | removeNodeByTempId(items[i].children, tempId) |
| | | ) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | }; |
| | | |
| | | const updateParentRef = (node, parent) => { |
| | | node.parentTempId = parent?.tempId ?? ""; |
| | | node.parentId = parent?.id ?? ""; |
| | | }; |
| | | |
| | | const handleSortEnd = evt => { |
| | | const { oldIndex, newIndex } = evt; |
| | | if ( |
| | | typeof oldIndex !== "number" || |
| | | typeof newIndex !== "number" || |
| | | oldIndex === newIndex |
| | | ) { |
| | | return; |
| | | } |
| | | const flat = buildFlatTree(); |
| | | if (oldIndex < 0 || oldIndex >= flat.length) return; |
| | | const dragged = flat[oldIndex]; |
| | | |
| | | // ææ¾åä½äºè¢«æè¡ä¸æ¹ä¸è¡çæ°æ®(ææç» DOM é¡ºåºæ¢ç®å°åæå¹³å表) |
| | | let aEntry = null; |
| | | if (newIndex > oldIndex) { |
| | | aEntry = flat[newIndex]; |
| | | } else if (newIndex > 0) { |
| | | aEntry = flat[newIndex - 1]; |
| | | } |
| | | |
| | | if (!aEntry) { |
| | | // æå°å表æå â æä¸ºä¸çº§ |
| | | removeNodeByTempId(dataValue.dataList, dragged.node.tempId); |
| | | dataValue.dataList.unshift(dragged.node); |
| | | updateParentRef(dragged.node, null); |
| | | } else if (aEntry.depth < dragged.depth) { |
| | | // æå°æè¡æ£ä¸æ¹ â æä¸ºè¯¥è¡çå级 |
| | | removeNodeByTempId(dataValue.dataList, dragged.node.tempId); |
| | | if (!aEntry.node.children) { |
| | | aEntry.node.children = []; |
| | | } |
| | | aEntry.node.children.push(dragged.node); |
| | | updateParentRef(dragged.node, aEntry.node); |
| | | } else { |
| | | // å级æå
¥:å䏿¾ç¬¬ä¸ä¸ªæ·±åº¦ä¸è¶
è¿è¢«æè¡çèç¹ |
| | | let i = flat.indexOf(aEntry); |
| | | while (i >= 0 && flat[i].depth > dragged.depth) { |
| | | i--; |
| | | } |
| | | const eEntry = flat[i]; |
| | | // æåèªå·±çåæ å
â ä¸å¤ç(鲿¢å½¢æå¾ªç¯) |
| | | if (!eEntry || eEntry.node.tempId === dragged.node.tempId) { |
| | | return; |
| | | } |
| | | removeNodeByTempId(dataValue.dataList, dragged.node.tempId); |
| | | const siblings = eEntry.parentArray; |
| | | const idx = siblings.indexOf(eEntry.node); |
| | | siblings.splice(idx + 1, 0, dragged.node); |
| | | updateParentRef(dragged.node, eEntry.parentNode); |
| | | } |
| | | |
| | | recalculateDemandedQuantities(); |
| | | }; |
| | | |
| | | const initSortable = () => { |
| | | if (!tableRef.value) return; |
| | | const tbody = tableRef.value.$el.querySelector(".el-table__body tbody"); |
| | | if (!tbody) return; |
| | | tableSortable?.destroy(); |
| | | tableSortable = new Sortable(tbody, { |
| | | animation: 150, |
| | | handle: ".drag-handle", |
| | | disabled: !dataValue.isEdit, |
| | | ghostClass: "sortable-ghost", |
| | | onEnd: handleSortEnd, |
| | | }); |
| | | }; |
| | | |
| | | const expandAllTree = () => { |
| | | const table = tableRef.value; |
| | | if (!table) return; |
| | | const walk = items => { |
| | | items.forEach(item => { |
| | | if (Array.isArray(item.children) && item.children.length > 0) { |
| | | table.toggleRowExpansion(item, true); |
| | | walk(item.children); |
| | | } |
| | | }); |
| | | }; |
| | | walk(dataValue.dataList); |
| | | }; |
| | | |
| | | // ç¼è¾æ¨¡å¼ä¸ä¸å
许æå ,ä¿è¯ææ½æ¶ DOM è¡é¡ºåºä¸ dataList ä¸è´ |
| | | const handleTreeExpandChange = (row, expanded) => { |
| | | if (dataValue.isEdit && expanded === false) { |
| | | nextTick(() => { |
| | | tableRef.value?.toggleRowExpansion(row, true); |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // å¤å±è¡æå åå
容ä¼éæ¯,éæ°å±å¼æ¶éæ°åå§åææ½ |
| | | const handleOuterExpandChange = (row, expandedRows) => { |
| | | if (Array.isArray(expandedRows) && expandedRows.some(r => r === row)) { |
| | | nextTick(() => initSortable()); |
| | | } |
| | | }; |
| | | |
| | | watch( |
| | | () => dataValue.isEdit, |
| | | val => { |
| | | tableSortable?.option("disabled", !val); |
| | | if (val) { |
| | | nextTick(() => { |
| | | initSortable(); |
| | | expandAllTree(); |
| | | }); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | const buildSubmitTree = (items: any[]) => { |
| | | return items.map((item: any) => { |
| | | const current = { ...item }; |
| | |
| | | } |
| | | }; |
| | | |
| | | const openProcessDialog = (row: any, event?: Event) => { |
| | | // ç¹å»æ¸
餿鮿¶ä¸å¼¹çª |
| | | const target = event?.target as HTMLElement | undefined; |
| | | if (target && target.closest(".el-input__clear")) { |
| | | return; |
| | | } |
| | | dataValue.currentProcessRow = row; |
| | | dataValue.showProcessDialog = true; |
| | | }; |
| | | |
| | | const clearProcess = (row: any) => { |
| | | row.processId = ""; |
| | | row.operationId = ""; |
| | | row.processName = ""; |
| | | row.operationName = ""; |
| | | }; |
| | | |
| | | const handleProcessConfirm = (processRow: any) => { |
| | | const row = dataValue.currentProcessRow; |
| | | if (!row) { |
| | | return; |
| | | } |
| | | row.processId = processRow.id; |
| | | row.operationId = processRow.id; |
| | | row.processName = processRow.name; |
| | | row.operationName = processRow.name; |
| | | handleProcessChange(row, processRow.id); |
| | | }; |
| | | |
| | | const handleUnitQuantityChange = () => { |
| | | recalculateDemandedQuantities(); |
| | | }; |
| | |
| | | }; |
| | | |
| | | const fetchProcessOptions = async () => { |
| | | const { data } = await list({}); |
| | | const { data } = await listAll(); |
| | | console.log(data, "dataValue.dataList"); |
| | | dataValue.processOptions = normalizeListData(data); |
| | | }; |
| | |
| | | // å
å 载工åºé项ï¼åå è½½æ°æ®ï¼ç¡®ä¿el-selectè½å¤æ£ç¡®åæ¾ |
| | | await fetchProcessOptions(); |
| | | await fetchData(); |
| | | await nextTick(); |
| | | initSortable(); |
| | | }); |
| | | </script> |
| | | |
| | | onBeforeUnmount(() => { |
| | | tableSortable?.destroy(); |
| | | tableSortable = null; |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .drag-tip { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | margin-bottom: 8px; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .drag-handle { |
| | | cursor: move; |
| | | color: #909399; |
| | | text-align: center; |
| | | } |
| | | |
| | | .drag-handle:hover { |
| | | color: var(--el-color-primary); |
| | | } |
| | | |
| | | :deep(.sortable-ghost) { |
| | | opacity: 0.6; |
| | | background-color: #f5f7fa !important; |
| | | } |
| | | </style> |