feat: 增加机器位置手动编辑功能及公开查看界面
This commit is contained in:
@@ -39,6 +39,7 @@
|
||||
<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>
|
||||
<a href="https://ticket.fse-media.group/machine-map.html" data-link="machine-map">机器位置</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<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" class="is-active">线上购卡</a>
|
||||
<a href="https://ticket.fse-media.group/machine-map.html" data-link="machine-map">机器位置</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<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" class="is-active">IC 卡查询</a>
|
||||
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
||||
<a href="https://ticket.fse-media.group/machine-map.html" data-link="machine-map">机器位置</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -686,6 +686,9 @@
|
||||
</div>
|
||||
<div style="font-size: 0.85em; color: #aaa;">站点: {{ loc.station || loc.station_code || '未知' }}</div>
|
||||
<div style="font-size: 0.8em; color: #888; margin-top: 4px;">坐标: {{ loc.x }}, {{ loc.y }}, {{ loc.z }}</div>
|
||||
<div style="margin-top: 8px;">
|
||||
<button class="btn sm primary" @click.stop="openEditMachineModal(id)">编辑</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -693,6 +696,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑机器位置 Modal -->
|
||||
<div v-if="showEditMachineModal" class="jr-admin-modal-overlay" @click.self="showEditMachineModal = false">
|
||||
<div class="jr-admin-modal">
|
||||
<div class="jr-admin-modal-header">
|
||||
<h3>编辑设备位置 - {{ editMachineForm.device_id }}</h3>
|
||||
<button class="jr-admin-modal-close" @click="showEditMachineModal = false"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="jr-admin-modal-body">
|
||||
<div class="jr-admin-form-group">
|
||||
<label>站点代码 / 名称</label>
|
||||
<input type="text" v-model="editMachineForm.station_code" class="jr-admin-input">
|
||||
</div>
|
||||
<div class="jr-admin-form-group" style="display: flex; gap: 10px;">
|
||||
<div style="flex: 1;">
|
||||
<label>X 坐标</label>
|
||||
<input type="number" v-model="editMachineForm.x" class="jr-admin-input">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label>Y 坐标</label>
|
||||
<input type="number" v-model="editMachineForm.y" class="jr-admin-input">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label>Z 坐标</label>
|
||||
<input type="number" v-model="editMachineForm.z" class="jr-admin-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="jr-admin-form-group">
|
||||
<label>类型</label>
|
||||
<select v-model="editMachineForm.type" class="jr-admin-input">
|
||||
<option value="TicketMachine">售票机</option>
|
||||
<option value="Gate">检票机</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="jr-admin-modal-footer">
|
||||
<button class="btn" @click="showEditMachineModal = false">取消</button>
|
||||
<button class="btn primary" @click="saveMachineLocation">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 车票记录 -->
|
||||
<div v-if="currentView === 'tickets'">
|
||||
<div class="grid">
|
||||
|
||||
+31
-1
@@ -1165,6 +1165,36 @@ createApp({
|
||||
}
|
||||
};
|
||||
|
||||
const showEditMachineModal = ref(false);
|
||||
const editMachineForm = reactive({ device_id: '', station_code: '', x: 0, y: 0, z: 0, type: 'TicketMachine' });
|
||||
|
||||
const openEditMachineModal = (id) => {
|
||||
const loc = machineLocations.value[id];
|
||||
if (!loc) return;
|
||||
editMachineForm.device_id = id;
|
||||
editMachineForm.station_code = loc.station_code || loc.station || '';
|
||||
editMachineForm.x = loc.x || 0;
|
||||
editMachineForm.y = loc.y || 0;
|
||||
editMachineForm.z = loc.z || 0;
|
||||
editMachineForm.type = loc.type || 'TicketMachine';
|
||||
showEditMachineModal.value = true;
|
||||
};
|
||||
|
||||
const saveMachineLocation = async () => {
|
||||
try {
|
||||
const payload = { ...editMachineForm };
|
||||
await requestJson(`/api/machine-location/${encodeURIComponent(payload.device_id)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
}, { expectOk: true });
|
||||
showEditMachineModal.value = false;
|
||||
await fetchMachineLocations();
|
||||
} catch (e) {
|
||||
alert('保存失败: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const ensureViewData = async (view = currentView.value, { force = false } = {}) => {
|
||||
await fetchCoreData({ force });
|
||||
if (view === 'tickets' && (force || !ticketDataLoaded)) await fetchTicketData();
|
||||
@@ -1780,7 +1810,7 @@ createApp({
|
||||
};
|
||||
|
||||
return {
|
||||
machineLocations, selectedMachineId, getMapX, getMapY,
|
||||
machineLocations, selectedMachineId, getMapX, getMapY, showEditMachineModal, editMachineForm, openEditMachineModal, saveMachineLocation,
|
||||
currentView, viewTitle, sidebarOpen,
|
||||
loadingState, isViewBusy, lastSyncText, currentViewSummary,
|
||||
stations, lines, fares, stats, config, recentLogs, ticketList, ticketStats, orderStats,
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>机器位置|FSE铁路运输票务系统</title>
|
||||
<link rel="icon" type="image/png" href="/FSE-ticket.png">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="/style.css?v=14" />
|
||||
</head>
|
||||
|
||||
<body class="public-search jr-public-page">
|
||||
<div class="jr-public-shell">
|
||||
<header class="jr-topbar">
|
||||
<div class="jr-topbar-inner">
|
||||
<a href="https://ticket.fse-media.group" id="homeLink" class="jr-top-link">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
<span>返回首页</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="brandLink">
|
||||
<img src="/FSE-ticket.png" alt="FSE Railway" class="jr-brand-logo" />
|
||||
<div class="jr-brand-copy">
|
||||
<strong>FarSight-T.N.E铁路运输</strong>
|
||||
<span>票务查询</span>
|
||||
</div>
|
||||
</a>
|
||||
<nav class="jr-nav" aria-label="站点导航">
|
||||
<a href="https://ticket.fse-media.group/home.html" data-link="home">首页</a>
|
||||
<a href="https://ticket.fse-media.group/order" data-link="order">线上预定</a>
|
||||
<a href="https://ticket.fse-media.group/search" data-link="search">车票查询</a>
|
||||
<a href="https://ticket.fse-media.group/ic-card/search" data-link="card-search">IC 卡查询</a>
|
||||
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
||||
<a href="https://ticket.fse-media.group/machine-map.html" data-link="machine-map" class="is-active">机器位置</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="jr-public-main">
|
||||
<section class="jr-page-intro">
|
||||
<span class="jr-kicker">MACHINE MAP</span>
|
||||
<h1>设备分布图</h1>
|
||||
<p>实时查看所有售票机和检票机的位置及状态。</p>
|
||||
</section>
|
||||
|
||||
<section class="jr-panel-card" style="margin-bottom:24px;">
|
||||
<div class="jr-panel-headline">
|
||||
<h2>机器位置</h2>
|
||||
<span class="jr-panel-note">Live Map</span>
|
||||
</div>
|
||||
<div id="map-container" style="position: relative; width: 100%; height: 600px; background: #1a1a1a; border-radius: 8px; overflow: hidden; border: 1px solid rgba(255,255,255,0.1);">
|
||||
<div id="map-loading" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #666;">
|
||||
加载中...
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<script src="/public-status.js"></script>
|
||||
<script>
|
||||
let machineLocations = {};
|
||||
let selectedMachineId = null;
|
||||
|
||||
function getMapX(x) {
|
||||
if (typeof x !== 'number') return 50;
|
||||
let min = 0, max = 0;
|
||||
const vals = Object.values(machineLocations).map(l => l.x).filter(v => typeof v === 'number');
|
||||
if (vals.length > 0) {
|
||||
min = Math.min(...vals) - 50;
|
||||
max = Math.max(...vals) + 50;
|
||||
} else {
|
||||
return 50;
|
||||
}
|
||||
if (max === min) return 50;
|
||||
return ((x - min) / (max - min)) * 90 + 5;
|
||||
}
|
||||
|
||||
function getMapY(z) {
|
||||
if (typeof z !== 'number') return 50;
|
||||
let min = 0, max = 0;
|
||||
const vals = Object.values(machineLocations).map(l => l.z).filter(v => typeof v === 'number');
|
||||
if (vals.length > 0) {
|
||||
min = Math.min(...vals) - 50;
|
||||
max = Math.max(...vals) + 50;
|
||||
} else {
|
||||
return 50;
|
||||
}
|
||||
if (max === min) return 50;
|
||||
return ((z - min) / (max - min)) * 90 + 5;
|
||||
}
|
||||
|
||||
function renderMap() {
|
||||
const container = document.getElementById('map-container');
|
||||
if (Object.keys(machineLocations).length === 0) {
|
||||
container.innerHTML = `<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #666;">暂无机器坐标数据</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const [id, loc] of Object.entries(machineLocations)) {
|
||||
const x = getMapX(loc.x);
|
||||
const y = getMapY(loc.z);
|
||||
const isSelected = selectedMachineId === id;
|
||||
const isGate = loc.type === 'Gate';
|
||||
const color = isGate ? '#ff9800' : '#4ade80';
|
||||
|
||||
html += `
|
||||
<div class="map-marker" style="position: absolute; left: ${x}%; top: ${y}%; transform: translate(-50%, -50%); cursor: pointer; z-index: ${isSelected ? 10 : 1};" onclick="selectMachine('${id}')">
|
||||
<div style="width: 20px; height: 20px; border-radius: 50%; background: ${color}; border: 2px solid #fff; box-shadow: 0 0 10px rgba(0,0,0,0.5);"></div>
|
||||
`;
|
||||
|
||||
if (isSelected) {
|
||||
html += `
|
||||
<div class="marker-popup" style="position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); background: #333; padding: 10px; border-radius: 6px; width: max-content; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 20; color: white; text-align: center;">
|
||||
<div style="font-weight: bold; margin-bottom: 4px;">${loc.device_id || id}</div>
|
||||
<div style="font-size: 0.85em; color: #aaa;">类型: ${isGate ? '检票机' : '售票机'}</div>
|
||||
`;
|
||||
if (isGate) {
|
||||
const mode = loc.gate_mode === 'entry' ? '进站' : (loc.gate_mode === 'exit' ? '出站' : '双向');
|
||||
html += `<div style="font-size: 0.85em; color: #aaa;">模式: ${mode}</div>`;
|
||||
}
|
||||
html += `
|
||||
<div style="font-size: 0.85em; color: #aaa;">站点: ${loc.station || loc.station_code || '未知'}</div>
|
||||
<div style="font-size: 0.8em; color: #888; margin-top: 4px;">坐标: ${loc.x}, ${loc.y}, ${loc.z}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
window.selectMachine = function(id) {
|
||||
selectedMachineId = id;
|
||||
renderMap();
|
||||
};
|
||||
|
||||
async function fetchLocations() {
|
||||
try {
|
||||
const res = await fetch('/api/public/machine-locations');
|
||||
const data = await res.json();
|
||||
if (data && data.ok) {
|
||||
machineLocations = data.data || {};
|
||||
renderMap();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchLocations();
|
||||
setInterval(fetchLocations, 60000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -38,9 +38,10 @@
|
||||
<a href="https://ticket.fse-media.group/home.html" data-link="home">首页</a>
|
||||
<a href="https://ticket.fse-media.group/order" data-link="order" class="is-active">线上预定</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>
|
||||
<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/machine-map.html" data-link="machine-map">机器位置</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<main class="jr-main">
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
<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>
|
||||
<a href="https://ticket.fse-media.group/ic-card/order" data-link="card-order">线上购卡</a>
|
||||
<a href="https://ticket.fse-media.group/machine-map.html" data-link="machine-map">机器位置</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user