初始提交
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
(() => {
|
||||
const $ = (sel) => document.querySelector(sel);
|
||||
const listEl = $('#list');
|
||||
const detailEl = $('#detail');
|
||||
const qEl = $('#q');
|
||||
const btn = $('#searchBtn');
|
||||
|
||||
const api = {
|
||||
searchTickets: async (q) => {
|
||||
const r = await fetch(`/api/public/tickets?q=${encodeURIComponent(q || '')}`);
|
||||
return r.json();
|
||||
},
|
||||
ticketDetail: async (id) => {
|
||||
const r = await fetch(`/api/public/tickets/${encodeURIComponent(id)}`);
|
||||
return r.json();
|
||||
},
|
||||
popular: async () => {
|
||||
const r = await fetch('/api/public/popular');
|
||||
return r.json();
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (value) => {
|
||||
if (value == null || value === '') return '---';
|
||||
|
||||
let ts = Number(value);
|
||||
if (Number.isFinite(ts)) {
|
||||
if (ts > 0 && ts < 1000000000000) ts *= 1000;
|
||||
const date = new Date(ts);
|
||||
if (!Number.isNaN(date.getTime())) {
|
||||
return date.toLocaleString('zh-CN', {
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date(value);
|
||||
if (!Number.isNaN(date.getTime())) {
|
||||
return date.toLocaleString('zh-CN', {
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const formatTrainType = (type) => {
|
||||
if (!type) return '普通';
|
||||
const t = type.toLowerCase();
|
||||
if (t === 'local') return '普通';
|
||||
if (t === 'ltd.exp' || t === 'express') return '特急';
|
||||
return type;
|
||||
};
|
||||
|
||||
const getTicketId = (obj) => (obj && (obj.ticket_id || obj["车票编号"] || obj.id)) || '';
|
||||
|
||||
const isValidStatus = (status) => {
|
||||
const s = String(status || '').toLowerCase();
|
||||
return s === '有效' || s === 'valid' || s === 'unused' || s === 'active' || s.includes('有效') || s.includes('未使用');
|
||||
};
|
||||
|
||||
const formatStatusText = (status) => {
|
||||
const s = String(status || '').toLowerCase();
|
||||
if (s === '有效' || s === 'valid' || s === 'unused' || s === 'active' || s.includes('有效') || s.includes('未使用')) return '有效';
|
||||
if (s === '已使用' || s === 'used') return '已使用';
|
||||
if (!s) return '未知';
|
||||
if (s === 'expired') return '失效';
|
||||
if (s === 'refunded') return '已退票';
|
||||
return String(status);
|
||||
};
|
||||
|
||||
const getEventType = (event) => String(event.type || event["类型"] || '').toLowerCase();
|
||||
|
||||
const formatEventTitle = (event) => {
|
||||
const type = getEventType(event);
|
||||
const action = String(event.action || event["动作"] || '').toLowerCase();
|
||||
|
||||
if (type === 'sale' || type === '售票') return '售票成功';
|
||||
if (type === 'entry' || action === 'entry') return '进站成功';
|
||||
if (type === 'exit' || action === 'exit') return '出站成功';
|
||||
if (type === 'status' || type === '状态') return '状态变更';
|
||||
|
||||
return event.type || event["类型"] || '状态更新';
|
||||
};
|
||||
|
||||
const formatEventLocation = (event) => {
|
||||
const type = getEventType(event);
|
||||
const stationName = event.station_name || event["售票站"] || event["发生站"] || '';
|
||||
const stationCode = event.station_code || event["站点编号"] || '';
|
||||
|
||||
if (type === 'sale' || type === '售票') {
|
||||
return stationName || '线上售票';
|
||||
}
|
||||
|
||||
if (!stationName && !stationCode) return '---';
|
||||
return [stationName, stationName && stationCode ? stationCode : ''].filter(Boolean).join(' ');
|
||||
};
|
||||
|
||||
const formatEventExtra = (event) => {
|
||||
const type = getEventType(event);
|
||||
if (type === 'sale' || type === '售票') {
|
||||
const amount = event.amount ?? event["售票额"];
|
||||
if (amount != null && amount !== '') return `票价:¥ ${amount}`;
|
||||
}
|
||||
|
||||
const stationEn = event.station_en || event["站点英文"] || '';
|
||||
const deviceId = event["设备编号"] || event.device_id || '';
|
||||
if (stationEn && deviceId) return `${stationEn} (${deviceId})`;
|
||||
if (deviceId) return `设备:${deviceId}`;
|
||||
return stationEn;
|
||||
};
|
||||
|
||||
function renderList(items) {
|
||||
listEl.innerHTML = '';
|
||||
if (items.length === 0) {
|
||||
listEl.innerHTML = '<div class="jr-center-empty"><p>未找到匹配结果。</p></div>';
|
||||
return;
|
||||
}
|
||||
items.forEach(it => {
|
||||
const id = it.ticket_id || it["车票编号"] || '';
|
||||
const row = document.createElement('div');
|
||||
row.className = 'jr-ticket-row';
|
||||
|
||||
const overview = it.overview || it["概览"] || null;
|
||||
const startName = overview ? (overview.start_name || overview["起点"]) : (it.start_name || it["起点"] || '---');
|
||||
const terminalName = overview ? (overview.terminal_name || overview["终点"]) : (it.terminal_name || it["终点"] || '---');
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="jr-ticket-row-head">
|
||||
<span class="jr-ticket-id mono">${id}</span>
|
||||
<i class="fas fa-chevron-right text-muted"></i>
|
||||
</div>
|
||||
<div class="jr-ticket-route">
|
||||
${startName} → ${terminalName}
|
||||
</div>
|
||||
`;
|
||||
row.onclick = () => loadDetail(id);
|
||||
listEl.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function openDetail(id) {
|
||||
if (window.location.hostname.includes('fse-media.group')) {
|
||||
window.open(`https://ticket.fse-media.group/detail/${id}`, '_blank');
|
||||
} else {
|
||||
window.open(`/${id}`, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
function renderDetail(d) {
|
||||
const ov = d.overview || d["概览"] || {};
|
||||
const evs = d.events || d["事件"] || [];
|
||||
const id = getTicketId(d) || getTicketId(ov);
|
||||
const stRaw = ov.status || ov["状态"] || d.status || d["状态"] || '';
|
||||
const statusText = formatStatusText(stRaw);
|
||||
const statusClass = isValidStatus(stRaw) ? 'jr-status-valid' : (statusText === '已使用' ? 'jr-status-used' : 'jr-status-expired');
|
||||
|
||||
detailEl.innerHTML = `
|
||||
<div class="jr-ticket-preview" onclick="openTicketDetail('${id}')" style="cursor:pointer;" title="点击查看电子票看板">
|
||||
<div class="jr-ticket-row-head">
|
||||
<span class="jr-ticket-id mono">${id} <i class="fas fa-external-link-alt" style="font-size:0.8em; margin-left:4px"></i></span>
|
||||
<span class="jr-status-pill ${statusClass}">${statusText}</span>
|
||||
</div>
|
||||
<div class="jr-route-board">
|
||||
<div class="jr-station-block">
|
||||
<div class="jr-station-line">
|
||||
<span class="jr-station-title">${ov.start_name || ov["起点"] || '---'}</span>
|
||||
<span class="jr-station-code">${ov.start_code || ov["起点编号"] || ''}</span>
|
||||
</div>
|
||||
<div class="jr-station-en">${ov.start_en || ov["起点英文"] || ''}</div>
|
||||
</div>
|
||||
<div class="jr-route-track"><i class="fas fa-train"></i></div>
|
||||
<div class="jr-station-block is-end">
|
||||
<div class="jr-station-line">
|
||||
<span class="jr-station-title">${ov.terminal_name || ov["终点"] || '---'}</span>
|
||||
<span class="jr-station-code">${ov.terminal_code || ov["终点编号"] || ''}</span>
|
||||
</div>
|
||||
<div class="jr-station-en">${ov.terminal_en || ov["终点英文"] || ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="jr-meta-grid">
|
||||
<div class="jr-meta-item"><span>车型</span><strong>${formatTrainType(ov.train_type || ov["车型"])}</strong></div>
|
||||
<div class="jr-meta-item"><span>乘次</span><strong>${ov.trips_total ?? ov["总乘次"] ?? 0}</strong></div>
|
||||
<div class="jr-meta-item"><span>票价</span><strong>${ov.amount ?? ov["金额"] ?? 0}</strong></div>
|
||||
<div class="jr-meta-item"><span>更新</span><strong>${formatTime(ov.last_update_ts ?? ov["上次更新时间"])}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="jr-panel-headline" style="margin:20px 0 14px;">
|
||||
<h3>流转记录</h3>
|
||||
<span class="jr-panel-note">Recent Events</span>
|
||||
</div>
|
||||
<div class="jr-history-list">
|
||||
${evs.map(e => `
|
||||
<div class="jr-history-item">
|
||||
<div class="jr-history-row">
|
||||
<span class="jr-history-title">${formatEventTitle(e)}</span>
|
||||
<span class="jr-history-time">${formatTime(e.ts || e["时间戳"])}</span>
|
||||
</div>
|
||||
<div class="jr-history-desc">
|
||||
<div>${formatEventLocation(e)}</div>
|
||||
<div style="margin-top:4px;">${formatEventExtra(e) || '---'}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function loadDetail(id) {
|
||||
detailEl.innerHTML = '<div class="jr-center-empty"><p>正在加载详情...</p></div>';
|
||||
try {
|
||||
const d = await api.ticketDetail(id);
|
||||
if (d && getTicketId(d) && (d.overview || d["概览"])) {
|
||||
renderDetail(d);
|
||||
} else {
|
||||
detailEl.innerHTML = '<div class="jr-center-empty"><p>未找到车票详情。</p></div>';
|
||||
}
|
||||
// Update URL without reload
|
||||
const newUrl = window.location.origin + window.location.pathname + '?id=' + encodeURIComponent(id);
|
||||
window.history.pushState({ path: newUrl }, '', newUrl);
|
||||
} catch (e) {
|
||||
detailEl.innerHTML = '<div class="jr-center-empty"><p>加载详情失败。</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function doSearch() {
|
||||
const q = (qEl.value || '').trim();
|
||||
listEl.innerHTML = '<div class="jr-center-empty"><p>正在搜索...</p></div>';
|
||||
try {
|
||||
const d = await api.searchTickets(q);
|
||||
renderList(d);
|
||||
} catch (e) {
|
||||
listEl.innerHTML = '<div class="jr-center-empty"><p>搜索失败。</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPopular() {
|
||||
try {
|
||||
const d = await api.popular();
|
||||
const ps = $('#popularStations');
|
||||
const pr = $('#popularRoutes');
|
||||
|
||||
ps.innerHTML = d.topStations.map(s => `
|
||||
<div class="jr-popular-item">
|
||||
<span><strong>${s.name}</strong></span>
|
||||
<span>${s.count} 次</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
pr.innerHTML = d.topRoutes.map(r => `
|
||||
<div class="jr-popular-item">
|
||||
<span><strong>${r.from} → ${r.to}</strong></span>
|
||||
<span>${r.count} 次</span>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (_) { }
|
||||
}
|
||||
|
||||
btn.onclick = doSearch;
|
||||
qEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') doSearch(); });
|
||||
window.openTicketDetail = openDetail;
|
||||
|
||||
const sp = new URLSearchParams(location.search);
|
||||
const qid = sp.get('id');
|
||||
if (qid) {
|
||||
qEl.value = qid;
|
||||
loadDetail(qid);
|
||||
doSearch();
|
||||
} else {
|
||||
doSearch();
|
||||
}
|
||||
loadPopular();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user