From e895212fa4215c50ce79ce4b448e064caf394776 Mon Sep 17 00:00:00 2001 From: riku <risaku@163.com> Date: 星期四, 03 七月 2025 17:35:46 +0800 Subject: [PATCH] 2025.7.3 动态溯源(待完成) --- src/utils/chart/chart-option.js | 4 src/views/sourcetrace/SourceTrace.vue | 171 +++++++++++++++---- src/components/map/MapLocate.vue | 0 src/views/sourcetrace/component/PollutedExceptionItem.vue | 32 ++- src/components.d.ts | 2 src/views/HomePage.vue | 2 src/views/sourcetrace/component/ClueRecordItem.vue | 182 +++++++++++++++++--- src/views/sourcetrace/component/SourceTraceFilter.vue | 99 +++++++++++ README.md | 4 src/components/chart/RealTimeLineChart.vue | 8 10 files changed, 421 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index bec6a41..a7d00bd 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ 鈹� 鈹� 鈹溾攢 map 鈹� 鈹� 鈹� 鈹溾攢 BaseMap.vue 鈹� 鈹� 鈹� 鈹溾攢 ConfigManage.vue -鈹� 鈹� 鈹� 鈹溾攢 MapLocation.vue +鈹� 鈹� 鈹� 鈹溾攢 MapLocate.vue 鈹� 鈹� 鈹� 鈹溾攢 MapScene.vue 鈹� 鈹� 鈹� 鈹斺攢 MapToolbox.vue 鈹� 鈹� 鈹溾攢 MessageBox.vue @@ -263,4 +263,4 @@ 鈹溾攢 vite.config.js 鈹斺攢 vitest.config.js -``` \ No newline at end of file +``` diff --git a/src/components.d.ts b/src/components.d.ts index 4fb05dd..9231243 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -65,7 +65,7 @@ GaugeChart: typeof import('./components/chart/GaugeChart.vue')['default'] GridSearch: typeof import('./components/grid/GridSearch.vue')['default'] HistoricalTrajectory: typeof import('./components/animation/HistoricalTrajectory.vue')['default'] - MapLocation: typeof import('./components/map/MapLocation.vue')['default'] + MapLocate: typeof import('./components/map/MapLocate.vue')['default'] MapScene: typeof import('./components/map/MapScene.vue')['default'] MapToolbox: typeof import('./components/map/MapToolbox.vue')['default'] MessageBox: typeof import('./components/MessageBox.vue')['default'] diff --git a/src/components/chart/RealTimeLineChart.vue b/src/components/chart/RealTimeLineChart.vue index a66e2ac..cc72183 100644 --- a/src/components/chart/RealTimeLineChart.vue +++ b/src/components/chart/RealTimeLineChart.vue @@ -22,9 +22,15 @@ // }; // } }, + // 鎶樼嚎鍥惧睍绀洪珮搴� chartHeight: { type: String, default: '140px' + }, + // 鎶樼嚎鍥綴杞村埢搴﹂棿璺� + yMinInterval: { + type: Number, + default: 1 } }, data() { @@ -45,7 +51,7 @@ refreshChart() { const { xAxis, series } = this.modelValue; if (!this.option) { - this.option = smallLineOption(xAxis, series); + this.option = smallLineOption(xAxis, series, this.yMinInterval); } else { this.option.xAxis[0].data = xAxis; this.option.series = series; diff --git a/src/components/map/MapLocation.vue b/src/components/map/MapLocate.vue similarity index 100% rename from src/components/map/MapLocation.vue rename to src/components/map/MapLocate.vue diff --git a/src/utils/chart/chart-option.js b/src/utils/chart/chart-option.js index 346c602..99337d0 100644 --- a/src/utils/chart/chart-option.js +++ b/src/utils/chart/chart-option.js @@ -121,7 +121,7 @@ } // 鎶樼嚎鍥� -function smallLineOption(_xAxis, _series) { +function smallLineOption(_xAxis, _series, yMinInterval) { var fontSize = fGetChartFontSize(); return { animationEasing: 'elasticOut', @@ -198,7 +198,7 @@ splitLine: { show: false }, - minInterval: 1, + minInterval: yMinInterval ? yMinInterval : 1, intervel: 1, min: function (value) { return Math.floor(value.min); diff --git a/src/views/HomePage.vue b/src/views/HomePage.vue index b31d689..28ddf5d 100644 --- a/src/views/HomePage.vue +++ b/src/views/HomePage.vue @@ -7,7 +7,7 @@ <!-- <SatelliteTelemetry></SatelliteTelemetry> --> <!-- <MissionManage></MissionManage> --> <ConfigManage></ConfigManage> - <!-- <MapLocation></MapLocation> --> + <!-- <MapLocate></MapLocate> --> <SceneSearch></SceneSearch> <MapScene></MapScene> <GridSearch></GridSearch> diff --git a/src/views/sourcetrace/SourceTrace.vue b/src/views/sourcetrace/SourceTrace.vue index d4c97a6..0145b9a 100644 --- a/src/views/sourcetrace/SourceTrace.vue +++ b/src/views/sourcetrace/SourceTrace.vue @@ -18,17 +18,30 @@ <el-col v-show="show" span="10"> <BaseCard> <template #content> - <el-checkbox-group v-model="selectedMsgTypes" size="default" :min="1"> - <el-space> - <el-checkbox value="1">寮傚父鍒囩墖</el-checkbox> - <el-checkbox value="2">姹℃煋绾跨储</el-checkbox> - </el-space> - </el-checkbox-group> + <el-row + justify="space-between" + align="middle" + style="border-bottom: 1px solid white" + > + <!-- 鏁版嵁鍒囩墖绛涢�夋潯浠� --> + <SourceTraceFilter + v-model:data-slice="selectedMsgTypes" + v-model:factor-type="selectedFactorTypes" + :factor-options="factorOptions" + v-model:scene-type="selectedSceneTypes" + :scene-options="sceneOptions" + ></SourceTraceFilter> + <!-- <el-divider direction="vertical"></el-divider> --> + <!-- 鏁版嵁鍒囩墖缁熻 --> + <div style="border-left: 1px solid white" class="p-l-8"> + <el-space direction="vertical"> + <el-text type="primary">婧簮锛歿{ countMsg1.type1 }}鏉�</el-text> + <el-text type="primary">绾跨储锛歿{ countMsg1.type2 }}鏉�</el-text> + <el-text type="primary">鎻愰啋锛歿{ countMsg1.type3 }}鏉�</el-text> + </el-space> + </div> + </el-row> <el-scrollbar ref="scrollbarRef" class="scrollbar"> - <!-- <div - ref="scrollContentRef" - style="display: flex; width: fit-content" - > --> <TransitionGroup name="list"> <div v-for="item in filterStreams" @@ -40,7 +53,6 @@ ></ClueRecordItem> </div> </TransitionGroup> - <!-- </div> --> </el-scrollbar> </template> </BaseCard> @@ -72,6 +84,9 @@ import PollutedExceptionItem from './component/PollutedExceptionItem.vue'; import ClueRecordItem from './component/ClueRecordItem.vue'; import PollutedClueItem from '@/views/sourcetrace/component/PollutedClueItem.vue'; +import SourceTraceFilter from '@/views/sourcetrace/component/SourceTraceFilter.vue'; + +const NO_SCENE = 'no_scene'; const props = defineProps({ factorType: String @@ -89,7 +104,11 @@ const selectedException = ref(); const selectedClue = ref(); -const selectedMsgTypes = ref(['1', '2']); +const selectedMsgTypes = ref(['1', '2', '3']); +const selectedFactorTypes = ref([]); +const factorOptions = ref([]); +const selectedSceneTypes = ref([]); +const sceneOptions = ref([]); function scrollToBottom() { const h1 = scrollContentRef.value.clientHeight + 100; @@ -107,8 +126,57 @@ const streams = reactive([]); const filterStreams = computed(() => { return streams.filter((v) => { - return selectedMsgTypes.value.indexOf(v._type) != -1; + // 鍒ゆ柇娑堟伅绫诲瀷鏄惁閫変腑 + const b1 = selectedMsgTypes.value.indexOf(v._type) != -1; + let b2, b3; + switch (v._type) { + case '1': + case '3': + // 鍒ゆ柇鐩戞祴鍥犲瓙绫诲瀷鏄惁閫変腑 + b2 = selectedFactorTypes.value.indexOf(v.pollutedData.factorId) != -1; + // 鍒ゆ柇鍦烘櫙绫诲瀷鏄惁閫変腑 + if (v.pollutedSource.sceneList.length == 0) { + b3 = selectedSceneTypes.value.indexOf(NO_SCENE) != -1; + } else { + b3 = + v.pollutedSource.sceneList.findIndex( + (v) => selectedSceneTypes.value.indexOf(v.typeId) != -1 + ) != -1; + } + break; + case '2': + b2 = true; + b3 = + v.sortedSceneList.findIndex( + (v) => selectedSceneTypes.value.indexOf(v.first.typeId) != -1 + ) != -1; + break; + } + + return b1 && b2 && b3; }); +}); +// 缁熻鍚勭被鍨嬫秷鎭殑鏁伴噺 +const countMsg1 = computed(() => { + const count = { + type1: 0, + type2: 0, + type3: 0 + }; + streams.forEach((v) => { + switch (v._type) { + case '1': + count.type1++; + break; + case '2': + count.type2++; + break; + case '3': + count.type3++; + break; + } + }); + return count; }); const inputVal = ref(''); @@ -116,27 +184,16 @@ websocket.send(inputVal.value); }; -let showFirstClueTask; function dealMsg(data) { const { type, content } = websocketMsgParser.parseMsg(data); + const obj = reactive(JSON.parse(content)); + obj._type = type; // 姹℃煋绾跨储 PollutedClue if (type == '1') { - const obj = reactive(JSON.parse(content)); - obj._type = type; - // obj.showMore = true; obj.showMore = false; console.log('姹℃煋寮傚父鍒囩墖: ', obj); - // if (streams.length == 0) { - // streams.push(obj); - // } else { - // // streams.forEach((s) => { - // // showMarksAndPolygon(s); - // // }); - // // hideAll(); - // streams.unshift(obj); - // } addNewMsg(obj); show.value = true; @@ -144,20 +201,61 @@ // scrollToTop(); // drawPolygon(obj.pollutedArea); parseChartData(obj); - - // if (showFirstClueTask) { - // clearTimeout(showFirstClueTask); - // } - // showFirstClueTask = setTimeout(() => { - // showMarksAndPolygon(obj); - // }, 1000); } else if (type == '2') { - const obj = JSON.parse(content); - obj._type = type; + // const obj = JSON.parse(content); + // obj._type = type; console.log('姹℃煋绾跨储缁撴灉: ', obj); obj._timestr = timeFormatter(obj.time); - // streams.unshift(obj); addNewMsg(obj); + } else if (type == '3') { + console.log('姹℃煋鎻愰啋鍒囩墖: ', obj); + addNewMsg(obj); + parseChartData(obj); + } + + optionsFilte(obj); +} + +// 瀵规暟鎹繘琛岀瓫閫夛紝鍖呮嫭鐩戞祴鍥犲瓙鍜屽満鏅被鍨� +function optionsFilte(objData) { + switch (objData._type) { + case '1': + case '3': + // 绛涢�夌洃娴嬪洜瀛愮被鍨� + if ( + factorOptions.value.findIndex( + (v) => v.value == objData.pollutedData.factorId + ) == -1 + ) { + factorOptions.value.push({ + label: objData.pollutedData.factorName, + value: objData.pollutedData.factorId + }); + selectedFactorTypes.value.push(objData.pollutedData.factorId); + } + // 绛涢�夊満鏅被鍨� + if (objData.pollutedSource.sceneList.length == 0) { + // 鑻ユ病鏈夋壘鍒伴闄╂簮鏃讹紝灏嗚鍒嗙被璁惧畾涓簄ull + if (sceneOptions.value.findIndex((v) => v.value == NO_SCENE) == -1) { + sceneOptions.value.push({ + label: '鏃�', + value: NO_SCENE + }); + selectedSceneTypes.value.push(NO_SCENE); + } + } else { + objData.pollutedSource.sceneList.forEach((s) => { + if (sceneOptions.value.findIndex((v) => v.value == s.typeId) == -1) { + sceneOptions.value.push({ + label: s.type, + value: s.typeId + }); + selectedSceneTypes.value.push(s.typeId); + } + }); + } + // case '2': + // break; } } @@ -202,6 +300,7 @@ function handleOpen(item) { switch (item._type) { case '1': + case '3': if (selectedException.value) { selectedException.value._selected = false; } diff --git a/src/views/sourcetrace/component/ClueRecordItem.vue b/src/views/sourcetrace/component/ClueRecordItem.vue index 28f2918..a8aecbb 100644 --- a/src/views/sourcetrace/component/ClueRecordItem.vue +++ b/src/views/sourcetrace/component/ClueRecordItem.vue @@ -1,35 +1,64 @@ <template> <div :class="'wrapper' + (item._selected ? ' wrapper-select' : '')"> - <div v-if="item._type == '1'"> - <el-row justify="space-between"> - <el-space> - <el-tag v-if="noWarn" type="info" effect="dark" size="small" - >寮傚父</el-tag - > - <el-tag v-else type="warning" effect="dark" size="small">寮傚父</el-tag> - <el-text type="primary">{{ - item.pollutedData.startTime + ' - ' + item.pollutedData.endTime - }}</el-text> - </el-space> - <el-link type="primary" @click="emits('open', item)"> 璇︽儏 </el-link> - </el-row> - <el-col :span="24"> - <el-text type="primary">{{ - item.pollutedData.factorName + - formatException(item.pollutedData.exceptionType) + - '锛�' + - formatDistanceType(item.pollutedArea.distanceType) - }}</el-text> - <el-text :type="noWarn ? 'primary' : 'warning'"> - {{ - item.pollutedSource.sceneList.length == 0 - ? '鏈壘鍒板彲鐤戞薄鏌撴簮' - : '鎵惧埌' + item.pollutedSource.sceneList.length + '涓彲鐤戞薄鏌撴簮' - }} - </el-text> + <el-row v-if="item._type == '1'"> + <el-col :span="3"> + <el-tag :type="noWarn ? 'info' : 'warning'" effect="dark" size="small" + >婧簮</el-tag + > </el-col> - <!-- <el-col :span="2"> </el-col> --> - </div> + <el-col :span="21"> + <el-row justify="space-between"> + <el-space> + <el-text type="primary" size="default"> + <el-icon><Timer /></el-icon> + {{ + item.pollutedData.startTime + ' - ' + item.pollutedData.endTime + }} + </el-text> + </el-space> + <el-link type="primary" @click="emits('open', item)"> 璇︽儏 </el-link> + </el-row> + <div> + <el-tag + effect="plain" + type="info" + size="small" + hit + round + class="m-r-4" + > + <div v-html="formatFactorName(item.pollutedData.factorName)"></div> + </el-tag> + <el-text type="primary"> + {{ item.pollutedData.exception + '锛�' }} + </el-text> + <el-text type="primary">{{ + formatDistanceType(item.pollutedArea.distanceType) + }}</el-text> + <el-text :type="noWarn ? 'primary' : 'warning'"> + {{ + item.pollutedSource.sceneList.length == 0 + ? '鏈壘鍒伴闄╂簮' + : '鎵惧埌' + item.pollutedSource.sceneList.length + '涓闄╂簮' + }} + </el-text> + </div> + <div v-if="item.pollutedSource.sceneList.length > 0"> + <div v-for="s in item.pollutedSource.sceneList" :key="s.guid"> + <img style="width: 24px" :src="sceneIcon(s.typeId)" :alt="s.type" /> + <el-text + type="warning" + tag="ins" + truncated + class="text-link" + @click="handleSetCenter(item, s)" + > + {{ s.name }} + </el-text> + </div> + </div> + </el-col> + </el-row> <div v-else-if="item._type == '2'"> <el-row justify="space-between"> <el-tag type="danger" effect="dark" size="small">绾跨储</el-tag> @@ -37,10 +66,73 @@ </el-row> <el-text type="danger">{{ item.advice }}</el-text> </div> + <el-row v-else-if="item._type == '3'"> + <el-col :span="3"> + <el-tag type="primary" effect="dark" size="small">鎻愰啋</el-tag> + </el-col> + <el-col :span="21"> + <el-row justify="space-between"> + <el-space> + <el-text type="primary" size="default"> + <el-icon><Timer /></el-icon> + {{ + item.pollutedData.startTime + ' - ' + item.pollutedData.endTime + }} + </el-text> + </el-space> + <el-link type="primary" @click="emits('open', item)"> 璇︽儏 </el-link> + </el-row> + <div> + <el-tag + effect="plain" + type="info" + size="small" + hit + round + class="m-r-4" + > + <div v-html="formatFactorName(item.pollutedData.factorName)"></div> + </el-tag> + <el-text type="primary">{{ item.pollutedData.exception }}</el-text> + </div> + <div v-if="item.pollutedSource.sceneList.length > 0"> + <div v-for="s in item.pollutedSource.sceneList" :key="s.guid"> + <img style="width: 24px" :src="sceneIcon(s.typeId)" :alt="s.type" /> + <el-text + type="warning" + tag="ins" + truncated + class="text-link" + @click="handleSetCenter(item, s)" + > + {{ s.name }} + </el-text> + </div> + </div> + </el-col> + + <!-- <el-row justify="space-between"> + <el-space> + <el-tag type="primary" effect="dark" size="small">鎻愰啋</el-tag> + <el-text type="primary">{{ + item.pollutedData.startTime + ' - ' + item.pollutedData.endTime + }}</el-text> + </el-space> + <el-link type="primary" @click="emits('open', item)"> 璇︽儏 </el-link> + </el-row> + <el-col :span="24"> + <el-tag effect="plain" type="info" size="small" hit round class="m-r-4"> + <div v-html="formatFactorName(item.pollutedData.factorName)"></div> + </el-tag> + <el-text type="primary">{{ item.pollutedData.exception }}</el-text> + </el-col> --> + </el-row> </div> </template> <script setup> import { computed } from 'vue'; +import { sceneTypes, sceneIcon } from '@/constant/scene-types'; +import MapUtil from '@/utils/map/util'; const props = defineProps({ item: Object @@ -78,6 +170,34 @@ break; } } + +function formatFactorName(name) { + switch (name) { + case 'PM25': + return 'PM<sub>2.5</sub>'; + // return '<span>PM2.5</span>'; + case 'PM10': + return 'PM<sub>10</sub>'; + case 'NO2': + return 'NO<sub>2</sub>'; + case 'H2S': + return 'H<sub>2</sub>S'; + case 'SO2': + return 'SO<sub>2</sub>'; + case 'O3': + return 'O<sub>3</sub>'; + case 'VOC': + return 'VOC<sub>s</sub>'; + + default: + break; + } +} + +function handleSetCenter(item, scene) { + MapUtil.setCenter([scene.longitude, scene.latitude], true); + emits('open', item); +} </script> <style scoped> .wrapper { @@ -93,4 +213,8 @@ .no-warning { color: var(--el-text-color-disabled) !important; } +.text-link { + width: 90%; + cursor: pointer; +} </style> diff --git a/src/views/sourcetrace/component/PollutedExceptionItem.vue b/src/views/sourcetrace/component/PollutedExceptionItem.vue index e7defcc..0aeedff 100644 --- a/src/views/sourcetrace/component/PollutedExceptionItem.vue +++ b/src/views/sourcetrace/component/PollutedExceptionItem.vue @@ -10,34 +10,43 @@ <BaseCard v-if="item" v-show="item.showMore"> <template #content> <el-scrollbar class="clue-card"> - <el-row justify="space-between"> - <!-- <el-tag v-if="index == 0" type="danger">鏈�鏂�</el-tag> --> - <el-text type="primary">{{ - '鍒囩墖鏃堕棿锛�' + - item.pollutedData.startTime + - ' - ' + - item.pollutedData.endTime - }}</el-text> + <el-row justify="space-between" align="bottom"> + <el-text type="warning" size="large"> 鍏稿瀷鍒囩墖 </el-text> + <el-link type="primary" :underline="true" @click="showMarksAndPolygon(item)" > - {{ item.showMore ? '鏀惰捣寮傚父' : '瀹氫綅寮傚父' }} + {{ item.showMore ? '鏀惰捣' : '瀹氫綅' }} + <el-icon size="large"><CircleClose /></el-icon> </el-link> </el-row> <div> <el-text type="primary"> - 姹℃煋鍖哄煙锛歿{ item.pollutedArea.address }} + <el-icon><Timer /></el-icon> + {{ + '鍒囩墖鏃舵锛�' + + item.pollutedData.startTime + + ' - ' + + item.pollutedData.endTime + }} </el-text> </div> <div> + <el-text type="primary"> + <el-icon><MapLocation /></el-icon> + {{ '椋庨櫓鍖哄煙锛�' + item.pollutedArea.address }} + </el-text> + </div> + <!-- <div> <el-text type="primary"> 婧簮璺濈锛歿{ formatDistanceType(item.pollutedArea.distanceType) }} </el-text> - </div> + </div> --> <div> <el-text type="primary"> + <el-icon><BellFilled /></el-icon> 寮傚父绫诲瀷锛歿{ item.pollutedData.exception }} </el-text> </div> @@ -92,6 +101,7 @@ :key="index1" :model-value="item1" chart-height="80px" + :y-min-interval="20" ></RealTimeLineChart> <!-- </div> --> <div class="border-dashed"> diff --git a/src/views/sourcetrace/component/SourceTraceFilter.vue b/src/views/sourcetrace/component/SourceTraceFilter.vue new file mode 100644 index 0000000..6368300 --- /dev/null +++ b/src/views/sourcetrace/component/SourceTraceFilter.vue @@ -0,0 +1,99 @@ +<template> + <div> + <div> + <el-space> + <el-text type="primary">鏁版嵁鍒囩墖</el-text> + <el-checkbox-group + :model-value="dataSlice" + @update:model-value="(e) => emits('update:data-slice', e)" + size="default" + :min="1" + > + <el-space> + <el-checkbox value="1">婧簮</el-checkbox> + <el-checkbox value="2">绾跨储</el-checkbox> + <el-checkbox value="3">鎻愮ず</el-checkbox> + </el-space> + </el-checkbox-group> + </el-space> + </div> + <div> + <el-space> + <el-text type="primary">鐩戞祴鍥犲瓙</el-text> + <el-checkbox-group + :model-value="factorType" + @update:model-value="(e) => emits('update:factor-type', e)" + size="default" + :min="1" + > + <el-space> + <el-checkbox + v-for="item in factorOptions" + :value="item.value" + :key="item.label" + > + {{ item.label }} + </el-checkbox> + </el-space> + </el-checkbox-group> + </el-space> + </div> + <div> + <el-space> + <el-text type="primary">鍦烘櫙绫诲瀷</el-text> + <el-checkbox-group + :model-value="sceneType" + @update:model-value="(e) => emits('update:scene-type', e)" + size="default" + :min="1" + > + <el-space> + <el-checkbox + v-for="item in sceneOptions" + :value="item.value" + :key="item.label" + > + {{ item.label }} + </el-checkbox> + </el-space> + </el-checkbox-group> + </el-space> + </div> + </div> +</template> +<script setup> +import { ref } from 'vue'; + +const props = defineProps({ + // 鏁版嵁鍒囩墖锛岀嚎绱€�佹彁绀恒�佹函婧� + dataSlice: Array, + // 鐩戞祴鍥犲瓙 + factorType: Array, + factorOptions: Array, + // 鍦烘櫙绫诲瀷 + sceneType: Array, + sceneOptions: Array +}); + +const emits = defineEmits([ + 'update:data-slice', + 'update:factor-type', + 'update:scene-type' +]); +</script> +<style scoped> +:deep(.el-checkbox) { + --el-checkbox-text-color: white; + --main-color: #23dad1; + --el-checkbox-checked-text-color: var(--main-color); + --el-checkbox-checked-input-border-color: var(--main-color); + --el-checkbox-checked-bg-color: var(--main-color); + --el-checkbox-input-border-color-hover: var(--main-color); + + --el-checkbox-disabled-checked-input-fill: var(--main-color); + --el-checkbox-disabled-checked-input-border-color: var(--main-color); + --el-checkbox-disabled-checked-icon-color: white; + margin-right: 6px; + /* height: initial; */ +} +</style> -- Gitblit v1.9.3