1063 lines
30 KiB
Vue
1063 lines
30 KiB
Vue
<template>
|
||
<div class="water-temperature">
|
||
<a-tabs v-model:activeKey="activeTabKey">
|
||
<a-tab-pane v-for="tab in tabsList" :key="tab.key" :tab="tab.name" />
|
||
<template #rightExtra>
|
||
<div class="search-bar">
|
||
<a-range-picker
|
||
v-model:value="dateRange"
|
||
format="YYYY-MM-DD HH:mm"
|
||
:show-time="showTimeConfig"
|
||
:allowClear="false"
|
||
:presets="DateSetting.RangeButton.month1"
|
||
:disabled-date="disabledDate"
|
||
/>
|
||
<a-button type="primary" class="search-btn" @click="handleSearch">
|
||
查询
|
||
</a-button>
|
||
<a-button
|
||
v-if="isSummaryMode"
|
||
class="search-btn"
|
||
@click="handleExport"
|
||
>
|
||
导出
|
||
</a-button>
|
||
</div>
|
||
</template>
|
||
</a-tabs>
|
||
|
||
<div class="tab-checkbox">
|
||
<a-checkbox-group
|
||
v-model:value="selectedColumns"
|
||
name="checkboxgroup"
|
||
:options="filteredCheckBoxOptions"
|
||
class="checkbox-group"
|
||
@change="handleCheckboxChange"
|
||
/>
|
||
</div>
|
||
|
||
<a-spin :spinning="isLoading" tip="加载中...">
|
||
<div class="tab-content">
|
||
<div class="content-body">
|
||
<div class="chart-wrapper">
|
||
<div ref="chartRef" class="chart-container"></div>
|
||
<a-empty
|
||
v-if="!chartData || chartData.length === 0"
|
||
description="暂无数据"
|
||
class="chart-empty"
|
||
/>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<BasicTable
|
||
:scrollY="360"
|
||
:scrollX="tableScrollX"
|
||
:columns="currentColumns"
|
||
:data="tableData"
|
||
>
|
||
<template #summary>
|
||
<a-table-summary fixed>
|
||
<template
|
||
v-for="summaryRow in summaryRows"
|
||
:key="summaryRow.label"
|
||
>
|
||
<a-table-summary-row class="summary-row">
|
||
<a-table-summary-cell :index="0">
|
||
{{ summaryRow.label }}
|
||
</a-table-summary-cell>
|
||
<a-table-summary-cell
|
||
v-for="col in summaryColumns"
|
||
:key="col.dataIndex"
|
||
:index="col.summaryIndex"
|
||
>
|
||
{{ summaryRow.cells[col.dataIndex]?.value ?? '-' }}
|
||
<Tooltip
|
||
v-if="summaryRow.cells[col.dataIndex]?.time"
|
||
:title="`时间:${
|
||
summaryRow.cells[col.dataIndex].time
|
||
}`"
|
||
placement="top"
|
||
>
|
||
<InfoCircleOutlined class="summary-tip-icon" />
|
||
</Tooltip>
|
||
</a-table-summary-cell>
|
||
</a-table-summary-row>
|
||
</template>
|
||
</a-table-summary>
|
||
</template>
|
||
</BasicTable>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-spin>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import {
|
||
ref,
|
||
watch,
|
||
computed,
|
||
onMounted,
|
||
onBeforeUnmount,
|
||
nextTick
|
||
} from 'vue';
|
||
import dayjs, { Dayjs } from 'dayjs';
|
||
import * as echarts from 'echarts';
|
||
import { Tooltip } from 'ant-design-vue';
|
||
import {
|
||
// getMonitorDataWaterTempList,
|
||
getMonitorDataWaterTemp,
|
||
getMonitorDataWaterTempPowerStation,
|
||
getMonitorDataWaterTempPowerStation2
|
||
} from '@/api/mapModal';
|
||
import { useModelStore } from '@/store/modules/model';
|
||
import BasicTable from '@/components/BasicTable/index.vue';
|
||
import { DateSetting } from '@/utils/enumeration';
|
||
import { InfoCircleOutlined } from '@ant-design/icons-vue';
|
||
|
||
const modelStore = useModelStore();
|
||
|
||
const props = defineProps({
|
||
code: { type: String, default: '' },
|
||
isActive: { type: Boolean, default: false },
|
||
stcd: { type: String, default: '' }
|
||
});
|
||
|
||
const hasLoaded = ref(false);
|
||
const isLoading = ref(false);
|
||
const tableData = ref<any[]>([]);
|
||
const chartData = ref<any[]>([]);
|
||
const chartRef = ref<HTMLElement>();
|
||
let chartInstance: echarts.ECharts | null = null;
|
||
|
||
const tabsList = [{ name: '水温', key: 'swjc.tabs.jcsj' }];
|
||
const activeTabKey = ref('swjc.tabs.jcsj');
|
||
|
||
// 时间选择器配置
|
||
const showTimeConfig = {
|
||
format: 'HH:mm',
|
||
hourStep: 1,
|
||
minuteStep: 5,
|
||
secondStep: 60
|
||
};
|
||
const disabledDate = (current: Dayjs) =>
|
||
current && current.isAfter(dayjs(), 'day');
|
||
|
||
const initDateRange = (): [Dayjs, Dayjs] => {
|
||
const startDate = dayjs().subtract(7, 'day').startOf('day');
|
||
return [startDate, dayjs()];
|
||
};
|
||
const dateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
|
||
|
||
// ==================== Checkbox 配置 ====================
|
||
const selectedColumns = ref<any[]>([]);
|
||
const checkBoxOptions = [
|
||
{ label: '综合分析', value: 'summary' },
|
||
{ label: '等深水温', value: 'deep' }
|
||
];
|
||
const showSummaryCheckbox = ref(false);
|
||
const showDeepCheckbox = ref(false);
|
||
|
||
const isDataEmpty = computed(() => {
|
||
const data = Array.isArray(chartData.value) ? chartData.value : [];
|
||
return data.length === 0;
|
||
});
|
||
|
||
const filteredCheckBoxOptions = computed(() => {
|
||
if (isDataEmpty.value) return [];
|
||
|
||
const available = checkBoxOptions.filter(opt => {
|
||
if (opt.value === 'summary' && !showSummaryCheckbox.value) return false;
|
||
if (opt.value === 'deep' && !showDeepCheckbox.value) return false;
|
||
return true;
|
||
});
|
||
|
||
// 互斥逻辑:已选中等温时只显示等温,已选中综合时只显示综合
|
||
const selected = selectedColumns.value;
|
||
if (selected.length === 1) {
|
||
const val = selected[0];
|
||
return available.filter(opt => opt.value === val);
|
||
}
|
||
|
||
return available;
|
||
});
|
||
|
||
const isDeepMode = computed(() => selectedColumns.value.includes('deep'));
|
||
const isSummaryMode = computed(() => selectedColumns.value.includes('summary'));
|
||
const currentChartMode = computed(() => {
|
||
if (isSummaryMode.value) return 'summary';
|
||
if (isDeepMode.value) return 'deep';
|
||
return 'base';
|
||
});
|
||
|
||
const handleCheckboxChange = (values: any[]) => {
|
||
const hasSummary = values.includes('summary');
|
||
const hasDeep = values.includes('deep');
|
||
if (hasSummary && hasDeep) {
|
||
// 互斥:同时勾选时保留最后点击的那个
|
||
selectedColumns.value = [values[values.length - 1]];
|
||
} else if (!hasSummary && !hasDeep) {
|
||
// 全部取消,清空选择
|
||
selectedColumns.value = [];
|
||
} else {
|
||
selectedColumns.value = values;
|
||
}
|
||
fetchData();
|
||
};
|
||
|
||
// ==================== 表格列配置 ====================
|
||
const baseWaterTempColumns = [
|
||
{
|
||
title: '时间',
|
||
dataIndex: 'tm',
|
||
fixed: 'left',
|
||
customRender: ({ text }: any) =>
|
||
text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-'
|
||
},
|
||
{
|
||
title: '水温(°C)',
|
||
dataIndex: 'wt',
|
||
summary: true,
|
||
customRender: ({ text }: any) =>
|
||
text !== undefined && text !== null ? Number(text).toFixed(1) : '-'
|
||
}
|
||
];
|
||
|
||
const deepModeExtraColumns = [
|
||
{
|
||
title: '等温水深(m)',
|
||
dataIndex: 'deep_wt',
|
||
summary: true,
|
||
customRender: ({ record }: any) => {
|
||
const val = record?.wtvtDataVo?.wthg;
|
||
return val !== undefined && val !== null ? Number(val).toFixed(2) : '-';
|
||
}
|
||
},
|
||
{
|
||
title: '垂向水温(℃)',
|
||
dataIndex: 'vert_wt',
|
||
summary: true,
|
||
customRender: ({ record }: any) => {
|
||
const val = record?.wtvtDataVo?.vwt;
|
||
return val !== undefined && val !== null ? Number(val).toFixed(1) : '-';
|
||
}
|
||
}
|
||
];
|
||
|
||
const summaryModeColumns = [
|
||
{
|
||
title: '时间',
|
||
dataIndex: 'tm',
|
||
width: 130,
|
||
fixed: 'left',
|
||
customRender: ({ text }: any) =>
|
||
text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-'
|
||
},
|
||
{
|
||
title: '出库水温(℃)',
|
||
dataIndex: 'wt',
|
||
summary: true,
|
||
customRender: ({ text }: any) =>
|
||
text !== undefined && text !== null ? Number(text).toFixed(1) : '-'
|
||
},
|
||
{
|
||
title: '入库水温(℃)',
|
||
dataIndex: 'iwt_wt',
|
||
summary: true,
|
||
customRender: ({ record }: any) =>
|
||
record?.iwtDataVo?.wt !== undefined && record?.iwtDataVo?.wt !== null
|
||
? Number(record.iwtDataVo.wt).toFixed(1)
|
||
: '-'
|
||
},
|
||
{
|
||
title: '天然水温(°C)',
|
||
dataIndex: 'natureTmp',
|
||
summary: true,
|
||
customRender: ({ text }: any) =>
|
||
text !== undefined && text !== null ? Number(text).toFixed(1) : '-'
|
||
},
|
||
{
|
||
title: '气温(℃)',
|
||
dataIndex: 'tmp_at',
|
||
summary: true,
|
||
width: 80,
|
||
customRender: ({ record }: any) =>
|
||
record?.tmpDataVo?.at !== undefined && record?.tmpDataVo?.at !== null
|
||
? Number(record.tmpDataVo.at).toFixed(1)
|
||
: '-'
|
||
},
|
||
{
|
||
title: '出库流量(m³/s)',
|
||
dataIndex: 'hydropw_qo',
|
||
summary: true,
|
||
width: 120,
|
||
customRender: ({ record }: any) =>
|
||
record?.hydropwDataVo?.qo !== undefined &&
|
||
record?.hydropwDataVo?.qo !== null
|
||
? Number(record.hydropwDataVo.qo).toFixed(3)
|
||
: '-'
|
||
},
|
||
{
|
||
title: '入库流量(m³/s)',
|
||
dataIndex: 'hydropw_qi',
|
||
summary: true,
|
||
width: 120,
|
||
customRender: ({ record }: any) =>
|
||
record?.hydropwDataVo?.qi !== undefined &&
|
||
record?.hydropwDataVo?.qi !== null
|
||
? Number(record.hydropwDataVo.qi).toFixed(3)
|
||
: '-'
|
||
}
|
||
];
|
||
|
||
const swjcColumns = computed(() => {
|
||
if (isSummaryMode.value) return summaryModeColumns;
|
||
if (isDeepMode.value)
|
||
return [...baseWaterTempColumns, ...deepModeExtraColumns];
|
||
return baseWaterTempColumns;
|
||
});
|
||
|
||
const currentColumns = swjcColumns;
|
||
|
||
const tableScrollX = computed(() => {
|
||
const columns = currentColumns.value;
|
||
const totalWidth = columns.reduce(
|
||
(sum: number, col: any) => sum + (col.width || 100),
|
||
0
|
||
);
|
||
return totalWidth > 600 ? totalWidth : undefined;
|
||
});
|
||
|
||
// ==================== 合计行 ====================
|
||
const dataPathMap: Record<string, string> = {
|
||
wt: 'wt',
|
||
iwt_wt: 'iwtDataVo.wt',
|
||
natureTmp: 'natureTmp',
|
||
tmp_at: 'tmpDataVo.at',
|
||
hydropw_qo: 'hydropwDataVo.qo',
|
||
hydropw_qi: 'hydropwDataVo.qi',
|
||
deep_wt: 'wtvtDataVo.wthg',
|
||
vert_wt: 'wtvtDataVo.vwt'
|
||
};
|
||
|
||
const summaryColumns = computed(() => {
|
||
const modeCols = swjcColumns.value;
|
||
return modeCols
|
||
.filter((col: any) => col.summary === true)
|
||
.map((col: any, idx: number) => ({ ...col, summaryIndex: idx + 1 }));
|
||
});
|
||
|
||
const getNestedValue = (obj: any, path: string) => {
|
||
if (!path || !obj) return undefined;
|
||
return path.split('.').reduce((acc, key) => acc?.[key], obj);
|
||
};
|
||
|
||
const calcSummary = (dataIndex: string, type: 'max' | 'min' | 'avg') => {
|
||
const data = Array.isArray(chartData.value) ? chartData.value : [];
|
||
if (!data || data.length === 0) return null;
|
||
const dataPath = dataPathMap[dataIndex] || dataIndex;
|
||
const values = data
|
||
.map((r: any) => {
|
||
const val = getNestedValue(r, dataPath);
|
||
return val !== null && val !== undefined && val !== '-'
|
||
? Number(val)
|
||
: NaN;
|
||
})
|
||
.filter((v: number) => !isNaN(v));
|
||
if (values.length === 0) return null;
|
||
if (type === 'max') return Math.max(...values);
|
||
if (type === 'min') return Math.min(...values);
|
||
return values.reduce((s, v) => s + v, 0) / values.length;
|
||
};
|
||
|
||
const getSummaryTime = (dataIndex: string, type: 'max' | 'min') => {
|
||
const data = Array.isArray(chartData.value) ? chartData.value : [];
|
||
if (!data || data.length === 0) return '';
|
||
const dataPath = dataPathMap[dataIndex] || dataIndex;
|
||
const records = data.filter((r: any) => {
|
||
const val = getNestedValue(r, dataPath);
|
||
return val !== null && val !== undefined && val !== '-';
|
||
});
|
||
if (records.length === 0) return '';
|
||
const target = Math[type](
|
||
...records.map((r: any) => Number(getNestedValue(r, dataPath)))
|
||
);
|
||
const record = records.find(
|
||
(r: any) => Number(getNestedValue(r, dataPath)) === target
|
||
);
|
||
return record?.tm ? dayjs(record.tm).format('YYYY-MM-DD HH:mm:ss') : '';
|
||
};
|
||
|
||
const formatSummaryValue = (dataIndex: string, value: number | null) => {
|
||
if (value === null) return '-';
|
||
if (dataIndex.includes('qo') || dataIndex.includes('qi'))
|
||
return Number(value).toFixed(3);
|
||
if (dataIndex === 'deep_wt') return Number(value).toFixed(2);
|
||
return Number(value).toFixed(1);
|
||
};
|
||
|
||
const summaryRows = computed(() => {
|
||
// Force dependency tracking
|
||
void chartData.value;
|
||
void summaryColumns.value;
|
||
|
||
if (!Array.isArray(chartData.value) || chartData.value.length === 0)
|
||
return [];
|
||
|
||
const cols = summaryColumns.value;
|
||
if (!cols || cols.length === 0) return [];
|
||
|
||
const rows = [
|
||
{
|
||
label: '最大值',
|
||
showTime: true,
|
||
cells: {} as Record<string, { value: string; time: string }>
|
||
},
|
||
{
|
||
label: '最小值',
|
||
showTime: true,
|
||
cells: {} as Record<string, { value: string; time: string }>
|
||
},
|
||
{
|
||
label: '平均值',
|
||
showTime: false,
|
||
cells: {} as Record<string, { value: string; time: string }>
|
||
}
|
||
];
|
||
|
||
cols.forEach((col: any) => {
|
||
const dataIndex = col.dataIndex;
|
||
const maxVal = calcSummary(dataIndex, 'max');
|
||
const minVal = calcSummary(dataIndex, 'min');
|
||
const avgVal = calcSummary(dataIndex, 'avg');
|
||
rows[0].cells[dataIndex] = {
|
||
value: formatSummaryValue(dataIndex, maxVal),
|
||
time: maxVal !== null ? getSummaryTime(dataIndex, 'max') : ''
|
||
};
|
||
rows[1].cells[dataIndex] = {
|
||
value: formatSummaryValue(dataIndex, minVal),
|
||
time: minVal !== null ? getSummaryTime(dataIndex, 'min') : ''
|
||
};
|
||
rows[2].cells[dataIndex] = {
|
||
value: formatSummaryValue(dataIndex, avgVal),
|
||
time: ''
|
||
};
|
||
});
|
||
return rows;
|
||
});
|
||
|
||
// ==================== 图表 ====================
|
||
function shouldStagger(width: number, dataLen: number) {
|
||
return dataLen > Math.floor(width / 100);
|
||
}
|
||
|
||
function getAxisLabelConfig(chartWidth: number, xAxisData: string[]) {
|
||
const needStagger = shouldStagger(chartWidth, xAxisData.length);
|
||
return {
|
||
fontSize: 12,
|
||
interval: 'auto',
|
||
formatter: (value: string, index: number) => {
|
||
const [date, time] = value.split(' ');
|
||
if (needStagger) {
|
||
return index % 2 === 0 ? `${date}\n${time}\n ` : ` \n${date}\n${time}`;
|
||
}
|
||
return `${date}\n${time}`;
|
||
}
|
||
};
|
||
}
|
||
|
||
const updateChart = (data: any[]) => {
|
||
if (!chartInstance) return;
|
||
if (!data || data.length === 0) {
|
||
chartInstance.clear();
|
||
return;
|
||
}
|
||
|
||
const sorted = [...data].sort(
|
||
(a, b) => new Date(a.tm).getTime() - new Date(b.tm).getTime()
|
||
);
|
||
const xAxisData = sorted.map(item =>
|
||
dayjs(item.tm).format('YYYY-MM-DD HH:mm')
|
||
);
|
||
const width = chartRef.value?.clientWidth || 0;
|
||
const axisLabelConfig = getAxisLabelConfig(width, xAxisData);
|
||
|
||
let option: any;
|
||
|
||
if (currentChartMode.value === 'base') {
|
||
option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(50,50,50,0.9)',
|
||
textStyle: { color: '#fff', fontSize: 12 },
|
||
axisPointer: { type: 'cross' },
|
||
formatter: (params: any) => {
|
||
if (!params?.length) return '';
|
||
const idx = params[0].dataIndex;
|
||
const fullTime = sorted[idx]?.tm
|
||
? dayjs(sorted[idx].tm).format('YYYY-MM-DD HH:mm')
|
||
: '';
|
||
let html = `<div style="font-size:16px;margin-bottom:8px;">${fullTime}</div>`;
|
||
params.forEach((p: any) => {
|
||
const v = p.value != null ? Number(p.value).toFixed(1) : '-';
|
||
html += `<div style="display:flex;align-items:center;justify-content:space-between;margin:4px 0;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${p.color};margin-right:8px;"></span><span style="flex:1;font-size:14px;">水温: </span><span style="font-size:14px;min-width:60px;text-align:right;"><strong>${v}</strong> °C</span></div>`;
|
||
});
|
||
return html;
|
||
}
|
||
},
|
||
legend: {
|
||
width: '80%',
|
||
type: 'scroll',
|
||
top: 10,
|
||
data: ['水温'],
|
||
textStyle: { fontSize: 12 }
|
||
},
|
||
grid: { left: 60, right: 40, top: 80, bottom: 80 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: xAxisData,
|
||
axisLine: { lineStyle: { color: '#000000' } },
|
||
axisTick: { show: false },
|
||
axisLabel: axisLabelConfig,
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||
}
|
||
},
|
||
yAxis: {
|
||
name: '水温(°C)',
|
||
type: 'value',
|
||
axisLine: { lineStyle: { color: '#000000' } },
|
||
scale: true,
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '水温',
|
||
type: 'line',
|
||
data: sorted.map(item => item.wt),
|
||
smooth: true,
|
||
symbol: 'none',
|
||
lineStyle: { color: '#4B79AB', width: 2 },
|
||
itemStyle: { color: '#4B79AB' },
|
||
areaStyle: { color: '#85A9D0' }
|
||
}
|
||
],
|
||
dataZoom: [
|
||
{ type: 'inside', xAxisIndex: [0], throttle: 50, start: 0, end: 100 }
|
||
],
|
||
toolbox: {
|
||
show: true,
|
||
feature: {
|
||
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
|
||
},
|
||
right: 20,
|
||
top: 10
|
||
}
|
||
};
|
||
} else if (currentChartMode.value === 'deep') {
|
||
option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(50,50,50,0.9)',
|
||
textStyle: { color: '#fff', fontSize: 12 },
|
||
axisPointer: { type: 'cross' },
|
||
formatter: (params: any) => {
|
||
if (!params?.length) return '';
|
||
const idx = params[0].dataIndex;
|
||
const fullTime = sorted[idx]?.tm
|
||
? dayjs(sorted[idx].tm).format('YYYY-MM-DD HH:mm')
|
||
: '';
|
||
let html = `<div style="font-size:16px;margin-bottom:8px;">${fullTime}</div>`;
|
||
params.forEach((p: any) => {
|
||
const isDepth = p.seriesName.includes('水深');
|
||
const v =
|
||
p.value != null
|
||
? isDepth
|
||
? Number(p.value).toFixed(2)
|
||
: Number(p.value).toFixed(1)
|
||
: '-';
|
||
const unit = isDepth ? 'm' : '°C';
|
||
html += `<div style="display:flex;align-items:center;justify-content:space-between;margin:4px 0;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${p.color};margin-right:8px;"></span><span style="flex:1;font-size:14px;">${p.seriesName}: </span><span style="font-size:14px;min-width:60px;text-align:right;"><strong>${v}</strong> ${unit}</span></div>`;
|
||
});
|
||
return html;
|
||
}
|
||
},
|
||
legend: {
|
||
type: 'scroll',
|
||
top: 10,
|
||
data: ['水温', '等温水深'],
|
||
textStyle: { fontSize: 12 }
|
||
},
|
||
grid: { left: 60, right: 60, top: 80, bottom: 80 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: xAxisData,
|
||
axisLine: { lineStyle: { color: '#000000' } },
|
||
axisTick: { show: false },
|
||
axisLabel: axisLabelConfig,
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||
}
|
||
},
|
||
yAxis: [
|
||
{
|
||
name: '水温(°C)',
|
||
type: 'value',
|
||
position: 'left',
|
||
axisLine: { lineStyle: { color: '#000000' } },
|
||
scale: true,
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||
}
|
||
},
|
||
{
|
||
name: '水深(m)',
|
||
type: 'value',
|
||
position: 'right',
|
||
axisLine: { lineStyle: { color: '#000000' } },
|
||
scale: true,
|
||
splitLine: { show: false }
|
||
}
|
||
],
|
||
series: [
|
||
{
|
||
name: '水温',
|
||
type: 'line',
|
||
yAxisIndex: 0,
|
||
data: sorted.map(item => item.wt),
|
||
smooth: true,
|
||
showSymbol: false,
|
||
lineStyle: { color: '#4B79AB', width: 2 },
|
||
itemStyle: { color: '#4B79AB' }
|
||
},
|
||
{
|
||
name: '等温水深',
|
||
type: 'line',
|
||
yAxisIndex: 1,
|
||
data: sorted.map(item => item?.wtvtDataVo?.wthg),
|
||
smooth: true,
|
||
showSymbol: false,
|
||
lineStyle: { color: '#AB7AE5', width: 2 },
|
||
itemStyle: { color: '#AB7AE5' }
|
||
}
|
||
],
|
||
dataZoom: [
|
||
{ type: 'inside', xAxisIndex: [0], throttle: 50, start: 0, end: 100 }
|
||
],
|
||
toolbox: {
|
||
show: true,
|
||
feature: {
|
||
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
|
||
},
|
||
right: 20,
|
||
top: 10
|
||
}
|
||
};
|
||
} else {
|
||
option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(50,50,50,0.9)',
|
||
textStyle: { color: '#fff', fontSize: 12 },
|
||
axisPointer: { type: 'cross' },
|
||
formatter: (params: any) => {
|
||
if (!params?.length) return '';
|
||
const idx = params[0].dataIndex;
|
||
const fullTime = sorted[idx]?.tm
|
||
? dayjs(sorted[idx].tm).format('YYYY-MM-DD HH:mm')
|
||
: '';
|
||
let html = `<div style="font-size:16px;margin-bottom:8px;">${fullTime}</div>`;
|
||
params.forEach((p: any) => {
|
||
const isFlow = p.seriesName.includes('流量');
|
||
const v =
|
||
p.value != null
|
||
? isFlow
|
||
? Number(p.value).toFixed(3)
|
||
: Number(p.value).toFixed(1)
|
||
: '-';
|
||
const unit = isFlow ? 'm³/s' : '°C';
|
||
html += `<div style="display:flex;align-items:center;justify-content:space-between;margin:4px 0;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${p.color};margin-right:8px;"></span><span style="flex:1;font-size:14px;">${p.seriesName}: </span><span style="font-size:14px;min-width:60px;text-align:right;"><strong>${v}</strong> ${unit}</span></div>`;
|
||
});
|
||
return html;
|
||
}
|
||
},
|
||
legend: {
|
||
type: 'scroll',
|
||
width: '80%',
|
||
right: 60,
|
||
top: 10,
|
||
data: [
|
||
'出库水温',
|
||
'入库水温',
|
||
'天然水温',
|
||
'气温',
|
||
'出库流量',
|
||
'入库流量'
|
||
],
|
||
textStyle: { fontSize: 12 }
|
||
},
|
||
grid: { left: 60, right: 60, top: 80, bottom: 80 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: xAxisData,
|
||
axisLine: { lineStyle: { color: '#000000' } },
|
||
axisTick: { show: false },
|
||
axisLabel: axisLabelConfig,
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||
}
|
||
},
|
||
yAxis: [
|
||
{
|
||
name: '水温(°C)',
|
||
type: 'value',
|
||
position: 'left',
|
||
axisLine: { lineStyle: { color: '#000000' } },
|
||
scale: true,
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||
}
|
||
},
|
||
{
|
||
name: '流量(m³/s)',
|
||
type: 'value',
|
||
position: 'right',
|
||
axisLine: { lineStyle: { color: '#000000' } },
|
||
scale: true,
|
||
splitLine: { show: false }
|
||
}
|
||
],
|
||
series: [
|
||
{
|
||
name: '出库水温',
|
||
type: 'line',
|
||
yAxisIndex: 0,
|
||
data: sorted.map(item => item.wt),
|
||
smooth: true,
|
||
showSymbol: false,
|
||
lineStyle: { color: '#78C300', width: 2 },
|
||
itemStyle: { color: '#78C300' }
|
||
},
|
||
{
|
||
name: '入库水温',
|
||
type: 'line',
|
||
yAxisIndex: 0,
|
||
data: sorted.map(item => item?.iwtDataVo?.wt),
|
||
smooth: true,
|
||
showSymbol: false,
|
||
lineStyle: { color: '#9557A4', width: 2 },
|
||
itemStyle: { color: '#9557A4' }
|
||
},
|
||
{
|
||
name: '天然水温',
|
||
type: 'line',
|
||
yAxisIndex: 0,
|
||
data: sorted.map(item => item.natureTmp),
|
||
smooth: true,
|
||
showSymbol: false,
|
||
lineStyle: { color: '#DF91AB', width: 2 },
|
||
itemStyle: { color: '#DF91AB' }
|
||
},
|
||
{
|
||
name: '气温',
|
||
type: 'line',
|
||
yAxisIndex: 0,
|
||
data: sorted.map(item => item?.tmpDataVo?.at),
|
||
smooth: true,
|
||
showSymbol: false,
|
||
lineStyle: { color: '#7399C6', width: 2 },
|
||
itemStyle: { color: '#7399C6' }
|
||
},
|
||
{
|
||
name: '出库流量',
|
||
type: 'line',
|
||
yAxisIndex: 1,
|
||
data: sorted.map(item => item?.hydropwDataVo?.qo),
|
||
smooth: true,
|
||
showSymbol: false,
|
||
lineStyle: { color: '#56C2E3', width: 2 },
|
||
itemStyle: { color: '#56C2E3' }
|
||
},
|
||
{
|
||
name: '入库流量',
|
||
type: 'line',
|
||
yAxisIndex: 1,
|
||
data: sorted.map(item => item?.hydropwDataVo?.qi),
|
||
smooth: true,
|
||
showSymbol: false,
|
||
lineStyle: { color: '#DBB629', width: 2 },
|
||
itemStyle: { color: '#DBB629' }
|
||
}
|
||
],
|
||
dataZoom: [
|
||
{ type: 'inside', xAxisIndex: [0], throttle: 50, start: 0, end: 100 }
|
||
],
|
||
toolbox: {
|
||
show: true,
|
||
feature: {
|
||
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
|
||
},
|
||
right: 20,
|
||
top: 10
|
||
}
|
||
};
|
||
}
|
||
|
||
chartInstance.setOption(option, true);
|
||
};
|
||
|
||
// ==================== 数据请求 ====================
|
||
const isQxdMode = computed(() => props.code === 'qxd.tabs.jcsj');
|
||
|
||
const fetchData = async () => {
|
||
if (!dateRange.value) return;
|
||
|
||
// qxd模式必须有stcd才能请求
|
||
if (isQxdMode.value && !props.stcd) {
|
||
return;
|
||
}
|
||
|
||
isLoading.value = true;
|
||
try {
|
||
// 确定使用的stcd:qxd模式使用props.stcd,否则使用modelStore.params.stcd
|
||
const currentStcd = isQxdMode.value ? props.stcd : modelStore.params.stcd;
|
||
|
||
const filterParams = {
|
||
filter: {
|
||
logic: 'and',
|
||
filters: [
|
||
{
|
||
field: 'stcd',
|
||
operator: 'eq',
|
||
dataType: 'string',
|
||
value: currentStcd
|
||
},
|
||
{
|
||
field: 'tm',
|
||
operator: 'gte',
|
||
dataType: 'date',
|
||
value: dateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
|
||
},
|
||
{
|
||
field: 'tm',
|
||
operator: 'lte',
|
||
dataType: 'date',
|
||
value: dateRange.value[1].format('YYYY-MM-DD HH:mm:ss')
|
||
}
|
||
]
|
||
},
|
||
sort: [{ field: 'tm', dir: 'asc' }]
|
||
};
|
||
|
||
// qxd模式只调用getMonitorDataWaterTemp
|
||
if (isQxdMode.value) {
|
||
const mainRes = await getMonitorDataWaterTemp(filterParams);
|
||
const mainData = mainRes?.data?.data || mainRes?.data?.records || [];
|
||
showSummaryCheckbox.value = false;
|
||
showDeepCheckbox.value = false;
|
||
selectedColumns.value = [];
|
||
tableData.value = [...mainData].reverse();
|
||
chartData.value = mainData;
|
||
nextTick(() => updateChart(mainData));
|
||
} else {
|
||
// 原有模式
|
||
const [mainRes, powerStationRes, powerStationRes2] = await Promise.all([
|
||
getMonitorDataWaterTemp(filterParams),
|
||
getMonitorDataWaterTempPowerStation({ stcd: currentStcd }),
|
||
getMonitorDataWaterTempPowerStation2({ stcd: currentStcd })
|
||
]);
|
||
|
||
const mainData = mainRes?.data?.data || mainRes?.data?.records || [];
|
||
showSummaryCheckbox.value = !!powerStationRes?.data?.ioWtrv;
|
||
const powerStation2Data =
|
||
powerStationRes2?.data?.data || powerStationRes2?.data || null;
|
||
showDeepCheckbox.value =
|
||
!!powerStationRes?.data?.hasRstcdWtvt && powerStation2Data !== null;
|
||
|
||
tableData.value = [...mainData].reverse();
|
||
chartData.value = mainData;
|
||
nextTick(() => updateChart(mainData));
|
||
}
|
||
} catch (error) {
|
||
console.error('获取数据失败:', error);
|
||
tableData.value = [];
|
||
chartData.value = [];
|
||
if (chartInstance) chartInstance.clear();
|
||
} finally {
|
||
isLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const handleSearch = () => fetchData();
|
||
const handleExport = () => {
|
||
/* 导出逻辑 */
|
||
};
|
||
|
||
const initData = () => {
|
||
if (hasLoaded.value) return;
|
||
nextTick(() => {
|
||
if (chartRef.value && !chartInstance)
|
||
chartInstance = echarts.init(chartRef.value);
|
||
});
|
||
fetchData();
|
||
hasLoaded.value = true;
|
||
};
|
||
|
||
watch(
|
||
() => props.isActive,
|
||
active => {
|
||
if (active && !hasLoaded.value) initData();
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
watch(
|
||
() => modelStore.params.stcd,
|
||
(newStcd, oldStcd) => {
|
||
// 非qxd模式才监听store的stcd变化
|
||
if (!isQxdMode.value && newStcd && newStcd !== oldStcd) fetchData();
|
||
}
|
||
);
|
||
|
||
// qxd模式下监听props.stcd变化
|
||
watch(
|
||
() => props.stcd,
|
||
(newStcd, oldStcd) => {
|
||
if (isQxdMode.value && newStcd && newStcd !== oldStcd) {
|
||
hasLoaded.value = false;
|
||
initData();
|
||
}
|
||
}
|
||
);
|
||
|
||
// ResizeObserver
|
||
let resizeObserver: ResizeObserver | null = null;
|
||
|
||
watch(
|
||
() => props.isActive,
|
||
active => {
|
||
if (active && chartRef.value) {
|
||
setTimeout(() => {
|
||
if (chartInstance) chartInstance.resize();
|
||
}, 200);
|
||
if (!resizeObserver) {
|
||
resizeObserver = new ResizeObserver(() => {
|
||
if (chartInstance) chartInstance.resize();
|
||
});
|
||
resizeObserver.observe(chartRef.value);
|
||
}
|
||
}
|
||
}
|
||
);
|
||
|
||
const handleResize = () => {
|
||
if (chartInstance) chartInstance.resize();
|
||
};
|
||
|
||
onMounted(() => {
|
||
window.addEventListener('resize', handleResize);
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
if (chartInstance) {
|
||
chartInstance.dispose();
|
||
chartInstance = null;
|
||
}
|
||
window.removeEventListener('resize', handleResize);
|
||
if (resizeObserver) {
|
||
resizeObserver.disconnect();
|
||
resizeObserver = null;
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.water-temperature {
|
||
width: 100%;
|
||
min-height: 600px;
|
||
position: relative;
|
||
|
||
:deep(.ant-tabs-nav) {
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.search-bar {
|
||
width: 100%;
|
||
margin-bottom: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
|
||
.search-btn {
|
||
margin-left: 8px;
|
||
}
|
||
}
|
||
|
||
.tab-checkbox {
|
||
position: absolute;
|
||
top: 12px;
|
||
left: 30%;
|
||
z-index: 100;
|
||
}
|
||
|
||
.summary-tip-icon {
|
||
color: rgb(53, 169, 255);
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.tab-content {
|
||
width: 100%;
|
||
|
||
.content-body {
|
||
width: 100%;
|
||
display: flex;
|
||
height: 600px;
|
||
position: relative;
|
||
|
||
.chart-wrapper {
|
||
flex: 1;
|
||
height: 100%;
|
||
position: relative;
|
||
box-sizing: border-box;
|
||
padding-top: 10px;
|
||
|
||
.chart-container {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
.chart-empty {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
.table-wrapper {
|
||
flex: 1;
|
||
min-width: 500px;
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
padding: 10px;
|
||
overflow: hidden;
|
||
overflow-x: auto;
|
||
border-left: 1px solid #dcdfe6;
|
||
margin-left: 10px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.summary-row {
|
||
background-color: #fafafa;
|
||
}
|
||
</style>
|