| | |
| | | <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: '季节指数 SEASONAL', value: 'SEASONAL' }, |
| | | { label: '同比增长 YOY', value: 'YOY' }, |
| | | ]; |
| | | |
| | | const WORK_ORDER_TYPE_OPTIONS = [ |
| | |
| | | } |
| | | } |
| | | |
| | | 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({ |
| | |
| | | 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({ |
| | |
| | | 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> |
| | |
| | | <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 |
| | |
| | | 置信区间: [{{ 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> |
| | |
| | | </Spin> |
| | | </div> |
| | | </Page> |
| | | </template> |
| | | </template> |