feat(web): 优化票务与IC卡查询页面的功能与UI
- 更新静态资源版本以清理浏览器缓存 - 新增查询概览模块与搜索辅助提示文字 - 添加XSS内容转义防护,优化列表项选中样式 - 重构IC卡查询页面布局,拆分详情与事件记录区域 - 优化移动端响应式展示效果
This commit is contained in:
+38
-17
@@ -3,6 +3,7 @@
|
||||
const inputEl = $('#queryInput');
|
||||
const queryBtn = $('#queryBtn');
|
||||
const summaryBoxEl = $('#summaryBox');
|
||||
const detailBoxEl = $('#detailBox');
|
||||
const eventBoxEl = $('#eventBox');
|
||||
const state = {
|
||||
cards: [],
|
||||
@@ -59,6 +60,9 @@
|
||||
|
||||
const buildCardPreview = (card) => {
|
||||
const shownCardId = card.display_card_id || card.card_id || '---';
|
||||
const detailHref = window.location.hostname.includes('fse-media.group')
|
||||
? `https://ticket.fse-media.group/ic/${encodeURIComponent(card.card_id || shownCardId)}`
|
||||
: `/ic/${encodeURIComponent(card.card_id || shownCardId)}`;
|
||||
return `
|
||||
<div class="jr-ticket-preview">
|
||||
<div class="jr-ticket-row-head">
|
||||
@@ -73,6 +77,12 @@
|
||||
<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(formatTime(card.created_ts))}</strong></div>
|
||||
</div>
|
||||
<div class="jr-action-row">
|
||||
<a href="${detailHref}" class="btn jr-secondary-btn" target="_blank" rel="noopener noreferrer">
|
||||
<i class="fas fa-id-card"></i>
|
||||
打开卡片页
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
@@ -92,23 +102,22 @@
|
||||
`).join('');
|
||||
};
|
||||
|
||||
const renderDetailPrompt = (message) => {
|
||||
detailBoxEl.innerHTML = `<div class="jr-center-empty" style="min-height:180px;"><p>${escapeHtml(message)}</p></div>`;
|
||||
};
|
||||
|
||||
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) {
|
||||
renderDetailPrompt('请选择左侧卡片查看详情。');
|
||||
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>
|
||||
`;
|
||||
detailBoxEl.innerHTML = buildCardPreview(card);
|
||||
eventBoxEl.innerHTML = buildEventsHtml(events);
|
||||
};
|
||||
|
||||
const renderCardList = () => {
|
||||
@@ -125,13 +134,13 @@
|
||||
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${isSelected ? ' is-active' : ''}" data-card-query="${escapeHtml(lookupKey)}">
|
||||
<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;">
|
||||
<div class="jr-list-meta">
|
||||
余额 ${escapeHtml(card.balance ?? 0)} · 凭证码 ${escapeHtml(voucherCode)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,7 +152,7 @@
|
||||
const q = item.getAttribute('data-card-query');
|
||||
if (q) {
|
||||
loadCardDetail(q).catch((error) => {
|
||||
renderEventPrompt(error.message || String(error));
|
||||
renderQueryError(error.message || String(error));
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -152,7 +161,8 @@
|
||||
|
||||
const loadCardDetail = async (q, options = {}) => {
|
||||
const { updateUrl = true } = options;
|
||||
eventBoxEl.innerHTML = '<div class="jr-center-empty" style="min-height:180px;"><p>正在加载卡片详情...</p></div>';
|
||||
renderDetailPrompt('正在加载卡片详情...');
|
||||
renderEventPrompt('正在加载事件记录...');
|
||||
const data = await api.query(q);
|
||||
const card = data.card || null;
|
||||
const events = data.events || [];
|
||||
@@ -174,13 +184,15 @@
|
||||
const loadAllCards = async () => {
|
||||
summaryBoxEl.className = 'jr-center-empty';
|
||||
summaryBoxEl.innerHTML = '<p>正在加载全部 IC 卡...</p>';
|
||||
renderEventPrompt('正在准备卡片详情...');
|
||||
renderDetailPrompt('正在准备卡片详情...');
|
||||
renderEventPrompt('正在准备事件记录...');
|
||||
const data = await api.query('');
|
||||
state.cards = Array.isArray(data.cards) ? data.cards : [];
|
||||
state.selectedQuery = '';
|
||||
renderCardList();
|
||||
|
||||
if (!state.cards.length) {
|
||||
renderDetailPrompt('当前暂无 IC 卡记录。');
|
||||
renderEventPrompt('当前暂无 IC 卡记录。');
|
||||
const newUrl = `${window.location.origin}${window.location.pathname}`;
|
||||
window.history.replaceState({ path: newUrl }, '', newUrl);
|
||||
@@ -200,21 +212,30 @@
|
||||
}
|
||||
summaryBoxEl.className = 'jr-center-empty';
|
||||
summaryBoxEl.innerHTML = '<p>正在查询 IC 卡...</p>';
|
||||
renderDetailPrompt('正在查询卡片详情...');
|
||||
renderEventPrompt('正在查询事件记录...');
|
||||
state.cards = [];
|
||||
await loadCardDetail(q);
|
||||
};
|
||||
|
||||
queryBtn.addEventListener('click', () => doQuery().catch((error) => renderEventPrompt(error.message || String(error))));
|
||||
const renderQueryError = (message) => {
|
||||
summaryBoxEl.className = 'jr-center-empty';
|
||||
summaryBoxEl.innerHTML = `<p>${escapeHtml(message)}</p>`;
|
||||
renderDetailPrompt(message);
|
||||
renderEventPrompt(message);
|
||||
};
|
||||
|
||||
queryBtn.addEventListener('click', () => doQuery().catch((error) => renderQueryError(error.message || String(error))));
|
||||
inputEl.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') doQuery().catch((error) => renderEventPrompt(error.message || String(error)));
|
||||
if (event.key === 'Enter') doQuery().catch((error) => renderQueryError(error.message || String(error)));
|
||||
});
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const q = params.get('q');
|
||||
if (q) {
|
||||
inputEl.value = q;
|
||||
doQuery().catch((error) => renderEventPrompt(error.message || String(error)));
|
||||
doQuery().catch((error) => renderQueryError(error.message || String(error)));
|
||||
} else {
|
||||
loadAllCards().catch((error) => renderEventPrompt(error.message || String(error)));
|
||||
loadAllCards().catch((error) => renderQueryError(error.message || String(error)));
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user