添加右侧展开,bug修改,地图优化

This commit is contained in:
扈兆增 2026-09-02 16:19:28 +08:00
parent d4882f469c
commit 29aaeefc60
65 changed files with 13217 additions and 2592 deletions

View File

@ -3,7 +3,7 @@
"version": "1.2.0",
"scripts": {
"dev": "vite serve --mode development",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode production",
"build": "vite build --mode production",
"build:mvn": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode production",
"serve": "vite preview",
"lint": "eslint src/**/*.{ts,js,vue} --fix",

View File

@ -29,15 +29,10 @@
<!-- Tabs 区域 - 等待加载完成后显示 -->
<a-tabs v-else :active-key="currentActiveKey" @change="onTabChange">
<a-tab-pane v-for="tab in tabsConfig" :key="tab.key" :tab="tab.name">
</a-tab-pane>
<a-tab-pane v-for="tab in tabsConfig" :key="tab.key" :tab="tab.name"> </a-tab-pane>
<template #rightExtra v-if="anchor.sttp == 'ENG' || anchor.sttp == 'eng'">
<a-tooltip :title="!hasPowerStatData ? '该电站无专题配置' : ''">
<a-button
type="primary"
:disabled="!hasPowerStatData"
@click="gotoEngTopic"
>
<a-button type="primary" :disabled="!hasPowerStatData" @click="gotoEngTopic">
<i class="icon iconfont icon-topic mr-[5px]"></i>
电站专题
</a-button></a-tooltip
@ -90,11 +85,7 @@
v-show="currentActiveKey === 'WaterTemperature'"
:is-active="currentActiveKey === 'WaterTemperature'"
:code="getTabCode('WaterTemperature')"
:stcd="
getTabCode('WaterTemperature') == 'qxd.tabs.jcsj'
? qxdSelectedStcd
: ''
"
:stcd="getTabCode('WaterTemperature') == 'qxd.tabs.jcsj' ? qxdSelectedStcd : ''"
:wt-config="modelStore.wtConfig"
/>
<!-- 监测数据 - 垂向水温 -->
@ -108,11 +99,7 @@
v-show="currentActiveKey === 'WaterQuality'"
:is-active="currentActiveKey === 'WaterQuality'"
:code="getTabCode('WaterQuality')"
:stcd="
getTabCode('WaterQuality') == 'qxd.tabs.szjc'
? qxdSelectedStcdWq
: ''
"
:stcd="getTabCode('WaterQuality') == 'qxd.tabs.szjc' ? qxdSelectedStcdWq : ''"
:wq-show="modelStore.wqShow"
/>
<!-- 监测数据 - 水电站告警情况 -->
@ -132,11 +119,7 @@
:is-active="currentActiveKey === 'FlowMeasure'"
:url="getTabUrl('FlowMeasure')"
:code="getTabCode('FlowMeasure')"
:stcd="
getTabCode('FlowMeasure') == 'qxd.tabs.lcjs'
? qxdSelectedStcdZq
: ''
"
:stcd="getTabCode('FlowMeasure') == 'qxd.tabs.lcjs' ? qxdSelectedStcdZq : ''"
/>
<!-- 在线监测数据 -->
<FishFacilityMonitorData
@ -247,46 +230,46 @@
</template>
<script lang="ts" setup>
import { ref, watch, computed, toRef } from 'vue';
import { ref, watch, computed, toRef } from "vue";
// Tab
import BasicInfo from './components/BasicInfo.vue'; //
import VideoInfo from './components/videoInfo.vue'; //
import NoLiveVideo from './components/NoLiveVideo.vue'; //
import PanoramaInfo from './components/PanoramaInfo.vue'; //
import MonitorInfo from './components/MonitorInfo.vue'; // 线
import WaterTemperature from './components/WaterTemperature.vue'; //
import VerticalWaterTemperature from './components/VerticalWaterTemperature.vue'; //
import WaterQuality from './components/WaterQuality.vue'; //
import AIYXSB from './components/AIYXSB/index.vue'; // AI
import EarlyWarningAlert from './components/EarlyWarningAlert.vue'; //
import FlowMeasure from './components/FlowMeasure.vue'; //
import FishFacilityMonitorData from './components/FishFacilityMonitorData.vue'; // 线
import FishFacilityRunState from './components/FishFacilityRunState.vue'; //
import FlowDischarge from './components/FlowDischarge.vue'; // -
import FishFacilityBuildState from './components/FishFacilityBuildState.vue'; //
import BasicInfo from "./components/BasicInfo.vue"; //
import VideoInfo from "./components/videoInfo.vue"; //
import NoLiveVideo from "./components/NoLiveVideo.vue"; //
import PanoramaInfo from "./components/PanoramaInfo.vue"; //
import MonitorInfo from "./components/MonitorInfo.vue"; // 线
import WaterTemperature from "./components/WaterTemperature.vue"; //
import VerticalWaterTemperature from "./components/VerticalWaterTemperature.vue"; //
import WaterQuality from "./components/WaterQuality.vue"; //
import AIYXSB from "./components/AIYXSB/index.vue"; // AI
import EarlyWarningAlert from "./components/EarlyWarningAlert.vue"; //
import FlowMeasure from "./components/FlowMeasure.vue"; //
import FishFacilityMonitorData from "./components/FishFacilityMonitorData.vue"; // 线
import FishFacilityRunState from "./components/FishFacilityRunState.vue"; //
import FlowDischarge from "./components/FlowDischarge.vue"; // -
import FishFacilityBuildState from "./components/FishFacilityBuildState.vue"; //
//
import NormalOperationData from './components/NormalOperationData/index.vue'; //
import ProcessDiagram from './components/ProcessDiagram.vue'; //
import NormalAddedSituation from './components/NormalAddedSituation.vue'; //
import NormalResearchSituation from './components/NormalResearchSituation.vue'; //
import NormalOperationData from "./components/NormalOperationData/index.vue"; //
import ProcessDiagram from "./components/ProcessDiagram.vue"; //
import NormalAddedSituation from "./components/NormalAddedSituation.vue"; //
import NormalResearchSituation from "./components/NormalResearchSituation.vue"; //
//
import NormalDataMonitoring2 from './components/NormalDataMonitoring2.vue'; //
import NormalDataMonitoring2 from "./components/NormalDataMonitoring2.vue"; //
// -
import liveFootage from './components/liveFootage.vue'; //
import onSitePhoto from './components/onSitePhoto.vue'; //
import liveFootage from "./components/liveFootage.vue"; //
import onSitePhoto from "./components/onSitePhoto.vue"; //
///
import Normal from './components/Normal.vue'; //
import Normal from "./components/Normal.vue"; //
//
import Ecology from './components/Ecology.vue'; //
import Ecology from "./components/Ecology.vue"; //
//
import DZGJ from './components/DZGJ.vue'; //
import DZGJ from "./components/DZGJ.vue"; //
import FishZHFX from './components/FishZHFX.vue'; //
import EcologicalFlow from './components/EcologicalFlow.vue'; //
import Attachment from './components/Attachment.vue'; // /
import WaterTemperatureRep from './components/WaterTemperatureRep.vue'; //
import { useModelStore } from '@/store/modules/model';
import { handleTabs } from './setting.config';
import FishZHFX from "./components/FishZHFX.vue"; //
import EcologicalFlow from "./components/EcologicalFlow.vue"; //
import Attachment from "./components/Attachment.vue"; // /
import WaterTemperatureRep from "./components/WaterTemperatureRep.vue"; //
import { useModelStore } from "@/store/modules/model";
import { handleTabs } from "./setting.config";
import {
getMonitorDataWt,
getMonitorDataWq,
@ -298,9 +281,9 @@ import {
getVideoList,
getMonitorDataWaterTempPowerStation,
getMonitorDataWaterTempPowerStation2,
checkPowerStationTopic
} from '@/api/mapModal';
import { useDraggable } from '@/utils/drag';
checkPowerStationTopic,
} from "@/api/mapModal";
import { useDraggable } from "@/utils/drag";
// import { useRouter } from 'vue-router';
const modelStore = useModelStore();
@ -312,7 +295,7 @@ const isLoading = ref(true);
//
const hasPowerStatData = ref(false);
// URL hbb
const hbbSearchParams = '?menu=dianZhanZhuanTi&page=dianZhanZhuanTi';
const hbbSearchParams = "?menu=dianZhanZhuanTi&page=dianZhanZhuanTi";
//
const isPowerStatPage = computed(() => {
return window.location.search === hbbSearchParams;
@ -331,26 +314,26 @@ const checkPowerStationTopicData = async (stcd: string) => {
try {
const res = await checkPowerStationTopic({
filter: {
logic: 'and',
logic: "and",
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
field: "stcd",
operator: "eq",
dataType: "string",
value: stcd,
},
{
field: 'bclData',
operator: 'contains',
dataType: 'string',
value: 'image/png'
}
]
}
field: "bclData",
operator: "contains",
dataType: "string",
value: "image/png",
},
],
},
});
hasPowerStatData.value = res?.data?.data && res?.data?.data?.length > 0;
} catch (error) {
console.error('查询电站专题配置失败:', error);
console.error("查询电站专题配置失败:", error);
hasPowerStatData.value = false;
}
};
@ -377,14 +360,14 @@ const gotoEngTopic = () => {
// 使 Vue Router
// router.push(url);
*/
console.log('电站专题跳转(暂未实现):', modelStore.params.stcd);
console.log("电站专题跳转(暂未实现):", modelStore.params.stcd);
};
// React
const app = 'hbb'; // React Session.getAppCode()
const env = 'hbb'; // React window.__env__
const isLCJ = env?.startsWith('lcj');
const sttpList = ['ENG', 'WT', 'WQ', 'FP', 'FB', 'VP', 'DW', 'EQ', 'VA'];
const app = "hbb"; // React Session.getAppCode()
const env = "hbb"; // React window.__env__
const isLCJ = env?.startsWith("lcj");
const sttpList = ["ENG", "WT", "WQ", "FP", "FB", "VP", "DW", "EQ", "VA"];
// ==================== Tab ====================
// 8: (React: isLCJ && sttpCode === 'WE')
@ -395,20 +378,18 @@ const checkFishTab = async (stcd: string) => {
if (data && data.length > 0) {
modelStore.fishTabInfo = data[0];
// hasWt/hasWq/hasFv FlowMeasure Tab
const flowTab = tabsConfig.value.find(
(item: any) => item.key === 'FlowMeasure'
);
const flowTab = tabsConfig.value.find((item: any) => item.key === "FlowMeasure");
if (flowTab && flowTab.tabs) {
flowTab.tabs = flowTab.tabs.filter((tab: any) => {
if (tab.name === '水温(℃)' && !data[0].hasWt) return false;
if (tab.name === '水质' && !data[0].hasWq) return false;
if (tab.name === '流速(m/s)' && !data[0].hasFv) return false;
if (tab.name === "水温(℃)" && !data[0].hasWt) return false;
if (tab.name === "水质" && !data[0].hasWq) return false;
if (tab.name === "流速(m/s)" && !data[0].hasFv) return false;
return true;
});
}
}
} catch (error) {
console.error('鱼类数据检查失败:', error);
console.error("鱼类数据检查失败:", error);
}
};
@ -417,25 +398,25 @@ const checkAnalysisAvailability = async (stcd: string) => {
try {
const filter = {
filter: {
logic: 'and',
logic: "and",
filters: [
{ field: 'stcd', operator: 'eq', value: stcd },
{ field: 'mway', operator: 'eq', value: '2' },
{ field: 'dtinType', operator: 'eq', value: '0' },
{ field: "stcd", operator: "eq", value: stcd },
{ field: "mway", operator: "eq", value: "2" },
{ field: "dtinType", operator: "eq", value: "0" },
{
logic: 'or',
logic: "or",
filters: [
{ field: 'engLctn3Code', operator: 'isnotnull' },
{ field: 'engLctn1Code', operator: 'isnotnull' }
]
}
]
}
{ field: "engLctn3Code", operator: "isnotnull" },
{ field: "engLctn1Code", operator: "isnotnull" },
],
},
],
},
};
const res = await checkAnalysisShow(filter);
modelStore.wqShow = !!res?.data;
} catch (error) {
console.error('水质 AI 预测检查失败:', error);
console.error("水质 AI 预测检查失败:", error);
modelStore.wqShow = false;
}
};
@ -448,26 +429,26 @@ const checkWaterTempConfig = async (stcd: string) => {
getMonitorDataWaterTempPowerStation2({ stcd }),
getBuildState({
filter: {
logic: 'and',
filters: [{ field: 'stcd', operator: 'eq', value: stcd }]
}
})
logic: "and",
filters: [{ field: "stcd", operator: "eq", value: stcd }],
},
}),
]);
modelStore.wtConfig = {
ioWtrv: configRes?.data?.ioWtrv,
hasRstcdWtvt: dwRes?.data
hasRstcdWtvt: dwRes?.data,
};
//
const buildData = buildRes?.data?.[0];
if (!buildData?.engIwtCode && !buildData?.engDwtCode) {
tabsConfig.value = tabsConfig.value.filter(
(item: any) => item.name !== '出入库水温对比'
(item: any) => item.name !== "出入库水温对比"
);
}
} catch (error) {
console.error('水温配置检查失败:', error);
console.error("水温配置检查失败:", error);
}
};
@ -476,28 +457,23 @@ const checkBuildState = async (stcd: string) => {
try {
const res = await getBuildState({
filter: {
logic: 'and',
filters: [{ field: 'stcd', operator: 'eq', value: stcd }]
}
logic: "and",
filters: [{ field: "stcd", operator: "eq", value: stcd }],
},
});
const buid = res?.data?.[0]?.bldsttCcode;
// 0 1 2
const zj = ['基础信息', '实时视频', '全景影像', '查看报告'];
const wj = ['基础信息', '查看报告'];
const zj = ["基础信息", "实时视频", "全景影像", "查看报告"];
const wj = ["基础信息", "查看报告"];
if (res?.data?.length > 0) {
const zJTabData = tabsConfig.value?.filter((item: any) =>
zj.includes(item.name)
);
const wJTabData = tabsConfig.value?.filter((item: any) =>
wj.includes(item.name)
);
tabsConfig.value =
buid == 1 ? zJTabData : buid == 0 ? wJTabData : tabsConfig.value;
const zJTabData = tabsConfig.value?.filter((item: any) => zj.includes(item.name));
const wJTabData = tabsConfig.value?.filter((item: any) => wj.includes(item.name));
tabsConfig.value = buid == 1 ? zJTabData : buid == 0 ? wJTabData : tabsConfig.value;
}
} catch (error) {
console.error('建设状态检查失败:', error);
console.error("建设状态检查失败:", error);
}
};
@ -507,17 +483,17 @@ const setQGCTabList = async (_tabs: any, stcd: string) => {
try {
// 1.
if (modelStore.params.sttp == 'ENG' && modelStore.params.eqtp != 'QEC') {
if (modelStore.params.sttp == "ENG" && modelStore.params.eqtp != "QEC") {
const res = await getBuildState({
filter: {
logic: 'and',
filters: [{ field: 'stcd', operator: 'eq', value: stcd }]
}
logic: "and",
filters: [{ field: "stcd", operator: "eq", value: stcd }],
},
});
const buid = res?.data?.[0]?.bldsttCcode;
const zj = ['基础信息', '实时视频', '全景影像', '查看报告'];
const wj = ['基础信息', '查看报告'];
const zj = ["基础信息", "实时视频", "全景影像", "查看报告"];
const wj = ["基础信息", "查看报告"];
if (res?.data?.length > 0) {
const zJTabData = tabs?.filter((item: any) => zj.includes(item.name));
@ -531,20 +507,16 @@ const setQGCTabList = async (_tabs: any, stcd: string) => {
let newTabs = [...tabs];
if (res4 && res4.data && res4.data.relList) {
const relList = res4.data.relList;
const jianCei = relList.find((item: any) => item.sttpCode === 'WTVT');
const aiShiBie = relList.filter((item: any) =>
item?.sttpCode?.startsWith('AI')
);
const jianCei = relList.find((item: any) => item.sttpCode === "WTVT");
const aiShiBie = relList.filter((item: any) => item?.sttpCode?.startsWith("AI"));
if (!jianCei) {
newTabs = newTabs.filter(
item => item.type !== 'VerticalWaterTemperature'
);
newTabs = newTabs.filter((item) => item.type !== "VerticalWaterTemperature");
} else {
modelStore.verticalWaterStcd = jianCei.stcd;
}
if (!aiShiBie.length) {
newTabs = newTabs.filter(item => item.type !== 'AIYXSB');
newTabs = newTabs.filter((item) => item.type !== "AIYXSB");
} else {
modelStore.aiSites = aiShiBie;
}
@ -553,65 +525,65 @@ const setQGCTabList = async (_tabs: any, stcd: string) => {
// 3.
let params = {
filter: {
logic: 'and',
logic: "and",
filters: [
{
field: 'sttpCode',
operator: 'startswith',
value: 'VD_'
field: "sttpCode",
operator: "startswith",
value: "VD_",
},
{
logic: 'or',
logic: "or",
filters: [
{
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: stcd
field: "rstcd",
operator: "eq",
dataType: "string",
value: stcd,
},
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
field: "stcd",
operator: "eq",
dataType: "string",
value: stcd,
},
{
field: 'fhstcd',
operator: 'eq',
dataType: 'string',
value: stcd
field: "fhstcd",
operator: "eq",
dataType: "string",
value: stcd,
},
{
field: 'stCode',
operator: 'eq',
dataType: 'string',
value: stcd
}
]
}
]
field: "stCode",
operator: "eq",
dataType: "string",
value: stcd,
},
],
},
],
},
select: ['stcd', 'stnm', 'sttpCode', 'sttpName', 'playWay', 'url'],
select: ["stcd", "stnm", "sttpCode", "sttpName", "playWay", "url"],
sort: [
{
field: 'stnm',
dir: 'asc'
}
]
field: "stnm",
dir: "asc",
},
],
};
const res2 = await getVideoList(params);
console.log('', modelStore.params.sttp);
console.log("", modelStore.params.sttp);
if (
res2?.data?.data?.length > 0 ||
res2?.data?.records?.length > 0 ||
modelStore.params.sttp == 'WQ'
modelStore.params.sttp == "WQ"
) {
tabsConfig.value = newTabs;
} else {
tabsConfig.value = newTabs.filter((item: any) => item.type != 'video');
tabsConfig.value = newTabs.filter((item: any) => item.type != "video");
}
} catch (error) {
console.error('QGC Tab 过滤失败:', error);
console.error("QGC Tab 过滤失败:", error);
}
};
@ -628,38 +600,36 @@ const applyTabFilters = async (params: any) => {
if (!stcd) return;
// React
if (isLCJ && sttpCode === 'VP') {
if (isLCJ && sttpCode === "VP") {
// LCJ
const res = await getVideoList({
filter: {
logic: 'and',
filters: [{ field: 'stcd', operator: 'eq', value: stcd }]
}
logic: "and",
filters: [{ field: "stcd", operator: "eq", value: stcd }],
},
});
if (!res?.data?.data?.length && !res?.data?.records?.length) {
tabsConfig.value = tabsConfig.value.filter(
item => item.name != '实时视频'
);
tabsConfig.value = tabsConfig.value.filter((item) => item.name != "实时视频");
}
} else if (isLCJ && sttpCode === 'WE') {
} else if (isLCJ && sttpCode === "WE") {
// LCJ
await checkFishTab(stcd);
} else if (sttpCode === 'WQ' || sttpCode === 'WQFB' || sttpCode === 'wq') {
} else if (sttpCode === "WQ" || sttpCode === "WQFB" || sttpCode === "wq") {
// AI React WQ WQFB/wq
await checkAnalysisAvailability(stcd);
} else if (sttpCode === 'WT' || sttpCode === 'wt') {
} else if (sttpCode === "WT" || sttpCode === "wt") {
// React WT wt
await checkWaterTempConfig(stcd);
} else if (
sttpList.includes(sttpCode?.toUpperCase()) ||
sttpCode?.startsWith('FP_') ||
sttpCode?.includes('DW')
sttpCode?.startsWith("FP_") ||
sttpCode?.includes("DW")
) {
// QGC Tab AI
await setQGCTabList(tabsConfig.value, stcd);
}
} catch (error) {
console.error('Tab 过滤逻辑执行失败:', error);
console.error("Tab 过滤逻辑执行失败:", error);
} finally {
isLoading.value = false; //
}
@ -675,31 +645,31 @@ const qxdConfigMap: Record<
selectProp: string;
}
> = {
'qxd.tabs.jcsj': {
"qxd.tabs.jcsj": {
//
sttpCode: 'WTRV',
sttpCode: "WTRV",
apiFn: getMonitorDataWt,
selectProp: 'qxdSelectedStcd'
selectProp: "qxdSelectedStcd",
},
'qxd.tabs.szjc': {
"qxd.tabs.szjc": {
//
sttpCode: 'WQ',
sttpCode: "WQ",
apiFn: getMonitorDataWq,
selectProp: 'qxdSelectedStcdWq'
selectProp: "qxdSelectedStcdWq",
},
'qxd.tabs.lcjs': {
"qxd.tabs.lcjs": {
//
sttpCode: 'ZQ',
sttpCode: "ZQ",
apiFn: getMonitorDataZq,
selectProp: 'qxdSelectedStcdZq'
}
selectProp: "qxdSelectedStcdZq",
},
};
//
const qxdStationOptions = ref<{ label: string; value: string }[]>([]);
const qxdSelectedStcd = ref<string>('');
const qxdSelectedStcdWq = ref<string>('');
const qxdSelectedStcdZq = ref<string>('');
const qxdSelectedStcd = ref<string>("");
const qxdSelectedStcdWq = ref<string>("");
const qxdSelectedStcdZq = ref<string>("");
const qxdLoadedCodes = ref<Set<string>>(new Set());
// tabqxd
@ -711,16 +681,16 @@ const getCurrentQxdConfig = (tabKey: string) => {
// stcd
const getQxdSelectedStcd = (tabKey: string) => {
const config = getCurrentQxdConfig(tabKey);
if (!config) return '';
if (!config) return "";
switch (config.selectProp) {
case 'qxdSelectedStcd':
case "qxdSelectedStcd":
return qxdSelectedStcd.value;
case 'qxdSelectedStcdWq':
case "qxdSelectedStcdWq":
return qxdSelectedStcdWq.value;
case 'qxdSelectedStcdZq':
case "qxdSelectedStcdZq":
return qxdSelectedStcdZq.value;
default:
return '';
return "";
}
};
@ -729,13 +699,13 @@ const setQxdSelectedStcd = (tabKey: string, value: string) => {
const config = getCurrentQxdConfig(tabKey);
if (!config) return;
switch (config.selectProp) {
case 'qxdSelectedStcd':
case "qxdSelectedStcd":
qxdSelectedStcd.value = value;
break;
case 'qxdSelectedStcdWq':
case "qxdSelectedStcdWq":
qxdSelectedStcdWq.value = value;
break;
case 'qxdSelectedStcdZq':
case "qxdSelectedStcdZq":
qxdSelectedStcdZq.value = value;
break;
}
@ -751,30 +721,30 @@ const loadQxdStationList = async (tabKey: string) => {
try {
const params = {
filter: {
logic: 'and',
logic: "and",
filters: [
{
field: 'fhstcd',
operator: 'eq',
dataType: 'string',
value: modelStore.params.stcd
field: "fhstcd",
operator: "eq",
dataType: "string",
value: modelStore.params.stcd,
},
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: config.sttpCode
}
]
field: "sttpCode",
operator: "eq",
dataType: "string",
value: config.sttpCode,
},
],
},
select: ['stcd', 'stnm']
select: ["stcd", "stnm"],
};
const res = await config.apiFn(params);
const stations = res?.data?.data || res?.data?.records || [];
if (stations.length > 0) {
qxdStationOptions.value = stations.map((item: any) => ({
label: item.stnm,
value: item.stcd
value: item.stcd,
}));
setQxdSelectedStcd(tabKey, stations[0].stcd);
}
@ -787,7 +757,7 @@ const loadQxdStationList = async (tabKey: string) => {
// tabstcd (v-model)
const currentQxdSelectedStcd = computed({
get: () => getQxdSelectedStcd(currentActiveKey.value),
set: (val: string) => setQxdSelectedStcd(currentActiveKey.value, val)
set: (val: string) => setQxdSelectedStcd(currentActiveKey.value, val),
});
// qxd
@ -812,7 +782,7 @@ interface TabItem {
// Tab URL
const getTabUrl = (key: string) => {
const tab = tabsConfig.value.find((item: any) => item.key === key);
return tab?.url || '';
return tab?.url || "";
};
// Tab Tab
const getTabChildren = (key: string) => {
@ -823,7 +793,7 @@ const getTabChildren = (key: string) => {
// Tab code
const getTabCode = (key: string) => {
const tab = tabsConfig.value.find((item: any) => item.key === key);
return tab?.code || '';
return tab?.code || "";
};
// Props
const props = defineProps<{
@ -834,22 +804,22 @@ const props = defineProps<{
}>();
//
useDraggable(toRef(props, 'visible'), { boundary: true, resetOnOpen: true });
useDraggable(toRef(props, "visible"), { boundary: true, resetOnOpen: true });
// Emits
// 'update:activeKey' v-model:active-key
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'change', key: string): void;
(e: "update:visible", value: boolean): void;
(e: "change", key: string): void;
}>();
// activeKey
const currentActiveKey = ref<string>('');
const anchor = ref<any>('');
const currentActiveKey = ref<string>("");
const anchor = ref<any>("");
// activeKey
watch(
() => props.activeKey,
newVal => {
(newVal) => {
if (newVal && newVal !== currentActiveKey.value) {
currentActiveKey.value = newVal;
}
@ -858,14 +828,14 @@ watch(
);
watch(
() => modelStore.params,
async newVal => {
async (newVal) => {
if (!newVal || Object.keys(newVal).length === 0) return;
console.log(newVal);
anchor.value = newVal;
let params = newVal;
if (['VD', 'WTRV', 'ZQ', 'WQ'].includes(params?.sttpCode)) {
if (["VD", "WTRV", "ZQ", "WQ"].includes(params?.sttpCode)) {
params.sttp = params.sttpCode;
} else if (['ENG_ALARM'].includes(params?.sttpMap)) {
} else if (["ENG_ALARM"].includes(params?.sttpMap)) {
params.sttp = params.sttpMap;
}
tabsConfig.value = handleTabs({ ...params });
@ -874,8 +844,8 @@ watch(
currentActiveKey.value = value?.key;
//
if (params?.sttp?.toUpperCase() === 'ENG') {
checkPowerStationTopicData(params.stcd);
if (params?.sttp?.toUpperCase() === "ENG") {
// checkPowerStationTopicData(params.stcd);
}
// Tab
@ -887,7 +857,7 @@ watch(
// tab qxd
watch(
() => currentActiveKey.value,
activeKey => {
(activeKey) => {
const code = getTabCode(activeKey);
if (code && qxdConfigMap[code]) {
loadQxdStationList(activeKey);
@ -900,42 +870,42 @@ const onTabChange = (key: string) => {
currentActiveKey.value = key;
// activeKey ( v-model)
// ()
emit('change', key);
emit("change", key);
};
//
const handleClose = () => {
emit('update:visible', false);
emit("update:visible", false);
modelStore.params = {
sttp: 'eng',
stcd: '',
date: '',
sttp: "eng",
stcd: "",
date: "",
show: true,
enfc: '0',
eqtp: '',
baseId: '',
sttpCode: '',
sttpMap: ''
enfc: "0",
eqtp: "",
baseId: "",
sttpCode: "",
sttpMap: "",
};
// ,
modelStore.showStcdSelector = false;
modelStore.stcdOptions = [];
// qxd
qxdStationOptions.value = [];
qxdSelectedStcd.value = '';
qxdSelectedStcdWq.value = '';
qxdSelectedStcdZq.value = '';
qxdSelectedStcd.value = "";
qxdSelectedStcdWq.value = "";
qxdSelectedStcdZq.value = "";
qxdLoadedCodes.value = new Set();
//
hasPowerStatData.value = false;
modelStore.verticalWaterStcd = '';
modelStore.verticalWaterStcd = "";
modelStore.aiSites = [];
modelStore.wtConfig = {};
modelStore.filter = {
stllgzlx: '', // -
stllTmType: '', // -
tm: '',
rangeTm: []
stllgzlx: "", // -
stllTmType: "", // -
tm: "",
rangeTm: [],
};
modelStore.wqShow = false;
modelStore.fishTabInfo = null;
@ -945,7 +915,7 @@ const handleClose = () => {
const modalData = ref(props.data);
watch(
() => props.data,
newVal => {
(newVal) => {
modalData.value = newVal;
}
);
@ -956,7 +926,7 @@ const filterOption = (input: string, option: any) => {
//
const handleStcdChange = (newStcd: string) => {
console.log('【测站切换】', newStcd);
console.log("【测站切换】", newStcd);
// BasicInfo.vue stcd , store
// tab ,
};

View File

@ -11,7 +11,7 @@ VITE_APP_BASE_URL = 'http://localhost:8093'
# 测试环境
# VITE_APP_BASE_URL = 'http://172.16.21.142:8093'
# 汤伟
VITE_APP_BASE_URL = 'http://10.84.111.235:8093'
VITE_APP_BASE_URL = 'http://10.84.216.252:8093'
# 李林
# VITE_APP_BASE_URL = 'http://10.84.111.25:8093'
# VITE_APP_BASE_URL = 'http://10.84.111.211:8093'

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1786 -829 )">
<path d="M 11.9999999556426 0 L 4.00000004435742 0 C 3.053226196683 0 2.28571434669061 0.818679320547503 2.28571434669061 1.82857143452381 L 2.28571434669061 3.1512380952381 C 1.60057497972548 3.40981403899944 1.14264753458247 4.10097919714247 1.14285720933208 4.87619047023809 L 1.14285720933208 6.19885714880952 C 0.457717842367174 6.45743309257087 -0.000209602775839812 7.1485982507139 0 7.92380952380952 L 0 12.8000000059524 C 0 14.48315352556 1.27918649766411 15.8476190416667 2.85714290699912 15.8476190416667 L 13.1428570930009 15.8476190416667 C 14.7208135023359 15.8476190416667 15.9999999187803 14.48315352556 15.9999999280262 12.8000000059524 L 15.9999999280262 7.92380952380952 C 16.0002096027758 7.1485982507139 15.5422821576328 6.45743309257087 14.8571427906679 6.19885714880952 L 14.8571427906679 4.87619047023809 C 14.8573524654175 4.10097919714247 14.3994250202745 3.40981403899944 13.7142856533094 3.1512380952381 L 13.7142856533094 1.82857143452381 C 13.7142856533094 0.818679320547503 12.946773803317 0 11.9999999556426 0 Z M 13.7142856533094 4.87619047023809 L 13.7142856533094 6.0952380952381 L 10.2857142579755 6.0952380952381 C 9.97012297849938 6.0952380952381 9.71428569766704 6.36813119743816 9.71428569766704 6.70476189880952 C 9.71428569766704 7.71465401278583 8.94677384767442 8.53333332738094 8 8.53333332738094 C 7.05322615232558 8.53333332738094 6.28571430233296 7.71465401278583 6.28571430233296 6.70476189880952 C 6.28571430233296 6.36813119743816 6.02987702150062 6.0952380952381 5.71428574202446 6.0952380952381 L 2.28571434669061 6.0952380952381 L 2.28571434669061 4.8761904702381 C 2.28571434669061 4.53955976886673 2.54155162752295 4.26666666666667 2.85714290699912 4.26666666666667 L 13.1428570930009 4.26666666666667 C 13.4584483724771 4.26666666666667 13.7142856533094 4.53955976886673 13.7142856533094 4.87619047023809 Z M 1.14285720933208 7.92380952380952 C 1.14285720933208 7.58717882243816 1.39869449016464 7.3142857202381 1.71428576964081 7.3142857202381 L 5.20000003272435 7.31428572023809 C 5.47269110222078 8.73247492680181 6.64277123544298 9.75055874974881 8 9.75055874974881 C 9.35722876455702 9.75055874974881 10.5273088977792 8.73247492680181 10.7999999672757 7.3142857202381 L 14.2857142303592 7.3142857202381 C 14.6013055098354 7.3142857202381 14.8571427906679 7.58717882243816 14.8571427906679 7.92380952380952 L 14.8571427906679 12.8000000059524 C 14.8571427906679 13.8098921199287 14.0896309406753 14.6285714345238 13.1428570930009 14.6285714345238 L 2.85714290699912 14.6285714345238 C 1.91036905932469 14.6285714345238 1.14285720933208 13.8098921199287 1.14285720933208 12.8000000059524 L 1.14285720933208 7.92380952380952 Z M 12.5714285326922 1.82857143452381 L 12.5714285326922 3.04761904166666 L 3.42857146730785 3.04761904166666 L 3.42857146730785 1.82857143452381 C 3.42857146730785 1.49194071529531 3.68440874814019 1.21904761309524 4.00000002761635 1.21904761309524 L 11.9999999556426 1.21904761309524 C 12.3155912518598 1.21904761309524 12.5714285326922 1.49194071529531 12.5714285326922 1.82857141666667 Z M 5.71428574202446 12.1904761845238 C 5.71428574202446 11.8538454831524 5.9701230228568 11.5809523809524 6.28571430233296 11.5809523809524 L 9.71428569766704 11.5809523809524 C 10.0298769817662 11.5809523809524 10.2857142663461 11.8538454871499 10.2857142663461 12.1904761934524 C 10.2857142663461 12.5271068997549 10.0298769817662 12.8000000059524 9.71428569766704 12.8000000059524 L 6.28571430233296 12.8000000059524 C 5.9701230228568 12.8000000059524 5.71428574202446 12.5271069037523 5.71428574202446 12.1904762023809 Z " fill-rule="nonzero" fill="#255aa7" stroke="none" transform="matrix(1 0 0 1 1786 829 )" />
</g>
</svg>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1754 -829 )">
<path d="M 0.89038789844335 0 L 10.9513354812796 0 C 11.1287252776408 0 11.2174193247947 0.0886947998772105 11.3061150796195 0.177389650310463 L 11.3948099806087 0.266084455805011 L 14.4104339965652 3.45909741428739 C 14.5878237929264 3.6364869971898 14.6765186939158 3.81387679916839 14.6765186939158 4.07996106960125 L 14.6765186939158 8.69209156199027 C 14.6765186939158 9.0468711603301 14.4104339965652 9.31295500384528 14.0556543982254 9.31295500384528 C 13.7008747998855 9.31295500384528 13.4347909563703 9.0468711603301 13.4347909563703 8.69209156199027 L 13.4347909563703 5.23299390615734 L 10.5965567423925 5.23299390615734 C 10.1530822430634 5.23299390615734 9.70960946263972 4.87821473473519 9.62091456165035 4.34604576695179 L 9.62091456165035 1.33042209927024 L 1.24516733949781 1.33042209927024 L 1.24516733949781 14.6346450355716 L 9.00004994577171 14.6346450355716 C 9.35482954411145 14.6346450355716 9.62091338762662 14.9007280196341 9.62091338762662 15.2555076179739 L 9.62091338762662 15.3442025189632 C 9.62091338762662 15.698982117303 9.35482954411145 15.9650668146536 9.08874399292563 15.9650668146536 L 0.979082855606066 15.9650668146536 C 0.535608732638593 15.9650668146536 0.092134716400551 15.6102872163137 0 15.0781195349007 L 0 0.975642933465427 C 0.00343992214061473 0.443474044324756 0.35821911603216 0 0.89038789844335 0 Z M 13.1687062590199 3.99126662361633 L 10.9513354868971 1.59650647643252 L 10.9513354868971 3.99126662361633 L 13.1687054051845 3.99126662361633 Z M 12.7252317596908 10.2885980608921 C 13.6121798988963 10.2885980608921 14.4991288919373 10.7320725602213 14.9426033912664 11.5303257984375 C 15.3860778905955 12.3285790366538 15.3860778905955 13.3042220768486 14.9426033912664 14.1024753150649 L 15.7408557756471 14.9007276994458 C 16.0069404729977 15.1668123967963 16.0069404729977 15.5215919951362 15.7408557756471 15.7876766924867 C 15.4747727915847 16.0537596765492 15.1199914799568 16.0537596765492 14.8539084958943 15.8763698801879 L 14.7652135949052 15.7876766924867 L 13.9669603566888 14.9894225948178 C 13.080013076936 15.5215919895188 11.9269802403799 15.4328970941469 11.0400312417216 14.8120327984565 C 10.2417780035053 14.1024753150649 9.88699840516551 13.0381373794981 10.2417780035053 11.9738002977667 C 10.6852525028344 10.9981581170245 11.6608946835765 10.2885980608921 12.725233478596 10.2885980608921 Z M 13.9669594972361 12.7720535416003 C 13.9669594972361 12.0624952043733 13.3460960553812 11.5303257984375 12.7252317596908 11.5303257984375 C 12.2817581198142 11.5303257984375 11.8382844799378 11.7964096419527 11.6608938241238 12.1511892402925 C 11.3948099806087 12.5059688386324 11.3948099806087 13.0381373794981 11.6608938241238 13.3929169778379 C 11.9269785214744 13.7476965761778 12.2817581198142 14.0137804196929 12.7252317596908 14.0137804196929 C 13.4347909563703 14.0137804196929 13.9669594972361 13.4816118788272 13.9669594972361 12.7720535416003 Z M 3.12549730248907 5.36519114038518 C 3.21419198440231 4.92030965519137 3.39158199983967 4.69786826379193 3.83505563971607 4.69786826379193 L 7.95345164763125 4.69786826379193 C 8.39692528750766 4.69786826379193 8.66300998485826 4.92030964957403 8.66300998485826 5.36519114038518 C 8.66300998485826 5.81007263119633 8.39692528750766 6.03251444389611 7.95345164763125 6.03251444389611 L 3.83505563971607 6.03251444389611 C 3.39158199983967 6.03251444389611 3.12549730248907 5.81007305811402 3.12549730248907 5.36519114038518 Z M 3.21419199001957 8.14570312899906 C 3.21419198440231 7.81204254734647 3.39158199983967 7.47838025240581 3.74636117126192 7.47838025240581 L 10.3482113678974 7.47838025240581 C 10.7029909662372 7.47838025240581 10.8803807625984 7.70082035181753 10.8803807625984 8.14570312899906 C 10.8803807625984 8.5905859061806 10.7029909662372 8.81302600559231 10.3482113678974 8.81302600559231 L 3.74636117126192 8.81302600559231 C 3.39158199983967 8.81302600559231 3.21419199001957 8.5905859061806 3.21419199001957 8.14570312899906 Z M 3.12549730248907 11.0374355942365 C 3.21419198440231 10.592553682125 3.39158199983967 10.3701127176432 3.83505563971607 10.3701127176432 L 7.95345164763125 10.3701127176432 C 8.39692528750766 10.3701127176432 8.66300998485826 10.5925536765076 8.66300998485826 11.0374355942365 C 8.66300998485826 11.482318371418 8.39692528750766 11.7047593302824 7.95345164763125 11.7047593302824 L 3.83505563971607 11.7047593302824 C 3.39158199983967 11.7047593302824 3.12549730248907 11.482318371418 3.12549730248907 11.0374355942365 Z " fill-rule="nonzero" fill="#255aa7" stroke="none" transform="matrix(1 0 0 1 1754 829 )" />
</g>
</svg>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -672 -170 )">
<path d="M 14.3232142857144 16 C 15.2428571428571 15.9232142857143 15.9875 15.1785714285714 16 14.2589285714286 L 16 1.66249999999999 C 15.9875 0.74464285714285 15.2428571428571 0 14.325 0 L 1.72857142857151 0 C 0.80892857142851 0 0.0642857142856883 0.74464285714285 0 1.66428571428571 L 0 14.2589285714286 C 0.0642857142856883 15.1785714285714 0.80892857142851 15.9232142857143 1.72857142857151 16 L 14.3232142857144 16 Z M 12.2446428571428 7.625 C 12.3125 7.53214285714286 11.7571428571429 7.0125 11.6875 6.9375 C 10.6964285714286 5.86785714285715 9.48214285714289 4.68214285714285 9.48214285714289 3.11607142857142 C 9.4803571428572 1.6125 10.7 0.394642857142856 12.2035714285714 0.394642857142856 C 13.7071428571428 0.394642857142856 14.925 1.6125 14.925 3.11607142857142 C 14.925 4.96250000000001 13.3589285714286 6.18571428571428 12.325 7.51964285714286 C 12.2982142857143 7.55535714285715 12.2714285714285 7.58928571428571 12.2446428571428 7.625 Z M 11.2375 11.1071428571428 L 14.8464285714285 7.28035714285714 C 14.9857142857143 7.13214285714285 15.2196428571428 7.125 15.3696428571428 7.26428571428571 C 15.5178571428571 7.40535714285714 15.525 7.63928571428571 15.3839285714286 7.78749999999999 L 11.7660714285714 11.625 L 14.3732142857143 14.1803571428571 C 14.5196428571428 14.3232142857143 14.5214285714285 14.5571428571428 14.3785714285715 14.7035714285714 C 14.3053571428571 14.7767857142857 14.2107142857143 14.8142857142857 14.1142857142856 14.8142857142857 C 14.0214285714285 14.8142857142857 13.9267857142856 14.7785714285714 13.8553571428572 14.7089285714286 L 8.46785714285716 9.42857142857142 L 3.93035714285713 14.2392857142857 C 3.85714285714289 14.3160714285714 3.75892857142856 14.3553571428571 3.66071428571433 14.3553571428571 C 3.56964285714287 14.3553571428571 3.47857142857151 14.3214285714286 3.40714285714284 14.2553571428571 C 3.25892857142856 14.1160714285714 3.25178571428569 13.8821428571428 3.39107142857142 13.7321428571428 L 7.93928571428569 8.91071428571429 L 5.68571428571431 6.70178571428571 L 2.18928571428569 10.4089285714286 C 2.11607142857144 10.4857142857143 2.01785714285711 10.525 1.91964285714289 10.525 C 1.82857142857142 10.525 1.73749999999995 10.4910714285714 1.66607142857151 10.425 C 1.51785714285711 10.2857142857143 1.51071428571436 10.0517857142857 1.64999999999998 9.90178571428571 L 5.15535714285716 6.18392857142857 L 1.68035714285713 2.77857142857142 C 1.53392857142853 2.63571428571429 1.53214285714284 2.40178571428571 1.67499999999995 2.25535714285715 C 1.81785714285718 2.10892857142856 2.05178571428564 2.10714285714286 2.19821428571436 2.25 L 8.44821428571436 8.37321428571428 L 9.57321428571436 7.18035714285713 C 9.71249999999998 7.03214285714286 9.94642857142856 7.02500000000001 10.0964285714285 7.16428571428571 C 10.2446428571428 7.30357142857142 10.2517857142857 7.53749999999999 10.1125 7.6875 L 8.97857142857151 8.88928571428571 L 11.2375 11.1071428571428 Z M 13.625 2.87142857142857 C 13.625 2.08639524845481 12.9886047515453 1.45 12.2035714285714 1.45 C 11.4185381055977 1.45 10.7821428571428 2.08639524845481 10.7821428571428 2.87142857142857 C 10.7821428571428 3.65646189440234 11.4185381055977 4.29285714285714 12.2035714285714 4.29285714285714 C 12.9886047515453 4.29285714285714 13.625 3.65646189440234 13.625 2.87142857142857 Z " fill-rule="nonzero" fill="#005293" stroke="none" transform="matrix(1 0 0 1 672 170 )" />
</g>
</svg>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -565 -875 )">
<path d="M 0 8 C 0 3.58181818749995 3.58181818749995 0 8 0 C 12.4181818125001 0 16 3.58181818749995 16 8 C 16 12.4181818125001 12.4181818125001 16 8 16 C 3.58181818749995 16 0 12.4181818125001 0 8 Z M 7.23490909374993 4.18436364062495 L 7.27999999999997 4.18200000000002 L 7.23509090624998 4.18436364062495 L 5.10563635937501 4.34854545312498 C 4.71454545312497 4.378727265625 4.37836362500002 4.71400000000006 4.3481818125 5.10545454687497 L 4.184545453125 7.23490909374993 C 4.16616282204484 7.47433213284717 4.25329832724606 7.70983708702352 4.42309090625008 7.87963635937501 L 8.12090909375002 11.5770909062501 C 8.27478417521331 11.7320563374394 8.48434384064558 11.8188926242638 8.70272726562496 11.8181818125 C 8.9210035982984 11.8186207899432 9.13040619707181 11.731822466318 9.28436364062497 11.5770909062501 L 11.5774545468751 9.28363635937501 C 11.8982525597308 8.96207485258412 11.898415190837 8.44158010127046 11.5778181875 8.11981818749996 L 7.88 4.42254545312494 C 7.7100917776836 4.25272220670638 7.47440966938086 4.16570316615639 7.23490909374993 4.18436364062495 Z M 5.66798476241524 6.28836102379478 C 5.66798476241524 5.94604586717719 5.94495902002711 5.66826642423223 6.28727273437494 5.66727273437505 C 6.62979956582933 5.66796697062637 6.90710481095891 5.94583482992755 6.90710481095891 6.28836236492305 C 6.90710481095891 6.45242147490467 6.84219401306905 6.6098168600837 6.72654545312503 6.72618181250004 C 6.48382821774419 6.96799606812897 6.09126268850582 6.96799606812897 5.84854545312498 6.72618181250004 C 5.7328960771963 6.60981674757102 5.66798476241524 6.45242078878493 5.66798476241524 6.28836102379478 Z " fill-rule="nonzero" fill="#ff424f" stroke="none" transform="matrix(1 0 0 1 565 875 )" />
</g>
</svg>

View File

@ -162,7 +162,7 @@
value="specify"
class="zhding"
>
<span class="option-label" style="min-width: 30px;">指定</span>
<span class="option-label" style="min-width: 30px">指定</span>
<div v-if="field.key === 'hour'" class="hour-grid">
<a-checkbox-group
v-model:value="fieldData[field.key].specifyValues"
@ -368,22 +368,10 @@ const formatHour = (val: number) => {
return val.toString().padStart(2, '0');
};
const isDaySpecific = () => {
const day = fieldData.day;
return day.type !== 'all' && day.type !== 'lastDay';
};
const isWeekSpecific = () => {
const week = fieldData.week;
return week.type !== 'all' && week.type !== 'none';
};
const generateFieldValue = (key: string) => {
const data = fieldData[key];
switch (data.type) {
case 'all':
if (key === 'day' && isWeekSpecific()) return '?';
if (key === 'week' && isDaySpecific()) return '?';
if (key === 'year') return '';
return '*';
case 'none':
@ -404,8 +392,6 @@ const generateFieldValue = (key: string) => {
return 'L';
case 'specify':
if (data.specifyValues.length === 0) {
if (key === 'day' && isWeekSpecific()) return '?';
if (key === 'week' && isDaySpecific()) return '?';
return '*';
}
return data.specifyValues
@ -527,6 +513,12 @@ const validateCron = (
errorMessage: '日和周不能同时指定,请将其中一个设为"不指定"'
};
}
if (day === '?' && week === '?') {
return {
valid: false,
errorMessage: '日和周不能同时为不指定,请至少指定其中一个'
};
}
return { valid: true, errorMessage: '' };
};
@ -699,8 +691,8 @@ watch(
align-items: flex-start !important;
// margin-top: px;
}
:deep(.ant-radio){
align-self:auto !important;
:deep(.ant-radio) {
align-self: auto !important;
// margin-top: 0px !important;
}
}

View File

@ -35,7 +35,6 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import dayjs from 'dayjs';
import { message } from 'ant-design-vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { queryPostUrlList } from '@/api/mapModal';

View File

@ -49,7 +49,7 @@
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, computed, h } from 'vue';
import { ref, watch, computed, h } from 'vue';
import { Button } from 'ant-design-vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { getMonitorDataFpq, getMonitorDataOnline } from '@/api/mapModal';
@ -60,7 +60,6 @@ import { useDraggable } from '@/utils/drag';
const loading = ref(false);
const modelStore = useModelStore();
const hasLoaded = ref(false);
const tableRef = ref();
const searchParams = ref([
dayjs().subtract(5, 'year').format('YYYY-MM-DD HH:mm:ss'),
@ -276,7 +275,6 @@ const initData = async () => {
const res1 = await getMonitorDataFpq(params1);
const res = await getMonitorDataFpq(params);
let stcd = '';
console.log(res1);
if (res) {
if (res?.data?.data?.length > 0) {
stcd = res.data.data.map(item => item.stcd);
@ -291,7 +289,6 @@ const initData = async () => {
}
}
}
console.log(stcd);
const filter = {
logic: 'and',
filters: [

View File

@ -85,9 +85,12 @@ const updateChart = (data: any[]) => {
return;
}
const sorted = [...data].sort(
(a, b) => new Date(a.tm).getTime() - new Date(b.tm).getTime()
);
const toTime = (tm: any) => {
if (!tm) return 0;
const t = new Date(tm).getTime();
return Number.isNaN(t) ? 0 : t;
};
const sorted = [...data].sort((a, b) => toTime(a.tm) - toTime(b.tm));
const xAxisData = sorted.map(item => {
const hm = dayjs(item.tm).format('HH:mm');
@ -399,57 +402,65 @@ const initDateRange = (): [Dayjs, Dayjs] => {
};
const dateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
// null/undefined//'-'/NaN '-'
const formatCell = (
text: any,
formatter: (num: number) => string = num => String(num)
) => {
if (text === null || text === undefined || text === '' || text === '-') {
return '-';
}
const num = Number(text);
if (Number.isNaN(num)) return '-';
return formatter(num);
};
//
const tableColumns = [
{
title: '时间',
dataIndex: 'tm',
width: 180,
width: 150,
fixed: 'left',
customRender: ({ text }: any) =>
text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '-'
text && text !== '-' ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '-'
},
{
title: '坝上水位(m)',
dataIndex: 'rz',
width: 130,
customRender: ({ text }: any) =>
text !== undefined && text !== null ? Number(text).toFixed(2) : '-'
width: 100,
customRender: ({ text }: any) => formatCell(text, v => v.toFixed(2))
},
{
title: '坝下水位(m)',
dataIndex: 'dz',
width: 130,
customRender: ({ text }: any) =>
text !== undefined && text !== null ? Number(text).toFixed(2) : '-'
width: 100,
customRender: ({ text }: any) => formatCell(text, v => v.toFixed(2))
},
{
title: '入库流量(m³/s)',
dataIndex: 'qi',
width: 130,
width: 120,
customRender: ({ text }: any) =>
text !== undefined && text !== null ? Math.round(Number(text)) : '-'
formatCell(text, v => String(Math.round(v)))
},
{
title: '出库流量(m³/s)',
dataIndex: 'qo',
width: 130,
customRender: ({ text }: any) =>
text !== undefined && text !== null ? Number(text) : '-'
width: 120,
customRender: ({ text }: any) => formatCell(text, v => String(v))
},
{
title: '生态流量(m³/s)',
dataIndex: 'qec',
width: 130,
customRender: ({ text }: any) =>
text !== undefined && text !== null ? Number(text).toFixed(1) : '-'
width: 120,
customRender: ({ text }: any) => formatCell(text, v => v.toFixed(1))
},
{
title: '生态流量限值(m³/s)',
dataIndex: 'qecLimit',
width: 150,
customRender: ({ text }: any) =>
text !== undefined && text !== null ? Number(text).toFixed(1) : '-'
width: 140,
customRender: ({ text }: any) => formatCell(text, v => v.toFixed(1))
}
];
const tableScrollX = tableColumns.reduce((s, c) => s + (c.width || 100), 0);

View File

@ -6,6 +6,7 @@
:src="iframeUrl"
class="panorama-info-iframe"
frameborder="0"
allowfullscreen
></iframe>
<a-select
v-show="iframeUrl != ''"

View File

@ -1,12 +0,0 @@
<template>
<div>
视频
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
</script>
<style lang="scss" scoped>
</style>

View File

@ -3916,10 +3916,41 @@ export class MapCesium implements MapInterface {
);
}
/** 获取 eng 渲染优先级:告警=300, eng=200, 普通=100与 2D 完全一致 */
/** 获取 eng 渲染优先级:大型=300/中型=200同规模已建>在建>未建,其他 eng=150普通=100与 2D 完全一致,报警点同规格内 +10 */
private getEntityEngRenderPriority(entity: Cesium.Entity): number {
if (this.isEntityEngAlarm(entity)) return 300;
return this.isEntityEng(entity) ? 200 : 100;
const e = entity as any;
const rawData: Record<string, any> = e._rawData || {};
const legendState = String(rawData.anchoPointState || '')
.trim()
.toLowerCase();
const isAlarm = this.isEntityEngAlarm(entity);
// 建设状态权重:已建 > 在建 > 未建
let buildRank = 0;
if (legendState.includes('_ubuilt')) {
buildRank = 2;
} else if (legendState.includes('_nbuilt')) {
buildRank = 1;
} else if (legendState.includes('_built')) {
buildRank = 3;
}
let base = 100;
// 大型电站优先级高于中型电站,同规模内已建优先
if (legendState.startsWith('large_eng_')) {
base = 300 + buildRank * 30;
} else if (legendState.startsWith('mid_eng_')) {
base = 200 + buildRank * 30;
} else if (this.isEntityEng(entity)) {
base = 150;
}
// 报警范围点仅在同规格内略微提升,不跨越大型/中型与建设状态层级
if (isAlarm) {
base += 10;
}
return base;
}
// ==================== 文本像素级精确测量 ====================

View File

@ -47,7 +47,7 @@ const VITE_APP_MAP_URL = import.meta.env.VITE_APP_MAP_URL;
const CENTER_positionCN = [114.17112499999996, 38]; // OpenLayers 使用 [lon, lat]
const MIN_ZOOM = 4.23;
const MAX_ZOOM = 22;
const INITIAL_ZOOM = 4.5;
const INITIAL_ZOOM = 4.4;
const BATCH_POPUP_MODE_ZOOM = 15;
const FULL_DISPLAY_NO_COLLISION_ZOOM = Number.POSITIVE_INFINITY;
const POINT_FADE_DURATION = 300; // 锚点/文字碰撞显隐的淡入淡出时长(ms)
@ -153,9 +153,9 @@ export class MapOl implements MapInterface {
});
const mouseWheelInteraction = new MouseWheelZoom({
duration: 100, // 缩放动画持续时间 (ms)Leaflet 默认也有动画
maxDelta: 2,
constrainResolution: false // ✅ 关键false 允许缩放到非整数级别 (如 4.5, 4.6),实现平滑缩放
duration: 100, // 缩放动画持续时间 (ms)
maxDelta: 1, // 每滚一格最大缩放步长,对齐 Mapbox 的 ~0.5 缩放幅度
constrainResolution: false // 允许缩放到非整数级别,实现平滑缩放
});
this.map = new OlMap({
@ -496,15 +496,11 @@ export class MapOl implements MapInterface {
}
const currentZoom: any = this.view ? this.view.getZoom() : 4.5;
const cachedDensityVisible = feature.get('_densityVisible');
const densityVisible =
typeof cachedDensityVisible === 'boolean'
? cachedDensityVisible
: currentZoom >= this.getFeatureDensityMinZoom(feature);
const iconCollisionVisible = feature.get('_iconCollisionVisible') !== false;
const iconTargetVisible =
legendVisible !== false &&
regionVisible !== false &&
densityVisible &&
iconCollisionVisible &&
this.shouldRenderNearbyFeature();
const dynamicScale = this.getDynamicPointScale(currentZoom);
@ -518,7 +514,6 @@ export class MapOl implements MapInterface {
dynamicScale,
labelLineCount
);
const iconCollisionVisible = feature.get('_iconCollisionVisible') !== false;
const labelCollisionVisible =
feature.get('_labelCollisionVisible') === true;
@ -1029,35 +1024,10 @@ export class MapOl implements MapInterface {
if (currentZoom >= FULL_DISPLAY_NO_COLLISION_ZOOM) {
viewportEntries.forEach(entry => {
const minZoom = this.getFeatureDensityMinZoom(entry.feature);
const densityVisible =
currentZoom >= minZoom ||
(entry.legendVisible &&
entry.regionVisible &&
!!entry.layerKey &&
entry.pixelX !== undefined &&
entry.pixelY !== undefined &&
!this.hasNearbyViewportFeature(
{
feature: entry.feature,
layerKey: entry.layerKey,
pixelX: entry.pixelX,
pixelY: entry.pixelY
},
densityPixelGrid,
densityIsolationThreshold
));
this.syncFeatureDensityVisible(
entry.feature,
densityVisible,
dirtyLayerKeys
);
const shouldRenderIcon =
!!entry.iconUrl &&
this.isIconReady(entry.iconUrl) &&
entry.legendVisible &&
entry.regionVisible &&
densityVisible &&
entry.nearbyVisible;
const hasLabel = !!entry.labelText;
@ -1103,23 +1073,11 @@ export class MapOl implements MapInterface {
const shouldRenderIcon =
!!entry.iconUrl &&
this.isIconReady(entry.iconUrl) &&
entry.legendVisible &&
entry.regionVisible &&
densityVisible &&
entry.nearbyVisible;
if (!shouldRenderIcon) {
this.syncFeatureIconCollisionVisible(
entry.feature,
false,
dirtyLayerKeys
);
this.syncFeatureLabelCollisionVisible(
entry.feature,
false,
dirtyLayerKeys
);
return;
}
@ -1138,9 +1096,18 @@ export class MapOl implements MapInterface {
}
const dynamicScale = this.getDynamicPointScale(currentZoom);
// 备注:图标碰撞盒适当小于视觉图标尺寸,允许相邻站点轻微贴近显示,
// 避免像“小浪底/三门峡”这类近点被过早裁掉成只剩一个点。
const iconCollisionSize = Math.max(14, 24 * dynamicScale);
// 备注:图标碰撞盒使用实际图标尺寸(从 iconSizeCache 获取),
// 再乘 0.9 略小于视觉尺寸,避免碰撞盒大于视觉图标导致误杀。
// 当缓存不可用时(图标尚未加载完),回退到固定 24px。
const cachedIconSize = this.iconSizeCache.get(entry.iconUrl);
const iconCollisionSize = cachedIconSize
? Math.max(
14,
Math.max(cachedIconSize.width, cachedIconSize.height) *
dynamicScale *
0.9
)
: Math.max(14, 24 * dynamicScale);
const iconPadding = 0;
const iconLeft = entry.pixelX - iconCollisionSize / 2 - iconPadding;
const iconRight = entry.pixelX + iconCollisionSize / 2 + iconPadding;
@ -1611,16 +1578,6 @@ export class MapOl implements MapInterface {
iconRect: { left: number; right: number; top: number; bottom: number },
labelRect: { left: number; right: number; top: number; bottom: number }
) {
// 标签避让图标时仅保留图标中心更小的保护区,并允许边缘接触不算碰撞,
// 避免像“三峡/黄龙滩”这类仅在边界轻微擦到时把文字过度压掉。
const inset = 4;
const padding = 0;
const narrowedIconRect = {
left: iconRect.left + inset,
right: iconRect.right - inset,
top: iconRect.top + inset,
bottom: iconRect.bottom - inset
};
// 标签块向上扩展避让边距(等价于给文字加"上边距"
// 让下方锚点文字不再紧贴上方锚点图标底部(含文字描边/图标视觉边缘)看着像重叠
const extendedLabelRect = {
@ -1631,10 +1588,10 @@ export class MapOl implements MapInterface {
};
return !(
narrowedIconRect.right + padding <= extendedLabelRect.left ||
narrowedIconRect.left - padding >= extendedLabelRect.right ||
narrowedIconRect.bottom + padding <= extendedLabelRect.top ||
narrowedIconRect.top - padding >= extendedLabelRect.bottom
iconRect.right <= extendedLabelRect.left ||
iconRect.left >= extendedLabelRect.right ||
iconRect.bottom <= extendedLabelRect.top ||
iconRect.top >= extendedLabelRect.bottom
);
}
@ -1776,10 +1733,37 @@ export class MapOl implements MapInterface {
}
private getFeatureEngRenderPriority(feature: Feature): number {
if (this.isFeatureEngAlarm(feature)) {
return 300;
const legendState = String(feature.get('anchoPointState') || '')
.trim()
.toLowerCase();
const isAlarm = this.isFeatureEngAlarm(feature);
// 建设状态权重:已建 > 在建 > 未建
let buildRank = 0;
if (legendState.includes('_ubuilt')) {
buildRank = 2;
} else if (legendState.includes('_nbuilt')) {
buildRank = 1;
} else if (legendState.includes('_built')) {
buildRank = 3;
}
return this.isFeatureEng(feature) ? 200 : 100;
let base = 100;
// 大型电站优先级高于中型电站,同规模内已建优先
if (legendState.startsWith('large_eng_')) {
base = 300 + buildRank * 30;
} else if (legendState.startsWith('mid_eng_')) {
base = 200 + buildRank * 30;
} else if (this.isFeatureEng(feature)) {
base = 150;
}
// 报警范围点仅在同规格内略微提升,不跨越大型/中型与建设状态层级
if (isAlarm) {
base += 10;
}
return base;
}
/**

View File

@ -45,6 +45,13 @@
</a-checkbox>
</a-form-item>
</a-col>
<a-col v-if="isMenu('shengTaiDiaoCha/luShengShengTaiDiaoCha')">
<a-form-item label="" name="">
<a-checkbox v-model:checked="formModel.fishSurveyZhuanZhi">
动物分布
</a-checkbox>
</a-form-item>
</a-col>
<a-col v-if="formModel.fishSurveyZhuanZhi">
<a-form-item label="" name="">
<a-date-picker
@ -71,6 +78,7 @@
:loading="fishSpeciesLoading"
:max-tag-count="1"
option-filter-prop="label"
@change="fishSurveyZhuanZhiParamsChange"
>
<a-select-option
v-for="item in fishSpeciesOptions"
@ -160,6 +168,8 @@ const DRAWER_STTP_CODES = [
'AIVD',
'DW',
'WE',
'WVA',
'FPRD'
];
const siteRangePicker = [
{ label: '全部', value: 'all' },
@ -354,9 +364,9 @@ const fetchFishSurveyOptions = async (
fishPointOptions.value = nextFishPointOptions;
fishSpeciesOptions.value = nextFishSpeciesOptions;
fishSurveyZhuanZhiParams.value.value = nextFishSpeciesOptions.map(
item => item.value
);
// fishSurveyZhuanZhiParams.value.value = nextFishSpeciesOptions.map(
// item => item.value
// );
syncFishAnchorPointSelection(nextFishPointOptions);
} finally {
fishPointLoading.value = false;
@ -552,6 +562,9 @@ const resetFishSurveyState = () => {
fishPointLoading.value = false;
fishSpeciesLoading.value = false;
syncFishPointLayerData([]);
// 退
modelStore.selectedFishSpecies = [];
modelStore.globalDrawerMode = 'basicInfo';
};
//
@ -702,6 +715,14 @@ const focusSelectedAnchorPoint = () => {
// sttpCode DRAWER_STTP_CODES
const handleAnchorPointChange = (value: string) => {
if (value == null) {
//
if (
formModel.value.fishSurveyZhuanZhi &&
fishSurveyZhuanZhiParams.value.value.length > 0
) {
modelStore.globalDrawerMode = 'speciesIntro';
return;
}
// 退
uiStore.setIsSearchActive(false);
uiStore.setGlobalDrawerOpen(false);
@ -727,6 +748,34 @@ const handleAnchorPointChange = (value: string) => {
return;
}
//
//
modelStore.globalDrawerMode = 'basicInfo';
uiStore.setIsSearchActive(true);
uiStore.setDrawerOpen(false);
setTimeout(() => {
uiStore.setGlobalDrawerOpen(true);
}, 300);
};
const fishSurveyZhuanZhiParamsChange = (value: string[]) => {
const selectedValues = Array.isArray(value) ? value : [];
//
if (!selectedValues.length) {
fishSurveyZhuanZhiParams.value.anchorPointSelect = null;
modelStore.selectedFishSpecies = [];
modelStore.globalDrawerMode = 'basicInfo';
uiStore.setIsSearchActive(false);
uiStore.setGlobalDrawerOpen(false);
setTimeout(() => {
uiStore.setDrawerOpen(true);
}, 300);
return;
}
const selectedSpecies = selectedValues
.map(v => fishSpeciesOptions.value.find(item => item.value === v))
.filter(Boolean) as { label: string; value: string }[];
//
modelStore.selectedFishSpecies = selectedSpecies;
modelStore.globalDrawerMode = 'speciesIntro';
uiStore.setIsSearchActive(true);
uiStore.setDrawerOpen(false);
setTimeout(() => {

View File

@ -7,6 +7,7 @@ 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 speciesIntroMod from '@/modules/rightSearchDrawer/speciesIntro/index.vue';
import { useModelStore } from '@/store/modules/model';
import GisView from '@/components/gis/GisView.vue';
@ -36,8 +37,15 @@ const isZhiNengPeiTu = computed(() => {
<div class="global-drawer-wrap">
<GlobalRightDrawer>
<basicInfoMod />
<monitoringTableMod />
<!-- 物种介绍模式鱼种下拉选择时展示动物/植物介绍 + 监测表格否则展示基础信息 + 监测表格 -->
<template v-if="modelStore.globalDrawerMode === 'speciesIntro'">
<speciesIntroMod />
<monitoringTableMod />
</template>
<template v-else>
<basicInfoMod />
<monitoringTableMod />
</template>
</GlobalRightDrawer>
</div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,967 @@
<template>
<div class="environmental-quality-container">
<!-- 搜索区域 -->
<a-form ref="formRef" :model="formState" layout="inline" class="search-form">
<a-space wrap>
<a-form-item label="所属基地" name="dataDimensionVal">
<a-select v-model:value="formState.dataDimensionVal" placeholder="请选择" style="width: 140px"
show-search :filter-option="filterBaseOption" option-filter-prop="label"
@change="handleBaseChange">
<a-select-option v-for="item in baseList" :key="item.wbsCode" :value="item.wbsCode"
:label="item.wbsName">
{{ item.wbsName }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="typeOne == 'all'" label="测站类型" name="dtinType">
<a-select v-model:value="formState.dtinType" placeholder="请选择" style="width: 120px" show-search
:filter-option="filterOption" option-filter-prop="label">
<a-select-option :value="''" label="全部">全部</a-select-option>
<a-select-option :value="'0'" label="自建水质站">自建水质站</a-select-option>
<a-select-option :value="'1'" label="国家水质站">国家水质站</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="时间维度" name="types">
<a-select v-model:value="formState.types" placeholder="请选择" style="width: 80px" show-search
:filter-option="filterOption" option-filter-prop="label">
<a-select-option value="year" label="年度">年度</a-select-option>
<a-select-option value="month" label="月度">月度</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="达标状态" name="sfdb">
<a-select v-model:value="formState.sfdb" placeholder="请选择" style="width: 160px" show-search
:filter-option="filterOption" option-filter-prop="label">
<a-select-option value="" label="全部">全部</a-select-option>
<a-select-option value="0" label="不达标">不达标</a-select-option>
<a-select-option value="1" label="达标">达标</a-select-option>
<a-select-option value="2" label="无目标等级/无数据">无目标等级/无数据</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="时间" name="dateValues">
<a-date-picker v-model:value="formState.dateValues"
:format="formState.types === 'month' ? 'YYYY-MM' : 'YYYY'" :picker="formState.types"
placeholder="请选择月份" style="width: 100px" :disabled-date="disabledMonthDate" />
</a-form-item>
<a-form-item label="断面名称" name="stnm">
<a-input v-model:value="formState.stnm" placeholder="请输入断面名称" style="width: 180px" allow-clear />
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">查询</a-button>
<a-button @click="handleReset">重置</a-button>
</a-space>
</a-form-item>
</a-space>
</a-form>
<!-- 图表和表格布局区域 -->
<div class="content-layout">
<!-- 左侧饼图区域 -->
<div class="chart-section">
<a-spin :spinning="pieLoading" tip="加载中...">
<div ref="pieChartRef" class="pie-chart-container"></div>
<div v-if="!showPieChart && !pieLoading" class="empty-overlay">
<a-empty description="暂无数据" />
</div>
</a-spin>
</div>
<!-- 右侧表格区域 -->
<div class="table-section">
<BasicTable ref="tableRef" :scrollY="360" :columns="columns" :list-url="wqGetKendoList"
:search-params="{ sort: sortConfig }" :transform-data="customTransform">
<template #action="{ record }">
<!-- 操作列 -->
<a @click="handleViewDetail(record)" class="text-link">
查看详情
</a>
</template>
</BasicTable>
</div>
</div>
<!-- 详情弹窗 -->
<a-modal v-model:open="detailModalVisible" :title="`${detailData.stnm || ''} 详细信息`" width="1536px" :footer="null"
@cancel="handleDetailModalClose">
<HJMZDTwoLays v-if="detailModalVisible" :tc="{
stcd: detailData.stcd || '',
startTime: detailData.tm,
endTime: (typeof detailData.tm === 'string' ? detailData.tm.split(' ')[0] : dayjs(detailData.tm).format('YYYY-MM-DD')) + ' 23:59:59',
sfdb: formState.sfdb || ''
}" :calculateFlag="{ calculateFlag: 0 }" />
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onBeforeUnmount, watch, nextTick, h } from 'vue'
import { message } from 'ant-design-vue'
import * as echarts from 'echarts'
import dayjs, { Dayjs } from 'dayjs'
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
import BasicTable from '@/components/BasicTable/index.vue'
import { wqGetKendoList } from '@/api/sz'
import HJMZDTwoLays from "./HJMZDTwoLays.vue"
import { useDraggable } from '@/utils/drag';
// dayjs
dayjs.extend(isSameOrAfter)
// Props
const props = defineProps<{
datas?: Array<{ wbsCode: string; wbsName: string }> //
type?: string //
modalData?: string //
dateValue?: string | Dayjs //
dataDimensionVal?: string //
buildType?: string | number //
typeOne: string //
baseid: string
}>()
// Refs
const formRef = ref()
const tableRef = ref()
const pieChartRef = ref<HTMLElement | null>(null)
let pieChartInstance: echarts.ECharts | null = null
// State
const baseid = ref(props.baseid || '')
const typeOne = ref(props.typeOne || '')
const originalMonth = ref<string>('') //
const formState = reactive({
dataDimensionVal: props.dataDimensionVal || '',
dtinType: props.buildType.toString() || '',
types: props.type || 'month',
sfdb: '',
dateValues: props.dateValue ? (typeof props.dateValue === 'string' ? dayjs(props.dateValue, props.type === 'month' ? 'YYYY-MM' : 'YYYY') : props.dateValue) : dayjs(),
stnm: ''
})
const kong = ref(true)
const baseList = computed(() => props.datas || [])
const pieLoading = ref(false)
const showPieChart = ref(true)
const pieCode = ref<string[]>([]) //
const displayTotal = ref<number>(0) //
//
const detailModalVisible = ref(false)
const detailData = ref<any>({})
const mapModalVisible = ref(false)
const mapModalData = ref<any>({})
useDraggable(detailModalVisible, { boundary: true, resetOnOpen: true });
//
const sortConfig = computed(() => [
{ field: 'rstcdStepSort', dir: 'asc' },
{ field: 'stnm', dir: 'asc' },
{ field: 'tm', dir: 'desc' }
])
//
const customTransform = (res: any) => {
return {
records: res?.data?.data || [],
total: res?.data?.total || 0
}
}
//
const getStnmMergeInfo = (records: any[]) => {
if (!records || records.length === 0) return {}
const mergeInfo: Record<number, { rowSpan: number }> = {}
let i = 0
while (i < records.length) {
const currentStnm = records[i].stnm
let rowSpan = 1
//
let j = i + 1
while (j < records.length && records[j].stnm === currentStnm) {
rowSpan++
j++
}
// rowSpan
mergeInfo[i] = { rowSpan }
// rowSpan 0
for (let k = i + 1; k < j; k++) {
mergeInfo[k] = { rowSpan: 0 }
}
i = j
}
return mergeInfo
}
//
const columns = [
{
title: '日期',
dataIndex: 'tm',
key: 'tm',
width: 120,
customRender: ({ record }: any) => {
const date = record.tm
if (!date) return '-'
if (typeof date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
return date
}
if (typeof date === 'string' && date.includes(' ')) {
return date.split(' ')[0]
}
return date
}
},
{
title: '断面名称',
dataIndex: 'stnm',
key: 'stnm',
width: 200,
ellipsis: true,
customCell: (record: any, rowIndex: number) => {
const tableData = tableRef.value?.tableData || []
const mergeInfo = getStnmMergeInfo(tableData)
return mergeInfo[rowIndex] || { rowSpan: 1 }
}
},
{
title: '测站类型',
dataIndex: 'sttypeName',
key: 'sttypeName',
width: 80
},
{
title: '达标状态',
dataIndex: 'sfdbName',
key: 'sfdbName',
width: 80,
customRender: ({ record }: any) => {
const sfdb = record.sfdbName
let color = '#000000'
if (sfdb === '达标') {
color = '#008000' // - 绿
} else if (sfdb === '不达标') {
color = '#FF0000' // -
}
return h('span', { style: { color } }, record.sfdbName || '-')
}
},
{
title: '水质要求',
dataIndex: 'wwqtgName',
key: 'wwqtgName',
width: 80
},
{
title: '当前水质',
dataIndex: 'wqGrdName',
key: 'wqGrdName',
width: 80
},
{
title: '去年同期水质',
dataIndex: 'beforeWqGrdName',
key: 'beforeWqGrdName',
width: 140
},
{
title: '操作',
key: 'action',
width: 100,
fixed: 'right',
slots: { customRender: 'action' }
}
]
//
const disabledMonthDate = (current: Dayjs) => {
if (!current) return false
const now = dayjs()
return current.isAfter(now, 'month')
}
//
const filterBaseOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
//
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
//
const fetchPieData = async () => {
pieLoading.value = true
try {
//
const timeFormat = formState.types === 'month' ? 'YYYY-MM' : 'YYYY'
const startTime = formState.dateValues ? formState.dateValues.format(timeFormat) : ''
if (!startTime || !formState.dataDimensionVal) {
showPieChart.value = false
return
}
//
const filters: any[] = [
formState.dataDimensionVal != 'all' ? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: typeOne.value == 'all' ? formState.dataDimensionVal : baseid.value
} : null,
formState.types === 'month' ? {
field: 'drMonth',
operator: 'eq',
dataType: 'string',
value: startTime
} : {
field: 'drYear',
operator: 'eq',
dataType: 'string',
value: startTime
},
{
field: 'TYPE',
operator: 'eq',
dataType: 'string',
value: 'DAY'
},
typeOne.value != 'all' ? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: formState.dataDimensionVal
} : null,
pieCode.value.length > 0 ? {
field: 'sfdb',
operator: 'in',
dataType: 'string',
value: pieCode.value
} : (formState.sfdb ? {
field: 'sfdb',
operator: 'eq',
dataType: 'string',
value: formState.sfdb
} : null),
formState.stnm ? {
field: 'stnm',
operator: 'contains',
dataType: 'string',
value: formState.stnm
} : null,
typeOne.value == 'all' && formState.dtinType != '' ? {
field: 'dtinType',
operator: 'eq',
dataType: 'string',
value: formState.dtinType
} : null
].filter(Boolean)
const params = {
filter: {
logic: 'and',
filters
},
group: [
{
"dir": "des",
"field": "sfdb"
}
]
}
// API
const res = await wqGetKendoList(params)
let data = res?.data?.data || res?.data || []
if (data.length === 0) {
//
if (pieChartInstance) {
pieChartInstance.dispose()
pieChartInstance = null
}
//
displayTotal.value = 0
pieCode.value = []
showPieChart.value = false
return
}
// -
const dataSource = data.map((item: any) => {
const name = item.keyName || 'null'
// -绿--
let color = '#d9d9d9'
if (name === '达标') {
color = '#52c41a' // 绿
} else if (name === '不达标') {
color = '#ff4d4f' //
}
return {
name: name,
value: item.count,
key: item.keyName === '达标' ? 'DB_STATUS_1' : item.keyName === '不达标' ? 'DB_STATUS_0' : 'CURNODATA',
keyData: item?.key,
itemStyle: {
color: color
}
}
})
// 使""
const filteredDataSource = dataSource
if (filteredDataSource.length === 0) {
showPieChart.value = false
return
}
showPieChart.value = true
//
await nextTick()
initPieChart(filteredDataSource)
} catch (error) {
console.error('获取饼图数据失败:', error)
message.error('获取饼图数据失败')
showPieChart.value = false
} finally {
pieLoading.value = false
}
}
//
const initPieChart = (data: any[]) => {
if (!pieChartRef.value) return
//
if (pieChartInstance) {
pieChartInstance.dispose()
pieChartInstance = null
}
pieChartInstance = echarts.init(pieChartRef.value)
// displayTotal
const total = data.reduce((sum, item) => sum + item.value, 0)
displayTotal.value = total
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} 次 ({d}%)'
},
legend: {
orient: 'vertical',
right: '10%',
top: '10%',
data: data.map(item => item.name)
},
title: [
{
text: displayTotal.value.toString(),
left: '39%',
top: '38%',
textAlign: 'center',
textStyle: {
fontSize: 20,
fontWeight: 'bold',
color: '#2f6b98'
}
},
{
text: '监测总次数',
left: '39%',
top: '44%',
textAlign: 'center',
textStyle: {
fontSize: 13,
color: '#333333',
fontWeight: '500'
}
},
{
text: '(次)',
left: '39%',
top: '49%',
textAlign: 'center',
textStyle: {
fontSize: 13,
color: '#333333',
fontWeight: '500'
}
}
],
series: [
{
name: '断面总数量',
type: 'pie',
radius: ['25%', '35%'],
center: ['40%', '45%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}'
},
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold'
}
},
data: data
}
]
}
pieChartInstance.setOption(option)
// -
pieChartInstance.on('legendselectchanged', (params: any) => {
kong.value = false
const selectedKeys = Object.keys(params.selected).filter(key => params.selected[key])
const allFalse = Object.values(params.selected).every(value => value === false)
//
if (allFalse) {
// 0
displayTotal.value = 0
} else {
//
displayTotal.value = data
.filter(item => selectedKeys.includes(item.name))
.reduce((sum, item) => sum + item.value, 0)
}
// title
pieChartInstance.setOption({
title: [
{
text: displayTotal.value.toString()
}
]
})
const selectedCodes = data
.filter(item => selectedKeys.includes(item.name))
.map(item => item.keyData)
pieCode.value = allFalse ? ['5'] : selectedCodes
//
handleSearch()
})
}
//
const handleBaseChange = () => {
formState.stnm = ''
handleSearch()
}
//
const handleSearch = () => {
const timeFormat = formState.types === 'month' ? 'YYYY-MM' : 'YYYY'
const startTime = formState.dateValues ? formState.dateValues.format(timeFormat) : ''
// console.log( typeOne.value )
// console.log( formState.dtinType )
// console.log(typeOne.value == 'all' && formState.dtinType !='')
//
const filters: any[] = [
formState.dataDimensionVal != 'all' ? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: typeOne.value == 'all' ? formState.dataDimensionVal : baseid.value
} : null,
formState.types === 'month' ? {
field: 'drMonth',
operator: 'eq',
dataType: 'string',
value: startTime
} : {
field: 'drYear',
operator: 'eq',
dataType: 'string',
value: startTime
},
{
field: 'TYPE',
operator: 'eq',
dataType: 'string',
value: 'DAY'
},
typeOne.value != 'all' ? {
field: 'rstcd',
operator: 'eq',
dataType: 'string',
value: formState.dataDimensionVal
} : null,
pieCode.value.length > 0 ? {
field: 'sfdb',
operator: 'in',
dataType: 'string',
value: pieCode.value
} : (formState.sfdb ? {
field: 'sfdb',
operator: 'eq',
dataType: 'string',
value: formState.sfdb
} : null),
formState.stnm ? {
field: 'stnm',
operator: 'contains',
dataType: 'string',
value: formState.stnm
} : null,
typeOne.value == 'all' && formState.dtinType != '' ? {
field: 'dtinType',
operator: 'eq',
dataType: 'string',
value: formState.dtinType
} : null
].filter(Boolean)
const filter = {
logic: 'and',
filters
}
//
tableRef.value?.getList(filter)
if (kong.value) {
//
fetchPieData()
}
kong.value = true
}
//
const handleReset = () => {
formState.dataDimensionVal = props.dataDimensionVal || ''
formState.dtinType = ''
formState.types = props.type || 'month'
formState.sfdb = ''
formState.dateValues =props.dateValue ? (typeof props.dateValue === 'string' ? dayjs(props.dateValue, props.type === 'month' ? 'YYYY-MM' : 'YYYY') : props.dateValue) : dayjs(),
formState.stnm = ''
pieCode.value = []
handleSearch()
}
//
const handleViewDetail = (record: any) => {
detailData.value = { ...record }
detailModalVisible.value = true
}
//
const handleDetailModalClose = () => {
detailModalVisible.value = false
detailData.value = {}
}
//
const handleMapModalClose = () => {
mapModalVisible.value = false
mapModalData.value = {}
}
//
watch(
() => formState.dateValues,
() => {
handleSearch()
}
)
// props.dataDimensionVal
watch(
() => props.dataDimensionVal,
(newVal) => {
//
formState.dataDimensionVal = newVal || ''
formState.dtinType = props.buildType.toString() || ''
formState.sfdb = ''
formState.stnm = ''
pieCode.value = []
//
nextTick(() => {
handleSearch()
})
}
)
// props.type
watch(
() => props.type,
(newType) => {
if (!newType || newType === formState.types) return
//
const currentValue: any = formState.dateValues
if (currentValue) {
if (newType === 'year') {
//
if (typeof currentValue.year === 'function') {
// dayjs
originalMonth.value = String(currentValue.month() + 1).padStart(2, '0')
const year = currentValue.year()
formState.dateValues = dayjs(String(year), 'YYYY')
} else if (typeof currentValue === 'string') {
//
const monthMatch = currentValue.match(/^(\d{4})-(\d{2})/)
if (monthMatch) {
originalMonth.value = monthMatch[2]
formState.dateValues = dayjs(monthMatch[1], 'YYYY')
} else {
const yearMatch = currentValue.match(/^(\d{4})/)
if (yearMatch) {
formState.dateValues = dayjs(yearMatch[1], 'YYYY')
}
}
}
} else if (newType === 'month') {
//
const monthToUse = originalMonth.value || '01'
if (typeof currentValue.year === 'function') {
const year = currentValue.year()
formState.dateValues = dayjs(`${year}-${monthToUse}`, 'YYYY-MM')
} else if (typeof currentValue === 'string') {
const yearMatch = currentValue.match(/^(\d{4})/)
if (yearMatch) {
formState.dateValues = dayjs(`${yearMatch[1]}-${monthToUse}`, 'YYYY-MM')
}
}
}
}
//
formState.types = newType
//
nextTick(() => {
handleSearch()
})
}
)
// props.typeOne , state
watch(
() => props.typeOne,
(newVal) => {
typeOne.value = newVal
//
}
)
// props.baseid state
watch(
() => props.baseid,
(newVal) => {
baseid.value = newVal || ''
}
)
//
watch(
() => formState.types,
(newType, oldType) => {
if (!formState.dateValues || newType === oldType) return
//
const currentValue: any = formState.dateValues
if (newType === 'year') {
//
if (currentValue && typeof currentValue.year === 'function') {
// dayjs
originalMonth.value = String(currentValue.month() + 1).padStart(2, '0')
const year = currentValue.year()
formState.dateValues = dayjs(String(year), 'YYYY')
} else if (typeof currentValue === 'string') {
//
const monthMatch = currentValue.match(/^(\d{4})-(\d{2})/)
if (monthMatch) {
originalMonth.value = monthMatch[2]
formState.dateValues = dayjs(monthMatch[1], 'YYYY')
} else {
const yearMatch = currentValue.match(/^(\d{4})/)
if (yearMatch) {
formState.dateValues = dayjs(yearMatch[1], 'YYYY')
}
}
}
} else if (newType === 'month') {
//
const monthToUse = originalMonth.value || '01'
if (currentValue && typeof currentValue.year === 'function') {
const year = currentValue.year()
formState.dateValues = dayjs(`${year}-${monthToUse}`, 'YYYY-MM')
} else if (typeof currentValue === 'string') {
const yearMatch = currentValue.match(/^(\d{4})/)
if (yearMatch) {
formState.dateValues = dayjs(`${yearMatch[1]}-${monthToUse}`, 'YYYY-MM')
}
}
}
//
nextTick(() => {
handleSearch()
})
}
)
//
onMounted(async () => {
await nextTick()
handleSearch()
// resize
window.addEventListener('resize', () => {
pieChartInstance?.resize()
})
})
//
onBeforeUnmount(() => {
pieChartInstance?.dispose()
pieChartInstance = null
window.removeEventListener('resize', () => {
pieChartInstance?.resize()
})
})
//
defineExpose({
handleSearch
})
</script>
<style scoped lang="scss">
.environmental-quality-container {
height: 100%;
display: flex;
flex-direction: column;
.search-form {
background: #fff;
border-radius: 4px;
padding: 0px 0px 16px 0px;
:deep(.ant-space) {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
:deep(.ant-form-item) {
margin-bottom: 0;
}
}
.content-layout {
flex: 1;
display: flex;
gap: 16px;
overflow: hidden;
.chart-section {
width: 35%;
min-width: 300px;
background: #fff;
border-radius: 4px;
padding: 16px;
position: relative;
.pie-chart-container {
width: 100%;
height: 482px;
}
.empty-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.9);
z-index: 10;
}
}
.table-section {
flex: 1;
background: #fff;
border-radius: 4px;
padding: 16px;
overflow: hidden;
}
}
.text-link {
color: #2f6b98;
&:hover {
color: #40a9ff;
}
}
.modal-content {
padding: 20px;
max-height: 600px;
overflow-y: auto;
pre {
background: #f5f5f5;
padding: 12px;
border-radius: 4px;
font-size: 12px;
margin-top: 16px;
}
}
}
:deep(.ant-spin-nested-loading) {
height: 100%;
}
:deep(.ant-spin-container) {
height: 100%;
}
</style>

View File

@ -0,0 +1,595 @@
<template>
<div>
<!-- 搜索表单 - 使用 Ant Design Vue 原生组件 -->
<a-form
ref="formRef"
:model="formState"
layout="inline"
@finish="onFinish"
style="margin-bottom: 16px"
>
<a-form-item label="达标状态" name="sfdb">
<a-select
v-model:value="formState.sfdb"
placeholder="请选择"
allow-clear
style="width: 160px"
>
<a-select-option value="">全部</a-select-option>
<a-select-option :value="'0'">不达标</a-select-option>
<a-select-option :value="'1'">达标</a-select-option>
<a-select-option :value="'2'">无目标等级/无数据</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" html-type="submit">查询</a-button>
<Tooltip title="重置">
<a-button @click="handleReset">重置</a-button>
</Tooltip>
</a-space>
</a-form-item>
</a-form>
<!-- 数据表格 -->
<BasicTable
ref="tableRef"
:columns="columns"
:list-url="
props.calculateFlag?.calculateFlag == 1
? dataGetKendoListCust
: NodataGetKendoListCust
"
:scrollY="500"
:search-params="{ sort: [{ field: 'tm', dir: 'desc' }] }"
:transform-data="customTransform"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted, h, nextTick } from 'vue';
import { omit } from 'lodash';
import { Button, Tooltip, Popover } from 'ant-design-vue';
import BasicTable from '@/components/BasicTable/index.vue';
import {
ruleGetKendoListCust,
noAuthGetKendoListCust,
stbprpGetStbprpYsByStcd,
dataGetKendoListCust,
NodataGetKendoListCust
} from '@/api/sz';
// ==================== Props ====================
interface Props {
tc: {
stcd: string;
startTime: string;
endTime: string;
sfdb: string;
};
calculateFlag?: {
calculateFlag: number;
};
}
const props = defineProps<Props>();
// ==================== ====================
const formRef = ref();
const tableRef = ref();
const col = ref<any[]>([]);
// sfdb ref
const formState = reactive({
sfdb: '1'
});
// ==================== ====================
const columnList = [
{
dataIndexRaw: '',
key: 'tm',
title: '时间',
dataIndex: 'tm',
children: [],
visible: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataType: 'date',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
sorter: true,
multiLink: false,
width: 160
},
{
dataIndexRaw: '',
key: 'wwqtg',
title: '水质要求',
dataIndex: 'wwqtgName',
children: [],
visible: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
sorter: true,
disabledDate: true,
multiLink: false,
width: 100
},
{
dataIndexRaw: '',
key: 'wqGrd',
title: '水质等级',
dataIndex: 'wqgrdName',
children: [],
visible: true,
fixed: '',
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
fromType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
sorter: true,
disabledDate: true,
multiLink: false,
width: 100
},
{
dataIndexRaw: 'sfdb',
key: 'sfdb',
title: '是否达标',
dataIndex: 'sfdbName',
children: [],
visible: true,
fixed: '',
ellipsis: true,
wrap: false,
// TODO: - BasicTable dictType/dictCode
// customRender
dictType: 'dataDict',
dictCode: 'sd_wq_r.sfdb',
dictDatas: {},
fromType: 'select',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
sorter: true,
disabledDate: true,
multiLink: false,
width: 100,
// Vue3 customRender
customRender: ({ record }: any) => {
// TODO:
// return getDictLabel('sd_wq_r.sfdb', record.sfdbName)
return record.sfdbName; //
}
}
];
// ==================== ====================
const columns = computed(() => {
if (!col.value.length) return columnList;
return [...columnList, ...col.value];
});
// ==================== ====================
const tableFilter = computed(() => {
let res: any = {
stcd: props.tc.stcd,
sfdb: formState.sfdb === '2' ? '' : formState.sfdb,
startTime: props.tc.startTime,
endTime: props.tc.endTime
};
if (res.sfdb === '') res = omit(res, 'sfdb');
return res;
});
// ==================== ====================
//
const onFinish = () => {
const filters: any[] = [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: tableFilter.value.stcd
},
formState.sfdb
? {
field: 'sfdb',
operator: 'eq',
dataType: 'string',
value: formState.sfdb
}
: null,
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: tableFilter.value.startTime
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: tableFilter.value.endTime
}
].filter(Boolean);
const filter = {
logic: 'and',
filters
};
//
tableRef.value?.getList(filter);
};
//
const handleReset = () => {
formState.sfdb = '1';
onFinish();
};
// TODO: - API
const getCol = async (params: any) => {
try {
const lo = await columYaoshu(params);
col.value = lo || [];
} catch (error) {
console.error('获取列配置失败:', error);
}
};
// TODO: -
const columYaoshu = async (params: any) => {
// API 1:
let params3 = {
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: params?.stcd
}
]
}
};
const MaxData = await ruleGetKendoListCust(params3);
// API 2:
let params2 = {
filter: {
logic: 'and',
filters: [
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: params?.stcd
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: params?.endTime
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: params?.startTime
}
]
}
};
const noAuthRes = await noAuthGetKendoListCust(params2);
const noAuthResData = noAuthRes?.data?.data || [];
// API 3:
let params1 = {
stcd: params?.stcd,
tbCode: 'WQ_R',
endTime: params.endTime,
startTime: params.startTime
};
const res = await stbprpGetStbprpYsByStcd(params1);
if (res?.data?.data) {
const processedColumns = res.data.data
.filter((item: any) => item.enable === '1')
.map((item: any) => {
const _t = item.ysShowName;
const reg = /[(][^()]+[)]/;
// TODO:
const _u = getUnitConfigByCode('WQ_R', item.ys)?.unit;
if (_u) {
item.ysShowName = reg.test(_t)
? _t.replace(reg, `(${_u})`)
: `${_t}(${_u})`;
}
let showValue = '';
if (noAuthResData.length) {
const { max, min } = noAuthResData[0];
const ys = item.ys;
const maxValue = max.filter((e: any) => e[ys]);
const minValue = min.filter((e: any) => e[ys]);
if (maxValue.length && minValue.length) {
showValue = `(${minValue[0]?.[ys]}-${maxValue[0]?.[ys]})`;
} else {
if (minValue.length) {
showValue = `>${minValue[0]?.[ys]}`;
}
if (maxValue.length) {
showValue = `<${maxValue[0]?.[ys]}`;
}
}
}
return {
dataIndexRaw: '',
key: item.ys.toLowerCase(),
// Vue3 title VNode
title: () =>
h('div', [
h(
'p',
{
style: {
overflow: 'hidden',
textOverflow: 'ellipsis',
wordBreak: 'keep-all'
}
},
item.ysShowName
),
h('p', showValue)
]),
dataIndex: item.ys.toLowerCase(),
children: [],
visible: true,
fixed: '',
sorter: true,
ellipsis: true,
wrap: false,
dictType: '',
dictCode: '',
dictDatas: {},
// Vue3 使 customRender render
customRender: ({ record }: any) => {
// TODO: MaxData
const datalist = MaxData?.data?.data;
const dynamicDataIndex = item.ys;
const dynamicDataName = item.ysShowName;
const matchedItem = datalist.find(
(m: any) => m.ys === dynamicDataIndex
);
//
if (
matchedItem &&
(matchedItem.isAsc == 1
? matchedItem?.maxVal
: matchedItem.minVal) &&
record[dynamicDataIndex?.toLowerCase()] &&
((matchedItem.isAsc == 1 &&
record[dynamicDataIndex?.toLowerCase()] >
matchedItem?.maxVal) ||
(matchedItem.isAsc != 1 &&
record[dynamicDataIndex?.toLowerCase()] < matchedItem.minVal))
) {
// Vue3 使 h() VNode
return h(
Popover,
{
content: h(
'div',
`预警标准: ${
matchedItem.isAsc === 1
? `>${matchedItem.maxVal}`
: `<${matchedItem.minVal}`
}`
),
trigger: 'hover'
},
() =>
h(
'span',
{
style: { color: 'red', cursor: 'pointer' }
// TODO:
},
transUnitRender(
record[dynamicDataIndex?.toLowerCase()],
'WQ_R',
dynamicDataIndex
)
)
);
}
//
// TODO:
return transUnitRender(
record[dynamicDataIndex?.toLowerCase()],
'WQ_R',
dynamicDataIndex
);
},
fromType: '',
dataType: '',
dataFormat: '',
selectMode: '',
unit: '',
remark: '',
rules: '',
ruleTips: '',
setting: {},
chartColor: '',
elementCode: '',
disabledDate: true,
multiLink: false
};
});
console.log(processedColumns);
onFinish();
return processedColumns || [];
}
};
// TODO: -
// ==================== ====================
/**
* 根据表代码和要素代码获取单位配置
* @param tbCode 表代码 ( 'WQ_R')
* @param ys 要素代码 ( 'DOX', 'CODCR')
* @returns 单位配置对象 { unit: string }
*/
const getUnitConfigByCode = (tbCode: string, ys: string) => {
// TODO:
const unitConfig: Record<string, Record<string, { unit: string }>> = {
WQ_R: {
PH: { unit: '' },
DOX: { unit: 'mg/L' },
CODMN: { unit: 'mg/L' },
CODCR: { unit: 'mg/L' },
NH3N: { unit: 'mg/L' },
TP: { unit: 'mg/L' },
TN: { unit: 'mg/L' },
WTMP: { unit: '℃' },
COND: { unit: 'μS/cm' },
CLARITY: { unit: 'm' }
}
};
return unitConfig[tbCode]?.[ys] || { unit: '' };
};
/**
* 带单位的值渲染根据要素类型设置不同小数位数不带单位
* @param value 数值
* @param tbCode 表代码
* @param ys 要素代码
* @returns 格式化后的字符串不带单位
*/
const transUnitRender = (value: any, tbCode: string, ys: string) => {
if (value === null || value === undefined || value === '') return '-';
const numValue = Number(value);
if (isNaN(numValue)) return String(value);
//
let formattedValue: string;
if (ys === 'WTMP' || ys === 'PH') {
// PH
formattedValue = numValue.toFixed(1);
} else if (ys === 'COND') {
//
formattedValue = Math.round(numValue).toString();
} else {
//
formattedValue = numValue.toFixed(2);
}
return formattedValue;
};
/**
* 纯单位转换不带渲染
* @param value 数值
* @param tbCode 表代码
* @param ys 要素代码
* @returns 转换后的数值
*/
const transUnit = (value: any, tbCode: string, ys: string) => {
if (value === null || value === undefined) return null;
return Number(value);
};
// ==================== ====================
// tc formState.sfdb
// watch(
// () => [props.tc.stcd, props.tc.startTime, props.tc.endTime, formState.sfdb],
// ([stcd, startTime, endTime]) => {
// if (!stcd) {
// return;
// }
// //
// tableRef.value.tableData = [];
// getCol({ stcd, startTime, endTime });
// },
// { immediate: false } // onMounted
// );
//
onMounted(() => {
nextTick(() => {});
getCol({
stcd: props.tc.stcd,
startTime: props.tc.startTime,
endTime: props.tc.endTime
});
});
//
const customTransform = (res: any) => {
return {
records: res?.data?.data || [],
total: res?.data?.total || 0
};
};
</script>
<style scoped>
/* 如需添加样式,请在此处编写 */
</style>

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="动物救助情况">
<SidePanelItem title="动物救助情况">
<!-- Loading 状态 -->
<div v-if="loading" class="loading-container">
<a-spin />
@ -164,7 +164,7 @@ const getData = async () => {
const res = await wvaGetKendoListCust(params);
if (res?.data?.data?.length > 0) {
// imgPath null
const filteredData = res.data.data
const filteredData = res.data.data;
originalMediaData.value = filteredData.map((item: any) => {
return {
url: `${baseUrl}?${item.fid}&view=jpg`,
@ -205,23 +205,23 @@ onUnmounted(() => {
</script>
<style lang="scss" scoped>
// Loading
.loading-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height:290px;
}
// Loading
.loading-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 290px;
}
//
.empty-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 290px;
}
//
.empty-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 290px;
}
.container {
width: 100%;
height: 290px;
@ -230,8 +230,6 @@ onUnmounted(() => {
position: relative;
overflow: hidden; //
//
.carousel-track {
display: flex; //
@ -285,7 +283,7 @@ onUnmounted(() => {
}
}
}
:deep(.ant-empty-description){
text-indent:0em !important;
:deep(.ant-empty-description) {
text-indent: 0em !important;
}
</style>

View File

@ -8,7 +8,6 @@
<template>
<SidePanelItem
title="出入库水温"
:moreSelect="select"
:scopeDate="scopeDate"
@update-values="handlePanelChange1"
>
@ -573,9 +572,10 @@ const handleChartClick = (params: any) => {
};
//
stationName.value = select.value.options?.find(
(item: any) => item.value === select.value.value
)?.title || '未知站点';
stationName.value =
select.value.options?.find(
(item: any) => item.value === select.value.value
)?.title || '未知站点';
//
modalVisible.value = true;
@ -597,11 +597,10 @@ const handleModalClose = () => {
*/
const handlePanelChange1 = (data: any, type: string) => {
// TODO:
if (data.moreSelect && data.scopeDate) {
select.value.value = data.moreSelect;
paramsOne.value.tm = data.scopeDate;
fetchChartData();
}
if (data.scopeDate) {
paramsOne.value.tm = data.scopeDate;
fetchChartData();
}
if (data?.selectedNodeExtra?.lttd && data?.selectedNodeExtra?.lgtd) {
const mapClass = MapClass.getInstance();
mapClass.flyTopanto(

View File

@ -52,18 +52,18 @@
:search-params="searchParams"
:transform-data="customTransform"
>
<template #coenvwState="{ column, record }">
<template #coenvwState="{ record }">
<a-tag :color="record.coenvwState > 0 ? 'green' : '#999'">
{{ record.coenvwState > 0 ? '在线' : '离线' }}
</a-tag>
</template>
<template #dvtp="{ column, record }">
<template #dvtp="{ record }">
{{ dvtpText(record.dvtp) }}
</template>
<template #stindxForEng="{ column, record }">
<template #stindxForEng="{ record }">
{{ record.dvtp == 2 ? '生态流量' : '出库流量' }}
</template>
<template #action="{ column, record }">
<template #action="{ record }">
<a @click="handleViewDetail(record, 'detail')" class="text-link">
查看详情
</a>
@ -73,10 +73,14 @@
</template>
<script lang="ts" setup>
import dayjs from 'dayjs';
import { ref, computed, onMounted, h } from 'vue';
import { Tooltip } from 'ant-design-vue';
import { QuestionCircleOutlined } from '@ant-design/icons-vue';
import { vmsstbprptGetKendoList,overviewvmsstbprptGetKendoList } from '@/api/home';
import {
vmsstbprptGetKendoList,
overviewvmsstbprptGetKendoList
} from '@/api/home';
import BasicTable from '@/components/BasicTable/index.vue';
import { useModelStore } from '@/store/modules/model';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
@ -142,11 +146,25 @@ const columns = computed(() => {
title: '建成时间',
dataIndex: 'jcdt',
width: '150px',
sort: true
sort: true,
customRender: ({ text }) =>
text ? dayjs(text).format('YYYY-MM-DD') : '-'
},
{
key: 'coenvwState',
title: h(Tooltip, { title: '最近1天有监测数据为在线否则为离线' }, { default: () => h('span', null, ['监测状态', h(QuestionCircleOutlined, { style: { marginLeft: '4px', color: '#000' } })]) }),
title: h(
Tooltip,
{ title: '最近1天有监测数据为在线否则为离线' },
{
default: () =>
h('span', null, [
'监测状态',
h(QuestionCircleOutlined, {
style: { marginLeft: '4px', color: '#000' }
})
])
}
),
dataIndex: 'coenvwState',
width: '120px',
slots: { customRender: 'coenvwState' }
@ -190,7 +208,9 @@ const columns = computed(() => {
title: '建成时间',
dataIndex: 'jcdt',
width: '150px',
sort: true
sort: true,
customRender: ({ text }) =>
text ? dayjs(text).format('YYYY-MM-DD') : '-'
},
{
key: 'stindx',
@ -204,7 +224,19 @@ const columns = computed(() => {
if (tabIndex.value !== '4') {
baseColumns.push({
key: 'coenvwState',
title: h(Tooltip, { title: '最近1天有监测数据为在线否则为离线' }, { default: () => h('span', null, ['监测状态', h(QuestionCircleOutlined, { style: { marginLeft: '4px', color: '#999' } })]) }),
title: h(
Tooltip,
{ title: '最近1天有监测数据为在线否则为离线' },
{
default: () =>
h('span', null, [
'监测状态',
h(QuestionCircleOutlined, {
style: { marginLeft: '4px', color: '#999' }
})
])
}
),
dataIndex: 'coenvwState',
width: '120px',
slots: { customRender: 'coenvwState' }
@ -315,7 +347,7 @@ const getRstcdData = async () => {
select: ['stcd', 'stnm']
};
const res = await overviewvmsstbprptGetKendoList(parmas);
rstcdOptions.value = res.data.data.map((item) => {
rstcdOptions.value = res.data.data.map(item => {
return {
value: item.stcd,
label: item.stnm
@ -323,7 +355,7 @@ const getRstcdData = async () => {
});
};
const handleBaseChange = () => {
searchData.value.stcd = null
searchData.value.stcd = null;
getRstcdData();
// handleSearch();
};
@ -344,7 +376,7 @@ const handleSearch = () => {
value: searchData.value.baseId
});
}
if(searchData.value.stcd){
if (searchData.value.stcd) {
filters.push({
field: 'stcd',
operator: 'eq',
@ -507,7 +539,7 @@ const handleReset = () => {
stnm: null,
stcd: null
};
getRstcdData()
getRstcdData();
handleSearch();
};
@ -622,7 +654,7 @@ onMounted(() => {
value: element.wbsCode
});
});
getRstcdData()
getRstcdData();
handleSearch();
});
</script>

View File

@ -0,0 +1,234 @@
<template>
<div class="lsstjk-table">
<!-- 表格 -->
<div class="table-wrapper">
<BasicTable
ref="tableRef"
:columns="columns"
:scroll-y="500"
:scroll-x="800"
:list-url="listUrl"
:search-params="searchParams"
:transform-data="customTransform"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch, onMounted } from 'vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { getVmsstbprptList, getTetSpecList } from '@/api/stdc';
import dayjs from 'dayjs';
// ==================== Props ====================
const props = defineProps<{
selectDateTime?: any;
orderIndex?: string;
hbrvcd?: string;
}>();
// ==================== ====================
const tableRef = ref();
const yearValue = ref(
props.selectDateTime ? dayjs(props.selectDateTime) : dayjs()
);
// ==================== URL ====================
const listUrl = computed(() => {
return props.orderIndex === '20' ? getTetSpecList : getVmsstbprptList;
});
// ==================== ====================
const columns = computed(() => {
if (props.orderIndex === '20') {
return [
{
key: 'name',
title: '名称',
dataIndex: 'name',
fixed: 'left' as const,
width: 200,
ellipsis: true
},
{
key: 'nameEn',
title: '英文名称',
dataIndex: 'nameEn',
width: 200,
ellipsis: true
},
{
key: 'protectlvlName',
title: '保护级别',
dataIndex: 'protectlvlName',
width: 200,
ellipsis: true
}
];
}else{
// orderIndex === '16'
return [
{
key: 'box',
title: '监测区域',
dataIndex: 'box',
fixed: 'left' as const,
width: 200,
ellipsis: true
},
{
key: 'stnm',
title: '站点名称',
dataIndex: 'stnm',
width: 200,
ellipsis: true
},
{
key: 'baseName',
title: '所属基地',
dataIndex: 'baseName',
width: 200,
ellipsis: true
},
{
key: 'stlc',
title: '所在地址',
dataIndex: 'stlc',
width: 200,
ellipsis: true
}
];
}
});
// ==================== ====================
const searchParams = computed(() => {
const filters: any[] = [];
if (props.hbrvcd) {
filters.push({
field: 'hbrvcd',
operator: 'in',
dataType: 'string',
value: props.hbrvcd
});
}
if (props.orderIndex === '20') {
filters.push({
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'TE_ANIMALS'
});
}
const startTime = `${dayjs(yearValue.value).format('YYYY')}-01-01 00:00:00`;
const endTime = `${dayjs(yearValue.value).format('YYYY')}-12-31 23:59:59`;
filters.push({
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: startTime
});
filters.push({
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: endTime
});
return {
filter: {
logic: 'and',
filters
},
sort: [],
group: [],
select: [],
groupResultFlat: false
};
});
// ==================== ====================
const customTransform = (res: any) => {
return {
records: res?.data?.data || [],
total: res?.data?.total || 0
};
};
// ==================== ====================
const handleSearch = () => {
const filter = getFilter();
tableRef.value?.getList(filter);
};
const getFilter = () => {
const filters: any[] = [
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'WVA'
}
];
if (props.hbrvcd) {
filters.push({
field: 'hbrvcd',
operator: 'in',
dataType: 'string',
value: props.hbrvcd
});
}
return {
logic: 'and',
filters
};
};
// ==================== selectDateTime ====================
watch(
() => props.selectDateTime,
newVal => {
if (newVal) {
yearValue.value = dayjs(newVal);
handleSearch();
}
},
{ immediate: false }
);
// ==================== orderIndex ====================
watch(
() => props.orderIndex,
() => {
handleSearch();
}
);
// ==================== ====================
onMounted(() => {
handleSearch();
});
</script>
<style lang="scss" scoped>
.lsstjk-table {
width: 100%;
.search-form {
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 16px;
}
.table-wrapper {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,378 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="监测设备统计">
<a-spin :spinning="dataLoading" tip="加载中...">
<div class="facility-grid">
<div
v-for="facility in facilities"
:key="facility.name"
class="facility-card"
@click="handleCardClick(facility)"
>
<div
style="
width: 60px;
height: 62px;
display: flex;
align-items: center;
justify-content: center;
"
>
<div class="facility-icon">
<i
style="color: #fff"
:class="facility.icon"
type="icon-shengtailiuliang2"
></i>
</div>
</div>
<div class="facility-info">
<div class="facility-name">{{ facility.name }}</div>
<div style="font-size: 16px">
<span class="facility-count">{{ facility.count }}</span>
<span>{{ facility.unit }}</span>
</div>
</div>
</div>
</div>
</a-spin>
</SidePanelItem>
<!-- 陆生生态调查概况弹框 -->
<a-modal
v-model:open="modalVisible"
:title="currentOrderIndex === '16' ? '红外相机信息' : '重点陆生动物'"
width="1536px"
:footer="null"
@cancel="handleModalClose"
>
<LsstjkTk
v-if="modalVisible"
:selectDateTime="datetimePicker.value"
:orderIndex="currentOrderIndex"
:hbrvcd="select.value"
/>
</a-modal>
</template>
<script lang="ts" setup>
import {
ref,
onMounted,
onUnmounted,
nextTick,
watch,
inject,
computed,
defineOptions
} from 'vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import LsstjkTk from './LsstjkTk.vue';
import { wbsbGetKendoList, getEvnmAutoMonitor } from '@/api/stdc';
import moment from 'moment';
import { useDraggable } from '@/utils/drag';
//
defineOptions({
name: 'LSJCSBTJ'
});
const JidiSelectEventStore = useJidiSelectEventStore();
const baseid = ref('');
const dataLoading = ref(false);
// ==================== ====================
const modalVisible = ref(false);
const currentOrderIndex = ref('16');
useDraggable(modalVisible, { boundary: true, resetOnOpen: true });
// ==================== ====================
//
const select = ref({
show: true,
value: undefined,
options: [],
picker: undefined,
format: undefined,
width: '120px'
});
//
const datetimePicker = ref({
show: true,
value: `${new Date().getFullYear()}`,
format: 'YYYY',
picker: 'year' as const,
options: []
});
const facilities: any = ref([
{
name: '红外摄像机',
count: 0,
unit: '个',
icon: 'icon iconfont icon-dongwujiuzhuzhan',
orderInx: '16'
},
{
name: '鸟类智能识别装置',
count: 0,
unit: '种',
icon: 'icon iconfont icon-shipinjiankongshebei',
orderInx: '20'
},
{
name: '动物智能识别装置',
count: 0,
unit: '个',
icon: 'icon iconfont icon-dongwujiuzhuzhan',
orderInx: '21'
}
]);
// ==================== ====================
const handlePanelChange = (data: any) => {
console.log('当前所有控件状态:', data);
if (data.select) {
select.value.value = data.select;
}
if (data.datetime) {
datetimePicker.value.value = data.datetime;
}
//
getCardData();
};
const handleCardClick = (facility: any) => {
console.log('卡片被点击,准备打开弹框');
currentOrderIndex.value = facility.orderInx;
modalVisible.value = true;
};
const handleModalClose = () => {
modalVisible.value = false;
};
// ==================== ====================
const getCardData = async () => {
if (!datetimePicker.value.value) return;
dataLoading.value = true;
try {
const year = moment(datetimePicker.value.value).format('YYYY');
const startTime = `${year}-01-01 00:00:00`;
const endTime = `${year}-12-31 23:59:59`;
const filters: any[] = [
{
field: 'showIds',
operator: 'in',
value: ['16', '20']
},
{
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: startTime
},
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: endTime
}
];
if (baseid.value && baseid.value !== 'all') {
filters.unshift({
field: 'hbrvcd',
operator: 'in',
value: [baseid.value]
});
}
const params = {
filter: {
logic: 'and',
filters
}
};
const res = await getEvnmAutoMonitor(params);
const data = res?.data?.data;
if (data && Array.isArray(data)) {
const wildAnimalItem = data.find((item: any) => item.orderInx === 16);
const keyAnimalItem = data.find((item: any) => item.orderInx === 20);
// facilities.value = [
// {
// name: '',
// count: wildAnimalItem?.cnt ?? 0,
// unit: '',
// icon: 'icon iconfont icon-dongwujiuzhuzhan',
// orderInx: '16'
// },
// {
// name: '',
// count: keyAnimalItem?.cnt ?? 0,
// unit: '',
// icon: 'icon iconfont icon-shipinjiankongshebei',
// orderInx: '20'
// }
// ];
}
} catch (error) {
console.error('获取统计数据失败:', error);
facilities.value = [
{
name: '野生动物监测',
count: 0,
unit: '个',
icon: 'icon iconfont icon-dongwujiuzhuzhan',
orderInx: '16'
},
{
name: '重点陆生动物',
count: 0,
unit: '种',
icon: 'icon iconfont icon-shipinjiankongshebei',
orderInx: '20'
}
];
} finally {
dataLoading.value = false;
}
};
// ==================== ====================
const getSelectData = async () => {
let params = {
filter: {
logic: 'and',
filters: [
{
field: 'wbsType',
operator: 'eq',
value: 'PSB_RVCD'
},
baseid.value != 'all'
? {
field: 'objId',
operator: 'eq',
value: baseid.value
}
: null
].filter(Boolean)
},
group: [
{
dir: 'asc',
field: 'orderIndex'
},
{
dir: 'asc',
field: 'wbsCode'
},
{
dir: 'asc',
field: 'wbsName'
},
{
dir: 'asc',
field: 'objid'
}
],
groupResultFlat: true
};
const res = await wbsbGetKendoList(params);
let data = res?.data?.data;
select.value.options = data.map((item: any) => {
return {
value: item.wbsCode,
label: item.wbsName
};
});
};
// 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(
() => injectedStation?.value || JidiSelectEventStore.selectedItem
);
// ==================== ====================
watch(
() => stationSource.value,
async newVal => {
if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode;
getSelectData();
getCardData();
}
},
{ deep: true, immediate: true }
);
</script>
<style lang="scss" scoped>
.facility-grid {
width: 406px;
flex-flow: wrap;
display: flex;
justify-content: space-between;
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;
}
.facility-card {
width: 200px;
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
margin: 4px 0px;
background: #fff;
border: 1px solid rgb(229, 236, 245);
border-radius: 2px;
transition: all 0.3s;
cursor: pointer;
box-sizing: border-box;
}
.facility-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: #2f6b98;
border-radius: 50%;
.anticon {
font-size: 24px;
color: #fff;
}
}
.facility-info {
flex: 1;
}
.facility-name {
font-size: 16px;
color: #333;
}
.facility-count {
font-size: 18px;
color: #2f6b98;
}
:deep(.ant-spin-nested-loading) {
min-height: 72px;
}
</style>

View File

@ -131,25 +131,45 @@ const isAlarmRangeLegendState = (legendState: string): boolean => {
const getPointRenderPriority = (point: Record<string, any>): number => {
const layerKey = String(point.layerKey || point.type || '').toLowerCase();
const legendState = String(point.anchoPointState || '').trim().toLowerCase();
const legendState = String(point.anchoPointState || '')
.trim()
.toLowerCase();
if (
const isAlarm =
layerKey.includes('eng_alarm_point') ||
layerKey.includes('alarm_range') ||
isAlarmRangeLegendState(legendState)
) {
return 300;
isAlarmRangeLegendState(legendState);
const isLarge = legendState.startsWith('large_eng_');
const isMid = legendState.startsWith('mid_eng_');
const isEng = layerKey.includes('eng_point') || isLarge || isMid;
// 建设状态权重:已建 > 在建 > 未建
let buildRank = 0;
if (legendState.includes('_ubuilt')) {
buildRank = 2;
} else if (legendState.includes('_nbuilt')) {
buildRank = 1;
} else if (legendState.includes('_built')) {
buildRank = 3;
}
if (
layerKey.includes('eng_point') ||
legendState.startsWith('large_eng_') ||
legendState.startsWith('mid_eng_')
) {
return 200;
let base = 100;
// 大型电站优先级最高,中型电站次之;同规模内已建优先
if (isLarge) {
base = 300 + buildRank * 30;
} else if (isMid) {
base = 200 + buildRank * 30;
} else if (isEng) {
base = 150;
}
return 100;
// 报警范围点仅在同规格内略微提升,不跨越大型/中型与建设状态层级
if (isAlarm) {
base += 10;
}
return base;
};
const normalizeText = (value: unknown): string => {

View File

@ -1,6 +1,6 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="栖息地保护工作开展情况">
<SidePanelItem title="栖息地监测设备统计">
<a-spin :spinning="loading" tip="加载中...">
<div class="facility-grid">
<div

View File

@ -0,0 +1,817 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem
title="栖息地分布统计"
:select="select"
:scopeDate="datetimePicker"
@update-values="handlePanelChange1"
>
<div class="chart-wrapper">
<a-spin :spinning="loading">
<!-- 始终渲染图表容器确保有固定宽高 -->
<div ref="chartRef" class="water-temp-chart"></div>
<!-- 无数据时显示Empty但不影响容器尺寸 -->
<div v-if="!loading && !hasData" class="empty-overlay">
<a-empty description="暂无数据" />
</div>
</a-spin>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import {
ref,
onMounted,
onUnmounted,
nextTick,
watch,
inject,
computed,
type Ref
} from 'vue';
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { msstbprptGetKendoList, sdrvwtsGetKendoList } from '@/api/qxd';
import { useModelStore } from '@/store/modules/model';
import dayjs from 'dayjs';
//
defineOptions({
name: 'qixidishuiwenbianhua'
});
// ==================== ====================
const JidiSelectEventStore = useJidiSelectEventStore();
//
const chartRef = ref<HTMLDivElement>();
let chartInstance: echarts.ECharts | null = null;
const baseid = ref('');
// Loading
const loading = ref(true);
const hasData = ref(true); // true initChart
//
const select = ref({
show: true,
value: undefined,
options: [],
picker: undefined,
format: undefined
});
//
const datetimePicker: any = ref({
show: true,
value: (() => {
const now = new Date();
const currentMonth = `${now.getFullYear()}-${String(
now.getMonth() + 1
).padStart(2, '0')}`;
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const lastMonthStr = `${lastMonth.getFullYear()}-${String(
lastMonth.getMonth() + 1
).padStart(2, '0')}`;
return [lastMonthStr, currentMonth];
})(),
format: 'YY-MM',
picker: 'month' as const,
options: []
});
//
const echartsData = ref<any[]>([]);
const modelStore = useModelStore();
//
const unita = ref('℃');
// - 使HSL
const Color = [
'#5470c6',
'#91cc75',
'#fac858',
'#ee6666',
'#73c0de',
'#3ba272',
'#fc8452',
'#9a60b4'
];
// ==================== ====================
/**
* 生成模拟数据 - 季节性水温变化
*/
const generateMockData = () => {
const data = [];
const baseTemp = 15; //
for (let i = 0; i < 12; i++) {
const month = String(i + 1).padStart(2, '0');
//
// 7-81-2
const temp =
baseTemp +
Math.sin((i / 12) * Math.PI * 2 - Math.PI / 2) * 10 +
Math.random() * 2;
data.push({
dt: `2024-${month}`,
wt: parseFloat(temp.toFixed(1))
});
}
return data;
};
/**
* 初始化图表
*/
const initChart = () => {
if (!chartRef.value) {
console.warn('图表容器未找到');
return;
}
//
if (chartInstance) {
chartInstance.dispose();
}
//
chartInstance = echarts.init(chartRef.value);
//
chartInstance.on('click', handleDataPointClick);
//
updateChart();
//
window.addEventListener('resize', handleResize);
};
/**
* 更新图表配置
*/
const updateChart = () => {
if (!chartInstance) return;
const _legendData = [`水温(${unita.value})`];
//
const hasData = echartsData.value && echartsData.value.length > 0;
const option: EChartsOption = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
borderColor: 'transparent',
textStyle: {
color: '#ffffff'
},
formatter: function (params: any) {
if (!params || params.length === 0) return '';
let res = `${params[0].name} <br/>`;
for (const item of params) {
const seriesName = item.seriesName ?? '';
let regx = /\(([^()]+?)\)/;
let unit = seriesName.match(regx);
//
const finalValue =
item.value !== undefined && item.value !== null
? Number(item.value).toFixed(1)
: '-';
if (item.value !== undefined && item.value !== null) {
res += `<span style="background: ${
item.color
}; height:10px; width: 10px; border-radius: 50%;display: inline-block;margin-right:10px;"></span> ${
item.seriesName
} ${finalValue}${unit?.[1] || ''} <br/>`;
}
}
return res;
}
},
// axisPointer
axisPointer: {
type: 'shadow',
label: {
show: false
},
shadowStyle: {
color: 'rgba(84, 112, 198, 0.2)'
}
},
color: Color,
legend: {
data: _legendData,
top: 0,
left: 'center',
selectedMode: 'multiple'
},
grid: {
top: 25,
left: '3%',
right: '3%',
bottom: '3%',
containLabel: true
},
dataZoom: {
type: 'inside',
start: 0,
end: 100
},
xAxis: [
{
type: 'category',
data: echartsData.value?.map((item: any) => item.dt) || [],
boundaryGap: true,
// X - 4
axisLabel: {
show: true,
rotate: 0, //
interval: (index: number, value: string): boolean => {
const total = echartsData.value?.length || 0;
if (total <= 4) return true; // 4
//
if (index === 0) return true;
// 34
const step = Math.floor((total - 1) / 3);
return index % step === 0 && index < total;
},
formatter: function (value: string) {
// "YYYY-MM-DD"
return value;
}
},
// X线
splitLine: {
show: true,
lineStyle: {
type: 'solid',
color: '#e0e0e0'
}
},
// XaxisPointer
axisPointer: {
type: 'shadow',
shadowStyle: {
color: 'rgba(84, 112, 198, 0.2)'
}
}
}
],
yAxis: [
{
type: 'value',
// name legend
name: _legendData[0], // "()"
nameLocation: 'end', // Y
nameGap: 8, // 线 20px
nameTextStyle: {
color: '#333333',
fontSize: 12
},
// Y
min:
hasData && echartsData.value.length > 0
? Math.floor(Math.min(...echartsData.value.map(item => item.wt)))
: undefined,
max:
hasData && echartsData.value.length > 0
? Math.ceil(Math.max(...echartsData.value.map(item => item.wt)))
: undefined,
//
axisLabel: {
show: true,
color: '#333',
formatter: (value: number) => `${Math.round(value)}` //
},
axisTick: {
show: true
},
axisLine: {
show: true,
lineStyle: {
color: '#333'
}
},
splitLine: {
show: true,
lineStyle: {
type: 'solid'
}
},
// Y
minInterval: 1,
maxInterval: 1
}
],
series: [
{
name: _legendData[0],
data: hasData
? echartsData.value?.map((item: any) => item.wt) || []
: [],
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 4,
lineStyle: {
width: 2
}
}
]
};
// 使
chartInstance.setOption(option, true);
};
/**
* 处理窗口大小变化
*/
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
};
/**
* 处理数据点点击事件
*/
const handleDataPointClick = (params: any) => {
console.log('=== 数据点点击事件 ===');
console.log('点击参数:', params);
if (!params || !params.dataIndex) {
console.warn('无效的数据点点击');
return;
}
const dataIndex = params.dataIndex;
const dataItem = echartsData.value[dataIndex];
if (dataItem) {
console.log('点击的数据:', {
日期: dataItem.dt,
水温: dataItem.wt,
测站代码: dataItem.stcd,
索引: dataIndex
});
modelStore.modalVisible = true;
modelStore.params.sttp = 'WT';
modelStore.title = '水温监测';
modelStore.params.stcd = dataItem.stcd;
modelStore.showStcdSelector = true;
modelStore.stcdOptions = select.value.options;
modelStore.filter.rangeTm = [
dayjs(dataItem.dt).startOf('day').format('YYYY-MM-DD HH:mm:ss'),
dayjs(dataItem.dt).endOf('day').format('YYYY-MM-DD HH:mm:ss')
];
}
};
/**
* 加载模拟数据
*/
const loadData = async () => {
try {
//
echartsData.value = generateMockData();
//
await nextTick();
updateChart();
} catch (error) {
console.error('加载水温数据失败:', error);
}
};
/**
* 加载图表数据
* @param startDate 开始日期 (格式: YYYY-MM-DD HH:mm:ss)
* @param endDate 结束日期 (格式: YYYY-MM-DD HH:mm:ss)
* @param stcd 测站代码
*/
const getChartData = async (
startDate: string,
endDate: string,
stcd: string
) => {
try {
console.log('=== 开始加载图表数据 ===');
console.log('请求参数:', { startDate, endDate, stcd });
// loading
loading.value = true;
hasData.value = false;
// TODO: API
//
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'DT',
operator: 'gte',
value: startDate // : "2026-05-01 00:00:00"
},
{
field: 'DT',
operator: 'lte',
value: endDate // : "2026-06-30 23:59:59"
},
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
}
]
},
sort: [
{
field: 'dt',
dir: 'asc'
}
]
};
// API
const res = await sdrvwtsGetKendoList(params);
console.log('API返回结果:', res);
let data = res?.data?.data || [];
console.log('原始数据条数:', data.length);
//
if (!data || data.length === 0) {
echartsData.value = [];
hasData.value = false;
await nextTick();
updateChart();
console.warn('未查询到水温数据');
return;
}
// X
echartsData.value = data
.map(item => {
//
if (!item.dt || typeof item.dt !== 'string') {
console.warn('无效的日期数据:', item);
return null;
}
// wt
const wt = typeof item.wt === 'number' ? item.wt : parseFloat(item.wt);
if (isNaN(wt)) {
console.warn('无效的水温数据:', item);
return null;
}
return {
dt: item.dt.substring(0, 10), // "2026-05-02"
wt: wt, // number
stcd: item.stcd || stcd // stcd API 使 stcd
};
})
.filter(Boolean); //
console.log('转换后的图表数据:', echartsData.value);
console.log('数据类型检查:', {
第一条数据: echartsData.value[0],
dt类型: typeof echartsData.value[0]?.dt,
wt类型: typeof echartsData.value[0]?.wt,
wt값: echartsData.value[0]?.wt
});
//
hasData.value = echartsData.value.length > 0;
console.log('hasData 设置为:', hasData.value);
//
await nextTick();
updateChart();
console.log('✅ 图表数据加载完成');
} catch (error) {
console.error('获取图表数据失败:', error);
hasData.value = false;
} finally {
// loading
await nextTick();
console.log('关闭 loading 状态');
loading.value = false;
}
};
/**
* YY-MM 格式转换为完整的日期时间范围
* @param dateRange ['26-05', '26-06'] 格式的数组
* @returns { startDate: string, endDate: string } 格式化后的日期范围
*/
const convertDateRange = (dateRange: string[]) => {
if (!dateRange || dateRange.length !== 2) {
return null;
}
//
const [startStr, endStr] = dateRange;
//
const today = new Date();
const currentDay = today.getDate();
// YY-MM
const [startYearStr, startMonthStr] = startStr.split('-');
const startYear = parseInt(startYearStr);
const startMonth = parseInt(startMonthStr);
const fullStartYear = startYear < 100 ? 2000 + startYear : startYear;
// YY-MM
const [endYearStr, endMonthStr] = endStr.split('-');
const endYear = parseInt(endYearStr);
const endMonth = parseInt(endMonthStr);
const fullEndYear = endYear < 100 ? 2000 + endYear : endYear;
// + + 00:00:00
// 31228
const startMonthLastDay = new Date(fullStartYear, startMonth, 0).getDate();
const actualStartDay = Math.min(currentDay, startMonthLastDay);
const startDate = `${fullStartYear}-${String(startMonth).padStart(
2,
'0'
)}-${String(actualStartDay).padStart(2, '0')} 00:00:00`;
// + + 23:59:59
//
const endMonthLastDay = new Date(fullEndYear, endMonth, 0).getDate();
const actualEndDay = Math.min(currentDay, endMonthLastDay);
const endDate = `${fullEndYear}-${String(endMonth).padStart(2, '0')}-${String(
actualEndDay
).padStart(2, '0')} 23:59:59`;
return {
startDate,
endDate
};
};
const handlePanelChange1 = async data => {
console.log('=== 用户交互触发 ===');
console.log('当前所有控件状态:', data);
// scopeDate: ['26-05', '26-06']
// select: "0836000011"
if (data.scopeDate && data.scopeDate.length > 0 && data.select) {
console.log('参数验证通过,开始加载数据');
//
const dateRange = convertDateRange(data.scopeDate);
if (dateRange) {
console.log('日期转换结果:', dateRange);
await getChartData(dateRange.startDate, dateRange.endDate, data.select);
} else {
console.error('日期转换失败');
}
} else {
loading.value = false;
hasData.value = false;
//
echartsData.value = [];
//
await nextTick();
updateChart();
console.warn('参数不完整:', {
scopeDate: data.scopeDate,
select: data.select
});
}
};
//
const getselsectData = async () => {
let params = {
filter: {
logic: 'and',
filters: [
baseid.value != 'all'
? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: baseid.value
}
: null,
{
field: 'fhstcd',
operator: 'isnotnull'
},
{
logic: 'or',
filters: [
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'WTRV'
}
]
}
].filter(Boolean)
},
select: ['stcd', 'stnm', 'fhstcd', 'fhstnm', 'sttpCode']
};
let res = await msstbprptGetKendoList(params);
console.log(res);
if (!res?.data?.data || res.data.data.length == 0) {
select.value.value = '';
select.value.options = [];
return;
}
select.value.value = res.data.data[0].stcd;
select.value.options = res.data.data.map((item: any) => {
return {
label: item.stnm,
value: item.stcd
};
});
//
};
////
// 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(
() => injectedStation?.value || JidiSelectEventStore.selectedItem
);
watch(
() => stationSource.value,
newVal => {
baseid.value = newVal.wbsCode;
getselsectData();
},
{ deep: true, immediate: true }
);
// ==================== ppbclTagList ====================
const MODULE_PATH = '@/modules/qixidishuiwenbianhua/index.vue';
const ppbclConfigMap = inject<
Ref<Record<string, { isShow: number; tagValue: string }>>
>('ppbclConfigMap', ref({}));
/** 根据 tagValue 计算默认日期范围YY-MM 格式) */
function getDateRangeFromTagValue(tagValue: string): [string, string] | null {
const now = dayjs();
let start: dayjs.Dayjs;
switch (tagValue) {
case '7d':
start = now.subtract(7, 'day');
break;
case '1M':
start = now.subtract(1, 'month');
break;
case 'a1M':
start = now.subtract(1, 'month').startOf('month');
break;
case '3M':
start = now.subtract(3, 'month');
break;
case 'a3M':
start = now.subtract(3, 'month').startOf('month');
break;
case '1y':
start = now.subtract(1, 'year');
break;
case '3y':
start = now.subtract(3, 'year');
break;
case '5y':
start = now.subtract(5, 'year');
break;
default:
return null;
}
return [start.format('YYYY-MM'), now.format('YYYY-MM')];
}
watch(
() => ppbclConfigMap?.value?.[MODULE_PATH],
config => {
if (config) {
// isShow
datetimePicker.value.show = config.isShow !== 0;
// tagValue
if (config.tagValue) {
const dateRange = getDateRangeFromTagValue(config.tagValue);
if (dateRange) {
datetimePicker.value.value = dateRange;
}
}
}
},
{ immediate: true }
);
// ==================== ====================
//
onMounted(() => {
// DOM
nextTick(() => {
setTimeout(() => {
//
if (chartRef.value) {
const rect = chartRef.value.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
initChart();
//
// loadData();
console.log('✅ 图表初始化完成,等待用户交互加载数据');
} else {
console.warn('图表容器尺寸为0启动重试机制');
// 退
let retryCount = 0;
const maxRetries = 5;
const retryInit = () => {
if (retryCount < maxRetries) {
retryCount++;
setTimeout(() => {
const newRect = chartRef.value?.getBoundingClientRect();
if (newRect && newRect.width > 0 && newRect.height > 0) {
initChart();
// loadData();
console.log('✅ 图表初始化完成(重试成功)');
} else {
retryInit();
}
}, 50 * Math.pow(2, retryCount));
} else {
console.error('图表容器初始化失败,已达到最大重试次数');
}
};
retryInit();
}
}
}, 50);
});
});
//
onUnmounted(() => {
//
window.removeEventListener('resize', handleResize);
//
if (chartInstance) {
//
chartInstance.off('click', handleDataPointClick);
chartInstance.dispose();
chartInstance = null;
}
});
</script>
<style lang="scss" scoped>
.chart-wrapper {
width: 100%;
height: 231px;
min-height: 231px;
position: relative;
/* 为empty-overlay提供定位上下文 */
.water-temp-chart {
width: 100% !important;
height: 100% !important;
min-width: 100%;
min-height: 231px;
}
.empty-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255);
z-index: 10;
}
}
:deep(.ant-spin-nested-loading > div > .ant-spin) {
background: rgba(255, 255, 255) !important;
}
</style>

View File

@ -0,0 +1,817 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem
title="监测结果统计"
:select="select"
:scopeDate="datetimePicker"
@update-values="handlePanelChange1"
>
<div class="chart-wrapper">
<a-spin :spinning="loading">
<!-- 始终渲染图表容器确保有固定宽高 -->
<div ref="chartRef" class="water-temp-chart"></div>
<!-- 无数据时显示Empty但不影响容器尺寸 -->
<div v-if="!loading && !hasData" class="empty-overlay">
<a-empty description="暂无数据" />
</div>
</a-spin>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import {
ref,
onMounted,
onUnmounted,
nextTick,
watch,
inject,
computed,
type Ref
} from 'vue';
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { msstbprptGetKendoList, sdrvwtsGetKendoList } from '@/api/qxd';
import { useModelStore } from '@/store/modules/model';
import dayjs from 'dayjs';
//
defineOptions({
name: 'qixidishuiwenbianhua'
});
// ==================== ====================
const JidiSelectEventStore = useJidiSelectEventStore();
//
const chartRef = ref<HTMLDivElement>();
let chartInstance: echarts.ECharts | null = null;
const baseid = ref('');
// Loading
const loading = ref(true);
const hasData = ref(true); // true initChart
//
const select = ref({
show: true,
value: undefined,
options: [],
picker: undefined,
format: undefined
});
//
const datetimePicker: any = ref({
show: true,
value: (() => {
const now = new Date();
const currentMonth = `${now.getFullYear()}-${String(
now.getMonth() + 1
).padStart(2, '0')}`;
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const lastMonthStr = `${lastMonth.getFullYear()}-${String(
lastMonth.getMonth() + 1
).padStart(2, '0')}`;
return [lastMonthStr, currentMonth];
})(),
format: 'YY-MM',
picker: 'month' as const,
options: []
});
//
const echartsData = ref<any[]>([]);
const modelStore = useModelStore();
//
const unita = ref('℃');
// - 使HSL
const Color = [
'#5470c6',
'#91cc75',
'#fac858',
'#ee6666',
'#73c0de',
'#3ba272',
'#fc8452',
'#9a60b4'
];
// ==================== ====================
/**
* 生成模拟数据 - 季节性水温变化
*/
const generateMockData = () => {
const data = [];
const baseTemp = 15; //
for (let i = 0; i < 12; i++) {
const month = String(i + 1).padStart(2, '0');
//
// 7-81-2
const temp =
baseTemp +
Math.sin((i / 12) * Math.PI * 2 - Math.PI / 2) * 10 +
Math.random() * 2;
data.push({
dt: `2024-${month}`,
wt: parseFloat(temp.toFixed(1))
});
}
return data;
};
/**
* 初始化图表
*/
const initChart = () => {
if (!chartRef.value) {
console.warn('图表容器未找到');
return;
}
//
if (chartInstance) {
chartInstance.dispose();
}
//
chartInstance = echarts.init(chartRef.value);
//
chartInstance.on('click', handleDataPointClick);
//
updateChart();
//
window.addEventListener('resize', handleResize);
};
/**
* 更新图表配置
*/
const updateChart = () => {
if (!chartInstance) return;
const _legendData = [`水温(${unita.value})`];
//
const hasData = echartsData.value && echartsData.value.length > 0;
const option: EChartsOption = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
borderColor: 'transparent',
textStyle: {
color: '#ffffff'
},
formatter: function (params: any) {
if (!params || params.length === 0) return '';
let res = `${params[0].name} <br/>`;
for (const item of params) {
const seriesName = item.seriesName ?? '';
let regx = /\(([^()]+?)\)/;
let unit = seriesName.match(regx);
//
const finalValue =
item.value !== undefined && item.value !== null
? Number(item.value).toFixed(1)
: '-';
if (item.value !== undefined && item.value !== null) {
res += `<span style="background: ${
item.color
}; height:10px; width: 10px; border-radius: 50%;display: inline-block;margin-right:10px;"></span> ${
item.seriesName
} ${finalValue}${unit?.[1] || ''} <br/>`;
}
}
return res;
}
},
// axisPointer
axisPointer: {
type: 'shadow',
label: {
show: false
},
shadowStyle: {
color: 'rgba(84, 112, 198, 0.2)'
}
},
color: Color,
legend: {
data: _legendData,
top: 0,
left: 'center',
selectedMode: 'multiple'
},
grid: {
top: 25,
left: '3%',
right: '3%',
bottom: '3%',
containLabel: true
},
dataZoom: {
type: 'inside',
start: 0,
end: 100
},
xAxis: [
{
type: 'category',
data: echartsData.value?.map((item: any) => item.dt) || [],
boundaryGap: true,
// X - 4
axisLabel: {
show: true,
rotate: 0, //
interval: (index: number, value: string): boolean => {
const total = echartsData.value?.length || 0;
if (total <= 4) return true; // 4
//
if (index === 0) return true;
// 34
const step = Math.floor((total - 1) / 3);
return index % step === 0 && index < total;
},
formatter: function (value: string) {
// "YYYY-MM-DD"
return value;
}
},
// X线
splitLine: {
show: true,
lineStyle: {
type: 'solid',
color: '#e0e0e0'
}
},
// XaxisPointer
axisPointer: {
type: 'shadow',
shadowStyle: {
color: 'rgba(84, 112, 198, 0.2)'
}
}
}
],
yAxis: [
{
type: 'value',
// name legend
name: _legendData[0], // "()"
nameLocation: 'end', // Y
nameGap: 8, // 线 20px
nameTextStyle: {
color: '#333333',
fontSize: 12
},
// Y
min:
hasData && echartsData.value.length > 0
? Math.floor(Math.min(...echartsData.value.map(item => item.wt)))
: undefined,
max:
hasData && echartsData.value.length > 0
? Math.ceil(Math.max(...echartsData.value.map(item => item.wt)))
: undefined,
//
axisLabel: {
show: true,
color: '#333',
formatter: (value: number) => `${Math.round(value)}` //
},
axisTick: {
show: true
},
axisLine: {
show: true,
lineStyle: {
color: '#333'
}
},
splitLine: {
show: true,
lineStyle: {
type: 'solid'
}
},
// Y
minInterval: 1,
maxInterval: 1
}
],
series: [
{
name: _legendData[0],
data: hasData
? echartsData.value?.map((item: any) => item.wt) || []
: [],
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 4,
lineStyle: {
width: 2
}
}
]
};
// 使
chartInstance.setOption(option, true);
};
/**
* 处理窗口大小变化
*/
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
};
/**
* 处理数据点点击事件
*/
const handleDataPointClick = (params: any) => {
console.log('=== 数据点点击事件 ===');
console.log('点击参数:', params);
if (!params || !params.dataIndex) {
console.warn('无效的数据点点击');
return;
}
const dataIndex = params.dataIndex;
const dataItem = echartsData.value[dataIndex];
if (dataItem) {
console.log('点击的数据:', {
日期: dataItem.dt,
水温: dataItem.wt,
测站代码: dataItem.stcd,
索引: dataIndex
});
modelStore.modalVisible = true;
modelStore.params.sttp = 'WT';
modelStore.title = '水温监测';
modelStore.params.stcd = dataItem.stcd;
modelStore.showStcdSelector = true;
modelStore.stcdOptions = select.value.options;
modelStore.filter.rangeTm = [
dayjs(dataItem.dt).startOf('day').format('YYYY-MM-DD HH:mm:ss'),
dayjs(dataItem.dt).endOf('day').format('YYYY-MM-DD HH:mm:ss')
];
}
};
/**
* 加载模拟数据
*/
const loadData = async () => {
try {
//
echartsData.value = generateMockData();
//
await nextTick();
updateChart();
} catch (error) {
console.error('加载水温数据失败:', error);
}
};
/**
* 加载图表数据
* @param startDate 开始日期 (格式: YYYY-MM-DD HH:mm:ss)
* @param endDate 结束日期 (格式: YYYY-MM-DD HH:mm:ss)
* @param stcd 测站代码
*/
const getChartData = async (
startDate: string,
endDate: string,
stcd: string
) => {
try {
console.log('=== 开始加载图表数据 ===');
console.log('请求参数:', { startDate, endDate, stcd });
// loading
loading.value = true;
hasData.value = false;
// TODO: API
//
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'DT',
operator: 'gte',
value: startDate // : "2026-05-01 00:00:00"
},
{
field: 'DT',
operator: 'lte',
value: endDate // : "2026-06-30 23:59:59"
},
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: stcd
}
]
},
sort: [
{
field: 'dt',
dir: 'asc'
}
]
};
// API
const res = await sdrvwtsGetKendoList(params);
console.log('API返回结果:', res);
let data = res?.data?.data || [];
console.log('原始数据条数:', data.length);
//
if (!data || data.length === 0) {
echartsData.value = [];
hasData.value = false;
await nextTick();
updateChart();
console.warn('未查询到水温数据');
return;
}
// X
echartsData.value = data
.map(item => {
//
if (!item.dt || typeof item.dt !== 'string') {
console.warn('无效的日期数据:', item);
return null;
}
// wt
const wt = typeof item.wt === 'number' ? item.wt : parseFloat(item.wt);
if (isNaN(wt)) {
console.warn('无效的水温数据:', item);
return null;
}
return {
dt: item.dt.substring(0, 10), // "2026-05-02"
wt: wt, // number
stcd: item.stcd || stcd // stcd API 使 stcd
};
})
.filter(Boolean); //
console.log('转换后的图表数据:', echartsData.value);
console.log('数据类型检查:', {
第一条数据: echartsData.value[0],
dt类型: typeof echartsData.value[0]?.dt,
wt类型: typeof echartsData.value[0]?.wt,
wt값: echartsData.value[0]?.wt
});
//
hasData.value = echartsData.value.length > 0;
console.log('hasData 设置为:', hasData.value);
//
await nextTick();
updateChart();
console.log('✅ 图表数据加载完成');
} catch (error) {
console.error('获取图表数据失败:', error);
hasData.value = false;
} finally {
// loading
await nextTick();
console.log('关闭 loading 状态');
loading.value = false;
}
};
/**
* YY-MM 格式转换为完整的日期时间范围
* @param dateRange ['26-05', '26-06'] 格式的数组
* @returns { startDate: string, endDate: string } 格式化后的日期范围
*/
const convertDateRange = (dateRange: string[]) => {
if (!dateRange || dateRange.length !== 2) {
return null;
}
//
const [startStr, endStr] = dateRange;
//
const today = new Date();
const currentDay = today.getDate();
// YY-MM
const [startYearStr, startMonthStr] = startStr.split('-');
const startYear = parseInt(startYearStr);
const startMonth = parseInt(startMonthStr);
const fullStartYear = startYear < 100 ? 2000 + startYear : startYear;
// YY-MM
const [endYearStr, endMonthStr] = endStr.split('-');
const endYear = parseInt(endYearStr);
const endMonth = parseInt(endMonthStr);
const fullEndYear = endYear < 100 ? 2000 + endYear : endYear;
// + + 00:00:00
// 31228
const startMonthLastDay = new Date(fullStartYear, startMonth, 0).getDate();
const actualStartDay = Math.min(currentDay, startMonthLastDay);
const startDate = `${fullStartYear}-${String(startMonth).padStart(
2,
'0'
)}-${String(actualStartDay).padStart(2, '0')} 00:00:00`;
// + + 23:59:59
//
const endMonthLastDay = new Date(fullEndYear, endMonth, 0).getDate();
const actualEndDay = Math.min(currentDay, endMonthLastDay);
const endDate = `${fullEndYear}-${String(endMonth).padStart(2, '0')}-${String(
actualEndDay
).padStart(2, '0')} 23:59:59`;
return {
startDate,
endDate
};
};
const handlePanelChange1 = async data => {
console.log('=== 用户交互触发 ===');
console.log('当前所有控件状态:', data);
// scopeDate: ['26-05', '26-06']
// select: "0836000011"
if (data.scopeDate && data.scopeDate.length > 0 && data.select) {
console.log('参数验证通过,开始加载数据');
//
const dateRange = convertDateRange(data.scopeDate);
if (dateRange) {
console.log('日期转换结果:', dateRange);
await getChartData(dateRange.startDate, dateRange.endDate, data.select);
} else {
console.error('日期转换失败');
}
} else {
loading.value = false;
hasData.value = false;
//
echartsData.value = [];
//
await nextTick();
updateChart();
console.warn('参数不完整:', {
scopeDate: data.scopeDate,
select: data.select
});
}
};
//
const getselsectData = async () => {
let params = {
filter: {
logic: 'and',
filters: [
baseid.value != 'all'
? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: baseid.value
}
: null,
{
field: 'fhstcd',
operator: 'isnotnull'
},
{
logic: 'or',
filters: [
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'WTRV'
}
]
}
].filter(Boolean)
},
select: ['stcd', 'stnm', 'fhstcd', 'fhstnm', 'sttpCode']
};
let res = await msstbprptGetKendoList(params);
console.log(res);
if (!res?.data?.data || res.data.data.length == 0) {
select.value.value = '';
select.value.options = [];
return;
}
select.value.value = res.data.data[0].stcd;
select.value.options = res.data.data.map((item: any) => {
return {
label: item.stnm,
value: item.stcd
};
});
//
};
////
// 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(
() => injectedStation?.value || JidiSelectEventStore.selectedItem
);
watch(
() => stationSource.value,
newVal => {
baseid.value = newVal.wbsCode;
getselsectData();
},
{ deep: true, immediate: true }
);
// ==================== ppbclTagList ====================
const MODULE_PATH = '@/modules/qixidishuiwenbianhua/index.vue';
const ppbclConfigMap = inject<
Ref<Record<string, { isShow: number; tagValue: string }>>
>('ppbclConfigMap', ref({}));
/** 根据 tagValue 计算默认日期范围YY-MM 格式) */
function getDateRangeFromTagValue(tagValue: string): [string, string] | null {
const now = dayjs();
let start: dayjs.Dayjs;
switch (tagValue) {
case '7d':
start = now.subtract(7, 'day');
break;
case '1M':
start = now.subtract(1, 'month');
break;
case 'a1M':
start = now.subtract(1, 'month').startOf('month');
break;
case '3M':
start = now.subtract(3, 'month');
break;
case 'a3M':
start = now.subtract(3, 'month').startOf('month');
break;
case '1y':
start = now.subtract(1, 'year');
break;
case '3y':
start = now.subtract(3, 'year');
break;
case '5y':
start = now.subtract(5, 'year');
break;
default:
return null;
}
return [start.format('YYYY-MM'), now.format('YYYY-MM')];
}
watch(
() => ppbclConfigMap?.value?.[MODULE_PATH],
config => {
if (config) {
// isShow
datetimePicker.value.show = config.isShow !== 0;
// tagValue
if (config.tagValue) {
const dateRange = getDateRangeFromTagValue(config.tagValue);
if (dateRange) {
datetimePicker.value.value = dateRange;
}
}
}
},
{ immediate: true }
);
// ==================== ====================
//
onMounted(() => {
// DOM
nextTick(() => {
setTimeout(() => {
//
if (chartRef.value) {
const rect = chartRef.value.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
initChart();
//
// loadData();
console.log('✅ 图表初始化完成,等待用户交互加载数据');
} else {
console.warn('图表容器尺寸为0启动重试机制');
// 退
let retryCount = 0;
const maxRetries = 5;
const retryInit = () => {
if (retryCount < maxRetries) {
retryCount++;
setTimeout(() => {
const newRect = chartRef.value?.getBoundingClientRect();
if (newRect && newRect.width > 0 && newRect.height > 0) {
initChart();
// loadData();
console.log('✅ 图表初始化完成(重试成功)');
} else {
retryInit();
}
}, 50 * Math.pow(2, retryCount));
} else {
console.error('图表容器初始化失败,已达到最大重试次数');
}
};
retryInit();
}
}
}, 50);
});
});
//
onUnmounted(() => {
//
window.removeEventListener('resize', handleResize);
//
if (chartInstance) {
//
chartInstance.off('click', handleDataPointClick);
chartInstance.dispose();
chartInstance = null;
}
});
</script>
<style lang="scss" scoped>
.chart-wrapper {
width: 100%;
height: 231px;
min-height: 231px;
position: relative;
/* 为empty-overlay提供定位上下文 */
.water-temp-chart {
width: 100% !important;
height: 100% !important;
min-width: 100%;
min-height: 231px;
}
.empty-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255);
z-index: 10;
}
}
:deep(.ant-spin-nested-loading > div > .ant-spin) {
background: rgba(255, 255, 255) !important;
}
</style>

View File

@ -123,12 +123,17 @@ const VPInfo = [
},
{
label: '保护对象',
value: 'protObj',
value: 'protobj',
col: 24
},
{
label: '保护方式',
value: 'bhfs',
value: 'protmthd',
col: 24
},
{
label: '站址',
value: 'stlc',
col: 24
}
];
@ -150,7 +155,7 @@ const VAInfo = [
},
{
label: '保护方式',
value: 'bhfs',
value: 'protmthd',
col: 24
}
];
@ -279,7 +284,7 @@ const VDInfo = [
{
label: '监控内容类型',
value: 'sttpName',
col: 12
col: 24
},
{
label: '所属行政区',
@ -343,7 +348,7 @@ const FHInfo = [
];
// ai视频
const AIVDInfo = [
{
{
label: '监控点名',
value: 'stnm',
col: 24
@ -353,7 +358,7 @@ const AIVDInfo = [
value: 'stindx',
col: 12
},
{
{
label: '监控内容类型',
value: 'sttpName',
col: 12
@ -373,25 +378,65 @@ const AIVDInfo = [
value: 'stlc',
col: 24
}
]
////调查断面
];
//调查断面
const WEInfo = [
{
{
label: '断面名称',
value: 'stnm',
col: 24
},
{
{
label: '站址',
value: 'stlc',
col: 24
},
{
{
label: '所在河段',
value: 'hbrvcdName',
col: 24
}
];
//野生动物监测
const WVAInfo = [
{
label: '测站名称',
value: 'stnm',
col: 24
},
]
{
label: '测站类型',
value: 'sttpName',
col: 24
},
{
label: '监测方式',
value: 'mwayName',
col: 24
}
];
const FPRDInfo = [
{
label: '监控点名',
value: 'stnm',
col: 24
},
{
label: '监控点类型',
value: 'stindx',
col: 24
},
{
label: '监控点地址',
value: 'stlc',
col: 24
},
{
label: '所属流域',
value: 'hbrvcdName',
col: 24
}
];
export {
ENGInfo,
DWInfo,
@ -405,5 +450,7 @@ export {
VDInfo,
FHInfo,
AIVDInfo,
WEInfo
WEInfo,
WVAInfo,
FPRDInfo
};

View File

@ -47,10 +47,12 @@ import {
WTInfo,
WQInfo,
ZQInfo,
VDInfo,//
VDInfo, //
FHInfo, //
AIVDInfo, // AI
WEInfo//
WEInfo, //
WVAInfo, //
FPRDInfo //
} from './config.js';
import { getStcdDetail } from '@/api/mapModal';
@ -77,8 +79,10 @@ const moduleMap = {
WQFBInfo: WQInfo,
WQFPInfo: WQInfo,
FHInfo: FHInfo,
AIVDInfo:AIVDInfo,
WEInfo:WEInfo,
AIVDInfo: AIVDInfo,
WEInfo: WEInfo,
WVAInfo: WVAInfo,
FPRDInfo: FPRDInfo
} as const;
watch(
() => modelStore.selectedAnchorPoint,

View File

@ -1,5 +1,5 @@
<template>
<SidePanelItem title="电站运行过程线" :shrink="false">
<SidePanelItem title="电站监测数据" :shrink="false">
<template #title-right-content>
<a-range-picker
class="w-[220px]"
@ -22,6 +22,19 @@
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>
@ -34,6 +47,7 @@ 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 BasicTable from '@/components/BasicTable/index.vue';
import { DateSetting } from '@/utils/enumeration';
const uiStore = useUiStore();
@ -41,6 +55,8 @@ const modelStore = useModelStore();
const isLoading = ref(false);
const chartData = ref<any[]>([]);
const tableData = ref<any[]>([]);
const tableRef = ref<any>();
// ==================== ====================
const chartRef = ref<HTMLElement>();
@ -366,6 +382,70 @@ const initDateRange = (): [Dayjs, Dayjs] => {
};
const dateRange = ref<[Dayjs, Dayjs] | undefined>(initDateRange());
// ==================== ====================
// null/undefined//'-'/NaN '-'
const formatCell = (
text: any,
formatter: (num: number) => string = num => String(num)
) => {
if (text === null || text === undefined || text === '' || text === '-') {
return '-';
}
const num = Number(text);
if (Number.isNaN(num)) return '-';
return formatter(num);
};
// MapModal/MonitorInfo
const tableColumns = [
{
title: '时间',
dataIndex: 'tm',
width: 150,
fixed: 'left',
customRender: ({ text }: any) =>
text && text !== '-' ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '-'
},
{
title: '坝上水位(m)',
dataIndex: 'rz',
width: 100,
customRender: ({ text }: any) => formatCell(text, v => v.toFixed(2))
},
{
title: '坝下水位(m)',
dataIndex: 'dz',
width: 100,
customRender: ({ text }: any) => formatCell(text, v => v.toFixed(2))
},
{
title: '入库流量(m³/s)',
dataIndex: 'qi',
width: 120,
customRender: ({ text }: any) =>
formatCell(text, v => String(Math.round(v)))
},
{
title: '出库流量(m³/s)',
dataIndex: 'qo',
width: 120,
customRender: ({ text }: any) => formatCell(text, v => String(v))
},
{
title: '生态流量(m³/s)',
dataIndex: 'qec',
width: 120,
customRender: ({ text }: any) => formatCell(text, v => v.toFixed(1))
},
{
title: '生态流量限值(m³/s)',
dataIndex: 'qecLimit',
width: 140,
customRender: ({ text }: any) => formatCell(text, v => v.toFixed(1))
}
];
const tableScrollX = tableColumns.reduce((s, c) => s + (c.width || 100), 0);
// modelStore.selectedAnchorPoint
const fetchData = async () => {
const stcd = modelStore.selectedAnchorPoint?.stcd;
@ -404,9 +484,11 @@ const fetchData = async () => {
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;
}
@ -462,4 +544,13 @@ watch(
left: 50%;
transform: translate(-50%, -50%);
}
.monitor-table {
margin-top: 12px;
border-radius: 2px;
box-sizing: border-box;
.table-container {
border: 1px solid #dcdfe6;
}
}
</style>

View File

@ -0,0 +1,128 @@
<!--
物种介绍动物/植物介绍面板
- 面板标题根据当前生态调查菜单动态展示水生生态调查鱼类介绍陆生生态调查动物介绍
- 右上角下拉框title-right-content 插槽选项为上方鱼种/动物种多选框已选中的数据默认展示第一条
- 主体区域只展示当前选中物种上方图片下方文字描述
- 数据来自 modelStore.selectedFishSpecies预留 image / intro 字段后续对接真实数据
-->
<template>
<SidePanelItem :title="panelTitle" :shrink="false">
<template #title-right-content>
<a-select
v-model:value="currentValue"
size="small"
style="width: 110px"
placeholder="请选择"
:options="speciesOptions"
/>
</template>
<a-empty v-if="!currentSpecies" description="请先在上方选择物种" />
<div v-else class="species-card">
<!-- 上方图片 -->
<a-image
v-if="currentSpecies.image"
class="species-image"
:src="currentSpecies.image"
:preview="false"
/>
<div v-else class="species-image species-image--empty">
<span>暂无图片</span>
</div>
<!-- 下方文字描述 -->
<div class="species-info">
<div class="species-name">{{ currentSpecies.label }}</div>
<div class="species-desc">
{{ currentSpecies.intro || '暂无描述' }}
</div>
</div>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useModelStore } from '@/store/modules/model';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
const modelStore = useModelStore();
const route = useRoute();
//
const panelTitle = computed(() => {
if (route.path.includes('shengTaiDiaoCha/shuiShengShengTaiDiaoCha')) {
return '鱼类介绍';
}
if (route.path.includes('shengTaiDiaoCha/luShengShengTaiDiaoCha')) {
return '动物介绍';
}
return '物种介绍';
});
// =
const speciesOptions = computed(() =>
(modelStore.selectedFishSpecies || []).map(item => ({
label: item.label,
value: item.value
}))
);
const currentValue = ref<string | undefined>();
//
const currentSpecies = computed(() =>
speciesOptions.value.find(item => item.value === currentValue.value)
);
//
watch(
() => modelStore.selectedFishSpecies,
list => {
currentValue.value = (list || [])[0]?.value;
},
{ immediate: true, deep: true }
);
</script>
<style lang="scss" scoped>
.species-card {
border: 1px solid #e5edf3;
border-radius: 4px;
overflow: hidden;
}
.species-image {
display: block;
width: 100%;
height: 200px;
object-fit: cover;
background: #f5f5f5;
}
.species-image--empty {
display: flex;
align-items: center;
justify-content: center;
color: #bfbfbf;
font-size: 14px;
}
.species-info {
padding: 10px 12px 12px;
}
.species-name {
font-size: 15px;
font-weight: 500;
color: #2f6b98;
margin-bottom: 6px;
}
.species-desc {
font-size: 13px;
line-height: 22px;
color: #595959;
word-break: break-all;
white-space: pre-wrap;
}
</style>

View File

@ -0,0 +1,494 @@
<!-- SidePanelItem.vue -->
<template>
<div>
<SidePanelItem title="生态环境部门要求生态流量达标情况">
<!-- <div class="body_topOne">
<a-radio-group v-model:value="mode" @change="dataChage">
<a-radio-button value="hour">逐时</a-radio-button>
<a-radio-button value="day"></a-radio-button>
</a-radio-group>
</div> -->
<div class="body_title">逐日</div>
<div class="body_main">
<div class="body_div" v-for="(i, index) in dataArr" :key="index">
<div class="body_div_icon">
<img :src="i.icon" alt="" />
</div>
<div class="body_div_text">{{ i.qecCnt }}</div>
<div class="body_div_text_sub">
{{ i.text }}
<span> () </span>
</div>
</div>
</div>
<div class="body_title">逐时</div>
<div class="body_main">
<div class="body_div" v-for="(i, index) in dataArr" :key="index">
<div class="body_div_icon">
<img :src="i.icon" alt="" />
</div>
<div class="body_div_text">{{ i.qecCnt }}</div>
<div class="body_div_text_sub">
{{ i.text }}
<span> () </span>
</div>
</div>
</div>
<!-- <a-spin :spinning="spinning">
<div>
<div v-for="el in datalist" @click="handleBarClick(el)">
<div :key="el.key">
<div class="boy_one">
<div style="flex: 1; white-space: nowrap; margin-right: 5px">
{{ el.name }}
</div>
<img style="flex: 1" src="@/assets/components/fgx.svg" alt="" />
</div>
<div class="body_zhu">
<div class="body_biao">
<div v-for="value in allArr">
<div :key="value" class="nei_body">
<div
v-if="
value <
(el?.key == null
? 1
: el.qecTCnt == 0
? 0
: el.qecCnt / el.qecTCnt) *
20
"
class="little_body"
:style="{ backgroundColor: el.color }"
></div>
</div>
</div>
</div>
<div class="body_text">{{ el.qecCnt }}/{{ el.qecTCnt }}</div>
</div>
</div>
</div>
</div>
</a-spin> -->
</SidePanelItem>
<!-- 自定义弹框 -->
<a-modal
v-model:open="modalVisible"
:title="'生态流量达标情况'"
width="1536px"
:footer="null"
>
<STLLXFFS
v-if="modalVisible"
:options="list"
:typeKey="selectedItem.key"
:time="mode"
:basicId="baseid"
:dateArr="dateArr"
/>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch, inject, computed, defineOptions } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import {
qgcetQgcStaticData,
evnmAutoMonitorGetKendoListCust
} from '@/api/stll';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { useMapViewStore } from '@/modules/map/stores/map-view.store';
import STLLXFFS from '@/modules/shengtaidabiaoMod/TwoLayer/ShengTaiLiuLiangDaBQKTwoLayer.vue';
import { useDraggable } from '@/utils/drag';
import icon from '@/assets/icons/client.svg';
// 便
defineOptions({
name: 'shengtaidabiaoMod'
});
const JidiSelectEventStore = useJidiSelectEventStore();
const mapViewStore = useMapViewStore();
const baseid = ref('');
const dateArr = ref<any[]>([]);
const mode = ref('hour');
const dataArr: any = ref([
{
qecCnt: '0', //
qecTCnt: '0', //
icon: icon,
text: '达标',
type: 1,
key: 'qecInterval'
},
{
icon: icon,
qecCnt: '0', //
qecTCnt: '0', //
text: '不达标',
type: 1,
key: 'qecInterval'
},
{
icon: icon,
qecCnt: '0', //
qecTCnt: '0', //
text: '无数据',
type: 1,
key: 'qecInterval'
}
]);
// const dataArr: any = ref([
// {
// name: '',
// qecCnt: '0', //
// qecTCnt: '0', //
// type: 1,
// key: 'qecInterval'
// },
// {
// name: '',
// qecCnt: '0', //
// qecTCnt: '0', //
// type: 2,
// key: 'mwrInterval'
// }
// ]);
const list: any = ref([
{ name: '生态环境部门要求', type: 1, color: '#77C300', key: 'qecInterval' },
{ name: '水利部门要求', type: 2, color: '#56C3E3', key: 'mwrInterval' },
{ name: '多年平均流量 10%', type: 3, color: '#F76B01', key: 'avqInterval' },
{ name: '无生态流量要求', type: 4, color: '#B4B4B4', key: null }
]);
const datalist: any = ref([]);
const allArr: any = ref(Array.from({ length: 20 }, (_, i) => i));
const spinning = ref(false);
const modalVisible = ref(false);
const selectedItem = ref<any>(null);
//
useDraggable(modalVisible, { boundary: true, resetOnOpen: true });
//
const handleBarClick = (item: any) => {
selectedItem.value = item;
modalVisible.value = true;
};
// type list dataArr
const setStyle = () => {
if (dataArr.value.length == 0) {
return false;
}
dataArr.value.forEach((item: any) => {
// list type
const matched = list.value.find(
(listItem: any) => listItem.type === item.type
);
if (matched) {
// dataArr
datalist.value.push({
...matched,
...item
});
}
});
console.log(datalist.value);
};
const dataChage = () => {
getData();
};
const getdate = (offsetYears = 0) => {
const formatDate = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
let startDate: string;
let endDate: string;
if (dateArr.value && dateArr.value.length === 2) {
const start = new Date(dateArr.value[0]);
const end = new Date(dateArr.value[1]);
if (offsetYears !== 0) {
start.setFullYear(start.getFullYear() + offsetYears);
end.setFullYear(end.getFullYear() + offsetYears);
}
startDate = `${formatDate(start)} 00:00:00`;
endDate = `${formatDate(end)} 23:59:59`;
} else {
const now = new Date();
const oneMonthAgo = new Date(now);
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
if (offsetYears !== 0) {
now.setFullYear(now.getFullYear() + offsetYears);
oneMonthAgo.setFullYear(oneMonthAgo.getFullYear() + offsetYears);
}
startDate = `${formatDate(oneMonthAgo)} 00:00:00`;
endDate = `${formatDate(now)} 23:59:59`;
}
return { startDate, endDate };
};
const getData = async () => {
const { startDate, endDate } = getdate(0);
let params = {
filter: {
logic: 'and',
filters: [
baseid.value != 'all'
? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: baseid.value
}
: null,
{
field: 'dtin',
operator: 'eq',
dataType: 'string',
value: '1'
},
{
field: 'type',
operator: 'eq',
dataType: 'string',
value: mode.value
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: startDate
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: endDate
}
].filter(Boolean)
}
};
spinning.value = true;
try {
let res = await qgcetQgcStaticData(params);
// API
if (res.data && res.data.list) {
const apiList = res.data.list;
//
// 使type: 1 type: 2
// if (apiList && apiList.length > 0) {
// dataArr.value = apiList.slice(0, 2).map((item: any) => ({
// // name: item.typeName || item.desc,
// qecCnt: item.qecCnt,
// qecTCnt: item.qecTCnt,
// type: item.type
// }));
// //
// datalist.value = [];
// setStyle();
// } else {
// //
// dataArr.value = [];
// datalist.value = [];
// }
}
} catch (error) {
console.error('获取生态流量达标数据失败:', error);
} finally {
spinning.value = false;
}
};
// 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(
() => injectedStation?.value || JidiSelectEventStore.selectedItem
);
watch(
() => stationSource.value,
newVal => {
if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode;
getData();
}
},
{ deep: true, immediate: true }
);
watch(
() => mapViewStore.searchTimeRange,
newVal => {
dateArr.value = newVal;
getData();
},
{ deep: true }
);
//
const titleData: any = ref({ cnt: 0 });
const getcont = async () => {
let params = {
filter: {
logic: 'and',
filters: [
{
field: 'showIds',
operator: 'in',
value: ['7']
}
]
}
};
let res = await evnmAutoMonitorGetKendoListCust(params);
let list = res?.data?.data[0];
if (list) {
titleData.value = list;
}
//
};
//
onMounted(() => {
setStyle();
getcont();
});
</script>
<style lang="scss" scoped>
.body_topOne {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.boy_one {
display: flex;
align-items: center;
margin: 5px 0px;
}
.body_title {
font-size: 14px;
font-weight: bold;
padding-left: 10px;
margin-left: 4px;
position: relative;
&::after {
content: '';
position: absolute;
top: 4px;
left: 0;
width: 4px;
height: 14px;
background-color: #2f6b98;
}
}
.body_main {
width: 100%;
display: flex;
gap: 10px;
padding: 6px 10px;
justify-content: space-between;
.body_div {
width: 33%;
height: 100%;
padding: 10px;
margin: 10px 0;
background-color: #f3f5fa;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
cursor: pointer;
border-radius: 4px;
.body_div_icon {
width: 30px;
height: 30px;
margin-bottom: 5px;
}
.body_div_text {
font-size: 28px;
font-weight: 500;
margin: 10px 0;
}
.body_div_text_sub {
font-size: 14px;
font-weight: 400;
span {
color: #949494;
}
}
}
}
.body_zhu {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: all 0.3s ease;
.body_biao {
width: 100%;
height: 52px;
border: 1px solid #ccdae7;
border-radius: 2px;
display: flex;
padding: 5px 1px;
overflow: hidden;
.nei_body {
width: 12px;
height: 100%;
border: 1px solid #ccdae7;
padding: 2px;
margin-left: 4px;
.little_body {
width: 100%;
height: 100%;
}
}
}
.body_text {
display: flex;
flex-direction: column;
width: 90px;
height: 100%;
justify-content: space-around;
margin-left: 6px;
align-items: center;
font-size: 18px;
font-weight: 500;
}
}
.ant-radio-group {
// border: 3px solid #2f6b98 !important;
// border-radius: 10px !important;
.ant-radio-button-wrapper-checked {
border: 1px solid #2f6b98 !important;
background-color: #2f6b98 !important;
color: #fff !important;
}
.ant-radio-button-wrapper {
border: 2px solid #2f6b98 !important;
}
.ant-radio-button-wrapper-checked :before {
background-color: #2f6b98 !important;
}
}
</style>

View File

@ -1,9 +1,9 @@
<!-- SidePanelItem.vue -->
<template>
<template>
<div>
<SidePanelItem title="生态流量达标情况" :click-action="{ show: true }">
<!-- 使用插槽自定义点击提示的内容 -->
<template #click-action-content>
<template #click-action-content>
<div v-if="titleData.cnt" style="max-width: 300px; line-height: 1.6">
<p>
1统计电站范围接入过生态流量数据的电站
@ -303,7 +303,9 @@ const getData = async () => {
};
// 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(() => injectedStation?.value || JidiSelectEventStore.selectedItem);
const stationSource = computed(
() => injectedStation?.value || JidiSelectEventStore.selectedItem
);
watch(
() => stationSource.value,
@ -318,8 +320,7 @@ watch(
watch(
() => mapViewStore.searchTimeRange,
(newVal) => {
newVal => {
dateArr.value = newVal;
getData();
},

View File

@ -65,7 +65,7 @@ import SidePanelItem from '@/components/SidePanelItem/index.vue';
import * as echarts from 'echarts';
import { eqqecRateCount, evnmAutoMonitorGetKendoListCust } from '@/api/stll';
import ModalYkzhbzdjcgz from '../shengtaidabiaoMod/TwoLayer/ModalYkzhbzdjcgz.vue';
import STLLXFFS from './TwoLayer/STLLXFFS.vue'
import STLLXFFS from './TwoLayer/STLLXFFS.vue';
import { useMapViewStore } from '@/modules/map/stores/map-view.store';
import { useDraggable } from '@/utils/drag';
@ -79,7 +79,7 @@ const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const mapViewStore = useMapViewStore();
//
let dateArr = ref<any[]>([]);
let dateArr = ref<any[]>([]);
const chartData = ref<{
categories: string[];
currentData: number[];
@ -312,7 +312,7 @@ const initChart = () => {
const handleChartClick = (params: any) => {
const categoryName = params.name;
const currentIndex = params.dataIndex;
//
//
//
detailModalTitle.value =
@ -321,20 +321,24 @@ const handleChartClick = (params: any) => {
: `生态流量达标率(按调节性能统计)`;
//
const baseId = mode.value === 'top' ? chartData.value.baseIds[currentIndex] : undefined;
const qecPerformance = mode.value === 'left' ? chartData.value.qecPerformances[currentIndex] : undefined;
//
const baseId =
mode.value === 'top' ? chartData.value.baseIds[currentIndex] : undefined;
const qecPerformance =
mode.value === 'left'
? chartData.value.qecPerformances[currentIndex]
: undefined;
//
const currentItem = {
baseId: baseId,
baseName: mode.value === 'top' ? categoryName :'',
baseName: mode.value === 'top' ? categoryName : '',
qecPerformance: qecPerformance,
qecPerformanceName: mode.value === 'left' ? categoryName : '',
qecRate: chartData.value.currentData[currentIndex],
beforeQecRate: chartData.value.lastYearData[currentIndex]
};
detailData.value = [currentItem];
detailData.value = [currentItem];
detailModalVisible.value = true;
};
@ -353,16 +357,15 @@ const getdate = (offsetYears = 0) => {
// dateArr.value 使
if (dateArr.value && dateArr.value.length === 2) {
const start = new Date(dateArr.value[0]);
const end = new Date(dateArr.value[1]);
// offsetYears 0
if (offsetYears !== 0) {
start.setFullYear(start.getFullYear() + offsetYears);
end.setFullYear(end.getFullYear() + offsetYears);
}
startDate = `${formatDate(start)} 00:00:00`;
endDate = `${formatDate(end)} 23:59:59`;
//
@ -475,9 +478,15 @@ const getEcharts = async () => {
]);
//
const currentApiData = (currentRes && currentRes.data && currentRes.data.data) ? currentRes.data.data : [];
const currentApiData =
currentRes && currentRes.data && currentRes.data.data
? currentRes.data.data
: [];
//
const lastYearApiData = (lastYearRes && lastYearRes.data && lastYearRes.data) ? lastYearRes.data.data : [];
const lastYearApiData =
lastYearRes && lastYearRes.data && lastYearRes.data
? lastYearRes.data.data
: [];
// mode
if (mode.value === 'top') {
@ -523,7 +532,9 @@ const getEcharts = async () => {
});
chartData.value = {
categories: currentApiData.map((item: any) => item.qecPerformanceName || ''),
categories: currentApiData.map(
(item: any) => item.qecPerformanceName || ''
),
currentData: currentApiData.map((item: any) =>
item.qecRate != null ? Number(item.qecRate) : 0
),
@ -532,7 +543,9 @@ const getEcharts = async () => {
return lastYearRate != null ? Number(lastYearRate) : 0;
}),
baseIds: [],
qecPerformances: currentApiData.map((item: any) => item.qecPerformance || '')
qecPerformances: currentApiData.map(
(item: any) => item.qecPerformance || ''
)
};
}
} catch (error) {
@ -577,7 +590,7 @@ const getcont = async () => {
if (list) {
titleData.value = list;
}
//
//
};
//
onMounted(() => {
@ -588,12 +601,12 @@ onMounted(() => {
chartInstance?.resize();
});
});
watch(
() => mapViewStore.searchTimeRange,
(newVal, oldVal) => {
console.log('时间范围变化:', oldVal, '->', newVal);
dateArr.value = newVal
dateArr.value = newVal;
loadData();
//
},

View File

@ -0,0 +1,493 @@
<!-- SidePanelItem.vue -->
<template>
<div>
<SidePanelItem title="水利部门要求生态流量达标情况">
<!-- <div class="body_topOne">
<a-radio-group v-model:value="mode" @change="dataChage">
<a-radio-button value="hour">逐时</a-radio-button>
<a-radio-button value="day"></a-radio-button>
</a-radio-group>
</div> -->
<div class="body_title">逐日</div>
<div class="body_main">
<div class="body_div" v-for="(i, index) in dataArr" :key="index">
<div class="body_div_icon">
<img :src="i.icon" alt="" />
</div>
<div class="body_div_text">{{ i.qecCnt }}</div>
<div class="body_div_text_sub">
{{ i.text }}
<span> () </span>
</div>
</div>
</div>
<div class="body_title">逐时</div>
<div class="body_main">
<div class="body_div" v-for="(i, index) in dataArr" :key="index">
<div class="body_div_icon">
<img :src="i.icon" alt="" />
</div>
<div class="body_div_text">{{ i.qecCnt }}</div>
<div class="body_div_text_sub">
{{ i.text }}
<span> () </span>
</div>
</div>
</div>
<!-- <a-spin :spinning="spinning">
<div>
<div v-for="el in datalist" @click="handleBarClick(el)">
<div :key="el.key">
<div class="boy_one">
<div style="flex: 1; white-space: nowrap; margin-right: 5px">
{{ el.name }}
</div>
<img style="flex: 1" src="@/assets/components/fgx.svg" alt="" />
</div>
<div class="body_zhu">
<div class="body_biao">
<div v-for="value in allArr">
<div :key="value" class="nei_body">
<div
v-if="
value <
(el?.key == null
? 1
: el.qecTCnt == 0
? 0
: el.qecCnt / el.qecTCnt) *
20
"
class="little_body"
:style="{ backgroundColor: el.color }"
></div>
</div>
</div>
</div>
<div class="body_text">{{ el.qecCnt }}/{{ el.qecTCnt }}</div>
</div>
</div>
</div>
</div>
</a-spin> -->
</SidePanelItem>
<!-- 自定义弹框 -->
<a-modal
v-model:open="modalVisible"
:title="'生态流量达标情况'"
width="1536px"
:footer="null"
>
<STLLXFFS
v-if="modalVisible"
:options="list"
:typeKey="selectedItem.key"
:time="mode"
:basicId="baseid"
:dateArr="dateArr"
/>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch, inject, computed, defineOptions } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import {
qgcetQgcStaticData,
evnmAutoMonitorGetKendoListCust
} from '@/api/stll';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { useMapViewStore } from '@/modules/map/stores/map-view.store';
import STLLXFFS from '@/modules/shengtaidabiaoMod/TwoLayer/ShengTaiLiuLiangDaBQKTwoLayer.vue';
import { useDraggable } from '@/utils/drag';
import icon from '@/assets/icons/client.svg';
// 便
defineOptions({
name: 'shengtaidabiaoMod'
});
const JidiSelectEventStore = useJidiSelectEventStore();
const mapViewStore = useMapViewStore();
const baseid = ref('');
const dateArr = ref<any[]>([]);
const mode = ref('hour');
const dataArr: any = ref([
{
qecCnt: '0', //
qecTCnt: '0', //
icon: icon,
text: '达标',
type: 1,
key: 'qecInterval'
},
{
icon: icon,
qecCnt: '0', //
qecTCnt: '0', //
text: '不达标',
type: 1,
key: 'qecInterval'
},
{
icon: icon,
qecCnt: '0', //
qecTCnt: '0', //
text: '无数据',
type: 1,
key: 'qecInterval'
}
]);
// const dataArr: any = ref([
// {
// name: '',
// qecCnt: '0', //
// qecTCnt: '0', //
// type: 1,
// key: 'qecInterval'
// },
// {
// name: '',
// qecCnt: '0', //
// qecTCnt: '0', //
// type: 2,
// key: 'mwrInterval'
// }
// ]);
const list: any = ref([
{ name: '生态环境部门要求', type: 1, color: '#77C300', key: 'qecInterval' },
{ name: '水利部门要求', type: 2, color: '#56C3E3', key: 'mwrInterval' },
{ name: '多年平均流量 10%', type: 3, color: '#F76B01', key: 'avqInterval' },
{ name: '无生态流量要求', type: 4, color: '#B4B4B4', key: null }
]);
const datalist: any = ref([]);
const allArr: any = ref(Array.from({ length: 20 }, (_, i) => i));
const spinning = ref(false);
const modalVisible = ref(false);
const selectedItem = ref<any>(null);
//
useDraggable(modalVisible, { boundary: true, resetOnOpen: true });
//
const handleBarClick = (item: any) => {
selectedItem.value = item;
modalVisible.value = true;
};
// type list dataArr
const setStyle = () => {
if (dataArr.value.length == 0) {
return false;
}
dataArr.value.forEach((item: any) => {
// list type
const matched = list.value.find(
(listItem: any) => listItem.type === item.type
);
if (matched) {
// dataArr
datalist.value.push({
...matched,
...item
});
}
});
console.log(datalist.value);
};
const dataChage = () => {
getData();
};
const getdate = (offsetYears = 0) => {
const formatDate = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
let startDate: string;
let endDate: string;
if (dateArr.value && dateArr.value.length === 2) {
const start = new Date(dateArr.value[0]);
const end = new Date(dateArr.value[1]);
if (offsetYears !== 0) {
start.setFullYear(start.getFullYear() + offsetYears);
end.setFullYear(end.getFullYear() + offsetYears);
}
startDate = `${formatDate(start)} 00:00:00`;
endDate = `${formatDate(end)} 23:59:59`;
} else {
const now = new Date();
const oneMonthAgo = new Date(now);
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
if (offsetYears !== 0) {
now.setFullYear(now.getFullYear() + offsetYears);
oneMonthAgo.setFullYear(oneMonthAgo.getFullYear() + offsetYears);
}
startDate = `${formatDate(oneMonthAgo)} 00:00:00`;
endDate = `${formatDate(now)} 23:59:59`;
}
return { startDate, endDate };
};
const getData = async () => {
const { startDate, endDate } = getdate(0);
let params = {
filter: {
logic: 'and',
filters: [
baseid.value != 'all'
? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: baseid.value
}
: null,
{
field: 'dtin',
operator: 'eq',
dataType: 'string',
value: '1'
},
{
field: 'type',
operator: 'eq',
dataType: 'string',
value: mode.value
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: startDate
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: endDate
}
].filter(Boolean)
}
};
spinning.value = true;
try {
let res = await qgcetQgcStaticData(params);
// API
if (res.data && res.data.list) {
const apiList = res.data.list;
//
// 使type: 1 type: 2
// if (apiList && apiList.length > 0) {
// dataArr.value = apiList.slice(0, 2).map((item: any) => ({
// // name: item.typeName || item.desc,
// qecCnt: item.qecCnt,
// qecTCnt: item.qecTCnt,
// type: item.type
// }));
// //
// datalist.value = [];
// setStyle();
// } else {
// //
// dataArr.value = [];
// datalist.value = [];
// }
}
} catch (error) {
console.error('获取生态流量达标数据失败:', error);
} finally {
spinning.value = false;
}
};
// 使 provide jidiStore
const injectedStation = inject<any>('dianZhanStation', null);
const stationSource = computed(
() => injectedStation?.value || JidiSelectEventStore.selectedItem
);
watch(
() => stationSource.value,
newVal => {
if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode;
getData();
}
},
{ deep: true, immediate: true }
);
watch(
() => mapViewStore.searchTimeRange,
newVal => {
dateArr.value = newVal;
getData();
},
{ deep: true }
);
//
const titleData: any = ref({ cnt: 0 });
const getcont = async () => {
let params = {
filter: {
logic: 'and',
filters: [
{
field: 'showIds',
operator: 'in',
value: ['7']
}
]
}
};
let res = await evnmAutoMonitorGetKendoListCust(params);
let list = res?.data?.data[0];
if (list) {
titleData.value = list;
}
//
};
//
onMounted(() => {
setStyle();
getcont();
});
</script>
<style lang="scss" scoped>
.body_topOne {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.boy_one {
display: flex;
align-items: center;
margin: 5px 0px;
}
.body_title {
font-size: 14px;
font-weight: bold;
padding-left: 10px;
margin-left: 4px;
position: relative;
&::after {
content: '';
position: absolute;
top: 4px;
left: 0;
width: 4px;
height: 14px;
background-color: #2f6b98;
}
}
.body_main {
width: 100%;
display: flex;
gap: 10px;
padding: 6px 10px;
justify-content: space-between;
.body_div {
width: 33%;
height: 100%;
padding: 10px;
margin: 10px 0;
background-color: #f3f5fa;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
border-radius: 4px;
.body_div_icon {
width: 30px;
height: 30px;
margin-bottom: 5px;
}
.body_div_text {
font-size: 28px;
font-weight: 500;
margin: 10px 0;
}
.body_div_text_sub {
font-size: 14px;
font-weight: 400;
span {
color: #949494;
}
}
}
}
.body_zhu {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: all 0.3s ease;
.body_biao {
width: 100%;
height: 52px;
border: 1px solid #ccdae7;
border-radius: 2px;
display: flex;
padding: 5px 1px;
overflow: hidden;
.nei_body {
width: 12px;
height: 100%;
border: 1px solid #ccdae7;
padding: 2px;
margin-left: 4px;
.little_body {
width: 100%;
height: 100%;
}
}
}
.body_text {
display: flex;
flex-direction: column;
width: 90px;
height: 100%;
justify-content: space-around;
margin-left: 6px;
align-items: center;
font-size: 18px;
font-weight: 500;
}
}
.ant-radio-group {
// border: 3px solid #2f6b98 !important;
// border-radius: 10px !important;
.ant-radio-button-wrapper-checked {
border: 1px solid #2f6b98 !important;
background-color: #2f6b98 !important;
color: #fff !important;
}
.ant-radio-button-wrapper {
border: 2px solid #2f6b98 !important;
}
.ant-radio-button-wrapper-checked :before {
background-color: #2f6b98 !important;
}
}
</style>

View File

@ -0,0 +1,592 @@
<template>
<div>
<SidePanelItem
title="各基地水温监测开展情况"
@update-values="handlePanelChange"
>
<!-- 加载状态 -->
<div v-if="dataLoading" class="chart-loading">
<a-spin tip="加载中..." />
</div>
<!-- 无数据提示 -->
<a-empty
v-if="!chartData.length && !dataLoading"
description="暂无数据"
class="chart-empty"
/>
<!-- 图表容器使用 display 控制避免 v-if 销毁 DOM-->
<div
ref="chartRef"
class="chart-container"
:style="{
display: chartData.length && !dataLoading ? 'block' : 'none'
}"
></div>
</SidePanelItem>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, nextTick, onBeforeUnmount, defineOptions } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
// import { wbsbGetKendoList } from '@/api/sz';
import * as echarts from 'echarts';
import dayjs from 'dayjs';
// TODO: API
import { qgcHourVGetKendoListCust, wbsbGetKendoList } from '@/api/stll';
import { useModelStore } from '@/store/modules/model';
//
defineOptions({
name: 'ShuiWenJianCeKaiZhanQingKuang'
});
// ==================== ====================
//
const datetimePicker = ref({
show: true,
value: dayjs().format('YYYY-MM-DD HH:mm'), //
format: 'YYYY-MM-DD HH:mm',
picker: 'date',
timeFormat: 'HH:mm',
options: []
});
//
const select = ref({
show: true,
value: '',
options: [],
picker: undefined,
format: undefined
});
const modelStore = useModelStore();
//
const wbsCode = ref(''); //
const selectData = ref(''); //
const chartData = ref<any[]>([]); //
const dataLoading = ref(false); //
const latestTime = ref(''); //
const hasTimeData = ref(false); //
//
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
//
const modalVisible = ref(false);
const modalData = ref<any>({
stcd: '',
stnm: '',
titleName: ''
});
// Store
const JidiSelectEventStore = useJidiSelectEventStore();
// ==================== ====================
/**
* 获取河段列表
*/
const getSelect = async () => {
if (!wbsCode.value) {
console.warn('wbsCode 为空,无法获取河段数据');
return;
}
let params = {
filter: {
logic: 'and',
filters: [
{
field: 'objId',
operator: 'eq',
value: wbsCode.value
},
{
field: 'wbsType',
operator: 'eq',
value: 'PSB_RVCD'
}
]
},
group: [
{
dir: 'asc',
field: 'orderIndex'
},
{
dir: 'asc',
field: 'wbsCode'
},
{
dir: 'asc',
field: 'wbsName'
},
{
dir: 'asc',
field: 'objid'
}
],
groupResultFlat: 'true'
};
try {
let res = await wbsbGetKendoList(params);
//
let data;
if (res && res.data && res.data.data) {
if (Array.isArray(res.data.data)) {
data = res.data.data;
} else if (res.data.data.data && Array.isArray(res.data.data.data)) {
data = res.data.data.data;
} else {
console.warn('无法解析河段数据:', res);
return;
}
} else {
console.warn('未获取到河段数据');
return;
}
if (data && data.length > 0) {
// ;
select.value.value = data[0].wbsCode;
selectData.value = data[0].wbsCode; //
select.value.options = data.map(item => ({
value: item.wbsCode,
label: item.wbsName
}));
//
await loadChartData();
} else {
console.warn('河段数据为空');
select.value.value = '';
select.value.options = [];
}
} catch (error) {
console.error('获取河段数据失败:', error);
}
};
/**
* 获取生态流量数据预留 API 位置
*/
const getList = async (rvcd: string, sDate: string) => {
// TODO: API
// React
// - baseId: JidiSelectEventStore.selectedItem
// - rvcd:
// - tm: 1
const currentTime = dayjs(sDate);
const startTime = currentTime.subtract(1, 'month');
let params = {
filter: {
logic: 'and',
filters: [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: wbsCode.value
},
{
field: 'rvcd',
operator: 'eq',
dataType: 'string',
value: rvcd
},
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: startTime.format('YYYY-MM-DD HH:mm:ss')
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: currentTime.format('YYYY-MM-DD HH:mm:ss')
}
]
},
sort: [
{
field: 'sort',
dir: 'asc'
}
]
};
// TODO: API FAA
const res = await qgcHourVGetKendoListCust(params);
//
if (!res || !res.data || !res.data.data || !Array.isArray(res.data.data)) {
console.warn('未获取到有效的生态流量数据:', res);
return [];
}
//
return res.data.data.map((item: any) => ({
TM: dayjs(item.tm).format('YYYY-MM-DD H时'),
stnm: item.stnm,
stcd: item.stcd,
qec: item.qec, //
qecLimit: item.qecLimit, //
qi: item.qi //
}));
};
/**
* 加载图表数据
*/
const loadChartData = async () => {
if (!selectData.value || !datetimePicker.value.value) {
return;
}
dataLoading.value = true;
try {
const data = await getList(selectData.value, datetimePicker.value.value);
if (data && data.length > 0) {
chartData.value = data;
hasTimeData.value = true;
//
if (data[0]?.TM) {
latestTime.value = data[0].TM;
}
//
await nextTick();
initChart();
} else {
chartData.value = [];
hasTimeData.value = false;
latestTime.value = '';
//
if (chartInstance) {
chartInstance.clear();
}
}
} catch (error) {
console.error('加载图表数据失败:', error);
chartData.value = [];
} finally {
dataLoading.value = false;
}
};
/**
* 初始化 ECharts 图表
*/
const initChart = () => {
if (!chartRef.value) {
console.warn('图表容器未准备好');
return;
}
//
if (chartInstance) {
chartInstance.dispose();
}
//
chartInstance = echarts.init(chartRef.value);
//
const xData: string[] = [];
const inData: (number | string)[] = []; //
const outData: (number | string)[] = []; //
let hasValidData = false; //
chartData.value.forEach(item => {
xData.push(item.stnm);
const qecValue = item.qec ?? '-';
const qiValue = item.qi ?? '-';
outData.push(qecValue);
inData.push(qiValue);
//
if (qecValue !== '-' || qiValue !== '-') {
hasValidData = true;
}
});
//
if (!hasValidData && chartData.value.length > 0) {
chartData.value = [];
if (chartInstance) {
chartInstance.clear();
}
return;
}
//
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'axis',
confine: true,
formatter: function (params: any) {
const datum = chartData.value.find(
(el: any) => el.stnm === params[0].name
);
const unit = 'm³/s'; // TODO:
const content = params
.map((item: any) => {
const value = item.value ?? '-';
return `<div>${item.marker}${item.seriesName}: &nbsp;&nbsp;&nbsp;&nbsp;<span style="float:right">${value} ${unit}</span></div>`;
})
.join('');
return `<h3 style="color:white">${params[0].name}<br/>${
datum?.TM || ''
}</h3>${content}`;
},
axisPointer: {
type: 'line'
}
},
legend: {
data: ['表层水温', '垂向水温'],
icon: 'roundRect',
top: 10
},
grid: {
left: 10,
right: 10,
bottom: 10,
top: 70,
containLabel: true
},
xAxis: {
type: 'category',
data: xData,
axisLine: {
lineStyle: {
color: '#505050'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#8f8f8f',
type: 'solid'
}
},
axisLabel: {
interval: xData.length > 12 ? 2 : 0,
rotate: 0,
formatter: (value: string, index: number) => {
const shortValue =
value.substring(0, 6) + (value.length > 6 ? '...' : '');
if (index % 2 !== 0) {
return `{a|${shortValue}}`;
} else {
return `\n{b|}${shortValue}`;
}
},
rich: {
a: {
height: 20,
align: 'center'
},
b: {
height: 30,
align: 'center'
}
}
}
},
yAxis: {
type: 'value',
minInterval: 1,
axisLine: {
lineStyle: {
color: '#505050'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#8f8f8f',
type: 'solid'
}
}
},
dataZoom: [
{
type: 'inside',
xAxisIndex: 0,
start: 0,
end: 100,
zoomOnMouseWheel: true,
moveOnMouseMove: true
}
],
series: [
{
name: '表层水温',
data: inData,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
connectNulls: true,
color: '#5470c6' // TODO:
},
{
name: '垂向水温',
data: outData,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
connectNulls: true,
color: '#91cc75' // TODO:
}
]
};
chartInstance.setOption(option);
//
chartInstance.on('click', handleChartClick);
//
window.addEventListener('resize', handleResize);
};
/**
* 图表点击事件
*/
const handleChartClick = (params: any) => {
const datum = chartData.value.find((item: any) => item.stnm === params.name);
if (datum) {
const currentTime = dayjs(datetimePicker.value.value);
const startTime = currentTime.subtract(1, 'month');
modalVisible.value = true;
modelStore.modalVisible = true;
modelStore.params.sttp = 'ENG';
modelStore.title = datum.stnm;
modelStore.params.stcd = datum.stcd;
modelStore.params.eqtp = 'QEC';
modelStore.filter.rangeTm = [startTime, currentTime];
modelStore.filter.stllgzlx = 'qecInterval';
modelStore.filter.stllTmType = 'day';
}
};
/**
* 窗口大小改变时重新调整图表
*/
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
};
/**
* 弹窗取消事件
*/
const handleModalCancel = () => {
modalVisible.value = false;
modalData.value = {
stcd: '',
stnm: '',
titleName: ''
};
};
/**
* 面板数据变化处理
*/
const handlePanelChange = async (data: any) => {
console.log('当前所有控件状态:', data);
//
if (data.select) {
select.value.value = data.select;
selectData.value = data.select;
}
if (data.datetime) {
datetimePicker.value.value = data.datetime;
}
//
if (data.select || data.datetime) {
await loadChartData();
}
};
// ==================== ====================
//
watch(
() => JidiSelectEventStore.selectedItem,
newVal => {
console.log('流域切换:', newVal);
if (newVal?.wbsCode) {
wbsCode.value = newVal.wbsCode;
getSelect();
}
},
{ deep: true, immediate: true }
);
//
onBeforeUnmount(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener('resize', handleResize);
});
</script>
<style lang="scss" scoped>
.chart-loading {
display: flex;
justify-content: center;
align-items: center;
height: 424px;
}
.chart-empty {
display: flex;
align-items: center;
height: 424px;
flex-direction: column;
/* align-content: center; */
justify-content: center;
text-align: center !important;
:deep(.ant-empty-description) {
text-align: center !important;
text-indent: 0;
width: 100%;
}
}
.chart-container {
width: 406px;
height: 424px;
}
.time-tip {
text-align: right;
font-size: 12px;
color: #8e8e8e;
padding: 8px 0;
margin-top: 4px;
}
</style>

View File

@ -1,26 +1,37 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="水温监测工作开展情况">
<SidePanelItem title="水温监测统计">
<a-spin :spinning="loading" tip="加载中...">
<div class="facility-grid">
<div v-for="facility in facilities" :key="facility.name" class="facility-card"
@click="handleFacilityClick(facility)">
<div style="
<div
v-for="facility in facilities"
:key="facility.name"
class="facility-card"
@click="handleFacilityClick(facility)"
>
<div
style="
width: 60px;
height: 62px;
display: flex;
align-items: center;
justify-content: center;
">
"
>
<div class="facility-icon">
<i style="color: #fff" :class="facility.icon" type="icon-shengtailiuliang2"></i>
<i
style="color: #fff"
:class="facility.icon"
type="icon-shengtailiuliang2"
></i>
</div>
</div>
<div class="facility-info">
<div class="facility-name">{{ facility.name }}</div>
<div style="font-size: 16px">
<span class="facility-count">{{ facility.count }}</span><span></span>
<span class="facility-count">{{ facility.count }}</span
><span></span>
</div>
</div>
</div>
@ -28,40 +39,54 @@
</a-spin>
<!-- 弹框 -->
<a-modal v-model:open="modalVisible" :title="'水温监测工作开展情况'" :footer="null" width="1536px" @cancel="handleCloseModal">
<ShuiwenjiancegongzuoEJ v-if="currentFacility" :activeKey="currentFacility.key" :tabs="facilities" :baseId="baseid" />
<a-modal
v-model:open="modalVisible"
:title="'水温监测工作开展情况'"
:footer="null"
width="1536px"
@cancel="handleCloseModal"
>
<ShuiwenjiancegongzuoEJ
v-if="currentFacility"
:activeKey="currentFacility.key"
:tabs="facilities"
:baseId="baseid"
/>
</a-modal>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch } from "vue";
import SidePanelItem from "@/components/SidePanelItem/index.vue";
import { getKendoListCust, baseEvnmAutoMonitorGetKendoListCust } from "@/api/sw";
import { useJidiSelectEventStore } from "@/store/modules/jidiSelectEvent";
import ShuiwenjiancegongzuoEJ from "./shuiwenjiancegongzuoEJ.vue"
import { ref, onMounted, watch } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import {
getKendoListCust,
baseEvnmAutoMonitorGetKendoListCust
} from '@/api/sw';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import ShuiwenjiancegongzuoEJ from './shuiwenjiancegongzuoEJ.vue';
import { useDraggable } from '@/utils/drag';
const JidiSelectEventStore = useJidiSelectEventStore();
// 便
defineOptions({
name: "shuiwenjiancegongzuokaizhangqingkuang",
name: 'shuiwenjiancegongzuokaizhangqingkuang'
});
//
const facilities = ref([
{
name: "表层水温",
name: '表层水温',
count: 0,
icon: "icon iconfont icon-shuizhijiancezhan",
key:'26'
icon: 'icon iconfont icon-shuizhijiancezhan',
key: '26'
},
{
name: "垂向水温",
name: '垂向水温',
count: 0,
icon: "icon iconfont icon-diwenshuijianhuan",
key:'27'
},
icon: 'icon iconfont icon-diwenshuijianhuan',
key: '27'
}
]);
// Loading
@ -76,8 +101,6 @@ const handleFacilityClick = (facility: any) => {
// console.log(facility);
currentFacility.value = facility;
modalVisible.value = true;
};
//
@ -93,30 +116,29 @@ const init = async () => {
try {
loading.value = true;
const params = {
"filter": {
"logic": "and",
"filters": [
filter: {
logic: 'and',
filters: [
{
"field": "showIds",
"operator": "in",
"value": [
"26",
"27"
]
field: 'showIds',
operator: 'in',
value: ['26', '27']
},
baseid.value != 'all' ? {
"field": "baseId",
"operator": "eq",
"value": baseid.value
} : null
baseid.value != 'all'
? {
field: 'baseId',
operator: 'eq',
value: baseid.value
}
: null
].filter(Boolean)
}
}
};
let res = await baseEvnmAutoMonitorGetKendoListCust(params);
console.log(res);
let data = res.data.data || res.data
let data = res.data.data || res.data;
data.forEach((item: any, index: any) => {
facilities.value[index].count = item.cnt
facilities.value[index].count = item.cnt;
});
} catch (error) {
console.error('获取水温监测数据失败:', error);
@ -130,10 +152,10 @@ onMounted(() => {
init();
});
const baseid = ref("")
const baseid = ref('');
watch(
() => JidiSelectEventStore.selectedItem,
(newVal) => {
newVal => {
baseid.value = newVal.wbsCode;
init();
},
@ -147,9 +169,9 @@ watch(
flex-flow: wrap;
display: flex;
justify-content: space-between;
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;
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;
}
.facility-card {

View File

@ -3,7 +3,6 @@
<SidePanelItem
title="沿程水温变化"
:prompt="prompts"
:moreSelect="select"
:datetimePicker="datetimePicker"
@update-values="handlePanelChange1"
>
@ -21,7 +20,14 @@
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, watch, computed } from 'vue';
import {
ref,
onMounted,
onBeforeUnmount,
watch,
computed,
defineOptions
} from 'vue';
import * as echarts from 'echarts';
import type { ECharts } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
@ -293,12 +299,14 @@ const init = async () => {
filter: {
logic: 'and',
filters: [
select.value.value != 'all' ? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: select.value.value
}:null,
select.value.value != 'all'
? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: select.value.value
}
: null,
{
field: 'tm',
operator: 'gte',
@ -374,7 +382,6 @@ const init = async () => {
};
//
const handlePanelChange1 = data => {
console.log('当前所有控件状态:', data);
if (data.moreSelect) {
select.value.value = data.moreSelect;
@ -402,15 +409,6 @@ const handlePanelChange1 = data => {
const previousDate = new Date(year, month - 2, day, 0, 0, 0);
// Date "YYYY-MM-DD HH:mm:ss"
function formatDate(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const h = String(date.getHours()).padStart(2, '0');
const min = String(date.getMinutes()).padStart(2, '0');
const s = String(date.getSeconds()).padStart(2, '0');
return `${y}-${m}-${d} ${h}:${min}:${s}`;
}
//
const result = [
@ -425,12 +423,21 @@ const handlePanelChange1 = data => {
}
};
function formatDate(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const h = String(date.getHours()).padStart(2, '0');
const min = String(date.getMinutes()).padStart(2, '0');
const s = String(date.getSeconds()).padStart(2, '0');
return `${y}-${m}-${d} ${h}:${min}:${s}`;
}
watch(
() => JidiSelectEventStore.selectedItem,
newVal => {
// if (newVal.baseId) {
select.value.value = newVal.wbsCode;
// init();
select.value.value = newVal.wbsCode;
init();
// }
},
{ deep: true, immediate: true }

View File

@ -0,0 +1,467 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem
title="各水电基地增殖站分布统计"
:datetimePicker="datetimePicker"
@update-values="handlePanelChange1"
>
<a-spin :spinning="loading" tip="加载中...">
<div
v-show="showemit && !loading"
ref="chartRef"
class="chart-container"
></div>
<div v-show="!showemit && !loading" class="chart-container">
<a-empty />
</div>
</a-spin>
</SidePanelItem>
</template>
<script lang="ts" setup>
import {
ref,
onMounted,
onBeforeUnmount,
watch,
computed,
defineOptions
} from 'vue';
import * as echarts from 'echarts';
import type { ECharts } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { getKendoListCust } from '@/api/sw';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { useModelStore } from '@/store/modules/model';
const modelStore = useModelStore();
// 便
defineOptions({
name: 'yanchengshuiwenChangeMod'
});
//
const getCurrentHourTime = () => {
const now = new Date();
//
now.setHours(now.getHours() - 1);
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hour = String(now.getHours()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}`;
};
const JidiSelectEventStore = useJidiSelectEventStore();
const paramsOne = {
tm: []
};
const dataOne = ref('');
// minTm
const rawTableData = ref<any[]>([]);
// 使 computed 使 prompts minTm
const showemit = ref(false);
const loading = ref(false);
// const selectOptions: any = ref([])
const optionsFmt = (options: any) =>
options.map((item: any) => ({
label: item.wbsName,
value: item.wbsCode
}));
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: ECharts | null = null;
// -
const stationNames = ref([]);
// - (°C) null使stcd
const waterTemperatures = ref<
{ value: number | null; stcd: string; stnm: string }[]
>([]);
// 使 computed 使 currentTime dataOne.value
const currentTime = computed(() => dataOne.value || getCurrentHourTime());
//
const select = ref({
show: true,
value: '',
options: [],
picker: undefined,
format: undefined
});
//
const datetimePicker = ref({
show: true,
value: computed(() => getCurrentHourTime()),
format: 'YYYY-MM-DD HH',
picker: 'date' as const,
options: []
});
//
const initChart = () => {
if (!chartRef.value) return;
chartInstance = echarts.init(chartRef.value);
const option = {
title: {
text: '水温(°C)',
left: 5,
top: 0,
textStyle: {
color: '#000000',
fontSize: 12,
fontWeight: 'normal'
}
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
zoomOnMouseWheel: true,
moveOnMouseMove: true,
filterMode: 'filter'
}
],
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
if (params && params.length > 0) {
const data = params[0];
// 线 `-`
if (data.value != null) {
return `${currentTime.value}<br/>${data.name}${data.value}°C`;
} else {
return `${currentTime.value}<br/>${data.name}-`;
}
}
return '';
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: stationNames.value,
boundaryGap: true,
axisLine: {
show: true,
lineStyle: {
color: '#8f8f8f'
}
},
axisTick: {
show: false
},
axisLabel: {
color: '#333',
fontSize: 12,
margin: 8,
interval: 0,
formatter: (value: string, index: number) => {
// 0, 2, 4, 6
// 1, 3, 5
//
if (index % 2 === 0) {
return `${value}\n `; // + +
} else {
return ` \n${value}`; // + +
}
}
},
splitLine: {
show: true,
lineStyle: {
color: '#e0e0e0',
type: 'solid'
}
}
},
yAxis: {
type: 'value',
min: 0,
// max: 10,
interval: 2,
axisLine: {
show: true,
lineStyle: {
color: '#8f8f8f'
}
},
axisTick: {
show: true,
length: 3,
lineStyle: {
color: '#8f8f8f'
}
},
axisLabel: {
color: '#333',
fontSize: 12
},
splitLine: {
show: true,
lineStyle: {
color: '#e0e0e0',
type: 'solid'
}
}
},
series: [
{
name: '水温',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
connectNulls: true, // 线线
lineStyle: {
color: '#6ca4f7',
width: 2
},
itemStyle: {
color: '#6ca4f7'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(108, 164, 247, 0.3)'
},
{
offset: 1,
color: 'rgba(108, 164, 247, 0.05)'
}
])
},
data: waterTemperatures.value
}
]
};
chartInstance.setOption(option);
//
chartInstance.on('click', (params: any) => {
if (params.componentType === 'series') {
console.log('点击数据点:', params);
console.log('stcd:', params.data.stcd);
console.log('stnm:', params.data.stnm);
console.log('temperature:', params.data.value);
console.log('temperature:', paramsOne.tm);
modelStore.modalVisible = true;
modelStore.params.sttp = 'WT';
modelStore.title = params.data.stnm;
modelStore.params.stcd = params.data.stcd;
modelStore.filter.rangeTm = [paramsOne.tm[1], paramsOne.tm[0]];
}
});
};
//
const handleResize = () => {
chartInstance?.resize();
};
const init = async () => {
if (loading.value) return; //
loading.value = true;
try {
const params = {
filter: {
logic: 'and',
filters: [
select.value.value != 'all'
? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: select.value.value
}
: null,
{
field: 'tm',
operator: 'gte',
dataType: 'date',
value: paramsOne.tm[1]
},
{
field: 'tm',
operator: 'lte',
dataType: 'date',
value: paramsOne.tm[0]
}
].filter(Boolean)
},
sort: [{ field: 'sort', dir: 'asc' }]
};
let res = await getKendoListCust(params);
let data = res.data.data || res.data;
// minTm
rawTableData.value = data;
if (data.length > 0) {
showemit.value = true;
} else {
showemit.value = false;
}
stationNames.value = data
.filter((item: any) => item.sttp == '1')
.map((item: any) => item.stnm);
console.log(stationNames.value);
// stcdstnm
waterTemperatures.value = data
.filter((item: any) => item.sttp == '2')
.map((item: any) => ({
value: item.temperature,
stcd: item.stcd,
stnm: item.stnm
}));
console.log(waterTemperatures.value);
//
setTimeout(() => {
if (!chartInstance) {
initChart();
} else {
//
chartInstance.setOption({
xAxis: {
data: stationNames.value
},
series: [
{
data: waterTemperatures.value
}
]
});
}
//
setTimeout(() => {
chartInstance?.resize();
}, 100);
}, 50);
} catch (error) {
console.error('数据加载失败:', error);
showemit.value = false;
} finally {
loading.value = false;
}
};
//
const handlePanelChange1 = data => {
console.log('当前所有控件状态:', data);
if (data.moreSelect) {
select.value.value = data.moreSelect;
}
if (data.datetime) {
dataOne.value = data.datetime;
const datetime = data.datetime;
//
const [datePart, hourPart] = datetime.split(' ');
const [year, month, day] = datePart.split('-').map(Number);
// Date
const currentDate = new Date(
year,
month - 1,
day,
parseInt(hourPart),
0,
0
);
// Date
const previousDate = new Date(year, month - 2, day, 0, 0, 0);
// Date "YYYY-MM-DD HH:mm:ss"
//
const result = [
formatDate(currentDate), // "2026-05-15 15:00:00"
formatDate(previousDate) // "2026-04-15 00:00:00"
];
paramsOne.tm = result;
console.log(paramsOne.tm, 'paramsOne.tm');
}
if (select.value.value || paramsOne.tm.length > 0) {
init();
}
};
function formatDate(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const h = String(date.getHours()).padStart(2, '0');
const min = String(date.getMinutes()).padStart(2, '0');
const s = String(date.getSeconds()).padStart(2, '0');
return `${y}-${m}-${d} ${h}:${min}:${s}`;
}
watch(
() => JidiSelectEventStore.selectedItem,
newVal => {
// if (newVal.baseId) {
select.value.value = newVal.wbsCode;
init();
// }
},
{ deep: true, immediate: true }
);
// JidiSelectEventStore.jidiData ""
watch(
() => JidiSelectEventStore.jidiData,
newVal => {
select.value.options = newVal.map(item => ({
value: item.wbsCode,
title: item.wbsName
}));
//
if (!select.value.value && newVal.length > 0) {
select.value.value = newVal[0].baseId || newVal[0].wbsCode;
}
},
{ deep: true, immediate: true }
);
//
onMounted(() => {
// DOM
// init();
window.addEventListener('resize', handleResize);
});
//
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
chartInstance?.dispose();
});
</script>
<style lang="scss" scoped>
.chart-container {
width: 100%;
height: 252px;
display: flex;
justify-content: center;
align-items: center;
}
:deep(.ant-spin-nested-loading) {
height: 252px !important;
}
</style>

View File

@ -800,7 +800,10 @@ export const useMapStore = defineStore('map', () => {
mapDataStore.setPointLayerCacheChecked(key, true);
}
} else {
console.warn(`图层 ${key} 没有缓存数据`);
// 无缓存数据:勾选态下触发加载,否则图层树勾选无缓存图层时锚点永远不出现。
// 不阻塞调用方updateLayerData 会 await 本函数),交给 loadLayerData 的
// 请求去重 / abort / 图例暂存机制统一处理。
void loadLayerData(layer);
}
};
@ -1360,6 +1363,7 @@ export const useMapStore = defineStore('map', () => {
requestData
});
if (hasEffectivePointLayerCache(key, requestIdentifier)) {
syncBackgroundCacheToReactiveStore(key);
const cachedList = getEffectivePointLayerData(key);

View File

@ -22,6 +22,12 @@ export const useModelStore = defineStore('model', () => {
const wqShow = ref<boolean>(false); // 水质 AI 预测显示
const fishTabInfo = ref<any>(null); // 鱼类监测数据信息
const selectedAnchorPoint = ref<any>(null); // 地图筛选下拉选中的点位对象(供基本信息等面板消费)
// 全局抽屉内容模式:'basicInfo' 展示基础信息,'speciesIntro' 展示物种介绍(动物/植物介绍)
const globalDrawerMode = ref<'basicInfo' | 'speciesIntro'>('basicInfo');
// 鱼种下拉选中的物种列表(供物种介绍面板展示;预留 image/intro 等字段,后续对接真实数据源)
const selectedFishSpecies = ref<
Array<{ label: string; value: string; image?: string; intro?: string }>
>([]);
const params = ref<{
sttp: string;
@ -77,6 +83,8 @@ export const useModelStore = defineStore('model', () => {
wtConfig,
wqShow,
fishTabInfo,
selectedAnchorPoint
selectedAnchorPoint,
globalDrawerMode,
selectedFishSpecies
};
});

View File

@ -8,6 +8,17 @@ import { ref } from 'vue';
const modules = import.meta.glob('../../views/**/**.vue');
export const Layout = () => import('@/layout/index.vue');
// 路由生成后后台预取所有页面 chunk首次导航若依赖懒加载 chunk 下载,
// 会被首页地图加载的十几个接口挤占连接导致页面迟迟不切换,预取后切页即时生效。
let viewPrefetchStarted = false;
const prefetchViewChunks = () => {
if (viewPrefetchStarted) return;
viewPrefetchStarted = true;
Object.values(modules).forEach(load => {
load().catch(() => {});
});
};
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
const res: RouteRecordRaw[] = [];
routes.forEach(route => {
@ -61,6 +72,8 @@ export const usePermissionStore = defineStore('permission', () => {
const asyncRoutes: any = response;
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
setRoutes(accessedRoutes);
// 后台预取页面 chunk不等结果不阻塞导航
prefetchViewChunks();
resolve(accessedRoutes);
})
.catch(error => {

View File

@ -173,15 +173,21 @@ export function downloadFileByUrl(url: string, fileName?: string) {
document.body.removeChild(link);
});
}
const modules = import.meta.glob('@/assets/legend/*.{svg,png}', {
eager: true
// 图例图标以 data URL 形式内联进打包产物(?raw + encodeURIComponent
// 图标零网络请求、无跨域/连接池争用问题,所有图层(无论是否勾选)的图标均即时可用。
const modules = import.meta.glob('@/assets/legend/*.svg', {
eager: true,
query: '?raw',
import: 'default'
});
// 图例图标映射
export const iconMap: Record<string, string> = {};
Object.entries(modules).forEach(([path, module]) => {
const fileName = path.match(/\/([^/]+)\.(svg|png)$/)?.[1];
const fileName = path.match(/\/([^/]+)\.svg$/)?.[1];
if (fileName) {
iconMap[fileName] = (module as any).default;
iconMap[fileName] = `data:image/svg+xml,${encodeURIComponent(
module as string
)}`;
}
});
// 获取图标路径

View File

@ -0,0 +1,296 @@
<template>
<a-modal
:visible="visible"
:title="title"
:footer="null"
width="1200px"
:body-style="{ padding: 0, height: '620px' }"
:z-index="1050"
@cancel="handleCancel"
destroy-on-close
>
<div class="msp-toolbar">
<span class="msp-toolbar-label">搜索范围</span>
<a-input-number
v-model:value="searchRadius"
:min="1"
:max="500"
style="width: 80px"
@blur="onRadiusConfirm"
@keyup.enter="onRadiusConfirm"
/>
<span class="msp-unit">km</span>
<a-button
type="primary"
size="small"
style="margin-left: 8px"
@click="handleSave"
>保存设置</a-button
>
</div>
<div class="msp-map-area">
<div ref="mapContainer" class="msp-map-container" />
<!-- 地图工具按钮悬浮在地图底部 -->
<div class="msp-map-tools">
<a-button size="small" class="msp-tool-btn" @click="handleZoomIn"
>+</a-button
>
<a-button size="small" class="msp-tool-btn" @click="handleZoomOut"
>-</a-button
>
<a-button
size="small"
class="msp-tool-btn"
:type="isMeasuring ? 'primary' : 'default'"
@click="handleToggleMeasure"
>测距</a-button
>
<a-button size="small" class="msp-tool-btn" @click="handleClearMeasure"
>清除</a-button
>
</div>
<!-- 右侧悬浮面板 -->
<div class="msp-right-panel">
<div class="msp-panel-title">站点列表</div>
<div class="msp-station-list">
<div
v-for="station in filteredStations"
:key="station.name"
class="msp-station-item"
:class="{ 'msp-station-selected': selectedNames.has(station.name) }"
@click="toggleStation(station.name)"
>
<span class="msp-station-name">{{ station.name }}</span>
<span class="msp-station-type">{{ station.type }}</span>
<span
class="msp-station-check"
:class="{ 'msp-check-visible': selectedNames.has(station.name) }"
>
<CheckOutlined />
</span>
</div>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue';
import { CheckOutlined } from '@ant-design/icons-vue';
import { StationMap } from './map.station';
const props = defineProps<{
visible: boolean;
title: string;
type: 'weather' | 'upstream';
centerLon?: number;
centerLat?: number;
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'save', selected: string[]): void;
}>();
const stationMap = new StationMap();
const mapContainer = ref<HTMLElement | null>(null);
const isMeasuring = ref(false);
const searchRadius = ref(10);
const allStations = [
{ name: '气象站A', type: '气象站' },
{ name: '气象站B', type: '气象站' },
{ name: '气象站C', type: '气象站' },
{ name: '上游站1', type: '上游站' },
{ name: '上游站2', type: '上游站' },
{ name: '上游站3', type: '上游站' }
];
const filteredStations = computed(() => {
const targetTypeLabel = props.type === 'weather' ? '气象站' : '上游站';
return allStations.filter(s => s.type === targetTypeLabel);
});
const selectedNames = ref<Set<string>>(new Set());
const toggleStation = (name: string) => {
const s = new Set(selectedNames.value);
if (s.has(name)) s.delete(name);
else s.add(name);
selectedNames.value = s;
};
const initMap = async () => {
await nextTick();
setTimeout(() => {
if (!mapContainer.value) return;
const lon = props.centerLon ?? 114.171;
const lat = props.centerLat ?? 38;
stationMap.init(mapContainer.value, lon, lat, searchRadius.value);
}, 100);
};
watch(
() => props.visible,
async val => {
if (val) {
//
searchRadius.value = 10;
isMeasuring.value = false;
selectedNames.value = new Set();
await initMap();
} else {
stationMap.destroy();
}
}
);
//
const onRadiusConfirm = () => {
const val = searchRadius.value;
if (val && val >= 1) {
stationMap.updateRange(val);
}
};
const handleZoomIn = () => stationMap.zoomIn();
const handleZoomOut = () => stationMap.zoomOut();
const handleToggleMeasure = () => {
stationMap.toggleMeasure();
isMeasuring.value = stationMap.isMeasureActive;
};
const handleClearMeasure = () => {
stationMap.clearMeasure();
isMeasuring.value = false;
};
const handleSave = () => {
emit('save', Array.from(selectedNames.value));
emit('update:visible', false);
};
const handleCancel = () => emit('update:visible', false);
</script>
<style scoped>
.msp-toolbar {
display: flex;
align-items: center;
padding: 8px 16px;
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
}
.msp-toolbar-label {
font-size: 13px;
color: #303133;
white-space: nowrap;
}
.msp-unit {
font-size: 13px;
color: #606266;
margin-left: 4px;
margin-right: 8px;
}
.msp-map-area {
position: relative;
height: calc(100% - 42px);
overflow: hidden;
}
.msp-map-container {
width: 100%;
height: 100%;
}
.msp-map-tools {
position: absolute;
left: 50%;
bottom: 16px;
transform: translateX(-50%);
display: flex;
gap: 6px;
background: rgba(255, 255, 255, 0.9);
padding: 6px 10px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 10;
}
.msp-tool-btn {
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
min-width: 30px;
height: 30px;
}
.msp-right-panel {
position: absolute;
right: 12px;
top: 12px;
bottom: 12px;
width: 200px;
background: rgba(255, 255, 255, 0.95);
border-radius: 6px;
padding: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
}
.msp-panel-title {
font-size: 13px;
font-weight: 600;
color: #005293;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #e8ecf1;
}
.msp-station-list {
flex: 1;
overflow-y: auto;
margin: 0 -10px;
padding: 0 10px;
}
.msp-station-item {
display: flex;
align-items: center;
padding: 8px 10px;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
margin-bottom: 4px;
}
.msp-station-item:hover {
background: #f0f5ff;
border-color: #d6e4ff;
}
.msp-station-selected {
background: #f0f5ff;
border-color: #005293;
}
.msp-station-name {
flex: 1;
font-size: 13px;
color: #303133;
}
.msp-station-type {
font-size: 11px;
color: #909399;
margin-right: 8px;
white-space: nowrap;
}
.msp-station-check {
width: 16px;
display: flex;
align-items: center;
justify-content: center;
color: #005293;
opacity: 0;
transition: opacity 0.15s;
}
.msp-check-visible {
opacity: 1;
}
</style>

View File

@ -0,0 +1,217 @@
<template>
<a-modal
:visible="visible"
:title="title"
width="80%"
style="min-width: 1200px"
:z-index="1050"
@cancel="handleCancel"
:footer="false"
destroy-on-close
>
<!-- 顶部站点tabs -->
<div class="sdm-top-tabs">
<div
v-for="(station, idx) in stations"
:key="station"
class="sdm-tab-item"
:class="{ 'sdm-tab-active': activeTabIndex === idx }"
@click="activeTabIndex = idx"
>
{{ station }}
</div>
</div>
<!-- 查询栏 -->
<div class="sdm-search-bar">
<div class="sdm-search-left">
<a-radio-group v-model:value="currentElement" button-style="solid">
<a-radio-button
v-for="el in elements"
:key="el.value"
:value="el.value"
>
{{ el.label }}
</a-radio-button>
</a-radio-group>
<a-range-picker
v-model:value="searchDateRange"
style="width: 260px; margin-left: 12px"
/>
<a-button type="primary" style="margin-left: 12px" @click="handleQuery">
查询
</a-button>
</div>
<div class="sdm-search-right">
<a-button type="primary" style="margin-left: 12px" @click="handleQuery">
自动识别
</a-button>
<a-button danger style="margin-left: 8px" @click="handleBatchDelete">
批量删除
</a-button>
</div>
</div>
<!-- 表格区域 -->
<div class="sdm-table-area">
<BasicTable
:columns="tableColumns"
:table-data="tableData"
:loading="loading"
:enable-row-selection="true"
row-key="id"
:pagination="{ pageSize: 10 }"
>
<template #operation="{ record }">
<a-button danger size="small" @click="handleDelete(record)">
删除
</a-button>
</template>
</BasicTable>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import BasicTable from '@/components/BasicTable/index.vue';
const props = defineProps<{
visible: boolean;
title: string;
stations: string[]; //
defaultDateRange: any; //
stationType: 'weather' | 'upstream';
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'delete', records: any[]): void;
}>();
// tab
const activeTabIndex = ref(0);
//
const elements = ref([
{ label: '降雨', value: 'rain' },
{ label: '气温', value: 'temp' },
{ label: '气压', value: 'press' },
{ label: '湿度', value: 'humidity' },
{ label: '风速', value: 'windSpeed' },
{ label: '风向', value: 'windDir' }
]);
const currentElement = ref('rain');
//
const searchDateRange = ref<any>(props.defaultDateRange);
//
const tableColumns = computed(() => [
{
title: '监测时间',
dataIndex: 'monitorTime',
width: 180
},
{
title: '数值',
dataIndex: 'value',
width: 120
},
{
title: '单位',
dataIndex: 'unit',
width: 80
},
{
title: '操作',
dataIndex: 'operation',
width: 80,
fixed: 'right'
}
]);
//
const mockData = [
{ id: 1, monitorTime: '2024-01-01 00:00', value: '12.5', unit: 'mm' },
{ id: 2, monitorTime: '2024-01-01 01:00', value: '0.0', unit: 'mm' },
{ id: 3, monitorTime: '2024-01-01 02:00', value: '0.0', unit: 'mm' },
{ id: 4, monitorTime: '2024-01-01 03:00', value: '5.2', unit: 'mm' },
{ id: 5, monitorTime: '2024-01-01 04:00', value: '8.7', unit: 'mm' },
{ id: 6, monitorTime: '2024-01-01 05:00', value: '0.0', unit: 'mm' },
{ id: 7, monitorTime: '2024-01-01 06:00', value: '0.0', unit: 'mm' },
{ id: 8, monitorTime: '2024-01-01 07:00', value: '0.0', unit: 'mm' }
];
const loading = ref(false);
const tableData = ref(mockData);
//
const handleQuery = () => {
// TODO:
loading.value = true;
setTimeout(() => {
loading.value = false;
}, 500);
};
const handleDelete = (record: any) => {
tableData.value = tableData.value.filter(r => r.id !== record.id);
emit('delete', [record]);
};
const handleBatchDelete = () => {
// TODO: selectionBasicTableselection
//
};
const handleCancel = () => {
emit('update:visible', false);
};
</script>
<style scoped lang="scss">
.sdm-top-tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #e8e8e8;
}
.sdm-tab-item {
padding: 6px 16px;
border: 1px solid #d9d9d9;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: #606266;
transition: all 0.2s;
}
.sdm-tab-item:hover {
color: #1890ff;
border-color: #1890ff;
}
.sdm-tab-active {
background: #1890ff;
color: #fff;
border-color: #1890ff;
}
.sdm-search-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
padding: 12px 16px;
background: #fafafa;
border-radius: 4px;
}
.sdm-search-left,
.sdm-search-right {
display: flex;
align-items: center;
}
.sdm-table-area {
min-height: 360px;
}
</style>

View File

@ -0,0 +1,308 @@
import OlMap from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import WMTS from 'ol/source/WMTS';
import WMTSTileGrid from 'ol/tilegrid/WMTS';
import { defaults as defaultInteractions, Draw } from 'ol/interaction';
import { getTopLeft, getWidth } from 'ol/extent';
import { get as getProjection, fromLonLat } from 'ol/proj';
import { getLength } from 'ol/sphere';
import Feature from 'ol/Feature';
import LineString from 'ol/geom/LineString';
import Point from 'ol/geom/Point';
import Polygon from 'ol/geom/Polygon';
import Style from 'ol/style/Style';
import Stroke from 'ol/style/Stroke';
import CircleStyle from 'ol/style/Circle';
import Fill from 'ol/style/Fill';
import Text from 'ol/style/Text';
// WMTS 底图配置
const VITE_APP_MAP_URL = import.meta.env.VITE_APP_MAP_URL || '';
const BASE_LAYER_CONFIG = {
id: 'customBaseLayer',
key: 'customBaseLayer',
type: 'wmts',
name: 'qgc_sx_gjjdx_arcgistiles_l13',
url:
VITE_APP_MAP_URL +
'/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=qgc_qsj_arcgistiles_l13&STYLE=&TILEMATRIX=EPSG:3857_qgc_qsj_arcgistiles_l13:{z}&TILEMATRIXSET=EPSG:3857_qgc_qsj_arcgistiles_l13&FORMAT=image/png&TILECOL={x}&TILEROW={y}',
matrixIds_index: [
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'10',
'11',
'12'
],
tileMatrixSetID: 'EPSG:3857_qgc_qsj_arcgistiles_l13'
};
export class StationMap {
map: OlMap | null = null;
view: View | null = null;
private measureSource: VectorSource | null = null;
private measureLayer: VectorLayer | null = null;
private drawInteraction: Draw | null = null;
private isMeasuring = false;
private anchorLayer: VectorLayer | null = null;
private anchorSource: VectorSource | null = null;
private rangeSource: VectorSource | null = null;
private rangeLayer: VectorLayer | null = null;
private centerLon = 114.171;
private centerLat = 38;
init(
container: HTMLElement,
centerLon: number,
centerLat: number,
radiusKm: number
) {
this.centerLon = centerLon;
this.centerLat = centerLat;
const zoom = this.calcZoomByRadius(radiusKm);
const projection = getProjection('EPSG:3857');
if (!projection) return;
const projectionExtent = projection.getExtent();
const size = getWidth(projectionExtent) / 256;
const resolutions: number[] = [];
const matrixIds: string[] = [];
const maxZoom = 13;
for (let z = 0; z <= maxZoom; ++z) {
resolutions[z] = size / Math.pow(2, z);
matrixIds[z] = `${BASE_LAYER_CONFIG.tileMatrixSetID}:${z}`;
}
const wmtsLayer = new TileLayer({
source: new WMTS({
url: BASE_LAYER_CONFIG.url.split('?')[0],
layer: 'qgc_qsj_arcgistiles_l13',
matrixSet: BASE_LAYER_CONFIG.tileMatrixSetID,
format: 'image/png',
projection: projection,
tileGrid: new WMTSTileGrid({
origin: getTopLeft(projectionExtent),
resolutions: resolutions,
matrixIds: matrixIds
}),
style: 'default',
wrapX: true,
crossOrigin: 'anonymous'
})
});
// 测量图层
this.measureSource = new VectorSource();
this.measureLayer = new VectorLayer({
source: this.measureSource,
style: new Style({
fill: new Fill({ color: 'rgba(255, 77, 79, 0.15)' }),
stroke: new Stroke({ color: '#ff4d4f', width: 2 })
})
});
// 锚点图层(预留)
this.anchorSource = new VectorSource();
this.anchorLayer = new VectorLayer({
source: this.anchorSource,
style: new Style({
image: new CircleStyle({
radius: 6,
fill: new Fill({ color: '#1890ff' }),
stroke: new Stroke({ color: '#fff', width: 2 })
})
})
});
// 范围指示圆
this.rangeSource = new VectorSource();
this.rangeLayer = new VectorLayer({
source: this.rangeSource,
style: new Style({
fill: new Fill({ color: 'rgba(255, 77, 79, 0.2)' }),
stroke: new Stroke({ color: '#ff4d4f', width: 2, lineDash: [6, 4] })
})
});
this.view = new View({
center: fromLonLat([centerLon, centerLat]),
zoom: zoom,
minZoom: 4,
maxZoom: 16,
projection: 'EPSG:3857'
});
this.map = new OlMap({
target: container,
layers: [wmtsLayer, this.measureLayer, this.anchorLayer, this.rangeLayer],
view: this.view,
controls: [],
interactions: defaultInteractions()
});
// 绘制初始范围圆
this.drawRangeCircle(radiusKm);
}
private calcZoomByRadius(radiusKm: number): number {
return Math.max(4, Math.min(14, Math.round(14 - Math.log2(radiusKm))));
}
destroy() {
if (this.map) {
this.map.setTarget(undefined);
this.map = null;
}
this.view = null;
}
/**
*
*/
toggleMeasure() {
if (!this.map || !this.measureSource) return;
this.isMeasuring ? this.stopMeasure() : this.startMeasure();
}
get isMeasureActive() {
return this.isMeasuring;
}
private startMeasure() {
if (!this.map || !this.measureSource) return;
this.isMeasuring = true;
this.drawInteraction = new Draw({
source: this.measureSource,
type: 'LineString',
style: new Style({
stroke: new Stroke({ color: '#ff4d4f', width: 2, lineDash: [4, 4] }),
image: new CircleStyle({
radius: 4,
fill: new Fill({ color: '#ff4d4f' })
})
})
});
this.drawInteraction.on('drawstart', () => {
this.measureSource?.clear();
});
this.drawInteraction.on('drawend', evt => {
const line = evt.feature.getGeometry() as LineString;
if (!line) return;
const length = getLength(line);
const label =
length > 1000
? `${(length / 1000).toFixed(2)} km`
: `${length.toFixed(1)} m`;
const coords = line.getLastCoordinate();
const labelFeature = new Feature(new Point(coords));
labelFeature.setStyle(
new Style({
text: new Text({
text: label,
font: '12px sans-serif',
fill: new Fill({ color: '#ff4d4f' }),
stroke: new Stroke({ color: '#fff', width: 3 }),
offsetY: -10
})
})
);
this.measureSource?.addFeature(labelFeature);
});
this.map.addInteraction(this.drawInteraction);
}
private stopMeasure() {
if (!this.map || !this.drawInteraction) return;
this.map.removeInteraction(this.drawInteraction);
this.drawInteraction = null;
this.isMeasuring = false;
}
clearMeasure() {
this.measureSource?.clear();
if (this.isMeasuring) this.stopMeasure();
}
zoomIn() {
if (!this.view) return;
const z = this.view.getZoom() || 6;
this.view.animate({ zoom: z + 1, duration: 250 });
}
zoomOut() {
if (!this.view) return;
const z = this.view.getZoom() || 6;
this.view.animate({ zoom: z - 1, duration: 250 });
}
flyTo(lon: number, lat: number, radiusKm: number) {
if (!this.view) return;
this.view.animate({
center: fromLonLat([lon, lat]),
zoom: this.calcZoomByRadius(radiusKm),
duration: 500
});
}
/**
*
*/
updateRange(radiusKm: number) {
if (!this.view) return;
this.view.animate({
zoom: this.calcZoomByRadius(radiusKm),
duration: 300
});
this.drawRangeCircle(radiusKm);
}
private drawRangeCircle(radiusKm: number) {
if (!this.rangeSource) return;
this.rangeSource.clear();
const radiusM = radiusKm * 1000;
const numPoints = 64;
const coords: number[][] = [];
const R = 6371000; // 地球平均半径(米)
for (let i = 0; i <= numPoints; i++) {
const bearing = (i * 2 * Math.PI) / numPoints; // 弧度,顺时针从北
const lat1 = (this.centerLat * Math.PI) / 180;
const lon1 = (this.centerLon * Math.PI) / 180;
// 球面 destination 计算haversine 公式逆运算)
const lat2 = Math.asin(
Math.sin(lat1) * Math.cos(radiusM / R) +
Math.cos(lat1) * Math.sin(radiusM / R) * Math.cos(bearing)
);
const lon2 =
lon1 +
Math.atan2(
Math.sin(bearing) * Math.sin(radiusM / R) * Math.cos(lat1),
Math.cos(radiusM / R) - Math.sin(lat1) * Math.sin(lat2)
);
coords.push(fromLonLat([(lon2 * 180) / Math.PI, (lat2 * 180) / Math.PI]));
}
const polygon = new Polygon([coords]);
const feature = new Feature(polygon);
this.rangeSource.addFeature(feature);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
<template>
<div class="moduleContent" style="color: red; z-index: 1000">
目标检测模型管理
</div>
</template>
<script setup lang="ts"></script>

View File

@ -1,15 +0,0 @@
<template>
<div class="shiXuMoXingGuanLi-page">时序模型管理</div>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss">
.shiXuMoXingGuanLi-page {
position: relative;
pointer-events: all;
z-index: 2000;
background-color: #fff;
width: 100%;
height: 100%;
}
</style>

View File

@ -2,9 +2,11 @@
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';
import QiXiDiFenBuTongJi from '@/modules/qixidifenbutongji/index.vue';
import QiXiDJianCeJieGuoTongJi from '@/modules/qixidijiancejieguotongji/index.vue';
// import QixidijchuXx from '@/modules/qixidijchuXx/index.vue';
// import QiXiDiShuiWenBianHua from '@/modules/qixidishuiwenbianhua/index.vue';
// import QiXiDiLiuLiangBianHua from '@/modules/qixidiliuliangbianhua/index.vue';
</script>
<template>
@ -14,10 +16,9 @@ import QiXiDiLiuLiangBianHua from '@/modules/qixidiliuliangbianhua/index.vue';
</div>
<div class="rightContent">
<RightDrawer>
<QiXiDiFenBuTongJi />
<QiXiDiBaoHuGongZuoKaiZhan />
<QiXiDiShuiWenBianHua />
<QiXiDiLiuLiangBianHua />
<QixidijchuXx />
<QiXiDJianCeJieGuoTongJi />
</RightDrawer>
</div>
</div>

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import JidiSelectorMod from "@/modules/jidiSelectorMod.vue";
import RightDrawer from "@/components/RightDrawer/index.vue";
import LSSTJCQKTT from "@/modules/lushengshengtaijiance/index.vue"
import Ysdwjc from "@/modules/Ysdwjc/index.vue"
import JidiSelectorMod from '@/modules/jidiSelectorMod.vue';
import RightDrawer from '@/components/RightDrawer/index.vue';
import LSJCSBTJ from '@/modules/lushengshebeitongji/index.vue';
import Ysdwjc from '@/modules/Ysdwjc/index.vue';
</script>
<template>
@ -12,8 +12,8 @@ import Ysdwjc from "@/modules/Ysdwjc/index.vue"
</div>
<div class="rightContent">
<RightDrawer>
<LSSTJCQKTT />
<Ysdwjc />
<LSJCSBTJ />
<Ysdwjc />
</RightDrawer>
</div>
</div>

View File

@ -1,11 +1,13 @@
<script setup lang="ts">
import JidiSelectorMod from '@/modules/jidiSelectorMod.vue';
import RightDrawer from '@/components/RightDrawer/index.vue';
import ShengtaidabiaoMod from '@/modules/shengtaidabiaoMod/index.vue'; // -
import ShengtaidabiaoTwoMod from '@/modules/shengtaidabiaoTwoMod/index.vue';
import StllyanchengChangeMod from '@/modules/stllyanchengChangeMod/index.vue' // -沿
// import ShengtaidabiaoMod from '@/modules/shengtaidabiaoMod/index.vue'; // -
// import ShengtaidabiaoTwoMod from '@/modules/shengtaidabiaoTwoMod/index.vue';
import ShengTaiHuangJingBuMod from '@/modules/shengTaiHuangJingBuMod/index.vue'; // -
import ShuiLiBuMod from '@/modules/shuiLiBuMod/index.vue'; // -
import StllyanchengChangeMod from '@/modules/stllyanchengChangeMod/index.vue'; // -沿
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { ref, onMounted, watch } from 'vue';
import { ref, watch } from 'vue';
const JidiSelectEventStore = useJidiSelectEventStore();
const baseid = ref('');
watch(
@ -26,9 +28,9 @@ watch(
</div>
<div class="rightContent">
<RightDrawer>
<ShengtaidabiaoMod />
<ShengtaidabiaoTwoMod v-if="baseid == 'all'" />
<StllyanchengChangeMod v-if="baseid != 'all'" />
<ShengTaiHuangJingBuMod />
<ShuiLiBuMod />
<StllyanchengChangeMod v-if="baseid != 'all'" />
</RightDrawer>
</div>
</div>

View File

@ -1,10 +1,24 @@
<script setup lang="ts">
import { ref, watch } from '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';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import ShuiWenJianCeKaiZhanQingKuang from '@/modules/shuiWenJianCeKaiZhanQingKuang/index.vue'; // -
const JidiSelectEventStore = useJidiSelectEventStore();
const baseid = ref('');
watch(
() => JidiSelectEventStore.selectedItem,
newVal => {
if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode;
}
},
{ deep: true, immediate: true }
);
</script>
<template>
@ -15,9 +29,10 @@ import ShuiwenJCGZKZQK from '@/modules/shuiwenjiancegongzuokaizhangqingkuang/ind
<div class="rightContent">
<RightDrawer>
<ShuiwenJCGZKZQK />
<YanchengshuiwenChangeMod />
<ChuixiangshuiwenChangeMod />
<ChurukushuiwenMod />
<YanchengshuiwenChangeMod v-if="baseid != 'all'" />
<ChuixiangshuiwenChangeMod v-if="baseid != 'all'" />
<ChurukushuiwenMod v-if="baseid != 'all'" />
<ShuiWenJianCeKaiZhanQingKuang v-if="baseid == 'all'" />
</RightDrawer>
</div>
</div>

View File

@ -5,15 +5,16 @@ import RightDrawer from '@/components/RightDrawer/index.vue';
import ShuiZhiJianCeGongZuoQingKuang from '@/modules/shuizhijiancegongzuoQK/index.vue';
import EnvironmentalQuality from '@/modules/EnvironmentalQuality/index.vue'; //
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import ShuiZhiDaBiaoQingKuang from '@/modules/shuiZhiDaBiaoQingKuang/index.vue'; // -
import SZYCBH from '@/modules/waterQuality/index.vue';
const JidiSelectEventStore = useJidiSelectEventStore();
const wbsCode = ref('');
const baseid = ref('');
watch(
() => JidiSelectEventStore.selectedItem,
newVal => {
console.log(newVal);
//
wbsCode.value = newVal.wbsCode;
baseid.value = newVal.wbsCode;
},
{ deep: true, immediate: true }
);
@ -27,8 +28,9 @@ watch(
<div class="rightContent">
<RightDrawer>
<ShuiZhiJianCeGongZuoQingKuang />
<EnvironmentalQuality />
<SZYCBH v-if="wbsCode != 'all'"></SZYCBH>
<ShuiZhiDaBiaoQingKuang v-if="baseid == 'all'" />
<EnvironmentalQuality v-if="baseid != 'all'" />
<SZYCBH v-if="baseid != 'all'"></SZYCBH>
</RightDrawer>
</div>
</div>

View File

@ -103,7 +103,7 @@ import { ref, reactive, computed, watch,onMounted } from 'vue';
import { message, Modal } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import dayjs from 'dayjs';
import CronPicker from './CronPicker.vue';
import CronPicker from '@/components/CronPicker/index.vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { getDictItemsByCode } from '@/api/dict';
import { useDraggable } from '@/utils/drag';

View File

@ -5,7 +5,8 @@ import RightDrawer from '@/components/RightDrawer/index.vue';
import ZengZhiJiHuaWanChengQingKuang from '@/modules/zengZhiJiHuaWanChengQingKuang/index.vue';
import ZengZhiZhanJieShaoMod from '@/modules/zengZhiZhanJieShaoMod/index.vue';
import ZZZJSYXQKTT from '@/modules/zengzhizhanjiansheyunxing/index.vue';
// import ZZZJSYXQKTT from '@/modules/zengzhizhanjiansheyunxing/index.vue';
import ZZZFBTJ from '@/modules/zengzhizhanfbutongji/index.vue';
import ZZZYXSJTJ from '@/modules/zengzhizhanyunxingsjtj/index.vue';
import FLZLLB from '@/modules/GYZLLB/index.vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
@ -32,7 +33,7 @@ watch(
</div>
<div class="rightContent">
<RightDrawer>
<ZZZJSYXQKTT />
<ZZZFBTJ />
<ZengZhiZhanJieShaoMod v-if="wbsCode != 'all'" />
<ZengZhiJiHuaWanChengQingKuang v-if="wbsCode != 'all'" />
<FLZLLB title="放流总量" />

View File

@ -1,7 +1,8 @@
import dayjs from 'dayjs';
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core';
import UniverPresetSheetsCoreZhCN from '@univerjs/preset-sheets-core/locales/zh-CN';
import { createUniver, LocaleType, mergeLocales } from '@univerjs/presets';
// Univer 单例缓存,避免重复初始化导致 Identifier already exists 警告
let cachedUniverInstance: { univer: any; univerAPI: any } | null = null;
interface dmFilter {
baseId?: string;
rstcd?: string;
@ -222,8 +223,22 @@ const initializeExcelWorksheet = (univerAPI: any) => {
}
};
// 创建Excel实例
export const createExcelInstance = container => {
// 创建Excel实例单例模式仅首次调用时创建后续复用缓存实例
export const createExcelInstance = async container => {
if (cachedUniverInstance) {
return cachedUniverInstance;
}
// 动态导入 Univer 模块,避免预加载时触发模块初始化导致 Identifier 警告
const [
{ UniverSheetsCorePreset },
UniverPresetSheetsCoreZhCN,
{ createUniver, LocaleType, mergeLocales }
] = await Promise.all([
import('@univerjs/preset-sheets-core'),
import('@univerjs/preset-sheets-core/locales/zh-CN'),
import('@univerjs/presets')
]);
const { univer, univerAPI } = createUniver({
locale: LocaleType.ZH_CN,
locales: {
@ -300,8 +315,14 @@ export const createExcelInstance = container => {
// 初始化Excel工作表
initializeExcelWorksheet(univerAPI);
cachedUniverInstance = { univer, univerAPI };
return { univer, univerAPI };
};
// 重置单例缓存(页面卸载时调用)
export const resetUniverInstance = () => {
cachedUniverInstance = null;
};
// 检查单元格编辑事件如果row是奇数根据column判断是否需要添加平均数公式
export const handleCellEdit = (params: any, fWorksheet: any, year: string) => {
const {

View File

@ -36,7 +36,11 @@ import {
import BasicSearch from '@/components/BasicSearch/index.vue';
import '@univerjs/preset-sheets-core/lib/index.css';
import { exportExcel } from './components/export';
import { createExcelInstance, renderDataToExcel } from './components/utils';
import {
createExcelInstance,
renderDataToExcel,
resetUniverInstance
} from './components/utils';
import dayjs from 'dayjs';
const basicSearchRef = ref();
@ -351,18 +355,18 @@ const initData = async (values: any) => {
loading.value = false;
});
};
onMounted(() => {
// Excel
const { univer, univerAPI } = createExcelInstance(containerRef.value);
onMounted(async () => {
// Excel Identifier
const { univer, univerAPI } = await createExcelInstance(containerRef.value);
// univer ref
univerInstanceRef.value = { univer, univerAPI };
initStcd(initSearchData);
initData(initSearchData);
});
onUnmounted(() => {
// Excel
const { univer, univerAPI } = univerInstanceRef.value;
univerAPI.dispose();
// Identifier
// ref Univer
resetUniverInstance();
});
</script>

View File

@ -97,7 +97,9 @@
</a-tabs>
<a-button type="primary">开始预测</a-button>
</div>
<a-button type="primary">历史预测</a-button>
<a-button class="chart-btn-primary" type="primary"
>历史预测</a-button
>
</div>
<!-- echarts容器 -->
<div
@ -406,7 +408,7 @@ onBeforeUnmount(() => {
.chart-container {
position: absolute;
left: 20%;
top: 10px;
top: 6px;
width: 80%;
display: flex;
justify-content: space-between;
@ -414,4 +416,8 @@ onBeforeUnmount(() => {
margin-bottom: 16px;
padding-right: 20px;
}
.chart-btn-primary {
position: absolute;
right: 10px;
}
</style>

View File

@ -1,5 +0,0 @@
<template>
<div class="moduleContent" style="color: red; z-index: 1000">智能告警</div>
</template>
<script setup lang="ts"></script>