<template>
|
<div class="app-container">
|
<PageHeader content="产品结构详情">
|
<template #right-button>
|
<el-button v-if="!dataValue.isEdit && !isOrderPage"
|
type="primary"
|
@click="dataValue.isEdit = true">编辑
|
</el-button>
|
<el-button v-if="dataValue.isEdit && !isOrderPage"
|
type="primary"
|
@click="cancelEdit">取消
|
</el-button>
|
<el-button v-if="!isOrderPage"
|
type="primary"
|
:loading="dataValue.loading"
|
@click="submit"
|
:disabled="!dataValue.isEdit">确认
|
</el-button>
|
</template>
|
</PageHeader>
|
<el-table :data="tableData"
|
border
|
:preserve-expanded-content="false"
|
:default-expand-all="true"
|
style="width: 100%"
|
@expand-change="handleOuterExpandChange">
|
<el-table-column type="expand">
|
<template #default="props">
|
<el-form ref="form"
|
:model="dataValue">
|
<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"
|
label="规格">
|
<template #default="{ row, $index }">
|
<el-form-item v-if="dataValue.isEdit"
|
:rules="[{ required: true, message: '请选择规格', trigger: ['blur','change'] }]"
|
style="margin: 0">
|
<el-select v-model="row.model"
|
placeholder="请选择规格"
|
clearable
|
:disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
|
style="width: 100%"
|
@visible-change="(v) => { if (v) openDialog(row.tempId) }">
|
<el-option v-if="row.model"
|
:label="row.model"
|
:value="row.model" />
|
</el-select>
|
</el-form-item>
|
</template>
|
</el-table-column>
|
<el-table-column prop="processName"
|
label="消耗工序">
|
<template #default="{ row, $index }">
|
<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-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>
|
<el-table-column prop="unitQuantity"
|
label="单位产出所需数量">
|
<template #default="{ row, $index }">
|
<el-form-item v-if="dataValue.isEdit"
|
:rules="[{ required: true, message: '请输入单位产出所需数量', trigger: ['blur','change'] }]"
|
style="margin: 0">
|
<el-input-number v-model="row.unitQuantity"
|
:min="0"
|
:step="1"
|
controls-position="right"
|
style="width: 100%"
|
@change="handleUnitQuantityChange"
|
:disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
|
</el-form-item>
|
</template>
|
</el-table-column>
|
<el-table-column v-if="isOrderPage"
|
prop="demandedQuantity"
|
label="需求总量">
|
<template #default="{ row, $index }">
|
<el-form-item v-if="dataValue.isEdit"
|
:rules="[{ required: true, message: '请输入需求总量', trigger: ['blur','change'] }]"
|
style="margin: 0">
|
<el-input-number v-model="row.demandedQuantity"
|
:min="0"
|
:step="1"
|
controls-position="right"
|
style="width: 100%"
|
:disabled="true" />
|
</el-form-item>
|
</template>
|
</el-table-column>
|
<el-table-column prop="unit"
|
label="单位">
|
<template #default="{ row, $index }">
|
<el-form-item v-if="dataValue.isEdit"
|
:rules="[{ required: true, message: '请输入单位', trigger: ['blur','change'] }]"
|
style="margin: 0">
|
<el-input v-model="row.unit"
|
placeholder="请输入单位"
|
clearable
|
:disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
|
</el-form-item>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作"
|
fixed="right"
|
width="200">
|
<template #default="{ row, $index }">
|
<el-button v-if="dataValue.isEdit && !dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
|
type="danger"
|
text
|
@click="removeItem(row.tempId)">删除
|
</el-button>
|
<el-button v-if="dataValue.isEdit"
|
type="primary"
|
text
|
@click="addItem2(row.tempId)">添加
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-form>
|
</template>
|
</el-table-column>
|
<el-table-column label="BOM编号"
|
prop="bomNo" />
|
<el-table-column label="产品名称"
|
prop="productName" />
|
<el-table-column label="规格型号"
|
prop="model" />
|
</el-table>
|
<product-select-dialog v-if="dataValue.showProductDialog"
|
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>
|
|
<script setup lang="ts">
|
import {
|
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 { 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();
|
const routeId = computed({
|
get() {
|
return route.query.id;
|
},
|
|
set(val) {
|
emit("update:router", val);
|
},
|
});
|
|
// 从路由参数获取产品信息
|
const routeBomNo = computed(() => route.query.bomNo || "");
|
const routeProductName = computed(() => route.query.productName || "");
|
const routeProductModelName = computed(
|
() => route.query.productModelName || ""
|
);
|
const routeOrderId = computed(() => route.query.orderId);
|
const pageType = computed(() => route.query.type);
|
const isOrderPage = computed(
|
() => pageType.value === "order" && routeOrderId.value
|
);
|
|
const dataValue = reactive({
|
dataList: [],
|
productOptions: [],
|
processOptions: [],
|
showProductDialog: false,
|
showProcessDialog: false,
|
currentRowIndex: null,
|
currentRowName: null,
|
currentProcessRow: null,
|
loading: false,
|
isEdit: false,
|
});
|
|
const normalizeListData = (source: any) => {
|
if (Array.isArray(source)) {
|
return source;
|
}
|
if (Array.isArray(source?.records)) {
|
return source.records;
|
}
|
return [];
|
};
|
|
const getProcessOptionById = (id: any) => {
|
if (id === undefined || id === null || id === "") {
|
return null;
|
}
|
return (
|
normalizeListData(dataValue.processOptions).find(
|
option => String(option.id) === String(id)
|
) || null
|
);
|
};
|
|
const syncProcessOperationFields = (item: any) => {
|
const processId = item.processId ?? item.operationId ?? "";
|
if (!processId) {
|
item.processId = "";
|
item.operationId = "";
|
item.processName = "";
|
item.operationName = "";
|
return;
|
}
|
|
const option = getProcessOptionById(processId);
|
const processName =
|
option?.name || item.processName || item.operationName || "";
|
|
item.processId = processId;
|
item.operationId = processId;
|
item.processName = processName;
|
item.operationName = processName;
|
};
|
|
const normalizeTreeData = (items: any[]) => {
|
items.forEach((item: any) => {
|
item.tempId = item.tempId || item.id || `${Date.now()}_${Math.random()}`;
|
syncProcessOperationFields(item);
|
if (Array.isArray(item.children) && item.children.length > 0) {
|
normalizeTreeData(item.children);
|
}
|
});
|
};
|
|
const toQuantityNumber = (value: any) => {
|
const numberValue = Number(value);
|
if (!Number.isFinite(numberValue)) {
|
return 0;
|
}
|
return Number(numberValue.toFixed(4));
|
};
|
|
const syncDemandedQuantityTree = (
|
items: any[],
|
parentDemandedQuantity: number | null = null
|
) => {
|
items.forEach((item: any) => {
|
if (parentDemandedQuantity !== null) {
|
item.demandedQuantity = toQuantityNumber(
|
parentDemandedQuantity * toQuantityNumber(item.unitQuantity)
|
);
|
}
|
|
if (Array.isArray(item.children) && item.children.length > 0) {
|
syncDemandedQuantityTree(
|
item.children,
|
toQuantityNumber(item.demandedQuantity)
|
);
|
}
|
});
|
};
|
|
const recalculateDemandedQuantities = () => {
|
if (!isOrderPage.value) {
|
return;
|
}
|
|
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 };
|
syncProcessOperationFields(current);
|
current.children = Array.isArray(current.children)
|
? buildSubmitTree(current.children)
|
: [];
|
return current;
|
});
|
};
|
|
const findSiblings = (items: any[], tempId: string): any[] | null => {
|
if (!items || items.length === 0) return null;
|
// 检查当前层级
|
if (items.some(item => item.tempId === tempId)) {
|
return items;
|
}
|
// 递归查找子级
|
for (const item of items) {
|
if (item.children && item.children.length > 0) {
|
const result = findSiblings(item.children, tempId);
|
if (result) return result;
|
}
|
}
|
return null;
|
};
|
|
const handleProcessChange = (row: any, value: any) => {
|
row.processId = value || "";
|
syncProcessOperationFields(row);
|
|
// 检查同一层级是否已经有其他不同的工序被选中
|
const siblings = findSiblings(dataValue.dataList, row.tempId);
|
if (siblings && value) {
|
const hasDifferentProcess = siblings.some(sibling => {
|
return sibling.tempId !== row.tempId && sibling.processId && sibling.processId !== value;
|
});
|
if (hasDifferentProcess) {
|
ElMessage.warning("同一层级已存在不同的工序,请先统一工序后再进行修改");
|
}
|
}
|
};
|
|
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 tableData = reactive([
|
{
|
productName: "",
|
model: "",
|
bomNo: "",
|
},
|
]);
|
|
const openDialog = (tempId: any) => {
|
console.log(tempId, "tempId");
|
dataValue.currentRowName = tempId;
|
dataValue.showProductDialog = true;
|
};
|
|
const fetchData = async () => {
|
if (isOrderPage.value) {
|
// 订单情况:使用订单的产品结构接口
|
const { data } = await listProcessBom({ orderId: routeOrderId.value });
|
dataValue.dataList = (data as any) || [];
|
normalizeTreeData(dataValue.dataList);
|
recalculateDemandedQuantities();
|
} else {
|
// 非订单情况:使用原来的接口
|
const { data } = await queryList(routeId.value);
|
dataValue.dataList = (data as any) || [];
|
console.log(dataValue);
|
normalizeTreeData(dataValue.dataList);
|
console.log(dataValue.dataList, "dataValue.dataList");
|
}
|
};
|
|
const fetchProcessOptions = async () => {
|
const { data } = await listAll();
|
console.log(data, "dataValue.dataList");
|
dataValue.processOptions = normalizeListData(data);
|
};
|
|
const handleProduct = (row: any) => {
|
if (!Array.isArray(row) || row.length === 0) {
|
ElMessage.warning("请选择一个产品");
|
return;
|
}
|
// 只允许一个:如果上游返回了多个,默认使用最后一次选择并覆盖当前值
|
const productData = row[row.length - 1];
|
|
// 最外层组件中,与当前产品相同的产品只能有一个
|
const isTopLevel = dataValue.dataList.some(
|
item => (item as any).tempId === dataValue.currentRowName
|
);
|
if (isTopLevel) {
|
if (
|
productData.productName === tableData[0].productName &&
|
productData.model === tableData[0].model
|
) {
|
// 查找是否已经有其他顶层行已经是这个产品
|
const hasOther = dataValue.dataList.some(
|
item =>
|
(item as any).tempId !== dataValue.currentRowName &&
|
(item as any).productName === tableData[0].productName &&
|
(item as any).model === tableData[0].model
|
);
|
if (hasOther) {
|
ElMessage.warning("最外层和当前产品一样的一级只能有一个");
|
return;
|
}
|
}
|
}
|
// dataValue.dataList[dataValue.currentRowIndex].productName =
|
// row[0].productName;
|
// dataValue.dataList[dataValue.currentRowIndex].model = row[0].model;
|
// dataValue.dataList[dataValue.currentRowIndex].productModelId = row[0].id;
|
// dataValue.dataList[dataValue.currentRowIndex].unit = row[0].unit || "";
|
dataValue.dataList.map(item => {
|
if (item.tempId === dataValue.currentRowName) {
|
item.productName = productData.productName;
|
item.model = productData.model;
|
item.productModelId = productData.id;
|
item.unit = productData.unit || "";
|
return;
|
}
|
childItem(item, dataValue.currentRowName, productData);
|
});
|
dataValue.showProductDialog = false;
|
};
|
const childItem = (item: any, tempId: any, productData: any) => {
|
if (item.tempId === tempId) {
|
item.productName = productData.productName;
|
item.model = productData.model;
|
item.productModelId = productData.id;
|
item.unit = productData.unit || "";
|
return true;
|
}
|
if (item.children && item.children.length > 0) {
|
for (let child of item.children) {
|
if (childItem(child, tempId, productData)) {
|
return true;
|
}
|
}
|
}
|
return false;
|
};
|
|
// 递归校验所有层级的表单数据
|
const validateAll = () => {
|
let isValid = true;
|
|
// 校验一组兄弟节点的工序是否都相同
|
const checkProcessUniqueness = (items: any[]) => {
|
if (!items || items.length === 0 || !isValid) return;
|
|
// 获取第一个非空的工序ID作为参考
|
const firstProcessId = items.find(item => item.processId)?.processId;
|
|
// 如果有工序ID,检查所有项是否都使用相同的工序
|
if (firstProcessId) {
|
for (const item of items) {
|
if (item.processId && item.processId !== firstProcessId) {
|
const option1 = getProcessOptionById(firstProcessId);
|
const option2 = getProcessOptionById(item.processId);
|
const processName1 = option1?.name || "未知工序";
|
const processName2 = option2?.name || "未知工序";
|
ElMessage.error(
|
`当前层级下工序不一致,请使用相同的工序。存在「${processName1}」和「${processName2}」`
|
);
|
isValid = false;
|
return;
|
}
|
}
|
}
|
|
// 递归校验子级的兄弟节点
|
for (const item of items) {
|
if (item.children && item.children.length > 0) {
|
checkProcessUniqueness(item.children);
|
}
|
}
|
};
|
|
// 校验函数
|
const validateItem = (item: any, isTopLevel = false) => {
|
if (!isValid) return;
|
// 校验当前项的必填字段
|
if (!item.model) {
|
ElMessage.error("请选择规格");
|
isValid = false;
|
return;
|
}
|
if (!isTopLevel && !item.processId) {
|
ElMessage.error("请选择消耗工序");
|
isValid = false;
|
return;
|
}
|
if (!item.unitQuantity) {
|
ElMessage.error("请输入单位产出所需数量");
|
isValid = false;
|
return;
|
}
|
if (isOrderPage.value && !item.demandedQuantity) {
|
ElMessage.error("请输入需求总量");
|
isValid = false;
|
return;
|
}
|
// if (!item.unit) {
|
// ElMessage.error("请输入单位");
|
// isValid = false;
|
// return;
|
// }
|
|
// 递归校验子项字段
|
if (item.children && item.children.length > 0) {
|
item.children.forEach(child => {
|
validateItem(child, false);
|
});
|
}
|
};
|
|
// 1. 首先校验同一父级下的同层消耗工序是否唯一
|
checkProcessUniqueness(dataValue.dataList);
|
if (!isValid) return false;
|
|
// 2. 然后遍历校验所有顶层项的字段必填情况
|
dataValue.dataList.forEach(item => {
|
validateItem(item, true);
|
});
|
|
return isValid;
|
};
|
|
const submit = () => {
|
dataValue.loading = true;
|
normalizeTreeData(dataValue.dataList);
|
recalculateDemandedQuantities();
|
|
// 先进行表单校验
|
const valid = validateAll();
|
console.log(dataValue.dataList, "dataValue.dataList");
|
if (valid) {
|
addBomDetail({
|
bomId: routeId.value,
|
children: buildSubmitTree(dataValue.dataList || []),
|
})
|
.then(res => {
|
router.go(-1);
|
ElMessage.success("保存成功");
|
dataValue.loading = false;
|
})
|
.catch(() => {
|
dataValue.loading = false;
|
});
|
} else {
|
dataValue.loading = false;
|
}
|
};
|
|
const removeItem = (tempId: string) => {
|
// 先尝试从顶层删除
|
const topIndex = dataValue.dataList.findIndex(item => item.tempId === tempId);
|
if (topIndex !== -1) {
|
dataValue.dataList.splice(topIndex, 1);
|
return;
|
}
|
|
// 递归删除子项
|
const delchildItem = (items: any[], tempId: any) => {
|
for (let i = 0; i < items.length; i++) {
|
const item = items[i];
|
if (item.tempId === tempId) {
|
items.splice(i, 1);
|
return true;
|
}
|
if (item.children && item.children.length > 0) {
|
if (delchildItem(item.children, tempId)) {
|
return true;
|
}
|
}
|
}
|
return false;
|
};
|
|
dataValue.dataList.forEach(item => {
|
if (item.children && item.children.length > 0) {
|
delchildItem(item.children, tempId);
|
}
|
});
|
};
|
const addItem2 = tempId => {
|
dataValue.dataList.map(item => {
|
if (item.tempId === tempId) {
|
if (!item.children) {
|
item.children = [];
|
}
|
item.children.push({
|
parentId: item.id || "",
|
parentTempId: item.tempId || "",
|
productName: "",
|
productId: "",
|
model: undefined,
|
productModelId: undefined,
|
processId: "",
|
processName: "",
|
operationId: "",
|
operationName: "",
|
unitQuantity: 1,
|
demandedQuantity: 0,
|
unit: "",
|
children: [],
|
|
tempId: new Date().getTime(),
|
});
|
recalculateDemandedQuantities();
|
return;
|
}
|
addchildItem(item, tempId);
|
});
|
};
|
const addchildItem = (item: any, tempId: any) => {
|
if (item.tempId === tempId) {
|
console.log(item, "item");
|
if (!item.children) {
|
item.children = [];
|
}
|
item.children.push({
|
parentId: item.id || "",
|
parentTempId: item.tempId || "",
|
productName: "",
|
productId: "",
|
model: undefined,
|
productModelId: undefined,
|
processId: "",
|
processName: "",
|
operationId: "",
|
operationName: "",
|
unitQuantity: 1,
|
demandedQuantity: 0,
|
children: [],
|
unit: "",
|
tempId: new Date().getTime(),
|
});
|
recalculateDemandedQuantities();
|
return true;
|
}
|
if (item.children && item.children.length > 0) {
|
for (let child of item.children) {
|
if (addchildItem(child, tempId)) {
|
return true;
|
}
|
}
|
}
|
return false;
|
};
|
|
const getPropPath = (row, field) => {
|
// 为每个row生成唯一的路径
|
// 使用row.id或索引作为唯一标识
|
let path = "dataList";
|
|
// 简单实现:使用row的id或一个唯一标识
|
const uniqueId = row.id || Math.floor(Math.random() * 10000);
|
path += `.${uniqueId}`;
|
|
return path + `.${field}`;
|
};
|
|
const cancelEdit = () => {
|
dataValue.isEdit = false;
|
// dataValue.dataList = dataValue.dataList.filter(item => item.id !== undefined);
|
fetchData();
|
};
|
|
onMounted(async () => {
|
// 从路由参数回显数据
|
tableData[0].productName = routeProductName.value as string;
|
tableData[0].model = routeProductModelName.value as string;
|
tableData[0].bomNo = routeBomNo.value as string;
|
|
// 订单情况下禁用编辑
|
if (isOrderPage.value) {
|
dataValue.isEdit = false;
|
}
|
|
// 先加载工序选项,再加载数据,确保el-select能够正确回显
|
await fetchProcessOptions();
|
await fetchData();
|
await nextTick();
|
initSortable();
|
});
|
|
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>
|