simulation.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. <template>
  2. <div class="realtime-container">
  3. <div ref="chart" class="chart" :style="{height: '400px', width: '100%'}" />
  4. <div class="realtime-foot">
  5. <div class="realtime-foot-title">实时动态模拟</div>
  6. <div class="realtime-foot-actions">
  7. <template v-if="stopped">
  8. <div class="realtime-foot-action">
  9. <svg-icon icon-class="realtime-stop" class-name="realtime-foot-action-icon" />
  10. <div class="realtime-foot-action-label">已终止</div>
  11. </div>
  12. </template>
  13. <template v-else>
  14. <div class="realtime-foot-action" v-if="task.status === 0">
  15. <svg-icon icon-class="realtime-pause" class-name="realtime-foot-action-icon" @click="pause" />
  16. <div class="realtime-foot-action-label">暂停任务</div>
  17. </div>
  18. <div class="realtime-foot-action" v-if="task.status === 3">
  19. <svg-icon icon-class="realtime-play" class-name="realtime-foot-action-icon" @click="play" />
  20. <div class="realtime-foot-action-label">开始任务</div>
  21. </div>
  22. <div class="realtime-foot-action" v-if="task.status === 0 || task.status === 3">
  23. <svg-icon icon-class="realtime-stop" class-name="realtime-foot-action-icon" @click="stop" />
  24. <div class="realtime-foot-action-label">终止任务</div>
  25. </div>
  26. </template>
  27. </div>
  28. <div class="realtime-foot-time">
  29. <div class="realtime-foot-time-label">施测时间:</div>
  30. <div class="realtime-foot-time-value">{{ task.createTime }}</div>
  31. </div>
  32. </div>
  33. </div>
  34. </template>
  35. <script>
  36. import * as echarts from "echarts";
  37. import resize from '@/utils/resize'
  38. import { getConfig } from '@/api/site/site'
  39. import { getSiteSection } from '@/api/site/berthing'
  40. import { getCarLocation, getWaterLevel, taskAction } from '@/api/analysis/achievement'
  41. import CarSvg from '@/assets/images/car.svg'
  42. export default {
  43. mixins: [resize],
  44. props: {
  45. siteId: Number | String,
  46. positions: Array,
  47. measureLine: Array,
  48. siteRealTime: Object,
  49. task: Object,
  50. },
  51. data() {
  52. return {
  53. sections: [],
  54. config: {},
  55. location: 0,
  56. waterlevel: 0,
  57. stopped: false,
  58. }
  59. },
  60. mounted() {
  61. this.loadSection();
  62. this.loadSiteConfig();
  63. this.loadCarLocation();
  64. this.timer1 = setInterval(() => this.loadCarLocation(), 5e3)
  65. this.loadWaterLevel();
  66. this.timer2 = setInterval(() => this.loadWaterLevel(), 5e3)
  67. this.$nextTick(() => {
  68. this.chart = echarts.init(this.$refs.chart, 'macarons');
  69. })
  70. },
  71. beforeDestroy() {
  72. if (this.timer1) clearTimeout(this.timer1);
  73. if (this.timer2) clearTimeout(this.timer2);
  74. if (this.chart) {
  75. this.chart.dispose()
  76. this.chart = null
  77. }
  78. },
  79. methods: {
  80. pause() {
  81. this.$modal.confirm('是否确认暂停当前任务?').then(() => {
  82. return taskAction(this.siteId, 1);
  83. }).then(() => {
  84. this.$emit('refresh');
  85. this.$modal.msgSuccess("暂停成功");
  86. }).catch(() => {});
  87. },
  88. play() {
  89. this.$modal.confirm('是否确认重启当前任务?').then(() => {
  90. return taskAction(this.siteId, 2);
  91. }).then(() => {
  92. this.$emit('refresh');
  93. this.$modal.msgSuccess("重启成功");
  94. }).catch(() => {});
  95. },
  96. stop() {
  97. this.$modal.confirm('是否确认终止当前任务?').then(() => {
  98. return taskAction(this.siteId, 0);
  99. }).then(() => {
  100. this.stopped = true;
  101. this.$modal.msgSuccess("终止成功");
  102. }).catch(() => {});
  103. },
  104. loadCarLocation() {
  105. getCarLocation({ siteId: this.siteId }).then((res) => {
  106. this.location = res.data?.[0].position || 0;
  107. this.setOptions();
  108. })
  109. },
  110. loadSection() {
  111. getSiteSection(this.siteId).then((res) => {
  112. this.sections = JSON.parse(res.data.positions) || [];
  113. this.setOptions();
  114. })
  115. },
  116. loadWaterLevel() {
  117. getWaterLevel(this.siteId).then((res) => {
  118. this.waterlevel = res.data.waterlevel || 0;
  119. this.setOptions();
  120. })
  121. },
  122. loadSiteConfig() {
  123. getConfig(this.siteId).then((res) => {
  124. this.config = res.data || {};
  125. this.setOptions();
  126. })
  127. },
  128. setOptions() {
  129. if (!this.chart || !this.config || this.sections.length === 0) {
  130. return;
  131. }
  132. const carWidth = 50; // 小车宽度
  133. const carHeight = 32; // 小车高度
  134. const barWidth = 18; // 立柱宽度
  135. const boxWidth = carWidth * 1.2; // 小车充电盒子宽度
  136. const waterlevel = this.waterlevel;
  137. const start = this.config.offset;
  138. const location = this.location;
  139. const { sections, bar1X, bar2X } = this.calcSection();
  140. const x = sections.map(([x]) => x);
  141. const maxX = Math.max(...x);
  142. const minX = Math.min(...x);
  143. const y = sections.map(([,y]) => y);
  144. const maxY = Math.max(...y);
  145. const minY = Math.min(...y);
  146. const disY = maxY - minY;
  147. const barY = maxY + disY * 1.2;
  148. const lineY = maxY + disY * 0.8;
  149. const stopSeries = this.positions.map(({x}) => [x, lineY]);
  150. const stopLineSeries = this.measureLine.map(({pn, wspeed}) => ({xAxis: this.positions[pn - 1].x, name: `${wspeed}m³/h`}));
  151. const options = {
  152. grid: {
  153. top: 0,
  154. left: 0,
  155. right: 0,
  156. bottom: 0,
  157. show: false,
  158. },
  159. tooltip: {
  160. trigger: 'item',
  161. },
  162. xAxis: {
  163. type: 'value',
  164. min: minX,
  165. max: maxX,
  166. show: false,
  167. },
  168. yAxis: {
  169. min: minY - disY * 0.1,
  170. max: maxY + disY * 1.5,
  171. type: 'value',
  172. show: false,
  173. },
  174. series: [
  175. {
  176. data: sections,
  177. type: 'line',
  178. // symbol: 'none',
  179. animation: false,
  180. z: 10,
  181. lineStyle: {
  182. width: 1,
  183. color: '#FF8500',
  184. },
  185. areaStyle: {
  186. opacity: 1,
  187. color: '#ffc27f',
  188. },
  189. tooltip: {
  190. show: false,
  191. }
  192. },
  193. {
  194. data: [[minX, waterlevel],[maxX, waterlevel]],
  195. type: 'line',
  196. symbol: 'none',
  197. z: 0,
  198. lineStyle: {
  199. width: 0,
  200. },
  201. areaStyle: {
  202. opacity: 1,
  203. color: '#a5cdf7',
  204. },
  205. tooltip: {
  206. show: false,
  207. }
  208. },
  209. {
  210. data: [[bar1X, lineY],[bar2X, lineY]],
  211. type: 'line',
  212. symbol: 'none',
  213. z: 2,
  214. lineStyle: {
  215. width: 2,
  216. color: '#54606C',
  217. },
  218. tooltip: {
  219. show: false,
  220. }
  221. },
  222. {
  223. data: stopSeries,
  224. type: 'line',
  225. z: 4,
  226. symbol: 'circle',
  227. symbolSize: (_, params) => {
  228. if (params.dataIndex < this.measureLine.length) {
  229. return 20
  230. } else {
  231. return 10
  232. }
  233. },
  234. lineStyle: {
  235. width: 0,
  236. },
  237. itemStyle: {
  238. color: (params) => {
  239. if (params.dataIndex < this.measureLine.length) {
  240. return '#55DA74'
  241. } else {
  242. return '#FF8500'
  243. }
  244. },
  245. },
  246. markLine: {
  247. data: stopLineSeries,
  248. symbol: 'none',
  249. lineStyle: {
  250. color: '#FF8500'
  251. },
  252. label: {
  253. show: true,
  254. position: 'middle',
  255. formatter: '{b}',
  256. color: '#FF8500',
  257. },
  258. tooltip: {
  259. show: false
  260. }
  261. },
  262. tooltip: {
  263. formatter: (params) => {
  264. return `起点距: ${params.value[0]}`;
  265. }
  266. }
  267. },
  268. {
  269. data: [[bar1X, barY],[bar2X, barY]],
  270. type: 'bar',
  271. barWidth: barWidth,
  272. z: 3,
  273. itemStyle: {
  274. color: '#A6B7C7',
  275. },
  276. tooltip: {
  277. show: false
  278. }
  279. },
  280. {
  281. data: [[location, lineY]],
  282. type: 'scatter',
  283. symbol: `image://${CarSvg}`,
  284. symbolSize: [carWidth, carHeight],
  285. symbolOffset: [0, -6],
  286. silent: true,
  287. z: 3,
  288. itemStyle: {
  289. opacity: 1
  290. },
  291. tooltip: {
  292. show: false
  293. }
  294. },
  295. {
  296. data: [[start, lineY]],
  297. type: 'scatter',
  298. symbol: 'rect',
  299. symbolSize: [boxWidth, 30],
  300. symbolOffset: [0, '-50%'],
  301. silent: true,
  302. z: 4,
  303. itemStyle: {
  304. color: '#778CB2',
  305. opacity: 0.5
  306. },
  307. tooltip: {
  308. show: false
  309. }
  310. },
  311. ]
  312. };
  313. this.chart.setOption(options);
  314. },
  315. calcSection() {
  316. const { local, offset } = this.config;
  317. const sections = [...this.sections.map(({x, y}) => [x, y])];
  318. const firstPoint = this.sections[0];
  319. const lastPoint = this.sections[this.sections.length - 1];
  320. let bar1X = 0;
  321. let bar2X = 0;
  322. if (local === 1) {
  323. bar1X = offset
  324. bar2X = lastPoint.x + (firstPoint.x - offset)
  325. if (offset < firstPoint.x) {
  326. sections.unshift([offset, firstPoint.y])
  327. sections.push([bar2X, lastPoint.y])
  328. }
  329. } else {
  330. bar1X = firstPoint.x - (offset - lastPoint.x)
  331. bar2X = offset
  332. if (offset > lastPoint.x) {
  333. sections.unshift([bar1X, firstPoint.y])
  334. sections.push([offset, lastPoint.y])
  335. }
  336. }
  337. const x = sections.map(([x]) => x);
  338. const min = Math.min(...x);
  339. const max = Math.max(...x);
  340. const width = max - min;
  341. const gap = width * 0.05;
  342. sections.unshift([min - gap, firstPoint.y]);
  343. sections.push([max + gap, lastPoint.y]);
  344. return { sections, bar1X, bar2X };
  345. }
  346. }
  347. }
  348. </script>
  349. <style scoped>
  350. .realtime-container {
  351. height: 500px;
  352. background: linear-gradient(0, #FFFFFF 57%, #D1E8FF 100%);
  353. border-radius: 4px;
  354. padding: 0 20px;
  355. }
  356. .realtime-foot {
  357. height: 100px;
  358. background: #fff;
  359. display: flex;
  360. align-items: center;
  361. justify-content: space-between;
  362. }
  363. .realtime-foot-title {
  364. font-size: 18px;
  365. font-weight: 600;
  366. color: #1D2738;
  367. }
  368. .realtime-foot-actions {
  369. display: flex;
  370. align-items: center;
  371. }
  372. .realtime-foot-action {
  373. display: flex;
  374. flex-direction: column;
  375. align-items: center;
  376. margin: 0 25px;
  377. cursor: pointer;
  378. }
  379. .realtime-foot-action-icon {
  380. width: 40px;
  381. height: 40px;
  382. }
  383. .realtime-foot-action-label {
  384. font-size: 12px;
  385. line-height: 20px;
  386. color: #54606C;
  387. }
  388. .realtime-foot-time {
  389. display: flex;
  390. align-items: center;
  391. font-size: 14px;
  392. }
  393. .realtime-foot-time-label {
  394. color: #54606C;
  395. }
  396. .realtime-foot-time-value {
  397. color: #1D2738;
  398. }
  399. </style>