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

916 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>
<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>
</div>
</div>
</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';
import BasicTable from '@/components/BasicTable/index.vue';
import { useModelStore } from '@/store/modules/model';
const modelStore = useModelStore();
// ==================== Props 定义 ====================
interface Props {
tm?: string;
dataDimensionVal?: string;
rvcd?: string;
stcd?: string;
jdList?: any[];
}
const props = withDefaults(defineProps<Props>(), {
tm: '',
dataDimensionVal: '',
rvcd: '',
stcd: '',
jdList: () => []
});
// ==================== 类型定义 ====================
interface DefautState {
dataDimensionType: string;
dataDimensionVal: any;
tm: string | Dayjs;
startDt: string;
endDt: string;
stcd: string;
rvcd: string;
}
interface OptionItem {
label: string;
value: string;
}
// ==================== 响应式状态 ====================
const formRef = ref();
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const chartLoading = ref(false);
const tableLoading = ref(false);
const exportLoading = ref(false);
const tableRef = ref<any>(null);
const sort = [
{
field: 'dt',
dir: 'desc'
}
];
// 下拉列表
const lyList = ref<OptionItem[]>([]);
const dmList = ref<OptionItem[]>([]);
// 表单值
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 || ''
});
// 表格数据
const tableData = ref<any[]>([]);
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
showTotal: (total: number) => `共 ${total} 条`
});
// 图表数据
const optionData = ref<any[]>([]);
// ==================== 工具函数 ====================
/**
* 单位转换
*/
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);
};
/**
* 获取单位配置
*/
const getUnitConfigByCode = (code: string, type: string): { unit: string } => {
return { unit: '℃' };
};
/**
* 获取颜色配置
*/
const getColorByCodeAndType = (code: string[], typeKey: string[]): string[] => {
// 实测值(蓝)、去年同期(紫)、天然(绿)
return ['#5470c6', '#9b59b6', '#91cc75'];
};
/**
* 日期格式化
*/
const tmFmt = (d: string) => moment(d).format('YYYY-MM-DD');
/**
* 格式化数值:保留一位小数,四舍五入,空值显示 -
*/
const formatNumber = (value: any): string => {
if (value === null || value === undefined || value === '') return '-';
const numValue = Number(value);
if (isNaN(numValue)) return '-';
return numValue.toFixed(1);
};
/**
* 格式化日期为 YYYY-MM-DD 格式
*/
const formatDate = (dateStr: string): string => {
if (!dateStr) return '-';
try {
return dayjs(dateStr).format('YYYY-MM-DD');
} catch (error) {
return dateStr;
}
};
/**
* 计算变化趋势:实测值 - 去年同期值
*/
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);
};
/**
* 禁用未来日期
*/
const disabledDate = (current: Dayjs) => {
return current && current > dayjs().endOf('day');
};
// ==================== 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();
}
} catch (error) {
// 忽略错误
}
};
/**
* 获取断面下拉列表
*/
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('获取断面列表失败');
}
};
/**
* 获取图表数据
*/
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
}
}
});
}
}
} catch (error) {
console.error('获取图表数据失败:', error);
message.error('获取图表数据失败');
if (chartInstance) {
chartInstance.clear();
chartInstance.setOption({
title: {
show: true,
text: '数据加载失败',
left: 'center',
top: 'center',
textStyle: { color: '#f00' }
}
});
}
} finally {
chartLoading.value = false;
if (chartInstance) {
chartInstance.hideLoading();
}
}
};
/**
* 获取表格数据
*/
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')
}
]
};
tableRef.value?.getList(filter);
} catch (error) {
// 忽略错误
}
};
// ==================== 图表相关 ====================
/**
* 初始化图表
*/
const initChart = () => {
if (!chartRef.value) return;
chartInstance = echarts.init(chartRef.value);
};
/**
* 更新图表(有数据时调用)
*/
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);
}
};
// ==================== 事件处理 ====================
/**
* 基地变化
*/
const handleBaseChange = async (value: string) => {
formValue.dataDimensionVal = value;
formValue.rvcd = '';
formValue.stcd = '';
lyList.value = [];
dmList.value = [];
await fetchLyData();
};
/**
* 流域变化
*/
const handleRvcdChange = async (value: string) => {
formValue.rvcd = value;
formValue.stcd = '';
dmList.value = [];
await fetchDmData();
};
/**
* 搜索
*/
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);
};
/**
* 导出
*/
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'
)}`
};
message.success('导出功能待实现');
console.log('导出参数:', params);
} catch (error) {
console.error('导出失败:', error);
message.error('导出失败');
} finally {
exportLoading.value = false;
}
};
/**
* 表格分页变化
*/
const handleTableChange = (pag: any) => {
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
handleSearch();
};
/**
* 构建表格过滤参数
*/
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']);
};
// ==================== 表格列配置 ====================
const { unit } = getUnitConfigByCode('Other', 'ACTUALTEMP');
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
}
];
// ==================== 监听器 ====================
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 }
);
// ==================== 生命周期 ====================
onMounted(() => {
setTimeout(() => {
initChart();
}, 100);
fetchLyData();
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
chartInstance?.dispose();
});
// ==================== 工具方法 ====================
const handleResize = () => {
chartInstance?.resize();
};
const customTransform = (res: any) => {
console.log('表格数据:', res);
return {
records: res?.data?.data || [],
total: res?.data?.total || 0
};
};
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;
};
defineExpose({
handleSearch,
getChartData,
getTableData
});
</script>
<style scoped lang="scss">
.monthly-average-container {
width: 100%;
/* padding: 16px; */
.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;
}
.table-wrapper {
flex: 7;
background: #fff;
border-radius: 4px;
/* padding: 16px; */
box-sizing: border-box;
}
.text_hocer {
color: #2f6b98;
&:hover {
color: #40a9ff;
}
}
}
:deep(.ant-spin-nested-loading) {
width: 100%;
}
</style>