改进后台
This commit is contained in:
@@ -7,9 +7,8 @@ public record DeviceRegisterRequest
|
||||
[Required]
|
||||
public string DeviceCode { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(200)]
|
||||
public string DeviceName { get; set; } = string.Empty;
|
||||
public string? DeviceName { get; set; }
|
||||
|
||||
[MaxLength(50)]
|
||||
public string? DeviceType { get; set; }
|
||||
@@ -27,6 +26,7 @@ public record DeviceRegisterResponse
|
||||
public string? Token { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public int DeviceId { get; set; }
|
||||
public string? DeviceName { get; set; }
|
||||
}
|
||||
|
||||
public record DeviceRefreshTokenRequest
|
||||
|
||||
@@ -40,11 +40,23 @@ public class DeviceService
|
||||
};
|
||||
}
|
||||
|
||||
// 更新设备信息
|
||||
device.DeviceName = request.DeviceName;
|
||||
device.DeviceType = request.DeviceType;
|
||||
device.OsVersion = request.OsVersion;
|
||||
device.AppVersion = request.AppVersion;
|
||||
// 更新设备信息 (如果提供了则使用,否则保留原值)
|
||||
if (!string.IsNullOrWhiteSpace(request.DeviceName))
|
||||
{
|
||||
device.DeviceName = request.DeviceName;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(request.DeviceType))
|
||||
{
|
||||
device.DeviceType = request.DeviceType;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(request.OsVersion))
|
||||
{
|
||||
device.OsVersion = request.OsVersion;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(request.AppVersion))
|
||||
{
|
||||
device.AppVersion = request.AppVersion;
|
||||
}
|
||||
device.IsActive = true;
|
||||
device.ActivatedAt = device.ActivatedAt ?? DateTime.UtcNow;
|
||||
device.LastSeenAt = DateTime.UtcNow;
|
||||
@@ -60,6 +72,7 @@ public class DeviceService
|
||||
Success = true,
|
||||
Token = token,
|
||||
DeviceId = device.Id,
|
||||
DeviceName = device.DeviceName,
|
||||
Message = "注册成功"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5264",
|
||||
"applicationUrl": "http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
||||
Binary file not shown.
563
src/DRS9.Dashboard.Server/wwwroot/device-client.html
Normal file
563
src/DRS9.Dashboard.Server/wwwroot/device-client.html
Normal 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>
|
||||
Reference in New Issue
Block a user