@@ -81,27 +94,28 @@
}
- 内容名称
+ 应用名称
- 内容类型
+ 应用类型
请选择...
- Dashboard
- WebRotator
- Image
- Video
+ 网页链接
+ 轮播
@if (_editingApp != null)
{
- 内容类型创建后不可修改
+ 应用类型创建后不可修改
}
-
- 内容 URL
-
-
+ @if (_appType == "Dashboard" || (_editingApp != null && _editingApp.Type == "Dashboard"))
+ {
+
+ URL
+
+
+ }
描述
@@ -116,6 +130,120 @@
}
+
+@if (_showRotatorModal)
+{
+
+
+
+
+
+
+
+
+
+ @if (!string.IsNullOrEmpty(_rotatorError))
+ {
+
+ @_rotatorError
+
+ }
+
+
+ 类型
+
+ 网页链接
+ 图片
+ 视频
+
+
+
+
+ 时长(秒)
+
+
+
+
+
+ 添加
+
+
+
+
+
+
+
+
+
+
+ @if (_rotatorItems == null || _rotatorItems.Count == 0)
+ {
+
暂无轮播项
+ }
+ else
+ {
+
+ @for (int i = 0; i < _rotatorItems.Count; i++)
+ {
+ var item = _rotatorItems[i];
+ var isFirst = (i == 0);
+ var isLast = (i == _rotatorItems.Count - 1);
+
+
+
+
+
@item.ItemType
+
@item.Url
+
+ @item.Duration 秒
+
+
+
+
+ MoveRotatorItemUp(item, i))" disabled="@isFirst">
+
+
+ MoveRotatorItemDown(item, i))" disabled="@isLast">
+
+
+ DeleteRotatorItem(item))">
+
+
+
+
+ }
+
+ }
+
+
+
+
+
+
+
+}
+
@code {
private List
_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 _rotatorItems = new();
+ private Dictionary _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("confirm", $"确定要删除内容 '{app.Name}' 吗?"))
+ if (await JSRuntime.InvokeAsync("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}";
+ }
+ }
}
diff --git a/src/DRS9.Dashboard.Server/Components/Pages/Home.razor b/src/DRS9.Dashboard.Server/Components/Pages/Home.razor
index 23d9458..b05d718 100644
--- a/src/DRS9.Dashboard.Server/Components/Pages/Home.razor
+++ b/src/DRS9.Dashboard.Server/Components/Pages/Home.razor
@@ -44,7 +44,7 @@ else
@_stats.TotalApplications
-
内容数量
+
应用数量
diff --git a/src/DRS9.Dashboard.Server/Controllers/FileUploadController.cs b/src/DRS9.Dashboard.Server/Controllers/FileUploadController.cs
new file mode 100644
index 0000000..5256094
--- /dev/null
+++ b/src/DRS9.Dashboard.Server/Controllers/FileUploadController.cs
@@ -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 _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 logger)
+ {
+ _environment = environment;
+ _logger = logger;
+ }
+
+ ///
+ /// 上传文件(图片或视频)
+ ///
+ [HttpPost]
+ public async Task> 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}" });
+ }
+ }
+
+ ///
+ /// 批量上传文件
+ ///
+ [HttpPost("batch")]
+ public async Task> UploadFiles(List files)
+ {
+ if (files == null || files.Count == 0)
+ {
+ return BadRequest(new { success = false, message = "请选择文件" });
+ }
+
+ var results = new List();
+ 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);
+ }
+ }
+
+ ///
+ /// 删除文件
+ ///
+ [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;
+}
diff --git a/src/DRS9.Dashboard.Server/Controllers/RotatorItemsController.cs b/src/DRS9.Dashboard.Server/Controllers/RotatorItemsController.cs
new file mode 100644
index 0000000..63d0dff
--- /dev/null
+++ b/src/DRS9.Dashboard.Server/Controllers/RotatorItemsController.cs
@@ -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;
+ }
+
+ ///
+ /// 获取指定应用的所有轮播项
+ ///
+ [HttpGet]
+ public async Task>> GetRotatorItems(int applicationId)
+ {
+ var items = await _rotatorItemService.GetByApplicationIdAsync(applicationId);
+ return Ok(items);
+ }
+
+ ///
+ /// 添加轮播项
+ ///
+ [HttpPost]
+ public async Task> 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 });
+ }
+ }
+
+ ///
+ /// 更新轮播项
+ ///
+ [HttpPut("{id}")]
+ public async Task> 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);
+ }
+
+ ///
+ /// 删除轮播项
+ ///
+ [HttpDelete("{id}")]
+ public async Task DeleteRotatorItem(int id)
+ {
+ var success = await _rotatorItemService.DeleteAsync(id);
+ if (!success)
+ return NotFound(new { success = false, message = "轮播项不存在" });
+ return Ok(new { success = true, message = "删除成功" });
+ }
+
+ ///
+ /// 批量更新轮播项顺序
+ ///
+ [HttpPost("reorder")]
+ public async Task ReorderRotatorItems(int applicationId, [FromBody] List itemIds)
+ {
+ await _rotatorItemService.ReorderAsync(applicationId, itemIds);
+ return Ok(new { success = true, message = "顺序更新成功" });
+ }
+}
diff --git a/src/DRS9.Dashboard.Server/Controllers/RotatorViewerController.cs b/src/DRS9.Dashboard.Server/Controllers/RotatorViewerController.cs
new file mode 100644
index 0000000..a64bf30
--- /dev/null
+++ b/src/DRS9.Dashboard.Server/Controllers/RotatorViewerController.cs
@@ -0,0 +1,86 @@
+using DRS9.Dashboard.Application.DTOs;
+using DRS9.Dashboard.Application.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace DRS9.Dashboard.Server.Controllers;
+
+///
+/// 轮播显示控制器 - 供设备客户端使用
+///
+[Route("rotator")]
+public class RotatorViewerController : Controller
+{
+ private readonly RotatorItemService _rotatorItemService;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly ILogger _logger;
+
+ public RotatorViewerController(
+ RotatorItemService rotatorItemService,
+ IHttpContextAccessor httpContextAccessor,
+ ILogger logger)
+ {
+ _rotatorItemService = rotatorItemService;
+ _httpContextAccessor = httpContextAccessor;
+ _logger = logger;
+ }
+
+ ///
+ /// 获取指定应用的轮播配置 (JSON)
+ ///
+ [HttpGet("{applicationId}/config.json")]
+ public async Task 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(), switchIntervals = new List() });
+ }
+ }
+
+ ///
+ /// 显示轮播页面
+ ///
+ [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;
+ }
+}
diff --git a/src/DRS9.Dashboard.Server/Program.cs b/src/DRS9.Dashboard.Server/Program.cs
index d740ed3..496a696 100644
--- a/src/DRS9.Dashboard.Server/Program.cs
+++ b/src/DRS9.Dashboard.Server/Program.cs
@@ -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(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();
builder.Services.AddScoped();
+builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
@@ -153,6 +158,9 @@ app.UseAntiforgery();
app.UseWebSockets();
app.MapControllers();
+app.MapControllerRoute(
+ name: "default",
+ pattern: "{controller=Home}/{action=Index}/{id?}");
// Blazor Server
app.MapRazorComponents()
diff --git a/src/DRS9.Dashboard.Server/Services/ApiClientService.cs b/src/DRS9.Dashboard.Server/Services/ApiClientService.cs
index 04c6eb9..08bd081 100644
--- a/src/DRS9.Dashboard.Server/Services/ApiClientService.cs
+++ b/src/DRS9.Dashboard.Server/Services/ApiClientService.cs
@@ -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() ?? new DashboardStatsDto();
}
+
+ // 轮播项 API
+ public async Task> GetRotatorItemsAsync(int applicationId)
+ {
+ AddAuthHeader();
+ var response = await _httpClient.GetAsync($"/api/admin/applications/{applicationId}/rotator-items");
+ if (!response.IsSuccessStatusCode) return new List();
+ return await response.Content.ReadFromJsonAsync>() ?? new List();
+ }
+
+ public async Task CreateRotatorItemAsync(int applicationId, RotatorItemCreateRequest request)
+ {
+ AddAuthHeader();
+ var response = await _httpClient.PostAsJsonAsync($"/api/admin/applications/{applicationId}/rotator-items", request);
+ return response.IsSuccessStatusCode;
+ }
+
+ public async Task DeleteRotatorItemAsync(int applicationId, int itemId)
+ {
+ AddAuthHeader();
+ var response = await _httpClient.DeleteAsync($"/api/admin/applications/{applicationId}/rotator-items/{itemId}");
+ return response.IsSuccessStatusCode;
+ }
+
+ public async Task ReorderRotatorItemsAsync(int applicationId, List itemIds)
+ {
+ AddAuthHeader();
+ var response = await _httpClient.PostAsJsonAsync($"/api/admin/applications/{applicationId}/rotator-items/reorder", itemIds);
+ return response.IsSuccessStatusCode;
+ }
+
+ public async Task 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();
+ }
}
diff --git a/src/DRS9.Dashboard.Server/Views/RotatorViewer/ViewRotator.cshtml b/src/DRS9.Dashboard.Server/Views/RotatorViewer/ViewRotator.cshtml
new file mode 100644
index 0000000..acf8b02
--- /dev/null
+++ b/src/DRS9.Dashboard.Server/Views/RotatorViewer/ViewRotator.cshtml
@@ -0,0 +1,356 @@
+@model int
+@{
+ ViewData["Title"] = "轮播显示";
+ var applicationId = Model;
+}
+
+
+
+
+
+ DRS9 轮播 - 应用 #@applicationId
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DRS9.Dashboard.Server/Views/_ViewImports.cshtml b/src/DRS9.Dashboard.Server/Views/_ViewImports.cshtml
new file mode 100644
index 0000000..1016af9
--- /dev/null
+++ b/src/DRS9.Dashboard.Server/Views/_ViewImports.cshtml
@@ -0,0 +1,2 @@
+@using DRS9.Dashboard.Server
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
diff --git a/src/DRS9.Dashboard.Server/Views/_ViewStart.cshtml b/src/DRS9.Dashboard.Server/Views/_ViewStart.cshtml
new file mode 100644
index 0000000..9d06493
--- /dev/null
+++ b/src/DRS9.Dashboard.Server/Views/_ViewStart.cshtml
@@ -0,0 +1,3 @@
+@{
+ Layout = null;
+}
diff --git a/src/DRS9.Dashboard.Server/dashboard.db b/src/DRS9.Dashboard.Server/dashboard.db
index bfc3f91..d3a9ce1 100644
Binary files a/src/DRS9.Dashboard.Server/dashboard.db and b/src/DRS9.Dashboard.Server/dashboard.db differ
diff --git a/src/DRS9.Dashboard.Server/wwwroot/css/app.css b/src/DRS9.Dashboard.Server/wwwroot/css/app.css
index f2b84f1..5102181 100644
--- a/src/DRS9.Dashboard.Server/wwwroot/css/app.css
+++ b/src/DRS9.Dashboard.Server/wwwroot/css/app.css
@@ -215,6 +215,11 @@ main {
color: #f57c00;
}
+.content-icon.rotator {
+ background-color: #e1f5fe;
+ color: #0288d1;
+}
+
/* Drag and Drop */
.draggable-item {
cursor: move;