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:
+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>暂无可显示的 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>${escapeHtml(message)}</p>`;
|
||||
eventBoxEl.innerHTML = '<div class="jr-center-empty" style="min-height:180px;"><p>暂无可显示的事件记录。</p></div>';
|
||||
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)));
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user