feat(ui): 新增购票兑换菜单,优化输入交互与界面布局

新增new_menu中间页面,拆分首页NEW按钮为购票和兑换码两个入口
抽离通用输入渲染函数,重构线上券码和IC卡兑换页面的代码逻辑
优化输入交互,按键、退格、清空时实时更新输入显示与提示
完善键盘快捷键支持,调整部分按钮布局与显示文本
统一音频播放逻辑,仅购票时播放欢迎音效
This commit is contained in:
2026-07-29 11:34:47 +08:00
parent c840e2abfe
commit 4db60ddc9e
+127 -35
View File
@@ -1440,8 +1440,8 @@ local function showHome()
local cx = math.floor((w - btnW) / 2) + 1
addButton(cx, y-4, 'NEW', btnW, btnH, {colors.green, colors.white}, function()
resetTicketFlow()
stopAudio(); playAudioFile('Audio/welcome.wav'); waitAudioComplete()
state.page = 'departure'
stopAudio()
state.page = 'new_menu'
end)
addButton(cx, y, 'SINGLE', btnW, btnH, {colors.blue, colors.white}, function()
resetTicketFlow()
@@ -1452,34 +1452,48 @@ local function showHome()
resetCardFlow('open')
stopAudio(); state.page = 'card_home'
end)
addButton(cx, y+8, 'ONLINE', btnW, btnH, {colors.cyan, colors.white}, function()
resetTicketFlow()
stopAudio(); state.page = 'online'
end)
else
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'
stopAudio()
state.page = 'new_menu'
end)
addButton(x2, y-2, 'SINGLE', btnW, btnH, {colors.blue, colors.white}, function()
resetTicketFlow()
stopAudio(); playAudioFile('Audio/welcome.wav'); waitAudioComplete()
state.page = 'single_segment'
end)
addButton(x1, y+2, 'CARD', btnW, btnH, {colors.orange, colors.white}, function()
addButton(cx, 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
local function showNewMenu()
while state.page == 'new_menu' do
drawHeader('Ticket Service', 'Buy ticket or redeem voucher')
Buttons = {}
local btnW, btnH = 16, 3
local cx = math.floor((w - btnW) / 2) + 1
local y = math.floor(h / 2) - 3
addButton(cx, y, 'BUY TICKET', btnW, btnH, {colors.green, colors.white}, function()
resetTicketFlow()
stopAudio(); playAudioFile('Audio/welcome.wav'); waitAudioComplete()
state.page = 'departure'
end)
addButton(cx, y + 4, 'REDEEM CODE', btnW, btnH, {colors.cyan, colors.white}, function()
resetTicketFlow()
stopAudio()
state.page = 'online'
end)
addButton(2, h-3, '<-back', 8, 3, {colors.black, colors.red}, function() state.page = 'home' end)
waitButtons()
end
end
local function showCardHome()
while state.page == 'card_home' do
drawHeader('IC Card Service', 'Open card or redeem online order')
@@ -1643,9 +1657,24 @@ local function showCardOnlineRedeem()
{'Z','X','C','V','B','N','M'}
}
local function clearLine(y)
termDev.setBackgroundColor(colors.black)
termDev.setCursorPos(1, y)
termDev.write(string.rep(' ', w))
end
local function renderInput()
local placeholder = code .. string.rep('_', math.max(0, 5-#code))
clearLine(4)
clearLine(5)
centerText(4, '[' .. placeholder .. ']', colors.yellow)
if msg and #msg > 0 then centerText(5, msg, msgCol) end
end
local function submitCode()
if #code ~= 5 then
msg, msgCol = 'Need 5 chars', colors.red
renderInput()
return
end
local res = fetchJSON(API_BASE .. '/public/ic-cards/orders/' .. code)
@@ -1654,17 +1683,20 @@ local function showCardOnlineRedeem()
end
if not (res and res.ok) then
msg, msgCol = 'Voucher Invalid', colors.red
renderInput()
return
end
local d = res.data or res
local cardStatus = tostring(d.status or ''):lower()
if d.consumed or (cardStatus ~= '' and cardStatus ~= 'pending_pickup' and isCardOrderLike(d)) then
msg, msgCol = 'Already Used!', colors.red
renderInput()
return
end
local cardOrder = buildCardOrderState(d, code)
if not cardOrder then
msg, msgCol = 'Not a card order', colors.red
renderInput()
return
end
resetCardFlow('redeem')
@@ -1682,10 +1714,7 @@ local function showCardOnlineRedeem()
end
while state.page == 'card_online' do
local placeholder = code .. string.rep('_', math.max(0, 5-#code))
drawHeader('Redeem IC Card', 'Type 5 chars then OK')
centerText(4, '[' .. placeholder .. ']', colors.yellow)
if msg and #msg > 0 then centerText(5, msg, msgCol) end
Buttons = {}
local keyW, keyH = (w < 44 and 2 or 3), 2
local kbW = 10 * (keyW + 1)
@@ -1694,15 +1723,29 @@ local function showCardOnlineRedeem()
local y, x = sY + (rIdx-1) * (keyH + 1), sX + (rIdx-1)
for _, ch in ipairs(row) do
addButton(x, y, ch, keyW, keyH, {colors.black, colors.white}, function()
if #code < 5 then code = code .. ch end
if #code < 5 then
code = code .. ch
msg = ''
renderInput()
end
end)
x = x + keyW + 1
end
end
addButton(2, h-3, '<-back', 8, 3, {colors.black, colors.red}, function() state.page = 'card_home' end)
addButton(w-31, h-3, 'BKSP', 8, 3, {colors.black, colors.red}, function() code = code:sub(1, -2) end)
addButton(w-21, h-3, 'CLEAR', 8, 3, {colors.black, colors.red}, function() code = '' end)
addButton(w-31, h-3, 'BKSP', 8, 3, {colors.black, colors.red}, function()
code = code:sub(1, -2)
msg = ''
renderInput()
end)
addButton(w-21, h-3, 'CLEAR', 8, 3, {colors.black, colors.red}, function()
code = ''
msg = ''
renderInput()
end)
addButton(w-11, h-3, 'OK', 10, 3, {colors.black, colors.green}, submitCode)
renderInput()
while state.page == 'card_online' do
local ev, p1, p2, p3 = os.pullEvent()
if ev == 'mouse_click' or ev == 'monitor_touch' then
for _, b in ipairs(Buttons) do
@@ -1710,10 +1753,16 @@ local function showCardOnlineRedeem()
end
elseif ev == 'char' and #code < 5 then
code = code .. tostring(p1 or ''):upper()
msg = ''
renderInput()
elseif ev == 'key' and p1 == keys.backspace then
code = code:sub(1, -2)
msg = ''
renderInput()
elseif ev == 'key' and (p1 == keys.enter or p1 == keys.numPadEnter) then
submitCode()
elseif ev == 'config_updated' then
end
end
end
end
@@ -2531,6 +2580,20 @@ local function showOnlineVoucher()
{'Z','X','C','V','B','N','M'}
}
local function clearLine(y)
termDev.setBackgroundColor(colors.black)
termDev.setCursorPos(1, y)
termDev.write(string.rep(' ', w))
end
local function renderInput()
local placeholder = code .. string.rep('_', math.max(0, 5-#code))
clearLine(4)
clearLine(5)
centerText(4, '[' .. placeholder .. ']', colors.yellow)
if msg and #msg > 0 then centerText(5, msg, msgCol) end
end
local function submitCode()
if #code == 5 then
local res = fetchJSON(API_BASE .. '/public/orders/' .. code)
@@ -2540,6 +2603,7 @@ local function showOnlineVoucher()
-- Check if already consumed
if d.consumed then
msg, msgCol = 'Already Used!', colors.red
renderInput()
return
end
@@ -2553,15 +2617,16 @@ local function showOnlineVoucher()
state.page = 'order'
else
msg, msgCol = 'Voucher Invalid', colors.red
renderInput()
end
else
msg, msgCol = 'Need 5 chars', colors.red
renderInput()
end
end
while state.page == 'online' do
local placeholder = code .. string.rep('_', math.max(0, 5-#code))
drawHeader('Enter Voucher', 'Type 5 chars then OK')
centerText(4, '[' .. placeholder .. ']', colors.yellow)
if msg and #msg > 0 then centerText(5, msg, msgCol) end
Buttons = {}
local keyW, keyH = (w < 44 and 2 or 3), 2
@@ -2571,7 +2636,13 @@ local function showOnlineVoucher()
for rIdx, row in ipairs(rows) do
local y, x = sY + (rIdx-1) * (keyH + 1), sX + (rIdx-1)
for _, ch in ipairs(row) do
addButton(x, y, ch, keyW, keyH, {colors.black, colors.white}, function() if #code < 5 then code = code .. ch end end)
addButton(x, y, ch, keyW, keyH, {colors.black, colors.white}, function()
if #code < 5 then
code = code .. ch
msg = ''
renderInput()
end
end)
x = x + keyW + 1
end
end
@@ -2581,30 +2652,50 @@ local function showOnlineVoucher()
local gap = 2
local total = bw + gap + cw + gap + ow
local ax = math.floor((w - total) / 2) + 1
local smallMode = false
if ax < 1 then
smallMode = true
bw, cw, ow = 6, 6, 6
total = bw + 1 + cw + 1 + ow
gap = 1
total = bw + gap + cw + gap + ow
ax = math.floor((w - total) / 2) + 1
addButton(math.max(1, ax), actY, 'BKSP', bw, 3, {colors.black, colors.red}, function() code = code:sub(1, -2) end)
addButton(math.max(1, ax) + bw + 1, actY, 'CLEAR', cw, 3, {colors.black, colors.red}, function() code = '' end)
addButton(math.max(1, ax) + bw + 1 + cw + 1, actY, 'OK', ow, 3, {colors.black, colors.green}, submitCode)
else
addButton(ax, actY, 'Backspace', bw, 3, {colors.black, colors.red}, function() code = code:sub(1, -2) end)
addButton(ax + bw + gap, actY, 'Clear', cw, 3, {colors.black, colors.red}, function() code = '' end)
addButton(ax + bw + gap + cw + gap, actY, 'OK', ow, 3, {colors.black, colors.green}, submitCode)
end
local bx = math.max(1, ax)
addButton(bx, actY, smallMode and 'BKSP' or 'Backspace', bw, 3, {colors.black, colors.red}, function()
code = code:sub(1, -2)
msg = ''
renderInput()
end)
addButton(bx + bw + gap, actY, smallMode and 'CLEAR' or 'Clear', cw, 3, {colors.black, colors.red}, function()
code = ''
msg = ''
renderInput()
end)
addButton(bx + bw + gap + cw + gap, actY, 'OK', ow, 3, {colors.black, colors.green}, submitCode)
addButton(2, h-3, '<-back', 8, 3, {colors.black, colors.red}, function() state.page = 'home' end)
renderInput()
while state.page == 'online' do
local ev, p1, p2, p3 = os.pullEvent()
if ev == 'mouse_click' or ev == 'monitor_touch' then
for _, b in ipairs(Buttons) do if inRect(b, p2, p3) then clickSound(); if b.onClick then b.onClick() end; break end end
elseif ev == 'char' and #code < 5 then code = code .. p1:upper()
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()
for _, b in ipairs(Buttons) do
if inRect(b, p2, p3) then clickSound(); if b.onClick then b.onClick() end; break end
end
elseif ev == 'char' and #code < 5 then
code = code .. tostring(p1 or ''):upper()
msg = ''
renderInput()
elseif ev == 'key' and p1 == keys.backspace then
code = code:sub(1, -2)
msg = ''
renderInput()
elseif ev == 'key' and (p1 == keys.enter or p1 == keys.numPadEnter) then
submitCode()
elseif ev == 'config_updated' then
end
end
end
end
-- ###########################
-- Main entry point
@@ -2612,6 +2703,7 @@ end
local function mainPageLoop()
while true do
if state.page == 'home' then showHome()
elseif state.page == 'new_menu' then showNewMenu()
elseif state.page == 'single_segment' then showSingleSegmentSelection()
elseif state.page == 'card_home' then showCardHome()
elseif state.page == 'card_name' then showCardNameInput()