IntersectionMap.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. <template>
  2. <div class="map-wrapper" ref="wrapper">
  3. <div class="konva-container" ref="konvaContainer"></div>
  4. </div>
  5. </template>
  6. <script>
  7. import Konva from 'konva';
  8. export default {
  9. name: 'IntersectionMap',
  10. props: {
  11. mapData: {
  12. type: Object,
  13. required: true
  14. }
  15. },
  16. data() {
  17. return {
  18. stage: null,
  19. layer: null,
  20. armsNodes: {},
  21. panelNodes: {},
  22. resizeObserver: null,
  23. C: {
  24. BG: '#212842', ROAD: '#3d3938', YELLOW: '#D9A73D', WHITE: '#E0E0E0',
  25. SIGNAL_RED: '#FF5252', SIGNAL_GREEN: '#8DF582',
  26. PANEL_BG: 'rgba(30, 30, 40, 0.85)', BLUE: '#448AFF'
  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. this.initResizeObserver();
  44. },
  45. beforeDestroy() {
  46. if (this.resizeObserver) {
  47. this.resizeObserver.disconnect();
  48. }
  49. if (this.stage) {
  50. this.stage.destroy();
  51. }
  52. },
  53. watch: {
  54. mapData: {
  55. handler(newData, oldData) {
  56. if (!newData) return;
  57. if (!oldData || JSON.stringify(newData.armsConfig) !== JSON.stringify(oldData.armsConfig)) {
  58. this.renderStaticConfig();
  59. }
  60. this.updateDynamicSignals();
  61. },
  62. deep: true
  63. }
  64. },
  65. methods: {
  66. initKonvaStage() {
  67. const { stageSize, halfRoad, roadWidth } = this.sizeConfig;
  68. const center = stageSize / 2;
  69. this.stage = new Konva.Stage({
  70. container: this.$refs.konvaContainer,
  71. width: stageSize,
  72. height: stageSize
  73. });
  74. this.layer = new Konva.Layer();
  75. this.stage.add(this.layer);
  76. this.layer.add(new Konva.Rect({ width: stageSize, height: stageSize, fill: this.C.BG }));
  77. this.layer.add(new Konva.Rect({ x: center - halfRoad, y: center - halfRoad, width: roadWidth, height: roadWidth, fill: this.C.ROAD }));
  78. this.armsNodes = {
  79. N: this.createRoadArm(center, center - halfRoad, 0),
  80. E: this.createRoadArm(center + halfRoad, center, 90),
  81. S: this.createRoadArm(center, center + halfRoad, 180),
  82. W: this.createRoadArm(center - halfRoad, center, 270)
  83. };
  84. Object.values(this.armsNodes).forEach(arm => this.layer.add(arm));
  85. this.createCenterPanel(center);
  86. this.layer.draw();
  87. },
  88. initResizeObserver() {
  89. this.resizeObserver = new ResizeObserver(() => {
  90. // 使用 requestAnimationFrame 防抖,让渲染更平滑
  91. window.requestAnimationFrame(() => {
  92. this.handleResize();
  93. });
  94. });
  95. if (this.$refs.wrapper) {
  96. this.resizeObserver.observe(this.$refs.wrapper);
  97. }
  98. },
  99. handleResize() {
  100. if (!this.stage || !this.$refs.wrapper) return;
  101. // 现在拿到的是纯粹的父容器尺寸,绝对不会被 Canvas 撑大
  102. const containerWidth = this.$refs.wrapper.clientWidth;
  103. const containerHeight = this.$refs.wrapper.clientHeight;
  104. // 如果容器被隐藏或没尺寸,则跳过
  105. if (containerWidth === 0 || containerHeight === 0) return;
  106. const scaleX = containerWidth / this.sizeConfig.stageSize;
  107. const scaleY = containerHeight / this.sizeConfig.stageSize;
  108. // 取宽和高中最小的缩放比,保证图形能完整显示在容器内且不变形
  109. const scale = Math.min(scaleX, scaleY);
  110. // 缩放舞台物理尺寸
  111. this.stage.width(this.sizeConfig.stageSize * scale);
  112. this.stage.height(this.sizeConfig.stageSize * scale);
  113. // 缩放内部虚拟坐标系
  114. this.stage.scale({ x: scale, y: scale });
  115. },
  116. createRoadArm(x, y, rotation) {
  117. const { halfRoad, roadWidth, laneWidth } = this.sizeConfig;
  118. const group = new Konva.Group({ x, y, rotation });
  119. group.add(new Konva.Rect({ x: -halfRoad, y: -350, width: roadWidth, height: 350, fill: this.C.ROAD }));
  120. group.add(new Konva.Line({ points: [0, -350, 0, -35], stroke: this.C.YELLOW, strokeWidth: 3 }));
  121. group.add(new Konva.Path({ data: `M -160 -350 L -160 -30 Q -160 0 -180 0`, stroke: this.C.YELLOW, strokeWidth: 3 }));
  122. group.add(new Konva.Path({ data: `M 160 -350 L 160 -30 Q 160 0 180 0`, stroke: this.C.YELLOW, strokeWidth: 3 }));
  123. group.add(new Konva.Line({ points: [-160, -35, 0, -35], stroke: this.C.WHITE, strokeWidth: 4 }));
  124. for (let i = 1; i < 4; i++) {
  125. let ox = i * laneWidth;
  126. group.add(new Konva.Line({ points: [-ox, -35, -ox, -120], stroke: this.C.WHITE, strokeWidth: 2 }));
  127. group.add(new Konva.Line({ points: [-ox, -120, -ox, -350], stroke: this.C.WHITE, strokeWidth: 2, dash: [15, 15] }));
  128. group.add(new Konva.Line({ points: [ox, -35, ox, -350], stroke: this.C.WHITE, strokeWidth: 2, dash: [15, 15] }));
  129. }
  130. const lightGroup = new Konva.Group();
  131. const rectOpts = { y: -16, width: 8, height: 24, cornerRadius: 2, offsetX: 4, offsetY: 12 };
  132. for (let lx = -148; lx <= -20; lx += 16) lightGroup.add(new Konva.Rect({ x: lx, ...rectOpts }));
  133. for (let rx = 20; rx <= 148; rx += 16) lightGroup.add(new Konva.Rect({ x: rx, ...rectOpts }));
  134. group.add(lightGroup);
  135. group.lightGroup = lightGroup;
  136. group.arrowNodes = { 0: null, 1: null, 2: null, 3: null };
  137. group.cameraNode = null;
  138. return group;
  139. },
  140. createCenterPanel(center) {
  141. const panelGroup = new Konva.Group({ x: center - 80, y: center - 45 });
  142. panelGroup.add(new Konva.Rect({ width: 160, height: 90, fill: this.C.PANEL_BG, cornerRadius: 8 }));
  143. const labelFont = { fontSize: 18, fontFamily: 'monospace', fontStyle: 'bold', fill: this.C.WHITE };
  144. const valueFont = { fontSize: 28, fontFamily: 'monospace', fontStyle: 'bold' };
  145. this.panelNodes.nsLabel = new Konva.Text({ ...labelFont, x: 15, y: 22, text: '相位-:' });
  146. this.panelNodes.nsVal = new Konva.Text({ ...valueFont, x: 90, y: 15, text: '--', fill: this.C.SIGNAL_GREEN });
  147. this.panelNodes.ewLabel = new Konva.Text({ ...labelFont, x: 15, y: 55, text: '相位-:' });
  148. this.panelNodes.ewVal = new Konva.Text({ ...valueFont, x: 90, y: 48, text: '--', fill: this.C.SIGNAL_GREEN });
  149. panelGroup.add(this.panelNodes.nsLabel, this.panelNodes.nsVal, this.panelNodes.ewLabel, this.panelNodes.ewVal);
  150. this.layer.add(panelGroup);
  151. },
  152. createArrowIcon(type, x, y, color = this.C.WHITE) {
  153. const group = new Konva.Group({ x, y, scaleX: 0.65, scaleY: 0.65 });
  154. group.add(new Konva.Circle({ x: 0, y: -35, radius: 3, fill: color, name: 'colorFill' }));
  155. let pathData = '';
  156. if (type === 'S') pathData = 'M 0 -35 L 0 0 M -7 -10 L 0 0 L 7 -10';
  157. 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';
  158. 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';
  159. 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';
  160. group.add(new Konva.Path({ data: pathData, stroke: color, strokeWidth: 3, lineCap: 'round', lineJoin: 'round', name: 'colorStroke' }));
  161. return group;
  162. },
  163. createCameraIcon(type, x, y) {
  164. const group = new Konva.Group({ x, y });
  165. if (type === 1) {
  166. group.add(new Konva.Line({ points: [-16, -30, 16, -30], stroke: this.C.BLUE, strokeWidth: 2.5, lineCap: 'round' }));
  167. group.add(new Konva.Line({ points: [0, -30, 0, -10], stroke: this.C.BLUE, strokeWidth: 2.5 }));
  168. const body = new Konva.Group({ y: -10, rotation: 15 });
  169. body.add(new Konva.Rect({ x: -16, y: -8, width: 32, height: 16, stroke: this.C.BLUE, strokeWidth: 2.5, cornerRadius: 2 }));
  170. body.add(new Konva.Rect({ x: 16, y: -4, width: 6, height: 8, stroke: this.C.BLUE, strokeWidth: 2.5, cornerRadius: 1 }));
  171. group.add(body);
  172. } else if (type === 2) {
  173. group.add(new Konva.Rect({ x: -14, y: -24, width: 28, height: 24, stroke: this.C.BLUE, strokeWidth: 2.5, cornerRadius: 6 }));
  174. group.add(new Konva.Circle({ x: 0, y: -12, radius: 5, stroke: this.C.BLUE, strokeWidth: 2.5 }));
  175. group.add(new Konva.Line({ points: [-10, 0, 10, 0], stroke: this.C.BLUE, strokeWidth: 2.5, lineCap: 'round' }));
  176. group.add(new Konva.Line({ points: [0, 0, 0, 6], stroke: this.C.BLUE, strokeWidth: 2.5 }));
  177. }
  178. return group;
  179. },
  180. renderStaticConfig() {
  181. const config = this.mapData.armsConfig;
  182. if (!config) return;
  183. Object.keys(config).forEach(dir => {
  184. const armData = config[dir];
  185. const armNode = this.armsNodes[dir];
  186. if (armNode.cameraNode) armNode.cameraNode.destroy();
  187. if (armData.cameraType > 0) {
  188. const cam = this.createCameraIcon(armData.cameraType, -80, -190);
  189. armNode.add(cam);
  190. armNode.cameraNode = cam;
  191. }
  192. armData.lanes.forEach((type, index) => {
  193. if (armNode.arrowNodes[index]) armNode.arrowNodes[index].destroy();
  194. if (type) {
  195. const lx = -20 - (index * this.sizeConfig.laneWidth);
  196. const arrow = this.createArrowIcon(type, lx, -80, this.C.WHITE);
  197. armNode.add(arrow);
  198. armNode.arrowNodes[index] = arrow;
  199. }
  200. });
  201. });
  202. this.layer.draw();
  203. },
  204. updateDynamicSignals() {
  205. const signals = this.mapData.signals;
  206. if (!signals) return;
  207. const nsColor = signals.ns.isGreen ? this.C.SIGNAL_GREEN : this.C.SIGNAL_RED;
  208. const ewColor = signals.ew.isGreen ? this.C.SIGNAL_GREEN : this.C.SIGNAL_RED;
  209. const dyeArm = (armNode, color) => {
  210. armNode.lightGroup.getChildren().forEach(r => r.fill(color));
  211. Object.values(armNode.arrowNodes).forEach(arr => {
  212. if (arr) {
  213. arr.findOne('.colorFill').fill(color);
  214. arr.findOne('.colorStroke').stroke(color);
  215. }
  216. });
  217. };
  218. dyeArm(this.armsNodes.N, nsColor);
  219. dyeArm(this.armsNodes.S, nsColor);
  220. dyeArm(this.armsNodes.E, ewColor);
  221. dyeArm(this.armsNodes.W, ewColor);
  222. this.panelNodes.nsLabel.text(`${signals.ns.phaseName}:`);
  223. this.panelNodes.nsVal.text(signals.ns.time.toString().padStart(2, '0')).fill(nsColor);
  224. this.panelNodes.ewLabel.text(`${signals.ew.phaseName}:`);
  225. this.panelNodes.ewVal.text(signals.ew.time.toString().padStart(2, '0')).fill(ewColor);
  226. this.layer.draw();
  227. }
  228. }
  229. };
  230. </script>
  231. <style scoped>
  232. /* 定义外层包裹容器 */
  233. .map-wrapper {
  234. width: 100%;
  235. height: 100%;
  236. overflow: hidden;
  237. background-color: #212842;
  238. position: relative; /* 让内部绝对定位元素以此为参考 */
  239. }
  240. .konva-container {
  241. position: absolute; /* 必须有,让 Canvas 脱离文档流 */
  242. top: 50%;
  243. left: 50%;
  244. transform: translate(-50%, -50%);
  245. }
  246. </style>