Files

391 lines
19 KiB
HTML
Raw Permalink Normal View History

2026-06-21 10:00:13 +08:00
<!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>
2026-06-21 10:00:13 +08:00
<link rel="icon" type="image/png" href="/FSE-ticket.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="/style.css?v=13">
2026-06-21 10:00:13 +08:00
<style>
[v-cloak] {
display: none;
}
body.jr-ticket-board-page {
display: block !important;
overflow-y: auto !important;
min-height: 100vh !important;
width: 100% !important;
background: linear-gradient(180deg, #eef4ee 0, #eef4ee 146px, #f7f8f4 146px, #f7f8f4 100%) !important;
}
body.jr-ticket-board-page #app,
body.jr-ticket-board-page .jr-public-shell,
body.jr-ticket-board-page .jr-topbar,
body.jr-ticket-board-page .jr-brandbar,
body.jr-ticket-board-page .jr-public-main {
display: block !important;
width: 100% !important;
min-width: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
float: none !important;
}
body.jr-ticket-board-page .jr-topbar-inner,
body.jr-ticket-board-page .jr-brandbar-inner,
body.jr-ticket-board-page .jr-public-main {
width: min(1280px, calc(100% - 40px)) !important;
margin: 0 auto !important;
}
</style>
</head>
<body class="public-search jr-public-page jr-ticket-board-page">
<div id="app" v-cloak class="jr-public-shell">
<header class="jr-topbar">
<div class="jr-topbar-inner">
<a href="javascript:void(0)" @click="goHome" class="jr-top-link">
<i class="fas fa-arrow-left"></i>
<span>返回查询</span>
2026-06-21 10:00:13 +08:00
</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>
2026-06-21 10:00:13 +08:00
</div>
</div>
</header>
<div class="jr-brandbar">
<div class="jr-brandbar-inner">
<a href="javascript:void(0)" @click="goHome" class="jr-brand">
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
<div class="jr-brand-copy">
<strong>FarSight-T.N.E铁路运输</strong>
<span>电子客票信息</span>
2026-06-21 10:00:13 +08:00
</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" 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/order" data-link="card-order">线上购卡</a>
2026-06-21 10:00:13 +08:00
</nav>
</div>
</div>
<main class="jr-public-main">
<section class="jr-page-intro">
<span class="jr-kicker">ELECTRONIC TICKET</span>
<h1>查看车票状态与最近流转记录</h1>
<p>用于查看单张电子客票的乘车信息、状态与进出站记录</p>
2026-06-21 10:00:13 +08:00
</section>
<div v-if="loading" class="jr-panel-card">
<div class="jr-center-empty">
<p>正在读取车票数据...</p>
2026-06-21 10:00:13 +08:00
</div>
</div>
<template v-if="!loading && hasTicket">
<section class="jr-board-layout">
<article class="jr-board-card">
<div class="jr-panel-headline">
<h2 class="mono">{{ ticket.ticket_id }}</h2>
<span class="jr-status-pill" :class="statusInfo.class.replace('status-', 'jr-status-')">
{{ statusInfo.text }}
</span>
2026-06-21 10:00:13 +08:00
</div>
<div class="jr-route-board">
<div class="jr-station-block">
<div class="jr-station-line">
<span class="jr-station-title">{{ ticket.overview.start_name }}</span>
<span class="jr-station-code">{{ ticket.overview.start_code }}</span>
</div>
<div class="jr-station-en">{{ ticket.overview.start_en }}</div>
</div>
<div class="jr-route-track"><i class="fas fa-train"></i></div>
<div class="jr-station-block is-end">
<div class="jr-station-line">
<span class="jr-station-title">{{ ticket.overview.terminal_name }}</span>
<span class="jr-station-code">{{ ticket.overview.terminal_code }}</span>
</div>
<div class="jr-station-en">{{ ticket.overview.terminal_en }}</div>
</div>
</div>
<div class="jr-meta-grid">
<div class="jr-meta-item">
<span>车型</span>
2026-06-21 10:00:13 +08:00
<strong>{{ formatTrainType(ticket.overview.train_type) }}</strong>
</div>
<div class="jr-meta-item">
<span>票价</span>
<strong>¥ {{ ticket.overview.amount || 0 }}</strong>
2026-06-21 10:00:13 +08:00
</div>
<div class="jr-meta-item">
<span>乘次</span>
<strong>{{ (ticket.overview.trips_remaining == null ? 1 : ticket.overview.trips_remaining) }} / {{ (ticket.overview.trips_total == null ? 1 : ticket.overview.trips_total) }}</strong>
2026-06-21 10:00:13 +08:00
</div>
<div class="jr-meta-item">
<span>更新时间</span>
2026-06-21 10:00:13 +08:00
<strong>{{ formatTime(ticket.overview.last_update_ts) }}</strong>
</div>
</div>
</article>
<aside class="jr-board-card">
<div class="jr-panel-headline">
<h3>流转记录</h3>
2026-06-21 10:00:13 +08:00
<span class="jr-panel-note">Recent Events</span>
</div>
<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)"
:key="ev.ts || ev['时间戳'] || Math.random()"
class="jr-history-item">
2026-06-21 10:00:13 +08:00
<div class="jr-history-row">
<span class="jr-history-title">{{ formatEvent(ev) }}</span>
<span class="jr-history-time">{{ formatTime(ev['时间戳'] || ev.ts) }}</span>
2026-06-21 10:00:13 +08:00
</div>
<div class="jr-history-desc">
<div>{{ formatEventLocation(ev) }}</div>
<div v-if="formatEventMeta(ev)" style="margin-top:4px;">{{ formatEventMeta(ev) }}</div>
2026-06-21 10:00:13 +08:00
</div>
</div>
</div>
<div v-else class="jr-center-empty">
<p>暂无流转记录。</p>
2026-06-21 10:00:13 +08:00
</div>
</aside>
</section>
</template>
<div v-if="!loading && !hasTicket" class="jr-panel-card">
<div class="jr-center-empty">
<h2 style="margin:0 0 10px;">无效车票</h2>
<p>未找到该车票的详细信息。</p>
2026-06-21 10:00:13 +08:00
<div class="jr-action-row">
<button @click="goHome" class="btn primary jr-search-button">返回查询</button>
2026-06-21 10:00:13 +08:00
</div>
</div>
</div>
<footer class="site-footer jr-footer-space">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">粤ICP备2025450093号</a>
<span class="version">v1.0.12</span>
2026-06-21 10:00:13 +08:00
</footer>
</main>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="/custom-dialog.js?v=12"></script>
2026-06-21 10:00:13 +08:00
<script src="/public-status.js?v=13"></script>
<script src="/ai-assistant.js?v=6"></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.querySelectorAll('[data-link]').forEach((el) => {
const key = el.getAttribute('data-link');
if (links[key]) el.href = links[key];
});
});
</script>
<script>
const { createApp, ref, onMounted, computed } = Vue;
createApp({
setup() {
const loading = ref(true);
const ticket = ref(null);
const sp = new URLSearchParams(window.location.search);
const idFromQuery = sp.get('id') || '';
const pathParts = window.location.pathname.split('/').filter(Boolean);
const idFromPath = pathParts.length ? pathParts[pathParts.length - 1] : '';
const ticketid = decodeURIComponent(idFromPath || idFromQuery || '');
const hasTicket = computed(() => {
return ticket.value && (ticket.value.ticket_id || ticket.value.id) && ticket.value.overview != null;
});
const statusInfo = computed(() => {
if (!hasTicket.value) return {};
let raw = '';
if (ticket.value && ticket.value.overview && ticket.value.overview.status != null) {
raw = ticket.value.overview.status;
2026-06-21 10:00:13 +08:00
}
if (!raw && ticket.value && ticket.value.status != null) {
raw = ticket.value.status;
2026-06-21 10:00:13 +08:00
}
const status = String(raw).toLowerCase();
if (
status === '有效' ||
2026-06-21 10:00:13 +08:00
status === 'valid' ||
status === 'unused' ||
status === 'active' ||
status.includes('有效') ||
status.includes('未使用') ||
2026-06-21 10:00:13 +08:00
status.includes('unused')
) {
return { text: '有效', class: 'status-valid' };
2026-06-21 10:00:13 +08:00
}
if (
status === '已使用' ||
status === 'used' ||
status.includes('已使用') ||
status.includes('used')
) {
return { text: '已使用', class: 'status-used' };
2026-06-21 10:00:13 +08:00
}
return { text: '失效', class: 'status-expired' };
2026-06-21 10:00:13 +08:00
});
const formatTime = (timestamp) => {
if (!timestamp) return '---';
let ts = Number(timestamp);
if (!Number.isFinite(ts)) return String(timestamp);
if (ts > 0 && ts < 1000000000000) ts *= 1000;
2026-06-21 10:00:13 +08:00
const date = new Date(ts);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
2026-06-21 10:00:13 +08:00
};
const formatEvent = (event) => {
const type = String(event.type || event['类型'] || '').toLowerCase();
const action = String(event.action || event['动作'] || '').toLowerCase();
2026-06-21 10:00:13 +08:00
if (type === '状态' || type === 'status') {
const actionMap = { entry: '进站成功', exit: '出站成功' };
return actionMap[action] || '状态变更';
2026-06-21 10:00:13 +08:00
}
const typeMap = {
sale: '售票成功',
售票: '售票成功',
entry: '进站成功',
exit: '出站成功'
};
return typeMap[type] || event.type || event['类型'] || '状态变更';
2026-06-21 10:00:13 +08:00
};
const formatEventLocation = (event) => {
const type = String(event.type || event['类型'] || '').toLowerCase();
const stationName = event.station_name || event['售票站'] || event['发生站'] || '';
const stationCode = event.station_code || event['站点编号'] || '';
2026-06-21 10:00:13 +08:00
if (type === 'sale' || type === '售票') {
return stationName || '线上售票';
2026-06-21 10:00:13 +08:00
}
if (!stationName && !stationCode) return '---';
return [stationName, stationName && stationCode ? stationCode : ''].filter(Boolean).join(' ');
};
const formatEventMeta = (event) => {
const type = String(event.type || event['类型'] || '').toLowerCase();
if (type === 'sale' || type === '售票') {
const amount = event.amount ?? event['售票额'];
if (amount != null && amount !== '') return `票价:¥ ${amount}`;
2026-06-21 10:00:13 +08:00
}
const stationEn = event.station_en || event['站点英文'] || '';
const deviceId = event.device_id || event['设备编号'] || '';
2026-06-21 10:00:13 +08:00
if (stationEn && deviceId) return `${stationEn} (${deviceId})`;
if (deviceId) return `设备:${deviceId}`;
2026-06-21 10:00:13 +08:00
return stationEn;
};
const formatTrainType = (type) => {
if (!type) return '普通';
const t = String(type).toLowerCase();
if (t === 'local') return '普通';
if (t === 'ltd.exp' || t === 'express' || t === 'exp' || t === 'express_train') return '特急';
if (t.includes('特急')) return '特急';
2026-06-21 10:00:13 +08:00
return String(type);
};
const fetchData = async () => {
loading.value = true;
ticket.value = null;
2026-06-21 10:00:13 +08:00
try {
const response = await fetch(`/api/public/tickets/${ticketid}`);
if (response.status === 404) {
ticket.value = null;
} else {
const data = await response.json();
const id = (data && (data.ticket_id || data['车票编号'] || data.id)) || ticketid;
2026-06-21 10:00:13 +08:00
let overview = null;
if (data) {
if (data.overview != null) overview = data.overview;
else if (data['概览'] != null) overview = data['概览'];
2026-06-21 10:00:13 +08:00
else if (data.summary != null) overview = data.summary;
}
let events = [];
if (data) {
if (Array.isArray(data.events)) events = data.events;
else if (data['事件'] != null) events = data['事件'];
2026-06-21 10:00:13 +08:00
}
if (id && overview != null) {
const out = {};
if (data && typeof data === 'object') {
for (const key in data) out[key] = data[key];
2026-06-21 10:00:13 +08:00
}
out.ticket_id = id;
out.overview = overview;
out.events = events;
ticket.value = out;
} else {
ticket.value = null;
}
}
} catch (e) {
console.error('获取车票数据失败:', e);
2026-06-21 10:00:13 +08:00
ticket.value = null;
} finally {
loading.value = false;
}
};
const goHome = () => {
if (window.location.hostname.includes('fse-media.group')) {
window.location.href = 'https://ticket.fse-media.group/search';
} else {
window.location.href = '/ticket-search.html';
}
};
onMounted(() => {
fetchData();
});
return {
loading,
ticket,
hasTicket,
statusInfo,
formatTime,
formatEvent,
formatEventLocation,
formatEventMeta,
formatTrainType,
goHome
2026-06-21 10:00:13 +08:00
};
}
}).mount('#app');
</script>
</body>
</html>