From 27c8277d717b34b55f3ea967027b53b067f77df3 Mon Sep 17 00:00:00 2001
From: xiaoyi <908147524@qq.com>
Date: 星期四, 20 八月 2026 16:58:49 +0800
Subject: [PATCH] feat 功能变更
---
src/api/bi/decision/forecast.ts | 11
src/views/mes/pro/alternativeplan/data.ts | 196 ++
src/views/hrm/workhour/index.vue | 142 ++
src/api/mes/pro/maintenancecalendar/index.ts | 59
src/views/hrm/performance-appraise/data.ts | 203 ++
src/views/hrm/performance-appraise/modules/result.vue | 138 ++
src/views/aftersales/compensation/modules/ticket-select.vue | 70 +
src/views/mes/dv/telemetry/check.vue | 168 ++
src/views/aftersales/compensation/modules/process-select-modal.vue | 78 +
src/views/hrm/performance-appraise/index.vue | 304 ++++
src/views/bi/decision/forecast-analysis/index.vue | 159 ++
src/views/mes/pro/maintenancecalendar/index.vue | 192 ++
src/views/mes/pro/maintenancecalendar/data.ts | 246 +++
src/views/mes/pro/alternativeplan/index.vue | 188 ++
src/views/hrm/performance-appraise/modules/audit-modal.vue | 91 +
src/api/hrm/work-hour-statistics/index.ts | 55
src/views/aftersales/compensation/modules/form.vue | 95 +
src/views/aftersales/compensation/data.ts | 245 +++
src/views/aftersales/compensation/modules/audit-modal.vue | 91 +
src/views/hrm/performance-appraise/modules/process-select-modal.vue | 78 +
src/api/mes/pro/alternativeplan/index.ts | 58
src/views/aftersales/compensation/index.vue | 240 +++
src/views/mes/pro/alternativeplan/modules/form.vue | 91 +
src/views/hrm/performance-appraise/modules/score.vue | 180 ++
src/api/hrm/performance-appraise/index.ts | 156 ++
src/api/aftersales/compensation.ts | 86 +
src/api/mes/dv/telemetry/index.ts | 30
src/views/hrm/performance-appraise/modules/form.vue | 95 +
src/views/mes/dv/telemetry/index.vue | 33
src/views/mes/pro/maintenancecalendar/modules/form.vue | 95 +
src/packages/constants/src/dict-enum.ts | 6
src/views/hrm/performance-appraise/modules/item.vue | 147 ++
src/views/mes/dv/machinery/modules/telemetry-list.vue | 2
33 files changed, 4,017 insertions(+), 11 deletions(-)
diff --git a/src/api/aftersales/compensation.ts b/src/api/aftersales/compensation.ts
new file mode 100644
index 0000000..4522bd7
--- /dev/null
+++ b/src/api/aftersales/compensation.ts
@@ -0,0 +1,86 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace AfterSaleCompensationApi {
+ /** 鍞悗璧斾粯/鏈嶅姟琛ュ伩鍗� */
+ export interface Compensation {
+ id?: number;
+ compensationNo?: string; // 璧斾粯鍗曞彿
+ compensationType?: number; // 璧斾粯绫诲瀷锛�1鏁呴殰璧斾粯 2鏈嶅姟琛ュ伩
+ ticketId?: number; // 鍏宠仈鍞悗宸ュ崟缂栧彿
+ customerId?: number; // 瀹㈡埛缂栧彿
+ customerName?: string; // 瀹㈡埛鍚嶇О
+ saleOrderId?: number; // 鍏宠仈閿�鍞鍗曠紪鍙�
+ amount?: number; // 璧斾粯閲戦
+ payee?: string; // 璧斾粯瀵硅薄
+ reason?: string; // 璧斾粯鍘熷洜
+ auditStatus?: number; // 瀹℃壒鐘舵�侊細0鏈彁浜� 10瀹℃壒涓� 20瀹℃牳閫氳繃 30瀹℃牳涓嶉�氳繃 40宸插彇娑�
+ auditStatusName?: string; // 瀹℃壒鐘舵�佸悕绉�
+ processInstanceId?: string; // 瀹℃壒娴佺▼瀹炰緥缂栧彿
+ auditUserId?: number; // 瀹℃牳浜�
+ auditTime?: string; // 瀹℃牳鏃堕棿
+ auditRemark?: string; // 瀹℃牳澶囨敞
+ payStatus?: number; // 鏀粯鐘舵�侊紙璐﹀姟鑱斿姩棰勭暀锛�
+ remark?: string; // 澶囨敞
+ createTime?: string;
+ }
+
+ /** 瀹℃壒鍙傛暟 */
+ export interface AuditReq {
+ id: number;
+ auditStatus: number; // 20閫氳繃 30椹冲洖
+ remark?: string;
+ }
+}
+
+/** 鍒涘缓璧斾粯/鏈嶅姟琛ュ伩鍗� */
+export function createCompensation(data: AfterSaleCompensationApi.Compensation) {
+ return requestClient.post<number>('/aftersales/compensation/create', data);
+}
+
+/** 鏇存柊璧斾粯/鏈嶅姟琛ュ伩鍗� */
+export function updateCompensation(data: AfterSaleCompensationApi.Compensation) {
+ return requestClient.put<boolean>('/aftersales/compensation/update', data);
+}
+
+/** 鍒犻櫎璧斾粯/鏈嶅姟琛ュ伩鍗� */
+export function deleteCompensation(id: number) {
+ return requestClient.delete<boolean>('/aftersales/compensation/delete', {
+ params: { id },
+ });
+}
+
+/** 鑾峰彇璧斾粯/鏈嶅姟琛ュ伩鍗� */
+export function getCompensation(id: number) {
+ return requestClient.get<AfterSaleCompensationApi.Compensation>(
+ `/aftersales/compensation/get?id=${id}`,
+ );
+}
+
+/** 鑾峰彇璧斾粯/鏈嶅姟琛ュ伩鍗曞垎椤� */
+export function getCompensationPage(params: PageParam) {
+ return requestClient.get<PageResult<AfterSaleCompensationApi.Compensation>>(
+ '/aftersales/compensation/page',
+ { params },
+ );
+}
+
+/** 鎻愪氦璧斾粯/鏈嶅姟琛ュ伩鍗曞鎵� */
+export function submitCompensation(id: number, processDefinitionKey: string) {
+ return requestClient.put<boolean>('/aftersales/compensation/submit', null, {
+ params: { id, processDefinitionKey },
+ });
+}
+
+/** 瀹℃壒璧斾粯/鏈嶅姟琛ュ伩鍗� */
+export function auditCompensation(data: AfterSaleCompensationApi.AuditReq) {
+ return requestClient.put<boolean>('/aftersales/compensation/audit', data);
+}
+
+/** 鑾峰彇璧斾粯/鏈嶅姟琛ュ伩鍗曞鎵规祦绋嬪畾涔夊垪琛� */
+export function getApproveProcessList() {
+ return requestClient.get<{ id: string; name: string; version: number; key: string }[]>(
+ '/aftersales/compensation/approve-process-list',
+ );
+}
diff --git a/src/api/bi/decision/forecast.ts b/src/api/bi/decision/forecast.ts
index e85d458..3a055ae 100644
--- a/src/api/bi/decision/forecast.ts
+++ b/src/api/bi/decision/forecast.ts
@@ -27,4 +27,15 @@
return requestClient.post('/bi/decision/forecast/generate', null, {
params: { forecastCode },
});
+}
+
+export function generateForecastWorkOrder(data: {
+ forecastId: number;
+ workOrderName?: string;
+ workOrderType: number;
+ requestDate?: string;
+ quantity: number;
+ remark?: string;
+}) {
+ return requestClient.post<number>('/bi/decision/forecast/generate-work-order', data);
}
\ No newline at end of file
diff --git a/src/api/hrm/performance-appraise/index.ts b/src/api/hrm/performance-appraise/index.ts
new file mode 100644
index 0000000..c9768cb
--- /dev/null
+++ b/src/api/hrm/performance-appraise/index.ts
@@ -0,0 +1,156 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace HrmPerformanceAppraiseApi {
+ /** 鑰冩牳鎸囨爣 */
+ export interface AppraiseItem {
+ id?: number;
+ appraiseId?: number;
+ itemName?: string; // 鎸囨爣鍚嶇О
+ weight?: number; // 鏉冮噸
+ scoringStandard?: string; // 璇勫垎鏍囧噯
+ scorerType?: number; // 璇勫垎浜虹被鍨嬶細1鑷瘎 2涓婄骇璇�
+ sort?: number; // 鎺掑簭
+ /** 璇勫垎鏃跺洖濉敤 */
+ selfScore?: number;
+ superiorScore?: number;
+ }
+
+ /** 鑰冩牳缁撴灉鎸囨爣鏄庣粏 */
+ export interface ResultItem {
+ id?: number;
+ resultId?: number;
+ itemId?: number;
+ itemName?: string;
+ weight?: number;
+ selfScore?: number;
+ superiorScore?: number;
+ }
+
+ /** 缁╂晥鑰冩牳鏂规 */
+ export interface Appraise {
+ id?: number;
+ name?: string; // 鏂规鍚嶇О
+ periodType?: number; // 鑰冩牳鍛ㄦ湡绫诲瀷锛�1鏈堝害 2瀛e害 3骞村害
+ period?: string; // 鑰冩牳鍛ㄦ湡鎻忚堪
+ scopeType?: number; // 鑰冩牳鑼冨洿绫诲瀷锛�1鍏ㄩ儴鍛樺伐 2鎸囧畾鐝粍
+ status?: number; // 鏂规鐘舵�侊細0鑽夌 10宸插彂甯� 20杩涜涓� 30宸插畬鎴�
+ statusName?: string;
+ auditStatus?: number; // 瀹℃壒鐘舵�侊細0鏈彁浜� 10瀹℃壒涓� 20閫氳繃 30椹冲洖 40宸插彇娑�
+ auditStatusName?: string;
+ processInstanceId?: string;
+ auditUserId?: number;
+ auditTime?: string;
+ auditRemark?: string;
+ remark?: string;
+ itemCount?: number; // 鑰冩牳鎸囨爣鏁�
+ resultCount?: number; // 宸茶瘎鍒嗕汉鏁�
+ createTime?: string;
+ }
+
+ /** 鑰冩牳缁撴灉锛堟寜鍛樺伐鑱氬悎锛� */
+ export interface AppraiseResult {
+ id?: number;
+ appraiseId?: number;
+ employeeId?: number;
+ employeeName?: string;
+ selfScore?: number; // 鑷瘎鎬诲垎
+ superiorScore?: number; // 涓婄骇璇勬�诲垎
+ finalScore?: number; // 缁煎悎寰楀垎
+ grade?: number; // 妗g骇锛�1浼樼 2鑹ソ 3鍚堟牸 4寰呮敼杩�
+ comment?: string; // 璇勮
+ items?: ResultItem[]; // 鎸囨爣璇勫垎鏄庣粏
+ }
+}
+
+/** 鍒涘缓缁╂晥鑰冩牳鏂规 */
+export function createAppraise(data: HrmPerformanceAppraiseApi.Appraise) {
+ return requestClient.post<number>('/hrm/performance-appraise/create', data);
+}
+
+/** 鏇存柊缁╂晥鑰冩牳鏂规 */
+export function updateAppraise(data: HrmPerformanceAppraiseApi.Appraise) {
+ return requestClient.put<boolean>('/hrm/performance-appraise/update', data);
+}
+
+/** 鍒犻櫎缁╂晥鑰冩牳鏂规 */
+export function deleteAppraise(ids: number[]) {
+ return requestClient.delete<boolean>('/hrm/performance-appraise/delete', {
+ params: { ids: ids.join(',') },
+ });
+}
+
+/** 鑾峰彇缁╂晥鑰冩牳鏂规 */
+export function getAppraise(id: number) {
+ return requestClient.get<HrmPerformanceAppraiseApi.Appraise>(
+ `/hrm/performance-appraise/get?id=${id}`,
+ );
+}
+
+/** 鑾峰彇缁╂晥鑰冩牳鏂规鍒嗛〉 */
+export function getAppraisePage(params: PageParam) {
+ return requestClient.get<PageResult<HrmPerformanceAppraiseApi.Appraise>>(
+ '/hrm/performance-appraise/page',
+ { params },
+ );
+}
+
+/** 鑾峰彇鑰冩牳鎸囨爣鍒楄〃 */
+export function getAppraiseItemList(appraiseId: number) {
+ return requestClient.get<HrmPerformanceAppraiseApi.AppraiseItem[]>(
+ `/hrm/performance-appraise/item-list?appraiseId=${appraiseId}`,
+ );
+}
+
+/** 淇濆瓨鑰冩牳鎸囨爣锛堟暣琛ㄦ浛鎹級 */
+export function saveAppraiseItems(data: {
+ appraiseId: number;
+ items: HrmPerformanceAppraiseApi.AppraiseItem[];
+}) {
+ return requestClient.post<boolean>('/hrm/performance-appraise/item-save', data);
+}
+
+/** 鎻愪氦缁╂晥鑰冩牳鏂规瀹℃壒 */
+export function submitAppraise(id: number, processDefinitionKey: string) {
+ return requestClient.post<boolean>('/hrm/performance-appraise/submit', null, {
+ params: { id, processDefinitionKey },
+ });
+}
+
+/** 瀹℃壒缁╂晥鑰冩牳鏂规 */
+export function auditAppraise(data: { id: number; auditStatus: number; remark?: string }) {
+ return requestClient.put<boolean>('/hrm/performance-appraise/audit', data);
+}
+
+/** 鑾峰彇缁╂晥鑰冩牳鏂规瀹℃壒娴佺▼瀹氫箟鍒楄〃 */
+export function getAppraiseProcessList() {
+ return requestClient.get<{ id: string; name: string; version: number; key: string }[]>(
+ '/hrm/performance-appraise/approve-process-list',
+ );
+}
+
+/** 鑾峰彇鑰冩牳缁撴灉鍒嗛〉 */
+export function getAppraiseResultPage(params: PageParam) {
+ return requestClient.get<PageResult<HrmPerformanceAppraiseApi.AppraiseResult>>(
+ '/hrm/performance-appraise/result-page',
+ { params },
+ );
+}
+
+/** 淇濆瓨鍛樺伐鑰冩牳缁撴灉锛堣嚜鍔ㄨ绠楃患鍚堝緱鍒嗕笌妗g骇锛� */
+export function saveAppraiseResult(data: {
+ appraiseId: number;
+ employeeId: number;
+ comment?: string;
+ items: { itemId: number; selfScore?: number; superiorScore?: number }[];
+}) {
+ return requestClient.post<boolean>('/hrm/performance-appraise/result-save', data);
+}
+
+/** 鑾峰彇鍛樺伐鑰冩牳缁撴灉锛堝惈鎸囨爣鏄庣粏锛� */
+export function getAppraiseResult(appraiseId: number, employeeId: number) {
+ return requestClient.get<HrmPerformanceAppraiseApi.AppraiseResult>(
+ `/hrm/performance-appraise/result?appraiseId=${appraiseId}&employeeId=${employeeId}`,
+ );
+}
diff --git a/src/api/hrm/work-hour-statistics/index.ts b/src/api/hrm/work-hour-statistics/index.ts
new file mode 100644
index 0000000..dcd73c6
--- /dev/null
+++ b/src/api/hrm/work-hour-statistics/index.ts
@@ -0,0 +1,55 @@
+import { requestClient } from '#/api/request';
+
+export namespace HrmWorkHourStatisticsApi {
+ /** 鍛樺伐宸ユ椂锛堣�冨嫟鍙e緞锛� */
+ export interface EmployeeWorkHour {
+ userId?: number;
+ userName?: string;
+ deptId?: number;
+ deptName?: string;
+ workDays?: number; // 鍑哄嫟澶╂暟
+ workHours?: number; // 姝e父宸ユ椂锛堜汉鏃讹級
+ overtimeHours?: number; // 鍔犵彮宸ユ椂锛堜汉鏃讹級
+ }
+
+ /** 鐝粍宸ユ椂锛堟姤宸ュ彛寰勶級 */
+ export interface TeamWorkHour {
+ teamId?: number;
+ teamName?: string;
+ taskCount?: number; // 浠诲姟鏁�
+ totalDuration?: number; // 鎬荤敓浜ф椂闀匡紙浜烘棩锛�
+ workHours?: number; // 鎬诲伐鏃讹紙浜烘椂锛�
+ }
+
+ export interface EmployeeWorkHourReq {
+ userId?: number;
+ deptId?: number;
+ startDate?: string;
+ endDate?: string;
+ }
+
+ export interface TeamWorkHourReq {
+ startTime?: string;
+ endTime?: string;
+ }
+}
+
+/** 鍛樺伐宸ユ椂缁熻锛堣�冨嫟鍙e緞锛� */
+export function getEmployeeWorkHourSummary(
+ params: HrmWorkHourStatisticsApi.EmployeeWorkHourReq = {},
+) {
+ return requestClient.get<HrmWorkHourStatisticsApi.EmployeeWorkHour[]>(
+ '/hrm/work-hour-statistics/employee-summary',
+ { params },
+ );
+}
+
+/** 鐝粍宸ユ椂缁熻锛堟姤宸ュ彛寰勶紝璺ㄦā鍧楄仛鍚� MES锛� */
+export function getTeamWorkHourSummary(
+ params: HrmWorkHourStatisticsApi.TeamWorkHourReq = {},
+) {
+ return requestClient.get<HrmWorkHourStatisticsApi.TeamWorkHour[]>(
+ '/hrm/work-hour-statistics/team-summary',
+ { params },
+ );
+}
diff --git a/src/api/mes/dv/telemetry/index.ts b/src/api/mes/dv/telemetry/index.ts
index 23af31b..2619c56 100644
--- a/src/api/mes/dv/telemetry/index.ts
+++ b/src/api/mes/dv/telemetry/index.ts
@@ -41,6 +41,16 @@
tbDeviceId?: string;
deviceName?: string;
}
+
+ /** 渚涚數鍙傛暟姣斿鏍¢獙缁熻锛堟寜璁惧鑱氬悎寮傚父鐜囷級 */
+ export interface CheckSummary {
+ tbDeviceId?: string; // 鏁伴噰璁惧ID
+ deviceName?: string; // 璁惧鍚嶇О
+ totalCount?: number; // 鍙傛暟鎬绘暟
+ normalCount?: number; // 姝e父鏁伴噺
+ anomalyCount?: number; // 寮傚父鏁伴噺
+ anomalyRate?: number; // 寮傚父鐜�(%)
+ }
}
/** 鎷夊彇鏁伴噰鏁版嵁骞跺叆搴擄紙杩� 5 鍒嗛挓璁惧鏁版嵁锛� */
@@ -70,3 +80,23 @@
'/mes/dv/telemetry/device-list',
);
}
+
+/** 渚涚數鍙傛暟姣斿鏍¢獙鏄庣粏锛堝悇璁惧鏈�鏂板弬鏁� vs 鏍囧噯鍊硷級 */
+export function getTelemetryCheckList(
+ params: MesDvTelemetryApi.LatestReq = {},
+) {
+ return requestClient.get<MesDvTelemetryApi.Telemetry[]>(
+ '/mes/dv/telemetry/check',
+ { params },
+ );
+}
+
+/** 渚涚數鍙傛暟姣斿鏍¢獙缁熻锛堟寜璁惧鑱氬悎寮傚父鐜囷級 */
+export function getTelemetryCheckSummary(
+ params: MesDvTelemetryApi.LatestReq = {},
+) {
+ return requestClient.get<MesDvTelemetryApi.CheckSummary[]>(
+ '/mes/dv/telemetry/check-summary',
+ { params },
+ );
+}
diff --git a/src/api/mes/pro/alternativeplan/index.ts b/src/api/mes/pro/alternativeplan/index.ts
new file mode 100644
index 0000000..4b9f4fa
--- /dev/null
+++ b/src/api/mes/pro/alternativeplan/index.ts
@@ -0,0 +1,58 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesProAlternativePlanApi {
+ /** MES 浣滀笟鏇夸唬鏂规 */
+ export interface AlternativePlan {
+ id?: number;
+ code?: string; // 鏂规缂栫爜
+ name?: string; // 鏂规鍚嶇О
+ mainProcessId?: number; // 涓诲伐搴忕紪鍙�
+ mainProcessName?: string; // 涓诲伐搴忓悕绉�
+ altProcessId?: number; // 鏇夸唬宸ュ簭缂栧彿
+ altProcessName?: string; // 鏇夸唬宸ュ簭鍚嶇О
+ altCondition?: string; // 鏇夸唬鏉′欢
+ scope?: string; // 閫傜敤鑼冨洿
+ status?: number; // 鐘舵�侊細0鑽夌 10宸插彂甯�
+ remark?: string; // 澶囨敞
+ createTime?: string;
+ }
+}
+
+/** 鍒涘缓浣滀笟鏇夸唬鏂规 */
+export function createPlan(data: MesProAlternativePlanApi.AlternativePlan) {
+ return requestClient.post('/mes/pro/alternative-plan/create', data);
+}
+
+/** 鏇存柊浣滀笟鏇夸唬鏂规 */
+export function updatePlan(data: MesProAlternativePlanApi.AlternativePlan) {
+ return requestClient.put('/mes/pro/alternative-plan/update', data);
+}
+
+/** 鍒犻櫎浣滀笟鏇夸唬鏂规 */
+export function deletePlan(ids: number[]) {
+ return requestClient.delete('/mes/pro/alternative-plan/delete', {
+ params: { ids: ids.join(',') },
+ });
+}
+
+/** 鑾峰彇浣滀笟鏇夸唬鏂规 */
+export function getPlan(id: number) {
+ return requestClient.get<MesProAlternativePlanApi.AlternativePlan>(
+ `/mes/pro/alternative-plan/get?id=${id}`,
+ );
+}
+
+/** 鑾峰彇浣滀笟鏇夸唬鏂规鍒嗛〉 */
+export function getPlanPage(params: PageParam) {
+ return requestClient.get<PageResult<MesProAlternativePlanApi.AlternativePlan>>(
+ '/mes/pro/alternative-plan/page',
+ { params },
+ );
+}
+
+/** 鍙戝竷浣滀笟鏇夸唬鏂规 */
+export function publishPlan(id: number) {
+ return requestClient.put(`/mes/pro/alternative-plan/publish?id=${id}`);
+}
diff --git a/src/api/mes/pro/maintenancecalendar/index.ts b/src/api/mes/pro/maintenancecalendar/index.ts
new file mode 100644
index 0000000..8416df7
--- /dev/null
+++ b/src/api/mes/pro/maintenancecalendar/index.ts
@@ -0,0 +1,59 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesProMaintenanceCalendarApi {
+ /** MES 鐢靛姏杩愮淮鏃ュ巻 */
+ export interface MaintenanceCalendar {
+ id?: number;
+ code?: string; // 鏃ュ巻缂栫爜
+ name?: string; // 鏃ュ巻鍚嶇О
+ workDate?: string; // 浣滀笟鏃ユ湡
+ shift?: number; // 鐝
+ workType?: number; // 浣滀笟绫诲瀷锛�1鍙樼數杩愮淮 2绾胯矾宸℃ 3鍙戠數杩愮淮 4鍌ㄨ兘鍏呮斁鐢� 5鐢电綉璋冨害
+ region?: string; // 浣滀笟鍖哄煙/绾胯矾
+ teamId?: number; // 鍊肩彮鐝粍缂栧彿
+ teamName?: string; // 鍊肩彮鐝粍鍚嶇О
+ personnel?: string; // 鍊肩彮浜哄憳
+ status?: number; // 鐘舵�侊細0鑽夌 10宸插彂甯�
+ remark?: string; // 澶囨敞
+ createTime?: string;
+ }
+}
+
+/** 鍒涘缓鐢靛姏杩愮淮鏃ュ巻 */
+export function createCalendar(data: MesProMaintenanceCalendarApi.MaintenanceCalendar) {
+ return requestClient.post('/mes/pro/maintenance-calendar/create', data);
+}
+
+/** 鏇存柊鐢靛姏杩愮淮鏃ュ巻 */
+export function updateCalendar(data: MesProMaintenanceCalendarApi.MaintenanceCalendar) {
+ return requestClient.put('/mes/pro/maintenance-calendar/update', data);
+}
+
+/** 鍒犻櫎鐢靛姏杩愮淮鏃ュ巻 */
+export function deleteCalendar(ids: number[]) {
+ return requestClient.delete('/mes/pro/maintenance-calendar/delete', {
+ params: { ids: ids.join(',') },
+ });
+}
+
+/** 鑾峰彇鐢靛姏杩愮淮鏃ュ巻 */
+export function getCalendar(id: number) {
+ return requestClient.get<MesProMaintenanceCalendarApi.MaintenanceCalendar>(
+ `/mes/pro/maintenance-calendar/get?id=${id}`,
+ );
+}
+
+/** 鑾峰彇鐢靛姏杩愮淮鏃ュ巻鍒嗛〉 */
+export function getCalendarPage(params: PageParam) {
+ return requestClient.get<PageResult<MesProMaintenanceCalendarApi.MaintenanceCalendar>>(
+ '/mes/pro/maintenance-calendar/page',
+ { params },
+ );
+}
+
+/** 鍙戝竷鐢靛姏杩愮淮鏃ュ巻 */
+export function publishCalendar(id: number) {
+ return requestClient.put(`/mes/pro/maintenance-calendar/publish?id=${id}`);
+}
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index 46210bb..09686f3 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -148,6 +148,7 @@
AFTERSALE_ISSUE_TYPE: 'aftersale_issue_type', // 鍞悗闂绫诲瀷
AFTERSALE_RETURN_TYPE: 'aftersale_return_type', // 鍞悗閫�璐х被鍨�
AFTERSALE_SEVERITY_LEVEL: 'aftersale_severity_level', // 鍞悗涓ラ噸绋嬪害
+ AFTER_SALE_COMPENSATION_TYPE: 'after_sale_compensation_type', // 鍞悗璧斾粯绫诲瀷
} as const;
/** ========== ERP - 浼佷笟璧勬簮璁″垝妯″潡 ========== */
@@ -231,6 +232,7 @@
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_PRO_CALENDAR_WORK_TYPE: 'mes_pro_calendar_work_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 缂栫爜瑙勫垯鍒嗘绫诲瀷
@@ -364,6 +366,10 @@
HRM_CONTRACT_TERM_TYPE: 'hrm_contract_term_type', // HRM 鍚堝悓鏈熼檺绫诲瀷
HRM_CONTRACT_STATUS: 'hrm_contract_status', // HRM 鍚堝悓鐘舵��
HRM_CONTRACT_TERMINATE_STATUS: 'hrm_contract_terminate_status', // HRM 鍚堝悓瑙i櫎缁堟鐘舵��
+ HRM_PERFORMANCE_PERIOD_TYPE: 'hrm_performance_period_type', // HRM 缁╂晥鑰冩牳鍛ㄦ湡绫诲瀷
+ HRM_PERFORMANCE_SCOPE_TYPE: 'hrm_performance_scope_type', // HRM 缁╂晥鑰冩牳鑼冨洿绫诲瀷
+ HRM_PERFORMANCE_SCORER_TYPE: 'hrm_performance_scorer_type', // HRM 缁╂晥鑰冩牳璇勫垎浜虹被鍨�
+ HRM_PERFORMANCE_GRADE: 'hrm_performance_grade', // HRM 缁╂晥鑰冩牳妗g骇
} as const;
/** 瀛楀吀绫诲瀷鏋氫妇 - 缁熶竴瀵煎嚭 */
diff --git a/src/views/aftersales/compensation/data.ts b/src/views/aftersales/compensation/data.ts
new file mode 100644
index 0000000..0df9b32
--- /dev/null
+++ b/src/views/aftersales/compensation/data.ts
@@ -0,0 +1,245 @@
+import type { VbenFormApi, 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 CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+import AfterSaleTicketSelect from './modules/ticket-select.vue';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 瀹℃壒鐘舵�佸父閲� */
+export const AUDIT_STATUS = {
+ DRAFT: 0,
+ PROCESS: 10,
+ APPROVE: 20,
+ REJECT: 30,
+ CANCEL: 40,
+};
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType, formApi?: VbenFormApi): VbenFormSchema[] {
+ const isReadonly = formType === 'detail';
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'compensationNo',
+ label: '璧斾粯鍗曞彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '绯荤粺鑷姩鐢熸垚',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'compensationType',
+ label: '璧斾粯绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.AFTER_SALE_COMPENSATION_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨璧斾粯绫诲瀷',
+ disabled: isReadonly,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'ticketId',
+ label: '鍏宠仈鍞悗宸ュ崟',
+ component: markRaw(AfterSaleTicketSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨鍞悗宸ュ崟',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '瀹㈡埛',
+ component: markRaw(CrmCustomerSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨瀹㈡埛',
+ disabled: isReadonly,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'saleOrderId',
+ label: '鍏宠仈閿�鍞鍗�',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 0,
+ placeholder: '璇疯緭鍏ラ攢鍞鍗曠紪鍙�',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'amount',
+ label: '璧斾粯閲戦',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ヨ禂浠橀噾棰�',
+ disabled: isReadonly,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'payee',
+ label: '璧斾粯瀵硅薄',
+ component: 'Input',
+ componentProps: {
+ placeholder: '濡傦細寮犱笁',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'reason',
+ label: '璧斾粯鍘熷洜',
+ component: 'Textarea',
+ formItemClass: 'col-span-3',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ禂浠樺師鍥�',
+ rows: 3,
+ disabled: isReadonly,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: isReadonly,
+ },
+ formItemClass: 'col-span-3',
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'compensationNo',
+ label: '璧斾粯鍗曞彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ禂浠樺崟鍙�',
+ },
+ },
+ {
+ fieldName: 'compensationType',
+ label: '璧斾粯绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.AFTER_SALE_COMPENSATION_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨璧斾粯绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '瀹㈡埛',
+ component: markRaw(CrmCustomerSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨瀹㈡埛',
+ },
+ },
+ {
+ fieldName: 'auditStatus',
+ label: '瀹℃壒鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '鏈彁浜�', value: AUDIT_STATUS.DRAFT },
+ { label: '瀹℃壒涓�', value: AUDIT_STATUS.PROCESS },
+ { label: '瀹℃牳閫氳繃', value: AUDIT_STATUS.APPROVE },
+ { label: '瀹℃牳涓嶉�氳繃', value: AUDIT_STATUS.REJECT },
+ { label: '宸插彇娑�', value: AUDIT_STATUS.CANCEL },
+ ],
+ placeholder: '璇烽�夋嫨瀹℃壒鐘舵��',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勮〃鏍煎垪 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 50 },
+ { type: 'seq', title: '搴忓彿', width: 60 },
+ {
+ field: 'compensationNo',
+ title: '璧斾粯鍗曞彿',
+ minWidth: 160,
+ slots: { default: 'compensationNo' },
+ },
+ {
+ field: 'compensationType',
+ title: '璧斾粯绫诲瀷',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.AFTER_SALE_COMPENSATION_TYPE },
+ },
+ },
+ {
+ field: 'customerName',
+ title: '瀹㈡埛鍚嶇О',
+ minWidth: 150,
+ },
+ {
+ field: 'amount',
+ title: '璧斾粯閲戦',
+ width: 110,
+ },
+ {
+ field: 'payee',
+ title: '璧斾粯瀵硅薄',
+ minWidth: 110,
+ },
+ {
+ field: 'reason',
+ title: '璧斾粯鍘熷洜',
+ minWidth: 200,
+ showOverflow: true,
+ },
+ {
+ field: 'auditStatus',
+ title: '瀹℃壒鐘舵��',
+ width: 110,
+ slots: { default: 'auditStatus' },
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'action',
+ title: '鎿嶄綔',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/aftersales/compensation/index.vue b/src/views/aftersales/compensation/index.vue
new file mode 100644
index 0000000..d540693
--- /dev/null
+++ b/src/views/aftersales/compensation/index.vue
@@ -0,0 +1,240 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { AfterSaleCompensationApi } from '#/api/aftersales/compensation';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+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 {
+ deleteCompensation,
+ getApproveProcessList,
+ getCompensationPage,
+} from '#/api/aftersales/compensation';
+import { $t } from '#/locales';
+
+import { AUDIT_STATUS, useGridColumns, useGridFormSchema } from './data';
+import AuditModal from './modules/audit-modal.vue';
+import Form from './modules/form.vue';
+import ProcessSelectModal from './modules/process-select-modal.vue';
+
+/** 鍞悗璧斾粯/鏈嶅姟琛ュ伩鍗曞垪琛� */
+defineOptions({ name: 'AfterSaleCompensation' });
+
+const router = useRouter();
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+ connectedComponent: ProcessSelectModal,
+ destroyOnClose: true,
+});
+
+const [AuditFormModal, auditModalApi] = useVbenModal({
+ connectedComponent: AuditModal,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: AfterSaleCompensationApi.Compensation) {
+ formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+function handleDetail(row: AfterSaleCompensationApi.Compensation) {
+ formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+async function handleDelete(id: number) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting'),
+ duration: 0,
+ });
+ try {
+ await deleteCompensation(id);
+ message.success($t('ui.actionMessage.deleteSuccess'));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛� */
+async function handleSubmit(row: AfterSaleCompensationApi.Compensation) {
+ const hideLoading = message.loading({
+ content: '鑾峰彇瀹℃壒娴佺▼...',
+ duration: 0,
+ });
+ try {
+ const processList = await getApproveProcessList();
+ hideLoading();
+ if (!processList || processList.length === 0) {
+ message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堥厤缃� BPM 娴佺▼');
+ return;
+ }
+ processModalApi.setData({ id: row.id, processList }).open();
+ } catch {
+ hideLoading();
+ }
+}
+
+/** 瀹℃壒锛堥�氳繃/椹冲洖锛� */
+function handleAudit(row: AfterSaleCompensationApi.Compensation) {
+ auditModalApi.setData({ id: row.id }).open();
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleViewProcess(row: AfterSaleCompensationApi.Compensation) {
+ if (!row.processInstanceId) {
+ message.warning('璇ュ崟鎹皻鏈彁浜ゅ鎵�');
+ return;
+ }
+ router.push({
+ name: 'BpmProcessInstanceDetail',
+ query: { id: row.processInstanceId },
+ });
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+ records,
+}: {
+ records: AfterSaleCompensationApi.Compensation[];
+}) {
+ 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 getCompensationPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<AfterSaleCompensationApi.Compensation>,
+ gridEvents: {
+ checkboxAll: handleRowCheckboxChange,
+ checkboxChange: handleRowCheckboxChange,
+ },
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <ProcessModal @success="handleRefresh" />
+ <AuditFormModal @success="handleRefresh" />
+ <Grid table-title="璧斾粯/鏈嶅姟琛ュ伩鍗曞垪琛�">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['璧斾粯/鏈嶅姟琛ュ伩鍗�']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['aftersales:compensation:create'],
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </template>
+ <template #compensationNo="{ row }">
+ <a class="link" @click="handleDetail(row)">{{ row.compensationNo }}</a>
+ </template>
+ <template #auditStatus="{ row }">
+ <Tag v-if="row.auditStatus === AUDIT_STATUS.DRAFT" color="default">鏈彁浜�</Tag>
+ <Tag v-else-if="row.auditStatus === AUDIT_STATUS.PROCESS" color="processing">瀹℃壒涓�</Tag>
+ <Tag v-else-if="row.auditStatus === AUDIT_STATUS.APPROVE" color="success">瀹℃牳閫氳繃</Tag>
+ <Tag v-else-if="row.auditStatus === AUDIT_STATUS.REJECT" color="error">瀹℃牳涓嶉�氳繃</Tag>
+ <Tag v-else-if="row.auditStatus === AUDIT_STATUS.CANCEL" color="warning">宸插彇娑�</Tag>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.detail'),
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['aftersales:compensation:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['aftersales:compensation:update'],
+ ifShow: row.auditStatus === AUDIT_STATUS.DRAFT,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '鎻愪氦瀹℃壒',
+ type: 'link',
+ auth: ['aftersales:compensation:submit'],
+ ifShow: row.auditStatus === AUDIT_STATUS.DRAFT || row.auditStatus === AUDIT_STATUS.REJECT,
+ onClick: handleSubmit.bind(null, row),
+ },
+ {
+ label: '瀹℃壒',
+ type: 'link',
+ auth: ['aftersales:compensation:audit'],
+ ifShow: row.auditStatus === AUDIT_STATUS.PROCESS,
+ onClick: handleAudit.bind(null, row),
+ },
+ {
+ label: '鏌ョ湅瀹℃壒',
+ type: 'link',
+ ifShow: row.auditStatus !== AUDIT_STATUS.DRAFT && !!row.processInstanceId,
+ onClick: handleViewProcess.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['aftersales:compensation:delete'],
+ ifShow: row.auditStatus === AUDIT_STATUS.DRAFT,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.compensationNo]),
+ confirm: handleDelete.bind(null, row.id!),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/aftersales/compensation/modules/audit-modal.vue b/src/views/aftersales/compensation/modules/audit-modal.vue
new file mode 100644
index 0000000..6b8e5ad
--- /dev/null
+++ b/src/views/aftersales/compensation/modules/audit-modal.vue
@@ -0,0 +1,91 @@
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { auditCompensation } from '#/api/aftersales/compensation';
+
+const emit = defineEmits(['success']);
+
+const compensationId = ref<number>();
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 100,
+ },
+ wrapperClass: 'grid-cols-1',
+ layout: 'vertical',
+ schema: [
+ {
+ fieldName: 'auditStatus',
+ label: '瀹℃壒缁撴灉',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '閫氳繃', value: 20 },
+ { label: '椹冲洖', value: 30 },
+ ],
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'remark',
+ label: '瀹℃壒澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ鎵瑰娉�',
+ rows: 3,
+ },
+ },
+ ],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ const values = await formApi.getValues<{
+ auditStatus: number;
+ remark?: string;
+ }>();
+ await auditCompensation({
+ id: compensationId.value!,
+ auditStatus: values.auditStatus,
+ remark: values.remark,
+ });
+ await modalApi.close();
+ emit('success');
+ message.success('瀹℃壒瀹屾垚');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ compensationId.value = undefined;
+ await formApi.resetForm();
+ return;
+ }
+ const data = modalApi.getData<{ id: number }>();
+ compensationId.value = data?.id;
+ await formApi.setValues({ auditStatus: 20 });
+ },
+});
+</script>
+
+<template>
+ <Modal title="瀹℃壒璧斾粯/鏈嶅姟琛ュ伩鍗�" class="w-1/3">
+ <Form class="mx-3" />
+ </Modal>
+</template>
diff --git a/src/views/aftersales/compensation/modules/form.vue b/src/views/aftersales/compensation/modules/form.vue
new file mode 100644
index 0000000..eb5fb22
--- /dev/null
+++ b/src/views/aftersales/compensation/modules/form.vue
@@ -0,0 +1,95 @@
+<script lang="ts" setup>
+import type { AfterSaleCompensationApi } from '#/api/aftersales/compensation';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createCompensation,
+ getCompensation,
+ updateCompensation,
+} from '#/api/aftersales/compensation';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<AfterSaleCompensationApi.Compensation>();
+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: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data =
+ (await formApi.getValues()) as AfterSaleCompensationApi.Compensation;
+ try {
+ await (formType.value === 'create'
+ ? createCompensation(data)
+ : updateCompensation(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, formApi));
+ if (!data || !data.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getCompensation(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/aftersales/compensation/modules/process-select-modal.vue b/src/views/aftersales/compensation/modules/process-select-modal.vue
new file mode 100644
index 0000000..9ec118e
--- /dev/null
+++ b/src/views/aftersales/compensation/modules/process-select-modal.vue
@@ -0,0 +1,78 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Select } from 'ant-design-vue';
+
+import { submitCompensation } from '#/api/aftersales/compensation';
+
+interface ProcessDefinition {
+ id: string;
+ name: string;
+ version: number;
+ key: string;
+}
+
+const emit = defineEmits(['success']);
+
+const compensationId = 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 submitCompensation(compensationId.value!, selectedProcessKey.value);
+ await modalApi.close();
+ emit('success');
+ message.success('鎻愪氦鎴愬姛');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ compensationId.value = undefined;
+ selectedProcessKey.value = '';
+ processList.value = [];
+ return;
+ }
+ const data = modalApi.getData<{ id: number; processList: ProcessDefinition[] }>();
+ if (!data) {
+ return;
+ }
+ compensationId.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/aftersales/compensation/modules/ticket-select.vue b/src/views/aftersales/compensation/modules/ticket-select.vue
new file mode 100644
index 0000000..15361a5
--- /dev/null
+++ b/src/views/aftersales/compensation/modules/ticket-select.vue
@@ -0,0 +1,70 @@
+<script lang="ts" setup>
+import type { AftersalesTicketApi } from '#/api/aftersales/ticket';
+
+import { computed, onMounted, ref } from 'vue';
+
+import { Select } from 'ant-design-vue';
+
+import { getTicketPage } from '#/api/aftersales/ticket';
+
+defineOptions({ name: 'AfterSaleTicketSelect', 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 ticketList = ref<AftersalesTicketApi.Ticket[]>([]);
+
+const selectValue = computed({
+ get: () => props.modelValue,
+ set: (value: number | undefined) => {
+ emit('update:modelValue', value);
+ },
+});
+
+async function loadTickets() {
+ const { list } = await getTicketPage({ pageNo: 1, pageSize: 200 });
+ ticketList.value = list || [];
+}
+
+onMounted(loadTickets);
+</script>
+
+<template>
+ <Select
+ v-bind="$attrs"
+ v-model:value="selectValue"
+ :allow-clear="allowClear"
+ :disabled="disabled"
+ :field-names="{ label: 'no', value: 'id' }"
+ :options="ticketList"
+ :placeholder="placeholder"
+ class="w-full"
+ option-filter-prop="no"
+ show-search
+ >
+ <template #option="{ value: id, label: no }">
+ <div class="flex items-center gap-2">
+ <span>{{ no }}</span>
+ <span class="text-xs text-gray-400">
+ {{ ticketList.find((t) => t.id === id)?.name }}
+ </span>
+ </div>
+ </template>
+ </Select>
+</template>
diff --git a/src/views/bi/decision/forecast-analysis/index.vue b/src/views/bi/decision/forecast-analysis/index.vue
index 413733b..404c042 100644
--- a/src/views/bi/decision/forecast-analysis/index.vue
+++ b/src/views/bi/decision/forecast-analysis/index.vue
@@ -1,12 +1,16 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
-import { Page } from '@vben/common-ui';
+import { Page, useVbenModal } from '@vben/common-ui';
-import { DatePicker, Select, Spin } from 'ant-design-vue';
+import { DatePicker, message, Select, Spin } from 'ant-design-vue';
import dayjs from 'dayjs';
-import { getForecastList } from '#/api/bi/decision/forecast';
+import { useVbenForm } from '#/adapter/form';
+import {
+ generateForecastWorkOrder,
+ getForecastList,
+} from '#/api/bi/decision/forecast';
defineOptions({ name: 'DecisionForecastAnalysis' });
@@ -18,6 +22,12 @@
{ label: '鏃ヨ礋鑽烽娴�', value: 'load_forecast_daily' },
{ label: '鍛ㄨ礋鑽烽娴�', value: 'load_forecast_weekly' },
{ label: '鏈堣礋鑽烽娴�', value: 'load_forecast_monthly' },
+];
+
+const WORK_ORDER_TYPE_OPTIONS = [
+ { label: '鐢靛姏杩愮淮', value: 4 },
+ { label: '鍙戠數', value: 5 },
+ { label: '妫�淇�', value: 6 },
];
async function loadData() {
@@ -32,10 +42,135 @@
}
onMounted(loadData);
+
+/** ========== 鐢熸垚宸ュ崟 ========== */
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 100,
+ },
+ wrapperClass: 'grid-cols-1',
+ layout: 'vertical',
+ schema: [
+ {
+ fieldName: 'forecastId',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'workOrderName',
+ label: '宸ュ崟鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ伐鍗曞悕绉�',
+ },
+ },
+ {
+ fieldName: 'workOrderType',
+ label: '宸ュ崟绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: WORK_ORDER_TYPE_OPTIONS,
+ placeholder: '璇烽�夋嫨宸ュ崟绫诲瀷',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'requestDate',
+ label: '闇�姹傛棩鏈�',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '璇烽�夋嫨闇�姹傛棩鏈�',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ },
+ },
+ {
+ fieldName: 'quantity',
+ label: '鐢熶骇鏁伴噺',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ョ敓浜ф暟閲�',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 3,
+ },
+ },
+ ],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ const values = await formApi.getValues<{
+ forecastId: number;
+ workOrderName?: string;
+ workOrderType: number;
+ requestDate?: string;
+ quantity: number;
+ remark?: string;
+ }>();
+ await generateForecastWorkOrder({
+ forecastId: Number(values.forecastId),
+ workOrderName: values.workOrderName,
+ workOrderType: Number(values.workOrderType),
+ requestDate: values.requestDate,
+ quantity: Number(values.quantity ?? 1),
+ remark: values.remark,
+ });
+ await modalApi.close();
+ message.success('宸ュ崟鐢熸垚鎴愬姛');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ return;
+ }
+ const data = modalApi.getData<{ item: any }>();
+ const item = data?.item;
+ await formApi.resetForm();
+ await formApi.setValues({
+ forecastId: item?.id,
+ workOrderType: 4,
+ requestDate: item?.pointTime ?? dayjs().format('YYYY-MM-DD HH:mm:ss'),
+ quantity: item?.forecastValue ?? 1,
+ remark: `鐢辫礋鑽烽娴嬶紙${item?.forecastName ?? forecastCode.value}锛夎嚜鍔ㄧ敓鎴恅,
+ });
+ },
+});
+
+function handleGenerate(item: any) {
+ modalApi.setData({ item }).open();
+}
</script>
<template>
<Page :auto-content-height="true">
+ <Modal title="璐熻嵎棰勬祴鐢熸垚鐢熶骇宸ュ崟" class="w-1/2">
+ <Form class="mx-3" />
+ </Modal>
<div class="p-4">
<div class="mb-4 flex items-center gap-4">
<h2 class="text-lg font-bold">璐熻嵎棰勬祴</h2>
@@ -52,7 +187,11 @@
鏆傛棤棰勬祴鏁版嵁锛岃鍏堥厤缃� KPI 鎸囨爣鍜岄璀﹁鍒欏悗鏌ョ湅
</div>
<div v-else class="grid grid-cols-1 gap-4 lg:grid-cols-2">
- <div v-for="item in forecastData" :key="item.id" class="rounded-lg border p-4">
+ <div
+ v-for="item in forecastData"
+ :key="item.id"
+ class="rounded-lg border p-4"
+ >
<div class="mb-2 flex items-center justify-between">
<span class="font-medium">{{ item.forecastName }}</span>
<span class="text-xs text-gray-400">{{ item.pointTime }}</span>
@@ -70,9 +209,19 @@
<div v-if="item.dimension" class="mt-1 text-xs text-gray-400">
{{ item.dimension }}: {{ item.dimensionValue }}
</div>
+ <div class="mt-3">
+ <a-button
+ type="primary"
+ ghost
+ size="small"
+ @click="handleGenerate(item)"
+ >
+ 鐢熸垚宸ュ崟
+ </a-button>
+ </div>
</div>
</div>
</Spin>
</div>
</Page>
-</template>
\ No newline at end of file
+</template>
diff --git a/src/views/hrm/performance-appraise/data.ts b/src/views/hrm/performance-appraise/data.ts
new file mode 100644
index 0000000..3ea1477
--- /dev/null
+++ b/src/views/hrm/performance-appraise/data.ts
@@ -0,0 +1,203 @@
+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 APPRAISE_STATUS = {
+ DRAFT: 0,
+ PUBLISHED: 10,
+ PROCESSING: 20,
+ FINISHED: 30,
+};
+
+/** 瀹℃壒鐘舵�佸父閲� */
+export const AUDIT_STATUS = {
+ DRAFT: 0,
+ PROCESS: 10,
+ APPROVE: 20,
+ REJECT: 30,
+ CANCEL: 40,
+};
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+ const isReadonly = formType === 'detail';
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '鏂规鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ柟妗堝悕绉�',
+ disabled: isReadonly,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'periodType',
+ label: '鑰冩牳鍛ㄦ湡绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.HRM_PERFORMANCE_PERIOD_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鑰冩牳鍛ㄦ湡绫诲瀷',
+ disabled: isReadonly,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'period',
+ label: '鑰冩牳鍛ㄦ湡',
+ component: 'Input',
+ componentProps: {
+ placeholder: '濡傦細2026-08',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'scopeType',
+ label: '鑰冩牳鑼冨洿',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.HRM_PERFORMANCE_SCOPE_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鑰冩牳鑼冨洿',
+ disabled: isReadonly,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-3',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ autoSize: { minRows: 1, maxRows: 1 },
+ disabled: isReadonly,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '鏂规鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ柟妗堝悕绉�',
+ },
+ },
+ {
+ fieldName: 'periodType',
+ label: '鑰冩牳鍛ㄦ湡绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.HRM_PERFORMANCE_PERIOD_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鑰冩牳鍛ㄦ湡绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鏂规鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '鑽夌', value: APPRAISE_STATUS.DRAFT },
+ { label: '宸插彂甯�', value: APPRAISE_STATUS.PUBLISHED },
+ { label: '杩涜涓�', value: APPRAISE_STATUS.PROCESSING },
+ { label: '宸插畬鎴�', value: APPRAISE_STATUS.FINISHED },
+ ],
+ placeholder: '璇烽�夋嫨鏂规鐘舵��',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勮〃鏍煎垪 */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'checkbox', width: 50 },
+ { type: 'seq', title: '搴忓彿', width: 60 },
+ {
+ field: 'name',
+ title: '鏂规鍚嶇О',
+ minWidth: 200,
+ slots: { default: 'name' },
+ },
+ {
+ field: 'periodType',
+ title: '鑰冩牳鍛ㄦ湡绫诲瀷',
+ width: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.HRM_PERFORMANCE_PERIOD_TYPE },
+ },
+ },
+ {
+ field: 'period',
+ title: '鑰冩牳鍛ㄦ湡',
+ width: 110,
+ },
+ {
+ field: 'scopeType',
+ title: '鑰冩牳鑼冨洿',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.HRM_PERFORMANCE_SCOPE_TYPE },
+ },
+ },
+ {
+ field: 'status',
+ title: '鏂规鐘舵��',
+ width: 100,
+ slots: { default: 'status' },
+ },
+ {
+ field: 'auditStatus',
+ title: '瀹℃壒鐘舵��',
+ width: 100,
+ slots: { default: 'auditStatus' },
+ },
+ {
+ field: 'itemCount',
+ title: '鎸囨爣鏁�',
+ width: 80,
+ },
+ {
+ field: 'resultCount',
+ title: '宸茶瘎鍒嗕汉鏁�',
+ width: 100,
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 170,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'action',
+ title: '鎿嶄綔',
+ width: 320,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/hrm/performance-appraise/index.vue b/src/views/hrm/performance-appraise/index.vue
new file mode 100644
index 0000000..c6f1597
--- /dev/null
+++ b/src/views/hrm/performance-appraise/index.vue
@@ -0,0 +1,304 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmPerformanceAppraiseApi } from '#/api/hrm/performance-appraise';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+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 {
+ deleteAppraise,
+ getAppraisePage,
+ getAppraiseProcessList,
+} from '#/api/hrm/performance-appraise';
+import { $t } from '#/locales';
+
+import { APPRAISE_STATUS, AUDIT_STATUS, useGridColumns, useGridFormSchema } from './data';
+import AuditModal from './modules/audit-modal.vue';
+import Form from './modules/form.vue';
+import ItemModal from './modules/item.vue';
+import ProcessSelectModal from './modules/process-select-modal.vue';
+import ResultModal from './modules/result.vue';
+import ScoreModal from './modules/score.vue';
+
+/** HRM 缁╂晥鑰冩牳鏂规鍒楄〃 */
+defineOptions({ name: 'HrmPerformanceAppraise' });
+
+const router = useRouter();
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ItemFormModal, itemModalApi] = useVbenModal({
+ connectedComponent: ItemModal,
+ destroyOnClose: true,
+});
+
+const [ScoreFormModal, scoreModalApi] = useVbenModal({
+ connectedComponent: ScoreModal,
+ destroyOnClose: true,
+});
+
+const [ResultFormModal, resultModalApi] = useVbenModal({
+ connectedComponent: ResultModal,
+ destroyOnClose: true,
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+ connectedComponent: ProcessSelectModal,
+ destroyOnClose: true,
+});
+
+const [AuditFormModal, auditModalApi] = useVbenModal({
+ connectedComponent: AuditModal,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: HrmPerformanceAppraiseApi.Appraise) {
+ formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+function handleDetail(row: HrmPerformanceAppraiseApi.Appraise) {
+ formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+function handleItems(row: HrmPerformanceAppraiseApi.Appraise) {
+ itemModalApi.setData({ id: row.id }).open();
+}
+
+function handleScore(row: HrmPerformanceAppraiseApi.Appraise) {
+ scoreModalApi.setData({ id: row.id }).open();
+}
+
+function handleResult(row: HrmPerformanceAppraiseApi.Appraise) {
+ resultModalApi.setData({ id: row.id }).open();
+}
+
+async function handleDelete(ids: number[]) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting'),
+ duration: 0,
+ });
+ try {
+ await deleteAppraise(ids);
+ message.success($t('ui.actionMessage.deleteSuccess'));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛� */
+async function handleSubmit(row: HrmPerformanceAppraiseApi.Appraise) {
+ const hideLoading = message.loading({
+ content: '鑾峰彇瀹℃壒娴佺▼...',
+ duration: 0,
+ });
+ try {
+ const processList = await getAppraiseProcessList();
+ hideLoading();
+ if (!processList || processList.length === 0) {
+ message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堥厤缃� BPM 娴佺▼');
+ return;
+ }
+ processModalApi.setData({ id: row.id, processList }).open();
+ } catch {
+ hideLoading();
+ }
+}
+
+/** 瀹℃壒锛堥�氳繃/椹冲洖锛� */
+function handleAudit(row: HrmPerformanceAppraiseApi.Appraise) {
+ auditModalApi.setData({ id: row.id }).open();
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleViewProcess(row: HrmPerformanceAppraiseApi.Appraise) {
+ if (!row.processInstanceId) {
+ message.warning('璇ユ柟妗堝皻鏈彁浜ゅ鎵�');
+ return;
+ }
+ router.push({
+ name: 'BpmProcessInstanceDetail',
+ query: { id: row.processInstanceId },
+ });
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+ records,
+}: {
+ records: HrmPerformanceAppraiseApi.Appraise[];
+}) {
+ 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 getAppraisePage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<HrmPerformanceAppraiseApi.Appraise>,
+ gridEvents: {
+ checkboxAll: handleRowCheckboxChange,
+ checkboxChange: handleRowCheckboxChange,
+ },
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <ItemFormModal @success="handleRefresh" />
+ <ScoreFormModal @success="handleRefresh" />
+ <ResultFormModal @success="handleRefresh" />
+ <ProcessModal @success="handleRefresh" />
+ <AuditFormModal @success="handleRefresh" />
+ <Grid table-title="缁╂晥鑰冩牳鏂规鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['缁╂晥鑰冩牳鏂规']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['hrm:performance-appraise:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: '鎵归噺鍒犻櫎',
+ type: 'primary',
+ danger: true,
+ disabled: isEmpty(checkedIds),
+ icon: ACTION_ICON.DELETE,
+ auth: ['hrm:performance-appraise:delete'],
+ popConfirm: {
+ title: `鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛焋,
+ confirm: handleDelete.bind(null, checkedIds),
+ },
+ },
+ ]"
+ />
+ </template>
+ <template #name="{ row }">
+ <a class="link" @click="handleDetail(row)">{{ row.name }}</a>
+ </template>
+ <template #status="{ row }">
+ <Tag v-if="row.status === APPRAISE_STATUS.DRAFT" color="default">鑽夌</Tag>
+ <Tag v-else-if="row.status === APPRAISE_STATUS.PUBLISHED" color="processing">宸插彂甯�</Tag>
+ <Tag v-else-if="row.status === APPRAISE_STATUS.PROCESSING" color="warning">杩涜涓�</Tag>
+ <Tag v-else-if="row.status === APPRAISE_STATUS.FINISHED" color="success">宸插畬鎴�</Tag>
+ </template>
+ <template #auditStatus="{ row }">
+ <Tag v-if="row.auditStatus === AUDIT_STATUS.DRAFT" color="default">鏈彁浜�</Tag>
+ <Tag v-else-if="row.auditStatus === AUDIT_STATUS.PROCESS" color="processing">瀹℃壒涓�</Tag>
+ <Tag v-else-if="row.auditStatus === AUDIT_STATUS.APPROVE" color="success">閫氳繃</Tag>
+ <Tag v-else-if="row.auditStatus === AUDIT_STATUS.REJECT" color="error">椹冲洖</Tag>
+ <Tag v-else-if="row.auditStatus === AUDIT_STATUS.CANCEL" color="warning">宸插彇娑�</Tag>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '鎸囨爣',
+ type: 'link',
+ auth: ['hrm:performance-appraise:update'],
+ ifShow: row.status === APPRAISE_STATUS.DRAFT,
+ onClick: handleItems.bind(null, row),
+ },
+ {
+ label: '璇勫垎',
+ type: 'link',
+ auth: ['hrm:performance-appraise:update'],
+ ifShow: row.status === APPRAISE_STATUS.PUBLISHED || row.status === APPRAISE_STATUS.PROCESSING,
+ onClick: handleScore.bind(null, row),
+ },
+ {
+ label: '缁撴灉',
+ type: 'link',
+ ifShow: (row.resultCount ?? 0) > 0,
+ onClick: handleResult.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['hrm:performance-appraise:update'],
+ ifShow: row.status === APPRAISE_STATUS.DRAFT,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '鎻愪氦瀹℃壒',
+ type: 'link',
+ auth: ['hrm:performance-appraise:submit'],
+ ifShow: row.auditStatus === AUDIT_STATUS.DRAFT || row.auditStatus === AUDIT_STATUS.REJECT,
+ onClick: handleSubmit.bind(null, row),
+ },
+ {
+ label: '瀹℃壒',
+ type: 'link',
+ auth: ['hrm:performance-appraise:audit'],
+ ifShow: row.auditStatus === AUDIT_STATUS.PROCESS,
+ onClick: handleAudit.bind(null, row),
+ },
+ {
+ label: '鏌ョ湅瀹℃壒',
+ type: 'link',
+ ifShow: row.auditStatus !== AUDIT_STATUS.DRAFT && !!row.processInstanceId,
+ onClick: handleViewProcess.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['hrm:performance-appraise:delete'],
+ ifShow: row.status === APPRAISE_STATUS.DRAFT,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+ confirm: handleDelete.bind(null, [row.id!]),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/hrm/performance-appraise/modules/audit-modal.vue b/src/views/hrm/performance-appraise/modules/audit-modal.vue
new file mode 100644
index 0000000..9bc18f5
--- /dev/null
+++ b/src/views/hrm/performance-appraise/modules/audit-modal.vue
@@ -0,0 +1,91 @@
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { auditAppraise } from '#/api/hrm/performance-appraise';
+
+const emit = defineEmits(['success']);
+
+const appraiseId = ref<number>();
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 100,
+ },
+ wrapperClass: 'grid-cols-1',
+ layout: 'vertical',
+ schema: [
+ {
+ fieldName: 'auditStatus',
+ label: '瀹℃壒缁撴灉',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '閫氳繃', value: 20 },
+ { label: '椹冲洖', value: 30 },
+ ],
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'remark',
+ label: '瀹℃壒澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ鎵瑰娉�',
+ rows: 3,
+ },
+ },
+ ],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ const values = await formApi.getValues<{
+ auditStatus: number;
+ remark?: string;
+ }>();
+ await auditAppraise({
+ id: appraiseId.value!,
+ auditStatus: values.auditStatus,
+ remark: values.remark,
+ });
+ await modalApi.close();
+ emit('success');
+ message.success('瀹℃壒瀹屾垚');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ appraiseId.value = undefined;
+ await formApi.resetForm();
+ return;
+ }
+ const data = modalApi.getData<{ id: number }>();
+ appraiseId.value = data?.id;
+ await formApi.setValues({ auditStatus: 20 });
+ },
+});
+</script>
+
+<template>
+ <Modal title="瀹℃壒缁╂晥鑰冩牳鏂规" class="w-1/3">
+ <Form class="mx-3" />
+ </Modal>
+</template>
diff --git a/src/views/hrm/performance-appraise/modules/form.vue b/src/views/hrm/performance-appraise/modules/form.vue
new file mode 100644
index 0000000..c2979f3
--- /dev/null
+++ b/src/views/hrm/performance-appraise/modules/form.vue
@@ -0,0 +1,95 @@
+<script lang="ts" setup>
+import type { HrmPerformanceAppraiseApi } from '#/api/hrm/performance-appraise';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createAppraise,
+ getAppraise,
+ updateAppraise,
+} from '#/api/hrm/performance-appraise';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<HrmPerformanceAppraiseApi.Appraise>();
+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 HrmPerformanceAppraiseApi.Appraise;
+ try {
+ await (formType.value === 'create'
+ ? createAppraise(data)
+ : updateAppraise(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 getAppraise(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/hrm/performance-appraise/modules/item.vue b/src/views/hrm/performance-appraise/modules/item.vue
new file mode 100644
index 0000000..c017d22
--- /dev/null
+++ b/src/views/hrm/performance-appraise/modules/item.vue
@@ -0,0 +1,147 @@
+<script lang="ts" setup>
+import type { HrmPerformanceAppraiseApi } from '#/api/hrm/performance-appraise';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import {
+ getAppraiseItemList,
+ saveAppraiseItems,
+} from '#/api/hrm/performance-appraise';
+
+const emit = defineEmits(['success']);
+
+const appraiseId = ref<number>();
+const items = ref<HrmPerformanceAppraiseApi.AppraiseItem[]>([]);
+
+const SCORER_TYPE_OPTIONS = [
+ { label: '鑷瘎', value: 1 },
+ { label: '涓婄骇璇�', value: 2 },
+];
+
+function addRow() {
+ items.value.push({
+ itemName: '',
+ weight: undefined,
+ scoringStandard: '',
+ scorerType: 1,
+ sort: items.value.length + 1,
+ });
+}
+
+function removeRow(index: number) {
+ items.value.splice(index, 1);
+ items.value.forEach((item, idx) => {
+ item.sort = idx + 1;
+ });
+}
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ // 鏍¢獙锛氭寚鏍囧悕绉颁笌鏉冮噸蹇呭~
+ const invalid = items.value.some(
+ (item) => !item.itemName || item.weight == null,
+ );
+ if (invalid) {
+ message.warning('璇峰~鍐欏畬鏁寸殑鎸囨爣鍚嶇О涓庢潈閲�');
+ return;
+ }
+ modalApi.lock();
+ try {
+ await saveAppraiseItems({ appraiseId: appraiseId.value!, items: items.value });
+ await modalApi.close();
+ emit('success');
+ message.success('鎸囨爣淇濆瓨鎴愬姛');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ appraiseId.value = undefined;
+ items.value = [];
+ return;
+ }
+ const data = modalApi.getData<{ id: number }>();
+ appraiseId.value = data?.id;
+ modalApi.lock();
+ try {
+ items.value = await getAppraiseItemList(appraiseId.value!);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal title="閰嶇疆鑰冩牳鎸囨爣" class="w-3/4">
+ <div class="mb-3 flex items-center justify-between">
+ <span class="text-sm text-gray-500">璁剧疆鑰冩牳鎸囨爣鍚嶇О銆佹潈閲嶄笌璇勫垎鏍囧噯锛屾潈閲嶅悎璁″缓璁� 100</span>
+ <a-button type="primary" @click="addRow">娣诲姞鎸囨爣</a-button>
+ </div>
+ <a-table
+ :data-source="items"
+ :pagination="false"
+ :scroll="{ y: 360 }"
+ bordered
+ row-key="sort"
+ size="small"
+ >
+ <a-table-column title="鎸囨爣鍚嶇О" width="220">
+ <template #default="{ record }">
+ <a-input
+ v-model:value="record.itemName"
+ placeholder="璇疯緭鍏ユ寚鏍囧悕绉�"
+ />
+ </template>
+ </a-table-column>
+ <a-table-column title="鏉冮噸" width="120">
+ <template #default="{ record }">
+ <a-input-number
+ v-model:value="record.weight"
+ :min="0"
+ :precision="2"
+ class="!w-full"
+ placeholder="鏉冮噸"
+ />
+ </template>
+ </a-table-column>
+ <a-table-column title="璇勫垎鏍囧噯" min-width="220">
+ <template #default="{ record }">
+ <a-input
+ v-model:value="record.scoringStandard"
+ placeholder="璇疯緭鍏ヨ瘎鍒嗘爣鍑�"
+ />
+ </template>
+ </a-table-column>
+ <a-table-column title="璇勫垎浜�" width="120">
+ <template #default="{ record }">
+ <a-select
+ v-model:value="record.scorerType"
+ :options="SCORER_TYPE_OPTIONS"
+ class="!w-full"
+ />
+ </template>
+ </a-table-column>
+ <a-table-column title="鎺掑簭" width="90">
+ <template #default="{ record }">
+ <a-input-number
+ v-model:value="record.sort"
+ :min="1"
+ :precision="0"
+ class="!w-full"
+ />
+ </template>
+ </a-table-column>
+ <a-table-column title="鎿嶄綔" width="80" align="center">
+ <template #default="{ index }">
+ <a-button type="link" danger @click="removeRow(index)">鍒犻櫎</a-button>
+ </template>
+ </a-table-column>
+ </a-table>
+ </Modal>
+</template>
diff --git a/src/views/hrm/performance-appraise/modules/process-select-modal.vue b/src/views/hrm/performance-appraise/modules/process-select-modal.vue
new file mode 100644
index 0000000..c712ae8
--- /dev/null
+++ b/src/views/hrm/performance-appraise/modules/process-select-modal.vue
@@ -0,0 +1,78 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Select } from 'ant-design-vue';
+
+import { submitAppraise } from '#/api/hrm/performance-appraise';
+
+interface ProcessDefinition {
+ id: string;
+ name: string;
+ version: number;
+ key: string;
+}
+
+const emit = defineEmits(['success']);
+
+const appraiseId = 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 submitAppraise(appraiseId.value!, selectedProcessKey.value);
+ await modalApi.close();
+ emit('success');
+ message.success('鎻愪氦鎴愬姛');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ appraiseId.value = undefined;
+ selectedProcessKey.value = '';
+ processList.value = [];
+ return;
+ }
+ const data = modalApi.getData<{ id: number; processList: ProcessDefinition[] }>();
+ if (!data) {
+ return;
+ }
+ appraiseId.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/performance-appraise/modules/result.vue b/src/views/hrm/performance-appraise/modules/result.vue
new file mode 100644
index 0000000..ab45867
--- /dev/null
+++ b/src/views/hrm/performance-appraise/modules/result.vue
@@ -0,0 +1,138 @@
+<script lang="ts" setup>
+import type { HrmPerformanceAppraiseApi } from '#/api/hrm/performance-appraise';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Modal } from 'ant-design-vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictLabel } from '@vben/hooks';
+
+import { getAppraiseResultPage } from '#/api/hrm/performance-appraise';
+
+const emit = defineEmits(['success']);
+
+const appraiseId = ref<number>();
+const resultList = ref<HrmPerformanceAppraiseApi.AppraiseResult[]>([]);
+const total = ref(0);
+const pageNo = ref(1);
+const pageSize = ref(10);
+const loading = ref(false);
+
+const detailOpen = ref(false);
+const detailRows = ref<HrmPerformanceAppraiseApi.ResultItem[]>([]);
+
+function getGradeLabel(grade?: number): string {
+ return grade == null ? '-' : getDictLabel(DICT_TYPE.HRM_PERFORMANCE_GRADE, grade);
+}
+
+async function loadResults() {
+ loading.value = true;
+ try {
+ const data = await getAppraiseResultPage({
+ appraiseId: appraiseId.value!,
+ pageNo: pageNo.value,
+ pageSize: pageSize.value,
+ });
+ resultList.value = data.list ?? [];
+ total.value = data.total ?? 0;
+ } finally {
+ loading.value = false;
+ }
+}
+
+function handlePageChange(page: number, size: number) {
+ pageNo.value = page;
+ pageSize.value = size;
+ loadResults();
+}
+
+function handleDetail(row: HrmPerformanceAppraiseApi.AppraiseResult) {
+ detailRows.value = row.items ?? [];
+ detailOpen.value = true;
+}
+
+const [ModalRoot, modalApi] = useVbenModal({
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ appraiseId.value = undefined;
+ resultList.value = [];
+ total.value = 0;
+ pageNo.value = 1;
+ return;
+ }
+ const data = modalApi.getData<{ id: number }>();
+ appraiseId.value = data?.id;
+ await loadResults();
+ },
+});
+</script>
+
+<template>
+ <ModalRoot title="鑰冩牳缁撴灉" class="w-3/4">
+ <div class="mb-3 text-sm text-gray-500">
+ 缁煎悎寰楀垎 = 危(鏉冮噸 脳 鍗曟寚鏍囧緱鍒�) / 危鏉冮噸锛涙。绾� 鈮�90 浼樼 / 鈮�80 鑹ソ / 鈮�60 鍚堟牸 / <60 寰呮敼杩�
+ </div>
+ <a-table
+ :data-source="resultList"
+ :loading="loading"
+ :pagination="{
+ current: pageNo,
+ pageSize,
+ total,
+ showSizeChanger: true,
+ showTotal: (t: number) => `鍏� ${t} 浜篳,
+ }"
+ :scroll="{ y: 380 }"
+ bordered
+ row-key="id"
+ size="small"
+ @change="(pag: { current?: number; pageSize?: number }) =>
+ handlePageChange(pag.current ?? 1, pag.pageSize ?? 10)"
+ >
+ <a-table-column title="鍛樺伐" data-index="employeeName" min-width="120" />
+ <a-table-column title="鑷瘎鎬诲垎" data-index="selfScore" width="100" />
+ <a-table-column title="涓婄骇璇勬�诲垎" data-index="superiorScore" width="110" />
+ <a-table-column title="缁煎悎寰楀垎" data-index="finalScore" width="100">
+ <template #default="{ record }">
+ <span class="font-medium">{{ record.finalScore ?? '-' }}</span>
+ </template>
+ </a-table-column>
+ <a-table-column title="妗g骇" width="90">
+ <template #default="{ record }">
+ <a-tag :color="record.grade === 1 ? 'success' : record.grade === 2 ? 'processing' : record.grade === 3 ? 'warning' : 'error'">
+ {{ getGradeLabel(record.grade) }}
+ </a-tag>
+ </template>
+ </a-table-column>
+ <a-table-column title="璇勮" data-index="comment" min-width="160" ellipsis />
+ <a-table-column title="鎿嶄綔" width="90" align="center">
+ <template #default="{ record }">
+ <a-button type="link" @click="handleDetail(record)">鏄庣粏</a-button>
+ </template>
+ </a-table-column>
+ </a-table>
+
+ <Modal
+ v-model:open="detailOpen"
+ title="鎸囨爣璇勫垎鏄庣粏"
+ width="640px"
+ :footer="null"
+ >
+ <a-table
+ :data-source="detailRows"
+ :pagination="false"
+ bordered
+ row-key="id"
+ size="small"
+ >
+ <a-table-column title="鎸囨爣鍚嶇О" data-index="itemName" min-width="160" />
+ <a-table-column title="鏉冮噸" data-index="weight" width="90" />
+ <a-table-column title="鑷瘎寰楀垎" data-index="selfScore" width="100" />
+ <a-table-column title="涓婄骇璇勫緱鍒�" data-index="superiorScore" width="110" />
+ </a-table>
+ </Modal>
+ </ModalRoot>
+</template>
diff --git a/src/views/hrm/performance-appraise/modules/score.vue b/src/views/hrm/performance-appraise/modules/score.vue
new file mode 100644
index 0000000..1a2a2c7
--- /dev/null
+++ b/src/views/hrm/performance-appraise/modules/score.vue
@@ -0,0 +1,180 @@
+<script lang="ts" setup>
+import type { HrmPerformanceAppraiseApi } from '#/api/hrm/performance-appraise';
+
+import { markRaw, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import {
+ getAppraiseItemList,
+ getAppraiseResult,
+ saveAppraiseResult,
+} from '#/api/hrm/performance-appraise';
+import { HrmEmployeeSelect } from '#/views/hrm/employee/components';
+
+const emit = defineEmits(['success']);
+
+const appraiseId = ref<number>();
+const employeeId = ref<number>();
+const items = ref<HrmPerformanceAppraiseApi.AppraiseItem[]>([]);
+const comment = ref<string>('');
+
+const SCORER_TYPE_OPTIONS = [
+ { label: '鑷瘎', value: 1 },
+ { label: '涓婄骇璇�', value: 2 },
+];
+
+/** 鍔犺浇鎸囨爣鍒楄〃锛堜粎鍦ㄩ娆℃墦寮�鏃讹級 */
+async function loadItems() {
+ items.value = await getAppraiseItemList(appraiseId.value!);
+}
+
+/** 鍒囨崲鍛樺伐鏃跺洖濉凡鏈夎瘎鍒� */
+async function handleEmployeeChange(item: { id?: number } | undefined) {
+ employeeId.value = item?.id;
+ comment.value = '';
+ if (!item?.id) {
+ items.value.forEach((it) => {
+ it.selfScore = undefined;
+ it.superiorScore = undefined;
+ });
+ return;
+ }
+ const result = await getAppraiseResult(appraiseId.value!, item.id);
+ if (result?.items) {
+ items.value.forEach((it) => {
+ const detail = result.items!.find((ri) => ri.itemId === it.id);
+ it.selfScore = detail?.selfScore;
+ it.superiorScore = detail?.superiorScore;
+ });
+ comment.value = result.comment ?? '';
+ } else {
+ items.value.forEach((it) => {
+ it.selfScore = undefined;
+ it.superiorScore = undefined;
+ });
+ }
+}
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (!employeeId.value) {
+ message.warning('璇烽�夋嫨鍛樺伐');
+ return;
+ }
+ modalApi.lock();
+ try {
+ await saveAppraiseResult({
+ appraiseId: appraiseId.value!,
+ employeeId: employeeId.value!,
+ comment: comment.value,
+ items: items.value.map((item) => ({
+ itemId: item.id!,
+ selfScore: item.selfScore,
+ superiorScore: item.superiorScore,
+ })),
+ });
+ await modalApi.close();
+ emit('success');
+ message.success('璇勫垎淇濆瓨鎴愬姛锛屽凡鑷姩璁$畻缁煎悎寰楀垎');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ appraiseId.value = undefined;
+ employeeId.value = undefined;
+ items.value = [];
+ comment.value = '';
+ return;
+ }
+ const data = modalApi.getData<{ id: number }>();
+ appraiseId.value = data?.id;
+ modalApi.lock();
+ try {
+ await loadItems();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal title="鍛樺伐鑰冩牳璇勫垎" class="w-3/4">
+ <div class="mb-4 flex items-center gap-4">
+ <span class="shrink-0">鑰冩牳鍛樺伐锛�</span>
+ <HrmEmployeeSelect
+ :model-value="employeeId"
+ class="!w-72"
+ @change="handleEmployeeChange"
+ @update:model-value="(value: number | undefined) => (employeeId = value)"
+ />
+ </div>
+ <a-table
+ :data-source="items"
+ :pagination="false"
+ :scroll="{ y: 300 }"
+ bordered
+ row-key="id"
+ size="small"
+ >
+ <a-table-column title="鎸囨爣鍚嶇О" min-width="180">
+ <template #default="{ record }">
+ <span>{{ record.itemName }}</span>
+ </template>
+ </a-table-column>
+ <a-table-column title="鏉冮噸" width="90">
+ <template #default="{ record }">
+ <span>{{ record.weight }}</span>
+ </template>
+ </a-table-column>
+ <a-table-column title="璇勫垎浜�" width="90">
+ <template #default="{ record }">
+ <span>{{ record.scorerType === 1 ? '鑷瘎' : '涓婄骇璇�' }}</span>
+ </template>
+ </a-table-column>
+ <a-table-column title="鑷瘎寰楀垎" width="130">
+ <template #default="{ record }">
+ <a-input-number
+ v-model:value="record.selfScore"
+ :min="0"
+ :max="100"
+ :precision="1"
+ class="!w-full"
+ placeholder="鑷瘎寰楀垎"
+ />
+ </template>
+ </a-table-column>
+ <a-table-column title="涓婄骇璇勫緱鍒�" width="130">
+ <template #default="{ record }">
+ <a-input-number
+ v-model:value="record.superiorScore"
+ :min="0"
+ :max="100"
+ :precision="1"
+ class="!w-full"
+ placeholder="涓婄骇璇勫緱鍒�"
+ />
+ </template>
+ </a-table-column>
+ <a-table-column title="璇勫垎鏍囧噯" min-width="180">
+ <template #default="{ record }">
+ <span class="text-xs text-gray-400">{{ record.scoringStandard || '-' }}</span>
+ </template>
+ </a-table-column>
+ </a-table>
+ <div class="mt-3">
+ <span class="mr-2">璇勮锛�</span>
+ <a-textarea
+ v-model:value="comment"
+ :rows="2"
+ placeholder="璇疯緭鍏ヨ瘎璇�"
+ class="!w-full"
+ />
+ </div>
+ </Modal>
+</template>
diff --git a/src/views/hrm/workhour/index.vue b/src/views/hrm/workhour/index.vue
new file mode 100644
index 0000000..0b5c5b6
--- /dev/null
+++ b/src/views/hrm/workhour/index.vue
@@ -0,0 +1,142 @@
+<script lang="ts" setup>
+ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+ import type { HrmWorkHourStatisticsApi } from '#/api/hrm/work-hour-statistics';
+
+ import { onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue';
+
+ import { Page } from '@vben/common-ui';
+ import { useVbenVxeGrid } from '#/adapter/vxe-table';
+
+ import { Tabs } from 'ant-design-vue';
+
+ import {
+ getEmployeeWorkHourSummary,
+ getTeamWorkHourSummary,
+ } from '#/api/hrm/work-hour-statistics';
+
+ /** HRM 宸ユ椂缁熻锛堣�冨嫟鍙e緞 + 鎶ュ伐鍙e緞锛� */
+ defineOptions({ name: 'HrmWorkHourStatistics' });
+
+ const activeTab = ref<'employee' | 'team'>('employee');
+
+ const contentRef = useTemplateRef('contentRef');
+ let resizeObserver: ResizeObserver | undefined;
+
+ /** 璁$畻琛ㄦ牸楂樺害锛氬唴瀹瑰尯鍙敤楂樺害 - Tabs 瀵艰埅绛夊浐瀹氬紑閿�锛岃〃鏍兼拺婊¢〉闈� */
+ function calcTableHeight() {
+ const el = contentRef.value;
+ if (!el) return;
+ const height = Math.max(el.clientHeight - 115, 240);
+ employeeGridApi.setGridOptions({ height });
+ teamGridApi.setGridOptions({ height });
+ }
+
+ onMounted(() => {
+ calcTableHeight();
+ resizeObserver = new ResizeObserver(calcTableHeight);
+ if (contentRef.value) resizeObserver.observe(contentRef.value);
+ });
+
+ onBeforeUnmount(() => {
+ resizeObserver?.disconnect();
+ });
+
+ const [EmployeeGrid, employeeGridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: [
+ {
+ fieldName: 'dateRange',
+ label: '鏃ユ湡鑼冨洿',
+ component: 'RangePicker',
+ componentProps: {
+ class: '!w-full',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: ['寮�濮嬫棩鏈�', '缁撴潫鏃ユ湡'],
+ },
+ },
+ ],
+ },
+ gridOptions: {
+ columns: [
+ { field: 'userName', title: '鍛樺伐濮撳悕', minWidth: 140 },
+ { field: 'deptName', title: '閮ㄩ棬', minWidth: 140 },
+ { field: 'workDays', title: '鍑哄嫟澶╂暟', width: 110 },
+ { field: 'workHours', title: '姝e父宸ユ椂(浜烘椂)', width: 150 },
+ { field: 'overtimeHours', title: '鍔犵彮宸ユ椂(浜烘椂)', width: 150 },
+ ],
+ height: 400,
+ keepSource: true,
+ // 杩斿洖瑁告暟缁勶紝闇�鍏抽棴鍒嗛〉
+ pagerConfig: { enabled: false },
+ proxyConfig: {
+ ajax: {
+ query: async (_params, formValues) => {
+ const range = formValues?.dateRange ?? [];
+ return await getEmployeeWorkHourSummary({
+ startDate: range?.[0],
+ endDate: range?.[1],
+ });
+ },
+ },
+ },
+ rowConfig: { keyField: 'userId', isHover: true },
+ toolbarConfig: { refresh: true, search: true },
+ } as VxeTableGridOptions<HrmWorkHourStatisticsApi.EmployeeWorkHour>,
+ });
+
+ const [TeamGrid, teamGridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: [
+ {
+ fieldName: 'timeRange',
+ label: '鏃ユ湡鑼冨洿',
+ component: 'RangePicker',
+ componentProps: {
+ class: '!w-full',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ placeholder: ['寮�濮嬫椂闂�', '缁撴潫鏃堕棿'],
+ },
+ },
+ ],
+ },
+ gridOptions: {
+ columns: [
+ { field: 'teamName', title: '鐝粍鍚嶇О', minWidth: 160 },
+ { field: 'taskCount', title: '浠诲姟鏁�', width: 100 },
+ { field: 'totalDuration', title: '鎬荤敓浜ф椂闀�(浜烘棩)', width: 160 },
+ { field: 'workHours', title: '鎬诲伐鏃�(浜烘椂)', width: 150 },
+ ],
+ height: 400,
+ keepSource: true,
+ pagerConfig: { enabled: false },
+ proxyConfig: {
+ ajax: {
+ query: async (_params, formValues) => {
+ const range = formValues?.timeRange ?? [];
+ return await getTeamWorkHourSummary({
+ startTime: range?.[0],
+ endTime: range?.[1],
+ });
+ },
+ },
+ },
+ rowConfig: { keyField: 'teamId', isHover: true },
+ toolbarConfig: { refresh: true, search: true },
+ } as VxeTableGridOptions<HrmWorkHourStatisticsApi.TeamWorkHour>,
+ });
+</script>
+
+<template>
+ <Page auto-content-height>
+ <div ref="contentRef" class="flex h-full w-full flex-col">
+ <Tabs v-model:active-key="activeTab" class="min-h-0 flex-1">
+ <Tabs.TabPane key="employee" tab="鍛樺伐宸ユ椂锛堣�冨嫟鍙e緞锛�">
+ <EmployeeGrid table-title="鍛樺伐宸ユ椂缁熻" />
+ </Tabs.TabPane>
+ <Tabs.TabPane key="team" tab="鐝粍宸ユ椂锛堟姤宸ュ彛寰勶級">
+ <TeamGrid table-title="鐝粍宸ユ椂缁熻" />
+ </Tabs.TabPane>
+ </Tabs>
+ </div>
+ </Page>
+</template>
diff --git a/src/views/mes/dv/machinery/modules/telemetry-list.vue b/src/views/mes/dv/machinery/modules/telemetry-list.vue
index da1de2c..b852866 100644
--- a/src/views/mes/dv/machinery/modules/telemetry-list.vue
+++ b/src/views/mes/dv/machinery/modules/telemetry-list.vue
@@ -18,6 +18,8 @@
columns: useLatestColumns(),
height: 320,
keepSource: true,
+ // /latest 杩斿洖瑁告暟缁勶紝闇�鍏抽棴鍒嗛〉锛屽惁鍒� vxe 浼氭寜 {list,total} 瑙f瀽瀵艰嚧涓嶅睍绀�
+ pagerConfig: { enabled: false },
proxyConfig: {
ajax: {
query: async () =>
diff --git a/src/views/mes/dv/telemetry/check.vue b/src/views/mes/dv/telemetry/check.vue
new file mode 100644
index 0000000..141678f
--- /dev/null
+++ b/src/views/mes/dv/telemetry/check.vue
@@ -0,0 +1,168 @@
+<script lang="ts" setup>
+ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+ import type { MesDvTelemetryApi } from '#/api/mes/dv/telemetry';
+
+ import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue';
+
+ import { Page } from '@vben/common-ui';
+ import { useVbenVxeGrid } from '#/adapter/vxe-table';
+
+ import { Button, Card, Select, Statistic, Tag } from 'ant-design-vue';
+
+ import {
+ getTelemetryCheckList,
+ getTelemetryCheckSummary,
+ getTelemetryDeviceList,
+ } from '#/api/mes/dv/telemetry';
+
+ /** MES 渚涚數鍙傛暟姣斿鏍¢獙锛堝悇璁惧鏈�鏂板弬鏁� vs 鏍囧噯鍊硷級 */
+ defineOptions({ name: 'MesDvTelemetryCheck' });
+
+ const devices = ref<MesDvTelemetryApi.DeviceItem[]>([]);
+ const selectedDeviceId = ref<string>();
+ const summaryList = ref<MesDvTelemetryApi.CheckSummary[]>([]);
+
+ const contentRef = useTemplateRef('contentRef');
+ let resizeObserver: ResizeObserver | undefined;
+
+ /** 璁$畻琛ㄦ牸楂樺害锛氬唴瀹瑰尯鍙敤楂樺害 - 璁惧閫夋嫨琛�/缁熻鍗$墖/宸ュ叿鏍忕瓑鍥哄畾寮�閿� */
+ function calcTableHeight() {
+ const el = contentRef.value;
+ if (!el) return;
+ const height = Math.max(el.clientHeight - 230, 240);
+ checkGridApi.setGridOptions({ height });
+ }
+
+ onMounted(async () => {
+ calcTableHeight();
+ resizeObserver = new ResizeObserver(calcTableHeight);
+ if (contentRef.value) resizeObserver.observe(contentRef.value);
+ devices.value = await getTelemetryDeviceList();
+ await loadSummary();
+ });
+
+ onBeforeUnmount(() => resizeObserver?.disconnect());
+
+ async function loadSummary() {
+ summaryList.value = await getTelemetryCheckSummary({
+ tbDeviceId: selectedDeviceId.value,
+ });
+ }
+
+ /** 閫夎澶囨垨鍒锋柊锛氶噸杞界粺璁″崱鐗� + 鏄庣粏琛� */
+ async function refreshAll() {
+ await loadSummary();
+ checkGridApi.query();
+ }
+
+ /** 閫変腑璁惧鐨勭粺璁★紱鏈�夋嫨鍒欐眹鎬诲叏閮ㄨ澶� */
+ const currentSummary = computed(() => {
+ if (selectedDeviceId.value) {
+ return (
+ summaryList.value.find((s) => s.tbDeviceId === selectedDeviceId.value) ??
+ {}
+ );
+ }
+ return summaryList.value.reduce(
+ (acc, s) => {
+ acc.totalCount = (acc.totalCount ?? 0) + (s.totalCount ?? 0);
+ acc.normalCount = (acc.normalCount ?? 0) + (s.normalCount ?? 0);
+ acc.anomalyCount = (acc.anomalyCount ?? 0) + (s.anomalyCount ?? 0);
+ return acc;
+ },
+ { totalCount: 0, normalCount: 0, anomalyCount: 0 } as MesDvTelemetryApi.CheckSummary,
+ );
+ });
+
+ const anomalyRate = computed(() => {
+ const total = currentSummary.value.totalCount ?? 0;
+ if (total <= 0) return 0;
+ return Number(
+ (((currentSummary.value.anomalyCount ?? 0) * 100) / total).toFixed(1),
+ );
+ });
+
+ const [CheckGrid, checkGridApi] = useVbenVxeGrid({
+ gridOptions: {
+ columns: [
+ { field: 'deviceName', title: '璁惧鍚嶇О', minWidth: 150 },
+ { field: 'paramName', title: '淇″彿鍚嶇О', minWidth: 140 },
+ { field: 'paramKeyName', title: '璁惧KEY鍚嶇О', minWidth: 130 },
+ { field: 'standardValue', title: '鏍囧噯鍊�', width: 110 },
+ { field: 'timelyValue', title: '瀹炴椂鍊�', width: 110 },
+ { field: 'avgValue', title: '鍧囧��', width: 110 },
+ { field: 'maxValue', title: '鏈�澶у��', width: 110 },
+ { field: 'minValue', title: '鏈�灏忓��', width: 110 },
+ {
+ field: 'whetherAnomaly',
+ title: '鏄惁寮傚父',
+ width: 100,
+ slots: { default: 'anomaly' },
+ },
+ ],
+ height: 400,
+ keepSource: true,
+ // /check 杩斿洖瑁告暟缁勶紝闇�鍏抽棴鍒嗛〉
+ pagerConfig: { enabled: false },
+ proxyConfig: {
+ ajax: {
+ query: async () =>
+ await getTelemetryCheckList({
+ tbDeviceId: selectedDeviceId.value,
+ }),
+ },
+ },
+ rowConfig: { keyField: 'id', isHover: true },
+ toolbarConfig: { refresh: true },
+ } as VxeTableGridOptions<MesDvTelemetryApi.Telemetry>,
+ });
+</script>
+
+<template>
+ <Page auto-content-height>
+ <div ref="contentRef" class="flex h-full w-full flex-col p-4">
+ <div class="mb-4 flex flex-wrap items-center gap-4">
+ <Select
+ v-model:value="selectedDeviceId"
+ class="w-72"
+ allow-clear
+ placeholder="璇烽�夋嫨璁惧锛堜笉閫夊垯灞曠ず鍏ㄩ儴璁惧锛�"
+ :options="
+ devices.map((d) => ({ label: d.deviceName, value: d.tbDeviceId }))
+ "
+ @change="refreshAll"
+ />
+ <Button @click="refreshAll">鍒锋柊</Button>
+ </div>
+ <div class="mb-4 grid grid-cols-2 gap-4 lg:grid-cols-4">
+ <Card>
+ <Statistic title="鍙傛暟鎬绘暟" :value="currentSummary.totalCount ?? 0" />
+ </Card>
+ <Card>
+ <Statistic
+ title="姝e父"
+ :value="currentSummary.normalCount ?? 0"
+ :value-style="{ color: '#52c41a' }"
+ />
+ </Card>
+ <Card>
+ <Statistic
+ title="寮傚父"
+ :value="currentSummary.anomalyCount ?? 0"
+ :value-style="{ color: '#ff4d4f' }"
+ />
+ </Card>
+ <Card>
+ <Statistic title="寮傚父鐜�" :value="anomalyRate" suffix="%" />
+ </Card>
+ </div>
+ <CheckGrid table-title="渚涚數鍙傛暟姣斿鏄庣粏" class="min-h-0 flex-1">
+ <template #anomaly="{ row }">
+ <Tag :color="row.whetherAnomaly ? 'red' : 'green'">
+ {{ row.whetherAnomaly ? '寮傚父' : '姝e父' }}
+ </Tag>
+ </template>
+ </CheckGrid>
+ </div>
+ </Page>
+</template>
diff --git a/src/views/mes/dv/telemetry/index.vue b/src/views/mes/dv/telemetry/index.vue
index 69e3c82..27b8ef1 100644
--- a/src/views/mes/dv/telemetry/index.vue
+++ b/src/views/mes/dv/telemetry/index.vue
@@ -2,7 +2,7 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesDvTelemetryApi } from '#/api/mes/dv/telemetry';
- import { onBeforeUnmount, ref } from 'vue';
+ import { onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
@@ -24,11 +24,31 @@
const lastUpdateTime = ref<Date>();
let refreshTimer: ReturnType<typeof setInterval> | undefined;
+ const contentRef = useTemplateRef('contentRef');
+ let resizeObserver: ResizeObserver | undefined;
+
+ /** 璁$畻琛ㄦ牸楂樺害锛氬唴瀹瑰尯鍙敤楂樺害 - 宸ュ叿鏍忚/Tabs瀵艰埅绛夊浐瀹氬紑閿�锛屼繚璇佽〃鏍兼拺婊¢〉闈紙瀵归綈璁惧鍙拌处椤碉級 */
+ function calcTableHeight() {
+ const el = contentRef.value;
+ if (!el) return;
+ const height = Math.max(el.clientHeight - 115, 240);
+ latestGridApi.setGridOptions({ height });
+ historyGridApi.setGridOptions({ height });
+ }
+
+ onMounted(() => {
+ calcTableHeight();
+ resizeObserver = new ResizeObserver(calcTableHeight);
+ if (contentRef.value) resizeObserver.observe(contentRef.value);
+ });
+
const [LatestGrid, latestGridApi] = useVbenVxeGrid({
gridOptions: {
columns: useLatestColumns(),
- height: 'auto',
+ height: 400,
keepSource: true,
+ // /latest 杩斿洖瑁告暟缁勶紝闇�鍏抽棴鍒嗛〉锛屽惁鍒� vxe 浼氭寜 {list,total} 瑙f瀽瀵艰嚧涓嶅睍绀�
+ pagerConfig: { enabled: false },
proxyConfig: {
ajax: {
query: async () => await getLatestTelemetry(),
@@ -43,7 +63,7 @@
formOptions: { schema: useHistoryGridFormSchema() },
gridOptions: {
columns: useHistoryColumns(),
- height: 'auto',
+ height: 400,
keepSource: true,
proxyConfig: {
ajax: {
@@ -85,7 +105,7 @@
}
/** 鍒囨崲瀹炴椂鑷姩鍒锋柊锛堟瘡 30 绉掞級 */
- function toggleAutoRefresh(checked: boolean) {
+ function toggleAutoRefresh(checked: boolean | string | number) {
if (checked) {
refreshTimer = setInterval(() => {
if (activeTab.value === 'latest') {
@@ -99,6 +119,7 @@
}
onBeforeUnmount(() => {
+ resizeObserver?.disconnect();
if (refreshTimer) {
clearInterval(refreshTimer);
}
@@ -107,7 +128,7 @@
<template>
<Page auto-content-height>
- <div class="p-4">
+ <div ref="contentRef" class="flex h-full w-full flex-col">
<div class="mb-4 flex items-center gap-4">
<Button type="primary" :loading="pulling" @click="handlePull">
鎷夊彇鏈�鏂版暟鎹�
@@ -122,7 +143,7 @@
鏈�杩戞媺鍙栵細{{ dayjs(lastUpdateTime).format('YYYY-MM-DD HH:mm:ss') }}
</span>
</div>
- <Tabs v-model:active-key="activeTab">
+ <Tabs v-model:active-key="activeTab" class="min-h-0 flex-1">
<Tabs.TabPane key="latest" tab="瀹炴椂鏁版嵁">
<LatestGrid table-title="瀹炴椂鏁版嵁">
<template #anomaly="{ row }">
diff --git a/src/views/mes/pro/alternativeplan/data.ts b/src/views/mes/pro/alternativeplan/data.ts
new file mode 100644
index 0000000..f712018
--- /dev/null
+++ b/src/views/mes/pro/alternativeplan/data.ts
@@ -0,0 +1,196 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { markRaw } from 'vue';
+
+import { ProProcessSelect } from '#/views/mes/pro/process/components';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 鐘舵�佸父閲� */
+export const PLAN_STATUS = {
+ DRAFT: 0,
+ PUBLISHED: 10,
+};
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType, formApi?: VbenFormApi): 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: 'mainProcessId',
+ label: '涓诲伐搴�',
+ component: markRaw(ProProcessSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨涓诲伐搴�',
+ disabled: isReadonly,
+ onChange: (item: { name?: string } | undefined) => {
+ formApi?.setFieldValue('mainProcessName', item?.name ?? '');
+ },
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'altProcessId',
+ label: '鏇夸唬宸ュ簭',
+ component: markRaw(ProProcessSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨鏇夸唬宸ュ簭',
+ disabled: isReadonly,
+ onChange: (item: { name?: string } | undefined) => {
+ formApi?.setFieldValue('altProcessName', item?.name ?? '');
+ },
+ },
+ },
+ {
+ fieldName: 'altCondition',
+ label: '鏇夸唬鏉′欢',
+ component: 'Input',
+ componentProps: {
+ placeholder: '濡傦細璁惧鏁呴殰/璐熻嵎鍙楅檺/浜哄憳涓嶈冻',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'scope',
+ label: '閫傜敤鑼冨洿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '濡傦細110kV 绾胯矾',
+ disabled: isReadonly,
+ },
+ },
+ {
+ 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: '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: 'mainProcessName',
+ title: '涓诲伐搴�',
+ minWidth: 130,
+ },
+ {
+ field: 'altProcessName',
+ title: '鏇夸唬宸ュ簭',
+ minWidth: 130,
+ },
+ {
+ field: 'altCondition',
+ title: '鏇夸唬鏉′欢',
+ minWidth: 200,
+ },
+ {
+ field: 'scope',
+ title: '閫傜敤鑼冨洿',
+ 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/alternativeplan/index.vue b/src/views/mes/pro/alternativeplan/index.vue
new file mode 100644
index 0000000..3cb6506
--- /dev/null
+++ b/src/views/mes/pro/alternativeplan/index.vue
@@ -0,0 +1,188 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProAlternativePlanApi } from '#/api/mes/pro/alternativeplan';
+
+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/alternativeplan';
+import { $t } from '#/locales';
+
+import { PLAN_STATUS, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** MES 浣滀笟鏇夸唬鏂规鍒楄〃 */
+defineOptions({ name: 'MesProAlternativePlan' });
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: MesProAlternativePlanApi.AlternativePlan) {
+ formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+function handleDetail(row: MesProAlternativePlanApi.AlternativePlan) {
+ 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: MesProAlternativePlanApi.AlternativePlan) {
+ await publishPlan(row.id!);
+ message.success('宸插彂甯�');
+ handleRefresh();
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+ records,
+}: {
+ records: MesProAlternativePlanApi.AlternativePlan[];
+}) {
+ 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<MesProAlternativePlanApi.AlternativePlan>,
+ 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-alternative-plan:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: '鎵归噺鍒犻櫎',
+ type: 'primary',
+ danger: true,
+ disabled: isEmpty(checkedIds),
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pro-alternative-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-alternative-plan:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pro-alternative-plan:update'],
+ ifShow: row.status === PLAN_STATUS.DRAFT,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '鍙戝竷',
+ type: 'link',
+ auth: ['mes:pro-alternative-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-alternative-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/alternativeplan/modules/form.vue b/src/views/mes/pro/alternativeplan/modules/form.vue
new file mode 100644
index 0000000..b5d912a
--- /dev/null
+++ b/src/views/mes/pro/alternativeplan/modules/form.vue
@@ -0,0 +1,91 @@
+<script lang="ts" setup>
+import type { MesProAlternativePlanApi } from '#/api/mes/pro/alternativeplan';
+
+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/alternativeplan';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProAlternativePlanApi.AlternativePlan>();
+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: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data =
+ (await formApi.getValues()) as MesProAlternativePlanApi.AlternativePlan;
+ 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, formApi));
+ 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/maintenancecalendar/data.ts b/src/views/mes/pro/maintenancecalendar/data.ts
new file mode 100644
index 0000000..f578aac
--- /dev/null
+++ b/src/views/mes/pro/maintenancecalendar/data.ts
@@ -0,0 +1,246 @@
+import type { VbenFormApi, 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 { CalTeamSelect } from '#/views/mes/cal/team/components';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 鐘舵�佸父閲� */
+export const CAL_STATUS = {
+ DRAFT: 0,
+ PUBLISHED: 10,
+};
+
+/** 琛ㄥ崟鐨勯厤缃」 */
+export function useFormSchema(formType: FormType, formApi?: VbenFormApi): 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: 'workDate',
+ label: '浣滀笟鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '璇烽�夋嫨浣滀笟鏃ユ湡',
+ valueFormat: 'YYYY-MM-DD',
+ disabled: isReadonly,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'shift',
+ label: '鐝',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.MES_CAL_SHIFT_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鐝',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'workType',
+ label: '浣滀笟绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.MES_PRO_CALENDAR_WORK_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨浣滀笟绫诲瀷',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'region',
+ label: '浣滀笟鍖哄煙/绾胯矾',
+ component: 'Input',
+ componentProps: {
+ placeholder: '濡傦細110kV 鍩庡尯绾�',
+ disabled: isReadonly,
+ },
+ },
+ {
+ fieldName: 'teamId',
+ label: '鍊肩彮鐝粍',
+ component: markRaw(CalTeamSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨鍊肩彮鐝粍',
+ disabled: isReadonly,
+ onChange: (item: { name?: string } | undefined) => {
+ formApi?.setFieldValue('teamName', item?.name ?? '');
+ },
+ },
+ },
+ {
+ fieldName: 'personnel',
+ label: '鍊肩彮浜哄憳',
+ component: 'Input',
+ componentProps: {
+ placeholder: '濡傦細寮犱笁,鏉庡洓',
+ disabled: isReadonly,
+ },
+ },
+ {
+ 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: 'workType',
+ label: '浣滀笟绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PRO_CALENDAR_WORK_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨浣滀笟绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '鑽夌', value: CAL_STATUS.DRAFT },
+ { label: '宸插彂甯�', value: CAL_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: 180,
+ },
+ {
+ field: 'workDate',
+ title: '浣滀笟鏃ユ湡',
+ width: 120,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'shift',
+ title: '鐝',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_CAL_SHIFT_TYPE },
+ },
+ },
+ {
+ field: 'workType',
+ title: '浣滀笟绫诲瀷',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PRO_CALENDAR_WORK_TYPE },
+ },
+ },
+ {
+ field: 'region',
+ title: '浣滀笟鍖哄煙/绾胯矾',
+ minWidth: 140,
+ },
+ {
+ field: 'teamName',
+ title: '鍊肩彮鐝粍',
+ minWidth: 130,
+ },
+ {
+ field: 'personnel',
+ title: '鍊肩彮浜哄憳',
+ 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/maintenancecalendar/index.vue b/src/views/mes/pro/maintenancecalendar/index.vue
new file mode 100644
index 0000000..cdd4dee
--- /dev/null
+++ b/src/views/mes/pro/maintenancecalendar/index.vue
@@ -0,0 +1,192 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProMaintenanceCalendarApi } from '#/api/mes/pro/maintenancecalendar';
+
+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 {
+ deleteCalendar,
+ getCalendarPage,
+ publishCalendar,
+} from '#/api/mes/pro/maintenancecalendar';
+import { $t } from '#/locales';
+
+import { CAL_STATUS, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+/** MES 鐢靛姏杩愮淮鏃ュ巻鍒楄〃 */
+defineOptions({ name: 'MesProMaintenanceCalendar' });
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: MesProMaintenanceCalendarApi.MaintenanceCalendar) {
+ formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+function handleDetail(row: MesProMaintenanceCalendarApi.MaintenanceCalendar) {
+ 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 deleteCalendar(ids);
+ message.success($t('ui.actionMessage.deleteSuccess'));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+async function handlePublish(row: MesProMaintenanceCalendarApi.MaintenanceCalendar) {
+ await publishCalendar(row.id!);
+ message.success('宸插彂甯�');
+ handleRefresh();
+}
+
+const checkedIds = ref<number[]>([]);
+function handleRowCheckboxChange({
+ records,
+}: {
+ records: MesProMaintenanceCalendarApi.MaintenanceCalendar[];
+}) {
+ 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 getCalendarPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesProMaintenanceCalendarApi.MaintenanceCalendar>,
+ 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-calendar:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: '鎵归噺鍒犻櫎',
+ type: 'primary',
+ danger: true,
+ disabled: isEmpty(checkedIds),
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pro-maintenance-calendar: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 === CAL_STATUS.DRAFT" color="default">鑽夌</Tag>
+ <Tag v-else-if="row.status === CAL_STATUS.PUBLISHED" color="success">宸插彂甯�</Tag>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.detail'),
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['mes:pro-maintenance-calendar:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pro-maintenance-calendar:update'],
+ ifShow: row.status === CAL_STATUS.DRAFT,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '鍙戝竷',
+ type: 'link',
+ auth: ['mes:pro-maintenance-calendar:update'],
+ ifShow: row.status === CAL_STATUS.DRAFT,
+ popConfirm: {
+ title: '纭鍙戝竷璇ユ棩鍘嗭紵',
+ confirm: handlePublish.bind(null, row),
+ },
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pro-maintenance-calendar:delete'],
+ ifShow: row.status === CAL_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/maintenancecalendar/modules/form.vue b/src/views/mes/pro/maintenancecalendar/modules/form.vue
new file mode 100644
index 0000000..6e2be9e
--- /dev/null
+++ b/src/views/mes/pro/maintenancecalendar/modules/form.vue
@@ -0,0 +1,95 @@
+<script lang="ts" setup>
+import type { MesProMaintenanceCalendarApi } from '#/api/mes/pro/maintenancecalendar';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createCalendar,
+ getCalendar,
+ updateCalendar,
+} from '#/api/mes/pro/maintenancecalendar';
+import { $t } from '#/locales';
+
+import type { FormType } from '../data';
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProMaintenanceCalendarApi.MaintenanceCalendar>();
+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: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data =
+ (await formApi.getValues()) as MesProMaintenanceCalendarApi.MaintenanceCalendar;
+ try {
+ await (formType.value === 'create'
+ ? createCalendar(data)
+ : updateCalendar(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, formApi));
+ if (!data || !data.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getCalendar(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>
--
Gitblit v1.9.3