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