Merge branch 'main' of http://121.37.111.42:3000/zhengsl/WholeProcessPlatform into main_hzz
This commit is contained in:
commit
d984fcfbff
@ -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'
|
||||
|
||||
# 测试环境线上
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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> = [
|
||||
|
||||
@ -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: '实时视频',
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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) => {
|
||||
@ -348,12 +314,12 @@ const filterOption = (input: string, option: any) => {
|
||||
// 获取饼图数据
|
||||
const fetchPieData = async () => {
|
||||
pieLoading.value = true
|
||||
|
||||
|
||||
try {
|
||||
// 计算时间参数
|
||||
const timeFormat = formState.types === 'month' ? 'YYYY-MM' : 'YYYY'
|
||||
const startTime = formState.dateValues ? formState.dateValues.format(timeFormat) : ''
|
||||
|
||||
|
||||
if (!startTime || !formState.dataDimensionVal) {
|
||||
showPieChart.value = false
|
||||
return
|
||||
@ -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,12 +385,18 @@ const fetchPieData = async () => {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters
|
||||
}
|
||||
},
|
||||
group: [
|
||||
{
|
||||
"dir": "des",
|
||||
"field": "sfdb"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 调用API获取饼图数据
|
||||
const res = await wqGetKendoList(params)
|
||||
|
||||
|
||||
let data = res?.data?.data || res?.data || []
|
||||
|
||||
if (data.length === 0) {
|
||||
@ -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
|
||||
@ -444,11 +435,11 @@ const fetchPieData = async () => {
|
||||
}
|
||||
|
||||
showPieChart.value = true
|
||||
|
||||
|
||||
// 初始化或更新饼图
|
||||
await nextTick()
|
||||
initPieChart(filteredDataSource)
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取饼图数据失败:', error)
|
||||
message.error('获取饼图数据失败')
|
||||
@ -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
|
||||
|
||||
}
|
||||
|
||||
// 重置处理
|
||||
@ -660,8 +670,7 @@ const handleReset = () => {
|
||||
formState.dateValues = dayjs()
|
||||
formState.stnm = ''
|
||||
pieCode.value = []
|
||||
|
||||
fetchPieData()
|
||||
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
@ -676,55 +685,163 @@ 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
|
||||
window.addEventListener('resize', () => {
|
||||
pieChartInstance?.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;
|
||||
@ -841,4 +958,4 @@ defineExpose({
|
||||
:deep(.ant-spin-container) {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@ -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>
|
||||
@ -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);
|
||||
@ -189,25 +177,25 @@ const parseApiResponse = (items: any[], currentValueForApi: string, lastYearValu
|
||||
console.log(`\n--- 处理第${groupIndex + 1}个group ---`);
|
||||
console.log('group.key:', group.key);
|
||||
console.log('group.items数量:', group.items?.length);
|
||||
|
||||
|
||||
group.items?.forEach((mainGroup, mainIndex) => {
|
||||
console.log(` → 第${mainIndex + 1}个mainGroup:`);
|
||||
console.log(' field:', mainGroup.field);
|
||||
console.log(' key:', mainGroup.key);
|
||||
|
||||
|
||||
// 判断是哪种结构:检查第二层的field字段
|
||||
const field = mainGroup.field;
|
||||
|
||||
|
||||
if (field === 'ennm' || field === 'baseId') {
|
||||
// 提取中文名称:从mainGroup.key获取(站点名或基地ID)
|
||||
let displayName = '';
|
||||
|
||||
|
||||
// mainGroup.key 可能是站点名(如"两河口")或基地ID(如"02")
|
||||
if (mainGroup.key && typeof mainGroup.key === 'string') {
|
||||
displayName = mainGroup.key;
|
||||
console.log(' 从mainGroup.key获取名称:', displayName);
|
||||
}
|
||||
|
||||
|
||||
// 如果mainGroup.key不是有效的中文名称,尝试从subGroup.key获取
|
||||
if (!displayName || !/[\u4e00-\u9fa5]/.test(displayName)) {
|
||||
const subGroup = mainGroup.items?.[0];
|
||||
@ -217,7 +205,7 @@ const parseApiResponse = (items: any[], currentValueForApi: string, lastYearValu
|
||||
console.log(' 从subGroup.key获取名称:', displayName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果还是没有,尝试从最内层数据获取
|
||||
if (!displayName) {
|
||||
const subGroup = mainGroup.items?.[0];
|
||||
@ -232,25 +220,26 @@ const parseApiResponse = (items: any[], currentValueForApi: string, lastYearValu
|
||||
console.log(' 从最内层数据获取名称:', displayName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!displayName) {
|
||||
console.log(' ⚠️ 跳过:没有名称');
|
||||
return; // 跳过没有名称的数据
|
||||
}
|
||||
|
||||
|
||||
let currentPeriodValue: number | null = null;
|
||||
let lastYearPeriodValue: number | null = null;
|
||||
|
||||
// 遍历时间分组:需要深入到第3层
|
||||
const subGroup = mainGroup.items?.[0];
|
||||
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/wqRate)和年度数据(COUNT_DRYEAR)
|
||||
const value = dataItem.COUNT_DRMONTH ?? dataItem.COUNT_DRYEAR ?? dataItem.wqRate;
|
||||
console.log(` 时间:${periodKey}, 值:${value}`);
|
||||
|
||||
// 判断是当前期还是去年同期
|
||||
@ -268,7 +257,29 @@ const parseApiResponse = (items: any[], currentValueForApi: string, lastYearValu
|
||||
const finalDisplayName = displayName.replace(/干流$/, '\n干流');
|
||||
|
||||
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: {
|
||||
@ -718,22 +750,26 @@ const updateChartData = (currentLabel: string, lastYearLabel: string) => {
|
||||
};
|
||||
|
||||
chartInstance.setOption(option, { notMerge: true });
|
||||
|
||||
|
||||
// 移除旧的点击事件监听
|
||||
chartInstance.off('click');
|
||||
|
||||
|
||||
// 绑定点击事件
|
||||
chartInstance.on('click', (params: any) => {
|
||||
if (params.componentType === 'series' && params.seriesType === 'bar') {
|
||||
// 获取点击的柱子对应的数据
|
||||
const dataIndex = params.dataIndex;
|
||||
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;
|
||||
@ -924,27 +938,27 @@ const getChangeClass = () => {
|
||||
|
||||
.modal-content {
|
||||
padding: 20px;
|
||||
|
||||
|
||||
.detail-info {
|
||||
.info-header {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 2px solid #e8e8e8;
|
||||
|
||||
|
||||
h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.time-info {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.info-body {
|
||||
.info-item {
|
||||
display: flex;
|
||||
@ -953,54 +967,54 @@ const getChangeClass = () => {
|
||||
padding: 12px 16px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
|
||||
.value {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
|
||||
|
||||
&.current {
|
||||
color: #4A8BC2;
|
||||
}
|
||||
|
||||
|
||||
&.last-year {
|
||||
color: #9B59B6;
|
||||
}
|
||||
|
||||
|
||||
&.change {
|
||||
&.positive {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
|
||||
&.negative {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
|
||||
&.neutral {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.custom-section {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e8e8e8;
|
||||
|
||||
|
||||
h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
|
||||
@ -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(() => {
|
||||
// 延迟初始化,确保容器已渲染
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 23:00',
|
||||
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 => {
|
||||
};
|
||||
};
|
||||
|
||||
// Y轴配置(左1右N原则)
|
||||
const yAxisData = indicatorConfig.map((config, index) => {
|
||||
const isLeft = index === 0;
|
||||
const offset = index < 2 ? 0 : (index - 1) * 60;
|
||||
// Y轴配置(左1右1原则,最多显示2个Y轴)
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
// 最多只显示2个Y轴(索引0和1)
|
||||
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 = ` 限值: ${limits.min}-${limits.max}${unit ? unit : ''}`;
|
||||
} else if (limits.min !== undefined) {
|
||||
// 只有下限
|
||||
limitText = ` 限值: ${limits.min}${unit ? unit : ''}`;
|
||||
} else if (limits.max !== undefined) {
|
||||
// 只有上限
|
||||
limitText = ` 限值: ${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 };
|
||||
|
||||
// 最多只显示2个Y轴
|
||||
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;
|
||||
}
|
||||
|
||||
// 两个及以上Y轴:左侧1个,右侧其余
|
||||
// 两个Y轴:左侧1个,右侧1个
|
||||
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>
|
||||
|
||||
|
||||
399
frontend/src/modules/waterQuality/padig.ts
Normal file
399
frontend/src/modules/waterQuality/padig.ts
Normal 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": []
|
||||
}
|
||||
];
|
||||
Loading…
Reference in New Issue
Block a user