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/index.vue | 365 ++++++++++++++++++++++
src/pages/productionDesign/bom/structure.vue | 534 +++++++++++++++++++++++++++++++++
src/api/productionManagement/bom.js | 4
src/pages/productionDesign/bom/BomStructureItem.vue | 44 ++
4 files changed, 937 insertions(+), 10 deletions(-)
diff --git a/src/api/productionManagement/bom.js b/src/api/productionManagement/bom.js
index cb42000..46a9c9f 100644
--- a/src/api/productionManagement/bom.js
+++ b/src/api/productionManagement/bom.js
@@ -48,7 +48,7 @@
// 鑾峰彇浜у搧鍒楄〃 (鐢ㄤ簬鏂板BOM鏃堕�夋嫨浜у搧)
export function getProductList(query) {
return request({
- url: "/product/ledger/listPage",
+ url: "/basic/product/pageModel",
method: "get",
params: query,
});
@@ -67,7 +67,7 @@
// 淇濆瓨 BOM 缁撴瀯
export function addStructure(data) {
return request({
- url: "/technologyBomStructure/batchSave",
+ url: "/technologyBomStructure",
method: "post",
data: data,
});
diff --git a/src/pages/productionDesign/bom/BomStructureItem.vue b/src/pages/productionDesign/bom/BomStructureItem.vue
index 689010c..e49f35c 100644
--- a/src/pages/productionDesign/bom/BomStructureItem.vue
+++ b/src/pages/productionDesign/bom/BomStructureItem.vue
@@ -58,23 +58,39 @@
</view>
</view>
</view>
+ <view v-if="isEdit"
+ class="item-actions">
+ <up-button size="mini"
+ type="primary"
+ @click.stop="emitEdit">缂栬緫</up-button>
+ <up-button size="mini"
+ type="success"
+ @click.stop="emitAddChild">娣诲姞</up-button>
+ <up-button size="mini"
+ type="error"
+ @click.stop="emitRemove">鍒犻櫎</up-button>
+ </view>
</view>
</view>
<!-- 閫掑綊灞曠ず瀛愯妭鐐� -->
<view v-if="hasChildren && isExpanded"
class="children-container">
<BomStructureItem v-for="(child, index) in item.children"
- :key="index"
+ :key="child.tempId || child.id || index"
:item="child"
:level="level + 1"
:isLast="index === item.children.length - 1"
- :processOptions="processOptions" />
+ :processOptions="processOptions"
+ :isEdit="isEdit"
+ @edit="$emit('edit', $event)"
+ @add-child="$emit('add-child', $event)"
+ @remove="$emit('remove', $event)" />
</view>
</view>
</template>
<script setup>
- import { ref, computed, defineProps } from "vue";
+ import { ref, computed, defineProps, defineEmits } from "vue";
const props = defineProps({
item: {
@@ -93,7 +109,12 @@
type: Array,
default: () => [],
},
+ isEdit: {
+ type: Boolean,
+ default: false,
+ },
});
+ const emit = defineEmits(["edit", "add-child", "remove"]);
const isExpanded = ref(true);
const hasChildren = computed(
@@ -109,6 +130,16 @@
const getProcessName = id => {
const process = props.processOptions.find(p => p.id === id);
return process ? process.name : "-";
+ };
+
+ const emitEdit = () => {
+ emit("edit", props.item);
+ };
+ const emitAddChild = () => {
+ emit("add-child", props.item);
+ };
+ const emitRemove = () => {
+ emit("remove", props.item);
};
</script>
@@ -253,4 +284,11 @@
.children-container {
position: relative;
}
+
+ .item-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 16rpx;
+ margin-top: 16rpx;
+ }
</style>
diff --git a/src/pages/productionDesign/bom/index.vue b/src/pages/productionDesign/bom/index.vue
index 97ba5f6..c3368d8 100644
--- a/src/pages/productionDesign/bom/index.vue
+++ b/src/pages/productionDesign/bom/index.vue
@@ -57,6 +57,14 @@
size="small"
type="primary"
@click="goStructure(item)">鏌ョ湅璇︽儏</up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="warning"
+ @click="openEdit(item)">淇敼</up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="error"
+ @click="handleDelete(item)">鍒犻櫎</up-button>
</view>
</view>
<up-loadmore :status="pageStatus" />
@@ -66,13 +74,119 @@
<up-empty text="鏆傛棤BOM鏁版嵁"
mode="list"></up-empty>
</view>
+ <view class="fab-button"
+ @click="openAdd">
+ <up-icon name="plus"
+ size="24"
+ color="#ffffff"></up-icon>
+ </view>
+ <up-popup :show="showFormPopup"
+ mode="bottom"
+ round
+ @close="closeFormPopup">
+ <view class="popup-container">
+ <view class="popup-header">
+ <text class="popup-cancel"
+ @click="closeFormPopup">鍙栨秷</text>
+ <text class="popup-title">{{ formMode === 'add' ? '鏂板BOM' : '淇敼BOM' }}</text>
+ <text class="popup-confirm"
+ @click="submitForm">纭畾</text>
+ </view>
+ <view class="popup-body">
+ <up-form ref="bomFormRef"
+ :model="bomForm"
+ :rules="bomRules"
+ label-width="110">
+ <up-form-item label="浜у搧"
+ prop="productModelId"
+ required>
+ <up-input v-model="bomForm.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="bomForm.productModelName"
+ readonly
+ placeholder="--" />
+ </up-form-item>
+ <up-form-item label="鐗堟湰鍙�"
+ prop="version"
+ required>
+ <up-input v-model="bomForm.version"
+ placeholder="璇疯緭鍏ョ増鏈彿"
+ clearable />
+ </up-form-item>
+ <up-form-item label="澶囨敞"
+ prop="remark">
+ <up-textarea v-model="bomForm.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ auto-height />
+ </up-form-item>
+ </up-form>
+ </view>
+ </view>
+ </up-popup>
+ <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>
</template>
<script setup>
import { reactive, ref } from "vue";
import { onReachBottom, onShow } from "@dcloudio/uni-app";
- import { listPage } from "@/api/productionManagement/bom";
+ import {
+ listPage,
+ add,
+ update,
+ batchDelete,
+ getProductList,
+ } from "@/api/productionManagement/bom";
const queryParams = reactive({
productName: "",
@@ -85,6 +199,34 @@
size: 3,
total: 0,
});
+ const showFormPopup = ref(false);
+ const formMode = ref("add");
+ const bomFormRef = ref(null);
+ const bomForm = reactive({
+ id: undefined,
+ productName: "",
+ productModelName: "",
+ productModelId: "",
+ remark: "",
+ version: "",
+ });
+ const bomRules = {
+ productModelId: [{ required: true, message: "璇烽�夋嫨浜у搧", trigger: "blur" }],
+ version: [{ required: true, message: "璇疯緭鍏ョ増鏈彿", trigger: "blur" }],
+ };
+
+ 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();
@@ -144,6 +286,153 @@
});
};
+ const openAdd = () => {
+ formMode.value = "add";
+ Object.assign(bomForm, {
+ id: undefined,
+ productName: "",
+ productModelName: "",
+ productModelId: "",
+ remark: "",
+ version: "",
+ });
+ showFormPopup.value = true;
+ };
+
+ const openEdit = row => {
+ formMode.value = "edit";
+ Object.assign(bomForm, {
+ id: row.id,
+ productName: row.productName || "",
+ productModelName: row.productModelName || "",
+ productModelId: row.productModelId || "",
+ remark: row.remark || "",
+ version: row.version || "",
+ });
+ showFormPopup.value = true;
+ };
+
+ const closeFormPopup = () => {
+ showFormPopup.value = false;
+ };
+
+ const submitForm = () => {
+ if (!bomFormRef.value) return;
+ bomFormRef.value.validate(valid => {
+ if (!valid) return;
+ const payload = { ...bomForm };
+ const req = formMode.value === "add" ? add(payload) : update(payload);
+ req
+ .then(res => {
+ if (res && res.code !== undefined && res.code !== 200) {
+ uni.showToast({
+ title: res.msg || "鎻愪氦澶辫触",
+ icon: "none",
+ });
+ return;
+ }
+ uni.showToast({
+ title: "鎻愪氦鎴愬姛",
+ icon: "success",
+ });
+ closeFormPopup();
+ handleSearch();
+ })
+ .catch(() => {
+ uni.showToast({
+ title: "鎻愪氦澶辫触",
+ icon: "error",
+ });
+ });
+ });
+ };
+
+ const handleDelete = row => {
+ if (!row?.id) return;
+ uni.showModal({
+ title: "鎻愮ず",
+ content: "纭鍒犻櫎璇OM锛�",
+ confirmText: "纭",
+ cancelText: "鍙栨秷",
+ success: res => {
+ if (!res.confirm) return;
+ batchDelete([row.id])
+ .then(result => {
+ if (result && result.code !== undefined && result.code !== 200) {
+ uni.showToast({
+ title: result.msg || "鍒犻櫎澶辫触",
+ icon: "none",
+ });
+ return;
+ }
+ uni.showToast({
+ title: "鍒犻櫎鎴愬姛",
+ icon: "success",
+ });
+ handleSearch();
+ })
+ .catch(() => {
+ uni.showToast({
+ title: "鍒犻櫎澶辫触",
+ icon: "error",
+ });
+ });
+ },
+ });
+ };
+
+ 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 => {
+ bomForm.productModelId = row.id;
+ bomForm.productName = row.productName || "";
+ bomForm.productModelName = row.model || "";
+ showProductPicker.value = false;
+ };
+
onReachBottom(() => {
getList();
});
@@ -176,4 +465,78 @@
margin: 0 !important;
margin-bottom: 15rpx !important;
}
+
+ .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>
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