轮播应用OK
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NavLink class="nav-link" href="applications">
|
||||
<i class="bi bi-grid"></i> 内容管理
|
||||
<i class="bi bi-grid"></i> 应用管理
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
@page "/applications"
|
||||
@rendermode @(new InteractiveServerRenderMode())
|
||||
@using DRS9.Dashboard.Application.DTOs
|
||||
@inject Services.ApiClientService ApiClient
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<PageTitle>内容管理 - DRS9 信息发布系统</PageTitle>
|
||||
<PageTitle>应用管理 - DRS9 信息发布系统</PageTitle>
|
||||
|
||||
<h3 class="mb-4">内容管理</h3>
|
||||
<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>
|
||||
<span><i class="bi bi-grid"></i> 应用列表</span>
|
||||
<button class="btn btn-primary btn-sm" @onclick="ShowAddModal">
|
||||
<i class="bi bi-plus-lg"></i> 添加内容
|
||||
<i class="bi bi-plus-lg"></i> 添加应用
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -25,7 +26,7 @@
|
||||
{
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="bi bi-inbox fs-1 d-block mb-3"></i>
|
||||
<p>暂无内容,点击上方按钮添加</p>
|
||||
<p>暂无应用,点击上方按钮添加</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@@ -42,12 +43,24 @@
|
||||
</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="text-muted mb-2 small">@GetAppTypeDisplay(app.Type)</p>
|
||||
<p class="mb-2 small">@app.Description</p>
|
||||
@if (app.Type == "Rotator")
|
||||
{
|
||||
<span class="badge bg-info">
|
||||
<i class="bi bi-list-ul"></i> @(_rotatorItemsCount.TryGetValue(app.Id, out var count) ? count : 0) 个轮播项
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end gap-2">
|
||||
@if (app.Type == "Rotator")
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-info" @onclick="@(() => EditRotatorItems(app))">
|
||||
<i class="bi bi-list-ul"></i> 轮播项
|
||||
</button>
|
||||
}
|
||||
<button class="btn btn-sm btn-outline-primary" @onclick="@(() => EditApp(app))">
|
||||
<i class="bi bi-pencil"></i> 编辑
|
||||
</button>
|
||||
@@ -63,14 +76,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑 Modal -->
|
||||
<!-- 添加/编辑应用 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>
|
||||
<h5 class="modal-title">@(_editingApp?.Id == null ? "添加应用" : "编辑应用")</h5>
|
||||
<button type="button" class="btn-close" @onclick="CloseModal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -81,27 +94,28 @@
|
||||
</div>
|
||||
}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">内容名称</label>
|
||||
<label class="form-label">应用名称</label>
|
||||
<input type="text" class="form-control" @bind="_appName" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">内容类型</label>
|
||||
<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>
|
||||
<option value="Dashboard">网页链接</option>
|
||||
<option value="Rotator">轮播</option>
|
||||
</select>
|
||||
@if (_editingApp != null)
|
||||
{
|
||||
<small class="text-muted">内容类型创建后不可修改</small>
|
||||
<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>
|
||||
@if (_appType == "Dashboard" || (_editingApp != null && _editingApp.Type == "Dashboard"))
|
||||
{
|
||||
<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>
|
||||
@@ -116,6 +130,120 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- 编辑轮播项 Modal -->
|
||||
@if (_showRotatorModal)
|
||||
{
|
||||
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5)" tabindex="-1" @onclick="CloseRotatorModal">
|
||||
<div class="modal-dialog modal-lg" @onclick:stopPropagation="true">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">编辑轮播项 - @_editingRotatorApp?.Name</h5>
|
||||
<button type="button" class="btn-close" @onclick="CloseRotatorModal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- 添加新轮播项 -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-plus-circle"></i> 添加轮播项
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (!string.IsNullOrEmpty(_rotatorError))
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">
|
||||
@_rotatorError
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">类型</label>
|
||||
<select class="form-select" @bind="_newRotatorItemType">
|
||||
<option value="Webpage">网页链接</option>
|
||||
<option value="Image">图片</option>
|
||||
<option value="Video">视频</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">URL</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" @bind="_newRotatorUrl" placeholder="输入链接或上传文件">
|
||||
@if (_newRotatorItemType == "Image" || _newRotatorItemType == "Video")
|
||||
{
|
||||
<button class="btn btn-outline-secondary" @onclick="TriggerFileUpload">
|
||||
<i class="bi bi-upload"></i> 上传
|
||||
</button>
|
||||
<input type="file" class="d-none" @ref="_fileInput" onchange="@OnFileSelected">
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">时长(秒)</label>
|
||||
<input type="number" class="form-control" @bind="_newRotatorDuration" min="1" max="3600">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label"> </label>
|
||||
<button class="btn btn-primary w-100" @onclick="AddRotatorItem">
|
||||
<i class="bi bi-plus"></i> 添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 轮播项列表 -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-list-ul"></i> 轮播项列表 (@_rotatorItems.Count)</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (_rotatorItems == null || _rotatorItems.Count == 0)
|
||||
{
|
||||
<p class="text-muted text-center mb-0">暂无轮播项</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="list-group">
|
||||
@for (int i = 0; i < _rotatorItems.Count; i++)
|
||||
{
|
||||
var item = _rotatorItems[i];
|
||||
var isFirst = (i == 0);
|
||||
var isLast = (i == _rotatorItems.Count - 1);
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi @GetRotatorItemIcon(item.ItemType) me-2"></i>
|
||||
<div>
|
||||
<div class="fw-bold">@item.ItemType</div>
|
||||
<small class="text-muted">@item.Url</small>
|
||||
<div>
|
||||
<span class="badge bg-secondary">@item.Duration 秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="@(() => MoveRotatorItemUp(item, i))" disabled="@isFirst">
|
||||
<i class="bi bi-arrow-up"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="@(() => MoveRotatorItemDown(item, i))" disabled="@isLast">
|
||||
<i class="bi bi-arrow-down"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="@(() => DeleteRotatorItem(item))">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @onclick="CloseRotatorModal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<ApplicationDto> _applications = new();
|
||||
private ApplicationDto? _editingApp;
|
||||
@@ -126,9 +254,39 @@
|
||||
private bool _showModal = false;
|
||||
private string _errorMessage = "";
|
||||
|
||||
// 轮播项相关
|
||||
private ApplicationDto? _editingRotatorApp;
|
||||
private bool _showRotatorModal = false;
|
||||
private List<RotatorItemDto> _rotatorItems = new();
|
||||
private Dictionary<int, int> _rotatorItemsCount = new();
|
||||
private string _newRotatorItemType = "Webpage";
|
||||
private string _newRotatorUrl = "";
|
||||
private int _newRotatorDuration = 10;
|
||||
private string _rotatorError = "";
|
||||
private ElementReference _fileInput;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadApplications();
|
||||
}
|
||||
|
||||
private async Task LoadApplications()
|
||||
{
|
||||
_applications = await ApiClient.GetApplicationsAsync();
|
||||
// 加载每个轮播应用的项数量
|
||||
_rotatorItemsCount.Clear();
|
||||
foreach (var app in _applications.Where(a => a.Type == "Rotator"))
|
||||
{
|
||||
try
|
||||
{
|
||||
var items = await ApiClient.GetRotatorItemsAsync(app.Id);
|
||||
_rotatorItemsCount[app.Id] = items.Count;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_rotatorItemsCount[app.Id] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowAddModal()
|
||||
@@ -162,20 +320,19 @@
|
||||
|
||||
private async Task SaveApp()
|
||||
{
|
||||
// Validate inputs
|
||||
if (string.IsNullOrWhiteSpace(_appName))
|
||||
{
|
||||
_errorMessage = "请输入内容名称";
|
||||
_errorMessage = "请输入应用名称";
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(_appType))
|
||||
{
|
||||
_errorMessage = "请选择内容类型";
|
||||
_errorMessage = "请选择应用类型";
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(_appContentUrl))
|
||||
if (_appType == "Dashboard" && string.IsNullOrWhiteSpace(_appContentUrl))
|
||||
{
|
||||
_errorMessage = "请输入内容 URL";
|
||||
_errorMessage = "请输入网页链接 URL";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -203,7 +360,7 @@
|
||||
if (success)
|
||||
{
|
||||
CloseModal();
|
||||
_applications = await ApiClient.GetApplicationsAsync();
|
||||
await LoadApplications();
|
||||
await JSRuntime.InvokeVoidAsync("alert", "保存成功");
|
||||
}
|
||||
else
|
||||
@@ -214,12 +371,12 @@
|
||||
|
||||
private async Task DeleteApp(ApplicationDto app)
|
||||
{
|
||||
if (await JSRuntime.InvokeAsync<bool>("confirm", $"确定要删除内容 '{app.Name}' 吗?"))
|
||||
if (await JSRuntime.InvokeAsync<bool>("confirm", $"确定要删除应用 '{app.Name}' 吗?"))
|
||||
{
|
||||
var success = await ApiClient.DeleteApplicationAsync(app.Id);
|
||||
if (success)
|
||||
{
|
||||
_applications = await ApiClient.GetApplicationsAsync();
|
||||
await LoadApplications();
|
||||
await JSRuntime.InvokeVoidAsync("alert", "删除成功");
|
||||
}
|
||||
else
|
||||
@@ -229,21 +386,193 @@
|
||||
}
|
||||
}
|
||||
|
||||
private string GetAppTypeDisplay(string type) => type switch
|
||||
{
|
||||
"Dashboard" => "网页链接",
|
||||
"Rotator" => "轮播",
|
||||
_ => type
|
||||
};
|
||||
|
||||
private string GetAppTypeClass(string type) => type switch
|
||||
{
|
||||
"Dashboard" => "dashboard",
|
||||
"WebRotator" => "web",
|
||||
"Image" => "image",
|
||||
"Video" => "video",
|
||||
"Rotator" => "rotator",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
private string GetAppIcon(string type) => type switch
|
||||
{
|
||||
"Dashboard" => "bi-bar-chart",
|
||||
"WebRotator" => "bi-arrow-repeat",
|
||||
"Image" => "bi-image",
|
||||
"Video" => "bi-play-circle",
|
||||
"Rotator" => "bi-arrow-repeat",
|
||||
_ => "bi-file-earmark"
|
||||
};
|
||||
|
||||
private string GetRotatorItemIcon(string type) => type switch
|
||||
{
|
||||
"Image" => "bi-image",
|
||||
"Video" => "bi-play-circle",
|
||||
"Webpage" => "bi-globe",
|
||||
_ => "bi-file-earmark"
|
||||
};
|
||||
|
||||
// 轮播项管理
|
||||
private async Task EditRotatorItems(ApplicationDto app)
|
||||
{
|
||||
_editingRotatorApp = app;
|
||||
_rotatorError = "";
|
||||
await LoadRotatorItems(app.Id);
|
||||
_showRotatorModal = true;
|
||||
}
|
||||
|
||||
private async Task LoadRotatorItems(int applicationId)
|
||||
{
|
||||
try
|
||||
{
|
||||
_rotatorItems = await ApiClient.GetRotatorItemsAsync(applicationId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_rotatorItems = new();
|
||||
_rotatorError = $"加载失败: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseRotatorModal()
|
||||
{
|
||||
_showRotatorModal = false;
|
||||
_editingRotatorApp = null;
|
||||
_rotatorItems = new();
|
||||
_newRotatorUrl = "";
|
||||
_rotatorError = "";
|
||||
}
|
||||
|
||||
private async Task AddRotatorItem()
|
||||
{
|
||||
if (_editingRotatorApp == null) return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_newRotatorUrl))
|
||||
{
|
||||
_rotatorError = "请输入 URL";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var request = new RotatorItemCreateRequest
|
||||
{
|
||||
ItemType = _newRotatorItemType,
|
||||
Url = _newRotatorUrl,
|
||||
Duration = _newRotatorDuration,
|
||||
Order = _rotatorItems.Count + 1
|
||||
};
|
||||
|
||||
var success = await ApiClient.CreateRotatorItemAsync(_editingRotatorApp.Id, request);
|
||||
if (success)
|
||||
{
|
||||
_newRotatorUrl = "";
|
||||
_rotatorError = "";
|
||||
await LoadRotatorItems(_editingRotatorApp.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_rotatorError = "添加失败";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_rotatorError = $"添加失败: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteRotatorItem(RotatorItemDto item)
|
||||
{
|
||||
if (_editingRotatorApp == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var success = await ApiClient.DeleteRotatorItemAsync(_editingRotatorApp.Id, item.Id);
|
||||
if (success)
|
||||
{
|
||||
await LoadRotatorItems(_editingRotatorApp.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_rotatorError = "删除失败";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_rotatorError = "删除失败";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MoveRotatorItemUp(RotatorItemDto item, int index)
|
||||
{
|
||||
if (_editingRotatorApp == null || index <= 0 || index >= _rotatorItems.Count) return;
|
||||
|
||||
// 交换顺序
|
||||
_rotatorItems[index].Order = _rotatorItems[index - 1].Order;
|
||||
_rotatorItems[index - 1].Order = item.Order;
|
||||
|
||||
// 更新到服务器
|
||||
var itemIds = _rotatorItems.OrderBy(x => x.Order).Select(x => x.Id).ToList();
|
||||
await ApiClient.ReorderRotatorItemsAsync(_editingRotatorApp.Id, itemIds);
|
||||
await LoadRotatorItems(_editingRotatorApp.Id);
|
||||
}
|
||||
|
||||
private async Task MoveRotatorItemDown(RotatorItemDto item, int index)
|
||||
{
|
||||
if (_editingRotatorApp == null || index < 0 || index >= _rotatorItems.Count - 1) return;
|
||||
|
||||
// 交换顺序
|
||||
_rotatorItems[index].Order = _rotatorItems[index + 1].Order;
|
||||
_rotatorItems[index + 1].Order = item.Order;
|
||||
|
||||
// 更新到服务器
|
||||
var itemIds = _rotatorItems.OrderBy(x => x.Order).Select(x => x.Id).ToList();
|
||||
await ApiClient.ReorderRotatorItemsAsync(_editingRotatorApp.Id, itemIds);
|
||||
await LoadRotatorItems(_editingRotatorApp.Id);
|
||||
}
|
||||
|
||||
private async Task TriggerFileUpload()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("click", _fileInput);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_rotatorError = "无法打开文件选择器";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnFileSelected(ChangeEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var files = (e.Value as IBrowserFile[]);
|
||||
if (files != null && files.Length > 0)
|
||||
{
|
||||
var file = files[0];
|
||||
var result = await ApiClient.UploadFileAsync(file);
|
||||
if (result?.Success == true)
|
||||
{
|
||||
_newRotatorUrl = result.Url ?? "";
|
||||
if (!string.IsNullOrEmpty(result.ItemType))
|
||||
{
|
||||
_newRotatorItemType = result.ItemType;
|
||||
}
|
||||
_rotatorError = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
_rotatorError = result?.Message ?? "上传失败";
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_rotatorError = $"上传失败: {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ else
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h3>@_stats.TotalApplications</h3>
|
||||
<p>内容数量</p>
|
||||
<p>应用数量</p>
|
||||
</div>
|
||||
<i class="bi bi-grid fs-1 opacity-50"></i>
|
||||
</div>
|
||||
|
||||
244
src/DRS9.Dashboard.Server/Controllers/FileUploadController.cs
Normal file
244
src/DRS9.Dashboard.Server/Controllers/FileUploadController.cs
Normal file
@@ -0,0 +1,244 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DRS9.Dashboard.Server.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/[controller]")]
|
||||
// [Authorize] - 暂时禁用认证以便测试
|
||||
public class UploadController : ControllerBase
|
||||
{
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
private readonly ILogger<UploadController> _logger;
|
||||
|
||||
// 允许的图片扩展名
|
||||
private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp" };
|
||||
// 允许的视频扩展名
|
||||
private static readonly string[] VideoExtensions = { ".mp4", ".webm", ".ogg", ".mov", ".avi", ".mkv" };
|
||||
// 最大文件大小 (50MB)
|
||||
private const long MaxFileSize = 50 * 1024 * 1024;
|
||||
|
||||
public UploadController(IWebHostEnvironment environment, ILogger<UploadController> logger)
|
||||
{
|
||||
_environment = environment;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件(图片或视频)
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<object>> UploadFile(IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return BadRequest(new { success = false, message = "请选择文件" });
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
if (file.Length > MaxFileSize)
|
||||
{
|
||||
return BadRequest(new { success = false, message = $"文件大小不能超过 {MaxFileSize / 1024 / 1024}MB" });
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
|
||||
// 确定文件类型
|
||||
string itemType;
|
||||
string subFolder;
|
||||
|
||||
if (ImageExtensions.Contains(extension))
|
||||
{
|
||||
itemType = "Image";
|
||||
subFolder = "images";
|
||||
}
|
||||
else if (VideoExtensions.Contains(extension))
|
||||
{
|
||||
itemType = "Video";
|
||||
subFolder = "videos";
|
||||
}
|
||||
else
|
||||
{
|
||||
return BadRequest(new { success = false, message = "不支持的文件类型,请上传图片或视频" });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 创建上传目录
|
||||
var uploadsFolder = Path.Combine(_environment.WebRootPath, "uploads", subFolder);
|
||||
if (!Directory.Exists(uploadsFolder))
|
||||
{
|
||||
Directory.CreateDirectory(uploadsFolder);
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
var uniqueFileName = $"{Guid.NewGuid()}{extension}";
|
||||
var filePath = Path.Combine(uploadsFolder, uniqueFileName);
|
||||
|
||||
// 保存文件
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
// 返回文件 URL
|
||||
var fileUrl = $"/uploads/{subFolder}/{uniqueFileName}";
|
||||
|
||||
_logger.LogInformation("文件上传成功: {FileName} -> {Url}", file.FileName, fileUrl);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = "上传成功",
|
||||
url = fileUrl,
|
||||
itemType = itemType,
|
||||
fileName = file.FileName
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "文件上传失败");
|
||||
return StatusCode(500, new { success = false, message = $"上传失败: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量上传文件
|
||||
/// </summary>
|
||||
[HttpPost("batch")]
|
||||
public async Task<ActionResult<object>> UploadFiles(List<IFormFile> files)
|
||||
{
|
||||
if (files == null || files.Count == 0)
|
||||
{
|
||||
return BadRequest(new { success = false, message = "请选择文件" });
|
||||
}
|
||||
|
||||
var results = new List<object>();
|
||||
var successCount = 0;
|
||||
var failCount = 0;
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var result = await UploadSingleFile(file);
|
||||
if (result.success)
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
failCount++;
|
||||
}
|
||||
results.Add(new
|
||||
{
|
||||
fileName = file.FileName,
|
||||
success = result.success,
|
||||
message = result.message,
|
||||
url = result.url,
|
||||
itemType = result.itemType
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = $"上传完成: {successCount} 个成功, {failCount} 个失败",
|
||||
results = results
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<(bool success, string message, string? url, string? itemType)> UploadSingleFile(IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return (false, "请选择文件", null, null);
|
||||
}
|
||||
|
||||
if (file.Length > MaxFileSize)
|
||||
{
|
||||
return (false, $"文件大小不能超过 {MaxFileSize / 1024 / 1024}MB", null, null);
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
|
||||
string itemType;
|
||||
string subFolder;
|
||||
|
||||
if (ImageExtensions.Contains(extension))
|
||||
{
|
||||
itemType = "Image";
|
||||
subFolder = "images";
|
||||
}
|
||||
else if (VideoExtensions.Contains(extension))
|
||||
{
|
||||
itemType = "Video";
|
||||
subFolder = "videos";
|
||||
}
|
||||
else
|
||||
{
|
||||
return (false, "不支持的文件类型", null, null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var uploadsFolder = Path.Combine(_environment.WebRootPath, "uploads", subFolder);
|
||||
if (!Directory.Exists(uploadsFolder))
|
||||
{
|
||||
Directory.CreateDirectory(uploadsFolder);
|
||||
}
|
||||
|
||||
var uniqueFileName = $"{Guid.NewGuid()}{extension}";
|
||||
var filePath = Path.Combine(uploadsFolder, uniqueFileName);
|
||||
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
var fileUrl = $"/uploads/{subFolder}/{uniqueFileName}";
|
||||
return (true, "上传成功", fileUrl, itemType);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "文件上传失败: {FileName}", file.FileName);
|
||||
return (false, $"上传失败: {ex.Message}", null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件
|
||||
/// </summary>
|
||||
[HttpDelete]
|
||||
public ActionResult DeleteFile([FromBody] FileDeleteRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Url))
|
||||
{
|
||||
return BadRequest(new { success = false, message = "请提供文件 URL" });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 从 URL 中提取文件路径
|
||||
var filePath = Path.Combine(_environment.WebRootPath, request.Url.TrimStart('/'));
|
||||
|
||||
if (System.IO.File.Exists(filePath))
|
||||
{
|
||||
System.IO.File.Delete(filePath);
|
||||
_logger.LogInformation("文件删除成功: {Url}", request.Url);
|
||||
return Ok(new { success = true, message = "删除成功" });
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound(new { success = false, message = "文件不存在" });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "文件删除失败: {Url}", request.Url);
|
||||
return StatusCode(500, new { success = false, message = $"删除失败: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class FileDeleteRequest
|
||||
{
|
||||
public string Url { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using DRS9.Dashboard.Application.DTOs;
|
||||
using DRS9.Dashboard.Application.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DRS9.Dashboard.Server.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/applications/{applicationId}/rotator-items")]
|
||||
// [Authorize] - 暂时禁用认证以便测试
|
||||
public class RotatorItemsController : ControllerBase
|
||||
{
|
||||
private readonly RotatorItemService _rotatorItemService;
|
||||
|
||||
public RotatorItemsController(RotatorItemService rotatorItemService)
|
||||
{
|
||||
_rotatorItemService = rotatorItemService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定应用的所有轮播项
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<RotatorItemDto>>> GetRotatorItems(int applicationId)
|
||||
{
|
||||
var items = await _rotatorItemService.GetByApplicationIdAsync(applicationId);
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加轮播项
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<RotatorItemDto>> CreateRotatorItem(int applicationId, [FromBody] RotatorItemCreateRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = await _rotatorItemService.CreateAsync(applicationId, request);
|
||||
if (item == null)
|
||||
return BadRequest(new { success = false, message = "应用不存在或类型不是轮播" });
|
||||
return Ok(item);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new { success = false, message = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { success = false, message = ex.Message + " | " + ex.StackTrace });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新轮播项
|
||||
/// </summary>
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<RotatorItemDto>> UpdateRotatorItem(int id, [FromBody] RotatorItemUpdateRequest request)
|
||||
{
|
||||
var item = await _rotatorItemService.UpdateAsync(id, request);
|
||||
if (item == null)
|
||||
return NotFound(new { success = false, message = "轮播项不存在" });
|
||||
return Ok(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除轮播项
|
||||
/// </summary>
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult> DeleteRotatorItem(int id)
|
||||
{
|
||||
var success = await _rotatorItemService.DeleteAsync(id);
|
||||
if (!success)
|
||||
return NotFound(new { success = false, message = "轮播项不存在" });
|
||||
return Ok(new { success = true, message = "删除成功" });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量更新轮播项顺序
|
||||
/// </summary>
|
||||
[HttpPost("reorder")]
|
||||
public async Task<ActionResult> ReorderRotatorItems(int applicationId, [FromBody] List<int> itemIds)
|
||||
{
|
||||
await _rotatorItemService.ReorderAsync(applicationId, itemIds);
|
||||
return Ok(new { success = true, message = "顺序更新成功" });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using DRS9.Dashboard.Application.DTOs;
|
||||
using DRS9.Dashboard.Application.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DRS9.Dashboard.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 轮播显示控制器 - 供设备客户端使用
|
||||
/// </summary>
|
||||
[Route("rotator")]
|
||||
public class RotatorViewerController : Controller
|
||||
{
|
||||
private readonly RotatorItemService _rotatorItemService;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly ILogger<RotatorViewerController> _logger;
|
||||
|
||||
public RotatorViewerController(
|
||||
RotatorItemService rotatorItemService,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<RotatorViewerController> logger)
|
||||
{
|
||||
_rotatorItemService = rotatorItemService;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定应用的轮播配置 (JSON)
|
||||
/// </summary>
|
||||
[HttpGet("{applicationId}/config.json")]
|
||||
public async Task<ActionResult> GetConfig(int applicationId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var items = await _rotatorItemService.GetByApplicationIdAsync(applicationId);
|
||||
|
||||
// 只返回启用的项目
|
||||
var enabledItems = items.Where(i => i.IsEnabled).OrderBy(i => i.Order).ToList();
|
||||
|
||||
var config = new
|
||||
{
|
||||
urls = enabledItems.Select(i => GetAbsoluteUrl(i.Url)).ToList(),
|
||||
switchIntervals = enabledItems.Select(i => i.Duration).ToList()
|
||||
};
|
||||
|
||||
return Json(config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "获取轮播配置失败: ApplicationId={ApplicationId}", applicationId);
|
||||
return Json(new { urls = new List<string>(), switchIntervals = new List<int>() });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示轮播页面
|
||||
/// </summary>
|
||||
[HttpGet("{applicationId}")]
|
||||
public IActionResult ViewRotator(int applicationId)
|
||||
{
|
||||
return View(applicationId);
|
||||
}
|
||||
|
||||
private string GetAbsoluteUrl(string relativeOrAbsoluteUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(relativeOrAbsoluteUrl))
|
||||
return string.Empty;
|
||||
|
||||
// 如果已经是绝对 URL,直接返回
|
||||
if (relativeOrAbsoluteUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
|
||||
relativeOrAbsoluteUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return relativeOrAbsoluteUrl;
|
||||
}
|
||||
|
||||
// 转换为绝对 URL
|
||||
var request = _httpContextAccessor.HttpContext?.Request;
|
||||
if (request != null)
|
||||
{
|
||||
var baseUrl = $"{request.Scheme}://{request.Host}";
|
||||
return relativeOrAbsoluteUrl.StartsWith("/") ? baseUrl + relativeOrAbsoluteUrl : baseUrl + "/" + relativeOrAbsoluteUrl;
|
||||
}
|
||||
|
||||
return relativeOrAbsoluteUrl;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@ using Microsoft.OpenApi.Models;
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddControllersWithViews(); // 支持 MVC 视图
|
||||
builder.Services.AddRazorPages();
|
||||
|
||||
// Configure Database
|
||||
builder.Services.AddDbContext<DashboardDbContext>(options =>
|
||||
@@ -42,6 +43,9 @@ builder.Services.AddAuthorization();
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
// Add HttpContextAccessor for MVC controllers
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
// Configure Blazor Server Circuit
|
||||
builder.Services.AddServerSideBlazor()
|
||||
.AddCircuitOptions(options =>
|
||||
@@ -78,6 +82,7 @@ builder.Services.AddSingleton(new JwtTokenService(
|
||||
|
||||
builder.Services.AddScoped<DeviceService>();
|
||||
builder.Services.AddScoped<ApplicationService>();
|
||||
builder.Services.AddScoped<RotatorItemService>();
|
||||
builder.Services.AddScoped<DeviceManagementService>();
|
||||
builder.Services.AddScoped<PlaylistService>();
|
||||
builder.Services.AddScoped<BatchManagementService>();
|
||||
@@ -153,6 +158,9 @@ app.UseAntiforgery();
|
||||
app.UseWebSockets();
|
||||
|
||||
app.MapControllers();
|
||||
app.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||
|
||||
// Blazor Server
|
||||
app.MapRazorComponents<DRS9.Dashboard.Server.Components.App>()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using DRS9.Dashboard.Application.DTOs;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
|
||||
namespace DRS9.Dashboard.Server.Services;
|
||||
|
||||
@@ -251,4 +252,48 @@ public class ApiClientService
|
||||
return new DashboardStatsDto();
|
||||
return await response.Content.ReadFromJsonAsync<DashboardStatsDto>() ?? new DashboardStatsDto();
|
||||
}
|
||||
|
||||
// 轮播项 API
|
||||
public async Task<List<RotatorItemDto>> GetRotatorItemsAsync(int applicationId)
|
||||
{
|
||||
AddAuthHeader();
|
||||
var response = await _httpClient.GetAsync($"/api/admin/applications/{applicationId}/rotator-items");
|
||||
if (!response.IsSuccessStatusCode) return new List<RotatorItemDto>();
|
||||
return await response.Content.ReadFromJsonAsync<List<RotatorItemDto>>() ?? new List<RotatorItemDto>();
|
||||
}
|
||||
|
||||
public async Task<bool> CreateRotatorItemAsync(int applicationId, RotatorItemCreateRequest request)
|
||||
{
|
||||
AddAuthHeader();
|
||||
var response = await _httpClient.PostAsJsonAsync($"/api/admin/applications/{applicationId}/rotator-items", request);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteRotatorItemAsync(int applicationId, int itemId)
|
||||
{
|
||||
AddAuthHeader();
|
||||
var response = await _httpClient.DeleteAsync($"/api/admin/applications/{applicationId}/rotator-items/{itemId}");
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public async Task<bool> ReorderRotatorItemsAsync(int applicationId, List<int> itemIds)
|
||||
{
|
||||
AddAuthHeader();
|
||||
var response = await _httpClient.PostAsJsonAsync($"/api/admin/applications/{applicationId}/rotator-items/reorder", itemIds);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public async Task<FileUploadResponse?> UploadFileAsync(IBrowserFile file, string? itemType = null)
|
||||
{
|
||||
AddAuthHeader();
|
||||
using var content = new StreamContent(file.OpenReadStream(maxAllowedSize: 50 * 1024 * 1024));
|
||||
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(file.ContentType);
|
||||
|
||||
var formData = new MultipartFormDataContent();
|
||||
formData.Add(content, "file", file.Name);
|
||||
|
||||
var response = await _httpClient.PostAsync("/api/admin/upload", formData);
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
return await response.Content.ReadFromJsonAsync<FileUploadResponse>();
|
||||
}
|
||||
}
|
||||
|
||||
356
src/DRS9.Dashboard.Server/Views/RotatorViewer/ViewRotator.cshtml
Normal file
356
src/DRS9.Dashboard.Server/Views/RotatorViewer/ViewRotator.cshtml
Normal file
@@ -0,0 +1,356 @@
|
||||
@model int
|
||||
@{
|
||||
ViewData["Title"] = "轮播显示";
|
||||
var applicationId = Model;
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DRS9 轮播 - 应用 #@applicationId</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.iframe-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.iframe-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.iframe-wrapper.active {
|
||||
opacity: 1;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.iframe-wrapper.loading {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.media-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.media-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
background: #f8f9fa;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.error-url {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
word-break: break-all;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid rgba(255,255,255,0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="loading-overlay" id="loadingOverlay">
|
||||
<div class="spinner"></div>
|
||||
<div class="loading-text">正在加载轮播内容...</div>
|
||||
</div>
|
||||
|
||||
<div class="iframe-container" id="iframeContainer"></div>
|
||||
|
||||
<script>
|
||||
const APPLICATION_ID = @applicationId;
|
||||
const CONFIG_URL = `/rotator/${APPLICATION_ID}/config.json`;
|
||||
|
||||
let urls = [];
|
||||
let switchIntervals = [];
|
||||
let currentIndex = 0;
|
||||
let rotationInterval = null;
|
||||
let iframeWrappers = [null, null];
|
||||
let currentWrapperIndex = 0;
|
||||
|
||||
// 加载配置文件
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const response = await fetch(CONFIG_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
const config = await response.json();
|
||||
urls = config.urls || [];
|
||||
switchIntervals = config.switchIntervals || [];
|
||||
console.log(`配置加载成功: ${urls.length} 个URL`);
|
||||
|
||||
if (urls.length === 0) {
|
||||
showError('此轮播应用没有配置任何内容');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('配置加载失败:', err);
|
||||
showError('配置加载失败: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 检测 URL 类型
|
||||
function getUrlType(url) {
|
||||
const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg', '.webp', '.ico'];
|
||||
const videoExts = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv', '.flv'];
|
||||
|
||||
const lowerUrl = url.toLowerCase();
|
||||
for (const ext of imageExts) {
|
||||
if (lowerUrl.endsWith(ext)) return 'image';
|
||||
}
|
||||
for (const ext of videoExts) {
|
||||
if (lowerUrl.endsWith(ext)) return 'video';
|
||||
}
|
||||
return 'page';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
document.getElementById('iframeContainer').innerHTML = `
|
||||
<div class="error-message">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<div class="error-text">加载失败</div>
|
||||
<div class="error-url">${message}</div>
|
||||
</div>
|
||||
`;
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
const overlay = document.getElementById('loadingOverlay');
|
||||
if (overlay) {
|
||||
overlay.style.opacity = '0';
|
||||
overlay.style.transition = 'opacity 0.5s';
|
||||
setTimeout(() => overlay.remove(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
function createIframeWrapper() {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'iframe-wrapper';
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function loadUrl(url, wrapperIndex) {
|
||||
const wrapper = iframeWrappers[wrapperIndex];
|
||||
const urlType = getUrlType(url);
|
||||
|
||||
wrapper.innerHTML = '';
|
||||
wrapper.classList.remove('active');
|
||||
wrapper.classList.add('loading');
|
||||
|
||||
if (urlType === 'image') {
|
||||
const img = document.createElement('img');
|
||||
img.className = 'media-image';
|
||||
img.onload = () => {
|
||||
hideLoading();
|
||||
activateWrapper(wrapperIndex);
|
||||
};
|
||||
img.onerror = () => {
|
||||
hideLoading();
|
||||
activateWrapper(wrapperIndex);
|
||||
};
|
||||
img.src = url;
|
||||
wrapper.appendChild(img);
|
||||
|
||||
} else if (urlType === 'video') {
|
||||
const video = document.createElement('video');
|
||||
video.className = 'media-video';
|
||||
video.autoplay = true;
|
||||
video.muted = false;
|
||||
video.playsInline = true;
|
||||
video.loop = false;
|
||||
|
||||
video.onloadedmetadata = () => {
|
||||
hideLoading();
|
||||
activateWrapper(wrapperIndex);
|
||||
video.play().catch(err => {
|
||||
console.log('有声播放失败,切换为静音:', err);
|
||||
video.muted = true;
|
||||
video.play().catch(err2 => console.log('静音播放也失败:', err2));
|
||||
});
|
||||
};
|
||||
|
||||
video.onerror = () => {
|
||||
hideLoading();
|
||||
activateWrapper(wrapperIndex);
|
||||
};
|
||||
video.src = url;
|
||||
wrapper.appendChild(video);
|
||||
|
||||
} else {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.scrolling = 'no';
|
||||
let loaded = false;
|
||||
let errorShown = false;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
if (!loaded && !errorShown) {
|
||||
errorShown = true;
|
||||
hideLoading();
|
||||
activateWrapper(wrapperIndex);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
iframe.onload = () => {
|
||||
if (!errorShown) {
|
||||
loaded = true;
|
||||
clearTimeout(timeout);
|
||||
hideLoading();
|
||||
activateWrapper(wrapperIndex);
|
||||
}
|
||||
};
|
||||
|
||||
iframe.onerror = () => {
|
||||
if (!errorShown) {
|
||||
clearTimeout(timeout);
|
||||
errorShown = true;
|
||||
hideLoading();
|
||||
activateWrapper(wrapperIndex);
|
||||
}
|
||||
};
|
||||
|
||||
iframe.src = url;
|
||||
wrapper.appendChild(iframe);
|
||||
}
|
||||
}
|
||||
|
||||
function activateWrapper(wrapperIndex) {
|
||||
iframeWrappers.forEach((w, i) => {
|
||||
if (i === wrapperIndex) {
|
||||
w.classList.add('active');
|
||||
w.classList.remove('loading');
|
||||
} else {
|
||||
w.classList.remove('active');
|
||||
}
|
||||
});
|
||||
startRotationTimer();
|
||||
}
|
||||
|
||||
function startRotationTimer() {
|
||||
if (rotationInterval) {
|
||||
clearInterval(rotationInterval);
|
||||
}
|
||||
if (urls.length === 0) return;
|
||||
|
||||
const effectiveIndex = (currentIndex - 1 + urls.length) % urls.length;
|
||||
const interval = switchIntervals[effectiveIndex];
|
||||
rotationInterval = setInterval(loadNextUrl, interval * 1000);
|
||||
}
|
||||
|
||||
function loadNextUrl() {
|
||||
if (urls.length === 0) return;
|
||||
|
||||
const nextWrapperIndex = (currentWrapperIndex + 1) % 2;
|
||||
const url = urls[currentIndex];
|
||||
|
||||
loadUrl(url, nextWrapperIndex);
|
||||
|
||||
currentWrapperIndex = nextWrapperIndex;
|
||||
currentIndex = (currentIndex + 1) % urls.length;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const container = document.getElementById('iframeContainer');
|
||||
|
||||
iframeWrappers[0] = createIframeWrapper();
|
||||
iframeWrappers[1] = createIframeWrapper();
|
||||
container.appendChild(iframeWrappers[0]);
|
||||
container.appendChild(iframeWrappers[1]);
|
||||
|
||||
currentIndex = 0;
|
||||
currentWrapperIndex = 0;
|
||||
|
||||
await loadConfig();
|
||||
if (urls.length > 0) {
|
||||
loadNextUrl();
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = init;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
2
src/DRS9.Dashboard.Server/Views/_ViewImports.cshtml
Normal file
2
src/DRS9.Dashboard.Server/Views/_ViewImports.cshtml
Normal file
@@ -0,0 +1,2 @@
|
||||
@using DRS9.Dashboard.Server
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
3
src/DRS9.Dashboard.Server/Views/_ViewStart.cshtml
Normal file
3
src/DRS9.Dashboard.Server/Views/_ViewStart.cshtml
Normal file
@@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
Binary file not shown.
@@ -215,6 +215,11 @@ main {
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
.content-icon.rotator {
|
||||
background-color: #e1f5fe;
|
||||
color: #0288d1;
|
||||
}
|
||||
|
||||
/* Drag and Drop */
|
||||
.draggable-item {
|
||||
cursor: move;
|
||||
|
||||
Reference in New Issue
Block a user