| | |
| | | <template> |
| | | <el-row> |
| | | <el-col span="2" class="flex-col"> |
| | | <el-row justify="end"> |
| | | <CardButton |
| | | direction="left" |
| | | name="动态溯源" |
| | | @click="() => (show = !show)" |
| | | ></CardButton> |
| | | </el-row> |
| | | <el-row class="flex-col"> |
| | | <PollutedExceptionItem |
| | | :item="selectedException" |
| | | @showMarksAndPolygon="showMarksAndPolygon" |
| | | ></PollutedExceptionItem> |
| | | </el-row> |
| | | </el-col> |
| | | <el-col v-show="show" span="10"> |
| | | <BaseCard> |
| | | <template #content> |
| | | <!-- <div> |
| | | <el-input type="text" v-model="inputVal" /> |
| | | <button @click="handleSend">send</button> |
| | | <button @click="handleLink">link</button> |
| | | </div> --> |
| | | <!-- <div> |
| | | <el-text type="primary" size="large" tag="b">动态溯源</el-text> |
| | | </div> --> |
| | | <el-scrollbar ref="scrollbarRef" :height="height" class="scrollbar"> |
| | | <div ref="scrollContentRef"> |
| | | <div v-for="(item, index) in streams" :key="index"> |
| | | <el-row gap="4"> |
| | | <!-- <el-tag v-if="index == 0" type="danger">最新</el-tag> --> |
| | | <el-text type="primary">{{ |
| | | '线索时间:' + |
| | | item.pollutedData.startTime + |
| | | ' - ' + |
| | | item.pollutedData.endTime |
| | | }}</el-text> |
| | | </el-row> |
| | | <div> |
| | | <el-text type="primary"> |
| | | 污染区域:{{ item.pollutedArea.address }} |
| | | </el-text> |
| | | </div> |
| | | <div> |
| | | <el-text type="primary"> |
| | | 溯源距离:{{ |
| | | formatDistanceType(item.pollutedArea.distanceType) |
| | | }} |
| | | </el-text> |
| | | </div> |
| | | <el-row> |
| | | <el-col :span="6"> |
| | | <el-statistic |
| | | title="突变因子" |
| | | :value="item.pollutedData.factorName" |
| | | /> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-statistic |
| | | title="变化幅度" |
| | | :value="formatPercentage(item.pollutedData.avgPer)" |
| | | /> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-statistic |
| | | title="发生次数" |
| | | :value="item.pollutedData.times" |
| | | /> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-statistic |
| | | title="平均风速" |
| | | :value="item.pollutedData.windSpeed" |
| | | suffix="m/s" |
| | | /> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row justify="space-between"> |
| | | <el-link |
| | | type="primary" |
| | | underline |
| | | @click="item.showMore = !item.showMore" |
| | | > |
| | | {{ |
| | | (item.showMore ? '收起溯源场景' : '查看溯源场景') + |
| | | '(' + |
| | | item.pollutedSource.sceneList.length + |
| | | ')' |
| | | }} |
| | | </el-link> |
| | | <el-link |
| | | type="primary" |
| | | underline |
| | | @click="drawPolygon(item.pollutedArea)" |
| | | > |
| | | 定位异常 |
| | | </el-link> |
| | | </el-row> |
| | | <div v-show="item.showMore" style="width: 320px; height: 140px"> |
| | | <RealTimeLineChart |
| | | v-for="(item1, index1) in item._chartOptions" |
| | | :key="index1" |
| | | :model-value="item1" |
| | | ></RealTimeLineChart> |
| | | </div> |
| | | <SceneTable |
| | | v-show="item.showMore" |
| | | :show-marks="item.showMore" |
| | | :scene-list="item.pollutedSource.sceneList" |
| | | ></SceneTable> |
| | | <el-divider /> |
| | | |
| | | <!-- <el-space gap="4"> |
| | | <el-tag :type="item.status == 1 ? 'danger' : 'info'">{{ |
| | | item._statusStr |
| | | }}</el-tag> |
| | | <el-text type="default">{{ item.exception }}</el-text> |
| | | </el-space> |
| | | <el-row gap="4"> |
| | | <el-text type="primary">发生时间:</el-text> |
| | | <el-text type="primary">{{ |
| | | item.startTime + ' 至 ' |
| | | }}</el-text> |
| | | <el-text type="primary">{{ |
| | | item.status == 1 ? '当前' : item.endTime |
| | | }}</el-text> |
| | | </el-row> |
| | | <el-row> |
| | | <el-col :span="6"> |
| | | <el-statistic title="因子" :value="item.factorName" /> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-statistic title="均值" :value="item.avg" /> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-statistic title="峰值" :value="item.max" /> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-statistic title="谷值" :value="item.min" /> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row justify="space-between"> |
| | | <el-link |
| | | type="primary" |
| | | @click="item.showMore = !item.showMore" |
| | | > |
| | | {{ |
| | | (item.showMore ? '收起溯源场景' : '查看溯源场景') + |
| | | '(' + |
| | | item.relatedSceneList.length + |
| | | ')' |
| | | }} |
| | | </el-link> |
| | | <el-link type="primary" @click="drawSector(item)"> |
| | | 查看异常 |
| | | </el-link> |
| | | </el-row> |
| | | <SceneTable |
| | | v-show="item.showMore" |
| | | :scene-list="item.relatedSceneList" |
| | | ></SceneTable> |
| | | <el-divider /> --> |
| | | <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-scrollbar ref="scrollbarRef" class="scrollbar"> |
| | | <!-- <div |
| | | ref="scrollContentRef" |
| | | style="display: flex; width: fit-content" |
| | | > --> |
| | | <TransitionGroup name="list"> |
| | | <div |
| | | v-for="item in filterStreams" |
| | | :key="item.guid ? item.guid : item.time" |
| | | > |
| | | <ClueRecordItem |
| | | :item="item" |
| | | @open="handleOpen(item)" |
| | | ></ClueRecordItem> |
| | | </div> |
| | | </div> |
| | | </TransitionGroup> |
| | | <!-- </div> --> |
| | | </el-scrollbar> |
| | | </template> |
| | | </BaseCard> |
| | | </el-col> |
| | | <el-col span="2"> |
| | | <CardButton name="动态溯源" @click="() => (show = !show)"></CardButton> |
| | | </el-col> |
| | | <PollutedClueItem |
| | | v-model="clueDialog" |
| | | :item="selectedClue" |
| | | ></PollutedClueItem> |
| | | </el-row> |
| | | </template> |
| | | <script setup> |
| | |
| | | * 动态溯源信息管理 |
| | | * 通过websocket方式接收后端推送的异常信息并展示 |
| | | */ |
| | | import { reactive, ref, onMounted, onUnmounted, inject } from 'vue'; |
| | | import { reactive, ref, onMounted, onUnmounted, computed, watch } from 'vue'; |
| | | import moment from 'moment'; |
| | | |
| | | import websocket from '@/api/websocket'; |
| | | import sector from '@/utils/map/sector'; |
| | | import mapUtil from '@/utils/map/util'; |
| | |
| | | import { map, onMapMounted } from '@/utils/map/index_old'; |
| | | import { FactorDatas } from '@/model/FactorDatas'; |
| | | import factorDataParser from '@/utils/chart/factor-data-parser'; |
| | | import websocketMsgParser from '@/views/sourcetrace/websocketMsgParser.js'; |
| | | |
| | | const START_STR = '##'; |
| | | const SPLIT_STR = '&&'; |
| | | const END_STR = '%%'; |
| | | import PollutedExceptionItem from './component/PollutedExceptionItem.vue'; |
| | | import ClueRecordItem from './component/ClueRecordItem.vue'; |
| | | import PollutedClueItem from '@/views/sourcetrace/component/PollutedClueItem.vue'; |
| | | |
| | | const props = defineProps({ |
| | | factorType: String |
| | |
| | | |
| | | const emits = defineEmits(['update:factorType']); |
| | | |
| | | const height = `60vh`; |
| | | const height = `30vh`; |
| | | const width = `60vh`; |
| | | |
| | | const show = ref(false); |
| | | const clueDialog = ref(false); |
| | | const scrollContentRef = ref(); |
| | | const scrollbarRef = ref(); |
| | | |
| | | const selectedException = ref(); |
| | | const selectedClue = ref(); |
| | | const selectedMsgTypes = ref(['1', '2']); |
| | | |
| | | function scrollToBottom() { |
| | | const h1 = scrollContentRef.value.clientHeight + 100; |
| | |
| | | } |
| | | |
| | | const streams = reactive([]); |
| | | const filterStreams = computed(() => { |
| | | return streams.filter((v) => { |
| | | return selectedMsgTypes.value.indexOf(v._type) != -1; |
| | | }); |
| | | }); |
| | | |
| | | const inputVal = ref(''); |
| | | const handleSend = () => { |
| | | websocket.send(inputVal.value); |
| | | }; |
| | | |
| | | const dealMsg = (data) => { |
| | | const [type, content] = data |
| | | .substring(START_STR.length, data.length - END_STR.length) |
| | | .split(SPLIT_STR); |
| | | let showFirstClueTask; |
| | | function dealMsg(data) { |
| | | const { type, content } = websocketMsgParser.parseMsg(data); |
| | | |
| | | // 污染线索 PollutedClue |
| | | if (type == '1') { |
| | | const obj = JSON.parse(content); |
| | | obj.showMore = true; |
| | | console.log('污染线索: ', obj); |
| | | drawPolygon(obj.pollutedArea); |
| | | parseChartData(obj); |
| | | if (streams.length == 0) { |
| | | streams.push(obj); |
| | | } else { |
| | | streams.forEach((s) => { |
| | | s.showMore = false; |
| | | }); |
| | | streams.unshift(obj); |
| | | show.value = true; |
| | | } |
| | | 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; |
| | | |
| | | // scrollToBottom(); |
| | | scrollToTop(); |
| | | } |
| | | // 污染分析结果 AnalysisResult |
| | | else if (type == '2') { |
| | | // scrollToTop(); |
| | | // drawPolygon(obj.pollutedArea); |
| | | parseChartData(obj); |
| | | |
| | | // if (showFirstClueTask) { |
| | | // clearTimeout(showFirstClueTask); |
| | | // } |
| | | // showFirstClueTask = setTimeout(() => { |
| | | // showMarksAndPolygon(obj); |
| | | // }, 1000); |
| | | } else if (type == '2') { |
| | | const obj = JSON.parse(content); |
| | | console.log('污染分析结果: ', obj); |
| | | obj._type = type; |
| | | console.log('污染线索结果: ', obj); |
| | | obj._timestr = timeFormatter(obj.time); |
| | | // streams.unshift(obj); |
| | | addNewMsg(obj); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | // 按照一定时间间隔新增一条消息 |
| | | let addNewMsgTask; |
| | | const leftMsgList = []; |
| | | function addNewMsg(msg, inside) { |
| | | if (!addNewMsgTask && (leftMsgList.length == 0 || inside)) { |
| | | streams.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); |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | websocket.registerReceiveEvent(dealMsg); |
| | |
| | | // } |
| | | }); |
| | | |
| | | // 绘制污染区域 |
| | | let pollutedAreaPolygon; |
| | | function drawPolygon(pollutedArea) { |
| | | if (pollutedAreaPolygon) { |
| | | map.remove(pollutedAreaPolygon); |
| | | watch(clueDialog, (nV, oV) => { |
| | | if (nV != oV && !nV) { |
| | | selectedClue.value._selected = false; |
| | | } |
| | | const bounds = pollutedArea.polygon.map((v) => [v.first, v.second]); |
| | | // eslint-disable-next-line no-undef |
| | | pollutedAreaPolygon = new AMap.Polygon({ |
| | | map: map, //显示该覆盖物的地图对象 |
| | | strokeWeight: 2, //轮廓线宽度 |
| | | path: bounds, //多边形轮廓线的节点坐标数组 |
| | | fillOpacity: 0, //多边形填充透明度 |
| | | fillColor: '#CCF3FF', //多边形填充颜色 |
| | | strokeColor: '#02ffea', //线条颜色 |
| | | // strokeColor: '#0552f7', //线条颜色 |
| | | strokeStyle: 'dashed', |
| | | zIndex: 9 |
| | | }); |
| | | |
| | | function handleOpen(item) { |
| | | switch (item._type) { |
| | | case '1': |
| | | if (selectedException.value) { |
| | | selectedException.value._selected = false; |
| | | } |
| | | showMarksAndPolygon(item); |
| | | selectedException.value = item; |
| | | break; |
| | | case '2': |
| | | if (selectedClue.value) { |
| | | selectedClue.value._selected = false; |
| | | } |
| | | selectedClue.value = item; |
| | | clueDialog.value = true; |
| | | break; |
| | | } |
| | | item._selected = true; |
| | | } |
| | | |
| | | function hideAll() { |
| | | streams.forEach((s) => { |
| | | if (polygonMap.has(s.guid)) { |
| | | s.showMore = false; |
| | | map.remove(polygonMap.get(s.guid)); |
| | | } |
| | | }); |
| | | map.setFitView(); |
| | | } |
| | | |
| | | function showMarksAndPolygon(item) { |
| | | item.showMore = !item.showMore; |
| | | if (item.showMore) { |
| | | hideAll(); |
| | | item.showMore = true; |
| | | drawPolygon(item); |
| | | } else { |
| | | if (polygonMap.has(item.guid)) { |
| | | map.remove(polygonMap.get(item.guid)); |
| | | selectedException.value._selected = false; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 绘制污染区域(高德地图的多边形对象,通过vue的reactive包装为响应式对象后,无法正确在地图中使用) |
| | | const polygonMap = new Map(); |
| | | function drawPolygon(item) { |
| | | const pollutedArea = item.pollutedArea; |
| | | if (polygonMap.has(item.guid)) { |
| | | map.add(polygonMap.get(item.guid)); |
| | | } else { |
| | | const bounds = pollutedArea.polygon.map((v) => [v.first, v.second]); |
| | | // eslint-disable-next-line no-undef |
| | | const pollutedAreaPolygon = new AMap.Polygon({ |
| | | map: map, //显示该覆盖物的地图对象 |
| | | strokeWeight: 2, //轮廓线宽度 |
| | | path: bounds, //多边形轮廓线的节点坐标数组 |
| | | fillOpacity: 0, //多边形填充透明度 |
| | | fillColor: '#CCF3FF', //多边形填充颜色 |
| | | strokeColor: '#02ffea', //线条颜色 |
| | | // strokeColor: '#0552f7', //线条颜色 |
| | | strokeStyle: 'dashed', |
| | | zIndex: 9 |
| | | }); |
| | | polygonMap.set(item.guid, pollutedAreaPolygon); |
| | | } |
| | | // map.setFitView(polygonMap.get(item.guid)); |
| | | } |
| | | |
| | | function parseChartData(obj) { |
| | | // console.log('折线图:start'); |
| | | const factorDatas = new FactorDatas(); |
| | | factorDatas.setData(obj.pollutedData.dataVoList, 0, () => { |
| | | factorDatas.setData(obj.pollutedData.historyDataList, 0, () => { |
| | | obj._chartOptions = factorDataParser.parseData(factorDatas, [ |
| | | { |
| | | label: obj.pollutedData.factorName, |
| | |
| | | value: obj.pollutedData.factorId + '' |
| | | } |
| | | ]); |
| | | console.log('折线图:', obj._chartOptions); |
| | | // console.log('折线图:', obj._chartOptions); |
| | | }); |
| | | } |
| | | |
| | | 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; |
| | | } |
| | | function timeFormatter(time) { |
| | | return moment(time).format('YYYY-MM-DD HH:mm:ss'); |
| | | } |
| | | |
| | | /******************************************************************************************************************** */ |
| | |
| | | --el-statistic-content-color: white; |
| | | } |
| | | |
| | | :deep(.el-text) { |
| | | :deep(.el-text.el-text--primary) { |
| | | --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; |
| | | width: 400px; |
| | | /* max-width: 60vw; */ |
| | | height: 45vh; |
| | | /* color: #02ffea; */ |
| | | padding-right: 10px; |
| | | } |
| | | |
| | | .clue-card { |
| | | padding: 0 4px; |
| | | /* margin-right: 2px; */ |
| | | width: 340px; |
| | | height: 35vh; |
| | | border-right: 1px solid white; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .border-dashed { |
| | | /* border: 1px dashed white; */ |
| | | border: 1px dashed #ffbc58; |
| | | 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> |