From 5c243d9d05da668a32b1bc9a4f543ea081488d11 Mon Sep 17 00:00:00 2001
From: xiaoyi <908147524@qq.com>
Date: 星期四, 20 八月 2026 14:08:03 +0800
Subject: [PATCH] feat 功能变更
---
src/views/mes/qc/outsourcetest/data.ts | 263 +
src/views/crm/contract/components/command-form.vue | 90
src/views/mes/pd/tech-confirm/index.vue | 198 +
src/views/mes/pro/emergencyplan/data.ts | 187 +
src/views/mes/pd/ctc-reel/index.vue | 150 +
src/api/mes/pro/maintenance-plan/index.ts | 90
src/views/crm/cancellation/index.vue | 189 +
src/views/wls/stocktaking/task/index.vue | 63
src/views/crm/contract/components/command-form-data.ts | 105
src/views/crm/contract/components/command-list-data.ts | 55
src/api/crm/cancellation/index.ts | 77
src/api/mes/pd/ctc/index.ts | 128
src/views/crm/refund/index.vue | 184 +
src/views/erp/purchase/plan/modules/process-select-modal.vue | 79
src/api/crm/saleQuotation/index.ts | 7
src/api/wls/warehouse/area/index.ts | 1
src/api/hrm/certificate/index.ts | 52
src/views/crm/contract/detail/index.vue | 8
src/views/hrm/certificate/modules/form.vue | 91
src/views/mes/dv/statistics/summary.vue | 109
src/api/mes/pd/tech-confirm/index.ts | 63
src/views/mes/pd/ctc/components/index.ts | 1
src/views/crm/refund/data.ts | 208 +
src/api/crm/credit-rating/index.ts | 63
src/views/mes/pd/ctc/modules/form.vue | 134
src/api/erp/purchase/plan/index.ts | 102
src/api/wls/productissue/index.ts | 1
src/api/crm/customer/index.ts | 5
src/views/mes/pro/workorder/data.ts | 31
src/views/mes/process-design/process/data.ts | 19
src/views/hrm/certificate/data.ts | 197 +
src/views/mes/pd/ctc/modules/calc-modal.vue | 344 ++
src/views/wls/productissue/data.ts | 22
src/views/mes/dv/machinery/modules/form.vue | 6
src/api/crm/refund/index.ts | 68
src/views/crm/saleQuotation/data.ts | 87
src/views/erp/purchase/plan/modules/form.vue | 236 +
src/views/mes/qc/outsourcetest/modules/form.vue | 101
src/api/mes/qc/outsourcetest/index.ts | 64
src/views/erp/purchase/plan/data.ts | 242 +
src/views/mes/pd/ctc/index.vue | 161 +
src/packages/constants/src/dict-enum.ts | 32
src/views/mes/pro/emergencyplan/index.vue | 188 +
src/views/aftersales/ticket/data.ts | 4
src/views/mes/pro/task/data.ts | 14
src/views/wls/warehouse/data.ts | 19
src/views/mes/pd/ctc/components/select.vue | 64
src/api/mes/pro/task/index.ts | 2
src/views/mes/pro/emergencyplan/modules/form.vue | 91
src/views/mes/pd/ctc/data.ts | 262 +
src/views/crm/credit-rating/data.ts | 212 +
src/views/crm/refund/modules/form.vue | 92
src/views/mes/qc/outsourcetest/index.vue | 210 +
src/views/crm/contract/data.ts | 102
src/api/crm/contract-command/index.ts | 66
src/api/mes/dv/machinery/index.ts | 1
src/views/crm/cancellation/modules/form.vue | 95
vite.config.ts | 2
src/api/mes/pro/process/index.ts | 1
src/views/erp/purchase/plan/index.vue | 300 ++
src/api/mes/pro/feedback/index.ts | 37
src/views/mes/pd/tech-confirm/modules/form.vue | 100
src/views/wls/warehouse/area/data.ts | 18
src/views/mes/pro/maintenance-plan/modules/form.vue | 220 +
src/views/mes/pd/ctc/modules/index.ts | 1
src/api/mes/dv/statistics/index.ts | 32
src/views/crm/cancellation/data.ts | 227 +
src/views/crm/credit-rating/modules/form.vue | 94
src/views/hrm/certificate/index.vue | 169 +
src/api/crm/contract/index.ts | 8
src/packages/constants/src/biz-mes-enum.ts | 4
src/views/mes/dv/machinery/data.ts | 9
src/api/mes/pd/ctc-reel/index.ts | 60
src/api/mes/pro/emergencyplan/index.ts | 53
src/views/crm/credit-rating/index.vue | 156 +
src/views/mes/pro/maintenance-plan/index.vue | 271 +
src/views/mes/pro/maintenance-plan/data.ts | 244 +
src/api/wls/warehouse/index.ts | 1
src/components/table-action/icons.ts | 1
src/api/mes/pro/workorder/index.ts | 2
src/views/mes/pd/ctc-reel/modules/form.vue | 126
src/views/mes/md/item/data.ts | 19
src/views/mes/pro/workresult/summary.vue | 168 +
src/api/mes/md/item/index.ts | 1
src/api/wls/stocktaking/task/line/index.ts | 14
src/views/mes/pd/ctc-reel/data.ts | 206 +
src/views/crm/customer/data.ts | 64
src/views/crm/contract/components/command-list.vue | 188 +
src/views/mes/pd/tech-confirm/data.ts | 260 +
89 files changed, 8,790 insertions(+), 1 deletions(-)
diff --git a/src/api/crm/cancellation/index.ts b/src/api/crm/cancellation/index.ts
new file mode 100644
index 0000000..684850a
--- /dev/null
+++ b/src/api/crm/cancellation/index.ts
@@ -0,0 +1,77 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace CrmCancellationApi {
+ export interface Cancellation {
+ id?: number;
+ cancellationNo?: string;
+ customerId?: number;
+ customerName?: string;
+ contractId?: number;
+ contractNo?: string;
+ cancellationType?: string;
+ cancelReason?: string;
+ expectedCancelDate?: string;
+ actualCancelDate?: string;
+ refundAmount?: number;
+ applyUserId?: number;
+ applyUserName?: string;
+ applyTime?: string;
+ auditStatus?: number;
+ processInstanceId?: string;
+ remark?: string;
+ createTime?: string;
+ }
+ export interface PageParams extends PageParam {
+ cancellationNo?: string;
+ customerId?: number;
+ cancellationType?: string;
+ auditStatus?: number;
+ }
+}
+
+/** 鏌ヨ閿�鎴风敵璇峰垎椤� */
+export function getCancellationPage(
+ params: CrmCancellationApi.PageParams,
+) {
+ return requestClient.get<PageResult<CrmCancellationApi.Cancellation>>(
+ '/crm/cancellation/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ閿�鎴风敵璇疯鎯� */
+export function getCancellation(id: number) {
+ return requestClient.get<CrmCancellationApi.Cancellation>(
+ `/crm/cancellation/get?id=${id}`,
+ );
+}
+
+/** 鏂板閿�鎴风敵璇� */
+export function createCancellation(data: CrmCancellationApi.Cancellation) {
+ return requestClient.post('/crm/cancellation/create', data);
+}
+
+/** 淇敼閿�鎴风敵璇� */
+export function updateCancellation(data: CrmCancellationApi.Cancellation) {
+ return requestClient.put('/crm/cancellation/update', data);
+}
+
+/** 瀹℃壒閿�鎴风敵璇� */
+export function auditCancellation(data: {
+ id: number;
+ auditStatus: number;
+}) {
+ return requestClient.put('/crm/cancellation/audit', data);
+}
+
+/** 鍒犻櫎閿�鎴风敵璇� */
+export function deleteCancellation(id: number) {
+ return requestClient.delete(`/crm/cancellation/delete?id=${id}`);
+}
+
+/** 瀵煎嚭閿�鎴风敵璇� */
+export function exportCancellation(params: Record<string, unknown>) {
+ return requestClient.download('/crm/cancellation/export-excel', { params });
+}
diff --git a/src/api/crm/contract-command/index.ts b/src/api/crm/contract-command/index.ts
new file mode 100644
index 0000000..cd4e15d
--- /dev/null
+++ b/src/api/crm/contract-command/index.ts
@@ -0,0 +1,66 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace CrmContractCommandApi {
+ export interface ContractCommand {
+ id?: number;
+ contractId?: number;
+ contractNo?: string;
+ commandType?: string;
+ targetModule?: string;
+ targetId?: number;
+ bizNo?: string;
+ operatorId?: number;
+ operatorName?: string;
+ operateTime?: string;
+ status?: number;
+ remark?: string;
+ createTime?: string;
+ }
+ export interface PageParams extends PageParam {
+ contractId?: number;
+ commandType?: string;
+ status?: number;
+ bizNo?: string;
+ }
+}
+
+/** 鏌ヨ鍚堝悓鎸囦护鍒嗛〉 */
+export function getContractCommandPage(
+ params: CrmContractCommandApi.PageParams,
+) {
+ return requestClient.get<PageResult<CrmContractCommandApi.ContractCommand>>(
+ '/crm/contract-command/page',
+ { params },
+ );
+}
+
+/** 鏂板鍚堝悓鎸囦护 */
+export function createContractCommand(
+ data: CrmContractCommandApi.ContractCommand,
+) {
+ return requestClient.post('/crm/contract-command/create', data);
+}
+
+/** 鏇存柊鍚堝悓鎸囦护 */
+export function updateContractCommand(
+ data: CrmContractCommandApi.ContractCommand,
+) {
+ return requestClient.put('/crm/contract-command/update', data);
+}
+
+/** 鎵ц鍚堝悓鎸囦护 */
+export function executeContractCommand(id: number) {
+ return requestClient.put(`/crm/contract-command/execute?id=${id}`);
+}
+
+/** 鍙栨秷鍚堝悓鎸囦护 */
+export function cancelContractCommand(id: number) {
+ return requestClient.put(`/crm/contract-command/cancel?id=${id}`);
+}
+
+/** 鍒犻櫎鍚堝悓鎸囦护 */
+export function deleteContractCommand(id: number) {
+ return requestClient.delete(`/crm/contract-command/delete?id=${id}`);
+}
diff --git a/src/api/crm/contract/index.ts b/src/api/crm/contract/index.ts
index 7bf66e4..35b4e05 100644
--- a/src/api/crm/contract/index.ts
+++ b/src/api/crm/contract/index.ts
@@ -43,6 +43,14 @@
contactName?: string;
orderId?: number; // 鍏宠仈 ERP 閿�鍞鍗曠紪鍙�
orderNo?: string; // ERP 閿�鍞鍗曞崟鍙�
+ contractType?: string; // 鍚堝悓绫诲瀷锛堜緵鐢ㄧ數/鑳芥簮鏈嶅姟绛夛級
+ voltageLevel?: string; // 鐢靛帇绛夌骇
+ electricityType?: string; // 鐢ㄧ數绫诲瀷
+ contractCapacity?: number; // 鍚堝悓瀹归噺锛坘VA锛�
+ tariffMode?: string; // 鐢典环妯″紡
+ meterMode?: string; // 璁¢噺鏂瑰紡
+ payCycle?: number; // 缂磋垂鍛ㄦ湡锛堟湀锛�
+ commandCount?: number; // 鎸囦护鏁伴噺
}
/** 鍚堝悓浜у搧淇℃伅 */
diff --git a/src/api/crm/credit-rating/index.ts b/src/api/crm/credit-rating/index.ts
new file mode 100644
index 0000000..e33d7fd
--- /dev/null
+++ b/src/api/crm/credit-rating/index.ts
@@ -0,0 +1,63 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace CrmCreditRatingApi {
+ export interface CreditRating {
+ id?: number;
+ customerId?: number;
+ customerName?: string;
+ level?: string;
+ score?: number;
+ evalType?: string;
+ evalReason?: string;
+ operatorId?: number;
+ operatorName?: string;
+ operateTime?: string;
+ validUntil?: string;
+ remark?: string;
+ createTime?: string;
+ }
+ export interface PageParams extends PageParam {
+ customerId?: number;
+ level?: string;
+ evalType?: string;
+ }
+}
+
+/** 鏌ヨ淇$敤璇勭骇鍒嗛〉 */
+export function getCreditRatingPage(
+ params: CrmCreditRatingApi.PageParams,
+) {
+ return requestClient.get<PageResult<CrmCreditRatingApi.CreditRating>>(
+ '/crm/credit-rating/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ淇$敤璇勭骇璇︽儏 */
+export function getCreditRating(id: number) {
+ return requestClient.get<CrmCreditRatingApi.CreditRating>(
+ `/crm/credit-rating/get?id=${id}`,
+ );
+}
+
+/** 鏂板淇$敤璇勭骇 */
+export function createCreditRating(data: CrmCreditRatingApi.CreditRating) {
+ return requestClient.post('/crm/credit-rating/create', data);
+}
+
+/** 淇敼淇$敤璇勭骇 */
+export function updateCreditRating(data: CrmCreditRatingApi.CreditRating) {
+ return requestClient.put('/crm/credit-rating/update', data);
+}
+
+/** 鍒犻櫎淇$敤璇勭骇 */
+export function deleteCreditRating(id: number) {
+ return requestClient.delete(`/crm/credit-rating/delete?id=${id}`);
+}
+
+/** 瀵煎嚭淇$敤璇勭骇 */
+export function exportCreditRating(params: Record<string, unknown>) {
+ return requestClient.download('/crm/credit-rating/export-excel', { params });
+}
diff --git a/src/api/crm/customer/index.ts b/src/api/crm/customer/index.ts
index 4c17fde..b5f7a1c 100644
--- a/src/api/crm/customer/index.ts
+++ b/src/api/crm/customer/index.ts
@@ -36,6 +36,11 @@
createTime: Date; // 鍒涘缓鏃堕棿
updateTime: Date; // 鏇存柊鏃堕棿
poolDay?: number; // 璺濈杩涘叆鍏捣澶╂暟
+ customerType?: string; // 瀹㈡埛鍒嗙被锛堟斂浼�/宸ヤ笟/灞呮皯绛夛級
+ creditLevel?: string; // 褰撳墠淇$敤绛夌骇
+ creditScore?: number; // 褰撳墠淇$敤鍒�
+ overdueAmount?: number; // 娆犺垂閲戦锛堟湭鍥炴+閫炬湡锛�
+ overdueCount?: number; // 娆犺垂绗旀暟
}
/** 瀹㈡埛瀵煎叆璇锋眰 */
diff --git a/src/api/crm/refund/index.ts b/src/api/crm/refund/index.ts
new file mode 100644
index 0000000..24bd37a
--- /dev/null
+++ b/src/api/crm/refund/index.ts
@@ -0,0 +1,68 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace CrmRefundApi {
+ export interface Refund {
+ id?: number;
+ refundNo?: string;
+ customerId?: number;
+ customerName?: string;
+ contractId?: number;
+ contractNo?: string;
+ refundType?: string;
+ refundAmount?: number;
+ refundReason?: string;
+ applyUserId?: number;
+ applyUserName?: string;
+ applyTime?: string;
+ auditStatus?: number;
+ processInstanceId?: string;
+ remark?: string;
+ createTime?: string;
+ }
+ export interface PageParams extends PageParam {
+ refundNo?: string;
+ customerId?: number;
+ refundType?: string;
+ auditStatus?: number;
+ }
+}
+
+/** 鏌ヨ閫�璐瑰崟鍒嗛〉 */
+export function getRefundPage(params: CrmRefundApi.PageParams) {
+ return requestClient.get<PageResult<CrmRefundApi.Refund>>(
+ '/crm/refund/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ閫�璐瑰崟璇︽儏 */
+export function getRefund(id: number) {
+ return requestClient.get<CrmRefundApi.Refund>(`/crm/refund/get?id=${id}`);
+}
+
+/** 鏂板閫�璐瑰崟 */
+export function createRefund(data: CrmRefundApi.Refund) {
+ return requestClient.post('/crm/refund/create', data);
+}
+
+/** 淇敼閫�璐瑰崟 */
+export function updateRefund(data: CrmRefundApi.Refund) {
+ return requestClient.put('/crm/refund/update', data);
+}
+
+/** 瀹℃壒閫�璐瑰崟 */
+export function auditRefund(data: { id: number; auditStatus: number }) {
+ return requestClient.put('/crm/refund/audit', data);
+}
+
+/** 鍒犻櫎閫�璐瑰崟 */
+export function deleteRefund(id: number) {
+ return requestClient.delete(`/crm/refund/delete?id=${id}`);
+}
+
+/** 瀵煎嚭閫�璐瑰崟 */
+export function exportRefund(params: Record<string, unknown>) {
+ return requestClient.download('/crm/refund/export-excel', { params });
+}
diff --git a/src/api/crm/saleQuotation/index.ts b/src/api/crm/saleQuotation/index.ts
index 108444b..5d1106d 100644
--- a/src/api/crm/saleQuotation/index.ts
+++ b/src/api/crm/saleQuotation/index.ts
@@ -34,6 +34,13 @@
createTime?: Date;
updateTime?: Date;
items?: SaleQuotationItem[];
+ quotationType?: string; // 鎶ヤ环绫诲瀷锛坓eneral/power/energy锛�
+ capacity?: number; // 鍚堝悓瀹归噺锛坘VA锛�
+ maxDemand?: number; // 鏈�澶ч渶閲忥紙kW锛�
+ unitPrice?: number; // 鐢靛害鍗曚环锛堝厓/kWh锛�
+ tariffMode?: string; // 鐢典环妯″紡
+ powerFactorFee?: number; // 鍔涜皟鐢佃垂锛堝厓锛�
+ billingCycle?: number; // 缁撶畻鍛ㄦ湡锛堟湀锛�
}
/** 鎶ヤ环鍗曠墿鏂欐槑缁� */
diff --git a/src/api/erp/purchase/plan/index.ts b/src/api/erp/purchase/plan/index.ts
new file mode 100644
index 0000000..6ba0506
--- /dev/null
+++ b/src/api/erp/purchase/plan/index.ts
@@ -0,0 +1,102 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace ErpPurchasePlanApi {
+ /** 閲囪喘璁″垝淇℃伅 */
+ export interface PurchasePlan {
+ id?: number; // 缂栧彿
+ no?: string; // 璁″垝鍗曞彿
+ name?: string; // 璁″垝鍚嶇О
+ status?: number; // 鐘舵�侊細0鏈彁浜� 10瀹℃壒涓� 20瀹℃牳閫氳繃 30瀹℃牳涓嶉�氳繃 40宸插彇娑�
+ processInstanceId?: string; // 宸ヤ綔娴佺紪鍙�
+ demandSource?: number; // 闇�姹傛潵婧愶細1搴撳瓨棰勮 2杩愮淮/鐢熶骇璁″垝
+ demandSourceName?: string; // 闇�姹傛潵婧愬悕绉�
+ planDate?: string; // 璁″垝鏃ユ湡
+ planUserId?: number; // 缂栧埗浜虹紪鍙�
+ planUserName?: string; // 缂栧埗浜哄鍚�
+ planDeptId?: number; // 缂栧埗閮ㄩ棬缂栧彿
+ planDeptName?: string; // 缂栧埗閮ㄩ棬鍚嶇О
+ supplierId?: number; // 渚涘簲鍟嗙紪鍙�
+ supplierName?: string; // 渚涘簲鍟嗗悕绉�
+ totalCount?: number; // 鍚堣鏁伴噺
+ generatedFlag?: boolean; // 鏄惁宸茬敓鎴愰噰璐敵璇�
+ remark?: string; // 澶囨敞
+ productNames?: string; // 浜у搧鍚嶇О鍒楄〃
+ items?: PurchasePlanItem[]; // 璁″垝鏄庣粏
+ createTime?: string; // 鍒涘缓鏃堕棿
+ }
+
+ /** 閲囪喘璁″垝鏄庣粏 */
+ export interface PurchasePlanItem {
+ id?: number; // 缂栧彿
+ productId: number; // 浜у搧缂栧彿
+ productName?: string; // 浜у搧鍚嶇О
+ productUnitId?: number; // 浜у搧鍗曚綅缂栧彿
+ productUnitName?: string; // 浜у搧鍗曚綅鍚嶇О
+ count: number; // 璁″垝閲囪喘鏁伴噺
+ demandTime?: string; // 闇�姹傛棩鏈�
+ remark?: string; // 澶囨敞
+ }
+}
+
+/** 鍒涘缓閲囪喘璁″垝 */
+export function createPurchasePlan(data: ErpPurchasePlanApi.PurchasePlan) {
+ return requestClient.post('/erp/purchase-plan/create', data);
+}
+
+/** 鏇存柊閲囪喘璁″垝 */
+export function updatePurchasePlan(data: ErpPurchasePlanApi.PurchasePlan) {
+ return requestClient.put('/erp/purchase-plan/update', data);
+}
+
+/** 鎻愪氦閲囪喘璁″垝瀹℃壒 */
+export function submitPurchasePlan(id: number, processDefinitionKey: string) {
+ return requestClient.put('/erp/purchase-plan/submit', null, {
+ params: { id, processDefinitionKey },
+ });
+}
+
+/** 鏍规嵁搴撳瓨棰勮鐢熸垚閲囪喘璁″垝 */
+export function generatePlanFromStockAlert(params: { name?: string; supplierId?: number }) {
+ return requestClient.post<number>('/erp/purchase-plan/generate-from-stock-alert', null, {
+ params,
+ });
+}
+
+/** 鐢熸垚閲囪喘鐢宠 */
+export function generatePurchaseRequests(id: number) {
+ return requestClient.post<number[]>('/erp/purchase-plan/generate-requests', null, {
+ params: { id },
+ });
+}
+
+/** 鍒犻櫎閲囪喘璁″垝 */
+export function deletePurchasePlan(ids: number[]) {
+ return requestClient.delete('/erp/purchase-plan/delete', { params: { ids: ids.join(',') } });
+}
+
+/** 鑾峰彇閲囪喘璁″垝璇︽儏 */
+export function getPurchasePlan(id: number) {
+ return requestClient.get<ErpPurchasePlanApi.PurchasePlan>(
+ `/erp/purchase-plan/get?id=${id}`,
+ );
+}
+
+/** 鑾峰彇閲囪喘璁″垝鍒嗛〉 */
+export function getPurchasePlanPage(params: PageParam) {
+ return requestClient.get<PageResult<ErpPurchasePlanApi.PurchasePlan>>(
+ '/erp/purchase-plan/page',
+ { params },
+ );
+}
+
+/** 瀵煎嚭閲囪喘璁″垝 Excel */
+export function exportPurchasePlan(params: any) {
+ return requestClient.download('/erp/purchase-plan/export-excel', { params });
+}
+
+/** 鑾峰彇瀹℃壒娴佺▼鍒楄〃 */
+export function getApproveProcessList(id: number) {
+ return requestClient.get(`/erp/purchase-plan/approve-process-list`, { params: { id } });
+}
diff --git a/src/api/hrm/certificate/index.ts b/src/api/hrm/certificate/index.ts
new file mode 100644
index 0000000..de69612
--- /dev/null
+++ b/src/api/hrm/certificate/index.ts
@@ -0,0 +1,52 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace HrmCertificateApi {
+ /** 璇佷功璧勮川 */
+ export interface Certificate {
+ id?: number;
+ employeeId?: number; // 鍛樺伐缂栧彿
+ employeeName?: string; // 鍛樺伐濮撳悕
+ certType?: number; // 璇佷功绫诲瀷锛�1鐗圭浣滀笟 2浠庝笟璧勮川 3鎶�鑳界瓑绾� 4瀹夊叏鍩硅
+ certTypeName?: string; // 璇佷功绫诲瀷鍚嶇О
+ certName?: string; // 璇佷功鍚嶇О
+ certNo?: string; // 璇佷功缂栧彿
+ issueOrg?: string; // 鍙戣瘉鏈烘瀯
+ issueDate?: string; // 鍙戣瘉鏃ユ湡
+ expireDate?: string; // 鏈夋晥鏈熻嚦
+ expireStatus?: number; // 鍒版湡鐘舵�侊細0鏈夋晥 1鍗冲皢鍒版湡 2宸茶繃鏈�
+ remark?: string; // 澶囨敞
+ createTime?: string;
+ }
+}
+
+/** 鍒涘缓璇佷功璧勮川 */
+export function createCertificate(data: HrmCertificateApi.Certificate) {
+ return requestClient.post('/hrm/certificate/create', data);
+}
+
+/** 鏇存柊璇佷功璧勮川 */
+export function updateCertificate(data: HrmCertificateApi.Certificate) {
+ return requestClient.put('/hrm/certificate/update', data);
+}
+
+/** 鍒犻櫎璇佷功璧勮川 */
+export function deleteCertificate(ids: number[]) {
+ return requestClient.delete('/hrm/certificate/delete', { params: { ids: ids.join(',') } });
+}
+
+/** 鑾峰彇璇佷功璧勮川璇︽儏 */
+export function getCertificate(id: number) {
+ return requestClient.get<HrmCertificateApi.Certificate>(
+ `/hrm/certificate/get?id=${id}`,
+ );
+}
+
+/** 鑾峰彇璇佷功璧勮川鍒嗛〉 */
+export function getCertificatePage(params: PageParam) {
+ return requestClient.get<PageResult<HrmCertificateApi.Certificate>>(
+ '/hrm/certificate/page',
+ { params },
+ );
+}
diff --git a/src/api/mes/dv/machinery/index.ts b/src/api/mes/dv/machinery/index.ts
index c46b39f..601c672 100644
--- a/src/api/mes/dv/machinery/index.ts
+++ b/src/api/mes/dv/machinery/index.ts
@@ -7,6 +7,7 @@
export interface Machinery {
id?: number; // 璁惧缂栧彿
code?: string; // 璁惧缂栫爜
+ tbDeviceId?: string; // 鏁伴噰璁惧ID锛堝閮ㄦ暟閲� tbDeviceId锛�
name?: string; // 璁惧鍚嶇О
brand?: string; // 鍝佺墝
specification?: string; // 瑙勬牸鍨嬪彿
diff --git a/src/api/mes/dv/statistics/index.ts b/src/api/mes/dv/statistics/index.ts
new file mode 100644
index 0000000..e13b505
--- /dev/null
+++ b/src/api/mes/dv/statistics/index.ts
@@ -0,0 +1,32 @@
+import { requestClient } from '#/api/request';
+
+export namespace MesDvStatisticsApi {
+ /** 璁惧缁熻姹囨�� */
+ export interface StatisticsSummary {
+ totalMachinery?: number; // 璁惧鎬绘暟
+ producingCount?: number; // 鐢熶骇涓�
+ stoppedCount?: number; // 鍋滄満
+ maintainingCount?: number; // 缁存姢涓�
+ faultRate?: number; // 鏁呴殰鐜�%
+ totalRepair?: number; // 缁翠慨鍗曟�绘暟
+ processingRepair?: number; // 缁翠慨涓�/寰呴獙鏀�
+ finishedRepair?: number; // 宸茬‘璁ょ淮淇�
+ totalMainten?: number; // 淇濆吇璁板綍鎬绘暟
+ finishedMainten?: number; // 宸插畬鎴愪繚鍏�
+ typeDistribution?: TypeItem[]; // 鎸夎澶囩被鍨嬪垎甯�
+ }
+
+ /** 璁惧绫诲瀷鍒嗗竷椤� */
+ export interface TypeItem {
+ machineryTypeId?: number;
+ machineryTypeName?: string;
+ count?: number;
+ }
+}
+
+/** 璁惧缁熻姹囨�� */
+export function getDvStatisticsSummary() {
+ return requestClient.get<MesDvStatisticsApi.StatisticsSummary>(
+ '/mes/dv/statistics/summary',
+ );
+}
diff --git a/src/api/mes/md/item/index.ts b/src/api/mes/md/item/index.ts
index 6ad491e..4469943 100644
--- a/src/api/mes/md/item/index.ts
+++ b/src/api/mes/md/item/index.ts
@@ -18,6 +18,7 @@
safeStockFlag?: boolean; // 鏄惁鍚敤瀹夊叏搴撳瓨
minStock?: number; // 鏈�浣庡簱瀛橀噺
maxStock?: number; // 鏈�楂樺簱瀛橀噺
+ materialCategory?: number; // 鐢靛姏鐗╄祫鍒嗙被
highValue?: boolean; // 鏄惁楂樺�肩墿鏂�
batchFlag?: boolean; // 鏄惁鍚敤鎵规绠$悊
remark?: string; // 澶囨敞
diff --git a/src/api/mes/pd/ctc-reel/index.ts b/src/api/mes/pd/ctc-reel/index.ts
new file mode 100644
index 0000000..b8de0c3
--- /dev/null
+++ b/src/api/mes/pd/ctc-reel/index.ts
@@ -0,0 +1,60 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesPdCtcReelApi {
+ /** 鎹綅瀵肩嚎鐩樺叿璁$畻 */
+ export interface CtcReel {
+ id?: number; // 缂栧彿
+ ctcId?: number; // 鎹綅瀵肩嚎浜у搧ID
+ ctcOrderSpec?: string; // 鎹綅瀵肩嚎璁㈣揣瑙勬牸
+ reelType?: string; // 鐩樺叿绫诲瀷
+ reelOuterDiameter?: number; // 鐩樺叿澶栧緞 mm
+ reelInnerDiameter?: number; // 鐩樿姱鍐呭緞 mm
+ reelWidth?: number; // 鐩樺 mm
+ maxCapacity?: number; // 鏈�澶у閲� km
+ recommendedLength?: number; // 鎺ㄨ崘姣忕洏闀垮害 km
+ netWeight?: number; // 鍑�閲� kg
+ grossWeight?: number; // 姣涢噸 kg
+ calcFormula?: string; // 璁$畻鍏紡
+ remark?: string; // 澶囨敞
+ createTime?: Date; // 鍒涘缓鏃堕棿
+ }
+}
+
+/** 鏌ヨ鐩樺叿璁$畻鍒嗛〉 */
+export function getCtcReelPage(params: PageParam) {
+ return requestClient.get<PageResult<MesPdCtcReelApi.CtcReel>>(
+ '/mes/pd/ctc-reel/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ鐩樺叿璁$畻璇︽儏 */
+export function getCtcReel(id: number) {
+ return requestClient.get<MesPdCtcReelApi.CtcReel>(
+ `/mes/pd/ctc-reel/get?id=${id}`,
+ );
+}
+
+/** 鏌ヨ鎸囧畾鎹綅瀵肩嚎浜у搧鐨勭洏鍏峰垪琛� */
+export function getCtcReelListByCtcId(ctcId: number) {
+ return requestClient.get<MesPdCtcReelApi.CtcReel[]>(
+ `/mes/pd/ctc-reel/list-by-ctc?ctcId=${ctcId}`,
+ );
+}
+
+/** 鏂板鐩樺叿璁$畻璁板綍 */
+export function createCtcReel(data: MesPdCtcReelApi.CtcReel) {
+ return requestClient.post('/mes/pd/ctc-reel/create', data);
+}
+
+/** 淇敼鐩樺叿璁$畻璁板綍 */
+export function updateCtcReel(data: MesPdCtcReelApi.CtcReel) {
+ return requestClient.put('/mes/pd/ctc-reel/update', data);
+}
+
+/** 鍒犻櫎鐩樺叿璁$畻璁板綍 */
+export function deleteCtcReel(id: number) {
+ return requestClient.delete(`/mes/pd/ctc-reel/delete?id=${id}`);
+}
diff --git a/src/api/mes/pd/ctc/index.ts b/src/api/mes/pd/ctc/index.ts
new file mode 100644
index 0000000..55e5a0c
--- /dev/null
+++ b/src/api/mes/pd/ctc/index.ts
@@ -0,0 +1,128 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesPdCtcApi {
+ /** 鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁� */
+ export interface Ctc {
+ id?: number; // 缂栧彿
+ itemId?: number; // 鐗╂枡ID
+ itemName?: string; // 鐗╂枡鍚嶇О
+ itemCode?: string; // 鐗╂枡缂栫爜
+ orderSpec?: string; // 璁㈣揣瑙勬牸
+ bareConductorSpec?: string; // 瑁稿绾胯鏍�
+ enameledSpec?: string; // 婕嗗寘绾胯鏍�
+ transpositionSpec?: string; // 鎹綅瑙勬牸
+ conductorType?: string; // 瀵间綋绫诲瀷
+ crossSection?: number; // 鎴潰绉� mm虏
+ outerDiameter?: number; // 澶栧緞 mm
+ resistance20c?: number; // 20鈩冪洿娴佺數闃� 惟/km
+ wrappingAngle?: number; // 缁曞寘瑙掑害 掳
+ insulationThickness?: number; // 缁濈紭鍘氬害 mm
+ ratedVoltage?: string; // 棰濆畾鐢靛帇 kV
+ testVoltage?: string; // 璇曢獙鐢靛帇 kV
+ minBendingRadius?: number; // 鏈�灏忓集鏇插崐寰� mm
+ weightPerKm?: number; // 姣忓叕閲岄噸閲� kg/km
+ standardCode?: string; // 鎵ц鏍囧噯浠e彿
+ remark?: string; // 澶囨敞
+ createTime?: Date; // 鍒涘缓鏃堕棿
+ }
+
+ /** 缁曞寘瑙掑害璁$畻 */
+ export interface AngleCalcReq {
+ conductorThickness?: number; // 瀵间綋鍘氬害 mm
+ conductorWidth?: number; // 瀵间綋瀹藉害 mm
+ cornerRadius?: number; // 鍦嗚鍗婂緞 mm
+ insulationThickness?: number; // 缁濈紭鍘氬害 mm
+ tapeWidth?: number; // 绾稿甫瀹藉害 mm
+ overlapWidth?: number; // 閲嶅彔瀹藉害 mm
+ }
+
+ /** 缁曞寘瑙掑害璁$畻缁撴灉 */
+ export interface AngleCalcResp {
+ circumference?: number; // 瀵肩嚎鍛ㄩ暱 mm
+ wrappingAngle?: number; // 缁曞寘瑙掑害 掳
+ wrappingPitch?: number; // 缁曞寘鑺傝窛 mm
+ }
+
+ /** 鐩存祦鐢甸樆璁$畻 */
+ export interface ResistanceCalcReq {
+ conductorThickness?: number; // 瀵间綋鍘氬害 mm
+ conductorWidth?: number; // 瀵间綋瀹藉害 mm
+ temperature?: number; // 璁$畻娓╁害 鈩�
+ material?: string; // 瀵间綋鏉愯川
+ }
+
+ /** 鐩存祦鐢甸樆璁$畻缁撴灉 */
+ export interface ResistanceCalcResp {
+ crossSection?: number; // 鎴潰绉� mm虏
+ resistance20cKm?: number; // 20鈩冪洿娴佺數闃� 惟/km
+ resistanceAtTemp?: number; // 鎸囧畾娓╁害鐩存祦鐢甸樆 惟/m
+ resistanceAtTempKm?: number; // 鎸囧畾娓╁害鐩存祦鐢甸樆 惟/km
+ }
+
+ /** 鐩樺叿瀹归噺璁$畻 */
+ export interface ReelCalcReq {
+ reelOuterDiameter?: number; // 鐩樺叿澶栧緞 d1 mm
+ reelInnerDiameter?: number; // 鐩樿姱鍐呭緞 d2 mm
+ reelWidth?: number; // 鐩樺 l2 mm
+ ctcThickness?: number; // CTC 鏈�澶ч珮搴� mm
+ ctcWidth?: number; // CTC 鏈�澶у搴� mm
+ }
+
+ /** 鐩樺叿瀹归噺璁$畻缁撴灉 */
+ export interface ReelCalcResp {
+ capacityMeter?: number; // 姣忕洏闀垮害 m
+ capacityKm?: number; // 姣忕洏闀垮害 km
+ }
+}
+
+/** 鏌ヨ鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁板垎椤� */
+export function getCtcPage(params: PageParam) {
+ return requestClient.get<PageResult<MesPdCtcApi.Ctc>>('/mes/pd/ctc/page', {
+ params,
+ });
+}
+
+/** 鏌ヨ鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁拌鎯� */
+export function getCtc(id: number) {
+ return requestClient.get<MesPdCtcApi.Ctc>(`/mes/pd/ctc/get?id=${id}`);
+}
+
+/** 鏂板鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁� */
+export function createCtc(data: MesPdCtcApi.Ctc) {
+ return requestClient.post('/mes/pd/ctc/create', data);
+}
+
+/** 淇敼鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁� */
+export function updateCtc(data: MesPdCtcApi.Ctc) {
+ return requestClient.put('/mes/pd/ctc/update', data);
+}
+
+/** 鍒犻櫎鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁� */
+export function deleteCtc(id: number) {
+ return requestClient.delete(`/mes/pd/ctc/delete?id=${id}`);
+}
+
+/** 瀵煎嚭鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁� */
+export function exportCtc(params: any) {
+ return requestClient.download('/mes/pd/ctc/export-excel', { params });
+}
+
+/** 缁曞寘瑙掑害璁$畻 */
+export function calcCtcAngle(data: MesPdCtcApi.AngleCalcReq) {
+ return requestClient.post<MesPdCtcApi.AngleCalcResp>('/mes/pd/ctc/calc-angle', data);
+}
+
+/** 鐩存祦鐢甸樆璁$畻 */
+export function calcCtcResistance(data: MesPdCtcApi.ResistanceCalcReq) {
+ return requestClient.post<MesPdCtcApi.ResistanceCalcResp>(
+ '/mes/pd/ctc/calc-resistance',
+ data,
+ );
+}
+
+/** 鐩樺叿瀹归噺璁$畻 */
+export function calcCtcReel(data: MesPdCtcApi.ReelCalcReq) {
+ return requestClient.post<MesPdCtcApi.ReelCalcResp>('/mes/pd/ctc/calc-reel', data);
+}
diff --git a/src/api/mes/pd/tech-confirm/index.ts b/src/api/mes/pd/tech-confirm/index.ts
new file mode 100644
index 0000000..be86723
--- /dev/null
+++ b/src/api/mes/pd/tech-confirm/index.ts
@@ -0,0 +1,63 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesPdTechConfirmApi {
+ /** 浜у搧鎶�鏈‘璁ゅ嚱 */
+ export interface TechConfirm {
+ id?: number; // 缂栧彿
+ confirmNo?: string; // 纭鍑界紪鍙�
+ customerId?: number; // 瀹㈡埛ID
+ customerName?: string; // 瀹㈡埛鍚嶇О
+ productName?: string; // 浜у搧鍚嶇О
+ specification?: string; // 瑙勬牸鍨嬪彿
+ techParams?: string; // 鎶�鏈弬鏁� JSON
+ templateType?: string; // 妯℃澘绫诲瀷
+ drafterId?: number; // 缂栧埗浜篒D
+ drafterName?: string; // 缂栧埗浜哄鍚�
+ draftTime?: Date; // 缂栧埗鏃堕棿
+ reviewerId?: number; // 瀹℃牳浜篒D
+ reviewerName?: string; // 瀹℃牳浜哄鍚�
+ reviewTime?: Date; // 瀹℃牳鏃堕棿
+ customerConfirmUser?: string; // 瀹㈡埛纭浜�
+ customerConfirmTime?: Date; // 瀹㈡埛纭鏃堕棿
+ status?: number; // 鐘舵��
+ remark?: string; // 澶囨敞
+ createTime?: Date; // 鍒涘缓鏃堕棿
+ }
+}
+
+/** 鏌ヨ浜у搧鎶�鏈‘璁ゅ嚱鍒嗛〉 */
+export function getTechConfirmPage(params: PageParam) {
+ return requestClient.get<PageResult<MesPdTechConfirmApi.TechConfirm>>(
+ '/mes/pd/tech-confirm/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ浜у搧鎶�鏈‘璁ゅ嚱璇︽儏 */
+export function getTechConfirm(id: number) {
+ return requestClient.get<MesPdTechConfirmApi.TechConfirm>(
+ `/mes/pd/tech-confirm/get?id=${id}`,
+ );
+}
+
+/** 鏂板浜у搧鎶�鏈‘璁ゅ嚱 */
+export function createTechConfirm(data: MesPdTechConfirmApi.TechConfirm) {
+ return requestClient.post('/mes/pd/tech-confirm/create', data);
+}
+
+/** 淇敼浜у搧鎶�鏈‘璁ゅ嚱 */
+export function updateTechConfirm(data: MesPdTechConfirmApi.TechConfirm) {
+ return requestClient.put('/mes/pd/tech-confirm/update', data);
+}
+
+/** 鍒犻櫎浜у搧鎶�鏈‘璁ゅ嚱 */
+export function deleteTechConfirm(id: number) {
+ return requestClient.delete(`/mes/pd/tech-confirm/delete?id=${id}`);
+}
+
+/** 瀵煎嚭浜у搧鎶�鏈‘璁ゅ嚱 */
+export function exportTechConfirm(params: any) {
+ return requestClient.download('/mes/pd/tech-confirm/export-excel', { params });
+}
diff --git a/src/api/mes/pro/emergencyplan/index.ts b/src/api/mes/pro/emergencyplan/index.ts
new file mode 100644
index 0000000..d4a0887
--- /dev/null
+++ b/src/api/mes/pro/emergencyplan/index.ts
@@ -0,0 +1,53 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesProEmergencyPlanApi {
+ /** 搴旀�ュ缃妗� */
+ export interface EmergencyPlan {
+ id?: number;
+ code?: string; // 棰勬缂栫爜
+ name?: string; // 棰勬鍚嶇О
+ planType?: number; // 棰勬绫诲瀷锛�1鍋滅數 2璁惧鏁呴殰 3鐏伨 4鑷劧鐏惧 5鍏朵粬
+ content?: string; // 澶勭疆鍐呭
+ orgUnit?: string; // 璐d换鍗曚綅
+ status?: number; // 鐘舵�侊細0鑽夌 10宸插彂甯�
+ remark?: string; // 澶囨敞
+ createTime?: string;
+ }
+}
+
+/** 鍒涘缓搴旀�ュ缃妗� */
+export function createPlan(data: MesProEmergencyPlanApi.EmergencyPlan) {
+ return requestClient.post('/mes/pro/emergency-plan/create', data);
+}
+
+/** 鏇存柊搴旀�ュ缃妗� */
+export function updatePlan(data: MesProEmergencyPlanApi.EmergencyPlan) {
+ return requestClient.put('/mes/pro/emergency-plan/update', data);
+}
+
+/** 鍒犻櫎搴旀�ュ缃妗� */
+export function deletePlan(ids: number[]) {
+ return requestClient.delete('/mes/pro/emergency-plan/delete', { params: { ids: ids.join(',') } });
+}
+
+/** 鑾峰彇搴旀�ュ缃妗� */
+export function getPlan(id: number) {
+ return requestClient.get<MesProEmergencyPlanApi.EmergencyPlan>(
+ `/mes/pro/emergency-plan/get?id=${id}`,
+ );
+}
+
+/** 鑾峰彇搴旀�ュ缃妗堝垎椤� */
+export function getPlanPage(params: PageParam) {
+ return requestClient.get<PageResult<MesProEmergencyPlanApi.EmergencyPlan>>(
+ '/mes/pro/emergency-plan/page',
+ { params },
+ );
+}
+
+/** 鍙戝竷搴旀�ュ缃妗� */
+export function publishPlan(id: number) {
+ return requestClient.put(`/mes/pro/emergency-plan/publish?id=${id}`);
+}
diff --git a/src/api/mes/pro/feedback/index.ts b/src/api/mes/pro/feedback/index.ts
index 7666071..44f3f35 100644
--- a/src/api/mes/pro/feedback/index.ts
+++ b/src/api/mes/pro/feedback/index.ts
@@ -204,3 +204,40 @@
`/mes/pro/feedback/list-by-task?taskId=${taskId}`,
);
}
+
+/** 浣滀笟鎴愭灉姹囨�婚」锛堟寜宸ュ崟绫诲瀷锛� */
+export interface WorkResultTypeItem {
+ workOrderType?: number;
+ workOrderTypeName?: string;
+ feedbackQuantity?: number;
+ qualifiedQuantity?: number;
+ feedbackCount?: number;
+}
+
+/** 浣滀笟鎴愭灉姹囨�婚」锛堟寜鏃ユ湡锛� */
+export interface WorkResultDateItem {
+ date?: string;
+ feedbackQuantity?: number;
+ qualifiedQuantity?: number;
+ feedbackCount?: number;
+}
+
+/** 浣滀笟鎴愭灉姹囨�� */
+export interface WorkResultSummary {
+ totalFeedbackQuantity?: number;
+ totalQualifiedQuantity?: number;
+ totalFeedbackCount?: number;
+ byType?: WorkResultTypeItem[];
+ byDate?: WorkResultDateItem[];
+}
+
+/** 浣滀笟鎴愭灉姹囨�荤粺璁� */
+export function getFeedbackSummary(params: {
+ beginDate?: string;
+ endDate?: string;
+ workOrderType?: number;
+}) {
+ return requestClient.get<WorkResultSummary>('/mes/pro/feedback/summary', {
+ params,
+ });
+}
diff --git a/src/api/mes/pro/maintenance-plan/index.ts b/src/api/mes/pro/maintenance-plan/index.ts
new file mode 100644
index 0000000..cd95a2b
--- /dev/null
+++ b/src/api/mes/pro/maintenance-plan/index.ts
@@ -0,0 +1,90 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesProMaintenancePlanApi {
+ /** 鐢靛姏浣滀笟璁″垝 */
+ export interface MaintenancePlan {
+ id?: number; // 缂栧彿
+ code?: string; // 璁″垝缂栫爜
+ name?: string; // 璁″垝鍚嶇О
+ planType?: number; // 璁″垝绫诲瀷锛�1鐢电綉杩愯 2鍙戠數 3鍙樼數杩愮淮 4绾胯矾妫�淇�
+ station?: string; // 绔欑偣
+ line?: string; // 绾胯矾
+ teamId?: number; // 鐝粍缂栧彿
+ teamName?: string; // 鐝粍鍚嶇О
+ planDate?: string; // 璁″垝鏃ユ湡
+ status?: number; // 鐘舵�侊細0鑽夌 10宸蹭笅鍙� 20宸插畬鎴� 30宸插彇娑�
+ workOrderId?: number; // 鐢熸垚鐨勫伐鍗曠紪鍙�
+ workOrderCode?: string; // 鐢熸垚鐨勫伐鍗曠紪鐮�
+ remark?: string; // 澶囨敞
+ items?: PlanItem[]; // 澶囦欢闇�姹傛槑缁�
+ createTime?: string; // 鍒涘缓鏃堕棿
+ }
+
+ /** 澶囦欢闇�姹傛槑缁� */
+ export interface PlanItem {
+ id?: number; // 缂栧彿
+ itemId: number; // 鐗╂枡/澶囦欢缂栧彿
+ itemName?: string; // 鐗╂枡鍚嶇О
+ itemCode?: string; // 鐗╂枡缂栫爜
+ unitMeasureName?: string; // 璁¢噺鍗曚綅鍚嶇О
+ quantity: number; // 闇�姹傛暟閲�
+ remark?: string; // 澶囨敞
+ }
+}
+
+/** 鍒涘缓鐢靛姏浣滀笟璁″垝 */
+export function createPlan(data: MesProMaintenancePlanApi.MaintenancePlan) {
+ return requestClient.post('/mes/pro/maintenance-plan/create', data);
+}
+
+/** 鏇存柊鐢靛姏浣滀笟璁″垝 */
+export function updatePlan(data: MesProMaintenancePlanApi.MaintenancePlan) {
+ return requestClient.put('/mes/pro/maintenance-plan/update', data);
+}
+
+/** 鍒犻櫎鐢靛姏浣滀笟璁″垝 */
+export function deletePlan(ids: number[]) {
+ return requestClient.delete('/mes/pro/maintenance-plan/delete', { params: { ids: ids.join(',') } });
+}
+
+/** 鑾峰彇鐢靛姏浣滀笟璁″垝璇︽儏 */
+export function getPlan(id: number) {
+ return requestClient.get<MesProMaintenancePlanApi.MaintenancePlan>(
+ `/mes/pro/maintenance-plan/get?id=${id}`,
+ );
+}
+
+/** 鑾峰彇鐢靛姏浣滀笟璁″垝鍒嗛〉 */
+export function getPlanPage(params: PageParam) {
+ return requestClient.get<PageResult<MesProMaintenancePlanApi.MaintenancePlan>>(
+ '/mes/pro/maintenance-plan/page',
+ { params },
+ );
+}
+
+/** 瀵煎嚭鐢靛姏浣滀笟璁″垝 */
+export function exportPlan(params: any) {
+ return requestClient.download('/mes/pro/maintenance-plan/export-excel', { params });
+}
+
+/** 涓嬪彂鐢靛姏浣滀笟璁″垝 */
+export function issuePlan(id: number) {
+ return requestClient.put(`/mes/pro/maintenance-plan/issue?id=${id}`);
+}
+
+/** 瀹屾垚鐢靛姏浣滀笟璁″垝 */
+export function finishPlan(id: number) {
+ return requestClient.put(`/mes/pro/maintenance-plan/finish?id=${id}`);
+}
+
+/** 鏍规嵁澶囦欢闇�姹傜敓鎴愰噰璐鍒� */
+export function generatePurchasePlan(id: number) {
+ return requestClient.post<number>(`/mes/pro/maintenance-plan/generate-purchase-plan?id=${id}`);
+}
+
+/** 鐢熸垚杩愮淮/鍙戠數/妫�淇伐鍗� */
+export function generateWorkOrder(id: number) {
+ return requestClient.post<number>(`/mes/pro/maintenance-plan/generate-work-order?id=${id}`);
+}
diff --git a/src/api/mes/pro/process/index.ts b/src/api/mes/pro/process/index.ts
index a4ff1ea..a55daf6 100644
--- a/src/api/mes/pro/process/index.ts
+++ b/src/api/mes/pro/process/index.ts
@@ -8,6 +8,7 @@
id?: number;
code?: string;
name?: string;
+ type?: number; // 鐢靛姏宸ヨ壓鍒嗙被
attention?: string;
status?: number;
remark?: string;
diff --git a/src/api/mes/pro/task/index.ts b/src/api/mes/pro/task/index.ts
index a059e1c..f832ffa 100644
--- a/src/api/mes/pro/task/index.ts
+++ b/src/api/mes/pro/task/index.ts
@@ -27,6 +27,8 @@
workstationId?: number; // 宸ヤ綔绔欑紪鍙�
workstationCode?: string; // 宸ヤ綔绔欑紪鐮�
workstationName?: string; // 宸ヤ綔绔欏悕绉�
+ teamId?: number; // 鐝粍缂栧彿
+ teamName?: string; // 鐝粍鍚嶇О
routeId?: number; // 宸ヨ壓璺嚎缂栧彿
processId?: number; // 宸ュ簭缂栧彿
processName?: string; // 宸ュ簭鍚嶇О
diff --git a/src/api/mes/pro/workorder/index.ts b/src/api/mes/pro/workorder/index.ts
index 39bcf69..1718d76 100644
--- a/src/api/mes/pro/workorder/index.ts
+++ b/src/api/mes/pro/workorder/index.ts
@@ -24,6 +24,8 @@
clientId?: number; // 瀹㈡埛缂栧彿
clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
+ saleOrderId?: number; // 鍏宠仈钀ラ攢璁㈠崟锛圗RP閿�鍞鍗曪級缂栧彿
+ saleOrderNo?: string; // 鍏宠仈钀ラ攢璁㈠崟锛圗RP閿�鍞鍗曪級鍗曞彿
vendorId?: number; // 渚涘簲鍟嗙紪鍙�
vendorName?: string; // 渚涘簲鍟嗗悕绉�
vendorCode?: string; // 渚涘簲鍟嗙紪鐮�
diff --git a/src/api/mes/qc/outsourcetest/index.ts b/src/api/mes/qc/outsourcetest/index.ts
new file mode 100644
index 0000000..610095c
--- /dev/null
+++ b/src/api/mes/qc/outsourcetest/index.ts
@@ -0,0 +1,64 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesQcOutsourceTestApi {
+ /** 濮斿妫�娴�/璇曢獙鍗� */
+ export interface OutsourceTest {
+ id?: number;
+ code?: string; // 鍗曟嵁缂栫爜
+ name?: string; // 鍗曟嵁鍚嶇О
+ testType?: number; // 妫�娴嬬被鍨嬶細1绗笁鏂规娴� 2濮斿璇曢獙
+ itemId?: number; // 妫�娴嬪璞$紪鍙�
+ itemName?: string; // 妫�娴嬪璞″悕绉�
+ testOrg?: string; // 妫�娴嬫満鏋�
+ testDate?: string; // 妫�娴嬫棩鏈�
+ result?: number; // 妫�娴嬬粨鏋滐細1鍚堟牸 2涓嶅悎鏍�
+ reportNo?: string; // 妫�娴嬫姤鍛婄紪鍙�
+ status?: number; // 鐘舵�侊細0鑽夌 10宸叉彁浜� 20宸插畬鎴�
+ remark?: string; // 澶囨敞
+ blobIds?: number[]; // 闄勪欢
+ attachmentList?: { id: number; name?: string; url?: string }[];
+ createTime?: string;
+ }
+}
+
+/** 鍒涘缓濮斿妫�娴�/璇曢獙鍗� */
+export function createTest(data: MesQcOutsourceTestApi.OutsourceTest) {
+ return requestClient.post('/mes/qc/outsource-test/create', data);
+}
+
+/** 鏇存柊濮斿妫�娴�/璇曢獙鍗� */
+export function updateTest(data: MesQcOutsourceTestApi.OutsourceTest) {
+ return requestClient.put('/mes/qc/outsource-test/update', data);
+}
+
+/** 鍒犻櫎濮斿妫�娴�/璇曢獙鍗� */
+export function deleteTest(ids: number[]) {
+ return requestClient.delete('/mes/qc/outsource-test/delete', { params: { ids: ids.join(',') } });
+}
+
+/** 鑾峰彇濮斿妫�娴�/璇曢獙鍗曡鎯� */
+export function getTest(id: number) {
+ return requestClient.get<MesQcOutsourceTestApi.OutsourceTest>(
+ `/mes/qc/outsource-test/get?id=${id}`,
+ );
+}
+
+/** 鑾峰彇濮斿妫�娴�/璇曢獙鍗曞垎椤� */
+export function getTestPage(params: PageParam) {
+ return requestClient.get<PageResult<MesQcOutsourceTestApi.OutsourceTest>>(
+ '/mes/qc/outsource-test/page',
+ { params },
+ );
+}
+
+/** 鎻愪氦濮斿妫�娴�/璇曢獙鍗� */
+export function submitTest(id: number) {
+ return requestClient.put(`/mes/qc/outsource-test/submit?id=${id}`);
+}
+
+/** 瀹屾垚濮斿妫�娴�/璇曢獙鍗� */
+export function finishTest(id: number) {
+ return requestClient.put(`/mes/qc/outsource-test/finish?id=${id}`);
+}
diff --git a/src/api/wls/productissue/index.ts b/src/api/wls/productissue/index.ts
index 5da6598..2767812 100644
--- a/src/api/wls/productissue/index.ts
+++ b/src/api/wls/productissue/index.ts
@@ -16,6 +16,7 @@
clientCode?: string; // 瀹㈡埛缂栫爜
clientName?: string; // 瀹㈡埛鍚嶇О
requiredTime?: number; // 闇�姹傛椂闂�
+ issueType?: number; // 鍗曟嵁绫诲瀷锛�1=姝e父棰嗘枡锛�2=绱ф�ヨˉ鏂欙級
status?: number; // 鍗曟嵁鐘舵��
remark?: string; // 澶囨敞
createTime?: Date; // 鍒涘缓鏃堕棿
diff --git a/src/api/wls/stocktaking/task/line/index.ts b/src/api/wls/stocktaking/task/line/index.ts
index ad51625..d305c80 100644
--- a/src/api/wls/stocktaking/task/line/index.ts
+++ b/src/api/wls/stocktaking/task/line/index.ts
@@ -36,6 +36,20 @@
>('/mes/wm/stocktaking-task-line/page', { params });
}
+/** 涓嬭浇鐩樼偣瀹炵洏鏁板鍏ユā鏉� */
+export function importStockTakingQuantityTemplate() {
+ return requestClient.download('/mes/wm/stocktaking-task-line/get-import-template');
+}
+
+/** 瀵煎叆鐩樼偣瀹炵洏鏁伴噺 */
+export function importStockTakingQuantity(taskId: number, file: File) {
+ return requestClient.upload<{
+ successCount?: number;
+ failureCount?: number;
+ failureMessages?: string[];
+ }>(`/mes/wm/stocktaking-task-line/import-excel?taskId=${taskId}`, { file });
+}
+
/** 鏌ヨ鐩樼偣浠诲姟琛岀簿绠�鍒楄〃 */
export function getStockTakingTaskLineSimpleList(taskId: number) {
return requestClient.get<WlsStockTakingTaskLineApi.StockTakingTaskLine[]>(
diff --git a/src/api/wls/warehouse/area/index.ts b/src/api/wls/warehouse/area/index.ts
index 3190654..15adb86 100644
--- a/src/api/wls/warehouse/area/index.ts
+++ b/src/api/wls/warehouse/area/index.ts
@@ -8,6 +8,7 @@
id?: number; // 搴撲綅缂栧彿
code?: string; // 搴撲綅缂栫爜
name?: string; // 搴撲綅鍚嶇О
+ type?: number; // 搴撳尯绫诲瀷
warehouseId?: number; // 浠撳簱缂栧彿
warehouseName?: string; // 浠撳簱鍚嶇О
locationId?: number; // 搴撳尯缂栧彿
diff --git a/src/api/wls/warehouse/index.ts b/src/api/wls/warehouse/index.ts
index 661b62e..66ffebb 100644
--- a/src/api/wls/warehouse/index.ts
+++ b/src/api/wls/warehouse/index.ts
@@ -8,6 +8,7 @@
id?: number; // 浠撳簱缂栧彿
code?: string; // 浠撳簱缂栫爜
name?: string; // 浠撳簱鍚嶇О
+ type?: number; // 浠撳簱绫诲瀷
address?: string; // 鍦板潃
area?: number; // 闈㈢Н
chargeUserId?: number; // 璐熻矗浜�
diff --git a/src/components/table-action/icons.ts b/src/components/table-action/icons.ts
index 578265f..c56cb1b 100644
--- a/src/components/table-action/icons.ts
+++ b/src/components/table-action/icons.ts
@@ -13,4 +13,5 @@
CLOSE: 'lucide:x',
BOOK: 'lucide:book',
AUDIT: 'lucide:file-check',
+ CALC: 'lucide:calculator',
};
diff --git a/src/packages/constants/src/biz-mes-enum.ts b/src/packages/constants/src/biz-mes-enum.ts
index 85c58db..14f9650 100644
--- a/src/packages/constants/src/biz-mes-enum.ts
+++ b/src/packages/constants/src/biz-mes-enum.ts
@@ -249,12 +249,16 @@
SELF: 1, // 鑷鐢熶骇
OUTSOURCE: 2, // 浠e伐
PURCHASE: 3, // 閲囪喘
+ POWER_OPERATION: 4, // 鐢靛姏杩愮淮
+ POWER_GENERATION: 5, // 鍙戠數
+ POWER_MAINTENANCE: 6, // 妫�淇�
} as const;
/** MES 宸ュ崟鏉ユ簮绫诲瀷鏋氫妇 */
export const MesProWorkOrderSourceTypeEnum = {
ORDER: 1, // 瀹㈡埛璁㈠崟
STORE: 2, // 搴撳瓨澶囪揣
+ MAINTENANCE_PLAN: 3, // 杩愮淮璁″垝
} as const;
/** MES 鐢熶骇浠诲姟鐘舵�佹灇涓� */
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index 2a62e5a..46210bb 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -127,6 +127,18 @@
CRM_SALES_TARGET_PERIOD_TYPE: 'crm_sales_target_period_type', // CRM 閿�鍞洰鏍囧懆鏈熺被鍨�
CRM_SALES_TARGET_DIMENSION: 'crm_sales_target_dimension', // CRM 閿�鍞洰鏍囩淮搴�
CRM_SALES_TARGET_STATUS: 'crm_sales_target_status', // CRM 閿�鍞洰鏍囩姸鎬�
+ CRM_CUSTOMER_TYPE: 'crm_customer_type', // CRM 瀹㈡埛鍒嗙被锛堢數鍔涳級
+ CRM_CREDIT_LEVEL: 'crm_credit_level', // CRM 瀹㈡埛淇$敤绛夌骇
+ CRM_QUOTATION_TYPE: 'crm_quotation_type', // CRM 鎶ヤ环鍗曠被鍨�
+ CRM_TARIFF_MODE: 'crm_tariff_mode', // CRM 鐢典环妯″紡
+ CRM_CONTRACT_TYPE: 'crm_contract_type', // CRM 鍚堝悓绫诲瀷
+ CRM_VOLTAGE_LEVEL: 'crm_voltage_level', // CRM 鐢靛帇绛夌骇
+ CRM_ELECTRICITY_TYPE: 'crm_electricity_type', // CRM 鐢ㄧ數绫诲瀷
+ CRM_METER_MODE: 'crm_meter_mode', // CRM 璁¢噺鏂瑰紡
+ CRM_COMMAND_TYPE: 'crm_command_type', // CRM 鍚堝悓鎸囦护绫诲瀷
+ CRM_COMMAND_STATUS: 'crm_command_status', // CRM 鍚堝悓鎸囦护鐘舵��
+ CRM_REFUND_TYPE: 'crm_refund_type', // CRM 閫�璐圭被鍨�
+ CRM_CANCELLATION_TYPE: 'crm_cancellation_type', // CRM 閿�鎴风被鍨�
} as const;
/** ========== AFTERSALES - 鍞悗鏈嶅姟妯″潡 ========== */
@@ -212,6 +224,13 @@
/** ========== MES - 鍒堕�犳墽琛屾ā鍧� ========== */
const MES_DICT = {
MES_MD_ITEM_OR_PRODUCT: 'mes_md_item_or_product', // MES 鐗╂枡/浜у搧鏍囪瘑
+ MES_MATERIAL_CATEGORY: 'mes_material_category', // MES 鐢靛姏鐗╄祫鍒嗙被
+ MES_PRO_MAINTENANCE_PLAN_TYPE: 'mes_pro_maintenance_plan_type', // MES 鐢靛姏浣滀笟璁″垝绫诲瀷
+ MES_PRO_MAINTENANCE_PLAN_STATUS: 'mes_pro_maintenance_plan_status', // MES 鐢靛姏浣滀笟璁″垝鐘舵��
+ MES_QC_OUTSOURCE_TEST_TYPE: 'mes_qc_outsource_test_type', // MES 濮斿妫�娴嬬被鍨�
+ MES_QC_OUTSOURCE_TEST_RESULT: 'mes_qc_outsource_test_result', // MES 濮斿妫�娴嬬粨鏋�
+ MES_PRO_PROCESS_TYPE: 'mes_pro_process_type', // MES 鐢靛姏宸ヨ壓鍒嗙被
+ MES_PRO_EMERGENCY_PLAN_TYPE: 'mes_pro_emergency_plan_type', // MES 搴旀�ュ缃妗堢被鍨�
MES_MD_AUTO_CODE_CYCLE_METHOD: 'mes_md_auto_code_cycle_method', // MES 缂栫爜瑙勫垯寰幆鏂瑰紡
MES_MD_AUTO_CODE_PADDED_METHOD: 'mes_md_auto_code_padded_method', // MES 缂栫爜瑙勫垯琛ラ綈鏂瑰紡
MES_MD_AUTO_CODE_PART_TYPE: 'mes_md_auto_code_part_type', // MES 缂栫爜瑙勫垯鍒嗘绫诲瀷
@@ -252,6 +271,10 @@
MES_PD_VERSION_CHANGE_TYPE: 'mes_pd_version_change_type', // MES 鐗堟湰鍙樻洿绫诲瀷
MES_PD_DOCUMENT_TYPE: 'mes_pd_document_type', // MES 璁捐璧勬枡绫诲瀷
MES_PD_ARCHIVE_STATUS: 'mes_pd_archive_status', // MES 璁捐褰掓。鐘舵��
+ PRODUCT_CONDUCTOR_TYPE: 'product_conductor_type', // 鎹綅瀵肩嚎瀵间綋绫诲瀷
+ PRODUCT_REEL_TYPE: 'product_reel_type', // 鎹綅瀵肩嚎鐩樺叿绫诲瀷
+ PRODUCT_TECH_CONFIRM_TEMPLATE: 'product_tech_confirm_template', // 鎶�鏈‘璁ゅ嚱妯℃澘绫诲瀷
+ PRODUCT_TECH_CONFIRM_STATUS: 'product_tech_confirm_status', // 鎶�鏈‘璁ゅ嚱鐘舵��
MES_PRO_FEEDBACK_CHANNEL: 'mes_pro_feedback_channel', // MES 鐢熶骇鎶ュ伐閫斿緞
MES_PRO_ANDON_STATUS: 'mes_pro_andon_status', // MES 瀹夌伅澶勭疆鐘舵��
MES_PRO_ANDON_LEVEL: 'mes_pro_andon_level', // MES 瀹夌伅绾у埆
@@ -270,6 +293,7 @@
MES_WM_BARCODE_FORMAT: 'mes_wm_barcode_format', // MES 鏉$爜鏍煎紡
MES_WM_PACKAGE_STATUS: 'mes_wm_package_status', // MES 瑁呯鍗曠姸鎬�
MES_WM_PRODUCT_SALES_STATUS: 'mes_wm_product_sales_status', // MES 閿�鍞嚭搴撳崟鐘舵��
+ MES_WM_WAREHOUSE_TYPE: 'mes_wm_warehouse_type', // MES 浠撳簱/搴撳尯绫诲瀷
MES_WM_QUALITY_STATUS: 'mes_wm_quality_status', // MES 璐ㄩ噺鐘舵��
MES_WM_MISC_ISSUE_TYPE: 'mes_wm_misc_issue_type', // MES 鏉傞」鍑哄簱绫诲瀷
MES_WM_MISC_ISSUE_STATUS: 'mes_wm_misc_issue_status', // MES 鏉傞」鍑哄簱鍗曠姸鎬�
@@ -285,6 +309,7 @@
MES_WM_RETURN_ISSUE_STATUS: 'mes_wm_return_issue_status', // MES 鐢熶骇閫�鏂欏崟鐘舵��
MES_WM_RETURN_ISSUE_TYPE: 'mes_wm_return_issue_type', // MES 閫�鏂欑被鍨�
MES_WM_PRODUCT_ISSUE_STATUS: 'mes_wm_product_issue_status', // MES 棰嗘枡鍑哄簱鍗曠姸鎬�
+ MES_WM_PRODUCT_ISSUE_TYPE: 'mes_wm_product_issue_type', // MES 鐢熶骇棰嗘枡鍗曟嵁绫诲瀷
MES_WM_PRODUCT_RECEIPT_STATUS: 'mes_wm_product_receipt_status', // MES 浜у搧鍏ュ簱鍗曠姸鎬�
MES_WM_STOCK_TAKING_TYPE: 'mes_wm_stock_taking_type', // MES 鐩樼偣绫诲瀷
MES_WM_STOCK_TAKING_TASK_STATUS: 'mes_wm_stock_taking_task_status', // MES 鐩樼偣浠诲姟鐘舵��
@@ -296,6 +321,12 @@
MES_PD_TEMPLATE_TYPE: 'mes_pd_template_type', // MES 宸ヨ壓妯℃澘绫诲瀷
MES_NCR_STATUS: 'mes_ncr_status', // MES 涓嶅悎鏍煎搧澶勭悊鍗曠姸鎬�
MES_NCR_DISPOSITION: 'mes_ncr_disposition', // MES 涓嶅悎鏍煎搧澶勭疆鏂瑰紡
+ MES_PD_PRODUCT_ARCHIVE_TYPE: 'mes_pd_product_archive_type', // MES 浜у搧妗f绫诲瀷
+ MES_PD_AUDIT_STATUS: 'mes_pd_audit_status', // MES 浜у搧璁捐瀹℃壒鐘舵��
+ MES_PD_PRODUCT_STATUS: 'mes_pd_product_status', // MES 浜у搧妗f鍚敤鐘舵��
+ MES_PD_PRICE_OBJECT_TYPE: 'mes_pd_price_object_type', // MES 瀹氫环瀵硅薄绫诲瀷
+ MES_PD_DISCOUNT_POLICY_TYPE: 'mes_pd_discount_policy_type', // MES 浼樻儬绛栫暐绫诲瀷
+ MES_PD_TIERED_PRICE_STATUS: 'mes_pd_tiered_price_status', // MES 闃舵鐢典环鍚敤鐘舵��
} as const;
/** ========== WMS - 浠撳偍绠$悊妯″潡 ========== */
@@ -317,6 +348,7 @@
const HRM_DICT = {
HRM_DEGREE: 'hrm_degree', // HRM 瀛﹀巻
HRM_EMPLOYEE_STATUS: 'hrm_employee_status', // HRM 鍛樺伐鐘舵��
+ HRM_CERTIFICATE_TYPE: 'hrm_certificate_type', // HRM 璇佷功璧勮川绫诲瀷
HRM_GENDER: 'hrm_gender', // HRM 鎬у埆
HRM_AUDIT_STATUS: 'hrm_audit_status', // HRM 瀹℃壒鐘舵��
HRM_LEAVE_TYPE: 'hrm_leave_type', // HRM 璇峰亣绫诲瀷
diff --git a/src/views/aftersales/ticket/data.ts b/src/views/aftersales/ticket/data.ts
index 15c83cc..50066ad 100644
--- a/src/views/aftersales/ticket/data.ts
+++ b/src/views/aftersales/ticket/data.ts
@@ -25,6 +25,10 @@
{ label: '涓�鑸棶棰�', value: 1 },
{ label: '缁翠慨闂', value: 2 },
{ label: '閫�璐ч棶棰�', value: 3 },
+ { label: '鍋滅數鎶ヤ慨', value: 4 },
+ { label: '鐢ㄧ數鏁呴殰', value: 5 },
+ { label: '鏈嶅姟鎶曡瘔', value: 6 },
+ { label: '渚涚數璐ㄩ噺寮傝', value: 7 },
];
/** 涓ラ噸绋嬪害閫夐」 */
diff --git a/src/views/crm/cancellation/data.ts b/src/views/crm/cancellation/data.ts
new file mode 100644
index 0000000..a05ece5
--- /dev/null
+++ b/src/views/crm/cancellation/data.ts
@@ -0,0 +1,227 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmCancellationApi } from '#/api/crm/cancellation';
+import type { CrmCustomerApi } from '#/api/crm/customer';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+/** 鏂板/淇敼閿�鎴风敵璇� */
+export function useFormSchema(
+ formApi?: VbenFormApi,
+): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'cancellationNo',
+ label: '閿�鎴风紪鍙�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '涓嶅~鍒欒嚜鍔ㄧ敓鎴�',
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '瀹㈡埛',
+ component: markRaw(CrmCustomerSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨瀹㈡埛',
+ onChange: (item?: CrmCustomerApi.Customer) => {
+ formApi?.setFieldValue('customerName', item?.name);
+ },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'customerName',
+ label: '瀹㈡埛鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '瀹㈡埛鍚嶇О',
+ readonly: true,
+ },
+ },
+ {
+ fieldName: 'cancellationType',
+ label: '閿�鎴风被鍨�',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.CRM_CANCELLATION_TYPE),
+ placeholder: '璇烽�夋嫨閿�鎴风被鍨�',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'contractNo',
+ label: '鍚堝悓缂栧彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ叧鑱斿悎鍚岀紪鍙�',
+ },
+ },
+ {
+ fieldName: 'expectedCancelDate',
+ label: '棰勮閿�鎴锋棩鏈�',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ format: 'YYYY-MM-DD',
+ valueFormat: 'YYYY-MM-DD',
+ },
+ },
+ {
+ fieldName: 'actualCancelDate',
+ label: '瀹為檯閿�鎴锋棩鏈�',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ format: 'YYYY-MM-DD',
+ valueFormat: 'YYYY-MM-DD',
+ },
+ },
+ {
+ fieldName: 'refundAmount',
+ label: '閫�璐归噾棰�',
+ component: 'InputNumber',
+ componentProps: {
+ class: 'w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ラ��璐归噾棰濓紙鍏冿級',
+ },
+ },
+ {
+ fieldName: 'applyUserName',
+ label: '鐢宠浜�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ敵璇蜂汉濮撳悕',
+ },
+ },
+ {
+ fieldName: 'applyTime',
+ label: '鐢宠鏃堕棿',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ showTime: true,
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ },
+ },
+ {
+ fieldName: 'cancelReason',
+ label: '閿�鎴峰師鍥�',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '璇疯緭鍏ラ攢鎴峰師鍥�',
+ rows: 2,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'cancellationNo',
+ label: '閿�鎴风紪鍙�',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ラ攢鎴风紪鍙�',
+ },
+ },
+ {
+ fieldName: 'cancellationType',
+ label: '閿�鎴风被鍨�',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.CRM_CANCELLATION_TYPE),
+ placeholder: '璇烽�夋嫨閿�鎴风被鍨�',
+ },
+ },
+ {
+ fieldName: 'auditStatus',
+ label: '瀹℃壒鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.CRM_AUDIT_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨瀹℃壒鐘舵��',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<CrmCancellationApi.Cancellation>['columns'] {
+ return [
+ { field: 'cancellationNo', title: '閿�鎴风紪鍙�', minWidth: 170 },
+ { field: 'customerName', title: '瀹㈡埛鍚嶇О', minWidth: 180 },
+ {
+ field: 'cancellationType',
+ title: '閿�鎴风被鍨�',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CANCELLATION_TYPE },
+ },
+ },
+ { field: 'contractNo', title: '鍚堝悓缂栧彿', minWidth: 140 },
+ {
+ field: 'expectedCancelDate',
+ title: '棰勮閿�鎴锋棩鏈�',
+ width: 120,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'actualCancelDate',
+ title: '瀹為檯閿�鎴锋棩鏈�',
+ width: 120,
+ formatter: 'formatDate',
+ },
+ { field: 'refundAmount', title: '閫�璐归噾棰�', width: 120 },
+ { field: 'applyUserName', title: '鐢宠浜�', width: 100 },
+ {
+ field: 'auditStatus',
+ title: '瀹℃壒鐘舵��',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
+ },
+ },
+ {
+ title: '鎿嶄綔',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/crm/cancellation/index.vue b/src/views/crm/cancellation/index.vue
new file mode 100644
index 0000000..731d4e1
--- /dev/null
+++ b/src/views/crm/cancellation/index.vue
@@ -0,0 +1,189 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmCancellationApi } from '#/api/crm/cancellation';
+
+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 {
+ auditCancellation,
+ deleteCancellation,
+ exportCancellation,
+ getCancellationPage,
+} from '#/api/crm/cancellation';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+ const formValues = await gridApi.formApi.getValues();
+ const data = await exportCancellation(formValues);
+ downloadFileFromBlobPart({ fileName: '閿�鎴风敵璇�.xls', source: data });
+}
+
+/** 鏂板 */
+function handleCreate() {
+ formModalApi.setData(null).open();
+}
+
+/** 缂栬緫 */
+function handleEdit(row: CrmCancellationApi.Cancellation) {
+ formModalApi.setData({ cancellation: row }).open();
+}
+
+/** 鍒犻櫎 */
+async function handleDelete(row: CrmCancellationApi.Cancellation) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.cancellationNo ?? '']),
+ duration: 0,
+ });
+ try {
+ await deleteCancellation(row.id!);
+ message.success(
+ $t('ui.actionMessage.deleteSuccess', [row.cancellationNo ?? '']),
+ );
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 瀹℃壒锛堥�氳繃/椹冲洖锛� */
+async function handleAudit(
+ row: CrmCancellationApi.Cancellation,
+ pass: boolean,
+) {
+ const hideLoading = message.loading({
+ content: pass ? '瀹℃壒閫氳繃涓�...' : '瀹℃壒椹冲洖涓�...',
+ duration: 0,
+ });
+ try {
+ await auditCancellation({ id: row.id!, auditStatus: pass ? 20 : 30 });
+ message.success(pass ? '瀹℃壒閫氳繃' : '瀹℃壒椹冲洖');
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) =>
+ await getCancellationPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<CrmCancellationApi.Cancellation>,
+});
+</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: ['crm:cancellation:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['crm:cancellation:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '閫氳繃',
+ type: 'link',
+ icon: ACTION_ICON.AUDIT,
+ auth: ['crm:cancellation:update'],
+ ifShow: row.auditStatus === 0 || row.auditStatus === 10,
+ popConfirm: {
+ title: '纭瀹℃壒閫氳繃璇ラ攢鎴风敵璇凤紵',
+ confirm: handleAudit.bind(null, row, true),
+ },
+ },
+ {
+ label: '椹冲洖',
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.AUDIT,
+ auth: ['crm:cancellation:update'],
+ ifShow: row.auditStatus === 0 || row.auditStatus === 10,
+ popConfirm: {
+ title: '纭椹冲洖璇ラ攢鎴风敵璇凤紵',
+ confirm: handleAudit.bind(null, row, false),
+ },
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['crm:cancellation:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['crm:cancellation:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [
+ row.cancellationNo ?? '',
+ ]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/crm/cancellation/modules/form.vue b/src/views/crm/cancellation/modules/form.vue
new file mode 100644
index 0000000..5a7fe02
--- /dev/null
+++ b/src/views/crm/cancellation/modules/form.vue
@@ -0,0 +1,95 @@
+<script lang="ts" setup>
+import type { CrmCancellationApi } from '#/api/crm/cancellation';
+
+import { computed, ref } from 'vue';
+
+import { useVbenForm, useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import {
+ createCancellation,
+ getCancellation,
+ updateCancellation,
+} from '#/api/crm/cancellation';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+type CancellationFormModalData = {
+ cancellation?: Pick<CrmCancellationApi.Cancellation, 'id'>;
+};
+
+const emit = defineEmits(['success']);
+const formData = ref<Partial<CrmCancellationApi.Cancellation>>();
+const getTitle = computed(() => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['閿�鎴风敵璇�'])
+ : $t('ui.actionTitle.create', ['閿�鎴风敵璇�']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 110,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data =
+ (await formApi.getValues()) as CrmCancellationApi.Cancellation;
+ if (formData.value?.id) {
+ data.id = formData.value.id;
+ }
+ try {
+ await (formData.value?.id
+ ? updateCancellation(data)
+ : createCancellation(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ formApi.setState({ schema: useFormSchema(formApi) });
+ await formApi.resetForm();
+ const data = modalApi.getData() as null | CancellationFormModalData;
+ if (!data?.cancellation?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getCancellation(data.cancellation.id);
+ if (formData.value) {
+ await formApi.setValues(formData.value as any);
+ }
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-3/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/crm/contract/components/command-form-data.ts b/src/views/crm/contract/components/command-form-data.ts
new file mode 100644
index 0000000..0a50d56
--- /dev/null
+++ b/src/views/crm/contract/components/command-form-data.ts
@@ -0,0 +1,105 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+/** 鏂板/淇敼鍚堝悓鎸囦护 */
+export function useFormSchema(
+ formApi?: VbenFormApi,
+): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'contractId',
+ label: '鍚堝悓ID',
+ component: 'InputNumber',
+ componentProps: {
+ class: 'w-full',
+ precision: 0,
+ placeholder: '鍚堝悓ID',
+ },
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'contractNo',
+ label: '鍚堝悓缂栧彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '鍚堝悓缂栧彿',
+ readonly: true,
+ },
+ },
+ {
+ fieldName: 'commandType',
+ label: '鎸囦护绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_COMMAND_TYPE),
+ placeholder: '璇烽�夋嫨鎸囦护绫诲瀷',
+ allowClear: true,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'targetModule',
+ label: '鐩爣妯″潡',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: 'MES 宸ュ崟', value: 'mes-work-order' },
+ { label: 'MES 鎶勮〃', value: 'mes-metering' },
+ { label: 'SRM 鏈嶅姟鍗�', value: 'srm-service' },
+ ],
+ placeholder: '璇烽�夋嫨鐩爣妯″潡',
+ },
+ },
+ {
+ fieldName: 'bizNo',
+ label: '涓氬姟鍗曞彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヤ笟鍔″崟鍙�',
+ },
+ },
+ {
+ fieldName: 'operatorName',
+ label: '鎿嶄綔浜�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ搷浣滀汉濮撳悕',
+ },
+ },
+ {
+ fieldName: 'operateTime',
+ label: '鎿嶄綔鏃堕棿',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ showTime: true,
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
diff --git a/src/views/crm/contract/components/command-form.vue b/src/views/crm/contract/components/command-form.vue
new file mode 100644
index 0000000..8a64bac
--- /dev/null
+++ b/src/views/crm/contract/components/command-form.vue
@@ -0,0 +1,90 @@
+<script lang="ts" setup>
+import type { CrmContractCommandApi } from '#/api/crm/contract-command';
+
+import { ref } from 'vue';
+
+import { useVbenForm, useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import {
+ createContractCommand,
+ updateContractCommand,
+} from '#/api/crm/contract-command';
+import { $t } from '#/locales';
+
+import { useFormSchema } from './command-form-data';
+
+const emit = defineEmits(['success']);
+
+const formData = ref<Partial<CrmContractCommandApi.ContractCommand>>();
+const getTitle = () => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['鎸囦护'])
+ : $t('ui.actionTitle.create', ['鎸囦护']);
+};
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 110,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data =
+ (await formApi.getValues()) as CrmContractCommandApi.ContractCommand;
+ if (formData.value?.id) {
+ data.id = formData.value.id;
+ }
+ try {
+ await (formData.value?.id
+ ? updateContractCommand(data)
+ : createContractCommand(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ formApi.setState({ schema: useFormSchema(formApi) });
+ await formApi.resetForm();
+ const data = modalApi.getData() as null | {
+ command?: CrmContractCommandApi.ContractCommand;
+ contractNo?: string;
+ };
+ if (data?.contractNo) {
+ await formApi.setFieldValue('contractNo', data.contractNo);
+ }
+ if (!data?.command?.id) {
+ return;
+ }
+ formData.value = data.command;
+ await formApi.setValues(formData.value as any);
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle()" class="w-2/3">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/crm/contract/components/command-list-data.ts b/src/views/crm/contract/components/command-list-data.ts
new file mode 100644
index 0000000..04b43ad
--- /dev/null
+++ b/src/views/crm/contract/components/command-list-data.ts
@@ -0,0 +1,55 @@
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmContractCommandApi } from '#/api/crm/contract-command';
+
+import { DICT_TYPE } from '@vben/constants';
+
+/** 鍚堝悓鎸囦护璇︽儏鍒楄〃瀛楁 */
+export function useDetailListColumns(): VxeTableGridOptions<CrmContractCommandApi.ContractCommand>['columns'] {
+ return [
+ {
+ field: 'commandType',
+ title: '鎸囦护绫诲瀷',
+ width: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_COMMAND_TYPE },
+ },
+ },
+ {
+ field: 'targetModule',
+ title: '鐩爣妯″潡',
+ width: 130,
+ },
+ {
+ field: 'bizNo',
+ title: '涓氬姟鍗曞彿',
+ minWidth: 140,
+ },
+ {
+ field: 'operatorName',
+ title: '鎿嶄綔浜�',
+ width: 100,
+ },
+ {
+ field: 'operateTime',
+ title: '鎿嶄綔鏃堕棿',
+ minWidth: 160,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_COMMAND_STATUS },
+ },
+ },
+ {
+ title: '鎿嶄綔',
+ width: 220,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/crm/contract/components/command-list.vue b/src/views/crm/contract/components/command-list.vue
new file mode 100644
index 0000000..bea274d
--- /dev/null
+++ b/src/views/crm/contract/components/command-list.vue
@@ -0,0 +1,188 @@
+<!-- 鍚堝悓鎸囦护涓嬪彂鍒楄〃锛氱敤浜庛�愬悎鍚屻�戣鎯� Tab锛屽睍绀哄苟涓嬪彂/鎵ц/鍙栨秷鍚堝悓鎸囦护 -->
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmContractCommandApi } from '#/api/crm/contract-command';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ cancelContractCommand,
+ deleteContractCommand,
+ executeContractCommand,
+ getContractCommandPage,
+} from '#/api/crm/contract-command';
+import { $t } from '#/locales';
+
+import CommandForm from './command-form.vue';
+import { useDetailListColumns } from './command-list-data';
+
+const props = defineProps<{
+ contractId?: number; // 鍚堝悓缂栧彿
+ contractNo?: string; // 鍚堝悓鍗曞彿
+}>();
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: CommandForm,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 鍒涘缓鎸囦护 */
+function handleCreate() {
+ formModalApi
+ .setData({ contractNo: props.contractNo })
+ .open();
+}
+
+/** 缂栬緫鎸囦护 */
+function handleEdit(row: CrmContractCommandApi.ContractCommand) {
+ formModalApi.setData({ command: row }).open();
+}
+
+/** 鎵ц鎸囦护 */
+async function handleExecute(row: CrmContractCommandApi.ContractCommand) {
+ const hideLoading = message.loading({ content: '鎸囦护鎵ц涓�...', duration: 0 });
+ try {
+ await executeContractCommand(row.id!);
+ message.success('鎸囦护鎵ц瀹屾垚');
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 鍙栨秷鎸囦护 */
+async function handleCancel(row: CrmContractCommandApi.ContractCommand) {
+ const hideLoading = message.loading({ content: '鎸囦护鍙栨秷涓�...', duration: 0 });
+ try {
+ await cancelContractCommand(row.id!);
+ message.success('鎸囦护宸插彇娑�');
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 鍒犻櫎鎸囦护 */
+async function handleDelete(row: CrmContractCommandApi.ContractCommand) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.bizNo ?? '']),
+ duration: 0,
+ });
+ try {
+ await deleteContractCommand(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.bizNo ?? '']));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ columns: useDetailListColumns(),
+ height: 400,
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }) => {
+ if (!props.contractId) {
+ return { list: [], total: 0 };
+ }
+ return await getContractCommandPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ contractId: props.contractId,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: false,
+ },
+ } as VxeTableGridOptions<CrmContractCommandApi.ContractCommand>,
+});
+</script>
+
+<template>
+ <div>
+ <FormModal @success="handleRefresh" />
+ <Grid>
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: '涓嬪彂鎸囦护',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['crm:contract-command:create'],
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '鎵ц',
+ type: 'link',
+ icon: ACTION_ICON.AUDIT,
+ auth: ['crm:contract-command:update'],
+ ifShow: row.status === 0 || row.status === 10,
+ popConfirm: {
+ title: '纭鎵ц璇ユ寚浠わ紵',
+ confirm: handleExecute.bind(null, row),
+ },
+ },
+ {
+ label: '鍙栨秷',
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.CLOSE,
+ auth: ['crm:contract-command:update'],
+ ifShow: row.status === 0 || row.status === 10,
+ popConfirm: {
+ title: '纭鍙栨秷璇ユ寚浠わ紵',
+ confirm: handleCancel.bind(null, row),
+ },
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['crm:contract-command:update'],
+ ifShow: row.status === 0,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['crm:contract-command:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [
+ row.bizNo ?? '',
+ ]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </div>
+</template>
diff --git a/src/views/crm/contract/data.ts b/src/views/crm/contract/data.ts
index 7dabc09..87bfcf2 100644
--- a/src/views/crm/contract/data.ts
+++ b/src/views/crm/contract/data.ts
@@ -1,6 +1,8 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
import { useUserStore } from '@vben/stores';
import { erpPriceInputFormatter, erpPriceMultiply } from '@vben/utils';
@@ -199,6 +201,78 @@
},
},
{
+ fieldName: 'contractType',
+ label: '鍚堝悓绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_CONTRACT_TYPE),
+ placeholder: '璇烽�夋嫨鍚堝悓绫诲瀷',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'voltageLevel',
+ label: '鐢靛帇绛夌骇',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_VOLTAGE_LEVEL),
+ placeholder: '璇烽�夋嫨鐢靛帇绛夌骇',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'electricityType',
+ label: '鐢ㄧ數绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_ELECTRICITY_TYPE),
+ placeholder: '璇烽�夋嫨鐢ㄧ數绫诲瀷',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'contractCapacity',
+ label: '鍚堝悓瀹归噺锛坘VA锛�',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ュ悎鍚屽閲�',
+ },
+ },
+ {
+ fieldName: 'tariffMode',
+ label: '鐢典环妯″紡',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_TARIFF_MODE),
+ placeholder: '璇烽�夋嫨鐢典环妯″紡',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'meterMode',
+ label: '璁¢噺鏂瑰紡',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_METER_MODE),
+ placeholder: '璇烽�夋嫨璁¢噺鏂瑰紡',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'payCycle',
+ label: '缂磋垂鍛ㄦ湡锛堟湀锛�',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 1,
+ precision: 0,
+ placeholder: '璇疯緭鍏ョ即璐瑰懆鏈�',
+ },
+ },
+ {
fieldName: 'remark',
label: '澶囨敞',
component: 'Textarea',
@@ -353,6 +427,34 @@
slots: { default: 'businessName' },
},
{
+ title: '鍚堝悓绫诲瀷',
+ field: 'contractType',
+ minWidth: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CONTRACT_TYPE },
+ },
+ },
+ {
+ title: '鐢靛帇绛夌骇',
+ field: 'voltageLevel',
+ minWidth: 90,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_VOLTAGE_LEVEL },
+ },
+ },
+ {
+ title: '鍚堝悓瀹归噺',
+ field: 'contractCapacity',
+ minWidth: 100,
+ },
+ {
+ title: '鎸囦护鏁�',
+ field: 'commandCount',
+ minWidth: 80,
+ },
+ {
title: '鍚堝悓閲戦锛堝厓锛�',
field: 'totalPrice',
minWidth: 140,
diff --git a/src/views/crm/contract/detail/index.vue b/src/views/crm/contract/detail/index.vue
index 972e42f..a5258db 100644
--- a/src/views/crm/contract/detail/index.vue
+++ b/src/views/crm/contract/detail/index.vue
@@ -26,6 +26,7 @@
import Form from '../modules/form.vue';
import { useDetailSchema } from './data';
import ContractDetailsInfo from './modules/info.vue';
+import CommandDetailsList from '../components/command-list.vue';
const props = defineProps<{ id?: number }>();
@@ -171,6 +172,13 @@
<Tabs.TabPane tab="鎿嶄綔鏃ュ織" key="6" :force-render="true">
<OperateLog :log-list="logList" />
</Tabs.TabPane>
+ <Tabs.TabPane tab="鎸囦护涓嬪彂" key="7" :force-render="true">
+ <CommandDetailsList
+ v-if="contractId"
+ :contract-id="contractId"
+ :contract-no="contract.no"
+ />
+ </Tabs.TabPane>
</Tabs>
</Card>
</Page>
diff --git a/src/views/crm/credit-rating/data.ts b/src/views/crm/credit-rating/data.ts
new file mode 100644
index 0000000..e8e59c2
--- /dev/null
+++ b/src/views/crm/credit-rating/data.ts
@@ -0,0 +1,212 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmCustomerApi } from '#/api/crm/customer';
+import type { CrmCreditRatingApi } from '#/api/crm/credit-rating';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+/** 鏂板/淇敼淇$敤璇勭骇 */
+export function useFormSchema(
+ formApi?: VbenFormApi,
+): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '瀹㈡埛',
+ component: markRaw(CrmCustomerSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨瀹㈡埛',
+ onChange: (item?: CrmCustomerApi.Customer) => {
+ formApi?.setFieldValue('customerName', item?.name);
+ },
+ },
+ },
+ {
+ fieldName: 'customerName',
+ label: '瀹㈡埛鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '瀹㈡埛鍚嶇О',
+ readonly: true,
+ },
+ },
+ {
+ fieldName: 'level',
+ label: '淇$敤绛夌骇',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.CRM_CREDIT_LEVEL),
+ placeholder: '璇烽�夋嫨淇$敤绛夌骇',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'score',
+ label: '淇$敤鍒�',
+ component: 'InputNumber',
+ componentProps: {
+ class: 'w-full',
+ min: 0,
+ max: 100,
+ precision: 0,
+ placeholder: '0-100',
+ },
+ },
+ {
+ fieldName: 'evalType',
+ label: '璇勫畾绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '鍒濊瘎', value: 'INITIAL' },
+ { label: '澶嶈瘎', value: 'REVIEW' },
+ { label: '鍔ㄦ�佽皟鏁�', value: 'ADJUST' },
+ ],
+ placeholder: '璇烽�夋嫨璇勫畾绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'evalReason',
+ label: '璇勫畾鐞嗙敱',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ瘎瀹氱悊鐢�',
+ rows: 2,
+ },
+ },
+ {
+ fieldName: 'operateTime',
+ label: '璇勫畾鏃堕棿',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ showTime: true,
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ },
+ },
+ {
+ fieldName: 'validUntil',
+ label: '鏈夋晥鏈熻嚦',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ format: 'YYYY-MM-DD',
+ valueFormat: 'YYYY-MM-DD',
+ },
+ },
+ {
+ fieldName: 'operatorName',
+ label: '璇勫畾浜�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ瘎瀹氫汉濮撳悕',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'level',
+ label: '淇$敤绛夌骇',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.CRM_CREDIT_LEVEL),
+ placeholder: '璇烽�夋嫨淇$敤绛夌骇',
+ },
+ },
+ {
+ fieldName: 'evalType',
+ label: '璇勫畾绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '鍒濊瘎', value: 'INITIAL' },
+ { label: '澶嶈瘎', value: 'REVIEW' },
+ { label: '鍔ㄦ�佽皟鏁�', value: 'ADJUST' },
+ ],
+ placeholder: '璇烽�夋嫨璇勫畾绫诲瀷',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<CrmCreditRatingApi.CreditRating>['columns'] {
+ return [
+ { field: 'customerName', title: '瀹㈡埛鍚嶇О', minWidth: 180 },
+ {
+ field: 'level',
+ title: '淇$敤绛夌骇',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CREDIT_LEVEL },
+ },
+ },
+ { field: 'score', title: '淇$敤鍒�', width: 90 },
+ {
+ field: 'evalType',
+ title: '璇勫畾绫诲瀷',
+ width: 120,
+ slots: { default: 'evalType' },
+ },
+ { field: 'evalReason', title: '璇勫畾鐞嗙敱', minWidth: 200, showOverflow: true },
+ { field: 'operatorName', title: '璇勫畾浜�', width: 100 },
+ {
+ field: 'operateTime',
+ title: '璇勫畾鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'validUntil',
+ title: '鏈夋晥鏈熻嚦',
+ width: 120,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/crm/credit-rating/index.vue b/src/views/crm/credit-rating/index.vue
new file mode 100644
index 0000000..4821827
--- /dev/null
+++ b/src/views/crm/credit-rating/index.vue
@@ -0,0 +1,156 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmCreditRatingApi } from '#/api/crm/credit-rating';
+
+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 {
+ deleteCreditRating,
+ exportCreditRating,
+ getCreditRatingPage,
+} from '#/api/crm/credit-rating';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+ const formValues = await gridApi.formApi.getValues();
+ const data = await exportCreditRating(formValues);
+ downloadFileFromBlobPart({ fileName: '瀹㈡埛淇$敤璇勭骇.xls', source: data });
+}
+
+/** 鏂板 */
+function handleCreate() {
+ formModalApi.setData(null).open();
+}
+
+/** 缂栬緫 */
+function handleEdit(row: CrmCreditRatingApi.CreditRating) {
+ formModalApi.setData({ creditRating: row }).open();
+}
+
+/** 鍒犻櫎 */
+async function handleDelete(row: CrmCreditRatingApi.CreditRating) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.customerName ?? '']),
+ duration: 0,
+ });
+ try {
+ await deleteCreditRating(row.id!);
+ message.success(
+ $t('ui.actionMessage.deleteSuccess', [row.customerName ?? '']),
+ );
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) =>
+ await getCreditRatingPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<CrmCreditRatingApi.CreditRating>,
+});
+</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: ['crm:credit-rating:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['crm:credit-rating:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #evalType="{ row }">
+ {{
+ row.evalType === 'INITIAL'
+ ? '鍒濊瘎'
+ : row.evalType === 'REVIEW'
+ ? '澶嶈瘎'
+ : '鍔ㄦ�佽皟鏁�'
+ }}
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['crm:credit-rating:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['crm:credit-rating:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [
+ row.customerName ?? '',
+ ]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/crm/credit-rating/modules/form.vue b/src/views/crm/credit-rating/modules/form.vue
new file mode 100644
index 0000000..20eb67b
--- /dev/null
+++ b/src/views/crm/credit-rating/modules/form.vue
@@ -0,0 +1,94 @@
+<script lang="ts" setup>
+import type { CrmCreditRatingApi } from '#/api/crm/credit-rating';
+
+import { computed, ref } from 'vue';
+
+import { useVbenForm, useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import {
+ createCreditRating,
+ getCreditRating,
+ updateCreditRating,
+} from '#/api/crm/credit-rating';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+type CreditRatingFormModalData = {
+ creditRating?: Pick<CrmCreditRatingApi.CreditRating, 'id'>;
+};
+
+const emit = defineEmits(['success']);
+const formData = ref<Partial<CrmCreditRatingApi.CreditRating>>();
+const getTitle = computed(() => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['淇$敤璇勭骇'])
+ : $t('ui.actionTitle.create', ['淇$敤璇勭骇']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 110,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as CrmCreditRatingApi.CreditRating;
+ if (formData.value?.id) {
+ data.id = formData.value.id;
+ }
+ try {
+ await (formData.value?.id
+ ? updateCreditRating(data)
+ : createCreditRating(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ formApi.setState({ schema: useFormSchema(formApi) });
+ await formApi.resetForm();
+ const data = modalApi.getData() as null | CreditRatingFormModalData;
+ if (!data?.creditRating?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getCreditRating(data.creditRating.id);
+ if (formData.value) {
+ await formApi.setValues(formData.value as any);
+ }
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-3/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/crm/customer/data.ts b/src/views/crm/customer/data.ts
index d3372ee..203cd55 100644
--- a/src/views/crm/customer/data.ts
+++ b/src/views/crm/customer/data.ts
@@ -45,6 +45,16 @@
},
},
{
+ fieldName: 'customerType',
+ label: '瀹㈡埛鍒嗙被',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_TYPE),
+ placeholder: '璇烽�夋嫨瀹㈡埛鍒嗙被',
+ allowClear: true,
+ },
+ },
+ {
fieldName: 'mobile',
label: '鎵嬫満',
component: 'Input',
@@ -202,6 +212,26 @@
},
},
{
+ fieldName: 'customerType',
+ label: '瀹㈡埛鍒嗙被',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_TYPE),
+ placeholder: '璇烽�夋嫨瀹㈡埛鍒嗙被',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'creditLevel',
+ label: '淇$敤绛夌骇',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_CREDIT_LEVEL),
+ placeholder: '璇烽�夋嫨淇$敤绛夌骇',
+ allowClear: true,
+ },
+ },
+ {
fieldName: 'createTime',
label: '鍒涘缓鏃堕棿',
component: 'RangePicker',
@@ -276,6 +306,40 @@
},
},
{
+ field: 'customerType',
+ title: '瀹㈡埛鍒嗙被',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CUSTOMER_TYPE },
+ },
+ },
+ {
+ field: 'creditLevel',
+ title: '淇$敤绛夌骇',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_CREDIT_LEVEL },
+ },
+ },
+ {
+ field: 'creditScore',
+ title: '淇$敤鍒�',
+ minWidth: 90,
+ },
+ {
+ field: 'overdueAmount',
+ title: '娆犺垂閲戦',
+ minWidth: 110,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'overdueCount',
+ title: '娆犺垂绗旀暟',
+ minWidth: 90,
+ },
+ {
field: 'mobile',
title: '鎵嬫満',
minWidth: 120,
diff --git a/src/views/crm/refund/data.ts b/src/views/crm/refund/data.ts
new file mode 100644
index 0000000..f239359
--- /dev/null
+++ b/src/views/crm/refund/data.ts
@@ -0,0 +1,208 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmCustomerApi } from '#/api/crm/customer';
+import type { CrmRefundApi } from '#/api/crm/refund';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+/** 鏂板/淇敼閫�璐瑰崟 */
+export function useFormSchema(
+ formApi?: VbenFormApi,
+): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'refundNo',
+ label: '閫�璐瑰崟鍙�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '涓嶅~鍒欒嚜鍔ㄧ敓鎴�',
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '瀹㈡埛',
+ component: markRaw(CrmCustomerSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨瀹㈡埛',
+ onChange: (item?: CrmCustomerApi.Customer) => {
+ formApi?.setFieldValue('customerName', item?.name);
+ },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'customerName',
+ label: '瀹㈡埛鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '瀹㈡埛鍚嶇О',
+ readonly: true,
+ },
+ },
+ {
+ fieldName: 'refundType',
+ label: '閫�璐圭被鍨�',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.CRM_REFUND_TYPE),
+ placeholder: '璇烽�夋嫨閫�璐圭被鍨�',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'refundAmount',
+ label: '閫�璐归噾棰�',
+ component: 'InputNumber',
+ componentProps: {
+ class: 'w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ラ��璐归噾棰濓紙鍏冿級',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'contractNo',
+ label: '鍚堝悓缂栧彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ叧鑱斿悎鍚岀紪鍙�',
+ },
+ },
+ {
+ fieldName: 'applyUserName',
+ label: '鐢宠浜�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ敵璇蜂汉濮撳悕',
+ },
+ },
+ {
+ fieldName: 'applyTime',
+ label: '鐢宠鏃堕棿',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ showTime: true,
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ },
+ },
+ {
+ fieldName: 'refundReason',
+ label: '閫�璐瑰師鍥�',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '璇疯緭鍏ラ��璐瑰師鍥�',
+ rows: 2,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'refundNo',
+ label: '閫�璐瑰崟鍙�',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ラ��璐瑰崟鍙�',
+ },
+ },
+ {
+ fieldName: 'refundType',
+ label: '閫�璐圭被鍨�',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.CRM_REFUND_TYPE),
+ placeholder: '璇烽�夋嫨閫�璐圭被鍨�',
+ },
+ },
+ {
+ fieldName: 'auditStatus',
+ label: '瀹℃壒鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.CRM_AUDIT_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨瀹℃壒鐘舵��',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<CrmRefundApi.Refund>['columns'] {
+ return [
+ { field: 'refundNo', title: '閫�璐瑰崟鍙�', minWidth: 170 },
+ { field: 'customerName', title: '瀹㈡埛鍚嶇О', minWidth: 180 },
+ {
+ field: 'refundType',
+ title: '閫�璐圭被鍨�',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_REFUND_TYPE },
+ },
+ },
+ { field: 'refundAmount', title: '閫�璐归噾棰�', width: 120 },
+ { field: 'contractNo', title: '鍚堝悓缂栧彿', minWidth: 140 },
+ { field: 'applyUserName', title: '鐢宠浜�', width: 100 },
+ {
+ field: 'applyTime',
+ title: '鐢宠鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'auditStatus',
+ title: '瀹℃壒鐘舵��',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/crm/refund/index.vue b/src/views/crm/refund/index.vue
new file mode 100644
index 0000000..bb053b4
--- /dev/null
+++ b/src/views/crm/refund/index.vue
@@ -0,0 +1,184 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmRefundApi } from '#/api/crm/refund';
+
+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 {
+ auditRefund,
+ deleteRefund,
+ exportRefund,
+ getRefundPage,
+} from '#/api/crm/refund';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+ const formValues = await gridApi.formApi.getValues();
+ const data = await exportRefund(formValues);
+ downloadFileFromBlobPart({ fileName: '閫�璐瑰崟.xls', source: data });
+}
+
+/** 鏂板 */
+function handleCreate() {
+ formModalApi.setData(null).open();
+}
+
+/** 缂栬緫 */
+function handleEdit(row: CrmRefundApi.Refund) {
+ formModalApi.setData({ refund: row }).open();
+}
+
+/** 鍒犻櫎 */
+async function handleDelete(row: CrmRefundApi.Refund) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.refundNo ?? '']),
+ duration: 0,
+ });
+ try {
+ await deleteRefund(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.refundNo ?? '']));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 瀹℃壒锛堥�氳繃/椹冲洖锛� */
+async function handleAudit(row: CrmRefundApi.Refund, pass: boolean) {
+ const hideLoading = message.loading({
+ content: pass ? '瀹℃壒閫氳繃涓�...' : '瀹℃壒椹冲洖涓�...',
+ duration: 0,
+ });
+ try {
+ await auditRefund({ id: row.id!, auditStatus: pass ? 20 : 30 });
+ message.success(pass ? '瀹℃壒閫氳繃' : '瀹℃壒椹冲洖');
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) =>
+ await getRefundPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<CrmRefundApi.Refund>,
+});
+</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: ['crm:refund:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['crm:refund:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '閫氳繃',
+ type: 'link',
+ icon: ACTION_ICON.AUDIT,
+ auth: ['crm:refund:update'],
+ ifShow: row.auditStatus === 0 || row.auditStatus === 10,
+ popConfirm: {
+ title: '纭瀹℃壒閫氳繃璇ラ��璐瑰崟锛�',
+ confirm: handleAudit.bind(null, row, true),
+ },
+ },
+ {
+ label: '椹冲洖',
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.AUDIT,
+ auth: ['crm:refund:update'],
+ ifShow: row.auditStatus === 0 || row.auditStatus === 10,
+ popConfirm: {
+ title: '纭椹冲洖璇ラ��璐瑰崟锛�',
+ confirm: handleAudit.bind(null, row, false),
+ },
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['crm:refund:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['crm:refund:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [
+ row.refundNo ?? '',
+ ]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/crm/refund/modules/form.vue b/src/views/crm/refund/modules/form.vue
new file mode 100644
index 0000000..60ae9e6
--- /dev/null
+++ b/src/views/crm/refund/modules/form.vue
@@ -0,0 +1,92 @@
+<script lang="ts" setup>
+import type { CrmRefundApi } from '#/api/crm/refund';
+
+import { computed, ref } from 'vue';
+
+import { useVbenForm, useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import {
+ createRefund,
+ getRefund,
+ updateRefund,
+} from '#/api/crm/refund';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+type RefundFormModalData = {
+ refund?: Pick<CrmRefundApi.Refund, 'id'>;
+};
+
+const emit = defineEmits(['success']);
+const formData = ref<Partial<CrmRefundApi.Refund>>();
+const getTitle = computed(() => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['閫�璐瑰崟'])
+ : $t('ui.actionTitle.create', ['閫�璐瑰崟']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 110,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as CrmRefundApi.Refund;
+ if (formData.value?.id) {
+ data.id = formData.value.id;
+ }
+ try {
+ await (formData.value?.id ? updateRefund(data) : createRefund(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ formApi.setState({ schema: useFormSchema(formApi) });
+ await formApi.resetForm();
+ const data = modalApi.getData() as null | RefundFormModalData;
+ if (!data?.refund?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getRefund(data.refund.id);
+ if (formData.value) {
+ await formApi.setValues(formData.value as any);
+ }
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-3/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/crm/saleQuotation/data.ts b/src/views/crm/saleQuotation/data.ts
index ab372f0..109c6e4 100644
--- a/src/views/crm/saleQuotation/data.ts
+++ b/src/views/crm/saleQuotation/data.ts
@@ -1,6 +1,9 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
import { erpPriceMultiply } from '#/packages/utils/src';
import { z } from '#/adapter/form';
@@ -156,6 +159,81 @@
},
},
{
+ fieldName: 'quotationType',
+ label: '鎶ヤ环绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_QUOTATION_TYPE),
+ placeholder: '璇烽�夋嫨鎶ヤ环绫诲瀷',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'capacity',
+ label: '鍚堝悓瀹归噺锛坘VA锛�',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ュ悎鍚屽閲�',
+ },
+ },
+ {
+ fieldName: 'maxDemand',
+ label: '鏈�澶ч渶閲忥紙kW锛�',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ユ渶澶ч渶閲�',
+ },
+ },
+ {
+ fieldName: 'unitPrice',
+ label: '鐢靛害鍗曚环锛堝厓/kWh锛�',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 4,
+ placeholder: '璇疯緭鍏ョ數搴﹀崟浠�',
+ },
+ },
+ {
+ fieldName: 'tariffMode',
+ label: '鐢典环妯″紡',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_TARIFF_MODE),
+ placeholder: '璇烽�夋嫨鐢典环妯″紡',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'powerFactorFee',
+ label: '鍔涜皟鐢佃垂锛堝厓锛�',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ュ姏璋冪數璐�',
+ },
+ },
+ {
+ fieldName: 'billingCycle',
+ label: '缁撶畻鍛ㄦ湡锛堟湀锛�',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 1,
+ precision: 0,
+ placeholder: '璇疯緭鍏ョ粨绠楀懆鏈�',
+ },
+ },
+ {
fieldName: 'remark',
label: '澶囨敞',
component: 'Textarea',
@@ -283,6 +361,15 @@
fixed: 'left',
},
{
+ field: 'quotationType',
+ title: '鎶ヤ环绫诲瀷',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_QUOTATION_TYPE },
+ },
+ },
+ {
field: 'customerName',
title: '瀹㈡埛鍚嶇О',
minWidth: 120,
diff --git a/src/views/erp/purchase/plan/data.ts b/src/views/erp/purchase/plan/data.ts
new file mode 100644
index 0000000..9a279a5
--- /dev/null
+++ b/src/views/erp/purchase/plan/data.ts
@@ -0,0 +1,242 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { getDictOptions } from '@vben/hooks';
+
+import { getSupplierSimpleList } from '#/api/srm/supplier';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 鐘舵�佸父閲� */
+export const PURCHASE_PLAN_STATUS = {
+ DRAFT: 0, // 鏈彁浜わ紙鑽夌锛�
+ PROCESSING: 10, // 瀹℃壒涓�
+ APPROVED: 20, // 瀹℃牳閫氳繃
+ REJECTED: 30, // 瀹℃牳涓嶉�氳繃
+ CANCELLED: 40, // 宸插彇娑�
+};
+
+/** 闇�姹傛潵婧愬父閲� */
+export const DEMAND_SOURCE = {
+ STOCK_ALERT: 1, // 搴撳瓨棰勮
+ PLAN: 2, // 杩愮淮/鐢熶骇璁″垝
+};
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+ const isReadonly = formType === 'detail';
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '璁″垝鍗曞彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '绯荤粺鑷姩鐢熸垚',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '璁″垝鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ鍒掑悕绉�',
+ disabled: isReadonly,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'planDate',
+ label: '璁″垝鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '閫夋嫨璁″垝鏃ユ湡',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ disabled: isReadonly,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'demandSource',
+ label: '闇�姹傛潵婧�',
+ component: 'Select',
+ componentProps: {
+ placeholder: '璇烽�夋嫨闇�姹傛潵婧�',
+ allowClear: true,
+ options: getDictOptions('erp_purchase_plan_demand_source', 'number'),
+ disabled: isReadonly,
+ },
+ },
+ {
+ label: '渚涘簲鍟�',
+ fieldName: 'supplierId',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '璇烽�夋嫨渚涘簲鍟�',
+ allowClear: true,
+ showSearch: true,
+ api: getSupplierSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: isReadonly,
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'items',
+ label: '璁″垝鏄庣粏',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '璁″垝鍗曞彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ鍒掑崟鍙�',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '璁″垝鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ鍒掑悕绉�',
+ },
+ },
+ {
+ fieldName: 'demandSource',
+ label: '闇�姹傛潵婧�',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions('erp_purchase_plan_demand_source', 'number'),
+ placeholder: '璇烽�夋嫨闇�姹傛潵婧�',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '鏈彁浜�', value: PURCHASE_PLAN_STATUS.DRAFT },
+ { label: '瀹℃壒涓�', value: PURCHASE_PLAN_STATUS.PROCESSING },
+ { label: '瀹℃牳閫氳繃', value: PURCHASE_PLAN_STATUS.APPROVED },
+ { label: '瀹℃牳涓嶉�氳繃', value: PURCHASE_PLAN_STATUS.REJECTED },
+ { label: '宸插彇娑�', value: PURCHASE_PLAN_STATUS.CANCELLED },
+ ],
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ },
+ {
+ fieldName: 'planDate',
+ label: '璁″垝鏃ユ湡',
+ component: 'RangePicker',
+ componentProps: getRangePickerDefaultProps(),
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勮〃鏍煎垪 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 50 },
+ { type: 'seq', title: '搴忓彿', width: 60 },
+ {
+ field: 'no',
+ title: '璁″垝鍗曞彿',
+ minWidth: 140,
+ },
+ {
+ field: 'name',
+ title: '璁″垝鍚嶇О',
+ minWidth: 180,
+ },
+ {
+ field: 'demandSourceName',
+ title: '闇�姹傛潵婧�',
+ width: 110,
+ },
+ {
+ field: 'planUserName',
+ title: '缂栧埗浜�',
+ width: 110,
+ },
+ {
+ field: 'planDeptName',
+ title: '缂栧埗閮ㄩ棬',
+ width: 120,
+ },
+ {
+ field: 'supplierName',
+ title: '渚涘簲鍟�',
+ minWidth: 130,
+ },
+ {
+ field: 'productNames',
+ title: '浜у搧',
+ minWidth: 180,
+ },
+ {
+ field: 'totalCount',
+ title: '鍚堣鏁伴噺',
+ width: 100,
+ formatter: 'formatAmount3',
+ },
+ {
+ field: 'planDate',
+ title: '璁″垝鏃ユ湡',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 100,
+ slots: { default: 'status' },
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'action',
+ title: '鎿嶄綔',
+ width: 280,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/erp/purchase/plan/index.vue b/src/views/erp/purchase/plan/index.vue
new file mode 100644
index 0000000..671a09f
--- /dev/null
+++ b/src/views/erp/purchase/plan/index.vue
@@ -0,0 +1,300 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ErpPurchasePlanApi } from '#/api/erp/purchase/plan';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { confirm, Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deletePurchasePlan,
+ exportPurchasePlan,
+ generatePlanFromStockAlert,
+ generatePurchaseRequests,
+ getApproveProcessList,
+ getPurchasePlanPage,
+} from '#/api/erp/purchase/plan';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+import ProcessSelectModal from './modules/process-select-modal.vue';
+
+/** ERP 閲囪喘璁″垝鍒楄〃 */
+defineOptions({ name: 'ErpPurchasePlan' });
+
+const router = useRouter();
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+ connectedComponent: ProcessSelectModal,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+ const data = await exportPurchasePlan(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '閲囪喘璁″垝.xls', source: data });
+}
+
+/** 鏂板閲囪喘璁″垝 */
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 缂栬緫閲囪喘璁″垝 */
+function handleEdit(row: ErpPurchasePlanApi.PurchasePlan) {
+ formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+/** 鍒犻櫎閲囪喘璁″垝 */
+async function handleDelete(ids: number[]) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting'),
+ duration: 0,
+ });
+ try {
+ await deletePurchasePlan(ids);
+ message.success($t('ui.actionMessage.deleteSuccess'));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 搴撳瓨棰勮鐢熸垚閲囪喘璁″垝 */
+async function handleGenerateFromStockAlert() {
+ const hideLoading = message.loading({
+ content: '姝e湪鏍规嵁搴撳瓨棰勮鐢熸垚閲囪喘璁″垝...',
+ duration: 0,
+ });
+ try {
+ const planId = await generatePlanFromStockAlert({});
+ hideLoading();
+ message.success(`宸茬敓鎴愰噰璐鍒掞紙缂栧彿 ${planId}锛夛紝璇锋牳瀵规槑缁嗗悗鎻愪氦瀹℃壒`);
+ handleRefresh();
+ } catch {
+ hideLoading();
+ }
+}
+
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛� */
+async function handleSubmit(row: ErpPurchasePlanApi.PurchasePlan) {
+ const hideLoading = message.loading({
+ content: '鑾峰彇瀹℃壒娴佺▼...',
+ duration: 0,
+ });
+ try {
+ const processList = await getApproveProcessList(row.id!);
+ hideLoading();
+ if (!processList || processList.length === 0) {
+ message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堥厤缃� BPM 娴佺▼');
+ return;
+ }
+ processModalApi.setData({ id: row.id, processList }).open();
+ } catch {
+ hideLoading();
+ }
+}
+
+/** 鐢熸垚閲囪喘鐢宠 */
+function handleGenerateRequests(row: ErpPurchasePlanApi.PurchasePlan) {
+ confirm({
+ content: `纭鏍规嵁璁″垝銆�${row.name}銆嶇敓鎴愰噰璐敵璇凤紵`,
+ }).then(() => {
+ const hideLoading = message.loading({
+ content: '姝e湪鐢熸垚閲囪喘鐢宠...',
+ duration: 0,
+ });
+ generatePurchaseRequests(row.id!)
+ .then((requestIds) => {
+ message.success(`宸茬敓鎴愰噰璐敵璇凤紙缂栧彿 ${requestIds.join(', ')}锛塦);
+ handleRefresh();
+ })
+ .finally(() => {
+ hideLoading();
+ });
+ });
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+ records,
+}: {
+ records: ErpPurchasePlanApi.PurchasePlan[];
+}) {
+ checkedIds.value = records.map((item) => item.id!);
+}
+
+/** 鏌ョ湅璇︽儏 */
+function handleDetail(row: ErpPurchasePlanApi.PurchasePlan) {
+ formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleViewProcess(row: ErpPurchasePlanApi.PurchasePlan) {
+ if (!row.processInstanceId) {
+ message.warning('璇ヨ鍒掑皻鏈彁浜ゅ鎵�');
+ return;
+ }
+ router.push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getPurchasePlanPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<ErpPurchasePlanApi.PurchasePlan>,
+ gridEvents: {
+ checkboxAll: handleRowCheckboxChange,
+ checkboxChange: handleRowCheckboxChange,
+ },
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <ProcessModal @success="handleRefresh" />
+ <Grid table-title="閲囪喘璁″垝鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['閲囪喘璁″垝']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['erp:purchase-plan:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: '搴撳瓨棰勮鐢熸垚',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['erp:purchase-plan:create'],
+ popConfirm: {
+ title: '鏍规嵁瀹夊叏搴撳瓨缂洪噺鑷姩鐢熸垚鑽夌閲囪喘璁″垝锛�',
+ confirm: handleGenerateFromStockAlert,
+ },
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['erp:purchase-plan:export'],
+ onClick: handleExport,
+ },
+ {
+ label: '鎵归噺鍒犻櫎',
+ type: 'primary',
+ danger: true,
+ disabled: isEmpty(checkedIds),
+ icon: ACTION_ICON.DELETE,
+ auth: ['erp:purchase-plan:delete'],
+ popConfirm: {
+ title: `鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛焋,
+ confirm: handleDelete.bind(null, checkedIds),
+ },
+ },
+ ]"
+ />
+ </template>
+ <template #status="{ row }">
+ <Tag v-if="row.status === 0" color="default">鏈彁浜�</Tag>
+ <Tag v-else-if="row.status === 10" color="processing">瀹℃壒涓�</Tag>
+ <Tag v-else-if="row.status === 20" color="success">瀹℃牳閫氳繃</Tag>
+ <Tag v-else-if="row.status === 30" color="error">瀹℃牳涓嶉�氳繃</Tag>
+ <Tag v-else-if="row.status === 40" color="warning">宸插彇娑�</Tag>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.detail'),
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['erp:purchase-plan:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['erp:purchase-plan:update'],
+ ifShow: () => row.status === 0,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '鎻愪氦',
+ type: 'link',
+ ifShow: () => row.status === 0 || row.status === 30,
+ onClick: handleSubmit.bind(null, row),
+ },
+ {
+ label: '鐢熸垚鐢宠',
+ type: 'link',
+ auth: ['erp:purchase-plan:generate-request'],
+ ifShow: () => row.status === 20 && !row.generatedFlag,
+ onClick: handleGenerateRequests.bind(null, row),
+ },
+ {
+ label: '鏌ョ湅瀹℃壒',
+ type: 'link',
+ ifShow: () => row.status !== 0,
+ onClick: handleViewProcess.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['erp:purchase-plan:delete'],
+ ifShow: () => row.status === 0,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.no]),
+ confirm: handleDelete.bind(null, [row.id!]),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/erp/purchase/plan/modules/form.vue b/src/views/erp/purchase/plan/modules/form.vue
new file mode 100644
index 0000000..8ad6dc6
--- /dev/null
+++ b/src/views/erp/purchase/plan/modules/form.vue
@@ -0,0 +1,236 @@
+<script lang="ts" setup>
+import type { ErpPurchasePlanApi } from '#/api/erp/purchase/plan';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Button, DatePicker, Input, InputNumber, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createPurchasePlan,
+ getPurchasePlan,
+ updatePurchasePlan,
+} from '#/api/erp/purchase/plan';
+import { MdmItemSelect } from '#/views/basicData/mdm/components';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<ErpPurchasePlanApi.PurchasePlan>();
+const formType = ref<FormType>('create');
+const items = ref<ErpPurchasePlanApi.PurchasePlanItem[]>([]);
+
+const getTitle = computed(() => {
+ if (formType.value === 'create') {
+ return $t('ui.actionTitle.create', ['閲囪喘璁″垝']);
+ } else if (formType.value === 'edit') {
+ return $t('ui.actionTitle.edit', ['閲囪喘璁″垝']);
+ } else {
+ return '閲囪喘璁″垝璇︽儏';
+ }
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 120,
+ },
+ wrapperClass: 'grid-cols-3',
+ layout: 'vertical',
+ schema: useFormSchema(formType.value),
+ showDefaultActions: false,
+});
+
+function handleAddItem() {
+ items.value.push({
+ productId: undefined as unknown as number,
+ productName: '',
+ productUnitId: undefined as unknown as number,
+ productUnitName: '',
+ count: 1,
+ demandTime: undefined,
+ remark: '',
+ });
+}
+
+function handleRemoveItem(index: number) {
+ items.value.splice(index, 1);
+}
+
+function handleItemSelect(index: number, item: any) {
+ const row = items.value[index];
+ if (!row) {
+ return;
+ }
+ row.productId = item?.id;
+ row.productName = item?.name;
+ row.productUnitId = item?.unitMeasureId;
+ row.productUnitName = item?.unitMeasureName;
+}
+
+/** 鍒涘缓鎴栨洿鏂伴噰璐鍒� */
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ if (!items.value.length) {
+ message.error('璇疯嚦灏戞坊鍔犱竴鏉¤鍒掓槑缁�');
+ return;
+ }
+ if (items.value.some((item) => !item.productId || !item.count)) {
+ message.error('璇峰畬鍠勮鍒掓槑缁嗙殑浜у搧涓庢暟閲�');
+ return;
+ }
+ modalApi.lock();
+ const data =
+ (await formApi.getValues()) as ErpPurchasePlanApi.PurchasePlan;
+ data.items = items.value.map((item) => ({
+ productId: item.productId,
+ productUnitId: item.productUnitId,
+ count: item.count,
+ demandTime: item.demandTime,
+ remark: item.remark,
+ }));
+ try {
+ await (formType.value === 'create'
+ ? createPurchasePlan(data)
+ : updatePurchasePlan(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ items.value = [];
+ return;
+ }
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ formApi.setDisabled(formType.value === 'detail');
+ formApi.updateSchema(useFormSchema(formType.value));
+ if (!data || !data.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getPurchasePlan(data.id);
+ await formApi.setValues(formData.value);
+ items.value = (formData.value.items ?? []).map((item) => ({
+ ...item,
+ }));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal
+ :title="getTitle"
+ class="w-3/4"
+ :show-confirm-button="formType !== 'detail'"
+ >
+ <Form class="mx-3">
+ <template #items>
+ <div class="mt-2">
+ <div v-if="formType !== 'detail'" class="mb-2">
+ <Button type="dashed" @click="handleAddItem">+ 娣诲姞鏄庣粏</Button>
+ </div>
+ <table class="w-full border-collapse border border-gray-200 text-sm">
+ <thead>
+ <tr class="bg-gray-100">
+ <th class="border border-gray-200 px-2 py-1 w-12">搴忓彿</th>
+ <th class="border border-gray-200 px-2 py-1">浜у搧</th>
+ <th class="border border-gray-200 px-2 py-1 w-20">鍗曚綅</th>
+ <th class="border border-gray-200 px-2 py-1 w-32">鏁伴噺</th>
+ <th class="border border-gray-200 px-2 py-1 w-40">闇�姹傛棩鏈�</th>
+ <th class="border border-gray-200 px-2 py-1">澶囨敞</th>
+ <th
+ v-if="formType !== 'detail'"
+ class="border border-gray-200 px-2 py-1 w-16"
+ >
+ 鎿嶄綔
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(item, index) in items" :key="index">
+ <td class="border border-gray-200 px-2 py-1 text-center">
+ {{ index + 1 }}
+ </td>
+ <td class="border border-gray-200 px-2 py-1">
+ <MdmItemSelect
+ v-if="formType !== 'detail'"
+ :model-value="item.productId"
+ placeholder="璇烽�夋嫨浜у搧"
+ @change="handleItemSelect(index, $event)"
+ />
+ <span v-else>{{ item.productName }}</span>
+ </td>
+ <td class="border border-gray-200 px-2 py-1 text-center">
+ {{ item.productUnitName || '-' }}
+ </td>
+ <td class="border border-gray-200 px-2 py-1">
+ <InputNumber
+ v-if="formType !== 'detail'"
+ v-model:value="item.count"
+ class="!w-full"
+ :min="0.01"
+ :precision="2"
+ />
+ <span v-else>{{ item.count }}</span>
+ </td>
+ <td class="border border-gray-200 px-2 py-1">
+ <DatePicker
+ v-if="formType !== 'detail'"
+ v-model:value="item.demandTime"
+ class="!w-full"
+ value-format="YYYY-MM-DD"
+ />
+ <span v-else>{{ item.demandTime || '-' }}</span>
+ </td>
+ <td class="border border-gray-200 px-2 py-1">
+ <Input
+ v-if="formType !== 'detail'"
+ v-model:value="item.remark"
+ placeholder="澶囨敞"
+ />
+ <span v-else>{{ item.remark || '-' }}</span>
+ </td>
+ <td
+ v-if="formType !== 'detail'"
+ class="border border-gray-200 px-2 py-1 text-center"
+ >
+ <Button type="link" danger @click="handleRemoveItem(index)">
+ 鍒犻櫎
+ </Button>
+ </td>
+ </tr>
+ <tr v-if="!items.length">
+ <td
+ :colspan="formType === 'detail' ? 6 : 7"
+ class="border border-gray-200 px-2 py-4 text-center text-gray-400"
+ >
+ 鏆傛棤鏄庣粏锛岃娣诲姞
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </template>
+ </Form>
+ </Modal>
+</template>
diff --git a/src/views/erp/purchase/plan/modules/process-select-modal.vue b/src/views/erp/purchase/plan/modules/process-select-modal.vue
new file mode 100644
index 0000000..dd7a2a8
--- /dev/null
+++ b/src/views/erp/purchase/plan/modules/process-select-modal.vue
@@ -0,0 +1,79 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Select } from 'ant-design-vue';
+
+import { submitPurchasePlan } from '#/api/erp/purchase/plan';
+
+interface ProcessDefinition {
+ id: string;
+ name: string;
+ version: number;
+ key: string;
+}
+
+const emit = defineEmits(['success']);
+
+const planId = ref<number>();
+const processList = ref<ProcessDefinition[]>([]);
+const selectedProcessKey = ref<string>('');
+
+const processOptions = computed(() => {
+ return processList.value.map((p) => ({
+ label: `${p.name} (V${p.version})`,
+ value: p.key,
+ }));
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (!selectedProcessKey.value) {
+ message.warning('璇烽�夋嫨瀹℃壒娴佺▼');
+ return;
+ }
+ modalApi.lock();
+ try {
+ await submitPurchasePlan(planId.value!, selectedProcessKey.value);
+ await modalApi.close();
+ emit('success');
+ message.success('鎻愪氦鎴愬姛');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ planId.value = undefined;
+ selectedProcessKey.value = '';
+ processList.value = [];
+ return;
+ }
+ const data = modalApi.getData<{ id: number; processList: ProcessDefinition[] }>();
+ if (!data) {
+ return;
+ }
+ planId.value = data.id;
+ processList.value = data.processList || [];
+ // 榛樿閫夋嫨绗竴涓祦绋�
+ if (processList.value.length > 0) {
+ selectedProcessKey.value = processList.value[0]?.key ?? '';
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal title="閫夋嫨瀹℃壒娴佺▼" class="w-1/3">
+ <div class="mb-4">
+ <span class="mr-2">瀹℃壒娴佺▼锛�</span>
+ <Select
+ v-model:value="selectedProcessKey"
+ :options="processOptions"
+ style="width: 200px"
+ placeholder="璇烽�夋嫨瀹℃壒娴佺▼"
+ />
+ </div>
+ </Modal>
+</template>
diff --git a/src/views/hrm/certificate/data.ts b/src/views/hrm/certificate/data.ts
new file mode 100644
index 0000000..be46e17
--- /dev/null
+++ b/src/views/hrm/certificate/data.ts
@@ -0,0 +1,197 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { HrmEmployeeSelect } from '#/views/hrm/employee/components';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+ const isReadonly = formType === 'detail';
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'employeeId',
+ label: '鍛樺伐',
+ component: markRaw(HrmEmployeeSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨鍛樺伐',
+ disabled: isReadonly,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'certType',
+ label: '璇佷功绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.HRM_CERTIFICATE_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨璇佷功绫诲瀷',
+ disabled: isReadonly,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'certName',
+ label: '璇佷功鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ瘉涔﹀悕绉�',
+ disabled: isReadonly,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'certNo',
+ label: '璇佷功缂栧彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ瘉涔︾紪鍙�',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'issueOrg',
+ label: '鍙戣瘉鏈烘瀯',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ彂璇佹満鏋�',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'issueDate',
+ label: '鍙戣瘉鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '閫夋嫨鍙戣瘉鏃ユ湡',
+ valueFormat: 'YYYY-MM-DD',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'expireDate',
+ label: '鏈夋晥鏈熻嚦',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '閫夋嫨鏈夋晥鏈熻嚦',
+ valueFormat: 'YYYY-MM-DD',
+ disabled: isReadonly,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: isReadonly,
+ },
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'employeeId',
+ label: '鍛樺伐',
+ component: markRaw(HrmEmployeeSelect),
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇烽�夋嫨鍛樺伐',
+ },
+ },
+ {
+ fieldName: 'certType',
+ label: '璇佷功绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.HRM_CERTIFICATE_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨璇佷功绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'certName',
+ label: '璇佷功鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ瘉涔﹀悕绉�',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勮〃鏍煎垪 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 50 },
+ { type: 'seq', title: '搴忓彿', width: 60 },
+ {
+ field: 'employeeName',
+ title: '鍛樺伐',
+ minWidth: 120,
+ },
+ {
+ field: 'certType',
+ title: '璇佷功绫诲瀷',
+ width: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.HRM_CERTIFICATE_TYPE },
+ },
+ },
+ {
+ field: 'certName',
+ title: '璇佷功鍚嶇О',
+ minWidth: 200,
+ },
+ {
+ field: 'certNo',
+ title: '璇佷功缂栧彿',
+ minWidth: 130,
+ },
+ {
+ field: 'issueOrg',
+ title: '鍙戣瘉鏈烘瀯',
+ minWidth: 140,
+ },
+ {
+ field: 'expireDate',
+ title: '鏈夋晥鏈熻嚦',
+ width: 120,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'expireStatus',
+ title: '鍒版湡鐘舵��',
+ width: 100,
+ slots: { default: 'expireStatus' },
+ },
+ {
+ field: 'action',
+ title: '鎿嶄綔',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/hrm/certificate/index.vue b/src/views/hrm/certificate/index.vue
new file mode 100644
index 0000000..63953bd
--- /dev/null
+++ b/src/views/hrm/certificate/index.vue
@@ -0,0 +1,169 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmCertificateApi } from '#/api/hrm/certificate';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { deleteCertificate, getCertificatePage } from '#/api/hrm/certificate';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** HRM 璇佷功璧勮川鍒楄〃 */
+defineOptions({ name: 'HrmCertificate' });
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: HrmCertificateApi.Certificate) {
+ formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+function handleDetail(row: HrmCertificateApi.Certificate) {
+ formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+async function handleDelete(ids: number[]) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting'),
+ duration: 0,
+ });
+ try {
+ await deleteCertificate(ids);
+ message.success($t('ui.actionMessage.deleteSuccess'));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+ records,
+}: {
+ records: HrmCertificateApi.Certificate[];
+}) {
+ checkedIds.value = records.map((item) => item.id!);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getCertificatePage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<HrmCertificateApi.Certificate>,
+ gridEvents: {
+ checkboxAll: handleRowCheckboxChange,
+ checkboxChange: handleRowCheckboxChange,
+ },
+});
+</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: ['hrm:certificate:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: '鎵归噺鍒犻櫎',
+ type: 'primary',
+ danger: true,
+ disabled: isEmpty(checkedIds),
+ icon: ACTION_ICON.DELETE,
+ auth: ['hrm:certificate:delete'],
+ popConfirm: {
+ title: `鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛焋,
+ confirm: handleDelete.bind(null, checkedIds),
+ },
+ },
+ ]"
+ />
+ </template>
+ <template #expireStatus="{ row }">
+ <Tag v-if="row.expireStatus === 0" color="success">鏈夋晥</Tag>
+ <Tag v-else-if="row.expireStatus === 1" color="warning">鍗冲皢鍒版湡</Tag>
+ <Tag v-else-if="row.expireStatus === 2" color="error">宸茶繃鏈�</Tag>
+ <Tag v-else color="default">-</Tag>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.detail'),
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['hrm:certificate:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['hrm:certificate:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['hrm:certificate:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.certName]),
+ confirm: handleDelete.bind(null, [row.id!]),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/hrm/certificate/modules/form.vue b/src/views/hrm/certificate/modules/form.vue
new file mode 100644
index 0000000..471833d
--- /dev/null
+++ b/src/views/hrm/certificate/modules/form.vue
@@ -0,0 +1,91 @@
+<script lang="ts" setup>
+import type { HrmCertificateApi } from '#/api/hrm/certificate';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createCertificate, getCertificate, updateCertificate } from '#/api/hrm/certificate';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<HrmCertificateApi.Certificate>();
+const formType = ref<FormType>('create');
+
+const getTitle = computed(() => {
+ if (formType.value === 'create') {
+ return $t('ui.actionTitle.create', ['璇佷功璧勮川']);
+ } else if (formType.value === 'edit') {
+ return $t('ui.actionTitle.edit', ['璇佷功璧勮川']);
+ } else {
+ return '璇佷功璧勮川璇︽儏';
+ }
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 120,
+ },
+ wrapperClass: 'grid-cols-3',
+ layout: 'vertical',
+ schema: useFormSchema(formType.value),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data =
+ (await formApi.getValues()) as HrmCertificateApi.Certificate;
+ try {
+ await (formType.value === 'create'
+ ? createCertificate(data)
+ : updateCertificate(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } 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.setDisabled(formType.value === 'detail');
+ formApi.updateSchema(useFormSchema(formType.value));
+ if (!data || !data.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getCertificate(data.id);
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-2/3" :show-confirm-button="formType !== 'detail'">
+ <Form class="mx-3" />
+ </Modal>
+</template>
diff --git a/src/views/mes/dv/machinery/data.ts b/src/views/mes/dv/machinery/data.ts
index 8b39782..ddacc2f 100644
--- a/src/views/mes/dv/machinery/data.ts
+++ b/src/views/mes/dv/machinery/data.ts
@@ -76,6 +76,15 @@
rules: 'required',
},
{
+ fieldName: 'tbDeviceId',
+ label: '鏁伴噰璁惧ID',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ暟閲囪澶嘔D锛堝閮ㄦ暟閲� tbDeviceId锛�',
+ },
+ help: '涓庢暟閲囩郴缁熻澶嘔D瀵瑰簲锛岀敤浜庡叧鑱旇澶囧疄鏃舵暟鎹�',
+ },
+ {
fieldName: 'brand',
label: '鍝佺墝',
component: 'Input',
diff --git a/src/views/mes/dv/machinery/modules/form.vue b/src/views/mes/dv/machinery/modules/form.vue
index a11e275..365f935 100644
--- a/src/views/mes/dv/machinery/modules/form.vue
+++ b/src/views/mes/dv/machinery/modules/form.vue
@@ -23,6 +23,7 @@
import CheckRecordList from "./check-record-list.vue";
import MaintenRecordList from "./mainten-record-list.vue";
import RepairList from "./repair-list.vue";
+ import TelemetryList from "./telemetry-list.vue";
const emit = defineEmits(["success"]);
const formType = ref<FormType>("create"); // 琛ㄥ崟妯″紡
@@ -133,6 +134,11 @@
tab="缁翠慨璁板綍">
<RepairList :machinery-id="formData.id" />
</Tabs.TabPane>
+ <Tabs.TabPane key="telemetry"
+ tab="鏁伴噰鏁版嵁"
+ :disabled="!formData.tbDeviceId">
+ <TelemetryList :tb-device-id="formData.tbDeviceId" />
+ </Tabs.TabPane>
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center">
diff --git a/src/views/mes/dv/statistics/summary.vue b/src/views/mes/dv/statistics/summary.vue
new file mode 100644
index 0000000..8f3e3cf
--- /dev/null
+++ b/src/views/mes/dv/statistics/summary.vue
@@ -0,0 +1,109 @@
+<script lang="ts" setup>
+import type { MesDvStatisticsApi } from '#/api/mes/dv/statistics';
+
+import { onMounted, ref } from 'vue';
+
+import { Page } from '@vben/common-ui';
+
+import { Card, Col, Row, Table, Tag } from 'ant-design-vue';
+
+import { getDvStatisticsSummary } from '#/api/mes/dv/statistics';
+
+defineOptions({ name: 'MesDvStatistics' });
+
+const loading = ref(false);
+const summary = ref<MesDvStatisticsApi.StatisticsSummary>({});
+
+async function loadSummary() {
+ loading.value = true;
+ try {
+ summary.value = await getDvStatisticsSummary();
+ } finally {
+ loading.value = false;
+ }
+}
+
+onMounted(loadSummary);
+</script>
+
+<template>
+ <Page>
+ <Row :gutter="16" class="mb-4">
+ <Col :span="4">
+ <Card :loading="loading">
+ <div class="text-sm text-gray-500">璁惧鎬绘暟</div>
+ <div class="mt-2 text-2xl font-bold">
+ {{ summary.totalMachinery ?? 0 }}
+ </div>
+ </Card>
+ </Col>
+ <Col :span="5">
+ <Card :loading="loading">
+ <div class="text-sm text-gray-500">鐢熶骇涓� / 鍋滄満 / 缁存姢涓�</div>
+ <div class="mt-2 text-2xl font-bold">
+ <span class="text-green-600">{{ summary.producingCount ?? 0 }}</span>
+ <span class="mx-1 text-gray-400">/</span>
+ <span class="text-red-600">{{ summary.stoppedCount ?? 0 }}</span>
+ <span class="mx-1 text-gray-400">/</span>
+ <span class="text-orange-600">{{ summary.maintainingCount ?? 0 }}</span>
+ </div>
+ </Card>
+ </Col>
+ <Col :span="5">
+ <Card :loading="loading">
+ <div class="text-sm text-gray-500">璁惧鏁呴殰鐜�</div>
+ <div class="mt-2 text-2xl font-bold text-red-600">
+ {{ (summary.faultRate ?? 0).toFixed(1) }}%
+ </div>
+ </Card>
+ </Col>
+ <Col :span="5">
+ <Card :loading="loading">
+ <div class="text-sm text-gray-500">缁翠慨鍗曪紙鎬绘暟 / 寰呭鐞嗭級</div>
+ <div class="mt-2 text-2xl font-bold">
+ {{ summary.totalRepair ?? 0 }}
+ <span class="text-sm text-gray-400">
+ / {{ summary.processingRepair ?? 0 }}
+ </span>
+ </div>
+ </Card>
+ </Col>
+ <Col :span="5">
+ <Card :loading="loading">
+ <div class="text-sm text-gray-500">淇濆吇璁板綍锛堟�绘暟 / 宸插畬鎴愶級</div>
+ <div class="mt-2 text-2xl font-bold">
+ {{ summary.totalMainten ?? 0 }}
+ <span class="text-sm text-gray-400">
+ / {{ summary.finishedMainten ?? 0 }}
+ </span>
+ </div>
+ </Card>
+ </Col>
+ </Row>
+
+ <Card title="鎸夎澶囩被鍨嬪垎甯�" :loading="loading">
+ <Table
+ row-key="machineryTypeId"
+ :data-source="summary.typeDistribution ?? []"
+ :pagination="false"
+ size="middle"
+ >
+ <Table.Column title="璁惧绫诲瀷" data-index="machineryTypeName">
+ <template #default="{ record }">
+ <Tag color="blue">{{ record.machineryTypeName || '鏈垎绫�' }}</Tag>
+ </template>
+ </Table.Column>
+ <Table.Column title="璁惧鏁伴噺" data-index="count" align="right" />
+ <Table.Column title="鍗犳瘮" align="right">
+ <template #default="{ record }">
+ {{
+ summary.totalMachinery
+ ? ((record.count / summary.totalMachinery) * 100).toFixed(1)
+ : 0
+ }}%
+ </template>
+ </Table.Column>
+ </Table>
+ </Card>
+ </Page>
+</template>
diff --git a/src/views/mes/md/item/data.ts b/src/views/mes/md/item/data.ts
index 5d74bb2..6893c75 100644
--- a/src/views/mes/md/item/data.ts
+++ b/src/views/mes/md/item/data.ts
@@ -178,6 +178,15 @@
rules: z.number().default(0),
},
{
+ fieldName: 'materialCategory',
+ label: '鐗╄祫鍒嗙被',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.MES_MATERIAL_CATEGORY, 'number'),
+ placeholder: '璇烽�夋嫨鐗╄祫鍒嗙被',
+ },
+ },
+ {
fieldName: 'remark',
label: '澶囨敞',
component: 'Textarea',
@@ -298,6 +307,16 @@
},
},
{
+ field: 'materialCategory',
+ title: '鐗╄祫鍒嗙被',
+ width: 120,
+ align: 'center',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_MATERIAL_CATEGORY },
+ },
+ },
+ {
field: 'safeStockFlag',
title: '瀹夊叏搴撳瓨',
width: 120,
diff --git a/src/views/mes/pd/ctc-reel/data.ts b/src/views/mes/pd/ctc-reel/data.ts
new file mode 100644
index 0000000..cc8ef48
--- /dev/null
+++ b/src/views/mes/pd/ctc-reel/data.ts
@@ -0,0 +1,206 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdCtcReelApi } from '#/api/mes/pd/ctc-reel';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { PdCtcSelect } from '#/views/mes/pd/ctc/components';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼鐩樺叿璁$畻璁板綍 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'ctcId',
+ label: '鎹綅瀵肩嚎浜у搧',
+ component: markRaw(PdCtcSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨鎹綅瀵肩嚎浜у搧',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'reelType',
+ label: '鐩樺叿绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.PRODUCT_REEL_TYPE, 'string'),
+ placeholder: '璇烽�夋嫨鐩樺叿绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'reelOuterDiameter',
+ label: '鐩樺叿澶栧緞(mm)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 0,
+ placeholder: '璇疯緭鍏ョ洏鍏峰寰� d1',
+ },
+ },
+ {
+ fieldName: 'reelInnerDiameter',
+ label: '鐩樿姱鍐呭緞(mm)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 0,
+ placeholder: '璇疯緭鍏ョ洏鑺唴寰� d2',
+ },
+ },
+ {
+ fieldName: 'reelWidth',
+ label: '鐩樺(mm)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 0,
+ placeholder: '璇疯緭鍏ョ洏瀹� l2',
+ },
+ },
+ {
+ fieldName: 'maxCapacity',
+ label: '鏈�澶у閲�(km)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 4,
+ placeholder: '璇疯緭鍏ユ渶澶у閲�',
+ },
+ },
+ {
+ fieldName: 'recommendedLength',
+ label: '鎺ㄨ崘姣忕洏闀垮害(km)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ユ帹鑽愭瘡鐩橀暱搴�',
+ },
+ },
+ {
+ fieldName: 'netWeight',
+ label: '鍑�閲�(kg)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ュ噣閲�',
+ },
+ },
+ {
+ fieldName: 'grossWeight',
+ label: '姣涢噸(kg)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ユ瘺閲�',
+ },
+ },
+ {
+ fieldName: 'calcFormula',
+ label: '璁$畻鍏紡',
+ component: 'Textarea',
+ formItemClass: 'col-span-3',
+ componentProps: {
+ placeholder: '((d1-60)+(d2+10))脳INT(l2/(瀹�+1))脳(d1-d2-70)/(楂�+0.2)/1274',
+ rows: 2,
+ },
+ help: '瀹归噺璁$畻锛�((d1-60)+(d2+10))脳INT(l2/(CTC瀹�+1))脳(d1-d2-70)/(CTC楂�+0.2)/1274',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-3',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'ctcId',
+ label: '鎹綅瀵肩嚎浜у搧',
+ component: markRaw(PdCtcSelect),
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇烽�夋嫨鎹綅瀵肩嚎浜у搧',
+ },
+ },
+ {
+ fieldName: 'reelType',
+ label: '鐩樺叿绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.PRODUCT_REEL_TYPE, 'string'),
+ placeholder: '璇烽�夋嫨鐩樺叿绫诲瀷',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesPdCtcReelApi.CtcReel>['columns'] {
+ return [
+ { field: 'ctcOrderSpec', title: '鎹綅瀵肩嚎瑙勬牸', minWidth: 160 },
+ {
+ field: 'reelType',
+ title: '鐩樺叿绫诲瀷',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PRODUCT_REEL_TYPE },
+ },
+ },
+ { field: 'reelOuterDiameter', title: '鐩樺叿澶栧緞(mm)', width: 110 },
+ { field: 'reelInnerDiameter', title: '鐩樿姱鍐呭緞(mm)', width: 110 },
+ { field: 'reelWidth', title: '鐩樺(mm)', width: 100 },
+ { field: 'maxCapacity', title: '鏈�澶у閲�(km)', width: 120 },
+ { field: 'recommendedLength', title: '鎺ㄨ崘闀垮害(km)', width: 120 },
+ { field: 'netWeight', title: '鍑�閲�(kg)', width: 100 },
+ { field: 'grossWeight', title: '姣涢噸(kg)', width: 100 },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 220,
+ fixed: 'right',
+ slots: {
+ default: 'actions',
+ },
+ },
+ ];
+}
diff --git a/src/views/mes/pd/ctc-reel/index.vue b/src/views/mes/pd/ctc-reel/index.vue
new file mode 100644
index 0000000..8f6737a
--- /dev/null
+++ b/src/views/mes/pd/ctc-reel/index.vue
@@ -0,0 +1,150 @@
+<script lang="ts" setup>
+ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+ import type { MesPdCtcReelApi } from '#/api/mes/pd/ctc-reel';
+
+ import { ref } from 'vue';
+
+ import { Page, useVbenModal } from '@vben/common-ui';
+ import { $t } from '@vben/locales';
+
+ import { Button, message } from 'ant-design-vue';
+
+ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+ import {
+ deleteCtcReel,
+ getCtcReelPage,
+ } from '#/api/mes/pd/ctc-reel';
+
+ import { useGridColumns, useGridFormSchema } from './data';
+ import Form from './modules/form.vue';
+
+ const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+ });
+
+ /** 鍒锋柊琛ㄦ牸 */
+ function handleRefresh() {
+ gridApi.query();
+ }
+
+ /** 鏂板 */
+ function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+ }
+
+ /** 鏌ョ湅 */
+ function handleDetail(row: MesPdCtcReelApi.CtcReel) {
+ formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+ }
+
+ /** 缂栬緫 */
+ function handleEdit(row: MesPdCtcReelApi.CtcReel) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
+ }
+
+ /** 鍒犻櫎 */
+ async function handleDelete(row: MesPdCtcReelApi.CtcReel) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.ctcOrderSpec ?? '']),
+ duration: 0,
+ });
+ try {
+ await deleteCtcReel(row.id!);
+ message.success(
+ $t('ui.actionMessage.deleteSuccess', [row.ctcOrderSpec ?? '']),
+ );
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+ }
+
+ const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) =>
+ await getCtcReelPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesPdCtcReelApi.CtcReel>,
+ });
+</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:pd-ctc-reel:create'],
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </template>
+ <template #ctcOrderSpec="{ row }">
+ <Button type="link" @click="handleDetail(row)">
+ {{ row.ctcOrderSpec }}
+ </Button>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pd-ctc-reel:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pd-ctc-reel:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [
+ row.ctcOrderSpec ?? '',
+ ]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ {
+ label: $t('common.detail'),
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['mes:pd-ctc-reel:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/pd/ctc-reel/modules/form.vue b/src/views/mes/pd/ctc-reel/modules/form.vue
new file mode 100644
index 0000000..970f400
--- /dev/null
+++ b/src/views/mes/pd/ctc-reel/modules/form.vue
@@ -0,0 +1,126 @@
+<script lang="ts" setup>
+ import type { FormType } from '../data';
+
+ import type { MesPdCtcReelApi } from '#/api/mes/pd/ctc-reel';
+
+ import { computed, ref } from 'vue';
+
+ import { useVbenModal } from '@vben/common-ui';
+ import { $t } from '@vben/locales';
+
+ import { Button, message } from 'ant-design-vue';
+
+ import { useVbenForm } from '#/adapter/form';
+ import {
+ createCtcReel,
+ getCtcReel,
+ updateCtcReel,
+ } from '#/api/mes/pd/ctc-reel';
+ import { CalcModal } from '#/views/mes/pd/ctc/modules';
+
+ import { useFormSchema } from '../data';
+
+ const emit = defineEmits(['success']);
+ const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
+ const formData = ref<MesPdCtcReelApi.CtcReel>();
+ const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
+ const getTitle = computed(() => {
+ if (formType.value === 'detail') {
+ return '鏌ョ湅鐩樺叿璁$畻';
+ }
+ return formType.value === 'update' ? '淇敼鐩樺叿璁$畻' : '鏂板鐩樺叿璁$畻';
+ });
+
+ const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 130,
+ },
+ wrapperClass: 'grid-cols-3',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+ });
+
+ const [CalcModal, calcModalApi] = useVbenModal({
+ connectedComponent: CalcModal,
+ destroyOnClose: true,
+ });
+
+ /** 鎵撳紑瀹归噺璁$畻宸ュ叿 */
+ function handleOpenCalc() {
+ calcModalApi.open();
+ }
+
+ /** 鍥炲~鏈�澶у閲忥紙km锛� */
+ function handleFillReel(value: { capacityKm: number }) {
+ formApi.setFieldValue('maxCapacity', value.capacityKm);
+ }
+
+ const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (isDetail.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data = (await formApi.getValues()) as MesPdCtcReelApi.CtcReel;
+ try {
+ await (data.id ? updateCtcReel(data) : createCtcReel(data));
+ // 鍏抽棴骞舵彁绀�
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } 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() });
+ formApi.setDisabled(formType.value === 'detail');
+ modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getCtcReel(data.id);
+ // 璁剧疆鍒� values
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ });
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-4/5">
+ <Form class="mx-4" />
+ <template #prepend-footer>
+ <Button
+ v-if="!isDetail"
+ type="default"
+ @click="handleOpenCalc"
+ >
+ 瀹归噺璁$畻
+ </Button>
+ </template>
+ <CalcModal @reel="handleFillReel" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pd/ctc/components/index.ts b/src/views/mes/pd/ctc/components/index.ts
new file mode 100644
index 0000000..1462a8b
--- /dev/null
+++ b/src/views/mes/pd/ctc/components/index.ts
@@ -0,0 +1 @@
+export { default as PdCtcSelect } from './select.vue';
diff --git a/src/views/mes/pd/ctc/components/select.vue b/src/views/mes/pd/ctc/components/select.vue
new file mode 100644
index 0000000..7508b0e
--- /dev/null
+++ b/src/views/mes/pd/ctc/components/select.vue
@@ -0,0 +1,64 @@
+<script lang="ts" setup>
+ import { onMounted, ref } from 'vue';
+
+ import { Select } from 'ant-design-vue';
+
+ import { getCtcPage } from '#/api/mes/pd/ctc';
+
+ defineOptions({ name: 'PdCtcSelect', inheritAttrs: false });
+
+ const props = withDefaults(
+ defineProps<{
+ allowClear?: boolean;
+ disabled?: boolean;
+ modelValue?: number;
+ placeholder?: string;
+ }>(),
+ {
+ allowClear: true,
+ disabled: false,
+ modelValue: undefined,
+ placeholder: '璇烽�夋嫨鎹綅瀵肩嚎浜у搧',
+ },
+ );
+
+ const emit = defineEmits<{
+ 'update:modelValue': [value: number | undefined];
+ }>();
+
+ const options = ref<{ label: string; value: number }[]>([]);
+ const searchText = ref('');
+
+ async function load() {
+ const data = await getCtcPage({
+ pageNo: 1,
+ pageSize: 100,
+ orderSpec: searchText.value || undefined,
+ });
+ options.value = (data.list ?? []).map((item) => ({
+ label: `${item.orderSpec ?? ''}${item.itemName ? `锛�${item.itemName}锛塦 : ''}`,
+ value: item.id!,
+ }));
+ }
+
+ onMounted(load);
+
+ function handleSearch(val: string) {
+ searchText.value = val;
+ load();
+ }
+</script>
+
+<template>
+ <Select
+ :model-value="modelValue"
+ :options="options"
+ :disabled="disabled"
+ :placeholder="placeholder"
+ :allow-clear="allowClear"
+ show-search
+ :filter-option="false"
+ @update:value="(v: number | undefined) => emit('update:modelValue', v)"
+ @search="handleSearch"
+ />
+</template>
diff --git a/src/views/mes/pd/ctc/data.ts b/src/views/mes/pd/ctc/data.ts
new file mode 100644
index 0000000..324297c
--- /dev/null
+++ b/src/views/mes/pd/ctc/data.ts
@@ -0,0 +1,262 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdCtcApi } from '#/api/mes/pd/ctc';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { MdItemSelect } from '#/views/mes/md/item/components';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼鎹綅瀵肩嚎浜у搧鎶�鏈弬鏁� */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'itemId',
+ label: '鐗╂枡',
+ component: markRaw(MdItemSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨鎹綅瀵肩嚎鐗╂枡',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'orderSpec',
+ label: '璁㈣揣瑙勬牸',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ璐ц鏍�',
+ },
+ },
+ {
+ fieldName: 'bareConductorSpec',
+ label: '瑁稿绾胯鏍�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ8瀵肩嚎瑙勬牸',
+ },
+ },
+ {
+ fieldName: 'enameledSpec',
+ label: '婕嗗寘绾胯鏍�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ紗鍖呯嚎瑙勬牸',
+ },
+ },
+ {
+ fieldName: 'transpositionSpec',
+ label: '鎹綅瑙勬牸',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ崲浣嶈鏍�',
+ },
+ },
+ {
+ fieldName: 'conductorType',
+ label: '瀵间綋绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.PRODUCT_CONDUCTOR_TYPE, 'string'),
+ placeholder: '璇烽�夋嫨瀵间綋绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'crossSection',
+ label: '鎴潰绉�(mm虏)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ユ埅闈㈢Н',
+ },
+ },
+ {
+ fieldName: 'outerDiameter',
+ label: '澶栧緞(mm)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ュ寰�',
+ },
+ },
+ {
+ fieldName: 'resistance20c',
+ label: '20鈩冪洿娴佺數闃�(惟/km)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 6,
+ placeholder: '璇疯緭鍏�20鈩冪洿娴佺數闃�',
+ },
+ },
+ {
+ fieldName: 'wrappingAngle',
+ label: '缁曞寘瑙掑害(掳)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ョ粫鍖呰搴�',
+ },
+ },
+ {
+ fieldName: 'insulationThickness',
+ label: '缁濈紭鍘氬害(mm)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ョ粷缂樺帤搴�',
+ },
+ },
+ {
+ fieldName: 'ratedVoltage',
+ label: '棰濆畾鐢靛帇(kV)',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ラ瀹氱數鍘�',
+ },
+ },
+ {
+ fieldName: 'testVoltage',
+ label: '璇曢獙鐢靛帇(kV)',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ瘯楠岀數鍘�',
+ },
+ },
+ {
+ fieldName: 'minBendingRadius',
+ label: '鏈�灏忓集鏇插崐寰�(mm)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ユ渶灏忓集鏇插崐寰�',
+ },
+ },
+ {
+ fieldName: 'weightPerKm',
+ label: '姣忓叕閲岄噸閲�(kg/km)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ユ瘡鍏噷閲嶉噺',
+ },
+ },
+ {
+ fieldName: 'standardCode',
+ label: '鎵ц鏍囧噯',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ墽琛屾爣鍑嗕唬鍙�',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-3',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'itemId',
+ label: '鐗╂枡',
+ component: markRaw(MdItemSelect),
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇烽�夋嫨鎹綅瀵肩嚎鐗╂枡',
+ },
+ },
+ {
+ fieldName: 'orderSpec',
+ label: '璁㈣揣瑙勬牸',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ヨ璐ц鏍�',
+ },
+ },
+ {
+ fieldName: 'conductorType',
+ label: '瀵间綋绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.PRODUCT_CONDUCTOR_TYPE, 'string'),
+ placeholder: '璇烽�夋嫨瀵间綋绫诲瀷',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesPdCtcApi.Ctc>['columns'] {
+ return [
+ { field: 'itemName', title: '鐗╂枡鍚嶇О', minWidth: 140 },
+ { field: 'itemCode', title: '鐗╂枡缂栫爜', minWidth: 120 },
+ { field: 'orderSpec', title: '璁㈣揣瑙勬牸', minWidth: 140 },
+ { field: 'transpositionSpec', title: '鎹綅瑙勬牸', minWidth: 120 },
+ {
+ field: 'conductorType',
+ title: '瀵间綋绫诲瀷',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PRODUCT_CONDUCTOR_TYPE },
+ },
+ },
+ { field: 'crossSection', title: '鎴潰绉�(mm虏)', width: 110 },
+ { field: 'outerDiameter', title: '澶栧緞(mm)', width: 100 },
+ { field: 'resistance20c', title: '20鈩冪數闃�(惟/km)', width: 140 },
+ { field: 'wrappingAngle', title: '缁曞寘瑙掑害(掳)', width: 110 },
+ { field: 'ratedVoltage', title: '棰濆畾鐢靛帇(kV)', width: 110 },
+ { field: 'standardCode', title: '鎵ц鏍囧噯', minWidth: 140 },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 220,
+ fixed: 'right',
+ slots: {
+ default: 'actions',
+ },
+ },
+ ];
+}
diff --git a/src/views/mes/pd/ctc/index.vue b/src/views/mes/pd/ctc/index.vue
new file mode 100644
index 0000000..d1d500e
--- /dev/null
+++ b/src/views/mes/pd/ctc/index.vue
@@ -0,0 +1,161 @@
+<script lang="ts" setup>
+ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+ import type { MesPdCtcApi } from '#/api/mes/pd/ctc';
+
+ import { ref } from 'vue';
+
+ import { Page, useVbenModal } from '@vben/common-ui';
+ import { $t } from '@vben/locales';
+ import { downloadFileFromBlobPart } from '@vben/utils';
+
+ import { Button, message } from 'ant-design-vue';
+
+ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+ import {
+ deleteCtc,
+ exportCtc,
+ getCtcPage,
+ } from '#/api/mes/pd/ctc';
+
+ import { useGridColumns, useGridFormSchema } from './data';
+ import Form from './modules/form.vue';
+
+ const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+ });
+
+ /** 鍒锋柊琛ㄦ牸 */
+ function handleRefresh() {
+ gridApi.query();
+ }
+
+ /** 鏂板 */
+ function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+ }
+
+ /** 鏌ョ湅 */
+ function handleDetail(row: MesPdCtcApi.Ctc) {
+ formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+ }
+
+ /** 缂栬緫 */
+ function handleEdit(row: MesPdCtcApi.Ctc) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
+ }
+
+ /** 鍒犻櫎 */
+ async function handleDelete(row: MesPdCtcApi.Ctc) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.orderSpec ?? '']),
+ duration: 0,
+ });
+ try {
+ await deleteCtc(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.orderSpec ?? '']));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+ }
+
+ /** 瀵煎嚭 */
+ async function handleExport() {
+ const data = await exportCtc(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) =>
+ await getCtcPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesPdCtcApi.Ctc>,
+ });
+</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:pd-ctc:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:pd-ctc:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #orderSpec="{ row }">
+ <Button type="link" @click="handleDetail(row)">
+ {{ row.orderSpec || row.itemName }}
+ </Button>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pd-ctc:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pd-ctc:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.orderSpec ?? '']),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ {
+ label: $t('common.detail'),
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['mes:pd-ctc:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/pd/ctc/modules/calc-modal.vue b/src/views/mes/pd/ctc/modules/calc-modal.vue
new file mode 100644
index 0000000..30fb2ba
--- /dev/null
+++ b/src/views/mes/pd/ctc/modules/calc-modal.vue
@@ -0,0 +1,344 @@
+<script lang="ts" setup>
+ import { reactive, ref } from 'vue';
+
+ import { useVbenModal } from '@vben/common-ui';
+ import { $t } from '@vben/locales';
+
+ import {
+ Button,
+ Descriptions,
+ DescriptionsItem,
+ InputNumber,
+ message,
+ Select,
+ Tabs,
+ } from 'ant-design-vue';
+
+ import {
+ calcCtcAngle,
+ calcCtcResistance,
+ calcCtcReel,
+ } from '#/api/mes/pd/ctc';
+
+ const emit = defineEmits<{
+ angle: [value: { wrappingAngle: number }];
+ resistance: [value: { resistance20c: number }];
+ reel: [value: { capacityKm: number }];
+ }>();
+
+ /** 缁曞寘瑙掑害璁$畻琛ㄥ崟 */
+ const angleForm = reactive<{
+ conductorThickness?: number;
+ conductorWidth?: number;
+ cornerRadius?: number;
+ insulationThickness?: number;
+ tapeWidth?: number;
+ overlapWidth?: number;
+ }>({});
+ const angleResult = ref<{
+ circumference?: number;
+ wrappingAngle?: number;
+ wrappingPitch?: number;
+ }>();
+
+ /** 鐩存祦鐢甸樆璁$畻琛ㄥ崟 */
+ const resistanceForm = reactive<{
+ conductorThickness?: number;
+ conductorWidth?: number;
+ temperature?: number;
+ material?: string;
+ }>({ temperature: 75, material: 'copper' });
+ const resistanceResult = ref<{
+ crossSection?: number;
+ resistance20cKm?: number;
+ resistanceAtTemp?: number;
+ resistanceAtTempKm?: number;
+ }>();
+
+ /** 鐩樺叿瀹归噺璁$畻琛ㄥ崟 */
+ const reelForm = reactive<{
+ reelOuterDiameter?: number;
+ reelInnerDiameter?: number;
+ reelWidth?: number;
+ ctcThickness?: number;
+ ctcWidth?: number;
+ }>({});
+ const reelResult = ref<{
+ capacityMeter?: number;
+ capacityKm?: number;
+ }>();
+
+ /** 缁曞寘瑙掑害璁$畻 */
+ async function handleCalcAngle() {
+ const data = await calcCtcAngle(angleForm);
+ angleResult.value = data;
+ }
+
+ /** 鐩存祦鐢甸樆璁$畻 */
+ async function handleCalcResistance() {
+ const data = await calcCtcResistance(resistanceForm);
+ resistanceResult.value = data;
+ }
+
+ /** 鐩樺叿瀹归噺璁$畻 */
+ async function handleCalcReel() {
+ const data = await calcCtcReel(reelForm);
+ reelResult.value = data;
+ }
+
+ /** 鍥炲~缁曞寘瑙掑害 */
+ function handleFillAngle() {
+ if (angleResult.value?.wrappingAngle == null) {
+ message.warning('璇峰厛鎵ц缁曞寘瑙掑害璁$畻');
+ return;
+ }
+ emit('angle', { wrappingAngle: angleResult.value.wrappingAngle });
+ message.success($t('ui.actionMessage.operationSuccess'));
+ }
+
+ /** 鍥炲~鐩存祦鐢甸樆 */
+ function handleFillResistance() {
+ if (resistanceResult.value?.resistance20cKm == null) {
+ message.warning('璇峰厛鎵ц鐩存祦鐢甸樆璁$畻');
+ return;
+ }
+ emit('resistance', { resistance20c: resistanceResult.value.resistance20cKm });
+ message.success($t('ui.actionMessage.operationSuccess'));
+ }
+
+ /** 鍥炲~鐩樺叿瀹归噺 */
+ function handleFillReel() {
+ if (reelResult.value?.capacityKm == null) {
+ message.warning('璇峰厛鎵ц鐩樺叿瀹归噺璁$畻');
+ return;
+ }
+ emit('reel', { capacityKm: reelResult.value.capacityKm });
+ message.success($t('ui.actionMessage.operationSuccess'));
+ }
+
+ const [Modal, modalApi] = useVbenModal({
+ onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ angleResult.value = undefined;
+ resistanceResult.value = undefined;
+ reelResult.value = undefined;
+ }
+ },
+ });
+</script>
+
+<template>
+ <Modal title="鎹綅瀵肩嚎璁$畻宸ュ叿" class="w-2/3">
+ <Tabs>
+ <Tabs.TabPane key="angle" tab="缁曞寘瑙掑害">
+ <div class="grid grid-cols-3 gap-4">
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">瀵间綋鍘氬害(mm)</span>
+ <InputNumber
+ v-model:value="angleForm.conductorThickness"
+ class="!flex-1"
+ :min="0"
+ :precision="2"
+ placeholder="濡� 19.08"
+ />
+ </div>
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">瀵间綋瀹藉害(mm)</span>
+ <InputNumber
+ v-model:value="angleForm.conductorWidth"
+ class="!flex-1"
+ :min="0"
+ :precision="2"
+ placeholder="濡� 12.6"
+ />
+ </div>
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">鍦嗚鍗婂緞(mm)</span>
+ <InputNumber
+ v-model:value="angleForm.cornerRadius"
+ class="!flex-1"
+ :min="0"
+ :precision="2"
+ placeholder="濡� 1"
+ />
+ </div>
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">缁濈紭鍘氬害(mm)</span>
+ <InputNumber
+ v-model:value="angleForm.insulationThickness"
+ class="!flex-1"
+ :min="0"
+ :precision="2"
+ placeholder="濡� 0.75"
+ />
+ </div>
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">绾稿甫瀹藉害(mm)</span>
+ <InputNumber
+ v-model:value="angleForm.tapeWidth"
+ class="!flex-1"
+ :min="0"
+ :precision="2"
+ placeholder="濡� 22"
+ />
+ </div>
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">閲嶅彔瀹藉害(mm)</span>
+ <InputNumber
+ v-model:value="angleForm.overlapWidth"
+ class="!flex-1"
+ :min="0"
+ :precision="2"
+ placeholder="濡� 2"
+ />
+ </div>
+ </div>
+ <div class="mt-4 flex gap-2">
+ <Button type="primary" @click="handleCalcAngle">璁$畻</Button>
+ <Button @click="handleFillAngle">鍥炲~缁曞寘瑙掑害</Button>
+ </div>
+ <Descriptions v-if="angleResult" class="mt-4" :column="3" bordered>
+ <DescriptionsItem label="瀵肩嚎鍛ㄩ暱(mm)">
+ {{ angleResult.circumference }}
+ </DescriptionsItem>
+ <DescriptionsItem label="缁曞寘瑙掑害(掳)">
+ {{ angleResult.wrappingAngle }}
+ </DescriptionsItem>
+ <DescriptionsItem label="缁曞寘鑺傝窛(mm)">
+ {{ angleResult.wrappingPitch }}
+ </DescriptionsItem>
+ </Descriptions>
+ </Tabs.TabPane>
+
+ <Tabs.TabPane key="resistance" tab="鐩存祦鐢甸樆">
+ <div class="grid grid-cols-3 gap-4">
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">瀵间綋鍘氬害(mm)</span>
+ <InputNumber
+ v-model:value="resistanceForm.conductorThickness"
+ class="!flex-1"
+ :min="0"
+ :precision="2"
+ placeholder="濡� 1.27"
+ />
+ </div>
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">瀵间綋瀹藉害(mm)</span>
+ <InputNumber
+ v-model:value="resistanceForm.conductorWidth"
+ class="!flex-1"
+ :min="0"
+ :precision="2"
+ placeholder="濡� 5.7"
+ />
+ </div>
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">璁$畻娓╁害(鈩�)</span>
+ <InputNumber
+ v-model:value="resistanceForm.temperature"
+ class="!flex-1"
+ :min="0"
+ :precision="0"
+ />
+ </div>
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">瀵间綋鏉愯川</span>
+ <Select
+ v-model:value="resistanceForm.material"
+ class="!flex-1"
+ :options="[
+ { label: '閾�', value: 'copper' },
+ { label: '閾�', value: 'aluminum' },
+ ]"
+ />
+ </div>
+ </div>
+ <div class="mt-4 flex gap-2">
+ <Button type="primary" @click="handleCalcResistance">璁$畻</Button>
+ <Button @click="handleFillResistance">鍥炲~20鈩冪數闃�</Button>
+ </div>
+ <Descriptions v-if="resistanceResult" class="mt-4" :column="2" bordered>
+ <DescriptionsItem label="鎴潰绉�(mm虏)">
+ {{ resistanceResult.crossSection }}
+ </DescriptionsItem>
+ <DescriptionsItem label="20鈩冪數闃�(惟/km)">
+ {{ resistanceResult.resistance20cKm }}
+ </DescriptionsItem>
+ <DescriptionsItem label="璁$畻娓╁害鐢甸樆(惟/m)">
+ {{ resistanceResult.resistanceAtTemp }}
+ </DescriptionsItem>
+ <DescriptionsItem label="璁$畻娓╁害鐢甸樆(惟/km)">
+ {{ resistanceResult.resistanceAtTempKm }}
+ </DescriptionsItem>
+ </Descriptions>
+ </Tabs.TabPane>
+
+ <Tabs.TabPane key="reel" tab="鐩樺叿瀹归噺">
+ <div class="grid grid-cols-3 gap-4">
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">鐩樺叿澶栧緞 d1(mm)</span>
+ <InputNumber
+ v-model:value="reelForm.reelOuterDiameter"
+ class="!flex-1"
+ :min="0"
+ :precision="0"
+ placeholder="濡� 1600"
+ />
+ </div>
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">鐩樿姱鍐呭緞 d2(mm)</span>
+ <InputNumber
+ v-model:value="reelForm.reelInnerDiameter"
+ class="!flex-1"
+ :min="0"
+ :precision="0"
+ placeholder="濡� 1100"
+ />
+ </div>
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">鐩樺 l2(mm)</span>
+ <InputNumber
+ v-model:value="reelForm.reelWidth"
+ class="!flex-1"
+ :min="0"
+ :precision="0"
+ placeholder="濡� 500"
+ />
+ </div>
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">CTC楂樺害(mm)</span>
+ <InputNumber
+ v-model:value="reelForm.ctcThickness"
+ class="!flex-1"
+ :min="0"
+ :precision="2"
+ placeholder="濡� 19.08"
+ />
+ </div>
+ <div class="flex items-center gap-2">
+ <span class="w-28 text-right">CTC瀹藉害(mm)</span>
+ <InputNumber
+ v-model:value="reelForm.ctcWidth"
+ class="!flex-1"
+ :min="0"
+ :precision="2"
+ placeholder="濡� 12.6"
+ />
+ </div>
+ </div>
+ <div class="mt-4 flex gap-2">
+ <Button type="primary" @click="handleCalcReel">璁$畻</Button>
+ <Button @click="handleFillReel">鍥炲~鏈�澶у閲�</Button>
+ </div>
+ <Descriptions v-if="reelResult" class="mt-4" :column="2" bordered>
+ <DescriptionsItem label="姣忕洏闀垮害(m)">
+ {{ reelResult.capacityMeter }}
+ </DescriptionsItem>
+ <DescriptionsItem label="姣忕洏闀垮害(km)">
+ {{ reelResult.capacityKm }}
+ </DescriptionsItem>
+ </Descriptions>
+ </Tabs.TabPane>
+ </Tabs>
+ </Modal>
+</template>
diff --git a/src/views/mes/pd/ctc/modules/form.vue b/src/views/mes/pd/ctc/modules/form.vue
new file mode 100644
index 0000000..989abef
--- /dev/null
+++ b/src/views/mes/pd/ctc/modules/form.vue
@@ -0,0 +1,134 @@
+<script lang="ts" setup>
+ import type { FormType } from '../data';
+
+ import type { MesPdCtcApi } from '#/api/mes/pd/ctc';
+
+ import { computed, ref } from 'vue';
+
+ import { useVbenModal } from '@vben/common-ui';
+ import { $t } from '@vben/locales';
+
+ import { Button, message } from 'ant-design-vue';
+
+ import { useVbenForm } from '#/adapter/form';
+ import {
+ createCtc,
+ getCtc,
+ updateCtc,
+ } from '#/api/mes/pd/ctc';
+
+ import { useFormSchema } from '../data';
+ import CalcModal from './calc-modal.vue';
+
+ const emit = defineEmits(['success']);
+ const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
+ const formData = ref<MesPdCtcApi.Ctc>();
+ const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
+ const getTitle = computed(() => {
+ if (formType.value === 'detail') {
+ return '鏌ョ湅鎹綅瀵肩嚎鎶�鏈弬鏁�';
+ }
+ return formType.value === 'update' ? '淇敼鎹綅瀵肩嚎鎶�鏈弬鏁�' : '鏂板鎹綅瀵肩嚎鎶�鏈弬鏁�';
+ });
+
+ const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 130,
+ },
+ wrapperClass: 'grid-cols-3',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+ });
+
+ const [CalcModal, calcModalApi] = useVbenModal({
+ connectedComponent: CalcModal,
+ destroyOnClose: true,
+ });
+
+ /** 鎵撳紑璁$畻宸ュ叿 */
+ function handleOpenCalc() {
+ calcModalApi.open();
+ }
+
+ /** 鍥炲~缁曞寘瑙掑害 */
+ function handleFillAngle(value: { wrappingAngle: number }) {
+ formApi.setFieldValue('wrappingAngle', value.wrappingAngle);
+ }
+
+ /** 鍥炲~鐩存祦鐢甸樆 */
+ function handleFillResistance(value: { resistance20c: number }) {
+ formApi.setFieldValue('resistance20c', value.resistance20c);
+ }
+
+ const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (isDetail.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data = (await formApi.getValues()) as MesPdCtcApi.Ctc;
+ try {
+ await (data.id ? updateCtc(data) : createCtc(data));
+ // 鍏抽棴骞舵彁绀�
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } 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() });
+ formApi.setDisabled(formType.value === 'detail');
+ modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getCtc(data.id);
+ // 璁剧疆鍒� values
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ });
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-4/5">
+ <Form class="mx-4" />
+ <template #prepend-footer>
+ <Button
+ v-if="!isDetail"
+ type="default"
+ @click="handleOpenCalc"
+ >
+ 璁$畻宸ュ叿
+ </Button>
+ </template>
+ <CalcModal
+ @angle="handleFillAngle"
+ @resistance="handleFillResistance"
+ />
+ </Modal>
+</template>
diff --git a/src/views/mes/pd/ctc/modules/index.ts b/src/views/mes/pd/ctc/modules/index.ts
new file mode 100644
index 0000000..42004c2
--- /dev/null
+++ b/src/views/mes/pd/ctc/modules/index.ts
@@ -0,0 +1 @@
+export { default as CalcModal } from './calc-modal.vue';
diff --git a/src/views/mes/pd/tech-confirm/data.ts b/src/views/mes/pd/tech-confirm/data.ts
new file mode 100644
index 0000000..ea967b2
--- /dev/null
+++ b/src/views/mes/pd/tech-confirm/data.ts
@@ -0,0 +1,260 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmCustomerApi } from '#/api/crm/customer';
+import type { MesPdTechConfirmApi } from '#/api/mes/pd/tech-confirm';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼浜у搧鎶�鏈‘璁ゅ嚱 */
+export function useFormSchema(
+ formApi?: VbenFormApi,
+): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'confirmNo',
+ label: '纭鍑界紪鍙�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ‘璁ゅ嚱缂栧彿',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'customerId',
+ label: '瀹㈡埛',
+ component: markRaw(CrmCustomerSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨瀹㈡埛',
+ onChange: (item?: CrmCustomerApi.Customer) => {
+ formApi?.setFieldValue('customerName', item?.name);
+ },
+ },
+ },
+ {
+ fieldName: 'customerName',
+ label: '瀹㈡埛鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ鎴峰悕绉�',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'productName',
+ label: '浜у搧鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヤ骇鍝佸悕绉�',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'specification',
+ label: '瑙勬牸鍨嬪彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ鏍煎瀷鍙�',
+ },
+ },
+ {
+ fieldName: 'techParams',
+ label: '鎶�鏈弬鏁�(JSON)',
+ component: 'Textarea',
+ formItemClass: 'col-span-3',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ妧鏈弬鏁� JSON锛屽 {"thickness":"1.27","width":"5.7"}',
+ rows: 4,
+ },
+ },
+ {
+ fieldName: 'templateType',
+ label: '妯℃澘绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.PRODUCT_TECH_CONFIRM_TEMPLATE, 'string'),
+ placeholder: '璇烽�夋嫨妯℃澘绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.PRODUCT_TECH_CONFIRM_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ },
+ {
+ fieldName: 'drafterName',
+ label: '缂栧埗浜�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ紪鍒朵汉濮撳悕',
+ },
+ },
+ {
+ fieldName: 'draftTime',
+ label: '缂栧埗鏃堕棿',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ showTime: true,
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ },
+ },
+ {
+ fieldName: 'reviewerName',
+ label: '瀹℃牳浜�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ鏍镐汉濮撳悕',
+ },
+ },
+ {
+ fieldName: 'reviewTime',
+ label: '瀹℃牳鏃堕棿',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ showTime: true,
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ },
+ },
+ {
+ fieldName: 'customerConfirmUser',
+ label: '瀹㈡埛纭浜�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ鎴风‘璁や汉',
+ },
+ },
+ {
+ fieldName: 'customerConfirmTime',
+ label: '瀹㈡埛纭鏃堕棿',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ showTime: true,
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-3',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'confirmNo',
+ label: '纭鍑界紪鍙�',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ョ‘璁ゅ嚱缂栧彿',
+ },
+ },
+ {
+ fieldName: 'productName',
+ label: '浜у搧鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ヤ骇鍝佸悕绉�',
+ },
+ },
+ {
+ fieldName: 'templateType',
+ label: '妯℃澘绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.PRODUCT_TECH_CONFIRM_TEMPLATE, 'string'),
+ placeholder: '璇烽�夋嫨妯℃澘绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.PRODUCT_TECH_CONFIRM_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesPdTechConfirmApi.TechConfirm>['columns'] {
+ return [
+ { field: 'confirmNo', title: '纭鍑界紪鍙�', minWidth: 160 },
+ { field: 'customerName', title: '瀹㈡埛鍚嶇О', minWidth: 180 },
+ { field: 'productName', title: '浜у搧鍚嶇О', minWidth: 140 },
+ { field: 'specification', title: '瑙勬牸鍨嬪彿', minWidth: 140 },
+ {
+ field: 'templateType',
+ title: '妯℃澘绫诲瀷',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PRODUCT_TECH_CONFIRM_TEMPLATE },
+ },
+ },
+ { field: 'drafterName', title: '缂栧埗浜�', width: 90 },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.PRODUCT_TECH_CONFIRM_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 260,
+ fixed: 'right',
+ slots: {
+ default: 'actions',
+ },
+ },
+ ];
+}
diff --git a/src/views/mes/pd/tech-confirm/index.vue b/src/views/mes/pd/tech-confirm/index.vue
new file mode 100644
index 0000000..88f2bf0
--- /dev/null
+++ b/src/views/mes/pd/tech-confirm/index.vue
@@ -0,0 +1,198 @@
+<script lang="ts" setup>
+ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+ import type { MesPdTechConfirmApi } from '#/api/mes/pd/tech-confirm';
+
+ import { ref } from 'vue';
+
+ import { Page, useVbenModal } from '@vben/common-ui';
+ import { $t } from '@vben/locales';
+ import { downloadFileFromBlobPart } from '@vben/utils';
+
+ import { Button, message } from 'ant-design-vue';
+
+ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+ import {
+ deleteTechConfirm,
+ exportTechConfirm,
+ getTechConfirmPage,
+ updateTechConfirm,
+ } from '#/api/mes/pd/tech-confirm';
+
+ import { useGridColumns, useGridFormSchema } from './data';
+ import Form from './modules/form.vue';
+
+ const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+ });
+
+ /** 鍒锋柊琛ㄦ牸 */
+ function handleRefresh() {
+ gridApi.query();
+ }
+
+ /** 鏂板 */
+ function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+ }
+
+ /** 鏌ョ湅 */
+ function handleDetail(row: MesPdTechConfirmApi.TechConfirm) {
+ formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+ }
+
+ /** 缂栬緫 */
+ function handleEdit(row: MesPdTechConfirmApi.TechConfirm) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
+ }
+
+ /** 鍒犻櫎 */
+ async function handleDelete(row: MesPdTechConfirmApi.TechConfirm) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.confirmNo ?? '']),
+ duration: 0,
+ });
+ try {
+ await deleteTechConfirm(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.confirmNo ?? '']));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+ }
+
+ /** 鐘舵�佹祦杞� */
+ async function handleStatus(row: MesPdTechConfirmApi.TechConfirm, next: number) {
+ const hideLoading = message.loading({ content: '鐘舵�佹洿鏂颁腑...', duration: 0 });
+ try {
+ await updateTechConfirm({ ...row, status: next });
+ message.success($t('ui.actionMessage.operationSuccess'));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+ }
+
+ /** 瀵煎嚭 */
+ async function handleExport() {
+ const data = await exportTechConfirm(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) =>
+ await getTechConfirmPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesPdTechConfirmApi.TechConfirm>,
+ });
+</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:pd-tech-confirm:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:pd-tech-confirm:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #confirmNo="{ row }">
+ <Button type="link" @click="handleDetail(row)">
+ {{ row.confirmNo }}
+ </Button>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '鎻愪氦瀹℃牳',
+ type: 'link',
+ icon: ACTION_ICON.AUDIT,
+ auth: ['mes:pd-tech-confirm:update'],
+ ifShow: row.status === 0,
+ onClick: handleStatus.bind(null, row, 1),
+ },
+ {
+ label: '瀹℃牳閫氳繃',
+ type: 'link',
+ icon: ACTION_ICON.AUDIT,
+ auth: ['mes:pd-tech-confirm:update'],
+ ifShow: row.status === 1,
+ onClick: handleStatus.bind(null, row, 2),
+ },
+ {
+ label: '瀹㈡埛纭',
+ type: 'link',
+ icon: ACTION_ICON.AUDIT,
+ auth: ['mes:pd-tech-confirm:update'],
+ ifShow: row.status === 2,
+ onClick: handleStatus.bind(null, row, 3),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pd-tech-confirm:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pd-tech-confirm:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.confirmNo ?? '']),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ {
+ label: $t('common.detail'),
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['mes:pd-tech-confirm:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/pd/tech-confirm/modules/form.vue b/src/views/mes/pd/tech-confirm/modules/form.vue
new file mode 100644
index 0000000..5ae8653
--- /dev/null
+++ b/src/views/mes/pd/tech-confirm/modules/form.vue
@@ -0,0 +1,100 @@
+<script lang="ts" setup>
+ import type { FormType } from '../data';
+
+ import type { MesPdTechConfirmApi } from '#/api/mes/pd/tech-confirm';
+
+ import { computed, ref } from 'vue';
+
+ import { useVbenModal } from '@vben/common-ui';
+ import { $t } from '@vben/locales';
+
+ import { message } from 'ant-design-vue';
+
+ import { useVbenForm } from '#/adapter/form';
+ import {
+ createTechConfirm,
+ getTechConfirm,
+ updateTechConfirm,
+ } from '#/api/mes/pd/tech-confirm';
+
+ import { useFormSchema } from '../data';
+
+ const emit = defineEmits(['success']);
+ const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
+ const formData = ref<MesPdTechConfirmApi.TechConfirm>();
+ const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
+ const getTitle = computed(() => {
+ if (formType.value === 'detail') {
+ return '鏌ョ湅鎶�鏈‘璁ゅ嚱';
+ }
+ return formType.value === 'update' ? '淇敼鎶�鏈‘璁ゅ嚱' : '鏂板鎶�鏈‘璁ゅ嚱';
+ });
+
+ const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 130,
+ },
+ wrapperClass: 'grid-cols-3',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+ });
+
+ const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (isDetail.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data = (await formApi.getValues()) as MesPdTechConfirmApi.TechConfirm;
+ try {
+ await (data.id ? updateTechConfirm(data) : createTechConfirm(data));
+ // 鍏抽棴骞舵彁绀�
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } 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(formApi) });
+ formApi.setDisabled(formType.value === 'detail');
+ modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getTechConfirm(data.id);
+ // 璁剧疆鍒� values
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ });
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-4/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pro/emergencyplan/data.ts b/src/views/mes/pro/emergencyplan/data.ts
new file mode 100644
index 0000000..04e599c
--- /dev/null
+++ b/src/views/mes/pro/emergencyplan/data.ts
@@ -0,0 +1,187 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 鐘舵�佸父閲� */
+export const PLAN_STATUS = {
+ DRAFT: 0,
+ PUBLISHED: 10,
+};
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+ const isReadonly = formType === 'detail';
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '棰勬缂栫爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '绯荤粺鑷姩鐢熸垚',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '棰勬鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ラ妗堝悕绉�',
+ disabled: isReadonly,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'planType',
+ label: '棰勬绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨棰勬绫诲瀷',
+ disabled: isReadonly,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'orgUnit',
+ label: '璐d换鍗曚綅',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ矗浠诲崟浣�/閮ㄩ棬',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'content',
+ label: '澶勭疆鍐呭',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ簲鎬ュ缃唴瀹�',
+ rows: 4,
+ disabled: isReadonly,
+ },
+ formItemClass: 'col-span-3',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: isReadonly,
+ },
+ formItemClass: 'col-span-3',
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '棰勬缂栫爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ラ妗堢紪鐮�',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '棰勬鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ラ妗堝悕绉�',
+ },
+ },
+ {
+ fieldName: 'planType',
+ label: '棰勬绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨棰勬绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '鑽夌', value: PLAN_STATUS.DRAFT },
+ { label: '宸插彂甯�', value: PLAN_STATUS.PUBLISHED },
+ ],
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勮〃鏍煎垪 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 50 },
+ { type: 'seq', title: '搴忓彿', width: 60 },
+ {
+ field: 'code',
+ title: '棰勬缂栫爜',
+ minWidth: 150,
+ slots: { default: 'code' },
+ },
+ {
+ field: 'name',
+ title: '棰勬鍚嶇О',
+ minWidth: 200,
+ },
+ {
+ field: 'planType',
+ title: '棰勬绫诲瀷',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE },
+ },
+ },
+ {
+ field: 'orgUnit',
+ title: '璐d换鍗曚綅',
+ minWidth: 140,
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 100,
+ slots: { default: 'status' },
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'action',
+ title: '鎿嶄綔',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/mes/pro/emergencyplan/index.vue b/src/views/mes/pro/emergencyplan/index.vue
new file mode 100644
index 0000000..501f310
--- /dev/null
+++ b/src/views/mes/pro/emergencyplan/index.vue
@@ -0,0 +1,188 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProEmergencyPlanApi } from '#/api/mes/pro/emergencyplan';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { deletePlan, getPlanPage, publishPlan } from '#/api/mes/pro/emergencyplan';
+import { $t } from '#/locales';
+
+import { PLAN_STATUS, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** MES 搴旀�ュ缃妗堝垪琛� */
+defineOptions({ name: 'MesProEmergencyPlan' });
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: MesProEmergencyPlanApi.EmergencyPlan) {
+ formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+function handleDetail(row: MesProEmergencyPlanApi.EmergencyPlan) {
+ formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+async function handleDelete(ids: number[]) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting'),
+ duration: 0,
+ });
+ try {
+ await deletePlan(ids);
+ message.success($t('ui.actionMessage.deleteSuccess'));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+async function handlePublish(row: MesProEmergencyPlanApi.EmergencyPlan) {
+ await publishPlan(row.id!);
+ message.success('宸插彂甯�');
+ handleRefresh();
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+ records,
+}: {
+ records: MesProEmergencyPlanApi.EmergencyPlan[];
+}) {
+ checkedIds.value = records.map((item) => item.id!);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getPlanPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesProEmergencyPlanApi.EmergencyPlan>,
+ gridEvents: {
+ checkboxAll: handleRowCheckboxChange,
+ checkboxChange: handleRowCheckboxChange,
+ },
+});
+</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-emergency-plan:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: '鎵归噺鍒犻櫎',
+ type: 'primary',
+ danger: true,
+ disabled: isEmpty(checkedIds),
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pro-emergency-plan:delete'],
+ popConfirm: {
+ title: `鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛焋,
+ confirm: handleDelete.bind(null, checkedIds),
+ },
+ },
+ ]"
+ />
+ </template>
+ <template #code="{ row }">
+ <a class="link" @click="handleDetail(row)">{{ row.code }}</a>
+ </template>
+ <template #status="{ row }">
+ <Tag v-if="row.status === PLAN_STATUS.DRAFT" color="default">鑽夌</Tag>
+ <Tag v-else-if="row.status === PLAN_STATUS.PUBLISHED" color="success">宸插彂甯�</Tag>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.detail'),
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['mes:pro-emergency-plan:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pro-emergency-plan:update'],
+ ifShow: row.status === PLAN_STATUS.DRAFT,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '鍙戝竷',
+ type: 'link',
+ auth: ['mes:pro-emergency-plan:update'],
+ ifShow: row.status === PLAN_STATUS.DRAFT,
+ popConfirm: {
+ title: '纭鍙戝竷璇ラ妗堬紵',
+ confirm: handlePublish.bind(null, row),
+ },
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pro-emergency-plan:delete'],
+ ifShow: row.status === PLAN_STATUS.DRAFT,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.code]),
+ confirm: handleDelete.bind(null, [row.id!]),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/pro/emergencyplan/modules/form.vue b/src/views/mes/pro/emergencyplan/modules/form.vue
new file mode 100644
index 0000000..12a80ff
--- /dev/null
+++ b/src/views/mes/pro/emergencyplan/modules/form.vue
@@ -0,0 +1,91 @@
+<script lang="ts" setup>
+import type { MesProEmergencyPlanApi } from '#/api/mes/pro/emergencyplan';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createPlan, getPlan, updatePlan } from '#/api/mes/pro/emergencyplan';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProEmergencyPlanApi.EmergencyPlan>();
+const formType = ref<FormType>('create');
+
+const getTitle = computed(() => {
+ if (formType.value === 'create') {
+ return $t('ui.actionTitle.create', ['搴旀�ュ缃妗�']);
+ } else if (formType.value === 'edit') {
+ return $t('ui.actionTitle.edit', ['搴旀�ュ缃妗�']);
+ } else {
+ return '搴旀�ュ缃妗堣鎯�';
+ }
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 120,
+ },
+ wrapperClass: 'grid-cols-3',
+ layout: 'vertical',
+ schema: useFormSchema(formType.value),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data =
+ (await formApi.getValues()) as MesProEmergencyPlanApi.EmergencyPlan;
+ try {
+ await (formType.value === 'create'
+ ? createPlan(data)
+ : updatePlan(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } 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.setDisabled(formType.value === 'detail');
+ formApi.updateSchema(useFormSchema(formType.value));
+ if (!data || !data.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getPlan(data.id);
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-2/3" :show-confirm-button="formType !== 'detail'">
+ <Form class="mx-3" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pro/maintenance-plan/data.ts b/src/views/mes/pro/maintenance-plan/data.ts
new file mode 100644
index 0000000..75dc159
--- /dev/null
+++ b/src/views/mes/pro/maintenance-plan/data.ts
@@ -0,0 +1,244 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 璁″垝鐘舵�佸父閲� */
+export const PLAN_STATUS = {
+ DRAFT: 0,
+ ISSUED: 10,
+ FINISHED: 20,
+ CANCELLED: 30,
+};
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+ const isReadonly = formType === 'detail';
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '璁″垝缂栫爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '绯荤粺鑷姩鐢熸垚',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '璁″垝鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ鍒掑悕绉�',
+ disabled: isReadonly,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'planType',
+ label: '璁″垝绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.MES_PRO_MAINTENANCE_PLAN_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨璁″垝绫诲瀷',
+ disabled: isReadonly,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'station',
+ label: '绔欑偣',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ珯鐐�',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'line',
+ label: '绾胯矾',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ嚎璺�',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'planDate',
+ label: '璁″垝鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '閫夋嫨璁″垝鏃ユ湡',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ disabled: isReadonly,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'teamId',
+ label: '鐝粍',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ placeholder: '鐢辫鍒掓墽琛屽垎閰�',
+ },
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: isReadonly,
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'items',
+ label: '澶囦欢/鐗╄祫闇�姹�',
+ component: 'Input',
+ formItemClass: 'col-span-3',
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '璁″垝缂栫爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ鍒掔紪鐮�',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '璁″垝鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ鍒掑悕绉�',
+ },
+ },
+ {
+ fieldName: 'planType',
+ label: '璁″垝绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PRO_MAINTENANCE_PLAN_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨璁″垝绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '鑽夌', value: PLAN_STATUS.DRAFT },
+ { label: '宸蹭笅鍙�', value: PLAN_STATUS.ISSUED },
+ { label: '宸插畬鎴�', value: PLAN_STATUS.FINISHED },
+ { label: '宸插彇娑�', value: PLAN_STATUS.CANCELLED },
+ ],
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ },
+ {
+ fieldName: 'planDate',
+ label: '璁″垝鏃ユ湡',
+ component: 'RangePicker',
+ componentProps: getRangePickerDefaultProps(),
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勮〃鏍煎垪 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 50 },
+ { type: 'seq', title: '搴忓彿', width: 60 },
+ {
+ field: 'code',
+ title: '璁″垝缂栫爜',
+ minWidth: 150,
+ slots: { default: 'code' },
+ },
+ {
+ field: 'name',
+ title: '璁″垝鍚嶇О',
+ minWidth: 180,
+ },
+ {
+ field: 'planType',
+ title: '璁″垝绫诲瀷',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PRO_MAINTENANCE_PLAN_TYPE },
+ },
+ },
+ {
+ field: 'station',
+ title: '绔欑偣',
+ minWidth: 140,
+ },
+ {
+ field: 'line',
+ title: '绾胯矾',
+ minWidth: 140,
+ },
+ {
+ field: 'teamName',
+ title: '鐝粍',
+ width: 110,
+ },
+ {
+ field: 'planDate',
+ title: '璁″垝鏃ユ湡',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 100,
+ slots: { default: 'status' },
+ },
+ {
+ field: 'workOrderCode',
+ title: '鐢熸垚鐨勫伐鍗�',
+ minWidth: 150,
+ },
+ {
+ field: 'action',
+ title: '鎿嶄綔',
+ width: 300,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/mes/pro/maintenance-plan/index.vue b/src/views/mes/pro/maintenance-plan/index.vue
new file mode 100644
index 0000000..b58d3e6
--- /dev/null
+++ b/src/views/mes/pro/maintenance-plan/index.vue
@@ -0,0 +1,271 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProMaintenancePlanApi } from '#/api/mes/pro/maintenance-plan';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deletePlan,
+ exportPlan,
+ finishPlan,
+ generatePurchasePlan,
+ generateWorkOrder,
+ getPlanPage,
+ issuePlan,
+} from '#/api/mes/pro/maintenance-plan';
+import { $t } from '#/locales';
+
+import { PLAN_STATUS, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** MES 鐢靛姏浣滀笟璁″垝鍒楄〃 */
+defineOptions({ name: 'MesProMaintenancePlan' });
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+async function handleExport() {
+ const data = await exportPlan(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '鐢靛姏浣滀笟璁″垝.xls', source: data });
+}
+
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: MesProMaintenancePlanApi.MaintenancePlan) {
+ formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+function handleDetail(row: MesProMaintenancePlanApi.MaintenancePlan) {
+ formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+async function handleDelete(ids: number[]) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting'),
+ duration: 0,
+ });
+ try {
+ await deletePlan(ids);
+ message.success($t('ui.actionMessage.deleteSuccess'));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+async function handleIssue(row: MesProMaintenancePlanApi.MaintenancePlan) {
+ await issuePlan(row.id!);
+ message.success('宸蹭笅鍙�');
+ handleRefresh();
+}
+
+async function handleFinish(row: MesProMaintenancePlanApi.MaintenancePlan) {
+ await finishPlan(row.id!);
+ message.success('宸插畬鎴�');
+ handleRefresh();
+}
+
+async function handleGeneratePurchasePlan(
+ row: MesProMaintenancePlanApi.MaintenancePlan,
+) {
+ const hideLoading = message.loading({
+ content: '姝e湪鏍规嵁澶囦欢闇�姹傜敓鎴愰噰璐鍒�...',
+ duration: 0,
+ });
+ try {
+ const planId = await generatePurchasePlan(row.id!);
+ message.success(`宸茬敓鎴愰噰璐鍒掞紙缂栧彿 ${planId}锛夛紝璇峰湪閲囪喘绠$悊涓牳瀵规彁浜ゅ鎵筦);
+ } finally {
+ hideLoading();
+ }
+}
+
+async function handleGenerateWorkOrder(
+ row: MesProMaintenancePlanApi.MaintenancePlan,
+) {
+ const hideLoading = message.loading({
+ content: '姝e湪鐢熸垚宸ュ崟...',
+ duration: 0,
+ });
+ try {
+ const workOrderId = await generateWorkOrder(row.id!);
+ message.success(`宸茬敓鎴愬伐鍗曪紙缂栧彿 ${workOrderId}锛夛紝鍙湪鐢熶骇璁㈠崟涓煡鐪媊);
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+ records,
+}: {
+ records: MesProMaintenancePlanApi.MaintenancePlan[];
+}) {
+ checkedIds.value = records.map((item) => item.id!);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getPlanPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesProMaintenancePlanApi.MaintenancePlan>,
+ gridEvents: {
+ checkboxAll: handleRowCheckboxChange,
+ checkboxChange: handleRowCheckboxChange,
+ },
+});
+</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-maintenance-plan:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:pro-maintenance-plan:export'],
+ onClick: handleExport,
+ },
+ {
+ label: '鎵归噺鍒犻櫎',
+ type: 'primary',
+ danger: true,
+ disabled: isEmpty(checkedIds),
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pro-maintenance-plan:delete'],
+ popConfirm: {
+ title: `鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛焋,
+ confirm: handleDelete.bind(null, checkedIds),
+ },
+ },
+ ]"
+ />
+ </template>
+ <template #code="{ row }">
+ <a class="link" @click="handleDetail(row)">{{ row.code }}</a>
+ </template>
+ <template #status="{ row }">
+ <Tag v-if="row.status === PLAN_STATUS.DRAFT" color="default">鑽夌</Tag>
+ <Tag v-else-if="row.status === PLAN_STATUS.ISSUED" color="processing">宸蹭笅鍙�</Tag>
+ <Tag v-else-if="row.status === PLAN_STATUS.FINISHED" color="success">宸插畬鎴�</Tag>
+ <Tag v-else-if="row.status === PLAN_STATUS.CANCELLED" color="warning">宸插彇娑�</Tag>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.detail'),
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['mes:pro-maintenance-plan:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pro-maintenance-plan:update'],
+ ifShow: row.status === PLAN_STATUS.DRAFT,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '涓嬪彂',
+ type: 'link',
+ auth: ['mes:pro-maintenance-plan:update'],
+ ifShow: row.status === PLAN_STATUS.DRAFT,
+ popConfirm: {
+ title: '纭涓嬪彂璇ヨ鍒掞紵',
+ confirm: handleIssue.bind(null, row),
+ },
+ },
+ {
+ label: '鐢熸垚閲囪喘璁″垝',
+ type: 'link',
+ auth: ['mes:pro-maintenance-plan:generate-purchase-plan'],
+ ifShow: row.status === PLAN_STATUS.ISSUED,
+ onClick: handleGeneratePurchasePlan.bind(null, row),
+ },
+ {
+ label: '鐢熸垚宸ュ崟',
+ type: 'link',
+ auth: ['mes:pro-maintenance-plan:generate-work-order'],
+ ifShow: row.status === PLAN_STATUS.ISSUED && !row.workOrderId,
+ onClick: handleGenerateWorkOrder.bind(null, row),
+ },
+ {
+ label: '瀹屾垚',
+ type: 'link',
+ auth: ['mes:pro-maintenance-plan:update'],
+ ifShow: row.status === PLAN_STATUS.ISSUED,
+ popConfirm: {
+ title: '纭瀹屾垚璇ヨ鍒掞紵',
+ confirm: handleFinish.bind(null, row),
+ },
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pro-maintenance-plan:delete'],
+ ifShow: row.status === PLAN_STATUS.DRAFT,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.code]),
+ confirm: handleDelete.bind(null, [row.id!]),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/pro/maintenance-plan/modules/form.vue b/src/views/mes/pro/maintenance-plan/modules/form.vue
new file mode 100644
index 0000000..ee7a49d
--- /dev/null
+++ b/src/views/mes/pro/maintenance-plan/modules/form.vue
@@ -0,0 +1,220 @@
+<script lang="ts" setup>
+import type { MesProMaintenancePlanApi } from '#/api/mes/pro/maintenance-plan';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Button, DatePicker, Input, InputNumber, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createPlan,
+ getPlan,
+ updatePlan,
+} from '#/api/mes/pro/maintenance-plan';
+import { MdItemSelect } from '#/views/mes/md/item/components';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProMaintenancePlanApi.MaintenancePlan>();
+const formType = ref<FormType>('create');
+const items = ref<MesProMaintenancePlanApi.PlanItem[]>([]);
+
+const getTitle = computed(() => {
+ if (formType.value === 'create') {
+ return $t('ui.actionTitle.create', ['鐢靛姏浣滀笟璁″垝']);
+ } else if (formType.value === 'edit') {
+ return $t('ui.actionTitle.edit', ['鐢靛姏浣滀笟璁″垝']);
+ } else {
+ return '鐢靛姏浣滀笟璁″垝璇︽儏';
+ }
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 120,
+ },
+ wrapperClass: 'grid-cols-3',
+ layout: 'vertical',
+ schema: useFormSchema(formType.value),
+ showDefaultActions: false,
+});
+
+function handleAddItem() {
+ items.value.push({
+ itemId: undefined as unknown as number,
+ itemName: '',
+ itemCode: '',
+ unitMeasureName: '',
+ quantity: 1,
+ remark: '',
+ });
+}
+
+function handleRemoveItem(index: number) {
+ items.value.splice(index, 1);
+}
+
+function handleItemSelect(index: number, item: any) {
+ const row = items.value[index];
+ if (!row) {
+ return;
+ }
+ row.itemId = item?.id;
+ row.itemName = item?.name;
+ row.itemCode = item?.code;
+ row.unitMeasureName = item?.unitMeasureName;
+}
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ if (!items.value.length) {
+ message.error('璇疯嚦灏戞坊鍔犱竴鏉″浠�/鐗╄祫闇�姹�');
+ return;
+ }
+ if (items.value.some((item) => !item.itemId || !item.quantity)) {
+ message.error('璇峰畬鍠勫浠堕渶姹傜殑浜у搧涓庢暟閲�');
+ return;
+ }
+ modalApi.lock();
+ const data =
+ (await formApi.getValues()) as MesProMaintenancePlanApi.MaintenancePlan;
+ data.items = items.value.map((item) => ({
+ itemId: item.itemId,
+ quantity: item.quantity,
+ remark: item.remark,
+ }));
+ try {
+ await (formType.value === 'create'
+ ? createPlan(data)
+ : updatePlan(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ items.value = [];
+ return;
+ }
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ formApi.setDisabled(formType.value === 'detail');
+ formApi.updateSchema(useFormSchema(formType.value));
+ if (!data || !data.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getPlan(data.id);
+ await formApi.setValues(formData.value);
+ items.value = (formData.value.items ?? []).map((item) => ({ ...item }));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal
+ :title="getTitle"
+ class="w-3/4"
+ :show-confirm-button="formType !== 'detail'"
+ >
+ <Form class="mx-3">
+ <template #items>
+ <div class="mt-2">
+ <div v-if="formType !== 'detail'" class="mb-2">
+ <Button type="dashed" @click="handleAddItem">+ 娣诲姞澶囦欢闇�姹�</Button>
+ </div>
+ <table class="w-full border-collapse border border-gray-200 text-sm">
+ <thead>
+ <tr class="bg-gray-100">
+ <th class="border border-gray-200 px-2 py-1 w-12">搴忓彿</th>
+ <th class="border border-gray-200 px-2 py-1">鐗╂枡/澶囦欢</th>
+ <th class="border border-gray-200 px-2 py-1 w-20">鍗曚綅</th>
+ <th class="border border-gray-200 px-2 py-1 w-32">闇�姹傛暟閲�</th>
+ <th class="border border-gray-200 px-2 py-1">澶囨敞</th>
+ <th
+ v-if="formType !== 'detail'"
+ class="border border-gray-200 px-2 py-1 w-16"
+ >
+ 鎿嶄綔
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(item, index) in items" :key="index">
+ <td class="border border-gray-200 px-2 py-1 text-center">
+ {{ index + 1 }}
+ </td>
+ <td class="border border-gray-200 px-2 py-1">
+ <MdItemSelect
+ v-if="formType !== 'detail'"
+ :model-value="item.itemId"
+ placeholder="璇烽�夋嫨鐗╂枡/澶囦欢"
+ @change="handleItemSelect(index, $event)"
+ />
+ <span v-else>{{ item.itemName }}</span>
+ </td>
+ <td class="border border-gray-200 px-2 py-1 text-center">
+ {{ item.unitMeasureName || '-' }}
+ </td>
+ <td class="border border-gray-200 px-2 py-1">
+ <InputNumber
+ v-if="formType !== 'detail'"
+ v-model:value="item.quantity"
+ class="!w-full"
+ :min="0.01"
+ :precision="2"
+ />
+ <span v-else>{{ item.quantity }}</span>
+ </td>
+ <td class="border border-gray-200 px-2 py-1">
+ <Input
+ v-if="formType !== 'detail'"
+ v-model:value="item.remark"
+ placeholder="澶囨敞"
+ />
+ <span v-else>{{ item.remark || '-' }}</span>
+ </td>
+ <td
+ v-if="formType !== 'detail'"
+ class="border border-gray-200 px-2 py-1 text-center"
+ >
+ <Button type="link" danger @click="handleRemoveItem(index)">
+ 鍒犻櫎
+ </Button>
+ </td>
+ </tr>
+ <tr v-if="!items.length">
+ <td
+ :colspan="formType === 'detail' ? 5 : 6"
+ class="border border-gray-200 px-2 py-4 text-center text-gray-400"
+ >
+ 鏆傛棤澶囦欢闇�姹傦紝璇锋坊鍔�
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </template>
+ </Form>
+ </Modal>
+</template>
diff --git a/src/views/mes/pro/task/data.ts b/src/views/mes/pro/task/data.ts
index aeb9868..167bac7 100644
--- a/src/views/mes/pro/task/data.ts
+++ b/src/views/mes/pro/task/data.ts
@@ -19,6 +19,7 @@
import { getRangePickerDefaultProps } from '#/utils';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
+import { CalTeamSelect } from '#/views/mes/cal/team/components';
import { ProProcessSelect } from '#/views/mes/process-design/process/components';
import { RouteColorPicker } from '#/views/mes/process-design/route/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
@@ -347,6 +348,11 @@
width: 120,
},
{
+ field: 'teamName',
+ title: '鐝粍',
+ width: 120,
+ },
+ {
field: 'quantity',
title: '鎺掍骇鏁伴噺',
width: 100,
@@ -406,6 +412,14 @@
rules: 'selectRequired',
},
{
+ fieldName: 'teamId',
+ label: '鐝粍',
+ component: markRaw(CalTeamSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨鐝粍',
+ },
+ },
+ {
fieldName: 'quantity',
label: '鎺掍骇鏁伴噺',
component: 'InputNumber',
diff --git a/src/views/mes/pro/workorder/data.ts b/src/views/mes/pro/workorder/data.ts
index f62e323..e046fdf 100644
--- a/src/views/mes/pro/workorder/data.ts
+++ b/src/views/mes/pro/workorder/data.ts
@@ -3,6 +3,7 @@
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import type { MesProWorkOrderBomApi } from '#/api/mes/pro/workorder/bom';
import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
+import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
import { h, markRaw } from 'vue';
@@ -23,6 +24,7 @@
MdItemSelect,
} from '#/views/mes/md/item/components';
import { MdVendorSelect } from '#/views/mes/md/vendor/components';
+import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components';
/** 琛ㄥ崟绫诲瀷 */
export type FormType = 'confirm' | 'create' | 'detail' | 'finish' | 'update';
@@ -192,6 +194,30 @@
componentProps: {
disabled: headerReadonly,
placeholder: '璇烽�夋嫨瀹㈡埛',
+ },
+ dependencies: {
+ triggerFields: ['orderSourceType'],
+ show: (values) =>
+ values.orderSourceType === MesProWorkOrderSourceTypeEnum.ORDER,
+ },
+ },
+ {
+ fieldName: 'saleOrderId',
+ label: '钀ラ攢璁㈠崟',
+ component: markRaw(ErpSaleOrderSelect),
+ componentProps: {
+ disabled: headerReadonly,
+ placeholder: '璇烽�夋嫨钀ラ攢璁㈠崟锛圗RP閿�鍞鍗曪級',
+ // 閫変腑钀ラ攢璁㈠崟鍚庤嚜鍔ㄥ甫鍏ュ鎴�
+ onChange: async (item?: ErpSaleOrderApi.SaleOrder) => {
+ if (!item) {
+ return;
+ }
+ const values = (await formApi?.getValues()) ?? {};
+ if (values.clientId == null && item.customerId) {
+ await formApi?.setFieldValue('clientId', item.customerId);
+ }
+ },
},
dependencies: {
triggerFields: ['orderSourceType'],
@@ -398,6 +424,11 @@
width: 120,
},
{
+ field: 'saleOrderNo',
+ title: '钀ラ攢璁㈠崟鍙�',
+ width: 140,
+ },
+ {
field: 'requestDate',
title: '闇�姹傛棩鏈�',
width: 180,
diff --git a/src/views/mes/pro/workresult/summary.vue b/src/views/mes/pro/workresult/summary.vue
new file mode 100644
index 0000000..1c6eea6
--- /dev/null
+++ b/src/views/mes/pro/workresult/summary.vue
@@ -0,0 +1,168 @@
+<script lang="ts" setup>
+import type { WorkResultSummary } from '#/api/mes/pro/feedback';
+
+import { onMounted, reactive, ref } from 'vue';
+
+import { Page } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import {
+ Button,
+ Card,
+ Col,
+ DatePicker,
+ Row,
+ Select,
+ Space,
+ Table,
+ Tag,
+} from 'ant-design-vue';
+
+import { getFeedbackSummary } from '#/api/mes/pro/feedback';
+
+defineOptions({ name: 'MesWorkResultSummary' });
+
+const loading = ref(false);
+const summary = ref<WorkResultSummary>({});
+
+const queryForm = reactive<{
+ beginDate?: string;
+ endDate?: string;
+ workOrderType?: number;
+}>({});
+
+const workOrderTypeOptions = getDictOptions(
+ DICT_TYPE.MES_PRO_WORK_ORDER_TYPE,
+ 'number',
+).map((option) => ({ label: option.label, value: option.value as number }));
+
+async function loadSummary() {
+ loading.value = true;
+ try {
+ summary.value = await getFeedbackSummary({
+ beginDate: queryForm.beginDate,
+ endDate: queryForm.endDate,
+ workOrderType: queryForm.workOrderType,
+ });
+ } finally {
+ loading.value = false;
+ }
+}
+
+function handleRangeChange(_: unknown, dateStrings: string[]) {
+ queryForm.beginDate = dateStrings[0] || undefined;
+ queryForm.endDate = dateStrings[1] || undefined;
+}
+
+function resetQuery() {
+ queryForm.beginDate = undefined;
+ queryForm.endDate = undefined;
+ queryForm.workOrderType = undefined;
+ loadSummary();
+}
+
+onMounted(loadSummary);
+</script>
+
+<template>
+ <Page>
+ <Card class="mb-4">
+ <Space wrap>
+ <DatePicker.RangePicker
+ :placeholder="['寮�濮嬫棩鏈�', '缁撴潫鏃ユ湡']"
+ value-format="YYYY-MM-DD"
+ @change="handleRangeChange"
+ />
+ <Select
+ v-model:value="queryForm.workOrderType"
+ allow-clear
+ :options="workOrderTypeOptions"
+ placeholder="宸ュ崟绫诲瀷"
+ style="width: 160px"
+ />
+ <Button type="primary" :loading="loading" @click="loadSummary">
+ 鏌ヨ
+ </Button>
+ <Button @click="resetQuery">閲嶇疆</Button>
+ </Space>
+ </Card>
+
+ <Row :gutter="16" class="mb-4">
+ <Col :span="8">
+ <Card>
+ <div class="text-sm text-gray-500">绱鎶ュ伐鏁伴噺</div>
+ <div class="mt-2 text-2xl font-bold">
+ {{ summary.totalFeedbackQuantity ?? 0 }}
+ </div>
+ </Card>
+ </Col>
+ <Col :span="8">
+ <Card>
+ <div class="text-sm text-gray-500">绱鍚堟牸鏁伴噺</div>
+ <div class="mt-2 text-2xl font-bold text-green-600">
+ {{ summary.totalQualifiedQuantity ?? 0 }}
+ </div>
+ </Card>
+ </Col>
+ <Col :span="8">
+ <Card>
+ <div class="text-sm text-gray-500">绱鎶ュ伐鍗曟暟</div>
+ <div class="mt-2 text-2xl font-bold text-blue-600">
+ {{ summary.totalFeedbackCount ?? 0 }}
+ </div>
+ </Card>
+ </Col>
+ </Row>
+
+ <Card class="mb-4" title="鎸夊伐鍗曠被鍨嬫眹鎬�">
+ <Table
+ row-key="workOrderType"
+ :data-source="summary.byType ?? []"
+ :loading="loading"
+ :pagination="false"
+ size="middle"
+ >
+ <Table.Column title="宸ュ崟绫诲瀷" data-index="workOrderTypeName">
+ <template #default="{ record }">
+ <Tag color="blue">{{ record.workOrderTypeName }}</Tag>
+ </template>
+ </Table.Column>
+ <Table.Column
+ title="鎶ュ伐鏁伴噺"
+ data-index="feedbackQuantity"
+ align="right"
+ />
+ <Table.Column
+ title="鍚堟牸鏁伴噺"
+ data-index="qualifiedQuantity"
+ align="right"
+ />
+ <Table.Column title="鎶ュ伐鍗曟暟" data-index="feedbackCount" align="right" />
+ </Table>
+ </Card>
+
+ <Card title="鎸夋棩鏈熸眹鎬�">
+ <Table
+ row-key="date"
+ :data-source="summary.byDate ?? []"
+ :loading="loading"
+ :pagination="false"
+ size="middle"
+ >
+ <Table.Column title="鏃ユ湡" data-index="date" />
+ <Table.Column
+ title="鎶ュ伐鏁伴噺"
+ data-index="feedbackQuantity"
+ align="right"
+ />
+ <Table.Column
+ title="鍚堟牸鏁伴噺"
+ data-index="qualifiedQuantity"
+ align="right"
+ />
+ <Table.Column title="鎶ュ伐鍗曟暟" data-index="feedbackCount" align="right" />
+ </Table>
+ </Card>
+ </Page>
+</template>
diff --git a/src/views/mes/process-design/process/data.ts b/src/views/mes/process-design/process/data.ts
index 1cf6134..146b7a2 100644
--- a/src/views/mes/process-design/process/data.ts
+++ b/src/views/mes/process-design/process/data.ts
@@ -66,6 +66,16 @@
rules: z.string().min(1, '宸ュ簭鍚嶇О涓嶈兘涓虹┖').max(100),
},
{
+ fieldName: 'type',
+ label: '鐢靛姏宸ヨ壓鍒嗙被',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PRO_PROCESS_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鐢靛姏宸ヨ壓鍒嗙被',
+ },
+ },
+ {
fieldName: 'status',
label: '鐘舵��',
component: 'RadioGroup',
@@ -148,6 +158,15 @@
},
{ field: 'name', title: '宸ュ簭鍚嶇О', minWidth: 180 },
{
+ field: 'type',
+ title: '鐢靛姏宸ヨ壓鍒嗙被',
+ width: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PRO_PROCESS_TYPE },
+ },
+ },
+ {
field: 'status',
title: '鐘舵��',
width: 100,
diff --git a/src/views/mes/qc/outsourcetest/data.ts b/src/views/mes/qc/outsourcetest/data.ts
new file mode 100644
index 0000000..93e0f84
--- /dev/null
+++ b/src/views/mes/qc/outsourcetest/data.ts
@@ -0,0 +1,263 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 鐘舵�佸父閲� */
+export const TEST_STATUS = {
+ DRAFT: 0,
+ SUBMITTED: 10,
+ FINISHED: 20,
+};
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+ const isReadonly = formType === 'detail';
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '鍗曟嵁缂栫爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '绯荤粺鑷姩鐢熸垚',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '鍗曟嵁鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ崟鎹悕绉�',
+ disabled: isReadonly,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'testType',
+ label: '妫�娴嬬被鍨�',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.MES_QC_OUTSOURCE_TEST_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨妫�娴嬬被鍨�',
+ disabled: isReadonly,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'itemId',
+ label: '妫�娴嬪璞�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '妫�娴嬪璞★紙鐗╂枡/璁惧锛�',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'testOrg',
+ label: '妫�娴嬫満鏋�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ娴嬫満鏋�',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'testDate',
+ label: '妫�娴嬫棩鏈�',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '閫夋嫨妫�娴嬫棩鏈�',
+ showTime: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ disabled: isReadonly,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'result',
+ label: '妫�娴嬬粨鏋�',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.MES_QC_OUTSOURCE_TEST_RESULT, 'number'),
+ placeholder: '璇烽�夋嫨妫�娴嬬粨鏋�',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'reportNo',
+ label: '鎶ュ憡缂栧彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ娴嬫姤鍛婄紪鍙�',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: isReadonly,
+ },
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'blobIds',
+ label: '妫�娴嬫姤鍛�/璧勮川鏂囦欢',
+ component: 'FileUpload',
+ componentProps: {
+ valueKey: 'id',
+ maxNumber: 10,
+ multiple: true,
+ disabled: isReadonly,
+ },
+ formItemClass: 'col-span-3',
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '鍗曟嵁缂栫爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ崟鎹紪鐮�',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '鍗曟嵁鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ崟鎹悕绉�',
+ },
+ },
+ {
+ fieldName: 'testType',
+ label: '妫�娴嬬被鍨�',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_QC_OUTSOURCE_TEST_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨妫�娴嬬被鍨�',
+ },
+ },
+ {
+ fieldName: 'result',
+ label: '妫�娴嬬粨鏋�',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_QC_OUTSOURCE_TEST_RESULT, 'number'),
+ placeholder: '璇烽�夋嫨妫�娴嬬粨鏋�',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '鑽夌', value: TEST_STATUS.DRAFT },
+ { label: '宸叉彁浜�', value: TEST_STATUS.SUBMITTED },
+ { label: '宸插畬鎴�', value: TEST_STATUS.FINISHED },
+ ],
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ },
+ {
+ fieldName: 'testDate',
+ label: '妫�娴嬫棩鏈�',
+ component: 'RangePicker',
+ componentProps: getRangePickerDefaultProps(),
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勮〃鏍煎垪 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 50 },
+ { type: 'seq', title: '搴忓彿', width: 60 },
+ {
+ field: 'code',
+ title: '鍗曟嵁缂栫爜',
+ minWidth: 150,
+ slots: { default: 'code' },
+ },
+ {
+ field: 'name',
+ title: '鍗曟嵁鍚嶇О',
+ minWidth: 180,
+ },
+ {
+ field: 'testType',
+ title: '妫�娴嬬被鍨�',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_QC_OUTSOURCE_TEST_TYPE },
+ },
+ },
+ {
+ field: 'itemName',
+ title: '妫�娴嬪璞�',
+ minWidth: 140,
+ },
+ {
+ field: 'testOrg',
+ title: '妫�娴嬫満鏋�',
+ minWidth: 160,
+ },
+ {
+ field: 'testDate',
+ title: '妫�娴嬫棩鏈�',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'result',
+ title: '妫�娴嬬粨鏋�',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_QC_OUTSOURCE_TEST_RESULT },
+ },
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 100,
+ slots: { default: 'status' },
+ },
+ {
+ field: 'action',
+ title: '鎿嶄綔',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/mes/qc/outsourcetest/index.vue b/src/views/mes/qc/outsourcetest/index.vue
new file mode 100644
index 0000000..cf9e3c7
--- /dev/null
+++ b/src/views/mes/qc/outsourcetest/index.vue
@@ -0,0 +1,210 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesQcOutsourceTestApi } from '#/api/mes/qc/outsourcetest';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { isEmpty } from '@vben/utils';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteTest,
+ finishTest,
+ getTestPage,
+ submitTest,
+} from '#/api/mes/qc/outsourcetest';
+import { $t } from '#/locales';
+
+import { TEST_STATUS, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** MES 濮斿妫�娴�/璇曢獙鍗曞垪琛� */
+defineOptions({ name: 'MesQcOutsourceTest' });
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: MesQcOutsourceTestApi.OutsourceTest) {
+ formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+function handleDetail(row: MesQcOutsourceTestApi.OutsourceTest) {
+ formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+async function handleDelete(ids: number[]) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting'),
+ duration: 0,
+ });
+ try {
+ await deleteTest(ids);
+ message.success($t('ui.actionMessage.deleteSuccess'));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+async function handleSubmit(row: MesQcOutsourceTestApi.OutsourceTest) {
+ await submitTest(row.id!);
+ message.success('宸叉彁浜�');
+ handleRefresh();
+}
+
+async function handleFinish(row: MesQcOutsourceTestApi.OutsourceTest) {
+ await finishTest(row.id!);
+ message.success('宸插畬鎴�');
+ handleRefresh();
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+ records,
+}: {
+ records: MesQcOutsourceTestApi.OutsourceTest[];
+}) {
+ checkedIds.value = records.map((item) => item.id!);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getTestPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesQcOutsourceTestApi.OutsourceTest>,
+ gridEvents: {
+ checkboxAll: handleRowCheckboxChange,
+ checkboxChange: handleRowCheckboxChange,
+ },
+});
+</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:qc-outsource-test:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: '鎵归噺鍒犻櫎',
+ type: 'primary',
+ danger: true,
+ disabled: isEmpty(checkedIds),
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:qc-outsource-test:delete'],
+ popConfirm: {
+ title: `鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛焋,
+ confirm: handleDelete.bind(null, checkedIds),
+ },
+ },
+ ]"
+ />
+ </template>
+ <template #code="{ row }">
+ <a class="link" @click="handleDetail(row)">{{ row.code }}</a>
+ </template>
+ <template #status="{ row }">
+ <Tag v-if="row.status === TEST_STATUS.DRAFT" color="default">鑽夌</Tag>
+ <Tag v-else-if="row.status === TEST_STATUS.SUBMITTED" color="processing">宸叉彁浜�</Tag>
+ <Tag v-else-if="row.status === TEST_STATUS.FINISHED" color="success">宸插畬鎴�</Tag>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.detail'),
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['mes:qc-outsource-test:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:qc-outsource-test:update'],
+ ifShow: row.status === TEST_STATUS.DRAFT,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '鎻愪氦',
+ type: 'link',
+ auth: ['mes:qc-outsource-test:update'],
+ ifShow: row.status === TEST_STATUS.DRAFT,
+ popConfirm: {
+ title: '纭鎻愪氦璇ュ崟鎹紵',
+ confirm: handleSubmit.bind(null, row),
+ },
+ },
+ {
+ label: '瀹屾垚',
+ type: 'link',
+ auth: ['mes:qc-outsource-test:update'],
+ ifShow: row.status === TEST_STATUS.SUBMITTED,
+ popConfirm: {
+ title: '纭瀹屾垚璇ュ崟鎹紵',
+ confirm: handleFinish.bind(null, row),
+ },
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:qc-outsource-test:delete'],
+ ifShow: row.status === TEST_STATUS.DRAFT,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.code]),
+ confirm: handleDelete.bind(null, [row.id!]),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/qc/outsourcetest/modules/form.vue b/src/views/mes/qc/outsourcetest/modules/form.vue
new file mode 100644
index 0000000..a69a4b4
--- /dev/null
+++ b/src/views/mes/qc/outsourcetest/modules/form.vue
@@ -0,0 +1,101 @@
+<script lang="ts" setup>
+import type { MesQcOutsourceTestApi } from '#/api/mes/qc/outsourcetest';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createTest, getTest, updateTest } from '#/api/mes/qc/outsourcetest';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesQcOutsourceTestApi.OutsourceTest>();
+const formType = ref<FormType>('create');
+
+const getTitle = computed(() => {
+ if (formType.value === 'create') {
+ return $t('ui.actionTitle.create', ['濮斿妫�娴�/璇曢獙鍗�']);
+ } else if (formType.value === 'edit') {
+ return $t('ui.actionTitle.edit', ['濮斿妫�娴�/璇曢獙鍗�']);
+ } else {
+ return '濮斿妫�娴�/璇曢獙鍗曡鎯�';
+ }
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 140,
+ },
+ wrapperClass: 'grid-cols-3',
+ layout: 'vertical',
+ schema: useFormSchema(formType.value),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data =
+ (await formApi.getValues()) as MesQcOutsourceTestApi.OutsourceTest;
+ try {
+ await (formType.value === 'create'
+ ? createTest(data)
+ : updateTest(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } 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.setDisabled(formType.value === 'detail');
+ formApi.updateSchema(useFormSchema(formType.value));
+ if (!data || !data.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getTest(data.id);
+ await formApi.setValues(formData.value);
+ if (formData.value?.attachmentList?.length) {
+ const blobIds = formData.value.attachmentList.map((item) => ({
+ uid: String(item.id),
+ name: item.name || '',
+ url: item.url || '',
+ status: 'done',
+ id: item.id,
+ }));
+ await formApi.setFieldValue('blobIds', blobIds);
+ }
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-2/3" :show-confirm-button="formType !== 'detail'">
+ <Form class="mx-3" />
+ </Modal>
+</template>
diff --git a/src/views/wls/productissue/data.ts b/src/views/wls/productissue/data.ts
index 2ee6dcc..d76c1db 100644
--- a/src/views/wls/productissue/data.ts
+++ b/src/views/wls/productissue/data.ts
@@ -93,6 +93,19 @@
rules: 'required',
},
{
+ fieldName: 'issueType',
+ label: '鍗曟嵁绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.MES_WM_PRODUCT_ISSUE_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鍗曟嵁绫诲瀷',
+ },
+ dependencies: {
+ triggerFields: ['status'],
+ show: (values) => values.status === undefined || values.status === 0,
+ },
+ },
+ {
fieldName: 'requiredTime',
label: '闇�姹傛椂闂�',
component: 'DatePicker',
@@ -196,6 +209,15 @@
minWidth: 150,
},
{
+ field: 'issueType',
+ title: '鍗曟嵁绫诲瀷',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_WM_PRODUCT_ISSUE_TYPE },
+ },
+ },
+ {
field: 'workOrderCode',
title: '鐢熶骇璁㈠崟',
minWidth: 140,
diff --git a/src/views/wls/stocktaking/task/index.vue b/src/views/wls/stocktaking/task/index.vue
index 4ffefe8..c38699d 100644
--- a/src/views/wls/stocktaking/task/index.vue
+++ b/src/views/wls/stocktaking/task/index.vue
@@ -2,6 +2,8 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmStockTakingTaskApi } from '#/api/mes/wm/stocktaking/task';
+import { ref } from 'vue';
+
import { Page, useVbenModal } from '@vben/common-ui';
import { MesWmStockTakingTaskStatusEnum } from '@vben/constants';
import { downloadFileFromBlobPart } from '@vben/utils';
@@ -15,6 +17,10 @@
exportStockTaking,
getStockTakingPage,
} from '#/api/mes/wm/stocktaking/task';
+import {
+ importStockTakingQuantity,
+ importStockTakingQuantityTemplate,
+} from '#/api/wls/stocktaking/task/line';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@@ -83,6 +89,41 @@
downloadFileFromBlobPart({ fileName: '鐩樼偣浠诲姟.xls', source: data });
}
+/** 涓嬭浇鐩樼偣瀵煎叆妯℃澘 */
+async function handleImportTemplate() {
+ const data = await importStockTakingQuantityTemplate();
+ downloadFileFromBlobPart({ fileName: '鐩樼偣瀹炵洏鏁板鍏ユā鏉�.xls', source: data });
+}
+
+const importFileRef = ref<HTMLInputElement>();
+const importTaskId = ref<number>();
+
+/** 瑙﹀彂瀵煎叆鏂囦欢閫夋嫨 */
+function handleImport(row: MesWmStockTakingTaskApi.StockTakingTask) {
+ importTaskId.value = row.id!;
+ importFileRef.value?.click();
+}
+
+/** 瀵煎叆瀹炵洏鏁版枃浠堕�夋嫨鍚庝笂浼� */
+async function handleImportFileChange(event: Event) {
+ const file = (event.target as HTMLInputElement).files?.[0];
+ if (!file || !importTaskId.value) {
+ return;
+ }
+ const hideLoading = message.loading({ content: '姝e湪瀵煎叆瀹炵洏鏁�...', duration: 0 });
+ try {
+ const result = await importStockTakingQuantity(importTaskId.value, file);
+ message.success(`瀵煎叆瀹屾垚锛氭垚鍔� ${result.successCount ?? 0} 鏉★紝澶辫触 ${result.failureCount ?? 0} 鏉);
+ if (result.failureMessages?.length) {
+ message.warning(result.failureMessages.slice(0, 5).join('锛�'));
+ }
+ handleRefresh();
+ } finally {
+ (event.target as HTMLInputElement).value = '';
+ hideLoading();
+ }
+}
+
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
@@ -137,6 +178,13 @@
auth: ['mes:wm-stock-taking-task:export'],
onClick: handleExport,
},
+ {
+ label: '瀵煎叆妯℃澘',
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:wm-stock-taking-task:query'],
+ onClick: handleImportTemplate,
+ },
]"
/>
</template>
@@ -183,6 +231,14 @@
onClick: handleExecute.bind(null, row),
},
{
+ label: '瀵煎叆瀹炵洏鏁�',
+ type: 'link',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['mes:wm-stock-taking-task:update'],
+ ifShow: row.status === StatusEnum.APPROVING,
+ onClick: handleImport.bind(null, row),
+ },
+ {
label: '鍙栨秷',
type: 'link',
danger: true,
@@ -197,5 +253,12 @@
/>
</template>
</Grid>
+ <input
+ ref="importFileRef"
+ class="hidden"
+ type="file"
+ accept=".xls,.xlsx"
+ @change="handleImportFileChange"
+ />
</Page>
</template>
diff --git a/src/views/wls/warehouse/area/data.ts b/src/views/wls/warehouse/area/data.ts
index 0d2d766..2e00eaf 100644
--- a/src/views/wls/warehouse/area/data.ts
+++ b/src/views/wls/warehouse/area/data.ts
@@ -102,6 +102,15 @@
rules: 'required',
},
{
+ fieldName: 'type',
+ label: '搴撳尯绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.MES_WM_WAREHOUSE_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨搴撳尯绫诲瀷',
+ },
+ },
+ {
fieldName: 'area',
label: '闈㈢Н锛堛帯锛�',
component: 'InputNumber',
@@ -275,6 +284,15 @@
minWidth: 160,
},
{
+ field: 'type',
+ title: '搴撳尯绫诲瀷',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_WM_WAREHOUSE_TYPE },
+ },
+ },
+ {
field: 'area',
title: '闈㈢Н锛堛帯锛�',
width: 100,
diff --git a/src/views/wls/warehouse/data.ts b/src/views/wls/warehouse/data.ts
index 6eaf201..e99d439 100644
--- a/src/views/wls/warehouse/data.ts
+++ b/src/views/wls/warehouse/data.ts
@@ -6,6 +6,7 @@
import { h } from 'vue';
import { DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
@@ -68,6 +69,15 @@
placeholder: '璇疯緭鍏ヤ粨搴撳悕绉�',
},
rules: 'required',
+ },
+ {
+ fieldName: 'type',
+ label: '浠撳簱绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.MES_WM_WAREHOUSE_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨浠撳簱绫诲瀷',
+ },
},
{
fieldName: 'chargeUserId',
@@ -175,6 +185,15 @@
minWidth: 160,
},
{
+ field: 'type',
+ title: '浠撳簱绫诲瀷',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_WM_WAREHOUSE_TYPE },
+ },
+ },
+ {
field: 'address',
title: '浠撳簱鍦板潃',
minWidth: 180,
diff --git a/vite.config.ts b/vite.config.ts
index 8404698..0d6b580 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -76,7 +76,7 @@
proxy: {
"/admin-api": {
changeOrigin: true,
- target: "http://192.168.0.10:48080",
+ target: "http://127.0.0.1:48080",
ws: true,
},
},
--
Gitblit v1.9.3