feat(web, server): 更新品牌文案,新增IC卡批量查询并重构搜索页面

统一替换全站所有HTML页面的品牌标题为FarSight-T.N.E铁路运输,调整部分页面的中文显示文案,例如删除ticket-board.html中的冗余说明文字。格式化重构blog.html的代码结构与缩进,修复末尾无换行的问题。
后端完善/ic-cards/query接口:支持空查询返回全部IC卡列表,按创建时间倒序排序,添加卡片状态和类型的标准化标签,优化请求日志记录。
全面重构IC卡搜索页面的前端逻辑,新增批量查看所有IC卡功能,支持点击卡片查看详情与操作历史,优化状态管理与渲染流程。
This commit is contained in:
2026-06-28 11:02:32 +08:00
parent 7fe1acd9d7
commit 042720d812
12 changed files with 219 additions and 100 deletions
+10 -2
View File
@@ -345,8 +345,16 @@ router.post('/orders/:code/consume', async (req, res) => {
router.get('/ic-cards/query', async (req, res) => { router.get('/ic-cards/query', async (req, res) => {
const q = String(req.query.q || '').trim(); const q = String(req.query.q || '').trim();
if (!q) { if (!q) {
appendReqLog(req, { category: 'public', type: 'ic_card_query_invalid', level: 'warn', detail: { q } }); const cards = (DataService.getIcCards() || [])
return res.status(400).json({ ok: false, error: 'query required' }); .slice()
.sort((a, b) => Number(b?.created_ts || 0) - Number(a?.created_ts || 0))
.map((card) => ({
...presentIcCard(card),
status_label: mapIcCardStatus(card.status),
card_type_label: mapIcCardType(card.card_type)
}));
appendReqLog(req, { category: 'public', type: 'ic_card_query_all', detail: { total: cards.length } });
return res.json({ ok: true, cards });
} }
const normCardId = normalizeIcCardId(q); const normCardId = normalizeIcCardId(q);
const normOrderCode = String(q || '').trim().toUpperCase(); const normOrderCode = String(q || '').trim().toUpperCase();
+14 -6
View File
@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -9,11 +10,13 @@
<link rel="stylesheet" href="style.css?v=13"> <link rel="stylesheet" href="style.css?v=13">
<link rel="stylesheet" href="blog.css?v=2"> <link rel="stylesheet" href="blog.css?v=2">
</head> </head>
<body class="public-search"> <body class="public-search">
<div class="public-container"> <div class="public-container">
<header class="search-header" style="text-align: left;"> <header class="search-header" style="text-align: left;">
<div style="margin-bottom: 10px; text-align: left;"> <div style="margin-bottom: 10px; text-align: left;">
<a href="https://ticket.fse-media.group" id="homeLink" style="color: var(--primary); text-decoration: none; font-weight: 500;"> <a href="https://ticket.fse-media.group" id="homeLink"
style="color: var(--primary); text-decoration: none; font-weight: 500;">
<i class="fas fa-arrow-left"></i> 返回首页 <i class="fas fa-arrow-left"></i> 返回首页
</a> </a>
</div> </div>
@@ -43,15 +46,20 @@ FMG
</a> </a>
</div> </div>
<div class="card" style="margin-top: 20px;"> <div class="card" style="margin-top: 20px;">
<div class="card-title" style="font-size: 1.25rem; font-weight: 700; margin-bottom: 20px; color: var(--text);"> <div class="card-title"
<i class="fas fa-server text-primary"></i> 服务器状态</div> style="font-size: 1.25rem; font-weight: 700; margin-bottom: 20px; color: var(--text);">
<i class="fas fa-server text-primary"></i> 服务器状态
</div>
<div style="overflow-x: auto; width: 100%;"> <div style="overflow-x: auto; width: 100%;">
<iframe id="mc-status-traintown-online-" frameborder="0" width="1000" height="500" style="max-width:100%; border-radius: 8px;" scrolling="no" src="https://motd.minebbs.com/iframe?ip=traintown.online&stype=auto&dark=true"></iframe> <iframe id="mc-status-traintown-online-" frameborder="0" width="1000" height="500"
style="max-width:100%; border-radius: 8px;" scrolling="no"
src="https://motd.minebbs.com/iframe?ip=traintown.online&stype=auto&dark=true"></iframe>
</div> </div>
</div> </div>
</section> </section>
</main> </main>
<footer style="margin-top: 60px; padding: 20px 0; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.85rem; text-align: center;"> <footer
style="margin-top: 60px; padding: 20px 0; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.85rem; text-align: center;">
<p>&copy; 2026 FSE Media Group. All rights reserved.</p> <p>&copy; 2026 FSE Media Group. All rights reserved.</p>
</footer> </footer>
<footer class="site-footer"> <footer class="site-footer">
@@ -62,5 +70,5 @@ FMG
<script src="/custom-dialog.js?v=12"></script> <script src="/custom-dialog.js?v=12"></script>
<script src="blog.js?v=2"></script> <script src="blog.js?v=2"></script>
</body> </body>
</html>
</html>
+1 -1
View File
@@ -30,7 +30,7 @@
<a href="/" class="jr-brand" id="icBrandLink"> <a href="/" class="jr-brand" id="icBrandLink">
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" /> <img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
<div class="jr-brand-copy"> <div class="jr-brand-copy">
<strong>FSE 铁路运输</strong> <strong>FarSight-T.N.E铁路运输</strong>
<span>IC 卡管理后台</span> <span>IC 卡管理后台</span>
</div> </div>
</a> </a>
+2 -2
View File
@@ -30,8 +30,8 @@
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink"> <a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" /> <img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
<div class="jr-brand-copy"> <div class="jr-brand-copy">
<strong>FSE Railway</strong> <strong>FarSight-T.N.E铁路运输</strong>
<span>IC Card Detail</span> <span>IC卡 详情</span>
</div> </div>
</a> </a>
<nav class="jr-nav" aria-label="站点导航"> <nav class="jr-nav" aria-label="站点导航">
+2 -2
View File
@@ -30,8 +30,8 @@
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink"> <a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" /> <img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
<div class="jr-brand-copy"> <div class="jr-brand-copy">
<strong>FSE Railway</strong> <strong>FarSight-T.N.E铁路运输</strong>
<span>IC Card Online Order</span> <span>IC卡 线上预定</span>
</div> </div>
</a> </a>
<nav class="jr-nav" aria-label="站点导航"> <nav class="jr-nav" aria-label="站点导航">
+2 -2
View File
@@ -30,8 +30,8 @@
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink"> <a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" /> <img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
<div class="jr-brand-copy"> <div class="jr-brand-copy">
<strong>FSE Railway</strong> <strong>FarSight-T.N.E铁路运输</strong>
<span>IC Card Search</span> <span>IC卡 查询</span>
</div> </div>
</a> </a>
<nav class="jr-nav" aria-label="站点导航"> <nav class="jr-nav" aria-label="站点导航">
+125 -22
View File
@@ -4,6 +4,10 @@
const queryBtn = $('#queryBtn'); const queryBtn = $('#queryBtn');
const summaryBoxEl = $('#summaryBox'); const summaryBoxEl = $('#summaryBox');
const eventBoxEl = $('#eventBox'); const eventBoxEl = $('#eventBox');
const state = {
cards: [],
selectedQuery: ''
};
const api = { const api = {
async request(url) { async request(url) {
@@ -19,6 +23,15 @@
} }
}; };
const getStatusClass = (status) => {
const s = String(status || '').trim().toLowerCase();
if (s === 'active') return 'jr-status-valid';
if (s === 'pending_pickup') return 'jr-status-used';
return 'jr-status-expired';
};
const getLookupKey = (card) => String(card?.card_id || '').trim();
const escapeHtml = (value) => String(value == null ? '' : value) const escapeHtml = (value) => String(value == null ? '' : value)
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
@@ -44,18 +57,17 @@
return map[String(event?.type || '').toLowerCase()] || (event?.type || '事件'); return map[String(event?.type || '').toLowerCase()] || (event?.type || '事件');
}; };
const renderSummary = (card) => { const buildCardPreview = (card) => {
const shownCardId = card.display_card_id || card.card_id || '---'; const shownCardId = card.display_card_id || card.card_id || '---';
summaryBoxEl.className = ''; return `
summaryBoxEl.innerHTML = `
<div class="jr-ticket-preview"> <div class="jr-ticket-preview">
<div class="jr-ticket-row-head"> <div class="jr-ticket-row-head">
<span class="jr-ticket-id mono">${escapeHtml(shownCardId)}</span> <span class="jr-ticket-id mono">${escapeHtml(shownCardId)}</span>
<span class="jr-status-pill ${card.status === 'active' ? 'jr-status-valid' : (card.status === 'pending_pickup' ? 'jr-status-used' : 'jr-status-expired')}">${escapeHtml(card.status_label || card.status || '未知')}</span> <span class="jr-status-pill ${getStatusClass(card.status)}">${escapeHtml(card.status_label || card.status || '未知')}</span>
</div> </div>
<div class="jr-meta-grid"> <div class="jr-meta-grid">
<div class="jr-meta-item"><span>持卡人</span><strong>${escapeHtml(card.holder_name || '未登记')}</strong></div> <div class="jr-meta-item"><span>持卡人</span><strong>${escapeHtml(card.holder_name || '未登记')}</strong></div>
<div class="jr-meta-item"><span>卡片类型</span><strong>IC 储值卡</strong></div> <div class="jr-meta-item"><span>卡片类型</span><strong>${escapeHtml(card.card_type_label || 'IC 储值卡')}</strong></div>
<div class="jr-meta-item"><span>余额</span><strong>${escapeHtml(card.balance ?? 0)}</strong></div> <div class="jr-meta-item"><span>余额</span><strong>${escapeHtml(card.balance ?? 0)}</strong></div>
<div class="jr-meta-item"><span>首次充值</span><strong>${escapeHtml(card.purchase_amount ?? card.balance ?? 0)}</strong></div> <div class="jr-meta-item"><span>首次充值</span><strong>${escapeHtml(card.purchase_amount ?? card.balance ?? 0)}</strong></div>
<div class="jr-meta-item"><span>凭证码</span><strong>${escapeHtml(card.voucher_code || card.code || card.order_code || '---')}</strong></div> <div class="jr-meta-item"><span>凭证码</span><strong>${escapeHtml(card.voucher_code || card.code || card.order_code || '---')}</strong></div>
@@ -65,12 +77,11 @@
`; `;
}; };
const renderEvents = (events) => { const buildEventsHtml = (events) => {
if (!events.length) { if (!events.length) {
eventBoxEl.innerHTML = '<div class="jr-center-empty" style="min-height:180px;"><p>暂无相关事件记录。</p></div>'; return '<div class="jr-center-empty" style="min-height:180px;"><p>暂无相关事件记录。</p></div>';
return;
} }
eventBoxEl.innerHTML = events.map((event) => ` return events.map((event) => `
<div class="jr-history-item"> <div class="jr-history-item">
<div class="jr-history-row"> <div class="jr-history-row">
<span class="jr-history-title">${escapeHtml(eventTitle(event))}</span> <span class="jr-history-title">${escapeHtml(eventTitle(event))}</span>
@@ -81,37 +92,129 @@
`).join(''); `).join('');
}; };
const renderError = (message) => { const renderEventPrompt = (message) => {
eventBoxEl.innerHTML = `<div class="jr-center-empty" style="min-height:180px;"><p>${escapeHtml(message)}</p></div>`;
};
const renderSelectedCard = (card, events) => {
if (!card) {
renderEventPrompt('请选择左侧卡片查看详情与事件记录。');
return;
}
eventBoxEl.innerHTML = `
${buildCardPreview(card)}
<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">${buildEventsHtml(events)}</div>
`;
};
const renderCardList = () => {
if (!state.cards.length) {
summaryBoxEl.className = 'jr-center-empty'; summaryBoxEl.className = 'jr-center-empty';
summaryBoxEl.innerHTML = `<p>${escapeHtml(message)}</p>`; summaryBoxEl.innerHTML = '<p>暂无可显示的 IC 卡记录。</p>';
eventBoxEl.innerHTML = '<div class="jr-center-empty" style="min-height:180px;"><p>暂无可显示的事件记录。</p></div>'; return;
}
summaryBoxEl.className = 'jr-scroll-box';
summaryBoxEl.innerHTML = state.cards.map((card) => {
const lookupKey = getLookupKey(card);
const shownCardId = card.display_card_id || card.card_id || '---';
const voucherCode = card.voucher_code || card.code || card.order_code || '---';
const isSelected = lookupKey && state.selectedQuery === lookupKey;
return `
<div class="jr-ticket-row" data-card-query="${escapeHtml(lookupKey)}" style="${isSelected ? 'background:#f7faf7;border-left:4px solid #0b6b3a;padding-left:12px;padding-right:12px;' : ''}">
<div class="jr-ticket-row-head">
<span class="jr-ticket-id mono">${escapeHtml(shownCardId)}</span>
<span class="jr-status-pill ${getStatusClass(card.status)}">${escapeHtml(card.status_label || card.status || '未知')}</span>
</div>
<div class="jr-ticket-route">${escapeHtml(card.holder_name || '未登记持卡人')}</div>
<div class="text-muted" style="margin-top:6px; font-size:0.9rem;">
余额 ${escapeHtml(card.balance ?? 0)} · 凭证码 ${escapeHtml(voucherCode)}
</div>
</div>
`;
}).join('');
summaryBoxEl.querySelectorAll('[data-card-query]').forEach((item) => {
item.addEventListener('click', () => {
const q = item.getAttribute('data-card-query');
if (q) {
loadCardDetail(q).catch((error) => {
renderEventPrompt(error.message || String(error));
});
}
});
});
};
const loadCardDetail = async (q, options = {}) => {
const { updateUrl = true } = options;
eventBoxEl.innerHTML = '<div class="jr-center-empty" style="min-height:180px;"><p>正在加载卡片详情...</p></div>';
const data = await api.query(q);
const card = data.card || null;
const events = data.events || [];
const lookupKey = getLookupKey(card) || q;
if (card) {
const existingIdx = state.cards.findIndex((item) => getLookupKey(item) === lookupKey);
if (existingIdx >= 0) state.cards[existingIdx] = card;
else state.cards = [card];
}
state.selectedQuery = lookupKey;
renderCardList();
renderSelectedCard(card, events);
if (updateUrl) {
const newUrl = `${window.location.origin}${window.location.pathname}?q=${encodeURIComponent(q)}`;
window.history.replaceState({ path: newUrl }, '', newUrl);
}
};
const loadAllCards = async () => {
summaryBoxEl.className = 'jr-center-empty';
summaryBoxEl.innerHTML = '<p>正在加载全部 IC 卡...</p>';
renderEventPrompt('正在准备卡片详情...');
const data = await api.query('');
state.cards = Array.isArray(data.cards) ? data.cards : [];
state.selectedQuery = '';
renderCardList();
if (!state.cards.length) {
renderEventPrompt('当前暂无 IC 卡记录。');
const newUrl = `${window.location.origin}${window.location.pathname}`;
window.history.replaceState({ path: newUrl }, '', newUrl);
return;
}
await loadCardDetail(getLookupKey(state.cards[0]), { updateUrl: false });
const newUrl = `${window.location.origin}${window.location.pathname}`;
window.history.replaceState({ path: newUrl }, '', newUrl);
}; };
const doQuery = async () => { const doQuery = async () => {
const q = inputEl.value.trim(); const q = inputEl.value.trim();
if (!q) { if (!q) {
renderError('请输入卡号或凭证码'); await loadAllCards();
return; return;
} }
summaryBoxEl.className = 'jr-center-empty'; summaryBoxEl.className = 'jr-center-empty';
summaryBoxEl.innerHTML = '<p>正在查询 IC 卡...</p>'; summaryBoxEl.innerHTML = '<p>正在查询 IC 卡...</p>';
eventBoxEl.innerHTML = '<div class="jr-center-empty" style="min-height:180px;"><p>正在加载事件记录...</p></div>'; state.cards = [];
const data = await api.query(q); await loadCardDetail(q);
renderSummary(data.card || {});
renderEvents(data.events || []);
const newUrl = `${window.location.origin}${window.location.pathname}?q=${encodeURIComponent(q)}`;
window.history.replaceState({ path: newUrl }, '', newUrl);
}; };
queryBtn.addEventListener('click', () => doQuery().catch((error) => renderError(error.message || String(error)))); queryBtn.addEventListener('click', () => doQuery().catch((error) => renderEventPrompt(error.message || String(error))));
inputEl.addEventListener('keydown', (event) => { inputEl.addEventListener('keydown', (event) => {
if (event.key === 'Enter') doQuery().catch((error) => renderError(error.message || String(error))); if (event.key === 'Enter') doQuery().catch((error) => renderEventPrompt(error.message || String(error)));
}); });
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const q = params.get('q'); const q = params.get('q');
if (q) { if (q) {
inputEl.value = q; inputEl.value = q;
doQuery().catch((error) => renderError(error.message || String(error))); doQuery().catch((error) => renderEventPrompt(error.message || String(error)));
} else {
loadAllCards().catch((error) => renderEventPrompt(error.message || String(error)));
} }
})(); })();
+1 -1
View File
@@ -23,7 +23,7 @@
<a href="/" class="jr-brand"> <a href="/" class="jr-brand">
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" /> <img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
<div class="jr-brand-copy"> <div class="jr-brand-copy">
<strong>FSE 铁路运输</strong> <strong>FarSight-T.N.E铁路运输</strong>
<span>控制台登录</span> <span>控制台登录</span>
</div> </div>
</a> </a>
+2 -2
View File
@@ -63,7 +63,7 @@
<a href="javascript:void(0)" @click="goHome" class="jr-brand"> <a href="javascript:void(0)" @click="goHome" class="jr-brand">
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" /> <img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
<div class="jr-brand-copy"> <div class="jr-brand-copy">
<strong>FSE铁路票务系统</strong> <strong>FarSight-T.N.E铁路运输</strong>
<span>电子客票信息</span> <span>电子客票信息</span>
</div> </div>
</a> </a>
@@ -80,7 +80,7 @@
<section class="jr-page-intro"> <section class="jr-page-intro">
<span class="jr-kicker">ELECTRONIC TICKET</span> <span class="jr-kicker">ELECTRONIC TICKET</span>
<h1>查看车票状态与最近流转记录</h1> <h1>查看车票状态与最近流转记录</h1>
<p>用于查看单张电子客票的乘车信息、状态与进出站记录,便于旅客和工作人员快速确认票据状态。</p> <p>用于查看单张电子客票的乘车信息、状态与进出站记录</p>
</section> </section>
<div v-if="loading" class="jr-panel-card"> <div v-if="loading" class="jr-panel-card">
<div class="jr-center-empty"> <div class="jr-center-empty">
+1 -1
View File
@@ -30,7 +30,7 @@
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink"> <a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" /> <img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
<div class="jr-brand-copy"> <div class="jr-brand-copy">
<strong>FSE铁路票务系统</strong> <strong>FarSight-T.N.E铁路运输</strong>
<span>线上预定</span> <span>线上预定</span>
</div> </div>
</a> </a>
+1 -1
View File
@@ -34,7 +34,7 @@
<a href="https://ticket.fse-media.group" class="jr-brand" id="routeBrandLink"> <a href="https://ticket.fse-media.group" class="jr-brand" id="routeBrandLink">
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" /> <img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
<div class="jr-brand-copy"> <div class="jr-brand-copy">
<strong>FSE 铁路运输</strong> <strong>FarSight-T.N.E铁路运输</strong>
<span>线路规划后台</span> <span>线路规划后台</span>
</div> </div>
</a> </a>
+1 -1
View File
@@ -31,7 +31,7 @@
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink"> <a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" /> <img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
<div class="jr-brand-copy"> <div class="jr-brand-copy">
<strong>FSE铁路售票系统</strong> <strong>FarSight-T.N.E铁路运输</strong>
<span>票务查询</span> <span>票务查询</span>
</div> </div>
</a> </a>