From 89710438fe00a9068208e806289f2ae083fc0f47 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 03 十一月 2026 11:07:09 +0800
Subject: [PATCH] feat(app): 补齐生产准备检查、收发存汇总入口,修正质量检验工作台归组
---
src/pages.json | 12 +
src/pages/mes/pro/mps/preparation-check.vue | 123 +++++++++++++++++
src/pages/workbench/index.vue | 2
src/config/app-routes.ts | 3
src/api/wls.ts | 18 ++
src/pages/mes/pro/mps/index.vue | 5
src/api/production.ts | 45 ++++++
src/pages/wls/stocksummary/index.vue | 159 ++++++++++++++++++++++
8 files changed, 367 insertions(+), 0 deletions(-)
diff --git a/src/api/production.ts b/src/api/production.ts
index c568f31..c10d9f5 100644
--- a/src/api/production.ts
+++ b/src/api/production.ts
@@ -30,6 +30,48 @@
createTime?: string;
}
+/** 鐢熶骇鍑嗗妫�鏌� - 鐗╂枡榻愬鏄庣粏 */
+export interface MpsPreparationMaterial {
+ itemId?: number;
+ itemCode?: string;
+ itemName?: string;
+ specification?: string;
+ unitMeasureName?: string;
+ requiredQuantity?: number;
+ stockQuantity?: number;
+ purchaseOrderQuantity?: number;
+ shortageQuantity?: number;
+ sufficient?: boolean;
+}
+
+/** 鐢熶骇鍑嗗妫�鏌� - 璁惧鍙敤鎬ф槑缁� */
+export interface MpsPreparationEquipment {
+ processId?: number;
+ processName?: string;
+ machineryId?: number;
+ machineryCode?: string;
+ machineryName?: string;
+ status?: number;
+ statusName?: string;
+ available?: boolean;
+ reason?: string;
+}
+
+/** 鐢熶骇鍑嗗妫�鏌ョ粨鏋� */
+export interface MpsPreparationCheck {
+ mpsId?: number;
+ mpsCode?: string;
+ productId?: number;
+ productName?: string;
+ quantity?: number;
+ materialReady?: boolean;
+ equipmentReady?: boolean;
+ ready?: boolean;
+ summary?: string;
+ materials?: MpsPreparationMaterial[];
+ equipments?: MpsPreparationEquipment[];
+}
+
export interface WorkOrder {
id?: number;
parentId?: number;
@@ -341,6 +383,9 @@
export function cancelMps(accessToken: string, id: number) {
return request<boolean>(`/mes/pro-mps/cancel${query({ id })}`, { method: "POST", header: header(accessToken) });
}
+export function getMpsPreparationCheck(accessToken: string, id: number) {
+ return request<MpsPreparationCheck>(`/mes/pro-mps/preparation-check${query({ id })}`, { header: header(accessToken) });
+}
export function getWorkOrderPage(accessToken: string, params: { pageNo: number; pageSize: number; code?: string; name?: string; orderSourceCode?: string; productId?: number; clientId?: number; status?: number; type?: number }) {
return request<PageResult<WorkOrder>>(`/mes/pro/work-order/page${query(params)}`, { header: header(accessToken) });
diff --git a/src/api/wls.ts b/src/api/wls.ts
index 349ee95..8104313 100644
--- a/src/api/wls.ts
+++ b/src/api/wls.ts
@@ -125,6 +125,21 @@
createTime?: string;
}
+/** 浠撳偍鏀跺彂瀛樻眹鎬昏 */
+export interface StockSummary {
+ itemId?: number;
+ itemCode?: string;
+ itemName?: string;
+ specification?: string;
+ unitMeasureName?: string;
+ warehouseId?: number;
+ warehouseName?: string;
+ openingQuantity?: number;
+ inQuantity?: number;
+ outQuantity?: number;
+ closingQuantity?: number;
+}
+
export interface ArrivalNotice {
id?: number;
code?: string;
@@ -570,6 +585,9 @@
export function getTransactionPage(accessToken: string, params: { pageNo: number; pageSize: number; bizCode?: string }) {
return request<PageResult<Transaction>>(`/mes/wm/transaction/page${query(params)}`, { header: header(accessToken) });
}
+export function getStockSummaryPage(accessToken: string, params: { pageNo: number; pageSize: number; itemId?: number; warehouseId?: number; startTime?: string; endTime?: string }) {
+ return request<PageResult<StockSummary>>(`/mes/wm/transaction/summary${query(params)}`, { header: header(accessToken) });
+}
export function getTransferPage(accessToken: string, params: { pageNo: number; pageSize: number; code?: string; name?: string; type?: number; status?: number }) { return request<PageResult<Transfer>>(`/mes/wm/transfer/page${query(params)}`, { header: header(accessToken) }); }
export function getTransfer(accessToken: string, id: number) { return request<Transfer>(`/mes/wm/transfer/get${query({ id })}`, { header: header(accessToken) }); }
export function createTransfer(accessToken: string, data: Transfer) { return request<number>("/mes/wm/transfer/create", { method: "POST", data, header: header(accessToken) }); }
diff --git a/src/config/app-routes.ts b/src/config/app-routes.ts
index e4e124a..5ef60e7 100644
--- a/src/config/app-routes.ts
+++ b/src/config/app-routes.ts
@@ -103,6 +103,9 @@
"wls/transaction/index": "/pages/wls/transaction/index",
"wls/warehouse/index": "/pages/wls/warehouse/index",
"wls/materialstock/index": "/pages/wls/material-stock/index",
+ "wls/stocksummary/index": "/pages/wls/stocksummary/index",
+ "wls/warehouse/location/index": "/pages/wls/warehouse/location",
+ "wls/warehouse/area/index": "/pages/wls/warehouse/area",
"wls/arrivalnotice/index": "/pages/wls/arrival-notice/index",
"wls/itemreceipt/index": "/pages/wls/item-receipt/index",
"wls/returnvendor/index": "/pages/wls/return-vendor/index",
diff --git a/src/pages.json b/src/pages.json
index 8244e35..8eb30f5 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -210,6 +210,12 @@
}
},
{
+ "path": "stocksummary/index",
+ "style": {
+ "navigationBarTitleText": "鏀跺彂瀛樻眹鎬�"
+ }
+ },
+ {
"path": "arrival-notice/index",
"style": {
"navigationBarTitleText": "鍒拌揣閫氱煡"
@@ -1031,6 +1037,12 @@
}
},
{
+ "path": "mps/preparation-check",
+ "style": {
+ "navigationBarTitleText": "鐢熶骇鍑嗗妫�鏌�"
+ }
+ },
+ {
"path": "workorder/index",
"style": {
"navigationBarTitleText": "鐢熶骇璁㈠崟"
diff --git a/src/pages/mes/pro/mps/index.vue b/src/pages/mes/pro/mps/index.vue
index 9f35565..5e9fc51 100644
--- a/src/pages/mes/pro/mps/index.vue
+++ b/src/pages/mes/pro/mps/index.vue
@@ -54,6 +54,10 @@
function goForm(id?: number, mode: "edit" | "detail" = "edit") {
uni.navigateTo({ url: `/pages/mes/pro/mps/form${id ? `?id=${id}&mode=${mode}` : ""}` });
}
+function goPreparationCheck(id?: number) {
+ if (!id) return;
+ uni.navigateTo({ url: `/pages/mes/pro/mps/preparation-check?id=${id}` });
+}
async function confirmAction(title: string, content: string) {
return new Promise<boolean>(resolve => uni.showModal({ title, content, success: res => resolve(res.confirm) }));
}
@@ -130,6 +134,7 @@
<view class="actions" @click.stop>
<button @click="goForm(row.id,'detail')">璇︽儏</button>
<button v-if="canUpdate && row.status === 0" @click="goForm(row.id)">缂栬緫</button>
+ <button @click="goPreparationCheck(row.id)">鍑嗗妫�鏌�</button>
<button v-if="canIssue && row.status === 0" @click="issue(row)">涓嬪彂</button>
<button v-if="canCancel && row.status === 1" class="danger" @click="cancel(row)">鍙栨秷</button>
<button v-if="canDelete && row.status === 0" class="danger" @click="remove(row)">鍒犻櫎</button>
diff --git a/src/pages/mes/pro/mps/preparation-check.vue b/src/pages/mes/pro/mps/preparation-check.vue
new file mode 100644
index 0000000..b24aabe
--- /dev/null
+++ b/src/pages/mes/pro/mps/preparation-check.vue
@@ -0,0 +1,123 @@
+<script setup lang="ts">
+import { computed, ref } from "vue";
+import { onLoad, onPullDownRefresh } from "@dcloudio/uni-app";
+import { getMpsPreparationCheck, type MpsPreparationCheck } from "../../../../api/production";
+import { useAuth } from "../../../../stores/auth";
+
+const { isLoggedIn, state } = useAuth();
+const mpsId = ref<number>();
+const result = ref<MpsPreparationCheck>();
+const loading = ref(false);
+const loadError = ref("");
+const canQuery = computed(() => !!state.accessToken);
+const shortageRows = computed(() => (result.value?.materials || []).filter(item => !item.sufficient));
+const readyMaterials = computed(() => (result.value?.materials || []).filter(item => item.sufficient));
+const unavailableEquipments = computed(() => (result.value?.equipments || []).filter(item => !item.available));
+/** 鍚庣鍙兘娌¤繑鍥炶澶囨槑缁嗭紙浜у搧鏈粦瀹氬伐鑹鸿矾绾匡級锛屾鏃剁粰鐢ㄦ埛涓�涓彲鎵ц鐨勬彁绀� */
+const equipmentHint = computed(() => {
+ if ((result.value?.equipments || []).length) return "";
+ return "璇ヤ骇鍝佸皻鏈淮鎶ゅ伐鑹鸿矾绾挎垨宸ュ簭璁惧锛屾棤娉曡嚜鍔ㄥ垽鏂澶囧彲鐢ㄦ�э紝璇峰埌銆屽伐鑹鸿矾绾裤�嶇淮鎶ゅ悗鍐嶆鏌�";
+});
+function formatNumber(value?: number) {
+ const number = Number(value || 0);
+ return Number.isInteger(number) ? String(number) : number.toFixed(2);
+}
+async function load() {
+ if (!state.accessToken || !mpsId.value || loading.value) return;
+ loading.value = true;
+ loadError.value = "";
+ try {
+ result.value = await getMpsPreparationCheck(state.accessToken, mpsId.value);
+ } catch (error) {
+ loadError.value = error instanceof Error ? error.message : "鐢熶骇鍑嗗妫�鏌ュけ璐�";
+ } finally {
+ loading.value = false;
+ }
+}
+function goBack() {
+ uni.navigateBack();
+}
+onLoad(options => {
+ const id = Number(options?.id);
+ mpsId.value = Number.isFinite(id) && id > 0 ? id : undefined;
+ if (!mpsId.value) {
+ loadError.value = "缂哄皯鐢熶骇璁″垝缂栧彿锛岃浠庣敓浜ц鍒掑垪琛ㄩ噸鏂拌繘鍏�";
+ return;
+ }
+ load();
+});
+onPullDownRefresh(async () => {
+ await load();
+ uni.stopPullDownRefresh();
+});
+</script>
+<template>
+ <view class="page">
+ <template v-if="isLoggedIn && canQuery">
+ <view v-if="loadError" class="state error" @click="load">{{ loadError }}锛岀偣鍑婚噸璇�</view>
+ <scroll-view v-else-if="result" class="content" scroll-y>
+ <view :class="['banner', result.ready ? 'ok' : 'warn']">
+ <text class="banner-title">{{ result.ready ? "鍑嗗宸ヤ綔宸插氨缁�" : "鏆備笉婊¤冻寮�宸ユ潯浠�" }}</text>
+ <text class="banner-sub">{{ result.summary || "绯荤粺鏈繑鍥炴鏌ョ粨璁�" }}</text>
+ <view class="banner-tags">
+ <text :class="['tag', result.materialReady ? 'tag-success' : 'tag-danger']">{{ result.materialReady ? "鐗╂枡榻愬" : "鐗╂枡鐭己" }}</text>
+ <text :class="['tag', result.equipmentReady ? 'tag-success' : 'tag-danger']">{{ result.equipmentReady ? "璁惧鍙敤" : "璁惧涓嶅彲鐢�" }}</text>
+ </view>
+ </view>
+ <view class="card">
+ <view class="card-title">鐢熶骇璁″垝</view>
+ <view class="grid">
+ <text>璁″垝鍗曞彿锛歿{ result.mpsCode || "--" }}</text>
+ <text>浜у搧锛歿{ result.productName || "--" }}</text>
+ <text>璁″垝鏁伴噺锛歿{ formatNumber(result.quantity) }}</text>
+ </view>
+ </view>
+ <view v-if="shortageRows.length" class="card">
+ <view class="card-title danger">鐗╂枡缂哄彛 {{ shortageRows.length }} 椤�</view>
+ <view v-for="item in shortageRows" :key="item.itemId" class="item">
+ <view class="item-head"><text class="name">{{ item.itemName || "鏈懡鍚嶇墿鏂�" }}</text><text class="tag tag-danger">缂� {{ formatNumber(item.shortageQuantity) }}{{ item.unitMeasureName || "" }}</text></view>
+ <view class="grid">
+ <text>鐗╂枡缂栫爜锛歿{ item.itemCode || "--" }}</text>
+ <text>瑙勬牸鍨嬪彿锛歿{ item.specification || "--" }}</text>
+ <text>闇�姹傛暟閲忥細{{ formatNumber(item.requiredQuantity) }}</text>
+ <text>鐜版湁搴撳瓨锛歿{ formatNumber(item.stockQuantity) }}</text>
+ <text>鍦ㄩ�旈噰璐細{{ formatNumber(item.purchaseOrderQuantity) }}</text>
+ <text>缂哄彛鏁伴噺锛歿{ formatNumber(item.shortageQuantity) }}</text>
+ </view>
+ </view>
+ </view>
+ <view v-if="readyMaterials.length" class="card">
+ <view class="card-title">鐗╂枡榻愬 {{ readyMaterials.length }} 椤�</view>
+ <view v-for="item in readyMaterials" :key="item.itemId" class="item">
+ <view class="item-head"><text class="name">{{ item.itemName || "鏈懡鍚嶇墿鏂�" }}</text><text class="tag tag-success">鍏呰冻</text></view>
+ <view class="grid">
+ <text>鐗╂枡缂栫爜锛歿{ item.itemCode || "--" }}</text>
+ <text>闇�姹傛暟閲忥細{{ formatNumber(item.requiredQuantity) }}</text>
+ <text>鐜版湁搴撳瓨锛歿{ formatNumber(item.stockQuantity) }}</text>
+ <text>鍦ㄩ�旈噰璐細{{ formatNumber(item.purchaseOrderQuantity) }}</text>
+ </view>
+ </view>
+ </view>
+ <view class="card">
+ <view class="card-title">璁惧鍙敤鎬�</view>
+ <view v-if="equipmentHint" class="empty">{{ equipmentHint }}</view>
+ <view v-for="item in result.equipments || []" :key="`${item.processId}-${item.machineryId}`" class="item">
+ <view class="item-head"><text class="name">{{ item.processName || "鏈懡鍚嶅伐搴�" }}</text><text :class="['tag', item.available ? 'tag-success' : 'tag-danger']">{{ item.available ? "鍙敤" : "涓嶅彲鐢�" }}</text></view>
+ <view class="grid">
+ <text>璁惧缂栫爜锛歿{ item.machineryCode || "--" }}</text>
+ <text>璁惧鍚嶇О锛歿{ item.machineryName || "--" }}</text>
+ <text>璁惧鐘舵�侊細{{ item.statusName || "--" }}</text>
+ <text v-if="item.reason">涓嶅彲鐢ㄥ師鍥狅細{{ item.reason }}</text>
+ </view>
+ </view>
+ </view>
+ </scroll-view>
+ <view v-else class="state">{{ loading ? "妫�鏌ヤ腑..." : "鏆傛棤妫�鏌ョ粨鏋�" }}</view>
+ </template>
+ <view v-else class="state">鐧诲綍鍚庢煡鐪嬬敓浜у噯澶囨鏌�</view>
+ <button class="back" @click="goBack">杩斿洖鐢熶骇璁″垝</button>
+ </view>
+</template>
+<style lang="scss" scoped>
+.page{height:100vh;display:flex;flex-direction:column;padding:0 24rpx;box-sizing:border-box;overflow:hidden;background:#f3f6fb}.content{height:0;flex:1;padding:20rpx 0 140rpx;box-sizing:border-box}.banner{display:flex;flex-direction:column;gap:10rpx;padding:26rpx 24rpx;margin-bottom:16rpx;background:#fff;border-radius:16rpx;box-shadow:0 10rpx 24rpx rgba(43,69,107,.08);border-left:8rpx solid #17865a}.banner.warn{border-left-color:#c65050}.banner-title{color:#20334c;font-size:32rpx;font-weight:600}.banner-sub{color:#66778c;font-size:24rpx;line-height:1.6}.banner-tags{display:flex;gap:12rpx;margin-top:6rpx}.card{padding:22rpx;margin-bottom:16rpx;background:#fff;border-radius:16rpx;box-shadow:0 10rpx 24rpx rgba(43,69,107,.08)}.card-title{margin-bottom:12rpx;color:#20334c;font-size:28rpx;font-weight:600}.card-title.danger{color:#c65050}.item{padding:16rpx 0;border-top:1rpx solid #edf1f6}.item-head{display:flex;align-items:center;justify-content:space-between;gap:12rpx}.name{overflow:hidden;color:#31445c;font-size:27rpx;font-weight:600;text-overflow:ellipsis;white-space:nowrap}.tag{flex:none;display:inline-flex;align-items:center;height:40rpx;padding:0 14rpx;font-size:22rpx;border-radius:999rpx;white-space:nowrap}.tag-success{color:#17865a;background:#eafaf2;border:1rpx solid #b8efd5}.tag-danger{color:#c65050;background:#fff7f7;border:1rpx solid #e3a0a0}.grid{display:grid;grid-template-columns:1fr 1fr;gap:8rpx;margin-top:10rpx;color:#66778c;font-size:23rpx}.grid text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty{padding:16rpx 0;color:#8795a8;font-size:24rpx;line-height:1.6}.more,.state{display:block;padding:35rpx;color:#8795a8;font-size:26rpx;text-align:center}.error{color:#c3654d}.back{position:fixed;left:24rpx;right:24rpx;bottom:calc(24rpx + env(safe-area-inset-bottom));height:84rpx;line-height:84rpx;color:var(--color-primary, #1769d8);font-size:28rpx;background:#fff;border:1rpx solid var(--color-primary-border, #8ab3ee);border-radius:14rpx}.back::after{border:0}
+</style>
diff --git a/src/pages/wls/stocksummary/index.vue b/src/pages/wls/stocksummary/index.vue
new file mode 100644
index 0000000..3903914
--- /dev/null
+++ b/src/pages/wls/stocksummary/index.vue
@@ -0,0 +1,159 @@
+<script setup lang="ts">
+import { computed, ref } from "vue";
+import { onPullDownRefresh, onShow } from "@dcloudio/uni-app";
+import { getStockSummaryPage, getWarehouseSimpleList, type StockSummary, type Warehouse } from "../../../api/wls";
+import { useAuth } from "../../../stores/auth";
+import { hasMenuComponent } from "../components/shared";
+
+const { hasPermission, isLoggedIn, state } = useAuth();
+const rows = ref<StockSummary[]>([]);
+const warehouses = ref<Warehouse[]>([]);
+const warehouseId = ref<number | undefined>(undefined);
+const startDate = ref("");
+const endDate = ref("");
+const pageNo = ref(1);
+const total = ref(0);
+const loading = ref(false);
+const loadError = ref("");
+const canQuery = computed(() => hasPermission("mes:wm-transaction:query") || hasMenuComponent(state.menus, "wls/stocksummary/index"));
+const hasMore = computed(() => rows.value.length < total.value);
+const warehouseOptions = computed(() => ["鍏ㄩ儴浠撳簱", ...warehouses.value.map(item => `${item.code || "--"} - ${item.name || "--"}`)]);
+const warehouseLabel = computed(() => {
+ const index = warehouses.value.findIndex(item => item.id === warehouseId.value);
+ return index >= 0 ? warehouseOptions.value[index + 1] : "鍏ㄩ儴浠撳簱";
+});
+/** 褰撳墠宸插姞杞借鐨勫悎璁★紝鏂逛究浠撶鍦ㄦ墜鏈轰笂蹇�熺湅鏀跺彂瀛樻�婚噺 */
+const totals = computed(() => rows.value.reduce(
+ (sum, row) => ({
+ opening: sum.opening + Number(row.openingQuantity || 0),
+ inbound: sum.inbound + Number(row.inQuantity || 0),
+ outbound: sum.outbound + Number(row.outQuantity || 0),
+ closing: sum.closing + Number(row.closingQuantity || 0),
+ }),
+ { opening: 0, inbound: 0, outbound: 0, closing: 0 }
+));
+function formatNumber(value?: number) {
+ const number = Number(value || 0);
+ return Number.isInteger(number) ? String(number) : number.toFixed(2);
+}
+/** 鏈熷垵 + 鍏ュ簱 - 鍑哄簱 涓庢湡鏈笉涓�鑷存椂缁欏嚭鎻愮ず锛岄伩鍏嶆妸閿欒鐨勮处璇绘垚鍑嗙殑 */
+function mismatch(row: StockSummary) {
+ const expected = Number(row.openingQuantity || 0) + Number(row.inQuantity || 0) - Number(row.outQuantity || 0);
+ return Math.abs(expected - Number(row.closingQuantity || 0)) > 0.0001;
+}
+async function loadWarehouses() {
+ if (!state.accessToken || warehouses.value.length) return;
+ warehouses.value = await getWarehouseSimpleList(state.accessToken);
+}
+async function load(reset = false) {
+ if (!state.accessToken || loading.value || !canQuery.value) return;
+ if (reset) { pageNo.value = 1; rows.value = []; }
+ loading.value = true;
+ loadError.value = "";
+ try {
+ await loadWarehouses();
+ const data = await getStockSummaryPage(state.accessToken, {
+ pageNo: pageNo.value,
+ pageSize: 15,
+ warehouseId: warehouseId.value,
+ startTime: startDate.value ? `${startDate.value} 00:00:00` : undefined,
+ endTime: endDate.value ? `${endDate.value} 23:59:59` : undefined,
+ });
+ total.value = data.total;
+ rows.value = reset ? data.list : [...rows.value, ...data.list];
+ pageNo.value += 1;
+ } catch (error) {
+ loadError.value = error instanceof Error ? error.message : "鏀跺彂瀛樻眹鎬诲姞杞藉け璐�";
+ } finally {
+ loading.value = false;
+ }
+}
+function pickWarehouse(event: { detail: { value: string } }) {
+ const index = Number(event.detail.value);
+ warehouseId.value = index === 0 ? undefined : warehouses.value[index - 1]?.id;
+ load(true);
+}
+function pickStart(event: { detail: { value: string } }) {
+ startDate.value = event.detail.value;
+ if (endDate.value && startDate.value > endDate.value) {
+ uni.showToast({ title: `寮�濮嬫棩鏈� ${startDate.value} 鏅氫簬缁撴潫鏃ユ湡 ${endDate.value}锛岃閲嶆柊閫夋嫨`, icon: "none" });
+ return;
+ }
+ load(true);
+}
+function pickEnd(event: { detail: { value: string } }) {
+ endDate.value = event.detail.value;
+ if (startDate.value && endDate.value < startDate.value) {
+ uni.showToast({ title: `缁撴潫鏃ユ湡 ${endDate.value} 鏃╀簬寮�濮嬫棩鏈� ${startDate.value}锛岃閲嶆柊閫夋嫨`, icon: "none" });
+ return;
+ }
+ load(true);
+}
+function resetFilter() {
+ warehouseId.value = undefined;
+ startDate.value = "";
+ endDate.value = "";
+ load(true);
+}
+onShow(() => load(true));
+onPullDownRefresh(async () => {
+ await load(true);
+ uni.stopPullDownRefresh();
+});
+</script>
+<template>
+ <view class="page">
+ <template v-if="isLoggedIn">
+ <view v-if="canQuery" class="filters">
+ <picker :range="warehouseOptions" @change="pickWarehouse">
+ <view class="picker"><text>{{ warehouseLabel }}</text><text>鈥�</text></view>
+ </picker>
+ <view class="dates">
+ <picker mode="date" :value="startDate" @change="pickStart">
+ <view class="picker"><text>{{ startDate || "寮�濮嬫棩鏈�" }}</text><text>鈥�</text></view>
+ </picker>
+ <text class="dash">鑷�</text>
+ <picker mode="date" :value="endDate" @change="pickEnd">
+ <view class="picker"><text>{{ endDate || "缁撴潫鏃ユ湡" }}</text><text>鈥�</text></view>
+ </picker>
+ </view>
+ <view class="filter-actions">
+ <text>榛樿缁熻杩戜笁涓湀锛涘凡鍔犺浇 {{ rows.length }} / {{ total }} 琛�</text>
+ <text class="reset" @click="resetFilter">閲嶇疆</text>
+ </view>
+ </view>
+ <view v-if="canQuery && rows.length" class="totals">
+ <view><text>鏈熷垵</text><text>{{ formatNumber(totals.opening) }}</text></view>
+ <view><text>鍏ュ簱</text><text class="pos">{{ formatNumber(totals.inbound) }}</text></view>
+ <view><text>鍑哄簱</text><text class="neg">{{ formatNumber(totals.outbound) }}</text></view>
+ <view><text>鏈熸湯</text><text>{{ formatNumber(totals.closing) }}</text></view>
+ </view>
+ <scroll-view v-if="canQuery" class="content" scroll-y lower-threshold="80" @scrolltolower="hasMore && load()">
+ <view v-if="loadError" class="state error" @click="load(true)">{{ loadError }}锛岀偣鍑婚噸璇�</view>
+ <view v-else-if="rows.length">
+ <view v-for="row in rows" :key="`${row.itemId}-${row.warehouseId}`" class="row">
+ <view class="head">
+ <view><text class="name">{{ row.itemName || "鏈懡鍚嶇墿鏂�" }}</text><text>{{ row.itemCode || "鏆傛棤缂栫爜" }} / {{ row.warehouseName || "鏈寚瀹氫粨搴�" }}</text></view>
+ <text :class="['status', mismatch(row) ? 'tag-danger' : 'tag-success']">{{ mismatch(row) ? "璐﹀疄寰呮牳" : "璐﹀疄鐩哥" }}</text>
+ </view>
+ <view class="grid">
+ <text>瑙勬牸鍨嬪彿锛歿{ row.specification || "--" }}</text>
+ <text>鍗曚綅锛歿{ row.unitMeasureName || "--" }}</text>
+ <text>鏈熷垵鏁伴噺锛歿{ formatNumber(row.openingQuantity) }}</text>
+ <text>鏈湡鍏ュ簱锛歿{ formatNumber(row.inQuantity) }}</text>
+ <text>鏈湡鍑哄簱锛歿{ formatNumber(row.outQuantity) }}</text>
+ <text>鏈熸湯鏁伴噺锛歿{ formatNumber(row.closingQuantity) }}</text>
+ </view>
+ </view>
+ <text class="more">{{ loading ? "鍔犺浇涓�..." : hasMore ? "涓婃媺鍔犺浇鏇村" : "娌℃湁鏇村浜�" }}</text>
+ </view>
+ <view v-else class="state">{{ loading ? "鍔犺浇涓�..." : "鎵�閫夋潯浠朵笅鏆傛棤鏀跺彂瀛樻暟鎹�" }}</view>
+ </scroll-view>
+ <view v-else class="state">鏆傛棤鏀跺彂瀛樻眹鎬绘煡鐪嬫潈闄�</view>
+ </template>
+ <view v-else class="state">鐧诲綍鍚庢煡鐪嬫敹鍙戝瓨姹囨��</view>
+ </view>
+</template>
+<style lang="scss" scoped>
+.page{height:100vh;display:flex;flex-direction:column;padding:0 24rpx;box-sizing:border-box;overflow:hidden;background:#f3f6fb}.filters{flex:none;padding:20rpx;margin:20rpx 0 14rpx;background:#fff;border-radius:16rpx;box-shadow:0 8rpx 20rpx rgba(43,69,107,.06)}.picker{display:flex;align-items:center;justify-content:space-between;gap:12rpx;min-height:66rpx;padding:0 18rpx;color:#31445c;font-size:25rpx;background:#f6f8fb;border-radius:12rpx}.picker text:first-child{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.picker text:last-child{flex:none;color:#8092a7;font-size:32rpx}.dates{display:flex;align-items:center;gap:12rpx;margin-top:12rpx}.dates picker{flex:1;min-width:0}.dash{flex:none;color:#8795a8;font-size:24rpx}.filter-actions{display:flex;align-items:center;justify-content:space-between;margin-top:12rpx;color:#8795a8;font-size:22rpx}.filter-actions .reset{color:var(--color-primary, #1769d8);font-size:24rpx}.totals{flex:none;display:flex;margin-bottom:14rpx;background:#fff;border-radius:16rpx;box-shadow:0 8rpx 20rpx rgba(43,69,107,.06)}.totals view{flex:1;display:flex;flex-direction:column;align-items:center;gap:6rpx;padding:16rpx 0;border-right:1rpx solid #f1f4f8}.totals view:last-child{border-right:0}.totals text:first-child{color:#8795a8;font-size:22rpx}.totals text:last-child{color:#20334c;font-size:27rpx;font-weight:600}.totals .pos{color:#17865a}.totals .neg{color:#c65050}.content{height:0;flex:1;padding-bottom:80rpx;box-sizing:border-box}.row{padding:24rpx 22rpx 20rpx;margin-bottom:18rpx;background:#fff;border-radius:16rpx;box-shadow:0 10rpx 24rpx rgba(43,69,107,.08)}.head{display:flex;align-items:flex-start;justify-content:space-between;gap:16rpx}.head>view:first-child{display:flex;min-width:0;flex:1;flex-direction:column;gap:8rpx}.head>view:first-child text:last-child{color:#78879b;font-size:23rpx}.name{overflow:hidden;color:#20334c;font-size:30rpx;font-weight:600;text-overflow:ellipsis;white-space:nowrap}.status{display:inline-flex;align-items:center;flex:none;height:44rpx;padding:0 18rpx;font-size:23rpx;border-radius:999rpx}.tag-success{color:#17865a;background:#eafaf2;border:1rpx solid #b8efd5}.tag-danger{color:#c65050;background:#fff7f7;border:1rpx solid #e3a0a0}.grid{display:grid;grid-template-columns:1fr 1fr;gap:10rpx;padding-top:16rpx;margin-top:16rpx;color:#66778c;font-size:23rpx;border-top:1rpx solid #edf1f6}.grid text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.more,.state{display:block;padding:35rpx;color:#8795a8;font-size:26rpx;text-align:center}.error{color:#c3654d}
+</style>
diff --git a/src/pages/workbench/index.vue b/src/pages/workbench/index.vue
index e96ed78..39b5f67 100644
--- a/src/pages/workbench/index.vue
+++ b/src/pages/workbench/index.vue
@@ -159,6 +159,8 @@
*/
function resolveModuleGroup(app: WorkbenchApp): string {
if (MODULE_GROUPS.includes(app.group)) return app.group;
+ // 璐ㄩ噺绠$悊锛堟潵鏂�/杩囩▼/鍑鸿揣/閫�璐ф楠屻�佷笉鍚堟牸鍝併�佹壒娆¤拷婧級灞炰簬鐢熶骇绠℃帶锛屾柟妗堥噷涓嶅崟鍒楁ā鍧�
+ if (app.group === "璐ㄩ噺绠$悊") return "鐢熶骇绠℃帶";
if (app.group === "鍩虹鏁版嵁") {
return /璁惧绫诲瀷|mes\/dv\//.test(`${app.name} ${app.component}`)
? "璁惧绠$悊"
--
Gitblit v1.9.3