riku
2025-06-12 a3b2d94cbfb9bea819346a1b738237f72819a833
动态溯源(待完成)
已修改7个文件
已添加3个文件
1292 ■■■■ 文件已修改
src/api/index.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/scene/SceneTable.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/chart/chart-option.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/realtimemode/RealtimeMode.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sourcetrace/SourceTrace copy.vue 345 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sourcetrace/SourceTrace.vue 188 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sourcetrace/UnderwayAdvice.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sourcetrace/component/ClueRecordItem.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sourcetrace/component/PollutedClueItem.vue 309 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sourcetrace/component/PollutedExceptionItem.vue 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/index.js
@@ -13,10 +13,10 @@
}
if (debug) {
  // ip1 = 'http://192.168.0.103:8084/';
  ip1 = 'http://localhost:8084/';
  // ws = `192.168.0.103:9031`;
  ws = `localhost:9031`;
  ip1 = 'http://192.168.0.103:8084/';
  // ip1 = 'http://localhost:8084/';
  ws = `192.168.0.103:9031`;
  // ws = `localhost:9031`;
}
const $http = axios.create({
src/components/scene/SceneTable.vue
@@ -29,6 +29,7 @@
        <div>{{ parseInt(row.length) + 'ç±³' }}</div>
      </template>
    </el-table-column>
    <slot></slot>
    <!-- <el-table-column
      prop="districtName"
      label="区县"
src/utils/chart/chart-option.js
@@ -137,7 +137,7 @@
      left: '12%',
      right: '2%',
      top: '7%',
      bottom: '20%'
      bottom: '30%'
    },
    legend: {
      show: false
src/views/realtimemode/RealtimeMode.vue
@@ -29,7 +29,7 @@
      class="source-trace"
      v-model:factorType="factorType"
    ></SourceTrace>
    <UnderwayAdvice></UnderwayAdvice>
    <!-- <PollutedClueItem></PollutedClueItem> -->
  </div>
</template>
@@ -45,6 +45,7 @@
import DeviceChange from './component/DeviceChange.vue';
import SourceTrace from '@/views/sourcetrace/SourceTrace.vue';
import UnderwayAdvice from '@/views/sourcetrace/UnderwayAdvice.vue';
import PollutedClueItem from '@/views/sourcetrace/component/PollutedClueItem.vue';
import { realTimeMapAnimation } from '@/utils/map/animation';
import {
  fetchHistoryData,
@@ -67,7 +68,8 @@
    RealTimeTrend,
    DeviceChange,
    SourceTrace,
    UnderwayAdvice
    UnderwayAdvice,
    PollutedClueItem
  },
  setup() {
    const { loading, fetchData } = useFetchData(10000);
@@ -142,7 +144,7 @@
                // startTime: '2024-12-13 16:35:00',
                // startTime: '2024-11-27 11:50:41', // Pm, ä¸­è·ç¦»å·¥åœ°
                // startTime: '2024-08-30 15:28:00', // voc è¿‘距离汽修
                startTime: '2024-07-23 15:22:00',
                startTime: '2024-07-23 15:21:30',
                // startTime: '2024-07-23 14:39:00',
                endTime: '2025-01-16 11:51:41',
                page,
src/views/sourcetrace/SourceTrace copy.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,345 @@
<template>
  <el-row>
    <el-col span="2">
      <CardButton
        direction="left"
        name="动态溯源"
        @click="() => (show = !show)"
      ></CardButton>
    </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"
            >
              <TransitionGroup name="list">
                <div v-for="(item, index) in streams" :key="index">
                  <PollutedExceptionItem
                    :item="item"
                    @showMarksAndPolygon="showMarksAndPolygon"
                  ></PollutedExceptionItem>
                </div>
              </TransitionGroup>
            </div>
          </el-scrollbar>
        </template>
      </BaseCard>
    </el-col>
  </el-row>
</template>
<script setup>
/**
 * åŠ¨æ€æº¯æºä¿¡æ¯ç®¡ç†
 * é€šè¿‡websocket方式接收后端推送的异常信息并展示
 */
import { reactive, ref, onMounted, onUnmounted, unref } from 'vue';
import websocket from '@/api/websocket';
import sector from '@/utils/map/sector';
import mapUtil from '@/utils/map/util';
import { sceneTypes, sceneIcon } from '@/constant/scene-types';
import marks from '@/utils/map/marks';
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';
import PollutedExceptionItem from './component/PollutedExceptionItem.vue';
const props = defineProps({
  factorType: String
});
const emits = defineEmits(['update:factorType']);
const height = `30vh`;
const width = `60vh`;
const show = ref(false);
const scrollContentRef = ref();
const scrollbarRef = ref();
function scrollToBottom() {
  const h1 = scrollContentRef.value.clientHeight + 100;
  setTimeout(() => {
    scrollbarRef.value.setScrollTop(h1);
  }, 100);
}
function scrollToTop() {
  setTimeout(() => {
    scrollbarRef.value.setScrollTop(0);
  }, 100);
}
const streams = reactive([]);
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);
  }
}
onMounted(() => {
  websocket.registerReceiveEvent(dealMsg);
});
onUnmounted(() => {
  websocket.removeReceiveEvent(dealMsg);
  sector.clearSectorPt();
  mapUtil.clearMap();
  // if (layer != undefined) {
  //   mapUtil.removeViews(layer);
  //   layer = undefined;
  // }
});
function hideAll() {
  streams.forEach((s) => {
    if (polygonMap.has(s.guid)) {
      s.showMore = false;
      map.remove(polygonMap.get(s.guid));
    }
  });
}
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));
    }
  }
}
// ç»˜åˆ¶æ±¡æŸ“区域(高德地图的多边形对象,通过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.historyDataList, 0, () => {
    obj._chartOptions = factorDataParser.parseData(factorDatas, [
      {
        label: obj.pollutedData.factorName,
        name: obj.pollutedData.factorName,
        value: obj.pollutedData.factorId + ''
      }
    ]);
    // console.log('折线图:', obj._chartOptions);
  });
}
/******************************************************************************************************************** */
/**
 * æ·»åŠ ä¸€æ¡å·¥ä½œæµä¿¡æ¯
 * @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);
  }
}
</script>
<style scoped>
:deep(.el-statistic) {
  --el-statistic-title-color: rgb(215, 215, 215);
  --el-statistic-content-color: white;
}
:deep(.el-text.el-text--primary) {
  --el-text-color: white;
}
:deep(.el-link) {
  --el-link-text-color: #23dad1;
}
.scrollbar {
  min-width: 300px;
  max-width: 60vw;
  /* height: 35vh; */
  /* color: #02ffea; */
}
.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;
}
</style>
<style>
.list-move, /* å¯¹ç§»åŠ¨ä¸­çš„å…ƒç´ åº”ç”¨çš„è¿‡æ¸¡ */
.list-enter-active,
.list-leave-active {
  transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(-30px);
}
/* ç¡®ä¿å°†ç¦»å¼€çš„元素从布局流中删除
  ä»¥ä¾¿èƒ½å¤Ÿæ­£ç¡®åœ°è®¡ç®—移动的动画。 */
.list-leave-active {
  position: absolute;
}
</style>
src/views/sourcetrace/SourceTrace.vue
@@ -1,33 +1,54 @@
<template>
  <el-row>
    <el-col span="2">
      <CardButton
        direction="left"
        name="动态溯源"
        @click="() => (show = !show)"
      ></CardButton>
    <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>
          <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
            <!-- <div
              ref="scrollContentRef"
              style="display: flex; width: fit-content"
            >
            > -->
            <TransitionGroup name="list">
              <div v-for="(item, index) in streams" :key="index">
                <PollutedClueItem
              <div
                v-for="item in filterStreams"
                :key="item.guid ? item.guid : item.time"
              >
                <ClueRecordItem
                  :item="item"
                  @showMarksAndPolygon="showMarksAndPolygon"
                ></PollutedClueItem>
                  @open="handleOpen(item)"
                ></ClueRecordItem>
              </div>
            </TransitionGroup>
            </div>
            <!-- </div> -->
          </el-scrollbar>
        </template>
      </BaseCard>
    </el-col>
    <PollutedClueItem
      v-model="clueDialog"
      :item="selectedClue"
    ></PollutedClueItem>
  </el-row>
</template>
<script setup>
@@ -35,7 +56,9 @@
 * åŠ¨æ€æº¯æºä¿¡æ¯ç®¡ç†
 * é€šè¿‡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 sector from '@/utils/map/sector';
import mapUtil from '@/utils/map/util';
@@ -46,7 +69,9 @@
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';
const props = defineProps({
  factorType: String
@@ -58,8 +83,13 @@
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;
@@ -75,6 +105,11 @@
}
const streams = reactive([]);
const filterStreams = computed(() => {
  return streams.filter((v) => {
    return selectedMsgTypes.value.indexOf(v._type) != -1;
  });
});
const inputVal = ref('');
const handleSend = () => {
@@ -88,32 +123,60 @@
  // æ±¡æŸ“线索 PollutedClue
  if (type == '1') {
    const obj = reactive(JSON.parse(content));
    obj._type = type;
    // obj.showMore = true;
    obj.showMore = false;
    console.log('污染线索: ', obj);
    console.log('污染异常切片: ', obj);
    if (streams.length == 0) {
      streams.push(obj);
    } else {
      // streams.forEach((s) => {
      //   showMarksAndPolygon(s);
      // });
      hideAll();
      streams.unshift(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();
    // scrollToTop();
    // drawPolygon(obj.pollutedArea);
    parseChartData(obj);
    if (showFirstClueTask) {
      clearTimeout(showFirstClueTask);
    }
    showFirstClueTask = setTimeout(() => {
      showMarksAndPolygon(obj);
    // if (showFirstClueTask) {
    //   clearTimeout(showFirstClueTask);
    // }
    // showFirstClueTask = setTimeout(() => {
    //   showMarksAndPolygon(obj);
    // }, 1000);
  } else if (type == '2') {
    const obj = JSON.parse(content);
    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);
  }
}
@@ -129,6 +192,32 @@
  //   layer = undefined;
  // }
});
watch(clueDialog, (nV, oV) => {
  if (nV != oV && !nV) {
    selectedClue.value._selected = false;
  }
});
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) => {
@@ -148,6 +237,7 @@
  } else {
    if (polygonMap.has(item.guid)) {
      map.remove(polygonMap.get(item.guid));
      selectedException.value._selected = false;
    }
  }
}
@@ -190,6 +280,10 @@
    ]);
    // console.log('折线图:', obj._chartOptions);
  });
}
function timeFormatter(time) {
  return moment(time).format('YYYY-MM-DD HH:mm:ss');
}
/******************************************************************************************************************** */
@@ -301,11 +395,27 @@
  --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: 45vh;
  /* color: #02ffea; */
  padding-right: 10px;
}
.clue-card {
@@ -323,18 +433,24 @@
  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: translateX(30px);
  transform: scaleY(0.01) translate(300px, 0);
}
/* ç¡®ä¿å°†ç¦»å¼€çš„元素从布局流中删除
@@ -342,4 +458,4 @@
.list-leave-active {
  position: absolute;
}
</style>
</style>
src/views/sourcetrace/UnderwayAdvice.vue
@@ -26,42 +26,45 @@
          </el-text>
        </el-row>
      </template> -->
      <TransitionGroup name="list">
        <div v-for="(item, index) in analysisResultList" :key="index">
          <template v-if="index == 0">
            <el-row justify="space-between">
              <el-text size="small">{{ item._timestr }}</el-text>
              <el-tag type="danger" effect="dark">最新线索</el-tag>
            </el-row>
            <el-space>
              <el-icon color="#F56C6C" :size="40"
                ><WarnTriangleFilled
              /></el-icon>
              <el-text>
                {{ item.advice }}
              </el-text>
            </el-space>
            <el-row justify="space-between">
              <el-link type="primary" :underline="true" @click="showPolyline">
                {{ lineShow ? '收起路线' : '定位路线' }}
              </el-link>
              <el-text size="small">
                æŽ¨èè·¯çº¿æ€»é•¿{{ item.direction.distance }}ç±³
              </el-text>
            </el-row>
            <el-divider>历史线索</el-divider>
          </template>
          <template v-else>
            <el-row>
              <el-text size="small">{{ item._timestr }}</el-text>
            </el-row>
            <el-space>
              <!-- <el-icon color="#F56C6C" :size="40"><WarnTriangleFilled /></el-icon> -->
              <el-text>
                {{ item.advice }}
              </el-text>
            </el-space>
            <!-- <el-row justify="space-between">
      <el-button icon="Plus" @click="addAdvice"></el-button>
      <el-button icon="Minus" @click="removeAdvice"></el-button>
      <el-scrollbar height="200">
        <TransitionGroup name="list">
          <div v-for="(item, index) in analysisResultList" :key="index">
            <template v-if="index == 0">
              <el-row justify="space-between">
                <el-text size="small">{{ item._timestr }}</el-text>
                <el-tag type="danger" effect="dark">最新线索</el-tag>
              </el-row>
              <el-space>
                <el-icon color="#F56C6C" :size="40"
                  ><WarnTriangleFilled
                /></el-icon>
                <el-text>
                  {{ item.advice }}
                </el-text>
              </el-space>
              <el-row justify="space-between">
                <el-link type="primary" :underline="true" @click="showPolyline">
                  {{ lineShow ? '收起路线' : '定位路线' }}
                </el-link>
                <el-text size="small">
                  æŽ¨èè·¯çº¿æ€»é•¿{{ item.direction.distance }}ç±³
                </el-text>
              </el-row>
              <el-divider>历史线索</el-divider>
            </template>
            <template v-else>
              <el-row>
                <el-text size="small">{{ item._timestr }}</el-text>
              </el-row>
              <el-space>
                <!-- <el-icon color="#F56C6C" :size="40"><WarnTriangleFilled /></el-icon> -->
                <el-text>
                  {{ item.advice }}
                </el-text>
              </el-space>
              <!-- <el-row justify="space-between">
              <el-link type="primary" :underline="true" @click="showPolyline">
                {{ lineShow ? '收起路线' : '定位路线' }}
              </el-link>
@@ -69,10 +72,11 @@
                æŽ¨èè·¯çº¿æ€»é•¿{{ item.direction.distance }}ç±³
              </el-text>
            </el-row> -->
            <el-divider></el-divider>
          </template>
        </div>
      </TransitionGroup>
              <el-divider></el-divider>
            </template>
          </div>
        </TransitionGroup>
      </el-scrollbar>
    </template>
    <template #footer> </template>
  </CardDialog>
@@ -144,6 +148,14 @@
function timeFormatter(time) {
  return moment(time).format('YYYY-MM-DD HH:mm:ss');
}
function addAdvice() {
  analysisResultList.unshift(analysisResultList[0]);
}
function removeAdvice() {
  analysisResultList.splice(0, 1);
}
</script>
<style scoped>
:deep(.el-text) {
@@ -155,7 +167,7 @@
  /* color: #ffd82a; */
}
</style>
<style>
<!-- <style>
.list-move, /* å¯¹ç§»åŠ¨ä¸­çš„å…ƒç´ åº”ç”¨çš„è¿‡æ¸¡ */
.list-enter-active,
.list-leave-active {
@@ -165,7 +177,7 @@
.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
  transform: translateX(-30px);
}
/* ç¡®ä¿å°†ç¦»å¼€çš„元素从布局流中删除
@@ -173,4 +185,4 @@
.list-leave-active {
  position: absolute;
}
</style>
</style> -->
src/views/sourcetrace/component/ClueRecordItem.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
<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-col>
      <!-- <el-col :span="2"> </el-col> -->
    </div>
    <div v-else-if="item._type == '2'">
      <el-row justify="space-between">
        <el-tag type="danger" effect="dark" size="small">线索</el-tag>
        <el-link type="primary" @click="emits('open')"> è¯¦æƒ… </el-link>
      </el-row>
      <el-text type="danger">{{ item.advice }}</el-text>
    </div>
  </div>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
  item: Object
});
const emits = defineEmits(['open']);
const noWarn = computed(() => {
  return props.item && props.item.pollutedSource.sceneList.length == 0;
});
function formatException(value) {
  switch (value) {
    case 4:
      return '量级突变';
    case 9:
      return '快速上升';
    default:
      return '量级突变';
  }
}
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;
  }
}
</script>
<style scoped>
.wrapper {
  margin-bottom: 8px;
  border-bottom: 1px dashed rgba(253, 253, 253, 0.651);
}
.wrapper-select {
  background: linear-gradient(
    rgba(228, 228, 228, 0.274),
    rgba(64, 165, 248, 0.089)
  );
}
.no-warning {
  color: var(--el-text-color-disabled) !important;
}
</style>
src/views/sourcetrace/component/PollutedClueItem.vue
@@ -1,222 +1,135 @@
<template>
  <!-- <BaseCard>
    <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-link
        type="primary"
        :underline="true"
        @click="showMarksAndPolygon(item)"
      >
        {{ item.showMore ? '收起异常' : '定位异常' }}
      </el-link>
    </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>
    <div>
      <el-text type="primary">
        å¼‚常类型:{{ item.pollutedData.exception }}
      </el-text>
    </div>
    <el-row style="border-top: 1px solid white">
      <el-col :span="6">
        <el-statistic title="突变因子" :value="item.pollutedData.factorName" />
      </el-col>
      <el-col :span="6">
        <el-statistic
          v-if="item.pollutedData.exceptionType == 4"
          title="变化幅度"
          :value="formatPercentage(item.pollutedData.avgPer)"
        />
        <el-statistic
          v-else-if="item.pollutedData.exceptionType == 9"
          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="showMarksAndPolygon(item)"
                  >
                    {{
                      (item.showMore ? '收起溯源场景' : '查看溯源场景') +
                      '(' +
                      item.pollutedSource.sceneList.length +
                      ')'
                    }}
                  </el-link> -->
    </el-row>
    <!-- <div style="width: 320px; height: 80px"> -->
    <RealTimeLineChart
      v-for="(item1, index1) in item._chartOptions"
      :key="index1"
      :model-value="item1"
      chart-height="80px"
    ></RealTimeLineChart>
    <!-- </div> -->
    <div class="border-dashed">
      <el-text type="" tag="mark">
        {{ item.pollutedSource.conclusion }}
      </el-text>
    </div>
    <SceneTable
      :show-marks="item.showMore"
      :scene-list="item.pollutedSource.sceneList"
    ></SceneTable>
    <!-- <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-scrollbar>
  <!-- </template>
  </BaseCard> -->
  <CardDialog
    :model-value="modelValue"
    @update:model-value="handleDialogShow"
    title="污染线索"
    draggable
    :modal="false"
    width="400px"
  >
    <template #default>
      <template v-if="item">
        <el-row>
          <el-text type="primary" size="small">{{ item._timestr }}</el-text>
        </el-row>
        <el-space>
          <el-icon color="#F56C6C" :size="40"><WarnTriangleFilled /></el-icon>
          <el-text type="primary">
            {{ item.advice }}
          </el-text>
        </el-space>
        <el-row justify="space-between">
          <el-text type="primary" size="small">
            æŽ¨èè·¯çº¿æ€»é•¿{{ item.direction.distance }}ç±³
          </el-text>
          <el-link type="primary" size="small" @click="showPolyline">
            {{ lineShow ? '收起路线' : '定位路线' }}
          </el-link>
        </el-row>
        <SceneTable
          :show-marks="lineShow"
          :scene-list="formatSceneList(item.sortedSceneList)"
        >
          <el-table-column prop="_count" width="42" label="溯源次数" />
        </SceneTable>
      </template>
    </template>
    <template #footer> </template>
  </CardDialog>
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted, onUnmounted, reactive, watch } from 'vue';
import { sceneTypes, sceneIcon } from '@/constant/scene-types';
import mapLine from '@/utils/map/line';
import mapUtil from '@/utils/map/util';
import marks from '@/utils/map/marks';
const props = defineProps({
  modelValue: Boolean,
  item: Object
});
const emits = defineEmits(['showMarksAndPolygon']);
const lineShow = ref(true);
// ä¸Šä¸€æ¡å¯¼èˆªè·¯çº¿
let lastPolyline = undefined;
// ä¸Šä¸€ä¸ªå¯¼èˆªç›®çš„地
// let lastDestination = undefined;
// let layer = undefined;
function showMarksAndPolygon(item) {
  emits('showMarksAndPolygon', item);
}
const emits = defineEmits(['update:modelValue']);
function formatPercentage(value) {
  return Math.round(value * 100) + '%';
}
watch(
  () => [props.item, props.modelValue],
  (nV, oV) => {
    if (nV[0] != oV[0]) {
      const polyline = mapLine.drawDirection(
        nV[0].direction.paths.map((v) => [v.first, v.second])
      );
      // polylineList.unshift(polyline);
      if (lastPolyline) {
        mapUtil.removeViews(lastPolyline);
      }
      lastPolyline = polyline;
      lineShow.value = true;
    } else if (nV[1]) {
      showPolyline(true);
    }
  }
);
function formatDistanceType(value) {
  switch (value) {
    case 'TYPE1':
      return '50ç±³';
    case 'TYPE2':
      return '50ç±³ - 500ç±³';
    case 'TYPE3':
      return '50ç±³ - 1公里';
    case 'TYPE4':
      return '50ç±³ - 2公里';
function showPolyline(show) {
  if (typeof show === 'boolean') {
    lineShow.value = show;
  } else {
    lineShow.value = !lineShow.value;
  }
    default:
      break;
  if (lineShow.value && lastPolyline) {
    mapUtil.addViews(lastPolyline);
  } else {
    mapUtil.removeViews(lastPolyline);
  }
}
function formatChangeRate() {
// function removeLayer() {
//   if (layer != undefined) {
//     mapUtil.removeViews(layer);
//     layer = undefined;
//   }
// }
// function drawMarks(sceneList) {
//   removeLayer();
//   if (sceneList.length != 0) {
//     const icons = [];
//     sceneList.forEach((s) => {
//       icons.push(sceneIcon(s.typeId));
//     });
//     layer = marks.createLabelMarks(icons, sceneList, false);
//   }
// }
function formatSceneList(sceneList) {
  return sceneList.map((v) => {
    return {
      ...v.first,
      _count: v.second
    };
  });
}
function handleDialogShow(value) {
  showPolyline(value);
  emits('update:modelValue', value);
}
</script>
<style scoped>
:deep(.el-statistic) {
  --el-statistic-title-color: rgb(215, 215, 215);
  --el-statistic-content-color: white;
}
:deep(.el-text.el-text--primary) {
/* :deep(.el-text.el-text--primary) {
  --el-text-color: white;
}
:deep(.el-link) {
  --el-link-text-color: #23dad1;
}
.scrollbar {
  min-width: 300px;
  max-width: 60vw;
  /* height: 35vh; */
  /* color: #02ffea; */
}
.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;
}
} */
</style>
src/views/sourcetrace/component/PollutedExceptionItem.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,237 @@
<template>
  <!-- <CardDialog
    :model-value="modelValue"
    @update:model-value="handleDialogShow"
    title="污染异常"
    draggable
    :modal="false"
  >
    <template #default> -->
  <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-link
            type="primary"
            :underline="true"
            @click="showMarksAndPolygon(item)"
          >
            {{ item.showMore ? '收起异常' : '定位异常' }}
          </el-link>
        </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>
        <div>
          <el-text type="primary">
            å¼‚常类型:{{ item.pollutedData.exception }}
          </el-text>
        </div>
        <el-row style="border-top: 1px solid white">
          <el-col :span="6">
            <el-statistic
              title="突变因子"
              :value="item.pollutedData.factorName"
            />
          </el-col>
          <el-col :span="6">
            <el-statistic
              v-if="item.pollutedData.exceptionType == 4"
              title="变化幅度"
              :value="formatPercentage(item.pollutedData.avgPer)"
            />
            <el-statistic
              v-else-if="item.pollutedData.exceptionType == 9"
              title="变化速率"
              :value="formatChangeRate(item.pollutedData.avgRate)"
              suffix=""
            />
          </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="showMarksAndPolygon(item)"
                  >
                    {{
                      (item.showMore ? '收起溯源场景' : '查看溯源场景') +
                      '(' +
                      item.pollutedSource.sceneList.length +
                      ')'
                    }}
                  </el-link> -->
        </el-row>
        <!-- <div style="width: 320px; height: 80px"> -->
        <RealTimeLineChart
          v-for="(item1, index1) in item._chartOptions"
          :key="index1"
          :model-value="item1"
          chart-height="80px"
        ></RealTimeLineChart>
        <!-- </div> -->
        <div class="border-dashed">
          <el-text type="" tag="mark">
            {{ item.pollutedSource.conclusion }}
          </el-text>
        </div>
        <SceneTable
          :show-marks="item.showMore"
          :scene-list="item.pollutedSource.sceneList"
        ></SceneTable>
        <!-- <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-scrollbar>
    </template>
  </BaseCard>
  <!-- </template>
  </CardDialog> -->
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
  modelValue: Boolean,
  item: Object
});
const emits = defineEmits(['showMarksAndPolygon', 'update:modelValue']);
function showMarksAndPolygon(item) {
  emits('showMarksAndPolygon', item);
}
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 formatChangeRate(value) {
  return Math.round(value * 100) / 100 + '';
}
</script>
<style scoped>
:deep(.el-statistic) {
  --el-statistic-title-color: rgb(215, 215, 215);
  --el-statistic-content-color: white;
}
:deep(.el-text.el-text--primary) {
  --el-text-color: white;
}
:deep(.el-link) {
  --el-link-text-color: #23dad1;
}
.scrollbar {
  min-width: 300px;
  max-width: 60vw;
  /* height: 35vh; */
  /* color: #02ffea; */
}
.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;
}
</style>