feat(admin): 优化换乘设置与坐标绑定逻辑,自动识别同名换乘站
This commit is contained in:
@@ -86,6 +86,33 @@ const buildRouteGraph = (stations, lines, fares, cfg) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Auto-link stations with the same name (Chinese or English)
|
||||||
|
const stationsByName = new Map();
|
||||||
|
const normKey = (s) => String(s || '').trim().toLowerCase().replace(/\s+/g, '');
|
||||||
|
for (const s of (stations || [])) {
|
||||||
|
const code = String(s?.code || '').trim();
|
||||||
|
if (!code) continue;
|
||||||
|
const cn = normKey(s?.name || s?.cn_name || s?.station_name);
|
||||||
|
const en = normKey(s?.en_name || s?.en || s?.enName);
|
||||||
|
if (cn) {
|
||||||
|
if (!stationsByName.has(cn)) stationsByName.set(cn, []);
|
||||||
|
stationsByName.get(cn).push(code);
|
||||||
|
}
|
||||||
|
if (en && en !== cn) {
|
||||||
|
if (!stationsByName.has(en)) stationsByName.set(en, []);
|
||||||
|
stationsByName.get(en).push(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const codes of stationsByName.values()) {
|
||||||
|
if (codes.length < 2) continue;
|
||||||
|
for (let i = 0; i < codes.length; i++) {
|
||||||
|
for (let j = i + 1; j < codes.length; j++) {
|
||||||
|
addTransferEdge(codes[i], codes[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const transfers = Array.isArray(cfg?.transfers) ? cfg.transfers : [];
|
const transfers = Array.isArray(cfg?.transfers) ? cfg.transfers : [];
|
||||||
for (const p of transfers) {
|
for (const p of transfers) {
|
||||||
const u = String(p?.[0] || '').trim();
|
const u = String(p?.[0] || '').trim();
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
"mtimeMs": 1783666241687.1482
|
"mtimeMs": 1783666241687.1482
|
||||||
},
|
},
|
||||||
"index.js": {
|
"index.js": {
|
||||||
"size": 82021,
|
"size": 84094,
|
||||||
"mtimeMs": 1783775091220.4558
|
"mtimeMs": 1783777527829.4636
|
||||||
},
|
},
|
||||||
"ic-card-admin.js": {
|
"ic-card-admin.js": {
|
||||||
"size": 19095,
|
"size": 19095,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"ticket-order.js": 17,
|
"ticket-order.js": 17,
|
||||||
"ticket-search.js": 12,
|
"ticket-search.js": 12,
|
||||||
"ticket-route.js": 3,
|
"ticket-route.js": 3,
|
||||||
"index.js": 3,
|
"index.js": 4,
|
||||||
"ic-card-admin.js": 3,
|
"ic-card-admin.js": 3,
|
||||||
"ic-card-order.js": 3,
|
"ic-card-order.js": 3,
|
||||||
"ic-card-detail.js": 2,
|
"ic-card-detail.js": 2,
|
||||||
|
|||||||
+24
-9
@@ -361,16 +361,27 @@
|
|||||||
<label class="switch-label">
|
<label class="switch-label">
|
||||||
<input type="checkbox" v-model="stationForm.transfer_enabled">
|
<input type="checkbox" v-model="stationForm.transfer_enabled">
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
<span class="label-text">可换乘</span>
|
<span class="label-text">设置跨名/强制换乘</span>
|
||||||
</label>
|
</label>
|
||||||
|
<p class="text-muted" style="font-size: 0.85em; margin-top: 6px;">
|
||||||
|
<i class="fas fa-magic text-primary"></i> 提示:系统会自动将中文名或英文名相同的站点连接为换乘站,无需手动设置。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4" v-if="stationForm.transfer_enabled">
|
||||||
<label>可换乘到的站点</label>
|
<label>可换乘到的站点</label>
|
||||||
<select v-model="stationForm.transfer_to" multiple class="w-100" :disabled="!stationForm.transfer_enabled" style="height: 180px;">
|
<select v-model="stationForm.transfer_to" multiple class="w-100" style="height: 180px;">
|
||||||
<option v-for="t in transferTargets" :key="t.code" :value="t.code">
|
<optgroup label="同名站点 (已自动换乘)" v-if="transferTargets.sameName.length > 0">
|
||||||
{{ t.name }} ({{ t.en_name }}) - {{ t.code }}
|
<option v-for="t in transferTargets.sameName" :key="t.code" :value="t.code">
|
||||||
</option>
|
{{ t.name }} ({{ t.en_name }}) - {{ t.code }}
|
||||||
|
</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="其他站点">
|
||||||
|
<option v-for="t in transferTargets.others" :key="t.code" :value="t.code">
|
||||||
|
{{ t.name }} ({{ t.en_name }}) - {{ t.code }}
|
||||||
|
</option>
|
||||||
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
|
<p class="text-muted" style="font-size: 0.8em; margin-top: 4px;">按住 Ctrl / Cmd 键多选</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="danger" @click="deleteStation(stationFormOriginalCode)">删除</button>
|
<button class="danger" @click="deleteStation(stationFormOriginalCode)">删除</button>
|
||||||
@@ -814,10 +825,14 @@
|
|||||||
</label>
|
</label>
|
||||||
<select v-if="mapBindingMode" v-model="mapBindingStation" style="width: 240px;">
|
<select v-if="mapBindingMode" v-model="mapBindingStation" style="width: 240px;">
|
||||||
<option value="">-- 选择要绑定的站点 --</option>
|
<option value="">-- 选择要绑定的站点 --</option>
|
||||||
<option v-for="s in stations" :key="s.code" :value="s.code">{{ s.name }} ({{ s.code }})</option>
|
<optgroup v-for="g in unboundStationsGrouped" :key="g.name" :label="g.name + (g.isTransfer ? ' (换乘站)' : '') + (g.isBound ? ' - 已绑定' : '')">
|
||||||
|
<option v-for="s in g.stations" :key="s.code" :value="s.code">
|
||||||
|
{{ s.name }} ({{ s.code }})
|
||||||
|
</option>
|
||||||
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
<div v-if="mapBindingMode" class="text-muted" style="font-size: 0.9em;">
|
<div v-if="mapBindingMode" class="text-muted" style="font-size: 0.9em;">
|
||||||
在左侧选择站点后,点击下方图片中的位置即可绑定。绑定的坐标将用于网页订票展示。
|
在左侧选择站点后,点击下方图片中的位置即可绑定。同名换乘站会自动同步坐标绑定。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -963,7 +978,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=11"></script>
|
||||||
<script src="index.js?v=3"></script>
|
<script src="index.js?v=4"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const isDomain = location.hostname.includes('fse-media.group');
|
const isDomain = location.hostname.includes('fse-media.group');
|
||||||
|
|||||||
+63
-11
@@ -1143,6 +1143,29 @@ createApp({
|
|||||||
const mapBindingStation = ref('');
|
const mapBindingStation = ref('');
|
||||||
const mapImageRef = ref(null);
|
const mapImageRef = ref(null);
|
||||||
|
|
||||||
|
const unboundStationsGrouped = computed(() => {
|
||||||
|
const groups = {};
|
||||||
|
stations.value.forEach(s => {
|
||||||
|
const name = String(s.name || s.cn_name || '').trim();
|
||||||
|
if (!groups[name]) groups[name] = [];
|
||||||
|
groups[name].push(s);
|
||||||
|
});
|
||||||
|
const out = [];
|
||||||
|
for (const [name, list] of Object.entries(groups)) {
|
||||||
|
out.push({
|
||||||
|
name,
|
||||||
|
isTransfer: list.length > 1,
|
||||||
|
stations: list,
|
||||||
|
isBound: list.every(s => s.svg_x != null && s.svg_y != null)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out.sort((a, b) => {
|
||||||
|
if (a.isBound !== b.isBound) return a.isBound ? 1 : -1;
|
||||||
|
if (a.isTransfer !== b.isTransfer) return a.isTransfer ? -1 : 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const onMapImageClick = async (e) => {
|
const onMapImageClick = async (e) => {
|
||||||
if (!mapBindingMode.value || !mapBindingStation.value) return;
|
if (!mapBindingMode.value || !mapBindingStation.value) return;
|
||||||
const img = mapImageRef.value;
|
const img = mapImageRef.value;
|
||||||
@@ -1157,16 +1180,30 @@ createApp({
|
|||||||
const station = stations.value.find(s => s.code === mapBindingStation.value);
|
const station = stations.value.find(s => s.code === mapBindingStation.value);
|
||||||
if (!station) return;
|
if (!station) return;
|
||||||
|
|
||||||
station.svg_x = Number(percentX.toFixed(2));
|
const pctX = Number(percentX.toFixed(2));
|
||||||
station.svg_y = Number(percentY.toFixed(2));
|
const pctY = Number(percentY.toFixed(2));
|
||||||
|
|
||||||
|
const targetName = String(station.name || station.cn_name || '').trim().toLowerCase();
|
||||||
|
|
||||||
|
// Auto-bind all stations with the same name (transfers)
|
||||||
|
const stationsToUpdate = stations.value.filter(s => {
|
||||||
|
if (s.code === mapBindingStation.value) return true;
|
||||||
|
const sName = String(s.name || s.cn_name || '').trim().toLowerCase();
|
||||||
|
return targetName && sName === targetName;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await requestJson(`/api/stations/${encodeURIComponent(station.code)}`, {
|
for (const s of stationsToUpdate) {
|
||||||
method: 'PUT',
|
s.svg_x = pctX;
|
||||||
headers: {'Content-Type': 'application/json'},
|
s.svg_y = pctY;
|
||||||
body: JSON.stringify(station)
|
await requestJson(`/api/stations/${encodeURIComponent(s.code)}`, {
|
||||||
}, { expectOk: true });
|
method: 'PUT',
|
||||||
// Optional: find the next station without coordinates and select it
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(s)
|
||||||
|
}, { expectOk: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the next station without coordinates and select it
|
||||||
const nextStation = stations.value.find(s => s.svg_x == null && s.svg_y == null);
|
const nextStation = stations.value.find(s => s.svg_x == null && s.svg_y == null);
|
||||||
if (nextStation) {
|
if (nextStation) {
|
||||||
mapBindingStation.value = nextStation.code;
|
mapBindingStation.value = nextStation.code;
|
||||||
@@ -1342,9 +1379,24 @@ createApp({
|
|||||||
|
|
||||||
const transferTargets = computed(() => {
|
const transferTargets = computed(() => {
|
||||||
const fromCode = stationForm.code;
|
const fromCode = stationForm.code;
|
||||||
return stations.value
|
const fromName = String(stationForm.name || '').trim().toLowerCase();
|
||||||
.filter(s => s && s.code && s.code !== fromCode)
|
|
||||||
.map(s => ({ code: s.code, name: s.name || s.cn_name || s.code, en_name: s.en_name || s.enName || '' }));
|
const sameName = [];
|
||||||
|
const others = [];
|
||||||
|
|
||||||
|
stations.value.forEach(s => {
|
||||||
|
if (!s || !s.code || s.code === fromCode) return;
|
||||||
|
const targetName = String(s.name || s.cn_name || s.code).trim().toLowerCase();
|
||||||
|
const opt = { code: s.code, name: s.name || s.cn_name || s.code, en_name: s.en_name || s.enName || '' };
|
||||||
|
|
||||||
|
if (fromName && targetName === fromName) {
|
||||||
|
sameName.push(opt);
|
||||||
|
} else {
|
||||||
|
others.push(opt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { sameName, others };
|
||||||
});
|
});
|
||||||
|
|
||||||
const openLineModal = () => {
|
const openLineModal = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user