初始提交

This commit is contained in:
2026-06-21 10:00:13 +08:00
commit 7a5dc32672
1441 changed files with 266348 additions and 0 deletions
+275
View File
@@ -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();
})();