This commit is contained in:
Zhanghu
2026-01-13 13:50:27 +08:00
commit a9efcd55a7
72 changed files with 8430 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<link href="/css/app.css" rel="stylesheet" />
<link rel="icon" type="image/png" href="/favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.server.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,63 @@
@inherits LayoutComponentBase
<div class="page">
<main>
<div class="top-row px-4">
<h4 class="mb-0">
<i class="bi bi-display"></i> DRS9 信息发布系统
</h4>
<div class="ms-auto">
<a href="/swagger" target="_blank" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-book"></i> API 文档
</a>
</div>
</div>
<div class="container-fluid">
<div class="row">
<!-- 侧边导航 -->
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse show">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<i class="bi bi-house-door"></i> 仪表盘
</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="devices">
<i class="bi bi-pc-display"></i> 设备管理
</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="applications">
<i class="bi bi-grid"></i> 内容管理
</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="batch">
<i class="bi bi-collection"></i> 批量操作
</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="versions">
<i class="bi bi-arrow-repeat"></i> 版本管理
</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="guest">
<i class="bi bi-link-45deg"></i> 访客链接
</NavLink>
</li>
</ul>
</div>
</nav>
<!-- 主内容区 -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
@Body
</main>
</div>
</div>
</main>
</div>

View File

@@ -0,0 +1,249 @@
@page "/applications"
@rendermode @(new InteractiveServerRenderMode())
@inject Services.ApiClientService ApiClient
@inject IJSRuntime JSRuntime
<PageTitle>内容管理 - DRS9 信息发布系统</PageTitle>
<h3 class="mb-4">内容管理</h3>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-grid"></i> 内容列表</span>
<button class="btn btn-primary btn-sm" @onclick="ShowAddModal">
<i class="bi bi-plus-lg"></i> 添加内容
</button>
</div>
<div class="card-body">
@if (_applications == null)
{
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status"></div>
</div>
}
else if (_applications.Count == 0)
{
<div class="text-center text-muted py-4">
<i class="bi bi-inbox fs-1 d-block mb-3"></i>
<p>暂无内容,点击上方按钮添加</p>
</div>
}
else
{
<div class="row">
@foreach (var app in _applications)
{
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<div class="d-flex align-items-start">
<div class="content-icon @GetAppTypeClass(app.Type)">
<i class="bi @GetAppIcon(app.Type)"></i>
</div>
<div class="flex-grow-1">
<h6 class="mb-1">@app.Name</h6>
<p class="text-muted mb-2 small">@app.Type</p>
<p class="mb-2 small">@app.Description</p>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end gap-2">
<button class="btn btn-sm btn-outline-primary" @onclick="@(() => EditApp(app))">
<i class="bi bi-pencil"></i> 编辑
</button>
<button class="btn btn-sm btn-outline-danger" @onclick="@(() => DeleteApp(app))">
<i class="bi bi-trash"></i> 删除
</button>
</div>
</div>
</div>
}
</div>
}
</div>
</div>
<!-- 添加/编辑 Modal -->
@if (_showModal)
{
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5)" tabindex="-1" @onclick="CloseModal">
<div class="modal-dialog" @onclick:stopPropagation="true">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(_editingApp?.Id == null ? "添加内容" : "编辑内容")</h5>
<button type="button" class="btn-close" @onclick="CloseModal"></button>
</div>
<div class="modal-body">
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger" role="alert">
@_errorMessage
</div>
}
<div class="mb-3">
<label class="form-label">内容名称</label>
<input type="text" class="form-control" @bind="_appName" />
</div>
<div class="mb-3">
<label class="form-label">内容类型</label>
<select class="form-select" @bind="_appType" disabled="@(_editingApp != null)">
<option value="">请选择...</option>
<option value="Dashboard">Dashboard</option>
<option value="WebRotator">WebRotator</option>
<option value="Image">Image</option>
<option value="Video">Video</option>
</select>
@if (_editingApp != null)
{
<small class="text-muted">内容类型创建后不可修改</small>
}
</div>
<div class="mb-3">
<label class="form-label">内容 URL</label>
<input type="url" class="form-control" @bind="_appContentUrl" placeholder="https://..." />
</div>
<div class="mb-3">
<label class="form-label">描述</label>
<textarea class="form-control" @bind="_appDescription" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseModal">取消</button>
<button type="button" class="btn btn-primary" @onclick="SaveApp">保存</button>
</div>
</div>
</div>
</div>
}
@code {
private List<ApplicationDto> _applications = new();
private ApplicationDto? _editingApp;
private string _appName = "";
private string _appType = "";
private string _appContentUrl = "";
private string _appDescription = "";
private bool _showModal = false;
private string _errorMessage = "";
protected override async Task OnInitializedAsync()
{
_applications = await ApiClient.GetApplicationsAsync();
}
private void ShowAddModal()
{
_editingApp = null;
_appName = "";
_appType = "";
_appContentUrl = "";
_appDescription = "";
_errorMessage = "";
_showModal = true;
}
private void EditApp(ApplicationDto app)
{
_editingApp = app;
_appName = app.Name;
_appType = app.Type;
_appContentUrl = app.ContentUrl;
_appDescription = app.Description ?? "";
_errorMessage = "";
_showModal = true;
}
private void CloseModal()
{
_showModal = false;
_editingApp = null;
_errorMessage = "";
}
private async Task SaveApp()
{
// Validate inputs
if (string.IsNullOrWhiteSpace(_appName))
{
_errorMessage = "请输入内容名称";
return;
}
if (string.IsNullOrWhiteSpace(_appType))
{
_errorMessage = "请选择内容类型";
return;
}
if (string.IsNullOrWhiteSpace(_appContentUrl))
{
_errorMessage = "请输入内容 URL";
return;
}
bool success;
if (_editingApp == null)
{
success = await ApiClient.CreateApplicationAsync(new ApplicationCreateRequest
{
Name = _appName,
Type = _appType,
ContentUrl = _appContentUrl,
Description = _appDescription
});
}
else
{
success = await ApiClient.UpdateApplicationAsync(_editingApp.Id, new ApplicationUpdateRequest
{
Name = _appName,
Description = _appDescription,
ContentUrl = _appContentUrl
});
}
if (success)
{
CloseModal();
_applications = await ApiClient.GetApplicationsAsync();
await JSRuntime.InvokeVoidAsync("alert", "保存成功");
}
else
{
_errorMessage = "保存失败,请检查输入数据或稍后重试";
}
}
private async Task DeleteApp(ApplicationDto app)
{
if (await JSRuntime.InvokeAsync<bool>("confirm", $"确定要删除内容 '{app.Name}' 吗?"))
{
var success = await ApiClient.DeleteApplicationAsync(app.Id);
if (success)
{
_applications = await ApiClient.GetApplicationsAsync();
await JSRuntime.InvokeVoidAsync("alert", "删除成功");
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "删除失败");
}
}
}
private string GetAppTypeClass(string type) => type switch
{
"Dashboard" => "dashboard",
"WebRotator" => "web",
"Image" => "image",
"Video" => "video",
_ => ""
};
private string GetAppIcon(string type) => type switch
{
"Dashboard" => "bi-bar-chart",
"WebRotator" => "bi-arrow-repeat",
"Image" => "bi-image",
"Video" => "bi-play-circle",
_ => "bi-file-earmark"
};
}

View File

@@ -0,0 +1,234 @@
@page "/batch"
@rendermode @(new InteractiveServerRenderMode())
@inject Services.ApiClientService ApiClient
@inject IJSRuntime JSRuntime
<PageTitle>批量操作 - DRS9 信息发布系统</PageTitle>
<h3 class="mb-4">批量操作</h3>
<div class="row">
<!-- 批量分配内容 -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-grid"></i> 批量分配内容
</div>
<div class="card-body">
<p class="text-muted">将内容批量分配给多个设备或设备组</p>
<div class="mb-3">
<label class="form-label">选择设备组</label>
<select class="form-select" @bind="@_batchAssignGroupId">
<option value="0">选择设备组...</option>
@if (_groups != null)
{
@foreach (var group in _groups)
{
<option value="@group.Id">@group.Name</option>
}
}
</select>
</div>
<div class="mb-3">
<label class="form-label">选择内容</label>
<div class="border rounded p-2" style="max-height: 150px; overflow-y: auto;">
@if (_applications != null)
{
@foreach (var app in _applications)
{
<div class="form-check">
<input type="checkbox" class="form-check-input"
id="batch-app-@app.Id"
@bind="@_selectedAppIds[app.Id]" />
<label class="form-check-label" for="batch-app-@app.Id">
@app.Name (@app.Type)
</label>
</div>
}
}
</div>
</div>
<button class="btn btn-primary w-100" @onclick="ExecuteBatchAssign">
<i class="bi bi-check-lg"></i> 执行分配
</button>
</div>
</div>
</div>
<!-- 批量推送 -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-send"></i> 批量推送命令
</div>
<div class="card-body">
<p class="text-muted">向多个设备发送控制命令</p>
<div class="mb-3">
<label class="form-label">选择设备</label>
<div class="border rounded p-2" style="max-height: 150px; overflow-y: auto;">
@if (_devices != null)
{
@foreach (var device in _devices)
{
<div class="form-check">
<input type="checkbox" class="form-check-input"
id="batch-device-@device.Id"
@bind="@_selectedDeviceIds[device.Id]" />
<label class="form-check-label" for="batch-device-@device.Id">
@device.DeviceName
</label>
</div>
}
}
</div>
</div>
<div class="mb-3">
<label class="form-label">推送命令</label>
<select class="form-select" @bind="@_batchCommand">
<option value="refresh">刷新内容</option>
<option value="restart">重启应用</option>
<option value="screenshot">获取截图</option>
<option value="clear_cache">清除缓存</option>
</select>
</div>
<button class="btn btn-success w-100" @onclick="ExecuteBatchPush">
<i class="bi bi-send"></i> 发送命令
</button>
</div>
</div>
</div>
<!-- 批量启用/禁用 -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<i class="bi bi-power"></i> 批量启用/禁用
</div>
<div class="card-body">
<p class="text-muted">批量启用或禁用多个设备</p>
<div class="mb-3">
<label class="form-label">选择设备</label>
<div class="border rounded p-2" style="max-height: 150px; overflow-y: auto;">
@if (_devices != null)
{
@foreach (var device in _devices)
{
<div class="form-check">
<input type="checkbox" class="form-check-input"
id="toggle-device-@device.Id"
@bind="@_toggleDeviceIds[device.Id]" />
<label class="form-check-label" for="toggle-device-@device.Id">
@device.DeviceName
</label>
</div>
}
}
</div>
</div>
<div class="btn-group w-100" role="group">
<button class="btn btn-success flex-grow-1" @onclick="@(() => ExecuteBatchToggle(true))">
<i class="bi bi-check-circle"></i> 批量启用
</button>
<button class="btn btn-danger flex-grow-1" @onclick="@(() => ExecuteBatchToggle(false))">
<i class="bi bi-x-circle"></i> 批量禁用
</button>
</div>
</div>
</div>
</div>
</div>
@code {
private List<DeviceDto> _devices = new();
private List<ApplicationDto> _applications = new();
private List<DeviceGroupDto> _groups = new();
private Dictionary<int, bool> _selectedAppIds = new();
private Dictionary<int, bool> _selectedDeviceIds = new();
private Dictionary<int, bool> _toggleDeviceIds = new();
private int _batchAssignGroupId = 0;
private string _batchCommand = "refresh";
protected override async Task OnInitializedAsync()
{
await LoadData();
}
private async Task LoadData()
{
var devicesTask = ApiClient.GetDevicesAsync();
var appsTask = ApiClient.GetApplicationsAsync();
var groupsTask = ApiClient.GetDeviceGroupsAsync();
_devices = await devicesTask;
_applications = await appsTask;
_groups = await groupsTask;
_selectedAppIds = _applications.ToDictionary(a => a.Id, _ => false);
_selectedDeviceIds = _devices.ToDictionary(d => d.Id, _ => false);
_toggleDeviceIds = _devices.ToDictionary(d => d.Id, _ => false);
}
private async Task ExecuteBatchAssign()
{
var selectedApps = _selectedAppIds.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToList();
if (_batchAssignGroupId > 0)
{
var groupDevices = _devices.Where(d => d.DeviceGroupId == _batchAssignGroupId).Select(d => d.Id).ToList();
if (groupDevices.Count == 0 || selectedApps.Count == 0)
{
await JSRuntime.InvokeVoidAsync("alert", "请选择设备和内容");
return;
}
await ApiClient.BatchAssignContentAsync(new BatchAssignContentRequest
{
DeviceIds = groupDevices,
ApplicationIds = selectedApps
});
await JSRuntime.InvokeVoidAsync("alert", "批量分配完成");
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "请选择设备组");
}
}
private async Task ExecuteBatchPush()
{
var selectedDevices = _selectedDeviceIds.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToList();
if (selectedDevices.Count == 0)
{
await JSRuntime.InvokeVoidAsync("alert", "请选择设备");
return;
}
await ApiClient.BatchPushAsync(new BatchPushRequest
{
DeviceIds = selectedDevices,
MessageType = _batchCommand
});
await JSRuntime.InvokeVoidAsync("alert", "命令推送完成");
}
private async Task ExecuteBatchToggle(bool enable)
{
var selectedDevices = _toggleDeviceIds.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToList();
if (selectedDevices.Count == 0)
{
await JSRuntime.InvokeVoidAsync("alert", "请选择设备");
return;
}
await ApiClient.BatchToggleDevicesAsync(new BatchToggleDevicesRequest
{
DeviceIds = selectedDevices,
IsEnabled = enable
});
await JSRuntime.InvokeVoidAsync("alert", $"批量{(enable ? "启用" : "禁用")}完成");
await LoadData();
}
}

View File

@@ -0,0 +1,512 @@
@page "/devices"
@rendermode @(new InteractiveServerRenderMode())
@inject Services.ApiClientService ApiClient
@inject IJSRuntime JSRuntime
<PageTitle>设备管理 - DRS9 信息发布系统</PageTitle>
<h3 class="mb-4">设备管理</h3>
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-pc-display"></i> 设备列表</span>
<button class="btn btn-primary btn-sm" @onclick="ShowAddModal">
<i class="bi bi-plus-lg"></i> 添加设备
</button>
</div>
<div class="card-body">
@if (_devices == null)
{
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status"></div>
</div>
}
else if (_devices.Count == 0)
{
<div class="text-center text-muted py-4">
<i class="bi bi-inbox fs-1 d-block mb-3"></i>
<p>暂无设备,点击上方按钮添加</p>
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>设备名称</th>
<th>设备编码</th>
<th>分组</th>
<th>状态</th>
<th>最后在线</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach (var device in _devices)
{
<tr>
<td>@device.Id</td>
<td>@device.DeviceName</td>
<td><code>@device.DeviceCode</code></td>
<td>@device.DeviceGroupName</td>
<td>
@if (device.IsEnabled)
{
<span class="badge bg-success">启用</span>
}
else
{
<span class="badge bg-secondary">禁用</span>
}
</td>
<td>@device.LastSeenAt?.ToString("MM-dd HH:mm") ?? "从未"</td>
<td>
<button class="btn btn-sm btn-outline-primary" @onclick="@(() => EditDevice(device))">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-info" @onclick="@(() => AssignContent(device))">
<i class="bi bi-grid"></i>
</button>
<button class="btn btn-sm btn-outline-success" @onclick="@(() => PushToDevice(device))">
<i class="bi bi-arrow-repeat"></i>
</button>
<button class="btn btn-sm btn-outline-danger" @onclick="@(() => DeleteDevice(device))">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
</div>
</div>
<!-- 设备分组 -->
<div class="row mt-3">
<div class="col-md-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-folder"></i> 设备分组</span>
<button class="btn btn-primary btn-sm" @onclick="ShowAddGroupModal">
<i class="bi bi-plus-lg"></i> 添加分组
</button>
</div>
<div class="card-body">
@if (_groups == null)
{
<div class="text-center py-3">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
}
else if (_groups.Count == 0)
{
<p class="text-muted text-center mb-0">暂无分组</p>
}
else
{
<div class="d-flex flex-wrap gap-2">
@foreach (var group in _groups)
{
<div class="card" style="min-width: 200px;">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">@group.Name</h6>
<small class="text-muted">@group.Description</small>
</div>
<button class="btn btn-sm btn-outline-danger" @onclick="@(() => DeleteGroup(group))">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
}
</div>
}
</div>
</div>
</div>
</div>
<!-- 添加/编辑设备 Modal -->
@if (_showModal)
{
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5)" tabindex="-1" @onclick="CloseModal">
<div class="modal-dialog" @onclick:stopPropagation="true">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@(_editingDevice?.Id == null ? "添加设备" : "编辑设备")</h5>
<button type="button" class="btn-close" @onclick="CloseModal"></button>
</div>
<div class="modal-body">
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger" role="alert">
@_errorMessage
</div>
}
<form id="deviceForm">
<div class="mb-3">
<label class="form-label">设备名称</label>
<input type="text" class="form-control" @bind="_editDeviceName" required />
</div>
<div class="mb-3">
<label class="form-label">设备编码</label>
<input type="text" class="form-control" @bind="_editDeviceCode" required />
</div>
<div class="mb-3">
<label class="form-label">分组</label>
<select class="form-select" @bind="_editGroupId">
<option value="0">无分组</option>
@if (_groups != null)
{
@foreach (var group in _groups)
{
<option value="@group.Id">@group.Name</option>
}
}
</select>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" @bind="_editIsEnabled" />
<label class="form-check-label">启用设备</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseModal">取消</button>
<button type="button" class="btn btn-primary" @onclick="SaveDevice">保存</button>
</div>
</div>
</div>
</div>
}
<!-- 添加分组 Modal -->
@if (_showGroupModal)
{
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5)" tabindex="-1" @onclick="CloseGroupModal">
<div class="modal-dialog" @onclick:stopPropagation="true">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加分组</h5>
<button type="button" class="btn-close" @onclick="CloseGroupModal"></button>
</div>
<div class="modal-body">
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger" role="alert">
@_errorMessage
</div>
}
<div class="mb-3">
<label class="form-label">分组名称</label>
<input type="text" class="form-control" @bind="_newGroupName" />
</div>
<div class="mb-3">
<label class="form-label">描述</label>
<input type="text" class="form-control" @bind="_newGroupDesc" />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseGroupModal">取消</button>
<button type="button" class="btn btn-primary" @onclick="SaveGroup">保存</button>
</div>
</div>
</div>
</div>
}
<!-- 分配内容 Modal -->
@if (_assigningDevice != null)
{
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5)" tabindex="-1" @onclick="CloseAssignModal">
<div class="modal-dialog modal-lg" @onclick:stopPropagation="true">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">分配内容 - @_assigningDevice.DeviceName</h5>
<button type="button" class="btn-close" @onclick="CloseAssignModal"></button>
</div>
<div class="modal-body">
@if (_applications == null)
{
<div class="text-center py-3">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
}
else if (_applications.Count == 0)
{
<p class="text-muted">暂无可用内容</p>
}
else
{
<div class="row">
@foreach (var app in _applications)
{
<div class="col-md-4 mb-2">
<div class="card">
<div class="card-body">
<div class="form-check">
<input type="checkbox" class="form-check-input"
id="app-@app.Id"
@bind="@_selectedAppIds[app.Id]" />
<label class="form-check-label" for="app-@app.Id">
@app.Name
</label>
<br />
<small class="text-muted">@app.Type</small>
</div>
</div>
</div>
</div>
}
</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseAssignModal">取消</button>
<button type="button" class="btn btn-primary" @onclick="SaveAssignment">保存</button>
</div>
</div>
</div>
</div>
}
@code {
private List<DeviceDto> _devices = new();
private List<DeviceGroupDto> _groups = new();
private List<ApplicationDto> _applications = new();
private DeviceDto? _editingDevice;
private DeviceDto? _assigningDevice;
private Dictionary<int, bool> _selectedAppIds = new();
private string _newGroupName = "";
private string _newGroupDesc = "";
private string _editDeviceName = "";
private string _editDeviceCode = "";
private int _editGroupId = 0;
private bool _editIsEnabled = true;
private bool _showModal = false;
private bool _showGroupModal = false;
private string _errorMessage = "";
private string _successMessage = "";
protected override async Task OnInitializedAsync()
{
await LoadData();
}
private async Task LoadData()
{
var devicesTask = ApiClient.GetDevicesAsync();
var groupsTask = ApiClient.GetDeviceGroupsAsync();
var appsTask = ApiClient.GetApplicationsAsync();
_devices = await devicesTask;
_groups = await groupsTask;
_applications = await appsTask;
}
private void ShowAddModal()
{
_editingDevice = null;
_editDeviceName = "";
_editDeviceCode = "";
_editGroupId = 0;
_editIsEnabled = true;
_errorMessage = "";
_showModal = true;
}
private void EditDevice(DeviceDto device)
{
_editingDevice = device;
_editDeviceName = device.DeviceName;
_editDeviceCode = device.DeviceCode;
_editGroupId = device.DeviceGroupId ?? 0;
_editIsEnabled = device.IsEnabled;
_errorMessage = "";
_showModal = true;
}
private void CloseModal()
{
_showModal = false;
_editingDevice = null;
_errorMessage = "";
}
private async Task SaveDevice()
{
// Validate inputs
if (string.IsNullOrWhiteSpace(_editDeviceName))
{
_errorMessage = "请输入设备名称";
return;
}
if (string.IsNullOrWhiteSpace(_editDeviceCode))
{
_errorMessage = "请输入设备编码";
return;
}
bool success;
if (_editingDevice == null)
{
success = await ApiClient.CreateDeviceAsync(new DeviceCreateRequest
{
DeviceName = _editDeviceName,
DeviceCode = _editDeviceCode,
DeviceGroupId = _editGroupId > 0 ? _editGroupId : null
});
}
else
{
success = await ApiClient.UpdateDeviceAsync(_editingDevice.Id, new DeviceUpdateRequest
{
DeviceName = _editDeviceName,
DeviceGroupId = _editGroupId > 0 ? _editGroupId : null,
IsEnabled = _editIsEnabled
});
}
if (success)
{
CloseModal();
await LoadData();
await JSRuntime.InvokeVoidAsync("alert", "保存成功");
}
else
{
_errorMessage = "保存失败,请检查输入数据或稍后重试";
}
}
private async Task DeleteDevice(DeviceDto device)
{
if (await JSRuntime.InvokeAsync<bool>("confirm", $"确定要删除设备 '{device.DeviceName}' 吗?"))
{
var success = await ApiClient.DeleteDeviceAsync(device.Id);
if (success)
{
await LoadData();
await JSRuntime.InvokeVoidAsync("alert", "删除成功");
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "删除失败");
}
}
}
private async Task PushToDevice(DeviceDto device)
{
var success = await ApiClient.PushToDeviceAsync(device.Id, "refresh");
if (success)
{
await JSRuntime.InvokeVoidAsync("alert", $"已向设备 '{device.DeviceName}' 发送刷新命令");
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "发送命令失败,设备可能不在线");
}
}
private async Task AssignContent(DeviceDto device)
{
_assigningDevice = device;
_selectedAppIds = _applications.ToDictionary(a => a.Id, _ => false);
await LoadAssignedContent(device.Id);
}
private async Task LoadAssignedContent(int deviceId)
{
// TODO: 从 API 获取已分配的内容
}
private void CloseAssignModal()
{
_assigningDevice = null;
_selectedAppIds.Clear();
}
private async Task SaveAssignment()
{
if (_assigningDevice != null)
{
var selectedIds = _selectedAppIds.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToList();
var success = await ApiClient.AssignContentAsync(_assigningDevice.Id, selectedIds);
if (success)
{
CloseAssignModal();
await JSRuntime.InvokeVoidAsync("alert", "内容分配成功");
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "内容分配失败");
}
}
}
private void ShowAddGroupModal()
{
_newGroupName = "";
_newGroupDesc = "";
_errorMessage = "";
_showGroupModal = true;
}
private void CloseGroupModal()
{
_showGroupModal = false;
_errorMessage = "";
}
private async Task SaveGroup()
{
if (string.IsNullOrWhiteSpace(_newGroupName))
{
_errorMessage = "请输入分组名称";
return;
}
var success = await ApiClient.CreateDeviceGroupAsync(new DeviceGroupCreateRequest
{
Name = _newGroupName,
Description = _newGroupDesc
});
if (success)
{
CloseGroupModal();
await LoadData();
await JSRuntime.InvokeVoidAsync("alert", "分组创建成功");
}
else
{
_errorMessage = "创建失败,请检查输入或稍后重试";
}
}
private async Task DeleteGroup(DeviceGroupDto group)
{
if (await JSRuntime.InvokeAsync<bool>("confirm", $"确定要删除分组 '{group.Name}' 吗?"))
{
var success = await ApiClient.DeleteDeviceGroupAsync(group.Id);
if (success)
{
await LoadData();
await JSRuntime.InvokeVoidAsync("alert", "删除成功");
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "删除失败");
}
}
}
}

View File

@@ -0,0 +1,167 @@
@page "/guest"
@rendermode @(new InteractiveServerRenderMode())
@inject Services.ApiClientService ApiClient
@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager
<PageTitle>访客链接 - DRS9 信息发布系统</PageTitle>
<h3 class="mb-4">访客访问链接</h3>
<div class="row">
<div class="col-md-6">
<!-- 创建访客链接 -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-link-45deg"></i> 创建临时访问链接
</div>
<div class="card-body">
<p class="text-muted">生成临时访问链接,允许用户在浏览器中查看设备内容,无需登录。</p>
<div class="mb-3">
<label class="form-label">关联设备</label>
<select class="form-select" @bind="@_selectedDeviceId">
<option value="0">无(通用访客模式)</option>
@if (_devices != null)
{
@foreach (var device in _devices)
{
<option value="@device.Id">@device.DeviceName (@device.DeviceCode)</option>
}
}
</select>
<small class="text-muted">选择设备后,访客将看到该设备分配的内容</small>
</div>
<div class="mb-3">
<label class="form-label">有效时长</label>
<select class="form-select" @bind="@_validityMinutes">
<option value="30">30 分钟</option>
<option value="60" selected>1 小时</option>
<option value="120">2 小时</option>
<option value="360">6 小时</option>
<option value="720">12 小时</option>
<option value="1440">24 小时</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">描述(可选)</label>
<input type="text" class="form-control" @bind="@_description" placeholder="例如:会议展示、临时访问" />
</div>
<button class="btn btn-primary w-100" @onclick="CreateGuestLink">
<i class="bi bi-plus-lg"></i> 生成访客链接
</button>
</div>
</div>
</div>
<div class="col-md-6">
<!-- 生成的链接 -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-list-check"></i> 已生成的链接</span>
<button class="btn btn-sm btn-outline-secondary" @onclick="LoadData">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
</div>
<div class="card-body">
@if (_generatedLinks.Count == 0)
{
<p class="text-muted text-center mb-0">暂无生成的链接</p>
}
else
{
<div class="list-group">
@foreach (var link in _generatedLinks)
{
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div style="flex: 1;">
<h6 class="mb-1">@link.Description</h6>
<p class="mb-1">
<small class="text-muted">
设备: @(link.DeviceId > 0 ? $"设备 {link.DeviceId}" : "通用")
| 过期时间: @link.ExpiresAt?.ToString("MM-dd HH:mm")
</small>
</p>
<div class="input-group input-group-sm mt-2">
<input type="text" class="form-control" readonly value="@link.FullUrl" />
<button class="btn btn-outline-secondary" type="button" @onclick="@(() => CopyLink(link.FullUrl))">
<i class="bi bi-clipboard"></i>
</button>
</div>
</div>
<div class="text-end ms-2">
@if (link.ExpiresAt > DateTime.UtcNow)
{
<span class="badge bg-success">有效</span>
}
else
{
<span class="badge bg-secondary">已过期</span>
}
</div>
</div>
</div>
}
</div>
}
</div>
</div>
</div>
</div>
@code {
private List<DeviceDto> _devices = new();
private List<(string Description, int DeviceId, string FullUrl, DateTime? ExpiresAt)> _generatedLinks = new();
private int _selectedDeviceId = 0;
private int _validityMinutes = 60;
private string _description = "";
protected override async Task OnInitializedAsync()
{
await LoadData();
}
private async Task LoadData()
{
_devices = await ApiClient.GetDevicesAsync();
}
private async Task CreateGuestLink()
{
var result = await ApiClient.CreateGuestAccessAsync(new GuestAccessCreateRequest
{
DeviceId = _selectedDeviceId > 0 ? _selectedDeviceId : null,
ValidityMinutes = _validityMinutes,
Description = string.IsNullOrWhiteSpace(_description) ? null : _description
});
if (result != null && result.Success)
{
var fullUrl = $"{NavigationManager.BaseUri.TrimEnd('/')}{result.AccessUrl}";
_generatedLinks.Insert(0, (
_description ?? "临时访问",
_selectedDeviceId,
fullUrl,
result.ExpiresAt
));
await JSRuntime.InvokeVoidAsync("alert", $"访客链接已生成!\n\n访问地址: {fullUrl}");
_description = "";
}
else
{
await JSRuntime.InvokeVoidAsync("alert", "生成访客链接失败");
}
}
private async Task CopyLink(string url)
{
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", url);
await JSRuntime.InvokeVoidAsync("alert", "链接已复制到剪贴板");
}
}

View File

@@ -0,0 +1,113 @@
@page "/"
@rendermode @(new InteractiveServerRenderMode())
@inject Services.ApiClientService ApiClient
<PageTitle>仪表盘 - DRS9 信息发布系统</PageTitle>
<h3 class="mb-4">系统概览</h3>
@if (_stats == null)
{
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
</div>
}
else
{
<div class="row">
<div class="col-md-4">
<div class="stat-card">
<div class="d-flex justify-content-between align-items-center">
<div>
<h3>@_stats.TotalDevices</h3>
<p>设备总数</p>
</div>
<i class="bi bi-pc-display fs-1 opacity-50"></i>
</div>
</div>
</div>
<div class="col-md-4">
<div class="stat-card success">
<div class="d-flex justify-content-between align-items-center">
<div>
<h3>@_stats.OnlineDevices</h3>
<p>在线设备</p>
</div>
<i class="bi bi-wifi fs-1 opacity-50"></i>
</div>
</div>
</div>
<div class="col-md-4">
<div class="stat-card info">
<div class="d-flex justify-content-between align-items-center">
<div>
<h3>@_stats.TotalApplications</h3>
<p>内容数量</p>
</div>
<i class="bi bi-grid fs-1 opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-lightning-charge"></i> 快速操作</span>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="devices" class="btn btn-outline-primary text-start">
<i class="bi bi-plus-circle"></i> 添加新设备
</a>
<a href="applications" class="btn btn-outline-success text-start">
<i class="bi bi-file-plus"></i> 创建新内容
</a>
<a href="batch" class="btn btn-outline-warning text-start">
<i class="bi bi-collection"></i> 批量分配内容
</a>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-info-circle"></i> 系统信息
</div>
<div class="card-body">
<table class="table table-sm">
<tr>
<th>系统名称</th>
<td>DRS9 信息发布系统</td>
</tr>
<tr>
<th>版本</th>
<td>v1.0.0</td>
</tr>
<tr>
<th>API 文档</th>
<td><a href="/swagger" target="_blank">Swagger UI</a></td>
</tr>
<tr>
<th>WebSocket</th>
<td><code>ws://localhost:5264/ws</code></td>
</tr>
</table>
</div>
</div>
</div>
</div>
}
@code {
private DashboardStatsDto? _stats;
protected override async Task OnInitializedAsync()
{
_stats = await ApiClient.GetStatsAsync();
}
}

View File

@@ -0,0 +1,161 @@
@page "/versions"
@rendermode @(new InteractiveServerRenderMode())
@inject Services.ApiClientService ApiClient
@inject IJSRuntime JSRuntime
<PageTitle>版本管理 - DRS9 信息发布系统</PageTitle>
<h3 class="mb-4">版本管理 (OTA)</h3>
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-arrow-repeat"></i> 版本列表</span>
<button class="btn btn-primary btn-sm" @onclick="ShowAddModal">
<i class="bi bi-plus-lg"></i> 添加版本
</button>
</div>
<div class="card-body">
@if (_versions == null)
{
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status"></div>
</div>
}
else if (_versions.Count == 0)
{
<div class="text-center text-muted py-4">
<i class="bi bi-inbox fs-1 d-block mb-3"></i>
<p>暂无版本记录</p>
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>平台</th>
<th>版本号</th>
<th>版本名称</th>
<th>下载地址</th>
<th>大小</th>
<th>强制更新</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach (var version in _versions)
{
<tr>
<td>
<span class="badge @GetPlatformBadgeClass(version.Platform)">
@version.Platform
</span>
</td>
<td><code>@version.Version</code></td>
<td>@version.VersionName</td>
<td>
<a href="@version.DownloadUrl" target="_blank" class="text-truncate d-inline-block" style="max-width: 200px;">
@version.DownloadUrl
</a>
</td>
<td>@(version.FileSize.HasValue ? $"{version.FileSize / 1024.0 / 1024.0:F2} MB" : "-")</td>
<td>
@if (version.IsForceUpdate)
{
<span class="badge bg-danger">是</span>
}
else
{
<span class="badge bg-secondary">否</span>
}
</td>
<td>
@if (version.PublishedAt.HasValue)
{
<span class="badge bg-success">已发布</span>
}
else
{
<span class="badge bg-warning text-dark">草稿</span>
}
</td>
<td>
@if (version.PublishedAt.HasValue)
{
<button class="btn btn-sm btn-outline-success" @onclick="@(() => PushUpdate(version))" disabled>
<i class="bi bi-check-lg"></i> 已推送
</button>
}
else
{
<button class="btn btn-sm btn-outline-success" @onclick="@(() => PushUpdate(version))">
<i class="bi bi-send"></i> 推送更新
</button>
}
<button class="btn btn-sm btn-outline-danger" @onclick="@(() => DeleteVersion(version))">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
</div>
</div>
@code {
private List<AppVersionDto> _versions = new();
private bool _showModal = false;
protected override async Task OnInitializedAsync()
{
_versions = await ApiClient.GetVersionsAsync();
}
private void ShowAddModal()
{
_showModal = true;
}
private void CloseModal()
{
_showModal = false;
}
private async Task PushUpdate(AppVersionDto version)
{
if (await JSRuntime.InvokeAsync<bool>("confirm", $"确定要向所有设备推送版本 {version.Version} 的更新通知吗?"))
{
await ApiClient.PushUpdateAsync(version.Id);
version.PublishedAt = DateTime.UtcNow;
await JSRuntime.InvokeVoidAsync("alert", "更新通知已推送");
}
}
private async Task DeleteVersion(AppVersionDto version)
{
if (await JSRuntime.InvokeAsync<bool>("confirm", $"确定要删除版本 {version.Version} 吗?"))
{
await ApiClient.DeleteVersionAsync(version.Id);
_versions = await ApiClient.GetVersionsAsync();
}
}
private string GetPlatformBadgeClass(string platform) => platform switch
{
"android" => "bg-success",
"ios" => "bg-primary",
"windows" => "bg-info",
"macos" => "bg-secondary",
"linux" => "bg-warning text-dark",
_ => "bg-secondary"
};
}

View File

@@ -0,0 +1,12 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(DRS9.Dashboard.Server.Components.Layout.MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(DRS9.Dashboard.Server.Components.Layout.MainLayout)">
<p role="alert">抱歉,未找到该页面。</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -0,0 +1,10 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using DRS9.Dashboard.Server.Components
@using DRS9.Dashboard.Application.DTOs
@using DRS9.Dashboard.Server.Services

View File

@@ -0,0 +1,98 @@
using DRS9.Dashboard.Application.DTOs;
using DRS9.Dashboard.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DRS9.Dashboard.Server.Controllers;
[ApiController]
[Route("api/admin/versions")]
// [Authorize] - 暂时禁用认证以便测试
public class AppVersionsController : ControllerBase
{
private readonly AppVersionService _appVersionService;
private readonly ILogger<AppVersionsController> _logger;
public AppVersionsController(AppVersionService appVersionService, ILogger<AppVersionsController> logger)
{
_appVersionService = appVersionService;
_logger = logger;
}
/// <summary>
/// 获取所有版本
/// </summary>
[HttpGet]
public async Task<ActionResult<List<AppVersionDto>>> GetAll()
{
var result = await _appVersionService.GetAllAsync();
return Ok(new { success = true, data = result });
}
/// <summary>
/// 创建新版本
/// </summary>
[HttpPost]
public async Task<ActionResult<object>> Create([FromBody] AppVersionCreateRequest request)
{
var result = await _appVersionService.CreateAsync(request);
if (result == null)
{
return BadRequest(new { success = false, message = "创建失败" });
}
_logger.LogInformation("App version created: {Platform} {Version}", request.Platform, request.Version);
return Ok(new { success = true, data = result, message = "创建成功" });
}
/// <summary>
/// 删除版本
/// </summary>
[HttpDelete("{id}")]
public async Task<ActionResult> Delete(int id)
{
var success = await _appVersionService.DeleteAsync(id);
if (!success)
{
return NotFound(new { success = false, message = "版本不存在" });
}
_logger.LogInformation("App version deleted: {Id}", id);
return Ok(new { success = true, message = "删除成功" });
}
/// <summary>
/// 推送升级通知
/// </summary>
[HttpPost("{id}/push-notification")]
public async Task<ActionResult> PushNotification(int id, [FromQuery] string? platform = null)
{
var success = await _appVersionService.PushUpdateNotificationAsync(id, platform);
if (!success)
{
return NotFound(new { success = false, message = "版本不存在" });
}
return Ok(new { success = true, message = "通知已推送" });
}
}
// 设备端版本检查控制器
[ApiController]
[Route("api/[controller]")]
public class VersionsController : ControllerBase
{
private readonly AppVersionService _appVersionService;
public VersionsController(AppVersionService appVersionService)
{
_appVersionService = appVersionService;
}
/// <summary>
/// 检查版本更新
/// </summary>
[HttpPost("check")]
public async Task<ActionResult<CheckVersionResponse>> CheckVersion([FromBody] CheckVersionRequest request)
{
var result = await _appVersionService.CheckVersionAsync(request.Platform, request.CurrentVersion);
return Ok(result);
}
}

View File

@@ -0,0 +1,86 @@
using DRS9.Dashboard.Application.DTOs;
using DRS9.Dashboard.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DRS9.Dashboard.Server.Controllers;
[ApiController]
[Route("api/admin/[controller]")]
// [Authorize] - 暂时禁用认证以便测试 // TODO: 后续添加管理员角色验证
public class ApplicationsController : ControllerBase
{
private readonly ApplicationService _applicationService;
private readonly ILogger<ApplicationsController> _logger;
public ApplicationsController(ApplicationService applicationService, ILogger<ApplicationsController> logger)
{
_applicationService = applicationService;
_logger = logger;
}
/// <summary>
/// 获取所有应用
/// </summary>
[HttpGet]
public async Task<ActionResult<ApplicationListResponse>> GetAll()
{
var result = await _applicationService.GetAllAsync();
return Ok(result);
}
/// <summary>
/// 获取应用详情
/// </summary>
[HttpGet("{id}")]
public async Task<ActionResult<ApplicationResponse>> GetById(int id)
{
var result = await _applicationService.GetByIdAsync(id);
if (result == null || !result.Success)
{
return NotFound(new { success = false, message = "应用不存在" });
}
return Ok(result);
}
/// <summary>
/// 创建新应用
/// </summary>
[HttpPost]
public async Task<ActionResult<ApplicationResponse>> Create([FromBody] ApplicationCreateRequest request)
{
var result = await _applicationService.CreateAsync(request);
_logger.LogInformation("Application created: {Name}", request.Name);
return CreatedAtAction(nameof(GetById), new { id = result.Data?.Id }, result);
}
/// <summary>
/// 更新应用
/// </summary>
[HttpPut("{id}")]
public async Task<ActionResult<ApplicationResponse>> Update(int id, [FromBody] ApplicationUpdateRequest request)
{
var result = await _applicationService.UpdateAsync(id, request);
if (result == null || !result.Success)
{
return NotFound(new { success = false, message = "应用不存在" });
}
_logger.LogInformation("Application updated: {Id}", id);
return Ok(result);
}
/// <summary>
/// 删除应用
/// </summary>
[HttpDelete("{id}")]
public async Task<ActionResult> Delete(int id)
{
var success = await _applicationService.DeleteAsync(id);
if (!success)
{
return NotFound(new { success = false, message = "应用不存在" });
}
_logger.LogInformation("Application deleted: {Id}", id);
return Ok(new { success = true, message = "删除成功" });
}
}

View File

@@ -0,0 +1,128 @@
using DRS9.Dashboard.Application.DTOs;
using DRS9.Dashboard.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DRS9.Dashboard.Server.Controllers;
[ApiController]
[Route("api/[controller]")]
public class DevicesController : ControllerBase
{
private readonly DeviceService _deviceService;
private readonly DeviceManagementService _deviceManagementService;
private readonly PlaylistService _playlistService;
private readonly ILogger<DevicesController> _logger;
public DevicesController(DeviceService deviceService, DeviceManagementService deviceManagementService, PlaylistService playlistService, ILogger<DevicesController> logger)
{
_deviceService = deviceService;
_deviceManagementService = deviceManagementService;
_playlistService = playlistService;
_logger = logger;
}
/// <summary>
/// 设备注册 - 使用设备码激活设备
/// </summary>
[HttpPost("register")]
public async Task<ActionResult<DeviceRegisterResponse>> Register([FromBody] DeviceRegisterRequest request)
{
_logger.LogInformation("Device registration attempt: {DeviceCode}", request.DeviceCode);
var result = await _deviceService.RegisterAsync(request);
if (!result.Success)
{
_logger.LogWarning("Device registration failed: {DeviceCode} - {Message}", request.DeviceCode, result.Message);
return BadRequest(result);
}
_logger.LogInformation("Device registered successfully: {DeviceCode}", request.DeviceCode);
return Ok(result);
}
/// <summary>
/// 更新设备心跳时间
/// </summary>
[HttpPost("heartbeat")]
[Authorize]
public async Task<IActionResult> Heartbeat()
{
var deviceIdClaim = User.FindFirst("deviceId")?.Value;
if (int.TryParse(deviceIdClaim, out var deviceId))
{
await _deviceService.UpdateLastSeenAsync(deviceId);
return Ok(new { success = true });
}
return Unauthorized();
}
/// <summary>
/// 获取设备信息
/// </summary>
[HttpGet("info")]
[Authorize]
public async Task<IActionResult> GetInfo()
{
var deviceIdClaim = User.FindFirst("deviceId")?.Value;
if (int.TryParse(deviceIdClaim, out var deviceId))
{
var device = await _deviceService.GetDeviceAsync(deviceId);
if (device == null)
{
return NotFound(new { success = false, message = "设备不存在" });
}
return Ok(new
{
success = true,
data = new
{
id = device.Id,
deviceCode = device.DeviceCode,
deviceName = device.DeviceName,
deviceType = device.DeviceType,
isEnabled = device.IsEnabled
}
});
}
return Unauthorized();
}
/// <summary>
/// 获取设备分配的内容
/// </summary>
[HttpGet("content")]
[Authorize]
public async Task<IActionResult> GetContent()
{
var deviceIdClaim = User.FindFirst("deviceId")?.Value;
if (int.TryParse(deviceIdClaim, out var deviceId))
{
var result = await _deviceManagementService.GetDeviceContentAsync(deviceId);
return Ok(result);
}
return Unauthorized();
}
/// <summary>
/// 获取设备播放列表(基于内容编排)
/// </summary>
[HttpGet("playlist")]
[Authorize]
public async Task<IActionResult> GetPlaylist()
{
var deviceIdClaim = User.FindFirst("deviceId")?.Value;
if (int.TryParse(deviceIdClaim, out var deviceId))
{
var result = await _playlistService.GetDevicePlaylistAsync(deviceId);
return Ok(result);
}
return Unauthorized();
}
}

View File

@@ -0,0 +1,372 @@
using DRS9.Dashboard.Application.DTOs;
using DRS9.Dashboard.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DRS9.Dashboard.Server.Controllers;
[ApiController]
[Route("api/admin/devices")]
// [Authorize] - 暂时禁用认证以便测试 // TODO: 后续添加管理员角色验证
public class DevicesManagementController : ControllerBase
{
private readonly DeviceManagementService _deviceManagementService;
private readonly DashboardWebSocketManager _wsManager;
private readonly ILogger<DevicesManagementController> _logger;
public DevicesManagementController(
DeviceManagementService deviceManagementService,
DashboardWebSocketManager wsManager,
ILogger<DevicesManagementController> logger)
{
_deviceManagementService = deviceManagementService;
_wsManager = wsManager;
_logger = logger;
}
// ===== 设备管理 =====
/// <summary>
/// 获取所有设备
/// </summary>
[HttpGet]
public async Task<ActionResult<DeviceListResponse>> GetAll()
{
var result = await _deviceManagementService.GetAllDevicesAsync();
return Ok(result);
}
/// <summary>
/// 获取在线设备
/// </summary>
[HttpGet("online")]
public async Task<ActionResult<DeviceListResponse>> GetOnline()
{
var result = await _deviceManagementService.GetOnlineDevicesAsync();
return Ok(result);
}
/// <summary>
/// 获取设备详情
/// </summary>
[HttpGet("{id}")]
public async Task<ActionResult<DeviceDto>> GetById(int id)
{
var result = await _deviceManagementService.GetDeviceByIdAsync(id);
if (result == null)
{
return NotFound(new { success = false, message = "设备不存在" });
}
return Ok(new { success = true, data = result });
}
/// <summary>
/// 创建设备(生成设备码)
/// </summary>
[HttpPost]
public async Task<ActionResult<object>> Create([FromBody] DeviceCreateRequest request)
{
var result = await _deviceManagementService.CreateDeviceAsync(request);
if (result == null)
{
return BadRequest(new { success = false, message = "设备码已存在" });
}
_logger.LogInformation("Device created: {DeviceCode}", request.DeviceCode);
return Ok(new { success = true, data = result, message = "创建成功" });
}
/// <summary>
/// 更新设备
/// </summary>
[HttpPut("{id}")]
public async Task<ActionResult<object>> Update(int id, [FromBody] DeviceUpdateRequest request)
{
var result = await _deviceManagementService.UpdateDeviceAsync(id, request);
if (result == null)
{
return NotFound(new { success = false, message = "设备不存在" });
}
_logger.LogInformation("Device updated: {Id}", id);
return Ok(new { success = true, data = result, message = "更新成功" });
}
/// <summary>
/// 删除设备
/// </summary>
[HttpDelete("{id}")]
public async Task<ActionResult> Delete(int id)
{
var success = await _deviceManagementService.DeleteDeviceAsync(id);
if (!success)
{
return NotFound(new { success = false, message = "设备不存在" });
}
_logger.LogInformation("Device deleted: {Id}", id);
return Ok(new { success = true, message = "删除成功" });
}
// ===== 设备内容分配 =====
/// <summary>
/// 获取设备分配的内容
/// </summary>
[HttpGet("{id}/content")]
public async Task<ActionResult<DeviceContentResponse>> GetContent(int id)
{
var result = await _deviceManagementService.GetDeviceContentAsync(id);
return Ok(result);
}
/// <summary>
/// 为设备分配内容
/// </summary>
[HttpPost("{id}/content")]
public async Task<ActionResult> AssignContent(int id, [FromBody] DeviceAssignmentRequest request)
{
if (request.DeviceId != id)
{
return BadRequest(new { success = false, message = "设备ID不匹配" });
}
var success = await _deviceManagementService.AssignContentToDeviceAsync(id, request.ApplicationIds);
if (!success)
{
return NotFound(new { success = false, message = "设备不存在" });
}
_logger.LogInformation("Content assigned to device {DeviceId}: {Count} applications", id, request.ApplicationIds.Count);
return Ok(new { success = true, message = "内容分配成功,已通知设备刷新" });
}
// ===== 推送控制 =====
/// <summary>
/// 刷新设备内容
/// </summary>
[HttpPost("{id}/push/refresh")]
public async Task<ActionResult> PushRefresh(int id)
{
var success = await _deviceManagementService.PushMessageAsync(id, "content_refresh", new { message = "请刷新内容" });
if (!success)
{
return NotFound(new { success = false, message = "设备不在线或不存在" });
}
_logger.LogInformation("Push refresh to device: {DeviceId}", id);
return Ok(new { success = true, message = "推送成功" });
}
/// <summary>
/// 重启设备应用
/// </summary>
[HttpPost("{id}/push/restart")]
public async Task<ActionResult> PushRestart(int id)
{
var success = await _deviceManagementService.PushMessageAsync(id, "app_restart", new { message = "请重启应用" });
if (!success)
{
return NotFound(new { success = false, message = "设备不在线或不存在" });
}
_logger.LogInformation("Push restart to device: {DeviceId}", id);
return Ok(new { success = true, message = "推送成功" });
}
/// <summary>
/// 获取 WebSocket 连接统计
/// </summary>
[HttpGet("stats/connections")]
public ActionResult GetConnectionStats()
{
var totalConnections = _wsManager.GetConnectionCount();
return Ok(new
{
success = true,
data = new
{
totalConnections,
timestamp = DateTime.UtcNow
}
});
}
}
// ===== 设备分组控制器 =====
[ApiController]
[Route("api/admin/device-groups")]
// [Authorize] - 暂时禁用认证以便测试
public class DeviceGroupsController : ControllerBase
{
private readonly DeviceManagementService _deviceManagementService;
private readonly ILogger<DeviceGroupsController> _logger;
public DeviceGroupsController(DeviceManagementService deviceManagementService, ILogger<DeviceGroupsController> logger)
{
_deviceManagementService = deviceManagementService;
_logger = logger;
}
/// <summary>
/// 获取所有设备分组
/// </summary>
[HttpGet]
public async Task<ActionResult<DeviceGroupListResponse>> GetAll()
{
var result = await _deviceManagementService.GetAllGroupsAsync();
return Ok(result);
}
/// <summary>
/// 创建设备分组
/// </summary>
[HttpPost]
public async Task<ActionResult<object>> Create([FromBody] DeviceGroupCreateRequest request)
{
var result = await _deviceManagementService.CreateGroupAsync(request);
if (result == null)
{
return BadRequest(new { success = false, message = "创建失败" });
}
_logger.LogInformation("Device group created: {Name}", request.Name);
return Ok(new { success = true, data = result, message = "创建成功" });
}
/// <summary>
/// 更新设备分组
/// </summary>
[HttpPut("{id}")]
public async Task<ActionResult<object>> Update(int id, [FromBody] DeviceGroupUpdateRequest request)
{
var result = await _deviceManagementService.UpdateGroupAsync(id, request);
if (result == null)
{
return NotFound(new { success = false, message = "分组不存在" });
}
_logger.LogInformation("Device group updated: {Id}", id);
return Ok(new { success = true, data = result, message = "更新成功" });
}
/// <summary>
/// 删除设备分组
/// </summary>
[HttpDelete("{id}")]
public async Task<ActionResult> Delete(int id)
{
var success = await _deviceManagementService.DeleteGroupAsync(id);
if (!success)
{
return NotFound(new { success = false, message = "分组不存在" });
}
_logger.LogInformation("Device group deleted: {Id}", id);
return Ok(new { success = true, message = "删除成功" });
}
}
// ===== 批量管理控制器 =====
[ApiController]
[Route("api/admin/batch")]
// [Authorize] - 暂时禁用认证以便测试
public class BatchManagementController : ControllerBase
{
private readonly BatchManagementService _batchService;
private readonly ILogger<BatchManagementController> _logger;
public BatchManagementController(BatchManagementService batchService, ILogger<BatchManagementController> logger)
{
_batchService = batchService;
_logger = logger;
}
/// <summary>
/// 批量为设备分配内容
/// </summary>
[HttpPost("assign-content")]
public async Task<ActionResult<BatchOperationResponse>> BatchAssignContent([FromBody] BatchAssignContentRequest request)
{
var result = await _batchService.BatchAssignContentAsync(request);
_logger.LogInformation("Batch assign content: {Count} devices", request.DeviceIds.Count);
return Ok(result);
}
/// <summary>
/// 批量推送消息到设备
/// </summary>
[HttpPost("push")]
public async Task<ActionResult<BatchOperationResponse>> BatchPush([FromBody] BatchPushRequest request)
{
var result = await _batchService.BatchPushAsync(request);
_logger.LogInformation("Batch push: {Type} to {Count} devices", request.MessageType, request.DeviceIds.Count);
return Ok(result);
}
/// <summary>
/// 批量推送消息到设备分组
/// </summary>
[HttpPost("push-to-groups")]
public async Task<ActionResult<BatchOperationResponse>> BatchPushToGroups([FromBody] BatchPushToGroupRequest request)
{
var result = await _batchService.BatchPushToGroupAsync(request);
_logger.LogInformation("Batch push to groups: {Type} to {Count} groups", request.MessageType, request.DeviceGroupIds.Count);
return Ok(result);
}
/// <summary>
/// 批量启用/禁用设备
/// </summary>
[HttpPost("toggle-devices")]
public async Task<ActionResult<BatchOperationResponse>> BatchToggleDevices([FromBody] BatchToggleDevicesRequest request)
{
var result = await _batchService.BatchToggleDevicesAsync(request);
_logger.LogInformation("Batch toggle devices: {Enabled} for {Count} devices", request.IsEnabled, request.DeviceIds.Count);
return Ok(result);
}
/// <summary>
/// 批量分配播放列表到分组
/// </summary>
[HttpPost("assign-playlist")]
public async Task<ActionResult<BatchOperationResponse>> BatchAssignPlaylist([FromBody] BatchAssignPlaylistRequest request)
{
var result = await _batchService.BatchAssignPlaylistToGroupAsync(request);
_logger.LogInformation("Batch assign playlist: {PlaylistId} to {Count} groups", request.PlaylistId, request.DeviceGroupIds.Count);
return Ok(result);
}
}
// ===== 仪表盘统计控制器 =====
[ApiController]
[Route("api/admin/stats")]
// [Authorize] - 暂时禁用认证以便测试
public class DashboardController : ControllerBase
{
private readonly DeviceManagementService _deviceManagementService;
private readonly ApplicationService _applicationService;
public DashboardController(
DeviceManagementService deviceManagementService,
ApplicationService applicationService)
{
_deviceManagementService = deviceManagementService;
_applicationService = applicationService;
}
/// <summary>
/// 获取仪表盘统计数据
/// </summary>
[HttpGet]
public async Task<ActionResult<DashboardStatsDto>> GetStats()
{
var devices = await _deviceManagementService.GetAllDevicesAsync();
var applications = await _applicationService.GetAllAsync();
var stats = new DashboardStatsDto
{
TotalDevices = devices.Data.Count,
OnlineDevices = devices.Data.Count(d => d.IsEnabled),
TotalApplications = applications.Total
};
return Ok(stats);
}
}

View File

@@ -0,0 +1,71 @@
using DRS9.Dashboard.Application.DTOs;
using DRS9.Dashboard.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DRS9.Dashboard.Server.Controllers;
[ApiController]
[Route("api/admin/[controller]")]
[Authorize]
public class GuestAccessController : ControllerBase
{
private readonly GuestAccessService _guestAccessService;
private readonly ILogger<GuestAccessController> _logger;
public GuestAccessController(GuestAccessService guestAccessService, ILogger<GuestAccessController> logger)
{
_guestAccessService = guestAccessService;
_logger = logger;
}
/// <summary>
/// 创建临时访问链接
/// </summary>
[HttpPost("create")]
public async Task<ActionResult<GuestAccessResponse>> CreateGuestAccess([FromBody] GuestAccessCreateRequest request)
{
var result = await _guestAccessService.CreateGuestTokenAsync(request);
if (!result.Success)
{
return BadRequest(result);
}
_logger.LogInformation("Guest access created: {Description}", request.Description);
return Ok(result);
}
}
// 访客端内容查看控制器
[ApiController]
[Route("api/guest")]
public class GuestViewController : ControllerBase
{
private readonly PlaylistService _playlistService;
private readonly DeviceManagementService _deviceManagementService;
public GuestViewController(PlaylistService playlistService, DeviceManagementService deviceManagementService)
{
_playlistService = playlistService;
_deviceManagementService = deviceManagementService;
}
/// <summary>
/// 访客获取内容(使用设备分配的内容)
/// </summary>
[HttpGet("content/{deviceId}")]
public async Task<IActionResult> GetContent(int deviceId)
{
var result = await _deviceManagementService.GetDeviceContentAsync(deviceId);
return Ok(result);
}
/// <summary>
/// 访客获取播放列表
/// </summary>
[HttpGet("playlist/{deviceId}")]
public async Task<IActionResult> GetPlaylist(int deviceId)
{
var result = await _playlistService.GetDevicePlaylistAsync(deviceId);
return Ok(result);
}
}

View File

@@ -0,0 +1,110 @@
using DRS9.Dashboard.Application.DTOs;
using DRS9.Dashboard.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DRS9.Dashboard.Server.Controllers;
// 播放列表功能已禁用
// [ApiController]
// [Route("api/admin/[controller]")]
// // [Authorize] - 暂时禁用认证以便测试
// public class PlaylistsController : ControllerBase
// {
// private readonly PlaylistService _playlistService;
// private readonly ILogger<PlaylistsController> _logger;
//
// public PlaylistsController(PlaylistService playlistService, ILogger<PlaylistsController> logger)
// {
// _playlistService = playlistService;
// _logger = logger;
// }
//
// /// <summary>
// /// 获取所有播放列表
// /// </summary>
// [HttpGet]
// public async Task<ActionResult<PlaylistListResponse>> GetAll()
// {
// var result = await _playlistService.GetAllAsync();
// return Ok(result);
// }
//
// /// <summary>
// /// 获取播放列表详情
// /// </summary>
// [HttpGet("{id}")]
// public async Task<ActionResult<PlaylistDetailDto>> GetById(int id)
// {
// var result = await _playlistService.GetByIdAsync(id);
// if (result == null)
// {
// return NotFound(new { success = false, message = "播放列表不存在" });
// }
// return Ok(new { success = true, data = result });
// }
//
// /// <summary>
// /// 创建播放列表
// /// </summary>
// [HttpPost]
// public async Task<ActionResult<object>> Create([FromBody] PlaylistCreateRequest request)
// {
// var result = await _playlistService.CreateAsync(request);
// if (result == null)
// {
// return BadRequest(new { success = false, message = "创建失败" });
// }
// _logger.LogInformation("Playlist created: {Name}", request.Name);
// return Ok(new { success = true, data = result, message = "创建成功" });
// }
//
// /// <summary>
// /// 更新播放列表
// /// </summary>
// [HttpPut("{id}")]
// public async Task<ActionResult<object>> Update(int id, [FromBody] PlaylistUpdateRequest request)
// {
// var result = await _playlistService.UpdateAsync(id, request);
// if (result == null)
// {
// return NotFound(new { success = false, message = "播放列表不存在" });
// }
// _logger.LogInformation("Playlist updated: {Id}", id);
// return Ok(new { success = true, data = result, message = "更新成功" });
// }
//
// /// <summary>
// /// 删除播放列表
// /// </summary>
// [HttpDelete("{id}")]
// public async Task<ActionResult> Delete(int id)
// {
// var success = await _playlistService.DeleteAsync(id);
// if (!success)
// {
// return NotFound(new { success = false, message = "播放列表不存在" });
// }
// _logger.LogInformation("Playlist deleted: {Id}", id);
// return Ok(new { success = true, message = "删除成功" });
// }
//
// /// <summary>
// /// 将播放列表分配到设备分组
// /// </summary>
// [HttpPost("{id}/assign")]
// public async Task<ActionResult> AssignToGroup(int id, [FromBody] GroupAssignRequest request)
// {
// var success = await _playlistService.AssignToGroupAsync(id, request.DeviceGroupId);
// if (!success)
// {
// return NotFound(new { success = false, message = "播放列表不存在" });
// }
// return Ok(new { success = true, message = "分配成功" });
// }
// }
//
// public record GroupAssignRequest
// {
// public int? DeviceGroupId { get; set; }
// }

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Folder Include="Components\" />
<Folder Include="Components\Pages\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.*" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.*">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DRS9.Dashboard.Application\DRS9.Dashboard.Application.csproj" />
<ProjectReference Include="..\DRS9.Dashboard.Infrastructure\DRS9.Dashboard.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
@DRS9.Dashboard.Server_HostAddress = http://localhost:5264
GET {{DRS9.Dashboard.Server_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -0,0 +1,122 @@
using System.Net.WebSockets;
using System.Text;
using DRS9.Dashboard.Application.Services;
using System.Text.Json;
namespace DRS9.Dashboard.Server.Middleware;
public class WebSocketMiddleware
{
private readonly RequestDelegate _next;
private readonly DashboardWebSocketManager _wsManager;
private readonly ILogger<WebSocketMiddleware> _logger;
private const int BufferSize = 4096;
public WebSocketMiddleware(RequestDelegate next, DashboardWebSocketManager wsManager, ILogger<WebSocketMiddleware> logger)
{
_next = next;
_wsManager = wsManager;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
await _next(context);
return;
}
var deviceId = context.Request.Query["deviceId"].ToString();
var parsedDeviceId = int.TryParse(deviceId, out var id) ? id : (int?)null;
using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
var connection = _wsManager.AddConnection(webSocket, parsedDeviceId);
_logger.LogInformation("WebSocket connection established: {ConnectionId}", connection.ConnectionId);
try
{
await ReceiveMessagesAsync(connection);
}
catch (Exception ex)
{
_logger.LogError(ex, "WebSocket error for connection: {ConnectionId}", connection.ConnectionId);
}
finally
{
_wsManager.RemoveConnection(connection.ConnectionId);
await webSocket.CloseAsync(
connection.WebSocket.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
connection.WebSocket.CloseStatusDescription,
CancellationToken.None);
}
}
private async Task ReceiveMessagesAsync(WebSocketConnection connection)
{
var buffer = new byte[BufferSize];
var segment = new ArraySegment<byte>(buffer);
while (connection.WebSocket.State == WebSocketState.Open)
{
var result = await connection.WebSocket.ReceiveAsync(segment, CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
_logger.LogInformation("WebSocket close requested: {ConnectionId}", connection.ConnectionId);
break;
}
if (result.MessageType == WebSocketMessageType.Text)
{
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
await ProcessMessageAsync(connection, message);
}
}
}
private async Task ProcessMessageAsync(WebSocketConnection connection, string message)
{
try
{
var jsonDoc = JsonDocument.Parse(message);
var messageType = jsonDoc.RootElement.GetProperty("type").GetString();
_logger.LogDebug("Received message from {ConnectionId}: {Type}", connection.ConnectionId, messageType);
// Handle different message types
switch (messageType)
{
case "ping":
await _wsManager.SendMessageAsync(connection, new WebSocketMessage
{
Type = "pong",
Data = new { timestamp = DateTime.UtcNow }
});
break;
case "heartbeat":
connection.LastActivityAt = DateTime.UtcNow;
await _wsManager.SendMessageAsync(connection, new WebSocketMessage
{
Type = "heartbeat_ack",
Data = new { timestamp = DateTime.UtcNow }
});
break;
default:
_logger.LogWarning("Unknown message type: {Type} from {ConnectionId}", messageType, connection.ConnectionId);
break;
}
}
catch (JsonException ex)
{
_logger.LogWarning(ex, "Invalid JSON message from {ConnectionId}", connection.ConnectionId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing message from {ConnectionId}", connection.ConnectionId);
}
}
}

View File

@@ -0,0 +1,164 @@
using System.Text;
using DRS9.Dashboard.Application.Services;
using DRS9.Dashboard.Infrastructure.Data;
using DRS9.Dashboard.Server.Middleware;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Configure Database
builder.Services.AddDbContext<DashboardDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
// Configure JWT Authentication
var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key not configured");
var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? throw new InvalidOperationException("JWT Issuer not configured");
var jwtAudience = builder.Configuration["Jwt:Audience"] ?? throw new InvalidOperationException("JWT Audience not configured");
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuer,
ValidAudience = jwtAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
});
builder.Services.AddAuthorization();
// Add Blazor Server
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// Configure Blazor Server Circuit
builder.Services.AddServerSideBlazor()
.AddCircuitOptions(options =>
{
options.DetailedErrors = true;
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(3);
});
// Register HttpClient and API Client Service
builder.Services.AddHttpClient<DRS9.Dashboard.Server.Services.ApiClientService>(client =>
{
client.BaseAddress = new Uri("http://localhost:5000");
});
// Configure CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
// Register Application Services
var jwtExpiry = int.TryParse(builder.Configuration["Jwt:ExpiryMinutes"], out var expiry) ? expiry : 43200;
builder.Services.AddSingleton(new JwtTokenService(
jwtKey,
jwtIssuer,
jwtAudience,
jwtExpiry
));
builder.Services.AddScoped<DeviceService>();
builder.Services.AddScoped<ApplicationService>();
builder.Services.AddScoped<DeviceManagementService>();
builder.Services.AddScoped<PlaylistService>();
builder.Services.AddScoped<BatchManagementService>();
builder.Services.AddScoped<AppVersionService>();
builder.Services.AddScoped<GuestAccessService>();
builder.Services.AddSingleton<DashboardWebSocketManager>();
// Configure OpenAPI/Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "DRS9 Dashboard API",
Version = "v1",
Description = "信息发布系统 API"
});
// Add JWT Authentication to Swagger
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
});
var app = builder.Build();
// Initialize Database
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<DashboardDbContext>();
await DataSeeder.SeedAsync(dbContext);
}
// Configure the HTTP request pipeline.
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "DRS9 Dashboard API v1");
});
app.UseCors("AllowAll");
// 静态文件
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
// Add anti-forgery for Blazor Server
app.UseAntiforgery();
app.UseWebSockets();
app.MapControllers();
// Blazor Server
app.MapRazorComponents<DRS9.Dashboard.Server.Components.App>()
.AddInteractiveServerRenderMode();
// WebSocket endpoint
app.Map("/ws", app => app.UseMiddleware<WebSocketMiddleware>());
app.Run();

View File

@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5264",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,254 @@
using DRS9.Dashboard.Application.DTOs;
namespace DRS9.Dashboard.Server.Services;
public class ApiClientService
{
private readonly HttpClient _httpClient;
public ApiClientService(HttpClient httpClient)
{
_httpClient = httpClient;
}
private void AddAuthHeader()
{
// TODO: 实现认证 token 管理
}
// 设备管理 API
public async Task<List<DeviceDto>> GetDevicesAsync()
{
AddAuthHeader();
var response = await _httpClient.GetAsync("/api/admin/devices");
if (!response.IsSuccessStatusCode) return new List<DeviceDto>();
var result = await response.Content.ReadFromJsonAsync<DeviceListResponse>();
return result?.Data ?? new List<DeviceDto>();
}
public async Task<DeviceDto?> GetDeviceAsync(int id)
{
AddAuthHeader();
var response = await _httpClient.GetAsync($"/api/admin/devices/{id}");
if (!response.IsSuccessStatusCode) return null;
var result = await response.Content.ReadFromJsonAsync<DeviceDto>();
return result;
}
public async Task<bool> CreateDeviceAsync(DeviceCreateRequest device)
{
AddAuthHeader();
var response = await _httpClient.PostAsJsonAsync("/api/admin/devices", device);
return response.IsSuccessStatusCode;
}
public async Task<bool> UpdateDeviceAsync(int id, DeviceUpdateRequest device)
{
AddAuthHeader();
var response = await _httpClient.PutAsJsonAsync($"/api/admin/devices/{id}", device);
return response.IsSuccessStatusCode;
}
public async Task<bool> DeleteDeviceAsync(int id)
{
AddAuthHeader();
var response = await _httpClient.DeleteAsync($"/api/admin/devices/{id}");
return response.IsSuccessStatusCode;
}
public async Task<bool> AssignContentAsync(int deviceId, List<int> applicationIds)
{
AddAuthHeader();
var response = await _httpClient.PostAsJsonAsync($"/api/admin/devices/{deviceId}/content",
new DeviceAssignmentRequest { DeviceId = deviceId, ApplicationIds = applicationIds });
return response.IsSuccessStatusCode;
}
public async Task<bool> PushToDeviceAsync(int deviceId, string messageType)
{
AddAuthHeader();
var response = await _httpClient.PostAsJsonAsync($"/api/admin/devices/{deviceId}/push",
new PushMessageRequest { DeviceId = deviceId, MessageType = messageType });
return response.IsSuccessStatusCode;
}
// 设备分组 API
public async Task<List<DeviceGroupDto>> GetDeviceGroupsAsync()
{
AddAuthHeader();
var response = await _httpClient.GetAsync("/api/admin/device-groups");
if (!response.IsSuccessStatusCode) return new List<DeviceGroupDto>();
var result = await response.Content.ReadFromJsonAsync<DeviceGroupListResponse>();
return result?.Data ?? new List<DeviceGroupDto>();
}
public async Task<bool> CreateDeviceGroupAsync(DeviceGroupCreateRequest group)
{
AddAuthHeader();
var response = await _httpClient.PostAsJsonAsync("/api/admin/device-groups", group);
return response.IsSuccessStatusCode;
}
public async Task<bool> DeleteDeviceGroupAsync(int id)
{
AddAuthHeader();
var response = await _httpClient.DeleteAsync($"/api/admin/device-groups/{id}");
return response.IsSuccessStatusCode;
}
// 应用/内容管理 API
public async Task<List<ApplicationDto>> GetApplicationsAsync()
{
AddAuthHeader();
var response = await _httpClient.GetAsync("/api/admin/applications");
if (!response.IsSuccessStatusCode) return new List<ApplicationDto>();
var result = await response.Content.ReadFromJsonAsync<ApplicationListResponse>();
return result?.Data ?? new List<ApplicationDto>();
}
public async Task<ApplicationDto?> GetApplicationAsync(int id)
{
AddAuthHeader();
var response = await _httpClient.GetAsync($"/api/admin/applications/{id}");
if (!response.IsSuccessStatusCode) return null;
return await response.Content.ReadFromJsonAsync<ApplicationDto>();
}
public async Task<bool> CreateApplicationAsync(ApplicationCreateRequest application)
{
AddAuthHeader();
var response = await _httpClient.PostAsJsonAsync("/api/admin/applications", application);
return response.IsSuccessStatusCode;
}
public async Task<bool> UpdateApplicationAsync(int id, ApplicationUpdateRequest application)
{
AddAuthHeader();
var response = await _httpClient.PutAsJsonAsync($"/api/admin/applications/{id}", application);
return response.IsSuccessStatusCode;
}
public async Task<bool> DeleteApplicationAsync(int id)
{
AddAuthHeader();
var response = await _httpClient.DeleteAsync($"/api/admin/applications/{id}");
return response.IsSuccessStatusCode;
}
// 播放列表 API
public async Task<List<PlaylistDto>> GetPlaylistsAsync()
{
AddAuthHeader();
var response = await _httpClient.GetAsync("/api/admin/playlists");
if (!response.IsSuccessStatusCode) return new List<PlaylistDto>();
return await response.Content.ReadFromJsonAsync<List<PlaylistDto>>() ?? new List<PlaylistDto>();
}
public async Task<PlaylistDto?> GetPlaylistAsync(int id)
{
AddAuthHeader();
var response = await _httpClient.GetAsync($"/api/admin/playlists/{id}");
if (!response.IsSuccessStatusCode) return null;
return await response.Content.ReadFromJsonAsync<PlaylistDto>();
}
public async Task<bool> CreatePlaylistAsync(object playlist)
{
AddAuthHeader();
var response = await _httpClient.PostAsJsonAsync("/api/admin/playlists", playlist);
return response.IsSuccessStatusCode;
}
public async Task<bool> UpdatePlaylistAsync(int id, object playlist)
{
AddAuthHeader();
var response = await _httpClient.PutAsJsonAsync($"/api/admin/playlists/{id}", playlist);
return response.IsSuccessStatusCode;
}
public async Task<bool> DeletePlaylistAsync(int id)
{
AddAuthHeader();
var response = await _httpClient.DeleteAsync($"/api/admin/playlists/{id}");
return response.IsSuccessStatusCode;
}
public async Task<bool> AssignPlaylistToGroupAsync(int playlistId, int groupId)
{
AddAuthHeader();
var response = await _httpClient.PostAsJsonAsync($"/api/admin/playlists/{playlistId}/assign",
new { DeviceGroupId = groupId });
return response.IsSuccessStatusCode;
}
// 批量操作 API
public async Task<bool> BatchAssignContentAsync(BatchAssignContentRequest request)
{
AddAuthHeader();
var response = await _httpClient.PostAsJsonAsync("/api/admin/batch/assign-content", request);
return response.IsSuccessStatusCode;
}
public async Task<bool> BatchPushAsync(BatchPushRequest request)
{
AddAuthHeader();
var response = await _httpClient.PostAsJsonAsync("/api/admin/batch/push", request);
return response.IsSuccessStatusCode;
}
public async Task<bool> BatchToggleDevicesAsync(BatchToggleDevicesRequest request)
{
AddAuthHeader();
var response = await _httpClient.PostAsJsonAsync("/api/admin/batch/toggle", request);
return response.IsSuccessStatusCode;
}
// 版本管理 API
public async Task<List<AppVersionDto>> GetVersionsAsync()
{
AddAuthHeader();
var response = await _httpClient.GetAsync("/api/admin/versions");
if (!response.IsSuccessStatusCode) return new List<AppVersionDto>();
return await response.Content.ReadFromJsonAsync<List<AppVersionDto>>() ?? new List<AppVersionDto>();
}
public async Task<bool> CreateVersionAsync(object version)
{
AddAuthHeader();
var response = await _httpClient.PostAsJsonAsync("/api/admin/versions", version);
return response.IsSuccessStatusCode;
}
public async Task<bool> DeleteVersionAsync(int id)
{
AddAuthHeader();
var response = await _httpClient.DeleteAsync($"/api/admin/versions/{id}");
return response.IsSuccessStatusCode;
}
public async Task<bool> PushUpdateAsync(int versionId)
{
AddAuthHeader();
var response = await _httpClient.PostAsJsonAsync($"/api/admin/versions/{versionId}/push", new { });
return response.IsSuccessStatusCode;
}
// 访客链接 API
public async Task<GuestAccessResponse?> CreateGuestAccessAsync(GuestAccessCreateRequest request)
{
AddAuthHeader();
var response = await _httpClient.PostAsJsonAsync("/api/admin/guest-access/create", request);
if (!response.IsSuccessStatusCode) return null;
return await response.Content.ReadFromJsonAsync<GuestAccessResponse>();
}
// 统计 API
public async Task<DashboardStatsDto> GetStatsAsync()
{
AddAuthHeader();
var response = await _httpClient.GetAsync("/api/admin/stats");
if (!response.IsSuccessStatusCode)
return new DashboardStatsDto();
return await response.Content.ReadFromJsonAsync<DashboardStatsDto>() ?? new DashboardStatsDto();
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,19 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Data Source=dashboard.db"
},
"Jwt": {
"Key": "DRS9_DASHBOARD_SECRET_KEY_2026_CHANGE_THIS_IN_PRODUCTION",
"Issuer": "DRS9.Dashboard",
"Audience": "DRS9.Dashboard.Clients",
"ExpiryMinutes": 43200
}
}

Binary file not shown.

View File

@@ -0,0 +1,252 @@
/* DRS9 Dashboard Admin Styles */
.page {
position: relative;
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1;
background-color: #f8f9fa;
}
.top-row {
background-color: #fff;
border-bottom: 1px solid #dee2e6;
padding: 1rem 0;
align-items: center;
}
.sidebar {
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
min-height: calc(100vh - 60px);
}
.sidebar .nav-link {
color: #495057;
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.sidebar .nav-link:hover {
color: #0d6efd;
background-color: #e9ecef;
}
.sidebar .nav-link.active {
color: #0d6efd;
background-color: #e7f1ff;
}
.sidebar .nav-link i {
font-size: 1.1rem;
}
.nav-item {
margin-bottom: 0.25rem;
}
/* Card Styles */
.card {
border: none;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 1rem;
}
.card-header {
background-color: #fff;
border-bottom: 1px solid #dee2e6;
font-weight: 600;
}
/* Status Badges */
.badge {
padding: 0.5em 0.75em;
}
/* Table Styles */
.table {
background-color: #fff;
}
.table thead th {
border-bottom: 2px solid #dee2e6;
font-weight: 600;
}
/* Form Styles */
.form-label {
font-weight: 500;
}
/* Action Buttons */
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
/* Modal Customization */
.modal-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
/* Stats Cards */
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
padding: 1.5rem;
margin-bottom: 1rem;
}
.stat-card.success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.stat-card.warning {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-card.info {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.stat-card h3 {
font-size: 2rem;
margin-bottom: 0;
}
.stat-card p {
margin-bottom: 0;
opacity: 0.9;
}
/* Loading Spinner */
.spinner-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
/* Device Status Indicators */
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 5px;
}
.status-online {
background-color: #28a745;
}
.status-offline {
background-color: #6c757d;
}
.status-error {
background-color: #dc3545;
}
/* Responsive */
@media (max-width: 768px) {
.sidebar {
position: fixed;
top: 60px;
left: -250px;
width: 250px;
height: calc(100vh - 60px);
transition: left 0.3s;
z-index: 1000;
}
.sidebar.show {
left: 0;
}
main {
margin-left: 0 !important;
}
}
/* Content Type Icons */
.content-icon {
width: 48px;
height: 48px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin-right: 1rem;
}
.content-icon.video {
background-color: #e3f2fd;
color: #1976d2;
}
.content-icon.image {
background-color: #f3e5f5;
color: #7b1fa2;
}
.content-icon.web {
background-color: #e8f5e9;
color: #388e3c;
}
.content-icon.dashboard {
background-color: #fff3e0;
color: #f57c00;
}
/* Drag and Drop */
.draggable-item {
cursor: move;
transition: transform 0.2s, box-shadow 0.2s;
}
.draggable-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.draggable-item.dragging {
opacity: 0.5;
}
.drop-zone {
border: 2px dashed #dee2e6;
border-radius: 8px;
padding: 2rem;
text-align: center;
transition: border-color 0.2s, background-color 0.2s;
}
.drop-zone.drag-over {
border-color: #0d6efd;
background-color: #e7f1ff;
}
/* Toast Notifications */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
}

View File

@@ -0,0 +1,173 @@
<!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;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #000;
overflow: hidden;
}
#viewer {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
#viewer iframe {
width: 100%;
height: 100%;
border: none;
}
#viewer img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
#viewer video {
width: 100%;
height: 100%;
object-fit: contain;
}
.loading {
color: #fff;
font-size: 18px;
}
.error {
color: #f44;
text-align: center;
padding: 20px;
}
.info {
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 10px 20px;
border-radius: 5px;
font-size: 14px;
}
</style>
</head>
<body>
<div id="viewer">
<div class="loading">正在加载内容...</div>
</div>
<script>
const API_BASE = '/api/guest';
// 从 URL 获取设备 ID
const urlParams = new URLSearchParams(window.location.search);
const deviceId = urlParams.get('deviceId') || urlParams.get('d');
if (!deviceId) {
showError('请指定设备 ID?deviceId=1 或 ?d=1');
} else {
loadContent(deviceId);
}
async function loadContent(deviceId) {
try {
// 先尝试获取播放列表
const playlistRes = await fetch(`${API_BASE}/playlist/${deviceId}`);
const playlistData = await playlistRes.json();
if (playlistData.success && playlistData.data.length > 0) {
showPlaylist(playlistData.data, playlistData.loopMode);
} else {
// 回退到直接获取内容
const contentRes = await fetch(`${API_BASE}/content/${deviceId}`);
const contentData = await contentRes.json();
if (contentData.success && contentData.data.length > 0) {
showContent(contentData.data);
} else {
showError('暂无内容');
}
}
// 显示设备信息
document.getElementById('viewer').innerHTML += `
<div class="info">设备 ${deviceId}</div>
`;
} catch (err) {
showError('加载失败:' + err.message);
}
}
function showPlaylist(items, loopMode) {
let currentIndex = 0;
const showItem = (index) => {
if (index >= items.length) {
if (loopMode === 'Loop') {
index = 0;
} else {
// Once 模式,结束
return;
}
}
const item = items[index];
const duration = item.duration * 1000;
if (item.applicationType === 'Video' || item.contentUrl.endsWith('.mp4')) {
document.getElementById('viewer').innerHTML = `
<video src="${item.contentUrl}" autoplay muted></video>
`;
const video = document.querySelector('video');
video.onended = () => showItem(index + 1);
} else if (item.applicationType === 'Image' || /\.(jpg|jpeg|png|gif|svg|webp)$/i.test(item.contentUrl)) {
document.getElementById('viewer').innerHTML = `
<img src="${item.contentUrl}" alt="${item.applicationName}">
`;
setTimeout(() => showItem(index + 1), duration);
} else {
// Web 内容
document.getElementById('viewer').innerHTML = `
<iframe src="${item.contentUrl}" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
`;
setTimeout(() => showItem(index + 1), duration);
}
currentIndex = index;
};
showItem(0);
}
function showContent(items) {
showPlaylist(items.map(item => ({
applicationName: item.applicationName,
applicationType: item.applicationType,
contentUrl: item.contentUrl,
duration: item.duration
})), 'Loop');
}
function showError(message) {
document.getElementById('viewer').innerHTML = `
<div class="error">${message}</div>
`;
}
</script>
</body>
</html>