feat(web): 优化区间选择界面线路图布局
1. 实现正交布线与圆角拐角连接,减少斜线交叉 2. 动态调整线路间距与节点位置,提高整体紧凑性 3. 优化线路名称与换乘站标签的避让逻辑,避免遮挡 4. 删除不再使用的全局预览页(map-preview.html)
This commit is contained in:
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
1. 完成功能制作后自行编辑git commit并推送
|
1. 完成功能制作后自行编辑git commit并推送
|
||||||
2.
|
2. 提交时请添加功能描述
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>全网线路图预览 - FSE铁路运输票务系统</title>
|
|
||||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
|
||||||
<style>
|
|
||||||
body { margin: 0; padding: 0; background: #f8fafc; display: flex; flex-direction: column; height: 100vh; font-family: system-ui, -apple-system, sans-serif; }
|
|
||||||
header { background: white; padding: 16px 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center; z-index: 10; }
|
|
||||||
h1 { margin: 0; font-size: 18px; color: #0f172a; font-weight: 700; }
|
|
||||||
.btn { background: #3b82f6; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: 500; transition: background 0.2s; }
|
|
||||||
.btn:hover { background: #2563eb; }
|
|
||||||
.btn-secondary { background: #e2e8f0; color: #475569; }
|
|
||||||
.btn-secondary:hover { background: #cbd5e1; }
|
|
||||||
.controls { display: flex; gap: 12px; }
|
|
||||||
#map-container { flex: 1; overflow: auto; display: flex; justify-content: center; align-items: center; padding: 32px; position: relative; }
|
|
||||||
#map-container svg { box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05); border-radius: 12px; background: white; max-width: none; transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); transform-origin: center; }
|
|
||||||
.loading-hint { color: #64748b; font-size: 15px; position: absolute; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>全网线路图预览 (正交布线)</h1>
|
|
||||||
<div class="controls">
|
|
||||||
<a href="/index.html" class="btn btn-secondary">返回后台</a>
|
|
||||||
<button class="btn" onclick="loadMap()">重新生成</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div id="map-container">
|
|
||||||
<div class="loading-hint">正在计算布局,请稍候...</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
let scale = 1;
|
|
||||||
async function loadMap() {
|
|
||||||
const container = document.getElementById('map-container');
|
|
||||||
container.innerHTML = '<div class="loading-hint">正在计算正交布局,请稍候...</div>';
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/public/fares/map/light?t=' + Date.now());
|
|
||||||
const svg = await res.text();
|
|
||||||
container.innerHTML = svg;
|
|
||||||
scale = 1;
|
|
||||||
updateZoom();
|
|
||||||
} catch (e) {
|
|
||||||
container.innerHTML = '<div style="color: #ef4444;">加载失败</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function updateZoom() {
|
|
||||||
const svg = document.querySelector('#map-container svg');
|
|
||||||
if(svg) svg.style.transform = `scale(${scale})`;
|
|
||||||
}
|
|
||||||
document.getElementById('map-container').addEventListener('wheel', (e) => {
|
|
||||||
if (e.ctrlKey || e.metaKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
scale += e.deltaY * -0.002;
|
|
||||||
scale = Math.min(Math.max(0.125, scale), 4);
|
|
||||||
updateZoom();
|
|
||||||
}
|
|
||||||
}, { passive: false });
|
|
||||||
loadMap();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
+123
-16
@@ -1,3 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* @Author: HenryDu8133 813367384@qq.com
|
||||||
|
* @Date: 2025-12-06 23:59:11
|
||||||
|
* @LastEditors: HenryDu8133 813367384@qq.com
|
||||||
|
* @LastEditTime: 2026-07-15 23:25:36
|
||||||
|
* @FilePath: \TicketMachine\web\ticket-order.js
|
||||||
|
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||||
|
*/
|
||||||
(() => {
|
(() => {
|
||||||
const $ = (sel) => document.querySelector(sel);
|
const $ = (sel) => document.querySelector(sel);
|
||||||
const fromEl = $('#from');
|
const fromEl = $('#from');
|
||||||
@@ -56,13 +64,101 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const buildTransitPath = (points, radius = 12) => {
|
const buildTransitPath = (points, radius = 12) => {
|
||||||
if (!points || points.length === 0) return '';
|
const src = Array.isArray(points) ? points.filter(Boolean) : [];
|
||||||
if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
|
if (src.length === 0) return '';
|
||||||
|
if (src.length === 1) return `M ${src[0].x} ${src[0].y}`;
|
||||||
|
|
||||||
let d = `M ${points[0].x} ${points[0].y}`;
|
const fmt = (n) => {
|
||||||
for (let i = 1; i < points.length; i++) {
|
const v = Number(n);
|
||||||
d += ` L ${points[i].x} ${points[i].y}`;
|
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 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);
|
||||||
|
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;
|
return d;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -194,8 +290,17 @@
|
|||||||
const legendW = 10;
|
const legendW = 10;
|
||||||
const startX = 140;
|
const startX = 140;
|
||||||
const baseY = 80;
|
const baseY = 80;
|
||||||
const lineGapY = 80;
|
const totalLines = currentLineIdx;
|
||||||
const minGapX = 80;
|
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 = {};
|
const occurrences = {};
|
||||||
for (const li of lineStops) {
|
for (const li of lineStops) {
|
||||||
@@ -272,7 +377,7 @@
|
|||||||
const hasEn = !!enName;
|
const hasEn = !!enName;
|
||||||
const nameLen = Math.max(name.length * 13 + 16, hasEn ? enName.length * 6.5 + 16 : 0);
|
const nameLen = Math.max(name.length * 13 + 16, hasEn ? enName.length * 6.5 + 16 : 0);
|
||||||
const h = hasEn ? 36 : 24;
|
const h = hasEn ? 36 : 24;
|
||||||
const yOffset = count * 42 - (h / 2);
|
const yOffset = count * labelStackGap - (h / 2);
|
||||||
|
|
||||||
let lx, textX;
|
let lx, textX;
|
||||||
if (isStart) {
|
if (isStart) {
|
||||||
@@ -301,7 +406,8 @@
|
|||||||
const canonical = stationCanonicalByCode[code] || code;
|
const canonical = stationCanonicalByCode[code] || code;
|
||||||
return {
|
return {
|
||||||
x: stationXByCanonical[canonical] || 0,
|
x: stationXByCanonical[canonical] || 0,
|
||||||
y: stationYByCanonical[code] ?? (baseY + li.idx * lineGapY)
|
y: stationYByCanonical[code] ?? (baseY + li.idx * lineGapY),
|
||||||
|
code
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,8 +417,9 @@
|
|||||||
// Add line name labels at ends
|
// Add line name labels at ends
|
||||||
const pStart = pts[0];
|
const pStart = pts[0];
|
||||||
const pEnd = pts[pts.length - 1];
|
const pEnd = pts[pts.length - 1];
|
||||||
svgContent += placeLabel(pStart.x, pStart.y, true, li.name, li.enName, li.color);
|
const yLabel = baseY + li.idx * lineGapY;
|
||||||
svgContent += placeLabel(pEnd.x, pEnd.y, false, li.name, li.enName, li.color);
|
svgContent += placeLabel(pStart.x, yLabel, true, li.name, li.enName, li.color);
|
||||||
|
svgContent += placeLabel(pEnd.x, yLabel, false, li.name, li.enName, li.color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,10 +462,10 @@
|
|||||||
const codeW = primaryCode.length * 6 + 10;
|
const codeW = primaryCode.length * 6 + 10;
|
||||||
|
|
||||||
if (preferTop) {
|
if (preferTop) {
|
||||||
const bottomY = y - 14;
|
const bottomY = y - 22;
|
||||||
const enY = bottomY;
|
const enY = bottomY;
|
||||||
const nameY = showEn ? bottomY - 12 : bottomY;
|
const nameY = showEn ? bottomY - 12 : bottomY;
|
||||||
const codeY = nameY - 16;
|
const codeY = nameY - 18;
|
||||||
|
|
||||||
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 - 8}" width="${codeW}" height="12" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1.5" />`;
|
||||||
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}" text-anchor="middle" fill="#64748b" font-size="${codeFontSize}" font-weight="700" letter-spacing="0.02em">${primaryCode}</text>`;
|
||||||
@@ -369,9 +476,9 @@
|
|||||||
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="3" stroke-linejoin="round" paint-order="stroke fill">${enName}</text>`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const topY = y + 24;
|
const topY = y + 34;
|
||||||
const codeY = topY;
|
const codeY = topY;
|
||||||
const nameY = topY + 16;
|
const nameY = topY + 18;
|
||||||
const enY = nameY + 12;
|
const enY = nameY + 12;
|
||||||
|
|
||||||
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 - 8}" width="${codeW}" height="12" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1.5" />`;
|
||||||
@@ -537,7 +644,7 @@
|
|||||||
const x = stationXByCanonical[canonical];
|
const x = stationXByCanonical[canonical];
|
||||||
const y = stationYByCanonical[normalized];
|
const y = stationYByCanonical[normalized];
|
||||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
||||||
return { x, y };
|
return { x, y, code: normalized };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMapStationEl(code) {
|
function getMapStationEl(code) {
|
||||||
|
|||||||
Reference in New Issue
Block a user