Просмотр исходного кода

[101904] 功率上报页面: 批次查询/查看曲线改数/确认, 含菜单 SQL

taosj 1 неделя назад
Родитель
Сommit
301ad48ca4

+ 63 - 0
forecast-backend-vue/src/api/data/powerForecastReport.js

@@ -0,0 +1,63 @@
+import request from '@/utils/request'
+
+// 查询功率预测上报批次列表(分页)
+export function listPowerForecastReportBatch(data) {
+  return request({
+    url: '/power/forecastReportBatch/list',
+    method: 'post',
+    data
+  })
+}
+
+// 确认功率预测上报批次
+export function confirmPowerForecastReportBatch(batchNo) {
+  return request({
+    url: '/power/forecastReportBatch/confirm/' + batchNo,
+    method: 'post'
+  })
+}
+
+// 新增功率预测上报批次
+export function createPowerForecastReportBatch(data) {
+  return request({
+    url: '/power/forecastReportBatch/create',
+    method: 'post',
+    data
+  })
+}
+
+// 查询短期功率上报数据列表
+export function listShortTermPowerForecastReport(query) {
+  return request({
+    url: '/power/shortTermPowerForecastReport/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 修改短期功率上报数据
+export function updateShortTermPowerForecastReport(data) {
+  return request({
+    url: '/power/shortTermPowerForecastReport',
+    method: 'put',
+    data
+  })
+}
+
+// 查询超短期功率上报数据列表
+export function listSuperShortTermPowerForecastReport(query) {
+  return request({
+    url: '/supershort/superShortTermForecastReport/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 修改超短期功率上报数据
+export function updateSuperShortTermPowerForecastReport(data) {
+  return request({
+    url: '/supershort/superShortTermForecastReport',
+    method: 'put',
+    data
+  })
+}

+ 16 - 2
forecast-backend-vue/src/views/ufo/electricity/components/PowerLineChart.vue

@@ -20,9 +20,12 @@ const props = defineProps({
   series: {type: Array, default: () => demoSeries},
   yAxisName: {type: String, default: 'MW'},
   legendSelected: {type: Object, default: () => null},
-  legendWidth: {type: [String, Number], default: null}
+  legendWidth: {type: [String, Number], default: null},
+  pointClickable: {type: Boolean, default: false}
 });
 
+const emit = defineEmits(['point-click']);
+
 const chartRef = ref();
 let chartInstance = null;
 
@@ -57,7 +60,8 @@ const chartOption = computed(() => {
       name: item.name,
       type: 'line',
       smooth: true,
-      showSymbol: false,
+      showSymbol: props.pointClickable,
+      symbolSize: props.pointClickable ? 6 : undefined,
       data: item.data
     }))
   };
@@ -68,6 +72,16 @@ const renderChart = async () => {
   if (!chartRef.value) return;
   if (!chartInstance) {
     chartInstance = echarts.init(chartRef.value);
+    chartInstance.on('click', (params) => {
+      if (!props.pointClickable || params.componentType !== 'series') return;
+      emit('point-click', {
+        dataIndex: params.dataIndex,
+        seriesIndex: params.seriesIndex,
+        seriesName: params.seriesName,
+        name: params.name,
+        value: params.value
+      });
+    });
   }
   chartInstance.setOption(chartOption.value, true);
 };

+ 360 - 0
forecast-backend-vue/src/views/ufo/electricity/forecast/PowerForecastReport.vue

@@ -0,0 +1,360 @@
+<template>
+  <div class="power-forecast-report-page">
+    <a-card :bordered="false">
+      <a-row :gutter="[12, 12]" align="middle">
+        <a-col :xs="24" :md="6">
+          <StationSelector v-model:value="query.stationCode" :options="stationOptions" :multiple="false" />
+        </a-col>
+        <a-col :xs="24" :md="4">
+          <a-select v-model:value="query.type" :options="typeOptions" class="full-control" />
+        </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="4">
+          <a-button type="primary" :loading="loading" :disabled="!queryValid" @click="handleQuery">查询</a-button>
+        </a-col>
+      </a-row>
+    </a-card>
+
+    <a-card :bordered="false" class="content-card">
+      <template v-if="viewMode === 'table'">
+        <a-table
+          :columns="columns"
+          :data-source="batchRows"
+          :loading="loading"
+          :row-key="(record) => record.id"
+          :pagination="pagination"
+          :locale="{emptyText: '暂无数据'}"
+          :scroll="{x: 'max-content'}"
+          size="middle"
+          bordered
+          @change="handleTableChange"
+        >
+          <template #bodyCell="{column, record}">
+            <template v-if="column.dataIndex === 'file'">
+              -
+            </template>
+            <template v-else-if="column.dataIndex === 'channel'">
+              -
+            </template>
+            <template v-else-if="column.dataIndex === 'status'">
+              <a-tag :color="statusMap[record.status]?.color">{{ statusMap[record.status]?.text || '-' }}</a-tag>
+            </template>
+            <template v-else-if="column.dataIndex === 'action'">
+              <a-button v-if="record.status === STATUS_PENDING" type="link" size="small" @click="handleViewData(record)">查看数据</a-button>
+              <span v-else>等待上传</span>
+            </template>
+          </template>
+        </a-table>
+      </template>
+
+      <template v-else>
+        <div class="curve-header">
+          <a-space>
+            <a-typography-text strong>
+              {{ activeBatch?.stationName }}  {{ activeBatch?.reportDateTime }}
+            </a-typography-text>
+            <a-tag :color="statusMap[activeBatch?.status]?.color">{{ statusMap[activeBatch?.status]?.text }}</a-tag>
+          </a-space>
+        </div>
+        <PowerLineChart
+          :x-axis="curveAxis"
+          :series="curveSeries"
+          height="420px"
+          y-axis-name="kW"
+          point-clickable
+          @point-click="handlePointClick"
+        />
+        <div class="curve-footer">
+          <a-button @click="handleCancelView">取消</a-button>
+          <a-button type="primary" :loading="confirming" :disabled="activeBatch?.status !== STATUS_PENDING" @click="handleConfirm">确认数据</a-button>
+        </div>
+      </template>
+    </a-card>
+
+    <a-modal
+      v-model:open="editModal.open"
+      title="修改预测功率"
+      :confirm-loading="editSaving"
+      ok-text="保存"
+      cancel-text="取消"
+      @ok="handleEditSave"
+    >
+      <a-form layout="vertical">
+        <a-form-item label="时间">
+          <span>{{ editModal.timeLabel }}</span>
+        </a-form-item>
+        <a-form-item label="预测功率(kW)">
+          <a-input-number v-model:value="editPower" :min="0" :step="0.1" class="full-control" />
+        </a-form-item>
+      </a-form>
+    </a-modal>
+  </div>
+</template>
+
+<script setup name="PowerForecastReport">
+import {computed, onMounted, reactive, ref} from 'vue';
+import {message} from 'ant-design-vue';
+import dayjs from 'dayjs';
+import {listStation} from '@/api/config/station';
+import {
+  confirmPowerForecastReportBatch,
+  listPowerForecastReportBatch,
+  listShortTermPowerForecastReport,
+  listSuperShortTermPowerForecastReport,
+  updateShortTermPowerForecastReport,
+  updateSuperShortTermPowerForecastReport
+} from '@/api/data/powerForecastReport';
+import PowerLineChart from '../components/PowerLineChart.vue';
+import StationSelector from '../components/StationSelector.vue';
+import {normalizeRows} from '../components/dataAdapter';
+import {toStationOptions} from '../components/stationAdapter';
+
+const STATUS_PENDING = 1;
+
+const loading = ref(false);
+const confirming = ref(false);
+const editSaving = ref(false);
+const stationOptions = ref([]);
+const batchRows = ref([]);
+const viewMode = ref('table');
+const activeBatch = ref(null);
+const reportRows = ref([]);
+
+const query = reactive({
+  stationCode: undefined,
+  type: 'short',
+  startTime: dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
+  endTime: dayjs().format('YYYY-MM-DD')
+});
+
+const pagination = reactive({
+  current: 1,
+  pageSize: 10,
+  total: 0
+});
+
+const typeOptions = [
+  {label: '短期', value: 'short'},
+  {label: '超短期', value: 'ultra'}
+];
+
+const statusMap = {
+  1: {text: '待确认', color: 'orange'},
+  2: {text: '已确认', color: 'green'}
+};
+
+const columns = [
+  {title: '上报日期时间', dataIndex: 'reportDateTime', width: 180},
+  {title: '场站', dataIndex: 'stationName', width: 160},
+  {title: '文件', dataIndex: 'file', width: 120},
+  {title: '通道', dataIndex: 'channel', width: 120},
+  {title: '状态', dataIndex: 'status', width: 100},
+  {title: '操作', dataIndex: 'action', width: 120}
+];
+
+const editModal = reactive({
+  open: false,
+  row: null,
+  timeLabel: ''
+});
+const editPower = ref(undefined);
+
+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 !start.isAfter(end);
+});
+
+const curveAxis = computed(() => reportRows.value.map((item) => item.timeLabel));
+const curveSeries = computed(() => [
+  {name: '预测功率', color: '#1677ff', data: reportRows.value.map((item) => Number(item.power))}
+]);
+
+const loadStationOptions = async () => {
+  const res = await listStation({pageNum: 1, pageSize: 500});
+  stationOptions.value = toStationOptions(normalizeRows(res));
+  if (!query.stationCode && stationOptions.value.length) {
+    query.stationCode = stationOptions.value[0].value;
+  }
+};
+
+const formatReportDateTime = (row) => {
+  const day = String(row.reportDay ?? '');
+  if (day.length !== 8) return '-';
+  return `${day.slice(0, 4)}-${day.slice(4, 6)}-${day.slice(6, 8)} ${row.estReportTime ?? ''}`;
+};
+
+const fetchBatchList = async () => {
+  const res = await listPowerForecastReportBatch({
+    stationCode: query.stationCode,
+    type: query.type,
+    startTime: query.startTime,
+    endTime: query.endTime,
+    pageNum: pagination.current,
+    pageSize: pagination.pageSize
+  });
+  batchRows.value = normalizeRows(res).map((row) => ({
+    ...row,
+    reportDateTime: formatReportDateTime(row)
+  }));
+  pagination.total = res?.total || 0;
+};
+
+// 事件1: 重置页码查询
+const handleQuery = async () => {
+  if (!query.stationCode) {
+    message.warning('请先选择场站');
+    return;
+  }
+  if (!queryValid.value) return;
+
+  pagination.current = 1;
+  loading.value = true;
+  try {
+    await fetchBatchList();
+  } catch (e) {
+    /* 错误提示由 request 拦截器处理 */
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleTableChange = (pager) => {
+  pagination.current = pager.current;
+  pagination.pageSize = pager.pageSize;
+  loading.value = true;
+  fetchBatchList()
+    .catch(() => {})
+    .finally(() => {
+      loading.value = false;
+    });
+};
+
+const fetchReportList = async (batch) => {
+  const fn = batch.type === 'short' ? listShortTermPowerForecastReport : listSuperShortTermPowerForecastReport;
+  const res = await fn({batchNo: batch.batchNo, pageNum: 1, pageSize: 1000});
+  reportRows.value = normalizeRows(res)
+    .sort((a, b) => Number(a.forecastTime) - Number(b.forecastTime))
+    .map((row) => ({
+      ...row,
+      timeLabel: dayjs.unix(Number(row.forecastTime)).format('YYYY-MM-DD HH:mm'),
+      power: Number(row.power)
+    }));
+};
+
+// 事件2: 隐藏 table, 显示该批次数据曲线
+const handleViewData = async (record) => {
+  activeBatch.value = record;
+  reportRows.value = [];
+  viewMode.value = 'curve';
+  try {
+    await fetchReportList(record);
+  } catch (e) {
+    /* 错误提示由 request 拦截器处理 */
+  }
+};
+
+// 点击曲线节点弹出编辑弹窗
+const handlePointClick = ({dataIndex}) => {
+  const batch = activeBatch.value;
+  if (!batch || Number(batch.status) !== STATUS_PENDING) return;
+  const row = reportRows.value[dataIndex];
+  if (!row) return;
+  editModal.row = row;
+  editModal.timeLabel = row.timeLabel;
+  editPower.value = Number(row.power);
+  editModal.open = true;
+};
+
+const handleEditSave = async () => {
+  if (editPower.value === null || editPower.value === undefined) {
+    message.warning('请输入预测功率');
+    return;
+  }
+  const row = editModal.row;
+  const batch = activeBatch.value;
+  if (!row || !batch) return;
+
+  const fn = batch.type === 'short' ? updateShortTermPowerForecastReport : updateSuperShortTermPowerForecastReport;
+  editSaving.value = true;
+  try {
+    await fn({Id: row.Id, power: editPower.value, batchNo: batch.batchNo});
+    row.power = Number(editPower.value);
+    message.success('修改成功');
+    editModal.open = false;
+  } catch (e) {
+    /* 错误提示由 request 拦截器处理 */
+  } finally {
+    editSaving.value = false;
+  }
+};
+
+// 事件3: 返回 table
+const handleCancelView = () => {
+  viewMode.value = 'table';
+  activeBatch.value = null;
+  reportRows.value = [];
+};
+
+// 事件4: 确认批次
+const handleConfirm = async () => {
+  const batch = activeBatch.value;
+  if (!batch) return;
+  confirming.value = true;
+  try {
+    await confirmPowerForecastReportBatch(batch.batchNo);
+    message.success('确认成功');
+    handleCancelView();
+    await fetchBatchList();
+  } catch (e) {
+    /* 错误提示由 request 拦截器处理 */
+  } finally {
+    confirming.value = false;
+  }
+};
+
+onMounted(async () => {
+  await loadStationOptions();
+  await handleQuery();
+});
+</script>
+
+<style lang="less" scoped>
+.power-forecast-report-page {
+  padding: 16px;
+}
+.full-control {
+  width: 100%;
+}
+.content-card {
+  margin-top: 16px;
+}
+.curve-header {
+  margin-bottom: 12px;
+}
+.curve-footer {
+  display: flex;
+  justify-content: center;
+  gap: 12px;
+  margin-top: 16px;
+}
+</style>

+ 39 - 0
release_note/20261015/20261015_power_forecast_report_menu.sql

@@ -0,0 +1,39 @@
+-- 功率上报菜单补丁(任务 101904, 幂等, 可重复执行)
+-- 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 = '功率预测'),
+10, 'power-forecast-report', 'ufo/electricity/forecast/PowerForecastReport', '', 'PowerForecastReport', 1, 0, 'C', '0', '0', 'power:forecastReportBatch:list', 'SendOutlined', 'admin', now(), '', NULL, '功率上报页面'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu t WHERE t.menu_name = '功率上报');
+
+-- 2. 增加 功率上报 按钮权限(确认/新增)
+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 = '功率上报'),
+1, '', '', '', '', 1, 0, 'F', '0', '0', 'power:forecastReportBatch:edit', '#', 'admin', now(), '', NULL, '确认上报批次'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu t WHERE t.menu_name = '功率上报确认');
+
+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 = '功率上报'),
+2, '', '', '', '', 1, 0, 'F', '0', '0', 'power:forecastReportBatch:add', '#', 'admin', now(), '', NULL, '新增上报批次'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu t WHERE t.menu_name = '功率上报新增');
+
+-- 3. 授权给管理员角色(role_id = 1)
+INSERT INTO sys_role_menu (role_id, menu_id)
+SELECT 1, menu_id FROM sys_menu t WHERE t.menu_name = '功率上报'
+AND NOT EXISTS (SELECT 1 FROM sys_role_menu r WHERE r.role_id = 1 AND r.menu_id = (SELECT menu_id FROM sys_menu t WHERE t.menu_name = '功率上报'));
+
+INSERT INTO sys_role_menu (role_id, menu_id)
+SELECT 1, menu_id FROM sys_menu t WHERE t.menu_name = '功率上报确认'
+AND NOT EXISTS (SELECT 1 FROM sys_role_menu r WHERE r.role_id = 1 AND r.menu_id = (SELECT menu_id FROM sys_menu t WHERE t.menu_name = '功率上报确认'));
+
+INSERT INTO sys_role_menu (role_id, menu_id)
+SELECT 1, menu_id FROM sys_menu t WHERE t.menu_name = '功率上报新增'
+AND NOT EXISTS (SELECT 1 FROM sys_role_menu r WHERE r.role_id = 1 AND r.menu_id = (SELECT menu_id FROM sys_menu t WHERE t.menu_name = '功率上报新增'));