| | |
| | | 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" |
| | |
| | | computed, |
| | | defineAsyncComponent, |
| | | defineComponent, |
| | | nextTick, |
| | | onBeforeUnmount, |
| | | onMounted, |
| | | reactive, |
| | | ref, |
| | | watch, |
| | | } from "vue"; |
| | | import { |
| | | queryList, |
| | |
| | | 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 emit = defineEmits(["update:router"]); |
| | | const form = ref(); |
| | | const tableRef = ref(); |
| | | let tableSortable = null; |
| | | |
| | | const route = useRoute(); |
| | | const router = useRouter(); |
| | |
| | | |
| | | 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) => { |
| | |
| | | // 先加载工序选项,再加载数据,确保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> |