WholeProcessPlatform/frontend-sjgl/src/views/monitorData/VerticalTemp/index.vue

751 lines
18 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>
<div class="w-full h-full flex flex-col pt-[20px] pl-[20px] body_one">
<!-- 搜索组件 -->
<VerticalTempSearch
@export-btn="exportBtn"
@search-finish="onSearchFinish"
@reset="onReset"
@seeEdit="handleSeeEdit"
@batch-delete="handleBatchDelete"
:selected-count="currentSearchParams.timeScale === 'tm' ? selectedRowsForChart.length : 0"
:exportLoading="exportLoading"
ref="searchRef"
/>
<!-- 垂向水温列勾选 -->
<div class="tab-checkbox-cxsw px-2">
<div class="checkbox-title">水深:</div>
<div class="checkbox-group">
<a-checkbox
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@change="handleSelectAllToggle"
class="select-all-checkbox"
>
全选
</a-checkbox>
<a-checkbox-group
v-model:value="selectedColumns"
:options="checkboxOptions"
@change="handleCheckboxChange"
/>
</div>
</div>
<!-- 图表和表格区域 -->
<div class="w-full h-full flex-1 flex overflow-hidden px-2 pb-2">
<!-- 左侧图表 -->
<div class="w-[50%] h-full relative">
<div ref="chartRef" class="w-full h-full"></div>
<a-empty
v-if="!chartData || chartData.length === 0"
description="暂无数据"
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
/>
</div>
<!-- 右侧表格 -->
<div class="w-[50%] h-[100%] border-l border-[#dcdfe6] ml-2 pl-2 flex">
<BasicTable
ref="tableRef"
row-key="dt"
:scrollY="tableScrollY"
:scrollX="tableScrollX"
:columns="tableColumns"
:list-url="getVerticalList"
:transformData="transformData"
:searchParams="{
sort: sort
}"
:enable-row-selection="true"
:enable-row-highlight="false"
:min-selection-count="1"
@selection-change="handleCxswSelectionChange"
>
<template #action="{ record }">
<template v-if="currentSearchParams.timeScale === 'tm'">
<a-button
type="link"
class="text-[#2f6b98]"
size="small"
@click="handleEdit(record)"
>编辑</a-button
>
<a-button
type="link"
danger
size="small"
@click="handleDelete(record)"
>删除</a-button
>
</template>
</template>
</BasicTable>
</div>
</div>
<EditVerticalTempModal
v-model:open="editVisible"
:record="editRecord"
:time-scale="currentSearchParams.timeScale"
:visible-depths="selectedColumns"
@success="handleEditSuccess"
/>
<DeleteConfirmModal
ref="deleteModalRef"
:delete-fn="handleDeleteFn"
title="删除垂向水温数据"
label="数据"
@success="handleEditSuccess"
/>
<!-- 操作日志弹框 -->
<OperationLogModal
v-model:open="operationLogVisible"
:table-name="operationTableName"
/>
</div>
</template>
<script setup lang="ts">
import {
ref,
computed,
onMounted,
nextTick,
watch,
onBeforeUnmount
} from 'vue';
import dayjs from 'dayjs';
import * as echarts from 'echarts';
import VerticalTempSearch from './VerticalTempSearch.vue';
import EditVerticalTempModal from './EditVerticalTempModal.vue';
import DeleteConfirmModal from '@/views/DataQueryMenuModule/components/conventionalHydropower/BasicData/DeleteConfirmModal.vue';
import OperationLogModal from '@/components/OperationLogModal/index.vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { message } from 'ant-design-vue';
import { getVerticalList, deleteVerticalInfo } from '@/api/DataQueryMenuModule';
import { calcTableScrollY } from '@/utils/index';
const sort = ref<any>([
{
field: 'dt',
dir: 'asc'
}
]);
const tableRef = ref();
const searchRef = ref();
const tableScrollY = ref<string | number>(0);
const currentSearchParams = ref<any>({});
const exportLoading = ref(false);
const tableData = ref<any[]>([]);
const editVisible = ref(false);
const editRecord = ref<any>(null);
const deleteModalRef = ref();
const operationLogVisible = ref(false);
const operationTableName = 'SD_WTVT_R';
// 图表相关
const chartRef = ref<HTMLElement>();
let chartInstance: echarts.ECharts | null = null;
const chartData = ref<any[]>([]);
const selectedRowsForChart = ref<any[]>([]);
// 勾选相关
const columns = ref<any[]>([]);
const selectedColumns = ref<string[]>([]);
const checkboxOptions = ref<any[]>([]);
const isAllSelected = computed(() => {
return (
checkboxOptions.value.length > 0 &&
selectedColumns.value.length === checkboxOptions.value.length
);
});
const isIndeterminate = computed(() => {
const total = checkboxOptions.value.length;
const sel = selectedColumns.value.length;
return sel > 0 && sel < total;
});
const handleCheckboxChange = (values: string[]) => {
selectedColumns.value = values;
// 勾选变化时,根据当前选中的行重新渲染图表
if (selectedRowsForChart.value.length > 0) {
nextTick(() => {
updateChart(selectedRowsForChart.value);
});
}
};
const handleSelectAllToggle = (e: any) => {
if (e.target.checked) {
selectedColumns.value = checkboxOptions.value.map((opt: any) => opt.value);
} else {
selectedColumns.value = [];
}
// 全选/取消全选时,同步更新图表
if (selectedRowsForChart.value.length > 0) {
nextTick(() => {
updateChart(selectedRowsForChart.value);
});
}
};
// 表格列
const tableColumns = computed(() => {
const fixedCols = [
{
title: '测站名称',
dataIndex: 'stnm',
width: 150,
fixed: 'left'
},
{
title: '时间',
dataIndex: 'dt',
width: 160,
fixed: 'left',
customRender: ({ text }: any) =>
text
? dayjs(text).format(
currentSearchParams.value.timeScale === 'month'
? 'YYYY-MM'
: currentSearchParams.value.timeScale === 'dt'
? 'YYYY-MM-DD'
: 'YYYY-MM-DD HH:mm:ss'
)
: '-'
}
];
const dataCols = columns.value
.filter((col: any) => selectedColumns.value.includes(col.dataIndex))
.map((col: any) => ({
title: col.title,
dataIndex: col.dataIndex,
width: 120,
customRender: ({ record }: any) => {
const val = record.dataList?.[col.dataIndex];
return val !== undefined && val !== null
? parseFloat(val).toFixed(1)
: '-';
}
}));
const actionCols =
currentSearchParams.value.timeScale === 'tm'
? [
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 120
}
]
: [];
return [...fixedCols, ...dataCols, ...actionCols];
});
const tableScrollX = computed(() => {
const cols = tableColumns.value;
const total = cols.reduce((s, c) => s + (c.width || 100), 0);
return total > 600 ? total : undefined;
});
// 默认选中行
const defaultSelectedRowKeys = computed(() => {
if (tableData.value && tableData.value.length > 0) {
return [tableData.value[0].dt];
}
return [];
});
// 图表颜色
const COLORS = [
'#5470c6',
'#91cc75',
'#fac858',
'#ee6666',
'#73c0de',
'#3ba272',
'#fc8452',
'#9a60b4',
'#ea7ccc',
'#5470c6',
'#73c0de',
'#fac858',
'#91cc75',
'#ee6666',
'#3ba272',
'#fc8452',
'#9a60b4',
'#ea7ccc',
'#5470c6',
'#73c0de'
];
// 更新图表
const updateChart = (selectedRows: any[]) => {
if (!chartInstance) return;
if (!selectedRows || selectedRows.length === 0) {
chartInstance.clear();
return;
}
const series = selectedRows.map((row, index) => {
const data = Object.entries(row.dataList || {})
.filter(([depth]) => selectedColumns.value.includes(depth))
.map(([depth, temp]) => [parseFloat(temp as string), parseFloat(depth)])
.sort((a, b) => a[1] - b[1]);
return {
name: dayjs(row.dt).format(
currentSearchParams.value.timeScale === 'month'
? 'YYYY-MM'
: currentSearchParams.value.timeScale === 'dt'
? 'YYYY-MM-DD'
: 'YYYY-MM-DD HH:mm'
),
type: 'line',
data,
smooth: false,
symbol: 'circle',
symbolSize: 4,
lineStyle: { width: 2, color: COLORS[index % COLORS.length] },
itemStyle: { color: COLORS[index % COLORS.length] }
};
});
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
formatter: (value: any) => {
if (value.axisDimension == 'x') {
return value.value.toFixed(1) + '℃';
} else if (value.axisDimension == 'y') {
return value.value.toFixed(2) + 'm';
}
return value.value;
}
}
},
formatter: (params: any) => {
let html = params[0].seriesName + '\n';
params.forEach((item: any) => {
html += `<div style="display:flex;"><div style="width:40px;">${item.data[1]}m :</div> <div>${item.data[0]}℃</div></div>`;
});
return html;
}
},
legend: {
type: 'scroll',
top: 40,
data: series.map(s => s.name),
textStyle: { fontSize: 12 }
},
grid: {
left: '8%',
right: 70,
top: '15%',
bottom: '8%',
containLabel: false
},
xAxis: {
position: 'top',
name: '水温 (℃)',
nameLocation: 'end',
type: 'value',
scale: true,
boundaryGap: ['10%', '10%'],
axisLabel: {
fontSize: 12,
color: '#000000',
formatter: function (value) {
return value.toFixed(1) + '℃';
}
},
splitLine: { show: true }
},
yAxis: {
name: '水深 (m)',
type: 'value',
inverse: true,
nameLocation: 'end',
min: 0,
axisLabel: {
fontSize: 12,
color: '#000000',
formatter: function (value) {
return value + 'm';
}
},
splitLine: { show: true }
},
series,
toolbox: {
show: true,
feature: {
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
},
right: 20,
top: 10
},
dataZoom: {
type: 'inside',
xAxisIndex: [0],
throttle: 50,
start: 0,
end: 100
}
};
chartInstance.setOption(option, true);
};
// 表格行勾选变化
const handleCxswSelectionChange = (
selectedRowKeys: string[],
selectedRows: any[]
) => {
selectedRowsForChart.value = selectedRows;
updateChart(selectedRows);
};
// 搜索完成
const onSearchFinish = async (values: any) => {
currentSearchParams.value = values;
await nextTick();
initTable(values);
};
const onReset = async (values: any) => {
currentSearchParams.value = values;
await nextTick();
initTable(values);
};
const exportBtn = () => {
if (exportLoading.value) return;
exportLoading.value = true;
searchRef.value.btnLoading = true;
tableRef.value
.exportTable({
fileName: `垂向水温数据_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
searchRef.value.btnLoading = false;
});
};
const handleEdit = (record: any) => {
editRecord.value = { ...record };
editVisible.value = true;
};
// 删除兼容单条record 本身批量record.records 数组)
const handleDeleteFn = async (record: any, reason: string) => {
const list = Array.isArray(record.records) ? record.records : [record];
const dataList = list.map((r: any) => ({
id: r.stcd ?? r.id,
tm: r.tm ?? r.dt
}));
return deleteVerticalInfo({
dataList,
dataType: 'TIME',
source: reason
});
};
// 删除后刷新:清空选中(让批量删除按钮回到禁用状态)并重新加载列表
const refreshAfterDelete = () => {
selectedRowsForChart.value = [];
tableRef.value?.clearSelection();
initTable(currentSearchParams.value);
};
const handleDelete = (record: any) => {
deleteModalRef.value?.open(record, () => {
refreshAfterDelete();
});
};
// 批量删除(仅小时 tm 模式,与单条删除一致)
const handleBatchDelete = () => {
if (currentSearchParams.value.timeScale !== 'tm') {
message.info('仅小时数据支持删除');
return;
}
if (selectedRowsForChart.value.length === 0) {
message.info('请先勾选要删除的数据');
return;
}
deleteModalRef.value?.open(
{
records: [...selectedRowsForChart.value],
stnm: `选中的 ${selectedRowsForChart.value.length} 条数据`
},
() => {
refreshAfterDelete();
}
);
};
// 查看修改(操作日志)
const handleSeeEdit = () => {
operationLogVisible.value = true;
};
const handleEditSuccess = () => {
initTable(currentSearchParams.value);
};
const initTable = (values: any) => {
const filters = [
values.rvcd && values.rvcd !== 'all'
? {
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: values.rvcd
}
: null,
values.rstcd
? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: values.rstcd
}
: null,
values.stcd
? {
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: values.stcd
}
: null
].filter(Boolean);
if (values.timeScale == 'tm' || values.timeScale == 'dt') {
filters.push(
{
field: 'drtp',
operator: 'eq',
dataType: 'string',
value: values.timeScale == 'tm' ? 'HOUR' : 'DAY'
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: dayjs(values.jcdt.min).format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: dayjs(values.jcdt.max).format('YYYY-MM-DD HH:mm:ss')
}
);
} else if (values.timeScale == 'month') {
filters.push(
{
field: 'drtp',
operator: 'eq',
dataType: 'string',
value: 'MON'
},
{
field: 'startYear',
operator: 'gte',
dataType: 'date',
value: dayjs(values.jcdt.min).format('YYYY')
},
{
field: 'startMonth',
operator: 'gte',
dataType: 'date',
value: dayjs(values.jcdt.min).format('MM')
},
{
field: 'endYear',
operator: 'lte',
dataType: 'date',
value: dayjs(values.jcdt.max).format('YYYY')
},
{
field: 'endMonth',
operator: 'lte',
dataType: 'date',
value: dayjs(values.jcdt.max).format('MM')
}
);
}
const params = {
logic: 'and',
filters
};
// 重置勾选和图表状态
selectedColumns.value = [];
checkboxOptions.value = [];
columns.value = [];
selectedRowsForChart.value = [];
if (chartInstance) chartInstance.clear();
tableRef.value.getList(params);
};
const transformData = (data: any) => {
const apiColumns = data?.data?.data?.[0]?.columns || [];
const dataSource = data?.data?.data?.[0]?.dataSource || [];
// 过滤出非固定列
const dynamicColumns = apiColumns.filter(
(item: any) => item.key !== 'stnm' && item.key !== 'dt'
);
columns.value = dynamicColumns;
checkboxOptions.value = dynamicColumns.map((col: any) => ({
label: col.key + 'm',
value: col.dataIndex
}));
selectedColumns.value = checkboxOptions.value.map((opt: any) => opt.value);
// 转换数据格式为 dataList 结构
const formattedData = dataSource
.map((row: any) => {
const dataList: any = {};
dynamicColumns.forEach((col: any) => {
dataList[col.dataIndex] = row.dataList[col.dataIndex];
});
return {
...row,
stnm: row.stnm,
dt: row.dt,
dataList
};
})
.reverse();
const total = data?.data?.total || 0;
tableData.value = formattedData;
// 初始化图表:默认选中第一条
if (formattedData.length > 0) {
selectedRowsForChart.value = [formattedData[0]];
nextTick(() => {
chartData.value = JSON.parse(JSON.stringify(formattedData));
updateChart([formattedData[0]]);
// 手动设置表格默认选中第一行
tableRef.value?.setSelectedRowKeys?.([formattedData[0].dt]);
});
} else {
chartData.value = [];
if (chartInstance) chartInstance.clear();
}
return {
records: formattedData,
total
};
};
// 窗口大小变化时重绘图表
const handleResize = () => {
if (chartInstance) chartInstance.resize();
};
// ResizeObserver
let resizeObserver: ResizeObserver | null = null;
onMounted(() => {
nextTick(() => {
tableScrollY.value = calcTableScrollY();
// 初始化图表
if (chartRef.value && !chartInstance) {
chartInstance = echarts.init(chartRef.value);
}
});
// 监听容器大小变化
if (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;
}
});
</script>
<style scoped lang="scss">
.tab-checkbox-cxsw {
display: flex;
gap: 8px;
padding-top: 8px;
padding-bottom: 8px;
.select-all-checkbox {
flex-shrink: 0;
}
.checkbox-title {
width: 60px;
flex-shrink: 0;
padding-right: 10px;
font-weight: 500;
white-space: nowrap;
text-align: right;
}
.checkbox-group {
max-height: 68px;
overflow-y: auto;
}
}
// 禁用表格行点击高亮样式
:deep(.ant-table-tbody .ant-table-row:hover > td) {
background-color: #ffffff !important;
}
:deep(.ant-table-tbody .ant-table-row-selected > td) {
background-color: #ffffff !important;
}
:deep(.ant-table-tbody .ant-table-row > td) {
cursor: default !important;
}
.body_one {
position: relative;
z-index: 900;
pointer-events: all;
}
</style>