From fdd158306bf6dc3584f5110a385323f4a8dfb463 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 02 九月 2026 14:25:10 +0800
Subject: [PATCH] feat(wms): 重构仓库管理系统并优化图表显示
---
src/views/mes/pro/scan-in/index.vue | 144 ++
src/views/mes/qc/batchtrace/modules/TraceGraph.vue | 47 +
src/views/dashboard/bagCodeHome/data.ts | 21
pnpm-lock.yaml | 3
src/adapter/vxe-table.ts | 17
src/views/mes/qc/batchtrace/modules/trace-detail.vue | 148 +-
src/views/dashboard/bagCodeHome/modules/scan-region-chart.vue | 36
src/views/wls/returnsales/modules/form.vue | 33
src/views/dashboard/bagCodeHome/modules/kpi-cards.vue | 108 ++
src/views/wls/salesnotice/components/select-dialog.vue | 14
src/api/mes/trace-cross-region/index.ts | 21
src/api/mes/trace/batch.ts | 150 +++
src/views/dashboard/bagCodeHome/modules/business-flow.vue | 74 +
src/views/erp/sale/order/index.vue | 31
src/views/wls/salesnotice/components/select.vue | 1
src/views/dashboard/bagCodeHome/index.vue | 120 ++
src/views/wls/warehouse/data.ts | 39
src/views/wls/transaction/data.ts | 1
src/views/mes/qc/batchtrace/modules/TraceStageOverview.vue | 40
src/views/wls/materialstock/data.ts | 21
src/api/mes/pro/scanEvent/index.ts | 7
src/views/dashboard/bagCodeHome/modules/alert-panel.vue | 74 +
src/views/wls/returnsales/modules/line-list.vue | 7
src/views/dashboard/bagCodeHome/chart-options.ts | 72 +
src/views/wls/salesnotice/modules/form.vue | 26
src/views/wls/materialstock/components/index.ts | 1
src/views/wls/materialstock/components/select-dialog.vue | 3
src/views/wls/materialstock/components/select.vue | 3
src/views/wls/transaction/index.vue | 30
src/views/wls/salesnotice/data.ts | 135 ++
src/packages/constants/src/biz-mes-enum.ts | 6
src/views/wls/materialstock/index.vue | 41
src/views/mes/qc/seedQuality/record.vue | 284 ++++++
src/api/mes/qc/seedQuality/index.ts | 25
src/views/wls/returnsales/data.ts | 99 --
src/views/dashboard/bagCodeHome/modules/shortcuts.vue | 98 +
src/views/wls/materialstock/components/stock-bag-modal.vue | 63 +
/dev/null | 74 -
src/views/mes/qc/batchtrace/modules/TraceDecision.vue | 46 +
src/api/erp/sale/order/index.ts | 2
src/api/mes/bag-code-home/index.ts | 5
src/views/wls/salesnotice/modules/line-form.vue | 92 ++
src/views/mes/qc/batchtrace/modules/TraceTimeline.vue | 37
package.json | 1
src/packages/effects/plugins/src/echarts/echarts.ts | 2
src/api/mes/wm/materialstock/index.ts | 42
src/views/wls/returnsales/modules/line-form.vue | 40
src/views/mes/qc/seedQuality/index.vue | 52
src/views/wls/productsales/data.ts | 56 -
src/api/mes/wm/warehouse/index.ts | 1
50 files changed, 1,877 insertions(+), 616 deletions(-)
diff --git a/package.json b/package.json
index a26e953..24978f2 100644
--- a/package.json
+++ b/package.json
@@ -60,6 +60,7 @@
"cropperjs": "catalog:",
"dayjs": "catalog:",
"dhtmlx-gantt": "catalog:",
+ "echarts": "catalog:",
"diagram-js": "catalog:",
"fast-xml-parser": "catalog:",
"gsap": "^3.15.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index caab85b..29cb323 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -644,6 +644,9 @@
diagram-js:
specifier: 'catalog:'
version: 15.17.0
+ echarts:
+ specifier: 'catalog:'
+ version: 6.1.0
fast-xml-parser:
specifier: 'catalog:'
version: 4.5.6
diff --git a/src/adapter/vxe-table.ts b/src/adapter/vxe-table.ts
index 01f7374..e6412e4 100644
--- a/src/adapter/vxe-table.ts
+++ b/src/adapter/vxe-table.ts
@@ -121,8 +121,9 @@
// 琛ㄦ牸閰嶇疆椤瑰彲浠ョ敤 cellRender: { name: 'CellTag' },
// props: { data: [{value, color}], labels?: {[value]: label} }
+ // 涔熷吋瀹� options 鏍煎紡: [{ label, value, color }]
// 鏀寔涓ょ鍖归厤妯″紡:
- // 1. 绮剧‘鍖归厤: data 鍜� labels 涓兘鏄簿纭�� (鐘舵��/绫诲瀷鏋氫妇)
+ // 1. 绮剧‘鍖归厤: data/labels/options 涓兘鏄簿纭�� (鐘舵��/绫诲瀷鏋氫妇)
// 2. 闃堝�煎尮閰�: data 涓洪檷搴忛槇鍊�, label 鏄剧ず鍘熷鏁板�� (璇勫垎)
vxeUI.renderer.add('CellTag', {
renderTableDefault(renderOpts, params) {
@@ -130,15 +131,25 @@
const { column, row } = params;
const rawValue = row[column.field];
+ // 鍏煎 options 鏍煎紡锛岀簿纭尮閰嶆樉绀烘枃鏈笌棰滆壊
+ const matchedOption = props?.options?.find(
+ (item: any) =>
+ item.value === rawValue || Number(item.value) === Number(rawValue),
+ );
+
// 鏌ユ壘鏄剧ず鏂囨湰
let label = rawValue;
- if (props?.labels && rawValue !== undefined && rawValue !== null) {
+ if (matchedOption) {
+ label = matchedOption.label;
+ } else if (props?.labels && rawValue !== undefined && rawValue !== null) {
label = props.labels[rawValue] ?? rawValue;
}
// 鏌ユ壘棰滆壊: 浼樺厛绮剧‘鍖归厤锛屽惁鍒欓槇鍊煎尮閰�
let color: string | undefined;
- if (props?.data) {
+ if (matchedOption) {
+ color = matchedOption.color;
+ } else if (props?.data) {
const exact = props.data.find(
(item: any) => item.value === rawValue,
);
diff --git a/src/api/erp/sale/order/index.ts b/src/api/erp/sale/order/index.ts
index 5468cab..bd1da49 100644
--- a/src/api/erp/sale/order/index.ts
+++ b/src/api/erp/sale/order/index.ts
@@ -29,8 +29,6 @@
needProduction?: number; // 鏄惁闇�瑕佺敓浜э細0-涓嶉渶瑕侊紝1-闇�瑕�
productionStatus?: number; // 鐢熶骇鐘舵�侊細0-鏈畬鎴愶紝1-宸插畬鎴�
productionFinishTime?: Date; // 鐢熶骇瀹屾垚鏃堕棿
- hasSalesNotice?: boolean; // 鏄惁宸茬敓鎴愬彂璐ч�氱煡鍗�
- canCreateSalesNotice?: boolean; // 鏄惁鍙互鐢熸垚鍙戣揣閫氱煡鍗�
items?: SaleOrderItem[]; // 閿�鍞鍗曚骇鍝佹槑缁嗗垪琛�
}
diff --git a/src/api/mes/bag-code-home/index.ts b/src/api/mes/bag-code-home/index.ts
index 478c99e..b017adc 100644
--- a/src/api/mes/bag-code-home/index.ts
+++ b/src/api/mes/bag-code-home/index.ts
@@ -17,6 +17,11 @@
batchCount: number; // 鎵规鎬绘暟
bagCodeCount: number; // 琚嬬爜鎬绘暟
palletCount: number; // 鎵樼洏鐮佹�绘暟
+ salesNoticeCount: number; // 鍙戣揣閫氱煡鍗曟暟
+ productSalesCount: number; // 閿�鍞嚭搴撳崟鏁�
+ returnSalesCount: number; // 閿�鍞��璐у崟鏁�
+ stockWarningPendingCount: number; // 鏈鐞嗗簱瀛橀璀︽暟
+ ncrPendingCount: number; // 寰呰瘎瀹′笉鍚堟牸鍝佸崟鏁�
batchStatusList: StatusItem[]; // 鎵规鐘舵�佸垎甯�
bagCodeStatusList: StatusItem[]; // 琚嬬爜鐘舵�佸垎甯�
}
diff --git a/src/api/mes/pro/scanEvent/index.ts b/src/api/mes/pro/scanEvent/index.ts
index d09dd24..e19bb1f 100644
--- a/src/api/mes/pro/scanEvent/index.ts
+++ b/src/api/mes/pro/scanEvent/index.ts
@@ -3,10 +3,14 @@
import { requestClient } from '#/api/request';
export namespace MesProScanEventApi {
+ /** 鎵爜绫诲瀷锛氳鐮� / 鎵樼洏鐮� / 鎵规鐮� */
+ export type ScanType = 'BAG' | 'PALLET' | 'BATCH';
+
export interface ScanEvent {
id?: number;
eventId?: string;
bagCode?: string;
+ scanType?: ScanType;
batchCode?: string;
quantity?: number;
deviceCode?: string;
@@ -31,8 +35,9 @@
export interface InboundParams {
eventId: string;
bagCode: string;
+ scanType: ScanType;
quantity: number;
- deviceCode: string;
+ deviceCode?: string;
lineCode?: string;
locationId: number;
areaId: number;
diff --git a/src/api/mes/qc/seedQuality/index.ts b/src/api/mes/qc/seedQuality/index.ts
index fa5e1f9..4979bed 100644
--- a/src/api/mes/qc/seedQuality/index.ts
+++ b/src/api/mes/qc/seedQuality/index.ts
@@ -1,5 +1,24 @@
import { requestClient } from '#/api/request';
+export interface SeedQualityPageParams {
+ pageNo?: number;
+ pageSize?: number;
+ code?: string;
+ batchCode?: string;
+ result?: number;
+}
+
+export interface SeedQualityRecord {
+ id: number;
+ code: string;
+ batchCode?: string;
+ result: number;
+ reason?: string;
+ inspectorId?: number;
+ inspectedAt?: string;
+ createTime?: string;
+}
+
export namespace MesQcSeedQualityApi {
export interface Indicator {
code: string;
@@ -10,13 +29,17 @@
export interface SubmitParams {
code: string;
- itemId: number;
batchCode?: string;
indicators: Indicator[];
reason?: string;
+ qualifiedWarehouseId: number; // 鐩爣鍚堟牸搴擄紙璐ㄦ鍚堟牸鍚庡叆璇ヤ粨搴擄紝浠撳簱绫诲瀷涓哄悎鏍煎簱20锛�
}
}
+export function getSeedQualityPage(params: SeedQualityPageParams) {
+ return requestClient.get<{ list: SeedQualityRecord[]; total: number }>('/mes/qc/seed-quality/page', { params });
+}
+
export function submitSeedQuality(data: MesQcSeedQualityApi.SubmitParams) {
return requestClient.post<number>('/mes/qc/seed-quality/submit', data);
}
diff --git a/src/api/mes/trace-cross-region/index.ts b/src/api/mes/trace-cross-region/index.ts
new file mode 100644
index 0000000..13f2245
--- /dev/null
+++ b/src/api/mes/trace-cross-region/index.ts
@@ -0,0 +1,21 @@
+import { requestClient } from '#/api/request';
+
+export namespace MesTraceCrossRegionApi {
+ /** 绐滆揣棰勮姹囨�� */
+ export interface Summary {
+ total?: number; // 棰勮鎬绘暟
+ pending?: number; // 寰呭鐞嗘暟閲�
+ handled?: number; // 宸插鐞嗘暟閲�
+ falseAlarm?: number; // 璇姤鏁伴噺
+ crossProvince?: number; // 璺ㄧ渷绐滆揣鏁伴噺
+ crossCity?: number; // 璺ㄥ競绐滆揣鏁伴噺
+ }
+}
+
+/** 鑾峰緱绐滆揣棰勮姹囨�� */
+export function getTraceCrossRegionSummary(startTime?: string, endTime?: string) {
+ return requestClient.get<MesTraceCrossRegionApi.Summary>(
+ '/mes/trace-cross-region/summary',
+ { params: { startTime, endTime } },
+ );
+}
diff --git a/src/api/mes/trace/batch.ts b/src/api/mes/trace/batch.ts
new file mode 100644
index 0000000..c94fc98
--- /dev/null
+++ b/src/api/mes/trace/batch.ts
@@ -0,0 +1,150 @@
+import { requestClient } from '#/api/request';
+
+export namespace MesTraceBatchApi {
+ export interface Worker {
+ workerType?: number;
+ workerTypeName?: string;
+ userName?: string;
+ remark?: string;
+ }
+
+ export interface Pallet {
+ code?: string;
+ palletNo?: number;
+ bagCount?: number;
+ capacity?: number;
+ }
+
+ export interface Bag {
+ code?: string;
+ bagNo?: number;
+ palletCode?: string;
+ status?: number;
+ statusName?: string;
+ inboundTime?: string;
+ }
+
+ export interface Inbound {
+ eventId?: string;
+ deviceCode?: string;
+ lineCode?: string;
+ quantity?: number;
+ status?: string;
+ eventTime?: string;
+ }
+
+ export interface Indicator {
+ indicatorCode?: string;
+ indicatorName?: string;
+ value?: number;
+ minValue?: number;
+ maxValue?: number;
+ pass?: boolean;
+ }
+
+ export interface Quality {
+ result?: number;
+ resultName?: string;
+ reason?: string;
+ inspectorName?: string;
+ inspectedAt?: string;
+ indicators?: Indicator[];
+ }
+
+ export interface PurchaseSource {
+ receiptCode?: string;
+ receiptDate?: string;
+ purchaseOrderCode?: string;
+ vendorCode?: string;
+ vendorName?: string;
+ iqcCode?: string;
+ checkResult?: number;
+ itemCode?: string;
+ itemName?: string;
+ receivedQuantity?: number;
+ }
+
+ export interface SalesTarget {
+ salesCode?: string;
+ salesDate?: string;
+ salesOrderCode?: string;
+ clientName?: string;
+ oqcCode?: string;
+ checkResult?: number;
+ itemCode?: string;
+ itemName?: string;
+ quantity?: number;
+ qualityStatus?: number;
+ }
+
+ export interface ProcessTrace {
+ eventType?: 'OUT' | 'IN';
+ workOrderCode?: string;
+ processCode?: string;
+ processName?: string;
+ quantity?: number;
+ qualityStatus?: number;
+ eventTime?: string;
+ }
+
+ export interface Timeline {
+ stage?: string;
+ eventType?: string;
+ eventName?: string;
+ eventTime?: string;
+ quantity?: number;
+ status?: string;
+ }
+
+ export interface Decision {
+ qualityRisk?: boolean;
+ inboundQuantity?: number;
+ salesTargetCount?: number;
+ consumerScanCount?: number;
+ riskLevel?: string;
+ }
+
+ export interface Detail {
+ sourceType?: string;
+ requestCode?: string;
+ itemCode?: string;
+ itemName?: string;
+ itemSpecification?: string;
+ batchCode?: string;
+ tempCode?: string;
+ batchStatus?: number;
+ batchStatusName?: string;
+ produceDate?: string;
+ lineCode?: string;
+ lineName?: string;
+ ownerUserName?: string;
+ planQuantity?: number;
+ bagQuantity?: number;
+ palletQuantity?: number;
+ inboundTime?: string;
+ remark?: string;
+ workers?: Worker[];
+ palletCount?: number;
+ pallets?: Pallet[];
+ bagTotal?: number;
+ bags?: Bag[];
+ inbound?: Inbound;
+ quality?: Quality;
+ antiFake?: {
+ scanCount?: number;
+ firstScanTime?: string;
+ lastScanTime?: string;
+ };
+ purchaseSources?: PurchaseSource[];
+ salesTargets?: SalesTarget[];
+ processTraces?: ProcessTrace[];
+ timeline?: Timeline[];
+ decision?: Decision;
+ }
+}
+
+export function getBatchTraceDetail(code: string) {
+ return requestClient.get<MesTraceBatchApi.Detail>('/mes/trace/batch/detail', {
+ params: { code },
+ });
+}
\ No newline at end of file
diff --git a/src/api/mes/wm/materialstock/index.ts b/src/api/mes/wm/materialstock/index.ts
index 7316dc2..43315a5 100644
--- a/src/api/mes/wm/materialstock/index.ts
+++ b/src/api/mes/wm/materialstock/index.ts
@@ -34,6 +34,9 @@
warningLevel?: 'LOW_STOCK' | 'EXPIRING' | 'AGEING' | null; // 椋庨櫓绫诲瀷
receiptTime?: string; // 鍏ュ簱鏃ユ湡
frozen?: boolean; // 鏄惁鍐荤粨
+ bagCount?: number; // 褰撳墠琚嬬爜鏁伴噺
+ availableBagCount?: number; // 鍙敤琚嬬爜鏁伴噺
+ bagDetailSupported?: boolean; // 鏄惁瀛樺湪琚嬬爜鏄庣粏
createTime?: Date; // 鍒涘缓鏃堕棿
}
@@ -52,8 +55,14 @@
}
}
+/** 搴撳瓨鍙拌处鍒嗛〉鏌ヨ鍙傛暟 */
+export interface MaterialStockPageParams extends PageParam {
+ warehouseType?: number; // 浠撳簱绫诲瀷锛�10鏆傚瓨搴擄紝20鍚堟牸搴擄紙鎸夌被鍨嬭繃婊や粨搴擄級
+ stockState?: number; // 搴撳瓨鐘舵�侊細10鏆傚瓨搴擄紝20鍚堟牸搴擄紝30宸插嚭搴�
+}
+
/** 鏌ヨ搴撳瓨鍙拌处鍒嗛〉 */
-export function getMaterialStockPage(params: PageParam) {
+export function getMaterialStockPage(params: MaterialStockPageParams) {
return requestClient.get<PageResult<MesWmMaterialStockApi.MaterialStock>>(
'/mes/wm/material-stock/page',
{ params },
@@ -61,6 +70,36 @@
}
/** 鏌ヨ搴撳瓨鍙拌处璇︽儏 */
+export interface MaterialStockBagDetail {
+ id: number;
+ materialStockId: number;
+ bagCodeId?: number;
+ bagCode: string;
+ batchId?: number;
+ batchCode?: string;
+ warehouseId?: number;
+ locationId?: number;
+ areaId?: number;
+ stockState?: number;
+ status?: number;
+ quantity?: number;
+ frozen?: boolean;
+ inboundTime?: string;
+ outboundTime?: string;
+}
+
+export function getMaterialStockBagDetails(params: {
+ bagCode?: string;
+ materialStockId: number;
+ pageNo?: number;
+ pageSize?: number;
+}) {
+ return requestClient.get<PageResult<MaterialStockBagDetail>>(
+ '/mes/wm/material-stock/bag-details',
+ { params },
+ );
+}
+
export function getMaterialStock(id: number) {
return requestClient.get<MesWmMaterialStockApi.MaterialStock>(
`/mes/wm/material-stock/get?id=${id}`,
@@ -160,6 +199,7 @@
/** 搴撳瓨姹囨�绘煡璇㈠弬鏁� */
export interface MaterialStockSummaryParams {
stockState?: number; // 搴撳瓨鐘舵�侊細10鏆傚瓨搴擄紝20鍚堟牸搴擄紝30宸插嚭搴�
+ warehouseType?: number; // 浠撳簱绫诲瀷锛�10鏆傚瓨搴擄紝20鍚堟牸搴擄紙鎸夌被鍨嬭繃婊や粨搴擄級
itemId?: number; // 鍝佺ID
batchCode?: string; // 鎵规鍙�
warehouseId?: number; // 浠撳簱ID
diff --git a/src/api/mes/wm/warehouse/index.ts b/src/api/mes/wm/warehouse/index.ts
index 661b62e..4ce30c8 100644
--- a/src/api/mes/wm/warehouse/index.ts
+++ b/src/api/mes/wm/warehouse/index.ts
@@ -12,6 +12,7 @@
area?: number; // 闈㈢Н
chargeUserId?: number; // 璐熻矗浜�
frozen?: boolean; // 鏄惁鍐荤粨
+ type?: number; // 浠撳簱绫诲瀷锛�10鏆傚瓨搴擄紝20鍚堟牸搴�
remark?: string; // 澶囨敞
createTime?: Date; // 鍒涘缓鏃堕棿
}
diff --git a/src/packages/constants/src/biz-mes-enum.ts b/src/packages/constants/src/biz-mes-enum.ts
index e26d510..1938a04 100644
--- a/src/packages/constants/src/biz-mes-enum.ts
+++ b/src/packages/constants/src/biz-mes-enum.ts
@@ -233,6 +233,12 @@
OUT: 30, // 宸插嚭搴�
} as const;
+/** MES 浠撳簱绫诲瀷鏋氫妇锛堝榻愬悗绔� MesWmWarehouseTypeEnum锛� */
+export const MesWmWarehouseTypeEnum = {
+ TEMP: 10, // 鏆傚瓨搴擄紙濡傝櫄鎷熺嚎杈逛粨搴擄紝瀛樻斁鎵爜鍏ュ簱寰呮/鏆傚瓨搴撳瓨锛�
+ QUALIFIED: 20, // 鍚堟牸搴擄紙瀛樻斁璐ㄦ鍚堟牸銆佸彲鎷h揣搴撳瓨锛�
+} as const;
+
/** MES 搴撳瓨棰勮绫诲瀷鏋氫妇 */
export const MesWmStockWarningTypeEnum = {
LOW_STOCK: 10, // 浣庡簱瀛�
diff --git a/src/packages/effects/plugins/src/echarts/echarts.ts b/src/packages/effects/plugins/src/echarts/echarts.ts
index bd9b828..0022b76 100644
--- a/src/packages/effects/plugins/src/echarts/echarts.ts
+++ b/src/packages/effects/plugins/src/echarts/echarts.ts
@@ -3,6 +3,7 @@
EffectScatterChart,
FunnelChart,
GaugeChart,
+ GraphChart,
LineChart,
LinesChart,
MapChart,
@@ -48,6 +49,7 @@
LineChart,
FunnelChart,
GaugeChart,
+ GraphChart,
ScatterChart,
EffectScatterChart,
LinesChart,
diff --git a/src/views/dashboard/bagCodeHome/chart-options.ts b/src/views/dashboard/bagCodeHome/chart-options.ts
index 045568b..a9c4cf2 100644
--- a/src/views/dashboard/bagCodeHome/chart-options.ts
+++ b/src/views/dashboard/bagCodeHome/chart-options.ts
@@ -1,5 +1,16 @@
import type { EChartsOption } from '@vben/plugins/echarts';
+/** 鍐滀笟涓婚锛氱繝缁夸富鑹诧紙瀵归綈鐧诲綍椤碉紝娴呰壊鐜荤拑鐗堬級 */
+export const AGRI_GREEN = '#10b981';
+export const AGRI_GREEN_LIGHT = '#34d399';
+export const AGRI_GREEN_PALE = '#6ee7b7';
+export const AGRI_TEXT = '#134e4a';
+export const AGRI_TEXT_MUTED = 'rgba(15, 78, 60, 0.62)';
+export const AGRI_SPLIT = 'rgba(16, 185, 129, 0.18)';
+
+/** 娣卞簳閫氱敤鏂囨湰鏍峰紡 */
+const darkText: NonNullable<EChartsOption['textStyle']> = { color: AGRI_TEXT };
+
/** 琚嬬爜鐢熸垚瓒嬪娍鏌辩姸鍥鹃厤缃� */
export function getBagCodeTrendChartOptions(
dates: string[],
@@ -7,19 +18,31 @@
): EChartsOption {
return {
grid: { bottom: 40, left: 50, right: 20, top: 20 },
- legend: { bottom: 0, data: ['琚嬬爜鐢熸垚閲�'] },
+ legend: { bottom: 0, data: ['琚嬬爜鐢熸垚閲�'], textStyle: { color: AGRI_TEXT_MUTED } },
series: [
{
barMaxWidth: 32,
data: counts,
- itemStyle: { borderRadius: 4, color: '#409EFF' },
+ itemStyle: { borderRadius: 4, color: AGRI_GREEN },
name: '琚嬬爜鐢熸垚閲�',
type: 'bar',
},
],
+ textStyle: darkText,
tooltip: { axisPointer: { type: 'shadow' }, trigger: 'axis' },
- xAxis: { axisTick: { show: false }, data: dates, type: 'category' },
- yAxis: { minInterval: 1, type: 'value' },
+ xAxis: {
+ axisLine: { lineStyle: { color: AGRI_SPLIT } },
+ axisLabel: { color: AGRI_TEXT_MUTED },
+ axisTick: { show: false },
+ data: dates,
+ type: 'category',
+ },
+ yAxis: {
+ minInterval: 1,
+ splitLine: { lineStyle: { color: AGRI_SPLIT } },
+ axisLabel: { color: AGRI_TEXT_MUTED },
+ type: 'value',
+ },
};
}
@@ -28,18 +51,53 @@
data: Array<{ itemStyle: { color: string }; name: string; value: number }>,
): EChartsOption {
return {
- legend: { bottom: 0, type: 'scroll' },
+ legend: { bottom: 0, textStyle: { color: AGRI_TEXT_MUTED }, type: 'scroll' },
series: [
{
avoidLabelOverlap: true,
data,
emphasis: { label: { fontSize: 14, fontWeight: 'bold', show: true } },
- itemStyle: { borderColor: '#fff', borderRadius: 6, borderWidth: 2 },
- label: { formatter: '{b}\n{c}', show: true },
+ itemStyle: { borderColor: '#ffffff', borderRadius: 6, borderWidth: 2 },
+ label: { color: AGRI_TEXT, formatter: '{b}\n{c}', show: true },
radius: ['40%', '70%'],
type: 'pie',
},
],
+ textStyle: darkText,
tooltip: { formatter: '{b}: {c} ({d}%)', trigger: 'item' },
};
}
+
+/** 娑堣垂鑰呮壂鐮佸湴鍖哄垎甯冩í鍚戞煴鐘跺浘閰嶇疆 */
+export function getRegionBarChartOptions(
+ data: Array<{ name: string; count: number }>,
+): EChartsOption {
+ const sorted = [...data].sort((a, b) => b.count - a.count);
+ return {
+ grid: { bottom: 20, left: 16, right: 40, top: 20, containLabel: true },
+ series: [
+ {
+ barMaxWidth: 20,
+ data: sorted.map((d) => d.count),
+ itemStyle: { borderRadius: 4, color: AGRI_GREEN },
+ name: '鎵爜娆℃暟',
+ type: 'bar',
+ },
+ ],
+ textStyle: darkText,
+ tooltip: { axisPointer: { type: 'shadow' }, trigger: 'axis' },
+ xAxis: {
+ axisLabel: { color: AGRI_TEXT_MUTED },
+ axisLine: { lineStyle: { color: AGRI_SPLIT } },
+ splitLine: { lineStyle: { color: AGRI_SPLIT } },
+ type: 'value',
+ },
+ yAxis: {
+ axisLabel: { color: AGRI_TEXT },
+ axisLine: { lineStyle: { color: AGRI_SPLIT } },
+ data: sorted.map((d) => d.name),
+ type: 'category',
+ inverse: true,
+ },
+ };
+}
diff --git a/src/views/dashboard/bagCodeHome/data.ts b/src/views/dashboard/bagCodeHome/data.ts
index 6dfefbc..beb3a96 100644
--- a/src/views/dashboard/bagCodeHome/data.ts
+++ b/src/views/dashboard/bagCodeHome/data.ts
@@ -10,21 +10,26 @@
lineCount: 0,
mdmItemCount: 0,
mesItemCount: 0,
+ ncrPendingCount: 0,
palletCount: 0,
+ productSalesCount: 0,
+ returnSalesCount: 0,
+ salesNoticeCount: 0,
+ stockWarningPendingCount: 0,
unitMeasureCount: 0,
};
-/** 鎵规鐘舵�佸搴旂殑棰滆壊鏄犲皠锛堢姸鎬佸瓧鍏革細10 鑽夌 20 鐢熶骇涓� 30 宸插畬鎴� 40 宸蹭綔搴燂級 */
+/** 鎵规鐘舵�佸搴旂殑棰滆壊鏄犲皠锛堢姸鎬佸瓧鍏革細10 鑽夌 20 鐢熶骇涓� 30 宸插畬鎴� 40 宸蹭綔搴燂級瀵归綈鍐滀笟缈犵豢涓婚 */
export const BATCH_STATUS_COLOR_MAP: Record<number, string> = {
- 10: '#909399', // 鑽夌
- 20: '#409EFF', // 鐢熶骇涓�
- 30: '#67C23A', // 宸插畬鎴�
- 40: '#F56C6C', // 宸蹭綔搴�
+ 10: '#94a3b8', // 鑽夌
+ 20: '#10b981', // 鐢熶骇涓�
+ 30: '#34d399', // 宸插畬鎴�
+ 40: '#fca5a5', // 宸蹭綔搴�
};
/** 琚嬬爜鐘舵�佸搴旂殑棰滆壊鏄犲皠锛堢姸鎬佸瓧鍏革細10 宸茬敓鎴� 20 宸插鍑哄嵃鍒� 30 宸蹭綔搴燂級 */
export const BAG_CODE_STATUS_COLOR_MAP: Record<number, string> = {
- 10: '#409EFF', // 宸茬敓鎴�
- 20: '#E6A23C', // 宸插鍑哄嵃鍒�
- 30: '#F56C6C', // 宸蹭綔搴�
+ 10: '#10b981', // 宸茬敓鎴�
+ 20: '#e6a23c', // 宸插鍑哄嵃鍒�
+ 30: '#fca5a5', // 宸蹭綔搴�
};
diff --git a/src/views/dashboard/bagCodeHome/index.vue b/src/views/dashboard/bagCodeHome/index.vue
index 3887aca..c268b96 100644
--- a/src/views/dashboard/bagCodeHome/index.vue
+++ b/src/views/dashboard/bagCodeHome/index.vue
@@ -1,5 +1,6 @@
<script lang="ts" setup>
import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
+import type { MesTraceCrossRegionApi } from '#/api/mes/trace-cross-region';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
@@ -9,12 +10,16 @@
import { Col, Row, message } from 'ant-design-vue';
import { getBagCodeHomeSummary } from '#/api/mes/bag-code-home';
+import { getTraceCrossRegionSummary } from '#/api/mes/trace-cross-region';
+import { getTraceConsumerScanSummary } from '#/api/mes/trace-consumer-scan';
import { defaultSummary } from './data';
-import BagCodeCards from './modules/bag-code-cards.vue';
+import AlertPanel from './modules/alert-panel.vue';
import BagCodeStatusChart from './modules/bag-code-status-chart.vue';
-import BasicCards from './modules/basic-cards.vue';
import BatchStatusChart from './modules/batch-status-chart.vue';
+import BusinessFlow from './modules/business-flow.vue';
+import KpiCards from './modules/kpi-cards.vue';
+import ScanRegionChart from './modules/scan-region-chart.vue';
import Shortcuts from './modules/shortcuts.vue';
import TrendChart from './modules/trend-chart.vue';
@@ -22,6 +27,8 @@
const router = useRouter();
const summary = ref<MesBagCodeHomeApi.Summary>(defaultSummary); // 棣栭〉姹囨�荤粺璁�
+const crossRegion = ref<MesTraceCrossRegionApi.Summary>({}); // 绐滆揣棰勮姹囨��
+const scanTotal = ref(0); // 绱鎵爜娆℃暟
/** 璺宠浆鍒扮洰鏍囬〉闈紙鎸夎矾鐢� name锛屾湭鍔犺浇鏃舵彁绀猴級 */
async function handleNavigate(name: string) {
@@ -32,9 +39,12 @@
}
}
-/** 鍔犺浇棣栭〉姹囨�荤粺璁� */
+/** 鍔犺浇棣栭〉姹囨�荤粺璁°�佺獪璐ч璀︽眹鎬汇�佹秷璐硅�呮壂鐮佹眹鎬� */
async function loadSummary() {
summary.value = await getBagCodeHomeSummary();
+ crossRegion.value = await getTraceCrossRegionSummary();
+ const scan = await getTraceConsumerScanSummary();
+ scanTotal.value = scan.total ?? 0;
}
onMounted(() => {
@@ -44,26 +54,94 @@
<template>
<Page>
- <!-- 绗竴琛岋細鍩虹鏁版嵁鍗$墖 -->
- <BasicCards :summary="summary" class="mb-2" @navigate="handleNavigate" />
+ <div class="agri-dashboard">
+ <!-- 绗竴琛岋細涓氬姟鍏ㄩ摼璺� -->
+ <BusinessFlow
+ :cross-region-total="crossRegion.total ?? 0"
+ :scan-total="scanTotal"
+ :summary="summary"
+ @navigate="handleNavigate"
+ />
- <!-- 绗簩琛岋細涓�琚嬩竴鐮佸崱鐗� -->
- <BagCodeCards :summary="summary" class="mb-4" @navigate="handleNavigate" />
+ <!-- 绗簩琛岋細鍏ㄩ摼璺� KPI 鎬昏 -->
+ <KpiCards :scan-total="scanTotal" :summary="summary" @navigate="handleNavigate" />
- <!-- 绗笁琛岋細琚嬬爜瓒嬪娍 + 鎵规/琚嬬爜鐘舵�佸垎甯� -->
- <Row :gutter="16" class="mb-4">
- <Col :lg="12" :md="24" :sm="24" :xl="12" :xs="24" class="mb-4">
- <TrendChart />
- </Col>
- <Col :lg="6" :md="12" :sm="24" :xl="6" :xs="24" class="mb-4">
- <BatchStatusChart :list="summary.batchStatusList" />
- </Col>
- <Col :lg="6" :md="12" :sm="24" :xl="6" :xs="24" class="mb-4">
- <BagCodeStatusChart :list="summary.bagCodeStatusList" />
- </Col>
- </Row>
+ <!-- 绗笁琛岋細琚嬬爜鐢熸垚瓒嬪娍 + 娑堣垂鑰呮壂鐮佸湴鍖哄垎甯� -->
+ <Row :gutter="16" class="mb-4">
+ <Col :lg="12" :md="24" :sm="24" :xl="12" :xs="24" class="mb-4">
+ <TrendChart />
+ </Col>
+ <Col :lg="12" :md="24" :sm="24" :xl="12" :xs="24" class="mb-4">
+ <ScanRegionChart />
+ </Col>
+ </Row>
- <!-- 绗洓琛岋細蹇嵎鍏ュ彛 -->
- <Shortcuts @navigate="handleNavigate" />
+ <!-- 绗洓琛岋細鎵规/琚嬬爜鐘舵�佸垎甯� + 寰呭姙涓庨璀� -->
+ <Row :gutter="16" class="mb-4">
+ <Col :lg="6" :md="12" :sm="24" :xl="6" :xs="24" class="mb-4">
+ <BatchStatusChart :list="summary.batchStatusList" />
+ </Col>
+ <Col :lg="6" :md="12" :sm="24" :xl="6" :xs="24" class="mb-4">
+ <BagCodeStatusChart :list="summary.bagCodeStatusList" />
+ </Col>
+ <Col :lg="12" :md="24" :sm="24" :xl="12" :xs="24" class="mb-4">
+ <AlertPanel :cross-region="crossRegion" :summary="summary" @navigate="handleNavigate" />
+ </Col>
+ </Row>
+
+ <!-- 绗簲琛岋細鎸変笟鍔″煙鍒嗙粍鐨勫揩鎹峰叆鍙� -->
+ <Shortcuts @navigate="handleNavigate" />
+ </div>
</Page>
</template>
+
+<style lang="scss">
+/* 鍐滀笟缈犵豢鐜荤拑鎷熸�侀椤典富棰� 鈥斺�� 瀵归綈鐧诲綍椤� `agri-auth` 鑹茶皟锛岄噰鐢ㄦ洿娓呮贰鐨勬祬鑹茬幓鐠冧笌楂樼骇娓愬彉 */
+.agri-dashboard {
+ position: relative;
+ padding: 1rem;
+ border-radius: 1.25rem;
+ background:
+ radial-gradient(900px 480px at 8% 0%, rgba(110, 231, 183, 0.42), transparent 55%),
+ radial-gradient(800px 420px at 96% 100%, rgba(6, 95, 70, 0.12), transparent 60%),
+ linear-gradient(160deg, #f0fdf9 0%, #dcf7ea 50%, #c7f0d9 100%);
+ isolation: isolate;
+ color: #0f5c49;
+
+ /* 楂樼骇鐧界幓鐠冨崱鐗囷細鍐呮弿杈� + 纾ㄧ爞 + 鏌斿拰灞傛闃村奖 */
+ .ant-card {
+ color: #135746;
+ background: linear-gradient(160deg, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0.55));
+ border: 1px solid rgba(16, 185, 129, 0.18);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.8),
+ 0 12px 30px rgba(6, 78, 59, 0.08);
+ backdrop-filter: blur(14px);
+ }
+
+ .ant-card-head {
+ border-bottom-color: rgba(16, 185, 129, 0.14);
+ }
+
+ .ant-card-head-title,
+ .ant-card-head-title .anticon {
+ color: #0f766e;
+ }
+
+ .ant-card:hover {
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.9),
+ 0 18px 44px rgba(6, 78, 59, 0.14);
+ border-color: rgba(16, 185, 129, 0.34);
+ }
+
+ /* 瑕嗙洊 App 娴呰壊涓婚鐨勮涔夎壊锛氱粺涓�涓烘煍鍜屽ⅷ缁� */
+ .text-muted-foreground {
+ color: rgba(15, 78, 60, 0.62);
+ }
+
+ .text-2xl.font-bold {
+ color: #047857;
+ }
+}
+</style>
diff --git a/src/views/dashboard/bagCodeHome/modules/alert-panel.vue b/src/views/dashboard/bagCodeHome/modules/alert-panel.vue
new file mode 100644
index 0000000..40ec45a
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/alert-panel.vue
@@ -0,0 +1,74 @@
+<script lang="ts" setup>
+import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
+import type { MesTraceCrossRegionApi } from '#/api/mes/trace-cross-region';
+
+import { computed } from 'vue';
+
+import { IconifyIcon } from '@vben/icons';
+
+import { Badge, Card } from 'ant-design-vue';
+
+defineOptions({ name: 'BagCodeHomeAlertPanel' });
+
+const props = defineProps<{
+ summary: MesBagCodeHomeApi.Summary;
+ crossRegion: MesTraceCrossRegionApi.Summary;
+}>();
+
+const emit = defineEmits<{
+ navigate: [name: string];
+}>();
+
+/** 寰呭姙涓庨璀﹀垪琛� */
+const alertItems = computed(() => [
+ {
+ count: props.crossRegion.pending ?? 0,
+ desc: '寰呭鐞嗙殑绐滆揣棰勮',
+ icon: 'lucide:shield-alert',
+ iconClass: 'bg-[rgba(239,68,68,0.12)] text-[#ef4444]',
+ label: '绐滆揣棰勮',
+ routeName: 'MesTraceCrossRegion',
+ },
+ {
+ count: props.summary.stockWarningPendingCount ?? 0,
+ desc: '鏈鐞嗙殑搴撳瓨棰勮',
+ icon: 'lucide:bell-alert',
+ iconClass: 'bg-[rgba(245,158,11,0.14)] text-[#f59e0b]',
+ label: '搴撳瓨棰勮',
+ routeName: 'MesWmStockWarning',
+ },
+ {
+ count: props.summary.ncrPendingCount ?? 0,
+ desc: '寰呰瘎瀹$殑涓嶅悎鏍煎搧鍗�',
+ icon: 'lucide:shield-x',
+ iconClass: 'bg-[rgba(16,185,129,0.14)] text-[#10b981]',
+ label: '涓嶅悎鏍煎搧',
+ routeName: 'MesQcNcr',
+ },
+]);
+</script>
+
+<template>
+ <Card title="寰呭姙涓庨璀�" class="h-full">
+ <div class="flex flex-col">
+ <div
+ v-for="item in alertItems"
+ :key="item.label"
+ class="flex cursor-pointer items-center gap-3 border-b border-[rgba(16,185,129,0.16)] px-2 py-4 transition-colors last:border-b-0 hover:bg-[rgba(16,185,129,0.08)]"
+ @click="emit('navigate', item.routeName)"
+ >
+ <div
+ class="flex size-10 flex-shrink-0 items-center justify-center rounded-lg"
+ :class="item.iconClass"
+ >
+ <IconifyIcon class="size-5" :icon="item.icon" />
+ </div>
+ <div class="flex flex-1 flex-col gap-0.5">
+ <span class="text-sm font-medium">{{ item.label }}</span>
+ <span class="text-muted-foreground text-xs">{{ item.desc }}</span>
+ </div>
+ <Badge :count="item.count" :show-zero="false" />
+ </div>
+ </div>
+ </Card>
+</template>
diff --git a/src/views/dashboard/bagCodeHome/modules/bag-code-cards.vue b/src/views/dashboard/bagCodeHome/modules/bag-code-cards.vue
deleted file mode 100644
index 42fa28b..0000000
--- a/src/views/dashboard/bagCodeHome/modules/bag-code-cards.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-<script lang="ts" setup>
-import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
-
-import { CountTo } from '@vben/common-ui';
-import { IconifyIcon } from '@vben/icons';
-
-import { Card, Col, Row } from 'ant-design-vue';
-
-defineOptions({ name: 'BagCodeHomeBagCodeCards' });
-
-const props = defineProps<{
- summary: MesBagCodeHomeApi.Summary;
-}>();
-
-const emit = defineEmits<{
- navigate: [name: string];
-}>();
-
-/** 涓�琚嬩竴鐮佸崱鐗囬厤缃� */
-const cards = [
- {
- gradient: 'linear-gradient(135deg, #0ea5e9, #38bdf8)',
- icon: 'lucide:factory',
- name: '浜х嚎',
- routeName: 'MesProLine',
- value: () => props.summary.lineCount,
- },
- {
- gradient: 'linear-gradient(135deg, #f59e0b, #fbbf24)',
- icon: 'lucide:layers',
- name: '鐢熶骇鎵规',
- routeName: 'MesProBatch',
- value: () => props.summary.batchCount,
- },
- {
- gradient: 'linear-gradient(135deg, #ec4899, #f472b6)',
- icon: 'lucide:scan-barcode',
- name: '琚嬬爜',
- routeName: 'MesProBagCode',
- value: () => props.summary.bagCodeCount,
- },
- {
- gradient: 'linear-gradient(135deg, #14b8a6, #2dd4bf)',
- icon: 'lucide:grid-2x2',
- name: '鎵樼洏鐮�',
- routeName: 'MesProPallet',
- value: () => props.summary.palletCount,
- },
-];
-</script>
-
-<template>
- <Row :gutter="16">
- <Col v-for="card in cards" :key="card.name" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
- <Card
- class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg"
- @click="emit('navigate', card.routeName)"
- >
- <div class="flex items-center gap-4">
- <div
- class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
- :style="{ background: card.gradient }"
- >
- <IconifyIcon class="size-7" :icon="card.icon" />
- </div>
- <div>
- <div class="text-muted-foreground mb-1 text-sm">{{ card.name }}</div>
- <CountTo class="text-2xl font-bold leading-tight" :end-val="card.value()" :duration="1500" />
- </div>
- </div>
- </Card>
- </Col>
- </Row>
-</template>
diff --git a/src/views/dashboard/bagCodeHome/modules/basic-cards.vue b/src/views/dashboard/bagCodeHome/modules/basic-cards.vue
deleted file mode 100644
index 3ca866a..0000000
--- a/src/views/dashboard/bagCodeHome/modules/basic-cards.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-<script lang="ts" setup>
-import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
-
-import { CountTo } from '@vben/common-ui';
-import { IconifyIcon } from '@vben/icons';
-
-import { Card, Col, Row } from 'ant-design-vue';
-
-defineOptions({ name: 'BagCodeHomeBasicCards' });
-
-const props = defineProps<{
- summary: MesBagCodeHomeApi.Summary;
-}>();
-
-const emit = defineEmits<{
- navigate: [name: string];
-}>();
-
-/** 鍩虹鏁版嵁鍗$墖閰嶇疆 */
-const cards = [
- {
- gradient: 'linear-gradient(135deg, #409eff, #66b1ff)',
- icon: 'lucide:package',
- name: '鍝佺鎬绘暟',
- routeName: 'MdmItem',
- value: () => props.summary.mdmItemCount,
- },
- {
- gradient: 'linear-gradient(135deg, #67c23a, #85ce61)',
- icon: 'lucide:box',
- name: 'MES 鍝佺',
- routeName: 'MesMdItem',
- value: () => props.summary.mesItemCount,
- },
- {
- gradient: 'linear-gradient(135deg, #e6a23c, #ebb563)',
- icon: 'lucide:folder-tree',
- name: '鍝佺鍒嗙被',
- routeName: 'MesMdItemType',
- value: () => props.summary.itemTypeCount,
- },
- {
- gradient: 'linear-gradient(135deg, #7c3aed, #9461f5)',
- icon: 'lucide:ruler',
- name: '璁¢噺鍗曚綅',
- routeName: 'MdmUnit',
- value: () => props.summary.unitMeasureCount,
- },
-];
-</script>
-
-<template>
- <Row :gutter="16">
- <Col v-for="card in cards" :key="card.name" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
- <Card
- class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg"
- @click="emit('navigate', card.routeName)"
- >
- <div class="flex items-center gap-4">
- <div
- class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
- :style="{ background: card.gradient }"
- >
- <IconifyIcon class="size-7" :icon="card.icon" />
- </div>
- <div>
- <div class="text-muted-foreground mb-1 text-sm">{{ card.name }}</div>
- <CountTo class="text-2xl font-bold leading-tight" :end-val="card.value()" :duration="1500" />
- </div>
- </div>
- </Card>
- </Col>
- </Row>
-</template>
diff --git a/src/views/dashboard/bagCodeHome/modules/business-flow.vue b/src/views/dashboard/bagCodeHome/modules/business-flow.vue
new file mode 100644
index 0000000..595a1b9
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/business-flow.vue
@@ -0,0 +1,74 @@
+<script lang="ts" setup>
+import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
+
+import { computed } from 'vue';
+
+import { IconifyIcon } from '@vben/icons';
+
+import { Card } from 'ant-design-vue';
+
+defineOptions({ name: 'BagCodeHomeBusinessFlow' });
+
+const props = defineProps<{
+ summary: MesBagCodeHomeApi.Summary;
+ scanTotal: number; // 绱鎵爜娆℃暟
+ crossRegionTotal: number; // 绐滆揣棰勮鎬绘暟
+}>();
+
+const emit = defineEmits<{
+ navigate: [name: string];
+}>();
+
+/** 鏁板�煎厹搴曪細瀛楁缂哄け鏃舵寜 0 澶勭悊锛岄伩鍏� toLocaleString 鎶ラ敊 */
+const num = (v?: number) => v ?? 0;
+
+/** 鍏ㄩ摼璺笟鍔¤妭鐐癸細鍝佺 鈫� 浜х嚎 鈫� 鎵规 鈫� 琚嬬爜 鈫� 鎵樼洏 鈫� 鍑哄簱 鈫� 娴侀�氭壂鐮� 鈫� 闃茬獪婧簮锛堢繝缁夸富棰橈紝娴侀��/闃茬獪淇濈暀璇箟鑹诧級 */
+const nodes = computed(() => [
+ { color: '#10b981', count: num(props.summary.mdmItemCount), icon: 'lucide:package', label: '鍝佺', routeName: 'MdmItem' },
+ { color: '#34d399', count: num(props.summary.lineCount), icon: 'lucide:factory', label: '浜х嚎', routeName: 'MesProLine' },
+ { color: '#0d9488', count: num(props.summary.batchCount), icon: 'lucide:layers', label: '鐢熶骇鎵规', routeName: 'MesProBatch' },
+ { color: '#059669', count: num(props.summary.bagCodeCount), icon: 'lucide:scan-barcode', label: '琚嬬爜', routeName: 'MesProBagCode' },
+ { color: '#2dd4bf', count: num(props.summary.palletCount), icon: 'lucide:grid-2x2', label: '鎵樼洏鐮�', routeName: 'MesProPallet' },
+ { color: '#0f766e', count: num(props.summary.productSalesCount), icon: 'lucide:truck', label: '閿�鍞嚭搴�', routeName: 'MesWmProductSales' },
+ { color: '#f59e0b', count: num(props.scanTotal), icon: 'lucide:scan-line', label: '娴侀�氭壂鐮�', routeName: 'MesConsumerScanStatistics' },
+ { color: '#f43f5e', count: num(props.crossRegionTotal), icon: 'lucide:shield-alert', label: '闃茬獪/婧簮', routeName: 'MesTraceCrossRegion' },
+]);
+</script>
+
+<template>
+ <Card class="mb-4">
+ <template #title>
+ <span class="flex items-center gap-2">
+ <IconifyIcon class="size-5" icon="lucide:waypoints" />
+ 涓氬姟鍏ㄩ摼璺�
+ </span>
+ </template>
+ <!-- 浠庡乏鍒板彸涓茶仈姣忎釜涓氬姟鑺傜偣锛屼腑闂寸敤绠ご琛旀帴 -->
+ <div class="flex flex-col gap-3 lg:flex-row lg:items-stretch">
+ <template v-for="(node, index) in nodes" :key="node.label">
+ <div
+ class="flex min-w-0 flex-1 cursor-pointer items-center gap-3 rounded-xl border border-[rgba(16,185,129,0.22)] bg-[rgba(255,255,255,0.68)] p-3 backdrop-blur transition-all hover:-translate-y-0.5 hover:bg-[rgba(16,185,129,0.12)] hover:shadow-md"
+ @click="emit('navigate', node.routeName)"
+ >
+ <div
+ class="flex size-11 flex-shrink-0 items-center justify-center rounded-xl text-white"
+ :style="{ background: node.color }"
+ >
+ <IconifyIcon class="size-6" :icon="node.icon" />
+ </div>
+ <div class="flex min-w-0 flex-col gap-0.5">
+ <span class="text-muted-foreground truncate text-xs">{{ node.label }}</span>
+ <span class="text-lg font-bold leading-tight">{{ node.count.toLocaleString() }}</span>
+ </div>
+ </div>
+ <!-- 鑺傜偣闂寸澶达紙灏忓睆闅愯棌锛� -->
+ <div
+ v-if="index < nodes.length - 1"
+ class="hidden items-center lg:flex"
+ >
+ <IconifyIcon class="text-muted-foreground size-4" icon="lucide:chevron-right" />
+ </div>
+ </template>
+ </div>
+ </Card>
+</template>
diff --git a/src/views/dashboard/bagCodeHome/modules/kpi-cards.vue b/src/views/dashboard/bagCodeHome/modules/kpi-cards.vue
new file mode 100644
index 0000000..92b823a
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/kpi-cards.vue
@@ -0,0 +1,108 @@
+<script lang="ts" setup>
+import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
+
+import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
+
+import { gsap } from 'gsap';
+import { IconifyIcon } from '@vben/icons';
+
+import { Card, Col, Row } from 'ant-design-vue';
+
+defineOptions({ name: 'BagCodeHomeKpiCards' });
+
+const props = defineProps<{
+ summary: MesBagCodeHomeApi.Summary;
+ scanTotal: number; // 绱鎵爜娆℃暟
+}>();
+
+const emit = defineEmits<{
+ navigate: [name: string];
+}>();
+
+/** 鍏ㄩ摼璺� KPI 鍗$墖锛氳鐩栧熀纭�鏁版嵁/鐢熶骇/涓�琚嬩竴鐮�/鍑哄簱/娴侀��/浠撳偍/璐ㄩ噺锛堢繝缁夸富棰橈紝棰勮/涓嶅悎鏍间繚鐣欒涔夎壊锛� */
+const cards = computed(() => [
+ { gradient: 'linear-gradient(135deg, #10b981, #047857)', icon: 'lucide:package', name: '鍝佺鎬绘暟', routeName: 'MdmItem', sub: '鍩虹鏁版嵁', value: () => props.summary.mdmItemCount ?? 0 },
+ { gradient: 'linear-gradient(135deg, #34d399, #059669)', icon: 'lucide:layers', name: '鐢熶骇鎵规', routeName: 'MesProBatch', sub: '涓�琚嬩竴鐮�', value: () => props.summary.batchCount ?? 0 },
+ { gradient: 'linear-gradient(135deg, #059669, #047857)', icon: 'lucide:scan-barcode', name: '琚嬬爜', routeName: 'MesProBagCode', sub: '璧嬬爜', value: () => props.summary.bagCodeCount ?? 0 },
+ { gradient: 'linear-gradient(135deg, #6ee7b7, #10b981)', icon: 'lucide:grid-2x2', name: '鎵樼洏鐮�', routeName: 'MesProPallet', sub: '鎵樼洏', value: () => props.summary.palletCount ?? 0 },
+ { gradient: 'linear-gradient(135deg, #0d9488, #0f766e)', icon: 'lucide:truck', name: '閿�鍞嚭搴撳崟', routeName: 'MesWmProductSales', sub: '鍑哄簱娴侀��', value: () => props.summary.productSalesCount ?? 0 },
+ { gradient: 'linear-gradient(135deg, #10b981, #0d9488)', icon: 'lucide:scan-line', name: '绱鎵爜', routeName: 'MesConsumerScanStatistics', sub: '婧簮', value: () => props.scanTotal ?? 0 },
+ { gradient: 'linear-gradient(135deg, #f59e0b, #b45309)', icon: 'lucide:bell-alert', name: '搴撳瓨棰勮', routeName: 'MesWmStockWarning', sub: '浠撳偍', value: () => props.summary.stockWarningPendingCount ?? 0 },
+ { gradient: 'linear-gradient(135deg, #f43f5e, #e11d48)', icon: 'lucide:shield-x', name: '寰呰瘎瀹′笉鍚堟牸', routeName: 'MesQcNcr', sub: '璐ㄩ噺', value: () => props.summary.ncrPendingCount ?? 0 },
+]);
+
+/** 姣忎釜鍗$墖鐩爣鏁板瓧锛沝isplay 涓� GSAP 璁℃暟鍔ㄧ敾杩囩▼涓睍绀虹殑鏂囨湰 */
+const targets = computed(() => cards.value.map((card) => card.value()));
+const display = ref<string[]>(cards.value.map(() => '0'));
+
+let tweens: gsap.core.Tween[] = [];
+
+/** 鐢� GSAP 鎶婃暟瀛椾粠 0 婊氬姩鍒扮洰鏍囧�硷紙鍗冨垎浣嶆牸寮忓寲锛� */
+function animateTo(to: number[]) {
+ tweens.forEach((t) => t.kill());
+ tweens = [];
+ display.value = to.map(() => '0');
+ to.forEach((endVal, index) => {
+ const proxy = { v: 0 };
+ tweens.push(
+ gsap.to(proxy, {
+ duration: 1.2,
+ ease: 'power2.out',
+ onComplete: () => {
+ display.value[index] = endVal.toLocaleString();
+ },
+ onUpdate: () => {
+ display.value[index] = Math.round(proxy.v).toLocaleString();
+ },
+ v: endVal,
+ }),
+ );
+ });
+}
+
+onMounted(() => {
+ animateTo(targets.value);
+});
+
+// 姹囨�绘暟鎹姞杞藉悗锛屾暟瀛楃敱 0 婊氬姩鍒扮湡瀹炲��
+watch(targets, (val) => animateTo(val));
+
+onUnmounted(() => {
+ tweens.forEach((t) => t.kill());
+ tweens = [];
+});
+</script>
+
+<template>
+ <Row :gutter="16">
+ <Col
+ v-for="(card, index) in cards"
+ :key="card.name"
+ :lg="6"
+ :md="12"
+ :sm="12"
+ :xl="6"
+ :xs="24"
+ class="mb-4"
+ >
+ <Card
+ class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg"
+ @click="emit('navigate', card.routeName)"
+ >
+ <div class="flex items-center gap-4">
+ <div
+ class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white"
+ :style="{ background: card.gradient }"
+ >
+ <IconifyIcon class="size-7" :icon="card.icon" />
+ </div>
+ <div class="min-w-0">
+ <div class="text-muted-foreground mb-1 text-sm">{{ card.name }}</div>
+ <div class="text-2xl font-bold leading-tight">{{ display[index] }}</div>
+ <div class="text-muted-foreground mt-1 truncate text-xs">{{ card.sub }}</div>
+ </div>
+ </div>
+ </Card>
+ </Col>
+ </Row>
+</template>
diff --git a/src/views/dashboard/bagCodeHome/modules/scan-region-chart.vue b/src/views/dashboard/bagCodeHome/modules/scan-region-chart.vue
new file mode 100644
index 0000000..a9d8b57
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/scan-region-chart.vue
@@ -0,0 +1,36 @@
+<script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+
+import { onMounted, ref } from 'vue';
+
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
+
+import { Card } from 'ant-design-vue';
+
+import { getTraceConsumerScanDistribution } from '#/api/mes/trace-consumer-scan';
+
+import { getRegionBarChartOptions } from '../chart-options';
+
+defineOptions({ name: 'BagCodeHomeScanRegionChart' });
+
+const chartRef = ref<EchartsUIType>();
+const { renderEcharts } = useEcharts(chartRef);
+
+/** 鍔犺浇娑堣垂鑰呮壂鐮佸湴鍖猴紙鐪佷唤锛夊垎甯� */
+async function loadData() {
+ const data = await getTraceConsumerScanDistribution('province');
+ // 鍙� Top 10 鐪佷唤锛岄伩鍏嶆í鍚戣繃闀�
+ const top = data.slice(0, 10).map((d) => ({ count: d.count, name: d.name }));
+ await renderEcharts(getRegionBarChartOptions(top));
+}
+
+onMounted(() => {
+ loadData();
+});
+</script>
+
+<template>
+ <Card title="娑堣垂鑰呮壂鐮佸湴鍖哄垎甯�" class="h-full">
+ <EchartsUI ref="chartRef" class="h-[320px] w-full" />
+ </Card>
+</template>
diff --git a/src/views/dashboard/bagCodeHome/modules/shortcuts.vue b/src/views/dashboard/bagCodeHome/modules/shortcuts.vue
index bac404f..e257af2 100644
--- a/src/views/dashboard/bagCodeHome/modules/shortcuts.vue
+++ b/src/views/dashboard/bagCodeHome/modules/shortcuts.vue
@@ -9,12 +9,74 @@
const router = useRouter();
-/** 蹇嵎鍏ュ彛鍒楄〃锛屾寜涓氬姟鍩熺簿绠�涓� 4 涓牳蹇冨叆鍙� */
-const shortcuts = [
- { bgColor: '#409EFF', icon: 'lucide:package', name: '鍝佺绠$悊', routeName: 'MdmItem' },
- { bgColor: '#E6A23C', icon: 'lucide:folder-tree', name: '鍝佺鍒嗙被', routeName: 'MesMdItemType' },
- { bgColor: '#f59e0b', icon: 'lucide:layers', name: '鎵规鐢熶骇', routeName: 'MesProBatch' },
- { bgColor: '#ec4899', icon: 'lucide:scan-barcode', name: '璧嬬爜绠$悊', routeName: 'MesProBagCode' },
+interface Shortcut {
+ icon: string;
+ name: string;
+ routeName: string;
+}
+
+interface ShortcutGroup {
+ title: string;
+ items: Shortcut[];
+}
+
+/** 鎸変笟鍔″煙鍒嗙粍鐨勫揩鎹峰叆鍙� */
+const groups: ShortcutGroup[] = [
+ {
+ title: '鍩虹鏁版嵁',
+ items: [
+ { icon: 'lucide:package', name: '鍝佺绠$悊', routeName: 'MdmItem' },
+ { icon: 'lucide:folder-tree', name: '鍝佺鍒嗙被', routeName: 'MesMdItemType' },
+ { icon: 'lucide:ruler', name: '璁¢噺鍗曚綅', routeName: 'MdmUnit' },
+ ],
+ },
+ {
+ title: '涓�琚嬩竴鐮佺敓浜�',
+ items: [
+ { icon: 'lucide:factory', name: '浜х嚎缁存姢', routeName: 'MesProLine' },
+ { icon: 'lucide:layers', name: '鎵规鐢熶骇', routeName: 'MesProBatch' },
+ { icon: 'lucide:scan-barcode', name: '璧嬬爜绠$悊', routeName: 'MesProBagCode' },
+ { icon: 'lucide:grid-2x2', name: '鎵樼洏鐮�', routeName: 'MesProPallet' },
+ { icon: 'lucide:scan-line', name: '鎵爜鍏ュ簱', routeName: 'MesProScanIn' },
+ ],
+ },
+ {
+ title: '鍑哄簱娴侀��',
+ items: [
+ { icon: 'lucide:send', name: '鍙戣揣閫氱煡', routeName: 'MesWmSalesNotice' },
+ { icon: 'lucide:truck', name: '閿�鍞嚭搴�', routeName: 'MesWmProductSales' },
+ { icon: 'lucide:rotate-ccw', name: '閿�鍞��璐�', routeName: 'MesWmReturnSales' },
+ ],
+ },
+ {
+ title: '闃蹭吉闃茬獪璐�',
+ items: [
+ { icon: 'lucide:shield-alert', name: '绐滆揣棰勮', routeName: 'MesTraceCrossRegion' },
+ { icon: 'lucide:map-pin', name: '鎵爜鍦扮悊鍒嗗竷', routeName: 'MesConsumerScanDistribution' },
+ { icon: 'lucide:database-check', name: '瀹屾暣鎬ф牎楠�', routeName: 'MesTraceIntegrity' },
+ ],
+ },
+ {
+ title: '婧簮鍒嗘瀽',
+ items: [{ icon: 'lucide:scan-line', name: '娑堣垂鑰呮壂鐮佺粺璁�', routeName: 'MesConsumerScanStatistics' }],
+ },
+ {
+ title: '浠撳偍鐗╂祦',
+ items: [
+ { icon: 'lucide:warehouse', name: '浠撳簱璁剧疆', routeName: 'MesWmWarehouse' },
+ { icon: 'lucide:boxes', name: '搴撳瓨鐜版湁閲�', routeName: 'MesWmMaterialStock' },
+ { icon: 'lucide:bell-alert', name: '搴撳瓨棰勮', routeName: 'MesWmStockWarning' },
+ { icon: 'lucide:move-right', name: '杞Щ璋冩嫧', routeName: 'MesWmTransfer' },
+ ],
+ },
+ {
+ title: '璐ㄩ噺绠$悊',
+ items: [
+ { icon: 'lucide:list-tree', name: '鎵规杩芥函', routeName: 'BatchTrace' },
+ { icon: 'lucide:shield-x', name: '涓嶅悎鏍煎搧', routeName: 'MesQcNcr' },
+ { icon: 'lucide:bean', name: '绉嶅瓙璐ㄦ', routeName: 'MesQcSeedQuality' },
+ ],
+ },
];
/** 璺宠浆鍒扮洰鏍囬〉闈紙鎸夎矾鐢� name锛屾湭鍔犺浇鏃舵彁绀猴級 */
@@ -30,18 +92,20 @@
<template>
<Card title="蹇嵎鍏ュ彛" class="h-full">
<Row :gutter="16">
- <Col v-for="item in shortcuts" :key="item.name" :lg="6" :md="8" :sm="12" :xs="12" class="mb-4">
- <div
- class="hover:bg-accent flex cursor-pointer flex-col items-center gap-2 rounded-lg py-3 transition-all hover:-translate-y-0.5"
- @click="handleNavigate(item.routeName)"
- >
- <div
- class="flex size-12 items-center justify-center rounded-xl text-white"
- :style="{ background: item.bgColor }"
- >
- <IconifyIcon class="size-6" :icon="item.icon" />
+ <Col v-for="group in groups" :key="group.title" :lg="8" :md="12" :sm="24" :xs="24" class="mb-4">
+ <div class="rounded-xl border border-[rgba(16,185,129,0.16)] bg-[rgba(255,255,255,0.55)] p-3 backdrop-blur">
+ <div class="text-muted-foreground mb-2 text-xs font-medium">{{ group.title }}</div>
+ <div class="flex flex-wrap gap-2">
+ <div
+ v-for="item in group.items"
+ :key="item.name"
+ class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-[rgba(16,185,129,0.22)] px-2 py-1.5 text-sm transition-colors hover:bg-[rgba(16,185,129,0.12)]"
+ @click="handleNavigate(item.routeName)"
+ >
+ <IconifyIcon class="size-4 text-[#0d9488]" :icon="item.icon" />
+ <span>{{ item.name }}</span>
+ </div>
</div>
- <span class="text-sm">{{ item.name }}</span>
</div>
</Col>
</Row>
diff --git a/src/views/erp/sale/order/index.vue b/src/views/erp/sale/order/index.vue
index 7071c8e..ac02eb7 100644
--- a/src/views/erp/sale/order/index.vue
+++ b/src/views/erp/sale/order/index.vue
@@ -19,12 +19,10 @@
getSaleOrderPage,
updateSaleOrderStatus,
} from '#/api/erp/sale/order';
-import { generateFromSaleOrder } from '#/api/mes/wm/salesnotice';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
-import SalesNoticeForm from '#/views/wls/salesnotice/modules/form.vue';
/** ERP 閿�鍞鍗曞垪琛� */
defineOptions({ name: 'ErpSaleOrder' });
@@ -34,11 +32,6 @@
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
- destroyOnClose: true,
-});
-
-const [SalesNoticeFormModal, salesNoticeFormModalApi] = useVbenModal({
- connectedComponent: SalesNoticeForm,
destroyOnClose: true,
});
@@ -149,21 +142,6 @@
}
}
-/** 鐢熸垚鍙戣揣閫氱煡鍗� */
-async function handleGenerateNotice(row: ErpSaleOrderApi.SaleOrder) {
- try {
- const noticeId = await generateFromSaleOrder(row.id!);
- message.success('鐢熸垚鍙戣揣閫氱煡鍗曟垚鍔�');
- handleRefresh();
- // 鐩存帴鎵撳紑鍙戣揣閫氱煡鍗曡鎯呭脊妗�
- salesNoticeFormModalApi
- .setData({ formType: 'detail', id: noticeId })
- .open();
- } catch (e) {
- console.error(e);
- }
-}
-
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
@@ -219,7 +197,6 @@
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
- <SalesNoticeFormModal />
<Grid table-title="閿�鍞鍗曞垪琛�">
<template #toolbar-tools>
<TableAction
@@ -354,14 +331,6 @@
title: `纭鍚敤${row.no}鍚楋紵`,
confirm: handleEnable.bind(null, row),
},
- },
- {
- label: '鐢熸垚鍙戣揣閫氱煡鍗�',
- type: 'link',
- icon: 'ant-design:file-protect-outlined',
- auth: ['mes:wm-sales-notice:create'],
- ifShow: () => row.status === 20 && row.canCreateSalesNotice,
- onClick: handleGenerateNotice.bind(null, row),
},
{
label: $t('common.delete'),
diff --git a/src/views/mes/pro/scan-in/index.vue b/src/views/mes/pro/scan-in/index.vue
index 7042afc..3a5d2b9 100644
--- a/src/views/mes/pro/scan-in/index.vue
+++ b/src/views/mes/pro/scan-in/index.vue
@@ -6,32 +6,76 @@
import { gsap } from 'gsap';
import {
- getLatestScanEvent,
getScanDashboard,
getScanEventPage,
inboundScanEvent,
type MesProScanEventApi,
} from '#/api/mes/pro/scanEvent';
+import { getLineSimpleList, type MesProLineApi } from '#/api/mes/pro/line';
+import {
+ getWarehouseLocationSimpleList,
+ type MesWmWarehouseLocationApi,
+} from '#/api/mes/wm/warehouse/location';
+import {
+ getWarehouseAreaSimpleList,
+ type MesWmWarehouseAreaApi,
+} from '#/api/mes/wm/warehouse/area';
interface ScanRecord extends MesProScanEventApi.ScanEvent {
key: number;
}
const code = ref('');
-const deviceCode = ref('');
+const scanType = ref<MesProScanEventApi.ScanType>('BAG');
const lineCode = ref('');
const locationId = ref<number>();
const areaId = ref<number>();
+const lines = ref<MesProLineApi.Line[]>([]);
+const locations = ref<MesWmWarehouseLocationApi.WarehouseLocation[]>([]);
+const areas = ref<MesWmWarehouseAreaApi.WarehouseArea[]>([]);
const loading = ref(false);
const refreshing = ref(false);
const records = ref<ScanRecord[]>([]);
const dashboard = ref<MesProScanEventApi.Dashboard>({});
let timer: ReturnType<typeof setInterval> | undefined;
+/** 鎵爜绫诲瀷涓枃鍚� */
+const SCAN_TYPE_LABEL: Record<MesProScanEventApi.ScanType, string> = {
+ BAG: '琚嬬爜',
+ PALLET: '鎵樼洏鐮�',
+ BATCH: '鎵规鐮�',
+};
+/** 鎵爜绫诲瀷涓嬫媺閫夐」 */
+const SCAN_TYPE_OPTIONS = [
+ { label: '琚嬬爜', value: 'BAG' },
+ { label: '鎵樼洏鐮�', value: 'PALLET' },
+ { label: '鎵规鐮�', value: 'BATCH' },
+] as const;
+
const latest = computed(() => dashboard.value.latest);
const successCount = computed(() => dashboard.value.success ?? records.value.filter((item) => item.status === 'SUCCESS').length);
const failedCount = computed(() => dashboard.value.failed ?? records.value.filter((item) => item.status === 'FAILED').length);
-const isReady = computed(() => Boolean(deviceCode.value.trim() && locationId.value && areaId.value));
+const isReady = computed(() => Boolean(locationId.value && areaId.value));
+const lineOptions = computed(() =>
+ lines.value.map((item) => ({
+ label: item.code ? `${item.code} ${item.name ?? ''}`.trim() : (item.name ?? ''),
+ value: item.code ?? '',
+ })),
+);
+const locationOptions = computed(() =>
+ locations.value.map((item) => ({
+ label: item.code ? `${item.code} ${item.name ?? ''}`.trim() : (item.name ?? ''),
+ value: item.id ?? 0,
+ })),
+);
+const areaOptions = computed(() =>
+ areas.value.map((item) => ({
+ label: item.code ? `${item.code} ${item.name ?? ''}`.trim() : (item.name ?? ''),
+ value: item.id ?? 0,
+ })),
+);
+const locationName = computed(() => locations.value.find((item) => item.id === locationId.value)?.name ?? '');
+const areaName = computed(() => areas.value.find((item) => item.id === areaId.value)?.name ?? '');
const pageRoot = ref<HTMLElement>();
let animationContext: gsap.Context | undefined;
let feedbackTimeline: gsap.core.Timeline | undefined;
@@ -41,7 +85,7 @@
try {
const [summary, page] = await Promise.all([
getScanDashboard(),
- getScanEventPage({ pageNo: 1, pageSize: 20, deviceCode: deviceCode.value.trim() || undefined }),
+ getScanEventPage({ pageNo: 1, pageSize: 20 }),
]);
dashboard.value = summary;
records.value = (page.list ?? []).map((item, index) => ({ ...item, key: item.id ?? index }));
@@ -50,28 +94,57 @@
}
}
+async function loadOptions() {
+ try {
+ const [lineList, locationList] = await Promise.all([
+ getLineSimpleList(),
+ getWarehouseLocationSimpleList(),
+ ]);
+ lines.value = lineList ?? [];
+ locations.value = locationList ?? [];
+ } catch {
+ lines.value = [];
+ locations.value = [];
+ }
+}
+
+async function loadAreas() {
+ areas.value = [];
+ areaId.value = undefined;
+ if (!locationId.value) return;
+ try {
+ areas.value = (await getWarehouseAreaSimpleList(locationId.value)) ?? [];
+ } catch {
+ areas.value = [];
+ }
+}
+
+function onScanTypeChange() {
+ code.value = '';
+}
+
async function submitScan() {
const bagCode = code.value.trim();
- if (!bagCode || !deviceCode.value.trim()) {
- message.warning('璇峰厛濉啓璁惧缂栫爜锛屽啀鎵弿琚嬬爜');
+ if (!bagCode) {
+ message.warning('璇峰厛鎵弿杩芥函鐮�');
return;
}
if (!locationId.value || !areaId.value) {
- message.warning('璇峰厛閰嶇疆 WIP 鏆傚瓨搴撳尯鍜屽簱浣�');
+ message.warning('璇峰厛閫夋嫨 WIP 鏆傚瓨搴撳尯鍜屽簱浣�');
return;
}
loading.value = true;
try {
await inboundScanEvent({
- eventId: `${deviceCode.value.trim()}-${Date.now()}`,
+ eventId: `${scanType.value}-${Date.now()}`,
bagCode,
+ scanType: scanType.value,
quantity: 1,
- deviceCode: deviceCode.value.trim(),
- lineCode: lineCode.value.trim() || undefined,
+ lineCode: lineCode.value || undefined,
locationId: locationId.value,
areaId: areaId.value,
});
- message.success(`琚嬬爜 ${bagCode} 宸插畬鎴愬叆搴揱);
+ message.success(`${SCAN_TYPE_LABEL[scanType.value]} ${bagCode} 宸插畬鎴愬叆搴揱);
if (animationContext && pageRoot.value && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
feedbackTimeline?.kill();
feedbackTimeline = gsap.timeline();
@@ -84,8 +157,23 @@
code.value = '';
await refresh();
} catch (error) {
- const response = error as { message?: string; msg?: string };
- message.error(response.message ?? response.msg ?? '鎵爜鍏ュ簱澶辫触锛岃鏌ョ湅浜嬩欢璁板綍');
+ const response = error as {
+ message?: string;
+ msg?: string;
+ response?: { data?: { message?: string; msg?: string } };
+ };
+ const errorMessage =
+ response.response?.data?.message ??
+ response.response?.data?.msg ??
+ response.message ??
+ response.msg;
+ if (errorMessage && /鍏ュ簱浜嬪姟鏁伴噺蹇呴』涓烘鏁皘鏁伴噺.*涓� 0|quantity.*positive/i.test(errorMessage)) {
+ message.error('鍏ュ簱鏁伴噺涓嶈兘涓� 0锛岃妫�鏌ヨ鎵规鐨勮鏁伴厤缃�');
+ } else if (!errorMessage || /UnexpectedRollbackException|绯荤粺寮傚父|Internal Server Error/i.test(errorMessage)) {
+ message.error('鎵爜鍏ュ簱澶辫触锛岃妫�鏌ユ壒娆℃暟閲忓拰搴撲綅閰嶇疆');
+ } else {
+ message.error(errorMessage);
+ }
await refresh();
} finally {
loading.value = false;
@@ -93,7 +181,6 @@
}
async function refreshLatest() {
- if (deviceCode.value.trim()) await getLatestScanEvent(deviceCode.value.trim());
await refresh();
}
@@ -128,6 +215,7 @@
}
}, pageRoot.value);
}
+ await loadOptions();
await refresh();
timer = setInterval(refreshLatest, 15000);
});
@@ -157,30 +245,32 @@
<div class="metric-card"><div class="metric-icon blue"><IconifyIcon icon="ant-design:scan-outlined" /></div><div><span>绱鎵爜浜嬩欢</span><strong>{{ dashboard.total ?? 0 }}</strong><small>鎸佺画鎺ユ敹</small></div></div>
<div class="metric-card"><div class="metric-icon green"><IconifyIcon icon="ant-design:check-circle-outlined" /></div><div><span>鎴愬姛鍏ュ簱</span><strong>{{ successCount }}</strong><small>宸插啓鍏ュ簱瀛樹簨鍔�</small></div></div>
<div class="metric-card"><div class="metric-icon red"><IconifyIcon icon="ant-design:warning-outlined" /></div><div><span>澶辫触浜嬩欢</span><strong>{{ failedCount }}</strong><small>闇�鍏虫敞寮傚父璁板綍</small></div></div>
- <div class="metric-card"><div class="metric-icon orange"><IconifyIcon icon="ant-design:inbox-outlined" /></div><div><span>鏈�杩戝叆搴撹鐮�</span><strong class="text-value">{{ latest?.bagCode ?? '-' }}</strong><small>{{ latest?.eventTime ?? '鏆傛棤璁板綍' }}</small></div></div>
+ <div class="metric-card"><div class="metric-icon orange"><IconifyIcon icon="ant-design:inbox-outlined" /></div><div><span>鏈�杩戝叆搴撹拷婧爜</span><strong class="text-value">{{ latest?.bagCode ?? '-' }}</strong><small>{{ latest?.eventTime ?? '鏆傛棤璁板綍' }}</small></div></div>
</section>
<section class="main-grid">
<a-card class="scan-card" :bordered="false">
<template #title><div class="card-title"><IconifyIcon icon="ant-design:scan-outlined" />鎵爜鍏ュ簱 <a-tag color="blue">鑷姩妯″紡</a-tag></div></template>
<div class="scan-panel">
- <div class="scan-intro"><div class="scan-ring"><IconifyIcon icon="ant-design:scan-outlined" /></div><div><h2>绛夊緟鎵爜</h2><p>灏嗚鐮佸鍑嗗伐涓氱浉鏈猴紝鎴栦娇鐢ㄦ壂鐮佹灙鎵弿鍚庡洖杞︾‘璁�</p></div></div>
- <a-input-search v-model:value="code" class="scan-input" size="large" enter-button="纭鍏ュ簱" :loading="loading" :disabled="loading" placeholder="璇锋壂鎻忚鐮侊紝鎵爜鏋洖杞﹀悗鑷姩鍏ュ簱" @search="submitScan" />
- <div class="scan-hint"><IconifyIcon icon="ant-design:clock-circle-outlined" /> 鏈嶅姟绔褰曞噯纭叆搴撴椂闂� <span>路</span> <IconifyIcon icon="ant-design:inbox-outlined" /> 姣忚鏁伴噺鎸� 1 璁�</div>
+ <div class="scan-intro"><div class="scan-ring"><IconifyIcon icon="ant-design:scan-outlined" /></div><div><h2>绛夊緟鎵爜</h2><p>閫夋嫨杩芥函鐮佺被鍨嬪悗鎵爜锛屾垨浣跨敤鎵爜鏋壂鎻忓悗鍥炶溅纭</p></div></div>
+ <div class="scan-row">
+ <a-select v-model:value="scanType" class="scan-type" :options="SCAN_TYPE_OPTIONS" size="large" @change="onScanTypeChange" />
+ <a-input-search v-model:value="code" class="scan-input" size="large" enter-button="纭鍏ュ簱" :loading="loading" :disabled="loading" placeholder="璇锋壂鎻忚拷婧爜锛屾壂鐮佹灙鍥炶溅鍚庤嚜鍔ㄥ叆搴�" @search="submitScan" />
+ </div>
+ <div class="scan-hint"><IconifyIcon icon="ant-design:clock-circle-outlined" /> 鏈嶅姟绔褰曞噯纭叆搴撴椂闂� <span>路</span> <IconifyIcon icon="ant-design:inbox-outlined" /> 琚嬬爜鎸� 1 琚嬨�佹墭鐩樻寜宸茶琚嬫暟銆佹壒娆℃寜鎵规琚嬫暟鍏ュ簱</div>
</div>
<a-divider />
<a-form layout="vertical" class="config-form">
- <a-form-item label="璁惧缂栫爜" required><a-input v-model:value="deviceCode" prefix="璁惧" placeholder="渚嬪锛欳AM-LINE-01" /></a-form-item>
- <a-form-item label="浜х嚎缂栫爜"><a-input v-model:value="lineCode" placeholder="渚嬪锛歀INE-A" /></a-form-item>
- <a-form-item label="WIP 鏆傚瓨搴撳尯" required><a-input-number v-model:value="locationId" class="full-width" :min="1" placeholder="搴撳尯缂栧彿" /></a-form-item>
- <a-form-item label="WIP 鏆傚瓨搴撲綅" required><a-input-number v-model:value="areaId" class="full-width" :min="1" placeholder="搴撲綅缂栧彿" /></a-form-item>
+ <a-form-item label="鐢熶骇浜х嚎"><a-select v-model:value="lineCode" :options="lineOptions" allow-clear show-search option-filter-prop="label" placeholder="璇烽�夋嫨浜х嚎" /></a-form-item>
+ <a-form-item label="WIP 鏆傚瓨搴撳尯" required><a-select v-model:value="locationId" :options="locationOptions" show-search option-filter-prop="label" placeholder="璇烽�夋嫨搴撳尯" @change="loadAreas" /></a-form-item>
+ <a-form-item label="WIP 鏆傚瓨搴撲綅" required><a-select v-model:value="areaId" :options="areaOptions" show-search option-filter-prop="label" :disabled="!locationId" placeholder="璇烽�夋嫨搴撲綅" /></a-form-item>
</a-form>
</a-card>
<a-card class="status-card" :bordered="false">
<template #title><div class="card-title"><ApiOutlined />閲囬泦璁惧鐘舵��</div></template>
- <div class="device-state"><span class="online-badge"><span class="status-dot" aria-hidden="true" /><span>{{ isReady ? '璁惧宸插氨缁�' : '绛夊緟閰嶇疆' }}</span></span><span class="muted">15 绉掕嚜鍔ㄥ埛鏂�</span></div>
- <div class="status-list"><div><span>褰撳墠璁惧</span><strong>{{ deviceCode || '鏈厤缃�' }}</strong></div><div><span>鐢熶骇浜х嚎</span><strong>{{ lineCode || '鏈厤缃�' }}</strong></div><div><span>鏆傚瓨浣嶇疆</span><strong>{{ locationId && areaId ? `${locationId} 鍖� / ${areaId} 浣峘 : '鏈厤缃�' }}</strong></div><div><span>鏈�杩戝叆搴撴椂闂�</span><strong>{{ latest?.eventTime || '-' }}</strong></div><div><span>鏈�杩戠敓浜ф壒娆�</span><strong>{{ latest?.batchCode || '-' }}</strong></div></div>
+ <div class="device-state"><span class="online-badge"><span class="status-dot" aria-hidden="true" /><span>{{ isReady ? '浣嶇疆宸插氨缁�' : '绛夊緟閰嶇疆' }}</span></span><span class="muted">15 绉掕嚜鍔ㄥ埛鏂�</span></div>
+ <div class="status-list"><div><span>鐢熶骇浜х嚎</span><strong>{{ lineCode || '鏈厤缃�' }}</strong></div><div><span>鏆傚瓨浣嶇疆</span><strong>{{ locationId && areaId ? `${locationName || locationId} / ${areaName || areaId}` : '鏈厤缃�' }}</strong></div><div><span>鏈�杩戝叆搴撴椂闂�</span><strong>{{ latest?.eventTime || '-' }}</strong></div><div><span>鏈�杩戠敓浜ф壒娆�</span><strong>{{ latest?.batchCode || '-' }}</strong></div><div><span>鏈�杩戞壂鐮佺被鍨�</span><strong>{{ latest?.scanType ? SCAN_TYPE_LABEL[latest.scanType] : '-' }}</strong></div></div>
<div class="sync-box"><IconifyIcon icon="ant-design:check-circle-outlined" /><div><strong>浠撳偍绯荤粺鍚屾姝e父</strong><span>鎵爜浜嬩欢涓庡簱瀛樹簨鍔″疄鏃跺啓鍏�</span></div></div>
</a-card>
</section>
@@ -188,7 +278,7 @@
<a-card class="records-card" :bordered="false">
<template #title><div class="card-title"><IconifyIcon icon="ant-design:clock-circle-outlined" />鏈�杩戞壂鐮佷簨浠� <span class="record-count">鏈�杩� 20 鏉�</span></div></template>
<a-table :data-source="records" :pagination="false" :scroll="{ x: 1050 }" row-key="key" size="middle">
- <a-table-column key="bagCode" title="琚嬬爜" data-index="bagCode" /><a-table-column key="batchCode" title="鐢熶骇鎵规" data-index="batchCode" /><a-table-column key="deviceCode" title="璁惧 / 浜х嚎"><template #default="{ record }">{{ record.deviceCode || '-' }}<span class="sub-text">{{ record.lineCode || '-' }}</span></template></a-table-column>
+ <a-table-column key="bagCode" title="杩芥函鐮�" data-index="bagCode" /><a-table-column key="scanType" title="绫诲瀷"><template #default="{ record }">{{ record.scanType ? SCAN_TYPE_LABEL[record.scanType as MesProScanEventApi.ScanType] : '琚嬬爜' }}</template></a-table-column><a-table-column key="batchCode" title="鐢熶骇鎵规" data-index="batchCode" /><a-table-column key="lineCode" title="浜х嚎"><template #default="{ record }">{{ record.lineCode || '-' }}</template></a-table-column>
<a-table-column key="status" title="鐘舵��"><template #default="{ record }"><a-tag :color="record.status === 'SUCCESS' ? 'success' : 'error'">{{ record.status === 'SUCCESS' ? '鎴愬姛鍏ュ簱' : '澶辫触' }}</a-tag></template></a-table-column><a-table-column key="errorCode" title="閿欒鐮�" data-index="errorCode" /><a-table-column key="errorMessage" title="澶辫触鍘熷洜" data-index="errorMessage" /><a-table-column key="eventTime" title="澶勭悊鏃堕棿" data-index="eventTime" /><a-table-column key="transactionId" title="搴撳瓨浜嬪姟" data-index="transactionId" />
</a-table>
</a-card>
@@ -203,8 +293,8 @@
.header-status, .device-state { display: flex; align-items: center; gap: 10px; color: #16835b; font-size: 13px; font-weight: 600; }.header-status :deep(.ant-btn) { color: #607089; }
.status-dot { display: inline-block; flex: 0 0 8px; width: 8px; height: 8px; margin: 0 2px 0 1px; border-radius: 50%; background: #20b878; box-shadow: 0 0 0 4px #d9f7e9; transform-origin: center; }
.metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 18px; }.metric-card { display: flex; align-items: flex-start; gap: 16px; padding: 18px 20px; background: #fff; border: 1px solid #e8edf5; border-radius: 10px; box-shadow: 0 3px 12px #243b5310; }.metric-card > div:last-child { min-width: 0; padding-top: 1px; }.metric-icon { flex: 0 0 42px; display: grid; place-items: center; width: 42px; height: 42px; margin-top: 1px; border-radius: 9px; font-size: 20px; }.metric-icon.blue { color: #1677ff; background: #eaf3ff; }.metric-icon.green { color: #17a673; background: #e6f8f0; }.metric-icon.red { color: #e05252; background: #fff0f0; }.metric-icon.orange { color: #d58a17; background: #fff6df; }.metric-card span, .metric-card small { display: block; color: #7b8799; font-size: 12px; }.metric-card strong { display: block; margin: 4px 0; font-size: 25px; line-height: 1.1; }.metric-card .text-value { overflow: hidden; max-width: 180px; text-overflow: ellipsis; white-space: nowrap; font-size: 18px; }
-.main-grid { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(320px, .8fr); gap: 18px; margin-bottom: 18px; }.scan-card, .status-card, .records-card { border-radius: 10px; box-shadow: 0 3px 12px #243b5310; }.card-title { display: flex; align-items: center; gap: 10px; min-height: 24px; font-weight: 700; }.card-title :deep(.iconify) { flex: 0 0 auto; margin-top: 1px; }.card-title :deep(.anticon) { color: #1677ff; }.card-title .ant-tag { margin-left: 4px; font-weight: 500; }.scan-panel { padding: 8px 0 4px; }.scan-intro { display: flex; align-items: center; gap: 16px; margin: 4px 0 22px; }.scan-ring { display: grid; place-items: center; width: 58px; height: 58px; margin: 4px 2px 4px 4px; border: 1px solid #b9d5ff; border-radius: 50%; color: #1677ff; background: #f1f7ff; font-size: 25px; }.scan-intro h2 { margin: 0 0 7px; font-size: 19px; line-height: 1.35; }.scan-intro p { margin: 0; color: #7b8799; font-size: 13px; }.scan-input :deep(input) { height: 50px; font-size: 16px; }.scan-input :deep(.ant-input-group-addon .ant-btn) { height: 50px; font-weight: 600; }.scan-hint { display: flex; align-items: center; gap: 8px; margin-top: 15px; padding-left: 2px; color: #8793a5; font-size: 12px; line-height: 1.5; }.scan-hint span { margin: 0 2px; color: #b7c0cd; }.config-form { display: grid; grid-template-columns: repeat(2, 1fr); column-gap: 18px; }.config-form :deep(.ant-form-item) { margin-bottom: 14px; }.full-width { width: 100%; }
+.main-grid { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(320px, .8fr); gap: 18px; margin-bottom: 18px; }.scan-card, .status-card, .records-card { border-radius: 10px; box-shadow: 0 3px 12px #243b5310; }.card-title { display: flex; align-items: center; gap: 10px; min-height: 24px; font-weight: 700; }.card-title :deep(.iconify) { flex: 0 0 auto; margin-top: 1px; }.card-title :deep(.anticon) { color: #1677ff; }.card-title .ant-tag { margin-left: 4px; font-weight: 500; }.scan-panel { padding: 8px 0 4px; }.scan-intro { display: flex; align-items: center; gap: 16px; margin: 4px 0 22px; }.scan-ring { display: grid; place-items: center; width: 58px; height: 58px; margin: 4px 2px 4px 4px; border: 1px solid #b9d5ff; border-radius: 50%; color: #1677ff; background: #f1f7ff; font-size: 25px; }.scan-intro h2 { margin: 0 0 7px; font-size: 19px; line-height: 1.35; }.scan-intro p { margin: 0; color: #7b8799; font-size: 13px; }.scan-row { display: flex; align-items: stretch; gap: 10px; }.scan-type { flex: 0 0 150px; }.scan-type :deep(.ant-select-selector) { height: 50px !important; }.scan-type :deep(.ant-select-selection-item) { line-height: 48px !important; }.scan-input { flex: 1 1 auto; min-width: 0; }.scan-input :deep(input) { height: 50px; font-size: 16px; }.scan-input :deep(.ant-input-group-addon .ant-btn) { height: 50px; font-weight: 600; }.scan-hint { display: flex; align-items: center; gap: 8px; margin-top: 15px; padding-left: 2px; color: #8793a5; font-size: 12px; line-height: 1.5; }.scan-hint span { margin: 0 2px; color: #b7c0cd; }.config-form { display: grid; grid-template-columns: repeat(2, 1fr); column-gap: 18px; }.config-form :deep(.ant-form-item) { margin-bottom: 14px; }.full-width { width: 100%; }
.device-state { justify-content: space-between; margin-bottom: 20px; }.online-badge { display: inline-flex; align-items: center; gap: 10px; min-height: 24px; color: #16835b; white-space: nowrap; }.muted, .sub-text { color: #8793a5; font-size: 12px; }.status-list { border-top: 1px solid #edf0f5; }.status-list div { display: flex; justify-content: space-between; gap: 12px; padding: 13px 0; border-bottom: 1px solid #edf0f5; font-size: 13px; }.status-list span { color: #7b8799; }.status-list strong { max-width: 190px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; }.sync-box { display: flex; gap: 10px; margin-top: 20px; padding: 12px; border-radius: 7px; color: #16835b; background: #effaf5; }.sync-box div { display: flex; flex-direction: column; gap: 3px; }.sync-box span { color: #5f8b76; font-size: 12px; }.record-count { margin-left: 4px; color: #9aa5b5; font-size: 12px; font-weight: 400; }.sub-text { display: block; margin-top: 3px; }
@media (max-width: 900px) { .metric-grid { grid-template-columns: repeat(2, 1fr); }.main-grid { grid-template-columns: 1fr; } }
-@media (max-width: 600px) { .scan-page { padding: 14px; }.page-header { display: block; }.header-status { margin-top: 14px; }.metric-grid { grid-template-columns: 1fr; }.config-form { grid-template-columns: 1fr; } h1 { font-size: 23px; } }
+@media (max-width: 600px) { .scan-page { padding: 14px; }.page-header { display: block; }.header-status { margin-top: 14px; }.metric-grid { grid-template-columns: 1fr; }.config-form { grid-template-columns: 1fr; }.scan-row { flex-wrap: wrap; }.scan-type { flex-basis: 100%; } h1 { font-size: 23px; } }
</style>
diff --git a/src/views/mes/qc/batchtrace/modules/TraceDecision.vue b/src/views/mes/qc/batchtrace/modules/TraceDecision.vue
new file mode 100644
index 0000000..6d21ae1
--- /dev/null
+++ b/src/views/mes/qc/batchtrace/modules/TraceDecision.vue
@@ -0,0 +1,46 @@
+<script lang="ts" setup>
+import { computed } from 'vue';
+import type { MesTraceBatchApi } from '#/api/mes/trace/batch';
+
+const props = defineProps<{ detail: MesTraceBatchApi.Detail }>();
+const decision = computed(() => props.detail.decision);
+const qualityRisk = computed(() => decision.value?.qualityRisk ?? props.detail.quality?.result === 2);
+const riskLevel = computed(() => decision.value?.riskLevel || (qualityRisk.value ? 'HIGH' : 'NORMAL'));
+const riskText = computed(() => qualityRisk.value ? '楂橀闄�' : riskLevel.value === 'NORMAL' ? '姝e父' : riskLevel.value);
+</script>
+
+<template>
+ <div class="grid gap-4 p-4 md:grid-cols-3">
+ <div class="rounded border p-4">
+ <div class="text-muted-foreground text-sm">璐ㄩ噺鍐崇瓥</div>
+ <div class="mt-2 text-lg font-semibold" :class="qualityRisk ? 'text-red-600' : 'text-green-600'">
+ {{ detail.quality?.resultName || '寰呮楠�' }}
+ </div>
+ <div class="text-muted-foreground mt-2 text-sm">{{ detail.quality?.reason || '鏆傛棤寮傚父璇存槑' }}</div>
+ </div>
+ <div class="rounded border p-4">
+ <div class="text-muted-foreground text-sm">搴撳瓨鍐崇瓥</div>
+ <div class="mt-2 text-lg font-semibold">鍏ュ簱 {{ decision?.inboundQuantity ?? detail.inbound?.quantity ?? 0 }}</div>
+ <div class="text-muted-foreground mt-2 text-sm">宸插缓绔嬮攢鍞幓鍚� {{ decision?.salesTargetCount ?? detail.salesTargets?.length ?? 0 }} 鏉�</div>
+ </div>
+ <div class="rounded border p-4">
+ <div class="text-muted-foreground text-sm">闃蹭吉鍐崇瓥</div>
+ <div class="mt-2 text-lg font-semibold">{{ detail.antiFake?.scanCount ?? 0 }} 娆�</div>
+ <div class="text-muted-foreground mt-2 text-sm">娑堣垂鑰呴獙璇佹鏁�</div>
+ </div>
+ </div>
+ <div class="mx-4 mb-4 rounded border p-4" :class="qualityRisk ? 'border-red-200 bg-red-50' : 'border-green-200 bg-green-50'">
+ <div class="flex items-center justify-between">
+ <div class="font-medium">缁煎悎椋庨櫓绛夌骇</div>
+ <a-tag :color="qualityRisk ? 'red' : riskLevel === 'NORMAL' ? 'green' : 'orange'">{{ riskText }}</a-tag>
+ </div>
+ </div>
+ <div class="mx-4 mb-4 rounded border p-4">
+ <div class="mb-3 font-medium">璐d换閾�</div>
+ <div class="grid gap-2 text-sm md:grid-cols-3">
+ <div>璐d换浜猴細{{ detail.ownerUserName || '鏈褰�' }}</div>
+ <div>鐢熶骇绾匡細{{ detail.lineName || detail.lineCode || '鏈褰�' }}</div>
+ <div>鐢熶骇浜哄憳锛歿{ detail.workers?.length ?? 0 }} 浜�</div>
+ </div>
+ </div>
+</template>
diff --git a/src/views/mes/qc/batchtrace/modules/TraceGraph.vue b/src/views/mes/qc/batchtrace/modules/TraceGraph.vue
new file mode 100644
index 0000000..e2734cb
--- /dev/null
+++ b/src/views/mes/qc/batchtrace/modules/TraceGraph.vue
@@ -0,0 +1,47 @@
+<script lang="ts" setup>
+import type { EchartsUIType, EChartsOption } from '@vben/plugins/echarts';
+import type { MesTraceBatchApi } from '#/api/mes/trace/batch';
+
+import { nextTick, onMounted, ref, watch } from 'vue';
+
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
+
+const props = defineProps<{ detail: MesTraceBatchApi.Detail }>();
+const chartRef = ref<EchartsUIType>();
+const { renderEcharts } = useEcharts(chartRef);
+const render = () => renderEcharts(buildOption());
+
+// use-echarts 鍦� onMounted 鍚庢墠灏� isActive 缃负 true锛�
+// 鏁呮覆鏌撳繀椤绘斁鍒� onMounted锛坕mmediate watch 鍦� setup 闃舵浼氳闈欓粯涓㈠純锛�
+onMounted(() => {
+ void nextTick(render);
+});
+watch(() => props.detail, () => void render(), { deep: true });
+
+const buildOption = (): EChartsOption => {
+ const nodes: { id: string; name: string; category: number; symbolSize: number }[] = [];
+ const links: { source: string; target: string }[] = [];
+ const add = (id: string, name: string, category: number, symbolSize = 42) => {
+ if (!nodes.some((node) => node.id === id)) nodes.push({ id, name, category, symbolSize });
+ };
+ const connect = (source: string, target: string) => {
+ if (source !== target && !links.some((link) => link.source === source && link.target === target)) links.push({ source, target });
+ };
+ const root = `batch:${props.detail.batchCode || props.detail.requestCode || 'unknown'}`;
+ add(root, props.detail.batchCode || '鐢熶骇鎵规', 1, 58);
+ (props.detail.purchaseSources ?? []).slice(0, 12).forEach((item, index) => { const id = `source:${index}`; add(id, item.vendorName || item.receiptCode || '閲囪喘鏉ユ簮', 0); connect(id, root); });
+ if (props.detail.quality) { add('quality', props.detail.quality.resultName || '璐ㄩ噺妫�楠�', 2); connect(root, 'quality'); }
+ add('warehouse', '鎵爜鍏ュ簱', 3); connect(root, 'warehouse');
+ (props.detail.salesTargets ?? []).slice(0, 12).forEach((item, index) => { const id = `sales:${index}`; add(id, item.clientName || item.salesCode || '閿�鍞幓鍚�', 4); connect('warehouse', id); });
+ if (props.detail.antiFake?.scanCount) { add('consumer', `娑堣垂鑰呴獙璇乗n${props.detail.antiFake.scanCount} 娆, 5); connect('warehouse', 'consumer'); }
+ const series = { type: 'graph' as const, layout: 'force' as const, roam: true, draggable: true, data: nodes, links, categories: ['鏉ユ簮', '鐢熶骇鎵规', '璐ㄩ噺', '浠撳偍', '閿�鍞�', '娑堣垂'].map((name) => ({ name })), label: { show: true, fontSize: 12 }, force: { repulsion: 260, edgeLength: 110 }, lineStyle: { color: '#94a3b8', curveness: 0.15 } };
+ return { tooltip: { trigger: 'item' }, legend: [{ data: ['鏉ユ簮', '鐢熶骇鎵规', '璐ㄩ噺', '浠撳偍', '閿�鍞�', '娑堣垂'] }], series: [series] };
+};
+</script>
+
+<template>
+ <div class="p-4">
+ <EchartsUI ref="chartRef" :height="'420px'" class="w-full rounded border bg-muted/20" />
+ <div class="text-muted-foreground mt-2 text-center text-xs">鍥捐氨榛樿灞曠ず鎵规绾у叧绯伙紝鏉ユ簮鍜岄攢鍞妭鐐瑰悇鏈�澶� 12 鏉�</div>
+ </div>
+</template>
diff --git a/src/views/mes/qc/batchtrace/modules/TraceStageOverview.vue b/src/views/mes/qc/batchtrace/modules/TraceStageOverview.vue
new file mode 100644
index 0000000..a4aaa1a
--- /dev/null
+++ b/src/views/mes/qc/batchtrace/modules/TraceStageOverview.vue
@@ -0,0 +1,40 @@
+<script lang="ts" setup>
+import { computed, onMounted, onUnmounted, ref } from 'vue';
+import { gsap } from 'gsap';
+import type { MesTraceBatchApi } from '#/api/mes/trace/batch';
+
+const props = defineProps<{ detail: MesTraceBatchApi.Detail }>();
+const qualityRisk = computed(() => props.detail.decision?.qualityRisk ?? props.detail.quality?.result === 2);
+const riskLevel = computed(() => props.detail.decision?.riskLevel || (qualityRisk.value ? 'HIGH' : 'NORMAL'));
+const containerRef = ref<HTMLElement>();
+let animationContext: gsap.Context | undefined;
+
+onMounted(() => {
+ if (!containerRef.value) return;
+ animationContext = gsap.context(() => {
+ gsap.from('.trace-stage-card', { autoAlpha: 0, y: 12, duration: 0.3, stagger: 0.04, ease: 'power2.out' });
+ }, containerRef.value);
+});
+
+onUnmounted(() => animationContext?.revert());
+</script>
+
+<template>
+ <div ref="containerRef" class="grid grid-cols-2 gap-3 p-4 lg:grid-cols-4">
+ <div v-for="stage in [
+ { name: '鏉ユ簮', value: detail.purchaseSources?.length ? '宸插缓绔�' : '寰呰ˉ鍏�' },
+ { name: '鐢熶骇鎵规', value: detail.batchStatusName || '鏈煡' },
+ { name: '璐ㄩ噺妫�楠�', value: detail.quality?.resultName || '寰呮楠�' },
+ { name: '鍖呰璧嬬爜', value: `${detail.bagTotal ?? 0} 琚� / ${detail.palletCount ?? 0} 鎵榒 },
+ { name: '浠撳偍鍏ュ簱', value: detail.inbound ? '宸插叆搴�' : '鏈叆搴�' },
+ { name: '閿�鍞幓鍚�', value: `${detail.salesTargets?.length ?? 0} 鏉 },
+ { name: '娑堣垂鑰呴獙璇�', value: `${detail.antiFake?.scanCount ?? 0} 娆℃壂鐮乣 },
+ { name: '椋庨櫓鐘舵��', value: qualityRisk ? '璐ㄩ噺寮傚父' : riskLevel === 'NORMAL' ? '姝e父' : riskLevel },
+ ]" :key="stage.name" class="trace-stage-card rounded border bg-white p-4 shadow-sm">
+ <div class="text-muted-foreground text-sm">{{ stage.name }}</div>
+ <div class="mt-2 text-base font-semibold" :class="stage.name === '椋庨櫓鐘舵��' ? qualityRisk ? 'text-red-600' : riskLevel === 'NORMAL' ? 'text-green-600' : 'text-orange-600' : ''">
+ {{ stage.value }}
+ </div>
+ </div>
+ </div>
+</template>
\ No newline at end of file
diff --git a/src/views/mes/qc/batchtrace/modules/TraceTimeline.vue b/src/views/mes/qc/batchtrace/modules/TraceTimeline.vue
new file mode 100644
index 0000000..4235ef8
--- /dev/null
+++ b/src/views/mes/qc/batchtrace/modules/TraceTimeline.vue
@@ -0,0 +1,37 @@
+<script lang="ts" setup>
+import { computed } from 'vue';
+import type { MesTraceBatchApi } from '#/api/mes/trace/batch';
+
+const props = defineProps<{ detail: MesTraceBatchApi.Detail }>();
+
+const formatTime = (value?: string) => value ? new Date(value).toLocaleString() : '鏈褰�';
+
+const timeline = computed(() => {
+ if (props.detail.timeline?.length) return props.detail.timeline;
+ return [
+ ...(props.detail.purchaseSources?.length ? [{ stage: '鏉ユ簮', eventName: '閲囪喘鏉ユ簮', status: `${props.detail.purchaseSources.length} 鏉¤褰昤 }] : []),
+ { stage: '鐢熶骇', eventType: 'BATCH_CREATED', eventName: '鐢熶骇鎵规寤虹珛', eventTime: props.detail.produceDate, status: props.detail.batchStatusName },
+ { stage: '鍖呰', eventName: '鍖呰璧嬬爜', quantity: props.detail.bagTotal },
+ ...(props.detail.inbound ? [{ stage: '浠撳偍', eventType: 'INBOUND', eventName: '浠撳偍鍏ュ簱', eventTime: props.detail.inbound.eventTime, quantity: props.detail.inbound.quantity, status: props.detail.inbound.status }] : []),
+ ...(props.detail.salesTargets ?? []).map((item) => ({ stage: '閿�鍞�', eventType: 'OUTBOUND', eventName: `閿�鍞幓鍚� 路 ${item.clientName || '鏈褰曞鎴�'}`, eventTime: item.salesDate, quantity: item.quantity, status: item.qualityStatus == null ? undefined : String(item.qualityStatus) })),
+ ...(props.detail.antiFake?.scanCount ? [{ stage: '娑堣垂', eventType: 'CONSUMER_SCANNED', eventName: '娑堣垂鑰呴獙璇�', eventTime: props.detail.antiFake.lastScanTime, quantity: props.detail.antiFake.scanCount, status: `绱鎵爜 ${props.detail.antiFake.scanCount} 娆 }] : []),
+ ];
+});
+
+const colorMap: Record<string, string> = { 鏉ユ簮: 'blue', 鐢熶骇: 'blue', 鍖呰: 'purple', 浠撳偍: 'green', 閿�鍞�: 'orange', 娑堣垂: 'purple', 椋庨櫓: 'red' };
+</script>
+
+<template>
+ <div class="p-4">
+ <a-timeline>
+ <a-timeline-item v-for="(item, index) in timeline" :key="`${item.eventType || item.stage}-${index}`" :color="colorMap[item.stage || '鐢熶骇'] || 'blue'">
+ <div class="font-medium">{{ item.eventName || '杩芥函浜嬩欢' }}</div>
+ <div class="text-muted-foreground text-sm">
+ {{ item.stage || '鏈垎绫�' }} 路 鏁伴噺锛歿{ item.quantity ?? '鈥�' }} 路 {{ formatTime(item.eventTime) }}
+ <span v-if="item.status"> 路 {{ item.status }}</span>
+ </div>
+ </a-timeline-item>
+ <a-empty v-if="!timeline.length" description="鏆傛棤杩芥函浜嬩欢" />
+ </a-timeline>
+ </div>
+</template>
diff --git a/src/views/mes/qc/batchtrace/modules/trace-detail.vue b/src/views/mes/qc/batchtrace/modules/trace-detail.vue
index 8d01d5b..b26d2ff 100644
--- a/src/views/mes/qc/batchtrace/modules/trace-detail.vue
+++ b/src/views/mes/qc/batchtrace/modules/trace-detail.vue
@@ -1,34 +1,29 @@
<script lang="ts" setup>
-import type { MesWmBatchApi } from '#/api/mes/wm/batch';
+import type { MesTraceBatchApi } from '#/api/mes/trace/batch';
-import { ref } from 'vue';
+import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
-import { Tabs, Tooltip } from 'ant-design-vue';
+import { Descriptions, Spin, Tabs, Tag, message } from 'ant-design-vue';
-import { useDescription } from '#/components/description';
+import { getBatchTraceDetail } from '#/api/mes/trace/batch';
-import { useDetailSchema } from '../data';
-import ProcessTraceList from './process-trace-list.vue';
-import PurchaseSourceList from './purchase-source-list.vue';
-import SalesTargetList from './sales-target-list.vue';
-import TraceList from './trace-list.vue';
+import TraceDecision from './TraceDecision.vue';
+import TraceGraph from './TraceGraph.vue';
+import TraceStageOverview from './TraceStageOverview.vue';
+import TraceTimeline from './TraceTimeline.vue';
-const detailData = ref<MesWmBatchApi.Batch>(); // 褰撳墠鎵规璇︽儏
-const subTabsName = ref<
- | 'backward'
- | 'forward'
- | 'purchase'
- | 'sales'
- | 'process'
->('forward'); // 褰撳墠婵�娲荤殑杩芥函椤电
-
-const [Descriptions] = useDescription({
- bordered: true,
- column: 3,
- schema: useDetailSchema(),
+const detailData = ref<MesTraceBatchApi.Detail>();
+const loading = ref(false);
+const activeTab = ref('overview');
+const formatDate = (value?: string) => value ? new Date(value).toLocaleString() : '鏈褰�';
+const riskLevel = computed(() => {
+ if (!detailData.value) return 'NORMAL';
+ return detailData.value.decision?.riskLevel || (detailData.value.quality?.result === 2 ? 'HIGH' : 'NORMAL');
});
+const riskLabel = computed(() => riskLevel.value === 'HIGH' ? '楂橀闄�' : riskLevel.value === 'NORMAL' ? '姝e父' : '闇�鍏虫敞');
+const riskColor = computed(() => riskLevel.value === 'HIGH' ? 'red' : riskLevel.value === 'NORMAL' ? 'green' : 'orange');
const [Modal, modalApi] = useVbenModal({
onOpenChange(isOpen) {
@@ -36,66 +31,71 @@
detailData.value = undefined;
return;
}
- detailData.value = modalApi.getData<MesWmBatchApi.Batch>();
- subTabsName.value = 'forward';
+ const row = modalApi.getData<{ code?: string }>();
+ if (row?.code) {
+ void loadDetail(row.code);
+ }
+ activeTab.value = 'overview';
},
});
+
+async function loadDetail(code: string) {
+ loading.value = true;
+ try {
+ detailData.value = await getBatchTraceDetail(code);
+ } catch {
+ message.error('鎵规杩芥函璇︽儏鍔犺浇澶辫触');
+ } finally {
+ loading.value = false;
+ }
+}
</script>
<template>
<Modal
- title="鎵规杩芥函"
- class="w-4/5"
+ title="鍏ㄩ摼璺壒娆¤拷婧�"
+ class="w-11/12"
:show-cancel-button="false"
:show-confirm-button="false"
>
- <Descriptions class="mx-4 mt-2" :data="detailData" />
- <Tabs
- v-if="detailData?.code"
- v-model:active-key="subTabsName"
- class="mx-4 mt-4"
- type="card"
- >
- <Tabs.TabPane key="forward">
- <template #tab>
- <Tooltip title="椤虹潃鐢熶骇娴佸線涓嬫父鏌ワ細璇ユ壒娆¤鍝簺宸ュ崟娑堣�楀悗锛屼骇鍑烘垚浜嗗摢浜涙垚鍝佹壒娆�">
- <span>鍚戝墠杩芥函</span>
- </Tooltip>
- </template>
- <TraceList :batch-code="detailData.code" direction="forward" />
- </Tabs.TabPane>
- <Tabs.TabPane key="backward">
- <template #tab>
- <Tooltip title="閫嗙潃鐢熶骇娴佸線涓婃父鏌ワ細鐢熶骇璇ユ壒娆℃椂锛屾秷鑰椾簡鍝簺鍘熸枡鎵规">
- <span>鍚戝悗杩芥函</span>
- </Tooltip>
- </template>
- <TraceList :batch-code="detailData.code" direction="backward" />
- </Tabs.TabPane>
- <Tabs.TabPane key="purchase">
- <template #tab>
- <Tooltip title="鍘熸枡渚ф潵婧愶細璇ユ壒娆″搴旂殑閲囪喘鍏ュ簱鍗曚笌 IQC 鏉ユ枡妫�楠岃褰�">
- <span>閲囪喘鏉ユ簮</span>
- </Tooltip>
- </template>
- <PurchaseSourceList :batch-code="detailData.code" />
- </Tabs.TabPane>
- <Tabs.TabPane key="sales">
- <template #tab>
- <Tooltip title="鎴愬搧渚у幓鍚戯細璇ユ壒娆″搴旂殑閿�鍞嚭搴撳崟涓� OQC 鍑哄巶妫�楠岃褰�">
- <span>閿�鍞幓鍚�</span>
- </Tooltip>
- </template>
- <SalesTargetList :batch-code="detailData.code" />
- </Tabs.TabPane>
- <Tabs.TabPane key="process">
- <template #tab>
- <Tooltip title="鍚勫伐搴忕殑鎶曞叆/浜у嚭浜嬩欢锛氳鎵规鍦ㄥ摢浜涘伐搴忚鎶曞叆鎴栦骇鍑猴紝鍙婃暟閲忓拰璐ㄩ噺鐘舵��">
- <span>宸ュ簭娴佽浆</span>
- </Tooltip>
- </template>
- <ProcessTraceList :batch-code="detailData.code" />
- </Tabs.TabPane>
- </Tabs>
+ <Spin :spinning="loading">
+ <div v-if="detailData" class="px-2">
+ <div class="mb-4 grid grid-cols-2 gap-3 lg:grid-cols-5">
+ <div class="rounded border bg-blue-50 px-4 py-3"><div class="text-muted-foreground text-xs">琚嬬爜鏁伴噺</div><div class="mt-1 text-lg font-semibold text-blue-700">{{ detailData.bagTotal ?? 0 }}</div></div>
+ <div class="rounded border bg-violet-50 px-4 py-3"><div class="text-muted-foreground text-xs">鎵樼洏鏁伴噺</div><div class="mt-1 text-lg font-semibold text-violet-700">{{ detailData.palletCount ?? 0 }}</div></div>
+ <div class="rounded border bg-green-50 px-4 py-3"><div class="text-muted-foreground text-xs">鍏ュ簱鏁伴噺</div><div class="mt-1 text-lg font-semibold text-green-700">{{ detailData.decision?.inboundQuantity ?? detailData.inbound?.quantity ?? 0 }}</div></div>
+ <div class="rounded border bg-orange-50 px-4 py-3"><div class="text-muted-foreground text-xs">閿�鍞幓鍚�</div><div class="mt-1 text-lg font-semibold text-orange-700">{{ detailData.decision?.salesTargetCount ?? detailData.salesTargets?.length ?? 0 }}</div></div>
+ <div class="col-span-2 rounded border px-4 py-3 lg:col-span-1" :class="riskLevel === 'HIGH' ? 'border-red-200 bg-red-50' : riskLevel === 'NORMAL' ? 'border-green-200 bg-green-50' : 'border-orange-200 bg-orange-50'"><div class="text-muted-foreground text-xs">缁煎悎椋庨櫓</div><div class="mt-1"><Tag :color="riskColor">{{ riskLabel }}</Tag></div></div>
+ </div>
+ <Descriptions bordered :column="{ xs: 1, sm: 2, md: 3 }" size="small">
+ <Descriptions.Item label="杩芥函鍏ュ彛">{{ detailData.requestCode || '鏈褰�' }}</Descriptions.Item>
+ <Descriptions.Item label="鍏ュ彛绫诲瀷"><Tag color="blue">{{ detailData.sourceType || 'BATCH' }}</Tag></Descriptions.Item>
+ <Descriptions.Item label="鎵规鐘舵��">{{ detailData.batchStatusName || '鏈煡' }}</Descriptions.Item>
+ <Descriptions.Item label="鍝佺">{{ detailData.itemName || detailData.itemCode || '鏈褰�' }}</Descriptions.Item>
+ <Descriptions.Item label="鎵规鍙�">{{ detailData.batchCode || '鏈褰�' }}</Descriptions.Item>
+ <Descriptions.Item label="璐d换浜�">{{ detailData.ownerUserName || '鏈褰�' }}</Descriptions.Item>
+ <Descriptions.Item label="鐢熶骇绾�">{{ detailData.lineName || detailData.lineCode || '鏈褰�' }}</Descriptions.Item>
+ <Descriptions.Item label="璁″垝鏁伴噺">{{ detailData.planQuantity ?? 0 }}</Descriptions.Item>
+ <Descriptions.Item label="鐢熶骇鏃ユ湡">{{ formatDate(detailData.produceDate) }}</Descriptions.Item>
+ <Descriptions.Item label="鍏ュ簱鏃堕棿">{{ formatDate(detailData.inboundTime || detailData.inbound?.eventTime) }}</Descriptions.Item>
+ </Descriptions>
+
+ <Tabs v-model:active-key="activeTab" class="mt-4" type="card">
+ <Tabs.TabPane key="overview" tab="闃舵鎬昏">
+ <TraceStageOverview :detail="detailData" />
+ </Tabs.TabPane>
+ <Tabs.TabPane key="timeline" tab="鍏ㄩ摼璺椂闂寸嚎">
+ <TraceTimeline :detail="detailData" />
+ </Tabs.TabPane>
+ <Tabs.TabPane key="graph" tab="娴佸姩鍥捐氨">
+ <TraceGraph :detail="detailData" />
+ </Tabs.TabPane>
+ <Tabs.TabPane key="decision" tab="鍐崇瓥鍒嗘瀽">
+ <TraceDecision :detail="detailData" />
+ </Tabs.TabPane>
+ </Tabs>
+ </div>
+ <div v-else-if="!loading" class="py-12 text-center text-muted-foreground">鏆傛棤杩芥函鏁版嵁</div>
+ </Spin>
</Modal>
</template>
diff --git a/src/views/mes/qc/seedQuality/index.vue b/src/views/mes/qc/seedQuality/index.vue
index 0a61ccc..ca55a98 100644
--- a/src/views/mes/qc/seedQuality/index.vue
+++ b/src/views/mes/qc/seedQuality/index.vue
@@ -3,14 +3,12 @@
import { message, Modal } from 'ant-design-vue';
import { IconifyIcon } from '@vben/icons';
+import { MesWmWarehouseTypeEnum } from '@vben/constants';
import { gsap } from 'gsap';
-import MdItemSelect from '#/views/mes/md/item/components/select.vue';
-
-import {
- submitSeedQuality,
- type MesQcSeedQualityApi,
-} from '#/api/mes/qc/seedQuality';
+import type { MesWmWarehouseApi } from '#/api/mes/wm/warehouse';
+import { submitSeedQuality } from '#/api/mes/qc/seedQuality';
+import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
interface IndicatorForm {
code: string;
@@ -23,12 +21,15 @@
const pageRoot = ref<HTMLElement>();
const code = ref('');
-const itemId = ref<number>();
const batchCode = ref('');
const reason = ref('');
const loading = ref(false);
const submittedId = ref<number>();
let animationContext: gsap.Context | undefined;
+
+/** 鍙�夊悎鏍间粨搴擄紙浠撳簱绫诲瀷=20 鍚堟牸搴擄級 */
+const qualifiedWarehouses = ref<MesWmWarehouseApi.Warehouse[]>([]);
+const qualifiedWarehouseId = ref<number>();
const indicators = ref<IndicatorForm[]>([
{ code: 'purity', label: '绾害', unit: '%', minValue: 0, maxValue: 100 },
@@ -50,7 +51,6 @@
function resetForm(clearResult = true) {
code.value = '';
- itemId.value = undefined;
batchCode.value = '';
reason.value = '';
if (clearResult) submittedId.value = undefined;
@@ -62,12 +62,16 @@
}
async function submit() {
- if (!code.value.trim() || !itemId.value) {
- message.warning('璇峰~鍐欒鐮佸苟閫夋嫨鐗╂枡');
+ if (!code.value.trim()) {
+ message.warning('璇峰~鍐欒鐮�');
return;
}
if (!allEntered.value) {
message.warning('璇峰厛濉啓鍥涢」鎸囨爣鐨勫疄娴嬪��');
+ return;
+ }
+ if (!qualifiedWarehouseId.value) {
+ message.warning('璇烽�夋嫨鍏ュ簱鍚堟牸搴�');
return;
}
if (indicators.value.some((item) => item.minValue === undefined || item.maxValue === undefined || item.minValue > item.maxValue)) {
@@ -98,7 +102,6 @@
try {
submittedId.value = await submitSeedQuality({
code: code.value.trim(),
- itemId: itemId.value,
batchCode: batchCode.value.trim() || undefined,
indicators: indicators.value.map(({ code: indicatorCode, value, minValue, maxValue }) => ({
code: indicatorCode,
@@ -107,6 +110,7 @@
maxValue: maxValue as number,
})),
reason: reason.value.trim() || undefined,
+ qualifiedWarehouseId: qualifiedWarehouseId.value,
});
message.success(`璐ㄦ鎻愪氦鎴愬姛锛岃川妫�缂栧彿锛�${submittedId.value}`);
if (pageRoot.value && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
@@ -122,6 +126,14 @@
}
onMounted(async () => {
+ try {
+ const warehouses = await getWarehouseSimpleList();
+ qualifiedWarehouses.value = (warehouses ?? []).filter(
+ (item) => item.type === MesWmWarehouseTypeEnum.QUALIFIED,
+ );
+ } catch {
+ qualifiedWarehouses.value = [];
+ }
await nextTick();
if (!pageRoot.value) return;
animationContext = gsap.context(() => {
@@ -162,10 +174,16 @@
<a-form layout="vertical">
<div class="base-grid">
<a-form-item label="琚嬬爜" required><a-input v-model:value="code" size="large" :disabled="loading" placeholder="璇疯緭鍏ユ垨鎵弿琚嬬爜" /></a-form-item>
- <a-form-item label="鐗╂枡" required>
- <MdItemSelect v-model="itemId" :disabled="loading" size="large" placeholder="璇烽�夋嫨鐗╂枡" />
- </a-form-item>
<a-form-item label="鐢熶骇鎵规"><a-input v-model:value="batchCode" size="large" :disabled="loading" placeholder="璇疯緭鍏ユ壒娆″彿锛堥�夊~锛�" /></a-form-item>
+ <a-form-item label="鍏ュ簱鍚堟牸搴�" required class="full-width">
+ <a-select
+ v-model:value="qualifiedWarehouseId"
+ size="large"
+ :disabled="loading"
+ placeholder="璇烽�夋嫨璐ㄦ鍚堟牸鍚庡叆搴撶殑鍚堟牸搴�"
+ :options="qualifiedWarehouses.map((item) => ({ label: `${item.name}锛�${item.code}锛塦, value: item.id }))"
+ />
+ </a-form-item>
</div>
<a-divider orientation="left">鎸囨爣缁撴灉</a-divider>
@@ -190,7 +208,7 @@
<template #title><div class="card-title"><IconifyIcon icon="ep:data-analysis" />鍒ゅ畾璇存槑</div></template>
<div class="result-hero" :class="overallPassed ? 'passed' : allEntered ? 'failed-result' : 'pending-result'"><IconifyIcon :icon="overallPassed ? 'ep:circle-check-filled' : allEntered ? 'ep:warning-filled' : 'ep:edit-pen'" /><strong>{{ overallPassed ? '褰撳墠缁撴灉鍚堟牸' : allEntered ? '褰撳墠瀛樺湪涓嶅悎鏍奸」' : '绛夊緟褰曞叆鎸囨爣' }}</strong><span>{{ allEntered ? '瀹炴祴鍊奸渶澶勪簬鏈�灏忓�间笌鏈�澶у�间箣闂�' : `宸插綍鍏� ${enteredCount} / 4 椤瑰疄娴嬪�糮 }}</span></div>
<div class="rule-list"><div><IconifyIcon icon="ep:check" /><span>鍥涢」鎸囨爣蹇呴』鍏ㄩ儴鎻愪氦</span></div><div><IconifyIcon icon="ep:check" /><span>杈圭晫鍊兼寜鍚堟牸澶勭悊</span></div><div><IconifyIcon icon="ep:check" /><span>涓嶅悎鏍肩粨鏋滈渶淇濈暀鍘熷洜</span></div><div><IconifyIcon icon="ep:check" /><span>璐ㄦ鍛樹笌妫�楠屾椂闂寸敱鏈嶅姟绔褰�</span></div></div>
- <a-alert message="搴撳瓨娴佽浆鐢卞悗绔鐞�" description="鍚堟牸鍝佸皢浠庢殏瀛樺簱杞叆姝e紡搴撳瓨锛涗笉鍚堟牸鍝佸皢鐩存帴娓呴櫎鏆傚瓨搴撳瓨骞朵繚鐣欎笉鍚堟牸鍘熷洜銆傝川妫�鍛樺拰妫�楠屾椂闂寸敱鏈嶅姟绔褰曪紝鏈〉闈笉閫夋嫨浠撳簱銆佸簱鍖烘垨搴撲綅銆�" type="info" show-icon />
+ <a-alert message="搴撳瓨娴佽浆鐢卞悗绔鐞�" description="鍚堟牸鍝佸皢鎸夋墍閫夈�屽叆搴撳悎鏍煎簱銆嶄粠鏆傚瓨搴撳瓨鍏ヨ鍚堟牸搴擄紱涓嶅悎鏍煎搧灏嗙洿鎺ユ竻闄ゆ殏瀛樺簱瀛樺苟淇濈暀涓嶅悎鏍煎師鍥犮�傝川妫�鍛樺拰妫�楠屾椂闂寸敱鏈嶅姟绔褰曘��" type="info" show-icon />
<div v-if="submittedId" class="success-note"><IconifyIcon icon="ep:success-filled" /><span>鏈�杩戞彁浜よ川妫�缂栧彿锛�<strong>{{ submittedId }}</strong></span></div>
</a-card>
</section>
@@ -200,9 +218,9 @@
<style scoped>
.quality-page { min-height: 100%; padding: 24px; background: #f4f7fb; color: #172033; }
.page-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 22px; }.eyebrow { color: #1677ff; font-size: 12px; font-weight: 700; letter-spacing: .08em; }.page-header h1 { margin: 6px 0; font-size: 28px; }.page-header p { margin: 0; color: #718096; }.header-badge { display: inline-flex; align-items: center; gap: 8px; padding: 9px 13px; border: 1px solid #cfe0fb; border-radius: 7px; color: #1677ff; background: #f0f6ff; font-size: 13px; font-weight: 600; }
-.summary-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 18px; }.summary-card { display: flex; align-items: flex-start; gap: 14px; padding: 17px 19px; border: 1px solid #e8edf5; border-radius: 10px; background: #fff; box-shadow: 0 3px 12px #243b5310; }.summary-card > .iconify { flex: 0 0 38px; width: 38px; height: 38px; padding: 9px; border-radius: 8px; font-size: 20px; }.summary-card.blue > .iconify { color: #1677ff; background: #eaf3ff; }.summary-card.green > .iconify { color: #17a673; background: #e6f8f0; }.summary-card.red > .iconify { color: #e05252; background: #fff0f0; }.summary-card span, .summary-card small { display: block; color: #7b8799; font-size: 12px; }.summary-card strong { display: block; margin: 4px 0; font-size: 23px; }.content-grid { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(310px, .75fr); gap: 18px; }.form-card, .result-card { border-radius: 10px; box-shadow: 0 3px 12px #243b5310; }.card-title { display: flex; align-items: center; gap: 9px; font-weight: 700; }.card-title :deep(.iconify) { color: #1677ff; }.base-grid { display: grid; grid-template-columns: 1.3fr 1fr 1.2fr; gap: 16px; }.full-width { width: 100%; }
+.summary-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 18px; }.summary-card { display: flex; align-items: flex-start; gap: 14px; padding: 17px 19px; border: 1px solid #e8edf5; border-radius: 10px; background: #fff; box-shadow: 0 3px 12px #243b5310; }.summary-card > .iconify { flex: 0 0 38px; width: 38px; height: 38px; padding: 9px; border-radius: 8px; font-size: 20px; }.summary-card.blue > .iconify { color: #1677ff; background: #eaf3ff; }.summary-card.green > .iconify { color: #17a673; background: #e6f8f0; }.summary-card.red > .iconify { color: #e05252; background: #fff0f0; }.summary-card span, .summary-card small { display: block; color: #7b8799; font-size: 12px; }.summary-card strong { display: block; margin: 4px 0; font-size: 23px; }.content-grid { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(310px, .75fr); gap: 18px; }.form-card, .result-card { border-radius: 10px; box-shadow: 0 3px 12px #243b5310; }.card-title { display: flex; align-items: center; gap: 9px; font-weight: 700; }.card-title :deep(.iconify) { color: #1677ff; }.base-grid { display: grid; grid-template-columns: 1.3fr 1.2fr; gap: 16px; }.full-width { width: 100%; }
.indicator-list { display: flex; flex-direction: column; gap: 10px; }.indicator-row { display: flex; align-items: center; gap: 9px; padding: 11px 12px; border: 1px solid #e8edf5; border-radius: 7px; background: #fbfcfe; }.indicator-row.failed { border-color: #ffd6d6; background: #fffafa; }.indicator-name { display: flex; align-items: center; gap: 8px; min-width: 82px; }.indicator-name small { color: #8793a5; font-size: 12px; }.indicator-dot { width: 8px; height: 8px; border-radius: 50%; }.indicator-dot.pass { background: #20b878; }.indicator-dot.fail { background: #e05252; }.indicator-input { width: 105px; }.range-input { width: 88px; }.range-separator { margin-left: auto; color: #8793a5; font-size: 12px; }.reason-item { margin-top: 18px; }.form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px; }.form-actions :deep(.iconify) { margin-right: 5px; }
.result-hero { display: flex; flex-direction: column; align-items: center; gap: 8px; margin: 8px 0 22px; padding: 25px 15px; border-radius: 8px; text-align: center; }.result-hero :deep(.iconify) { font-size: 35px; }.result-hero strong { font-size: 18px; }.result-hero span { color: #718096; font-size: 12px; }.result-hero.passed { color: #16835b; background: #effaf5; }.result-hero.failed-result { color: #c53e3e; background: #fff3f3; }.result-hero.pending-result { color: #1677ff; background: #f0f6ff; }.rule-list { display: flex; flex-direction: column; gap: 14px; margin-bottom: 22px; }.rule-list div { display: flex; align-items: center; gap: 9px; color: #59677b; font-size: 13px; }.rule-list :deep(.iconify) { color: #20b878; }.success-note { display: flex; align-items: center; gap: 8px; margin-top: 16px; padding: 11px 12px; border-radius: 7px; color: #16835b; background: #effaf5; font-size: 13px; }
-@media (max-width: 1050px) { .content-grid { grid-template-columns: 1fr; }.base-grid { grid-template-columns: repeat(3, 1fr); } }
+@media (max-width: 1050px) { .content-grid { grid-template-columns: 1fr; }.base-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 760px) { .quality-page { padding: 14px; }.page-header { display: block; }.header-badge { margin-top: 14px; }.summary-grid, .base-grid { grid-template-columns: 1fr; }.indicator-row { flex-wrap: wrap; }.range-separator { margin-left: 0; }.indicator-input { flex: 1; min-width: 105px; }.range-input { flex: 1; min-width: 75px; } }
</style>
diff --git a/src/views/mes/qc/seedQuality/record.vue b/src/views/mes/qc/seedQuality/record.vue
new file mode 100644
index 0000000..00cc702
--- /dev/null
+++ b/src/views/mes/qc/seedQuality/record.vue
@@ -0,0 +1,284 @@
+<script setup lang="ts">
+import { computed, onMounted, ref } from 'vue';
+
+import type { TablePaginationConfig } from 'ant-design-vue';
+import { IconifyIcon } from '@vben/icons';
+
+import {
+ getSeedQualityPage,
+ type SeedQualityRecord,
+} from '#/api/mes/qc/seedQuality';
+
+const records = ref<SeedQualityRecord[]>([]);
+const loading = ref(false);
+const page = ref(1);
+const pageSize = ref(10);
+
+const code = ref('');
+const batchCode = ref('');
+const result = ref<number>();
+
+const total = ref(0);
+const passTotal = ref(0);
+const failTotal = ref(0);
+
+const processingTotal = computed(() =>
+ Math.max(total.value - passTotal.value - failTotal.value, 0),
+);
+
+const resultLabel = (value: number) =>
+ value === 1 ? '鍚堟牸' : value === 2 ? '涓嶅悎鏍�' : '澶勭悊涓�';
+const resultColor = (value: number) =>
+ value === 1 ? '#16a34a' : value === 2 ? '#dc2626' : '#d97706';
+const resultBg = (value: number) =>
+ value === 1 ? '#ecfdf3' : value === 2 ? '#fef2f2' : '#fffbeb';
+const formatDateTime = (value?: string) =>
+ value ? new Date(value).toLocaleString() : '鈥�';
+
+async function loadRecords() {
+ loading.value = true;
+ try {
+ const data = await getSeedQualityPage({
+ pageNo: page.value,
+ pageSize: pageSize.value,
+ code: code.value.trim() || undefined,
+ batchCode: batchCode.value.trim() || undefined,
+ result: result.value,
+ });
+ records.value = data.list;
+ total.value = data.total;
+ } finally {
+ loading.value = false;
+ }
+}
+
+async function loadStats() {
+ try {
+ const [all, pass, fail] = await Promise.all([
+ getSeedQualityPage({ pageNo: 1, pageSize: 1 }),
+ getSeedQualityPage({ pageNo: 1, pageSize: 1, result: 1 }),
+ getSeedQualityPage({ pageNo: 1, pageSize: 1, result: 2 }),
+ ]);
+ total.value = all.total;
+ passTotal.value = pass.total;
+ failTotal.value = fail.total;
+ } catch {
+ // 缁熻鍔犺浇澶辫触涓嶉樆濉炲垪琛�
+ }
+}
+
+function searchRecords() {
+ page.value = 1;
+ void loadRecords();
+}
+
+function resetFilters() {
+ code.value = '';
+ batchCode.value = '';
+ result.value = undefined;
+ searchRecords();
+}
+
+function refreshAll() {
+ void Promise.all([loadRecords(), loadStats()]);
+}
+
+function handleTableChange(pagination: TablePaginationConfig) {
+ page.value = pagination.current || 1;
+ pageSize.value = pagination.pageSize || 10;
+ void loadRecords();
+}
+
+onMounted(refreshAll);
+</script>
+
+<template>
+ <div class="record-page">
+ <!-- 椤靛ご -->
+ <header class="page-header">
+ <div class="header-copy">
+ <div class="eyebrow">MES / 璐ㄩ噺绠$悊</div>
+ <h1>璐ㄦ璁板綍</h1>
+ <p>鎸夎鐮佹矇娣�姣忎竴琚嬬瀛愮殑妫�楠岀粨璁猴紝杩借釜鍚堟牸銆佷笉鍚堟牸涓庡緟澶勭悊缁撴灉</p>
+ </div>
+ <div class="header-badge">
+ <span class="badge-pulse" />
+ 瀹炴椂姹囨��
+ </div>
+ </header>
+
+ <!-- 缁撴灉缁熻 -->
+ <section class="stat-grid" aria-label="璐ㄦ缁撴灉姹囨��">
+ <div class="stat-card total">
+ <div class="stat-icon"><IconifyIcon icon="ep:document" /></div>
+ <div class="stat-main"><span>璁板綍鎬绘暟</span><strong>{{ total.toLocaleString() }}</strong><small>鎸夎鐮佽</small></div>
+ </div>
+ <div class="stat-card pass">
+ <div class="stat-icon"><IconifyIcon icon="ep:success-filled" /></div>
+ <div class="stat-main"><span>璐ㄦ鍚堟牸</span><strong>{{ passTotal.toLocaleString() }}</strong><small>鍙甯稿叆鍚堟牸搴�</small></div>
+ </div>
+ <div class="stat-card fail">
+ <div class="stat-icon"><IconifyIcon icon="ep:warning-filled" /></div>
+ <div class="stat-main"><span>璐ㄦ涓嶅悎鏍�</span><strong>{{ failTotal.toLocaleString() }}</strong><small>宸叉爣璁版姤搴�</small></div>
+ </div>
+ <div class="stat-card pending">
+ <div class="stat-icon"><IconifyIcon icon="ep:clock" /></div>
+ <div class="stat-main"><span>澶勭悊涓�</span><strong>{{ processingTotal.toLocaleString() }}</strong><small>寰呭畾缁撹</small></div>
+ </div>
+ </section>
+
+ <!-- 璁板綍鍒楄〃 -->
+ <section class="record-card">
+ <div class="card-toolbar">
+ <div class="card-title">
+ <IconifyIcon icon="ep:list" />
+ 鏄庣粏鍒楄〃
+ <span class="count-chip">{{ total.toLocaleString() }} 鏉�</span>
+ </div>
+ <div class="filters">
+ <a-input v-model:value="code" allow-clear placeholder="琚嬬爜" @press-enter="searchRecords">
+ <template #prefix><IconifyIcon icon="ep:box" /></template>
+ </a-input>
+ <a-input v-model:value="batchCode" allow-clear placeholder="鐢熶骇鎵规" @press-enter="searchRecords">
+ <template #prefix><IconifyIcon icon="ep:collection-tag" /></template>
+ </a-input>
+ <a-select
+ v-model:value="result"
+ allow-clear
+ placeholder="妫�楠岀粨鏋�"
+ :options="[
+ { label: '鍚堟牸', value: 1 },
+ { label: '涓嶅悎鏍�', value: 2 },
+ { label: '澶勭悊涓�', value: 0 },
+ ]"
+ />
+ <a-button type="primary" @click="searchRecords"><IconifyIcon icon="ep:search" />鏌ヨ</a-button>
+ <a-button @click="resetFilters"><IconifyIcon icon="ep:refresh-left" />閲嶇疆</a-button>
+ <a-tooltip title="鍒锋柊鏁版嵁">
+ <a-button :loading="loading" aria-label="鍒锋柊鏁版嵁" @click="refreshAll"><IconifyIcon icon="ep:refresh" /></a-button>
+ </a-tooltip>
+ </div>
+ </div>
+
+ <a-table
+ :data-source="records"
+ :loading="loading"
+ :scroll="{ x: 1120 }"
+ :pagination="{
+ current: page,
+ pageSize,
+ total,
+ showSizeChanger: true,
+ showTotal: (t: number) => `鍏� ${t} 鏉,
+ }"
+ row-key="id"
+ size="middle"
+ @change="handleTableChange"
+ >
+ <template #emptyText>
+ <div class="empty-state">
+ <IconifyIcon icon="ep:data-board" />
+ <p>鏆傛棤璐ㄦ璁板綍</p>
+ </div>
+ </template>
+ <a-table-column key="id" title="璐ㄦ缂栧彿" data-index="id" width="100" align="right">
+ <template #default="{ record }"><span class="mono muted">{{ record.id }}</span></template>
+ </a-table-column>
+ <a-table-column key="code" title="琚嬬爜" data-index="code" min-width="220">
+ <template #default="{ record }"><span class="mono strong">{{ record.code }}</span></template>
+ </a-table-column>
+ <a-table-column key="batchCode" title="鐢熶骇鎵规" data-index="batchCode" min-width="190">
+ <template #default="{ record }">{{ record.batchCode || '鈥�' }}</template>
+ </a-table-column>
+ <a-table-column key="result" title="妫�楠岀粨鏋�" width="130">
+ <template #default="{ record }">
+ <span class="result-tag" :style="{ color: resultColor(record.result), background: resultBg(record.result) }">
+ <i class="result-dot" :style="{ background: resultColor(record.result) }" />
+ {{ resultLabel(record.result) }}
+ </span>
+ </template>
+ </a-table-column>
+ <a-table-column key="reason" title="涓嶅悎鏍煎師鍥�" data-index="reason" min-width="220" ellipsis>
+ <template #default="{ record }">
+ <span v-if="record.reason" class="reason-text">{{ record.reason }}</span>
+ <span v-else class="muted">鈥�</span>
+ </template>
+ </a-table-column>
+ <a-table-column key="inspectedAt" title="妫�楠屾椂闂�" width="190">
+ <template #default="{ record }"><span class="mono muted">{{ formatDateTime(record.inspectedAt) }}</span></template>
+ </a-table-column>
+ <a-table-column key="createTime" title="鎻愪氦鏃堕棿" width="190">
+ <template #default="{ record }"><span class="mono muted">{{ formatDateTime(record.createTime) }}</span></template>
+ </a-table-column>
+ </a-table>
+ </section>
+ </div>
+</template>
+
+<style scoped>
+.record-page { min-height: 100%; padding: 20px 24px 32px; background: #f4f7fb; color: #172033; }
+
+/* ===== 椤靛ご ===== */
+.page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; margin-bottom: 18px; }
+.header-copy .eyebrow { color: #1677ff; font-size: 12px; font-weight: 700; letter-spacing: .08em; }
+.header-copy h1 { margin: 6px 0; font-size: 26px; line-height: 1.2; }
+.header-copy p { margin: 0; color: #718096; }
+.header-badge { display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px; border: 1px solid #cfe0fb; border-radius: 999px; color: #1677ff; background: #f0f6ff; font-size: 13px; font-weight: 600; }
+.badge-pulse { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; box-shadow: 0 0 0 0 rgba(34, 197, 94, .45); animation: pulse 2s infinite; }
+@keyframes pulse { 70% { box-shadow: 0 0 0 7px rgba(34, 197, 94, 0); } 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); } }
+
+/* ===== 缁撴灉缁熻 ===== */
+.stat-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-bottom: 18px; }
+.stat-card { display: flex; align-items: center; gap: 13px; padding: 15px 17px; border: 1px solid #e8edf5; border-radius: 12px; background: #fff; box-shadow: 0 3px 12px #243b5310; }
+.stat-icon { flex: 0 0 40px; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: 10px; font-size: 21px; }
+.stat-icon :deep(.iconify) { font-size: 21px; }
+.stat-main span, .stat-main small { display: block; color: #7b8799; font-size: 12px; }
+.stat-main small { margin-top: 2px; }
+.stat-main strong { display: block; margin: 3px 0 0; font-size: 23px; line-height: 1; font-variant-numeric: tabular-nums; }
+.stat-card.total .stat-icon { color: #1677ff; background: #eaf3ff; }
+.stat-card.total .stat-main strong { color: #1677ff; }
+.stat-card.pass .stat-icon { color: #16a34a; background: #ecfdf3; }
+.stat-card.pass .stat-main strong { color: #16a34a; }
+.stat-card.fail .stat-icon { color: #dc2626; background: #fef2f2; }
+.stat-card.fail .stat-main strong { color: #dc2626; }
+.stat-card.pending .stat-icon { color: #d97706; background: #fffbeb; }
+.stat-card.pending .stat-main strong { color: #d97706; }
+
+/* ===== 鏄庣粏鍗$墖 ===== */
+.record-card { padding: 6px 16px 16px; border: 1px solid #e8edf5; border-radius: 12px; background: #fff; box-shadow: 0 3px 12px #243b5310; }
+.card-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 13px 0 15px; flex-wrap: wrap; border-bottom: 1px dashed #eef1f6; }
+.card-title { display: inline-flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 700; }
+.card-title :deep(.iconify) { color: #1677ff; }
+.count-chip { padding: 1px 9px; border-radius: 999px; background: #f0f6ff; color: #1677ff; font-size: 12px; font-weight: 600; }
+.filters { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
+.filters .ant-input { width: 150px; }
+.filters .ant-select { width: 128px; }
+.filters .ant-btn { display: inline-flex; align-items: center; gap: 5px; }
+.filters :deep(.ant-btn .iconify) { font-size: 14px; }
+
+/* ===== 鍗曞厓鏍� ===== */
+.strong { font-weight: 600; color: #172033; }
+.muted { color: #8793a5; }
+.mono { font-variant-numeric: tabular-nums; }
+.result-tag { display: inline-flex; align-items: center; gap: 6px; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; line-height: 20px; }
+.result-dot { width: 6px; height: 6px; border-radius: 50%; }
+.reason-text { color: #59677b; }
+.empty-state { padding: 34px 0; text-align: center; color: #b6c0cf; }
+.empty-state :deep(.iconify) { font-size: 42px; color: #d5dce6; }
+.empty-state p { margin: 10px 0 0; }
+
+/* 鐘舵�佹劅鐭ヨ锛氫笉鍚堟牸琛岃交楂樹寒 */
+:deep(.ant-table-row) { transition: background-color .18s ease; }
+:deep(.ant-table-row:hover) { background: #f7faff !important; }
+
+/* ===== 鍝嶅簲寮� ===== */
+@media (max-width: 1100px) { .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
+@media (max-width: 760px) {
+ .record-page { padding: 14px; }
+ .page-header { display: block; }
+ .header-badge { margin-top: 12px; }
+ .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .card-toolbar { align-items: flex-start; flex-direction: column; }
+ .filters .ant-input { width: 100%; flex: 1; }
+}
+</style>
diff --git a/src/views/wls/materialstock/components/index.ts b/src/views/wls/materialstock/components/index.ts
index 60d3bf1..7555981 100644
--- a/src/views/wls/materialstock/components/index.ts
+++ b/src/views/wls/materialstock/components/index.ts
@@ -2,3 +2,4 @@
export { default as WmMaterialStockSelect } from './select.vue';
export { default as WmStockAdjustModal } from './stock-adjust-modal.vue';
export { default as WmStockMoveModal } from './move-modal.vue';
+export { default as WmStockBagModal } from './stock-bag-modal.vue';
diff --git a/src/views/wls/materialstock/components/select-dialog.vue b/src/views/wls/materialstock/components/select-dialog.vue
index cc5e91c..87a44bd 100644
--- a/src/views/wls/materialstock/components/select-dialog.vue
+++ b/src/views/wls/materialstock/components/select-dialog.vue
@@ -22,6 +22,7 @@
mdmItemId?: number;
virtualFilter?: VirtualFilter;
warehouseId?: number;
+ warehouseType?: number;
}>(),
{
batchId: undefined,
@@ -29,6 +30,7 @@
mdmItemId: undefined,
virtualFilter: 'exclude',
warehouseId: undefined,
+ warehouseType: undefined,
},
);
@@ -163,6 +165,7 @@
itemTypeId: searchItemTypeId.value,
itemId: props.mdmItemId,
warehouseId: formValues.warehouseId ?? props.warehouseId,
+ warehouseType: props.warehouseType,
virtualFilter:
props.virtualFilter === 'all' ? undefined : props.virtualFilter,
});
diff --git a/src/views/wls/materialstock/components/select.vue b/src/views/wls/materialstock/components/select.vue
index cbd0599..a983e31 100644
--- a/src/views/wls/materialstock/components/select.vue
+++ b/src/views/wls/materialstock/components/select.vue
@@ -24,6 +24,7 @@
placeholder?: string;
virtualFilter?: 'all' | 'exclude' | 'only';
warehouseId?: number;
+ warehouseType?: number;
}>(),
{
allowClear: true,
@@ -35,6 +36,7 @@
placeholder: '璇烽�夋嫨搴撳瓨',
virtualFilter: 'exclude',
warehouseId: undefined,
+ warehouseType: undefined,
},
);
@@ -156,6 +158,7 @@
:mdm-item-id="mdmItemId"
:virtual-filter="virtualFilter"
:warehouse-id="warehouseId"
+ :warehouse-type="warehouseType"
@selected="handleSelected"
/>
</template>
diff --git a/src/views/wls/materialstock/components/stock-bag-modal.vue b/src/views/wls/materialstock/components/stock-bag-modal.vue
new file mode 100644
index 0000000..308dd35
--- /dev/null
+++ b/src/views/wls/materialstock/components/stock-bag-modal.vue
@@ -0,0 +1,63 @@
+<script lang="ts" setup>
+import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getMaterialStockBagDetails } from '#/api/mes/wm/materialstock';
+
+const stockId = ref(0);
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ columns: [
+ { field: 'bagCode', title: '琚嬬爜', minWidth: 180 },
+ { field: 'batchCode', title: '鎵规鍙�', minWidth: 150 },
+ {
+ field: 'stockState',
+ title: '搴撳瓨鐘舵��',
+ width: 110,
+ cellRender: {
+ name: 'CellTag',
+ props: {
+ options: [
+ { label: '鏆傚瓨搴�', value: 10, color: 'orange' },
+ { label: '鍚堟牸搴�', value: 20, color: 'green' },
+ { label: '宸插嚭搴�', value: 30, color: 'default' },
+ ],
+ },
+ },
+ },
+ { field: 'quantity', title: '鏁伴噺', width: 80 },
+ { field: 'inboundTime', title: '鍏ュ簱鏃堕棿', width: 180 },
+ ],
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }) => getMaterialStockBagDetails({
+ materialStockId: stockId.value,
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ }),
+ },
+ },
+ rowConfig: { keyField: 'id' },
+ },
+});
+
+const [Modal, modalApi] = useVbenModal({
+ onOpenChange(isOpen) {
+ if (!isOpen) return;
+ const data = modalApi.getData<{ materialStockId?: number }>();
+ stockId.value = data?.materialStockId ?? 0;
+ gridApi.query();
+ },
+});
+
+defineExpose({ open: (materialStockId: number) => modalApi.setData({ materialStockId }).open() });
+</script>
+
+<template>
+ <Modal title="搴撳瓨琚嬬爜鏄庣粏" class="w-[900px]">
+ <Grid />
+ </Modal>
+</template>
diff --git a/src/views/wls/materialstock/data.ts b/src/views/wls/materialstock/data.ts
index 1edf653..aacee75 100644
--- a/src/views/wls/materialstock/data.ts
+++ b/src/views/wls/materialstock/data.ts
@@ -183,6 +183,11 @@
slots: { default: 'batchCode' },
},
{
+ field: 'bagCount',
+ title: '琚嬬爜鏁�',
+ width: 90,
+ },
+ {
field: 'warehouseName',
title: '浠撳簱',
minWidth: 100,
@@ -219,16 +224,12 @@
},
},
},
- ...(onMove
- ? [
- {
- title: '鎿嶄綔',
- width: 80,
- fixed: 'right' as const,
- slots: { default: 'actions' } as const,
- },
- ]
- : []),
+ {
+ title: '鎿嶄綔',
+ width: onMove ? 150 : 90,
+ fixed: 'right' as const,
+ slots: { default: 'actions' } as const,
+ },
];
}
diff --git a/src/views/wls/materialstock/index.vue b/src/views/wls/materialstock/index.vue
index 17e3625..6fc3d37 100644
--- a/src/views/wls/materialstock/index.vue
+++ b/src/views/wls/materialstock/index.vue
@@ -17,9 +17,13 @@
} from '#/api/mes/wm/materialstock';
import { $t } from '#/locales';
import { MdItemTypeTree } from '#/views/mes/md/item/type/components';
-import { WmBatchDetail } from '#/views/wls/batch/components';
+import QrTraceModal from '#/views/mes/pro/components/qr-trace-modal.vue';
import AreaForm from '#/views/wls/warehouse/area/modules/form.vue';
-import { WmStockAdjustModal, WmStockMoveModal } from './components';
+import {
+ WmStockAdjustModal,
+ WmStockBagModal,
+ WmStockMoveModal,
+} from './components';
import { useGridColumns, useGridFormSchema } from './data';
@@ -38,7 +42,20 @@
destroyOnClose: true,
});
-const batchDetailRef = ref<InstanceType<typeof WmBatchDetail>>();
+const [QrTraceQueryModal, qrTraceQueryModalApi] = useVbenModal({
+ connectedComponent: QrTraceModal,
+ destroyOnClose: true,
+});
+
+const [StockBagModal, stockBagModalApi] = useVbenModal({
+ connectedComponent: WmStockBagModal,
+ destroyOnClose: true,
+});
+
+function handleOpenStockBags(row: MesWmMaterialStockApi.MaterialStock) {
+ if (!row.id) return;
+ stockBagModalApi.setData({ materialStockId: row.id }).open();
+}
/** 鍒锋柊琛ㄦ牸 */
function handleRefresh() {
@@ -89,12 +106,12 @@
gridApi.query();
}
-/** 鎵撳紑鎵规璇︽儏寮圭獥 */
+/** 鎵撳紑鎵规杩芥函寮圭獥 */
function handleOpenBatchDetail(row: MesWmMaterialStockApi.MaterialStock) {
- if (!row.batchId) {
+ if (!row.batchCode) {
return;
}
- batchDetailRef.value?.open(row.batchId);
+ qrTraceQueryModalApi.setData({ code: row.batchCode }).open();
}
/** 澶勭悊鍐荤粨鐘舵�佸垏鎹� */
@@ -151,8 +168,10 @@
</script>
<template>
- <Page auto-content-height><AreaModal />
- <WmBatchDetail ref="batchDetailRef" />
+ <Page auto-content-height>
+ <AreaModal />
+ <QrTraceQueryModal />
+ <StockBagModal />
<StockAdjustModal @success="handleStockAdjustSuccess" />
<StockMoveModal @success="handleStockMoveSuccess" />
@@ -212,6 +231,12 @@
<TableAction
:actions="[
{
+ label: '鏌ョ湅琚嬬爜',
+ type: 'link',
+ ifShow: row.bagDetailSupported,
+ onClick: handleOpenStockBags.bind(null, row),
+ },
+ {
label: '绉诲簱',
type: 'link',
auth: ['mes:wm-material-stock:move'],
diff --git a/src/views/wls/productsales/data.ts b/src/views/wls/productsales/data.ts
index 4834c35..9a4ac8b 100644
--- a/src/views/wls/productsales/data.ts
+++ b/src/views/wls/productsales/data.ts
@@ -13,6 +13,7 @@
DICT_TYPE,
MesAutoCodeRuleCode,
MesWmSalesNoticeStatusEnum,
+ MesWmWarehouseTypeEnum,
} from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
@@ -21,7 +22,6 @@
import { z } from '#/adapter/form';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
-import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components';
import { getRangePickerDefaultProps } from '#/utils';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { WmMaterialStockSelect } from '#/views/wls/materialstock/components';
@@ -125,52 +125,16 @@
component: markRaw(WmSalesNoticeSelect),
componentProps: {
disabled: isHeaderReadonly(formType),
- // 閫夋嫨鍙戣揣閫氱煡鍗曞悗锛岃嚜鍔ㄥ洖濉攢鍞鍗曞彿銆佸鎴枫�佹敹璐т汉淇℃伅
+ // 閫夋嫨鍙戣揣閫氱煡鍗曞悗锛岃嚜鍔ㄥ洖濉鎴枫�佹敹璐т汉淇℃伅
onChange: async (notice?: MesWmSalesNoticeApi.SalesNotice) => {
await formApi?.setValues({
clientId: notice?.clientId,
contactAddress: notice?.recipientAddress,
contactName: notice?.recipientName,
contactTelephone: notice?.recipientTelephone,
- salesOrderCode: notice?.salesOrderCode,
});
},
status: MesWmSalesNoticeStatusEnum.APPROVED,
- },
- },
- {
- fieldName: 'saleOrderId',
- label: '閿�鍞鍗曠紪鍙�',
- component: markRaw(ErpSaleOrderSelect),
- componentProps: {
- disabled: isHeaderReadonly(formType),
- placeholder: '璇烽�夋嫨閿�鍞鍗�',
- onChange: (item: any) => {
- if (formApi) {
- formApi.setFieldValue('salesOrderCode', item?.no ?? '');
- if (item?.customerId) {
- formApi.setFieldValue('clientId', item.customerId);
- }
- }
- },
- },
- rules: 'selectRequired',
- dependencies: {
- triggerFields: ['salesOrderCode'],
- if: () => formType === 'create',
- },
- },
- {
- fieldName: 'salesOrderCode',
- label: '閿�鍞鍗曠紪鍙�',
- component: 'Input',
- componentProps: {
- disabled: true,
- placeholder: '閿�鍞鍗曠紪鍙�',
- },
- dependencies: {
- triggerFields: [''],
- if: () => formType !== 'create',
},
},
{
@@ -285,15 +249,6 @@
},
},
{
- fieldName: 'salesOrderCode',
- label: '閿�鍞鍗曠紪鍙�',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ラ攢鍞鍗曠紪鍙�',
- },
- },
- {
fieldName: 'clientId',
label: '瀹㈡埛',
component: markRaw(CrmCustomerSelect),
@@ -343,11 +298,6 @@
{
field: 'noticeCode',
title: '鍙戣揣閫氱煡鍗曞彿',
- minWidth: 160,
- },
- {
- field: 'salesOrderCode',
- title: '閿�鍞鍗曠紪鍙�',
minWidth: 160,
},
{
@@ -651,6 +601,8 @@
label: '閫夋嫨搴撳瓨',
component: markRaw(WmMaterialStockSelect),
componentProps: {
+ // 鍙粠鍚堟牸浠撳簱锛坱ype=20锛夐�夋嫨鍙嫞璐у簱瀛�
+ warehouseType: MesWmWarehouseTypeEnum.QUALIFIED,
// 閫夋嫨搴撳瓨璁板綍鍚庯紝鑷姩鍥炲~浠撳簱/搴撳尯/搴撲綅/鎵规/鏁伴噺
onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => {
await formApi?.setValues({
diff --git a/src/views/wls/returnsales/data.ts b/src/views/wls/returnsales/data.ts
index 9b61d3c..c45c5f9 100644
--- a/src/views/wls/returnsales/data.ts
+++ b/src/views/wls/returnsales/data.ts
@@ -1,4 +1,3 @@
-import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmReturnSalesApi } from '#/api/mes/wm/returnsales';
@@ -15,7 +14,6 @@
import { z } from '#/adapter/form';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
-import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { WmBatchSelect } from '#/views/wls/batch/components';
import {
@@ -36,7 +34,6 @@
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
- onOrderChange?: (item: Record<string, unknown> | undefined) => void,
): VbenFormSchema[] {
return [
{
@@ -88,33 +85,6 @@
placeholder: '璇疯緭鍏ラ��璐у崟鍚嶇О',
},
rules: 'required',
- },
- {
- fieldName: 'salesOrderCode',
- component: 'Input',
- dependencies: {
- triggerFields: [''],
- show: () => false,
- },
- },
- {
- fieldName: 'saleOrderId',
- label: '閿�鍞鍗曞彿',
- component: markRaw(ErpSaleOrderSelect),
- componentProps: {
- outStatus: 3,
- placeholder: '璇烽�夋嫨閿�鍞鍗�',
- onChange: (item: any) => {
- if (formApi) {
- formApi.setFieldValue('salesOrderCode', item?.no ?? '');
- if (item?.customerId) {
- formApi.setFieldValue('clientId', item.customerId);
- }
- }
- onOrderChange?.(item);
- },
- },
- rules: 'selectRequired',
},
{
fieldName: 'clientId',
@@ -183,15 +153,6 @@
},
},
{
- fieldName: 'salesOrderCode',
- label: '閿�鍞鍗曞彿',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ラ攢鍞鍗曞彿',
- },
- },
- {
fieldName: 'clientId',
label: '瀹㈡埛',
component: markRaw(CrmCustomerSelect),
@@ -225,11 +186,6 @@
field: 'name',
title: '閫�璐у崟鍚嶇О',
minWidth: 150,
- },
- {
- field: 'salesOrderCode',
- title: '閿�鍞鍗曞彿',
- minWidth: 140,
},
{
field: 'clientCode',
@@ -343,34 +299,15 @@
}
/** 閫�璐у崟琛屾柊澧�/淇敼鐨勮〃鍗� */
-export function useLineFormSchema(
- clientId?: number,
- salesOrderCode?: string,
- items?: ErpSaleOrderApi.SaleOrderItem[],
- onItemChange?: (item: ErpSaleOrderApi.SaleOrderItem | undefined) => void,
-): VbenFormSchema[] {
- const hasItems = items && items.length > 0;
+export function useLineFormSchema(clientId?: number): VbenFormSchema[] {
return [
{
fieldName: 'itemId',
label: '鐗╂枡',
- component: hasItems ? 'Select' : markRaw(MdItemSelect),
- componentProps: hasItems
- ? {
- placeholder: '璇烽�夋嫨鐗╂枡',
- options: items.map((item) => ({
- label: `${item.productName || ''}锛�${item.productBarCode || ''}锛塦,
- value: item.productId,
- })),
- onChange: (value: number | undefined) => {
- const selected = items.find((i) => i.productId === value);
- onItemChange?.(selected);
- },
- }
- : {
- placeholder: '璇烽�夋嫨鐗╂枡',
- productIds: [],
- },
+ component: markRaw(MdItemSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨鐗╂枡',
+ },
rules: 'selectRequired',
},
{
@@ -383,30 +320,6 @@
placeholder: '璇疯緭鍏ラ��璐ф暟閲�',
precision: 2,
},
- dependencies: {
- triggerFields: ['itemId'],
- componentProps: (values) => {
- const selected = items?.find((i) => i.productId === values.itemId);
- return {
- class: '!w-full',
- max: selected?.count ?? undefined,
- min: 0,
- placeholder: '璇疯緭鍏ラ��璐ф暟閲�',
- precision: 2,
- };
- },
- rules: (values) => {
- const selected = items?.find((i) => i.productId === values.itemId);
- const max = selected?.count;
- if (max !== undefined && max !== null) {
- return z
- .number({ message: '璇疯緭鍏ラ��璐ф暟閲�' })
- .min(0)
- .max(max, `鍙��璐ф暟閲忎笉鑳借秴杩� ${max}`);
- }
- return z.number({ message: '璇疯緭鍏ラ��璐ф暟閲�' }).min(0);
- },
- },
rules: 'required',
},
{
@@ -417,7 +330,6 @@
clientId,
disabled: true,
placeholder: '璇烽�夋嫨鎵规',
- salesOrderCode,
},
dependencies: {
triggerFields: ['itemId'],
@@ -426,7 +338,6 @@
disabled: true,
itemId: values.itemId,
placeholder: '璇烽�夋嫨鎵规',
- salesOrderCode,
}),
},
},
diff --git a/src/views/wls/returnsales/modules/form.vue b/src/views/wls/returnsales/modules/form.vue
index 2f57364..61978eb 100644
--- a/src/views/wls/returnsales/modules/form.vue
+++ b/src/views/wls/returnsales/modules/form.vue
@@ -19,9 +19,6 @@
submitReturnSales,
updateReturnSales,
} from '#/api/mes/wm/returnsales';
-import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
-
-import { getSaleOrder, getSaleOrderPage } from '#/api/erp/sale/order';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
@@ -31,7 +28,6 @@
const formType = ref<FormType>('create');
const formData = ref<MesWmReturnSalesApi.ReturnSales>();
const originalSnapshot = ref(''); // 琛ㄥ崟鍘熷鏁版嵁蹇収锛岀敤浜庢彁浜ゆ椂璺宠繃鏈彉鏇寸殑淇濆瓨璇锋眰
-const saleOrderItems = ref<ErpSaleOrderApi.SaleOrderItem[]>([]); // 褰撳墠閿�鍞鍗曠殑鐗╂枡琛�
const isEditable = computed(() =>
// 鏄惁涓虹紪杈戞ā寮忥紙鍙繚瀛橈級
['create', 'update'].includes(formType.value),
@@ -173,38 +169,13 @@
// 鍔犺浇鏁版嵁
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
- formApi.setState({
- schema: useFormSchema(formType.value, formApi, (item) => {
- if (item?.id) {
- getSaleOrder(item.id as number).then((order) => {
- saleOrderItems.value = (order as ErpSaleOrderApi.SaleOrder)?.items || [];
- });
- } else {
- saleOrderItems.value = [];
- }
- }),
- });
+ formApi.setState({ schema: useFormSchema(formType.value, formApi) });
formApi.setDisabled(!isEditable.value);
modalApi.setState({ showConfirmButton: isEditable.value });
if (data?.id) {
modalApi.lock();
try {
formData.value = await getReturnSales(data.id);
- // 閫氳繃閿�鍞鍗曞彿鍙嶆煡璁㈠崟ID锛岀‘淇� ErpSaleOrderSelect 鑳芥纭洖鏄�
- if (formData.value?.salesOrderCode && !formData.value.saleOrderId) {
- const orderPage = await getSaleOrderPage({
- pageNo: 1,
- pageSize: 1,
- no: formData.value.salesOrderCode,
- } as Record<string, unknown> as never);
- const list = (orderPage as Record<string, unknown>)?.list as Array<Record<string, unknown>> | undefined;
- if (list?.length) {
- const orderId = list[0].id as number;
- (formData.value as Record<string, unknown>).saleOrderId = orderId;
- const order = await getSaleOrder(orderId);
- saleOrderItems.value = order?.items || [];
- }
- }
// 璁剧疆鍒� values
await formApi.setValues(formData.value);
} finally {
@@ -227,8 +198,6 @@
:client-id="formData.clientId"
:form-type="formType"
:return-id="formData.id"
- :sale-order-items="saleOrderItems"
- :sales-order-code="formData.salesOrderCode"
/>
</div>
</template>
diff --git a/src/views/wls/returnsales/modules/line-form.vue b/src/views/wls/returnsales/modules/line-form.vue
index 06adfaf..0b574d2 100644
--- a/src/views/wls/returnsales/modules/line-form.vue
+++ b/src/views/wls/returnsales/modules/line-form.vue
@@ -1,5 +1,4 @@
<script lang="ts" setup>
-import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
import type { MesWmReturnSalesLineApi } from '#/api/mes/wm/returnsales/line';
import { computed, ref } from 'vue';
@@ -9,7 +8,6 @@
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
-import { getBatchPage } from '#/api/mes/wm/batch';
import {
createReturnSalesLine,
getReturnSalesLine,
@@ -22,7 +20,6 @@
const emit = defineEmits(['success']);
const formData = ref<MesWmReturnSalesLineApi.ReturnSalesLine>();
const returnId = ref<number>(); // 鎵�灞為��璐у崟缂栧彿
-const maxQuantity = ref<number>(); // 褰撳墠閫変腑鐗╂枡鍙��璐х殑鏈�澶ф暟閲�
const getTitle = computed(() => {
return formData.value?.id
@@ -76,39 +73,10 @@
const data = modalApi.getData<{
clientId?: number;
id?: number;
- items?: ErpSaleOrderApi.SaleOrderItem[];
returnId: number;
- salesOrderCode?: string;
}>();
returnId.value = data.returnId;
- maxQuantity.value = undefined;
- formApi.setState({
- schema: useLineFormSchema(
- data.clientId,
- data.salesOrderCode,
- data.items || [],
- async (item) => {
- maxQuantity.value = item?.count;
- // 閲嶇疆鎵规
- await formApi.setFieldValue('batchId', undefined);
- if (item?.batchCode) {
- try {
- const batchPage = await getBatchPage({
- code: item.batchCode,
- pageNo: 1,
- pageSize: 1,
- });
- const batchList = batchPage?.list;
- if (batchList?.length) {
- await formApi.setFieldValue('batchId', batchList[0].id);
- }
- } catch {
- // 鏌ヨ澶辫触鏃跺拷鐣�
- }
- }
- },
- ),
- });
+ formApi.setState({ schema: useLineFormSchema(data.clientId) });
if (!data.id) {
return;
}
@@ -127,11 +95,5 @@
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
- <div
- v-if="maxQuantity !== undefined"
- class="mx-4 mt-2 text-sm text-blue-600"
- >
- 褰撳墠鐗╂枡鍙��璐ф暟閲忥細<span class="font-bold">{{ maxQuantity }}</span>
- </div>
</Modal>
</template>
diff --git a/src/views/wls/returnsales/modules/line-list.vue b/src/views/wls/returnsales/modules/line-list.vue
index 9c635e2..26da0d9 100644
--- a/src/views/wls/returnsales/modules/line-list.vue
+++ b/src/views/wls/returnsales/modules/line-list.vue
@@ -1,7 +1,6 @@
<script lang="ts" setup>
import type { FormType } from '../data';
-import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmReturnSalesDetailApi } from '#/api/mes/wm/returnsales/detail';
import type { MesWmReturnSalesLineApi } from '#/api/mes/wm/returnsales/line';
@@ -30,8 +29,6 @@
clientId?: number;
formType: FormType;
returnId: number;
- saleOrderItems?: ErpSaleOrderApi.SaleOrderItem[];
- salesOrderCode?: string;
}>();
const isEditable = computed(() =>
@@ -66,9 +63,7 @@
lineFormModalApi
.setData({
clientId: props.clientId,
- items: props.saleOrderItems,
returnId: props.returnId,
- salesOrderCode: props.salesOrderCode,
})
.open();
}
@@ -79,9 +74,7 @@
.setData({
clientId: props.clientId,
id: row.id,
- items: props.saleOrderItems,
returnId: props.returnId,
- salesOrderCode: props.salesOrderCode,
})
.open();
}
diff --git a/src/views/wls/salesnotice/components/select-dialog.vue b/src/views/wls/salesnotice/components/select-dialog.vue
index 3205536..58e933a 100644
--- a/src/views/wls/salesnotice/components/select-dialog.vue
+++ b/src/views/wls/salesnotice/components/select-dialog.vue
@@ -46,15 +46,6 @@
},
},
{
- fieldName: 'saleOrderCode',
- label: '閿�鍞鍗曠紪鍙�',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ラ攢鍞鍗曠紪鍙�',
- },
- },
- {
fieldName: 'clientId',
label: '瀹㈡埛',
component: markRaw(CrmCustomerSelect),
@@ -100,11 +91,6 @@
field: 'name',
title: '閫氱煡鍗曞悕绉�',
minWidth: 150,
- },
- {
- field: 'saleOrderCode',
- title: '閿�鍞鍗曠紪鍙�',
- minWidth: 140,
},
{
field: 'clientName',
diff --git a/src/views/wls/salesnotice/components/select.vue b/src/views/wls/salesnotice/components/select.vue
index 744b348..2208925 100644
--- a/src/views/wls/salesnotice/components/select.vue
+++ b/src/views/wls/salesnotice/components/select.vue
@@ -119,7 +119,6 @@
<div>缂栧彿锛歿{ selectedItem.code || '-' }}</div>
<div>鍚嶇О锛歿{ selectedItem.name || '-' }}</div>
<div>瀹㈡埛锛歿{ selectedItem.clientName || '-' }}</div>
- <div>閿�鍞鍗曪細{{ selectedItem.saleOrderCode || '-' }}</div>
</div>
</template>
<Input
diff --git a/src/views/wls/salesnotice/data.ts b/src/views/wls/salesnotice/data.ts
index 6818446..dc2d2a7 100644
--- a/src/views/wls/salesnotice/data.ts
+++ b/src/views/wls/salesnotice/data.ts
@@ -8,6 +8,7 @@
import {
DICT_TYPE,
MesAutoCodeRuleCode,
+ MesWmWarehouseTypeEnum,
} from '@vben/constants';
import { Button } from 'ant-design-vue';
@@ -15,8 +16,11 @@
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
-import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components';
-import { MdItemSelect } from '#/views/mes/md/item/components';
+import {
+ getStockSummaryByBatch,
+ getStockSummaryByItem,
+ type MesWmMaterialStockApi,
+} from '#/api/mes/wm/materialstock';
/** 琛ㄥ崟绫诲瀷 */
export type FormType = 'create' | 'detail' | 'update';
@@ -83,26 +87,11 @@
rules: 'required',
},
{
- fieldName: 'saleOrderId',
- label: '閿�鍞鍗�',
- component: markRaw(ErpSaleOrderSelect),
- componentProps: {
- placeholder: '璇烽�夋嫨閿�鍞鍗�',
- // 閫夋嫨閿�鍞鍗曞悗锛岃嚜鍔ㄥ洖濉鎴蜂俊鎭�
- onChange: (item: any) => {
- if (item?.customerId && formApi) {
- formApi.setFieldValue('clientId', item.customerId);
- }
- },
- },
- },
- {
fieldName: 'clientId',
label: '瀹㈡埛',
component: markRaw(CrmCustomerSelect),
componentProps: {
placeholder: '璇烽�夋嫨瀹㈡埛',
- disabled: true,
},
rules: 'selectRequired',
},
@@ -177,15 +166,6 @@
},
},
{
- fieldName: 'saleOrderCode',
- label: '閿�鍞鍗曠紪鍙�',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ラ攢鍞鍗曠紪鍙�',
- },
- },
- {
fieldName: 'clientId',
label: '瀹㈡埛',
component: markRaw(CrmCustomerSelect),
@@ -209,11 +189,6 @@
field: 'name',
title: '閫氱煡鍗曞悕绉�',
minWidth: 150,
- },
- {
- field: 'saleOrderCode',
- title: '閿�鍞鍗曠紪鍙�',
- minWidth: 140,
},
{
field: 'clientName',
@@ -349,15 +324,98 @@
];
}
-/** 閫氱煡鍗曡鏂板/淇敼鐨勮〃鍗� */
+/** 姝e紡搴擄紙鍚堟牸搴撳簱瀛橈紝stockState = 20锛夋湁搴撳瓨鐨勭墿鏂欓�夐」 */
+export async function loadQualifiedStockItemOptions(): Promise<
+ { label: string; value: number }[]
+> {
+ const list = await getStockSummaryByItem({
+ stockState: 20,
+ warehouseType: MesWmWarehouseTypeEnum.QUALIFIED,
+ });
+ return list.map((item: MesWmMaterialStockApi.MaterialStockSummary) => ({
+ label: [item.itemName, item.itemCode, `搴撳瓨 ${item.totalQuantity}`]
+ .filter(Boolean)
+ .join(' '),
+ value: item.itemId as number,
+ }));
+}
+
+/** 鎸囧畾鐗╂枡鐨勬寮忓簱瀛樻壒娆¢�夐」 */
+export async function loadQualifiedStockBatchOptions(
+ itemId: number,
+): Promise<{ label: string; value: number }[]> {
+ const list = await getStockSummaryByBatch({
+ stockState: 20,
+ itemId,
+ warehouseType: MesWmWarehouseTypeEnum.QUALIFIED,
+ });
+ return list.map((item: MesWmMaterialStockApi.MaterialStockSummary) => ({
+ label: [item.batchCode, `搴撳瓨 ${item.totalQuantity}`].filter(Boolean).join(' '),
+ value: item.batchId as number,
+ }));
+}
+
+/**
+ * 鍙彂璐ф暟閲忥細鎵�閫夌墿鏂�/鎵规鐨勬寮忓簱瀛橈紙stockState = 20锛夋眹鎬汇��
+ *
+ * 鎸囧畾鎵规鏃剁簿纭埌璇ユ壒娆★紝鍚﹀垯杩斿洖鐗╂枡鍏ㄩ儴姝e紡搴撳瓨銆�
+ */
+export async function loadAvailableQuantity(
+ itemId?: number,
+ batchId?: number,
+): Promise<number> {
+ if (!itemId) {
+ return 0;
+ }
+ if (batchId) {
+ const list = await getStockSummaryByBatch({
+ stockState: 20,
+ itemId,
+ warehouseType: MesWmWarehouseTypeEnum.QUALIFIED,
+ });
+ return list.find((item) => item.batchId === batchId)?.totalQuantity ?? 0;
+ }
+ const list = await getStockSummaryByItem({
+ stockState: 20,
+ itemId,
+ warehouseType: MesWmWarehouseTypeEnum.QUALIFIED,
+ });
+ return list.find((item) => item.itemId === itemId)?.totalQuantity ?? 0;
+}
+
+/**
+ * 閫氱煡鍗曡鏂板/淇敼鐨勮〃鍗曘��
+ *
+ * 鐗╂枡銆佹壒娆″潎浠庢寮忓簱搴撳瓨锛坰tockState = 20锛変腑閫夋嫨锛�
+ * 鎵规閫夐」闅忕墿鏂欒仈鍔ㄥ姞杞斤紝鍙敤搴撳瓨涓嶈冻鐢卞悗绔繚瀛樻椂鏍¢獙銆�
+ */
export function useLineFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'itemId',
label: '鐗╂枡',
- component: markRaw(MdItemSelect),
+ component: 'Select',
componentProps: {
- placeholder: '璇烽�夋嫨鐗╂枡',
+ allowClear: true,
+ filterOption: true,
+ optionFilterProp: 'label',
+ options: [],
+ placeholder: '璇烽�夋嫨鐗╂枡锛堜粎姝e紡搴撳簱瀛橈級',
+ showSearch: true,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'batchId',
+ label: '鎵规',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ filterOption: true,
+ optionFilterProp: 'label',
+ options: [],
+ placeholder: '璇烽�夋嫨鎵规锛堜粎姝e紡搴撳簱瀛橈級',
+ showSearch: true,
},
rules: 'selectRequired',
},
@@ -368,17 +426,18 @@
componentProps: {
class: '!w-full',
min: 0.01,
- placeholder: '璇疯緭鍏ュ彂璐ф暟閲�',
+ placeholder: '璇疯緭鍏ュ彂璐ф暟閲忥紙涓嶅彲瓒呰繃姝e紡搴撳瓨锛�',
precision: 2,
},
rules: 'required',
},
{
- fieldName: 'batchCode',
- label: '鎵规鍙�',
+ fieldName: 'availableQuantity',
+ label: '鍙彂璐ф暟閲�',
component: 'Input',
componentProps: {
- placeholder: '璇疯緭鍏ユ壒娆″彿',
+ disabled: true,
+ placeholder: '閫夋嫨鐗╂枡/鎵规鍚庤嚜鍔ㄥ甫鍑�',
},
},
{
diff --git a/src/views/wls/salesnotice/modules/form.vue b/src/views/wls/salesnotice/modules/form.vue
index 044cb38..0f8266b 100644
--- a/src/views/wls/salesnotice/modules/form.vue
+++ b/src/views/wls/salesnotice/modules/form.vue
@@ -1,7 +1,6 @@
<script lang="ts" setup>
import type { FormType } from '../data';
-import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
import type { MesWmSalesNoticeApi } from '#/api/mes/wm/salesnotice';
import { computed, ref } from 'vue';
@@ -18,8 +17,6 @@
submitSalesNotice,
updateSalesNotice,
} from '#/api/mes/wm/salesnotice';
-import { createSalesNoticeLine } from '#/api/mes/wm/salesnotice/line';
-import { getSaleOrderItemListByOrderId } from '#/api/erp/sale/order';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
@@ -110,29 +107,6 @@
}
// 鏂板锛氫繚鎸佸脊绐楁墦寮�锛屽垏鎹㈠埌缂栬緫妯″紡
const id = await createSalesNotice(data);
- // 閫夋嫨浜嗛攢鍞鍗曟椂锛岃嚜鍔ㄥ甫鍏ヨ鍗曚骇鍝佹槑缁�
- if (data.saleOrderId) {
- try {
- const items = await getSaleOrderItemListByOrderId(data.saleOrderId);
- if (items?.length) {
- await Promise.all(items.map((item: ErpSaleOrderApi.SaleOrderItem) =>
- createSalesNoticeLine({
- noticeId: id,
- saleOrderItemId: item.id,
- itemId: item.productId,
- itemCode: item.productBarCode,
- itemName: item.productName,
- unitMeasureName: item.productUnitName,
- quantity: item.count,
- batchCode: item.batchCode,
- oqcCheckFlag: false,
- }),
- ));
- }
- } catch {
- // 鑾峰彇璁㈠崟鏄庣粏澶辫触涓嶅奖鍝嶄富娴佺▼
- }
- }
formData.value = {
...data,
id,
diff --git a/src/views/wls/salesnotice/modules/line-form.vue b/src/views/wls/salesnotice/modules/line-form.vue
index f33e49a..ddc7c5e 100644
--- a/src/views/wls/salesnotice/modules/line-form.vue
+++ b/src/views/wls/salesnotice/modules/line-form.vue
@@ -15,7 +15,12 @@
} from '#/api/mes/wm/salesnotice/line';
import { $t } from '#/locales';
-import { useLineFormSchema } from '../data';
+import {
+ loadAvailableQuantity,
+ loadQualifiedStockBatchOptions,
+ loadQualifiedStockItemOptions,
+ useLineFormSchema,
+} from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmSalesNoticeLineApi.SalesNoticeLine>();
@@ -26,6 +31,26 @@
? $t('ui.actionTitle.edit', ['鍙戣揣閫氱煡鍗曡'])
: $t('ui.actionTitle.create', ['鍙戣揣閫氱煡鍗曡']);
});
+
+async function updateBatchOptions(itemId?: number) {
+ const batchOptions = itemId
+ ? await loadQualifiedStockBatchOptions(itemId)
+ : [];
+ formApi.updateSchema([
+ {
+ fieldName: 'batchId',
+ componentProps: { options: batchOptions },
+ },
+ ]);
+}
+
+async function refreshAvailableQuantity(itemId?: number, batchId?: number) {
+ const quantity = await loadAvailableQuantity(itemId, batchId);
+ await formApi.setFieldValue(
+ 'availableQuantity',
+ quantity > 0 ? `${quantity}` : undefined,
+ );
+}
const [Form, formApi] = useVbenForm({
commonConfig: {
@@ -38,6 +63,32 @@
layout: 'horizontal',
schema: useLineFormSchema(),
showDefaultActions: false,
+ handleValuesChange: async (values, fieldsChanged) => {
+ // setValues 缂栬緫鍥炲~鏃� itemId/batchId 鍚屾椂鍙樺寲锛屼笉娓呮壒娆′粎鍒锋柊閫夐」
+ const itemChanged = fieldsChanged.includes('itemId');
+ const batchChanged = fieldsChanged.includes('batchId');
+ if (itemChanged && batchChanged) {
+ await updateBatchOptions(values.itemId as number | undefined);
+ await refreshAvailableQuantity(
+ values.itemId as number | undefined,
+ values.batchId as number | undefined,
+ );
+ return;
+ }
+ if (itemChanged) {
+ await formApi.setFieldValue('batchId', undefined);
+ await updateBatchOptions(values.itemId as number | undefined);
+ await refreshAvailableQuantity(values.itemId as number | undefined);
+ return;
+ }
+ if (batchChanged) {
+ await updateBatchOptions(values.itemId as number | undefined);
+ await refreshAvailableQuantity(
+ values.itemId as number | undefined,
+ values.batchId as number | undefined,
+ );
+ }
+ },
wrapperClass: 'grid-cols-3',
});
@@ -48,9 +99,12 @@
return;
}
modalApi.lock();
- // 鎻愪氦琛ㄥ崟
- const data =
- (await formApi.getValues()) as MesWmSalesNoticeLineApi.SalesNoticeLine;
+ // 鎻愪氦琛ㄥ崟锛堝彲鍙戣揣鏁伴噺浠呬綔鎻愮ず锛屼笉鎻愪氦鍒板悗绔級
+ const values = (await formApi.getValues()) as MesWmSalesNoticeLineApi.SalesNoticeLine & {
+ availableQuantity?: unknown;
+ };
+ delete values.availableQuantity;
+ const data = values;
data.noticeId = noticeId.value;
try {
await (formData.value?.id
@@ -60,6 +114,15 @@
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
+ } catch (error) {
+ const response = error as {
+ response?: { data?: { message?: string; msg?: string } };
+ };
+ message.error(
+ response.response?.data?.message ??
+ response.response?.data?.msg ??
+ '淇濆瓨澶辫触锛岃妫�鏌ユ寮忓簱搴撳瓨鏄惁鍏呰冻',
+ );
} finally {
modalApi.unlock();
}
@@ -72,14 +135,27 @@
// 鍔犺浇鏁版嵁
const data = modalApi.getData<{ id?: number; noticeId: number }>();
noticeId.value = data.noticeId;
- if (!data.id) {
- return;
- }
modalApi.lock();
try {
+ // 鐗╂枡閫夐」锛氫粎灞曠ず姝e紡搴撴湁搴撳瓨鐨勭墿鏂�
+ const itemOptions = await loadQualifiedStockItemOptions();
+ formApi.updateSchema([
+ {
+ fieldName: 'itemId',
+ componentProps: { options: itemOptions },
+ },
+ ]);
+ if (!data.id) {
+ return;
+ }
formData.value = await getSalesNoticeLine(data.id);
- // 璁剧疆鍒� values
+ // 璁剧疆鍒� values锛岃仈鍔ㄥ姞杞芥壒娆¢�夐」
await formApi.setValues(formData.value);
+ await updateBatchOptions(formData.value?.itemId);
+ await refreshAvailableQuantity(
+ formData.value?.itemId,
+ formData.value?.batchId,
+ );
} finally {
modalApi.unlock();
}
diff --git a/src/views/wls/transaction/data.ts b/src/views/wls/transaction/data.ts
index 117aa3f..b0a257f 100644
--- a/src/views/wls/transaction/data.ts
+++ b/src/views/wls/transaction/data.ts
@@ -177,6 +177,7 @@
field: 'batchCode',
title: '鎵规鍙�',
width: 120,
+ slots: { default: 'batchCode' },
},
{
field: 'quantity',
diff --git a/src/views/wls/transaction/index.vue b/src/views/wls/transaction/index.vue
index 8bd48e4..75a3630 100644
--- a/src/views/wls/transaction/index.vue
+++ b/src/views/wls/transaction/index.vue
@@ -2,14 +2,28 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmTransactionApi } from '#/api/mes/wm/transaction';
-import { Page } from '@vben/common-ui';
+import { Page, useVbenModal } from '@vben/common-ui';
-import { Tag } from 'ant-design-vue';
+import { Button, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getTransactionPage } from '#/api/mes/wm/transaction';
+import QrTraceModal from '#/views/mes/pro/components/qr-trace-modal.vue';
+
import { useGridColumns, useGridFormSchema } from './data';
+
+const [QrTraceQueryModal, qrTraceQueryModalApi] = useVbenModal({
+ connectedComponent: QrTraceModal,
+ destroyOnClose: true,
+});
+
+function handleBatchClick(row: MesWmTransactionApi.Transaction) {
+ if (!row.batchCode) {
+ return;
+ }
+ qrTraceQueryModalApi.setData({ code: row.batchCode }).open();
+}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
@@ -50,7 +64,19 @@
<template>
<Page auto-content-height>
+ <QrTraceQueryModal />
<Grid table-title="搴撳瓨浜嬪姟娴佹按">
+ <template #batchCode="{ row }">
+ <Button
+ v-if="row.batchCode"
+ type="link"
+ class="!px-0"
+ @click="handleBatchClick(row)"
+ >
+ {{ row.batchCode }}
+ </Button>
+ <span v-else>-</span>
+ </template>
<template #typeName="{ row }">
<Tag :color="row.type === 1 ? 'green' : 'red'">
{{ row.typeName || '-' }}
diff --git a/src/views/wls/warehouse/data.ts b/src/views/wls/warehouse/data.ts
index 6eaf201..387ab05 100644
--- a/src/views/wls/warehouse/data.ts
+++ b/src/views/wls/warehouse/data.ts
@@ -5,7 +5,7 @@
import { h } from 'vue';
-import { DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants';
+import { DICT_TYPE, MesAutoCodeRuleCode, MesWmWarehouseTypeEnum } from '@vben/constants';
import { Button } from 'ant-design-vue';
@@ -101,6 +101,19 @@
},
},
{
+ fieldName: 'type',
+ label: '浠撳簱绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ placeholder: '璇烽�夋嫨浠撳簱绫诲瀷',
+ options: [
+ { label: '鏆傚瓨搴�', value: MesWmWarehouseTypeEnum.TEMP },
+ { label: '鍚堟牸搴�', value: MesWmWarehouseTypeEnum.QUALIFIED },
+ ],
+ },
+ rules: 'required',
+ },
+ {
fieldName: 'frozen',
label: '鏄惁鍐荤粨',
component: 'Switch',
@@ -142,6 +155,19 @@
componentProps: {
allowClear: true,
placeholder: '璇疯緭鍏ヤ粨搴撳悕绉�',
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '浠撳簱绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '鏆傚瓨搴�', value: MesWmWarehouseTypeEnum.TEMP },
+ { label: '鍚堟牸搴�', value: MesWmWarehouseTypeEnum.QUALIFIED },
+ ],
+ placeholder: '璇烽�夋嫨浠撳簱绫诲瀷',
},
},
{
@@ -192,6 +218,17 @@
userList.find((user) => user.id === cellValue)?.nickname ?? '',
},
{
+ field: 'type',
+ title: '浠撳簱绫诲瀷',
+ width: 100,
+ formatter: ({ cellValue }) =>
+ cellValue === MesWmWarehouseTypeEnum.TEMP
+ ? '鏆傚瓨搴�'
+ : cellValue === MesWmWarehouseTypeEnum.QUALIFIED
+ ? '鍚堟牸搴�'
+ : '',
+ },
+ {
field: 'frozen',
title: '鍐荤粨',
width: 90,
--
Gitblit v1.9.3