| | |
| | | <template> |
| | | <el-row> |
| | | <el-col span="2"> |
| | | <CardButton |
| | | direction="left" |
| | | name="动态溯源" |
| | | @click="() => (show = !show)" |
| | | ></CardButton> |
| | | <el-row |
| | | :style=" |
| | | direction == 'left' |
| | | ? 'flex-direction: row;' |
| | | : 'flex-direction: row-reverse' |
| | | " |
| | | > |
| | | <el-col span="2" class="flex-col"> |
| | | <el-row justify="end"> |
| | | <CardButton |
| | | :direction="direction" |
| | | name="动态溯源" |
| | | @click="handleDisplayChange" |
| | | ></CardButton> |
| | | </el-row> |
| | | <el-row class="flex-col"> |
| | | <PollutedWarnItem |
| | | :item="selectedWarning" |
| | | @showMarksAndPolygon="showMarksAndPolygon" |
| | | ></PollutedWarnItem> |
| | | <PollutedExceptionItem |
| | | :item="selectedException" |
| | | @showMarksAndPolygon="showMarksAndPolygon" |
| | | ></PollutedExceptionItem> |
| | | <PollutedClueItem |
| | | v-model="clueDialog" |
| | | :item="selectedClue" |
| | | ></PollutedClueItem> |
| | | </el-row> |
| | | </el-col> |
| | | <el-col v-show="show" span="10"> |
| | | <BaseCard> |
| | | <template #content> |
| | | <el-scrollbar ref="scrollbarRef" class="scrollbar"> |
| | | <div |
| | | ref="scrollContentRef" |
| | | style="display: flex; width: fit-content" |
| | | > |
| | | <template v-for="(item, index) in streams" :key="index"> |
| | | <PollutedClueItem :item="item"></PollutedClueItem> |
| | | </template> |
| | | <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="info">溯源:{{ countMsg1.type1 }}条</el-text> |
| | | <el-text type="info">线索:{{ countMsg1.type2 }}条</el-text> |
| | | <el-text type="info">提醒:{{ countMsg1.type3 }}条</el-text> |
| | | </el-space> |
| | | </div> |
| | | </el-row> |
| | | <el-scrollbar ref="scrollbarRef" class="scrollbar"> |
| | | <TransitionGroup name="list"> |
| | | <div |
| | | v-for="item in filterStreams" |
| | | :key="item.guid ? item.guid : item.time" |
| | | > |
| | | <ClueRecordItem |
| | | :item="item" |
| | | @open="handleOpen(item)" |
| | | ></ClueRecordItem> |
| | | </div> |
| | | </TransitionGroup> |
| | | </el-scrollbar> |
| | | </template> |
| | | </BaseCard> |
| | | </el-col> |
| | | <!-- <el-col v-if="direction == 'left'" span="2" class="flex-col"> |
| | | <el-row justify="start"> |
| | | <CardButton |
| | | direction="right" |
| | | name="动态溯源" |
| | | @click="handleDisplayChange" |
| | | ></CardButton> |
| | | </el-row> |
| | | <el-row class="flex-col"> |
| | | <PollutedExceptionItem |
| | | :item="selectedException" |
| | | @showMarksAndPolygon="showMarksAndPolygon" |
| | | ></PollutedExceptionItem> |
| | | <PollutedClueItem |
| | | v-model="clueDialog" |
| | | :item="selectedClue" |
| | | ></PollutedClueItem> |
| | | </el-row> |
| | | </el-col> --> |
| | | </el-row> |
| | | </template> |
| | | <script setup> |
| | |
| | | * 动态溯源信息管理 |
| | | * 通过websocket方式接收后端推送的异常信息并展示 |
| | | */ |
| | | import { reactive, ref, onMounted, onUnmounted, unref } from 'vue'; |
| | | import { reactive, ref, onMounted, onUnmounted, computed, watch } from 'vue'; |
| | | import moment from 'moment'; |
| | | |
| | | import websocket from '@/api/websocket'; |
| | | import dataAnalysisApi from '@/api/dataAnalysisApi'; |
| | | import sector from '@/utils/map/sector'; |
| | | import mapUtil from '@/utils/map/util'; |
| | | import { sceneTypes, sceneIcon } from '@/constant/scene-types'; |
| | |
| | | import factorDataParser from '@/utils/chart/factor-data-parser'; |
| | | import websocketMsgParser from '@/views/sourcetrace/websocketMsgParser.js'; |
| | | |
| | | import PollutedClueItem from './component/PollutedClueItem.vue'; |
| | | 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'; |
| | | import PollutedWarnItem from './component/PollutedWarnItem.vue'; |
| | | |
| | | const NO_SCENE = 'no_scene'; |
| | | |
| | | const MODE_REALTIME = 'realtime'; |
| | | const MODE_HISTORY = 'history'; |
| | | |
| | | const props = defineProps({ |
| | | factorType: String |
| | | direction: { |
| | | type: String, |
| | | default: 'right' |
| | | }, |
| | | factorType: String, |
| | | // 模式,realtime:实时模式;history:历史数据模式 |
| | | mode: { |
| | | type: String, |
| | | default: 'realtime' |
| | | }, |
| | | missionCode: String, |
| | | deviceCode: String |
| | | }); |
| | | |
| | | const emits = defineEmits(['update:factorType']); |
| | | |
| | | const height = `30vh`; |
| | | const width = `60vh`; |
| | | |
| | | const show = ref(false); |
| | | const scrollContentRef = ref(); |
| | | const scrollbarRef = ref(); |
| | | |
| | | const selectedWarning = ref(); |
| | | const selectedException = ref(); |
| | | const selectedClue = ref(); |
| | | const clueDialog = ref(false); |
| | | |
| | | const selectedMsgTypes = ref(['1', '2', '3']); |
| | | const selectedFactorTypes = ref([]); |
| | | const factorOptions = ref([]); |
| | | const selectedSceneTypes = ref([]); |
| | | const sceneOptions = ref([]); |
| | | |
| | | function handleDisplayChange() { |
| | | show.value = !show.value; |
| | | if ( |
| | | !show.value && |
| | | selectedException.value && |
| | | selectedException.value.showMore |
| | | ) { |
| | | showMarksAndPolygon(selectedException.value); |
| | | } |
| | | } |
| | | |
| | | function scrollToBottom() { |
| | | const h1 = scrollContentRef.value.clientHeight + 100; |
| | |
| | | }, 100); |
| | | } |
| | | |
| | | const streams = reactive([]); |
| | | const streams = ref([]); |
| | | const filterStreams = computed(() => { |
| | | return streams.value.filter((v) => { |
| | | // 判断消息类型是否选中 |
| | | 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.value.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(''); |
| | | const handleSend = () => { |
| | | websocket.send(inputVal.value); |
| | | }; |
| | | |
| | | let showFirstClueTask; |
| | | function dealMsg(data) { |
| | | const { type, content } = websocketMsgParser.parseMsg(data); |
| | | |
| | | // 污染线索 PollutedClue |
| | | if (type == '1') { |
| | | const obj = reactive(JSON.parse(content)); |
| | | // 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); |
| | | } |
| | | show.value = true; |
| | | |
| | | // scrollToBottom(); |
| | | scrollToTop(); |
| | | // drawPolygon(obj.pollutedArea); |
| | | parseChartData(obj); |
| | | |
| | | if (showFirstClueTask) { |
| | | clearTimeout(showFirstClueTask); |
| | | } |
| | | showFirstClueTask = setTimeout(() => { |
| | | showMarksAndPolygon(obj); |
| | | }, 1000); |
| | | const obj = reactive(JSON.parse(content)); |
| | | if (obj.deviceCode == props.deviceCode) { |
| | | obj._type = type; |
| | | dealObj(obj); |
| | | } |
| | | } |
| | | |
| | | function dealObj(obj) { |
| | | // 污染线索 PollutedClue |
| | | if (obj._type == '1') { |
| | | obj.showMore = false; |
| | | console.log('污染异常切片: ', obj); |
| | | |
| | | addNewMsg(obj); |
| | | show.value = true; |
| | | |
| | | // scrollToBottom(); |
| | | // scrollToTop(); |
| | | // drawPolygon(obj.pollutedArea); |
| | | parseChartData(obj); |
| | | } else if (obj._type == '2') { |
| | | // const obj = JSON.parse(content); |
| | | // obj._type = type; |
| | | console.log('污染线索结果: ', obj); |
| | | obj._timestr = timeFormatter(obj.time); |
| | | addNewMsg(obj); |
| | | } else if (obj._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) { |
| | | // 若没有找到风险源时,将该分类设定为null |
| | | 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; |
| | | } |
| | | } |
| | | |
| | | // 按照一定时间间隔新增一条消息 |
| | | let addNewMsgTask; |
| | | const leftMsgList = []; |
| | | function addNewMsg(msg, inside) { |
| | | if (!addNewMsgTask && (leftMsgList.length == 0 || inside)) { |
| | | streams.value.splice(0, 0, msg); |
| | | addNewMsgTask = setTimeout(() => { |
| | | clearTimeout(addNewMsgTask); |
| | | addNewMsgTask = undefined; |
| | | if (leftMsgList.length > 0) { |
| | | const next = leftMsgList.shift(); |
| | | addNewMsg(next, true); |
| | | } |
| | | }, 1000); |
| | | } else { |
| | | leftMsgList.push(msg); |
| | | } |
| | | } |
| | | |
| | | function fetchPollutionTraceHistory() { |
| | | dataAnalysisApi.fetchPollutionTraceHistory(props.missionCode).then((res) => { |
| | | const objList = JSON.parse(res.data); |
| | | objList.forEach((obj) => { |
| | | obj._type = obj.msgType + ''; |
| | | if (obj._type == '1') { |
| | | obj.showMore = false; |
| | | show.value = true; |
| | | parseChartData(obj); |
| | | } else if (obj._type == '2') { |
| | | obj._timestr = timeFormatter(obj.time); |
| | | } else if (obj._type == '3') { |
| | | parseChartData(obj); |
| | | } |
| | | optionsFilte(obj); |
| | | }); |
| | | streams.value = objList; |
| | | }); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | websocket.registerReceiveEvent(dealMsg); |
| | | if (props.mode == MODE_REALTIME) { |
| | | websocket.registerReceiveEvent(dealMsg); |
| | | } else if (props.missionCode) { |
| | | fetchPollutionTraceHistory(); |
| | | } |
| | | }); |
| | | onUnmounted(() => { |
| | | websocket.removeReceiveEvent(dealMsg); |
| | |
| | | // } |
| | | }); |
| | | |
| | | watch(clueDialog, (nV, oV) => { |
| | | if (nV != oV && !nV) { |
| | | selectedClue.value._selected = false; |
| | | } |
| | | }); |
| | | |
| | | watch( |
| | | () => props.missionCode, |
| | | (nV, oV) => { |
| | | if (nV != oV) { |
| | | fetchPollutionTraceHistory(); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | function handleOpen(item) { |
| | | // 去除上一个“溯源”和“提醒”消息的选中标志 |
| | | if (selectedWarning.value && selectedWarning.value._selected) { |
| | | selectedWarning.value._selected = false; |
| | | showMarksAndPolygon(selectedWarning.value); |
| | | } |
| | | if (selectedException.value && selectedException.value._selected) { |
| | | selectedException.value._selected = false; |
| | | showMarksAndPolygon(selectedException.value); |
| | | } |
| | | if (selectedClue.value && selectedClue.value._selected) { |
| | | selectedClue.value._selected = false; |
| | | clueDialog.value = false; |
| | | } |
| | | switch (item._type) { |
| | | case '1': |
| | | // 显示当前选中的相关地图标记 |
| | | showMarksAndPolygon(item); |
| | | // 更新选中的对象 |
| | | selectedException.value = item; |
| | | break; |
| | | case '2': |
| | | selectedClue.value = item; |
| | | clueDialog.value = true; |
| | | break; |
| | | case '3': |
| | | showMarksAndPolygon(item); |
| | | selectedWarning.value = item; |
| | | break; |
| | | } |
| | | item._selected = true; |
| | | } |
| | | |
| | | function hideAll() { |
| | | streams.forEach((s) => { |
| | | streams.value.forEach((s) => { |
| | | if (polygonMap.has(s.guid)) { |
| | | s.showMore = false; |
| | | map.remove(polygonMap.get(s.guid)); |
| | |
| | | } else { |
| | | if (polygonMap.has(item.guid)) { |
| | | map.remove(polygonMap.get(item.guid)); |
| | | item._selected = false; |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | function parseChartData(obj) { |
| | | // console.log('折线图:start'); |
| | | obj.pollutedData._startTime = moment(obj.pollutedData.startTime).format( |
| | | 'HH:mm:ss' |
| | | ); |
| | | obj.pollutedData._endTime = moment(obj.pollutedData.endTime).format( |
| | | 'HH:mm:ss' |
| | | ); |
| | | const factorDatas = new FactorDatas(); |
| | | factorDatas.setData(obj.pollutedData.historyDataList, 0, () => { |
| | | obj._chartOptions = factorDataParser.parseData(factorDatas, [ |
| | |
| | | }); |
| | | } |
| | | |
| | | function formatPercentage(value) { |
| | | return Math.round(value * 100) + '%'; |
| | | } |
| | | |
| | | function formatDistanceType(value) { |
| | | switch (value) { |
| | | case 'TYPE1': |
| | | return '50米'; |
| | | case 'TYPE2': |
| | | return '50米 - 500米'; |
| | | case 'TYPE3': |
| | | return '50米 - 1公里'; |
| | | case 'TYPE4': |
| | | return '50米 - 2公里'; |
| | | |
| | | default: |
| | | break; |
| | | } |
| | | } |
| | | |
| | | /******************************************************************************************************************** */ |
| | | |
| | | /** |
| | | * 添加一条工作流信息 |
| | | * @param {*} data |
| | | */ |
| | | const putWorkStream = (data) => { |
| | | data.substring(); |
| | | const obj = JSON.parse(data); |
| | | console.log('sourcetrace: ', obj); |
| | | |
| | | obj._statusStr = exceptionStatus(obj.status); |
| | | |
| | | if (streams.length == 0) { |
| | | streams.push(obj); |
| | | } else { |
| | | const index = streams.findIndex((v) => { |
| | | return v.guid == obj.guid; |
| | | }); |
| | | if (index != -1) { |
| | | const old = streams[index]; |
| | | obj.showMore = old.showMore; |
| | | old.relatedSceneList.forEach((s) => { |
| | | const index = obj.relatedSceneList.findIndex((v) => { |
| | | return v.guid == s.guid; |
| | | }); |
| | | if (index == -1) { |
| | | obj.relatedSceneList.push(s); |
| | | } |
| | | }); |
| | | streams.splice(index, 1, obj); |
| | | } else { |
| | | streams.unshift(obj); |
| | | } |
| | | |
| | | show.value = true; |
| | | } |
| | | |
| | | // scrollToBottom(); |
| | | scrollToTop(); |
| | | |
| | | const excObj = streams.find((v) => { |
| | | return v.factorId + '' == props.factorType; |
| | | }); |
| | | if (excObj) { |
| | | drawSector(excObj); |
| | | } |
| | | }; |
| | | |
| | | function exceptionStatus(value) { |
| | | switch (value) { |
| | | case 1: |
| | | return '异常发生中'; |
| | | case 2: |
| | | return '异常已结束'; |
| | | default: |
| | | return '异常已结束'; |
| | | } |
| | | } |
| | | |
| | | let lastDrawObjGuid; |
| | | function drawSector(exceptionObj) { |
| | | emits('update:factorType', exceptionObj.factorId + ''); |
| | | setTimeout(() => { |
| | | if (exceptionObj.guid != lastDrawObjGuid) { |
| | | sector.clearSectorPt(); |
| | | lastDrawObjGuid = exceptionObj.guid; |
| | | } |
| | | // 1. 绘制新扇形区域 |
| | | const datavo = exceptionObj.midData; |
| | | const factorDatas = new FactorDatas(); |
| | | factorDatas.setData([datavo], 0, () => { |
| | | const pr = sector.drawSectorPt(factorDatas, 0); |
| | | // 调整视角居中显示 |
| | | // mapUtil.setCenter(pr.p); |
| | | drawMarks(exceptionObj.relatedSceneList); |
| | | }); |
| | | }, 1000); |
| | | } |
| | | |
| | | let layer = undefined; |
| | | function drawMarks(sceneList) { |
| | | if (layer != undefined) { |
| | | mapUtil.removeViews(layer); |
| | | // layer = undefined; |
| | | } |
| | | if (sceneList.length != 0) { |
| | | const icons = []; |
| | | sceneList.forEach((s) => { |
| | | icons.push(sceneIcon(s.typeId)); |
| | | }); |
| | | layer = marks.createLabelMarks(icons, sceneList, true); |
| | | } |
| | | function timeFormatter(time) { |
| | | return moment(time).format('YYYY-MM-DD HH:mm:ss'); |
| | | } |
| | | </script> |
| | | <style scoped> |
| | |
| | | --el-statistic-content-color: white; |
| | | } |
| | | |
| | | :deep(.el-text.el-text--primary) { |
| | | :deep(.el-text.el-text--info) { |
| | | --el-text-color: white; |
| | | } |
| | | |
| | |
| | | --el-link-text-color: #23dad1; |
| | | } |
| | | |
| | | .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; */ |
| | | } |
| | | |
| | | .scrollbar { |
| | | min-width: 300px; |
| | | max-width: 60vw; |
| | | /* height: 35vh; */ |
| | | width: 400px; |
| | | /* max-width: 60vw; */ |
| | | height: 65vh; |
| | | /* color: #02ffea; */ |
| | | padding-right: 10px; |
| | | } |
| | | |
| | | .clue-card { |
| | |
| | | padding: 0px 1px; |
| | | margin-bottom: 4px; |
| | | } |
| | | .flex-col { |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: space-between; |
| | | } |
| | | </style> |
| | | <style> |
| | | .list-move, /* 对移动中的元素应用的过渡 */ |
| | | .list-enter-active, |
| | | .list-leave-active { |
| | | /* transition: all 0.8s cubic-bezier(0.55, 0, 0.1, 1); */ |
| | | transition: all 0.5s ease; |
| | | } |
| | | |
| | | .list-enter-from, |
| | | .list-leave-to { |
| | | opacity: 0; |
| | | transform: scaleY(0.01) translate(300px, 0); |
| | | } |
| | | |
| | | /* 确保将离开的元素从布局流中删除 |
| | | 以便能够正确地计算移动的动画。 */ |
| | | .list-leave-active { |
| | | position: absolute; |
| | | } |
| | | </style> |