添加下拉框选择,右侧切换功能

This commit is contained in:
扈兆增 2026-08-11 16:24:41 +08:00
parent 69e46e9914
commit 800a2665a0
40 changed files with 5538 additions and 551 deletions

View File

@ -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;
}

View File

@ -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();

View 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>

View File

@ -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: ''

View File

@ -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 {

View File

@ -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`;

View File

@ -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());
// tabqxd
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 = '';

View File

@ -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,

View File

@ -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) => {

View File

@ -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>

View File

@ -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;

View File

@ -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' // dwtpicon使""
}))
}));
} 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 {

View File

@ -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',

View File

@ -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;
}
};

View File

@ -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>

View File

@ -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>

View File

@ -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> (/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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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[]>([]); // series2
// showControlunit
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; // 01
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;
// 10051406160
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/minmax/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; // legendunit
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' }
}
});
// seriesyAxisIndex
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)
]);
// datanull
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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -816,6 +816,10 @@ export const useMapStore = defineStore('map', () => {
mapViewStore.setCheckedLayerKeys(checkKeys);
// 备注:勾选态变化后按最新勾选集合重建合并锚点数据,保证搜索下拉框等消费方
// 始终拿到当前勾选图层的数据,避免旧重建结果只包含部分图层导致下拉数据缺失。
syncPointDataForFilter(checkKeys);
// 收集变化的图层 key新增选中 / 取消选中)
const newlyChecked: string[] = [];
const newlyUnchecked: string[] = [];

View File

@ -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

View File

@ -126,3 +126,6 @@ svg {
.ant-dropdown {
z-index: 9999 !important;
}
.ant-table-pagination.ant-pagination {
padding: 0 4px !important;
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 />

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>