feat(web,installer): 更新下载源、升级资源缓存版本、本地化界面并新增管理功能
- 更新 update_machine.lua 和 installer.lua 中的远程资源下载地址,从旧云存储链接切换为 Gitea 仓库提交镜像地址 - 新增双向闸机专用安装脚本 installer_bi.lua - 为所有网页HTML文件更新静态资源的缓存版本号,避免浏览器加载过期的静态文件缓存 - 修复登录页面的乱码文本,替换为标准简体中文内容,修正ICP备案标识文本 - 新增管理后台概览板块、快捷操作按钮,优化IC卡管理界面与响应式布局样式
This commit is contained in:
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
local URL_ERROR = "http://cloud.fse-media.group/d/API/TicketMachine/error.dfpwm?sign=DvQ39wQDN6Cej4KPhAkM3JyXPM466koan6w9CckPxWU=:0"
|
local URL_ERROR = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/src/commit/108435e90d81c1c0a6e34486dee6cc6efd48ca53/error.dfpwm"
|
||||||
local URL_PASS = "http://cloud.fse-media.group/d/API/TicketMachine/pass.dfpwm?sign=ZJfGDYnaxQU4JrGSh4NDzKZtjq3eMjYh9KHmMSAMKZA=:0"
|
local URL_PASS = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/src/commit/108435e90d81c1c0a6e34486dee6cc6efd48ca53/pass.dfpwm"
|
||||||
local URL_GATE = "http://cloud.fse-media.group/d/API/TicketMachine/gate.lua?sign=OWy1wKkKhUhpnxXKeX6fRLePSg1XcaQgWOLvQbMuHRQ=:0"
|
local URL_GATE = "http://cloud.fse-media.group/d/API/TicketMachine/gate.lua?sign=OWy1wKkKhUhpnxXKeX6fRLePSg1XcaQgWOLvQbMuHRQ=:0"
|
||||||
|
|
||||||
local CONFIG_PATH = "gate_config.json"
|
local CONFIG_PATH = "gate_config.json"
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
local URL_ERROR = "http://cloud.fse-media.group/d/API/TicketMachine/error.dfpwm?sign=DvQ39wQDN6Cej4KPhAkM3JyXPM466koan6w9CckPxWU=:0"
|
||||||
|
local URL_PASS = "http://cloud.fse-media.group/d/API/TicketMachine/pass.dfpwm?sign=ZJfGDYnaxQU4JrGSh4NDzKZtjq3eMjYh9KHmMSAMKZA=:0"
|
||||||
|
local URL_GATE = "http://cloud.fse-media.group/d/API/TicketMachine/gate.lua?sign=OWy1wKkKhUhpnxXKeX6fRLePSg1XcaQgWOLvQbMuHRQ=:0"
|
||||||
|
|
||||||
|
local CONFIG_PATH = "gate_config.json"
|
||||||
|
|
||||||
|
local function trim(s)
|
||||||
|
return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", ""))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function splitCsv(s)
|
||||||
|
local out = {}
|
||||||
|
s = trim(s)
|
||||||
|
if #s == 0 then return out end
|
||||||
|
for part in s:gmatch("[^,/%s]+") do
|
||||||
|
local v = trim(part)
|
||||||
|
if #v > 0 then table.insert(out, v) end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
local function writeFile(path, content, binary)
|
||||||
|
local mode = binary and "wb" or "w"
|
||||||
|
local f = fs.open(path, mode)
|
||||||
|
if not f then return false end
|
||||||
|
f.write(content)
|
||||||
|
f.close()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function prompt(label)
|
||||||
|
term.write(label)
|
||||||
|
return trim(read() or "")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function promptOptionalUrl(label)
|
||||||
|
local raw = prompt(label)
|
||||||
|
raw = trim(raw)
|
||||||
|
if #raw == 0 then return nil end
|
||||||
|
return raw
|
||||||
|
end
|
||||||
|
|
||||||
|
local function httpGet(url)
|
||||||
|
if not http then return false, "HTTP API disabled" end
|
||||||
|
local okReq, err = pcall(function()
|
||||||
|
http.request({ url = url, method = "GET" })
|
||||||
|
end)
|
||||||
|
if not okReq then return false, tostring(err) end
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local ev, p1, p2, p3 = os.pullEvent()
|
||||||
|
if ev == "http_success" and p1 == url then
|
||||||
|
local res = p2
|
||||||
|
if type(res) == "table" and type(res.readAll) == "function" then
|
||||||
|
local data = res.readAll()
|
||||||
|
res.close()
|
||||||
|
return true, data
|
||||||
|
end
|
||||||
|
return false, "invalid http response"
|
||||||
|
end
|
||||||
|
if ev == "http_failure" and p1 == url then
|
||||||
|
local err = p2
|
||||||
|
local res = p3
|
||||||
|
if type(p2) == "table" and type(p2.readAll) == "function" then
|
||||||
|
res = p2
|
||||||
|
err = p3
|
||||||
|
end
|
||||||
|
if type(res) == "table" and type(res.readAll) == "function" then
|
||||||
|
local data = res.readAll()
|
||||||
|
res.close()
|
||||||
|
return false, data
|
||||||
|
end
|
||||||
|
return false, tostring(err or "http_failure")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function download(url, path, binary)
|
||||||
|
print("Downloading: " .. path)
|
||||||
|
local ok, data = httpGet(url)
|
||||||
|
if not ok then
|
||||||
|
print("Download failed: " .. tostring(data or ""))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if not writeFile(path, data, binary) then
|
||||||
|
print("Write failed: " .. path)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function normalizeMode(raw)
|
||||||
|
return (trim(raw):lower() == "exit") and "exit" or "entry"
|
||||||
|
end
|
||||||
|
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
print("Bidirectional Ticket Gate Installer")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
local stationsRaw = prompt("Station codes (comma or slash): ")
|
||||||
|
local stationCodes = splitCsv(stationsRaw)
|
||||||
|
if #stationCodes == 0 then
|
||||||
|
print("No station codes provided.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Set mode for each side (front/back).")
|
||||||
|
local frontRaw = prompt("Front mode (entry/exit): ")
|
||||||
|
local backRaw = prompt("Back mode (entry/exit): ")
|
||||||
|
local frontMode = normalizeMode(frontRaw)
|
||||||
|
local backMode = normalizeMode(backRaw)
|
||||||
|
local frontStationCode = trim(prompt("Front station code (default first code): "))
|
||||||
|
local backStationCode = trim(prompt("Back station code (default first code): "))
|
||||||
|
if #frontStationCode == 0 then frontStationCode = stationCodes[1] end
|
||||||
|
if #backStationCode == 0 then backStationCode = stationCodes[1] end
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Optional server config for ticket / IC card checks.")
|
||||||
|
local ticketServerUrl = promptOptionalUrl("Ticket check URL (blank=auto): ")
|
||||||
|
local cardServerUrl = promptOptionalUrl("IC card check URL (blank=same server): ")
|
||||||
|
|
||||||
|
local cfg = {
|
||||||
|
station_codes = stationCodes,
|
||||||
|
station_code = stationCodes[1],
|
||||||
|
side_modes = {
|
||||||
|
front = frontMode,
|
||||||
|
back = backMode,
|
||||||
|
},
|
||||||
|
side_station_codes = {
|
||||||
|
front = frontStationCode,
|
||||||
|
back = backStationCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ticketServerUrl then cfg.server_url = ticketServerUrl end
|
||||||
|
if cardServerUrl then cfg.card_server_url = cardServerUrl end
|
||||||
|
local okCfg, cfgJson = pcall(textutils.serializeJSON, cfg)
|
||||||
|
if not okCfg then
|
||||||
|
print("Config serialize failed.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not writeFile(CONFIG_PATH, cfgJson, false) then
|
||||||
|
print("Failed to write config.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not download(URL_PASS, "pass.dfpwm", true) then return end
|
||||||
|
if not download(URL_ERROR, "error.dfpwm", true) then return end
|
||||||
|
|
||||||
|
local okGate, gateCode = httpGet(URL_GATE)
|
||||||
|
if not okGate then
|
||||||
|
print("Download failed: startup")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
writeFile("startup.lua", gateCode, false)
|
||||||
|
writeFile("startup", gateCode, false)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Done.")
|
||||||
|
print("This gate now supports tickets and IC cards.")
|
||||||
|
print("Attach ticket_inspection_machine on FRONT and BACK.")
|
||||||
|
print("Reboot the computer to start the gate.")
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
local URL_MACHINE = "http://cloud.fse-media.group/d/API/TicketMachine/ticketmachine.lua?sign=UIiheDcpyzwKdovDZM3G-8IRZqApFgU2Kpnhe9k5ETQ=:0"
|
local URL_MACHINE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/src/commit/db1562b83045284bfdec9e4a3feb829193963943/ticketmachine.lua"
|
||||||
|
|
||||||
local function writeFile(path, content, binary)
|
local function writeFile(path, content, binary)
|
||||||
local mode = binary and "wb" or "w"
|
local mode = binary and "wb" or "w"
|
||||||
|
|||||||
+3
-2
@@ -6,7 +6,7 @@
|
|||||||
<title>FMG</title>
|
<title>FMG</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=12">
|
<link rel="stylesheet" href="style.css?v=13">
|
||||||
<link rel="stylesheet" href="blog.css?v=2">
|
<link rel="stylesheet" href="blog.css?v=2">
|
||||||
</head>
|
</head>
|
||||||
<body class="public-search">
|
<body class="public-search">
|
||||||
@@ -59,7 +59,8 @@ FMG
|
|||||||
<span class="version">v1.0.12</span>
|
<span class="version">v1.0.12</span>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="blog.js?v=2"></script>
|
<script src="blog.js?v=2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -6,7 +6,7 @@
|
|||||||
<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=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
</head>
|
</head>
|
||||||
<body class="public-search jr-public-page">
|
<body class="public-search jr-public-page">
|
||||||
<div class="jr-public-shell">
|
<div class="jr-public-shell">
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="/ai-assistant.js?v=6"></script>
|
<script src="/ai-assistant.js?v=6"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -247,3 +247,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>FSE 閾佽矾绁ㄥ姟绯荤粺 - IC 鍗$鐞?/title>
|
<title>FSE 閾佽矾绁ㄥ姟绯荤粺 - IC 鍗$鐞?/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=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="jr-admin-page jr-admin-ic-page jr-public-page">
|
<body class="jr-admin-page jr-admin-ic-page jr-public-page">
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="/ic-card-admin.js?v=2"></script>
|
<script src="/ic-card-admin.js?v=2"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -206,3 +206,4 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>IC 卡详情</title>
|
<title>IC 卡详情</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=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="public-search jr-public-page">
|
<body class="public-search jr-public-page">
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/ic-card-detail.js?v=2"></script>
|
<script src="/ic-card-detail.js?v=2"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script>document.addEventListener('DOMContentLoaded', () => {
|
<script>document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -110,3 +110,4 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>IC 鍗$嚎涓婅喘鍗?/title>
|
<title>IC 鍗$嚎涓婅喘鍗?/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=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="public-search jr-public-page">
|
<body class="public-search jr-public-page">
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/ic-card-order.js?v=2"></script>
|
<script src="/ic-card-order.js?v=2"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="/ai-assistant.js?v=6"></script>
|
<script src="/ai-assistant.js?v=6"></script>
|
||||||
@@ -129,3 +129,4 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>IC 鍗℃煡璇?/title>
|
<title>IC 鍗℃煡璇?/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=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="public-search jr-public-page">
|
<body class="public-search jr-public-page">
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/ic-card-search.js?v=2"></script>
|
<script src="/ic-card-search.js?v=2"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script>document.addEventListener('DOMContentLoaded', () => {
|
<script>document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -101,3 +101,4 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
+92
-5
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<!-- 充满未知和不稳定的票务系统! -->
|
<!-- 充满未知和不稳定的票务系统! -->
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>FSE铁路票务系统控制台</title>
|
<title>FSE铁路票务系统控制台</title>
|
||||||
<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=12">
|
<link rel="stylesheet" href="style.css?v=13">
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
<script src="/socket.io/socket.io.js"></script>
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -112,10 +112,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-admin-header-side">
|
<div class="jr-admin-header-side">
|
||||||
|
<div class="jr-admin-sync-meta">
|
||||||
|
<span class="jr-admin-sync-label">当前模块</span>
|
||||||
|
<strong>{{ viewTitle }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="jr-admin-sync-meta">
|
||||||
|
<span class="jr-admin-sync-label">最近同步</span>
|
||||||
|
<strong>{{ lastSyncText }}</strong>
|
||||||
|
</div>
|
||||||
<span class="jr-admin-header-pill" :class="connected ? 'is-online' : 'is-offline'">
|
<span class="jr-admin-header-pill" :class="connected ? 'is-online' : 'is-offline'">
|
||||||
<i class="fas fa-circle"></i>
|
<i class="fas fa-circle"></i>
|
||||||
{{ connected ? '服务器在线' : '服务器离线' }}
|
{{ connected ? '服务器在线' : '服务器离线' }}
|
||||||
</span>
|
</span>
|
||||||
|
<button class="btn primary" @click="refreshData" :disabled="isViewBusy">
|
||||||
|
<i class="fas" :class="isViewBusy ? 'fa-spinner fa-spin' : 'fa-rotate-right'"></i>
|
||||||
|
{{ isViewBusy ? '同步中' : '刷新视图' }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@@ -131,6 +143,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<p>当前模块:{{ viewTitle }},已加载 {{ lines.length }} 条线路、{{ stations.length }} 个站点。需要做批量调整时,可先在左侧切换模块再进入对应卡片操作区。</p>
|
<p>当前模块:{{ viewTitle }},已加载 {{ lines.length }} 条线路、{{ stations.length }} 个站点。需要做批量调整时,可先在左侧切换模块再进入对应卡片操作区。</p>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="jr-admin-overview-grid">
|
||||||
|
<article class="jr-admin-overview-card">
|
||||||
|
<span class="jr-admin-overview-label">当前模块</span>
|
||||||
|
<strong class="jr-admin-overview-value">{{ viewTitle }}</strong>
|
||||||
|
<p class="jr-admin-overview-note">{{ currentViewSummary }}</p>
|
||||||
|
</article>
|
||||||
|
<article class="jr-admin-overview-card">
|
||||||
|
<span class="jr-admin-overview-label">线路与站点</span>
|
||||||
|
<strong class="jr-admin-overview-value">{{ lines.length }} / {{ stations.length }}</strong>
|
||||||
|
<p class="jr-admin-overview-note">后台操作统一建立在线路、站点与票价的核心数据之上。</p>
|
||||||
|
</article>
|
||||||
|
<article class="jr-admin-overview-card">
|
||||||
|
<span class="jr-admin-overview-label">同步状态</span>
|
||||||
|
<strong class="jr-admin-overview-value">{{ isViewBusy ? '正在更新' : '数据已就绪' }}</strong>
|
||||||
|
<p class="jr-admin-overview-note">切换模块时只拉取当前视图需要的数据,减少等待与无效刷新。</p>
|
||||||
|
</article>
|
||||||
|
<article class="jr-admin-overview-card is-actions">
|
||||||
|
<span class="jr-admin-overview-label">快捷操作</span>
|
||||||
|
<div class="jr-admin-overview-actions">
|
||||||
|
<button class="btn" @click="currentView = 'management'"><i class="fas fa-network-wired"></i> 线路管理</button>
|
||||||
|
<button class="btn" @click="currentView = 'iccards'"><i class="fas fa-credit-card"></i> IC 卡务</button>
|
||||||
|
<button class="btn" @click="currentView = 'logs'"><i class="fas fa-list"></i> 查看日志</button>
|
||||||
|
<button class="btn primary" @click="refreshData" :disabled="isViewBusy"><i class="fas fa-rotate-right"></i> 立即同步</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
<!-- 仪表盘-->
|
<!-- 仪表盘-->
|
||||||
<div v-if="currentView === 'dashboard'">
|
<div v-if="currentView === 'dashboard'">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
@@ -479,6 +517,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="currentView === 'iccards'">
|
<div v-if="currentView === 'iccards'">
|
||||||
|
<section class="jr-admin-section-toolbar">
|
||||||
|
<div class="jr-admin-section-toolbar-copy">
|
||||||
|
<span class="jr-admin-overview-label">IC CARD DESK</span>
|
||||||
|
<strong>{{ currentViewSummary }}</strong>
|
||||||
|
<p>把检索、充值、状态维护和事件核对集中在同一工作流里,减少在列表和详情之间来回跳转的成本。</p>
|
||||||
|
</div>
|
||||||
|
<div class="jr-admin-overview-actions">
|
||||||
|
<button class="btn" @click="fetchIcCards(false)" :disabled="isViewBusy"><i class="fas fa-list"></i> 刷新列表</button>
|
||||||
|
<button class="btn primary" @click="loadIcCard(icSelectedId)" :disabled="!icSelectedId || isViewBusy"><i class="fas fa-id-card"></i> 刷新详情</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="stat-label">IC 卡总数</div>
|
<div class="stat-label">IC 卡总数</div>
|
||||||
@@ -500,9 +549,24 @@
|
|||||||
|
|
||||||
<div class="management-container ic-admin-layout">
|
<div class="management-container ic-admin-layout">
|
||||||
<div class="management-sidebar">
|
<div class="management-sidebar">
|
||||||
<div class="card" style="margin-bottom:0; display:flex; flex-direction:column; min-height: 520px;">
|
<div class="card jr-admin-note-card">
|
||||||
<div class="flex between mb-4">
|
<div class="flex between mb-4">
|
||||||
|
<h4>操作说明</h4>
|
||||||
|
<span class="badge">只读入口</span>
|
||||||
|
</div>
|
||||||
|
<p class="jr-admin-card-note">本模块不提供后台快速建卡,卡片发放流程保持在线上购卡或既有开卡流程中完成,后台仅负责检索、维护、充值与记录核对。</p>
|
||||||
|
<div class="jr-admin-note-list">
|
||||||
|
<div>1. 先检索卡号、订单号或持卡人。</div>
|
||||||
|
<div>2. 在详情面板修改状态并保存。</div>
|
||||||
|
<div>3. 需要补款时直接使用充值入口。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card jr-admin-list-card" style="margin-bottom:0; display:flex; flex-direction:column; min-height: 520px;">
|
||||||
|
<div class="flex between mb-4">
|
||||||
|
<div>
|
||||||
<h4>卡片列表</h4>
|
<h4>卡片列表</h4>
|
||||||
|
<div class="jr-admin-list-meta">支持按卡号、订单号、凭证码和持卡人姓名检索。</div>
|
||||||
|
</div>
|
||||||
<span class="badge">{{ icCards.length }}</span>
|
<span class="badge">{{ icCards.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mb-4" style="flex-wrap:wrap;">
|
<div class="flex mb-4" style="flex-wrap:wrap;">
|
||||||
@@ -536,8 +600,13 @@
|
|||||||
<div class="management-main">
|
<div class="management-main">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="flex between mb-4">
|
<div class="flex between mb-4">
|
||||||
|
<div>
|
||||||
<h4>卡片详情</h4>
|
<h4>卡片详情</h4>
|
||||||
|
<div class="jr-admin-list-meta">在同一面板直接处理状态维护、充值和记录核对。</div>
|
||||||
|
</div>
|
||||||
<div class="flex" style="gap:8px;">
|
<div class="flex" style="gap:8px;">
|
||||||
|
<button class="btn" @click="loadIcCard(icSelectedId)" :disabled="!icSelectedCard || isViewBusy"><i class="fas fa-rotate-right"></i> 刷新</button>
|
||||||
|
<button class="btn" @click="topupIcCard" :disabled="!icSelectedCard || isViewBusy"><i class="fas fa-wallet"></i> 充值</button>
|
||||||
<button class="btn danger" @click="deleteIcCard" :disabled="!icSelectedCard"><i class="fas fa-trash"></i> 删除卡</button>
|
<button class="btn danger" @click="deleteIcCard" :disabled="!icSelectedCard"><i class="fas fa-trash"></i> 删除卡</button>
|
||||||
<button class="btn primary" @click="saveIcCard" :disabled="!icSelectedCard"><i class="fas fa-save"></i> 保存</button>
|
<button class="btn primary" @click="saveIcCard" :disabled="!icSelectedCard"><i class="fas fa-save"></i> 保存</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -554,6 +623,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="badge" :class="icStatusInfo(icSelectedCard.status).className">{{ icStatusInfo(icSelectedCard.status).text }}</span>
|
<span class="badge" :class="icStatusInfo(icSelectedCard.status).className">{{ icStatusInfo(icSelectedCard.status).text }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="jr-admin-summary-grid">
|
||||||
|
<div class="jr-admin-summary-item">
|
||||||
|
<span>当前余额</span>
|
||||||
|
<strong>{{ formatMoney(icSelectedCard.balance) }}</strong>
|
||||||
|
<small>支持直接发起充值。</small>
|
||||||
|
</div>
|
||||||
|
<div class="jr-admin-summary-item">
|
||||||
|
<span>事件记录</span>
|
||||||
|
<strong>{{ icSelectedEvents.length }}</strong>
|
||||||
|
<small>用于追踪开卡与状态变更。</small>
|
||||||
|
</div>
|
||||||
|
<div class="jr-admin-summary-item">
|
||||||
|
<span>订单来源</span>
|
||||||
|
<strong>{{ cardOrderCode(icSelectedCard) }}</strong>
|
||||||
|
<small>自动识别线上订单或现场办卡。</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="ic-detail-grid">
|
<div class="ic-detail-grid">
|
||||||
<label class="ic-field">
|
<label class="ic-field">
|
||||||
<span>持卡人</span>
|
<span>持卡人</span>
|
||||||
@@ -841,9 +927,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="index.js?v=2"></script>
|
<script src="index.js?v=3"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const isDomain = location.hostname.includes('fse-media.group');
|
const isDomain = location.hostname.includes('fse-media.group');
|
||||||
@@ -873,3 +959,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+172
-52
@@ -63,6 +63,24 @@ createApp({
|
|||||||
const icCreateForm = reactive({ holder_name: '', balance: 50 });
|
const icCreateForm = reactive({ holder_name: '', balance: 50 });
|
||||||
const icDetailForm = reactive({ holder_name: '', status: 'active' });
|
const icDetailForm = reactive({ holder_name: '', status: 'active' });
|
||||||
let icCardSyncTimer = null;
|
let icCardSyncTimer = null;
|
||||||
|
let icCardSyncBusy = false;
|
||||||
|
let icListRequestSeq = 0;
|
||||||
|
let icDetailRequestSeq = 0;
|
||||||
|
let appMouseupHandler = null;
|
||||||
|
let coreLoaded = false;
|
||||||
|
let ticketDataLoaded = false;
|
||||||
|
let orderDataLoaded = false;
|
||||||
|
let logDataLoaded = false;
|
||||||
|
let assetsLoaded = false;
|
||||||
|
let fareMapLoaded = false;
|
||||||
|
const loadingState = reactive({
|
||||||
|
core: false,
|
||||||
|
tickets: false,
|
||||||
|
orders: false,
|
||||||
|
logs: false,
|
||||||
|
iccards: false
|
||||||
|
});
|
||||||
|
const lastSyncAt = ref(0);
|
||||||
|
|
||||||
// UI State
|
// UI State
|
||||||
const showAddLine = ref(false);
|
const showAddLine = ref(false);
|
||||||
@@ -103,6 +121,9 @@ createApp({
|
|||||||
confirm: (message) => Promise.resolve(window.confirm(message)),
|
confirm: (message) => Promise.resolve(window.confirm(message)),
|
||||||
prompt: (message, defaultValue) => Promise.resolve(window.prompt(message, defaultValue))
|
prompt: (message, defaultValue) => Promise.resolve(window.prompt(message, defaultValue))
|
||||||
};
|
};
|
||||||
|
const markSynced = () => {
|
||||||
|
lastSyncAt.value = Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
const buildAssetUrl = (name) => {
|
const buildAssetUrl = (name) => {
|
||||||
if (!name) return '';
|
if (!name) return '';
|
||||||
@@ -417,6 +438,7 @@ createApp({
|
|||||||
assetsManifest.updatedAt = data ? (data.updatedAt || null) : null;
|
assetsManifest.updatedAt = data ? (data.updatedAt || null) : null;
|
||||||
assetsRouteMapUrl.value = buildAssetUrl(assetsManifest.routeMap);
|
assetsRouteMapUrl.value = buildAssetUrl(assetsManifest.routeMap);
|
||||||
assetsFareTableUrl.value = buildAssetUrl(assetsManifest.fareTable);
|
assetsFareTableUrl.value = buildAssetUrl(assetsManifest.fareTable);
|
||||||
|
assetsLoaded = true;
|
||||||
|
|
||||||
assetsFarePreview.headers = [];
|
assetsFarePreview.headers = [];
|
||||||
assetsFarePreview.rows = [];
|
assetsFarePreview.rows = [];
|
||||||
@@ -454,6 +476,7 @@ createApp({
|
|||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
markSynced();
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadAssetFile = async (url, file) => {
|
const uploadAssetFile = async (url, file) => {
|
||||||
@@ -633,16 +656,31 @@ createApp({
|
|||||||
|
|
||||||
// --- Order Management ---
|
// --- Order Management ---
|
||||||
const fetchOrders = async () => {
|
const fetchOrders = async () => {
|
||||||
|
if (loadingState.orders) return;
|
||||||
|
loadingState.orders = true;
|
||||||
try {
|
try {
|
||||||
const res = await requestJson('/api/orders');
|
const res = await requestJson('/api/orders');
|
||||||
if (res && res.ok) orders.value = res.orders;
|
if (res && res.ok) {
|
||||||
} catch (e) { console.error(e); }
|
orders.value = res.orders || [];
|
||||||
|
orderDataLoaded = true;
|
||||||
|
markSynced();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
loadingState.orders = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchIcCards = async (keepSelection = true) => {
|
const fetchIcCards = async (keepSelection = true) => {
|
||||||
|
if (loadingState.iccards) return;
|
||||||
|
loadingState.iccards = true;
|
||||||
|
const requestSeq = ++icListRequestSeq;
|
||||||
const sp = new URLSearchParams();
|
const sp = new URLSearchParams();
|
||||||
if (icCardSearch.value.trim()) sp.set('q', icCardSearch.value.trim());
|
if (icCardSearch.value.trim()) sp.set('q', icCardSearch.value.trim());
|
||||||
|
try {
|
||||||
const res = await requestJson(`/api/ic-cards?${sp.toString()}`);
|
const res = await requestJson(`/api/ic-cards?${sp.toString()}`);
|
||||||
|
if (requestSeq !== icListRequestSeq) return;
|
||||||
icCards.value = res?.cards || [];
|
icCards.value = res?.cards || [];
|
||||||
if (keepSelection && icSelectedId.value && icCards.value.some((card) => card.card_id === icSelectedId.value)) {
|
if (keepSelection && icSelectedId.value && icCards.value.some((card) => card.card_id === icSelectedId.value)) {
|
||||||
await loadIcCard(icSelectedId.value);
|
await loadIcCard(icSelectedId.value);
|
||||||
@@ -651,16 +689,25 @@ createApp({
|
|||||||
icSelectedCard.value = null;
|
icSelectedCard.value = null;
|
||||||
icSelectedEvents.value = [];
|
icSelectedEvents.value = [];
|
||||||
}
|
}
|
||||||
|
markSynced();
|
||||||
|
} finally {
|
||||||
|
if (requestSeq === icListRequestSeq) {
|
||||||
|
loadingState.iccards = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadIcCard = async (id) => {
|
const loadIcCard = async (id) => {
|
||||||
|
const requestSeq = ++icDetailRequestSeq;
|
||||||
const res = await requestJson(`/api/ic-cards/${encodeURIComponent(id)}`);
|
const res = await requestJson(`/api/ic-cards/${encodeURIComponent(id)}`);
|
||||||
|
if (requestSeq !== icDetailRequestSeq) return;
|
||||||
const card = res?.card || null;
|
const card = res?.card || null;
|
||||||
icSelectedId.value = id;
|
icSelectedId.value = id;
|
||||||
icSelectedCard.value = card;
|
icSelectedCard.value = card;
|
||||||
icSelectedEvents.value = res?.events || [];
|
icSelectedEvents.value = res?.events || [];
|
||||||
icDetailForm.holder_name = card?.holder_name || '';
|
icDetailForm.holder_name = card?.holder_name || '';
|
||||||
icDetailForm.status = card?.status || 'active';
|
icDetailForm.status = card?.status || 'active';
|
||||||
|
markSynced();
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncSelectedIcCard = async () => {
|
const syncSelectedIcCard = async () => {
|
||||||
@@ -683,9 +730,15 @@ createApp({
|
|||||||
stopIcCardSync();
|
stopIcCardSync();
|
||||||
if (currentView.value !== 'iccards') return;
|
if (currentView.value !== 'iccards') return;
|
||||||
icCardSyncTimer = setInterval(() => {
|
icCardSyncTimer = setInterval(() => {
|
||||||
fetchIcCards(false).catch(console.error);
|
if (document.hidden || icCardSyncBusy) return;
|
||||||
syncSelectedIcCard().catch(console.error);
|
icCardSyncBusy = true;
|
||||||
}, 3000);
|
Promise.all([
|
||||||
|
fetchIcCards(false).catch(console.error),
|
||||||
|
icSelectedId.value ? syncSelectedIcCard().catch(console.error) : Promise.resolve()
|
||||||
|
]).finally(() => {
|
||||||
|
icCardSyncBusy = false;
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createIcCard = async () => {
|
const createIcCard = async () => {
|
||||||
@@ -775,10 +828,15 @@ createApp({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchLogs = async () => {
|
const fetchLogs = async () => {
|
||||||
|
if (logLoading.value) return;
|
||||||
logLoading.value = true;
|
logLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await requestJson(buildLogsUrl());
|
const res = await requestJson(buildLogsUrl());
|
||||||
if (res && res.ok) logs.value = res.logs || [];
|
if (res && res.ok) {
|
||||||
|
logs.value = res.logs || [];
|
||||||
|
logDataLoaded = true;
|
||||||
|
markSynced();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -823,66 +881,67 @@ createApp({
|
|||||||
return `¥${reg} / ¥${exp}`;
|
return `¥${reg} / ¥${exp}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchCoreData = async ({ force = false } = {}) => {
|
||||||
|
if (loadingState.core) return;
|
||||||
|
if (coreLoaded && !force) return;
|
||||||
|
loadingState.core = true;
|
||||||
try {
|
try {
|
||||||
const safeFetch = (url, defaultVal) => requestJson(url).catch(e => {
|
const safeFetch = (url, defaultVal) => requestJson(url).catch((e) => {
|
||||||
console.error(`Fetch failed for ${url}`, e);
|
console.error(`Fetch failed for ${url}`, e);
|
||||||
lastActionError.value = e?.message || String(e);
|
lastActionError.value = e?.message || String(e);
|
||||||
return defaultVal;
|
return defaultVal;
|
||||||
});
|
});
|
||||||
|
const [s, l, f, c, st] = await Promise.all([
|
||||||
const safeFetchList = (url, key) => requestJson(url).then(d => d?.[key] || []).catch(e => {
|
|
||||||
console.error(`Fetch list failed for ${url}`, e);
|
|
||||||
lastActionError.value = e?.message || String(e);
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
const [s, l, f, c, t, lg, st, ord, cards] = await Promise.all([
|
|
||||||
safeFetch('/api/stations', []),
|
safeFetch('/api/stations', []),
|
||||||
safeFetch('/api/lines', []),
|
safeFetch('/api/lines', []),
|
||||||
safeFetch('/api/fares', []),
|
safeFetch('/api/fares', []),
|
||||||
safeFetch('/api/config', {}),
|
safeFetch('/api/config', {}),
|
||||||
safeFetchList('/api/tickets', 'tickets'),
|
safeFetch('/api/stats/ticket/total', {}).then((d) => d.total || {})
|
||||||
safeFetchList(buildLogsUrl(), 'logs'),
|
|
||||||
safeFetch('/api/stats/ticket/total', {}).then(d => d.total || {}),
|
|
||||||
safeFetchList('/api/orders', 'orders'),
|
|
||||||
safeFetchList('/api/ic-cards', 'cards')
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
stations.value = s;
|
stations.value = s;
|
||||||
lines.value = l;
|
lines.value = l;
|
||||||
fares.value = f;
|
fares.value = f;
|
||||||
Object.assign(config, c);
|
Object.assign(config, c);
|
||||||
tickets.value = t;
|
|
||||||
logs.value = lg;
|
|
||||||
Object.assign(stats, st);
|
Object.assign(stats, st);
|
||||||
orders.value = ord;
|
|
||||||
icCards.value = cards;
|
|
||||||
|
|
||||||
// Refresh selected line if it exists
|
|
||||||
if (selectedLine.value) {
|
if (selectedLine.value) {
|
||||||
const found = lines.value.find(l => l.id === selectedLine.value.id);
|
const found = lines.value.find((line) => line.id === selectedLine.value.id);
|
||||||
if (found) selectedLine.value = found;
|
selectedLine.value = found || null;
|
||||||
}
|
}
|
||||||
if (icSelectedId.value && icCards.value.some((card) => card.card_id === icSelectedId.value)) {
|
coreLoaded = true;
|
||||||
await loadIcCard(icSelectedId.value);
|
markSynced();
|
||||||
}
|
|
||||||
|
|
||||||
loadFareMap();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to fetch data", e);
|
console.error('Failed to fetch core data', e);
|
||||||
|
} finally {
|
||||||
|
loadingState.core = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadFareMap = async () => {
|
const fetchTicketData = async () => {
|
||||||
|
if (loadingState.tickets) return;
|
||||||
|
loadingState.tickets = true;
|
||||||
|
try {
|
||||||
|
const res = await requestJson('/api/tickets');
|
||||||
|
tickets.value = res?.tickets || [];
|
||||||
|
ticketDataLoaded = true;
|
||||||
|
markSynced();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch tickets', e);
|
||||||
|
} finally {
|
||||||
|
loadingState.tickets = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFareMap = async ({ force = false } = {}) => {
|
||||||
|
if (fareMapLoading.value) return;
|
||||||
|
if (fareMapLoaded && !force) return;
|
||||||
fareMapLoading.value = true;
|
fareMapLoading.value = true;
|
||||||
fareMapError.value = '';
|
fareMapError.value = '';
|
||||||
try {
|
try {
|
||||||
// Change to fetch the SVG text directly from the public API
|
|
||||||
// Add timestamp to prevent caching
|
|
||||||
const r = await fetch(`/api/public/fares/map/light?t=${Date.now()}`);
|
const r = await fetch(`/api/public/fares/map/light?t=${Date.now()}`);
|
||||||
const svg = await r.text();
|
const svg = await r.text();
|
||||||
fareMapSvg.value = svg;
|
fareMapSvg.value = svg;
|
||||||
|
fareMapLoaded = true;
|
||||||
|
markSynced();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load fare map", e);
|
console.error("Failed to load fare map", e);
|
||||||
fareMapError.value = '加载失败';
|
fareMapError.value = '加载失败';
|
||||||
@@ -891,6 +950,25 @@ createApp({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ensureViewData = async (view = currentView.value, { force = false } = {}) => {
|
||||||
|
await fetchCoreData({ force });
|
||||||
|
if (view === 'tickets' && (force || !ticketDataLoaded)) await fetchTicketData();
|
||||||
|
if (view === 'vouchers' && (force || !orderDataLoaded)) await fetchOrders();
|
||||||
|
if (view === 'logs' && (force || !logDataLoaded)) await fetchLogs();
|
||||||
|
if (view === 'iccards') {
|
||||||
|
await fetchIcCards(true);
|
||||||
|
if (icSelectedId.value) {
|
||||||
|
await syncSelectedIcCard().catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (view === 'faremap' && (force || !fareMapLoaded)) await loadFareMap({ force });
|
||||||
|
if (view === 'assets' && (!assetsLoaded || force)) await fetchAssetsManifest();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
await ensureViewData(currentView.value, { force: true });
|
||||||
|
};
|
||||||
|
|
||||||
const zoomFareMapIn = () => { fareMapScale.value = Math.min(3, Math.round((fareMapScale.value + 0.1) * 100) / 100); };
|
const zoomFareMapIn = () => { fareMapScale.value = Math.min(3, Math.round((fareMapScale.value + 0.1) * 100) / 100); };
|
||||||
const zoomFareMapOut = () => { fareMapScale.value = Math.max(0.3, Math.round((fareMapScale.value - 0.1) * 100) / 100); };
|
const zoomFareMapOut = () => { fareMapScale.value = Math.max(0.3, Math.round((fareMapScale.value - 0.1) * 100) / 100); };
|
||||||
const zoomFareMapReset = () => { fareMapScale.value = 1; };
|
const zoomFareMapReset = () => { fareMapScale.value = 1; };
|
||||||
@@ -1237,11 +1315,15 @@ createApp({
|
|||||||
socket.on('stations:updated', (data) => {
|
socket.on('stations:updated', (data) => {
|
||||||
stations.value = data;
|
stations.value = data;
|
||||||
// Refresh map when stations change
|
// Refresh map when stations change
|
||||||
loadFareMap();
|
fareMapLoaded = false;
|
||||||
|
if (currentView.value === 'faremap') {
|
||||||
|
loadFareMap({ force: true });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('lines:updated', (data) => {
|
socket.on('lines:updated', (data) => {
|
||||||
lines.value = data;
|
lines.value = data;
|
||||||
|
coreLoaded = true;
|
||||||
// Update selectedLine reference if it exists
|
// Update selectedLine reference if it exists
|
||||||
if (selectedLine.value) {
|
if (selectedLine.value) {
|
||||||
const updated = data.find(l => l.id === selectedLine.value.id);
|
const updated = data.find(l => l.id === selectedLine.value.id);
|
||||||
@@ -1251,14 +1333,28 @@ createApp({
|
|||||||
selectedLine.value = null; // Line was deleted
|
selectedLine.value = null; // Line was deleted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadFareMap();
|
fareMapLoaded = false;
|
||||||
|
if (currentView.value === 'faremap') {
|
||||||
|
loadFareMap({ force: true });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('fares:updated', (data) => {
|
socket.on('fares:updated', (data) => {
|
||||||
fares.value = data;
|
fares.value = data;
|
||||||
loadFareMap();
|
coreLoaded = true;
|
||||||
|
fareMapLoaded = false;
|
||||||
|
if (currentView.value === 'faremap') {
|
||||||
|
loadFareMap({ force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
socket.on('config:updated', (data) => {
|
||||||
|
Object.assign(config, data);
|
||||||
|
coreLoaded = true;
|
||||||
|
fareMapLoaded = false;
|
||||||
|
if (currentView.value === 'faremap') {
|
||||||
|
loadFareMap({ force: true });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
socket.on('config:updated', (data) => { Object.assign(config, data); loadFareMap(); });
|
|
||||||
|
|
||||||
socket.on('stats:ticket:updated', (item) => {
|
socket.on('stats:ticket:updated', (item) => {
|
||||||
stats.sold_tickets += item.sold_tickets;
|
stats.sold_tickets += item.sold_tickets;
|
||||||
@@ -1281,15 +1377,12 @@ createApp({
|
|||||||
|
|
||||||
watch(currentView, (v) => {
|
watch(currentView, (v) => {
|
||||||
sidebarOpen.value = false;
|
sidebarOpen.value = false;
|
||||||
if (v === 'assets') fetchAssetsManifest();
|
|
||||||
if (v === 'logs') fetchLogs();
|
|
||||||
if (v === 'iccards') {
|
if (v === 'iccards') {
|
||||||
fetchIcCards(true).catch(console.error);
|
|
||||||
syncSelectedIcCard().catch(() => {});
|
|
||||||
startIcCardSync();
|
startIcCardSync();
|
||||||
} else {
|
} else {
|
||||||
stopIcCardSync();
|
stopIcCardSync();
|
||||||
}
|
}
|
||||||
|
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');
|
||||||
else sp.set('view', v);
|
else sp.set('view', v);
|
||||||
@@ -1300,13 +1393,11 @@ createApp({
|
|||||||
|
|
||||||
// Initial Load
|
// Initial Load
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData();
|
ensureViewData(currentView.value, { force: true }).catch(console.error);
|
||||||
fetchAssetsManifest();
|
|
||||||
if (currentView.value === 'iccards') {
|
if (currentView.value === 'iccards') {
|
||||||
fetchIcCards(true).catch(console.error);
|
|
||||||
startIcCardSync();
|
startIcCardSync();
|
||||||
}
|
}
|
||||||
window.addEventListener('mouseup', async () => {
|
appMouseupHandler = async () => {
|
||||||
if (draggingStationIndex.value !== null) {
|
if (draggingStationIndex.value !== null) {
|
||||||
if (selectedLine.value) {
|
if (selectedLine.value) {
|
||||||
try {
|
try {
|
||||||
@@ -1323,16 +1414,44 @@ createApp({
|
|||||||
}
|
}
|
||||||
draggingStationIndex.value = null;
|
draggingStationIndex.value = null;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
window.addEventListener('mouseup', appMouseupHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopIcCardSync();
|
stopIcCardSync();
|
||||||
|
if (appMouseupHandler) {
|
||||||
|
window.removeEventListener('mouseup', appMouseupHandler);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const recentLogs = computed(() => logs.value);
|
const recentLogs = computed(() => logs.value);
|
||||||
const orderList = computed(() => orders.value);
|
const orderList = computed(() => orders.value);
|
||||||
|
const lastSyncText = computed(() => lastSyncAt.value ? formatTime(lastSyncAt.value) : '尚未同步');
|
||||||
|
const isViewBusy = computed(() => {
|
||||||
|
if (loadingState.core) return true;
|
||||||
|
if (currentView.value === 'tickets') return loadingState.tickets;
|
||||||
|
if (currentView.value === 'vouchers') return loadingState.orders;
|
||||||
|
if (currentView.value === 'logs') return logLoading.value;
|
||||||
|
if (currentView.value === 'iccards') return loadingState.iccards;
|
||||||
|
if (currentView.value === 'faremap') return fareMapLoading.value;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
const currentViewSummary = computed(() => {
|
||||||
|
const map = {
|
||||||
|
dashboard: `已同步 ${lines.value.length} 条线路与 ${stations.value.length} 个站点`,
|
||||||
|
management: `当前可编辑 ${lines.value.length} 条线路与 ${stations.value.length} 个站点`,
|
||||||
|
faremap: fareMapLoading.value ? '票价图正在生成中' : '可导出当前铁路票价图',
|
||||||
|
tickets: `已加载 ${ticketList.value.length} 条车票记录`,
|
||||||
|
vouchers: `已加载 ${orders.value.length} 条凭证记录`,
|
||||||
|
iccards: `当前检索到 ${icCards.value.length} 张 IC 卡`,
|
||||||
|
assets: assetsManifest.routeMap ? `已上传线路图 ${assetsManifest.routeMap}` : '尚未上传线路图',
|
||||||
|
settings: '可维护优惠活动与导出数据',
|
||||||
|
logs: `当前筛选结果 ${logs.value.length} 条日志`
|
||||||
|
};
|
||||||
|
return map[currentView.value] || '后台模块已就绪';
|
||||||
|
});
|
||||||
const icCardStats = computed(() => ({
|
const icCardStats = computed(() => ({
|
||||||
total: icCards.value.length,
|
total: icCards.value.length,
|
||||||
pending: icCards.value.filter((card) => card.status === 'pending_pickup').length,
|
pending: icCards.value.filter((card) => card.status === 'pending_pickup').length,
|
||||||
@@ -1389,6 +1508,7 @@ createApp({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
currentView, viewTitle, connected, sidebarOpen,
|
currentView, viewTitle, connected, sidebarOpen,
|
||||||
|
loadingState, isViewBusy, lastSyncText, currentViewSummary,
|
||||||
stations, lines, fares, stats, config, recentLogs, ticketList,
|
stations, lines, fares, stats, config, recentLogs, ticketList,
|
||||||
logs, logCategory, logTypeFilter, logQuery, logMax, logLoading, fetchLogs,
|
logs, logCategory, logTypeFilter, logQuery, logMax, logLoading, fetchLogs,
|
||||||
orders, orderList, fetchOrders, deleteOrder,
|
orders, orderList, fetchOrders, deleteOrder,
|
||||||
|
|||||||
+20
-19
@@ -4,21 +4,21 @@
|
|||||||
<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>鎺у埗鍙扮櫥褰?/title>
|
<title>后台控制台登录</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="/style.css?v=12" />
|
<link rel="stylesheet" href="/style.css?v=13" />
|
||||||
</head>
|
</head>
|
||||||
<body class="jr-admin-login-page">
|
<body class="jr-admin-login-page">
|
||||||
<div class="jr-admin-login-shell">
|
<div class="jr-admin-login-shell">
|
||||||
<header class="jr-topbar">
|
<header class="jr-topbar">
|
||||||
<div class="jr-topbar-inner">
|
<div class="jr-topbar-inner">
|
||||||
<a href="/" class="jr-top-link">
|
<a href="/" class="jr-top-link">
|
||||||
<span>FSE閾佽矾绁ㄥ姟绯荤粺鎺у埗鍙?/span>
|
<span>FSE铁路票务系统控制台</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="jr-top-status is-checking" data-server-status-root>
|
<div class="jr-top-status is-checking" data-server-status-root>
|
||||||
<span class="jr-top-status-label">鏈嶅姟鍣ㄧ姸鎬?/span>
|
<span class="jr-top-status-label">服务器状态</span>
|
||||||
<span class="jr-top-status-dot"></span>
|
<span class="jr-top-status-dot"></span>
|
||||||
<span class="jr-top-status-value" data-server-status-value>妫€娴嬩腑</span>
|
<span class="jr-top-status-value" data-server-status-value>检测中</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -28,8 +28,8 @@
|
|||||||
<a href="/" class="jr-brand">
|
<a href="/" class="jr-brand">
|
||||||
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
||||||
<div class="jr-brand-copy">
|
<div class="jr-brand-copy">
|
||||||
<strong>FSE 閾佽矾杩愯緭</strong>
|
<strong>FSE 铁路运输</strong>
|
||||||
<span>鎺у埗鍙扮櫥褰?/span>
|
<span>控制台登录</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,39 +39,40 @@
|
|||||||
<section class="jr-admin-login-panel">
|
<section class="jr-admin-login-panel">
|
||||||
<div class="jr-admin-login-copy">
|
<div class="jr-admin-login-copy">
|
||||||
<span class="jr-kicker">OPERATIONS ACCESS</span>
|
<span class="jr-kicker">OPERATIONS ACCESS</span>
|
||||||
<h1>鍚庡彴鎺у埗鍙?/h1>
|
<h1>后台控制台</h1>
|
||||||
<p>绾胯矾缁存姢銆佺エ鎹鐞嗐€佹棩蹇楁煡璇笌 IC 鍗$鐞?/p>
|
<p>线路维护、票务管理、日志查询与 IC 卡管理统一从这里进入。</p>
|
||||||
<ul class="jr-admin-login-points">
|
<ul class="jr-admin-login-points">
|
||||||
<li>缁熶竴绠$悊绾胯矾銆佺エ浠峰拰璧勬簮鍥炬枃浠?/li>
|
<li>统一管理线路、票价和资源图文件</li>
|
||||||
<li>鏌ョ湅鐢靛瓙绁ㄣ€佸嚟璇佷笌鎿嶄綔鏃ュ織</li>
|
<li>查看电子票、凭证与操作日志</li>
|
||||||
<li>缁存姢 IC 鍗″彂琛屻€佸厖鍊间笌鐘舵€佽褰?/li>
|
<li>维护 IC 卡发放、充值与状态记录</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="jr-admin-login-card">
|
<section class="jr-admin-login-card">
|
||||||
<div class="jr-page-intro jr-page-intro-compact">
|
<div class="jr-page-intro jr-page-intro-compact">
|
||||||
<span class="jr-kicker">SIGN IN</span>
|
<span class="jr-kicker">SIGN IN</span>
|
||||||
<h2>鎺у埗鍙扮櫥褰?/h2>
|
<h2>控制台登录</h2>
|
||||||
<p>璇疯緭鍏ョ鐞嗗憳璐﹀彿鍜屽瘑鐮併€?/p>
|
<p>请输入管理员账号和密码。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-row"><input id="loginUser" type="text" placeholder="鐢ㄦ埛鍚? /></div>
|
<div class="login-row"><input id="loginUser" type="text" placeholder="用户名" /></div>
|
||||||
<div class="login-row"><input id="loginPass" type="password" placeholder="瀵嗙爜" /></div>
|
<div class="login-row"><input id="loginPass" type="password" placeholder="密码" /></div>
|
||||||
<div class="login-actions">
|
<div class="login-actions">
|
||||||
<button id="loginBtn" class="btn primary">鐧诲綍</button>
|
<button id="loginBtn" class="btn primary">登录</button>
|
||||||
<span id="loginHint" class="hint"></span>
|
<span id="loginHint" class="hint"></span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">绮CP澶?025450093鍙?/a>
|
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">粤ICP备2025450093号</a>
|
||||||
<span class="version">v1.0.12</span>
|
<span class="version">v1.0.12</span>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="login.js?v=2"></script>
|
<script src="login.js?v=2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+208
-7
@@ -28,6 +28,8 @@
|
|||||||
html, body {
|
html, body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -3727,6 +3729,31 @@ body.jr-admin-login-page {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-sync-meta {
|
||||||
|
min-width: 118px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #d8e2d4;
|
||||||
|
background: #f8fbf7;
|
||||||
|
color: #385446;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-sync-meta strong {
|
||||||
|
display: block;
|
||||||
|
color: #143423;
|
||||||
|
font-size: 0.94rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-sync-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #6a7d72;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jr-admin-header-pill {
|
.jr-admin-header-pill {
|
||||||
@@ -3762,6 +3789,145 @@ body.jr-admin-login-page {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-card {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #d7e0d3;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 10px 24px rgba(18, 50, 33, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-card.is-actions {
|
||||||
|
background: linear-gradient(180deg, #f8fbf7 0, #ffffff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-label {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #0b6b3a;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-value {
|
||||||
|
display: block;
|
||||||
|
color: #143423;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-note {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: #627368;
|
||||||
|
line-height: 1.7;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-actions,
|
||||||
|
.jr-admin-card-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-actions {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-section-toolbar {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding: 20px 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
border: 1px solid #d7e0d3;
|
||||||
|
background: linear-gradient(135deg, rgba(11, 107, 58, 0.05) 0, rgba(11, 107, 58, 0.015) 28%, #ffffff 28%, #ffffff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-section-toolbar-copy {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-section-toolbar-copy strong {
|
||||||
|
display: block;
|
||||||
|
color: #143423;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-section-toolbar-copy p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #627368;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-card-note,
|
||||||
|
.jr-admin-list-meta {
|
||||||
|
color: #6a7c70;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-note-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 14px;
|
||||||
|
color: #3c594a;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-list-card .jr-scroll-box {
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-summary-item {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid #dbe5d8;
|
||||||
|
background: #f8fbf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-summary-item span,
|
||||||
|
.jr-admin-summary-item small {
|
||||||
|
display: block;
|
||||||
|
color: #687a70;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-summary-item span {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-summary-item strong {
|
||||||
|
display: block;
|
||||||
|
color: #143423;
|
||||||
|
font-size: 1.18rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-summary-item small {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
.jr-admin-page .card {
|
.jr-admin-page .card {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #d7e0d3;
|
border: 1px solid #d7e0d3;
|
||||||
@@ -3962,6 +4128,16 @@ body.jr-admin-login-page {
|
|||||||
.jr-admin-login-panel {
|
.jr-admin-login-panel {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-grid,
|
||||||
|
.jr-admin-summary-grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-section-toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -3980,6 +4156,38 @@ body.jr-admin-login-page {
|
|||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jr-admin-login-copy,
|
||||||
|
.jr-admin-login-card {
|
||||||
|
padding: 22px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-header-side,
|
||||||
|
.jr-admin-overview-actions,
|
||||||
|
.jr-admin-card-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-sync-meta,
|
||||||
|
.jr-admin-overview-grid,
|
||||||
|
.jr-admin-summary-grid {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-grid,
|
||||||
|
.jr-admin-summary-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-card,
|
||||||
|
.jr-admin-section-toolbar {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-header-side .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Custom Dialog --- */
|
/* --- Custom Dialog --- */
|
||||||
|
|
||||||
@@ -4102,10 +4310,3 @@ body.jr-admin-login-page {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.jr-admin-login-copy,
|
|
||||||
.jr-admin-login-card {
|
|
||||||
padding: 22px;
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+103
-84
@@ -4,10 +4,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<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=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
<style>
|
<style>
|
||||||
[v-cloak] {
|
[v-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -49,12 +49,12 @@
|
|||||||
<div class="jr-topbar-inner">
|
<div class="jr-topbar-inner">
|
||||||
<a href="javascript:void(0)" @click="goHome" class="jr-top-link">
|
<a href="javascript:void(0)" @click="goHome" class="jr-top-link">
|
||||||
<i class="fas fa-arrow-left"></i>
|
<i class="fas fa-arrow-left"></i>
|
||||||
<span>杩斿洖鏌ヨ</span>
|
<span>返回查询</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="jr-top-status is-checking" data-server-status-root>
|
<div class="jr-top-status is-checking" data-server-status-root>
|
||||||
<span class="jr-top-status-label">鏈嶅姟鍣ㄧ姸鎬?/span>
|
<span class="jr-top-status-label">服务器状态</span>
|
||||||
<span class="jr-top-status-dot"></span>
|
<span class="jr-top-status-dot"></span>
|
||||||
<span class="jr-top-status-value" data-server-status-value>妫€娴嬩腑</span>
|
<span class="jr-top-status-value" data-server-status-value>检测中</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -63,28 +63,28 @@
|
|||||||
<a href="javascript:void(0)" @click="goHome" class="jr-brand">
|
<a href="javascript:void(0)" @click="goHome" class="jr-brand">
|
||||||
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
||||||
<div class="jr-brand-copy">
|
<div class="jr-brand-copy">
|
||||||
<strong>FSE閾佽矾绁ㄥ姟绯荤粺</strong>
|
<strong>FSE铁路票务系统</strong>
|
||||||
<span>鐢靛瓙瀹㈢エ淇℃伅</span>
|
<span>电子客票信息</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<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" 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>
|
||||||
</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">ELECTRONIC TICKET</span>
|
<span class="jr-kicker">ELECTRONIC TICKET</span>
|
||||||
<h1>鏌ョ湅杞︾エ鐘舵€佷笌鏈€杩戞祦杞褰?/h1>
|
<h1>查看车票状态与最近流转记录</h1>
|
||||||
<p>鐢ㄤ簬鏌ョ湅鍗曞紶鐢靛瓙瀹㈢エ鐨勪箻杞︿俊鎭€佺姸鎬佷笌杩涘嚭绔欒褰曪紝渚夸簬鏃呭鍜屽伐浣滀汉鍛樺揩閫熺‘璁ょエ鎹姸鎬併€?/p>
|
<p>用于查看单张电子客票的乘车信息、状态与进出站记录,便于旅客和工作人员快速确认票据状态。</p>
|
||||||
</section>
|
</section>
|
||||||
<div v-if="loading" class="jr-panel-card">
|
<div v-if="loading" class="jr-panel-card">
|
||||||
<div class="jr-center-empty">
|
<div class="jr-center-empty">
|
||||||
<p>姝e湪璇诲彇绁ㄦ嵁鏁版嵁...</p>
|
<p>正在读取车票数据...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="!loading && hasTicket">
|
<template v-if="!loading && hasTicket">
|
||||||
@@ -92,8 +92,9 @@
|
|||||||
<article class="jr-board-card">
|
<article class="jr-board-card">
|
||||||
<div class="jr-panel-headline">
|
<div class="jr-panel-headline">
|
||||||
<h2 class="mono">{{ ticket.ticket_id }}</h2>
|
<h2 class="mono">{{ ticket.ticket_id }}</h2>
|
||||||
<span class="jr-status-pill" :class="statusInfo.class.replace('status-', 'jr-status-')">{{
|
<span class="jr-status-pill" :class="statusInfo.class.replace('status-', 'jr-status-')">
|
||||||
statusInfo.text }}</span>
|
{{ statusInfo.text }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-route-board">
|
<div class="jr-route-board">
|
||||||
<div class="jr-station-block">
|
<div class="jr-station-block">
|
||||||
@@ -114,46 +115,44 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="jr-meta-grid">
|
<div class="jr-meta-grid">
|
||||||
<div class="jr-meta-item">
|
<div class="jr-meta-item">
|
||||||
<span>杞﹀瀷</span>
|
<span>车型</span>
|
||||||
<strong>{{ formatTrainType(ticket.overview.train_type) }}</strong>
|
<strong>{{ formatTrainType(ticket.overview.train_type) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-meta-item">
|
<div class="jr-meta-item">
|
||||||
<span>绁ㄤ环</span>
|
<span>票价</span>
|
||||||
<strong>楼 {{ ticket.overview.amount || 0 }}</strong>
|
<strong>¥ {{ ticket.overview.amount || 0 }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-meta-item">
|
<div class="jr-meta-item">
|
||||||
<span>涔樻</span>
|
<span>乘次</span>
|
||||||
<strong>{{ (ticket.overview.trips_remaining == null ? 1 :
|
<strong>{{ (ticket.overview.trips_remaining == null ? 1 : ticket.overview.trips_remaining) }} / {{ (ticket.overview.trips_total == null ? 1 : ticket.overview.trips_total) }}</strong>
|
||||||
ticket.overview.trips_remaining) }} / {{ (ticket.overview.trips_total == null ? 1 :
|
|
||||||
ticket.overview.trips_total) }}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-meta-item">
|
<div class="jr-meta-item">
|
||||||
<span>鏇存柊鏃堕棿</span>
|
<span>更新时间</span>
|
||||||
<strong>{{ formatTime(ticket.overview.last_update_ts) }}</strong>
|
<strong>{{ formatTime(ticket.overview.last_update_ts) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<aside class="jr-board-card">
|
<aside class="jr-board-card">
|
||||||
<div class="jr-panel-headline">
|
<div class="jr-panel-headline">
|
||||||
<h3>娴佽浆璁板綍</h3>
|
<h3>流转记录</h3>
|
||||||
<span class="jr-panel-note">Recent Events</span>
|
<span class="jr-panel-note">Recent Events</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-history-list" v-if="ticket.events && ticket.events.length > 0">
|
<div class="jr-history-list" v-if="ticket.events && ticket.events.length > 0">
|
||||||
<div v-for="ev in ticket.events.slice().reverse().slice(0, 5)"
|
<div v-for="ev in ticket.events.slice().reverse().slice(0, 5)"
|
||||||
:key="ev.ts || ev.鏃堕棿鎴? class="jr-history-item">
|
:key="ev.ts || ev['时间戳'] || Math.random()"
|
||||||
|
class="jr-history-item">
|
||||||
<div class="jr-history-row">
|
<div class="jr-history-row">
|
||||||
<span class="jr-history-title">{{ formatEvent(ev) }}</span>
|
<span class="jr-history-title">{{ formatEvent(ev) }}</span>
|
||||||
<span class="jr-history-time">{{ formatTime(ev.鏃堕棿鎴?|| ev.ts) }}</span>
|
<span class="jr-history-time">{{ formatTime(ev['时间戳'] || ev.ts) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-history-desc">
|
<div class="jr-history-desc">
|
||||||
<div>{{ formatEventLocation(ev) }}</div>
|
<div>{{ formatEventLocation(ev) }}</div>
|
||||||
<div v-if="formatEventMeta(ev)" style="margin-top:4px;">{{ formatEventMeta(ev) }}
|
<div v-if="formatEventMeta(ev)" style="margin-top:4px;">{{ formatEventMeta(ev) }}</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="jr-center-empty">
|
<div v-else class="jr-center-empty">
|
||||||
<p>鏆傛棤娴佽浆璁板綍銆?/p>
|
<p>暂无流转记录。</p>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
@@ -161,23 +160,23 @@
|
|||||||
|
|
||||||
<div v-if="!loading && !hasTicket" class="jr-panel-card">
|
<div v-if="!loading && !hasTicket" class="jr-panel-card">
|
||||||
<div class="jr-center-empty">
|
<div class="jr-center-empty">
|
||||||
<h2 style="margin:0 0 10px;">鏃犳晥杞︾エ</h2>
|
<h2 style="margin:0 0 10px;">无效车票</h2>
|
||||||
<p>鏈壘鍒拌杞︾エ鐨勮缁嗕俊鎭€?/p>
|
<p>未找到该车票的详细信息。</p>
|
||||||
<div class="jr-action-row">
|
<div class="jr-action-row">
|
||||||
<button @click="goHome" class="btn primary jr-search-button">杩斿洖鏌ヨ</button>
|
<button @click="goHome" class="btn primary jr-search-button">返回查询</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="site-footer jr-footer-space">
|
<footer class="site-footer jr-footer-space">
|
||||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">绮CP澶?025450093鍙?/a>
|
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">粤ICP备2025450093号</a>
|
||||||
<span class="version">v1.0.12</span>
|
<span class="version">v1.0.12</span>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="/ai-assistant.js?v=6"></script>
|
<script src="/ai-assistant.js?v=6"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -218,60 +217,74 @@
|
|||||||
const statusInfo = computed(() => {
|
const statusInfo = computed(() => {
|
||||||
if (!hasTicket.value) return {};
|
if (!hasTicket.value) return {};
|
||||||
let raw = '';
|
let raw = '';
|
||||||
if (ticket.value && ticket.value.overview) {
|
if (ticket.value && ticket.value.overview && ticket.value.overview.status != null) {
|
||||||
if (ticket.value.overview.status != null) raw = ticket.value.overview.status;
|
raw = ticket.value.overview.status;
|
||||||
}
|
}
|
||||||
if (!raw && ticket.value) {
|
if (!raw && ticket.value && ticket.value.status != null) {
|
||||||
if (ticket.value.status != null) raw = ticket.value.status;
|
raw = ticket.value.status;
|
||||||
}
|
}
|
||||||
const status = String(raw).toLowerCase();
|
const status = String(raw).toLowerCase();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
status === '鏈夋晥' ||
|
status === '有效' ||
|
||||||
status === 'valid' ||
|
status === 'valid' ||
|
||||||
status === 'unused' ||
|
status === 'unused' ||
|
||||||
status === 'active' ||
|
status === 'active' ||
|
||||||
status.includes('鏈夋晥') ||
|
status.includes('有效') ||
|
||||||
status.includes('鏈娇鐢?) ||
|
status.includes('未使用') ||
|
||||||
status.includes('unused')
|
status.includes('unused')
|
||||||
) {
|
) {
|
||||||
return { text: '鏈夋晥', class: 'status-valid' };
|
return { text: '有效', class: 'status-valid' };
|
||||||
}
|
}
|
||||||
if (status === '宸蹭娇鐢? || status === 'used' || status.includes('宸蹭娇鐢?) || status.includes('used')) {
|
if (
|
||||||
return { text: '宸蹭娇鐢?, class: 'status-used' };
|
status === '已使用' ||
|
||||||
|
status === 'used' ||
|
||||||
|
status.includes('已使用') ||
|
||||||
|
status.includes('used')
|
||||||
|
) {
|
||||||
|
return { text: '已使用', class: 'status-used' };
|
||||||
}
|
}
|
||||||
return { text: '澶辨晥', class: 'status-expired' };
|
return { text: '失效', class: 'status-expired' };
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatTime = (timestamp) => {
|
const formatTime = (timestamp) => {
|
||||||
if (!timestamp) return '---';
|
if (!timestamp) return '---';
|
||||||
let ts = Number(timestamp);
|
let ts = Number(timestamp);
|
||||||
if (!Number.isFinite(ts)) return String(timestamp);
|
if (!Number.isFinite(ts)) return String(timestamp);
|
||||||
if (ts > 0 && ts < 1000000000000) ts = ts * 1000;
|
if (ts > 0 && ts < 1000000000000) ts *= 1000;
|
||||||
const date = new Date(ts);
|
const date = new Date(ts);
|
||||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit'});
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatEvent = (event) => {
|
const formatEvent = (event) => {
|
||||||
const type = String(event.type || event.绫诲瀷 || '').toLowerCase();
|
const type = String(event.type || event['类型'] || '').toLowerCase();
|
||||||
const action = String(event.action || event.鍔ㄤ綔 || '').toLowerCase();
|
const action = String(event.action || event['动作'] || '').toLowerCase();
|
||||||
|
|
||||||
if (type === '鐘舵€? || type === 'status') {
|
if (type === '状态' || type === 'status') {
|
||||||
const actionMap = { 'entry': '杩涚珯鎴愬姛', 'exit': '鍑虹珯鎴愬姛' };
|
const actionMap = { entry: '进站成功', exit: '出站成功' };
|
||||||
return actionMap[action] || '鐘舵€佸彉鏇?;
|
return actionMap[action] || '状态变更';
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeMap = { 'sale': '鍞エ鎴愬姛', '鍞エ': '鍞エ鎴愬姛', 'entry': '杩涚珯鎴愬姛', 'exit': '鍑虹珯鎴愬姛' };
|
const typeMap = {
|
||||||
return typeMap[type] || event.type || event.绫诲瀷 || '鐘舵€佸彉鏇?;
|
sale: '售票成功',
|
||||||
|
售票: '售票成功',
|
||||||
|
entry: '进站成功',
|
||||||
|
exit: '出站成功'
|
||||||
|
};
|
||||||
|
return typeMap[type] || event.type || event['类型'] || '状态变更';
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatEventLocation = (event) => {
|
const formatEventLocation = (event) => {
|
||||||
const type = String(event.type || event.绫诲瀷 || '').toLowerCase();
|
const type = String(event.type || event['类型'] || '').toLowerCase();
|
||||||
const stationName = event.station_name || event.鍞エ绔?|| event.鍙戠敓绔?|| '';
|
const stationName = event.station_name || event['售票站'] || event['发生站'] || '';
|
||||||
const stationCode = event.station_code || event.绔欑偣缂栧彿 || '';
|
const stationCode = event.station_code || event['站点编号'] || '';
|
||||||
|
|
||||||
if (type === 'sale' || type === '鍞エ') {
|
if (type === 'sale' || type === '售票') {
|
||||||
return stationName || '绾夸笂鍞エ';
|
return stationName || '线上售票';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stationName && !stationCode) return '---';
|
if (!stationName && !stationCode) return '---';
|
||||||
@@ -279,25 +292,25 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatEventMeta = (event) => {
|
const formatEventMeta = (event) => {
|
||||||
const type = String(event.type || event.绫诲瀷 || '').toLowerCase();
|
const type = String(event.type || event['类型'] || '').toLowerCase();
|
||||||
if (type === 'sale' || type === '鍞エ') {
|
if (type === 'sale' || type === '售票') {
|
||||||
const amount = event.amount ?? event.鍞エ棰?
|
const amount = event.amount ?? event['售票额'];
|
||||||
if (amount != null && amount !== '') return `绁ㄤ环锛毬?${amount}`;
|
if (amount != null && amount !== '') return `票价:¥ ${amount}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stationEn = event.station_en || event.绔欑偣鑻辨枃 || '';
|
const stationEn = event.station_en || event['站点英文'] || '';
|
||||||
const deviceId = event.device_id || event.璁惧缂栧彿 || '';
|
const deviceId = event.device_id || event['设备编号'] || '';
|
||||||
if (stationEn && deviceId) return `${stationEn} (${deviceId})`;
|
if (stationEn && deviceId) return `${stationEn} (${deviceId})`;
|
||||||
if (deviceId) return `璁惧锛?{deviceId}`;
|
if (deviceId) return `设备:${deviceId}`;
|
||||||
return stationEn;
|
return stationEn;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTrainType = (type) => {
|
const formatTrainType = (type) => {
|
||||||
if (!type) return '鏅€?;
|
if (!type) return '普通';
|
||||||
const t = type.toLowerCase();
|
const t = String(type).toLowerCase();
|
||||||
if (t === 'local') return '鏅€?;
|
if (t === 'local') return '普通';
|
||||||
if (t === 'ltd.exp' || t === 'express' || t === 'exp' || t === 'express_train') return '鐗规€?;
|
if (t === 'ltd.exp' || t === 'express' || t === 'exp' || t === 'express_train') return '特急';
|
||||||
if (t.includes('鐗规€?)) return '鐗规€?;
|
if (t.includes('特急')) return '特急';
|
||||||
return String(type);
|
return String(type);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -311,22 +324,22 @@
|
|||||||
ticket.value = null;
|
ticket.value = null;
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const id = (data && (data.ticket_id || data.杞︾エ缂栧彿 || data.id)) || ticketid;
|
const id = (data && (data.ticket_id || data['车票编号'] || data.id)) || ticketid;
|
||||||
let overview = null;
|
let overview = null;
|
||||||
if (data) {
|
if (data) {
|
||||||
if (data.overview != null) overview = data.overview;
|
if (data.overview != null) overview = data.overview;
|
||||||
else if (data.姒傝 != null) overview = data.姒傝;
|
else if (data['概览'] != null) overview = data['概览'];
|
||||||
else if (data.summary != null) overview = data.summary;
|
else if (data.summary != null) overview = data.summary;
|
||||||
}
|
}
|
||||||
let events = [];
|
let events = [];
|
||||||
if (data) {
|
if (data) {
|
||||||
if (Array.isArray(data.events)) events = data.events;
|
if (Array.isArray(data.events)) events = data.events;
|
||||||
else if (data.浜嬩欢 != null) events = data.浜嬩欢;
|
else if (data['事件'] != null) events = data['事件'];
|
||||||
}
|
}
|
||||||
if (id && overview != null) {
|
if (id && overview != null) {
|
||||||
const out = {};
|
const out = {};
|
||||||
if (data && typeof data === 'object') {
|
if (data && typeof data === 'object') {
|
||||||
for (const k in data) out[k] = data[k];
|
for (const key in data) out[key] = data[key];
|
||||||
}
|
}
|
||||||
out.ticket_id = id;
|
out.ticket_id = id;
|
||||||
out.overview = overview;
|
out.overview = overview;
|
||||||
@@ -337,7 +350,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('鑾峰彇杞︾エ鏁版嵁澶辫触:', e);
|
console.error('获取车票数据失败:', e);
|
||||||
ticket.value = null;
|
ticket.value = null;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -357,16 +370,22 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading, ticket, hasTicket, statusInfo,
|
loading,
|
||||||
formatTime, formatEvent, formatEventLocation, formatEventMeta, formatTrainType, goHome
|
ticket,
|
||||||
|
hasTicket,
|
||||||
|
statusInfo,
|
||||||
|
formatTime,
|
||||||
|
formatEvent,
|
||||||
|
formatEventLocation,
|
||||||
|
formatEventMeta,
|
||||||
|
formatTrainType,
|
||||||
|
goHome
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}).mount('#app');
|
}).mount('#app');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>线上预定</title>
|
<title>线上预定</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=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="public-search jr-order-page">
|
<body class="public-search jr-order-page">
|
||||||
@@ -232,7 +232,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/ticket-order.js?v=21"></script>
|
<script src="/ticket-order.js?v=21"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="/ai-assistant.js?v=6"></script>
|
<script src="/ai-assistant.js?v=6"></script>
|
||||||
@@ -261,3 +261,4 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<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=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
<script src="/socket.io/socket.io.js"></script>
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -607,7 +607,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="ticket-route.js?v=2"></script>
|
<script src="ticket-route.js?v=2"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -641,3 +641,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>票务查询</title>
|
<title>票务查询</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=12" />
|
<link rel="stylesheet" href="/style.css?v=13" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="public-search jr-public-page">
|
<body class="public-search jr-public-page">
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/ticket-search.js?v=11"></script>
|
<script src="/ticket-search.js?v=11"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="/ai-assistant.js?v=6"></script>
|
<script src="/ai-assistant.js?v=6"></script>
|
||||||
@@ -146,3 +146,4 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -7,7 +7,7 @@
|
|||||||
<title>凭证详情</title>
|
<title>凭证详情</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=12" />
|
<link rel="stylesheet" href="/style.css?v=13" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="public-search jr-public-page">
|
<body class="public-search jr-public-page">
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/token.js?v=2"></script>
|
<script src="/token.js?v=2"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="/ai-assistant.js?v=6"></script>
|
<script src="/ai-assistant.js?v=6"></script>
|
||||||
@@ -137,3 +137,4 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user