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