zhangwencui
11 小时以前 6ca03cc8ac229c13702ce699bab4db6e93f3f172
1、供应商管理新增修改删除功能2、库存管理增加添加功能、详情、领用功能
已添加4个文件
已修改5个文件
1267 ■■■■■ 文件已修改
src/api/basicData/product.js 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInventory.js 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockUninventory.js 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/Record.vue 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/add.vue 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/detail.vue 294 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/selectProductModel.vue 228 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inventoryManagement/stockManagement/use.vue 183 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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,
  });
}
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({
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,
  });
};
// è§£å†»åº“存记录
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,
  });
};
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"
  }
}
}
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>
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>
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>
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>
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>