Files
FSE-Ticket.sys/web/index.html
T
2026-06-21 10:00:13 +08:00

876 lines
56 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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="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">
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="/socket.io/socket.io.js"></script>
</head>
<!--侧边栏-->
<body class="jr-admin-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="adminTopLink">
<i class="fas fa-train"></i>
<span>FSE 铁路运输后台系统</span>
</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>
</header>
<div class="jr-brandbar">
<div class="jr-brandbar-inner">
<a href="https://ticket.fse-media.group" class="jr-brand" id="adminBrandLink">
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
<div class="jr-brand-copy">
<strong>FSE 铁路运输</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">
<div class="sidebar" :class="{ open: sidebarOpen }">
<div class="jr-admin-sidebar-head">
<span class="jr-kicker">OPERATIONS CONSOLE</span>
<div class="brand">FSE铁路票务系统控制台</div>
<p class="jr-admin-sidebar-copy">后台管理页统一使用 JR 风格的门户视觉,集中处理票务、线路、资源和运营日志。</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 === 'dashboard'}" @click="currentView = 'dashboard'">
<span class="nav-icon"><i class="fas fa-chart-pie"></i></span> 仪表盘 </div>
<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 === 'faremap'}" @click="currentView = 'faremap'">
<span class="nav-icon"><i class="fas fa-map"></i></span> 票价地图
</div>
<div class="nav-item" :class="{active: currentView === 'tickets'}" @click="currentView = 'tickets'">
<span class="nav-icon"><i class="fas fa-ticket-alt"></i></span> 车票记录
</div>
<div class="nav-item" :class="{active: currentView === 'vouchers'}" @click="currentView = 'vouchers'">
<span class="nav-icon"><i class="fas fa-receipt"></i></span> 凭证管理
</div>
<div class="nav-item" :class="{active: currentView === 'iccards'}" @click="currentView = 'iccards'">
<span class="nav-icon"><i class="fas fa-credit-card"></i></span> IC 卡管理
</div>
<div class="nav-item" :class="{active: currentView === 'assets'}" @click="currentView = 'assets'">
<span class="nav-icon"><i class="fas fa-route"></i></span> 线路图
</div>
<div class="nav-item" :class="{active: currentView === 'settings'}" @click="currentView = 'settings'">
<span class="nav-icon"><i class="fas fa-cog"></i></span> 优惠设置
</div>
<div class="nav-item" :class="{active: currentView === 'logs'}" @click="currentView = 'logs'">
<span class="nav-icon"><i class="fas fa-list"></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; 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 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>
<span class="jr-kicker">JR STYLE ADMIN</span>
<h3 style="margin: 0;">{{ viewTitle }}</h3>
</div>
</div>
</div>
<div class="jr-admin-header-side">
<span class="jr-admin-header-pill" :class="connected ? 'is-online' : 'is-offline'">
<i class="fas fa-circle"></i>
{{ connected ? '服务器在线' : '服务器离线' }}
</span>
</div>
</div>
<div class="content">
<section class="jr-page-intro jr-admin-intro">
<span class="jr-kicker">CENTRAL MANAGEMENT</span>
<h1>铁路票务后台控制台</h1>
<p>参照公开页的信息层级组织后台入口,把线路、售票、资源和日志管理统一放入同一套铁路门户式界面。</p>
</section>
<section class="jr-home-alert jr-admin-alert">
<div class="jr-alert-title">
<i class="fas fa-circle-info"></i>
<span>控制台概览</span>
</div>
<p>当前模块:{{ viewTitle }},已加载 {{ lines.length }} 条线路、{{ stations.length }} 个站点。需要做批量调整时,可先在左侧切换模块再进入对应卡片操作区。</p>
</section>
<!-- 仪表盘-->
<div v-if="currentView === 'dashboard'">
<div class="grid">
<div class="card">
<div class="stat-label">今日售票额</div>
<div class="stat-value">{{ stats.sold_tickets || 0 }}</div>
</div>
<div class="card">
<div class="stat-label">站点数</div>
<div class="stat-value">{{ stations.length }}</div>
</div>
<div class="card">
<div class="stat-label">运营线路</div>
<div class="stat-value">{{ lines.length }}</div>
</div>
</div>
</div>
<!-- 线路与票价管理 -->
<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">
<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>选择左侧线路进行管理</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">
<svg width="100%" 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)" @mouseup="onStationDrop"
@mousemove="onStationDragOver(index)" @click="handleStationClick(sCode)"
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 === 'faremap'">
<div 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>
</div>
<!-- 凭证管理 -->
<div v-if="currentView === 'vouchers'">
<div class="card">
<div class="flex between mb-4">
<h4>凭证列表</h4>
<div class="flex">
<button @click="fetchOrders"><i class="fas fa-sync-alt"></i></button>
</div>
</div>
<table class="ticket-table">
<thead>
<tr>
<th>凭证</th>
<th>线路</th>
<th>车型</th>
<th>票价</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="o in orderList" :key="o.code">
<td class="mono" style="font-weight:bold; font-size:1.1em;">{{ o.code }}</td>
<td>{{ o.start_name }} <i class="fas fa-arrow-right text-muted"></i> {{
o.terminal_name }}</td>
<td>{{ formatTrainType(o.train_type) }}</td>
<td>{{ o.price }}</td>
<td><span class="badge" :class="formatTicketStatus(o.status).class">{{ formatTicketStatus(o.status).text }}</span></td>
<td>{{ formatTime(o.created_ts) }}</td>
<td>
<div class="flex" style="gap:4px;">
<a :href="'token.html?code='+o.code" target="_blank" class="btn sm"
title="查看"
style="height:28px; width:28px; padding:0; display:flex; align-items:center; justify-content:center;"><i
class="fas fa-eye"></i></a>
<button class="danger sm" @click="deleteOrder(o.code)"
style="height:28px; width:28px; padding:0; display:flex; align-items:center; justify-content:center;"><i
class="fas fa-trash"></i></button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-if="currentView === 'iccards'">
<div class="grid">
<div class="card">
<div class="stat-label">IC 卡总数</div>
<div class="stat-value">{{ icCardStats.total }}</div>
</div>
<div class="card">
<div class="stat-label">待领卡</div>
<div class="stat-value">{{ icCardStats.pending }}</div>
</div>
<div class="card">
<div class="stat-label">正常启用</div>
<div class="stat-value">{{ icCardStats.active }}</div>
</div>
<div class="card">
<div class="stat-label">储值总额</div>
<div class="stat-value">{{ formatMoney(icCardStats.balance) }}</div>
</div>
</div>
<div class="management-container ic-admin-layout">
<div class="management-sidebar">
<div class="card" style="margin-bottom:0; display:flex; flex-direction:column; min-height: 520px;">
<div class="flex between mb-4">
<h4>卡片列表</h4>
<span class="badge">{{ icCards.length }}</span>
</div>
<div class="flex mb-4" style="flex-wrap:wrap;">
<input v-model="icCardSearch" placeholder="搜索卡号 / 订单号 / 凭证码 / 姓名" style="flex:1;">
</div>
<div class="list-lines jr-scroll-box" style="flex:1; min-height:320px;">
<div v-if="!icCards.length" class="empty-state" style="padding:24px 0;">
<p>暂无 IC 卡记录。</p>
</div>
<div
v-for="card in icCards"
:key="card.card_id"
class="line-item ic-card-item"
:class="{ active: icSelectedId === card.card_id }"
@click="loadIcCard(card.card_id)"
>
<div class="line-color-dot" :style="{ background: icStatusColor(card.status) }"></div>
<div class="line-info">
<div class="line-name">{{ displayIcCardId(card) }}</div>
<div class="line-meta">{{ card.holder_name || '未登记持卡人' }} · IC 储值卡</div>
<div class="line-meta">订单 {{ cardOrderCode(card) }} · 余额 {{ formatMoney(card.balance) }}</div>
</div>
<div class="line-actions" style="opacity:1;">
<span class="badge" :class="icStatusInfo(card.status).className">{{ icStatusInfo(card.status).text }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="management-main">
<div class="card">
<div class="flex between mb-4">
<h4>卡片详情</h4>
<div class="flex" style="gap:8px;">
<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>
</div>
</div>
<div v-if="!icSelectedCard" class="empty-state">
<i class="fas fa-credit-card" style="font-size:48px; opacity:0.3;"></i>
<p>从左侧选择一张 IC 卡以查看详情。</p>
</div>
<div v-else>
<div class="flex between mb-4" style="align-items:flex-start;">
<div>
<div class="mono" style="font-size:1.4rem; font-weight:700;">{{ displayIcCardId(icSelectedCard) }}</div>
<div class="text-muted" style="margin-top:6px;">订单号 {{ cardOrderCode(icSelectedCard) }} · 来源 {{ icSelectedCard.source || '---' }}</div>
</div>
<span class="badge" :class="icStatusInfo(icSelectedCard.status).className">{{ icStatusInfo(icSelectedCard.status).text }}</span>
</div>
<div class="ic-detail-grid">
<label class="ic-field">
<span>持卡人</span>
<input v-model="icDetailForm.holder_name">
</label>
<label class="ic-field">
<span>卡片类型</span>
<input value="IC 储值卡" disabled>
</label>
<label class="ic-field">
<span>状态</span>
<select v-model="icDetailForm.status">
<option value="pending_pickup">待领卡</option>
<option value="active">正常</option>
<option value="disabled">停用</option>
<option value="lost">挂失</option>
<option value="refunded">已退卡</option>
</select>
</label>
<label class="ic-field">
<span>余额</span>
<input :value="formatMoney(icSelectedCard?.balance || 0)" disabled>
</label>
</div>
<div class="ic-inline-meta">
<div class="list-item"><span class="k">创建时间</span><span class="v">{{ formatTime(icSelectedCard.created_ts) }}</span></div>
<div class="list-item"><span class="k">最后更新</span><span class="v">{{ formatTime(icSelectedCard.last_update_ts) }}</span></div>
<div class="list-item"><span class="k">首次充值</span><span class="v">{{ formatMoney(icSelectedCard.purchase_amount ?? icSelectedCard.balance) }}</span></div>
<div class="list-item"><span class="k">购卡金额</span><span class="v">{{ formatMoney(icSelectedCard.purchase_amount) }}</span></div>
</div>
</div>
</div>
<div class="card" style="margin-bottom:0;">
<div class="flex between mb-4">
<h4>操作记录</h4>
</div>
<div class="timeline">
<div v-if="!icSelectedCard" class="loading">选择卡片后显示事件流。</div>
<div v-else-if="!icSelectedEvents.length" class="loading">暂无事件记录。</div>
<div v-for="(event, idx) in icSelectedEvents" :key="`${event.ts || 0}-${event.type || 'event'}-${idx}`" class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<div class="flex between">
<span style="font-weight:600;">{{ icEventTitle(event) }}</span>
<span class="text-muted" style="font-size:0.8rem;">{{ formatTime(event.ts) }}</span>
</div>
<div class="log-detail" style="margin-top:8px;">{{ formatIcEventDetail(event) }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 车票记录 -->
<div v-if="currentView === 'tickets'">
<div class="card mb-4">
<div class="flex between mb-4">
<h4>车票记录</h4>
<div class="flex">
<input v-model="ticketSearch" placeholder="搜索 Ticket ID / 站点" style="width: 200px;">
<button @click="refreshData"><i class="fas fa-sync-alt"></i></button>
</div>
</div>
<table class="ticket-table">
<thead>
<tr>
<th>ID</th>
<th>起点</th>
<th>终点</th>
<th>类型</th>
<th>状态</th>
<th>时间</th>
</tr>
</thead>
<tbody>
<tr v-for="t in ticketList" :key="t.ticket_id" @click="viewTicketDetails(t)"
class="clickable-row">
<td><span class="mono">{{ t.ticket_id }}</span></td>
<td>
<div class="st-container">
<div class="st-main-row">
<span class="st-name">{{ getStationInfo(t.start).name }}</span>
<span class="st-code">{{ t.start }}</span>
</div>
<div class="st-en">{{ getStationInfo(t.start).en_name }}</div>
</div>
</td>
<td>
<div class="st-container">
<div class="st-main-row">
<span class="st-name">{{ getStationInfo(t.terminal).name }}</span>
<span class="st-code">{{ t.terminal }}</span>
</div>
<div class="st-en">{{ getStationInfo(t.terminal).en_name }}</div>
</div>
</td>
<td>{{ formatTrainType(t.type) }}</td>
<td><span class="badge" :class="formatTicketStatus(t.status).class">{{
formatTicketStatus(t.status).text }}</span></td>
<td>{{ formatTime(t.ts) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 车票详情弹窗 -->
<div v-if="showTicketModal" class="modal show" @click.self="closeTicketModal">
<div class="modal-card" style="width: 600px;">
<div class="flex between mb-4">
<h4 class="modal-title">车票详情</h4>
<button class="sm" @click="closeTicketModal" title="关闭"><i
class="fas fa-times"></i></button>
</div>
<div v-if="selectedTicket">
<div class="ticket-header mb-4"
style="background: rgba(255,255,255,0.05); padding: 16px; border-radius: 8px;">
<div class="flex between mb-2">
<span class="mono text-muted">{{ selectedTicket.ticket_id }}</span>
<span class="badge"
:class="formatTicketStatus(selectedTicket.index.status).class">
{{ formatTicketStatus(selectedTicket.index.status).text }}
</span>
</div>
<div class="flex between" style="font-size: 1.2rem; font-weight: bold;">
<div class="st-container">
<div class="st-main-row">
<span class="st-name">{{ selectedTicket.index.start_name ||
selectedTicket.index.start }}</span>
<span class="st-code">{{ selectedTicket.index.start }}</span>
</div>
<div class="st-en">{{ selectedTicket.index.start_en || '' }}</div>
</div>
<i class="fas fa-arrow-right text-muted"></i>
<div class="st-container" style="align-items: flex-end;">
<div class="st-main-row">
<span class="st-name">{{ selectedTicket.index.terminal_name ||
selectedTicket.index.terminal }}</span>
<span class="st-code">{{ selectedTicket.index.terminal }}</span>
</div>
<div class="st-en">{{ selectedTicket.index.terminal_en || '' }}</div>
</div>
</div>
<div class="text-muted mt-2" style="font-size: 0.9rem;">
类型: {{ formatTrainType(selectedTicket.index.type ||
selectedTicket.index.train_type) }} | 票价: {{ selectedTicket.index.price ||
selectedTicket.index.cost }}
</div>
</div>
<h5>行程记录</h5>
<div class="timeline">
<div v-for="ev in selectedTicket.events" :key="ev.ts || ev['时间戳']" class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<div class="flex between">
<span style="font-weight: 600;">{{ formatTicketEvent(ev) }}</span>
<span class="text-muted" style="font-size: 0.8rem;">{{ formatTime(ev['时间戳'] || ev.ts) }}</span>
</div>
<div class="text-muted" style="font-size: 0.9rem;">
<div>{{ formatTicketEventLocation(ev) }}</div>
<div v-if="formatTicketEventExtra(ev)" style="margin-top: 4px;">{{ formatTicketEventExtra(ev) }}</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="loading">加载中...</div>
</div>
</div>
</div>
<div v-if="currentView === 'assets'">
<div class="card mb-4">
<div class="flex between mb-4" style="flex-wrap: wrap; gap: 10px;">
<div class="flex" style="align-items: center; gap: 10px;">
<h4>线路图</h4>
<span class="badge" v-if="assetsManifest.routeMap" style="font-family: monospace;">{{ assetsManifest.routeMap }}</span>
</div>
<div class="flex" style="gap: 8px;">
<label class="btn" style="cursor: pointer;">
<i class="fas fa-upload"></i> 上传线路图
<input type="file" hidden accept=".png,.jpg,.jpeg,.webp,.svg" @change="uploadRouteMap">
</label>
<a v-if="assetsRouteMapUrl" :href="assetsRouteMapUrl" target="_blank" class="btn">
<i class="fas fa-external-link-alt"></i> 打开
</a>
<button v-if="assetsManifest.routeMap" class="danger" @click="deleteRouteMap">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</div>
<div v-if="assetsRouteMapUrl" style="border: 1px solid var(--border); border-radius: 10px; overflow: hidden; background: #0b0b0f;">
<img :src="assetsRouteMapUrl" alt="线路图" style="display:block; width: 100%; height: auto;">
</div>
<div v-else class="loading">未上传线路图</div>
</div>
</div>
<!-- 设置 -->
<div v-if="currentView === 'settings'">
<div class="card mb-4">
<div class="mb-4">
<label style="display:block; margin-bottom:8px; font-weight:600;">优惠活动</label>
<div class="flex">
<input v-model="config.promotion.name" placeholder="活动名称">
<input v-model.number="config.promotion.discount" type="number" step="0.1"
placeholder="折扣 (0.1-1.0)">
<button @click="saveConfig"><i class="fas fa-save"></i> 保存</button>
</div>
</div>
</div>
<div class="card">
<h4>数据管理</h4>
<div class="flex">
<button @click="exportData"><i class="fas fa-file-export"></i> 导出数据 JSON</button>
</div>
</div>
</div>
<div v-if="currentView === 'logs'">
<div class="card">
<div class="flex between mb-4" style="flex-wrap: wrap; gap: 10px;">
<h4>日志</h4>
<div class="flex" style="gap: 8px;">
<button @click="fetchLogs" title="刷新"><i class="fas fa-sync-alt"></i></button>
</div>
</div>
<div class="flex mb-4" style="flex-wrap: wrap; gap: 10px; align-items: center;">
<select v-model="logCategory" style="width: 150px;">
<option value="">全部来源</option>
<option value="admin">admin</option>
<option value="public">public</option>
<option value="device">device</option>
<option value="system">system</option>
</select>
<input v-model="logTypeFilter" placeholder="type (可逗号分隔)" style="width: 260px;">
<input v-model="logQuery" placeholder="关键字" style="width: 220px;">
<input v-model.number="logMax" type="number" min="10" max="5000" step="10" style="width: 120px;">
<button @click="fetchLogs" :disabled="logLoading">
<i class="fas" :class="logLoading ? 'fa-spinner fa-spin' : 'fa-filter'"></i> 筛选
</button>
</div>
<div v-if="logLoading" class="loading">加载中...</div>
<table v-else>
<thead>
<tr>
<th>时间</th>
<th>来源</th>
<th>类型</th>
<th>IP</th>
<th>详情</th>
</tr>
</thead>
<tbody>
<tr v-for="(l, idx) in logs" :key="idx">
<td style="white-space: nowrap;">{{ formatTime(l.ts) }}</td>
<td><span class="badge">{{ l.category || 'legacy' }}</span></td>
<td class="mono">{{ l.type || 'event' }}</td>
<td class="mono">{{ l.ip || '' }}</td>
<td>
<details>
<summary class="text-muted" style="cursor: pointer;">查看</summary>
<pre style="white-space: pre-wrap; margin: 8px 0 0; font-size: 0.85rem; line-height: 1.35;">{{ formatLogDetail(l) }}</pre>
</details>
</td>
</tr>
</tbody>
</table>
</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=13"></script>
<script src="index.js?v=2"></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('adminTopLink').href = links.home;
document.getElementById('adminBrandLink').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>