WholeProcessPlatform/frontend/src/modules/EnvironmentalQuality/TwoLayers/EnvironmentalQualityTwoLayers.vue

845 lines
24 KiB
Vue
Raw Normal View History

2026-06-01 08:37:38 +08:00
<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.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-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-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-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 #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">
查看详情
</a>
</template>
</template>
</BasicTable>
</div>
</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>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import { message } from 'ant-design-vue'
import * as echarts from 'echarts'
import dayjs, { Dayjs } from 'dayjs'
import BasicTable from '@/components/BasicTable/index.vue'
import { wqGetKendoList } from '@/api/sz'
// Props 定义
const props = defineProps<{
datas?: Array<{ baseid: string; basename: string }> // 基地列表
type?: 'month' | 'year' // 时间维度
modalData?: string // 模态框数据
dateValue?: string | Dayjs // 日期值
dataDimensionVal?: string // 数据维度值
buildType?: string | number // 建设类型
}>()
// Refs
const formRef = ref()
const tableRef = ref()
const pieChartRef = ref<HTMLElement | null>(null)
let pieChartInstance: echarts.ECharts | null = null
// State
const formState = reactive({
dataDimensionVal: props.dataDimensionVal || '',
dtinType: '',
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 baseList = computed(() => props.datas || [])
const pieLoading = ref(false)
const showPieChart = ref(true)
const pieCode = ref<string[]>([]) // 饼图选中的代码,用于过滤表格
// 弹窗相关
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' }
])
// 获取列表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) => {
return {
records: res?.data?.data || [],
total: res?.data?.total || 0
}
}
// 表格列定义
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: 150,
ellipsis: true
},
{
title: '断面编码',
dataIndex: 'stcd',
key: 'stcd',
width: 120,
visible: false
},
{
title: '测站类型',
dataIndex: 'sttypeName',
key: 'sttypeName',
width: 120
},
{
title: '达标状态',
dataIndex: 'sfdbName',
key: 'sfdbName',
width: 120
},
{
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'
}
]
// 禁用未来月份
const disabledMonthDate = (current: Dayjs) => {
if (!current) return false
const now = 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) => {
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[] = [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: formState.dataDimensionVal
},
{
field: 'type',
operator: 'eq',
dataType: 'string',
value: 'DAY'
},
formState.types === 'month' ? {
field: 'drMonth',
operator: 'eq',
dataType: 'string',
value: startTime
} : {
field: 'drYear',
operator: 'eq',
dataType: 'string',
value: startTime
},
formState.dataDimensionVal !== 'all' ? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: formState.dataDimensionVal
} : null,
formState.sfdb ? {
field: 'sfdb',
operator: 'eq',
dataType: 'string',
value: formState.sfdb
} : null,
formState.stnm ? {
field: 'stnm',
operator: 'contains',
dataType: 'string',
value: formState.stnm
} : null,
formState.dataDimensionVal == 'all' && formState.dtinType ? {
field: 'dtinType',
operator: 'eq',
dataType: 'string',
value: formState.dtinType
} : null
].filter(Boolean)
const params = {
filter: {
logic: 'and',
filters
}
}
// 调用API获取饼图数据
const res = await wqGetKendoList(params)
let data = res?.data?.data || res?.data || []
if (data.length === 0) {
showPieChart.value = false
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 filteredDataSource = dataSource.filter(item => item.name !== '无数据')
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)
// 计算总数
const total = data.reduce((sum, item) => sum + item.value, 0)
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} 次 ({d}%)'
},
legend: {
orient: 'vertical',
right: '10%',
top: '10%',
data: data.map(item => item.name)
},
title: [
{
text: total.toString(),
left: '45%',
top: '50%',
textAlign: 'center',
textStyle: {
fontSize: 20,
fontWeight: 'bold',
color: '#333'
}
},
{
text: '监测总次数',
left: '45%',
top: '56%',
textAlign: 'center',
textStyle: {
fontSize: 12,
color: '#666'
}
},
{
text: '(次)',
left: '45%',
top: '61%',
textAlign: 'center',
textStyle: {
fontSize: 10,
color: '#999'
}
}
],
series: [
{
name: '断面总数量',
type: 'pie',
radius: ['20%', '35%'],
center: ['45%', '55%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}: {c} ({d}%)'
},
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold'
}
},
data: data
}
]
}
// 颜色配置(根据数据类型动态设置)
const colorConfig: any = {
color: ['#52c41a', '#ff4d4f', '#d9d9d9']
}
pieChartInstance.setOption({ ...option, ...colorConfig })
// 绑定点击事件 - 过滤表格数据
pieChartInstance.on('legendselectchanged', (params: any) => {
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'
}
const selectedCodes = data
.filter(item => selectedKeys.includes(item.name))
.map(item => item.keyData)
pieCode.value = allFalse ? ['5'] : selectedCodes
// 刷新表格
handleSearch()
})
}
// 基地变化处理
const handleBaseChange = () => {
formState.stnm = ''
fetchPieData()
handleSearch()
}
// 查询处理
const handleSearch = () => {
const timeFormat = formState.types === 'month' ? 'YYYY-MM' : 'YYYY'
const startTime = formState.dateValues ? formState.dateValues.format(timeFormat) : ''
const filters: any[] = [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: formState.dataDimensionVal
},
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'
},
formState.dataDimensionVal !== '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,
formState.dataDimensionVal === 'all' && formState.dtinType ? {
field: 'dtinType',
operator: 'eq',
dataType: 'string',
value: formState.dtinType
} : null
].filter(Boolean)
const filter = {
logic: 'and',
filters
}
// 刷新表格
tableRef.value?.getList(filter)
}
// 重置处理
const handleReset = () => {
formState.dataDimensionVal = props.dataDimensionVal || ''
formState.dtinType = ''
formState.types = props.type || 'month'
formState.sfdb = ''
formState.dateValues = dayjs()
formState.stnm = ''
pieCode.value = []
fetchPieData()
handleSearch()
}
// 查看详情
const handleViewDetail = (record: any) => {
detailData.value = { ...record }
detailModalVisible.value = true
}
// 详情弹窗关闭
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()
}
)
// 初始化
onMounted(async () => {
await nextTick()
fetchPieData()
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: 16px;
: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>