| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- <template>
- <div ref="konvaContainer" class="intersection-map-container"></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: {}, // 存储中央面板的文字实例
- // 颜色配置
- 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._roaPending = false;
- this._resizeObserver = new ResizeObserver(() => {
- if (!this._roaPending) {
- this._roaPending = true;
- requestAnimationFrame(() => {
- this._roaPending = false;
- this.fitToContainer();
- });
- }
- });
- this._resizeObserver.observe(this.$refs.konvaContainer);
- },
- 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: {
- // ================= 自适应缩放 =================
- fitToContainer() {
- if (!this.stage || !this.$refs.konvaContainer) return;
- const container = this.$refs.konvaContainer;
- const containerWidth = container.clientWidth;
- const containerHeight = container.clientHeight;
- if (containerWidth === 0 || containerHeight === 0) return;
- const designSize = this.sizeConfig.stageSize;
- const scale = Math.min(containerWidth / designSize, containerHeight / designSize);
- this.stage.width(containerWidth);
- this.stage.height(containerHeight);
- const offsetX = (containerWidth - designSize * scale) / 2;
- const offsetY = (containerHeight - designSize * scale) / 2;
- this.layer.scale({ x: scale, y: scale });
- this.layer.position({ x: offsetX, y: offsetY });
- this.layer.draw();
- },
- // ================= 初始化画布 =================
- initKonvaStage() {
- const { stageSize, halfRoad, roadWidth } = this.sizeConfig;
- const center = stageSize / 2;
- const container = this.$refs.konvaContainer;
- const initWidth = container.clientWidth || stageSize;
- const initHeight = container.clientHeight || stageSize;
- this.stage = new Konva.Stage({
- container: this.$refs.konvaContainer,
- width: initWidth,
- height: initHeight
- });
- 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();
- this.fitToContainer();
- },
- // ================= 创建道路骨架 (内置绘制逻辑) =================
- createRoadArm(x, y, rotation) {
- const { halfRoad, roadWidth, laneWidth } = this.sizeConfig;
- const group = new Konva.Group({ x, y, rotation });
-
- // 1. 路面与线条
- 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] }));
- }
-
- // 2. 信号灯带容器
- 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>
- .intersection-map-container {
- width: 100%;
- height: 100%;
- }
- </style>
|