|
|
@@ -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>
|