WholeProcessPlatform/frontend/src/modules/rightSearchDrawer/monitoringTable/components/QXDInfo.vue
2026-08-11 16:24:41 +08:00

761 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<SidePanelItem title="水温监测" :shrink="false">
<template #title-right-content>
<a-range-picker
class="w-[220px]"
v-model:value="wtDateRange"
format="YYYY-MM-DD"
:show-time="showTimeConfig"
:allowClear="false"
:presets="DateSetting.RangeButton.month1"
:disabled-date="disabledDate"
size="small"
@change="fetchWtData"
/>
</template>
<div class="station-select-row">
测站
<a-select
v-model:value="wtSelectedStcd"
:options="wtStationOptions"
class="station-select"
placeholder="请选择站点"
@change="fetchWtData"
/>
</div>
<a-spin :spinning="wtLoading" tip="加载中...">
<div class="monitor-chart">
<div ref="wtChartRef" class="chart-container"></div>
<a-empty v-if="wtIsEmpty" description="暂无数据" class="chart-empty" />
</div>
</a-spin>
</SidePanelItem>
<SidePanelItem title="流量监测" :shrink="false">
<template #title-right-content>
<a-range-picker
class="w-[220px]"
v-model:value="llDateRange"
format="YYYY-MM-DD"
:show-time="showTimeConfig"
:allowClear="false"
:presets="DateSetting.RangeButton.month1"
:disabled-date="disabledDate"
size="small"
@change="fetchLlData"
/>
</template>
<div class="station-select-row">
测站:
<a-select
v-model:value="llSelectedStcd"
:options="llStationOptions"
class="station-select"
placeholder="请选择站点"
@change="fetchLlData"
/>
</div>
<a-spin :spinning="llLoading" tip="加载中...">
<div class="monitor-chart">
<div ref="llChartRef" class="chart-container"></div>
<a-empty v-if="llIsEmpty" description="暂无数据" class="chart-empty" />
</div>
</a-spin>
</SidePanelItem>
<SidePanelItem title="实时视频" :shrink="false">
<template #title-right-content> </template>
</SidePanelItem>
</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 { useUiStore } from '@/store/modules/ui';
import {
getMonitorDataWaterTemp,
getMonitorDataWt,
getMonitorDataZq
} from '@/api/mapModal';
import { getFlowStationList } from '@/api/DataQueryMenuModule';
import { useModelStore } from '@/store/modules/model';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { DateSetting } from '@/utils/enumeration';
const uiStore = useUiStore();
const modelStore = useModelStore();
// ==================== 时间选择器配置 ====================
const showTimeConfig = {
format: 'HH:mm',
hourStep: 1,
minuteStep: 5,
secondStep: 60
};
const disabledDate = (current: Dayjs) =>
current && current.isAfter(dayjs(), 'day');
const initDateRange = (): [Dayjs, Dayjs] => {
return [dayjs().subtract(1, 'month'), dayjs()];
};
// ==================== 水温图表 ====================
const wtLoading = ref(false);
const wtChartData = ref<any[]>([]);
const wtDateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
const wtChartRef = ref<HTMLElement>();
let wtChartInstance: echarts.ECharts | null = null;
let wtResizeObserver: ResizeObserver | null = null;
const wtIsEmpty = computed(() => {
const data = Array.isArray(wtChartData.value) ? wtChartData.value : [];
return data.length === 0;
});
const initWtChart = () => {
if (!wtChartRef.value) return;
if (wtChartInstance) wtChartInstance.dispose();
wtChartInstance = echarts.init(wtChartRef.value);
updateWtChart(wtChartData.value);
};
function wtShouldStagger(width: number, dataLen: number) {
return dataLen > Math.floor(width / 100);
}
function getWtAxisLabelConfig(chartWidth: number, xAxisData: string[]) {
const needStagger = wtShouldStagger(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 updateWtChart = (data: any[]) => {
if (!wtChartInstance) return;
if (!data || data.length === 0) {
wtChartInstance.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 = wtChartRef.value?.clientWidth || 0;
const axisLabelConfig = getWtAxisLabelConfig(width, xAxisData);
const 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: 40, right: 20, top: 44, bottom: 50 },
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
}
};
wtChartInstance.setOption(option, true);
};
// 数据变化时动态刷新水温图表
watch(wtChartData, newData => {
updateWtChart(newData);
});
const fetchWtData = async () => {
const stcd = wtSelectedStcd.value;
if (!wtDateRange.value || !stcd) return;
wtLoading.value = true;
try {
const filterParams = {
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: wtDateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: wtDateRange.value[1].format('YYYY-MM-DD HH:mm:ss')
}
]
},
sort: [{ field: 'tm', dir: 'asc' }]
};
const res = await getMonitorDataWaterTemp(filterParams);
wtChartData.value = res?.data?.data || res?.data?.records || [];
} catch (error) {
console.error('获取水温数据失败:', error);
wtChartData.value = [];
} finally {
wtLoading.value = false;
}
};
// ==================== 流量图表 ====================
const llLoading = ref(false);
const llChartData = ref<any[]>([]);
const llDateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
const llChartRef = ref<HTMLElement>();
let llChartInstance: echarts.ECharts | null = null;
let llResizeObserver: ResizeObserver | null = null;
const llIsEmpty = computed(() => {
const data = Array.isArray(llChartData.value) ? llChartData.value : [];
return data.length === 0;
});
const initLlChart = () => {
if (!llChartRef.value) return;
if (llChartInstance) llChartInstance.dispose();
llChartInstance = echarts.init(llChartRef.value);
updateLlChart(llChartData.value);
};
const updateLlChart = (data: any[]) => {
if (!llChartInstance) return;
if (!data || data.length === 0) {
llChartInstance.clear();
return;
}
const sorted = [...data].sort(
(a, b) => new Date(a.tm).getTime() - new Date(b.tm).getTime()
);
const xAxisData = sorted.map(item => {
const hm = dayjs(item.tm).format('HH:mm');
if (hm === '00:00') {
const date = dayjs(item.tm).format('MM-DD');
return `${hm}\n${date}`;
}
return hm;
});
const zData = sorted.map(item => item.z);
const qData = sorted.map(item => item.q);
const vData = sorted.map(item => item.v);
const formatRules: Record<
string,
{ format: (v: number) => string; unit: string }
> = {
水位: { format: v => v.toFixed(2), unit: '(m)' },
流量: { format: v => String(Math.round(v)), unit: '(m³/s)' },
流速: { format: v => v.toFixed(2), unit: '(m³/s)' }
};
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(50, 50, 50, 0.9)',
textStyle: { color: '#fff', fontSize: 12 },
axisPointer: { type: 'cross' },
formatter: (params: any) => {
if (!params || params.length === 0) return '';
const dataIndex = params[0].dataIndex;
const fullTime = sorted[dataIndex]?.tm
? dayjs(sorted[dataIndex].tm).format('YYYY-MM-DD HH:mm:ss')
: '';
let html = `<div style="font-size:16px;margin-bottom:8px;">${fullTime}</div>`;
params.forEach((param: any) => {
if (param.value == null) return;
const rule = Object.entries(formatRules).find(([key]) =>
param.seriesName.includes(key)
);
const displayValue = rule
? rule[1].format(Number(param.value))
: param.value;
const unit = rule ? rule[1].unit : '';
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:${param.color};margin-right:8px;"></span>
<span style="flex:1;font-size:14px;text-align:left;margin-right:6px;">${param.seriesName}: </span>
<span style="font-size:14px;min-width:60px;text-align:right;"><strong>${displayValue}</strong> ${unit}</span>
</div>
`;
});
return html;
}
},
legend: {
top: 0,
data: ['水位', '流量', '流速'],
textStyle: { fontSize: 12 },
selected: {
水位: true,
流量: true,
流速: true
}
},
grid: {
left: 60,
right: 100,
top: 60,
bottom: 50
},
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#000000' } },
axisTick: { show: false },
axisLabel: { fontSize: 12 },
splitLine: {
show: true,
lineStyle: { color: '#bfbfbf', type: 'solid' }
}
},
yAxis: [
{
name: '水位(m)',
type: 'value',
position: 'left',
axisLine: { show: true, lineStyle: { color: '#56C2E3' } },
axisLabel: { color: '#56C2E3' },
nameTextStyle: { color: '#56C2E3' },
alignTicks: true,
scale: true,
splitNumber: 9
},
{
name: '流量(m³/s)',
type: 'value',
position: 'right',
axisLine: { show: true, lineStyle: { color: '#9556A4' } },
axisLabel: { color: '#9556A4' },
nameTextStyle: { color: '#9556A4' },
alignTicks: true,
scale: true,
splitNumber: 9
},
{
name: '流速(m³/s)',
type: 'value',
position: 'right',
axisLine: { show: true, lineStyle: { color: '#78C300' } },
axisLabel: { color: '#78C300' },
nameTextStyle: { color: '#78C300' },
alignTicks: true,
scale: true,
splitNumber: 9,
offset: 60
}
],
series: [
{
name: '水位',
type: 'line',
yAxisIndex: 0,
data: zData,
smooth: true,
symbol: 'circle',
symbolSize: 4,
lineStyle: { color: '#56C2E3', width: 2 },
itemStyle: { color: '#56C2E3' }
},
{
name: '流量',
type: 'line',
yAxisIndex: 1,
data: qData,
smooth: true,
symbol: 'circle',
symbolSize: 4,
lineStyle: { color: '#9556A4', width: 2 },
itemStyle: { color: '#9556A4' }
},
{
name: '流速',
type: 'line',
yAxisIndex: 2,
data: vData,
smooth: true,
symbol: 'circle',
symbolSize: 4,
lineStyle: { color: '#78C300', width: 2 },
itemStyle: { color: '#78C300' }
}
],
dataZoom: [
{
type: 'inside',
xAxisIndex: [0],
throttle: 50,
start: 0,
end: 100
}
],
toolbox: {
show: true,
feature: {
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
},
right: 20,
top: -6
}
};
llChartInstance.setOption(option, true);
};
// 数据变化时动态刷新流量图表
watch(llChartData, newData => {
updateLlChart(newData);
});
const fetchLlData = async () => {
const stcd = llSelectedStcd.value;
if (!llDateRange.value || !stcd) return;
llLoading.value = true;
try {
const filterParams = {
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: llDateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: llDateRange.value[1].format('YYYY-MM-DD HH:mm:ss')
}
]
},
sort: [{ field: 'tm', dir: 'asc' }]
};
const res = await getFlowStationList(filterParams);
llChartData.value = res?.data?.data || res?.data?.records || [];
} catch (error) {
console.error('获取流量数据失败:', error);
llChartData.value = [];
} finally {
llLoading.value = false;
}
};
// ==================== 测站选择器(水温) ====================
const wtStationOptions = ref<{ label: string; value: string }[]>([]);
const wtSelectedStcd = ref<string>('');
let wtStationReqSeq = 0; // 请求序号,用于竞态守卫(仅最后一次请求生效)
const loadWtStationList = async () => {
const fhstcd = modelStore.selectedAnchorPoint?.stcd;
if (!fhstcd) return;
const requestSeq = ++wtStationReqSeq;
wtStationOptions.value = []; // 先清空,等待加载
wtSelectedStcd.value = ''; // 同步清空选中值,避免上一父站选中残留
try {
const res = await getMonitorDataWt({
filter: {
logic: 'and',
filters: [
{
field: 'fhstcd',
operator: 'eq',
dataType: 'string',
value: fhstcd
},
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'WTRV'
}
]
},
select: ['stcd', 'stnm']
});
if (requestSeq !== wtStationReqSeq) return; // 非最新请求,丢弃结果
const stations = res?.data?.data || res?.data?.records || [];
if (stations.length > 0) {
wtStationOptions.value = stations.map((item: any) => ({
label: item.stnm,
value: item.stcd
}));
wtSelectedStcd.value = stations[0].stcd;
fetchWtData();
}
} catch (error) {
if (requestSeq !== wtStationReqSeq) return; // 非最新请求,忽略错误
console.error('获取水温测站列表失败:', error);
}
};
// ==================== 测站选择器(流量) ====================
const llStationOptions = ref<{ label: string; value: string }[]>([]);
const llSelectedStcd = ref<string>('');
let llStationReqSeq = 0; // 请求序号,用于竞态守卫(仅最后一次请求生效)
const loadLlStationList = async () => {
const fhstcd = modelStore.selectedAnchorPoint?.stcd;
if (!fhstcd) return;
const requestSeq = ++llStationReqSeq;
llStationOptions.value = []; // 先清空,等待加载
llSelectedStcd.value = ''; // 同步清空选中值,避免上一父站选中残留
try {
const res = await getMonitorDataZq({
filter: {
logic: 'and',
filters: [
{
field: 'fhstcd',
operator: 'eq',
dataType: 'string',
value: fhstcd
},
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'ZQ'
}
]
},
select: ['stcd', 'stnm']
});
if (requestSeq !== llStationReqSeq) return; // 非最新请求,丢弃结果
const stations = res?.data?.data || res?.data?.records || [];
if (stations.length > 0) {
llStationOptions.value = stations.map((item: any) => ({
label: item.stnm,
value: item.stcd
}));
llSelectedStcd.value = stations[0].stcd;
fetchLlData();
}
} catch (error) {
if (requestSeq !== llStationReqSeq) return; // 非最新请求,忽略错误
console.error('获取流量测站列表失败:', error);
}
};
// ==================== 生命周期 ====================
const safeResize = (
chart: echarts.ECharts | null,
el: HTMLElement | undefined
) => {
if (!chart) return;
// 容器不可见display:none / 宽度为 0时跳过避免收起/切换时做昂贵的同步重排
if (!el || el.clientWidth === 0) return;
chart.resize();
};
const handleResize = () => {
safeResize(wtChartInstance, wtChartRef.value);
safeResize(llChartInstance, llChartRef.value);
};
onMounted(() => {
nextTick(() => {
initWtChart();
initLlChart();
setTimeout(() => {
handleResize();
}, 200);
if (!wtResizeObserver && wtChartRef.value) {
wtResizeObserver = new ResizeObserver(() =>
safeResize(wtChartInstance, wtChartRef.value)
);
wtResizeObserver.observe(wtChartRef.value);
}
if (!llResizeObserver && llChartRef.value) {
llResizeObserver = new ResizeObserver(() =>
safeResize(llChartInstance, llChartRef.value)
);
llResizeObserver.observe(llChartRef.value);
}
});
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
if (wtChartInstance) {
wtChartInstance.dispose();
wtChartInstance = null;
}
if (llChartInstance) {
llChartInstance.dispose();
llChartInstance = null;
}
window.removeEventListener('resize', handleResize);
if (wtResizeObserver) {
wtResizeObserver.disconnect();
wtResizeObserver = null;
}
if (llResizeObserver) {
llResizeObserver.disconnect();
llResizeObserver = null;
}
});
// ==================== 联动 ====================
watch(
() => uiStore.searchDrawerOpen,
newVal => {
if (!newVal) {
wtDateRange.value = initDateRange();
llDateRange.value = initDateRange();
}
}
);
// 选中点位变化时重新加载测站列表并刷新图表;初始已有点位时立即加载
watch(
() => modelStore.selectedAnchorPoint?.stcd,
(newStcd, oldStcd) => {
if (!newStcd) return;
if (newStcd !== oldStcd) {
loadWtStationList();
loadLlStationList();
}
},
{ immediate: true }
);
// 全局时间范围变化时同步本地日期选择器(默认参数联动)
watch(
() => modelStore.filter.rangeTm,
newRange => {
if (Array.isArray(newRange) && newRange.length > 0) {
const next = [dayjs(newRange[0]), dayjs(newRange[1])];
wtDateRange.value = next;
llDateRange.value = next;
}
},
{ deep: true }
);
</script>
<style lang="scss" scoped>
.station-select-row {
margin-bottom: 12px;
.station-select {
width: 200px;
}
}
.monitor-chart {
position: relative;
width: 100%;
height: 300px;
.chart-container {
width: 100%;
height: 100%;
}
}
.chart-empty {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>