Compare commits
8 Commits
b614ff663c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ef9926dc58 | |||
| 07e4200c17 | |||
| 81debd3b55 | |||
| 0a70ffe931 | |||
| a4d97fbd5a | |||
| d6aa03d3a7 | |||
| 042720d812 | |||
| 7fe1acd9d7 |
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
scene: git_message
|
||||||
|
---
|
||||||
|
|
||||||
|
在此处编写规则,自定义 AI 生成提交信息的风格。
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
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.7"
|
local VERSION = "v1.5.8"
|
||||||
|
local VERSION_CHECK_INTERVAL = 60
|
||||||
|
|
||||||
local CONFIG_PATH = "gate_config.json"
|
local CONFIG_PATH = "gate_config.json"
|
||||||
|
|
||||||
@@ -26,6 +27,15 @@ local function trim(s)
|
|||||||
return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", ""))
|
return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", ""))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function normalizeVersionTag(v)
|
||||||
|
local s = trim(v)
|
||||||
|
if #s == 0 then return "" end
|
||||||
|
if s:sub(1, 1):lower() ~= "v" then
|
||||||
|
s = "v" .. s
|
||||||
|
end
|
||||||
|
return s:lower()
|
||||||
|
end
|
||||||
|
|
||||||
local function splitCsv(s)
|
local function splitCsv(s)
|
||||||
local out = {}
|
local out = {}
|
||||||
s = trim(s)
|
s = trim(s)
|
||||||
@@ -80,6 +90,8 @@ local inspection = peripheral.find("ticket_inspection_machine")
|
|||||||
|
|
||||||
local serverConnected = nil
|
local serverConnected = nil
|
||||||
local serverLastChangeTs = 0
|
local serverLastChangeTs = 0
|
||||||
|
local expectedGateVersion = nil
|
||||||
|
local versionMismatch = nil
|
||||||
|
|
||||||
local function setServerConnected(ok)
|
local function setServerConnected(ok)
|
||||||
if serverConnected == ok then return end
|
if serverConnected == ok then return end
|
||||||
@@ -126,10 +138,20 @@ local function drawVersionIndicator(w)
|
|||||||
local s = tostring(VERSION or "")
|
local s = tostring(VERSION or "")
|
||||||
if #s == 0 then return end
|
if #s == 0 then return end
|
||||||
if w < #s then return end
|
if w < #s then return end
|
||||||
|
local markerColor = colors.yellow
|
||||||
|
if versionMismatch == true then
|
||||||
|
markerColor = colors.red
|
||||||
|
elseif versionMismatch == false then
|
||||||
|
markerColor = colors.lime
|
||||||
|
end
|
||||||
termDev.setBackgroundColor(colors.black)
|
termDev.setBackgroundColor(colors.black)
|
||||||
termDev.setTextColor(colors.gray)
|
termDev.setTextColor(colors.gray)
|
||||||
termDev.setCursorPos(1, 1)
|
termDev.setCursorPos(1, 1)
|
||||||
termDev.write(s)
|
termDev.write(s)
|
||||||
|
if w >= (#s + 1) then
|
||||||
|
termDev.setTextColor(markerColor)
|
||||||
|
termDev.write("*")
|
||||||
|
end
|
||||||
termDev.setTextColor(colors.white)
|
termDev.setTextColor(colors.white)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -337,6 +359,23 @@ local function refreshStationNameMap(serverBase)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function refreshRemoteLuaVersion(serverBase)
|
||||||
|
serverBase = trim(serverBase or "")
|
||||||
|
if #serverBase == 0 then return false end
|
||||||
|
local url = serverBase:gsub("/+$", "") .. "/api/public/config"
|
||||||
|
local ok, parsed = getJSON(url)
|
||||||
|
if not ok or type(parsed) ~= "table" then return false end
|
||||||
|
local remote = normalizeVersionTag(type(parsed.lua_versions) == "table" and parsed.lua_versions.gate or nil)
|
||||||
|
if #remote == 0 then
|
||||||
|
expectedGateVersion = nil
|
||||||
|
versionMismatch = nil
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
expectedGateVersion = remote
|
||||||
|
versionMismatch = (remote ~= normalizeVersionTag(VERSION))
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
local function inferStationCodeFromName(name)
|
local function inferStationCodeFromName(name)
|
||||||
local key = normKey(name or "")
|
local key = normKey(name or "")
|
||||||
if #key == 0 then return "" end
|
if #key == 0 then return "" end
|
||||||
@@ -655,6 +694,10 @@ pcall(function()
|
|||||||
refreshStationNameMap(guessBaseFromStatusURL(serverURL))
|
refreshStationNameMap(guessBaseFromStatusURL(serverURL))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
pcall(function()
|
||||||
|
refreshRemoteLuaVersion(guessBaseFromStatusURL(serverURL))
|
||||||
|
end)
|
||||||
|
|
||||||
if not inspection then
|
if not inspection then
|
||||||
if modeBySide == nil then
|
if modeBySide == nil then
|
||||||
draw("Missing peripheral:", "ticket_inspection_machine", colors.red)
|
draw("Missing peripheral:", "ticket_inspection_machine", colors.red)
|
||||||
@@ -667,18 +710,6 @@ if next(stationSet) == nil then
|
|||||||
error("No station codes configured")
|
error("No station codes configured")
|
||||||
end
|
end
|
||||||
|
|
||||||
local stationListText = table.concat(cfg.station_codes, ",")
|
|
||||||
local function readyLine1()
|
|
||||||
if not modeBySide then
|
|
||||||
return "Ready (" .. mode:upper() .. ")"
|
|
||||||
end
|
|
||||||
local f = modeBySide.front and modeBySide.front:upper() or "-"
|
|
||||||
local b = modeBySide.back and modeBySide.back:upper() or "-"
|
|
||||||
return "Ready (BI) F:" .. f .. " B:" .. b
|
|
||||||
end
|
|
||||||
|
|
||||||
draw(readyLine1(), "Station: " .. stationListText, colors.lime)
|
|
||||||
|
|
||||||
local stationCodesPayload = {}
|
local stationCodesPayload = {}
|
||||||
for k, _ in pairs(stationSet) do table.insert(stationCodesPayload, k) end
|
for k, _ in pairs(stationSet) do table.insert(stationCodesPayload, k) end
|
||||||
table.sort(stationCodesPayload)
|
table.sort(stationCodesPayload)
|
||||||
@@ -785,6 +816,37 @@ local function actionForSide(side)
|
|||||||
return modeBySide[side] or mode
|
return modeBySide[side] or mode
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function shortModeLabel(v)
|
||||||
|
v = trim(v):lower()
|
||||||
|
if v == "exit" then return "OUT" end
|
||||||
|
return "IN"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readyLine1()
|
||||||
|
if not modeBySide then
|
||||||
|
return "READY " .. shortModeLabel(mode)
|
||||||
|
end
|
||||||
|
return "F " .. shortModeLabel(modeBySide.front or "") .. " B " .. shortModeLabel(modeBySide.back or "")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readyLine2()
|
||||||
|
if not modeBySide then
|
||||||
|
return "ST " .. stationCodeForSide(nil)
|
||||||
|
end
|
||||||
|
local frontCode = stationCodeForSide("front")
|
||||||
|
local backCode = stationCodeForSide("back")
|
||||||
|
if frontCode == backCode then
|
||||||
|
return "ST " .. frontCode
|
||||||
|
end
|
||||||
|
return "F " .. frontCode .. " B " .. backCode
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawReadyScreen()
|
||||||
|
draw(readyLine1(), readyLine2(), colors.lime)
|
||||||
|
end
|
||||||
|
|
||||||
|
drawReadyScreen()
|
||||||
|
|
||||||
local function collectInspectionDevices(side, modeBySideRef)
|
local function collectInspectionDevices(side, modeBySideRef)
|
||||||
local sideKnown = trimSide(side) ~= nil
|
local sideKnown = trimSide(side) ~= nil
|
||||||
local inspectionDevs = {}
|
local inspectionDevs = {}
|
||||||
@@ -1278,11 +1340,19 @@ local function processInspectionEvent(eventName, ev)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
draw(readyLine1(), "Station: " .. stationListText, colors.lime)
|
drawReadyScreen()
|
||||||
|
elseif ev[1] == "timer" and ev[2] == versionTimer then
|
||||||
|
pcall(function()
|
||||||
|
refreshRemoteLuaVersion(guessBaseFromStatusURL(serverURL))
|
||||||
|
end)
|
||||||
|
drawReadyScreen()
|
||||||
|
versionTimer = os.startTimer(VERSION_CHECK_INTERVAL)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
|
||||||
|
local URL_MACHINE_HTTP = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/ticketmachine.lua"
|
||||||
|
local URL_UPDATE_MACHINE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/update_machine.lua"
|
||||||
|
|
||||||
|
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 atomicWrite(path, content, binary)
|
||||||
|
local tmp = path .. ".new"
|
||||||
|
if fs.exists(tmp) then fs.delete(tmp) end
|
||||||
|
if not writeFile(tmp, content, binary) then return false end
|
||||||
|
if fs.exists(path) then fs.delete(path) end
|
||||||
|
fs.move(tmp, path)
|
||||||
|
return true
|
||||||
|
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
|
||||||
|
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
print("Ticket Machine Installer")
|
||||||
|
print("")
|
||||||
|
print("Downloading ticket machine program...")
|
||||||
|
|
||||||
|
local ok, code = httpGet(URL_MACHINE_HTTP)
|
||||||
|
|
||||||
|
if not ok or type(code) ~= "string" or #code == 0 then
|
||||||
|
print("Download failed: " .. tostring(code or ""))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local okUpdate, updateCode = httpGet(URL_UPDATE_MACHINE)
|
||||||
|
if not okUpdate or type(updateCode) ~= "string" or #updateCode == 0 then
|
||||||
|
print("Download failed: " .. tostring(updateCode or ""))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not atomicWrite("startup", code, false) then
|
||||||
|
print("Write failed: startup")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
atomicWrite("startup.lua", code, false)
|
||||||
|
if fs.exists("ticketmachine.lua") then atomicWrite("ticketmachine.lua", code, false) end
|
||||||
|
if not atomicWrite("update_machine.lua", updateCode, false) then
|
||||||
|
print("Write failed: update_machine.lua")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Done.")
|
||||||
|
print("Reboot the computer to start the ticket machine.")
|
||||||
+9
-26
@@ -1,6 +1,7 @@
|
|||||||
local URL_ERROR = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/src/commit/108435e90d81c1c0a6e34486dee6cc6efd48ca53/error.dfpwm"
|
local URL_ERROR = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/error.dfpwm"
|
||||||
local URL_PASS = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/src/commit/108435e90d81c1c0a6e34486dee6cc6efd48ca53/pass.dfpwm"
|
local URL_PASS = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/pass.dfpwm"
|
||||||
local URL_GATE = "http://cloud.fse-media.group/d/API/TicketMachine/gate.lua?sign=OWy1wKkKhUhpnxXKeX6fRLePSg1XcaQgWOLvQbMuHRQ=:0"
|
local URL_GATE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/gate.lua"
|
||||||
|
local URL_UPDATE_GATE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/update_gate.lua"
|
||||||
|
|
||||||
local CONFIG_PATH = "gate_config.json"
|
local CONFIG_PATH = "gate_config.json"
|
||||||
|
|
||||||
@@ -34,13 +35,6 @@ local function prompt(label)
|
|||||||
return trim(read() or "")
|
return trim(read() or "")
|
||||||
end
|
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)
|
local function httpGet(url)
|
||||||
if not http then return false, "HTTP API disabled" end
|
if not http then return false, "HTTP API disabled" end
|
||||||
local okReq, err = pcall(function()
|
local okReq, err = pcall(function()
|
||||||
@@ -95,27 +89,15 @@ term.setCursorPos(1, 1)
|
|||||||
print("Ticket Gate Installer")
|
print("Ticket Gate Installer")
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
local stationsRaw = prompt("Station codes (comma or slash): ")
|
local stationCode = trim(prompt("Station code: "))
|
||||||
local stationCodes = splitCsv(stationsRaw)
|
if #stationCode == 0 then
|
||||||
if #stationCodes == 0 then
|
print("No station code provided.")
|
||||||
print("No station codes provided.")
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local modeRaw = prompt("Gate mode (entry/exit): ")
|
local modeRaw = prompt("Gate mode (entry/exit): ")
|
||||||
local mode = (trim(modeRaw):lower() == "exit") and "exit" or "entry"
|
local mode = (trim(modeRaw):lower() == "exit") and "exit" or "entry"
|
||||||
local stationCode = prompt("Gate station code (default first code): ")
|
local cfg = { mode = mode, station_codes = { stationCode }, station_code = stationCode }
|
||||||
stationCode = trim(stationCode)
|
|
||||||
if #stationCode == 0 then stationCode = 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 = { mode = mode, station_codes = stationCodes, station_code = stationCode }
|
|
||||||
if ticketServerUrl then cfg.server_url = ticketServerUrl end
|
|
||||||
if cardServerUrl then cfg.card_server_url = cardServerUrl end
|
|
||||||
local okCfg, cfgJson = pcall(textutils.serializeJSON, cfg)
|
local okCfg, cfgJson = pcall(textutils.serializeJSON, cfg)
|
||||||
if not okCfg then
|
if not okCfg then
|
||||||
print("Config serialize failed.")
|
print("Config serialize failed.")
|
||||||
@@ -137,6 +119,7 @@ end
|
|||||||
|
|
||||||
writeFile("startup.lua", gateCode, false)
|
writeFile("startup.lua", gateCode, false)
|
||||||
writeFile("startup", gateCode, false)
|
writeFile("startup", gateCode, false)
|
||||||
|
if not download(URL_UPDATE_GATE, "update_gate.lua", false) then return end
|
||||||
|
|
||||||
print("")
|
print("")
|
||||||
print("Done.")
|
print("Done.")
|
||||||
|
|||||||
+12
-29
@@ -1,6 +1,7 @@
|
|||||||
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/raw/branch/main/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/raw/branch/main/pass.dfpwm"
|
||||||
local URL_GATE = "http://cloud.fse-media.group/d/API/TicketMachine/gate.lua?sign=OWy1wKkKhUhpnxXKeX6fRLePSg1XcaQgWOLvQbMuHRQ=:0"
|
local URL_GATE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/gate.lua"
|
||||||
|
local URL_UPDATE_GATE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/update_gate.lua"
|
||||||
|
|
||||||
local CONFIG_PATH = "gate_config.json"
|
local CONFIG_PATH = "gate_config.json"
|
||||||
|
|
||||||
@@ -33,13 +34,6 @@ local function prompt(label)
|
|||||||
return trim(read() or "")
|
return trim(read() or "")
|
||||||
end
|
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)
|
local function httpGet(url)
|
||||||
if not http then return false, "HTTP API disabled" end
|
if not http then return false, "HTTP API disabled" end
|
||||||
local okReq, err = pcall(function()
|
local okReq, err = pcall(function()
|
||||||
@@ -98,10 +92,9 @@ term.setCursorPos(1, 1)
|
|||||||
print("Bidirectional Ticket Gate Installer")
|
print("Bidirectional Ticket Gate Installer")
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
local stationsRaw = prompt("Station codes (comma or slash): ")
|
local stationCode = trim(prompt("Station code: "))
|
||||||
local stationCodes = splitCsv(stationsRaw)
|
if #stationCode == 0 then
|
||||||
if #stationCodes == 0 then
|
print("No station code provided.")
|
||||||
print("No station codes provided.")
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -111,30 +104,19 @@ local frontRaw = prompt("Front mode (entry/exit): ")
|
|||||||
local backRaw = prompt("Back mode (entry/exit): ")
|
local backRaw = prompt("Back mode (entry/exit): ")
|
||||||
local frontMode = normalizeMode(frontRaw)
|
local frontMode = normalizeMode(frontRaw)
|
||||||
local backMode = normalizeMode(backRaw)
|
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 = {
|
local cfg = {
|
||||||
station_codes = stationCodes,
|
station_codes = { stationCode },
|
||||||
station_code = stationCodes[1],
|
station_code = stationCode,
|
||||||
side_modes = {
|
side_modes = {
|
||||||
front = frontMode,
|
front = frontMode,
|
||||||
back = backMode,
|
back = backMode,
|
||||||
},
|
},
|
||||||
side_station_codes = {
|
side_station_codes = {
|
||||||
front = frontStationCode,
|
front = stationCode,
|
||||||
back = backStationCode,
|
back = stationCode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ticketServerUrl then cfg.server_url = ticketServerUrl end
|
|
||||||
if cardServerUrl then cfg.card_server_url = cardServerUrl end
|
|
||||||
local okCfg, cfgJson = pcall(textutils.serializeJSON, cfg)
|
local okCfg, cfgJson = pcall(textutils.serializeJSON, cfg)
|
||||||
if not okCfg then
|
if not okCfg then
|
||||||
print("Config serialize failed.")
|
print("Config serialize failed.")
|
||||||
@@ -156,6 +138,7 @@ end
|
|||||||
|
|
||||||
writeFile("startup.lua", gateCode, false)
|
writeFile("startup.lua", gateCode, false)
|
||||||
writeFile("startup", gateCode, false)
|
writeFile("startup", gateCode, false)
|
||||||
|
if not download(URL_UPDATE_GATE, "update_gate.lua", false) then return end
|
||||||
|
|
||||||
print("")
|
print("")
|
||||||
print("Done.")
|
print("Done.")
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
local URL_REFILL_MACHINE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/refillmachine.lua"
|
||||||
|
local URL_UPDATE_REFILL = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/update_refill.lua"
|
||||||
|
|
||||||
|
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 atomicWrite(path, content, binary)
|
||||||
|
local tmp = path .. ".new"
|
||||||
|
if fs.exists(tmp) then fs.delete(tmp) end
|
||||||
|
if not writeFile(tmp, content, binary) then return false end
|
||||||
|
if fs.exists(path) then fs.delete(path) end
|
||||||
|
fs.move(tmp, path)
|
||||||
|
return true
|
||||||
|
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 failErr = p2
|
||||||
|
local res = p3
|
||||||
|
if type(p2) == "table" and type(p2.readAll) == "function" then
|
||||||
|
res = p2
|
||||||
|
failErr = 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(failErr or "http_failure")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
print("Refill Machine Installer")
|
||||||
|
print("")
|
||||||
|
print("Downloading refill machine program...")
|
||||||
|
|
||||||
|
local ok, code = httpGet(URL_REFILL_MACHINE)
|
||||||
|
if not ok or type(code) ~= "string" or #code == 0 then
|
||||||
|
print("Download failed: " .. tostring(code or ""))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local okUpdate, updateCode = httpGet(URL_UPDATE_REFILL)
|
||||||
|
if not okUpdate or type(updateCode) ~= "string" or #updateCode == 0 then
|
||||||
|
print("Download failed: " .. tostring(updateCode or ""))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not atomicWrite("refillmachine.lua", code, false) then
|
||||||
|
print("Write failed: refillmachine.lua")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not atomicWrite("startup.lua", code, false) then
|
||||||
|
print("Write failed: startup.lua")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not atomicWrite("startup", code, false) then
|
||||||
|
print("Write failed: startup")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not atomicWrite("update_refill.lua", updateCode, false) then
|
||||||
|
print("Write failed: update_refill.lua")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Done.")
|
||||||
|
print("refillmachine.lua has been installed as startup.")
|
||||||
|
print("Reboot the computer to start the refill machine.")
|
||||||
+1
-1
@@ -550,7 +550,7 @@ local function refillLoop()
|
|||||||
|
|
||||||
local errMessage = okCall and tostring(newBalance or "refill_failed") or "refill_call_failed"
|
local errMessage = okCall and tostring(newBalance or "refill_failed") or "refill_call_failed"
|
||||||
drawErrorPage(errMessage)
|
drawErrorPage(errMessage)
|
||||||
sleep(2)
|
sleep(8)
|
||||||
state.page = "home"
|
state.page = "home"
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|||||||
+12
-4
@@ -148,14 +148,16 @@ const resolveCurrentStationCode = (body, resolveStation) => {
|
|||||||
|
|
||||||
// Config
|
// Config
|
||||||
router.get('/config', (req, res) => {
|
router.get('/config', (req, res) => {
|
||||||
|
const cfg = DataService.getConfig();
|
||||||
res.json({
|
res.json({
|
||||||
api_base: DataService.getConfig().api_base,
|
api_base: cfg.api_base,
|
||||||
current_station: DataService.getConfig().current_station,
|
current_station: cfg.current_station,
|
||||||
stations: DataService.getStations(),
|
stations: DataService.getStations(),
|
||||||
lines: DataService.getLines(),
|
lines: DataService.getLines(),
|
||||||
fares: DataService.getFares(),
|
fares: DataService.getFares(),
|
||||||
transfers: DataService.getConfig().transfers || [],
|
transfers: cfg.transfers || [],
|
||||||
promotion: DataService.getConfig().promotion || { name: '', discount: 1 }
|
promotion: cfg.promotion || { name: '', discount: 1 },
|
||||||
|
lua_versions: cfg.lua_versions || {}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -219,6 +221,12 @@ router.put('/config', async (req, res) => {
|
|||||||
if (!(d >= 0 && d <= 1)) return res.status(400).json({ ok: false, error: 'promotion.discount must be between 0 and 1' });
|
if (!(d >= 0 && d <= 1)) return res.status(400).json({ ok: false, error: 'promotion.discount must be between 0 and 1' });
|
||||||
cfg.promotion = { name: String(p.name || ''), discount: d };
|
cfg.promotion = { name: String(p.name || ''), discount: d };
|
||||||
}
|
}
|
||||||
|
if (incoming.lua_versions && typeof incoming.lua_versions === 'object') {
|
||||||
|
cfg.lua_versions = {
|
||||||
|
...(cfg.lua_versions || {}),
|
||||||
|
...(incoming.lua_versions || {})
|
||||||
|
};
|
||||||
|
}
|
||||||
await DataService.saveConfig(cfg);
|
await DataService.saveConfig(cfg);
|
||||||
appendReqLog(req, { category: 'admin', type: 'update_config_generic', detail: incoming });
|
appendReqLog(req, { category: 'admin', type: 'update_config_generic', detail: incoming });
|
||||||
io.emit('config:updated', cfg);
|
io.emit('config:updated', cfg);
|
||||||
|
|||||||
+12
-3
@@ -221,7 +221,8 @@ router.get('/fares/query', (req, res) => {
|
|||||||
router.get('/config', (req, res) => {
|
router.get('/config', (req, res) => {
|
||||||
const cfg = DataService.getConfig();
|
const cfg = DataService.getConfig();
|
||||||
res.json({
|
res.json({
|
||||||
promotion: { name: cfg.promotion?.name || '', discount: cfg.promotion?.discount ?? 1 }
|
promotion: { name: cfg.promotion?.name || '', discount: cfg.promotion?.discount ?? 1 },
|
||||||
|
lua_versions: cfg.lua_versions || {}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -345,8 +346,16 @@ router.post('/orders/:code/consume', async (req, res) => {
|
|||||||
router.get('/ic-cards/query', async (req, res) => {
|
router.get('/ic-cards/query', async (req, res) => {
|
||||||
const q = String(req.query.q || '').trim();
|
const q = String(req.query.q || '').trim();
|
||||||
if (!q) {
|
if (!q) {
|
||||||
appendReqLog(req, { category: 'public', type: 'ic_card_query_invalid', level: 'warn', detail: { q } });
|
const cards = (DataService.getIcCards() || [])
|
||||||
return res.status(400).json({ ok: false, error: 'query required' });
|
.slice()
|
||||||
|
.sort((a, b) => Number(b?.created_ts || 0) - Number(a?.created_ts || 0))
|
||||||
|
.map((card) => ({
|
||||||
|
...presentIcCard(card),
|
||||||
|
status_label: mapIcCardStatus(card.status),
|
||||||
|
card_type_label: mapIcCardType(card.card_type)
|
||||||
|
}));
|
||||||
|
appendReqLog(req, { category: 'public', type: 'ic_card_query_all', detail: { total: cards.length } });
|
||||||
|
return res.json({ ok: true, cards });
|
||||||
}
|
}
|
||||||
const normCardId = normalizeIcCardId(q);
|
const normCardId = normalizeIcCardId(q);
|
||||||
const normOrderCode = String(q || '').trim().toUpperCase();
|
const normOrderCode = String(q || '').trim().toUpperCase();
|
||||||
|
|||||||
+34
-9
@@ -21,14 +21,39 @@ const pool = mysql.createPool({
|
|||||||
queueLimit: 0
|
queueLimit: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const DEFAULT_LUA_VERSIONS = {
|
||||||
|
ticketmachine: 'v1.5.8',
|
||||||
|
gate: 'v1.5.8'
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeLuaVersions(input) {
|
||||||
|
const src = (input && typeof input === 'object') ? input : {};
|
||||||
|
return {
|
||||||
|
ticketmachine: String(src.ticketmachine || DEFAULT_LUA_VERSIONS.ticketmachine),
|
||||||
|
gate: String(src.gate || DEFAULT_LUA_VERSIONS.gate)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeConfig(input) {
|
||||||
|
const src = (input && typeof input === 'object') ? input : {};
|
||||||
|
return {
|
||||||
|
...src,
|
||||||
|
api_base: String(src.api_base || 'http://127.0.0.1:23333/api'),
|
||||||
|
current_station: (src.current_station && typeof src.current_station === 'object')
|
||||||
|
? src.current_station
|
||||||
|
: { name: 'Station1', code: '01-01' },
|
||||||
|
transfers: Array.isArray(src.transfers) ? src.transfers : [],
|
||||||
|
promotion: {
|
||||||
|
name: String(src?.promotion?.name || ''),
|
||||||
|
discount: Number(src?.promotion?.discount ?? 1)
|
||||||
|
},
|
||||||
|
lua_versions: normalizeLuaVersions(src.lua_versions)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// In-memory cache for synchronous read access
|
// In-memory cache for synchronous read access
|
||||||
const cache = {
|
const cache = {
|
||||||
config: {
|
config: normalizeConfig({}),
|
||||||
api_base: 'http://127.0.0.1:23333/api',
|
|
||||||
current_station: { name: 'Station1', code: '01-01' },
|
|
||||||
transfers: [],
|
|
||||||
promotion: { name: '', discount: 1 }
|
|
||||||
},
|
|
||||||
stations: [],
|
stations: [],
|
||||||
lines: [],
|
lines: [],
|
||||||
fares: [],
|
fares: [],
|
||||||
@@ -66,7 +91,7 @@ const DataService = {
|
|||||||
|
|
||||||
// Load Cache
|
// Load Cache
|
||||||
const [configs] = await conn.query('SELECT v FROM kv_store WHERE k = ?', ['config']);
|
const [configs] = await conn.query('SELECT v FROM kv_store WHERE k = ?', ['config']);
|
||||||
if (configs.length > 0) cache.config = 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 [stations] = await conn.query('SELECT data FROM stations');
|
const [stations] = await conn.query('SELECT data FROM stations');
|
||||||
@@ -114,8 +139,8 @@ const DataService = {
|
|||||||
// Config
|
// Config
|
||||||
getConfig: () => cache.config,
|
getConfig: () => cache.config,
|
||||||
saveConfig: async (cfg) => {
|
saveConfig: async (cfg) => {
|
||||||
cache.config = cfg;
|
cache.config = normalizeConfig(cfg);
|
||||||
await pool.query('INSERT INTO kv_store (k, v) VALUES (?, ?) ON DUPLICATE KEY UPDATE v = ?', ['config', JSON.stringify(cfg), JSON.stringify(cfg)]);
|
await pool.query('INSERT INTO kv_store (k, v) VALUES (?, ?) ON DUPLICATE KEY UPDATE v = ?', ['config', JSON.stringify(cache.config), JSON.stringify(cache.config)]);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Stations
|
// Stations
|
||||||
|
|||||||
+293
-62
@@ -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.7'
|
local VERSION = 'v1.5.8'
|
||||||
|
|
||||||
-- ###########################
|
-- ###########################
|
||||||
-- Core HTTP & JSON Utilities
|
-- Core HTTP & JSON Utilities
|
||||||
@@ -13,6 +13,17 @@ end
|
|||||||
|
|
||||||
local serverConnected = nil
|
local serverConnected = nil
|
||||||
local serverLastChangeTs = 0
|
local serverLastChangeTs = 0
|
||||||
|
local expectedMachineVersion = nil
|
||||||
|
local versionMismatch = nil
|
||||||
|
|
||||||
|
local function normalizeVersionTag(v)
|
||||||
|
local s = tostring(v or ''):gsub('^%s+', ''):gsub('%s+$', '')
|
||||||
|
if #s == 0 then return '' end
|
||||||
|
if s:sub(1, 1):lower() ~= 'v' then
|
||||||
|
s = 'v' .. s
|
||||||
|
end
|
||||||
|
return s:lower()
|
||||||
|
end
|
||||||
|
|
||||||
local function setServerConnected(ok)
|
local function setServerConnected(ok)
|
||||||
if serverConnected == ok then return end
|
if serverConnected == ok then return end
|
||||||
@@ -314,22 +325,93 @@ end
|
|||||||
-- ###########################
|
-- ###########################
|
||||||
-- Peripheral discovery
|
-- Peripheral discovery
|
||||||
-- ###########################
|
-- ###########################
|
||||||
local monitor = peripheral.find('monitor')
|
local SIDE_PRIORITY = { top = 1, bottom = 2, left = 3, right = 4, front = 5, back = 6 }
|
||||||
local ticketVendingMachine = peripheral.find('ticket_vending_machine')
|
local REDSTONE_SIDES = { 'right', 'left', 'top', 'bottom', 'front', 'back' }
|
||||||
local speaker = peripheral.find('speaker')
|
local monitor = nil
|
||||||
|
local monitorName = nil
|
||||||
|
local ticketVendingMachine = nil
|
||||||
|
local ticketVendingMachineName = nil
|
||||||
|
local speaker = nil
|
||||||
|
local speakerName = nil
|
||||||
|
local detectedPaymentSide = nil
|
||||||
local MOD_DEBUG = true
|
local MOD_DEBUG = true
|
||||||
|
|
||||||
pcall(math.randomseed, (os.epoch and os.epoch('utc')) or os.time())
|
pcall(math.randomseed, (os.epoch and os.epoch('utc')) or os.time())
|
||||||
|
|
||||||
local function safe(term)
|
local function comparePeripheralName(a, b)
|
||||||
if monitor then return peripheral.wrap(peripheral.getName(monitor)) end
|
local pa = SIDE_PRIORITY[tostring(a or '')] or 99
|
||||||
return term
|
local pb = SIDE_PRIORITY[tostring(b or '')] or 99
|
||||||
|
if pa ~= pb then return pa < pb end
|
||||||
|
return tostring(a or '') < tostring(b or '')
|
||||||
end
|
end
|
||||||
|
|
||||||
local termDev = safe(term)
|
local function peripheralTypeMatches(name, typeName)
|
||||||
if monitor then pcall(monitor.setTextScale, 0.5) end
|
if not peripheral or type(peripheral.getType) ~= 'function' then return false end
|
||||||
|
local got = peripheral.getType(name)
|
||||||
|
if type(got) == 'string' then return got == typeName end
|
||||||
|
if type(got) == 'table' then
|
||||||
|
for _, item in ipairs(got) do
|
||||||
|
if item == typeName then return true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function findPeripheralByType(typeName)
|
||||||
|
if not peripheral then return nil, nil end
|
||||||
|
if type(peripheral.getNames) == 'function' and type(peripheral.wrap) == 'function' then
|
||||||
|
local names = peripheral.getNames() or {}
|
||||||
|
table.sort(names, comparePeripheralName)
|
||||||
|
for _, name in ipairs(names) do
|
||||||
|
if peripheralTypeMatches(name, typeName) then
|
||||||
|
local okWrap, dev = pcall(peripheral.wrap, name)
|
||||||
|
if okWrap and type(dev) == 'table' then
|
||||||
|
return dev, name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if type(peripheral.find) == 'function' then
|
||||||
|
local dev = peripheral.find(typeName)
|
||||||
|
if type(dev) == 'table' then
|
||||||
|
local name = nil
|
||||||
|
if type(peripheral.getName) == 'function' then
|
||||||
|
local okName, gotName = pcall(peripheral.getName, dev)
|
||||||
|
if okName then name = gotName end
|
||||||
|
end
|
||||||
|
return dev, name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local termDev = term
|
||||||
local w, h = termDev.getSize()
|
local w, h = termDev.getSize()
|
||||||
|
|
||||||
|
local function refreshDevices()
|
||||||
|
local prevSignature = table.concat({
|
||||||
|
tostring(monitorName or ''),
|
||||||
|
tostring(ticketVendingMachineName or ''),
|
||||||
|
tostring(speakerName or '')
|
||||||
|
}, '|')
|
||||||
|
monitor, monitorName = findPeripheralByType('monitor')
|
||||||
|
ticketVendingMachine, ticketVendingMachineName = findPeripheralByType('ticket_vending_machine')
|
||||||
|
speaker, speakerName = findPeripheralByType('speaker')
|
||||||
|
termDev = monitor or term
|
||||||
|
if monitor then pcall(monitor.setTextScale, 0.5) end
|
||||||
|
w, h = termDev.getSize()
|
||||||
|
local nextSignature = table.concat({
|
||||||
|
tostring(monitorName or ''),
|
||||||
|
tostring(ticketVendingMachineName or ''),
|
||||||
|
tostring(speakerName or '')
|
||||||
|
}, '|')
|
||||||
|
if prevSignature ~= nextSignature then
|
||||||
|
os.queueEvent('config_updated')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
refreshDevices()
|
||||||
|
|
||||||
local function saveCardIssueSnapshot(cardData)
|
local function saveCardIssueSnapshot(cardData)
|
||||||
pcall(function()
|
pcall(function()
|
||||||
ensureDir('logs/last_card_issue.json')
|
ensureDir('logs/last_card_issue.json')
|
||||||
@@ -356,6 +438,14 @@ local function peripheralCallSucceeded(r1)
|
|||||||
return r1 ~= nil and r1 ~= false
|
return r1 ~= nil and r1 ~= false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function getTicketVendingMachine()
|
||||||
|
refreshDevices()
|
||||||
|
if type(ticketVendingMachine) == 'table' then
|
||||||
|
return ticketVendingMachine
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
local function callPeripheralMethods(dev, methodNames, variants)
|
local function callPeripheralMethods(dev, methodNames, variants)
|
||||||
if type(dev) ~= 'table' then return false, 'peripheral_unavailable' end
|
if type(dev) ~= 'table' then return false, 'peripheral_unavailable' end
|
||||||
for _, methodName in ipairs(methodNames) do
|
for _, methodName in ipairs(methodNames) do
|
||||||
@@ -373,7 +463,7 @@ local function callPeripheralMethods(dev, methodNames, variants)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function issueBlankICCard(holderName, initialBalance)
|
local function issueBlankICCard(holderName, initialBalance)
|
||||||
local dev = ticketVendingMachine
|
local dev = getTicketVendingMachine()
|
||||||
if type(dev) ~= 'table' then return false, '', 'peripheral_unavailable' end
|
if type(dev) ~= 'table' then return false, '', 'peripheral_unavailable' end
|
||||||
local safeHolderName = firstString(holderName, 'CARD USER')
|
local safeHolderName = firstString(holderName, 'CARD USER')
|
||||||
local safeInitialBalance = math.max(0, math.floor(tonumber(initialBalance) or 0))
|
local safeInitialBalance = math.max(0, math.floor(tonumber(initialBalance) or 0))
|
||||||
@@ -395,8 +485,9 @@ local function issueBlankICCard(holderName, initialBalance)
|
|||||||
return false, '', 'unsupported_method'
|
return false, '', 'unsupported_method'
|
||||||
end
|
end
|
||||||
|
|
||||||
local function writeICCard(cardData)
|
local function writeICCard(cardData, opts)
|
||||||
local dev = ticketVendingMachine
|
local dev = getTicketVendingMachine()
|
||||||
|
local options = opts or {}
|
||||||
local payload = {}
|
local payload = {}
|
||||||
for k, v in pairs(cardData or {}) do payload[k] = v end
|
for k, v in pairs(cardData or {}) do payload[k] = v end
|
||||||
payload.media = payload.media or 'ic_card'
|
payload.media = payload.media or 'ic_card'
|
||||||
@@ -409,7 +500,9 @@ local function writeICCard(cardData)
|
|||||||
saveCardIssueSnapshot(payload)
|
saveCardIssueSnapshot(payload)
|
||||||
|
|
||||||
local okWrite, methodName, r1, r2, r3 = callPeripheralMethods(dev,
|
local okWrite, methodName, r1, r2, r3 = callPeripheralMethods(dev,
|
||||||
{ 'issueCard', 'writeCard', 'writeICCard', 'issueTicketData', 'writeTicketData', 'issueICCard' },
|
options.writeOnly
|
||||||
|
and { 'writeCard', 'writeICCard', 'writeTicketData', 'issueTicketData' }
|
||||||
|
or { 'issueCard', 'writeCard', 'writeICCard', 'issueTicketData', 'writeTicketData', 'issueICCard' },
|
||||||
{
|
{
|
||||||
{ payload },
|
{ payload },
|
||||||
{ tostring(payload.holder_name or ''), tonumber(payload.initial_balance) or 0 },
|
{ tostring(payload.holder_name or ''), tonumber(payload.initial_balance) or 0 },
|
||||||
@@ -430,6 +523,81 @@ local function submitCardOpen(payload)
|
|||||||
return postJSON(API_BASE .. '/cards/open', payload)
|
return postJSON(API_BASE .. '/cards/open', payload)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function buildFinalCardData(payload, respData)
|
||||||
|
local data = (type(respData) == 'table') and respData or {}
|
||||||
|
return {
|
||||||
|
card_id = firstString(data.card_id, data.id, payload.card_id),
|
||||||
|
holder_name = payload.holder_name,
|
||||||
|
balance = firstNumber(data.balance, data.stored_value, payload.balance) or payload.balance,
|
||||||
|
deposit = firstNumber(data.deposit, payload.deposit) or payload.deposit,
|
||||||
|
topup = firstNumber(data.topup, data.first_topup, payload.topup) or payload.topup,
|
||||||
|
station_code = payload.station_code,
|
||||||
|
device = payload.device,
|
||||||
|
voucher_code = payload.voucher_code,
|
||||||
|
media = 'ic_card',
|
||||||
|
product_type = 'stored_value',
|
||||||
|
order_value = payload.order_value,
|
||||||
|
initial_balance = firstNumber(data.first_topup, data.topup, payload.topup, payload.balance) or payload.topup or payload.balance or 0
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function generateNumericCode(prefix, digits)
|
||||||
|
local width = math.max(1, math.floor(tonumber(digits) or 1))
|
||||||
|
local maxValue = (10 ^ width) - 1
|
||||||
|
local num = string.format('%0' .. tostring(width) .. 'd', math.random(0, maxValue))
|
||||||
|
return tostring(prefix or 'ID'):upper() .. '-' .. num
|
||||||
|
end
|
||||||
|
|
||||||
|
local function generateCardId()
|
||||||
|
return generateNumericCode('IC', 6)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function issueTicketFromPeripheral(fromNameEnArg, toNameEnArg, apiType, rides, cost, startStationArg, terminalStationArg, fromNameCnUArg, toNameCnUArg, fallbackTicketId)
|
||||||
|
local dev = getTicketVendingMachine()
|
||||||
|
if type(dev) ~= 'table' then
|
||||||
|
return false, '', 'peripheral_unavailable'
|
||||||
|
end
|
||||||
|
local fn = dev.issueTicket
|
||||||
|
if type(fn) ~= 'function' then
|
||||||
|
return false, '', 'unsupported_method'
|
||||||
|
end
|
||||||
|
|
||||||
|
local function normalizeIssuedTicketId(id)
|
||||||
|
if id == nil then return '' end
|
||||||
|
local s = tostring(id):gsub('%s+', '')
|
||||||
|
if #s == 0 then return '' end
|
||||||
|
local prefix, num = s:match('^([A-Za-z][A-Za-z])%-?([0-9]+)$')
|
||||||
|
if prefix and num then
|
||||||
|
prefix = prefix:upper()
|
||||||
|
if #num < 8 then
|
||||||
|
num = string.rep('0', 8 - #num) .. num
|
||||||
|
end
|
||||||
|
return prefix .. '-' .. num
|
||||||
|
end
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tryIssue(...)
|
||||||
|
local okCall, r1, r2, r3 = pcall(fn, ...)
|
||||||
|
if not (okCall and peripheralCallSucceeded(r1)) then
|
||||||
|
return false, '', okCall and 'issue_failed' or 'issue_call_failed'
|
||||||
|
end
|
||||||
|
local issuedId = extractPeripheralId(r2, r3, r1, fallbackTicketId)
|
||||||
|
local normalizedId = normalizeIssuedTicketId(issuedId)
|
||||||
|
if #normalizedId == 0 then
|
||||||
|
return false, '', 'invalid_ticket_id'
|
||||||
|
end
|
||||||
|
return true, normalizedId, 'issueTicket'
|
||||||
|
end
|
||||||
|
|
||||||
|
local okIssue, ticketId, issueErr = tryIssue(fromNameEnArg, toNameEnArg, apiType, rides, cost, startStationArg, terminalStationArg, fromNameCnUArg, toNameCnUArg)
|
||||||
|
if okIssue then
|
||||||
|
return true, ticketId, 'issueTicket'
|
||||||
|
end
|
||||||
|
|
||||||
|
return tryIssue(fromNameEnArg, toNameEnArg, apiType, rides, cost, startStationArg, terminalStationArg)
|
||||||
|
end
|
||||||
|
|
||||||
-- ###########################
|
-- ###########################
|
||||||
-- Audio Utilities & Playback
|
-- Audio Utilities & Playback
|
||||||
-- ###########################
|
-- ###########################
|
||||||
@@ -489,6 +657,15 @@ local function backgroundSyncTask()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function backgroundPeripheralTask()
|
||||||
|
while true do
|
||||||
|
local ev = os.pullEvent()
|
||||||
|
if ev == 'peripheral' or ev == 'peripheral_detach' then
|
||||||
|
pcall(refreshDevices)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function backgroundTicketUploadTask()
|
local function backgroundTicketUploadTask()
|
||||||
loadPendingUploadsOnce()
|
loadPendingUploadsOnce()
|
||||||
local backoff = 2
|
local backoff = 2
|
||||||
@@ -516,6 +693,17 @@ local stationByCode = {}
|
|||||||
local adjacency_regular, adjacency_express = {}, {}
|
local adjacency_regular, adjacency_express = {}, {}
|
||||||
local transferGroupByCode = {}
|
local transferGroupByCode = {}
|
||||||
|
|
||||||
|
local function updateVersionStateFromConfig()
|
||||||
|
local remote = normalizeVersionTag(type(CFG.lua_versions) == 'table' and CFG.lua_versions.ticketmachine or nil)
|
||||||
|
if #remote == 0 then
|
||||||
|
expectedMachineVersion = nil
|
||||||
|
versionMismatch = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
expectedMachineVersion = remote
|
||||||
|
versionMismatch = (remote ~= normalizeVersionTag(VERSION))
|
||||||
|
end
|
||||||
|
|
||||||
local function normalizeCode(s)
|
local function normalizeCode(s)
|
||||||
s = tostring(s or '')
|
s = tostring(s or '')
|
||||||
s = s:gsub('[\239\187\191]', ''):gsub('%s+', '')
|
s = s:gsub('[\239\187\191]', ''):gsub('%s+', '')
|
||||||
@@ -644,6 +832,7 @@ local function refreshConfigOnce()
|
|||||||
if f then f.write(textutils.serializeJSON(cfg)); f.close() end
|
if f then f.write(textutils.serializeJSON(cfg)); f.close() end
|
||||||
CFG = cfg
|
CFG = cfg
|
||||||
rebuildMaps()
|
rebuildMaps()
|
||||||
|
updateVersionStateFromConfig()
|
||||||
os.queueEvent('config_updated')
|
os.queueEvent('config_updated')
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -665,6 +854,7 @@ end
|
|||||||
|
|
||||||
CFG = loadConfig() or CFG
|
CFG = loadConfig() or CFG
|
||||||
rebuildMaps()
|
rebuildMaps()
|
||||||
|
updateVersionStateFromConfig()
|
||||||
|
|
||||||
|
|
||||||
-- ###########################
|
-- ###########################
|
||||||
@@ -889,10 +1079,20 @@ end
|
|||||||
|
|
||||||
local function drawVersionIndicator()
|
local function drawVersionIndicator()
|
||||||
if w < 1 then return end
|
if w < 1 then return end
|
||||||
|
local markerColor = colors.yellow
|
||||||
|
if versionMismatch == true then
|
||||||
|
markerColor = colors.red
|
||||||
|
elseif versionMismatch == false then
|
||||||
|
markerColor = colors.lime
|
||||||
|
end
|
||||||
termDev.setBackgroundColor(colors.black)
|
termDev.setBackgroundColor(colors.black)
|
||||||
termDev.setTextColor(colors.gray)
|
termDev.setTextColor(colors.gray)
|
||||||
termDev.setCursorPos(1, 1)
|
termDev.setCursorPos(1, 1)
|
||||||
termDev.write(tostring(VERSION))
|
termDev.write(tostring(VERSION))
|
||||||
|
if w >= (#tostring(VERSION) + 1) then
|
||||||
|
termDev.setTextColor(markerColor)
|
||||||
|
termDev.write('*')
|
||||||
|
end
|
||||||
termDev.setTextColor(colors.white)
|
termDev.setTextColor(colors.white)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1457,6 +1657,33 @@ local function computeCost(src, dst, trainType)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function paymentHintText()
|
||||||
|
if detectedPaymentSide and #tostring(detectedPaymentSide) > 0 then
|
||||||
|
return 'Payment side: ' .. tostring(detectedPaymentSide):upper()
|
||||||
|
end
|
||||||
|
return 'Insert payment on any side'
|
||||||
|
end
|
||||||
|
|
||||||
|
local function snapshotPaymentInputs()
|
||||||
|
local states = {}
|
||||||
|
if not redstone or type(redstone.getInput) ~= 'function' then return states end
|
||||||
|
for _, side in ipairs(REDSTONE_SIDES) do
|
||||||
|
states[side] = redstone.getInput(side) and true or false
|
||||||
|
end
|
||||||
|
return states
|
||||||
|
end
|
||||||
|
|
||||||
|
local function detectPaymentPulse(prevStates)
|
||||||
|
local nowStates = snapshotPaymentInputs()
|
||||||
|
for _, side in ipairs(REDSTONE_SIDES) do
|
||||||
|
if nowStates[side] and not prevStates[side] then
|
||||||
|
detectedPaymentSide = side
|
||||||
|
return true, side, nowStates
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false, nil, nowStates
|
||||||
|
end
|
||||||
|
|
||||||
local function drawOrder()
|
local function drawOrder()
|
||||||
if state.productMode == 'card' then
|
if state.productMode == 'card' then
|
||||||
local cardSub = (state.cardMode == 'redeem') and 'Redeem IC card order' or 'Open new stored-value card'
|
local cardSub = (state.cardMode == 'redeem') and 'Redeem IC card order' or 'Open new stored-value card'
|
||||||
@@ -1536,7 +1763,7 @@ local function drawOrder()
|
|||||||
if total <= 0 then
|
if total <= 0 then
|
||||||
centerText(statusY + 1, 'Ready to confirm', colors.lightGray)
|
centerText(statusY + 1, 'Ready to confirm', colors.lightGray)
|
||||||
else
|
else
|
||||||
centerText(statusY + 1, 'Insert payment on RIGHT side', colors.lightGray)
|
centerText(statusY + 1, paymentHintText(), colors.lightGray)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1603,6 +1830,8 @@ local function showOrderAndAudio()
|
|||||||
local reuseBlankCardId = firstString(state.pendingBlankCardId)
|
local reuseBlankCardId = firstString(state.pendingBlankCardId)
|
||||||
if #reuseBlankCardId > 0 then
|
if #reuseBlankCardId > 0 then
|
||||||
payload.card_id = reuseBlankCardId
|
payload.card_id = reuseBlankCardId
|
||||||
|
else
|
||||||
|
payload.card_id = generateCardId()
|
||||||
end
|
end
|
||||||
local okIssueBlank, blankCardId, issueMethod = false, '', 'reuse_pending'
|
local okIssueBlank, blankCardId, issueMethod = false, '', 'reuse_pending'
|
||||||
if #reuseBlankCardId == 0 then
|
if #reuseBlankCardId == 0 then
|
||||||
@@ -1616,13 +1845,19 @@ local function showOrderAndAudio()
|
|||||||
local okReq, code, parsed, err = submitCardOpen(payload)
|
local okReq, code, parsed, err = submitCardOpen(payload)
|
||||||
if okReq then
|
if okReq then
|
||||||
local respData = (type(parsed) == 'table' and (parsed.data or parsed.card or parsed)) or {}
|
local respData = (type(parsed) == 'table' and (parsed.data or parsed.card or parsed)) or {}
|
||||||
state.card_id = firstString(respData.card_id, respData.id, payload.card_id)
|
local finalCard = buildFinalCardData(payload, respData)
|
||||||
state.cardBalance = firstNumber(respData.balance, respData.stored_value, payload.balance) or payload.balance
|
local okWrite, writtenCard, writeMethod = writeICCard(finalCard, { writeOnly = true })
|
||||||
state.card_server_data = respData
|
if okWrite then
|
||||||
state.pendingBlankCardId = nil
|
state.card_id = firstString(writtenCard.card_id, finalCard.card_id)
|
||||||
confirmed = true
|
state.cardBalance = tonumber(writtenCard.balance) or finalCard.balance
|
||||||
statusMsg, statusCol = 'Card ready', colors.green
|
state.card_server_data = respData
|
||||||
if not state.doneAudioPlayed then playConfirmTicketMelody(); state.doneAudioPlayed = true end
|
state.pendingBlankCardId = nil
|
||||||
|
confirmed = true
|
||||||
|
statusMsg, statusCol = 'Card ready', colors.green
|
||||||
|
if not state.doneAudioPlayed then playConfirmTicketMelody(); state.doneAudioPlayed = true end
|
||||||
|
else
|
||||||
|
statusMsg, statusCol = 'Write failed: ' .. tostring(writeMethod), colors.red
|
||||||
|
end
|
||||||
else
|
else
|
||||||
local errorMsg = 'Card API Err'
|
local errorMsg = 'Card API Err'
|
||||||
if code == 409 then
|
if code == 409 then
|
||||||
@@ -1640,20 +1875,7 @@ local function showOrderAndAudio()
|
|||||||
local okReq, code, parsed, err = submitCardOpen(payload)
|
local okReq, code, parsed, err = submitCardOpen(payload)
|
||||||
if okReq then
|
if okReq then
|
||||||
local respData = (type(parsed) == 'table' and (parsed.data or parsed.card or parsed)) or {}
|
local respData = (type(parsed) == 'table' and (parsed.data or parsed.card or parsed)) or {}
|
||||||
local finalCard = {
|
local finalCard = buildFinalCardData(payload, respData)
|
||||||
card_id = firstString(respData.card_id, respData.id, payload.card_id),
|
|
||||||
holder_name = payload.holder_name,
|
|
||||||
balance = firstNumber(respData.balance, respData.stored_value, payload.balance) or payload.balance,
|
|
||||||
deposit = firstNumber(respData.deposit, payload.deposit) or payload.deposit,
|
|
||||||
topup = firstNumber(respData.topup, respData.first_topup, payload.topup) or payload.topup,
|
|
||||||
station_code = payload.station_code,
|
|
||||||
device = payload.device,
|
|
||||||
voucher_code = payload.voucher_code,
|
|
||||||
media = 'ic_card',
|
|
||||||
product_type = 'stored_value',
|
|
||||||
order_value = payload.order_value,
|
|
||||||
initial_balance = payload.topup
|
|
||||||
}
|
|
||||||
local okWrite, writtenCard, writeMethod = writeICCard(finalCard)
|
local okWrite, writtenCard, writeMethod = writeICCard(finalCard)
|
||||||
if okWrite then
|
if okWrite then
|
||||||
state.card_id = firstString(writtenCard.card_id, finalCard.card_id)
|
state.card_id = firstString(writtenCard.card_id, finalCard.card_id)
|
||||||
@@ -1736,12 +1958,12 @@ local function showOrderAndAudio()
|
|||||||
confirmAction()
|
confirmAction()
|
||||||
end
|
end
|
||||||
|
|
||||||
local prev = redstone.getInput('right')
|
local prevInputs = snapshotPaymentInputs()
|
||||||
while state.page == 'order' do
|
while state.page == 'order' do
|
||||||
local ev, p1, p2, p3 = os.pullEvent()
|
local ev, p1, p2, p3 = os.pullEvent()
|
||||||
if ev == 'redstone' then
|
if ev == 'redstone' then
|
||||||
local now = redstone.getInput('right')
|
local pulsed, _, nextInputs = detectPaymentPulse(prevInputs)
|
||||||
if now and not prev then
|
if pulsed then
|
||||||
playNote('hat', 20, 1, 0.01)
|
playNote('hat', 20, 1, 0.01)
|
||||||
state.paid = (state.paid or 0) + 1; render()
|
state.paid = (state.paid or 0) + 1; render()
|
||||||
if state.paid >= (state.cost or 0) then
|
if state.paid >= (state.cost or 0) then
|
||||||
@@ -1752,7 +1974,8 @@ local function showOrderAndAudio()
|
|||||||
sleep(0.5) -- Wait for UI/Audio slightly
|
sleep(0.5) -- Wait for UI/Audio slightly
|
||||||
confirmAction()
|
confirmAction()
|
||||||
end
|
end
|
||||||
end; prev = now
|
end
|
||||||
|
prevInputs = nextInputs
|
||||||
elseif ev == 'mouse_click' or ev == 'monitor_touch' then
|
elseif ev == 'mouse_click' or ev == 'monitor_touch' then
|
||||||
-- For mouse_click: p1=button, p2=x, p3=y
|
-- For mouse_click: p1=button, p2=x, p3=y
|
||||||
-- For monitor_touch: p1=side, p2=x, p3=y
|
-- For monitor_touch: p1=side, p2=x, p3=y
|
||||||
@@ -1785,18 +2008,14 @@ local function showPrePrintCheck()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function generateTicketId()
|
local function generateTicketId()
|
||||||
local function randLetter()
|
return generateNumericCode('TK', 8)
|
||||||
return string.char(string.byte('A') + math.random(0, 25))
|
|
||||||
end
|
|
||||||
local prefix = randLetter() .. randLetter()
|
|
||||||
local num = string.format('%08d', math.random(0, 99999999))
|
|
||||||
return prefix .. '-' .. num
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ensureTicketIdFormat(id)
|
local function ensureTicketIdFormat(id)
|
||||||
if id == nil then return generateTicketId() end
|
if id == nil then return '' end
|
||||||
local s = tostring(id)
|
local s = tostring(id)
|
||||||
s = s:gsub('%s+', '')
|
s = s:gsub('%s+', '')
|
||||||
|
if #s == 0 then return '' end
|
||||||
local prefix, num = s:match('^([A-Za-z][A-Za-z])%-?([0-9]+)$')
|
local prefix, num = s:match('^([A-Za-z][A-Za-z])%-?([0-9]+)$')
|
||||||
if prefix and num then
|
if prefix and num then
|
||||||
prefix = prefix:upper()
|
prefix = prefix:upper()
|
||||||
@@ -1890,22 +2109,35 @@ local function showDone()
|
|||||||
start_name = startObj and unicodeEscape(startObj.name) or nil,
|
start_name = startObj and unicodeEscape(startObj.name) or nil,
|
||||||
terminal_name = terminalObj and unicodeEscape(terminalObj.name) or nil,
|
terminal_name = terminalObj and unicodeEscape(terminalObj.name) or nil,
|
||||||
start_name_en = fromNameEn,
|
start_name_en = fromNameEn,
|
||||||
terminal_name_en = toNameEn
|
terminal_name_en = toNameEn,
|
||||||
|
ts = (os.epoch and os.epoch('utc')) or (os.time() * 1000)
|
||||||
}
|
}
|
||||||
if ticketVendingMachine and ticketVendingMachine.issueTicket then
|
|
||||||
local apiType = (state.trainType == 'Express') and 'limited_express' or 'local'
|
local apiType = (state.trainType == 'Express') and 'limited_express' or 'local'
|
||||||
local okCall, okIssue, ticketId = pcall(ticketVendingMachine.issueTicket, fromNameEnArg, toNameEnArg, apiType, rides, cost, startStationArg, terminalStationArg, fromNameCnUArg, toNameCnUArg)
|
local localGeneratedTicketId = generateTicketId()
|
||||||
if not (okCall and okIssue and ticketId) then
|
local okIssueTicket, issuedTicketId, issueMethod = issueTicketFromPeripheral(
|
||||||
okCall, okIssue, ticketId = pcall(ticketVendingMachine.issueTicket, fromNameEnArg, toNameEnArg, apiType, rides, cost, startStationArg, terminalStationArg)
|
fromNameEnArg,
|
||||||
end
|
toNameEnArg,
|
||||||
if okCall and okIssue and ticketId then
|
apiType,
|
||||||
state.ticket_id = ensureTicketIdFormat(ticketId)
|
rides,
|
||||||
issueSource = 'ticket_vending_machine'
|
cost,
|
||||||
else
|
startStationArg,
|
||||||
state.ticket_id = generateTicketId()
|
terminalStationArg,
|
||||||
end
|
fromNameCnUArg,
|
||||||
|
toNameCnUArg,
|
||||||
|
localGeneratedTicketId
|
||||||
|
)
|
||||||
|
if okIssueTicket then
|
||||||
|
state.ticket_id = issuedTicketId
|
||||||
|
issueSource = 'ticket_vending_machine'
|
||||||
else
|
else
|
||||||
state.ticket_id = generateTicketId()
|
local issueError = tostring(issueMethod or 'ticket_issue_failed')
|
||||||
|
print('Ticket issue failed: ' .. issueError)
|
||||||
|
_G.TICKET_MACHINE_LAST_TICKET.ticket_issue_error = issueError
|
||||||
|
showAlert('Ticket issue failed')
|
||||||
|
resetTicketFlow()
|
||||||
|
state.page = 'home'
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
pcall(function()
|
pcall(function()
|
||||||
@@ -2074,7 +2306,6 @@ local function showOnlineVoucher()
|
|||||||
elseif ev == 'key' and p1 == keys.backspace then code = code:sub(1, -2)
|
elseif ev == 'key' and p1 == keys.backspace then code = code:sub(1, -2)
|
||||||
elseif ev == 'key' and (p1 == keys.enter or p1 == keys.numPadEnter) then submitCode()
|
elseif ev == 'key' and (p1 == keys.enter or p1 == keys.numPadEnter) then submitCode()
|
||||||
elseif ev == 'config_updated' then
|
elseif ev == 'config_updated' then
|
||||||
-- Config updated in background, continue to redraw
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2101,4 +2332,4 @@ local function mainPageLoop()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
parallel.waitForAny(mainPageLoop, backgroundSyncTask, backgroundTicketUploadTask)
|
parallel.waitForAny(mainPageLoop, backgroundSyncTask, backgroundTicketUploadTask, backgroundPeripheralTask)
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
local URL_GATE = "http://cloud.fse-media.group/d/API/TicketMachine/gate.lua?sign=OWy1wKkKhUhpnxXKeX6fRLePSg1XcaQgWOLvQbMuHRQ=:0"
|
local URL_GATE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/gate.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"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
local URL_MACHINE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/src/commit/db1562b83045284bfdec9e4a3feb829193963943/ticketmachine.lua"
|
local URL_MACHINE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/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"
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
|
||||||
|
local URL_REFILL_MACHINE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/raw/branch/main/refillmachine.lua"
|
||||||
|
|
||||||
|
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 atomicWrite(path, content, binary)
|
||||||
|
local tmp = path .. ".new"
|
||||||
|
if fs.exists(tmp) then fs.delete(tmp) end
|
||||||
|
if not writeFile(tmp, content, binary) then return false end
|
||||||
|
if fs.exists(path) then fs.delete(path) end
|
||||||
|
fs.move(tmp, path)
|
||||||
|
return true
|
||||||
|
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 failErr = p2
|
||||||
|
local res = p3
|
||||||
|
if type(p2) == "table" and type(p2.readAll) == "function" then
|
||||||
|
res = p2
|
||||||
|
failErr = 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(failErr or "http_failure")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
print("Refill Machine Updater")
|
||||||
|
print("")
|
||||||
|
print("Downloading refill machine program...")
|
||||||
|
|
||||||
|
local ok, code = httpGet(URL_REFILL_MACHINE)
|
||||||
|
if not ok or type(code) ~= "string" or #code == 0 then
|
||||||
|
print("Download failed: " .. tostring(code or ""))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not atomicWrite("startup.lua", code, false) then
|
||||||
|
print("Write failed: startup.lua")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not atomicWrite("startup", code, false) then
|
||||||
|
print("Write failed: startup")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not atomicWrite("refillmachine.lua", code, false) then
|
||||||
|
print("Write failed: refillmachine.lua")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Done.")
|
||||||
|
print("refillmachine.lua has been updated and installed as startup.")
|
||||||
|
print("Reboot the computer to apply the update.")
|
||||||
+71
-63
@@ -1,66 +1,74 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>FMG</title>
|
|
||||||
<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="style.css?v=13">
|
|
||||||
<link rel="stylesheet" href="blog.css?v=2">
|
|
||||||
</head>
|
|
||||||
<body class="public-search">
|
|
||||||
<div class="public-container">
|
|
||||||
<header class="search-header" style="text-align: left;">
|
|
||||||
<div style="margin-bottom: 10px; text-align: left;">
|
|
||||||
<a href="https://ticket.fse-media.group" id="homeLink" style="color: var(--primary); text-decoration: none; font-weight: 500;">
|
|
||||||
<i class="fas fa-arrow-left"></i> 返回首页
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; margin-bottom: 10px;">
|
|
||||||
<img src="logo.png" alt="Logo" style="height: 40px; margin-right: 12px;">
|
|
||||||
<h1 style="font-weight: 800; letter-spacing: -1px; margin: 0; text-align: left; font-size: 26px;">
|
|
||||||
FMG
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<section class="tab-panel show">
|
|
||||||
<div class="portal-grid">
|
|
||||||
<a href="http://forum.fse-media.group" class="portal-card">
|
|
||||||
<div class="portal-icon">
|
|
||||||
<i class="fas fa-comments"></i>
|
|
||||||
</div>
|
|
||||||
<h3>论坛</h3>
|
|
||||||
<p>forum.fse-media.group</p>
|
|
||||||
</a>
|
|
||||||
<a href="http://b.igtm.ooooo.ink:11324" class="portal-card">
|
|
||||||
<div class="portal-icon">
|
|
||||||
<i class="fas fa-poll-h"></i>
|
|
||||||
</div>
|
|
||||||
<h3>问卷</h3>
|
|
||||||
<p>b.igtm.ooooo.ink</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="card" style="margin-top: 20px;">
|
|
||||||
<div class="card-title" style="font-size: 1.25rem; font-weight: 700; margin-bottom: 20px; color: var(--text);">
|
|
||||||
<i class="fas fa-server text-primary"></i> 服务器状态</div>
|
|
||||||
<div style="overflow-x: auto; width: 100%;">
|
|
||||||
<iframe id="mc-status-traintown-online-" frameborder="0" width="1000" height="500" style="max-width:100%; border-radius: 8px;" scrolling="no" src="https://motd.minebbs.com/iframe?ip=traintown.online&stype=auto&dark=true"></iframe>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<footer style="margin-top: 60px; padding: 20px 0; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.85rem; text-align: center;">
|
|
||||||
<p>© 2026 FSE Media Group. All rights reserved.</p>
|
|
||||||
</footer>
|
|
||||||
<footer class="site-footer">
|
|
||||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">粤ICP备2025450093号</a>
|
|
||||||
<span class="version">v1.0.12</span>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
<script src="/custom-dialog.js?v=12"></script>
|
|
||||||
<script src="blog.js?v=2"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>FMG</title>
|
||||||
|
<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="style.css?v=13">
|
||||||
|
<link rel="stylesheet" href="blog.css?v=2">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="public-search">
|
||||||
|
<div class="public-container">
|
||||||
|
<header class="search-header" style="text-align: left;">
|
||||||
|
<div style="margin-bottom: 10px; text-align: left;">
|
||||||
|
<a href="https://ticket.fse-media.group" id="homeLink"
|
||||||
|
style="color: var(--primary); text-decoration: none; font-weight: 500;">
|
||||||
|
<i class="fas fa-arrow-left"></i> 返回首页
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 10px;">
|
||||||
|
<img src="logo.png" alt="Logo" style="height: 40px; margin-right: 12px;">
|
||||||
|
<h1 style="font-weight: 800; letter-spacing: -1px; margin: 0; text-align: left; font-size: 26px;">
|
||||||
|
FMG
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<section class="tab-panel show">
|
||||||
|
<div class="portal-grid">
|
||||||
|
<a href="http://forum.fse-media.group" class="portal-card">
|
||||||
|
<div class="portal-icon">
|
||||||
|
<i class="fas fa-comments"></i>
|
||||||
|
</div>
|
||||||
|
<h3>论坛</h3>
|
||||||
|
<p>forum.fse-media.group</p>
|
||||||
|
</a>
|
||||||
|
<a href="http://b.igtm.ooooo.ink:11324" class="portal-card">
|
||||||
|
<div class="portal-icon">
|
||||||
|
<i class="fas fa-poll-h"></i>
|
||||||
|
</div>
|
||||||
|
<h3>问卷</h3>
|
||||||
|
<p>b.igtm.ooooo.ink</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="margin-top: 20px;">
|
||||||
|
<div class="card-title"
|
||||||
|
style="font-size: 1.25rem; font-weight: 700; margin-bottom: 20px; color: var(--text);">
|
||||||
|
<i class="fas fa-server text-primary"></i> 服务器状态
|
||||||
|
</div>
|
||||||
|
<div style="overflow-x: auto; width: 100%;">
|
||||||
|
<iframe id="mc-status-traintown-online-" frameborder="0" width="1000" height="500"
|
||||||
|
style="max-width:100%; border-radius: 8px;" scrolling="no"
|
||||||
|
src="https://motd.minebbs.com/iframe?ip=traintown.online&stype=auto&dark=true"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer
|
||||||
|
style="margin-top: 60px; padding: 20px 0; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.85rem; text-align: center;">
|
||||||
|
<p>© 2026 FSE Media Group. All rights reserved.</p>
|
||||||
|
</footer>
|
||||||
|
<footer class="site-footer">
|
||||||
|
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">粤ICP备2025450093号</a>
|
||||||
|
<span class="version">v1.0.12</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
|
<script src="blog.js?v=2"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
+53
-51
@@ -1,10 +1,10 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<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 閾佽矾绁ㄥ姟绯荤粺 - 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=13">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
<div class="jr-topbar-inner">
|
<div class="jr-topbar-inner">
|
||||||
<a href="/" class="jr-top-link" id="icTopLink">
|
<a href="/" class="jr-top-link" id="icTopLink">
|
||||||
<i class="fas fa-train"></i>
|
<i class="fas fa-train"></i>
|
||||||
<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>
|
||||||
@@ -30,16 +30,16 @@
|
|||||||
<a href="/" class="jr-brand" id="icBrandLink">
|
<a href="/" class="jr-brand" id="icBrandLink">
|
||||||
<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>FarSight-T.N.E铁路运输</strong>
|
||||||
<span>IC 鍗$鐞嗗悗鍙?/span>
|
<span>IC 卡管理后台</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">杞︾エ鏌ヨ</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>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,22 +48,26 @@
|
|||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="jr-admin-sidebar-head">
|
<div class="jr-admin-sidebar-head">
|
||||||
<span class="jr-kicker">IC CARD CONSOLE</span>
|
<span class="jr-kicker">IC CARD CONSOLE</span>
|
||||||
<div class="brand">FSE 閾佽矾绁ㄥ姟绯荤粺鎺у埗鍙?/div>
|
<div class="brand">FSE 铁路票务系统控制台</div>
|
||||||
<p class="jr-admin-sidebar-copy">缁熶竴绠$悊 IC 鍗″彂琛屻€佸厖鍊笺€佹寔鍗′汉淇℃伅鍜屽巻鍙叉搷浣滆褰曘€?/p>
|
<p class="jr-admin-sidebar-copy">统一管理 IC 卡发卡、充值、持卡人信息以及历史操作记录。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<a href="/" class="nav-item" style="text-decoration:none;">
|
<a href="/" class="nav-item" style="text-decoration:none;">
|
||||||
<span class="nav-icon"><i class="fas fa-home"></i></span> 杩斿洖棣栭〉
|
<span class="nav-icon"><i class="fas fa-home"></i></span>
|
||||||
|
返回首页
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin" class="nav-item" style="text-decoration:none;">
|
<a href="/admin" class="nav-item" style="text-decoration:none;">
|
||||||
<span class="nav-icon"><i class="fas fa-chart-pie"></i></span> 涓绘帶鍒跺彴
|
<span class="nav-icon"><i class="fas fa-chart-pie"></i></span>
|
||||||
|
主控制台
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin/ic-card" class="nav-item active" style="text-decoration:none;">
|
<a href="/admin/ic-card" class="nav-item active" style="text-decoration:none;">
|
||||||
<span class="nav-icon"><i class="fas fa-credit-card"></i></span> IC 鍗$鐞?</a>
|
<span class="nav-icon"><i class="fas fa-credit-card"></i></span>
|
||||||
|
IC 卡管理
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-footer jr-admin-sidebar-status">
|
<div class="sidebar-footer jr-admin-sidebar-status">
|
||||||
<div>IC Card Console</div>
|
<div>IC Card Console</div>
|
||||||
<div id="serverStatusText" style="margin-top:6px;">姝e湪妫€娴嬫湇鍔$姸鎬?..</div>
|
<div id="serverStatusText" style="margin-top:6px;">正在检测服务状态...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
@@ -72,42 +76,42 @@
|
|||||||
<div class="flex" style="gap: 12px;">
|
<div class="flex" style="gap: 12px;">
|
||||||
<div>
|
<div>
|
||||||
<span class="jr-kicker">JR STYLE ADMIN</span>
|
<span class="jr-kicker">JR STYLE ADMIN</span>
|
||||||
<h3 style="margin: 0;">IC 鍗$鐞?/h3>
|
<h3 style="margin: 0;">IC 卡管理</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<button id="refreshBtn"><i class="fas fa-sync-alt"></i> 鍒锋柊</button>
|
<button id="refreshBtn"><i class="fas fa-sync-alt"></i> 刷新</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<section class="jr-page-intro jr-admin-intro">
|
<section class="jr-page-intro jr-admin-intro">
|
||||||
<span class="jr-kicker">IC MANAGEMENT</span>
|
<span class="jr-kicker">IC MANAGEMENT</span>
|
||||||
<h1>IC 鍗″彂琛屼笌鐘舵€佺鐞?/h1>
|
<h1>IC 卡发行与状态管理</h1>
|
||||||
<p>寤剁画鍏紑椤电殑鐧藉簳闂ㄦ埛鍐欐硶锛岃鍙戝崱銆佸偍鍊煎拰浜嬩欢璁板綍鍦ㄥ悓涓€鍧楃鐞嗗伐浣滃尯涓繚鎸佹竻鏅扮殑闃呰鑺傚銆?/p>
|
<p>延续公共页面的白底门户风格,让发卡、储值与事件记录在同一块工作区域中保持清晰易读。</p>
|
||||||
</section>
|
</section>
|
||||||
<section class="jr-home-alert jr-admin-alert">
|
<section class="jr-home-alert jr-admin-alert">
|
||||||
<div class="jr-alert-title">
|
<div class="jr-alert-title">
|
||||||
<i class="fas fa-circle-info"></i>
|
<i class="fas fa-circle-info"></i>
|
||||||
<span>涓氬姟鑼冨洿</span>
|
<span>业务范围</span>
|
||||||
</div>
|
</div>
|
||||||
<p>褰撳墠椤甸潰鐢ㄤ簬澶勭悊 IC 鍗″垱寤恒€佷綑棰濈鐞嗐€佹寔鍗′汉璧勬枡鍜屼簨浠舵祦鏌ョ湅锛岄€傚悎浣滀负鍚庡彴鍗″姟绠$悊鐨勫崟鐙叆鍙c€?/p>
|
<p>当前页面用于处理 IC 卡创建、余额管理、持卡人资料和事件流查看,适合作为后台卡务管理的单独入口。</p>
|
||||||
</section>
|
</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>
|
||||||
<div class="stat-value" id="statTotal">0</div>
|
<div class="stat-value" id="statTotal">0</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="stat-label">寰呴鍗?/div>
|
<div class="stat-label">待领卡</div>
|
||||||
<div class="stat-value" id="statPending">0</div>
|
<div class="stat-value" id="statPending">0</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="stat-label">姝e父鍚敤</div>
|
<div class="stat-label">正常启用</div>
|
||||||
<div class="stat-value" id="statActive">0</div>
|
<div class="stat-value" id="statActive">0</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="stat-label">鍌ㄥ€兼€婚</div>
|
<div class="stat-label">储值总额</div>
|
||||||
<div class="stat-value" id="statBalance">0</div>
|
<div class="stat-value" id="statBalance">0</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,28 +119,25 @@
|
|||||||
<div class="management-sidebar">
|
<div class="management-sidebar">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="flex between mb-4">
|
<div class="flex between mb-4">
|
||||||
<h4>蹇€熷缓鍗?/h4>
|
<h4>快速建卡</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="ic-form-grid">
|
<div class="ic-form-grid">
|
||||||
<input id="createHolder" placeholder="鎸佸崱浜哄鍚嶏紙浠呰嫳鏂囦笌绗﹀彿锛?>
|
<input id="createHolder" placeholder="持卡人姓名,仅支持英文与常用符号">
|
||||||
<input id="createBalance" type="number" min="0" step="1" value="50"
|
<input id="createBalance" type="number" min="0" step="1" value="50" placeholder="初始余额">
|
||||||
placeholder="鍒濆浣欓">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted" style="margin-top:12px;">鍚庡彴寤哄崱涔熺粺涓€涓?IC 鍌ㄥ€煎崱锛屾寔鍗′汉濮撳悕浠呮敮鎸佽嫳鏂囦笌甯哥敤绗﹀彿銆? </div>
|
<div class="text-muted" style="margin-top:12px;">后台建卡统一创建为 IC 储值卡,持卡人姓名仅支持英文与常用符号。</div>
|
||||||
<div class="toolbar" style="margin-top: 14px;">
|
<div class="toolbar" style="margin-top: 14px;">
|
||||||
<button id="createBtn" class="btn primary"><i class="fas fa-plus"></i> 鍒涘缓 IC
|
<button id="createBtn" class="btn primary"><i class="fas fa-plus"></i> 创建 IC 卡</button>
|
||||||
鍗?/button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card"
|
<div class="card" style="margin-bottom:0; display:flex; flex-direction:column; min-height: 520px;">
|
||||||
style="margin-bottom:0; display:flex; flex-direction:column; min-height: 520px;">
|
|
||||||
<div class="flex between mb-4">
|
<div class="flex between mb-4">
|
||||||
<h4>鍗$墖鍒楄〃</h4>
|
<h4>卡片列表</h4>
|
||||||
<span class="badge" id="listCountBadge">0</span>
|
<span class="badge" id="listCountBadge">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mb-4" style="flex-wrap:wrap;">
|
<div class="flex mb-4" style="flex-wrap:wrap;">
|
||||||
<input id="searchInput" placeholder="鎼滅储鍗″彿 / 璁㈠崟鍙?/ 濮撳悕" style="flex:1;">
|
<input id="searchInput" placeholder="搜索卡号 / 订单号 / 姓名" style="flex:1;">
|
||||||
</div>
|
</div>
|
||||||
<div id="cardList" class="list-lines" style="flex:1; overflow-y:auto;"></div>
|
<div id="cardList" class="list-lines" style="flex:1; overflow-y:auto;"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,33 +146,31 @@
|
|||||||
<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">
|
||||||
<h4>鍗$墖璇︽儏</h4>
|
<h4>卡片详情</h4>
|
||||||
<div class="flex" style="gap:8px;">
|
<div class="flex" style="gap:8px;">
|
||||||
<button id="topupBtn" class="btn"><i class="fas fa-wallet"></i> 鍏呭€?/button>
|
<button id="topupBtn" class="btn"><i class="fas fa-wallet"></i> 充值</button>
|
||||||
<button id="saveBtn" class="btn primary"><i class="fas fa-save"></i>
|
<button id="saveBtn" class="btn primary"><i class="fas fa-save"></i> 保存</button>
|
||||||
淇濆瓨</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="detailPanel" class="empty-state">
|
<div id="detailPanel" class="empty-state">
|
||||||
<i class="fas fa-credit-card" style="font-size:48px; opacity:0.3;"></i>
|
<i class="fas fa-credit-card" style="font-size:48px; opacity:0.3;"></i>
|
||||||
<p>浠庡乏渚ч€夋嫨涓€寮?IC 鍗′互鏌ョ湅璇︽儏銆?/p>
|
<p>从左侧选择一张 IC 卡以查看详情。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="margin-bottom:0;">
|
<div class="card" style="margin-bottom:0;">
|
||||||
<div class="flex between mb-4">
|
<div class="flex between mb-4">
|
||||||
<h4>鎿嶄綔璁板綍</h4>
|
<h4>操作记录</h4>
|
||||||
</div>
|
</div>
|
||||||
<div id="eventList" class="timeline">
|
<div id="eventList" class="timeline">
|
||||||
<div class="loading">閫夋嫨鍗$墖鍚庢樉绀轰簨浠舵祦銆?/div>
|
<div class="loading">选择卡片后显示事件流。</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<a href="https://beian.miit.gov.cn/" target="_blank"
|
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">粤ICP备2025450093号</a>
|
||||||
rel="noopener noreferrer">绮CP澶?025450093鍙?/a>
|
|
||||||
<span class="version">v1.0.12</span>
|
<span class="version">v1.0.12</span>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,8 +193,11 @@
|
|||||||
'card-order': isDomain ? 'https://ticket.fse-media.group/ic-card/order' : '/ic-card-order.html'
|
'card-order': isDomain ? 'https://ticket.fse-media.group/ic-card/order' : '/ic-card-order.html'
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('icTopLink').href = links.home;
|
const topLink = document.getElementById('icTopLink');
|
||||||
document.getElementById('icBrandLink').href = links.home;
|
const brandLink = document.getElementById('icBrandLink');
|
||||||
|
|
||||||
|
if (topLink) topLink.href = links.home;
|
||||||
|
if (brandLink) brandLink.href = links.home;
|
||||||
|
|
||||||
document.querySelectorAll('[data-link]').forEach((el) => {
|
document.querySelectorAll('[data-link]').forEach((el) => {
|
||||||
const key = el.getAttribute('data-link');
|
const key = el.getAttribute('data-link');
|
||||||
|
|||||||
@@ -30,8 +30,8 @@
|
|||||||
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
||||||
<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 Railway</strong>
|
<strong>FarSight-T.N.E铁路运输</strong>
|
||||||
<span>IC Card Detail</span>
|
<span>IC卡 详情</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<nav class="jr-nav" aria-label="站点导航">
|
<nav class="jr-nav" aria-label="站点导航">
|
||||||
|
|||||||
+55
-37
@@ -1,10 +1,10 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<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>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=13">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
<div class="jr-topbar-inner">
|
<div class="jr-topbar-inner">
|
||||||
<a href="https://ticket.fse-media.group" id="homeLink" class="jr-top-link">
|
<a href="https://ticket.fse-media.group" id="homeLink" 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>
|
||||||
@@ -30,89 +30,89 @@
|
|||||||
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
||||||
<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 Railway</strong>
|
<strong>FarSight-T.N.E铁路运输</strong>
|
||||||
<span>IC Card Online Order</span>
|
<span>IC卡 线上预定</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">杞︾エ鏌ヨ</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 href="https://ticket.fse-media.group/ic-card/order" data-link="card-order" class="is-active">线上购卡</a>
|
||||||
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">IC CARD ORDER</span>
|
<span class="jr-kicker">IC CARD ORDER</span>
|
||||||
<h1>鍦ㄧ嚎璐拱 IC 鍗″苟鐢熸垚棰嗗崱鍑瘉</h1>
|
<h1>在线购买 IC 卡并生成领卡凭证</h1>
|
||||||
<p>鎻愪氦鎸佸崱浜哄鍚嶅苟閫夋嫨棣栨鍏呭€奸噾棰濆悗锛岀郴缁熶細鍗虫椂鐢熸垚鍗″彿鍜?5 浣嶅嚟璇佺爜锛屾梾瀹㈠彲鍑嚟璇佺爜鍒扮珯鍐呭姙鐞嗛鍗°€?/p>
|
<p>提交持卡人姓名并选择首次充值金额后,系统会即时生成卡号和 5 位凭证码,旅客可凭凭证码到站内办理领卡。</p>
|
||||||
</section>
|
</section>
|
||||||
<section class="jr-home-alert">
|
<section class="jr-home-alert">
|
||||||
<div class="jr-alert-title">
|
<div class="jr-alert-title">
|
||||||
<i class="fas fa-circle-info"></i>
|
<i class="fas fa-circle-info"></i>
|
||||||
<span>璐崱鎻愰啋</span>
|
<span>购卡提醒</span>
|
||||||
</div>
|
</div>
|
||||||
<p>绾夸笂璐崱鍒涘缓鍚庨粯璁ょ姸鎬佷负鈥滃緟棰嗗崱鈥濓紱鎸佸崱浜哄鍚嶄粎鏀寔鑻辨枃涓庡父鐢ㄧ鍙枫€傚闇€琛ユ煡鍑瘉鎴栧崱鐗囩姸鎬侊紝鍙墠寰€ IC 鍗℃煡璇㈤〉闈㈣緭鍏ュ崱鍙锋垨鍑瘉鐮佹绱€?/p>
|
<p>线上购卡创建后默认状态为“待领卡”;持卡人姓名仅支持英文与常用符号。如需补查凭证或卡片状态,可前往 IC 卡查询页输入卡号或凭证码检索。</p>
|
||||||
</section>
|
</section>
|
||||||
<section class="jr-grid-two">
|
<section class="jr-grid-two">
|
||||||
<article class="jr-panel-card">
|
<article class="jr-panel-card">
|
||||||
<div class="jr-panel-headline">
|
<div class="jr-panel-headline">
|
||||||
<h2>棣栨鍏呭€?/h2>
|
<h2>首次充值</h2>
|
||||||
<span class="jr-panel-note">First Top-up</span>
|
<span class="jr-panel-note">First Top-up</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="rechargeOptionList" class="jr-card-plan-grid">
|
<div id="rechargeOptionList" class="jr-card-plan-grid">
|
||||||
<div class="jr-center-empty">
|
<div class="jr-center-empty">
|
||||||
<p>姝e湪鍔犺浇鍏呭€奸厤缃?..</p>
|
<p>正在加载充值配置...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="customRechargeBox" class="jr-card-plan-custom-box">
|
<div id="customRechargeBox" class="jr-card-plan-custom-box">
|
||||||
<input id="customInitialBalance" class="jr-search-input" type="number" min="1" step="1"
|
<input id="customInitialBalance" class="jr-search-input" type="number" min="1" step="1"
|
||||||
placeholder="鑷畾涔夐娆″厖鍊奸噾棰濓紙閫夋嫨鈥滆嚜瀹氫箟鈥濆悗鍚敤锛? disabled>
|
placeholder="自定义首次充值金额,选择“自定义”后启用" disabled>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="jr-panel-headline" style="margin-top:24px;">
|
<div class="jr-panel-headline" style="margin-top:24px;">
|
||||||
<h3>鎸佸崱浜轰俊鎭?/h3>
|
<h3>持卡人信息</h3>
|
||||||
<span class="jr-panel-note">Order Form</span>
|
<span class="jr-panel-note">Order Form</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="ic-form-grid">
|
<div class="ic-form-grid">
|
||||||
<input id="holderName" class="jr-search-input" type="text" maxlength="24"
|
<input id="holderName" class="jr-search-input" type="text" maxlength="24"
|
||||||
placeholder="鎸佸崱浜哄鍚嶏紙浠呰嫳鏂囦笌绗﹀彿锛?>
|
placeholder="持卡人姓名,仅支持英文与常用符号">
|
||||||
</div>
|
</div>
|
||||||
<p id="holderNameHint" class="text-muted" style="margin-top:12px;">浠呮敮鎸佽嫳鏂囦笌甯哥敤绗﹀彿锛屼緥濡?`Alex
|
<p id="holderNameHint" class="text-muted" style="margin-top:12px;">仅支持英文与常用符号,例如 `Alex Smith`、`A.Brown`、`Chris-O'Neil`。</p>
|
||||||
Smith`銆乣A.Brown`銆乣Chris-O'Neil`銆?/p>
|
|
||||||
<div class="jr-action-row">
|
<div class="jr-action-row">
|
||||||
<button id="submitOrderBtn" class="btn primary jr-search-button"><i
|
<button id="submitOrderBtn" class="btn primary jr-search-button">
|
||||||
class="fas fa-credit-card"></i> 鎻愪氦璐崱</button>
|
<i class="fas fa-credit-card"></i>
|
||||||
|
提交购卡
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<div>
|
<div>
|
||||||
<article class="jr-panel-card" style="margin-bottom:20px;">
|
<article class="jr-panel-card" style="margin-bottom:20px;">
|
||||||
<div class="jr-panel-headline">
|
<div class="jr-panel-headline">
|
||||||
<h2>璐圭敤棰勪及</h2>
|
<h2>费用预估</h2>
|
||||||
<span class="jr-panel-note">Estimate</span>
|
<span class="jr-panel-note">Estimate</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="estimateBox" class="ic-inline-meta">
|
<div id="estimateBox" class="ic-inline-meta">
|
||||||
<div class="jr-center-empty">
|
<div class="jr-center-empty">
|
||||||
<p>璇烽€夋嫨棣栨鍏呭€奸噾棰濆悗鏌ョ湅璐圭敤鏋勬垚銆?/p>
|
<p>请选择首次充值金额后查看费用构成。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<article class="jr-panel-card">
|
<article class="jr-panel-card">
|
||||||
<div class="jr-panel-headline">
|
<div class="jr-panel-headline">
|
||||||
<h2>璐崱缁撴灉</h2>
|
<h2>购卡结果</h2>
|
||||||
<span class="jr-panel-note">Card Result</span>
|
<span class="jr-panel-note">Card Result</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="orderResultBox" class="jr-center-empty">
|
<div id="orderResultBox" class="jr-center-empty">
|
||||||
<p>鎻愪氦鍚庡皢鍦ㄦ鏄剧ず鍗″彿銆佸嚟璇佺爜鍜岄鍗℃彁绀恒€?/p>
|
<p>提交后将在此显示卡号、凭证码和领卡提示。</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<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>
|
||||||
@@ -121,11 +121,29 @@
|
|||||||
<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>
|
||||||
<script>document.addEventListener('DOMContentLoaded', () => {
|
<script>
|
||||||
const isDomain = location.hostname.includes('fse-media.group'); const links = {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
home: isDomain ? 'https://ticket.fse-media.group' : '/home.html', order: isDomain ? 'https://ticket.fse-media.group/order' : '/ticket-order.html', search: isDomain ? 'https://ticket.fse-media.group/search' : '/ticket-search.html', 'card-search': isDomain ? 'https://ticket.fse-media.group/ic-card/search' : '/ic-card-search.html', 'card-order': isDomain ? 'https://ticket.fse-media.group/ic-card/order' : '/ic-card-order.html'
|
const isDomain = location.hostname.includes('fse-media.group');
|
||||||
}; const homeLink = document.getElementById('homeLink'); const brandLink = document.getElementById('brandLink'); if (homeLink) homeLink.href = links.home; if (brandLink) brandLink.href = links.home; document.querySelectorAll('[data-link]').forEach((el) => { const key = el.getAttribute('data-link'); if (links[key]) el.href = links[key]; });
|
const links = {
|
||||||
});</script>
|
home: isDomain ? 'https://ticket.fse-media.group' : '/home.html',
|
||||||
|
order: isDomain ? 'https://ticket.fse-media.group/order' : '/ticket-order.html',
|
||||||
|
search: isDomain ? 'https://ticket.fse-media.group/search' : '/ticket-search.html',
|
||||||
|
'card-search': isDomain ? 'https://ticket.fse-media.group/ic-card/search' : '/ic-card-search.html',
|
||||||
|
'card-order': isDomain ? 'https://ticket.fse-media.group/ic-card/order' : '/ic-card-order.html'
|
||||||
|
};
|
||||||
|
|
||||||
|
const homeLink = document.getElementById('homeLink');
|
||||||
|
const brandLink = document.getElementById('brandLink');
|
||||||
|
|
||||||
|
if (homeLink) homeLink.href = links.home;
|
||||||
|
if (brandLink) brandLink.href = links.home;
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-link]').forEach((el) => {
|
||||||
|
const key = el.getAttribute('data-link');
|
||||||
|
if (links[key]) el.href = links[key];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+136
-43
@@ -1,13 +1,13 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<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>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=13">
|
<link rel="stylesheet" href="/style.css?v=14">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="public-search jr-public-page">
|
<body class="public-search jr-public-page">
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
<div class="jr-topbar-inner">
|
<div class="jr-topbar-inner">
|
||||||
<a href="https://ticket.fse-media.group" id="homeLink" class="jr-top-link">
|
<a href="https://ticket.fse-media.group" id="homeLink" 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>
|
||||||
@@ -30,75 +30,168 @@
|
|||||||
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
||||||
<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>FarSight-T.N.E铁路运输</strong>
|
||||||
<span>IC 鍗℃煡璇㈡湇鍔?/span>
|
<span>IC卡 查询</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">杞︾エ鏌ヨ</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 href="https://ticket.fse-media.group/ic-card/search" data-link="card-search" class="is-active">IC 卡查询</a>
|
||||||
鍗℃煡璇?/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">IC CARD SEARCH</span>
|
<span class="jr-kicker">IC CARD SEARCH</span>
|
||||||
<h1>鎸夊崱鍙锋垨鍑瘉鐮佹煡璇?IC 鍗$姸鎬?/h1>
|
<h1>按卡号或凭证码查询 IC 卡状态</h1>
|
||||||
<p>鍙煡璇?IC 鍗$殑褰撳墠鐘舵€併€佷綑棰濆拰鏈€杩戞搷浣滆褰曘€傝緭鍏ョ嚎涓婅喘鍗$敓鎴愮殑鍑瘉鐮佷篃鍙弽鏌ュ搴斿崱鐗囥€?/p>
|
<p>支持检索 IC 卡当前状态、余额和最近操作记录;输入线上购卡生成的凭证码,也能反查对应卡片。</p>
|
||||||
|
</section>
|
||||||
|
<section class="jr-query-overview jr-grid-three" aria-label="IC 卡查询摘要">
|
||||||
|
<article class="jr-query-stat">
|
||||||
|
<span class="jr-query-stat-label">检索方式</span>
|
||||||
|
<strong>卡号 / 凭证码</strong>
|
||||||
|
<p>支持凭证码反查对应卡片,也支持直接输入卡号查看当前状态与余额。</p>
|
||||||
|
</article>
|
||||||
|
<article class="jr-query-stat">
|
||||||
|
<span class="jr-query-stat-label">结果浏览</span>
|
||||||
|
<strong>列表与详情并排</strong>
|
||||||
|
<p>左侧浏览卡片列表,右侧查看卡片详情、状态提示和最近操作记录。</p>
|
||||||
|
</article>
|
||||||
|
<article class="jr-query-stat">
|
||||||
|
<span class="jr-query-stat-label">移动端</span>
|
||||||
|
<strong>触达区更大</strong>
|
||||||
|
<p>手机端自动切换为单列阅读,卡片点击区域与按钮尺寸都更适合触屏操作。</p>
|
||||||
|
</article>
|
||||||
</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">Card ID / Voucher Code</span>
|
<span class="jr-panel-note">Card ID / Voucher Code</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-search-form">
|
<div class="jr-search-form">
|
||||||
<input id="queryInput" class="jr-search-input" type="text"
|
<input id="queryInput" class="jr-search-input" type="text"
|
||||||
placeholder="杈撳叆鍗″彿鎴栧嚟璇佺爜锛屽 IC-348215 / M1SKP" />
|
placeholder="输入卡号或凭证码,例如 IC-348215 / M1SKP" />
|
||||||
<button id="queryBtn" class="btn primary jr-search-button"><i class="fas fa-search"></i> 鏌ヨ IC
|
<button id="queryBtn" class="btn primary jr-search-button">
|
||||||
鍗?/button>
|
<i class="fas fa-search"></i>
|
||||||
|
查询 IC 卡
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="jr-search-helper">留空可浏览全部 IC 卡;输入卡号或凭证码后,可直接定位到对应卡片详情。</p>
|
||||||
</section>
|
</section>
|
||||||
<section class="jr-grid-two">
|
<section class="jr-search-results">
|
||||||
<article class="jr-panel-card">
|
<article class="jr-panel-card">
|
||||||
<div class="jr-panel-headline">
|
<div class="jr-panel-headline">
|
||||||
<h3>鍗$墖姒傝</h3>
|
<h3>结果列表</h3>
|
||||||
<span class="jr-panel-note">Card Overview</span>
|
<span class="jr-panel-note">Card Results</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="summaryBox" class="jr-center-empty">
|
<div id="summaryBox" class="jr-scroll-box">
|
||||||
<p>璇疯緭鍏ュ崱鍙锋垨鍑瘉鐮佸紑濮嬫煡璇€?/p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<article class="jr-panel-card">
|
|
||||||
<div class="jr-panel-headline">
|
|
||||||
<h3>浜嬩欢璁板綍</h3>
|
|
||||||
<span class="jr-panel-note">Recent Events</span>
|
|
||||||
</div>
|
|
||||||
<div id="eventBox" class="jr-history-list">
|
|
||||||
<div class="jr-center-empty" style="min-height:180px;">
|
<div class="jr-center-empty" style="min-height:180px;">
|
||||||
<p>鏌ヨ鎴愬姛鍚庢樉绀哄缓鍗°€佽喘鍗°€佸厖鍊肩瓑鎿嶄綔璁板綍銆?/p>
|
<p>请输入卡号或凭证码开始查询。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
<section class="jr-detail-stack">
|
||||||
|
<article class="jr-panel-card">
|
||||||
|
<div class="jr-panel-headline">
|
||||||
|
<h3>卡片详情</h3>
|
||||||
|
<span class="jr-panel-note">Card Overview</span>
|
||||||
|
</div>
|
||||||
|
<div id="detailBox">
|
||||||
|
<div class="jr-center-empty" style="min-height:180px;">
|
||||||
|
<p>从左侧选择一张 IC 卡以查看详情。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="jr-panel-card">
|
||||||
|
<div class="jr-panel-headline">
|
||||||
|
<h3>事件记录</h3>
|
||||||
|
<span class="jr-panel-note">Recent Events</span>
|
||||||
|
</div>
|
||||||
|
<div id="eventBox" class="jr-history-list">
|
||||||
|
<div class="jr-center-empty" style="min-height:180px;">
|
||||||
|
<p>查询成功后会在这里显示建卡、购卡、充值等操作记录。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<div class="jr-grid-two">
|
||||||
|
<article class="jr-panel-card jr-guide-card">
|
||||||
|
<div class="jr-panel-headline">
|
||||||
|
<h3>状态说明</h3>
|
||||||
|
<span class="jr-panel-note">Card Status</span>
|
||||||
|
</div>
|
||||||
|
<div class="jr-guide-list">
|
||||||
|
<div class="jr-guide-item">
|
||||||
|
<strong>正常</strong>
|
||||||
|
<span>卡片已启用,可在检票设备直接刷卡进出站。</span>
|
||||||
|
</div>
|
||||||
|
<div class="jr-guide-item">
|
||||||
|
<strong>待领卡</strong>
|
||||||
|
<span>请持购卡凭证码前往站内售票机完成领卡后再使用。</span>
|
||||||
|
</div>
|
||||||
|
<div class="jr-guide-item">
|
||||||
|
<strong>不可用</strong>
|
||||||
|
<span>卡片已停用、挂失或退款,建议联系站务进行处理。</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="jr-panel-card jr-guide-card">
|
||||||
|
<div class="jr-panel-headline">
|
||||||
|
<h3>查询提示</h3>
|
||||||
|
<span class="jr-panel-note">Search Guide</span>
|
||||||
|
</div>
|
||||||
|
<div class="jr-guide-list">
|
||||||
|
<div class="jr-guide-item">
|
||||||
|
<strong>留空查询</strong>
|
||||||
|
<span>不输入关键字时,会按建卡时间倒序展示全部 IC 卡记录。</span>
|
||||||
|
</div>
|
||||||
|
<div class="jr-guide-item">
|
||||||
|
<strong>凭证反查</strong>
|
||||||
|
<span>购卡后若未领卡,可直接使用凭证码快速定位对应卡片。</span>
|
||||||
|
</div>
|
||||||
|
<div class="jr-guide-item">
|
||||||
|
<strong>手机查看</strong>
|
||||||
|
<span>移动端会把结果列表、详情和事件记录按顺序折叠为单列阅读。</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
<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="/custom-dialog.js?v=12"></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=3"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script>document.addEventListener('DOMContentLoaded', () => {
|
<script>
|
||||||
const isDomain = location.hostname.includes('fse-media.group'); const links = {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
home: isDomain ? 'https://ticket.fse-media.group' : '/home.html', order: isDomain ? 'https://ticket.fse-media.group/order' : '/ticket-order.html', search: isDomain ? 'https://ticket.fse-media.group/search' : '/ticket-search.html', 'card-search': isDomain ? 'https://ticket.fse-media.group/ic-card/search' : '/ic-card-search.html', 'card-order': isDomain ? 'https://ticket.fse-media.group/ic-card/order' : '/ic-card-order.html'
|
const isDomain = location.hostname.includes('fse-media.group');
|
||||||
}; const homeLink = document.getElementById('homeLink'); const brandLink = document.getElementById('brandLink'); if (homeLink) homeLink.href = links.home; if (brandLink) brandLink.href = links.home; document.querySelectorAll('[data-link]').forEach((el) => { const key = el.getAttribute('data-link'); if (links[key]) el.href = links[key]; });
|
const links = {
|
||||||
});</script>
|
home: isDomain ? 'https://ticket.fse-media.group' : '/home.html',
|
||||||
|
order: isDomain ? 'https://ticket.fse-media.group/order' : '/ticket-order.html',
|
||||||
|
search: isDomain ? 'https://ticket.fse-media.group/search' : '/ticket-search.html',
|
||||||
|
'card-search': isDomain ? 'https://ticket.fse-media.group/ic-card/search' : '/ic-card-search.html',
|
||||||
|
'card-order': isDomain ? 'https://ticket.fse-media.group/ic-card/order' : '/ic-card-order.html'
|
||||||
|
};
|
||||||
|
|
||||||
|
const homeLink = document.getElementById('homeLink');
|
||||||
|
const brandLink = document.getElementById('brandLink');
|
||||||
|
|
||||||
|
if (homeLink) homeLink.href = links.home;
|
||||||
|
if (brandLink) brandLink.href = links.home;
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-link]').forEach((el) => {
|
||||||
|
const key = el.getAttribute('data-link');
|
||||||
|
if (links[key]) el.href = links[key];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
+146
-22
@@ -3,7 +3,12 @@
|
|||||||
const inputEl = $('#queryInput');
|
const inputEl = $('#queryInput');
|
||||||
const queryBtn = $('#queryBtn');
|
const queryBtn = $('#queryBtn');
|
||||||
const summaryBoxEl = $('#summaryBox');
|
const summaryBoxEl = $('#summaryBox');
|
||||||
|
const detailBoxEl = $('#detailBox');
|
||||||
const eventBoxEl = $('#eventBox');
|
const eventBoxEl = $('#eventBox');
|
||||||
|
const state = {
|
||||||
|
cards: [],
|
||||||
|
selectedQuery: ''
|
||||||
|
};
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
async request(url) {
|
async request(url) {
|
||||||
@@ -19,6 +24,15 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
const s = String(status || '').trim().toLowerCase();
|
||||||
|
if (s === 'active') return 'jr-status-valid';
|
||||||
|
if (s === 'pending_pickup') return 'jr-status-used';
|
||||||
|
return 'jr-status-expired';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLookupKey = (card) => String(card?.card_id || '').trim();
|
||||||
|
|
||||||
const escapeHtml = (value) => String(value == null ? '' : value)
|
const escapeHtml = (value) => String(value == null ? '' : value)
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
@@ -44,33 +58,40 @@
|
|||||||
return map[String(event?.type || '').toLowerCase()] || (event?.type || '事件');
|
return map[String(event?.type || '').toLowerCase()] || (event?.type || '事件');
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderSummary = (card) => {
|
const buildCardPreview = (card) => {
|
||||||
const shownCardId = card.display_card_id || card.card_id || '---';
|
const shownCardId = card.display_card_id || card.card_id || '---';
|
||||||
summaryBoxEl.className = '';
|
const detailHref = window.location.hostname.includes('fse-media.group')
|
||||||
summaryBoxEl.innerHTML = `
|
? `https://ticket.fse-media.group/ic/${encodeURIComponent(card.card_id || shownCardId)}`
|
||||||
|
: `/ic/${encodeURIComponent(card.card_id || shownCardId)}`;
|
||||||
|
return `
|
||||||
<div class="jr-ticket-preview">
|
<div class="jr-ticket-preview">
|
||||||
<div class="jr-ticket-row-head">
|
<div class="jr-ticket-row-head">
|
||||||
<span class="jr-ticket-id mono">${escapeHtml(shownCardId)}</span>
|
<span class="jr-ticket-id mono">${escapeHtml(shownCardId)}</span>
|
||||||
<span class="jr-status-pill ${card.status === 'active' ? 'jr-status-valid' : (card.status === 'pending_pickup' ? 'jr-status-used' : 'jr-status-expired')}">${escapeHtml(card.status_label || card.status || '未知')}</span>
|
<span class="jr-status-pill ${getStatusClass(card.status)}">${escapeHtml(card.status_label || card.status || '未知')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-meta-grid">
|
<div class="jr-meta-grid">
|
||||||
<div class="jr-meta-item"><span>持卡人</span><strong>${escapeHtml(card.holder_name || '未登记')}</strong></div>
|
<div class="jr-meta-item"><span>持卡人</span><strong>${escapeHtml(card.holder_name || '未登记')}</strong></div>
|
||||||
<div class="jr-meta-item"><span>卡片类型</span><strong>IC 储值卡</strong></div>
|
<div class="jr-meta-item"><span>卡片类型</span><strong>${escapeHtml(card.card_type_label || 'IC 储值卡')}</strong></div>
|
||||||
<div class="jr-meta-item"><span>余额</span><strong>${escapeHtml(card.balance ?? 0)}</strong></div>
|
<div class="jr-meta-item"><span>余额</span><strong>${escapeHtml(card.balance ?? 0)}</strong></div>
|
||||||
<div class="jr-meta-item"><span>首次充值</span><strong>${escapeHtml(card.purchase_amount ?? card.balance ?? 0)}</strong></div>
|
<div class="jr-meta-item"><span>首次充值</span><strong>${escapeHtml(card.purchase_amount ?? card.balance ?? 0)}</strong></div>
|
||||||
<div class="jr-meta-item"><span>凭证码</span><strong>${escapeHtml(card.voucher_code || card.code || card.order_code || '---')}</strong></div>
|
<div class="jr-meta-item"><span>凭证码</span><strong>${escapeHtml(card.voucher_code || card.code || card.order_code || '---')}</strong></div>
|
||||||
<div class="jr-meta-item"><span>购卡时间</span><strong>${escapeHtml(formatTime(card.created_ts))}</strong></div>
|
<div class="jr-meta-item"><span>购卡时间</span><strong>${escapeHtml(formatTime(card.created_ts))}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="jr-action-row">
|
||||||
|
<a href="${detailHref}" class="btn jr-secondary-btn" target="_blank" rel="noopener noreferrer">
|
||||||
|
<i class="fas fa-id-card"></i>
|
||||||
|
打开卡片页
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderEvents = (events) => {
|
const buildEventsHtml = (events) => {
|
||||||
if (!events.length) {
|
if (!events.length) {
|
||||||
eventBoxEl.innerHTML = '<div class="jr-center-empty" style="min-height:180px;"><p>暂无相关事件记录。</p></div>';
|
return '<div class="jr-center-empty" style="min-height:180px;"><p>暂无相关事件记录。</p></div>';
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
eventBoxEl.innerHTML = events.map((event) => `
|
return events.map((event) => `
|
||||||
<div class="jr-history-item">
|
<div class="jr-history-item">
|
||||||
<div class="jr-history-row">
|
<div class="jr-history-row">
|
||||||
<span class="jr-history-title">${escapeHtml(eventTitle(event))}</span>
|
<span class="jr-history-title">${escapeHtml(eventTitle(event))}</span>
|
||||||
@@ -81,37 +102,140 @@
|
|||||||
`).join('');
|
`).join('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderError = (message) => {
|
const renderDetailPrompt = (message) => {
|
||||||
|
detailBoxEl.innerHTML = `<div class="jr-center-empty" style="min-height:180px;"><p>${escapeHtml(message)}</p></div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderEventPrompt = (message) => {
|
||||||
|
eventBoxEl.innerHTML = `<div class="jr-center-empty" style="min-height:180px;"><p>${escapeHtml(message)}</p></div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSelectedCard = (card, events) => {
|
||||||
|
if (!card) {
|
||||||
|
renderDetailPrompt('请选择左侧卡片查看详情。');
|
||||||
|
renderEventPrompt('请选择左侧卡片查看详情与事件记录。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
detailBoxEl.innerHTML = buildCardPreview(card);
|
||||||
|
eventBoxEl.innerHTML = buildEventsHtml(events);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCardList = () => {
|
||||||
|
if (!state.cards.length) {
|
||||||
|
summaryBoxEl.className = 'jr-center-empty';
|
||||||
|
summaryBoxEl.innerHTML = '<p>暂无可显示的 IC 卡记录。</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryBoxEl.className = 'jr-scroll-box';
|
||||||
|
summaryBoxEl.innerHTML = state.cards.map((card) => {
|
||||||
|
const lookupKey = getLookupKey(card);
|
||||||
|
const shownCardId = card.display_card_id || card.card_id || '---';
|
||||||
|
const voucherCode = card.voucher_code || card.code || card.order_code || '---';
|
||||||
|
const isSelected = lookupKey && state.selectedQuery === lookupKey;
|
||||||
|
return `
|
||||||
|
<div class="jr-ticket-row${isSelected ? ' is-active' : ''}" data-card-query="${escapeHtml(lookupKey)}">
|
||||||
|
<div class="jr-ticket-row-head">
|
||||||
|
<span class="jr-ticket-id mono">${escapeHtml(shownCardId)}</span>
|
||||||
|
<span class="jr-status-pill ${getStatusClass(card.status)}">${escapeHtml(card.status_label || card.status || '未知')}</span>
|
||||||
|
</div>
|
||||||
|
<div class="jr-ticket-route">${escapeHtml(card.holder_name || '未登记持卡人')}</div>
|
||||||
|
<div class="jr-list-meta">
|
||||||
|
余额 ${escapeHtml(card.balance ?? 0)} · 凭证码 ${escapeHtml(voucherCode)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
summaryBoxEl.querySelectorAll('[data-card-query]').forEach((item) => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
const q = item.getAttribute('data-card-query');
|
||||||
|
if (q) {
|
||||||
|
loadCardDetail(q).catch((error) => {
|
||||||
|
renderQueryError(error.message || String(error));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadCardDetail = async (q, options = {}) => {
|
||||||
|
const { updateUrl = true } = options;
|
||||||
|
renderDetailPrompt('正在加载卡片详情...');
|
||||||
|
renderEventPrompt('正在加载事件记录...');
|
||||||
|
const data = await api.query(q);
|
||||||
|
const card = data.card || null;
|
||||||
|
const events = data.events || [];
|
||||||
|
const lookupKey = getLookupKey(card) || q;
|
||||||
|
if (card) {
|
||||||
|
const existingIdx = state.cards.findIndex((item) => getLookupKey(item) === lookupKey);
|
||||||
|
if (existingIdx >= 0) state.cards[existingIdx] = card;
|
||||||
|
else state.cards = [card];
|
||||||
|
}
|
||||||
|
state.selectedQuery = lookupKey;
|
||||||
|
renderCardList();
|
||||||
|
renderSelectedCard(card, events);
|
||||||
|
if (updateUrl) {
|
||||||
|
const newUrl = `${window.location.origin}${window.location.pathname}?q=${encodeURIComponent(q)}`;
|
||||||
|
window.history.replaceState({ path: newUrl }, '', newUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadAllCards = async () => {
|
||||||
summaryBoxEl.className = 'jr-center-empty';
|
summaryBoxEl.className = 'jr-center-empty';
|
||||||
summaryBoxEl.innerHTML = `<p>${escapeHtml(message)}</p>`;
|
summaryBoxEl.innerHTML = '<p>正在加载全部 IC 卡...</p>';
|
||||||
eventBoxEl.innerHTML = '<div class="jr-center-empty" style="min-height:180px;"><p>暂无可显示的事件记录。</p></div>';
|
renderDetailPrompt('正在准备卡片详情...');
|
||||||
|
renderEventPrompt('正在准备事件记录...');
|
||||||
|
const data = await api.query('');
|
||||||
|
state.cards = Array.isArray(data.cards) ? data.cards : [];
|
||||||
|
state.selectedQuery = '';
|
||||||
|
renderCardList();
|
||||||
|
|
||||||
|
if (!state.cards.length) {
|
||||||
|
renderDetailPrompt('当前暂无 IC 卡记录。');
|
||||||
|
renderEventPrompt('当前暂无 IC 卡记录。');
|
||||||
|
const newUrl = `${window.location.origin}${window.location.pathname}`;
|
||||||
|
window.history.replaceState({ path: newUrl }, '', newUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadCardDetail(getLookupKey(state.cards[0]), { updateUrl: false });
|
||||||
|
const newUrl = `${window.location.origin}${window.location.pathname}`;
|
||||||
|
window.history.replaceState({ path: newUrl }, '', newUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
const doQuery = async () => {
|
const doQuery = async () => {
|
||||||
const q = inputEl.value.trim();
|
const q = inputEl.value.trim();
|
||||||
if (!q) {
|
if (!q) {
|
||||||
renderError('请输入卡号或凭证码');
|
await loadAllCards();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
summaryBoxEl.className = 'jr-center-empty';
|
summaryBoxEl.className = 'jr-center-empty';
|
||||||
summaryBoxEl.innerHTML = '<p>正在查询 IC 卡...</p>';
|
summaryBoxEl.innerHTML = '<p>正在查询 IC 卡...</p>';
|
||||||
eventBoxEl.innerHTML = '<div class="jr-center-empty" style="min-height:180px;"><p>正在加载事件记录...</p></div>';
|
renderDetailPrompt('正在查询卡片详情...');
|
||||||
const data = await api.query(q);
|
renderEventPrompt('正在查询事件记录...');
|
||||||
renderSummary(data.card || {});
|
state.cards = [];
|
||||||
renderEvents(data.events || []);
|
await loadCardDetail(q);
|
||||||
const newUrl = `${window.location.origin}${window.location.pathname}?q=${encodeURIComponent(q)}`;
|
|
||||||
window.history.replaceState({ path: newUrl }, '', newUrl);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
queryBtn.addEventListener('click', () => doQuery().catch((error) => renderError(error.message || String(error))));
|
const renderQueryError = (message) => {
|
||||||
|
summaryBoxEl.className = 'jr-center-empty';
|
||||||
|
summaryBoxEl.innerHTML = `<p>${escapeHtml(message)}</p>`;
|
||||||
|
renderDetailPrompt(message);
|
||||||
|
renderEventPrompt(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
queryBtn.addEventListener('click', () => doQuery().catch((error) => renderQueryError(error.message || String(error))));
|
||||||
inputEl.addEventListener('keydown', (event) => {
|
inputEl.addEventListener('keydown', (event) => {
|
||||||
if (event.key === 'Enter') doQuery().catch((error) => renderError(error.message || String(error)));
|
if (event.key === 'Enter') doQuery().catch((error) => renderQueryError(error.message || String(error)));
|
||||||
});
|
});
|
||||||
|
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const q = params.get('q');
|
const q = params.get('q');
|
||||||
if (q) {
|
if (q) {
|
||||||
inputEl.value = q;
|
inputEl.value = q;
|
||||||
doQuery().catch((error) => renderError(error.message || String(error)));
|
doQuery().catch((error) => renderQueryError(error.message || String(error)));
|
||||||
|
} else {
|
||||||
|
loadAllCards().catch((error) => renderQueryError(error.message || String(error)));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
+22
-1
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<!-- 充满未知和不稳定的票务系统! -->
|
<!-- 充满未知和不稳定的票务系统! -->
|
||||||
|
|
||||||
@@ -841,6 +841,27 @@
|
|||||||
<button @click="saveConfig"><i class="fas fa-save"></i> 保存</button>
|
<button @click="saveConfig"><i class="fas fa-save"></i> 保存</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="display:block; margin-bottom:8px; font-weight:600;">Lua 脚本更新控制</label>
|
||||||
|
<div class="flex" style="flex-direction: column; gap: 10px;">
|
||||||
|
<div class="flex" style="flex-wrap: wrap; gap: 10px; align-items: center;">
|
||||||
|
<span style="min-width: 70px;">售票机</span>
|
||||||
|
<input v-model="config.lua_versions.ticketmachine" placeholder="例如 v1.5.8" style="max-width: 180px;">
|
||||||
|
<button @click="bumpLuaVersion('ticketmachine')"><i class="fas fa-arrow-up"></i> 补丁 +1</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex" style="flex-wrap: wrap; gap: 10px; align-items: center;">
|
||||||
|
<span style="min-width: 70px;">检票机</span>
|
||||||
|
<input v-model="config.lua_versions.gate" placeholder="例如 v1.5.8" style="max-width: 180px;">
|
||||||
|
<button @click="bumpLuaVersion('gate')"><i class="fas fa-arrow-up"></i> 补丁 +1</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted" style="font-size: 0.9rem;">
|
||||||
|
每次发布新的 Lua 脚本后,在这里手动提升一次版本号;设备检测到不一致时会在左上角版本号旁显示更新标记。
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<button @click="saveConfig"><i class="fas fa-save"></i> 保存 Lua 版本</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h4>数据管理</h4>
|
<h4>数据管理</h4>
|
||||||
|
|||||||
+29
-2
@@ -44,7 +44,11 @@ createApp({
|
|||||||
const fares = ref([]);
|
const fares = ref([]);
|
||||||
const tickets = ref([]);
|
const tickets = ref([]);
|
||||||
const stats = reactive({ sold_tickets: 0, revenue: 0 });
|
const stats = reactive({ sold_tickets: 0, revenue: 0 });
|
||||||
const config = reactive({ api_base: '', promotion: { name: '', discount: 1 } });
|
const config = reactive({
|
||||||
|
api_base: '',
|
||||||
|
promotion: { name: '', discount: 1 },
|
||||||
|
lua_versions: { ticketmachine: 'v1.5.8', gate: 'v1.5.8' }
|
||||||
|
});
|
||||||
const logs = ref([]);
|
const logs = ref([]);
|
||||||
const logCategory = ref('');
|
const logCategory = ref('');
|
||||||
const logTypeFilter = ref('');
|
const logTypeFilter = ref('');
|
||||||
@@ -224,6 +228,20 @@ createApp({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeLuaVersion = (value) => {
|
||||||
|
let text = String(value || '').trim();
|
||||||
|
if (!text) text = 'v1.0.0';
|
||||||
|
if (!/^v/i.test(text)) text = `v${text}`;
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bumpPatchVersion = (value) => {
|
||||||
|
const normalized = normalizeLuaVersion(value);
|
||||||
|
const matched = /^v?(\d+)\.(\d+)\.(\d+)$/i.exec(normalized);
|
||||||
|
if (!matched) return normalized;
|
||||||
|
return `v${matched[1]}.${matched[2]}.${Number(matched[3]) + 1}`;
|
||||||
|
};
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const formatTime = (ts) => {
|
const formatTime = (ts) => {
|
||||||
if (ts == null || ts === '') return '---';
|
if (ts == null || ts === '') return '---';
|
||||||
@@ -1344,10 +1362,18 @@ createApp({
|
|||||||
|
|
||||||
const saveConfig = async () => {
|
const saveConfig = async () => {
|
||||||
await runMutation(async () => {
|
await runMutation(async () => {
|
||||||
|
config.lua_versions.ticketmachine = normalizeLuaVersion(config.lua_versions.ticketmachine);
|
||||||
|
config.lua_versions.gate = normalizeLuaVersion(config.lua_versions.gate);
|
||||||
await requestJson('/api/config', { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(config) }, { expectOk: true });
|
await requestJson('/api/config', { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(config) }, { expectOk: true });
|
||||||
}, { successMessage: '保存成功' });
|
}, { successMessage: '保存成功' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bumpLuaVersion = async (device) => {
|
||||||
|
if (!config.lua_versions[device]) config.lua_versions[device] = 'v1.0.0';
|
||||||
|
config.lua_versions[device] = bumpPatchVersion(config.lua_versions[device]);
|
||||||
|
await saveConfig();
|
||||||
|
};
|
||||||
|
|
||||||
const exportData = () => {
|
const exportData = () => {
|
||||||
window.open('/api/export', '_blank');
|
window.open('/api/export', '_blank');
|
||||||
};
|
};
|
||||||
@@ -1390,6 +1416,7 @@ createApp({
|
|||||||
});
|
});
|
||||||
socket.on('config:updated', (data) => {
|
socket.on('config:updated', (data) => {
|
||||||
Object.assign(config, data);
|
Object.assign(config, data);
|
||||||
|
if (!config.lua_versions) config.lua_versions = { ticketmachine: 'v1.5.8', gate: 'v1.5.8' };
|
||||||
coreLoaded = true;
|
coreLoaded = true;
|
||||||
fareMapLoaded = false;
|
fareMapLoaded = false;
|
||||||
if (currentView.value === 'faremap') {
|
if (currentView.value === 'faremap') {
|
||||||
@@ -1579,7 +1606,7 @@ createApp({
|
|||||||
|
|
||||||
saveCurrentFare, deleteCurrentFare, closeFareModal,
|
saveCurrentFare, deleteCurrentFare, closeFareModal,
|
||||||
|
|
||||||
saveConfig, exportData, exportFareMap,
|
saveConfig, bumpLuaVersion, exportData, exportFareMap,
|
||||||
formatTime, formatLogDetail, getStationName, getStationInfo, loadFareMap, fetchData, refreshData: fetchData,
|
formatTime, formatLogDetail, getStationName, getStationInfo, loadFareMap, fetchData, refreshData: fetchData,
|
||||||
fareMapScale, fareMapLoading, fareMapError, zoomFareMapIn, zoomFareMapOut, zoomFareMapReset,
|
fareMapScale, fareMapLoading, fareMapError, zoomFareMapIn, zoomFareMapOut, zoomFareMapReset,
|
||||||
isTransferStation, getTransferTitleSuffix, getTransferLineBadges
|
isTransferStation, getTransferTitleSuffix, getTransferLineBadges
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@
|
|||||||
<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>FarSight-T.N.E铁路运输</strong>
|
||||||
<span>控制台登录</span>
|
<span>控制台登录</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
+126
-2
@@ -2590,6 +2590,39 @@ body.jr-public-page {
|
|||||||
font-size: 0.86rem;
|
font-size: 0.86rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jr-query-overview {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-query-stat {
|
||||||
|
padding: 18px 20px;
|
||||||
|
background: linear-gradient(180deg, #f7faf7 0, #ffffff 100%);
|
||||||
|
border: 1px solid #d7e0d3;
|
||||||
|
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-query-stat-label {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #6a786d;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-query-stat strong {
|
||||||
|
display: block;
|
||||||
|
color: #163024;
|
||||||
|
font-size: 1.08rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-query-stat p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: #647266;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
.jr-grid-two {
|
.jr-grid-two {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -2693,6 +2726,13 @@ body.jr-public-page {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jr-search-helper {
|
||||||
|
margin: 12px 0 0;
|
||||||
|
color: #66756a;
|
||||||
|
line-height: 1.7;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
.jr-search-input,
|
.jr-search-input,
|
||||||
body.jr-public-page .jr-search-input {
|
body.jr-public-page .jr-search-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -2752,16 +2792,22 @@ body.jr-public-page .jr-search-button:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.jr-ticket-row {
|
.jr-ticket-row {
|
||||||
padding: 18px 0;
|
padding: 18px 14px;
|
||||||
border-bottom: 1px solid #e4ece2;
|
border-bottom: 1px solid #e4ece2;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jr-ticket-row:hover {
|
.jr-ticket-row:hover {
|
||||||
background: #f7faf7;
|
background: #f7faf7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jr-ticket-row.is-active {
|
||||||
|
background: linear-gradient(180deg, #f4f8f4 0, #ffffff 100%);
|
||||||
|
border-left-color: #0b6b3a;
|
||||||
|
}
|
||||||
|
|
||||||
.jr-ticket-row:last-child {
|
.jr-ticket-row:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
@@ -2779,6 +2825,13 @@ body.jr-public-page .jr-search-button:hover {
|
|||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jr-list-meta {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #728077;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
.jr-ticket-id {
|
.jr-ticket-id {
|
||||||
color: #1b3022;
|
color: #1b3022;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -2920,6 +2973,12 @@ body.jr-public-page .jr-search-button:hover {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jr-detail-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.jr-popular-item {
|
.jr-popular-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -3173,6 +3232,35 @@ body.jr-public-page .jr-secondary-btn:hover {
|
|||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jr-guide-card {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-guide-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-guide-item {
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #f7faf7;
|
||||||
|
border: 1px solid #dfe8dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-guide-item strong {
|
||||||
|
display: block;
|
||||||
|
color: #173225;
|
||||||
|
font-size: 0.98rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-guide-item span {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #647266;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
body.jr-ticket-board-page,
|
body.jr-ticket-board-page,
|
||||||
body.jr-ticket-board-page #app,
|
body.jr-ticket-board-page #app,
|
||||||
body.jr-ticket-board-page .jr-public-shell {
|
body.jr-ticket-board-page .jr-public-shell {
|
||||||
@@ -3584,6 +3672,37 @@ body.jr-ticket-board-page .jr-board-card:last-child {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jr-page-intro h1 {
|
||||||
|
font-size: clamp(1.75rem, 7vw, 2.35rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-panel-headline {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-query-stat,
|
||||||
|
.jr-ticket-preview,
|
||||||
|
.jr-history-item,
|
||||||
|
.jr-popular-item,
|
||||||
|
.jr-guide-item {
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-ticket-row {
|
||||||
|
padding: 16px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-scroll-box {
|
||||||
|
min-height: 260px;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-center-empty {
|
||||||
|
min-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
.jr-order-info-grid {
|
.jr-order-info-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -3610,6 +3729,11 @@ body.jr-ticket-board-page .jr-board-card:last-child {
|
|||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jr-action-row .btn,
|
||||||
|
.jr-action-row button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.jr-home-alert {
|
.jr-home-alert {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<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>FarSight-T.N.E铁路运输</strong>
|
||||||
<span>电子客票信息</span>
|
<span>电子客票信息</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<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">
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
||||||
<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>FarSight-T.N.E铁路运输</strong>
|
||||||
<span>线上预定</span>
|
<span>线上预定</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<a href="https://ticket.fse-media.group" class="jr-brand" id="routeBrandLink">
|
<a href="https://ticket.fse-media.group" class="jr-brand" id="routeBrandLink">
|
||||||
<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>FarSight-T.N.E铁路运输</strong>
|
||||||
<span>线路规划后台</span>
|
<span>线路规划后台</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
+26
-8
@@ -1,4 +1,4 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
@@ -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=13" />
|
<link rel="stylesheet" href="/style.css?v=14" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="public-search jr-public-page">
|
<body class="public-search jr-public-page">
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
||||||
<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>FarSight-T.N.E铁路运输</strong>
|
||||||
<span>票务查询</span>
|
<span>票务查询</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -49,13 +49,31 @@
|
|||||||
<section class="jr-page-intro">
|
<section class="jr-page-intro">
|
||||||
<span class="jr-kicker">TICKET SEARCH</span>
|
<span class="jr-kicker">TICKET SEARCH</span>
|
||||||
<h1>按票号、站点或日期快速查询票据</h1>
|
<h1>按票号、站点或日期快速查询票据</h1>
|
||||||
<p>输入完整票号、起终点、日期或关键词,左侧浏览结果,右侧查看票据详情与最近流转记录。</p>
|
<p>输入完整票号、起终点、日期或关键词,左侧浏览结果,右侧查看票据详情与最近流转记录。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="jr-query-overview jr-grid-three" aria-label="车票查询摘要">
|
||||||
|
<article class="jr-query-stat">
|
||||||
|
<span class="jr-query-stat-label">检索方式</span>
|
||||||
|
<strong>票号 / 站点 / 日期</strong>
|
||||||
|
<p>支持完整票号与站点关键词联合查询,适合快速反查近期票据。</p>
|
||||||
|
</article>
|
||||||
|
<article class="jr-query-stat">
|
||||||
|
<span class="jr-query-stat-label">结果浏览</span>
|
||||||
|
<strong>列表与详情并排</strong>
|
||||||
|
<p>左侧先筛选票据,右侧立即查看电子票概览与最近流转记录。</p>
|
||||||
|
</article>
|
||||||
|
<article class="jr-query-stat">
|
||||||
|
<span class="jr-query-stat-label">移动端</span>
|
||||||
|
<strong>单列阅读更顺手</strong>
|
||||||
|
<p>手机端自动切为单列,查询、结果与详情会按操作顺序依次展开。</p>
|
||||||
|
</article>
|
||||||
</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">Ticket ID / Station / Date</span>
|
<span class="jr-panel-note">Ticket ID / Station / Date</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-search-form">
|
<div class="jr-search-form">
|
||||||
<input id="q" class="jr-search-input" type="text"
|
<input id="q" class="jr-search-input" type="text"
|
||||||
@@ -63,6 +81,7 @@
|
|||||||
<button id="searchBtn" class="btn primary jr-search-button"><i class="fas fa-search"></i>
|
<button id="searchBtn" class="btn primary jr-search-button"><i class="fas fa-search"></i>
|
||||||
立即搜索</button>
|
立即搜索</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="jr-search-helper">可直接输入完整票号,也可输入起点、终点或日期关键字进行模糊检索。</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="jr-search-results">
|
<section class="jr-search-results">
|
||||||
@@ -78,7 +97,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<section id="detail-section">
|
<section id="detail-section" class="jr-detail-stack">
|
||||||
<article class="jr-panel-card" style="margin-bottom:20px;">
|
<article class="jr-panel-card" style="margin-bottom:20px;">
|
||||||
<div class="jr-panel-headline">
|
<div class="jr-panel-headline">
|
||||||
<h3>车票详情</h3>
|
<h3>车票详情</h3>
|
||||||
@@ -118,7 +137,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/custom-dialog.js?v=12"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/ticket-search.js?v=11"></script>
|
<script src="/ticket-search.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>
|
||||||
@@ -146,4 +165,3 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
+34
-6
@@ -4,6 +4,10 @@
|
|||||||
const detailEl = $('#detail');
|
const detailEl = $('#detail');
|
||||||
const qEl = $('#q');
|
const qEl = $('#q');
|
||||||
const btn = $('#searchBtn');
|
const btn = $('#searchBtn');
|
||||||
|
const state = {
|
||||||
|
items: [],
|
||||||
|
selectedId: ''
|
||||||
|
};
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
searchTickets: async (q) => {
|
searchTickets: async (q) => {
|
||||||
@@ -52,6 +56,13 @@
|
|||||||
return type;
|
return type;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const escapeHtml = (value) => String(value == null ? '' : value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
const getTicketId = (obj) => (obj && (obj.ticket_id || obj["车票编号"] || obj.id)) || '';
|
const getTicketId = (obj) => (obj && (obj.ticket_id || obj["车票编号"] || obj.id)) || '';
|
||||||
|
|
||||||
const isValidStatus = (status) => {
|
const isValidStatus = (status) => {
|
||||||
@@ -111,6 +122,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
function renderList(items) {
|
function renderList(items) {
|
||||||
|
state.items = items;
|
||||||
listEl.innerHTML = '';
|
listEl.innerHTML = '';
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
listEl.innerHTML = '<div class="jr-center-empty"><p>未找到匹配结果。</p></div>';
|
listEl.innerHTML = '<div class="jr-center-empty"><p>未找到匹配结果。</p></div>';
|
||||||
@@ -119,22 +131,35 @@
|
|||||||
items.forEach(it => {
|
items.forEach(it => {
|
||||||
const id = it.ticket_id || it["车票编号"] || '';
|
const id = it.ticket_id || it["车票编号"] || '';
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'jr-ticket-row';
|
const statusText = formatStatusText(it.status || it["状态"] || '');
|
||||||
|
const isSelected = state.selectedId === id;
|
||||||
|
row.className = `jr-ticket-row${isSelected ? ' is-active' : ''}`;
|
||||||
|
|
||||||
const overview = it.overview || it["概览"] || null;
|
const overview = it.overview || it["概览"] || null;
|
||||||
const startName = overview ? (overview.start_name || overview["起点"]) : (it.start_name || it["起点"] || '---');
|
const startName = overview ? (overview.start_name || overview["起点"]) : (it.start_name || it["起点"] || '---');
|
||||||
const terminalName = overview ? (overview.terminal_name || overview["终点"]) : (it.terminal_name || it["终点"] || '---');
|
const terminalName = overview ? (overview.terminal_name || overview["终点"]) : (it.terminal_name || it["终点"] || '---');
|
||||||
|
const updateTime = formatTime(
|
||||||
|
(overview && (overview.last_update_ts || overview["上次更新时间"])) ||
|
||||||
|
it.last_update_ts ||
|
||||||
|
it["上次更新时间"] ||
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<div class="jr-ticket-row-head">
|
<div class="jr-ticket-row-head">
|
||||||
<span class="jr-ticket-id mono">${id}</span>
|
<span class="jr-ticket-id mono">${escapeHtml(id)}</span>
|
||||||
<i class="fas fa-chevron-right text-muted"></i>
|
<span class="jr-status-pill ${isValidStatus(statusText) ? 'jr-status-valid' : (statusText === '已使用' ? 'jr-status-used' : 'jr-status-expired')}">${escapeHtml(statusText)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-ticket-route">
|
<div class="jr-ticket-route">
|
||||||
${startName} → ${terminalName}
|
${escapeHtml(startName)} → ${escapeHtml(terminalName)}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="jr-list-meta">最近更新 ${escapeHtml(updateTime)}</div>
|
||||||
`;
|
`;
|
||||||
row.onclick = () => loadDetail(id);
|
row.onclick = () => {
|
||||||
|
state.selectedId = id;
|
||||||
|
renderList(state.items);
|
||||||
|
loadDetail(id);
|
||||||
|
};
|
||||||
listEl.appendChild(row);
|
listEl.appendChild(row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -208,6 +233,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadDetail(id) {
|
async function loadDetail(id) {
|
||||||
|
state.selectedId = id;
|
||||||
|
renderList(state.items);
|
||||||
detailEl.innerHTML = '<div class="jr-center-empty"><p>正在加载详情...</p></div>';
|
detailEl.innerHTML = '<div class="jr-center-empty"><p>正在加载详情...</p></div>';
|
||||||
try {
|
try {
|
||||||
const d = await api.ticketDetail(id);
|
const d = await api.ticketDetail(id);
|
||||||
@@ -229,6 +256,7 @@
|
|||||||
listEl.innerHTML = '<div class="jr-center-empty"><p>正在搜索...</p></div>';
|
listEl.innerHTML = '<div class="jr-center-empty"><p>正在搜索...</p></div>';
|
||||||
try {
|
try {
|
||||||
const d = await api.searchTickets(q);
|
const d = await api.searchTickets(q);
|
||||||
|
state.selectedId = state.selectedId && d.some((item) => getTicketId(item) === state.selectedId) ? state.selectedId : '';
|
||||||
renderList(d);
|
renderList(d);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
listEl.innerHTML = '<div class="jr-center-empty"><p>搜索失败。</p></div>';
|
listEl.innerHTML = '<div class="jr-center-empty"><p>搜索失败。</p></div>';
|
||||||
|
|||||||
Reference in New Issue
Block a user