This commit is contained in:
扈兆增 2026-06-03 15:03:53 +08:00
commit d984fcfbff
12 changed files with 2042 additions and 555 deletions

View File

@ -7,12 +7,12 @@ VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台'
VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/dev-api'
# 本地环境
VITE_APP_BASE_URL = 'http://localhost:8093'
# VITE_APP_BASE_URL = 'http://localhost:8093'
# 测试环境
# VITE_APP_BASE_URL = 'http://172.16.21.142:8093'
# VITE_APP_BASE_URL = 'http://172.16.21.142:8096'
# 汤伟
# VITE_APP_BASE_URL = 'http://10.84.121.21:8093'
VITE_APP_BASE_URL = 'http://10.84.121.21:8093'
# VITE_APP_BASE_URL = 'http://192.168.1.162:8093'
# 测试环境线上

View File

@ -2,7 +2,7 @@ import request from '@/utils/request';
//水质监测工作开展情况
export function msstbprptGetKendoList(data: any) {
return request({
url: '/api/dec-lygk-base-server/base/msstbprpt/GetKendoList',
url: '/wq/msstbprpt/GetKendoList',
method: 'post',
data
});
@ -18,7 +18,7 @@ export function vmsstbprptGetKendoList(data: any) {
//地表水水质达标率
export function wqGetKendoListCust(data: any) {
return request({
url: '/api/wmp-env-server/env/wq/GetKendoListCust',
url: '/wq/GetKendoListCust',
method: 'post',
data
});
@ -27,7 +27,7 @@ export function wqGetKendoListCust(data: any) {
//环境质量满足度 - 表格数据列表
export function wqGetKendoList(data: any) {
return request({
url: '/api/wmp-env-server/env/wq/GetKendoListCust',
url: '/wq/GetKendoListCust',
method: 'post',
data
});
@ -35,7 +35,7 @@ export function wqGetKendoList(data: any) {
//沿程水质变化_ 图表数据
export function qgcGetKendoListCust(data: any) {
return request({
url: '/api/wmp-env-server/env/wq/along/qgc/GetKendoListCust',
url: '/wq/along/qgc/GetKendoListCust',
method: 'post',
data
});
@ -43,7 +43,46 @@ export function qgcGetKendoListCust(data: any) {
//沿程水质变化_ 根据流域获取河段 https://211.99.26.225:12122
export function wbsbGetKendoList(data: any) {
return request({
url: '/api/dec-lygk-base-server/base/wbsb/GetKendoList',
url: '/wq/wbsb/GetKendoList',
method: 'post',
data
});
}
// /获取预警规则限值
export function ruleGetKendoListCust(data: any) {
return request({
url: '/wq/rule/GetKendoListCust',
method: 'post',
data
});
}
//获取数据范围
export function noAuthGetKendoListCust(data: any) {
return request({
url: '/wq/data/noAuth/GetKendoListCust',
method: 'post',
data
});
}
//获取要素配置
export function stbprpGetStbprpYsByStcd(data: any) {
return request({
url: '/wq/stbprp/GetStbprpYsByStcd',
method: 'get',
params:data
});
}
//表格数据获取函数
export function dataGetKendoListCust(data: any) {
return request({
url: '/wq/data/GetKendoListCust?calculateFlag=1',
method: 'post',
data
});
}
export function NodataGetKendoListCust(data: any) {
return request({
url: '/wq/data/GetKendoListCust',
method: 'post',
data
});

View File

@ -1043,13 +1043,7 @@ const wtPointColumns: Array<any> = [
];
//水质基础信息
const FhWpPointColumns: Array<any> = [
{
name: '基本属性',
visible: false,
ruleTips: '',
type: '',
url: ''
},
{
name: '测站名称',
filed: 'stnm',
@ -1066,7 +1060,7 @@ const FhWpPointColumns: Array<any> = [
},
{
name: '站址',
filed: 'addvcdName',
filed: 'stlc',
visible: true,
type: 'input',
url: '',
@ -1093,6 +1087,14 @@ const FhWpPointColumns: Array<any> = [
type: 'number',
toFixed: 6,
url: '',
},
{
name: '建成日期',
filed: 'jcdt',
visible: true,
type: 'date',
format: 'YYYY-MM-DD',
url: '',
},
{
name: '类别',
@ -1115,14 +1117,6 @@ const FhWpPointColumns: Array<any> = [
type: 'select',
url: '',
},
{
name: '',
filed: '',
visible: true,
type: '',
url: '',
},
];
//栖息地基础信息 fh_point
const FhPointColumns: Array<any> = [

View File

@ -668,6 +668,12 @@ const WQFBTabs: Array<any> = [
key: 'WaterQuality',
type: 'WaterQuality',
default: true // 默认显示
},
{
name: '实时视频',
key: 'videoInfo',
type: 'video',
url: '/video/dataStcdFrame/getVideoMonitorList'
}
]
// const WQTabs: Array<any> = [
@ -767,7 +773,7 @@ const DWTabs: Array<any> = [
key: 'basicInfo',
type: 'basic',
default: true, // 默认显示
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo'
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo'
},
// {
// name: '实时视频',

View File

@ -43,7 +43,8 @@
</div>
<div v-if="datetimePicker.show">
<!-- 添加 locale 属性来设置语言 -->
<a-date-picker v-model:value="datetimeValue" show-time
<a-date-picker v-model:value="datetimeValue"
:show-time="timePickerConfig"
:style="{ width: datetimePicker.picker === 'year' ? '80px' : datetimePicker.picker === 'month' ? '90px' : '130px' }"
:format="datetimePicker.format !== null ? datetimePicker.format : undefined"
:picker="datetimePicker.picker" :allowClear="false" placeholder=" "
@ -149,12 +150,15 @@ const props = defineProps({
})
},
datetimePicker: { //
type: Object as () => SelectConfig,
type: Object as () => SelectConfig & {
timeFormat?: string; //
},
default: () => ({
show: false,
value: undefined,
format: null, //YYYY-MM-DD HH
picker: 'date' //date | week | month | quarter | year
picker: 'date', //date | week | month | quarter | year
timeFormat: 'HH:mm:ss' //
})
},
scopeDate: { //
@ -481,6 +485,26 @@ const computedScopeDatePresets = computed(() => {
return presets;
});
/**
* 计算属性根据 datetimePicker timeFormat 配置生成时间选择器的配置对象
*/
const timePickerConfig = computed(() => {
if (!props.datetimePicker.show || props.datetimePicker.picker !== 'date') {
return true; //
}
// timeFormat使
if (props.datetimePicker.timeFormat) {
return {
format: props.datetimePicker.timeFormat,
hideDisabledOptions: true, //
defaultValue: dayjs().startOf('hour') //
};
}
return true; //
});
/**
* 处理树节点选择事件
* 由于父节点已设置 selectable: false这里只会接收到叶子节点
@ -542,6 +566,12 @@ const handleDateTimeChange = (date: any | null, dateString: string) => {
if (props.datetimePicker.picker === 'month' && date) {
lastMonthValue.value = dayjs(date).format('YYYY-MM');
}
// timeFormat 'HH' 00
if (props.datetimePicker.timeFormat === 'HH' && date) {
const normalizedDate = dayjs(date).minute(0).second(0);
datetimeValue.value = normalizedDate;
}
};
const handleTabClick = (value: string) => {

View File

@ -4,35 +4,19 @@
<a-form ref="formRef" :model="formState" layout="inline" class="search-form">
<a-space wrap>
<a-form-item label="所属基地" name="dataDimensionVal">
<a-select
v-model:value="formState.dataDimensionVal"
placeholder="请选择"
style="width: 120px"
show-search
:filter-option="filterBaseOption"
option-filter-prop="label"
@change="handleBaseChange"
>
<a-select-option
v-for="item in baseList"
:key="item.baseid"
:value="item.baseid"
:label="item.basename"
>
<a-select v-model:value="formState.dataDimensionVal" placeholder="请选择" style="width: 120px"
show-search :filter-option="filterBaseOption" option-filter-prop="label"
@change="handleBaseChange">
<a-select-option v-for="item in baseList" :key="item.baseid" :value="item.baseid"
:label="item.basename">
{{ item.basename }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="formState.dataDimensionVal === 'all'" label="测站类型" name="dtinType">
<a-select
v-model:value="formState.dtinType"
placeholder="请选择"
style="width: 100px"
show-search
:filter-option="filterOption"
option-filter-prop="label"
>
<a-form-item v-if="typeOne == 'all'" label="测站类型" name="dtinType">
<a-select v-model:value="formState.dtinType" placeholder="请选择" style="width: 120px" show-search
:filter-option="filterOption" option-filter-prop="label">
<a-select-option value="" label="全部">全部</a-select-option>
<a-select-option value="0" label="自建水质站">自建水质站</a-select-option>
<a-select-option value="1" label="国家水质站">国家水质站</a-select-option>
@ -40,28 +24,16 @@
</a-form-item>
<a-form-item label="时间维度" name="types">
<a-select
v-model:value="formState.types"
placeholder="请选择"
style="width: 80px"
show-search
:filter-option="filterOption"
option-filter-prop="label"
>
<a-select v-model:value="formState.types" placeholder="请选择" style="width: 80px" show-search
:filter-option="filterOption" option-filter-prop="label">
<a-select-option value="year" label="年度">年度</a-select-option>
<a-select-option value="month" label="月度">月度</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="达标状态" name="sfdb">
<a-select
v-model:value="formState.sfdb"
placeholder="请选择"
style="width: 100px"
show-search
:filter-option="filterOption"
option-filter-prop="label"
>
<a-select v-model:value="formState.sfdb" placeholder="请选择" style="width: 100px" show-search
:filter-option="filterOption" option-filter-prop="label">
<a-select-option value="" label="全部">全部</a-select-option>
<a-select-option value="0" label="不达标">不达标</a-select-option>
<a-select-option value="1" label="达标">达标</a-select-option>
@ -70,31 +42,14 @@
</a-form-item>
<a-form-item label="时间" name="dateValues">
<a-month-picker
v-if="formState.types === 'month'"
v-model:value="formState.dateValues"
format="YYYY-MM"
placeholder="请选择月份"
style="width: 160px"
:disabled-date="disabledMonthDate"
/>
<a-year-picker
v-else
v-model:value="formState.dateValues"
format="YYYY"
placeholder="请选择年份"
style="width: 160px"
:disabled-date="disabledYearDate"
/>
<a-date-picker v-model:value="formState.dateValues"
:format="formState.types === 'month' ? 'YYYY-MM' : 'YYYY'" :picker="formState.types"
placeholder="请选择月份" style="width: 140px" :disabled-date="disabledMonthDate" />
</a-form-item>
<a-form-item label="断面名称" name="stnm">
<a-input
v-model:value="formState.stnm"
placeholder="请输入断面名称"
style="width: 200px"
allow-clear
/>
<a-input v-model:value="formState.stnm" placeholder="请输入断面名称" style="width: 200px" allow-clear />
</a-form-item>
<a-form-item>
@ -120,21 +75,9 @@
<!-- 右侧表格区域 -->
<div class="table-section">
<BasicTable
ref="tableRef"
:scrollY="360"
:columns="columns"
:list-url="wqGetKendoList"
:search-params="{ sort: sortConfig }"
:transform-data="customTransform"
>
<BasicTable ref="tableRef" :scrollY="360" :columns="columns" :list-url="wqGetKendoList"
:search-params="{ sort: sortConfig }" :transform-data="customTransform">
<template #bodyCell="{ column, record }">
<!-- 断面名称列 - 可点击 -->
<template v-if="column.key === 'stnm'">
<a @click="handleViewStnmDetail(record)" class="text-link">
{{ record.stnm }}
</a>
</template>
<!-- 操作列 -->
<template v-if="column.key === 'action'">
<a @click="handleViewDetail(record)" class="text-link">
@ -147,53 +90,44 @@
</div>
<!-- 详情弹窗 -->
<a-modal
v-model:open="detailModalVisible"
:title="`${detailData.stnm || ''} 详细信息`"
width="80%"
:footer="null"
@cancel="handleDetailModalClose"
>
<div class="modal-content">
<!-- TODO: 这里放置详情组件待后续实现 -->
<p>详情内容待实现...</p>
<pre>{{ detailData }}</pre>
</div>
</a-modal>
<!-- 地图弹窗断面名称点击 -->
<a-modal
v-model:open="mapModalVisible"
:title="`${mapModalData.titleName || ''} 详情信息`"
width="80%"
:footer="null"
@cancel="handleMapModalClose"
>
<div class="modal-content">
<!-- TODO: 这里放置地图标签页组件待后续实现 -->
<p>地图详情内容待实现...</p>
<pre>{{ mapModalData }}</pre>
</div>
<a-modal v-model:open="detailModalVisible" :title="`${detailData.stnm || ''} 详细信息`" width="80%" :footer="null"
@cancel="handleDetailModalClose">
<HJMZDTwoLays
:tc="{
stcd: detailData.stcd || '',
startTime:detailData.tm ,
endTime: (typeof detailData.tm === 'string' ? detailData.tm.split(' ')[0] : dayjs(detailData.tm).format('YYYY-MM-DD')) + ' 23:59:59',
sfdb: formState.sfdb || ''
}"
:calculateFlag="{ calculateFlag: 0 }"
/>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import { ref, reactive, computed, onMounted, onBeforeUnmount, watch, nextTick, h } from 'vue'
import { message } from 'ant-design-vue'
import * as echarts from 'echarts'
import dayjs, { Dayjs } from 'dayjs'
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
import BasicTable from '@/components/BasicTable/index.vue'
import { wqGetKendoList } from '@/api/sz'
import { wqGetKendoList } from '@/api/sz'
import HJMZDTwoLays from "./HJMZDTwoLays.vue"
// dayjs
dayjs.extend(isSameOrAfter)
// Props
const props = defineProps<{
datas?: Array<{ baseid: string; basename: string }> //
type?: 'month' | 'year' //
type?: string //
modalData?: string //
dateValue?: string | Dayjs //
dataDimensionVal?: string //
buildType?: string | number //
typeOne: string //
baseid: string
}>()
// Refs
@ -203,19 +137,23 @@ const pieChartRef = ref<HTMLElement | null>(null)
let pieChartInstance: echarts.ECharts | null = null
// State
const baseid = ref(props.baseid || '')
const typeOne = ref(props.typeOne || '')
const originalMonth = ref<string>('') //
const formState = reactive({
dataDimensionVal: props.dataDimensionVal || '',
dtinType: '',
dtinType: props.buildType || '',
types: props.type || 'month',
sfdb: '',
dateValues: props.dateValue ? (typeof props.dateValue === 'string' ? dayjs(props.dateValue, props.type === 'month' ? 'YYYY-MM' : 'YYYY') : props.dateValue) : dayjs(),
stnm: ''
})
const kong = ref(true)
const baseList = computed(() => props.datas || [])
const pieLoading = ref(false)
const showPieChart = ref(true)
const pieCode = ref<string[]>([]) //
const displayTotal = ref<number>(0) //
//
const detailModalVisible = ref(false)
@ -230,17 +168,7 @@ const sortConfig = computed(() => [
{ field: 'tm', dir: 'desc' }
])
// URL
const getListUrl = async (params: any) => {
try {
const res = await wqGetKendoList(params)
return res
} catch (error) {
console.error('获取表格数据失败:', error)
message.error('获取表格数据失败')
return { data: { data: [], total: 0 } }
}
}
//
const customTransform = (res: any) => {
@ -250,6 +178,40 @@ const customTransform = (res: any) => {
}
}
//
const getStnmMergeInfo = (records: any[]) => {
if (!records || records.length === 0) return {}
const mergeInfo: Record<number, { rowSpan: number }> = {}
let i = 0
while (i < records.length) {
const currentStnm = records[i].stnm
let rowSpan = 1
//
let j = i + 1
while (j < records.length && records[j].stnm === currentStnm) {
rowSpan++
j++
}
// rowSpan
mergeInfo[i] = { rowSpan }
// rowSpan 0
for (let k = i + 1; k < j; k++) {
mergeInfo[k] = { rowSpan: 0 }
}
i = j
}
return mergeInfo
}
//
const columns = [
{
@ -273,15 +235,13 @@ const columns = [
title: '断面名称',
dataIndex: 'stnm',
key: 'stnm',
width: 150,
ellipsis: true
},
{
title: '断面编码',
dataIndex: 'stcd',
key: 'stcd',
width: 120,
visible: false
width: 180,
ellipsis: true,
customCell: (record: any, rowIndex: number) => {
const tableData = tableRef.value?.tableData || []
const mergeInfo = getStnmMergeInfo(tableData)
return mergeInfo[rowIndex] || { rowSpan: 1 }
}
},
{
title: '测站类型',
@ -293,7 +253,19 @@ const columns = [
title: '达标状态',
dataIndex: 'sfdbName',
key: 'sfdbName',
width: 120
width: 120,
customRender: ({ record }: any) => {
const sfdb = record.sfdbName
let color = '#000000'
if (sfdb === '达标') {
color = '#008000' // - 绿
} else if (sfdb === '不达标') {
color = '#FF0000' // -
}
return h('span', { style: { color } }, record.sfdbName || '-')
}
},
{
title: '水质要求',
@ -328,12 +300,6 @@ const disabledMonthDate = (current: Dayjs) => {
return current.isAfter(now, 'month')
}
//
const disabledYearDate = (current: Dayjs) => {
if (!current) return false
const now = dayjs()
return current.year() > now.year()
}
//
const filterBaseOption = (input: string, option: any) => {
@ -361,18 +327,12 @@ const fetchPieData = async () => {
//
const filters: any[] = [
{
formState.dataDimensionVal != 'all' ? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: formState.dataDimensionVal
},
{
field: 'type',
operator: 'eq',
dataType: 'string',
value: 'DAY'
},
value: typeOne.value == 'all' ? formState.dataDimensionVal : baseid.value
} : null,
formState.types === 'month' ? {
field: 'drMonth',
operator: 'eq',
@ -384,25 +344,36 @@ const fetchPieData = async () => {
dataType: 'string',
value: startTime
},
formState.dataDimensionVal !== 'all' ? {
{
field: 'TYPE',
operator: 'eq',
dataType: 'string',
value: 'DAY'
},
typeOne.value != 'all' ? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: formState.dataDimensionVal
} : null,
formState.sfdb ? {
pieCode.value.length > 0 ? {
field: 'sfdb',
operator: 'in',
dataType: 'string',
value: pieCode.value
} : (formState.sfdb ? {
field: 'sfdb',
operator: 'eq',
dataType: 'string',
value: formState.sfdb
} : null,
} : null),
formState.stnm ? {
field: 'stnm',
operator: 'contains',
dataType: 'string',
value: formState.stnm
} : null,
formState.dataDimensionVal == 'all' && formState.dtinType ? {
typeOne.value == 'all' && formState.dtinType != '' ? {
field: 'dtinType',
operator: 'eq',
dataType: 'string',
@ -414,7 +385,13 @@ const fetchPieData = async () => {
filter: {
logic: 'and',
filters
}
},
group: [
{
"dir": "des",
"field": "sfdb"
}
]
}
// API
@ -427,16 +404,30 @@ const fetchPieData = async () => {
return
}
//
const dataSource = data.map((item: any) => ({
name: item.keyName || 'null',
value: item.count,
key: item.keyName === '达标' ? 'DB_STATUS_1' : item.keyName === '不达标' ? 'DB_STATUS_0' : 'CURNODATA',
keyData: item?.key
}))
// -
const dataSource = data.map((item: any) => {
const name = item.keyName || 'null'
// -绿--
let color = '#d9d9d9'
if (name === '达标') {
color = '#52c41a' // 绿
} else if (name === '不达标') {
color = '#ff4d4f' //
}
// ""
const filteredDataSource = dataSource.filter(item => item.name !== '无数据')
return {
name: name,
value: item.count,
key: item.keyName === '达标' ? 'DB_STATUS_1' : item.keyName === '不达标' ? 'DB_STATUS_0' : 'CURNODATA',
keyData: item?.key,
itemStyle: {
color: color
}
}
})
// 使""
const filteredDataSource = dataSource
if (filteredDataSource.length === 0) {
showPieChart.value = false
@ -470,8 +461,9 @@ const initPieChart = (data: any[]) => {
pieChartInstance = echarts.init(pieChartRef.value)
//
// displayTotal
const total = data.reduce((sum, item) => sum + item.value, 0)
displayTotal.value = total
const option = {
tooltip: {
@ -486,34 +478,36 @@ const initPieChart = (data: any[]) => {
},
title: [
{
text: total.toString(),
left: '45%',
top: '50%',
text: displayTotal.value.toString(),
left: '39%',
top: '38%',
textAlign: 'center',
textStyle: {
fontSize: 20,
fontWeight: 'bold',
color: '#333'
color: '#2f6b98'
}
},
{
text: '监测总次数',
left: '45%',
top: '56%',
left: '39%',
top: '44%',
textAlign: 'center',
textStyle: {
fontSize: 12,
color: '#666'
fontSize: 13,
color: '#333333',
fontWeight: '500'
}
},
{
text: '(次)',
left: '45%',
top: '61%',
left: '39%',
top: '49%',
textAlign: 'center',
textStyle: {
fontSize: 10,
color: '#999'
fontSize: 13,
color: '#333333',
fontWeight: '500'
}
}
],
@ -521,8 +515,8 @@ const initPieChart = (data: any[]) => {
{
name: '断面总数量',
type: 'pie',
radius: ['20%', '35%'],
center: ['45%', '55%'],
radius: ['25%', '35%'],
center: ['40%', '45%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
@ -531,7 +525,7 @@ const initPieChart = (data: any[]) => {
},
label: {
show: true,
formatter: '{b}: {c} ({d}%)'
formatter: '{b}'
},
emphasis: {
label: {
@ -545,31 +539,39 @@ const initPieChart = (data: any[]) => {
]
}
//
const colorConfig: any = {
color: ['#52c41a', '#ff4d4f', '#d9d9d9']
}
pieChartInstance.setOption({ ...option, ...colorConfig })
pieChartInstance.setOption(option)
// -
pieChartInstance.on('legendselectchanged', (params: any) => {
kong.value = false
const selectedKeys = Object.keys(params.selected).filter(key => params.selected[key])
const allFalse = Object.values(params.selected).every(value => value === false)
// keyData
const dataMap: Record<string, string> = {
'达标': 'DB_STATUS_1',
'不达标': 'DB_STATUS_0',
'无数据': 'CURNODATA'
//
if (allFalse) {
// 0
displayTotal.value = 0
} else {
//
displayTotal.value = data
.filter(item => selectedKeys.includes(item.name))
.reduce((sum, item) => sum + item.value, 0)
}
// title
pieChartInstance.setOption({
title: [
{
text: displayTotal.value.toString()
}
]
})
const selectedCodes = data
.filter(item => selectedKeys.includes(item.name))
.map(item => item.keyData)
pieCode.value = allFalse ? ['5'] : selectedCodes
//
handleSearch()
})
@ -578,7 +580,6 @@ const initPieChart = (data: any[]) => {
//
const handleBaseChange = () => {
formState.stnm = ''
fetchPieData()
handleSearch()
}
@ -586,14 +587,17 @@ const handleBaseChange = () => {
const handleSearch = () => {
const timeFormat = formState.types === 'month' ? 'YYYY-MM' : 'YYYY'
const startTime = formState.dateValues ? formState.dateValues.format(timeFormat) : ''
// console.log( typeOne.value )
// console.log( formState.dtinType )
// console.log(typeOne.value == 'all' && formState.dtinType !='')
// debugger
const filters: any[] = [
{
formState.dataDimensionVal != 'all' ? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: formState.dataDimensionVal
},
value: typeOne.value == 'all' ? formState.dataDimensionVal : baseid.value
} : null,
formState.types === 'month' ? {
field: 'drMonth',
operator: 'eq',
@ -606,12 +610,12 @@ const handleSearch = () => {
value: startTime
},
{
field: 'type',
field: 'TYPE',
operator: 'eq',
dataType: 'string',
value: 'DAY'
},
formState.dataDimensionVal !== 'all' ? {
typeOne.value != 'all' ? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
@ -634,7 +638,7 @@ const handleSearch = () => {
dataType: 'string',
value: formState.stnm
} : null,
formState.dataDimensionVal === 'all' && formState.dtinType ? {
typeOne.value == 'all' && formState.dtinType != '' ? {
field: 'dtinType',
operator: 'eq',
dataType: 'string',
@ -649,6 +653,12 @@ const handleSearch = () => {
//
tableRef.value?.getList(filter)
if (kong.value) {
// debugger
fetchPieData()
}
kong.value = true
}
//
@ -661,7 +671,6 @@ const handleReset = () => {
formState.stnm = ''
pieCode.value = []
fetchPieData()
handleSearch()
}
@ -676,53 +685,161 @@ const handleDetailModalClose = () => {
detailModalVisible.value = false
detailData.value = {}
}
//
const handleViewStnmDetail = (record: any) => {
const timeRange = formState.dateValues
? [dayjs(formState.dateValues).startOf(formState.types === 'month' ? 'month' : 'year'),
dayjs(formState.dateValues).endOf(formState.types === 'month' ? 'month' : 'year')]
: []
mapModalData.value = {
sttp: 'wq',
stcd: record.stcd,
titleName: record.stnm,
timeRange,
calculateFlag: null // TODO:
}
mapModalVisible.value = true
}
//
const handleMapModalClose = () => {
mapModalVisible.value = false
mapModalData.value = {}
}
//
watch(
() => [formState.dataDimensionVal, formState.types, formState.sfdb, formState.stnm, formState.dtinType],
() => {
fetchPieData()
handleSearch()
},
{ deep: true }
)
//
watch(
() => formState.dateValues,
() => {
fetchPieData()
handleSearch()
}
)
// props.dataDimensionVal
watch(
() => props.dataDimensionVal,
(newVal) => {
//
formState.dataDimensionVal = newVal || ''
formState.dtinType = ''
formState.sfdb = ''
formState.stnm = ''
pieCode.value = []
//
nextTick(() => {
handleSearch()
})
}
)
// props.type
watch(
() => props.type,
(newType) => {
if (!newType || newType === formState.types) return
//
const currentValue: any = formState.dateValues
if (currentValue) {
if (newType === 'year') {
//
if (typeof currentValue.year === 'function') {
// dayjs
originalMonth.value = String(currentValue.month() + 1).padStart(2, '0')
const year = currentValue.year()
formState.dateValues = dayjs(String(year), 'YYYY')
} else if (typeof currentValue === 'string') {
//
const monthMatch = currentValue.match(/^(\d{4})-(\d{2})/)
if (monthMatch) {
originalMonth.value = monthMatch[2]
formState.dateValues = dayjs(monthMatch[1], 'YYYY')
} else {
const yearMatch = currentValue.match(/^(\d{4})/)
if (yearMatch) {
formState.dateValues = dayjs(yearMatch[1], 'YYYY')
}
}
}
} else if (newType === 'month') {
//
const monthToUse = originalMonth.value || '01'
if (typeof currentValue.year === 'function') {
const year = currentValue.year()
formState.dateValues = dayjs(`${year}-${monthToUse}`, 'YYYY-MM')
} else if (typeof currentValue === 'string') {
const yearMatch = currentValue.match(/^(\d{4})/)
if (yearMatch) {
formState.dateValues = dayjs(`${yearMatch[1]}-${monthToUse}`, 'YYYY-MM')
}
}
}
}
//
formState.types = newType
//
nextTick(() => {
handleSearch()
})
}
)
// props.typeOne , state
watch(
() => props.typeOne,
(newVal) => {
typeOne.value = newVal
// debugger
}
)
// props.baseid state
watch(
() => props.baseid,
(newVal) => {
baseid.value = newVal || ''
}
)
//
watch(
() => formState.types,
(newType, oldType) => {
if (!formState.dateValues || newType === oldType) return
// debugger
const currentValue: any = formState.dateValues
if (newType === 'year') {
//
if (currentValue && typeof currentValue.year === 'function') {
// dayjs
originalMonth.value = String(currentValue.month() + 1).padStart(2, '0')
const year = currentValue.year()
formState.dateValues = dayjs(String(year), 'YYYY')
} else if (typeof currentValue === 'string') {
//
const monthMatch = currentValue.match(/^(\d{4})-(\d{2})/)
if (monthMatch) {
originalMonth.value = monthMatch[2]
formState.dateValues = dayjs(monthMatch[1], 'YYYY')
} else {
const yearMatch = currentValue.match(/^(\d{4})/)
if (yearMatch) {
formState.dateValues = dayjs(yearMatch[1], 'YYYY')
}
}
}
} else if (newType === 'month') {
//
const monthToUse = originalMonth.value || '01'
if (currentValue && typeof currentValue.year === 'function') {
const year = currentValue.year()
formState.dateValues = dayjs(`${year}-${monthToUse}`, 'YYYY-MM')
} else if (typeof currentValue === 'string') {
const yearMatch = currentValue.match(/^(\d{4})/)
if (yearMatch) {
formState.dateValues = dayjs(`${yearMatch[1]}-${monthToUse}`, 'YYYY-MM')
}
}
}
//
nextTick(() => {
handleSearch()
})
}
)
//
onMounted(async () => {
await nextTick()
fetchPieData()
handleSearch()
// resize
@ -756,7 +873,7 @@ defineExpose({
.search-form {
background: #fff;
border-radius: 4px;
padding: 16px;
padding: 0px 0px 16px 0px;
:deep(.ant-space) {
display: flex;

View File

@ -0,0 +1,540 @@
<template>
<div>
<!-- 搜索表单 - 使用 Ant Design Vue 原生组件 -->
<a-form ref="formRef" :model="formState" layout="inline" @finish="onFinish" style="margin-bottom: 16px">
<a-form-item label="达标状态" name="sfdb">
<a-select v-model:value="formState.sfdb" placeholder="请选择" allow-clear style="width: 150px">
<a-select-option value="">全部</a-select-option>
<a-select-option :value="'0'">不达标</a-select-option>
<a-select-option :value="'1'">达标</a-select-option>
<a-select-option :value="'2'">无目标等级/无数据</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" html-type="submit">查询</a-button>
<Tooltip title="重置">
<a-button @click="handleReset">重置</a-button>
</Tooltip>
</a-space>
</a-form-item>
</a-form>
<!-- 数据表格 -->
<BasicTable ref="tableRef" :columns="columns"
:list-url="props.calculateFlag?.calculateFlag == 1 ? dataGetKendoListCust : NodataGetKendoListCust"
:scrollY="500" :search-params="{ sort: [{ field: 'tm', dir: 'desc' }] }"
:transform-data="customTransform" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted, h, nextTick } from 'vue'
import { omit } from 'lodash-es'
import { Button, Tooltip, Popover } from 'ant-design-vue'
import BasicTable from '@/components/BasicTable/index.vue'
import { ruleGetKendoListCust, noAuthGetKendoListCust, stbprpGetStbprpYsByStcd, dataGetKendoListCust, NodataGetKendoListCust } from '@/api/sz'
// ==================== Props ====================
interface Props {
tc: {
stcd: string
startTime: string
endTime: string
sfdb: string
}
calculateFlag?: {
calculateFlag: number
}
}
const props = defineProps<Props>()
// ==================== ====================
const formRef = ref()
const tableRef = ref()
const col = ref<any[]>([])
// sfdb ref
const formState = reactive({
sfdb: '1'
})
// ==================== ====================
const columnList = [
{
dataIndexRaw: '',
key: 'tm',
title: '时间',
dataIndex: 'tm',
children: [],
visible: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataType: 'date',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
sorter: true,
multiLink: false
},
{
dataIndexRaw: '',
key: 'wwqtg',
title: '水质要求',
dataIndex: 'wwqtgName',
children: [],
visible: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
sorter: true,
disabledDate: true,
multiLink: false,
width: 100
},
{
dataIndexRaw: '',
key: 'wqGrd',
title: '水质等级',
dataIndex: 'wqgrdName',
children: [],
visible: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
sorter: true,
disabledDate: true,
multiLink: false,
width: 100
},
{
dataIndexRaw: 'sfdb',
key: 'sfdb',
title: '是否达标',
dataIndex: 'sfdbName',
children: [],
visible: true,
fixed: '',
ellipsis: true,
wrap: false,
// TODO: - BasicTable dictType/dictCode
// customRender
dictType: 'dataDict',
dictCode: 'sd_wq_r.sfdb',
dictDatas: {},
fromType: 'select',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
sorter: true,
disabledDate: true,
multiLink: false,
width: 100,
// Vue3 customRender
customRender: ({ record }: any) => {
// TODO:
// return getDictLabel('sd_wq_r.sfdb', record.sfdbName)
return record.sfdbName //
}
}
]
// ==================== ====================
const columns = computed(() => {
if (!col.value.length) return columnList
return [...columnList, ...col.value]
})
// ==================== ====================
const tableFilter = computed(() => {
let res: any = {
stcd: props.tc.stcd,
sfdb: formState.sfdb === '2' ? '' : formState.sfdb,
startTime: props.tc.startTime,
endTime: props.tc.endTime
}
if (res.sfdb === '') res = omit(res, 'sfdb')
return res
})
// ==================== ====================
//
const onFinish = () => {
const filters: any[] = [
{
"field": "stcd",
"operator": "eq",
"dataType": "string",
"value": tableFilter.value.stcd
},
formState.sfdb ? {
"field": "sfdb",
"operator": "eq",
"dataType": "string",
"value": formState.sfdb
} : null,
{
"field": "tm",
"operator": "gte",
"dataType": "date",
"value": tableFilter.value.startTime
},
{
"field": "tm",
"operator": "lte",
"dataType": "date",
"value": tableFilter.value.endTime
}
].filter(Boolean)
const filter = {
logic: 'and',
filters
}
//
tableRef.value?.getList(filter)
}
//
const handleReset = () => {
formState.sfdb = '1'
onFinish()
}
// TODO: - API
const getCol = async (params: any) => {
try {
const lo = await columYaoshu(params)
col.value = lo || []
} catch (error) {
console.error('获取列配置失败:', error)
}
}
// TODO: -
const columYaoshu = async (params: any) => {
// API 1:
let params3 = {
"filter": {
"logic": "and",
"filters": [
{
"field": "stcd",
"operator": "eq",
"dataType": "string",
"value": params?.stcd
}
]
}
}
const MaxData = await ruleGetKendoListCust(params3)
// API 2:
let params2 = {
"filter": {
"logic": "and",
"filters": [
{
"field": "stcd",
"operator": "eq",
"dataType": "string",
"value": params?.stcd
},
{
"field": "tm",
"operator": "lte",
"dataType": "date",
"value": params?.endTime
},
{
"field": "tm",
"operator": "gte",
"dataType": "date",
"value": params?.startTime
}
]
}
}
const noAuthRes = await noAuthGetKendoListCust(params2)
const noAuthResData = noAuthRes?.data?.data || []
// API 3:
let params1 = {
stcd: params?.stcd,
tbCode: 'WQ_R',
endTime: params.endTime,
startTime: params.startTime
}
const res = await stbprpGetStbprpYsByStcd(params1)
if (res?.data?.data) {
const processedColumns = res.data.data
.filter((item: any) => item.enable === '1')
.map((item: any) => {
const _t = item.ysShowName
const reg = /[(][^()]+[)]/
// TODO:
const _u = getUnitConfigByCode('WQ_R', item.ys)?.unit
if (_u) {
item.ysShowName = reg.test(_t)
? _t.replace(reg, `(${_u})`)
: `${_t}(${_u})`
}
let showValue = ''
if (noAuthResData.length) {
const { max, min } = noAuthResData[0]
const ys = item.ys
const maxValue = max.filter((e: any) => e[ys])
const minValue = min.filter((e: any) => e[ys])
if (maxValue.length && minValue.length) {
showValue = `(${minValue[0]?.[ys]}-${maxValue[0]?.[ys]})`
} else {
if (minValue.length) {
showValue = `>${minValue[0]?.[ys]}`
}
if (maxValue.length) {
showValue = `<${maxValue[0]?.[ys]}`
}
}
}
return {
dataIndexRaw: '',
key: item.ys.toLowerCase(),
// Vue3 title VNode
title: () => h('div', [
h('p', {
style: {
overflow: 'hidden',
textOverflow: 'ellipsis',
wordBreak: 'keep-all'
}
}, item.ysShowName),
h('p', showValue)
]),
dataIndex: item.ys.toLowerCase(),
children: [],
visible: true,
fixed: '',
sorter: true,
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
// Vue3 使 customRender render
customRender: ({ record }: any) => {
// TODO: MaxData
const datalist = MaxData?.data?.data
const dynamicDataIndex = item.ys
const dynamicDataName = item.ysShowName
const matchedItem = datalist.find((m: any) => m.ys === dynamicDataIndex)
//
if (
matchedItem &&
(matchedItem.isAsc == 1 ? matchedItem?.maxVal : matchedItem.minVal) &&
record[dynamicDataIndex?.toLowerCase()] &&
((matchedItem.isAsc == 1 && record[dynamicDataIndex?.toLowerCase()] > matchedItem?.maxVal) ||
(matchedItem.isAsc != 1 && record[dynamicDataIndex?.toLowerCase()] < matchedItem.minVal))
) {
// Vue3 使 h() VNode
return h(Popover, {
content: h('div', `预警标准: ${matchedItem.isAsc === 1 ? `>${matchedItem.maxVal}` : `<${matchedItem.minVal}`}`),
trigger: 'hover'
}, () =>
h('span', {
style: { color: 'red', cursor: 'pointer' },
// TODO:
}, transUnitRender(record[dynamicDataIndex?.toLowerCase()], 'WQ_R', dynamicDataIndex))
)
}
//
// TODO:
return transUnitRender(record[dynamicDataIndex?.toLowerCase()], 'WQ_R', dynamicDataIndex)
},
fromType: '',
dataType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
}
})
console.log(processedColumns)
onFinish()
return processedColumns || []
}
}
// TODO: -
// ==================== ====================
/**
* 根据表代码和要素代码获取单位配置
* @param tbCode 表代码 ( 'WQ_R')
* @param ys 要素代码 ( 'DOX', 'CODCR')
* @returns 单位配置对象 { unit: string }
*/
const getUnitConfigByCode = (tbCode: string, ys: string) => {
// TODO:
const unitConfig: Record<string, Record<string, { unit: string }>> = {
'WQ_R': {
'PH': { unit: '' },
'DOX': { unit: 'mg/L' },
'CODMN': { unit: 'mg/L' },
'CODCR': { unit: 'mg/L' },
'NH3N': { unit: 'mg/L' },
'TP': { unit: 'mg/L' },
'TN': { unit: 'mg/L' },
'WTMP': { unit: '℃' },
'COND': { unit: 'μS/cm' },
'CLARITY': { unit: 'm' }
}
}
return unitConfig[tbCode]?.[ys] || { unit: '' }
}
/**
* 带单位的值渲染根据要素类型设置不同小数位数不带单位
* @param value 数值
* @param tbCode 表代码
* @param ys 要素代码
* @returns 格式化后的字符串不带单位
*/
const transUnitRender = (value: any, tbCode: string, ys: string) => {
if (value === null || value === undefined || value === '') return '-'
const numValue = Number(value)
if (isNaN(numValue)) return String(value)
//
let formattedValue: string
if (ys === 'WTMP' || ys === 'PH') {
// PH
formattedValue = numValue.toFixed(1)
} else if (ys === 'COND') {
//
formattedValue = Math.round(numValue).toString()
} else {
//
formattedValue = numValue.toFixed(2)
}
return formattedValue
}
/**
* 纯单位转换不带渲染
* @param value 数值
* @param tbCode 表代码
* @param ys 要素代码
* @returns 转换后的数值
*/
const transUnit = (value: any, tbCode: string, ys: string) => {
if (value === null || value === undefined) return null
return Number(value)
}
// ==================== ====================
// tc formState.sfdb
watch(
() => [props.tc.stcd, props.tc.startTime, props.tc.endTime, formState.sfdb],
([stcd, startTime, endTime]) => {
if(!stcd){
return
}
// debugger
tableRef.value.tableData = []
getCol({ stcd, startTime, endTime })
},
{ immediate: false } // onMounted
)
//
onMounted(() => {
nextTick(() => {
})
getCol({
stcd: props.tc.stcd,
startTime: props.tc.startTime,
endTime: props.tc.endTime
})
})
//
const customTransform = (res: any) => {
return {
records: res?.data?.data || [],
total: res?.data?.total || 0
}
}
</script>
<style scoped>
/* 如需添加样式,请在此处编写 */
</style>

View File

@ -1,18 +1,18 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="地表水水质达标率" :select="select" :datetimePicker="datetimePicker"
@update-values="handlePanelChange1" style="height: 650px;">
@update-values="handlePanelChange1" :style="chartData.length === 0 && !loading && baseid != 'all' ? 'height: 300px' : 'height: 650px'">
<div class="body_item">
<div v-if="baseid == 'all'" class="tabs_all">
<div :class="tabs == 0 ? 'zhong_tabs' : 'no_tabs'" @click="handleTabChange(0)">自建水质站</div>
<div :class="tabs == 1 ? 'zhong_tabs' : 'no_tabs'" @click="handleTabChange(1)">国家水质站</div>
<div :class="tabs == '0' ? 'zhong_tabs' : 'no_tabs'" @click="handleTabChange(0)">自建水质站</div>
<div :class="tabs == '1' ? 'zhong_tabs' : 'no_tabs'" @click="handleTabChange(1)">国家水质站</div>
</div>
<div v-show="tabs == 0 || tabs == 1" class="tabs_body">
<div v-show="tabs == '0' || tabs == '1'" class="tabs_body">
<a-spin :spinning="loading">
<!-- 始终渲染图表容器确保有固定宽高 -->
<div ref="chartRef" class="chart-container"></div>
<!-- 无数据时显示Empty但不影响容器尺寸 -->
<div v-if="chartData.length === 0 && !loading" class="empty-overlay">
<div v-if="chartData.length === 0 && !loading" class="empty-overlay" :style="chartData.length === 0 && !loading && baseid != 'all' ? 'height: 300px' : 'height: 650px'">
<a-empty description="暂无数据" />
</div>
</a-spin>
@ -22,20 +22,10 @@
</SidePanelItem>
<!-- 环境质量满足度弹框 -->
<a-modal
v-model:open="modalVisible"
title="环境质量满足度"
width="1536px"
:footer="null"
centered
>
<EnvironmentalQualityTwoLayers
:datas="jiDiList"
:type="select.value "
:dateValue="datetimePicker.value"
:dataDimensionVal="baseid"
:buildType="tabs"
/>
<a-modal v-model:open="modalVisible" title="环境质量满足度" width="1536px" :footer="null" centered>
<EnvironmentalQualityTwoLayers :datas="(baseid == 'all' ? jiDiList : stationListForModal)" :type="select.value"
:dateValue="datetimePicker.value" :dataDimensionVal="(clickedBarData?.selectedId || baseid)"
:buildType="tabs" :typeOne="fieldOne" :baseid="baseid" />
</a-modal>
</template>
@ -51,14 +41,20 @@ defineOptions({
name: 'EnvironmentalQuality'
});
const JidiSelectEventStore = useJidiSelectEventStore();
const tabs = ref(0);
const tabs: any = ref('0');
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const loading = ref(false);
//
//
const chartData = ref<Array<{ name: string; current: number | null; lastYear: number | null }>>([]);
//
const stationListForModal = ref<Array<{ baseid: string; basename: string }>>([]);
// baseid/rstcd
const chartDataIdMap = ref<Array<string>>([]);
const select = ref({
show: true,
value: 'month',
@ -161,24 +157,16 @@ const jiDiList: any = ref([
"id": "AFBDFC67-B955-4EFD-959A-014CFB59EBFC"
}
])
// Store
const baseList = computed(() => {
// TODO:
// Store API
//
// return [
// { baseid: 'all', basename: '' },
// { baseid: '01', basename: '' },
// ...
// ]
//
return []
})
const fieldOne = ref('')
// API
const parseApiResponse = (items: any[], currentValueForApi: string, lastYearValue: string) => {
const result: Array<{ name: string; current: number | null; lastYear: number | null }> = [];
fieldOne.value = ''
// ID
chartDataIdMap.value = [];
// baseid != 'all'
const stations: Array<{ baseid: string; basename: string }> = [];
console.log('=== parseApiResponse 开始解析 ===');
console.log('currentValueForApi:', currentValueForApi);
@ -246,11 +234,12 @@ const parseApiResponse = (items: any[], currentValueForApi: string, lastYearValu
console.log(' subGroup.items数量:', subGroup?.items?.length);
subGroup?.items?.forEach(monthGroup => {
const periodKey = monthGroup.key; // "2026-05" "2025-05"
const periodKey = monthGroup.key; // "2026-05" "2025-05" "2026" "2025"
const dataItem = monthGroup.items?.[0];
if (dataItem) {
const value = dataItem.COUNT_DRMONTH ?? dataItem.wqRate;
// COUNT_DRMONTH/wqRateCOUNT_DRYEAR
const value = dataItem.COUNT_DRMONTH ?? dataItem.COUNT_DRYEAR ?? dataItem.wqRate;
console.log(` 时间:${periodKey}, 值:${value}`);
//
@ -269,6 +258,28 @@ const parseApiResponse = (items: any[], currentValueForApi: string, lastYearValu
console.log(` → 最终数据: {name: "${finalDisplayName}", current: ${currentPeriodValue}, lastYear: ${lastYearPeriodValue}}`);
// ID
if (field === 'baseId') {
fieldOne.value = 'all'
// baseid == 'all' ID
chartDataIdMap.value.push(mainGroup.key); // "02", "03"
console.log(' 记录基地ID:', mainGroup.key);
} else if (field === 'ennm') {
fieldOne.value = 'other'
// baseid != 'all'
const stationCode = subGroup?.key; // "008660306300000001"
chartDataIdMap.value.push(stationCode || '');
console.log(' 记录电站编码:', stationCode);
//
if (displayName && stationCode) {
stations.push({
baseid: stationCode,
basename: displayName
});
}
}
result.push({
name: finalDisplayName,
current: currentPeriodValue,
@ -278,6 +289,15 @@ const parseApiResponse = (items: any[], currentValueForApi: string, lastYearValu
});
});
//
if (stations.length > 0) {
stationListForModal.value = stations;
console.log('提取到的电站列表:', stations);
} else {
stationListForModal.value = [];
}
console.log('ID 映射关系:', chartDataIdMap.value);
console.log('\n=== parseApiResponse 解析完成 ===');
console.log('result数量:', result.length);
console.log('result:', result);
@ -293,7 +313,7 @@ const handleTabChange = (tab: number) => {
//tabs
watch(tabs, (newVal) => {
if (newVal === 0 || newVal === 1) {
if (newVal === '0' || newVal === '1') {
nextTick(() => {
setTimeout(() => {
getEchartsData();
@ -414,6 +434,11 @@ const getEchartsData = async () => {
}
// 3.
console.log(baseid.value)
if( ! baseid.value){
return
}
// debugger
let params = {
"filter": {
"logic": "and",
@ -428,11 +453,14 @@ const getEchartsData = async () => {
"operator": "eq",
"value": select.value.value == "year" ? "YEAR" : "MON"
},
{
baseid.value == 'all' ? {
"field": "dtinType",
"operator": "eq",
"value": tabs.value
},
"value": 0
} : {
"field": "rstcd",
"operator": "isnotnull"
}, ,
{
"logic": "or",
"filters": [
@ -453,10 +481,13 @@ const getEchartsData = async () => {
].filter(Boolean)
},
"group": [
{
baseid.value == 'all' ? {
"dir": "des",
"field": "baseStepSort"
},
} : {
"dir": "asc",
"field": "rstcdStepSort"
}, ,
baseid.value == 'all' ? {
"dir": "des",
"field": "baseId"
@ -475,7 +506,7 @@ const getEchartsData = async () => {
"dir": "desc",
"field": select.value.value == "year" ? "drYear" : "drMonth"
}
],
].filter(Boolean),
"groupResultFlat": false,
"aggregate": [
{
@ -484,7 +515,8 @@ const getEchartsData = async () => {
}
]
};
console.log('请求参数:', params);
// debugger
// 4. API
loading.value = true;
try {
@ -611,7 +643,7 @@ const updateChartData = (currentLabel: string, lastYearLabel: string) => {
const name = params[0].name.replace('\n', '');
let result = `<div style="font-weight: bold; margin-bottom: 8px;">${name}</div>`;
params.forEach((item: any) => {
const value = item.value != null ? Number(item.value).toFixed(1) : '-';
const value = item.value != null ? Number(item.value) : '-';
result += `<div style="display: flex; align-items: center; margin-bottom: 4px;">
<span style="display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: ${item.color}; margin-right: 8px;"></span>
<span>${item.seriesName}:</span>
@ -695,7 +727,7 @@ const updateChartData = (currentLabel: string, lastYearLabel: string) => {
{
name: legendCurrentLabel,
type: 'bar',
data: chartData.value.map(item => item.current != null ? Number(item.current).toFixed(1) : null),
data: chartData.value.map(item => item.current != null ? Math.floor(Number(item.current)) : null),
barWidth: 10,
barGap: '20%',
itemStyle: {
@ -706,7 +738,7 @@ const updateChartData = (currentLabel: string, lastYearLabel: string) => {
{
name: legendLastYearLabel,
type: 'bar',
data: chartData.value.map(item => item.lastYear != null ? Number(item.lastYear).toFixed(1) : null),
data: chartData.value.map(item => item.lastYear != null ? Math.floor(Number(item.lastYear)) : null),
barWidth: 10,
barGap: '20%',
itemStyle: {
@ -730,10 +762,14 @@ const updateChartData = (currentLabel: string, lastYearLabel: string) => {
const barData = chartData.value[dataIndex];
if (barData) {
// baseid rstcd
const selectedId = chartDataIdMap.value[dataIndex] || '';
// debugger
clickedBarData.value = {
name: barData.name.replace('\n', ''), //
current: barData.current,
lastYear: barData.lastYear
lastYear: barData.lastYear,
selectedId: selectedId // ID
};
modalVisible.value = true;
}
@ -756,11 +792,25 @@ watch(
//
const handlePanelChange1 = async (data) => {
console.log('当前所有控件状态:', data);
// debugger
//
// ,
if (data.select || data.datetime) {
await (select.value.value = data.select)
await (datetimePicker.value.value = data.datetime)
// select datetime
let formattedDatetime = data.datetime;
if (data.select === 'year') {
// datetime YYYY
if (data.datetime && typeof data.datetime === 'string') {
// YYYY-MM
const yearMatch = data.datetime.match(/^(\d{4})/);
if (yearMatch) {
formattedDatetime = yearMatch[1];
}
}
}
// month datetime
await (datetimePicker.value.value = formattedDatetime)
await (datetimePicker.value.picker = data.select)
await (datetimePicker.value.format = data.select == 'month' ? 'YYYY-MM' : 'YYYY')
getEchartsData()
@ -800,45 +850,9 @@ onUnmounted(() => {
//
const modalVisible = ref(false);
const clickedBarData = ref<{ name: string; current: number | null; lastYear: number | null } | null>(null);
const clickedBarData = ref<{ name: string; current: number | null; lastYear: number | null; selectedId?: string } | null>(null);
//
const modalTimeInfo = computed(() => {
if (!datetimePicker.value.value) return '';
const currentValue = datetimePicker.value.value;
const modeText = select.value.value === 'year' ? '年度' : '月度';
if (select.value.value === 'year') {
return `${currentValue}${modeText}`;
} else {
const [year, month] = currentValue.split('-');
return `${year}${month}${modeText}`;
}
});
//
const getChangeValue = () => {
if (!clickedBarData.value || clickedBarData.value.current == null || clickedBarData.value.lastYear == null) {
return '-';
}
const change = Number(clickedBarData.value.current) - Number(clickedBarData.value.lastYear);
const symbol = change > 0 ? '+' : '';
return `${symbol}${change.toFixed(1)}%`;
};
//
const getChangeClass = () => {
if (!clickedBarData.value || clickedBarData.value.current == null || clickedBarData.value.lastYear == null) {
return '';
}
const change = Number(clickedBarData.value.current) - Number(clickedBarData.value.lastYear);
if (change > 0) return 'positive';
if (change < 0) return 'negative';
return 'neutral';
};
</script>
@ -912,7 +926,7 @@ const getChangeClass = () => {
top: 0;
left: 0;
width: 100%;
height: 100%;
height: 300px;
display: flex;
align-items: center;
justify-content: center;

View File

@ -22,12 +22,12 @@
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useJidiSelectEventStore } from "@/store/modules/jidiSelectEvent";
// 便
defineOptions({
name: 'qixidibaohugongzuokaizhanQK'
});
const JidiSelectEventStore = useJidiSelectEventStore();
//
const facilities = ref([
{
@ -52,7 +52,15 @@ const facilities = ref([
},
]);
const baseid = ref('')
watch(
() => JidiSelectEventStore.selectedItem,
(newVal) => {
baseid.value = newVal.wbsCode;
},
{ deep: true, immediate: true }
);
//
onMounted(() => {
//

View File

@ -87,7 +87,7 @@ const getDataList = async () => {
"field": "baseId",
"operator": "eq",
"dataType": "string",
"value": "01"
"value": baseid.value
} : null,
{
"field": "isDeleted",
@ -166,7 +166,7 @@ const getDataList = async () => {
"field": "baseId",
"operator": "eq",
"dataType": "string",
"value": "01"
"value": baseid.value
} : null,
{
"field": "isDeleted",
@ -208,25 +208,30 @@ const getDataList = async () => {
}
// const params = [data1, data2]
const res1 = await msstbprptGetKendoList(data1);
// debugger
let finalData = facilities.value.map((item) => ({ ...item, count: 0 }))
if (res1.data) {
const { data } = res1
data.data.forEach((item: any, index: number) => {
const target = finalData.find((n) => n.key == item.key)
if (target) {
target.count = item.count
target.count = item.count || 0
}
})
}
console.log(finalData)
// debugger/
const res2 = await msstbprptGetKendoList(data2);
if (res2.data) {
const responseData = res2?.data?.data
const target1 = finalData.find((n) => n.key == '3')
if (target1) {
target1.count = responseData[0].count || 0
target1.count = responseData[0]?.count || 0
}
}
facilities.value = finalData
console.log(finalData)
// debugger
} catch (error) {
console.error('获取数据失败:', error);
} finally {

View File

@ -24,8 +24,8 @@ import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { qgcGetKendoListCust, wbsbGetKendoList } from '@/api/sz'
import { useJidiSelectEventStore } from "@/store/modules/jidiSelectEvent";
import { useModelStore } from "@/store/modules/model";
import { WATER_QUALITY_INDICATORS, type WaterQualityIndicator } from '@/modules/waterQuality/padig';
const indicators = WATER_QUALITY_INDICATORS;
// 便
defineOptions({
name: 'waterQuality'
@ -34,7 +34,7 @@ const modelStore = useModelStore();
const JidiSelectEventStore = useJidiSelectEventStore();
const iconmap = ref({
show: true,
value: '最新数据时间为2026-05-14 2300',
value: '',
icon: 'iconfont icon-time',
});
// ==================== ====================
@ -48,13 +48,15 @@ const hasData = ref(true); // 初始化为 true因为 initChart 会使用 moc
// 8:00
const now = new Date();
const todayAtEightAM = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0, 0);
const defaultValue = `${todayAtEightAM.getFullYear()}-${String(todayAtEightAM.getMonth() + 1).padStart(2, '0')}-${String(todayAtEightAM.getDate()).padStart(2, '0')} ${String(todayAtEightAM.getHours()).padStart(2, '0')}:${String(todayAtEightAM.getMinutes()).padStart(2, '0')}`;
const defaultValue = `${todayAtEightAM.getFullYear()}-${String(todayAtEightAM.getMonth() + 1).padStart(2, '0')}-${String(todayAtEightAM.getDate()).padStart(2, '0')} ${String(todayAtEightAM.getHours()).padStart(2, '0')}:00`;
// 👆 :00 timeFormat: 'HH'
const datetimePicker = ref({
show: true,
value: defaultValue,
format: 'YYYY-MM-DD hh:mm',
format: 'YYYY-MM-DD HH:mm', //
picker: 'date',
timeFormat: 'HH', //
options: []
});
const select = ref({
@ -68,6 +70,109 @@ const select = ref({
// ==================== 2====================
const visibleSeriesQueue = ref<string[]>([]);
// ==================== ====================
const indicatorConfig = ref<Array<{ key: string; name: string; unit: string; color: string; sort: number }>>([]);
// ==================== HSL ====================
const generateRandomColor = (index: number): string => {
// 使
const goldenAngle = 137.508;
const hue = (index * goldenAngle) % 360;
// 45%-65%
const saturation = 45 + Math.random() * 20;
// 45%-60%
const lightness = 45 + Math.random() * 15;
// HSL RGB
const c = (1 - Math.abs(2 * lightness / 100 - 1)) * saturation / 100;
const x = c * (1 - Math.abs((hue / 60) % 2 - 1));
const m = lightness / 100 - c / 2;
let r = 0, g = 0, b = 0;
if (hue < 60) {
[r, g, b] = [c, x, 0];
} else if (hue < 120) {
[r, g, b] = [x, c, 0];
} else if (hue < 180) {
[r, g, b] = [0, c, x];
} else if (hue < 240) {
[r, g, b] = [0, x, c];
} else if (hue < 300) {
[r, g, b] = [x, 0, c];
} else {
[r, g, b] = [c, 0, x];
}
const toHex = (value: number) => {
const hex = Math.round((value + m) * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};
// ==================== ====================
const generateIndicatorConfig = (data: StationData[]) => {
if (!data || data.length === 0) {
indicatorConfig.value = [];
return;
}
//
const excludeKeys = new Set([
'stcd', 'stcd2', 'stnm', 'rstcd', 'sttp', 'tm', 'SORT',
'limits', 'minTm', 'wwqtg', 'wqgrd', 'wqgrdName', 'min', 'max',
'_tls', 'id', 'recordUser', 'recordTime', 'modifyTime',
'displayRecordUser', 'departmentId', 'displayDepartment'
]);
// key
const validKeysSet = new Set<string>();
data.forEach(station => {
Object.keys(station).forEach(key => {
//
if (excludeKeys.has(key)) return;
const value = station[key as keyof typeof station];
// null NaN
if (value !== null && value !== undefined && typeof value === 'number' && !isNaN(value)) {
// indicators
const matchedIndicator = indicators.find(ind => ind.key.toLowerCase() === key.toLowerCase());
if (matchedIndicator) {
validKeysSet.add(matchedIndicator.key);
}
}
});
});
//
if (validKeysSet.size === 0) {
indicatorConfig.value = [];
return;
}
// indicators sort
const validIndicators = indicators
.filter(ind => validKeysSet.has(ind.key))
.sort((a, b) => a.sort - b.sort);
//
indicatorConfig.value = validIndicators.map((ind, index) => ({
key: ind.key,
name: ind.name,
unit: ind.description || '',
color: generateRandomColor(index),
sort: ind.sort
}));
console.log('动态生成指标配置:', indicatorConfig.value);
};
// ==================== ====================
const generateMockData = () => {
const stations = [
@ -82,6 +187,16 @@ const generateMockData = () => {
const tm = '2024-01-15 08:00:00';
//
const mockLimits = {
ph: { min: 6, max: 9 },
dox: { min: 6 },
codmn: { max: 4 },
nh3n: { max: 0.5 },
tp: { max: 0.1 },
tn: { max: 1.0 }
};
return stations.map((station, index) => ({
...station,
tm,
@ -91,27 +206,65 @@ const generateMockData = () => {
codmn: 1.5 + Math.random() * 2.0,
nh3n: 0.15 + Math.random() * 0.35,
tp: 0.05 + Math.random() * 0.15,
tn: 0.8 + Math.random() * 1.2
tn: 0.8 + Math.random() * 1.2,
limits: mockLimits //
}));
};
const mockData = ref(generateMockData());
//
interface StationData {
stnm: string;
stcd: string;
type: string;
tm: string;
SORT: number;
//
ph?: number | null;
dox?: number | null;
codmn?: number | null;
codcr?: number | null;
bod5?: number | null;
nh3n?: number | null;
tp?: number | null;
tn?: number | null;
cu?: number | null;
zn?: number | null;
f?: number | null;
se?: number | null;
ars?: number | null;
hg?: number | null;
cd?: number | null;
cr6?: number | null;
pb?: number | null;
cn?: number | null;
vlph?: number | null;
oil?: number | null;
las?: number | null;
s2?: number | null;
fcg?: number | null;
cl?: number | null;
so4?: number | null;
no3?: number | null;
thrd?: number | null;
cond?: number | null;
fe?: number | null;
mn?: number | null;
al?: number | null;
chla?: number | null;
clarity?: number | null;
tu?: number | null;
wtmp?: number | null;
limits: Record<string, { min?: number; max?: number }>;
[key: string]: any; //
}
// ==================== ====================
const indicatorConfig = [
{ key: 'ph', name: 'pH', unit: '', color: '#5470C6' },
{ key: 'dox', name: '溶解氧', unit: 'mg/L', color: '#91CC75' },
{ key: 'codmn', name: '高锰酸盐指数', unit: 'mg/L', color: '#FAC858' },
{ key: 'nh3n', name: '氨氮', unit: 'mg/L', color: '#EE6666' },
{ key: 'tp', name: '总磷', unit: 'mg/L', color: '#73C0DE' },
{ key: 'tn', name: '总氮', unit: 'mg/L', color: '#3BA272' }
];
const mockData = ref<StationData[]>([]);
// ==================== ====================
const getChartOption = (): EChartsOption => {
const data = mockData.value;
if (!data || data.length === 0) {
if (!data || data.length === 0 || indicatorConfig.value.length === 0) {
return {};
}
@ -142,16 +295,16 @@ const getChartOption = (): EChartsOption => {
});
//
const legendData = indicatorConfig.map(item => item.name);
const legendData = indicatorConfig.value.map(item => item.name);
//
const selectedState: Record<string, boolean> = {};
indicatorConfig.forEach(config => {
indicatorConfig.value.forEach(config => {
selectedState[config.name] = visibleSeriesQueue.value.includes(config.name);
});
// Series
const seriesData = indicatorConfig.map((config, index) => {
const seriesData = indicatorConfig.value.map((config, index) => {
const _key = config.key === 'dox' ? 'do' : config.key;
return {
name: config.name,
@ -182,12 +335,12 @@ const getChartOption = (): EChartsOption => {
// Y使
const calculateYAxisRange = (configIndex: number) => {
if (!selectedState[indicatorConfig[configIndex].name]) {
if (!selectedState[indicatorConfig.value[configIndex].name]) {
return null;
}
const values = data
.map(item => item[indicatorConfig[configIndex].key as keyof typeof item])
.map(item => item[indicatorConfig.value[configIndex].key as keyof typeof item])
.filter((val): val is number => typeof val === 'number' && !isNaN(val));
if (values.length === 0) {
@ -204,10 +357,38 @@ const getChartOption = (): EChartsOption => {
};
};
// Y1N
const yAxisData = indicatorConfig.map((config, index) => {
const isLeft = index === 0;
const offset = index < 2 ? 0 : (index - 1) * 60;
// Y112Y
const yAxisData = indicatorConfig.value.map((config, index) => {
const isShow = selectedState[config.name];
//
if (!isShow) {
return {
type: 'value' as const,
name: config.name,
show: false
};
}
// Y
let displayIndex = 0;
for (let i = 0; i < index; i++) {
if (selectedState[indicatorConfig.value[i].name]) {
displayIndex++;
}
}
// 2Y01
if (displayIndex >= 2) {
return {
type: 'value' as const,
name: config.name,
show: false
};
}
const isLeft = displayIndex === 0;
const offset = displayIndex === 1 ? 0 : 0;
const range = calculateYAxisRange(index);
return {
@ -237,7 +418,7 @@ const getChartOption = (): EChartsOption => {
type: 'solid' as const
}
},
show: selectedState[config.name]
show: true
};
});
@ -259,8 +440,11 @@ const getChartOption = (): EChartsOption => {
const tm = data.find(item => item.stnm === params[0].name)?.tm;
result += `${params[0].name}<br/>`;
//
const showOnlyOneSeries = visibleSeriesQueue.value.length === 1;
params.forEach((param: any) => {
const config = indicatorConfig.find(c => c.name === param.seriesName);
const config = indicatorConfig.value.find(c => c.name === param.seriesName);
const unit = config?.unit || '';
if (param.value !== null && param.value !== undefined) {
@ -272,7 +456,38 @@ const getChartOption = (): EChartsOption => {
displayValue = numValue.toFixed(1);
}
result += `${param.marker}${param.seriesName}${displayValue}${unit}<br/>`;
result += `${param.marker}${param.seriesName}${displayValue}${unit}`;
//
if (showOnlyOneSeries && config) {
// data
const stationData = data.find(item => item.stnm === param.name);
if (stationData && stationData.limits) {
const limitKey = config.key; // 'ph', 'dox'
const limits = stationData.limits[limitKey];
if (limits) {
//
let limitText = '';
const unit = config.unit || ''; //
if (limits.min !== undefined && limits.max !== undefined) {
//
limitText = `&nbsp;&nbsp;&nbsp;&nbsp;限值: ${limits.min}-${limits.max}${unit ? unit : ''}`;
} else if (limits.min !== undefined) {
//
limitText = `&nbsp;&nbsp;&nbsp;&nbsp;限值: ${limits.min}${unit ? unit : ''}`;
} else if (limits.max !== undefined) {
//
limitText = `&nbsp;&nbsp;&nbsp;&nbsp;限值: ${limits.max}${unit ? unit : ''}`;
}
result += ('<br/>'+limitText);
}
}
}
result += '<br/>';
}
});
@ -282,12 +497,18 @@ const getChartOption = (): EChartsOption => {
legend: {
type: 'scroll',
top: 0,
right: 10, //
data: legendData,
selected: selectedState,
inactiveColor: '#ccc',
itemWidth: 16,
itemHeight: 12,
itemGap: 8,
pageButtonItemGap: 5, //
pageIconColor: '#2f4554', //
pageIconInactiveColor: '#aaa', //
pageIconSize: 12, //
pageFormatter: '{current}/{total}', // /
textStyle: {
fontSize: 14
}
@ -374,7 +595,7 @@ const handleLegendSelectChanged = (params: any) => {
// selected
const finalSelected: Record<string, boolean> = {};
indicatorConfig.forEach(config => {
indicatorConfig.value.forEach(config => {
finalSelected[config.name] = visibleSeriesQueue.value.includes(config.name);
});
@ -387,13 +608,26 @@ const handleLegendSelectChanged = (params: any) => {
// Y
const newYAxis = currentOption.yAxis.map((item: any) => {
let isShow = false;
let displayIndex = 0;
// Y
for (const key in finalSelected) {
if (key === item.name) {
isShow = finalSelected[key];
break;
if (finalSelected[key]) {
const config = indicatorConfig.value.find(c => c.name === key);
if (config && config.name === item.name) {
isShow = true;
break;
}
displayIndex++;
}
}
return { ...item, show: isShow };
// 2Y
if (!isShow || displayIndex >= 2) {
return { ...item, show: false };
}
return { ...item, show: true };
});
currentOption.yAxis = newYAxis;
@ -410,24 +644,6 @@ const handleLegendSelectChanged = (params: any) => {
}
};
// ==================== ====================
const handleDataPointClick = (params: any) => {
if (!params || !params.dataIndex) return;
//
const dataIndex = params.dataIndex;
const stationData = mockData.value[dataIndex];
if (stationData && stationData.stcd) {
modelStore.modalVisible = true;
modelStore.params.sttp = "fh_wq_point";
modelStore.title = stationData.stnm + "详情信息";
// modelStore.isBasicEdit = true;
modelStore.params.stcd = stationData.stcd;
}
};
// ==================== Y ====================
const yAxisShowDynamic = (selected: Record<string, boolean>, options: any) => {
const allShow = options.yAxis?.filter((item: any) => item.show);
@ -456,10 +672,10 @@ const yAxisShowDynamic = (selected: Record<string, boolean>, options: any) => {
return;
}
// Y1
// Y11
if (showCount >= 2) {
let leftIndex = 0;
let rightIndex = 0;
let leftAssigned = false;
let rightAssigned = false;
options.yAxis = options.yAxis.map((item: any) => {
if (!item.show) {
@ -467,8 +683,8 @@ const yAxisShowDynamic = (selected: Record<string, boolean>, options: any) => {
}
// Y
if (leftIndex === 0) {
leftIndex++;
if (!leftAssigned) {
leftAssigned = true;
delete options.grid.right;
options.grid.left = '80px';
@ -477,108 +693,45 @@ const yAxisShowDynamic = (selected: Record<string, boolean>, options: any) => {
position: 'left',
offset: 0
};
} else if (!rightAssigned) {
// Y
rightAssigned = true;
options.grid.right = '60px';
return {
...item,
position: 'right',
offset: 0
};
} else {
// Y
rightIndex++;
if (rightIndex > 1) {
options.grid.right = '100px';
return {
...item,
position: 'right',
offset: 60
};
} else {
options.grid.right = '60px';
return {
...item,
position: 'right',
offset: 0
};
}
// Y
return {
...item,
show: false
};
}
});
}
};
// ==================== ====================
const handleDataPointClick = (params: any) => {
if (!params || !params.dataIndex) return;
//
const dataIndex = params.dataIndex;
const stationData = mockData.value[dataIndex];
if (stationData && stationData.stcd) {
modelStore.modalVisible = true;
modelStore.params.sttp = "fh_wq_point";
modelStore.title = stationData.stnm + "详情信息";
// modelStore.isBasicEdit = true;
modelStore.params.stcd = stationData.stcd;
}
};
// ==================== ====================
const initChart = async () => {
if (!chartRef.value) {
console.warn('图表容器未就绪');
return;
}
//
const rect = chartRef.value.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
console.warn('图表容器尺寸为0等待渲染...');
// 使 requestAnimationFrame
return new Promise((resolve) => {
requestAnimationFrame(() => {
setTimeout(() => {
initChart().then(resolve);
}, 50);
});
});
}
//
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
//
visibleSeriesQueue.value = [indicatorConfig[0].name];
console.log('初始化可见队列:', visibleSeriesQueue.value);
// ECharts
chartInstance = echarts.init(chartRef.value);
//
const option = getChartOption();
chartInstance.setOption(option);
//
chartInstance.on('legendselectchanged', handleLegendSelectChanged);
//
chartInstance.on('click', handleDataPointClick);
// resize
setTimeout(() => {
chartInstance?.resize();
}, 50);
console.log('图表初始化成功');
};
// ==================== ====================
const handleResize = () => {
chartInstance?.resize();
};
// ==================== ====================
onMounted(async () => {
await nextTick();
//
setTimeout(() => {
initChart();
}, 50);
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
if (chartInstance) {
chartInstance.off('legendselectchanged', handleLegendSelectChanged);
chartInstance.off('click', handleDataPointClick);
chartInstance.dispose();
chartInstance = null;
}
});
//
const getecharts = async () => {
if (!select.value.value) {
@ -595,15 +748,6 @@ const getecharts = async () => {
loading.value = true;
hasData.value = false;
//
if (!chartInstance) {
console.log('图表实例不存在,重新初始化...');
await nextTick();
await initChart();
//
await new Promise(resolve => setTimeout(resolve, 100));
}
let params = {
"filter": {
"logic": "and",
@ -684,6 +828,35 @@ const getecharts = async () => {
if (stnm.includes('出库水质站')) {
stnm = stnm.replace('出库水质站', '');
}
if (stnm.includes('出库水质_')) {
stnm = stnm.replace('出库水质_', '');
}
// min max
const limits: Record<string, { min?: number; max?: number }> = {};
//
if (item.min && Array.isArray(item.min)) {
item.min.forEach((limitItem: any) => {
Object.keys(limitItem).forEach(key => {
// key "PH", "DOX" indicatorConfig
const lowerKey = key.toLowerCase();
limits[lowerKey] = limits[lowerKey] || {};
limits[lowerKey].min = limitItem[key];
});
});
}
//
if (item.max && Array.isArray(item.max)) {
item.max.forEach((limitItem: any) => {
Object.keys(limitItem).forEach(key => {
const lowerKey = key.toLowerCase();
limits[lowerKey] = limits[lowerKey] || {};
limits[lowerKey].max = limitItem[key];
});
});
}
return {
stnm: stnm,
@ -694,9 +867,39 @@ const getecharts = async () => {
ph: item.ph != null ? Number(item.ph) : null,
dox: item.dox != null ? Number(item.dox) : null,
codmn: item.codmn != null ? Number(item.codmn) : null,
codcr: item.codcr != null ? Number(item.codcr) : null,
bod5: item.bod5 != null ? Number(item.bod5) : null,
nh3n: item.nh3n != null ? Number(item.nh3n) : null,
tp: item.tp != null ? Number(item.tp) : null,
tn: item.tn != null ? Number(item.tn) : null
tn: item.tn != null ? Number(item.tn) : null,
cu: item.cu != null ? Number(item.cu) : null,
zn: item.zn != null ? Number(item.zn) : null,
f: item.f != null ? Number(item.f) : null,
se: item.se != null ? Number(item.se) : null,
ars: item.ars != null ? Number(item.ars) : null,
hg: item.hg != null ? Number(item.hg) : null,
cd: item.cd != null ? Number(item.cd) : null,
cr6: item.cr6 != null ? Number(item.cr6) : null,
pb: item.pb != null ? Number(item.pb) : null,
cn: item.cn != null ? Number(item.cn) : null,
vlph: item.vlph != null ? Number(item.vlph) : null,
oil: item.oil != null ? Number(item.oil) : null,
las: item.las != null ? Number(item.las) : null,
s2: item.s2 != null ? Number(item.s2) : null,
fcg: item.fcg != null ? Number(item.fcg) : null,
cl: item.cl != null ? Number(item.cl) : null,
so4: item.so4 != null ? Number(item.so4) : null,
no3: item.no3 != null ? Number(item.no3) : null,
thrd: item.thrd != null ? Number(item.thrd) : null,
cond: item.cond != null ? Number(item.cond) : null,
fe: item.fe != null ? Number(item.fe) : null,
mn: item.mn != null ? Number(item.mn) : null,
al: item.al != null ? Number(item.al) : null,
chla: item.chla != null ? Number(item.chla) : null,
clarity: item.clarity != null ? Number(item.clarity) : null,
tu: item.tu != null ? Number(item.tu) : null,
wtmp: item.wtmp != null ? Number(item.wtmp) : null,
limits: limits //
};
});
@ -707,25 +910,46 @@ const getecharts = async () => {
hasData.value = chartData.length > 0;
console.log('hasData 设置为:', hasData.value);
if (!hasData.value) {
//
console.log('无数据,清空图表');
chartInstance?.clear();
indicatorConfig.value = [];
loading.value = false;
return;
}
//
generateIndicatorConfig(chartData);
//
if (indicatorConfig.value.length === 0) {
console.log('没有有效指标,显示空状态');
hasData.value = false;
chartInstance?.clear();
loading.value = false;
return;
}
// - 使 nextTick DOM
await nextTick();
if (chartInstance && hasData.value) {
// 使 requestAnimationFrame
if (chartInstance) {
//
requestAnimationFrame(() => {
const option = getChartOption();
chartInstance?.setOption(option, true);
chartInstance?.resize();
console.log('图表数据已更新,共', chartData.length, '个站点');
console.log('图表数据已更新,共', chartData.length, '个站点', indicatorConfig.value.length, '个指标');
});
} else if (!hasData.value) {
//
console.log('无数据,清空图表');
chartInstance?.clear();
} else {
//
await initChart();
}
} catch (error) {
console.error('获取图表数据失败:', error);
hasData.value = false;
indicatorConfig.value = [];
} finally {
// loading - 使 nextTick tick
await nextTick();
@ -761,6 +985,90 @@ const getEndTime = () => {
return `${year}-${month}-${day} 23:59:59`;
};
// ==================== ====================
const initChart = async () => {
if (!chartRef.value) {
console.warn('图表容器未就绪');
return;
}
//
const rect = chartRef.value.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
console.warn('图表容器尺寸为0等待渲染...');
// 使 requestAnimationFrame
return new Promise((resolve) => {
requestAnimationFrame(() => {
setTimeout(() => {
initChart().then(resolve);
}, 50);
});
});
}
//
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
//
if (indicatorConfig.value.length === 0) {
console.warn('没有有效的指标配置,跳过图表初始化');
hasData.value = false;
return;
}
// sort
visibleSeriesQueue.value = [indicatorConfig.value[0].name];
console.log('初始化可见队列:', visibleSeriesQueue.value);
// ECharts
chartInstance = echarts.init(chartRef.value);
//
const option = getChartOption();
chartInstance.setOption(option);
//
chartInstance.on('legendselectchanged', handleLegendSelectChanged);
//
chartInstance.on('click', handleDataPointClick);
// resize
setTimeout(() => {
chartInstance?.resize();
}, 50);
console.log('图表初始化成功');
};
// ==================== ====================
const handleResize = () => {
chartInstance?.resize();
};
// ==================== ====================
onMounted(async () => {
await nextTick();
//
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
if (chartInstance) {
chartInstance.off('legendselectchanged', handleLegendSelectChanged);
chartInstance.off('click', handleDataPointClick);
chartInstance.dispose();
chartInstance = null;
}
});
//
const getSelect = async () => {
if (!wbsCode.value) {
@ -847,15 +1155,29 @@ const getSelect = async () => {
console.error('获取河段数据失败:', error);
}
}
//
const handlePanelChange1 = async (data) => {
console.log('当前所有控件状态:', data);
//
if (data.datetime || data.select) {
select.value.value = data.select
datetimePicker.value.value = data.datetime
getecharts()
}
}
// datetimePicker iconmap
watch(
() => datetimePicker.value.value,
(newVal) => {
if (newVal) {
iconmap.value.value = `注:最新数据时间为${newVal}`;
}
},
{ immediate: true }
);
const wbsCode = ref('');
watch(
() => JidiSelectEventStore.selectedItem,
@ -867,6 +1189,19 @@ watch(
{ deep: true, immediate: true }
);
// tooltip formatter 使 visibleSeriesQueue
watch(
() => visibleSeriesQueue.value.length,
(newLength, oldLength) => {
//
if (newLength !== oldLength && chartInstance && indicatorConfig.value.length > 0) {
// option formatter visibleSeriesQueue
const newOption = getChartOption();
chartInstance.setOption(newOption, true);
}
}
);
</script>

View File

@ -0,0 +1,399 @@
export interface WaterQualityIndicator {
name: string;
key: string;
pkey: string;
fixed: number;
sort: number;
group: string;
groupSort: string;
description: string;
children: any[];
}
export const WATER_QUALITY_INDICATORS: WaterQualityIndicator[] = [
{
"name": "水温",
"key": "wtmp",
"pkey": "",
"fixed": 0,
"sort": 0,
"group": "",
"groupSort": "",
"description": "℃",
"children": []
},
{
"name": "PH",
"key": "ph",
"pkey": "",
"fixed": 0,
"sort": 1,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "溶解氧",
"key": "dox",
"pkey": "",
"fixed": 0,
"sort": 2,
"group": "",
"groupSort": "",
"description": "mg/L",
"children": []
},
{
"name": "高锰酸盐指数",
"key": "codmn",
"pkey": "",
"fixed": 0,
"sort": 3,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "化学需氧量",
"key": "codcr",
"pkey": "",
"fixed": 0,
"sort": 4,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "五日生化需氧量BODS",
"key": "bod5",
"pkey": "",
"fixed": 0,
"sort": 5,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "氨氮",
"key": "nh3n",
"pkey": "",
"fixed": 0,
"sort": 6,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "总磷",
"key": "tp",
"pkey": "",
"fixed": 0,
"sort": 7,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "总氮",
"key": "tn",
"pkey": "",
"fixed": 0,
"sort": 8,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "铜",
"key": "cu",
"pkey": "",
"fixed": 0,
"sort": 9,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "锌",
"key": "zn",
"pkey": "",
"fixed": 0,
"sort": 10,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "氟化物",
"key": "f",
"pkey": "",
"fixed": 0,
"sort": 11,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "硒",
"key": "se",
"pkey": "",
"fixed": 0,
"sort": 12,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "砷",
"key": "ars",
"pkey": "",
"fixed": 0,
"sort": 13,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "汞",
"key": "hg",
"pkey": "",
"fixed": 0,
"sort": 14,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "镉",
"key": "cd",
"pkey": "",
"fixed": 0,
"sort": 15,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "铬(六价)",
"key": "cr6",
"pkey": "",
"fixed": 0,
"sort": 16,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "铅",
"key": "pb",
"pkey": "",
"fixed": 0,
"sort": 17,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "氰化物",
"key": "cn",
"pkey": "",
"fixed": 0,
"sort": 18,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "挥发酚",
"key": "vlph",
"pkey": "",
"fixed": 0,
"sort": 19,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "石油类",
"key": "oil",
"pkey": "",
"fixed": 0,
"sort": 20,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "阴离子表面活性剂",
"key": "las",
"pkey": "",
"fixed": 0,
"sort": 21,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "硫化物",
"key": "s2",
"pkey": "",
"fixed": 0,
"sort": 22,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "粪大肠菌群",
"key": "fcg",
"pkey": "",
"fixed": 0,
"sort": 23,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "氯化物",
"key": "cl",
"pkey": "",
"fixed": 0,
"sort": 24,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "硫酸盐",
"key": "so4",
"pkey": "",
"fixed": 0,
"sort": 25,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "硝酸盐氮",
"key": "no3",
"pkey": "",
"fixed": 0,
"sort": 26,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "总硬度",
"key": "thrd",
"pkey": "",
"fixed": 0,
"sort": 27,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "电导率",
"key": "cond",
"pkey": "",
"fixed": 0,
"sort": 28,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "铁",
"key": "fe",
"pkey": "",
"fixed": 0,
"sort": 29,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "锰",
"key": "mn",
"pkey": "",
"fixed": 0,
"sort": 30,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "铝",
"key": "al",
"pkey": "",
"fixed": 0,
"sort": 31,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "叶绿素a",
"key": "chla",
"pkey": "",
"fixed": 0,
"sort": 32,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "透明度",
"key": "clarity",
"pkey": "",
"fixed": 0,
"sort": 33,
"group": "",
"groupSort": "",
"description": "",
"children": []
},
{
"name": "浊度",
"key": "tu",
"pkey": "",
"fixed": 0,
"sort": 34,
"group": "",
"groupSort": "",
"description": "",
"children": []
}
];