Browse Source

[0917] 超短期预测相关报表

taosj 2 weeks ago
parent
commit
9e11de5d35

+ 21 - 0
forecast-backend-vue/src/api/power/ultraShortTermForecastReport.js

@@ -0,0 +1,21 @@
+import request from '@/utils/request'
+
+// 查询超短期预测报表
+export function queryUltraShortTermForecastReport(query) {
+  return request({
+    url: '/data/ultraShortTermPowerForecastReport',
+    method: 'post',
+    data: query
+  })
+}
+
+// 导出超短期预测报表
+export function exportUltraShortTermForecastReport(data) {
+  return request({
+    url: '/data/ultraShortTermPowerForecastReport/export',
+    method: 'post',
+    data: data,
+    responseType: 'blob',
+    timeout: 120000
+  })
+}

+ 18 - 0
forecast-backend-vue/src/router/electricity.js

@@ -203,6 +203,24 @@ const electricityRoutes = [
         name: 'ShorTermForecastYearlyReport',
         component: () => import('@/views/ufo/datareport/shorTermForecastYearlyReport.vue'),
         meta: {title: '短期预测年报', icon: 'FileTextOutlined'}
+      },
+      {
+        path: 'datareport/ultra-shor-term-forecast-daily',
+        name: 'UltraShorTermForecastDailyReport',
+        component: () => import('@/views/ufo/datareport/ultraShortTermForecastDailyReport.vue'),
+        meta: {title: '超短期预测日报', icon: 'FileTextOutlined'}
+      },
+      {
+        path: 'datareport/ultra-shor-term-forecast-monthly',
+        name: 'UltraShorTermForecastMonthlyReport',
+        component: () => import('@/views/ufo/datareport/ultraShortTermForecastMonthlyReport.vue'),
+        meta: {title: '超短期预测月报', icon: 'FileTextOutlined'}
+      },
+      {
+        path: 'datareport/ultra-shor-term-forecast-yearly',
+        name: 'UltraShorTermForecastYearlyReport',
+        component: () => import('@/views/ufo/datareport/ultraShortTermForecastYearlyReport.vue'),
+        meta: {title: '超短期预测年报', icon: 'FileTextOutlined'}
       }
     ]
   }

+ 1 - 1
forecast-backend-vue/src/views/ufo/datareport/shorTermForecastDailyReport.vue

@@ -53,7 +53,7 @@ import { listStation } from '@/api/config/station';
 import { queryShortTermForecastReport, exportShortTermForecastReport } from '@/api/power/shortTermForecastReport';
 import BusinessDataTable from '../electricity/components/BusinessDataTable.vue';
 import StationSelector from '../electricity/components/StationSelector.vue';
-import { normalizeRows } from '../electricity/components/dataAdapter';
+import { normalizeRows, toStationOptions } from '../electricity/components/dataAdapter';
 
 const loading = ref(false);
 const exporting = ref(false);

+ 159 - 0
forecast-backend-vue/src/views/ufo/datareport/ultraShortTermForecastDailyReport.vue

@@ -0,0 +1,159 @@
+<template>
+  <div class="ultra-short-term-forecast-daily-page">
+    <a-card :bordered="false">
+      <a-row :gutter="[12, 12]" align="middle">
+        <a-col :xs="24" :md="6">
+          <StationSelector v-model:value="query.stationId" :options="stationOptions" :multiple="false" />
+        </a-col>
+        <a-col :xs="24" :md="5">
+          <a-date-picker
+            v-model:value="query.startTime"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+            placeholder="起始时间"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="5">
+          <a-date-picker
+            v-model:value="query.endTime"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+            placeholder="截止时间"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="8">
+          <a-space>
+            <a-button type="primary" :loading="loading" :disabled="!queryValid" @click="handleQuery">确认</a-button>
+            <a-button :loading="exporting" :disabled="!queryValid" @click="handleExport">导出</a-button>
+          </a-space>
+        </a-col>
+      </a-row>
+    </a-card>
+
+    <a-card title="数据详情" :bordered="false" style="margin-top: 16px">
+      <BusinessDataTable
+        :columns="tableColumns"
+        :data-source="tableRows"
+        :pagination="{ pageSize: 10 }"
+        :scroll-x="'max-content'"
+      />
+    </a-card>
+  </div>
+</template>
+
+<script setup name="ultraShortTermForecastDailyReport">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { message } from 'ant-design-vue';
+import dayjs from 'dayjs';
+import { saveAs } from 'file-saver';
+import { blobValidate } from '@/utils/bearjia';
+import { listStation } from '@/api/config/station';
+import { queryUltraShortTermForecastReport, exportUltraShortTermForecastReport } from '@/api/power/ultraShortTermForecastReport';
+import BusinessDataTable from '../electricity/components/BusinessDataTable.vue';
+import StationSelector from '../electricity/components/StationSelector.vue';
+import { normalizeRows, toStationOptions } from '../electricity/components/dataAdapter';
+
+const loading = ref(false);
+const exporting = ref(false);
+const stationOptions = ref([]);
+const tableRows = ref([]);
+
+const query = reactive({
+  stationId: undefined,
+  startTime: dayjs(),
+  endTime: dayjs(),
+  type: 'daily'
+})
+
+const tableColumns = [
+  { title: '时间', dataIndex: 'time', width: 120 },
+  { title: '开机容量(MW)', dataIndex: 'operatingCapacity', width: 130 },
+  { title: '实发功率(MW)', dataIndex: 'actualPower', width: 130 },
+  { title: '预测功率(MW)', dataIndex: 'forecastPower', width: 130 },
+  { title: '均方根差', dataIndex: 'rootMeanSquareError', width: 120 },
+  { title: '平均绝对误差', dataIndex: 'meanAbsoluteError', width: 130 },
+  { title: '相关性系数', dataIndex: 'correlationCoefficientR', width: 130 },
+  { title: '4小时前预测准确率(%)', dataIndex: 'aheadForecastAccuracy4h', width: 180 }
+]
+
+const queryValid = computed(() => {
+  if (!query.startTime || !query.endTime) return false
+  const start = dayjs(query.startTime)
+  const end = dayjs(query.endTime)
+  if (!start.isValid() || !end.isValid()) return false
+  return !end.isBefore(start)
+})
+
+const loadStationOptions = async () => {
+  const res = await listStation({ pageNum: 1, pageSize: 500 })
+  stationOptions.value = toStationOptions(normalizeRows(res))
+  if (!query.stationId && stationOptions.value.length) {
+    query.stationId = stationOptions.value[0].value
+  }
+}
+
+const buildParams = () => ({
+  type: query.type,
+  entityId: query.stationId,
+  startTime: dayjs(query.startTime).format('YYYY-MM-DD'),
+  endTime: dayjs(query.endTime).format('YYYY-MM-DD')
+})
+
+const handleQuery = async () => {
+  if (!query.stationId) {
+    message.warning('请先选择场站')
+    return
+  }
+  if (!queryValid.value) return
+
+  loading.value = true
+  try {
+    const res = await queryUltraShortTermForecastReport(buildParams())
+    tableRows.value = normalizeRows(res)
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleExport = async () => {
+  if (!query.stationId) {
+    message.warning('请先选择场站')
+    return
+  }
+  if (!queryValid.value) return
+
+  exporting.value = true
+  try {
+    const res = await exportUltraShortTermForecastReport(buildParams())
+    const isFile = await blobValidate(res)
+    if (isFile) {
+      const name = `超短期预测日报-${dayjs(query.startTime).format('YYYYMMDD')}-${dayjs(query.endTime).format('YYYYMMDD')}.xlsx`
+      saveAs(new Blob([res]), name)
+      message.success('导出成功')
+    } else {
+      const text = await res.text()
+      let msg = '导出失败'
+      try { msg = JSON.parse(text)?.msg || msg } catch (e) { /* ignore */ }
+      message.error(msg)
+    }
+  } finally {
+    exporting.value = false
+  }
+}
+
+onMounted(async () => {
+  await loadStationOptions();
+  await handleQuery();
+});
+</script>
+
+<style lang="less" scoped>
+.ultra-short-term-forecast-daily-page {
+  padding: 16px;
+}
+.full-control {
+  width: 100%;
+}
+</style>

+ 157 - 0
forecast-backend-vue/src/views/ufo/datareport/ultraShortTermForecastMonthlyReport.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="ultra-short-term-forecast-monthly-page">
+    <a-card :bordered="false">
+      <a-row :gutter="[12, 12]" align="middle">
+        <a-col :xs="24" :md="6">
+          <StationSelector v-model:value="query.stationId" :options="stationOptions" :multiple="false" />
+        </a-col>
+        <a-col :xs="24" :md="5">
+          <a-month-picker
+            v-model:value="query.startTime"
+            format="YYYY-MM"
+            placeholder="起始时间"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="5">
+          <a-month-picker
+            v-model:value="query.endTime"
+            format="YYYY-MM"
+            placeholder="截止时间"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="8">
+          <a-space>
+            <a-button type="primary" :loading="loading" :disabled="!queryValid" @click="handleQuery">确认</a-button>
+            <a-button :loading="exporting" :disabled="!queryValid" @click="handleExport">导出</a-button>
+          </a-space>
+        </a-col>
+      </a-row>
+    </a-card>
+
+    <a-card title="数据详情" :bordered="false" style="margin-top: 16px">
+      <BusinessDataTable
+        :columns="tableColumns"
+        :data-source="tableRows"
+        :pagination="{ pageSize: 10 }"
+        :scroll-x="'max-content'"
+      />
+    </a-card>
+  </div>
+</template>
+
+<script setup name="ultraShortTermForecastMonthlyReport">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { message } from 'ant-design-vue';
+import dayjs from 'dayjs';
+import { saveAs } from 'file-saver';
+import { blobValidate } from '@/utils/bearjia';
+import { listStation } from '@/api/config/station';
+import { queryUltraShortTermForecastReport, exportUltraShortTermForecastReport } from '@/api/power/ultraShortTermForecastReport';
+import BusinessDataTable from '../electricity/components/BusinessDataTable.vue';
+import StationSelector from '../electricity/components/StationSelector.vue';
+import { normalizeRows, toStationOptions } from '../electricity/components/dataAdapter';
+
+const loading = ref(false);
+const exporting = ref(false);
+const stationOptions = ref([]);
+const tableRows = ref([]);
+
+const query = reactive({
+  stationId: undefined,
+  startTime: dayjs().startOf('month'),
+  endTime: dayjs().startOf('month'),
+  type: 'monthly'
+})
+
+const tableColumns = [
+  { title: '时间', dataIndex: 'time', width: 120 },
+  { title: '开机容量(MW)', dataIndex: 'operatingCapacity', width: 130 },
+  { title: '实发功率(MW)', dataIndex: 'actualPower', width: 130 },
+  { title: '预测功率(MW)', dataIndex: 'forecastPower', width: 130 },
+  { title: '均方根差', dataIndex: 'rootMeanSquareError', width: 120 },
+  { title: '平均绝对误差', dataIndex: 'meanAbsoluteError', width: 130 },
+  { title: '相关性系数', dataIndex: 'correlationCoefficientR', width: 130 },
+  { title: '4小时前预测准确率(%)', dataIndex: 'aheadForecastAccuracy4h', width: 180 }
+]
+
+const queryValid = computed(() => {
+  if (!query.startTime || !query.endTime) return false
+  const start = dayjs(query.startTime)
+  const end = dayjs(query.endTime)
+  if (!start.isValid() || !end.isValid()) return false
+  return !end.isBefore(start)
+})
+
+const loadStationOptions = async () => {
+  const res = await listStation({ pageNum: 1, pageSize: 500 })
+  stationOptions.value = toStationOptions(normalizeRows(res))
+  if (!query.stationId && stationOptions.value.length) {
+    query.stationId = stationOptions.value[0].value
+  }
+}
+
+const buildParams = () => ({
+  type: query.type,
+  entityId: query.stationId,
+  startTime: dayjs(query.startTime).format('YYYY-MM') + '-01',
+  endTime: dayjs(query.endTime).endOf('month').format('YYYY-MM-DD HH:mm:ss')
+})
+
+const handleQuery = async () => {
+  if (!query.stationId) {
+    message.warning('请先选择场站')
+    return
+  }
+  if (!queryValid.value) return
+
+  loading.value = true
+  try {
+    const res = await queryUltraShortTermForecastReport(buildParams())
+    tableRows.value = normalizeRows(res)
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleExport = async () => {
+  if (!query.stationId) {
+    message.warning('请先选择场站')
+    return
+  }
+  if (!queryValid.value) return
+
+  exporting.value = true
+  try {
+    const res = await exportUltraShortTermForecastReport(buildParams())
+    const isFile = await blobValidate(res)
+    if (isFile) {
+      const name = `超短期预测月报-${dayjs(query.startTime).format('YYYYMM')}-${dayjs(query.endTime).format('YYYYMM')}.xlsx`
+      saveAs(new Blob([res]), name)
+      message.success('导出成功')
+    } else {
+      const text = await res.text()
+      let msg = '导出失败'
+      try { msg = JSON.parse(text)?.msg || msg } catch (e) { /* ignore */ }
+      message.error(msg)
+    }
+  } finally {
+    exporting.value = false
+  }
+}
+
+onMounted(async () => {
+  await loadStationOptions();
+  await handleQuery();
+});
+</script>
+
+<style lang="less" scoped>
+.ultra-short-term-forecast-monthly-page {
+  padding: 16px;
+}
+.full-control {
+  width: 100%;
+}
+</style>

+ 157 - 0
forecast-backend-vue/src/views/ufo/datareport/ultraShortTermForecastYearlyReport.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="ultra-short-term-forecast-yearly-page">
+    <a-card :bordered="false">
+      <a-row :gutter="[12, 12]" align="middle">
+        <a-col :xs="24" :md="6">
+          <StationSelector v-model:value="query.stationId" :options="stationOptions" :multiple="false" />
+        </a-col>
+        <a-col :xs="24" :md="5">
+          <a-year-picker
+            v-model:value="query.startTime"
+            format="YYYY"
+            placeholder="起始时间"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="5">
+          <a-year-picker
+            v-model:value="query.endTime"
+            format="YYYY"
+            placeholder="截止时间"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="8">
+          <a-space>
+            <a-button type="primary" :loading="loading" :disabled="!queryValid" @click="handleQuery">确认</a-button>
+            <a-button :loading="exporting" :disabled="!queryValid" @click="handleExport">导出</a-button>
+          </a-space>
+        </a-col>
+      </a-row>
+    </a-card>
+
+    <a-card title="数据详情" :bordered="false" style="margin-top: 16px">
+      <BusinessDataTable
+        :columns="tableColumns"
+        :data-source="tableRows"
+        :pagination="{ pageSize: 10 }"
+        :scroll-x="'max-content'"
+      />
+    </a-card>
+  </div>
+</template>
+
+<script setup name="ultraShortTermForecastYearlyReport">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { message } from 'ant-design-vue';
+import dayjs from 'dayjs';
+import { saveAs } from 'file-saver';
+import { blobValidate } from '@/utils/bearjia';
+import { listStation } from '@/api/config/station';
+import { queryUltraShortTermForecastReport, exportUltraShortTermForecastReport } from '@/api/power/ultraShortTermForecastReport';
+import BusinessDataTable from '../electricity/components/BusinessDataTable.vue';
+import StationSelector from '../electricity/components/StationSelector.vue';
+import { normalizeRows, toStationOptions } from '../electricity/components/dataAdapter';
+
+const loading = ref(false);
+const exporting = ref(false);
+const stationOptions = ref([]);
+const tableRows = ref([]);
+
+const query = reactive({
+  stationId: undefined,
+  startTime: dayjs().startOf('year'),
+  endTime: dayjs().startOf('year'),
+  type: 'yearly'
+})
+
+const tableColumns = [
+  { title: '时间', dataIndex: 'time', width: 120 },
+  { title: '开机容量(MW)', dataIndex: 'operatingCapacity', width: 130 },
+  { title: '实发功率(MW)', dataIndex: 'actualPower', width: 130 },
+  { title: '预测功率(MW)', dataIndex: 'forecastPower', width: 130 },
+  { title: '均方根差', dataIndex: 'rootMeanSquareError', width: 120 },
+  { title: '平均绝对误差', dataIndex: 'meanAbsoluteError', width: 130 },
+  { title: '相关性系数', dataIndex: 'correlationCoefficientR', width: 130 },
+  { title: '4小时前预测准确率(%)', dataIndex: 'aheadForecastAccuracy4h', width: 180 }
+]
+
+const queryValid = computed(() => {
+  if (!query.startTime || !query.endTime) return false
+  const start = dayjs(query.startTime)
+  const end = dayjs(query.endTime)
+  if (!start.isValid() || !end.isValid()) return false
+  return !end.isBefore(start)
+})
+
+const loadStationOptions = async () => {
+  const res = await listStation({ pageNum: 1, pageSize: 500 })
+  stationOptions.value = toStationOptions(normalizeRows(res))
+  if (!query.stationId && stationOptions.value.length) {
+    query.stationId = stationOptions.value[0].value
+  }
+}
+
+const buildParams = () => ({
+  type: query.type,
+  entityId: query.stationId,
+  startTime: dayjs(query.startTime).startOf('year').format('YYYY-MM-DD HH:mm:ss'),
+  endTime: dayjs(query.endTime).endOf('year').format('YYYY-MM-DD HH:mm:ss')
+})
+
+const handleQuery = async () => {
+  if (!query.stationId) {
+    message.warning('请先选择场站')
+    return
+  }
+  if (!queryValid.value) return
+
+  loading.value = true
+  try {
+    const res = await queryUltraShortTermForecastReport(buildParams())
+    tableRows.value = normalizeRows(res)
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleExport = async () => {
+  if (!query.stationId) {
+    message.warning('请先选择场站')
+    return
+  }
+  if (!queryValid.value) return
+
+  exporting.value = true
+  try {
+    const res = await exportUltraShortTermForecastReport(buildParams())
+    const isFile = await blobValidate(res)
+    if (isFile) {
+      const name = `超短期预测年报-${dayjs(query.startTime).format('YYYY')}-${dayjs(query.endTime).format('YYYY')}.xlsx`
+      saveAs(new Blob([res]), name)
+      message.success('导出成功')
+    } else {
+      const text = await res.text()
+      let msg = '导出失败'
+      try { msg = JSON.parse(text)?.msg || msg } catch (e) { /* ignore */ }
+      message.error(msg)
+    }
+  } finally {
+    exporting.value = false
+  }
+}
+
+onMounted(async () => {
+  await loadStationOptions();
+  await handleQuery();
+});
+</script>
+
+<style lang="less" scoped>
+.ultra-short-term-forecast-yearly-page {
+  padding: 16px;
+}
+.full-control {
+  width: 100%;
+}
+</style>

+ 12 - 0
release_note/2026.09.17/20260917_dp.sql

@@ -17,3 +17,15 @@ INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2087);
 INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2088);
 INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2089);
 INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2090);
+
+-- =============================================================================
+-- 任务编号 2026090806  增加"超短期预测报表"功能
+-- =============================================================================
+
+INSERT INTO "public"."sys_menu" ("menu_id", "menu_name", "parent_id", "order_num", "path", "component", "query", "route_name", "is_frame", "is_cache", "menu_type", "visible", "status", "perms", "icon", "create_by", "create_time", "update_by", "update_time", "remark") VALUES (2091, '超短期预测日报', 2016, 6, 'ultra-shor-term-forecast-daily', 'ufo/datareport/ultraShortTermForecastDailyReport', '', 'UltraShorTermForecastDailyReport', 1, 0, 'C', '0', '0', 'data:ultraShortTermForecastReport:daily', 'file-text', 'admin', '2026-09-14 00:00:00', '', NULL, '超短期预测日报页面');
+INSERT INTO "public"."sys_menu" ("menu_id", "menu_name", "parent_id", "order_num", "path", "component", "query", "route_name", "is_frame", "is_cache", "menu_type", "visible", "status", "perms", "icon", "create_by", "create_time", "update_by", "update_time", "remark") VALUES (2092, '超短期预测月报', 2016, 7, 'ultra-shor-term-forecast-monthly', 'ufo/datareport/ultraShortTermForecastMonthlyReport', '', 'UltraShorTermForecastMonthlyReport', 1, 0, 'C', '0', '0', 'data:ultraShortTermForecastReport:monthly', 'file-text', 'admin', '2026-09-14 00:00:00', '', NULL, '超短期预测月报页面');
+INSERT INTO "public"."sys_menu" ("menu_id", "menu_name", "parent_id", "order_num", "path", "component", "query", "route_name", "is_frame", "is_cache", "menu_type", "visible", "status", "perms", "icon", "create_by", "create_time", "update_by", "update_time", "remark") VALUES (2093, '超短期预测年报', 2016, 8, 'ultra-shor-term-forecast-yearly', 'ufo/datareport/ultraShortTermForecastYearlyReport', '', 'UltraShorTermForecastYearlyReport', 1, 0, 'C', '0', '0', 'data:ultraShortTermForecastReport:yearly', 'file-text', 'admin', '2026-09-14 00:00:00', '', NULL, '超短期预测年报页面');
+
+INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2091);
+INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2092);
+INSERT INTO sys_role_menu (role_id, menu_id) VALUES (1, 2093);