From 2379443c468dadd3edb9184be4e9119359e6b996 Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期五, 15 五月 2026 17:28:00 +0800
Subject: [PATCH] 库存管理重构

---
 /dev/null                                                |  134 -------------
 src/pages/inventoryManagement/stockManagement/index.vue  |  133 +++++++++----
 src/api/inventoryManagement/stockInventory.js            |    9 
 src/pages/inventoryManagement/stockManagement/Record.vue |  292 +++++++++++++++++++++++++++++
 4 files changed, 391 insertions(+), 177 deletions(-)

diff --git a/src/api/inventoryManagement/stockInventory.js b/src/api/inventoryManagement/stockInventory.js
index dfa5f46..c4115ea 100644
--- a/src/api/inventoryManagement/stockInventory.js
+++ b/src/api/inventoryManagement/stockInventory.js
@@ -8,6 +8,15 @@
     });
 };
 
+// 鍒嗛〉鏌ヨ鑱斿悎搴撳瓨璁板綍鍒楄〃锛堝寘鍚晢鍝佷俊鎭級
+export const getStockInventoryListPageCombined = (params) => {
+    return request({
+        url: "/stockInventory/pageListCombinedStockInventory",
+        method: "get",
+        params,
+    });
+};
+
 // 鍒涘缓搴撳瓨璁板綍
 export const createStockInventory = (params) => {
     return request({
diff --git a/src/pages/inventoryManagement/stockManagement/Qualified.vue b/src/pages/inventoryManagement/stockManagement/Qualified.vue
deleted file mode 100644
index 12b9060..0000000
--- a/src/pages/inventoryManagement/stockManagement/Qualified.vue
+++ /dev/null
@@ -1,151 +0,0 @@
-<template>
-  <view class="qualified-record-container">
-    <view class="search-section">
-      <view class="search-bar">
-        <view class="search-input">
-          <up-input
-            class="search-text"
-            placeholder="璇疯緭鍏ヤ骇鍝佸ぇ绫�"
-            v-model="searchForm.productName"
-            @confirm="handleQuery"
-            clearable
-          />
-        </view>
-        <view class="filter-button" @click="handleQuery">
-          <up-icon name="search" size="24" color="#999"></up-icon>
-        </view>
-      </view>
-    </view>
-
-    <scroll-view scroll-y class="ledger-list" v-if="tableData.length > 0" @scrolltolower="loadMore">
-      <view v-for="item in tableData" :key="item.id" class="ledger-item" :class="{ 'low-stock': isLowStock(item) }">
-        <view class="item-header">
-          <view class="item-left">
-            <view class="document-icon">
-              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
-            </view>
-            <text class="item-id">{{ item.productName }}</text>
-          </view>
-          <view class="item-right">
-            <text class="item-tag tag-type">鍚堟牸搴撳瓨</text>
-          </view>
-        </view>
-        
-        <up-divider></up-divider>
-
-        <view class="item-details">
-          <view class="detail-row">
-            <text class="detail-label">瑙勬牸鍨嬪彿</text>
-            <text class="detail-value">{{ item.model }}</text>
-          </view>
-          <view class="detail-row">
-            <text class="detail-label">搴撳瓨鏁伴噺</text>
-            <text class="detail-value">{{ item.qualitity }} {{ item.unit }}</text>
-          </view>
-          <view class="detail-row">
-            <text class="detail-label">閿佸畾/鍐荤粨</text>
-            <text class="detail-value">{{ item.lockedQuantity }} {{ item.unit }}</text>
-          </view>
-          <view class="detail-row">
-            <text class="detail-label">鍙敤搴撳瓨</text>
-            <text class="detail-value" style="color: #2979ff; font-weight: bold;">{{ item.unLockedQuantity }} {{ item.unit }}</text>
-          </view>
-          <view class="detail-row">
-            <text class="detail-label">搴撳瓨棰勮</text>
-            <text class="detail-value">{{ item.warnNum }}</text>
-          </view>
-          <view class="detail-row">
-            <text class="detail-label">鏇存柊鏃堕棿</text>
-            <text class="detail-value">{{ item.updateTime }}</text>
-          </view>
-        </view>
-      </view>
-      <up-loadmore :status="loadStatus" />
-    </scroll-view>
-    <view v-else-if="!loading" class="no-data">
-      <up-empty mode="data" text="鏆傛棤搴撳瓨鏁版嵁"></up-empty>
-    </view>
-  </view>
-</template>
-
-<script setup>
-import { ref, reactive, onMounted } from 'vue';
-import { getStockInventoryListPage } from "@/api/inventoryManagement/stockInventory.js";
-
-const tableData = ref([]);
-const loading = ref(false);
-const loadStatus = ref('loadmore');
-const page = reactive({ current: 1, size: 10 });
-const total = ref(0);
-const searchForm = reactive({ productName: '' });
-
-const handleQuery = () => {
-  page.current = 1;
-  tableData.value = [];
-  getList();
-};
-
-const getList = () => {
-  if (loading.value) return;
-  loading.value = true;
-  loadStatus.value = 'loading';
-  getStockInventoryListPage({ ...searchForm, ...page, type: 'qualified' }).then(res => {
-    loading.value = false;
-    const records = res.data.records || [];
-    tableData.value = page.current === 1 ? records : [...tableData.value, ...records];
-    total.value = res.data.total;
-    loadStatus.value = tableData.value.length >= total.value ? 'nomore' : 'loadmore';
-  }).catch(() => {
-    loading.value = false;
-    loadStatus.value = 'loadmore';
-  });
-};
-
-const loadMore = () => {
-  if (loadStatus.value === 'nomore' || loading.value) return;
-  page.current++;
-  getList();
-};
-
-const isLowStock = (row) => {
-  const stock = Number(row?.unLockedQuantity ?? 0);
-  const warn = Number(row?.warnNum ?? 0);
-  return Number.isFinite(stock) && Number.isFinite(warn) && stock < warn;
-};
-
-onMounted(() => {
-  getList();
-});
-</script>
-
-<style scoped lang="scss">
-@import '@/styles/sales-common.scss';
-
-.qualified-record-container {
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-}
-
-.tag-type {
-  background-color: #e3f2fd;
-  color: #2196f3;
-  padding: 2px 8px;
-  border-radius: 4px;
-  font-size: 12px;
-}
-
-.ledger-list {
-  flex: 1;
-  overflow-y: auto;
-}
-
-.low-stock {
-  background-color: #fde2e2;
-  color: #c45656;
-}
-
-.no-data {
-  padding-top: 100px;
-}
-</style>
diff --git a/src/pages/inventoryManagement/stockManagement/Record.vue b/src/pages/inventoryManagement/stockManagement/Record.vue
new file mode 100644
index 0000000..e4542a8
--- /dev/null
+++ b/src/pages/inventoryManagement/stockManagement/Record.vue
@@ -0,0 +1,292 @@
+<template>
+  <view class="record-container">
+    <view class="search-section">
+      <view class="search-bar">
+        <view class="search-input">
+          <up-input
+            class="search-text"
+            placeholder="璇疯緭鍏ヤ骇鍝佸ぇ绫�"
+            v-model="searchForm.productName"
+            @confirm="handleQuery"
+            clearable
+          />
+        </view>
+        <view class="filter-button" @click="handleQuery">
+          <up-icon name="search" size="24" color="#999"></up-icon>
+        </view>
+      </view>
+    </view>
+
+    <scroll-view scroll-y class="ledger-list" v-if="tableData.length > 0" @scrolltolower="loadMore">
+      <view v-for="item in tableData" :key="item.id" class="ledger-item">
+        <view class="item-header">
+          <view class="item-left">
+            <view class="document-icon">
+              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
+            </view>
+            <text class="item-id">{{ item.productName }}</text>
+          </view>
+        </view>
+        
+        <up-divider></up-divider>
+
+        <view class="item-details">
+          <view class="detail-row">
+            <text class="detail-label">瑙勬牸鍨嬪彿</text>
+            <text class="detail-value">{{ item.model }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">鍗曚綅</text>
+            <text class="detail-value">{{ item.unit }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">鎵瑰彿</text>
+            <text class="detail-value">{{ item.batchNo }}</text>
+          </view>
+          
+          <view class="quantity-section">
+            <view class="quantity-box qualified">
+              <text class="q-label">鍚堟牸搴撳瓨</text>
+              <text class="q-value">{{ item.qualifiedQuantity }}</text>
+            </view>
+            <view class="quantity-box unqualified">
+              <text class="q-label">涓嶅悎鏍煎簱瀛�</text>
+              <text class="q-value">{{ item.unQualifiedQuantity }}</text>
+            </view>
+          </view>
+
+          <view class="quantity-section">
+            <view class="quantity-box locked">
+              <text class="q-label">鍚堟牸鍐荤粨</text>
+              <text class="q-value">{{ item.qualifiedLockedQuantity }}</text>
+            </view>
+            <view class="quantity-box locked">
+              <text class="q-label">涓嶅悎鏍煎喕缁�</text>
+              <text class="q-value">{{ item.unQualifiedLockedQuantity }}</text>
+            </view>
+          </view>
+
+          <view class="detail-row">
+            <text class="detail-label">搴撳瓨棰勮</text>
+            <text class="detail-value">{{ item.warnNum }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">澶囨敞</text>
+            <text class="detail-value">{{ item.remark || '-' }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="detail-label">鏇存柊鏃堕棿</text>
+            <text class="detail-value">{{ item.updateTime }}</text>
+          </view>
+        </view>
+      </view>
+      <up-loadmore :status="loadStatus" />
+    </scroll-view>
+    <view v-else-if="!loading" class="no-data">
+      <up-empty mode="data" text="鏆傛棤搴撳瓨鏁版嵁"></up-empty>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue';
+import { getStockInventoryListPageCombined } from "@/api/inventoryManagement/stockInventory.js";
+
+const props = defineProps({
+  productId: {
+    type: Number,
+    required: true
+  }
+});
+
+const tableData = ref([]);
+const loading = ref(false);
+const loadStatus = ref('loadmore');
+const page = reactive({ current: 1, size: 10 });
+const total = ref(0);
+const searchForm = reactive({ 
+  productName: '',
+  topParentProductId: props.productId
+});
+
+const handleQuery = () => {
+  page.current = 1;
+  tableData.value = [];
+  getList();
+};
+
+const getList = () => {
+  if (loading.value) return;
+  loading.value = true;
+  loadStatus.value = 'loading';
+  
+  getStockInventoryListPageCombined({ 
+    ...searchForm, 
+    current: page.current,
+    size: page.size
+  }).then(res => {
+    loading.value = false;
+    const records = res.data.records || [];
+    tableData.value = page.current === 1 ? records : [...tableData.value, ...records];
+    total.value = res.data.total;
+    loadStatus.value = tableData.value.length >= total.value ? 'nomore' : 'loadmore';
+  }).catch(() => {
+    loading.value = false;
+    loadStatus.value = 'loadmore';
+  });
+};
+
+const loadMore = () => {
+  if (loadStatus.value === 'loadmore') {
+    page.current++;
+    getList();
+  }
+};
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped lang="scss">
+.record-container {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  background-color: #f5f7fa;
+}
+
+.search-section {
+  padding: 20rpx;
+  background-color: #ffffff;
+  position: sticky;
+  top: 0;
+  z-index: 10;
+}
+
+.search-bar {
+  display: flex;
+  align-items: center;
+  background-color: #f2f2f2;
+  border-radius: 40rpx;
+  padding: 0 30rpx;
+  height: 80rpx;
+}
+
+.search-input {
+  flex: 1;
+}
+
+.search-text {
+  font-size: 28rpx;
+}
+
+.filter-button {
+  padding-left: 20rpx;
+}
+
+.ledger-list {
+  flex: 1;
+  padding: 20rpx;
+  box-sizing: border-box;
+}
+
+.ledger-item {
+  background-color: #ffffff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+}
+
+.item-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+
+.item-left {
+  display: flex;
+  align-items: center;
+}
+
+.document-icon {
+  width: 40rpx;
+  height: 40rpx;
+  background: linear-gradient(135deg, #2979ff, #1565c0);
+  border-radius: 8rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16rpx;
+}
+
+.item-id {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #303133;
+}
+
+.item-details {
+  .detail-row {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 16rpx;
+    font-size: 26rpx;
+
+    .detail-label {
+      color: #909399;
+    }
+
+    .detail-value {
+      color: #303133;
+      font-weight: 500;
+    }
+  }
+}
+
+.quantity-section {
+  display: flex;
+  gap: 20rpx;
+  margin: 20rpx 0;
+  
+  .quantity-box {
+    flex: 1;
+    padding: 16rpx;
+    border-radius: 8rpx;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    
+    .q-label {
+      font-size: 22rpx;
+      margin-bottom: 8rpx;
+    }
+    
+    .q-value {
+      font-size: 32rpx;
+      font-weight: bold;
+    }
+    
+    &.qualified {
+      background-color: #ecf5ff;
+      color: #409eff;
+    }
+    
+    &.unqualified {
+      background-color: #fef0f0;
+      color: #f56c6c;
+    }
+    
+    &.locked {
+      background-color: #f4f4f5;
+      color: #909399;
+    }
+  }
+}
+
+.no-data {
+  padding-top: 200rpx;
+}
+</style>
diff --git a/src/pages/inventoryManagement/stockManagement/Unqualified.vue b/src/pages/inventoryManagement/stockManagement/Unqualified.vue
deleted file mode 100644
index 48dafc4..0000000
--- a/src/pages/inventoryManagement/stockManagement/Unqualified.vue
+++ /dev/null
@@ -1,134 +0,0 @@
-<template>
-  <view class="unqualified-record-container">
-    <view class="search-section">
-      <view class="search-bar">
-        <view class="search-input">
-          <up-input
-            class="search-text"
-            placeholder="璇疯緭鍏ヤ骇鍝佸ぇ绫�"
-            v-model="searchForm.productName"
-            @confirm="handleQuery"
-            clearable
-          />
-        </view>
-        <view class="filter-button" @click="handleQuery">
-          <up-icon name="search" size="24" color="#999"></up-icon>
-        </view>
-      </view>
-    </view>
-
-    <scroll-view scroll-y class="ledger-list" v-if="tableData.length > 0" @scrolltolower="loadMore">
-      <view v-for="item in tableData" :key="item.id" class="ledger-item">
-        <view class="item-header">
-          <view class="item-left">
-            <view class="document-icon">
-              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
-            </view>
-            <text class="item-id">{{ item.productName }}</text>
-          </view>
-          <view class="item-right">
-            <text class="item-tag tag-type" style="background-color: #fde2e2; color: #f56c6c;">涓嶅悎鏍煎簱瀛�</text>
-          </view>
-        </view>
-        
-        <up-divider></up-divider>
-
-        <view class="item-details">
-          <view class="detail-row">
-            <text class="detail-label">瑙勬牸鍨嬪彿</text>
-            <text class="detail-value">{{ item.model }}</text>
-          </view>
-          <view class="detail-row">
-            <text class="detail-label">搴撳瓨鏁伴噺</text>
-            <text class="detail-value">{{ item.qualitity }} {{ item.unit }}</text>
-          </view>
-          <view class="detail-row">
-            <text class="detail-label">閿佸畾/鍐荤粨</text>
-            <text class="detail-value">{{ item.lockedQuantity }} {{ item.unit }}</text>
-          </view>
-          <view class="detail-row">
-            <text class="detail-label">鍙敤搴撳瓨</text>
-            <text class="detail-value" style="color: #f56c6c; font-weight: bold;">{{ item.unLockedQuantity }} {{ item.unit }}</text>
-          </view>
-          <view class="detail-row">
-            <text class="detail-label">鏇存柊鏃堕棿</text>
-            <text class="detail-value">{{ item.updateTime }}</text>
-          </view>
-        </view>
-      </view>
-      <up-loadmore :status="loadStatus" />
-    </scroll-view>
-    <view v-else-if="!loading" class="no-data">
-      <up-empty mode="data" text="鏆傛棤涓嶅悎鏍煎簱瀛樻暟鎹�"></up-empty>
-    </view>
-  </view>
-</template>
-
-<script setup>
-import { ref, reactive, onMounted } from 'vue';
-import { getStockUninventoryListPage } from "@/api/inventoryManagement/stockUninventory.js";
-
-const tableData = ref([]);
-const loading = ref(false);
-const loadStatus = ref('loadmore');
-const page = reactive({ current: 1, size: 10 });
-const total = ref(0);
-const searchForm = reactive({ productName: '' });
-
-const handleQuery = () => {
-  page.current = 1;
-  tableData.value = [];
-  getList();
-};
-
-const getList = () => {
-  if (loading.value) return;
-  loading.value = true;
-  loadStatus.value = 'loading';
-  getStockUninventoryListPage({ ...searchForm, ...page, type: 'unqualified' }).then(res => {
-    loading.value = false;
-    const records = res.data.records || [];
-    tableData.value = page.current === 1 ? records : [...tableData.value, ...records];
-    total.value = res.data.total;
-    loadStatus.value = tableData.value.length >= total.value ? 'nomore' : 'loadmore';
-  }).catch(() => {
-    loading.value = false;
-    loadStatus.value = 'loadmore';
-  });
-};
-
-const loadMore = () => {
-  if (loadStatus.value === 'nomore' || loading.value) return;
-  page.current++;
-  getList();
-};
-
-onMounted(() => {
-  getList();
-});
-</script>
-
-<style scoped lang="scss">
-@import '@/styles/sales-common.scss';
-
-.unqualified-record-container {
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-}
-
-.tag-type {
-  padding: 2px 8px;
-  border-radius: 4px;
-  font-size: 12px;
-}
-
-.ledger-list {
-  flex: 1;
-  overflow-y: auto;
-}
-
-.no-data {
-  padding-top: 100px;
-}
-</style>
diff --git a/src/pages/inventoryManagement/stockManagement/index.vue b/src/pages/inventoryManagement/stockManagement/index.vue
index 98ebf44..45d27ad 100644
--- a/src/pages/inventoryManagement/stockManagement/index.vue
+++ b/src/pages/inventoryManagement/stockManagement/index.vue
@@ -1,57 +1,104 @@
 <template>
   <view class="app-container">
-    <PageHeader title="搴撳瓨绠$悊" @back="goBack" />
-    <up-tabs :list="tabs" @click="handleTabClick" :current="activeTab"/>
-    <swiper class="swiper-box" :current="activeTab" @change="handleSwiperChange">
-      <swiper-item class="swiper-item">
-        <qualified-record />
-      </swiper-item>
-      <swiper-item class="swiper-item">
-        <unqualified-record />
-      </swiper-item>
-    </swiper>
+    <PageHeader title="搴撳瓨绠$悊"
+                @back="goBack" />
+    <view v-if="loading"
+          class="loading-state">
+      <up-loading-icon text="鍔犺浇涓�..."></up-loading-icon>
+    </view>
+    <template v-else>
+      <up-tabs :list="tabs"
+               @click="handleTabClick"
+               :current="activeTab" />
+      <swiper class="swiper-box"
+              :current="activeTab"
+              @change="handleSwiperChange">
+        <swiper-item class="swiper-item"
+                     v-for="tab in products"
+                     :key="tab.id">
+          <record :product-id="tab.id"
+                  v-if="activeTab === products.indexOf(tab) || initializedTabs.includes(tab.id)" />
+        </swiper-item>
+      </swiper>
+    </template>
   </view>
 </template>
 
 <script setup>
-import { ref } from 'vue';
-import PageHeader from "@/components/PageHeader.vue";
-import QualifiedRecord from "./Qualified.vue";
-import UnqualifiedRecord from "./Unqualified.vue";
+  import { ref, onMounted } from "vue";
+  import PageHeader from "@/components/PageHeader.vue";
+  import Record from "./Record.vue";
+  import { productTreeList } from "@/api/basicData/product.js";
 
-const activeTab = ref(0);
-const tabs = ref([
-  { name: '鍚堟牸搴撳瓨' },
-  { name: '涓嶅悎鏍煎簱瀛�' }
-]);
+  const activeTab = ref(0);
+  const tabs = ref([]);
+  const products = ref([]);
+  const loading = ref(false);
+  const initializedTabs = ref([]);
 
-const handleTabClick = (item) => {
-  activeTab.value = item.index;
-};
+  const handleTabClick = item => {
+    activeTab.value = item.index;
+    if (!initializedTabs.value.includes(products.value[item.index].id)) {
+      initializedTabs.value.push(products.value[item.index].id);
+    }
+  };
 
-const handleSwiperChange = (e) => {
-  activeTab.value = e.detail.current;
-};
+  const handleSwiperChange = e => {
+    const index = e.detail.current;
+    activeTab.value = index;
+    if (!initializedTabs.value.includes(products.value[index].id)) {
+      initializedTabs.value.push(products.value[index].id);
+    }
+  };
 
-const goBack = () => {
-  uni.navigateBack();
-};
+  const fetchProducts = async () => {
+    loading.value = true;
+    try {
+      const res = await productTreeList();
+      // 杩囨护鏍硅妭鐐逛骇鍝�
+      products.value = res
+        .filter(item => item.parentId === null)
+        .map(({ id, productName }) => ({ id, productName }));
+      tabs.value = products.value.map(p => ({ name: p.productName }));
+
+      if (products.value.length > 0) {
+        activeTab.value = 0;
+        initializedTabs.value = [products.value[0].id];
+      }
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  onMounted(() => {
+    fetchProducts();
+  });
 </script>
 
 <style scoped lang="scss">
-.app-container {
-  display: flex;
-  flex-direction: column;
-  height: 100vh;
-  background-color: #f8f9fa;
-}
-.swiper-box {
-  flex: 1;
-}
-.swiper-item {
-  height: 100%;
-}
-:deep(.up-tabs) {
-  background-color: #fff;
-}
+  .app-container {
+    display: flex;
+    flex-direction: column;
+    height: 100vh;
+    background-color: #f8f9fa;
+  }
+  .loading-state {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  .swiper-box {
+    flex: 1;
+  }
+  .swiper-item {
+    height: 100%;
+  }
+  :deep(.up-tabs) {
+    background-color: #fff;
+  }
 </style>

--
Gitblit v1.9.3