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
+62 -1
View File
@@ -24,6 +24,7 @@ createApp({
dashboard: '仪表盘',
management: '线路与票价管理',
faremap: '票价地图',
machinemap: '机器位置',
tickets: '车票记录',
vouchers: '凭证管理',
iccards: 'IC 卡管理',
@@ -78,12 +79,14 @@ createApp({
let logDataLoaded = false;
let assetsLoaded = false;
let fareMapLoaded = false;
let machineLocationsLoaded = false;
const loadingState = reactive({
core: false,
tickets: false,
orders: false,
logs: false,
iccards: false
iccards: false,
machinemap: false
});
const lastSyncAt = ref(0);
@@ -1116,6 +1119,52 @@ createApp({
}
};
const machineLocations = ref({});
const selectedMachineId = ref('');
const getMapX = (x) => {
if (typeof x !== 'number') return 50;
// Assuming typical MC coords are between -10000 and +10000.
// We can auto-scale based on actual data bounds instead.
let min = 0, max = 0;
const vals = Object.values(machineLocations.value).map(l => l.x).filter(v => typeof v === 'number');
if (vals.length > 0) {
min = Math.min(...vals) - 50;
max = Math.max(...vals) + 50;
} else {
return 50;
}
if (max === min) return 50;
return ((x - min) / (max - min)) * 90 + 5; // 5% to 95% margin
};
const getMapY = (z) => {
if (typeof z !== 'number') return 50;
let min = 0, max = 0;
const vals = Object.values(machineLocations.value).map(l => l.z).filter(v => typeof v === 'number');
if (vals.length > 0) {
min = Math.min(...vals) - 50;
max = Math.max(...vals) + 50;
} else {
return 50;
}
if (max === min) return 50;
return ((z - min) / (max - min)) * 90 + 5;
};
const fetchMachineLocations = async () => {
loadingState.machinemap = true;
try {
const res = await requestJson('/api/machine-locations');
if (res && res.data) {
machineLocations.value = res.data;
}
machineLocationsLoaded = true;
} catch (e) {
console.error(e);
} finally {
loadingState.machinemap = false;
}
};
const ensureViewData = async (view = currentView.value, { force = false } = {}) => {
await fetchCoreData({ force });
if (view === 'tickets' && (force || !ticketDataLoaded)) await fetchTicketData();
@@ -1129,6 +1178,7 @@ createApp({
}
if (view === 'faremap' && (force || !fareMapLoaded)) await loadFareMap({ force });
if (view === 'assets' && (!assetsLoaded || force)) await fetchAssetsManifest();
if (view === 'machinemap') await fetchMachineLocations();
};
const fetchData = async () => {
@@ -1564,6 +1614,8 @@ createApp({
syncSelectedIcCard().catch(console.error);
});
let machineMapTimer = null;
watch(currentView, (v) => {
sidebarOpen.value = false;
if (v === 'iccards') {
@@ -1571,6 +1623,14 @@ createApp({
} else {
stopIcCardSync();
}
if (v === 'machinemap') {
if (!machineMapTimer) machineMapTimer = setInterval(fetchMachineLocations, 60000);
} else {
if (machineMapTimer) {
clearInterval(machineMapTimer);
machineMapTimer = null;
}
}
ensureViewData(v).catch(console.error);
const sp = new URLSearchParams(location.search);
if (v === 'dashboard') sp.delete('view');
@@ -1720,6 +1780,7 @@ createApp({
};
return {
machineLocations, selectedMachineId, getMapX, getMapY,
currentView, viewTitle, sidebarOpen,
loadingState, isViewBusy, lastSyncText, currentViewSummary,
stations, lines, fares, stats, config, recentLogs, ticketList, ticketStats, orderStats,