(() => { const $ = (sel) => document.querySelector(sel); const state = { cards: [], selectedId: '', selectedCard: null, selectedEvents: [] }; const appDialog = window.appDialog || { alert: (message) => Promise.resolve(window.alert(message)), confirm: (message) => Promise.resolve(window.confirm(message)), prompt: (message, defaultValue) => Promise.resolve(window.prompt(message, defaultValue)) }; const HOLDER_NAME_PATTERN = /^[A-Za-z][A-Za-z .,'()&@/_\-+]*$/; const displayCardId = (card) => String(card?.display_card_id || card?.card_id || '---'); const statusTextEl = $('#serverStatusText'); const listEl = $('#cardList'); const detailEl = $('#detailPanel'); const eventEl = $('#eventList'); const searchEl = $('#searchInput'); const api = { async request(url, opts = {}) { const res = await fetch(url, opts); const data = await res.json().catch(() => ({})); if (!res.ok || data.ok === false) { throw new Error(data.error || res.statusText || '请求失败'); } return data; }, fetchCards(q = '') { return api.request(`/api/ic-cards?q=${encodeURIComponent(q)}`); }, fetchCardDetail(id) { return api.request(`/api/ic-cards/${encodeURIComponent(id)}`); }, createCard(payload) { return api.request('/api/ic-cards', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); }, updateCard(id, payload) { return api.request(`/api/ic-cards/${encodeURIComponent(id)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); }, topup(id, amount) { return api.request(`/api/ic-cards/${encodeURIComponent(id)}/topup`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount }) }); } }; const escapeHtml = (value) => String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); const formatMoney = (value) => { const n = Number(value || 0); return Number.isFinite(n) ? n.toFixed(0) : '0'; }; 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 statusInfo = (status) => { const map = { pending_pickup: { text: '待领卡', className: 'badge-warning' }, active: { text: '正常', className: 'badge-success' }, disabled: { text: '停用', className: 'badge-danger' }, lost: { text: '挂失', className: 'badge-danger' }, refunded: { text: '已退卡', className: 'badge-secondary' } }; return map[String(status || '').toLowerCase()] || { text: status || '未知', className: 'badge-secondary' }; }; const eventTitle = (event) => { const map = { create: '后台建卡', update: '信息更新', topup: '余额充值', order_created: '线上购卡', activated: '正式启用' }; return map[String(event?.type || '').toLowerCase()] || (event?.type || '事件'); }; const renderStats = () => { const cards = state.cards; $('#statTotal').textContent = String(cards.length); $('#statPending').textContent = String(cards.filter((card) => card.status === 'pending_pickup').length); $('#statActive').textContent = String(cards.filter((card) => card.status === 'active').length); $('#statBalance').textContent = formatMoney(cards.reduce((sum, card) => sum + (Number(card.balance || 0) || 0), 0)); $('#listCountBadge').textContent = String(cards.length); }; const renderList = () => { if (!state.cards.length) { listEl.innerHTML = '

暂无 IC 卡记录。

'; return; } listEl.innerHTML = state.cards.map((card) => { const info = statusInfo(card.status); return `
${escapeHtml(displayCardId(card))}
${escapeHtml(card.holder_name || '未登记持卡人')} · IC 储值卡
订单 ${escapeHtml(card.order_code || '---')} · 余额 ${escapeHtml(formatMoney(card.balance))}
${escapeHtml(info.text)}
`; }).join(''); listEl.querySelectorAll('[data-id]').forEach((item) => { item.addEventListener('click', () => { const id = item.getAttribute('data-id'); if (id) loadCard(id); }); }); }; const renderDetail = () => { if (!state.selectedCard) { detailEl.className = 'empty-state'; detailEl.innerHTML = '

从左侧选择一张 IC 卡以查看详情。

'; eventEl.innerHTML = '
选择卡片后显示事件流。
'; return; } const card = state.selectedCard; const info = statusInfo(card.status); detailEl.className = ''; detailEl.innerHTML = `
${escapeHtml(displayCardId(card))}
订单号 ${escapeHtml(card.order_code || '---')} · 来源 ${escapeHtml(card.source || '---')}
${escapeHtml(info.text)}
创建时间${escapeHtml(formatTime(card.created_ts))}
最后更新${escapeHtml(formatTime(card.last_update_ts))}
首次充值${escapeHtml(formatMoney(card.purchase_amount ?? card.balance))}
购卡金额${escapeHtml(formatMoney(card.purchase_amount))}
`; const events = state.selectedEvents; eventEl.innerHTML = events.length ? 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 validateHolderName = (value) => { const holderName = String(value || '').trim(); if (!holderName) return '请输入持卡人姓名'; if (holderName.length > 24) return '持卡人姓名不能超过 24 个字符'; if (!HOLDER_NAME_PATTERN.test(holderName)) return '持卡人姓名仅支持英文与常用符号'; return ''; }; const currentDetailPayload = () => ({ holder_name: $('#detailHolder')?.value.trim() || '', status: $('#detailStatus')?.value || 'active', balance: Number($('#detailBalance')?.value || 0) || 0 }); async function refreshList(keepSelection = true) { const query = searchEl.value.trim(); const data = await api.fetchCards(query); state.cards = data.cards || []; renderStats(); renderList(); if (keepSelection && state.selectedId && state.cards.some((card) => card.card_id === state.selectedId)) { await loadCard(state.selectedId, false); } else if (state.selectedId && !state.cards.some((card) => card.card_id === state.selectedId)) { state.selectedId = ''; state.selectedCard = null; state.selectedEvents = []; renderDetail(); } } async function loadCard(id, rerenderList = true) { const data = await api.fetchCardDetail(id); state.selectedId = id; state.selectedCard = data.card || null; state.selectedEvents = data.events || []; if (rerenderList) renderList(); renderDetail(); } async function createCard() { const holder_name = $('#createHolder').value.trim(); const balance = Number($('#createBalance').value || 0) || 0; const holderNameError = validateHolderName(holder_name); if (holderNameError) { alert(holderNameError); return; } const data = await api.createCard({ holder_name, balance }); $('#createHolder').value = ''; $('#createBalance').value = '50'; await refreshList(false); if (data.card_id) await loadCard(data.card_id); alert(`IC 卡已创建:${displayCardId(data.card || data)}`); } async function saveCard() { if (!state.selectedId) { alert('请先选择一张 IC 卡'); return; } const payload = currentDetailPayload(); const holderNameError = validateHolderName(payload.holder_name); if (holderNameError) { alert(holderNameError); return; } await api.updateCard(state.selectedId, payload); await refreshList(false); await loadCard(state.selectedId); alert('已保存 IC 卡信息'); } async function topupCard() { if (!state.selectedId) { alert('请先选择一张 IC 卡'); return; } const raw = await appDialog.prompt({ title: 'IC 卡充值', message: `请输入给 ${displayCardId(state.selectedCard || { card_id: state.selectedId })} 充值的金额`, defaultValue: '50', placeholder: '请输入充值金额', confirmText: '确认充值' }); if (raw == null) return; const amount = Number(raw); if (!(amount > 0)) { alert('充值金额必须大于 0'); return; } await api.topup(state.selectedId, amount); await refreshList(false); await loadCard(state.selectedId); alert('充值成功'); } async function pingServer() { try { await fetch('/api/public/health', { cache: 'no-store' }); statusTextEl.textContent = '服务状态:在线'; } catch (_) { statusTextEl.textContent = '服务状态:离线'; } } $('#refreshBtn').addEventListener('click', () => refreshList(false).catch((error) => alert(error.message || String(error)))); $('#createBtn').addEventListener('click', () => createCard().catch((error) => alert(error.message || String(error)))); $('#saveBtn').addEventListener('click', () => saveCard().catch((error) => alert(error.message || String(error)))); $('#topupBtn').addEventListener('click', () => topupCard().catch((error) => alert(error.message || String(error)))); searchEl.addEventListener('keydown', (event) => { if (event.key === 'Enter') { refreshList(false).catch((error) => alert(error.message || String(error))); } }); searchEl.addEventListener('input', () => { window.clearTimeout(searchEl._timer); searchEl._timer = window.setTimeout(() => { refreshList(false).catch((error) => alert(error.message || String(error))); }, 240); }); (async () => { await pingServer(); await refreshList(false); if (state.cards[0]) await loadCard(state.cards[0].card_id); })().catch((error) => { statusTextEl.textContent = '服务状态:请求失败'; listEl.innerHTML = `
${escapeHtml(error.message || String(error))}
`; }); })();