Ver código fonte

[0018] 增加辐照度频率分布统计功能: DataController 新增日辐照度频率分布查询/导出接口(前端直接传stationId, 按日6区间统计条数与占比, endDay不含当天, 无数据日期补零, null辐照度跳过, W/㎡不做换算), 新增 selectIrradianceRange 左闭右开取数, 前端新增 statanalysis 页面与菜单SQL

taosj 1 semana atrás
pai
commit
0892e3eb7a

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

@@ -45,6 +45,8 @@ import com.ufo.project.data.domain.vo.PowerErrorStatCurveVO;
 import com.ufo.project.data.domain.vo.PowerErrorStatAccuracyVO;
 import com.ufo.project.data.domain.vo.PowerFrequencyDistributionQuery;
 import com.ufo.project.data.domain.vo.PowerFrequencyDistributionVO;
+import com.ufo.project.data.domain.vo.IrradianceFrequencyDistributionQuery;
+import com.ufo.project.data.domain.vo.IrradianceFrequencyDistributionVO;
 import com.ufo.project.data.service.IPvPlantCumulativeReportService;
 import com.ufo.project.data.service.IMediumTermPowerForecastReportService;
 import com.ufo.project.data.service.IShortTermPowerForecastReportService;
@@ -330,6 +332,69 @@ public class DataController extends BaseController
         util.exportExcel(response, list, "日功率频率分布统计");
     }
 
+    /**
+     * 日辐照度频率分布统计
+     */
+    @PreAuthorize("@ss.hasPermi('data:irradianceFrequencyDistribution:list')")
+    @PostMapping("/irradianceFrequencyDistribution")
+    public TableDataInfo irradianceFrequencyDistribution(@RequestBody IrradianceFrequencyDistributionQuery query)
+    {
+        List<IrradianceFrequencyDistributionVO> list = buildIrradianceDistributionList(
+                query.getStationId(), query.getStartDay(), query.getEndDay());
+        return getDataTable(list);
+    }
+
+    /**
+     * 日辐照度频率分布统计导出
+     */
+    @PreAuthorize("@ss.hasPermi('data:irradianceFrequencyDistribution:export')")
+    @Log(title = "辐照度频率分布统计", businessType = BusinessType.EXPORT)
+    @PostMapping("/irradianceFrequencyDistribution/export")
+    public void irradianceFrequencyDistributionExport(HttpServletResponse response, @RequestBody IrradianceFrequencyDistributionQuery query)
+    {
+        List<IrradianceFrequencyDistributionVO> list = buildIrradianceDistributionList(
+                query.getStationId(), query.getStartDay(), query.getEndDay());
+        ExcelUtil<IrradianceFrequencyDistributionVO> util = new ExcelUtil<>(IrradianceFrequencyDistributionVO.class);
+        util.exportExcel(response, list, "日辐照度频率分布统计");
+    }
+
+    /**
+     * 构建日辐照度频率分布统计列表(按日分组,无数据的日期补零)
+     * <p>取数区间左闭右开: tm &gt;= startDay 00:00(+08:00), tm &lt; endDay 00:00(+08:00)。</p>
+     * <p>辐照度单位为 W/m²,不做单位换算;tm 或辐照度为空的记录跳过。</p>
+     */
+    private List<IrradianceFrequencyDistributionVO> buildIrradianceDistributionList(Long stationId, String startDay, String endDay)
+    {
+        List<DataWeatherLocal> rows = stationId == null
+                ? new ArrayList<>()
+                : weatherLocalService.selectIrradianceRange(stationId, startDay, endDay);
+
+        LocalDate start = LocalDate.parse(startDay);
+        LocalDate end = LocalDate.parse(endDay);
+        LinkedHashMap<LocalDate, IrradianceFrequencyDistributionVO> dayMap = new LinkedHashMap<>();
+        for (LocalDate day = start; day.isBefore(end); day = day.plusDays(1))
+        {
+            dayMap.put(day, new IrradianceFrequencyDistributionVO(day.toString()));
+        }
+        if (rows != null)
+        {
+            for (DataWeatherLocal row : rows)
+            {
+                if (row.getTm() == null || row.getIrradiance() == null) continue;
+                LocalDate day = row.getTm().toInstant().atZone(ZoneId.of("Asia/Shanghai")).toLocalDate();
+                IrradianceFrequencyDistributionVO vo = dayMap.get(day);
+                if (vo == null) continue;
+                vo.bump(row.getIrradiance());
+            }
+        }
+        List<IrradianceFrequencyDistributionVO> result = new ArrayList<>(dayMap.values());
+        for (IrradianceFrequencyDistributionVO vo : result)
+        {
+            vo.buildPercentAndText();
+        }
+        return result;
+    }
+
     /**
      * 构建日功率频率分布统计列表(按日分组,无数据的日期补零)
      * <p>取数区间左闭右开: tm &gt;= startDay 00:00(+08:00), tm &lt; endDay 00:00(+08:00)。</p>

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

@@ -0,0 +1,55 @@
+package com.ufo.project.data.domain.vo;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 日辐照度频率分布统计查询参数
+ *
+ * @author ufo
+ * @date 2026-10-23
+ */
+public class IrradianceFrequencyDistributionQuery
+{
+    /** 场站ID(config_station_id) */
+    @NotNull(message = "场站ID不能为空")
+    private Long stationId;
+
+    /** 起始日期 yyyy-MM-dd(包含) */
+    @NotBlank(message = "起始日期不能为空")
+    private String startDay;
+
+    /** 结束日期 yyyy-MM-dd(不包含) */
+    @NotBlank(message = "结束日期不能为空")
+    private String endDay;
+
+    public Long getStationId()
+    {
+        return stationId;
+    }
+
+    public void setStationId(Long stationId)
+    {
+        this.stationId = stationId;
+    }
+
+    public String getStartDay()
+    {
+        return startDay;
+    }
+
+    public void setStartDay(String startDay)
+    {
+        this.startDay = startDay;
+    }
+
+    public String getEndDay()
+    {
+        return endDay;
+    }
+
+    public void setEndDay(String endDay)
+    {
+        this.endDay = endDay;
+    }
+}

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

@@ -0,0 +1,362 @@
+package com.ufo.project.data.domain.vo;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import com.ufo.base.framework.aspectj.lang.annotation.Excel;
+
+/**
+ * 日辐照度频率分布统计视图对象
+ * <p>每个区间包含条数(Num)、占比(Percent,%)与组合展示文本(Text,形如 18(29.51%),用于Excel导出列)。</p>
+ * <p>区间单位为 W/m²,直接取 data_weather_local.irradiance 总辐照度,不做单位换算。</p>
+ *
+ * @author ufo
+ * @date 2026-10-23
+ */
+public class IrradianceFrequencyDistributionVO implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 日期 yyyy-MM-dd */
+    @Excel(name = "时间")
+    private String day;
+
+    /** 数据总条数 */
+    @Excel(name = "数据总条数")
+    private int totalRecord;
+
+    /** [0,600)w/㎡ 条数 */
+    private int lt600Num;
+
+    /** [0,600)w/㎡ 占比(%) */
+    private BigDecimal lt600Percent;
+
+    /** [0,600)w/㎡ 组合展示文本 */
+    @Excel(name = "[0-600)w/㎡")
+    private String lt600Text;
+
+    /** [600,800)w/㎡ 条数 */
+    private int lt800Num;
+
+    /** [600,800)w/㎡ 占比(%) */
+    private BigDecimal lt800Percent;
+
+    /** [600,800)w/㎡ 组合展示文本 */
+    @Excel(name = "[600-800)w/㎡")
+    private String lt800Text;
+
+    /** [800,1000)w/㎡ 条数 */
+    private int lt1000Num;
+
+    /** [800,1000)w/㎡ 占比(%) */
+    private BigDecimal lt1000Percent;
+
+    /** [800,1000)w/㎡ 组合展示文本 */
+    @Excel(name = "[800-1000)w/㎡")
+    private String lt1000Text;
+
+    /** [1000,1200)w/㎡ 条数 */
+    private int lt1200Num;
+
+    /** [1000,1200)w/㎡ 占比(%) */
+    private BigDecimal lt1200Percent;
+
+    /** [1000,1200)w/㎡ 组合展示文本 */
+    @Excel(name = "[1000-1200)w/㎡")
+    private String lt1200Text;
+
+    /** [1200,1400)w/㎡ 条数 */
+    private int lt1400Num;
+
+    /** [1200,1400)w/㎡ 占比(%) */
+    private BigDecimal lt1400Percent;
+
+    /** [1200,1400)w/㎡ 组合展示文本 */
+    @Excel(name = "[1200-1400)w/㎡")
+    private String lt1400Text;
+
+    /** [1400,+∞)w/㎡ 条数 */
+    private int ge1400Num;
+
+    /** [1400,+∞)w/㎡ 占比(%) */
+    private BigDecimal ge1400Percent;
+
+    /** [1400,+∞)w/㎡ 组合展示文本 */
+    @Excel(name = "≥1400w/㎡")
+    private String ge1400Text;
+
+    public IrradianceFrequencyDistributionVO()
+    {
+    }
+
+    public IrradianceFrequencyDistributionVO(String day)
+    {
+        this.day = day;
+    }
+
+    /**
+     * 按辐照度值(W/m²)累加到对应区间
+     */
+    public void bump(double w)
+    {
+        totalRecord++;
+        if (w < 600)
+        {
+            lt600Num++;
+        }
+        else if (w < 800)
+        {
+            lt800Num++;
+        }
+        else if (w < 1000)
+        {
+            lt1000Num++;
+        }
+        else if (w < 1200)
+        {
+            lt1200Num++;
+        }
+        else if (w < 1400)
+        {
+            lt1400Num++;
+        }
+        else
+        {
+            ge1400Num++;
+        }
+    }
+
+    /**
+     * 计算各区间占比(保留两位小数,四舍五入)与组合展示文本 num(percent%)
+     */
+    public void buildPercentAndText()
+    {
+        lt600Percent = percentOf(lt600Num);
+        lt600Text = buildText(lt600Num, lt600Percent);
+        lt800Percent = percentOf(lt800Num);
+        lt800Text = buildText(lt800Num, lt800Percent);
+        lt1000Percent = percentOf(lt1000Num);
+        lt1000Text = buildText(lt1000Num, lt1000Percent);
+        lt1200Percent = percentOf(lt1200Num);
+        lt1200Text = buildText(lt1200Num, lt1200Percent);
+        lt1400Percent = percentOf(lt1400Num);
+        lt1400Text = buildText(lt1400Num, lt1400Percent);
+        ge1400Percent = percentOf(ge1400Num);
+        ge1400Text = buildText(ge1400Num, ge1400Percent);
+    }
+
+    private BigDecimal percentOf(int num)
+    {
+        if (totalRecord == 0)
+        {
+            return new BigDecimal("0.00");
+        }
+        return BigDecimal.valueOf((long) num * 100L)
+                .divide(BigDecimal.valueOf(totalRecord), 2, RoundingMode.HALF_UP);
+    }
+
+    private String buildText(int num, BigDecimal percent)
+    {
+        return num + "(" + percent.toPlainString() + "%)";
+    }
+
+    public String getDay()
+    {
+        return day;
+    }
+
+    public void setDay(String day)
+    {
+        this.day = day;
+    }
+
+    public int getTotalRecord()
+    {
+        return totalRecord;
+    }
+
+    public void setTotalRecord(int totalRecord)
+    {
+        this.totalRecord = totalRecord;
+    }
+
+    public int getLt600Num()
+    {
+        return lt600Num;
+    }
+
+    public void setLt600Num(int lt600Num)
+    {
+        this.lt600Num = lt600Num;
+    }
+
+    public BigDecimal getLt600Percent()
+    {
+        return lt600Percent;
+    }
+
+    public void setLt600Percent(BigDecimal lt600Percent)
+    {
+        this.lt600Percent = lt600Percent;
+    }
+
+    public String getLt600Text()
+    {
+        return lt600Text;
+    }
+
+    public void setLt600Text(String lt600Text)
+    {
+        this.lt600Text = lt600Text;
+    }
+
+    public int getLt800Num()
+    {
+        return lt800Num;
+    }
+
+    public void setLt800Num(int lt800Num)
+    {
+        this.lt800Num = lt800Num;
+    }
+
+    public BigDecimal getLt800Percent()
+    {
+        return lt800Percent;
+    }
+
+    public void setLt800Percent(BigDecimal lt800Percent)
+    {
+        this.lt800Percent = lt800Percent;
+    }
+
+    public String getLt800Text()
+    {
+        return lt800Text;
+    }
+
+    public void setLt800Text(String lt800Text)
+    {
+        this.lt800Text = lt800Text;
+    }
+
+    public int getLt1000Num()
+    {
+        return lt1000Num;
+    }
+
+    public void setLt1000Num(int lt1000Num)
+    {
+        this.lt1000Num = lt1000Num;
+    }
+
+    public BigDecimal getLt1000Percent()
+    {
+        return lt1000Percent;
+    }
+
+    public void setLt1000Percent(BigDecimal lt1000Percent)
+    {
+        this.lt1000Percent = lt1000Percent;
+    }
+
+    public String getLt1000Text()
+    {
+        return lt1000Text;
+    }
+
+    public void setLt1000Text(String lt1000Text)
+    {
+        this.lt1000Text = lt1000Text;
+    }
+
+    public int getLt1200Num()
+    {
+        return lt1200Num;
+    }
+
+    public void setLt1200Num(int lt1200Num)
+    {
+        this.lt1200Num = lt1200Num;
+    }
+
+    public BigDecimal getLt1200Percent()
+    {
+        return lt1200Percent;
+    }
+
+    public void setLt1200Percent(BigDecimal lt1200Percent)
+    {
+        this.lt1200Percent = lt1200Percent;
+    }
+
+    public String getLt1200Text()
+    {
+        return lt1200Text;
+    }
+
+    public void setLt1200Text(String lt1200Text)
+    {
+        this.lt1200Text = lt1200Text;
+    }
+
+    public int getLt1400Num()
+    {
+        return lt1400Num;
+    }
+
+    public void setLt1400Num(int lt1400Num)
+    {
+        this.lt1400Num = lt1400Num;
+    }
+
+    public BigDecimal getLt1400Percent()
+    {
+        return lt1400Percent;
+    }
+
+    public void setLt1400Percent(BigDecimal lt1400Percent)
+    {
+        this.lt1400Percent = lt1400Percent;
+    }
+
+    public String getLt1400Text()
+    {
+        return lt1400Text;
+    }
+
+    public void setLt1400Text(String lt1400Text)
+    {
+        this.lt1400Text = lt1400Text;
+    }
+
+    public int getGe1400Num()
+    {
+        return ge1400Num;
+    }
+
+    public void setGe1400Num(int ge1400Num)
+    {
+        this.ge1400Num = ge1400Num;
+    }
+
+    public BigDecimal getGe1400Percent()
+    {
+        return ge1400Percent;
+    }
+
+    public void setGe1400Percent(BigDecimal ge1400Percent)
+    {
+        this.ge1400Percent = ge1400Percent;
+    }
+
+    public String getGe1400Text()
+    {
+        return ge1400Text;
+    }
+
+    public void setGe1400Text(String ge1400Text)
+    {
+        this.ge1400Text = ge1400Text;
+    }
+}

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

@@ -86,4 +86,15 @@ public interface DataWeatherLocalMapper
      */
     public List<DataWeatherLocal> selectWeatherFieldsByTimeRange(@Param("configStationId") Long configStationId,
             @Param("startTime") String startTime, @Param("endTime") String endTime);
+
+    /**
+     * 按日期区间查询场站辐照度采样数据(左闭右开,结束日期不含当天)
+     *
+     * @param configStationId 场站ID
+     * @param startTime 起始时间(ISO 8601 字符串)
+     * @param endTime 结束时间(ISO 8601 字符串)
+     * @return 本地气象采样数据集合
+     */
+    public List<DataWeatherLocal> selectIrradianceRange(@Param("configStationId") Long configStationId,
+            @Param("startTime") String startTime, @Param("endTime") String endTime);
 }

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

@@ -114,6 +114,16 @@ public interface IDataWeatherLocalService
      */
     public List<DataWeatherLocal> selectForCumulativeIrradiance(String stationId, String startTime, String endTime);
 
+    /**
+     * 按日期区间查询场站辐照度采样数据(左闭右开,结束日期不含当天)
+     *
+     * @param configStationId 场站ID
+     * @param startDay 起始日期 yyyy-MM-dd(包含)
+     * @param endDay 结束日期 yyyy-MM-dd(不包含)
+     * @return 气象采样数据集合
+     */
+    public List<DataWeatherLocal> selectIrradianceRange(Long configStationId, String startDay, String endDay);
+
     /**
      * 新增本地气象采样数据
      *

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

@@ -307,6 +307,23 @@ public class DataWeatherLocalServiceImpl implements IDataWeatherLocalService
         return dataWeatherLocalMapper.selectWeatherFieldsByTimeRange(configStationId, startTime, endTime);
     }
 
+    /**
+     * 按日期区间查询场站辐照度采样数据(左闭右开)
+     * <p>tm &gt;= startDay 00:00:00(+08:00) and tm &lt; endDay 00:00:00(+08:00),即结束日期不含当天。</p>
+     *
+     * @param configStationId 场站ID
+     * @param startDay 起始日期 yyyy-MM-dd(包含)
+     * @param endDay 结束日期 yyyy-MM-dd(不包含)
+     * @return 气象采样数据集合
+     */
+    @Override
+    public List<DataWeatherLocal> selectIrradianceRange(Long configStationId, String startDay, String endDay)
+    {
+        String start = startDay + "T00:00:00+08:00";
+        String end = endDay + "T00:00:00+08:00";
+        return dataWeatherLocalMapper.selectIrradianceRange(configStationId, start, end);
+    }
+
     /**
      * 新增本地气象采样数据
      *

+ 9 - 0
forecast-backend-server/src/main/resources/mybatis/weather/DataWeatherLocalMapper.xml

@@ -62,6 +62,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         order by tm asc
     </select>
 
+    <!-- 按日期区间查询辐照度数据(左闭右开,结束日期不含当天) -->
+    <select id="selectIrradianceRange" resultMap="DataWeatherLocalResult">
+        <include refid="selectDataWeatherLocalVo"/>
+        where config_station_id = #{configStationId}
+          and tm &gt;= #{startTime}
+          and tm &lt; #{endTime}
+        order by tm asc
+    </select>
+
     <select id="selectDataWeatherLocalByTm" parameterType="Date" resultMap="DataWeatherLocalResult">
         <include refid="selectDataWeatherLocalVo"/>
         where tm = #{tm}

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

@@ -0,0 +1,21 @@
+import request from '@/utils/request'
+
+// 查询日辐照度频率分布统计
+export function listIrradianceFrequencyDistribution(data) {
+  return request({
+    url: '/data/irradianceFrequencyDistribution',
+    method: 'post',
+    data: data
+  })
+}
+
+// 导出日辐照度频率分布统计
+export function exportIrradianceFrequencyDistribution(data) {
+  return request({
+    url: '/data/irradianceFrequencyDistribution/export',
+    method: 'post',
+    data: data,
+    responseType: 'blob',
+    timeout: 120000
+  })
+}

+ 187 - 0
forecast-backend-vue/src/views/ufo/electricity/statanalysis/irradianceFrequencyDistribution.vue

@@ -0,0 +1,187 @@
+<template>
+  <div class="irradiance-frequency-distribution-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.startDay"
+            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.endDay"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+            placeholder="结束时间(不含当天)"
+            class="full-control"
+          />
+        </a-col>
+        <a-col :xs="24" :md="4">
+          <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" class="content-card">
+      <BusinessDataTable
+        :columns="columns"
+        :data-source="rows"
+        :pagination="{ pageSize: 10 }"
+        :row-key="'day'"
+        :scroll-x="'max-content'"
+      />
+    </a-card>
+  </div>
+</template>
+
+<script setup name="IrradianceFrequencyDistribution">
+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 {
+  exportIrradianceFrequencyDistribution,
+  listIrradianceFrequencyDistribution
+} from '@/api/data/irradianceFrequencyDistribution';
+import BusinessDataTable from '../components/BusinessDataTable.vue';
+import StationSelector from '../components/StationSelector.vue';
+import {normalizeRows} from '../components/dataAdapter';
+import {toStationOptions} from '../components/stationAdapter';
+
+const loading = ref(false);
+const exporting = ref(false);
+const stationOptions = ref([]);
+const rows = ref([]);
+
+const query = reactive({
+  stationId: undefined,
+  startDay: dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
+  endDay: dayjs().format('YYYY-MM-DD')
+});
+
+const queryValid = computed(() => {
+  if (!query.stationId || !query.startDay || !query.endDay) return false;
+  const start = dayjs(query.startDay);
+  const end = dayjs(query.endDay);
+  if (!start.isValid() || !end.isValid()) return false;
+  return !start.isAfter(end);
+});
+
+const renderBucket = (key) => ({record}) => {
+  const num = record[`${key}Num`] ?? 0;
+  const percent = record[`${key}Percent`] ?? '0.00';
+  return `${num}(${percent}%)`;
+};
+
+const bucketColumn = (key, title) => ({
+  title,
+  key: `${key}Text`,
+  width: 110,
+  align: 'right',
+  customRender: renderBucket(key)
+});
+
+const columns = [
+  {title: '时间', dataIndex: 'day', key: 'day', width: 120},
+  {title: '数据总条数', dataIndex: 'totalRecord', key: 'totalRecord', width: 110, align: 'right'},
+  bucketColumn('lt600', '[0-600)w/㎡'),
+  bucketColumn('lt800', '[600-800)w/㎡'),
+  bucketColumn('lt1000', '[800-1000)w/㎡'),
+  bucketColumn('lt1200', '[1000-1200)w/㎡'),
+  bucketColumn('lt1400', '[1200-1400)w/㎡'),
+  bucketColumn('ge1400', '≥1400w/㎡')
+];
+
+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 = () => ({
+  stationId: query.stationId,
+  startDay: query.startDay,
+  endDay: query.endDay
+});
+
+const fetchList = async () => {
+  const res = await listIrradianceFrequencyDistribution(buildParams());
+  rows.value = normalizeRows(res);
+};
+
+const handleQuery = async () => {
+  if (!query.stationId) {
+    message.warning('请先选择场站');
+    return;
+  }
+  if (!queryValid.value) return;
+
+  loading.value = true;
+  try {
+    await fetchList();
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleExport = async () => {
+  if (!query.stationId) {
+    message.warning('请先选择场站');
+    return;
+  }
+  if (!queryValid.value) return;
+
+  exporting.value = true;
+  try {
+    const res = await exportIrradianceFrequencyDistribution(buildParams());
+    const isFile = await blobValidate(res);
+    if (isFile) {
+      const name = `辐照度频率分布统计-${query.startDay}-${query.endDay}.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>
+.irradiance-frequency-distribution-page {
+  padding: 16px;
+}
+.full-control {
+  width: 100%;
+}
+.content-card {
+  margin-top: 16px;
+}
+</style>

+ 9 - 0
release_note/20261022/20261022_irradiance_frequency_distribution_menu.sql

@@ -0,0 +1,9 @@
+-- 辐照度频率分布统计菜单补丁(任务 2610018, 幂等, 可重复执行)
+-- 1. 增加 辐照度频率分布统计 二级菜单(挂在 统计分析 下)
+INSERT INTO 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)
+SELECT
+(SELECT COALESCE(MAX(menu_id), 0) + 1 FROM sys_menu),
+'辐照度频率分布统计',
+(SELECT menu_id FROM sys_menu t WHERE t.menu_name = '统计分析' AND t.parent_id = 0),
+22, 'irradiance-frequency-distribution', 'ufo/electricity/statanalysis/irradianceFrequencyDistribution', '', 'IrradianceFrequencyDistribution', 1, 0, 'C', '0', '0', 'data:irradianceFrequencyDistribution:list', 'SunOutlined', 'admin', now(), '', NULL, '辐照度频率分布统计页面'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu t WHERE t.menu_name = '辐照度频率分布统计');