改进后台

This commit is contained in:
Zhanghu
2026-01-13 15:04:30 +08:00
parent a9efcd55a7
commit 5ace3be63f
7 changed files with 618 additions and 10 deletions

View File

@@ -63,7 +63,39 @@
<span class="badge bg-secondary">禁用</span>
}
</td>
<td>@device.LastSeenAt?.ToString("MM-dd HH:mm") ?? "从未"</td>
<td>
@if (!device.LastSeenAt.HasValue)
{
<span class="text-muted">等待首次连接</span>
}
else
{
var timeDiff = DateTime.UtcNow - device.LastSeenAt.Value;
@if (timeDiff.TotalMinutes < 1)
{
<span class="text-success"><i class="bi bi-circle-fill"></i> 在线</span>
}
else if (timeDiff.TotalMinutes < 60)
{
var mins = (int)timeDiff.TotalMinutes;
<span class="text-warning">@mins 分钟前</span>
}
else if (timeDiff.TotalHours < 24)
{
var hours = (int)timeDiff.TotalHours;
<span class="text-info">@hours 小时前</span>
}
else if (timeDiff.TotalDays < 7)
{
var days = (int)timeDiff.TotalDays;
<span>@days 天前</span>
}
else
{
<span class="text-muted">@device.LastSeenAt.Value:MM-dd HH:mm</span>
}
}
</td>
<td>
<button class="btn btn-sm btn-outline-primary" @onclick="@(() => EditDevice(device))">
<i class="bi bi-pencil"></i>

View File

@@ -161,4 +161,4 @@ app.MapRazorComponents<DRS9.Dashboard.Server.Components.App>()
// WebSocket endpoint
app.Map("/ws", app => app.UseMiddleware<WebSocketMiddleware>());
app.Run();
app.Run("http://localhost:5000");

View File

@@ -5,7 +5,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5264",
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

View File

@@ -0,0 +1,563 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DRS9 设备端测试客户端</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
color: #eee;
}
/* 注册界面 */
#register-screen {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
}
.register-box {
background: rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
text-align: center;
max-width: 400px;
width: 90%;
}
.register-box h1 {
margin-bottom: 30px;
font-size: 28px;
}
.register-box input {
width: 100%;
padding: 15px;
margin-bottom: 20px;
border: none;
border-radius: 10px;
background: rgba(255,255,255,0.1);
color: #fff;
font-size: 16px;
}
.register-box input::placeholder {
color: rgba(255,255,255,0.5);
}
.register-box button {
width: 100%;
padding: 15px;
border: none;
border-radius: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: transform 0.2s;
}
.register-box button:hover {
transform: scale(1.02);
}
/* 内容显示区 */
#content-screen {
display: none;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#content-container {
width: 100%;
height: 100%;
}
#content-frame {
width: 100%;
height: 100%;
border: none;
background: #000;
display: block;
}
/* 状态栏 */
#status-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.8);
color: #0f0;
padding: 8px 15px;
font-size: 12px;
font-family: monospace;
z-index: 9999;
display: flex;
justify-content: space-between;
align-items: center;
}
#status-bar.connected {
background: rgba(0,100,0,0.8);
}
#status-bar.disconnected {
background: rgba(100,0,0,0.8);
}
/* 控制面板 */
#control-panel {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0,0,0,0.8);
padding: 15px;
border-radius: 10px;
z-index: 9999;
}
#control-panel button {
padding: 8px 15px;
margin: 5px;
border: none;
border-radius: 5px;
background: #444;
color: #fff;
cursor: pointer;
}
#control-panel button:hover {
background: #666;
}
/* 加载动画 */
.loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-size: 24px;
}
.loading::after {
content: '...';
animation: dots 1.5s steps(4, end) infinite;
}
@keyframes dots {
0%, 20% { content: '.'; }
40% { content: '..'; }
60%, 100% { content: '...'; }
}
/* 消息提示 */
.message {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.9);
color: #fff;
padding: 30px 50px;
border-radius: 15px;
font-size: 18px;
z-index: 10000;
}
/* 图片/视频全屏 */
#media-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: #000;
position: fixed;
top: 0;
left: 0;
}
#media-container img,
#media-container video {
width: 100%;
height: 100%;
object-fit: contain;
}
</style>
</head>
<body>
<!-- 注册界面 -->
<div id="register-screen">
<div class="register-box">
<h1>🖥️ DRS9 设备端</h1>
<p style="margin-bottom: 20px; color: rgba(255,255,255,0.7);">
请输入设备码以注册此设备
</p>
<input
type="text"
id="deviceCode"
placeholder="设备码 (如: TEST-DEVICE-001)"
autofocus
>
<button onclick="registerDevice()">注册设备</button>
<p id="register-error" style="color: #ff6b6b; margin-top: 15px; display: none;"></p>
</div>
</div>
<!-- 内容显示区 -->
<div id="content-screen">
<div id="status-bar" class="disconnected">
<span id="status-text">未连接</span>
<span id="device-info"></span>
</div>
<div id="content-container"></div>
<div id="control-panel">
<button onclick="loadContent()">🔄 刷新内容</button>
<button onclick="showDeviceInfo()"> 设备信息</button>
<button onclick="logout()">🚪 退出</button>
</div>
</div>
<script>
// 配置
const API_BASE = 'http://localhost:5000/api';
const WS_BASE = 'ws://localhost:5000/ws';
// 状态
let token = localStorage.getItem('drs9_deviceToken');
let deviceId = localStorage.getItem('drs9_deviceId');
let deviceName = localStorage.getItem('drs9_deviceName');
let ws = null;
let heartbeatInterval = null;
// 初始化
window.onload = function() {
if (token) {
showContentScreen();
loadContent();
connectWebSocket();
startHeartbeat();
} else {
showRegisterScreen();
}
// 支持回车键提交
document.getElementById('deviceCode').addEventListener('keypress', function(e) {
if (e.key === 'Enter') registerDevice();
});
};
// 显示注册界面
function showRegisterScreen() {
document.getElementById('register-screen').style.display = 'flex';
document.getElementById('content-screen').style.display = 'none';
document.getElementById('deviceCode').focus();
}
// 显示内容界面
function showContentScreen() {
document.getElementById('register-screen').style.display = 'none';
document.getElementById('content-screen').style.display = 'block';
updateDeviceInfo();
}
// 注册设备
async function registerDevice() {
const deviceCode = document.getElementById('deviceCode').value.trim();
const errorEl = document.getElementById('register-error');
if (!deviceCode) {
errorEl.textContent = '请输入设备码';
errorEl.style.display = 'block';
return;
}
errorEl.style.display = 'none';
console.log('开始注册设备:', deviceCode);
try {
const response = await fetch(`${API_BASE}/devices/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
deviceCode: deviceCode,
deviceType: 'Web',
osVersion: navigator.userAgent.substring(0, 50) // 限制在50字符以内
})
});
console.log('响应状态:', response.status, response.statusText);
const data = await response.json();
console.log('响应数据:', data);
if (data.success) {
token = data.token;
deviceId = data.deviceId;
deviceName = data.deviceName;
localStorage.setItem('drs9_deviceToken', token);
localStorage.setItem('drs9_deviceId', deviceId);
localStorage.setItem('drs9_deviceName', deviceName);
console.log('注册成功:', { deviceId, deviceName });
showContentScreen();
loadContent();
connectWebSocket();
startHeartbeat();
} else {
errorEl.textContent = data.message || '注册失败';
errorEl.style.display = 'block';
}
} catch (error) {
console.error('注册错误:', error);
errorEl.textContent = '网络错误: ' + error.message;
errorEl.style.display = 'block';
}
}
// 获取并显示内容
async function loadContent() {
showLoading('正在加载内容...');
try {
const response = await fetch(`${API_BASE}/devices/content`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
if (data.success) {
if (data.data && data.data.length > 0) {
displayContent(data.data[0]);
} else {
showMessage('暂无分配内容<br><small>请在管理后台为设备分配内容</small>');
}
} else {
showMessage('获取内容失败: ' + (data.message || '未知错误'));
}
} catch (error) {
showMessage('网络错误: ' + error.message);
}
}
// 显示内容
function displayContent(item) {
const container = document.getElementById('content-container');
const type = item.applicationType;
const url = item.contentUrl;
const name = item.applicationName;
console.log('显示内容:', { type, url, name });
switch (type) {
case 'Dashboard':
case 'WebRotator':
container.innerHTML = `<iframe id="content-frame" src="${url}" frameborder="0"></iframe>`;
break;
case 'Image':
container.innerHTML = `
<div id="media-container">
<img src="${url}" alt="${name}">
</div>
`;
break;
case 'Video':
container.innerHTML = `
<div id="media-container">
<video src="${url}" autoplay loop muted playsinline></video>
</div>
`;
break;
default:
container.innerHTML = `<iframe id="content-frame" src="${url}" frameborder="0"></iframe>`;
}
}
// WebSocket连接
function connectWebSocket() {
if (ws) {
ws.close();
}
ws = new WebSocket(`${WS_BASE}?token=${token}`);
ws.onopen = function() {
console.log('✅ WebSocket 已连接');
updateStatus('connected', '已连接');
};
ws.onmessage = function(event) {
try {
const msg = JSON.parse(event.data);
console.log('📨 收到消息:', msg);
switch (msg.type) {
case 'content_refresh':
console.log('🔄 刷新内容');
loadContent();
showNotification('内容已刷新');
break;
case 'app_restart':
console.log('🔃 重启应用');
showNotification('正在重启...');
setTimeout(() => location.reload(), 1000);
break;
default:
console.log('❓ 未知消息类型:', msg.type);
}
} catch (e) {
console.error('解析消息失败:', e);
}
};
ws.onerror = function(error) {
console.error('❌ WebSocket 错误:', error);
};
ws.onclose = function() {
console.log('⚠️ WebSocket 断开5秒后重连...');
updateStatus('disconnected', '连接断开 - 重连中...');
setTimeout(connectWebSocket, 5000);
};
}
// 心跳
function startHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
heartbeatInterval = setInterval(async () => {
try {
const response = await fetch(`${API_BASE}/devices/heartbeat`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
console.log('💓 心跳成功');
}
} catch (error) {
console.error('💔 心跳失败:', error);
}
}, 30000); // 30秒心跳
}
// 更新状态栏
function updateStatus(status, text) {
const statusBar = document.getElementById('status-bar');
const statusText = document.getElementById('status-text');
statusBar.className = status;
statusText.textContent = text;
}
// 更新设备信息
function updateDeviceInfo() {
document.getElementById('device-info').textContent =
`${deviceName || '未知设备'} (ID: ${deviceId || '?'})`;
}
// 显示加载
function showLoading(text) {
document.getElementById('content-container').innerHTML =
`<div class="loading">${text || '加载中'}</div>`;
}
// 显示消息
function showMessage(html) {
document.getElementById('content-container').innerHTML =
`<div class="message">${html}</div>`;
}
// 显示通知
function showNotification(text) {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 60px;
right: 20px;
background: rgba(0,200,0,0.9);
color: #fff;
padding: 15px 25px;
border-radius: 10px;
z-index: 10001;
animation: slideIn 0.3s ease;
`;
notification.textContent = text;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transition = 'opacity 0.3s';
setTimeout(() => notification.remove(), 300);
}, 2000);
}
// 显示设备信息
function showDeviceInfo() {
alert(`设备信息:\n名称: ${deviceName}\nID: ${deviceId}\n状态: ${ws?.connected ? '已连接' : '未连接'}`);
}
// 退出登录
function logout() {
if (confirm('确定要退出吗?')) {
if (ws) ws.close();
if (heartbeatInterval) clearInterval(heartbeatInterval);
localStorage.removeItem('drs9_deviceToken');
localStorage.removeItem('drs9_deviceId');
localStorage.removeItem('drs9_deviceName');
token = null;
deviceId = null;
deviceName = null;
showRegisterScreen();
}
}
// 添加CSS动画
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
`;
document.head.appendChild(style);
</script>
</body>
</html>