Bladeren bron

fix: 测流成果回放

hum 1 jaar geleden
bovenliggende
commit
71307ea0a6

+ 10 - 0
ruoyi-system/src/main/java/com/ruoyi/system/dto/TaskResultSortDTO.java

@@ -21,6 +21,16 @@ public class TaskResultSortDTO implements Serializable {
     @ExcelProperty(value = "成果ID",index = 0)
     private Long resultId;
 
+    /**
+     * 站点ID
+     */
+    private Long siteId;
+
+    /**
+     * 任务编号
+     */
+    private String taskid;
+
     /**
      * 创建时间
      */

+ 1 - 1
ruoyi-system/src/main/resources/mapper/TaskResultMapper.xml

@@ -66,7 +66,7 @@
     <!--查询单个-->
     <select id="queryByTimeQuery" resultMap="TaskResultSortDTOMap">
         select
-            result_id, flowsum, create_time, waterlevel
+            result_id, site_id, taskid, flowsum, create_time, waterlevel
         from task_result
         where site_id = #{scattperPlotQuery.siteId} and waterlevel is not null
         <if test="scattperPlotQuery.startTime != null and scattperPlotQuery.endTime != null">

+ 4 - 4
ruoyi-ui/src/api/analysis/achievement.js

@@ -39,19 +39,19 @@ export function listMeasureLine(data) {
   })
 }
 
-export function getCarInfo(siteId) {
+export function getCarInfo(params) {
   return request({
     url: '/reportData/carInfo',
     method: 'get',
-    params: { siteId },
+    params,
   })
 }
 
-export function getCarLocation(siteId) {
+export function getCarLocation(params) {
   return request({
     url: '/reportData/carLocation',
     method: 'get',
-    params: { siteId },
+    params,
   })
 }
 

+ 2 - 2
ruoyi-ui/src/views/analysis/task/realtime/realtime.vue

@@ -76,7 +76,7 @@ export default {
   },
   methods: {
     loadCarInfo() {
-      getCarInfo(this.siteId).then((res) => {
+      getCarInfo({ siteId: this.siteId }).then((res) => {
         this.carInfo = res.data?.[0] || {};
       })
     },
@@ -95,7 +95,7 @@ export default {
       })
     },
     loadTaskNotice() {
-      getTaskNotice(this.siteId, this.id).then((res) => {
+      getTaskNotice(this.siteId, this.task.taskid).then((res) => {
         this.messages = res.data.records || [];
       })
     },

+ 3 - 3
ruoyi-ui/src/views/analysis/task/realtime/simulation.vue

@@ -19,7 +19,7 @@
       </div>
       <div class="realtime-foot-time">
         <div class="realtime-foot-time-label">施测时间:</div>
-        <div class="realtime-foot-time-value">2024-4-2 12:50:32</div>
+        <div class="realtime-foot-time-value">{{ task.createTime }}</div>
       </div>
     </div>
   </div>
@@ -90,12 +90,12 @@ export default {
       this.$modal.confirm('是否确认中止当前任务?').then(() => {
         return taskAction(this.siteId, 0);
       }).then(() => {
-        this.$emit('refresh');
+        this.$router.back();
         this.$modal.msgSuccess("中止成功");
       }).catch(() => {});
     },
     loadCarLocation() {
-      getCarLocation(this.siteId).then((res) => {
+      getCarLocation({ siteId: this.siteId }).then((res) => {
         this.location = res.data?.[0].position || 0;
         this.setOptions();
       })

+ 80 - 0
ruoyi-ui/src/views/analysis/task/result/car.vue

@@ -0,0 +1,80 @@
+<template>
+  <div class="realtime-container">
+    <div class="title">小车状态</div>
+    <div class="info">
+      <div class="item">
+        <div class="item-icon"></div>
+        <div class="item-label">电压:</div>
+        <div class="item-value">{{carInfo.voltage || '-'}}V</div>
+      </div>
+      <div class="item">
+        <div class="item-icon"></div>
+        <div class="item-label">电流:</div>
+        <div class="item-value">{{carInfo.current || '-'}}A</div>
+      </div>
+      <div class="item">
+        <div class="item-icon"></div>
+        <div class="item-label">电量:</div>
+        <div class="item-value">{{carInfo.eq || '-'}}%</div>
+      </div>
+      <div class="item">
+        <div class="item-icon"></div>
+        <div class="item-label">信号强度:</div>
+        <div class="item-value">{{carInfo.signalstrength || '-'}}</div>
+      </div>
+      <div class="item">
+        <div class="item-icon"></div>
+        <div class="item-label">温度:</div>
+        <div class="item-value">{{carInfo.temperature || '-'}}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    carInfo: Object,
+  },
+}
+</script>
+
+<style scoped>
+.realtime-container {
+  background: #fff;
+  height: 190px;
+  border-radius: 4px;
+  padding: 10px 20px;
+}
+.title {
+  font-size: 18px;
+  line-height: 26px;
+  font-weight: 600;
+  color: #1D2738;
+}
+.info {
+  padding-top: 5px;
+  font-size: 14px;
+  line-height: 18px;
+  color: #54606C;
+}
+.item {
+  display: flex;
+  align-items: center;
+  padding: 5px 0;
+}
+.item-icon {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: #55DA74;
+  margin-right: 8px;
+}
+.item-label {
+  width: 70px;
+  color: #54606C;
+}
+.item-value {
+  color: #1D2738;
+}
+</style>

+ 115 - 0
ruoyi-ui/src/views/analysis/task/result/flow.vue

@@ -0,0 +1,115 @@
+<template>
+  <div class="realtime-container">
+    <div class="title">垂线点对应的流量图</div>
+    <div ref="chart" class="chart" :style="{height: '140px', width: '100%'}" />
+  </div>
+</template>
+
+<script>
+import * as echarts from "echarts";
+import resize from '@/utils/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    positions: Array,
+    measureLine: Array,
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.chart = echarts.init(this.$refs.chart, 'macarons');
+    })
+  },
+  beforeDestroy() {
+    if (this.chart) {
+      this.chart.dispose()
+      this.chart = null
+    }
+  },
+  methods: {
+    setOptions() {
+      if (!this.chart || !this.positions) {
+        return;
+      }
+      const xAxisData = this.positions.map(({x}) => x);
+      const seriesData = this.positions.map((_,index) => this.measureLine.find(({pn}) => pn - 1 === index)?.wspeed || 0);
+      const options = {
+        xAxis: {
+          name: '停泊点',
+          data: xAxisData,
+          type: 'category',
+          axisLabel: {
+            color: '#54606C'
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#E4E4E4'
+            }
+          }
+        },
+        yAxis: [
+          {
+            name: '流量(m³/h)',
+            axisLine: {
+              show: false
+            },
+            axisLabel: {
+              color: '#54606C'
+            },
+            nameTextStyle: {
+              color: '#8D99A4'
+            }
+          },
+        ],
+        grid: {
+          left: 20,
+          right: 0,
+          bottom: 0,
+          top: 30,
+          containLabel: true
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'line'
+          },
+          formatter: function(prams) {
+            return `停泊点:${prams[0].name}<br/>流量:${prams[0].data}m³/h`;
+          },
+          padding: [5, 10]
+        },
+        series: [{
+          name: '流量',
+          type: 'bar',
+          barWidth: 14,
+          data: seriesData,
+          itemStyle: {
+            color: '#7FC8E5'
+          }
+        }]
+      };
+      this.chart.setOption(options);
+    },
+  },
+  watch: {
+    measureLine() {
+      this.setOptions()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.realtime-container {
+  background: #fff;
+  height: 190px;
+  border-radius: 4px;
+  padding: 10px 20px;
+}
+.title {
+  font-size: 18px;
+  line-height: 26px;
+  font-weight: 600;
+  color: #1D2738;
+}
+</style>

+ 44 - 0
ruoyi-ui/src/views/analysis/task/result/message.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="realtime-container">
+    <div class="title">即时信息</div>
+    <div class="info">
+      <div class="item" v-for="item in messages">
+        <div class="item-value">{{ item.remark }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    messages: Array,
+  }
+}
+</script>
+
+<style scoped>
+.realtime-container {
+  background: #fff;
+  height: 190px;
+  border-radius: 4px;
+  padding: 10px 20px;
+}
+.title {
+  font-size: 18px;
+  line-height: 26px;
+  font-weight: 600;
+  color: #1D2738;
+}
+.info {
+  padding-top: 5px;
+  font-size: 14px;
+  line-height: 18px;
+  height: 140px;
+  overflow: auto;
+}
+.item {
+  padding: 5px 0;
+  color: #54606C;
+}
+</style>

+ 124 - 0
ruoyi-ui/src/views/analysis/task/result/movie.vue

@@ -0,0 +1,124 @@
+<template>
+  <div class="realtime-container">
+    <div class="title">
+      <span>实时视频</span>
+      <svg-icon icon-class="realtime-close" class-name="icon-close"/>
+    </div>
+    <div class="video"></div>
+    <div class="actions">
+      <div class="action-plus">
+        <svg-icon icon-class="realtime-plus" class-name="icon-plus"/>
+      </div>
+      <div class="action-circle">
+        <svg-icon icon-class="realtime-circle" class-name="icon-circle"/>
+        <div class="action-up">
+          <svg-icon icon-class="realtime-up" class-name="icon-up"/>
+        </div>
+        <div class="action-up">
+          <svg-icon icon-class="realtime-down" class-name="icon-down"/>
+        </div>
+        <div class="action-up">
+          <svg-icon icon-class="realtime-left" class-name="icon-left"/>
+        </div>
+        <div class="action-up">
+          <svg-icon icon-class="realtime-right" class-name="icon-right"/>
+        </div>
+      </div>
+      <div class="action-minus">
+        <svg-icon icon-class="realtime-minus" class-name="icon-minus"/>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+}
+</script>
+
+<style scoped>
+.realtime-container {
+  background: #fff;
+  height: 300px;
+  border-radius: 4px;
+  padding: 10px 20px;
+}
+.title {
+  font-size: 18px;
+  line-height: 26px;
+  font-weight: 600;
+  color: #1D2738;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.icon-close {
+  width: 36px;
+  height: 36px;
+  cursor: pointer;
+}
+.video {
+  margin-top: 10px;
+  height: 168px;
+  border-radius: 4px;
+  background: #000000;
+}
+.actions {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-top: 10px;
+}
+.icon-minus,
+.icon-plus {
+  width: 36px;
+  height: 36px;
+  cursor: pointer;
+}
+.action-circle {
+  margin: 0 20px;
+  width: 60px;
+  height: 60px;
+  position: relative;
+}
+.icon-circle {
+  width: 60px;
+  height: 60px;
+}
+.icon-up {
+  position: absolute;
+  top: 8px;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 14px;
+  height: 10px;
+  cursor: pointer;
+}
+.icon-down {
+  position: absolute;
+  bottom: 8px;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 14px;
+  height: 10px;
+  cursor: pointer;
+}
+.icon-left {
+  position: absolute;
+  top: 50%;
+  left: 8px;
+  transform: translateY(-50%);
+  width: 10px;
+  height: 14px;
+  cursor: pointer;
+}
+.icon-right {
+  position: absolute;
+  top: 50%;
+  right: 8px;
+  transform: translateY(-50%);
+  width: 10px;
+  height: 14px;
+  cursor: pointer;
+}
+</style>

+ 28 - 0
ruoyi-ui/src/views/analysis/task/result/report.vue

@@ -0,0 +1,28 @@
+<template>
+  <div class="realtime-container">
+    <el-table border :data="measureLine">
+      <el-table-column prop="pn" label="垂线编号" />
+      <el-table-column prop="pointX" label="起点距" />
+      <el-table-column prop="createTime" label="开始时间" />
+<!--      <el-table-column prop="area" label="过水面积" />-->
+      <el-table-column prop="waterlevel" label="水位" />
+      <el-table-column prop="wspeed" label="流速" />
+    </el-table>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    measureLine: Array,
+  },
+}
+</script>
+
+<style scoped>
+.realtime-container {
+  background: #fff;
+  border-radius: 4px;
+  padding: 20px;
+}
+</style>

+ 232 - 2
ruoyi-ui/src/views/analysis/task/result/result.vue

@@ -1,10 +1,240 @@
 <template>
-  <div>111</div>
+  <div class="hum-page-container">
+    <el-row :gutter="10">
+      <el-col :span="18">
+        <Simulation
+          ref="simulation"
+          :siteId="siteId"
+          :positions="positions"
+          :measureLine="measureLine"
+          :siteRealTime="siteRealTime"
+          :task="task"
+          :waterlevel="waterlevel"
+          :location="location"
+          :isplay="isplay"
+          @play="play"
+          @stop="stop"
+          @refresh="$emit('refresh')" />
+        <el-row :gutter="20" style="margin-top: 10px">
+          <el-col :span="8">
+            <Site :siteRealTime="siteRealTime" />
+          </el-col>
+          <el-col :span="8">
+            <Car :carInfo="carInfo" />
+          </el-col>
+          <el-col :span="8">
+            <Message :messages="messages" />
+          </el-col>
+        </el-row>
+      </el-col>
+      <el-col :span="6">
+        <Movie />
+        <Flow ref="flow" :positions="positions" :measureLine="measureLine" style="margin-top: 10px" />
+        <Water ref="water" :waterlevelList="waterlevelList" style="margin-top: 10px" />
+      </el-col>
+    </el-row>
+    <Report :measureLine="measureLine" style="margin-top: 10px" />
+  </div>
 </template>
 
 <script>
+import Simulation from './simulation'
+import Site from './site'
+import Car from './car'
+import Message from './message'
+import Movie from './movie'
+import Flow from './flow'
+import Water from './water'
+import Report from './report'
+import {
+  getCarInfo,
+  getSiteRealTime,
+  getTaskNotice,
+  listMeasureLine,
+  listWaterLevel,
+  getCarLocation,
+} from "@/api/analysis/achievement";
+
 export default {
-  name: "result"
+  components: {
+    Simulation,
+    Site,
+    Car,
+    Message,
+    Movie,
+    Flow,
+    Water,
+    Report,
+  },
+  props: {
+    task: Object,
+  },
+  data() {
+    return {
+      siteId: this.$route.params.siteId,
+      positions: [],
+      siteRealTime: {},
+      carInfo: {},
+      initcarInfo: {},
+      carInfoHistory: [],
+      messages: [],
+      messagesHistory: [],
+      measureLine: [],
+      measureLineHistory: [],
+      location: Number.MIN_VALUE,
+      initlocation: Number.MIN_VALUE,
+      locationHistory: [],
+      waterlevel: 0,
+      initwaterlevel: 0,
+      waterlevelList: [],
+      waterLevelHistory: [],
+      isplay: false,
+    }
+  },
+  methods: {
+    loadCarInfo() {
+      getCarInfo({
+        siteId: this.siteId,
+        startTime: this.task.createTime,
+        endTime: this.task.endtime,
+      }).then((res) => {
+        const history = res.data || [];
+        if (history.length > 0) {
+          this.carInfoHistory = history;
+          this.carInfo = history[history.length - 1];
+          this.initcarInfo = history[history.length - 1];
+        }
+      })
+    },
+    loadMeasureLine() {
+      const params = {
+        siteId: this.siteId,
+        taskId: this.task.taskid,
+      }
+      listMeasureLine(params).then((res) => {
+        const measureLine = res.data || [];
+        this.measureLine = measureLine;
+        this.measureLineHistory = measureLine;
+        this.positions = measureLine.map(({ pointX }) => ({ x: pointX }));
+      })
+    },
+    loadTaskNotice() {
+      getTaskNotice(this.siteId, this.task.taskid).then((res) => {
+        const messagesHistory = res.data.records || [];
+        this.messages = messagesHistory;
+        this.messagesHistory = messagesHistory;
+      })
+    },
+    loadSiteRealTime() {
+      getSiteRealTime(this.siteId).then((res) => {
+        this.siteRealTime = res.data || {};
+      })
+    },
+    loadWaterLevel() {
+      const params = {
+        startTime: this.task.starttime,
+        endTime: this.task.endtime,
+        siteId: this.siteId,
+        type: 1,
+        page: 1,
+        size: 10000
+      }
+      listWaterLevel(params).then((res) => {
+        const waterLevelHistory = res.data?.records || [];
+        if (waterLevelHistory.length > 0) {
+          this.waterlevel = waterLevelHistory[waterLevelHistory.length - 1].avgWaterlevel;
+          this.initwaterlevel = this.waterlevel;
+          this.waterlevelList = waterLevelHistory;
+          this.waterLevelHistory = waterLevelHistory;
+        }
+      })
+    },
+    loadCarLocation() {
+      getCarLocation({ siteId: this.siteId, startTime: this.task.starttime, endTime: this.task.endtime }).then((res) => {
+        const locationHistory = res.data || [];
+        if (locationHistory.length > 0) {
+          this.locationHistory = locationHistory;
+        }
+      })
+    },
+    play() {
+      this.isplay = true;
+      const dis = Date.now() - new Date(this.task.starttime);
+
+      this.timer = setInterval(() => {
+        const time = this.parseTime(new Date(Date.now() - dis))
+
+        if (time > this.task.endtime) {
+          this.stop();
+          return;
+        }
+
+        this.messages = this.messagesHistory.filter(({ createTime }) => createTime < time);
+        this.measureLine = this.measureLineHistory.filter(({ createTime }) => createTime < time);
+        // this.waterlevelList = this.waterLevelHistory.filter(({ createTime }) => createTime < time);
+
+        let location = {};
+        this.locationHistory.forEach((item) => {
+          if (item.createTime > time) {
+            return
+          }
+          if (!location.createTime || item.createTime > location.createTime) {
+            location = { ...item }
+          }
+        })
+        if (location.position) {
+          this.location = location.position;
+        }
+
+        let carInfo = this.carInfoHistory[0];
+        this.carInfoHistory.forEach((item) => {
+          if (item.createTime > time) {
+            return
+          }
+          if (!carInfo.createTime || item.createTime > carInfo.createTime) {
+            carInfo = { ...item };
+          }
+        })
+        this.carInfo = carInfo;
+
+        let waterlevel = this.waterLevelHistory[0];
+        this.waterLevelHistory.forEach((item) => {
+          if (item.createTime > time) {
+            return
+          }
+          if (!waterlevel.createTime || item.createTime > waterlevel.createTime) {
+            waterlevel = { ...item };
+          }
+        })
+        this.waterlevel = waterlevel.avgWaterlevel;
+      }, 500)
+    },
+    stop() {
+      this.isplay = false;
+      this.messages = [...this.messagesHistory];
+      this.measureLine = [...this.measureLineHistory];
+      this.waterlevelList = [...this.waterLevelHistory];
+      this.location = this.initlocation;
+      this.carInfo = this.initcarInfo;
+      this.waterlevel = this.initwaterlevel;
+      if (this.timer) {
+        clearInterval(this.timer);
+      }
+    },
+  },
+  mounted() {
+    this.loadCarInfo();
+    this.loadMeasureLine();
+    this.loadTaskNotice();
+    this.loadSiteRealTime();
+    this.loadWaterLevel();
+    this.loadCarLocation();
+  },
+  beforeDestroy() {
+    if (this.timer) {
+      clearInterval(this.timer);
+    }
+  }
 }
 </script>
 

+ 349 - 0
ruoyi-ui/src/views/analysis/task/result/simulation.vue

@@ -0,0 +1,349 @@
+<template>
+  <div class="realtime-container">
+    <div ref="chart" class="chart" :style="{height: '400px', width: '100%'}" />
+    <div class="realtime-foot">
+      <div class="realtime-foot-title">测流成果</div>
+      <div class="realtime-foot-actions">
+        <div class="realtime-foot-action" v-if="isplay">
+          <svg-icon icon-class="realtime-stop" class-name="realtime-foot-action-icon" @click="stop" />
+          <div class="realtime-foot-action-label">中止回放</div>
+        </div>
+        <div class="realtime-foot-action" v-else>
+          <svg-icon icon-class="realtime-play" class-name="realtime-foot-action-icon" @click="play" />
+          <div class="realtime-foot-action-label">回放</div>
+        </div>
+      </div>
+      <div class="realtime-foot-time">
+        <div class="realtime-foot-time-label">施测时间:</div>
+        <div class="realtime-foot-time-value">{{ task.createTime }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import * as echarts from "echarts";
+import resize from '@/utils/resize'
+import { getConfig } from '@/api/site/site'
+import { getSiteSection } from '@/api/site/berthing'
+import CarSvg from '@/assets/images/car.svg'
+
+export default {
+  mixins: [resize],
+  props: {
+    siteId: Number | String,
+    positions: Array,
+    measureLine: Array,
+    siteRealTime: Object,
+    task: Object,
+    location: Number,
+    waterlevel: Number,
+    isplay: Boolean,
+  },
+  data() {
+    return {
+      sections: [],
+      config: {},
+    }
+  },
+  mounted() {
+    this.loadSection();
+    this.loadSiteConfig();
+    this.$nextTick(() => {
+      this.chart = echarts.init(this.$refs.chart, 'macarons');
+    })
+  },
+  beforeDestroy() {
+    if (this.chart) {
+      this.chart.dispose()
+      this.chart = null
+    }
+  },
+  methods: {
+    play() {
+      this.$emit('play');
+    },
+    stop() {
+      this.$modal.confirm('是否确认中止回放?').then(() => {
+        this.$emit('stop');
+      }).catch(() => {});
+    },
+    loadSection() {
+      getSiteSection(this.siteId).then((res) => {
+        this.sections = JSON.parse(res.data.positions) || [];
+        this.setOptions();
+      })
+    },
+    loadSiteConfig() {
+      getConfig(this.siteId).then((res) => {
+        this.config = res.data || {};
+        this.setOptions();
+      })
+    },
+    setOptions() {
+      if (!this.chart || !this.config || this.sections.length === 0 || this.positions.length === 0) {
+        return;
+      }
+
+      const carWidth = 50; // 小车宽度
+      const carHeight = 32; // 小车高度
+      const barWidth = 18; // 立柱宽度
+      const boxWidth = carWidth * 1.2; // 小车充电盒子宽度
+
+      const waterlevel = this.waterlevel;
+      const start = this.config.offset;
+      const location = this.location < start ? start : this.location;
+
+      const { sections, bar1X, bar2X } = this.calcSection();
+
+      const x = sections.map(([x]) => x);
+      const maxX = Math.max(...x);
+      const minX = Math.min(...x);
+
+      const y = sections.map(([,y]) => y);
+      const maxY = Math.max(...y);
+      const minY = Math.min(...y);
+      const disY = maxY - minY;
+      const barY = maxY + disY * 1.2;
+      const lineY = maxY + disY * 0.8;
+
+      const stopSeries = this.positions.map(({x}) => [x, lineY]);
+      const stopLineSeries = this.measureLine.map(({pn, wspeed}) => ({xAxis: this.positions[pn - 1].x, name: `${wspeed}m³/h`}));
+
+      const options = {
+        grid: {
+          top: 0,
+          left: 0,
+          right: 0,
+          bottom: 0,
+          show: false,
+        },
+        xAxis: {
+          type: 'value',
+          min: minX,
+          max: maxX,
+          show: false,
+        },
+        yAxis: {
+          min: minY - disY * 0.1,
+          max: maxY + disY * 1.5,
+          type: 'value',
+          show: false,
+        },
+        series: [
+          {
+            data: sections,
+            type: 'line',
+            // symbol: 'none',
+            animation: false,
+            z: 10,
+            lineStyle: {
+              width: 1,
+              color: '#FF8500',
+            },
+            areaStyle: {
+              opacity: 1,
+              color: '#ffc27f',
+            }
+          },
+          {
+            data: [[minX, waterlevel],[maxX, waterlevel]],
+            type: 'line',
+            symbol: 'none',
+            z: 0,
+            lineStyle: {
+              width: 0,
+            },
+            areaStyle: {
+              opacity: 1,
+              color: '#a5cdf7',
+            }
+          },
+          {
+            data: [[bar1X, lineY],[bar2X, lineY]],
+            type: 'line',
+            symbol: 'none',
+            z: 2,
+            lineStyle: {
+              width: 2,
+              color: '#54606C',
+            },
+          },
+          {
+            data: stopSeries,
+            type: 'line',
+            z: 4,
+            symbol: 'circle',
+            symbolSize: (_, params) => {
+              if (params.dataIndex < this.measureLine.length) {
+                return 20
+              } else {
+                return 10
+              }
+            },
+            lineStyle: {
+              width: 0,
+            },
+            itemStyle: {
+              color: (params) => {
+                if (params.dataIndex < this.measureLine.length) {
+                  return '#55DA74'
+                } else {
+                  return '#FF8500'
+                }
+              },
+            },
+            markLine: {
+              data: stopLineSeries,
+              symbol: 'none',
+              lineStyle: {
+                color: '#FF8500'
+              },
+              label: {
+                show: true,
+                position: 'middle',
+                formatter: '{b}',
+                color: '#FF8500',
+              }
+            }
+          },
+          {
+            data: [[bar1X, barY],[bar2X, barY]],
+            type: 'bar',
+            barWidth: barWidth,
+            z: 3,
+            itemStyle: {
+              color: '#A6B7C7',
+            }
+          },
+          {
+            data: [[location, lineY]],
+            type: 'scatter',
+            symbol: `image://${CarSvg}`,
+            symbolSize: [carWidth, carHeight],
+            symbolOffset: [0, -6],
+            silent: true,
+            z: 3,
+            itemStyle: {
+              opacity: 1
+            }
+          },
+          {
+            data: [[start, lineY]],
+            type: 'scatter',
+            symbol: 'rect',
+            symbolSize: [boxWidth, 30],
+            symbolOffset: [0, '-50%'],
+            silent: true,
+            z: 4,
+            itemStyle: {
+              color: '#778CB2',
+              opacity: 0.5
+            }
+          },
+        ]
+      };
+      this.chart.setOption(options);
+    },
+    calcSection() {
+      const { local, offset } = this.config;
+      const sections = [...this.sections.map(({x, y}) => [x, y])];
+      const firstPoint = this.sections[0];
+      const lastPoint = this.sections[this.sections.length - 1];
+      let bar1X = 0;
+      let bar2X = 0;
+
+      if (local === 1) {
+        bar1X = offset
+        bar2X = lastPoint.x + (firstPoint.x - offset)
+        if (offset < firstPoint.x) {
+          sections.unshift([offset, firstPoint.y])
+          sections.push([bar2X, lastPoint.y])
+        }
+      } else {
+        bar1X = firstPoint.x - (offset - lastPoint.x)
+        bar2X = offset
+        if (offset > lastPoint.x) {
+          sections.unshift([bar1X, firstPoint.y])
+          sections.push([offset, lastPoint.y])
+        }
+      }
+
+      const x = sections.map(([x]) => x);
+      const min = Math.min(...x);
+      const max = Math.max(...x);
+
+      const width = max - min;
+      const gap = width * 0.05;
+
+      sections.unshift([min - gap, firstPoint.y]);
+      sections.push([max + gap, lastPoint.y]);
+
+      return { sections, bar1X, bar2X };
+    }
+  },
+  watch: {
+    waterlevel() {
+      this.setOptions();
+    },
+    location() {
+      this.setOptions();
+    },
+    measureLine() {
+      this.setOptions();
+    },
+  }
+}
+</script>
+
+<style scoped>
+.realtime-container {
+  height: 500px;
+  background: linear-gradient(0, #FFFFFF 57%, #D1E8FF 100%);
+  border-radius: 4px;
+  padding: 0 20px;
+}
+.realtime-foot {
+  height: 100px;
+  background: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.realtime-foot-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #1D2738;
+}
+.realtime-foot-actions {
+  display: flex;
+  align-items: center;
+}
+.realtime-foot-action {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: 0 25px;
+  cursor: pointer;
+}
+.realtime-foot-action-icon {
+  width: 40px;
+  height: 40px;
+}
+.realtime-foot-action-label {
+  font-size: 12px;
+  line-height: 20px;
+  color: #54606C;
+}
+.realtime-foot-time {
+  display: flex;
+  align-items: center;
+  font-size: 14px;
+}
+.realtime-foot-time-label {
+  color: #54606C;
+}
+.realtime-foot-time-value {
+  color: #1D2738;
+}
+</style>

+ 69 - 0
ruoyi-ui/src/views/analysis/task/result/site.vue

@@ -0,0 +1,69 @@
+<template>
+  <div class="realtime-container">
+    <div class="title">站点信息</div>
+    <div class="info">
+      <div class="item">
+        <div class="item-icon"></div>
+        <div class="item-label">站点名称:</div>
+        <div class="item-value">{{siteRealTime.siteName}}</div>
+      </div>
+      <div class="item">
+        <div class="item-icon"></div>
+        <div class="item-label">下次测流时间:</div>
+        <div class="item-value">{{ siteRealTime.nextTime || '-' }}</div>
+      </div>
+      <div class="item">
+        <div class="item-icon"></div>
+        <div class="item-label">站点状态:</div>
+        <div class="item-value">{{['空闲', '测流中'][siteRealTime.siteStatus]}}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    siteRealTime: Object,
+  },
+}
+</script>
+
+<style scoped>
+.realtime-container {
+  background: #fff;
+  height: 190px;
+  border-radius: 4px;
+  padding: 10px 20px;
+}
+.title {
+  font-size: 18px;
+  line-height: 26px;
+  font-weight: 600;
+  color: #1D2738;
+}
+.info {
+  padding-top: 5px;
+  font-size: 14px;
+  line-height: 18px;
+}
+.item {
+  display: flex;
+  align-items: center;
+  padding: 5px 0;
+}
+.item-icon {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: #55DA74;
+  margin-right: 8px;
+}
+.item-label {
+  width: 100px;
+  color: #54606C;
+}
+.item-value {
+  color: #1D2738;
+}
+</style>

+ 132 - 0
ruoyi-ui/src/views/analysis/task/result/water.vue

@@ -0,0 +1,132 @@
+<template>
+  <div class="realtime-container">
+    <div class="title">当日时间水位趋势图</div>
+    <div ref="chart" class="chart" :style="{height: '140px', width: '100%'}" />
+  </div>
+</template>
+
+<script>
+import * as echarts from "echarts";
+import resize from '@/utils/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    waterlevelList: Array,
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.chart = echarts.init(this.$refs.chart, 'macarons');
+    })
+  },
+  beforeDestroy() {
+    if (this.chart) {
+      this.chart.dispose()
+      this.chart = null
+    }
+  },
+  methods: {
+    setOptions() {
+      if (!this.chart) {
+        return;
+      }
+      const chartData = [...this.waterlevelList];
+      chartData.reverse()
+      const xAxisData = chartData.map(({ time }) => time);
+      const seriesData = chartData.map(({ avgWaterlevel }) => avgWaterlevel);
+      const options = {
+        xAxis: {
+          name: '时间',
+          data: xAxisData,
+          type: 'category',
+          axisLabel: {
+            color: '#54606C'
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#E4E4E4'
+            }
+          }
+        },
+        yAxis: [
+          {
+            name: '水位(m)',
+            axisLine: {
+              show: false
+            },
+            axisLabel: {
+              color: '#54606C'
+            },
+            nameTextStyle: {
+              color: '#8D99A4'
+            },
+            min: 'dataMin',
+            max: 'dataMax',
+          },
+        ],
+        grid: {
+          left: 20,
+          right: 0,
+          bottom: 0,
+          top: 30,
+          containLabel: true
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'line'
+          },
+          formatter: function(prams) {
+            return `时间:${prams[0].name}<br/>水位:${prams[0].data}m`;
+          },
+          padding: [5, 10]
+        },
+        series: [{
+          name: '水位',
+          type: 'line',
+          smooth: false,
+          data: seriesData,
+          areaStyle: {
+            normal: {
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                {
+                  offset: 0,
+                  color: "rgba(127, 200, 229, 0.4)"
+                },
+                {
+                  offset: 1,
+                  color: "rgba(119, 211, 247, 0.1)"
+                }
+              ], false),
+              shadowColor: "rgba(119, 211, 247, 0.1)",
+              shadowBlur: 20
+            }
+          },
+        }]
+      };
+      this.chart.clear();
+      this.chart.setOption(options);
+    }
+  },
+  watch: {
+    waterlevelList() {
+      this.setOptions()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.realtime-container {
+  background: #fff;
+  height: 190px;
+  border-radius: 4px;
+  padding: 10px 20px;
+}
+.title {
+  font-size: 18px;
+  line-height: 26px;
+  font-weight: 600;
+  color: #1D2738;
+}
+</style>