| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- <template>
- <div class="humidity-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-item label="总记录数(本地)">{{ summary.localTotalRecords }}</a-descriptions-item>
- <a-descriptions-item label="有效记录数(本地)">{{ summary.localValidRecords }}</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="ElectricityHumidityCurve">
- 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 {getRhsfcChart, exportRhsfcChart} 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 || summary.value.localTotalRecords > 0));
- const columns = [
- {title: '时间', dataIndex: 'time', width: 180, align: 'center', fixed: 'left'},
- {title: '来源', dataIndex: 'source', width: 100, align: 'center'},
- {title: '湿度(%)', dataIndex: 'rhsfc', 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 = (forecastStats, localStats) => {
- // 预测/本地采样时间点不同,用连续时间轴各自按自身采样点绘制,避免线条因对方缺数而中断
- // 按时间戳排序,保证数组顺序与时间一致,避免后端返回无序导致线条自我交叉
- const toPairs = (stats, key) => (stats || [])
- .map((item) => [dayjs(item.time).valueOf(), toNumber(item[key], 0)])
- .sort((a, b) => a[0] - b[0]);
- return {
- tooltip: {trigger: 'axis'},
- legend: {top: 0, left: 'center', data: ['预测', '本地']},
- grid: {left: 60, right: 24, top: 46, bottom: 60},
- xAxis: {
- type: 'time',
- axisLabel: {color: '#5a6a85'},
- axisPointer: {snap: true}
- },
- yAxis: {
- type: 'value',
- name: '湿度(%)',
- 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: toPairs(forecastStats, 'rhsfc')},
- {name: '本地', type: 'line', smooth: true, showSymbol: false, data: toPairs(localStats, 'rhsfc')}
- ]
- };
- };
- const renderChart = async (forecastStats, localStats) => {
- await nextTick();
- if (!chartRef.value) return;
- if (!chartInstance) {
- chartInstance = echarts.init(chartRef.value);
- }
- chartInstance.setOption(buildOption(forecastStats, localStats), 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 getRhsfcChart(params);
- const data = res?.data;
- if (!data) {
- summary.value = null;
- detailRows.value = [];
- renderChart([], []);
- return;
- }
- const forecast = data.forecast || {};
- const local = data.local || {};
- summary.value = {
- totalRecords: forecast.totalRecords || 0,
- validRecords: forecast.validRecords || 0,
- localTotalRecords: local.totalRecords || 0,
- localValidRecords: local.validRecords || 0
- };
- detailRows.value = [
- ...(forecast.stats || []).map((item, index) => ({
- key: `forecast-${index}`,
- time: item.time,
- source: item.source || '预测',
- rhsfc: toNumber(item.rhsfc, 0)
- })),
- ...(local.stats || []).map((item, index) => ({
- key: `local-${index}`,
- time: item.time,
- source: item.source || '本地',
- rhsfc: toNumber(item.rhsfc, 0)
- }))
- ];
- renderChart(forecast.stats || [], local.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 exportRhsfcChart(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>
- .humidity-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>
|