ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于JavaScript的宁波地铁网页设计:数据建模与换乘查询实战

基于JavaScript的宁波地铁网页设计:数据建模与换乘查询实战 简介一套基于JavaScript的宁波地铁网页设计源码面向Web前端初学者与网页设计人员用于练习移动端信息展示页面的搭建和动态交互开发。压缩包共包含44个文件其中15个JavaScript文件承载线路查询、站点信息更新等核心交互逻辑15张PNG图片提供线路图、站点图标与界面装饰素材12个CSS样式文件负责页面布局、配色和响应式适配另有HTML入口文件与说明文档整体约493KB模块划分清晰便于按功能快速查阅。目前已有92人学习下载适合作为综合运用HTML、CSS、JavaScript的前端练手案例。通过这套源码可完整看到页面路由、组件划分和状态管理思路也能参考移动端适配、数据展示及交互反馈的具体写法对理解和复刻地铁信息类网页有很大帮助。1. 基于JavaScript的宁波地铁网页设计源码先别急着画图拿到“基于JavaScript的宁波地铁网页设计源码”这个标题很多人第一反应是去找现成的图表库把线路图画得越炫越好。但实际做一次就会发现地铁网页真正难的不是绘制而是把站点、线路、换乘关系组织成JavaScript能高效查询的数据结构。站点坐标从哪里来最少换乘怎么算放大之后点击为什么命中错位这些问题都和数据模型直接相关。这篇文章会从零搭一个可运行的原生JavaScript版本覆盖数据建模、Canvas渲染、BFS/Dijkstra换乘查询并解决HBuilderX里常见的模块加载报错。适合拿来做网页设计作业、毕设二次开发或者想快速搭一个轻量地铁工具站的开发者。2. 把地铁拓扑结构转成JavaScript可用的数据模型2.1 先用站点思维而不是线路思维建模线路是给人看的站点才是给算法跑的。用数组存线路顺序时换乘站会被重复记录查询“某站在哪条线”需要遍历所有线代码很散。我一般会拆成三个基础对象station站点、line线路、transfer换乘关系。station里记录所属线路ID数组line里按顺序记录站点ID这样绘图时遍历线路画线交互时通过站点反查线路都方便。2.2 用JSON定义宁波地铁线路字段别贪多数据主体是两个字段一个线路数组一个站点对象。站点用对象数组id建议用拼音缩写或纯数字避免中文在URL和存储时产生转码问题。坐标用归一化坐标范围控制在0到1000后续做缩放平移只做线性变换。{ lines: [ { id: l1, name: 1号线, color: #e60012, stations: [zhanlan, xintiandijie, guqu] } ], stations: { zhanlan: { id: zhanlan, name: 展览中心, lines: [l1], x: 320, y: 150 }, xintiandijie: { id: xintiandijie, name: 新天地街, lines: [l1, l3], x: 500, y: 150, transfer: true } } }这段JSON把线路和站点分开存储。lines[].stations是站点ID数组绘图时按数组顺序连线stations[].lines用于换乘判断。x和y是归一化坐标范围建议固定在0到1000之间后面做缩放时可以直接用x * scale offset。transfer字段在换乘站标true渲染时给圆点加白边不写也不影响算法只是视觉样式不同。2.3 坐标归一化和数据校验的3个参数坐标获取最容易踩坑。用地图坐标拾取器拿到的经纬度直接写入JS会导致线路图在屏幕上出现严重的扁长变形。我一般先记录整条线路的最大最小经纬度然后按如下公式映射到0到1000画布function normalize(lon, lat, bounds, maxDim 1000) { const rangeLon bounds.maxLon - bounds.minLon; const rangeLat bounds.maxLat - bounds.minLat; const scale maxDim / Math.max(rangeLon, rangeLat); return { x: (lon - bounds.minLon) * scale, y: (maxDim - (lat - bounds.minLat) * scale) }; }这里有个易错点纬度增长方向是北方而Canvas里y轴向下所以y轴要翻转。bounds传四个字段minLon、maxLon、minLat、maxLat可以用在线工具一次性框选整个宁波地铁网。maxDim默认1000保证所有坐标都在方形区域内后续缩放逻辑更简单。参数作用典型值bounds经纬度范围用于归一化宁波约120.8-122.2E, 28.8-30.3NmaxDim归一化后的最大边长1000transfer换乘站标记true/false数据校验建议在页面加载后做一次遍历所有线路的站序列把出现在多条线路的站点自动标记为transfer避免人工维护遗漏。用ES6的filter函数可以很简洁function markTransfers(stations) { Object.values(stations).forEach(s { const lineCount Object.values(lines) .filter(line line.stations.includes(s.id)).length; if (lineCount 1) s.transfer true; }); }这段代码的逻辑是对每个站点统计它出现在哪些线路的stations数组里出现次数大于1就是换乘站。注意includes是ES6方法低版本浏览器需要额外处理。另外坐标归一化后保留两位小数就够了否则JSON文件体积变大调试时满屏小数也影响心情。3. Canvas与SVG渲染线路图两种方案的取舍与落地3.1 Canvas绘制贝塞尔曲线换乘站高亮靠“画两次”地铁线路通常用曲线连接站点视觉上比折线更接近真实线路图。Canvas的quadraticCurveTo足够应付大多数换乘弯折不需要引入第三方绘图库。我一般先把同一条线的站点坐标收集起来然后分段绘制function drawLines(ctx, lines) { lines.forEach(line { ctx.strokeStyle line.color; ctx.lineWidth 6; ctx.beginPath(); line.stations.forEach((stationId, index) { const st stations[stationId]; if (index 0) { ctx.moveTo(st.x, st.y); } else { const prev stations[line.stations[index - 1]]; const midX (prev.x st.x) / 2; ctx.quadraticCurveTo(midX, prev.y, st.x, st.y); } }); ctx.stroke(); }); }这段代码把站点之间的连线画成二次贝塞尔曲线控制点取两个站点连线的中点但y坐标用前一个站点的y这样可以形成平滑的横向弯折很符合地铁图风格。lineWidth设为6是考虑后续缩放后的最小可见宽度如果缩放到80%以下线宽最好按比例缩小否则线会比站点圆点还粗。换乘站高亮我习惯用“描边再填色”画两次先画一个半径为8的白色圆再画半径为5的线路色圆这样能保留线路颜色又不至于糊成一片。function drawStation(ctx, st, lineColor) { if (st.transfer) { ctx.fillStyle #fff; ctx.beginPath(); ctx.arc(st.x, st.y, 8, 0, Math.PI * 2); ctx.fill(); } ctx.fillStyle lineColor; ctx.beginPath(); ctx.arc(st.x, st.y, 5, 0, Math.PI * 2); ctx.fill(); }注意每个站点会在它所属的每条线重复绘制换乘站最后被绘制的那条线会覆盖前面线的颜色。这是正常的因为Canvas没有深度概念。如果希望换乘站同时保留多条线路颜色可以改成先画白底再画多个颜色的同心圆环代码量会多一些但视觉效果更专业。3.2 SVG方案更适合需要事件绑定的场景Canvas重绘方便但命中测试要自己算坐标。如果只需要点击站点弹窗SVG的circle自带click事件开发效率高很多。SVG方案用document.createElementNS创建节点路线用path站点用circle然后把宁波地铁JSON直接映射成DOM。function buildSVG(svg, lines, stations) { lines.forEach(line { const path document.createElementNS(http://www.w3.org/2000/svg, path); const d line.stations.map((id, i) { const st stations[id]; return i 0 ? M ${st.x} ${st.y} : L ${st.x} ${st.y}; }).join( ); path.setAttribute(d, d); path.setAttribute(stroke, line.color); path.setAttribute(stroke-width, 6); path.setAttribute(fill, none); svg.appendChild(path); }); }这段代码生成SVG的path元素M表示起点L表示直线连接到下一个站点。如果不是特别追求曲线效果直线路径的渲染性能更高。SVG还有一个好处可以直接在station对象上挂>function toCanvasPoint(clientX, clientY, canvas, scale, offsetX, offsetY) { const rect canvas.getBoundingClientRect(); const mouseX (clientX - rect.left - offsetX) / scale; const mouseY (clientY - rect.top - offsetY) / scale; return { x: mouseX, y: mouseY }; }注意getBoundingClientRect拿到的坐标是CSS像素如果canvas的width属性和CSS宽度不一致还要再乘一个canvas.width / rect.width比例。另一个坑是缩放后线宽被拉伸我会在drawLines里把线宽乘以1 / scale。最后平移时用ctx.translate会叠加变换状态我更喜欢手动维护offsetX/offsetY在每次绘制开始时调用ctx.setTransform(scale, 0, 0, scale, offsetX, offsetY)这样重绘逻辑最简单也不会出现变换累积导致的图形漂移。4. 最少换乘路径搜索BFS与权值调整4.1 用邻接表把线路图变成无向图最少换乘问题的标准做法是把“换乘”当作一次额外的代价。邻接表的节点是站点边是相邻站点之间的距离。同一条线路的两个相邻站点之间边权为1换乘行为不建普通边而是用单独的一个惩罚值。这样最少站数就是BFS求无权图最短路最少换乘就是带权最短路。function buildGraph(lines, stations) { const graph {}; Object.keys(stations).forEach(id { graph[id] []; }); lines.forEach(line { for (let i 0; i line.stations.length - 1; i) { const a line.stations[i]; const b line.stations[i 1]; graph[a].push({ to: b, line: line.id, weight: 1 }); graph[b].push({ to: a, line: line.id, weight: 1 }); } }); return graph; }这个函数遍历每条线路的相邻站对把一对站点加入无向边。line字段记住这条边属于哪条线路后面判断是否换乘要用。如果两条线在物理上共用一段区间节点会自然收到多条边不需要特殊处理因为它们的坐标来自同一个站点对象。4.2 BFS找最少站数Dijkstra找最少换乘最少站数直接套BFS因为每条边权都是1。但很多场景需要“换乘最少”这时单纯站数少不代表体验好。常见做法是把换乘惩罚设置为一个常量比如TRANSFER_PENALTY 4表示换乘一次相当于多坐4站然后跑Dijkstra。function dijkstra(graph, start, end, penalty) { const dist {}; const prev {}; const currentLine {}; const heap [[0, start]]; Object.keys(graph).forEach(id { dist[id] Infinity; }); dist[start] 0; while (heap.length) { heap.sort((a, b) a[0] - b[0]); const [d, u] heap.shift(); if (d ! dist[u]) continue; if (u end) break; graph[u].forEach(edge { let nd d edge.weight; if (currentLine[u] edge.line ! currentLine[u]) { nd penalty; } if (nd dist[edge.to]) { dist[edge.to] nd; prev[edge.to] u; currentLine[edge.to] edge.line; heap.push([nd, edge.to]); } }); } return { dist, prev }; }代码里有一个隐藏状态currentLine[u]表示走到站点u时所在线路需要额外数组维护。Dijkstra的堆用数组排序可以应对地铁网几百个站点的规模如果站点数上千再换二叉堆。penalty参数取值很关键设0等价于最少站数设4或5表示换乘一次折合多坐4站。宁波地铁1号线换2号线走通道大约5分钟设成4比较贴近实际体验。算法边权目标适用场景BFS全部1最少站数演示算法基础Dijkstra1换乘惩罚最少换乘真实出行推荐A*1惩罚启发搜索加速站点数很大时BFS代码更短适合放在网页里做“按站数排序”的备选方案function bfsMinStations(graph, start, end) { const queue [start]; const visited { [start]: 0 }; while (queue.length) { const u queue.shift(); if (u end) break; graph[u].forEach(edge { if (visited[edge.to] undefined) { visited[edge.to] visited[u] 1; queue.push(edge.to); } }); } return visited[end]; }4.3 把路径渲染到Canvas上注意换乘线颜色拿到prev数组后回溯出站点序列然后在重绘时额外走一条高亮路径。高亮路径直接在原线上方再画一条亮色线线宽设为10透明度0.6。绘制时不要在换乘站改变颜色否则路径看起来会断裂。我习惯整段路径统一一个高亮色再在换乘站画白色边框圆点视觉上更清楚。function drawRoute(ctx, routeIds, stationMap) { if (routeIds.length 2) return; ctx.strokeStyle rgba(0, 150, 136, 0.7); ctx.lineWidth 10; ctx.beginPath(); routeIds.forEach((id, i) { const st stationMap[id]; if (i 0) ctx.moveTo(st.x, st.y); else ctx.lineTo(st.x, st.y); }); ctx.stroke(); }这段代码把路径站点按顺序连线高亮色用薄荷绿和宁波地铁官方色系不冲突。lineWidth设为10覆盖原有线路色透明度0.7保证底图细节还能透出来。实际使用时我会把routeIds加上起点和终点名称显示在页面顶部的信息栏里方便核对结果。5. HBuilderX工程化配置、模块加载报错与数据过滤技巧5.1 HBuilderX里创建HTMLCSSJS网页项目的最小结构在HBuilderX中新建项目时选“基本HTML项目”会自动生成index.html、css、js目录。我习惯于把数据单独放在js/nbdt-data.js把算法放在js/route.js页面逻辑放在js/main.js。这样改站点数据和调渲染不会互相影响。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 link relstylesheet hrefcss/style.css title宁波地铁网页设计/title /head body canvas idmetroCanvas/canvas script srcjs/nbdt-data.js/script script srcjs/route.js/script script srcjs/main.js/script /body /html这种顺序加载脚本是经典方式不需要处理ES6 module跨域问题。main.js里在window.onload中初始化数据并绘制保证Canvas已经渲染完成。5.2 运行时报错failed to load module script和框架库漏洞提示如果改成script typemodule srcmain.js在HBuilderX内置浏览器里运行会报Failed to load module script: Unexpected token import原因是直接file协议加载ES模块受到CORS限制。常见做法是保持传统script标签或者在项目上右键运行到外部浏览器。另一个高频提示是“检测到目标站点存在JavaScript框架库漏洞”这是安全扫描工具识别到了老版本jQuery等依赖。用原生JavaScript写这个宁波地铁页面不引入任何框架就不会触发这个告警如果确实用了就把jQuery升级到3.7以上并移除未用插件。提示HBuilderX内置预览器对ES6支持有限我在本机调试时都用“运行到浏览器”保持语法在Chrome/Edge下执行。5.3 用filter函数做数据过滤避免全量重绘页面收到用户筛选请求时并不需要重建整个Canvas。常见需求是只看某条线或者只看换乘站。用Array.prototype.filter两步搞定function getStationsByLine(lineId) { const line lines.find(l l.id lineId); return line.stations .map(id stations[id]) .filter(st st st.online); }find定位线路对象map把ID转成站点对象filter过滤掉被临时关闭的站点。每次筛选后只调用一次drawLines和drawStation而不是重新解析JSON。最后在浏览器控制台用一段JavaScript验证整条链路const result dijkstra(graph, zhanlan, tianyi, 4); console.log(result.dist[tianyi], result.prev);看到输出一个距离值和一个对象引用就说明“数据建模 → 算法搜索 → Canvas渲染”这一整条链路已经闭环。后续要接上下拉选择起始站和终点站只需要把这两个ID替换成表单的值即可。本文还有配套的精品资源点击获取
返回列表