refactor(web): 重构全站界面与事件日志,优化功能与样式
1. 重构并统一票务、IC卡的事件日志格式化展示逻辑,替换原有零散的格式化函数,提升事件展示的一致性与可读性 2. 为AI助手面板新增折叠按钮与折叠逻辑,支持收起/展开面板,优化界面空间利用率 3. 移除多个页面的冗余介绍、提醒区块,精简页面结构 4. 修复IC卡购卡页面的充值选项按钮布局问题,将button元素改为div适配样式规范 5. 新增移动端卡片布局的横向滚动适配,优化小屏设备上的卡片展示与浏览体验 6. 更新后台管理页面的导航样式、文案与统计数据展示,新增车票、凭证的统计卡片 7. 统一全站品牌文案与页面格式,修复部分页面的DOCTYPE多余空格问题 8. 简化车票订单页面的地图加载逻辑,修复原有的冗余代码与加载异常问题
This commit is contained in:
+15
-210
@@ -56,49 +56,14 @@
|
||||
};
|
||||
|
||||
async function loadMap() {
|
||||
mapContainer.innerHTML = '<div class="loading">加载地图中...</div>';
|
||||
try {
|
||||
const res = await fetch('/api/public/fares/map/light');
|
||||
const svg = await res.text();
|
||||
mapContainer.innerHTML = svg;
|
||||
|
||||
const svgEl = mapContainer.querySelector('svg');
|
||||
if(!svgEl) return;
|
||||
|
||||
renderLineMap();
|
||||
} catch(e) {
|
||||
mapContainer.innerHTML = '<div class="error">加载地图失败</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function renderLineMap() {
|
||||
try {
|
||||
const [lines, stations] = await Promise.all([
|
||||
fetch('/api/public/lines').then(r=>r.json()),
|
||||
const [res, stationsData] = await Promise.all([
|
||||
fetch('/api/public/fares/map/light?t=' + Date.now()),
|
||||
fetch('/api/public/stations').then(r=>r.json())
|
||||
]);
|
||||
|
||||
let html = '';
|
||||
lines.forEach(line => {
|
||||
const stList = line.站序 || [];
|
||||
|
||||
});
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function renderSystemMap() {
|
||||
mapContainer.innerHTML = '<div class="loading">加载线路数据...</div>';
|
||||
try {
|
||||
const [linesData, stationsData] = await Promise.all([
|
||||
fetch('/api/public/lines').then(r=>r.json()),
|
||||
fetch('/api/public/stations').then(r=>r.json())
|
||||
]);
|
||||
|
||||
window.cachedStationsData = stationsData;
|
||||
stationTransfer = new Set();
|
||||
stationCanonicalByCode = {};
|
||||
stationCodesByCanonical = {};
|
||||
stationNameByCode = {};
|
||||
stationEnByCode = {};
|
||||
for (const s of stationsData) {
|
||||
@@ -108,182 +73,22 @@
|
||||
stationEnByCode[code] = String(s.en_name || s.英文名 || '');
|
||||
}
|
||||
|
||||
const transferGroups = buildTransferGroups(stationsData);
|
||||
stationCanonicalByCode = transferGroups.canonicalByCode;
|
||||
stationCodesByCanonical = transferGroups.codesByCanonical;
|
||||
for (const codes of Object.values(stationCodesByCanonical)) {
|
||||
if (codes.length >= 2) codes.forEach(code => stationTransfer.add(code));
|
||||
}
|
||||
|
||||
const lineStops = [];
|
||||
const lineColors = [];
|
||||
const linesByStation = new Map();
|
||||
for (let i = 0; i < linesData.length; i++) {
|
||||
const line = linesData[i] || {};
|
||||
const color = line.color || line.颜色 || '#93a2b7';
|
||||
const stopsRaw = Array.isArray(line.stops) ? line.stops : (Array.isArray(line.站点列表) ? line.站点列表 : []);
|
||||
const stops = stopsRaw.map(c => String(c || '').trim()).filter(Boolean);
|
||||
if (stops.length === 0) continue;
|
||||
lineStops.push({ idx: lineStops.length, name: line.name || line.线路名称 || '', color, stops });
|
||||
lineColors.push(color);
|
||||
for (const c of stops) {
|
||||
const arr = linesByStation.get(c) || [];
|
||||
arr.push(lineStops.length - 1);
|
||||
linesByStation.set(c, arr);
|
||||
}
|
||||
}
|
||||
|
||||
if (lineStops.length === 0) {
|
||||
mapContainer.innerHTML = '<div class="empty-state jr-empty-state"><p>暂无可显示的线路数据。</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [c, arr] of linesByStation.entries()) {
|
||||
const uniq = Array.from(new Set(arr));
|
||||
if (uniq.length >= 2) stationTransfer.add(c);
|
||||
}
|
||||
const svg = await res.text();
|
||||
mapContainer.innerHTML = svg;
|
||||
|
||||
// Build SVG
|
||||
const legendW = 210;
|
||||
const legendX = 16;
|
||||
const legendSwatchW = 22;
|
||||
const legendTextX = legendX + legendSwatchW + 10;
|
||||
const startX = legendW + 90;
|
||||
const baseY = 44;
|
||||
const lineGapY = 92;
|
||||
const minGapX = 78;
|
||||
|
||||
const occurrences = {};
|
||||
for (const li of lineStops) {
|
||||
for (let i = 0; i < li.stops.length; i++) {
|
||||
const code = li.stops[i];
|
||||
const canonical = stationCanonicalByCode[code] || code;
|
||||
if (!occurrences[canonical]) occurrences[canonical] = [];
|
||||
occurrences[canonical].push(i * minGapX);
|
||||
}
|
||||
}
|
||||
stationXByCanonical = {};
|
||||
for (const canonical of Object.keys(occurrences)) {
|
||||
const arr = occurrences[canonical];
|
||||
const avg = arr.reduce((a,b)=>a+b,0) / Math.max(1, arr.length);
|
||||
stationXByCanonical[canonical] = startX + Math.round(avg);
|
||||
}
|
||||
|
||||
for (let pass = 0; pass < 3; pass++) {
|
||||
for (const li of lineStops) {
|
||||
let prevX = startX - minGapX;
|
||||
for (const code of li.stops) {
|
||||
const canonical = stationCanonicalByCode[code] || code;
|
||||
const x = stationXByCanonical[canonical] ?? (prevX + minGapX);
|
||||
const nx = Math.max(x, prevX + minGapX);
|
||||
if (stationXByCanonical[canonical] == null || nx > stationXByCanonical[canonical]) stationXByCanonical[canonical] = nx;
|
||||
prevX = stationXByCanonical[canonical];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const primaryLineByStation = {};
|
||||
for (const [c, arr] of linesByStation.entries()) {
|
||||
const uniq = Array.from(new Set(arr)).sort((a,b)=>a-b);
|
||||
if (uniq.length === 0) continue;
|
||||
primaryLineByStation[c] = uniq[0];
|
||||
}
|
||||
|
||||
stationYByCanonical = {};
|
||||
for (const c of Object.keys(primaryLineByStation)) {
|
||||
const li = primaryLineByStation[c];
|
||||
if (li == null) continue;
|
||||
stationYByCanonical[c] = baseY + li * lineGapY;
|
||||
}
|
||||
|
||||
const allStations = Object.keys(primaryLineByStation);
|
||||
if (allStations.length === 0) {
|
||||
mapContainer.innerHTML = '<div class="empty-state jr-empty-state"><p>线路数据为空,请稍后再试。</p></div>';
|
||||
return;
|
||||
}
|
||||
const labelShownForCanonical = new Set();
|
||||
const transferColorsByStation = {};
|
||||
const primaryColorByStation = {};
|
||||
for (const c of allStations) {
|
||||
const lineIdxs = Array.from(new Set(linesByStation.get(c) || [])).sort((a,b)=>a-b);
|
||||
const colors = lineIdxs.map(i => lineStops[i]?.color).filter(Boolean);
|
||||
transferColorsByStation[c] = Array.from(new Set(colors));
|
||||
primaryColorByStation[c] = colors[0] || '#93a2b7';
|
||||
}
|
||||
|
||||
let svgContent = '';
|
||||
|
||||
for (const li of lineStops) {
|
||||
const yLine = baseY + li.idx * lineGapY;
|
||||
svgContent += `<rect x="${legendX}" y="${yLine-10}" width="${legendSwatchW}" height="10" rx="5" fill="${li.color}" />`;
|
||||
svgContent += `<text x="${legendTextX}" y="${yLine-1}" fill="#e5e7eb" font-size="16" font-weight="800">${li.name}</text>`;
|
||||
|
||||
if (li.stops.length >= 2) {
|
||||
const firstX = stationXByCanonical[stationCanonicalByCode[li.stops[0]] || li.stops[0]];
|
||||
let d = `M ${firstX} ${yLine}`;
|
||||
for (let i = 1; i < li.stops.length; i++) {
|
||||
const x = stationXByCanonical[stationCanonicalByCode[li.stops[i]] || li.stops[i]];
|
||||
d += ` L ${x} ${yLine}`;
|
||||
}
|
||||
svgContent += `<path d="${d}" fill="none" stroke="${li.color}" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" />`;
|
||||
}
|
||||
}
|
||||
|
||||
for (const canonical of Object.keys(stationCodesByCanonical)) {
|
||||
const codes = (stationCodesByCanonical[canonical] || []).filter(code => Number.isFinite(stationYByCanonical[code]));
|
||||
if (codes.length < 2) continue;
|
||||
const ys = codes.map(code => stationYByCanonical[code]).sort((a, b) => a - b);
|
||||
const x = stationXByCanonical[canonical];
|
||||
const labelY = ys[ys.length - 1] + 34;
|
||||
svgContent += `<line x1="${x}" y1="${ys[0]}" x2="${x}" y2="${ys[ys.length - 1]}" stroke="#cbd5e1" stroke-width="12" stroke-linecap="round" opacity="0.98" />`;
|
||||
svgContent += `<circle cx="${x}" cy="${(ys[0] + ys[ys.length - 1]) / 2}" r="7" fill="#ffffff" stroke="#111827" stroke-width="3" />`;
|
||||
svgContent += `<rect x="${x - 42}" y="${labelY - 16}" width="84" height="24" rx="12" fill="rgba(241,245,249,0.92)" stroke="#cbd5e1" stroke-width="1.5" />`;
|
||||
svgContent += `<text x="${x}" y="${labelY}" text-anchor="middle" fill="#334155" font-size="13" font-weight="800">${getStationDisplayName(canonical)}</text>`;
|
||||
labelShownForCanonical.add(canonical);
|
||||
}
|
||||
|
||||
for (const c of allStations) {
|
||||
const canonical = stationCanonicalByCode[c] || c;
|
||||
const x = stationXByCanonical[canonical];
|
||||
const yNode = stationYByCanonical[c] ?? baseY;
|
||||
const name = stationNameByCode[c] || c;
|
||||
const isTransfer = stationTransfer.has(c);
|
||||
const cls = isTransfer ? 'map-station transfer' : 'map-station';
|
||||
const ringSvg = '';
|
||||
const primaryColor = primaryColorByStation[c] || '#93a2b7';
|
||||
const outer = isTransfer
|
||||
? `<circle cx="${x}" cy="${yNode}" r="13" fill="#ffffff" stroke="${primaryColor}" stroke-width="5" />`
|
||||
: `<circle cx="${x}" cy="${yNode}" r="12" fill="#ffffff" stroke="${primaryColor}" stroke-width="5" />`;
|
||||
const transferFill = isTransfer ? `<circle cx="${x}" cy="${yNode}" r="7" fill="#ffffff" stroke="#111827" stroke-width="3" />` : '';
|
||||
const coreFill = isTransfer ? '#ffffff' : '#ffffff';
|
||||
const showLabel = !isTransfer || !labelShownForCanonical.has(canonical);
|
||||
const textSvg = showLabel
|
||||
? `<text x="${x}" y="${yNode+28}" text-anchor="middle" fill="#e5e7eb" font-size="14" font-weight="700">${name}</text>`
|
||||
: '';
|
||||
svgContent += `
|
||||
<g class="${cls}" data-code="${c}" onclick="handleStationClick('${c}')" style="cursor:pointer">
|
||||
${ringSvg}
|
||||
${outer}
|
||||
${transferFill}
|
||||
<circle class="node-core" cx="${x}" cy="${yNode}" r="8" fill="${coreFill}" stroke="#111827" stroke-width="3" />
|
||||
${textSvg}
|
||||
</g>
|
||||
`;
|
||||
}
|
||||
|
||||
const width = Math.max(560, Math.max(...allStations.map(c => stationXByCanonical[stationCanonicalByCode[c] || c] || 0)) + 120);
|
||||
const height = Math.max(260, baseY + lineStops.length * lineGapY + 60);
|
||||
mapContainer.innerHTML = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${svgContent}</svg>`;
|
||||
|
||||
// Bind click events (standard onclick attribute in SVG string might not work in some contexts, but usually fine in innerHTML)
|
||||
// Better to add event listeners via delegation
|
||||
const svgEl = mapContainer.querySelector('svg');
|
||||
if(!svgEl) return;
|
||||
|
||||
svgEl.querySelectorAll('.map-station').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
window.handleStationClick(el.getAttribute('data-code'));
|
||||
});
|
||||
});
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
mapContainer.innerHTML = '<div class="error">地图加载失败</div>';
|
||||
mapContainer.innerHTML = '<div class="error">加载地图失败</div>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
window.handleStationClick = (code) => {
|
||||
code = String(code || '').trim();
|
||||
// Toggle if already selected
|
||||
@@ -506,7 +311,7 @@
|
||||
}
|
||||
|
||||
// Load Map on Start
|
||||
renderSystemMap();
|
||||
loadMap();
|
||||
|
||||
async function previewPrice(force = false){
|
||||
const seq = ++previewSeq;
|
||||
|
||||
Reference in New Issue
Block a user