From 6ca03cc8ac229c13702ce699bab4db6e93f3f172 Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期二, 30 六月 2026 14:19:12 +0800
Subject: [PATCH] 1、供应商管理新增修改删除功能2、库存管理增加添加功能、详情、领用功能

---
 src/pages.json                                                       |   30 +
 src/pages/inventoryManagement/stockManagement/detail.vue             |  294 +++++++++++++
 src/pages/inventoryManagement/stockManagement/selectProductModel.vue |  228 ++++++++++
 src/api/basicData/product.js                                         |   80 ++-
 src/api/inventoryManagement/stockUninventory.js                      |   68 +-
 src/pages/inventoryManagement/stockManagement/add.vue                |  155 +++++++
 src/pages/inventoryManagement/stockManagement/use.vue                |  183 ++++++++
 src/api/inventoryManagement/stockInventory.js                        |   24 +
 src/pages/inventoryManagement/stockManagement/Record.vue             |  205 +++++++++
 9 files changed, 1,193 insertions(+), 74 deletions(-)

diff --git a/src/api/basicData/product.js b/src/api/basicData/product.js
index 10f8fd1..fbd0e5b 100644
--- a/src/api/basicData/product.js
+++ b/src/api/basicData/product.js
@@ -1,58 +1,66 @@
 // 浜у搧缁存姢椤甸潰鎺ュ彛
-import request from '@/utils/request'
+import request from "@/utils/request";
 
 // 浜у搧鏍戞煡璇�
 export function productTreeList(query) {
-    return request({
-        url: '/basic/product/list',
-        method: 'get',
-        params: query
-    })
+  return request({
+    url: "/basic/product/list",
+    method: "get",
+    params: query,
+  });
 }
 // 浜у搧鏍戞柊澧炰慨鏀�
 export function addOrEditProduct(query) {
-    return request({
-        url: '/basic/product/addOrEditProduct',
-        method: 'post',
-        data: query
-    })
+  return request({
+    url: "/basic/product/addOrEditProduct",
+    method: "post",
+    data: query,
+  });
 }
 // 瑙勬牸鍨嬪彿鏂板淇敼
 export function addOrEditProductModel(query) {
-    return request({
-        url: '/basic/product/addOrEditProductModel',
-        method: 'post',
-        data: query
-    })
+  return request({
+    url: "/basic/product/addOrEditProductModel",
+    method: "post",
+    data: query,
+  });
 }
 // 浜у搧鏍戝垹闄�
 export function delProduct(query) {
-    return request({
-        url: '/basic/product/delProduct',
-        method: 'delete',
-        data: query
-    })
+  return request({
+    url: "/basic/product/delProduct",
+    method: "delete",
+    data: query,
+  });
 }
 // 瑙勬牸鍨嬪彿鍒犻櫎
 export function delProductModel(query) {
-    return request({
-        url: '/basic/product/delProductModel',
-        method: 'delete',
-        data: query
-    })
+  return request({
+    url: "/basic/product/delProductModel",
+    method: "delete",
+    data: query,
+  });
 }
 // 瑙勬牸鍨嬪彿鏌ヨ
 export function modelList(query) {
-    return request({
-        url: '/basic/product/modelList',
-        method: 'get',
-        params: query
-    })
+  return request({
+    url: "/basic/product/modelList",
+    method: "get",
+    params: query,
+  });
 }
 export function modelListPage(query) {
-    return request({
-        url: '/basic/product/modelListPage',
-        method: 'get',
-        params: query
-    })
+  return request({
+    url: "/basic/product/modelListPage",
+    method: "get",
+    params: query,
+  });
+}
+
+export function pageModel(query) {
+  return request({
+    url: "/basic/product/pageModel",
+    method: "get",
+    params: query,
+  });
 }
diff --git a/src/api/inventoryManagement/stockInventory.js b/src/api/inventoryManagement/stockInventory.js
index 328657c..5dda278 100644
--- a/src/api/inventoryManagement/stockInventory.js
+++ b/src/api/inventoryManagement/stockInventory.js
@@ -17,6 +17,14 @@
   });
 };
 
+export const getStockInventoryBatchNoQty = params => {
+  return request({
+    url: "/stockInventory/getBatchNoQty",
+    method: "get",
+    params,
+  });
+};
+
 // 鍒涘缓搴撳瓨璁板綍
 export const createStockInventory = params => {
   return request({
@@ -26,6 +34,22 @@
   });
 };
 
+export const addStockInRecordOnly = params => {
+  return request({
+    url: "/stockInventory/addStockInRecordOnly",
+    method: "post",
+    data: params,
+  });
+};
+
+export const addStockOutRecordOnly = params => {
+  return request({
+    url: "/stockInventory/addStockOutRecordOnly",
+    method: "post",
+    data: params,
+  });
+};
+
 // 鍑忓皯搴撳瓨璁板綍
 export const subtractStockInventory = params => {
   return request({
diff --git a/src/api/inventoryManagement/stockUninventory.js b/src/api/inventoryManagement/stockUninventory.js
index fee8908..529759b 100644
--- a/src/api/inventoryManagement/stockUninventory.js
+++ b/src/api/inventoryManagement/stockUninventory.js
@@ -1,45 +1,53 @@
 import request from "@/utils/request";
 // 鍒嗛〉鏌ヨ搴撳瓨璁板綍鍒楄〃
-export const getStockUninventoryListPage = (params) => {
-    return request({
-        url: "/stockUninventory/pagestockUninventory",
-        method: "get",
-        params,
-    });
+export const getStockUninventoryListPage = params => {
+  return request({
+    url: "/stockUninventory/pagestockUninventory",
+    method: "get",
+    params,
+  });
 };
 
 // 鍒涘缓搴撳瓨璁板綍
-export const createStockUnInventory = (params) => {
-    return request({
-        url: "/stockUninventory/addstockUninventory",
-        method: "post",
-        data: params,
-    });
+export const createStockUnInventory = params => {
+  return request({
+    url: "/stockUninventory/addstockUninventory",
+    method: "post",
+    data: params,
+  });
 };
 
 // 鍑忓皯搴撳瓨璁板綍
-export const subtractStockUnInventory = (params) => {
-    return request({
-        url: "/stockUninventory/subtractstockUninventory",
-        method: "post",
-        data: params,
-    });
+export const subtractStockUnInventory = params => {
+  return request({
+    url: "/stockUninventory/subtractstockUninventory",
+    method: "post",
+    data: params,
+  });
 };
 
 // 鍐荤粨搴撳瓨璁板綍
-export const frozenStockUninventory = (params) => {
-    return request({
-        url: "/stockUninventory/frozenStock",
-        method: "post",
-        data: params,
-    });
+export const frozenStockUninventory = params => {
+  return request({
+    url: "/stockUninventory/frozenStock",
+    method: "post",
+    data: params,
+  });
 };
 
 // 瑙e喕搴撳瓨璁板綍
-export const thawStockUninventory = (params) => {
-    return request({
-        url: "/stockUninventory/thawStock",
-        method: "post",
-        data: params,
-    });
+export const thawStockUninventory = params => {
+  return request({
+    url: "/stockUninventory/thawStock",
+    method: "post",
+    data: params,
+  });
+};
+
+export const addUnqualifiedStockOutRecordOnly = params => {
+  return request({
+    url: "/stockUninventory/addStockOutRecordOnly",
+    method: "post",
+    data: params,
+  });
 };
diff --git a/src/pages.json b/src/pages.json
index 30a1a63..71f446c 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -936,6 +936,34 @@
       }
     },
     {
+      "path": "pages/inventoryManagement/stockManagement/add",
+      "style": {
+        "navigationBarTitleText": "鏂板搴撳瓨",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/inventoryManagement/stockManagement/detail",
+      "style": {
+        "navigationBarTitleText": "搴撳瓨璇︽儏",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/inventoryManagement/stockManagement/use",
+      "style": {
+        "navigationBarTitleText": "棰嗙敤",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/inventoryManagement/stockManagement/selectProductModel",
+      "style": {
+        "navigationBarTitleText": "閫夋嫨浜у搧",
+        "navigationStyle": "custom"
+      }
+    },
+    {
       "path": "pages/safeProduction/safeQualifications/index",
       "style": {
         "navigationBarTitleText": "瑙勭▼涓庤祫璐�",
@@ -1554,4 +1582,4 @@
     "navigationBarTitleText": "RuoYi",
     "navigationBarBackgroundColor": "#FFFFFF"
   }
-}
\ No newline at end of file
+}
diff --git a/src/pages/inventoryManagement/stockManagement/Record.vue b/src/pages/inventoryManagement/stockManagement/Record.vue
index e5d5ca2..087626f 100644
--- a/src/pages/inventoryManagement/stockManagement/Record.vue
+++ b/src/pages/inventoryManagement/stockManagement/Record.vue
@@ -91,6 +91,11 @@
             <text class="detail-value">{{ item.updateTime }}</text>
           </view>
         </view>
+        <view class="action-buttons">
+          <u-button size="small"
+                    class="action-btn"
+                    @click="openDetail(item)">璇︽儏</u-button>
+        </view>
       </view>
       <up-loadmore :status="loadStatus" />
     </scroll-view>
@@ -124,11 +129,17 @@
         </scroll-view>
       </view>
     </up-popup>
+    <view class="fab-button"
+          @click="openAddPopup">
+      <up-icon name="plus"
+               size="24"
+               color="#ffffff"></up-icon>
+    </view>
   </view>
 </template>
 
 <script setup>
-  import { ref, reactive, onMounted } from "vue";
+  import { ref, reactive, onMounted, onUnmounted } from "vue";
   import { getStockInventoryListPageCombined } from "@/api/inventoryManagement/stockInventory.js";
 
   const props = defineProps({
@@ -142,7 +153,6 @@
   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,
@@ -168,17 +178,18 @@
       size: page.size,
     })
       .then(res => {
-        loading.value = false;
-        const records = res.data.records || [];
+        const records = res?.data?.records || [];
         tableData.value =
           page.current === 1 ? records : [...tableData.value, ...records];
-        total.value = res.data.total;
+        const total = Number(res?.data?.total || 0);
         loadStatus.value =
-          tableData.value.length >= total.value ? "nomore" : "loadmore";
+          total > 0 && tableData.value.length >= total ? "nomore" : "loadmore";
       })
       .catch(() => {
-        loading.value = false;
         loadStatus.value = "loadmore";
+      })
+      .finally(() => {
+        loading.value = false;
       });
   };
 
@@ -213,12 +224,41 @@
     return batchNo.length > 25 || batchNo.includes(",") || batchNo.includes("锛�");
   };
 
+  const openDetail = row => {
+    if (!row?.productId || !row?.productModelId) return;
+    const productName = encodeURIComponent(row.productName || "");
+    const model = encodeURIComponent(row.model || "");
+    const unit = encodeURIComponent(row.unit || "");
+    uni.navigateTo({
+      url: `/pages/inventoryManagement/stockManagement/detail?topParentProductId=${props.productId}&productId=${row.productId}&productModelId=${row.productModelId}&productName=${productName}&model=${model}&unit=${unit}`,
+    });
+  };
+
+  const openAddPopup = () => {
+    uni.navigateTo({
+      url: `/pages/inventoryManagement/stockManagement/add?topParentProductId=${props.productId}`,
+    });
+  };
+
+  const onRefresh = payload => {
+    if (!payload) return;
+    if (String(payload.topParentProductId) !== String(props.productId)) return;
+    handleQuery();
+  };
+
   onMounted(() => {
     getList();
+    uni.$on("stockManagement:refresh", onRefresh);
+  });
+
+  onUnmounted(() => {
+    uni.$off("stockManagement:refresh", onRefresh);
   });
 </script>
 
 <style scoped lang="scss">
+  @import "@/static/scss/form-common.scss";
+
   .record-container {
     height: 100%;
     display: flex;
@@ -267,6 +307,19 @@
     padding: 30rpx;
     margin-bottom: 20rpx;
     box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+  }
+
+  .action-buttons {
+    display: flex;
+    gap: 12px;
+    padding-top: 10px;
+  }
+
+  .action-btn {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
   }
 
   .item-header {
@@ -440,4 +493,142 @@
   .no-data {
     padding-top: 200rpx;
   }
+
+  .fab-button {
+    position: fixed;
+    bottom: calc(30px + env(safe-area-inset-bottom));
+    right: 30px;
+    width: 56px;
+    height: 56px;
+    background: #2979ff;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    box-shadow: 0 4px 16px rgba(41, 121, 255, 0.3);
+    z-index: 1000;
+  }
+
+  .detail-popup {
+    background-color: #fff;
+    max-height: 75vh;
+    display: flex;
+    flex-direction: column;
+  }
+
+  .detail-summary {
+    padding: 0 30rpx 20rpx 30rpx;
+  }
+
+  .summary-row {
+    display: flex;
+    justify-content: space-between;
+    margin-top: 16rpx;
+  }
+
+  .summary-label {
+    font-size: 24rpx;
+    color: #909399;
+  }
+
+  .summary-value {
+    font-size: 24rpx;
+    color: #303133;
+    text-align: right;
+    margin-left: 20rpx;
+    flex: 1;
+  }
+
+  .detail-list-scroll {
+    flex: 1;
+    min-height: 0;
+  }
+
+  .detail-list {
+    padding: 20rpx 30rpx 40rpx 30rpx;
+  }
+
+  .detail-card {
+    background-color: #f8f9fa;
+    border-radius: 16rpx;
+    padding: 24rpx;
+    margin-bottom: 20rpx;
+  }
+
+  .detail-card-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 16rpx;
+  }
+
+  .detail-card-title {
+    font-size: 28rpx;
+    color: #303133;
+    font-weight: 600;
+  }
+
+  .detail-card-body {
+    .detail-row {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 12rpx;
+      font-size: 24rpx;
+    }
+
+    .detail-label {
+      color: #909399;
+    }
+
+    .detail-value {
+      color: #303133;
+      font-weight: 500;
+      text-align: right;
+      margin-left: 20rpx;
+      flex: 1;
+    }
+  }
+
+  .product-select-popup {
+    background-color: #fff;
+    max-height: 75vh;
+    display: flex;
+    flex-direction: column;
+  }
+
+  .product-search {
+    padding: 20rpx 30rpx;
+    display: flex;
+    gap: 16rpx;
+  }
+
+  .product-list-scroll {
+    flex: 1;
+    min-height: 0;
+  }
+
+  .product-model-list {
+    padding: 0 30rpx 40rpx 30rpx;
+  }
+
+  .product-model-item {
+    background-color: #f8f9fa;
+    border-radius: 16rpx;
+    padding: 24rpx;
+    margin-top: 20rpx;
+  }
+
+  .pm-name {
+    display: block;
+    font-size: 26rpx;
+    color: #303133;
+    font-weight: 600;
+  }
+
+  .pm-model {
+    display: block;
+    margin-top: 8rpx;
+    font-size: 24rpx;
+    color: #909399;
+  }
 </style>
diff --git a/src/pages/inventoryManagement/stockManagement/add.vue b/src/pages/inventoryManagement/stockManagement/add.vue
new file mode 100644
index 0000000..e60a6e0
--- /dev/null
+++ b/src/pages/inventoryManagement/stockManagement/add.vue
@@ -0,0 +1,155 @@
+<template>
+  <view class="account-detail">
+    <PageHeader title="鏂板搴撳瓨"
+                @back="goBack" />
+    <up-form ref="formRef"
+             :model="form"
+             label-width="110">
+      <up-form-item label="浜у搧"
+                    required>
+        <up-input v-model="form.productName"
+                  placeholder="璇烽�夋嫨"
+                  readonly
+                  @click="goSelectProduct" />
+        <template #right>
+          <up-icon name="arrow-right"
+                   @click="goSelectProduct"></up-icon>
+        </template>
+      </up-form-item>
+      <up-form-item label="瑙勬牸鍨嬪彿">
+        <up-input v-model="form.model"
+                  disabled
+                  placeholder="鑷姩濉厖" />
+      </up-form-item>
+      <up-form-item label="鍗曚綅">
+        <up-input v-model="form.unit"
+                  disabled
+                  placeholder="鑷姩濉厖" />
+      </up-form-item>
+      <up-form-item label="搴撳瓨绫诲瀷"
+                    required>
+        <up-input v-model="stockTypeText"
+                  disabled />
+      </up-form-item>
+      <up-form-item label="鏁伴噺"
+                    required>
+        <up-number-box v-model="form.qualitity"
+                       :min="1"
+                       :step="1" />
+      </up-form-item>
+      <up-form-item label="鎵瑰彿">
+        <up-input v-model="form.batchNo"
+                  placeholder="閫夊~"
+                  clearable />
+      </up-form-item>
+      <up-form-item v-if="form.type === 'qualified'"
+                    label="棰勮鏁伴噺">
+        <up-number-box v-model="form.warnNum"
+                       :min="0"
+                       :max="Number(form.qualitity || 0)"
+                       :step="1" />
+      </up-form-item>
+      <up-form-item label="澶囨敞">
+        <up-textarea v-model="form.remark"
+                     placeholder="閫夊~"
+                     auto-height />
+      </up-form-item>
+    </up-form>
+    <FooterButtons cancelText="鍙栨秷"
+                   confirmText="淇濆瓨"
+                   :loading="submitting"
+                   @cancel="goBack"
+                   @confirm="handleSubmit" />
+  </view>
+</template>
+
+<script setup>
+  import { onMounted, ref } from "vue";
+  import PageHeader from "@/components/PageHeader.vue";
+  import FooterButtons from "@/components/FooterButtons.vue";
+  import { addStockInRecordOnly } from "@/api/inventoryManagement/stockInventory.js";
+
+  const submitting = ref(false);
+  const formRef = ref(null);
+  const topParentProductId = ref(undefined);
+
+  const form = ref({
+    productId: undefined,
+    productModelId: undefined,
+    productName: "",
+    model: "",
+    unit: "",
+    type: "qualified",
+    qualitity: 1,
+    batchNo: null,
+    warnNum: 0,
+    remark: "",
+  });
+
+  const stockTypeText = ref("鍚堟牸搴撳瓨");
+
+  const getRequestErrorText = err => {
+    if (!err || typeof err !== "object") return "";
+    const msg = err?.msg || err?.message || err?.data?.msg || err?.data?.message;
+    return msg ? String(msg) : "";
+  };
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const goSelectProduct = () => {
+    const onSelected = row => {
+      form.value.productId = row.productId;
+      form.value.productModelId = row.id;
+      form.value.productName = row.productName || "";
+      form.value.model = row.model || "";
+      form.value.unit = row.unit || "";
+    };
+    uni.$once("stockManagement:selectedProductModel", onSelected);
+    uni.navigateTo({
+      url: `/pages/inventoryManagement/stockManagement/selectProductModel?topParentProductId=${topParentProductId.value || ""}`,
+      events: {
+        selected: onSelected,
+      },
+    });
+  };
+
+  const handleSubmit = () => {
+    if (!form.value.productModelId) {
+      uni.showToast({ title: "璇烽�夋嫨浜у搧", icon: "none" });
+      return;
+    }
+    const qty = Number(form.value.qualitity);
+    if (!qty || qty < 1) {
+      uni.showToast({ title: "鏁伴噺蹇呴』澶т簬0", icon: "none" });
+      return;
+    }
+
+    submitting.value = true;
+    const payload = { ...form.value };
+    if (payload.batchNo === "") payload.batchNo = null;
+    addStockInRecordOnly(payload)
+      .then(() => {
+        uni.showToast({ title: "鎻愪氦鎴愬姛", icon: "success" });
+        uni.$emit("stockManagement:refresh", { topParentProductId: topParentProductId.value });
+        goBack();
+      })
+      .catch(err => {
+        const title = getRequestErrorText(err);
+        if (title) uni.showToast({ title, icon: "none" });
+      })
+      .finally(() => {
+        submitting.value = false;
+      });
+  };
+
+  onMounted(() => {
+    const options = getCurrentPages()?.slice(-1)?.[0]?.options || {};
+    topParentProductId.value = options.topParentProductId ? Number(options.topParentProductId) : undefined;
+  });
+</script>
+
+<style scoped lang="scss">
+  @import "@/static/scss/form-common.scss";
+</style>
diff --git a/src/pages/inventoryManagement/stockManagement/detail.vue b/src/pages/inventoryManagement/stockManagement/detail.vue
new file mode 100644
index 0000000..eb4b06e
--- /dev/null
+++ b/src/pages/inventoryManagement/stockManagement/detail.vue
@@ -0,0 +1,294 @@
+<template>
+  <view class="app-container">
+    <PageHeader title="搴撳瓨璇︽儏"
+                @back="goBack" />
+    <view class="summary-card">
+      <view class="summary-row">
+        <text class="label">浜у搧</text>
+        <text class="value">{{ productName || '-' }}</text>
+      </view>
+      <view class="summary-row">
+        <text class="label">瑙勬牸</text>
+        <text class="value">{{ model || '-' }}</text>
+      </view>
+      <view class="summary-row">
+        <text class="label">鍗曚綅</text>
+        <text class="value">{{ unit || '-' }}</text>
+      </view>
+    </view>
+
+    <scroll-view scroll-y
+                 class="list-scroll"
+                 @scrolltolower="loadMore">
+      <view v-if="loading"
+            class="loading-state">
+        <up-loading-icon text="鍔犺浇涓�..."></up-loading-icon>
+      </view>
+      <view v-else-if="rows.length === 0"
+            class="no-data">
+        <up-empty mode="data"
+                  text="鏆傛棤鎵规鏁版嵁"></up-empty>
+      </view>
+      <view v-else
+            class="list">
+        <view v-for="row in rows"
+              :key="row.id || row.batchNo"
+              class="card">
+          <view class="card-header">
+            <text class="title">鎵瑰彿锛歿{ row.batchNo || '-' }}</text>
+          </view>
+          <view class="card-body">
+            <view class="kv-grid">
+              <view class="kv half">
+                <text class="k">鍚堟牸搴撳瓨</text>
+                <text class="v">{{ row.qualifiedQuantity ?? 0 }}</text>
+              </view>
+              <view class="kv half">
+                <text class="k">涓嶅悎鏍煎簱瀛�</text>
+                <text class="v">{{ row.unQualifiedQuantity ?? 0 }}</text>
+              </view>
+              <view class="kv half">
+                <text class="k">鍚堟牸鍙敤</text>
+                <text class="v">{{ calcQualifiedMax(row) }}</text>
+              </view>
+              <view class="kv half">
+                <text class="k">涓嶅悎鏍煎彲鐢�</text>
+                <text class="v">{{ calcUnqualifiedMax(row) }}</text>
+              </view>
+              <view class="kv full">
+                <text class="k">棰勮</text>
+                <text class="v">{{ row.warnNum ?? '-' }}</text>
+              </view>
+              <view class="kv full">
+                <text class="k">澶囨敞</text>
+                <text class="v">{{ row.remark || '-' }}</text>
+              </view>
+            </view>
+          </view>
+          <view class="card-footer">
+            <u-button type="primary"
+                      :disabled="!canUseRow(row)"
+                      :customStyle="{ width: '100%', height: '72rpx', lineHeight: '72rpx', borderRadius: '12rpx' }"
+                      @click="goUse(row)">
+              棰嗙敤
+            </u-button>
+          </view>
+        </view>
+        <up-loadmore :status="loadStatus" />
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script setup>
+  import { onMounted, reactive, ref } from "vue";
+  import PageHeader from "@/components/PageHeader.vue";
+  import { getStockInventoryBatchNoQty } from "@/api/inventoryManagement/stockInventory.js";
+
+  const loading = ref(false);
+  const loadStatus = ref("loadmore");
+  const rows = ref([]);
+  const total = ref(0);
+  const page = reactive({ current: 1, size: 20 });
+
+  const topParentProductId = ref(undefined);
+  const productId = ref(undefined);
+  const productModelId = ref(undefined);
+  const productName = ref("");
+  const model = ref("");
+  const unit = ref("");
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const calcQualifiedMax = row => {
+    return Math.max(
+      0,
+      Number(row?.qualifiedUnLockedQuantity || 0) +
+        Number(row?.qualifiedPendingOutQuantity || 0)
+    );
+  };
+
+  const calcUnqualifiedMax = row => {
+    return Math.max(
+      0,
+      Number(row?.unQualifiedUnLockedQuantity || 0) +
+        Number(row?.unQualifiedPendingOutQuantity || 0)
+    );
+  };
+
+  const canUseRow = row => {
+    return calcQualifiedMax(row) > 0 || calcUnqualifiedMax(row) > 0;
+  };
+
+  const fetchList = () => {
+    if (loading.value) return;
+    if (!productId.value || !productModelId.value) return;
+    loading.value = true;
+    loadStatus.value = "loading";
+
+    getStockInventoryBatchNoQty({
+      current: page.current,
+      size: page.size,
+      productId: productId.value,
+      productModelId: productModelId.value,
+    })
+      .then(res => {
+        const records = res?.records || res?.data?.records || res?.data || [];
+        const list = Array.isArray(records) ? records : [];
+        rows.value = page.current === 1 ? list : [...rows.value, ...list];
+        total.value = Number(res?.total ?? res?.data?.total ?? rows.value.length);
+        loadStatus.value = rows.value.length >= total.value ? "nomore" : "loadmore";
+      })
+      .catch(() => {
+        loadStatus.value = "loadmore";
+      })
+      .finally(() => {
+        loading.value = false;
+      });
+  };
+
+  const loadMore = () => {
+    if (loadStatus.value !== "loadmore") return;
+    page.current++;
+    fetchList();
+  };
+
+  const goUse = row => {
+    if (!row) return;
+    const qualifiedMax = calcQualifiedMax(row);
+    const unqualifiedMax = calcUnqualifiedMax(row);
+    const pn = encodeURIComponent(productName.value || "");
+    const md = encodeURIComponent(model.value || "");
+    const un = encodeURIComponent(unit.value || "");
+    const bn = encodeURIComponent(row.batchNo || "");
+    uni.navigateTo({
+      url: `/pages/inventoryManagement/stockManagement/use?topParentProductId=${topParentProductId.value || ""}&productId=${productId.value}&productModelId=${productModelId.value}&productName=${pn}&model=${md}&unit=${un}&batchNo=${bn}&qualifiedMax=${qualifiedMax}&unqualifiedMax=${unqualifiedMax}`,
+    });
+  };
+
+  onMounted(() => {
+    const options = getCurrentPages()?.slice(-1)?.[0]?.options || {};
+    topParentProductId.value = options.topParentProductId ? Number(options.topParentProductId) : undefined;
+    productId.value = options.productId ? Number(options.productId) : undefined;
+    productModelId.value = options.productModelId ? Number(options.productModelId) : undefined;
+    productName.value = decodeURIComponent(options.productName || "");
+    model.value = decodeURIComponent(options.model || "");
+    unit.value = decodeURIComponent(options.unit || "");
+    fetchList();
+  });
+</script>
+
+<style scoped lang="scss">
+  .app-container {
+    height: 100vh;
+    background-color: #f8f9fa;
+    display: flex;
+    flex-direction: column;
+  }
+
+  .summary-card {
+    background: #fff;
+    margin: 20rpx;
+    border-radius: 16rpx;
+    padding: 24rpx;
+    box-shadow: 0 6rpx 24rpx rgba(0, 0, 0, 0.06);
+  }
+
+  .summary-row {
+    display: flex;
+    justify-content: space-between;
+    margin-top: 12rpx;
+    font-size: 26rpx;
+  }
+
+  .label {
+    color: #909399;
+  }
+
+  .value {
+    color: #303133;
+    font-weight: 500;
+    text-align: right;
+    margin-left: 20rpx;
+    flex: 1;
+  }
+
+  .list-scroll {
+    flex: 1;
+    min-height: 0;
+  }
+
+  .list {
+    padding: 0 20rpx 20rpx 20rpx;
+  }
+
+  .card {
+    background: #fff;
+    border-radius: 16rpx;
+    padding: 24rpx;
+    margin-bottom: 20rpx;
+    box-shadow: 0 6rpx 24rpx rgba(0, 0, 0, 0.06);
+  }
+
+  .card-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 16rpx;
+  }
+
+  .title {
+    font-size: 28rpx;
+    font-weight: 600;
+    color: #303133;
+  }
+
+  .card-body {
+    .kv-grid {
+      display: flex;
+      flex-wrap: wrap;
+      margin-top: 8rpx;
+    }
+
+    .kv {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      font-size: 26rpx;
+      padding-top: 12rpx;
+      box-sizing: border-box;
+    }
+
+    .kv.half {
+      width: 50%;
+      padding-right: 16rpx;
+    }
+
+    .kv.full {
+      width: 100%;
+    }
+
+    .k {
+      color: #909399;
+    }
+
+    .v {
+      color: #303133;
+      font-weight: 500;
+      text-align: right;
+      margin-left: 20rpx;
+      flex: 1;
+    }
+  }
+
+  .card-footer {
+    margin-top: 20rpx;
+  }
+
+  .loading-state,
+  .no-data {
+    padding-top: 200rpx;
+  }
+</style>
diff --git a/src/pages/inventoryManagement/stockManagement/selectProductModel.vue b/src/pages/inventoryManagement/stockManagement/selectProductModel.vue
new file mode 100644
index 0000000..50892cb
--- /dev/null
+++ b/src/pages/inventoryManagement/stockManagement/selectProductModel.vue
@@ -0,0 +1,228 @@
+<template>
+  <view class="account-detail">
+    <PageHeader title="閫夋嫨浜у搧"
+                @back="goBack" />
+    <view class="search-bar">
+      <view class="search-input">
+        <up-input v-model="query.productName"
+                  placeholder="浜у搧鍚嶇О"
+                  clearable
+                  @confirm="handleQuery" />
+      </view>
+      <view class="search-input">
+        <up-input v-model="query.model"
+                  placeholder="瑙勬牸鍨嬪彿"
+                  clearable
+                  @confirm="handleQuery" />
+      </view>
+      <view class="filter-button"
+            @click="handleQuery">
+        <up-icon name="search"
+                 size="24"
+                 color="#999"></up-icon>
+      </view>
+    </view>
+    <scroll-view scroll-y
+                 class="list-scroll"
+                 @scrolltolower="loadMore">
+      <view v-if="loading"
+            class="loading-state">
+        <up-loading-icon text="鍔犺浇涓�..."></up-loading-icon>
+      </view>
+      <view v-else-if="list.length === 0"
+            class="no-data">
+        <up-empty mode="data"
+                  text="鏆傛棤鏁版嵁"></up-empty>
+      </view>
+      <view v-else
+            class="list">
+        <view v-for="row in list"
+              :key="row.id"
+              class="card"
+              @click="selectRow(row)">
+          <view class="card-title">
+            <text class="name">{{ row.productName }}</text>
+          </view>
+          <view class="card-sub">
+            <text class="label">瑙勬牸</text>
+            <text class="value">{{ row.model || '-' }}</text>
+          </view>
+          <view class="card-sub">
+            <text class="label">鍗曚綅</text>
+            <text class="value">{{ row.unit || '-' }}</text>
+          </view>
+        </view>
+        <up-loadmore :status="loadStatus" />
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script setup>
+  import { onMounted, reactive, ref, getCurrentInstance } from "vue";
+  import PageHeader from "@/components/PageHeader.vue";
+  import { pageModel } from "@/api/basicData/product.js";
+
+  const loading = ref(false);
+  const loadStatus = ref("loadmore");
+  const list = ref([]);
+  const total = ref(0);
+  const page = reactive({ current: 1, size: 20 });
+  const topParentProductId = ref(undefined);
+
+  const query = reactive({
+    productName: "",
+    model: "",
+  });
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const fetchList = () => {
+    if (loading.value) return;
+    loading.value = true;
+    loadStatus.value = "loading";
+    pageModel({
+      productName: query.productName,
+      model: query.model,
+      current: page.current,
+      size: page.size,
+      topProductParentId: topParentProductId.value,
+    })
+      .then(res => {
+        const records = res?.records || res?.data?.records || res?.data || [];
+        const rows = Array.isArray(records) ? records : [];
+        list.value = page.current === 1 ? rows : [...list.value, ...rows];
+        total.value = Number(res?.total ?? res?.data?.total ?? list.value.length);
+        loadStatus.value = list.value.length >= total.value ? "nomore" : "loadmore";
+      })
+      .catch(() => {
+        loadStatus.value = "loadmore";
+      })
+      .finally(() => {
+        loading.value = false;
+      });
+  };
+
+  const handleQuery = () => {
+    page.current = 1;
+    list.value = [];
+    fetchList();
+  };
+
+  const loadMore = () => {
+    if (loadStatus.value !== "loadmore") return;
+    page.current++;
+    fetchList();
+  };
+
+  const selectRow = row => {
+    const instance = getCurrentInstance();
+    const eventChannel = instance?.proxy?.getOpenerEventChannel?.();
+    if (eventChannel?.emit) {
+      eventChannel.emit("selected", row);
+    } else {
+      uni.$emit("stockManagement:selectedProductModel", row);
+    }
+    goBack();
+  };
+
+  onMounted(() => {
+    const options = getCurrentPages()?.slice(-1)?.[0]?.options || {};
+    topParentProductId.value = options.topParentProductId ? Number(options.topParentProductId) : undefined;
+    fetchList();
+  });
+</script>
+
+<style scoped lang="scss">
+  @import "@/static/scss/form-common.scss";
+
+  .search-bar {
+    margin: 20rpx;
+    background-color: #fff;
+    border-radius: 16rpx;
+    padding: 0 30rpx;
+    height: 80rpx;
+    display: flex;
+    align-items: center;
+    gap: 16rpx;
+
+    .search-input {
+      flex: 1;
+      min-width: 0;
+    }
+
+    :deep(.u-input) {
+      background-color: #f2f2f2;
+      border-radius: 40rpx;
+      padding: 0 30rpx;
+      height: 80rpx;
+      margin-bottom: 0;
+    }
+
+    :deep(.u-input__content__field-wrapper__field) {
+      height: 80rpx;
+      line-height: 80rpx;
+    }
+
+    .filter-button {
+      width: 80rpx;
+      height: 80rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-shrink: 0;
+    }
+  }
+
+  .list-scroll {
+    flex: 1;
+    min-height: 0;
+  }
+
+  .list {
+    padding: 0 20rpx 20rpx 20rpx;
+  }
+
+  .card {
+    background: #fff;
+    border-radius: 16rpx;
+    padding: 24rpx;
+    margin-bottom: 20rpx;
+  }
+
+  .card-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10rpx;
+  }
+
+  .name {
+    font-size: 30rpx;
+    font-weight: 600;
+    color: #303133;
+  }
+
+  .card-sub {
+    display: flex;
+    justify-content: space-between;
+    margin-top: 10rpx;
+    font-size: 26rpx;
+  }
+
+  .label {
+    color: #909399;
+  }
+
+  .value {
+    color: #303133;
+    font-weight: 500;
+  }
+
+  .loading-state,
+  .no-data {
+    padding-top: 200rpx;
+  }
+</style>
diff --git a/src/pages/inventoryManagement/stockManagement/use.vue b/src/pages/inventoryManagement/stockManagement/use.vue
new file mode 100644
index 0000000..023876c
--- /dev/null
+++ b/src/pages/inventoryManagement/stockManagement/use.vue
@@ -0,0 +1,183 @@
+<template>
+  <view class="account-detail">
+    <PageHeader title="棰嗙敤"
+                @back="goBack" />
+    <up-form ref="formRef"
+             :model="form"
+             label-width="110">
+      <up-form-item label="浜у搧">
+        <up-input v-model="form.productName"
+                  disabled />
+      </up-form-item>
+      <up-form-item label="瑙勬牸鍨嬪彿">
+        <up-input v-model="form.model"
+                  disabled />
+      </up-form-item>
+      <up-form-item label="鍗曚綅">
+        <up-input v-model="form.unit"
+                  disabled />
+      </up-form-item>
+      <up-form-item label="鎵瑰彿"
+                    v-if="form.batchNo">
+        <up-input v-model="form.batchNo"
+                  disabled />
+      </up-form-item>
+      <up-form-item label="搴撳瓨绫诲瀷"
+                    required>
+        <u-radio-group v-model="form.type"
+                       @change="onTypeChange">
+          <u-radio :customStyle="{ marginRight: '40rpx' }"
+                   label="鍚堟牸搴撳瓨"
+                   :disabled="qualifiedAvailable <= 0"
+                   name="qualified"></u-radio>
+          <u-radio label="涓嶅悎鏍煎簱瀛�"
+                   :disabled="unqualifiedAvailable <= 0"
+                   name="unqualified"></u-radio>
+        </u-radio-group>
+      </up-form-item>
+      <up-form-item label="鏁伴噺"
+                    required>
+        <up-number-box v-model="form.qualitity"
+                       :min="1"
+                       :max="maxQty"
+                       :step="1" />
+      </up-form-item>
+      <up-form-item label="澶囨敞">
+        <up-textarea v-model="form.remark"
+                     placeholder="閫夊~"
+                     auto-height />
+      </up-form-item>
+    </up-form>
+    <FooterButtons cancelText="鍙栨秷"
+                   confirmText="鎻愪氦"
+                   :loading="submitting"
+                   @cancel="goBack"
+                   @confirm="handleSubmit" />
+  </view>
+</template>
+
+<script setup>
+  import { onMounted, ref, computed } from "vue";
+  import PageHeader from "@/components/PageHeader.vue";
+  import FooterButtons from "@/components/FooterButtons.vue";
+  import { addStockOutRecordOnly } from "@/api/inventoryManagement/stockInventory.js";
+  import { addUnqualifiedStockOutRecordOnly } from "@/api/inventoryManagement/stockUninventory.js";
+
+  const submitting = ref(false);
+  const formRef = ref(null);
+  const maxQty = ref(1);
+  const qualifiedMax = ref(0);
+  const unqualifiedMax = ref(0);
+  const topParentProductId = ref(undefined);
+
+  const form = ref({
+    productId: undefined,
+    productModelId: undefined,
+    productName: "",
+    model: "",
+    unit: "",
+    batchNo: undefined,
+    type: "qualified",
+    qualitity: 1,
+    remark: "",
+  });
+
+  const qualifiedAvailable = computed(() => {
+    return Number(qualifiedMax.value || 0);
+  });
+  const unqualifiedAvailable = computed(() => {
+    return Number(unqualifiedMax.value || 0);
+  });
+  const currentAvailable = computed(() => {
+    return form.value.type === "qualified"
+      ? qualifiedAvailable.value
+      : unqualifiedAvailable.value;
+  });
+
+  const getRequestErrorText = err => {
+    if (!err || typeof err !== "object") return "";
+    const msg = err?.msg || err?.message || err?.data?.msg || err?.data?.message;
+    return msg ? String(msg) : "";
+  };
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const syncMax = () => {
+    if (form.value.type === "qualified" && qualifiedAvailable.value <= 0 && unqualifiedAvailable.value > 0) {
+      form.value.type = "unqualified";
+    }
+    if (form.value.type === "unqualified" && unqualifiedAvailable.value <= 0 && qualifiedAvailable.value > 0) {
+      form.value.type = "qualified";
+    }
+    maxQty.value = Math.max(1, Number(currentAvailable.value || 0));
+    if (Number(form.value.qualitity) > Number(maxQty.value)) {
+      form.value.qualitity = Number(maxQty.value);
+    }
+  };
+
+  const onTypeChange = () => {
+    form.value.qualitity = 1;
+    syncMax();
+  };
+
+  const handleSubmit = () => {
+    if (!form.value.productModelId) {
+      uni.showToast({ title: "鍙傛暟閿欒", icon: "none" });
+      return;
+    }
+    const qty = Number(form.value.qualitity);
+    const limit = form.value.type === "qualified" ? qualifiedMax.value : unqualifiedMax.value;
+    if (!qty || qty < 1) {
+      uni.showToast({ title: "鏁伴噺蹇呴』澶т簬0", icon: "none" });
+      return;
+    }
+    if (limit <= 0) {
+      uni.showToast({ title: "鏆傛棤鍙敤搴撳瓨", icon: "none" });
+      return;
+    }
+    if (qty > limit) {
+      uni.showToast({ title: "棰嗙敤鏁伴噺瓒呭嚭鍙敤鏁伴噺", icon: "none" });
+      return;
+    }
+
+    submitting.value = true;
+    const payload = { ...form.value };
+    const requestFn = payload.type === "qualified" ? addStockOutRecordOnly : addUnqualifiedStockOutRecordOnly;
+    requestFn(payload)
+      .then(() => {
+        uni.showToast({ title: "鎻愪氦鎴愬姛", icon: "success" });
+        uni.$emit("stockManagement:refresh", { topParentProductId: topParentProductId.value });
+        goBack();
+      })
+      .catch(err => {
+        const title = getRequestErrorText(err);
+        if (title) uni.showToast({ title, icon: "none" });
+      })
+      .finally(() => {
+        submitting.value = false;
+      });
+  };
+
+  onMounted(() => {
+    const options = getCurrentPages()?.slice(-1)?.[0]?.options || {};
+    topParentProductId.value = options.topParentProductId ? Number(options.topParentProductId) : undefined;
+    form.value.productId = options.productId ? Number(options.productId) : undefined;
+    form.value.productModelId = options.productModelId ? Number(options.productModelId) : undefined;
+    form.value.productName = decodeURIComponent(options.productName || "");
+    form.value.model = decodeURIComponent(options.model || "");
+    form.value.unit = decodeURIComponent(options.unit || "");
+    form.value.batchNo = options.batchNo ? decodeURIComponent(options.batchNo || "") : undefined;
+    qualifiedMax.value = options.qualifiedMax ? Number(options.qualifiedMax) : 0;
+    unqualifiedMax.value = options.unqualifiedMax ? Number(options.unqualifiedMax) : 0;
+
+    form.value.type = qualifiedMax.value > 0 ? "qualified" : "unqualified";
+    form.value.qualitity = 1;
+    syncMax();
+  });
+</script>
+
+<style scoped lang="scss">
+  @import "@/static/scss/form-common.scss";
+</style>

--
Gitblit v1.9.3