WholeProcessPlatform/frontend/src/modules/monthlyAvgWaterTemCompareHistory/monthlyAverage.vue

916 lines
21 KiB
Vue
Raw Normal View History

2026-05-26 19:30:22 +08:00
<template>
<div class="monthly-average-container">
<!-- 搜索表单 -->
<a-form
ref="formRef"
:model="formValue"
layout="inline"
class="search-form"
>
<a-form-item label="基地名称" name="dataDimensionVal">
<a-select
v-model:value="formValue.dataDimensionVal"
placeholder="请选择"
style="width: 200px"
@change="handleBaseChange"
>
<a-select-option
v-for="item in jdList"
:key="item.baseid"
:value="item.baseid"
>
{{ item.basename }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="所在流域" name="rvcd">
<a-select
v-model:value="formValue.rvcd"
placeholder="请选择"
style="width: 200px"
:disabled="!formValue.dataDimensionVal"
@change="handleRvcdChange"
>
<a-select-option
v-for="item in lyList"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="断面名称" name="stcd">
<a-select
v-model:value="formValue.stcd"
placeholder="请选择"
style="width: 200px"
:disabled="!formValue.rvcd"
>
<a-select-option
v-for="item in dmList"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="月份" name="tm">
<a-date-picker
v-model:value="formValue.tm"
picker="month"
:disabled-date="disabledDate"
format="YYYY-MM"
value-format="YYYY-MM-DD HH:mm:ss"
:allow-clear="false"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch"> 查询 </a-button>
<a-button :loading="exportLoading" @click="handleExport">
导出
</a-button>
</a-space>
</a-form-item>
</a-form>
<div style="width: 100%; display: flex">
<!-- 图表容器始终存在不再使用 v-if -->
<div class="chart-wrapper" ref="chartRef"></div>
<BasicTable
ref="tableRef"
:scrollY="460"
:columns="columns"
:list-url="DetGetKendoListCust"
:search-params="{ sort: sort }"
:transform-data="customTransform"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a @click="handleViewDetail(record)" class="text_hocer">查看详情</a>
</template>
</template>
</BasicTable>
2026-05-26 19:30:22 +08:00
</div>
</div>
2026-05-26 19:30:22 +08:00
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted, onBeforeUnmount } from 'vue';
import { message } from 'ant-design-vue';
import dayjs, { Dayjs } from 'dayjs';
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts';
import moment from 'moment';
import { omit } from 'lodash';
import { wbsbGetKendoList, getVmsstbprpt, DetGetKendoListCust } from '@/api/sw';
2026-05-26 19:30:22 +08:00
import BasicTable from '@/components/BasicTable/index.vue';
import { useModelStore } from '@/store/modules/model';
2026-05-26 19:30:22 +08:00
const modelStore = useModelStore();
// ==================== Props 定义 ====================
interface Props {
tm?: string;
dataDimensionVal?: string;
rvcd?: string;
stcd?: string;
jdList?: any[];
2026-05-26 19:30:22 +08:00
}
const props = withDefaults(defineProps<Props>(), {
tm: '',
dataDimensionVal: '',
rvcd: '',
stcd: '',
jdList: () => []
});
2026-05-26 19:30:22 +08:00
// ==================== 类型定义 ====================
interface DefautState {
dataDimensionType: string;
dataDimensionVal: any;
tm: string | Dayjs;
startDt: string;
endDt: string;
stcd: string;
rvcd: string;
2026-05-26 19:30:22 +08:00
}
interface OptionItem {
label: string;
value: string;
2026-05-26 19:30:22 +08:00
}
// ==================== 响应式状态 ====================
const formRef = ref();
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
2026-05-26 19:30:22 +08:00
const chartLoading = ref(false);
const tableLoading = ref(false);
const exportLoading = ref(false);
2026-05-26 19:30:22 +08:00
const tableRef = ref<any>(null);
const sort = [
{
field: 'dt',
dir: 'desc'
}
];
2026-05-26 19:30:22 +08:00
// 下拉列表
const lyList = ref<OptionItem[]>([]);
const dmList = ref<OptionItem[]>([]);
2026-05-26 19:30:22 +08:00
// 表单值
const formValue = reactive<DefautState>({
dataDimensionType: 'hyBase',
dataDimensionVal: props.dataDimensionVal || 'all',
tm: props.tm ? dayjs(props.tm) : dayjs(),
startDt: '',
endDt: '',
stcd: props.stcd || '',
rvcd: props.rvcd || ''
});
2026-05-26 19:30:22 +08:00
// 表格数据
const tableData = ref<any[]>([]);
2026-05-26 19:30:22 +08:00
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
showTotal: (total: number) => `${total}`
});
2026-05-26 19:30:22 +08:00
// 图表数据
const optionData = ref<any[]>([]);
2026-05-26 19:30:22 +08:00
// ==================== 工具函数 ====================
/**
* 单位转换
*/
const transUnit = (value: any, code: string, type: string): string => {
if (value === null || value === undefined) return '-';
const numValue = Number(value);
if (isNaN(numValue)) return '-';
return numValue.toFixed(2);
};
2026-05-26 19:30:22 +08:00
/**
* 获取单位配置
*/
const getUnitConfigByCode = (code: string, type: string): { unit: string } => {
return { unit: '℃' };
};
2026-05-26 19:30:22 +08:00
/**
* 获取颜色配置
*/
const getColorByCodeAndType = (code: string[], typeKey: string[]): string[] => {
// 实测值(蓝)、去年同期(紫)、天然(绿)
return ['#5470c6', '#9b59b6', '#91cc75'];
};
2026-05-26 19:30:22 +08:00
/**
* 日期格式化
*/
const tmFmt = (d: string) => moment(d).format('YYYY-MM-DD');
2026-05-26 19:30:22 +08:00
/**
* 格式化数值保留一位小数四舍五入空值显示 -
*/
const formatNumber = (value: any): string => {
if (value === null || value === undefined || value === '') return '-';
const numValue = Number(value);
if (isNaN(numValue)) return '-';
return numValue.toFixed(1);
};
2026-05-26 19:30:22 +08:00
/**
* 格式化日期为 YYYY-MM-DD 格式
*/
const formatDate = (dateStr: string): string => {
if (!dateStr) return '-';
try {
return dayjs(dateStr).format('YYYY-MM-DD');
} catch (error) {
return dateStr;
}
};
2026-05-26 19:30:22 +08:00
/**
* 计算变化趋势实测值 - 去年同期值
*/
const calculateComparison = (record: any): string => {
const wt = record.wt;
const beforeWt = record.beforeWt;
if (
wt === null ||
wt === undefined ||
wt === '' ||
beforeWt === null ||
beforeWt === undefined ||
beforeWt === ''
) {
return '-';
}
const wtNum = Number(wt);
const beforeWtNum = Number(beforeWt);
if (isNaN(wtNum) || isNaN(beforeWtNum)) return '-';
const diff = wtNum - beforeWtNum;
return diff.toFixed(1);
};
2026-05-26 19:30:22 +08:00
/**
* 禁用未来日期
*/
const disabledDate = (current: Dayjs) => {
return current && current > dayjs().endOf('day');
};
2026-05-26 19:30:22 +08:00
// ==================== API 调用 ====================
/**
* 获取流域下拉列表
*/
const fetchLyData = async () => {
try {
console.log('获取流域列表baseid:', formValue.dataDimensionVal);
let params = {
filter: {
logic: 'and',
filters: [
{
field: 'wbsType',
operator: 'eq',
value: 'PSB_RVCD'
},
formValue.dataDimensionVal != 'all'
? {
field: 'fullPath',
operator: 'startswith',
value: formValue.dataDimensionVal
}
: null
].filter(Boolean)
},
group: [
{ dir: 'asc', field: 'orderIndex' },
{ dir: 'asc', field: 'wbsCode' },
{ dir: 'asc', field: 'wbsName' },
{ dir: 'asc', field: 'objid' }
],
groupResultFlat: true
};
const res = await wbsbGetKendoList(params);
let data = res?.data?.data || res?.data || [];
if (data.length > 0) {
const options = data.map((item: any) => ({
label: item.wbsName,
value: item.wbsCode
}));
lyList.value = options;
const rvcd =
options.find((i: any) => i.value === props.rvcd)?.value ||
options[0]?.value;
formValue.rvcd = rvcd;
await fetchDmData();
} else {
lyList.value = [];
formValue.rvcd = '';
await fetchDmData();
2026-05-26 19:30:22 +08:00
}
} catch (error) {
// 忽略错误
}
};
2026-05-26 19:30:22 +08:00
/**
* 获取断面下拉列表
*/
const fetchDmData = async () => {
try {
console.log(
'获取断面列表baseid:',
formValue.dataDimensionVal,
'rvcd:',
formValue.rvcd
);
let params = {
filter: {
logic: 'and',
filters: [
{ field: 'sttpCode', operator: 'eq', value: 'WTRV' },
{ field: 'mway', operator: 'eq', value: 2 },
formValue.dataDimensionVal == 'all'
? {
field: 'rvcd',
operator: 'contains',
value: formValue.rvcd
}
: {
field: 'baseId',
operator: 'contains',
value: formValue.dataDimensionVal
}
]
},
select: ['stcd', 'stnm', 'lgtd', 'lttd', 'rstcdStepSort'],
sort: [{ field: 'rstcdStepSort', dir: 'desc' }]
};
const res = await getVmsstbprpt(params);
let resData = res?.data?.data || res?.data || [];
const options = resData.map((item: any) => ({
label: item.stnm,
value: item.stcd
}));
dmList.value = options;
const stcd =
options.find((i: any) => i.value === props.stcd)?.value ||
options[0]?.value;
formValue.stcd = stcd || '';
handleSearch();
} catch (error) {
console.error('获取断面列表失败:', error);
message.error('获取断面列表失败');
}
};
2026-05-26 19:30:22 +08:00
/**
* 获取图表数据
*/
const getChartData = async (value: any) => {
// 显示 loading
if (chartInstance) {
chartInstance.showLoading('default', {
text: '加载中...',
color: '#5470c6',
textColor: '#000',
maskColor: 'rgba(255, 255, 255, 0.8)',
zlevel: 0
});
}
chartLoading.value = true;
try {
const tmValue = dayjs.isDayjs(value.tm) ? value.tm : dayjs(value.tm);
let params = {
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: value.stcd
},
{
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: tmValue.startOf('month').format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: tmValue.endOf('month').format('YYYY-MM-DD HH:mm:ss')
}
]
}
};
const res = await DetGetKendoListCust(params);
let data = res?.data?.data || res?.data || [];
if (data.length > 0) {
optionData.value = data;
updateChart(); // 正常绘制图表
} else {
optionData.value = [];
// 数据为空:清除图表并显示“暂无数据”
if (chartInstance) {
chartInstance.clear();
chartInstance.setOption({
title: {
show: true,
text: '暂无数据',
left: 'center',
top: 'center',
textStyle: {
color: '#999',
fontSize: 14
}
}
2026-05-26 19:30:22 +08:00
});
}
2026-05-26 19:30:22 +08:00
}
} catch (error) {
console.error('获取图表数据失败:', error);
message.error('获取图表数据失败');
if (chartInstance) {
chartInstance.clear();
chartInstance.setOption({
title: {
show: true,
text: '数据加载失败',
left: 'center',
top: 'center',
textStyle: { color: '#f00' }
2026-05-26 19:30:22 +08:00
}
});
2026-05-26 19:30:22 +08:00
}
} finally {
chartLoading.value = false;
if (chartInstance) {
chartInstance.hideLoading();
}
}
};
2026-05-26 19:30:22 +08:00
/**
* 获取表格数据
*/
const getTableData = async (value: any) => {
try {
const tmValue = dayjs.isDayjs(value.tm) ? value.tm : dayjs(value.tm);
const filter = {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: value.stcd
},
{
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: tmValue.startOf('month').format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: tmValue.endOf('month').format('YYYY-MM-DD HH:mm:ss')
2026-05-26 19:30:22 +08:00
}
]
};
tableRef.value?.getList(filter);
} catch (error) {
// 忽略错误
}
};
2026-05-26 19:30:22 +08:00
// ==================== 图表相关 ====================
/**
* 初始化图表
*/
const initChart = () => {
if (!chartRef.value) return;
chartInstance = echarts.init(chartRef.value);
};
2026-05-26 19:30:22 +08:00
/**
* 更新图表有数据时调用
*/
const updateChart = () => {
if (!chartInstance) return;
// 清除可能残留的“暂无数据”title
chartInstance.setOption({ title: { show: false } }, false);
const { unit } = getUnitConfigByCode('Other', 'ACTUALTEMP');
const legendData = ['实测值', '去年同期', '天然'];
const xData: any[] = [];
const actualData: (number | null)[] = [];
const lastData: (number | null)[] = [];
const naturalData: (number | null)[] = [];
optionData.value.forEach((item: any) => {
actualData.push(
item.wt === null
? null
: Number(transUnit(item.wt, 'Other', 'ACTUALTEMP'))
);
lastData.push(
item.beforeWt === null
? null
: Number(transUnit(item.beforeWt, 'Other', 'LASTTEMP'))
);
naturalData.push(
item.actualTemp === null
? null
: Number(transUnit(item.actualTemp, 'Other', 'NATURALTEMP'))
);
xData.push({ value: item.dt });
});
const code = ['Other'];
const typeKey = ['ACTUALTEMP', 'LASTTEMP', 'NATURALTEMP'];
const colors = getColorByCodeAndType(code, typeKey);
const option: EChartsOption = {
color: colors,
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
const a = params
.map((item: any) => {
const value =
item.value !== null && item.value !== undefined
? item.value
: '-';
return `<div>${item.marker}${item.seriesName}:&nbsp;&nbsp;&nbsp;&nbsp;<span style="float:right">${value} ${unit}</span></div>`;
})
.join('');
return `<h3 style="color:#FFF">${tmFmt(params[0].name)}</h3>${a}`;
}
},
legend: {
data: legendData
},
xAxis: {
type: 'category',
boundaryGap: false,
axisTick: { show: true },
data: xData,
axisLabel: {
formatter: (value: string) => tmFmt(value)
}
},
yAxis: {
type: 'value',
name: `水温(${unit})`,
scale: true,
axisLine: { show: true }
},
dataZoom: [
{
type: 'inside',
show: false,
xAxisIndex: [0],
yAxisIndex: [], // 不控制 Y 轴缩放
zoomOnMouseWheel: true,
moveOnMouseMove: true
}
],
series: [
{
name: legendData[0],
data: actualData,
type: 'line',
connectNulls: true,
smooth: true
},
{
name: legendData[1],
data: lastData,
type: 'line',
connectNulls: true,
smooth: true
},
{
name: legendData[2],
data: naturalData,
type: 'line',
connectNulls: true,
smooth: true
}
]
};
try {
chartInstance.setOption(option, true); // notMerge = true 确保完全替换
} catch (error) {
console.error('图表更新失败:', error);
}
};
2026-05-26 19:30:22 +08:00
// ==================== 事件处理 ====================
/**
* 基地变化
*/
const handleBaseChange = async (value: string) => {
formValue.dataDimensionVal = value;
formValue.rvcd = '';
formValue.stcd = '';
lyList.value = [];
dmList.value = [];
await fetchLyData();
};
2026-05-26 19:30:22 +08:00
/**
* 流域变化
*/
const handleRvcdChange = async (value: string) => {
formValue.rvcd = value;
formValue.stcd = '';
dmList.value = [];
await fetchDmData();
};
2026-05-26 19:30:22 +08:00
/**
* 搜索
*/
const handleSearch = async () => {
// if (!formValue.stcd) {
// message.error('请选择断面名称')
// return
// }
const tmValue = dayjs.isDayjs(formValue.tm)
? formValue.tm.format('YYYY-MM-DD 00:00:00')
: moment(formValue.tm).format('YYYY-MM-DD 00:00:00');
const values = {
...formValue,
tm: tmValue
};
await getChartData(values);
await getTableData(values);
};
2026-05-26 19:30:22 +08:00
/**
* 导出
*/
const handleExport = async () => {
exportLoading.value = true;
try {
const tmValue = dayjs.isDayjs(formValue.tm)
? formValue.tm.format('YYYY-MM-DD 00:00:00')
: moment(formValue.tm).format('YYYY-MM-DD 00:00:00');
const params = {
...buildFilterParams({ ...formValue, tm: tmValue }),
exportType: 'excel',
exportFileName: `月平均水温历史对比 ${moment().format(
'YYYY-MM-DD HH-mm-ss'
)}`
};
2026-05-26 19:30:22 +08:00
message.success('导出功能待实现');
console.log('导出参数:', params);
} catch (error) {
console.error('导出失败:', error);
message.error('导出失败');
} finally {
exportLoading.value = false;
}
};
2026-05-26 19:30:22 +08:00
/**
* 表格分页变化
*/
const handleTableChange = (pag: any) => {
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
handleSearch();
};
2026-05-26 19:30:22 +08:00
/**
* 构建表格过滤参数
*/
const buildFilterParams = (values: any) => {
let _p: any = { ...values, stcd: values?.stcd || props?.stcd };
if (_p.tm) {
const tmValue = dayjs.isDayjs(_p.tm) ? _p.tm : dayjs(_p.tm);
_p.startDt = [
{
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: tmValue.startOf('month').format('YYYY-MM-DD HH:mm:ss')
}
];
_p.endDt = [
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: tmValue.endOf('month').format('YYYY-MM-DD HH:mm:ss')
}
];
}
if (!_p?.stcd) {
_p.stcd = '';
}
return omit(_p, ['dataDimensionType', 'dataDimensionVal', 'tm', 'rvcd']);
};
2026-05-26 19:30:22 +08:00
// ==================== 表格列配置 ====================
const { unit } = getUnitConfigByCode('Other', 'ACTUALTEMP');
2026-05-26 19:30:22 +08:00
const columns = [
{
title: '日期',
dataIndex: 'dt',
key: 'dt',
width: 120,
customRender: ({ record }: any) => formatDate(record.dt)
},
{
title: '日均水温(℃)实测值',
dataIndex: 'wt',
key: 'wt',
width: 150,
customRender: ({ record }: any) => formatNumber(record.wt)
},
{
title: '日均水温(℃)去年同期',
dataIndex: 'beforeWt',
key: 'beforeWt',
width: 150,
customRender: ({ record }: any) => formatNumber(record.beforeWt)
},
{
title: '天然值',
dataIndex: 'actualTemp',
key: 'actualTemp',
width: 120,
customRender: ({ record }: any) => formatNumber(record.actualTemp)
},
{
title: `变化趋势(${unit}`,
key: 'comparison',
width: 150,
customRender: ({ record }: any) => calculateComparison(record)
},
{
title: '操作',
key: 'action',
width: 100,
align: 'center' as const,
fixed: 'right' as const
}
];
2026-05-26 19:30:22 +08:00
// ==================== 监听器 ====================
watch(
() => [props.tm, props.dataDimensionVal, props.rvcd, props.stcd],
() => {
if (props.tm) formValue.tm = dayjs(props.tm);
if (props.dataDimensionVal)
formValue.dataDimensionVal = props.dataDimensionVal;
if (props.rvcd) formValue.rvcd = props.rvcd;
if (props.stcd) formValue.stcd = props.stcd;
},
{ deep: true }
);
2026-05-26 19:30:22 +08:00
// ==================== 生命周期 ====================
onMounted(() => {
setTimeout(() => {
initChart();
}, 100);
2026-05-26 19:30:22 +08:00
fetchLyData();
2026-05-26 19:30:22 +08:00
window.addEventListener('resize', handleResize);
});
2026-05-26 19:30:22 +08:00
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
chartInstance?.dispose();
});
2026-05-26 19:30:22 +08:00
// ==================== 工具方法 ====================
const handleResize = () => {
chartInstance?.resize();
};
2026-05-26 19:30:22 +08:00
const customTransform = (res: any) => {
console.log('表格数据:', res);
return {
records: res?.data?.data || [],
total: res?.data?.total || 0
};
2026-05-26 19:30:22 +08:00
};
const handleViewDetail = (record: any) => {
let stnm = record.stnm;
dmList.value.forEach((element: any) => {
if (element.value == formValue.stcd) {
stnm = element.label;
}
});
modelStore.modalVisible = true;
modelStore.params.sttp = 'wt_point';
modelStore.title = stnm + '详情信息';
modelStore.params.stcd = formValue.stcd;
};
2026-05-26 19:30:22 +08:00
defineExpose({
handleSearch,
getChartData,
getTableData
});
2026-05-26 19:30:22 +08:00
</script>
<style scoped lang="scss">
.monthly-average-container {
width: 100%;
/* padding: 16px; */
2026-05-26 19:30:22 +08:00
.search-form {
margin-bottom: 16px;
/* padding: 16px; */
background: #fff;
border-radius: 4px;
}
.chart-wrapper {
width: 40%;
height: 600px;
background: #fff;
border-radius: 4px;
/* padding: 16px; */
box-sizing: border-box;
}
2026-05-26 19:30:22 +08:00
.table-wrapper {
flex: 7;
background: #fff;
border-radius: 4px;
/* padding: 16px; */
box-sizing: border-box;
}
2026-05-26 19:30:22 +08:00
.text_hocer {
color: #2f6b98;
2026-05-26 19:30:22 +08:00
&:hover {
color: #40a9ff;
2026-05-26 19:30:22 +08:00
}
}
2026-05-26 19:30:22 +08:00
}
:deep(.ant-spin-nested-loading) {
width: 100%;
2026-05-26 19:30:22 +08:00
}
</style>