feat(ticket): 新增单程票购票与检票功能,优化日志与接口
- 前端新增单程票购票界面,支持票种切换与区间选择、价格预览 - 完善后端检票接口,添加单程票的校验与处理逻辑 - 优化售票机脚本的车票类型映射,新增单程车票支持 - 简化前端售票日志输出,移除冗余站点信息字段 - 新增服务层付费区间计算字段,优化公共接口返回数据
This commit is contained in:
+23
-9
@@ -1336,18 +1336,23 @@ router.post('/tickets/check', async (req, res) => {
|
|||||||
toFiniteNumberOrUndef(hintTripsRemaining);
|
toFiniteNumberOrUndef(hintTripsRemaining);
|
||||||
const tripsRemainEffective = (tripsRemain0 == null) ? tripsTotal : tripsRemain0;
|
const tripsRemainEffective = (tripsRemain0 == null) ? tripsTotal : tripsRemain0;
|
||||||
|
|
||||||
|
const isSingleTicket = String(cur.train_type || '').toLowerCase() === 'single';
|
||||||
|
|
||||||
if (action === 'entry') {
|
if (action === 'entry') {
|
||||||
if (!matchesStation(start)) return deny('wrong_station');
|
if (!isSingleTicket && !matchesStation(start)) return deny('wrong_station');
|
||||||
if (entered && !exited) return deny('already_entered');
|
if (entered && !exited) return deny('already_entered');
|
||||||
|
|
||||||
const ev = { ts, type: 'check', ticket_id, action, result: 'pass', station_code: start, station_name: resolveStationName(start), device };
|
const entryCode = isSingleTicket ? (station_code_raw || start) : start;
|
||||||
|
|
||||||
|
const ev = { ts, type: 'check', ticket_id, action, result: 'pass', station_code: entryCode, station_name: resolveStationName(entryCode), device };
|
||||||
await DataService.appendTicketEvent(ev);
|
await DataService.appendTicketEvent(ev);
|
||||||
await DataService.upsertTicketIndex({
|
await DataService.upsertTicketIndex({
|
||||||
ticket_id,
|
ticket_id,
|
||||||
entered: true,
|
entered: true,
|
||||||
exited: false,
|
exited: false,
|
||||||
last_action: 'entry',
|
last_action: 'entry',
|
||||||
last_station_code: start,
|
last_station_code: entryCode,
|
||||||
|
start: isSingleTicket ? entryCode : cur.start,
|
||||||
last_result: 'pass',
|
last_result: 'pass',
|
||||||
last_reason: '',
|
last_reason: '',
|
||||||
last_event: 'check',
|
last_event: 'check',
|
||||||
@@ -1357,22 +1362,31 @@ router.post('/tickets/check', async (req, res) => {
|
|||||||
});
|
});
|
||||||
io.emit('ticket:check', ev);
|
io.emit('ticket:check', ev);
|
||||||
appendReqLog(req, { category: 'device', type: 'ticket_check', detail: ev });
|
appendReqLog(req, { category: 'device', type: 'ticket_check', detail: ev });
|
||||||
return res.json({ ok: true, ticket_id, action, result: 'pass', station_code: start, trips_remaining: tripsRemainEffective, destroy_ticket: false });
|
return res.json({ ok: true, ticket_id, action, result: 'pass', station_code: entryCode, trips_remaining: tripsRemainEffective, destroy_ticket: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitCode = station_code_raw || terminal;
|
||||||
|
if (isSingleTicket) {
|
||||||
|
const fareInfo = LogicService.computeFareBoth(cur.start, exitCode);
|
||||||
|
if (!fareInfo || fareInfo.paid_segments !== 1) {
|
||||||
|
return deny('非单区间');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!matchesStation(terminal)) return deny('wrong_station');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!matchesStation(terminal)) return deny('wrong_station');
|
|
||||||
if (!entered) return deny('not_entered');
|
if (!entered) return deny('not_entered');
|
||||||
if (exited) return deny('already_exited');
|
if (exited) return deny('already_exited');
|
||||||
|
|
||||||
const newTripsRemaining = Math.max(0, (Number(tripsRemainEffective) || 0) - 1);
|
const newTripsRemaining = Math.max(0, (Number(tripsRemainEffective) || 0) - 1);
|
||||||
const shouldSetUsed = newTripsRemaining <= 0;
|
const shouldSetUsed = isSingleTicket ? true : (newTripsRemaining <= 0);
|
||||||
const ev = { ts, type: 'check', ticket_id, action, result: 'pass', station_code: terminal, station_name: resolveStationName(terminal), device, trips_remaining: newTripsRemaining };
|
const ev = { ts, type: 'check', ticket_id, action, result: 'pass', station_code: exitCode, station_name: resolveStationName(exitCode), device, trips_remaining: newTripsRemaining };
|
||||||
await DataService.appendTicketEvent(ev);
|
await DataService.appendTicketEvent(ev);
|
||||||
await DataService.upsertTicketIndex({
|
await DataService.upsertTicketIndex({
|
||||||
ticket_id,
|
ticket_id,
|
||||||
exited: true,
|
exited: true,
|
||||||
last_action: 'exit',
|
last_action: 'exit',
|
||||||
last_station_code: terminal,
|
last_station_code: exitCode,
|
||||||
last_result: 'pass',
|
last_result: 'pass',
|
||||||
last_reason: '',
|
last_reason: '',
|
||||||
last_event: 'check',
|
last_event: 'check',
|
||||||
@@ -1383,7 +1397,7 @@ router.post('/tickets/check', async (req, res) => {
|
|||||||
});
|
});
|
||||||
io.emit('ticket:check', ev);
|
io.emit('ticket:check', ev);
|
||||||
appendReqLog(req, { category: 'device', type: 'ticket_check', detail: ev });
|
appendReqLog(req, { category: 'device', type: 'ticket_check', detail: ev });
|
||||||
return res.json({ ok: true, ticket_id, action, result: 'pass', station_code: terminal, trips_remaining: newTripsRemaining, destroy_ticket: shouldSetUsed });
|
return res.json({ ok: true, ticket_id, action, result: 'pass', station_code: exitCode, trips_remaining: newTripsRemaining, destroy_ticket: shouldSetUsed });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/tickets/status', async (req, res) => {
|
router.post('/tickets/status', async (req, res) => {
|
||||||
|
|||||||
@@ -714,13 +714,19 @@ router.get('/tickets/:id', async (req, res, next) => {
|
|||||||
action: e.action,
|
action: e.action,
|
||||||
ts: e.ts,
|
ts: e.ts,
|
||||||
ticket_id: e.ticket_id,
|
ticket_id: e.ticket_id,
|
||||||
station_name: nameFor(e.station_code),
|
station_name: e.station_name || nameFor(e.station_code),
|
||||||
station_code: e.station_code || '',
|
station_code: e.station_code || '',
|
||||||
station_en: e.station_code ? enNameFor(e.station_code) : (e.station_en || ''),
|
station_en: e.station_code ? enNameFor(e.station_code) : (e.station_en || ''),
|
||||||
trips_remaining: e.trips_remaining ?? e.rides_remaining ?? null
|
trips_remaining: e.trips_remaining ?? e.rides_remaining ?? null
|
||||||
};
|
};
|
||||||
|
} else if (e.type === 'check') {
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
station_name: e.station_name || nameFor(e.station_code),
|
||||||
|
station_en: e.station_code ? enNameFor(e.station_code) : (e.station_en || '')
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return { raw: e };
|
return e;
|
||||||
});
|
});
|
||||||
|
|
||||||
appendReqLog(req, { category: 'public', type: 'ticket_detail', detail: { ticket_id: id, ok: true } });
|
appendReqLog(req, { category: 'public', type: 'ticket_detail', detail: { ticket_id: id, ok: true } });
|
||||||
|
|||||||
@@ -231,7 +231,8 @@ const LogicService = {
|
|||||||
regular_path: [src],
|
regular_path: [src],
|
||||||
express_path: [src],
|
express_path: [src],
|
||||||
regular_transfers: [],
|
regular_transfers: [],
|
||||||
express_transfers: []
|
express_transfers: [],
|
||||||
|
paid_segments: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,6 +246,7 @@ const LogicService = {
|
|||||||
const totals = accumulateFareAlongPath(graph, result.path);
|
const totals = accumulateFareAlongPath(graph, result.path);
|
||||||
if (!totals) return null;
|
if (!totals) return null;
|
||||||
const transfers = computeTransfersAlongPath(graph, result.path);
|
const transfers = computeTransfersAlongPath(graph, result.path);
|
||||||
|
const dstCost = result.dist.get(dst);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
regular: Math.round(Number(totals.regular ?? 0) || 0),
|
regular: Math.round(Number(totals.regular ?? 0) || 0),
|
||||||
@@ -253,6 +255,7 @@ const LogicService = {
|
|||||||
express_path: result.path,
|
express_path: result.path,
|
||||||
regular_transfers: transfers,
|
regular_transfers: transfers,
|
||||||
express_transfers: transfers,
|
express_transfers: transfers,
|
||||||
|
paid_segments: dstCost ? dstCost.paidSegments : Infinity
|
||||||
};
|
};
|
||||||
} catch (_) { return null; }
|
} catch (_) { return null; }
|
||||||
},
|
},
|
||||||
|
|||||||
+10
-2
@@ -250,6 +250,9 @@ local function normalizeTrainTypeLabel(v)
|
|||||||
if s == 'express' or s == 'limited_express' or s == 'limited express' then
|
if s == 'express' or s == 'limited_express' or s == 'limited express' then
|
||||||
return 'Express'
|
return 'Express'
|
||||||
end
|
end
|
||||||
|
if s == 'single' then
|
||||||
|
return 'single'
|
||||||
|
end
|
||||||
return 'Local'
|
return 'Local'
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -2090,10 +2093,13 @@ local function showDone()
|
|||||||
local toNameEnArg = tostring(toNameEn or '???')
|
local toNameEnArg = tostring(toNameEn or '???')
|
||||||
local fromNameCnUArg = tostring(fromNameCnU or '')
|
local fromNameCnUArg = tostring(fromNameCnU or '')
|
||||||
local toNameCnUArg = tostring(toNameCnU or '')
|
local toNameCnUArg = tostring(toNameCnU or '')
|
||||||
|
local issueArgsType = 'local'
|
||||||
|
if state.trainType == 'Express' then issueArgsType = 'limited_express'
|
||||||
|
elseif state.trainType == 'single' then issueArgsType = 'single' end
|
||||||
local issueArgs = {
|
local issueArgs = {
|
||||||
start_name_en = fromNameEnArg,
|
start_name_en = fromNameEnArg,
|
||||||
terminal_name_en = toNameEnArg,
|
terminal_name_en = toNameEnArg,
|
||||||
type = (state.trainType == 'Express') and 'limited_express' or 'local',
|
type = issueArgsType,
|
||||||
rides = rides,
|
rides = rides,
|
||||||
cost = cost,
|
cost = cost,
|
||||||
start_station = startStationArg,
|
start_station = startStationArg,
|
||||||
@@ -2113,7 +2119,9 @@ local function showDone()
|
|||||||
ts = (os.epoch and os.epoch('utc')) or (os.time() * 1000)
|
ts = (os.epoch and os.epoch('utc')) or (os.time() * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
local apiType = (state.trainType == 'Express') and 'limited_express' or 'local'
|
local apiType = 'local'
|
||||||
|
if state.trainType == 'Express' then apiType = 'limited_express'
|
||||||
|
elseif state.trainType == 'single' then apiType = 'single' end
|
||||||
local localGeneratedTicketId = generateTicketId()
|
local localGeneratedTicketId = generateTicketId()
|
||||||
local okIssueTicket, issuedTicketId, issueMethod = issueTicketFromPeripheral(
|
local okIssueTicket, issuedTicketId, issueMethod = issueTicketFromPeripheral(
|
||||||
fromNameEnArg,
|
fromNameEnArg,
|
||||||
|
|||||||
+1
-3
@@ -353,9 +353,7 @@ createApp({
|
|||||||
|
|
||||||
if (type === 'sale' || type === '售票') {
|
if (type === 'sale' || type === '售票') {
|
||||||
const cost = ev?.cost ?? ev?.price ?? ev?.amount ?? ev?.["票价"] ?? ev?.["售票额"] ?? '0';
|
const cost = ev?.cost ?? ev?.price ?? ev?.amount ?? ev?.["票价"] ?? ev?.["售票额"] ?? '0';
|
||||||
const code = ev?.station_code || ev?.["站点编号"] || '---';
|
return `机器ID: ${deviceId};票价: ${cost};时间: ${timeStr}`;
|
||||||
const name = ev?.station_name || ev?.["售票站"] || ev?.["发生站"] || (typeof getStationName === 'function' && code !== '---' ? getStationName(code) : code);
|
|
||||||
return `机器ID: ${deviceId};站点编号: ${code};站点中文: ${name};票价: ${cost};时间: ${timeStr}`;
|
|
||||||
}
|
}
|
||||||
if (type === 'entry' || action === 'entry') {
|
if (type === 'entry' || action === 'entry') {
|
||||||
const code = ev?.station_code || ev?.["站点编号"] || '---';
|
const code = ev?.station_code || ev?.["站点编号"] || '---';
|
||||||
|
|||||||
+64
-23
@@ -47,32 +47,73 @@
|
|||||||
<section class="jr-content-grid">
|
<section class="jr-content-grid">
|
||||||
<div class="jr-column-main">
|
<div class="jr-column-main">
|
||||||
<article class="jr-section-card" id="reservationPanel">
|
<article class="jr-section-card" id="reservationPanel">
|
||||||
<div class="jr-section-head">
|
<div class="jr-form-block" style="margin-bottom: 24px;">
|
||||||
<div>
|
<div class="jr-field-head">
|
||||||
<p class="jr-section-label">Reservation</p>
|
<span class="jr-field-label">票种</span>
|
||||||
<h2>选择乘车区间</h2>
|
<span class="jr-field-note">请选择购买的车票类型</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="jr-section-note">点击下方线路图中的站点,依次选择起点与终点。</p>
|
<div class="train-type-group jr-train-type-group">
|
||||||
</div>
|
<label>
|
||||||
<div id="stationMap" class="station-map-container jr-station-map">
|
<input type="radio" name="ticketClass" value="normal" checked>
|
||||||
<div class="loading">加载线路图中...</div>
|
<div class="type-card jr-type-card">
|
||||||
</div>
|
<div class="type-title">普通票</div>
|
||||||
<div class="jr-selection-summary">
|
<div class="type-desc">按起终点购票</div>
|
||||||
<div class="jr-selection-card jr-selection-start">
|
</div>
|
||||||
<span class="jr-selection-tag">出发站</span>
|
</label>
|
||||||
<strong id="fromDisplay">请在上方地图选择</strong>
|
<label>
|
||||||
<input id="from" type="hidden" />
|
<input type="radio" name="ticketClass" value="single">
|
||||||
</div>
|
<div class="type-card jr-type-card">
|
||||||
<div class="jr-selection-arrow">
|
<div class="type-title">单程票</div>
|
||||||
<i class="fas fa-arrow-right"></i>
|
<div class="type-desc">限乘一个区间</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-selection-card jr-selection-end">
|
</label>
|
||||||
<span class="jr-selection-tag">到达站</span>
|
|
||||||
<strong id="toDisplay">请在上方地图选择</strong>
|
|
||||||
<input id="to" type="hidden" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-form-grid">
|
|
||||||
|
<div id="normalTicketView">
|
||||||
|
<div class="jr-section-head">
|
||||||
|
<div>
|
||||||
|
<p class="jr-section-label">Reservation</p>
|
||||||
|
<h2>选择乘车区间</h2>
|
||||||
|
</div>
|
||||||
|
<p class="jr-section-note">点击下方线路图中的站点,依次选择起点与终点。</p>
|
||||||
|
</div>
|
||||||
|
<div id="stationMap" class="station-map-container jr-station-map">
|
||||||
|
<div class="loading">加载线路图中...</div>
|
||||||
|
</div>
|
||||||
|
<div class="jr-selection-summary">
|
||||||
|
<div class="jr-selection-card jr-selection-start">
|
||||||
|
<span class="jr-selection-tag">出发站</span>
|
||||||
|
<strong id="fromDisplay">请在上方地图选择</strong>
|
||||||
|
<input id="from" type="hidden" />
|
||||||
|
</div>
|
||||||
|
<div class="jr-selection-arrow">
|
||||||
|
<i class="fas fa-arrow-right"></i>
|
||||||
|
</div>
|
||||||
|
<div class="jr-selection-card jr-selection-end">
|
||||||
|
<span class="jr-selection-tag">到达站</span>
|
||||||
|
<strong id="toDisplay">请在上方地图选择</strong>
|
||||||
|
<input id="to" type="hidden" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="singleTicketView" style="display: none;">
|
||||||
|
<div class="jr-section-head">
|
||||||
|
<div>
|
||||||
|
<p class="jr-section-label">Single Trip</p>
|
||||||
|
<h2>选择单程区间</h2>
|
||||||
|
</div>
|
||||||
|
<p class="jr-section-note">根据线路和票价分类,购买后可乘坐该线路任意单一区间。</p>
|
||||||
|
</div>
|
||||||
|
<div id="singleSegmentList" class="list jr-price-list" style="max-height: 400px; overflow-y: auto;">
|
||||||
|
<div class="loading">加载区间数据...</div>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="singleSegmentSelection" value="" />
|
||||||
|
<input type="hidden" id="singleSegmentPrice" value="" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="jr-form-grid" style="margin-top: 24px;">
|
||||||
<div class="jr-form-block">
|
<div class="jr-form-block">
|
||||||
<div class="jr-field-head">
|
<div class="jr-field-head">
|
||||||
<span class="jr-field-label">车型</span>
|
<span class="jr-field-label">车型</span>
|
||||||
|
|||||||
+134
-8
@@ -529,10 +529,45 @@
|
|||||||
|
|
||||||
async function previewPrice(force = false){
|
async function previewPrice(force = false){
|
||||||
const seq = ++previewSeq;
|
const seq = ++previewSeq;
|
||||||
const from = (fromEl.value||'').trim();
|
const ticketClass = document.querySelector('input[name="ticketClass"]:checked').value;
|
||||||
const to = (toEl.value||'').trim();
|
|
||||||
const type = document.querySelector('input[name="trainType"]:checked').value;
|
const type = document.querySelector('input[name="trainType"]:checked').value;
|
||||||
const trips = normalizeTrips();
|
const trips = normalizeTrips();
|
||||||
|
|
||||||
|
if (ticketClass === 'single') {
|
||||||
|
const lineName = document.getElementById('singleSegmentSelection').value;
|
||||||
|
const priceStr = document.getElementById('singleSegmentPrice').value;
|
||||||
|
if (!lineName || !priceStr) {
|
||||||
|
priceBox.innerHTML = '<div class="empty-state jr-empty-state"><p>请选择单程区间</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cost = Number(priceStr) * trips;
|
||||||
|
priceBox.innerHTML = `
|
||||||
|
<div class="list-item jr-result-row">
|
||||||
|
<span class="k">线路</span>
|
||||||
|
<span class="v" style="font-weight:700; color:#0ea5e9;">${lineName}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item jr-result-row">
|
||||||
|
<span class="k">区间</span>
|
||||||
|
<span class="v">任意单区间</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item jr-result-row">
|
||||||
|
<span class="k">车型</span>
|
||||||
|
<span class="v">${type}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item jr-result-row">
|
||||||
|
<span class="k">单价</span>
|
||||||
|
<span class="v">$${priceStr}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item jr-result-row">
|
||||||
|
<span class="k">总价</span>
|
||||||
|
<span class="v" style="font-size:18px; font-weight:800; color:#0ea5e9;">$${cost}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const from = (fromEl.value||'').trim();
|
||||||
|
const to = (toEl.value||'').trim();
|
||||||
if(!from || !to){ priceBox.innerHTML = '<div class="empty-state jr-empty-state"><p>请选择起点与终点</p></div>'; return; }
|
if(!from || !to){ priceBox.innerHTML = '<div class="empty-state jr-empty-state"><p>请选择起点与终点</p></div>'; return; }
|
||||||
const previewKey = `${from}|${to}|${type}|${trips}`;
|
const previewKey = `${from}|${to}|${type}|${trips}`;
|
||||||
if (!force && previewKey === lastPreviewKey) return;
|
if (!force && previewKey === lastPreviewKey) return;
|
||||||
@@ -608,18 +643,31 @@
|
|||||||
|
|
||||||
// Create Order with auto-preview
|
// Create Order with auto-preview
|
||||||
async function createOrder(){
|
async function createOrder(){
|
||||||
const from = (fromEl.value||'').trim();
|
const ticketClass = document.querySelector('input[name="ticketClass"]:checked').value;
|
||||||
const to = (toEl.value||'').trim();
|
|
||||||
const type = document.querySelector('input[name="trainType"]:checked').value;
|
const type = document.querySelector('input[name="trainType"]:checked').value;
|
||||||
const trips = normalizeTrips();
|
const trips = normalizeTrips();
|
||||||
const ride_date = new Date().toISOString().slice(0, 10);
|
const ride_date = new Date().toISOString().slice(0, 10);
|
||||||
if(!from || !to){ alert('请完整填写信息'); return; }
|
|
||||||
|
|
||||||
// Auto-fetch price before creating
|
let payload = null;
|
||||||
await previewPrice(true);
|
if (ticketClass === 'single') {
|
||||||
|
const lineName = document.getElementById('singleSegmentSelection').value;
|
||||||
|
const priceStr = document.getElementById('singleSegmentPrice').value;
|
||||||
|
if (!lineName || !priceStr) {
|
||||||
|
alert('请选择单程区间');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload = { start: lineName, terminal: 'SINGLE_SEGMENT', train_type: 'single', price: Number(priceStr), trips, ride_date };
|
||||||
|
} else {
|
||||||
|
const from = (fromEl.value||'').trim();
|
||||||
|
const to = (toEl.value||'').trim();
|
||||||
|
if(!from || !to){ alert('请完整填写信息'); return; }
|
||||||
|
|
||||||
|
// Auto-fetch price before creating
|
||||||
|
await previewPrice(true);
|
||||||
|
payload = { start: from, terminal: to, train_type: type, trips, ride_date };
|
||||||
|
}
|
||||||
|
|
||||||
try{
|
try{
|
||||||
const payload = { start: from, terminal: to, train_type: type, trips, ride_date };
|
|
||||||
const res = await fetch('/api/public/orders', { method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify(payload) });
|
const res = await fetch('/api/public/orders', { method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify(payload) });
|
||||||
const r = await res.json();
|
const r = await res.json();
|
||||||
if(r && r.ok){
|
if(r && r.ok){
|
||||||
@@ -656,8 +704,86 @@
|
|||||||
document.querySelectorAll('input[name="trainType"]').forEach(el => {
|
document.querySelectorAll('input[name="trainType"]').forEach(el => {
|
||||||
el.onchange = () => { if(fromEl.value && toEl.value) previewPrice(); };
|
el.onchange = () => { if(fromEl.value && toEl.value) previewPrice(); };
|
||||||
});
|
});
|
||||||
|
document.querySelectorAll('input[name="ticketClass"]').forEach(r => r.addEventListener('change', toggleTicketClass));
|
||||||
if (tripsEl) {
|
if (tripsEl) {
|
||||||
tripsEl.oninput = () => { if(fromEl.value && toEl.value) previewPrice(); };
|
tripsEl.oninput = () => { if(fromEl.value && toEl.value) previewPrice(); };
|
||||||
tripsEl.onchange = () => { normalizeTrips(); if(fromEl.value && toEl.value) previewPrice(); };
|
tripsEl.onchange = () => { normalizeTrips(); if(fromEl.value && toEl.value) previewPrice(); };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleTicketClass() {
|
||||||
|
const isSingle = document.querySelector('input[name="ticketClass"]:checked').value === 'single';
|
||||||
|
if (isSingle) {
|
||||||
|
document.getElementById('normalTicketView').style.display = 'none';
|
||||||
|
document.getElementById('singleTicketView').style.display = 'block';
|
||||||
|
renderSingleSegments();
|
||||||
|
} else {
|
||||||
|
document.getElementById('normalTicketView').style.display = 'block';
|
||||||
|
document.getElementById('singleTicketView').style.display = 'none';
|
||||||
|
}
|
||||||
|
previewPrice(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderSingleSegments() {
|
||||||
|
const listEl = document.getElementById('singleSegmentList');
|
||||||
|
if (listEl.innerHTML.includes('jr-result-row')) return; // Already rendered
|
||||||
|
listEl.innerHTML = '<div class="loading">加载区间数据...</div>';
|
||||||
|
try {
|
||||||
|
const [lines, fares] = await Promise.all([
|
||||||
|
fetch('/api/public/lines').then(r=>r.json()),
|
||||||
|
fetch('/api/public/fares').then(r=>r.json())
|
||||||
|
]);
|
||||||
|
|
||||||
|
const type = document.querySelector('input[name="trainType"]:checked').value;
|
||||||
|
const segments = [];
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
const stops = Array.isArray(line.stops) ? line.stops : (Array.isArray(line.站点列表) ? line.站点列表 : []);
|
||||||
|
const lineFares = new Set();
|
||||||
|
for (let i = 0; i < stops.length - 1; i++) {
|
||||||
|
const a = stops[i], b = stops[i+1];
|
||||||
|
const f = fares.find(x => (x.from === a && x.to === b) || (x.from === b && x.to === a));
|
||||||
|
if (f) {
|
||||||
|
const cost = type === 'Express' ? (f.cost_express ?? f.cost ?? 0) : (f.cost_regular ?? f.cost ?? 0);
|
||||||
|
lineFares.add(Number(cost));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lineFares.forEach(cost => {
|
||||||
|
segments.push({
|
||||||
|
lineName: line.name || line.线路名称 || '未知线路',
|
||||||
|
color: line.color || line.颜色 || '#93a2b7',
|
||||||
|
cost
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (segments.length === 0) {
|
||||||
|
listEl.innerHTML = '<div class="empty-state jr-empty-state"><p>暂无单程区间数据</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
segments.forEach((seg, idx) => {
|
||||||
|
html += `
|
||||||
|
<label class="list-item jr-result-row" style="cursor: pointer; display: flex; align-items: center; gap: 12px; padding: 12px;">
|
||||||
|
<input type="radio" name="singleSegmentOption" value="${idx}" onchange="selectSingleSegment('${seg.lineName}', ${seg.cost})" style="margin: 0;">
|
||||||
|
<div style="width: 12px; height: 12px; border-radius: 50%; background-color: ${seg.color};"></div>
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<div style="font-weight: bold; color: #1e293b;">${seg.lineName}</div>
|
||||||
|
<div style="font-size: 12px; color: #64748b;">单区间票</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-weight: bold; color: #0ea5e9;">$${seg.cost}</div>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
listEl.innerHTML = html;
|
||||||
|
} catch (e) {
|
||||||
|
listEl.innerHTML = '<div class="error">加载区间数据失败</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.selectSingleSegment = (lineName, cost) => {
|
||||||
|
document.getElementById('singleSegmentSelection').value = lineName;
|
||||||
|
document.getElementById('singleSegmentPrice').value = cost;
|
||||||
|
previewPrice(true);
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
+1
-3
@@ -233,9 +233,7 @@ createApp({
|
|||||||
|
|
||||||
if (type === 'sale' || type === '售票') {
|
if (type === 'sale' || type === '售票') {
|
||||||
const cost = ev?.cost ?? ev?.price ?? ev?.amount ?? ev?.["票价"] ?? ev?.["售票额"] ?? '0';
|
const cost = ev?.cost ?? ev?.price ?? ev?.amount ?? ev?.["票价"] ?? ev?.["售票额"] ?? '0';
|
||||||
const code = ev?.station_code || ev?.["站点编号"] || '---';
|
return `机器ID: ${deviceId};票价: ${cost};时间: ${timeStr}`;
|
||||||
const name = ev?.station_name || ev?.["售票站"] || ev?.["发生站"] || (typeof getStationName === 'function' && code !== '---' ? getStationName(code) : code);
|
|
||||||
return `机器ID: ${deviceId};站点编号: ${code};站点中文: ${name};票价: ${cost};时间: ${timeStr}`;
|
|
||||||
}
|
}
|
||||||
if (type === 'entry' || action === 'entry') {
|
if (type === 'entry' || action === 'entry') {
|
||||||
const code = ev?.station_code || ev?.["站点编号"] || '---';
|
const code = ev?.station_code || ev?.["站点编号"] || '---';
|
||||||
|
|||||||
@@ -115,9 +115,7 @@
|
|||||||
|
|
||||||
if (type === 'sale' || type === '售票') {
|
if (type === 'sale' || type === '售票') {
|
||||||
const cost = ev.cost ?? ev.price ?? ev.amount ?? ev["票价"] ?? ev["售票额"] ?? '0';
|
const cost = ev.cost ?? ev.price ?? ev.amount ?? ev["票价"] ?? ev["售票额"] ?? '0';
|
||||||
const code = ev.station_code || ev["站点编号"] || '---';
|
return `机器ID: ${deviceId};票价: ${cost};时间: ${timeStr}`;
|
||||||
const name = ev.station_name || ev["售票站"] || ev["发生站"] || (typeof getStationName === 'function' && code !== '---' ? getStationName(code) : code);
|
|
||||||
return `机器ID: ${deviceId};站点编号: ${code};站点中文: ${name};票价: ${cost};时间: ${timeStr}`;
|
|
||||||
}
|
}
|
||||||
if (type === 'entry' || action === 'entry') {
|
if (type === 'entry' || action === 'entry') {
|
||||||
const code = ev.station_code || ev["站点编号"] || '---';
|
const code = ev.station_code || ev["站点编号"] || '---';
|
||||||
|
|||||||
Reference in New Issue
Block a user