2 天以前 dc1d067c566bbde1c7170186960a8bd27f210a47
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: '季节指数 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>