Compare commits
10 Commits
8a8ebd0df4
...
b614ff663c
| Author | SHA1 | Date | |
|---|---|---|---|
| b614ff663c | |||
| e78557f335 | |||
| 2ddcd18e1e | |||
| b1cb84f736 | |||
| 7fea8807b8 | |||
| 108435e90d | |||
| ea5c0a0d5a | |||
| db1562b830 | |||
| d35ae5e75b | |||
| 585e498235 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-1492
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
local URL_ERROR = "http://cloud.fse-media.group/d/API/TicketMachine/error.dfpwm?sign=DvQ39wQDN6Cej4KPhAkM3JyXPM466koan6w9CckPxWU=:0"
|
local URL_ERROR = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/src/commit/108435e90d81c1c0a6e34486dee6cc6efd48ca53/error.dfpwm"
|
||||||
local URL_PASS = "http://cloud.fse-media.group/d/API/TicketMachine/pass.dfpwm?sign=ZJfGDYnaxQU4JrGSh4NDzKZtjq3eMjYh9KHmMSAMKZA=:0"
|
local URL_PASS = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/src/commit/108435e90d81c1c0a6e34486dee6cc6efd48ca53/pass.dfpwm"
|
||||||
local URL_GATE = "http://cloud.fse-media.group/d/API/TicketMachine/gate.lua?sign=OWy1wKkKhUhpnxXKeX6fRLePSg1XcaQgWOLvQbMuHRQ=:0"
|
local URL_GATE = "http://cloud.fse-media.group/d/API/TicketMachine/gate.lua?sign=OWy1wKkKhUhpnxXKeX6fRLePSg1XcaQgWOLvQbMuHRQ=:0"
|
||||||
|
|
||||||
local CONFIG_PATH = "gate_config.json"
|
local CONFIG_PATH = "gate_config.json"
|
||||||
|
|||||||
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
local URL_MACHINE = "http://cloud.fse-media.group/d/API/TicketMachine/ticketmachine.lua?sign=UIiheDcpyzwKdovDZM3G-8IRZqApFgU2Kpnhe9k5ETQ=:0"
|
local URL_MACHINE = "http://gitea.fse-media.group/Henry_Du/FSE-Ticket.sys/src/commit/db1562b83045284bfdec9e4a3feb829193963943/ticketmachine.lua"
|
||||||
|
|
||||||
local function writeFile(path, content, binary)
|
local function writeFile(path, content, binary)
|
||||||
local mode = binary and "wb" or "w"
|
local mode = binary and "wb" or "w"
|
||||||
|
|||||||
+3
-2
@@ -6,7 +6,7 @@
|
|||||||
<title>FMG</title>
|
<title>FMG</title>
|
||||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="style.css?v=12">
|
<link rel="stylesheet" href="style.css?v=13">
|
||||||
<link rel="stylesheet" href="blog.css?v=2">
|
<link rel="stylesheet" href="blog.css?v=2">
|
||||||
</head>
|
</head>
|
||||||
<body class="public-search">
|
<body class="public-search">
|
||||||
@@ -59,7 +59,8 @@ FMG
|
|||||||
<span class="version">v1.0.12</span>
|
<span class="version">v1.0.12</span>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="blog.js?v=2"></script>
|
<script src="blog.js?v=2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -6,7 +6,7 @@
|
|||||||
<title>FSE 铁路票务系统 - 首页</title>
|
<title>FSE 铁路票务系统 - 首页</title>
|
||||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/style.css?v=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
</head>
|
</head>
|
||||||
<body class="public-search jr-public-page">
|
<body class="public-search jr-public-page">
|
||||||
<div class="jr-public-shell">
|
<div class="jr-public-shell">
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="/ai-assistant.js?v=6"></script>
|
<script src="/ai-assistant.js?v=6"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -247,3 +247,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>FSE 閾佽矾绁ㄥ姟绯荤粺 - IC 鍗$鐞?/title>
|
<title>FSE 閾佽矾绁ㄥ姟绯荤粺 - IC 鍗$鐞?/title>
|
||||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/style.css?v=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="jr-admin-page jr-admin-ic-page jr-public-page">
|
<body class="jr-admin-page jr-admin-ic-page jr-public-page">
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="/ic-card-admin.js?v=2"></script>
|
<script src="/ic-card-admin.js?v=2"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -206,3 +206,4 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>IC 卡详情</title>
|
<title>IC 卡详情</title>
|
||||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/style.css?v=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="public-search jr-public-page">
|
<body class="public-search jr-public-page">
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/ic-card-detail.js?v=2"></script>
|
<script src="/ic-card-detail.js?v=2"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script>document.addEventListener('DOMContentLoaded', () => {
|
<script>document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -110,3 +110,4 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>IC 鍗$嚎涓婅喘鍗?/title>
|
<title>IC 鍗$嚎涓婅喘鍗?/title>
|
||||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/style.css?v=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="public-search jr-public-page">
|
<body class="public-search jr-public-page">
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/ic-card-order.js?v=2"></script>
|
<script src="/ic-card-order.js?v=2"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="/ai-assistant.js?v=6"></script>
|
<script src="/ai-assistant.js?v=6"></script>
|
||||||
@@ -129,3 +129,4 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>IC 鍗℃煡璇?/title>
|
<title>IC 鍗℃煡璇?/title>
|
||||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/style.css?v=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="public-search jr-public-page">
|
<body class="public-search jr-public-page">
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/ic-card-search.js?v=2"></script>
|
<script src="/ic-card-search.js?v=2"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script>document.addEventListener('DOMContentLoaded', () => {
|
<script>document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -101,3 +101,4 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
+101
-29
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<!-- 充满未知和不稳定的票务系统! -->
|
<!-- 充满未知和不稳定的票务系统! -->
|
||||||
|
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>FSE铁路票务系统控制台</title>
|
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="style.css?v=12">
|
<link rel="stylesheet" href="style.css?v=14">
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
<script src="/socket.io/socket.io.js"></script>
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -22,11 +22,6 @@
|
|||||||
<i class="fas fa-train"></i>
|
<i class="fas fa-train"></i>
|
||||||
<span>FSE 铁路运输后台系统</span>
|
<span>FSE 铁路运输后台系统</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="jr-top-status is-checking" data-server-status-root>
|
|
||||||
<span class="jr-top-status-label">服务器状态</span>
|
|
||||||
<span class="jr-top-status-dot"></span>
|
|
||||||
<span class="jr-top-status-value" data-server-status-value>检测中</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -88,15 +83,6 @@
|
|||||||
<span class="nav-icon"><i class="fas fa-list"></i></span> 日志
|
<span class="nav-icon"><i class="fas fa-list"></i></span> 日志
|
||||||
</div>
|
</div>
|
||||||
</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; margin-bottom: 15px;">
|
|
||||||
<i class="fas fa-circle"
|
|
||||||
:style="{ color: connected ? '#10b981' : '#ef4444', fontSize: '0.6rem' }"></i>
|
|
||||||
<span>Status: {{ connected ? 'Connected' : 'Disconnected' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="sidebarOpen" class="sidebar-overlay" @click="sidebarOpen = false"></div>
|
<div v-if="sidebarOpen" class="sidebar-overlay" @click="sidebarOpen = false"></div>
|
||||||
|
|
||||||
@@ -112,10 +98,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-admin-header-side">
|
<div class="jr-admin-header-side">
|
||||||
<span class="jr-admin-header-pill" :class="connected ? 'is-online' : 'is-offline'">
|
<div class="jr-admin-sync-meta">
|
||||||
<i class="fas fa-circle"></i>
|
<span class="jr-admin-sync-label">当前模块</span>
|
||||||
{{ connected ? '服务器在线' : '服务器离线' }}
|
<strong>{{ viewTitle }}</strong>
|
||||||
</span>
|
</div>
|
||||||
|
<div class="jr-admin-sync-meta">
|
||||||
|
<span class="jr-admin-sync-label">最近同步</span>
|
||||||
|
<strong>{{ lastSyncText }}</strong>
|
||||||
|
</div>
|
||||||
|
<button class="btn primary" @click="refreshData" :disabled="isViewBusy">
|
||||||
|
<i class="fas" :class="isViewBusy ? 'fa-spinner fa-spin' : 'fa-rotate-right'"></i>
|
||||||
|
{{ isViewBusy ? '同步中' : '刷新视图' }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@@ -131,6 +125,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<p>当前模块:{{ viewTitle }},已加载 {{ lines.length }} 条线路、{{ stations.length }} 个站点。需要做批量调整时,可先在左侧切换模块再进入对应卡片操作区。</p>
|
<p>当前模块:{{ viewTitle }},已加载 {{ lines.length }} 条线路、{{ stations.length }} 个站点。需要做批量调整时,可先在左侧切换模块再进入对应卡片操作区。</p>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="jr-admin-overview-grid">
|
||||||
|
<article class="jr-admin-overview-card">
|
||||||
|
<span class="jr-admin-overview-label">当前模块</span>
|
||||||
|
<strong class="jr-admin-overview-value">{{ viewTitle }}</strong>
|
||||||
|
<p class="jr-admin-overview-note">{{ currentViewSummary }}</p>
|
||||||
|
</article>
|
||||||
|
<article class="jr-admin-overview-card">
|
||||||
|
<span class="jr-admin-overview-label">线路与站点</span>
|
||||||
|
<strong class="jr-admin-overview-value">{{ lines.length }} / {{ stations.length }}</strong>
|
||||||
|
<p class="jr-admin-overview-note">后台操作统一建立在线路、站点与票价的核心数据之上。</p>
|
||||||
|
</article>
|
||||||
|
<article class="jr-admin-overview-card">
|
||||||
|
<span class="jr-admin-overview-label">同步状态</span>
|
||||||
|
<strong class="jr-admin-overview-value">{{ isViewBusy ? '正在更新' : '数据已就绪' }}</strong>
|
||||||
|
<p class="jr-admin-overview-note">切换模块时只拉取当前视图需要的数据,减少等待与无效刷新。</p>
|
||||||
|
</article>
|
||||||
|
<article class="jr-admin-overview-card is-actions">
|
||||||
|
<span class="jr-admin-overview-label">快捷操作</span>
|
||||||
|
<div class="jr-admin-overview-actions">
|
||||||
|
<button class="btn" @click="currentView = 'management'"><i class="fas fa-network-wired"></i> 线路管理</button>
|
||||||
|
<button class="btn" @click="currentView = 'iccards'"><i class="fas fa-credit-card"></i> IC 卡务</button>
|
||||||
|
<button class="btn" @click="currentView = 'logs'"><i class="fas fa-list"></i> 查看日志</button>
|
||||||
|
<button class="btn primary" @click="refreshData" :disabled="isViewBusy"><i class="fas fa-rotate-right"></i> 立即同步</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
<!-- 仪表盘-->
|
<!-- 仪表盘-->
|
||||||
<div v-if="currentView === 'dashboard'">
|
<div v-if="currentView === 'dashboard'">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
@@ -255,8 +275,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 可视化线路编辑-->
|
<!-- 可视化线路编辑-->
|
||||||
<div class="visual-line-container">
|
<div class="visual-line-container"
|
||||||
<svg width="100%" height="200"
|
ref="visualLineViewport"
|
||||||
|
:class="{ 'is-panning': lineViewportPan.active }"
|
||||||
|
@mousedown="startLineViewportPan"
|
||||||
|
@mousemove="moveLineViewportPan">
|
||||||
|
<svg :width="lineEditorSvgWidth" height="200"
|
||||||
v-if="selectedLine.stations && selectedLine.stations.length > 0">
|
v-if="selectedLine.stations && selectedLine.stations.length > 0">
|
||||||
<!--站点连接线-->
|
<!--站点连接线-->
|
||||||
<line x1="50" y1="100" :x2="50 + (selectedLine.stations.length - 1) * 120" y2="100"
|
<line x1="50" y1="100" :x2="50 + (selectedLine.stations.length - 1) * 120" y2="100"
|
||||||
@@ -479,6 +503,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="currentView === 'iccards'">
|
<div v-if="currentView === 'iccards'">
|
||||||
|
<section class="jr-admin-section-toolbar">
|
||||||
|
<div class="jr-admin-section-toolbar-copy">
|
||||||
|
<span class="jr-admin-overview-label">IC CARD DESK</span>
|
||||||
|
<strong>{{ currentViewSummary }}</strong>
|
||||||
|
<p>把检索、充值、状态维护和事件核对集中在同一工作流里,减少在列表和详情之间来回跳转的成本。</p>
|
||||||
|
</div>
|
||||||
|
<div class="jr-admin-overview-actions">
|
||||||
|
<button class="btn" @click="fetchIcCards(false)" :disabled="isViewBusy"><i class="fas fa-list"></i> 刷新列表</button>
|
||||||
|
<button class="btn primary" @click="loadIcCard(icSelectedId)" :disabled="!icSelectedId || isViewBusy"><i class="fas fa-id-card"></i> 刷新详情</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="stat-label">IC 卡总数</div>
|
<div class="stat-label">IC 卡总数</div>
|
||||||
@@ -500,9 +535,24 @@
|
|||||||
|
|
||||||
<div class="management-container ic-admin-layout">
|
<div class="management-container ic-admin-layout">
|
||||||
<div class="management-sidebar">
|
<div class="management-sidebar">
|
||||||
<div class="card" style="margin-bottom:0; display:flex; flex-direction:column; min-height: 520px;">
|
<div class="card jr-admin-note-card">
|
||||||
<div class="flex between mb-4">
|
<div class="flex between mb-4">
|
||||||
<h4>卡片列表</h4>
|
<h4>操作说明</h4>
|
||||||
|
<span class="badge">只读入口</span>
|
||||||
|
</div>
|
||||||
|
<p class="jr-admin-card-note">本模块不提供后台快速建卡,卡片发放流程保持在线上购卡或既有开卡流程中完成,后台仅负责检索、维护、充值与记录核对。</p>
|
||||||
|
<div class="jr-admin-note-list">
|
||||||
|
<div>1. 先检索卡号、订单号或持卡人。</div>
|
||||||
|
<div>2. 在详情面板修改状态并保存。</div>
|
||||||
|
<div>3. 需要补款时直接使用充值入口。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card jr-admin-list-card" style="margin-bottom:0; display:flex; flex-direction:column; min-height: 520px;">
|
||||||
|
<div class="flex between mb-4">
|
||||||
|
<div>
|
||||||
|
<h4>卡片列表</h4>
|
||||||
|
<div class="jr-admin-list-meta">支持按卡号、订单号、凭证码和持卡人姓名检索。</div>
|
||||||
|
</div>
|
||||||
<span class="badge">{{ icCards.length }}</span>
|
<span class="badge">{{ icCards.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mb-4" style="flex-wrap:wrap;">
|
<div class="flex mb-4" style="flex-wrap:wrap;">
|
||||||
@@ -536,8 +586,13 @@
|
|||||||
<div class="management-main">
|
<div class="management-main">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="flex between mb-4">
|
<div class="flex between mb-4">
|
||||||
<h4>卡片详情</h4>
|
<div>
|
||||||
|
<h4>卡片详情</h4>
|
||||||
|
<div class="jr-admin-list-meta">在同一面板直接处理状态维护、充值和记录核对。</div>
|
||||||
|
</div>
|
||||||
<div class="flex" style="gap:8px;">
|
<div class="flex" style="gap:8px;">
|
||||||
|
<button class="btn" @click="loadIcCard(icSelectedId)" :disabled="!icSelectedCard || isViewBusy"><i class="fas fa-rotate-right"></i> 刷新</button>
|
||||||
|
<button class="btn" @click="topupIcCard" :disabled="!icSelectedCard || isViewBusy"><i class="fas fa-wallet"></i> 充值</button>
|
||||||
<button class="btn danger" @click="deleteIcCard" :disabled="!icSelectedCard"><i class="fas fa-trash"></i> 删除卡</button>
|
<button class="btn danger" @click="deleteIcCard" :disabled="!icSelectedCard"><i class="fas fa-trash"></i> 删除卡</button>
|
||||||
<button class="btn primary" @click="saveIcCard" :disabled="!icSelectedCard"><i class="fas fa-save"></i> 保存</button>
|
<button class="btn primary" @click="saveIcCard" :disabled="!icSelectedCard"><i class="fas fa-save"></i> 保存</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -554,6 +609,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="badge" :class="icStatusInfo(icSelectedCard.status).className">{{ icStatusInfo(icSelectedCard.status).text }}</span>
|
<span class="badge" :class="icStatusInfo(icSelectedCard.status).className">{{ icStatusInfo(icSelectedCard.status).text }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="jr-admin-summary-grid">
|
||||||
|
<div class="jr-admin-summary-item">
|
||||||
|
<span>当前余额</span>
|
||||||
|
<strong>{{ formatMoney(icSelectedCard.balance) }}</strong>
|
||||||
|
<small>支持直接发起充值。</small>
|
||||||
|
</div>
|
||||||
|
<div class="jr-admin-summary-item">
|
||||||
|
<span>事件记录</span>
|
||||||
|
<strong>{{ icSelectedEvents.length }}</strong>
|
||||||
|
<small>用于追踪开卡与状态变更。</small>
|
||||||
|
</div>
|
||||||
|
<div class="jr-admin-summary-item">
|
||||||
|
<span>订单来源</span>
|
||||||
|
<strong>{{ cardOrderCode(icSelectedCard) }}</strong>
|
||||||
|
<small>自动识别线上订单或现场办卡。</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="ic-detail-grid">
|
<div class="ic-detail-grid">
|
||||||
<label class="ic-field">
|
<label class="ic-field">
|
||||||
<span>持卡人</span>
|
<span>持卡人</span>
|
||||||
@@ -841,9 +913,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="index.js?v=6"></script>
|
||||||
<script src="index.js?v=2"></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');
|
||||||
@@ -873,3 +944,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+233
-66
@@ -34,8 +34,9 @@ createApp({
|
|||||||
return map[currentView.value] || '票价图';
|
return map[currentView.value] || '票价图';
|
||||||
});
|
});
|
||||||
|
|
||||||
const connected = ref(false);
|
// Prefer polling first so admin remains connected even when the proxy
|
||||||
const socket = io({ transports: ['websocket', 'polling'], upgrade: false });
|
// does not support WebSocket upgrades reliably.
|
||||||
|
const socket = io({ transports: ['polling', 'websocket'] });
|
||||||
|
|
||||||
// Data State
|
// Data State
|
||||||
const stations = ref([]);
|
const stations = ref([]);
|
||||||
@@ -63,6 +64,24 @@ createApp({
|
|||||||
const icCreateForm = reactive({ holder_name: '', balance: 50 });
|
const icCreateForm = reactive({ holder_name: '', balance: 50 });
|
||||||
const icDetailForm = reactive({ holder_name: '', status: 'active' });
|
const icDetailForm = reactive({ holder_name: '', status: 'active' });
|
||||||
let icCardSyncTimer = null;
|
let icCardSyncTimer = null;
|
||||||
|
let icCardSyncBusy = false;
|
||||||
|
let icListRequestSeq = 0;
|
||||||
|
let icDetailRequestSeq = 0;
|
||||||
|
let appMouseupHandler = null;
|
||||||
|
let coreLoaded = false;
|
||||||
|
let ticketDataLoaded = false;
|
||||||
|
let orderDataLoaded = false;
|
||||||
|
let logDataLoaded = false;
|
||||||
|
let assetsLoaded = false;
|
||||||
|
let fareMapLoaded = false;
|
||||||
|
const loadingState = reactive({
|
||||||
|
core: false,
|
||||||
|
tickets: false,
|
||||||
|
orders: false,
|
||||||
|
logs: false,
|
||||||
|
iccards: false
|
||||||
|
});
|
||||||
|
const lastSyncAt = ref(0);
|
||||||
|
|
||||||
// UI State
|
// UI State
|
||||||
const showAddLine = ref(false);
|
const showAddLine = ref(false);
|
||||||
@@ -82,6 +101,15 @@ createApp({
|
|||||||
const showFareModal = ref(false);
|
const showFareModal = ref(false);
|
||||||
const currentFare = reactive({ exists: false, cost_regular: 0, cost_express: 0 });
|
const currentFare = reactive({ exists: false, cost_regular: 0, cost_express: 0 });
|
||||||
const draggingStationIndex = ref(null);
|
const draggingStationIndex = ref(null);
|
||||||
|
const visualLineViewport = ref(null);
|
||||||
|
const lineViewportPan = reactive({
|
||||||
|
active: false,
|
||||||
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
scrollLeft: 0,
|
||||||
|
scrollTop: 0,
|
||||||
|
moved: false
|
||||||
|
});
|
||||||
const showStationModal = ref(false);
|
const showStationModal = ref(false);
|
||||||
const stationForm = reactive({ code: '', name: '', en_name: '', transfer_enabled: false, transfer_to: [] });
|
const stationForm = reactive({ code: '', name: '', en_name: '', transfer_enabled: false, transfer_to: [] });
|
||||||
const stationFormOriginalCode = ref('');
|
const stationFormOriginalCode = ref('');
|
||||||
@@ -103,6 +131,9 @@ createApp({
|
|||||||
confirm: (message) => Promise.resolve(window.confirm(message)),
|
confirm: (message) => Promise.resolve(window.confirm(message)),
|
||||||
prompt: (message, defaultValue) => Promise.resolve(window.prompt(message, defaultValue))
|
prompt: (message, defaultValue) => Promise.resolve(window.prompt(message, defaultValue))
|
||||||
};
|
};
|
||||||
|
const markSynced = () => {
|
||||||
|
lastSyncAt.value = Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
const buildAssetUrl = (name) => {
|
const buildAssetUrl = (name) => {
|
||||||
if (!name) return '';
|
if (!name) return '';
|
||||||
@@ -417,6 +448,7 @@ createApp({
|
|||||||
assetsManifest.updatedAt = data ? (data.updatedAt || null) : null;
|
assetsManifest.updatedAt = data ? (data.updatedAt || null) : null;
|
||||||
assetsRouteMapUrl.value = buildAssetUrl(assetsManifest.routeMap);
|
assetsRouteMapUrl.value = buildAssetUrl(assetsManifest.routeMap);
|
||||||
assetsFareTableUrl.value = buildAssetUrl(assetsManifest.fareTable);
|
assetsFareTableUrl.value = buildAssetUrl(assetsManifest.fareTable);
|
||||||
|
assetsLoaded = true;
|
||||||
|
|
||||||
assetsFarePreview.headers = [];
|
assetsFarePreview.headers = [];
|
||||||
assetsFarePreview.rows = [];
|
assetsFarePreview.rows = [];
|
||||||
@@ -454,6 +486,7 @@ createApp({
|
|||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
markSynced();
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadAssetFile = async (url, file) => {
|
const uploadAssetFile = async (url, file) => {
|
||||||
@@ -631,36 +664,90 @@ createApp({
|
|||||||
draggingStationIndex.value = null;
|
draggingStationIndex.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startLineViewportPan = (event) => {
|
||||||
|
const viewport = visualLineViewport.value;
|
||||||
|
if (!viewport) return;
|
||||||
|
if (event.button !== 0) return;
|
||||||
|
if (event.target && event.target.closest('.station-node')) return;
|
||||||
|
lineViewportPan.active = true;
|
||||||
|
lineViewportPan.moved = false;
|
||||||
|
lineViewportPan.startX = event.clientX;
|
||||||
|
lineViewportPan.startY = event.clientY;
|
||||||
|
lineViewportPan.scrollLeft = viewport.scrollLeft;
|
||||||
|
lineViewportPan.scrollTop = viewport.scrollTop;
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveLineViewportPan = (event) => {
|
||||||
|
if (!lineViewportPan.active) return;
|
||||||
|
const viewport = visualLineViewport.value;
|
||||||
|
if (!viewport) return;
|
||||||
|
const deltaX = event.clientX - lineViewportPan.startX;
|
||||||
|
const deltaY = event.clientY - lineViewportPan.startY;
|
||||||
|
if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2) {
|
||||||
|
lineViewportPan.moved = true;
|
||||||
|
}
|
||||||
|
viewport.scrollLeft = lineViewportPan.scrollLeft - deltaX;
|
||||||
|
viewport.scrollTop = lineViewportPan.scrollTop - deltaY;
|
||||||
|
};
|
||||||
|
|
||||||
|
const endLineViewportPan = () => {
|
||||||
|
lineViewportPan.active = false;
|
||||||
|
};
|
||||||
|
|
||||||
// --- Order Management ---
|
// --- Order Management ---
|
||||||
const fetchOrders = async () => {
|
const fetchOrders = async () => {
|
||||||
|
if (loadingState.orders) return;
|
||||||
|
loadingState.orders = true;
|
||||||
try {
|
try {
|
||||||
const res = await requestJson('/api/orders');
|
const res = await requestJson('/api/orders');
|
||||||
if (res && res.ok) orders.value = res.orders;
|
if (res && res.ok) {
|
||||||
} catch (e) { console.error(e); }
|
orders.value = res.orders || [];
|
||||||
|
orderDataLoaded = true;
|
||||||
|
markSynced();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
loadingState.orders = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchIcCards = async (keepSelection = true) => {
|
const fetchIcCards = async (keepSelection = true) => {
|
||||||
|
if (loadingState.iccards) return;
|
||||||
|
loadingState.iccards = true;
|
||||||
|
const requestSeq = ++icListRequestSeq;
|
||||||
const sp = new URLSearchParams();
|
const sp = new URLSearchParams();
|
||||||
if (icCardSearch.value.trim()) sp.set('q', icCardSearch.value.trim());
|
if (icCardSearch.value.trim()) sp.set('q', icCardSearch.value.trim());
|
||||||
const res = await requestJson(`/api/ic-cards?${sp.toString()}`);
|
try {
|
||||||
icCards.value = res?.cards || [];
|
const res = await requestJson(`/api/ic-cards?${sp.toString()}`);
|
||||||
if (keepSelection && icSelectedId.value && icCards.value.some((card) => card.card_id === icSelectedId.value)) {
|
if (requestSeq !== icListRequestSeq) return;
|
||||||
await loadIcCard(icSelectedId.value);
|
icCards.value = res?.cards || [];
|
||||||
} else if (icSelectedId.value && !icCards.value.some((card) => card.card_id === icSelectedId.value)) {
|
if (keepSelection && icSelectedId.value && icCards.value.some((card) => card.card_id === icSelectedId.value)) {
|
||||||
icSelectedId.value = '';
|
await loadIcCard(icSelectedId.value);
|
||||||
icSelectedCard.value = null;
|
} else if (icSelectedId.value && !icCards.value.some((card) => card.card_id === icSelectedId.value)) {
|
||||||
icSelectedEvents.value = [];
|
icSelectedId.value = '';
|
||||||
|
icSelectedCard.value = null;
|
||||||
|
icSelectedEvents.value = [];
|
||||||
|
}
|
||||||
|
markSynced();
|
||||||
|
} finally {
|
||||||
|
if (requestSeq === icListRequestSeq) {
|
||||||
|
loadingState.iccards = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadIcCard = async (id) => {
|
const loadIcCard = async (id) => {
|
||||||
|
const requestSeq = ++icDetailRequestSeq;
|
||||||
const res = await requestJson(`/api/ic-cards/${encodeURIComponent(id)}`);
|
const res = await requestJson(`/api/ic-cards/${encodeURIComponent(id)}`);
|
||||||
|
if (requestSeq !== icDetailRequestSeq) return;
|
||||||
const card = res?.card || null;
|
const card = res?.card || null;
|
||||||
icSelectedId.value = id;
|
icSelectedId.value = id;
|
||||||
icSelectedCard.value = card;
|
icSelectedCard.value = card;
|
||||||
icSelectedEvents.value = res?.events || [];
|
icSelectedEvents.value = res?.events || [];
|
||||||
icDetailForm.holder_name = card?.holder_name || '';
|
icDetailForm.holder_name = card?.holder_name || '';
|
||||||
icDetailForm.status = card?.status || 'active';
|
icDetailForm.status = card?.status || 'active';
|
||||||
|
markSynced();
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncSelectedIcCard = async () => {
|
const syncSelectedIcCard = async () => {
|
||||||
@@ -683,9 +770,15 @@ createApp({
|
|||||||
stopIcCardSync();
|
stopIcCardSync();
|
||||||
if (currentView.value !== 'iccards') return;
|
if (currentView.value !== 'iccards') return;
|
||||||
icCardSyncTimer = setInterval(() => {
|
icCardSyncTimer = setInterval(() => {
|
||||||
fetchIcCards(false).catch(console.error);
|
if (document.hidden || icCardSyncBusy) return;
|
||||||
syncSelectedIcCard().catch(console.error);
|
icCardSyncBusy = true;
|
||||||
}, 3000);
|
Promise.all([
|
||||||
|
fetchIcCards(false).catch(console.error),
|
||||||
|
icSelectedId.value ? syncSelectedIcCard().catch(console.error) : Promise.resolve()
|
||||||
|
]).finally(() => {
|
||||||
|
icCardSyncBusy = false;
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createIcCard = async () => {
|
const createIcCard = async () => {
|
||||||
@@ -775,10 +868,15 @@ createApp({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchLogs = async () => {
|
const fetchLogs = async () => {
|
||||||
|
if (logLoading.value) return;
|
||||||
logLoading.value = true;
|
logLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await requestJson(buildLogsUrl());
|
const res = await requestJson(buildLogsUrl());
|
||||||
if (res && res.ok) logs.value = res.logs || [];
|
if (res && res.ok) {
|
||||||
|
logs.value = res.logs || [];
|
||||||
|
logDataLoaded = true;
|
||||||
|
markSynced();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -823,66 +921,67 @@ createApp({
|
|||||||
return `¥${reg} / ¥${exp}`;
|
return `¥${reg} / ¥${exp}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchCoreData = async ({ force = false } = {}) => {
|
||||||
|
if (loadingState.core) return;
|
||||||
|
if (coreLoaded && !force) return;
|
||||||
|
loadingState.core = true;
|
||||||
try {
|
try {
|
||||||
const safeFetch = (url, defaultVal) => requestJson(url).catch(e => {
|
const safeFetch = (url, defaultVal) => requestJson(url).catch((e) => {
|
||||||
console.error(`Fetch failed for ${url}`, e);
|
console.error(`Fetch failed for ${url}`, e);
|
||||||
lastActionError.value = e?.message || String(e);
|
lastActionError.value = e?.message || String(e);
|
||||||
return defaultVal;
|
return defaultVal;
|
||||||
});
|
});
|
||||||
|
const [s, l, f, c, st] = await Promise.all([
|
||||||
const safeFetchList = (url, key) => requestJson(url).then(d => d?.[key] || []).catch(e => {
|
|
||||||
console.error(`Fetch list failed for ${url}`, e);
|
|
||||||
lastActionError.value = e?.message || String(e);
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
const [s, l, f, c, t, lg, st, ord, cards] = await Promise.all([
|
|
||||||
safeFetch('/api/stations', []),
|
safeFetch('/api/stations', []),
|
||||||
safeFetch('/api/lines', []),
|
safeFetch('/api/lines', []),
|
||||||
safeFetch('/api/fares', []),
|
safeFetch('/api/fares', []),
|
||||||
safeFetch('/api/config', {}),
|
safeFetch('/api/config', {}),
|
||||||
safeFetchList('/api/tickets', 'tickets'),
|
safeFetch('/api/stats/ticket/total', {}).then((d) => d.total || {})
|
||||||
safeFetchList(buildLogsUrl(), 'logs'),
|
|
||||||
safeFetch('/api/stats/ticket/total', {}).then(d => d.total || {}),
|
|
||||||
safeFetchList('/api/orders', 'orders'),
|
|
||||||
safeFetchList('/api/ic-cards', 'cards')
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
stations.value = s;
|
stations.value = s;
|
||||||
lines.value = l;
|
lines.value = l;
|
||||||
fares.value = f;
|
fares.value = f;
|
||||||
Object.assign(config, c);
|
Object.assign(config, c);
|
||||||
tickets.value = t;
|
|
||||||
logs.value = lg;
|
|
||||||
Object.assign(stats, st);
|
Object.assign(stats, st);
|
||||||
orders.value = ord;
|
|
||||||
icCards.value = cards;
|
|
||||||
|
|
||||||
// Refresh selected line if it exists
|
|
||||||
if (selectedLine.value) {
|
if (selectedLine.value) {
|
||||||
const found = lines.value.find(l => l.id === selectedLine.value.id);
|
const found = lines.value.find((line) => line.id === selectedLine.value.id);
|
||||||
if (found) selectedLine.value = found;
|
selectedLine.value = found || null;
|
||||||
}
|
}
|
||||||
if (icSelectedId.value && icCards.value.some((card) => card.card_id === icSelectedId.value)) {
|
coreLoaded = true;
|
||||||
await loadIcCard(icSelectedId.value);
|
markSynced();
|
||||||
}
|
|
||||||
|
|
||||||
loadFareMap();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to fetch data", e);
|
console.error('Failed to fetch core data', e);
|
||||||
|
} finally {
|
||||||
|
loadingState.core = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadFareMap = async () => {
|
const fetchTicketData = async () => {
|
||||||
|
if (loadingState.tickets) return;
|
||||||
|
loadingState.tickets = true;
|
||||||
|
try {
|
||||||
|
const res = await requestJson('/api/tickets');
|
||||||
|
tickets.value = res?.tickets || [];
|
||||||
|
ticketDataLoaded = true;
|
||||||
|
markSynced();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch tickets', e);
|
||||||
|
} finally {
|
||||||
|
loadingState.tickets = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFareMap = async ({ force = false } = {}) => {
|
||||||
|
if (fareMapLoading.value) return;
|
||||||
|
if (fareMapLoaded && !force) return;
|
||||||
fareMapLoading.value = true;
|
fareMapLoading.value = true;
|
||||||
fareMapError.value = '';
|
fareMapError.value = '';
|
||||||
try {
|
try {
|
||||||
// Change to fetch the SVG text directly from the public API
|
|
||||||
// Add timestamp to prevent caching
|
|
||||||
const r = await fetch(`/api/public/fares/map/light?t=${Date.now()}`);
|
const r = await fetch(`/api/public/fares/map/light?t=${Date.now()}`);
|
||||||
const svg = await r.text();
|
const svg = await r.text();
|
||||||
fareMapSvg.value = svg;
|
fareMapSvg.value = svg;
|
||||||
|
fareMapLoaded = true;
|
||||||
|
markSynced();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load fare map", e);
|
console.error("Failed to load fare map", e);
|
||||||
fareMapError.value = '加载失败';
|
fareMapError.value = '加载失败';
|
||||||
@@ -891,6 +990,25 @@ createApp({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ensureViewData = async (view = currentView.value, { force = false } = {}) => {
|
||||||
|
await fetchCoreData({ force });
|
||||||
|
if (view === 'tickets' && (force || !ticketDataLoaded)) await fetchTicketData();
|
||||||
|
if (view === 'vouchers' && (force || !orderDataLoaded)) await fetchOrders();
|
||||||
|
if (view === 'logs' && (force || !logDataLoaded)) await fetchLogs();
|
||||||
|
if (view === 'iccards') {
|
||||||
|
await fetchIcCards(true);
|
||||||
|
if (icSelectedId.value) {
|
||||||
|
await syncSelectedIcCard().catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (view === 'faremap' && (force || !fareMapLoaded)) await loadFareMap({ force });
|
||||||
|
if (view === 'assets' && (!assetsLoaded || force)) await fetchAssetsManifest();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
await ensureViewData(currentView.value, { force: true });
|
||||||
|
};
|
||||||
|
|
||||||
const zoomFareMapIn = () => { fareMapScale.value = Math.min(3, Math.round((fareMapScale.value + 0.1) * 100) / 100); };
|
const zoomFareMapIn = () => { fareMapScale.value = Math.min(3, Math.round((fareMapScale.value + 0.1) * 100) / 100); };
|
||||||
const zoomFareMapOut = () => { fareMapScale.value = Math.max(0.3, Math.round((fareMapScale.value - 0.1) * 100) / 100); };
|
const zoomFareMapOut = () => { fareMapScale.value = Math.max(0.3, Math.round((fareMapScale.value - 0.1) * 100) / 100); };
|
||||||
const zoomFareMapReset = () => { fareMapScale.value = 1; };
|
const zoomFareMapReset = () => { fareMapScale.value = 1; };
|
||||||
@@ -1093,6 +1211,10 @@ createApp({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleStationClick = async (code) => {
|
const handleStationClick = async (code) => {
|
||||||
|
if (lineViewportPan.moved) {
|
||||||
|
lineViewportPan.moved = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (stationEditMode.value) {
|
if (stationEditMode.value) {
|
||||||
openStationModal(code);
|
openStationModal(code);
|
||||||
return;
|
return;
|
||||||
@@ -1231,17 +1353,18 @@ createApp({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Socket Listeners
|
// Socket Listeners
|
||||||
socket.on('connect', () => { connected.value = true; });
|
|
||||||
socket.on('disconnect', () => { connected.value = false; });
|
|
||||||
|
|
||||||
socket.on('stations:updated', (data) => {
|
socket.on('stations:updated', (data) => {
|
||||||
stations.value = data;
|
stations.value = data;
|
||||||
// Refresh map when stations change
|
// Refresh map when stations change
|
||||||
loadFareMap();
|
fareMapLoaded = false;
|
||||||
|
if (currentView.value === 'faremap') {
|
||||||
|
loadFareMap({ force: true });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('lines:updated', (data) => {
|
socket.on('lines:updated', (data) => {
|
||||||
lines.value = data;
|
lines.value = data;
|
||||||
|
coreLoaded = true;
|
||||||
// Update selectedLine reference if it exists
|
// Update selectedLine reference if it exists
|
||||||
if (selectedLine.value) {
|
if (selectedLine.value) {
|
||||||
const updated = data.find(l => l.id === selectedLine.value.id);
|
const updated = data.find(l => l.id === selectedLine.value.id);
|
||||||
@@ -1251,14 +1374,28 @@ createApp({
|
|||||||
selectedLine.value = null; // Line was deleted
|
selectedLine.value = null; // Line was deleted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadFareMap();
|
fareMapLoaded = false;
|
||||||
|
if (currentView.value === 'faremap') {
|
||||||
|
loadFareMap({ force: true });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('fares:updated', (data) => {
|
socket.on('fares:updated', (data) => {
|
||||||
fares.value = data;
|
fares.value = data;
|
||||||
loadFareMap();
|
coreLoaded = true;
|
||||||
|
fareMapLoaded = false;
|
||||||
|
if (currentView.value === 'faremap') {
|
||||||
|
loadFareMap({ force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
socket.on('config:updated', (data) => {
|
||||||
|
Object.assign(config, data);
|
||||||
|
coreLoaded = true;
|
||||||
|
fareMapLoaded = false;
|
||||||
|
if (currentView.value === 'faremap') {
|
||||||
|
loadFareMap({ force: true });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
socket.on('config:updated', (data) => { Object.assign(config, data); loadFareMap(); });
|
|
||||||
|
|
||||||
socket.on('stats:ticket:updated', (item) => {
|
socket.on('stats:ticket:updated', (item) => {
|
||||||
stats.sold_tickets += item.sold_tickets;
|
stats.sold_tickets += item.sold_tickets;
|
||||||
@@ -1281,15 +1418,12 @@ createApp({
|
|||||||
|
|
||||||
watch(currentView, (v) => {
|
watch(currentView, (v) => {
|
||||||
sidebarOpen.value = false;
|
sidebarOpen.value = false;
|
||||||
if (v === 'assets') fetchAssetsManifest();
|
|
||||||
if (v === 'logs') fetchLogs();
|
|
||||||
if (v === 'iccards') {
|
if (v === 'iccards') {
|
||||||
fetchIcCards(true).catch(console.error);
|
|
||||||
syncSelectedIcCard().catch(() => {});
|
|
||||||
startIcCardSync();
|
startIcCardSync();
|
||||||
} else {
|
} else {
|
||||||
stopIcCardSync();
|
stopIcCardSync();
|
||||||
}
|
}
|
||||||
|
ensureViewData(v).catch(console.error);
|
||||||
const sp = new URLSearchParams(location.search);
|
const sp = new URLSearchParams(location.search);
|
||||||
if (v === 'dashboard') sp.delete('view');
|
if (v === 'dashboard') sp.delete('view');
|
||||||
else sp.set('view', v);
|
else sp.set('view', v);
|
||||||
@@ -1300,13 +1434,12 @@ createApp({
|
|||||||
|
|
||||||
// Initial Load
|
// Initial Load
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData();
|
ensureViewData(currentView.value, { force: true }).catch(console.error);
|
||||||
fetchAssetsManifest();
|
|
||||||
if (currentView.value === 'iccards') {
|
if (currentView.value === 'iccards') {
|
||||||
fetchIcCards(true).catch(console.error);
|
|
||||||
startIcCardSync();
|
startIcCardSync();
|
||||||
}
|
}
|
||||||
window.addEventListener('mouseup', async () => {
|
appMouseupHandler = async () => {
|
||||||
|
endLineViewportPan();
|
||||||
if (draggingStationIndex.value !== null) {
|
if (draggingStationIndex.value !== null) {
|
||||||
if (selectedLine.value) {
|
if (selectedLine.value) {
|
||||||
try {
|
try {
|
||||||
@@ -1323,16 +1456,48 @@ createApp({
|
|||||||
}
|
}
|
||||||
draggingStationIndex.value = null;
|
draggingStationIndex.value = null;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
window.addEventListener('mouseup', appMouseupHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopIcCardSync();
|
stopIcCardSync();
|
||||||
|
if (appMouseupHandler) {
|
||||||
|
window.removeEventListener('mouseup', appMouseupHandler);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const recentLogs = computed(() => logs.value);
|
const recentLogs = computed(() => logs.value);
|
||||||
const orderList = computed(() => orders.value);
|
const orderList = computed(() => orders.value);
|
||||||
|
const lineEditorSvgWidth = computed(() => {
|
||||||
|
const count = Array.isArray(selectedLine.value?.stations) ? selectedLine.value.stations.length : 0;
|
||||||
|
return Math.max(960, 100 + Math.max(0, count - 1) * 120 + 120);
|
||||||
|
});
|
||||||
|
const lastSyncText = computed(() => lastSyncAt.value ? formatTime(lastSyncAt.value) : '尚未同步');
|
||||||
|
const isViewBusy = computed(() => {
|
||||||
|
if (loadingState.core) return true;
|
||||||
|
if (currentView.value === 'tickets') return loadingState.tickets;
|
||||||
|
if (currentView.value === 'vouchers') return loadingState.orders;
|
||||||
|
if (currentView.value === 'logs') return logLoading.value;
|
||||||
|
if (currentView.value === 'iccards') return loadingState.iccards;
|
||||||
|
if (currentView.value === 'faremap') return fareMapLoading.value;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
const currentViewSummary = computed(() => {
|
||||||
|
const map = {
|
||||||
|
dashboard: `已同步 ${lines.value.length} 条线路与 ${stations.value.length} 个站点`,
|
||||||
|
management: `当前可编辑 ${lines.value.length} 条线路与 ${stations.value.length} 个站点`,
|
||||||
|
faremap: fareMapLoading.value ? '票价图正在生成中' : '可导出当前铁路票价图',
|
||||||
|
tickets: `已加载 ${ticketList.value.length} 条车票记录`,
|
||||||
|
vouchers: `已加载 ${orders.value.length} 条凭证记录`,
|
||||||
|
iccards: `当前检索到 ${icCards.value.length} 张 IC 卡`,
|
||||||
|
assets: assetsManifest.routeMap ? `已上传线路图 ${assetsManifest.routeMap}` : '尚未上传线路图',
|
||||||
|
settings: '可维护优惠活动与导出数据',
|
||||||
|
logs: `当前筛选结果 ${logs.value.length} 条日志`
|
||||||
|
};
|
||||||
|
return map[currentView.value] || '后台模块已就绪';
|
||||||
|
});
|
||||||
const icCardStats = computed(() => ({
|
const icCardStats = computed(() => ({
|
||||||
total: icCards.value.length,
|
total: icCards.value.length,
|
||||||
pending: icCards.value.filter((card) => card.status === 'pending_pickup').length,
|
pending: icCards.value.filter((card) => card.status === 'pending_pickup').length,
|
||||||
@@ -1388,7 +1553,8 @@ createApp({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentView, viewTitle, connected, sidebarOpen,
|
currentView, viewTitle, sidebarOpen,
|
||||||
|
loadingState, isViewBusy, lastSyncText, currentViewSummary,
|
||||||
stations, lines, fares, stats, config, recentLogs, ticketList,
|
stations, lines, fares, stats, config, recentLogs, ticketList,
|
||||||
logs, logCategory, logTypeFilter, logQuery, logMax, logLoading, fetchLogs,
|
logs, logCategory, logTypeFilter, logQuery, logMax, logLoading, fetchLogs,
|
||||||
orders, orderList, fetchOrders, deleteOrder,
|
orders, orderList, fetchOrders, deleteOrder,
|
||||||
@@ -1400,6 +1566,7 @@ createApp({
|
|||||||
|
|
||||||
// Management
|
// Management
|
||||||
selectedLine, fareMode, stationEditMode, fareSelection, showFareModal, currentFare, availableStations,
|
selectedLine, fareMode, stationEditMode, fareSelection, showFareModal, currentFare, availableStations,
|
||||||
|
visualLineViewport, lineViewportPan, lineEditorSvgWidth, startLineViewportPan, moveLineViewportPan,
|
||||||
selectLine, createLine, deleteLine, deleteStation, updateLineInfo, getFareText,
|
selectLine, createLine, deleteLine, deleteStation, updateLineInfo, getFareText,
|
||||||
isStationInLine, addStationToLine, removeStationFromLine,
|
isStationInLine, addStationToLine, removeStationFromLine,
|
||||||
handleStationClick, isStationSelected,
|
handleStationClick, isStationSelected,
|
||||||
|
|||||||
+17
-23
@@ -4,22 +4,17 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>鎺у埗鍙扮櫥褰?/title>
|
<title>后台控制台登录</title>
|
||||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
||||||
<link rel="stylesheet" href="/style.css?v=12" />
|
<link rel="stylesheet" href="/style.css?v=13" />
|
||||||
</head>
|
</head>
|
||||||
<body class="jr-admin-login-page">
|
<body class="jr-admin-login-page">
|
||||||
<div class="jr-admin-login-shell">
|
<div class="jr-admin-login-shell">
|
||||||
<header class="jr-topbar">
|
<header class="jr-topbar">
|
||||||
<div class="jr-topbar-inner">
|
<div class="jr-topbar-inner">
|
||||||
<a href="/" class="jr-top-link">
|
<a href="/" class="jr-top-link">
|
||||||
<span>FSE閾佽矾绁ㄥ姟绯荤粺鎺у埗鍙?/span>
|
<span>FSE铁路票务系统控制台</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="jr-top-status is-checking" data-server-status-root>
|
|
||||||
<span class="jr-top-status-label">鏈嶅姟鍣ㄧ姸鎬?/span>
|
|
||||||
<span class="jr-top-status-dot"></span>
|
|
||||||
<span class="jr-top-status-value" data-server-status-value>妫€娴嬩腑</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -28,8 +23,8 @@
|
|||||||
<a href="/" class="jr-brand">
|
<a href="/" class="jr-brand">
|
||||||
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
||||||
<div class="jr-brand-copy">
|
<div class="jr-brand-copy">
|
||||||
<strong>FSE 閾佽矾杩愯緭</strong>
|
<strong>FSE 铁路运输</strong>
|
||||||
<span>鎺у埗鍙扮櫥褰?/span>
|
<span>控制台登录</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,38 +34,37 @@
|
|||||||
<section class="jr-admin-login-panel">
|
<section class="jr-admin-login-panel">
|
||||||
<div class="jr-admin-login-copy">
|
<div class="jr-admin-login-copy">
|
||||||
<span class="jr-kicker">OPERATIONS ACCESS</span>
|
<span class="jr-kicker">OPERATIONS ACCESS</span>
|
||||||
<h1>鍚庡彴鎺у埗鍙?/h1>
|
<h1>后台控制台</h1>
|
||||||
<p>绾胯矾缁存姢銆佺エ鎹鐞嗐€佹棩蹇楁煡璇笌 IC 鍗$鐞?/p>
|
<p>线路维护、票务管理、日志查询与 IC 卡管理统一从这里进入。</p>
|
||||||
<ul class="jr-admin-login-points">
|
<ul class="jr-admin-login-points">
|
||||||
<li>缁熶竴绠$悊绾胯矾銆佺エ浠峰拰璧勬簮鍥炬枃浠?/li>
|
<li>统一管理线路、票价和资源图文件</li>
|
||||||
<li>鏌ョ湅鐢靛瓙绁ㄣ€佸嚟璇佷笌鎿嶄綔鏃ュ織</li>
|
<li>查看电子票、凭证与操作日志</li>
|
||||||
<li>缁存姢 IC 鍗″彂琛屻€佸厖鍊间笌鐘舵€佽褰?/li>
|
<li>维护 IC 卡发放、充值与状态记录</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="jr-admin-login-card">
|
<section class="jr-admin-login-card">
|
||||||
<div class="jr-page-intro jr-page-intro-compact">
|
<div class="jr-page-intro jr-page-intro-compact">
|
||||||
<span class="jr-kicker">SIGN IN</span>
|
<span class="jr-kicker">SIGN IN</span>
|
||||||
<h2>鎺у埗鍙扮櫥褰?/h2>
|
<h2>控制台登录</h2>
|
||||||
<p>璇疯緭鍏ョ鐞嗗憳璐﹀彿鍜屽瘑鐮併€?/p>
|
<p>请输入管理员账号和密码。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-row"><input id="loginUser" type="text" placeholder="鐢ㄦ埛鍚? /></div>
|
<div class="login-row"><input id="loginUser" type="text" placeholder="用户名" /></div>
|
||||||
<div class="login-row"><input id="loginPass" type="password" placeholder="瀵嗙爜" /></div>
|
<div class="login-row"><input id="loginPass" type="password" placeholder="密码" /></div>
|
||||||
<div class="login-actions">
|
<div class="login-actions">
|
||||||
<button id="loginBtn" class="btn primary">鐧诲綍</button>
|
<button id="loginBtn" class="btn primary">登录</button>
|
||||||
<span id="loginHint" class="hint"></span>
|
<span id="loginHint" class="hint"></span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">绮CP澶?025450093鍙?/a>
|
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">粤ICP备2025450093号</a>
|
||||||
<span class="version">v1.0.12</span>
|
<span class="version">v1.0.12</span>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
|
||||||
<script src="login.js?v=2"></script>
|
<script src="login.js?v=2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+230
-9
@@ -28,6 +28,8 @@
|
|||||||
html, body {
|
html, body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -684,6 +686,7 @@ main {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.management-main {
|
.management-main {
|
||||||
@@ -700,6 +703,7 @@ main {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-item {
|
.line-item {
|
||||||
@@ -769,18 +773,26 @@ main {
|
|||||||
|
|
||||||
.visual-line-container {
|
.visual-line-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-x: auto;
|
overflow: auto;
|
||||||
overflow-y: hidden;
|
|
||||||
background-color: #00000022;
|
background-color: #00000022;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
|
min-height: 0;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visual-line-container svg {
|
.visual-line-container svg {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-line-container.is-panning {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.station-node {
|
.station-node {
|
||||||
@@ -3381,6 +3393,12 @@ body.jr-ticket-board-page .jr-board-card:last-child {
|
|||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ic-admin-layout .management-sidebar,
|
||||||
|
.ic-admin-layout .management-main,
|
||||||
|
.jr-admin-list-card {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.ic-form-grid {
|
.ic-form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -3727,6 +3745,31 @@ body.jr-admin-login-page {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-sync-meta {
|
||||||
|
min-width: 118px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #d8e2d4;
|
||||||
|
background: #f8fbf7;
|
||||||
|
color: #385446;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-sync-meta strong {
|
||||||
|
display: block;
|
||||||
|
color: #143423;
|
||||||
|
font-size: 0.94rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-sync-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #6a7d72;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jr-admin-header-pill {
|
.jr-admin-header-pill {
|
||||||
@@ -3762,6 +3805,149 @@ body.jr-admin-login-page {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-card {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #d7e0d3;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 10px 24px rgba(18, 50, 33, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-card.is-actions {
|
||||||
|
background: linear-gradient(180deg, #f8fbf7 0, #ffffff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-label {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #0b6b3a;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-value {
|
||||||
|
display: block;
|
||||||
|
color: #143423;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-note {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: #627368;
|
||||||
|
line-height: 1.7;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-actions,
|
||||||
|
.jr-admin-card-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-actions {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-section-toolbar {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding: 20px 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
border: 1px solid #d7e0d3;
|
||||||
|
background: linear-gradient(135deg, rgba(11, 107, 58, 0.05) 0, rgba(11, 107, 58, 0.015) 28%, #ffffff 28%, #ffffff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-section-toolbar-copy {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-section-toolbar-copy strong {
|
||||||
|
display: block;
|
||||||
|
color: #143423;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-section-toolbar-copy p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #627368;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-card-note,
|
||||||
|
.jr-admin-list-meta {
|
||||||
|
color: #6a7c70;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-note-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 14px;
|
||||||
|
color: #3c594a;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-list-card .jr-scroll-box {
|
||||||
|
padding-right: 4px;
|
||||||
|
min-height: 320px;
|
||||||
|
max-height: 560px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-summary-item {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid #dbe5d8;
|
||||||
|
background: #f8fbf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-summary-item span,
|
||||||
|
.jr-admin-summary-item small {
|
||||||
|
display: block;
|
||||||
|
color: #687a70;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-summary-item span {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-summary-item strong {
|
||||||
|
display: block;
|
||||||
|
color: #143423;
|
||||||
|
font-size: 1.18rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-summary-item small {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
.jr-admin-page .card {
|
.jr-admin-page .card {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #d7e0d3;
|
border: 1px solid #d7e0d3;
|
||||||
@@ -3962,6 +4148,16 @@ body.jr-admin-login-page {
|
|||||||
.jr-admin-login-panel {
|
.jr-admin-login-panel {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-grid,
|
||||||
|
.jr-admin-summary-grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-section-toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -3980,6 +4176,38 @@ body.jr-admin-login-page {
|
|||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jr-admin-login-copy,
|
||||||
|
.jr-admin-login-card {
|
||||||
|
padding: 22px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-header-side,
|
||||||
|
.jr-admin-overview-actions,
|
||||||
|
.jr-admin-card-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-sync-meta,
|
||||||
|
.jr-admin-overview-grid,
|
||||||
|
.jr-admin-summary-grid {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-grid,
|
||||||
|
.jr-admin-summary-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-overview-card,
|
||||||
|
.jr-admin-section-toolbar {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-admin-header-side .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Custom Dialog --- */
|
/* --- Custom Dialog --- */
|
||||||
|
|
||||||
@@ -4102,10 +4330,3 @@ body.jr-admin-login-page {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.jr-admin-login-copy,
|
|
||||||
.jr-admin-login-card {
|
|
||||||
padding: 22px;
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+106
-88
@@ -4,10 +4,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>FSE閾佽矾鐢靛瓙瀹㈢エ</title>
|
<title>FSE铁路电子客票</title>
|
||||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/style.css?v=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
<style>
|
<style>
|
||||||
[v-cloak] {
|
[v-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -49,12 +49,12 @@
|
|||||||
<div class="jr-topbar-inner">
|
<div class="jr-topbar-inner">
|
||||||
<a href="javascript:void(0)" @click="goHome" class="jr-top-link">
|
<a href="javascript:void(0)" @click="goHome" class="jr-top-link">
|
||||||
<i class="fas fa-arrow-left"></i>
|
<i class="fas fa-arrow-left"></i>
|
||||||
<span>杩斿洖鏌ヨ</span>
|
<span>返回查询</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="jr-top-status is-checking" data-server-status-root>
|
<div class="jr-top-status is-checking" data-server-status-root>
|
||||||
<span class="jr-top-status-label">鏈嶅姟鍣ㄧ姸鎬?/span>
|
<span class="jr-top-status-label">服务器状态</span>
|
||||||
<span class="jr-top-status-dot"></span>
|
<span class="jr-top-status-dot"></span>
|
||||||
<span class="jr-top-status-value" data-server-status-value>妫€娴嬩腑</span>
|
<span class="jr-top-status-value" data-server-status-value>检测中</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -63,28 +63,28 @@
|
|||||||
<a href="javascript:void(0)" @click="goHome" class="jr-brand">
|
<a href="javascript:void(0)" @click="goHome" class="jr-brand">
|
||||||
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
||||||
<div class="jr-brand-copy">
|
<div class="jr-brand-copy">
|
||||||
<strong>FSE閾佽矾绁ㄥ姟绯荤粺</strong>
|
<strong>FSE铁路票务系统</strong>
|
||||||
<span>鐢靛瓙瀹㈢エ淇℃伅</span>
|
<span>电子客票信息</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<nav class="jr-nav" aria-label="绔欑偣瀵艰埅">
|
<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/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/order" data-link="order">线上预定</a>
|
||||||
<a href="https://ticket.fse-media.group/search" data-link="search" class="is-active">杞︾エ鏌ヨ</a>
|
<a href="https://ticket.fse-media.group/search" data-link="search" class="is-active">车票查询</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/search" data-link="card-search">IC 卡查询</a>
|
||||||
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">绾夸笂璐崱</a>
|
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<main class="jr-public-main">
|
<main class="jr-public-main">
|
||||||
<section class="jr-page-intro">
|
<section class="jr-page-intro">
|
||||||
<span class="jr-kicker">ELECTRONIC TICKET</span>
|
<span class="jr-kicker">ELECTRONIC TICKET</span>
|
||||||
<h1>鏌ョ湅杞︾エ鐘舵€佷笌鏈€杩戞祦杞褰?/h1>
|
<h1>查看车票状态与最近流转记录</h1>
|
||||||
<p>鐢ㄤ簬鏌ョ湅鍗曞紶鐢靛瓙瀹㈢エ鐨勪箻杞︿俊鎭€佺姸鎬佷笌杩涘嚭绔欒褰曪紝渚夸簬鏃呭鍜屽伐浣滀汉鍛樺揩閫熺‘璁ょエ鎹姸鎬併€?/p>
|
<p>用于查看单张电子客票的乘车信息、状态与进出站记录,便于旅客和工作人员快速确认票据状态。</p>
|
||||||
</section>
|
</section>
|
||||||
<div v-if="loading" class="jr-panel-card">
|
<div v-if="loading" class="jr-panel-card">
|
||||||
<div class="jr-center-empty">
|
<div class="jr-center-empty">
|
||||||
<p>姝e湪璇诲彇绁ㄦ嵁鏁版嵁...</p>
|
<p>正在读取车票数据...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="!loading && hasTicket">
|
<template v-if="!loading && hasTicket">
|
||||||
@@ -92,8 +92,9 @@
|
|||||||
<article class="jr-board-card">
|
<article class="jr-board-card">
|
||||||
<div class="jr-panel-headline">
|
<div class="jr-panel-headline">
|
||||||
<h2 class="mono">{{ ticket.ticket_id }}</h2>
|
<h2 class="mono">{{ ticket.ticket_id }}</h2>
|
||||||
<span class="jr-status-pill" :class="statusInfo.class.replace('status-', 'jr-status-')">{{
|
<span class="jr-status-pill" :class="statusInfo.class.replace('status-', 'jr-status-')">
|
||||||
statusInfo.text }}</span>
|
{{ statusInfo.text }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-route-board">
|
<div class="jr-route-board">
|
||||||
<div class="jr-station-block">
|
<div class="jr-station-block">
|
||||||
@@ -114,46 +115,44 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="jr-meta-grid">
|
<div class="jr-meta-grid">
|
||||||
<div class="jr-meta-item">
|
<div class="jr-meta-item">
|
||||||
<span>杞﹀瀷</span>
|
<span>车型</span>
|
||||||
<strong>{{ formatTrainType(ticket.overview.train_type) }}</strong>
|
<strong>{{ formatTrainType(ticket.overview.train_type) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-meta-item">
|
<div class="jr-meta-item">
|
||||||
<span>绁ㄤ环</span>
|
<span>票价</span>
|
||||||
<strong>楼 {{ ticket.overview.amount || 0 }}</strong>
|
<strong>¥ {{ ticket.overview.amount || 0 }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-meta-item">
|
<div class="jr-meta-item">
|
||||||
<span>涔樻</span>
|
<span>乘次</span>
|
||||||
<strong>{{ (ticket.overview.trips_remaining == null ? 1 :
|
<strong>{{ (ticket.overview.trips_remaining == null ? 1 : ticket.overview.trips_remaining) }} / {{ (ticket.overview.trips_total == null ? 1 : ticket.overview.trips_total) }}</strong>
|
||||||
ticket.overview.trips_remaining) }} / {{ (ticket.overview.trips_total == null ? 1 :
|
|
||||||
ticket.overview.trips_total) }}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-meta-item">
|
<div class="jr-meta-item">
|
||||||
<span>鏇存柊鏃堕棿</span>
|
<span>更新时间</span>
|
||||||
<strong>{{ formatTime(ticket.overview.last_update_ts) }}</strong>
|
<strong>{{ formatTime(ticket.overview.last_update_ts) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<aside class="jr-board-card">
|
<aside class="jr-board-card">
|
||||||
<div class="jr-panel-headline">
|
<div class="jr-panel-headline">
|
||||||
<h3>娴佽浆璁板綍</h3>
|
<h3>流转记录</h3>
|
||||||
<span class="jr-panel-note">Recent Events</span>
|
<span class="jr-panel-note">Recent Events</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-history-list" v-if="ticket.events && ticket.events.length > 0">
|
<div class="jr-history-list" v-if="ticket.events && ticket.events.length > 0">
|
||||||
<div v-for="ev in ticket.events.slice().reverse().slice(0, 5)"
|
<div v-for="ev in ticket.events.slice().reverse().slice(0, 5)"
|
||||||
:key="ev.ts || ev.鏃堕棿鎴? class="jr-history-item">
|
:key="ev.ts || ev['时间戳'] || Math.random()"
|
||||||
|
class="jr-history-item">
|
||||||
<div class="jr-history-row">
|
<div class="jr-history-row">
|
||||||
<span class="jr-history-title">{{ formatEvent(ev) }}</span>
|
<span class="jr-history-title">{{ formatEvent(ev) }}</span>
|
||||||
<span class="jr-history-time">{{ formatTime(ev.鏃堕棿鎴?|| ev.ts) }}</span>
|
<span class="jr-history-time">{{ formatTime(ev['时间戳'] || ev.ts) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="jr-history-desc">
|
<div class="jr-history-desc">
|
||||||
<div>{{ formatEventLocation(ev) }}</div>
|
<div>{{ formatEventLocation(ev) }}</div>
|
||||||
<div v-if="formatEventMeta(ev)" style="margin-top:4px;">{{ formatEventMeta(ev) }}
|
<div v-if="formatEventMeta(ev)" style="margin-top:4px;">{{ formatEventMeta(ev) }}</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="jr-center-empty">
|
<div v-else class="jr-center-empty">
|
||||||
<p>鏆傛棤娴佽浆璁板綍銆?/p>
|
<p>暂无流转记录。</p>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
@@ -161,23 +160,23 @@
|
|||||||
|
|
||||||
<div v-if="!loading && !hasTicket" class="jr-panel-card">
|
<div v-if="!loading && !hasTicket" class="jr-panel-card">
|
||||||
<div class="jr-center-empty">
|
<div class="jr-center-empty">
|
||||||
<h2 style="margin:0 0 10px;">鏃犳晥杞︾エ</h2>
|
<h2 style="margin:0 0 10px;">无效车票</h2>
|
||||||
<p>鏈壘鍒拌杞︾エ鐨勮缁嗕俊鎭€?/p>
|
<p>未找到该车票的详细信息。</p>
|
||||||
<div class="jr-action-row">
|
<div class="jr-action-row">
|
||||||
<button @click="goHome" class="btn primary jr-search-button">杩斿洖鏌ヨ</button>
|
<button @click="goHome" class="btn primary jr-search-button">返回查询</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="site-footer jr-footer-space">
|
<footer class="site-footer jr-footer-space">
|
||||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">绮CP澶?025450093鍙?/a>
|
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">粤ICP备2025450093号</a>
|
||||||
<span class="version">v1.0.12</span>
|
<span class="version">v1.0.12</span>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="/ai-assistant.js?v=6"></script>
|
<script src="/ai-assistant.js?v=6"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -218,60 +217,74 @@
|
|||||||
const statusInfo = computed(() => {
|
const statusInfo = computed(() => {
|
||||||
if (!hasTicket.value) return {};
|
if (!hasTicket.value) return {};
|
||||||
let raw = '';
|
let raw = '';
|
||||||
if (ticket.value && ticket.value.overview) {
|
if (ticket.value && ticket.value.overview && ticket.value.overview.status != null) {
|
||||||
if (ticket.value.overview.status != null) raw = ticket.value.overview.status;
|
raw = ticket.value.overview.status;
|
||||||
}
|
}
|
||||||
if (!raw && ticket.value) {
|
if (!raw && ticket.value && ticket.value.status != null) {
|
||||||
if (ticket.value.status != null) raw = ticket.value.status;
|
raw = ticket.value.status;
|
||||||
}
|
}
|
||||||
const status = String(raw).toLowerCase();
|
const status = String(raw).toLowerCase();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
status === '鏈夋晥' ||
|
status === '有效' ||
|
||||||
status === 'valid' ||
|
status === 'valid' ||
|
||||||
status === 'unused' ||
|
status === 'unused' ||
|
||||||
status === 'active' ||
|
status === 'active' ||
|
||||||
status.includes('鏈夋晥') ||
|
status.includes('有效') ||
|
||||||
status.includes('鏈娇鐢?) ||
|
status.includes('未使用') ||
|
||||||
status.includes('unused')
|
status.includes('unused')
|
||||||
) {
|
) {
|
||||||
return { text: '鏈夋晥', class: 'status-valid' };
|
return { text: '有效', class: 'status-valid' };
|
||||||
}
|
}
|
||||||
if (status === '宸蹭娇鐢? || status === 'used' || status.includes('宸蹭娇鐢?) || status.includes('used')) {
|
if (
|
||||||
return { text: '宸蹭娇鐢?, class: 'status-used' };
|
status === '已使用' ||
|
||||||
|
status === 'used' ||
|
||||||
|
status.includes('已使用') ||
|
||||||
|
status.includes('used')
|
||||||
|
) {
|
||||||
|
return { text: '已使用', class: 'status-used' };
|
||||||
}
|
}
|
||||||
return { text: '澶辨晥', class: 'status-expired' };
|
return { text: '失效', class: 'status-expired' };
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatTime = (timestamp) => {
|
const formatTime = (timestamp) => {
|
||||||
if (!timestamp) return '---';
|
if (!timestamp) return '---';
|
||||||
let ts = Number(timestamp);
|
let ts = Number(timestamp);
|
||||||
if (!Number.isFinite(ts)) return String(timestamp);
|
if (!Number.isFinite(ts)) return String(timestamp);
|
||||||
if (ts > 0 && ts < 1000000000000) ts = ts * 1000;
|
if (ts > 0 && ts < 1000000000000) ts *= 1000;
|
||||||
const date = new Date(ts);
|
const date = new Date(ts);
|
||||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit'});
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatEvent = (event) => {
|
const formatEvent = (event) => {
|
||||||
const type = String(event.type || event.绫诲瀷 || '').toLowerCase();
|
const type = String(event.type || event['类型'] || '').toLowerCase();
|
||||||
const action = String(event.action || event.鍔ㄤ綔 || '').toLowerCase();
|
const action = String(event.action || event['动作'] || '').toLowerCase();
|
||||||
|
|
||||||
if (type === '鐘舵€? || type === 'status') {
|
if (type === '状态' || type === 'status') {
|
||||||
const actionMap = { 'entry': '杩涚珯鎴愬姛', 'exit': '鍑虹珯鎴愬姛' };
|
const actionMap = { entry: '进站成功', exit: '出站成功' };
|
||||||
return actionMap[action] || '鐘舵€佸彉鏇?;
|
return actionMap[action] || '状态变更';
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeMap = { 'sale': '鍞エ鎴愬姛', '鍞エ': '鍞エ鎴愬姛', 'entry': '杩涚珯鎴愬姛', 'exit': '鍑虹珯鎴愬姛' };
|
const typeMap = {
|
||||||
return typeMap[type] || event.type || event.绫诲瀷 || '鐘舵€佸彉鏇?;
|
sale: '售票成功',
|
||||||
|
售票: '售票成功',
|
||||||
|
entry: '进站成功',
|
||||||
|
exit: '出站成功'
|
||||||
|
};
|
||||||
|
return typeMap[type] || event.type || event['类型'] || '状态变更';
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatEventLocation = (event) => {
|
const formatEventLocation = (event) => {
|
||||||
const type = String(event.type || event.绫诲瀷 || '').toLowerCase();
|
const type = String(event.type || event['类型'] || '').toLowerCase();
|
||||||
const stationName = event.station_name || event.鍞エ绔?|| event.鍙戠敓绔?|| '';
|
const stationName = event.station_name || event['售票站'] || event['发生站'] || '';
|
||||||
const stationCode = event.station_code || event.绔欑偣缂栧彿 || '';
|
const stationCode = event.station_code || event['站点编号'] || '';
|
||||||
|
|
||||||
if (type === 'sale' || type === '鍞エ') {
|
if (type === 'sale' || type === '售票') {
|
||||||
return stationName || '绾夸笂鍞エ';
|
return stationName || '线上售票';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stationName && !stationCode) return '---';
|
if (!stationName && !stationCode) return '---';
|
||||||
@@ -279,25 +292,25 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatEventMeta = (event) => {
|
const formatEventMeta = (event) => {
|
||||||
const type = String(event.type || event.绫诲瀷 || '').toLowerCase();
|
const type = String(event.type || event['类型'] || '').toLowerCase();
|
||||||
if (type === 'sale' || type === '鍞エ') {
|
if (type === 'sale' || type === '售票') {
|
||||||
const amount = event.amount ?? event.鍞エ棰?
|
const amount = event.amount ?? event['售票额'];
|
||||||
if (amount != null && amount !== '') return `绁ㄤ环锛毬?${amount}`;
|
if (amount != null && amount !== '') return `票价:¥ ${amount}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stationEn = event.station_en || event.绔欑偣鑻辨枃 || '';
|
const stationEn = event.station_en || event['站点英文'] || '';
|
||||||
const deviceId = event.device_id || event.璁惧缂栧彿 || '';
|
const deviceId = event.device_id || event['设备编号'] || '';
|
||||||
if (stationEn && deviceId) return `${stationEn} (${deviceId})`;
|
if (stationEn && deviceId) return `${stationEn} (${deviceId})`;
|
||||||
if (deviceId) return `璁惧锛?{deviceId}`;
|
if (deviceId) return `设备:${deviceId}`;
|
||||||
return stationEn;
|
return stationEn;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTrainType = (type) => {
|
const formatTrainType = (type) => {
|
||||||
if (!type) return '鏅€?;
|
if (!type) return '普通';
|
||||||
const t = type.toLowerCase();
|
const t = String(type).toLowerCase();
|
||||||
if (t === 'local') return '鏅€?;
|
if (t === 'local') return '普通';
|
||||||
if (t === 'ltd.exp' || t === 'express' || t === 'exp' || t === 'express_train') return '鐗规€?;
|
if (t === 'ltd.exp' || t === 'express' || t === 'exp' || t === 'express_train') return '特急';
|
||||||
if (t.includes('鐗规€?)) return '鐗规€?;
|
if (t.includes('特急')) return '特急';
|
||||||
return String(type);
|
return String(type);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -311,22 +324,22 @@
|
|||||||
ticket.value = null;
|
ticket.value = null;
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const id = (data && (data.ticket_id || data.杞︾エ缂栧彿 || data.id)) || ticketid;
|
const id = (data && (data.ticket_id || data['车票编号'] || data.id)) || ticketid;
|
||||||
let overview = null;
|
let overview = null;
|
||||||
if (data) {
|
if (data) {
|
||||||
if (data.overview != null) overview = data.overview;
|
if (data.overview != null) overview = data.overview;
|
||||||
else if (data.姒傝 != null) overview = data.姒傝;
|
else if (data['概览'] != null) overview = data['概览'];
|
||||||
else if (data.summary != null) overview = data.summary;
|
else if (data.summary != null) overview = data.summary;
|
||||||
}
|
}
|
||||||
let events = [];
|
let events = [];
|
||||||
if (data) {
|
if (data) {
|
||||||
if (Array.isArray(data.events)) events = data.events;
|
if (Array.isArray(data.events)) events = data.events;
|
||||||
else if (data.浜嬩欢 != null) events = data.浜嬩欢;
|
else if (data['事件'] != null) events = data['事件'];
|
||||||
}
|
}
|
||||||
if (id && overview != null) {
|
if (id && overview != null) {
|
||||||
const out = {};
|
const out = {};
|
||||||
if (data && typeof data === 'object') {
|
if (data && typeof data === 'object') {
|
||||||
for (const k in data) out[k] = data[k];
|
for (const key in data) out[key] = data[key];
|
||||||
}
|
}
|
||||||
out.ticket_id = id;
|
out.ticket_id = id;
|
||||||
out.overview = overview;
|
out.overview = overview;
|
||||||
@@ -337,7 +350,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('鑾峰彇杞︾エ鏁版嵁澶辫触:', e);
|
console.error('获取车票数据失败:', e);
|
||||||
ticket.value = null;
|
ticket.value = null;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -357,15 +370,20 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading, ticket, hasTicket, statusInfo,
|
loading,
|
||||||
formatTime, formatEvent, formatEventLocation, formatEventMeta, formatTrainType, goHome
|
ticket,
|
||||||
|
hasTicket,
|
||||||
|
statusInfo,
|
||||||
|
formatTime,
|
||||||
|
formatEvent,
|
||||||
|
formatEventLocation,
|
||||||
|
formatEventMeta,
|
||||||
|
formatTrainType,
|
||||||
|
goHome
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}).mount('#app');
|
}).mount('#app');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>线上预定</title>
|
<title>线上预定</title>
|
||||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/style.css?v=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="public-search jr-order-page">
|
<body class="public-search jr-order-page">
|
||||||
@@ -232,8 +232,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/ticket-order.js?v=20"></script>
|
<script src="/ticket-order.js?v=21"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="/ai-assistant.js?v=6"></script>
|
<script src="/ai-assistant.js?v=6"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -261,3 +261,4 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -408,6 +408,58 @@
|
|||||||
return merged;
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = points.map((pt, idx) => `${idx === 0 ? 'M' : 'L'} ${pt.x} ${pt.y}`).join(' ');
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const firstStation = svgEl.querySelector('.map-station');
|
||||||
|
if (firstStation) svgEl.insertBefore(group, firstStation);
|
||||||
|
else svgEl.appendChild(group);
|
||||||
|
}
|
||||||
|
|
||||||
function updateSelectionUI(skipPreview = false) {
|
function updateSelectionUI(skipPreview = false) {
|
||||||
if (!(selection[0] && selection[1])) {
|
if (!(selection[0] && selection[1])) {
|
||||||
currentRoute = [];
|
currentRoute = [];
|
||||||
@@ -447,6 +499,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderRouteOverlay();
|
||||||
|
|
||||||
// Auto preview if both selected
|
// Auto preview if both selected
|
||||||
if(!skipPreview && selection[0] && selection[1]) previewPrice();
|
if(!skipPreview && selection[0] && selection[1]) previewPrice();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
<title>FSE铁路票务系统 - 线路规划</title>
|
<title>FSE铁路票务系统 - 线路规划</title>
|
||||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/style.css?v=12">
|
<link rel="stylesheet" href="/style.css?v=13">
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
<script src="/socket.io/socket.io.js"></script>
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -607,9 +607,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="ticket-route.js?v=2"></script>
|
<script src="ticket-route.js?v=3"></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');
|
||||||
@@ -641,3 +641,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -24,7 +24,8 @@ createApp({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const connected = ref(false);
|
const connected = ref(false);
|
||||||
const socket = io({ transports: ['websocket'], upgrade: false, timeout: 20000 });
|
// Keep the legacy route console usable behind proxies that only allow polling.
|
||||||
|
const socket = io({ transports: ['polling', 'websocket'], timeout: 20000 });
|
||||||
|
|
||||||
|
|
||||||
const stations = ref([]);
|
const stations = ref([]);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>票务查询</title>
|
<title>票务查询</title>
|
||||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/style.css?v=12" />
|
<link rel="stylesheet" href="/style.css?v=13" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="public-search jr-public-page">
|
<body class="public-search jr-public-page">
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/ticket-search.js?v=11"></script>
|
<script src="/ticket-search.js?v=11"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="/ai-assistant.js?v=6"></script>
|
<script src="/ai-assistant.js?v=6"></script>
|
||||||
@@ -146,3 +146,4 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -7,7 +7,7 @@
|
|||||||
<title>凭证详情</title>
|
<title>凭证详情</title>
|
||||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/style.css?v=12" />
|
<link rel="stylesheet" href="/style.css?v=13" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="public-search jr-public-page">
|
<body class="public-search jr-public-page">
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=12"></script>
|
||||||
<script src="/token.js?v=2"></script>
|
<script src="/token.js?v=2"></script>
|
||||||
<script src="/public-status.js?v=13"></script>
|
<script src="/public-status.js?v=13"></script>
|
||||||
<script src="/ai-assistant.js?v=6"></script>
|
<script src="/ai-assistant.js?v=6"></script>
|
||||||
@@ -137,3 +137,4 @@
|
|||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user