style: 优化 TVM 购票地图,缩小元素尺寸,增加切换动画,适配移动端

This commit is contained in:
2026-07-16 22:16:10 +08:00
parent cfaa622dcd
commit 9d145c3d16
+59 -45
View File
@@ -92,44 +92,44 @@
let markerSvg = ''; let markerSvg = '';
if (isTransfer) { if (isTransfer) {
markerSvg += `<circle cx="${x}" cy="${y}" r="15" fill="#ffffff" stroke="#1f2937" stroke-width="2.5" />`; markerSvg += `<circle cx="${x}" cy="${y}" r="12" fill="#ffffff" stroke="#1f2937" stroke-width="2" />`;
markerSvg += pieSvg(x, y, 11.5, uniqueColors); markerSvg += pieSvg(x, y, 9, uniqueColors);
markerSvg += `<circle class="node-core" cx="${x}" cy="${y}" r="6.5" fill="#ffffff" stroke="#334155" stroke-width="2.25" />`; markerSvg += `<circle class="node-core" cx="${x}" cy="${y}" r="5" fill="#ffffff" stroke="#334155" stroke-width="2" />`;
} else { } else {
const color = forceColor || uniqueColors[0] || '#93a2b7'; const color = forceColor || uniqueColors[0] || '#93a2b7';
markerSvg += `<circle cx="${x}" cy="${y}" r="12.5" fill="${color}" stroke="${color}" stroke-width="4" />`; markerSvg += `<circle cx="${x}" cy="${y}" r="10" fill="${color}" stroke="${color}" stroke-width="2.5" />`;
markerSvg += `<circle class="node-core" cx="${x}" cy="${y}" r="7" fill="#ffffff" stroke="${color}" stroke-width="2.25" />`; markerSvg += `<circle class="node-core" cx="${x}" cy="${y}" r="5" fill="#ffffff" stroke="${color}" stroke-width="2" />`;
} }
const preferTop = i % 2 === 0; const preferTop = i % 2 === 0;
let labelSvg = ''; let labelSvg = '';
const codeFontSize = 9; const codeFontSize = 8;
const nameFontSize = 13; const nameFontSize = 11.5;
const enFontSize = 8.5; const enFontSize = 7.5;
const codeW = primaryCode.length * 6 + 10; const codeW = primaryCode.length * 5.5 + 8;
let textContent = ''; let textContent = '';
if (preferTop) { if (preferTop) {
const bottomY = y - 22; const bottomY = y - 18;
const enY = bottomY; const enY = bottomY;
const nameY = showEn ? bottomY - 12 : bottomY; const nameY = showEn ? bottomY - 10 : bottomY;
const codeY = nameY - 18; const codeY = nameY - 13;
textContent += `<rect x="${x - codeW / 2}" y="${codeY - 8}" width="${codeW}" height="12" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1.5" />`; textContent += `<rect x="${x - codeW / 2}" y="${codeY - 7}" width="${codeW}" height="10" rx="5" fill="#ffffff" stroke="#cbd5e1" stroke-width="1" />`;
textContent += `<text x="${x}" y="${codeY + 1}" text-anchor="middle" fill="#64748b" font-size="${codeFontSize}" font-weight="700" letter-spacing="0.02em">${primaryCode}</text>`; textContent += `<text x="${x}" y="${codeY + 1.5}" text-anchor="middle" fill="#64748b" font-size="${codeFontSize}" font-weight="700" letter-spacing="0.02em">${primaryCode}</text>`;
textContent += `<text x="${x}" y="${nameY}" text-anchor="middle" fill="#0f172a" font-size="${nameFontSize}" font-weight="800" stroke="#ffffff" stroke-width="4" stroke-linejoin="round" paint-order="stroke fill">${name}</text>`; textContent += `<text x="${x}" y="${nameY}" text-anchor="middle" fill="#0f172a" font-size="${nameFontSize}" font-weight="800" stroke="#ffffff" stroke-width="3" stroke-linejoin="round" paint-order="stroke fill">${name}</text>`;
if (showEn) { if (showEn) {
textContent += `<text x="${x}" y="${enY}" text-anchor="middle" fill="#64748b" font-size="${enFontSize}" font-weight="700" stroke="#ffffff" stroke-width="3" stroke-linejoin="round" paint-order="stroke fill">${enName}</text>`; textContent += `<text x="${x}" y="${enY}" text-anchor="middle" fill="#64748b" font-size="${enFontSize}" font-weight="700" stroke="#ffffff" stroke-width="2.5" stroke-linejoin="round" paint-order="stroke fill">${enName}</text>`;
} }
} else { } else {
const topY = y + 34; const topY = y + 26;
const codeY = topY; const codeY = topY;
const nameY = topY + 18; const nameY = topY + 13;
const enY = nameY + 12; const enY = nameY + 10;
textContent += `<rect x="${x - codeW / 2}" y="${codeY - 8}" width="${codeW}" height="12" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1.5" />`; textContent += `<rect x="${x - codeW / 2}" y="${codeY - 7}" width="${codeW}" height="10" rx="5" fill="#ffffff" stroke="#cbd5e1" stroke-width="1" />`;
textContent += `<text x="${x}" y="${codeY + 1}" text-anchor="middle" fill="#64748b" font-size="${codeFontSize}" font-weight="700" letter-spacing="0.02em">${primaryCode}</text>`; textContent += `<text x="${x}" y="${codeY + 1.5}" text-anchor="middle" fill="#64748b" font-size="${codeFontSize}" font-weight="700" letter-spacing="0.02em">${primaryCode}</text>`;
textContent += `<text x="${x}" y="${nameY}" text-anchor="middle" fill="#0f172a" font-size="${nameFontSize}" font-weight="800" stroke="#ffffff" stroke-width="4" stroke-linejoin="round" paint-order="stroke fill">${name}</text>`; textContent += `<text x="${x}" y="${nameY}" text-anchor="middle" fill="#0f172a" font-size="${nameFontSize}" font-weight="800" stroke="#ffffff" stroke-width="3" stroke-linejoin="round" paint-order="stroke fill">${name}</text>`;
if (showEn) { if (showEn) {
textContent += `<text x="${x}" y="${enY}" text-anchor="middle" fill="#64748b" font-size="${enFontSize}" font-weight="700" stroke="#ffffff" stroke-width="3" stroke-linejoin="round" paint-order="stroke fill">${enName}</text>`; textContent += `<text x="${x}" y="${enY}" text-anchor="middle" fill="#64748b" font-size="${enFontSize}" font-weight="700" stroke="#ffffff" stroke-width="2.5" stroke-linejoin="round" paint-order="stroke fill">${enName}</text>`;
} }
} }
labelSvg += textContent; labelSvg += textContent;
@@ -143,7 +143,7 @@
return ` return `
<g class="${classList}" data-code="${primaryCode}" data-canonical="${canonical}" onclick="handleStationClick('${primaryCode}')" style="cursor:pointer"> <g class="${classList}" data-code="${primaryCode}" data-canonical="${canonical}" onclick="handleStationClick('${primaryCode}')" style="cursor:pointer">
<rect x="${x - 20}" y="${y - 20}" width="40" height="40" fill="transparent" /> <rect x="${x - 16}" y="${y - 16}" width="32" height="32" fill="transparent" />
${markerSvg} ${markerSvg}
${labelSvg} ${labelSvg}
</g>`; </g>`;
@@ -159,7 +159,7 @@
const bg = isActive ? li.color : '#f8fafc'; const bg = isActive ? li.color : '#f8fafc';
const fg = isActive ? getContrastColor(li.color) : '#334155'; const fg = isActive ? getContrastColor(li.color) : '#334155';
const border = isActive ? li.color : '#cbd5e1'; const border = isActive ? li.color : '#cbd5e1';
html += `<button class="tvm-line-btn" style="padding: 8px 16px; border: 2px solid ${border}; background: ${bg}; color: ${fg}; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 14px; transition: all 0.2s;" onclick="selectTvmLine(${i})">${li.name}</button>`; html += `<button class="tvm-line-btn" style="padding: 6px 12px; border: 1.5px solid ${border}; background: ${bg}; color: ${fg}; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 13px; transition: all 0.25s ease;" onclick="selectTvmLine(${i})">${li.name}</button>`;
} }
container.innerHTML = html; container.innerHTML = html;
} }
@@ -179,9 +179,9 @@
return; return;
} }
const startX = 80; const startX = 40;
const gapX = 90; const gapX = 65;
const y = 80; const y = 60;
const pts = []; const pts = [];
for (let i = 0; i < li.stops.length; i++) { for (let i = 0; i < li.stops.length; i++) {
@@ -201,16 +201,17 @@
if (pts.length >= 2) { if (pts.length >= 2) {
let d = `M ${pts[0].x} ${y}`; let d = `M ${pts[0].x} ${y}`;
for(let i=1; i<pts.length; i++) d += ` L ${pts[i].x} ${y}`; for(let i=1; i<pts.length; i++) d += ` L ${pts[i].x} ${y}`;
svgContent += `<path d="${d}" fill="none" stroke="${li.color}" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" />`; svgContent += `<path d="${d}" fill="none" stroke="${li.color}" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" />`;
} }
for (let i = 0; i < pts.length; i++) { for (let i = 0; i < pts.length; i++) {
svgContent += renderStationNode(pts[i].x, pts[i].y, pts[i].code, i, li.color); svgContent += renderStationNode(pts[i].x, pts[i].y, pts[i].code, i, li.color);
} }
const width = startX + pts.length * gapX + 80; const width = startX + pts.length * gapX + 40;
const height = 160; const height = 120;
container.innerHTML = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" style="display:block; margin:auto;">${svgContent}</svg>`; // 添加 tvm-animate 类实现动画过渡
container.innerHTML = `<svg class="tvm-animate" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" style="display:block;">${svgContent}</svg>`;
} }
function renderTvmRoute() { function renderTvmRoute() {
@@ -236,9 +237,9 @@
container.style.display = 'flex'; container.style.display = 'flex';
if (title) title.style.display = 'block'; if (title) title.style.display = 'block';
const startX = 80; const startX = 40;
const gapX = 90; const gapX = 65;
const y = 80; const y = 60;
let svgContent = ''; let svgContent = '';
@@ -265,7 +266,7 @@
} }
for (const seg of segments) { for (const seg of segments) {
svgContent += `<line x1="${seg.x1}" y1="${y}" x2="${seg.x2}" y2="${y}" stroke="${seg.color}" stroke-width="8" stroke-linecap="round" />`; svgContent += `<line x1="${seg.x1}" y1="${y}" x2="${seg.x2}" y2="${y}" stroke="${seg.color}" stroke-width="6" stroke-linecap="round" />`;
} }
const pathD = `M ${startX} ${y} L ${startX + (routeCodes.length - 1) * gapX} ${y}`; const pathD = `M ${startX} ${y} L ${startX + (routeCodes.length - 1) * gapX} ${y}`;
@@ -277,7 +278,7 @@
for (let i = 0; i < numArrows; i++) { for (let i = 0; i < numArrows; i++) {
svgContent += ` svgContent += `
<g> <g>
<path d="M -6 -5 L 4 0 L -6 5" fill="none" stroke="#854d0e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" /> <path d="M -5 -4 L 3 0 L -5 4" fill="none" stroke="#854d0e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<animateMotion dur="${duration}s" repeatCount="indefinite" path="${pathD}" rotate="auto" begin="-${i * interval}s" calcMode="linear" /> <animateMotion dur="${duration}s" repeatCount="indefinite" path="${pathD}" rotate="auto" begin="-${i * interval}s" calcMode="linear" />
</g> </g>
`; `;
@@ -287,9 +288,9 @@
svgContent += renderStationNode(startX + i * gapX, y, routeCodes[i], i); svgContent += renderStationNode(startX + i * gapX, y, routeCodes[i], i);
} }
const width = startX + routeCodes.length * gapX + 80; const width = startX + routeCodes.length * gapX + 40;
const height = 160; const height = 120;
container.innerHTML = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" style="display:block; margin:auto;">${svgContent}</svg>`; container.innerHTML = `<svg class="tvm-animate" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" style="display:block;">${svgContent}</svg>`;
} }
async function renderSystemMap() { async function renderSystemMap() {
@@ -383,12 +384,25 @@
if (uniq.length >= 2) stationTransfer.add(c); if (uniq.length >= 2) stationTransfer.add(c);
} }
const fadeStyle = `<style>
@keyframes tvmFadeIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
.tvm-animate { animation: tvmFadeIn 0.3s ease-out forwards; }
.tvm-scroll-container { scroll-behavior: smooth; -webkit-overflow-scrolling: touch; }
.tvm-scroll-container::-webkit-scrollbar { height: 4px; }
.tvm-scroll-container::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 2px; }
.tvm-scroll-container::-webkit-scrollbar-track { background: transparent; }
</style>`;
mapContainer.innerHTML = ` mapContainer.innerHTML = `
<div class="tvm-layout" style="display: flex; flex-direction: column; gap: 20px; width: 100%;"> ${fadeStyle}
<div class="tvm-lines" id="tvmLines" style="display: flex; flex-wrap: wrap; gap: 8px;"></div> <div class="tvm-layout" style="display: flex; flex-direction: column; gap: 16px; width: 100%;">
<div class="tvm-single-line" id="tvmSingleLine" style="overflow-x: auto; padding: 10px 0; min-height: 160px; display: flex; align-items: center; justify-content: flex-start;"></div> <div class="tvm-lines" id="tvmLines" style="display: flex; flex-wrap: wrap; gap: 6px;"></div>
<div class="tvm-route-title" style="font-weight: bold; margin-bottom: -10px; color: #475569; display: none; font-size: 16px;" id="tvmRouteTitle">已选路径</div> <div class="tvm-single-line tvm-scroll-container" id="tvmSingleLine" style="overflow-x: auto; padding: 10px 0; min-height: 120px; display: flex; align-items: center; justify-content: flex-start;"></div>
<div class="tvm-route" id="tvmRoute" style="overflow-x: auto; padding: 10px 0; border-top: 2px dashed #cbd5e1; min-height: 160px; display: none; align-items: center; justify-content: flex-start;"></div> <div class="tvm-route-title" style="font-weight: bold; margin-bottom: -12px; color: #475569; display: none; font-size: 14px;" id="tvmRouteTitle">已选路径</div>
<div class="tvm-route tvm-scroll-container" id="tvmRoute" style="overflow-x: auto; padding: 10px 0; border-top: 1.5px dashed #cbd5e1; min-height: 120px; display: none; align-items: center; justify-content: flex-start;"></div>
</div> </div>
`; `;