feat: 新增设备位置追踪与网页管理页面

- 更新服务端默认Lua版本与客户端程序版本至 v1.5.16
- 新增后端API接口用于提交和查询设备位置数据
- 为售票机和检票机添加后台GPS位置同步任务,定期上报设备坐标
- 服务端新增设备位置缓存,并自动清理超过5分钟未更新的过期数据
- 新增网页端设备位置地图页面,支持实时展示、点击查看详情与每分钟自动刷新
This commit is contained in:
2026-07-11 14:58:30 +08:00
parent 2eddb8b84c
commit efc061e471
6 changed files with 211 additions and 19 deletions
+52
View File
@@ -64,6 +64,9 @@
<div class="nav-item" :class="{active: currentView === 'faremap'}" @click="currentView = 'faremap'">
<span class="nav-icon"><i class="fas fa-map"></i></span> 票价地图
</div>
<div class="nav-item" :class="{active: currentView === 'machinemap'}" @click="currentView = 'machinemap'">
<span class="nav-icon"><i class="fas fa-map-marker-alt"></i></span> 机器位置
</div>
<div class="nav-item" :class="{active: currentView === 'tickets'}" @click="currentView = 'tickets'">
<span class="nav-icon"><i class="fas fa-ticket-alt"></i></span> 车票记录
</div>
@@ -641,6 +644,55 @@
</div>
</div>
<!-- 机器位置 -->
<div v-if="currentView === 'machinemap'">
<div class="card">
<h3>设备分布图</h3>
<p class="text-muted">机器位置与状态 (每 60 秒自动更新)</p>
<div class="map-container" style="position: relative; width: 100%; height: 600px; background: #1a1a1a; border-radius: 8px; overflow: hidden; border: 1px solid rgba(255,255,255,0.1);">
<!-- We can use a simple coordinate mapping here. -->
<!-- First, calculate bounds to normalize coordinates -->
<div v-if="Object.keys(machineLocations).length === 0" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #666;">
暂无机器坐标数据
</div>
<template v-else>
<!-- Map markers -->
<div v-for="(loc, id) in machineLocations" :key="id"
class="map-marker"
:style="{
position: 'absolute',
left: getMapX(loc.x) + '%',
top: getMapY(loc.z) + '%',
transform: 'translate(-50%, -50%)',
cursor: 'pointer',
zIndex: selectedMachineId === id ? 10 : 1
}"
@click="selectedMachineId = id"
>
<div :class="['marker-icon', loc.type === 'Gate' ? 'gate' : 'ticket']"
:style="{
width: '20px', height: '20px', borderRadius: '50%',
background: loc.type === 'Gate' ? '#ff9800' : '#4ade80',
border: '2px solid #fff',
boxShadow: '0 0 10px rgba(0,0,0,0.5)'
}">
</div>
<div v-if="selectedMachineId === id" class="marker-popup"
style="position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); background: #333; padding: 10px; border-radius: 6px; width: max-content; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 20; color: white; text-align: center;">
<div style="font-weight: bold; margin-bottom: 4px;">{{ loc.device_id || id }}</div>
<div style="font-size: 0.85em; color: #aaa;">类型: {{ loc.type === 'Gate' ? '检票机' : '售票机' }}</div>
<div v-if="loc.type === 'Gate'" style="font-size: 0.85em; color: #aaa;">
模式: {{ loc.gate_mode === 'entry' ? '进站' : (loc.gate_mode === 'exit' ? '出站' : '双向') }}
</div>
<div style="font-size: 0.85em; color: #aaa;">站点: {{ loc.station || loc.station_code || '未知' }}</div>
<div style="font-size: 0.8em; color: #888; margin-top: 4px;">坐标: {{ loc.x }}, {{ loc.y }}, {{ loc.z }}</div>
</div>
</div>
</template>
</div>
</div>
</div>
<!-- 车票记录 -->
<div v-if="currentView === 'tickets'">
<div class="grid">