From d01f1f16cf677a10687fc0cfd593e576f445ba6e Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期二, 28 四月 2026 15:32:48 +0800
Subject: [PATCH] Bom模块开发
---
src/pages.json | 23 ++
src/pages/productionDesign/bom/index.vue | 179 ++++++++++++++++
src/pages/works.vue | 5
src/pages/productionDesign/bom/structure.vue | 100 +++++++++
src/api/productionManagement/bom.js | 82 +++++++
src/pages/productionDesign/bom/BomStructureItem.vue | 256 +++++++++++++++++++++++
src/api/productionManagement/processManagement.js | 7
7 files changed, 651 insertions(+), 1 deletions(-)
diff --git a/src/api/productionManagement/bom.js b/src/api/productionManagement/bom.js
new file mode 100644
index 0000000..cb42000
--- /dev/null
+++ b/src/api/productionManagement/bom.js
@@ -0,0 +1,82 @@
+import request from "@/utils/request";
+
+// BOM 鍒楄〃鍒嗛〉鏌ヨ
+export function listPage(query) {
+ return request({
+ url: "/technologyBom/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板 BOM
+export function add(data) {
+ return request({
+ url: "/technologyBom/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼 BOM
+export function update(data) {
+ return request({
+ url: "/technologyBom/update",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎 BOM
+export function batchDelete(ids) {
+ return request({
+ url: "/technologyBom/batchDelete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 澶嶅埗 BOM
+export function copy(data) {
+ return request({
+ url: "/technologyBom/copy",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鑾峰彇浜у搧鍒楄〃 (鐢ㄤ簬鏂板BOM鏃堕�夋嫨浜у搧)
+export function getProductList(query) {
+ return request({
+ url: "/product/ledger/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// --- BOM 缁撴瀯鐩稿叧 ---
+
+// 鏍规嵁 BOM ID 鑾峰彇缁撴瀯鍒楄〃
+export function queryStructureList(bomId) {
+ return request({
+ url: "/technologyBomStructure/listByBomId/" + bomId,
+ method: "get",
+ });
+}
+
+// 淇濆瓨 BOM 缁撴瀯
+export function addStructure(data) {
+ return request({
+ url: "/technologyBomStructure/batchSave",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鍒犻櫎 BOM 缁撴瀯椤�
+export function deleteStructure(id) {
+ return request({
+ url: "/technologyBomStructure/batchDelete/" + id,
+ method: "delete",
+ });
+}
diff --git a/src/api/productionManagement/processManagement.js b/src/api/productionManagement/processManagement.js
index 15331ba..0f8dc06 100644
--- a/src/api/productionManagement/processManagement.js
+++ b/src/api/productionManagement/processManagement.js
@@ -8,6 +8,13 @@
});
}
+export function list() {
+ return request({
+ url: "/technologyOperation/list",
+ method: "get",
+ });
+}
+
export function add(data) {
return request({
url: "/technologyOperation/add",
diff --git a/src/pages.json b/src/pages.json
index d6ade75..2e69ab8 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -402,6 +402,27 @@
}
},
{
+ "path": "pages/productionDesign/bom/index",
+ "style": {
+ "navigationBarTitleText": "BOM绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionDesign/bom/structure",
+ "style": {
+ "navigationBarTitleText": "BOM缁撴瀯",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionDesign/bom/edit",
+ "style": {
+ "navigationBarTitleText": "BOM璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
"path": "pages/cooperativeOffice/collaborativeApproval/index1",
"style": {
"navigationBarTitleText": "鍏嚭绠$悊",
@@ -1442,4 +1463,4 @@
"navigationBarTitleText": "RuoYi",
"navigationBarBackgroundColor": "#FFFFFF"
}
-}
+}
\ No newline at end of file
diff --git a/src/pages/productionDesign/bom/BomStructureItem.vue b/src/pages/productionDesign/bom/BomStructureItem.vue
new file mode 100644
index 0000000..689010c
--- /dev/null
+++ b/src/pages/productionDesign/bom/BomStructureItem.vue
@@ -0,0 +1,256 @@
+<template>
+ <view class="structure-item-wrapper"
+ :class="{ 'is-root': level === 0, 'is-last': isLast }">
+ <!-- 鏍戝舰杩炴帴绾� (闈炴牴鑺傜偣鏄剧ず) -->
+ <template v-if="level > 0">
+ <view class="line-v"></view>
+ <view class="line-h"></view>
+ </template>
+ <view class="structure-item-card"
+ :class="{ 'has-children': hasChildren }">
+ <view class="card-main">
+ <view class="item-header"
+ @click="toggleExpand">
+ <view class="header-left">
+ <view v-if="hasChildren"
+ class="expand-icon"
+ :class="{ 'is-expanded': isExpanded }">
+ <up-icon name="arrow-right"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ <view v-else
+ class="dot-icon"></view>
+ <text class="item-title">{{ item.productName || '鏈�夋嫨浜у搧' }}</text>
+ </view>
+ <up-tag v-if="hasChildren"
+ text="缁勫悎"
+ type="primary"
+ size="mini"
+ plain
+ shape="circle" />
+ </view>
+ <view class="item-body">
+ <view class="info-grid">
+ <view class="info-item">
+ <text class="label">瑙勬牸鍨嬪彿锛�</text>
+ <text class="value">{{ item.model || '-' }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">娑堣�楀伐搴忥細</text>
+ <text class="value">{{ getProcessName(item.processId) }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">鍗曚綅鏁伴噺锛�</text>
+ <text class="value highlight">{{ item.unitQuantity || 0 }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">闇�姹傛�婚噺锛�</text>
+ <text class="value highlight">{{ item.demandedQuantity || 0 }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">鍗曚綅锛�</text>
+ <text class="value">{{ item.unit || '-' }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">鐩樻暟锛�</text>
+ <text class="value">{{ item.diskQuantity || 0 }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+ <!-- 閫掑綊灞曠ず瀛愯妭鐐� -->
+ <view v-if="hasChildren && isExpanded"
+ class="children-container">
+ <BomStructureItem v-for="(child, index) in item.children"
+ :key="index"
+ :item="child"
+ :level="level + 1"
+ :isLast="index === item.children.length - 1"
+ :processOptions="processOptions" />
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { ref, computed, defineProps } from "vue";
+
+ const props = defineProps({
+ item: {
+ type: Object,
+ required: true,
+ },
+ level: {
+ type: Number,
+ default: 0,
+ },
+ isLast: {
+ type: Boolean,
+ default: false,
+ },
+ processOptions: {
+ type: Array,
+ default: () => [],
+ },
+ });
+
+ const isExpanded = ref(true);
+ const hasChildren = computed(
+ () => props.item.children && props.item.children.length > 0
+ );
+
+ const toggleExpand = () => {
+ if (hasChildren.value) {
+ isExpanded.value = !isExpanded.value;
+ }
+ };
+
+ const getProcessName = id => {
+ const process = props.processOptions.find(p => p.id === id);
+ return process ? process.name : "-";
+ };
+</script>
+
+<script>
+ export default {
+ name: "BomStructureItem",
+ };
+</script>
+
+<style scoped lang="scss">
+ .structure-item-wrapper {
+ position: relative;
+ padding-left: 44rpx;
+
+ &.is-root {
+ padding-left: 0;
+ }
+ }
+
+ // 鍨傜洿杩炴帴绾挎
+ .line-v {
+ position: absolute;
+ left: 18rpx; // 灞呬腑浜� 44rpx 鐨勭缉杩涘唴
+ top: -20rpx; // 鍚戜笂寤朵几瑕嗙洊涓婁竴涓妭鐐圭殑 margin-bottom
+ bottom: 0;
+ width: 2rpx;
+ background-color: #ddd;
+ z-index: 1;
+ }
+
+ // 鏈�鍚庝竴涓妭鐐圭殑鍨傜洿绾垮彧寤朵几鍒版按骞崇嚎浣嶇疆
+ .is-last > .line-v {
+ bottom: auto;
+ height: 60rpx; // 20rpx (top offset) + 40rpx (to horizontal line)
+ }
+
+ // 姘村钩杩炴帴绾�
+ .line-h {
+ position: absolute;
+ left: 18rpx;
+ top: 40rpx; // 瀵归綈鍒板崱鐗囧唴閮ㄥ浘鏍囦腑蹇� (padding 24 + icon 32/2)
+ width: 26rpx;
+ height: 2rpx;
+ background-color: #ddd;
+ z-index: 1;
+ }
+
+ .structure-item-card {
+ position: relative;
+ background: #fff;
+ border-radius: 16rpx;
+ margin-bottom: 20rpx;
+ padding: 24rpx;
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
+ border: 1rpx solid #f0f0f0;
+ transition: all 0.3s;
+ z-index: 2;
+
+ &:active {
+ background-color: #f9f9f9;
+ }
+
+ &.has-children {
+ border-left: 6rpx solid #3c9cff;
+ }
+ }
+
+ .card-main {
+ .item-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20rpx;
+
+ .header-left {
+ display: flex;
+ align-items: center;
+ flex: 1;
+
+ .expand-icon {
+ margin-right: 12rpx;
+ transition: transform 0.3s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32rpx;
+ height: 32rpx;
+
+ &.is-expanded {
+ transform: rotate(90deg);
+ }
+ }
+
+ .dot-icon {
+ width: 12rpx;
+ height: 12rpx;
+ border-radius: 50%;
+ background-color: #ccc;
+ margin-right: 20rpx;
+ margin-left: 10rpx;
+ }
+
+ .item-title {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #333;
+ line-height: 1.4;
+ }
+ }
+ }
+
+ .item-body {
+ .info-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12rpx 20rpx;
+
+ .info-item {
+ display: flex;
+ font-size: 24rpx;
+ line-height: 1.5;
+
+ .label {
+ color: #999;
+ white-space: nowrap;
+ }
+
+ .value {
+ color: #666;
+ word-break: break-all;
+
+ &.highlight {
+ color: #3c9cff;
+ font-weight: 500;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ .children-container {
+ position: relative;
+ }
+</style>
diff --git a/src/pages/productionDesign/bom/index.vue b/src/pages/productionDesign/bom/index.vue
new file mode 100644
index 0000000..97ba5f6
--- /dev/null
+++ b/src/pages/productionDesign/bom/index.vue
@@ -0,0 +1,179 @@
+<template>
+ <view class="bom-list">
+ <PageHeader title="BOM绠$悊"
+ @back="goBack" />
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input class="search-text"
+ v-model="queryParams.productName"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+ clearable
+ @change="handleSearch" />
+ </view>
+ <view class="filter-button"
+ @click="handleSearch">
+ <up-icon name="search"
+ size="24"
+ color="#999999"></up-icon>
+ </view>
+ </view>
+ </view>
+ <view v-if="list.length > 0"
+ class="ledger-list">
+ <view v-for="item in list"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="list-dot"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.bomNo || "-" }}</text>
+ </view>
+ <up-tag :text="'V' + (item.version || '1.0')"
+ type="primary"
+ size="mini" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">浜у搧鍚嶇О</text>
+ <text class="detail-value">{{ item.productName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ item.productModelName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">澶囨敞</text>
+ <text class="detail-value">{{ item.remark || "-" }}</text>
+ </view>
+ </view>
+ <view class="action-buttons">
+ <up-button class="action-btn"
+ size="small"
+ type="primary"
+ @click="goStructure(item)">鏌ョ湅璇︽儏</up-button>
+ </view>
+ </view>
+ <up-loadmore :status="pageStatus" />
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty text="鏆傛棤BOM鏁版嵁"
+ mode="list"></up-empty>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { reactive, ref } from "vue";
+ import { onReachBottom, onShow } from "@dcloudio/uni-app";
+ import { listPage } from "@/api/productionManagement/bom";
+
+ const queryParams = reactive({
+ productName: "",
+ });
+ const list = ref([]);
+ const pageStatus = ref("loadmore");
+
+ const page = reactive({
+ current: 1,
+ size: 3,
+ total: 0,
+ });
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const handleSearch = () => {
+ page.current = 1;
+ pageStatus.value = "loadmore";
+ list.value = [];
+ getList();
+ };
+
+ const getList = () => {
+ if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
+
+ pageStatus.value = "loading";
+ listPage({
+ current: page.current,
+ size: page.size,
+ productName: queryParams.productName,
+ })
+ .then(res => {
+ const records = res?.data?.records || res?.records || [];
+ const total = res?.data?.total || res?.total || 0;
+
+ if (page.current === 1) {
+ list.value = records;
+ } else {
+ list.value = [...list.value, ...records];
+ }
+
+ page.total = total;
+ if (list.value.length >= total) {
+ pageStatus.value = "nomore";
+ } else {
+ pageStatus.value = "loadmore";
+ page.current++;
+ }
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "error" });
+ pageStatus.value = "loadmore";
+ });
+ };
+
+ const goStructure = item => {
+ uni.navigateTo({
+ url: `/pages/productionDesign/bom/structure?id=${
+ item.id
+ }&bomNo=${encodeURIComponent(item.bomNo)}&productName=${encodeURIComponent(
+ item.productName || ""
+ )}&productModelName=${encodeURIComponent(
+ item.productModelName || ""
+ )}&remark=${encodeURIComponent(
+ item.remark || ""
+ )}&version=${encodeURIComponent(item.version || 1)}`,
+ });
+ };
+
+ onReachBottom(() => {
+ getList();
+ });
+
+ onShow(() => {
+ handleSearch();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .no-data {
+ padding-top: 100rpx;
+ text-align: center;
+ color: #999;
+ font-size: 28rpx;
+ }
+
+ .action-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 15rpx;
+ padding: 0 30rpx 30rpx;
+ flex-wrap: wrap;
+ }
+
+ .action-btn {
+ width: calc(50% - 15rpx);
+ margin: 0 !important;
+ margin-bottom: 15rpx !important;
+ }
+</style>
diff --git a/src/pages/productionDesign/bom/structure.vue b/src/pages/productionDesign/bom/structure.vue
new file mode 100644
index 0000000..5b7c2a7
--- /dev/null
+++ b/src/pages/productionDesign/bom/structure.vue
@@ -0,0 +1,100 @@
+<template>
+ <view class="structure-page">
+ <PageHeader :title="'BOM缁撴瀯 - ' + bomNo"
+ @back="goBack" />
+ <view class="info-card">
+ <view class="info-row">
+ <text class="info-label">浜у搧鍚嶇О锛�</text>
+ <text class="info-value">{{ productName }}-{{ productModelName }}</text>
+ </view>
+ </view>
+ <view class="structure-list"
+ v-if="dataList.length > 0">
+ <BomStructureItem v-for="(item, index) in dataList"
+ :key="index"
+ :item="item"
+ :level="0"
+ :isLast="index === dataList.length - 1"
+ :processOptions="processOptions" />
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty text="鏆傛棤缁撴瀯鏁版嵁"
+ mode="list"></up-empty>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, computed } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import { queryStructureList } from "@/api/productionManagement/bom";
+ import { list as getProcessList } from "@/api/productionManagement/processManagement";
+ import BomStructureItem from "./BomStructureItem.vue";
+
+ const bomId = ref(null);
+ const bomNo = ref("");
+ const productName = ref("");
+ const dataList = ref([]);
+ const processOptions = ref([]);
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const fetchData = () => {
+ queryStructureList(bomId.value).then(res => {
+ dataList.value = res.data || [];
+ });
+ };
+
+ const fetchProcess = () => {
+ getProcessList().then(res => {
+ processOptions.value = res.data || [];
+ });
+ };
+
+ const productModelName = ref("");
+
+ onLoad(options => {
+ bomId.value = options.id;
+ bomNo.value = decodeURIComponent(options.bomNo);
+ productName.value = decodeURIComponent(options.productName);
+ productModelName.value = decodeURIComponent(options.productModelName);
+ fetchData();
+ fetchProcess();
+ });
+</script>
+
+<style scoped lang="scss">
+ .structure-page {
+ background-color: #f5f5f5;
+ min-height: 100vh;
+ padding-bottom: 120rpx;
+ }
+
+ .info-card {
+ background: #fff;
+ padding: 30rpx;
+ margin-bottom: 20rpx;
+ .info-row {
+ display: flex;
+ font-size: 28rpx;
+ .info-label {
+ color: #666;
+ }
+ .info-value {
+ color: #333;
+ font-weight: bold;
+ }
+ }
+ }
+
+ .structure-list {
+ padding: 20rpx;
+ }
+
+ .no-data {
+ padding-top: 100rpx;
+ }
+</style>
diff --git a/src/pages/works.vue b/src/pages/works.vue
index ee32b41..71c1721 100644
--- a/src/pages/works.vue
+++ b/src/pages/works.vue
@@ -1001,6 +1001,11 @@
url: "/pages/productionDesign/processManagement/index",
});
break;
+ case "BOM":
+ uni.navigateTo({
+ url: "/pages/productionDesign/bom/index",
+ });
+ break;
default:
uni.showToast({
title: `鐐瑰嚮浜�${item.label}`,
--
Gitblit v1.9.3