From cfaa622dcdfce3da7c5d60805b303d012bad1c39 Mon Sep 17 00:00:00 2001
From: HenryDu8133 <813367384@qq.com>
Date: Thu, 16 Jul 2026 06:05:33 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E9=80=89=E6=8B=A9?=
=?UTF-8?q?=E8=B4=AD=E7=A5=A8=E5=8C=BA=E9=97=B4=E5=9C=B0=E5=9B=BE=E4=B8=BA?=
=?UTF-8?q?=20TVM=20=E7=80=91=E5=B8=83=E6=B5=81=E6=A0=B7=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
web/ticket-order.js | 754 +++++++++++++++-----------------------------
1 file changed, 246 insertions(+), 508 deletions(-)
diff --git a/web/ticket-order.js b/web/ticket-order.js
index 4d68825..e2d740a 100644
--- a/web/ticket-order.js
+++ b/web/ticket-order.js
@@ -27,7 +27,6 @@
}
};
- // Fetch and Render Map
const mapContainer = $('#stationMap');
let selection = [null, null];
let currentRoute = [];
@@ -36,10 +35,14 @@
let stationEnByCode = {};
let stationCanonicalByCode = {};
let stationCodesByCanonical = {};
- let stationXByCanonical = {};
- let stationYByCanonical = {};
let stationTransfer = new Set();
+ let lineStops = [];
+ let lineColors = [];
+ let linesByStation = new Map();
+ let lineIdxsByCanonical = {};
+ let activeLineIdx = 0;
+
const piePath = (cx, cy, r, a0, a1) => {
const x0 = cx + r * Math.cos(a0);
const y0 = cy + r * Math.sin(a0);
@@ -63,159 +66,232 @@
return out;
};
- const buildTransitPath = (points, radius = 12, options = {}) => {
- const src = Array.isArray(points) ? points.filter(Boolean) : [];
- if (src.length === 0) return '';
- if (src.length === 1) return `M ${src[0].x} ${src[0].y}`;
- const laneOffset = Number(options?.laneOffset) || 0;
-
- const fmt = (n) => {
- const v = Number(n);
- if (!Number.isFinite(v)) return '0';
- return String(Math.round(v * 10) / 10);
- };
- const eq = (a, b) => Math.abs(a.x - b.x) < 0.01 && Math.abs(a.y - b.y) < 0.01;
- const push = (arr, p) => {
- const x = Number(p?.x);
- const y = Number(p?.y);
- if (!Number.isFinite(x) || !Number.isFinite(y)) return;
- const item = { x, y, code: p?.code ? String(p.code).trim() : '' };
- if (arr.length && eq(arr[arr.length - 1], item)) return;
- arr.push(item);
- };
- const isTransfer = (p) => {
- const code = String(p?.code || '').trim();
- return !!code && stationTransfer && stationTransfer.has(code);
- };
- const resolveLaneBendX = (from, to) => {
- if (!laneOffset) return null;
- const fromTransfer = isTransfer(from);
- const toTransfer = isTransfer(to);
- if (!fromTransfer && !toTransfer) return null;
-
- const minX = Math.min(from.x, to.x);
- const maxX = Math.max(from.x, to.x);
- if ((maxX - minX) < 28) return null;
-
- const anchorX = toTransfer && !fromTransfer
- ? to.x
- : (fromTransfer && !toTransfer ? from.x : ((from.x + to.x) / 2));
- const candidate = Math.max(minX + 12, Math.min(maxX - 12, anchorX + laneOffset));
- if (Math.abs(candidate - from.x) < 4 || Math.abs(candidate - to.x) < 4) return null;
- return candidate;
- };
- const simplify = (arr) => {
- const out = [];
- for (const p of arr) push(out, p);
- let changed = true;
- while (changed) {
- changed = false;
- for (let i = 1; i < out.length - 1; i++) {
- const a = out[i - 1];
- const b = out[i];
- const c = out[i + 1];
- if ((a.x === b.x && b.x === c.x) || (a.y === b.y && b.y === c.y)) {
- out.splice(i, 1);
- changed = true;
- break;
- }
- }
- }
- return out;
- };
- const orth = [];
- push(orth, src[0]);
- for (let i = 1; i < src.length; i++) {
- const prev = orth[orth.length - 1];
- const next = src[i];
- const x2 = Number(next?.x);
- const y2 = Number(next?.y);
- if (!Number.isFinite(x2) || !Number.isFinite(y2)) continue;
-
- if (prev.x === x2 || prev.y === y2) {
- push(orth, next);
- continue;
- }
-
- const aT = isTransfer(prev);
- const bT = isTransfer(next);
- const bendX = resolveLaneBendX(prev, next);
- if (Number.isFinite(bendX)) {
- push(orth, { x: bendX, y: prev.y, code: prev?.code || next?.code });
- push(orth, { x: bendX, y: y2, code: next?.code || prev?.code });
- } else if (bT && !aT) {
- push(orth, { x: x2, y: prev.y, code: next?.code });
- } else if (aT && !bT) {
- push(orth, { x: prev.x, y: y2, code: prev?.code });
- } else {
- push(orth, { x: x2, y: prev.y, code: next?.code });
- }
- push(orth, next);
- }
-
- const pts = simplify(orth);
- if (pts.length === 0) return '';
- if (pts.length === 1) return `M ${fmt(pts[0].x)} ${fmt(pts[0].y)}`;
- if (radius <= 0 || pts.length === 2) {
- let d = `M ${fmt(pts[0].x)} ${fmt(pts[0].y)}`;
- for (let i = 1; i < pts.length; i++) d += ` L ${fmt(pts[i].x)} ${fmt(pts[i].y)}`;
- return d;
- }
-
- const rBase = Math.max(0, Number(radius) || 0);
- let d = `M ${fmt(pts[0].x)} ${fmt(pts[0].y)}`;
- for (let i = 1; i < pts.length - 1; i++) {
- const a = pts[i - 1];
- const b = pts[i];
- const c = pts[i + 1];
- const v1x = b.x - a.x;
- const v1y = b.y - a.y;
- const v2x = c.x - b.x;
- const v2y = c.y - b.y;
- const len1 = Math.hypot(v1x, v1y);
- const len2 = Math.hypot(v2x, v2y);
- if (!len1 || !len2) continue;
- const r = Math.min(rBase, len1 / 2, len2 / 2);
- const p1 = { x: b.x - (v1x / len1) * r, y: b.y - (v1y / len1) * r };
- const p2 = { x: b.x + (v2x / len2) * r, y: b.y + (v2y / len2) * r };
- d += ` L ${fmt(p1.x)} ${fmt(p1.y)} Q ${fmt(b.x)} ${fmt(b.y)} ${fmt(p2.x)} ${fmt(p2.y)}`;
- }
- d += ` L ${fmt(pts[pts.length - 1].x)} ${fmt(pts[pts.length - 1].y)}`;
- return d;
+ const getContrastColor = (hexcolor) => {
+ if (!hexcolor) return '#ffffff';
+ let hex = hexcolor.replace('#', '');
+ if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
+ const r = parseInt(hex.substr(0,2),16) || 0;
+ const g = parseInt(hex.substr(2,2),16) || 0;
+ const b = parseInt(hex.substr(4,2),16) || 0;
+ const yiq = ((r*299)+(g*587)+(b*114))/1000;
+ return (yiq >= 140) ? '#0f172a' : '#ffffff';
};
- async function loadMap() {
- 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 = '
加载地图失败
';
+ function renderStationNode(x, y, code, i, forceColor = null) {
+ const canonical = stationCanonicalByCode[code] || code;
+ const codes = stationCodesByCanonical[canonical] || [canonical];
+ const lineIdxs = Array.from(lineIdxsByCanonical[canonical] || []).sort((a, b) => a - b);
+ const colors = lineIdxs.map(idx => lineColors[idx]).filter(Boolean);
+ const uniqueColors = Array.from(new Set(colors));
+ const primaryCode = codes[0];
+
+ const isTransfer = uniqueColors.length >= 2 || codes.length >= 2;
+ const name = getStationDisplayName(primaryCode);
+ const enName = stationEnByCode[primaryCode] || '';
+ const showEn = !!enName;
+
+ let markerSvg = '';
+ if (isTransfer) {
+ markerSvg += ``;
+ markerSvg += pieSvg(x, y, 11.5, uniqueColors);
+ markerSvg += ``;
+ } else {
+ const color = forceColor || uniqueColors[0] || '#93a2b7';
+ markerSvg += ``;
+ markerSvg += ``;
}
+
+ const preferTop = i % 2 === 0;
+ let labelSvg = '';
+ const codeFontSize = 9;
+ const nameFontSize = 13;
+ const enFontSize = 8.5;
+ const codeW = primaryCode.length * 6 + 10;
+ let textContent = '';
+
+ if (preferTop) {
+ const bottomY = y - 22;
+ const enY = bottomY;
+ const nameY = showEn ? bottomY - 12 : bottomY;
+ const codeY = nameY - 18;
+ textContent += ``;
+ textContent += `${primaryCode}`;
+ textContent += `${name}`;
+ if (showEn) {
+ textContent += `${enName}`;
+ }
+ } else {
+ const topY = y + 34;
+ const codeY = topY;
+ const nameY = topY + 18;
+ const enY = nameY + 12;
+ textContent += ``;
+ textContent += `${primaryCode}`;
+ textContent += `${name}`;
+ if (showEn) {
+ textContent += `${enName}`;
+ }
+ }
+ labelSvg += textContent;
+
+ let classList = 'map-station';
+ if (isTransfer) classList += ' transfer';
+ if (selection[0] && canonical === (stationCanonicalByCode[selection[0]] || selection[0])) classList += ' start';
+ if (selection[1] && canonical === (stationCanonicalByCode[selection[1]] || selection[1])) classList += ' end';
+ if (Array.isArray(currentRoute) && currentRoute.includes(primaryCode)) classList += ' route';
+ if (Array.isArray(currentRouteTransfers) && currentRouteTransfers.includes(primaryCode)) classList += ' route-transfer';
+
+ return `
+
+
+ ${markerSvg}
+ ${labelSvg}
+ `;
}
- async function renderLineMap() {
- try {
- const [lines, stations] = await Promise.all([
- fetch('/api/public/lines').then(r=>r.json()),
- fetch('/api/public/stations').then(r=>r.json())
- ]);
+ function renderTvmLines() {
+ const container = document.getElementById('tvmLines');
+ if (!container) return;
+ let html = '';
+ for (let i = 0; i < lineStops.length; i++) {
+ const li = lineStops[i];
+ const isActive = (i === activeLineIdx);
+ const bg = isActive ? li.color : '#f8fafc';
+ const fg = isActive ? getContrastColor(li.color) : '#334155';
+ const border = isActive ? li.color : '#cbd5e1';
+ html += ``;
+ }
+ container.innerHTML = html;
+ }
+
+ window.selectTvmLine = (idx) => {
+ activeLineIdx = idx;
+ renderTvmLines();
+ renderTvmSingleLine();
+ };
+
+ function renderTvmSingleLine() {
+ const container = document.getElementById('tvmSingleLine');
+ if (!container) return;
+ const li = lineStops[activeLineIdx];
+ if (!li) {
+ container.innerHTML = '';
+ return;
+ }
+
+ const startX = 80;
+ const gapX = 90;
+ const y = 80;
+ const pts = [];
+
+ for (let i = 0; i < li.stops.length; i++) {
+ const code = li.stops[i];
+ if (code === '-') continue;
+ const x = startX + pts.length * gapX;
+ pts.push({ x, y, code });
+ }
+
+ if (pts.length === 0) {
+ container.innerHTML = '该线路暂无站点
';
+ return;
+ }
+
+ let svgContent = '';
+
+ if (pts.length >= 2) {
+ let d = `M ${pts[0].x} ${y}`;
+ for(let i=1; i`;
+ }
+
+ for (let i = 0; i < pts.length; i++) {
+ svgContent += renderStationNode(pts[i].x, pts[i].y, pts[i].code, i, li.color);
+ }
+
+ const width = startX + pts.length * gapX + 80;
+ const height = 160;
+ container.innerHTML = ``;
+ }
+
+ function renderTvmRoute() {
+ const container = document.getElementById('tvmRoute');
+ const title = document.getElementById('tvmRouteTitle');
+ if (!container) return;
+
+ if (!selection[0] || !selection[1] || !currentRoute || currentRoute.length < 2) {
+ container.style.display = 'none';
+ if (title) title.style.display = 'none';
+ container.innerHTML = '';
+ return;
+ }
+
+ const routeCodes = buildDisplayRouteCodes(currentRoute, selection[0], selection[1]);
+ if (routeCodes.length < 2) {
+ container.style.display = 'none';
+ if (title) title.style.display = 'none';
+ container.innerHTML = '';
+ return;
+ }
+
+ container.style.display = 'flex';
+ if (title) title.style.display = 'block';
+
+ const startX = 80;
+ const gapX = 90;
+ const y = 80;
+
+ let svgContent = '';
+
+ const segments = [];
+ for (let i = 0; i < routeCodes.length - 1; i++) {
+ const c1 = routeCodes[i];
+ const c2 = routeCodes[i+1];
- let html = '';
- lines.forEach(line => {
- const stList = line.站序 || [];
-
+ let color = '#93a2b7';
+ const can1 = stationCanonicalByCode[c1] || c1;
+ const can2 = stationCanonicalByCode[c2] || c2;
+ const canLines1 = Array.from(lineIdxsByCanonical[can1] || []);
+ const canLines2 = Array.from(lineIdxsByCanonical[can2] || []);
+ const canCommon = canLines1.filter(l => canLines2.includes(l));
+ if (canCommon.length > 0) {
+ color = lineColors[canCommon[0]];
+ }
+
+ segments.push({
+ x1: startX + i * gapX,
+ x2: startX + (i + 1) * gapX,
+ color
});
- } catch(e) {}
+ }
+
+ for (const seg of segments) {
+ svgContent += ``;
+ }
+
+ const pathD = `M ${startX} ${y} L ${startX + (routeCodes.length - 1) * gapX} ${y}`;
+ const totalDist = (routeCodes.length - 1) * gapX;
+ const duration = Math.max(1, totalDist / 150);
+ const numArrows = Math.max(1, Math.floor(duration / 0.8));
+ const interval = duration / numArrows;
+
+ for (let i = 0; i < numArrows; i++) {
+ svgContent += `
+
+
+
+
+ `;
+ }
+
+ for (let i = 0; i < routeCodes.length; i++) {
+ svgContent += renderStationNode(startX + i * gapX, y, routeCodes[i], i);
+ }
+
+ const width = startX + routeCodes.length * gapX + 80;
+ const height = 160;
+ container.innerHTML = ``;
}
-
-
async function renderSystemMap() {
mapContainer.innerHTML = '加载线路数据...
';
try {
@@ -244,12 +320,11 @@
if (codes.length >= 2) codes.forEach(code => stationTransfer.add(code));
}
- const lineStops = [];
- const lineColors = [];
- const linesByStation = new Map();
- const lineIdxsByCanonical = {};
+ lineStops = [];
+ lineColors = [];
+ linesByStation = new Map();
+ lineIdxsByCanonical = {};
- // NEW logic for breaking lines with '-'
let currentLineIdx = 0;
for (let i = 0; i < linesData.length; i++) {
const line = linesData[i] || {};
@@ -308,252 +383,36 @@
if (uniq.length >= 2) stationTransfer.add(c);
}
- // Build SVG
- const legendW = 10;
- const startX = 140;
- const baseY = 80;
- const totalLines = currentLineIdx;
- const maxStops = Math.max(2, ...lineStops.map(li => Array.isArray(li.stops) ? li.stops.length : 2));
- let lineGapY = 80;
- if (totalLines >= 14) lineGapY = 66;
- else if (totalLines >= 10) lineGapY = 72;
- lineGapY = Math.max(60, Math.min(84, lineGapY));
- let minGapX = 80;
- if (maxStops >= 24) minGapX = 64;
- else if (maxStops >= 18) minGapX = 72;
- minGapX = Math.max(56, Math.min(96, minGapX));
- const labelStackGap = Math.max(30, Math.round(lineGapY * 0.55));
-
- const occurrences = {};
- for (const li of lineStops) {
- for (let i = 0; i < li.stops.length; i++) {
- const code = li.stops[i];
- if (code === '-') continue;
- 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) {
- if (code === '-') continue;
- 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];
- }
- }
- }
-
- // 3. Assign Grid Y to canonical stations (Average of their lines)
- const hubYByCanonical = {};
- for (const canonical of Object.keys(lineIdxsByCanonical)) {
- const idxs = Array.from(lineIdxsByCanonical[canonical] || []).sort((a, b) => a - b);
- if (!idxs.length) continue;
- const ys = idxs.map(idx => baseY + idx * lineGapY);
- hubYByCanonical[canonical] = ys.reduce((sum, y) => sum + y, 0) / ys.length;
- }
-
- stationYByCanonical = {};
- for (const li of lineStops) {
- const yNode = baseY + li.idx * lineGapY;
- for (const c of li.stops) {
- const canonical = stationCanonicalByCode[c] || c;
- stationYByCanonical[c] = hubYByCanonical[canonical] ?? yNode;
- }
- }
-
- const getContrastColor = (hexcolor) => {
- if (!hexcolor) return '#ffffff';
- let hex = hexcolor.replace('#', '');
- if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
- const r = parseInt(hex.substr(0,2),16) || 0;
- const g = parseInt(hex.substr(2,2),16) || 0;
- const b = parseInt(hex.substr(4,2),16) || 0;
- const yiq = ((r*299)+(g*587)+(b*114))/1000;
- return (yiq >= 140) ? '#0f172a' : '#ffffff';
- };
-
- const labelPlacements = {};
- const getLaneOffset = (lineIdx) => {
- const center = (Math.max(totalLines, 1) - 1) / 2;
- return Math.max(-18, Math.min(18, Math.round((lineIdx - center) * 4)));
- };
- const placeLabel = (x, y, isStart, name, enName, color) => {
- const key = `${x},${y},${isStart ? 'left' : 'right'}`;
- const count = labelPlacements[key] || 0;
- labelPlacements[key] = count + 1;
-
- const textColor = getContrastColor(color);
- const subTextColor = textColor === '#ffffff' ? 'rgba(255,255,255,0.9)' : 'rgba(15,23,42,0.8)';
-
- const hasEn = !!enName;
- const nameLen = Math.max(name.length * 13 + 16, hasEn ? enName.length * 6.5 + 16 : 0);
- const h = hasEn ? 36 : 24;
- const yOffset = count * labelStackGap - (h / 2);
-
- let lx, textX;
- if (isStart) {
- lx = x - 54 - nameLen; // Pushed further out to avoid line overlap
- textX = lx + nameLen / 2;
- } else {
- lx = x + 54; // Pushed further out
- textX = lx + nameLen / 2;
- }
-
- let svg = ``;
- if (hasEn) {
- svg += `${name}`;
- svg += `${enName}`;
- } else {
- svg += `${name}`;
- }
- return svg;
- };
-
- let svgContent = '';
-
- for (const li of lineStops) {
- if (li.stops.length >= 2) {
- const pts = li.stops.map(code => {
- const canonical = stationCanonicalByCode[code] || code;
- return {
- x: stationXByCanonical[canonical] || 0,
- y: stationYByCanonical[code] ?? (baseY + li.idx * lineGapY),
- code
- };
- });
-
- const d = buildTransitPath(pts, 16, { laneOffset: getLaneOffset(li.idx) });
- svgContent += ``;
-
- // Add line name labels at ends
- const pStart = pts[0];
- const pEnd = pts[pts.length - 1];
- const yLabel = baseY + li.idx * lineGapY;
- svgContent += placeLabel(pStart.x, yLabel, true, li.name, li.enName, li.color);
- svgContent += placeLabel(pEnd.x, yLabel, false, li.name, li.enName, li.color);
- }
- }
-
- const sortedStations = Object.keys(stationXByCanonical).sort((a, b) => stationXByCanonical[a] - stationXByCanonical[b]);
- for (let i = 0; i < sortedStations.length; i++) {
- const canonical = sortedStations[i];
- const x = stationXByCanonical[canonical];
- const codes = stationCodesByCanonical[canonical] || [canonical];
- const lineIdxs = Array.from(lineIdxsByCanonical[canonical] || []).sort((a, b) => a - b);
- const colors = lineIdxs.map(idx => lineColors[idx]).filter(Boolean);
- const uniqueColors = Array.from(new Set(colors));
- const primaryCode = codes[0];
- const y = hubYByCanonical[canonical] ?? stationYByCanonical[primaryCode] ?? baseY;
- const isTransfer = uniqueColors.length >= 2 || codes.length >= 2;
- const name = getStationDisplayName(primaryCode);
-
- const enName = stationEnByCode[primaryCode] || '';
- const showEn = !!enName;
-
- let markerSvg = '';
- if (isTransfer) {
- markerSvg += ``;
- markerSvg += pieSvg(x, y, 11.5, uniqueColors);
- markerSvg += ``;
- } else {
- const color = uniqueColors[0] || '#93a2b7';
- markerSvg += ``;
- markerSvg += ``;
- }
-
- const preferTop = isTransfer || (i % 2 === 0 && y >= baseY + 20);
-
- let labelSvg = '';
- let textContent = '';
-
- const codeFontSize = 9;
- const nameFontSize = 13;
- const enFontSize = 8.5;
-
- const codeW = primaryCode.length * 6 + 10;
-
- if (preferTop) {
- const bottomY = y - 22;
- const enY = bottomY;
- const nameY = showEn ? bottomY - 12 : bottomY;
- const codeY = nameY - 18;
-
- textContent += ``;
- textContent += `${primaryCode}`;
-
- // Text with white halo for readability
- textContent += `${name}`;
- if (showEn) {
- textContent += `${enName}`;
- }
- } else {
- const topY = y + 34;
- const codeY = topY;
- const nameY = topY + 18;
- const enY = nameY + 12;
-
- textContent += ``;
- textContent += `${primaryCode}`;
-
- textContent += `${name}`;
- if (showEn) {
- textContent += `${enName}`;
- }
- }
- labelSvg += textContent;
-
- svgContent += `
-
-
- ${markerSvg}
- ${labelSvg}
-
- `;
- }
-
- const allCanonicals = Object.keys(stationXByCanonical);
- const minX = Math.min(...allCanonicals.map(c => stationXByCanonical[c] || 0));
- const maxX = Math.max(...allCanonicals.map(c => stationXByCanonical[c] || 0));
- const width = Math.max(560, maxX + 200);
- const height = Math.max(260, baseY + currentLineIdx * lineGapY + 120);
- mapContainer.innerHTML = ``;
+ mapContainer.innerHTML = `
+
+ `;
+
+ renderTvmLines();
+ renderTvmSingleLine();
} catch(e) {
console.error(e);
- mapContainer.innerHTML = '地图加载失败
';
+ mapContainer.innerHTML = '数据加载失败
';
}
}
-
+
window.handleStationClick = (code) => {
code = String(code || '').trim();
- // Toggle if already selected
if (selection[0] === code) {
selection[0] = null;
} else if (selection[1] === code) {
selection[1] = null;
} else {
- // Add new selection
if (!selection[0]) {
selection[0] = code;
} else if (!selection[1]) {
selection[1] = code;
} else {
- // When both ends are already selected, start a new selection flow.
selection[0] = code;
selection[1] = null;
currentRoute = [];
@@ -561,7 +420,6 @@
}
}
- // Ensure no gaps (if start removed, move end to start)
if (!selection[0] && selection[1]) {
selection[0] = selection[1];
selection[1] = null;
@@ -569,7 +427,7 @@
updateSelectionUI();
};
-
+
function normalizeTrips() {
const raw = Number(tripsEl?.value || 1);
const trips = Number.isFinite(raw) ? Math.max(1, Math.floor(raw)) : 1;
@@ -663,115 +521,15 @@
return merged;
}
- function getStationPoint(code) {
- const normalized = String(code || '').trim();
- if (!normalized) return null;
- const canonical = stationCanonicalByCode[normalized] || normalized;
- const x = stationXByCanonical[canonical];
- const y = stationYByCanonical[normalized];
- if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
- return { x, y, code: normalized };
- }
-
- 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;
-
- const existing = svgEl.querySelector('.route-overlay-group');
- if (existing) existing.remove();
-
- if (!Array.isArray(currentRoute) || currentRoute.length < 2) return;
-
- const points = currentRoute.map(getStationPoint).filter(Boolean);
- if (points.length < 2) return;
-
- const ns = 'http://www.w3.org/2000/svg';
- const group = document.createElementNS(ns, 'g');
- group.setAttribute('class', 'route-overlay-group');
-
- const pathData = buildTransitPath(points, 16);
- if (!pathData) return;
-
- const glow = document.createElementNS(ns, 'path');
- glow.setAttribute('d', pathData);
- glow.setAttribute('fill', 'none');
- glow.setAttribute('stroke', 'rgba(250, 204, 21, 0.38)');
- glow.setAttribute('stroke-width', '18');
- glow.setAttribute('stroke-linecap', 'round');
- glow.setAttribute('stroke-linejoin', 'round');
-
- const main = document.createElementNS(ns, 'path');
- main.setAttribute('d', pathData);
- main.setAttribute('fill', 'none');
- main.setAttribute('stroke', '#facc15');
- main.setAttribute('stroke-width', '8');
- main.setAttribute('stroke-linecap', 'round');
- main.setAttribute('stroke-linejoin', 'round');
-
- group.appendChild(glow);
- group.appendChild(main);
-
- // Dynamic arrows pointing to the destination
- let totalDist = 0;
- for (let i = 0; i < points.length - 1; i++) {
- const dx = points[i+1].x - points[i].x;
- const dy = points[i+1].y - points[i].y;
- totalDist += Math.sqrt(dx * dx + dy * dy);
- }
-
- const speed = 150; // pixels per second
- const duration = Math.max(1, totalDist / speed);
- const numArrows = Math.max(1, Math.floor(duration / 0.8));
- const interval = duration / numArrows;
-
- for (let i = 0; i < numArrows; i++) {
- const arrowG = document.createElementNS(ns, 'g');
- const arrowPath = document.createElementNS(ns, 'path');
- // Chevron shape
- arrowPath.setAttribute('d', 'M -6 -5 L 4 0 L -6 5');
- arrowPath.setAttribute('fill', 'none');
- arrowPath.setAttribute('stroke', '#854d0e'); // Dark brown/orange to contrast with yellow
- arrowPath.setAttribute('stroke-width', '2.5');
- arrowPath.setAttribute('stroke-linecap', 'round');
- arrowPath.setAttribute('stroke-linejoin', 'round');
-
- const animateMotion = document.createElementNS(ns, 'animateMotion');
- animateMotion.setAttribute('dur', `${duration}s`);
- animateMotion.setAttribute('repeatCount', 'indefinite');
- animateMotion.setAttribute('path', pathData);
- animateMotion.setAttribute('rotate', 'auto');
- animateMotion.setAttribute('begin', `-${i * interval}s`);
- // Use keyPoints/keyTimes to ensure constant speed along the path
- animateMotion.setAttribute('calcMode', 'linear');
-
- arrowG.appendChild(arrowPath);
- arrowG.appendChild(animateMotion);
- group.appendChild(arrowG);
- }
-
- const firstStation = svgEl.querySelector('.map-station');
- if (firstStation) svgEl.insertBefore(group, firstStation);
- else svgEl.appendChild(group);
-}
-
function updateSelectionUI(skipPreview = false) {
if (!(selection[0] && selection[1])) {
currentRoute = [];
currentRouteTransfers = [];
}
- // Update Inputs
fromEl.value = selection[0] || '';
toEl.value = selection[1] || '';
- // Update Displays with Chinese names
const getName = (code) => {
return stationNameByCode[String(code || '').trim()] || code;
};
@@ -779,41 +537,24 @@
$('#fromDisplay').textContent = selection[0] ? getName(selection[0]) : '请在上方地图选择';
$('#toDisplay').textContent = selection[1] ? getName(selection[1]) : '请在上方地图选择';
- // 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 (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.includes(code)) el.classList.add('route');
+ if (Array.isArray(currentRouteTransfers) && currentRouteTransfers.includes(code)) el.classList.add('route-transfer');
});
- if (Array.isArray(currentRoute) && currentRoute.length > 0) {
- for (const c of currentRoute) {
- const el = getMapStationEl(c);
- if (el) el.classList.add('route');
- }
- }
- if (Array.isArray(currentRouteTransfers) && currentRouteTransfers.length > 0) {
- for (const c of currentRouteTransfers) {
- const el = getMapStationEl(c);
- if (el) el.classList.add('route-transfer');
- }
- }
+ renderTvmRoute();
- renderRouteOverlay();
-
- // Auto preview if both selected
if(!skipPreview && selection[0] && selection[1]) previewPrice();
}
- // Load Map on Start
renderSystemMap();
- window.addEventListener('resize', () => {
- if (currentRoute.length > 0) renderRouteOverlay();
- });
-
async function previewPrice(force = false){
const seq = ++previewSeq;
const type = document.querySelector('input[name="trainType"]:checked').value;
@@ -894,7 +635,6 @@
}
}
- // Create Order with auto-preview
async function createOrder(){
const type = document.querySelector('input[name="trainType"]:checked').value;
const trips = normalizeTrips();
@@ -905,7 +645,6 @@
const to = (toEl.value||'').trim();
if(!from || !to){ alert('请完整填写信息'); return; }
- // Auto-fetch price before creating
await previewPrice(true);
payload = { start: from, terminal: to, train_type: type, trips, ride_date };
@@ -913,7 +652,6 @@
const res = await fetch('/api/public/orders', { method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify(payload) });
const r = await res.json();
if(r && r.ok){
- // Link to external subdomain or local page
const buildTokenLink = (code) => {
if (location.hostname.includes('fse-media.group')) {
return `https://ticket.fse-media.group/token?code=${encodeURIComponent(code)}`;
@@ -942,7 +680,7 @@
const btnCreate = $('#createOrder');
if(btnCreate) btnCreate.onclick = createOrder;
- // Listen for Type change to auto-update price
+
document.querySelectorAll('input[name="trainType"]').forEach(el => {
el.onchange = () => { if(fromEl.value && toEl.value) previewPrice(); };
});