simulation.vue 10 KB

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