
简介一套基于隧道施工人员安全管理的JavaScript前端系统源码以JavaScript为核心搭配HTML与CSS实现界面交互覆盖位置追踪、安全装备佩戴检测、作业环境监测、风险预警及培训教育等模块适合前端开发者、工程安全信息化人员及高校相关专业学生研究学习。压缩包共包含535个文件其中JavaScript脚本134个、CSS样式表85个、PNG图片161个、GIF动画96个另有TTF字体、Java、SVG、XML等辅助文件压缩后大小约29.65MB目录结构清晰便于按模块检索和二次开发。目前已有265人学习/下载。源码包含完整的模块化目录和基础配置前端逻辑中涵盖权限管理、数据统计、实时监控与预警等交互实现并配有大量图标、动效和样式素材可帮助读者快速把握隧道施工安全管理系统的技术架构与实际应用思路。1. 隧道安全监管的现场场景一张工牌、一块屏幕背后的JavaScript逻辑隧道工程项目部早上6点多作业人员依次刷工牌过闸机大屏实时刷新各作业面人数安全员手里的平板每隔几秒跳出一行气体监测值。这些交互全部由JavaScript前端完成工牌定位、气体浓度、报警联动都收敛到一块浏览器屏幕上洞口值班员能看后台管理员能查。源码包一共534个文件以JavaScript为骨架配合Kendo UI的多个CSS主题、PNG图标与GIF动效资源。它解决的问题很具体把分散的传感器数据、人员状态和考核规则做成一套能操作的系统界面。适合前端开发、物联网可视化以及工程信息化方向的人做二次开发或课程设计里面关于数据表格、图表、弹窗和轮询的写法也值得直接借鉴。2. 从534个文件反推前端架构Kendo UI主题、Maven工程布局与JS模块拆法2.1 文件清单里藏着技术选型主题CSS与图标资产文件清单不是随手导出的从类型分布就能读出一套系统的选型倾向。文件类型数量在系统里的角色PNG图像161按钮图标、工牌头像、设备示意图JavaScript脚本134页面控制器、Kendo组件配置、数据请求GIF动画96加载等待、报警闪烁、培训提示动图CSS样式表85页面布局、Kendo主题、打印样式字体文件18图标字体与界面符号Java / SVG / WOFF / XML少量后端支撑、矢量图形、资源描述85个CSS里能看到kendo.default-v2.min.css、kendo.bootstrap-v4.min.css、kendo.common-nova.min.css这样的命名这类主题文件是Kendo UI的标志。kendo.dataviz.mobile.min.css单独存在说明图表组件在手机端、平板端的样式是额外打包的隧道现场用平板看数据时这套样式会接管图表控件的大小和间距。项目没把主题全部杂在一起而是按模块引入静态资源体积也能控制住。2.2 Maven工程与前端资源目录mvnw与src/main/resources/static包里出现mvnw.cmd、mvnw和pom.xml说明前端资源被组织在一个Maven工程里。mvnw是Maven Wrapper脚本作用是固定Maven版本保证换一台机器构建时不会因为版本差异跑不起来pom.xml声明项目依赖和打包方式。常见结构是这样. ├── mvnw ├── mvnw.cmd ├── pom.xml ├── readme.txt └── src └── main └── resources └── static ├── css/ ├── js/ ├── images/ └── fonts/静态资源放在src/main/resources/static下Spring Boot启动后会把static目录映射为网站根路径index.html直接通过浏览器访问。CSS和JS引用路径因此不用写太长比如/css/kendo.default-v2.min.css。如果拿到源码后发现页面白屏先看引入路径是否写成了磁盘绝对路径这是Maven工程里最容易出现的问题。readme.txt在这种包里往往记录了启动端口和默认账号密码先读它比直接改代码省时间。2.3 134个JavaScript脚本按四层拆页面控制器、组件、数据服务与工具库JavaScript脚本占到134个数量多不代表混乱。这个规模的系统我一般按四层来拆页面控制器负责按钮、弹窗、路由转发组件配置层专门初始化Kendo UI控件数据服务层封装所有fetch和WebSocket工具库放日期格式化、坐标转换、防抖函数。这样人员列表页只关心列表怎么渲染不关心接口地址是谁。用Kendo UI的表格组件做一个例子$(#workerGrid).kendoGrid({ dataSource: { transport: { read: { url: /api/worker/list, dataType: json, type: GET } }, pageSize: 10, schema: { model: { id: workerId } } }, columns: [ { field: workerId, title: 工号, width: 90 }, { field: name, title: 姓名 }, { field: team, title: 班组 }, { field: zone, title: 作业面, values: [ { text: 掌子面, value: A }, { text: 二衬区, value: B } ]} ], pageable: true });dataSource.transport.read.url是数据接口地址pageSize把每页限制为10条columns里的field对应后端字段名values把后端存的A、B编码翻译成中文作业面。schema.model.id告诉表格用workerId作为行主键后面做选中、更新行时会用到。把这类初始化代码集中在一个js/component/grid.js里页面只需要调用对应的配置项134个脚本的协作关系就清楚了。3. 实时监控与三级预警环境监测轮询、WebSocket推送与界面联动的JavaScript实现3.1 先定监测字段和阈值表再写轮询拉取隧道作业面的气体监测不是简单显示数值前端需要根据浓度切换状态。先把阈值定义清楚代码才有依据。指标黄色预警红色报警甲烷 CH40.8%1.0%一氧化碳 CO24 ppm50 ppm二氧化硫 SO25 ppm10 ppmPM2.575 µg/m³150 µg/m³温度35℃40℃接口返回的JSON会带作业分区编号和一组气体读数{ zone: A, ch4: 0.65, co: 18, so2: 2, pm25: 42, temperature: 26.4, time: 2026-04-16 06:23:11 }前端轮询的实现很直接const INTERVAL 3000; async function loadGas() { try { const res await fetch(/api/monitor/gas?zone currentZone, { headers: { Accept: application/json } }); if (!res.ok) throw new Error(HTTP res.status); const data await res.json(); renderGas(data); checkAlarm(data); } catch (err) { showToast(监测服务暂不可用 err.message); } } setInterval(loadGas, INTERVAL);INTERVAL取3000毫秒3秒刷新一次适合非灾害性数据fetch请求带zone参数是为了按作业面切分数据避免把全线数据都塞到一张页面上。checkAlarm是独立的预警判断函数renderGas只做界面渲染两者分开后调试报警逻辑时不需要在setInterval回调里反复打断点。3.2 WebSocket推送接力预警事件不靠定时等待轮询的缺陷是延迟受刷新间隔限制气体浓度在两次请求之间骤升界面要等到下3秒才看到。隧道这种场景更直接的做法是用WebSocket把报警事件主动推过来轮询降级为兜底。const ws new WebSocket(ws:// location.host /ws/monitor); ws.onmessage function (evt) { const msg JSON.parse(evt.data); if (msg.type ALARM) { handleAlarm(msg.payload); } else if (msg.type REFRESH) { loadGas(); } }; ws.onclose function () { // 连接断开后回到轮询避免页面彻底失去数据 setInterval(loadGas, INTERVAL); };WebSocket地址按location.host拼接避免写死IP和端口。onmessage里根据msg.type区分报警和普通刷新ALARM消息立即触发handleAlarm普通数据则继续走loadGas刷一版读数。onclose事件触发说明长连接断了这里重新启动定时轮询比无限重试WebSocket更容易让现场情况可控。3.3 三级预警的界面联动窗口、语音与按钮状态报警分三级处理代码里用level表示0为正常1为黄色预警2为红色报警。三级联动的重点不是弹窗本身而是不同警示强度对应不同的界面行为。let lastNotify 0; function checkAlarm(data) { const level calcLevel(data); updateBadge(level); if (level 2 Date.now() - lastNotify 30000) { lastNotify Date.now(); $(#alarmWindow).kendoWindow({ modal: true, title: 红色报警, content: data.zone 甲烷浓度 data.ch4 %, width: 420 }).data(kendoWindow).open(); speak(甲烷浓度超限请立即撤离); } } function calcLevel(data) { if (data.ch4 1.0) return 2; if (data.ch4 0.8) return 1; return 0; }lastNotify是一个时间戳变量30秒内的重复报警不会把弹窗连续呼出三次避免值班人员在处理告警时被反复打断。speak函数可以调用浏览器的speechSynthesis也可以播放一段预置MP3。界面联动还包括level为2时给作业面选择按钮加disabled样式、把顶部状态栏背景切换成红色、让GIF报警图标开始闪烁。预警不只是出现一个弹窗而是整块页面进入对应状态。3.4 安全装备佩戴检测的前端边界把识别结果渲染成状态头盔、反光背心的检测属于视觉识别真正的判定逻辑放在后端或支持AI识别的摄像头设备上。前端拿到的是识别结果需要把它变成可操作的状态。const equipStatus { T001: { helmet: true, vest: true }, T002: { helmet: false, vest: true } }; function renderEquip(badgeNo) { const status equipStatus[badgeNo] || {}; $(#helmetDot).toggleClass(ok, !!status.helmet); $(#vestDot).toggleClass(warn, !status.vest); if (!status.helmet || !status.vest) { $(#equipTip).text(工牌 badgeNo 未正确佩戴安全装备); } }这个实现从后端数据结构出发helmet和vest是后端返回的布尔值前端用一个普通对象做状态映射再通过toggleClass切换红绿圆点。数据来源为摄像头识别的结果例如后端每5秒汇总一次识别记录前端刷新列表时一并拿到。边界划在这里的好处是设备升级或算法替换时前端逻辑不用动。4. 数据可视化与风险预判Kendo DataViz趋势图、轨迹回放与动态阈值下发4.1 一张风险趋势图怎么落到Kendo Chart上综合风险指数是把气体浓度、人员到位率、设备状态加权计算后的结果范围0到100。前端展示时把指数分成三段0到40低风险40到70中风险70到100高风险。$(#riskChart).kendoChart({ dataSource: { transport: { read: { url: /api/risk/trend, dataType: json } }, sort: { field: time, dir: asc } }, series: [{ type: line, field: riskIndex, name: 综合风险指数, color: #e67e22 }], categoryAxis: { field: time, labels: { rotation: -45 } }, valueAxis: { min: 0, max: 100, plotBands: [ { from: 0, to: 40, color: #d4efdf }, { from: 40, to: 70, color: #fdebd0 }, { from: 70, to: 100, color: #fadbd8 } ] } });风险区间颜色对应处置0 - 40绿正常作业40 - 70黄加密监测、提醒班组70 - 100红启动撤离预案series.field指定取数据里的riskIndex字段categoryAxis.field是时间字段每隔一段时间出现一个点。plotBands在数值轴上画背景色带比给折线单独标颜色更直观值班人员只扫一眼背景就知道当前处在哪个区间。4.2 施工轨迹回放坐标归一化、时间轴拖动与Canvas绘制定位数据回放最常见的问题是经纬度直接画到Canvas上坐标超出画布范围。需要把经纬度先换算成0到1的比例再乘画布的宽高。function playTrack(points) { const canvas document.getElementById(trackCanvas); const ctx canvas.getContext(2d); ctx.clearRect(0, 0, canvas.width, canvas.height); let index 0; const timer setInterval(() { if (index points.length) { clearInterval(timer); return; } const p points[index]; const x (p.lng - minLng) / (maxLng - minLng) * canvas.width; const y (maxLat - p.lat) / (maxLat - minLat) * canvas.height; ctx.fillStyle #1f6fb2; ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.fill(); index 1; }, 200); }minLng、maxLng、minLat、maxLat是当前时间段内的定位边界一次性从轨迹数据里算出来。y坐标使用maxLat减去当前纬度符合Canvas坐标系向下增长的规律否则轨迹会上下翻转。200毫秒的间隔模拟人员步行速度比较合适采样点间隔太大时把200改成100。播放过程中如果切换班组需要把timer先clear掉否则两条轨迹会在同一张画布上叠加。4.3 动态阈值下发后端按工况调整前端同步生效爆破、出渣、正常开挖三种工况下气体安全边界不一样。前端不应该把阈值写死在代码里而是接收后端下发的配置对象。const thresholdMap { normal: { ch4Yellow: 0.8, ch4Red: 1.0 }, blasting: { ch4Yellow: 0.5, ch4Red: 0.8 } }; function applyThreshold(config) { Object.assign(thresholdMap.normal, config); loadGas(); }后端在作业面切换到爆破工况时返回一份新的阈值JSONapplyThreshold把新阈值合并进当前配置再立刻调用loadGas重新校验最近一帧数据。Object.assign只覆盖存在的键不会清掉thresholdMap里的其他字段。这里的“下发”通常由后端主动推送前端收到后更新配置。这样做的好处是阈值调整不需要重新发布前端代码也不会出现前端版本与后端策略不一致的情况。5. HBuilderX配置与移动端模拟调试虚拟定位、真机预览与边界排查5.1 在HBuilderX里把HTML源码跑起来HBuilderX不只写uni-app普通HTML项目同样可以运行。把源码目录以“打开目录”方式导入后右键index.html选择“运行 - 运行到浏览器”工具会启动本地静态服务器浏览器里打开生成的地址即可预览。端口在“运行 - 运行到浏览器 - 配置Web服务器”里修改。http://localhost:8848/ └── src/main/resources/static/ # 静态资源根路径 ├── index.html ├── css/ ├── js/ └── images/如果项目结构里静态资源已经放在static目录运行目录要选包含static的那一层不要直接选static文件夹否则index.html里的路径会整体错位。第一次运行时发现图片不显示优先检查浏览器Network面板里资源请求路径是否带了两层static。5.2 虚拟定位控制台模拟工牌轨迹的几种写法在电脑上调试人员轨迹页面浏览器没有真实定位源。最省事的方式是在控制台覆盖定位接口navigator.geolocation.getCurrentPosition function (success) { success({ coords: { longitude: 103.708, latitude: 30.712, accuracy: 3 }, timestamp: Date.now() }); };coords里三个参数分别对应经度、纬度、精度半径accuracy给3米是为了让页面上的定位圆圈不至于太大。代码需要在页面加载位置监控逻辑之前执行可以用DevTools里的Snippets保存后在页面启动时先运行。Chrome DevTools自带的Sensors面板也能模拟经纬度不需要改代码适合快速换点测试要模拟连续移动轨迹时覆盖方式更好可以在setInterval里连续改经纬度让轨迹播放更接近真实路径。5.3 真机预览、跨域与路径排查清单拿手机看页面效果属于常见需求用手机浏览器访问电脑上HBuilderX启动的地址前提是手机和电脑在同一个局域网内。现象原因处理方式页面白屏资源路径写成了磁盘绝对路径统一改成相对路径检查css/js引入中文乱码HTML未声明UTF-8head里加meta charsetUTF-8请求报跨域后端响应未带CORS头让后端在响应头增加Access-Control-Allow-Origin手机打不开地址电脑防火墙拦截或不在同一网段检查防火墙入站规则放行HBuilderX对应端口接口返回404端口不对或应用未启动确认后端端口和前端请求地址一致跨域问题在这种前后端分离项目里最常见。浏览器发起请求时会先发送Origin头后端需要返回Access-Control-Allow-Origin。如果是Spring Boot后端可以在Controller上加CrossOrigin注解如果前端用Kendo DataSource还要检查transport.read.dataType是否写了json防止响应被按JSONP解析。真机访问出现白屏时先换手机自带浏览器试一次排除企业浏览器拦截组件导致的问题。6. 用MutationObserver做考勤埋点停留时长统计与掉线补传的落地技巧6.1 监听人员列表DOM变化并计算停留时长安全管理的考勤不止看刷卡时间还要知道人员在某个作业面停留了多久。常见做法是轮询接口算差值但轮询会频繁打后端而且统计口径跟界面展示的列表不一致。直接监听人员列表DOM用MutationObserver记录出现和消失的时间点。const onlineMap new Map(); const observer new MutationObserver(function (mutations) { mutations.forEach(function (m) { m.addedNodes.forEach(function (node) { if (node.nodeType ! 1) return; const badge node.dataset ? node.dataset.badge : null; if (badge !onlineMap.has(badge)) { onlineMap.set(badge, Date.now()); } }); m.removedNodes.forEach(function (node) { if (node.nodeType ! 1) return; const badge node.dataset ? node.dataset.badge : null; if (badge onlineMap.has(badge)) { const enterTime onlineMap.get(badge); const minutes Math.round((Date.now() - enterTime) / 60000); uploadAttendance(badge, minutes); onlineMap.delete(badge); } }); }); }); observer.observe(document.getElementById(workerList), { childList: true, subtree: true });>function uploadAttendance(badge, minutes) { const row { badge, minutes, endTime: Date.now() }; if (navigator.onLine) { fetch(/api/attendance, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(row) }); } else { const queue JSON.parse(localStorage.getItem(attQueue) || []); queue.push(row); localStorage.setItem(attQueue, JSON.stringify(queue)); } }navigator.onLine判断当前是否有网络离线时先把记录塞进localStorage队列再监听window的online事件等网络恢复后一次性把队列提交给后端。localStorage在这个场景下容量足够队列长度要限制在几百条以内超出后丢弃最早的记录。在beforeunload里要把onlineMap中剩余人员的时间先flush掉或退一步增加定时补扫机制否则直接关页面会让最后一段停留记录丢失。本文还有配套的精品资源点击获取