From b7eb1da8220f5b72f6891c3c6471ca386604cce4 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 19 八月 2026 10:38:21 +0800
Subject: [PATCH] feat(dashboard): 添加一袋一码首页并重构相关枚举常量
---
src/views/dashboard/bagCodeHome/index.vue | 69 +
src/views/dashboard/bagCodeHome/modules/trend-chart.vue | 48
src/packages/@core/ui-kit/layout-ui/src/vben-layout.vue | 2
dist.zip | 0
src/views/dashboard/bagCodeHome/modules/basic-cards.vue | 74 +
src/router/routes/modules/bagcode-home.ts | 16
src/views/mes/pro/batch/index.vue | 238 +++
src/router/routes/modules/dashboard.ts | 15
src/api/mes/pro/line/index.ts | 61
src/views/dashboard/bagCodeHome/data.ts | 30
src/views/mes/pro/line/components/index.ts | 1
src/api/mes/pro/pallet/index.ts | 110 +
src/views/mes/pro/bagCode/modules/bind-pallet.vue | 121 +
src/views/mes/pro/line/modules/form.vue | 111 +
src/packages/@core/preferences/src/config.ts | 2
src/views/mes/pro/line/components/select.vue | 113 +
src/views/dashboard/bagCodeHome/chart-options.ts | 45
src/views/dashboard/bagCodeHome/modules/bag-code-status-chart.vue | 43
src/views/mes/pro/pallet/index.vue | 169 ++
src/views/basicData/mdm/index.vue | 18
src/api/mes/pro/bagCode/index.ts | 129 +
src/views/mes/pro/pallet/modules/generate-form.vue | 122 +
src/views/mes/pro/bagCode/modules/generate-form.vue | 111 +
src/views/mes/md/item/type/modules/form.vue | 4
src/views/mes/pro/components/qr-trace-modal.vue | 569 ++++++++
src/views/mes/pro/pallet/data.ts | 109 +
src/views/mes/pro/bagCode/index.vue | 273 +++
src/views/mes/pro/line/data.ts | 152 ++
src/packages/constants/src/biz-mes-enum.ts | 30
src/views/basicData/mdm/modules/form.vue | 2
src/views/dashboard/bagCodeHome/modules/batch-status-chart.vue | 43
src/views/mes/pro/batch/modules/form.vue | 281 ++++
src/main.ts | 4
src/views/dashboard/bagCodeHome/modules/shortcuts.vue | 49
src/api/mes/bag-code-home/index.ts | 44
src/views/mes/md/item/data.ts | 18
src/views/mes/md/item/type/data.ts | 6
src/views/mes/md/item/type/index.vue | 4
src/views/dashboard/bagCodeHome/modules/bag-code-cards.vue | 74 +
src/views/mes/md/item/components/select-dialog.vue | 24
src/api/mes/pro/batch/index.ts | 136 +
src/views/basicData/mdm/data.ts | 28
src/views/mes/pro/line/index.vue | 143 ++
src/views/mes/pro/bagCode/data.ts | 130 +
src/packages/constants/src/dict-enum.ts | 4
src/views/mes/pro/batch/data.ts | 336 ++++
src/views/mes/md/item/components/select.vue | 10
47 files changed, 4,049 insertions(+), 72 deletions(-)
diff --git a/dist.zip b/dist.zip
new file mode 100644
index 0000000..d7474cd
--- /dev/null
+++ b/dist.zip
Binary files differ
diff --git a/src/api/mes/bag-code-home/index.ts b/src/api/mes/bag-code-home/index.ts
new file mode 100644
index 0000000..478c99e
--- /dev/null
+++ b/src/api/mes/bag-code-home/index.ts
@@ -0,0 +1,44 @@
+import { requestClient } from '#/api/request';
+
+export namespace MesBagCodeHomeApi {
+ /** 鍩虹鏁版嵁涓庝竴琚嬩竴鐮� 鐘舵�佸垎甯冮」 */
+ export interface StatusItem {
+ status: number; // 鐘舵�佸��
+ count: number; // 鏁伴噺
+ }
+
+ /** 鍩虹鏁版嵁涓庝竴琚嬩竴鐮� 棣栭〉姹囨�荤粺璁� */
+ export interface Summary {
+ mdmItemCount: number; // 鍝佺鎬绘暟锛坢dm_item锛�
+ mesItemCount: number; // MES 鍝佺鏁帮紙mes_md_item锛�
+ itemTypeCount: number; // 鍝佺鍒嗙被鏁�
+ unitMeasureCount: number; // 璁¢噺鍗曚綅鏁�
+ lineCount: number; // 浜х嚎鏁�
+ batchCount: number; // 鎵规鎬绘暟
+ bagCodeCount: number; // 琚嬬爜鎬绘暟
+ palletCount: number; // 鎵樼洏鐮佹�绘暟
+ batchStatusList: StatusItem[]; // 鎵规鐘舵�佸垎甯�
+ bagCodeStatusList: StatusItem[]; // 琚嬬爜鐘舵�佸垎甯�
+ }
+
+ /** 鍩虹鏁版嵁涓庝竴琚嬩竴鐮� 琚嬬爜鐢熸垚瓒嬪娍 */
+ export interface Trend {
+ date: string; // 鏃ユ湡
+ count: number; // 琚嬬爜鐢熸垚鏁伴噺
+ }
+}
+
+/** 鑾峰緱鍩虹鏁版嵁涓庝竴琚嬩竴鐮� 棣栭〉姹囨�荤粺璁� */
+export function getBagCodeHomeSummary() {
+ return requestClient.get<MesBagCodeHomeApi.Summary>(
+ '/mes/bag-code-home-statistics/summary',
+ );
+}
+
+/** 鑾峰緱琚嬬爜鐢熸垚瓒嬪娍 */
+export function getBagCodeHomeTrend(days?: number) {
+ return requestClient.get<MesBagCodeHomeApi.Trend[]>(
+ '/mes/bag-code-home-statistics/trend',
+ { params: { days } },
+ );
+}
diff --git a/src/api/mes/pro/bagCode/index.ts b/src/api/mes/pro/bagCode/index.ts
new file mode 100644
index 0000000..70d332c
--- /dev/null
+++ b/src/api/mes/pro/bagCode/index.ts
@@ -0,0 +1,129 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesProBagCodeApi {
+ /** 琚嬬爜锛堜竴琚嬩竴鐮侊級 */
+ export interface BagCode {
+ id?: number; // 缂栧彿
+ uuid?: string; // 鐢熸垚缁� UUID
+ code?: string; // 琚嬬爜锛堜簩缁寸爜鍐呭锛�
+ bagNo?: number; // 鎵规鍐呰搴忓彿
+ batchId?: number; // 鐢熶骇鎵规缂栧彿
+ batchCode?: string; // 鎵规鍙�
+ lineCode?: string; // 浜х嚎缂栫爜
+ lineName?: string; // 浜х嚎鍚嶇О
+ palletId?: number; // 缁戝畾鎵樼洏缂栧彿
+ palletCode?: string; // 鎵樼洏鐮�
+ status?: number; // 鐘舵�侊紙瀛楀吀 mes_pro_bag_code_status锛�
+ printTime?: string; // 瀵煎嚭锛堜氦浠樺嵃鍒凤級鏃堕棿
+ remark?: string; // 澶囨敞
+ createTime?: string; // 鍒涘缓鏃堕棿
+ }
+
+ /** 琚嬬爜鍒嗛〉鏌ヨ鍙傛暟 */
+ export interface PageParams extends PageParam {
+ code?: string;
+ batchId?: number;
+ batchCode?: string;
+ uuid?: string;
+ palletId?: number;
+ status?: number;
+ createTime?: string[];
+ }
+
+ /** 鐢熸垚鏁版嵁鍖呰姹� */
+ export interface GenerateParams {
+ batchId: number; // 鐢熶骇鎵规缂栧彿
+ count: number; // 鐢熸垚琚嬫暟
+ remark?: string; // 澶囨敞
+ }
+
+ /** 缁戝畾/瑙g粦鎵樼洏璇锋眰 */
+ export interface BindParams {
+ palletId: number; // 鎵樼洏缂栧彿
+ ids: number[]; // 琚嬬爜缂栧彿鍒楄〃
+ }
+
+ /** 鎵爜杩芥函淇℃伅 */
+ export interface BagCodeTrace {
+ id?: number; // 琚嬬爜缂栧彿
+ code?: string; // 琚嬬爜锛堜簩缁寸爜鍐呭锛�
+ bagNo?: number; // 琚嬪簭鍙�
+ status?: number; // 琚嬬爜鐘舵��
+ printTime?: string; // 瀵煎嚭鍗板埛鏃堕棿
+ batchId?: number; // 鎵规缂栧彿
+ batchCode?: string; // 鎵规鍙�
+ batchStatus?: number; // 鎵规鐘舵��
+ produceDate?: string; // 鐢熶骇鏃ユ湡
+ lineCode?: string; // 浜х嚎缂栫爜
+ lineName?: string; // 浜х嚎鍚嶇О
+ ownerUserId?: number; // 璐d换浜虹紪鍙�
+ ownerUserName?: string; // 璐d换浜烘樀绉�
+ palletId?: number; // 鎵樼洏缂栧彿
+ palletCode?: string; // 鎵樼洏鐮�
+ itemId?: number; // 鍝佺锛堢墿鏂欙級缂栧彿
+ itemCode?: string; // 鍝佺缂栫爜
+ itemName?: string; // 鍝佺鍚嶇О
+ itemSpecification?: string; // 鍝佺瑙勬牸
+ }
+}
+
+/** 鐢熸垚琚嬬爜鏁版嵁鍖咃紙杩斿洖鐢熸垚缁� UUID锛� */
+export function generateBagCodes(data: MesProBagCodeApi.GenerateParams) {
+ return requestClient.post<string>('/mes/pro/bag-code/generate', data);
+}
+
+/** 鍒犻櫎琚嬬爜鐢熸垚缁� */
+export function deleteBagCodesByUuid(uuid: string) {
+ return requestClient.delete(`/mes/pro/bag-code/delete-by-uuid?uuid=${uuid}`);
+}
+
+/** 琚嬬爜缁戝畾鎵樼洏 */
+export function bindBagCodePallet(data: MesProBagCodeApi.BindParams) {
+ return requestClient.put('/mes/pro/bag-code/bind-pallet', data);
+}
+
+/** 琚嬬爜瑙g粦鎵樼洏 */
+export function unbindBagCodePallet(data: MesProBagCodeApi.BindParams) {
+ return requestClient.put('/mes/pro/bag-code/unbind-pallet', data);
+}
+
+/** 鏌ヨ琚嬬爜璇︽儏 */
+export function getBagCode(id: number) {
+ return requestClient.get<MesProBagCodeApi.BagCode>(
+ `/mes/pro/bag-code/get?id=${id}`,
+ );
+}
+
+/** 鎵爜杩芥函锛氭牴鎹鐮佸弽鏌ユ壒娆°�佷骇绾裤�佽矗浠讳汉绛変俊鎭� */
+export function getBagCodeByCode(code: string) {
+ return requestClient.get<MesProBagCodeApi.BagCodeTrace>(
+ `/mes/pro/bag-code/get-by-code?code=${encodeURIComponent(code)}`,
+ );
+}
+
+/** 鏌ヨ琚嬬爜鍒嗛〉 */
+export function getBagCodePage(params: MesProBagCodeApi.PageParams) {
+ return requestClient.get<PageResult<MesProBagCodeApi.BagCode>>(
+ '/mes/pro/bag-code/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ鐢熸垚缁勭殑琚嬬爜鍒楄〃 */
+export function getBagCodeListByUuid(uuid: string) {
+ return requestClient.get<MesProBagCodeApi.BagCode[]>(
+ `/mes/pro/bag-code/list-by-uuid?uuid=${uuid}`,
+ );
+}
+
+/** 鎵归噺瀵煎嚭琚嬬爜 Excel锛堜氦浠樺嵃鍒凤級 */
+export function exportBagCodeExcel(params: MesProBagCodeApi.PageParams) {
+ return requestClient.download('/mes/pro/bag-code/export-excel', { params });
+}
+
+/** 鎵归噺瀵煎嚭琚嬬爜浜岀淮鐮佸浘鐗� ZIP锛坲uid 浼樺厛锛屽惁鍒欐寜 batchId锛� */
+export function exportBagQr(params: { uuid?: string; batchId?: number }) {
+ return requestClient.download('/mes/pro/bag-code/export-qr', { params });
+}
diff --git a/src/api/mes/pro/batch/index.ts b/src/api/mes/pro/batch/index.ts
new file mode 100644
index 0000000..01e4424
--- /dev/null
+++ b/src/api/mes/pro/batch/index.ts
@@ -0,0 +1,136 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+import type { MesProBagCodeApi } from '#/api/mes/pro/bagCode';
+import type { MesProPalletApi } from '#/api/mes/pro/pallet';
+
+export namespace MesProBatchApi {
+ /** 鐢熶骇鎵规浜哄憳閰嶇疆 */
+ export interface BatchWorker {
+ id?: number; // 缂栧彿
+ batchId?: number; // 鎵规缂栧彿
+ userId?: number; // 鐢熶骇浜哄憳鐢ㄦ埛缂栧彿
+ userName?: string; // 鐢熶骇浜哄憳鏄电О
+ workerType?: number; // 宀椾綅绫诲瀷锛堝瓧鍏� mes_pro_batch_worker_type锛�
+ remark?: string; // 澶囨敞
+ }
+
+ /** 鐢熶骇鎵规 */
+ export interface Batch {
+ id?: number; // 缂栧彿
+ code?: string; // 鎵规鍙凤紙鑷姩鐢熸垚锛�
+ itemId?: number; // 鍝佺锛堜骇鍝佺墿鏂欙級缂栧彿
+ itemCode?: string; // 鍝佺缂栫爜锛堝悗绔嫾鎺ワ級
+ itemName?: string; // 鍝佺鍚嶇О锛堝悗绔嫾鎺ワ級
+ itemSpecification?: string; // 鍝佺瑙勬牸锛堝悗绔嫾鎺ワ級
+ lineId?: number; // 浜х嚎缂栧彿
+ lineCode?: string; // 浜х嚎缂栫爜锛堝悗绔洖濉級
+ lineName?: string; // 浜х嚎鍚嶇О锛堝悗绔洖濉級
+ produceDate?: string; // 鐢熶骇鏃ユ湡
+ planQuantity?: number; // 璁″垝鏁伴噺
+ ownerUserId?: number; // 璐d换浜虹敤鎴风紪鍙�
+ ownerUserName?: string; // 璐d换浜烘樀绉�
+ bagQuantity?: number; // 宸茬敓鎴愯鐮佹暟閲�
+ palletQuantity?: number; // 宸茬敓鎴愭墭鐩樼爜鏁伴噺
+ status?: number; // 鎵规鐘舵�侊紙瀛楀吀 mes_pro_batch_status锛�
+ remark?: string; // 澶囨敞
+ createTime?: string; // 鍒涘缓鏃堕棿
+ workers?: BatchWorker[]; // 鐢熶骇浜哄憳閰嶇疆鍒楄〃
+ }
+
+ /** 鐢熶骇鎵规鎵爜杩芥函 */
+ export interface BatchTrace {
+ id?: number; // 鐢熶骇鎵规缂栧彿
+ code?: string; // 鎵规鍙凤紙浜岀淮鐮佸唴瀹癸級
+ status?: number; // 鎵规鐘舵�侊紙瀛楀吀 mes_pro_batch_status锛�
+ produceDate?: string; // 鐢熶骇鏃ユ湡
+ lineCode?: string; // 浜х嚎缂栫爜
+ lineName?: string; // 浜х嚎鍚嶇О
+ ownerUserId?: number; // 璐d换浜虹敤鎴风紪鍙�
+ ownerUserName?: string; // 璐d换浜烘樀绉�
+ planQuantity?: number; // 璁″垝鏁伴噺
+ bagQuantity?: number; // 宸茬敓鎴愯鐮佹暟閲�
+ palletQuantity?: number; // 宸茬敓鎴愭墭鐩樼爜鏁伴噺
+ remark?: string; // 澶囨敞
+ itemId?: number; // 鍝佺锛堢墿鏂欙級缂栧彿
+ itemCode?: string; // 鍝佺缂栫爜
+ itemName?: string; // 鍝佺鍚嶇О
+ itemSpecification?: string; // 鍝佺瑙勬牸
+ workers?: BatchWorker[]; // 鐢熶骇浜哄憳閰嶇疆鍒楄〃
+ pallets?: MesProPalletApi.Pallet[]; // 鎵樼洏鐮佸垪琛�
+ bags?: MesProBagCodeApi.BagCode[]; // 琚嬬爜鍒楄〃锛堟渶澶氬墠 2000 鏉★紝鎸夎搴忓彿鍗囧簭锛�
+ bagTotal?: number; // 琚嬬爜鎬绘暟
+ }
+
+ /** 鐢熶骇鎵规鍒嗛〉鏌ヨ鍙傛暟 */
+ export interface PageParams extends PageParam {
+ code?: string;
+ itemId?: number;
+ lineId?: number;
+ lineCode?: string;
+ lineName?: string;
+ ownerUserId?: number;
+ status?: number;
+ produceDate?: string[];
+ createTime?: string[];
+ }
+}
+
+/** 鏌ヨ鐢熶骇鎵规鍒嗛〉 */
+export function getBatchPage(params: MesProBatchApi.PageParams) {
+ return requestClient.get<PageResult<MesProBatchApi.Batch>>(
+ '/mes/pro/batch/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ鐢熶骇鎵规璇︽儏锛堝惈浜哄憳閰嶇疆锛� */
+export function getBatch(id: number) {
+ return requestClient.get<MesProBatchApi.Batch>(
+ `/mes/pro/batch/get?id=${id}`,
+ );
+}
+
+/** 鏂板鐢熶骇鎵规锛堣嚜鍔ㄧ敓鎴愭壒娆″彿锛� */
+export function createBatch(data: MesProBatchApi.Batch) {
+ return requestClient.post<number>('/mes/pro/batch/create', data);
+}
+
+/** 淇敼鐢熶骇鎵规 */
+export function updateBatch(data: MesProBatchApi.Batch) {
+ return requestClient.put('/mes/pro/batch/update', data);
+}
+
+/** 鍒犻櫎鐢熶骇鎵规 */
+export function deleteBatch(id: number) {
+ return requestClient.delete(`/mes/pro/batch/delete?id=${id}`);
+}
+
+/** 鏇存柊鐢熶骇鎵规鐘舵�� */
+export function updateBatchStatus(id: number, status: number) {
+ return requestClient.put(
+ `/mes/pro/batch/update-status?id=${id}&status=${status}`,
+ );
+}
+
+/** 鏌ヨ鐢熶骇鎵规绮剧畝鍒楄〃锛堜緵涓嬫媺閫夋嫨锛� */
+export function getBatchSimpleList() {
+ return requestClient.get<MesProBatchApi.Batch[]>('/mes/pro/batch/simple-list');
+}
+
+/** 瀵煎嚭鐢熶骇鎵规 Excel */
+export function exportBatchExcel(params: MesProBatchApi.PageParams) {
+ return requestClient.download('/mes/pro/batch/export-excel', { params });
+}
+
+/** 鎵爜杩芥函锛氭牴鎹壒娆″彿锛堜簩缁寸爜鍐呭锛夊弽鏌ユ壒娆°�佷汉鍛樸�佹墭鐩樸�佽鐮佷俊鎭� */
+export function getBatchByCode(code: string) {
+ return requestClient.get<MesProBatchApi.BatchTrace>(
+ `/mes/pro/batch/get-by-code?code=${encodeURIComponent(code)}`,
+ );
+}
+
+/** 鎵归噺瀵煎嚭鎵规浜岀淮鐮佸浘鐗� ZIP锛堟壒娆″浘 + 璇ユ壒娆″叏閮ㄦ墭鐩樺浘锛� */
+export function exportBatchQr(batchId: number) {
+ return requestClient.download(`/mes/pro/batch/export-qr?batchId=${batchId}`);
+}
diff --git a/src/api/mes/pro/line/index.ts b/src/api/mes/pro/line/index.ts
new file mode 100644
index 0000000..e13bcb5
--- /dev/null
+++ b/src/api/mes/pro/line/index.ts
@@ -0,0 +1,61 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesProLineApi {
+ /** MES 浜х嚎 */
+ export interface Line {
+ id?: number; // 缂栧彿
+ code?: string; // 浜х嚎缂栫爜
+ name?: string; // 浜х嚎鍚嶇О
+ status?: number; // 鐘舵�侊紙瀛楀吀 common_status锛�
+ remark?: string; // 澶囨敞
+ createTime?: string; // 鍒涘缓鏃堕棿
+ }
+
+ /** 浜х嚎鍒嗛〉鏌ヨ鍙傛暟 */
+ export interface PageParams extends PageParam {
+ code?: string;
+ name?: string;
+ status?: number;
+ createTime?: string[];
+ }
+}
+
+/** 鏌ヨ浜х嚎鍒嗛〉 */
+export function getLinePage(params: MesProLineApi.PageParams) {
+ return requestClient.get<PageResult<MesProLineApi.Line>>(
+ '/mes/pro/line/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ浜х嚎绮剧畝鍒楄〃锛堜緵涓嬫媺閫夋嫨锛屼粎鍚敤鐨勪骇绾匡級 */
+export function getLineSimpleList() {
+ return requestClient.get<MesProLineApi.Line[]>('/mes/pro/line/simple-list');
+}
+
+/** 鏌ヨ浜х嚎璇︽儏 */
+export function getLine(id: number) {
+ return requestClient.get<MesProLineApi.Line>(`/mes/pro/line/get?id=${id}`);
+}
+
+/** 鏂板浜х嚎 */
+export function createLine(data: MesProLineApi.Line) {
+ return requestClient.post('/mes/pro/line/create', data);
+}
+
+/** 淇敼浜х嚎 */
+export function updateLine(data: MesProLineApi.Line) {
+ return requestClient.put('/mes/pro/line/update', data);
+}
+
+/** 鍒犻櫎浜х嚎 */
+export function deleteLine(id: number) {
+ return requestClient.delete(`/mes/pro/line/delete?id=${id}`);
+}
+
+/** 瀵煎嚭浜х嚎 Excel */
+export function exportLineExcel(params: MesProLineApi.PageParams) {
+ return requestClient.download('/mes/pro/line/export-excel', { params });
+}
diff --git a/src/api/mes/pro/pallet/index.ts b/src/api/mes/pro/pallet/index.ts
new file mode 100644
index 0000000..2fef83b
--- /dev/null
+++ b/src/api/mes/pro/pallet/index.ts
@@ -0,0 +1,110 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+import type { MesProBagCodeApi } from '#/api/mes/pro/bagCode';
+
+export namespace MesProPalletApi {
+ /** 鎵樼洏鐮� */
+ export interface Pallet {
+ id?: number; // 缂栧彿
+ code?: string; // 鎵樼洏鐮�
+ batchId?: number; // 鐢熶骇鎵规缂栧彿
+ batchCode?: string; // 鎵规鍙�
+ palletNo?: number; // 鎵规鍐呮墭鐩樺簭鍙�
+ capacity?: number; // 鍙琚嬫暟锛堣/鎵樼洏锛�
+ bagCount?: number; // 宸茬粦瀹氳鐮佹暟閲�
+ status?: number; // 鐘舵�侊紙瀛楀吀 mes_pro_pallet_status锛�
+ remark?: string; // 澶囨敞
+ createTime?: string; // 鍒涘缓鏃堕棿
+ }
+
+ /** 鎵樼洏鐮佸垎椤垫煡璇㈠弬鏁� */
+ export interface PageParams extends PageParam {
+ code?: string;
+ batchId?: number;
+ batchCode?: string;
+ status?: number;
+ createTime?: string[];
+ }
+
+ /** 鎵樼洏鐮佹壂鐮佽拷婧� */
+ export interface PalletTrace {
+ id?: number; // 缂栧彿
+ code?: string; // 鎵樼洏鐮侊紙浜岀淮鐮佸唴瀹癸級
+ palletNo?: number; // 鎵规鍐呮墭鐩樺簭鍙�
+ capacity?: number; // 鍙琚嬫暟
+ bagCount?: number; // 宸茬粦瀹氳鐮佹暟閲�
+ status?: number; // 鐘舵�侊紙瀛楀吀 mes_pro_pallet_status锛�
+ batchId?: number; // 鐢熶骇鎵规缂栧彿
+ batchCode?: string; // 鎵规鍙�
+ remark?: string; // 澶囨敞
+ batchStatus?: number; // 鎵规鐘舵��
+ produceDate?: string; // 鐢熶骇鏃ユ湡
+ lineCode?: string; // 浜х嚎缂栫爜
+ lineName?: string; // 浜х嚎鍚嶇О
+ ownerUserId?: number; // 璐d换浜虹紪鍙�
+ ownerUserName?: string; // 璐d换浜烘樀绉�
+ itemId?: number; // 鍝佺锛堢墿鏂欙級缂栧彿
+ itemCode?: string; // 鍝佺缂栫爜
+ itemName?: string; // 鍝佺鍚嶇О
+ itemSpecification?: string; // 鍝佺瑙勬牸
+ bags?: MesProBagCodeApi.BagCode[]; // 璇ユ墭鐩樼粦瀹氱殑琚嬬爜鍒楄〃
+ }
+
+ /** 鎵归噺鐢熸垚璇锋眰 */
+ export interface BatchCreateParams {
+ batchId: number; // 鐢熶骇鎵规缂栧彿
+ count: number; // 鐢熸垚鏁伴噺
+ capacity?: number; // 鍙琚嬫暟
+ remark?: string; // 澶囨敞
+ }
+}
+
+/** 鎵归噺鐢熸垚鎵樼洏鐮侊紙杩斿洖鐢熸垚鐨勬墭鐩樼紪鍙峰垪琛級 */
+export function batchCreatePallet(data: MesProPalletApi.BatchCreateParams) {
+ return requestClient.post<number[]>('/mes/pro/pallet/batch-create', data);
+}
+
+/** 鍒犻櫎鎵樼洏鐮� */
+export function deletePallet(id: number) {
+ return requestClient.delete(`/mes/pro/pallet/delete?id=${id}`);
+}
+
+/** 鏌ヨ鎵樼洏鐮佽鎯� */
+export function getPallet(id: number) {
+ return requestClient.get<MesProPalletApi.Pallet>(
+ `/mes/pro/pallet/get?id=${id}`,
+ );
+}
+
+/** 鏌ヨ鎵樼洏鐮佸垎椤� */
+export function getPalletPage(params: MesProPalletApi.PageParams) {
+ return requestClient.get<PageResult<MesProPalletApi.Pallet>>(
+ '/mes/pro/pallet/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ鎵规鐨勬墭鐩樼爜鍒楄〃锛堜緵涓嬫媺閫夋嫨锛� */
+export function getPalletSimpleList(batchId: number) {
+ return requestClient.get<MesProPalletApi.Pallet[]>(
+ `/mes/pro/pallet/simple-list?batchId=${batchId}`,
+ );
+}
+
+/** 瀵煎嚭鎵樼洏鐮� Excel */
+export function exportPalletExcel(params: MesProPalletApi.PageParams) {
+ return requestClient.download('/mes/pro/pallet/export-excel', { params });
+}
+
+/** 鎵爜杩芥函锛氭牴鎹墭鐩樼爜锛堜簩缁寸爜鍐呭锛夊弽鏌ユ壒娆°�佸搧绉嶃�佽矗浠讳汉鍙婅鐮佸垪琛� */
+export function getPalletByCode(code: string) {
+ return requestClient.get<MesProPalletApi.PalletTrace>(
+ `/mes/pro/pallet/get-by-code?code=${encodeURIComponent(code)}`,
+ );
+}
+
+/** 鎵归噺瀵煎嚭鏌愭壒娆$殑鎵樼洏鐮佷簩缁寸爜鍥剧墖 ZIP */
+export function exportPalletQr(batchId: number) {
+ return requestClient.download(`/mes/pro/pallet/export-qr?batchId=${batchId}`);
+}
diff --git a/src/main.ts b/src/main.ts
index b692f75..6a42557 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -35,6 +35,10 @@
copyright: { companyName: __APP_NAME__ },
});
}
+ // 寮哄埗瑕嗙洊榛樿棣栭〉涓�"鍩虹鏁版嵁涓庝竴琚嬩竴鐮�"锛岀粫杩� localStorage 缂撳瓨
+ preferencesManager.updatePreferences({
+ app: { defaultHomePath: '/bagcode-home' },
+ });
if (__APP_FAVICON__) {
const link = document.querySelector('link[rel="icon"]');
if (link) {
diff --git a/src/packages/@core/preferences/src/config.ts b/src/packages/@core/preferences/src/config.ts
index b891f7c..d1a1460 100644
--- a/src/packages/@core/preferences/src/config.ts
+++ b/src/packages/@core/preferences/src/config.ts
@@ -17,7 +17,7 @@
contentPaddingTop: 0,
defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
- defaultHomePath: '/analytics',
+ defaultHomePath: '/bagcode-home',
dynamicTitle: true,
enableCheckUpdates: true,
enableCopyPreferences: true,
diff --git a/src/packages/@core/ui-kit/layout-ui/src/vben-layout.vue b/src/packages/@core/ui-kit/layout-ui/src/vben-layout.vue
index a9e5afe..1f079f2 100644
--- a/src/packages/@core/ui-kit/layout-ui/src/vben-layout.vue
+++ b/src/packages/@core/ui-kit/layout-ui/src/vben-layout.vue
@@ -495,7 +495,7 @@
</script>
<template>
- <div class="relative flex min-h-full w-full">
+ <div class="relative flex h-full w-full">
<LayoutSidebar
v-if="sidebarEnableState"
v-model:draggable="sidebarDraggable"
diff --git a/src/packages/constants/src/biz-mes-enum.ts b/src/packages/constants/src/biz-mes-enum.ts
index 85c58db..4c1981e 100644
--- a/src/packages/constants/src/biz-mes-enum.ts
+++ b/src/packages/constants/src/biz-mes-enum.ts
@@ -267,6 +267,36 @@
CANCELLED: MesOrderStatusConstants.CANCELLED,
} as const;
+/** MES 鐢熶骇鎵规鐘舵�佹灇涓撅紙瀵归綈鍚庣 MesProBatchStatusEnum锛� */
+export const MesProBatchStatusEnum = {
+ DRAFT: 10, // 鑽夌
+ PRODUCING: 20, // 鐢熶骇涓�
+ FINISHED: 30, // 宸插畬鎴�
+ CANCELED: 40, // 宸蹭綔搴�
+} as const;
+
+/** MES 鐢熶骇鎵规浜哄憳宀椾綅绫诲瀷鏋氫妇锛堝榻愬悗绔� MesProBatchWorkerTypeEnum锛� */
+export const MesProBatchWorkerTypeEnum = {
+ LEADER: 10, // 缁勯暱
+ OPERATOR: 20, // 鎿嶄綔宸�
+ PACKER: 30, // 鍖呰宸�
+ INSPECTOR: 40, // 璐ㄦ鍛�
+ OTHER: 50, // 鍏朵粬
+} as const;
+
+/** MES 琚嬬爜鐘舵�佹灇涓撅紙瀵归綈鍚庣 MesProBagCodeStatusEnum锛� */
+export const MesProBagCodeStatusEnum = {
+ GENERATED: 10, // 宸茬敓鎴�
+ PRINTED: 20, // 宸插鍑哄嵃鍒�
+ CANCELED: 30, // 宸蹭綔搴�
+} as const;
+
+/** MES 鎵樼洏鐮佺姸鎬佹灇涓撅紙瀵归綈鍚庣 MesProPalletStatusEnum锛� */
+export const MesProPalletStatusEnum = {
+ IN_USE: 10, // 鍚敤
+ CANCELED: 20, // 浣滃簾
+} as const;
+
/** MES 鐢熶骇鎶ュ伐鐘舵�佹灇涓� */
export const MesProFeedbackStatusEnum = {
PREPARE: MesOrderStatusConstants.DRAFT,
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index 2a62e5a..43c3df4 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -243,6 +243,10 @@
MES_PRO_WORK_ORDER_TYPE: 'mes_pro_work_order_type', // MES 宸ュ崟绫诲瀷
MES_PRO_WORK_ORDER_SOURCE_TYPE: 'mes_pro_work_order_source_type', // MES 宸ュ崟鏉ユ簮绫诲瀷
MES_PRO_TASK_STATUS: 'mes_pro_task_status', // MES 鐢熶骇浠诲姟鐘舵��
+ MES_PRO_BATCH_STATUS: 'mes_pro_batch_status', // MES 鐢熶骇鎵规鐘舵��
+ MES_PRO_BATCH_WORKER_TYPE: 'mes_pro_batch_worker_type', // MES 鐢熶骇鎵规浜哄憳宀椾綅绫诲瀷
+ MES_PRO_BAG_CODE_STATUS: 'mes_pro_bag_code_status', // MES 琚嬬爜鐘舵��
+ MES_PRO_PALLET_STATUS: 'mes_pro_pallet_status', // MES 鎵樼洏鐮佺姸鎬�
MES_PRO_FEEDBACK_STATUS: 'mes_pro_feedback_status', // MES 鐢熶骇鎶ュ伐鐘舵��
MES_PRO_FEEDBACK_TYPE: 'mes_pro_feedback_type', // MES 鐢熶骇鎶ュ伐绫诲瀷
MES_PD_PROJECT_STATUS: 'mes_pd_project_status', // MES 浜у搧璁捐椤圭洰鐘舵��
diff --git a/src/router/routes/modules/bagcode-home.ts b/src/router/routes/modules/bagcode-home.ts
new file mode 100644
index 0000000..0b7b2f0
--- /dev/null
+++ b/src/router/routes/modules/bagcode-home.ts
@@ -0,0 +1,16 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+const routes: RouteRecordRaw[] = [
+ {
+ name: 'BagCodeHome',
+ path: '/bagcode-home',
+ component: () => import('#/views/dashboard/bagCodeHome/index.vue'),
+ meta: {
+ icon: 'lucide:scan-barcode',
+ order: 1,
+ title: '鍩虹鏁版嵁涓庝竴琚嬩竴鐮�',
+ },
+ },
+];
+
+export default routes;
diff --git a/src/router/routes/modules/dashboard.ts b/src/router/routes/modules/dashboard.ts
index 1df827e..6f67ae2 100644
--- a/src/router/routes/modules/dashboard.ts
+++ b/src/router/routes/modules/dashboard.ts
@@ -1,17 +1,6 @@
import type { RouteRecordRaw } from 'vue-router';
-const routes: RouteRecordRaw[] = [
- {
- name: 'Analytics',
- path: '/analytics',
- component: () => import('#/views/dashboard/analytics/index.vue'),
- meta: {
- affixTab: true,
- icon: 'lucide:area-chart',
- order: -1,
- title: '棣栭〉',
- },
- },
-];
+// 鑰侀椤� /analytics 宸查殣钘忥紝鐧诲綍鍚庨粯璁よ烦杞� /bagcode-home
+const routes: RouteRecordRaw[] = [];
export default routes;
diff --git a/src/views/basicData/mdm/data.ts b/src/views/basicData/mdm/data.ts
index a2fb42c..0ec3d1d 100644
--- a/src/views/basicData/mdm/data.ts
+++ b/src/views/basicData/mdm/data.ts
@@ -95,12 +95,12 @@
{
component: "Input",
fieldName: "code",
- label: "鐗╂枡缂栫爜",
+ label: "鍝佺缂栫爜",
componentProps: {
maxLength: 64,
- placeholder: "璇疯緭鍏ョ墿鏂欑紪鐮�",
+ placeholder: "璇疯緭鍏ュ搧绉嶇紪鐮�",
},
- rules: z.string().min(1, '鐗╂枡缂栫爜涓嶈兘涓虹┖').max(64),
+ rules: z.string().min(1, '鍝佺缂栫爜涓嶈兘涓虹┖').max(64),
suffix: () =>
h(
Button,
@@ -119,19 +119,19 @@
{
component: "Input",
fieldName: "name",
- label: "鐗╂枡鍚嶇О",
+ label: "鍝佺鍚嶇О",
componentProps: {
- placeholder: "璇疯緭鍏ョ墿鏂欏悕绉�",
+ placeholder: "璇疯緭鍏ュ搧绉嶅悕绉�",
},
rules: "required",
},
{
component: "TreeSelect",
fieldName: "categoryId",
- label: "鐗╂枡鍒嗙被",
+ label: "鍝佺鍒嗙被",
rules: "selectRequired",
componentProps: {
- placeholder: "璇烽�夋嫨鐗╂枡鍒嗙被",
+ placeholder: "璇烽�夋嫨鍝佺鍒嗙被",
allowClear: true,
treeDefaultExpandAll: true,
treeData: categoryTreeData,
@@ -492,19 +492,19 @@
return [
{
fieldName: "code",
- label: "鐗╂枡缂栫爜",
+ label: "鍝佺缂栫爜",
component: "Input",
componentProps: {
- placeholder: "璇疯緭鍏ョ墿鏂欑紪鐮�",
+ placeholder: "璇疯緭鍏ュ搧绉嶇紪鐮�",
allowClear: true,
},
},
{
fieldName: "name",
- label: "鐗╂枡鍚嶇О",
+ label: "鍝佺鍚嶇О",
component: "Input",
componentProps: {
- placeholder: "璇疯緭鍏ョ墿鏂欏悕绉�",
+ placeholder: "璇疯緭鍏ュ搧绉嶅悕绉�",
allowClear: true,
},
},
@@ -527,12 +527,12 @@
{ type: "checkbox", width: 40 },
{
field: "code",
- title: "鐗╂枡缂栫爜",
+ title: "鍝佺缂栫爜",
minWidth: 120,
},
{
field: "name",
- title: "鐗╂枡鍚嶇О",
+ title: "鍝佺鍚嶇О",
minWidth: 180,
},
{
@@ -542,7 +542,7 @@
},
{
field: "categoryName",
- title: "鐗╂枡鍒嗙被",
+ title: "鍝佺鍒嗙被",
minWidth: 120,
slots: { default: "categoryName" },
},
diff --git a/src/views/basicData/mdm/index.vue b/src/views/basicData/mdm/index.vue
index 8995f78..7d92251 100644
--- a/src/views/basicData/mdm/index.vue
+++ b/src/views/basicData/mdm/index.vue
@@ -78,20 +78,20 @@
/** 瀵煎嚭琛ㄦ牸 */
// async function handleExport() {
// const data = await exportItem(await gridApi.formApi.getValues());
-// downloadFileFromBlobPart({ fileName: '鐗╂枡.xls', source: data });
+// downloadFileFromBlobPart({ fileName: '鍝佺.xls', source: data });
// }
-/** 鍒涘缓鐗╂枡 */
+/** 鍒涘缓鍝佺 */
function handleCreate() {
formModalApi.setData(null).open();
}
-/** 缂栬緫鐗╂枡 */
+/** 缂栬緫鍝佺 */
function handleEdit(row: MdmItemApi.Item) {
formModalApi.setData(row).open();
}
-/** 鍒犻櫎鐗╂枡 */
+/** 鍒犻櫎鍝佺 */
async function handleDelete(row: MdmItemApi.Item) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
@@ -106,7 +106,7 @@
}
}
-/** 鎵归噺鍒犻櫎鐗╂枡 */
+/** 鎵归噺鍒犻櫎鍝佺 */
async function handleDeleteBatch() {
await confirm($t('ui.actionMessage.deleteBatchConfirm'));
const hideLoading = message.loading({
@@ -141,10 +141,10 @@
const syncingId = ref<number | null>(null);
-/** 鍒囨崲鍚屾鐘舵�侊細鍏抽棴寮�鍏充細浠� MES 鍒犻櫎鐗╂枡锛岄渶纭 */
+/** 鍒囨崲鍚屾鐘舵�侊細鍏抽棴寮�鍏充細浠� MES 鍒犻櫎鍝佺锛岄渶纭 */
async function handleSyncChange(row: MdmItemApi.Item, checked: boolean) {
if (!checked) {
- await confirm('鍏抽棴鍚屾灏嗕粠 MES 鍒犻櫎璇ョ墿鏂欏強鍏宠仈閰嶇疆锛圔OM/SOP/SIP/鎵规閰嶇疆锛夛紝纭缁х画锛�');
+ await confirm('鍏抽棴鍚屾灏嗕粠 MES 鍒犻櫎璇ュ搧绉嶅強鍏宠仈閰嶇疆锛圔OM/SOP/SIP/鎵规閰嶇疆锛夛紝纭缁х画锛�');
}
syncingId.value = row.id!;
try {
@@ -211,12 +211,12 @@
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
- <Grid table-title="鐗╂枡鍒楄〃">
+ <Grid table-title="鍝佺鍒楄〃">
<template #toolbar-tools>
<TableAction
:actions="[
{
- label: $t('ui.actionTitle.create', ['鐗╂枡']),
+ label: $t('ui.actionTitle.create', ['鍝佺']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mdm:item:create'],
diff --git a/src/views/basicData/mdm/modules/form.vue b/src/views/basicData/mdm/modules/form.vue
index 5d82e00..c9578e9 100644
--- a/src/views/basicData/mdm/modules/form.vue
+++ b/src/views/basicData/mdm/modules/form.vue
@@ -83,7 +83,7 @@
</script>
<template>
- <Modal class="w-[1000px]" :title="formData?.id ? $t('ui.actionTitle.edit', ['鐗╂枡']) : $t('ui.actionTitle.create', ['鐗╂枡'])">
+ <Modal class="w-[1000px]" :title="formData?.id ? $t('ui.actionTitle.edit', ['鍝佺']) : $t('ui.actionTitle.create', ['鍝佺'])">
<Form class="mx-4" />
</Modal>
</template>
diff --git a/src/views/dashboard/bagCodeHome/chart-options.ts b/src/views/dashboard/bagCodeHome/chart-options.ts
new file mode 100644
index 0000000..045568b
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/chart-options.ts
@@ -0,0 +1,45 @@
+import type { EChartsOption } from '@vben/plugins/echarts';
+
+/** 琚嬬爜鐢熸垚瓒嬪娍鏌辩姸鍥鹃厤缃� */
+export function getBagCodeTrendChartOptions(
+ dates: string[],
+ counts: number[],
+): EChartsOption {
+ return {
+ grid: { bottom: 40, left: 50, right: 20, top: 20 },
+ legend: { bottom: 0, data: ['琚嬬爜鐢熸垚閲�'] },
+ series: [
+ {
+ barMaxWidth: 32,
+ data: counts,
+ itemStyle: { borderRadius: 4, color: '#409EFF' },
+ name: '琚嬬爜鐢熸垚閲�',
+ type: 'bar',
+ },
+ ],
+ tooltip: { axisPointer: { type: 'shadow' }, trigger: 'axis' },
+ xAxis: { axisTick: { show: false }, data: dates, type: 'category' },
+ yAxis: { minInterval: 1, type: 'value' },
+ };
+}
+
+/** 鐘舵�佸垎甯冪幆褰㈠浘閰嶇疆 */
+export function getStatusChartOptions(
+ data: Array<{ itemStyle: { color: string }; name: string; value: number }>,
+): EChartsOption {
+ return {
+ legend: { bottom: 0, 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 },
+ radius: ['40%', '70%'],
+ type: 'pie',
+ },
+ ],
+ tooltip: { formatter: '{b}: {c} ({d}%)', trigger: 'item' },
+ };
+}
diff --git a/src/views/dashboard/bagCodeHome/data.ts b/src/views/dashboard/bagCodeHome/data.ts
new file mode 100644
index 0000000..6dfefbc
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/data.ts
@@ -0,0 +1,30 @@
+import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
+
+/** 棣栭〉姹囨�荤粺璁¢粯璁ゅ�� */
+export const defaultSummary: MesBagCodeHomeApi.Summary = {
+ bagCodeCount: 0,
+ bagCodeStatusList: [],
+ batchCount: 0,
+ batchStatusList: [],
+ itemTypeCount: 0,
+ lineCount: 0,
+ mdmItemCount: 0,
+ mesItemCount: 0,
+ palletCount: 0,
+ unitMeasureCount: 0,
+};
+
+/** 鎵规鐘舵�佸搴旂殑棰滆壊鏄犲皠锛堢姸鎬佸瓧鍏革細10 鑽夌 20 鐢熶骇涓� 30 宸插畬鎴� 40 宸蹭綔搴燂級 */
+export const BATCH_STATUS_COLOR_MAP: Record<number, string> = {
+ 10: '#909399', // 鑽夌
+ 20: '#409EFF', // 鐢熶骇涓�
+ 30: '#67C23A', // 宸插畬鎴�
+ 40: '#F56C6C', // 宸蹭綔搴�
+};
+
+/** 琚嬬爜鐘舵�佸搴旂殑棰滆壊鏄犲皠锛堢姸鎬佸瓧鍏革細10 宸茬敓鎴� 20 宸插鍑哄嵃鍒� 30 宸蹭綔搴燂級 */
+export const BAG_CODE_STATUS_COLOR_MAP: Record<number, string> = {
+ 10: '#409EFF', // 宸茬敓鎴�
+ 20: '#E6A23C', // 宸插鍑哄嵃鍒�
+ 30: '#F56C6C', // 宸蹭綔搴�
+};
diff --git a/src/views/dashboard/bagCodeHome/index.vue b/src/views/dashboard/bagCodeHome/index.vue
new file mode 100644
index 0000000..3887aca
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/index.vue
@@ -0,0 +1,69 @@
+<script lang="ts" setup>
+import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
+
+import { onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { Page } from '@vben/common-ui';
+
+import { Col, Row, message } from 'ant-design-vue';
+
+import { getBagCodeHomeSummary } from '#/api/mes/bag-code-home';
+
+import { defaultSummary } from './data';
+import BagCodeCards from './modules/bag-code-cards.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 Shortcuts from './modules/shortcuts.vue';
+import TrendChart from './modules/trend-chart.vue';
+
+defineOptions({ name: 'DashboardBagCodeHome' });
+
+const router = useRouter();
+const summary = ref<MesBagCodeHomeApi.Summary>(defaultSummary); // 棣栭〉姹囨�荤粺璁�
+
+/** 璺宠浆鍒扮洰鏍囬〉闈紙鎸夎矾鐢� name锛屾湭鍔犺浇鏃舵彁绀猴級 */
+async function handleNavigate(name: string) {
+ try {
+ await router.push({ name });
+ } catch {
+ message.warning('褰撳墠鑿滃崟灏氭湭鍔犺浇锛岃浠庡乏渚ц彍鍗曡繘鍏ュ搴旈〉闈�');
+ }
+}
+
+/** 鍔犺浇棣栭〉姹囨�荤粺璁� */
+async function loadSummary() {
+ summary.value = await getBagCodeHomeSummary();
+}
+
+onMounted(() => {
+ loadSummary();
+});
+</script>
+
+<template>
+ <Page>
+ <!-- 绗竴琛岋細鍩虹鏁版嵁鍗$墖 -->
+ <BasicCards :summary="summary" class="mb-2" @navigate="handleNavigate" />
+
+ <!-- 绗簩琛岋細涓�琚嬩竴鐮佸崱鐗� -->
+ <BagCodeCards :summary="summary" class="mb-4" @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>
+
+ <!-- 绗洓琛岋細蹇嵎鍏ュ彛 -->
+ <Shortcuts @navigate="handleNavigate" />
+ </Page>
+</template>
diff --git a/src/views/dashboard/bagCodeHome/modules/bag-code-cards.vue b/src/views/dashboard/bagCodeHome/modules/bag-code-cards.vue
new file mode 100644
index 0000000..42fa28b
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/bag-code-cards.vue
@@ -0,0 +1,74 @@
+<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/bag-code-status-chart.vue b/src/views/dashboard/bagCodeHome/modules/bag-code-status-chart.vue
new file mode 100644
index 0000000..f9821b3
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/bag-code-status-chart.vue
@@ -0,0 +1,43 @@
+<script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
+
+import { ref, watch } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictLabel } from '@vben/hooks';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
+
+import { Card } from 'ant-design-vue';
+
+import { getStatusChartOptions } from '../chart-options';
+import { BAG_CODE_STATUS_COLOR_MAP } from '../data';
+
+defineOptions({ name: 'BagCodeHomeBagCodeStatusChart' });
+
+const props = defineProps<{
+ list: MesBagCodeHomeApi.StatusItem[];
+}>();
+
+const chartRef = ref<EchartsUIType>();
+const { renderEcharts } = useEcharts(chartRef);
+
+watch(
+ () => props.list,
+ async (list) => {
+ const data = list.map((item) => ({
+ itemStyle: { color: BAG_CODE_STATUS_COLOR_MAP[item.status] || '#909399' },
+ name: getDictLabel(DICT_TYPE.MES_PRO_BAG_CODE_STATUS, item.status) || `鐘舵��${item.status}`,
+ value: item.count,
+ }));
+ await renderEcharts(getStatusChartOptions(data));
+ },
+ { immediate: true },
+);
+</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/basic-cards.vue b/src/views/dashboard/bagCodeHome/modules/basic-cards.vue
new file mode 100644
index 0000000..3ca866a
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/basic-cards.vue
@@ -0,0 +1,74 @@
+<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/batch-status-chart.vue b/src/views/dashboard/bagCodeHome/modules/batch-status-chart.vue
new file mode 100644
index 0000000..45a27ff
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/batch-status-chart.vue
@@ -0,0 +1,43 @@
+<script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+import type { MesBagCodeHomeApi } from '#/api/mes/bag-code-home';
+
+import { ref, watch } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictLabel } from '@vben/hooks';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
+
+import { Card } from 'ant-design-vue';
+
+import { getStatusChartOptions } from '../chart-options';
+import { BATCH_STATUS_COLOR_MAP } from '../data';
+
+defineOptions({ name: 'BagCodeHomeBatchStatusChart' });
+
+const props = defineProps<{
+ list: MesBagCodeHomeApi.StatusItem[];
+}>();
+
+const chartRef = ref<EchartsUIType>();
+const { renderEcharts } = useEcharts(chartRef);
+
+watch(
+ () => props.list,
+ async (list) => {
+ const data = list.map((item) => ({
+ itemStyle: { color: BATCH_STATUS_COLOR_MAP[item.status] || '#909399' },
+ name: getDictLabel(DICT_TYPE.MES_PRO_BATCH_STATUS, item.status) || `鐘舵��${item.status}`,
+ value: item.count,
+ }));
+ await renderEcharts(getStatusChartOptions(data));
+ },
+ { immediate: true },
+);
+</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
new file mode 100644
index 0000000..bac404f
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/shortcuts.vue
@@ -0,0 +1,49 @@
+<script lang="ts" setup>
+import { useRouter } from 'vue-router';
+
+import { IconifyIcon } from '@vben/icons';
+
+import { Card, Col, Row, message } from 'ant-design-vue';
+
+defineOptions({ name: 'BagCodeHomeShortcuts' });
+
+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' },
+];
+
+/** 璺宠浆鍒扮洰鏍囬〉闈紙鎸夎矾鐢� name锛屾湭鍔犺浇鏃舵彁绀猴級 */
+async function handleNavigate(routeName: string) {
+ try {
+ await router.push({ name: routeName });
+ } catch {
+ message.warning('褰撳墠鑿滃崟灏氭湭鍔犺浇锛岃浠庡乏渚ц彍鍗曡繘鍏ュ搴旈〉闈�');
+ }
+}
+</script>
+
+<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" />
+ </div>
+ <span class="text-sm">{{ item.name }}</span>
+ </div>
+ </Col>
+ </Row>
+ </Card>
+</template>
diff --git a/src/views/dashboard/bagCodeHome/modules/trend-chart.vue b/src/views/dashboard/bagCodeHome/modules/trend-chart.vue
new file mode 100644
index 0000000..2443482
--- /dev/null
+++ b/src/views/dashboard/bagCodeHome/modules/trend-chart.vue
@@ -0,0 +1,48 @@
+<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, RadioButton, RadioGroup } from 'ant-design-vue';
+
+import { getBagCodeHomeTrend } from '#/api/mes/bag-code-home';
+
+import { getBagCodeTrendChartOptions } from '../chart-options';
+
+defineOptions({ name: 'BagCodeHomeTrendChart' });
+
+const trendDays = ref(7); // 褰撳墠閫変腑鐨勫ぉ鏁拌寖鍥�
+const chartRef = ref<EchartsUIType>();
+const { renderEcharts } = useEcharts(chartRef);
+
+/** 鍔犺浇琚嬬爜鐢熸垚瓒嬪娍鏁版嵁骞舵覆鏌撳浘琛� */
+async function loadData() {
+ const data = await getBagCodeHomeTrend(trendDays.value);
+ const dates = data.map((d) => d.date.slice(5));
+ const counts = data.map((d) => d.count);
+ await renderEcharts(getBagCodeTrendChartOptions(dates, counts));
+}
+
+/** 鍒囨崲澶╂暟鑼冨洿 */
+function handleDaysChange() {
+ loadData();
+}
+
+onMounted(() => {
+ loadData();
+});
+</script>
+
+<template>
+ <Card title="琚嬬爜鐢熸垚瓒嬪娍" class="h-full">
+ <template #extra>
+ <RadioGroup v-model:value="trendDays" size="small" @change="handleDaysChange">
+ <RadioButton :value="7">杩� 7 澶�</RadioButton>
+ <RadioButton :value="30">杩� 30 澶�</RadioButton>
+ </RadioGroup>
+ </template>
+ <EchartsUI ref="chartRef" class="h-[320px] w-full" />
+ </Card>
+</template>
diff --git a/src/views/mes/md/item/components/select-dialog.vue b/src/views/mes/md/item/components/select-dialog.vue
index d377f66..f30fedb 100644
--- a/src/views/mes/md/item/components/select-dialog.vue
+++ b/src/views/mes/md/item/components/select-dialog.vue
@@ -23,11 +23,11 @@
const open = ref(false); // 寮圭獥鏄惁鎵撳紑
const multiple = ref(true); // 鏄惁澶氶��
- const selectedRows = ref<MesMdItemApi.Item[]>([]); // 宸查�夌墿鏂欏垪琛�
+ const selectedRows = ref<MesMdItemApi.Item[]>([]); // 宸查�夊搧绉嶅垪琛�
const selectedItemTypeId = ref<number>(); // 褰撳墠绛涢�夊垎绫荤紪鍙�
- const preSelectedIds = ref<number[]>([]); // 棰勯�夌墿鏂欑紪鍙峰垪琛�
- const productIds = ref<number[]>([]); // 闄愬畾鍙�夌殑鐗╂枡缂栧彿鍒楄〃锛堜负绌哄垯涓嶉檺鍒讹級
- const typeTreeRef = ref<InstanceType<typeof MdItemTypeTree>>(); // 鐗╂枡鍒嗙被鏍�
+ const preSelectedIds = ref<number[]>([]); // 棰勯�夊搧绉嶇紪鍙峰垪琛�
+ const productIds = ref<number[]>([]); // 闄愬畾鍙�夌殑鍝佺缂栧彿鍒楄〃锛堜负绌哄垯涓嶉檺鍒讹級
+ const typeTreeRef = ref<InstanceType<typeof MdItemTypeTree>>(); // 鍝佺鍒嗙被鏍�
/** 鑾峰彇褰撳墠琛ㄦ牸鏁版嵁 */
function getTableRows() {
@@ -78,13 +78,13 @@
handleConfirm();
}
- /** 鎸夊垎绫荤瓫閫夌墿鏂� */
+ /** 鎸夊垎绫荤瓫閫夊搧绉� */
function handleItemTypeNodeClick(row: MesMdItemTypeApi.ItemType | undefined) {
selectedItemTypeId.value = row?.id;
gridApi.query();
}
- /** 鍥炴樉棰勯�夌墿鏂� */
+ /** 鍥炴樉棰勯�夊搧绉� */
async function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
@@ -138,7 +138,7 @@
itemTypeId: selectedItemTypeId.value,
status: CommonStatusEnum.ENABLE,
};
- // 闄愬畾鍙�夌墿鏂欒寖鍥�
+ // 闄愬畾鍙�夊搧绉嶈寖鍥�
if (productIds.value.length > 0) {
params.pageSize = 1000;
params.ids = productIds.value.join(',');
@@ -178,7 +178,7 @@
await gridApi.formApi.resetForm();
}
- /** 鎵撳紑鐗╂枡閫夋嫨寮圭獥 */
+ /** 鎵撳紑鍝佺閫夋嫨寮圭獥 */
async function openModal(
selectedIds?: number[],
options?: { multiple?: boolean; productIds?: number[] }
@@ -197,13 +197,13 @@
await applyPreSelection();
}
- /** 鍏抽棴鐗╂枡閫夋嫨寮圭獥 */
+ /** 鍏抽棴鍝佺閫夋嫨寮圭獥 */
async function closeModal() {
open.value = false;
await resetQueryState();
}
- /** 纭閫夋嫨鐗╂枡 */
+ /** 纭閫夋嫨鍝佺 */
function handleConfirm() {
const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
if (rows.length === 0) {
@@ -219,7 +219,7 @@
<template>
<Modal v-model:open="open"
- title="鐗╂枡浜у搧閫夋嫨"
+ title="鍝佺浜у搧閫夋嫨"
width="80%"
:destroy-on-close="true"
@ok="handleConfirm"
@@ -230,7 +230,7 @@
@node-click="handleItemTypeNodeClick" />
</Card>
<div class="w-4/5">
- <Grid table-title="鐗╂枡浜у搧鍒楄〃" />
+ <Grid table-title="鍝佺浜у搧鍒楄〃" />
</div>
</div>
</Modal>
diff --git a/src/views/mes/md/item/components/select.vue b/src/views/mes/md/item/components/select.vue
index 6a45bdb..3213a01 100644
--- a/src/views/mes/md/item/components/select.vue
+++ b/src/views/mes/md/item/components/select.vue
@@ -27,7 +27,7 @@
disabled: false,
modelValue: undefined,
multiple: false,
- placeholder: '璇烽�夋嫨浜у搧鐗╂枡',
+ placeholder: '璇烽�夋嫨浜у搧鍝佺',
productIds: () => [],
},
);
@@ -54,7 +54,7 @@
: props.modelValue != null),
);
-/** 鏍规嵁鐗╂枡缂栧彿鍥炴樉閫夋嫨鍣� */
+/** 鏍规嵁鍝佺缂栧彿鍥炴樉閫夋嫨鍣� */
async function resolveItemsById(values: number | number[] | undefined) {
if (values == null || (Array.isArray(values) && values.length === 0)) {
selectedItems.value = [];
@@ -80,7 +80,7 @@
{ immediate: true },
);
-/** 娓呯┖宸查�夌墿鏂� */
+/** 娓呯┖宸查�夊搧绉� */
function clearSelected() {
selectedItems.value = [];
if (props.multiple) {
@@ -92,7 +92,7 @@
}
}
-/** 鎵撳紑鐗╂枡閫夋嫨寮圭獥 */
+/** 鎵撳紑鍝佺閫夋嫨寮圭獥 */
function handleClick(event: MouseEvent) {
if (props.disabled) {
return;
@@ -114,7 +114,7 @@
});
}
-/** 鍥炲~閫変腑鐨勭墿鏂� */
+/** 鍥炲~閫変腑鐨勫搧绉� */
function handleSelected(rows: MesMdItemApi.Item[]) {
if (props.multiple) {
selectedItems.value = rows;
diff --git a/src/views/mes/md/item/data.ts b/src/views/mes/md/item/data.ts
index 5d74bb2..d18c30f 100644
--- a/src/views/mes/md/item/data.ts
+++ b/src/views/mes/md/item/data.ts
@@ -332,31 +332,31 @@
];
}
-/** 鐗╂枡閫夋嫨寮圭獥鎼滅储琛ㄥ崟 */
+/** 鍝佺閫夋嫨寮圭獥鎼滅储琛ㄥ崟 */
export function useItemSelectGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
- label: '鐗╂枡缂栫爜',
+ label: '鍝佺缂栫爜',
component: 'Input',
componentProps: {
allowClear: true,
- placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�',
+ placeholder: '璇疯緭鍏ュ搧绉嶇紪鐮�',
},
},
{
fieldName: 'name',
- label: '鐗╂枡鍚嶇О',
+ label: '鍝佺鍚嶇О',
component: 'Input',
componentProps: {
allowClear: true,
- placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�',
+ placeholder: '璇疯緭鍏ュ搧绉嶅悕绉�',
},
},
];
}
-/** 鐗╂枡閫夋嫨寮圭獥鍒楄〃瀛楁 */
+/** 鍝佺閫夋嫨寮圭獥鍒楄〃瀛楁 */
export function useItemSelectGridColumns(
multiple = true,
): VxeTableGridOptions<MesMdItemApi.Item>['columns'] {
@@ -364,12 +364,12 @@
{ type: multiple ? 'checkbox' : 'radio', width: 50 },
{
field: 'code',
- title: '鐗╂枡缂栫爜',
+ title: '鍝佺缂栫爜',
width: 180,
},
{
field: 'name',
- title: '鐗╂枡鍚嶇О',
+ title: '鍝佺鍚嶇О',
minWidth: 160,
align: 'left',
},
@@ -387,7 +387,7 @@
},
{
field: 'itemOrProduct',
- title: '鐗╂枡/浜у搧',
+ title: '鍝佺/浜у搧',
width: 110,
align: 'center',
cellRender: {
diff --git a/src/views/mes/md/item/type/data.ts b/src/views/mes/md/item/type/data.ts
index 3858003..b9a95de 100644
--- a/src/views/mes/md/item/type/data.ts
+++ b/src/views/mes/md/item/type/data.ts
@@ -22,7 +22,7 @@
/** 琛ㄥ崟绫诲瀷 */
export type FormType = 'create' | 'detail' | 'update';
-/** 鏂板/淇敼鐗╂枡鍒嗙被鐨勮〃鍗� */
+/** 鏂板/淇敼鍝佺鍒嗙被鐨勮〃鍗� */
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
@@ -99,7 +99,7 @@
},
{
fieldName: 'itemOrProduct',
- label: '鐗╂枡/浜у搧鏍囪瘑',
+ label: '鍝佺/浜у搧鏍囪瘑',
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
@@ -185,7 +185,7 @@
},
{
field: 'itemOrProduct',
- title: '鐗╂枡/浜у搧',
+ title: '鍝佺/浜у搧',
width: 130,
align: 'center',
cellRender: {
diff --git a/src/views/mes/md/item/type/index.vue b/src/views/mes/md/item/type/index.vue
index 062b7d9..5d2eccd 100644
--- a/src/views/mes/md/item/type/index.vue
+++ b/src/views/mes/md/item/type/index.vue
@@ -102,12 +102,12 @@
<template>
<Page auto-content-height><FormModal @success="handleRefresh" />
- <Grid table-title="鐗╂枡鍒嗙被鍒楄〃">
+ <Grid table-title="鍝佺鍒嗙被鍒楄〃">
<template #toolbar-tools>
<TableAction
:actions="[
{
- label: $t('ui.actionTitle.create', ['鐗╂枡鍒嗙被']),
+ label: $t('ui.actionTitle.create', ['鍝佺鍒嗙被']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:md-item-type:create'],
diff --git a/src/views/mes/md/item/type/modules/form.vue b/src/views/mes/md/item/type/modules/form.vue
index 50d1ede..3e3bc58 100644
--- a/src/views/mes/md/item/type/modules/form.vue
+++ b/src/views/mes/md/item/type/modules/form.vue
@@ -21,8 +21,8 @@
const formData = ref<MesMdItemTypeApi.ItemType>();
const getTitle = computed(() => {
return formData.value?.id
- ? $t('ui.actionTitle.edit', ['鐗╂枡鍒嗙被'])
- : $t('ui.actionTitle.create', ['鐗╂枡鍒嗙被']);
+ ? $t('ui.actionTitle.edit', ['鍝佺鍒嗙被'])
+ : $t('ui.actionTitle.create', ['鍝佺鍒嗙被']);
});
const [Form, formApi] = useVbenForm({
diff --git a/src/views/mes/pro/bagCode/data.ts b/src/views/mes/pro/bagCode/data.ts
new file mode 100644
index 0000000..f611bcc
--- /dev/null
+++ b/src/views/mes/pro/bagCode/data.ts
@@ -0,0 +1,130 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProBagCodeApi } from '#/api/mes/pro/bagCode';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '琚嬬爜',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ヨ鐮侊紙浜岀淮鐮佸唴瀹癸級',
+ },
+ },
+ {
+ fieldName: 'batchCode',
+ label: '鎵规鍙�',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ユ壒娆″彿',
+ },
+ },
+ {
+ fieldName: 'uuid',
+ label: '鐢熸垚缁�',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ョ敓鎴愮粍缂栧彿',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '琚嬬爜鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PRO_BAG_CODE_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨琚嬬爜鐘舵��',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '鍒涘缓鏃堕棿',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesProBagCodeApi.BagCode>['columns'] {
+ return [
+ { type: 'checkbox', width: 50 },
+ {
+ field: 'code',
+ title: '琚嬬爜锛堜簩缁寸爜鍐呭锛�',
+ fixed: 'left',
+ minWidth: 220,
+ },
+ {
+ field: 'bagNo',
+ title: '琚嬪簭鍙�',
+ width: 80,
+ },
+ {
+ field: 'uuid',
+ title: '鐢熸垚缁�',
+ minWidth: 150,
+ },
+ {
+ field: 'batchCode',
+ title: '鎵规鍙�',
+ minWidth: 190,
+ },
+ {
+ field: 'lineCode',
+ title: '浜х嚎缂栫爜',
+ width: 100,
+ },
+ {
+ field: 'lineName',
+ title: '浜х嚎鍚嶇О',
+ width: 120,
+ },
+ {
+ field: 'palletCode',
+ title: '缁戝畾鎵樼洏鐮�',
+ minWidth: 190,
+ },
+ {
+ field: 'status',
+ title: '琚嬬爜鐘舵��',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PRO_BAG_CODE_STATUS },
+ },
+ },
+ {
+ field: 'printTime',
+ title: '瀵煎嚭鍗板埛鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/mes/pro/bagCode/index.vue b/src/views/mes/pro/bagCode/index.vue
new file mode 100644
index 0000000..f4ffa22
--- /dev/null
+++ b/src/views/mes/pro/bagCode/index.vue
@@ -0,0 +1,273 @@
+<script lang="ts" setup>
+ import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+ import type { MesProBagCodeApi } from "#/api/mes/pro/bagCode";
+
+ import { confirm, Page, useVbenModal } from "@vben/common-ui";
+ import { MesProBagCodeStatusEnum } from "@vben/constants";
+ import { downloadFileFromBlobPart } from "@vben/utils";
+
+ import { message } from "ant-design-vue";
+
+ import { ACTION_ICON, TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
+ import {
+ deleteBagCodesByUuid,
+ exportBagCodeExcel,
+ exportBagQr,
+ getBagCodePage,
+ unbindBagCodePallet,
+ } from "#/api/mes/pro/bagCode";
+ import { $t } from "#/locales";
+
+ import { useGridColumns, useGridFormSchema } from "./data";
+ import BindPalletModal from "./modules/bind-pallet.vue";
+ import GenerateModal from "./modules/generate-form.vue";
+ import QrTraceModal from "../components/qr-trace-modal.vue";
+
+ defineOptions({ name: "MesProBagCode" });
+
+ const [GenerateFormModal, generateFormModalApi] = useVbenModal({
+ connectedComponent: GenerateModal,
+ destroyOnClose: true,
+ });
+
+ const [BindPalletFormModal, bindPalletFormModalApi] = useVbenModal({
+ connectedComponent: BindPalletModal,
+ destroyOnClose: true,
+ });
+
+ const [QrTraceQueryModal, qrTraceQueryModalApi] = useVbenModal({
+ connectedComponent: QrTraceModal,
+ destroyOnClose: true,
+ });
+
+ /** 鍒锋柊琛ㄦ牸 */
+ function handleRefresh() {
+ gridApi.query();
+ }
+
+ /** 鐢熸垚鏁版嵁鍖� */
+ function handleGenerate() {
+ generateFormModalApi.open();
+ }
+
+ /** 鎵爜杩芥函 */
+ function handleTrace() {
+ qrTraceQueryModalApi.open();
+ }
+
+ /** 鏌ョ湅浜岀淮鐮侊紙琛屽唴棰勫~鐮佸�硷級 */
+ function handleViewQr(row: MesProBagCodeApi.BagCode) {
+ qrTraceQueryModalApi.setData({ code: row.code }).open();
+ }
+
+ /** 瀵煎嚭浜岀淮鐮� ZIP锛堟寜褰撳墠鏌ヨ鏉′欢鐨勭敓鎴愮粍/鎵规锛� */
+ async function handleExportQr() {
+ const values = await gridApi.formApi.getValues();
+ const params: { uuid?: string; batchId?: number } = {};
+ if (values.uuid) {
+ params.uuid = values.uuid;
+ }
+ if (values.batchId) {
+ params.batchId = values.batchId;
+ }
+ if (!params.uuid && !params.batchId) {
+ message.warning("璇峰厛鎸夌敓鎴愮粍鎴栨壒娆$瓫閫夊悗鍐嶅鍑轰簩缁寸爜");
+ return;
+ }
+ const data = await exportBagQr(params);
+ downloadFileFromBlobPart({ fileName: "琚嬬爜浜岀淮鐮�.zip", source: data });
+ }
+
+ /** 瀵煎嚭琚嬬爜 Excel锛堜氦浠樺嵃鍒凤級锛屽鍑哄悗琚嬬爜鐘舵�佸彉涓哄凡鍗板埛锛岄渶鍒锋柊鍒楄〃 */
+ async function handleExport() {
+ const data = await exportBagCodeExcel(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: "琚嬬爜鏁版嵁鍖�.xls", source: data });
+ message.success("瀵煎嚭鎴愬姛锛屽鍑虹殑琚嬬爜宸叉爣璁颁负宸插嵃鍒�");
+ handleRefresh();
+ }
+
+ /** 鑾峰彇鍕鹃�夌殑琚嬬爜璁板綍 */
+ function getSelectedRecords(): MesProBagCodeApi.BagCode[] | undefined {
+ const records = gridApi.grid.getCheckboxRecords() as MesProBagCodeApi.BagCode[];
+ if (records.length === 0) {
+ message.warning("璇疯嚦灏戝嬀閫変竴鏉¤鐮佽褰�");
+ return undefined;
+ }
+ return records;
+ }
+
+ /** 鎵归噺缁戝畾鎵樼洏 */
+ function handleBindPallet() {
+ const records = getSelectedRecords();
+ if (!records) return;
+ const batchIds = new Set(records.map((record) => record.batchId));
+ if (batchIds.size > 1) {
+ message.warning("璇烽�夋嫨鍚屼竴鎵规涓嬬殑琚嬬爜杩涜缁戝畾");
+ return;
+ }
+ if (records.some((record) => record.palletId)) {
+ message.warning("鎵�閫夎鐮佷腑瀛樺湪宸茬粦瀹氭墭鐩樼殑璁板綍锛岃鍏堣В缁�");
+ return;
+ }
+ if (
+ records.some(
+ (record) => record.status === MesProBagCodeStatusEnum.CANCELED
+ )
+ ) {
+ message.warning("宸蹭綔搴熺殑琚嬬爜涓嶅厑璁哥粦瀹氭墭鐩�");
+ return;
+ }
+ const first = records[0]!;
+ bindPalletFormModalApi
+ .setData({
+ batchId: first.batchId,
+ batchCode: first.batchCode,
+ ids: records.map((record) => record.id!),
+ })
+ .open();
+ }
+
+ /** 鎵归噺瑙g粦鎵樼洏 */
+ async function handleUnbindPallet() {
+ const records = getSelectedRecords();
+ if (!records) return;
+ if (records.some((record) => !record.palletId)) {
+ message.warning("鎵�閫夎鐮佷腑瀛樺湪鏈粦瀹氭墭鐩樼殑璁板綍锛屾棤娉曡В缁�");
+ return;
+ }
+ const palletIds = new Set(records.map((record) => record.palletId));
+ if (palletIds.size > 1) {
+ message.warning("璇烽�夋嫨缁戝畾鍦ㄥ悓涓�鎵樼洏涓婄殑琚嬬爜杩涜瑙g粦");
+ return;
+ }
+ try {
+ await confirm(`纭瑙g粦鎵�閫� ${records.length} 鏉¤鐮佸悧锛焋);
+ } catch {
+ return;
+ }
+ await unbindBagCodePallet({
+ palletId: records[0]!.palletId!,
+ ids: records.map((record) => record.id!),
+ });
+ message.success($t("ui.actionMessage.operationSuccess"));
+ handleRefresh();
+ }
+
+ /** 鍒犻櫎琚嬬爜鐢熸垚缁勶紙鏁版嵁鍖咃級 */
+ async function handleDeleteGroup(row: MesProBagCodeApi.BagCode) {
+ await deleteBagCodesByUuid(row.uuid!);
+ message.success($t("ui.actionMessage.deleteSuccess", [row.uuid]));
+ handleRefresh();
+ }
+
+ const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ checkboxConfig: {
+ highlight: true,
+ reserve: true,
+ },
+ columns: useGridColumns(),
+ height: "auto",
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getBagCodePage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: "id",
+ isHover: true,
+ },
+ showOverflow: true,
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesProBagCodeApi.BagCode>,
+ });
+</script>
+
+<template>
+ <Page auto-content-height>
+ <GenerateFormModal @success="handleRefresh" />
+ <BindPalletFormModal @success="handleRefresh" />
+ <QrTraceQueryModal />
+ <Grid table-title="琚嬬爜鍒楄〃锛堜竴琚嬩竴鐮侊級">
+ <template #toolbar-tools>
+ <TableAction :actions="[
+ {
+ label: '鐢熸垚鏁版嵁鍖�',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['mes:pro-bag-code:create'],
+ onClick: handleGenerate,
+ },
+ {
+ label: '缁戝畾鎵樼洏',
+ type: 'primary',
+ auth: ['mes:pro-bag-code:update'],
+ onClick: handleBindPallet,
+ },
+ {
+ label: '瑙g粦鎵樼洏',
+ auth: ['mes:pro-bag-code:update'],
+ onClick: handleUnbindPallet,
+ },
+ {
+ label: '鎵爜杩芥函',
+ icon: ACTION_ICON.SEARCH,
+ auth: ['mes:pro-bag-code:query'],
+ onClick: handleTrace,
+ },
+ {
+ label: '瀵煎嚭浜岀淮鐮�',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:pro-bag-code:export'],
+ onClick: handleExportQr,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:pro-bag-code:export'],
+ onClick: handleExport,
+ },
+ ]" />
+ </template>
+ <template #actions="{ row }">
+ <TableAction :actions="[
+ {
+ label: '鏌ョ湅浜岀淮鐮�',
+ type: 'link',
+ auth: ['mes:pro-bag-code:query'],
+ onClick: handleViewQr.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pro-bag-code:delete'],
+ ifShow:
+ row.status === MesProBagCodeStatusEnum.GENERATED &&
+ !row.palletId,
+ popConfirm: {
+ title:
+ '鍒犻櫎灏嗙Щ闄よ琚嬬爜鎵�灞炵殑鏁翠釜鐢熸垚缁勶紙鏁版嵁鍖咃級锛岀‘璁ゅ垹闄ゅ悧锛�',
+ confirm: handleDeleteGroup.bind(null, row),
+ },
+ },
+ ]" />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/pro/bagCode/modules/bind-pallet.vue b/src/views/mes/pro/bagCode/modules/bind-pallet.vue
new file mode 100644
index 0000000..a84abba
--- /dev/null
+++ b/src/views/mes/pro/bagCode/modules/bind-pallet.vue
@@ -0,0 +1,121 @@
+<script lang="ts" setup>
+ import type { VbenFormSchema } from "#/adapter/form";
+
+ import { useVbenModal } from "@vben/common-ui";
+ import { MesProPalletStatusEnum } from "@vben/constants";
+
+ import { message } from "ant-design-vue";
+
+ import { useVbenForm } from "#/adapter/form";
+ import { bindBagCodePallet } from "#/api/mes/pro/bagCode";
+ import { getPalletSimpleList } from "#/api/mes/pro/pallet";
+ import { $t } from "#/locales";
+
+ defineOptions({ name: "MesProBagCodeBindPallet" });
+
+ /** 寮圭獥浼犲叆鏁版嵁 */
+ interface ModalData {
+ batchId: number;
+ batchCode?: string;
+ ids: number[];
+ }
+
+ const emit = defineEmits(["success"]);
+
+ /** 鏋勫缓琛ㄥ崟锛堟墭鐩樹笅鎷夐�夐」鐢卞閮ㄥ姞杞芥敞鍏ワ級 */
+ function buildSchema(palletOptions: { label: string; value: number }[]): VbenFormSchema[] {
+ return [
+ {
+ fieldName: "batchCode",
+ label: "鐢熶骇鎵规",
+ component: "Input",
+ componentProps: {
+ disabled: true,
+ },
+ },
+ {
+ fieldName: "bagCount",
+ label: "缁戝畾琚嬫暟",
+ component: "InputNumber",
+ componentProps: {
+ class: "!w-full",
+ disabled: true,
+ },
+ },
+ {
+ fieldName: "palletId",
+ label: "鐩爣鎵樼洏",
+ component: "Select",
+ componentProps: {
+ optionFilterProp: "label",
+ options: palletOptions,
+ placeholder: "璇烽�夋嫨鐩爣鎵樼洏",
+ showSearch: true,
+ },
+ rules: "selectRequired",
+ },
+ ];
+ }
+
+ const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: "w-full",
+ },
+ labelWidth: 100,
+ },
+ layout: "horizontal",
+ schema: [],
+ showDefaultActions: false,
+ });
+
+ const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) return;
+ const data = modalApi.getData<ModalData>();
+ const values = await formApi.getValues();
+ modalApi.lock();
+ try {
+ await bindBagCodePallet({ palletId: values.palletId, ids: data.ids });
+ message.success($t("ui.actionMessage.operationSuccess"));
+ emit("success");
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) return;
+ const data = modalApi.getData<ModalData>();
+ modalApi.lock();
+ try {
+ // 鍔犺浇璇ユ壒娆′笅鐨勬墭鐩橈紙鎺掗櫎宸蹭綔搴燂級
+ const pallets = await getPalletSimpleList(data.batchId);
+ const options = pallets
+ .filter(
+ (pallet) => pallet.status !== MesProPalletStatusEnum.CANCELED
+ )
+ .map((pallet) => ({
+ label: `${pallet.code}锛堝凡瑁� ${pallet.bagCount ?? 0} 琚� / 鍙 ${
+ pallet.capacity ?? "涓嶉檺"
+ } 琚嬶級`,
+ value: pallet.id!,
+ }));
+ await formApi.setState({ schema: buildSchema(options) });
+ await formApi.setValues({
+ bagCount: data.ids.length,
+ batchCode: data.batchCode,
+ });
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ });
+</script>
+
+<template>
+ <Modal title="琚嬬爜缁戝畾鎵樼洏" class="w-2/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pro/bagCode/modules/generate-form.vue b/src/views/mes/pro/bagCode/modules/generate-form.vue
new file mode 100644
index 0000000..6ea36bc
--- /dev/null
+++ b/src/views/mes/pro/bagCode/modules/generate-form.vue
@@ -0,0 +1,111 @@
+<script lang="ts" setup>
+ import type { VbenFormSchema } from "#/adapter/form";
+ import type { MesProBagCodeApi } from "#/api/mes/pro/bagCode";
+
+ import { useVbenModal } from "@vben/common-ui";
+ import { MesProBatchStatusEnum } from "@vben/constants";
+
+ import { message } from "ant-design-vue";
+
+ import { useVbenForm } from "#/adapter/form";
+ import { generateBagCodes } from "#/api/mes/pro/bagCode";
+ import { getBatchSimpleList } from "#/api/mes/pro/batch";
+
+ defineOptions({ name: "MesProBagCodeGenerateForm" });
+
+ const emit = defineEmits(["success"]);
+
+ /** 鏋勫缓琛ㄥ崟锛堟壒娆′笅鎷夐�夐」鐢卞閮ㄥ姞杞芥敞鍏ワ級 */
+ function buildSchema(batchOptions: { label: string; value: number }[]): VbenFormSchema[] {
+ return [
+ {
+ fieldName: "batchId",
+ label: "鐢熶骇鎵规",
+ component: "Select",
+ componentProps: {
+ options: batchOptions,
+ optionFilterProp: "label",
+ placeholder: "璇烽�夋嫨鐢熶骇涓殑鎵规",
+ showSearch: true,
+ },
+ rules: "selectRequired",
+ },
+ {
+ fieldName: "count",
+ label: "鐢熸垚琚嬫暟",
+ component: "InputNumber",
+ componentProps: {
+ class: "!w-full",
+ min: 1,
+ placeholder: "璇疯緭鍏ラ渶瑕佺敓鎴愮殑琚嬬爜鏁伴噺",
+ precision: 0,
+ },
+ rules: "required",
+ },
+ {
+ fieldName: "remark",
+ label: "澶囨敞",
+ component: "Textarea",
+ componentProps: {
+ placeholder: "璇疯緭鍏ュ娉�",
+ rows: 2,
+ },
+ },
+ ];
+ }
+
+ const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: "w-full",
+ },
+ labelWidth: 100,
+ },
+ layout: "horizontal",
+ schema: [],
+ showDefaultActions: false,
+ });
+
+ const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) return;
+ modalApi.lock();
+ try {
+ const values =
+ (await formApi.getValues()) as MesProBagCodeApi.GenerateParams;
+ await generateBagCodes(values);
+ message.success(`宸叉垚鍔熺敓鎴� ${values.count} 鏉¤鐮乣);
+ emit("success");
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) return;
+ modalApi.lock();
+ try {
+ // 浠呯敓浜т腑鐨勬壒娆″厑璁哥敓鎴愯鐮�
+ const batches = await getBatchSimpleList();
+ const options = batches
+ .filter(
+ (batch) => batch.status === MesProBatchStatusEnum.PRODUCING
+ )
+ .map((batch) => ({
+ label: batch.code!,
+ value: batch.id!,
+ }));
+ await formApi.setState({ schema: buildSchema(options) });
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ });
+</script>
+
+<template>
+ <Modal title="鐢熸垚琚嬬爜鏁版嵁鍖�" class="w-2/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pro/batch/data.ts b/src/views/mes/pro/batch/data.ts
new file mode 100644
index 0000000..f226782
--- /dev/null
+++ b/src/views/mes/pro/batch/data.ts
@@ -0,0 +1,336 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProBatchApi } from '#/api/mes/pro/batch';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { MdItemSelect } from '#/views/mes/md/item/components';
+import { ProLineSelect } from '#/views/mes/pro/line/components';
+import { UserSelect } from '#/views/system/user/components';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+ const disabled = formType === 'detail';
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '鎵规鍙�',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ placeholder: '淇濆瓨鍚庤嚜鍔ㄧ敓鎴�',
+ },
+ dependencies: {
+ triggerFields: [''],
+ show: () => formType !== 'create',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鎵规鐘舵��',
+ component: 'Select',
+ componentProps: {
+ disabled: true,
+ options: getDictOptions(DICT_TYPE.MES_PRO_BATCH_STATUS, 'number'),
+ },
+ dependencies: {
+ triggerFields: [''],
+ show: () => formType !== 'create',
+ },
+ },
+ {
+ fieldName: 'itemId',
+ label: '鍝佺',
+ component: markRaw(MdItemSelect),
+ componentProps: {
+ disabled,
+ placeholder: '璇烽�夋嫨鍝佺锛堜骇鍝佸搧绉嶏級',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'lineId',
+ label: '浜х嚎',
+ component: markRaw(ProLineSelect),
+ componentProps: {
+ disabled,
+ placeholder: '璇烽�夋嫨浜х嚎',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'produceDate',
+ label: '鐢熶骇鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ disabled,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ placeholder: '璇烽�夋嫨鐢熶骇鏃ユ湡',
+ showTime: true,
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'planQuantity',
+ label: '璁″垝鏁伴噺',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ disabled,
+ min: 0,
+ placeholder: '璇疯緭鍏ヨ鍒掓暟閲�',
+ precision: 2,
+ },
+ },
+ {
+ fieldName: 'ownerUserId',
+ label: '璐d换浜�',
+ component: markRaw(UserSelect),
+ componentProps: {
+ disabled,
+ placeholder: '璇烽�夋嫨璐d换浜�',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-3',
+ componentProps: {
+ disabled,
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '鎵规鍙�',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ユ壒娆″彿',
+ },
+ },
+ {
+ fieldName: 'itemId',
+ label: '鍝佺',
+ component: markRaw(MdItemSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨鍝佺',
+ },
+ },
+ {
+ fieldName: 'lineCode',
+ label: '浜х嚎缂栫爜',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ヤ骇绾跨紪鐮�',
+ },
+ },
+ {
+ fieldName: 'lineName',
+ label: '浜х嚎鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ヤ骇绾垮悕绉�',
+ },
+ },
+ {
+ fieldName: 'ownerUserId',
+ label: '璐d换浜�',
+ component: markRaw(UserSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨璐d换浜�',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鎵规鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PRO_BATCH_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鎵规鐘舵��',
+ },
+ },
+ {
+ fieldName: 'produceDate',
+ label: '鐢熶骇鏃ユ湡',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '鍒涘缓鏃堕棿',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesProBatchApi.Batch>['columns'] {
+ return [
+ {
+ field: 'code',
+ title: '鎵规鍙�',
+ fixed: 'left',
+ minWidth: 200,
+ slots: { default: 'code' },
+ },
+ {
+ field: 'itemCode',
+ title: '鍝佺缂栫爜',
+ width: 120,
+ },
+ {
+ field: 'itemName',
+ title: '鍝佺鍚嶇О',
+ width: 140,
+ },
+ {
+ field: 'lineCode',
+ title: '浜х嚎缂栫爜',
+ width: 100,
+ },
+ {
+ field: 'lineName',
+ title: '浜х嚎鍚嶇О',
+ width: 120,
+ },
+ {
+ field: 'produceDate',
+ title: '鐢熶骇鏃ユ湡',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'planQuantity',
+ title: '璁″垝鏁伴噺',
+ width: 100,
+ },
+ {
+ field: 'ownerUserName',
+ title: '璐d换浜�',
+ width: 100,
+ },
+ {
+ field: 'bagQuantity',
+ title: '琚嬬爜鏁伴噺',
+ width: 90,
+ },
+ {
+ field: 'palletQuantity',
+ title: '鎵樼洏鐮佹暟閲�',
+ width: 100,
+ },
+ {
+ field: 'status',
+ title: '鎵规鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PRO_BATCH_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 鐢熶骇浜哄憳閰嶇疆瀛愯〃鐨勫瓧娈碉紙鍙紪杈戞�侊級 */
+export function useWorkerEditGridColumns(): VxeTableGridOptions<MesProBatchApi.BatchWorker>['columns'] {
+ return [
+ {
+ field: 'userId',
+ title: '鐢熶骇浜哄憳',
+ minWidth: 200,
+ slots: { default: 'userId' },
+ },
+ {
+ field: 'workerType',
+ title: '宀椾綅绫诲瀷',
+ width: 160,
+ slots: { default: 'workerType' },
+ },
+ {
+ field: 'remark',
+ title: '澶囨敞',
+ minWidth: 160,
+ slots: { default: 'remark' },
+ },
+ {
+ title: '鎿嶄綔',
+ width: 80,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 鐢熶骇浜哄憳閰嶇疆瀛愯〃鐨勫瓧娈碉紙鍙鎬侊級 */
+export function useWorkerReadGridColumns(): VxeTableGridOptions<MesProBatchApi.BatchWorker>['columns'] {
+ return [
+ {
+ field: 'userName',
+ title: '浜哄憳濮撳悕',
+ minWidth: 160,
+ },
+ {
+ field: 'workerType',
+ title: '宀椾綅绫诲瀷',
+ width: 160,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PRO_BATCH_WORKER_TYPE },
+ },
+ },
+ {
+ field: 'remark',
+ title: '澶囨敞',
+ minWidth: 160,
+ },
+ ];
+}
diff --git a/src/views/mes/pro/batch/index.vue b/src/views/mes/pro/batch/index.vue
new file mode 100644
index 0000000..73dfe65
--- /dev/null
+++ b/src/views/mes/pro/batch/index.vue
@@ -0,0 +1,238 @@
+<script lang="ts" setup>
+ import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+ import type { MesProBatchApi } from "#/api/mes/pro/batch";
+
+ import { Page, useVbenModal } from "@vben/common-ui";
+ import { MesProBatchStatusEnum } from "@vben/constants";
+ import { downloadFileFromBlobPart } from "@vben/utils";
+
+ import { Button, message } from "ant-design-vue";
+
+ import { ACTION_ICON, TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
+ import {
+ deleteBatch,
+ exportBatchExcel,
+ exportBatchQr,
+ getBatchPage,
+ updateBatchStatus,
+ } from "#/api/mes/pro/batch";
+ import { $t } from "#/locales";
+
+ import { useGridColumns, useGridFormSchema } from "./data";
+ import Form from "./modules/form.vue";
+ import QrTraceModal from "../components/qr-trace-modal.vue";
+
+ const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+ });
+
+ const [QrTraceQueryModal, qrTraceQueryModalApi] = useVbenModal({
+ connectedComponent: QrTraceModal,
+ destroyOnClose: true,
+ });
+
+ /** 鍒锋柊琛ㄦ牸 */
+ function handleRefresh() {
+ gridApi.query();
+ }
+
+ /** 鎵爜杩芥函 */
+ function handleTrace() {
+ qrTraceQueryModalApi.open();
+ }
+
+ /** 鏌ョ湅浜岀淮鐮侊紙琛屽唴棰勫~鐮佸�硷級 */
+ function handleViewQr(row: MesProBatchApi.Batch) {
+ qrTraceQueryModalApi.setData({ code: row.code }).open();
+ }
+
+ /** 鎵撳嵃鏁村浜岀淮鐮侊紙鎵规鍥� + 鍏ㄩ儴鎵樼洏鍥� ZIP锛� */
+ async function handlePrintQrSet(row: MesProBatchApi.Batch) {
+ const data = await exportBatchQr(row.id!);
+ downloadFileFromBlobPart({
+ fileName: `鎵规浜岀淮鐮�-${row.code}.zip`,
+ source: data,
+ });
+ }
+
+ /** 鏂板鎵规 */
+ function handleCreate() {
+ formModalApi.setData({ formType: "create" }).open();
+ }
+
+ /** 鏌ョ湅璇︽儏 */
+ function handleDetail(row: MesProBatchApi.Batch) {
+ formModalApi.setData({ formType: "detail", id: row.id }).open();
+ }
+
+ /** 缂栬緫鎵规 */
+ function handleEdit(row: MesProBatchApi.Batch) {
+ formModalApi.setData({ formType: "update", id: row.id }).open();
+ }
+
+ /** 鍒犻櫎鎵规 */
+ async function handleDelete(row: MesProBatchApi.Batch) {
+ await deleteBatch(row.id!);
+ message.success($t("ui.actionMessage.deleteSuccess", [row.code]));
+ handleRefresh();
+ }
+
+ /** 鏇存柊鎵规鐘舵�� */
+ async function handleUpdateStatus(
+ row: MesProBatchApi.Batch,
+ status: number,
+ statusName: string
+ ) {
+ await updateBatchStatus(row.id!, status);
+ message.success(`鎵规宸�${statusName}`);
+ handleRefresh();
+ }
+
+ /** 瀵煎嚭琛ㄦ牸 */
+ async function handleExport() {
+ const data = await exportBatchExcel(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: "鐢熶骇鎵规.xls", source: data });
+ }
+
+ const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: "auto",
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getBatchPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: "id",
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesProBatchApi.Batch>,
+ });
+</script>
+
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <QrTraceQueryModal />
+ <Grid table-title="鐢熶骇鎵规鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['鎵规']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['mes:pro-batch:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: '鎵爜杩芥函',
+ icon: ACTION_ICON.SEARCH,
+ auth: ['mes:pro-batch:query'],
+ onClick: handleTrace,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:pro-batch:export'],
+ onClick: handleExport,
+ },
+ ]" />
+ </template>
+ <template #code="{ row }">
+ <Button type="link" @click="handleDetail(row)">
+ {{ row.code }}
+ </Button>
+ </template>
+ <template #actions="{ row }">
+ <TableAction :actions="[
+ {
+ label: '鏌ョ湅浜岀淮鐮�',
+ type: 'link',
+ auth: ['mes:pro-batch:query'],
+ onClick: handleViewQr.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pro-batch:update'],
+ ifShow:
+ row.status === MesProBatchStatusEnum.DRAFT ||
+ row.status === MesProBatchStatusEnum.PRODUCING,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pro-batch:delete'],
+ ifShow:
+ row.status === MesProBatchStatusEnum.DRAFT ||
+ row.status === MesProBatchStatusEnum.CANCELED,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.code]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ :drop-down-actions="[
+ {
+ label: '寮�濮嬬敓浜�',
+ auth: ['mes:pro-batch:update'],
+ ifShow: row.status === MesProBatchStatusEnum.DRAFT,
+ popConfirm: {
+ title: '纭寮�濮嬬敓浜ц鎵规鍚楋紵',
+ confirm: () =>
+ handleUpdateStatus(row, MesProBatchStatusEnum.PRODUCING, '寮�濮嬬敓浜�'),
+ },
+ },
+ {
+ label: '瀹屾垚',
+ auth: ['mes:pro-batch:update'],
+ ifShow: row.status === MesProBatchStatusEnum.PRODUCING,
+ popConfirm: {
+ title: '纭瀹屾垚璇ユ壒娆″悧锛�',
+ confirm: () =>
+ handleUpdateStatus(row, MesProBatchStatusEnum.FINISHED, '瀹屾垚'),
+ },
+ },
+ {
+ label: '浣滃簾',
+ auth: ['mes:pro-batch:update'],
+ ifShow:
+ row.status === MesProBatchStatusEnum.DRAFT ||
+ row.status === MesProBatchStatusEnum.PRODUCING,
+ popConfirm: {
+ title: '纭浣滃簾璇ユ壒娆″悧锛熶綔搴熷悗涓嶅彲鎭㈠銆�',
+ confirm: () =>
+ handleUpdateStatus(row, MesProBatchStatusEnum.CANCELED, '浣滃簾'),
+ },
+ },
+ {
+ label: '鎵撳嵃鏁村浜岀淮鐮�',
+ auth: ['mes:pro-batch:export'],
+ onClick: () => handlePrintQrSet(row),
+ },
+ ]" />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/pro/batch/modules/form.vue b/src/views/mes/pro/batch/modules/form.vue
new file mode 100644
index 0000000..2e45243
--- /dev/null
+++ b/src/views/mes/pro/batch/modules/form.vue
@@ -0,0 +1,281 @@
+<script lang="ts" setup>
+ import type { FormType } from "../data";
+
+ import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+ import type { MesProBatchApi } from "#/api/mes/pro/batch";
+ import type { SystemUserApi } from "#/api/system/user";
+
+ import { computed, nextTick, ref } from "vue";
+
+ import { useVbenModal } from "@vben/common-ui";
+ import { DICT_TYPE } from "@vben/constants";
+ import { getDictOptions } from "@vben/hooks";
+
+ import { Button, Input, message, Select } from "ant-design-vue";
+
+ import { useVbenForm } from "#/adapter/form";
+ import { TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
+ import { createBatch, getBatch, updateBatch } from "#/api/mes/pro/batch";
+ import { $t } from "#/locales";
+ import { UserSelect } from "#/views/system/user/components";
+
+ import {
+ useFormSchema,
+ useWorkerEditGridColumns,
+ useWorkerReadGridColumns,
+ } from "../data";
+
+ defineOptions({ name: "MesProBatchForm" });
+
+ const emit = defineEmits(["success"]);
+
+ const formType = ref<FormType>("create");
+ const formData = ref<MesProBatchApi.Batch>();
+
+ const isEditable = computed(() =>
+ ["create", "update"].includes(formType.value)
+ );
+
+ const getTitle = computed(() => {
+ switch (formType.value) {
+ case "detail": {
+ return $t("ui.actionTitle.view", ["鐢熶骇鎵规"]);
+ }
+ case "update": {
+ return $t("ui.actionTitle.edit", ["鐢熶骇鎵规"]);
+ }
+ default: {
+ return $t("ui.actionTitle.create", ["鐢熶骇鎵规"]);
+ }
+ }
+ });
+
+ const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: "w-full",
+ },
+ formItemClass: "col-span-1",
+ labelWidth: 110,
+ },
+ layout: "horizontal",
+ schema: [],
+ showDefaultActions: false,
+ wrapperClass: "grid-cols-3",
+ });
+
+ // ========== 鐢熶骇浜哄憳閰嶇疆瀛愯〃 ==========
+ const [WorkerGrid, workerGridApi] = useVbenVxeGrid({
+ gridOptions: {
+ autoResize: true,
+ border: true,
+ columns: useWorkerEditGridColumns(),
+ minHeight: 160,
+ pagerConfig: { enabled: false },
+ rowConfig: { isHover: true, keyField: "_rowKey" },
+ showOverflow: true,
+ toolbarConfig: { enabled: false },
+ } as VxeTableGridOptions<MesProBatchApi.BatchWorker>,
+ });
+
+ /** 鏂板琛岀殑涓存椂琛屽彿锛堣礋鏁拌嚜澧烇紝閬垮厤涓� loadData 鐨� index 鍐茬獊锛� */
+ let workerRowSeq = 0;
+
+ /** 娣诲姞浜哄憳琛� */
+ function handleAddWorker() {
+ workerGridApi.grid.insertAt({ _rowKey: -(++workerRowSeq) }, -1);
+ }
+
+ /** 鍒犻櫎浜哄憳琛� */
+ function handleDeleteWorker(row: MesProBatchApi.BatchWorker) {
+ workerGridApi.grid.remove(row);
+ }
+
+ /** 浜哄憳閫夋嫨缂栧彿鍙樺寲锛堝崟閫夊満鏅笅鍙栫涓�涓�硷級 */
+ function handleWorkerUserIdChange(
+ value: number | number[] | undefined,
+ row: MesProBatchApi.BatchWorker
+ ) {
+ row.userId = Array.isArray(value) ? value[0] : value;
+ }
+
+ /** 浜哄憳閫夋嫨鍙樺寲鏃惰褰曞鍚嶏紙渚夸簬鏍¢獙鎻愮ず锛� */
+ function handleWorkerUserChange(
+ user: SystemUserApi.User | SystemUserApi.User[] | undefined,
+ row: MesProBatchApi.BatchWorker
+ ) {
+ row.userName = Array.isArray(user) ? user[0]?.nickname : user?.nickname;
+ }
+
+ /** 宀椾綅绫诲瀷瀛楀吀閫夐」 */
+ const workerTypeOptions = computed(() =>
+ getDictOptions(DICT_TYPE.MES_PRO_BATCH_WORKER_TYPE, "number").map(
+ (item) => ({ label: item.label, value: item.value as number })
+ )
+ );
+
+ /** 鏍¢獙骞惰幏鍙栦汉鍛橀厤缃垪琛� */
+ async function getValidWorkers(): Promise<
+ MesProBatchApi.BatchWorker[] | undefined
+ > {
+ // getData() 鍙繑鍥� loadData 鍔犺浇鐨勬暟鎹紱insertAt 鎻掑叆鐨勮闇�閫氳繃
+ // getInsertRecords() 鑾峰彇锛屽苟鎺掗櫎 getRemoveRecords() 涓凡鍒犻櫎鐨勮
+ const data = workerGridApi.grid.getData() as Array<
+ MesProBatchApi.BatchWorker & { _rowKey?: number }
+ >;
+ const removeRecords = workerGridApi.grid.getRemoveRecords() as Array<
+ MesProBatchApi.BatchWorker & { _rowKey?: number }
+ >;
+ const insertRecords = workerGridApi.grid.getInsertRecords() as Array<
+ MesProBatchApi.BatchWorker & { _rowKey?: number }
+ >;
+ const rows = [
+ ...data.filter(
+ (row) => !removeRecords.some((removed) => removed._rowKey === row._rowKey)
+ ),
+ ...insertRecords,
+ ].map(({ _rowKey, ...rest }) => rest);
+ if (rows.length === 0) {
+ message.warning("璇疯嚦灏戦厤缃竴鍚嶇敓浜т汉鍛�");
+ return undefined;
+ }
+ for (const [index, row] of rows.entries()) {
+ if (!row.userId) {
+ message.warning(`璇烽�夋嫨绗� ${index + 1} 琛岀殑鐢熶骇浜哄憳`);
+ return undefined;
+ }
+ if (!row.workerType) {
+ message.warning(`璇烽�夋嫨绗� ${index + 1} 琛岀殑宀椾綅绫诲瀷`);
+ return undefined;
+ }
+ }
+ return rows;
+ }
+
+ /** 绛夊緟浜哄憳瀛愯〃 grid 鎸傝浇瀹屾垚锛堝脊绐楁墦寮�鐬棿 onOpenChange 鍐呴儴琛ㄦ牸鍙兘灏氭湭鎸傝浇锛� */
+ async function waitWorkerGridReady(): Promise<void> {
+ await nextTick();
+ for (let i = 0; i < 100; i++) {
+ if (typeof workerGridApi.grid?.loadData === "function") {
+ return;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 30));
+ }
+ }
+
+ const [Modal, modalApi] = useVbenModal({
+ showCancelButton: false, // 鑷畾涔夊彇娑�/鍏抽棴鎸夐挳锛岀鐢ㄩ粯璁ゅ彇娑堟寜閽伩鍏嶉噸澶�
+ async onConfirm() {
+ if (!isEditable.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) return;
+ const workers = await getValidWorkers();
+ if (!workers) return;
+ modalApi.lock();
+ try {
+ const data = (await formApi.getValues()) as MesProBatchApi.Batch;
+ if (formData.value?.id) {
+ await updateBatch({ ...data, id: formData.value.id, workers });
+ } else {
+ await createBatch({ ...data, workers });
+ }
+ message.success($t("ui.actionMessage.operationSuccess"));
+ emit("success");
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ formApi.setState({ schema: useFormSchema(formType.value) });
+ // 鏍规嵁琛ㄥ崟鎬佸垏鎹汉鍛樺瓙琛ㄥ垪
+ workerGridApi.setGridOptions({
+ columns: isEditable.value
+ ? useWorkerEditGridColumns()
+ : useWorkerReadGridColumns(),
+ });
+ if (data?.id) {
+ modalApi.lock();
+ try {
+ formData.value = await getBatch(data.id);
+ await formApi.setValues(formData.value);
+ const workers = (formData.value.workers ?? []).map(
+ (worker, index) => ({ ...worker, _rowKey: worker.id ?? index })
+ );
+ await waitWorkerGridReady();
+ await workerGridApi.grid.loadData(workers);
+ } finally {
+ modalApi.unlock();
+ }
+ } else {
+ await waitWorkerGridReady();
+ await workerGridApi.grid.loadData([]);
+ }
+ },
+ });
+</script>
+
+<template>
+ <Modal :title="getTitle"
+ class="w-3/5"
+ content-class="!max-h-[80vh] overflow-y-auto">
+ <Form class="mx-4" />
+ <div class="mx-4 mb-4">
+ <div class="mb-2 flex items-center justify-between">
+ <span class="font-medium">鐢熶骇浜哄憳閰嶇疆</span>
+ <Button v-if="isEditable" size="small" type="primary" @click="handleAddWorker">
+ 娣诲姞浜哄憳
+ </Button>
+ </div>
+ <WorkerGrid>
+ <template #userId="{ row }">
+ <UserSelect
+ :model-value="row.userId"
+ placeholder="璇烽�夋嫨鐢熶骇浜哄憳"
+ @update:model-value="handleWorkerUserIdChange($event, row)"
+ @change="handleWorkerUserChange($event, row)"
+ />
+ </template>
+ <template #workerType="{ row }">
+ <Select
+ v-model:value="row.workerType"
+ :options="workerTypeOptions"
+ class="w-full"
+ placeholder="璇烽�夋嫨宀椾綅绫诲瀷"
+ />
+ </template>
+ <template #remark="{ row }">
+ <Input v-model:value="row.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ onClick: handleDeleteWorker.bind(null, row),
+ },
+ ]"
+ />
+ </template>
+ </WorkerGrid>
+ </div>
+ <template #append-footer>
+ <div class="flex gap-2">
+ <Button @click="modalApi.close()">
+ {{ isEditable ? '鍙栨秷' : '鍏抽棴' }}
+ </Button>
+ </div>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/mes/pro/components/qr-trace-modal.vue b/src/views/mes/pro/components/qr-trace-modal.vue
new file mode 100644
index 0000000..44813d4
--- /dev/null
+++ b/src/views/mes/pro/components/qr-trace-modal.vue
@@ -0,0 +1,569 @@
+<script lang="ts" setup>
+ import type { MesProBagCodeApi } from "#/api/mes/pro/bagCode";
+ import type { MesProBatchApi } from "#/api/mes/pro/batch";
+ import type { MesProPalletApi } from "#/api/mes/pro/pallet";
+
+ import { ref } from "vue";
+
+ import { Barcode, useVbenModal } from "@vben/common-ui";
+ import { DICT_TYPE } from "@vben/constants";
+ import { getDictLabel } from "@vben/hooks";
+
+ import { Button, Empty, Input, message, Spin, Tag } from "ant-design-vue";
+
+ import { getBagCodeByCode } from "#/api/mes/pro/bagCode";
+ import { getBatchByCode } from "#/api/mes/pro/batch";
+ import { getPalletByCode } from "#/api/mes/pro/pallet";
+ import { useDescription } from "#/components/description";
+
+ defineOptions({ name: "MesProQrTraceModal" });
+
+ type TraceType = "batch" | "pallet" | "bag";
+
+ const MAX_CODE_LENGTH = 64;
+
+ const codeInput = ref<string>();
+ const currentCode = ref<string>();
+ const traceType = ref<TraceType>();
+ const loading = ref(false);
+ const searched = ref(false);
+ const searchError = ref(false);
+ const bagTrace = ref<MesProBagCodeApi.BagCodeTrace>();
+ const palletTrace = ref<MesProPalletApi.PalletTrace>();
+ const batchTrace = ref<MesProBatchApi.BatchTrace>();
+ const barcodeRef = ref<InstanceType<typeof Barcode>>();
+
+ const traceTypeLabel = ref("");
+
+ /** 璇嗗埆浜岀淮鐮佸唴瀹圭被鍨嬶細鎵规鍙� / 鎵樼洏鐮� / 琚嬬爜 */
+ function resolveCodeType(raw: string): TraceType | undefined {
+ const value = raw.trim().toUpperCase();
+ if (/^.*-P\d{2}$/.test(value)) {
+ return "pallet";
+ }
+ if (/^.*-\d{4}$/.test(value)) {
+ return "bag";
+ }
+ return "batch";
+ }
+
+ function resetState() {
+ currentCode.value = undefined;
+ traceType.value = undefined;
+ traceTypeLabel.value = "";
+ bagTrace.value = undefined;
+ palletTrace.value = undefined;
+ batchTrace.value = undefined;
+ searched.value = false;
+ searchError.value = false;
+ }
+
+ /** 鎵爜/杈撳叆浜岀淮鐮佸唴瀹规煡璇㈣拷婧俊鎭� */
+ async function handleSearch(raw?: string) {
+ const value = (raw ?? codeInput.value)?.trim().toUpperCase();
+ if (!value) {
+ message.warning("璇锋壂鎻忔垨杈撳叆浜岀淮鐮佸唴瀹�");
+ return;
+ }
+ if (value.length > MAX_CODE_LENGTH) {
+ message.warning("浜岀淮鐮佸唴瀹硅繃闀匡紝璇锋鏌ユ槸鍚︽壂鎻忔纭�");
+ return;
+ }
+ const type = resolveCodeType(value);
+ if (!type) {
+ message.warning("鏃犳硶璇嗗埆浜岀淮鐮佺被鍨�");
+ return;
+ }
+ loading.value = true;
+ searched.value = true;
+ searchError.value = false;
+ currentCode.value = value;
+ traceType.value = undefined;
+ traceTypeLabel.value = "";
+ bagTrace.value = undefined;
+ palletTrace.value = undefined;
+ batchTrace.value = undefined;
+ try {
+ if (type === "batch") {
+ batchTrace.value = await getBatchByCode(value);
+ traceTypeLabel.value = "鎵规鍙�";
+ } else if (type === "pallet") {
+ palletTrace.value = await getPalletByCode(value);
+ traceTypeLabel.value = "鎵樼洏鐮�";
+ } else {
+ bagTrace.value = await getBagCodeByCode(value);
+ traceTypeLabel.value = "琚嬬爜";
+ }
+ traceType.value = type;
+ } catch {
+ searchError.value = true;
+ currentCode.value = undefined;
+ traceType.value = undefined;
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ /** 褰撳墠杩芥函鏁版嵁鐨勫叧閿俊鎭紙涓嬭浇/鎵撳嵃鍥剧墖鍙犲姞鏂囧瓧鐢級 */
+ function buildLabelText(): { label: string; value: string }[] {
+ if (traceType.value === "bag" && bagTrace.value) {
+ const d = bagTrace.value;
+ return [
+ { label: "琚嬬爜", value: d.code || "" },
+ { label: "琚嬪簭鍙�", value: d.bagNo ? String(d.bagNo) : "" },
+ { label: "鎵规鍙�", value: d.batchCode || "" },
+ { label: "鍝佺", value: d.itemName || "" },
+ { label: "瑙勬牸", value: d.itemSpecification || "" },
+ {
+ label: "浜х嚎",
+ value: [d.lineCode, d.lineName].filter(Boolean).join(" / "),
+ },
+ { label: "鐢熶骇鏃ユ湡", value: d.produceDate || "" },
+ { label: "璐d换浜�", value: d.ownerUserName || "" },
+ ];
+ }
+ if (traceType.value === "pallet" && palletTrace.value) {
+ const d = palletTrace.value;
+ return [
+ { label: "鎵樼洏鐮�", value: d.code || "" },
+ { label: "鎵樼洏搴忓彿", value: d.palletNo ? String(d.palletNo) : "" },
+ { label: "鎵规鍙�", value: d.batchCode || "" },
+ { label: "鍝佺", value: d.itemName || "" },
+ { label: "瑙勬牸", value: d.itemSpecification || "" },
+ {
+ label: "浜х嚎",
+ value: [d.lineCode, d.lineName].filter(Boolean).join(" / "),
+ },
+ { label: "鐢熶骇鏃ユ湡", value: d.produceDate || "" },
+ { label: "璐d换浜�", value: d.ownerUserName || "" },
+ ];
+ }
+ if (batchTrace.value) {
+ const d = batchTrace.value;
+ return [
+ { label: "鎵规鍙�", value: d.code || "" },
+ { label: "鍝佺", value: d.itemName || "" },
+ { label: "瑙勬牸", value: d.itemSpecification || "" },
+ {
+ label: "浜х嚎",
+ value: [d.lineCode, d.lineName].filter(Boolean).join(" / "),
+ },
+ { label: "鐢熶骇鏃ユ湡", value: d.produceDate || "" },
+ { label: "璁″垝鏁伴噺", value: d.planQuantity ? String(d.planQuantity) : "" },
+ { label: "璐d换浜�", value: d.ownerUserName || "" },
+ ];
+ }
+ return [];
+ }
+
+ function escapeHtml(value: string) {
+ return value
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">");
+ }
+
+ /** 涓嬭浇浜岀淮鐮� PNG锛堜簩缁寸爜 + 鍏抽敭淇℃伅鏂囧瓧鍚堟垚锛� */
+ function handleDownload() {
+ const base64 = barcodeRef.value?.getImageBase64();
+ if (!base64) {
+ message.warning("浜岀淮鐮佺敓鎴愬け璐ワ紝鏃犳硶涓嬭浇");
+ return;
+ }
+ const items = buildLabelText();
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+ if (!ctx) {
+ message.error("Canvas 鍒濆鍖栧け璐�");
+ return;
+ }
+ const img = new Image();
+ img.onload = () => {
+ const padding = 20;
+ const infoX = img.width + padding * 2;
+ const infoWidth = 320;
+ canvas.width = infoX + infoWidth + padding;
+ canvas.height = Math.max(img.height + padding * 2, 300);
+ ctx.fillStyle = "#ffffff";
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(img, padding, padding);
+ ctx.fillStyle = "#000000";
+ let y = padding + 10;
+ const lineHeight = 26;
+ items.forEach((item) => {
+ ctx.font = "bold 14px 'Microsoft YaHei', Arial, sans-serif";
+ ctx.fillText(`${item.label}:`, infoX, y);
+ y += 18;
+ ctx.font = "14px 'Microsoft YaHei', Arial, sans-serif";
+ const text = item.value || "-";
+ const maxWidth = infoWidth - 10;
+ if (ctx.measureText(text).width > maxWidth) {
+ let currentText = text;
+ while (currentText.length > 0) {
+ let line = "";
+ for (let i = 0; i < currentText.length; i++) {
+ const testLine = line + currentText[i];
+ if (ctx.measureText(testLine).width > maxWidth) {
+ break;
+ }
+ line = testLine;
+ }
+ ctx.fillText(line, infoX + 5, y);
+ y += 16;
+ currentText = currentText.slice(line.length);
+ }
+ y += 6;
+ } else {
+ ctx.fillText(text, infoX + 5, y);
+ y += lineHeight;
+ }
+ });
+ const link = document.createElement("a");
+ link.href = canvas.toDataURL("image/png");
+ link.download = `${currentCode.value || "qr"}_${Date.now()}.png`;
+ link.click();
+ message.success("涓嬭浇鎴愬姛");
+ };
+ img.src = base64;
+ }
+
+ /** 鎵撳嵃浜岀淮鐮� */
+ function handlePrint() {
+ const base64 = barcodeRef.value?.getImageBase64();
+ if (!base64) {
+ message.warning("浜岀淮鐮佺敓鎴愬け璐ワ紝鏃犳硶鎵撳嵃");
+ return;
+ }
+ const items = buildLabelText();
+ const printWindow = window.open("", "_blank");
+ if (!printWindow) {
+ message.error("鏃犳硶鎵撳紑鎵撳嵃绐楀彛锛岃妫�鏌ユ祻瑙堝櫒璁剧疆");
+ return;
+ }
+ printWindow.document.write(`<!DOCTYPE html>
+<html><head><meta charset="UTF-8"><title>鎵撳嵃浜岀淮鐮�</title>
+<style>*{margin:0;padding:0}body{font-family:'Microsoft YaHei',Arial,sans-serif;padding:20px}.print-container{text-align:center}.qr-img{max-width:100%;margin:20px 0}.info{margin-top:20px;text-align:left;font-size:13px}.info p{margin:4px 0}@media print{body{padding:0}.print-container{padding:20px}}</style>
+</head><body><div class="print-container">
+<img src="${base64}" class="qr-img" alt="浜岀淮鐮�" />
+<div class="info">
+${items.map((item) => `<p><strong>${escapeHtml(item.label)}:</strong> ${escapeHtml(item.value || "-")}</p>`).join("")}
+</div></div></body></html>`);
+ printWindow.document.close();
+ printWindow.addEventListener("load", () => {
+ setTimeout(() => printWindow.print(), 500);
+ });
+ }
+
+ const [Modal, modalApi] = useVbenModal({
+ onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ return;
+ }
+ const data = modalApi.getData<{ code?: string }>() || {};
+ codeInput.value = data.code ?? undefined;
+ resetState();
+ if (data.code) {
+ handleSearch(data.code);
+ }
+ },
+ });
+
+ const [BagDescriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ {
+ field: "code",
+ label: "琚嬬爜锛堜簩缁寸爜鍐呭锛�",
+ span: 2,
+ },
+ {
+ field: "bagNo",
+ label: "琚嬪簭鍙�",
+ },
+ {
+ field: "status",
+ label: "琚嬬爜鐘舵��",
+ render: (val) =>
+ val ? getDictLabel(DICT_TYPE.MES_PRO_BAG_CODE_STATUS, val) : "",
+ },
+ {
+ field: "batchCode",
+ label: "鐢熶骇鎵规鍙�",
+ },
+ {
+ field: "batchStatus",
+ label: "鎵规鐘舵��",
+ render: (val) =>
+ val ? getDictLabel(DICT_TYPE.MES_PRO_BATCH_STATUS, val) : "",
+ },
+ {
+ field: "itemName",
+ label: "鍝佺",
+ },
+ {
+ field: "itemSpecification",
+ label: "瑙勬牸",
+ },
+ {
+ field: "produceDate",
+ label: "鐢熶骇鏃ユ湡",
+ },
+ {
+ field: "ownerUserName",
+ label: "璐d换浜�",
+ },
+ {
+ field: "lineCode",
+ label: "浜х嚎",
+ render: (val, data) =>
+ [val, data?.lineName].filter(Boolean).join(" / "),
+ },
+ {
+ field: "palletCode",
+ label: "缁戝畾鎵樼洏鐮�",
+ render: (val) => val || "鏈粦瀹�",
+ },
+ {
+ field: "printTime",
+ label: "瀵煎嚭鍗板埛鏃堕棿",
+ },
+ ],
+ });
+
+ const [PalletDescriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ {
+ field: "code",
+ label: "鎵樼洏鐮侊紙浜岀淮鐮佸唴瀹癸級",
+ span: 2,
+ },
+ {
+ field: "palletNo",
+ label: "鎵樼洏搴忓彿",
+ },
+ {
+ field: "capacity",
+ label: "鍙琚嬫暟锛堣/鎵樼洏锛�",
+ },
+ {
+ field: "bagCount",
+ label: "宸茶琚嬫暟",
+ },
+ {
+ field: "status",
+ label: "鎵樼洏鐘舵��",
+ render: (val) =>
+ val ? getDictLabel(DICT_TYPE.MES_PRO_PALLET_STATUS, val) : "",
+ },
+ {
+ field: "batchCode",
+ label: "鐢熶骇鎵规鍙�",
+ },
+ {
+ field: "batchStatus",
+ label: "鎵规鐘舵��",
+ render: (val) =>
+ val ? getDictLabel(DICT_TYPE.MES_PRO_BATCH_STATUS, val) : "",
+ },
+ {
+ field: "itemName",
+ label: "鍝佺",
+ },
+ {
+ field: "itemSpecification",
+ label: "瑙勬牸",
+ },
+ {
+ field: "produceDate",
+ label: "鐢熶骇鏃ユ湡",
+ },
+ {
+ field: "ownerUserName",
+ label: "璐d换浜�",
+ },
+ {
+ field: "lineCode",
+ label: "浜х嚎",
+ render: (val, data) =>
+ [val, data?.lineName].filter(Boolean).join(" / "),
+ },
+ ],
+ });
+
+ const [BatchDescriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ {
+ field: "code",
+ label: "鎵规鍙凤紙浜岀淮鐮佸唴瀹癸級",
+ span: 2,
+ },
+ {
+ field: "status",
+ label: "鎵规鐘舵��",
+ render: (val) =>
+ val ? getDictLabel(DICT_TYPE.MES_PRO_BATCH_STATUS, val) : "",
+ },
+ {
+ field: "itemName",
+ label: "鍝佺",
+ },
+ {
+ field: "itemSpecification",
+ label: "瑙勬牸",
+ },
+ {
+ field: "lineCode",
+ label: "浜х嚎",
+ render: (val, data) =>
+ [val, data?.lineName].filter(Boolean).join(" / "),
+ },
+ {
+ field: "produceDate",
+ label: "鐢熶骇鏃ユ湡",
+ },
+ {
+ field: "planQuantity",
+ label: "璁″垝鏁伴噺",
+ },
+ {
+ field: "bagQuantity",
+ label: "宸茬敓鎴愯鐮佹暟閲�",
+ },
+ {
+ field: "palletQuantity",
+ label: "宸茬敓鎴愭墭鐩樼爜鏁伴噺",
+ },
+ {
+ field: "ownerUserName",
+ label: "璐d换浜�",
+ },
+ {
+ field: "remark",
+ label: "澶囨敞",
+ },
+ ],
+ });
+</script>
+
+<template>
+ <Modal
+ :show-confirm-button="false"
+ cancel-text="鍏抽棴"
+ class="w-3/5"
+ title="鎵爜杩芥函 / 鏌ョ湅浜岀淮鐮�"
+ >
+ <div class="mx-4">
+ <div class="mb-4 flex gap-2">
+ <Input
+ v-model:value="codeInput"
+ allow-clear
+ class="!w-96"
+ placeholder="璇锋壂鎻忔垨杈撳叆鎵规鍙� / 鎵樼洏鐮� / 琚嬬爜锛屽洖杞︽煡璇�"
+ @press-enter="() => handleSearch()"
+ />
+ <Button :loading="loading" type="primary" @click="() => handleSearch()">
+ 鏌ヨ
+ </Button>
+ </div>
+ <Spin :spinning="loading">
+ <template v-if="currentCode && traceType">
+ <div
+ class="mb-4 flex items-center gap-6 rounded bg-gray-100 p-5"
+ >
+ <div class="flex-shrink-0">
+ <Barcode ref="barcodeRef" :content="currentCode" :width="200" />
+ </div>
+ <div class="min-w-0 flex-1">
+ <p class="text-lg font-bold break-all">{{ currentCode }}</p>
+ <p class="mt-1 text-xs text-gray-500">
+ 鐮佺被鍨嬶細{{ traceTypeLabel }}
+ </p>
+ </div>
+ </div>
+ <template v-if="traceType === 'batch' && batchTrace">
+ <BatchDescriptions :data="batchTrace" class="mt-2" />
+ <div
+ v-if="batchTrace.workers?.length"
+ class="mt-3 rounded border border-solid border-gray-200 p-3"
+ >
+ <p class="mb-2 font-bold">
+ 鐢熶骇浜哄憳锛坽{ batchTrace.workers.length }}锛�
+ </p>
+ <div class="flex flex-wrap gap-2">
+ <Tag v-for="w in batchTrace.workers" :key="w.id">
+ {{ w.userName
+ }}<template v-if="w.workerType"
+ >锛坽{
+ getDictLabel(DICT_TYPE.MES_PRO_BATCH_WORKER_TYPE, w.workerType)
+ }}锛�</template
+ >
+ </Tag>
+ </div>
+ </div>
+ <div
+ v-if="batchTrace.pallets?.length"
+ class="mt-3 rounded border border-solid border-gray-200 p-3"
+ >
+ <p class="mb-2 font-bold">
+ 鎵樼洏鐮侊紙{{ batchTrace.pallets.length }}锛�
+ </p>
+ <div class="flex flex-wrap gap-2">
+ <Tag v-for="p in batchTrace.pallets" :key="p.id">
+ {{ p.code }}
+ </Tag>
+ </div>
+ </div>
+ <div
+ v-if="batchTrace.bags?.length"
+ class="mt-3 rounded border border-solid border-gray-200 p-3"
+ >
+ <p class="mb-2 font-bold">
+ 琚嬬爜
+ <span
+ v-if="
+ batchTrace.bagTotal != null &&
+ batchTrace.bagTotal > batchTrace.bags.length
+ "
+ class="text-xs text-gray-400"
+ >
+ 锛堜粎灞曠ず鍓� {{ batchTrace.bags.length }} 鏉★紝鍏�
+ {{ batchTrace.bagTotal }} 鏉★級
+ </span>
+ </p>
+ <div class="flex max-h-48 flex-wrap gap-2 overflow-y-auto">
+ <Tag v-for="b in batchTrace.bags" :key="b.id">
+ {{ b.code }}
+ </Tag>
+ </div>
+ </div>
+ </template>
+ <BagDescriptions
+ v-else-if="traceType === 'bag' && bagTrace"
+ :data="bagTrace"
+ class="mt-2"
+ />
+ <PalletDescriptions
+ v-else-if="traceType === 'pallet' && palletTrace"
+ :data="palletTrace"
+ class="mt-2"
+ />
+ </template>
+ <Empty
+ v-else-if="searched"
+ :description="searchError ? '璇ョ爜涓嶅瓨鍦ㄦ垨宸插垹闄�' : '鏈煡璇㈠埌杩芥函淇℃伅'"
+ />
+ </Spin>
+ </div>
+ <template v-if="currentCode && traceType" #append-footer>
+ <div class="flex gap-2">
+ <Button type="primary" @click="handlePrint">鎵撳嵃</Button>
+ <Button @click="handleDownload">涓嬭浇浜岀淮鐮�</Button>
+ </div>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/mes/pro/line/components/index.ts b/src/views/mes/pro/line/components/index.ts
new file mode 100644
index 0000000..4f469e7
--- /dev/null
+++ b/src/views/mes/pro/line/components/index.ts
@@ -0,0 +1 @@
+export { default as ProLineSelect } from './select.vue';
diff --git a/src/views/mes/pro/line/components/select.vue b/src/views/mes/pro/line/components/select.vue
new file mode 100644
index 0000000..ef58674
--- /dev/null
+++ b/src/views/mes/pro/line/components/select.vue
@@ -0,0 +1,113 @@
+<script lang="ts" setup>
+import type { MesProLineApi } from '#/api/mes/pro/line';
+
+import { computed, onMounted, ref, watch } from 'vue';
+
+import { Select, Tag, Tooltip } from 'ant-design-vue';
+
+import { getLineSimpleList } from '#/api/mes/pro/line';
+
+defineOptions({ name: 'ProLineSelect', inheritAttrs: false });
+
+const props = withDefaults(
+ defineProps<{
+ allowClear?: boolean;
+ disabled?: boolean;
+ modelValue?: number;
+ placeholder?: string;
+ }>(),
+ {
+ allowClear: true,
+ disabled: false,
+ modelValue: undefined,
+ placeholder: '璇烽�夋嫨浜х嚎',
+ },
+);
+
+const emit = defineEmits<{
+ change: [item: MesProLineApi.Line | undefined];
+ 'update:modelValue': [value: number | undefined];
+}>();
+
+const allList = ref<MesProLineApi.Line[]>([]);
+const selectedItem = ref<MesProLineApi.Line>();
+
+const selectValue = computed({
+ get: () => props.modelValue,
+ set: (value: number | undefined) => {
+ emit('update:modelValue', value);
+ },
+});
+
+/** 鍓嶇杩囨护锛氭寜浜х嚎鍚嶇О鎴栫紪鐮佹ā绯婂尮閰� */
+function handleFilter(input: string, option: any) {
+ const keyword = input.toLowerCase();
+ const item = option?.item as MesProLineApi.Line | undefined;
+ return Boolean(
+ item?.name?.toLowerCase().includes(keyword) ||
+ item?.code?.toLowerCase().includes(keyword),
+ );
+}
+
+/** 鏍规嵁褰撳墠鍊煎悓姝� tooltip 灞曠ず鐨勪骇绾胯鎯� */
+function syncSelectedItem(value: number | undefined) {
+ selectedItem.value =
+ value === undefined
+ ? undefined
+ : allList.value.find((item) => item.id === value);
+}
+
+/** 闄� v-model 澶栵紝棰濆鎶涘嚭瀹屾暣浜х嚎瀵硅薄缁欎笟鍔¤〃鍗曚娇鐢� */
+function handleChange(value: any) {
+ const nextValue = value === undefined ? undefined : Number(value);
+ syncSelectedItem(nextValue);
+ emit('change', selectedItem.value);
+}
+
+watch(
+ () => props.modelValue,
+ (value) => {
+ syncSelectedItem(value);
+ },
+);
+
+onMounted(async () => {
+ allList.value = await getLineSimpleList();
+ syncSelectedItem(props.modelValue);
+});
+</script>
+
+<template>
+ <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
+ <template #title>
+ <div v-if="selectedItem" class="leading-6">
+ <div>缂栫爜锛歿{ selectedItem.code || '-' }}</div>
+ <div>鍚嶇О锛歿{ selectedItem.name || '-' }}</div>
+ <div>澶囨敞锛歿{ selectedItem.remark || '-' }}</div>
+ </div>
+ </template>
+ <Select
+ v-bind="$attrs"
+ v-model:value="selectValue"
+ :allow-clear="allowClear"
+ :disabled="disabled"
+ :filter-option="handleFilter"
+ :placeholder="placeholder"
+ class="w-full"
+ show-search
+ @change="handleChange"
+ >
+ <Select.Option
+ v-for="item in allList"
+ :key="item.id"
+ :item="item"
+ :value="item.id"
+ >
+ <div class="flex items-center gap-2">
+ <span>{{ item.name }}</span>
+ <Tag v-if="item.code" color="default">{{ item.code }}</Tag>
+ </div>
+ </Select.Option>
+ </Select>
+ </Tooltip>
+</template>
diff --git a/src/views/mes/pro/line/data.ts b/src/views/mes/pro/line/data.ts
new file mode 100644
index 0000000..4614e4e
--- /dev/null
+++ b/src/views/mes/pro/line/data.ts
@@ -0,0 +1,152 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProLineApi } from '#/api/mes/pro/line';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼浜х嚎鐨勮〃鍗� */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+ const disabled = formType === 'detail';
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '浜х嚎缂栫爜',
+ component: 'Input',
+ componentProps: {
+ disabled,
+ maxLength: 100,
+ placeholder: '璇疯緭鍏ヤ骇绾跨紪鐮侊紙鍙備笌鎵规鍙风敓鎴愶級',
+ },
+ rules: z.string().min(1, '浜х嚎缂栫爜涓嶈兘涓虹┖').max(100),
+ },
+ {
+ fieldName: 'name',
+ label: '浜х嚎鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ disabled,
+ maxLength: 100,
+ placeholder: '璇疯緭鍏ヤ骇绾垮悕绉�',
+ },
+ rules: z.string().min(1, '浜х嚎鍚嶇О涓嶈兘涓虹┖').max(100),
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'RadioGroup',
+ componentProps: {
+ buttonStyle: 'solid',
+ disabled,
+ optionType: 'button',
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ defaultValue: CommonStatusEnum.ENABLE,
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ disabled,
+ maxLength: 250,
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '浜х嚎缂栫爜',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ヤ骇绾跨紪鐮�',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '浜х嚎鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ヤ骇绾垮悕绉�',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '鍒涘缓鏃堕棿',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesProLineApi.Line>['columns'] {
+ return [
+ {
+ field: 'code',
+ title: '浜х嚎缂栫爜',
+ minWidth: 150,
+ slots: { default: 'code' },
+ },
+ { field: 'name', title: '浜х嚎鍚嶇О', minWidth: 180 },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ { field: 'remark', title: '澶囨敞', minWidth: 180 },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 150,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/mes/pro/line/index.vue b/src/views/mes/pro/line/index.vue
new file mode 100644
index 0000000..6c613a7
--- /dev/null
+++ b/src/views/mes/pro/line/index.vue
@@ -0,0 +1,143 @@
+<script lang="ts" setup>
+ import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+ import type { MesProLineApi } from "#/api/mes/pro/line";
+
+ import { Page, useVbenModal } from "@vben/common-ui";
+ import { downloadFileFromBlobPart } from "@vben/utils";
+
+ import { Button, message } from "ant-design-vue";
+
+ import { ACTION_ICON, TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
+ import {
+ deleteLine,
+ exportLineExcel,
+ getLinePage,
+ } from "#/api/mes/pro/line";
+ import { $t } from "#/locales";
+
+ import { useGridColumns, useGridFormSchema } from "./data";
+ import Form from "./modules/form.vue";
+
+ defineOptions({ name: "MesProLine" });
+
+ const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+ });
+
+ /** 鍒锋柊琛ㄦ牸 */
+ function handleRefresh() {
+ gridApi.query();
+ }
+
+ /** 鏂板浜х嚎 */
+ function handleCreate() {
+ formModalApi.setData({ formType: "create" }).open();
+ }
+
+ /** 鏌ョ湅璇︽儏 */
+ function handleDetail(row: MesProLineApi.Line) {
+ formModalApi.setData({ formType: "detail", id: row.id }).open();
+ }
+
+ /** 缂栬緫浜х嚎 */
+ function handleEdit(row: MesProLineApi.Line) {
+ formModalApi.setData({ formType: "update", id: row.id }).open();
+ }
+
+ /** 鍒犻櫎浜х嚎 */
+ async function handleDelete(row: MesProLineApi.Line) {
+ await deleteLine(row.id!);
+ message.success($t("ui.actionMessage.deleteSuccess", [row.name]));
+ handleRefresh();
+ }
+
+ /** 瀵煎嚭琛ㄦ牸 */
+ async function handleExport() {
+ const data = await exportLineExcel(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: "浜х嚎缁存姢.xls", source: data });
+ }
+
+ const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: "auto",
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getLinePage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: "id",
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesProLineApi.Line>,
+ });
+</script>
+
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <Grid table-title="浜х嚎鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['浜х嚎']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['mes:pro-line:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:pro-line:export'],
+ onClick: handleExport,
+ },
+ ]" />
+ </template>
+ <template #code="{ row }">
+ <Button type="link" @click="handleDetail(row)">
+ {{ row.code }}
+ </Button>
+ </template>
+ <template #actions="{ row }">
+ <TableAction :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pro-line:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pro-line:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]" />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/pro/line/modules/form.vue b/src/views/mes/pro/line/modules/form.vue
new file mode 100644
index 0000000..067e1e0
--- /dev/null
+++ b/src/views/mes/pro/line/modules/form.vue
@@ -0,0 +1,111 @@
+<script lang="ts" setup>
+ import type { FormType } from "../data";
+ import type { MesProLineApi } from "#/api/mes/pro/line";
+
+ import { computed, ref } from "vue";
+
+ import { useVbenModal } from "@vben/common-ui";
+
+ import { Button, message } from "ant-design-vue";
+
+ import { useVbenForm } from "#/adapter/form";
+ import { createLine, getLine, updateLine } from "#/api/mes/pro/line";
+ import { $t } from "#/locales";
+
+ import { useFormSchema } from "../data";
+
+ defineOptions({ name: "MesProLineForm" });
+
+ const emit = defineEmits(["success"]);
+
+ const formType = ref<FormType>("create");
+ const formData = ref<MesProLineApi.Line>();
+
+ const isEditable = computed(() =>
+ ["create", "update"].includes(formType.value)
+ );
+
+ const getTitle = computed(() => {
+ switch (formType.value) {
+ case "detail": {
+ return $t("ui.actionTitle.view", ["浜х嚎"]);
+ }
+ case "update": {
+ return $t("ui.actionTitle.edit", ["浜х嚎"]);
+ }
+ default: {
+ return $t("ui.actionTitle.create", ["浜х嚎"]);
+ }
+ }
+ });
+
+ const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: "w-full",
+ },
+ formItemClass: "col-span-1",
+ labelWidth: 100,
+ },
+ layout: "horizontal",
+ schema: [],
+ showDefaultActions: false,
+ wrapperClass: "grid-cols-2",
+ });
+
+ const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (!isEditable.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) return;
+ modalApi.lock();
+ try {
+ const data = (await formApi.getValues()) as MesProLineApi.Line;
+ if (formData.value?.id) {
+ await updateLine({ ...data, id: formData.value.id });
+ } else {
+ await createLine(data);
+ }
+ message.success($t("ui.actionMessage.operationSuccess"));
+ emit("success");
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ formApi.setState({ schema: useFormSchema(formType.value) });
+ if (data?.id) {
+ modalApi.lock();
+ try {
+ formData.value = await getLine(data.id);
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ }
+ },
+ });
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-2/5">
+ <Form class="mx-4" />
+ <template #append-footer>
+ <div class="flex gap-2">
+ <Button @click="modalApi.close()">
+ {{ isEditable ? '鍙栨秷' : '鍏抽棴' }}
+ </Button>
+ </div>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/mes/pro/pallet/data.ts b/src/views/mes/pro/pallet/data.ts
new file mode 100644
index 0000000..ee2db7a
--- /dev/null
+++ b/src/views/mes/pro/pallet/data.ts
@@ -0,0 +1,109 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProPalletApi } from '#/api/mes/pro/pallet';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '鎵樼洏鐮�',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ユ墭鐩樼爜',
+ },
+ },
+ {
+ fieldName: 'batchCode',
+ label: '鎵规鍙�',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ユ壒娆″彿',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鎵樼洏鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PRO_PALLET_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鎵樼洏鐘舵��',
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '鍒涘缓鏃堕棿',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesProPalletApi.Pallet>['columns'] {
+ return [
+ {
+ field: 'code',
+ title: '鎵樼洏鐮�',
+ fixed: 'left',
+ minWidth: 210,
+ },
+ {
+ field: 'palletNo',
+ title: '鎵樼洏搴忓彿',
+ width: 90,
+ },
+ {
+ field: 'batchCode',
+ title: '鎵规鍙�',
+ minWidth: 190,
+ },
+ {
+ field: 'capacity',
+ title: '鍙琚嬫暟锛堣/鎵樼洏锛�',
+ width: 150,
+ },
+ {
+ field: 'bagCount',
+ title: '宸茬粦琚嬬爜鏁伴噺',
+ width: 120,
+ },
+ {
+ field: 'status',
+ title: '鎵樼洏鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PRO_PALLET_STATUS },
+ },
+ },
+ {
+ field: 'remark',
+ title: '澶囨敞',
+ minWidth: 140,
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 90,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/mes/pro/pallet/index.vue b/src/views/mes/pro/pallet/index.vue
new file mode 100644
index 0000000..403a339
--- /dev/null
+++ b/src/views/mes/pro/pallet/index.vue
@@ -0,0 +1,169 @@
+<script lang="ts" setup>
+ import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+ import type { MesProPalletApi } from "#/api/mes/pro/pallet";
+
+ import { Page, useVbenModal } from "@vben/common-ui";
+ import { downloadFileFromBlobPart } from "@vben/utils";
+
+ import { message } from "ant-design-vue";
+
+ import { ACTION_ICON, TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
+ import {
+ deletePallet,
+ exportPalletExcel,
+ exportPalletQr,
+ getPalletPage,
+ } from "#/api/mes/pro/pallet";
+ import { $t } from "#/locales";
+
+ import { useGridColumns, useGridFormSchema } from "./data";
+ import GenerateModal from "./modules/generate-form.vue";
+ import QrTraceModal from "../components/qr-trace-modal.vue";
+
+ defineOptions({ name: "MesProPallet" });
+
+ const [GenerateFormModal, generateFormModalApi] = useVbenModal({
+ connectedComponent: GenerateModal,
+ destroyOnClose: true,
+ });
+
+ const [QrTraceQueryModal, qrTraceQueryModalApi] = useVbenModal({
+ connectedComponent: QrTraceModal,
+ destroyOnClose: true,
+ });
+
+ /** 鍒锋柊琛ㄦ牸 */
+ function handleRefresh() {
+ gridApi.query();
+ }
+
+ /** 鎵归噺鐢熸垚鎵樼洏鐮� */
+ function handleCreate() {
+ generateFormModalApi.open();
+ }
+
+ /** 鎵爜杩芥函 */
+ function handleTrace() {
+ qrTraceQueryModalApi.open();
+ }
+
+ /** 鏌ョ湅浜岀淮鐮侊紙琛屽唴棰勫~鐮佸�硷級 */
+ function handleViewQr(row: MesProPalletApi.Pallet) {
+ qrTraceQueryModalApi.setData({ code: row.code }).open();
+ }
+
+ /** 瀵煎嚭浜岀淮鐮� ZIP锛堟寜褰撳墠鏌ヨ鏉′欢鐨勬壒娆★級 */
+ async function handleExportQr() {
+ const values = await gridApi.formApi.getValues();
+ if (!values.batchId) {
+ message.warning("璇峰厛鎸夋壒娆$瓫閫夊悗鍐嶅鍑轰簩缁寸爜");
+ return;
+ }
+ const data = await exportPalletQr(values.batchId);
+ downloadFileFromBlobPart({ fileName: "鎵樼洏鐮佷簩缁寸爜.zip", source: data });
+ }
+
+ /** 鍒犻櫎鎵樼洏鐮� */
+ async function handleDelete(row: MesProPalletApi.Pallet) {
+ await deletePallet(row.id!);
+ message.success($t("ui.actionMessage.deleteSuccess", [row.code]));
+ handleRefresh();
+ }
+
+ /** 瀵煎嚭琛ㄦ牸 */
+ async function handleExport() {
+ const data = await exportPalletExcel(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: "鎵樼洏鐮�.xls", source: data });
+ }
+
+ const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: "auto",
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getPalletPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: "id",
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesProPalletApi.Pallet>,
+ });
+</script>
+
+<template>
+ <Page auto-content-height>
+ <GenerateFormModal @success="handleRefresh" />
+ <QrTraceQueryModal />
+ <Grid table-title="鎵樼洏鐮佸垪琛�">
+ <template #toolbar-tools>
+ <TableAction :actions="[
+ {
+ label: '鎵归噺鐢熸垚',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['mes:pro-pallet:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: '鎵爜杩芥函',
+ icon: ACTION_ICON.SEARCH,
+ auth: ['mes:pro-pallet:query'],
+ onClick: handleTrace,
+ },
+ {
+ label: '瀵煎嚭浜岀淮鐮�',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:pro-pallet:export'],
+ onClick: handleExportQr,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:pro-pallet:export'],
+ onClick: handleExport,
+ },
+ ]" />
+ </template>
+ <template #actions="{ row }">
+ <TableAction :actions="[
+ {
+ label: '鏌ョ湅浜岀淮鐮�',
+ type: 'link',
+ auth: ['mes:pro-pallet:query'],
+ onClick: handleViewQr.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pro-pallet:delete'],
+ ifShow: !row.bagCount,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.code]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]" />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/pro/pallet/modules/generate-form.vue b/src/views/mes/pro/pallet/modules/generate-form.vue
new file mode 100644
index 0000000..c34f952
--- /dev/null
+++ b/src/views/mes/pro/pallet/modules/generate-form.vue
@@ -0,0 +1,122 @@
+<script lang="ts" setup>
+ import type { VbenFormSchema } from "#/adapter/form";
+ import type { MesProPalletApi } from "#/api/mes/pro/pallet";
+
+ import { useVbenModal } from "@vben/common-ui";
+ import { MesProBatchStatusEnum } from "@vben/constants";
+
+ import { message } from "ant-design-vue";
+
+ import { useVbenForm } from "#/adapter/form";
+ import { getBatchSimpleList } from "#/api/mes/pro/batch";
+ import { batchCreatePallet } from "#/api/mes/pro/pallet";
+
+ defineOptions({ name: "MesProPalletGenerateForm" });
+
+ const emit = defineEmits(["success"]);
+
+ /** 鏋勫缓琛ㄥ崟锛堟壒娆′笅鎷夐�夐」鐢卞閮ㄥ姞杞芥敞鍏ワ級 */
+ function buildSchema(batchOptions: { label: string; value: number }[]): VbenFormSchema[] {
+ return [
+ {
+ fieldName: "batchId",
+ label: "鐢熶骇鎵规",
+ component: "Select",
+ componentProps: {
+ options: batchOptions,
+ optionFilterProp: "label",
+ placeholder: "璇烽�夋嫨鐢熶骇涓殑鎵规",
+ showSearch: true,
+ },
+ rules: "selectRequired",
+ },
+ {
+ fieldName: "count",
+ label: "鐢熸垚鏁伴噺",
+ component: "InputNumber",
+ componentProps: {
+ class: "!w-full",
+ min: 1,
+ placeholder: "璇疯緭鍏ラ渶瑕佺敓鎴愮殑鎵樼洏鏁伴噺",
+ precision: 0,
+ },
+ rules: "required",
+ },
+ {
+ fieldName: "capacity",
+ label: "鍙琚嬫暟",
+ component: "InputNumber",
+ componentProps: {
+ class: "!w-full",
+ min: 1,
+ placeholder: "璇疯緭鍏ユ瘡鎵樼洏鍙琚嬫暟锛堣/鎵樼洏锛�",
+ precision: 0,
+ },
+ },
+ {
+ fieldName: "remark",
+ label: "澶囨敞",
+ component: "Textarea",
+ componentProps: {
+ placeholder: "璇疯緭鍏ュ娉�",
+ rows: 2,
+ },
+ },
+ ];
+ }
+
+ const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: "w-full",
+ },
+ labelWidth: 100,
+ },
+ layout: "horizontal",
+ schema: [],
+ showDefaultActions: false,
+ });
+
+ const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) return;
+ modalApi.lock();
+ try {
+ const values =
+ (await formApi.getValues()) as MesProPalletApi.BatchCreateParams;
+ const ids = await batchCreatePallet(values);
+ message.success(`宸叉垚鍔熺敓鎴� ${ids.length} 涓墭鐩樼爜`);
+ emit("success");
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) return;
+ modalApi.lock();
+ try {
+ // 浠呯敓浜т腑鐨勬壒娆″厑璁哥敓鎴愭墭鐩樼爜
+ const batches = await getBatchSimpleList();
+ const options = batches
+ .filter(
+ (batch) => batch.status === MesProBatchStatusEnum.PRODUCING
+ )
+ .map((batch) => ({
+ label: batch.code!,
+ value: batch.id!,
+ }));
+ await formApi.setState({ schema: buildSchema(options) });
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ });
+</script>
+
+<template>
+ <Modal title="鎵归噺鐢熸垚鎵樼洏鐮�" class="w-2/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
--
Gitblit v1.9.3