基于JS Canvas的客舱座位选择器:点击交互与存储问题及优化咨询
客车/飞机座位选择器优化方案
核心问题拆解
当前通过Canvas实现了动态行列的座位生成,但存在两个核心卡点:
- 无法处理座位的点击选择与状态存储
- 后续需要实现婴儿座标记及基于距离的噪音等级显示
优化实现方案
1. 用数据模型管理座位状态
Canvas本身没有DOM元素,必须通过数据对象记录每个座位的位置、编号、选中状态、是否为婴儿座等信息,彻底将数据与绘制逻辑分离。
首先定义并生成座位数据结构:
// 初始化座位数据数组 let seats = []; const seatSize = window.innerWidth / 10; const seatGap = 1.25; const seatRows = 14; const seatColumns = 4; const aisleOffset = seatSize; // 过道宽度 // 批量生成座位数据 let seatNumber = 1; for (let row = 0; row < seatRows; row++) { for (let col = 0; col < seatColumns; col++) { // 计算座位Canvas坐标(区分过道左右) let x = col < seatColumns / 2 ? col * seatSize + 10 : col * seatSize + aisleOffset + 10; let y = (row * seatSize * seatGap) + (seatSize * 2) + 10; seats.push({ id: seatNumber, row: row + 1, col: col + 1, x, y, width: seatSize, height: seatSize, isSelected: false, isBabySeat: false, noiseLevel: 0 // 后续用于存储噪音等级 }); seatNumber++; } }
2. 实现点击检测与状态更新
给Canvas绑定点击事件,通过坐标匹配找到目标座位,更新状态后触发重绘:
const canvas = document.getElementById("seats"); canvas.addEventListener('click', (e) => { const rect = canvas.getBoundingClientRect(); // 转换为Canvas内部相对坐标 const clickX = e.clientX - rect.left; const clickY = e.clientY - rect.top; // 查找被点击的座位 const clickedSeat = seats.find(seat => { return clickX >= seat.x && clickX <= seat.x + seat.width && clickY >= seat.y && clickY <= seat.y + seat.height; }); if (clickedSeat) { // 切换选中状态 clickedSeat.isSelected = !clickedSeat.isSelected; // 重绘所有座位 renderSeats(); } });
3. 封装渲染函数实现数据同步
把所有绘制逻辑封装成renderSeats函数,每次状态变化时调用,确保视图与数据完全同步:
function renderSeats() { const ctx = canvas.getContext('2d'); // 清空画布 ctx.clearRect(0, 0, canvas.width, canvas.height); // 计算客车尺寸并设置Canvas大小 const busLength = (seatRows * seatSize * seatGap) + (seatSize * 2) - seatSize * (seatGap - 1); const busWidth = (seatColumns * seatSize) + aisleOffset; canvas.width = busWidth + 20; canvas.height = busLength + 20; // 绘制客车边框与Front标识 ctx.font = 0.15 * busWidth + "px Arial"; ctx.fillStyle = "grey"; ctx.textAlign = "center"; ctx.fillText("Front", busWidth / 2 + 5, seatSize * 1.5); ctx.strokeStyle = "gray"; ctx.lineWidth = "2"; ctx.beginPath(); ctx.roundRect(10, 10, busWidth, busLength, [5]); ctx.stroke(); // 逐个绘制座位 seats.forEach(seat => { // 设置座位样式:选中状态用蓝色填充,婴儿座用黄色填充 ctx.strokeStyle = "gray"; ctx.lineWidth = "2"; if (seat.isSelected) { ctx.fillStyle = "#4285F4"; ctx.fillRect(seat.x, seat.y, seat.width, seat.height); } if (seat.isBabySeat) { ctx.fillStyle = "#FBBC05"; ctx.fillRect(seat.x, seat.y, seat.width, seat.height); } ctx.beginPath(); ctx.roundRect(seat.x, seat.y, seat.width, seat.height, [5]); ctx.stroke(); // 绘制座位号(选中/婴儿座用白色文字) ctx.font = seatSize / 2 + "px Arial"; ctx.fillStyle = seat.isSelected || seat.isBabySeat ? "white" : "grey"; ctx.textAlign = "center"; ctx.fillText(seat.id, seat.x + seat.width/2, seat.y + seat.height/1.6); }); } // 初始化渲染 renderSeats();
4. 实现婴儿座噪音等级计算
根据选中的婴儿座,计算其他座位的曼哈顿距离并划分噪音等级(距离越近噪音越高):
// 标记婴儿座并计算所有座位噪音等级 function setBabySeat(seatId) { // 重置所有婴儿座标记 seats.forEach(s => s.isBabySeat = false); const babySeat = seats.find(s => s.id === seatId); if (babySeat) { babySeat.isBabySeat = true; // 计算每个座位与婴儿座的曼哈顿距离 seats.forEach(seat => { const distance = Math.abs(seat.row - babySeat.row) + Math.abs(seat.col - babySeat.col); // 划分噪音等级:0=低,1=中,2=高 if (distance <= 1) seat.noiseLevel = 2; else if (distance <= 3) seat.noiseLevel = 1; else seat.noiseLevel = 0; }); renderSeats(); } } // 示例:标记ID为5的座位为婴儿座 // setBabySeat(5);
方案优势
- 数据驱动视图:所有座位状态统一存储在数据模型中,避免Canvas绘制与状态脱节
- 响应式适配:窗口大小变化时,重新计算
seatSize后调用renderSeats()即可更新视图 - 扩展性强:后续可轻松添加已售座位标记、价格显示、座位筛选等功能
内容的提问来源于stack exchange,提问作者TinyOctopus
相关产品推荐
相关产品推荐

