riku
2025-06-05 16eb4bd55a4fd61ddd7a171b1a07378c45d1665b
动态溯源(待完成)
已修改10个文件
276 ■■■■ 文件已修改
src/api/index.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/device/DeviceManage.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/scene/SceneTable.vue 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/styles/elementUI.scss 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/factor/data.js 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/map/line.js 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/realtimemode/RealtimeMode.vue 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sourcetrace/SourceTrace.vue 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sourcetrace/UnderwayAdvice.vue 83 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/index.js
@@ -13,10 +13,10 @@
}
if (debug) {
  // ip1 = 'http://192.168.0.110:8084/';
  ip1 = 'http://localhost:8084/';
  // ws = `192.168.0.110:9031`;
  ws = `localhost:9031`;
  ip1 = 'http://192.168.0.110:8084/';
  // ip1 = 'http://localhost:8084/';
  ws = `192.168.0.110:9031`;
  // ws = `localhost:9031`;
}
const $http = axios.create({
src/components.d.ts
@@ -31,6 +31,7 @@
    ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
    ElDialog: typeof import('element-plus/es')['ElDialog']
    ElDivider: typeof import('element-plus/es')['ElDivider']
    ElDropdown: typeof import('element-plus/es')['ElDropdown']
    ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
    ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
@@ -48,6 +49,7 @@
    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
    ElSelect: typeof import('element-plus/es')['ElSelect']
    ElSlider: typeof import('element-plus/es')['ElSlider']
    ElSpace: typeof import('element-plus/es')['ElSpace']
    ElStatistic: typeof import('element-plus/es')['ElStatistic']
    ElSwitch: typeof import('element-plus/es')['ElSwitch']
    ElTable: typeof import('element-plus/es')['ElTable']
src/components/device/DeviceManage.vue
@@ -7,11 +7,7 @@
  >
    设备管理
  </el-button> -->
  <CardDialog
    :model-value="modelValue"
    @changed="handleChange"
    title="走航设备管理"
  >
  <CardDialog v-bind="$attrs" title="走航设备管理">
    <el-row class="device-table">
      <el-col :span="20">
        <el-table
@@ -88,9 +84,9 @@
    return { loading, fetchData };
  },
  props: {
    modelValue: Boolean
    // modelValue: Boolean
  },
  emits: ['update:modelValue'],
  // emits: ['update:modelValue'],
  data() {
    return {
      dialogVisible: false,
@@ -105,9 +101,9 @@
    }
  },
  methods: {
    handleChange(value) {
      this.$emit('update:modelValue', value);
    },
    // handleChange(value) {
    //   this.$emit('update:modelValue', value);
    // },
    deleteDevice(row) {
      this.onConfirm = () => {
        this.deviceStore.deleteDevice(row.deviceCode);
src/components/scene/SceneTable.vue
@@ -49,7 +49,7 @@
  </el-table>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, onUnmounted } from 'vue';
import { sceneTypes, sceneIcon } from '@/constant/scene-types';
import MapUtil from '@/utils/map/util';
import marks from '@/utils/map/marks';
@@ -86,14 +86,15 @@
  }
});
onUnmounted(() => {
  removeLayer();
});
watch(showSceneList, (nV, oV) => {
  if (nV && props.showMarks) {
    drawMarks(nV);
  } else {
    if (layer != undefined) {
      MapUtil.removeViews(layer);
      layer = undefined;
    }
    removeLayer();
  }
});
@@ -103,20 +104,21 @@
    if (showSceneList.value && nV) {
      drawMarks(showSceneList.value);
    } else {
      if (layer != undefined) {
        MapUtil.removeViews(layer);
        layer = undefined;
      }
      removeLayer();
    }
  },
  { immediate: true }
);
function drawMarks(sceneList) {
function removeLayer() {
  if (layer != undefined) {
    MapUtil.removeViews(layer);
    layer = undefined;
  }
}
function drawMarks(sceneList) {
  removeLayer();
  if (sceneList.length != 0) {
    const icons = [];
    sceneList.forEach((s) => {
src/styles/elementUI.scss
@@ -32,3 +32,16 @@
.el-button-custom:focus-visible {
  outline: 0px solid var(--el-button-outline-color);
}
// .el-statistic {
//   --el-statistic-title-color: rgb(215, 215, 215);
//   --el-statistic-content-color: white;
// }
// .el-text {
//   --el-text-color: white;
// }
// .el-link {
//   --el-link-text-color: #23dad1;
// }
src/utils/factor/data.js
@@ -182,6 +182,7 @@
var fetchingTask;
var isFetching;
var pause = false;
/**
 * 开启实时数据循环获取
 * @param {Object} params
@@ -200,12 +201,12 @@
    interval = 10 * 1000;
  }
  if (_interval) {
    interval = _interval
    interval = _interval;
  }
  // 开始循环任务
  clearFetchingTask();
  fetchingTask = setInterval(() => {
    if (isFetching) {
    if (isFetching || pause) {
      return;
    }
    isFetching = true;
@@ -237,6 +238,11 @@
  }, interval);
}
function pauseTask() {
  pause = !pause;
  return pause;
}
/**
 * 清理历史实时数据获取任务
 */
@@ -248,4 +254,9 @@
  }
}
export { fetchHistoryData, startLoopFetchRealTimeData, clearFetchingTask };
export {
  fetchHistoryData,
  startLoopFetchRealTimeData,
  clearFetchingTask,
  pauseTask
};
src/utils/map/line.js
@@ -22,7 +22,8 @@
function drawDirection(path) {
  const polyline = newPolyline(path, '#02ffea');
  map.add(polyline)
  map.add(polyline);
  return polyline;
}
export default {
src/views/realtimemode/RealtimeMode.vue
@@ -5,9 +5,9 @@
      <el-button
        type="primary"
        class="p-events-auto el-button-custom"
        @click="clearFetchingTask"
        @click="pauseTask"
      >
        停止
        {{ pause ? '继续' : '暂停' }}
      </el-button>
    </el-row>
    <el-row class="m-t-2">
@@ -49,7 +49,8 @@
import {
  fetchHistoryData,
  startLoopFetchRealTimeData,
  clearFetchingTask
  clearFetchingTask,
  pauseTask
} from '@/utils/factor/data';
import thirdPartyDataApi from '@/api/thirdPartyDataApi';
import websocket from '@/api/websocket';
@@ -61,7 +62,13 @@
// const mode = 'product';
export default {
  components: { DashBoard, RealTimeTrend, DeviceChange, SourceTrace, UnderwayAdvice },
  components: {
    DashBoard,
    RealTimeTrend,
    DeviceChange,
    SourceTrace,
    UnderwayAdvice
  },
  setup() {
    const { loading, fetchData } = useFetchData(10000);
    return { loading, fetchData };
@@ -76,7 +83,8 @@
      // 新获取的监测数据
      factorDatas: new FactorDatas(),
      // 全部监测数据
      allFactorDatas: new FactorDatas()
      allFactorDatas: new FactorDatas(),
      pause: false
    };
  },
  watch: {
@@ -127,10 +135,12 @@
          mode == 'debug'
            ? {
                deviceCode: this.deviceCode,
                startTime: '2025-01-16 11:34:00',
                endTime: '2025-01-16 11:35:00',
                // startTime: '2025-01-16 11:34:00',
                // endTime: '2025-01-16 11:35:00',
                startTime: '2024-11-27 11:50:41',
                endTime: '2025-01-16 11:51:41',
                page,
                perPage: 100
                perPage: 10
              }
            : {
                deviceCode: this.deviceCode,
@@ -145,6 +155,9 @@
        });
      });
    },
    pauseTask() {
      this.pause = pauseTask();
    },
    clearFetchingTask() {
      clearFetchingTask();
    },
src/views/sourcetrace/SourceTrace.vue
@@ -18,14 +18,17 @@
          <!-- <div>
            <el-text type="primary" size="large" tag="b">动态溯源</el-text>
          </div> -->
          <el-scrollbar ref="scrollbarRef" :height="height" class="scrollbar">
            <el-row ref="scrollContentRef">
          <el-scrollbar ref="scrollbarRef" class="scrollbar">
              <div
              ref="scrollContentRef"
              style="display: flex; width: fit-content"
            >
              <el-scrollbar
                v-for="(item, index) in streams"
                :key="index"
                class="clue-card"
              >
                <el-row>
                <el-row justify="space-between">
                  <!-- <el-tag v-if="index == 0" type="danger">最新</el-tag> -->
                  <el-text type="primary">{{
                    '线索时间:' +
@@ -33,6 +36,13 @@
                    ' - ' +
                    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">
@@ -46,7 +56,7 @@
                    }}
                  </el-text>
                </div>
                <el-row>
                <el-row style="border-top: 1px solid white">
                  <el-col :span="6">
                    <el-statistic
                      title="突变因子"
@@ -86,13 +96,6 @@
                      ')'
                    }}
                  </el-link> -->
                  <el-link
                    type="primary"
                    underline
                    @click="showMarksAndPolygon(item)"
                  >
                    {{ item.showMore ? '收起异常' : '定位异常' }}
                  </el-link>
                </el-row>
                <div style="width: 320px; height: 140px">
                  <RealTimeLineChart
@@ -100,6 +103,11 @@
                    :key="index1"
                    :model-value="item1"
                  ></RealTimeLineChart>
                </div>
                <div class="border-dashed">
                  <el-text type="" tag="mark">
                    {{ item.pollutedSource.conclusion }}
                  </el-text>
                </div>
                <SceneTable
                  :show-marks="item.showMore"
@@ -156,8 +164,8 @@
                  :scene-list="item.relatedSceneList"
                ></SceneTable>
                <el-divider /> -->
              </el-scrollbar>
              </div>
            </el-row>
          </el-scrollbar>
        </template>
      </BaseCard>
@@ -178,9 +186,7 @@
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 websocketMsgParser from '@/views/sourcetrace/websocketMsgParser.js';
const props = defineProps({
  factorType: String
@@ -188,7 +194,7 @@
const emits = defineEmits(['update:factorType']);
const height = `48vh`;
const height = `30vh`;
const width = `60vh`;
const show = ref(false);
@@ -217,7 +223,7 @@
let showFirstClueTask;
function dealMsg(data) {
  const {type, content} = websocketMsgParser.parseMsg(data)
  const { type, content } = websocketMsgParser.parseMsg(data);
  // 污染线索 PollutedClue
  if (type == '1') {
@@ -229,12 +235,13 @@
    if (streams.length == 0) {
      streams.push(obj);
    } else {
      streams.forEach((s) => {
        s.showMore = false;
      });
      // streams.forEach((s) => {
      //   showMarksAndPolygon(s);
      // });
      hideAll();
      streams.unshift(obj);
      show.value = true;
    }
    show.value = true;
    // scrollToBottom();
    scrollToTop();
@@ -263,9 +270,20 @@
  // }
});
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)) {
@@ -296,11 +314,11 @@
    });
    polygonMap.set(item.guid, pollutedAreaPolygon);
  }
  map.setFitView(polygonMap.get(item.guid));
  // map.setFitView(polygonMap.get(item.guid));
}
function parseChartData(obj) {
  console.log('折线图:start');
  // console.log('折线图:start');
  const factorDatas = new FactorDatas();
  factorDatas.setData(obj.pollutedData.dataVoList, 0, () => {
    obj._chartOptions = factorDataParser.parseData(factorDatas, [
@@ -310,7 +328,7 @@
        value: obj.pollutedData.factorId + ''
      }
    ]);
    console.log('折线图:', obj._chartOptions);
    // console.log('折线图:', obj._chartOptions);
  });
}
@@ -435,7 +453,7 @@
  --el-statistic-content-color: white;
}
:deep(.el-text) {
:deep(.el-text.el-text--primary) {
  --el-text-color: white;
}
@@ -446,14 +464,23 @@
.scrollbar {
  min-width: 300px;
  max-width: 60vw;
  /* height: 35vh; */
  /* color: #02ffea; */
}
.clue-card {
  padding: 0 4px;
  margin-right: 2px;
  width: 320px;
  border: 1px solid white;
  /* 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/UnderwayAdvice.vue
@@ -1,29 +1,57 @@
<template>
  <CardDialog
    v-model="dialogVisible"
    title="走航路线推荐"
    title="走航智能分析"
    draggable
    :modal="false"
    width="400px"
  >
    <template #default> </template>
    <template #default>
      <template v-if="latestResult">
        <el-row>
          <el-text size="small">{{ latestResult._timestr }}</el-text>
        </el-row>
        <el-space>
          <el-icon color="#F56C6C" :size="40"><WarnTriangleFilled /></el-icon>
          <el-text>
            {{ latestResult.advice }}
          </el-text>
        </el-space>
        <el-row justify="end">
          <el-link type="primary" :underline="true" @click="showPolyline">
            {{ lineShow ? '收起路线' : '定位路线' }}
          </el-link>
          <el-text size="small">
            推荐路线总长{{ latestResult.direction.distance }}米
          </el-text>
        </el-row>
      </template>
    </template>
    <template #footer> </template>
  </CardDialog>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import moment from 'moment';
import { ref, onMounted, onUnmounted, reactive } from 'vue';
import websocket from '@/api/websocket';
import websocketMsgParser from '@/views/sourcetrace/websocketMsgParser.js';
import mapLine from '@/utils/map/line';
import mapUtil from '@/utils/map/util';
const dialogVisible = ref(true);
const dialogVisible = ref(false);
const lineShow = ref(true);
const latestResult = ref();
let latestPolyline = undefined;
const analysisResultList = reactive([]);
const polylineList = [];
onMounted(() => {
  websocket.registerReceiveEvent(dealMsg);
});
onUnmounted(() => {
  websocket.removeReceiveEvent(dealMsg);
  showPolyline(false);
});
function dealMsg(data) {
@@ -32,13 +60,50 @@
  if (type == '2') {
    const obj = JSON.parse(content);
    console.log('污染分析结果: ', obj);
    obj._timestr = timeFormatter(obj.time);
    analysisResultList.unshift(obj);
    latestResult.value = obj;
    obj.sortedSceneList;
    obj.time;
    obj.advice;
    obj.direction;
    // obj.sortedSceneList;
    // obj.time;
    // obj.advice;
    // obj.direction;
    mapLine.drawDirection(obj.direction.paths.map((v) => [v.first, v.second]));
    const polyline = mapLine.drawDirection(
      obj.direction.paths.map((v) => [v.first, v.second])
    );
    polylineList.unshift(polyline);
    if (latestPolyline) {
      mapUtil.removeViews(latestPolyline);
  }
    latestPolyline = polyline;
    dialogVisible.value = true;
  }
}
function showPolyline(show) {
  if (typeof show === 'boolean') {
    lineShow.value = show;
  } else {
    lineShow.value = !lineShow.value;
  }
  if (lineShow.value) {
    mapUtil.addViews(latestPolyline);
  } else {
    mapUtil.removeViews(latestPolyline);
  }
}
function timeFormatter(time) {
  return moment(time).format('YYYY-MM-DD HH:mm:ss');
}
</script>
<style scoped>
:deep(.el-text) {
  --el-text-color: white;
}
:deep(.el-link) {
  --el-link-text-color: #23dad1;
}
</style>