From 4c8d18cc5ed8a7b0e220c91a858d16d0310896df Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期一, 29 六月 2026 16:50:03 +0800
Subject: [PATCH] BOM新增修改删除功能开发、以及BOM结构的可编辑
---
src/pages/productionDesign/bom/structure.vue | 534 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 530 insertions(+), 4 deletions(-)
diff --git a/src/pages/productionDesign/bom/structure.vue b/src/pages/productionDesign/bom/structure.vue
index 5b7c2a7..9fe18a4 100644
--- a/src/pages/productionDesign/bom/structure.vue
+++ b/src/pages/productionDesign/bom/structure.vue
@@ -11,16 +11,148 @@
<view class="structure-list"
v-if="dataList.length > 0">
<BomStructureItem v-for="(item, index) in dataList"
- :key="index"
+ :key="item.tempId || item.id || index"
:item="item"
:level="0"
:isLast="index === dataList.length - 1"
- :processOptions="processOptions" />
+ :processOptions="processOptions"
+ :isEdit="isEdit"
+ @edit="openNodeEditor"
+ @add-child="addChild"
+ @remove="removeNode" />
</view>
<view v-else
class="no-data">
<up-empty text="鏆傛棤缁撴瀯鏁版嵁"
mode="list"></up-empty>
+ </view>
+ <!-- <view v-if="isEdit"
+ class="fab-button"
+ @click="addRoot">
+ <up-icon name="plus"
+ size="24"
+ color="#ffffff"></up-icon>
+ </view> -->
+ <up-popup :show="showNodePopup"
+ mode="bottom"
+ round
+ @close="closeNodeEditor">
+ <view class="popup-container">
+ <view class="popup-header">
+ <text class="popup-cancel"
+ @click="closeNodeEditor">鍙栨秷</text>
+ <text class="popup-title">缂栬緫鑺傜偣</text>
+ <text class="popup-confirm"
+ @click="confirmNodeEdit">纭畾</text>
+ </view>
+ <view class="popup-body">
+ <up-form :model="nodeForm"
+ label-width="110">
+ <up-form-item label="浜у搧"
+ required>
+ <up-input v-model="nodeForm.productName"
+ readonly
+ placeholder="鐐瑰嚮閫夋嫨浜у搧"
+ @click="openProductPicker" />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click="openProductPicker"></up-icon>
+ </template>
+ </up-form-item>
+ <up-form-item label="瑙勬牸鍨嬪彿">
+ <up-input v-model="nodeForm.model"
+ readonly
+ placeholder="--" />
+ </up-form-item>
+ <up-form-item label="娑堣�楀伐搴�">
+ <up-input v-model="nodeForm.processName"
+ readonly
+ placeholder="鐐瑰嚮閫夋嫨宸ュ簭"
+ @click="showProcessPicker = true" />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click="showProcessPicker = true"></up-icon>
+ </template>
+ </up-form-item>
+ <up-form-item label="鍗曚綅浜у嚭鎵�闇�鏁伴噺">
+ <up-input v-model="nodeForm.unitQuantity"
+ type="number"
+ placeholder="璇疯緭鍏�" />
+ </up-form-item>
+ <up-form-item label="鍗曚綅">
+ <up-input v-model="nodeForm.unit"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鐩樻暟">
+ <up-input v-model="nodeForm.diskQuantity"
+ type="number"
+ placeholder="璇疯緭鍏�" />
+ </up-form-item>
+ </up-form>
+ </view>
+ </view>
+ </up-popup>
+ <up-action-sheet :show="showProcessPicker"
+ :actions="processActionList"
+ title="閫夋嫨宸ュ簭"
+ @select="onProcessSelect"
+ @close="showProcessPicker = false" />
+ <up-popup :show="showProductPicker"
+ mode="bottom"
+ round
+ @close="showProductPicker = false">
+ <view class="popup-container">
+ <view class="popup-header">
+ <text class="popup-cancel"
+ @click="showProductPicker = false">鍙栨秷</text>
+ <text class="popup-title">閫夋嫨浜у搧</text>
+ <text class="popup-confirm"
+ @click="handleProductSearch">鎼滅储</text>
+ </view>
+ <view class="popup-body">
+ <view class="picker-search">
+ <up-input v-model="productQuery.productName"
+ placeholder="浜у搧鍚嶇О"
+ clearable
+ @change="handleProductSearch" />
+ <up-input v-model="productQuery.model"
+ placeholder="瑙勬牸鍨嬪彿"
+ clearable
+ @change="handleProductSearch" />
+ </view>
+ <scroll-view scroll-y
+ class="picker-list"
+ @scrolltolower="loadMoreProducts">
+ <view v-for="row in productList"
+ :key="row.id"
+ class="picker-item"
+ @click="selectProduct(row)">
+ <view class="picker-item__title">
+ <text>{{ row.productName || '-' }}</text>
+ </view>
+ <view class="picker-item__sub">
+ <text>{{ row.model || '-' }}</text>
+ <text class="picker-item__unit">{{ row.unit || '-' }}</text>
+ </view>
+ </view>
+ <up-loadmore :status="productPageStatus" />
+ </scroll-view>
+ </view>
+ </view>
+ </up-popup>
+ <view class="bottom-actions">
+ <up-button v-if="!isEdit"
+ type="primary"
+ @click="startEdit">缂栬緫</up-button>
+ <template v-else>
+ <up-button class="bottom-actions__cancel"
+ @click="cancelEdit">鍙栨秷</up-button>
+ <up-button class="bottom-actions__confirm"
+ type="primary"
+ :loading="saving"
+ @click="saveEdit">纭</up-button>
+ </template>
</view>
</view>
</template>
@@ -28,7 +160,11 @@
<script setup>
import { ref, reactive, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
- import { queryStructureList } from "@/api/productionManagement/bom";
+ import {
+ queryStructureList,
+ addStructure,
+ getProductList,
+ } from "@/api/productionManagement/bom";
import { list as getProcessList } from "@/api/productionManagement/processManagement";
import BomStructureItem from "./BomStructureItem.vue";
@@ -36,25 +172,305 @@
const bomNo = ref("");
const productName = ref("");
const dataList = ref([]);
+ const originalDataList = ref([]);
const processOptions = ref([]);
+ const isEdit = ref(false);
+ const saving = ref(false);
+
+ const showNodePopup = ref(false);
+ const editingNode = ref(null);
+ const nodeForm = reactive({
+ tempId: "",
+ id: undefined,
+ productModelId: "",
+ productName: "",
+ model: "",
+ unit: "",
+ processId: "",
+ processName: "",
+ unitQuantity: "",
+ demandedQuantity: "",
+ diskQuantity: "",
+ children: [],
+ });
+ const showProcessPicker = ref(false);
+ const processActionList = computed(() => {
+ return (processOptions.value || []).map(p => ({
+ name: p.name,
+ value: p.id,
+ }));
+ });
+
+ const showProductPicker = ref(false);
+ const productQuery = reactive({
+ productName: "",
+ model: "",
+ });
+ const productList = ref([]);
+ const productPage = reactive({
+ current: 1,
+ size: 20,
+ total: 0,
+ });
+ const productPageStatus = ref("loadmore");
const goBack = () => {
uni.navigateBack();
};
+ const genTempId = () => `${Date.now()}_${Math.random()}`;
+
+ const normalizeTreeData = items => {
+ if (!Array.isArray(items)) return;
+ items.forEach(item => {
+ item.tempId = item.tempId || item.id || genTempId();
+ const pid = item.processId ?? item.operationId ?? "";
+ item.processId = pid;
+ if (pid && !item.processName) {
+ const opt = processOptions.value.find(p => String(p.id) === String(pid));
+ item.processName = opt?.name || item.operationName || "";
+ }
+ if (Array.isArray(item.children) && item.children.length > 0) {
+ normalizeTreeData(item.children);
+ } else if (!Array.isArray(item.children)) {
+ item.children = [];
+ }
+ });
+ };
+
const fetchData = () => {
queryStructureList(bomId.value).then(res => {
dataList.value = res.data || [];
+ normalizeTreeData(dataList.value);
+ originalDataList.value = JSON.parse(JSON.stringify(dataList.value || []));
});
};
const fetchProcess = () => {
getProcessList().then(res => {
processOptions.value = res.data || [];
+ normalizeTreeData(dataList.value);
});
};
const productModelName = ref("");
+
+ const startEdit = () => {
+ isEdit.value = true;
+ originalDataList.value = JSON.parse(JSON.stringify(dataList.value || []));
+ };
+
+ const cancelEdit = () => {
+ isEdit.value = false;
+ dataList.value = JSON.parse(JSON.stringify(originalDataList.value || []));
+ normalizeTreeData(dataList.value);
+ };
+
+ const buildSubmitTree = items => {
+ return (items || []).map(item => {
+ const processId = item.processId ?? item.operationId ?? "";
+ return {
+ id: item.id,
+ bomId: bomId.value,
+ productModelId: item.productModelId,
+ productName: item.productName,
+ model: item.model,
+ unit: item.unit,
+ processId: processId,
+ operationId: processId,
+ processName: item.processName || item.operationName || "",
+ operationName: item.processName || item.operationName || "",
+ unitQuantity: item.unitQuantity,
+ demandedQuantity: item.demandedQuantity,
+ diskQuantity: item.diskQuantity,
+ children: buildSubmitTree(item.children || []),
+ };
+ });
+ };
+
+ const saveEdit = () => {
+ if (!isEdit.value) return;
+ saving.value = true;
+ addStructure({
+ bomId: bomId.value,
+ children: buildSubmitTree(dataList.value || []),
+ })
+ .then(res => {
+ if (res && res.code !== undefined && res.code !== 200) {
+ uni.showToast({
+ title: res.msg || "淇濆瓨澶辫触",
+ icon: "none",
+ });
+ return;
+ }
+ uni.showToast({
+ title: "淇濆瓨鎴愬姛",
+ icon: "success",
+ });
+ isEdit.value = false;
+ fetchData();
+ })
+ .finally(() => {
+ saving.value = false;
+ });
+ };
+
+ const emptyNode = () => ({
+ tempId: genTempId(),
+ id: undefined,
+ productModelId: "",
+ productName: "",
+ model: "",
+ unit: "",
+ processId: "",
+ processName: "",
+ unitQuantity: "",
+ demandedQuantity: "",
+ diskQuantity: "",
+ children: [],
+ });
+
+ const addRoot = () => {
+ dataList.value = Array.isArray(dataList.value) ? dataList.value : [];
+ dataList.value.push(emptyNode());
+ };
+
+ const addChild = node => {
+ if (!node.children || !Array.isArray(node.children)) node.children = [];
+ node.children.push(emptyNode());
+ };
+
+ const removeByTempId = (items, tempId) => {
+ const idx = (items || []).findIndex(i => i.tempId === tempId);
+ if (idx !== -1) {
+ items.splice(idx, 1);
+ return true;
+ }
+ for (const it of items || []) {
+ if (Array.isArray(it.children) && it.children.length > 0) {
+ if (removeByTempId(it.children, tempId)) return true;
+ }
+ }
+ return false;
+ };
+
+ const removeNode = node => {
+ uni.showModal({
+ title: "鎻愮ず",
+ content: "纭鍒犻櫎璇ヨ妭鐐癸紵",
+ confirmText: "纭",
+ cancelText: "鍙栨秷",
+ success: res => {
+ if (!res.confirm) return;
+ removeByTempId(dataList.value, node.tempId);
+ },
+ });
+ };
+
+ const openNodeEditor = node => {
+ if (!isEdit.value) return;
+ editingNode.value = node;
+ Object.assign(nodeForm, {
+ tempId: node.tempId,
+ id: node.id,
+ productModelId: node.productModelId || "",
+ productName: node.productName || "",
+ model: node.model || "",
+ unit: node.unit || "",
+ processId: node.processId || "",
+ processName: node.processName || "",
+ unitQuantity: node.unitQuantity ?? "",
+ demandedQuantity: node.demandedQuantity ?? "",
+ diskQuantity: node.diskQuantity ?? "",
+ children: node.children || [],
+ });
+ showNodePopup.value = true;
+ };
+
+ const closeNodeEditor = () => {
+ showNodePopup.value = false;
+ editingNode.value = null;
+ showProcessPicker.value = false;
+ };
+
+ const confirmNodeEdit = () => {
+ if (!editingNode.value) {
+ closeNodeEditor();
+ return;
+ }
+ Object.assign(editingNode.value, {
+ productModelId: nodeForm.productModelId,
+ productName: nodeForm.productName,
+ model: nodeForm.model,
+ unit: nodeForm.unit,
+ processId: nodeForm.processId,
+ processName: nodeForm.processName,
+ unitQuantity: nodeForm.unitQuantity,
+ diskQuantity: nodeForm.diskQuantity,
+ });
+ closeNodeEditor();
+ };
+
+ const onProcessSelect = item => {
+ nodeForm.processId = item.value;
+ nodeForm.processName = item.name;
+ showProcessPicker.value = false;
+ };
+
+ const openProductPicker = () => {
+ showProductPicker.value = true;
+ handleProductSearch();
+ };
+
+ const handleProductSearch = () => {
+ productPage.current = 1;
+ productPageStatus.value = "loadmore";
+ productList.value = [];
+ loadMoreProducts();
+ };
+
+ const loadMoreProducts = () => {
+ if (
+ productPageStatus.value === "loading" ||
+ productPageStatus.value === "nomore"
+ ) {
+ return;
+ }
+ productPageStatus.value = "loading";
+ getProductList({
+ current: productPage.current,
+ size: productPage.size,
+ productName: productQuery.productName,
+ model: productQuery.model,
+ })
+ .then(res => {
+ const records = res?.data?.records || res?.records || res?.data || [];
+ const total = res?.data?.total || res?.total || 0;
+ const next = Array.isArray(records) ? records : [];
+ productList.value =
+ productPage.current === 1 ? next : [...productList.value, ...next];
+ productPage.total = Number(total || productList.value.length);
+ if (productList.value.length >= productPage.total) {
+ productPageStatus.value = "nomore";
+ } else {
+ productPageStatus.value = "loadmore";
+ productPage.current++;
+ }
+ })
+ .catch(() => {
+ productPageStatus.value = "loadmore";
+ });
+ };
+
+ const selectProduct = row => {
+ nodeForm.productModelId = row.id;
+ nodeForm.productName = row.productName || "";
+ nodeForm.model = row.model || "";
+ if (!nodeForm.unit) {
+ nodeForm.unit = row.unit || "";
+ }
+ showProductPicker.value = false;
+ };
onLoad(options => {
bomId.value = options.id;
@@ -70,7 +486,7 @@
.structure-page {
background-color: #f5f5f5;
min-height: 100vh;
- padding-bottom: 120rpx;
+ padding-bottom: 200rpx;
}
.info-card {
@@ -97,4 +513,114 @@
.no-data {
padding-top: 100rpx;
}
+
+ .fab-button {
+ position: fixed;
+ right: 30rpx;
+ bottom: 150rpx;
+ width: 96rpx;
+ height: 96rpx;
+ border-radius: 48rpx;
+ background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 10rpx 20rpx rgba(0, 108, 251, 0.25);
+ z-index: 1000;
+ }
+
+ .bottom-actions {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #fff;
+ padding: 16rpx 24rpx;
+ display: flex;
+ gap: 16rpx;
+ box-shadow: 0 -6rpx 18rpx rgba(0, 0, 0, 0.06);
+ z-index: 1100;
+ }
+
+ .bottom-actions__cancel {
+ flex: 1;
+ }
+
+ .bottom-actions__confirm {
+ flex: 2;
+ }
+
+ .popup-container {
+ background: #fff;
+ border-radius: 20rpx 20rpx 0 0;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .popup-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 24rpx 28rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+ }
+
+ .popup-title {
+ font-size: 30rpx;
+ font-weight: 600;
+ color: #333;
+ }
+
+ .popup-cancel {
+ font-size: 28rpx;
+ color: #666;
+ }
+
+ .popup-confirm {
+ font-size: 28rpx;
+ color: #006cfb;
+ font-weight: 600;
+ }
+
+ .popup-body {
+ padding: 20rpx 24rpx 30rpx;
+ overflow: hidden;
+ flex: 1;
+ }
+
+ .picker-search {
+ display: flex;
+ gap: 16rpx;
+ margin-bottom: 16rpx;
+ }
+
+ .picker-list {
+ height: 60vh;
+ }
+
+ .picker-item {
+ padding: 22rpx 0;
+ border-bottom: 1rpx solid #f5f5f5;
+ }
+
+ .picker-item__title {
+ font-size: 28rpx;
+ color: #333;
+ font-weight: 600;
+ }
+
+ .picker-item__sub {
+ margin-top: 6rpx;
+ font-size: 24rpx;
+ color: #666;
+ display: flex;
+ justify-content: space-between;
+ gap: 16rpx;
+ }
+
+ .picker-item__unit {
+ color: #999;
+ white-space: nowrap;
+ }
</style>
--
Gitblit v1.9.3