From d78815179b7d49509e6637d547ca8bbd525f7fcf Mon Sep 17 00:00:00 2001
From: yuan <123@>
Date: 星期一, 07 九月 2026 11:45:41 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_pro_河南鹤壁' into dev_pro_河南鹤壁
---
src/views/productionManagement/productStructure/Detail/index.vue | 283 +++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 files changed, 266 insertions(+), 17 deletions(-)
diff --git a/src/views/productionManagement/productStructure/Detail/index.vue b/src/views/productionManagement/productStructure/Detail/index.vue
index ec18ddf..6899eb0 100644
--- a/src/views/productionManagement/productStructure/Detail/index.vue
+++ b/src/views/productionManagement/productStructure/Detail/index.vue
@@ -22,16 +22,34 @@
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>鎷栧姩鎵嬫焺璋冩暣缁撴瀯:鍚屽眰绾х洿鎺ユ嫋鍔ㄦ帓搴�;鎷栧埌鏌愯姝d笅鏂规垚涓哄叾瀛愮骇;鎷栧埌鍒楄〃鏈�鍓嶆垚涓轰竴绾�</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="鎷栧姩璋冩暣椤哄簭:鎷栧埌鏌愯姝d笅鏂规垚涓哄叾瀛愮骇,鎷栧埌鍒楄〃鏈�鍓嶆垚涓轰竴绾�">
+ <el-icon><Rank /></el-icon>
+ </div>
+ </template>
+ </el-table-column>
<el-table-column prop="productName"
label="浜у搧" />
<el-table-column prop="model"
@@ -59,18 +77,20 @@
<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>
@@ -150,6 +170,9 @@
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>
@@ -158,18 +181,23 @@
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",
@@ -178,8 +206,16 @@
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();
@@ -210,8 +246,10 @@
productOptions: [],
processOptions: [],
showProductDialog: false,
+ showProcessDialog: false,
currentRowIndex: null,
currentRowName: null,
+ currentProcessRow: null,
loading: false,
isEdit: false,
});
@@ -303,6 +341,155 @@
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) {
+ // 鎷栧埌鏌愯姝d笅鏂� 鈫� 鎴愪负璇ヨ鐨勫瓙绾�
+ 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 };
@@ -346,6 +533,35 @@
}
};
+ 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();
};
@@ -382,7 +598,7 @@
};
const fetchProcessOptions = async () => {
- const { data } = await list({});
+ const { data } = await listAll();
console.log(data, "dataValue.dataList");
dataValue.processOptions = normalizeListData(data);
};
@@ -693,5 +909,38 @@
// 鍏堝姞杞藉伐搴忛�夐」锛屽啀鍔犺浇鏁版嵁锛岀‘淇漞l-select鑳藉姝g‘鍥炴樉
await fetchProcessOptions();
await fetchData();
+ await nextTick();
+ initSortable();
});
-</script>
\ No newline at end of file
+
+ 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>
\ No newline at end of file
--
Gitblit v1.9.3