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