feat(ticketmachine, gate): 新增单程票购票功能并优化自动更新逻辑
- 新增完整的单程票购票流程,包含线路票价数据构建、选择页面与订单适配逻辑 - 将版本检查间隔从60秒调整为5秒,新增版本不匹配时自动运行对应更新脚本并重启的逻辑 - 修正列车类型标签格式,将'single'统一为'Single' - 优化主页按钮布局,新增单程票购票入口 - 完善终端显示与订单成本计算逻辑,适配单程票场景 - 调整gate.lua的终端标题文字显示位置
This commit is contained in:
@@ -2,7 +2,7 @@ local DEFAULT_SERVER_BASE = "http://ticket.fse-media.group"
|
||||
local DEFAULT_SERVER_PATH = "/api/tickets/check"
|
||||
local GATE_OPEN_SECONDS = 2
|
||||
local VERSION = "v1.5.8"
|
||||
local VERSION_CHECK_INTERVAL = 60
|
||||
local VERSION_CHECK_INTERVAL = 5
|
||||
|
||||
local CONFIG_PATH = "gate_config.json"
|
||||
|
||||
@@ -158,7 +158,7 @@ end
|
||||
local function draw(statusLine1, statusLine2, statusColor)
|
||||
clear()
|
||||
local w, h = termDev.getSize()
|
||||
centerText(1, "GATE", colors.cyan)
|
||||
centerText(2, "GATE", colors.cyan)
|
||||
drawVersionIndicator(w)
|
||||
drawServerStatusIndicator(w)
|
||||
if statusLine1 and #statusLine1 > 0 then
|
||||
@@ -1352,6 +1352,21 @@ while true do
|
||||
pcall(function()
|
||||
refreshRemoteLuaVersion(guessBaseFromStatusURL(serverURL))
|
||||
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()
|
||||
versionTimer = os.startTimer(VERSION_CHECK_INTERVAL)
|
||||
end
|
||||
|
||||
+152
-20
@@ -251,7 +251,7 @@ local function normalizeTrainTypeLabel(v)
|
||||
return 'Express'
|
||||
end
|
||||
if s == 'single' then
|
||||
return 'single'
|
||||
return 'Single'
|
||||
end
|
||||
return 'Local'
|
||||
end
|
||||
@@ -657,6 +657,20 @@ local function backgroundSyncTask()
|
||||
while true do
|
||||
sleep(5)
|
||||
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
|
||||
|
||||
@@ -1075,6 +1089,7 @@ end
|
||||
local function stationDisplay(code)
|
||||
code = normalizeCode(code)
|
||||
if #code == 0 then return '' end
|
||||
if code == 'SINGLE_SEGMENT' then return 'Any 1 Segment' end
|
||||
local st = stationByCode[code]
|
||||
if st then return tostring(st.en_name or st.name or code) .. ' ' .. code end
|
||||
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)
|
||||
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()
|
||||
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
|
||||
drawHeader('FSE Ticket Machine', 'Select Mode', true)
|
||||
Buttons = {}
|
||||
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)
|
||||
if x1 < 2 or (x2 + btnW) > w-1 then
|
||||
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()
|
||||
stopAudio(); playAudioFile('Audio/welcome.wav'); waitAudioComplete()
|
||||
state.page = 'departure'
|
||||
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')
|
||||
stopAudio(); state.page = 'card_home'
|
||||
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()
|
||||
stopAudio(); state.page = 'online'
|
||||
end)
|
||||
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()
|
||||
stopAudio(); playAudioFile('Audio/welcome.wav'); waitAudioComplete()
|
||||
state.page = 'departure'
|
||||
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()
|
||||
stopAudio(); state.page = 'online'
|
||||
stopAudio(); playAudioFile('Audio/welcome.wav'); waitAudioComplete()
|
||||
state.page = 'single_segment'
|
||||
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')
|
||||
stopAudio(); state.page = 'card_home'
|
||||
end)
|
||||
addButton(x2, y+2, 'ONLINE', btnW, btnH, {colors.cyan, colors.white}, function()
|
||||
resetTicketFlow()
|
||||
stopAudio(); state.page = 'online'
|
||||
end)
|
||||
end
|
||||
waitButtons()
|
||||
end
|
||||
@@ -1692,8 +1816,8 @@ local function drawOrder()
|
||||
local cardSub = (state.cardMode == 'redeem') and 'Redeem IC card order' or 'Open new stored-value card'
|
||||
drawHeader('Confirm Card', cardSub)
|
||||
else
|
||||
local depLine = stationDisplay(state.departure)
|
||||
local terLine = stationDisplay(state.terminal)
|
||||
local depLine = (state.trainType == 'single') and state.departure or stationDisplay(state.departure)
|
||||
local terLine = (state.trainType == 'single') and 'Any 1 Segment' or stationDisplay(state.terminal)
|
||||
drawHeader('Confirm Order', 'From: ' .. depLine .. ' To: ' .. terLine)
|
||||
end
|
||||
local y = 5
|
||||
@@ -1726,8 +1850,10 @@ local function drawOrder()
|
||||
L('Type', state.trainType or 'N/A', colors.lightBlue)
|
||||
local rides = tonumber(state.trips) or 1
|
||||
if cost == nil then
|
||||
local unit = computeCost(state.departure or '', state.terminal or '', state.trainType or 'Local')
|
||||
cost = unit and (unit * rides) or nil
|
||||
if state.trainType ~= 'single' then
|
||||
local unit = computeCost(state.departure or '', state.terminal or '', state.trainType or 'Local')
|
||||
cost = unit and (unit * rides) or nil
|
||||
end
|
||||
end
|
||||
L('Trips', rides)
|
||||
L('Cost', cost, colors.red)
|
||||
@@ -1784,18 +1910,23 @@ local function showOrderAndAudio()
|
||||
state.cost = 0
|
||||
end
|
||||
elseif not state.voucher_code then
|
||||
local unit = computeCost(state.departure or '', state.terminal or '', state.trainType or 'Local')
|
||||
if not unit then
|
||||
showAlert('No route')
|
||||
state.page = 'terminal'
|
||||
return
|
||||
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')
|
||||
if not unit then
|
||||
showAlert('No route')
|
||||
state.page = 'terminal'
|
||||
return
|
||||
end
|
||||
state.cost = unit * (state.trips or 1)
|
||||
end
|
||||
state.cost = unit * (state.trips or 1)
|
||||
else
|
||||
if state.cost == nil then state.cost = 0 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')
|
||||
state.page = state.voucher_code and 'online' or 'terminal'
|
||||
return
|
||||
@@ -2324,6 +2455,7 @@ end
|
||||
local function mainPageLoop()
|
||||
while true do
|
||||
if state.page == 'home' then showHome()
|
||||
elseif state.page == 'single_segment' then showSingleSegmentSelection()
|
||||
elseif state.page == 'card_home' then showCardHome()
|
||||
elseif state.page == 'card_name' then showCardNameInput()
|
||||
elseif state.page == 'card_topup' then showCardTopup()
|
||||
|
||||
Reference in New Issue
Block a user