IntersectionMap.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. <template>
  2. <div ref="konvaContainer" class="intersection-map-container"></div>
  3. </template>
  4. <script>
  5. import Konva from 'konva';
  6. export default {
  7. name: 'IntersectionMap',
  8. props: {
  9. // 接收父组件传来的路口数据
  10. mapData: {
  11. type: Object,
  12. required: true
  13. }
  14. },
  15. data() {
  16. return {
  17. stage: null,
  18. layer: null,
  19. armsNodes: {}, // 存储四个方向的道路实例
  20. panelNodes: {}, // 存储中央面板的文字实例
  21. // 颜色配置
  22. C: {
  23. BG: '#212842', ROAD: '#3d3938', YELLOW: '#D9A73D', WHITE: '#E0E0E0',
  24. SIGNAL_RED: '#FF5252', SIGNAL_GREEN: '#8DF582',
  25. PANEL_BG: 'rgba(30, 30, 40, 0.85)', BLUE: '#448AFF'
  26. },
  27. // 尺寸配置
  28. sizeConfig: {
  29. stageSize: 900,
  30. laneWidth: 40,
  31. halfRoad: 160,
  32. roadWidth: 320,
  33. armLength: 350
  34. }
  35. };
  36. },
  37. mounted() {
  38. this.initKonvaStage();
  39. if (this.mapData && Object.keys(this.mapData).length > 0) {
  40. this.renderStaticConfig();
  41. this.updateDynamicSignals();
  42. }
  43. // 监听容器尺寸变化,自适应缩放
  44. this._roaPending = false;
  45. this._resizeObserver = new ResizeObserver(() => {
  46. if (!this._roaPending) {
  47. this._roaPending = true;
  48. requestAnimationFrame(() => {
  49. this._roaPending = false;
  50. this.fitToContainer();
  51. });
  52. }
  53. });
  54. this._resizeObserver.observe(this.$refs.konvaContainer);
  55. },
  56. beforeDestroy() {
  57. if (this._resizeObserver) {
  58. this._resizeObserver.disconnect();
  59. }
  60. if (this.stage) {
  61. this.stage.destroy();
  62. }
  63. },
  64. watch: {
  65. // 监听数据变化
  66. mapData: {
  67. handler(newData, oldData) {
  68. if (!newData) return;
  69. // 简单判断:如果是首次加载数据,或者配置变了,需要重新渲染静态配置
  70. if (!oldData || JSON.stringify(newData.armsConfig) !== JSON.stringify(oldData.armsConfig)) {
  71. this.renderStaticConfig();
  72. }
  73. // 每次数据变化都更新信号状态(倒计时和红绿灯)
  74. this.updateDynamicSignals();
  75. },
  76. deep: true // 开启深度监听
  77. }
  78. },
  79. methods: {
  80. // ================= 自适应缩放 =================
  81. fitToContainer() {
  82. if (!this.stage || !this.$refs.konvaContainer) return;
  83. const container = this.$refs.konvaContainer;
  84. const containerWidth = container.clientWidth;
  85. const containerHeight = container.clientHeight;
  86. if (containerWidth === 0 || containerHeight === 0) return;
  87. const designSize = this.sizeConfig.stageSize;
  88. const scale = Math.min(containerWidth / designSize, containerHeight / designSize);
  89. this.stage.width(containerWidth);
  90. this.stage.height(containerHeight);
  91. const offsetX = (containerWidth - designSize * scale) / 2;
  92. const offsetY = (containerHeight - designSize * scale) / 2;
  93. this.layer.scale({ x: scale, y: scale });
  94. this.layer.position({ x: offsetX, y: offsetY });
  95. this.layer.draw();
  96. },
  97. // ================= 初始化画布 =================
  98. initKonvaStage() {
  99. const { stageSize, halfRoad, roadWidth } = this.sizeConfig;
  100. const center = stageSize / 2;
  101. const container = this.$refs.konvaContainer;
  102. const initWidth = container.clientWidth || stageSize;
  103. const initHeight = container.clientHeight || stageSize;
  104. this.stage = new Konva.Stage({
  105. container: this.$refs.konvaContainer,
  106. width: initWidth,
  107. height: initHeight
  108. });
  109. this.layer = new Konva.Layer();
  110. this.stage.add(this.layer);
  111. // 绘制背景
  112. this.layer.add(new Konva.Rect({ width: stageSize, height: stageSize, fill: this.C.BG }));
  113. // 绘制中心路口交叉区
  114. this.layer.add(new Konva.Rect({ x: center - halfRoad, y: center - halfRoad, width: roadWidth, height: roadWidth, fill: this.C.ROAD }));
  115. // 创建四个方向的道路骨架
  116. this.armsNodes = {
  117. N: this.createRoadArm(center, center - halfRoad, 0),
  118. E: this.createRoadArm(center + halfRoad, center, 90),
  119. S: this.createRoadArm(center, center + halfRoad, 180),
  120. W: this.createRoadArm(center - halfRoad, center, 270)
  121. };
  122. Object.values(this.armsNodes).forEach(arm => this.layer.add(arm));
  123. // 创建中央面板
  124. this.createCenterPanel(center);
  125. this.layer.draw();
  126. this.fitToContainer();
  127. },
  128. // ================= 创建道路骨架 (内置绘制逻辑) =================
  129. createRoadArm(x, y, rotation) {
  130. const { halfRoad, roadWidth, laneWidth } = this.sizeConfig;
  131. const group = new Konva.Group({ x, y, rotation });
  132. // 1. 路面与线条
  133. group.add(new Konva.Rect({ x: -halfRoad, y: -350, width: roadWidth, height: 350, fill: this.C.ROAD }));
  134. group.add(new Konva.Line({ points: [0, -350, 0, -35], stroke: this.C.YELLOW, strokeWidth: 3 }));
  135. group.add(new Konva.Path({ data: `M -160 -350 L -160 -30 Q -160 0 -180 0`, stroke: this.C.YELLOW, strokeWidth: 3 }));
  136. group.add(new Konva.Path({ data: `M 160 -350 L 160 -30 Q 160 0 180 0`, stroke: this.C.YELLOW, strokeWidth: 3 }));
  137. group.add(new Konva.Line({ points: [-160, -35, 0, -35], stroke: this.C.WHITE, strokeWidth: 4 }));
  138. for (let i = 1; i < 4; i++) {
  139. let ox = i * laneWidth;
  140. group.add(new Konva.Line({ points: [-ox, -35, -ox, -120], stroke: this.C.WHITE, strokeWidth: 2 }));
  141. group.add(new Konva.Line({ points: [-ox, -120, -ox, -350], stroke: this.C.WHITE, strokeWidth: 2, dash: [15, 15] }));
  142. group.add(new Konva.Line({ points: [ox, -35, ox, -350], stroke: this.C.WHITE, strokeWidth: 2, dash: [15, 15] }));
  143. }
  144. // 2. 信号灯带容器
  145. const lightGroup = new Konva.Group();
  146. const rectOpts = { y: -16, width: 8, height: 24, cornerRadius: 2, offsetX: 4, offsetY: 12 };
  147. for (let lx = -148; lx <= -20; lx += 16) lightGroup.add(new Konva.Rect({ x: lx, ...rectOpts }));
  148. for (let rx = 20; rx <= 148; rx += 16) lightGroup.add(new Konva.Rect({ x: rx, ...rectOpts }));
  149. group.add(lightGroup);
  150. group.lightGroup = lightGroup;
  151. // 预留动态挂载点
  152. group.arrowNodes = { 0: null, 1: null, 2: null, 3: null };
  153. group.cameraNode = null;
  154. return group;
  155. },
  156. createCenterPanel(center) {
  157. const panelGroup = new Konva.Group({ x: center - 80, y: center - 45 });
  158. panelGroup.add(new Konva.Rect({ width: 160, height: 90, fill: this.C.PANEL_BG, cornerRadius: 8 }));
  159. const labelFont = { fontSize: 18, fontFamily: 'monospace', fontStyle: 'bold', fill: this.C.WHITE };
  160. const valueFont = { fontSize: 28, fontFamily: 'monospace', fontStyle: 'bold' };
  161. this.panelNodes.nsLabel = new Konva.Text({ ...labelFont, x: 15, y: 22, text: '相位-:' });
  162. this.panelNodes.nsVal = new Konva.Text({ ...valueFont, x: 90, y: 15, text: '--', fill: this.C.SIGNAL_GREEN });
  163. this.panelNodes.ewLabel = new Konva.Text({ ...labelFont, x: 15, y: 55, text: '相位-:' });
  164. this.panelNodes.ewVal = new Konva.Text({ ...valueFont, x: 90, y: 48, text: '--', fill: this.C.SIGNAL_GREEN });
  165. panelGroup.add(this.panelNodes.nsLabel, this.panelNodes.nsVal, this.panelNodes.ewLabel, this.panelNodes.ewVal);
  166. this.layer.add(panelGroup);
  167. },
  168. // ================= 图标工厂函数 =================
  169. createArrowIcon(type, x, y, color = this.C.WHITE) {
  170. const group = new Konva.Group({ x, y, scaleX: 0.65, scaleY: 0.65 });
  171. group.add(new Konva.Circle({ x: 0, y: -35, radius: 3, fill: color, name: 'colorFill' }));
  172. let pathData = '';
  173. if (type === 'S') pathData = 'M 0 -35 L 0 0 M -7 -10 L 0 0 L 7 -10';
  174. else if (type === 'L') pathData = 'M 0 -35 L 0 -15 Q 0 0 15 0 M 5 -7 L 15 0 L 5 7';
  175. else if (type === 'R') pathData = 'M 0 -35 L 0 -15 Q 0 0 -15 0 M -5 -7 L -15 0 L -5 7';
  176. else if (type === 'U') pathData = 'M 0 -35 L 0 -15 Q 0 0 14 0 Q 28 0 28 -15 L 28 -25 M 21 -18 L 28 -25 L 35 -18';
  177. group.add(new Konva.Path({ data: pathData, stroke: color, strokeWidth: 3, lineCap: 'round', lineJoin: 'round', name: 'colorStroke' }));
  178. return group;
  179. },
  180. createCameraIcon(type, x, y) {
  181. const group = new Konva.Group({ x, y });
  182. if (type === 1) {
  183. group.add(new Konva.Line({ points: [-16, -30, 16, -30], stroke: this.C.BLUE, strokeWidth: 2.5, lineCap: 'round' }));
  184. group.add(new Konva.Line({ points: [0, -30, 0, -10], stroke: this.C.BLUE, strokeWidth: 2.5 }));
  185. const body = new Konva.Group({ y: -10, rotation: 15 });
  186. body.add(new Konva.Rect({ x: -16, y: -8, width: 32, height: 16, stroke: this.C.BLUE, strokeWidth: 2.5, cornerRadius: 2 }));
  187. body.add(new Konva.Rect({ x: 16, y: -4, width: 6, height: 8, stroke: this.C.BLUE, strokeWidth: 2.5, cornerRadius: 1 }));
  188. group.add(body);
  189. } else if (type === 2) {
  190. group.add(new Konva.Rect({ x: -14, y: -24, width: 28, height: 24, stroke: this.C.BLUE, strokeWidth: 2.5, cornerRadius: 6 }));
  191. group.add(new Konva.Circle({ x: 0, y: -12, radius: 5, stroke: this.C.BLUE, strokeWidth: 2.5 }));
  192. group.add(new Konva.Line({ points: [-10, 0, 10, 0], stroke: this.C.BLUE, strokeWidth: 2.5, lineCap: 'round' }));
  193. group.add(new Konva.Line({ points: [0, 0, 0, 6], stroke: this.C.BLUE, strokeWidth: 2.5 }));
  194. }
  195. return group;
  196. },
  197. // ================= 核心更新逻辑 =================
  198. // 渲染或更新静态设备(摄像头、箭头配置)
  199. renderStaticConfig() {
  200. const config = this.mapData.armsConfig;
  201. if (!config) return;
  202. Object.keys(config).forEach(dir => {
  203. const armData = config[dir];
  204. const armNode = this.armsNodes[dir];
  205. // 挂载摄像头
  206. if (armNode.cameraNode) armNode.cameraNode.destroy();
  207. if (armData.cameraType > 0) {
  208. const cam = this.createCameraIcon(armData.cameraType, -80, -190);
  209. armNode.add(cam);
  210. armNode.cameraNode = cam;
  211. }
  212. // 挂载车道箭头
  213. armData.lanes.forEach((type, index) => {
  214. if (armNode.arrowNodes[index]) armNode.arrowNodes[index].destroy();
  215. if (type) {
  216. const lx = -20 - (index * this.sizeConfig.laneWidth);
  217. const arrow = this.createArrowIcon(type, lx, -80, this.C.WHITE);
  218. armNode.add(arrow);
  219. armNode.arrowNodes[index] = arrow;
  220. }
  221. });
  222. });
  223. this.layer.draw();
  224. },
  225. // 根据实时倒计时和红绿灯更新颜色
  226. updateDynamicSignals() {
  227. const signals = this.mapData.signals;
  228. if (!signals) return;
  229. const nsColor = signals.ns.isGreen ? this.C.SIGNAL_GREEN : this.C.SIGNAL_RED;
  230. const ewColor = signals.ew.isGreen ? this.C.SIGNAL_GREEN : this.C.SIGNAL_RED;
  231. // 辅助函数:染色
  232. const dyeArm = (armNode, color) => {
  233. armNode.lightGroup.getChildren().forEach(r => r.fill(color));
  234. Object.values(armNode.arrowNodes).forEach(arr => {
  235. if (arr) {
  236. arr.findOne('.colorFill').fill(color);
  237. arr.findOne('.colorStroke').stroke(color);
  238. }
  239. });
  240. };
  241. dyeArm(this.armsNodes.N, nsColor);
  242. dyeArm(this.armsNodes.S, nsColor);
  243. dyeArm(this.armsNodes.E, ewColor);
  244. dyeArm(this.armsNodes.W, ewColor);
  245. // 更新中央面板
  246. this.panelNodes.nsLabel.text(`${signals.ns.phaseName}:`);
  247. this.panelNodes.nsVal.text(signals.ns.time.toString().padStart(2, '0')).fill(nsColor);
  248. this.panelNodes.ewLabel.text(`${signals.ew.phaseName}:`);
  249. this.panelNodes.ewVal.text(signals.ew.time.toString().padStart(2, '0')).fill(ewColor);
  250. this.layer.draw();
  251. }
  252. }
  253. };
  254. </script>
  255. <style scoped>
  256. .intersection-map-container {
  257. width: 100%;
  258. height: 100%;
  259. }
  260. </style>