因为线上环境没有配置 IP Hash (Sticky Sessions),导致默认的 polling 长轮询请求会随机分配到不同后端,从而引发 400 Bad Request 和连接断开重连刷屏的问题。 现将 transports 锁定为 ['websocket'],直接使用 WebSocket 连接。
629 lines
38 KiB
HTML
629 lines
38 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<!-- 充满未知和不稳定的票务系统! -->
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>线路规划|FSE铁路运输票务系统</title>
|
|
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
|
<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=21">
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
<script src="/socket.io/socket.io.js"></script>
|
|
</head>
|
|
|
|
<body class="jr-admin-page jr-admin-route-page jr-public-page">
|
|
<div class="jr-public-shell">
|
|
<header class="jr-topbar">
|
|
<div class="jr-topbar-inner">
|
|
<a href="https://ticket.fse-media.group" class="jr-top-link" id="routeTopLink">
|
|
<i class="fas fa-train"></i>
|
|
<span>FSE铁路运输票务系统</span>
|
|
</a>
|
|
|
|
</div>
|
|
</header>
|
|
|
|
<div class="jr-brandbar">
|
|
<div class="jr-brandbar-inner">
|
|
<a href="https://ticket.fse-media.group" class="jr-brand" id="routeBrandLink">
|
|
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
|
<div class="jr-brand-copy">
|
|
<strong>FarSight-T.N.E铁路运输</strong>
|
|
<span>线路规划后台</span>
|
|
</div>
|
|
</a>
|
|
<nav class="jr-nav" aria-label="站点导航">
|
|
<a href="https://ticket.fse-media.group/home.html" data-link="home">首页</a>
|
|
<a href="https://ticket.fse-media.group/order" data-link="order">线上预定</a>
|
|
<a href="https://ticket.fse-media.group/search" data-link="search">车票查询</a>
|
|
<a href="https://ticket.fse-media.group/ic-card/search" data-link="card-search">IC 卡查询</a>
|
|
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<main class="jr-public-main jr-admin-main-shell">
|
|
<div id="app" class="jr-admin-app" v-cloak>
|
|
<div class="sidebar" :class="{ open: sidebarOpen }">
|
|
<div class="jr-admin-sidebar-head">
|
|
<span class="jr-kicker">ROUTE PLANNING</span>
|
|
<div class="brand">FSE铁路售票线路规划系统</div>
|
|
<p class="jr-admin-sidebar-copy">维护线路、站点换乘关系与票价地图资源。</p>
|
|
</div>
|
|
<div class="nav">
|
|
<a href="https://ticket.fse-media.group" id="homeLink" class="nav-item" style="text-decoration: none;">
|
|
<span class="nav-icon"><i class="fas fa-home"></i></span> 返回首页
|
|
</a>
|
|
<div class="nav-item" :class="{active: currentView === 'management'}"
|
|
@click="currentView = 'management'">
|
|
<span class="nav-icon"><i class="fas fa-network-wired"></i></span> 线路规划
|
|
</div>
|
|
<div class="nav-item" :class="{active: currentView === 'stations'}"
|
|
@click="currentView = 'stations'">
|
|
<span class="nav-icon"><i class="fas fa-map-marker-alt"></i></span> 站点管理
|
|
</div>
|
|
<div class="nav-item" :class="{active: currentView === 'faremap'}" @click="currentView = 'faremap'">
|
|
<span class="nav-icon"><i class="fas fa-map"></i></span> 票价地图
|
|
</div>
|
|
</div>
|
|
<div class="jr-admin-sidebar-status">
|
|
<div class="jr-admin-sidebar-status-label">Server: {{ connected ? 'Online' : 'Offline' }}</div>
|
|
<div class="flex" style="align-items: center; gap: 6px;">
|
|
<i class="fas fa-circle"
|
|
:style="{ color: connected ? '#10b981' : '#ef4444', fontSize: '0.6rem' }"></i>
|
|
<span>Status: {{ connected ? 'Connected' : 'Disconnected' }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sidebar-faremap">
|
|
<div class="sidebar-faremap-title">票价地图预览</div>
|
|
<div class="sidebar-faremap-box" @click="currentView = 'faremap'">
|
|
<div v-if="fareMapLoading" class="text-muted" style="padding: 10px;">加载中...</div>
|
|
<div v-else-if="fareMapError" class="text-muted" style="padding: 10px;">{{ fareMapError }}</div>
|
|
<div v-else class="sidebar-faremap-canvas" v-html="fareMapSvg"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="sidebarOpen" class="sidebar-overlay" @click="sidebarOpen = false"></div>
|
|
|
|
<div class="main">
|
|
<div class="header">
|
|
<div class="jr-admin-header-copy">
|
|
<div class="flex" style="gap: 12px;">
|
|
<button class="icon-btn mobile-only" @click="sidebarOpen = !sidebarOpen" title="菜单"><i
|
|
class="fas fa-bars"></i></button>
|
|
<div>
|
|
<h3 style="margin: 0;">{{ viewTitle }}</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="jr-admin-header-side">
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="content">
|
|
<section class="jr-page-intro jr-admin-intro">
|
|
<span class="jr-kicker">LINE CONTROL</span>
|
|
<h1>线路规划与票价维护</h1>
|
|
<p>线路结构、站点编辑、换乘关系和票价地图</p>
|
|
</section>
|
|
|
|
<div v-if="currentView === 'management'" class="management-container">
|
|
<div class="management-sidebar">
|
|
<div class="card"
|
|
style="height: 100%; display: flex; flex-direction: column; margin-bottom: 0;">
|
|
<div class="flex between mb-4">
|
|
<h4>线路列表</h4>
|
|
<button @click="showAddLine = true" title="新建线路"><i class="fas fa-plus"></i></button>
|
|
</div>
|
|
|
|
<!--添加车站-->
|
|
<div v-if="showAddLine" class="mb-4"
|
|
style="background: rgba(255,255,255,0.05); padding: 10px; border-radius: 8px;">
|
|
<input v-model="newLine.id" placeholder="线路编号 (如 L1)"
|
|
style="margin-bottom: 8px; width: 100%;">
|
|
<input v-model="newLine.name" placeholder="中文名称"
|
|
style="margin-bottom: 8px; width: 100%;">
|
|
<input v-model="newLine.en_name" placeholder="英文名称"
|
|
style="margin-bottom: 8px; width: 100%;">
|
|
<div class="flex">
|
|
<input type="text" v-model="newLine.color" placeholder="#HEX颜色" style="flex: 1;">
|
|
<input type="color" v-model="newLine.color" title="选择颜色"
|
|
style="width: 40px; padding: 0; border: none; height: 32px;">
|
|
<button @click="createLine" style="padding: 0 12px;" title="确认创建线路"><i
|
|
class="fas fa-check"></i></button>
|
|
<button class="danger" @click="showAddLine = false" title="取消"
|
|
style="padding: 0 12px;"><i class="fas fa-times"></i></button>
|
|
</div>
|
|
</div>
|
|
<div class="list-lines" style="flex: 1; overflow-y: auto;">
|
|
<div v-for="l in lines" :key="l.id" class="line-item"
|
|
:class="{active: selectedLine && selectedLine.id === l.id}" @click="selectLine(l)">
|
|
<div class="line-color-dot" :style="{background: l.color}"></div>
|
|
<div class="line-info">
|
|
<div class="line-name">{{ l.name || l.id }}</div>
|
|
<div class="line-meta">{{ (l.stations || []).length }} 站</div>
|
|
</div>
|
|
<div class="line-actions" v-if="selectedLine && selectedLine.id === l.id">
|
|
<button class="danger sm" @click.stop="deleteLine(l.id)"><i
|
|
class="fas fa-trash"></i></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!--右侧面板-->
|
|
<div class="management-main" :class="{'is-empty': !selectedLine}">
|
|
<div class="card mb-4">
|
|
<div class="flex between">
|
|
<div v-if="selectedLine">
|
|
<div class="flex">
|
|
<h3 :style="{color: selectedLine.color}">{{ selectedLine.name || selectedLine.id
|
|
}}</h3>
|
|
<span class="badge">{{ selectedLine.id }}</span>
|
|
</div>
|
|
<div class="flex mt-2" style="align-items:center; gap:8px;">
|
|
<label style="font-size:0.8em; color:var(--muted);">EN:</label>
|
|
<span style="font-size:0.9em;">{{ selectedLine.en_name || 'N/A' }}</span>
|
|
</div>
|
|
</div>
|
|
<div v-else>
|
|
<h4 class="desktop-only">选择左侧线路进行管理</h4>
|
|
<h4 class="mobile-only-inline">请选择上方线路进行管理</h4>
|
|
</div>
|
|
|
|
<div class="flex">
|
|
<button v-if="selectedLine" @click="openLineModal" title="编辑线路"><i
|
|
class="fas fa-pen"></i></button>
|
|
<button @click="refreshData" title="刷新"><i class="fas fa-sync-alt"></i></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 可视化线路编辑-->
|
|
<div class="card visual-editor" v-if="selectedLine">
|
|
<div class="editor-toolbar flex between mb-4" style="flex-wrap: wrap; gap: 10px;">
|
|
<div class="flex">
|
|
<label class="switch-label">
|
|
<input type="checkbox" v-model="fareMode">
|
|
<span class="slider"></span>
|
|
<span class="label-text"><i class="fas fa-coins"></i> 票价设置/车站编辑模式</span>
|
|
</label>
|
|
<label class="switch-label" style="margin-left: 10px;">
|
|
<input type="checkbox" v-model="stationEditMode">
|
|
<span class="slider"></span>
|
|
<span class="label-text"><i class="fas fa-exchange-alt"></i> 换乘设置模式</span>
|
|
</label>
|
|
<div v-if="fareMode" class="hint-text text-warning">
|
|
<i class="fas fa-info-circle"></i> 点击两个站点以设置票价
|
|
</div>
|
|
<div v-else-if="stationEditMode" class="hint-text text-info">
|
|
<i class="fas fa-info-circle"></i> 点击站点以设置换乘
|
|
</div>
|
|
<div v-else class="hint-text text-muted">
|
|
<i class="fas fa-info-circle"></i> 点击站点删除
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex"
|
|
style="background: rgba(255,255,255,0.05); padding: 8px; border-radius: 6px;">
|
|
<div style="font-weight: bold; margin-right: 8px;">添加站点:</div>
|
|
<input v-model="newStation.code" placeholder="编号 (01-01)" style="width: 100px;">
|
|
<input v-model="newStation.name" placeholder="中文名" style=" width: 120px;">
|
|
<input v-model="newStation.en_name" placeholder="英文名" style=" width: 120px;">
|
|
<button @click="addStationToLine"
|
|
:disabled="!newStation.code || !newStation.name"><i class="fas fa-plus"></i>
|
|
添加</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 可视化线路编辑-->
|
|
<div class="visual-line-container" ref="visualLineContainer"
|
|
:class="{ 'is-panning': isPanning }"
|
|
@mousedown="onPanStart"
|
|
@mousemove="onPanMove"
|
|
@mouseup="onPanEnd"
|
|
@mouseleave="onPanEnd">
|
|
<svg :style="{ minWidth: '100%', width: Math.max(100, 50 + selectedLine.stations.length * 120 + 50) + 'px' }" height="200"
|
|
v-if="selectedLine.stations && selectedLine.stations.length > 0">
|
|
<!--站点连接线-->
|
|
<line x1="50" y1="100" :x2="50 + (selectedLine.stations.length - 1) * 120" y2="100"
|
|
:stroke="selectedLine.color" stroke-width="4" stroke-linecap="round" />
|
|
|
|
<!--票价显示-->
|
|
<g v-for="(s, i) in selectedLine.stations.slice(0, selectedLine.stations.length-1)"
|
|
:key="'fare-'+i">
|
|
<text :x="50 + i * 120 + 60" y="90" text-anchor="middle" fill="#f59e0b"
|
|
font-size="10" font-weight="bold">{{ getFareText(i) }}</text>
|
|
</g>
|
|
|
|
<!--车站节点-->
|
|
<g v-for="(sCode, index) in selectedLine.stations" :key="sCode"
|
|
@mousedown="onStationDragStart(index, $event)"
|
|
@mouseup="onStationDrop"
|
|
@click="handleStationClick(sCode)"
|
|
@touchstart.passive="onStationTouchStart(index, $event)"
|
|
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" />
|
|
|
|
<!--节点标签-->
|
|
<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>
|
|
|
|
<!--删除-->
|
|
<title>{{ getStationName(sCode) }} ({{ sCode }}){{ getTransferTitleSuffix(sCode)
|
|
}}</title>
|
|
</g>
|
|
</svg>
|
|
<div v-else class="empty-state">
|
|
<i class="fas fa-subway"
|
|
style="font-size: 48px; margin-bottom: 16px; opacity: 0.3;"></i>
|
|
<p>此线路暂无站点,请从上方添加</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showFareModal" class="modal show">
|
|
<div class="modal-card">
|
|
<h4 class="modal-title">设置票价</h4>
|
|
<div class="mb-4 text-center">
|
|
<div class="flex between"
|
|
style="justify-content: center; gap: 20px; font-size: 1.1em; font-weight: bold;">
|
|
<span>{{ getStationName(fareSelection[0]) }}</span>
|
|
<i class="fas fa-arrow-right text-muted"></i>
|
|
<span>{{ getStationName(fareSelection[1]) }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>常规票价</label>
|
|
<input v-model.number="currentFare.cost_regular" type="number" class="w-100">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>特急票价</label>
|
|
<input v-model.number="currentFare.cost_express" type="number" class="w-100">
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="danger" @click="deleteCurrentFare"
|
|
v-if="currentFare.exists">删除</button>
|
|
<button @click="saveCurrentFare">保存</button>
|
|
<button class="danger" @click="closeFareModal">取消</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showStationModal" class="modal show">
|
|
<div class="modal-card">
|
|
<h4 class="modal-title">站点编辑</h4>
|
|
<div class="mb-4">
|
|
<label>站点编号</label>
|
|
<input v-model="stationForm.code" class="w-100">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>中文名</label>
|
|
<input v-model="stationForm.name" class="w-100">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>英文名</label>
|
|
<input v-model="stationForm.en_name" class="w-100">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="switch-label">
|
|
<input type="checkbox" v-model="stationForm.transfer_enabled">
|
|
<span class="slider"></span>
|
|
<span class="label-text">可换乘</span>
|
|
</label>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>可换乘到的站点</label>
|
|
<select v-model="stationForm.transfer_to" multiple class="w-100"
|
|
:disabled="!stationForm.transfer_enabled" style="height: 180px;">
|
|
<option v-for="t in transferTargets" :key="t.code" :value="t.code">
|
|
{{ t.name }} ({{ t.en_name }}) - {{ t.code }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="danger" @click="deleteStation(stationFormOriginalCode)">删除</button>
|
|
<button @click="saveStationSettings">保存</button>
|
|
<button class="danger" @click="closeStationModal">取消</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showLineModal" class="modal show">
|
|
<div class="modal-card">
|
|
<h4 class="modal-title">线路编辑</h4>
|
|
<div class="mb-4">
|
|
<label>线路编号</label>
|
|
<input v-model="lineForm.id" class="w-100">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>中文名</label>
|
|
<input v-model="lineForm.name" class="w-100">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>英文名</label>
|
|
<input v-model="lineForm.en_name" class="w-100">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>颜色</label>
|
|
<div class="flex" style="gap:8px;">
|
|
<input type="text" v-model="lineForm.color" class="w-100" placeholder="#3366cc">
|
|
<input type="color" v-model="lineForm.color" title="选择颜色"
|
|
style="width: 48px; padding: 0; border: none; height: 32px;">
|
|
</div>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button @click="saveLineSettings">保存</button>
|
|
<button class="danger" @click="closeLineModal">取消</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="currentView === 'stations'" class="card">
|
|
<div class="flex between mb-4" style="flex-wrap: wrap; gap: 12px;">
|
|
<h4 style="margin: 0;">站点总览 ({{ stations.length }})</h4>
|
|
<div class="flex" style="gap: 12px; flex-wrap: wrap;">
|
|
<div class="search-box">
|
|
<i class="fas fa-search"></i>
|
|
<input v-model="stationSearch" placeholder="搜索站点编号/名称..." style="width: 200px;">
|
|
</div>
|
|
<button @click="showAddStation = true"><i class="fas fa-plus"></i> 新增站点</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showAddStation" class="mb-4" style="background: rgba(255,255,255,0.05); padding: 15px; border-radius: 8px;">
|
|
<h5 style="margin-top:0; margin-bottom:12px;">创建新站点</h5>
|
|
<div class="flex" style="flex-wrap: wrap; gap: 12px;">
|
|
<input v-model="newStation.code" placeholder="站点编号 (必填)" style="flex: 1; min-width: 120px;">
|
|
<input v-model="newStation.name" placeholder="中文名称 (必填)" style="flex: 1; min-width: 120px;">
|
|
<input v-model="newStation.en_name" placeholder="英文名称" style="flex: 1; min-width: 120px;">
|
|
<button @click="addStationToLine" :disabled="!newStation.code || !newStation.name" style="min-width: 80px;">创建</button>
|
|
<button class="danger" @click="showAddStation = false" style="min-width: 80px;">取消</button>
|
|
</div>
|
|
<p class="text-muted mt-2" style="font-size: 0.85rem; margin-bottom: 0;"><i class="fas fa-info-circle"></i> 在此创建的站点将被保存到数据库中,可以在线路规划中添加到任意线路。</p>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="ticket-table" style="width: 100%;">
|
|
<thead>
|
|
<tr>
|
|
<th>站点编号</th>
|
|
<th>中文名称</th>
|
|
<th>英文名称</th>
|
|
<th>所属线路</th>
|
|
<th>换乘支持</th>
|
|
<th style="text-align: right;">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="s in stationList" :key="s.code">
|
|
<td class="mono" style="font-weight:bold;">{{ s.code }}</td>
|
|
<td>{{ s.name || s.cn_name || '-' }}</td>
|
|
<td>{{ s.en_name || s.enName || '-' }}</td>
|
|
<td>
|
|
<div class="flex" style="gap: 4px; flex-wrap: wrap;">
|
|
<span v-for="l in getStationLines(s.code)" :key="l.id" class="badge" :style="{background: l.color, color: '#fff', border: 'none'}">{{ l.id }}</span>
|
|
<span v-if="getStationLines(s.code).length === 0" class="text-muted" style="font-size: 0.85em;">暂无线路</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span v-if="s.transfer_enabled" class="badge" style="background: #10b981; color: white;">是</span>
|
|
<span v-else class="text-muted">-</span>
|
|
</td>
|
|
<td style="text-align: right;">
|
|
<button class="sm" @click="editStationGlobal(s.code)" title="编辑"><i class="fas fa-pen"></i></button>
|
|
<button class="danger sm" @click="deleteStationGlobal(s.code)" title="删除"><i class="fas fa-trash"></i></button>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="stationList.length === 0">
|
|
<td colspan="6" class="text-center text-muted" style="padding: 30px;">无匹配站点</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 票价地图 -->
|
|
<div v-if="currentView === 'faremap'" class="card faremap-card">
|
|
<div class="flex between mb-4">
|
|
<h4>票价地图</h4>
|
|
<div class="flex" style="flex-wrap: wrap; gap: 8px;">
|
|
<button @click="loadFareMap" title="刷新"><i class="fas fa-sync-alt"></i></button>
|
|
<button @click="zoomFareMapOut" title="缩小"><i class="fas fa-minus"></i></button>
|
|
<button @click="zoomFareMapIn" title="放大"><i class="fas fa-plus"></i></button>
|
|
<button @click="zoomFareMapReset" title="重置"><i class="fas fa-crosshairs"></i></button>
|
|
<button @click="exportFareMap" title="导出图像"><i class="fas fa-download"></i></button>
|
|
</div>
|
|
</div>
|
|
<div v-if="fareMapLoading" class="loading">加载中...</div>
|
|
<div v-else-if="fareMapError" class="loading">{{ fareMapError }}</div>
|
|
<div v-else class="faremap-viewport">
|
|
<div class="faremap-canvas" :style="{ transform: `scale(${fareMapScale})` }"
|
|
v-html="fareMapSvg">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 提取出来的弹窗,避免被 currentView = 'management' 屏蔽 -->
|
|
<div v-if="showFareModal" class="modal show">
|
|
<div class="modal-card">
|
|
<h4 class="modal-title">设置票价</h4>
|
|
<div class="mb-4 text-center">
|
|
<div class="flex between"
|
|
style="justify-content: center; gap: 20px; font-size: 1.1em; font-weight: bold;">
|
|
<span>{{ getStationName(fareSelection[0]) }}</span>
|
|
<i class="fas fa-arrow-right text-muted"></i>
|
|
<span>{{ getStationName(fareSelection[1]) }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>常规票价</label>
|
|
<input v-model.number="currentFare.cost_regular" type="number" class="w-100">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>特急票价</label>
|
|
<input v-model.number="currentFare.cost_express" type="number" class="w-100">
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="danger" @click="deleteCurrentFare"
|
|
v-if="currentFare.exists">删除</button>
|
|
<button @click="saveCurrentFare">保存</button>
|
|
<button class="danger" @click="closeFareModal">取消</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showStationModal" class="modal show">
|
|
<div class="modal-card">
|
|
<h4 class="modal-title">站点编辑</h4>
|
|
<div class="mb-4">
|
|
<label>站点编号</label>
|
|
<input v-model="stationForm.code" class="w-100">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>中文名</label>
|
|
<input v-model="stationForm.name" class="w-100">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>英文名</label>
|
|
<input v-model="stationForm.en_name" class="w-100">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>所属线路</label>
|
|
<select v-model="stationForm.lines" multiple class="w-100" style="height: 100px;">
|
|
<option v-for="l in lines" :key="l.id" :value="l.id">
|
|
{{ l.name || l.id }} ({{ l.id }})
|
|
</option>
|
|
</select>
|
|
<p class="text-muted mt-1" style="font-size: 0.8em;">按住 Ctrl / Command 多选,保存后会自动同步到线路图中。</p>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="switch-label">
|
|
<input type="checkbox" v-model="stationForm.transfer_enabled">
|
|
<span class="slider"></span>
|
|
<span class="label-text">可换乘</span>
|
|
</label>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>可换乘到的站点</label>
|
|
<select v-model="stationForm.transfer_to" multiple class="w-100"
|
|
:disabled="!stationForm.transfer_enabled" style="height: 180px;">
|
|
<option v-for="t in transferTargets" :key="t.code" :value="t.code">
|
|
{{ t.name }} ({{ t.en_name }}) - {{ t.code }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="danger" @click="deleteStation(stationFormOriginalCode)">删除</button>
|
|
<button @click="saveStationSettings">保存</button>
|
|
<button class="danger" @click="closeStationModal">取消</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showLineModal" class="modal show">
|
|
<div class="modal-card">
|
|
<h4 class="modal-title">线路编辑</h4>
|
|
<div class="mb-4">
|
|
<label>线路编号</label>
|
|
<input v-model="lineForm.id" class="w-100">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>中文名</label>
|
|
<input v-model="lineForm.name" class="w-100">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>英文名</label>
|
|
<input v-model="lineForm.en_name" class="w-100">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label>颜色</label>
|
|
<div class="flex" style="gap:8px;">
|
|
<input type="text" v-model="lineForm.color" class="w-100" placeholder="#3366cc">
|
|
<input type="color" v-model="lineForm.color" title="选择颜色"
|
|
style="width: 48px; padding: 0; border: none; height: 32px;">
|
|
</div>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button @click="saveLineSettings">保存</button>
|
|
<button class="danger" @click="closeLineModal">取消</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer class="site-footer">
|
|
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">粤ICP备2025450093号</a>
|
|
<span class="version">v1.0.12</span>
|
|
</footer>
|
|
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
<script src="/custom-dialog.js?v=11"></script>
|
|
<script src="/public-status.js?v=14"></script>
|
|
<script src="ticket-route.js?v=8"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const isDomain = location.hostname.includes('fse-media.group');
|
|
const links = {
|
|
home: isDomain ? 'https://ticket.fse-media.group' : '/home.html',
|
|
order: isDomain ? 'https://ticket.fse-media.group/order' : '/ticket-order.html',
|
|
search: isDomain ? 'https://ticket.fse-media.group/search' : '/ticket-search.html',
|
|
'card-search': isDomain ? 'https://ticket.fse-media.group/ic-card/search' : '/ic-card-search.html',
|
|
'card-order': isDomain ? 'https://ticket.fse-media.group/ic-card/order' : '/ic-card-order.html'
|
|
};
|
|
|
|
document.getElementById('homeLink').href = links.home;
|
|
document.getElementById('routeTopLink').href = links.home;
|
|
document.getElementById('routeBrandLink').href = links.home;
|
|
|
|
document.querySelectorAll('[data-link]').forEach((el) => {
|
|
const key = el.getAttribute('data-link');
|
|
if (links[key]) el.href = links[key];
|
|
});
|
|
});
|
|
</script>
|
|
|
|
|
|
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
|
|
|
|
|