chore(backend,frontend): 移除设备位置管理全链路功能与相关代码
- 删除后端所有机器位置相关的API接口、缓存逻辑与后台同步任务 - 移除前端的机器位置导航入口、地图页面与管理代码 - 将原机器地图页面重构为车票查询页面 - 调整Lua程序的默认版本号 - 删除无用的.gitignore文件 - 优化前端页面的代码格式与导航结构
This commit is contained in:
-27
@@ -1,27 +0,0 @@
|
|||||||
# Node.js
|
|
||||||
node_modules/
|
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
|
|
||||||
# IDE / Editor
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Project Specific (Logs & Cache)
|
|
||||||
logs/
|
|
||||||
.dbg/
|
|
||||||
.last_update_ts
|
|
||||||
.last_update_version
|
|
||||||
config.json
|
|
||||||
gate_config.json
|
|
||||||
web/data/
|
|
||||||
|
|
||||||
# Environment Variables
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# Trae AI
|
|
||||||
.trae/
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
local DEFAULT_SERVER_BASE = "http://ticket.fse-media.group"
|
local DEFAULT_SERVER_BASE = "http://ticket.fse-media.group"
|
||||||
local DEFAULT_SERVER_PATH = "/api/tickets/check"
|
local DEFAULT_SERVER_PATH = "/api/tickets/check"
|
||||||
local GATE_OPEN_SECONDS = 2
|
local GATE_OPEN_SECONDS = 2
|
||||||
local VERSION = "v1.5.18"
|
local VERSION = "v1.5.15"
|
||||||
local VERSION_CHECK_INTERVAL = 5
|
local VERSION_CHECK_INTERVAL = 5
|
||||||
|
|
||||||
local CONFIG_PATH = "gate_config.json"
|
local CONFIG_PATH = "gate_config.json"
|
||||||
@@ -192,7 +192,6 @@ local function checkAndApplyUpdate()
|
|||||||
print("OTA Update Error: " .. tostring(res))
|
print("OTA Update Error: " .. tostring(res))
|
||||||
term.setTextColor(colors.white)
|
term.setTextColor(colors.white)
|
||||||
end
|
end
|
||||||
sleep(3)
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
lastUpdateError = "loadfile failed: " .. tostring(err)
|
lastUpdateError = "loadfile failed: " .. tostring(err)
|
||||||
@@ -201,7 +200,6 @@ local function checkAndApplyUpdate()
|
|||||||
print("OTA Update Error: " .. tostring(err))
|
print("OTA Update Error: " .. tostring(err))
|
||||||
term.setTextColor(colors.white)
|
term.setTextColor(colors.white)
|
||||||
end
|
end
|
||||||
sleep(3)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
isUpdating = false
|
isUpdating = false
|
||||||
@@ -1500,49 +1498,21 @@ local function processInspectionEvent(eventName, ev)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function mainLoop()
|
local versionTimer = os.startTimer(VERSION_CHECK_INTERVAL)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
while true do
|
while true do
|
||||||
local ev = pack(os.pullEvent())
|
local ev = pack(os.pullEvent())
|
||||||
if ev[1] == "ticket_scanned" or ev[1] == "ic_card_scanned" then
|
if ev[1] == "ticket_scanned" or ev[1] == "ic_card_scanned" then
|
||||||
processInspectionEvent(ev[1], ev)
|
processInspectionEvent(ev[1], ev)
|
||||||
os.sleep(0.35)
|
os.sleep(0.35)
|
||||||
drawReadyScreen()
|
drawReadyScreen()
|
||||||
end
|
elseif ev[1] == "timer" and ev[2] == versionTimer then
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function backgroundSyncTask()
|
|
||||||
while true do
|
|
||||||
sleep(VERSION_CHECK_INTERVAL)
|
|
||||||
pcall(function()
|
pcall(function()
|
||||||
refreshRemoteLuaVersion(guessBaseFromStatusURL(serverURL))
|
refreshRemoteLuaVersion(guessBaseFromStatusURL(serverURL))
|
||||||
end)
|
end)
|
||||||
pcall(checkAndApplyUpdate)
|
pcall(checkAndApplyUpdate)
|
||||||
|
versionTimer = os.startTimer(VERSION_CHECK_INTERVAL)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function backgroundLocationSyncTask()
|
|
||||||
sleep(5)
|
|
||||||
local x, y, z
|
|
||||||
if gps and type(gps.locate) == "function" then
|
|
||||||
x, y, z = gps.locate(2)
|
|
||||||
end
|
|
||||||
local label = os.getComputerLabel()
|
|
||||||
if not label or label == "" then label = "#" .. tostring(os.getComputerID()) end
|
|
||||||
local payload = {
|
|
||||||
device_id = label,
|
|
||||||
type = "Gate",
|
|
||||||
gate_mode = modeBySide and "bidirectional" or mode,
|
|
||||||
station_code = stationCodeForSide(nil) or "",
|
|
||||||
x = x,
|
|
||||||
y = y,
|
|
||||||
z = z
|
|
||||||
}
|
|
||||||
local serverBase = guessBaseFromStatusURL(serverURL)
|
|
||||||
pcall(postCheck, serverBase .. '/api/machine-location', payload)
|
|
||||||
while true do
|
|
||||||
sleep(86400)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
parallel.waitForAny(mainLoop, backgroundSyncTask, backgroundLocationSyncTask)
|
|
||||||
|
|||||||
@@ -163,33 +163,6 @@ router.get('/check-update', (req, res) => {
|
|||||||
res.json({ update_counter: globalUpdateCounter });
|
res.json({ update_counter: globalUpdateCounter });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/machine-location', async (req, res) => {
|
|
||||||
const data = req.body || {};
|
|
||||||
if (!data.device_id) return res.status(400).json({ ok: false, error: 'device_id required' });
|
|
||||||
if (!data.station && data.station_code) {
|
|
||||||
data.station = resolveStationName(data.station_code);
|
|
||||||
}
|
|
||||||
await DataService.updateMachineLocation(data.device_id, data);
|
|
||||||
res.json({ ok: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/machine-locations', (req, res) => {
|
|
||||||
const locs = DataService.getMachineLocations() || {};
|
|
||||||
res.json({ ok: true, data: locs });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/machine-location/:id', async (req, res) => {
|
|
||||||
const id = req.params.id;
|
|
||||||
const data = req.body || {};
|
|
||||||
const current = DataService.getMachineLocations()[id];
|
|
||||||
if (!current) return res.status(404).json({ ok: false, error: 'machine not found' });
|
|
||||||
if (!data.station && data.station_code) {
|
|
||||||
data.station = resolveStationName(data.station_code);
|
|
||||||
}
|
|
||||||
await DataService.updateMachineLocation(id, data);
|
|
||||||
res.json({ ok: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
router.get('/config', (req, res) => {
|
router.get('/config', (req, res) => {
|
||||||
const cfg = DataService.getConfig();
|
const cfg = DataService.getConfig();
|
||||||
|
|||||||
@@ -224,11 +224,6 @@ router.get('/fares/query', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/machine-locations', (req, res) => {
|
|
||||||
const locs = DataService.getMachineLocations() || {};
|
|
||||||
res.json({ ok: true, data: locs });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/config', (req, res) => {
|
router.get('/config', (req, res) => {
|
||||||
const cfg = DataService.getConfig();
|
const cfg = DataService.getConfig();
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
+3
-20
@@ -22,8 +22,8 @@ const pool = mysql.createPool({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const DEFAULT_LUA_VERSIONS = {
|
const DEFAULT_LUA_VERSIONS = {
|
||||||
ticketmachine: 'v1.5.18',
|
ticketmachine: 'v1.5.8',
|
||||||
gate: 'v1.5.18'
|
gate: 'v1.5.8'
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeLuaVersions(input) {
|
function normalizeLuaVersions(input) {
|
||||||
@@ -64,8 +64,7 @@ const cache = {
|
|||||||
icCardIndex: {},
|
icCardIndex: {},
|
||||||
icCardEvents: [],
|
icCardEvents: [],
|
||||||
statsTicket: [],
|
statsTicket: [],
|
||||||
statsGate: [],
|
statsGate: []
|
||||||
machineLocations: {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DataService = {
|
const DataService = {
|
||||||
@@ -95,9 +94,6 @@ const DataService = {
|
|||||||
if (configs.length > 0) cache.config = normalizeConfig(configs[0].v);
|
if (configs.length > 0) cache.config = normalizeConfig(configs[0].v);
|
||||||
else await conn.query('INSERT INTO kv_store (k, v) VALUES (?, ?)', ['config', JSON.stringify(cache.config)]);
|
else await conn.query('INSERT INTO kv_store (k, v) VALUES (?, ?)', ['config', JSON.stringify(cache.config)]);
|
||||||
|
|
||||||
const [machLocs] = await conn.query('SELECT v FROM kv_store WHERE k = ?', ['machine_locations']);
|
|
||||||
if (machLocs.length > 0) cache.machineLocations = machLocs[0].v || {};
|
|
||||||
|
|
||||||
const [stations] = await conn.query('SELECT data FROM stations');
|
const [stations] = await conn.query('SELECT data FROM stations');
|
||||||
cache.stations = stations.map(r => r.data);
|
cache.stations = stations.map(r => r.data);
|
||||||
|
|
||||||
@@ -526,19 +522,6 @@ const DataService = {
|
|||||||
}),
|
}),
|
||||||
saveExport: () => {
|
saveExport: () => {
|
||||||
try { fs.writeFileSync(paths.export, JSON.stringify(DataService.buildExportPayload(), null, 2)); } catch(e){}
|
try { fs.writeFileSync(paths.export, JSON.stringify(DataService.buildExportPayload(), null, 2)); } catch(e){}
|
||||||
},
|
|
||||||
|
|
||||||
getMachineLocations: () => cache.machineLocations,
|
|
||||||
updateMachineLocation: async (deviceId, data) => {
|
|
||||||
cache.machineLocations[deviceId] = {
|
|
||||||
...cache.machineLocations[deviceId],
|
|
||||||
...data,
|
|
||||||
last_update_ts: Date.now()
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
await pool.query('INSERT INTO kv_store (k, v) VALUES (?, ?) ON DUPLICATE KEY UPDATE v = VALUES(v)',
|
|
||||||
['machine_locations', JSON.stringify(cache.machineLocations)]);
|
|
||||||
} catch(e) { console.error('updateMachineLocation error', e); }
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+3
-24
@@ -1,6 +1,6 @@
|
|||||||
local CURRENT_STATION_CODE = 'Ticket-Machine'
|
local CURRENT_STATION_CODE = 'Ticket-Machine'
|
||||||
local API_BASE = 'http://ticket.fse-media.group/api'
|
local API_BASE = 'http://ticket.fse-media.group/api'
|
||||||
local VERSION = 'v1.5.18'
|
local VERSION = 'v1.5.15'
|
||||||
local UPDATE_CHECK_INTERVAL = 5
|
local UPDATE_CHECK_INTERVAL = 5
|
||||||
|
|
||||||
-- ###########################
|
-- ###########################
|
||||||
@@ -722,7 +722,6 @@ local function checkAndApplyUpdate()
|
|||||||
print("OTA Update Error: " .. tostring(res))
|
print("OTA Update Error: " .. tostring(res))
|
||||||
term.setTextColor(colors.white)
|
term.setTextColor(colors.white)
|
||||||
end
|
end
|
||||||
sleep(3)
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
lastUpdateError = "loadfile failed: " .. tostring(err)
|
lastUpdateError = "loadfile failed: " .. tostring(err)
|
||||||
@@ -731,7 +730,6 @@ local function checkAndApplyUpdate()
|
|||||||
print("OTA Update Error: " .. tostring(err))
|
print("OTA Update Error: " .. tostring(err))
|
||||||
term.setTextColor(colors.white)
|
term.setTextColor(colors.white)
|
||||||
end
|
end
|
||||||
sleep(3)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
isUpdating = false
|
isUpdating = false
|
||||||
@@ -777,26 +775,7 @@ local function backgroundTicketUploadTask()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function backgroundLocationSyncTask()
|
|
||||||
sleep(5)
|
|
||||||
local x, y, z
|
|
||||||
if gps and type(gps.locate) == "function" then
|
|
||||||
x, y, z = gps.locate(2)
|
|
||||||
end
|
|
||||||
local payload = {
|
|
||||||
device_id = currentDeviceId(),
|
|
||||||
type = "TicketMachine",
|
|
||||||
station = (CFG.current_station and (CFG.current_station.en_name or CFG.current_station.name)) or state.stationName,
|
|
||||||
station_code = state.stationCode or CURRENT_STATION_CODE,
|
|
||||||
x = x,
|
|
||||||
y = y,
|
|
||||||
z = z
|
|
||||||
}
|
|
||||||
postJSONResult(API_BASE .. '/machine-location', payload)
|
|
||||||
while true do
|
|
||||||
sleep(86400)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ###########################
|
-- ###########################
|
||||||
-- Data logic & Config
|
-- Data logic & Config
|
||||||
@@ -2549,4 +2528,4 @@ local function mainPageLoop()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
parallel.waitForAny(mainPageLoop, backgroundSyncTask, backgroundTicketUploadTask, backgroundPeripheralTask, backgroundLocationSyncTask)
|
parallel.waitForAny(mainPageLoop, backgroundSyncTask, backgroundTicketUploadTask, backgroundPeripheralTask)
|
||||||
|
|||||||
@@ -39,7 +39,6 @@
|
|||||||
<a href="https://ticket.fse-media.group/search" data-link="search">车票查询</a>
|
<a href="https://ticket.fse-media.group/search" data-link="search">车票查询</a>
|
||||||
<a href="https://ticket.fse-media.group/ic-card/search" data-link="card-search">IC 卡查询</a>
|
<a href="https://ticket.fse-media.group/ic-card/search" data-link="card-search">IC 卡查询</a>
|
||||||
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
||||||
<a href="https://ticket.fse-media.group/machine-map.html" data-link="machine-map">机器位置</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
<a href="https://ticket.fse-media.group/search" data-link="search">车票查询</a>
|
<a href="https://ticket.fse-media.group/search" data-link="search">车票查询</a>
|
||||||
<a href="https://ticket.fse-media.group/ic-card/search" data-link="card-search">IC 卡查询</a>
|
<a href="https://ticket.fse-media.group/ic-card/search" data-link="card-search">IC 卡查询</a>
|
||||||
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order" class="is-active">线上购卡</a>
|
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order" class="is-active">线上购卡</a>
|
||||||
<a href="https://ticket.fse-media.group/machine-map.html" data-link="machine-map">机器位置</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
<a href="https://ticket.fse-media.group/search" data-link="search">车票查询</a>
|
<a href="https://ticket.fse-media.group/search" data-link="search">车票查询</a>
|
||||||
<a href="https://ticket.fse-media.group/ic-card/search" data-link="card-search" class="is-active">IC 卡查询</a>
|
<a href="https://ticket.fse-media.group/ic-card/search" data-link="card-search" class="is-active">IC 卡查询</a>
|
||||||
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
||||||
<a href="https://ticket.fse-media.group/machine-map.html" data-link="machine-map">机器位置</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,9 +64,6 @@
|
|||||||
<div class="nav-item" :class="{active: currentView === 'faremap'}" @click="currentView = 'faremap'">
|
<div class="nav-item" :class="{active: currentView === 'faremap'}" @click="currentView = 'faremap'">
|
||||||
<span class="nav-icon"><i class="fas fa-map"></i></span> 票价地图
|
<span class="nav-icon"><i class="fas fa-map"></i></span> 票价地图
|
||||||
</div>
|
</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'">
|
<div class="nav-item" :class="{active: currentView === 'tickets'}" @click="currentView = 'tickets'">
|
||||||
<span class="nav-icon"><i class="fas fa-ticket-alt"></i></span> 车票记录
|
<span class="nav-icon"><i class="fas fa-ticket-alt"></i></span> 车票记录
|
||||||
</div>
|
</div>
|
||||||
@@ -644,99 +641,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 style="margin-top: 8px;">
|
|
||||||
<button class="btn sm primary" @click.stop="openEditMachineModal(id)">编辑</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 编辑机器位置 Modal -->
|
|
||||||
<div v-if="showEditMachineModal" class="jr-admin-modal-overlay" @click.self="showEditMachineModal = false">
|
|
||||||
<div class="jr-admin-modal">
|
|
||||||
<div class="jr-admin-modal-header">
|
|
||||||
<h3>编辑设备位置 - {{ editMachineForm.device_id }}</h3>
|
|
||||||
<button class="jr-admin-modal-close" @click="showEditMachineModal = false"><i class="fas fa-times"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="jr-admin-modal-body">
|
|
||||||
<div class="jr-admin-form-group">
|
|
||||||
<label>站点代码 / 名称</label>
|
|
||||||
<input type="text" v-model="editMachineForm.station_code" class="jr-admin-input">
|
|
||||||
</div>
|
|
||||||
<div class="jr-admin-form-group" style="display: flex; gap: 10px;">
|
|
||||||
<div style="flex: 1;">
|
|
||||||
<label>X 坐标</label>
|
|
||||||
<input type="number" v-model="editMachineForm.x" class="jr-admin-input">
|
|
||||||
</div>
|
|
||||||
<div style="flex: 1;">
|
|
||||||
<label>Y 坐标</label>
|
|
||||||
<input type="number" v-model="editMachineForm.y" class="jr-admin-input">
|
|
||||||
</div>
|
|
||||||
<div style="flex: 1;">
|
|
||||||
<label>Z 坐标</label>
|
|
||||||
<input type="number" v-model="editMachineForm.z" class="jr-admin-input">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="jr-admin-form-group">
|
|
||||||
<label>类型</label>
|
|
||||||
<select v-model="editMachineForm.type" class="jr-admin-input">
|
|
||||||
<option value="TicketMachine">售票机</option>
|
|
||||||
<option value="Gate">检票机</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="jr-admin-modal-footer">
|
|
||||||
<button class="btn" @click="showEditMachineModal = false">取消</button>
|
|
||||||
<button class="btn primary" @click="saveMachineLocation">保存</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 车票记录 -->
|
<!-- 车票记录 -->
|
||||||
<div v-if="currentView === 'tickets'">
|
<div v-if="currentView === 'tickets'">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
|
|||||||
+1
-92
@@ -24,7 +24,6 @@ createApp({
|
|||||||
dashboard: '仪表盘',
|
dashboard: '仪表盘',
|
||||||
management: '线路与票价管理',
|
management: '线路与票价管理',
|
||||||
faremap: '票价地图',
|
faremap: '票价地图',
|
||||||
machinemap: '机器位置',
|
|
||||||
tickets: '车票记录',
|
tickets: '车票记录',
|
||||||
vouchers: '凭证管理',
|
vouchers: '凭证管理',
|
||||||
iccards: 'IC 卡管理',
|
iccards: 'IC 卡管理',
|
||||||
@@ -79,14 +78,12 @@ createApp({
|
|||||||
let logDataLoaded = false;
|
let logDataLoaded = false;
|
||||||
let assetsLoaded = false;
|
let assetsLoaded = false;
|
||||||
let fareMapLoaded = false;
|
let fareMapLoaded = false;
|
||||||
let machineLocationsLoaded = false;
|
|
||||||
const loadingState = reactive({
|
const loadingState = reactive({
|
||||||
core: false,
|
core: false,
|
||||||
tickets: false,
|
tickets: false,
|
||||||
orders: false,
|
orders: false,
|
||||||
logs: false,
|
logs: false,
|
||||||
iccards: false,
|
iccards: false
|
||||||
machinemap: false
|
|
||||||
});
|
});
|
||||||
const lastSyncAt = ref(0);
|
const lastSyncAt = ref(0);
|
||||||
|
|
||||||
@@ -1119,82 +1116,6 @@ 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 showEditMachineModal = ref(false);
|
|
||||||
const editMachineForm = reactive({ device_id: '', station_code: '', x: 0, y: 0, z: 0, type: 'TicketMachine' });
|
|
||||||
|
|
||||||
const openEditMachineModal = (id) => {
|
|
||||||
const loc = machineLocations.value[id];
|
|
||||||
if (!loc) return;
|
|
||||||
editMachineForm.device_id = id;
|
|
||||||
editMachineForm.station_code = loc.station_code || loc.station || '';
|
|
||||||
editMachineForm.x = loc.x || 0;
|
|
||||||
editMachineForm.y = loc.y || 0;
|
|
||||||
editMachineForm.z = loc.z || 0;
|
|
||||||
editMachineForm.type = loc.type || 'TicketMachine';
|
|
||||||
showEditMachineModal.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveMachineLocation = async () => {
|
|
||||||
try {
|
|
||||||
const payload = { ...editMachineForm };
|
|
||||||
await requestJson(`/api/machine-location/${encodeURIComponent(payload.device_id)}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
}, { expectOk: true });
|
|
||||||
showEditMachineModal.value = false;
|
|
||||||
await fetchMachineLocations();
|
|
||||||
} catch (e) {
|
|
||||||
alert('保存失败: ' + e.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensureViewData = async (view = currentView.value, { force = false } = {}) => {
|
const ensureViewData = async (view = currentView.value, { force = false } = {}) => {
|
||||||
await fetchCoreData({ force });
|
await fetchCoreData({ force });
|
||||||
if (view === 'tickets' && (force || !ticketDataLoaded)) await fetchTicketData();
|
if (view === 'tickets' && (force || !ticketDataLoaded)) await fetchTicketData();
|
||||||
@@ -1208,7 +1129,6 @@ createApp({
|
|||||||
}
|
}
|
||||||
if (view === 'faremap' && (force || !fareMapLoaded)) await loadFareMap({ force });
|
if (view === 'faremap' && (force || !fareMapLoaded)) await loadFareMap({ force });
|
||||||
if (view === 'assets' && (!assetsLoaded || force)) await fetchAssetsManifest();
|
if (view === 'assets' && (!assetsLoaded || force)) await fetchAssetsManifest();
|
||||||
if (view === 'machinemap') await fetchMachineLocations();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -1644,8 +1564,6 @@ createApp({
|
|||||||
syncSelectedIcCard().catch(console.error);
|
syncSelectedIcCard().catch(console.error);
|
||||||
});
|
});
|
||||||
|
|
||||||
let machineMapTimer = null;
|
|
||||||
|
|
||||||
watch(currentView, (v) => {
|
watch(currentView, (v) => {
|
||||||
sidebarOpen.value = false;
|
sidebarOpen.value = false;
|
||||||
if (v === 'iccards') {
|
if (v === 'iccards') {
|
||||||
@@ -1653,14 +1571,6 @@ createApp({
|
|||||||
} else {
|
} else {
|
||||||
stopIcCardSync();
|
stopIcCardSync();
|
||||||
}
|
}
|
||||||
if (v === 'machinemap') {
|
|
||||||
if (!machineMapTimer) machineMapTimer = setInterval(fetchMachineLocations, 60000);
|
|
||||||
} else {
|
|
||||||
if (machineMapTimer) {
|
|
||||||
clearInterval(machineMapTimer);
|
|
||||||
machineMapTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ensureViewData(v).catch(console.error);
|
ensureViewData(v).catch(console.error);
|
||||||
const sp = new URLSearchParams(location.search);
|
const sp = new URLSearchParams(location.search);
|
||||||
if (v === 'dashboard') sp.delete('view');
|
if (v === 'dashboard') sp.delete('view');
|
||||||
@@ -1810,7 +1720,6 @@ createApp({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
machineLocations, selectedMachineId, getMapX, getMapY, showEditMachineModal, editMachineForm, openEditMachineModal, saveMachineLocation,
|
|
||||||
currentView, viewTitle, sidebarOpen,
|
currentView, viewTitle, sidebarOpen,
|
||||||
loadingState, isViewBusy, lastSyncText, currentViewSummary,
|
loadingState, isViewBusy, lastSyncText, currentViewSummary,
|
||||||
stations, lines, fares, stats, config, recentLogs, ticketList, ticketStats, orderStats,
|
stations, lines, fares, stats, config, recentLogs, ticketList, ticketStats, orderStats,
|
||||||
|
|||||||
+85
-102
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>机器位置|FSE铁路运输票务系统</title>
|
<title>票务查询|FSE铁路运输票务系统</title>
|
||||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/style.css?v=14" />
|
<link rel="stylesheet" href="/style.css?v=14" />
|
||||||
@@ -38,130 +38,113 @@
|
|||||||
<nav class="jr-nav" aria-label="站点导航">
|
<nav class="jr-nav" aria-label="站点导航">
|
||||||
<a href="https://ticket.fse-media.group/home.html" data-link="home">首页</a>
|
<a href="https://ticket.fse-media.group/home.html" data-link="home">首页</a>
|
||||||
<a href="https://ticket.fse-media.group/order" data-link="order">线上预定</a>
|
<a href="https://ticket.fse-media.group/order" data-link="order">线上预定</a>
|
||||||
<a href="https://ticket.fse-media.group/search" data-link="search">车票查询</a>
|
<a href="https://ticket.fse-media.group/search" data-link="search" class="is-active">车票查询</a>
|
||||||
<a href="https://ticket.fse-media.group/ic-card/search" data-link="card-search">IC 卡查询</a>
|
<a href="https://ticket.fse-media.group/ic-card/search" data-link="card-search">IC 卡查询</a>
|
||||||
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
||||||
<a href="https://ticket.fse-media.group/machine-map.html" data-link="machine-map" class="is-active">机器位置</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main class="jr-public-main">
|
<main class="jr-public-main">
|
||||||
<section class="jr-page-intro">
|
<section class="jr-page-intro">
|
||||||
<span class="jr-kicker">MACHINE MAP</span>
|
<span class="jr-kicker">TICKET SEARCH</span>
|
||||||
<h1>设备分布图</h1>
|
<h1>按票号、站点或日期快速查询票据</h1>
|
||||||
<p>实时查看所有售票机和检票机的位置及状态。</p>
|
<p>输入完整票号、起终点、日期或关键词,左侧浏览结果,右侧查看票据详情与最近流转记录。</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<section class="jr-panel-card" style="margin-bottom:24px;">
|
<section class="jr-panel-card" style="margin-bottom:24px;">
|
||||||
<div class="jr-panel-headline">
|
<div class="jr-panel-headline">
|
||||||
<h2>机器位置</h2>
|
<h2>检索条件</h2>
|
||||||
<span class="jr-panel-note">Live Map</span>
|
<span class="jr-panel-note">Ticket ID / Station / Date</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="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);">
|
<div class="jr-search-form">
|
||||||
<div id="map-loading" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #666;">
|
<input id="q" class="jr-search-input" type="text"
|
||||||
加载中...
|
placeholder="输入完整票号 / 起点 / 终点 / 日期 (YYYY-MM-DD)" />
|
||||||
|
<button id="searchBtn" class="btn primary jr-search-button"><i class="fas fa-search"></i>
|
||||||
|
立即搜索</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="jr-search-helper">可直接输入完整票号,也可输入起点、终点或日期关键字进行模糊检索。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="jr-search-results">
|
||||||
|
<article class="jr-panel-card">
|
||||||
|
<div class="jr-panel-headline">
|
||||||
|
<h3>结果列表</h3>
|
||||||
|
<span class="jr-panel-note">Search Results</span>
|
||||||
|
</div>
|
||||||
|
<div id="list" class="jr-scroll-box">
|
||||||
|
<div class="jr-center-empty">
|
||||||
|
<p>请输入关键词开始查询。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<section id="detail-section" class="jr-detail-stack">
|
||||||
|
<article class="jr-panel-card" style="margin-bottom:20px;">
|
||||||
|
<div class="jr-panel-headline">
|
||||||
|
<h3>车票详情</h3>
|
||||||
|
<span class="jr-panel-note">Ticket Overview</span>
|
||||||
|
</div>
|
||||||
|
<div id="detail">
|
||||||
|
<div class="jr-center-empty">
|
||||||
|
<p>从左侧选择一张车票以查看详情。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div class="jr-grid-two">
|
||||||
|
<article class="jr-panel-card">
|
||||||
|
<div class="jr-panel-headline">
|
||||||
|
<h3>热门站点</h3>
|
||||||
|
<span class="jr-panel-note">Popular Stations</span>
|
||||||
|
</div>
|
||||||
|
<div id="popularStations" class="jr-popular-list"></div>
|
||||||
|
</article>
|
||||||
|
<article class="jr-panel-card">
|
||||||
|
<div class="jr-panel-headline">
|
||||||
|
<h3>热门路线</h3>
|
||||||
|
<span class="jr-panel-note">Popular Routes</span>
|
||||||
|
</div>
|
||||||
|
<div id="popularRoutes" class="jr-popular-list"></div>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="site-footer jr-footer-space">
|
||||||
|
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">粤ICP备2025450093号</a>
|
||||||
|
<span class="version">v1.0.12</span>
|
||||||
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/public-status.js"></script>
|
|
||||||
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
|
<script src="/ticket-search.js?v=12"></script>
|
||||||
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script>
|
<script>
|
||||||
let machineLocations = {};
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
let selectedMachineId = null;
|
const isDomain = location.hostname.includes('fse-media.group');
|
||||||
|
const links = {
|
||||||
function getMapX(x) {
|
home: isDomain ? 'https://ticket.fse-media.group' : '/home.html',
|
||||||
if (typeof x !== 'number') return 50;
|
order: isDomain ? 'https://ticket.fse-media.group/order' : '/ticket-order.html',
|
||||||
let min = 0, max = 0;
|
search: isDomain ? 'https://ticket.fse-media.group/search' : '/ticket-search.html',
|
||||||
const vals = Object.values(machineLocations).map(l => l.x).filter(v => typeof v === 'number');
|
'card-search': isDomain ? 'https://ticket.fse-media.group/ic-card/search' : '/ic-card-search.html',
|
||||||
if (vals.length > 0) {
|
'card-order': isDomain ? 'https://ticket.fse-media.group/ic-card/order' : '/ic-card-order.html'
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMapY(z) {
|
|
||||||
if (typeof z !== 'number') return 50;
|
|
||||||
let min = 0, max = 0;
|
|
||||||
const vals = Object.values(machineLocations).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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMap() {
|
|
||||||
const container = document.getElementById('map-container');
|
|
||||||
if (Object.keys(machineLocations).length === 0) {
|
|
||||||
container.innerHTML = `<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #666;">暂无机器坐标数据</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
for (const [id, loc] of Object.entries(machineLocations)) {
|
|
||||||
const x = getMapX(loc.x);
|
|
||||||
const y = getMapY(loc.z);
|
|
||||||
const isSelected = selectedMachineId === id;
|
|
||||||
const isGate = loc.type === 'Gate';
|
|
||||||
const color = isGate ? '#ff9800' : '#4ade80';
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<div class="map-marker" style="position: absolute; left: ${x}%; top: ${y}%; transform: translate(-50%, -50%); cursor: pointer; z-index: ${isSelected ? 10 : 1};" onclick="selectMachine('${id}')">
|
|
||||||
<div style="width: 20px; height: 20px; border-radius: 50%; background: ${color}; border: 2px solid #fff; box-shadow: 0 0 10px rgba(0,0,0,0.5);"></div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
html += `
|
|
||||||
<div 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;">类型: ${isGate ? '检票机' : '售票机'}</div>
|
|
||||||
`;
|
|
||||||
if (isGate) {
|
|
||||||
const mode = loc.gate_mode === 'entry' ? '进站' : (loc.gate_mode === 'exit' ? '出站' : '双向');
|
|
||||||
html += `<div style="font-size: 0.85em; color: #aaa;">模式: ${mode}</div>`;
|
|
||||||
}
|
|
||||||
html += `
|
|
||||||
<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>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
html += `</div>`;
|
|
||||||
}
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.selectMachine = function(id) {
|
|
||||||
selectedMachineId = id;
|
|
||||||
renderMap();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function fetchLocations() {
|
const homeLink = document.getElementById('homeLink');
|
||||||
try {
|
const brandLink = document.getElementById('brandLink');
|
||||||
const res = await fetch('/api/public/machine-locations');
|
if (homeLink) homeLink.href = links.home;
|
||||||
const data = await res.json();
|
if (brandLink) brandLink.href = links.home;
|
||||||
if (data && data.ok) {
|
|
||||||
machineLocations = data.data || {};
|
|
||||||
renderMap();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.querySelectorAll('[data-link]').forEach((el) => {
|
||||||
fetchLocations();
|
const key = el.getAttribute('data-link');
|
||||||
setInterval(fetchLocations, 60000);
|
if (links[key]) el.href = links[key];
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
<a href="https://ticket.fse-media.group/search" data-link="search">车票查询</a>
|
<a href="https://ticket.fse-media.group/search" data-link="search">车票查询</a>
|
||||||
<a href="https://ticket.fse-media.group/ic-card/search" data-link="card-search">IC 卡查询</a>
|
<a href="https://ticket.fse-media.group/ic-card/search" data-link="card-search">IC 卡查询</a>
|
||||||
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
||||||
<a href="https://ticket.fse-media.group/machine-map.html" data-link="machine-map">机器位置</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,7 +41,6 @@
|
|||||||
<a href="https://ticket.fse-media.group/search" data-link="search" class="is-active">车票查询</a>
|
<a href="https://ticket.fse-media.group/search" data-link="search" class="is-active">车票查询</a>
|
||||||
<a href="https://ticket.fse-media.group/ic-card/search" data-link="card-search">IC 卡查询</a>
|
<a href="https://ticket.fse-media.group/ic-card/search" data-link="card-search">IC 卡查询</a>
|
||||||
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
||||||
<a href="https://ticket.fse-media.group/machine-map.html" data-link="machine-map">机器位置</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user