|
|
@@ -1,5 +1,7 @@
|
|
|
<template>
|
|
|
- <div ref="konvaContainer" class="intersection-map-container"></div>
|
|
|
+ <div class="map-wrapper" ref="wrapper">
|
|
|
+ <div class="konva-container" ref="konvaContainer"></div>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
@@ -8,7 +10,6 @@ import Konva from 'konva';
|
|
|
export default {
|
|
|
name: 'IntersectionMap',
|
|
|
props: {
|
|
|
- // 接收父组件传来的路口数据
|
|
|
mapData: {
|
|
|
type: Object,
|
|
|
required: true
|
|
|
@@ -18,17 +19,16 @@ export default {
|
|
|
return {
|
|
|
stage: null,
|
|
|
layer: null,
|
|
|
- armsNodes: {}, // 存储四个方向的道路实例
|
|
|
- panelNodes: {}, // 存储中央面板的文字实例
|
|
|
- // 颜色配置
|
|
|
+ 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,
|
|
|
+ stageSize: 900,
|
|
|
laneWidth: 40,
|
|
|
halfRoad: 160,
|
|
|
roadWidth: 320,
|
|
|
@@ -42,89 +42,44 @@ export default {
|
|
|
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);
|
|
|
+ this.initResizeObserver();
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
- if (this._resizeObserver) {
|
|
|
- this._resizeObserver.disconnect();
|
|
|
+ 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 // 开启深度监听
|
|
|
+ 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
|
|
|
+ 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),
|
|
|
@@ -133,18 +88,50 @@ export default {
|
|
|
};
|
|
|
Object.values(this.armsNodes).forEach(arm => this.layer.add(arm));
|
|
|
|
|
|
- // 创建中央面板
|
|
|
this.createCenterPanel(center);
|
|
|
this.layer.draw();
|
|
|
- this.fitToContainer();
|
|
|
},
|
|
|
|
|
|
- // ================= 创建道路骨架 (内置绘制逻辑) =================
|
|
|
+ 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 });
|
|
|
|
|
|
- // 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 }));
|
|
|
@@ -158,7 +145,6 @@ export default {
|
|
|
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 }));
|
|
|
@@ -166,7 +152,6 @@ export default {
|
|
|
group.add(lightGroup);
|
|
|
group.lightGroup = lightGroup;
|
|
|
|
|
|
- // 预留动态挂载点
|
|
|
group.arrowNodes = { 0: null, 1: null, 2: null, 3: null };
|
|
|
group.cameraNode = null;
|
|
|
|
|
|
@@ -190,7 +175,6 @@ export default {
|
|
|
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' }));
|
|
|
@@ -221,8 +205,6 @@ export default {
|
|
|
return group;
|
|
|
},
|
|
|
|
|
|
- // ================= 核心更新逻辑 =================
|
|
|
- // 渲染或更新静态设备(摄像头、箭头配置)
|
|
|
renderStaticConfig() {
|
|
|
const config = this.mapData.armsConfig;
|
|
|
if (!config) return;
|
|
|
@@ -231,7 +213,6 @@ export default {
|
|
|
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);
|
|
|
@@ -239,7 +220,6 @@ export default {
|
|
|
armNode.cameraNode = cam;
|
|
|
}
|
|
|
|
|
|
- // 挂载车道箭头
|
|
|
armData.lanes.forEach((type, index) => {
|
|
|
if (armNode.arrowNodes[index]) armNode.arrowNodes[index].destroy();
|
|
|
if (type) {
|
|
|
@@ -253,7 +233,6 @@ export default {
|
|
|
this.layer.draw();
|
|
|
},
|
|
|
|
|
|
- // 根据实时倒计时和红绿灯更新颜色
|
|
|
updateDynamicSignals() {
|
|
|
const signals = this.mapData.signals;
|
|
|
if (!signals) return;
|
|
|
@@ -261,7 +240,6 @@ export default {
|
|
|
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 => {
|
|
|
@@ -277,7 +255,6 @@ export default {
|
|
|
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);
|
|
|
|
|
|
@@ -291,8 +268,18 @@ export default {
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.intersection-map-container {
|
|
|
+/* 定义外层包裹容器 */
|
|
|
+.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>
|