feat(ticketmachine, gate): 新增单程票购票功能并优化自动更新逻辑

- 新增完整的单程票购票流程,包含线路票价数据构建、选择页面与订单适配逻辑
- 将版本检查间隔从60秒调整为5秒,新增版本不匹配时自动运行对应更新脚本并重启的逻辑
- 修正列车类型标签格式,将'single'统一为'Single'
- 优化主页按钮布局,新增单程票购票入口
- 完善终端显示与订单成本计算逻辑,适配单程票场景
- 调整gate.lua的终端标题文字显示位置
This commit is contained in:
2026-07-10 08:16:54 +08:00
parent 52ac98bead
commit c80ebba3ec
2 changed files with 169 additions and 22 deletions
+17 -2
View File
@@ -2,7 +2,7 @@ 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.8" local VERSION = "v1.5.8"
local VERSION_CHECK_INTERVAL = 60 local VERSION_CHECK_INTERVAL = 5
local CONFIG_PATH = "gate_config.json" local CONFIG_PATH = "gate_config.json"
@@ -158,7 +158,7 @@ end
local function draw(statusLine1, statusLine2, statusColor) local function draw(statusLine1, statusLine2, statusColor)
clear() clear()
local w, h = termDev.getSize() local w, h = termDev.getSize()
centerText(1, "GATE", colors.cyan) centerText(2, "GATE", colors.cyan)
drawVersionIndicator(w) drawVersionIndicator(w)
drawServerStatusIndicator(w) drawServerStatusIndicator(w)
if statusLine1 and #statusLine1 > 0 then if statusLine1 and #statusLine1 > 0 then
@@ -1352,6 +1352,21 @@ while true do
pcall(function() pcall(function()
refreshRemoteLuaVersion(guessBaseFromStatusURL(serverURL)) refreshRemoteLuaVersion(guessBaseFromStatusURL(serverURL))
end) end)
if versionMismatch == true then
pcall(function()
clear()
local w, h = termDev.getSize()
centerText(math.floor(h / 2), "Auto Updating...", colors.yellow)
end)
pcall(function()
if shell and type(shell.run) == "function" then
shell.run("update_gate.lua")
else
os.run(getfenv and getfenv() or _ENV, "update_gate.lua")
end
end)
os.reboot()
end
drawReadyScreen() drawReadyScreen()
versionTimer = os.startTimer(VERSION_CHECK_INTERVAL) versionTimer = os.startTimer(VERSION_CHECK_INTERVAL)
end end
+144 -12
View File
@@ -251,7 +251,7 @@ local function normalizeTrainTypeLabel(v)
return 'Express' return 'Express'
end end
if s == 'single' then if s == 'single' then
return 'single' return 'Single'
end end
return 'Local' return 'Local'
end end
@@ -657,6 +657,20 @@ local function backgroundSyncTask()
while true do while true do
sleep(5) sleep(5)
pcall(refreshConfigOnce) pcall(refreshConfigOnce)
if versionMismatch == true then
pcall(function()
clear()
centerText(math.floor(h/2), "Auto Updating...", colors.yellow)
end)
pcall(function()
if shell and type(shell.run) == "function" then
shell.run("update_machine.lua")
else
os.run(getfenv and getfenv() or _ENV, "update_machine.lua")
end
end)
os.reboot()
end
end end
end end
@@ -1075,6 +1089,7 @@ end
local function stationDisplay(code) local function stationDisplay(code)
code = normalizeCode(code) code = normalizeCode(code)
if #code == 0 then return '' end if #code == 0 then return '' end
if code == 'SINGLE_SEGMENT' then return 'Any 1 Segment' end
local st = stationByCode[code] local st = stationByCode[code]
if st then return tostring(st.en_name or st.name or code) .. ' ' .. code end if st then return tostring(st.en_name or st.name or code) .. ' ' .. code end
return code return code
@@ -1153,43 +1168,152 @@ local function addCancelButton()
addButton(2, h, 'CANCEL', 8, 1, {colors.red, colors.white}, function() ui_cancel_request = true end) addButton(2, h, 'CANCEL', 8, 1, {colors.red, colors.white}, function() ui_cancel_request = true end)
end end
local singleSegments = nil
local singleSegmentScroll = 0
local function buildSingleSegments()
local segments = {}
local fares = CFG.fares or {}
for _, line in ipairs(CFG.lines or {}) do
local stops = line.stops or line['站点列表'] or {}
local lineFares = {}
for i = 1, #stops - 1 do
local a, b = stops[i], stops[i+1]
local f = nil
for _, fx in ipairs(fares) do
if (fx.from == a and fx.to == b) or (fx.from == b and fx.to == a) then
f = fx
break
end
end
if f then
local cost = tonumber(f.cost_regular or f.cost) or 0
lineFares[cost] = true
end
end
for cost, _ in pairs(lineFares) do
table.insert(segments, {
lineName = line.name or line['线路名称'] or 'Unknown',
color = line.color or line['颜色'],
cost = cost
})
end
end
return segments
end
local function showSingleSegmentSelection()
if not singleSegments then
singleSegments = buildSingleSegments()
end
while state.page == 'single_segment' do
drawHeader('Select Single Segment', 'Select a line and fare')
Buttons = {}
local itemsPerPage = math.floor((h - 10) / 4) * 2
if itemsPerPage < 2 then itemsPerPage = 2 end
local maxScroll = math.ceil(#singleSegments / itemsPerPage) - 1
if maxScroll < 0 then maxScroll = 0 end
if singleSegmentScroll > maxScroll then singleSegmentScroll = maxScroll end
if singleSegmentScroll < 0 then singleSegmentScroll = 0 end
local startIdx = singleSegmentScroll * itemsPerPage + 1
local endIdx = math.min(startIdx + itemsPerPage - 1, #singleSegments)
local bw = math.floor((w - 6) / 2)
local bx1, bx2 = 2, w - bw - 1
local by = 5
for i = startIdx, endIdx do
local seg = singleSegments[i]
local colIdx = (i - startIdx) % 2
local rowIdx = math.floor((i - startIdx) / 2)
local x = colIdx == 0 and bx1 or bx2
local y = by + rowIdx * 4
local label = seg.lineName .. ' $' .. tostring(seg.cost)
if #label > bw then label = label:sub(1, bw) end
local isSelected = (state.singleSegment == seg)
addButton(x, y, label, bw, 3, {isSelected and colors.green or colors.blue, colors.white}, function()
state.singleSegment = seg
end)
end
if singleSegmentScroll > 0 then
addButton(w-8, 2, 'UP', 6, 1, {colors.gray, colors.white}, function() singleSegmentScroll = singleSegmentScroll - 1 end)
end
if singleSegmentScroll < maxScroll then
addButton(w-8, h-5, 'DN', 6, 1, {colors.gray, colors.white}, function() singleSegmentScroll = singleSegmentScroll + 1 end)
end
addCancelButton()
addButton(2, h-3, '<-back', 8, 3, {colors.black, colors.red}, function() state.page = 'home' end)
if state.singleSegment then
addButton(w-9, h-3, 'NEXT->', 8, 3, {colors.black, colors.green}, function()
state.departure = state.singleSegment.lineName
state.terminal = 'SINGLE_SEGMENT'
state.trainType = 'single'
state.trips = 1
state.cost = state.singleSegment.cost
state.page = 'order'
end)
end
waitButtons()
if ui_cancel_request then
renderConfirmCancel()
waitButtons()
if ui_cancel_confirmed then stopAudio(); state.page = 'home'; ui_cancel_confirmed = false end
ui_cancel_request = false
end
end
end
local function showHome() local function showHome()
state.stationName = (CFG.current_station and (CFG.current_station.en_name or CFG.current_station.name)) or 'Station' state.stationName = (CFG.current_station and (CFG.current_station.en_name or CFG.current_station.name)) or 'Station'
state.stationCode = (CFG.current_station and CFG.current_station.code) or CURRENT_STATION_CODE state.stationCode = (CFG.current_station and CFG.current_station.code) or CURRENT_STATION_CODE
drawHeader('FSE Ticket Machine', 'Select Mode', true) drawHeader('FSE Ticket Machine', 'Select Mode', true)
Buttons = {} Buttons = {}
local btnW, btnH = 12, 3 local btnW, btnH = 12, 3
local y = math.floor(h/2) - 2 local y = math.floor(h/2) - 3
local x1, x2, x3 = math.floor(w/2) - btnW - 2, math.floor(w/2) + 3, math.floor(w/2) - math.floor(btnW / 2) local x1, x2, x3 = math.floor(w/2) - btnW - 2, math.floor(w/2) + 3, math.floor(w/2) - math.floor(btnW / 2)
if x1 < 2 or (x2 + btnW) > w-1 then if x1 < 2 or (x2 + btnW) > w-1 then
local cx = math.floor((w - btnW) / 2) + 1 local cx = math.floor((w - btnW) / 2) + 1
addButton(cx, y-2, 'NEW', btnW, btnH, {colors.green, colors.white}, function() addButton(cx, y-4, 'NEW', btnW, btnH, {colors.green, colors.white}, function()
resetTicketFlow() resetTicketFlow()
stopAudio(); playAudioFile('Audio/welcome.wav'); waitAudioComplete() stopAudio(); playAudioFile('Audio/welcome.wav'); waitAudioComplete()
state.page = 'departure' state.page = 'departure'
end) end)
addButton(cx, y+2, 'CARD', btnW, btnH, {colors.orange, colors.white}, function() addButton(cx, y, 'SINGLE', btnW, btnH, {colors.blue, colors.white}, function()
resetTicketFlow()
stopAudio(); playAudioFile('Audio/welcome.wav'); waitAudioComplete()
state.page = 'single_segment'
end)
addButton(cx, y+4, 'CARD', btnW, btnH, {colors.orange, colors.white}, function()
resetCardFlow('open') resetCardFlow('open')
stopAudio(); state.page = 'card_home' stopAudio(); state.page = 'card_home'
end) end)
addButton(cx, y+6, 'ONLINE', btnW, btnH, {colors.cyan, colors.white}, function() addButton(cx, y+8, 'ONLINE', btnW, btnH, {colors.cyan, colors.white}, function()
resetTicketFlow() resetTicketFlow()
stopAudio(); state.page = 'online' stopAudio(); state.page = 'online'
end) end)
else else
addButton(x1, y, 'NEW', btnW, btnH, {colors.green, colors.white}, function() local cx = math.floor((w - btnW) / 2) + 1
addButton(x1, y-2, 'NEW', btnW, btnH, {colors.green, colors.white}, function()
resetTicketFlow() resetTicketFlow()
stopAudio(); playAudioFile('Audio/welcome.wav'); waitAudioComplete() stopAudio(); playAudioFile('Audio/welcome.wav'); waitAudioComplete()
state.page = 'departure' state.page = 'departure'
end) end)
addButton(x2, y, 'ONLINE', btnW, btnH, {colors.cyan, colors.white}, function() addButton(x2, y-2, 'SINGLE', btnW, btnH, {colors.blue, colors.white}, function()
resetTicketFlow() resetTicketFlow()
stopAudio(); state.page = 'online' stopAudio(); playAudioFile('Audio/welcome.wav'); waitAudioComplete()
state.page = 'single_segment'
end) end)
addButton(x3, y+4, 'CARD', btnW, btnH, {colors.orange, colors.white}, function() addButton(x1, y+2, 'CARD', btnW, btnH, {colors.orange, colors.white}, function()
resetCardFlow('open') resetCardFlow('open')
stopAudio(); state.page = 'card_home' stopAudio(); state.page = 'card_home'
end) end)
addButton(x2, y+2, 'ONLINE', btnW, btnH, {colors.cyan, colors.white}, function()
resetTicketFlow()
stopAudio(); state.page = 'online'
end)
end end
waitButtons() waitButtons()
end end
@@ -1692,8 +1816,8 @@ local function drawOrder()
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'
drawHeader('Confirm Card', cardSub) drawHeader('Confirm Card', cardSub)
else else
local depLine = stationDisplay(state.departure) local depLine = (state.trainType == 'single') and state.departure or stationDisplay(state.departure)
local terLine = stationDisplay(state.terminal) local terLine = (state.trainType == 'single') and 'Any 1 Segment' or stationDisplay(state.terminal)
drawHeader('Confirm Order', 'From: ' .. depLine .. ' To: ' .. terLine) drawHeader('Confirm Order', 'From: ' .. depLine .. ' To: ' .. terLine)
end end
local y = 5 local y = 5
@@ -1726,9 +1850,11 @@ local function drawOrder()
L('Type', state.trainType or 'N/A', colors.lightBlue) L('Type', state.trainType or 'N/A', colors.lightBlue)
local rides = tonumber(state.trips) or 1 local rides = tonumber(state.trips) or 1
if cost == nil then if cost == nil then
if state.trainType ~= 'single' then
local unit = computeCost(state.departure or '', state.terminal or '', state.trainType or 'Local') local unit = computeCost(state.departure or '', state.terminal or '', state.trainType or 'Local')
cost = unit and (unit * rides) or nil cost = unit and (unit * rides) or nil
end end
end
L('Trips', rides) L('Trips', rides)
L('Cost', cost, colors.red) L('Cost', cost, colors.red)
if state.voucher_code then if state.voucher_code then
@@ -1784,6 +1910,10 @@ local function showOrderAndAudio()
state.cost = 0 state.cost = 0
end end
elseif not state.voucher_code then elseif not state.voucher_code then
if state.trainType == 'single' then
-- Cost is already set in showSingleSegmentSelection
if state.cost == nil then state.cost = 0 end
else
local unit = computeCost(state.departure or '', state.terminal or '', state.trainType or 'Local') local unit = computeCost(state.departure or '', state.terminal or '', state.trainType or 'Local')
if not unit then if not unit then
showAlert('No route') showAlert('No route')
@@ -1791,11 +1921,12 @@ local function showOrderAndAudio()
return return
end end
state.cost = unit * (state.trips or 1) state.cost = unit * (state.trips or 1)
end
else else
if state.cost == nil then state.cost = 0 end if state.cost == nil then state.cost = 0 end
end end
if state.productMode ~= 'card' and sameLogicalStation(state.departure, state.terminal) then if state.productMode ~= 'card' and state.trainType ~= 'single' and sameLogicalStation(state.departure, state.terminal) then
showAlert('Same station') showAlert('Same station')
state.page = state.voucher_code and 'online' or 'terminal' state.page = state.voucher_code and 'online' or 'terminal'
return return
@@ -2324,6 +2455,7 @@ end
local function mainPageLoop() local function mainPageLoop()
while true do while true do
if state.page == 'home' then showHome() if state.page == 'home' then showHome()
elseif state.page == 'single_segment' then showSingleSegmentSelection()
elseif state.page == 'card_home' then showCardHome() elseif state.page == 'card_home' then showCardHome()
elseif state.page == 'card_name' then showCardNameInput() elseif state.page == 'card_name' then showCardNameInput()
elseif state.page == 'card_topup' then showCardTopup() elseif state.page == 'card_topup' then showCardTopup()