118 lines
5.3 KiB
JavaScript
118 lines
5.3 KiB
JavaScript
(() => {
|
|
const $ = (sel) => document.querySelector(sel);
|
|
const inputEl = $('#queryInput');
|
|
const queryBtn = $('#queryBtn');
|
|
const summaryBoxEl = $('#summaryBox');
|
|
const eventBoxEl = $('#eventBox');
|
|
|
|
const api = {
|
|
async request(url) {
|
|
const res = await fetch(url);
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok || data.ok === false) {
|
|
throw new Error(data.error || res.statusText || '请求失败');
|
|
}
|
|
return data;
|
|
},
|
|
query(q) {
|
|
return api.request(`/api/public/ic-cards/query?q=${encodeURIComponent(q)}`);
|
|
}
|
|
};
|
|
|
|
const escapeHtml = (value) => String(value == null ? '' : value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
|
|
const formatTime = (value) => {
|
|
if (value == null || value === '') return '---';
|
|
const ts = Number(value);
|
|
const date = Number.isFinite(ts) ? new Date(ts) : new Date(value);
|
|
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString('zh-CN', { hour12: false });
|
|
};
|
|
|
|
const eventTitle = (event) => {
|
|
const map = {
|
|
create: '后台建卡',
|
|
update: '信息更新',
|
|
topup: '余额充值',
|
|
order_created: '线上购卡',
|
|
activated: '正式启用'
|
|
};
|
|
return map[String(event?.type || '').toLowerCase()] || (event?.type || '事件');
|
|
};
|
|
|
|
const renderSummary = (card) => {
|
|
const shownCardId = card.display_card_id || card.card_id || '---';
|
|
summaryBoxEl.className = '';
|
|
summaryBoxEl.innerHTML = `
|
|
<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>
|
|
</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.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(formatTime(card.created_ts))}</strong></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
};
|
|
|
|
const renderEvents = (events) => {
|
|
if (!events.length) {
|
|
eventBoxEl.innerHTML = '<div class="jr-center-empty" style="min-height:180px;"><p>暂无相关事件记录。</p></div>';
|
|
return;
|
|
}
|
|
eventBoxEl.innerHTML = events.map((event) => `
|
|
<div class="jr-history-item">
|
|
<div class="jr-history-row">
|
|
<span class="jr-history-title">${escapeHtml(eventTitle(event))}</span>
|
|
<span class="jr-history-time">${escapeHtml(formatTime(event.ts))}</span>
|
|
</div>
|
|
<div class="jr-history-desc">${escapeHtml(typeof event.detail === 'string' ? event.detail : JSON.stringify(event.detail || {}, null, 2))}</div>
|
|
</div>
|
|
`).join('');
|
|
};
|
|
|
|
const renderError = (message) => {
|
|
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>';
|
|
};
|
|
|
|
const doQuery = async () => {
|
|
const q = inputEl.value.trim();
|
|
if (!q) {
|
|
renderError('请输入卡号或凭证码');
|
|
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);
|
|
};
|
|
|
|
queryBtn.addEventListener('click', () => doQuery().catch((error) => renderError(error.message || String(error))));
|
|
inputEl.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter') doQuery().catch((error) => renderError(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)));
|
|
}
|
|
})();
|