refactor(web): 将换乘站合并为单点并重绘订票线路图

This commit is contained in:
2026-07-12 08:35:07 +08:00
parent 83c7d25cbe
commit a651fac095
4 changed files with 42 additions and 64 deletions
+2 -2
View File
@@ -16,8 +16,8 @@
"mtimeMs": 1783602619602.658 "mtimeMs": 1783602619602.658
}, },
"ticket-order.js": { "ticket-order.js": {
"size": 34258, "size": 33737,
"mtimeMs": 1783784148462.1907 "mtimeMs": 1783816412239.149
}, },
"ticket-search.js": { "ticket-search.js": {
"size": 17269, "size": 17269,
+1 -1
View File
@@ -3,7 +3,7 @@
"blog.css": 2, "blog.css": 2,
"custom-dialog.js": 11, "custom-dialog.js": 11,
"public-status.js": 14, "public-status.js": 14,
"ticket-order.js": 22, "ticket-order.js": 23,
"ticket-search.js": 12, "ticket-search.js": 12,
"ticket-route.js": 3, "ticket-route.js": 3,
"index.js": 5, "index.js": 5,
+1 -1
View File
@@ -158,7 +158,7 @@
</div> </div>
<script src="/custom-dialog.js?v=11"></script> <script src="/custom-dialog.js?v=11"></script>
<script src="/ticket-order.js?v=22"></script> <script src="/ticket-order.js?v=23"></script>
<script src="/public-status.js?v=14"></script> <script src="/public-status.js?v=14"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
+38 -60
View File
@@ -236,20 +236,20 @@
} }
} }
const primaryLineByStation = {}; const hubYByCanonical = {};
for (const [c, arr] of linesByStation.entries()) { for (const canonical of Object.keys(lineIdxsByCanonical)) {
const uniq = Array.from(new Set(arr)).sort((a,b)=>a-b); const idxs = Array.from(lineIdxsByCanonical[canonical] || []).sort((a, b) => a - b);
if (uniq.length === 0) continue; if (!idxs.length) continue;
primaryLineByStation[c] = uniq[0]; const ys = idxs.map(idx => baseY + idx * lineGapY);
hubYByCanonical[canonical] = ys.reduce((sum, y) => sum + y, 0) / ys.length;
} }
stationYByCanonical = {}; stationYByCanonical = {};
for (const li of lineStops) { for (const li of lineStops) {
const yNode = baseY + li.idx * lineGapY; const yNode = baseY + li.idx * lineGapY;
for (const c of li.stops) { for (const c of li.stops) {
if (stationYByCanonical[c] == null) { const canonical = stationCanonicalByCode[c] || c;
stationYByCanonical[c] = yNode; stationYByCanonical[c] = hubYByCanonical[canonical] ?? yNode;
}
} }
} }
@@ -270,7 +270,7 @@
const canonical = stationCanonicalByCode[code] || code; const canonical = stationCanonicalByCode[code] || code;
return { return {
x: stationXByCanonical[canonical] || 0, x: stationXByCanonical[canonical] || 0,
y: yLine y: stationYByCanonical[code] ?? yLine
}; };
}); });
@@ -282,68 +282,46 @@
} }
} }
const canonicalYs = {}; const sortedStations = Object.keys(stationXByCanonical).sort((a, b) => stationXByCanonical[a] - stationXByCanonical[b]);
for (const li of lineStops) { for (let i = 0; i < sortedStations.length; i++) {
const yNode = baseY + li.idx * lineGapY; const canonical = sortedStations[i];
for (const c of li.stops) {
const canonical = stationCanonicalByCode[c] || c;
if (!canonicalYs[canonical]) canonicalYs[canonical] = [];
// Only add if we haven't added this Y for this canonical yet
if (!canonicalYs[canonical].some(n => n.y === yNode)) {
canonicalYs[canonical].push({ y: yNode, color: li.color, code: c });
}
}
}
for (const canonical of Object.keys(canonicalYs)) {
const x = stationXByCanonical[canonical]; const x = stationXByCanonical[canonical];
const nodes = canonicalYs[canonical]; const codes = stationCodesByCanonical[canonical] || [canonical];
const ys = nodes.map(n => n.y); const lineIdxs = Array.from(lineIdxsByCanonical[canonical] || []).sort((a, b) => a - b);
const minY = Math.min(...ys); const colors = lineIdxs.map(idx => lineColors[idx]).filter(Boolean);
const maxY = Math.max(...ys); const uniqueColors = Array.from(new Set(colors));
const isTransfer = nodes.length >= 2 || stationCodesByCanonical[canonical]?.length >= 2; const primaryCode = codes[0];
const y = hubYByCanonical[canonical] ?? stationYByCanonical[primaryCode] ?? baseY;
const isTransfer = uniqueColors.length >= 2 || codes.length >= 2;
const name = getStationDisplayName(primaryCode);
// Vertical pill let markerSvg = '';
if (maxY > minY) { if (isTransfer) {
svgContent += `<rect x="${x - 6}" y="${minY + 12}" width="12" height="${Math.max(0, maxY - minY - 24)}" rx="6" fill="#f8fafc" stroke="#94a3b8" stroke-width="2" />`; markerSvg += `<circle cx="${x}" cy="${y}" r="15" fill="#ffffff" stroke="#1f2937" stroke-width="2.5" />`;
markerSvg += pieSvg(x, y, 11.5, uniqueColors);
markerSvg += `<circle class="node-core" cx="${x}" cy="${y}" r="6.5" fill="#ffffff" stroke="#334155" stroke-width="2.25" />`;
} else {
const color = uniqueColors[0] || '#93a2b7';
markerSvg += `<circle cx="${x}" cy="${y}" r="12.5" fill="#ffffff" stroke="${color}" stroke-width="5" />`;
markerSvg += `<circle class="node-core" cx="${x}" cy="${y}" r="7" fill="#ffffff" stroke="#334155" stroke-width="2.25" />`;
} }
const primaryCode = nodes[0].code; const preferTop = isTransfer || (i % 2 === 0 && y > baseY + 20);
const name = stationNameByCode[primaryCode] || primaryCode; const labelY = preferTop ? (y - 22) : (y + 30);
const labelClass = preferTop ? 'station-label-top' : 'station-label-bottom';
// Nodes
let nodesSvg = '';
for (const node of nodes) {
const rOuter = isTransfer ? 13 : 12;
const outer = `<circle cx="${x}" cy="${node.y}" r="${rOuter}" fill="#ffffff" stroke="${node.color}" stroke-width="5" />`;
const transferFill = isTransfer ? `<circle cx="${x}" cy="${node.y}" r="7" fill="#ffffff" stroke="#475569" stroke-width="2.5" />` : '';
nodesSvg += `
${outer}
${transferFill}
<circle class="node-core" cx="${x}" cy="${node.y}" r="8" fill="#ffffff" stroke="#334155" stroke-width="2.5" />
`;
}
// Label
let labelSvg = ''; let labelSvg = '';
if (isTransfer) { if (isTransfer) {
const labelY = minY - 34; // above the top node const labelW = Math.max(72, name.length * 14 + 18);
const transferName = getStationDisplayName(canonical); labelSvg += `<rect x="${x - labelW / 2}" y="${labelY - 16}" width="${labelW}" height="24" rx="12" fill="rgba(248,250,252,0.96)" stroke="#cbd5e1" stroke-width="1.5" />`;
const transferLabelW = Math.max(72, transferName.length * 14 + 18); labelSvg += `<text class="${labelClass}" x="${x}" y="${labelY}" text-anchor="middle" fill="#334155" font-size="13" font-weight="800">${name}</text>`;
labelSvg += `<rect x="${x - transferLabelW / 2}" y="${labelY - 16}" width="${transferLabelW}" height="24" rx="12" fill="rgba(248,250,252,0.96)" stroke="#cbd5e1" stroke-width="1.5" />`;
labelSvg += `<text x="${x}" y="${labelY}" text-anchor="middle" fill="#334155" font-size="13" font-weight="800">${transferName}</text>`;
} else { } else {
const labelY = (nodes[0].y === baseY || nodes[0].y <= baseY + lineGapY) ? maxY + 28 : maxY + 30; labelSvg += `<text class="${labelClass}" x="${x}" y="${labelY}" text-anchor="middle" fill="#334155" font-size="13.5" font-weight="700">${name}</text>`;
labelSvg += `<text x="${x}" y="${labelY}" text-anchor="middle" fill="#334155" font-size="14" font-weight="700">${name}</text>`;
} }
const hitY = (minY + maxY) / 2;
const hitHeight = Math.max(30, maxY - minY + 30);
svgContent += ` svgContent += `
<g class="map-station ${isTransfer ? 'transfer' : ''}" data-code="${primaryCode}" data-canonical="${canonical}" onclick="handleStationClick('${primaryCode}')" style="cursor:pointer"> <g class="map-station ${isTransfer ? 'transfer' : ''}" data-code="${primaryCode}" data-canonical="${canonical}" onclick="handleStationClick('${primaryCode}')" style="cursor:pointer">
<rect x="${x - 20}" y="${hitY - hitHeight/2}" width="40" height="${hitHeight}" fill="transparent" /> <rect x="${x - 20}" y="${y - 20}" width="40" height="40" fill="transparent" />
${nodesSvg} ${markerSvg}
${labelSvg} ${labelSvg}
</g> </g>
`; `;