Переглянути джерело

feat: 实时动态模拟站点信息及小车状态水位趋势实时更新

hum 1 рік тому
батько
коміт
85ead5e081

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

@@ -1,7 +1,10 @@
 <template>
   <div v-if="task.taskid">
-    <Realtime v-if="task.status === 0 || task.status === 3" :task="task" @refresh="loadTask" />
-    <Result v-if="task.status === 2" :task="task" @refresh="loadTask" />
+    <template v-if="task.status === 2">
+      <Waiting v-if="isFromHome" :task="task" @refresh="loadTask" />
+      <Result v-else :task="task" @refresh="loadTask" />
+    </template>
+    <Realtime v-else :task="task" @refresh="loadTask" />
   </div>
 </template>
 
@@ -9,15 +12,18 @@
 import {getAchievement, getLatestTask} from '@/api/analysis/achievement';
 import Realtime from './realtime/realtime';
 import Result from './result/result';
+import Waiting from './waiting/waiting';
 
 export default {
   components: {
     Realtime,
     Result,
+    Waiting,
   },
   data() {
     return {
       siteId: 0,
+      isFromHome: true,
       task: {},
     }
   },
@@ -29,7 +35,8 @@ export default {
   methods: {
     loadTask() {
       const { taskid } = this.$route.query;
-      const promise = taskid ? getAchievement(taskid) : getLatestTask(this.siteId);
+      this.isFromHome = !taskid;
+      const promise = this.isFromHome ? getLatestTask(this.siteId) : getAchievement(taskid);
       promise.then((res) => {
         if (res.data === null) {
           this.$message.error("测流任务不存在");

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

@@ -4,10 +4,6 @@
     <div class="realtime-foot">
       <div class="realtime-foot-title">测流成果</div>
       <div class="realtime-foot-actions">
-        <div class="realtime-foot-action">
-          <svg-icon icon-class="manual" class-name="realtime-foot-action-icon" @click="handleManualMeasure" />
-          <div class="realtime-foot-action-label">手动测流</div>
-        </div>
         <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>
@@ -22,8 +18,6 @@
         <div class="realtime-foot-time-value">{{ task.createTime }}</div>
       </div>
     </div>
-
-    <Manual ref="manual" />
   </div>
 </template>
 
@@ -33,12 +27,8 @@ import resize from '@/utils/resize'
 import { getConfig } from '@/api/site/site'
 import { getSiteSection } from '@/api/site/berthing'
 import CarSvg from '@/assets/images/car.svg'
-import Manual from './manual';
 
 export default {
-  components: {
-    Manual,
-  },
   mixins: [resize],
   props: {
     siteId: Number | String,
@@ -90,9 +80,6 @@ export default {
         this.setOptions();
       })
     },
-    handleManualMeasure() {
-      this.$refs.manual.open({ siteId: this.siteId });
-    },
     setOptions() {
       if (!this.chart || !this.config || this.sections.length === 0) {
         return;

+ 80 - 0
ruoyi-ui/src/views/analysis/task/waiting/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 || '-' | twoDecimalPlaces}}%</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>

+ 110 - 0
ruoyi-ui/src/views/analysis/task/waiting/flow.vue

@@ -0,0 +1,110 @@
+<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";
+require('echarts/theme/macarons') // echarts theme
+import resize from '@/utils/resize'
+
+export default {
+  mixins: [resize],
+  props: {
+    positions: Array,
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.chart = echarts.init(this.$refs.chart, 'macarons');
+    })
+  },
+  beforeDestroy() {
+    if (this.chart) {
+      this.chart.dispose()
+      this.chart = null
+    }
+  },
+  methods: {
+    setOptions(chartData) {
+      if (!this.chart || !this.positions) {
+        return;
+      }
+      const xAxisData = this.positions.map(({x}) => x);
+      const seriesData = this.positions.map((_,index) => chartData.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);
+    }
+  }
+}
+</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>

ruoyi-ui/src/views/analysis/task/result/manual.vue → ruoyi-ui/src/views/analysis/task/waiting/manual.vue


+ 44 - 0
ruoyi-ui/src/views/analysis/task/waiting/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/waiting/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/waiting/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>

+ 406 - 0
ruoyi-ui/src/views/analysis/task/waiting/simulation.vue

@@ -0,0 +1,406 @@
+<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">
+          <svg-icon icon-class="manual" class-name="realtime-foot-action-icon" @click="handleManualMeasure" />
+          <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>
+
+    <Manual ref="manual" />
+  </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 { getCarLocation, getWaterLevel, taskAction } from '@/api/analysis/achievement'
+import CarSvg from '@/assets/images/car.svg'
+import Manual from './manual';
+
+export default {
+  mixins: [resize],
+  components: {
+    Manual,
+  },
+  props: {
+    siteId: Number | String,
+    positions: Array,
+    measureLine: Array,
+    siteRealTime: Object,
+    task: Object,
+  },
+  data() {
+    return {
+      sections: [],
+      config: {},
+      location: 0,
+      waterlevel: 0,
+    }
+  },
+  mounted() {
+    this.loadSection();
+    this.loadSiteConfig();
+    this.loadCarLocation();
+    this.timer1 = setInterval(() => this.loadCarLocation(), 5e3)
+    this.loadWaterLevel();
+    this.timer2 = setInterval(() => this.loadWaterLevel(), 5e3)
+    this.$nextTick(() => {
+      this.chart = echarts.init(this.$refs.chart, 'macarons');
+    })
+  },
+  beforeDestroy() {
+    if (this.timer1) clearTimeout(this.timer1);
+    if (this.timer2) clearTimeout(this.timer2);
+    if (this.chart) {
+      this.chart.dispose()
+      this.chart = null
+    }
+  },
+  methods: {
+    pause() {
+      this.$modal.confirm('是否确认暂停当前任务?').then(() => {
+        return taskAction(this.siteId, 1);
+      }).then(() => {
+        this.$emit('refresh');
+        this.$modal.msgSuccess("暂停成功");
+      }).catch(() => {});
+    },
+    play() {
+      this.$modal.confirm('是否确认重启当前任务?').then(() => {
+        return taskAction(this.siteId, 2);
+      }).then(() => {
+        this.$emit('refresh');
+        this.$modal.msgSuccess("重启成功");
+      }).catch(() => {});
+    },
+    stop() {
+      this.$modal.confirm('是否确认终止当前任务?').then(() => {
+        return taskAction(this.siteId, 0);
+      }).then(() => {
+        this.$router.back();
+        this.$modal.msgSuccess("终止成功");
+      }).catch(() => {});
+    },
+    loadCarLocation() {
+      getCarLocation({ siteId: this.siteId }).then((res) => {
+        this.location = res.data?.[0].position || 0;
+        this.setOptions();
+      })
+    },
+    loadSection() {
+      getSiteSection(this.siteId).then((res) => {
+        this.sections = JSON.parse(res.data.positions) || [];
+        this.setOptions();
+      })
+    },
+    loadWaterLevel() {
+      getWaterLevel(this.siteId).then((res) => {
+        this.waterlevel = res.data.waterlevel || 0;
+        this.setOptions();
+      })
+    },
+    loadSiteConfig() {
+      getConfig(this.siteId).then((res) => {
+        this.config = res.data || {};
+        this.setOptions();
+      })
+    },
+    setOptions() {
+      if (!this.chart || !this.config || this.sections.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;
+
+      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,
+        },
+        tooltip: {
+          trigger: 'item',
+        },
+        series: [
+          {
+            data: sections,
+            type: 'line',
+            // symbol: 'none',
+            animation: false,
+            z: 10,
+            lineStyle: {
+              width: 1,
+              color: '#FF8500',
+            },
+            areaStyle: {
+              opacity: 1,
+              color: '#ffc27f',
+            },
+            tooltip: {
+              show: false,
+            }
+          },
+          {
+            data: [[minX, waterlevel],[maxX, waterlevel]],
+            type: 'line',
+            symbol: 'none',
+            z: 0,
+            lineStyle: {
+              width: 0,
+            },
+            areaStyle: {
+              opacity: 1,
+              color: '#a5cdf7',
+            },
+            tooltip: {
+              show: false,
+            }
+          },
+          {
+            data: [[bar1X, lineY],[bar2X, lineY]],
+            type: 'line',
+            symbol: 'none',
+            z: 2,
+            lineStyle: {
+              width: 2,
+              color: '#54606C',
+            },
+            tooltip: {
+              show: false,
+            }
+          },
+          {
+            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',
+              },
+              tooltip: {
+                show: false,
+              }
+            },
+            tooltip: {
+              formatter: (params) => {
+                return `起点距: ${params.value[0]}`;
+              }
+            }
+          },
+          {
+            data: [[bar1X, barY],[bar2X, barY]],
+            type: 'bar',
+            barWidth: barWidth,
+            z: 3,
+            itemStyle: {
+              color: '#A6B7C7',
+            },
+            tooltip: {
+              show: false,
+            }
+          },
+          {
+            data: [[location, lineY]],
+            type: 'scatter',
+            symbol: `image://${CarSvg}`,
+            symbolSize: [carWidth, carHeight],
+            symbolOffset: [0, -6],
+            silent: true,
+            z: 3,
+            itemStyle: {
+              opacity: 1
+            },
+            tooltip: {
+              show: false,
+            }
+          },
+          {
+            data: [[start, lineY]],
+            type: 'scatter',
+            symbol: 'rect',
+            symbolSize: [boxWidth, 30],
+            symbolOffset: [0, '-50%'],
+            silent: true,
+            z: 4,
+            itemStyle: {
+              color: '#778CB2',
+              opacity: 0.5
+            },
+            tooltip: {
+              show: false,
+            }
+          },
+        ]
+      };
+      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 };
+    },
+    handleManualMeasure() {
+      this.$refs.manual.open({ siteId: this.siteId });
+    },
+  }
+}
+</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/waiting/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>

+ 157 - 0
ruoyi-ui/src/views/analysis/task/waiting/waiting.vue

@@ -0,0 +1,157 @@
+<template>
+  <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"
+          @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" style="margin-top: 10px" />
+        <Water ref="water" 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,
+  getPointConfigByPlanid,
+} from "@/api/analysis/achievement";
+
+export default {
+  components: {
+    Simulation,
+    Site,
+    Car,
+    Message,
+    Movie,
+    Flow,
+    Water,
+    Report,
+  },
+  props: {
+    task: Object,
+  },
+  data() {
+    return {
+      siteId: this.$route.params.siteId,
+      positions: [],
+      carInfo: {},
+      siteRealTime: {},
+      messages: [],
+      measureLine: [],
+    }
+  },
+  methods: {
+    loadCarInfo() {
+      getCarInfo({ siteId: this.siteId }).then((res) => {
+        this.carInfo = res.data?.[0] || {};
+      })
+    },
+    loadMeasureLine() {
+      const params = {
+        siteId: this.siteId,
+        taskId: this.task.taskid,
+      }
+      listMeasureLine(params).then((res) => {
+        const measureLine = res.data || [];
+        this.measureLine = measureLine;
+        this.$refs.flow.setOptions(measureLine)
+        this.$nextTick(() => {
+          this.$refs.simulation.setOptions()
+        })
+      })
+    },
+    loadTaskNotice() {
+      getTaskNotice(this.siteId, this.task.taskid).then((res) => {
+        this.messages = res.data.records || [];
+      })
+    },
+    loadSiteRealTime() {
+      getSiteRealTime(this.siteId).then((res) => {
+        this.siteRealTime = res.data || {};
+      })
+    },
+    loadWaterLevel() {
+      const endTime = new Date()
+      const startTime = new Date()
+      startTime.setHours(0)
+      startTime.setMinutes(0)
+      startTime.setSeconds(0)
+      startTime.setMilliseconds(0)
+      const params = {
+        startTime: this.parseTime(startTime),
+        endTime: this.parseTime(endTime),
+        siteId: this.siteId,
+        type: 1,
+        page: 1,
+        size: 10000
+      }
+      listWaterLevel(params).then((res) => {
+        this.$refs.water.setOptions(res.data?.records || [])
+      })
+    },
+    loadPositions() {
+      getPointConfigByPlanid(this.siteId, this.task.planid).then((res) => {
+        this.positions = JSON.parse(res.data.positions || '[]');
+      })
+    }
+  },
+  mounted() {
+    this.loadPositions();
+    this.loadCarInfo();
+    this.timer1 = setInterval(() => this.loadCarInfo(), 5e3)
+    this.loadMeasureLine();
+    // this.timer2 = setInterval(() => this.loadMeasureLine(), 5e3)
+    this.loadTaskNotice();
+    // this.timer3 = setInterval(() => this.loadTaskNotice(), 5e3)
+    this.loadSiteRealTime();
+    this.timer4 = setInterval(() => this.loadSiteRealTime(), 5e3)
+    this.loadWaterLevel();
+    this.timer5 = setInterval(() => this.loadWaterLevel(), 5e3)
+  },
+  beforeDestroy() {
+    if (this.timer1) clearTimeout(this.timer1);
+    if (this.timer2) clearTimeout(this.timer2);
+    if (this.timer3) clearTimeout(this.timer3);
+    if (this.timer4) clearTimeout(this.timer4);
+    if (this.timer5) clearTimeout(this.timer5);
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 123 - 0
ruoyi-ui/src/views/analysis/task/waiting/water.vue

@@ -0,0 +1,123 @@
+<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";
+require('echarts/theme/macarons') // echarts theme
+import resize from '@/utils/resize'
+
+export default {
+  mixins: [resize],
+  mounted() {
+    this.$nextTick(() => {
+      this.chart = echarts.init(this.$refs.chart, 'macarons');
+    })
+  },
+  beforeDestroy() {
+    if (this.chart) {
+      this.chart.dispose()
+      this.chart = null
+    }
+  },
+  methods: {
+    setOptions(chartData) {
+      if (!this.chart) {
+        return;
+      }
+      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.setOption(options);
+    }
+  }
+}
+</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>