From a308b5b28bff5f745bed6fc002bc2d29206590f4 Mon Sep 17 00:00:00 2001
From: huminmin <mac@MacBook-Pro.local>
Date: 星期五, 24 四月 2026 14:16:38 +0800
Subject: [PATCH] 库存管理按照原材料和成品来区分

---
 src/views/inventoryManagement/stockManagement/New.vue           |   27 ++-
 src/views/inventoryManagement/stockManagement/FrozenAndThaw.vue |   60 ++++++--
 src/views/inventoryManagement/stockManagement/index.vue         |   57 ++++---
 src/views/inventoryManagement/stockManagement/Record.vue        |  224 ++++++++++++++++++++++++++++++++
 src/views/inventoryManagement/stockManagement/Subtract.vue      |   37 ++++-
 src/api/inventoryManagement/stockInventory.js                   |    9 +
 6 files changed, 361 insertions(+), 53 deletions(-)

diff --git a/src/api/inventoryManagement/stockInventory.js b/src/api/inventoryManagement/stockInventory.js
index aed71e2..027b9fa 100644
--- a/src/api/inventoryManagement/stockInventory.js
+++ b/src/api/inventoryManagement/stockInventory.js
@@ -17,6 +17,15 @@
     });
 };
 
+// 鍒嗛〉鏌ヨ鑱斿悎搴撳瓨璁板綍鍒楄〃锛堝寘鍚晢鍝佷俊鎭級
+export const getStockInventoryListPageCombined = (params) => {
+    return request({
+        url: "/stockInventory/pageListCombinedStockInventory",
+        method: "get",
+        params,
+    });
+};
+
 // 鍑忓皯搴撳瓨璁板綍
 export const subtractStockInventory = (params) => {
     return request({
diff --git a/src/views/inventoryManagement/stockManagement/FrozenAndThaw.vue b/src/views/inventoryManagement/stockManagement/FrozenAndThaw.vue
index 463cb83..a7a9400 100644
--- a/src/views/inventoryManagement/stockManagement/FrozenAndThaw.vue
+++ b/src/views/inventoryManagement/stockManagement/FrozenAndThaw.vue
@@ -8,10 +8,27 @@
     >
       <el-form label-width="140px" :model="formState" ref="formRef">
         <el-form-item
+            label="搴撳瓨绫诲瀷"
+            prop="type"
+            :rules="[
+                {
+                required: true,
+                message: '璇烽�夋嫨搴撳瓨绫诲瀷',
+                trigger: 'change',
+              }
+            ]"
+        >
+          <el-select v-model="formState.type" placeholder="璇烽�夋嫨搴撳瓨绫诲瀷" @change="handleChangeType">
+            <el-option label="鍚堟牸搴撳瓨" value="qualified" :disabled="(operationType === 'frozen' && props.record.qualifiedUnLockedQuantity <= 0) || (operationType === 'thaw' && props.record.qualifiedLockedQuantity <= 0)" />
+            <el-option label="涓嶅悎鏍煎簱瀛�" value="unqualified" :disabled="(operationType === 'frozen' && props.record.unQualifiedUnLockedQuantity <= 0) || (operationType === 'thaw' && props.record.unQualifiedLockedQuantity <= 0)" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item
             :label="operationType === 'frozen' ? '鍐荤粨鏁伴噺锛�' : '瑙e喕鏁伴噺锛�'"
             prop="lockedQuantity"
         >
-          <el-input-number v-model="formState.lockedQuantity" :step="1" :min="1" precision="0" style="width: 100%" :max="maxCount" />
+          <el-input-number v-model="formState.lockedQuantity" :step="1" :min="maxCount > 0 ? 1 : 0" precision="0" style="width: 100%" :max="maxCount" :disabled="maxCount < 1" />
         </el-form-item>
       </el-form>
 
@@ -26,7 +43,7 @@
 </template>
 
 <script setup>
-import {ref, computed, getCurrentInstance} from "vue";
+import {ref, computed, getCurrentInstance, onMounted} from "vue";
 import {frozenStockInventory, thawStockInventory} from "@/api/inventoryManagement/stockInventory.js";
 import {frozenStockUninventory, thawStockUninventory} from "@/api/inventoryManagement/stockUninventory.js";
 
@@ -42,12 +59,6 @@
     default: 'frozen',
   },
 
-  type: {
-    type: String,
-    required: true,
-    default: 'qualified',
-  },
-
   record: {
     type: Object,
     default: () => {},
@@ -58,7 +69,8 @@
 
 // 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
 const formState = ref({
-  lockedQuantity: 0,
+  type: undefined,
+  lockedQuantity: undefined,
 });
 
 const isShow = computed({
@@ -76,7 +88,8 @@
 const closeModal = () => {
   // 閲嶇疆琛ㄥ崟鏁版嵁
   formState.value = {
-    lockedQuantity: undefined
+    lockedQuantity: undefined,
+    type: undefined,
   };
   isShow.value = false;
 };
@@ -84,17 +97,32 @@
 const maxCount = computed(() => {
   // 鍐荤粨搴撳瓨鏈�澶ф暟閲忎负鏈В鍐绘暟閲�
   if (props.operationType === 'frozen') {
-    return props.record.unLockedQuantity
+    // 鍐荤粨鍚堟牸搴撳瓨鏈�澶ф暟閲忎负鏈В鍐诲悎鏍兼暟閲�
+    if (formState.value.type === 'qualified') {
+      return Math.max(0, props.record.qualifiedUnLockedQuantity || 0)
+    }
+    // 鍐荤粨涓嶅悎鏍煎簱瀛樻渶澶ф暟閲忎负鏈В鍐讳笉鍚堟牸鏁伴噺
+    return Math.max(0, props.record.unQualifiedUnLockedQuantity || 0)
   }
   // 瑙e喕搴撳瓨鏈�澶ф暟閲忎负宸插喕缁撴暟閲�
-  return props.record.lockedQuantity
+  if (formState.value.type === 'qualified') {
+    // 瑙e喕鍚堟牸搴撳瓨鏈�澶ф暟閲忎负宸插喕缁撳悎鏍兼暟閲�
+    return Math.max(0, props.record.qualifiedLockedQuantity || 0)
+  }
+  // 瑙e喕涓嶅悎鏍煎簱瀛樻渶澶ф暟閲忎负宸插喕缁撲笉鍚堟牸鏁伴噺
+  return Math.max(0, props.record.unQualifiedLockedQuantity || 0)
 })
+
+const handleChangeType = (type) => {
+  formState.value.lockedQuantity = maxCount.value;
+}
 
 const handleSubmit = () => {
   proxy.$refs["formRef"].validate(valid => {
     if (valid) {
-      const data = Object.assign({id: props.record.id}, formState.value);
-      if (props.type === 'qualified') {
+      const data = Object.assign({}, formState.value);
+      if (formState.value.type === 'qualified') {
+        data.id = props.record.qualifiedId;
         // 鍐荤粨
         if (props.operationType === 'frozen') {
           frozenStockInventory(data).then(res => {
@@ -122,6 +150,7 @@
           })
         }
       } else {
+        data.id = props.record.unQualifiedId;
         if (props.operationType === 'frozen') {
           frozenStockUninventory(data).then(res => {
             if (res.code === 200) {
@@ -153,7 +182,6 @@
 };
 
 onMounted(() => {
-  formState.value.lockedQuantity = maxCount.value;
 })
 
 defineExpose({
@@ -161,4 +189,4 @@
   handleSubmit,
   isShow,
 });
-</script>
+</script>
\ No newline at end of file
diff --git a/src/views/inventoryManagement/stockManagement/New.vue b/src/views/inventoryManagement/stockManagement/New.vue
index d4ea20b..5d832c0 100644
--- a/src/views/inventoryManagement/stockManagement/New.vue
+++ b/src/views/inventoryManagement/stockManagement/New.vue
@@ -44,6 +44,23 @@
           <el-input v-model="formState.unit"  disabled />
         </el-form-item>
 
+        <el-form-item
+            label="搴撳瓨绫诲瀷"
+            prop="type"
+            :rules="[
+                {
+                required: true,
+                message: '璇烽�夋嫨搴撳瓨绫诲瀷',
+                trigger: 'change',
+              }
+            ]"
+        >
+          <el-select v-model="formState.type" placeholder="璇烽�夋嫨搴撳瓨绫诲瀷">
+            <el-option label="鍚堟牸搴撳瓨" value="qualified" />
+            <el-option label="涓嶅悎鏍煎簱瀛�" value="unqualified" />
+          </el-select>
+        </el-form-item>
+
         <el-form-item label="鎵瑰彿" prop="batchNo">
           <el-input
             v-model="formState.batchNo"
@@ -98,13 +115,7 @@
   visible: {
     type: Boolean,
     required: true,
-  },
-
-  type: {
-    type: String,
-    required: true,
-    default: 'qualified',
-  },
+  }
 });
 
 const emit = defineEmits(['update:visible', 'completed']);
@@ -118,6 +129,7 @@
   materialCode: "",
   unit: "",
   batchNo: "",
+  type: undefined,
   qualitity: 0,
   warnNum: 0,
   remark: '',
@@ -146,6 +158,7 @@
     materialCode: "",
     unit: "",
     batchNo: "",
+    type: undefined,
     qualitity: 0,
     warnNum: 0,
     remark: "",
diff --git a/src/views/inventoryManagement/stockManagement/Record.vue b/src/views/inventoryManagement/stockManagement/Record.vue
new file mode 100644
index 0000000..5b0a5f1
--- /dev/null
+++ b/src/views/inventoryManagement/stockManagement/Record.vue
@@ -0,0 +1,224 @@
+<template>
+  <div class="app-container">
+    <div class="search_form">
+      <div>
+        <span class="search_title ml10">浜у搧澶х被锛�</span>
+        <el-input v-model="searchForm.productName"
+                  style="width: 240px"
+                  placeholder="璇疯緭鍏�"
+                  clearable/>
+        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">鎼滅储</el-button>
+      </div>
+      <div>
+         <el-button type="primary" @click="isShowNewModal = true">鏂板搴撳瓨</el-button>
+        <el-button type="info" plain icon="Upload" @click="isShowImportModal = true">
+          瀵煎叆搴撳瓨
+        </el-button>
+        <el-button @click="handleOut">瀵煎嚭</el-button>
+      </div>
+    </div>
+    <div class="table_list">
+      <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
+        :expand-row-keys="expandedRowKeys" :row-key="(row, index) => index" style="width: 100%"
+        :row-class-name="tableRowClassName" height="calc(100vh - 18.5em)">
+        <el-table-column align="center" type="selection" width="55" />
+        <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+        <el-table-column label="浜у搧澶х被" prop="productName" show-overflow-tooltip />
+        <el-table-column label="瑙勬牸鍨嬪彿" prop="model" show-overflow-tooltip />
+        <el-table-column label="鍗曚綅" prop="unit" show-overflow-tooltip />
+        <el-table-column label="鏂欏彿" prop="materialCode" show-overflow-tooltip />
+        <el-table-column label="鍚堟牸鎵瑰彿" prop="qualifiedBatchNo" show-overflow-tooltip />
+        <el-table-column label="涓嶅悎鏍兼壒鍙�" prop="unQualifiedBatchNo" show-overflow-tooltip />
+        <el-table-column label="鍚堟牸搴撳瓨鏁伴噺" prop="qualifiedQuantity" show-overflow-tooltip />
+        <el-table-column label="涓嶅悎鏍煎簱瀛樻暟閲�" prop="unQualifiedQuantity" show-overflow-tooltip />
+        <el-table-column label="鍚堟牸鍐荤粨鏁伴噺" prop="qualifiedLockedQuantity" show-overflow-tooltip />
+        <el-table-column label="涓嶅悎鏍煎喕缁撴暟閲�" prop="unQualifiedLockedQuantity" show-overflow-tooltip />
+        <el-table-column label="搴撳瓨棰勮鏁伴噺" prop="warnNum"  show-overflow-tooltip />
+        <el-table-column label="澶囨敞" prop="remark"  show-overflow-tooltip />
+        <el-table-column label="鏈�杩戞洿鏂版椂闂�" prop="updateTime" show-overflow-tooltip />
+        <el-table-column fixed="right" label="鎿嶄綔" min-width="90" align="center">
+          <template #default="scope">
+            <el-button link type="primary" @click="showSubtractModal(scope.row)" :disabled="scope.row.unQualifiedUnLockedQuantity === 0 && scope.row.qualifiedUnLockedQuantity === 0">棰嗙敤</el-button>
+            <el-button link type="primary" v-if="scope.row.unQualifiedUnLockedQuantity > 0 || scope.row.qualifiedUnLockedQuantity > 0" @click="showFrozenModal(scope.row)">鍐荤粨</el-button>
+            <el-button link type="primary" v-if="scope.row.qualifiedLockedQuantity > 0 || scope.row.unQualifiedLockedQuantity > 0" @click="showThawModal(scope.row)">瑙e喕</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper"
+        :page="page.current" :limit="page.size" @pagination="paginationChange" />
+    </div>
+    <new-stock-inventory v-if="isShowNewModal"
+                 v-model:visible="isShowNewModal"
+                 :top-product-parent-id="props.productId"
+                 @completed="handleQuery" />
+
+    <subtract-stock-inventory v-if="isShowSubtractModal"
+                 v-model:visible="isShowSubtractModal"
+                 :record="record"
+                 :type="record.stockType"
+                 @completed="handleQuery" />
+    <!-- 瀵煎叆搴撳瓨-->
+    <import-stock-inventory v-if="isShowImportModal"
+                 v-model:visible="isShowImportModal"
+                 @uploadSuccess="handleQuery" />
+    <!-- 鍐荤粨/瑙e喕搴撳瓨-->
+    <frozen-and-thaw-stock-inventory v-if="isShowFrozenAndThawModal"
+                 v-model:visible="isShowFrozenAndThawModal"
+                 :record="record"
+                 :operation-type="operationType"
+                 :type="record.stockType"
+                 @completed="handleQuery" />
+  </div>
+</template>
+
+<script setup>
+import pagination from '@/components/PIMTable/Pagination.vue'
+import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue'
+import {ElMessage, ElMessageBox} from "element-plus";
+import {
+  getStockInventoryListPageCombined
+} from "@/api/inventoryManagement/stockInventory.js";
+const props = defineProps({
+  productId: {
+    type: Number,
+    required: true,
+    default: 0
+  }
+});
+
+const NewStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/New.vue"));
+const SubtractStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/Subtract.vue"));
+const ImportStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/Import.vue"));
+const FrozenAndThawStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/FrozenAndThaw.vue"));
+const { proxy } = getCurrentInstance()
+const tableData = ref([])
+const selectedRows = ref([])
+const record = ref({})
+const tableLoading = ref(false)
+const page = reactive({
+  current: 1,
+  size: 100,
+})
+const total = ref(0)
+// 鏄惁鏄剧ず鏂板寮规
+const isShowNewModal = ref(false)
+// 鏄惁鏄剧ず棰嗙敤寮规
+const isShowSubtractModal = ref(false)
+// 鏄惁鏄剧ず鍐荤粨/瑙e喕寮规
+const isShowFrozenAndThawModal = ref(false)
+// 鎿嶄綔绫诲瀷
+const operationType = ref('frozen')
+// 鏄惁鏄剧ず瀵煎叆寮规
+const isShowImportModal = ref(false)
+const data = reactive({
+  searchForm: {
+    productName: '',
+    topParentProductId: props.productId,
+  }
+})
+const { searchForm } = toRefs(data)
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+  page.current = 1
+  getList()
+}
+const paginationChange = (obj) => {
+  page.current = obj.page;
+  page.size = obj.limit;
+  getList()
+}
+const getList = () => {
+  tableLoading.value = true
+  getStockInventoryListPageCombined({ ...searchForm.value, ...page }).then(res => {
+    tableLoading.value = false
+    tableData.value = res.data.records
+    total.value = res.data.total
+    // 鏁版嵁鍔犺浇瀹屾垚鍚庢鏌ュ簱瀛�
+    // checkStockAndCreatePurchase();
+  }).catch(() => {
+    tableLoading.value = false
+  })
+}
+
+const handleFileSuccess = (response) => {
+  const { code, msg } = response;
+  if (code == 200) {
+    ElMessage({ message: "瀵煎叆鎴愬姛", type: "success" });
+    upload.open = false;
+    emits("uploadSuccess");
+  } else {
+    ElMessage({ message: msg, type: "error" });
+  }
+};
+
+// 鐐瑰嚮棰嗙敤
+const showSubtractModal = (row) => {
+  record.value = row
+  isShowSubtractModal.value = true
+}
+
+// 鐐瑰嚮鍐荤粨
+const showFrozenModal = (row) => {
+  record.value = row
+  isShowFrozenAndThawModal.value = true
+  operationType.value = 'frozen'
+}
+
+// 鐐瑰嚮瑙e喕
+const showThawModal = (row) => {
+  record.value = row
+  isShowFrozenAndThawModal.value = true
+  operationType.value = 'thaw'
+}
+
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+  // 杩囨护鎺夊瓙鏁版嵁
+  selectedRows.value = selection.filter(item => item.id);
+  console.log('selection', selectedRows.value)
+}
+const expandedRowKeys = ref([])
+
+// 琛ㄦ牸琛岀被鍚�
+const tableRowClassName = ({ row }) => {
+  const stock = Number(row?.qualifiedUnLockedQuantity ?? 0);
+  const warn = Number(row?.warnNum ?? 0);
+  if (!Number.isFinite(stock) || !Number.isFinite(warn)) {
+    return '';
+  }
+  return stock < warn ? 'row-low-stock' : '';
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+  ElMessageBox.confirm(
+    '鏄惁纭瀵煎嚭锛�',
+    '瀵煎嚭', {
+    confirmButtonText: '纭',
+    cancelButtonText: '鍙栨秷',
+    type: 'warning',
+  }
+  ).then(() => {
+    proxy.download("/stockInventory/exportStockInventory", {topParentProductId: props.productId}, '搴撳瓨淇℃伅.xlsx')
+  }).catch(() => {
+    proxy.$modal.msg("宸插彇娑�")
+  })
+}
+
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style scoped lang="scss">
+:deep(.row-low-stock td) {
+  background-color: #fde2e2;
+  color: #c45656;
+}
+
+:deep(.row-low-stock:hover > td) {
+  background-color: #fcd4d4;
+}
+</style>
diff --git a/src/views/inventoryManagement/stockManagement/Subtract.vue b/src/views/inventoryManagement/stockManagement/Subtract.vue
index 62988d1..fce06c5 100644
--- a/src/views/inventoryManagement/stockManagement/Subtract.vue
+++ b/src/views/inventoryManagement/stockManagement/Subtract.vue
@@ -45,6 +45,23 @@
         </el-form-item>
 
         <el-form-item
+            label="搴撳瓨绫诲瀷"
+            prop="type"
+            :rules="[
+                {
+                required: true,
+                message: '璇烽�夋嫨搴撳瓨绫诲瀷',
+                trigger: 'change',
+              }
+            ]"
+        >
+          <el-select v-model="formState.type" placeholder="璇烽�夋嫨搴撳瓨绫诲瀷" @change="handleTypeChange">
+            <el-option label="鍚堟牸搴撳瓨" value="qualified" :disabled="props.record.qualifiedUnLockedQuantity <= 0" />
+            <el-option label="涓嶅悎鏍煎簱瀛�" value="unqualified" :disabled="props.record.unQualifiedUnLockedQuantity <= 0" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item
             label="鏁伴噺"
             prop="qualitity"
         >
@@ -87,11 +104,6 @@
     type: Object,
     default: () => {},
   },
-  type: {
-    type: String,
-    required: true,
-    default: 'qualified',
-  },
 });
 
 const emit = defineEmits(['update:visible', 'completed']);
@@ -101,8 +113,19 @@
 })
 
 const maxQuality = computed(() => {
-  return props.record.unLockedQuantity ? props.record.unLockedQuantity :  0;
+  let max = 0;
+  if (formState.value.type === 'qualified') {
+    max = props.record.qualifiedUnLockedQuantity ? props.record.qualifiedUnLockedQuantity :  0;
+  } else {
+    max = props.record.unQualifiedUnLockedQuantity ? props.record.unQualifiedUnLockedQuantity :  0;
+  }
+  // 纭繚 max 鑷冲皯涓� 1锛岄伩鍏� min > max 鐨勯敊璇�
+  return Math.max(max, 1);
 })
+
+const handleTypeChange = () => {
+  formState.value.qualitity = undefined;
+}
 
 const initFormData = () => {
   if (props.record) {
@@ -178,7 +201,7 @@
         proxy.$modal.msgError("璇烽�夋嫨瑙勬牸");
         return;
       }
-      if (props.type === 'qualified') {
+      if (formState.value.type === 'qualified') {
         subtractStockInventory(formState.value).then(res => {
           // 鍏抽棴妯℃�佹
           isShow.value = false;
diff --git a/src/views/inventoryManagement/stockManagement/index.vue b/src/views/inventoryManagement/stockManagement/index.vue
index 347de38..b3aa7ee 100644
--- a/src/views/inventoryManagement/stockManagement/index.vue
+++ b/src/views/inventoryManagement/stockManagement/index.vue
@@ -1,33 +1,44 @@
 <template>
   <div class="app-container">
-    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
-      <el-tab-pane v-for="tab in tabs"
-                   :label="tab.label"
-                   :name="tab.name"
-                   :key="tab.name">
-        <component :is="tab.name === 'qualified' ? QualifiedRecord : UnqualifiedRecord" />
-      </el-tab-pane>
-    </el-tabs>
+    <div v-loading="loading" element-loading-text="鍔犺浇涓�..." style="min-height: 80vh;">
+      <el-tabs v-model="activeTab" @tab-change="handleTabChange" v-if="!loading">
+        <el-tab-pane v-for="tab in products"
+                     :label="tab.productName"
+                     :name="tab.id"
+                     :key="tab.id">
+          <Record :product-id="tab.id" v-if="tab.id === activeTab" />
+        </el-tab-pane>
+      </el-tabs>
+    </div>
   </div>
 </template>
 
 <script setup>
-import QualifiedRecord from "@/views/inventoryManagement/stockManagement/Qualified.vue";
-import UnqualifiedRecord from "@/views/inventoryManagement/stockManagement/Unqualified.vue";
-
-const activeTab = ref('qualified')
-const tabs = ref([
-  {
-    label: '鍚堟牸搴撳瓨',
-    name: 'qualified'
-  },
-  {
-    label: '涓嶅悎鏍煎簱瀛�',
-    name: 'unqualified'
-  }
-])
+import { ref, onMounted } from 'vue';
+import { productTreeList } from "@/api/basicData/product.js";
+import Record from "@/views/inventoryManagement/stockManagement/Record.vue";
+const products = ref([])
+const activeTab = ref(null)
+const loading = ref(false)
 
 const handleTabChange = (tabName) => {
   activeTab.value = tabName;
 }
-</script>
+
+const fetchProducts = async () => {
+  loading.value = true;
+  try {
+    const res = await productTreeList();
+    products.value = res.filter((item) => item.parentId === null).map(({ id, productName }) => ({ id, productName }));
+    if (products.value.length > 0) {
+      activeTab.value = products.value[0].id;
+    }
+  } finally {
+    loading.value = false;
+  }
+}
+
+onMounted(() => {
+  fetchProducts();
+})
+</script>
\ No newline at end of file

--
Gitblit v1.9.3