refactor(web): 优化订票线路图布局与换乘站视觉表现
This commit is contained in:
+46
-13
@@ -118,6 +118,7 @@
|
||||
const lineStops = [];
|
||||
const lineColors = [];
|
||||
const linesByStation = new Map();
|
||||
const lineIdxsByCanonical = {};
|
||||
|
||||
// NEW logic for breaking lines with '-'
|
||||
let currentLineIdx = 0;
|
||||
@@ -139,6 +140,9 @@
|
||||
const arr = linesByStation.get(stopCode) || [];
|
||||
arr.push(currentLineIdx);
|
||||
linesByStation.set(stopCode, arr);
|
||||
const canonical = stationCanonicalByCode[stopCode] || stopCode;
|
||||
if (!lineIdxsByCanonical[canonical]) lineIdxsByCanonical[canonical] = new Set();
|
||||
lineIdxsByCanonical[canonical].add(currentLineIdx);
|
||||
}
|
||||
currentSegment = [];
|
||||
addedAny = true;
|
||||
@@ -154,6 +158,9 @@
|
||||
const arr = linesByStation.get(stopCode) || [];
|
||||
arr.push(currentLineIdx);
|
||||
linesByStation.set(stopCode, arr);
|
||||
const canonical = stationCanonicalByCode[stopCode] || stopCode;
|
||||
if (!lineIdxsByCanonical[canonical]) lineIdxsByCanonical[canonical] = new Set();
|
||||
lineIdxsByCanonical[canonical].add(currentLineIdx);
|
||||
}
|
||||
addedAny = true;
|
||||
}
|
||||
@@ -192,6 +199,11 @@
|
||||
occurrences[canonical].push(i * minGapX);
|
||||
}
|
||||
}
|
||||
const stationDegreeByCanonical = {};
|
||||
for (const canonical of Object.keys(lineIdxsByCanonical)) {
|
||||
stationDegreeByCanonical[canonical] = lineIdxsByCanonical[canonical].size;
|
||||
}
|
||||
|
||||
stationXByCanonical = {};
|
||||
for (const canonical of Object.keys(occurrences)) {
|
||||
const arr = occurrences[canonical];
|
||||
@@ -199,6 +211,16 @@
|
||||
stationXByCanonical[canonical] = startX + Math.round(avg);
|
||||
}
|
||||
|
||||
const sortedCanonicals = Object.keys(stationXByCanonical).sort((a, b) => stationXByCanonical[a] - stationXByCanonical[b]);
|
||||
for (let i = 1; i < sortedCanonicals.length; i++) {
|
||||
const prev = sortedCanonicals[i - 1];
|
||||
const cur = sortedCanonicals[i];
|
||||
const minSpacing = (stationDegreeByCanonical[prev] >= 2 || stationDegreeByCanonical[cur] >= 2) ? 108 : 82;
|
||||
if (stationXByCanonical[cur] < stationXByCanonical[prev] + minSpacing) {
|
||||
stationXByCanonical[cur] = stationXByCanonical[prev] + minSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
for (let pass = 0; pass < 3; pass++) {
|
||||
for (const li of lineStops) {
|
||||
let prevX = startX - minGapX;
|
||||
@@ -206,7 +228,8 @@
|
||||
if (code === '-') continue;
|
||||
const canonical = stationCanonicalByCode[code] || code;
|
||||
const x = stationXByCanonical[canonical] ?? (prevX + minGapX);
|
||||
const nx = Math.max(x, prevX + minGapX);
|
||||
const spacing = stationDegreeByCanonical[canonical] >= 2 ? 104 : minGapX;
|
||||
const nx = Math.max(x, prevX + spacing);
|
||||
if (stationXByCanonical[canonical] == null || nx > stationXByCanonical[canonical]) stationXByCanonical[canonical] = nx;
|
||||
prevX = stationXByCanonical[canonical];
|
||||
}
|
||||
@@ -282,7 +305,7 @@
|
||||
|
||||
// Vertical pill
|
||||
if (maxY > minY) {
|
||||
svgContent += `<rect x="${x - 8}" y="${minY}" width="16" height="${maxY - minY}" rx="8" fill="#ffffff" stroke="#111827" stroke-width="3" />`;
|
||||
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" />`;
|
||||
}
|
||||
|
||||
const primaryCode = nodes[0].code;
|
||||
@@ -293,11 +316,11 @@
|
||||
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="#111827" stroke-width="3" />` : '';
|
||||
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="#111827" stroke-width="3" />
|
||||
<circle class="node-core" cx="${x}" cy="${node.y}" r="8" fill="#ffffff" stroke="#334155" stroke-width="2.5" />
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -305,18 +328,20 @@
|
||||
let labelSvg = '';
|
||||
if (isTransfer) {
|
||||
const labelY = minY - 34; // above the top node
|
||||
labelSvg += `<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" />`;
|
||||
labelSvg += `<text x="${x}" y="${labelY}" text-anchor="middle" fill="#334155" font-size="13" font-weight="800">${getStationDisplayName(canonical)}</text>`;
|
||||
const transferName = getStationDisplayName(canonical);
|
||||
const transferLabelW = Math.max(72, transferName.length * 14 + 18);
|
||||
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 {
|
||||
const labelY = maxY + 28; // below the single node
|
||||
labelSvg += `<text x="${x}" y="${labelY}" text-anchor="middle" fill="#e5e7eb" font-size="14" font-weight="700">${name}</text>`;
|
||||
const labelY = (nodes[0].y === baseY || nodes[0].y <= baseY + lineGapY) ? maxY + 28 : maxY + 30;
|
||||
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 += `
|
||||
<g class="map-station ${isTransfer ? 'transfer' : ''}" data-code="${primaryCode}" 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" />
|
||||
${nodesSvg}
|
||||
${labelSvg}
|
||||
@@ -470,6 +495,13 @@
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function getMapStationEl(code) {
|
||||
const normalized = String(code || '').trim();
|
||||
if (!normalized) return null;
|
||||
const canonical = stationCanonicalByCode[normalized] || normalized;
|
||||
return document.querySelector(`.map-station[data-canonical="${canonical}"]`);
|
||||
}
|
||||
|
||||
function renderRouteOverlay() {
|
||||
const svgEl = mapContainer.querySelector('svg');
|
||||
if (!svgEl) return;
|
||||
@@ -569,20 +601,21 @@
|
||||
// Update Map Styles
|
||||
document.querySelectorAll('.map-station').forEach(el => {
|
||||
const code = el.getAttribute('data-code');
|
||||
const canonical = el.getAttribute('data-canonical');
|
||||
el.classList.remove('start', 'end', 'selected', 'route', 'route-transfer');
|
||||
if(code === selection[0]) el.classList.add('start');
|
||||
if(code === selection[1]) el.classList.add('end');
|
||||
if (selection[0] && canonical === (stationCanonicalByCode[selection[0]] || selection[0])) el.classList.add('start');
|
||||
if (selection[1] && canonical === (stationCanonicalByCode[selection[1]] || selection[1])) el.classList.add('end');
|
||||
});
|
||||
|
||||
if (Array.isArray(currentRoute) && currentRoute.length > 0) {
|
||||
for (const c of currentRoute) {
|
||||
const el = document.querySelector(`.map-station[data-code="${c}"]`);
|
||||
const el = getMapStationEl(c);
|
||||
if (el) el.classList.add('route');
|
||||
}
|
||||
}
|
||||
if (Array.isArray(currentRouteTransfers) && currentRouteTransfers.length > 0) {
|
||||
for (const c of currentRouteTransfers) {
|
||||
const el = document.querySelector(`.map-station[data-code="${c}"]`);
|
||||
const el = getMapStationEl(c);
|
||||
if (el) el.classList.add('route-transfer');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user