WholeProcessPlatform/frontend/src/modules/rightSearchDrawer/monitoringTable/components/WaterTemperature.vue
2026-09-03 16:47:28 +08:00

551 lines
15 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="dateRange"
format="YYYY-MM-DD"
:show-time="showTimeConfig"
:allowClear="false"
:presets="DateSetting.RangeButton.month1"
:disabled-date="disabledDate"
size="small"
@change="handleSearch"
/>
</template>
<a-spin :spinning="isLoading" tip="加载中...">
<div class="monitor-chart">
<div ref="chartRef" class="chart-container"></div>
<a-empty
v-if="!chartData || chartData.length === 0"
description="暂无数据"
class="chart-empty"
/>
</div>
<div class="monitor-table">
<a-spin :spinning="tableLoading" tip="加载中...">
<BasicTable
ref="tableRef"
:scrollY="240"
:scrollX="tableScrollX"
:columns="currentColumns"
:data="tableData"
:paginationConfig="{
showSizeChanger: false,
showQuickJumper: false
}"
@sort-change="handleSortChange"
>
<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>
</a-spin>
</div>
</a-spin>
</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 { Tooltip } from 'ant-design-vue';
import { useUiStore } from '@/store/modules/ui';
import { getMonitorDataWaterTemp } from '@/api/mapModal';
import { useModelStore } from '@/store/modules/model';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { useServerSortTableData } from '@/hooks/useServerSortTableData';
import { DateSetting } from '@/utils/enumeration';
import { InfoCircleOutlined } from '@ant-design/icons-vue';
const uiStore = useUiStore();
const modelStore = useModelStore();
const isLoading = ref(false);
const chartData = ref<any[]>([]);
const tableRef = ref<any>();
// ==================== 图表 ====================
const chartRef = ref<HTMLElement>();
let chartInstance: echarts.ECharts | null = null;
let resizeObserver: ResizeObserver | null = null;
const initChart = () => {
if (!chartRef.value) return;
if (chartInstance) chartInstance.dispose();
chartInstance = echarts.init(chartRef.value);
updateChart(chartData.value);
};
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);
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
}
};
chartInstance.setOption(option, true);
};
// 数据变化时动态刷新图表
watch(chartData, newData => {
updateChart(newData);
});
const handleResize = () => {
if (chartInstance) chartInstance.resize();
};
onMounted(() => {
nextTick(() => {
initChart();
setTimeout(() => {
if (chartInstance) chartInstance.resize();
}, 200);
if (!resizeObserver && chartRef.value) {
resizeObserver = new ResizeObserver(() => {
if (chartInstance) chartInstance.resize();
});
resizeObserver.observe(chartRef.value);
}
});
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', handleResize);
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
});
// 时间选择器配置
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 dateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
// ==================== 表格列配置 ====================
const baseWaterTempColumns = [
{
title: '时间',
dataIndex: 'tm',
fixed: 'left',
sort: true,
customRender: ({ text }: any) =>
text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-'
},
{
title: '水温(°C)',
dataIndex: 'wt',
summary: true,
sort: true,
customRender: ({ text }: any) =>
text !== undefined && text !== null ? Number(text).toFixed(1) : '-'
}
];
const currentColumns = baseWaterTempColumns;
const tableScrollX = computed(() => {
const columns = currentColumns;
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'
};
const summaryColumns = computed(() => {
return currentColumns
.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;
});
// ==================== 数据请求 ====================
// 构造表格/图表请求参数stcd + 时间范围),排序传入时叠加 sort
const buildSearchParams = (sort?: { field: string; dir: 'asc' | 'desc' }) => {
const stcd = modelStore.selectedAnchorPoint?.stcd;
if (!dateRange.value || !stcd) return null;
return {
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
},
{
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: sort ? [sort] : [{ field: 'tm', dir: 'asc' }]
};
};
// data 模式表格:点击排序时重新请求接口(服务端排序)
const {
tableData,
isLoading: tableLoading,
handleSortChange
} = useServerSortTableData<any>({
request: getMonitorDataWaterTemp,
buildSearchParams
});
const fetchData = async () => {
const params = buildSearchParams();
if (!params) return;
isLoading.value = true;
try {
const res = await getMonitorDataWaterTemp(params);
const rawData = res?.data?.data || res?.data?.records || [];
chartData.value = rawData;
tableData.value = [...rawData].reverse();
} catch (error) {
console.error('获取数据失败:', error);
chartData.value = [];
tableData.value = [];
} finally {
isLoading.value = false;
}
};
// 时间选择器 change 触发查询
const handleSearch = () => fetchData();
watch(
() => uiStore.searchDrawerOpen,
newVal => {
if (!newVal) {
dateRange.value = initDateRange();
}
}
);
// 选中点位变化时刷新;初始已有点位时立即加载
watch(
() => modelStore.selectedAnchorPoint?.stcd,
(newStcd, oldStcd) => {
if (!newStcd) return;
if (newStcd !== oldStcd) fetchData();
},
{ immediate: true }
);
// 全局时间范围变化时同步本地日期选择器(默认参数联动)
watch(
() => modelStore.filter.rangeTm,
newRange => {
if (Array.isArray(newRange) && newRange.length > 0) {
dateRange.value = [dayjs(newRange[0]), dayjs(newRange[1])];
}
},
{ deep: true }
);
</script>
<style lang="scss" scoped>
.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%);
}
.monitor-table {
margin-top: 12px;
border-radius: 2px;
box-sizing: border-box;
.table-container {
border: 1px solid #dcdfe6;
}
:deep(.summary-tip-icon) {
color: rgb(53, 169, 255);
margin-left: 4px;
}
}
.summary-row {
background-color: #fafafa;
}
</style>