From 178c6c22dbe2e75cbd02f0732ba3bf742222263d Mon Sep 17 00:00:00 2001
From: ZN <zhang_12370@163.com>
Date: 星期一, 16 三月 2026 16:53:33 +0800
Subject: [PATCH] feat(耗材物流): 新增耗材物流模块相关页面与接口
---
src/pages/index.vue | 112 ++
src/components/ProductSelectDialog.vue | 180 +++
src/pages.json | 63 +
src/pages/consumablesLogistics/receiptManagement/index.vue | 231 ++++
src/pages/consumablesLogistics/stockManagement/Unqualified.vue | 91 +
src/api/consumablesLogistics/consumablesUninventory.js | 42
src/pages/consumablesLogistics/dispatchLog/view.vue | 180 +++
src/pages/consumablesLogistics/stockManagement/subtract.vue | 209 ++++
src/pages/consumablesLogistics/dispatchLog/index.vue | 223 ++++
src/pages/consumablesLogistics/stockManagement/view.vue | 121 ++
src/pages/consumablesLogistics/stockReport/index.vue | 242 ++++
src/pages/consumablesLogistics/stockManagement/add.vue | 283 +++++
src/api/consumablesLogistics/consumablesIn.js | 58 +
src/pages/consumablesLogistics/receiptManagement/view.vue | 180 +++
src/pages/consumablesLogistics/stockManagement/index.vue | 319 ++++++
src/api/consumablesLogistics/consumablesOutRecord.js | 18
src/pages/consumablesLogistics/receiptManagement/Record.vue | 197 +++
src/components/PIMTable/Pagination.vue | 64 +
src/pages/consumablesLogistics/dispatchLog/Record.vue | 197 +++
src/pages/consumablesLogistics/stockManagement/Qualified.vue | 91 +
20 files changed, 3,093 insertions(+), 8 deletions(-)
diff --git a/src/api/consumablesLogistics/consumablesIn.js b/src/api/consumablesLogistics/consumablesIn.js
new file mode 100644
index 0000000..401e57f
--- /dev/null
+++ b/src/api/consumablesLogistics/consumablesIn.js
@@ -0,0 +1,58 @@
+import request from "@/utils/request";
+
+export const getConsumablesInListPage = (params) => {
+ return request({
+ url: "/consumablesInventory/pageConsumablesInventory",
+ method: "get",
+ params,
+ });
+};
+
+export const createConsumablesIn = (params) => {
+ return request({
+ url: "/consumablesInventory/addConsumablesInventory",
+ method: "post",
+ data: params,
+ });
+};
+
+export const subtractConsumablesIn = (params) => {
+ return request({
+ url: "/consumablesInventory/subtractConsumablesInventory",
+ method: "post",
+ data: params,
+ });
+};
+
+export const getConsumablesInReportList = (params) => {
+ return request({
+ url: "/consumablesInventory/ConsumablesInventoryPage",
+ method: "get",
+ params,
+ });
+};
+
+export const getConsumablesInInAndOutReportList = (params) => {
+ return request({
+ url: "/consumablesInventory/ConsumablesInAndOutRecord",
+ method: "get",
+ params,
+ });
+};
+
+export const frozenConsumablesIn = (params) => {
+ return request({
+ url: "/consumablesInventory/frozenConsumables",
+ method: "post",
+ data: params,
+ });
+};
+
+export const thawConsumablesIn = (params) => {
+ return request({
+ url: "/consumablesInventory/thawConsumables",
+ method: "post",
+ data: params,
+ });
+};
+
diff --git a/src/api/consumablesLogistics/consumablesOutRecord.js b/src/api/consumablesLogistics/consumablesOutRecord.js
new file mode 100644
index 0000000..efd886b
--- /dev/null
+++ b/src/api/consumablesLogistics/consumablesOutRecord.js
@@ -0,0 +1,18 @@
+import request from "@/utils/request";
+
+export const getConsumablesOutRecordPage = (params) => {
+ return request({
+ url: "/consumablesOutRecord/listPage",
+ method: "get",
+ params,
+ });
+};
+
+export const delConsumablesOutRecord = (ids) => {
+ return request({
+ url: "/consumablesOutRecord",
+ method: "delete",
+ data: ids,
+ });
+};
+
diff --git a/src/api/consumablesLogistics/consumablesUninventory.js b/src/api/consumablesLogistics/consumablesUninventory.js
new file mode 100644
index 0000000..49eaffe
--- /dev/null
+++ b/src/api/consumablesLogistics/consumablesUninventory.js
@@ -0,0 +1,42 @@
+import request from "@/utils/request";
+
+export const getConsumablesUninventoryListPage = (params) => {
+ return request({
+ url: "/consumablesUnInventory/pageConsumablesUnInventory",
+ method: "get",
+ params,
+ });
+};
+
+export const createConsumablesUnInventory = (params) => {
+ return request({
+ url: "/consumablesUninventory/addstockUninventory",
+ method: "post",
+ data: params,
+ });
+};
+
+export const subtractConsumablesUnInventory = (params) => {
+ return request({
+ url: "/consumablesUninventory/subtractstockUninventory",
+ method: "post",
+ data: params,
+ });
+};
+
+export const frozenConsumablesUninventory = (params) => {
+ return request({
+ url: "/consumablesUninventory/frozenStock",
+ method: "post",
+ data: params,
+ });
+};
+
+export const thawConsumablesUninventory = (params) => {
+ return request({
+ url: "/consumablesUninventory/thawStock",
+ method: "post",
+ data: params,
+ });
+};
+
diff --git a/src/components/PIMTable/Pagination.vue b/src/components/PIMTable/Pagination.vue
new file mode 100644
index 0000000..ec6a410
--- /dev/null
+++ b/src/components/PIMTable/Pagination.vue
@@ -0,0 +1,64 @@
+<template>
+ <div class="pagination-container" v-show="total > 0">
+ <el-pagination
+ v-model:current-page="currentPage"
+ v-model:page-size="pageSize"
+ :total="total"
+ :layout="layout"
+ :page-sizes="pageSizes"
+ background
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+import { computed } from "vue";
+
+const props = defineProps({
+ total: { type: Number, default: 0 },
+ page: { type: Number, default: 1 },
+ limit: { type: Number, default: 10 },
+ pageSizes: {
+ type: Array,
+ default: () => [10, 20, 50, 100],
+ },
+ layout: {
+ type: String,
+ default: "total, sizes, prev, pager, next, jumper",
+ },
+});
+
+const emit = defineEmits(["pagination"]);
+
+const currentPage = computed({
+ get: () => props.page,
+ set: val => {
+ emit("pagination", { page: val, limit: props.limit });
+ },
+});
+
+const pageSize = computed({
+ get: () => props.limit,
+ set: val => {
+ emit("pagination", { page: 1, limit: val });
+ },
+});
+
+const handleSizeChange = val => {
+ emit("pagination", { page: 1, limit: val });
+};
+
+const handleCurrentChange = val => {
+ emit("pagination", { page: val, limit: props.limit });
+};
+</script>
+
+<style scoped>
+.pagination-container {
+ display: flex;
+ justify-content: flex-end;
+ padding-top: 12px;
+}
+</style>
diff --git a/src/components/ProductSelectDialog.vue b/src/components/ProductSelectDialog.vue
new file mode 100644
index 0000000..3780394
--- /dev/null
+++ b/src/components/ProductSelectDialog.vue
@@ -0,0 +1,180 @@
+<template>
+ <el-dialog
+ v-model="visibleProxy"
+ title="閫夋嫨浜у搧"
+ width="900px"
+ @close="handleClose"
+ >
+ <div class="search-row">
+ <el-input
+ v-model="keyword"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�/瑙勬牸鎼滅储"
+ clearable
+ style="width: 320px"
+ @keyup.enter="handleQuery"
+ />
+ <el-button type="primary" @click="handleQuery">鏌ヨ</el-button>
+ <el-button @click="handleReset">閲嶇疆</el-button>
+ </div>
+
+ <el-table
+ v-loading="loading"
+ :data="tableData"
+ border
+ height="420"
+ style="width: 100%"
+ @selection-change="handleSelectionChange"
+ @row-click="handleRowClick"
+ >
+ <el-table-column
+ v-if="!single"
+ type="selection"
+ width="55"
+ align="center"
+ />
+ <el-table-column label="浜у搧鍚嶇О" prop="productName" min-width="180" />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="model" min-width="180" />
+ <el-table-column label="鍗曚綅" prop="unit" width="100" />
+ <el-table-column label="浜у搧绫诲瀷" prop="parentName" width="120" />
+ </el-table>
+
+ <Pagination
+ :total="total"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="handlePagination"
+ />
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" :disabled="selectedRows.length === 0" @click="handleConfirm">
+ 纭畾
+ </el-button>
+ <el-button @click="visibleProxy = false">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { computed, onMounted, reactive, ref, watch } from "vue";
+import Pagination from "@/components/PIMTable/Pagination.vue";
+import { modelListPage } from "@/api/basicData/product.js";
+
+const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ single: { type: Boolean, default: false },
+});
+
+const emit = defineEmits(["update:modelValue", "confirm"]);
+
+const visibleProxy = computed({
+ get: () => props.modelValue,
+ set: val => emit("update:modelValue", val),
+});
+
+const keyword = ref("");
+const loading = ref(false);
+const tableData = ref([]);
+const selectedRows = ref([]);
+const total = ref(0);
+
+const page = reactive({
+ current: 1,
+ size: 10,
+});
+
+const normalizeRow = row => {
+ if (!row) return row;
+ return {
+ ...row,
+ productId: row.productId ?? row.product_id ?? row.product?.id,
+ productName: row.productName ?? row.product_name ?? row.name,
+ model: row.model ?? row.productModelName ?? row.modelName ?? row.specificationModel,
+ id: row.id ?? row.productModelId ?? row.modelId,
+ unit: row.unit ?? row.productUnit,
+ productType: row.productType ?? row.type,
+ };
+};
+
+const fetchList = () => {
+ loading.value = true;
+ const params = {
+ current: page.current,
+ size: page.size,
+ };
+ if (keyword.value) {
+ params.keyword = keyword.value;
+ params.productName = keyword.value;
+ params.model = keyword.value;
+ }
+ modelListPage(params)
+ .then(res => {
+ const records = res?.data?.records || [];
+ tableData.value = records.map(normalizeRow);
+ total.value = res?.data?.total || 0;
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+};
+
+const handleQuery = () => {
+ page.current = 1;
+ fetchList();
+};
+
+const handleReset = () => {
+ keyword.value = "";
+ page.current = 1;
+ fetchList();
+};
+
+const handlePagination = ({ page: p, limit }) => {
+ page.current = p;
+ page.size = limit;
+ fetchList();
+};
+
+const handleSelectionChange = selection => {
+ selectedRows.value = Array.isArray(selection) ? selection : [];
+};
+
+const handleRowClick = row => {
+ if (!props.single) return;
+ selectedRows.value = row ? [row] : [];
+};
+
+const handleConfirm = () => {
+ if (selectedRows.value.length === 0) return;
+ emit("confirm", selectedRows.value);
+ visibleProxy.value = false;
+};
+
+const handleClose = () => {
+ selectedRows.value = [];
+};
+
+watch(
+ () => props.modelValue,
+ val => {
+ if (!val) return;
+ page.current = 1;
+ selectedRows.value = [];
+ fetchList();
+ }
+);
+
+onMounted(() => {
+ if (props.modelValue) fetchList();
+});
+</script>
+
+<style scoped>
+.search-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding-bottom: 12px;
+}
+</style>
diff --git a/src/pages.json b/src/pages.json
index ac70378..a1b9843 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -757,6 +757,69 @@
}
},
{
+ "path": "pages/consumablesLogistics/receiptManagement/index",
+ "style": {
+ "navigationBarTitleText": "鑰楁潗鍏ュ簱绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/consumablesLogistics/receiptManagement/view",
+ "style": {
+ "navigationBarTitleText": "鍏ュ簱璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/consumablesLogistics/dispatchLog/index",
+ "style": {
+ "navigationBarTitleText": "鑰楁潗鍑哄簱鍙拌处",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/consumablesLogistics/dispatchLog/view",
+ "style": {
+ "navigationBarTitleText": "鍑哄簱璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/consumablesLogistics/stockManagement/index",
+ "style": {
+ "navigationBarTitleText": "鑰楁潗搴撳瓨绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/consumablesLogistics/stockManagement/add",
+ "style": {
+ "navigationBarTitleText": "鏂板搴撳瓨",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/consumablesLogistics/stockManagement/subtract",
+ "style": {
+ "navigationBarTitleText": "鍑哄簱",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/consumablesLogistics/stockManagement/view",
+ "style": {
+ "navigationBarTitleText": "搴撳瓨璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/consumablesLogistics/stockReport/index",
+ "style": {
+ "navigationBarTitleText": "鑰楁潗搴撳瓨鎶ヨ〃",
+ "navigationStyle": "custom"
+ }
+ },
+ {
"path": "pages/safeProduction/safeQualifications/index",
"style": {
"navigationBarTitleText": "瑙勭▼涓庤祫璐�",
diff --git a/src/pages/consumablesLogistics/dispatchLog/Record.vue b/src/pages/consumablesLogistics/dispatchLog/Record.vue
new file mode 100644
index 0000000..a4afc5e
--- /dev/null
+++ b/src/pages/consumablesLogistics/dispatchLog/Record.vue
@@ -0,0 +1,197 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title ml10">鍑哄簱鏃ユ湡锛�</span>
+ <el-date-picker
+ v-model="searchForm.timeStr"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ @change="handleQuery"
+ />
+ <span class="search_title ml10">浜у搧鍚嶇О锛�</span>
+ <el-input
+ v-model="searchForm.productName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ clearable
+ @keyup.enter="handleQuery"
+ />
+ <span class="search_title ml10">鏉ユ簮锛�</span>
+ <el-select
+ v-model="searchForm.recordType"
+ style="width: 240px"
+ placeholder="璇烽�夋嫨"
+ clearable
+ >
+ <el-option
+ v-for="item in stockRecordTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">鎼滅储</el-button>
+ </div>
+ <div>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+
+ <div class="table_list">
+ <el-table
+ :data="tableData"
+ border
+ v-loading="tableLoading"
+ @selection-change="handleSelectionChange"
+ style="width: 100%"
+ 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="createTime" width="160" show-overflow-tooltip />
+ <el-table-column label="浜у搧鍚嶇О" prop="productName" min-width="160" show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="model" min-width="160" show-overflow-tooltip />
+ <el-table-column label="鍗曚綅" prop="unit" width="100" show-overflow-tooltip />
+ <el-table-column label="鍑哄簱鏁伴噺" prop="stockOutNum" width="110" show-overflow-tooltip />
+ <el-table-column label="鍑哄簱浜�" prop="createBy" width="120" show-overflow-tooltip />
+ <el-table-column label="鏉ユ簮" prop="recordType" width="140" show-overflow-tooltip>
+ <template #default="scope">
+ {{ getRecordType(scope.row.recordType) }}
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <Pagination
+ v-show="total > 0"
+ :total="total"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { onMounted, reactive, ref, toRefs, watch } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import Pagination from "@/components/PIMTable/Pagination.vue";
+import {
+ delConsumablesOutRecord,
+ getConsumablesOutRecordPage,
+} from "@/api/consumablesLogistics/consumablesOutRecord.js";
+import {
+ findAllQualifiedStockOutRecordTypeOptions,
+ findAllUnQualifiedStockOutRecordTypeOptions,
+} from "@/api/basicData/enum.js";
+
+const props = defineProps({
+ type: {
+ type: String,
+ required: true,
+ default: "0",
+ },
+});
+
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const stockRecordTypeOptions = ref([]);
+
+const page = reactive({
+ current: 1,
+ size: 100,
+});
+const total = ref(0);
+
+const data = reactive({
+ searchForm: {
+ timeStr: "",
+ productName: "",
+ recordType: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+const getRecordType = (recordType) => {
+ return stockRecordTypeOptions.value.find((item) => item.value === recordType)?.label || "";
+};
+
+const fetchStockRecordTypeOptions = () => {
+ const api =
+ props.type === "1"
+ ? findAllUnQualifiedStockOutRecordTypeOptions
+ : findAllQualifiedStockOutRecordTypeOptions;
+ api()
+ .then((res) => {
+ stockRecordTypeOptions.value = res.data || [];
+ })
+ .catch(() => {
+ stockRecordTypeOptions.value = [];
+ });
+};
+
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+
+const paginationChange = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ getConsumablesOutRecordPage({ ...searchForm.value, ...page, type: props.type })
+ .then(res => {
+ tableData.value = res?.data?.records || [];
+ total.value = res?.data?.total || 0;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const handleSelectionChange = selection => {
+ selectedRows.value = Array.isArray(selection) ? selection : [];
+};
+
+const handleDelete = () => {
+ const ids = selectedRows.value.map(i => i.id).filter(Boolean);
+ if (ids.length === 0) {
+ ElMessage.warning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => delConsumablesOutRecord(ids))
+ .then(() => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {});
+};
+
+watch(
+ () => props.type,
+ () => {
+ searchForm.value.recordType = "";
+ fetchStockRecordTypeOptions();
+ handleQuery();
+ }
+);
+
+onMounted(() => {
+ fetchStockRecordTypeOptions();
+ getList();
+});
+</script>
diff --git a/src/pages/consumablesLogistics/dispatchLog/index.vue b/src/pages/consumablesLogistics/dispatchLog/index.vue
new file mode 100644
index 0000000..132494f
--- /dev/null
+++ b/src/pages/consumablesLogistics/dispatchLog/index.vue
@@ -0,0 +1,223 @@
+<template>
+ <view class="dispatch-page">
+ <PageHeader title="鍑哄簱鍙拌处" @back="goBack" />
+ <view class="tabs-wrap">
+ <view
+ v-for="tab in tabs"
+ :key="tab.name"
+ class="tab-item"
+ :class="{ active: activeTab === tab.name }"
+ @click="activeTab = tab.name"
+ >
+ <text>{{ tab.label }}</text>
+ </view>
+ </view>
+ <view class="search-section">
+ <view class="search-row">
+ <view class="search-input-wrap">
+ <up-input v-model="searchForm.productName" placeholder="浜у搧澶х被" clearable />
+ </view>
+ <view class="btn-search" @click="handleQuery">
+ <view class="btn-search-inner">
+ <up-icon name="search" size="22" color="#fff"></up-icon>
+ <text>鎼滅储</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view class="list-section">
+ <view v-if="tableData.length > 0">
+ <view v-for="(item, index) in tableData" :key="item.id || index" class="card-item">
+ <view class="card-click" @click="goDetail(item)">
+ <view class="card-header">
+ <view class="header-main">
+ <text class="product-name">{{ item.productName }}</text>
+ <text class="outbound-date">{{ item.createTime }}</text>
+ </view>
+ </view>
+ <up-divider />
+ <view class="card-body">
+ <view class="row"><text class="l">瑙勬牸鍨嬪彿</text><text class="r">{{ item.model }}</text></view>
+ <view class="row"><text class="l">鍗曚綅</text><text class="r">{{ item.unit }}</text></view>
+ <view class="row"><text class="l">鍑哄簱鏁伴噺</text><text class="r highlight">{{ item.stockOutNum }}</text></view>
+ <view class="row"><text class="l">鍑哄簱浜�</text><text class="r">{{ item.createBy }}</text></view>
+ <view class="row" v-if="item.recordType !== undefined"><text class="l">鏉ユ簮</text><text class="r">{{ getRecordType(item.recordType) || item.recordType }}</text></view>
+ <view class="row"><text class="l">姣涢噸(鍚�)</text><text class="r">{{ item.grossWeight ?? '-' }}</text></view>
+ <view class="row"><text class="l">鐨噸(鍚�)</text><text class="r">{{ item.tareWeight ?? '-' }}</text></view>
+ <view class="row"><text class="l">鍑�閲�(鍚�)</text><text class="r">{{ item.netWeight ?? '-' }}</text></view>
+ <view class="row"><text class="l">杩囩鏃ユ湡</text><text class="r">{{ item.weighingDate || '-' }}</text></view>
+ <view class="row"><text class="l">杩囩鍛�</text><text class="r">{{ item.weighingOperator || '-' }}</text></view>
+ </view>
+ </view>
+ <view class="card-actions">
+ <view class="btn-delete" @click.stop="handleDeleteSingle(item)">鍒犻櫎</view>
+ </view>
+ </view>
+ </view>
+ <view v-else class="no-data">鏆傛棤鏁版嵁</view>
+ </view>
+ <view class="load-more-wrap" v-if="tableData.length > 0">
+ <u-loadmore :status="loadStatus" @loadmore="loadMore" />
+ </view>
+ </view>
+</template>
+
+<script setup>
+import { reactive, ref, toRefs, watch } from "vue";
+import { onReachBottom, onShow } from "@dcloudio/uni-app";
+import PageHeader from "@/components/PageHeader.vue";
+import { getConsumablesOutRecordPage, delConsumablesOutRecord } from "@/api/consumablesLogistics/consumablesOutRecord.js";
+import { findAllQualifiedStockOutRecordTypeOptions, findAllUnQualifiedStockOutRecordTypeOptions } from "@/api/basicData/enum.js";
+
+const activeTab = ref("qualified");
+const stockRecordTypeOptions = ref([]);
+const tabs = [
+ { label: "鍚堟牸鍑哄簱", name: "qualified", type: "0" },
+ { label: "涓嶅悎鏍煎嚭搴�", name: "unqualified", type: "1" },
+];
+const tableData = ref([]);
+const total = ref(0);
+const loadStatus = ref("loadmore");
+const page = reactive({ current: 1, size: 20 });
+const data = reactive({ searchForm: { productName: "" } });
+const { searchForm } = toRefs(data);
+
+const currentType = () => tabs.find((t) => t.name === activeTab.value)?.type || "0";
+
+function getRecordType(recordType) {
+ if (recordType == null || recordType === "") return "";
+ return stockRecordTypeOptions.value.find((item) => item.value === recordType)?.label || "";
+}
+
+function fetchRecordTypeOptions() {
+ const api =
+ currentType() === "1"
+ ? findAllUnQualifiedStockOutRecordTypeOptions
+ : findAllQualifiedStockOutRecordTypeOptions;
+ api()
+ .then((res) => {
+ const list = res.data != null ? res.data : res;
+ stockRecordTypeOptions.value = Array.isArray(list) ? list : [];
+ })
+ .catch(() => {
+ stockRecordTypeOptions.value = [];
+ });
+}
+
+const getList = () => {
+ const isFirstPage = page.current === 1;
+ if (isFirstPage) {
+ uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
+ }
+ getConsumablesOutRecordPage({
+ ...page,
+ type: currentType(),
+ productName: searchForm.value.productName,
+ })
+ .then((res) => {
+ uni.hideLoading();
+ const records = res.data?.records || [];
+ const totalCount = res.data?.total || 0;
+ if (isFirstPage) {
+ tableData.value = records;
+ fetchRecordTypeOptions();
+ } else {
+ tableData.value = [...tableData.value, ...records];
+ }
+ total.value = totalCount;
+ loadStatus.value = tableData.value.length >= totalCount || totalCount === 0 ? "nomore" : "loadmore";
+ })
+ .catch(() => {
+ uni.hideLoading();
+ loadStatus.value = "error";
+ if (isFirstPage) {
+ uni.showToast({ title: "鍔犺浇澶辫触", icon: "none" });
+ }
+ });
+};
+
+const loadMore = () => {
+ if (loadStatus.value === "nomore" || loadStatus.value === "loading") return;
+ loadStatus.value = "loading";
+ page.current++;
+ getList();
+};
+
+watch(activeTab, () => {
+ page.current = 1;
+ loadStatus.value = "loadmore";
+ stockRecordTypeOptions.value = [];
+ getList();
+});
+
+const handleQuery = () => {
+ page.current = 1;
+ loadStatus.value = "loadmore";
+ getList();
+};
+
+const goDetail = (item) => {
+ if (!item?.id) return;
+ try {
+ uni.setStorageSync(
+ "dispatchDetailItem",
+ JSON.stringify({
+ item,
+ type: currentType(),
+ })
+ );
+ } catch (e) {}
+ uni.navigateTo({
+ url: "/pages/consumablesLogistics/dispatchLog/view?id=" + item.id,
+ });
+};
+
+const handleDeleteSingle = (item) => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "鍒犻櫎",
+ content: "纭鍒犻櫎璇ユ潯鍑哄簱璁板綍锛�",
+ success: (res) => {
+ if (!res.confirm) return;
+ delConsumablesOutRecord([item.id])
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ });
+ },
+ });
+};
+
+const goBack = () => uni.navigateBack();
+onShow(() => getList());
+onReachBottom(() => loadMore());
+</script>
+
+<style lang="scss" scoped>
+.dispatch-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 40rpx; }
+.tabs-wrap { display: flex; background: #fff; padding: 24rpx; gap: 24rpx; }
+.tab-item { flex: 1; text-align: center; padding: 20rpx; border-radius: 12rpx; background: #f0f0f0; font-size: 28rpx; color: #666; }
+.tab-item.active { background: #2979ff; color: #fff; }
+.search-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
+.search-row { display: flex; align-items: center; }
+.search-input-wrap { flex: 1; margin-right: 20rpx; min-width: 0; }
+.btn-search { display: flex; align-items: center; justify-content: center; width: 160rpx; min-height: 72rpx; flex-shrink: 0; padding: 20rpx 24rpx; background: #2979ff; color: #fff; border-radius: 12rpx; font-size: 28rpx; box-sizing: border-box; text-align: center; }
+.btn-search-inner { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 8rpx; }
+.list-section { margin: 24rpx; }
+.card-item { background: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
+.card-header { padding: 4rpx 0 12rpx; }
+.header-main { display: flex; justify-content: space-between; gap: 16rpx; }
+.product-name { font-size: 30rpx; font-weight: 500; color: #333; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.outbound-date { font-size: 24rpx; color: #999; }
+.card-body .row { display: flex; justify-content: space-between; padding: 6rpx 0; font-size: 26rpx; }
+.card-body .l { color: #666; }
+.card-body .r { color: #333; }
+.card-body .r.highlight { color: #2979ff; font-weight: 500; }
+.card-actions { display: flex; justify-content: flex-end; margin-top: 12rpx; }
+.btn-delete { color: #f56c6c; font-size: 28rpx; }
+.no-data { text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx; }
+.load-more-wrap { padding: 24rpx 24rpx 8rpx; }
+</style>
diff --git a/src/pages/consumablesLogistics/dispatchLog/view.vue b/src/pages/consumablesLogistics/dispatchLog/view.vue
new file mode 100644
index 0000000..0008348
--- /dev/null
+++ b/src/pages/consumablesLogistics/dispatchLog/view.vue
@@ -0,0 +1,180 @@
+<template>
+ <view class="detail-page">
+ <PageHeader title="鍑哄簱璇︽儏" @back="goBack" />
+ <view v-if="loading" class="loading-wrap">
+ <text class="loading-text">鍔犺浇涓�...</text>
+ </view>
+ <view v-else-if="detail" class="detail-wrap">
+ <view class="section-card">
+ <view class="section-head">
+ <view class="section-dot"></view>
+ <text class="section-title">鍩虹淇℃伅</text>
+ </view>
+ <view class="section-body">
+ <view class="detail-row">
+ <text class="label">搴忓彿</text>
+ <text class="value">{{ detail.index ?? '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鍑哄簱鎵规</text>
+ <text class="value value-strong">{{ detail.outboundBatches || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鍑哄簱鏃堕棿</text>
+ <text class="value">{{ detail.createTime || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">浜у搧澶х被</text>
+ <text class="value value-strong">{{ detail.productName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">瑙勬牸鍨嬪彿</text>
+ <text class="value">{{ detail.model || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鍗曚綅</text>
+ <text class="value">{{ detail.unit || '-' }}</text>
+ </view>
+ <view class="detail-row detail-row-highlight">
+ <text class="label">鍑哄簱鏁伴噺</text>
+ <text class="value value-num">{{ detail.stockOutNum ?? '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鍑哄簱浜�</text>
+ <text class="value">{{ detail.createBy || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鏉ユ簮</text>
+ <text class="value">{{ getRecordType(detail.recordType) || '-' }}</text>
+ </view>
+ </view>
+ </view>
+ <view class="section-card">
+ <view class="section-head">
+ <view class="section-dot"></view>
+ <text class="section-title">鍑哄簱淇℃伅</text>
+ </view>
+ <view class="section-body">
+ <view class="detail-row">
+ <text class="label">杞︾墝鍙�</text>
+ <text class="value">{{ detail.licensePlateNo || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">姣涢噸(鍚�)</text>
+ <text class="value">{{ detail.grossWeight ?? '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鐨噸(鍚�)</text>
+ <text class="value">{{ detail.tareWeight ?? '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鍑�閲�(鍚�)</text>
+ <text class="value">{{ detail.netWeight ?? '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">杩囩鏃ユ湡</text>
+ <text class="value">{{ detail.weighingDate || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">杩囩鍛�</text>
+ <text class="value">{{ detail.weighingOperator || '-' }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else class="empty">
+ <text class="empty-text">鏆傛棤璇︽儏鏁版嵁</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
+import PageHeader from "@/components/PageHeader.vue";
+import { findAllQualifiedStockOutRecordTypeOptions, findAllUnQualifiedStockOutRecordTypeOptions } from "@/api/basicData/enum.js";
+
+const detail = ref(null);
+const loading = ref(true);
+const stockRecordTypeOptions = ref([]);
+
+function normalizeDetail(raw) {
+ if (!raw) return null;
+ const d = typeof raw === "object" ? raw : {};
+ return {
+ index: d.index ?? 1,
+ outboundBatches: d.outboundBatches,
+ createTime: d.createTime,
+ productName: d.productName,
+ model: d.model,
+ unit: d.unit,
+ stockOutNum: d.stockOutNum,
+ createBy: d.createBy,
+ recordType: d.recordType,
+ licensePlateNo: d.licensePlateNo,
+ grossWeight: d.grossWeight,
+ tareWeight: d.tareWeight,
+ netWeight: d.netWeight,
+ weighingDate: d.weighingDate,
+ weighingOperator: d.weighingOperator,
+ };
+}
+
+function getRecordType(recordType) {
+ if (recordType == null || recordType === "") return "";
+ return stockRecordTypeOptions.value.find((item) => item.value === recordType)?.label || "";
+}
+
+function fetchRecordTypeOptions(type) {
+ const api =
+ type === "1" ? findAllUnQualifiedStockOutRecordTypeOptions : findAllQualifiedStockOutRecordTypeOptions;
+ api()
+ .then((res) => {
+ const data = res.data != null ? res.data : res;
+ stockRecordTypeOptions.value = Array.isArray(data) ? data : [];
+ })
+ .catch(() => {
+ stockRecordTypeOptions.value = [];
+ });
+}
+
+onLoad(() => {
+ const cached = uni.getStorageSync("dispatchDetailItem");
+ if (cached) {
+ try {
+ const payload = typeof cached === "string" ? JSON.parse(cached) : cached;
+ const item = payload && payload.item != null ? payload.item : payload;
+ const type = payload && payload.type != null ? payload.type : "0";
+ detail.value = normalizeDetail({ ...item, index: 1 });
+ fetchRecordTypeOptions(type);
+ uni.removeStorageSync("dispatchDetailItem");
+ } catch (e) {
+ uni.removeStorageSync("dispatchDetailItem");
+ }
+ }
+ loading.value = false;
+});
+
+const goBack = () => uni.navigateBack();
+</script>
+
+<style lang="scss" scoped>
+.detail-page { min-height: 100vh; background: linear-gradient(180deg, #e8eef7 0%, #f2f5fa 100%); padding-bottom: 48rpx; }
+.loading-wrap { padding: 120rpx 48rpx; text-align: center; }
+.loading-text { color: #8c9aa8; font-size: 28rpx; }
+.empty { padding: 120rpx 48rpx; text-align: center; }
+.empty-text { color: #8c9aa8; font-size: 28rpx; }
+.detail-wrap { padding: 24rpx 24rpx 32rpx; }
+.section-card { background: #fff; border-radius: 24rpx; overflow: hidden; margin-bottom: 28rpx; box-shadow: 0 8rpx 32rpx rgba(41, 121, 255, 0.06); border: 1rpx solid rgba(41, 121, 255, 0.06); }
+.section-head { display: flex; align-items: center; padding: 28rpx 32rpx; background: linear-gradient(135deg, #f8fbff 0%, #f0f6ff 100%); border-bottom: 1rpx solid #eef3fa; }
+.section-dot { width: 8rpx; height: 8rpx; border-radius: 50%; background: #2979ff; margin-right: 16rpx; }
+.section-title { font-size: 30rpx; font-weight: 600; color: #1e3a5f; letter-spacing: 0.5rpx; }
+.section-body { padding: 8rpx 32rpx 24rpx; }
+.detail-row { display: flex; align-items: center; min-height: 96rpx; padding: 0 16rpx; border-radius: 12rpx; font-size: 28rpx; margin-bottom: 4rpx; }
+.detail-row .label { width: 200rpx; flex-shrink: 0; color: #6b7c93; font-size: 26rpx; }
+.detail-row .value { flex: 1; color: #2c3e50; text-align: right; word-break: break-all; font-size: 28rpx; }
+.detail-row .value-strong { color: #1e3a5f; font-weight: 500; }
+.detail-row .value-num { color: #2979ff; font-weight: 600; font-size: 32rpx; }
+.detail-row-highlight { background: linear-gradient(90deg, rgba(41, 121, 255, 0.06) 0%, transparent 100%); margin: 12rpx -16rpx 4rpx; padding: 20rpx 16rpx; }
+</style>
+
diff --git a/src/pages/consumablesLogistics/receiptManagement/Record.vue b/src/pages/consumablesLogistics/receiptManagement/Record.vue
new file mode 100644
index 0000000..9770be2
--- /dev/null
+++ b/src/pages/consumablesLogistics/receiptManagement/Record.vue
@@ -0,0 +1,197 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title ml10">鍏ュ簱鏃ユ湡锛�</span>
+ <el-date-picker
+ v-model="searchForm.timeStr"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ @change="handleQuery"
+ />
+ <span class="search_title ml10">浜у搧鍚嶇О锛�</span>
+ <el-input
+ v-model="searchForm.productName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ clearable
+ @keyup.enter="handleQuery"
+ />
+ <span class="search_title ml10">鏉ユ簮锛�</span>
+ <el-select
+ v-model="searchForm.recordType"
+ style="width: 240px"
+ placeholder="璇烽�夋嫨"
+ clearable
+ >
+ <el-option
+ v-for="item in stockRecordTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">鎼滅储</el-button>
+ </div>
+ <div>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+
+ <div class="table_list">
+ <el-table
+ :data="tableData"
+ border
+ v-loading="tableLoading"
+ @selection-change="handleSelectionChange"
+ style="width: 100%"
+ 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="createTime" width="160" show-overflow-tooltip />
+ <el-table-column label="浜у搧鍚嶇О" prop="productName" min-width="160" show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="model" min-width="160" show-overflow-tooltip />
+ <el-table-column label="鍗曚綅" prop="unit" width="100" show-overflow-tooltip />
+ <el-table-column label="鍏ュ簱鏁伴噺" prop="stockInNum" width="110" show-overflow-tooltip />
+ <el-table-column label="鍏ュ簱浜�" prop="createBy" width="120" show-overflow-tooltip />
+ <el-table-column label="鏉ユ簮" prop="recordType" width="140" show-overflow-tooltip>
+ <template #default="scope">
+ {{ getRecordType(scope.row.recordType) }}
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <Pagination
+ v-show="total > 0"
+ :total="total"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { onMounted, reactive, ref, toRefs, watch } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import Pagination from "@/components/PIMTable/Pagination.vue";
+import {
+ batchDeleteConsumablesInRecords,
+ getConsumablesInRecordListPage,
+} from "@/api/consumablesLogistics/consumablesInRecord.js";
+import {
+ findAllQualifiedStockInRecordTypeOptions,
+ findAllUnQualifiedStockInRecordTypeOptions,
+} from "@/api/basicData/enum.js";
+
+const props = defineProps({
+ type: {
+ type: String,
+ required: true,
+ default: "0",
+ },
+});
+
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const stockRecordTypeOptions = ref([]);
+
+const page = reactive({
+ current: 1,
+ size: 100,
+});
+const total = ref(0);
+
+const data = reactive({
+ searchForm: {
+ timeStr: "",
+ productName: "",
+ recordType: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+const getRecordType = (recordType) => {
+ return stockRecordTypeOptions.value.find((item) => item.value === recordType)?.label || "";
+};
+
+const fetchStockRecordTypeOptions = () => {
+ const api =
+ props.type === "1"
+ ? findAllUnQualifiedStockInRecordTypeOptions
+ : findAllQualifiedStockInRecordTypeOptions;
+ api()
+ .then((res) => {
+ stockRecordTypeOptions.value = res.data || [];
+ })
+ .catch(() => {
+ stockRecordTypeOptions.value = [];
+ });
+};
+
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+
+const paginationChange = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ getConsumablesInRecordListPage({ ...searchForm.value, ...page, type: props.type })
+ .then(res => {
+ tableData.value = res?.data?.records || [];
+ total.value = res?.data?.total || 0;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const handleSelectionChange = selection => {
+ selectedRows.value = Array.isArray(selection) ? selection : [];
+};
+
+const handleDelete = () => {
+ const ids = selectedRows.value.map(i => i.id).filter(Boolean);
+ if (ids.length === 0) {
+ ElMessage.warning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => batchDeleteConsumablesInRecords(ids))
+ .then(() => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {});
+};
+
+watch(
+ () => props.type,
+ () => {
+ searchForm.value.recordType = "";
+ fetchStockRecordTypeOptions();
+ handleQuery();
+ }
+);
+
+onMounted(() => {
+ fetchStockRecordTypeOptions();
+ getList();
+});
+</script>
diff --git a/src/pages/consumablesLogistics/receiptManagement/index.vue b/src/pages/consumablesLogistics/receiptManagement/index.vue
new file mode 100644
index 0000000..db0f94e
--- /dev/null
+++ b/src/pages/consumablesLogistics/receiptManagement/index.vue
@@ -0,0 +1,231 @@
+<template>
+ <view class="receipt-page">
+ <PageHeader title="鍏ュ簱绠$悊" @back="goBack" />
+ <view class="tabs-wrap">
+ <view
+ v-for="tab in tabs"
+ :key="tab.name"
+ class="tab-item"
+ :class="{ active: activeTab === tab.name }"
+ @click="activeTab = tab.name"
+ >
+ <text>{{ tab.label }}</text>
+ </view>
+ </view>
+ <view class="search-section">
+ <view class="search-row">
+ <view class="search-input-wrap">
+ <up-input v-model="searchForm.productName" placeholder="浜у搧澶х被" clearable />
+ </view>
+ <view class="btn-search" @click="handleQuery">
+ <view class="btn-search-inner">
+ <up-icon name="search" size="22" color="#fff"></up-icon>
+ <text>鎼滅储</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view class="list-section">
+ <view v-if="tableData.length > 0">
+ <view v-for="(item, index) in tableData" :key="item.id || index" class="card-item">
+ <view class="card-click" @click="goDetail(item)">
+ <view class="card-header">
+ <view class="header-main">
+ <text class="product-name">{{ item.productName }}</text>
+ <text class="inbound-date">{{ item.createTime || item.inboundDate }}</text>
+ </view>
+ </view>
+ <up-divider />
+ <view class="card-body">
+ <view class="row"><text class="l">瑙勬牸鍨嬪彿</text><text class="r">{{ item.model }}</text></view>
+ <view class="row"><text class="l">鍗曚綅</text><text class="r">{{ item.unit }}</text></view>
+ <view class="row"><text class="l">鍏ュ簱鏁伴噺</text><text class="r highlight">{{ item.stockInNum }}</text></view>
+ <view class="row"><text class="l">鍏ュ簱浜�</text><text class="r">{{ item.createBy }}</text></view>
+ <view class="row" v-if="item.recordType !== undefined"><text class="l">鏉ユ簮</text><text class="r">{{ getRecordType(item.recordType) || item.recordType }}</text></view>
+ </view>
+ </view>
+ <view class="card-actions">
+ <view class="btn-delete" @click.stop="handleDeleteSingle(item)">鍒犻櫎</view>
+ </view>
+ </view>
+ </view>
+ <view v-else class="no-data">鏆傛棤鏁版嵁</view>
+ </view>
+ <view class="load-more-wrap" v-if="tableData.length > 0">
+ <u-loadmore :status="loadStatus" @loadmore="loadMore" />
+ </view>
+ </view>
+</template>
+
+<script setup>
+import { reactive, ref, toRefs, watch } from "vue";
+import { onReachBottom, onShow } from "@dcloudio/uni-app";
+import PageHeader from "@/components/PageHeader.vue";
+import request from "@/utils/request";
+import { findAllQualifiedStockInRecordTypeOptions, findAllUnQualifiedStockInRecordTypeOptions } from "@/api/basicData/enum.js";
+
+const activeTab = ref("qualified");
+const stockRecordTypeOptions = ref([]);
+const tabs = [
+ { label: "鍚堟牸鍏ュ簱", name: "qualified", type: "0" },
+ { label: "涓嶅悎鏍煎叆搴�", name: "unqualified", type: "1" },
+];
+const tableData = ref([]);
+const total = ref(0);
+const loadStatus = ref("loadmore");
+const page = reactive({ current: 1, size: 4 });
+const data = reactive({ searchForm: { productName: "" } });
+const { searchForm } = toRefs(data);
+
+const currentType = () => tabs.find((t) => t.name === activeTab.value)?.type || "0";
+
+function getRecordType(recordType) {
+ if (recordType == null || recordType === "") return "";
+ return stockRecordTypeOptions.value.find((item) => item.value === recordType)?.label || "";
+}
+
+function fetchRecordTypeOptions() {
+ const api =
+ currentType() === "1"
+ ? findAllUnQualifiedStockInRecordTypeOptions
+ : findAllQualifiedStockInRecordTypeOptions;
+ api()
+ .then((res) => {
+ const data = res.data != null ? res.data : res;
+ stockRecordTypeOptions.value = Array.isArray(data) ? data : [];
+ })
+ .catch(() => {
+ stockRecordTypeOptions.value = [];
+ });
+}
+
+const getList = () => {
+ const isFirstPage = page.current === 1;
+ if (isFirstPage) {
+ uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
+ }
+ request({
+ url: "/consumablesInRecord/listPage",
+ method: "get",
+ params: {
+ ...page,
+ type: currentType(),
+ productName: searchForm.value.productName,
+ },
+ })
+ .then((res) => {
+ uni.hideLoading();
+ const records = res.data?.records || [];
+ const totalCount = res.data?.total || 0;
+ if (isFirstPage) {
+ tableData.value = records;
+ fetchRecordTypeOptions();
+ } else {
+ tableData.value = [...tableData.value, ...records];
+ }
+ total.value = totalCount;
+ loadStatus.value = tableData.value.length >= totalCount || totalCount === 0 ? "nomore" : "loadmore";
+ })
+ .catch(() => {
+ uni.hideLoading();
+ loadStatus.value = "error";
+ if (isFirstPage) {
+ uni.showToast({ title: "鍔犺浇澶辫触", icon: "none" });
+ }
+ });
+};
+
+const loadMore = () => {
+ if (loadStatus.value === "nomore" || loadStatus.value === "loading") return;
+ loadStatus.value = "loading";
+ page.current++;
+ getList();
+};
+
+watch(activeTab, () => {
+ page.current = 1;
+ loadStatus.value = "loadmore";
+ stockRecordTypeOptions.value = [];
+ getList();
+});
+
+const handleQuery = () => {
+ page.current = 1;
+ loadStatus.value = "loadmore";
+ getList();
+};
+
+const goDetail = (item) => {
+ if (!item?.id) return;
+ try {
+ uni.setStorageSync(
+ "receiptDetailItem",
+ JSON.stringify({
+ item,
+ type: currentType(),
+ })
+ );
+ } catch (e) {}
+ uni.navigateTo({
+ url: "/pages/consumablesLogistics/receiptManagement/view?id=" + item.id,
+ });
+};
+
+const handleDeleteSingle = (item) => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "鍒犻櫎",
+ content: "纭鍒犻櫎璇ユ潯鍏ュ簱璁板綍锛�",
+ success: (res) => {
+ if (!res.confirm) return;
+ request({
+ url: "/consumablesInRecord",
+ method: "delete",
+ data: [item.id],
+ })
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ });
+ },
+ });
+};
+
+const goBack = () => uni.navigateBack();
+
+onShow(() => {
+ getList();
+});
+onReachBottom(() => {
+ loadMore();
+});
+</script>
+
+<style lang="scss" scoped>
+.receipt-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 40rpx; }
+.tabs-wrap { display: flex; background: #fff; padding: 24rpx; gap: 24rpx; }
+.tab-item { flex: 1; text-align: center; padding: 20rpx; border-radius: 12rpx; background: #f0f0f0; font-size: 28rpx; color: #666; }
+.tab-item.active { background: #2979ff; color: #fff; }
+.search-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
+.search-row { display: flex; align-items: center; }
+.search-input-wrap { flex: 1; margin-right: 20rpx; min-width: 0; }
+.btn-search { display: flex; align-items: center; justify-content: center; width: 160rpx; min-height: 72rpx; flex-shrink: 0; padding: 20rpx 24rpx; background: #2979ff; color: #fff; border-radius: 12rpx; font-size: 28rpx; box-sizing: border-box; text-align: center; }
+.btn-search-inner { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 8rpx; }
+.list-section { margin: 24rpx; }
+.card-item { background: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
+.card-header { padding: 4rpx 0 12rpx; }
+.header-main { display: flex; justify-content: space-between; gap: 16rpx; }
+.product-name { font-size: 30rpx; font-weight: 500; color: #333; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.inbound-date { font-size: 24rpx; color: #999; }
+.card-body .row { display: flex; justify-content: space-between; padding: 6rpx 0; font-size: 26rpx; }
+.card-body .l { color: #666; }
+.card-body .r { color: #333; }
+.card-body .r.highlight { color: #2979ff; font-weight: 500; }
+.card-actions { display: flex; justify-content: flex-end; margin-top: 12rpx; }
+.btn-delete { color: #f56c6c; font-size: 28rpx; }
+.no-data { text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx; }
+.load-more-wrap { padding: 24rpx 24rpx 8rpx; }
+</style>
diff --git a/src/pages/consumablesLogistics/receiptManagement/view.vue b/src/pages/consumablesLogistics/receiptManagement/view.vue
new file mode 100644
index 0000000..180eb9a
--- /dev/null
+++ b/src/pages/consumablesLogistics/receiptManagement/view.vue
@@ -0,0 +1,180 @@
+<template>
+ <view class="detail-page">
+ <PageHeader title="鍏ュ簱璇︽儏" @back="goBack" />
+ <view v-if="loading" class="loading-wrap">
+ <text class="loading-text">鍔犺浇涓�...</text>
+ </view>
+ <view v-else-if="detail" class="detail-wrap">
+ <view class="section-card">
+ <view class="section-head">
+ <view class="section-dot"></view>
+ <text class="section-title">鍩虹淇℃伅</text>
+ </view>
+ <view class="section-body">
+ <view class="detail-row">
+ <text class="label">搴忓彿</text>
+ <text class="value">{{ detail.index ?? '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鍏ュ簱鎵规</text>
+ <text class="value value-strong">{{ detail.inboundBatches || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鍏ュ簱鏃堕棿</text>
+ <text class="value">{{ detail.createTime || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">浜у搧澶х被</text>
+ <text class="value value-strong">{{ detail.productName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">瑙勬牸鍨嬪彿</text>
+ <text class="value">{{ detail.model || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鍗曚綅</text>
+ <text class="value">{{ detail.unit || '-' }}</text>
+ </view>
+ <view class="detail-row detail-row-highlight">
+ <text class="label">鍏ュ簱鏁伴噺</text>
+ <text class="value value-num">{{ detail.stockInNum ?? '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鍏ュ簱浜�</text>
+ <text class="value">{{ detail.createBy || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鏉ユ簮</text>
+ <text class="value">{{ getRecordType(detail.recordType) || '-' }}</text>
+ </view>
+ </view>
+ </view>
+ <view class="section-card">
+ <view class="section-head">
+ <view class="section-dot"></view>
+ <text class="section-title">鍏ュ簱淇℃伅</text>
+ </view>
+ <view class="section-body">
+ <view class="detail-row">
+ <text class="label">杞︾墝鍙�</text>
+ <text class="value">{{ detail.licensePlateNo || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">姣涢噸(鍚�)</text>
+ <text class="value">{{ detail.grossWeight ?? '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鐨噸(鍚�)</text>
+ <text class="value">{{ detail.tareWeight ?? '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鍑�閲�(鍚�)</text>
+ <text class="value">{{ detail.netWeight ?? '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">杩囩鏃ユ湡</text>
+ <text class="value">{{ detail.weighingDate || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">杩囩鍛�</text>
+ <text class="value">{{ detail.weighingOperator || '-' }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else class="empty">
+ <text class="empty-text">鏆傛棤璇︽儏鏁版嵁</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
+import PageHeader from "@/components/PageHeader.vue";
+import { findAllQualifiedStockInRecordTypeOptions, findAllUnQualifiedStockInRecordTypeOptions } from "@/api/basicData/enum.js";
+
+const detail = ref(null);
+const loading = ref(true);
+const stockRecordTypeOptions = ref([]);
+
+function normalizeDetail(raw) {
+ if (!raw) return null;
+ const d = typeof raw === "object" ? raw : {};
+ return {
+ index: d.index ?? 1,
+ inboundBatches: d.inboundBatches,
+ createTime: d.createTime,
+ productName: d.productName,
+ model: d.model,
+ unit: d.unit,
+ stockInNum: d.stockInNum,
+ createBy: d.createBy,
+ recordType: d.recordType,
+ licensePlateNo: d.licensePlateNo,
+ grossWeight: d.grossWeight,
+ tareWeight: d.tareWeight,
+ netWeight: d.netWeight,
+ weighingDate: d.weighingDate,
+ weighingOperator: d.weighingOperator,
+ };
+}
+
+function getRecordType(recordType) {
+ if (recordType == null || recordType === "") return "";
+ return stockRecordTypeOptions.value.find((item) => item.value === recordType)?.label || "";
+}
+
+function fetchRecordTypeOptions(type) {
+ const api =
+ type === "1" ? findAllUnQualifiedStockInRecordTypeOptions : findAllQualifiedStockInRecordTypeOptions;
+ api()
+ .then((res) => {
+ const data = res.data != null ? res.data : res;
+ stockRecordTypeOptions.value = Array.isArray(data) ? data : [];
+ })
+ .catch(() => {
+ stockRecordTypeOptions.value = [];
+ });
+}
+
+onLoad(() => {
+ const cached = uni.getStorageSync("receiptDetailItem");
+ if (cached) {
+ try {
+ const payload = typeof cached === "string" ? JSON.parse(cached) : cached;
+ const item = payload && payload.item != null ? payload.item : payload;
+ const type = payload && payload.type != null ? payload.type : "0";
+ detail.value = normalizeDetail({ ...item, index: 1 });
+ fetchRecordTypeOptions(type);
+ uni.removeStorageSync("receiptDetailItem");
+ } catch (e) {
+ uni.removeStorageSync("receiptDetailItem");
+ }
+ }
+ loading.value = false;
+});
+
+const goBack = () => uni.navigateBack();
+</script>
+
+<style lang="scss" scoped>
+.detail-page { min-height: 100vh; background: linear-gradient(180deg, #e8eef7 0%, #f2f5fa 100%); padding-bottom: 48rpx; }
+.loading-wrap { padding: 120rpx 48rpx; text-align: center; }
+.loading-text { color: #8c9aa8; font-size: 28rpx; }
+.empty { padding: 120rpx 48rpx; text-align: center; }
+.empty-text { color: #8c9aa8; font-size: 28rpx; }
+.detail-wrap { padding: 24rpx 24rpx 32rpx; }
+.section-card { background: #fff; border-radius: 24rpx; overflow: hidden; margin-bottom: 28rpx; box-shadow: 0 8rpx 32rpx rgba(41, 121, 255, 0.06); border: 1rpx solid rgba(41, 121, 255, 0.06); }
+.section-head { display: flex; align-items: center; padding: 28rpx 32rpx; background: linear-gradient(135deg, #f8fbff 0%, #f0f6ff 100%); border-bottom: 1rpx solid #eef3fa; }
+.section-dot { width: 8rpx; height: 8rpx; border-radius: 50%; background: #2979ff; margin-right: 16rpx; }
+.section-title { font-size: 30rpx; font-weight: 600; color: #1e3a5f; letter-spacing: 0.5rpx; }
+.section-body { padding: 8rpx 32rpx 24rpx; }
+.detail-row { display: flex; align-items: center; min-height: 96rpx; padding: 0 16rpx; border-radius: 12rpx; font-size: 28rpx; margin-bottom: 4rpx; }
+.detail-row .label { width: 200rpx; flex-shrink: 0; color: #6b7c93; font-size: 26rpx; }
+.detail-row .value { flex: 1; color: #2c3e50; text-align: right; word-break: break-all; font-size: 28rpx; }
+.detail-row .value-strong { color: #1e3a5f; font-weight: 500; }
+.detail-row .value-num { color: #2979ff; font-weight: 600; font-size: 32rpx; }
+.detail-row-highlight { background: linear-gradient(90deg, rgba(41, 121, 255, 0.06) 0%, transparent 100%); margin: 12rpx -16rpx 4rpx; padding: 20rpx 16rpx; }
+</style>
+
diff --git a/src/pages/consumablesLogistics/stockManagement/Qualified.vue b/src/pages/consumablesLogistics/stockManagement/Qualified.vue
new file mode 100644
index 0000000..ab42864
--- /dev/null
+++ b/src/pages/consumablesLogistics/stockManagement/Qualified.vue
@@ -0,0 +1,91 @@
+<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
+ @keyup.enter="handleQuery"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">鎼滅储</el-button>
+ </div>
+ </div>
+
+ <div class="table_list">
+ <el-table
+ :data="tableData"
+ border
+ v-loading="tableLoading"
+ style="width: 100%"
+ height="calc(100vh - 18.5em)"
+ >
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="浜у搧鍚嶇О" prop="productName" min-width="180" show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="model" min-width="160" show-overflow-tooltip />
+ <el-table-column label="鍗曚綅" prop="unit" width="100" show-overflow-tooltip />
+ <el-table-column label="搴撳瓨鏁伴噺" prop="qualitity" width="110" show-overflow-tooltip />
+ <el-table-column label="鍐荤粨鏁伴噺" prop="lockedQuantity" width="110" show-overflow-tooltip />
+ <el-table-column label="鏈�杩戞洿鏂版椂闂�" prop="updateTime" width="180" show-overflow-tooltip />
+ <el-table-column label="澶囨敞" prop="remark" min-width="140" show-overflow-tooltip />
+ </el-table>
+
+ <Pagination
+ v-show="total > 0"
+ :total="total"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { reactive, ref, toRefs } from "vue";
+import Pagination from "@/components/PIMTable/Pagination.vue";
+import { getConsumablesInListPage } from "@/api/consumablesLogistics/consumablesIn.js";
+
+const tableData = ref([]);
+const tableLoading = ref(false);
+
+const page = reactive({
+ current: 1,
+ size: 100,
+});
+const total = ref(0);
+
+const data = reactive({
+ searchForm: {
+ productName: "",
+ },
+});
+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;
+ getConsumablesInListPage({ ...searchForm.value, ...page })
+ .then(res => {
+ tableData.value = res?.data?.records || [];
+ total.value = res?.data?.total || 0;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+getList();
+</script>
diff --git a/src/pages/consumablesLogistics/stockManagement/Unqualified.vue b/src/pages/consumablesLogistics/stockManagement/Unqualified.vue
new file mode 100644
index 0000000..76b193e
--- /dev/null
+++ b/src/pages/consumablesLogistics/stockManagement/Unqualified.vue
@@ -0,0 +1,91 @@
+<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
+ @keyup.enter="handleQuery"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">鎼滅储</el-button>
+ </div>
+ </div>
+
+ <div class="table_list">
+ <el-table
+ :data="tableData"
+ border
+ v-loading="tableLoading"
+ style="width: 100%"
+ height="calc(100vh - 18.5em)"
+ >
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="浜у搧鍚嶇О" prop="productName" min-width="180" show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="model" min-width="160" show-overflow-tooltip />
+ <el-table-column label="鍗曚綅" prop="unit" width="100" show-overflow-tooltip />
+ <el-table-column label="搴撳瓨鏁伴噺" prop="qualitity" width="110" show-overflow-tooltip />
+ <el-table-column label="鍐荤粨鏁伴噺" prop="lockedQuantity" width="110" show-overflow-tooltip />
+ <el-table-column label="鏈�杩戞洿鏂版椂闂�" prop="updateTime" width="180" show-overflow-tooltip />
+ <el-table-column label="澶囨敞" prop="remark" min-width="140" show-overflow-tooltip />
+ </el-table>
+
+ <Pagination
+ v-show="total > 0"
+ :total="total"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { reactive, ref, toRefs } from "vue";
+import Pagination from "@/components/PIMTable/Pagination.vue";
+import { getConsumablesUninventoryListPage } from "@/api/consumablesLogistics/consumablesUninventory.js";
+
+const tableData = ref([]);
+const tableLoading = ref(false);
+
+const page = reactive({
+ current: 1,
+ size: 100,
+});
+const total = ref(0);
+
+const data = reactive({
+ searchForm: {
+ productName: "",
+ },
+});
+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;
+ getConsumablesUninventoryListPage({ ...searchForm.value, ...page })
+ .then(res => {
+ tableData.value = res?.data?.records || [];
+ total.value = res?.data?.total || 0;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+getList();
+</script>
diff --git a/src/pages/consumablesLogistics/stockManagement/add.vue b/src/pages/consumablesLogistics/stockManagement/add.vue
new file mode 100644
index 0000000..fb36b60
--- /dev/null
+++ b/src/pages/consumablesLogistics/stockManagement/add.vue
@@ -0,0 +1,283 @@
+<template>
+ <view class="add-stock-page">
+ <PageHeader title="鏂板搴撳瓨" @back="goBack" />
+
+ <scroll-view scroll-y class="content-scroll">
+ <view class="form-section">
+ <view class="form-row">
+ <text class="form-label required">浜у搧鍚嶇О</text>
+ <view class="selector-trigger" @click="openProductSelector">
+ <text class="selector-text" :class="{ placeholder: !form.productName }">
+ {{ form.productName || "璇烽�夋嫨浜у搧" }}
+ </text>
+ <up-icon name="arrow-right" size="16" color="#999"></up-icon>
+ </view>
+ </view>
+ <view class="form-row">
+ <text class="form-label">瑙勬牸</text>
+ <up-input v-model="form.productModelName" disabled placeholder="璇烽�夋嫨浜у搧鍚庤嚜鍔ㄥ甫鍑�" />
+ </view>
+ <view class="form-row">
+ <text class="form-label">鍗曚綅</text>
+ <up-input v-model="form.unit" disabled placeholder="璇烽�夋嫨浜у搧鍚庤嚜鍔ㄥ甫鍑�" />
+ </view>
+ </view>
+
+ <view v-if="isQualified" class="form-section">
+ <view class="section-title">杩囩淇℃伅</view>
+ <view class="form-row">
+ <text class="form-label">杞︾墝鍙�</text>
+ <up-input v-model="form.licensePlateNo" placeholder="璇疯緭鍏ヨ溅鐗屽彿" />
+ </view>
+ <view class="form-row">
+ <text class="form-label">姣涢噸(鍚�)</text>
+ <up-input v-model="form.grossWeight" type="number" placeholder="璇疯緭鍏ユ瘺閲�" />
+ </view>
+ <view class="form-row">
+ <text class="form-label">鐨噸(鍚�)</text>
+ <up-input v-model="form.tareWeight" type="number" placeholder="璇疯緭鍏ョ毊閲�" />
+ </view>
+ <view class="form-row">
+ <text class="form-label">鍑�閲�(鍚�)</text>
+ <up-input v-model="form.netWeight" type="number" disabled placeholder="鑷姩璁$畻" />
+ </view>
+ <view class="form-row">
+ <text class="form-label">杩囩鏃ユ湡</text>
+ <view class="selector-trigger" @click="openWeighingDatePicker">
+ <text class="selector-text" :class="{ placeholder: !form.weighingDate }">
+ {{ form.weighingDate || "璇烽�夋嫨杩囩鏃ユ湡" }}
+ </text>
+ <up-icon name="calendar" size="16" color="#999"></up-icon>
+ </view>
+ </view>
+ <view class="form-row">
+ <text class="form-label">杩囩鍛�</text>
+ <up-input v-model="form.weighingOperator" placeholder="璇疯緭鍏ヨ繃纾呭憳" />
+ </view>
+ </view>
+
+ <view class="form-section">
+ <view class="form-row">
+ <text class="form-label">澶囨敞</text>
+ <up-input v-model="form.remark" type="textarea" placeholder="閫夊~" />
+ </view>
+ </view>
+ </scroll-view>
+
+ <view class="bottom-bar">
+ <view class="btn-submit" @click="handleSubmit">鎻愪氦</view>
+ </view>
+
+ <up-popup :show="showProductPopup" mode="bottom" @close="showProductPopup = false">
+ <view class="product-popup">
+ <view class="popup-header">
+ <text class="popup-title">閫夋嫨浜у搧</text>
+ </view>
+ <view class="popup-search">
+ <up-input v-model="productQuery.productName" placeholder="浜у搧澶х被" clearable />
+ <up-input v-model="productQuery.model" placeholder="鍨嬪彿鍚嶇О" clearable />
+ <view class="popup-search-btn" @click="loadProductList">鎼滅储</view>
+ </view>
+ <scroll-view scroll-y class="product-list">
+ <view
+ v-for="item in productList"
+ :key="item.id"
+ class="product-item"
+ @click="selectProduct(item)"
+ >
+ <view class="product-name-row">
+ <text class="product-name">{{ item.productName }}</text>
+ <text class="product-unit">{{ item.unit }}</text>
+ </view>
+ <view class="product-model">鍨嬪彿锛歿{ item.model }}</view>
+ </view>
+ <view v-if="!productLoading && productList.length === 0" class="no-data">鏆傛棤鏁版嵁</view>
+ </scroll-view>
+ </view>
+ </up-popup>
+
+ <up-popup :show="showWeighingDatePicker" mode="bottom" @close="showWeighingDatePicker = false">
+ <up-datetime-picker
+ :show="true"
+ v-model="weighingDateValue"
+ mode="datetime"
+ @confirm="onWeighingDateConfirm"
+ @cancel="showWeighingDatePicker = false"
+ />
+ </up-popup>
+ </view>
+</template>
+
+<script setup>
+import { computed, reactive, ref, watch } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
+import dayjs from "dayjs";
+import PageHeader from "@/components/PageHeader.vue";
+import { createConsumablesIn } from "@/api/consumablesLogistics/consumablesIn.js";
+import { createConsumablesUnInventory } from "@/api/consumablesLogistics/consumablesUninventory.js";
+import { productModelList } from "@/api/basicData/productModel.js";
+
+const form = reactive({
+ productId: undefined,
+ productModelId: undefined,
+ productName: "",
+ productModelName: "",
+ unit: "",
+ productType: undefined,
+ licensePlateNo: "",
+ grossWeight: "",
+ tareWeight: "",
+ netWeight: "",
+ weighingDate: "",
+ weighingOperator: "",
+ remark: "",
+});
+
+const type = ref("0");
+const isQualified = computed(() => type.value === "0");
+
+const showProductPopup = ref(false);
+const productQuery = reactive({
+ productName: "",
+ model: "",
+});
+const productList = ref([]);
+const productLoading = ref(false);
+
+const showWeighingDatePicker = ref(false);
+const weighingDateValue = ref(Date.now());
+
+onLoad((options) => {
+ if (options && options.type != null) {
+ type.value = options.type;
+ }
+});
+
+const openProductSelector = () => {
+ showProductPopup.value = true;
+ if (productList.value.length === 0) {
+ loadProductList();
+ }
+};
+
+const loadProductList = () => {
+ productLoading.value = true;
+ productModelList({
+ productName: productQuery.productName || "",
+ model: productQuery.model || "",
+ current: 1,
+ size: 20,
+ })
+ .then((res) => {
+ const data = res?.records || res?.data?.records || [];
+ productList.value = Array.isArray(data) ? data : [];
+ })
+ .finally(() => {
+ productLoading.value = false;
+ });
+};
+
+const selectProduct = (item) => {
+ form.productId = item.productId || item.id;
+ form.productModelId = item.id;
+ form.productName = item.productName;
+ form.productModelName = item.model;
+ form.unit = item.unit;
+ form.productType = item.productType;
+ showProductPopup.value = false;
+};
+
+const computeNetWeight = () => {
+ const gross = Number(form.grossWeight);
+ const tare = Number(form.tareWeight);
+ if (!isNaN(gross) && !isNaN(tare)) {
+ const net = Number((gross - tare).toFixed(2));
+ form.netWeight = net > 0 ? net : 0;
+ } else {
+ form.netWeight = "";
+ }
+};
+
+watch(
+ () => [form.grossWeight, form.tareWeight],
+ () => {
+ computeNetWeight();
+ }
+);
+
+const openWeighingDatePicker = () => {
+ weighingDateValue.value = form.weighingDate
+ ? dayjs(form.weighingDate, "YYYY-MM-DD HH:mm:ss").valueOf()
+ : Date.now();
+ showWeighingDatePicker.value = true;
+};
+
+const onWeighingDateConfirm = (e) => {
+ const ts = e?.value ?? weighingDateValue.value;
+ form.weighingDate = dayjs(ts).format("YYYY-MM-DD HH:mm:ss");
+ showWeighingDatePicker.value = false;
+};
+
+const handleSubmit = () => {
+ if (!form.productName || !form.productModelId) {
+ uni.showToast({ title: "璇烽�夋嫨浜у搧", icon: "none" });
+ return;
+ }
+ const payload = {
+ productId: form.productId,
+ productModelId: form.productModelId,
+ productName: form.productName,
+ productModelName: form.productModelName,
+ unit: form.unit,
+ productType: form.productType,
+ licensePlateNo: form.licensePlateNo,
+ grossWeight: form.grossWeight,
+ tareWeight: form.tareWeight,
+ netWeight: form.netWeight,
+ weighingDate: form.weighingDate,
+ weighingOperator: form.weighingOperator,
+ remark: form.remark,
+ };
+ const api = isQualified.value ? createConsumablesIn : createConsumablesUnInventory;
+ api(payload)
+ .then(() => {
+ uni.showToast({ title: "鏂板鎴愬姛", icon: "success" });
+ setTimeout(() => {
+ uni.navigateBack();
+ }, 400);
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏂板澶辫触", icon: "none" });
+ });
+};
+
+const goBack = () => uni.navigateBack();
+</script>
+
+<style lang="scss" scoped>
+.add-stock-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 100rpx; }
+.content-scroll { height: calc(100vh - 100rpx); }
+.form-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
+.section-title { font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; }
+.form-row { margin-bottom: 24rpx; }
+.form-label { display: block; font-size: 26rpx; color: #666; margin-bottom: 12rpx; }
+.form-label.required:before { content: "*"; color: #f56c6c; margin-right: 6rpx; }
+.selector-trigger { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; background: #f5f5f5; border-radius: 12rpx; }
+.selector-text { font-size: 28rpx; color: #333; }
+.selector-text.placeholder { color: #999; }
+.bottom-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); background: #fff; box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04); }
+.btn-submit { height: 88rpx; border-radius: 999rpx; background: #2979ff; color: #fff; font-size: 30rpx; display: flex; align-items: center; justify-content: center; }
+.product-popup { background: #fff; border-top-left-radius: 20rpx; border-top-right-radius: 20rpx; padding: 24rpx; }
+.popup-header { text-align: center; margin-bottom: 20rpx; }
+.popup-title { font-size: 30rpx; font-weight: 500; color: #333; }
+.popup-search { display: grid; grid-template-columns: 1fr 1fr 140rpx; gap: 16rpx; align-items: center; margin-bottom: 16rpx; }
+.popup-search-btn { height: 72rpx; border-radius: 12rpx; background: #2979ff; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 28rpx; }
+.product-list { max-height: 60vh; }
+.product-item { padding: 20rpx 12rpx; border-bottom: 1rpx solid #eee; }
+.product-name-row { display: flex; justify-content: space-between; align-items: center; }
+.product-name { font-size: 28rpx; color: #333; font-weight: 500; }
+.product-unit { font-size: 24rpx; color: #999; }
+.product-model { font-size: 24rpx; color: #666; margin-top: 8rpx; }
+.no-data { text-align: center; padding: 40rpx 0; color: #999; }
+</style>
+
diff --git a/src/pages/consumablesLogistics/stockManagement/index.vue b/src/pages/consumablesLogistics/stockManagement/index.vue
new file mode 100644
index 0000000..85021f6
--- /dev/null
+++ b/src/pages/consumablesLogistics/stockManagement/index.vue
@@ -0,0 +1,319 @@
+<template>
+ <view class="stock-mgmt-page">
+ <PageHeader title="搴撳瓨绠$悊" @back="goBack" />
+
+ <view class="tabs-wrap">
+ <view
+ v-for="tab in tabs"
+ :key="tab.name"
+ class="tab-item"
+ :class="{ active: activeTab === tab.name }"
+ @click="activeTab = tab.name"
+ >
+ <text>{{ tab.label }}</text>
+ </view>
+ </view>
+
+ <view class="search-section">
+ <view class="search-row">
+ <view class="search-input-wrap">
+ <up-input
+ v-model="searchForm.productName"
+ placeholder="浜у搧澶х被"
+ clearable
+ />
+ </view>
+ <view class="btn-search" @click="handleQuery">
+ <view class="btn-search-inner">
+ <up-icon name="search" size="22" color="#fff"></up-icon>
+ <text>鎼滅储</text>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <view class="list-section">
+ <view v-if="tableData.length > 0">
+ <view
+ v-for="(item, index) in tableData"
+ :key="item.id || index"
+ class="card-item"
+ >
+ <view class="card-click" @click="goDetail(item)">
+ <view class="card-header">
+ <view class="header-main">
+ <text class="product-name">{{ item.productName }}</text>
+ <text class="sub-title">{{ item.model || item.updateTime }}</text>
+ </view>
+ </view>
+ <up-divider />
+ <view class="card-body">
+ <view class="row"><text class="l">瑙勬牸鍨嬪彿</text><text class="r">{{ item.model }}</text></view>
+ <view class="row"><text class="l">鍗曚綅</text><text class="r">{{ item.unit }}</text></view>
+ <view class="row"><text class="l">鎬诲簱瀛樻暟</text><text class="r highlight">{{ item.qualitity }}</text></view>
+ <view class="row"><text class="l">鍐荤粨鏁伴噺</text><text class="r">{{ item.lockedQuantity || 0 }}</text></view>
+ <view class="row"><text class="l">鍙敤搴撳瓨</text><text class="r">{{ item.unLockedQuantity ?? (item.qualitity - (item.lockedQuantity || 0)) }}</text></view>
+ <view class="row"><text class="l">鏈�杩戞洿鏂版椂闂�</text><text class="r">{{ item.updateTime }}</text></view>
+ </view>
+ </view>
+ <view class="card-actions">
+ <view
+ class="btn-link btn-link-primary"
+ :class="{ disabled: !(item.unLockedQuantity > 0) }"
+ @click="openSubtract(item)"
+ >鍑哄簱</view>
+ <view
+ class="btn-link btn-link-warn"
+ v-if="item.unLockedQuantity > 0"
+ @click="openFrozen(item)"
+ >鍐荤粨</view>
+ <view
+ class="btn-link btn-link-plain"
+ v-if="(item.lockedQuantity || 0) > 0"
+ @click="openThaw(item)"
+ >瑙e喕</view>
+ </view>
+ </view>
+ <view class="load-more-wrap">
+ <u-loadmore :status="loadStatus" @loadmore="loadMore" />
+ </view>
+ </view>
+ <view v-else class="no-data">鏆傛棤鏁版嵁</view>
+ </view>
+
+ <up-popup :show="showQuantityPopup" mode="center" round="16" @close="closeQuantityPopup">
+ <view class="popup-inner">
+ <view class="popup-title">{{ quantityTitle }}</view>
+ <view class="form-row">
+ <text class="form-label">鏁伴噺</text>
+ <up-input v-model="quantityForm.num" type="number" :placeholder="'鏈�澶�' + maxQuantity" />
+ </view>
+ <view class="popup-footer">
+ <view class="btn-cancel" @click="closeQuantityPopup">鍙栨秷</view>
+ <view class="btn-ok" @click="submitQuantity">纭畾</view>
+ </view>
+ </view>
+ </up-popup>
+
+ <view class="fab-button" @click="goAdd">
+ <up-icon name="plus" size="24" color="#ffffff"></up-icon>
+ </view>
+ </view>
+</template>
+
+<script setup>
+import { computed, reactive, ref, toRefs, watch } from "vue";
+import { onReachBottom, onShow } from "@dcloudio/uni-app";
+import PageHeader from "@/components/PageHeader.vue";
+import { frozenConsumablesIn, getConsumablesInListPage, thawConsumablesIn } from "@/api/consumablesLogistics/consumablesIn.js";
+import { frozenConsumablesUninventory, getConsumablesUninventoryListPage, thawConsumablesUninventory } from "@/api/consumablesLogistics/consumablesUninventory.js";
+
+const activeTab = ref("qualified");
+const tabs = [
+ { label: "鍚堟牸搴撳瓨", name: "qualified" },
+ { label: "涓嶅悎鏍煎簱瀛�", name: "unqualified" },
+];
+const tableData = ref([]);
+const total = ref(0);
+const loadStatus = ref("loadmore");
+const showQuantityPopup = ref(false);
+const quantityOp = ref("");
+const currentRecord = ref(null);
+const page = reactive({ current: 1, size: 20 });
+const data = reactive({
+ searchForm: { productName: "" },
+ quantityForm: { num: "" },
+});
+const { searchForm, quantityForm } = toRefs(data);
+
+const isQualified = () => activeTab.value === "qualified";
+const getList = () => {
+ const isFirstPage = page.current === 1;
+ if (isFirstPage) {
+ uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
+ }
+ const params = { ...page, productName: searchForm.value.productName };
+ const api = isQualified() ? getConsumablesInListPage : getConsumablesUninventoryListPage;
+ api(params)
+ .then((res) => {
+ uni.hideLoading();
+ const records = res.data?.records || [];
+ const totalCount = res.data?.total || 0;
+ if (isFirstPage) {
+ tableData.value = records;
+ } else {
+ tableData.value = [...tableData.value, ...records];
+ }
+ total.value = totalCount;
+ loadStatus.value = tableData.value.length >= totalCount || totalCount === 0 ? "nomore" : "loadmore";
+ })
+ .catch(() => {
+ uni.hideLoading();
+ loadStatus.value = "error";
+ if (isFirstPage) {
+ uni.showToast({ title: "鍔犺浇澶辫触", icon: "none" });
+ }
+ });
+};
+
+const loadMore = () => {
+ if (loadStatus.value === "nomore" || loadStatus.value === "loading") return;
+ loadStatus.value = "loading";
+ page.current++;
+ getList();
+};
+
+watch(activeTab, () => {
+ page.current = 1;
+ loadStatus.value = "loadmore";
+ getList();
+});
+
+const handleQuery = () => {
+ page.current = 1;
+ loadStatus.value = "loadmore";
+ getList();
+};
+
+const goAdd = () => {
+ const type = isQualified() ? "0" : "1";
+ uni.navigateTo({
+ url: `/pages/consumablesLogistics/stockManagement/add?type=${type}`,
+ });
+};
+
+const quantityTitle = computed(() => {
+ if (quantityOp.value === "frozen") return "鍐荤粨搴撳瓨";
+ if (quantityOp.value === "thaw") return "瑙e喕搴撳瓨";
+ return "";
+});
+
+const maxQuantity = computed(() => {
+ if (!currentRecord.value) return 0;
+ if (quantityOp.value === "frozen") return currentRecord.value.unLockedQuantity || 0;
+ if (quantityOp.value === "thaw") return currentRecord.value.lockedQuantity || 0;
+ return 0;
+});
+
+const openSubtract = (row) => {
+ if (!(row.unLockedQuantity > 0)) return;
+ try {
+ uni.setStorageSync(
+ "stockSubtractRecord",
+ JSON.stringify({
+ item: row,
+ type: isQualified() ? "0" : "1",
+ })
+ );
+ } catch (e) {}
+ const typeParam = isQualified() ? "0" : "1";
+ uni.navigateTo({
+ url: `/pages/consumablesLogistics/stockManagement/subtract?type=${typeParam}&id=${row.id}`,
+ });
+};
+
+const openFrozen = (row) => {
+ quantityOp.value = "frozen";
+ currentRecord.value = row;
+ quantityForm.value.num = "";
+ showQuantityPopup.value = true;
+};
+
+const openThaw = (row) => {
+ quantityOp.value = "thaw";
+ currentRecord.value = row;
+ quantityForm.value.num = "";
+ showQuantityPopup.value = true;
+};
+
+const closeQuantityPopup = () => {
+ showQuantityPopup.value = false;
+ currentRecord.value = null;
+ quantityOp.value = "";
+};
+
+const submitQuantity = () => {
+ const num = Number(quantityForm.value.num);
+ if (!num || num <= 0 || num > maxQuantity.value) {
+ uni.showToast({ title: `璇疯緭鍏� 1~${maxQuantity.value} 涔嬮棿鐨勬暟閲廯, icon: "none" });
+ return;
+ }
+ const id = currentRecord.value?.id;
+ if (!id) return;
+ const base = { id, lockedQuantity: num };
+ let promise;
+ if (quantityOp.value === "frozen") {
+ promise = isQualified() ? frozenConsumablesIn(base) : frozenConsumablesUninventory(base);
+ } else {
+ promise = isQualified() ? thawConsumablesIn(base) : thawConsumablesUninventory(base);
+ }
+ promise
+ .then(() => {
+ uni.showToast({ title: "鎿嶄綔鎴愬姛", icon: "success" });
+ closeQuantityPopup();
+ getList();
+ })
+ .catch(() => uni.showToast({ title: "鎿嶄綔澶辫触", icon: "none" }));
+};
+
+const goDetail = (item) => {
+ if (!item) return;
+ try {
+ uni.setStorageSync(
+ "stockDetailItem",
+ JSON.stringify({
+ item,
+ type: isQualified() ? "0" : "1",
+ })
+ );
+ } catch (e) {}
+ if (!item.id) {
+ uni.navigateTo({ url: "/pages/consumablesLogistics/stockManagement/view" });
+ } else {
+ uni.navigateTo({ url: "/pages/consumablesLogistics/stockManagement/view?id=" + item.id });
+ }
+};
+
+const goBack = () => uni.navigateBack();
+onShow(() => getList());
+onReachBottom(() => {
+ loadMore();
+});
+</script>
+
+<style lang="scss" scoped>
+.stock-mgmt-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 120rpx; }
+.tabs-wrap { display: flex; background: #fff; padding: 24rpx; gap: 24rpx; }
+.tab-item { flex: 1; text-align: center; padding: 20rpx; border-radius: 12rpx; background: #f0f0f0; font-size: 28rpx; color: #666; }
+.tab-item.active { background: #2979ff; color: #fff; }
+.search-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
+.search-row { display: flex; align-items: center; }
+.search-input-wrap { flex: 1; margin-right: 20rpx; min-width: 0; }
+.btn-search { display: flex; align-items: center; justify-content: center; width: 180rpx; min-height: 72rpx; flex-shrink: 0; padding: 20rpx 24rpx; background: #2979ff; color: #fff; border-radius: 12rpx; font-size: 28rpx; box-sizing: border-box; text-align: center; }
+.btn-search-inner { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 8rpx; }
+.list-section { padding: 0 24rpx; }
+.card-item { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
+.card-header { padding: 8rpx 0; }
+.header-main { display: flex; flex-direction: column; gap: 8rpx; }
+.product-name { font-size: 30rpx; font-weight: 500; color: #333; }
+.sub-title { font-size: 24rpx; color: #999; }
+.card-body .row { display: flex; justify-content: space-between; padding: 12rpx 0; font-size: 26rpx; }
+.card-body .l { color: #666; }
+.card-body .r { color: #333; }
+.card-body .r.highlight { color: #2979ff; font-weight: 500; }
+.card-actions { display: flex; gap: 16rpx; margin-top: 16rpx; padding-top: 16rpx; border-top: 1rpx solid #eee; justify-content: flex-end; }
+.btn-link { min-width: 120rpx; padding: 10rpx 20rpx; border-radius: 24rpx; font-size: 26rpx; text-align: center; border-width: 1rpx; border-style: solid; }
+.btn-link-primary { color: #ffffff; background: #2979ff; border-color: #2979ff; }
+.btn-link-warn { color: #ff9f1a; background: rgba(255, 159, 26, 0.08); border-color: rgba(255, 159, 26, 0.6); }
+.btn-link-plain { color: #666666; background: #f5f5f5; border-color: #e0e0e0; }
+.btn-link.disabled { color: #cccccc; background: #f5f5f5; border-color: #e0e0e0; }
+.no-data { text-align: center; padding: 80rpx 0; color: #999; font-size: 28rpx; }
+.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; }
+.popup-inner { padding: 40rpx; min-width: 560rpx; background: #fff; }
+.popup-title { font-size: 32rpx; font-weight: 500; margin-bottom: 32rpx; }
+.form-row { margin-bottom: 24rpx; }
+.form-row .form-label { display: block; font-size: 26rpx; color: #666; margin-bottom: 12rpx; }
+.popup-footer { display: flex; gap: 24rpx; margin-top: 40rpx; }
+.btn-cancel { flex: 1; text-align: center; padding: 24rpx; background: #f0f0f0; border-radius: 12rpx; }
+.btn-ok { flex: 1; text-align: center; padding: 24rpx; background: #2979ff; color: #fff; border-radius: 12rpx; }
+</style>
diff --git a/src/pages/consumablesLogistics/stockManagement/subtract.vue b/src/pages/consumablesLogistics/stockManagement/subtract.vue
new file mode 100644
index 0000000..5e688bf
--- /dev/null
+++ b/src/pages/consumablesLogistics/stockManagement/subtract.vue
@@ -0,0 +1,209 @@
+<template>
+ <view class="subtract-stock-page">
+ <PageHeader title="鍑哄簱" @back="goBack" />
+
+ <scroll-view scroll-y class="content-scroll">
+ <view class="form-section">
+ <view class="section-title">搴撳瓨淇℃伅</view>
+ <view class="info-row">
+ <text class="label">浜у搧澶х被</text>
+ <text class="value">{{ stockRecord.productName }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">瑙勬牸鍨嬪彿</text>
+ <text class="value">{{ stockRecord.model }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">鍙敤搴撳瓨</text>
+ <text class="value highlight">{{ stockRecord.unLockedQuantity }}</text>
+ </view>
+ </view>
+
+ <view class="form-section">
+ <view class="section-title">鍑哄簱淇℃伅</view>
+ <view class="form-row">
+ <text class="form-label required">鍑哄簱鏁伴噺</text>
+ <up-input v-model="form.stockOutNum" type="number" :placeholder="'鏈�澶�' + stockRecord.unLockedQuantity" />
+ </view>
+ <view class="form-row" v-if="isQualified">
+ <text class="form-label">杞︾墝鍙�</text>
+ <up-input v-model="form.licensePlateNo" placeholder="璇疯緭鍏ヨ溅鐗屽彿" />
+ </view>
+ <view class="form-row" v-if="isQualified">
+ <text class="form-label">姣涢噸(鍚�)</text>
+ <up-input v-model="form.grossWeight" type="number" placeholder="璇疯緭鍏ユ瘺閲�" />
+ </view>
+ <view class="form-row" v-if="isQualified">
+ <text class="form-label">鐨噸(鍚�)</text>
+ <up-input v-model="form.tareWeight" type="number" placeholder="璇疯緭鍏ョ毊閲�" />
+ </view>
+ <view class="form-row" v-if="isQualified">
+ <text class="form-label">鍑�閲�(鍚�)</text>
+ <up-input v-model="form.netWeight" type="number" disabled placeholder="鑷姩璁$畻" />
+ </view>
+ <view class="form-row" v-if="isQualified">
+ <text class="form-label">杩囩鏃ユ湡</text>
+ <view class="selector-trigger" @click="openWeighingDatePicker">
+ <text class="selector-text" :class="{ placeholder: !form.weighingDate }">
+ {{ form.weighingDate || "璇烽�夋嫨杩囩鏃ユ湡" }}
+ </text>
+ <up-icon name="calendar" size="16" color="#999"></up-icon>
+ </view>
+ </view>
+ <view class="form-row" v-if="isQualified">
+ <text class="form-label">杩囩鍛�</text>
+ <up-input v-model="form.weighingOperator" placeholder="璇疯緭鍏ヨ繃纾呭憳" />
+ </view>
+ <view class="form-row">
+ <text class="form-label">澶囨敞</text>
+ <up-input v-model="form.remark" type="textarea" placeholder="閫夊~" />
+ </view>
+ </view>
+ </scroll-view>
+
+ <view class="bottom-bar">
+ <view class="btn-submit" @click="handleSubmit">鎻愪氦</view>
+ </view>
+
+ <up-popup :show="showWeighingDatePicker" mode="bottom" @close="showWeighingDatePicker = false">
+ <up-datetime-picker
+ :show="true"
+ v-model="weighingDateValue"
+ mode="datetime"
+ @confirm="onWeighingDateConfirm"
+ @cancel="showWeighingDatePicker = false"
+ />
+ </up-popup>
+ </view>
+</template>
+
+<script setup>
+import { computed, reactive, ref, watch } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
+import dayjs from "dayjs";
+import PageHeader from "@/components/PageHeader.vue";
+import { subtractConsumablesIn } from "@/api/consumablesLogistics/consumablesIn.js";
+import { subtractConsumablesUnInventory } from "@/api/consumablesLogistics/consumablesUninventory.js";
+
+const type = ref("0");
+const isQualified = computed(() => type.value === "0");
+
+const stockRecord = reactive({
+ id: "",
+ productName: "",
+ model: "",
+ unLockedQuantity: 0,
+});
+
+const form = reactive({
+ stockOutNum: "",
+ licensePlateNo: "",
+ grossWeight: "",
+ tareWeight: "",
+ netWeight: "",
+ weighingDate: "",
+ weighingOperator: "",
+ remark: "",
+});
+
+const showWeighingDatePicker = ref(false);
+const weighingDateValue = ref(Date.now());
+
+onLoad((options) => {
+ if (options && options.type != null) {
+ type.value = options.type;
+ }
+ const cached = uni.getStorageSync("stockSubtractRecord");
+ if (cached) {
+ try {
+ const payload = typeof cached === "string" ? JSON.parse(cached) : cached;
+ const item = payload && payload.item != null ? payload.item : payload;
+ stockRecord.id = item.id;
+ stockRecord.productName = item.productName;
+ stockRecord.model = item.model;
+ stockRecord.unLockedQuantity = item.unLockedQuantity || 0;
+ uni.removeStorageSync("stockSubtractRecord");
+ } catch (e) {
+ uni.removeStorageSync("stockSubtractRecord");
+ }
+ }
+});
+
+const computeNetWeight = () => {
+ const gross = Number(form.grossWeight);
+ const tare = Number(form.tareWeight);
+ if (!isNaN(gross) && !isNaN(tare)) {
+ const net = Number((gross - tare).toFixed(2));
+ form.netWeight = net > 0 ? net : 0;
+ } else {
+ form.netWeight = "";
+ }
+};
+
+watch(
+ () => [form.grossWeight, form.tareWeight],
+ () => computeNetWeight()
+);
+
+const openWeighingDatePicker = () => {
+ weighingDateValue.value = form.weighingDate
+ ? dayjs(form.weighingDate, "YYYY-MM-DD HH:mm:ss").valueOf()
+ : Date.now();
+ showWeighingDatePicker.value = true;
+};
+
+const onWeighingDateConfirm = (e) => {
+ const ts = e?.value ?? weighingDateValue.value;
+ form.weighingDate = dayjs(ts).format("YYYY-MM-DD HH:mm:ss");
+ showWeighingDatePicker.value = false;
+};
+
+const handleSubmit = () => {
+ const outNum = Number(form.stockOutNum);
+ if (!outNum || outNum <= 0 || outNum > Number(stockRecord.unLockedQuantity)) {
+ uni.showToast({ title: `璇疯緭鍏� 1~${stockRecord.unLockedQuantity} 涔嬮棿鐨勬暟閲廯, icon: "none" });
+ return;
+ }
+ const api = isQualified.value ? subtractConsumablesIn : subtractConsumablesUnInventory;
+ api({
+ id: stockRecord.id,
+ stockOutNum: outNum,
+ licensePlateNo: form.licensePlateNo,
+ grossWeight: form.grossWeight,
+ tareWeight: form.tareWeight,
+ netWeight: form.netWeight,
+ weighingDate: form.weighingDate,
+ weighingOperator: form.weighingOperator,
+ remark: form.remark,
+ })
+ .then(() => {
+ uni.showToast({ title: "鍑哄簱鎴愬姛", icon: "success" });
+ setTimeout(() => uni.navigateBack(), 400);
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍑哄簱澶辫触", icon: "none" });
+ });
+};
+
+const goBack = () => uni.navigateBack();
+</script>
+
+<style lang="scss" scoped>
+.subtract-stock-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 100rpx; }
+.content-scroll { height: calc(100vh - 100rpx); }
+.form-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
+.section-title { font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; }
+.info-row { display: flex; justify-content: space-between; padding: 12rpx 0; font-size: 26rpx; }
+.info-row .label { color: #666; }
+.info-row .value { color: #333; }
+.info-row .value.highlight { color: #2979ff; font-weight: 500; }
+.form-row { margin-bottom: 24rpx; }
+.form-label { display: block; font-size: 26rpx; color: #666; margin-bottom: 12rpx; }
+.form-label.required:before { content: "*"; color: #f56c6c; margin-right: 6rpx; }
+.selector-trigger { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; background: #f5f5f5; border-radius: 12rpx; }
+.selector-text { font-size: 28rpx; color: #333; }
+.selector-text.placeholder { color: #999; }
+.bottom-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); background: #fff; box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04); }
+.btn-submit { height: 88rpx; border-radius: 999rpx; background: #2979ff; color: #fff; font-size: 30rpx; display: flex; align-items: center; justify-content: center; }
+</style>
+
diff --git a/src/pages/consumablesLogistics/stockManagement/view.vue b/src/pages/consumablesLogistics/stockManagement/view.vue
new file mode 100644
index 0000000..fd68338
--- /dev/null
+++ b/src/pages/consumablesLogistics/stockManagement/view.vue
@@ -0,0 +1,121 @@
+<template>
+ <view class="detail-page">
+ <PageHeader title="搴撳瓨璇︽儏" @back="goBack" />
+ <view v-if="loading" class="loading-wrap">
+ <text class="loading-text">鍔犺浇涓�...</text>
+ </view>
+ <view v-else-if="detail" class="detail-wrap">
+ <view class="section-card">
+ <view class="section-head">
+ <view class="section-dot"></view>
+ <text class="section-title">鍩虹淇℃伅</text>
+ </view>
+ <view class="section-body">
+ <view class="detail-row">
+ <text class="label">搴忓彿</text>
+ <text class="value">{{ detail.index ?? '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">浜у搧澶х被</text>
+ <text class="value value-strong">{{ detail.productName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">瑙勬牸鍨嬪彿</text>
+ <text class="value">{{ detail.model || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鍗曚綅</text>
+ <text class="value">{{ detail.unit || '-' }}</text>
+ </view>
+ <view class="detail-row detail-row-highlight">
+ <text class="label">鎬诲簱瀛�</text>
+ <text class="value value-num">{{ detail.qualitity ?? '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鍐荤粨鏁伴噺</text>
+ <text class="value">{{ detail.lockedQuantity ?? 0 }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鍙敤搴撳瓨</text>
+ <text class="value">{{ detail.unLockedQuantity ?? '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">搴撳瓨绫诲瀷</text>
+ <text class="value">{{ detail.typeLabel }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鏈�杩戞洿鏂版椂闂�</text>
+ <text class="value">{{ detail.updateTime || '-' }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else class="empty">
+ <text class="empty-text">鏆傛棤璇︽儏鏁版嵁</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
+import PageHeader from "@/components/PageHeader.vue";
+
+const detail = ref(null);
+const loading = ref(true);
+
+function normalizeDetail(raw, type) {
+ if (!raw) return null;
+ const d = typeof raw === "object" ? raw : {};
+ return {
+ index: d.index ?? 1,
+ productName: d.productName,
+ model: d.model,
+ unit: d.unit,
+ qualitity: d.qualitity,
+ lockedQuantity: d.lockedQuantity,
+ unLockedQuantity: d.unLockedQuantity ?? (d.qualitity - (d.lockedQuantity || 0)),
+ updateTime: d.updateTime,
+ typeLabel: type === "1" ? "涓嶅悎鏍煎簱瀛�" : "鍚堟牸搴撳瓨",
+ };
+}
+
+onLoad(() => {
+ const cached = uni.getStorageSync("stockDetailItem");
+ if (cached) {
+ try {
+ const payload = typeof cached === "string" ? JSON.parse(cached) : cached;
+ const item = payload && payload.item != null ? payload.item : payload;
+ const type = payload && payload.type != null ? payload.type : "0";
+ detail.value = normalizeDetail({ ...item, index: 1 }, type);
+ uni.removeStorageSync("stockDetailItem");
+ } catch (e) {
+ uni.removeStorageSync("stockDetailItem");
+ }
+ }
+ loading.value = false;
+});
+
+const goBack = () => uni.navigateBack();
+</script>
+
+<style lang="scss" scoped>
+.detail-page { min-height: 100vh; background: linear-gradient(180deg, #e8eef7 0%, #f2f5fa 100%); padding-bottom: 48rpx; }
+.loading-wrap { padding: 120rpx 48rpx; text-align: center; }
+.loading-text { color: #8c9aa8; font-size: 28rpx; }
+.empty { padding: 120rpx 48rpx; text-align: center; }
+.empty-text { color: #8c9aa8; font-size: 28rpx; }
+.detail-wrap { padding: 24rpx 24rpx 32rpx; }
+.section-card { background: #fff; border-radius: 24rpx; overflow: hidden; margin-bottom: 28rpx; box-shadow: 0 8rpx 32rpx rgba(41, 121, 255, 0.06); border: 1rpx solid rgba(41, 121, 255, 0.06); }
+.section-head { display: flex; align-items: center; padding: 28rpx 32rpx; background: linear-gradient(135deg, #f8fbff 0%, #f0f6ff 100%); border-bottom: 1rpx solid #eef3fa; }
+.section-dot { width: 8rpx; height: 8rpx; border-radius: 50%; background: #2979ff; margin-right: 16rpx; }
+.section-title { font-size: 30rpx; font-weight: 600; color: #1e3a5f; letter-spacing: 0.5rpx; }
+.section-body { padding: 8rpx 32rpx 24rpx; }
+.detail-row { display: flex; align-items: center; min-height: 96rpx; padding: 0 16rpx; border-radius: 12rpx; font-size: 28rpx; margin-bottom: 4rpx; }
+.detail-row .label { width: 200rpx; flex-shrink: 0; color: #6b7c93; font-size: 26rpx; }
+.detail-row .value { flex: 1; color: #2c3e50; text-align: right; word-break: break-all; font-size: 28rpx; }
+.detail-row .value-strong { color: #1e3a5f; font-weight: 500; }
+.detail-row .value-num { color: #2979ff; font-weight: 600; font-size: 32rpx; }
+.detail-row-highlight { background: linear-gradient(90deg, rgba(41, 121, 255, 0.06) 0%, transparent 100%); margin: 12rpx -16rpx 4rpx; padding: 20rpx 16rpx; }
+</style>
+
diff --git a/src/pages/consumablesLogistics/stockReport/index.vue b/src/pages/consumablesLogistics/stockReport/index.vue
new file mode 100644
index 0000000..a46a065
--- /dev/null
+++ b/src/pages/consumablesLogistics/stockReport/index.vue
@@ -0,0 +1,242 @@
+<template>
+ <view class="report-page">
+ <PageHeader title="搴撳瓨鎶ヨ〃" @back="goBack" />
+ <view class="tabs-wrap">
+ <view
+ v-for="t in reportTypes"
+ :key="t.value"
+ class="tab-item"
+ :class="{ active: searchForm.reportType === t.value }"
+ @click="searchForm.reportType = t.value"
+ >
+ <text>{{ t.label }}</text>
+ </view>
+ </view>
+ <view class="list-section">
+ <view class="section-header">
+ <text class="table-title">{{ tableTitle }}</text>
+ </view>
+ <view v-if="tableData.length > 0">
+ <view v-for="(item, index) in tableData" :key="index" class="card-item">
+ <view class="card-header">
+ <view class="header-main">
+ <text class="product-name">{{ item.productName }}</text>
+ <text class="sub-title">{{ item.model }}</text>
+ </view>
+ </view>
+ <up-divider />
+ <view class="card-body">
+ <view class="row"><text class="l">鍗曚綅</text><text class="r">{{ item.unit }}</text></view>
+ <view class="row" v-if="searchForm.reportType !== 'inout'"><text class="l">鍏ュ簱鏃堕棿</text><text class="r">{{ item.createTime }}</text></view>
+ <view class="row" v-if="searchForm.reportType !== 'inout'"><text class="l">鍏ュ簱鎵规</text><text class="r">{{ item.inboundBatches }}</text></view>
+ <view class="row"><text class="l">鍏ュ簱鏁伴噺</text><text class="r">{{ item.totalStockIn ?? item.stockInNum }}</text></view>
+ <view class="row" v-if="searchForm.reportType === 'inout'"><text class="l">鍑哄簱鏁伴噺</text><text class="r">{{ item.totalStockOut }}</text></view>
+ <view class="row"><text class="l">鐜板湪搴撳瓨</text><text class="r highlight">{{ item.currentStock }}</text></view>
+ <view class="row" v-if="item.createBy"><text class="l">鍏ュ簱浜�</text><text class="r">{{ item.createBy }}</text></view>
+ </view>
+ </view>
+ <view class="load-more-wrap">
+ <u-loadmore :status="loadStatus" @loadmore="loadMore" />
+ </view>
+ </view>
+ <view v-else class="no-data">鏆傛棤鏁版嵁</view>
+ </view>
+ <up-popup :show="showDatePicker" mode="bottom" @close="showDatePicker = false">
+ <up-datetime-picker
+ v-model="dateValue"
+ :mode="datePickerMode"
+ @confirm="onDateConfirm"
+ @cancel="showDatePicker = false"
+ />
+ </up-popup>
+ </view>
+</template>
+
+<script setup>
+import { ref, reactive, toRefs, computed, watch } from "vue";
+import dayjs from "dayjs";
+import PageHeader from "@/components/PageHeader.vue";
+import { formatDateToYMD } from "@/utils/ruoyi";
+import { onShow, onReachBottom } from "@dcloudio/uni-app";
+import { getConsumablesInReportList, getConsumablesInInAndOutReportList } from "@/api/consumablesLogistics/consumablesIn.js";
+
+const reportTypes = [
+ { label: "鏃ユ姤", value: "daily" },
+ { label: "鏈堟姤", value: "monthly" },
+ { label: "杩涘嚭瀛樻姤琛�", value: "inout" },
+];
+const tableData = ref([]);
+const showDatePicker = ref(false);
+const dateValue = ref(Date.now());
+const datePickerTarget = ref("");
+const loadStatus = ref("loadmore");
+const page = reactive({ current: 1, size: 20 });
+const data = reactive({
+ searchForm: {
+ reportType: "daily",
+ singleDate: "",
+ startMonth: "",
+ endMonth: "",
+ startDate: "",
+ endDate: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+const datePickerMode = computed(() => {
+ if (datePickerTarget.value === "startMonth" || datePickerTarget.value === "endMonth") return "month";
+ return "date";
+});
+
+const tableTitle = computed(() => {
+ const m = { daily: "鏃ユ姤璇︾粏鏁版嵁", monthly: "鏈堟姤璇︾粏鏁版嵁", inout: "杩涘嚭瀛樻姤琛ㄨ缁嗘暟鎹�" };
+ return m[searchForm.value.reportType] || "鎶ヨ〃鏁版嵁";
+});
+
+const getQueryParams = () => {
+ const p = {
+ reportType: searchForm.value.reportType,
+ current: page.current,
+ size: page.size,
+ };
+ if (searchForm.value.reportType === "daily") {
+ p.reportDate = searchForm.value.singleDate;
+ } else if (searchForm.value.reportType === "monthly") {
+ p.startMonth = searchForm.value.startMonth;
+ p.endMonth = searchForm.value.endMonth;
+ } else if (searchForm.value.reportType === "monthly") {
+ p.startMonth = searchForm.value.startMonth;
+ p.endMonth = searchForm.value.endMonth;
+ } else {
+ p.startDate = searchForm.value.startDate;
+ p.endDate = searchForm.value.endDate;
+ }
+ return p;
+};
+
+const getList = () => {
+ const isFirstPage = page.current === 1;
+ if (isFirstPage) {
+ uni.showLoading({ title: "鏌ヨ涓�...", mask: true });
+ }
+ const params = getQueryParams();
+ const isInout = searchForm.value.reportType === "inout";
+ const api = isInout ? getConsumablesInInAndOutReportList : getConsumablesInReportList;
+ api(params)
+ .then((res) => {
+ uni.hideLoading();
+ const records = res.data?.records || [];
+ const total = res.data?.total || records.length;
+ if (isFirstPage) {
+ tableData.value = records;
+ } else {
+ tableData.value = [...tableData.value, ...records];
+ }
+ if (tableData.value.length >= total || total === 0) {
+ loadStatus.value = "nomore";
+ } else {
+ loadStatus.value = "loadmore";
+ }
+ })
+ .catch(() => {
+ uni.hideLoading();
+ loadStatus.value = "error";
+ if (isFirstPage) {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ }
+ });
+};
+
+const handleQuery = () => {
+ page.current = 1;
+ loadStatus.value = "loadmore";
+ getList();
+};
+
+const loadMore = () => {
+ if (loadStatus.value === "nomore" || loadStatus.value === "loading") return;
+ loadStatus.value = "loading";
+ page.current++;
+ getList();
+};
+
+const openDatePicker = (target) => {
+ datePickerTarget.value = target;
+ let val = "";
+ if (target === "single") val = searchForm.value.singleDate;
+ else if (target === "startMonth") val = searchForm.value.startMonth;
+ else if (target === "endMonth") val = searchForm.value.endMonth;
+ dateValue.value = val ? new Date(val).getTime() : Date.now();
+ showDatePicker.value = true;
+};
+
+const onDateConfirm = (e) => {
+ const isMonth = datePickerTarget.value === "startMonth" || datePickerTarget.value === "endMonth";
+ const str = isMonth ? dayjs(e.value).format("YYYY-MM") : formatDateToYMD(e.value);
+ if (datePickerTarget.value === "single") searchForm.value.singleDate = str;
+ else if (datePickerTarget.value === "startMonth") searchForm.value.startMonth = str;
+ else if (datePickerTarget.value === "endMonth") searchForm.value.endMonth = str;
+ showDatePicker.value = false;
+ handleQuery();
+};
+
+const initDefaultDates = () => {
+ const today = dayjs();
+ if (!searchForm.value.singleDate) {
+ searchForm.value.singleDate = today.format("YYYY-MM-DD");
+ }
+ if (!searchForm.value.startMonth || !searchForm.value.endMonth) {
+ const startOfMonth = today.startOf("month").format("YYYY-MM-DD");
+ const endOfMonth = today.endOf("month").format("YYYY-MM-DD");
+ searchForm.value.startMonth = startOfMonth;
+ searchForm.value.endMonth = endOfMonth;
+ }
+ if (!searchForm.value.startDate || !searchForm.value.endDate) {
+ searchForm.value.endDate = today.format("YYYY-MM-DD");
+ searchForm.value.startDate = today.subtract(6, "day").format("YYYY-MM-DD");
+ }
+};
+
+watch(
+ () => searchForm.value.reportType,
+ () => {
+ handleQuery();
+ }
+);
+
+onShow(() => {
+ initDefaultDates();
+ handleQuery();
+});
+
+onReachBottom(() => loadMore());
+
+const goBack = () => uni.navigateBack();
+</script>
+
+<style lang="scss" scoped>
+.report-page { min-height: 100vh; background: #f5f5f5; padding-bottom: 40rpx; }
+.tabs-wrap { display: flex; background: #fff; padding: 24rpx; gap: 24rpx; }
+.tab-item { flex: 1; text-align: center; padding: 20rpx; border-radius: 12rpx; background: #f0f0f0; font-size: 28rpx; color: #666; }
+.tab-item.active { background: #2979ff; color: #fff; }
+.search-section { background: #fff; margin: 24rpx; padding: 24rpx; border-radius: 16rpx; }
+.search-row { display: flex; align-items: center; margin-bottom: 0; flex-wrap: wrap; }
+.search-row .label { width: 140rpx; font-size: 26rpx; color: #666; }
+.search-row .label.end { margin-left: 24rpx; }
+.date-picker { flex: 1; min-width: 200rpx; padding: 20rpx; background: #f5f5f5; border-radius: 12rpx; font-size: 28rpx; }
+.btn-row { display: flex; gap: 24rpx; margin-top: 24rpx; }
+.btn-query { flex: 1; text-align: center; padding: 24rpx; background: #2979ff; color: #fff; border-radius: 12rpx; }
+.btn-reset { flex: 1; text-align: center; padding: 24rpx; background: #e0e0e0; border-radius: 12rpx; }
+.list-section { margin: 24rpx; }
+.section-header { margin-bottom: 16rpx; padding: 16rpx 20rpx; }
+.table-title { font-size: 30rpx; font-weight: 500; color: #333; }
+.card-item { background: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
+.card-header { padding: 4rpx 0 12rpx; }
+.header-main { display: flex; flex-direction: column; gap: 6rpx; }
+.product-name { font-size: 30rpx; font-weight: 500; color: #333; }
+.sub-title { font-size: 24rpx; color: #999; }
+.card-body .row { display: flex; justify-content: space-between; padding: 6rpx 0; font-size: 26rpx; }
+.card-body .l { color: #666; } .card-body .r { color: #333; } .card-body .r.highlight { color: #2979ff; font-weight: 500; }
+.no-data { text-align: center; padding: 60rpx 0; color: #999; font-size: 28rpx; }
+.load-more-wrap { padding: 24rpx 0 8rpx; }
+</style>
diff --git a/src/pages/index.vue b/src/pages/index.vue
index f4bc2c3..0782afe 100644
--- a/src/pages/index.vue
+++ b/src/pages/index.vue
@@ -237,6 +237,31 @@
</up-grid>
</view>
</view>
+ <!-- 鑰楁潗鐗╂枡妯″潡 -->
+ <view class="common-module material-module"
+ v-if="hasMaterialItems">
+ <view class="module-header">
+ <view class="module-title-container">
+ <text class="module-title">鑰楁潗鐗╂枡</text>
+ </view>
+ </view>
+ <view class="module-content">
+ <up-grid :border="false"
+ col="4">
+ <up-grid-item v-for="(item, index) in materialItems"
+ :key="index"
+ @click="handleCommonItemClick(item)">
+ <view class="icon-container"
+ :style="{ background: item.bgColor }">
+ <up-icon :name="item.icon"
+ :size="58"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-label">{{item.label}}</text>
+ </up-grid-item>
+ </up-grid>
+ </view>
+ </view>
</view>
</template>
@@ -483,6 +508,29 @@
{
icon: "/static/images/icon/caigoutaizhang@2x.png",
label: "搴撳瓨鎶ヨ〃",
+ },
+ ]);
+ // 鑰楁潗鐗╂枡鍔熻兘鏁版嵁
+ const materialItems = reactive([
+ {
+ icon: "/static/images/icon/caigoutaizhang@2x.png",
+ label: "鍏ュ簱绠$悊",
+ module: "material",
+ },
+ {
+ icon: "/static/images/icon/caigoutaizhang@2x.png",
+ label: "鍑哄簱鍙拌处",
+ module: "material",
+ },
+ {
+ icon: "/static/images/icon/caigoutaizhang@2x.png",
+ label: "搴撳瓨绠$悊",
+ module: "material",
+ },
+ {
+ icon: "/static/images/icon/caigoutaizhang@2x.png",
+ label: "搴撳瓨鎶ヨ〃",
+ module: "material",
},
]);
@@ -795,22 +843,34 @@
break;
case "鍏ュ簱绠$悊":
uni.navigateTo({
- url: "/pages/inventoryManagement/receiptManagement/index",
+ url:
+ item.module === "material"
+ ? "/pages/consumablesLogistics/receiptManagement/index"
+ : "/pages/inventoryManagement/receiptManagement/index",
});
break;
case "鍑哄簱鍙拌处":
uni.navigateTo({
- url: "/pages/inventoryManagement/dispatchLog/index",
+ url:
+ item.module === "material"
+ ? "/pages/consumablesLogistics/dispatchLog/index"
+ : "/pages/inventoryManagement/dispatchLog/index",
});
break;
case "搴撳瓨绠$悊":
uni.navigateTo({
- url: "/pages/inventoryManagement/stockManagement/index",
+ url:
+ item.module === "material"
+ ? "/pages/consumablesLogistics/stockManagement/index"
+ : "/pages/inventoryManagement/stockManagement/index",
});
break;
case "搴撳瓨鎶ヨ〃":
uni.navigateTo({
- url: "/pages/inventoryManagement/stockReport/index",
+ url:
+ item.module === "material"
+ ? "/pages/consumablesLogistics/stockReport/index"
+ : "/pages/inventoryManagement/stockReport/index",
});
break;
default:
@@ -1136,10 +1196,26 @@
// 杩囨护浠撳偍鐗╂祦鑿滃崟
const originalWarehouseLogistics = [
- { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "鍏ュ簱绠$悊" },
- { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "鍑哄簱鍙拌处" },
- { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "搴撳瓨绠$悊" },
- { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "搴撳瓨鎶ヨ〃" },
+ {
+ icon: "/static/images/icon/caigoutaizhang@2x.png",
+ label: "鍏ュ簱绠$悊",
+ module: "warehouse",
+ },
+ {
+ icon: "/static/images/icon/caigoutaizhang@2x.png",
+ label: "鍑哄簱鍙拌处",
+ module: "warehouse",
+ },
+ {
+ icon: "/static/images/icon/caigoutaizhang@2x.png",
+ label: "搴撳瓨绠$悊",
+ module: "warehouse",
+ },
+ {
+ icon: "/static/images/icon/caigoutaizhang@2x.png",
+ label: "搴撳瓨鎶ヨ〃",
+ module: "warehouse",
+ },
];
const filteredWarehouseLogistics = originalWarehouseLogistics.filter(
item => allowedMenuTitles.has(item.label)
@@ -1149,6 +1225,25 @@
warehouseLogisticsItems.length,
...filteredWarehouseLogistics
);
+
+ // 杩囨护鑰楁潗鐗╂枡鑿滃崟
+ const materialCandidates = label => {
+ const list = [label, `鑰楁潗${label}`];
+ if (label.endsWith("绠$悊")) {
+ list.push(`鑰楁潗${label.replace("绠$悊", "")}`);
+ }
+ return list;
+ };
+ const originalMaterial = [
+ { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "鍏ュ簱绠$悊", module: "material" },
+ { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "鍑哄簱鍙拌处", module: "material" },
+ { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "搴撳瓨绠$悊", module: "material" },
+ { icon: "/static/images/icon/caigoutaizhang@2x.png", label: "搴撳瓨鎶ヨ〃", module: "material" },
+ ];
+ const filteredMaterial = originalMaterial.filter(item => {
+ return materialCandidates(item.label).some(t => allowedMenuTitles.has(t));
+ });
+ materialItems.splice(0, materialItems.length, ...filteredMaterial);
};
// 妫�鏌ユā鍧楁槸鍚︽湁鑿滃崟椤归渶瑕佹樉绀�
@@ -1162,6 +1257,7 @@
const hasWarehouseLogisticsItems = computed(
() => warehouseLogisticsItems.length > 0
);
+ const hasMaterialItems = computed(() => materialItems.length > 0);
onMounted(() => {
// 姣忔杩涘叆棣栭〉閮藉己鍒跺埛鏂扮敤鎴蜂俊鎭拰璺敱鏉冮檺锛屼笉鍋氭湰鍦扮紦瀛樺垽鏂�
--
Gitblit v1.9.3