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 鍚堟牸 / &lt;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