| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- <template>
- <div class="map-wrapper" ref="wrapper">
- <div class="konva-container" ref="konvaContainer"></div>
- </div>
- </template>
- <script>
- import Konva from 'konva';
- export default {
- name: 'IntersectionMap',
- props: {
- mapData: {
- type: Object,
- required: true
- }
- },
- data() {
- return {
- stage: null,
- layer: null,
- armsNodes: {},
- panelNodes: {},
- resizeObserver: null,
- C: {
- BG: '#212842', ROAD: '#3d3938', YELLOW: '#D9A73D', WHITE: '#E0E0E0',
- SIGNAL_RED: '#FF5252', SIGNAL_GREEN: '#8DF582',
- PANEL_BG: 'rgba(30, 30, 40, 0.85)', BLUE: '#448AFF'
- },
- sizeConfig: {
- stageSize: 900,
- laneWidth: 40,
- halfRoad: 160,
- roadWidth: 320,
- armLength: 350
- }
- };
- },
- mounted() {
- this.initKonvaStage();
- if (this.mapData && Object.keys(this.mapData).length > 0) {
- this.renderStaticConfig();
- this.updateDynamicSignals();
- }
- this.initResizeObserver();
- },
- beforeDestroy() {
- if (this.resizeObserver) {
- this.resizeObserver.disconnect();
- }
- if (this.stage) {
- this.stage.destroy();
- }
- },
- watch: {
- mapData: {
- handler(newData, oldData) {
- if (!newData) return;
- if (!oldData || JSON.stringify(newData.armsConfig) !== JSON.stringify(oldData.armsConfig)) {
- this.renderStaticConfig();
- }
- this.updateDynamicSignals();
- },
- deep: true
- }
- },
- methods: {
- initKonvaStage() {
- const { stageSize, halfRoad, roadWidth } = this.sizeConfig;
- const center = stageSize / 2;
- this.stage = new Konva.Stage({
- container: this.$refs.konvaContainer,
- width: stageSize,
- height: stageSize
- });
- this.layer = new Konva.Layer();
- this.stage.add(this.layer);
- this.layer.add(new Konva.Rect({ width: stageSize, height: stageSize, fill: this.C.BG }));
- this.layer.add(new Konva.Rect({ x: center - halfRoad, y: center - halfRoad, width: roadWidth, height: roadWidth, fill: this.C.ROAD }));
- this.armsNodes = {
- N: this.createRoadArm(center, center - halfRoad, 0),
- E: this.createRoadArm(center + halfRoad, center, 90),
- S: this.createRoadArm(center, center + halfRoad, 180),
- W: this.createRoadArm(center - halfRoad, center, 270)
- };
- Object.values(this.armsNodes).forEach(arm => this.layer.add(arm));
- this.createCenterPanel(center);
- this.layer.draw();
- },
- initResizeObserver() {
- this.resizeObserver = new ResizeObserver(() => {
- // 使用 requestAnimationFrame 防抖,让渲染更平滑
- window.requestAnimationFrame(() => {
- this.handleResize();
- });
- });
- if (this.$refs.wrapper) {
- this.resizeObserver.observe(this.$refs.wrapper);
- }
- },
- handleResize() {
- if (!this.stage || !this.$refs.wrapper) return;
- // 现在拿到的是纯粹的父容器尺寸,绝对不会被 Canvas 撑大
- const containerWidth = this.$refs.wrapper.clientWidth;
- const containerHeight = this.$refs.wrapper.clientHeight;
- // 如果容器被隐藏或没尺寸,则跳过
- if (containerWidth === 0 || containerHeight === 0) return;
- const scaleX = containerWidth / this.sizeConfig.stageSize;
- const scaleY = containerHeight / this.sizeConfig.stageSize;
-
- // 取宽和高中最小的缩放比,保证图形能完整显示在容器内且不变形
- const scale = Math.min(scaleX, scaleY);
- // 缩放舞台物理尺寸
- this.stage.width(this.sizeConfig.stageSize * scale);
- this.stage.height(this.sizeConfig.stageSize * scale);
-
- // 缩放内部虚拟坐标系
- this.stage.scale({ x: scale, y: scale });
- },
- createRoadArm(x, y, rotation) {
- const { halfRoad, roadWidth, laneWidth } = this.sizeConfig;
- const group = new Konva.Group({ x, y, rotation });
-
- group.add(new Konva.Rect({ x: -halfRoad, y: -350, width: roadWidth, height: 350, fill: this.C.ROAD }));
- group.add(new Konva.Line({ points: [0, -350, 0, -35], stroke: this.C.YELLOW, strokeWidth: 3 }));
- group.add(new Konva.Path({ data: `M -160 -350 L -160 -30 Q -160 0 -180 0`, stroke: this.C.YELLOW, strokeWidth: 3 }));
- group.add(new Konva.Path({ data: `M 160 -350 L 160 -30 Q 160 0 180 0`, stroke: this.C.YELLOW, strokeWidth: 3 }));
- group.add(new Konva.Line({ points: [-160, -35, 0, -35], stroke: this.C.WHITE, strokeWidth: 4 }));
-
- for (let i = 1; i < 4; i++) {
- let ox = i * laneWidth;
- group.add(new Konva.Line({ points: [-ox, -35, -ox, -120], stroke: this.C.WHITE, strokeWidth: 2 }));
- group.add(new Konva.Line({ points: [-ox, -120, -ox, -350], stroke: this.C.WHITE, strokeWidth: 2, dash: [15, 15] }));
- group.add(new Konva.Line({ points: [ox, -35, ox, -350], stroke: this.C.WHITE, strokeWidth: 2, dash: [15, 15] }));
- }
-
- const lightGroup = new Konva.Group();
- const rectOpts = { y: -16, width: 8, height: 24, cornerRadius: 2, offsetX: 4, offsetY: 12 };
- for (let lx = -148; lx <= -20; lx += 16) lightGroup.add(new Konva.Rect({ x: lx, ...rectOpts }));
- for (let rx = 20; rx <= 148; rx += 16) lightGroup.add(new Konva.Rect({ x: rx, ...rectOpts }));
- group.add(lightGroup);
- group.lightGroup = lightGroup;
- group.arrowNodes = { 0: null, 1: null, 2: null, 3: null };
- group.cameraNode = null;
- return group;
- },
- createCenterPanel(center) {
- const panelGroup = new Konva.Group({ x: center - 80, y: center - 45 });
- panelGroup.add(new Konva.Rect({ width: 160, height: 90, fill: this.C.PANEL_BG, cornerRadius: 8 }));
-
- const labelFont = { fontSize: 18, fontFamily: 'monospace', fontStyle: 'bold', fill: this.C.WHITE };
- const valueFont = { fontSize: 28, fontFamily: 'monospace', fontStyle: 'bold' };
-
- this.panelNodes.nsLabel = new Konva.Text({ ...labelFont, x: 15, y: 22, text: '相位-:' });
- this.panelNodes.nsVal = new Konva.Text({ ...valueFont, x: 90, y: 15, text: '--', fill: this.C.SIGNAL_GREEN });
-
- this.panelNodes.ewLabel = new Konva.Text({ ...labelFont, x: 15, y: 55, text: '相位-:' });
- this.panelNodes.ewVal = new Konva.Text({ ...valueFont, x: 90, y: 48, text: '--', fill: this.C.SIGNAL_GREEN });
-
- panelGroup.add(this.panelNodes.nsLabel, this.panelNodes.nsVal, this.panelNodes.ewLabel, this.panelNodes.ewVal);
- this.layer.add(panelGroup);
- },
- createArrowIcon(type, x, y, color = this.C.WHITE) {
- const group = new Konva.Group({ x, y, scaleX: 0.65, scaleY: 0.65 });
- group.add(new Konva.Circle({ x: 0, y: -35, radius: 3, fill: color, name: 'colorFill' }));
- let pathData = '';
- if (type === 'S') pathData = 'M 0 -35 L 0 0 M -7 -10 L 0 0 L 7 -10';
- 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';
- 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';
- 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';
- group.add(new Konva.Path({ data: pathData, stroke: color, strokeWidth: 3, lineCap: 'round', lineJoin: 'round', name: 'colorStroke' }));
- return group;
- },
- createCameraIcon(type, x, y) {
- const group = new Konva.Group({ x, y });
- if (type === 1) {
- group.add(new Konva.Line({ points: [-16, -30, 16, -30], stroke: this.C.BLUE, strokeWidth: 2.5, lineCap: 'round' }));
- group.add(new Konva.Line({ points: [0, -30, 0, -10], stroke: this.C.BLUE, strokeWidth: 2.5 }));
- const body = new Konva.Group({ y: -10, rotation: 15 });
- body.add(new Konva.Rect({ x: -16, y: -8, width: 32, height: 16, stroke: this.C.BLUE, strokeWidth: 2.5, cornerRadius: 2 }));
- body.add(new Konva.Rect({ x: 16, y: -4, width: 6, height: 8, stroke: this.C.BLUE, strokeWidth: 2.5, cornerRadius: 1 }));
- group.add(body);
- } else if (type === 2) {
- group.add(new Konva.Rect({ x: -14, y: -24, width: 28, height: 24, stroke: this.C.BLUE, strokeWidth: 2.5, cornerRadius: 6 }));
- group.add(new Konva.Circle({ x: 0, y: -12, radius: 5, stroke: this.C.BLUE, strokeWidth: 2.5 }));
- group.add(new Konva.Line({ points: [-10, 0, 10, 0], stroke: this.C.BLUE, strokeWidth: 2.5, lineCap: 'round' }));
- group.add(new Konva.Line({ points: [0, 0, 0, 6], stroke: this.C.BLUE, strokeWidth: 2.5 }));
- }
- return group;
- },
- renderStaticConfig() {
- const config = this.mapData.armsConfig;
- if (!config) return;
- Object.keys(config).forEach(dir => {
- const armData = config[dir];
- const armNode = this.armsNodes[dir];
- if (armNode.cameraNode) armNode.cameraNode.destroy();
- if (armData.cameraType > 0) {
- const cam = this.createCameraIcon(armData.cameraType, -80, -190);
- armNode.add(cam);
- armNode.cameraNode = cam;
- }
- armData.lanes.forEach((type, index) => {
- if (armNode.arrowNodes[index]) armNode.arrowNodes[index].destroy();
- if (type) {
- const lx = -20 - (index * this.sizeConfig.laneWidth);
- const arrow = this.createArrowIcon(type, lx, -80, this.C.WHITE);
- armNode.add(arrow);
- armNode.arrowNodes[index] = arrow;
- }
- });
- });
- this.layer.draw();
- },
- updateDynamicSignals() {
- const signals = this.mapData.signals;
- if (!signals) return;
- const nsColor = signals.ns.isGreen ? this.C.SIGNAL_GREEN : this.C.SIGNAL_RED;
- const ewColor = signals.ew.isGreen ? this.C.SIGNAL_GREEN : this.C.SIGNAL_RED;
- const dyeArm = (armNode, color) => {
- armNode.lightGroup.getChildren().forEach(r => r.fill(color));
- Object.values(armNode.arrowNodes).forEach(arr => {
- if (arr) {
- arr.findOne('.colorFill').fill(color);
- arr.findOne('.colorStroke').stroke(color);
- }
- });
- };
- dyeArm(this.armsNodes.N, nsColor);
- dyeArm(this.armsNodes.S, nsColor);
- dyeArm(this.armsNodes.E, ewColor);
- dyeArm(this.armsNodes.W, ewColor);
- this.panelNodes.nsLabel.text(`${signals.ns.phaseName}:`);
- this.panelNodes.nsVal.text(signals.ns.time.toString().padStart(2, '0')).fill(nsColor);
-
- this.panelNodes.ewLabel.text(`${signals.ew.phaseName}:`);
- this.panelNodes.ewVal.text(signals.ew.time.toString().padStart(2, '0')).fill(ewColor);
- this.layer.draw();
- }
- }
- };
- </script>
- <style scoped>
- /* 定义外层包裹容器 */
- .map-wrapper {
- width: 100%;
- height: 100%;
- overflow: hidden;
- background-color: #212842;
- position: relative; /* 让内部绝对定位元素以此为参考 */
- }
- .konva-container {
- position: absolute; /* 必须有,让 Canvas 脱离文档流 */
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- }
- </style>
|