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:
+10
-2
@@ -345,8 +345,16 @@ router.post('/orders/:code/consume', async (req, res) => {
|
||||
router.get('/ic-cards/query', async (req, res) => {
|
||||
const q = String(req.query.q || '').trim();
|
||||
if (!q) {
|
||||
appendReqLog(req, { category: 'public', type: 'ic_card_query_invalid', level: 'warn', detail: { q } });
|
||||
return res.status(400).json({ ok: false, error: 'query required' });
|
||||
const cards = (DataService.getIcCards() || [])
|
||||
.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 normOrderCode = String(q || '').trim().toUpperCase();
|
||||
|
||||
+14
-6
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<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="blog.css?v=2">
|
||||
</head>
|
||||
|
||||
<body class="public-search">
|
||||
<div class="public-container">
|
||||
<header class="search-header" style="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> 返回首页
|
||||
</a>
|
||||
</div>
|
||||
@@ -43,15 +46,20 @@ FMG
|
||||
</a>
|
||||
</div>
|
||||
<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);">
|
||||
<i class="fas fa-server text-primary"></i> 服务器状态</div>
|
||||
<div class="card-title"
|
||||
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%;">
|
||||
<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>
|
||||
</section>
|
||||
</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>© 2026 FSE Media Group. All rights reserved.</p>
|
||||
</footer>
|
||||
<footer class="site-footer">
|
||||
@@ -62,5 +70,5 @@ FMG
|
||||
<script src="/custom-dialog.js?v=12"></script>
|
||||
<script src="blog.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -30,7 +30,7 @@
|
||||
<a href="/" class="jr-brand" id="icBrandLink">
|
||||
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
||||
<div class="jr-brand-copy">
|
||||
<strong>FSE 铁路运输</strong>
|
||||
<strong>FarSight-T.N.E铁路运输</strong>
|
||||
<span>IC 卡管理后台</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
||||
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
||||
<div class="jr-brand-copy">
|
||||
<strong>FSE Railway</strong>
|
||||
<span>IC Card Detail</span>
|
||||
<strong>FarSight-T.N.E铁路运输</strong>
|
||||
<span>IC卡 详情</span>
|
||||
</div>
|
||||
</a>
|
||||
<nav class="jr-nav" aria-label="站点导航">
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
||||
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
||||
<div class="jr-brand-copy">
|
||||
<strong>FSE Railway</strong>
|
||||
<span>IC Card Online Order</span>
|
||||
<strong>FarSight-T.N.E铁路运输</strong>
|
||||
<span>IC卡 线上预定</span>
|
||||
</div>
|
||||
</a>
|
||||
<nav class="jr-nav" aria-label="站点导航">
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
||||
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
||||
<div class="jr-brand-copy">
|
||||
<strong>FSE Railway</strong>
|
||||
<span>IC Card Search</span>
|
||||
<strong>FarSight-T.N.E铁路运输</strong>
|
||||
<span>IC卡 查询</span>
|
||||
</div>
|
||||
</a>
|
||||
<nav class="jr-nav" aria-label="站点导航">
|
||||
|
||||
+125
-22
@@ -4,6 +4,10 @@
|
||||
const queryBtn = $('#queryBtn');
|
||||
const summaryBoxEl = $('#summaryBox');
|
||||
const eventBoxEl = $('#eventBox');
|
||||
const state = {
|
||||
cards: [],
|
||||
selectedQuery: ''
|
||||
};
|
||||
|
||||
const api = {
|
||||
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)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
@@ -44,18 +57,17 @@
|
||||
return map[String(event?.type || '').toLowerCase()] || (event?.type || '事件');
|
||||
};
|
||||
|
||||
const renderSummary = (card) => {
|
||||
const buildCardPreview = (card) => {
|
||||
const shownCardId = card.display_card_id || card.card_id || '---';
|
||||
summaryBoxEl.className = '';
|
||||
summaryBoxEl.innerHTML = `
|
||||
return `
|
||||
<div class="jr-ticket-preview">
|
||||
<div class="jr-ticket-row-head">
|
||||
<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 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>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.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>
|
||||
@@ -65,12 +77,11 @@
|
||||
`;
|
||||
};
|
||||
|
||||
const renderEvents = (events) => {
|
||||
const buildEventsHtml = (events) => {
|
||||
if (!events.length) {
|
||||
eventBoxEl.innerHTML = '<div class="jr-center-empty" style="min-height:180px;"><p>暂无相关事件记录。</p></div>';
|
||||
return;
|
||||
return '<div class="jr-center-empty" style="min-height:180px;"><p>暂无相关事件记录。</p></div>';
|
||||
}
|
||||
eventBoxEl.innerHTML = events.map((event) => `
|
||||
return events.map((event) => `
|
||||
<div class="jr-history-item">
|
||||
<div class="jr-history-row">
|
||||
<span class="jr-history-title">${escapeHtml(eventTitle(event))}</span>
|
||||
@@ -81,37 +92,129 @@
|
||||
`).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.innerHTML = `<p>${escapeHtml(message)}</p>`;
|
||||
eventBoxEl.innerHTML = '<div class="jr-center-empty" style="min-height:180px;"><p>暂无可显示的事件记录。</p></div>';
|
||||
summaryBoxEl.innerHTML = '<p>暂无可显示的 IC 卡记录。</p>';
|
||||
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 q = inputEl.value.trim();
|
||||
if (!q) {
|
||||
renderError('请输入卡号或凭证码');
|
||||
await loadAllCards();
|
||||
return;
|
||||
}
|
||||
summaryBoxEl.className = 'jr-center-empty';
|
||||
summaryBoxEl.innerHTML = '<p>正在查询 IC 卡...</p>';
|
||||
eventBoxEl.innerHTML = '<div class="jr-center-empty" style="min-height:180px;"><p>正在加载事件记录...</p></div>';
|
||||
const data = await api.query(q);
|
||||
renderSummary(data.card || {});
|
||||
renderEvents(data.events || []);
|
||||
const newUrl = `${window.location.origin}${window.location.pathname}?q=${encodeURIComponent(q)}`;
|
||||
window.history.replaceState({ path: newUrl }, '', newUrl);
|
||||
state.cards = [];
|
||||
await loadCardDetail(q);
|
||||
};
|
||||
|
||||
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) => {
|
||||
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 q = params.get('q');
|
||||
if (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
@@ -23,7 +23,7 @@
|
||||
<a href="/" class="jr-brand">
|
||||
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
||||
<div class="jr-brand-copy">
|
||||
<strong>FSE 铁路运输</strong>
|
||||
<strong>FarSight-T.N.E铁路运输</strong>
|
||||
<span>控制台登录</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<a href="javascript:void(0)" @click="goHome" class="jr-brand">
|
||||
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
||||
<div class="jr-brand-copy">
|
||||
<strong>FSE铁路票务系统</strong>
|
||||
<strong>FarSight-T.N.E铁路运输</strong>
|
||||
<span>电子客票信息</span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -80,7 +80,7 @@
|
||||
<section class="jr-page-intro">
|
||||
<span class="jr-kicker">ELECTRONIC TICKET</span>
|
||||
<h1>查看车票状态与最近流转记录</h1>
|
||||
<p>用于查看单张电子客票的乘车信息、状态与进出站记录,便于旅客和工作人员快速确认票据状态。</p>
|
||||
<p>用于查看单张电子客票的乘车信息、状态与进出站记录</p>
|
||||
</section>
|
||||
<div v-if="loading" class="jr-panel-card">
|
||||
<div class="jr-center-empty">
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
||||
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
||||
<div class="jr-brand-copy">
|
||||
<strong>FSE铁路票务系统</strong>
|
||||
<strong>FarSight-T.N.E铁路运输</strong>
|
||||
<span>线上预定</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<a href="https://ticket.fse-media.group" class="jr-brand" id="routeBrandLink">
|
||||
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
||||
<div class="jr-brand-copy">
|
||||
<strong>FSE 铁路运输</strong>
|
||||
<strong>FarSight-T.N.E铁路运输</strong>
|
||||
<span>线路规划后台</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<a href="https://ticket.fse-media.group" class="jr-brand" id="brandLink">
|
||||
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
||||
<div class="jr-brand-copy">
|
||||
<strong>FSE铁路售票系统</strong>
|
||||
<strong>FarSight-T.N.E铁路运输</strong>
|
||||
<span>票务查询</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user