From a4025834e2304dc57f6e98d58feeb9416bd90609 Mon Sep 17 00:00:00 2001
From: xiaoyi <908147524@qq.com>
Date: 星期四, 20 八月 2026 14:10:49 +0800
Subject: [PATCH] feat 功能变更

---
 src/views/bi/decision/alert-rule/data.ts              |  119 +
 src/views/bi/decision/trend-analysis/index.vue        |   87 +
 src/views/bi/decision/kpi-config/modules/form.vue     |   74 +
 src/api/bi/decision/forecast.ts                       |   30 
 src/views/bi/decision/alert-center/index.vue          |  179 ++
 src/views/mes/pd/product/modules/form.vue             |  100 +
 src/views/mes/pd/basedata/index.vue                   |   28 
 src/api/mes/pd/product/index.ts                       |   76 +
 src/views/mes/pd/product/data.ts                      |  181 ++
 src/api/bi/decision/kpi.ts                            |   77 +
 src/views/mes/pd/basedata/modules/price-calc.vue      |  171 ++
 src/views/bi/decision/alert-rule/index.vue            |   88 +
 src/views/mes/dv/telemetry/data.ts                    |   98 +
 src/views/bi/decision/forecast-analysis/index.vue     |   78 +
 src/views/mes/pd/basedata/modules/base-table.vue      |  175 ++
 src/views/mes/pd/product/index.vue                    |  214 +++
 src/views/bi/decision/kpi-config/index.vue            |   88 +
 src/views/bi/decision/kpi-dashboard/index.vue         |   90 +
 src/views/mes/pd/product/modules/audit.vue            |   94 +
 src/api/mes/pd/price/index.ts                         |   38 
 src/views/mes/pd/basedata/data.ts                     |  808 ++++++++++++
 src/views/bi/decision/alert-rule/modules/form.vue     |   95 +
 src/views/bi/decision/kpi-config/data.ts              |  154 ++
 src/views/mes/pd/basedata/modules/form.vue            |  102 +
 src/api/mes/dv/telemetry/index.ts                     |   72 +
 src/api/mes/pd/basedata/index.ts                      |  330 +++++
 src/views/mes/dv/telemetry/index.vue                  |  147 ++
 src/views/mes/dv/machinery/modules/telemetry-list.vue |   50 
 src/api/bi/decision/alert.ts                          |   68 +
 29 files changed, 3,911 insertions(+), 0 deletions(-)

diff --git a/src/api/bi/decision/alert.ts b/src/api/bi/decision/alert.ts
new file mode 100644
index 0000000..43ab3f5
--- /dev/null
+++ b/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 } });
+}
\ No newline at end of file
diff --git a/src/api/bi/decision/forecast.ts b/src/api/bi/decision/forecast.ts
new file mode 100644
index 0000000..e85d458
--- /dev/null
+++ b/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 },
+  });
+}
\ No newline at end of file
diff --git a/src/api/bi/decision/kpi.ts b/src/api/bi/decision/kpi.ts
new file mode 100644
index 0000000..c7fcfbf
--- /dev/null
+++ b/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 } });
+}
\ No newline at end of file
diff --git a/src/api/mes/dv/telemetry/index.ts b/src/api/mes/dv/telemetry/index.ts
new file mode 100644
index 0000000..23af31b
--- /dev/null
+++ b/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',
+  );
+}
diff --git a/src/api/mes/pd/basedata/index.ts b/src/api/mes/pd/basedata/index.ts
new file mode 100644
index 0000000..c0b468e
--- /dev/null
+++ b/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; // 妗d綅涓嬮檺锛堝惈锛宬Wh锛�
+    endValue?: number; // 妗d綅涓婇檺锛堜笉鍚紝kWh锛屽彲绌�=涓婁笉灏侀《锛�
+    pricePerUnit?: number; // 妗d綅鍗曚环
+    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 });
+}
diff --git a/src/api/mes/pd/price/index.ts b/src/api/mes/pd/price/index.ts
new file mode 100644
index 0000000..9b18c2b
--- /dev/null
+++ b/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锛坈alcType=1 鏃跺繀濉級
+    voltageLevelId?: number; // 鐢靛帇绛夌骇ID锛坈alcType=2 鏃跺繀濉級
+    quantity: number; // 鐢ㄧ數閲忥紙kWh锛�
+  }
+
+  /** 闃舵鐢典环娴嬬畻鍒嗘。鏄庣粏 */
+  export interface PriceCalcDetail {
+    startValue?: number; // 妗d綅涓嬮檺锛堝惈锛宬Wh锛�
+    endValue?: number; // 妗d綅涓婇檺锛堜笉鍚紝kWh锛屽彲绌�=涓婁笉灏侀《锛�
+    tierQuantity?: number; // 钀藉叆鏈。鐢甸噺锛坘Wh锛�
+    pricePerUnit?: number; // 妗d綅鍗曚环
+    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,
+  );
+}
diff --git a/src/api/mes/pd/product/index.ts b/src/api/mes/pd/product/index.ts
new file mode 100644
index 0000000..fb9c7e5
--- /dev/null
+++ b/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 {
+  /** 浜у搧妗f */
+  export interface Product {
+    id?: number; // 缂栧彿
+    archiveType?: number; // 妗f绫诲瀷锛�1鐢靛姏鏈嶅姟/2渚涚數浜у搧/3澧炲�艰兘婧愪骇鍝侊級
+    archiveTypeName?: string; // 妗f绫诲瀷鍚嶇О
+    code?: string; // 妗f缂栫爜
+    name?: string; // 妗f鍚嶇О
+    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; // 鍒涘缓鏃堕棿
+  }
+
+  /** 浜у搧妗f瀹℃壒鍙傛暟 */
+  export interface ProductAudit {
+    id: number; // 缂栧彿
+    pass: boolean; // 瀹℃壒缁撴灉锛歵rue=閫氳繃 / false=椹冲洖
+    auditRemark?: string; // 瀹℃壒鎰忚
+  }
+}
+
+/** 鏌ヨ浜у搧妗f鍒嗛〉 */
+export function getProductPage(params: PageParam) {
+  return requestClient.get<PageResult<MesPdProductApi.Product>>(
+    '/mes/pd/product/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ浜у搧妗f璇︽儏 */
+export function getProduct(id: number) {
+  return requestClient.get<MesPdProductApi.Product>(
+    `/mes/pd/product/get?id=${id}`,
+  );
+}
+
+/** 鏂板浜у搧妗f */
+export function createProduct(data: MesPdProductApi.Product) {
+  return requestClient.post('/mes/pd/product/create', data);
+}
+
+/** 淇敼浜у搧妗f */
+export function updateProduct(data: MesPdProductApi.Product) {
+  return requestClient.put('/mes/pd/product/update', data);
+}
+
+/** 鍒犻櫎浜у搧妗f */
+export function deleteProduct(id: number) {
+  return requestClient.delete(`/mes/pd/product/delete?id=${id}`);
+}
+
+/** 瀵煎嚭浜у搧妗f 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}`);
+}
+
+/** 瀹℃壒浜у搧妗f锛堝鎵逛腑 鈫� 閫氳繃/椹冲洖锛� */
+export function auditProduct(data: MesPdProductApi.ProductAudit) {
+  return requestClient.put('/mes/pd/product/audit', data);
+}
diff --git a/src/views/bi/decision/alert-center/index.vue b/src/views/bi/decision/alert-center/index.vue
new file mode 100644
index 0000000..c47a01a
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/bi/decision/alert-rule/data.ts b/src/views/bi/decision/alert-rule/data.ts
new file mode 100644
index 0000000..4491cb2
--- /dev/null
+++ b/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 '璀﹀憡';
+}
diff --git a/src/views/bi/decision/alert-rule/index.vue b/src/views/bi/decision/alert-rule/index.vue
new file mode 100644
index 0000000..159d8a0
--- /dev/null
+++ b/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(`姝e湪鍒犻櫎銆�${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>
\ No newline at end of file
diff --git a/src/views/bi/decision/alert-rule/modules/form.vue b/src/views/bi/decision/alert-rule/modules/form.vue
new file mode 100644
index 0000000..3b220f5
--- /dev/null
+++ b/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>
diff --git a/src/views/bi/decision/forecast-analysis/index.vue b/src/views/bi/decision/forecast-analysis/index.vue
new file mode 100644
index 0000000..413733b
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/bi/decision/kpi-config/data.ts b/src/views/bi/decision/kpi-config/data.ts
new file mode 100644
index 0000000..c56b089
--- /dev/null
+++ b/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: '璇疯緭鍏PI鍚嶇О' },
+    },
+    {
+      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: '璇疯緭鍏PI缂栫爜锛堝敮涓�锛�' },
+      rules: 'required',
+    },
+    {
+      fieldName: 'name',
+      label: 'KPI鍚嶇О',
+      component: 'Input',
+      componentProps: { placeholder: '璇疯緭鍏PI鍚嶇О' },
+      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: '瀹夊叏',
+};
diff --git a/src/views/bi/decision/kpi-config/index.vue b/src/views/bi/decision/kpi-config/index.vue
new file mode 100644
index 0000000..d1dc6bc
--- /dev/null
+++ b/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(`姝e湪鍒犻櫎銆�${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>
\ No newline at end of file
diff --git a/src/views/bi/decision/kpi-config/modules/form.vue b/src/views/bi/decision/kpi-config/modules/form.vue
new file mode 100644
index 0000000..9bd8b57
--- /dev/null
+++ b/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>
diff --git a/src/views/bi/decision/kpi-dashboard/index.vue b/src/views/bi/decision/kpi-dashboard/index.vue
new file mode 100644
index 0000000..a41563a
--- /dev/null
+++ b/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 '姝e父';
+}
+
+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>
\ No newline at end of file
diff --git a/src/views/bi/decision/trend-analysis/index.vue b/src/views/bi/decision/trend-analysis/index.vue
new file mode 100644
index 0000000..7919f94
--- /dev/null
+++ b/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">姝e父</span>
+            </template>
+          </template>
+        </Table>
+      </Spin>
+    </div>
+  </Page>
+</template>
\ No newline at end of file
diff --git a/src/views/mes/dv/machinery/modules/telemetry-list.vue b/src/views/mes/dv/machinery/modules/telemetry-list.vue
new file mode 100644
index 0000000..da1de2c
--- /dev/null
+++ b/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 ? '寮傚父' : '姝e父' }}
+      </Tag>
+    </template>
+  </Grid>
+</template>
diff --git a/src/views/mes/dv/telemetry/data.ts b/src/views/mes/dv/telemetry/data.ts
new file mode 100644
index 0000000..417341d
--- /dev/null
+++ b/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: '姝e父', 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',
+    },
+  ];
+}
diff --git a/src/views/mes/dv/telemetry/index.vue b/src/views/mes/dv/telemetry/index.vue
new file mode 100644
index 0000000..69e3c82
--- /dev/null
+++ b/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 ? '寮傚父' : '姝e父' }}
+              </Tag>
+            </template>
+          </LatestGrid>
+        </Tabs.TabPane>
+        <Tabs.TabPane key="history" tab="鍘嗗彶璁板綍">
+          <HistoryGrid table-title="鍘嗗彶璁板綍">
+            <template #anomaly="{ row }">
+              <Tag :color="row.whetherAnomaly ? 'red' : 'green'">
+                {{ row.whetherAnomaly ? '寮傚父' : '姝e父' }}
+              </Tag>
+            </template>
+          </HistoryGrid>
+        </Tabs.TabPane>
+      </Tabs>
+    </div>
+  </Page>
+</template>
diff --git a/src/views/mes/pd/basedata/data.ts b/src/views/mes/pd/basedata/data.ts
new file mode 100644
index 0000000..c539b3c
--- /dev/null
+++ b/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: '妗d綅鍗曚环',
+      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: '璇疯緭鍏ユ湇鍔″椁怚D' },
+    },
+  ],
+  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: '妗d綅涓嬮檺(鍚�,kWh)',
+      component: 'InputNumber',
+      componentProps: { class: '!w-full', min: 0, precision: 2 },
+      rules: 'required',
+    },
+    {
+      fieldName: 'endValue',
+      label: '妗d綅涓婇檺(涓嶅惈,kWh)',
+      component: 'InputNumber',
+      componentProps: {
+        class: '!w-full',
+        min: 0,
+        precision: 2,
+        placeholder: '涓虹┖琛ㄧず涓婁笉灏侀《',
+      },
+    },
+    {
+      fieldName: 'pricePerUnit',
+      label: '妗d綅鍗曚环',
+      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,
+];
diff --git a/src/views/mes/pd/basedata/index.vue b/src/views/mes/pd/basedata/index.vue
new file mode 100644
index 0000000..8a15b50
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/pd/basedata/modules/base-table.vue b/src/views/mes/pd/basedata/modules/base-table.vue
new file mode 100644
index 0000000..17dec71
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/pd/basedata/modules/form.vue b/src/views/mes/pd/basedata/modules/form.vue
new file mode 100644
index 0000000..e133432
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/pd/basedata/modules/price-calc.vue b/src/views/mes/pd/basedata/modules/price-calc.vue
new file mode 100644
index 0000000..5b45dda
--- /dev/null
+++ b/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: '妗d綅涓嬮檺(鍚�,kWh)', dataIndex: 'startValue', key: 'startValue' },
+          { title: '妗d綅涓婇檺(涓嶅惈,kWh)', dataIndex: 'endValue', key: 'endValue' },
+          { title: '鏈。鐢甸噺(kWh)', dataIndex: 'tierQuantity', key: 'tierQuantity' },
+          { title: '妗d綅鍗曚环', 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>
diff --git a/src/views/mes/pd/product/data.ts b/src/views/mes/pd/product/data.ts
new file mode 100644
index 0000000..c1af453
--- /dev/null
+++ b/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;
+
+/** 鏂板/淇敼浜у搧妗f */
+export function useFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'archiveType',
+      label: '妗f绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PD_PRODUCT_ARCHIVE_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨妗f绫诲瀷',
+      },
+      rules: 'selectRequired',
+    },
+    {
+      fieldName: 'name',
+      label: '妗f鍚嶇О',
+      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: '妗f绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: getDictOptions(DICT_TYPE.MES_PD_PRODUCT_ARCHIVE_TYPE, 'number'),
+        placeholder: '璇烽�夋嫨妗f绫诲瀷',
+      },
+    },
+    {
+      fieldName: 'name',
+      label: '妗f鍚嶇О',
+      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: '妗f缂栫爜', width: 150 },
+    { field: 'name', title: '妗f鍚嶇О', minWidth: 160 },
+    {
+      field: 'archiveType',
+      title: '妗f绫诲瀷',
+      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',
+      },
+    },
+  ];
+}
diff --git a/src/views/mes/pd/product/index.vue b/src/views/mes/pd/product/index.vue
new file mode 100644
index 0000000..bc25d7c
--- /dev/null
+++ b/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: '浜у搧妗f.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="浜у搧妗f鍒楄〃">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['浜у搧妗f']),
+              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>
diff --git a/src/views/mes/pd/product/modules/audit.vue b/src/views/mes/pd/product/modules/audit.vue
new file mode 100644
index 0000000..99b8627
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/pd/product/modules/form.vue b/src/views/mes/pd/product/modules/form.vue
new file mode 100644
index 0000000..3575ac6
--- /dev/null
+++ b/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 '鏌ョ湅浜у搧妗f';
+    }
+    return formType.value === 'update' ? '淇敼浜у搧妗f' : '鏂板浜у搧妗f';
+  });
+
+  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>

--
Gitblit v1.9.3