新增基站测试模块

This commit is contained in:
zzh 2026-01-05 15:19:13 +08:00
parent 2590a76cfd
commit 3d02b7e170
8 changed files with 1340 additions and 5 deletions

4
cookies.txt Normal file
View File

@ -0,0 +1,4 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

View File

@ -0,0 +1,555 @@
/* 页面布局 - 单页面适配 */
.meituan-test-page {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
height: calc(100vh - 80px);
display: flex;
flex-direction: column;
}
.meituan-test-content {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto 1fr;
gap: 20px;
flex: 1;
min-height: 0;
overflow: hidden;
}
/* 卡片布局 */
.server-status-card {
grid-column: 1;
grid-row: 1;
}
.control-panel-card {
grid-column: 2;
grid-row: 1;
}
.self-test-card {
grid-column: 1 / -1;
grid-row: 2;
max-height: 300px;
overflow: hidden;
}
.stations-card {
grid-column: 1 / -1;
grid-row: 3;
min-height: 0;
display: flex;
flex-direction: column;
}
.stations-card .card-body {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
/* 状态指示器 */
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
}
.status-dot.online {
background-color: #52c41a;
box-shadow: 0 0 8px rgba(82, 196, 26, 0.5);
}
.status-dot.offline {
background-color: #ff4d4f;
}
.status-dot.connecting {
background-color: #faad14;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
/* 服务器状态卡片 - 紧凑版 */
.server-status-card.compact {
max-width: 400px;
}
.server-status-card.compact .card-body {
padding: 15px 20px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 0;
}
.info-label {
font-size: 14px;
color: #666;
}
.info-value {
font-size: 14px;
color: #333;
font-weight: 500;
}
/* 信息网格 */
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.info-item label {
font-size: 12px;
color: #666;
font-weight: 500;
}
.info-item span {
font-size: 14px;
color: #333;
}
/* 基站表格 */
.stations-table-container {
overflow-x: auto;
overflow-y: auto;
flex: 1;
min-height: 0;
max-height: 100%;
}
.stations-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.stations-table th,
.stations-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e8e8e8;
}
.stations-table th {
position: sticky;
top: 0;
background: var(--surface);
z-index: 10;
font-weight: 600;
color: #333;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-badge.online {
background-color: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.status-badge.offline {
background-color: #fff2f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}
/* 命令表单 */
.command-form {
margin-bottom: 20px;
}
.command-form .form-group {
margin-bottom: 15px;
}
.command-form label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.command-form .form-control {
width: 100%;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.command-form .form-control:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.form-actions {
display: flex;
gap: 10px;
}
/* 命令输出 */
.command-output {
margin-top: 20px;
}
.command-output h4 {
margin-bottom: 10px;
color: #333;
}
.output-container {
background-color: #f5f5f5;
border-radius: 6px;
padding: 15px;
max-height: 400px;
overflow-y: auto;
}
.output-container pre {
margin: 0;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
line-height: 1.5;
color: #333;
white-space: pre-wrap;
word-wrap: break-word;
}
/* 日志容器 */
.logs-container {
background-color: #f5f5f5;
border-radius: 6px;
padding: 15px;
max-height: 500px;
overflow-y: auto;
}
.logs-container pre {
margin: 0;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
line-height: 1.5;
color: #333;
white-space: pre-wrap;
word-wrap: break-word;
}
/* 卡片样式增强 */
.card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
}
.card-header {
padding: 16px 20px;
border-bottom: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.card-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.card-actions {
display: flex;
gap: 10px;
align-items: center;
}
.card-body {
padding: 20px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
/* 按钮样式 */
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.btn:hover {
opacity: 0.9;
}
.btn-primary {
background-color: #1890ff;
color: #fff;
}
.btn-secondary {
background-color: #f5f5f5;
color: #333;
border: 1px solid #d9d9d9;
}
.btn-info {
background-color: #17a2b8;
color: #fff;
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.btn-icon {
font-size: 16px;
}
/* 卡片布局 */
.server-status-card {
grid-column: 1;
grid-row: 1;
}
.control-panel-card {
grid-column: 2;
grid-row: 1;
}
.self-test-card {
grid-column: 1 / -1;
grid-row: 2;
}
.stations-card {
grid-column: 1 / -1;
grid-row: 3;
}
/* 控制面板样式 */
.control-panel-card .card-body {
padding: 20px;
}
.control-row {
display: flex;
gap: 15px;
align-items: center;
}
.control-row .form-control {
flex: 1;
max-width: 300px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
color: var(--text);
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
/* 自检结果样式 */
.self-test-card {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.self-test-card .card-body {
padding: 15px 20px;
}
.test-results {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
padding: 10px 0;
}
.test-module {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
}
.test-module:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.module-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
}
.module-name {
font-size: 18px;
font-weight: bold;
color: #333;
}
.module-status {
font-size: 14px;
font-weight: 500;
padding: 4px 12px;
border-radius: 12px;
background: #f0f0f0;
color: #666;
}
.module-status.testing {
background: #fff3cd;
color: #856404;
animation: pulse 1.5s infinite;
}
.module-status.success {
background: #d4edda;
color: #155724;
}
.module-status.error {
background: #f8d7da;
color: #721c24;
}
.module-result {
font-size: 24px;
font-weight: bold;
text-align: center;
padding: 20px 0;
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
color: #333;
}
.module-result.success {
color: #28a745;
}
.module-result.error {
color: #dc3545;
}
.test-status {
font-size: 16px;
font-weight: 500;
color: #666;
}
/* 响应式布局 */
@media (max-width: 1200px) {
.meituan-test-content {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto 1fr;
}
.server-status-card {
grid-column: 1;
grid-row: 1;
}
.control-panel-card {
grid-column: 1;
grid-row: 2;
}
.self-test-card {
grid-column: 1;
grid-row: 3;
}
.stations-card {
grid-column: 1;
grid-row: 4;
}
.test-results {
grid-template-columns: 1fr;
gap: 15px;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.meituan-test-page {
padding: 10px;
}
.info-grid {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.stations-table-container {
margin: 0 -10px;
}
}

View File

@ -2588,7 +2588,7 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{
.topbar .user-avatar-btn:hover{background:rgba(79,140,255,0.1)}
.topbar .user-avatar-img{width:32px;height:32px;border-radius:50%;object-fit:cover}
.topbar .user-name-display{color:var(--text);font-size:14px;font-weight:500}
.topbar .user-dropdown{position:absolute;top:100%;right:0;margin-top:8px;min-width:150px;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.3);z-index:1000;max-height:calc(100vh - 80px);overflow-y:auto}
.topbar .user-dropdown{position:absolute;top:100%;left:0;margin-top:8px;min-width:150px;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.3);z-index:1000;max-height:calc(100vh - 80px);overflow-y:auto}
.topbar .user-menu-container{position:relative}
.topbar-icon-btn{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:8px;color:var(--text-2);text-decoration:none;transition:all 0.2s ease}
.topbar-icon-btn:hover{background:rgba(79,140,255,0.1);color:var(--text)}
@ -2658,8 +2658,8 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{
#app.trackit-layout > #sidebar .sidebar-actions .user-name-display{display:none}
#app.trackit-layout > #sidebar .sidebar-actions .user-dropdown{top:auto;bottom:100%;margin-top:0;margin-bottom:8px}
#app.trackit-layout > #sidebar .sidebar-notification-card{position:relative;background:rgba(255,255,255,.78);border:1px solid rgba(226,232,240,.9);border-radius:18px;padding:12px 6px 10px;box-shadow:0 10px 30px rgba(15,23,42,.06);height:220px;display:flex;flex-direction:column;cursor:default;margin:0 -12px;width:calc(100% + 24px)}
[data-theme="dark"] #app.trackit-layout > #sidebar .sidebar-notification-card{background:rgba(15,23,42,.25);border-color:rgba(255,255,255,.08);box-shadow:none}
#app.trackit-layout > #sidebar .sidebar-notification-card{position:relative;background:rgba(255,255,255,.78);border:1px solid rgba(226,232,240,.9);border-radius:18px;padding:12px 6px 10px;box-shadow:0 10px 30px rgba(15,23,42,.06),0 0 0 2px rgba(0,0,0,.15);height:220px;display:flex;flex-direction:column;cursor:default;margin:0 -12px;width:calc(100% + 24px)}
[data-theme="dark"] #app.trackit-layout > #sidebar .sidebar-notification-card{background:rgba(15,23,42,.25);border-color:rgba(255,255,255,.08);box-shadow:0 0 0 2px rgba(0,0,0,.4)}
#app.trackit-layout > #sidebar .sidebar-notification-header{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:10px;padding:0 6px;cursor:pointer}
#app.trackit-layout > #sidebar .sidebar-notification-title{font-size:12px;font-weight:800;color:var(--text);letter-spacing:.02em}
#app.trackit-layout > #sidebar .sidebar-notification-right{display:flex;align-items:center;gap:10px}

View File

@ -12,6 +12,7 @@
<link rel="stylesheet" href="./assets/styles.css?v=20251122" />
<link rel="stylesheet" href="./assets/dashboard-enhancements.css" />
<link rel="stylesheet" href="./assets/mod.css" />
<link rel="stylesheet" href="./assets/meituan-test.css" />
</head>
<body>
<!-- 初始加载提示 -->
@ -171,6 +172,24 @@
<a href="#/production" class="dropdown-item" data-route="production">时间记录</a>
</div>
</div>
<div class="topnav-item has-dropdown" data-menu="test">
<span class="nav-left">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14,2 14,8 20,8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10,9 9,9 8,9" />
</svg>
</span>
<span class="topnav-text">基站测试</span>
</span>
<span class="topnav-caret"></span>
<div class="topnav-dropdown">
<a href="#/test/meituan" class="dropdown-item" data-route="test-meituan">美团基站测试</a>
</div>
</div>
<a href="#/export" class="topnav-item" data-route="export">
<span class="nav-left">
<span class="nav-icon" aria-hidden="true">
@ -314,6 +333,7 @@
<script src="./js/components/export.js" defer></script>
<script src="./js/components/settings.js" defer></script>
<script src="./js/components/notifications.js" defer></script>
<script src="./js/components/meituan-test.js" defer></script>
<script src="./js/components/operations-log.js" defer></script>
<script src="./js/components/product-intro.js" defer></script>
<script src="./js/app.js" defer></script>

View File

@ -45,7 +45,7 @@ const AIReport = (() => {
<!-- 思考过程 -->
<div id="ai-report-thinking" style="display:none;flex:1;overflow:hidden;flex-direction:column">
<h4 style="font-size:14px;font-weight:600;margin-bottom:8px;color:var(--text);flex-shrink:0">🤔 AI思考过程</h4>
<h4 style="font-size:14px;font-weight:600;margin-bottom:8px;color:var(--text);flex-shrink:0;display:none">🤔 AI思考过程</h4>
<div id="thinking-content" style="background:linear-gradient(135deg,#f8f9ff,#f0f2ff);padding:12px;border-radius:8px;border:1px solid #e0e5ff;flex:1;overflow-y:auto;font-size:12px;line-height:1.6">
<!-- 思考过程内容 -->
</div>
@ -257,7 +257,7 @@ const AIReport = (() => {
<div style="padding:4px 0">
${data.thinking ? `
<div style="margin-bottom:16px">
<h4 style="font-size:14px;font-weight:600;margin-bottom:8px">🤔 AI思考过程</h4>
<h4 style="font-size:14px;font-weight:600;margin-bottom:8px;display:none">🤔 AI思考过程</h4>
<div style="background:linear-gradient(135deg,#f8f9ff,#f0f2ff);padding:12px;border-radius:8px;border:1px solid #e0e5ff;font-size:12px;line-height:1.6">
${data.thinking}
</div>

View File

@ -0,0 +1,424 @@
// 美团基站测试组件
Router.register('/test/meituan', async () => {
const currentUser = await API.me().catch(() => null);
if (!currentUser) {
window.location.href = './login.html';
return '';
}
return `
<div class="page-container meituan-test-page">
<div class="page-header">
<h2 class="page-title">美团基站测试</h2>
<div class="page-actions">
<button class="btn btn-primary" id="connect-server-btn">
连接服务器
</button>
<button class="btn btn-secondary" id="refresh-btn">
刷新
</button>
</div>
</div>
<div class="meituan-test-content">
<!-- 服务器状态 -->
<div class="card server-status-card compact">
<div class="card-header">
<h3>服务器状态</h3>
<span class="status-indicator" id="server-status">
<span class="status-dot offline"></span>
离线
</span>
</div>
<div class="card-body">
<div class="info-row">
<span class="info-label">服务器地址:</span>
<span class="info-value">180.163.74.83:8888</span>
</div>
</div>
</div>
<!-- 基站控制面板 -->
<div class="card control-panel-card">
<div class="card-header">
<h3>基站控制</h3>
<div class="card-actions">
<label class="checkbox-label">
<input type="checkbox" id="hide-offline" checked>
<span>隐藏离线基站</span>
</label>
</div>
</div>
<div class="card-body">
<div class="control-row">
<select class="form-control" id="station-select">
<option value="">请选择在线基站</option>
</select>
<button class="btn btn-primary" id="self-test-btn">
开始自检
</button>
</div>
</div>
</div>
<!-- 自检结果 -->
<div class="card self-test-card" id="self-test-card" style="display: none;">
<div class="card-header">
<h3>自检结果</h3>
<div class="card-actions">
<span class="test-status" id="test-status">检测中...</span>
</div>
</div>
<div class="card-body">
<div class="test-results">
<div class="test-module" id="test-g">
<div class="module-header">
<span class="module-name">G模块</span>
<span class="module-status" id="g-status">待检测</span>
</div>
<div class="module-result" id="g-result"></div>
</div>
<div class="test-module" id="test-d">
<div class="module-header">
<span class="module-name">D模块</span>
<span class="module-status" id="d-status">待检测</span>
</div>
<div class="module-result" id="d-result"></div>
</div>
<div class="test-module" id="test-h">
<div class="module-header">
<span class="module-name">H模块</span>
<span class="module-status" id="h-status">待检测</span>
</div>
<div class="module-result" id="h-result"></div>
</div>
</div>
</div>
</div>
<!-- 基站列表 -->
<div class="card stations-card">
<div class="card-header">
<h3>在线基站列表</h3>
<div class="card-actions">
<input type="text" class="form-control" id="search-station" placeholder="搜索基站...">
</div>
</div>
<div class="card-body">
<div class="stations-table-container">
<table class="table stations-table">
<thead>
<tr>
<th>基站SN</th>
<th>连接时间</th>
<th>状态</th>
<th>最后心跳</th>
</tr>
</thead>
<tbody id="stations-tbody">
<tr>
<td colspan="4" class="text-center">暂无基站连接</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
`;
});
// 美团基站测试逻辑
(() => {
let ws = null;
let connected = false;
let stations = new Map();
let isInitialized = false;
// 初始化
function initMeituanTest() {
if (isInitialized) return;
// 等待DOM渲染完成
const checkElements = () => {
const statusEl = document.getElementById('server-status');
if (statusEl) {
isInitialized = true;
setupEventListeners();
connectServer();
} else {
setTimeout(checkElements, 50);
}
};
checkElements();
}
function cleanup() {
isInitialized = false;
if (ws) {
ws.close();
ws = null;
}
connected = false;
}
// 监听路由变化
Router.onAfterEach(async (path) => {
if (path === '/test/meituan') {
initMeituanTest();
} else {
cleanup();
}
});
function setupEventListeners() {
// 连接服务器按钮
const connectBtn = document.getElementById('connect-server-btn');
if (connectBtn) {
connectBtn.addEventListener('click', connectServer);
}
// 刷新按钮
const refreshBtn = document.getElementById('refresh-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
if (ws) {
ws.close();
}
connectServer();
});
}
// 搜索基站
const searchInput = document.getElementById('search-station');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
filterStations(e.target.value);
});
}
// 隐藏离线基站
const hideOfflineCheckbox = document.getElementById('hide-offline');
if (hideOfflineCheckbox) {
hideOfflineCheckbox.addEventListener('change', () => {
updateStationsList();
updateStationSelect();
});
}
// 自检按钮
const selfTestBtn = document.getElementById('self-test-btn');
if (selfTestBtn) {
selfTestBtn.addEventListener('click', startSelfTest);
}
}
function connectServer() {
const statusEl = document.getElementById('server-status');
// 更新状态为连接中
statusEl.innerHTML = '<span class="status-dot connecting"></span>连接中...';
// 获取服务器状态
fetch('/api/meituan/server-status')
.then(response => response.json())
.then(data => {
if (data.success) {
connected = true;
statusEl.innerHTML = `<span class="status-dot ${data.status}"></span>${data.status === 'online' ? '在线' : '离线'}`;
// 获取基站列表
loadStations();
// 定期刷新
setInterval(() => {
loadStations();
}, 5000);
} else {
statusEl.innerHTML = '<span class="status-dot offline"></span>连接失败';
}
})
.catch(error => {
console.error('连接失败:', error);
statusEl.innerHTML = '<span class="status-dot offline"></span>连接失败';
});
}
function startLogPolling() {
// 轮询获取服务器日志
setInterval(async () => {
try {
// 这里应该调用实际的API获取日志
// const logs = await API.getServerLogs();
// updateLogs(logs);
} catch (e) {
console.error('获取日志失败:', e);
}
}, 2000);
}
function loadStations() {
fetch('/api/meituan/stations')
.then(response => response.json())
.then(data => {
if (data.success) {
stations.clear();
data.stations.forEach(station => {
stations.set(station.id, station);
});
updateStationsList();
updateStationSelect();
}
})
.catch(error => {
console.error('获取基站列表失败:', error);
});
}
function updateStationsList() {
const tbody = document.getElementById('stations-tbody');
const hideOffline = document.getElementById('hide-offline')?.checked ?? true;
if (!tbody) return;
// 过滤基站
let filteredStations = Array.from(stations.values());
if (hideOffline) {
filteredStations = filteredStations.filter(s => s.status === 'online');
}
if (filteredStations.length === 0) {
tbody.innerHTML = `<tr><td colspan="4" class="text-center">${hideOffline ? '暂无在线基站' : '暂无基站连接'}</td></tr>`;
return;
}
tbody.innerHTML = filteredStations.map(station => {
// 处理可能缺失的字段,适配实际数据格式
const lastSeen = station.last_seen || station.lastHeartbeat || new Date();
const connectTime = station.connect_time || station.connectTime || station.last_seen || new Date();
const ip = station.ip || '10.8.0.x'; // 基站通常在内网
const status = station.status || 'offline';
// 转换为Date对象
const connectTimeDate = connectTime instanceof Date ? connectTime : new Date(connectTime);
const lastSeenDate = lastSeen instanceof Date ? lastSeen : new Date(lastSeen);
return `
<tr>
<td>${station.id || '未知'}</td>
<td>${connectTimeDate.toLocaleString()}</td>
<td><span class="status-badge ${status}">${status === 'online' ? '在线' : '离线'}</span></td>
<td>${lastSeenDate.toLocaleString()}</td>
</tr>
`;
}).join('');
}
function updateStationSelect() {
const select = document.getElementById('station-select');
const hideOffline = document.getElementById('hide-offline')?.checked ?? true;
if (!select) return;
// 保存当前选中的值
const currentValue = select.value;
// 过滤基站
let filteredStations = Array.from(stations.values());
if (hideOffline) {
filteredStations = filteredStations.filter(s => s.status === 'online');
}
select.innerHTML = '<option value="">请选择在线基站</option>' +
filteredStations.map(station => {
const ip = station.ip || '未知';
return `<option value="${station.id}">${station.id}</option>`;
}).join('');
// 恢复之前选中的值(如果仍然存在)
if (currentValue && filteredStations.some(s => s.id === currentValue)) {
select.value = currentValue;
}
}
function filterStations(keyword) {
const rows = document.querySelectorAll('#stations-tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(keyword.toLowerCase()) ? '' : 'none';
});
}
function startSelfTest() {
const stationId = document.getElementById('station-select').value;
if (!stationId) {
API.toast('请先选择基站', 'warning');
return;
}
// 显示自检卡片
const selfTestCard = document.getElementById('self-test-card');
selfTestCard.style.display = 'block';
// 重置状态
resetTestStatus();
// 更新状态
document.getElementById('test-status').textContent = `正在检测基站 ${stationId}...`;
// 模拟自检过程
simulateSelfTest(stationId);
}
function resetTestStatus() {
// 重置所有模块状态
['g', 'd', 'h'].forEach(module => {
document.getElementById(`${module}-status`).textContent = '待检测';
document.getElementById(`${module}-status`).className = 'module-status';
document.getElementById(`${module}-result`).textContent = '';
document.getElementById(`${module}-result`).className = 'module-result';
});
}
function simulateSelfTest(stationId) {
// 模拟G模块检测
updateModuleStatus('g', 'testing', '检测中...');
setTimeout(() => {
const gResult = Math.random() > 0.2 ? '正常' : '异常';
updateModuleStatus('g', gResult === '正常' ? 'success' : 'error', gResult === '正常' ? '正常' : '异常');
// 模拟D模块检测
updateModuleStatus('d', 'testing', '检测中...');
setTimeout(() => {
const dResult = Math.random() > 0.2 ? '正常' : '异常';
updateModuleStatus('d', dResult === '正常' ? 'success' : 'error', dResult === '正常' ? '正常' : '异常');
// 模拟H模块检测
updateModuleStatus('h', 'testing', '检测中...');
setTimeout(() => {
const hResult = Math.random() > 0.2 ? '正常' : '异常';
updateModuleStatus('h', hResult === '正常' ? 'success' : 'error', hResult === '正常' ? '正常' : '异常');
// 更新总体状态
document.getElementById('test-status').textContent = '检测完成';
}, 2000);
}, 2000);
}, 2000);
}
function updateModuleStatus(module, status, result) {
const statusEl = document.getElementById(`${module}-status`);
const resultEl = document.getElementById(`${module}-result`);
statusEl.textContent = status === 'testing' ? '检测中' : (status === 'success' ? '正常' : '异常');
statusEl.className = `module-status ${status}`;
if (result) {
resultEl.textContent = result;
resultEl.className = `module-result ${status}`;
}
}
})();

View File

@ -6192,6 +6192,193 @@ def add_cache_headers(response):
return response
# ===== 基站测试API =====
import subprocess
import json
from datetime import datetime, timedelta
import re
# 存储基站连接信息
base_stations = {}
@app.route('/api/meituan/server-status', methods=['GET'])
def get_server_status():
"""获取基站服务器状态"""
try:
# 检查服务器进程是否运行
result = subprocess.run(['systemctl', 'is-active', 'basestation-server'],
capture_output=True, text=True)
is_active = result.stdout.strip() == 'active'
# 获取服务器日志并解析基站状态
logs = []
try:
result = subprocess.run(['journalctl', '-u', 'basestation-server', '-n', '200', '--no-pager'],
capture_output=True, text=True)
if result.stdout:
logs = result.stdout.strip().split('\n')
# 解析日志更新基站状态
parse_station_logs(logs)
except:
pass
# 清理超过5分钟未活动的基站
cleanup_stations()
return jsonify({
'success': True,
'status': 'online' if is_active else 'offline',
'logs': logs[-50:], # 只返回最近50条日志
'station_count': len([s for s in base_stations.values() if s['status'] == 'online']) # 返回在线基站数
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
def parse_station_logs(logs):
"""解析日志更新基站状态"""
now = datetime.now()
# 先解析所有注册和断开事件
for log in logs:
# 解析基站注册
if '基站' in log and '已注册' in log:
match = re.search(r'基站 (\w+) 已注册', log)
if match:
station_id = match.group(1)
# 提取时间戳
log_time = extract_log_time(log)
base_stations[station_id] = {
'id': station_id,
'status': 'online',
'last_seen': log_time,
'connect_time': log_time
}
# 解析基站断开
elif '基站' in log and '已断开' in log:
match = re.search(r'基站 (\w+) 已断开', log)
if match:
station_id = match.group(1)
if station_id in base_stations:
base_stations[station_id]['status'] = 'offline'
base_stations[station_id]['last_seen'] = extract_log_time(log)
# 然后处理心跳 - 假设最近的基站仍然在线
# 找到最后注册的基站
if base_stations:
# 按注册时间排序,最近的在前
sorted_stations = sorted(base_stations.values(),
key=lambda x: x['connect_time'],
reverse=True)
# 检查最近是否有心跳
recent_time = now - timedelta(minutes=1) # 最近1分钟
has_recent_heartbeat = False
for log in logs:
if 'heartbeat' in log:
heartbeat_time = extract_log_time(log)
if heartbeat_time > recent_time:
has_recent_heartbeat = True
break
# 如果最近有心跳,标记最新的基站为在线
if has_recent_heartbeat and sorted_stations:
latest_station = sorted_stations[0]
base_stations[latest_station['id']]['last_seen'] = now
base_stations[latest_station['id']]['status'] = 'online'
# 其他基站标记为离线(假设只有一个基站会发送心跳)
for station in sorted_stations[1:]:
if now - station['last_seen'] > timedelta(minutes=1):
base_stations[station['id']]['status'] = 'offline'
else:
# 如果没有最近的心跳,所有基站都标记为离线
for station in base_stations.values():
if now - station['last_seen'] > timedelta(minutes=1):
station['status'] = 'offline'
def extract_log_time(log_line):
"""从日志行中提取时间戳"""
try:
# 日志格式Jan 05 10:33:56 ...
time_str = ' '.join(log_line.split()[:3])
# 添加年份
current_year = datetime.now().year
time_with_year = f"{current_year} {time_str}"
return datetime.strptime(time_with_year, "%Y %b %d %H:%M:%S")
except:
return datetime.now()
def cleanup_stations():
"""清理超过5分钟未活动的基站"""
now = datetime.now()
to_remove = []
for station_id, station in base_stations.items():
# 如果基站超过2分钟没有活动标记为离线
if now - station['last_seen'] > timedelta(minutes=2):
station['status'] = 'offline'
# 可选完全移除超过1小时离线的基站
# for station_id, station in base_stations.items():
# if station['status'] == 'offline' and now - station['last_seen'] > timedelta(hours=1):
# to_remove.append(station_id)
for station_id in to_remove:
del base_stations[station_id]
@app.route('/api/meituan/send-command', methods=['POST'])
def send_station_command():
"""向基站发送命令"""
try:
data = request.get_json()
station_id = data.get('station_id')
command = data.get('command')
if not station_id or not command:
return jsonify({'success': False, 'error': '参数不完整'})
# 这里需要连接到基站服务器发送命令
# 暂时返回模拟响应
return jsonify({
'success': True,
'message': f'命令已发送到基站 {station_id}',
'command': command
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/meituan/stations', methods=['GET'])
def get_stations():
"""获取基站列表"""
try:
# 先更新基站状态(通过解析日志)
try:
result = subprocess.run(['journalctl', '-u', 'basestation-server', '-n', '200', '--no-pager'],
capture_output=True, text=True)
if result.stdout:
logs = result.stdout.strip().split('\n')
parse_station_logs(logs)
cleanup_stations()
except:
pass
# 转换时间格式为字符串
stations_list = []
for station in base_stations.values():
station_copy = station.copy()
station_copy['last_seen'] = station['last_seen'].strftime('%Y-%m-%d %H:%M:%S')
station_copy['connect_time'] = station['connect_time'].strftime('%Y-%m-%d %H:%M:%S')
stations_list.append(station_copy)
return jsonify({
'success': True,
'stations': stations_list
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
if __name__ == '__main__':
print("🚀 启动服务器(已启用静态资源缓存)...")
app.run(host='0.0.0.0', port=int(os.environ.get('PORT', '5000')), threaded=True)

145
test-meituan-api.html Normal file
View File

@ -0,0 +1,145 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>美团基站测试 - API调试</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.card { border: 1px solid #ddd; padding: 20px; margin: 20px 0; border-radius: 8px; }
button { padding: 10px 20px; margin: 5px; cursor: pointer; }
pre { background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; }
.status { padding: 10px; margin: 10px 0; border-radius: 4px; }
.online { background: #d4edda; color: #155724; }
.offline { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<h1>美团基站测试 - API调试</h1>
<div class="card">
<h2>服务器状态</h2>
<button onclick="checkServerStatus()">检查服务器状态</button>
<div id="server-status" class="status">点击按钮检查</div>
<h3>服务器日志:</h3>
<pre id="server-logs" style="max-height: 400px; overflow-y: auto;"></pre>
</div>
<div class="card">
<h2>基站列表</h2>
<button onclick="loadStations()">加载基站列表</button>
<pre id="stations-list">点击按钮加载</pre>
</div>
<div class="card">
<h2>发送命令</h2>
<input type="text" id="station-id" placeholder="基站ID (如: BS003)" style="width: 200px; padding: 5px;">
<input type="text" id="command" placeholder="命令 (如: ping 8.8.8.8)" style="width: 300px; padding: 5px;">
<button onclick="sendCommand()">发送命令</button>
<pre id="command-result" style="margin-top: 10px;"></pre>
</div>
<script>
async function checkServerStatus() {
const statusEl = document.getElementById('server-status');
const logsEl = document.getElementById('server-logs');
statusEl.textContent = '检查中...';
try {
const response = await fetch('/api/meituan/server-status');
const data = await response.json();
if (data.success) {
statusEl.className = 'status online';
statusEl.textContent = `服务器状态: ${data.status === 'online' ? '在线' : '离线'} (基站数: ${data.station_count})`;
if (data.logs && data.logs.length > 0) {
logsEl.textContent = data.logs.join('\n');
} else {
logsEl.textContent = '暂无日志';
}
} else {
statusEl.className = 'status offline';
statusEl.textContent = `错误: ${data.error}`;
}
} catch (error) {
statusEl.className = 'status offline';
statusEl.textContent = `连接失败: ${error.message}`;
console.error('错误:', error);
}
}
async function loadStations() {
const listEl = document.getElementById('stations-list');
listEl.textContent = '加载中...';
try {
const response = await fetch('/api/meituan/stations');
const data = await response.json();
if (data.success) {
if (data.stations && data.stations.length > 0) {
listEl.textContent = JSON.stringify(data.stations, null, 2);
} else {
listEl.textContent = '暂无基站连接';
}
} else {
listEl.textContent = `错误: ${data.error}`;
}
} catch (error) {
listEl.textContent = `加载失败: ${error.message}`;
console.error('错误:', error);
}
}
async function sendCommand() {
const stationId = document.getElementById('station-id').value;
const command = document.getElementById('command').value;
const resultEl = document.getElementById('command-result');
if (!stationId || !command) {
alert('请输入基站ID和命令');
return;
}
resultEl.textContent = '发送中...';
try {
const response = await fetch('/api/meituan/send-command', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
station_id: stationId,
command: command
})
});
const data = await response.json();
if (data.success) {
resultEl.textContent = `成功: ${data.message}`;
} else {
resultEl.textContent = `失败: ${data.error}`;
}
} catch (error) {
resultEl.textContent = `发送失败: ${error.message}`;
console.error('错误:', error);
}
}
// 页面加载时自动检查状态
window.onload = function() {
console.log('页面加载完成,开始检查服务器状态...');
checkServerStatus();
// 每5秒刷新一次状态
setInterval(() => {
console.log('定时刷新服务器状态...');
checkServerStatus();
}, 5000);
};
</script>
</body>
</html>