(() => { const $ = (sel) => document.querySelector(sel); const inputEl = $('#queryInput'); const queryBtn = $('#queryBtn'); const summaryBoxEl = $('#summaryBox'); const detailBoxEl = $('#detailBox'); const eventBoxEl = $('#eventBox'); const state = { cards: [], selectedQuery: '' }; 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 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, '>') .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 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 `
${escapeHtml(shownCardId)} ${escapeHtml(card.status_label || card.status || '未知')}
持卡人${escapeHtml(card.holder_name || '未登记')}
卡片类型${escapeHtml(card.card_type_label || 'IC 储值卡')}
余额${escapeHtml(card.balance ?? 0)}
首次充值${escapeHtml(card.purchase_amount ?? card.balance ?? 0)}
凭证码${escapeHtml(card.voucher_code || card.code || card.order_code || '---')}
购卡时间${escapeHtml(formatTime(card.created_ts))}
打开卡片页
`; }; const buildEventsHtml = (events) => { if (!events.length) { return '

暂无相关事件记录。

'; } return events.map((event) => `
${escapeHtml(eventTitle(event))} ${escapeHtml(formatTime(event.ts))}
${escapeHtml(typeof event.detail === 'string' ? event.detail : JSON.stringify(event.detail || {}, null, 2))}
`).join(''); }; const renderDetailPrompt = (message) => { detailBoxEl.innerHTML = `

${escapeHtml(message)}

`; }; const renderEventPrompt = (message) => { eventBoxEl.innerHTML = `

${escapeHtml(message)}

`; }; const renderSelectedCard = (card, events) => { if (!card) { renderDetailPrompt('请选择左侧卡片查看详情。'); renderEventPrompt('请选择左侧卡片查看详情与事件记录。'); return; } detailBoxEl.innerHTML = buildCardPreview(card); eventBoxEl.innerHTML = buildEventsHtml(events); }; const renderCardList = () => { if (!state.cards.length) { summaryBoxEl.className = 'jr-center-empty'; summaryBoxEl.innerHTML = '

暂无可显示的 IC 卡记录。

'; 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 `
${escapeHtml(shownCardId)} ${escapeHtml(card.status_label || card.status || '未知')}
${escapeHtml(card.holder_name || '未登记持卡人')}
余额 ${escapeHtml(card.balance ?? 0)} · 凭证码 ${escapeHtml(voucherCode)}
`; }).join(''); summaryBoxEl.querySelectorAll('[data-card-query]').forEach((item) => { item.addEventListener('click', () => { const q = item.getAttribute('data-card-query'); if (q) { loadCardDetail(q).catch((error) => { renderQueryError(error.message || String(error)); }); } }); }); }; const loadCardDetail = async (q, options = {}) => { const { updateUrl = true } = options; renderDetailPrompt('正在加载卡片详情...'); renderEventPrompt('正在加载事件记录...'); 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 = '

正在加载全部 IC 卡...

'; 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); 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) { await loadAllCards(); return; } summaryBoxEl.className = 'jr-center-empty'; summaryBoxEl.innerHTML = '

正在查询 IC 卡...

'; renderDetailPrompt('正在查询卡片详情...'); renderEventPrompt('正在查询事件记录...'); state.cards = []; await loadCardDetail(q); }; const renderQueryError = (message) => { summaryBoxEl.className = 'jr-center-empty'; summaryBoxEl.innerHTML = `

${escapeHtml(message)}

`; 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) => renderQueryError(error.message || String(error))); }); const params = new URLSearchParams(location.search); const q = params.get('q'); if (q) { inputEl.value = q; doQuery().catch((error) => renderQueryError(error.message || String(error))); } else { loadAllCards().catch((error) => renderQueryError(error.message || String(error))); } })();