添加下拉框选择,右侧切换功能
This commit is contained in:
parent
69e46e9914
commit
800a2665a0
@ -2,7 +2,7 @@
|
||||
<div
|
||||
id="map-baselayer"
|
||||
class="baselayer-switcher"
|
||||
:style="{ right: drawerOpen ? '480px' : '12px' }"
|
||||
:style="{ right: drawerOpen ? '480px' : '26px' }"
|
||||
v-if="uiStore.mapType == '2D'"
|
||||
>
|
||||
<div
|
||||
@ -21,7 +21,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import { useMapConfigStore } from '@/modules/map/stores/map-config.store';
|
||||
import { useMapViewStore } from '@/modules/map/stores/map-view.store';
|
||||
@ -41,14 +41,9 @@ const props = defineProps({
|
||||
const uiStore = useUiStore();
|
||||
const mapConfigStore = useMapConfigStore();
|
||||
const mapViewStore = useMapViewStore();
|
||||
const drawerOpen = ref(uiStore.drawerOpen);
|
||||
|
||||
// 监听 store 中的 drawerOpen 变化
|
||||
watch(
|
||||
() => uiStore.drawerOpen,
|
||||
newVal => {
|
||||
drawerOpen.value = newVal;
|
||||
}
|
||||
// 抽屉展开(页面自带抽屉或全局抽屉任一打开)时,底图切换整体左移
|
||||
const drawerOpen = computed(
|
||||
() => uiStore.drawerOpen || uiStore.globalDrawerOpen
|
||||
);
|
||||
const layers = [
|
||||
{ key: 's_province_boundaries', name: '矢量', img: shiliangImg },
|
||||
@ -66,7 +61,7 @@ const activeKey = ref(layers[0].key);
|
||||
// 监听外部通过 store 切换底图(如电站专题页)
|
||||
watch(
|
||||
() => mapViewStore.activeBaseLayerKey,
|
||||
(newKey) => {
|
||||
newKey => {
|
||||
if (newKey && layers.some(l => l.key === newKey)) {
|
||||
activeKey.value = newKey;
|
||||
}
|
||||
|
||||
@ -66,6 +66,7 @@ interface Props {
|
||||
processEmptyValues?: boolean; // 新增:是否开启空值处理
|
||||
enableEllipsis?: boolean; // 新增:是否开启超出隐藏
|
||||
enableSort?: boolean; // 新增:是否启用排序功能
|
||||
paginationConfig?: Record<string, any>; // 自定义分页配置,传入则覆盖默认分页配置
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@ -165,7 +166,8 @@ const rowSelection = computed(() => ({
|
||||
}));
|
||||
|
||||
// --- Pagination Config ---
|
||||
const paginationConfig = computed(() => ({
|
||||
const paginationConfig = computed(() => {
|
||||
const defaultConfig: Record<string, any> = {
|
||||
total: total.value,
|
||||
current: page.value,
|
||||
pageSize: size.value,
|
||||
@ -173,7 +175,10 @@ const paginationConfig = computed(() => ({
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
pageSizeOptions: ['20', '50', '100']
|
||||
}));
|
||||
};
|
||||
// 传入的自定义分页配置与默认配置合并,未传的字段使用默认值
|
||||
return { ...defaultConfig, ...props.paginationConfig };
|
||||
});
|
||||
|
||||
const $slots = useSlots();
|
||||
|
||||
|
||||
115
frontend/src/components/GlobalRightDrawer/index.vue
Normal file
115
frontend/src/components/GlobalRightDrawer/index.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<!-- 全局右侧抽屉:由 AppMain 承载,展示锚点搜索内容(基本信息 + 监测表格),受 uiStore.globalDrawerOpen 控制 -->
|
||||
<template>
|
||||
<div class="global-right-drawer">
|
||||
<div
|
||||
@click="handleToggle"
|
||||
class="drawerController1"
|
||||
v-if="!uiStore.globalDrawerOpen"
|
||||
>
|
||||
<img src="../../assets/components/arrow-left.png" alt="" />
|
||||
</div>
|
||||
|
||||
<a-drawer
|
||||
:get-container="false"
|
||||
:style="{ position: 'relative' }"
|
||||
v-model:open="uiStore.globalDrawerOpen"
|
||||
:mask="false"
|
||||
placement="right"
|
||||
width="450"
|
||||
:closable="false"
|
||||
:headerStyle="{ color: '#FAFCFE' }"
|
||||
>
|
||||
<div @click="handleToggle" class="drawerController">
|
||||
<img src="../../assets/components/arrow-right.png" alt="" />
|
||||
</div>
|
||||
<div style="padding: 16px 16px 0" class="text_she">
|
||||
<slot />
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineOptions } from 'vue';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
|
||||
// 定义组件名 (便于调试)
|
||||
defineOptions({
|
||||
name: 'globalRightDrawer'
|
||||
});
|
||||
|
||||
const uiStore = useUiStore();
|
||||
|
||||
const handleToggle = () => {
|
||||
uiStore.setGlobalDrawerOpen(!uiStore.globalDrawerOpen);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.global-right-drawer {
|
||||
width: 450px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
pointer-events: all;
|
||||
|
||||
.drawerController1 {
|
||||
width: 18px;
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
height: 88px;
|
||||
line-height: 88px;
|
||||
top: 45%;
|
||||
vertical-align: middle;
|
||||
background-image: url(../../assets/components/bg-toggle.e1dabcf3.svg);
|
||||
background-repeat: no-repeat;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.drawerController {
|
||||
width: 18px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
height: 88px;
|
||||
line-height: 88px;
|
||||
top: 45%;
|
||||
left: -18px;
|
||||
vertical-align: middle;
|
||||
background-image: url(../../assets/components/bg-toggle.e1dabcf3.svg);
|
||||
background-repeat: no-repeat;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer {
|
||||
// margin: 3px 0px;
|
||||
.ant-drawer-content {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ant-drawer-content-wrapper {
|
||||
border: 2px solid #c5d6e2 !important;
|
||||
box-shadow: 3px 3px 3px 9px #e5edf3 !important;
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding: 0px !important;
|
||||
}
|
||||
}
|
||||
.text_she {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
font-variant: tabular-nums;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
||||
Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji,
|
||||
Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
}
|
||||
</style>
|
||||
@ -2349,7 +2349,7 @@ const FBPointColumns: Array<any> = [
|
||||
},
|
||||
{
|
||||
name: '生产工艺',
|
||||
filed: 'zzfkgy',
|
||||
filed: 'zzflgy',
|
||||
visible: true,
|
||||
type: 'select',
|
||||
url: ''
|
||||
@ -2478,7 +2478,7 @@ const VaPointColumns: Array<any> = [
|
||||
},
|
||||
{
|
||||
name: '保护方式',
|
||||
filed: 'bhfs',
|
||||
filed: 'protmthd',
|
||||
visible: true,
|
||||
type: 'input',
|
||||
url: ''
|
||||
@ -2586,7 +2586,7 @@ const VPPointColumns: Array<any> = [
|
||||
},
|
||||
{
|
||||
name: '保护方式',
|
||||
filed: 'bhfs',
|
||||
filed: 'protmthd',
|
||||
visible: true,
|
||||
type: 'input',
|
||||
url: ''
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
<div class="tab-content">
|
||||
<div class="content-body">
|
||||
<div class="chart-wrapper">
|
||||
<MonitorInfoEcharts :data="chartData" :is-active="isActive" />
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
<a-empty
|
||||
v-if="!chartData || chartData.length === 0"
|
||||
description="暂无数据"
|
||||
@ -45,12 +45,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import * as echarts from 'echarts';
|
||||
import { getMonitorData } from '@/api/mapModal';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import MonitorInfoEcharts from '../componentsEcharts/MonitorInfoEcharts.vue';
|
||||
import { DateSetting } from '@/utils/enumeration';
|
||||
|
||||
const modelStore = useModelStore();
|
||||
@ -64,6 +64,323 @@ const isLoading = ref(false);
|
||||
const tableData = ref<any[]>([]);
|
||||
const chartData = ref<any[]>([]);
|
||||
|
||||
// ==================== 图表 ====================
|
||||
const chartRef = ref<HTMLElement>();
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
if (chartInstance) chartInstance.dispose();
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
updateChart(chartData.value);
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
const updateChart = (data: any[]) => {
|
||||
if (!chartInstance) return;
|
||||
if (!data || data.length === 0) {
|
||||
chartInstance.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const sorted = [...data].sort(
|
||||
(a, b) => new Date(a.tm).getTime() - new Date(b.tm).getTime()
|
||||
);
|
||||
|
||||
const xAxisData = sorted.map(item => {
|
||||
const hm = dayjs(item.tm).format('HH:mm');
|
||||
if (hm === '00:00') {
|
||||
const date = dayjs(item.tm).format('MM-DD');
|
||||
return `${hm}\n${date}`;
|
||||
}
|
||||
return hm;
|
||||
});
|
||||
|
||||
const rzData = sorted.map(item => item.rz);
|
||||
const dzData = sorted.map(item => item.dz);
|
||||
const qiData = sorted.map(item => item.qi);
|
||||
const qoData = sorted.map(item => item.qo);
|
||||
const qecData = sorted.map(item => item.qec);
|
||||
const qecLimitData = sorted.map(item => item.qecLimit);
|
||||
|
||||
const formatRules: Record<
|
||||
string,
|
||||
{ format: (v: number) => string; unit: string }
|
||||
> = {
|
||||
坝上水位: { format: v => v.toFixed(2), unit: '(m)' },
|
||||
坝下水位: { format: v => v.toFixed(2), unit: '(m)' },
|
||||
入库流量: { format: v => String(Math.round(v)), unit: '(m³/s)' },
|
||||
出库流量: { format: v => String(Number(v)), unit: '(m³/s)' },
|
||||
生态流量: { format: v => v.toFixed(1), unit: '(m³/s)' },
|
||||
生态流量限值: { format: v => v.toFixed(1), unit: '(m³/s)' }
|
||||
};
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
textStyle: { color: '#fff', fontSize: 12 },
|
||||
axisPointer: { type: 'cross' },
|
||||
formatter: (params: any) => {
|
||||
if (!params || params.length === 0) return '';
|
||||
const dataIndex = params[0].dataIndex;
|
||||
const fullTime = sorted[dataIndex]?.tm
|
||||
? dayjs(sorted[dataIndex].tm).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '';
|
||||
let html = `<div style="font-size:16px;margin-bottom:8px;">${fullTime}</div>`;
|
||||
params.forEach((param: any) => {
|
||||
if (param.value == null) return;
|
||||
const rule = Object.entries(formatRules).find(([key]) =>
|
||||
param.seriesName.includes(key)
|
||||
);
|
||||
const displayValue = rule
|
||||
? rule[1].format(Number(param.value))
|
||||
: param.value;
|
||||
const unit = rule ? rule[1].unit : '';
|
||||
html += `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin:4px 0;">
|
||||
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:8px;"></span>
|
||||
<span style="flex:1;font-size:14px;text-align:left;margin-right:6px;">${param.seriesName}: </span>
|
||||
<span style="font-size:14px;min-width:60px;text-align:right;"><strong>${displayValue}</strong> ${unit}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
return html;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
top: 10,
|
||||
right: 60,
|
||||
width: '80%',
|
||||
data: [
|
||||
'坝上水位',
|
||||
'坝下水位',
|
||||
'入库流量',
|
||||
'出库流量',
|
||||
'生态流量',
|
||||
'生态流量限值'
|
||||
],
|
||||
textStyle: { fontSize: 12 },
|
||||
selected: {
|
||||
坝上水位: false,
|
||||
坝下水位: false,
|
||||
入库流量: true,
|
||||
出库流量: true,
|
||||
生态流量: false,
|
||||
生态流量限值: false
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 60,
|
||||
top: 80,
|
||||
bottom: 60
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { fontSize: 12 },
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
name: '水位(m)',
|
||||
type: 'value',
|
||||
position: 'left',
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
alignTicks: true,
|
||||
scale: true,
|
||||
splitNumber: 9
|
||||
// boundaryGap: ['10%', 0]
|
||||
},
|
||||
{
|
||||
name: '流量(m³/s)',
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
splitNumber: 9
|
||||
// boundaryGap: ['10%', 0]
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '坝上水位',
|
||||
type: 'line',
|
||||
yAxisIndex: 0,
|
||||
data: rzData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#56C2E3', width: 2 },
|
||||
itemStyle: { color: '#56C2E3' },
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: 'Max' },
|
||||
{ type: 'min', name: 'Min' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '坝下水位',
|
||||
type: 'line',
|
||||
yAxisIndex: 0,
|
||||
data: dzData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#7399C6', width: 2 },
|
||||
itemStyle: { color: '#7399C6' },
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: 'Max' },
|
||||
{ type: 'min', name: 'Min' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '入库流量',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: qiData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#4B79AB', width: 2 },
|
||||
itemStyle: { color: '#4B79AB' },
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: 'Max' },
|
||||
{ type: 'min', name: 'Min' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '出库流量',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: qoData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#78C300', width: 2 },
|
||||
itemStyle: { color: '#78C300' },
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: 'Max' },
|
||||
{ type: 'min', name: 'Min' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '生态流量',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: qecData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#00A050', width: 2 },
|
||||
itemStyle: { color: '#00A050' },
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: 'Max' },
|
||||
{ type: 'min', name: 'Min' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '生态流量限值',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: qecLimitData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#F7A737', width: 2 },
|
||||
itemStyle: { color: '#F7A737' },
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', name: 'Max' },
|
||||
{ type: 'min', name: 'Min' }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0],
|
||||
throttle: 50,
|
||||
start: 0,
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
|
||||
},
|
||||
right: 20,
|
||||
top: 10
|
||||
}
|
||||
};
|
||||
|
||||
chartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
// 激活时初始化图表并处理自适应,数据变化时动态刷新
|
||||
watch(
|
||||
() => props.isActive,
|
||||
active => {
|
||||
if (!active) return;
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
setTimeout(() => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
}, 200);
|
||||
if (!resizeObserver && chartRef.value) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
});
|
||||
resizeObserver.observe(chartRef.value);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(chartData, newData => {
|
||||
updateChart(newData);
|
||||
});
|
||||
|
||||
const handleResize = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
});
|
||||
|
||||
const tabsList = [{ name: '电站运行过程线', key: 'dzxq.tabs.jcsj' }];
|
||||
const activeTabKey = ref('dzxq.tabs.jcsj');
|
||||
|
||||
@ -261,6 +578,11 @@ watch(
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding-top: 10px;
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
|
||||
@ -32,6 +32,7 @@ import {
|
||||
getNormalAddedSituationYear,
|
||||
getNormalResearchSituation
|
||||
} from '@/api/mapModal';
|
||||
import dayjs from 'dayjs';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
|
||||
@ -45,8 +46,9 @@ const hasLoaded = ref(false);
|
||||
const tableRef = ref();
|
||||
|
||||
// 搜索参数
|
||||
|
||||
const searchParams = ref({
|
||||
tm: null as string | null
|
||||
tm: dayjs().format('YYYY')
|
||||
});
|
||||
|
||||
// 表格列配置
|
||||
@ -109,7 +111,10 @@ const getYearList = async () => {
|
||||
|
||||
// 刷新表格
|
||||
const refreshTable = () => {
|
||||
const year = searchParams.value.tm;
|
||||
let year = searchParams.value.tm;
|
||||
if (year == null) {
|
||||
year = dayjs().format('YYYY');
|
||||
}
|
||||
const startDate = `${year}-01-01 00:00:00`;
|
||||
const endDate = `${year}-12-31 23:59:59`;
|
||||
|
||||
|
||||
@ -612,8 +612,6 @@ const applyTabFilters = async (params: any) => {
|
||||
try {
|
||||
const sttpCode = params?.sttp;
|
||||
const stcd = params?.stcd;
|
||||
const eqtp = params?.eqtp;
|
||||
const baseId = modelStore.baseId;
|
||||
|
||||
if (!stcd) return;
|
||||
|
||||
@ -690,7 +688,6 @@ const qxdStationOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const qxdSelectedStcd = ref<string>('');
|
||||
const qxdSelectedStcdWq = ref<string>('');
|
||||
const qxdSelectedStcdZq = ref<string>('');
|
||||
const qxdLoadedCodes = ref<Set<string>>(new Set());
|
||||
|
||||
// 获取当前tab对应的qxd配置
|
||||
const getCurrentQxdConfig = (tabKey: string) => {
|
||||
@ -731,13 +728,18 @@ const setQxdSelectedStcd = (tabKey: string, value: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 加载栖息地站点列表
|
||||
// 加载栖息地站点列表(每次切换都会重新加载,保留 qxd 模式判断)
|
||||
let qxdRequestSeq = 0; // 请求序号,用于竞态守卫(仅最后一次请求生效)
|
||||
const loadQxdStationList = async (tabKey: string) => {
|
||||
const code = getTabCode(tabKey);
|
||||
console.log(code);
|
||||
|
||||
if (!code || !qxdConfigMap[code]) return;
|
||||
if (qxdLoadedCodes.value.has(code)) return;
|
||||
console.log(modelStore.params.stcd);
|
||||
|
||||
const config = qxdConfigMap[code];
|
||||
const requestSeq = ++qxdRequestSeq;
|
||||
qxdStationOptions.value = []; // 先清空,等待加载
|
||||
try {
|
||||
const params = {
|
||||
filter: {
|
||||
@ -760,7 +762,9 @@ const loadQxdStationList = async (tabKey: string) => {
|
||||
select: ['stcd', 'stnm']
|
||||
};
|
||||
const res = await config.apiFn(params);
|
||||
if (requestSeq !== qxdRequestSeq) return; // 非最新请求,丢弃结果
|
||||
const stations = res?.data?.data || res?.data?.records || [];
|
||||
console.log(res);
|
||||
if (stations.length > 0) {
|
||||
qxdStationOptions.value = stations.map((item: any) => ({
|
||||
label: item.stnm,
|
||||
@ -768,8 +772,8 @@ const loadQxdStationList = async (tabKey: string) => {
|
||||
}));
|
||||
setQxdSelectedStcd(tabKey, stations[0].stcd);
|
||||
}
|
||||
qxdLoadedCodes.value.add(code);
|
||||
} catch (error) {
|
||||
if (requestSeq !== qxdRequestSeq) return; // 非最新请求,忽略错误
|
||||
console.error(`获取站点列表失败(${code}):`, error);
|
||||
}
|
||||
};
|
||||
@ -793,12 +797,6 @@ const handleQxdStationChange = () => {
|
||||
// import MapView from './components/MapView.vue';
|
||||
// import SurroundingInfo from './components/SurroundingInfo.vue';
|
||||
|
||||
// 定义 Tab 配置项接口
|
||||
interface TabItem {
|
||||
key: string;
|
||||
title: string;
|
||||
url: string;
|
||||
}
|
||||
// 获取 Tab 配置项的 URL
|
||||
const getTabUrl = (key: string) => {
|
||||
const tab = tabsConfig.value.find((item: any) => item.key === key);
|
||||
@ -914,7 +912,6 @@ const handleClose = () => {
|
||||
qxdSelectedStcd.value = '';
|
||||
qxdSelectedStcdWq.value = '';
|
||||
qxdSelectedStcdZq.value = '';
|
||||
qxdLoadedCodes.value = new Set();
|
||||
// 清理电站专题状态
|
||||
hasPowerStatData.value = false;
|
||||
modelStore.verticalWaterStcd = '';
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<div
|
||||
id="map-controller"
|
||||
class="map-controller"
|
||||
:style="{ right: drawerOpen ? '480px' : '12px' }"
|
||||
:style="{ right: drawerOpen ? '480px' : '26px' }"
|
||||
>
|
||||
<div
|
||||
class="map-controller-group"
|
||||
@ -74,16 +74,11 @@ const map = toRef(props, 'map');
|
||||
|
||||
// 使用 Pinia store
|
||||
const uiStore = useUiStore();
|
||||
const drawerOpen = ref(uiStore.drawerOpen);
|
||||
const tjVisible = ref(false);
|
||||
|
||||
// 监听 store 中的 drawerOpen 变化
|
||||
watch(
|
||||
() => uiStore.drawerOpen,
|
||||
newVal => {
|
||||
drawerOpen.value = newVal;
|
||||
}
|
||||
// 抽屉展开(页面自带抽屉或全局抽屉任一打开)时,控制器整体左移
|
||||
const drawerOpen = computed(
|
||||
() => uiStore.drawerOpen || uiStore.globalDrawerOpen
|
||||
);
|
||||
const tjVisible = ref(false);
|
||||
|
||||
watch(
|
||||
() => uiStore.mapType,
|
||||
|
||||
@ -140,6 +140,19 @@ const modelStore = useModelStore();
|
||||
const mapClass = MapClass.getInstance();
|
||||
const ENG_POINT_LAYER_KEY = 'eng_point';
|
||||
const YLFB_POINT_LAYER_KEY = 'ylfb_point';
|
||||
// 需要弹右侧搜索抽屉的 sttpCode(按图层类型全局配置)
|
||||
const DRAWER_STTP_CODES = [
|
||||
'ENG',
|
||||
'WTRV',
|
||||
'WTVT',
|
||||
'WQ',
|
||||
'FP',
|
||||
'FH',
|
||||
'ZQ',
|
||||
'VP',
|
||||
'VA',
|
||||
'EQ'
|
||||
];
|
||||
const siteRangePicker = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '大型电站', value: 'large_eng_built' },
|
||||
@ -231,7 +244,10 @@ watch(
|
||||
anchorPointSelect: null,
|
||||
fishSurveyZhuanZhi: false
|
||||
};
|
||||
handleSearch(false);
|
||||
uiStore.setGlobalDrawerOpen(false);
|
||||
setTimeout(() => {
|
||||
uiStore.setDrawerOpen(true);
|
||||
}, 300);
|
||||
resetFishSurveyState();
|
||||
}
|
||||
);
|
||||
@ -599,7 +615,10 @@ watch(
|
||||
return;
|
||||
}
|
||||
formModel.value.anchorPointSelect = null;
|
||||
handleSearch(false);
|
||||
uiStore.setGlobalDrawerOpen(false);
|
||||
setTimeout(() => {
|
||||
uiStore.setDrawerOpen(true);
|
||||
}, 300);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
@ -641,7 +660,10 @@ watch(
|
||||
|
||||
const handleCapacityChange = (value: string) => {
|
||||
formModel.value.anchorPointSelect = null;
|
||||
handleSearch(false);
|
||||
uiStore.setGlobalDrawerOpen(false);
|
||||
setTimeout(() => {
|
||||
uiStore.setDrawerOpen(true);
|
||||
}, 300);
|
||||
mapOrchestrator.changeEngPointCapacity(value);
|
||||
};
|
||||
|
||||
@ -665,26 +687,38 @@ const focusSelectedAnchorPoint = () => {
|
||||
// isFishSurveyMode.value ? anchorPointOptions.value : undefined
|
||||
// );
|
||||
};
|
||||
|
||||
// 备注:搜索定位只分发定位命令,不在筛选组件里直接操作地图实例。
|
||||
// 抽屉是否弹出由选中点位的 sttpCode 决定(命中 DRAWER_STTP_CODES 才弹)。
|
||||
const handleAnchorPointChange = (value: string) => {
|
||||
if (value == null) handleSearch(false);
|
||||
if (!value) return;
|
||||
focusSelectedAnchorPoint();
|
||||
handleSearch(true);
|
||||
};
|
||||
|
||||
// 备注:用 function 声明以提升到作用域顶部,供上方 immediate watch 安全调用。
|
||||
function handleSearch(open: boolean) {
|
||||
if (uiStore.searchDrawerOpen === open) {
|
||||
if (value == null) {
|
||||
// 清空:先收起全局抽屉,等完全关闭后再恢复页面自带抽屉
|
||||
uiStore.setGlobalDrawerOpen(false);
|
||||
setTimeout(() => {
|
||||
uiStore.setDrawerOpen(true);
|
||||
}, 300);
|
||||
return;
|
||||
}
|
||||
if (!value) return;
|
||||
focusSelectedAnchorPoint();
|
||||
|
||||
const matched = anchorPointOptions.value.find(item => item.stcd === value);
|
||||
const sttpCode = String(
|
||||
matched?.sttpCode || matched?.sttp || ''
|
||||
).toUpperCase();
|
||||
// 非抽屉图层:不弹抽屉,收起全局抽屉后恢复页面自带抽屉
|
||||
if (!DRAWER_STTP_CODES.includes(sttpCode)) {
|
||||
uiStore.setGlobalDrawerOpen(false);
|
||||
setTimeout(() => {
|
||||
uiStore.setDrawerOpen(true);
|
||||
}, 300);
|
||||
return;
|
||||
}
|
||||
// 需要抽屉:先收起页面自带抽屉,等完全关闭后再展示全局抽屉
|
||||
uiStore.setDrawerOpen(false);
|
||||
setTimeout(() => {
|
||||
uiStore.setSearchDrawerOpen(open);
|
||||
uiStore.setDrawerOpen(true);
|
||||
}, 500);
|
||||
}
|
||||
uiStore.setGlobalDrawerOpen(true);
|
||||
}, 300);
|
||||
};
|
||||
watch(
|
||||
() => uiStore.mapSwitchCompletedTick,
|
||||
async (newVal, oldVal) => {
|
||||
|
||||
@ -4,17 +4,20 @@ import { useTagsViewStore } from '@/store/modules/tagsView';
|
||||
import { useRoute } from 'vue-router';
|
||||
import MapModal from '@/components/MapModal/index.vue';
|
||||
import ylfbModal from '@/components/ylfbModal/index.vue';
|
||||
import GlobalRightDrawer from '@/components/GlobalRightDrawer/index.vue';
|
||||
import basicInfoMod from '@/modules/rightSearchDrawer/basicInfo/index.vue';
|
||||
import monitoringTableMod from '@/modules/rightSearchDrawer/monitoringTable/index.vue';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import GisView from '@/components/gis/GisView.vue';
|
||||
|
||||
const modelStore = useModelStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
const router = useRoute();
|
||||
const routeKey = computed(() => router.path + Math.random());
|
||||
const route = useRoute();
|
||||
const routeKey = computed(() => route.path + Math.random());
|
||||
|
||||
// 智能配图页面隐藏底层 GisView(避免地图叠放冲突)
|
||||
const isZhiNengPeiTu = computed(() => {
|
||||
return router.path === '/zhiNengFenXi/zhiNengPeiTu/index';
|
||||
return route.path === '/zhiNengFenXi/zhiNengPeiTu/index';
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -31,6 +34,14 @@ const isZhiNengPeiTu = computed(() => {
|
||||
</router-view>
|
||||
</div>
|
||||
|
||||
<!-- 全局抽屉:承载锚点搜索内容(基本信息 + 监测表格),由锚点下拉 sttpCode 驱动打开 -->
|
||||
<div class="global-drawer-wrap">
|
||||
<GlobalRightDrawer>
|
||||
<basicInfoMod />
|
||||
<monitoringTableMod />
|
||||
</GlobalRightDrawer>
|
||||
</div>
|
||||
|
||||
<MapModal
|
||||
v-model:visible="modelStore.modalVisible"
|
||||
:title="modelStore.title"
|
||||
@ -58,4 +69,13 @@ const isZhiNengPeiTu = computed(() => {
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.global-drawer-wrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 450px;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -52,13 +52,20 @@
|
||||
:footer="null"
|
||||
@cancel="handleModalClose"
|
||||
>
|
||||
<FishResource v-if="modalVisible" :modalData = "modalData" />
|
||||
<FishResource v-if="modalVisible" :modalData="modalData" />
|
||||
</a-modal>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue';
|
||||
import {
|
||||
ref,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
nextTick,
|
||||
watch,
|
||||
defineOptions
|
||||
} from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts } from 'echarts';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
@ -832,7 +839,7 @@ watch(
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
width: 400px;
|
||||
height: 290px;
|
||||
// border: 1px solid #7fd6ff;
|
||||
border-radius: 5px;
|
||||
|
||||
@ -3,39 +3,115 @@
|
||||
<SidePanelItem title="设施类型及接入情况">
|
||||
<a-spin :spinning="loading">
|
||||
<div v-if="dataJson.length > 0" class="card-container">
|
||||
<div v-for="(item) in dataJson" class="facility-card" @click="imgclick(item)">
|
||||
<template v-for="(item, index) in dataJson" :key="index">
|
||||
<div
|
||||
v-if="index < 2"
|
||||
:key="index"
|
||||
:class="'facility-card' + index"
|
||||
@click="imgclick(item)"
|
||||
>
|
||||
<div class="img_title">
|
||||
<div class="img_icon">
|
||||
<i class="icon iconfont" :class="item?.icon" />
|
||||
</div>
|
||||
<div class="img_text">
|
||||
<div>{{ item.name }}</div>
|
||||
<div class="text_num">{{ item.totalNum }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="img_num">
|
||||
<div>
|
||||
已接入
|
||||
<span
|
||||
:class="{
|
||||
img_num_span1: index == 0,
|
||||
img_num_span3: index == 1
|
||||
}"
|
||||
>{{ item.accessNum }}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
未接入
|
||||
<span class="img_num_span2">{{
|
||||
Number(item.totalNum) - Number(item.accessNum)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="img_sider">
|
||||
<div
|
||||
class="img_sider_inner"
|
||||
:style="{
|
||||
width: getPercent(item) + '%',
|
||||
backgroundColor: getBarColor(index)
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-for="(item, index) in dataJson" :key="index">
|
||||
<div
|
||||
v-if="index >= 2 && index < 4"
|
||||
:class="'facility-card' + index"
|
||||
@click="imgclick(item)"
|
||||
>
|
||||
<div class="img_num">
|
||||
<div class="img_text1">
|
||||
<div>{{ item.name }}</div>
|
||||
</div>
|
||||
<div class="img_text2">
|
||||
{{ item.totalNum }}
|
||||
</div>
|
||||
<div class="img_text3">设备总数</div>
|
||||
</div>
|
||||
<div class="img_title">
|
||||
<div class="img_icon">
|
||||
<i class="icon iconfont" :class="item?.icon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else style="width: 100%;height: 158px;display: flex; align-items: center; justify-content: center;">
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
style="
|
||||
width: 100%;
|
||||
height: 158px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
"
|
||||
>
|
||||
<a-empty description="暂无数据" />
|
||||
</div>
|
||||
</a-spin>
|
||||
</SidePanelItem>
|
||||
|
||||
<!-- 自定义弹框 -->
|
||||
<a-modal v-model:open="modalVisible" title="设施类型及接入情况" width="1536px" :footer="null" @cancel="handleModalClose">
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
title="设施类型及接入情况"
|
||||
width="1536px"
|
||||
:footer="null"
|
||||
@cancel="handleModalClose"
|
||||
>
|
||||
<div class="modal-content">
|
||||
|
||||
<DiwenshuijianhuansheshileixingzuchengjijieruqingkuangTwoLayers ref="twoLayersRef" :datas="JidiSelectEventStore.jidiData" :BID="baseid"
|
||||
:data="selectedItem" :res="res">
|
||||
<DiwenshuijianhuansheshileixingzuchengjijieruqingkuangTwoLayers
|
||||
ref="twoLayersRef"
|
||||
:datas="JidiSelectEventStore.jidiData"
|
||||
:BID="baseid"
|
||||
:data="selectedItem"
|
||||
:res="res"
|
||||
>
|
||||
</DiwenshuijianhuansheshileixingzuchengjijieruqingkuangTwoLayers>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { ref, onMounted, watch, defineOptions } from 'vue';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import { dwInfoGetKendoListCust } from "@/api/sw";
|
||||
import { useJidiSelectEventStore } from "@/store/modules/jidiSelectEvent";
|
||||
import DiwenshuijianhuansheshileixingzuchengjijieruqingkuangTwoLayers from "./TwoLayers/diwenshuijianhuansheshileixingzuchengjijieruqingkuangTwoLayers.vue"
|
||||
import { dwInfoGetKendoListCust } from '@/api/sw';
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import DiwenshuijianhuansheshileixingzuchengjijieruqingkuangTwoLayers from './TwoLayers/diwenshuijianhuansheshileixingzuchengjijieruqingkuangTwoLayers.vue';
|
||||
import { useDraggable } from '@/utils/drag';
|
||||
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
@ -63,13 +139,13 @@ const iconMap: Record<string, string> = {
|
||||
const res = ref({
|
||||
hydrodtin: '',
|
||||
bldstt: ''
|
||||
})
|
||||
});
|
||||
// 响应式数据
|
||||
const dataJson = ref<DataString[]>([]);
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
const baseid = ref('')
|
||||
const baseid = ref('');
|
||||
|
||||
// 弹框相关
|
||||
const modalVisible = ref(false);
|
||||
@ -81,28 +157,30 @@ const getListData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
"filter": {
|
||||
"logic": "and",
|
||||
"filters": [
|
||||
baseid.value == 'all' ? null : {
|
||||
"field": "baseId",
|
||||
"operator": "contains",
|
||||
"dataType": "string",
|
||||
"value": baseid.value
|
||||
}
|
||||
].filter(Boolean),
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
baseid.value == 'all'
|
||||
? null
|
||||
: {
|
||||
field: 'baseId',
|
||||
operator: 'contains',
|
||||
dataType: 'string',
|
||||
value: baseid.value
|
||||
}
|
||||
].filter(Boolean)
|
||||
}
|
||||
};
|
||||
|
||||
let res = await dwInfoGetKendoListCust(params)
|
||||
let data = res?.data?.data || res?.data || []
|
||||
let res = await dwInfoGetKendoListCust(params);
|
||||
let data = res?.data?.data || res?.data || [];
|
||||
|
||||
// 合并接口数据和icon配置
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
dataJson.value = data.map((item: any) => ({
|
||||
...item,
|
||||
icon: iconMap[item.dwtp] || 'icon-dwsjhQita' // 根据dwtp匹配icon,默认使用"其他"图标
|
||||
}))
|
||||
}));
|
||||
} else {
|
||||
dataJson.value = [];
|
||||
}
|
||||
@ -115,9 +193,9 @@ const getListData = async () => {
|
||||
};
|
||||
watch(
|
||||
() => JidiSelectEventStore.selectedItem,
|
||||
(newVal) => {
|
||||
newVal => {
|
||||
baseid.value = newVal.wbsCode;
|
||||
getListData()
|
||||
getListData();
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
@ -132,18 +210,28 @@ watch(
|
||||
// };
|
||||
const imgclick = (item: DataString) => {
|
||||
// 点击图片处理逻辑
|
||||
console.log(item)
|
||||
console.log(item);
|
||||
selectedItem.value = item;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 模态框关闭处理
|
||||
const handleModalClose = () => {
|
||||
// 重置子组件的搜索状态
|
||||
if (twoLayersRef.value && twoLayersRef.value.resetSearchState) {
|
||||
twoLayersRef.value.resetSearchState()
|
||||
twoLayersRef.value.resetSearchState();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 进度条百分比:已接入 / 总数
|
||||
const getPercent = (item: DataString) => {
|
||||
const total = Number(item.totalNum);
|
||||
const access = Number(item.accessNum);
|
||||
if (!total || total <= 0) return 0;
|
||||
return Math.min(Math.round((access / total) * 100), 100);
|
||||
};
|
||||
// 进度条颜色:与已接入数字颜色保持一致
|
||||
const getBarColor = (index: number) => (index === 0 ? '#2d6ac8' : '#ff9900');
|
||||
|
||||
// 页面加载时执行的逻辑
|
||||
onMounted(() => {
|
||||
@ -157,51 +245,99 @@ onMounted(() => {
|
||||
width: 406px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-x: auto;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
|
||||
.facility-card {
|
||||
width: 95.5px;
|
||||
height: 158px;
|
||||
border: 1px solid rgb(229, 236, 245);
|
||||
border-radius: 2px;
|
||||
.facility-card0,
|
||||
.facility-card1 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #f3f5fa;
|
||||
padding: 14px;
|
||||
border-radius: 4px;
|
||||
.img_title {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.img_num {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
color: #949494;
|
||||
font-size: 10px;
|
||||
span {
|
||||
margin-left: 5px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.img_num_span1 {
|
||||
color: #2d6ac8;
|
||||
}
|
||||
.img_num_span2 {
|
||||
color: #000000;
|
||||
}
|
||||
.img_num_span3 {
|
||||
color: #ff9900;
|
||||
}
|
||||
}
|
||||
.img_sider {
|
||||
height: 7px;
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
background-color: #e5ecf5;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
margin-top: 10px;
|
||||
.img_sider_inner {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.img_icon {
|
||||
margin-top: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 40px;
|
||||
background-color: rgb(83, 137, 181);
|
||||
border-radius: 14px;
|
||||
background-color: #e2e9f8;
|
||||
line-height: 40px;
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.img_text {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
|
||||
.text_num {
|
||||
width: 100%;
|
||||
margin-top: 3px;
|
||||
text-align: center;
|
||||
// font-size: 16px;
|
||||
color: #2f6b98;
|
||||
font-size: 18px;
|
||||
color: #2652a0;
|
||||
i {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
.facility-card2,
|
||||
.facility-card3 {
|
||||
position: relative;
|
||||
width: 198px;
|
||||
background-color: #f3f5fa;
|
||||
padding: 14px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.img_text1 {
|
||||
font-size: 16px;
|
||||
color: #363636;
|
||||
}
|
||||
.img_text2 {
|
||||
color: #000000;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.img_text3 {
|
||||
color: #949494;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-spin-nested-loading) {
|
||||
height: 158px !important;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
|
||||
@ -116,27 +116,11 @@ const VPInfo = [
|
||||
value: 'stnm',
|
||||
col: 24
|
||||
},
|
||||
{
|
||||
label: '站址',
|
||||
value: 'stlc',
|
||||
col: 24
|
||||
},
|
||||
{
|
||||
label: '所属流域',
|
||||
value: 'hbrvcdName',
|
||||
col: 24
|
||||
},
|
||||
{
|
||||
label: '建成日期',
|
||||
value: 'jcdt',
|
||||
type: 'date',
|
||||
col: 12
|
||||
},
|
||||
{
|
||||
label: '面积(km²)',
|
||||
value: 'area',
|
||||
col: 12
|
||||
},
|
||||
{
|
||||
label: '保护对象',
|
||||
value: 'protObj',
|
||||
@ -154,22 +138,11 @@ const VAInfo = [
|
||||
value: 'stnm',
|
||||
col: 24
|
||||
},
|
||||
{
|
||||
label: '站址',
|
||||
value: 'stlc',
|
||||
col: 24
|
||||
},
|
||||
{
|
||||
label: '所属流域',
|
||||
value: 'hbrvcdName',
|
||||
col: 24
|
||||
},
|
||||
{
|
||||
label: '建成日期',
|
||||
value: 'jcdt',
|
||||
type: 'date',
|
||||
col: 12
|
||||
},
|
||||
{
|
||||
label: '保护对象',
|
||||
value: 'protobj',
|
||||
|
||||
@ -79,7 +79,6 @@ watch(
|
||||
async newVal => {
|
||||
if (!newVal?.stcd) return;
|
||||
loading.value = true;
|
||||
console.log(newVal);
|
||||
try {
|
||||
logo.value = '';
|
||||
const res = await getStcdDetail(
|
||||
@ -87,7 +86,6 @@ watch(
|
||||
newVal.stcd
|
||||
);
|
||||
const data = res.data.msStbprpT || {};
|
||||
console.log(data);
|
||||
if (data.logo) {
|
||||
logo.value = baseUrl + '/?' + data.logo;
|
||||
} else {
|
||||
@ -96,13 +94,17 @@ watch(
|
||||
if (data.sttpCode == 'ENG') {
|
||||
data.sttp = data.sttpCode;
|
||||
}
|
||||
if (data.sttp && data.sttp.includes('_')) {
|
||||
const code = data.sttp.split('_')[0];
|
||||
formDiv.value = moduleMap[code + 'Info'];
|
||||
formDiv.value = moduleMap[code + 'Info'] || null;
|
||||
} else {
|
||||
// 处理没有下划线的情况
|
||||
formDiv.value = moduleMap[data.sttp + 'Info'];
|
||||
}
|
||||
|
||||
if (data.ttpwr) {
|
||||
data.ttpwr = data.ttpwr / 1000;
|
||||
}
|
||||
console.log(data);
|
||||
info.value = data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@ -110,13 +112,14 @@ watch(
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
const iconClick = () => {
|
||||
if (info.value.sttpMap == 'ylfb') {
|
||||
modelStore.ylfbModalVisible = true;
|
||||
modelStore.params = info.value;
|
||||
} else {
|
||||
modelStore.modalVisible = true;
|
||||
modelStore.params = info.value;
|
||||
modelStore.params = modelStore.selectedAnchorPoint;
|
||||
modelStore.title = info.value.titleName || info.value.stnm;
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,151 +0,0 @@
|
||||
<template>
|
||||
<SidePanelItem title="电站运行过程线" :shrink="false">
|
||||
<template #title-right-content>
|
||||
<a-range-picker
|
||||
class="w-[220px]"
|
||||
v-model:value="dateRange"
|
||||
format="YYYY-MM-DD"
|
||||
:show-time="showTimeConfig"
|
||||
:allowClear="false"
|
||||
:presets="DateSetting.RangeButton.month1"
|
||||
:disabled-date="disabledDate"
|
||||
size="small"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
<a-spin :spinning="isLoading" tip="加载中...">
|
||||
<div class="monitor-chart">
|
||||
<MonitorInfoEcharts :data="chartData" :is-active="true" />
|
||||
<a-empty
|
||||
v-if="!chartData || chartData.length === 0"
|
||||
description="暂无数据"
|
||||
class="chart-empty"
|
||||
/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onUpdated } from 'vue';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import { getMonitorData } from '@/api/mapModal';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import MonitorInfoEcharts from '@/components/MapModal/componentsEcharts/MonitorInfoEcharts.vue';
|
||||
import { DateSetting } from '@/utils/enumeration';
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const modelStore = useModelStore();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const chartData = ref<any[]>([]);
|
||||
|
||||
// 时间选择器配置(默认参数与 MonitorInfo 保持一致)
|
||||
const showTimeConfig = {
|
||||
format: 'HH:mm',
|
||||
hourStep: 1,
|
||||
minuteStep: 5,
|
||||
secondStep: 60
|
||||
};
|
||||
const disabledDate = (current: Dayjs) =>
|
||||
current && current.isAfter(dayjs(), 'day');
|
||||
|
||||
const initDateRange = (): [Dayjs, Dayjs] => {
|
||||
return [dayjs().subtract(1, 'month'), dayjs()];
|
||||
};
|
||||
const dateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
|
||||
|
||||
// 数据请求(查询参数取 modelStore.selectedAnchorPoint)
|
||||
const fetchData = async () => {
|
||||
const stcd = modelStore.selectedAnchorPoint?.stcd;
|
||||
if (!dateRange.value || !stcd) return;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const filterParams = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: stcd
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[1].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
]
|
||||
},
|
||||
sort: [{ field: 'tm', dir: 'asc' }]
|
||||
};
|
||||
|
||||
const res = await getMonitorData(filterParams);
|
||||
const rawData = res?.data?.data || res?.data?.records || [];
|
||||
|
||||
chartData.value = rawData;
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
chartData.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 时间选择器 change 触发查询
|
||||
const handleSearch = () => fetchData();
|
||||
watch(
|
||||
() => uiStore.searchDrawerOpen,
|
||||
newVal => {
|
||||
if (!newVal) {
|
||||
dateRange.value = initDateRange();
|
||||
}
|
||||
}
|
||||
);
|
||||
// 选中点位变化时刷新;初始已有点位时立即加载
|
||||
watch(
|
||||
() => modelStore.selectedAnchorPoint?.stcd,
|
||||
(newStcd, oldStcd) => {
|
||||
if (!newStcd) return;
|
||||
if (newStcd !== oldStcd) fetchData();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 全局时间范围变化时同步本地日期选择器(默认参数联动)
|
||||
watch(
|
||||
() => modelStore.filter.rangeTm,
|
||||
newRange => {
|
||||
if (Array.isArray(newRange) && newRange.length > 0) {
|
||||
dateRange.value = [dayjs(newRange[0]), dayjs(newRange[1])];
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.monitor-chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<SidePanelItem title="监测数据" :shrink="false">
|
||||
<template #title-right-content>
|
||||
<a-date-picker
|
||||
class="w-[110px]"
|
||||
v-model:value="year"
|
||||
picker="year"
|
||||
format="YYYY"
|
||||
value-format="YYYY"
|
||||
:allowClear="false"
|
||||
size="small"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
<!-- 选中行的放生照片走马灯,展示在表格上方 10px 外边距 -->
|
||||
<a-spin :spinning="imageLoading" tip="加载中...">
|
||||
<a-carousel
|
||||
v-if="imageUrls.length > 0"
|
||||
class="animal-carousel"
|
||||
dots-class="carousel-dots"
|
||||
>
|
||||
<div
|
||||
v-for="(url, index) in imageUrls"
|
||||
:key="index"
|
||||
class="carousel-item"
|
||||
>
|
||||
<a-image :src="url" alt="放生照片" class="carousel-img" />
|
||||
</div>
|
||||
</a-carousel>
|
||||
</a-spin>
|
||||
<div class="monitor-table">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:scrollY="300"
|
||||
:scrollX="tableScrollX"
|
||||
:columns="tableColumns"
|
||||
:list-url="getTableList"
|
||||
:transform-data="transformTableData"
|
||||
:enableRowHighlight="true"
|
||||
row-key="_rowKey"
|
||||
:paginationConfig="{
|
||||
showSizeChanger: false,
|
||||
showQuickJumper: false
|
||||
}"
|
||||
@row-click="handleRowClick"
|
||||
@data-loaded="handleDataLoaded"
|
||||
/>
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import { getNormal2Year, getNormal2List, getIdUrl } from '@/api/mapModal';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const modelStore = useModelStore();
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
const tableRef = ref();
|
||||
const year = ref<string>('');
|
||||
const tableColumns = ref([
|
||||
{
|
||||
title: '种类',
|
||||
dataIndex: 'tetp',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '放生日期',
|
||||
dataIndex: 'tm',
|
||||
width: 88
|
||||
},
|
||||
{
|
||||
title: '救助数量(只)',
|
||||
dataIndex: 'tecnt',
|
||||
width: 98
|
||||
},
|
||||
{
|
||||
title: '放生地点',
|
||||
dataIndex: 'stlc'
|
||||
}
|
||||
]);
|
||||
const tableScrollX = ref(320);
|
||||
|
||||
// ==================== 放生照片走马灯 ====================
|
||||
const imageLoading = ref(false);
|
||||
const imageUrls = ref<string[]>([]);
|
||||
|
||||
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
|
||||
|
||||
const loadImages = async (record: any) => {
|
||||
imageUrls.value = [];
|
||||
if (!record) return;
|
||||
const fids = String(record?.fid || '')
|
||||
.split(',')
|
||||
.map((item: string) => item.trim())
|
||||
.filter(Boolean);
|
||||
if (fids.length === 0) return;
|
||||
imageLoading.value = true;
|
||||
try {
|
||||
const res: any = await getIdUrl({ id: fids.join(',') });
|
||||
const data = res?.data || {};
|
||||
imageUrls.value = fids
|
||||
.map((fid: string) => data?.[fid])
|
||||
.filter(
|
||||
(item: any) =>
|
||||
item &&
|
||||
IMAGE_EXTENSIONS.includes(String(item.ext || '').toLowerCase())
|
||||
)
|
||||
.map((item: any) => item.fullpath)
|
||||
.filter(Boolean);
|
||||
} catch (error) {
|
||||
console.error('获取放生照片失败:', error);
|
||||
imageUrls.value = [];
|
||||
} finally {
|
||||
imageLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRowClick = (record: any) => {
|
||||
loadImages(record);
|
||||
};
|
||||
|
||||
// 数据加载完成后默认展示第一条的照片;无数据时清空图片
|
||||
const handleDataLoaded = (params: any, data: any) => {
|
||||
const records = data?.records || [];
|
||||
if (records.length > 0) {
|
||||
loadImages(records[0]);
|
||||
} else {
|
||||
imageUrls.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 年份与数据查询 ====================
|
||||
const getDefaultYear = (yearList: any[]) => {
|
||||
const firstItem = yearList?.[0];
|
||||
if (typeof firstItem === 'string' || typeof firstItem === 'number') {
|
||||
return String(firstItem);
|
||||
}
|
||||
return firstItem?.yr || firstItem?.tm || firstItem?.year || null;
|
||||
};
|
||||
|
||||
const getYearRange = (yearValue: string) => ({
|
||||
startDate: `${yearValue}-01-01 00:00:00`,
|
||||
endDate: `${yearValue}-12-31 23:59:59`
|
||||
});
|
||||
|
||||
const transformTableData = (res: any) => {
|
||||
const records = res?.data?.records || res?.data?.data || res?.data || [];
|
||||
const total = res?.data?.data?.total || res?.data?.total || res?.total || 0;
|
||||
|
||||
return {
|
||||
records: records.map((item: any, index: number) => ({
|
||||
...item,
|
||||
_rowKey:
|
||||
item?.id ||
|
||||
item?.fid ||
|
||||
`${item?.stcd || 'stcd'}-${item?.tm || year.value || 'tm'}-${index}`
|
||||
})),
|
||||
total
|
||||
};
|
||||
};
|
||||
|
||||
const buildFilter = () => ({
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: modelStore.selectedAnchorPoint?.stcd
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: getYearRange(year.value).startDate
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: getYearRange(year.value).endDate
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const getTableList = (params: any) => {
|
||||
const take = Number(params?.take || DEFAULT_PAGE_SIZE);
|
||||
const currentPage = Number(params?.skip || 1);
|
||||
const skip = Math.max(currentPage - 1, 0) * take;
|
||||
|
||||
return getNormal2List({
|
||||
...params,
|
||||
take,
|
||||
skip,
|
||||
filter: params?.filter || buildFilter()
|
||||
});
|
||||
};
|
||||
|
||||
const refreshTable = () => {
|
||||
if (!modelStore.selectedAnchorPoint?.stcd || !year.value) return;
|
||||
tableRef.value?.getList(buildFilter());
|
||||
};
|
||||
|
||||
// 获取年份列表并赋值默认年份(年份列表第一个),然后自动查询
|
||||
const getYearList = async () => {
|
||||
const stcd = modelStore.selectedAnchorPoint?.stcd;
|
||||
if (!stcd) return;
|
||||
try {
|
||||
const res: any = await getNormal2Year({
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: stcd
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const yearData = res?.data?.data || res?.data?.records || res?.data || [];
|
||||
const defaultYear =
|
||||
getDefaultYear(yearData) || String(new Date().getFullYear());
|
||||
|
||||
year.value = String(defaultYear);
|
||||
refreshTable();
|
||||
} catch (error) {
|
||||
console.error('获取年份列表失败:', error);
|
||||
if (tableRef.value) {
|
||||
tableRef.value.loading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
if (!year.value) return;
|
||||
imageUrls.value = []; // 切换年份时先清空图片,避免加载期间显示旧数据图片
|
||||
refreshTable();
|
||||
};
|
||||
|
||||
// ==================== 联动 ====================
|
||||
watch(
|
||||
() => uiStore.searchDrawerOpen,
|
||||
newVal => {
|
||||
if (!newVal) {
|
||||
year.value = '';
|
||||
imageUrls.value = [];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 选中点位变化时重新加载;初始已有点位时立即加载
|
||||
watch(
|
||||
() => modelStore.selectedAnchorPoint?.stcd,
|
||||
(newStcd, oldStcd) => {
|
||||
if (!newStcd) return;
|
||||
if (newStcd !== oldStcd) {
|
||||
year.value = '';
|
||||
imageUrls.value = [];
|
||||
getYearList();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.animal-carousel {
|
||||
margin-top: 10px;
|
||||
// height: 300px;
|
||||
background: #f5f5f5;
|
||||
|
||||
// a-carousel 内部滑块为库生成元素,需穿透样式撑满高度并居中
|
||||
::v-deep(.slick-list),
|
||||
::v-deep(.slick-track),
|
||||
::v-deep(.slick-slide) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::v-deep(.slick-slide > div) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.carousel-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
:deep(.ant-image) {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-image-img) {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.monitor-table {
|
||||
margin-top: 10px;
|
||||
width: 410px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,417 @@
|
||||
<template>
|
||||
<SidePanelItem title="生态流量泄放设施" :shrink="false">
|
||||
<template #title-right-content>
|
||||
<a-range-picker
|
||||
class="w-[220px]"
|
||||
v-model:value="dateRange"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
:show-time="showTimeConfig"
|
||||
:allowClear="false"
|
||||
:presets="DateSetting.RangeButton.month1"
|
||||
:disabled-date="disabledDate"
|
||||
size="small"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
<a-spin :spinning="isLoading" tip="加载中...">
|
||||
<div class="monitor-chart">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
<a-empty
|
||||
v-if="!chartData || chartData.length === 0"
|
||||
description="暂无数据"
|
||||
class="chart-empty"
|
||||
/>
|
||||
</div>
|
||||
<div class="monitor-table">
|
||||
<BasicTable
|
||||
:scrollY="300"
|
||||
:scrollX="tableScrollX"
|
||||
:columns="tableColumns"
|
||||
:data="tableData"
|
||||
:paginationConfig="{
|
||||
showSizeChanger: false,
|
||||
showQuickJumper: false
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import * as echarts from 'echarts';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import { getFlowDischargeYear, getFlowDischargeList } from '@/api/mapModal';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { DateSetting } from '@/utils/enumeration';
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const modelStore = useModelStore();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const tableData = ref<any[]>([]);
|
||||
const chartData = ref<any[]>([]);
|
||||
const chartRef = ref<HTMLElement>();
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
// ==================== 时间选择器配置 ====================
|
||||
const showTimeConfig = {
|
||||
format: 'HH:mm',
|
||||
hourStep: 1,
|
||||
minuteStep: 5,
|
||||
secondStep: 60
|
||||
};
|
||||
const disabledDate = (current: Dayjs) =>
|
||||
current && current.isAfter(dayjs(), 'day');
|
||||
|
||||
const initDateRange = (): [Dayjs, Dayjs] => {
|
||||
return [dayjs().subtract(7, 'day'), dayjs()];
|
||||
};
|
||||
const dateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
|
||||
|
||||
// ==================== 表格 ====================
|
||||
const tableColumns = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'tm',
|
||||
width: 180,
|
||||
fixed: 'left',
|
||||
customRender: ({ text }: any) =>
|
||||
text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-'
|
||||
},
|
||||
{
|
||||
title: '流量(m³/s)',
|
||||
dataIndex: 'q',
|
||||
width: 130,
|
||||
customRender: ({ text }: any) =>
|
||||
text !== undefined && text !== null ? Number(text) : '-'
|
||||
}
|
||||
];
|
||||
const tableScrollX = tableColumns.reduce((s, c) => s + (c.width || 100), 0);
|
||||
|
||||
// ==================== 图表(流量单线) ====================
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
if (chartInstance) chartInstance.dispose();
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
updateChart(chartData.value);
|
||||
};
|
||||
|
||||
const updateChart = (data: any[]) => {
|
||||
if (!chartInstance) return;
|
||||
if (!data || data.length === 0) {
|
||||
chartInstance.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const sorted = [...data].sort(
|
||||
(a, b) => new Date(a.tm).getTime() - new Date(b.tm).getTime()
|
||||
);
|
||||
|
||||
const xAxisData = sorted.map(item => {
|
||||
const hm = dayjs(item.tm).format('HH:mm');
|
||||
if (hm === '00:00') {
|
||||
const date = dayjs(item.tm).format('MM-DD');
|
||||
return `${hm}\n${date}`;
|
||||
}
|
||||
return hm;
|
||||
});
|
||||
|
||||
const qData = sorted.map(item => item.q);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
textStyle: { color: '#fff', fontSize: 12 },
|
||||
axisPointer: { type: 'cross' },
|
||||
formatter: (params: any) => {
|
||||
if (!params || params.length === 0) return '';
|
||||
const dataIndex = params[0].dataIndex;
|
||||
const fullTime = sorted[dataIndex]?.tm
|
||||
? dayjs(sorted[dataIndex].tm).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '';
|
||||
let html = `<div style="font-size:16px;margin-bottom:8px;">${fullTime}</div>`;
|
||||
params.forEach((param: any) => {
|
||||
if (param.value == null) return;
|
||||
const displayValue = Number(param.value).toFixed(2);
|
||||
html += `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin:4px 0;">
|
||||
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:8px;"></span>
|
||||
<span style="flex:1;font-size:14px;text-align:left;margin-right:6px;">${param.seriesName}: </span>
|
||||
<span style="font-size:14px;min-width:60px;text-align:right;"><strong>${displayValue}</strong> (m³/s)</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
return html;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
top: 10,
|
||||
data: ['流量'],
|
||||
textStyle: { fontSize: 12 }
|
||||
},
|
||||
grid: { left: 50, right: 20, top: 44, bottom: 50 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { fontSize: 12 },
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
name: '流量(m³/s)',
|
||||
type: 'value',
|
||||
position: 'left',
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
scale: true,
|
||||
splitNumber: 9
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '流量',
|
||||
type: 'line',
|
||||
data: qData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#12C1EA', width: 2 },
|
||||
itemStyle: { color: '#12C1EA' }
|
||||
}
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0],
|
||||
throttle: 50,
|
||||
start: 0,
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
|
||||
},
|
||||
right: 20,
|
||||
top: 10
|
||||
}
|
||||
};
|
||||
|
||||
chartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
// 数据变化时动态刷新图表
|
||||
watch(chartData, newData => {
|
||||
updateChart(newData);
|
||||
});
|
||||
|
||||
const safeResize = (
|
||||
chart: echarts.ECharts | null,
|
||||
el: HTMLElement | undefined
|
||||
) => {
|
||||
if (!chart) return;
|
||||
// 容器不可见(display:none / 宽度为 0)时跳过,避免收起/切换时做昂贵的同步重排
|
||||
if (!el || el.clientWidth === 0) return;
|
||||
chart.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
setTimeout(() => {
|
||||
safeResize(chartInstance, chartRef.value);
|
||||
}, 200);
|
||||
if (!resizeObserver && chartRef.value) {
|
||||
resizeObserver = new ResizeObserver(() =>
|
||||
safeResize(chartInstance, chartRef.value)
|
||||
);
|
||||
resizeObserver.observe(chartRef.value);
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', () =>
|
||||
safeResize(chartInstance, chartRef.value)
|
||||
);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener('resize', () =>
|
||||
safeResize(chartInstance, chartRef.value)
|
||||
);
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 默认日期 ====================
|
||||
// 用年份接口返回的最后一个日期作为结束日期,往前 7 天为开始日期
|
||||
const fetchDefaultDate = async () => {
|
||||
const stcd = modelStore.selectedAnchorPoint?.stcd;
|
||||
if (!stcd) return;
|
||||
try {
|
||||
const filterParams = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: stcd
|
||||
},
|
||||
{
|
||||
field: 'sttpCode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: 'EQ'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const res = await getFlowDischargeYear(filterParams);
|
||||
const dates = res?.data?.data || [];
|
||||
|
||||
if (dates && dates.length > 0) {
|
||||
// 使用最后一个日期作为结束日期
|
||||
const endDate = dayjs(dates[dates.length - 1]);
|
||||
const startDate = endDate.subtract(7, 'day');
|
||||
dateRange.value = [startDate, endDate];
|
||||
} else {
|
||||
// 没有数据默认当前天
|
||||
dateRange.value = [dayjs().subtract(7, 'day'), dayjs()];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取默认日期失败:', error);
|
||||
// 失败时使用默认前7天
|
||||
dateRange.value = [dayjs().subtract(7, 'day'), dayjs()];
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 数据请求 ====================
|
||||
const fetchData = async () => {
|
||||
const stcd = modelStore.selectedAnchorPoint?.stcd;
|
||||
if (!dateRange.value || !stcd) return;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const filterParams = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: stcd
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[1].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
]
|
||||
},
|
||||
sort: [{ field: 'tm', dir: 'asc' }]
|
||||
};
|
||||
|
||||
const res = await getFlowDischargeList('EQ_R', filterParams);
|
||||
const rawData = res?.data?.data || res?.data?.records || [];
|
||||
|
||||
tableData.value = [...rawData].reverse();
|
||||
chartData.value = rawData;
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
tableData.value = [];
|
||||
chartData.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 时间选择器 change 触发查询
|
||||
const handleSearch = () => fetchData();
|
||||
|
||||
// ==================== 联动 ====================
|
||||
watch(
|
||||
() => uiStore.searchDrawerOpen,
|
||||
newVal => {
|
||||
if (!newVal) {
|
||||
dateRange.value = initDateRange();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 选中点位变化时重新加载;初始已有点位时立即加载
|
||||
watch(
|
||||
() => modelStore.selectedAnchorPoint?.stcd,
|
||||
async (newStcd, oldStcd) => {
|
||||
if (!newStcd) return;
|
||||
if (newStcd !== oldStcd) {
|
||||
dateRange.value = initDateRange();
|
||||
await fetchDefaultDate();
|
||||
fetchData();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 全局时间范围变化时同步本地日期选择器(默认参数联动)
|
||||
watch(
|
||||
() => modelStore.filter.rangeTm,
|
||||
newRange => {
|
||||
if (Array.isArray(newRange) && newRange.length > 0) {
|
||||
dateRange.value = [dayjs(newRange[0]), dayjs(newRange[1])];
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.monitor-chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.monitor-table {
|
||||
margin-top: 10px;
|
||||
width: 410px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,493 @@
|
||||
<template>
|
||||
<SidePanelItem title="流量监测数据" :shrink="false">
|
||||
<template #title-right-content>
|
||||
<a-range-picker
|
||||
class="w-[220px]"
|
||||
v-model:value="dateRange"
|
||||
format="YYYY-MM-DD"
|
||||
:show-time="showTimeConfig"
|
||||
:allowClear="false"
|
||||
:presets="DateSetting.RangeButton.month1"
|
||||
:disabled-date="disabledDate"
|
||||
size="small"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
<a-spin :spinning="isLoading" tip="加载中...">
|
||||
<div class="monitor-chart">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
<a-empty
|
||||
v-if="isDataEmpty"
|
||||
description="暂无数据"
|
||||
class="chart-empty"
|
||||
/>
|
||||
</div>
|
||||
<div class="monitor-table">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:scrollY="240"
|
||||
:scrollX="tableScrollX"
|
||||
:columns="tableColumns"
|
||||
:data="tableData"
|
||||
:paginationConfig="{
|
||||
showSizeChanger: false,
|
||||
showQuickJumper: false
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
computed,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
nextTick
|
||||
} from 'vue';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import * as echarts from 'echarts';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import { getFlowStationList } from '@/api/DataQueryMenuModule';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { DateSetting } from '@/utils/enumeration';
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const modelStore = useModelStore();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const chartData = ref<any[]>([]);
|
||||
const tableData = ref<any[]>([]);
|
||||
const tableRef = ref<any>();
|
||||
|
||||
const isDataEmpty = computed(() => {
|
||||
const data = Array.isArray(chartData.value) ? chartData.value : [];
|
||||
return data.length === 0;
|
||||
});
|
||||
|
||||
// ==================== 图表 ====================
|
||||
const chartRef = ref<HTMLElement>();
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
if (chartInstance) chartInstance.dispose();
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
updateChart(chartData.value);
|
||||
};
|
||||
|
||||
const updateChart = (data: any[]) => {
|
||||
if (!chartInstance) return;
|
||||
if (!data || data.length === 0) {
|
||||
chartInstance.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const sorted = [...data].sort(
|
||||
(a, b) => new Date(a.tm).getTime() - new Date(b.tm).getTime()
|
||||
);
|
||||
|
||||
const xAxisData = sorted.map(item => {
|
||||
const hm = dayjs(item.tm).format('HH:mm');
|
||||
if (hm === '00:00') {
|
||||
const date = dayjs(item.tm).format('MM-DD');
|
||||
return `${hm}\n${date}`;
|
||||
}
|
||||
return hm;
|
||||
});
|
||||
|
||||
const zData = sorted.map(item => item.z);
|
||||
const qData = sorted.map(item => item.q);
|
||||
const vData = sorted.map(item => item.v);
|
||||
|
||||
const waterValues = zData.filter(v => v !== null && v !== undefined);
|
||||
const waterMin = waterValues.length > 0 ? Math.min(...waterValues) : 0;
|
||||
const waterMax = waterValues.length > 0 ? Math.max(...waterValues) : 10;
|
||||
|
||||
const flowValues = [...qData, ...vData].filter(
|
||||
v => v !== null && v !== undefined
|
||||
);
|
||||
const flowMin = flowValues.length > 0 ? Math.min(...flowValues) : 0;
|
||||
const flowMax = flowValues.length > 0 ? Math.max(...flowValues) : 10;
|
||||
|
||||
const formatRules: Record<
|
||||
string,
|
||||
{ format: (v: number) => string; unit: string }
|
||||
> = {
|
||||
水位: { format: v => v.toFixed(2), unit: '(m)' },
|
||||
流量: { format: v => String(Math.round(v)), unit: '(m³/s)' },
|
||||
流速: { format: v => v.toFixed(2), unit: '(m³/s)' }
|
||||
};
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
textStyle: { color: '#fff', fontSize: 12 },
|
||||
axisPointer: { type: 'cross' },
|
||||
formatter: (params: any) => {
|
||||
if (!params || params.length === 0) return '';
|
||||
const dataIndex = params[0].dataIndex;
|
||||
const fullTime = sorted[dataIndex]?.tm
|
||||
? dayjs(sorted[dataIndex].tm).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '';
|
||||
let html = `<div style="font-size:16px;margin-bottom:8px;">${fullTime}</div>`;
|
||||
params.forEach((param: any) => {
|
||||
if (param.value == null) return;
|
||||
const rule = Object.entries(formatRules).find(([key]) =>
|
||||
param.seriesName.includes(key)
|
||||
);
|
||||
const displayValue = rule
|
||||
? rule[1].format(Number(param.value))
|
||||
: param.value;
|
||||
const unit = rule ? rule[1].unit : '';
|
||||
html += `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin:4px 0;">
|
||||
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:8px;"></span>
|
||||
<span style="flex:1;font-size:14px;text-align:left;margin-right:6px;">${param.seriesName}: </span>
|
||||
<span style="font-size:14px;min-width:60px;text-align:right;"><strong>${displayValue}</strong> ${unit}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
return html;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
top: 0,
|
||||
data: ['水位', '流量', '流速'],
|
||||
textStyle: { fontSize: 12 },
|
||||
selected: {
|
||||
水位: true,
|
||||
流量: true,
|
||||
流速: true
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 100,
|
||||
top: 60,
|
||||
bottom: 50
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { fontSize: 12 },
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
name: '水位(m)',
|
||||
type: 'value',
|
||||
position: 'left',
|
||||
axisLine: { show: true, lineStyle: { color: '#56C2E3' } },
|
||||
axisLabel: { color: '#56C2E3' },
|
||||
nameTextStyle: { color: '#56C2E3' },
|
||||
alignTicks: true,
|
||||
scale: true,
|
||||
splitNumber: 9
|
||||
},
|
||||
{
|
||||
name: '流量(m³/s)',
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
axisLine: { show: true, lineStyle: { color: '#9556A4' } },
|
||||
axisLabel: { color: '#9556A4' },
|
||||
nameTextStyle: { color: '#9556A4' },
|
||||
alignTicks: true,
|
||||
scale: true,
|
||||
splitNumber: 9
|
||||
},
|
||||
{
|
||||
name: '流速(m³/s)',
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
axisLine: { show: true, lineStyle: { color: '#78C300' } },
|
||||
axisLabel: { color: '#78C300' },
|
||||
nameTextStyle: { color: '#78C300' },
|
||||
alignTicks: true,
|
||||
scale: true,
|
||||
splitNumber: 9,
|
||||
offset: 60
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '水位',
|
||||
type: 'line',
|
||||
yAxisIndex: 0,
|
||||
data: zData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#56C2E3', width: 2 },
|
||||
itemStyle: { color: '#56C2E3' }
|
||||
},
|
||||
{
|
||||
name: '流量',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: qData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#9556A4', width: 2 },
|
||||
itemStyle: { color: '#9556A4' }
|
||||
},
|
||||
{
|
||||
name: '流速',
|
||||
type: 'line',
|
||||
yAxisIndex: 2,
|
||||
data: vData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#78C300', width: 2 },
|
||||
itemStyle: { color: '#78C300' }
|
||||
}
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0],
|
||||
throttle: 50,
|
||||
start: 0,
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
|
||||
},
|
||||
right: 20,
|
||||
top: -6
|
||||
}
|
||||
};
|
||||
|
||||
chartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
// 数据变化时动态刷新图表
|
||||
watch(chartData, newData => {
|
||||
updateChart(newData);
|
||||
});
|
||||
|
||||
const safeResize = () => {
|
||||
if (!chartInstance) return;
|
||||
// 容器不可见(display:none / 宽度为 0)时跳过,避免收起/切换时做昂贵的同步重排
|
||||
if (!chartRef.value || chartRef.value.clientWidth === 0) return;
|
||||
chartInstance.resize();
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
safeResize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
setTimeout(() => {
|
||||
safeResize();
|
||||
}, 200);
|
||||
if (!resizeObserver && chartRef.value) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
safeResize();
|
||||
});
|
||||
resizeObserver.observe(chartRef.value);
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 时间选择器配置
|
||||
const showTimeConfig = {
|
||||
format: 'HH:mm',
|
||||
hourStep: 1,
|
||||
minuteStep: 5,
|
||||
secondStep: 60
|
||||
};
|
||||
const disabledDate = (current: Dayjs) =>
|
||||
current && current.isAfter(dayjs(), 'day');
|
||||
|
||||
const initDateRange = (): [Dayjs, Dayjs] => {
|
||||
return [dayjs().subtract(1, 'month'), dayjs()];
|
||||
};
|
||||
const dateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
|
||||
|
||||
// ==================== 表格列配置 ====================
|
||||
const tableColumns = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'tm',
|
||||
width: 160,
|
||||
fixed: 'left',
|
||||
customRender: ({ text }: any) =>
|
||||
text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '-'
|
||||
},
|
||||
{
|
||||
title: '水位(m)',
|
||||
dataIndex: 'z',
|
||||
width: 120,
|
||||
customRender: ({ text }: any) =>
|
||||
text !== undefined && text !== null ? Number(text).toFixed(2) : '-'
|
||||
},
|
||||
{
|
||||
title: '流量(m³/s)',
|
||||
dataIndex: 'q',
|
||||
width: 130,
|
||||
customRender: ({ text }: any) =>
|
||||
text !== undefined && text !== null ? Number(text).toFixed(1) : '-'
|
||||
},
|
||||
{
|
||||
title: '流速(m³/s)',
|
||||
dataIndex: 'v',
|
||||
width: 130,
|
||||
customRender: ({ text }: any) =>
|
||||
text !== undefined && text !== null ? Number(text).toFixed(2) : '-'
|
||||
}
|
||||
];
|
||||
|
||||
const tableScrollX = computed(() => {
|
||||
const totalWidth = tableColumns.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 100),
|
||||
0
|
||||
);
|
||||
return totalWidth > 600 ? totalWidth : undefined;
|
||||
});
|
||||
|
||||
// ==================== 数据请求 ====================
|
||||
// 数据请求(查询参数取 modelStore.selectedAnchorPoint)
|
||||
const fetchData = async () => {
|
||||
const stcd = modelStore.selectedAnchorPoint?.stcd;
|
||||
if (!dateRange.value || !stcd) return;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const filterParams = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: stcd
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[1].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
]
|
||||
},
|
||||
sort: [{ field: 'tm', dir: 'asc' }]
|
||||
};
|
||||
|
||||
const res = await getFlowStationList(filterParams);
|
||||
const rawData = res?.data?.data || res?.data?.records || [];
|
||||
|
||||
tableData.value = [...rawData].reverse();
|
||||
chartData.value = rawData;
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
tableData.value = [];
|
||||
chartData.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 时间选择器 change 触发查询
|
||||
const handleSearch = () => fetchData();
|
||||
watch(
|
||||
() => uiStore.searchDrawerOpen,
|
||||
newVal => {
|
||||
if (!newVal) {
|
||||
dateRange.value = initDateRange();
|
||||
}
|
||||
}
|
||||
);
|
||||
// 选中点位变化时刷新;初始已有点位时立即加载
|
||||
watch(
|
||||
() => modelStore.selectedAnchorPoint?.stcd,
|
||||
(newStcd, oldStcd) => {
|
||||
if (!newStcd) return;
|
||||
if (newStcd !== oldStcd) fetchData();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 全局时间范围变化时同步本地日期选择器(默认参数联动)
|
||||
watch(
|
||||
() => modelStore.filter.rangeTm,
|
||||
newRange => {
|
||||
if (Array.isArray(newRange) && newRange.length > 0) {
|
||||
dateRange.value = [dayjs(newRange[0]), dayjs(newRange[1])];
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.monitor-chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.monitor-table {
|
||||
margin-top: 10px;
|
||||
box-sizing: border-box;
|
||||
// 表格列数多、tableScrollX 较宽时必须限制在抽屉宽度内横向滚动,
|
||||
// 否则在 RightDrawer 的 .ant-drawer-content { overflow: visible } 下会溢出抽屉
|
||||
width: 410px;
|
||||
overflow-x: auto;
|
||||
|
||||
.table-container {
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,303 @@
|
||||
<template>
|
||||
<SidePanelItem title="过鱼总量" :shrink="false">
|
||||
<template #title-right-content>
|
||||
<a-date-picker
|
||||
class="w-[110px]"
|
||||
v-model:value="year"
|
||||
picker="year"
|
||||
format="YYYY"
|
||||
:allowClear="false"
|
||||
:disabled-date="disabledDate"
|
||||
size="small"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
<a-spin :spinning="isLoading" tip="加载中...">
|
||||
<div class="monitor-chart" :style="{ height: chartHeight + 'px' }">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
<a-empty
|
||||
v-if="isDataEmpty"
|
||||
description="暂无数据"
|
||||
class="chart-empty"
|
||||
/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
computed,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
nextTick
|
||||
} from 'vue';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import * as echarts from 'echarts';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import { yearGetYearFpStatistics } from '@/api/gyss';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const modelStore = useModelStore();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const chartData = ref<any[]>([]);
|
||||
|
||||
const isDataEmpty = computed(() => {
|
||||
const data = Array.isArray(chartData.value) ? chartData.value : [];
|
||||
return data.length === 0;
|
||||
});
|
||||
|
||||
// ==================== 图表高度自适应 legend 行数 ====================
|
||||
// 基础高度(1 行 legend 时的布局,饼图/标题均按此基准固定)
|
||||
const BASE_CHART_HEIGHT = 300;
|
||||
const LEGEND_ROW_HEIGHT = 20; // 每多一行 legend 增加的高度
|
||||
const ITEMS_PER_ROW = 3; // 一行约 3-4 个,按 3 保守估算避免遮挡
|
||||
|
||||
const legendRows = computed(() => {
|
||||
const len = chartData.value.length;
|
||||
return len === 0 ? 1 : Math.ceil(len / ITEMS_PER_ROW);
|
||||
});
|
||||
|
||||
const chartHeight = computed(() => {
|
||||
return BASE_CHART_HEIGHT + (legendRows.value - 1) * LEGEND_ROW_HEIGHT;
|
||||
});
|
||||
|
||||
// ==================== 图表 ====================
|
||||
const chartRef = ref<HTMLElement>();
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
// 固定色板(避免每次渲染随机变色)
|
||||
const COLORS = [
|
||||
'#5470c6',
|
||||
'#91cc75',
|
||||
'#fac858',
|
||||
'#ee6666',
|
||||
'#73c0de',
|
||||
'#3ba272',
|
||||
'#fc8452',
|
||||
'#9a60b4',
|
||||
'#ea7ccc',
|
||||
'#56C2E3',
|
||||
'#7399C6',
|
||||
'#4B79AB',
|
||||
'#78C300',
|
||||
'#00A050',
|
||||
'#F7A737'
|
||||
];
|
||||
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
if (chartInstance) chartInstance.dispose();
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
updateChart(chartData.value);
|
||||
};
|
||||
|
||||
const updateChart = (data: any[]) => {
|
||||
if (!chartInstance) return;
|
||||
if (!data || data.length === 0) {
|
||||
chartInstance.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const unit = '尾';
|
||||
const totalAmount = data.reduce(
|
||||
(sum, item) => sum + Number(item.value || 0),
|
||||
0
|
||||
);
|
||||
|
||||
const option: any = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: `{b}: {c} ${unit} ({d}%)`,
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: { color: '#fff', fontSize: 12 }
|
||||
},
|
||||
title: {
|
||||
text: `${totalAmount}`,
|
||||
subtext: `总量(${unit})`,
|
||||
left: '49%',
|
||||
top: 117,
|
||||
textAlign: 'center',
|
||||
textVerticalAlign: 'middle',
|
||||
textStyle: { fontSize: 24, fontWeight: 'bold', color: '#333' },
|
||||
subtextStyle: { fontSize: 12, color: '#999', align: 'center' }
|
||||
},
|
||||
legend: {
|
||||
type: 'plain',
|
||||
orient: 'horizontal',
|
||||
left: 'center',
|
||||
bottom: 5,
|
||||
width: '90%',
|
||||
itemWidth: 25,
|
||||
itemHeight: 13,
|
||||
itemGap: 8,
|
||||
formatter: (name: string) => {
|
||||
const found = data.find(item => item.name === name);
|
||||
const value = found ? found.value : '-';
|
||||
const maxLength = 7;
|
||||
const truncatedName =
|
||||
name.length > maxLength ? name.substring(0, maxLength) + '...' : name;
|
||||
return `${truncatedName} ${value}${unit}`;
|
||||
},
|
||||
textStyle: { fontSize: 12, color: '#666' }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: [75, 105],
|
||||
center: ['50%', 132],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: { borderRadius: 4, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { show: false },
|
||||
emphasis: {
|
||||
label: { show: false },
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)'
|
||||
}
|
||||
},
|
||||
labelLine: { show: false },
|
||||
data: data.map((item, idx) => ({
|
||||
...item,
|
||||
itemStyle: { color: COLORS[idx % COLORS.length] }
|
||||
}))
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
// 数据变化时动态刷新图表
|
||||
watch(chartData, newData => {
|
||||
updateChart(newData);
|
||||
});
|
||||
|
||||
const safeResize = () => {
|
||||
if (!chartInstance) return;
|
||||
// 容器不可见(display:none / 宽度为 0)时跳过,避免收起/切换时做昂贵的同步重排
|
||||
if (!chartRef.value || chartRef.value.clientWidth === 0) return;
|
||||
chartInstance.resize();
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
safeResize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
setTimeout(() => {
|
||||
safeResize();
|
||||
}, 200);
|
||||
if (!resizeObserver && chartRef.value) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
safeResize();
|
||||
});
|
||||
resizeObserver.observe(chartRef.value);
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 年份选择器配置(默认上一年)
|
||||
const disabledDate = (current: Dayjs) =>
|
||||
current && current.isAfter(dayjs(), 'year');
|
||||
|
||||
const initYear = (): Dayjs => dayjs().subtract(1, 'year');
|
||||
const year = ref<Dayjs>(initYear());
|
||||
|
||||
// ==================== 数据请求 ====================
|
||||
// 数据请求(查询参数取 modelStore.selectedAnchorPoint)
|
||||
const fetchData = async () => {
|
||||
const stcd = modelStore.selectedAnchorPoint?.stcd;
|
||||
if (!year.value || !stcd) return;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const res = await yearGetYearFpStatistics({
|
||||
year: year.value.format('YYYY'),
|
||||
stcd
|
||||
});
|
||||
|
||||
const responseData = res?.data || [];
|
||||
const firstBasin = Array.isArray(responseData) ? responseData[0] : null;
|
||||
|
||||
if (!firstBasin || !firstBasin.fpFtpStatitcsVos?.length) {
|
||||
chartData.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 取第一个流域的鱼类组成分布
|
||||
chartData.value = firstBasin.fpFtpStatitcsVos.map((fish: any) => ({
|
||||
name: fish.fishName,
|
||||
value: fish.fcnt
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取过鱼统计数据失败:', error);
|
||||
chartData.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 年份选择 change 触发查询
|
||||
const handleSearch = () => fetchData();
|
||||
watch(
|
||||
() => uiStore.searchDrawerOpen,
|
||||
newVal => {
|
||||
if (!newVal) {
|
||||
year.value = initYear();
|
||||
}
|
||||
}
|
||||
);
|
||||
// 选中点位变化时刷新;初始已有点位时立即加载
|
||||
watch(
|
||||
() => modelStore.selectedAnchorPoint?.stcd,
|
||||
(newStcd, oldStcd) => {
|
||||
if (!newStcd) return;
|
||||
if (newStcd !== oldStcd) fetchData();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.monitor-chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
// 高度由 chartHeight 内联样式动态控制(随 legend 行数增长)
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,444 @@
|
||||
<template>
|
||||
<SidePanelItem title="监测数据" :shrink="false">
|
||||
<template #title-right-content>
|
||||
<a-date-picker
|
||||
class="w-[110px]"
|
||||
v-model:value="year"
|
||||
picker="year"
|
||||
format="YYYY"
|
||||
value-format="YYYY"
|
||||
:allowClear="false"
|
||||
size="small"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
<a-spin :spinning="chartLoading" tip="加载中...">
|
||||
<div class="monitor-chart">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
<a-empty
|
||||
v-if="isChartEmpty"
|
||||
description="暂无数据"
|
||||
class="chart-empty"
|
||||
/>
|
||||
</div>
|
||||
</a-spin>
|
||||
<div class="monitor-table">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:scrollY="300"
|
||||
:scrollX="tableScrollX"
|
||||
:columns="tableColumns"
|
||||
:list-url="getTableList"
|
||||
:transform-data="transformTableData"
|
||||
row-key="_rowKey"
|
||||
:paginationConfig="{
|
||||
showSizeChanger: false,
|
||||
showQuickJumper: false
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
computed,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
nextTick
|
||||
} from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import {
|
||||
getNormalDataMonitoring2Year,
|
||||
getNormalDataMonitoring2List
|
||||
} from '@/api/mapModal';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const modelStore = useModelStore();
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
const tableRef = ref();
|
||||
const year = ref<string>('');
|
||||
|
||||
// ==================== 图表(柱状图:保护对象 x 轴,两两堆叠) ====================
|
||||
const chartLoading = ref(false);
|
||||
const chartData = ref<any[]>([]);
|
||||
const chartRef = ref<HTMLElement>();
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const toNum = (val: any) => {
|
||||
if (val === undefined || val === null || val === '' || isNaN(Number(val)))
|
||||
return 0;
|
||||
return Number(val);
|
||||
};
|
||||
|
||||
// 相同保护对象聚合求和
|
||||
const chartAggData = computed(() => {
|
||||
const map = new Map<
|
||||
string,
|
||||
{ tecnt: number; plantask: number; surnum: number; transplant: number }
|
||||
>();
|
||||
(Array.isArray(chartData.value) ? chartData.value : []).forEach(item => {
|
||||
const name = item?.tetp ?? '未知';
|
||||
if (!map.has(name)) {
|
||||
map.set(name, { tecnt: 0, plantask: 0, surnum: 0, transplant: 0 });
|
||||
}
|
||||
const agg = map.get(name)!;
|
||||
agg.tecnt += toNum(item.tecnt);
|
||||
agg.plantask += toNum(item.plantask);
|
||||
agg.surnum += toNum(item.surnum);
|
||||
agg.transplant += toNum(item.transplant);
|
||||
});
|
||||
return [...map.entries()].map(([name, agg]) => ({ name, ...agg }));
|
||||
});
|
||||
|
||||
const isChartEmpty = computed(() => chartAggData.value.length === 0);
|
||||
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
if (chartInstance) chartInstance.dispose();
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
updateChart();
|
||||
};
|
||||
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) return;
|
||||
if (isChartEmpty.value) {
|
||||
chartInstance.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = chartAggData.value;
|
||||
const xAxisData = data.map(item => item.name);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
backgroundColor: 'rgba(50,50,50,0.9)',
|
||||
textStyle: { color: '#fff', fontSize: 12 }
|
||||
},
|
||||
legend: {
|
||||
top: 0,
|
||||
data: ['实际种植', '种植要求', '存活数量', '移栽规模'],
|
||||
textStyle: { fontSize: 12 }
|
||||
},
|
||||
grid: { left: 50, right: 20, top: 50, bottom: 30 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { fontSize: 12, interval: 0 }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '实际种植',
|
||||
type: 'bar',
|
||||
stack: 'plant',
|
||||
barMaxWidth: 40,
|
||||
data: data.map(item => item.tecnt),
|
||||
itemStyle: { color: '#5B8FF9' }
|
||||
},
|
||||
{
|
||||
name: '种植要求',
|
||||
type: 'bar',
|
||||
stack: 'plant',
|
||||
barMaxWidth: 40,
|
||||
data: data.map(item => item.plantask),
|
||||
itemStyle: { color: '#63DAAB' }
|
||||
},
|
||||
{
|
||||
name: '存活数量',
|
||||
type: 'bar',
|
||||
stack: 'transplant',
|
||||
barMaxWidth: 40,
|
||||
data: data.map(item => item.surnum),
|
||||
itemStyle: { color: '#657798' }
|
||||
},
|
||||
{
|
||||
name: '移栽规模',
|
||||
type: 'bar',
|
||||
stack: 'transplant',
|
||||
barMaxWidth: 40,
|
||||
data: data.map(item => item.transplant),
|
||||
itemStyle: { color: '#F6C022' }
|
||||
}
|
||||
],
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
|
||||
},
|
||||
right: 0,
|
||||
top: 10
|
||||
}
|
||||
};
|
||||
|
||||
chartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
// 图表数据变化时动态刷新
|
||||
watch(chartAggData, () => {
|
||||
updateChart();
|
||||
});
|
||||
|
||||
const safeResize = (
|
||||
chart: echarts.ECharts | null,
|
||||
el: HTMLElement | undefined
|
||||
) => {
|
||||
if (!chart) return;
|
||||
// 容器不可见(display:none / 宽度为 0)时跳过,避免收起/切换时做昂贵的同步重排
|
||||
if (!el || el.clientWidth === 0) return;
|
||||
chart.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
setTimeout(() => {
|
||||
safeResize(chartInstance, chartRef.value);
|
||||
}, 200);
|
||||
if (!resizeObserver && chartRef.value) {
|
||||
resizeObserver = new ResizeObserver(() =>
|
||||
safeResize(chartInstance, chartRef.value)
|
||||
);
|
||||
resizeObserver.observe(chartRef.value);
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', () =>
|
||||
safeResize(chartInstance, chartRef.value)
|
||||
);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener('resize', () =>
|
||||
safeResize(chartInstance, chartRef.value)
|
||||
);
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 表格 ====================
|
||||
const formatValue = (val: any) =>
|
||||
val !== undefined && val !== null && String(val).trim() !== '' ? val : '-';
|
||||
|
||||
const tableColumns = ref([
|
||||
{
|
||||
title: '保护对象',
|
||||
dataIndex: 'tetp',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '实际种植/种植要求',
|
||||
dataIndex: 'surnum',
|
||||
width: 140,
|
||||
customRender: ({ record }: any) =>
|
||||
`${formatValue(record?.tecnt)}/${formatValue(record?.plantask)}`
|
||||
},
|
||||
{
|
||||
title: '存活数量/移栽规模',
|
||||
dataIndex: 'treeNum',
|
||||
width: 140,
|
||||
customRender: ({ record }: any) =>
|
||||
`${formatValue(record?.surnum)}/${formatValue(record?.transplant)}`
|
||||
}
|
||||
]);
|
||||
const tableScrollX = ref(320);
|
||||
|
||||
const transformTableData = (res: any) => {
|
||||
const records = res?.data?.records || res?.data?.data || res?.data || [];
|
||||
const total = res?.data?.data?.total || res?.data?.total || res?.total || 0;
|
||||
|
||||
return {
|
||||
records: records.map((item: any, index: number) => ({
|
||||
...item,
|
||||
_rowKey:
|
||||
item?.id ||
|
||||
item?.fid ||
|
||||
`${item?.stcd || 'stcd'}-${item?.tm || year.value || 'tm'}-${index}`
|
||||
})),
|
||||
total
|
||||
};
|
||||
};
|
||||
|
||||
// ==================== 年份与数据查询 ====================
|
||||
const getDefaultYear = (yearList: any[]) => {
|
||||
const firstItem = yearList?.[0];
|
||||
if (typeof firstItem === 'string' || typeof firstItem === 'number') {
|
||||
return String(firstItem);
|
||||
}
|
||||
return firstItem?.yr || firstItem?.tm || firstItem?.year || null;
|
||||
};
|
||||
|
||||
const buildFilter = () => ({
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'in',
|
||||
dataType: 'string',
|
||||
value: [modelStore.selectedAnchorPoint?.stcd]
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: year.value
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const getTableList = (params: any) => {
|
||||
const take = Number(params?.take || DEFAULT_PAGE_SIZE);
|
||||
const currentPage = Number(params?.skip || 1);
|
||||
const skip = Math.max(currentPage - 1, 0) * take;
|
||||
|
||||
return getNormalDataMonitoring2List({
|
||||
...params,
|
||||
take,
|
||||
skip,
|
||||
filter: params?.filter || buildFilter()
|
||||
});
|
||||
};
|
||||
|
||||
const refreshTable = () => {
|
||||
if (!modelStore.selectedAnchorPoint?.stcd || !year.value) return;
|
||||
tableRef.value?.getList(buildFilter());
|
||||
};
|
||||
|
||||
// 图表使用全量数据(不分页)按保护对象聚合
|
||||
const fetchChartData = async () => {
|
||||
if (!modelStore.selectedAnchorPoint?.stcd || !year.value) return;
|
||||
chartLoading.value = true;
|
||||
try {
|
||||
const res = await getNormalDataMonitoring2List({
|
||||
filter: buildFilter(),
|
||||
take: 10000,
|
||||
skip: 0
|
||||
});
|
||||
chartData.value = res?.data?.records || res?.data?.data || res?.data || [];
|
||||
} catch (error) {
|
||||
console.error('获取植物园图表数据失败:', error);
|
||||
chartData.value = [];
|
||||
} finally {
|
||||
chartLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshAll = () => {
|
||||
refreshTable();
|
||||
fetchChartData();
|
||||
};
|
||||
|
||||
// 获取年份列表并赋值默认年份(年份列表第一个),然后自动查询
|
||||
const getYearList = async () => {
|
||||
const stcd = modelStore.selectedAnchorPoint?.stcd;
|
||||
if (!stcd) return;
|
||||
try {
|
||||
const res: any = await getNormalDataMonitoring2Year({
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: stcd
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const yearData = res?.data?.data || res?.data?.records || res?.data || [];
|
||||
const defaultYear =
|
||||
getDefaultYear(yearData) || String(new Date().getFullYear());
|
||||
|
||||
year.value = String(defaultYear);
|
||||
refreshAll();
|
||||
} catch (error) {
|
||||
console.error('获取年份列表失败:', error);
|
||||
if (tableRef.value) {
|
||||
tableRef.value.loading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
if (!year.value) return;
|
||||
chartData.value = []; // 切换年份时先清空图表,避免加载期间显示旧数据
|
||||
refreshAll();
|
||||
};
|
||||
|
||||
// ==================== 联动 ====================
|
||||
watch(
|
||||
() => uiStore.searchDrawerOpen,
|
||||
newVal => {
|
||||
if (!newVal) {
|
||||
year.value = '';
|
||||
chartData.value = [];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 选中点位变化时重新加载;初始已有点位时立即加载
|
||||
watch(
|
||||
() => modelStore.selectedAnchorPoint?.stcd,
|
||||
(newStcd, oldStcd) => {
|
||||
if (!newStcd) return;
|
||||
if (newStcd !== oldStcd) {
|
||||
year.value = '';
|
||||
chartData.value = [];
|
||||
getYearList();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.monitor-chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.monitor-table {
|
||||
margin-top: 10px;
|
||||
width: 410px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,760 @@
|
||||
<template>
|
||||
<SidePanelItem title="水温监测" :shrink="false">
|
||||
<template #title-right-content>
|
||||
<a-range-picker
|
||||
class="w-[220px]"
|
||||
v-model:value="wtDateRange"
|
||||
format="YYYY-MM-DD"
|
||||
:show-time="showTimeConfig"
|
||||
:allowClear="false"
|
||||
:presets="DateSetting.RangeButton.month1"
|
||||
:disabled-date="disabledDate"
|
||||
size="small"
|
||||
@change="fetchWtData"
|
||||
/>
|
||||
</template>
|
||||
<div class="station-select-row">
|
||||
测站:
|
||||
<a-select
|
||||
v-model:value="wtSelectedStcd"
|
||||
:options="wtStationOptions"
|
||||
class="station-select"
|
||||
placeholder="请选择站点"
|
||||
@change="fetchWtData"
|
||||
/>
|
||||
</div>
|
||||
<a-spin :spinning="wtLoading" tip="加载中...">
|
||||
<div class="monitor-chart">
|
||||
<div ref="wtChartRef" class="chart-container"></div>
|
||||
<a-empty v-if="wtIsEmpty" description="暂无数据" class="chart-empty" />
|
||||
</div>
|
||||
</a-spin>
|
||||
</SidePanelItem>
|
||||
|
||||
<SidePanelItem title="流量监测" :shrink="false">
|
||||
<template #title-right-content>
|
||||
<a-range-picker
|
||||
class="w-[220px]"
|
||||
v-model:value="llDateRange"
|
||||
format="YYYY-MM-DD"
|
||||
:show-time="showTimeConfig"
|
||||
:allowClear="false"
|
||||
:presets="DateSetting.RangeButton.month1"
|
||||
:disabled-date="disabledDate"
|
||||
size="small"
|
||||
@change="fetchLlData"
|
||||
/>
|
||||
</template>
|
||||
<div class="station-select-row">
|
||||
测站:
|
||||
<a-select
|
||||
v-model:value="llSelectedStcd"
|
||||
:options="llStationOptions"
|
||||
class="station-select"
|
||||
placeholder="请选择站点"
|
||||
@change="fetchLlData"
|
||||
/>
|
||||
</div>
|
||||
<a-spin :spinning="llLoading" tip="加载中...">
|
||||
<div class="monitor-chart">
|
||||
<div ref="llChartRef" class="chart-container"></div>
|
||||
<a-empty v-if="llIsEmpty" description="暂无数据" class="chart-empty" />
|
||||
</div>
|
||||
</a-spin>
|
||||
</SidePanelItem>
|
||||
|
||||
<SidePanelItem title="实时视频" :shrink="false">
|
||||
<template #title-right-content> </template>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
computed,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
nextTick
|
||||
} from 'vue';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import * as echarts from 'echarts';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import {
|
||||
getMonitorDataWaterTemp,
|
||||
getMonitorDataWt,
|
||||
getMonitorDataZq
|
||||
} from '@/api/mapModal';
|
||||
import { getFlowStationList } from '@/api/DataQueryMenuModule';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import { DateSetting } from '@/utils/enumeration';
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const modelStore = useModelStore();
|
||||
|
||||
// ==================== 时间选择器配置 ====================
|
||||
const showTimeConfig = {
|
||||
format: 'HH:mm',
|
||||
hourStep: 1,
|
||||
minuteStep: 5,
|
||||
secondStep: 60
|
||||
};
|
||||
const disabledDate = (current: Dayjs) =>
|
||||
current && current.isAfter(dayjs(), 'day');
|
||||
|
||||
const initDateRange = (): [Dayjs, Dayjs] => {
|
||||
return [dayjs().subtract(1, 'month'), dayjs()];
|
||||
};
|
||||
|
||||
// ==================== 水温图表 ====================
|
||||
const wtLoading = ref(false);
|
||||
const wtChartData = ref<any[]>([]);
|
||||
const wtDateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
|
||||
const wtChartRef = ref<HTMLElement>();
|
||||
let wtChartInstance: echarts.ECharts | null = null;
|
||||
let wtResizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const wtIsEmpty = computed(() => {
|
||||
const data = Array.isArray(wtChartData.value) ? wtChartData.value : [];
|
||||
return data.length === 0;
|
||||
});
|
||||
|
||||
const initWtChart = () => {
|
||||
if (!wtChartRef.value) return;
|
||||
if (wtChartInstance) wtChartInstance.dispose();
|
||||
wtChartInstance = echarts.init(wtChartRef.value);
|
||||
updateWtChart(wtChartData.value);
|
||||
};
|
||||
|
||||
function wtShouldStagger(width: number, dataLen: number) {
|
||||
return dataLen > Math.floor(width / 100);
|
||||
}
|
||||
|
||||
function getWtAxisLabelConfig(chartWidth: number, xAxisData: string[]) {
|
||||
const needStagger = wtShouldStagger(chartWidth, xAxisData.length);
|
||||
return {
|
||||
fontSize: 12,
|
||||
interval: 'auto',
|
||||
formatter: (value: string, index: number) => {
|
||||
const [date, time] = value.split(' ');
|
||||
if (needStagger) {
|
||||
return index % 2 === 0 ? `${date}\n${time}\n ` : ` \n${date}\n${time}`;
|
||||
}
|
||||
return `${date}\n${time}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const updateWtChart = (data: any[]) => {
|
||||
if (!wtChartInstance) return;
|
||||
if (!data || data.length === 0) {
|
||||
wtChartInstance.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const sorted = [...data].sort(
|
||||
(a, b) => new Date(a.tm).getTime() - new Date(b.tm).getTime()
|
||||
);
|
||||
const xAxisData = sorted.map(item =>
|
||||
dayjs(item.tm).format('YYYY-MM-DD HH:mm')
|
||||
);
|
||||
const width = wtChartRef.value?.clientWidth || 0;
|
||||
const axisLabelConfig = getWtAxisLabelConfig(width, xAxisData);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50,50,50,0.9)',
|
||||
textStyle: { color: '#fff', fontSize: 12 },
|
||||
axisPointer: { type: 'cross' },
|
||||
formatter: (params: any) => {
|
||||
if (!params?.length) return '';
|
||||
const idx = params[0].dataIndex;
|
||||
const fullTime = sorted[idx]?.tm
|
||||
? dayjs(sorted[idx].tm).format('YYYY-MM-DD HH:mm')
|
||||
: '';
|
||||
let html = `<div style="font-size:16px;margin-bottom:8px;">${fullTime}</div>`;
|
||||
params.forEach((p: any) => {
|
||||
const v = p.value != null ? Number(p.value).toFixed(1) : '-';
|
||||
html += `<div style="display:flex;align-items:center;justify-content:space-between;margin:4px 0;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${p.color};margin-right:8px;"></span><span style="flex:1;font-size:14px;">水温: </span><span style="font-size:14px;min-width:60px;text-align:right;"><strong>${v}</strong> °C</span></div>`;
|
||||
});
|
||||
return html;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
width: '80%',
|
||||
type: 'scroll',
|
||||
top: 10,
|
||||
data: ['水温'],
|
||||
textStyle: { fontSize: 12 }
|
||||
},
|
||||
grid: { left: 40, right: 20, top: 44, bottom: 50 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: axisLabelConfig,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
name: '水温(°C)',
|
||||
type: 'value',
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
scale: true,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '水温',
|
||||
type: 'line',
|
||||
data: sorted.map(item => item.wt),
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
lineStyle: { color: '#4B79AB', width: 2 },
|
||||
itemStyle: { color: '#4B79AB' },
|
||||
areaStyle: { color: '#85A9D0' }
|
||||
}
|
||||
],
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: [0], throttle: 50, start: 0, end: 100 }
|
||||
],
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
|
||||
},
|
||||
right: 20,
|
||||
top: 10
|
||||
}
|
||||
};
|
||||
|
||||
wtChartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
// 数据变化时动态刷新水温图表
|
||||
watch(wtChartData, newData => {
|
||||
updateWtChart(newData);
|
||||
});
|
||||
|
||||
const fetchWtData = async () => {
|
||||
const stcd = wtSelectedStcd.value;
|
||||
if (!wtDateRange.value || !stcd) return;
|
||||
wtLoading.value = true;
|
||||
|
||||
try {
|
||||
const filterParams = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: stcd
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: wtDateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: wtDateRange.value[1].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
]
|
||||
},
|
||||
sort: [{ field: 'tm', dir: 'asc' }]
|
||||
};
|
||||
|
||||
const res = await getMonitorDataWaterTemp(filterParams);
|
||||
wtChartData.value = res?.data?.data || res?.data?.records || [];
|
||||
} catch (error) {
|
||||
console.error('获取水温数据失败:', error);
|
||||
wtChartData.value = [];
|
||||
} finally {
|
||||
wtLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 流量图表 ====================
|
||||
const llLoading = ref(false);
|
||||
const llChartData = ref<any[]>([]);
|
||||
const llDateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
|
||||
const llChartRef = ref<HTMLElement>();
|
||||
let llChartInstance: echarts.ECharts | null = null;
|
||||
let llResizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const llIsEmpty = computed(() => {
|
||||
const data = Array.isArray(llChartData.value) ? llChartData.value : [];
|
||||
return data.length === 0;
|
||||
});
|
||||
|
||||
const initLlChart = () => {
|
||||
if (!llChartRef.value) return;
|
||||
if (llChartInstance) llChartInstance.dispose();
|
||||
llChartInstance = echarts.init(llChartRef.value);
|
||||
updateLlChart(llChartData.value);
|
||||
};
|
||||
|
||||
const updateLlChart = (data: any[]) => {
|
||||
if (!llChartInstance) return;
|
||||
if (!data || data.length === 0) {
|
||||
llChartInstance.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const sorted = [...data].sort(
|
||||
(a, b) => new Date(a.tm).getTime() - new Date(b.tm).getTime()
|
||||
);
|
||||
|
||||
const xAxisData = sorted.map(item => {
|
||||
const hm = dayjs(item.tm).format('HH:mm');
|
||||
if (hm === '00:00') {
|
||||
const date = dayjs(item.tm).format('MM-DD');
|
||||
return `${hm}\n${date}`;
|
||||
}
|
||||
return hm;
|
||||
});
|
||||
|
||||
const zData = sorted.map(item => item.z);
|
||||
const qData = sorted.map(item => item.q);
|
||||
const vData = sorted.map(item => item.v);
|
||||
|
||||
const formatRules: Record<
|
||||
string,
|
||||
{ format: (v: number) => string; unit: string }
|
||||
> = {
|
||||
水位: { format: v => v.toFixed(2), unit: '(m)' },
|
||||
流量: { format: v => String(Math.round(v)), unit: '(m³/s)' },
|
||||
流速: { format: v => v.toFixed(2), unit: '(m³/s)' }
|
||||
};
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.9)',
|
||||
textStyle: { color: '#fff', fontSize: 12 },
|
||||
axisPointer: { type: 'cross' },
|
||||
formatter: (params: any) => {
|
||||
if (!params || params.length === 0) return '';
|
||||
const dataIndex = params[0].dataIndex;
|
||||
const fullTime = sorted[dataIndex]?.tm
|
||||
? dayjs(sorted[dataIndex].tm).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '';
|
||||
let html = `<div style="font-size:16px;margin-bottom:8px;">${fullTime}</div>`;
|
||||
params.forEach((param: any) => {
|
||||
if (param.value == null) return;
|
||||
const rule = Object.entries(formatRules).find(([key]) =>
|
||||
param.seriesName.includes(key)
|
||||
);
|
||||
const displayValue = rule
|
||||
? rule[1].format(Number(param.value))
|
||||
: param.value;
|
||||
const unit = rule ? rule[1].unit : '';
|
||||
html += `
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin:4px 0;">
|
||||
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:8px;"></span>
|
||||
<span style="flex:1;font-size:14px;text-align:left;margin-right:6px;">${param.seriesName}: </span>
|
||||
<span style="font-size:14px;min-width:60px;text-align:right;"><strong>${displayValue}</strong> ${unit}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
return html;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
top: 0,
|
||||
data: ['水位', '流量', '流速'],
|
||||
textStyle: { fontSize: 12 },
|
||||
selected: {
|
||||
水位: true,
|
||||
流量: true,
|
||||
流速: true
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 100,
|
||||
top: 60,
|
||||
bottom: 50
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { fontSize: 12 },
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
name: '水位(m)',
|
||||
type: 'value',
|
||||
position: 'left',
|
||||
axisLine: { show: true, lineStyle: { color: '#56C2E3' } },
|
||||
axisLabel: { color: '#56C2E3' },
|
||||
nameTextStyle: { color: '#56C2E3' },
|
||||
alignTicks: true,
|
||||
scale: true,
|
||||
splitNumber: 9
|
||||
},
|
||||
{
|
||||
name: '流量(m³/s)',
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
axisLine: { show: true, lineStyle: { color: '#9556A4' } },
|
||||
axisLabel: { color: '#9556A4' },
|
||||
nameTextStyle: { color: '#9556A4' },
|
||||
alignTicks: true,
|
||||
scale: true,
|
||||
splitNumber: 9
|
||||
},
|
||||
{
|
||||
name: '流速(m³/s)',
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
axisLine: { show: true, lineStyle: { color: '#78C300' } },
|
||||
axisLabel: { color: '#78C300' },
|
||||
nameTextStyle: { color: '#78C300' },
|
||||
alignTicks: true,
|
||||
scale: true,
|
||||
splitNumber: 9,
|
||||
offset: 60
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '水位',
|
||||
type: 'line',
|
||||
yAxisIndex: 0,
|
||||
data: zData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#56C2E3', width: 2 },
|
||||
itemStyle: { color: '#56C2E3' }
|
||||
},
|
||||
{
|
||||
name: '流量',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: qData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#9556A4', width: 2 },
|
||||
itemStyle: { color: '#9556A4' }
|
||||
},
|
||||
{
|
||||
name: '流速',
|
||||
type: 'line',
|
||||
yAxisIndex: 2,
|
||||
data: vData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { color: '#78C300', width: 2 },
|
||||
itemStyle: { color: '#78C300' }
|
||||
}
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0],
|
||||
throttle: 50,
|
||||
start: 0,
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
|
||||
},
|
||||
right: 20,
|
||||
top: -6
|
||||
}
|
||||
};
|
||||
|
||||
llChartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
// 数据变化时动态刷新流量图表
|
||||
watch(llChartData, newData => {
|
||||
updateLlChart(newData);
|
||||
});
|
||||
|
||||
const fetchLlData = async () => {
|
||||
const stcd = llSelectedStcd.value;
|
||||
if (!llDateRange.value || !stcd) return;
|
||||
llLoading.value = true;
|
||||
|
||||
try {
|
||||
const filterParams = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: stcd
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: llDateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: llDateRange.value[1].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
]
|
||||
},
|
||||
sort: [{ field: 'tm', dir: 'asc' }]
|
||||
};
|
||||
|
||||
const res = await getFlowStationList(filterParams);
|
||||
llChartData.value = res?.data?.data || res?.data?.records || [];
|
||||
} catch (error) {
|
||||
console.error('获取流量数据失败:', error);
|
||||
llChartData.value = [];
|
||||
} finally {
|
||||
llLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 测站选择器(水温) ====================
|
||||
const wtStationOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const wtSelectedStcd = ref<string>('');
|
||||
let wtStationReqSeq = 0; // 请求序号,用于竞态守卫(仅最后一次请求生效)
|
||||
const loadWtStationList = async () => {
|
||||
const fhstcd = modelStore.selectedAnchorPoint?.stcd;
|
||||
if (!fhstcd) return;
|
||||
const requestSeq = ++wtStationReqSeq;
|
||||
wtStationOptions.value = []; // 先清空,等待加载
|
||||
wtSelectedStcd.value = ''; // 同步清空选中值,避免上一父站选中残留
|
||||
try {
|
||||
const res = await getMonitorDataWt({
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'fhstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: fhstcd
|
||||
},
|
||||
{
|
||||
field: 'sttpCode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: 'WTRV'
|
||||
}
|
||||
]
|
||||
},
|
||||
select: ['stcd', 'stnm']
|
||||
});
|
||||
if (requestSeq !== wtStationReqSeq) return; // 非最新请求,丢弃结果
|
||||
const stations = res?.data?.data || res?.data?.records || [];
|
||||
if (stations.length > 0) {
|
||||
wtStationOptions.value = stations.map((item: any) => ({
|
||||
label: item.stnm,
|
||||
value: item.stcd
|
||||
}));
|
||||
wtSelectedStcd.value = stations[0].stcd;
|
||||
fetchWtData();
|
||||
}
|
||||
} catch (error) {
|
||||
if (requestSeq !== wtStationReqSeq) return; // 非最新请求,忽略错误
|
||||
console.error('获取水温测站列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 测站选择器(流量) ====================
|
||||
const llStationOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const llSelectedStcd = ref<string>('');
|
||||
let llStationReqSeq = 0; // 请求序号,用于竞态守卫(仅最后一次请求生效)
|
||||
const loadLlStationList = async () => {
|
||||
const fhstcd = modelStore.selectedAnchorPoint?.stcd;
|
||||
if (!fhstcd) return;
|
||||
const requestSeq = ++llStationReqSeq;
|
||||
llStationOptions.value = []; // 先清空,等待加载
|
||||
llSelectedStcd.value = ''; // 同步清空选中值,避免上一父站选中残留
|
||||
try {
|
||||
const res = await getMonitorDataZq({
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'fhstcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: fhstcd
|
||||
},
|
||||
{
|
||||
field: 'sttpCode',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: 'ZQ'
|
||||
}
|
||||
]
|
||||
},
|
||||
select: ['stcd', 'stnm']
|
||||
});
|
||||
if (requestSeq !== llStationReqSeq) return; // 非最新请求,丢弃结果
|
||||
const stations = res?.data?.data || res?.data?.records || [];
|
||||
if (stations.length > 0) {
|
||||
llStationOptions.value = stations.map((item: any) => ({
|
||||
label: item.stnm,
|
||||
value: item.stcd
|
||||
}));
|
||||
llSelectedStcd.value = stations[0].stcd;
|
||||
fetchLlData();
|
||||
}
|
||||
} catch (error) {
|
||||
if (requestSeq !== llStationReqSeq) return; // 非最新请求,忽略错误
|
||||
console.error('获取流量测站列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
const safeResize = (
|
||||
chart: echarts.ECharts | null,
|
||||
el: HTMLElement | undefined
|
||||
) => {
|
||||
if (!chart) return;
|
||||
// 容器不可见(display:none / 宽度为 0)时跳过,避免收起/切换时做昂贵的同步重排
|
||||
if (!el || el.clientWidth === 0) return;
|
||||
chart.resize();
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
safeResize(wtChartInstance, wtChartRef.value);
|
||||
safeResize(llChartInstance, llChartRef.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initWtChart();
|
||||
initLlChart();
|
||||
setTimeout(() => {
|
||||
handleResize();
|
||||
}, 200);
|
||||
if (!wtResizeObserver && wtChartRef.value) {
|
||||
wtResizeObserver = new ResizeObserver(() =>
|
||||
safeResize(wtChartInstance, wtChartRef.value)
|
||||
);
|
||||
wtResizeObserver.observe(wtChartRef.value);
|
||||
}
|
||||
if (!llResizeObserver && llChartRef.value) {
|
||||
llResizeObserver = new ResizeObserver(() =>
|
||||
safeResize(llChartInstance, llChartRef.value)
|
||||
);
|
||||
llResizeObserver.observe(llChartRef.value);
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (wtChartInstance) {
|
||||
wtChartInstance.dispose();
|
||||
wtChartInstance = null;
|
||||
}
|
||||
if (llChartInstance) {
|
||||
llChartInstance.dispose();
|
||||
llChartInstance = null;
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (wtResizeObserver) {
|
||||
wtResizeObserver.disconnect();
|
||||
wtResizeObserver = null;
|
||||
}
|
||||
if (llResizeObserver) {
|
||||
llResizeObserver.disconnect();
|
||||
llResizeObserver = null;
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 联动 ====================
|
||||
watch(
|
||||
() => uiStore.searchDrawerOpen,
|
||||
newVal => {
|
||||
if (!newVal) {
|
||||
wtDateRange.value = initDateRange();
|
||||
llDateRange.value = initDateRange();
|
||||
}
|
||||
}
|
||||
);
|
||||
// 选中点位变化时重新加载测站列表并刷新图表;初始已有点位时立即加载
|
||||
watch(
|
||||
() => modelStore.selectedAnchorPoint?.stcd,
|
||||
(newStcd, oldStcd) => {
|
||||
if (!newStcd) return;
|
||||
if (newStcd !== oldStcd) {
|
||||
loadWtStationList();
|
||||
loadLlStationList();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 全局时间范围变化时同步本地日期选择器(默认参数联动)
|
||||
watch(
|
||||
() => modelStore.filter.rangeTm,
|
||||
newRange => {
|
||||
if (Array.isArray(newRange) && newRange.length > 0) {
|
||||
const next = [dayjs(newRange[0]), dayjs(newRange[1])];
|
||||
wtDateRange.value = next;
|
||||
llDateRange.value = next;
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.station-select-row {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.station-select {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.monitor-chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<SidePanelItem title="垂向水温监测数据" :shrink="false">
|
||||
<template #title-right-content>
|
||||
<a-range-picker
|
||||
class="w-[220px]"
|
||||
v-model:value="dateRange"
|
||||
format="YYYY-MM-DD"
|
||||
:show-time="showTimeConfig"
|
||||
:allowClear="false"
|
||||
:presets="DateSetting.RangeButton.month1"
|
||||
:disabled-date="disabledDate"
|
||||
size="small"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
<a-spin :spinning="isLoading" tip="加载中...">
|
||||
<div class="monitor-chart">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
<a-empty
|
||||
v-if="!chartData || chartData.length === 0"
|
||||
description="暂无数据"
|
||||
class="chart-empty"
|
||||
/>
|
||||
</div>
|
||||
<div class="monitor-table">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
row-key="dt"
|
||||
:scrollY="240"
|
||||
:scrollX="tableScrollX"
|
||||
:columns="tableColumns"
|
||||
:data="tableData"
|
||||
:paginationConfig="{
|
||||
showSizeChanger: false,
|
||||
showQuickJumper: false
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
computed,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
nextTick
|
||||
} from 'vue';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import * as echarts from 'echarts';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import { getMonitorDataWaterTempVertical } from '@/api/mapModal';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { DateSetting } from '@/utils/enumeration';
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const modelStore = useModelStore();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const tableData = ref<any[]>([]);
|
||||
const chartData = ref<any[]>([]);
|
||||
const tableRef = ref<any>();
|
||||
|
||||
// ==================== 图表 ====================
|
||||
const chartRef = ref<HTMLElement>();
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
if (chartInstance) chartInstance.dispose();
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
updateChart(chartData.value);
|
||||
};
|
||||
|
||||
const COLORS = [
|
||||
'#5470c6',
|
||||
'#91cc75',
|
||||
'#fac858',
|
||||
'#ee6666',
|
||||
'#73c0de',
|
||||
'#3ba272',
|
||||
'#fc8452',
|
||||
'#9a60b4',
|
||||
'#ea7ccc',
|
||||
'#5470c6',
|
||||
'#73c0de',
|
||||
'#fac858',
|
||||
'#91cc75',
|
||||
'#ee6666',
|
||||
'#3ba272',
|
||||
'#fc8452',
|
||||
'#9a60b4',
|
||||
'#ea7ccc',
|
||||
'#5470c6',
|
||||
'#73c0de'
|
||||
];
|
||||
|
||||
const updateChart = (rows: any[]) => {
|
||||
if (!chartInstance) return;
|
||||
if (!rows || rows.length === 0) {
|
||||
chartInstance.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// 为每个数据行生成一条垂向水温分布曲线
|
||||
const series = rows.map((row, index) => {
|
||||
const data = Object.entries(row.dataList || {})
|
||||
.map(([depth, temp]) => [parseFloat(temp as string), parseFloat(depth)])
|
||||
.sort((a, b) => a[1] - b[1]);
|
||||
|
||||
return {
|
||||
name: dayjs(row.dt).format('YYYY-MM-DD HH:mm'),
|
||||
type: 'line',
|
||||
data,
|
||||
smooth: false,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: { width: 2, color: COLORS[index % COLORS.length] },
|
||||
itemStyle: { color: COLORS[index % COLORS.length] }
|
||||
};
|
||||
});
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '垂向水温分布',
|
||||
top: 0,
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
formatter: (value: any) => {
|
||||
if (value.axisDimension == 'x') {
|
||||
return value.value.toFixed(1) + '℃';
|
||||
} else if (value.axisDimension == 'y') {
|
||||
return value.value.toFixed(2) + 'm';
|
||||
}
|
||||
|
||||
return value.value;
|
||||
}
|
||||
}
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
let html = params[0].seriesName + '\n';
|
||||
params.forEach((item: any) => {
|
||||
html += `<div style="display:flex;"><div style="width:40px;">${item.data[1]}m :</div> <div>${item.data[0]}℃</div></div>`;
|
||||
});
|
||||
return html;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
top: 30,
|
||||
data: series.map(s => s.name),
|
||||
textStyle: { fontSize: 12 }
|
||||
},
|
||||
grid: {
|
||||
left: 40,
|
||||
right: 70,
|
||||
top: 80,
|
||||
bottom: 40,
|
||||
containLabel: false
|
||||
},
|
||||
xAxis: {
|
||||
position: 'top',
|
||||
name: '水温 (℃)',
|
||||
nameLocation: 'end',
|
||||
type: 'value',
|
||||
scale: true,
|
||||
boundaryGap: ['10%', '10%'],
|
||||
axisLabel: {
|
||||
fontSize: 12,
|
||||
color: '#000000',
|
||||
formatter: function (value: number) {
|
||||
return value.toFixed(1) + '℃';
|
||||
}
|
||||
},
|
||||
splitLine: { show: true }
|
||||
},
|
||||
yAxis: {
|
||||
name: '水深 (m)',
|
||||
type: 'value',
|
||||
inverse: true,
|
||||
nameLocation: 'end',
|
||||
min: 0,
|
||||
axisLabel: {
|
||||
fontSize: 12,
|
||||
color: '#000000',
|
||||
formatter: function (value: number) {
|
||||
return value + 'm';
|
||||
}
|
||||
},
|
||||
splitLine: { show: true }
|
||||
},
|
||||
series,
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
|
||||
},
|
||||
right: 20,
|
||||
top: 10
|
||||
},
|
||||
dataZoom: {
|
||||
type: 'inside',
|
||||
xAxisIndex: [0],
|
||||
throttle: 50,
|
||||
start: 0,
|
||||
end: 100
|
||||
}
|
||||
};
|
||||
|
||||
chartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
// 数据变化时动态刷新图表
|
||||
watch(chartData, newData => {
|
||||
updateChart(newData);
|
||||
});
|
||||
|
||||
const safeResize = () => {
|
||||
if (!chartInstance) return;
|
||||
// 容器不可见(display:none / 宽度为 0)时跳过,避免收起/切换时对大量 series 做昂贵的同步重排
|
||||
if (!chartRef.value || chartRef.value.clientWidth === 0) return;
|
||||
chartInstance.resize();
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
safeResize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
setTimeout(() => {
|
||||
safeResize();
|
||||
}, 200);
|
||||
if (!resizeObserver && chartRef.value) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
safeResize();
|
||||
});
|
||||
resizeObserver.observe(chartRef.value);
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 固定按小时粒度查询
|
||||
const showTimeConfig = {
|
||||
format: 'HH:mm',
|
||||
hourStep: 1,
|
||||
minuteStep: 5,
|
||||
secondStep: 60
|
||||
};
|
||||
const disabledDate = (current: Dayjs) =>
|
||||
current && current.isAfter(dayjs(), 'day');
|
||||
|
||||
const initDateRange = (): [Dayjs, Dayjs] => {
|
||||
const startDate = dayjs().subtract(7, 'day').startOf('day');
|
||||
return [startDate, dayjs()];
|
||||
};
|
||||
const dateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
|
||||
|
||||
// ==================== 表格列 ====================
|
||||
const columns = ref<any[]>([]);
|
||||
|
||||
const tableColumns = computed(() => {
|
||||
const fixedCols = [
|
||||
{
|
||||
title: '测站名称',
|
||||
dataIndex: 'stnm',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'dt',
|
||||
width: 140,
|
||||
customRender: ({ text }: any) =>
|
||||
text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-'
|
||||
}
|
||||
];
|
||||
console.log(columns.value);
|
||||
|
||||
const excludeDataIndexes = fixedCols.map(col => col.dataIndex);
|
||||
// 展示全部水深列
|
||||
const dataCols = columns.value
|
||||
|
||||
.filter((col: any) => !excludeDataIndexes.includes(col.dataIndex))
|
||||
.map((col: any) => ({
|
||||
title: col.title,
|
||||
dataIndex: col.dataIndex,
|
||||
width: 120,
|
||||
customRender: ({ record }: any) => {
|
||||
const val = record.dataList?.[col.dataIndex];
|
||||
return val !== undefined && val !== null ? val : '-';
|
||||
}
|
||||
}));
|
||||
|
||||
return [...fixedCols, ...dataCols];
|
||||
});
|
||||
|
||||
const tableScrollX = computed(() => {
|
||||
const cols = tableColumns.value;
|
||||
const total = cols.reduce((s: number, c: any) => s + (c.width || 100), 0);
|
||||
console.log(total);
|
||||
return total > 600 ? total : undefined;
|
||||
});
|
||||
|
||||
// ==================== 数据请求 ====================
|
||||
// 数据请求(查询参数取 modelStore.selectedAnchorPoint)
|
||||
const fetchData = async () => {
|
||||
const stcd = modelStore.selectedAnchorPoint?.stcd;
|
||||
if (!dateRange.value || !stcd) return;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const filterParams = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: stcd
|
||||
},
|
||||
{
|
||||
field: 'drtp',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: 'HOUR'
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[1].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
]
|
||||
},
|
||||
sort: [{ field: 'dt', dir: 'asc' }]
|
||||
};
|
||||
|
||||
const res = await getMonitorDataWaterTempVertical(filterParams);
|
||||
const responseData = res?.data?.data || res?.data || {};
|
||||
const item = Array.isArray(responseData) ? responseData[0] : responseData;
|
||||
|
||||
const cols = item?.columns || [];
|
||||
const dataSource = item?.dataSource || item?.data || [];
|
||||
|
||||
columns.value = cols;
|
||||
tableData.value = [...dataSource].reverse();
|
||||
chartData.value = JSON.parse(JSON.stringify(tableData.value));
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
tableData.value = [];
|
||||
chartData.value = [];
|
||||
columns.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 时间选择器 change 触发查询
|
||||
const handleSearch = () => fetchData();
|
||||
watch(
|
||||
() => uiStore.searchDrawerOpen,
|
||||
newVal => {
|
||||
if (!newVal) {
|
||||
dateRange.value = initDateRange();
|
||||
}
|
||||
}
|
||||
);
|
||||
// 选中点位变化时刷新;初始已有点位时立即加载
|
||||
watch(
|
||||
() => modelStore.selectedAnchorPoint?.stcd,
|
||||
(newStcd, oldStcd) => {
|
||||
if (!newStcd) return;
|
||||
if (newStcd !== oldStcd) fetchData();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 全局时间范围变化时同步本地日期选择器(默认参数联动)
|
||||
watch(
|
||||
() => modelStore.filter.rangeTm,
|
||||
newRange => {
|
||||
if (Array.isArray(newRange) && newRange.length > 0) {
|
||||
dateRange.value = [dayjs(newRange[0]), dayjs(newRange[1])];
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.monitor-chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.monitor-table {
|
||||
margin-top: 10px;
|
||||
box-sizing: border-box;
|
||||
// 表格列数多、tableScrollX 较宽(可达数千像素),必须限制在抽屉宽度内横向滚动,
|
||||
// 否则在 RightDrawer 的 .ant-drawer-content { overflow: visible } 下会溢出抽屉,
|
||||
// 抽屉滑出动画时看起来像"抽屉变 100% 宽"
|
||||
width: 410px;
|
||||
overflow-x: auto;
|
||||
|
||||
.table-container {
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,696 @@
|
||||
<template>
|
||||
<SidePanelItem title="水质监测数据" :shrink="false">
|
||||
<template #title-right-content>
|
||||
<a-range-picker
|
||||
class="w-[220px]"
|
||||
v-model:value="dateRange"
|
||||
format="YYYY-MM-DD HH"
|
||||
:show-time="showTimeConfig"
|
||||
:allowClear="false"
|
||||
:presets="DateSetting.RangeButton.hour"
|
||||
:disabled-date="disabledDate"
|
||||
size="small"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div class="tab-checkbox" v-if="filteredCheckBoxOptions.length > 0">
|
||||
<a-checkbox-group
|
||||
v-model:value="selectedColumns"
|
||||
name="checkboxgroup"
|
||||
:options="filteredCheckBoxOptions"
|
||||
class="checkbox-group"
|
||||
@change="handleCheckboxChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a-spin :spinning="isLoading" tip="加载中...">
|
||||
<div class="monitor-chart">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
<a-empty
|
||||
v-if="isDataEmpty"
|
||||
description="暂无数据"
|
||||
class="chart-empty"
|
||||
/>
|
||||
</div>
|
||||
<div class="monitor-table">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:scrollY="240"
|
||||
:scrollX="tableScrollX"
|
||||
:columns="currentColumns"
|
||||
:data="tableData"
|
||||
:paginationConfig="{
|
||||
showSizeChanger: false,
|
||||
showQuickJumper: false
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
computed,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
nextTick
|
||||
} from 'vue';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import * as echarts from 'echarts';
|
||||
import {
|
||||
getMonitorDataWaterQuality,
|
||||
getMonitorDataWaterQualityDetail,
|
||||
getMonitorDataWaterQualityList
|
||||
} from '@/api/mapModal';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { DateSetting } from '@/utils/enumeration';
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const modelStore = useModelStore();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const tableData = ref<any[]>([]);
|
||||
const chartData = ref<any[]>([]);
|
||||
const tableRef = ref<any>();
|
||||
|
||||
// ==================== 图表 ====================
|
||||
const chartRef = ref<HTMLElement>();
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
if (chartInstance) chartInstance.dispose();
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
updateChart(chartData.value, allDetailParams.value);
|
||||
bindLegendEvent();
|
||||
};
|
||||
|
||||
// 时间选择器配置
|
||||
const showTimeConfig = {
|
||||
format: 'HH',
|
||||
hourStep: 1,
|
||||
minuteStep: 5,
|
||||
secondStep: 60
|
||||
};
|
||||
const disabledDate = (current: Dayjs) =>
|
||||
current && current.isAfter(dayjs(), 'day');
|
||||
|
||||
const initDateRange = (): [Dayjs, Dayjs] => {
|
||||
const startDate = dayjs().subtract(7, 'day').startOf('day');
|
||||
return [startDate, dayjs()];
|
||||
};
|
||||
const dateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
|
||||
|
||||
// ==================== Checkbox 配置 ====================
|
||||
const selectedColumns = ref<any[]>([]);
|
||||
const checkBoxOptions = [{ label: '综合分析', value: 'summary' }];
|
||||
const showSummaryCheckbox = ref(false);
|
||||
|
||||
const isDataEmpty = computed(() => {
|
||||
const data = Array.isArray(chartData.value) ? chartData.value : [];
|
||||
return data.length === 0;
|
||||
});
|
||||
|
||||
const filteredCheckBoxOptions = computed(() => {
|
||||
if (isDataEmpty.value) return [];
|
||||
|
||||
const available = checkBoxOptions.filter(opt => {
|
||||
if (opt.value === 'summary' && !showSummaryCheckbox.value) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
return available;
|
||||
});
|
||||
|
||||
const isSummaryMode = computed(() => selectedColumns.value.includes('summary'));
|
||||
|
||||
const handleCheckboxChange = (values: any[]) => {
|
||||
selectedColumns.value = values;
|
||||
if (values.length === 0) {
|
||||
selectedColumns.value = [];
|
||||
}
|
||||
fetchData();
|
||||
};
|
||||
|
||||
// ==================== 动态参数(用于图表和表格) ====================
|
||||
const allDetailParams = ref<any[]>([]); // 所有参数(用于表格列判断和图表series)
|
||||
const visibleSeriesNames = ref<string[]>([]); // 当前图表显示的series名称(最多2个)
|
||||
|
||||
// 解析showControl获取unit
|
||||
const parseUnit = (showControl: string) => {
|
||||
if (!showControl) return '';
|
||||
try {
|
||||
const config = JSON.parse(showControl);
|
||||
return config.unit || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 表格列配置 ====================
|
||||
// 固定左侧列
|
||||
const fixedColumns = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'tm',
|
||||
width: 160,
|
||||
customRender: ({ text }: any) =>
|
||||
text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-'
|
||||
},
|
||||
{
|
||||
title: '水质要求',
|
||||
dataIndex: 'wwqtgName',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '水质等级',
|
||||
dataIndex: 'wqgrdName',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '是否达标',
|
||||
dataIndex: 'sfdb',
|
||||
width: 100,
|
||||
customRender: ({ text }: any) => {
|
||||
const isUnqualified = text == 0; // 0是不达标,1是达标
|
||||
return {
|
||||
props: {
|
||||
style: { color: isUnqualified ? 'red' : 'inherit' }
|
||||
},
|
||||
children: text == 0 ? '不达标' : text == 1 ? '达标' : '-'
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 动态水质参数列,根据接口返回生成
|
||||
const fixedQualityColumns = computed(() => {
|
||||
const columns = allDetailParams.value.map((p: any) => {
|
||||
const unit = parseUnit(p.showControl);
|
||||
const title = unit ? `${p.ysShowName}(${unit})` : p.ysShowName;
|
||||
const dataIndex = p.ys.toLowerCase();
|
||||
const isEnabled = p.enable == 1;
|
||||
|
||||
// 宽度判断:默认100,超过5个字140,超过6个字160
|
||||
const titleLen = title.length;
|
||||
let width = 100;
|
||||
if (titleLen > 6) {
|
||||
width = 160;
|
||||
} else if (titleLen > 5) {
|
||||
width = 140;
|
||||
}
|
||||
|
||||
const fieldUpper = p.ys.toUpperCase(); // max/min 中的字段是大写
|
||||
|
||||
return {
|
||||
title,
|
||||
dataIndex,
|
||||
enabled: isEnabled,
|
||||
width,
|
||||
customRender: ({ text, record }: any) => {
|
||||
if (!isEnabled) return '-'; // enable=0 显示-
|
||||
if (text === undefined || text === null || text === '') return '-';
|
||||
const num = Number(text);
|
||||
if (isNaN(num)) return '-';
|
||||
|
||||
// 判断是否超出 max/min(max/min 从 record 中取)
|
||||
const maxList = record?.max;
|
||||
const minList = record?.min;
|
||||
let isOutOfRange = false;
|
||||
|
||||
if (Array.isArray(maxList) && maxList.length > 0) {
|
||||
const maxItem = maxList.find((m: any) => m[fieldUpper] !== undefined);
|
||||
if (maxItem && num > maxItem[fieldUpper]) {
|
||||
isOutOfRange = true;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(minList) && minList.length > 0) {
|
||||
const minItem = minList.find((m: any) => m[fieldUpper] !== undefined);
|
||||
if (minItem && num < minItem[fieldUpper]) {
|
||||
isOutOfRange = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
style: { color: isOutOfRange ? 'red' : 'inherit' }
|
||||
},
|
||||
children: num
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 综合分析模式下添加出库流量列
|
||||
if (isSummaryMode.value) {
|
||||
columns.push({
|
||||
title: '出库流量(m³/s)',
|
||||
dataIndex: 'qo',
|
||||
width: 140,
|
||||
customRender: ({ text }: any) => {
|
||||
if (text === undefined || text === null || text === '') return '-';
|
||||
const num = Number(text);
|
||||
return isNaN(num) ? '-' : num;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return columns;
|
||||
});
|
||||
|
||||
const currentColumns = computed(() => {
|
||||
return [...fixedColumns, ...fixedQualityColumns.value];
|
||||
});
|
||||
|
||||
const tableScrollX = computed(() => {
|
||||
const columns = currentColumns.value;
|
||||
const totalWidth = columns.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 120),
|
||||
0
|
||||
);
|
||||
return totalWidth > 600 ? totalWidth : undefined;
|
||||
});
|
||||
|
||||
// ==================== 图表 ====================
|
||||
function shouldStagger(width: number, dataLen: number) {
|
||||
return dataLen > Math.floor(width / 100);
|
||||
}
|
||||
|
||||
function getAxisLabelConfig(chartWidth: number, xAxisData: string[]) {
|
||||
const needStagger = shouldStagger(chartWidth, xAxisData.length);
|
||||
return {
|
||||
fontSize: 12,
|
||||
interval: 'auto',
|
||||
formatter: (value: string, index: number) => {
|
||||
const [date, time] = value.split(' ');
|
||||
if (needStagger) {
|
||||
return index % 2 === 0 ? `${date}\n${time}\n ` : ` \n${date}\n${time}`;
|
||||
}
|
||||
return `${date}\n${time}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const updateChart = (data: any[], enabledParams: any[]) => {
|
||||
if (!chartInstance) return;
|
||||
if (!data || data.length === 0) {
|
||||
chartInstance.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const sorted = [...data].sort(
|
||||
(a, b) => new Date(a.tm).getTime() - new Date(b.tm).getTime()
|
||||
);
|
||||
const xAxisData = sorted.map(item =>
|
||||
dayjs(item.tm).format('YYYY-MM-DD HH:mm')
|
||||
);
|
||||
const width = chartRef.value?.clientWidth || 0;
|
||||
const axisLabelConfig = getAxisLabelConfig(width, xAxisData);
|
||||
|
||||
// 生成所有series(从接口获取所有参数)
|
||||
const allSeries = enabledParams.map((param: any) => {
|
||||
const showControl = param.showControl || '{}';
|
||||
let lineColor = '';
|
||||
let unit = '';
|
||||
try {
|
||||
const config = JSON.parse(showControl);
|
||||
lineColor = config.lineColor || '';
|
||||
unit = config.unit || '';
|
||||
} catch {
|
||||
// 解析失败使用默认颜色
|
||||
}
|
||||
const dataIndex = param.ys.toLowerCase();
|
||||
const legendName = param.ysShowName; // legend不显示unit
|
||||
const yAxisName = unit ? `${param.ysShowName}\n${unit}` : param.ysShowName; // Y轴显示名称+unit
|
||||
const color = lineColor;
|
||||
|
||||
return {
|
||||
name: legendName,
|
||||
ysShowName: param.ysShowName,
|
||||
unit,
|
||||
yAxisName,
|
||||
type: 'line',
|
||||
data: sorted.map(item => item[dataIndex]),
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
lineStyle: { color: color, width: 2 },
|
||||
itemStyle: { color: color }
|
||||
};
|
||||
});
|
||||
|
||||
// 综合分析模式下添加出库流量series(默认不选中)
|
||||
if (isSummaryMode.value) {
|
||||
allSeries.push({
|
||||
name: '出库流量',
|
||||
ysShowName: '出库流量',
|
||||
unit: 'm³/s',
|
||||
yAxisName: '出库流量\nm³/s',
|
||||
type: 'line',
|
||||
data: sorted.map(item => item.qo),
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
lineStyle: { color: '#4B79AB', width: 2 },
|
||||
itemStyle: { color: '#4B79AB' }
|
||||
});
|
||||
}
|
||||
|
||||
const legendData = allSeries.map((s: any) => s.name);
|
||||
|
||||
// 初始化可见series:默认只选中溶解氧
|
||||
if (visibleSeriesNames.value.length === 0) {
|
||||
const defaultName = '溶解氧';
|
||||
if (legendData.includes(defaultName)) {
|
||||
visibleSeriesNames.value = [defaultName];
|
||||
} else if (legendData.length > 0) {
|
||||
visibleSeriesNames.value = [legendData[0]];
|
||||
}
|
||||
}
|
||||
|
||||
// series 包含所有数据,通过 legend selected 控制显示
|
||||
const series = allSeries;
|
||||
|
||||
// 构建Y轴配置(最多两个,基于 visibleSeriesNames)
|
||||
const yAxisConfig: any[] = [];
|
||||
const visibleSeriesList = series.filter((s: any) =>
|
||||
visibleSeriesNames.value.includes(s.name)
|
||||
);
|
||||
|
||||
if (visibleSeriesList.length >= 1) {
|
||||
yAxisConfig.push({
|
||||
name: visibleSeriesList[0].yAxisName,
|
||||
type: 'value',
|
||||
position: 'left',
|
||||
scale: true,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||||
}
|
||||
});
|
||||
// 给所有series设置yAxisIndex
|
||||
series.forEach((s: any) => {
|
||||
s.yAxisIndex = visibleSeriesNames.value.includes(s.name) ? 0 : 0;
|
||||
});
|
||||
}
|
||||
if (visibleSeriesList.length >= 2) {
|
||||
yAxisConfig.push({
|
||||
name: visibleSeriesList[1].yAxisName,
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
scale: true,
|
||||
splitLine: { show: false }
|
||||
});
|
||||
visibleSeriesList[0].yAxisIndex = 0;
|
||||
visibleSeriesList[1].yAxisIndex = 1;
|
||||
}
|
||||
|
||||
// 构建legend selected状态
|
||||
const legendSelected = legendData.reduce((acc: any, name) => {
|
||||
acc[name] = visibleSeriesNames.value.includes(name);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const option: any = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50,50,50,0.9)',
|
||||
textStyle: { color: '#fff', fontSize: 12 },
|
||||
axisPointer: { type: 'cross' },
|
||||
formatter: (params: any) => {
|
||||
if (!params?.length) return '';
|
||||
const idx = params[0].dataIndex;
|
||||
const fullTime = sorted[idx]?.tm
|
||||
? dayjs(sorted[idx].tm).format('YYYY-MM-DD HH:mm')
|
||||
: '';
|
||||
let html = `<div style="font-size:16px;margin-bottom:8px;">${fullTime}</div>`;
|
||||
params.forEach((p: any) => {
|
||||
const v = p.value != null ? Number(p.value).toFixed(2) : '-';
|
||||
html += `<div style="display:flex;align-items:center;justify-content:space-between;margin:4px 0;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${p.color};margin-right:8px;"></span><span style="flex:1;font-size:14px;">${p.seriesName}: </span><span style="font-size:14px;min-width:60px;text-align:right;"><strong>${v}</strong></span></div>`;
|
||||
});
|
||||
return html;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
width: '80%',
|
||||
right: 60,
|
||||
top: 10,
|
||||
data: legendData,
|
||||
textStyle: { fontSize: 12 },
|
||||
selected: legendSelected
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: series.length >= 2 ? 60 : 40,
|
||||
top: 80,
|
||||
bottom: 60
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: axisLabelConfig,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||||
}
|
||||
},
|
||||
yAxis: yAxisConfig.length === 1 ? yAxisConfig[0] : yAxisConfig,
|
||||
series,
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: [0], throttle: 50, start: 0, end: 100 }
|
||||
],
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
|
||||
},
|
||||
right: 20,
|
||||
top: 10
|
||||
}
|
||||
};
|
||||
|
||||
chartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
// 绑定legend切换事件(只绑定一次)
|
||||
const bindLegendEvent = () => {
|
||||
if (!chartInstance) return;
|
||||
chartInstance.off('legendselectchanged');
|
||||
chartInstance.on('legendselectchanged', (params: any) => {
|
||||
const selected = params.selected;
|
||||
const name = params.name;
|
||||
const wasSelected = selected[name]; // echarts already updated internal state
|
||||
|
||||
if (wasSelected) {
|
||||
// 用户点击了选中(从取消变为选中)
|
||||
const prevVisible = [...visibleSeriesNames.value];
|
||||
if (prevVisible.length >= 2) {
|
||||
// 已有2个选中,取消第一个,保留第二个和新点击的
|
||||
visibleSeriesNames.value = [prevVisible[1], name];
|
||||
} else {
|
||||
visibleSeriesNames.value = [...new Set([...prevVisible, name])];
|
||||
}
|
||||
} else {
|
||||
// 用户点击了取消选中
|
||||
visibleSeriesNames.value = visibleSeriesNames.value.filter(
|
||||
n => n !== name
|
||||
);
|
||||
}
|
||||
|
||||
// 重新渲染图表(使用allDetailParams包含所有参数)
|
||||
updateChart(chartData.value, allDetailParams.value);
|
||||
});
|
||||
};
|
||||
|
||||
// ==================== 数据请求 ====================
|
||||
const fetchData = async () => {
|
||||
const stcd = modelStore.selectedAnchorPoint?.stcd;
|
||||
if (!dateRange.value || !stcd) return;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const filterParams = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: stcd
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[1].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
]
|
||||
},
|
||||
sort: [{ field: 'tm', dir: 'asc' }]
|
||||
};
|
||||
|
||||
// 并发请求三个接口
|
||||
const [qualityRes, detailRes, listRes] = await Promise.all([
|
||||
getMonitorDataWaterQuality(filterParams),
|
||||
getMonitorDataWaterQualityDetail({
|
||||
stcd,
|
||||
tbCode: 'WQ_R',
|
||||
startTime: dateRange.value[0].format('YYYY-MM-DD HH:mm:ss'),
|
||||
endTime: dateRange.value[1].format('YYYY-MM-DD HH:mm:ss')
|
||||
}),
|
||||
getMonitorDataWaterQualityList(filterParams)
|
||||
]);
|
||||
|
||||
// 判断是否显示综合分析(只有当data不为null且不为空数组时才显示)
|
||||
const qualityData = qualityRes?.data?.data || qualityRes?.data?.records;
|
||||
showSummaryCheckbox.value = !!qualityData && qualityData.length > 0;
|
||||
|
||||
// 处理detail数据
|
||||
const detailData = detailRes?.data?.data || detailRes?.data || [];
|
||||
allDetailParams.value = detailData; // 所有参数用于表格列判断
|
||||
|
||||
// 重置可见series(因为参数可能变化)
|
||||
visibleSeriesNames.value = [];
|
||||
|
||||
// 获取列表数据
|
||||
const listData = listRes?.data?.data || listRes?.data?.records || [];
|
||||
|
||||
tableData.value = [...listData].reverse();
|
||||
chartData.value = listData;
|
||||
nextTick(() => updateChart(listData, allDetailParams.value));
|
||||
} catch (error) {
|
||||
console.error('获取水质数据失败:', error);
|
||||
tableData.value = [];
|
||||
chartData.value = [];
|
||||
allDetailParams.value = [];
|
||||
if (chartInstance) chartInstance.clear();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 时间选择器 change 触发查询
|
||||
const handleSearch = () => fetchData();
|
||||
|
||||
const safeResize = () => {
|
||||
if (!chartInstance) return;
|
||||
// 容器不可见(display:none / 宽度为 0)时跳过,避免收起/切换时做昂贵的同步重排
|
||||
if (!chartRef.value || chartRef.value.clientWidth === 0) return;
|
||||
chartInstance.resize();
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
safeResize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
setTimeout(() => {
|
||||
safeResize();
|
||||
}, 200);
|
||||
if (!resizeObserver && chartRef.value) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
safeResize();
|
||||
});
|
||||
resizeObserver.observe(chartRef.value);
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => uiStore.searchDrawerOpen,
|
||||
newVal => {
|
||||
if (!newVal) {
|
||||
dateRange.value = initDateRange();
|
||||
}
|
||||
}
|
||||
);
|
||||
// 选中点位变化时刷新;初始已有点位时立即加载
|
||||
watch(
|
||||
() => modelStore.selectedAnchorPoint?.stcd,
|
||||
(newStcd, oldStcd) => {
|
||||
if (!newStcd) return;
|
||||
if (newStcd !== oldStcd) fetchData();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 全局时间范围变化时同步本地日期选择器(默认参数联动)
|
||||
watch(
|
||||
() => modelStore.filter.rangeTm,
|
||||
newRange => {
|
||||
if (Array.isArray(newRange) && newRange.length > 0) {
|
||||
dateRange.value = [dayjs(newRange[0]), dayjs(newRange[1])];
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tab-checkbox {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.monitor-chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.monitor-table {
|
||||
margin-top: 10px;
|
||||
box-sizing: border-box;
|
||||
// 表格列数多、tableScrollX 较宽(可达数千像素),必须限制在抽屉宽度内横向滚动,
|
||||
// 否则在 RightDrawer 的 .ant-drawer-content { overflow: visible } 下会溢出抽屉,
|
||||
// 抽屉滑出动画时看起来像"抽屉变 100% 宽"
|
||||
width: 410px;
|
||||
overflow-x: auto;
|
||||
|
||||
.table-container {
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,531 @@
|
||||
<template>
|
||||
<SidePanelItem title="水温监测数据" :shrink="false">
|
||||
<template #title-right-content>
|
||||
<a-range-picker
|
||||
class="w-[220px]"
|
||||
v-model:value="dateRange"
|
||||
format="YYYY-MM-DD"
|
||||
:show-time="showTimeConfig"
|
||||
:allowClear="false"
|
||||
:presets="DateSetting.RangeButton.month1"
|
||||
:disabled-date="disabledDate"
|
||||
size="small"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
<a-spin :spinning="isLoading" tip="加载中...">
|
||||
<div class="monitor-chart">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
<a-empty
|
||||
v-if="!chartData || chartData.length === 0"
|
||||
description="暂无数据"
|
||||
class="chart-empty"
|
||||
/>
|
||||
</div>
|
||||
<div class="monitor-table">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
:scrollY="240"
|
||||
:scrollX="tableScrollX"
|
||||
:columns="currentColumns"
|
||||
:data="tableData"
|
||||
:paginationConfig="{
|
||||
showSizeChanger: false,
|
||||
showQuickJumper: false
|
||||
}"
|
||||
>
|
||||
<template #summary>
|
||||
<a-table-summary fixed>
|
||||
<template
|
||||
v-for="summaryRow in summaryRows"
|
||||
:key="summaryRow.label"
|
||||
>
|
||||
<a-table-summary-row class="summary-row">
|
||||
<a-table-summary-cell :index="0">
|
||||
{{ summaryRow.label }}
|
||||
</a-table-summary-cell>
|
||||
<a-table-summary-cell
|
||||
v-for="col in summaryColumns"
|
||||
:key="col.dataIndex"
|
||||
:index="col.summaryIndex"
|
||||
>
|
||||
{{ summaryRow.cells[col.dataIndex]?.value ?? '-' }}
|
||||
<Tooltip
|
||||
v-if="summaryRow.cells[col.dataIndex]?.time"
|
||||
:title="`时间:${summaryRow.cells[col.dataIndex].time}`"
|
||||
placement="top"
|
||||
>
|
||||
<InfoCircleOutlined class="summary-tip-icon" />
|
||||
</Tooltip>
|
||||
</a-table-summary-cell>
|
||||
</a-table-summary-row>
|
||||
</template>
|
||||
</a-table-summary>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</a-spin>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
computed,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
nextTick
|
||||
} from 'vue';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import * as echarts from 'echarts';
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import { getMonitorDataWaterTemp } from '@/api/mapModal';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import BasicTable from '@/components/BasicTable/index.vue';
|
||||
import { DateSetting } from '@/utils/enumeration';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const modelStore = useModelStore();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const chartData = ref<any[]>([]);
|
||||
const tableData = ref<any[]>([]);
|
||||
const tableRef = ref<any>();
|
||||
|
||||
// ==================== 图表 ====================
|
||||
const chartRef = ref<HTMLElement>();
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
if (chartInstance) chartInstance.dispose();
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
updateChart(chartData.value);
|
||||
};
|
||||
|
||||
function shouldStagger(width: number, dataLen: number) {
|
||||
return dataLen > Math.floor(width / 100);
|
||||
}
|
||||
|
||||
function getAxisLabelConfig(chartWidth: number, xAxisData: string[]) {
|
||||
const needStagger = shouldStagger(chartWidth, xAxisData.length);
|
||||
return {
|
||||
fontSize: 12,
|
||||
interval: 'auto',
|
||||
formatter: (value: string, index: number) => {
|
||||
const [date, time] = value.split(' ');
|
||||
if (needStagger) {
|
||||
return index % 2 === 0 ? `${date}\n${time}\n ` : ` \n${date}\n${time}`;
|
||||
}
|
||||
return `${date}\n${time}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const updateChart = (data: any[]) => {
|
||||
if (!chartInstance) return;
|
||||
if (!data || data.length === 0) {
|
||||
chartInstance.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const sorted = [...data].sort(
|
||||
(a, b) => new Date(a.tm).getTime() - new Date(b.tm).getTime()
|
||||
);
|
||||
const xAxisData = sorted.map(item =>
|
||||
dayjs(item.tm).format('YYYY-MM-DD HH:mm')
|
||||
);
|
||||
const width = chartRef.value?.clientWidth || 0;
|
||||
const axisLabelConfig = getAxisLabelConfig(width, xAxisData);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50,50,50,0.9)',
|
||||
textStyle: { color: '#fff', fontSize: 12 },
|
||||
axisPointer: { type: 'cross' },
|
||||
formatter: (params: any) => {
|
||||
if (!params?.length) return '';
|
||||
const idx = params[0].dataIndex;
|
||||
const fullTime = sorted[idx]?.tm
|
||||
? dayjs(sorted[idx].tm).format('YYYY-MM-DD HH:mm')
|
||||
: '';
|
||||
let html = `<div style="font-size:16px;margin-bottom:8px;">${fullTime}</div>`;
|
||||
params.forEach((p: any) => {
|
||||
const v = p.value != null ? Number(p.value).toFixed(1) : '-';
|
||||
html += `<div style="display:flex;align-items:center;justify-content:space-between;margin:4px 0;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${p.color};margin-right:8px;"></span><span style="flex:1;font-size:14px;">水温: </span><span style="font-size:14px;min-width:60px;text-align:right;"><strong>${v}</strong> °C</span></div>`;
|
||||
});
|
||||
return html;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
width: '80%',
|
||||
type: 'scroll',
|
||||
top: 10,
|
||||
data: ['水温'],
|
||||
textStyle: { fontSize: 12 }
|
||||
},
|
||||
grid: { left: 40, right: 20, top: 44, bottom: 50 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: axisLabelConfig,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
name: '水温(°C)',
|
||||
type: 'value',
|
||||
axisLine: { lineStyle: { color: '#000000' } },
|
||||
scale: true,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '水温',
|
||||
type: 'line',
|
||||
data: sorted.map(item => item.wt),
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
lineStyle: { color: '#4B79AB', width: 2 },
|
||||
itemStyle: { color: '#4B79AB' },
|
||||
areaStyle: { color: '#85A9D0' }
|
||||
}
|
||||
],
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: [0], throttle: 50, start: 0, end: 100 }
|
||||
],
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
|
||||
},
|
||||
right: 20,
|
||||
top: 10
|
||||
}
|
||||
};
|
||||
|
||||
chartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
// 数据变化时动态刷新图表
|
||||
watch(chartData, newData => {
|
||||
updateChart(newData);
|
||||
});
|
||||
|
||||
const handleResize = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
setTimeout(() => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
}, 200);
|
||||
if (!resizeObserver && chartRef.value) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
});
|
||||
resizeObserver.observe(chartRef.value);
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 时间选择器配置
|
||||
const showTimeConfig = {
|
||||
format: 'HH:mm',
|
||||
hourStep: 1,
|
||||
minuteStep: 5,
|
||||
secondStep: 60
|
||||
};
|
||||
const disabledDate = (current: Dayjs) =>
|
||||
current && current.isAfter(dayjs(), 'day');
|
||||
|
||||
const initDateRange = (): [Dayjs, Dayjs] => {
|
||||
return [dayjs().subtract(1, 'month'), dayjs()];
|
||||
};
|
||||
const dateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
|
||||
|
||||
// ==================== 表格列配置 ====================
|
||||
const baseWaterTempColumns = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'tm',
|
||||
fixed: 'left',
|
||||
customRender: ({ text }: any) =>
|
||||
text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-'
|
||||
},
|
||||
{
|
||||
title: '水温(°C)',
|
||||
dataIndex: 'wt',
|
||||
summary: true,
|
||||
customRender: ({ text }: any) =>
|
||||
text !== undefined && text !== null ? Number(text).toFixed(1) : '-'
|
||||
}
|
||||
];
|
||||
|
||||
const currentColumns = baseWaterTempColumns;
|
||||
|
||||
const tableScrollX = computed(() => {
|
||||
const columns = currentColumns;
|
||||
const totalWidth = columns.reduce(
|
||||
(sum: number, col: any) => sum + (col.width || 100),
|
||||
0
|
||||
);
|
||||
return totalWidth > 600 ? totalWidth : undefined;
|
||||
});
|
||||
|
||||
// ==================== 合计行 ====================
|
||||
const dataPathMap: Record<string, string> = {
|
||||
wt: 'wt'
|
||||
};
|
||||
|
||||
const summaryColumns = computed(() => {
|
||||
return currentColumns
|
||||
.filter((col: any) => col.summary === true)
|
||||
.map((col: any, idx: number) => ({ ...col, summaryIndex: idx + 1 }));
|
||||
});
|
||||
|
||||
const getNestedValue = (obj: any, path: string) => {
|
||||
if (!path || !obj) return undefined;
|
||||
return path.split('.').reduce((acc, key) => acc?.[key], obj);
|
||||
};
|
||||
|
||||
const calcSummary = (dataIndex: string, type: 'max' | 'min' | 'avg') => {
|
||||
const data = Array.isArray(chartData.value) ? chartData.value : [];
|
||||
if (!data || data.length === 0) return null;
|
||||
const dataPath = dataPathMap[dataIndex] || dataIndex;
|
||||
const values = data
|
||||
.map((r: any) => {
|
||||
const val = getNestedValue(r, dataPath);
|
||||
return val !== null && val !== undefined && val !== '-'
|
||||
? Number(val)
|
||||
: NaN;
|
||||
})
|
||||
.filter((v: number) => !isNaN(v));
|
||||
if (values.length === 0) return null;
|
||||
if (type === 'max') return Math.max(...values);
|
||||
if (type === 'min') return Math.min(...values);
|
||||
return values.reduce((s, v) => s + v, 0) / values.length;
|
||||
};
|
||||
|
||||
const getSummaryTime = (dataIndex: string, type: 'max' | 'min') => {
|
||||
const data = Array.isArray(chartData.value) ? chartData.value : [];
|
||||
if (!data || data.length === 0) return '';
|
||||
const dataPath = dataPathMap[dataIndex] || dataIndex;
|
||||
const records = data.filter((r: any) => {
|
||||
const val = getNestedValue(r, dataPath);
|
||||
return val !== null && val !== undefined && val !== '-';
|
||||
});
|
||||
if (records.length === 0) return '';
|
||||
const target = Math[type](
|
||||
...records.map((r: any) => Number(getNestedValue(r, dataPath)))
|
||||
);
|
||||
const record = records.find(
|
||||
(r: any) => Number(getNestedValue(r, dataPath)) === target
|
||||
);
|
||||
return record?.tm ? dayjs(record.tm).format('YYYY-MM-DD HH:mm:ss') : '';
|
||||
};
|
||||
|
||||
const formatSummaryValue = (dataIndex: string, value: number | null) => {
|
||||
if (value === null) return '-';
|
||||
if (dataIndex.includes('qo') || dataIndex.includes('qi'))
|
||||
return Number(value).toFixed(3);
|
||||
if (dataIndex === 'deep_wt') return Number(value).toFixed(2);
|
||||
return Number(value).toFixed(1);
|
||||
};
|
||||
|
||||
const summaryRows = computed(() => {
|
||||
// Force dependency tracking
|
||||
void chartData.value;
|
||||
void summaryColumns.value;
|
||||
|
||||
if (!Array.isArray(chartData.value) || chartData.value.length === 0)
|
||||
return [];
|
||||
|
||||
const cols = summaryColumns.value;
|
||||
if (!cols || cols.length === 0) return [];
|
||||
|
||||
const rows = [
|
||||
{
|
||||
label: '最大值',
|
||||
showTime: true,
|
||||
cells: {} as Record<string, { value: string; time: string }>
|
||||
},
|
||||
{
|
||||
label: '最小值',
|
||||
showTime: true,
|
||||
cells: {} as Record<string, { value: string; time: string }>
|
||||
},
|
||||
{
|
||||
label: '平均值',
|
||||
showTime: false,
|
||||
cells: {} as Record<string, { value: string; time: string }>
|
||||
}
|
||||
];
|
||||
|
||||
cols.forEach((col: any) => {
|
||||
const dataIndex = col.dataIndex;
|
||||
const maxVal = calcSummary(dataIndex, 'max');
|
||||
const minVal = calcSummary(dataIndex, 'min');
|
||||
const avgVal = calcSummary(dataIndex, 'avg');
|
||||
rows[0].cells[dataIndex] = {
|
||||
value: formatSummaryValue(dataIndex, maxVal),
|
||||
time: maxVal !== null ? getSummaryTime(dataIndex, 'max') : ''
|
||||
};
|
||||
rows[1].cells[dataIndex] = {
|
||||
value: formatSummaryValue(dataIndex, minVal),
|
||||
time: minVal !== null ? getSummaryTime(dataIndex, 'min') : ''
|
||||
};
|
||||
rows[2].cells[dataIndex] = {
|
||||
value: formatSummaryValue(dataIndex, avgVal),
|
||||
time: ''
|
||||
};
|
||||
});
|
||||
return rows;
|
||||
});
|
||||
|
||||
// ==================== 数据请求 ====================
|
||||
// 数据请求(查询参数取 modelStore.selectedAnchorPoint)
|
||||
const fetchData = async () => {
|
||||
const stcd = modelStore.selectedAnchorPoint?.stcd;
|
||||
if (!dateRange.value || !stcd) return;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const filterParams = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: stcd
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[1].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
]
|
||||
},
|
||||
sort: [{ field: 'tm', dir: 'asc' }]
|
||||
};
|
||||
|
||||
const res = await getMonitorDataWaterTemp(filterParams);
|
||||
const rawData = res?.data?.data || res?.data?.records || [];
|
||||
|
||||
chartData.value = rawData;
|
||||
tableData.value = [...rawData].reverse();
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
chartData.value = [];
|
||||
tableData.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 时间选择器 change 触发查询
|
||||
const handleSearch = () => fetchData();
|
||||
watch(
|
||||
() => uiStore.searchDrawerOpen,
|
||||
newVal => {
|
||||
if (!newVal) {
|
||||
dateRange.value = initDateRange();
|
||||
}
|
||||
}
|
||||
);
|
||||
// 选中点位变化时刷新;初始已有点位时立即加载
|
||||
watch(
|
||||
() => modelStore.selectedAnchorPoint?.stcd,
|
||||
(newStcd, oldStcd) => {
|
||||
if (!newStcd) return;
|
||||
if (newStcd !== oldStcd) fetchData();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 全局时间范围变化时同步本地日期选择器(默认参数联动)
|
||||
watch(
|
||||
() => modelStore.filter.rangeTm,
|
||||
newRange => {
|
||||
if (Array.isArray(newRange) && newRange.length > 0) {
|
||||
dateRange.value = [dayjs(newRange[0]), dayjs(newRange[1])];
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.monitor-chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.monitor-table {
|
||||
margin-top: 12px;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
.table-container {
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
:deep(.summary-tip-icon) {
|
||||
color: rgb(53, 169, 255);
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
</style>
|
||||
@ -1,17 +1,48 @@
|
||||
<template>
|
||||
<SidePanelItem title="电站运行过程线" :shrink="false">
|
||||
<template #title-right-content>
|
||||
<a-range-picker
|
||||
class="w-[220px]"
|
||||
v-model:value="dateRange"
|
||||
format="YYYY-MM-DD"
|
||||
:show-time="showTimeConfig"
|
||||
:allowClear="false"
|
||||
:presets="DateSetting.RangeButton.month1"
|
||||
:disabled-date="disabledDate"
|
||||
size="small"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
<a-spin :spinning="isLoading" tip="加载中...">
|
||||
<div class="monitor-chart">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
<a-empty
|
||||
v-if="!chartData || chartData.length === 0"
|
||||
description="暂无数据"
|
||||
class="chart-empty"
|
||||
/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</SidePanelItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onBeforeUnmount, onMounted, nextTick } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import * as echarts from 'echarts';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import { getMonitorData } from '@/api/mapModal';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||||
import { DateSetting } from '@/utils/enumeration';
|
||||
|
||||
const props = defineProps({
|
||||
data: { type: Array as () => any[], default: () => [] },
|
||||
isActive: { type: Boolean, default: false }
|
||||
});
|
||||
const uiStore = useUiStore();
|
||||
const modelStore = useModelStore();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const chartData = ref<any[]>([]);
|
||||
|
||||
// ==================== 图表 ====================
|
||||
const chartRef = ref<HTMLElement>();
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
@ -21,7 +52,7 @@ const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
if (chartInstance) chartInstance.dispose();
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
updateChart(props.data);
|
||||
updateChart(chartData.value);
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
@ -283,11 +314,16 @@ const updateChart = (data: any[]) => {
|
||||
chartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
// 激活时初始化图表并处理自适应,数据变化时动态刷新
|
||||
watch(
|
||||
() => props.isActive,
|
||||
active => {
|
||||
if (!active) return;
|
||||
// 数据变化时动态刷新图表
|
||||
watch(chartData, newData => {
|
||||
updateChart(newData);
|
||||
});
|
||||
|
||||
const handleResize = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
setTimeout(() => {
|
||||
@ -300,22 +336,6 @@ watch(
|
||||
resizeObserver.observe(chartRef.value);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
newData => {
|
||||
updateChart(newData);
|
||||
}
|
||||
);
|
||||
|
||||
const handleResize = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
@ -330,11 +350,116 @@ onBeforeUnmount(() => {
|
||||
resizeObserver = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 时间选择器配置(默认参数与 MonitorInfo 保持一致)
|
||||
const showTimeConfig = {
|
||||
format: 'HH:mm',
|
||||
hourStep: 1,
|
||||
minuteStep: 5,
|
||||
secondStep: 60
|
||||
};
|
||||
const disabledDate = (current: Dayjs) =>
|
||||
current && current.isAfter(dayjs(), 'day');
|
||||
|
||||
const initDateRange = (): [Dayjs, Dayjs] => {
|
||||
return [dayjs().subtract(1, 'month'), dayjs()];
|
||||
};
|
||||
const dateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
|
||||
|
||||
// 数据请求(查询参数取 modelStore.selectedAnchorPoint)
|
||||
const fetchData = async () => {
|
||||
const stcd = modelStore.selectedAnchorPoint?.stcd;
|
||||
if (!dateRange.value || !stcd) return;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const filterParams = {
|
||||
filter: {
|
||||
logic: 'and',
|
||||
filters: [
|
||||
{
|
||||
field: 'stcd',
|
||||
operator: 'eq',
|
||||
dataType: 'string',
|
||||
value: stcd
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'gte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[0].format('YYYY-MM-DD HH:mm:ss')
|
||||
},
|
||||
{
|
||||
field: 'tm',
|
||||
operator: 'lte',
|
||||
dataType: 'date',
|
||||
value: dateRange.value[1].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
]
|
||||
},
|
||||
sort: [{ field: 'tm', dir: 'asc' }]
|
||||
};
|
||||
|
||||
const res = await getMonitorData(filterParams);
|
||||
const rawData = res?.data?.data || res?.data?.records || [];
|
||||
|
||||
chartData.value = rawData;
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
chartData.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 时间选择器 change 触发查询
|
||||
const handleSearch = () => fetchData();
|
||||
watch(
|
||||
() => uiStore.searchDrawerOpen,
|
||||
newVal => {
|
||||
if (!newVal) {
|
||||
dateRange.value = initDateRange();
|
||||
}
|
||||
}
|
||||
);
|
||||
// 选中点位变化时刷新;初始已有点位时立即加载
|
||||
watch(
|
||||
() => modelStore.selectedAnchorPoint?.stcd,
|
||||
(newStcd, oldStcd) => {
|
||||
if (!newStcd) return;
|
||||
if (newStcd !== oldStcd) fetchData();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 全局时间范围变化时同步本地日期选择器(默认参数联动)
|
||||
watch(
|
||||
() => modelStore.filter.rangeTm,
|
||||
newRange => {
|
||||
if (Array.isArray(newRange) && newRange.length > 0) {
|
||||
dateRange.value = [dayjs(newRange[0]), dayjs(newRange[1])];
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-container {
|
||||
.monitor-chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 电站运行过程线 -->
|
||||
<monitorInfoMod v-if="sttpValue == 'ENG'" />
|
||||
<!-- 水温监控 -->
|
||||
<WaterTemperature v-if="sttpValue == 'WTRV'" />
|
||||
<!-- 垂向水温监控 -->
|
||||
<VerticalWaterTemperature v-if="sttpValue == 'WTVT'" />
|
||||
<!-- 水质监控 -->
|
||||
<WaterQuality v-if="sttpValue == 'WQ'" />
|
||||
<!-- 过鱼总量监控 -->
|
||||
<GYZLLB v-if="sttpValue == 'FP'" />
|
||||
<!-- 栖息地 FH -->
|
||||
<QXDInfo v-if="sttpValue == 'FH'" />
|
||||
<!-- 流量站 -->
|
||||
<FlowMeasure v-if="sttpValue == 'ZQ'" />
|
||||
<!-- 植物园 VP -->
|
||||
<Plant v-if="sttpValue == 'VP'" />
|
||||
<!-- 动物园 VA -->
|
||||
<Animal v-if="sttpValue == 'VA'" />
|
||||
<!-- 生态流量泄放设施 EQ -->
|
||||
<FlowDischarge v-if="sttpValue == 'EQ'" />
|
||||
<!-- 鱼类增殖站 -->
|
||||
<!-- 生态调查 -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useModelStore } from '@/store/modules/model';
|
||||
import WaterTemperature from '@/modules/rightSearchDrawer/monitoringTable/components/WaterTemperature.vue';
|
||||
import VerticalWaterTemperature from '@/modules/rightSearchDrawer/monitoringTable/components/VerticalWaterTemperature.vue';
|
||||
import WaterQuality from '@/modules/rightSearchDrawer/monitoringTable/components/WaterQuality.vue';
|
||||
import monitorInfoMod from '@/modules/rightSearchDrawer/monitoringTable/components/monitorInfo.vue';
|
||||
import GYZLLB from '@/modules/rightSearchDrawer/monitoringTable/components/GYZLLB.vue';
|
||||
import QXDInfo from '@/modules/rightSearchDrawer/monitoringTable/components/QXDInfo.vue';
|
||||
import FlowMeasure from '@/modules/rightSearchDrawer/monitoringTable/components/FlowMeasure.vue';
|
||||
import Plant from '@/modules/rightSearchDrawer/monitoringTable/components/Plant.vue';
|
||||
import Animal from '@/modules/rightSearchDrawer/monitoringTable/components/Animal.vue';
|
||||
import FlowDischarge from '@/modules/rightSearchDrawer/monitoringTable/components/FlowDischarge.vue';
|
||||
|
||||
const modelStore = useModelStore();
|
||||
const sttpValue = ref('');
|
||||
|
||||
// 选中点位变化时刷新;初始已有点位时立即加载
|
||||
watch(
|
||||
() => modelStore.selectedAnchorPoint,
|
||||
newVal => {
|
||||
console.log(newVal);
|
||||
sttpValue.value = newVal?.sttpCode || '';
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@ -816,6 +816,10 @@ export const useMapStore = defineStore('map', () => {
|
||||
|
||||
mapViewStore.setCheckedLayerKeys(checkKeys);
|
||||
|
||||
// 备注:勾选态变化后按最新勾选集合重建合并锚点数据,保证搜索下拉框等消费方
|
||||
// 始终拿到当前勾选图层的数据,避免旧重建结果只包含部分图层导致下拉数据缺失。
|
||||
syncPointDataForFilter(checkKeys);
|
||||
|
||||
// 收集变化的图层 key(新增选中 / 取消选中)
|
||||
const newlyChecked: string[] = [];
|
||||
const newlyUnchecked: string[] = [];
|
||||
|
||||
@ -12,6 +12,8 @@ export const useUiStore = defineStore('ui', () => {
|
||||
const skipRoamCameraRestore = ref(false);
|
||||
// 搜索抽屉状态
|
||||
const searchDrawerOpen = ref(false);
|
||||
// 全局兜底抽屉状态(AppMain 通用抽屉,给未自带抽屉的页面用)
|
||||
const globalDrawerOpen = ref(false);
|
||||
|
||||
// 切换抽屉状态
|
||||
const toggleDrawer = () => {
|
||||
@ -23,10 +25,12 @@ export const useUiStore = defineStore('ui', () => {
|
||||
};
|
||||
// 设置搜索抽屉状态
|
||||
const setSearchDrawerOpen = (open: boolean) => {
|
||||
console.log(open);
|
||||
|
||||
searchDrawerOpen.value = open;
|
||||
};
|
||||
// 设置全局兜底抽屉状态
|
||||
const setGlobalDrawerOpen = (open: boolean) => {
|
||||
globalDrawerOpen.value = open;
|
||||
};
|
||||
|
||||
const markMapSwitchCompleted = () => {
|
||||
mapSwitchCompletedTick.value += 1;
|
||||
@ -40,6 +44,8 @@ export const useUiStore = defineStore('ui', () => {
|
||||
setDrawerOpen,
|
||||
setSearchDrawerOpen,
|
||||
searchDrawerOpen,
|
||||
globalDrawerOpen,
|
||||
setGlobalDrawerOpen,
|
||||
mapType,
|
||||
mapSwitchCompletedTick,
|
||||
markMapSwitchCompleted
|
||||
|
||||
@ -126,3 +126,6 @@ svg {
|
||||
.ant-dropdown {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
.ant-table-pagination.ant-pagination {
|
||||
padding: 0 4px !important;
|
||||
}
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import basicInfoMod from '@/modules/rightSearchDrawer/basicInfo/index.vue';
|
||||
import JidiSelectorMod from '@/modules/jidiSelectorMod.vue';
|
||||
import RightDrawer from '@/components/RightDrawer/index.vue';
|
||||
import GuoYuSheShiJieShao from '@/modules/guoyusheshijieshao/index.vue';
|
||||
@ -10,7 +8,6 @@ import GYZLLB from '@/modules/GYZLLB/index.vue';
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import { ref, watch } from 'vue';
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
const uiStore = useUiStore();
|
||||
const baseid = ref('');
|
||||
watch(
|
||||
() => JidiSelectEventStore.selectedItem,
|
||||
@ -28,15 +25,10 @@ watch(
|
||||
</div>
|
||||
<div class="rightContent">
|
||||
<RightDrawer>
|
||||
<div v-show="uiStore.searchDrawerOpen">
|
||||
<basicInfoMod />
|
||||
</div>
|
||||
<div v-show="!uiStore.searchDrawerOpen">
|
||||
<GuoYuSheShiJieShao v-if="baseid != 'all'" />
|
||||
<GuoYuSheShiJianSheQingKuang v-if="baseid == 'all'" />
|
||||
<GuoYuJianCeTJ />
|
||||
<GYZLLB title="过鱼总量" />
|
||||
</div>
|
||||
</RightDrawer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import JidiSelectorMod from '@/modules/jidiSelectorMod.vue';
|
||||
import RightDrawer from '@/components/RightDrawer/index.vue';
|
||||
import HuanbaoMod from '@/modules/huanbaoMod/index.vue'; // 环保设施建设情况
|
||||
import Dianxingcuoshijieshao from '@/modules/dianxingcuoshijieshao/index.vue'; // 环保设施建设情况
|
||||
import basicInfoMod from '@/modules/rightSearchDrawer/basicInfo/index.vue';
|
||||
import monitorInfoMod from '@/modules/rightSearchDrawer/monitorInfo/index.vue';
|
||||
const uiStore = useUiStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -16,14 +12,8 @@ const uiStore = useUiStore();
|
||||
</div>
|
||||
<div class="rightContent">
|
||||
<RightDrawer>
|
||||
<div v-show="uiStore.searchDrawerOpen">
|
||||
<basicInfoMod />
|
||||
<!-- <monitorInfoMod /> -->
|
||||
</div>
|
||||
<div v-show="!uiStore.searchDrawerOpen">
|
||||
<HuanbaoMod />
|
||||
<Dianxingcuoshijieshao />
|
||||
</div>
|
||||
</RightDrawer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import basicInfoMod from '@/modules/rightSearchDrawer/basicInfo/index.vue';
|
||||
import JidiSelectorMod from '@/modules/jidiSelectorMod.vue';
|
||||
import RightDrawer from '@/components/RightDrawer/index.vue';
|
||||
import HuanbaozdjcgzkzQK from '@/modules/huanbaozdjcgzkzQK/index.vue';
|
||||
@ -9,7 +7,6 @@ import SZYCBH from '@/modules/waterQuality/index.vue';
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
const wbsCode = ref('');
|
||||
const uiStore = useUiStore();
|
||||
watch(
|
||||
() => JidiSelectEventStore.selectedItem,
|
||||
newVal => {
|
||||
@ -26,13 +23,8 @@ watch(
|
||||
</div>
|
||||
<div class="rightContent">
|
||||
<RightDrawer>
|
||||
<div v-show="uiStore.searchDrawerOpen">
|
||||
<basicInfoMod />
|
||||
</div>
|
||||
<div v-show="!uiStore.searchDrawerOpen">
|
||||
<HuanbaozdjcgzkzQK />
|
||||
<SZYCBH v-if="wbsCode != 'all'" />
|
||||
</div>
|
||||
</RightDrawer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import JidiSelectorMod from '@/modules/jidiSelectorMod.vue';
|
||||
import RightDrawer from '@/components/RightDrawer/index.vue';
|
||||
import jidiInfoMod from '@/modules/jidiInfoMod/index.vue';
|
||||
import shuidianhuangjingjieruMod from '@/modules/shuidianhuangjingjieruMod/index.vue';
|
||||
import basicInfoMod from '@/modules/rightSearchDrawer/basicInfo/index.vue';
|
||||
import monitorInfoMod from '@/modules/rightSearchDrawer/monitorInfo/index.vue';
|
||||
const uiStore = useUiStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -16,14 +12,8 @@ const uiStore = useUiStore();
|
||||
</div>
|
||||
<div class="rightContent">
|
||||
<RightDrawer>
|
||||
<div v-show="uiStore.searchDrawerOpen">
|
||||
<basicInfoMod />
|
||||
<monitorInfoMod />
|
||||
</div>
|
||||
<div v-show="!uiStore.searchDrawerOpen">
|
||||
<jidiInfoMod />
|
||||
<shuidianhuangjingjieruMod />
|
||||
</div>
|
||||
</RightDrawer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import basicInfoMod from '@/modules/rightSearchDrawer/basicInfo/index.vue';
|
||||
import JidiSelectorMod from '@/modules/jidiSelectorMod.vue';
|
||||
import RightDrawer from '@/components/RightDrawer/index.vue';
|
||||
import QiXiDiBaoHuGongZuoKaiZhan from '@/modules/qixidibaohugongzuokaizhanQK/index.vue';
|
||||
import QixidijchuXx from '@/modules/qixidijchuXx/index.vue';
|
||||
import QiXiDiShuiWenBianHua from '@/modules/qixidishuiwenbianhua/index.vue';
|
||||
import QiXiDiLiuLiangBianHua from '@/modules/qixidiliuliangbianhua/index.vue';
|
||||
const uiStore = useUiStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -17,15 +14,10 @@ const uiStore = useUiStore();
|
||||
</div>
|
||||
<div class="rightContent">
|
||||
<RightDrawer>
|
||||
<div v-show="uiStore.searchDrawerOpen">
|
||||
<basicInfoMod />
|
||||
</div>
|
||||
<div v-show="!uiStore.searchDrawerOpen">
|
||||
<QiXiDiBaoHuGongZuoKaiZhan />
|
||||
<QiXiDiShuiWenBianHua />
|
||||
<QiXiDiLiuLiangBianHua />
|
||||
<QixidijchuXx />
|
||||
</div>
|
||||
</RightDrawer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import basicInfoMod from '@/modules/rightSearchDrawer/basicInfo/index.vue';
|
||||
import monitoringTableMod from '@/modules/rightSearchDrawer/monitoringTable/index.vue';
|
||||
import JidiSelectorMod from '@/modules/jidiSelectorMod.vue';
|
||||
import RightDrawer from '@/components/RightDrawer/index.vue';
|
||||
import DiWenSJHSSLXZCJJRQK from '@/modules/diwenshuijianhuansheshileixingzuchengjijieruqingkuang/index.vue';
|
||||
@ -17,6 +18,7 @@ const uiStore = useUiStore();
|
||||
<RightDrawer>
|
||||
<div v-show="uiStore.searchDrawerOpen">
|
||||
<basicInfoMod />
|
||||
<monitoringTableMod />
|
||||
</div>
|
||||
<div v-show="!uiStore.searchDrawerOpen">
|
||||
<SheShiLeiXingJieShao />
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import basicInfoMod from '@/modules/rightSearchDrawer/basicInfo/index.vue';
|
||||
import JidiSelectorMod from '@/modules/jidiSelectorMod.vue';
|
||||
import RightDrawer from '@/components/RightDrawer/index.vue';
|
||||
import MonthlyAvgWaterTemCompareHistory from '@/modules/monthlyAvgWaterTemCompareHistory/index.vue'; // 月平均水温历史对比
|
||||
import ShuiWenNianNeiFenBu from '@/modules/shuiWenNianNeiFenBu/index.vue'; // 水温年内分布
|
||||
const uiStore = useUiStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -15,13 +12,8 @@ const uiStore = useUiStore();
|
||||
</div>
|
||||
<div class="rightContent">
|
||||
<RightDrawer>
|
||||
<div v-show="uiStore.searchDrawerOpen">
|
||||
<basicInfoMod />
|
||||
</div>
|
||||
<div v-show="!uiStore.searchDrawerOpen">
|
||||
<MonthlyAvgWaterTemCompareHistory />
|
||||
<ShuiWenNianNeiFenBu />
|
||||
</div>
|
||||
</RightDrawer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import basicInfoMod from '@/modules/rightSearchDrawer/basicInfo/index.vue';
|
||||
import JidiSelectorMod from '@/modules/jidiSelectorMod.vue';
|
||||
import RightDrawer from '@/components/RightDrawer/index.vue';
|
||||
import ChuixiangshuiwenChangeMod from '@/modules/chuixiangshuiwenChangeMod/index.vue'; // 垂向水温变化
|
||||
import ChurukushuiwenMod from '@/modules/churukushuiwenMod/index.vue'; // 出入库水温变化
|
||||
import YanchengshuiwenChangeMod from '@/modules/yanchengshuiwenChangeMod/index.vue'; // 沿程水温变化
|
||||
import ShuiwenJCGZKZQK from '@/modules/shuiwenjiancegongzuokaizhangqingkuang/index.vue';
|
||||
const uiStore = useUiStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -17,15 +14,10 @@ const uiStore = useUiStore();
|
||||
</div>
|
||||
<div class="rightContent">
|
||||
<RightDrawer>
|
||||
<div v-show="uiStore.searchDrawerOpen">
|
||||
<basicInfoMod />
|
||||
</div>
|
||||
<div v-show="!uiStore.searchDrawerOpen">
|
||||
<ShuiwenJCGZKZQK />
|
||||
<YanchengshuiwenChangeMod />
|
||||
<ChuixiangshuiwenChangeMod />
|
||||
<ChurukushuiwenMod />
|
||||
</div>
|
||||
</RightDrawer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import basicInfoMod from '@/modules/rightSearchDrawer/basicInfo/index.vue';
|
||||
import JidiSelectorMod from '@/modules/jidiSelectorMod.vue';
|
||||
import RightDrawer from '@/components/RightDrawer/index.vue';
|
||||
import ShuiZhiJianCeGongZuoQingKuang from '@/modules/shuizhijiancegongzuoQK/index.vue';
|
||||
@ -9,7 +7,6 @@ import EnvironmentalQuality from '@/modules/EnvironmentalQuality/index.vue'; //
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
import SZYCBH from '@/modules/waterQuality/index.vue';
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
const uiStore = useUiStore();
|
||||
const wbsCode = ref('');
|
||||
watch(
|
||||
() => JidiSelectEventStore.selectedItem,
|
||||
@ -29,14 +26,9 @@ watch(
|
||||
</div>
|
||||
<div class="rightContent">
|
||||
<RightDrawer>
|
||||
<div v-show="uiStore.searchDrawerOpen">
|
||||
<basicInfoMod />
|
||||
</div>
|
||||
<div v-show="!uiStore.searchDrawerOpen">
|
||||
<ShuiZhiJianCeGongZuoQingKuang />
|
||||
<EnvironmentalQuality />
|
||||
<SZYCBH v-if="wbsCode != 'all'"></SZYCBH>
|
||||
</div>
|
||||
</RightDrawer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
import { ref, watch } from 'vue';
|
||||
import JidiSelectorMod from '@/modules/jidiSelectorMod.vue';
|
||||
import RightDrawer from '@/components/RightDrawer/index.vue';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import basicInfoMod from '@/modules/rightSearchDrawer/basicInfo/index.vue';
|
||||
|
||||
import ZengZhiJiHuaWanChengQingKuang from '@/modules/zengZhiJiHuaWanChengQingKuang/index.vue';
|
||||
import ZengZhiZhanJieShaoMod from '@/modules/zengZhiZhanJieShaoMod/index.vue';
|
||||
@ -12,7 +10,6 @@ import ZZZYXSJTJ from '@/modules/zengzhizhanyunxingsjtj/index.vue';
|
||||
import FLZLLB from '@/modules/GYZLLB/index.vue';
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
const uiStore = useUiStore();
|
||||
const wbsCode = ref('all');
|
||||
watch(
|
||||
() => JidiSelectEventStore.selectedItem,
|
||||
@ -35,16 +32,11 @@ watch(
|
||||
</div>
|
||||
<div class="rightContent">
|
||||
<RightDrawer>
|
||||
<div v-show="uiStore.searchDrawerOpen">
|
||||
<basicInfoMod />
|
||||
</div>
|
||||
<div v-show="!uiStore.searchDrawerOpen">
|
||||
<ZZZJSYXQKTT />
|
||||
<ZengZhiZhanJieShaoMod v-if="wbsCode != 'all'" />
|
||||
<ZengZhiJiHuaWanChengQingKuang v-if="wbsCode != 'all'" />
|
||||
<FLZLLB title="放流总量" />
|
||||
<ZZZYXSJTJ />
|
||||
</div>
|
||||
</RightDrawer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { useUiStore } from '@/store/modules/ui';
|
||||
import basicInfoMod from '@/modules/rightSearchDrawer/basicInfo/index.vue';
|
||||
import JidiSelectorMod from '@/modules/jidiSelectorMod.vue';
|
||||
import RightDrawer from '@/components/RightDrawer/index.vue';
|
||||
import ZhiWuYuanJianSheJiJieRuQingKuangBar from '@/modules/ZhenXiZhiWuYuanMod/ZhiWuYuanJianSheJiJieRuQingKuangBar/index.vue'; // 植物园建设及接入情况
|
||||
@ -9,7 +7,6 @@ import Dwjzqk from '@/modules/ZhenXiZhiWuYuanMod/Dwjzqk/index.vue';
|
||||
import JZZQKJS from '@/modules/DongWuJiuZhuZhan/JiuZhuZhanQingKuangJieShao.vue'; // 救助站情况介绍
|
||||
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
||||
const JidiSelectEventStore = useJidiSelectEventStore();
|
||||
const uiStore = useUiStore();
|
||||
const wbsCode = ref('all');
|
||||
watch(
|
||||
() => JidiSelectEventStore.selectedItem,
|
||||
@ -32,14 +29,9 @@ watch(
|
||||
</div>
|
||||
<div class="rightContent">
|
||||
<RightDrawer>
|
||||
<div v-show="uiStore.searchDrawerOpen">
|
||||
<basicInfoMod />
|
||||
</div>
|
||||
<div v-show="!uiStore.searchDrawerOpen">
|
||||
<JZZQKJS v-if="wbsCode != 'all'" />
|
||||
<ZhiWuYuanJianSheJiJieRuQingKuangBar v-if="wbsCode == 'all'" />
|
||||
<Dwjzqk />
|
||||
</div>
|
||||
</RightDrawer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user