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

835 lines
23 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>
</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-es'
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>