From dc1d067c566bbde1c7170186960a8bd27f210a47 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 09 九月 2026 13:45:58 +0800
Subject: [PATCH] feat(bi): 新增决策总览仪表盘并优化预测分析功能
---
src/views/bi/decision/forecast-analysis/index.vue | 157 ++++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 145 insertions(+), 12 deletions(-)
diff --git a/src/views/bi/decision/forecast-analysis/index.vue b/src/views/bi/decision/forecast-analysis/index.vue
index 404c042..20eeb4b 100644
--- a/src/views/bi/decision/forecast-analysis/index.vue
+++ b/src/views/bi/decision/forecast-analysis/index.vue
@@ -1,27 +1,41 @@
<script lang="ts" setup>
-import { onMounted, ref } from 'vue';
+import { computed, onMounted, ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
-import { DatePicker, message, Select, Spin } from 'ant-design-vue';
+import { Button, DatePicker, InputNumber, message, Select, Spin } from 'ant-design-vue';
import dayjs from 'dayjs';
import { useVbenForm } from '#/adapter/form';
import {
+ generateForecast,
generateForecastWorkOrder,
+ generateKpiForecast,
getForecastList,
} from '#/api/bi/decision/forecast';
+import type { DecisionForecastApi } from '#/api/bi/decision/forecast';
+import { getKpiOverview } from '#/api/bi/decision/kpi';
+import type { DecisionKpiApi } from '#/api/bi/decision/kpi';
defineOptions({ name: 'DecisionForecastAnalysis' });
const loading = ref(false);
-const forecastData = ref<any[]>([]);
+const generating = ref(false);
+const forecastData = ref<DecisionForecastApi.ForecastItem[]>([]);
const forecastCode = ref('load_forecast_daily');
const FORECAST_OPTIONS = [
{ label: '鏃ヨ礋鑽烽娴�', value: 'load_forecast_daily' },
{ label: '鍛ㄨ礋鑽烽娴�', value: 'load_forecast_weekly' },
{ label: '鏈堣礋鑽烽娴�', value: 'load_forecast_monthly' },
+];
+
+const MODEL_OPTIONS: Array<{ label: string; value: DecisionForecastApi.ForecastModel }> = [
+ { label: '绠�鍗曠Щ鍔ㄥ钩鍧� SMA', value: 'SMA' },
+ { label: '鍔犳潈绉诲姩骞冲潎 WMA', value: 'WMA' },
+ { label: '绾挎�у洖褰� LR', value: 'LR' },
+ { label: '瀛h妭鎸囨暟 SEASONAL', value: 'SEASONAL' },
+ { label: '鍚屾瘮澧為暱 YOY', value: 'YOY' },
];
const WORK_ORDER_TYPE_OPTIONS = [
@@ -41,7 +55,74 @@
}
}
-onMounted(loadData);
+/** ========== 鎸囧畾棰勬祴缂栫爜鐢熸垚 ========== */
+const model = ref<DecisionForecastApi.ForecastModel>('SMA');
+
+async function handleGenerateForecast() {
+ generating.value = true;
+ try {
+ await generateForecast(forecastCode.value, { model: model.value });
+ message.success(`宸茬敓鎴� ${forecastCode.value} 棰勬祴`);
+ await loadData();
+ } catch {
+ message.error('鐢熸垚澶辫触锛岃妫�鏌ラ娴嬮厤缃�');
+ } finally {
+ generating.value = false;
+ }
+}
+
+/** ========== 鍩轰簬 KPI 鍘嗗彶婊氬姩棰勬祴 ========== */
+const kpiList = ref<DecisionKpiApi.KpiItem[]>([]);
+const selectedKpi = ref<string>('');
+const kpiModel = ref<DecisionForecastApi.ForecastModel>('SMA');
+const forecastPeriods = ref(30);
+const intervalMinutes = ref(360);
+
+const kpiOptions = computed(() =>
+ kpiList.value.map((k) => ({
+ label: `${k.name}${k.unit ? `锛�${k.unit}锛塦 : ''}`,
+ value: k.code,
+ })),
+);
+
+async function loadKpis() {
+ try {
+ const overview = await getKpiOverview();
+ kpiList.value = overview.kpis || [];
+ if (!selectedKpi.value && kpiList.value.length > 0) {
+ selectedKpi.value = kpiList.value[0].code;
+ }
+ } catch {
+ kpiList.value = [];
+ }
+}
+
+async function handleGenerateKpiForecast() {
+ if (!selectedKpi.value) {
+ message.warning('璇峰厛閫夋嫨 KPI');
+ return;
+ }
+ generating.value = true;
+ try {
+ const count = await generateKpiForecast({
+ kpiCode: selectedKpi.value,
+ model: kpiModel.value,
+ periods: forecastPeriods.value,
+ intervalMinutes: intervalMinutes.value,
+ });
+ message.success(`宸插熀浜� KPI 鐢熸垚 ${count} 鏉¢娴媊);
+ forecastCode.value = selectedKpi.value;
+ await loadData();
+ } catch (e) {
+ const msg =
+ typeof e === 'string'
+ ? e
+ : (e as Error | undefined)?.message || '鏆傛棤鍘嗗彶鏁版嵁锛屾棤娉曢娴�';
+ message.error(msg);
+ } finally {
+ generating.value = false;
+ }
+}
/** ========== 鐢熸垚宸ュ崟 ========== */
const [Form, formApi] = useVbenForm({
@@ -148,7 +229,7 @@
if (!isOpen) {
return;
}
- const data = modalApi.getData<{ item: any }>();
+ const data = modalApi.getData<{ item: DecisionForecastApi.ForecastItem }>();
const item = data?.item;
await formApi.resetForm();
await formApi.setValues({
@@ -156,14 +237,18 @@
workOrderType: 4,
requestDate: item?.pointTime ?? dayjs().format('YYYY-MM-DD HH:mm:ss'),
quantity: item?.forecastValue ?? 1,
- remark: `鐢辫礋鑽烽娴嬶紙${item?.forecastName ?? forecastCode.value}锛夎嚜鍔ㄧ敓鎴恅,
+ remark: `鐢遍娴嬶紙${item?.forecastName ?? forecastCode.value}锛夎嚜鍔ㄧ敓鎴恅,
});
},
});
-function handleGenerate(item: any) {
+function handleGenerate(item: DecisionForecastApi.ForecastItem) {
modalApi.setData({ item }).open();
}
+
+onMounted(async () => {
+ await Promise.all([loadData(), loadKpis()]);
+});
</script>
<template>
@@ -171,20 +256,65 @@
<Modal title="璐熻嵎棰勬祴鐢熸垚鐢熶骇宸ュ崟" class="w-1/2">
<Form class="mx-3" />
</Modal>
- <div class="p-4">
- <div class="mb-4 flex items-center gap-4">
- <h2 class="text-lg font-bold">璐熻嵎棰勬祴</h2>
+ <div class="p-4 space-y-4">
+ <!-- 澶撮儴锛氶娴嬬紪鐮� + 妯″瀷 -->
+ <div class="flex flex-wrap items-center gap-4">
+ <h2 class="text-lg font-bold">棰勬祴鍒嗘瀽</h2>
<Select
v-model:value="forecastCode"
:options="FORECAST_OPTIONS"
style="width: 180px"
@change="loadData"
/>
+ <Select v-model:value="model" :options="MODEL_OPTIONS" style="width: 200px" />
+ <Button type="primary" :loading="generating" @click="handleGenerateForecast">
+ 鐢熸垚棰勬祴
+ </Button>
+ </div>
+
+ <!-- KPI 婊氬姩棰勬祴 -->
+ <div class="rounded-lg border border-gray-200 bg-white p-4">
+ <div class="mb-3 text-sm font-medium">鍩轰簬 KPI 鍘嗗彶婊氬姩棰勬祴</div>
+ <div class="grid grid-cols-2 gap-3 lg:grid-cols-5">
+ <Select
+ v-model:value="selectedKpi"
+ :options="kpiOptions"
+ style="width: 100%"
+ placeholder="閫夋嫨 KPI"
+ show-search
+ option-filter-prop="label"
+ />
+ <Select v-model:value="kpiModel" :options="MODEL_OPTIONS" style="width: 100%" />
+ <div>
+ <InputNumber
+ v-model:value="forecastPeriods"
+ class="!w-full"
+ :min="1"
+ :max="90"
+ placeholder="棰勬祴鏈熸暟"
+ />
+ </div>
+ <div>
+ <InputNumber
+ v-model:value="intervalMinutes"
+ class="!w-full"
+ :min="60"
+ :step="60"
+ placeholder="闂撮殧鍒嗛挓"
+ />
+ </div>
+ <Button :loading="generating" @click="handleGenerateKpiForecast">
+ 鐢熸垚 KPI 棰勬祴
+ </Button>
+ </div>
+ <div class="mt-2 text-xs text-gray-400">
+ 浣跨敤 KPI 鍘嗗彶蹇収鎸夋墍閫夋ā鍨嬫粴鍔ㄩ娴嬶紝棰勬祴鏈熸暟榛樿 30銆佷笂闄� 90锛涢棿闅斿垎閽熼粯璁� 360锛�6 灏忔椂锛夈��
+ </div>
</div>
<Spin :spinning="loading">
<div v-if="forecastData.length === 0" class="py-12 text-center text-gray-400">
- 鏆傛棤棰勬祴鏁版嵁锛岃鍏堥厤缃� KPI 鎸囨爣鍜岄璀﹁鍒欏悗鏌ョ湅
+ 鏆傛棤棰勬祴鏁版嵁锛岃閰嶇疆 KPI 鎸囨爣鍜岄璀﹁鍒欏悗鏌ョ湅
</div>
<div v-else class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<div
@@ -206,6 +336,9 @@
缃俊鍖洪棿: [{{ item.lowerBound }} ~ {{ item.upperBound }}]
</span>
</div>
+ <div v-if="item.modelVersion" class="mt-1 text-xs text-gray-400">
+ 妯″瀷: {{ item.modelVersion }}
+ </div>
<div v-if="item.dimension" class="mt-1 text-xs text-gray-400">
{{ item.dimension }}: {{ item.dimensionValue }}
</div>
@@ -224,4 +357,4 @@
</Spin>
</div>
</Page>
-</template>
+</template>
\ No newline at end of file
--
Gitblit v1.9.3