xiaoyi
4 天以前 a4025834e2304dc57f6e98d58feeb9416bd90609
feat 功能变更
已添加29个文件
3911 ■■■■■ 文件已修改
src/api/bi/decision/alert.ts 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bi/decision/forecast.ts 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bi/decision/kpi.ts 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/dv/telemetry/index.ts 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/basedata/index.ts 330 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/price/index.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/product/index.ts 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bi/decision/alert-center/index.vue 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bi/decision/alert-rule/data.ts 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bi/decision/alert-rule/index.vue 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bi/decision/alert-rule/modules/form.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bi/decision/forecast-analysis/index.vue 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bi/decision/kpi-config/data.ts 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bi/decision/kpi-config/index.vue 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bi/decision/kpi-config/modules/form.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bi/decision/kpi-dashboard/index.vue 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bi/decision/trend-analysis/index.vue 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/machinery/modules/telemetry-list.vue 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/telemetry/data.ts 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/dv/telemetry/index.vue 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/basedata/data.ts 808 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/basedata/index.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/basedata/modules/base-table.vue 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/basedata/modules/form.vue 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/basedata/modules/price-calc.vue 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/product/data.ts 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/product/index.vue 214 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/product/modules/audit.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/product/modules/form.vue 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bi/decision/alert.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
import { requestClient } from '#/api/request';
export namespace DecisionAlertApi {
  export interface AlertRecord {
    id: number;
    ruleId: number;
    kpiCode: string;
    kpiName: string;
    actualValue: number;
    thresholdValue: number;
    severity: number;
    alertMessage: string;
    status: number;
    handlerId?: number;
    handleTime?: string;
    handleRemark?: string;
    triggeredTime: string;
    createTime: string;
  }
  export interface AlertRule {
    id?: number;
    kpiId: number;
    name: string;
    severity: number;
    comparisonOperator: string;
    thresholdValue: number;
    notificationType?: string;
    enabled?: number;
    remark?: string;
  }
}
export function getAlertPage(params: Record<string, unknown>) {
  return requestClient.get('/bi/decision/alert/page', { params });
}
export function getUnprocessedCount() {
  return requestClient.get<number>('/bi/decision/alert/unprocessed-count');
}
export function confirmAlert(id: number) {
  return requestClient.post(`/bi/decision/alert/confirm/${id}`);
}
export function handleAlert(id: number, handleRemark?: string) {
  return requestClient.post('/bi/decision/alert/handle', { id, handleRemark });
}
export function getAlertRulePage(params: Record<string, unknown>) {
  return requestClient.get('/bi/decision/alert-rule/page', { params });
}
export function getAlertRule(id: number) {
  return requestClient.get<DecisionAlertApi.AlertRule>('/bi/decision/alert-rule/get', { params: { id } });
}
export function createAlertRule(data: DecisionAlertApi.AlertRule) {
  return requestClient.post('/bi/decision/alert-rule/create', data);
}
export function updateAlertRule(data: DecisionAlertApi.AlertRule) {
  return requestClient.put('/bi/decision/alert-rule/update', data);
}
export function deleteAlertRule(id: number) {
  return requestClient.delete('/bi/decision/alert-rule/delete', { params: { id } });
}
src/api/bi/decision/forecast.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
import { requestClient } from '#/api/request';
export namespace DecisionForecastApi {
  export interface ForecastItem {
    id: number;
    forecastCode: string;
    forecastName: string;
    pointTime: string;
    forecastValue: number;
    actualValue?: number;
    lowerBound?: number;
    upperBound?: number;
    confidenceLevel?: number;
    modelVersion?: string;
    dimension?: string;
    dimensionValue?: string;
  }
}
export function getForecastList(forecastCode: string, params?: Record<string, unknown>) {
  return requestClient.get<DecisionForecastApi.ForecastItem[]>('/bi/decision/forecast/list', {
    params: { forecastCode, ...params },
  });
}
export function generateForecast(forecastCode: string) {
  return requestClient.post('/bi/decision/forecast/generate', null, {
    params: { forecastCode },
  });
}
src/api/bi/decision/kpi.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
import { requestClient } from '#/api/request';
export namespace DecisionKpiApi {
  export interface KpiItem {
    code: string;
    name: string;
    category: string;
    unit: string;
    value: number;
    alertStatus: 'normal' | 'warn' | 'critical';
    chartType: string;
    status: number;
  }
  export interface KpiOverview {
    kpis: KpiItem[];
    total: number;
  }
  export interface KpiDefinition {
    id?: number;
    code: string;
    name: string;
    category: string;
    unit?: string;
    dataSource?: string;
    querySql?: string;
    chartType?: string;
    thresholdWarn?: number;
    thresholdCritical?: number;
    comparisonOperator?: string;
    refreshInterval?: number;
    sort?: number;
    status?: number;
    remark?: string;
  }
}
export function getKpiOverview() {
  return requestClient.get<DecisionKpiApi.KpiOverview>('/bi/decision/kpi/overview');
}
export function getKpiDetail(kpiCode: string) {
  return requestClient.get<Record<string, unknown>>(`/bi/decision/kpi/detail/${kpiCode}`);
}
export function getKpiByCategory(category: string) {
  return requestClient.get<Record<string, unknown>>(`/bi/decision/kpi/category/${category}`);
}
export function refreshKpi(kpiCode: string) {
  return requestClient.post<number>(`/bi/decision/kpi/refresh/${kpiCode}`);
}
export function getKpiDefinitionPage(params: Record<string, unknown>) {
  return requestClient.get('/bi/decision/kpi-definition/page', { params });
}
export function getKpiDefinitionList() {
  return requestClient.get('/bi/decision/kpi-definition/list-all');
}
export function getKpiDefinition(id: number) {
  return requestClient.get<DecisionKpiApi.KpiDefinition>('/bi/decision/kpi-definition/get', { params: { id } });
}
export function createKpiDefinition(data: DecisionKpiApi.KpiDefinition) {
  return requestClient.post('/bi/decision/kpi-definition/create', data);
}
export function updateKpiDefinition(data: DecisionKpiApi.KpiDefinition) {
  return requestClient.put('/bi/decision/kpi-definition/update', data);
}
export function deleteKpiDefinition(id: number) {
  return requestClient.delete('/bi/decision/kpi-definition/delete', { params: { id } });
}
src/api/mes/dv/telemetry/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesDvTelemetryApi {
  /** MES è®¾å¤‡æ•°é‡‡é¥æµ‹æ•°æ® */
  export interface Telemetry {
    id?: number; // ç¼–号
    tbDeviceId?: string; // æ•°é‡‡è®¾å¤‡ID
    deviceName?: string; // è®¾å¤‡åç§°
    paramName?: string; // ä¿¡å·åç§°
    paramKeyName?: string; // è®¾å¤‡KEY名称
    standardValue?: string; // æ ‡å‡†å€¼
    timelyValue?: string; // ä¿¡å·å€¼(实时)
    avgValue?: string; // å‡å€¼
    maxValue?: string; // æœ€å¤§å€¼
    minValue?: string; // æœ€å°å€¼
    whetherAnomaly?: boolean; // æ˜¯å¦å¼‚常
    telemetryDataTime?: Date; // é¥æµ‹æ•°æ®æ—¶é—´
    billNo?: string; // å•据号
    shiftName?: string; // ç­æ¬¡
    pullTime?: Date; // æ‹‰å–æ—¶é—´
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
  /** æ‹‰å–数采数据结果 */
  export interface PullRespVO {
    recordCount?: number; // æœ¬æ¬¡æ‹‰å–新增数据条数
    deviceCount?: number; // æœ¬æ¬¡æ‹‰å–涉及设备数
    pullTime?: Date; // æ‹‰å–æ—¶é—´
  }
  /** æ•°é‡‡è®¾å¤‡ä¸‹æ‹‰é¡¹ */
  export interface DeviceItem {
    tbDeviceId?: string;
    deviceName?: string;
  }
  /** æœ€æ–°é¥æµ‹æŸ¥è¯¢å‚æ•° */
  export interface LatestReq {
    tbDeviceId?: string;
    deviceName?: string;
  }
}
/** æ‹‰å–数采数据并入库(近 5 åˆ†é’Ÿè®¾å¤‡æ•°æ®ï¼‰ */
export function pullTelemetry() {
  return requestClient.get<MesDvTelemetryApi.PullRespVO>('/mes/dv/telemetry/pull');
}
/** åˆ†é¡µæŸ¥è¯¢æ•°é‡‡é¥æµ‹æ•°æ®ï¼ˆåŽ†å²è®°å½•ï¼‰ */
export function getTelemetryPage(params: PageParam) {
  return requestClient.get<PageResult<MesDvTelemetryApi.Telemetry>>(
    '/mes/dv/telemetry/page',
    { params },
  );
}
/** æŸ¥è¯¢å„设备最新遥测数据(实时监控) */
export function getLatestTelemetry(params: MesDvTelemetryApi.LatestReq = {}) {
  return requestClient.get<MesDvTelemetryApi.Telemetry[]>(
    '/mes/dv/telemetry/latest',
    { params },
  );
}
/** æŸ¥è¯¢æ•°é‡‡è®¾å¤‡ä¸‹æ‹‰åˆ—表 */
export function getTelemetryDeviceList() {
  return requestClient.get<MesDvTelemetryApi.DeviceItem[]>(
    '/mes/dv/telemetry/device-list',
  );
}
src/api/mes/pd/basedata/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,330 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesPdBaseDataApi {
  /** ç”¨ç”µç±»åž‹ */
  export interface ElectricityType {
    id?: number; // ç¼–号
    code?: string; // ç±»åž‹ç¼–码
    name?: string; // ç±»åž‹åç§°
    description?: string; // ç±»åž‹è¯´æ˜Ž
    sort?: number; // æŽ’序
    status?: number; // å¯ç”¨çŠ¶æ€ï¼ˆ1启用/0禁用)
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
  /** ç”µåŽ‹ç­‰çº§ */
  export interface VoltageLevel {
    id?: number; // ç¼–号
    code?: string; // ç­‰çº§ç¼–码
    name?: string; // ç­‰çº§åç§°
    voltageValue?: number; // ç”µåŽ‹æ•°å€¼ï¼ˆkV)
    sort?: number; // æŽ’序
    status?: number; // å¯ç”¨çŠ¶æ€ï¼ˆ1启用/0禁用)
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
  /** æœåС套餐 */
  export interface ServicePackage {
    id?: number; // ç¼–号
    code?: string; // å¥—餐编码
    name?: string; // å¥—餐名称
    description?: string; // å¥—餐说明
    price?: number; // å¥—餐价格
    effectiveDate?: Date; // ç”Ÿæ•ˆæ—¥æœŸ
    expireDate?: Date; // å¤±æ•ˆæ—¥æœŸ
    status?: number; // å¯ç”¨çŠ¶æ€ï¼ˆ1启用/0禁用)
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
  /** å®šä»·æ ‡å‡† */
  export interface PriceStandard {
    id?: number; // ç¼–号
    objectType?: number; // å®šä»·å¯¹è±¡ç±»åž‹ï¼ˆ1服务套餐/2电压等级/3用电类型)
    objectTypeName?: string; // å®šä»·å¯¹è±¡ç±»åž‹åç§°
    objectId?: number; // å®šä»·å¯¹è±¡ID
    objectName?: string; // å®šä»·å¯¹è±¡åç§°
    price?: number; // ä»·æ ¼
    unit?: string; // è®¡ä»·å•位
    effectiveDate?: Date; // ç”Ÿæ•ˆæ—¥æœŸ
    status?: number; // å¯ç”¨çŠ¶æ€ï¼ˆ1启用/0禁用)
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
  /** ä¼˜æƒ ç­–ç•¥ */
  export interface DiscountPolicy {
    id?: number; // ç¼–号
    name?: string; // ç­–略名称
    policyType?: number; // ç­–略类型(1折扣/2立减)
    policyTypeName?: string; // ç­–略类型名称
    policyValue?: number; // ç­–略值(折扣:0-1 æ¯”例;立减:金额)
    targetType?: number; // é€‚用对象类型(1服务套餐/2电压等级/3用电类型,可空=全局)
    targetId?: number; // é€‚用对象ID
    startDate?: Date; // ç”Ÿæ•ˆæ—¥æœŸ
    endDate?: Date; // å¤±æ•ˆæ—¥æœŸ
    status?: number; // å¯ç”¨çŠ¶æ€ï¼ˆ1启用/0禁用)
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
  /** é˜¶æ¢¯ç”µä»· */
  export interface TieredPrice {
    id?: number; // ç¼–号
    packageId?: number; // æœåС套餐ID
    voltageLevelId?: number; // ç”µåŽ‹ç­‰çº§ID
    startValue?: number; // æ¡£ä½ä¸‹é™ï¼ˆå«ï¼ŒkWh)
    endValue?: number; // æ¡£ä½ä¸Šé™ï¼ˆä¸å«ï¼ŒkWh,可空=上不封顶)
    pricePerUnit?: number; // æ¡£ä½å•ä»·
    status?: number; // å¯ç”¨çŠ¶æ€ï¼ˆ1启用/0禁用)
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
}
// ==================== ç”¨ç”µç±»åž‹ ====================
/** æŸ¥è¯¢ç”¨ç”µç±»åž‹åˆ†é¡µ */
export function getElectricityTypePage(params: PageParam) {
  return requestClient.get<PageResult<MesPdBaseDataApi.ElectricityType>>(
    '/mes/pd/electricity-type/page',
    { params },
  );
}
/** æŸ¥è¯¢ç”¨ç”µç±»åž‹è¯¦æƒ… */
export function getElectricityType(id: number) {
  return requestClient.get<MesPdBaseDataApi.ElectricityType>(
    `/mes/pd/electricity-type/get?id=${id}`,
  );
}
/** æŸ¥è¯¢ç”¨ç”µç±»åž‹åˆ—表 */
export function getElectricityTypeList() {
  return requestClient.get<MesPdBaseDataApi.ElectricityType[]>(
    '/mes/pd/electricity-type/list',
  );
}
/** æ–°å¢žç”¨ç”µç±»åž‹ */
export function createElectricityType(data: MesPdBaseDataApi.ElectricityType) {
  return requestClient.post('/mes/pd/electricity-type/create', data);
}
/** ä¿®æ”¹ç”¨ç”µç±»åž‹ */
export function updateElectricityType(data: MesPdBaseDataApi.ElectricityType) {
  return requestClient.put('/mes/pd/electricity-type/update', data);
}
/** åˆ é™¤ç”¨ç”µç±»åž‹ */
export function deleteElectricityType(id: number) {
  return requestClient.delete(`/mes/pd/electricity-type/delete?id=${id}`);
}
/** å¯¼å‡ºç”¨ç”µç±»åž‹ Excel */
export function exportElectricityType(params: any) {
  return requestClient.download('/mes/pd/electricity-type/export-excel', {
    params,
  });
}
// ==================== ç”µåŽ‹ç­‰çº§ ====================
/** æŸ¥è¯¢ç”µåŽ‹ç­‰çº§åˆ†é¡µ */
export function getVoltageLevelPage(params: PageParam) {
  return requestClient.get<PageResult<MesPdBaseDataApi.VoltageLevel>>(
    '/mes/pd/voltage-level/page',
    { params },
  );
}
/** æŸ¥è¯¢ç”µåŽ‹ç­‰çº§è¯¦æƒ… */
export function getVoltageLevel(id: number) {
  return requestClient.get<MesPdBaseDataApi.VoltageLevel>(
    `/mes/pd/voltage-level/get?id=${id}`,
  );
}
/** æŸ¥è¯¢ç”µåŽ‹ç­‰çº§åˆ—è¡¨ */
export function getVoltageLevelList() {
  return requestClient.get<MesPdBaseDataApi.VoltageLevel[]>(
    '/mes/pd/voltage-level/list',
  );
}
/** æ–°å¢žç”µåŽ‹ç­‰çº§ */
export function createVoltageLevel(data: MesPdBaseDataApi.VoltageLevel) {
  return requestClient.post('/mes/pd/voltage-level/create', data);
}
/** ä¿®æ”¹ç”µåŽ‹ç­‰çº§ */
export function updateVoltageLevel(data: MesPdBaseDataApi.VoltageLevel) {
  return requestClient.put('/mes/pd/voltage-level/update', data);
}
/** åˆ é™¤ç”µåŽ‹ç­‰çº§ */
export function deleteVoltageLevel(id: number) {
  return requestClient.delete(`/mes/pd/voltage-level/delete?id=${id}`);
}
/** å¯¼å‡ºç”µåŽ‹ç­‰çº§ Excel */
export function exportVoltageLevel(params: any) {
  return requestClient.download('/mes/pd/voltage-level/export-excel', { params });
}
// ==================== æœåС套餐 ====================
/** æŸ¥è¯¢æœåŠ¡å¥—é¤åˆ†é¡µ */
export function getServicePackagePage(params: PageParam) {
  return requestClient.get<PageResult<MesPdBaseDataApi.ServicePackage>>(
    '/mes/pd/service-package/page',
    { params },
  );
}
/** æŸ¥è¯¢æœåŠ¡å¥—é¤è¯¦æƒ… */
export function getServicePackage(id: number) {
  return requestClient.get<MesPdBaseDataApi.ServicePackage>(
    `/mes/pd/service-package/get?id=${id}`,
  );
}
/** æŸ¥è¯¢æœåŠ¡å¥—é¤åˆ—è¡¨ */
export function getServicePackageList() {
  return requestClient.get<MesPdBaseDataApi.ServicePackage[]>(
    '/mes/pd/service-package/list',
  );
}
/** æ–°å¢žæœåС套餐 */
export function createServicePackage(data: MesPdBaseDataApi.ServicePackage) {
  return requestClient.post('/mes/pd/service-package/create', data);
}
/** ä¿®æ”¹æœåС套餐 */
export function updateServicePackage(data: MesPdBaseDataApi.ServicePackage) {
  return requestClient.put('/mes/pd/service-package/update', data);
}
/** åˆ é™¤æœåС套餐 */
export function deleteServicePackage(id: number) {
  return requestClient.delete(`/mes/pd/service-package/delete?id=${id}`);
}
/** å¯¼å‡ºæœåС套餐 Excel */
export function exportServicePackage(params: any) {
  return requestClient.download('/mes/pd/service-package/export-excel', {
    params,
  });
}
// ==================== å®šä»·æ ‡å‡† ====================
/** æŸ¥è¯¢å®šä»·æ ‡å‡†åˆ†é¡µ */
export function getPriceStandardPage(params: PageParam) {
  return requestClient.get<PageResult<MesPdBaseDataApi.PriceStandard>>(
    '/mes/pd/price-standard/page',
    { params },
  );
}
/** æŸ¥è¯¢å®šä»·æ ‡å‡†è¯¦æƒ… */
export function getPriceStandard(id: number) {
  return requestClient.get<MesPdBaseDataApi.PriceStandard>(
    `/mes/pd/price-standard/get?id=${id}`,
  );
}
/** æ–°å¢žå®šä»·æ ‡å‡† */
export function createPriceStandard(data: MesPdBaseDataApi.PriceStandard) {
  return requestClient.post('/mes/pd/price-standard/create', data);
}
/** ä¿®æ”¹å®šä»·æ ‡å‡† */
export function updatePriceStandard(data: MesPdBaseDataApi.PriceStandard) {
  return requestClient.put('/mes/pd/price-standard/update', data);
}
/** åˆ é™¤å®šä»·æ ‡å‡† */
export function deletePriceStandard(id: number) {
  return requestClient.delete(`/mes/pd/price-standard/delete?id=${id}`);
}
/** å¯¼å‡ºå®šä»·æ ‡å‡† Excel */
export function exportPriceStandard(params: any) {
  return requestClient.download('/mes/pd/price-standard/export-excel', { params });
}
// ==================== ä¼˜æƒ ç­–ç•¥ ====================
/** æŸ¥è¯¢ä¼˜æƒ ç­–略分页 */
export function getDiscountPolicyPage(params: PageParam) {
  return requestClient.get<PageResult<MesPdBaseDataApi.DiscountPolicy>>(
    '/mes/pd/discount-policy/page',
    { params },
  );
}
/** æŸ¥è¯¢ä¼˜æƒ ç­–略详情 */
export function getDiscountPolicy(id: number) {
  return requestClient.get<MesPdBaseDataApi.DiscountPolicy>(
    `/mes/pd/discount-policy/get?id=${id}`,
  );
}
/** æ–°å¢žä¼˜æƒ ç­–ç•¥ */
export function createDiscountPolicy(data: MesPdBaseDataApi.DiscountPolicy) {
  return requestClient.post('/mes/pd/discount-policy/create', data);
}
/** ä¿®æ”¹ä¼˜æƒ ç­–ç•¥ */
export function updateDiscountPolicy(data: MesPdBaseDataApi.DiscountPolicy) {
  return requestClient.put('/mes/pd/discount-policy/update', data);
}
/** åˆ é™¤ä¼˜æƒ ç­–ç•¥ */
export function deleteDiscountPolicy(id: number) {
  return requestClient.delete(`/mes/pd/discount-policy/delete?id=${id}`);
}
/** å¯¼å‡ºä¼˜æƒ ç­–ç•¥ Excel */
export function exportDiscountPolicy(params: any) {
  return requestClient.download('/mes/pd/discount-policy/export-excel', {
    params,
  });
}
// ==================== é˜¶æ¢¯ç”µä»· ====================
/** æŸ¥è¯¢é˜¶æ¢¯ç”µä»·åˆ†é¡µ */
export function getTieredPricePage(params: PageParam) {
  return requestClient.get<PageResult<MesPdBaseDataApi.TieredPrice>>(
    '/mes/pd/tiered-price/page',
    { params },
  );
}
/** æŸ¥è¯¢é˜¶æ¢¯ç”µä»·è¯¦æƒ… */
export function getTieredPrice(id: number) {
  return requestClient.get<MesPdBaseDataApi.TieredPrice>(
    `/mes/pd/tiered-price/get?id=${id}`,
  );
}
/** æ–°å¢žé˜¶æ¢¯ç”µä»· */
export function createTieredPrice(data: MesPdBaseDataApi.TieredPrice) {
  return requestClient.post('/mes/pd/tiered-price/create', data);
}
/** ä¿®æ”¹é˜¶æ¢¯ç”µä»· */
export function updateTieredPrice(data: MesPdBaseDataApi.TieredPrice) {
  return requestClient.put('/mes/pd/tiered-price/update', data);
}
/** åˆ é™¤é˜¶æ¢¯ç”µä»· */
export function deleteTieredPrice(id: number) {
  return requestClient.delete(`/mes/pd/tiered-price/delete?id=${id}`);
}
/** å¯¼å‡ºé˜¶æ¢¯ç”µä»· Excel */
export function exportTieredPrice(params: any) {
  return requestClient.download('/mes/pd/tiered-price/export-excel', { params });
}
src/api/mes/pd/price/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
import { requestClient } from '#/api/request';
export namespace MesPdPriceApi {
  /** é˜¶æ¢¯ç”µä»·æµ‹ç®—请求 */
  export interface PriceCalcReq {
    calcType: number; // æµ‹ç®—类型(1服务套餐/2电压等级)
    packageId?: number; // æœåС套餐ID(calcType=1 æ—¶å¿…填)
    voltageLevelId?: number; // ç”µåŽ‹ç­‰çº§ID(calcType=2 æ—¶å¿…填)
    quantity: number; // ç”¨ç”µé‡ï¼ˆkWh)
  }
  /** é˜¶æ¢¯ç”µä»·æµ‹ç®—分档明细 */
  export interface PriceCalcDetail {
    startValue?: number; // æ¡£ä½ä¸‹é™ï¼ˆå«ï¼ŒkWh)
    endValue?: number; // æ¡£ä½ä¸Šé™ï¼ˆä¸å«ï¼ŒkWh,可空=上不封顶)
    tierQuantity?: number; // è½å…¥æœ¬æ¡£ç”µé‡ï¼ˆkWh)
    pricePerUnit?: number; // æ¡£ä½å•ä»·
    subtotal?: number; // å°è®¡
  }
  /** é˜¶æ¢¯ç”µä»·æµ‹ç®—响应 */
  export interface PriceCalcResp {
    packageName?: string; // æœåŠ¡å¥—é¤åç§°
    voltageLevelName?: string; // ç”µåŽ‹ç­‰çº§åç§°
    originalPrice?: number; // åŽŸä»·ï¼ˆæœªä¼˜æƒ ï¼‰
    discountAmount?: number; // ä¼˜æƒ é‡‘额
    finalPrice?: number; // ä¼˜æƒ åŽæ€»ä»·
    details?: PriceCalcDetail[]; // åˆ†æ¡£æ˜Žç»†
  }
}
/** é˜¶æ¢¯ç”µä»·æµ‹ç®— */
export function calcPrice(data: MesPdPriceApi.PriceCalcReq) {
  return requestClient.post<MesPdPriceApi.PriceCalcResp>(
    '/mes/pd/price/calc',
    data,
  );
}
src/api/mes/pd/product/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesPdProductApi {
  /** äº§å“æ¡£æ¡ˆ */
  export interface Product {
    id?: number; // ç¼–号
    archiveType?: number; // æ¡£æ¡ˆç±»åž‹ï¼ˆ1电力服务/2供电产品/3增值能源产品)
    archiveTypeName?: string; // æ¡£æ¡ˆç±»åž‹åç§°
    code?: string; // æ¡£æ¡ˆç¼–码
    name?: string; // æ¡£æ¡ˆåç§°
    spec?: string; // è§„格型号
    unit?: string; // å•位
    price?: number; // åŸºå‡†ä»·æ ¼
    auditStatus?: number; // å®¡æ‰¹çŠ¶æ€ï¼ˆ0未提交/10审批中/20审核通过/30审核不通过)
    auditStatusName?: string; // å®¡æ‰¹çŠ¶æ€åç§°
    auditRemark?: string; // å®¡æ‰¹æ„è§
    auditTime?: Date; // å®¡æ‰¹æ—¶é—´
    status?: number; // å¯ç”¨çŠ¶æ€ï¼ˆ1启用/0禁用)
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
  /** äº§å“æ¡£æ¡ˆå®¡æ‰¹å‚æ•° */
  export interface ProductAudit {
    id: number; // ç¼–号
    pass: boolean; // å®¡æ‰¹ç»“果:true=通过 / false=驳回
    auditRemark?: string; // å®¡æ‰¹æ„è§
  }
}
/** æŸ¥è¯¢äº§å“æ¡£æ¡ˆåˆ†é¡µ */
export function getProductPage(params: PageParam) {
  return requestClient.get<PageResult<MesPdProductApi.Product>>(
    '/mes/pd/product/page',
    { params },
  );
}
/** æŸ¥è¯¢äº§å“æ¡£æ¡ˆè¯¦æƒ… */
export function getProduct(id: number) {
  return requestClient.get<MesPdProductApi.Product>(
    `/mes/pd/product/get?id=${id}`,
  );
}
/** æ–°å¢žäº§å“æ¡£æ¡ˆ */
export function createProduct(data: MesPdProductApi.Product) {
  return requestClient.post('/mes/pd/product/create', data);
}
/** ä¿®æ”¹äº§å“æ¡£æ¡ˆ */
export function updateProduct(data: MesPdProductApi.Product) {
  return requestClient.put('/mes/pd/product/update', data);
}
/** åˆ é™¤äº§å“æ¡£æ¡ˆ */
export function deleteProduct(id: number) {
  return requestClient.delete(`/mes/pd/product/delete?id=${id}`);
}
/** å¯¼å‡ºäº§å“æ¡£æ¡ˆ Excel */
export function exportProduct(params: any) {
  return requestClient.download('/mes/pd/product/export-excel', { params });
}
/** æäº¤å®¡æ‰¹ï¼ˆæœªæäº¤/驳回 â†’ å®¡æ‰¹ä¸­ï¼‰ */
export function submitProduct(id: number) {
  return requestClient.put(`/mes/pd/product/submit?id=${id}`);
}
/** å®¡æ‰¹äº§å“æ¡£æ¡ˆï¼ˆå®¡æ‰¹ä¸­ â†’ é€šè¿‡/驳回) */
export function auditProduct(data: MesPdProductApi.ProductAudit) {
  return requestClient.put('/mes/pd/product/audit', data);
}
src/views/bi/decision/alert-center/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,179 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DecisionAlertApi } from '#/api/bi/decision/alert';
import { onMounted, reactive, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { confirmAlert, getAlertPage, getUnprocessedCount, handleAlert } from '#/api/bi/decision/alert';
defineOptions({ name: 'DecisionAlertCenter' });
const unprocessedCount = ref(0);
const queryParams = reactive<Record<string, unknown>>({});
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: [
      {
        fieldName: 'status',
        label: '状态',
        component: 'Select',
        componentProps: {
          options: [
            { label: '全部', value: '' },
            { label: '未处理', value: 0 },
            { label: '已确认', value: 1 },
            { label: '已处理', value: 2 },
          ],
          allowClear: true,
        },
      },
      {
        fieldName: 'severity',
        label: '严重级别',
        component: 'Select',
        componentProps: {
          options: [
            { label: '全部', value: '' },
            { label: '警告', value: 1 },
            { label: '严重', value: 2 },
            { label: '紧急', value: 3 },
          ],
          allowClear: true,
        },
      },
    ],
  },
  gridOptions: {
    columns: [
      { field: 'id', title: 'ID', width: 60 },
      { field: 'kpiName', title: 'KPI名称', width: 140 },
      {
        field: 'severity', title: '严重级别', width: 90,
        slots: { default: 'severitySlot' },
      },
      {
        field: 'actualValue', title: '实际值', width: 100,
        formatter: ({ cellValue }: any) => cellValue?.toLocaleString(),
      },
      {
        field: 'thresholdValue', title: '阈值', width: 100,
        formatter: ({ cellValue }: any) => cellValue?.toLocaleString(),
      },
      { field: 'alertMessage', title: '预警消息', minWidth: 200 },
      {
        field: 'status', title: '状态', width: 80,
        slots: { default: 'statusSlot' },
      },
      { field: 'triggeredTime', title: '触发时间', width: 170 },
      { field: 'handleRemark', title: '处理备注', width: 140 },
      {
        title: '操作', width: 160,
        slots: { default: 'actionSlot' },
        fixed: 'right',
      },
    ],
    height: 'auto',
    proxyConfig: {
      ajax: {
        query: async ({ page, form }: any) => {
          const res = await getAlertPage({ ...page, ...form, ...queryParams });
          return res;
        },
      },
    },
  } as VxeTableGridOptions<DecisionAlertApi.AlertRecord>,
});
async function loadUnprocessed() {
  try {
    unprocessedCount.value = await getUnprocessedCount();
  } catch {
    // ignore
  }
}
async function handleConfirm(row: DecisionAlertApi.AlertRecord) {
  const hide = message.loading('确认中...', 0);
  try {
    await confirmAlert(row.id);
    message.success('已确认');
    gridApi.query();
    loadUnprocessed();
  } finally {
    hide();
  }
}
async function handleProcess(row: DecisionAlertApi.AlertRecord) {
  const hide = message.loading('处理中...', 0);
  try {
    await handleAlert(row.id, '已处理');
    message.success('已处理');
    gridApi.query();
    loadUnprocessed();
  } finally {
    hide();
  }
}
function severityColor(severity: number): string {
  if (severity === 3) return 'red';
  if (severity === 2) return 'orange';
  return 'blue';
}
function severityText(severity: number): string {
  if (severity === 3) return '紧急';
  if (severity === 2) return '严重';
  return '警告';
}
function statusColor(status: number): string {
  if (status === 0) return 'red';
  if (status === 1) return 'blue';
  return 'green';
}
function statusText(status: number): string {
  if (status === 0) return '未处理';
  if (status === 1) return '已确认';
  return '已处理';
}
onMounted(() => {
  loadUnprocessed();
});
</script>
<template>
  <Page :auto-content-height="true">
    <div class="mb-3">
      <span class="text-lg font-bold">预警中心</span>
      <Tag v-if="unprocessedCount > 0" color="red" class="ml-2">
        {{ unprocessedCount }} æ¡æœªå¤„理
      </Tag>
    </div>
    <Grid>
      <template #severitySlot="{ row }">
        <Tag :color="severityColor(row.severity)">{{ severityText(row.severity) }}</Tag>
      </template>
      <template #statusSlot="{ row }">
        <Tag :color="statusColor(row.status)">{{ statusText(row.status) }}</Tag>
      </template>
      <template #actionSlot="{ row }">
        <TableAction
          :actions="[
            { label: '确认', type: 'link', icon: ACTION_ICON.AUDIT, ifShow: row.status === 0, onClick: handleConfirm.bind(null, row) },
            { label: '处理', type: 'link', icon: ACTION_ICON.EDIT, ifShow: row.status < 2, onClick: handleProcess.bind(null, row) },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/bi/decision/alert-rule/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,119 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { field: 'id', title: 'ID', width: 60 },
    { field: 'name', title: '规则名称', width: 150 },
    {
      field: 'severity', title: '严重级别', width: 90,
      slots: { default: 'severitySlot' },
    },
    { field: 'comparisonOperator', title: '运算符', width: 70 },
    { field: 'thresholdValue', title: '阈值', width: 100 },
    {
      field: 'enabled', title: '启用', width: 70,
      slots: { default: 'enabledSlot' },
    },
    {
      title: '操作', width: 160,
      slots: { default: 'actionSlot' },
      fixed: 'right',
    },
  ];
}
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'enabled',
      label: '状态',
      component: 'Select',
      componentProps: {
        options: [
          { label: '全部', value: '' },
          { label: '启用', value: 1 },
          { label: '禁用', value: 0 },
        ],
        allowClear: true,
      },
    },
  ];
}
export function useFormSchema(kpiOptions: { label: string; value: number }[]): VbenFormSchema[] {
  return [
    {
      fieldName: 'kpiId',
      label: '关联KPI',
      component: 'Select',
      componentProps: { options: kpiOptions },
      rules: 'required',
    },
    {
      fieldName: 'name',
      label: '规则名称',
      component: 'Input',
      componentProps: { placeholder: '请输入规则名称' },
      rules: 'required',
    },
    {
      fieldName: 'severity',
      label: '严重级别',
      component: 'Select',
      componentProps: {
        options: [
          { label: '警告', value: 1 },
          { label: '严重', value: 2 },
          { label: '紧急', value: 3 },
        ],
      },
      rules: 'required',
    },
    {
      fieldName: 'comparisonOperator',
      label: '比较运算符',
      component: 'Select',
      componentProps: {
        options: [
          { label: '大于 >', value: '>' },
          { label: '小于 <', value: '<' },
          { label: '大于等于 >=', value: '>=' },
          { label: '小于等于 <=', value: '<=' },
        ],
      },
      rules: 'required',
    },
    {
      fieldName: 'thresholdValue', label: '阈值', component: 'InputNumber',
      componentProps: { placeholder: '阈值', style: 'width:100%' },
      rules: 'required',
    },
    {
      fieldName: 'enabled',
      label: '启用',
      component: 'Switch',
      componentProps: { checkedValue: 1, unCheckedValue: 0 },
    },
    {
      fieldName: 'notificationType', label: '通知方式', component: 'Input',
      componentProps: { placeholder: 'system/message/email' },
    },
    {
      fieldName: 'remark', label: '备注', component: 'InputTextArea',
      componentProps: { placeholder: '备注', rows: 2 },
    },
  ];
}
export function severityColor(severity: number): string {
  if (severity === 3) return 'red';
  if (severity === 2) return 'orange';
  return 'blue';
}
export function severityText(severity: number): string {
  if (severity === 3) return '紧急';
  if (severity === 2) return '严重';
  return '警告';
}
src/views/bi/decision/alert-rule/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DecisionAlertApi } from '#/api/bi/decision/alert';
import { Page, useVbenModal } from '@vben/common-ui';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteAlertRule, getAlertRulePage } from '#/api/bi/decision/alert';
import { severityColor, severityText, useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'DecisionAlertRule' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    proxyConfig: {
      ajax: {
        query: async ({ page, form }: any) => {
          const res = await getAlertRulePage({ ...page, ...form });
          return res;
        },
      },
    },
  } as VxeTableGridOptions<DecisionAlertApi.AlertRule>,
});
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData(null).open();
}
function handleEdit(row: DecisionAlertApi.AlertRule) {
  formModalApi.setData(row).open();
}
async function handleDelete(row: DecisionAlertApi.AlertRule) {
  const hide = message.loading(`正在删除「${row.name}」...`, 0);
  try {
    await deleteAlertRule(row.id!);
    message.success('删除成功');
    handleRefresh();
  } finally {
    hide();
  }
}
</script>
<template>
  <Page :auto-content-height="true">
    <div class="mb-3 flex items-center justify-between">
      <span class="text-lg font-bold">预警规则配置</span>
      <a-button type="primary" @click="handleCreate">新增</a-button>
    </div>
    <Grid>
      <template #severitySlot="{ row }">
        <Tag :color="severityColor(row.severity)">{{ severityText(row.severity) }}</Tag>
      </template>
      <template #enabledSlot="{ row }">
        <Tag :color="row.enabled === 1 ? 'green' : 'default'">{{ row.enabled === 1 ? '启用' : '禁用' }}</Tag>
      </template>
      <template #actionSlot="{ row }">
        <TableAction
          :actions="[
            { label: '编辑', type: 'link', icon: ACTION_ICON.EDIT, onClick: handleEdit.bind(null, row) },
            { label: '删除', type: 'link', danger: true, icon: ACTION_ICON.DELETE, onClick: handleDelete.bind(null, row) },
          ]"
        />
      </template>
    </Grid>
    <FormModal />
  </Page>
</template>
src/views/bi/decision/alert-rule/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
<script lang="ts" setup>
import type { DecisionAlertApi } from '#/api/bi/decision/alert';
import { computed, onMounted, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createAlertRule, getAlertRule, updateAlertRule } from '#/api/bi/decision/alert';
import { getKpiDefinitionList } from '#/api/bi/decision/kpi';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<DecisionAlertApi.AlertRule>();
const kpiOptions = ref<{ label: string; value: number }[]>([]);
const getTitle = computed(() => {
  return formData.value?.id ? '编辑预警规则' : '新增预警规则';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-2',
    labelWidth: 120,
  },
  layout: 'horizontal',
  schema: useFormSchema([]),
  showDefaultActions: false,
});
async function refreshKpiOptions() {
  try {
    const res: any = await getKpiDefinitionList();
    const list = res?.list || [];
    kpiOptions.value = list.map((k: any) => ({ label: `${k.name}(${k.code})`, value: k.id }));
    formApi.setState((prev) => ({
      ...prev,
      schema: useFormSchema(kpiOptions.value),
    }));
  } catch {
    // ignore
  }
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as DecisionAlertApi.AlertRule;
    try {
      await (formData.value?.id ? updateAlertRule(data) : createAlertRule(data));
      await modalApi.close();
      emit('success');
      message.success('操作成功');
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    await refreshKpiOptions();
    const data = modalApi.getData<DecisionAlertApi.AlertRule>();
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getAlertRule(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
onMounted(() => {
  refreshKpiOptions();
});
</script>
<template>
  <Modal :title="getTitle">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/bi/decision/forecast-analysis/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { DatePicker, Select, Spin } from 'ant-design-vue';
import dayjs from 'dayjs';
import { getForecastList } from '#/api/bi/decision/forecast';
defineOptions({ name: 'DecisionForecastAnalysis' });
const loading = ref(false);
const forecastData = ref<any[]>([]);
const forecastCode = ref('load_forecast_daily');
const FORECAST_OPTIONS = [
  { label: '日负荷预测', value: 'load_forecast_daily' },
  { label: '周负荷预测', value: 'load_forecast_weekly' },
  { label: '月负荷预测', value: 'load_forecast_monthly' },
];
async function loadData() {
  loading.value = true;
  try {
    forecastData.value = await getForecastList(forecastCode.value);
  } catch {
    forecastData.value = [];
  } finally {
    loading.value = false;
  }
}
onMounted(loadData);
</script>
<template>
  <Page :auto-content-height="true">
    <div class="p-4">
      <div class="mb-4 flex items-center gap-4">
        <h2 class="text-lg font-bold">负荷预测</h2>
        <Select
          v-model:value="forecastCode"
          :options="FORECAST_OPTIONS"
          style="width: 180px"
          @change="loadData"
        />
      </div>
      <Spin :spinning="loading">
        <div v-if="forecastData.length === 0" class="py-12 text-center text-gray-400">
          æš‚无预测数据,请先配置 KPI æŒ‡æ ‡å’Œé¢„警规则后查看
        </div>
        <div v-else class="grid grid-cols-1 gap-4 lg:grid-cols-2">
          <div v-for="item in forecastData" :key="item.id" class="rounded-lg border p-4">
            <div class="mb-2 flex items-center justify-between">
              <span class="font-medium">{{ item.forecastName }}</span>
              <span class="text-xs text-gray-400">{{ item.pointTime }}</span>
            </div>
            <div class="mb-1 text-2xl font-bold">
              {{ item.forecastValue ?? '-' }}
              <span class="ml-1 text-sm font-normal text-gray-400">(预测)</span>
            </div>
            <div v-if="item.actualValue != null" class="text-sm text-gray-500">
              å®žé™…值: {{ item.actualValue }}
              <span v-if="item.lowerBound != null" class="ml-2">
                ç½®ä¿¡åŒºé—´: [{{ item.lowerBound }} ~ {{ item.upperBound }}]
              </span>
            </div>
            <div v-if="item.dimension" class="mt-1 text-xs text-gray-400">
              {{ item.dimension }}: {{ item.dimensionValue }}
            </div>
          </div>
        </div>
      </Spin>
    </div>
  </Page>
</template>
src/views/bi/decision/kpi-config/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,154 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { field: 'code', title: 'KPI编码', width: 140 },
    { field: 'name', title: 'KPI名称', width: 150 },
    {
      field: 'category', title: '分类', width: 100,
      slots: { default: 'categorySlot' },
    },
    { field: 'unit', title: '单位', width: 70 },
    { field: 'chartType', title: '图表类型', width: 90 },
    {
      field: 'status', title: '状态', width: 70,
      slots: { default: 'statusSlot' },
    },
    { field: 'sort', title: '排序', width: 60 },
    {
      title: '操作', width: 160,
      slots: { default: 'actionSlot' },
      fixed: 'right',
    },
  ];
}
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'name',
      label: 'KPI名称',
      component: 'Input',
      componentProps: { placeholder: '请输入KPI名称' },
    },
    {
      fieldName: 'category',
      label: '分类',
      component: 'Select',
      componentProps: {
        options: [
          { label: '全部', value: '' },
          { label: '供电量', value: 'power_supply' },
          { label: '设备运行', value: 'device_operation' },
          { label: '生产', value: 'production' },
          { label: '质量', value: 'quality' },
          { label: '采购', value: 'procurement' },
          { label: '安全', value: 'safety' },
        ],
        allowClear: true,
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        options: [
          { label: '全部', value: '' },
          { label: '启用', value: 1 },
          { label: '禁用', value: 0 },
        ],
        allowClear: true,
      },
    },
  ];
}
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: 'KPI编码',
      component: 'Input',
      componentProps: { placeholder: '请输入KPI编码(唯一)' },
      rules: 'required',
    },
    {
      fieldName: 'name',
      label: 'KPI名称',
      component: 'Input',
      componentProps: { placeholder: '请输入KPI名称' },
      rules: 'required',
    },
    {
      fieldName: 'category',
      label: '分类',
      component: 'Select',
      componentProps: {
        options: [
          { label: '供电量', value: 'power_supply' },
          { label: '设备运行', value: 'device_operation' },
          { label: '生产', value: 'production' },
          { label: '质量', value: 'quality' },
          { label: '采购', value: 'procurement' },
          { label: '安全', value: 'safety' },
        ],
      },
      rules: 'required',
    },
    { fieldName: 'unit', label: '单位', component: 'Input', componentProps: { placeholder: '单位' } },
    {
      fieldName: 'chartType',
      label: '图表类型',
      component: 'Select',
      componentProps: {
        options: [
          { label: '数字卡片', value: 'number' },
          { label: '柱状图', value: 'bar' },
          { label: '折线图', value: 'line' },
          { label: '饼图', value: 'pie' },
        ],
      },
    },
    {
      fieldName: 'comparisonOperator',
      label: '比较运算符',
      component: 'Select',
      componentProps: {
        options: [
          { label: '大于 >', value: '>' },
          { label: '小于 <', value: '<' },
          { label: '大于等于 >=', value: '>=' },
          { label: '小于等于 <=', value: '<=' },
        ],
      },
    },
    { fieldName: 'thresholdWarn', label: '警告阈值', component: 'InputNumber', componentProps: { placeholder: '警告阈值', style: 'width:100%' } },
    { fieldName: 'thresholdCritical', label: '严重阈值', component: 'InputNumber', componentProps: { placeholder: '严重阈值', style: 'width:100%' } },
    { fieldName: 'refreshInterval', label: '刷新间隔(秒)', component: 'InputNumber', componentProps: { placeholder: '默认300', style: 'width:100%' } },
    { fieldName: 'sort', label: '排序', component: 'InputNumber', componentProps: { placeholder: '排序号', style: 'width:100%' } },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        options: [
          { label: '启用', value: 1 },
          { label: '禁用', value: 0 },
        ],
      },
    },
    { fieldName: 'querySql', label: '查询SQL', component: 'InputTextArea', componentProps: { placeholder: 'SELECT SUM(...) FROM ...', rows: 3 } },
    { fieldName: 'remark', label: '备注', component: 'InputTextArea', componentProps: { placeholder: '备注', rows: 2 } },
  ];
}
export const CATEGORY_MAP: Record<string, string> = {
  power_supply: '供电量',
  device_operation: '设备运行',
  production: '生产',
  quality: '质量',
  procurement: '采购',
  safety: '安全',
};
src/views/bi/decision/kpi-config/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DecisionKpiApi } from '#/api/bi/decision/kpi';
import { Page, useVbenModal } from '@vben/common-ui';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteKpiDefinition, getKpiDefinitionPage } from '#/api/bi/decision/kpi';
import { CATEGORY_MAP, useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'DecisionKpiConfig' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    proxyConfig: {
      ajax: {
        query: async ({ page, form }: any) => {
          const res = await getKpiDefinitionPage({ ...page, ...form });
          return res;
        },
      },
    },
  } as VxeTableGridOptions<DecisionKpiApi.KpiDefinition>,
});
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData(null).open();
}
function handleEdit(row: DecisionKpiApi.KpiDefinition) {
  formModalApi.setData(row).open();
}
async function handleDelete(row: DecisionKpiApi.KpiDefinition) {
  const hide = message.loading(`正在删除「${row.name}」...`, 0);
  try {
    await deleteKpiDefinition(row.id!);
    message.success(`删除「${row.name}」成功`);
    handleRefresh();
  } finally {
    hide();
  }
}
</script>
<template>
  <Page :auto-content-height="true">
    <div class="mb-3 flex items-center justify-between">
      <span class="text-lg font-bold">KPI指标定义</span>
      <a-button type="primary" @click="handleCreate">新增</a-button>
    </div>
    <Grid>
      <template #categorySlot="{ row }">
        <Tag color="blue">{{ CATEGORY_MAP[row.category] || row.category }}</Tag>
      </template>
      <template #statusSlot="{ row }">
        <Tag :color="row.status === 1 ? 'green' : 'default'">{{ row.status === 1 ? '启用' : '禁用' }}</Tag>
      </template>
      <template #actionSlot="{ row }">
        <TableAction
          :actions="[
            { label: '编辑', type: 'link', icon: ACTION_ICON.EDIT, onClick: handleEdit.bind(null, row) },
            { label: '删除', type: 'link', danger: true, icon: ACTION_ICON.DELETE, onClick: handleDelete.bind(null, row) },
          ]"
        />
      </template>
    </Grid>
    <FormModal />
  </Page>
</template>
src/views/bi/decision/kpi-config/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
<script lang="ts" setup>
import type { DecisionKpiApi } from '#/api/bi/decision/kpi';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createKpiDefinition, getKpiDefinition, updateKpiDefinition } from '#/api/bi/decision/kpi';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<DecisionKpiApi.KpiDefinition>();
const getTitle = computed(() => {
  return formData.value?.id ? '编辑KPI指标' : '新增KPI指标';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-2',
    labelWidth: 120,
  },
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as DecisionKpiApi.KpiDefinition;
    try {
      await (formData.value?.id ? updateKpiDefinition(data) : createKpiDefinition(data));
      await modalApi.close();
      emit('success');
      message.success('操作成功');
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    const data = modalApi.getData<DecisionKpiApi.KpiDefinition>();
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getKpiDefinition(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/bi/decision/kpi-dashboard/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Spin, Tag } from 'ant-design-vue';
import { getKpiOverview } from '#/api/bi/decision/kpi';
import type { DecisionKpiApi } from '#/api/bi/decision/kpi';
defineOptions({ name: 'DecisionKpiDashboard' });
const loading = ref(true);
const overview = ref<DecisionKpiApi.KpiOverview>({ kpis: [], total: 0 });
async function loadData() {
  loading.value = true;
  try {
    overview.value = await getKpiOverview();
  } catch {
    // ignore
  } finally {
    loading.value = false;
  }
}
const CATEGORY_MAP: Record<string, string> = {
  power_supply: '供电量',
  device_operation: '设备运行',
  production: '生产',
  quality: '质量',
  procurement: '采购',
  safety: '安全',
};
const CATEGORY_COLORS: Record<string, string> = {
  power_supply: '#1677ff',
  device_operation: '#722ed1',
  production: '#13c2c2',
  quality: '#52c41a',
  procurement: '#fa8c16',
  safety: '#ff4d4f',
};
function alertStatusColor(status: string): string {
  if (status === 'critical') return 'red';
  if (status === 'warn') return 'orange';
  return 'green';
}
function alertStatusText(status: string): string {
  if (status === 'critical') return '严重';
  if (status === 'warn') return '警告';
  return '正常';
}
onMounted(loadData);
</script>
<template>
  <Page :auto-content-height="true">
    <Spin :spinning="loading">
      <div class="p-4">
        <h2 class="mb-4 text-lg font-bold">电力 KPI çœ‹æ¿</h2>
        <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
          <div
            v-for="kpi in overview.kpis"
            :key="kpi.code"
            class="rounded-lg border p-4 shadow-sm transition-shadow hover:shadow-md"
            :style="{ borderTop: `3px solid ${CATEGORY_COLORS[kpi.category] || '#1677ff'}` }"
          >
            <div class="mb-1 flex items-center justify-between">
              <span class="text-xs text-gray-500">{{ CATEGORY_MAP[kpi.category] || kpi.category }}</span>
              <Tag :color="alertStatusColor(kpi.alertStatus)">
                {{ alertStatusText(kpi.alertStatus) }}
              </Tag>
            </div>
            <div class="mb-1 text-2xl font-bold">
              {{ kpi.value ?? '-' }}
            </div>
            <div class="flex items-center justify-between">
              <span class="text-sm font-medium">{{ kpi.name }}</span>
              <span class="text-xs text-gray-400">{{ kpi.unit || '' }}</span>
            </div>
          </div>
        </div>
      </div>
    </Spin>
  </Page>
</template>
src/views/bi/decision/trend-analysis/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Select, Spin, Table } from 'ant-design-vue';
import { getKpiOverview } from '#/api/bi/decision/kpi';
defineOptions({ name: 'DecisionTrendAnalysis' });
const loading = ref(false);
const trendData = ref<any[]>([]);
const selectedCategory = ref<string>('');
const CATEGORY_OPTIONS = [
  { label: '全部', value: '' },
  { label: '供电量', value: 'power_supply' },
  { label: '设备运行', value: 'device_operation' },
  { label: '生产', value: 'production' },
  { label: '质量', value: 'quality' },
  { label: '采购', value: 'procurement' },
  { label: '安全', value: 'safety' },
];
const columns = [
  { title: 'KPI名称', dataIndex: 'name', key: 'name', width: 150 },
  { title: '分类', dataIndex: 'category', key: 'category', width: 100 },
  { title: '当前值', dataIndex: 'value', key: 'value', width: 120 },
  { title: '单位', dataIndex: 'unit', key: 'unit', width: 80 },
  { title: '预警状态', dataIndex: 'alertStatus', key: 'alertStatus', width: 100 },
];
async function loadData() {
  loading.value = true;
  try {
    const overview = await getKpiOverview();
    let list = overview.kpis || [];
    if (selectedCategory.value) {
      list = list.filter((k) => k.category === selectedCategory.value);
    }
    trendData.value = list;
  } catch {
    trendData.value = [];
  } finally {
    loading.value = false;
  }
}
onMounted(loadData);
</script>
<template>
  <Page :auto-content-height="true">
    <div class="p-4">
      <div class="mb-4 flex items-center gap-4">
        <h2 class="text-lg font-bold">趋势分析</h2>
        <Select
          v-model:value="selectedCategory"
          :options="CATEGORY_OPTIONS"
          style="width: 160px"
          allow-clear
          @change="loadData"
        />
      </div>
      <Spin :spinning="loading">
        <Table
          :columns="columns"
          :data-source="trendData"
          :pagination="{ pageSize: 20 }"
          row-key="code"
          bordered
          size="middle"
        >
          <template #bodyCell="{ column, record }">
            <template v-if="column.key === 'alertStatus'">
              <span v-if="record.alertStatus === 'critical'" class="text-red-500">严重</span>
              <span v-else-if="record.alertStatus === 'warn'" class="text-orange-500">警告</span>
              <span v-else class="text-green-500">正常</span>
            </template>
          </template>
        </Table>
      </Spin>
    </div>
  </Page>
</template>
src/views/mes/dv/machinery/modules/telemetry-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
<script lang="ts" setup>
  import type { VxeTableGridOptions } from '#/adapter/vxe-table';
  import type { MesDvTelemetryApi } from '#/api/mes/dv/telemetry';
  import { watch } from 'vue';
  import { Tag } from 'ant-design-vue';
  import { useVbenVxeGrid } from '#/adapter/vxe-table';
  import { getLatestTelemetry } from '#/api/mes/dv/telemetry';
  import { useLatestColumns } from '../../telemetry/data';
  const props = defineProps<{ tbDeviceId?: string }>();
  const [Grid, gridApi] = useVbenVxeGrid({
    gridOptions: {
      columns: useLatestColumns(),
      height: 320,
      keepSource: true,
      proxyConfig: {
        ajax: {
          query: async () =>
            await getLatestTelemetry({ tbDeviceId: props.tbDeviceId }),
        },
      },
      rowConfig: { keyField: 'id', isHover: true },
      toolbarConfig: { refresh: true },
    } as VxeTableGridOptions<MesDvTelemetryApi.Telemetry>,
  });
  watch(
    () => props.tbDeviceId,
    (value) => {
      if (value) {
        gridApi.query();
      }
    },
  );
</script>
<template>
  <Grid table-title="数采实时数据">
    <template #anomaly="{ row }">
      <Tag :color="row.whetherAnomaly ? 'red' : 'green'">
        {{ row.whetherAnomaly ? '异常' : '正常' }}
      </Tag>
    </template>
  </Grid>
</template>
src/views/mes/dv/telemetry/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,98 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesDvTelemetryApi } from '#/api/mes/dv/telemetry';
/** æ˜¯å¦å¼‚常选项(布尔值,后端直接接收) */
export const ANOMALY_OPTIONS = [
  { label: '正常', value: false },
  { label: '异常', value: true },
];
/** åŽ†å²è®°å½•æœç´¢è¡¨å• */
export function useHistoryGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'deviceName',
      label: '设备名称',
      component: 'Input',
      componentProps: { allowClear: true, placeholder: '请输入设备名称' },
    },
    {
      fieldName: 'paramName',
      label: '信号名称',
      component: 'Input',
      componentProps: { allowClear: true, placeholder: '请输入信号名称' },
    },
    {
      fieldName: 'whetherAnomaly',
      label: '是否异常',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: ANOMALY_OPTIONS,
        placeholder: '请选择是否异常',
      },
    },
    {
      fieldName: 'telemetryDataTime',
      label: '遥测时间',
      component: 'RangePicker',
      componentProps: { class: '!w-full', valueFormat: 'YYYY-MM-DD HH:mm:ss' },
    },
  ];
}
/** å®žæ—¶æ•°æ®å­—段 */
export function useLatestColumns(): VxeTableGridOptions<MesDvTelemetryApi.Telemetry>['columns'] {
  return [
    { field: 'deviceName', title: '设备名称', minWidth: 140 },
    { field: 'paramName', title: '信号名称', minWidth: 140 },
    { field: 'timelyValue', title: '信号值(实时)', width: 120 },
    {
      field: 'whetherAnomaly',
      title: '是否异常',
      width: 100,
      slots: { default: 'anomaly' },
    },
    { field: 'shiftName', title: '班次', width: 100 },
    {
      field: 'telemetryDataTime',
      title: '遥测时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    { field: 'standardValue', title: '标准值', width: 110 },
    { field: 'avgValue', title: '均值', width: 110 },
    { field: 'maxValue', title: '最大值', width: 110 },
    { field: 'minValue', title: '最小值', width: 110 },
  ];
}
/** åŽ†å²è®°å½•å­—æ®µ */
export function useHistoryColumns(): VxeTableGridOptions<MesDvTelemetryApi.Telemetry>['columns'] {
  return [
    { field: 'deviceName', title: '设备名称', minWidth: 140 },
    { field: 'paramName', title: '信号名称', minWidth: 140 },
    { field: 'timelyValue', title: '信号值', width: 110 },
    {
      field: 'whetherAnomaly',
      title: '是否异常',
      width: 100,
      slots: { default: 'anomaly' },
    },
    { field: 'shiftName', title: '班次', width: 100 },
    { field: 'billNo', title: '单据号', minWidth: 120 },
    {
      field: 'telemetryDataTime',
      title: '遥测时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      field: 'pullTime',
      title: '拉取时间',
      width: 180,
      formatter: 'formatDateTime',
    },
  ];
}
src/views/mes/dv/telemetry/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,147 @@
<script lang="ts" setup>
  import type { VxeTableGridOptions } from '#/adapter/vxe-table';
  import type { MesDvTelemetryApi } from '#/api/mes/dv/telemetry';
  import { onBeforeUnmount, ref } from 'vue';
  import { Page } from '@vben/common-ui';
  import { useVbenVxeGrid } from '#/adapter/vxe-table';
  import { Button, message, Switch, Tabs, Tag } from 'ant-design-vue';
  import dayjs from 'dayjs';
  import {
    getLatestTelemetry,
    getTelemetryPage,
    pullTelemetry,
  } from '#/api/mes/dv/telemetry';
  import { useHistoryColumns, useHistoryGridFormSchema, useLatestColumns } from './data';
  const activeTab = ref<'latest' | 'history'>('latest');
  const autoRefresh = ref(false);
  const pulling = ref(false);
  const lastUpdateTime = ref<Date>();
  let refreshTimer: ReturnType<typeof setInterval> | undefined;
  const [LatestGrid, latestGridApi] = useVbenVxeGrid({
    gridOptions: {
      columns: useLatestColumns(),
      height: 'auto',
      keepSource: true,
      proxyConfig: {
        ajax: {
          query: async () => await getLatestTelemetry(),
        },
      },
      rowConfig: { keyField: 'id', isHover: true },
      toolbarConfig: { refresh: true },
    } as VxeTableGridOptions<MesDvTelemetryApi.Telemetry>,
  });
  const [HistoryGrid, historyGridApi] = useVbenVxeGrid({
    formOptions: { schema: useHistoryGridFormSchema() },
    gridOptions: {
      columns: useHistoryColumns(),
      height: 'auto',
      keepSource: true,
      proxyConfig: {
        ajax: {
          query: async ({ page }, formValues) =>
            await getTelemetryPage({
              pageNo: page.currentPage,
              pageSize: page.pageSize,
              ...formValues,
            }),
        },
      },
      rowConfig: { keyField: 'id', isHover: true },
      toolbarConfig: { refresh: true, search: true },
    } as VxeTableGridOptions<MesDvTelemetryApi.Telemetry>,
  });
  /** æ‹‰å–最新数采数据并刷新当前 Tab */
  async function handlePull() {
    pulling.value = true;
    try {
      const result = await pullTelemetry();
      lastUpdateTime.value = result.pullTime;
      message.success(
        `拉取成功,新增 ${result.recordCount} æ¡ï¼Œæ¶‰åŠè®¾å¤‡ ${result.deviceCount} å°`,
      );
      refreshActive();
    } finally {
      pulling.value = false;
    }
  }
  /** åˆ·æ–°å½“前 Tab æ•°æ® */
  function refreshActive() {
    if (activeTab.value === 'latest') {
      latestGridApi.query();
    } else {
      historyGridApi.query();
    }
  }
  /** åˆ‡æ¢å®žæ—¶è‡ªåŠ¨åˆ·æ–°ï¼ˆæ¯ 30 ç§’) */
  function toggleAutoRefresh(checked: boolean) {
    if (checked) {
      refreshTimer = setInterval(() => {
        if (activeTab.value === 'latest') {
          latestGridApi.query();
        }
      }, 30_000);
    } else if (refreshTimer) {
      clearInterval(refreshTimer);
      refreshTimer = undefined;
    }
  }
  onBeforeUnmount(() => {
    if (refreshTimer) {
      clearInterval(refreshTimer);
    }
  });
</script>
<template>
  <Page auto-content-height>
    <div class="p-4">
      <div class="mb-4 flex items-center gap-4">
        <Button type="primary" :loading="pulling" @click="handlePull">
          æ‹‰å–最新数据
        </Button>
        <Switch
          v-model:checked="autoRefresh"
          checked-children="实时自动刷新"
          un-checked-children="实时自动刷新"
          @change="toggleAutoRefresh"
        />
        <span v-if="lastUpdateTime" class="text-sm text-gray-500">
          æœ€è¿‘拉取:{{ dayjs(lastUpdateTime).format('YYYY-MM-DD HH:mm:ss') }}
        </span>
      </div>
      <Tabs v-model:active-key="activeTab">
        <Tabs.TabPane key="latest" tab="实时数据">
          <LatestGrid table-title="实时数据">
            <template #anomaly="{ row }">
              <Tag :color="row.whetherAnomaly ? 'red' : 'green'">
                {{ row.whetherAnomaly ? '异常' : '正常' }}
              </Tag>
            </template>
          </LatestGrid>
        </Tabs.TabPane>
        <Tabs.TabPane key="history" tab="历史记录">
          <HistoryGrid table-title="历史记录">
            <template #anomaly="{ row }">
              <Tag :color="row.whetherAnomaly ? 'red' : 'green'">
                {{ row.whetherAnomaly ? '异常' : '正常' }}
              </Tag>
            </template>
          </HistoryGrid>
        </Tabs.TabPane>
      </Tabs>
    </div>
  </Page>
</template>
src/views/mes/pd/basedata/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,808 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesPdBaseDataApi } from '#/api/mes/pd/basedata';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import {
  createDiscountPolicy,
  createElectricityType,
  createPriceStandard,
  createServicePackage,
  createTieredPrice,
  createVoltageLevel,
  deleteDiscountPolicy,
  deleteElectricityType,
  deletePriceStandard,
  deleteServicePackage,
  deleteTieredPrice,
  deleteVoltageLevel,
  exportDiscountPolicy,
  exportElectricityType,
  exportPriceStandard,
  exportServicePackage,
  exportTieredPrice,
  exportVoltageLevel,
  getDiscountPolicy,
  getDiscountPolicyPage,
  getElectricityType,
  getElectricityTypeList,
  getElectricityTypePage,
  getPriceStandard,
  getPriceStandardPage,
  getServicePackage,
  getServicePackageList,
  getServicePackagePage,
  getTieredPrice,
  getTieredPricePage,
  getVoltageLevel,
  getVoltageLevelList,
  getVoltageLevelPage,
  updateDiscountPolicy,
  updateElectricityType,
  updatePriceStandard,
  updateServicePackage,
  updateTieredPrice,
  updateVoltageLevel,
} from '#/api/mes/pd/basedata';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** å®žä½“ API æ˜ å°„(泛型 CRUD) */
export interface EntityApi {
  page: (params: any) => Promise<any>;
  get: (id: number) => Promise<any>;
  create: (data: any) => Promise<any>;
  update: (data: any) => Promise<any>;
  remove: (id: number) => Promise<any>;
  exportExcel: (params: any) => Promise<any>;
}
/** åŸºç¡€æ•°æ® Tab é…ç½® */
export interface BaseDataEntity {
  key: string;
  title: string;
  api: EntityApi;
  columns: VxeTableGridOptions<any>['columns'];
  gridFormSchema: VbenFormSchema[];
  formSchema: VbenFormSchema[];
}
/** åŠ è½½å®šä»·å¯¹è±¡ä¸‹æ‹‰ï¼ˆ1服务套餐/2电压等级/3用电类型) */
export async function loadObjectOptions(
  type?: number | null,
): Promise<{ label: string; value: number }[]> {
  if (type === 1) {
    const list = await getServicePackageList();
    return list.map((i) => ({ label: i.name ?? '', value: i.id ?? 0 }));
  }
  if (type === 2) {
    const list = await getVoltageLevelList();
    return list.map((i) => ({ label: i.name ?? '', value: i.id ?? 0 }));
  }
  if (type === 3) {
    const list = await getElectricityTypeList();
    return list.map((i) => ({ label: i.name ?? '', value: i.id ?? 0 }));
  }
  return [];
}
/** å¯¹è±¡ç±»åž‹ä¸‹æ‹‰ï¼ˆ1服务套餐/2电压等级/3用电类型) */
function useObjectTypeOptions() {
  return getDictOptions(DICT_TYPE.MES_PD_PRICE_OBJECT_TYPE, 'number');
}
/** é€šç”¨å¯ç”¨çŠ¶æ€åˆ— */
function useStatusColumn(title = '启用状态') {
  return {
    field: 'status',
    title,
    width: 90,
    cellRender: {
      name: 'CellDict',
      props: { type: DICT_TYPE.MES_PD_PRODUCT_STATUS },
    },
  } as const;
}
/** ç”¨ç”µç±»åž‹ */
export const ElectricityTypeConfig: BaseDataEntity = {
  key: 'electricityType',
  title: '用电类型',
  api: {
    page: getElectricityTypePage,
    get: getElectricityType,
    create: createElectricityType,
    update: updateElectricityType,
    remove: deleteElectricityType,
    exportExcel: exportElectricityType,
  },
  columns: [
    { field: 'code', title: '类型编码', width: 140 },
    { field: 'name', title: '类型名称', minWidth: 160 },
    { field: 'description', title: '类型说明', minWidth: 200 },
    { field: 'sort', title: '排序', width: 80 },
    useStatusColumn(),
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    { title: '操作', width: 200, fixed: 'right', slots: { default: 'actions' } },
  ],
  gridFormSchema: [
    {
      fieldName: 'name',
      label: '类型名称',
      component: 'Input',
      componentProps: { placeholder: '请输入类型名称' },
    },
  ],
  formSchema: [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'code',
      label: '类型编码',
      component: 'Input',
      componentProps: { placeholder: '请输入类型编码' },
      rules: 'required',
    },
    {
      fieldName: 'name',
      label: '类型名称',
      component: 'Input',
      componentProps: { placeholder: '请输入类型名称' },
      rules: 'required',
    },
    {
      fieldName: 'description',
      label: '类型说明',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: { placeholder: '请输入类型说明', rows: 2 },
    },
    {
      fieldName: 'sort',
      label: '排序',
      component: 'InputNumber',
      componentProps: { class: '!w-full', min: 0, precision: 0 },
    },
    {
      fieldName: 'status',
      label: '启用状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PD_PRODUCT_STATUS, 'number'),
        placeholder: '请选择启用状态',
      },
    },
  ],
};
/** ç”µåŽ‹ç­‰çº§ */
export const VoltageLevelConfig: BaseDataEntity = {
  key: 'voltageLevel',
  title: '电压等级',
  api: {
    page: getVoltageLevelPage,
    get: getVoltageLevel,
    create: createVoltageLevel,
    update: updateVoltageLevel,
    remove: deleteVoltageLevel,
    exportExcel: exportVoltageLevel,
  },
  columns: [
    { field: 'code', title: '等级编码', width: 140 },
    { field: 'name', title: '等级名称', minWidth: 160 },
    { field: 'voltageValue', title: '电压数值(kV)', width: 120 },
    { field: 'sort', title: '排序', width: 80 },
    useStatusColumn(),
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    { title: '操作', width: 200, fixed: 'right', slots: { default: 'actions' } },
  ],
  gridFormSchema: [
    {
      fieldName: 'name',
      label: '等级名称',
      component: 'Input',
      componentProps: { placeholder: '请输入等级名称' },
    },
  ],
  formSchema: [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'code',
      label: '等级编码',
      component: 'Input',
      componentProps: { placeholder: '请输入等级编码' },
      rules: 'required',
    },
    {
      fieldName: 'name',
      label: '等级名称',
      component: 'Input',
      componentProps: { placeholder: '请输入等级名称' },
      rules: 'required',
    },
    {
      fieldName: 'voltageValue',
      label: '电压数值(kV)',
      component: 'InputNumber',
      componentProps: { class: '!w-full', min: 0, precision: 2 },
    },
    {
      fieldName: 'sort',
      label: '排序',
      component: 'InputNumber',
      componentProps: { class: '!w-full', min: 0, precision: 0 },
    },
    {
      fieldName: 'status',
      label: '启用状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PD_PRODUCT_STATUS, 'number'),
        placeholder: '请选择启用状态',
      },
    },
  ],
};
/** æœåС套餐 */
export const ServicePackageConfig: BaseDataEntity = {
  key: 'servicePackage',
  title: '服务套餐',
  api: {
    page: getServicePackagePage,
    get: getServicePackage,
    create: createServicePackage,
    update: updateServicePackage,
    remove: deleteServicePackage,
    exportExcel: exportServicePackage,
  },
  columns: [
    { field: 'code', title: '套餐编码', width: 150 },
    { field: 'name', title: '套餐名称', minWidth: 160 },
    {
      field: 'price',
      title: '套餐价格',
      width: 110,
      formatter: 'formatNumber',
    },
    {
      field: 'effectiveDate',
      title: '生效日期',
      width: 160,
      formatter: 'formatDateTime',
    },
    {
      field: 'expireDate',
      title: '失效日期',
      width: 160,
      formatter: 'formatDateTime',
    },
    useStatusColumn(),
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    { title: '操作', width: 200, fixed: 'right', slots: { default: 'actions' } },
  ],
  gridFormSchema: [
    {
      fieldName: 'name',
      label: '套餐名称',
      component: 'Input',
      componentProps: { placeholder: '请输入套餐名称' },
    },
  ],
  formSchema: [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'code',
      label: '套餐编码',
      component: 'Input',
      componentProps: { placeholder: '保存时自动生成', disabled: true },
    },
    {
      fieldName: 'name',
      label: '套餐名称',
      component: 'Input',
      componentProps: { placeholder: '请输入套餐名称' },
      rules: 'required',
    },
    {
      fieldName: 'description',
      label: '套餐说明',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: { placeholder: '请输入套餐说明', rows: 2 },
    },
    {
      fieldName: 'price',
      label: '套餐价格',
      component: 'InputNumber',
      componentProps: { class: '!w-full', min: 0, precision: 2 },
    },
    {
      fieldName: 'effectiveDate',
      label: '生效日期',
      component: 'DatePicker',
      componentProps: { class: '!w-full', valueFormat: 'YYYY-MM-DD HH:mm:ss' },
    },
    {
      fieldName: 'expireDate',
      label: '失效日期',
      component: 'DatePicker',
      componentProps: { class: '!w-full', valueFormat: 'YYYY-MM-DD HH:mm:ss' },
    },
    {
      fieldName: 'status',
      label: '启用状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PD_PRODUCT_STATUS, 'number'),
        placeholder: '请选择启用状态',
      },
    },
  ],
};
/** å®šä»·æ ‡å‡† */
export const PriceStandardConfig: BaseDataEntity = {
  key: 'priceStandard',
  title: '定价标准',
  api: {
    page: getPriceStandardPage,
    get: getPriceStandard,
    create: createPriceStandard,
    update: updatePriceStandard,
    remove: deletePriceStandard,
    exportExcel: exportPriceStandard,
  },
  columns: [
    {
      field: 'objectType',
      title: '定价对象类型',
      width: 120,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_PD_PRICE_OBJECT_TYPE },
      },
    },
    { field: 'objectName', title: '定价对象', minWidth: 180 },
    {
      field: 'price',
      title: 'ä»·æ ¼',
      width: 110,
      formatter: 'formatNumber',
    },
    { field: 'unit', title: '计价单位', width: 100 },
    {
      field: 'effectiveDate',
      title: '生效日期',
      width: 160,
      formatter: 'formatDateTime',
    },
    useStatusColumn(),
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    { title: '操作', width: 200, fixed: 'right', slots: { default: 'actions' } },
  ],
  gridFormSchema: [
    {
      fieldName: 'objectType',
      label: '定价对象类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: useObjectTypeOptions(),
        placeholder: '请选择定价对象类型',
      },
    },
  ],
  formSchema: [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'objectType',
      label: '定价对象类型',
      component: 'Select',
      rules: 'selectRequired',
      componentProps: {
        allowClear: true,
        options: useObjectTypeOptions(),
        placeholder: '请选择定价对象类型',
      },
    },
    {
      fieldName: 'objectId',
      label: '定价对象',
      component: 'Select',
      rules: 'selectRequired',
      dependencies: {
        triggerFields: ['objectType'],
        async componentProps(values, form) {
          const options = await loadObjectOptions(values.objectType);
          return {
            allowClear: true,
            placeholder: '请选择定价对象',
            options,
            onChange: (value: number) => {
              const opt = options.find((o) => o.value === value);
              form.setFieldValue('objectName', opt?.label ?? '');
            },
          };
        },
      },
    },
    {
      fieldName: 'objectName',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'price',
      label: 'ä»·æ ¼',
      component: 'InputNumber',
      componentProps: { class: '!w-full', min: 0, precision: 2 },
      rules: 'required',
    },
    {
      fieldName: 'unit',
      label: '计价单位',
      component: 'Input',
      componentProps: { placeholder: '请输入计价单位' },
    },
    {
      fieldName: 'effectiveDate',
      label: '生效日期',
      component: 'DatePicker',
      componentProps: { class: '!w-full', valueFormat: 'YYYY-MM-DD HH:mm:ss' },
    },
    {
      fieldName: 'status',
      label: '启用状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PD_PRODUCT_STATUS, 'number'),
        placeholder: '请选择启用状态',
      },
    },
  ],
};
/** ä¼˜æƒ ç­–ç•¥ */
export const DiscountPolicyConfig: BaseDataEntity = {
  key: 'discountPolicy',
  title: '优惠策略',
  api: {
    page: getDiscountPolicyPage,
    get: getDiscountPolicy,
    create: createDiscountPolicy,
    update: updateDiscountPolicy,
    remove: deleteDiscountPolicy,
    exportExcel: exportDiscountPolicy,
  },
  columns: [
    { field: 'name', title: '策略名称', minWidth: 160 },
    {
      field: 'policyType',
      title: '策略类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_PD_DISCOUNT_POLICY_TYPE },
      },
    },
    {
      field: 'policyValue',
      title: '策略值',
      width: 110,
      formatter: 'formatNumber',
    },
    {
      field: 'targetType',
      title: '适用对象类型',
      width: 120,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_PD_PRICE_OBJECT_TYPE },
      },
    },
    {
      field: 'startDate',
      title: '生效日期',
      width: 160,
      formatter: 'formatDateTime',
    },
    {
      field: 'endDate',
      title: '失效日期',
      width: 160,
      formatter: 'formatDateTime',
    },
    useStatusColumn(),
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    { title: '操作', width: 200, fixed: 'right', slots: { default: 'actions' } },
  ],
  gridFormSchema: [
    {
      fieldName: 'name',
      label: '策略名称',
      component: 'Input',
      componentProps: { placeholder: '请输入策略名称' },
    },
    {
      fieldName: 'policyType',
      label: '策略类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PD_DISCOUNT_POLICY_TYPE, 'number'),
        placeholder: '请选择策略类型',
      },
    },
  ],
  formSchema: [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'name',
      label: '策略名称',
      component: 'Input',
      componentProps: { placeholder: '请输入策略名称' },
      rules: 'required',
    },
    {
      fieldName: 'policyType',
      label: '策略类型',
      component: 'Select',
      rules: 'selectRequired',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PD_DISCOUNT_POLICY_TYPE, 'number'),
        placeholder: '请选择策略类型',
      },
    },
    {
      fieldName: 'policyValue',
      label: '策略值',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '折扣填0-1比例,立减填金额',
      },
      help: '折扣:0-1 ä¹‹é—´çš„æ¯”例;立减:按金额减免',
      rules: 'required',
    },
    {
      fieldName: 'targetType',
      label: '适用对象类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: useObjectTypeOptions(),
        placeholder: '不选则全局适用',
      },
    },
    {
      fieldName: 'targetId',
      label: '适用对象',
      component: 'Select',
      dependencies: {
        triggerFields: ['targetType'],
        disabled: (values) => !values.targetType,
        async componentProps(values) {
          const options = await loadObjectOptions(values.targetType);
          return {
            allowClear: true,
            placeholder: values.targetType ? '请选择适用对象' : '全局适用',
            options,
          };
        },
      },
    },
    {
      fieldName: 'startDate',
      label: '生效日期',
      component: 'DatePicker',
      componentProps: { class: '!w-full', valueFormat: 'YYYY-MM-DD HH:mm:ss' },
    },
    {
      fieldName: 'endDate',
      label: '失效日期',
      component: 'DatePicker',
      componentProps: { class: '!w-full', valueFormat: 'YYYY-MM-DD HH:mm:ss' },
    },
    {
      fieldName: 'status',
      label: '启用状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PD_PRODUCT_STATUS, 'number'),
        placeholder: '请选择启用状态',
      },
    },
  ],
};
/** é˜¶æ¢¯ç”µä»· */
export const TieredPriceConfig: BaseDataEntity = {
  key: 'tieredPrice',
  title: '阶梯电价',
  api: {
    page: getTieredPricePage,
    get: getTieredPrice,
    create: createTieredPrice,
    update: updateTieredPrice,
    remove: deleteTieredPrice,
    exportExcel: exportTieredPrice,
  },
  columns: [
    { field: 'packageId', title: '服务套餐ID', width: 110 },
    { field: 'voltageLevelId', title: '电压等级ID', width: 110 },
    { field: 'startValue', title: '下限(含,kWh)', width: 120 },
    { field: 'endValue', title: '上限(不含,kWh)', width: 130 },
    {
      field: 'pricePerUnit',
      title: '档位单价',
      width: 110,
      formatter: 'formatNumber',
    },
    useStatusColumn(),
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    { title: '操作', width: 200, fixed: 'right', slots: { default: 'actions' } },
  ],
  gridFormSchema: [
    {
      fieldName: 'packageId',
      label: '服务套餐ID',
      component: 'Input',
      componentProps: { placeholder: '请输入服务套餐ID' },
    },
  ],
  formSchema: [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'packageId',
      label: '服务套餐',
      component: 'Select',
      componentProps: {
        allowClear: true,
        placeholder: '选择服务套餐(与电压等级二选一)',
      },
      dependencies: {
        triggerFields: ['voltageLevelId'],
        disabled: (values) => !!values.voltageLevelId,
        async componentProps(values) {
          const list = await getServicePackageList();
          return {
            allowClear: true,
            options: list.map((i) => ({ label: i.name, value: i.id })),
            placeholder: '选择服务套餐(与电压等级二选一)',
          };
        },
      },
    },
    {
      fieldName: 'voltageLevelId',
      label: '电压等级',
      component: 'Select',
      componentProps: {
        allowClear: true,
        placeholder: '选择电压等级(与服务套餐二选一)',
      },
      dependencies: {
        triggerFields: ['packageId'],
        disabled: (values) => !!values.packageId,
        async componentProps() {
          const list = await getVoltageLevelList();
          return {
            allowClear: true,
            options: list.map((i) => ({ label: i.name, value: i.id })),
            placeholder: '选择电压等级(与服务套餐二选一)',
          };
        },
      },
    },
    {
      fieldName: 'startValue',
      label: '档位下限(含,kWh)',
      component: 'InputNumber',
      componentProps: { class: '!w-full', min: 0, precision: 2 },
      rules: 'required',
    },
    {
      fieldName: 'endValue',
      label: '档位上限(不含,kWh)',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '为空表示上不封顶',
      },
    },
    {
      fieldName: 'pricePerUnit',
      label: '档位单价',
      component: 'InputNumber',
      componentProps: { class: '!w-full', min: 0, precision: 4 },
      rules: 'required',
    },
    {
      fieldName: 'status',
      label: '启用状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PD_TIERED_PRICE_STATUS, 'number'),
        placeholder: '请选择启用状态',
      },
    },
  ],
};
/** å…¨éƒ¨ Tab é…ç½® */
export const BaseDataConfigs: BaseDataEntity[] = [
  ElectricityTypeConfig,
  VoltageLevelConfig,
  ServicePackageConfig,
  PriceStandardConfig,
  DiscountPolicyConfig,
  TieredPriceConfig,
];
src/views/mes/pd/basedata/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
<script lang="ts" setup>
  import { ref } from 'vue';
  import { Page } from '@vben/common-ui';
  import { Tabs } from 'ant-design-vue';
  import { BaseDataConfigs } from './data';
  import BaseTable from './modules/base-table.vue';
  const activeKey = ref('electricityType');
</script>
<template>
  <Page auto-content-height>
    <div class="p-4">
      <Tabs v-model:activeKey="activeKey">
        <Tabs.TabPane
          v-for="cfg in BaseDataConfigs"
          :key="cfg.key"
          :tab="cfg.title"
        >
          <BaseTable :key="cfg.key" :config="cfg" />
        </Tabs.TabPane>
      </Tabs>
    </div>
  </Page>
</template>
src/views/mes/pd/basedata/modules/base-table.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,175 @@
<script lang="ts" setup>
  import type { VxeTableGridOptions } from '#/adapter/vxe-table';
  import type { BaseDataEntity } from '../data';
  import { useVbenModal } from '@vben/common-ui';
  import { $t } from '@vben/locales';
  import { downloadFileFromBlobPart } from '@vben/utils';
  import { message } from 'ant-design-vue';
  import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
  import Form from './form.vue';
  import PriceCalc from './price-calc.vue';
  const props = defineProps<{ config: BaseDataEntity }>();
  const [FormModal, formModalApi] = useVbenModal({
    connectedComponent: Form,
    destroyOnClose: true,
  });
  const [PriceCalcModal, priceCalcModalApi] = useVbenModal({
    connectedComponent: PriceCalc,
    destroyOnClose: true,
  });
  /** åˆ·æ–°è¡¨æ ¼ */
  function handleRefresh() {
    gridApi.query();
  }
  /** æ–°å¢ž */
  function handleCreate() {
    formModalApi.setData({ config: props.config, formType: 'create' }).open();
  }
  /** æŸ¥çœ‹ */
  function handleDetail(row: any) {
    formModalApi
      .setData({ config: props.config, formType: 'detail', id: row.id })
      .open();
  }
  /** ç¼–辑 */
  function handleEdit(row: any) {
    formModalApi
      .setData({ config: props.config, formType: 'update', id: row.id })
      .open();
  }
  /** åˆ é™¤ */
  async function handleDelete(row: any) {
    const hideLoading = message.loading({
      content: $t('ui.actionMessage.deleting', [row.name ?? row.code ?? '']),
      duration: 0,
    });
    try {
      await props.config.api.remove(row.id);
      message.success($t('ui.actionMessage.deleteSuccess', [row.name ?? row.code ?? '']));
      handleRefresh();
    } finally {
      hideLoading();
    }
  }
  /** å¯¼å‡º */
  async function handleExport() {
    const data = await props.config.api.exportExcel(
      await gridApi.formApi.getValues(),
    );
    downloadFileFromBlobPart({ fileName: `${props.config.title}.xls`, source: data });
  }
  /** é˜¶æ¢¯ç”µä»·æµ‹ç®— */
  function handleCalc() {
    priceCalcModalApi.open();
  }
  const [Grid, gridApi] = useVbenVxeGrid({
    formOptions: {
      schema: props.config.gridFormSchema,
    },
    gridOptions: {
      columns: props.config.columns,
      height: 'auto',
      keepSource: true,
      proxyConfig: {
        ajax: {
          query: async ({ page }, formValues) =>
            await props.config.api.page({
              pageNo: page.currentPage,
              pageSize: page.pageSize,
              ...formValues,
            }),
        },
      },
      rowConfig: {
        keyField: 'id',
        isHover: true,
      },
      toolbarConfig: {
        refresh: true,
        search: true,
      },
    } as VxeTableGridOptions<any>,
  });
</script>
<template>
  <FormModal @success="handleRefresh" />
  <PriceCalcModal v-if="config.key === 'tieredPrice'" />
  <Grid :table-title="`${config.title}列表`">
    <template #toolbar-tools>
      <TableAction
        :actions="[
          {
            label: $t('ui.actionTitle.create', [config.title]),
            type: 'primary',
            icon: ACTION_ICON.ADD,
            auth: ['mes:pd-basedata:create'],
            onClick: handleCreate,
          },
          {
            label: '阶梯电价测算',
            type: 'primary',
            icon: ACTION_ICON.CALC,
            auth: ['mes:pd-basedata:query'],
            ifShow: config.key === 'tieredPrice',
            onClick: handleCalc,
          },
          {
            label: $t('ui.actionTitle.export'),
            type: 'primary',
            icon: ACTION_ICON.DOWNLOAD,
            auth: ['mes:pd-basedata:export'],
            onClick: handleExport,
          },
        ]"
      />
    </template>
    <template #actions="{ row }">
      <TableAction
        :actions="[
          {
            label: $t('common.edit'),
            type: 'link',
            icon: ACTION_ICON.EDIT,
            auth: ['mes:pd-basedata:update'],
            onClick: handleEdit.bind(null, row),
          },
          {
            label: $t('common.delete'),
            type: 'link',
            danger: true,
            icon: ACTION_ICON.DELETE,
            auth: ['mes:pd-basedata:delete'],
            popConfirm: {
              title: $t('ui.actionMessage.deleteConfirm', [
                row.name ?? row.code ?? '',
              ]),
              confirm: handleDelete.bind(null, row),
            },
          },
          {
            label: $t('common.detail'),
            type: 'link',
            icon: ACTION_ICON.VIEW,
            auth: ['mes:pd-basedata:query'],
            onClick: handleDetail.bind(null, row),
          },
        ]"
      />
    </template>
  </Grid>
</template>
src/views/mes/pd/basedata/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
<script lang="ts" setup>
  import type { BaseDataEntity, FormType } from '../data';
  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';
  const emit = defineEmits(['success']);
  const config = ref<BaseDataEntity>();
  const formType = ref<FormType>('create'); // è¡¨å•模式
  const formData = ref<any>();
  const isDetail = computed(() => formType.value === 'detail'); // æ˜¯å¦æŸ¥çœ‹æ¨¡å¼
  const getTitle = computed(() => {
    const title = config.value?.title ?? '';
    if (formType.value === 'detail') {
      return `查看${title}`;
    }
    return formType.value === 'update' ? `修改${title}` : `新增${title}`;
  });
  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();
      try {
        if (data.id) {
          await config.value!.api.update(data);
        } else {
          await config.value!.api.create(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<{
        config: BaseDataEntity;
        formType: FormType;
        id?: number;
      }>();
      config.value = data.config;
      formType.value = data.formType;
      formApi.setState({ schema: data.config.formSchema });
      formApi.setDisabled(formType.value === 'detail');
      modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
      if (!data.id) {
        return;
      }
      modalApi.lock();
      try {
        formData.value = await data.config.api.get(data.id);
        // è®¾ç½®åˆ° values
        await formApi.setValues(formData.value);
      } finally {
        modalApi.unlock();
      }
    },
  });
</script>
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/mes/pd/basedata/modules/price-calc.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,171 @@
<script lang="ts" setup>
  import type { VbenFormSchema } from '#/adapter/form';
  import type { MesPdPriceApi } from '#/api/mes/pd/price';
  import { ref } from 'vue';
  import { useVbenModal } from '@vben/common-ui';
  import { $t } from '@vben/locales';
  import { Descriptions, Empty, Table } from 'ant-design-vue';
  import { useVbenForm } from '#/adapter/form';
  import { calcPrice } from '#/api/mes/pd/price';
  import { getServicePackageList, getVoltageLevelList } from '#/api/mes/pd/basedata';
  const result = ref<MesPdPriceApi.PriceCalcResp>();
  const hasResult = ref(false);
  const [Form, formApi] = useVbenForm({
    commonConfig: {
      componentProps: {
        class: 'w-full',
      },
      formItemClass: 'col-span-1',
      labelWidth: 130,
    },
    wrapperClass: 'grid-cols-3',
    layout: 'horizontal',
    schema: [],
    showDefaultActions: false,
  });
  /** æµ‹ç®—表单字段 */
  function useCalcSchema(): VbenFormSchema[] {
    return [
      {
        fieldName: 'calcType',
        label: '测算类型',
        component: 'Select',
        rules: 'selectRequired',
        componentProps: {
          options: [
            { label: '服务套餐', value: 1 },
            { label: '电压等级', value: 2 },
          ],
          placeholder: '请选择测算类型',
        },
      },
      {
        fieldName: 'packageId',
        label: '服务套餐',
        component: 'Select',
        rules: 'selectRequired',
        dependencies: {
          triggerFields: ['calcType'],
          show: (values) => values.calcType === 1,
          async componentProps(values, form) {
            if (values.calcType !== 1) {
              form.setFieldValue('packageId', undefined);
              return { allowClear: true, options: [], placeholder: '请选择服务套餐' };
            }
            const list = await getServicePackageList();
            return {
              allowClear: true,
              options: list.map((i) => ({ label: i.name, value: i.id })),
              placeholder: '请选择服务套餐',
            };
          },
        },
      },
      {
        fieldName: 'voltageLevelId',
        label: '电压等级',
        component: 'Select',
        rules: 'selectRequired',
        dependencies: {
          triggerFields: ['calcType'],
          show: (values) => values.calcType === 2,
          async componentProps(values, form) {
            if (values.calcType !== 2) {
              form.setFieldValue('voltageLevelId', undefined);
              return { allowClear: true, options: [], placeholder: '请选择电压等级' };
            }
            const list = await getVoltageLevelList();
            return {
              allowClear: true,
              options: list.map((i) => ({ label: i.name, value: i.id })),
              placeholder: '请选择电压等级',
            };
          },
        },
      },
      {
        fieldName: 'quantity',
        label: '用电量(kWh)',
        component: 'InputNumber',
        componentProps: { class: '!w-full', min: 0, precision: 2 },
        rules: 'required',
      },
    ];
  }
  const [Modal, modalApi] = useVbenModal({
    async onConfirm() {
      const { valid } = await formApi.validate();
      if (!valid) {
        return;
      }
      modalApi.lock();
      try {
        const data = (await formApi.getValues()) as MesPdPriceApi.PriceCalcReq;
        result.value = await calcPrice(data);
        hasResult.value = true;
      } finally {
        modalApi.unlock();
      }
    },
    async onOpenChange(isOpen: boolean) {
      if (!isOpen) {
        return;
      }
      result.value = undefined;
      hasResult.value = false;
      formApi.setState({ schema: useCalcSchema() });
      formApi.resetForm();
      modalApi.setState({ confirmText: '测算', showConfirmButton: true });
    },
  });
</script>
<template>
  <Modal :title="'阶梯电价测算'" class="w-4/5">
    <Form class="mx-4 mb-4" />
    <div v-if="hasResult && result" class="mx-4">
      <Descriptions
        :column="3"
        bordered
        size="small"
        class="mb-4"
      >
        <Descriptions.Item label="原价">
          {{ result.originalPrice ?? 0 }}
        </Descriptions.Item>
        <Descriptions.Item label="优惠金额">
          {{ result.discountAmount ?? 0 }}
        </Descriptions.Item>
        <Descriptions.Item label="优惠后总价">
          <span class="text-lg font-bold text-primary">
            {{ result.finalPrice ?? 0 }}
          </span>
        </Descriptions.Item>
      </Descriptions>
      <Table
        :columns="[
          { title: '档位下限(含,kWh)', dataIndex: 'startValue', key: 'startValue' },
          { title: '档位上限(不含,kWh)', dataIndex: 'endValue', key: 'endValue' },
          { title: '本档电量(kWh)', dataIndex: 'tierQuantity', key: 'tierQuantity' },
          { title: '档位单价', dataIndex: 'pricePerUnit', key: 'pricePerUnit' },
          { title: '小计', dataIndex: 'subtotal', key: 'subtotal' },
        ]"
        :data-source="result.details ?? []"
        :pagination="false"
        size="small"
        class="mb-4"
      />
    </div>
    <div v-else class="mx-4">
      <Empty description="请选择测算类型并输入用电量后点击「测算」" />
    </div>
  </Modal>
</template>
src/views/mes/pd/product/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,181 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesPdProductApi } from '#/api/mes/pd/product';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/** å®¡æ‰¹çŠ¶æ€å¸¸é‡ */
export const AuditStatus = {
  DRAFT: 0, // æœªæäº¤
  PROCESS: 10, // å®¡æ‰¹ä¸­
  APPROVE: 20, // å®¡æ ¸é€šè¿‡
  REJECT: 30, // å®¡æ ¸ä¸é€šè¿‡
} as const;
/** æ–°å¢ž/修改产品档案 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'archiveType',
      label: '档案类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PD_PRODUCT_ARCHIVE_TYPE, 'number'),
        placeholder: '请选择档案类型',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'name',
      label: '档案名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入档案名称',
      },
      rules: 'required',
    },
    {
      fieldName: 'spec',
      label: '规格型号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入规格型号',
      },
    },
    {
      fieldName: 'unit',
      label: '单位',
      component: 'Input',
      componentProps: {
        placeholder: '请输入单位',
      },
    },
    {
      fieldName: 'price',
      label: '基准价格',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        precision: 2,
        placeholder: '请输入基准价格',
      },
    },
    {
      fieldName: 'status',
      label: '启用状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PD_PRODUCT_STATUS, 'number'),
        placeholder: '请选择启用状态',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'archiveType',
      label: '档案类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PD_PRODUCT_ARCHIVE_TYPE, 'number'),
        placeholder: '请选择档案类型',
      },
    },
    {
      fieldName: 'name',
      label: '档案名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入档案名称',
      },
    },
    {
      fieldName: 'auditStatus',
      label: '审批状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PD_AUDIT_STATUS, 'number'),
        placeholder: '请选择审批状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesPdProductApi.Product>['columns'] {
  return [
    { field: 'code', title: '档案编码', width: 150 },
    { field: 'name', title: '档案名称', minWidth: 160 },
    {
      field: 'archiveType',
      title: '档案类型',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_PD_PRODUCT_ARCHIVE_TYPE },
      },
    },
    { field: 'spec', title: '规格型号', width: 140 },
    { field: 'unit', title: '单位', width: 80 },
    {
      field: 'price',
      title: '基准价格',
      width: 100,
      formatter: 'formatNumber',
    },
    {
      field: 'auditStatus',
      title: '审批状态',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_PD_AUDIT_STATUS },
      },
    },
    { field: 'auditRemark', title: '审批意见', width: 150 },
    {
      field: 'createTime',
      title: '创建时间',
      width: 170,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 260,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/mes/pd/product/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,214 @@
<script lang="ts" setup>
  import type { VxeTableGridOptions } from '#/adapter/vxe-table';
  import type { MesPdProductApi } from '#/api/mes/pd/product';
  import { useVbenModal } from '@vben/common-ui';
  import { $t } from '@vben/locales';
  import { downloadFileFromBlobPart } from '@vben/utils';
  import { message } from 'ant-design-vue';
  import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
  import {
    deleteProduct,
    exportProduct,
    getProductPage,
    submitProduct,
  } from '#/api/mes/pd/product';
  import { AuditStatus, useGridColumns, useGridFormSchema } from './data';
  import Audit from './modules/audit.vue';
  import Form from './modules/form.vue';
  const [FormModal, formModalApi] = useVbenModal({
    connectedComponent: Form,
    destroyOnClose: true,
  });
  const [AuditModal, auditModalApi] = useVbenModal({
    connectedComponent: Audit,
    destroyOnClose: true,
  });
  /** åˆ·æ–°è¡¨æ ¼ */
  function handleRefresh() {
    gridApi.query();
  }
  /** æ–°å¢ž */
  function handleCreate() {
    formModalApi.setData({ formType: 'create' }).open();
  }
  /** æŸ¥çœ‹ */
  function handleDetail(row: MesPdProductApi.Product) {
    formModalApi.setData({ id: row.id, formType: 'detail' }).open();
  }
  /** ç¼–辑 */
  function handleEdit(row: MesPdProductApi.Product) {
    formModalApi.setData({ id: row.id, formType: 'update' }).open();
  }
  /** åˆ é™¤ */
  async function handleDelete(row: MesPdProductApi.Product) {
    const hideLoading = message.loading({
      content: $t('ui.actionMessage.deleting', [row.code ?? '']),
      duration: 0,
    });
    try {
      await deleteProduct(row.id!);
      message.success($t('ui.actionMessage.deleteSuccess', [row.code ?? '']));
      handleRefresh();
    } finally {
      hideLoading();
    }
  }
  /** æäº¤å®¡æ‰¹ï¼ˆæœªæäº¤/驳回 â†’ å®¡æ‰¹ä¸­ï¼‰ */
  async function handleSubmit(row: MesPdProductApi.Product) {
    const hideLoading = message.loading({ content: '提交审批中...', duration: 0 });
    try {
      await submitProduct(row.id!);
      message.success('提交成功');
      handleRefresh();
    } finally {
      hideLoading();
    }
  }
  /** æ‰“开审批弹窗(通过/驳回) */
  function handleAudit(row: MesPdProductApi.Product, pass: boolean) {
    auditModalApi.setData({ row, pass }).open();
  }
  /** å¯¼å‡º */
  async function handleExport() {
    const data = await exportProduct(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 getProductPage({
              pageNo: page.currentPage,
              pageSize: page.pageSize,
              ...formValues,
            }),
        },
      },
      rowConfig: {
        keyField: 'id',
        isHover: true,
      },
      toolbarConfig: {
        refresh: true,
        search: true,
      },
    } as VxeTableGridOptions<MesPdProductApi.Product>,
  });
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <AuditModal @success="handleRefresh" />
    <Grid table-title="产品档案列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['产品档案']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:pd-product:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['mes:pd-product:export'],
              onClick: handleExport,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '提交审批',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['mes:pd-product:submit'],
              ifShow:
                row.auditStatus === AuditStatus.DRAFT ||
                row.auditStatus === AuditStatus.REJECT,
              popConfirm: {
                title: '确认提交审批?',
                confirm: handleSubmit.bind(null, row),
              },
            },
            {
              label: '审核通过',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['mes:pd-product:audit'],
              ifShow: row.auditStatus === AuditStatus.PROCESS,
              onClick: handleAudit.bind(null, row, true),
            },
            {
              label: '审核驳回',
              type: 'link',
              danger: true,
              icon: ACTION_ICON.AUDIT,
              auth: ['mes:pd-product:audit'],
              ifShow: row.auditStatus === AuditStatus.PROCESS,
              onClick: handleAudit.bind(null, row, false),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:pd-product:update'],
              ifShow:
                row.auditStatus === AuditStatus.DRAFT ||
                row.auditStatus === AuditStatus.REJECT,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:pd-product:delete'],
              ifShow:
                row.auditStatus === AuditStatus.DRAFT ||
                row.auditStatus === AuditStatus.REJECT,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.code ?? '']),
                confirm: handleDelete.bind(null, row),
              },
            },
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['mes:pd-product:query'],
              onClick: handleDetail.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/mes/pd/product/modules/audit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
<script lang="ts" setup>
  import type { VbenFormSchema } from '#/adapter/form';
  import type { MesPdProductApi } from '#/api/mes/pd/product';
  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 { auditProduct } from '#/api/mes/pd/product';
  const emit = defineEmits(['success']);
  const pass = ref(true); // true=通过 / false=驳回
  const rowData = ref<MesPdProductApi.Product>();
  const getTitle = computed(() => (pass.value ? '审核通过' : '审核驳回'));
  const [Form, formApi] = useVbenForm({
    commonConfig: {
      componentProps: {
        class: 'w-full',
      },
      formItemClass: 'col-span-1',
      labelWidth: 130,
    },
    wrapperClass: 'grid-cols-1',
    layout: 'horizontal',
    schema: [],
    showDefaultActions: false,
  });
  /** å®¡æ ¸è¡¨å•字段 */
  function useAuditSchema(): VbenFormSchema[] {
    return [
      {
        fieldName: 'auditRemark',
        label: '审批意见',
        component: 'Textarea',
        formItemClass: 'col-span-1',
        componentProps: {
          rows: 3,
          placeholder: pass.value
            ? '请输入审批意见(可选)'
            : '请输入驳回原因(必填)',
        },
        rules: pass.value ? '' : 'required',
      },
    ];
  }
  const [Modal, modalApi] = useVbenModal({
    async onConfirm() {
      const { valid } = await formApi.validate();
      if (!valid) {
        return;
      }
      modalApi.lock();
      const values = await formApi.getValues();
      try {
        await auditProduct({
          id: rowData.value!.id!,
          pass: pass.value,
          auditRemark: values.auditRemark,
        });
        await modalApi.close();
        emit('success');
        message.success(pass.value ? '审批通过' : '审批驳回');
      } finally {
        modalApi.unlock();
      }
    },
    async onOpenChange(isOpen: boolean) {
      if (!isOpen) {
        return;
      }
      const data = modalApi.getData<{
        row: MesPdProductApi.Product;
        pass: boolean;
      }>();
      rowData.value = data.row;
      pass.value = data.pass;
      formApi.setState({ schema: useAuditSchema() });
      formApi.resetForm();
    },
  });
</script>
<template>
  <Modal :title="getTitle" class="w-2/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/mes/pd/product/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,100 @@
<script lang="ts" setup>
  import type { FormType } from '../data';
  import type { MesPdProductApi } from '#/api/mes/pd/product';
  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 {
    createProduct,
    getProduct,
    updateProduct,
  } from '#/api/mes/pd/product';
  import { useFormSchema } from '../data';
  const emit = defineEmits(['success']);
  const formType = ref<FormType>('create'); // è¡¨å•模式
  const formData = ref<MesPdProductApi.Product>();
  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 MesPdProductApi.Product;
      try {
        await (data.id ? updateProduct(data) : createProduct(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 getProduct(data.id);
        // è®¾ç½®åˆ° values
        await formApi.setValues(formData.value);
      } finally {
        modalApi.unlock();
      }
    },
  });
</script>
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
  </Modal>
</template>