Files
argus-go/src/rear/examples/sse_admin/index.html
T
jimmyandClaude cf3f269b7e feat: 完善EXIF管理和SSE实时通信功能
主要功能:
- 新增完整的EXIF数据管理功能
  * PhotoExif表结构设计和模型定义
  * EXIF数据提取、存储和查询接口
  * 支持相机信息、GPS、拍摄参数等多维度查询

- 实现SSE实时通信系统
  * 支持广播消息和定向发送
  * 客户端连接管理和心跳检测
  * Web管理面板用于监控和测试

- 代码结构重构
  * 模型文件重新组织到tables目录
  * 新增repositories和handlers层
  * 完善依赖注入容器配置

- 示例代码和文档
  * EXIF使用示例和集成指南
  * SSE管理面板和测试工具
  * 更新项目文档和待办事项

技术细节:
- 使用GORM进行数据库操作和迁移
- 基于Gin框架的HTTP API设计
- 支持多种EXIF查询和统计功能
- 实时事件推送和客户端状态管理

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-16 15:07:45 +08:00

561 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE 管理面板</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stat-value {
font-size: 2em;
font-weight: bold;
color: #007bff;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.control-panel {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
input, textarea, select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-right: 10px;
margin-bottom: 5px;
}
button:hover {
background: #0056b3;
}
button.danger {
background: #dc3545;
}
button.danger:hover {
background: #c82333;
}
.events {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.events-header {
padding: 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.event-log {
height: 400px;
overflow-y: auto;
padding: 0;
}
.event-item {
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
font-family: 'Monaco', 'Consolas', monospace;
font-size: 13px;
}
.event-item:last-child {
border-bottom: none;
}
.event-timestamp {
color: #666;
font-size: 12px;
}
.event-type {
color: #007bff;
font-weight: 500;
}
.event-data {
background: #f8f9fa;
padding: 8px;
border-radius: 4px;
margin-top: 5px;
white-space: pre-wrap;
}
.status {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.status.connected {
background: #d4edda;
color: #155724;
}
.status.disconnected {
background: #f8d7da;
color: #721c24;
}
.clients-list {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-top: 20px;
}
.client-item {
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.client-info {
flex: 1;
}
.client-id {
font-family: monospace;
background: #f8f9fa;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>SSE 服务器管理面板</h1>
<p>实时监控和管理 Server-Sent Events 连接</p>
<div class="status connected" id="connectionStatus">连接状态: 断开</div>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-value" id="clientCount">0</div>
<div>活跃连接</div>
</div>
<div class="stat-card">
<div class="stat-value" id="eventCount">0</div>
<div>接收事件数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="uptime">00:00:00</div>
<div>运行时间</div>
</div>
</div>
<div class="controls">
<div class="control-panel">
<h3>广播消息</h3>
<div class="form-group">
<label for="broadcastMessage">消息内容:</label>
<textarea id="broadcastMessage" rows="3" placeholder="输入要广播的消息..."></textarea>
</div>
<div class="form-group">
<label for="eventType">事件类型 (可选):</label>
<input type="text" id="eventType" placeholder="例如: notification, update">
</div>
<button onclick="broadcastMessage()">广播消息</button>
<button onclick="startTestEvents()">开始测试事件</button>
</div>
<div class="control-panel">
<h3>定向发送</h3>
<div class="form-group">
<label for="targetClientId">客户端 ID:</label>
<select id="targetClientId">
<option value="">选择客户端...</option>
</select>
</div>
<div class="form-group">
<label for="targetMessage">消息内容:</label>
<textarea id="targetMessage" rows="2" placeholder="输入消息..."></textarea>
</div>
<button onclick="sendToClient()">发送消息</button>
<button onclick="refreshClients()">刷新客户端列表</button>
</div>
</div>
<div class="events">
<div class="events-header">
<h3>实时事件日志</h3>
<div>
<button onclick="clearEventLog()">清空日志</button>
<button onclick="toggleConnection()" id="toggleBtn">连接 SSE</button>
</div>
</div>
<div class="event-log" id="eventLog">
<div class="event-item">
<div class="event-timestamp">等待连接...</div>
</div>
</div>
</div>
<div class="clients-list">
<div class="events-header">
<h3>客户端列表</h3>
<button onclick="refreshClients()">刷新</button>
</div>
<div id="clientsList">
<div class="client-item">
<div class="client-info">暂无客户端连接</div>
</div>
</div>
</div>
</div>
<script>
let eventSource = null;
let eventCount = 0;
let startTime = Date.now();
let uptimeInterval = null;
function updateUptime() {
const elapsed = Date.now() - startTime;
const hours = Math.floor(elapsed / 3600000).toString().padStart(2, '0');
const minutes = Math.floor((elapsed % 3600000) / 60000).toString().padStart(2, '0');
const seconds = Math.floor((elapsed % 60000) / 1000).toString().padStart(2, '0');
document.getElementById('uptime').textContent = `${hours}:${minutes}:${seconds}`;
}
function connectSSE() {
if (eventSource) {
eventSource.close();
}
eventSource = new EventSource('/api/v1/events');
eventSource.onopen = function(event) {
document.getElementById('connectionStatus').textContent = '连接状态: 已连接';
document.getElementById('connectionStatus').className = 'status connected';
document.getElementById('toggleBtn').textContent = '断开连接';
if (!uptimeInterval) {
startTime = Date.now();
uptimeInterval = setInterval(updateUptime, 1000);
}
addEventToLog('系统', '连接', 'SSE 连接已建立', {});
};
eventSource.onmessage = function(event) {
eventCount++;
document.getElementById('eventCount').textContent = eventCount;
try {
const data = JSON.parse(event.data);
addEventToLog(data.type || 'message', 'data', data.message || event.data, data);
} catch (e) {
addEventToLog('message', 'data', event.data, {});
}
};
eventSource.addEventListener('ping', function(event) {
try {
const data = JSON.parse(event.data);
addEventToLog('ping', 'ping', `服务器心跳: ${data.time}`, data);
} catch (e) {
addEventToLog('ping', 'ping', event.data, {});
}
});
eventSource.addEventListener('business_update', function(event) {
try {
const data = JSON.parse(event.data);
addEventToLog('business_update', 'business', data.message, data);
} catch (e) {
addEventToLog('business_update', 'business', event.data, {});
}
});
eventSource.addEventListener('image_processed', function(event) {
try {
const data = JSON.parse(event.data);
addEventToLog('image_processed', 'image', data.message, data);
} catch (e) {
addEventToLog('image_processed', 'image', event.data, {});
}
});
eventSource.addEventListener('user_activity', function(event) {
try {
const data = JSON.parse(event.data);
addEventToLog('user_activity', 'user', data.message, data);
} catch (e) {
addEventToLog('user_activity', 'user', event.data, {});
}
});
eventSource.onerror = function(event) {
document.getElementById('connectionStatus').textContent = '连接状态: 连接错误';
document.getElementById('connectionStatus').className = 'status disconnected';
document.getElementById('toggleBtn').textContent = '重新连接';
addEventToLog('系统', '错误', 'SSE 连接发生错误', {});
};
}
function disconnectSSE() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
document.getElementById('connectionStatus').textContent = '连接状态: 已断开';
document.getElementById('connectionStatus').className = 'status disconnected';
document.getElementById('toggleBtn').textContent = '连接 SSE';
if (uptimeInterval) {
clearInterval(uptimeInterval);
uptimeInterval = null;
}
addEventToLog('系统', '断开', 'SSE 连接已断开', {});
}
function toggleConnection() {
if (eventSource && eventSource.readyState === EventSource.OPEN) {
disconnectSSE();
} else {
connectSSE();
}
}
function addEventToLog(type, category, message, data) {
const eventLog = document.getElementById('eventLog');
const eventItem = document.createElement('div');
eventItem.className = 'event-item';
const timestamp = new Date().toLocaleTimeString();
eventItem.innerHTML = `
<div class="event-timestamp">[${timestamp}]</div>
<div class="event-type">${type}</div>
<div>${message}</div>
${Object.keys(data).length > 0 ? `<div class="event-data">${JSON.stringify(data, null, 2)}</div>` : ''}
`;
eventLog.insertBefore(eventItem, eventLog.firstChild);
// 限制日志条数
if (eventLog.children.length > 100) {
eventLog.removeChild(eventLog.lastChild);
}
}
function clearEventLog() {
document.getElementById('eventLog').innerHTML = '';
eventCount = 0;
document.getElementById('eventCount').textContent = eventCount;
}
async function broadcastMessage() {
const message = document.getElementById('broadcastMessage').value;
const eventType = document.getElementById('eventType').value;
if (!message.trim()) {
alert('请输入消息内容');
return;
}
try {
const response = await fetch('/api/v1/events/broadcast', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message,
event_type: eventType || undefined
})
});
const result = await response.json();
if (result.success) {
document.getElementById('broadcastMessage').value = '';
document.getElementById('eventType').value = '';
addEventToLog('系统', '广播', `消息已广播到 ${result.data.client_count} 个客户端`, result.data);
} else {
alert('发送失败: ' + result.message);
}
} catch (error) {
alert('发送失败: ' + error.message);
}
}
async function sendToClient() {
const clientId = document.getElementById('targetClientId').value;
const message = document.getElementById('targetMessage').value;
if (!clientId || !message.trim()) {
alert('请选择客户端并输入消息内容');
return;
}
try {
const response = await fetch(`/api/v1/events/send/${clientId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message
})
});
const result = await response.json();
if (result.success) {
document.getElementById('targetMessage').value = '';
addEventToLog('系统', '定向发送', `消息已发送到客户端 ${clientId}`, result.data);
} else {
alert('发送失败: ' + result.message);
}
} catch (error) {
alert('发送失败: ' + error.message);
}
}
async function startTestEvents() {
try {
const response = await fetch('/api/v1/events/test?type=admin_test&interval=3&count=5', {
method: 'POST'
});
const result = await response.json();
if (result.success) {
addEventToLog('系统', '测试', '测试事件序列已启动', result.data);
} else {
alert('启动失败: ' + result.message);
}
} catch (error) {
alert('启动失败: ' + error.message);
}
}
async function refreshClients() {
try {
const response = await fetch('/api/v1/events/clients');
const result = await response.json();
if (result.success) {
document.getElementById('clientCount').textContent = result.data.total_clients;
// 更新客户端下拉列表
const select = document.getElementById('targetClientId');
select.innerHTML = '<option value="">选择客户端...</option>';
// 更新客户端列表
const clientsList = document.getElementById('clientsList');
if (result.data.clients.length === 0) {
clientsList.innerHTML = '<div class="client-item"><div class="client-info">暂无客户端连接</div></div>';
} else {
clientsList.innerHTML = result.data.clients.map(client => {
const option = document.createElement('option');
option.value = client.id;
option.textContent = `${client.id.substring(0, 8)}... (${client.remote_addr})`;
select.appendChild(option);
const connectedTime = new Date(client.connected_at).toLocaleString();
const lastPing = new Date(client.last_ping).toLocaleString();
return `
<div class="client-item">
<div class="client-info">
<div><strong>客户端 ID:</strong> <span class="client-id">${client.id}</span></div>
<div><strong>IP 地址:</strong> ${client.remote_addr}</div>
<div><strong>连接时间:</strong> ${connectedTime}</div>
<div><strong>最后心跳:</strong> ${lastPing}</div>
</div>
<button class="danger" onclick="disconnectClient('${client.id}')">断开</button>
</div>
`;
}).join('');
}
}
} catch (error) {
console.error('刷新客户端列表失败:', error);
}
}
async function disconnectClient(clientId) {
if (!confirm('确定要断开此客户端连接吗?')) {
return;
}
try {
const response = await fetch(`/api/v1/events/clients/${clientId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
addEventToLog('系统', '断开客户端', `客户端 ${clientId} 已断开`, result.data);
refreshClients();
} else {
alert('断开失败: ' + result.message);
}
} catch (error) {
alert('断开失败: ' + error.message);
}
}
// 页面加载时自动连接并刷新客户端列表
window.addEventListener('load', function() {
connectSSE();
refreshClients();
setInterval(refreshClients, 10000); // 每10秒刷新一次客户端列表
});
// 页面卸载时断开连接
window.addEventListener('beforeunload', function() {
disconnectSSE();
});
</script>
</body>
</html>