feat(web): 订票地图升级为静态SVG并叠加动态坐标交互;完善断点支持

This commit is contained in:
2026-07-11 21:15:48 +08:00
parent 690173c522
commit aede84f164
21 changed files with 316 additions and 262 deletions
+92 -27
View File
@@ -7,7 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>控制台|FSE铁路运输票务系统</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="style.css?v=14">
<link rel="stylesheet" href="style.css?v=13">
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="/socket.io/socket.io.js"></script>
</head>
@@ -238,6 +238,9 @@
<button @click="addStationToLine"
:disabled="!newStation.code || !newStation.name"><i class="fas fa-plus"></i>
添加</button>
<button @click="addBreakpointToLine" class="btn" style="margin-left: 8px;" title="在末尾插入一个线路断点">
<i class="fas fa-cut"></i> 插入断点
</button>
</div>
</div>
@@ -261,37 +264,45 @@
</g>
<!--车站节点-->
<g v-for="(sCode, index) in selectedLine.stations" :key="sCode"
<g v-for="(sCode, index) in selectedLine.stations" :key="index"
@mousedown="onStationDragStart(index)" @mouseup="onStationDrop"
@mousemove="onStationDragOver(index)" @click="handleStationClick(sCode)"
@mousemove="onStationDragOver(index)" @click="handleStationClick(sCode, index)"
class="station-node" :class="{
'selected': isStationSelected(sCode),
'fare-source': fareSelection[0] === sCode,
'fare-target': fareSelection[1] === sCode
}">
<!--车站节点图形-->
<circle :cx="50 + index * 120" cy="100" r="14" fill="var(--bg)"
:stroke="selectedLine.color" stroke-width="3" />
<circle v-if="isStationSelected(sCode)" :cx="50 + index * 120" cy="100" r="8"
:fill="selectedLine.color" />
<template v-if="sCode === '-'">
<rect :x="50 + index * 120 - 10" :y="90" width="20" height="20" fill="var(--bg)" stroke="var(--danger)" stroke-dasharray="2,2" />
<text :x="50 + index * 120" y="105" text-anchor="middle" fill="var(--danger)" font-size="12" style="pointer-events: none;">✂</text>
<text :x="50 + index * 120" y="135" text-anchor="middle" fill="var(--danger)" font-size="10" style="pointer-events: none;">断点</text>
</template>
<template v-else>
<!--车站节点图形-->
<circle :cx="50 + index * 120" cy="100" r="14" fill="var(--bg)"
:stroke="selectedLine.color" stroke-width="3" />
<circle v-if="isStationSelected(sCode)" :cx="50 + index * 120" cy="100" r="8"
:fill="selectedLine.color" />
<!--节点标签-->
<text :x="50 + index * 120" y="70" text-anchor="middle" fill="var(--text)"
font-weight="bold" font-size="12" style="pointer-events: none;">{{
getStationName(sCode) }}</text>
<text :x="50 + index * 120" y="135" text-anchor="middle" fill="var(--muted)"
font-size="10" style="pointer-events: none;">{{ sCode }}</text>
<g v-if="getTransferLineBadges(sCode).length > 0">
<g v-for="(li, liIdx) in getTransferLineBadges(sCode)" :key="`${sCode}-xfer-${li.id}`">
<circle :cx="(50 + index * 120) + (liIdx - (getTransferLineBadges(sCode).length - 1) / 2) * 14" cy="150" r="5"
:fill="li.color" stroke="#ffffff" stroke-width="1" />
<text :x="(50 + index * 120) + (liIdx - (getTransferLineBadges(sCode).length - 1) / 2) * 14" y="165" text-anchor="middle"
fill="var(--muted)" font-size="7" style="pointer-events: none;">{{ li.id }}</text>
<!--节点标签-->
<text :x="50 + index * 120" y="70" text-anchor="middle" fill="var(--text)"
font-weight="bold" font-size="12" style="pointer-events: none;">{{
getStationName(sCode) }}</text>
<text :x="50 + index * 120" y="135" text-anchor="middle" fill="var(--muted)"
font-size="10" style="pointer-events: none;">{{ sCode }}</text>
<g v-if="getTransferLineBadges(sCode).length > 0">
<g v-for="(li, liIdx) in getTransferLineBadges(sCode)" :key="`${sCode}-xfer-${li.id}`">
<circle :cx="(50 + index * 120) + (liIdx - (getTransferLineBadges(sCode).length - 1) / 2) * 14" cy="150" r="5"
:fill="li.color" stroke="#ffffff" stroke-width="1" />
<text :x="(50 + index * 120) + (liIdx - (getTransferLineBadges(sCode).length - 1) / 2) * 14" y="165" text-anchor="middle"
fill="var(--muted)" font-size="7" style="pointer-events: none;">{{ li.id }}</text>
</g>
</g>
</g>
<!--删除-->
<title>{{ getStationName(sCode) }} ({{ sCode }}){{ getTransferTitleSuffix(sCode) }}</title>
<!--删除-->
<title>{{ getStationName(sCode) }} ({{ sCode }}){{ getTransferTitleSuffix(sCode) }}</title>
</template>
</g>
</svg>
<div v-else class="empty-state">
@@ -793,8 +804,62 @@
</div>
</div>
<div v-if="assetsRouteMapUrl" style="border: 1px solid var(--border); border-radius: 10px; overflow: hidden; background: #0b0b0f;">
<img :src="assetsRouteMapUrl" alt="线路图" style="display:block; width: 100%; height: auto;">
<div v-if="assetsRouteMapUrl" style="border: 1px solid var(--border); border-radius: 10px; overflow: hidden; background: #0b0b0f; padding: 16px;">
<div class="flex mb-4" style="gap: 10px; align-items: center; background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px;">
<label class="switch-label">
<input type="checkbox" v-model="mapBindingMode">
<span class="slider"></span>
<span class="label-text">开启坐标绑定模式</span>
</label>
<select v-if="mapBindingMode" v-model="mapBindingStation" style="width: 240px;">
<option value="">-- 选择要绑定的站点 --</option>
<option v-for="s in stations" :key="s.code" :value="s.code">{{ s.name }} ({{ s.code }})</option>
</select>
<div v-if="mapBindingMode" class="text-muted" style="font-size: 0.9em;">
在左侧选择站点后,点击下方图片中的位置即可绑定。绑定的坐标将用于网页订票展示。
</div>
</div>
<div style="position: relative; display: inline-block; width: 100%;" :style="{ cursor: mapBindingMode ? 'crosshair' : 'default' }" @click="onMapImageClick">
<img :src="assetsRouteMapUrl" alt="线路图" style="display:block; width: 100%; height: auto;" ref="mapImageRef">
<!-- 坐标标记点 -->
<template v-for="s in stations" :key="'marker-'+s.code">
<div v-if="s.svg_x != null && s.svg_y != null"
:style="{
position: 'absolute',
left: s.svg_x + '%',
top: s.svg_y + '%',
transform: 'translate(-50%, -50%)',
width: s.code === mapBindingStation ? '16px' : '10px',
height: s.code === mapBindingStation ? '16px' : '10px',
borderRadius: '50%',
backgroundColor: s.code === mapBindingStation ? 'var(--primary)' : 'rgba(255,50,50,0.7)',
border: '2px solid white',
boxShadow: '0 0 4px rgba(0,0,0,0.5)',
pointerEvents: 'none',
transition: 'all 0.2s ease'
}">
</div>
<div v-if="mapBindingMode && s.svg_x != null && s.svg_y != null"
:style="{
position: 'absolute',
left: s.svg_x + '%',
top: `calc(${s.svg_y}% + 12px)`,
transform: 'translateX(-50%)',
color: 'white',
background: 'rgba(0,0,0,0.7)',
padding: '2px 4px',
borderRadius: '4px',
fontSize: '10px',
pointerEvents: 'none',
whiteSpace: 'nowrap'
}">
{{ s.name }}
</div>
</template>
</div>
</div>
<div v-else class="loading">未上传线路图</div>
</div>
@@ -897,8 +962,8 @@
</div>
</main>
</div>
<script src="/custom-dialog.js?v=12"></script>
<script src="index.js?v=6"></script>
<script src="/custom-dialog.js?v=11"></script>
<script src="index.js?v=3"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const isDomain = location.hostname.includes('fse-media.group');