initial
This commit is contained in:
21
src/DRS9.Dashboard.Server/Components/App.razor
Normal file
21
src/DRS9.Dashboard.Server/Components/App.razor
Normal 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>
|
||||
63
src/DRS9.Dashboard.Server/Components/Layout/MainLayout.razor
Normal file
63
src/DRS9.Dashboard.Server/Components/Layout/MainLayout.razor
Normal 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>
|
||||
249
src/DRS9.Dashboard.Server/Components/Pages/Applications.razor
Normal file
249
src/DRS9.Dashboard.Server/Components/Pages/Applications.razor
Normal 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"
|
||||
};
|
||||
}
|
||||
234
src/DRS9.Dashboard.Server/Components/Pages/Batch.razor
Normal file
234
src/DRS9.Dashboard.Server/Components/Pages/Batch.razor
Normal 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();
|
||||
}
|
||||
}
|
||||
512
src/DRS9.Dashboard.Server/Components/Pages/Devices.razor
Normal file
512
src/DRS9.Dashboard.Server/Components/Pages/Devices.razor
Normal 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", "删除失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
167
src/DRS9.Dashboard.Server/Components/Pages/Guest.razor
Normal file
167
src/DRS9.Dashboard.Server/Components/Pages/Guest.razor
Normal 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", "链接已复制到剪贴板");
|
||||
}
|
||||
}
|
||||
113
src/DRS9.Dashboard.Server/Components/Pages/Home.razor
Normal file
113
src/DRS9.Dashboard.Server/Components/Pages/Home.razor
Normal 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();
|
||||
}
|
||||
}
|
||||
161
src/DRS9.Dashboard.Server/Components/Pages/Versions.razor
Normal file
161
src/DRS9.Dashboard.Server/Components/Pages/Versions.razor
Normal 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"
|
||||
};
|
||||
}
|
||||
12
src/DRS9.Dashboard.Server/Components/Routes.razor
Normal file
12
src/DRS9.Dashboard.Server/Components/Routes.razor
Normal 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>
|
||||
10
src/DRS9.Dashboard.Server/Components/_Imports.razor
Normal file
10
src/DRS9.Dashboard.Server/Components/_Imports.razor
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 = "删除成功" });
|
||||
}
|
||||
}
|
||||
128
src/DRS9.Dashboard.Server/Controllers/DevicesController.cs
Normal file
128
src/DRS9.Dashboard.Server/Controllers/DevicesController.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
110
src/DRS9.Dashboard.Server/Controllers/PlaylistsController.cs
Normal file
110
src/DRS9.Dashboard.Server/Controllers/PlaylistsController.cs
Normal 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; }
|
||||
// }
|
||||
29
src/DRS9.Dashboard.Server/DRS9.Dashboard.Server.csproj
Normal file
29
src/DRS9.Dashboard.Server/DRS9.Dashboard.Server.csproj
Normal 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>
|
||||
6
src/DRS9.Dashboard.Server/DRS9.Dashboard.Server.http
Normal file
6
src/DRS9.Dashboard.Server/DRS9.Dashboard.Server.http
Normal file
@@ -0,0 +1,6 @@
|
||||
@DRS9.Dashboard.Server_HostAddress = http://localhost:5264
|
||||
|
||||
GET {{DRS9.Dashboard.Server_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
122
src/DRS9.Dashboard.Server/Middleware/WebSocketMiddleware.cs
Normal file
122
src/DRS9.Dashboard.Server/Middleware/WebSocketMiddleware.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
164
src/DRS9.Dashboard.Server/Program.cs
Normal file
164
src/DRS9.Dashboard.Server/Program.cs
Normal 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();
|
||||
14
src/DRS9.Dashboard.Server/Properties/launchSettings.json
Normal file
14
src/DRS9.Dashboard.Server/Properties/launchSettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
254
src/DRS9.Dashboard.Server/Services/ApiClientService.cs
Normal file
254
src/DRS9.Dashboard.Server/Services/ApiClientService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
8
src/DRS9.Dashboard.Server/appsettings.Development.json
Normal file
8
src/DRS9.Dashboard.Server/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/DRS9.Dashboard.Server/appsettings.json
Normal file
19
src/DRS9.Dashboard.Server/appsettings.json
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
src/DRS9.Dashboard.Server/dashboard.db
Normal file
BIN
src/DRS9.Dashboard.Server/dashboard.db
Normal file
Binary file not shown.
252
src/DRS9.Dashboard.Server/wwwroot/css/app.css
Normal file
252
src/DRS9.Dashboard.Server/wwwroot/css/app.css
Normal 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;
|
||||
}
|
||||
173
src/DRS9.Dashboard.Server/wwwroot/viewer.html
Normal file
173
src/DRS9.Dashboard.Server/wwwroot/viewer.html
Normal 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>
|
||||
Reference in New Issue
Block a user