|
|
@@ -0,0 +1,296 @@
|
|
|
+<template>
|
|
|
+ <div class="wind-speed-curve-page">
|
|
|
+ <a-card :bordered="false" class="query-card">
|
|
|
+ <a-form :model="query" layout="inline" class="query-form">
|
|
|
+ <a-form-item label="场站">
|
|
|
+ <a-select
|
|
|
+ v-model:value="query.stationId"
|
|
|
+ :options="stationOptions"
|
|
|
+ show-search
|
|
|
+ :filter-option="filterOption"
|
|
|
+ placeholder="请选择场站"
|
|
|
+ class="station-control"
|
|
|
+ />
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="时间">
|
|
|
+ <a-range-picker
|
|
|
+ v-model:value="query.timeRange"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ class="time-control"
|
|
|
+ />
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item class="query-actions">
|
|
|
+ <a-space>
|
|
|
+ <a-button type="primary" :loading="loading" @click="handleQuery">确认</a-button>
|
|
|
+ <a-button :disabled="!hasData" @click="handleExport">导出</a-button>
|
|
|
+ </a-space>
|
|
|
+ </a-form-item>
|
|
|
+ </a-form>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card :bordered="false" class="chart-card" title="风速曲线图">
|
|
|
+ <div ref="chartRef" class="curve-chart" />
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card :bordered="false" class="detail-card" title="明细数据">
|
|
|
+ <a-descriptions v-if="summary" size="small" :column="4" bordered class="summary">
|
|
|
+ <a-descriptions-item label="总记录数">{{ summary.totalRecords }}</a-descriptions-item>
|
|
|
+ <a-descriptions-item label="有效记录数">{{ summary.validRecords }}</a-descriptions-item>
|
|
|
+ </a-descriptions>
|
|
|
+ <a-table
|
|
|
+ :columns="columns"
|
|
|
+ :data-source="detailRows"
|
|
|
+ :loading="loading"
|
|
|
+ :pagination="pagination"
|
|
|
+ :scroll="{ x: 500, y: 480 }"
|
|
|
+ row-key="key"
|
|
|
+ size="middle"
|
|
|
+ bordered
|
|
|
+ />
|
|
|
+ </a-card>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup name="ElectricityWindSpeedCurve">
|
|
|
+import {computed, nextTick, onBeforeUnmount, onMounted, reactive, ref} from 'vue';
|
|
|
+import * as echarts from 'echarts';
|
|
|
+import dayjs from 'dayjs';
|
|
|
+import {message} from 'ant-design-vue';
|
|
|
+import {saveAs} from 'file-saver';
|
|
|
+import {listStation} from '@/api/config/station';
|
|
|
+import {getWsChart, exportWsChart} from '@/api/weather/weatherRaw';
|
|
|
+import {blobValidate} from '@/utils/bearjia';
|
|
|
+import {normalizeRows, toNumber} from '../components/dataAdapter';
|
|
|
+
|
|
|
+const loading = ref(false);
|
|
|
+const stationOptions = ref([]);
|
|
|
+const chartRef = ref();
|
|
|
+const detailRows = ref([]);
|
|
|
+const summary = ref(null);
|
|
|
+
|
|
|
+let chartInstance = null;
|
|
|
+
|
|
|
+const query = reactive({
|
|
|
+ stationId: undefined,
|
|
|
+ timeRange: [
|
|
|
+ dayjs().subtract(3, 'day').format('YYYY-MM-DD'),
|
|
|
+ dayjs().add(3, 'day').format('YYYY-MM-DD')
|
|
|
+ ]
|
|
|
+});
|
|
|
+
|
|
|
+const hasData = computed(() => !!summary.value && summary.value.totalRecords > 0);
|
|
|
+
|
|
|
+const columns = [
|
|
|
+ {title: '时间', dataIndex: 'time', width: 180, align: 'center', fixed: 'left'},
|
|
|
+ {title: '风速(m/s)', dataIndex: 'ws', width: 150, align: 'right'}
|
|
|
+];
|
|
|
+
|
|
|
+const pagination = {
|
|
|
+ pageSize: 20,
|
|
|
+ showSizeChanger: true,
|
|
|
+ showTotal: (total) => `共 ${total} 条`
|
|
|
+};
|
|
|
+
|
|
|
+const filterOption = (input, option) => String(option?.label || '').toLowerCase().includes(input.toLowerCase());
|
|
|
+
|
|
|
+const toTimeParams = () => {
|
|
|
+ const [start, end] = query.timeRange || [];
|
|
|
+ return {
|
|
|
+ stationId: query.stationId,
|
|
|
+ startTime: dayjs(start).startOf('day').unix(),
|
|
|
+ endTime: dayjs(end).endOf('day').unix()
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+const loadStations = async () => {
|
|
|
+ const res = await listStation({pageNum: 1, pageSize: 500});
|
|
|
+ const list = normalizeRows(res);
|
|
|
+ stationOptions.value = list.map((item) => ({
|
|
|
+ label: item.stationName || item.name || item.id,
|
|
|
+ value: item.id ?? item.stationId
|
|
|
+ }));
|
|
|
+ // 自动选中第一个场站
|
|
|
+ if (stationOptions.value.length && !query.stationId) {
|
|
|
+ query.stationId = stationOptions.value[0].value;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const buildOption = (stats) => {
|
|
|
+ const rows = (stats || []).map((item) => ({time: item.time, ws: toNumber(item.ws, 0)}));
|
|
|
+ return {
|
|
|
+ tooltip: {trigger: 'axis'},
|
|
|
+ legend: {top: 0, left: 'center', data: ['风速']},
|
|
|
+ grid: {left: 60, right: 24, top: 46, bottom: 60},
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ boundaryGap: false,
|
|
|
+ data: rows.map((item) => item.time),
|
|
|
+ axisLabel: {color: '#5a6a85'}
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ name: '风速(m/s)',
|
|
|
+ splitLine: {lineStyle: {color: '#e6eef8'}},
|
|
|
+ axisLabel: {color: '#5a6a85'}
|
|
|
+ },
|
|
|
+ dataZoom: [
|
|
|
+ {type: 'inside', start: 0, end: 100},
|
|
|
+ {type: 'slider', height: 18, bottom: 12, start: 0, end: 100}
|
|
|
+ ],
|
|
|
+ series: [{
|
|
|
+ name: '风速',
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ showSymbol: false,
|
|
|
+ data: rows.map((item) => item.ws)
|
|
|
+ }]
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+const renderChart = async (stats) => {
|
|
|
+ await nextTick();
|
|
|
+ if (!chartRef.value) return;
|
|
|
+ if (!chartInstance) {
|
|
|
+ chartInstance = echarts.init(chartRef.value);
|
|
|
+ }
|
|
|
+ chartInstance.setOption(buildOption(stats), true);
|
|
|
+};
|
|
|
+
|
|
|
+const resizeChart = () => {
|
|
|
+ chartInstance?.resize();
|
|
|
+};
|
|
|
+
|
|
|
+const handleQuery = async () => {
|
|
|
+ if (!query.stationId) {
|
|
|
+ message.warning('请先选择场站');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const params = toTimeParams();
|
|
|
+ if (!params.startTime || !params.endTime) {
|
|
|
+ message.warning('请选择时间范围');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const res = await getWsChart(params);
|
|
|
+ const data = res?.data;
|
|
|
+ if (!data) {
|
|
|
+ summary.value = null;
|
|
|
+ detailRows.value = [];
|
|
|
+ renderChart([]);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ summary.value = {
|
|
|
+ totalRecords: data.totalRecords,
|
|
|
+ validRecords: data.validRecords
|
|
|
+ };
|
|
|
+ detailRows.value = (data.stats || []).map((item, index) => ({
|
|
|
+ key: index,
|
|
|
+ time: item.time,
|
|
|
+ ws: toNumber(item.ws, 0)
|
|
|
+ }));
|
|
|
+ renderChart(data.stats || []);
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handleExport = async () => {
|
|
|
+ if (!query.stationId) {
|
|
|
+ message.warning('请先选择场站');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const params = toTimeParams();
|
|
|
+ if (!params.startTime || !params.endTime) {
|
|
|
+ message.warning('请选择时间范围');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const station = stationOptions.value.find((item) => String(item.value) === String(query.stationId));
|
|
|
+ const [start, end] = query.timeRange || [];
|
|
|
+ try {
|
|
|
+ const res = await exportWsChart(params);
|
|
|
+ const isFile = await blobValidate(res);
|
|
|
+ if (isFile) {
|
|
|
+ saveAs(new Blob([res]), `风速曲线_${station?.label || query.stationId}_${start}_${end}.xlsx`);
|
|
|
+ message.success('导出成功');
|
|
|
+ } else {
|
|
|
+ const text = await res.text();
|
|
|
+ let msg = '导出失败';
|
|
|
+ try {
|
|
|
+ msg = JSON.parse(text)?.msg || msg;
|
|
|
+ } catch (e) {
|
|
|
+ /* ignore */
|
|
|
+ }
|
|
|
+ message.error(msg);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('导出失败:', error);
|
|
|
+ message.error('导出失败');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ window.addEventListener('resize', resizeChart);
|
|
|
+ await loadStations();
|
|
|
+ await handleQuery();
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ window.removeEventListener('resize', resizeChart);
|
|
|
+ chartInstance?.dispose();
|
|
|
+ chartInstance = null;
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="less" scoped>
|
|
|
+.wind-speed-curve-page {
|
|
|
+ padding: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.query-card {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.query-form {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 12px 16px;
|
|
|
+ align-items: flex-start;
|
|
|
+
|
|
|
+ :deep(.ant-form-item) {
|
|
|
+ margin: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.ant-form-item-label) {
|
|
|
+ min-width: 48px;
|
|
|
+ text-align: left;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.station-control {
|
|
|
+ width: 240px;
|
|
|
+}
|
|
|
+
|
|
|
+.time-control {
|
|
|
+ width: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+.query-actions {
|
|
|
+ margin-left: auto !important;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-card {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.curve-chart {
|
|
|
+ width: 100%;
|
|
|
+ height: 460px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-card {
|
|
|
+ .summary {
|
|
|
+ margin-bottom: 16px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|