Pārlūkot izejas kodu

[0917] 短期预测相关报表;辐照度累计值报表接口

taosj 1 mēnesi atpakaļ
vecāks
revīzija
7c7380f6cd

+ 76 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/controller/DataController.java

@@ -20,8 +20,11 @@ import com.ufo.base.framework.aspectj.lang.annotation.Log;
 import com.ufo.base.framework.aspectj.lang.enums.BusinessType;
 import com.ufo.base.framework.web.controller.BaseController;
 import com.ufo.base.framework.web.page.TableDataInfo;
+import com.ufo.base.common.utils.DateUtils;
 import com.ufo.base.common.utils.poi.ExcelUtil;
+import com.ufo.project.data.domain.CumulativeIrradianceQuery;
 import com.ufo.project.data.domain.IrradiancePowerComparisonQuery;
+import com.ufo.project.data.domain.vo.CumulativeIrradianceVO;
 import com.ufo.project.data.domain.vo.IrradiancePowerComparisonVO;
 import com.ufo.project.data.domain.vo.ShortTermPowerForecastReportQuery;
 import com.ufo.project.data.domain.vo.ShortTermPowerForecastReportVO;
@@ -144,6 +147,70 @@ public class DataController extends BaseController
         util.exportExcel(response, list, "超短期预测报表");
     }
 
+    /**
+     * 辐照度累计值报表
+     */
+    @PreAuthorize("@ss.hasPermi('data:cumulativeIrradiance:list')")
+    @PostMapping("/cumulativeIrradiance")
+    public TableDataInfo cumulativeIrradiance(@RequestBody CumulativeIrradianceQuery query)
+    {
+        List<CumulativeIrradianceVO> list = buildCumulativeIrradianceList(
+                query.getEntityId(), query.getStartTime(), query.getEndTime());
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出辐照度累计值报表
+     */
+    @PreAuthorize("@ss.hasPermi('data:cumulativeIrradiance:export')")
+    @Log(title = "辐照度累计值报表", businessType = BusinessType.EXPORT)
+    @PostMapping("/cumulativeIrradiance/export")
+    public void cumulativeIrradianceExport(HttpServletResponse response,
+            @RequestBody CumulativeIrradianceQuery query)
+    {
+        List<CumulativeIrradianceVO> list = buildCumulativeIrradianceList(
+                query.getEntityId(), query.getStartTime(), query.getEndTime());
+        ExcelUtil<CumulativeIrradianceVO> util = new ExcelUtil<>(CumulativeIrradianceVO.class);
+        util.exportExcel(response, list, "辐照度累计值报表");
+    }
+
+    /**
+     * 构建辐照度累计值数据列表
+     */
+    private List<CumulativeIrradianceVO> buildCumulativeIrradianceList(String entityId, String startTime, String endTime)
+    {
+        if (entityId == null || startTime == null || endTime == null)
+        {
+            return Collections.emptyList();
+        }
+        List<DataWeatherLocal> rawData = weatherLocalService.selectForCumulativeIrradiance(
+                entityId, startTime, endTime + " 23:59:59");
+        if (rawData == null || rawData.isEmpty())
+        {
+            return Collections.emptyList();
+        }
+
+        List<CumulativeIrradianceVO> result = new ArrayList<>();
+        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+        for (DataWeatherLocal item : rawData)
+        {
+            CumulativeIrradianceVO vo = new CumulativeIrradianceVO();
+            if (item.getTm() != null)
+            {
+                vo.setTime(dateFormat.format(item.getTm()));
+            }
+            vo.setTotalIrradiation(toBigDecimal(item.getIrradiance()));
+            vo.setBoardTemperature(toBigDecimal(item.getBoardTemperature()));
+            vo.setWindSpeed(toBigDecimal(item.getWindSpeed()));
+            vo.setWindDirection(toBigDecimal(item.getWindDirection()));
+            vo.setTemperature(toBigDecimal(item.getTemperature()));
+            vo.setHumidity(toBigDecimal(item.getHumidity()));
+            vo.setPressure(toBigDecimal(item.getPressure()));
+            result.add(vo);
+        }
+        return result;
+    }
+
     /**
      * 构建对比数据列表
      */
@@ -297,4 +364,13 @@ public class DataController extends BaseController
                 .atOffset(ZoneOffset.ofHours(8))
                 .toString();
     }
+
+    /**
+     * 安全转换 Double 为 BigDecimal
+     */
+    private java.math.BigDecimal toBigDecimal(Double value)
+    {
+        if (value == null) return null;
+        return java.math.BigDecimal.valueOf(value);
+    }
 }

+ 46 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/domain/CumulativeIrradianceQuery.java

@@ -0,0 +1,46 @@
+package com.ufo.project.data.domain;
+
+import java.io.Serializable;
+
+/**
+ * 辐照度累计值报表查询参数
+ *
+ * @author ufo
+ * @date 2026-09-14
+ */
+public class CumulativeIrradianceQuery implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 场站/实体 ID */
+    private String entityId;
+
+    /** 开始时间(格式:yyyy-MM-dd) */
+    private String startTime;
+
+    /** 结束时间(格式:yyyy-MM-dd) */
+    private String endTime;
+
+    public String getEntityId() {
+        return entityId;
+    }
+
+    public void setEntityId(String entityId) {
+        this.entityId = entityId;
+    }
+
+    public String getStartTime() {
+        return startTime;
+    }
+
+    public void setStartTime(String startTime) {
+        this.startTime = startTime;
+    }
+
+    public String getEndTime() {
+        return endTime;
+    }
+
+    public void setEndTime(String endTime) {
+        this.endTime = endTime;
+    }
+}

+ 112 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/domain/vo/CumulativeIrradianceVO.java

@@ -0,0 +1,112 @@
+package com.ufo.project.data.domain.vo;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+import com.ufo.base.framework.aspectj.lang.annotation.Excel;
+
+/**
+ * 辐照度累计值报表视图对象
+ *
+ * @author ufo
+ * @date 2026-09-14
+ */
+public class CumulativeIrradianceVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 时间 */
+    @Excel(name = "时间")
+    private String time;
+
+    /** 总辐照度 W/㎡ */
+    @Excel(name = "总辐照度(W/㎡)")
+    private BigDecimal totalIrradiation;
+
+    /** 组件温度(℃) */
+    @Excel(name = "组件温度(℃)")
+    private BigDecimal boardTemperature;
+
+    /** 风速(m/s) */
+    @Excel(name = "风速(m/s)")
+    private BigDecimal windSpeed;
+
+    /** 风向(度) */
+    @Excel(name = "风向(度)")
+    private BigDecimal windDirection;
+
+    /** 温度(℃) */
+    @Excel(name = "温度(℃)")
+    private BigDecimal temperature;
+
+    /** 湿度(%RH) */
+    @Excel(name = "湿度(%RH)")
+    private BigDecimal humidity;
+
+    /** 气压(hPa) */
+    @Excel(name = "气压(hPa)")
+    private BigDecimal pressure;
+
+    public String getTime() {
+        return time;
+    }
+
+    public void setTime(String time) {
+        this.time = time;
+    }
+
+    public BigDecimal getTotalIrradiation() {
+        return totalIrradiation;
+    }
+
+    public void setTotalIrradiation(BigDecimal totalIrradiation) {
+        this.totalIrradiation = totalIrradiation;
+    }
+
+    public BigDecimal getBoardTemperature() {
+        return boardTemperature;
+    }
+
+    public void setBoardTemperature(BigDecimal boardTemperature) {
+        this.boardTemperature = boardTemperature;
+    }
+
+    public BigDecimal getWindSpeed() {
+        return windSpeed;
+    }
+
+    public void setWindSpeed(BigDecimal windSpeed) {
+        this.windSpeed = windSpeed;
+    }
+
+    public BigDecimal getWindDirection() {
+        return windDirection;
+    }
+
+    public void setWindDirection(BigDecimal windDirection) {
+        this.windDirection = windDirection;
+    }
+
+    public BigDecimal getTemperature() {
+        return temperature;
+    }
+
+    public void setTemperature(BigDecimal temperature) {
+        this.temperature = temperature;
+    }
+
+    public BigDecimal getHumidity() {
+        return humidity;
+    }
+
+    public void setHumidity(BigDecimal humidity) {
+        this.humidity = humidity;
+    }
+
+    public BigDecimal getPressure() {
+        return pressure;
+    }
+
+    public void setPressure(BigDecimal pressure) {
+        this.pressure = pressure;
+    }
+}

+ 15 - 1
forecast-backend-server/src/main/java/com/ufo/project/weather/domain/DataWeatherLocal.java

@@ -34,6 +34,10 @@ public class DataWeatherLocal extends BaseEntity
     @Excel(name = "斜面辐照度,单位 W/m²")
     private Double irradiance;
 
+    /** 组件温度,单位 ℃ */
+    @Excel(name = "组件温度,单位 ℃")
+    private Double boardTemperature;
+
     /** 温度,单位 ℃ */
     @Excel(name = "温度,单位 ℃")
     private Double temperature;
@@ -86,10 +90,19 @@ public class DataWeatherLocal extends BaseEntity
         this.irradiance = irradiance;
     }
 
-    public Double getIrradiance() 
+    public Double getIrradiance()
     {
         return irradiance;
     }
+    public void setBoardTemperature(Double boardTemperature)
+    {
+        this.boardTemperature = boardTemperature;
+    }
+
+    public Double getBoardTemperature()
+    {
+        return boardTemperature;
+    }
     public void setTemperature(Double temperature) 
     {
         this.temperature = temperature;
@@ -143,6 +156,7 @@ public class DataWeatherLocal extends BaseEntity
             .append("deviceId", getConfigStationId())
             .append("horizontalIrradiance", getHorizontalIrradiance())
             .append("irradiance", getIrradiance())
+            .append("boardTemperature", getBoardTemperature())
             .append("temperature", getTemperature())
             .append("pressure", getPressure())
             .append("humidity", getHumidity())

+ 1 - 1
forecast-backend-server/src/main/java/com/ufo/project/weather/mapper/DataWeatherLocalMapper.java

@@ -70,7 +70,7 @@ public interface DataWeatherLocalMapper
 
     /**
      * 批量删除本地气象采样数据
-     * 
+     *
      * @param tms 需要删除的数据主键集合
      * @return 结果
      */

+ 10 - 0
forecast-backend-server/src/main/java/com/ufo/project/weather/service/IDataWeatherLocalService.java

@@ -94,6 +94,16 @@ public interface IDataWeatherLocalService
      */
     public List<DataWeatherLocal> selectIrradianceByTimeRange(Long stationId, Long startTime, Long endTime);
 
+    /**
+     * 查询场站指定时间范围内的气象采样数据(返回原始数据列表,按 tm 升序,用于辐照度累计值报表)
+     *
+     * @param stationId 场站ID
+     * @param startTime 起始时间(格式:yyyy-MM-dd)
+     * @param endTime 结束时间(格式:yyyy-MM-dd)
+     * @return 气象采样数据集合
+     */
+    public List<DataWeatherLocal> selectForCumulativeIrradiance(String stationId, String startTime, String endTime);
+
     /**
      * 新增本地气象采样数据
      * 

+ 34 - 0
forecast-backend-server/src/main/java/com/ufo/project/weather/service/impl/DataWeatherLocalServiceImpl.java

@@ -211,6 +211,40 @@ public class DataWeatherLocalServiceImpl implements IDataWeatherLocalService
         return dataWeatherLocalMapper.selectDataWeatherLocalListByTimeRange(stationId, startTime, endTime);
     }
 
+    /**
+     * 查询场站指定时间范围内的气象采样数据(用于辐照度累计值报表)
+     *
+     * @param stationId 场站ID
+     * @param startTime 起始时间(格式:yyyy-MM-dd)
+     * @param endTime 结束时间(格式:yyyy-MM-dd)
+     * @return 气象采样数据集合
+     */
+    @Override
+    public List<DataWeatherLocal> selectForCumulativeIrradiance(String stationId, String startTime, String endTime)
+    {
+        Long startTs = parseDateToTimestamp(startTime);
+        Long endTs = parseDateToTimestamp(endTime + " 23:59:59");
+        return dataWeatherLocalMapper.selectDataWeatherLocalListByTimeRange(
+                Long.valueOf(stationId), startTs, endTs);
+    }
+
+    /**
+     * 将日期字符串转为 unix 秒级时间戳
+     */
+    private Long parseDateToTimestamp(String dateStr)
+    {
+        try
+        {
+            java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            Date d = sdf.parse(dateStr.trim());
+            return d.getTime() / 1000;
+        }
+        catch (Exception e)
+        {
+            return null;
+        }
+    }
+
     /**
      * 通用曲线数据构建:查询、清洗并格式化时间
      * 过滤取值为空、时间为空的记录

+ 5 - 1
forecast-backend-server/src/main/resources/mybatis/weather/DataWeatherLocalMapper.xml

@@ -9,6 +9,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="configStationId"    column="config_station_id"    />
         <result property="horizontalIrradiance"    column="horizontal_irradiance"    />
         <result property="irradiance"    column="irradiance"    />
+        <result property="boardTemperature"    column="board_temperature"    />
         <result property="temperature"    column="temperature"    />
         <result property="pressure"    column="pressure"    />
         <result property="humidity"    column="humidity"    />
@@ -17,7 +18,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectDataWeatherLocalVo">
-        select tm, config_station_id, horizontal_irradiance, irradiance, temperature, pressure, humidity, wind_speed, wind_direction from data_weather_local
+        select tm, config_station_id, horizontal_irradiance, irradiance, board_temperature, temperature, pressure, humidity, wind_speed, wind_direction from data_weather_local
     </sql>
 
     <select id="selectDataWeatherLocalList" parameterType="DataWeatherLocal" resultMap="DataWeatherLocalResult">
@@ -61,6 +62,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="configStationId != null">config_station_id,</if>
             <if test="horizontalIrradiance != null">horizontal_irradiance,</if>
             <if test="irradiance != null">irradiance,</if>
+            <if test="boardTemperature != null">board_temperature,</if>
             <if test="temperature != null">temperature,</if>
             <if test="pressure != null">pressure,</if>
             <if test="humidity != null">humidity,</if>
@@ -72,6 +74,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="configStationId != null">#{configStationId},</if>
             <if test="horizontalIrradiance != null">#{horizontalIrradiance},</if>
             <if test="irradiance != null">#{irradiance},</if>
+            <if test="boardTemperature != null">#{boardTemperature},</if>
             <if test="temperature != null">#{temperature},</if>
             <if test="pressure != null">#{pressure},</if>
             <if test="humidity != null">#{humidity},</if>
@@ -86,6 +89,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="configStationId != null">config_station_id = #{configStationId},</if>
             <if test="horizontalIrradiance != null">horizontal_irradiance = #{horizontalIrradiance},</if>
             <if test="irradiance != null">irradiance = #{irradiance},</if>
+            <if test="boardTemperature != null">board_temperature = #{boardTemperature},</if>
             <if test="temperature != null">temperature = #{temperature},</if>
             <if test="pressure != null">pressure = #{pressure},</if>
             <if test="humidity != null">humidity = #{humidity},</if>

+ 21 - 0
forecast-backend-vue/src/api/data/shortTermForecastReport.js

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

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

@@ -185,6 +185,24 @@ const electricityRoutes = [
         name: 'ElectricityFaultProcessLog',
         component: () => import('@/views/ufo/electricity/fault/FaultProcessLog.vue'),
         meta: {title: '故障处理日志', icon: 'ProfileOutlined'}
+      },
+      {
+        path: 'datareport/shor-term-forecast-daily',
+        name: 'ShorTermForecastDailyReport',
+        component: () => import('@/views/ufo/datareport/shorTermForecastDailyReport.vue'),
+        meta: {title: '短期预测日报', icon: 'FileTextOutlined'}
+      },
+      {
+        path: 'datareport/shor-term-forecast-monthly',
+        name: 'ShorTermForecastMonthlyReport',
+        component: () => import('@/views/ufo/datareport/shorTermForecastMonthlyReport.vue'),
+        meta: {title: '短期预测月报', icon: 'FileTextOutlined'}
+      },
+      {
+        path: 'datareport/shor-term-forecast-yearly',
+        name: 'ShorTermForecastYearlyReport',
+        component: () => import('@/views/ufo/datareport/shorTermForecastYearlyReport.vue'),
+        meta: {title: '短期预测年报', icon: 'FileTextOutlined'}
       }
     ]
   }

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

@@ -0,0 +1,157 @@
+<template>
+  <div class="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"
+            picker="day"
+            format="YYYY-MM-DD"
+            placeholder="开始时间"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="5">
+          <a-date-picker
+            v-model:value="query.endTime"
+            picker="day"
+            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 :bordered="false">
+      <a-table
+        :columns="columns"
+        :data-source="tableRows"
+        :loading="loading"
+        :pagination="{ pageSize: 10 }"
+        row-key="time"
+        size="middle"
+      />
+    </a-card>
+  </div>
+</template>
+
+<script setup name="ShorTermForecastDailyReport">
+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 { listShortTermForecastReport, exportShortTermForecastReport } from '@/api/data/shortTermForecastReport'
+import StationSelector from '../electricity/components/StationSelector.vue'
+import { normalizeRows } from '../electricity/components/dataAdapter'
+import { toStationOptions } from '../electricity/components/stationAdapter'
+
+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 columns = [
+  { 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: '最大误差合格率(%)', dataIndex: 'maximumErrorPassRate', width: 150 }
+]
+
+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 listShortTermForecastReport(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 exportShortTermForecastReport(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>
+.short-term-forecast-daily-page { padding: 16px; }
+.full-control { width: 100%; }
+</style>

+ 153 - 0
forecast-backend-vue/src/views/ufo/datareport/shorTermForecastMonthlyReport.vue

@@ -0,0 +1,153 @@
+<template>
+  <div class="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"
+            placeholder="开始时间"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="5">
+          <a-month-picker
+            v-model:value="query.endTime"
+            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 :bordered="false">
+      <a-table
+        :columns="columns"
+        :data-source="tableRows"
+        :loading="loading"
+        :pagination="{ pageSize: 10 }"
+        row-key="time"
+        size="middle"
+      />
+    </a-card>
+  </div>
+</template>
+
+<script setup name="ShorTermForecastMonthlyReport">
+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 { listShortTermForecastReport, exportShortTermForecastReport } from '@/api/data/shortTermForecastReport'
+import StationSelector from '../electricity/components/StationSelector.vue'
+import { normalizeRows } from '../electricity/components/dataAdapter'
+import { toStationOptions } from '../electricity/components/stationAdapter'
+
+const loading = ref(false)
+const exporting = ref(false)
+const stationOptions = ref([])
+const tableRows = ref([])
+
+const query = reactive({
+  stationId: undefined,
+  startTime: dayjs(),
+  endTime: dayjs(),
+  type: 'monthly'
+})
+
+const columns = [
+  { 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: '最大误差合格率(%)', dataIndex: 'maximumErrorPassRate', width: 150 }
+]
+
+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 listShortTermForecastReport(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 exportShortTermForecastReport(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>
+.short-term-forecast-monthly-page { padding: 16px; }
+.full-control { width: 100%; }
+</style>

+ 153 - 0
forecast-backend-vue/src/views/ufo/datareport/shorTermForecastYearlyReport.vue

@@ -0,0 +1,153 @@
+<template>
+  <div class="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"
+            placeholder="开始时间"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="5">
+          <a-year-picker
+            v-model:value="query.endTime"
+            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 :bordered="false">
+      <a-table
+        :columns="columns"
+        :data-source="tableRows"
+        :loading="loading"
+        :pagination="{ pageSize: 10 }"
+        row-key="time"
+        size="middle"
+      />
+    </a-card>
+  </div>
+</template>
+
+<script setup name="ShorTermForecastYearlyReport">
+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 { listShortTermForecastReport, exportShortTermForecastReport } from '@/api/data/shortTermForecastReport'
+import StationSelector from '../electricity/components/StationSelector.vue'
+import { normalizeRows } from '../electricity/components/dataAdapter'
+import { toStationOptions } from '../electricity/components/stationAdapter'
+
+const loading = ref(false)
+const exporting = ref(false)
+const stationOptions = ref([])
+const tableRows = ref([])
+
+const query = reactive({
+  stationId: undefined,
+  startTime: dayjs(),
+  endTime: dayjs(),
+  type: 'yearly'
+})
+
+const columns = [
+  { 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: '最大误差合格率(%)', dataIndex: 'maximumErrorPassRate', width: 150 }
+]
+
+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 listShortTermForecastReport(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 exportShortTermForecastReport(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>
+.short-term-forecast-yearly-page { padding: 16px; }
+.full-control { width: 100%; }
+</style>

+ 6 - 1
release_note/2026.09.17/20260917_dp.sql

@@ -3,4 +3,9 @@ UPDATE "public"."sys_menu" set status = 1 where menu_name in ('多源多模型
 
 -- data_weather_local 斜面辐照度字段 -> 总辐照度
 ALTER TABLE "public"."data_weather_local" RENAME COLUMN "inclined_irradiance" TO "irradiance";
-COMMENT ON COLUMN "public"."data_weather_local"."irradiance" IS '总辐照度,单位 W/m²';
+COMMENT ON COLUMN "public"."data_weather_local"."irradiance" IS '总辐照度,单位 W/m²';
+
+-- 短期预测报表菜单
+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 (2071, '短期预测日报', 2016, 3, 'shor-term-forecast-daily', 'ufo/datareport/shorTermForecastDailyReport', '', 'ShorTermForecastDailyReport', 1, 0, 'C', '0', '0', 'data:shortTermForecastReport: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 (2072, '短期预测月报', 2016, 4, 'shor-term-forecast-monthly', 'ufo/datareport/shorTermForecastMonthlyReport', '', 'ShorTermForecastMonthlyReport', 1, 0, 'C', '0', '0', 'data:shortTermForecastReport: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 (2073, '短期预测年报', 2016, 5, 'shor-term-forecast-yearly', 'ufo/datareport/shorTermForecastYearlyReport', '', 'ShorTermForecastYearlyReport', 1, 0, 'C', '0', '0', 'data:shortTermForecastReport:yearly', 'file-text', 'admin', '2026-09-14 00:00:00', '', NULL, '短期预测年报页面');