xiaoyi
4 天以前 5c243d9d05da668a32b1bc9a4f543ea081488d11
feat 功能变更
已修改32个文件
已添加57个文件
8791 ■■■■■ 文件已修改
src/api/crm/cancellation/index.ts 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/contract-command/index.ts 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/contract/index.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/credit-rating/index.ts 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/customer/index.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/refund/index.ts 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/saleQuotation/index.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/plan/index.ts 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/certificate/index.ts 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/dv/machinery/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/dv/statistics/index.ts 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/md/item/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/ctc-reel/index.ts 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/ctc/index.ts 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/tech-confirm/index.ts 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/emergencyplan/index.ts 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/feedback/index.ts 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/maintenance-plan/index.ts 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/process/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/task/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/workorder/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/outsourcetest/index.ts 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/wls/productissue/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/wls/stocktaking/task/line/index.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/wls/warehouse/area/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/wls/warehouse/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/table-action/icons.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/biz-mes-enum.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/ticket/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/cancellation/data.ts 227 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/cancellation/index.vue 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/cancellation/modules/form.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/components/command-form-data.ts 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/components/command-form.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/components/command-list-data.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/components/command-list.vue 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/data.ts 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/contract/detail/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/credit-rating/data.ts 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/credit-rating/index.vue 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/credit-rating/modules/form.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/customer/data.ts 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/refund/data.ts 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/refund/index.vue 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/refund/modules/form.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/saleQuotation/data.ts 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/plan/data.ts 242 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/plan/index.vue 300 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/plan/modules/form.vue 236 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/plan/modules/process-select-modal.vue 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/certificate/data.ts 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/certificate/index.vue 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/certificate/modules/form.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/machinery/data.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/machinery/modules/form.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/statistics/summary.vue 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/data.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/ctc-reel/data.ts 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/ctc-reel/index.vue 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/ctc-reel/modules/form.vue 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/ctc/components/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/ctc/components/select.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/ctc/data.ts 262 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/ctc/index.vue 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/ctc/modules/calc-modal.vue 344 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/ctc/modules/form.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/ctc/modules/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/tech-confirm/data.ts 260 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/tech-confirm/index.vue 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/tech-confirm/modules/form.vue 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/emergencyplan/data.ts 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/emergencyplan/index.vue 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/emergencyplan/modules/form.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/maintenance-plan/data.ts 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/maintenance-plan/index.vue 271 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/maintenance-plan/modules/form.vue 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/data.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/data.ts 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workresult/summary.vue 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/process/data.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/outsourcetest/data.ts 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/outsourcetest/index.vue 210 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/outsourcetest/modules/form.vue 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productissue/data.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/stocktaking/task/index.vue 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/warehouse/area/data.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/warehouse/data.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/cancellation/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmCancellationApi {
  export interface Cancellation {
    id?: number;
    cancellationNo?: string;
    customerId?: number;
    customerName?: string;
    contractId?: number;
    contractNo?: string;
    cancellationType?: string;
    cancelReason?: string;
    expectedCancelDate?: string;
    actualCancelDate?: string;
    refundAmount?: number;
    applyUserId?: number;
    applyUserName?: string;
    applyTime?: string;
    auditStatus?: number;
    processInstanceId?: string;
    remark?: string;
    createTime?: string;
  }
  export interface PageParams extends PageParam {
    cancellationNo?: string;
    customerId?: number;
    cancellationType?: string;
    auditStatus?: number;
  }
}
/** æŸ¥è¯¢é”€æˆ·ç”³è¯·åˆ†é¡µ */
export function getCancellationPage(
  params: CrmCancellationApi.PageParams,
) {
  return requestClient.get<PageResult<CrmCancellationApi.Cancellation>>(
    '/crm/cancellation/page',
    { params },
  );
}
/** æŸ¥è¯¢é”€æˆ·ç”³è¯·è¯¦æƒ… */
export function getCancellation(id: number) {
  return requestClient.get<CrmCancellationApi.Cancellation>(
    `/crm/cancellation/get?id=${id}`,
  );
}
/** æ–°å¢žé”€æˆ·ç”³è¯· */
export function createCancellation(data: CrmCancellationApi.Cancellation) {
  return requestClient.post('/crm/cancellation/create', data);
}
/** ä¿®æ”¹é”€æˆ·ç”³è¯· */
export function updateCancellation(data: CrmCancellationApi.Cancellation) {
  return requestClient.put('/crm/cancellation/update', data);
}
/** å®¡æ‰¹é”€æˆ·ç”³è¯· */
export function auditCancellation(data: {
  id: number;
  auditStatus: number;
}) {
  return requestClient.put('/crm/cancellation/audit', data);
}
/** åˆ é™¤é”€æˆ·ç”³è¯· */
export function deleteCancellation(id: number) {
  return requestClient.delete(`/crm/cancellation/delete?id=${id}`);
}
/** å¯¼å‡ºé”€æˆ·ç”³è¯· */
export function exportCancellation(params: Record<string, unknown>) {
  return requestClient.download('/crm/cancellation/export-excel', { params });
}
src/api/crm/contract-command/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmContractCommandApi {
  export interface ContractCommand {
    id?: number;
    contractId?: number;
    contractNo?: string;
    commandType?: string;
    targetModule?: string;
    targetId?: number;
    bizNo?: string;
    operatorId?: number;
    operatorName?: string;
    operateTime?: string;
    status?: number;
    remark?: string;
    createTime?: string;
  }
  export interface PageParams extends PageParam {
    contractId?: number;
    commandType?: string;
    status?: number;
    bizNo?: string;
  }
}
/** æŸ¥è¯¢åˆåŒæŒ‡ä»¤åˆ†é¡µ */
export function getContractCommandPage(
  params: CrmContractCommandApi.PageParams,
) {
  return requestClient.get<PageResult<CrmContractCommandApi.ContractCommand>>(
    '/crm/contract-command/page',
    { params },
  );
}
/** æ–°å¢žåˆåŒæŒ‡ä»¤ */
export function createContractCommand(
  data: CrmContractCommandApi.ContractCommand,
) {
  return requestClient.post('/crm/contract-command/create', data);
}
/** æ›´æ–°åˆåŒæŒ‡ä»¤ */
export function updateContractCommand(
  data: CrmContractCommandApi.ContractCommand,
) {
  return requestClient.put('/crm/contract-command/update', data);
}
/** æ‰§è¡ŒåˆåŒæŒ‡ä»¤ */
export function executeContractCommand(id: number) {
  return requestClient.put(`/crm/contract-command/execute?id=${id}`);
}
/** å–消合同指令 */
export function cancelContractCommand(id: number) {
  return requestClient.put(`/crm/contract-command/cancel?id=${id}`);
}
/** åˆ é™¤åˆåŒæŒ‡ä»¤ */
export function deleteContractCommand(id: number) {
  return requestClient.delete(`/crm/contract-command/delete?id=${id}`);
}
src/api/crm/contract/index.ts
@@ -43,6 +43,14 @@
    contactName?: string;
    orderId?: number; // å…³è” ERP é”€å”®è®¢å•编号
    orderNo?: string; // ERP é”€å”®è®¢å•单号
    contractType?: string; // åˆåŒç±»åž‹ï¼ˆä¾›ç”¨ç”µ/能源服务等)
    voltageLevel?: string; // ç”µåŽ‹ç­‰çº§
    electricityType?: string; // ç”¨ç”µç±»åž‹
    contractCapacity?: number; // åˆåŒå®¹é‡ï¼ˆkVA)
    tariffMode?: string; // ç”µä»·æ¨¡å¼
    meterMode?: string; // è®¡é‡æ–¹å¼
    payCycle?: number; // ç¼´è´¹å‘¨æœŸï¼ˆæœˆï¼‰
    commandCount?: number; // æŒ‡ä»¤æ•°é‡
  }
  /** åˆåŒäº§å“ä¿¡æ¯ */
src/api/crm/credit-rating/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmCreditRatingApi {
  export interface CreditRating {
    id?: number;
    customerId?: number;
    customerName?: string;
    level?: string;
    score?: number;
    evalType?: string;
    evalReason?: string;
    operatorId?: number;
    operatorName?: string;
    operateTime?: string;
    validUntil?: string;
    remark?: string;
    createTime?: string;
  }
  export interface PageParams extends PageParam {
    customerId?: number;
    level?: string;
    evalType?: string;
  }
}
/** æŸ¥è¯¢ä¿¡ç”¨è¯„级分页 */
export function getCreditRatingPage(
  params: CrmCreditRatingApi.PageParams,
) {
  return requestClient.get<PageResult<CrmCreditRatingApi.CreditRating>>(
    '/crm/credit-rating/page',
    { params },
  );
}
/** æŸ¥è¯¢ä¿¡ç”¨è¯„级详情 */
export function getCreditRating(id: number) {
  return requestClient.get<CrmCreditRatingApi.CreditRating>(
    `/crm/credit-rating/get?id=${id}`,
  );
}
/** æ–°å¢žä¿¡ç”¨è¯„级 */
export function createCreditRating(data: CrmCreditRatingApi.CreditRating) {
  return requestClient.post('/crm/credit-rating/create', data);
}
/** ä¿®æ”¹ä¿¡ç”¨è¯„级 */
export function updateCreditRating(data: CrmCreditRatingApi.CreditRating) {
  return requestClient.put('/crm/credit-rating/update', data);
}
/** åˆ é™¤ä¿¡ç”¨è¯„级 */
export function deleteCreditRating(id: number) {
  return requestClient.delete(`/crm/credit-rating/delete?id=${id}`);
}
/** å¯¼å‡ºä¿¡ç”¨è¯„级 */
export function exportCreditRating(params: Record<string, unknown>) {
  return requestClient.download('/crm/credit-rating/export-excel', { params });
}
src/api/crm/customer/index.ts
@@ -36,6 +36,11 @@
    createTime: Date; // åˆ›å»ºæ—¶é—´
    updateTime: Date; // æ›´æ–°æ—¶é—´
    poolDay?: number; // è·ç¦»è¿›å…¥å…¬æµ·å¤©æ•°
    customerType?: string; // å®¢æˆ·åˆ†ç±»ï¼ˆæ”¿ä¼/工业/居民等)
    creditLevel?: string; // å½“前信用等级
    creditScore?: number; // å½“前信用分
    overdueAmount?: number; // æ¬ è´¹é‡‘额(未回款+逾期)
    overdueCount?: number; // æ¬ è´¹ç¬”æ•°
  }
  /** å®¢æˆ·å¯¼å…¥è¯·æ±‚ */
src/api/crm/refund/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace CrmRefundApi {
  export interface Refund {
    id?: number;
    refundNo?: string;
    customerId?: number;
    customerName?: string;
    contractId?: number;
    contractNo?: string;
    refundType?: string;
    refundAmount?: number;
    refundReason?: string;
    applyUserId?: number;
    applyUserName?: string;
    applyTime?: string;
    auditStatus?: number;
    processInstanceId?: string;
    remark?: string;
    createTime?: string;
  }
  export interface PageParams extends PageParam {
    refundNo?: string;
    customerId?: number;
    refundType?: string;
    auditStatus?: number;
  }
}
/** æŸ¥è¯¢é€€è´¹å•分页 */
export function getRefundPage(params: CrmRefundApi.PageParams) {
  return requestClient.get<PageResult<CrmRefundApi.Refund>>(
    '/crm/refund/page',
    { params },
  );
}
/** æŸ¥è¯¢é€€è´¹å•详情 */
export function getRefund(id: number) {
  return requestClient.get<CrmRefundApi.Refund>(`/crm/refund/get?id=${id}`);
}
/** æ–°å¢žé€€è´¹å• */
export function createRefund(data: CrmRefundApi.Refund) {
  return requestClient.post('/crm/refund/create', data);
}
/** ä¿®æ”¹é€€è´¹å• */
export function updateRefund(data: CrmRefundApi.Refund) {
  return requestClient.put('/crm/refund/update', data);
}
/** å®¡æ‰¹é€€è´¹å• */
export function auditRefund(data: { id: number; auditStatus: number }) {
  return requestClient.put('/crm/refund/audit', data);
}
/** åˆ é™¤é€€è´¹å• */
export function deleteRefund(id: number) {
  return requestClient.delete(`/crm/refund/delete?id=${id}`);
}
/** å¯¼å‡ºé€€è´¹å• */
export function exportRefund(params: Record<string, unknown>) {
  return requestClient.download('/crm/refund/export-excel', { params });
}
src/api/crm/saleQuotation/index.ts
@@ -34,6 +34,13 @@
    createTime?: Date;
    updateTime?: Date;
    items?: SaleQuotationItem[];
    quotationType?: string; // æŠ¥ä»·ç±»åž‹ï¼ˆgeneral/power/energy)
    capacity?: number; // åˆåŒå®¹é‡ï¼ˆkVA)
    maxDemand?: number; // æœ€å¤§éœ€é‡ï¼ˆkW)
    unitPrice?: number; // ç”µåº¦å•价(元/kWh)
    tariffMode?: string; // ç”µä»·æ¨¡å¼
    powerFactorFee?: number; // åŠ›è°ƒç”µè´¹ï¼ˆå…ƒï¼‰
    billingCycle?: number; // ç»“算周期(月)
  }
  /** æŠ¥ä»·å•物料明细 */
src/api/erp/purchase/plan/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace ErpPurchasePlanApi {
  /** é‡‡è´­è®¡åˆ’信息 */
  export interface PurchasePlan {
    id?: number; // ç¼–号
    no?: string; // è®¡åˆ’单号
    name?: string; // è®¡åˆ’名称
    status?: number; // çŠ¶æ€ï¼š0未提交 10审批中 20审核通过 30审核不通过 40已取消
    processInstanceId?: string; // å·¥ä½œæµç¼–号
    demandSource?: number; // éœ€æ±‚来源:1库存预警 2运维/生产计划
    demandSourceName?: string; // éœ€æ±‚来源名称
    planDate?: string; // è®¡åˆ’日期
    planUserId?: number; // ç¼–制人编号
    planUserName?: string; // ç¼–制人姓名
    planDeptId?: number; // ç¼–制部门编号
    planDeptName?: string; // ç¼–制部门名称
    supplierId?: number; // ä¾›åº”商编号
    supplierName?: string; // ä¾›åº”商名称
    totalCount?: number; // åˆè®¡æ•°é‡
    generatedFlag?: boolean; // æ˜¯å¦å·²ç”Ÿæˆé‡‡è´­ç”³è¯·
    remark?: string; // å¤‡æ³¨
    productNames?: string; // äº§å“åç§°åˆ—表
    items?: PurchasePlanItem[]; // è®¡åˆ’明细
    createTime?: string; // åˆ›å»ºæ—¶é—´
  }
  /** é‡‡è´­è®¡åˆ’明细 */
  export interface PurchasePlanItem {
    id?: number; // ç¼–号
    productId: number; // äº§å“ç¼–号
    productName?: string; // äº§å“åç§°
    productUnitId?: number; // äº§å“å•位编号
    productUnitName?: string; // äº§å“å•位名称
    count: number; // è®¡åˆ’采购数量
    demandTime?: string; // éœ€æ±‚日期
    remark?: string; // å¤‡æ³¨
  }
}
/** åˆ›å»ºé‡‡è´­è®¡åˆ’ */
export function createPurchasePlan(data: ErpPurchasePlanApi.PurchasePlan) {
  return requestClient.post('/erp/purchase-plan/create', data);
}
/** æ›´æ–°é‡‡è´­è®¡åˆ’ */
export function updatePurchasePlan(data: ErpPurchasePlanApi.PurchasePlan) {
  return requestClient.put('/erp/purchase-plan/update', data);
}
/** æäº¤é‡‡è´­è®¡åˆ’审批 */
export function submitPurchasePlan(id: number, processDefinitionKey: string) {
  return requestClient.put('/erp/purchase-plan/submit', null, {
    params: { id, processDefinitionKey },
  });
}
/** æ ¹æ®åº“存预警生成采购计划 */
export function generatePlanFromStockAlert(params: { name?: string; supplierId?: number }) {
  return requestClient.post<number>('/erp/purchase-plan/generate-from-stock-alert', null, {
    params,
  });
}
/** ç”Ÿæˆé‡‡è´­ç”³è¯· */
export function generatePurchaseRequests(id: number) {
  return requestClient.post<number[]>('/erp/purchase-plan/generate-requests', null, {
    params: { id },
  });
}
/** åˆ é™¤é‡‡è´­è®¡åˆ’ */
export function deletePurchasePlan(ids: number[]) {
  return requestClient.delete('/erp/purchase-plan/delete', { params: { ids: ids.join(',') } });
}
/** èŽ·å–é‡‡è´­è®¡åˆ’è¯¦æƒ… */
export function getPurchasePlan(id: number) {
  return requestClient.get<ErpPurchasePlanApi.PurchasePlan>(
    `/erp/purchase-plan/get?id=${id}`,
  );
}
/** èŽ·å–é‡‡è´­è®¡åˆ’åˆ†é¡µ */
export function getPurchasePlanPage(params: PageParam) {
  return requestClient.get<PageResult<ErpPurchasePlanApi.PurchasePlan>>(
    '/erp/purchase-plan/page',
    { params },
  );
}
/** å¯¼å‡ºé‡‡è´­è®¡åˆ’ Excel */
export function exportPurchasePlan(params: any) {
  return requestClient.download('/erp/purchase-plan/export-excel', { params });
}
/** èŽ·å–å®¡æ‰¹æµç¨‹åˆ—è¡¨ */
export function getApproveProcessList(id: number) {
  return requestClient.get(`/erp/purchase-plan/approve-process-list`, { params: { id } });
}
src/api/hrm/certificate/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace HrmCertificateApi {
  /** è¯ä¹¦èµ„è´¨ */
  export interface Certificate {
    id?: number;
    employeeId?: number; // å‘˜å·¥ç¼–号
    employeeName?: string; // å‘˜å·¥å§“名
    certType?: number; // è¯ä¹¦ç±»åž‹ï¼š1特种作业 2从业资质 3技能等级 4安全培训
    certTypeName?: string; // è¯ä¹¦ç±»åž‹åç§°
    certName?: string; // è¯ä¹¦åç§°
    certNo?: string; // è¯ä¹¦ç¼–号
    issueOrg?: string; // å‘证机构
    issueDate?: string; // å‘证日期
    expireDate?: string; // æœ‰æ•ˆæœŸè‡³
    expireStatus?: number; // åˆ°æœŸçŠ¶æ€ï¼š0有效 1即将到期 2已过期
    remark?: string; // å¤‡æ³¨
    createTime?: string;
  }
}
/** åˆ›å»ºè¯ä¹¦èµ„è´¨ */
export function createCertificate(data: HrmCertificateApi.Certificate) {
  return requestClient.post('/hrm/certificate/create', data);
}
/** æ›´æ–°è¯ä¹¦èµ„è´¨ */
export function updateCertificate(data: HrmCertificateApi.Certificate) {
  return requestClient.put('/hrm/certificate/update', data);
}
/** åˆ é™¤è¯ä¹¦èµ„è´¨ */
export function deleteCertificate(ids: number[]) {
  return requestClient.delete('/hrm/certificate/delete', { params: { ids: ids.join(',') } });
}
/** èŽ·å–è¯ä¹¦èµ„è´¨è¯¦æƒ… */
export function getCertificate(id: number) {
  return requestClient.get<HrmCertificateApi.Certificate>(
    `/hrm/certificate/get?id=${id}`,
  );
}
/** èŽ·å–è¯ä¹¦èµ„è´¨åˆ†é¡µ */
export function getCertificatePage(params: PageParam) {
  return requestClient.get<PageResult<HrmCertificateApi.Certificate>>(
    '/hrm/certificate/page',
    { params },
  );
}
src/api/mes/dv/machinery/index.ts
@@ -7,6 +7,7 @@
  export interface Machinery {
    id?: number; // è®¾å¤‡ç¼–号
    code?: string; // è®¾å¤‡ç¼–码
    tbDeviceId?: string; // æ•°é‡‡è®¾å¤‡ID(外部数采 tbDeviceId)
    name?: string; // è®¾å¤‡åç§°
    brand?: string; // å“ç‰Œ
    specification?: string; // è§„格型号
src/api/mes/dv/statistics/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
import { requestClient } from '#/api/request';
export namespace MesDvStatisticsApi {
  /** è®¾å¤‡ç»Ÿè®¡æ±‡æ€» */
  export interface StatisticsSummary {
    totalMachinery?: number; // è®¾å¤‡æ€»æ•°
    producingCount?: number; // ç”Ÿäº§ä¸­
    stoppedCount?: number; // åœæœº
    maintainingCount?: number; // ç»´æŠ¤ä¸­
    faultRate?: number; // æ•…障率%
    totalRepair?: number; // ç»´ä¿®å•总数
    processingRepair?: number; // ç»´ä¿®ä¸­/待验收
    finishedRepair?: number; // å·²ç¡®è®¤ç»´ä¿®
    totalMainten?: number; // ä¿å…»è®°å½•总数
    finishedMainten?: number; // å·²å®Œæˆä¿å…»
    typeDistribution?: TypeItem[]; // æŒ‰è®¾å¤‡ç±»åž‹åˆ†å¸ƒ
  }
  /** è®¾å¤‡ç±»åž‹åˆ†å¸ƒé¡¹ */
  export interface TypeItem {
    machineryTypeId?: number;
    machineryTypeName?: string;
    count?: number;
  }
}
/** è®¾å¤‡ç»Ÿè®¡æ±‡æ€» */
export function getDvStatisticsSummary() {
  return requestClient.get<MesDvStatisticsApi.StatisticsSummary>(
    '/mes/dv/statistics/summary',
  );
}
src/api/mes/md/item/index.ts
@@ -18,6 +18,7 @@
    safeStockFlag?: boolean; // æ˜¯å¦å¯ç”¨å®‰å…¨åº“å­˜
    minStock?: number; // æœ€ä½Žåº“存量
    maxStock?: number; // æœ€é«˜åº“存量
    materialCategory?: number; // ç”µåŠ›ç‰©èµ„åˆ†ç±»
    highValue?: boolean; // æ˜¯å¦é«˜å€¼ç‰©æ–™
    batchFlag?: boolean; // æ˜¯å¦å¯ç”¨æ‰¹æ¬¡ç®¡ç†
    remark?: string; // å¤‡æ³¨
src/api/mes/pd/ctc-reel/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesPdCtcReelApi {
  /** æ¢ä½å¯¼çº¿ç›˜å…·è®¡ç®— */
  export interface CtcReel {
    id?: number; // ç¼–号
    ctcId?: number; // æ¢ä½å¯¼çº¿äº§å“ID
    ctcOrderSpec?: string; // æ¢ä½å¯¼çº¿è®¢è´§è§„æ ¼
    reelType?: string; // ç›˜å…·ç±»åž‹
    reelOuterDiameter?: number; // ç›˜å…·å¤–径 mm
    reelInnerDiameter?: number; // ç›˜èН内径 mm
    reelWidth?: number; // ç›˜å®½ mm
    maxCapacity?: number; // æœ€å¤§å®¹é‡ km
    recommendedLength?: number; // æŽ¨èæ¯ç›˜é•¿åº¦ km
    netWeight?: number; // å‡€é‡ kg
    grossWeight?: number; // æ¯›é‡ kg
    calcFormula?: string; // è®¡ç®—公式
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
}
/** æŸ¥è¯¢ç›˜å…·è®¡ç®—分页 */
export function getCtcReelPage(params: PageParam) {
  return requestClient.get<PageResult<MesPdCtcReelApi.CtcReel>>(
    '/mes/pd/ctc-reel/page',
    { params },
  );
}
/** æŸ¥è¯¢ç›˜å…·è®¡ç®—详情 */
export function getCtcReel(id: number) {
  return requestClient.get<MesPdCtcReelApi.CtcReel>(
    `/mes/pd/ctc-reel/get?id=${id}`,
  );
}
/** æŸ¥è¯¢æŒ‡å®šæ¢ä½å¯¼çº¿äº§å“çš„盘具列表 */
export function getCtcReelListByCtcId(ctcId: number) {
  return requestClient.get<MesPdCtcReelApi.CtcReel[]>(
    `/mes/pd/ctc-reel/list-by-ctc?ctcId=${ctcId}`,
  );
}
/** æ–°å¢žç›˜å…·è®¡ç®—记录 */
export function createCtcReel(data: MesPdCtcReelApi.CtcReel) {
  return requestClient.post('/mes/pd/ctc-reel/create', data);
}
/** ä¿®æ”¹ç›˜å…·è®¡ç®—记录 */
export function updateCtcReel(data: MesPdCtcReelApi.CtcReel) {
  return requestClient.put('/mes/pd/ctc-reel/update', data);
}
/** åˆ é™¤ç›˜å…·è®¡ç®—记录 */
export function deleteCtcReel(id: number) {
  return requestClient.delete(`/mes/pd/ctc-reel/delete?id=${id}`);
}
src/api/mes/pd/ctc/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,128 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesPdCtcApi {
  /** æ¢ä½å¯¼çº¿äº§å“æŠ€æœ¯å‚æ•° */
  export interface Ctc {
    id?: number; // ç¼–号
    itemId?: number; // ç‰©æ–™ID
    itemName?: string; // ç‰©æ–™åç§°
    itemCode?: string; // ç‰©æ–™ç¼–码
    orderSpec?: string; // è®¢è´§è§„æ ¼
    bareConductorSpec?: string; // è£¸å¯¼çº¿è§„æ ¼
    enameledSpec?: string; // æ¼†åŒ…线规格
    transpositionSpec?: string; // æ¢ä½è§„æ ¼
    conductorType?: string; // å¯¼ä½“类型
    crossSection?: number; // æˆªé¢ç§¯ mm²
    outerDiameter?: number; // å¤–径 mm
    resistance20c?: number; // 20℃直流电阻 Î©/km
    wrappingAngle?: number; // ç»•包角度 Â°
    insulationThickness?: number; // ç»ç¼˜åŽšåº¦ mm
    ratedVoltage?: string; // é¢å®šç”µåŽ‹ kV
    testVoltage?: string; // è¯•验电压 kV
    minBendingRadius?: number; // æœ€å°å¼¯æ›²åŠå¾„ mm
    weightPerKm?: number; // æ¯å…¬é‡Œé‡é‡ kg/km
    standardCode?: string; // æ‰§è¡Œæ ‡å‡†ä»£å·
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
  /** ç»•包角度计算 */
  export interface AngleCalcReq {
    conductorThickness?: number; // å¯¼ä½“厚度 mm
    conductorWidth?: number; // å¯¼ä½“宽度 mm
    cornerRadius?: number; // åœ†è§’半径 mm
    insulationThickness?: number; // ç»ç¼˜åŽšåº¦ mm
    tapeWidth?: number; // çº¸å¸¦å®½åº¦ mm
    overlapWidth?: number; // é‡å å®½åº¦ mm
  }
  /** ç»•包角度计算结果 */
  export interface AngleCalcResp {
    circumference?: number; // å¯¼çº¿å‘¨é•¿ mm
    wrappingAngle?: number; // ç»•包角度 Â°
    wrappingPitch?: number; // ç»•包节距 mm
  }
  /** ç›´æµç”µé˜»è®¡ç®— */
  export interface ResistanceCalcReq {
    conductorThickness?: number; // å¯¼ä½“厚度 mm
    conductorWidth?: number; // å¯¼ä½“宽度 mm
    temperature?: number; // è®¡ç®—温度 â„ƒ
    material?: string; // å¯¼ä½“材质
  }
  /** ç›´æµç”µé˜»è®¡ç®—结果 */
  export interface ResistanceCalcResp {
    crossSection?: number; // æˆªé¢ç§¯ mm²
    resistance20cKm?: number; // 20℃直流电阻 Î©/km
    resistanceAtTemp?: number; // æŒ‡å®šæ¸©åº¦ç›´æµç”µé˜» Î©/m
    resistanceAtTempKm?: number; // æŒ‡å®šæ¸©åº¦ç›´æµç”µé˜» Î©/km
  }
  /** ç›˜å…·å®¹é‡è®¡ç®— */
  export interface ReelCalcReq {
    reelOuterDiameter?: number; // ç›˜å…·å¤–径 d1 mm
    reelInnerDiameter?: number; // ç›˜èН内径 d2 mm
    reelWidth?: number; // ç›˜å®½ l2 mm
    ctcThickness?: number; // CTC æœ€å¤§é«˜åº¦ mm
    ctcWidth?: number; // CTC æœ€å¤§å®½åº¦ mm
  }
  /** ç›˜å…·å®¹é‡è®¡ç®—结果 */
  export interface ReelCalcResp {
    capacityMeter?: number; // æ¯ç›˜é•¿åº¦ m
    capacityKm?: number; // æ¯ç›˜é•¿åº¦ km
  }
}
/** æŸ¥è¯¢æ¢ä½å¯¼çº¿äº§å“æŠ€æœ¯å‚数分页 */
export function getCtcPage(params: PageParam) {
  return requestClient.get<PageResult<MesPdCtcApi.Ctc>>('/mes/pd/ctc/page', {
    params,
  });
}
/** æŸ¥è¯¢æ¢ä½å¯¼çº¿äº§å“æŠ€æœ¯å‚数详情 */
export function getCtc(id: number) {
  return requestClient.get<MesPdCtcApi.Ctc>(`/mes/pd/ctc/get?id=${id}`);
}
/** æ–°å¢žæ¢ä½å¯¼çº¿äº§å“æŠ€æœ¯å‚æ•° */
export function createCtc(data: MesPdCtcApi.Ctc) {
  return requestClient.post('/mes/pd/ctc/create', data);
}
/** ä¿®æ”¹æ¢ä½å¯¼çº¿äº§å“æŠ€æœ¯å‚æ•° */
export function updateCtc(data: MesPdCtcApi.Ctc) {
  return requestClient.put('/mes/pd/ctc/update', data);
}
/** åˆ é™¤æ¢ä½å¯¼çº¿äº§å“æŠ€æœ¯å‚æ•° */
export function deleteCtc(id: number) {
  return requestClient.delete(`/mes/pd/ctc/delete?id=${id}`);
}
/** å¯¼å‡ºæ¢ä½å¯¼çº¿äº§å“æŠ€æœ¯å‚æ•° */
export function exportCtc(params: any) {
  return requestClient.download('/mes/pd/ctc/export-excel', { params });
}
/** ç»•包角度计算 */
export function calcCtcAngle(data: MesPdCtcApi.AngleCalcReq) {
  return requestClient.post<MesPdCtcApi.AngleCalcResp>('/mes/pd/ctc/calc-angle', data);
}
/** ç›´æµç”µé˜»è®¡ç®— */
export function calcCtcResistance(data: MesPdCtcApi.ResistanceCalcReq) {
  return requestClient.post<MesPdCtcApi.ResistanceCalcResp>(
    '/mes/pd/ctc/calc-resistance',
    data,
  );
}
/** ç›˜å…·å®¹é‡è®¡ç®— */
export function calcCtcReel(data: MesPdCtcApi.ReelCalcReq) {
  return requestClient.post<MesPdCtcApi.ReelCalcResp>('/mes/pd/ctc/calc-reel', data);
}
src/api/mes/pd/tech-confirm/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesPdTechConfirmApi {
  /** äº§å“æŠ€æœ¯ç¡®è®¤å‡½ */
  export interface TechConfirm {
    id?: number; // ç¼–号
    confirmNo?: string; // ç¡®è®¤å‡½ç¼–号
    customerId?: number; // å®¢æˆ·ID
    customerName?: string; // å®¢æˆ·åç§°
    productName?: string; // äº§å“åç§°
    specification?: string; // è§„格型号
    techParams?: string; // æŠ€æœ¯å‚æ•° JSON
    templateType?: string; // æ¨¡æ¿ç±»åž‹
    drafterId?: number; // ç¼–制人ID
    drafterName?: string; // ç¼–制人姓名
    draftTime?: Date; // ç¼–制时间
    reviewerId?: number; // å®¡æ ¸äººID
    reviewerName?: string; // å®¡æ ¸äººå§“名
    reviewTime?: Date; // å®¡æ ¸æ—¶é—´
    customerConfirmUser?: string; // å®¢æˆ·ç¡®è®¤äºº
    customerConfirmTime?: Date; // å®¢æˆ·ç¡®è®¤æ—¶é—´
    status?: number; // çŠ¶æ€
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
}
/** æŸ¥è¯¢äº§å“æŠ€æœ¯ç¡®è®¤å‡½åˆ†é¡µ */
export function getTechConfirmPage(params: PageParam) {
  return requestClient.get<PageResult<MesPdTechConfirmApi.TechConfirm>>(
    '/mes/pd/tech-confirm/page',
    { params },
  );
}
/** æŸ¥è¯¢äº§å“æŠ€æœ¯ç¡®è®¤å‡½è¯¦æƒ… */
export function getTechConfirm(id: number) {
  return requestClient.get<MesPdTechConfirmApi.TechConfirm>(
    `/mes/pd/tech-confirm/get?id=${id}`,
  );
}
/** æ–°å¢žäº§å“æŠ€æœ¯ç¡®è®¤å‡½ */
export function createTechConfirm(data: MesPdTechConfirmApi.TechConfirm) {
  return requestClient.post('/mes/pd/tech-confirm/create', data);
}
/** ä¿®æ”¹äº§å“æŠ€æœ¯ç¡®è®¤å‡½ */
export function updateTechConfirm(data: MesPdTechConfirmApi.TechConfirm) {
  return requestClient.put('/mes/pd/tech-confirm/update', data);
}
/** åˆ é™¤äº§å“æŠ€æœ¯ç¡®è®¤å‡½ */
export function deleteTechConfirm(id: number) {
  return requestClient.delete(`/mes/pd/tech-confirm/delete?id=${id}`);
}
/** å¯¼å‡ºäº§å“æŠ€æœ¯ç¡®è®¤å‡½ */
export function exportTechConfirm(params: any) {
  return requestClient.download('/mes/pd/tech-confirm/export-excel', { params });
}
src/api/mes/pro/emergencyplan/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProEmergencyPlanApi {
  /** åº”急处置预案 */
  export interface EmergencyPlan {
    id?: number;
    code?: string; // é¢„案编码
    name?: string; // é¢„案名称
    planType?: number; // é¢„案类型:1停电 2设备故障 3火灾 4自然灾害 5其他
    content?: string; // å¤„置内容
    orgUnit?: string; // è´£ä»»å•位
    status?: number; // çŠ¶æ€ï¼š0草稿 10已发布
    remark?: string; // å¤‡æ³¨
    createTime?: string;
  }
}
/** åˆ›å»ºåº”急处置预案 */
export function createPlan(data: MesProEmergencyPlanApi.EmergencyPlan) {
  return requestClient.post('/mes/pro/emergency-plan/create', data);
}
/** æ›´æ–°åº”急处置预案 */
export function updatePlan(data: MesProEmergencyPlanApi.EmergencyPlan) {
  return requestClient.put('/mes/pro/emergency-plan/update', data);
}
/** åˆ é™¤åº”急处置预案 */
export function deletePlan(ids: number[]) {
  return requestClient.delete('/mes/pro/emergency-plan/delete', { params: { ids: ids.join(',') } });
}
/** èŽ·å–åº”æ€¥å¤„ç½®é¢„æ¡ˆ */
export function getPlan(id: number) {
  return requestClient.get<MesProEmergencyPlanApi.EmergencyPlan>(
    `/mes/pro/emergency-plan/get?id=${id}`,
  );
}
/** èŽ·å–åº”æ€¥å¤„ç½®é¢„æ¡ˆåˆ†é¡µ */
export function getPlanPage(params: PageParam) {
  return requestClient.get<PageResult<MesProEmergencyPlanApi.EmergencyPlan>>(
    '/mes/pro/emergency-plan/page',
    { params },
  );
}
/** å‘布应急处置预案 */
export function publishPlan(id: number) {
  return requestClient.put(`/mes/pro/emergency-plan/publish?id=${id}`);
}
src/api/mes/pro/feedback/index.ts
@@ -204,3 +204,40 @@
    `/mes/pro/feedback/list-by-task?taskId=${taskId}`,
  );
}
/** ä½œä¸šæˆæžœæ±‡æ€»é¡¹ï¼ˆæŒ‰å·¥å•类型) */
export interface WorkResultTypeItem {
  workOrderType?: number;
  workOrderTypeName?: string;
  feedbackQuantity?: number;
  qualifiedQuantity?: number;
  feedbackCount?: number;
}
/** ä½œä¸šæˆæžœæ±‡æ€»é¡¹ï¼ˆæŒ‰æ—¥æœŸï¼‰ */
export interface WorkResultDateItem {
  date?: string;
  feedbackQuantity?: number;
  qualifiedQuantity?: number;
  feedbackCount?: number;
}
/** ä½œä¸šæˆæžœæ±‡æ€» */
export interface WorkResultSummary {
  totalFeedbackQuantity?: number;
  totalQualifiedQuantity?: number;
  totalFeedbackCount?: number;
  byType?: WorkResultTypeItem[];
  byDate?: WorkResultDateItem[];
}
/** ä½œä¸šæˆæžœæ±‡æ€»ç»Ÿè®¡ */
export function getFeedbackSummary(params: {
  beginDate?: string;
  endDate?: string;
  workOrderType?: number;
}) {
  return requestClient.get<WorkResultSummary>('/mes/pro/feedback/summary', {
    params,
  });
}
src/api/mes/pro/maintenance-plan/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProMaintenancePlanApi {
  /** ç”µåŠ›ä½œä¸šè®¡åˆ’ */
  export interface MaintenancePlan {
    id?: number; // ç¼–号
    code?: string; // è®¡åˆ’编码
    name?: string; // è®¡åˆ’名称
    planType?: number; // è®¡åˆ’类型:1电网运行 2发电 3变电运维 4线路检修
    station?: string; // ç«™ç‚¹
    line?: string; // çº¿è·¯
    teamId?: number; // ç­ç»„编号
    teamName?: string; // ç­ç»„名称
    planDate?: string; // è®¡åˆ’日期
    status?: number; // çŠ¶æ€ï¼š0草稿 10已下发 20已完成 30已取消
    workOrderId?: number; // ç”Ÿæˆçš„工单编号
    workOrderCode?: string; // ç”Ÿæˆçš„工单编码
    remark?: string; // å¤‡æ³¨
    items?: PlanItem[]; // å¤‡ä»¶éœ€æ±‚明细
    createTime?: string; // åˆ›å»ºæ—¶é—´
  }
  /** å¤‡ä»¶éœ€æ±‚明细 */
  export interface PlanItem {
    id?: number; // ç¼–号
    itemId: number; // ç‰©æ–™/备件编号
    itemName?: string; // ç‰©æ–™åç§°
    itemCode?: string; // ç‰©æ–™ç¼–码
    unitMeasureName?: string; // è®¡é‡å•位名称
    quantity: number; // éœ€æ±‚数量
    remark?: string; // å¤‡æ³¨
  }
}
/** åˆ›å»ºç”µåŠ›ä½œä¸šè®¡åˆ’ */
export function createPlan(data: MesProMaintenancePlanApi.MaintenancePlan) {
  return requestClient.post('/mes/pro/maintenance-plan/create', data);
}
/** æ›´æ–°ç”µåŠ›ä½œä¸šè®¡åˆ’ */
export function updatePlan(data: MesProMaintenancePlanApi.MaintenancePlan) {
  return requestClient.put('/mes/pro/maintenance-plan/update', data);
}
/** åˆ é™¤ç”µåŠ›ä½œä¸šè®¡åˆ’ */
export function deletePlan(ids: number[]) {
  return requestClient.delete('/mes/pro/maintenance-plan/delete', { params: { ids: ids.join(',') } });
}
/** èŽ·å–ç”µåŠ›ä½œä¸šè®¡åˆ’è¯¦æƒ… */
export function getPlan(id: number) {
  return requestClient.get<MesProMaintenancePlanApi.MaintenancePlan>(
    `/mes/pro/maintenance-plan/get?id=${id}`,
  );
}
/** èŽ·å–ç”µåŠ›ä½œä¸šè®¡åˆ’åˆ†é¡µ */
export function getPlanPage(params: PageParam) {
  return requestClient.get<PageResult<MesProMaintenancePlanApi.MaintenancePlan>>(
    '/mes/pro/maintenance-plan/page',
    { params },
  );
}
/** å¯¼å‡ºç”µåŠ›ä½œä¸šè®¡åˆ’ */
export function exportPlan(params: any) {
  return requestClient.download('/mes/pro/maintenance-plan/export-excel', { params });
}
/** ä¸‹å‘电力作业计划 */
export function issuePlan(id: number) {
  return requestClient.put(`/mes/pro/maintenance-plan/issue?id=${id}`);
}
/** å®Œæˆç”µåŠ›ä½œä¸šè®¡åˆ’ */
export function finishPlan(id: number) {
  return requestClient.put(`/mes/pro/maintenance-plan/finish?id=${id}`);
}
/** æ ¹æ®å¤‡ä»¶éœ€æ±‚生成采购计划 */
export function generatePurchasePlan(id: number) {
  return requestClient.post<number>(`/mes/pro/maintenance-plan/generate-purchase-plan?id=${id}`);
}
/** ç”Ÿæˆè¿ç»´/发电/检修工单 */
export function generateWorkOrder(id: number) {
  return requestClient.post<number>(`/mes/pro/maintenance-plan/generate-work-order?id=${id}`);
}
src/api/mes/pro/process/index.ts
@@ -8,6 +8,7 @@
    id?: number;
    code?: string;
    name?: string;
    type?: number; // ç”µåŠ›å·¥è‰ºåˆ†ç±»
    attention?: string;
    status?: number;
    remark?: string;
src/api/mes/pro/task/index.ts
@@ -27,6 +27,8 @@
    workstationId?: number; // å·¥ä½œç«™ç¼–号
    workstationCode?: string; // å·¥ä½œç«™ç¼–码
    workstationName?: string; // å·¥ä½œç«™åç§°
    teamId?: number; // ç­ç»„编号
    teamName?: string; // ç­ç»„名称
    routeId?: number; // å·¥è‰ºè·¯çº¿ç¼–号
    processId?: number; // å·¥åºç¼–号
    processName?: string; // å·¥åºåç§°
src/api/mes/pro/workorder/index.ts
@@ -24,6 +24,8 @@
    clientId?: number; // å®¢æˆ·ç¼–号
    clientCode?: string; // å®¢æˆ·ç¼–码
    clientName?: string; // å®¢æˆ·åç§°
    saleOrderId?: number; // å…³è”营销订单(ERP销售订单)编号
    saleOrderNo?: string; // å…³è”营销订单(ERP销售订单)单号
    vendorId?: number; // ä¾›åº”商编号
    vendorName?: string; // ä¾›åº”商名称
    vendorCode?: string; // ä¾›åº”商编码
src/api/mes/qc/outsourcetest/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesQcOutsourceTestApi {
  /** å§”外检测/试验单 */
  export interface OutsourceTest {
    id?: number;
    code?: string; // å•据编码
    name?: string; // å•据名称
    testType?: number; // æ£€æµ‹ç±»åž‹ï¼š1第三方检测 2委外试验
    itemId?: number; // æ£€æµ‹å¯¹è±¡ç¼–号
    itemName?: string; // æ£€æµ‹å¯¹è±¡åç§°
    testOrg?: string; // æ£€æµ‹æœºæž„
    testDate?: string; // æ£€æµ‹æ—¥æœŸ
    result?: number; // æ£€æµ‹ç»“果:1合格 2不合格
    reportNo?: string; // æ£€æµ‹æŠ¥å‘Šç¼–号
    status?: number; // çŠ¶æ€ï¼š0草稿 10已提交 20已完成
    remark?: string; // å¤‡æ³¨
    blobIds?: number[]; // é™„ä»¶
    attachmentList?: { id: number; name?: string; url?: string }[];
    createTime?: string;
  }
}
/** åˆ›å»ºå§”外检测/试验单 */
export function createTest(data: MesQcOutsourceTestApi.OutsourceTest) {
  return requestClient.post('/mes/qc/outsource-test/create', data);
}
/** æ›´æ–°å§”外检测/试验单 */
export function updateTest(data: MesQcOutsourceTestApi.OutsourceTest) {
  return requestClient.put('/mes/qc/outsource-test/update', data);
}
/** åˆ é™¤å§”外检测/试验单 */
export function deleteTest(ids: number[]) {
  return requestClient.delete('/mes/qc/outsource-test/delete', { params: { ids: ids.join(',') } });
}
/** èŽ·å–å§”å¤–æ£€æµ‹/试验单详情 */
export function getTest(id: number) {
  return requestClient.get<MesQcOutsourceTestApi.OutsourceTest>(
    `/mes/qc/outsource-test/get?id=${id}`,
  );
}
/** èŽ·å–å§”å¤–æ£€æµ‹/试验单分页 */
export function getTestPage(params: PageParam) {
  return requestClient.get<PageResult<MesQcOutsourceTestApi.OutsourceTest>>(
    '/mes/qc/outsource-test/page',
    { params },
  );
}
/** æäº¤å§”外检测/试验单 */
export function submitTest(id: number) {
  return requestClient.put(`/mes/qc/outsource-test/submit?id=${id}`);
}
/** å®Œæˆå§”外检测/试验单 */
export function finishTest(id: number) {
  return requestClient.put(`/mes/qc/outsource-test/finish?id=${id}`);
}
src/api/wls/productissue/index.ts
@@ -16,6 +16,7 @@
    clientCode?: string; // å®¢æˆ·ç¼–码
    clientName?: string; // å®¢æˆ·åç§°
    requiredTime?: number; // éœ€æ±‚æ—¶é—´
    issueType?: number; // å•据类型(1=正常领料,2=紧急补料)
    status?: number; // å•据状态
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
src/api/wls/stocktaking/task/line/index.ts
@@ -36,6 +36,20 @@
  >('/mes/wm/stocktaking-task-line/page', { params });
}
/** ä¸‹è½½ç›˜ç‚¹å®žç›˜æ•°å¯¼å…¥æ¨¡æ¿ */
export function importStockTakingQuantityTemplate() {
  return requestClient.download('/mes/wm/stocktaking-task-line/get-import-template');
}
/** å¯¼å…¥ç›˜ç‚¹å®žç›˜æ•°é‡ */
export function importStockTakingQuantity(taskId: number, file: File) {
  return requestClient.upload<{
    successCount?: number;
    failureCount?: number;
    failureMessages?: string[];
  }>(`/mes/wm/stocktaking-task-line/import-excel?taskId=${taskId}`, { file });
}
/** æŸ¥è¯¢ç›˜ç‚¹ä»»åŠ¡è¡Œç²¾ç®€åˆ—è¡¨ */
export function getStockTakingTaskLineSimpleList(taskId: number) {
  return requestClient.get<WlsStockTakingTaskLineApi.StockTakingTaskLine[]>(
src/api/wls/warehouse/area/index.ts
@@ -8,6 +8,7 @@
    id?: number; // åº“位编号
    code?: string; // åº“位编码
    name?: string; // åº“位名称
    type?: number; // åº“区类型
    warehouseId?: number; // ä»“库编号
    warehouseName?: string; // ä»“库名称
    locationId?: number; // åº“区编号
src/api/wls/warehouse/index.ts
@@ -8,6 +8,7 @@
    id?: number; // ä»“库编号
    code?: string; // ä»“库编码
    name?: string; // ä»“库名称
    type?: number; // ä»“库类型
    address?: string; // åœ°å€
    area?: number; // é¢ç§¯
    chargeUserId?: number; // è´Ÿè´£äºº
src/components/table-action/icons.ts
@@ -13,4 +13,5 @@
  CLOSE: 'lucide:x',
  BOOK: 'lucide:book',
  AUDIT: 'lucide:file-check',
  CALC: 'lucide:calculator',
};
src/packages/constants/src/biz-mes-enum.ts
@@ -249,12 +249,16 @@
  SELF: 1, // è‡ªè¡Œç”Ÿäº§
  OUTSOURCE: 2, // ä»£å·¥
  PURCHASE: 3, // é‡‡è´­
  POWER_OPERATION: 4, // ç”µåŠ›è¿ç»´
  POWER_GENERATION: 5, // å‘电
  POWER_MAINTENANCE: 6, // æ£€ä¿®
} as const;
/** MES å·¥å•来源类型枚举 */
export const MesProWorkOrderSourceTypeEnum = {
  ORDER: 1, // å®¢æˆ·è®¢å•
  STORE: 2, // åº“存备货
  MAINTENANCE_PLAN: 3, // è¿ç»´è®¡åˆ’
} as const;
/** MES ç”Ÿäº§ä»»åŠ¡çŠ¶æ€æžšä¸¾ */
src/packages/constants/src/dict-enum.ts
@@ -127,6 +127,18 @@
  CRM_SALES_TARGET_PERIOD_TYPE: 'crm_sales_target_period_type', // CRM é”€å”®ç›®æ ‡å‘¨æœŸç±»åž‹
  CRM_SALES_TARGET_DIMENSION: 'crm_sales_target_dimension', // CRM é”€å”®ç›®æ ‡ç»´åº¦
  CRM_SALES_TARGET_STATUS: 'crm_sales_target_status', // CRM é”€å”®ç›®æ ‡çŠ¶æ€
  CRM_CUSTOMER_TYPE: 'crm_customer_type', // CRM å®¢æˆ·åˆ†ç±»ï¼ˆç”µåŠ›ï¼‰
  CRM_CREDIT_LEVEL: 'crm_credit_level', // CRM å®¢æˆ·ä¿¡ç”¨ç­‰çº§
  CRM_QUOTATION_TYPE: 'crm_quotation_type', // CRM æŠ¥ä»·å•类型
  CRM_TARIFF_MODE: 'crm_tariff_mode', // CRM ç”µä»·æ¨¡å¼
  CRM_CONTRACT_TYPE: 'crm_contract_type', // CRM åˆåŒç±»åž‹
  CRM_VOLTAGE_LEVEL: 'crm_voltage_level', // CRM ç”µåŽ‹ç­‰çº§
  CRM_ELECTRICITY_TYPE: 'crm_electricity_type', // CRM ç”¨ç”µç±»åž‹
  CRM_METER_MODE: 'crm_meter_mode', // CRM è®¡é‡æ–¹å¼
  CRM_COMMAND_TYPE: 'crm_command_type', // CRM åˆåŒæŒ‡ä»¤ç±»åž‹
  CRM_COMMAND_STATUS: 'crm_command_status', // CRM åˆåŒæŒ‡ä»¤çŠ¶æ€
  CRM_REFUND_TYPE: 'crm_refund_type', // CRM é€€è´¹ç±»åž‹
  CRM_CANCELLATION_TYPE: 'crm_cancellation_type', // CRM é”€æˆ·ç±»åž‹
} as const;
/** ========== AFTERSALES - å”®åŽæœåŠ¡æ¨¡å— ========== */
@@ -212,6 +224,13 @@
/** ========== MES - åˆ¶é€ æ‰§è¡Œæ¨¡å— ========== */
const MES_DICT = {
  MES_MD_ITEM_OR_PRODUCT: 'mes_md_item_or_product', // MES ç‰©æ–™/产品标识
  MES_MATERIAL_CATEGORY: 'mes_material_category', // MES ç”µåŠ›ç‰©èµ„åˆ†ç±»
  MES_PRO_MAINTENANCE_PLAN_TYPE: 'mes_pro_maintenance_plan_type', // MES ç”µåŠ›ä½œä¸šè®¡åˆ’ç±»åž‹
  MES_PRO_MAINTENANCE_PLAN_STATUS: 'mes_pro_maintenance_plan_status', // MES ç”µåŠ›ä½œä¸šè®¡åˆ’çŠ¶æ€
  MES_QC_OUTSOURCE_TEST_TYPE: 'mes_qc_outsource_test_type', // MES å§”外检测类型
  MES_QC_OUTSOURCE_TEST_RESULT: 'mes_qc_outsource_test_result', // MES å§”外检测结果
  MES_PRO_PROCESS_TYPE: 'mes_pro_process_type', // MES ç”µåŠ›å·¥è‰ºåˆ†ç±»
  MES_PRO_EMERGENCY_PLAN_TYPE: 'mes_pro_emergency_plan_type', // MES åº”急处置预案类型
  MES_MD_AUTO_CODE_CYCLE_METHOD: 'mes_md_auto_code_cycle_method', // MES ç¼–码规则循环方式
  MES_MD_AUTO_CODE_PADDED_METHOD: 'mes_md_auto_code_padded_method', // MES ç¼–码规则补齐方式
  MES_MD_AUTO_CODE_PART_TYPE: 'mes_md_auto_code_part_type', // MES ç¼–码规则分段类型
@@ -252,6 +271,10 @@
  MES_PD_VERSION_CHANGE_TYPE: 'mes_pd_version_change_type', // MES ç‰ˆæœ¬å˜æ›´ç±»åž‹
  MES_PD_DOCUMENT_TYPE: 'mes_pd_document_type', // MES è®¾è®¡èµ„料类型
  MES_PD_ARCHIVE_STATUS: 'mes_pd_archive_status', // MES è®¾è®¡å½’档状态
  PRODUCT_CONDUCTOR_TYPE: 'product_conductor_type', // æ¢ä½å¯¼çº¿å¯¼ä½“类型
  PRODUCT_REEL_TYPE: 'product_reel_type', // æ¢ä½å¯¼çº¿ç›˜å…·ç±»åž‹
  PRODUCT_TECH_CONFIRM_TEMPLATE: 'product_tech_confirm_template', // æŠ€æœ¯ç¡®è®¤å‡½æ¨¡æ¿ç±»åž‹
  PRODUCT_TECH_CONFIRM_STATUS: 'product_tech_confirm_status', // æŠ€æœ¯ç¡®è®¤å‡½çŠ¶æ€
  MES_PRO_FEEDBACK_CHANNEL: 'mes_pro_feedback_channel', // MES ç”Ÿäº§æŠ¥å·¥é€”径
  MES_PRO_ANDON_STATUS: 'mes_pro_andon_status', // MES å®‰ç¯å¤„置状态
  MES_PRO_ANDON_LEVEL: 'mes_pro_andon_level', // MES å®‰ç¯çº§åˆ«
@@ -270,6 +293,7 @@
  MES_WM_BARCODE_FORMAT: 'mes_wm_barcode_format', // MES æ¡ç æ ¼å¼
  MES_WM_PACKAGE_STATUS: 'mes_wm_package_status', // MES è£…箱单状态
  MES_WM_PRODUCT_SALES_STATUS: 'mes_wm_product_sales_status', // MES é”€å”®å‡ºåº“单状态
  MES_WM_WAREHOUSE_TYPE: 'mes_wm_warehouse_type', // MES ä»“库/库区类型
  MES_WM_QUALITY_STATUS: 'mes_wm_quality_status', // MES è´¨é‡çŠ¶æ€
  MES_WM_MISC_ISSUE_TYPE: 'mes_wm_misc_issue_type', // MES æ‚项出库类型
  MES_WM_MISC_ISSUE_STATUS: 'mes_wm_misc_issue_status', // MES æ‚项出库单状态
@@ -285,6 +309,7 @@
  MES_WM_RETURN_ISSUE_STATUS: 'mes_wm_return_issue_status', // MES ç”Ÿäº§é€€æ–™å•状态
  MES_WM_RETURN_ISSUE_TYPE: 'mes_wm_return_issue_type', // MES é€€æ–™ç±»åž‹
  MES_WM_PRODUCT_ISSUE_STATUS: 'mes_wm_product_issue_status', // MES é¢†æ–™å‡ºåº“单状态
  MES_WM_PRODUCT_ISSUE_TYPE: 'mes_wm_product_issue_type', // MES ç”Ÿäº§é¢†æ–™å•据类型
  MES_WM_PRODUCT_RECEIPT_STATUS: 'mes_wm_product_receipt_status', // MES äº§å“å…¥åº“单状态
  MES_WM_STOCK_TAKING_TYPE: 'mes_wm_stock_taking_type', // MES ç›˜ç‚¹ç±»åž‹
  MES_WM_STOCK_TAKING_TASK_STATUS: 'mes_wm_stock_taking_task_status', // MES ç›˜ç‚¹ä»»åŠ¡çŠ¶æ€
@@ -296,6 +321,12 @@
  MES_PD_TEMPLATE_TYPE: 'mes_pd_template_type', // MES å·¥è‰ºæ¨¡æ¿ç±»åž‹
  MES_NCR_STATUS: 'mes_ncr_status', // MES ä¸åˆæ ¼å“å¤„理单状态
  MES_NCR_DISPOSITION: 'mes_ncr_disposition', // MES ä¸åˆæ ¼å“å¤„置方式
  MES_PD_PRODUCT_ARCHIVE_TYPE: 'mes_pd_product_archive_type', // MES äº§å“æ¡£æ¡ˆç±»åž‹
  MES_PD_AUDIT_STATUS: 'mes_pd_audit_status', // MES äº§å“è®¾è®¡å®¡æ‰¹çŠ¶æ€
  MES_PD_PRODUCT_STATUS: 'mes_pd_product_status', // MES äº§å“æ¡£æ¡ˆå¯ç”¨çŠ¶æ€
  MES_PD_PRICE_OBJECT_TYPE: 'mes_pd_price_object_type', // MES å®šä»·å¯¹è±¡ç±»åž‹
  MES_PD_DISCOUNT_POLICY_TYPE: 'mes_pd_discount_policy_type', // MES ä¼˜æƒ ç­–略类型
  MES_PD_TIERED_PRICE_STATUS: 'mes_pd_tiered_price_status', // MES é˜¶æ¢¯ç”µä»·å¯ç”¨çŠ¶æ€
} as const;
/** ========== WMS - ä»“储管理模块 ========== */
@@ -317,6 +348,7 @@
const HRM_DICT = {
  HRM_DEGREE: 'hrm_degree', // HRM å­¦åކ
  HRM_EMPLOYEE_STATUS: 'hrm_employee_status', // HRM å‘˜å·¥çŠ¶æ€
  HRM_CERTIFICATE_TYPE: 'hrm_certificate_type', // HRM è¯ä¹¦èµ„质类型
  HRM_GENDER: 'hrm_gender', // HRM æ€§åˆ«
  HRM_AUDIT_STATUS: 'hrm_audit_status', // HRM å®¡æ‰¹çŠ¶æ€
  HRM_LEAVE_TYPE: 'hrm_leave_type', // HRM è¯·å‡ç±»åž‹
src/views/aftersales/ticket/data.ts
@@ -25,6 +25,10 @@
  { label: '一般问题', value: 1 },
  { label: '维修问题', value: 2 },
  { label: '退货问题', value: 3 },
  { label: '停电报修', value: 4 },
  { label: '用电故障', value: 5 },
  { label: '服务投诉', value: 6 },
  { label: '供电质量异议', value: 7 },
];
/** ä¸¥é‡ç¨‹åº¦é€‰é¡¹ */
src/views/crm/cancellation/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,227 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmCancellationApi } from '#/api/crm/cancellation';
import type { CrmCustomerApi } from '#/api/crm/customer';
import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
/** æ–°å¢ž/修改销户申请 */
export function useFormSchema(
  formApi?: VbenFormApi,
): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'cancellationNo',
      label: '销户编号',
      component: 'Input',
      componentProps: {
        placeholder: '不填则自动生成',
      },
    },
    {
      fieldName: 'customerId',
      label: '客户',
      component: markRaw(CrmCustomerSelect),
      componentProps: {
        placeholder: '请选择客户',
        onChange: (item?: CrmCustomerApi.Customer) => {
          formApi?.setFieldValue('customerName', item?.name);
        },
      },
      rules: 'required',
    },
    {
      fieldName: 'customerName',
      label: '客户名称',
      component: 'Input',
      componentProps: {
        placeholder: '客户名称',
        readonly: true,
      },
    },
    {
      fieldName: 'cancellationType',
      label: '销户类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.CRM_CANCELLATION_TYPE),
        placeholder: '请选择销户类型',
      },
      rules: 'required',
    },
    {
      fieldName: 'contractNo',
      label: '合同编号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入关联合同编号',
      },
    },
    {
      fieldName: 'expectedCancelDate',
      label: '预计销户日期',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
      fieldName: 'actualCancelDate',
      label: '实际销户日期',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
      fieldName: 'refundAmount',
      label: '退费金额',
      component: 'InputNumber',
      componentProps: {
        class: 'w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入退费金额(元)',
      },
    },
    {
      fieldName: 'applyUserName',
      label: '申请人',
      component: 'Input',
      componentProps: {
        placeholder: '请输入申请人姓名',
      },
    },
    {
      fieldName: 'applyTime',
      label: '申请时间',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD HH:mm:ss',
        showTime: true,
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
      fieldName: 'cancelReason',
      label: '销户原因',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入销户原因',
        rows: 2,
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'cancellationNo',
      label: '销户编号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入销户编号',
      },
    },
    {
      fieldName: 'cancellationType',
      label: '销户类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.CRM_CANCELLATION_TYPE),
        placeholder: '请选择销户类型',
      },
    },
    {
      fieldName: 'auditStatus',
      label: '审批状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.CRM_AUDIT_STATUS, 'number'),
        placeholder: '请选择审批状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<CrmCancellationApi.Cancellation>['columns'] {
  return [
    { field: 'cancellationNo', title: '销户编号', minWidth: 170 },
    { field: 'customerName', title: '客户名称', minWidth: 180 },
    {
      field: 'cancellationType',
      title: '销户类型',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_CANCELLATION_TYPE },
      },
    },
    { field: 'contractNo', title: '合同编号', minWidth: 140 },
    {
      field: 'expectedCancelDate',
      title: '预计销户日期',
      width: 120,
      formatter: 'formatDate',
    },
    {
      field: 'actualCancelDate',
      title: '实际销户日期',
      width: 120,
      formatter: 'formatDate',
    },
    { field: 'refundAmount', title: '退费金额', width: 120 },
    { field: 'applyUserName', title: '申请人', width: 100 },
    {
      field: 'auditStatus',
      title: '审批状态',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
      },
    },
    {
      title: '操作',
      width: 200,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/crm/cancellation/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,189 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmCancellationApi } from '#/api/crm/cancellation';
import { Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  auditCancellation,
  deleteCancellation,
  exportCancellation,
  getCancellationPage,
} from '#/api/crm/cancellation';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** å¯¼å‡ºè¡¨æ ¼ */
async function handleExport() {
  const formValues = await gridApi.formApi.getValues();
  const data = await exportCancellation(formValues);
  downloadFileFromBlobPart({ fileName: '销户申请.xls', source: data });
}
/** æ–°å¢ž */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** ç¼–辑 */
function handleEdit(row: CrmCancellationApi.Cancellation) {
  formModalApi.setData({ cancellation: row }).open();
}
/** åˆ é™¤ */
async function handleDelete(row: CrmCancellationApi.Cancellation) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.cancellationNo ?? '']),
    duration: 0,
  });
  try {
    await deleteCancellation(row.id!);
    message.success(
      $t('ui.actionMessage.deleteSuccess', [row.cancellationNo ?? '']),
    );
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** å®¡æ‰¹ï¼ˆé€šè¿‡/驳回) */
async function handleAudit(
  row: CrmCancellationApi.Cancellation,
  pass: boolean,
) {
  const hideLoading = message.loading({
    content: pass ? '审批通过中...' : '审批驳回中...',
    duration: 0,
  });
  try {
    await auditCancellation({ id: row.id!, auditStatus: pass ? 20 : 30 });
    message.success(pass ? '审批通过' : '审批驳回');
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getCancellationPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<CrmCancellationApi.Cancellation>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="销户申请列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['销户申请']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['crm:cancellation:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['crm:cancellation:export'],
              onClick: handleExport,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '通过',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['crm:cancellation:update'],
              ifShow: row.auditStatus === 0 || row.auditStatus === 10,
              popConfirm: {
                title: '确认审批通过该销户申请?',
                confirm: handleAudit.bind(null, row, true),
              },
            },
            {
              label: '驳回',
              type: 'link',
              danger: true,
              icon: ACTION_ICON.AUDIT,
              auth: ['crm:cancellation:update'],
              ifShow: row.auditStatus === 0 || row.auditStatus === 10,
              popConfirm: {
                title: '确认驳回该销户申请?',
                confirm: handleAudit.bind(null, row, false),
              },
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['crm:cancellation:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['crm:cancellation:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [
                  row.cancellationNo ?? '',
                ]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/crm/cancellation/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
<script lang="ts" setup>
import type { CrmCancellationApi } from '#/api/crm/cancellation';
import { computed, ref } from 'vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import {
  createCancellation,
  getCancellation,
  updateCancellation,
} from '#/api/crm/cancellation';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
type CancellationFormModalData = {
  cancellation?: Pick<CrmCancellationApi.Cancellation, 'id'>;
};
const emit = defineEmits(['success']);
const formData = ref<Partial<CrmCancellationApi.Cancellation>>();
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['销户申请'])
    : $t('ui.actionTitle.create', ['销户申请']);
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 110,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data =
      (await formApi.getValues()) as CrmCancellationApi.Cancellation;
    if (formData.value?.id) {
      data.id = formData.value.id;
    }
    try {
      await (formData.value?.id
        ? updateCancellation(data)
        : createCancellation(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    formApi.setState({ schema: useFormSchema(formApi) });
    await formApi.resetForm();
    const data = modalApi.getData() as null | CancellationFormModalData;
    if (!data?.cancellation?.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getCancellation(data.cancellation.id);
      if (formData.value) {
        await formApi.setValues(formData.value as any);
      }
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/crm/contract/components/command-form-data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,105 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
/** æ–°å¢ž/修改合同指令 */
export function useFormSchema(
  formApi?: VbenFormApi,
): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'contractId',
      label: '合同ID',
      component: 'InputNumber',
      componentProps: {
        class: 'w-full',
        precision: 0,
        placeholder: '合同ID',
      },
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'contractNo',
      label: '合同编号',
      component: 'Input',
      componentProps: {
        placeholder: '合同编号',
        readonly: true,
      },
    },
    {
      fieldName: 'commandType',
      label: '指令类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.CRM_COMMAND_TYPE),
        placeholder: '请选择指令类型',
        allowClear: true,
      },
      rules: 'required',
    },
    {
      fieldName: 'targetModule',
      label: '目标模块',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: [
          { label: 'MES å·¥å•', value: 'mes-work-order' },
          { label: 'MES æŠ„表', value: 'mes-metering' },
          { label: 'SRM æœåŠ¡å•', value: 'srm-service' },
        ],
        placeholder: '请选择目标模块',
      },
    },
    {
      fieldName: 'bizNo',
      label: '业务单号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入业务单号',
      },
    },
    {
      fieldName: 'operatorName',
      label: '操作人',
      component: 'Input',
      componentProps: {
        placeholder: '请输入操作人姓名',
      },
    },
    {
      fieldName: 'operateTime',
      label: '操作时间',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD HH:mm:ss',
        showTime: true,
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
src/views/crm/contract/components/command-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
<script lang="ts" setup>
import type { CrmContractCommandApi } from '#/api/crm/contract-command';
import { ref } from 'vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import {
  createContractCommand,
  updateContractCommand,
} from '#/api/crm/contract-command';
import { $t } from '#/locales';
import { useFormSchema } from './command-form-data';
const emit = defineEmits(['success']);
const formData = ref<Partial<CrmContractCommandApi.ContractCommand>>();
const getTitle = () => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['指令'])
    : $t('ui.actionTitle.create', ['指令']);
};
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 110,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data =
      (await formApi.getValues()) as CrmContractCommandApi.ContractCommand;
    if (formData.value?.id) {
      data.id = formData.value.id;
    }
    try {
      await (formData.value?.id
        ? updateContractCommand(data)
        : createContractCommand(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    formApi.setState({ schema: useFormSchema(formApi) });
    await formApi.resetForm();
    const data = modalApi.getData() as null | {
      command?: CrmContractCommandApi.ContractCommand;
      contractNo?: string;
    };
    if (data?.contractNo) {
      await formApi.setFieldValue('contractNo', data.contractNo);
    }
    if (!data?.command?.id) {
      return;
    }
    formData.value = data.command;
    await formApi.setValues(formData.value as any);
  },
});
</script>
<template>
  <Modal :title="getTitle()" class="w-2/3">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/crm/contract/components/command-list-data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmContractCommandApi } from '#/api/crm/contract-command';
import { DICT_TYPE } from '@vben/constants';
/** åˆåŒæŒ‡ä»¤è¯¦æƒ…列表字段 */
export function useDetailListColumns(): VxeTableGridOptions<CrmContractCommandApi.ContractCommand>['columns'] {
  return [
    {
      field: 'commandType',
      title: '指令类型',
      width: 120,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_COMMAND_TYPE },
      },
    },
    {
      field: 'targetModule',
      title: '目标模块',
      width: 130,
    },
    {
      field: 'bizNo',
      title: '业务单号',
      minWidth: 140,
    },
    {
      field: 'operatorName',
      title: '操作人',
      width: 100,
    },
    {
      field: 'operateTime',
      title: '操作时间',
      minWidth: 160,
      formatter: 'formatDateTime',
    },
    {
      field: 'status',
      title: '状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_COMMAND_STATUS },
      },
    },
    {
      title: '操作',
      width: 220,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/crm/contract/components/command-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,188 @@
<!-- åˆåŒæŒ‡ä»¤ä¸‹å‘列表:用于【合同】详情 Tab,展示并下发/执行/取消合同指令 -->
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmContractCommandApi } from '#/api/crm/contract-command';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  cancelContractCommand,
  deleteContractCommand,
  executeContractCommand,
  getContractCommandPage,
} from '#/api/crm/contract-command';
import { $t } from '#/locales';
import CommandForm from './command-form.vue';
import { useDetailListColumns } from './command-list-data';
const props = defineProps<{
  contractId?: number; // åˆåŒç¼–号
  contractNo?: string; // åˆåŒå•号
}>();
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: CommandForm,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»ºæŒ‡ä»¤ */
function handleCreate() {
  formModalApi
    .setData({ contractNo: props.contractNo })
    .open();
}
/** ç¼–辑指令 */
function handleEdit(row: CrmContractCommandApi.ContractCommand) {
  formModalApi.setData({ command: row }).open();
}
/** æ‰§è¡ŒæŒ‡ä»¤ */
async function handleExecute(row: CrmContractCommandApi.ContractCommand) {
  const hideLoading = message.loading({ content: '指令执行中...', duration: 0 });
  try {
    await executeContractCommand(row.id!);
    message.success('指令执行完成');
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** å–消指令 */
async function handleCancel(row: CrmContractCommandApi.ContractCommand) {
  const hideLoading = message.loading({ content: '指令取消中...', duration: 0 });
  try {
    await cancelContractCommand(row.id!);
    message.success('指令已取消');
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** åˆ é™¤æŒ‡ä»¤ */
async function handleDelete(row: CrmContractCommandApi.ContractCommand) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.bizNo ?? '']),
    duration: 0,
  });
  try {
    await deleteContractCommand(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.bizNo ?? '']));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useDetailListColumns(),
    height: 400,
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }) => {
          if (!props.contractId) {
            return { list: [], total: 0 };
          }
          return await getContractCommandPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            contractId: props.contractId,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: false,
    },
  } as VxeTableGridOptions<CrmContractCommandApi.ContractCommand>,
});
</script>
<template>
  <div>
    <FormModal @success="handleRefresh" />
    <Grid>
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: '下发指令',
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['crm:contract-command:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '执行',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['crm:contract-command:update'],
              ifShow: row.status === 0 || row.status === 10,
              popConfirm: {
                title: '确认执行该指令?',
                confirm: handleExecute.bind(null, row),
              },
            },
            {
              label: '取消',
              type: 'link',
              danger: true,
              icon: ACTION_ICON.CLOSE,
              auth: ['crm:contract-command:update'],
              ifShow: row.status === 0 || row.status === 10,
              popConfirm: {
                title: '确认取消该指令?',
                confirm: handleCancel.bind(null, row),
              },
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['crm:contract-command:update'],
              ifShow: row.status === 0,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['crm:contract-command:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [
                  row.bizNo ?? '',
                ]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </div>
</template>
src/views/crm/contract/data.ts
@@ -1,6 +1,8 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { useUserStore } from '@vben/stores';
import { erpPriceInputFormatter, erpPriceMultiply } from '@vben/utils';
@@ -199,6 +201,78 @@
      },
    },
    {
      fieldName: 'contractType',
      label: '合同类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.CRM_CONTRACT_TYPE),
        placeholder: '请选择合同类型',
        allowClear: true,
      },
    },
    {
      fieldName: 'voltageLevel',
      label: '电压等级',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.CRM_VOLTAGE_LEVEL),
        placeholder: '请选择电压等级',
        allowClear: true,
      },
    },
    {
      fieldName: 'electricityType',
      label: '用电类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.CRM_ELECTRICITY_TYPE),
        placeholder: '请选择用电类型',
        allowClear: true,
      },
    },
    {
      fieldName: 'contractCapacity',
      label: '合同容量(kVA)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入合同容量',
      },
    },
    {
      fieldName: 'tariffMode',
      label: '电价模式',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.CRM_TARIFF_MODE),
        placeholder: '请选择电价模式',
        allowClear: true,
      },
    },
    {
      fieldName: 'meterMode',
      label: '计量方式',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.CRM_METER_MODE),
        placeholder: '请选择计量方式',
        allowClear: true,
      },
    },
    {
      fieldName: 'payCycle',
      label: '缴费周期(月)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 1,
        precision: 0,
        placeholder: '请输入缴费周期',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
@@ -353,6 +427,34 @@
      slots: { default: 'businessName' },
    },
    {
      title: '合同类型',
      field: 'contractType',
      minWidth: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_CONTRACT_TYPE },
      },
    },
    {
      title: '电压等级',
      field: 'voltageLevel',
      minWidth: 90,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_VOLTAGE_LEVEL },
      },
    },
    {
      title: '合同容量',
      field: 'contractCapacity',
      minWidth: 100,
    },
    {
      title: '指令数',
      field: 'commandCount',
      minWidth: 80,
    },
    {
      title: '合同金额(元)',
      field: 'totalPrice',
      minWidth: 140,
src/views/crm/contract/detail/index.vue
@@ -26,6 +26,7 @@
import Form from '../modules/form.vue';
import { useDetailSchema } from './data';
import ContractDetailsInfo from './modules/info.vue';
import CommandDetailsList from '../components/command-list.vue';
const props = defineProps<{ id?: number }>();
@@ -171,6 +172,13 @@
        <Tabs.TabPane tab="操作日志" key="6" :force-render="true">
          <OperateLog :log-list="logList" />
        </Tabs.TabPane>
        <Tabs.TabPane tab="指令下发" key="7" :force-render="true">
          <CommandDetailsList
            v-if="contractId"
            :contract-id="contractId"
            :contract-no="contract.no"
          />
        </Tabs.TabPane>
      </Tabs>
    </Card>
  </Page>
src/views/crm/credit-rating/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,212 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmCustomerApi } from '#/api/crm/customer';
import type { CrmCreditRatingApi } from '#/api/crm/credit-rating';
import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
/** æ–°å¢ž/修改信用评级 */
export function useFormSchema(
  formApi?: VbenFormApi,
): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'customerId',
      label: '客户',
      component: markRaw(CrmCustomerSelect),
      componentProps: {
        placeholder: '请选择客户',
        onChange: (item?: CrmCustomerApi.Customer) => {
          formApi?.setFieldValue('customerName', item?.name);
        },
      },
    },
    {
      fieldName: 'customerName',
      label: '客户名称',
      component: 'Input',
      componentProps: {
        placeholder: '客户名称',
        readonly: true,
      },
    },
    {
      fieldName: 'level',
      label: '信用等级',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.CRM_CREDIT_LEVEL),
        placeholder: '请选择信用等级',
      },
      rules: 'required',
    },
    {
      fieldName: 'score',
      label: '信用分',
      component: 'InputNumber',
      componentProps: {
        class: 'w-full',
        min: 0,
        max: 100,
        precision: 0,
        placeholder: '0-100',
      },
    },
    {
      fieldName: 'evalType',
      label: '评定类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: [
          { label: '初评', value: 'INITIAL' },
          { label: '复评', value: 'REVIEW' },
          { label: '动态调整', value: 'ADJUST' },
        ],
        placeholder: '请选择评定类型',
      },
    },
    {
      fieldName: 'evalReason',
      label: '评定理由',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入评定理由',
        rows: 2,
      },
    },
    {
      fieldName: 'operateTime',
      label: '评定时间',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD HH:mm:ss',
        showTime: true,
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
      fieldName: 'validUntil',
      label: '有效期至',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
    },
    {
      fieldName: 'operatorName',
      label: '评定人',
      component: 'Input',
      componentProps: {
        placeholder: '请输入评定人姓名',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'level',
      label: '信用等级',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.CRM_CREDIT_LEVEL),
        placeholder: '请选择信用等级',
      },
    },
    {
      fieldName: 'evalType',
      label: '评定类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: [
          { label: '初评', value: 'INITIAL' },
          { label: '复评', value: 'REVIEW' },
          { label: '动态调整', value: 'ADJUST' },
        ],
        placeholder: '请选择评定类型',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<CrmCreditRatingApi.CreditRating>['columns'] {
  return [
    { field: 'customerName', title: '客户名称', minWidth: 180 },
    {
      field: 'level',
      title: '信用等级',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_CREDIT_LEVEL },
      },
    },
    { field: 'score', title: '信用分', width: 90 },
    {
      field: 'evalType',
      title: '评定类型',
      width: 120,
      slots: { default: 'evalType' },
    },
    { field: 'evalReason', title: '评定理由', minWidth: 200, showOverflow: true },
    { field: 'operatorName', title: '评定人', width: 100 },
    {
      field: 'operateTime',
      title: '评定时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      field: 'validUntil',
      title: '有效期至',
      width: 120,
      formatter: 'formatDate',
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 160,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/crm/credit-rating/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,156 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmCreditRatingApi } from '#/api/crm/credit-rating';
import { Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteCreditRating,
  exportCreditRating,
  getCreditRatingPage,
} from '#/api/crm/credit-rating';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** å¯¼å‡ºè¡¨æ ¼ */
async function handleExport() {
  const formValues = await gridApi.formApi.getValues();
  const data = await exportCreditRating(formValues);
  downloadFileFromBlobPart({ fileName: '客户信用评级.xls', source: data });
}
/** æ–°å¢ž */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** ç¼–辑 */
function handleEdit(row: CrmCreditRatingApi.CreditRating) {
  formModalApi.setData({ creditRating: row }).open();
}
/** åˆ é™¤ */
async function handleDelete(row: CrmCreditRatingApi.CreditRating) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.customerName ?? '']),
    duration: 0,
  });
  try {
    await deleteCreditRating(row.id!);
    message.success(
      $t('ui.actionMessage.deleteSuccess', [row.customerName ?? '']),
    );
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getCreditRatingPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<CrmCreditRatingApi.CreditRating>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="客户信用评级列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['信用评级']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['crm:credit-rating:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['crm:credit-rating:export'],
              onClick: handleExport,
            },
          ]"
        />
      </template>
      <template #evalType="{ row }">
        {{
          row.evalType === 'INITIAL'
            ? '初评'
            : row.evalType === 'REVIEW'
              ? '复评'
              : '动态调整'
        }}
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['crm:credit-rating:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['crm:credit-rating:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [
                  row.customerName ?? '',
                ]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/crm/credit-rating/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
<script lang="ts" setup>
import type { CrmCreditRatingApi } from '#/api/crm/credit-rating';
import { computed, ref } from 'vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import {
  createCreditRating,
  getCreditRating,
  updateCreditRating,
} from '#/api/crm/credit-rating';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
type CreditRatingFormModalData = {
  creditRating?: Pick<CrmCreditRatingApi.CreditRating, 'id'>;
};
const emit = defineEmits(['success']);
const formData = ref<Partial<CrmCreditRatingApi.CreditRating>>();
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['信用评级'])
    : $t('ui.actionTitle.create', ['信用评级']);
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 110,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as CrmCreditRatingApi.CreditRating;
    if (formData.value?.id) {
      data.id = formData.value.id;
    }
    try {
      await (formData.value?.id
        ? updateCreditRating(data)
        : createCreditRating(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    formApi.setState({ schema: useFormSchema(formApi) });
    await formApi.resetForm();
    const data = modalApi.getData() as null | CreditRatingFormModalData;
    if (!data?.creditRating?.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getCreditRating(data.creditRating.id);
      if (formData.value) {
        await formApi.setValues(formData.value as any);
      }
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/crm/customer/data.ts
@@ -45,6 +45,16 @@
      },
    },
    {
      fieldName: 'customerType',
      label: '客户分类',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_TYPE),
        placeholder: '请选择客户分类',
        allowClear: true,
      },
    },
    {
      fieldName: 'mobile',
      label: '手机',
      component: 'Input',
@@ -202,6 +212,26 @@
      },
    },
    {
      fieldName: 'customerType',
      label: '客户分类',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.CRM_CUSTOMER_TYPE),
        placeholder: '请选择客户分类',
        allowClear: true,
      },
    },
    {
      fieldName: 'creditLevel',
      label: '信用等级',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.CRM_CREDIT_LEVEL),
        placeholder: '请选择信用等级',
        allowClear: true,
      },
    },
    {
      fieldName: 'createTime',
      label: '创建时间',
      component: 'RangePicker',
@@ -276,6 +306,40 @@
      },
    },
    {
      field: 'customerType',
      title: '客户分类',
      minWidth: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_CUSTOMER_TYPE },
      },
    },
    {
      field: 'creditLevel',
      title: '信用等级',
      minWidth: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_CREDIT_LEVEL },
      },
    },
    {
      field: 'creditScore',
      title: '信用分',
      minWidth: 90,
    },
    {
      field: 'overdueAmount',
      title: '欠费金额',
      minWidth: 110,
      formatter: 'formatAmount2',
    },
    {
      field: 'overdueCount',
      title: '欠费笔数',
      minWidth: 90,
    },
    {
      field: 'mobile',
      title: '手机',
      minWidth: 120,
src/views/crm/refund/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,208 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmCustomerApi } from '#/api/crm/customer';
import type { CrmRefundApi } from '#/api/crm/refund';
import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
/** æ–°å¢ž/修改退费单 */
export function useFormSchema(
  formApi?: VbenFormApi,
): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'refundNo',
      label: '退费单号',
      component: 'Input',
      componentProps: {
        placeholder: '不填则自动生成',
      },
    },
    {
      fieldName: 'customerId',
      label: '客户',
      component: markRaw(CrmCustomerSelect),
      componentProps: {
        placeholder: '请选择客户',
        onChange: (item?: CrmCustomerApi.Customer) => {
          formApi?.setFieldValue('customerName', item?.name);
        },
      },
      rules: 'required',
    },
    {
      fieldName: 'customerName',
      label: '客户名称',
      component: 'Input',
      componentProps: {
        placeholder: '客户名称',
        readonly: true,
      },
    },
    {
      fieldName: 'refundType',
      label: '退费类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.CRM_REFUND_TYPE),
        placeholder: '请选择退费类型',
      },
      rules: 'required',
    },
    {
      fieldName: 'refundAmount',
      label: '退费金额',
      component: 'InputNumber',
      componentProps: {
        class: 'w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入退费金额(元)',
      },
      rules: 'required',
    },
    {
      fieldName: 'contractNo',
      label: '合同编号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入关联合同编号',
      },
    },
    {
      fieldName: 'applyUserName',
      label: '申请人',
      component: 'Input',
      componentProps: {
        placeholder: '请输入申请人姓名',
      },
    },
    {
      fieldName: 'applyTime',
      label: '申请时间',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD HH:mm:ss',
        showTime: true,
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
      fieldName: 'refundReason',
      label: '退费原因',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入退费原因',
        rows: 2,
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'refundNo',
      label: '退费单号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入退费单号',
      },
    },
    {
      fieldName: 'refundType',
      label: '退费类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.CRM_REFUND_TYPE),
        placeholder: '请选择退费类型',
      },
    },
    {
      fieldName: 'auditStatus',
      label: '审批状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.CRM_AUDIT_STATUS, 'number'),
        placeholder: '请选择审批状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<CrmRefundApi.Refund>['columns'] {
  return [
    { field: 'refundNo', title: '退费单号', minWidth: 170 },
    { field: 'customerName', title: '客户名称', minWidth: 180 },
    {
      field: 'refundType',
      title: '退费类型',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_REFUND_TYPE },
      },
    },
    { field: 'refundAmount', title: '退费金额', width: 120 },
    { field: 'contractNo', title: '合同编号', minWidth: 140 },
    { field: 'applyUserName', title: '申请人', width: 100 },
    {
      field: 'applyTime',
      title: '申请时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      field: 'auditStatus',
      title: '审批状态',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 200,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/crm/refund/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,184 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmRefundApi } from '#/api/crm/refund';
import { Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  auditRefund,
  deleteRefund,
  exportRefund,
  getRefundPage,
} from '#/api/crm/refund';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** å¯¼å‡ºè¡¨æ ¼ */
async function handleExport() {
  const formValues = await gridApi.formApi.getValues();
  const data = await exportRefund(formValues);
  downloadFileFromBlobPart({ fileName: '退费单.xls', source: data });
}
/** æ–°å¢ž */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** ç¼–辑 */
function handleEdit(row: CrmRefundApi.Refund) {
  formModalApi.setData({ refund: row }).open();
}
/** åˆ é™¤ */
async function handleDelete(row: CrmRefundApi.Refund) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.refundNo ?? '']),
    duration: 0,
  });
  try {
    await deleteRefund(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.refundNo ?? '']));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** å®¡æ‰¹ï¼ˆé€šè¿‡/驳回) */
async function handleAudit(row: CrmRefundApi.Refund, pass: boolean) {
  const hideLoading = message.loading({
    content: pass ? '审批通过中...' : '审批驳回中...',
    duration: 0,
  });
  try {
    await auditRefund({ id: row.id!, auditStatus: pass ? 20 : 30 });
    message.success(pass ? '审批通过' : '审批驳回');
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getRefundPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<CrmRefundApi.Refund>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="退费单列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['退费单']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['crm:refund:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['crm:refund:export'],
              onClick: handleExport,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '通过',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['crm:refund:update'],
              ifShow: row.auditStatus === 0 || row.auditStatus === 10,
              popConfirm: {
                title: '确认审批通过该退费单?',
                confirm: handleAudit.bind(null, row, true),
              },
            },
            {
              label: '驳回',
              type: 'link',
              danger: true,
              icon: ACTION_ICON.AUDIT,
              auth: ['crm:refund:update'],
              ifShow: row.auditStatus === 0 || row.auditStatus === 10,
              popConfirm: {
                title: '确认驳回该退费单?',
                confirm: handleAudit.bind(null, row, false),
              },
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['crm:refund:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['crm:refund:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [
                  row.refundNo ?? '',
                ]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/crm/refund/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,92 @@
<script lang="ts" setup>
import type { CrmRefundApi } from '#/api/crm/refund';
import { computed, ref } from 'vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import {
  createRefund,
  getRefund,
  updateRefund,
} from '#/api/crm/refund';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
type RefundFormModalData = {
  refund?: Pick<CrmRefundApi.Refund, 'id'>;
};
const emit = defineEmits(['success']);
const formData = ref<Partial<CrmRefundApi.Refund>>();
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['退费单'])
    : $t('ui.actionTitle.create', ['退费单']);
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 110,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as CrmRefundApi.Refund;
    if (formData.value?.id) {
      data.id = formData.value.id;
    }
    try {
      await (formData.value?.id ? updateRefund(data) : createRefund(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    formApi.setState({ schema: useFormSchema(formApi) });
    await formApi.resetForm();
    const data = modalApi.getData() as null | RefundFormModalData;
    if (!data?.refund?.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getRefund(data.refund.id);
      if (formData.value) {
        await formApi.setValues(formData.value as any);
      }
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/crm/saleQuotation/data.ts
@@ -1,6 +1,9 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { erpPriceMultiply } from '#/packages/utils/src';
import { z } from '#/adapter/form';
@@ -156,6 +159,81 @@
      },
    },
    {
      fieldName: 'quotationType',
      label: '报价类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.CRM_QUOTATION_TYPE),
        placeholder: '请选择报价类型',
        allowClear: true,
      },
    },
    {
      fieldName: 'capacity',
      label: '合同容量(kVA)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入合同容量',
      },
    },
    {
      fieldName: 'maxDemand',
      label: '最大需量(kW)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入最大需量',
      },
    },
    {
      fieldName: 'unitPrice',
      label: '电度单价(元/kWh)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 4,
        placeholder: '请输入电度单价',
      },
    },
    {
      fieldName: 'tariffMode',
      label: '电价模式',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.CRM_TARIFF_MODE),
        placeholder: '请选择电价模式',
        allowClear: true,
      },
    },
    {
      fieldName: 'powerFactorFee',
      label: '力调电费(元)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入力调电费',
      },
    },
    {
      fieldName: 'billingCycle',
      label: '结算周期(月)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 1,
        precision: 0,
        placeholder: '请输入结算周期',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
@@ -283,6 +361,15 @@
      fixed: 'left',
    },
    {
      field: 'quotationType',
      title: '报价类型',
      minWidth: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.CRM_QUOTATION_TYPE },
      },
    },
    {
      field: 'customerName',
      title: '客户名称',
      minWidth: 120,
src/views/erp/purchase/plan/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,242 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getDictOptions } from '@vben/hooks';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getRangePickerDefaultProps } from '#/utils';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'edit';
/** çŠ¶æ€å¸¸é‡ */
export const PURCHASE_PLAN_STATUS = {
  DRAFT: 0, // æœªæäº¤ï¼ˆè‰ç¨¿ï¼‰
  PROCESSING: 10, // å®¡æ‰¹ä¸­
  APPROVED: 20, // å®¡æ ¸é€šè¿‡
  REJECTED: 30, // å®¡æ ¸ä¸é€šè¿‡
  CANCELLED: 40, // å·²å–消
};
/** éœ€æ±‚来源常量 */
export const DEMAND_SOURCE = {
  STOCK_ALERT: 1, // åº“存预警
  PLAN: 2, // è¿ç»´/生产计划
};
/** è¡¨å•的配置项 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  const isReadonly = formType === 'detail';
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'no',
      label: '计划单号',
      component: 'Input',
      componentProps: {
        placeholder: '系统自动生成',
        disabled: true,
      },
    },
    {
      fieldName: 'name',
      label: '计划名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入计划名称',
        disabled: isReadonly,
      },
      rules: 'required',
    },
    {
      fieldName: 'planDate',
      label: '计划日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '选择计划日期',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        disabled: isReadonly,
      },
      rules: 'required',
    },
    {
      fieldName: 'demandSource',
      label: '需求来源',
      component: 'Select',
      componentProps: {
        placeholder: '请选择需求来源',
        allowClear: true,
        options: getDictOptions('erp_purchase_plan_demand_source', 'number'),
        disabled: isReadonly,
      },
    },
    {
      label: '供应商',
      fieldName: 'supplierId',
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请选择供应商',
        allowClear: true,
        showSearch: true,
        api: getSupplierSimpleList,
        labelField: 'name',
        valueField: 'id',
        disabled: isReadonly,
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        autoSize: { minRows: 1, maxRows: 1 },
        disabled: isReadonly,
      },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'items',
      label: '计划明细',
      component: 'Input',
      formItemClass: 'col-span-3',
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'no',
      label: '计划单号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入计划单号',
      },
    },
    {
      fieldName: 'name',
      label: '计划名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入计划名称',
      },
    },
    {
      fieldName: 'demandSource',
      label: '需求来源',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions('erp_purchase_plan_demand_source', 'number'),
        placeholder: '请选择需求来源',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: [
          { label: '未提交', value: PURCHASE_PLAN_STATUS.DRAFT },
          { label: '审批中', value: PURCHASE_PLAN_STATUS.PROCESSING },
          { label: '审核通过', value: PURCHASE_PLAN_STATUS.APPROVED },
          { label: '审核不通过', value: PURCHASE_PLAN_STATUS.REJECTED },
          { label: '已取消', value: PURCHASE_PLAN_STATUS.CANCELLED },
        ],
        placeholder: '请选择状态',
      },
    },
    {
      fieldName: 'planDate',
      label: '计划日期',
      component: 'RangePicker',
      componentProps: getRangePickerDefaultProps(),
    },
  ];
}
/** åˆ—表的表格列 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'checkbox', width: 50 },
    { type: 'seq', title: '序号', width: 60 },
    {
      field: 'no',
      title: '计划单号',
      minWidth: 140,
    },
    {
      field: 'name',
      title: '计划名称',
      minWidth: 180,
    },
    {
      field: 'demandSourceName',
      title: '需求来源',
      width: 110,
    },
    {
      field: 'planUserName',
      title: '编制人',
      width: 110,
    },
    {
      field: 'planDeptName',
      title: '编制部门',
      width: 120,
    },
    {
      field: 'supplierName',
      title: '供应商',
      minWidth: 130,
    },
    {
      field: 'productNames',
      title: '产品',
      minWidth: 180,
    },
    {
      field: 'totalCount',
      title: '合计数量',
      width: 100,
      formatter: 'formatAmount3',
    },
    {
      field: 'planDate',
      title: '计划日期',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      field: 'status',
      title: '状态',
      width: 100,
      slots: { default: 'status' },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      field: 'action',
      title: '操作',
      width: 280,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/erp/purchase/plan/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,300 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpPurchasePlanApi } from '#/api/erp/purchase/plan';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { confirm, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deletePurchasePlan,
  exportPurchasePlan,
  generatePlanFromStockAlert,
  generatePurchaseRequests,
  getApproveProcessList,
  getPurchasePlanPage,
} from '#/api/erp/purchase/plan';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import ProcessSelectModal from './modules/process-select-modal.vue';
/** ERP é‡‡è´­è®¡åˆ’列表 */
defineOptions({ name: 'ErpPurchasePlan' });
const router = useRouter();
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ProcessModal, processModalApi] = useVbenModal({
  connectedComponent: ProcessSelectModal,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** å¯¼å‡ºè¡¨æ ¼ */
async function handleExport() {
  const data = await exportPurchasePlan(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '采购计划.xls', source: data });
}
/** æ–°å¢žé‡‡è´­è®¡åˆ’ */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** ç¼–辑采购计划 */
function handleEdit(row: ErpPurchasePlanApi.PurchasePlan) {
  formModalApi.setData({ formType: 'edit', id: row.id }).open();
}
/** åˆ é™¤é‡‡è´­è®¡åˆ’ */
async function handleDelete(ids: number[]) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting'),
    duration: 0,
  });
  try {
    await deletePurchasePlan(ids);
    message.success($t('ui.actionMessage.deleteSuccess'));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** åº“存预警生成采购计划 */
async function handleGenerateFromStockAlert() {
  const hideLoading = message.loading({
    content: '正在根据库存预警生成采购计划...',
    duration: 0,
  });
  try {
    const planId = await generatePlanFromStockAlert({});
    hideLoading();
    message.success(`已生成采购计划(编号 ${planId}),请核对明细后提交审批`);
    handleRefresh();
  } catch {
    hideLoading();
  }
}
/** æäº¤å®¡æ‰¹ - å…ˆèŽ·å–æµç¨‹åˆ—è¡¨ */
async function handleSubmit(row: ErpPurchasePlanApi.PurchasePlan) {
  const hideLoading = message.loading({
    content: '获取审批流程...',
    duration: 0,
  });
  try {
    const processList = await getApproveProcessList(row.id!);
    hideLoading();
    if (!processList || processList.length === 0) {
      message.warning('暂无可用审批流程,请先配置 BPM æµç¨‹');
      return;
    }
    processModalApi.setData({ id: row.id, processList }).open();
  } catch {
    hideLoading();
  }
}
/** ç”Ÿæˆé‡‡è´­ç”³è¯· */
function handleGenerateRequests(row: ErpPurchasePlanApi.PurchasePlan) {
  confirm({
    content: `确认根据计划「${row.name}」生成采购申请?`,
  }).then(() => {
    const hideLoading = message.loading({
      content: '正在生成采购申请...',
      duration: 0,
    });
    generatePurchaseRequests(row.id!)
      .then((requestIds) => {
        message.success(`已生成采购申请(编号 ${requestIds.join(', ')})`);
        handleRefresh();
      })
      .finally(() => {
        hideLoading();
      });
  });
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
  records,
}: {
  records: ErpPurchasePlanApi.PurchasePlan[];
}) {
  checkedIds.value = records.map((item) => item.id!);
}
/** æŸ¥çœ‹è¯¦æƒ… */
function handleDetail(row: ErpPurchasePlanApi.PurchasePlan) {
  formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** æŸ¥çœ‹å®¡æ‰¹è¯¦æƒ… */
function handleViewProcess(row: ErpPurchasePlanApi.PurchasePlan) {
  if (!row.processInstanceId) {
    message.warning('该计划尚未提交审批');
    return;
  }
  router.push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } });
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getPurchasePlanPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<ErpPurchasePlanApi.PurchasePlan>,
  gridEvents: {
    checkboxAll: handleRowCheckboxChange,
    checkboxChange: handleRowCheckboxChange,
  },
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ProcessModal @success="handleRefresh" />
    <Grid table-title="采购计划列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['采购计划']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['erp:purchase-plan:create'],
              onClick: handleCreate,
            },
            {
              label: '库存预警生成',
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['erp:purchase-plan:create'],
              popConfirm: {
                title: '根据安全库存缺量自动生成草稿采购计划?',
                confirm: handleGenerateFromStockAlert,
              },
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['erp:purchase-plan:export'],
              onClick: handleExport,
            },
            {
              label: '批量删除',
              type: 'primary',
              danger: true,
              disabled: isEmpty(checkedIds),
              icon: ACTION_ICON.DELETE,
              auth: ['erp:purchase-plan:delete'],
              popConfirm: {
                title: `是否删除所选中数据?`,
                confirm: handleDelete.bind(null, checkedIds),
              },
            },
          ]"
        />
      </template>
      <template #status="{ row }">
        <Tag v-if="row.status === 0" color="default">未提交</Tag>
        <Tag v-else-if="row.status === 10" color="processing">审批中</Tag>
        <Tag v-else-if="row.status === 20" color="success">审核通过</Tag>
        <Tag v-else-if="row.status === 30" color="error">审核不通过</Tag>
        <Tag v-else-if="row.status === 40" color="warning">已取消</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['erp:purchase-plan:query'],
              onClick: handleDetail.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['erp:purchase-plan:update'],
              ifShow: () => row.status === 0,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '提交',
              type: 'link',
              ifShow: () => row.status === 0 || row.status === 30,
              onClick: handleSubmit.bind(null, row),
            },
            {
              label: '生成申请',
              type: 'link',
              auth: ['erp:purchase-plan:generate-request'],
              ifShow: () => row.status === 20 && !row.generatedFlag,
              onClick: handleGenerateRequests.bind(null, row),
            },
            {
              label: '查看审批',
              type: 'link',
              ifShow: () => row.status !== 0,
              onClick: handleViewProcess.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['erp:purchase-plan:delete'],
              ifShow: () => row.status === 0,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.no]),
                confirm: handleDelete.bind(null, [row.id!]),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/erp/purchase/plan/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,236 @@
<script lang="ts" setup>
import type { ErpPurchasePlanApi } from '#/api/erp/purchase/plan';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, DatePicker, Input, InputNumber, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createPurchasePlan,
  getPurchasePlan,
  updatePurchasePlan,
} from '#/api/erp/purchase/plan';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
import { $t } from '#/locales';
import type { FormType } from '../data';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<ErpPurchasePlanApi.PurchasePlan>();
const formType = ref<FormType>('create');
const items = ref<ErpPurchasePlanApi.PurchasePlanItem[]>([]);
const getTitle = computed(() => {
  if (formType.value === 'create') {
    return $t('ui.actionTitle.create', ['采购计划']);
  } else if (formType.value === 'edit') {
    return $t('ui.actionTitle.edit', ['采购计划']);
  } else {
    return '采购计划详情';
  }
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-3',
  layout: 'vertical',
  schema: useFormSchema(formType.value),
  showDefaultActions: false,
});
function handleAddItem() {
  items.value.push({
    productId: undefined as unknown as number,
    productName: '',
    productUnitId: undefined as unknown as number,
    productUnitName: '',
    count: 1,
    demandTime: undefined,
    remark: '',
  });
}
function handleRemoveItem(index: number) {
  items.value.splice(index, 1);
}
function handleItemSelect(index: number, item: any) {
  const row = items.value[index];
  if (!row) {
    return;
  }
  row.productId = item?.id;
  row.productName = item?.name;
  row.productUnitId = item?.unitMeasureId;
  row.productUnitName = item?.unitMeasureName;
}
/** åˆ›å»ºæˆ–更新采购计划 */
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    if (!items.value.length) {
      message.error('请至少添加一条计划明细');
      return;
    }
    if (items.value.some((item) => !item.productId || !item.count)) {
      message.error('请完善计划明细的产品与数量');
      return;
    }
    modalApi.lock();
    const data =
      (await formApi.getValues()) as ErpPurchasePlanApi.PurchasePlan;
    data.items = items.value.map((item) => ({
      productId: item.productId,
      productUnitId: item.productUnitId,
      count: item.count,
      demandTime: item.demandTime,
      remark: item.remark,
    }));
    try {
      await (formType.value === 'create'
        ? createPurchasePlan(data)
        : updatePurchasePlan(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      items.value = [];
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema(formType.value));
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getPurchasePlan(data.id);
      await formApi.setValues(formData.value);
      items.value = (formData.value.items ?? []).map((item) => ({
        ...item,
      }));
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal
    :title="getTitle"
    class="w-3/4"
    :show-confirm-button="formType !== 'detail'"
  >
    <Form class="mx-3">
      <template #items>
        <div class="mt-2">
          <div v-if="formType !== 'detail'" class="mb-2">
            <Button type="dashed" @click="handleAddItem">+ æ·»åŠ æ˜Žç»†</Button>
          </div>
          <table class="w-full border-collapse border border-gray-200 text-sm">
            <thead>
              <tr class="bg-gray-100">
                <th class="border border-gray-200 px-2 py-1 w-12">序号</th>
                <th class="border border-gray-200 px-2 py-1">产品</th>
                <th class="border border-gray-200 px-2 py-1 w-20">单位</th>
                <th class="border border-gray-200 px-2 py-1 w-32">数量</th>
                <th class="border border-gray-200 px-2 py-1 w-40">需求日期</th>
                <th class="border border-gray-200 px-2 py-1">备注</th>
                <th
                  v-if="formType !== 'detail'"
                  class="border border-gray-200 px-2 py-1 w-16"
                >
                  æ“ä½œ
                </th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="(item, index) in items" :key="index">
                <td class="border border-gray-200 px-2 py-1 text-center">
                  {{ index + 1 }}
                </td>
                <td class="border border-gray-200 px-2 py-1">
                  <MdmItemSelect
                    v-if="formType !== 'detail'"
                    :model-value="item.productId"
                    placeholder="请选择产品"
                    @change="handleItemSelect(index, $event)"
                  />
                  <span v-else>{{ item.productName }}</span>
                </td>
                <td class="border border-gray-200 px-2 py-1 text-center">
                  {{ item.productUnitName || '-' }}
                </td>
                <td class="border border-gray-200 px-2 py-1">
                  <InputNumber
                    v-if="formType !== 'detail'"
                    v-model:value="item.count"
                    class="!w-full"
                    :min="0.01"
                    :precision="2"
                  />
                  <span v-else>{{ item.count }}</span>
                </td>
                <td class="border border-gray-200 px-2 py-1">
                  <DatePicker
                    v-if="formType !== 'detail'"
                    v-model:value="item.demandTime"
                    class="!w-full"
                    value-format="YYYY-MM-DD"
                  />
                  <span v-else>{{ item.demandTime || '-' }}</span>
                </td>
                <td class="border border-gray-200 px-2 py-1">
                  <Input
                    v-if="formType !== 'detail'"
                    v-model:value="item.remark"
                    placeholder="备注"
                  />
                  <span v-else>{{ item.remark || '-' }}</span>
                </td>
                <td
                  v-if="formType !== 'detail'"
                  class="border border-gray-200 px-2 py-1 text-center"
                >
                  <Button type="link" danger @click="handleRemoveItem(index)">
                    åˆ é™¤
                  </Button>
                </td>
              </tr>
              <tr v-if="!items.length">
                <td
                  :colspan="formType === 'detail' ? 6 : 7"
                  class="border border-gray-200 px-2 py-4 text-center text-gray-400"
                >
                  æš‚无明细,请添加
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      </template>
    </Form>
  </Modal>
</template>
src/views/erp/purchase/plan/modules/process-select-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,79 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, Select } from 'ant-design-vue';
import { submitPurchasePlan } from '#/api/erp/purchase/plan';
interface ProcessDefinition {
  id: string;
  name: string;
  version: number;
  key: string;
}
const emit = defineEmits(['success']);
const planId = ref<number>();
const processList = ref<ProcessDefinition[]>([]);
const selectedProcessKey = ref<string>('');
const processOptions = computed(() => {
  return processList.value.map((p) => ({
    label: `${p.name} (V${p.version})`,
    value: p.key,
  }));
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (!selectedProcessKey.value) {
      message.warning('请选择审批流程');
      return;
    }
    modalApi.lock();
    try {
      await submitPurchasePlan(planId.value!, selectedProcessKey.value);
      await modalApi.close();
      emit('success');
      message.success('提交成功');
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      planId.value = undefined;
      selectedProcessKey.value = '';
      processList.value = [];
      return;
    }
    const data = modalApi.getData<{ id: number; processList: ProcessDefinition[] }>();
    if (!data) {
      return;
    }
    planId.value = data.id;
    processList.value = data.processList || [];
    // é»˜è®¤é€‰æ‹©ç¬¬ä¸€ä¸ªæµç¨‹
    if (processList.value.length > 0) {
      selectedProcessKey.value = processList.value[0]?.key ?? '';
    }
  },
});
</script>
<template>
  <Modal title="选择审批流程" class="w-1/3">
    <div class="mb-4">
      <span class="mr-2">审批流程:</span>
      <Select
        v-model:value="selectedProcessKey"
        :options="processOptions"
        style="width: 200px"
        placeholder="请选择审批流程"
      />
    </div>
  </Modal>
</template>
src/views/hrm/certificate/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,197 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { HrmEmployeeSelect } from '#/views/hrm/employee/components';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'edit';
/** è¡¨å•的配置项 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  const isReadonly = formType === 'detail';
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'employeeId',
      label: '员工',
      component: markRaw(HrmEmployeeSelect),
      componentProps: {
        placeholder: '请选择员工',
        disabled: isReadonly,
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'certType',
      label: '证书类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.HRM_CERTIFICATE_TYPE, 'number'),
        placeholder: '请选择证书类型',
        disabled: isReadonly,
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'certName',
      label: '证书名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入证书名称',
        disabled: isReadonly,
      },
      rules: 'required',
    },
    {
      fieldName: 'certNo',
      label: '证书编号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入证书编号',
        disabled: isReadonly,
      },
    },
    {
      fieldName: 'issueOrg',
      label: '发证机构',
      component: 'Input',
      componentProps: {
        placeholder: '请输入发证机构',
        disabled: isReadonly,
      },
    },
    {
      fieldName: 'issueDate',
      label: '发证日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '选择发证日期',
        valueFormat: 'YYYY-MM-DD',
        disabled: isReadonly,
      },
    },
    {
      fieldName: 'expireDate',
      label: '有效期至',
      component: 'DatePicker',
      componentProps: {
        placeholder: '选择有效期至',
        valueFormat: 'YYYY-MM-DD',
        disabled: isReadonly,
      },
      rules: 'required',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        autoSize: { minRows: 1, maxRows: 1 },
        disabled: isReadonly,
      },
      formItemClass: 'col-span-2',
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'employeeId',
      label: '员工',
      component: markRaw(HrmEmployeeSelect),
      componentProps: {
        allowClear: true,
        placeholder: '请选择员工',
      },
    },
    {
      fieldName: 'certType',
      label: '证书类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_CERTIFICATE_TYPE, 'number'),
        placeholder: '请选择证书类型',
      },
    },
    {
      fieldName: 'certName',
      label: '证书名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入证书名称',
      },
    },
  ];
}
/** åˆ—表的表格列 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'checkbox', width: 50 },
    { type: 'seq', title: '序号', width: 60 },
    {
      field: 'employeeName',
      title: '员工',
      minWidth: 120,
    },
    {
      field: 'certType',
      title: '证书类型',
      width: 120,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_CERTIFICATE_TYPE },
      },
    },
    {
      field: 'certName',
      title: '证书名称',
      minWidth: 200,
    },
    {
      field: 'certNo',
      title: '证书编号',
      minWidth: 130,
    },
    {
      field: 'issueOrg',
      title: '发证机构',
      minWidth: 140,
    },
    {
      field: 'expireDate',
      title: '有效期至',
      width: 120,
      formatter: 'formatDate',
    },
    {
      field: 'expireStatus',
      title: '到期状态',
      width: 100,
      slots: { default: 'expireStatus' },
    },
    {
      field: 'action',
      title: '操作',
      width: 200,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/hrm/certificate/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,169 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmCertificateApi } from '#/api/hrm/certificate';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { isEmpty } from '@vben/utils';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteCertificate, getCertificatePage } from '#/api/hrm/certificate';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
/** HRM è¯ä¹¦èµ„质列表 */
defineOptions({ name: 'HrmCertificate' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
function handleEdit(row: HrmCertificateApi.Certificate) {
  formModalApi.setData({ formType: 'edit', id: row.id }).open();
}
function handleDetail(row: HrmCertificateApi.Certificate) {
  formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
async function handleDelete(ids: number[]) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting'),
    duration: 0,
  });
  try {
    await deleteCertificate(ids);
    message.success($t('ui.actionMessage.deleteSuccess'));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
  records,
}: {
  records: HrmCertificateApi.Certificate[];
}) {
  checkedIds.value = records.map((item) => item.id!);
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getCertificatePage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmCertificateApi.Certificate>,
  gridEvents: {
    checkboxAll: handleRowCheckboxChange,
    checkboxChange: handleRowCheckboxChange,
  },
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="证书资质列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['证书资质']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:certificate:create'],
              onClick: handleCreate,
            },
            {
              label: '批量删除',
              type: 'primary',
              danger: true,
              disabled: isEmpty(checkedIds),
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:certificate:delete'],
              popConfirm: {
                title: `是否删除所选中数据?`,
                confirm: handleDelete.bind(null, checkedIds),
              },
            },
          ]"
        />
      </template>
      <template #expireStatus="{ row }">
        <Tag v-if="row.expireStatus === 0" color="success">有效</Tag>
        <Tag v-else-if="row.expireStatus === 1" color="warning">即将到期</Tag>
        <Tag v-else-if="row.expireStatus === 2" color="error">已过期</Tag>
        <Tag v-else color="default">-</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['hrm:certificate:query'],
              onClick: handleDetail.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:certificate:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:certificate:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.certName]),
                confirm: handleDelete.bind(null, [row.id!]),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/certificate/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,91 @@
<script lang="ts" setup>
import type { HrmCertificateApi } from '#/api/hrm/certificate';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createCertificate, getCertificate, updateCertificate } from '#/api/hrm/certificate';
import { $t } from '#/locales';
import type { FormType } from '../data';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<HrmCertificateApi.Certificate>();
const formType = ref<FormType>('create');
const getTitle = computed(() => {
  if (formType.value === 'create') {
    return $t('ui.actionTitle.create', ['证书资质']);
  } else if (formType.value === 'edit') {
    return $t('ui.actionTitle.edit', ['证书资质']);
  } else {
    return '证书资质详情';
  }
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-3',
  layout: 'vertical',
  schema: useFormSchema(formType.value),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data =
      (await formApi.getValues()) as HrmCertificateApi.Certificate;
    try {
      await (formType.value === 'create'
        ? createCertificate(data)
        : updateCertificate(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema(formType.value));
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getCertificate(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-2/3" :show-confirm-button="formType !== 'detail'">
    <Form class="mx-3" />
  </Modal>
</template>
src/views/mes/dv/machinery/data.ts
@@ -76,6 +76,15 @@
      rules: 'required',
    },
    {
      fieldName: 'tbDeviceId',
      label: '数采设备ID',
      component: 'Input',
      componentProps: {
        placeholder: '请输入数采设备ID(外部数采 tbDeviceId)',
      },
      help: '与数采系统设备ID对应,用于关联设备实时数据',
    },
    {
      fieldName: 'brand',
      label: '品牌',
      component: 'Input',
src/views/mes/dv/machinery/modules/form.vue
@@ -23,6 +23,7 @@
  import CheckRecordList from "./check-record-list.vue";
  import MaintenRecordList from "./mainten-record-list.vue";
  import RepairList from "./repair-list.vue";
  import TelemetryList from "./telemetry-list.vue";
  const emit = defineEmits(["success"]);
  const formType = ref<FormType>("create"); // è¡¨å•模式
@@ -133,6 +134,11 @@
                    tab="维修记录">
        <RepairList :machinery-id="formData.id" />
      </Tabs.TabPane>
      <Tabs.TabPane key="telemetry"
                    tab="数采数据"
                    :disabled="!formData.tbDeviceId">
        <TelemetryList :tb-device-id="formData.tbDeviceId" />
      </Tabs.TabPane>
    </Tabs>
    <template #prepend-footer>
      <div class="flex flex-auto items-center">
src/views/mes/dv/statistics/summary.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
<script lang="ts" setup>
import type { MesDvStatisticsApi } from '#/api/mes/dv/statistics';
import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Card, Col, Row, Table, Tag } from 'ant-design-vue';
import { getDvStatisticsSummary } from '#/api/mes/dv/statistics';
defineOptions({ name: 'MesDvStatistics' });
const loading = ref(false);
const summary = ref<MesDvStatisticsApi.StatisticsSummary>({});
async function loadSummary() {
  loading.value = true;
  try {
    summary.value = await getDvStatisticsSummary();
  } finally {
    loading.value = false;
  }
}
onMounted(loadSummary);
</script>
<template>
  <Page>
    <Row :gutter="16" class="mb-4">
      <Col :span="4">
        <Card :loading="loading">
          <div class="text-sm text-gray-500">设备总数</div>
          <div class="mt-2 text-2xl font-bold">
            {{ summary.totalMachinery ?? 0 }}
          </div>
        </Card>
      </Col>
      <Col :span="5">
        <Card :loading="loading">
          <div class="text-sm text-gray-500">生产中 / åœæœº / ç»´æŠ¤ä¸­</div>
          <div class="mt-2 text-2xl font-bold">
            <span class="text-green-600">{{ summary.producingCount ?? 0 }}</span>
            <span class="mx-1 text-gray-400">/</span>
            <span class="text-red-600">{{ summary.stoppedCount ?? 0 }}</span>
            <span class="mx-1 text-gray-400">/</span>
            <span class="text-orange-600">{{ summary.maintainingCount ?? 0 }}</span>
          </div>
        </Card>
      </Col>
      <Col :span="5">
        <Card :loading="loading">
          <div class="text-sm text-gray-500">设备故障率</div>
          <div class="mt-2 text-2xl font-bold text-red-600">
            {{ (summary.faultRate ?? 0).toFixed(1) }}%
          </div>
        </Card>
      </Col>
      <Col :span="5">
        <Card :loading="loading">
          <div class="text-sm text-gray-500">维修单(总数 / å¾…处理)</div>
          <div class="mt-2 text-2xl font-bold">
            {{ summary.totalRepair ?? 0 }}
            <span class="text-sm text-gray-400">
              / {{ summary.processingRepair ?? 0 }}
            </span>
          </div>
        </Card>
      </Col>
      <Col :span="5">
        <Card :loading="loading">
          <div class="text-sm text-gray-500">保养记录(总数 / å·²å®Œæˆï¼‰</div>
          <div class="mt-2 text-2xl font-bold">
            {{ summary.totalMainten ?? 0 }}
            <span class="text-sm text-gray-400">
              / {{ summary.finishedMainten ?? 0 }}
            </span>
          </div>
        </Card>
      </Col>
    </Row>
    <Card title="按设备类型分布" :loading="loading">
      <Table
        row-key="machineryTypeId"
        :data-source="summary.typeDistribution ?? []"
        :pagination="false"
        size="middle"
      >
        <Table.Column title="设备类型" data-index="machineryTypeName">
          <template #default="{ record }">
            <Tag color="blue">{{ record.machineryTypeName || '未分类' }}</Tag>
          </template>
        </Table.Column>
        <Table.Column title="设备数量" data-index="count" align="right" />
        <Table.Column title="占比" align="right">
          <template #default="{ record }">
            {{
              summary.totalMachinery
                ? ((record.count / summary.totalMachinery) * 100).toFixed(1)
                : 0
            }}%
          </template>
        </Table.Column>
      </Table>
    </Card>
  </Page>
</template>
src/views/mes/md/item/data.ts
@@ -178,6 +178,15 @@
      rules: z.number().default(0),
    },
    {
      fieldName: 'materialCategory',
      label: '物资分类',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.MES_MATERIAL_CATEGORY, 'number'),
        placeholder: '请选择物资分类',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
@@ -298,6 +307,16 @@
      },
    },
    {
      field: 'materialCategory',
      title: '物资分类',
      width: 120,
      align: 'center',
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_MATERIAL_CATEGORY },
      },
    },
    {
      field: 'safeStockFlag',
      title: '安全库存',
      width: 120,
src/views/mes/pd/ctc-reel/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,206 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesPdCtcReelApi } from '#/api/mes/pd/ctc-reel';
import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { PdCtcSelect } from '#/views/mes/pd/ctc/components';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** æ–°å¢ž/修改盘具计算记录 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'ctcId',
      label: '换位导线产品',
      component: markRaw(PdCtcSelect),
      componentProps: {
        placeholder: '请选择换位导线产品',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'reelType',
      label: '盘具类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.PRODUCT_REEL_TYPE, 'string'),
        placeholder: '请选择盘具类型',
      },
    },
    {
      fieldName: 'reelOuterDiameter',
      label: '盘具外径(mm)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 0,
        placeholder: '请输入盘具外径 d1',
      },
    },
    {
      fieldName: 'reelInnerDiameter',
      label: '盘芯内径(mm)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 0,
        placeholder: '请输入盘芯内径 d2',
      },
    },
    {
      fieldName: 'reelWidth',
      label: '盘宽(mm)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 0,
        placeholder: '请输入盘宽 l2',
      },
    },
    {
      fieldName: 'maxCapacity',
      label: '最大容量(km)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 4,
        placeholder: '请输入最大容量',
      },
    },
    {
      fieldName: 'recommendedLength',
      label: '推荐每盘长度(km)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入推荐每盘长度',
      },
    },
    {
      fieldName: 'netWeight',
      label: '净重(kg)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入净重',
      },
    },
    {
      fieldName: 'grossWeight',
      label: '毛重(kg)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入毛重',
      },
    },
    {
      fieldName: 'calcFormula',
      label: '计算公式',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: {
        placeholder: '((d1-60)+(d2+10))×INT(l2/(宽+1))×(d1-d2-70)/(高+0.2)/1274',
        rows: 2,
      },
      help: '容量计算:((d1-60)+(d2+10))×INT(l2/(CTC宽+1))×(d1-d2-70)/(CTC高+0.2)/1274',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'ctcId',
      label: '换位导线产品',
      component: markRaw(PdCtcSelect),
      componentProps: {
        allowClear: true,
        placeholder: '请选择换位导线产品',
      },
    },
    {
      fieldName: 'reelType',
      label: '盘具类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.PRODUCT_REEL_TYPE, 'string'),
        placeholder: '请选择盘具类型',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesPdCtcReelApi.CtcReel>['columns'] {
  return [
    { field: 'ctcOrderSpec', title: '换位导线规格', minWidth: 160 },
    {
      field: 'reelType',
      title: '盘具类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.PRODUCT_REEL_TYPE },
      },
    },
    { field: 'reelOuterDiameter', title: '盘具外径(mm)', width: 110 },
    { field: 'reelInnerDiameter', title: '盘芯内径(mm)', width: 110 },
    { field: 'reelWidth', title: '盘宽(mm)', width: 100 },
    { field: 'maxCapacity', title: '最大容量(km)', width: 120 },
    { field: 'recommendedLength', title: '推荐长度(km)', width: 120 },
    { field: 'netWeight', title: '净重(kg)', width: 100 },
    { field: 'grossWeight', title: '毛重(kg)', width: 100 },
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 220,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/mes/pd/ctc-reel/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,150 @@
<script lang="ts" setup>
  import type { VxeTableGridOptions } from '#/adapter/vxe-table';
  import type { MesPdCtcReelApi } from '#/api/mes/pd/ctc-reel';
  import { ref } from 'vue';
  import { Page, useVbenModal } from '@vben/common-ui';
  import { $t } from '@vben/locales';
  import { Button, message } from 'ant-design-vue';
  import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
  import {
    deleteCtcReel,
    getCtcReelPage,
  } from '#/api/mes/pd/ctc-reel';
  import { useGridColumns, useGridFormSchema } from './data';
  import Form from './modules/form.vue';
  const [FormModal, formModalApi] = useVbenModal({
    connectedComponent: Form,
    destroyOnClose: true,
  });
  /** åˆ·æ–°è¡¨æ ¼ */
  function handleRefresh() {
    gridApi.query();
  }
  /** æ–°å¢ž */
  function handleCreate() {
    formModalApi.setData({ formType: 'create' }).open();
  }
  /** æŸ¥çœ‹ */
  function handleDetail(row: MesPdCtcReelApi.CtcReel) {
    formModalApi.setData({ id: row.id, formType: 'detail' }).open();
  }
  /** ç¼–辑 */
  function handleEdit(row: MesPdCtcReelApi.CtcReel) {
    formModalApi.setData({ id: row.id, formType: 'update' }).open();
  }
  /** åˆ é™¤ */
  async function handleDelete(row: MesPdCtcReelApi.CtcReel) {
    const hideLoading = message.loading({
      content: $t('ui.actionMessage.deleting', [row.ctcOrderSpec ?? '']),
      duration: 0,
    });
    try {
      await deleteCtcReel(row.id!);
      message.success(
        $t('ui.actionMessage.deleteSuccess', [row.ctcOrderSpec ?? '']),
      );
      handleRefresh();
    } finally {
      hideLoading();
    }
  }
  const [Grid, gridApi] = useVbenVxeGrid({
    formOptions: {
      schema: useGridFormSchema(),
    },
    gridOptions: {
      columns: useGridColumns(),
      height: 'auto',
      keepSource: true,
      proxyConfig: {
        ajax: {
          query: async ({ page }, formValues) =>
            await getCtcReelPage({
              pageNo: page.currentPage,
              pageSize: page.pageSize,
              ...formValues,
            }),
        },
      },
      rowConfig: {
        keyField: 'id',
        isHover: true,
      },
      toolbarConfig: {
        refresh: true,
        search: true,
      },
    } as VxeTableGridOptions<MesPdCtcReelApi.CtcReel>,
  });
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="换位导线盘具计算列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['盘具计算']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:pd-ctc-reel:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #ctcOrderSpec="{ row }">
        <Button type="link" @click="handleDetail(row)">
          {{ row.ctcOrderSpec }}
        </Button>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:pd-ctc-reel:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:pd-ctc-reel:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [
                  row.ctcOrderSpec ?? '',
                ]),
                confirm: handleDelete.bind(null, row),
              },
            },
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['mes:pd-ctc-reel:query'],
              onClick: handleDetail.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/mes/pd/ctc-reel/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,126 @@
<script lang="ts" setup>
  import type { FormType } from '../data';
  import type { MesPdCtcReelApi } from '#/api/mes/pd/ctc-reel';
  import { computed, ref } from 'vue';
  import { useVbenModal } from '@vben/common-ui';
  import { $t } from '@vben/locales';
  import { Button, message } from 'ant-design-vue';
  import { useVbenForm } from '#/adapter/form';
  import {
    createCtcReel,
    getCtcReel,
    updateCtcReel,
  } from '#/api/mes/pd/ctc-reel';
  import { CalcModal } from '#/views/mes/pd/ctc/modules';
  import { useFormSchema } from '../data';
  const emit = defineEmits(['success']);
  const formType = ref<FormType>('create'); // è¡¨å•模式
  const formData = ref<MesPdCtcReelApi.CtcReel>();
  const isDetail = computed(() => formType.value === 'detail'); // æ˜¯å¦æŸ¥çœ‹æ¨¡å¼
  const getTitle = computed(() => {
    if (formType.value === 'detail') {
      return '查看盘具计算';
    }
    return formType.value === 'update' ? '修改盘具计算' : '新增盘具计算';
  });
  const [Form, formApi] = useVbenForm({
    commonConfig: {
      componentProps: {
        class: 'w-full',
      },
      formItemClass: 'col-span-1',
      labelWidth: 130,
    },
    wrapperClass: 'grid-cols-3',
    layout: 'horizontal',
    schema: [],
    showDefaultActions: false,
  });
  const [CalcModal, calcModalApi] = useVbenModal({
    connectedComponent: CalcModal,
    destroyOnClose: true,
  });
  /** æ‰“开容量计算工具 */
  function handleOpenCalc() {
    calcModalApi.open();
  }
  /** å›žå¡«æœ€å¤§å®¹é‡ï¼ˆkm) */
  function handleFillReel(value: { capacityKm: number }) {
    formApi.setFieldValue('maxCapacity', value.capacityKm);
  }
  const [Modal, modalApi] = useVbenModal({
    async onConfirm() {
      if (isDetail.value) {
        await modalApi.close();
        return;
      }
      const { valid } = await formApi.validate();
      if (!valid) {
        return;
      }
      modalApi.lock();
      // æäº¤è¡¨å•
      const data = (await formApi.getValues()) as MesPdCtcReelApi.CtcReel;
      try {
        await (data.id ? updateCtcReel(data) : createCtcReel(data));
        // å…³é—­å¹¶æç¤º
        await modalApi.close();
        emit('success');
        message.success($t('ui.actionMessage.operationSuccess'));
      } finally {
        modalApi.unlock();
      }
    },
    async onOpenChange(isOpen: boolean) {
      if (!isOpen) {
        formData.value = undefined;
        return;
      }
      // åŠ è½½æ•°æ®
      const data = modalApi.getData<{ formType: FormType; id?: number }>();
      formType.value = data.formType;
      formApi.setState({ schema: useFormSchema() });
      formApi.setDisabled(formType.value === 'detail');
      modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
      if (!data?.id) {
        return;
      }
      modalApi.lock();
      try {
        formData.value = await getCtcReel(data.id);
        // è®¾ç½®åˆ° values
        await formApi.setValues(formData.value);
      } finally {
        modalApi.unlock();
      }
    },
  });
</script>
<template>
  <Modal :title="getTitle" class="w-4/5">
    <Form class="mx-4" />
    <template #prepend-footer>
      <Button
        v-if="!isDetail"
        type="default"
        @click="handleOpenCalc"
      >
        å®¹é‡è®¡ç®—
      </Button>
    </template>
    <CalcModal @reel="handleFillReel" />
  </Modal>
</template>
src/views/mes/pd/ctc/components/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
export { default as PdCtcSelect } from './select.vue';
src/views/mes/pd/ctc/components/select.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
<script lang="ts" setup>
  import { onMounted, ref } from 'vue';
  import { Select } from 'ant-design-vue';
  import { getCtcPage } from '#/api/mes/pd/ctc';
  defineOptions({ name: 'PdCtcSelect', inheritAttrs: false });
  const props = withDefaults(
    defineProps<{
      allowClear?: boolean;
      disabled?: boolean;
      modelValue?: number;
      placeholder?: string;
    }>(),
    {
      allowClear: true,
      disabled: false,
      modelValue: undefined,
      placeholder: '请选择换位导线产品',
    },
  );
  const emit = defineEmits<{
    'update:modelValue': [value: number | undefined];
  }>();
  const options = ref<{ label: string; value: number }[]>([]);
  const searchText = ref('');
  async function load() {
    const data = await getCtcPage({
      pageNo: 1,
      pageSize: 100,
      orderSpec: searchText.value || undefined,
    });
    options.value = (data.list ?? []).map((item) => ({
      label: `${item.orderSpec ?? ''}${item.itemName ? `(${item.itemName})` : ''}`,
      value: item.id!,
    }));
  }
  onMounted(load);
  function handleSearch(val: string) {
    searchText.value = val;
    load();
  }
</script>
<template>
  <Select
    :model-value="modelValue"
    :options="options"
    :disabled="disabled"
    :placeholder="placeholder"
    :allow-clear="allowClear"
    show-search
    :filter-option="false"
    @update:value="(v: number | undefined) => emit('update:modelValue', v)"
    @search="handleSearch"
  />
</template>
src/views/mes/pd/ctc/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,262 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesPdCtcApi } from '#/api/mes/pd/ctc';
import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { MdItemSelect } from '#/views/mes/md/item/components';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** æ–°å¢ž/修改换位导线产品技术参数 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'itemId',
      label: '物料',
      component: markRaw(MdItemSelect),
      componentProps: {
        placeholder: '请选择换位导线物料',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'orderSpec',
      label: '订货规格',
      component: 'Input',
      componentProps: {
        placeholder: '请输入订货规格',
      },
    },
    {
      fieldName: 'bareConductorSpec',
      label: '裸导线规格',
      component: 'Input',
      componentProps: {
        placeholder: '请输入裸导线规格',
      },
    },
    {
      fieldName: 'enameledSpec',
      label: '漆包线规格',
      component: 'Input',
      componentProps: {
        placeholder: '请输入漆包线规格',
      },
    },
    {
      fieldName: 'transpositionSpec',
      label: '换位规格',
      component: 'Input',
      componentProps: {
        placeholder: '请输入换位规格',
      },
    },
    {
      fieldName: 'conductorType',
      label: '导体类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.PRODUCT_CONDUCTOR_TYPE, 'string'),
        placeholder: '请选择导体类型',
      },
    },
    {
      fieldName: 'crossSection',
      label: '截面积(mm²)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入截面积',
      },
    },
    {
      fieldName: 'outerDiameter',
      label: '外径(mm)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入外径',
      },
    },
    {
      fieldName: 'resistance20c',
      label: '20℃直流电阻(Ω/km)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 6,
        placeholder: '请输入20℃直流电阻',
      },
    },
    {
      fieldName: 'wrappingAngle',
      label: '绕包角度(°)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入绕包角度',
      },
    },
    {
      fieldName: 'insulationThickness',
      label: '绝缘厚度(mm)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入绝缘厚度',
      },
    },
    {
      fieldName: 'ratedVoltage',
      label: '额定电压(kV)',
      component: 'Input',
      componentProps: {
        placeholder: '请输入额定电压',
      },
    },
    {
      fieldName: 'testVoltage',
      label: '试验电压(kV)',
      component: 'Input',
      componentProps: {
        placeholder: '请输入试验电压',
      },
    },
    {
      fieldName: 'minBendingRadius',
      label: '最小弯曲半径(mm)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入最小弯曲半径',
      },
    },
    {
      fieldName: 'weightPerKm',
      label: '每公里重量(kg/km)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入每公里重量',
      },
    },
    {
      fieldName: 'standardCode',
      label: '执行标准',
      component: 'Input',
      componentProps: {
        placeholder: '请输入执行标准代号',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'itemId',
      label: '物料',
      component: markRaw(MdItemSelect),
      componentProps: {
        allowClear: true,
        placeholder: '请选择换位导线物料',
      },
    },
    {
      fieldName: 'orderSpec',
      label: '订货规格',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入订货规格',
      },
    },
    {
      fieldName: 'conductorType',
      label: '导体类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.PRODUCT_CONDUCTOR_TYPE, 'string'),
        placeholder: '请选择导体类型',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesPdCtcApi.Ctc>['columns'] {
  return [
    { field: 'itemName', title: '物料名称', minWidth: 140 },
    { field: 'itemCode', title: '物料编码', minWidth: 120 },
    { field: 'orderSpec', title: '订货规格', minWidth: 140 },
    { field: 'transpositionSpec', title: '换位规格', minWidth: 120 },
    {
      field: 'conductorType',
      title: '导体类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.PRODUCT_CONDUCTOR_TYPE },
      },
    },
    { field: 'crossSection', title: '截面积(mm²)', width: 110 },
    { field: 'outerDiameter', title: '外径(mm)', width: 100 },
    { field: 'resistance20c', title: '20℃电阻(Ω/km)', width: 140 },
    { field: 'wrappingAngle', title: '绕包角度(°)', width: 110 },
    { field: 'ratedVoltage', title: '额定电压(kV)', width: 110 },
    { field: 'standardCode', title: '执行标准', minWidth: 140 },
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 220,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/mes/pd/ctc/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,161 @@
<script lang="ts" setup>
  import type { VxeTableGridOptions } from '#/adapter/vxe-table';
  import type { MesPdCtcApi } from '#/api/mes/pd/ctc';
  import { ref } from 'vue';
  import { Page, useVbenModal } from '@vben/common-ui';
  import { $t } from '@vben/locales';
  import { downloadFileFromBlobPart } from '@vben/utils';
  import { Button, message } from 'ant-design-vue';
  import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
  import {
    deleteCtc,
    exportCtc,
    getCtcPage,
  } from '#/api/mes/pd/ctc';
  import { useGridColumns, useGridFormSchema } from './data';
  import Form from './modules/form.vue';
  const [FormModal, formModalApi] = useVbenModal({
    connectedComponent: Form,
    destroyOnClose: true,
  });
  /** åˆ·æ–°è¡¨æ ¼ */
  function handleRefresh() {
    gridApi.query();
  }
  /** æ–°å¢ž */
  function handleCreate() {
    formModalApi.setData({ formType: 'create' }).open();
  }
  /** æŸ¥çœ‹ */
  function handleDetail(row: MesPdCtcApi.Ctc) {
    formModalApi.setData({ id: row.id, formType: 'detail' }).open();
  }
  /** ç¼–辑 */
  function handleEdit(row: MesPdCtcApi.Ctc) {
    formModalApi.setData({ id: row.id, formType: 'update' }).open();
  }
  /** åˆ é™¤ */
  async function handleDelete(row: MesPdCtcApi.Ctc) {
    const hideLoading = message.loading({
      content: $t('ui.actionMessage.deleting', [row.orderSpec ?? '']),
      duration: 0,
    });
    try {
      await deleteCtc(row.id!);
      message.success($t('ui.actionMessage.deleteSuccess', [row.orderSpec ?? '']));
      handleRefresh();
    } finally {
      hideLoading();
    }
  }
  /** å¯¼å‡º */
  async function handleExport() {
    const data = await exportCtc(await gridApi.formApi.getValues());
    downloadFileFromBlobPart({ fileName: '换位导线技术参数.xls', source: data });
  }
  const [Grid, gridApi] = useVbenVxeGrid({
    formOptions: {
      schema: useGridFormSchema(),
    },
    gridOptions: {
      columns: useGridColumns(),
      height: 'auto',
      keepSource: true,
      proxyConfig: {
        ajax: {
          query: async ({ page }, formValues) =>
            await getCtcPage({
              pageNo: page.currentPage,
              pageSize: page.pageSize,
              ...formValues,
            }),
        },
      },
      rowConfig: {
        keyField: 'id',
        isHover: true,
      },
      toolbarConfig: {
        refresh: true,
        search: true,
      },
    } as VxeTableGridOptions<MesPdCtcApi.Ctc>,
  });
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="换位导线技术参数列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['换位导线技术参数']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:pd-ctc:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['mes:pd-ctc:export'],
              onClick: handleExport,
            },
          ]"
        />
      </template>
      <template #orderSpec="{ row }">
        <Button type="link" @click="handleDetail(row)">
          {{ row.orderSpec || row.itemName }}
        </Button>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:pd-ctc:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:pd-ctc:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.orderSpec ?? '']),
                confirm: handleDelete.bind(null, row),
              },
            },
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['mes:pd-ctc:query'],
              onClick: handleDetail.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/mes/pd/ctc/modules/calc-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,344 @@
<script lang="ts" setup>
  import { reactive, ref } from 'vue';
  import { useVbenModal } from '@vben/common-ui';
  import { $t } from '@vben/locales';
  import {
    Button,
    Descriptions,
    DescriptionsItem,
    InputNumber,
    message,
    Select,
    Tabs,
  } from 'ant-design-vue';
  import {
    calcCtcAngle,
    calcCtcResistance,
    calcCtcReel,
  } from '#/api/mes/pd/ctc';
  const emit = defineEmits<{
    angle: [value: { wrappingAngle: number }];
    resistance: [value: { resistance20c: number }];
    reel: [value: { capacityKm: number }];
  }>();
  /** ç»•包角度计算表单 */
  const angleForm = reactive<{
    conductorThickness?: number;
    conductorWidth?: number;
    cornerRadius?: number;
    insulationThickness?: number;
    tapeWidth?: number;
    overlapWidth?: number;
  }>({});
  const angleResult = ref<{
    circumference?: number;
    wrappingAngle?: number;
    wrappingPitch?: number;
  }>();
  /** ç›´æµç”µé˜»è®¡ç®—表单 */
  const resistanceForm = reactive<{
    conductorThickness?: number;
    conductorWidth?: number;
    temperature?: number;
    material?: string;
  }>({ temperature: 75, material: 'copper' });
  const resistanceResult = ref<{
    crossSection?: number;
    resistance20cKm?: number;
    resistanceAtTemp?: number;
    resistanceAtTempKm?: number;
  }>();
  /** ç›˜å…·å®¹é‡è®¡ç®—表单 */
  const reelForm = reactive<{
    reelOuterDiameter?: number;
    reelInnerDiameter?: number;
    reelWidth?: number;
    ctcThickness?: number;
    ctcWidth?: number;
  }>({});
  const reelResult = ref<{
    capacityMeter?: number;
    capacityKm?: number;
  }>();
  /** ç»•包角度计算 */
  async function handleCalcAngle() {
    const data = await calcCtcAngle(angleForm);
    angleResult.value = data;
  }
  /** ç›´æµç”µé˜»è®¡ç®— */
  async function handleCalcResistance() {
    const data = await calcCtcResistance(resistanceForm);
    resistanceResult.value = data;
  }
  /** ç›˜å…·å®¹é‡è®¡ç®— */
  async function handleCalcReel() {
    const data = await calcCtcReel(reelForm);
    reelResult.value = data;
  }
  /** å›žå¡«ç»•包角度 */
  function handleFillAngle() {
    if (angleResult.value?.wrappingAngle == null) {
      message.warning('请先执行绕包角度计算');
      return;
    }
    emit('angle', { wrappingAngle: angleResult.value.wrappingAngle });
    message.success($t('ui.actionMessage.operationSuccess'));
  }
  /** å›žå¡«ç›´æµç”µé˜» */
  function handleFillResistance() {
    if (resistanceResult.value?.resistance20cKm == null) {
      message.warning('请先执行直流电阻计算');
      return;
    }
    emit('resistance', { resistance20c: resistanceResult.value.resistance20cKm });
    message.success($t('ui.actionMessage.operationSuccess'));
  }
  /** å›žå¡«ç›˜å…·å®¹é‡ */
  function handleFillReel() {
    if (reelResult.value?.capacityKm == null) {
      message.warning('请先执行盘具容量计算');
      return;
    }
    emit('reel', { capacityKm: reelResult.value.capacityKm });
    message.success($t('ui.actionMessage.operationSuccess'));
  }
  const [Modal, modalApi] = useVbenModal({
    onOpenChange(isOpen: boolean) {
      if (!isOpen) {
        angleResult.value = undefined;
        resistanceResult.value = undefined;
        reelResult.value = undefined;
      }
    },
  });
</script>
<template>
  <Modal title="换位导线计算工具" class="w-2/3">
    <Tabs>
      <Tabs.TabPane key="angle" tab="绕包角度">
        <div class="grid grid-cols-3 gap-4">
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">导体厚度(mm)</span>
            <InputNumber
              v-model:value="angleForm.conductorThickness"
              class="!flex-1"
              :min="0"
              :precision="2"
              placeholder="如 19.08"
            />
          </div>
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">导体宽度(mm)</span>
            <InputNumber
              v-model:value="angleForm.conductorWidth"
              class="!flex-1"
              :min="0"
              :precision="2"
              placeholder="如 12.6"
            />
          </div>
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">圆角半径(mm)</span>
            <InputNumber
              v-model:value="angleForm.cornerRadius"
              class="!flex-1"
              :min="0"
              :precision="2"
              placeholder="如 1"
            />
          </div>
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">绝缘厚度(mm)</span>
            <InputNumber
              v-model:value="angleForm.insulationThickness"
              class="!flex-1"
              :min="0"
              :precision="2"
              placeholder="如 0.75"
            />
          </div>
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">纸带宽度(mm)</span>
            <InputNumber
              v-model:value="angleForm.tapeWidth"
              class="!flex-1"
              :min="0"
              :precision="2"
              placeholder="如 22"
            />
          </div>
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">重叠宽度(mm)</span>
            <InputNumber
              v-model:value="angleForm.overlapWidth"
              class="!flex-1"
              :min="0"
              :precision="2"
              placeholder="如 2"
            />
          </div>
        </div>
        <div class="mt-4 flex gap-2">
          <Button type="primary" @click="handleCalcAngle">计算</Button>
          <Button @click="handleFillAngle">回填绕包角度</Button>
        </div>
        <Descriptions v-if="angleResult" class="mt-4" :column="3" bordered>
          <DescriptionsItem label="导线周长(mm)">
            {{ angleResult.circumference }}
          </DescriptionsItem>
          <DescriptionsItem label="绕包角度(°)">
            {{ angleResult.wrappingAngle }}
          </DescriptionsItem>
          <DescriptionsItem label="绕包节距(mm)">
            {{ angleResult.wrappingPitch }}
          </DescriptionsItem>
        </Descriptions>
      </Tabs.TabPane>
      <Tabs.TabPane key="resistance" tab="直流电阻">
        <div class="grid grid-cols-3 gap-4">
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">导体厚度(mm)</span>
            <InputNumber
              v-model:value="resistanceForm.conductorThickness"
              class="!flex-1"
              :min="0"
              :precision="2"
              placeholder="如 1.27"
            />
          </div>
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">导体宽度(mm)</span>
            <InputNumber
              v-model:value="resistanceForm.conductorWidth"
              class="!flex-1"
              :min="0"
              :precision="2"
              placeholder="如 5.7"
            />
          </div>
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">计算温度(℃)</span>
            <InputNumber
              v-model:value="resistanceForm.temperature"
              class="!flex-1"
              :min="0"
              :precision="0"
            />
          </div>
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">导体材质</span>
            <Select
              v-model:value="resistanceForm.material"
              class="!flex-1"
              :options="[
                { label: '铜', value: 'copper' },
                { label: '铝', value: 'aluminum' },
              ]"
            />
          </div>
        </div>
        <div class="mt-4 flex gap-2">
          <Button type="primary" @click="handleCalcResistance">计算</Button>
          <Button @click="handleFillResistance">回填20℃电阻</Button>
        </div>
        <Descriptions v-if="resistanceResult" class="mt-4" :column="2" bordered>
          <DescriptionsItem label="截面积(mm²)">
            {{ resistanceResult.crossSection }}
          </DescriptionsItem>
          <DescriptionsItem label="20℃电阻(Ω/km)">
            {{ resistanceResult.resistance20cKm }}
          </DescriptionsItem>
          <DescriptionsItem label="计算温度电阻(Ω/m)">
            {{ resistanceResult.resistanceAtTemp }}
          </DescriptionsItem>
          <DescriptionsItem label="计算温度电阻(Ω/km)">
            {{ resistanceResult.resistanceAtTempKm }}
          </DescriptionsItem>
        </Descriptions>
      </Tabs.TabPane>
      <Tabs.TabPane key="reel" tab="盘具容量">
        <div class="grid grid-cols-3 gap-4">
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">盘具外径 d1(mm)</span>
            <InputNumber
              v-model:value="reelForm.reelOuterDiameter"
              class="!flex-1"
              :min="0"
              :precision="0"
              placeholder="如 1600"
            />
          </div>
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">盘芯内径 d2(mm)</span>
            <InputNumber
              v-model:value="reelForm.reelInnerDiameter"
              class="!flex-1"
              :min="0"
              :precision="0"
              placeholder="如 1100"
            />
          </div>
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">盘宽 l2(mm)</span>
            <InputNumber
              v-model:value="reelForm.reelWidth"
              class="!flex-1"
              :min="0"
              :precision="0"
              placeholder="如 500"
            />
          </div>
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">CTC高度(mm)</span>
            <InputNumber
              v-model:value="reelForm.ctcThickness"
              class="!flex-1"
              :min="0"
              :precision="2"
              placeholder="如 19.08"
            />
          </div>
          <div class="flex items-center gap-2">
            <span class="w-28 text-right">CTC宽度(mm)</span>
            <InputNumber
              v-model:value="reelForm.ctcWidth"
              class="!flex-1"
              :min="0"
              :precision="2"
              placeholder="如 12.6"
            />
          </div>
        </div>
        <div class="mt-4 flex gap-2">
          <Button type="primary" @click="handleCalcReel">计算</Button>
          <Button @click="handleFillReel">回填最大容量</Button>
        </div>
        <Descriptions v-if="reelResult" class="mt-4" :column="2" bordered>
          <DescriptionsItem label="每盘长度(m)">
            {{ reelResult.capacityMeter }}
          </DescriptionsItem>
          <DescriptionsItem label="每盘长度(km)">
            {{ reelResult.capacityKm }}
          </DescriptionsItem>
        </Descriptions>
      </Tabs.TabPane>
    </Tabs>
  </Modal>
</template>
src/views/mes/pd/ctc/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,134 @@
<script lang="ts" setup>
  import type { FormType } from '../data';
  import type { MesPdCtcApi } from '#/api/mes/pd/ctc';
  import { computed, ref } from 'vue';
  import { useVbenModal } from '@vben/common-ui';
  import { $t } from '@vben/locales';
  import { Button, message } from 'ant-design-vue';
  import { useVbenForm } from '#/adapter/form';
  import {
    createCtc,
    getCtc,
    updateCtc,
  } from '#/api/mes/pd/ctc';
  import { useFormSchema } from '../data';
  import CalcModal from './calc-modal.vue';
  const emit = defineEmits(['success']);
  const formType = ref<FormType>('create'); // è¡¨å•模式
  const formData = ref<MesPdCtcApi.Ctc>();
  const isDetail = computed(() => formType.value === 'detail'); // æ˜¯å¦æŸ¥çœ‹æ¨¡å¼
  const getTitle = computed(() => {
    if (formType.value === 'detail') {
      return '查看换位导线技术参数';
    }
    return formType.value === 'update' ? '修改换位导线技术参数' : '新增换位导线技术参数';
  });
  const [Form, formApi] = useVbenForm({
    commonConfig: {
      componentProps: {
        class: 'w-full',
      },
      formItemClass: 'col-span-1',
      labelWidth: 130,
    },
    wrapperClass: 'grid-cols-3',
    layout: 'horizontal',
    schema: [],
    showDefaultActions: false,
  });
  const [CalcModal, calcModalApi] = useVbenModal({
    connectedComponent: CalcModal,
    destroyOnClose: true,
  });
  /** æ‰“开计算工具 */
  function handleOpenCalc() {
    calcModalApi.open();
  }
  /** å›žå¡«ç»•包角度 */
  function handleFillAngle(value: { wrappingAngle: number }) {
    formApi.setFieldValue('wrappingAngle', value.wrappingAngle);
  }
  /** å›žå¡«ç›´æµç”µé˜» */
  function handleFillResistance(value: { resistance20c: number }) {
    formApi.setFieldValue('resistance20c', value.resistance20c);
  }
  const [Modal, modalApi] = useVbenModal({
    async onConfirm() {
      if (isDetail.value) {
        await modalApi.close();
        return;
      }
      const { valid } = await formApi.validate();
      if (!valid) {
        return;
      }
      modalApi.lock();
      // æäº¤è¡¨å•
      const data = (await formApi.getValues()) as MesPdCtcApi.Ctc;
      try {
        await (data.id ? updateCtc(data) : createCtc(data));
        // å…³é—­å¹¶æç¤º
        await modalApi.close();
        emit('success');
        message.success($t('ui.actionMessage.operationSuccess'));
      } finally {
        modalApi.unlock();
      }
    },
    async onOpenChange(isOpen: boolean) {
      if (!isOpen) {
        formData.value = undefined;
        return;
      }
      // åŠ è½½æ•°æ®
      const data = modalApi.getData<{ formType: FormType; id?: number }>();
      formType.value = data.formType;
      formApi.setState({ schema: useFormSchema() });
      formApi.setDisabled(formType.value === 'detail');
      modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
      if (!data?.id) {
        return;
      }
      modalApi.lock();
      try {
        formData.value = await getCtc(data.id);
        // è®¾ç½®åˆ° values
        await formApi.setValues(formData.value);
      } finally {
        modalApi.unlock();
      }
    },
  });
</script>
<template>
  <Modal :title="getTitle" class="w-4/5">
    <Form class="mx-4" />
    <template #prepend-footer>
      <Button
        v-if="!isDetail"
        type="default"
        @click="handleOpenCalc"
      >
        è®¡ç®—工具
      </Button>
    </template>
    <CalcModal
      @angle="handleFillAngle"
      @resistance="handleFillResistance"
    />
  </Modal>
</template>
src/views/mes/pd/ctc/modules/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
export { default as CalcModal } from './calc-modal.vue';
src/views/mes/pd/tech-confirm/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,260 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmCustomerApi } from '#/api/crm/customer';
import type { MesPdTechConfirmApi } from '#/api/mes/pd/tech-confirm';
import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** æ–°å¢ž/修改产品技术确认函 */
export function useFormSchema(
  formApi?: VbenFormApi,
): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'confirmNo',
      label: '确认函编号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入确认函编号',
      },
      rules: 'required',
    },
    {
      fieldName: 'customerId',
      label: '客户',
      component: markRaw(CrmCustomerSelect),
      componentProps: {
        placeholder: '请选择客户',
        onChange: (item?: CrmCustomerApi.Customer) => {
          formApi?.setFieldValue('customerName', item?.name);
        },
      },
    },
    {
      fieldName: 'customerName',
      label: '客户名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入客户名称',
      },
      rules: 'required',
    },
    {
      fieldName: 'productName',
      label: '产品名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入产品名称',
      },
      rules: 'required',
    },
    {
      fieldName: 'specification',
      label: '规格型号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入规格型号',
      },
    },
    {
      fieldName: 'techParams',
      label: '技术参数(JSON)',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: {
        placeholder: '请输入技术参数 JSON,如 {"thickness":"1.27","width":"5.7"}',
        rows: 4,
      },
    },
    {
      fieldName: 'templateType',
      label: '模板类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.PRODUCT_TECH_CONFIRM_TEMPLATE, 'string'),
        placeholder: '请选择模板类型',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.PRODUCT_TECH_CONFIRM_STATUS, 'number'),
        placeholder: '请选择状态',
      },
    },
    {
      fieldName: 'drafterName',
      label: '编制人',
      component: 'Input',
      componentProps: {
        placeholder: '请输入编制人姓名',
      },
    },
    {
      fieldName: 'draftTime',
      label: '编制时间',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD HH:mm:ss',
        showTime: true,
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
      fieldName: 'reviewerName',
      label: '审核人',
      component: 'Input',
      componentProps: {
        placeholder: '请输入审核人姓名',
      },
    },
    {
      fieldName: 'reviewTime',
      label: '审核时间',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD HH:mm:ss',
        showTime: true,
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
      fieldName: 'customerConfirmUser',
      label: '客户确认人',
      component: 'Input',
      componentProps: {
        placeholder: '请输入客户确认人',
      },
    },
    {
      fieldName: 'customerConfirmTime',
      label: '客户确认时间',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD HH:mm:ss',
        showTime: true,
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'confirmNo',
      label: '确认函编号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入确认函编号',
      },
    },
    {
      fieldName: 'productName',
      label: '产品名称',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入产品名称',
      },
    },
    {
      fieldName: 'templateType',
      label: '模板类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.PRODUCT_TECH_CONFIRM_TEMPLATE, 'string'),
        placeholder: '请选择模板类型',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.PRODUCT_TECH_CONFIRM_STATUS, 'number'),
        placeholder: '请选择状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesPdTechConfirmApi.TechConfirm>['columns'] {
  return [
    { field: 'confirmNo', title: '确认函编号', minWidth: 160 },
    { field: 'customerName', title: '客户名称', minWidth: 180 },
    { field: 'productName', title: '产品名称', minWidth: 140 },
    { field: 'specification', title: '规格型号', minWidth: 140 },
    {
      field: 'templateType',
      title: '模板类型',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.PRODUCT_TECH_CONFIRM_TEMPLATE },
      },
    },
    { field: 'drafterName', title: '编制人', width: 90 },
    {
      field: 'status',
      title: '状态',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.PRODUCT_TECH_CONFIRM_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 260,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/mes/pd/tech-confirm/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,198 @@
<script lang="ts" setup>
  import type { VxeTableGridOptions } from '#/adapter/vxe-table';
  import type { MesPdTechConfirmApi } from '#/api/mes/pd/tech-confirm';
  import { ref } from 'vue';
  import { Page, useVbenModal } from '@vben/common-ui';
  import { $t } from '@vben/locales';
  import { downloadFileFromBlobPart } from '@vben/utils';
  import { Button, message } from 'ant-design-vue';
  import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
  import {
    deleteTechConfirm,
    exportTechConfirm,
    getTechConfirmPage,
    updateTechConfirm,
  } from '#/api/mes/pd/tech-confirm';
  import { useGridColumns, useGridFormSchema } from './data';
  import Form from './modules/form.vue';
  const [FormModal, formModalApi] = useVbenModal({
    connectedComponent: Form,
    destroyOnClose: true,
  });
  /** åˆ·æ–°è¡¨æ ¼ */
  function handleRefresh() {
    gridApi.query();
  }
  /** æ–°å¢ž */
  function handleCreate() {
    formModalApi.setData({ formType: 'create' }).open();
  }
  /** æŸ¥çœ‹ */
  function handleDetail(row: MesPdTechConfirmApi.TechConfirm) {
    formModalApi.setData({ id: row.id, formType: 'detail' }).open();
  }
  /** ç¼–辑 */
  function handleEdit(row: MesPdTechConfirmApi.TechConfirm) {
    formModalApi.setData({ id: row.id, formType: 'update' }).open();
  }
  /** åˆ é™¤ */
  async function handleDelete(row: MesPdTechConfirmApi.TechConfirm) {
    const hideLoading = message.loading({
      content: $t('ui.actionMessage.deleting', [row.confirmNo ?? '']),
      duration: 0,
    });
    try {
      await deleteTechConfirm(row.id!);
      message.success($t('ui.actionMessage.deleteSuccess', [row.confirmNo ?? '']));
      handleRefresh();
    } finally {
      hideLoading();
    }
  }
  /** çŠ¶æ€æµè½¬ */
  async function handleStatus(row: MesPdTechConfirmApi.TechConfirm, next: number) {
    const hideLoading = message.loading({ content: '状态更新中...', duration: 0 });
    try {
      await updateTechConfirm({ ...row, status: next });
      message.success($t('ui.actionMessage.operationSuccess'));
      handleRefresh();
    } finally {
      hideLoading();
    }
  }
  /** å¯¼å‡º */
  async function handleExport() {
    const data = await exportTechConfirm(await gridApi.formApi.getValues());
    downloadFileFromBlobPart({ fileName: '产品技术确认函.xls', source: data });
  }
  const [Grid, gridApi] = useVbenVxeGrid({
    formOptions: {
      schema: useGridFormSchema(),
    },
    gridOptions: {
      columns: useGridColumns(),
      height: 'auto',
      keepSource: true,
      proxyConfig: {
        ajax: {
          query: async ({ page }, formValues) =>
            await getTechConfirmPage({
              pageNo: page.currentPage,
              pageSize: page.pageSize,
              ...formValues,
            }),
        },
      },
      rowConfig: {
        keyField: 'id',
        isHover: true,
      },
      toolbarConfig: {
        refresh: true,
        search: true,
      },
    } as VxeTableGridOptions<MesPdTechConfirmApi.TechConfirm>,
  });
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="产品技术确认函列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['技术确认函']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:pd-tech-confirm:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['mes:pd-tech-confirm:export'],
              onClick: handleExport,
            },
          ]"
        />
      </template>
      <template #confirmNo="{ row }">
        <Button type="link" @click="handleDetail(row)">
          {{ row.confirmNo }}
        </Button>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '提交审核',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['mes:pd-tech-confirm:update'],
              ifShow: row.status === 0,
              onClick: handleStatus.bind(null, row, 1),
            },
            {
              label: '审核通过',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['mes:pd-tech-confirm:update'],
              ifShow: row.status === 1,
              onClick: handleStatus.bind(null, row, 2),
            },
            {
              label: '客户确认',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['mes:pd-tech-confirm:update'],
              ifShow: row.status === 2,
              onClick: handleStatus.bind(null, row, 3),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:pd-tech-confirm:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:pd-tech-confirm:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.confirmNo ?? '']),
                confirm: handleDelete.bind(null, row),
              },
            },
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['mes:pd-tech-confirm:query'],
              onClick: handleDetail.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/mes/pd/tech-confirm/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,100 @@
<script lang="ts" setup>
  import type { FormType } from '../data';
  import type { MesPdTechConfirmApi } from '#/api/mes/pd/tech-confirm';
  import { computed, ref } from 'vue';
  import { useVbenModal } from '@vben/common-ui';
  import { $t } from '@vben/locales';
  import { message } from 'ant-design-vue';
  import { useVbenForm } from '#/adapter/form';
  import {
    createTechConfirm,
    getTechConfirm,
    updateTechConfirm,
  } from '#/api/mes/pd/tech-confirm';
  import { useFormSchema } from '../data';
  const emit = defineEmits(['success']);
  const formType = ref<FormType>('create'); // è¡¨å•模式
  const formData = ref<MesPdTechConfirmApi.TechConfirm>();
  const isDetail = computed(() => formType.value === 'detail'); // æ˜¯å¦æŸ¥çœ‹æ¨¡å¼
  const getTitle = computed(() => {
    if (formType.value === 'detail') {
      return '查看技术确认函';
    }
    return formType.value === 'update' ? '修改技术确认函' : '新增技术确认函';
  });
  const [Form, formApi] = useVbenForm({
    commonConfig: {
      componentProps: {
        class: 'w-full',
      },
      formItemClass: 'col-span-1',
      labelWidth: 130,
    },
    wrapperClass: 'grid-cols-3',
    layout: 'horizontal',
    schema: [],
    showDefaultActions: false,
  });
  const [Modal, modalApi] = useVbenModal({
    async onConfirm() {
      if (isDetail.value) {
        await modalApi.close();
        return;
      }
      const { valid } = await formApi.validate();
      if (!valid) {
        return;
      }
      modalApi.lock();
      // æäº¤è¡¨å•
      const data = (await formApi.getValues()) as MesPdTechConfirmApi.TechConfirm;
      try {
        await (data.id ? updateTechConfirm(data) : createTechConfirm(data));
        // å…³é—­å¹¶æç¤º
        await modalApi.close();
        emit('success');
        message.success($t('ui.actionMessage.operationSuccess'));
      } finally {
        modalApi.unlock();
      }
    },
    async onOpenChange(isOpen: boolean) {
      if (!isOpen) {
        formData.value = undefined;
        return;
      }
      // åŠ è½½æ•°æ®
      const data = modalApi.getData<{ formType: FormType; id?: number }>();
      formType.value = data.formType;
      formApi.setState({ schema: useFormSchema(formApi) });
      formApi.setDisabled(formType.value === 'detail');
      modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
      if (!data?.id) {
        return;
      }
      modalApi.lock();
      try {
        formData.value = await getTechConfirm(data.id);
        // è®¾ç½®åˆ° values
        await formApi.setValues(formData.value);
      } finally {
        modalApi.unlock();
      }
    },
  });
</script>
<template>
  <Modal :title="getTitle" class="w-4/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/mes/pro/emergencyplan/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,187 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'edit';
/** çŠ¶æ€å¸¸é‡ */
export const PLAN_STATUS = {
  DRAFT: 0,
  PUBLISHED: 10,
};
/** è¡¨å•的配置项 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  const isReadonly = formType === 'detail';
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'code',
      label: '预案编码',
      component: 'Input',
      componentProps: {
        placeholder: '系统自动生成',
        disabled: true,
      },
    },
    {
      fieldName: 'name',
      label: '预案名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入预案名称',
        disabled: isReadonly,
      },
      rules: 'required',
    },
    {
      fieldName: 'planType',
      label: '预案类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE, 'number'),
        placeholder: '请选择预案类型',
        disabled: isReadonly,
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'orgUnit',
      label: '责任单位',
      component: 'Input',
      componentProps: {
        placeholder: '请输入责任单位/部门',
        disabled: isReadonly,
      },
    },
    {
      fieldName: 'content',
      label: '处置内容',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入应急处置内容',
        rows: 4,
        disabled: isReadonly,
      },
      formItemClass: 'col-span-3',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        autoSize: { minRows: 1, maxRows: 1 },
        disabled: isReadonly,
      },
      formItemClass: 'col-span-3',
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '预案编码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入预案编码',
      },
    },
    {
      fieldName: 'name',
      label: '预案名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入预案名称',
      },
    },
    {
      fieldName: 'planType',
      label: '预案类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE, 'number'),
        placeholder: '请选择预案类型',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: [
          { label: '草稿', value: PLAN_STATUS.DRAFT },
          { label: '已发布', value: PLAN_STATUS.PUBLISHED },
        ],
        placeholder: '请选择状态',
      },
    },
  ];
}
/** åˆ—表的表格列 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'checkbox', width: 50 },
    { type: 'seq', title: '序号', width: 60 },
    {
      field: 'code',
      title: '预案编码',
      minWidth: 150,
      slots: { default: 'code' },
    },
    {
      field: 'name',
      title: '预案名称',
      minWidth: 200,
    },
    {
      field: 'planType',
      title: '预案类型',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE },
      },
    },
    {
      field: 'orgUnit',
      title: '责任单位',
      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/emergencyplan/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,188 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProEmergencyPlanApi } from '#/api/mes/pro/emergencyplan';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { isEmpty } from '@vben/utils';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deletePlan, getPlanPage, publishPlan } from '#/api/mes/pro/emergencyplan';
import { $t } from '#/locales';
import { PLAN_STATUS, useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
/** MES åº”急处置预案列表 */
defineOptions({ name: 'MesProEmergencyPlan' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
function handleEdit(row: MesProEmergencyPlanApi.EmergencyPlan) {
  formModalApi.setData({ formType: 'edit', id: row.id }).open();
}
function handleDetail(row: MesProEmergencyPlanApi.EmergencyPlan) {
  formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
async function handleDelete(ids: number[]) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting'),
    duration: 0,
  });
  try {
    await deletePlan(ids);
    message.success($t('ui.actionMessage.deleteSuccess'));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
async function handlePublish(row: MesProEmergencyPlanApi.EmergencyPlan) {
  await publishPlan(row.id!);
  message.success('已发布');
  handleRefresh();
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
  records,
}: {
  records: MesProEmergencyPlanApi.EmergencyPlan[];
}) {
  checkedIds.value = records.map((item) => item.id!);
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getPlanPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesProEmergencyPlanApi.EmergencyPlan>,
  gridEvents: {
    checkboxAll: handleRowCheckboxChange,
    checkboxChange: handleRowCheckboxChange,
  },
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="应急处置预案列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['应急处置预案']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:pro-emergency-plan:create'],
              onClick: handleCreate,
            },
            {
              label: '批量删除',
              type: 'primary',
              danger: true,
              disabled: isEmpty(checkedIds),
              icon: ACTION_ICON.DELETE,
              auth: ['mes:pro-emergency-plan:delete'],
              popConfirm: {
                title: `是否删除所选中数据?`,
                confirm: handleDelete.bind(null, checkedIds),
              },
            },
          ]"
        />
      </template>
      <template #code="{ row }">
        <a class="link" @click="handleDetail(row)">{{ row.code }}</a>
      </template>
      <template #status="{ row }">
        <Tag v-if="row.status === PLAN_STATUS.DRAFT" color="default">草稿</Tag>
        <Tag v-else-if="row.status === PLAN_STATUS.PUBLISHED" color="success">已发布</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['mes:pro-emergency-plan:query'],
              onClick: handleDetail.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:pro-emergency-plan:update'],
              ifShow: row.status === PLAN_STATUS.DRAFT,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '发布',
              type: 'link',
              auth: ['mes:pro-emergency-plan:update'],
              ifShow: row.status === PLAN_STATUS.DRAFT,
              popConfirm: {
                title: '确认发布该预案?',
                confirm: handlePublish.bind(null, row),
              },
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:pro-emergency-plan:delete'],
              ifShow: row.status === PLAN_STATUS.DRAFT,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
                confirm: handleDelete.bind(null, [row.id!]),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/mes/pro/emergencyplan/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,91 @@
<script lang="ts" setup>
import type { MesProEmergencyPlanApi } from '#/api/mes/pro/emergencyplan';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createPlan, getPlan, updatePlan } from '#/api/mes/pro/emergencyplan';
import { $t } from '#/locales';
import type { FormType } from '../data';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesProEmergencyPlanApi.EmergencyPlan>();
const formType = ref<FormType>('create');
const getTitle = computed(() => {
  if (formType.value === 'create') {
    return $t('ui.actionTitle.create', ['应急处置预案']);
  } else if (formType.value === 'edit') {
    return $t('ui.actionTitle.edit', ['应急处置预案']);
  } else {
    return '应急处置预案详情';
  }
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-3',
  layout: 'vertical',
  schema: useFormSchema(formType.value),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data =
      (await formApi.getValues()) as MesProEmergencyPlanApi.EmergencyPlan;
    try {
      await (formType.value === 'create'
        ? createPlan(data)
        : updatePlan(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema(formType.value));
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getPlan(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-2/3" :show-confirm-button="formType !== 'detail'">
    <Form class="mx-3" />
  </Modal>
</template>
src/views/mes/pro/maintenance-plan/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,244 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getRangePickerDefaultProps } from '#/utils';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'edit';
/** è®¡åˆ’状态常量 */
export const PLAN_STATUS = {
  DRAFT: 0,
  ISSUED: 10,
  FINISHED: 20,
  CANCELLED: 30,
};
/** è¡¨å•的配置项 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  const isReadonly = formType === 'detail';
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'code',
      label: '计划编码',
      component: 'Input',
      componentProps: {
        placeholder: '系统自动生成',
        disabled: true,
      },
    },
    {
      fieldName: 'name',
      label: '计划名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入计划名称',
        disabled: isReadonly,
      },
      rules: 'required',
    },
    {
      fieldName: 'planType',
      label: '计划类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.MES_PRO_MAINTENANCE_PLAN_TYPE, 'number'),
        placeholder: '请选择计划类型',
        disabled: isReadonly,
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'station',
      label: '站点',
      component: 'Input',
      componentProps: {
        placeholder: '请输入站点',
        disabled: isReadonly,
      },
    },
    {
      fieldName: 'line',
      label: '线路',
      component: 'Input',
      componentProps: {
        placeholder: '请输入线路',
        disabled: isReadonly,
      },
    },
    {
      fieldName: 'planDate',
      label: '计划日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '选择计划日期',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        disabled: isReadonly,
      },
      rules: 'required',
    },
    {
      fieldName: 'teamId',
      label: '班组',
      component: 'Input',
      componentProps: {
        disabled: true,
        placeholder: '由计划执行分配',
      },
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        autoSize: { minRows: 1, maxRows: 1 },
        disabled: isReadonly,
      },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'items',
      label: '备件/物资需求',
      component: 'Input',
      formItemClass: 'col-span-3',
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '计划编码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入计划编码',
      },
    },
    {
      fieldName: 'name',
      label: '计划名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入计划名称',
      },
    },
    {
      fieldName: 'planType',
      label: '计划类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PRO_MAINTENANCE_PLAN_TYPE, 'number'),
        placeholder: '请选择计划类型',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: [
          { label: '草稿', value: PLAN_STATUS.DRAFT },
          { label: '已下发', value: PLAN_STATUS.ISSUED },
          { label: '已完成', value: PLAN_STATUS.FINISHED },
          { label: '已取消', value: PLAN_STATUS.CANCELLED },
        ],
        placeholder: '请选择状态',
      },
    },
    {
      fieldName: 'planDate',
      label: '计划日期',
      component: 'RangePicker',
      componentProps: getRangePickerDefaultProps(),
    },
  ];
}
/** åˆ—表的表格列 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'checkbox', width: 50 },
    { type: 'seq', title: '序号', width: 60 },
    {
      field: 'code',
      title: '计划编码',
      minWidth: 150,
      slots: { default: 'code' },
    },
    {
      field: 'name',
      title: '计划名称',
      minWidth: 180,
    },
    {
      field: 'planType',
      title: '计划类型',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_PRO_MAINTENANCE_PLAN_TYPE },
      },
    },
    {
      field: 'station',
      title: '站点',
      minWidth: 140,
    },
    {
      field: 'line',
      title: '线路',
      minWidth: 140,
    },
    {
      field: 'teamName',
      title: '班组',
      width: 110,
    },
    {
      field: 'planDate',
      title: '计划日期',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      field: 'status',
      title: '状态',
      width: 100,
      slots: { default: 'status' },
    },
    {
      field: 'workOrderCode',
      title: '生成的工单',
      minWidth: 150,
    },
    {
      field: 'action',
      title: '操作',
      width: 300,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/mes/pro/maintenance-plan/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,271 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProMaintenancePlanApi } from '#/api/mes/pro/maintenance-plan';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deletePlan,
  exportPlan,
  finishPlan,
  generatePurchasePlan,
  generateWorkOrder,
  getPlanPage,
  issuePlan,
} from '#/api/mes/pro/maintenance-plan';
import { $t } from '#/locales';
import { PLAN_STATUS, useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
/** MES ç”µåŠ›ä½œä¸šè®¡åˆ’åˆ—è¡¨ */
defineOptions({ name: 'MesProMaintenancePlan' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
function handleRefresh() {
  gridApi.query();
}
async function handleExport() {
  const data = await exportPlan(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '电力作业计划.xls', source: data });
}
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
function handleEdit(row: MesProMaintenancePlanApi.MaintenancePlan) {
  formModalApi.setData({ formType: 'edit', id: row.id }).open();
}
function handleDetail(row: MesProMaintenancePlanApi.MaintenancePlan) {
  formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
async function handleDelete(ids: number[]) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting'),
    duration: 0,
  });
  try {
    await deletePlan(ids);
    message.success($t('ui.actionMessage.deleteSuccess'));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
async function handleIssue(row: MesProMaintenancePlanApi.MaintenancePlan) {
  await issuePlan(row.id!);
  message.success('已下发');
  handleRefresh();
}
async function handleFinish(row: MesProMaintenancePlanApi.MaintenancePlan) {
  await finishPlan(row.id!);
  message.success('已完成');
  handleRefresh();
}
async function handleGeneratePurchasePlan(
  row: MesProMaintenancePlanApi.MaintenancePlan,
) {
  const hideLoading = message.loading({
    content: '正在根据备件需求生成采购计划...',
    duration: 0,
  });
  try {
    const planId = await generatePurchasePlan(row.id!);
    message.success(`已生成采购计划(编号 ${planId}),请在采购管理中核对提交审批`);
  } finally {
    hideLoading();
  }
}
async function handleGenerateWorkOrder(
  row: MesProMaintenancePlanApi.MaintenancePlan,
) {
  const hideLoading = message.loading({
    content: '正在生成工单...',
    duration: 0,
  });
  try {
    const workOrderId = await generateWorkOrder(row.id!);
    message.success(`已生成工单(编号 ${workOrderId}),可在生产订单中查看`);
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
  records,
}: {
  records: MesProMaintenancePlanApi.MaintenancePlan[];
}) {
  checkedIds.value = records.map((item) => item.id!);
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getPlanPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesProMaintenancePlanApi.MaintenancePlan>,
  gridEvents: {
    checkboxAll: handleRowCheckboxChange,
    checkboxChange: handleRowCheckboxChange,
  },
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="电力作业计划列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['电力作业计划']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:pro-maintenance-plan:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['mes:pro-maintenance-plan:export'],
              onClick: handleExport,
            },
            {
              label: '批量删除',
              type: 'primary',
              danger: true,
              disabled: isEmpty(checkedIds),
              icon: ACTION_ICON.DELETE,
              auth: ['mes:pro-maintenance-plan:delete'],
              popConfirm: {
                title: `是否删除所选中数据?`,
                confirm: handleDelete.bind(null, checkedIds),
              },
            },
          ]"
        />
      </template>
      <template #code="{ row }">
        <a class="link" @click="handleDetail(row)">{{ row.code }}</a>
      </template>
      <template #status="{ row }">
        <Tag v-if="row.status === PLAN_STATUS.DRAFT" color="default">草稿</Tag>
        <Tag v-else-if="row.status === PLAN_STATUS.ISSUED" color="processing">已下发</Tag>
        <Tag v-else-if="row.status === PLAN_STATUS.FINISHED" color="success">已完成</Tag>
        <Tag v-else-if="row.status === PLAN_STATUS.CANCELLED" color="warning">已取消</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['mes:pro-maintenance-plan:query'],
              onClick: handleDetail.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:pro-maintenance-plan:update'],
              ifShow: row.status === PLAN_STATUS.DRAFT,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '下发',
              type: 'link',
              auth: ['mes:pro-maintenance-plan:update'],
              ifShow: row.status === PLAN_STATUS.DRAFT,
              popConfirm: {
                title: '确认下发该计划?',
                confirm: handleIssue.bind(null, row),
              },
            },
            {
              label: '生成采购计划',
              type: 'link',
              auth: ['mes:pro-maintenance-plan:generate-purchase-plan'],
              ifShow: row.status === PLAN_STATUS.ISSUED,
              onClick: handleGeneratePurchasePlan.bind(null, row),
            },
            {
              label: '生成工单',
              type: 'link',
              auth: ['mes:pro-maintenance-plan:generate-work-order'],
              ifShow: row.status === PLAN_STATUS.ISSUED && !row.workOrderId,
              onClick: handleGenerateWorkOrder.bind(null, row),
            },
            {
              label: '完成',
              type: 'link',
              auth: ['mes:pro-maintenance-plan:update'],
              ifShow: row.status === PLAN_STATUS.ISSUED,
              popConfirm: {
                title: '确认完成该计划?',
                confirm: handleFinish.bind(null, row),
              },
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:pro-maintenance-plan:delete'],
              ifShow: row.status === PLAN_STATUS.DRAFT,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
                confirm: handleDelete.bind(null, [row.id!]),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/mes/pro/maintenance-plan/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,220 @@
<script lang="ts" setup>
import type { MesProMaintenancePlanApi } from '#/api/mes/pro/maintenance-plan';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, DatePicker, Input, InputNumber, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createPlan,
  getPlan,
  updatePlan,
} from '#/api/mes/pro/maintenance-plan';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { $t } from '#/locales';
import type { FormType } from '../data';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesProMaintenancePlanApi.MaintenancePlan>();
const formType = ref<FormType>('create');
const items = ref<MesProMaintenancePlanApi.PlanItem[]>([]);
const getTitle = computed(() => {
  if (formType.value === 'create') {
    return $t('ui.actionTitle.create', ['电力作业计划']);
  } else if (formType.value === 'edit') {
    return $t('ui.actionTitle.edit', ['电力作业计划']);
  } else {
    return '电力作业计划详情';
  }
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-3',
  layout: 'vertical',
  schema: useFormSchema(formType.value),
  showDefaultActions: false,
});
function handleAddItem() {
  items.value.push({
    itemId: undefined as unknown as number,
    itemName: '',
    itemCode: '',
    unitMeasureName: '',
    quantity: 1,
    remark: '',
  });
}
function handleRemoveItem(index: number) {
  items.value.splice(index, 1);
}
function handleItemSelect(index: number, item: any) {
  const row = items.value[index];
  if (!row) {
    return;
  }
  row.itemId = item?.id;
  row.itemName = item?.name;
  row.itemCode = item?.code;
  row.unitMeasureName = item?.unitMeasureName;
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    if (!items.value.length) {
      message.error('请至少添加一条备件/物资需求');
      return;
    }
    if (items.value.some((item) => !item.itemId || !item.quantity)) {
      message.error('请完善备件需求的产品与数量');
      return;
    }
    modalApi.lock();
    const data =
      (await formApi.getValues()) as MesProMaintenancePlanApi.MaintenancePlan;
    data.items = items.value.map((item) => ({
      itemId: item.itemId,
      quantity: item.quantity,
      remark: item.remark,
    }));
    try {
      await (formType.value === 'create'
        ? createPlan(data)
        : updatePlan(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      items.value = [];
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema(formType.value));
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getPlan(data.id);
      await formApi.setValues(formData.value);
      items.value = (formData.value.items ?? []).map((item) => ({ ...item }));
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal
    :title="getTitle"
    class="w-3/4"
    :show-confirm-button="formType !== 'detail'"
  >
    <Form class="mx-3">
      <template #items>
        <div class="mt-2">
          <div v-if="formType !== 'detail'" class="mb-2">
            <Button type="dashed" @click="handleAddItem">+ æ·»åŠ å¤‡ä»¶éœ€æ±‚</Button>
          </div>
          <table class="w-full border-collapse border border-gray-200 text-sm">
            <thead>
              <tr class="bg-gray-100">
                <th class="border border-gray-200 px-2 py-1 w-12">序号</th>
                <th class="border border-gray-200 px-2 py-1">物料/备件</th>
                <th class="border border-gray-200 px-2 py-1 w-20">单位</th>
                <th class="border border-gray-200 px-2 py-1 w-32">需求数量</th>
                <th class="border border-gray-200 px-2 py-1">备注</th>
                <th
                  v-if="formType !== 'detail'"
                  class="border border-gray-200 px-2 py-1 w-16"
                >
                  æ“ä½œ
                </th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="(item, index) in items" :key="index">
                <td class="border border-gray-200 px-2 py-1 text-center">
                  {{ index + 1 }}
                </td>
                <td class="border border-gray-200 px-2 py-1">
                  <MdItemSelect
                    v-if="formType !== 'detail'"
                    :model-value="item.itemId"
                    placeholder="请选择物料/备件"
                    @change="handleItemSelect(index, $event)"
                  />
                  <span v-else>{{ item.itemName }}</span>
                </td>
                <td class="border border-gray-200 px-2 py-1 text-center">
                  {{ item.unitMeasureName || '-' }}
                </td>
                <td class="border border-gray-200 px-2 py-1">
                  <InputNumber
                    v-if="formType !== 'detail'"
                    v-model:value="item.quantity"
                    class="!w-full"
                    :min="0.01"
                    :precision="2"
                  />
                  <span v-else>{{ item.quantity }}</span>
                </td>
                <td class="border border-gray-200 px-2 py-1">
                  <Input
                    v-if="formType !== 'detail'"
                    v-model:value="item.remark"
                    placeholder="备注"
                  />
                  <span v-else>{{ item.remark || '-' }}</span>
                </td>
                <td
                  v-if="formType !== 'detail'"
                  class="border border-gray-200 px-2 py-1 text-center"
                >
                  <Button type="link" danger @click="handleRemoveItem(index)">
                    åˆ é™¤
                  </Button>
                </td>
              </tr>
              <tr v-if="!items.length">
                <td
                  :colspan="formType === 'detail' ? 5 : 6"
                  class="border border-gray-200 px-2 py-4 text-center text-gray-400"
                >
                  æš‚无备件需求,请添加
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      </template>
    </Form>
  </Modal>
</template>
src/views/mes/pro/task/data.ts
@@ -19,6 +19,7 @@
import { getRangePickerDefaultProps } from '#/utils';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { CalTeamSelect } from '#/views/mes/cal/team/components';
import { ProProcessSelect } from '#/views/mes/process-design/process/components';
import { RouteColorPicker } from '#/views/mes/process-design/route/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
@@ -347,6 +348,11 @@
      width: 120,
    },
    {
      field: 'teamName',
      title: '班组',
      width: 120,
    },
    {
      field: 'quantity',
      title: '排产数量',
      width: 100,
@@ -406,6 +412,14 @@
      rules: 'selectRequired',
    },
    {
      fieldName: 'teamId',
      label: '班组',
      component: markRaw(CalTeamSelect),
      componentProps: {
        placeholder: '请选择班组',
      },
    },
    {
      fieldName: 'quantity',
      label: '排产数量',
      component: 'InputNumber',
src/views/mes/pro/workorder/data.ts
@@ -3,6 +3,7 @@
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import type { MesProWorkOrderBomApi } from '#/api/mes/pro/workorder/bom';
import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
import { h, markRaw } from 'vue';
@@ -23,6 +24,7 @@
  MdItemSelect,
} from '#/views/mes/md/item/components';
import { MdVendorSelect } from '#/views/mes/md/vendor/components';
import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components';
/** è¡¨å•类型 */
export type FormType = 'confirm' | 'create' | 'detail' | 'finish' | 'update';
@@ -192,6 +194,30 @@
      componentProps: {
        disabled: headerReadonly,
        placeholder: '请选择客户',
      },
      dependencies: {
        triggerFields: ['orderSourceType'],
        show: (values) =>
          values.orderSourceType === MesProWorkOrderSourceTypeEnum.ORDER,
      },
    },
    {
      fieldName: 'saleOrderId',
      label: '营销订单',
      component: markRaw(ErpSaleOrderSelect),
      componentProps: {
        disabled: headerReadonly,
        placeholder: '请选择营销订单(ERP销售订单)',
        // é€‰ä¸­è¥é”€è®¢å•后自动带入客户
        onChange: async (item?: ErpSaleOrderApi.SaleOrder) => {
          if (!item) {
            return;
          }
          const values = (await formApi?.getValues()) ?? {};
          if (values.clientId == null && item.customerId) {
            await formApi?.setFieldValue('clientId', item.customerId);
          }
        },
      },
      dependencies: {
        triggerFields: ['orderSourceType'],
@@ -398,6 +424,11 @@
      width: 120,
    },
    {
      field: 'saleOrderNo',
      title: '营销订单号',
      width: 140,
    },
    {
      field: 'requestDate',
      title: '需求日期',
      width: 180,
src/views/mes/pro/workresult/summary.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,168 @@
<script lang="ts" setup>
import type { WorkResultSummary } from '#/api/mes/pro/feedback';
import { onMounted, reactive, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import {
  Button,
  Card,
  Col,
  DatePicker,
  Row,
  Select,
  Space,
  Table,
  Tag,
} from 'ant-design-vue';
import { getFeedbackSummary } from '#/api/mes/pro/feedback';
defineOptions({ name: 'MesWorkResultSummary' });
const loading = ref(false);
const summary = ref<WorkResultSummary>({});
const queryForm = reactive<{
  beginDate?: string;
  endDate?: string;
  workOrderType?: number;
}>({});
const workOrderTypeOptions = getDictOptions(
  DICT_TYPE.MES_PRO_WORK_ORDER_TYPE,
  'number',
).map((option) => ({ label: option.label, value: option.value as number }));
async function loadSummary() {
  loading.value = true;
  try {
    summary.value = await getFeedbackSummary({
      beginDate: queryForm.beginDate,
      endDate: queryForm.endDate,
      workOrderType: queryForm.workOrderType,
    });
  } finally {
    loading.value = false;
  }
}
function handleRangeChange(_: unknown, dateStrings: string[]) {
  queryForm.beginDate = dateStrings[0] || undefined;
  queryForm.endDate = dateStrings[1] || undefined;
}
function resetQuery() {
  queryForm.beginDate = undefined;
  queryForm.endDate = undefined;
  queryForm.workOrderType = undefined;
  loadSummary();
}
onMounted(loadSummary);
</script>
<template>
  <Page>
    <Card class="mb-4">
      <Space wrap>
        <DatePicker.RangePicker
          :placeholder="['开始日期', '结束日期']"
          value-format="YYYY-MM-DD"
          @change="handleRangeChange"
        />
        <Select
          v-model:value="queryForm.workOrderType"
          allow-clear
          :options="workOrderTypeOptions"
          placeholder="工单类型"
          style="width: 160px"
        />
        <Button type="primary" :loading="loading" @click="loadSummary">
          æŸ¥è¯¢
        </Button>
        <Button @click="resetQuery">重置</Button>
      </Space>
    </Card>
    <Row :gutter="16" class="mb-4">
      <Col :span="8">
        <Card>
          <div class="text-sm text-gray-500">累计报工数量</div>
          <div class="mt-2 text-2xl font-bold">
            {{ summary.totalFeedbackQuantity ?? 0 }}
          </div>
        </Card>
      </Col>
      <Col :span="8">
        <Card>
          <div class="text-sm text-gray-500">累计合格数量</div>
          <div class="mt-2 text-2xl font-bold text-green-600">
            {{ summary.totalQualifiedQuantity ?? 0 }}
          </div>
        </Card>
      </Col>
      <Col :span="8">
        <Card>
          <div class="text-sm text-gray-500">累计报工单数</div>
          <div class="mt-2 text-2xl font-bold text-blue-600">
            {{ summary.totalFeedbackCount ?? 0 }}
          </div>
        </Card>
      </Col>
    </Row>
    <Card class="mb-4" title="按工单类型汇总">
      <Table
        row-key="workOrderType"
        :data-source="summary.byType ?? []"
        :loading="loading"
        :pagination="false"
        size="middle"
      >
        <Table.Column title="工单类型" data-index="workOrderTypeName">
          <template #default="{ record }">
            <Tag color="blue">{{ record.workOrderTypeName }}</Tag>
          </template>
        </Table.Column>
        <Table.Column
          title="报工数量"
          data-index="feedbackQuantity"
          align="right"
        />
        <Table.Column
          title="合格数量"
          data-index="qualifiedQuantity"
          align="right"
        />
        <Table.Column title="报工单数" data-index="feedbackCount" align="right" />
      </Table>
    </Card>
    <Card title="按日期汇总">
      <Table
        row-key="date"
        :data-source="summary.byDate ?? []"
        :loading="loading"
        :pagination="false"
        size="middle"
      >
        <Table.Column title="日期" data-index="date" />
        <Table.Column
          title="报工数量"
          data-index="feedbackQuantity"
          align="right"
        />
        <Table.Column
          title="合格数量"
          data-index="qualifiedQuantity"
          align="right"
        />
        <Table.Column title="报工单数" data-index="feedbackCount" align="right" />
      </Table>
    </Card>
  </Page>
</template>
src/views/mes/process-design/process/data.ts
@@ -66,6 +66,16 @@
      rules: z.string().min(1, '工序名称不能为空').max(100),
    },
    {
      fieldName: 'type',
      label: '电力工艺分类',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PRO_PROCESS_TYPE, 'number'),
        placeholder: '请选择电力工艺分类',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
@@ -148,6 +158,15 @@
    },
    { field: 'name', title: '工序名称', minWidth: 180 },
    {
      field: 'type',
      title: '电力工艺分类',
      width: 120,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_PRO_PROCESS_TYPE },
      },
    },
    {
      field: 'status',
      title: '状态',
      width: 100,
src/views/mes/qc/outsourcetest/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,263 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getRangePickerDefaultProps } from '#/utils';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'edit';
/** çŠ¶æ€å¸¸é‡ */
export const TEST_STATUS = {
  DRAFT: 0,
  SUBMITTED: 10,
  FINISHED: 20,
};
/** è¡¨å•的配置项 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  const isReadonly = formType === 'detail';
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'code',
      label: '单据编码',
      component: 'Input',
      componentProps: {
        placeholder: '系统自动生成',
        disabled: true,
      },
    },
    {
      fieldName: 'name',
      label: '单据名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入单据名称',
        disabled: isReadonly,
      },
      rules: 'required',
    },
    {
      fieldName: 'testType',
      label: '检测类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.MES_QC_OUTSOURCE_TEST_TYPE, 'number'),
        placeholder: '请选择检测类型',
        disabled: isReadonly,
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'itemId',
      label: '检测对象',
      component: 'Input',
      componentProps: {
        placeholder: '检测对象(物料/设备)',
        disabled: isReadonly,
      },
    },
    {
      fieldName: 'testOrg',
      label: '检测机构',
      component: 'Input',
      componentProps: {
        placeholder: '请输入检测机构',
        disabled: isReadonly,
      },
    },
    {
      fieldName: 'testDate',
      label: '检测日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '选择检测日期',
        showTime: true,
        format: 'YYYY-MM-DD HH:mm:ss',
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        disabled: isReadonly,
      },
      rules: 'required',
    },
    {
      fieldName: 'result',
      label: '检测结果',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.MES_QC_OUTSOURCE_TEST_RESULT, 'number'),
        placeholder: '请选择检测结果',
        disabled: isReadonly,
      },
    },
    {
      fieldName: 'reportNo',
      label: '报告编号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入检测报告编号',
        disabled: isReadonly,
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        autoSize: { minRows: 1, maxRows: 1 },
        disabled: isReadonly,
      },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'blobIds',
      label: '检测报告/资质文件',
      component: 'FileUpload',
      componentProps: {
        valueKey: 'id',
        maxNumber: 10,
        multiple: true,
        disabled: isReadonly,
      },
      formItemClass: 'col-span-3',
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '单据编码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入单据编码',
      },
    },
    {
      fieldName: 'name',
      label: '单据名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入单据名称',
      },
    },
    {
      fieldName: 'testType',
      label: '检测类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_QC_OUTSOURCE_TEST_TYPE, 'number'),
        placeholder: '请选择检测类型',
      },
    },
    {
      fieldName: 'result',
      label: '检测结果',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_QC_OUTSOURCE_TEST_RESULT, 'number'),
        placeholder: '请选择检测结果',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: [
          { label: '草稿', value: TEST_STATUS.DRAFT },
          { label: '已提交', value: TEST_STATUS.SUBMITTED },
          { label: '已完成', value: TEST_STATUS.FINISHED },
        ],
        placeholder: '请选择状态',
      },
    },
    {
      fieldName: 'testDate',
      label: '检测日期',
      component: 'RangePicker',
      componentProps: getRangePickerDefaultProps(),
    },
  ];
}
/** åˆ—表的表格列 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'checkbox', width: 50 },
    { type: 'seq', title: '序号', width: 60 },
    {
      field: 'code',
      title: '单据编码',
      minWidth: 150,
      slots: { default: 'code' },
    },
    {
      field: 'name',
      title: '单据名称',
      minWidth: 180,
    },
    {
      field: 'testType',
      title: '检测类型',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_QC_OUTSOURCE_TEST_TYPE },
      },
    },
    {
      field: 'itemName',
      title: '检测对象',
      minWidth: 140,
    },
    {
      field: 'testOrg',
      title: '检测机构',
      minWidth: 160,
    },
    {
      field: 'testDate',
      title: '检测日期',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      field: 'result',
      title: '检测结果',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_QC_OUTSOURCE_TEST_RESULT },
      },
    },
    {
      field: 'status',
      title: '状态',
      width: 100,
      slots: { default: 'status' },
    },
    {
      field: 'action',
      title: '操作',
      width: 260,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/mes/qc/outsourcetest/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,210 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcOutsourceTestApi } from '#/api/mes/qc/outsourcetest';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { isEmpty } from '@vben/utils';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteTest,
  finishTest,
  getTestPage,
  submitTest,
} from '#/api/mes/qc/outsourcetest';
import { $t } from '#/locales';
import { TEST_STATUS, useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
/** MES å§”外检测/试验单列表 */
defineOptions({ name: 'MesQcOutsourceTest' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
function handleEdit(row: MesQcOutsourceTestApi.OutsourceTest) {
  formModalApi.setData({ formType: 'edit', id: row.id }).open();
}
function handleDetail(row: MesQcOutsourceTestApi.OutsourceTest) {
  formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
async function handleDelete(ids: number[]) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting'),
    duration: 0,
  });
  try {
    await deleteTest(ids);
    message.success($t('ui.actionMessage.deleteSuccess'));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
async function handleSubmit(row: MesQcOutsourceTestApi.OutsourceTest) {
  await submitTest(row.id!);
  message.success('已提交');
  handleRefresh();
}
async function handleFinish(row: MesQcOutsourceTestApi.OutsourceTest) {
  await finishTest(row.id!);
  message.success('已完成');
  handleRefresh();
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
  records,
}: {
  records: MesQcOutsourceTestApi.OutsourceTest[];
}) {
  checkedIds.value = records.map((item) => item.id!);
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getTestPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesQcOutsourceTestApi.OutsourceTest>,
  gridEvents: {
    checkboxAll: handleRowCheckboxChange,
    checkboxChange: handleRowCheckboxChange,
  },
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="委外检测/试验单列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['委外检测/试验单']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:qc-outsource-test:create'],
              onClick: handleCreate,
            },
            {
              label: '批量删除',
              type: 'primary',
              danger: true,
              disabled: isEmpty(checkedIds),
              icon: ACTION_ICON.DELETE,
              auth: ['mes:qc-outsource-test:delete'],
              popConfirm: {
                title: `是否删除所选中数据?`,
                confirm: handleDelete.bind(null, checkedIds),
              },
            },
          ]"
        />
      </template>
      <template #code="{ row }">
        <a class="link" @click="handleDetail(row)">{{ row.code }}</a>
      </template>
      <template #status="{ row }">
        <Tag v-if="row.status === TEST_STATUS.DRAFT" color="default">草稿</Tag>
        <Tag v-else-if="row.status === TEST_STATUS.SUBMITTED" color="processing">已提交</Tag>
        <Tag v-else-if="row.status === TEST_STATUS.FINISHED" color="success">已完成</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['mes:qc-outsource-test:query'],
              onClick: handleDetail.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:qc-outsource-test:update'],
              ifShow: row.status === TEST_STATUS.DRAFT,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '提交',
              type: 'link',
              auth: ['mes:qc-outsource-test:update'],
              ifShow: row.status === TEST_STATUS.DRAFT,
              popConfirm: {
                title: '确认提交该单据?',
                confirm: handleSubmit.bind(null, row),
              },
            },
            {
              label: '完成',
              type: 'link',
              auth: ['mes:qc-outsource-test:update'],
              ifShow: row.status === TEST_STATUS.SUBMITTED,
              popConfirm: {
                title: '确认完成该单据?',
                confirm: handleFinish.bind(null, row),
              },
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:qc-outsource-test:delete'],
              ifShow: row.status === TEST_STATUS.DRAFT,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
                confirm: handleDelete.bind(null, [row.id!]),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/mes/qc/outsourcetest/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,101 @@
<script lang="ts" setup>
import type { MesQcOutsourceTestApi } from '#/api/mes/qc/outsourcetest';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createTest, getTest, updateTest } from '#/api/mes/qc/outsourcetest';
import { $t } from '#/locales';
import type { FormType } from '../data';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesQcOutsourceTestApi.OutsourceTest>();
const formType = ref<FormType>('create');
const getTitle = computed(() => {
  if (formType.value === 'create') {
    return $t('ui.actionTitle.create', ['委外检测/试验单']);
  } else if (formType.value === 'edit') {
    return $t('ui.actionTitle.edit', ['委外检测/试验单']);
  } else {
    return '委外检测/试验单详情';
  }
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 140,
  },
  wrapperClass: 'grid-cols-3',
  layout: 'vertical',
  schema: useFormSchema(formType.value),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data =
      (await formApi.getValues()) as MesQcOutsourceTestApi.OutsourceTest;
    try {
      await (formType.value === 'create'
        ? createTest(data)
        : updateTest(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema(formType.value));
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getTest(data.id);
      await formApi.setValues(formData.value);
      if (formData.value?.attachmentList?.length) {
        const blobIds = formData.value.attachmentList.map((item) => ({
          uid: String(item.id),
          name: item.name || '',
          url: item.url || '',
          status: 'done',
          id: item.id,
        }));
        await formApi.setFieldValue('blobIds', blobIds);
      }
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-2/3" :show-confirm-button="formType !== 'detail'">
    <Form class="mx-3" />
  </Modal>
</template>
src/views/wls/productissue/data.ts
@@ -93,6 +93,19 @@
      rules: 'required',
    },
    {
      fieldName: 'issueType',
      label: '单据类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.MES_WM_PRODUCT_ISSUE_TYPE, 'number'),
        placeholder: '请选择单据类型',
      },
      dependencies: {
        triggerFields: ['status'],
        show: (values) => values.status === undefined || values.status === 0,
      },
    },
    {
      fieldName: 'requiredTime',
      label: '需求时间',
      component: 'DatePicker',
@@ -196,6 +209,15 @@
      minWidth: 150,
    },
    {
      field: 'issueType',
      title: '单据类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_WM_PRODUCT_ISSUE_TYPE },
      },
    },
    {
      field: 'workOrderCode',
      title: '生产订单',
      minWidth: 140,
src/views/wls/stocktaking/task/index.vue
@@ -2,6 +2,8 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmStockTakingTaskApi } from '#/api/mes/wm/stocktaking/task';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { MesWmStockTakingTaskStatusEnum } from '@vben/constants';
import { downloadFileFromBlobPart } from '@vben/utils';
@@ -15,6 +17,10 @@
  exportStockTaking,
  getStockTakingPage,
} from '#/api/mes/wm/stocktaking/task';
import {
  importStockTakingQuantity,
  importStockTakingQuantityTemplate,
} from '#/api/wls/stocktaking/task/line';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@@ -83,6 +89,41 @@
  downloadFileFromBlobPart({ fileName: '盘点任务.xls', source: data });
}
/** ä¸‹è½½ç›˜ç‚¹å¯¼å…¥æ¨¡æ¿ */
async function handleImportTemplate() {
  const data = await importStockTakingQuantityTemplate();
  downloadFileFromBlobPart({ fileName: '盘点实盘数导入模板.xls', source: data });
}
const importFileRef = ref<HTMLInputElement>();
const importTaskId = ref<number>();
/** è§¦å‘导入文件选择 */
function handleImport(row: MesWmStockTakingTaskApi.StockTakingTask) {
  importTaskId.value = row.id!;
  importFileRef.value?.click();
}
/** å¯¼å…¥å®žç›˜æ•°æ–‡ä»¶é€‰æ‹©åŽä¸Šä¼  */
async function handleImportFileChange(event: Event) {
  const file = (event.target as HTMLInputElement).files?.[0];
  if (!file || !importTaskId.value) {
    return;
  }
  const hideLoading = message.loading({ content: '正在导入实盘数...', duration: 0 });
  try {
    const result = await importStockTakingQuantity(importTaskId.value, file);
    message.success(`导入完成:成功 ${result.successCount ?? 0} æ¡ï¼Œå¤±è´¥ ${result.failureCount ?? 0} æ¡`);
    if (result.failureMessages?.length) {
      message.warning(result.failureMessages.slice(0, 5).join(';'));
    }
    handleRefresh();
  } finally {
    (event.target as HTMLInputElement).value = '';
    hideLoading();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
@@ -137,6 +178,13 @@
              auth: ['mes:wm-stock-taking-task:export'],
              onClick: handleExport,
            },
            {
              label: '导入模板',
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['mes:wm-stock-taking-task:query'],
              onClick: handleImportTemplate,
            },
          ]"
        />
      </template>
@@ -183,6 +231,14 @@
              onClick: handleExecute.bind(null, row),
            },
            {
              label: '导入实盘数',
              type: 'link',
              icon: ACTION_ICON.UPLOAD,
              auth: ['mes:wm-stock-taking-task:update'],
              ifShow: row.status === StatusEnum.APPROVING,
              onClick: handleImport.bind(null, row),
            },
            {
              label: '取消',
              type: 'link',
              danger: true,
@@ -197,5 +253,12 @@
        />
      </template>
    </Grid>
    <input
      ref="importFileRef"
      class="hidden"
      type="file"
      accept=".xls,.xlsx"
      @change="handleImportFileChange"
    />
  </Page>
</template>
src/views/wls/warehouse/area/data.ts
@@ -102,6 +102,15 @@
      rules: 'required',
    },
    {
      fieldName: 'type',
      label: '库区类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.MES_WM_WAREHOUSE_TYPE, 'number'),
        placeholder: '请选择库区类型',
      },
    },
    {
      fieldName: 'area',
      label: '面积(㎡)',
      component: 'InputNumber',
@@ -275,6 +284,15 @@
      minWidth: 160,
    },
    {
      field: 'type',
      title: '库区类型',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_WM_WAREHOUSE_TYPE },
      },
    },
    {
      field: 'area',
      title: '面积(㎡)',
      width: 100,
src/views/wls/warehouse/data.ts
@@ -6,6 +6,7 @@
import { h } from 'vue';
import { DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
@@ -68,6 +69,15 @@
        placeholder: '请输入仓库名称',
      },
      rules: 'required',
    },
    {
      fieldName: 'type',
      label: '仓库类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.MES_WM_WAREHOUSE_TYPE, 'number'),
        placeholder: '请选择仓库类型',
      },
    },
    {
      fieldName: 'chargeUserId',
@@ -175,6 +185,15 @@
      minWidth: 160,
    },
    {
      field: 'type',
      title: '仓库类型',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_WM_WAREHOUSE_TYPE },
      },
    },
    {
      field: 'address',
      title: '仓库地址',
      minWidth: 180,
vite.config.ts
@@ -76,7 +76,7 @@
        proxy: {
          "/admin-api": {
            changeOrigin: true,
            target: "http://192.168.0.10:48080",
            target: "http://127.0.0.1:48080",
            ws: true,
          },
        },