xiaoyi
3 天以前 27c8277d717b34b55f3ea967027b53b067f77df3
feat 功能变更
已修改6个文件
已添加27个文件
4028 ■■■■■ 文件已修改
src/api/aftersales/compensation.ts 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bi/decision/forecast.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/performance-appraise/index.ts 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/work-hour-statistics/index.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/dv/telemetry/index.ts 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/alternativeplan/index.ts 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/maintenancecalendar/index.ts 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/compensation/data.ts 245 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/compensation/index.vue 240 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/compensation/modules/audit-modal.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/compensation/modules/form.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/compensation/modules/process-select-modal.vue 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/compensation/modules/ticket-select.vue 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bi/decision/forecast-analysis/index.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/performance-appraise/data.ts 203 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/performance-appraise/index.vue 304 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/performance-appraise/modules/audit-modal.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/performance-appraise/modules/form.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/performance-appraise/modules/item.vue 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/performance-appraise/modules/process-select-modal.vue 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/performance-appraise/modules/result.vue 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/performance-appraise/modules/score.vue 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/workhour/index.vue 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/machinery/modules/telemetry-list.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/telemetry/check.vue 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/telemetry/index.vue 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/alternativeplan/data.ts 196 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/alternativeplan/index.vue 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/alternativeplan/modules/form.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/maintenancecalendar/data.ts 246 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/maintenancecalendar/index.vue 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/maintenancecalendar/modules/form.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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',
  );
}
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);
}
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季度 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; // æ¡£çº§ï¼š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 },
  );
}
/** ä¿å­˜å‘˜å·¥è€ƒæ ¸ç»“果(自动计算综合得分与档级) */
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}`,
  );
}
src/api/hrm/work-hour-statistics/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
import { requestClient } from '#/api/request';
export namespace HrmWorkHourStatisticsApi {
  /** å‘˜å·¥å·¥æ—¶ï¼ˆè€ƒå‹¤å£å¾„) */
  export interface EmployeeWorkHour {
    userId?: number;
    userName?: string;
    deptId?: number;
    deptName?: string;
    workDays?: number; // å‡ºå‹¤å¤©æ•°
    workHours?: number; // æ­£å¸¸å·¥æ—¶ï¼ˆäººæ—¶ï¼‰
    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;
  }
}
/** å‘˜å·¥å·¥æ—¶ç»Ÿè®¡ï¼ˆè€ƒå‹¤å£å¾„) */
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 },
  );
}
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; // æ­£å¸¸æ•°é‡
    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 },
  );
}
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}`);
}
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}`);
}
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 åˆåŒè§£é™¤ç»ˆæ­¢çŠ¶æ€
  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 ç»©æ•ˆè€ƒæ ¸æ¡£çº§
} as const;
/** å­—典类型枚举 - ç»Ÿä¸€å¯¼å‡º */
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' },
    },
  ];
}
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>
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>
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>
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>
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>
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>
</template>
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' },
    },
  ];
}
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>
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>
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>
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>
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>
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="档级" 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>
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>
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 å·¥æ—¶ç»Ÿè®¡ï¼ˆè€ƒå‹¤å£å¾„ + æŠ¥å·¥å£å¾„) */
  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: '正常工时(人时)', 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="员工工时(考勤口径)">
          <EmployeeGrid table-title="员工工时统计" />
        </Tabs.TabPane>
        <Tabs.TabPane key="team" tab="班组工时(报工口径)">
          <TeamGrid table-title="班组工时统计" />
        </Tabs.TabPane>
      </Tabs>
    </div>
  </Page>
</template>
src/views/mes/dv/machinery/modules/telemetry-list.vue
@@ -18,6 +18,8 @@
      columns: useLatestColumns(),
      height: 320,
      keepSource: true,
      // /latest è¿”回裸数组,需关闭分页,否则 vxe ä¼šæŒ‰ {list,total} è§£æžå¯¼è‡´ä¸å±•示
      pagerConfig: { enabled: false },
      proxyConfig: {
        ajax: {
          query: async () =>
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="正常"
            :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 ? '异常' : '正常' }}
          </Tag>
        </template>
      </CheckGrid>
    </div>
  </Page>
</template>
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} è§£æžå¯¼è‡´ä¸å±•示
      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 }">
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' },
    },
  ];
}
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>
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>
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' },
    },
  ];
}
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>
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>