WholeProcessPlatform/frontend/src/modules/EnvironmentalQuality/TwoLayers/EnvironmentalQualityTwoLayers.vue
2026-06-16 17:39:51 +08:00

966 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="environmental-quality-container">
<!-- 搜索区域 -->
<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.wbsCode" :value="item.wbsCode"
:label="item.wbsName">
{{ item.wbsName }}
</a-select-option>
</a-select>
</a-form-item>
<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>
</a-select>
</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-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-option value="" label="全部">全部</a-select-option>
<a-select-option value="0" label="不达标">不达标</a-select-option>
<a-select-option value="1" label="达标">达标</a-select-option>
<a-select-option value="2" label="无目标等级/无数据">无目标等级/无数据</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="时间" name="dateValues">
<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-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">查询</a-button>
<a-button @click="handleReset">重置</a-button>
</a-space>
</a-form-item>
</a-space>
</a-form>
<!-- 图表和表格布局区域 -->
<div class="content-layout">
<!-- 左侧饼图区域 -->
<div class="chart-section">
<a-spin :spinning="pieLoading" tip="加载中...">
<div ref="pieChartRef" class="pie-chart-container"></div>
<div v-if="!showPieChart && !pieLoading" class="empty-overlay">
<a-empty description="暂无数据" />
</div>
</a-spin>
</div>
<!-- 右侧表格区域 -->
<div class="table-section">
<BasicTable ref="tableRef" :scrollY="360" :columns="columns" :list-url="wqGetKendoList"
:search-params="{ sort: sortConfig }" :transform-data="customTransform">
<template #action="{ record }">
<!-- 操作列 -->
<a @click="handleViewDetail(record)" class="text-link">
查看详情
</a>
</template>
</BasicTable>
</div>
</div>
<!-- 详情弹窗 -->
<a-modal v-model:open="detailModalVisible" :title="`${detailData.stnm || ''} 详细信息`" width="1536px" :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, 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 HJMZDTwoLays from "./HJMZDTwoLays.vue"
// 注册 dayjs 插件
dayjs.extend(isSameOrAfter)
// Props 定义
const props = defineProps<{
datas?: Array<{ wbsCode: string; wbsName: string }> // 基地列表
type?: string // 时间维度
modalData?: string // 模态框数据
dateValue?: string | Dayjs // 日期值
dataDimensionVal?: string // 数据维度值
buildType?: string | number // 建设类型
typeOne: string //区分基地和站点
baseid: string
}>()
// Refs
const formRef = ref()
const tableRef = ref()
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: props.buildType.toString() || '',
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)
const detailData = ref<any>({})
const mapModalVisible = ref(false)
const mapModalData = ref<any>({})
// 排序配置
const sortConfig = computed(() => [
{ field: 'rstcdStepSort', dir: 'asc' },
{ field: 'stnm', dir: 'asc' },
{ field: 'tm', dir: 'desc' }
])
// 自定义数据转换
const customTransform = (res: any) => {
return {
records: res?.data?.data || [],
total: res?.data?.total || 0
}
}
// 计算断面名称合并信息
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 = [
{
title: '日期',
dataIndex: 'tm',
key: 'tm',
width: 120,
customRender: ({ record }: any) => {
const date = record.tm
if (!date) return '-'
if (typeof date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
return date
}
if (typeof date === 'string' && date.includes(' ')) {
return date.split(' ')[0]
}
return date
}
},
{
title: '断面名称',
dataIndex: 'stnm',
key: 'stnm',
width: 180,
ellipsis: true,
customCell: (record: any, rowIndex: number) => {
const tableData = tableRef.value?.tableData || []
const mergeInfo = getStnmMergeInfo(tableData)
return mergeInfo[rowIndex] || { rowSpan: 1 }
}
},
{
title: '测站类型',
dataIndex: 'sttypeName',
key: 'sttypeName',
width: 120
},
{
title: '达标状态',
dataIndex: 'sfdbName',
key: 'sfdbName',
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: '水质要求',
dataIndex: 'wwqtgName',
key: 'wwqtgName',
width: 120
},
{
title: '当前水质',
dataIndex: 'wqGrdName',
key: 'wqGrdName',
width: 120
},
{
title: '去年同期水质',
dataIndex: 'beforeWqGrdName',
key: 'beforeWqGrdName',
width: 150
},
{
title: '操作',
key: 'action',
width: 100,
fixed: 'right',
slots: { customRender: 'action' }
}
]
// 禁用未来月份
const disabledMonthDate = (current: Dayjs) => {
if (!current) return false
const now = dayjs()
return current.isAfter(now, 'month')
}
// 基地选择器搜索过滤函数
const filterBaseOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
// 通用下拉框搜索过滤函数
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
// 获取饼图数据
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
}
// 构建请求参数
const filters: any[] = [
formState.dataDimensionVal != 'all' ? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: typeOne.value == 'all' ? formState.dataDimensionVal : baseid.value
} : null,
formState.types === 'month' ? {
field: 'drMonth',
operator: 'eq',
dataType: 'string',
value: startTime
} : {
field: 'drYear',
operator: 'eq',
dataType: 'string',
value: startTime
},
{
field: 'TYPE',
operator: 'eq',
dataType: 'string',
value: 'DAY'
},
typeOne.value != 'all' ? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: formState.dataDimensionVal
} : null,
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),
formState.stnm ? {
field: 'stnm',
operator: 'contains',
dataType: 'string',
value: formState.stnm
} : null,
typeOne.value == 'all' && formState.dtinType != '' ? {
field: 'dtinType',
operator: 'eq',
dataType: 'string',
value: formState.dtinType
} : null
].filter(Boolean)
const params = {
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) {
// 销毁图表实例
if (pieChartInstance) {
pieChartInstance.dispose()
pieChartInstance = null
}
// 重置相关状态
displayTotal.value = 0
pieCode.value = []
showPieChart.value = false
return
}
// 转换数据格式 - 为每个数据项设置对应颜色
const dataSource = data.map((item: any) => {
const name = item.keyName || 'null'
// 根据名称设置颜色:达标-绿色,不达标-红色,其他-灰色
let color = '#d9d9d9'
if (name === '达标') {
color = '#52c41a' // 绿色
} else if (name === '不达标') {
color = '#ff4d4f' // 红色
}
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
return
}
showPieChart.value = true
// 初始化或更新饼图
await nextTick()
initPieChart(filteredDataSource)
} catch (error) {
console.error('获取饼图数据失败:', error)
message.error('获取饼图数据失败')
showPieChart.value = false
} finally {
pieLoading.value = false
}
}
// 初始化饼图
const initPieChart = (data: any[]) => {
if (!pieChartRef.value) return
// 如果实例已存在,先销毁
if (pieChartInstance) {
pieChartInstance.dispose()
pieChartInstance = null
}
pieChartInstance = echarts.init(pieChartRef.value)
// 计算总数并初始化 displayTotal
const total = data.reduce((sum, item) => sum + item.value, 0)
displayTotal.value = total
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} 次 ({d}%)'
},
legend: {
orient: 'vertical',
right: '10%',
top: '10%',
data: data.map(item => item.name)
},
title: [
{
text: displayTotal.value.toString(),
left: '39%',
top: '38%',
textAlign: 'center',
textStyle: {
fontSize: 20,
fontWeight: 'bold',
color: '#2f6b98'
}
},
{
text: '监测总次数',
left: '39%',
top: '44%',
textAlign: 'center',
textStyle: {
fontSize: 13,
color: '#333333',
fontWeight: '500'
}
},
{
text: '(次)',
left: '39%',
top: '49%',
textAlign: 'center',
textStyle: {
fontSize: 13,
color: '#333333',
fontWeight: '500'
}
}
],
series: [
{
name: '断面总数量',
type: 'pie',
radius: ['25%', '35%'],
center: ['40%', '45%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}'
},
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold'
}
},
data: data
}
]
}
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)
// 动态计算当前显示的总数
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()
})
}
// 基地变化处理
const handleBaseChange = () => {
formState.stnm = ''
handleSearch()
}
// 查询处理
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: typeOne.value == 'all' ? formState.dataDimensionVal : baseid.value
} : null,
formState.types === 'month' ? {
field: 'drMonth',
operator: 'eq',
dataType: 'string',
value: startTime
} : {
field: 'drYear',
operator: 'eq',
dataType: 'string',
value: startTime
},
{
field: 'TYPE',
operator: 'eq',
dataType: 'string',
value: 'DAY'
},
typeOne.value != 'all' ? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: formState.dataDimensionVal
} : null,
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),
formState.stnm ? {
field: 'stnm',
operator: 'contains',
dataType: 'string',
value: formState.stnm
} : null,
typeOne.value == 'all' && formState.dtinType != '' ? {
field: 'dtinType',
operator: 'eq',
dataType: 'string',
value: formState.dtinType
} : null
].filter(Boolean)
const filter = {
logic: 'and',
filters
}
// 刷新表格
tableRef.value?.getList(filter)
if (kong.value) {
// debugger
fetchPieData()
}
kong.value = true
}
// 重置处理
const handleReset = () => {
formState.dataDimensionVal = props.dataDimensionVal || ''
formState.dtinType = ''
formState.types = props.type || 'month'
formState.sfdb = ''
formState.dateValues = dayjs()
formState.stnm = ''
pieCode.value = []
handleSearch()
}
// 查看详情
const handleViewDetail = (record: any) => {
detailData.value = { ...record }
detailModalVisible.value = true
}
// 详情弹窗关闭
const handleDetailModalClose = () => {
detailModalVisible.value = false
detailData.value = {}
}
// 地图弹窗关闭
const handleMapModalClose = () => {
mapModalVisible.value = false
mapModalData.value = {}
}
// 监听日期变化
watch(
() => formState.dateValues,
() => {
handleSearch()
}
)
// 监听 props.dataDimensionVal 变化,重置表单状态并重新加载数据
watch(
() => props.dataDimensionVal,
(newVal) => {
// 重置表单状态
formState.dataDimensionVal = newVal || ''
formState.dtinType = props.buildType.toString() || ''
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()
handleSearch()
// 监听窗口resize
window.addEventListener('resize', () => {
pieChartInstance?.resize()
})
})
// 组件卸载时清理
onBeforeUnmount(() => {
pieChartInstance?.dispose()
pieChartInstance = null
window.removeEventListener('resize', () => {
pieChartInstance?.resize()
})
})
// 暴露方法给父组件
defineExpose({
handleSearch
})
</script>
<style scoped lang="scss">
.environmental-quality-container {
height: 100%;
display: flex;
flex-direction: column;
.search-form {
background: #fff;
border-radius: 4px;
padding: 0px 0px 16px 0px;
:deep(.ant-space) {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
:deep(.ant-form-item) {
margin-bottom: 0;
}
}
.content-layout {
flex: 1;
display: flex;
gap: 16px;
overflow: hidden;
.chart-section {
width: 35%;
min-width: 300px;
background: #fff;
border-radius: 4px;
padding: 16px;
position: relative;
.pie-chart-container {
width: 100%;
height: 482px;
}
.empty-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.9);
z-index: 10;
}
}
.table-section {
flex: 1;
background: #fff;
border-radius: 4px;
padding: 16px;
overflow: hidden;
}
}
.text-link {
color: #2f6b98;
&:hover {
color: #40a9ff;
}
}
.modal-content {
padding: 20px;
max-height: 600px;
overflow-y: auto;
pre {
background: #f5f5f5;
padding: 12px;
border-radius: 4px;
font-size: 12px;
margin-top: 16px;
}
}
}
:deep(.ant-spin-nested-loading) {
height: 100%;
}
:deep(.ant-spin-container) {
height: 100%;
}
</style>