<!DOCTYPE html>
|
<html lang="zh-CN">
|
<head>
|
<meta charset="UTF-8">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>机器人日志系统</title>
|
<style>
|
* {
|
margin: 0;
|
padding: 0;
|
box-sizing: border-box;
|
}
|
|
body {
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
background-color: #f5f7fa;
|
color: #333;
|
}
|
|
.header {
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
color: white;
|
padding: 15px 30px;
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
}
|
|
.header h1 {
|
font-size: 22px;
|
font-weight: 600;
|
}
|
|
.header button {
|
background-color: rgba(255, 255, 255, 0.2);
|
color: white;
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
padding: 8px 20px;
|
border-radius: 20px;
|
cursor: pointer;
|
font-size: 14px;
|
transition: all 0.3s ease;
|
}
|
|
.header button:hover {
|
background-color: rgba(255, 255, 255, 0.3);
|
}
|
|
.container {
|
padding: 30px;
|
}
|
|
.filter-section {
|
background-color: white;
|
padding: 20px;
|
border-radius: 12px;
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
margin-bottom: 20px;
|
display: grid;
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
gap: 15px;
|
animation: fadeIn 0.5s ease-in-out;
|
}
|
|
@keyframes fadeIn {
|
from {
|
opacity: 0;
|
transform: translateY(20px);
|
}
|
to {
|
opacity: 1;
|
transform: translateY(0);
|
}
|
}
|
|
.filter-group {
|
display: flex;
|
flex-direction: column;
|
}
|
|
.filter-group label {
|
margin-bottom: 8px;
|
font-weight: 500;
|
color: #666;
|
font-size: 14px;
|
}
|
|
.filter-group select,
|
.filter-group input {
|
padding: 10px 12px;
|
border: 2px solid #e0e0e0;
|
border-radius: 8px;
|
font-size: 14px;
|
transition: border-color 0.3s ease;
|
}
|
|
.filter-group select:focus,
|
.filter-group input:focus {
|
outline: none;
|
border-color: #667eea;
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
}
|
|
.filter-actions {
|
grid-column: 1 / -1;
|
display: flex;
|
gap: 10px;
|
margin-top: 10px;
|
}
|
|
.btn {
|
padding: 10px 20px;
|
border: none;
|
border-radius: 8px;
|
font-size: 14px;
|
font-weight: 500;
|
cursor: pointer;
|
transition: all 0.3s ease;
|
}
|
|
.btn-primary {
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
color: white;
|
}
|
|
.btn-primary:hover {
|
transform: translateY(-2px);
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
}
|
|
.btn-secondary {
|
background-color: #f0f0f0;
|
color: #333;
|
}
|
|
.btn-secondary:hover {
|
background-color: #e0e0e0;
|
}
|
|
.log-section {
|
background-color: white;
|
border-radius: 12px;
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
overflow: hidden;
|
}
|
|
.log-header {
|
padding: 20px;
|
border-bottom: 1px solid #f0f0f0;
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.log-header h2 {
|
font-size: 18px;
|
font-weight: 600;
|
color: #333;
|
}
|
|
.log-content {
|
padding: 0;
|
}
|
|
table {
|
width: 100%;
|
border-collapse: collapse;
|
}
|
|
th, td {
|
padding: 15px 20px;
|
text-align: left;
|
border-bottom: 1px solid #f0f0f0;
|
}
|
|
th {
|
background-color: #f9f9f9;
|
font-weight: 600;
|
color: #666;
|
font-size: 14px;
|
text-transform: uppercase;
|
letter-spacing: 0.5px;
|
}
|
|
tr:hover {
|
background-color: #f9f9f9;
|
}
|
|
td {
|
font-size: 14px;
|
color: #555;
|
}
|
|
.loading {
|
text-align: center;
|
padding: 60px;
|
color: #999;
|
font-size: 16px;
|
}
|
|
.error {
|
text-align: center;
|
padding: 60px;
|
color: #ff4d4f;
|
font-size: 16px;
|
}
|
|
.empty {
|
text-align: center;
|
padding: 60px;
|
color: #999;
|
font-size: 16px;
|
}
|
|
.status-badge {
|
display: inline-block;
|
padding: 4px 12px;
|
border-radius: 12px;
|
font-size: 12px;
|
font-weight: 500;
|
}
|
|
.status-up {
|
background-color: #e6f7ee;
|
color: #52c41a;
|
}
|
|
.status-down {
|
background-color: #fff2e8;
|
color: #fa8c16;
|
}
|
|
@media (max-width: 768px) {
|
.container {
|
padding: 15px;
|
}
|
|
.filter-section {
|
grid-template-columns: 1fr;
|
}
|
|
.filter-actions {
|
flex-direction: column;
|
}
|
|
.btn {
|
width: 100%;
|
}
|
|
table {
|
display: block;
|
overflow-x: auto;
|
}
|
}
|
</style>
|
</head>
|
<body>
|
<div class="header">
|
<h1>机器人上下行日志</h1>
|
<button id="logoutBtn">登出</button>
|
</div>
|
<div class="container">
|
<div class="filter-section">
|
<div class="filter-group">
|
<label for="deviceId">小车编号</label>
|
<input type="text" id="deviceId" placeholder="请输入小车编号">
|
</div>
|
<div class="filter-group">
|
<label for="messageType">消息类型</label>
|
<select id="messageType">
|
<option value="">全部</option>
|
<!-- 从后台接口获取 -->
|
</select>
|
</div>
|
<div class="filter-group">
|
<label for="tag">标签</label>
|
<select id="tag">
|
<option value="">全部</option>
|
<!-- 从后台接口获取 -->
|
</select>
|
</div>
|
<div class="filter-group">
|
<label for="startTime">开始时间</label>
|
<input type="datetime-local" id="startTime" step="1">
|
</div>
|
<div class="filter-group">
|
<label for="endTime">结束时间</label>
|
<input type="datetime-local" id="endTime" step="1">
|
</div>
|
<div class="filter-actions">
|
<button class="btn btn-primary" id="searchBtn">查询</button>
|
<button class="btn btn-secondary" id="resetBtn">重置</button>
|
<button class="btn btn-secondary" id="refreshBtn">刷新</button>
|
</div>
|
</div>
|
|
<div class="log-section">
|
<div class="log-header">
|
<h2>日志记录</h2>
|
<span id="recordCount">共 0 条记录</span>
|
</div>
|
<div class="log-content">
|
<div id="loading" class="loading">加载中...</div>
|
<div id="error" class="error" style="display: none;"></div>
|
<div id="empty" class="empty" style="display: none;">暂无符合条件的日志记录</div>
|
<table id="logTable" style="display: none;">
|
<thead>
|
<tr>
|
<th>时间</th>
|
<th>设备ID</th>
|
<th>消息类型</th>
|
<th>标签</th>
|
<th>消息内容</th>
|
<th>操作</th>
|
</tr>
|
</thead>
|
<tbody id="logTableBody">
|
</tbody>
|
</table>
|
</div>
|
</div>
|
</div>
|
<script>
|
// 检查登录状态
|
function checkLogin() {
|
if (!localStorage.getItem('loggedIn')) {
|
window.location.href = '/login';
|
}
|
}
|
|
// 登出功能
|
document.getElementById('logoutBtn').addEventListener('click', function () {
|
localStorage.removeItem('loggedIn');
|
window.location.href = '/login';
|
});
|
|
// 重置筛选条件
|
document.getElementById('resetBtn').addEventListener('click', function () {
|
document.getElementById('deviceId').value = '';
|
document.getElementById('messageType').value = '';
|
document.getElementById('tag').value = '';
|
document.getElementById('startTime').value = '';
|
document.getElementById('endTime').value = '';
|
loadLogData();
|
});
|
|
// 查询按钮点击事件
|
document.getElementById('searchBtn').addEventListener('click', loadLogData);
|
|
// 刷新按钮点击事件
|
document.getElementById('refreshBtn').addEventListener('click', loadLogData);
|
|
// 加载日志数据
|
function loadLogData() {
|
document.getElementById('loading').style.display = 'block';
|
document.getElementById('error').style.display = 'none';
|
document.getElementById('empty').style.display = 'none';
|
document.getElementById('logTable').style.display = 'none';
|
|
// 获取筛选条件
|
const deviceId = document.getElementById('deviceId').value;
|
const messageType = document.getElementById('messageType').value;
|
const tag = document.getElementById('tag').value;
|
const startTime = document.getElementById('startTime').value;
|
const endTime = document.getElementById('endTime').value;
|
|
// 构建查询参数
|
const params = new URLSearchParams();
|
if (deviceId) params.append('deviceId', deviceId);
|
if (messageType) params.append('type', messageType);
|
if (tag) params.append('event', tag);
|
if (startTime) {
|
// 使用原始的日期格式
|
params.append('startTime', startTime+"Z");
|
}
|
if (endTime) {
|
// 使用原始的日期格式
|
params.append('endTime', endTime+"Z");
|
}
|
|
// 调用查询接口
|
fetch(`/deviceLog/query?${params.toString()}`)
|
.then(response => response.json())
|
.then(data => {
|
document.getElementById('loading').style.display = 'none';
|
|
// 检查接口返回格式
|
if (data && data.code === 200 && data.data) {
|
// 应用筛选条件(如果后台没有处理筛选)
|
let filteredData = data.data;
|
if (deviceId) {
|
// 模糊匹配设备ID
|
filteredData = filteredData.filter(item => {
|
return item.deviceId && item.deviceId.includes(deviceId);
|
});
|
}
|
if (messageType) {
|
// 直接使用messageType进行匹配
|
filteredData = filteredData.filter(item => {
|
return item.type && item.type === messageType;
|
});
|
}
|
if (tag) {
|
// 使用event字段作为标签进行筛选
|
filteredData = filteredData.filter(item => {
|
return item.event && item.event === tag;
|
});
|
}
|
if (startTime) {
|
const start = new Date(startTime).getTime();
|
filteredData = filteredData.filter(item => {
|
const itemTime = new Date(item.timestamp).getTime();
|
return itemTime >= start;
|
});
|
}
|
if (endTime) {
|
const end = new Date(endTime).getTime();
|
filteredData = filteredData.filter(item => {
|
const itemTime = new Date(item.timestamp).getTime();
|
return itemTime <= end;
|
});
|
}
|
|
// 更新记录数
|
document.getElementById('recordCount').textContent = `共 ${filteredData.length} 条记录`;
|
|
if (filteredData && filteredData.length > 0) {
|
document.getElementById('logTable').style.display = 'table';
|
const tbody = document.getElementById('logTableBody');
|
tbody.innerHTML = '';
|
|
filteredData.forEach(item => {
|
const row = document.createElement('tr');
|
const statusClass = item.type === 'up' ? 'status-up' : 'status-down';
|
const statusText = item.type === 'up' ? '上行' : '下行';
|
const sourceHexStr = item.sourceHexStr || '';
|
|
// 格式化时间戳
|
let formattedTime = '-';
|
if (item.timestamp) {
|
try {
|
const date = new Date(item.timestamp/1000/1000);
|
formattedTime = date.toLocaleString('zh-CN', {
|
year: 'numeric',
|
month: '2-digit',
|
day: '2-digit',
|
hour: '2-digit',
|
minute: '2-digit',
|
second: '2-digit'
|
});
|
} catch (e) {
|
formattedTime = item.timestamp;
|
}
|
}
|
|
row.innerHTML = `
|
<td>${formattedTime}</td>
|
<td>${item.deviceId || '-'}</td>
|
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
<td>${item.event || '-'}</td>
|
<td>${sourceHexStr}</td>
|
<td>
|
<button class="btn btn-secondary parse-btn" data-hex="${sourceHexStr}">解析</button>
|
</td>
|
`;
|
tbody.appendChild(row);
|
});
|
|
// 为解析按钮添加点击事件
|
document.querySelectorAll('.parse-btn').forEach(btn => {
|
btn.addEventListener('click', function() {
|
const hexData = this.getAttribute('data-hex');
|
console.log('解析按钮点击,hexData:', hexData);
|
if (hexData) {
|
parseHexData(hexData);
|
} else {
|
alert('没有可解析的消息内容');
|
}
|
});
|
});
|
} else {
|
document.getElementById('empty').style.display = 'block';
|
}
|
} else {
|
document.getElementById('error').textContent = '加载数据失败: ' + (data.message || '未知错误');
|
document.getElementById('error').style.display = 'block';
|
}
|
})
|
.catch(error => {
|
document.getElementById('loading').style.display = 'none';
|
document.getElementById('error').textContent = '加载数据失败: ' + error.message;
|
document.getElementById('error').style.display = 'block';
|
});
|
}
|
|
// 从后台接口获取消息类型和标签
|
function loadFilterOptions() {
|
// 获取消息类型
|
fetch('/deviceLog/queryType')
|
.then(response => response.json())
|
.then(data => {
|
if (data && data.code === 200 && data.data) {
|
const messageTypeSelect = document.getElementById('messageType');
|
messageTypeSelect.innerHTML = '<option value="">全部</option>';
|
data.data.forEach(type => {
|
const option = document.createElement('option');
|
option.value = type.label;
|
option.textContent = type.value;
|
messageTypeSelect.appendChild(option);
|
});
|
// 消息类型变化时,重新加载标签
|
messageTypeSelect.addEventListener('change', function () {
|
loadTags(this.value);
|
});
|
// 初始加载标签
|
loadTags('');
|
}
|
})
|
.catch(error => {
|
console.error('加载消息类型失败:', error);
|
});
|
}
|
|
// 加载标签
|
function loadTags(directionType) {
|
let url = '/deviceLog/queryEvent';
|
if (directionType) {
|
url += '?directionType=' + directionType;
|
}
|
fetch(url)
|
.then(response => response.json())
|
.then(data => {
|
if (data && data.code === 200 && data.data) {
|
const tagSelect = document.getElementById('tag');
|
tagSelect.innerHTML = '<option value="">全部</option>';
|
data.data.forEach(tag => {
|
const option = document.createElement('option');
|
option.value = tag.value;
|
option.textContent = tag.label;
|
tagSelect.appendChild(option);
|
});
|
}
|
})
|
.catch(error => {
|
console.error('加载标签失败:', error);
|
});
|
}
|
|
// 解析十六进制数据
|
function parseHexData(hexData) {
|
console.log('开始解析,hexData:', hexData);
|
|
// 先关闭之前的加载状态和结果弹窗
|
const oldLoading = document.getElementById('parseLoading');
|
if (oldLoading) {
|
oldLoading.remove();
|
}
|
|
const oldResult = document.querySelector('[id^="resultDiv"]');
|
if (oldResult) {
|
oldResult.remove();
|
}
|
|
// 显示加载状态
|
const loadingDiv = document.createElement('div');
|
loadingDiv.className = 'loading';
|
loadingDiv.textContent = '解析中...';
|
loadingDiv.style.position = 'fixed';
|
loadingDiv.style.top = '50%';
|
loadingDiv.style.left = '50%';
|
loadingDiv.style.transform = 'translate(-50%, -50%)';
|
loadingDiv.style.backgroundColor = 'rgba(255, 255, 255, 0.9)';
|
loadingDiv.style.padding = '20px';
|
loadingDiv.style.borderRadius = '8px';
|
loadingDiv.style.boxShadow = '0 2px 10px rgba(0,0,0,0.1)';
|
loadingDiv.id = 'parseLoading';
|
document.body.appendChild(loadingDiv);
|
|
// 构建请求URL
|
const url = `/proxy/decode?hexData=${encodeURIComponent(hexData)}`;
|
console.log('请求URL:', url);
|
|
// 调用解析接口
|
fetch(url, {
|
method: 'GET',
|
headers: {
|
'Content-Type': 'application/json',
|
'Accept': 'application/json'
|
},
|
mode: 'cors' // 允许跨域请求
|
})
|
.then(response => {
|
console.log('响应状态:', response.status);
|
console.log('响应头:', response.headers);
|
if (!response.ok) {
|
throw new Error(`解析失败,状态码: ${response.status}`);
|
}
|
return response.json();
|
})
|
.then(data => {
|
console.log('解析结果:', data);
|
// 移除加载状态
|
document.getElementById('parseLoading').remove();
|
|
// 显示解析结果
|
const resultDiv = document.createElement('div');
|
resultDiv.id = 'resultDiv_' + Date.now(); // 添加唯一ID
|
resultDiv.style.position = 'fixed';
|
resultDiv.style.top = '50%';
|
resultDiv.style.left = '50%';
|
resultDiv.style.transform = 'translate(-50%, -50%)';
|
resultDiv.style.backgroundColor = 'white';
|
resultDiv.style.padding = '20px';
|
resultDiv.style.borderRadius = '8px';
|
resultDiv.style.boxShadow = '0 2px 20px rgba(0,0,0,0.2)';
|
resultDiv.style.maxWidth = '80%';
|
resultDiv.style.maxHeight = '80%';
|
resultDiv.style.overflow = 'auto';
|
resultDiv.style.zIndex = '1000';
|
|
// 构建结果HTML
|
let resultHTML = '<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">';
|
resultHTML += '<h3 style="margin: 0;">解析结果</h3>';
|
resultHTML += '<button id="closeResult" style="padding: 5px 10px; background-color: #f0f0f0; color: #333; border: none; border-radius: 4px; cursor: pointer;">×</button>';
|
resultHTML += '</div>';
|
resultHTML += '<pre style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace; margin: 0;">';
|
resultHTML += JSON.stringify(data, null, 2);
|
resultHTML += '</pre>';
|
|
resultDiv.innerHTML = resultHTML;
|
document.body.appendChild(resultDiv);
|
|
// 关闭按钮点击事件
|
document.getElementById('closeResult').addEventListener('click', function() {
|
resultDiv.remove();
|
});
|
})
|
.catch(error => {
|
console.error('解析错误:', error);
|
// 移除加载状态
|
document.getElementById('parseLoading').remove();
|
|
// 显示详细的错误信息
|
let errorMessage = '解析失败: ' + error.message;
|
if (error.message.includes('Failed to fetch')) {
|
errorMessage += '\n可能的原因:\n1. 解析服务未启动\n2. 跨域问题\n3. 网络连接问题';
|
}
|
alert(errorMessage);
|
});
|
}
|
|
// 页面加载时检查登录状态并加载数据
|
checkLogin();
|
loadFilterOptions();
|
loadLogData();
|
</script>
|
</body>
|
</html>
|