feat(web): 优化票务与IC卡查询页面的功能与UI

- 更新静态资源版本以清理浏览器缓存
- 新增查询概览模块与搜索辅助提示文字
- 添加XSS内容转义防护,优化列表项选中样式
- 重构IC卡查询页面布局,拆分详情与事件记录区域
- 优化移动端响应式展示效果
This commit is contained in:
2026-06-28 11:20:57 +08:00
parent 042720d812
commit d6aa03d3a7
5 changed files with 314 additions and 49 deletions
+34 -6
View File
@@ -4,6 +4,10 @@
const detailEl = $('#detail');
const qEl = $('#q');
const btn = $('#searchBtn');
const state = {
items: [],
selectedId: ''
};
const api = {
searchTickets: async (q) => {
@@ -52,6 +56,13 @@
return type;
};
const escapeHtml = (value) => String(value == null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
const getTicketId = (obj) => (obj && (obj.ticket_id || obj["车票编号"] || obj.id)) || '';
const isValidStatus = (status) => {
@@ -111,6 +122,7 @@
};
function renderList(items) {
state.items = items;
listEl.innerHTML = '';
if (items.length === 0) {
listEl.innerHTML = '<div class="jr-center-empty"><p>未找到匹配结果。</p></div>';
@@ -119,22 +131,35 @@
items.forEach(it => {
const id = it.ticket_id || it["车票编号"] || '';
const row = document.createElement('div');
row.className = 'jr-ticket-row';
const statusText = formatStatusText(it.status || it["状态"] || '');
const isSelected = state.selectedId === id;
row.className = `jr-ticket-row${isSelected ? ' is-active' : ''}`;
const overview = it.overview || it["概览"] || null;
const startName = overview ? (overview.start_name || overview["起点"]) : (it.start_name || it["起点"] || '---');
const terminalName = overview ? (overview.terminal_name || overview["终点"]) : (it.terminal_name || it["终点"] || '---');
const updateTime = formatTime(
(overview && (overview.last_update_ts || overview["上次更新时间"])) ||
it.last_update_ts ||
it["上次更新时间"] ||
''
);
row.innerHTML = `
<div class="jr-ticket-row-head">
<span class="jr-ticket-id mono">${id}</span>
<i class="fas fa-chevron-right text-muted"></i>
<span class="jr-ticket-id mono">${escapeHtml(id)}</span>
<span class="jr-status-pill ${isValidStatus(statusText) ? 'jr-status-valid' : (statusText === '已使用' ? 'jr-status-used' : 'jr-status-expired')}">${escapeHtml(statusText)}</span>
</div>
<div class="jr-ticket-route">
${startName}${terminalName}
${escapeHtml(startName)}${escapeHtml(terminalName)}
</div>
<div class="jr-list-meta">最近更新 ${escapeHtml(updateTime)}</div>
`;
row.onclick = () => loadDetail(id);
row.onclick = () => {
state.selectedId = id;
renderList(state.items);
loadDetail(id);
};
listEl.appendChild(row);
});
}
@@ -208,6 +233,8 @@
}
async function loadDetail(id) {
state.selectedId = id;
renderList(state.items);
detailEl.innerHTML = '<div class="jr-center-empty"><p>正在加载详情...</p></div>';
try {
const d = await api.ticketDetail(id);
@@ -229,6 +256,7 @@
listEl.innerHTML = '<div class="jr-center-empty"><p>正在搜索...</p></div>';
try {
const d = await api.searchTickets(q);
state.selectedId = state.selectedId && d.some((item) => getTicketId(item) === state.selectedId) ? state.selectedId : '';
renderList(d);
} catch (e) {
listEl.innerHTML = '<div class="jr-center-empty"><p>搜索失败。</p></div>';