- 修复已登录用户访问 /remote/:token 路由被重定向到首页的问题
- 路由守卫优先检查静态路由,静态路由直接放行不走权限验证
- 后端生成的 accessUrl 使用 Hash 路由格式 (/#/remote/{token})
- 前端 remote-desktop-modal 中修正链接格式为 Hash 路由
- 新增远程桌面访问页面 /views/remote/index.vue
431 lines
18 KiB
C#
431 lines
18 KiB
C#
using AmtScanner.Api.Data;
|
||
using AmtScanner.Api.Models;
|
||
using AmtScanner.Api.Services;
|
||
using Microsoft.AspNetCore.Authorization;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.EntityFrameworkCore;
|
||
|
||
namespace AmtScanner.Api.Controllers;
|
||
|
||
/// <summary>
|
||
/// 菜单控制器
|
||
/// </summary>
|
||
[ApiController]
|
||
public class MenuController : ControllerBase
|
||
{
|
||
private readonly IMenuService _menuService;
|
||
private readonly AppDbContext _context;
|
||
|
||
public MenuController(IMenuService menuService, AppDbContext context)
|
||
{
|
||
_menuService = menuService;
|
||
_context = context;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取用户菜单(adminSystem 前端使用的路由)
|
||
/// </summary>
|
||
[Authorize]
|
||
[HttpGet("api/v3/system/menus/simple")]
|
||
public async Task<ActionResult<ApiResponse<List<MenuDto>>>> GetUserMenus()
|
||
{
|
||
var userIdClaim = User.FindFirst("userId")?.Value;
|
||
if (string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out var userId))
|
||
{
|
||
return Ok(ApiResponse<List<MenuDto>>.Fail(401, "无效的用户"));
|
||
}
|
||
|
||
var menus = await _menuService.GetUserMenusAsync(userId);
|
||
return Ok(ApiResponse<List<MenuDto>>.Success(menus));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取所有菜单列表
|
||
/// </summary>
|
||
[Authorize]
|
||
[HttpGet("api/menu/list")]
|
||
public async Task<ActionResult<ApiResponse<List<MenuDto>>>> GetAllMenus()
|
||
{
|
||
var menus = await _menuService.GetAllMenusAsync();
|
||
return Ok(ApiResponse<List<MenuDto>>.Success(menus));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建菜单
|
||
/// </summary>
|
||
[Authorize]
|
||
[HttpPost("api/menu")]
|
||
public async Task<ActionResult<ApiResponse<Menu>>> CreateMenu([FromBody] CreateMenuRequest request)
|
||
{
|
||
Console.WriteLine($"[CreateMenu] 收到请求: ParentId={request.ParentId}, Name={request.Name}, Path={request.Path}, Component={request.Component}");
|
||
|
||
// 处理路径:子菜单(有 ParentId)的路径不能以 / 开头
|
||
var menuPath = request.Path;
|
||
if (request.ParentId.HasValue && menuPath.StartsWith("/"))
|
||
{
|
||
// 子菜单:取最后一段路径作为相对路径
|
||
var pathParts = menuPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||
menuPath = pathParts.Length > 0 ? pathParts[^1] : menuPath.TrimStart('/');
|
||
Console.WriteLine($"[CreateMenu] 子菜单路径已修正: {request.Path} -> {menuPath}");
|
||
}
|
||
|
||
// 目录(无 ParentId)的路径必须以 / 开头
|
||
if (!request.ParentId.HasValue && !menuPath.StartsWith("/"))
|
||
{
|
||
menuPath = "/" + menuPath;
|
||
Console.WriteLine($"[CreateMenu] 目录路径已修正: {request.Path} -> {menuPath}");
|
||
}
|
||
|
||
// 检查 Name 是否唯一,如果重复则自动生成唯一名称
|
||
var menuName = request.Name;
|
||
if (await _context.Menus.AnyAsync(m => m.Name == menuName))
|
||
{
|
||
// 生成唯一名称:原名称 + 时间戳
|
||
menuName = $"{request.Name}_{DateTime.Now:yyyyMMddHHmmss}";
|
||
Console.WriteLine($"[CreateMenu] 菜单名称已修正(避免重复): {request.Name} -> {menuName}");
|
||
}
|
||
|
||
var menu = new Menu
|
||
{
|
||
ParentId = request.ParentId,
|
||
Name = menuName,
|
||
Path = menuPath,
|
||
Component = request.Component,
|
||
Title = request.Title,
|
||
Icon = request.Icon,
|
||
Sort = request.Sort,
|
||
IsHide = request.IsHide,
|
||
KeepAlive = request.KeepAlive,
|
||
Link = request.Link,
|
||
IsIframe = request.IsIframe,
|
||
Roles = request.Roles != null ? string.Join(",", request.Roles) : null
|
||
};
|
||
|
||
_context.Menus.Add(menu);
|
||
await _context.SaveChangesAsync();
|
||
|
||
Console.WriteLine($"[CreateMenu] 菜单已创建: Id={menu.Id}, ParentId={menu.ParentId}");
|
||
|
||
// 如果有组件路径且不是目录组件,自动创建 Vue 组件文件
|
||
if (!string.IsNullOrEmpty(request.Component) &&
|
||
request.Component != "/index/index" &&
|
||
request.AutoCreateComponent)
|
||
{
|
||
try
|
||
{
|
||
await CreateVueComponentAsync(request.Component, request.Title ?? request.Name);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"[CreateMenu] 创建组件文件失败: {ex.Message}");
|
||
// 不影响菜单创建,只是记录日志
|
||
}
|
||
}
|
||
|
||
return Ok(ApiResponse<Menu>.Success(menu, "菜单创建成功"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建 Vue 组件文件
|
||
/// </summary>
|
||
private async Task CreateVueComponentAsync(string componentPath, string title)
|
||
{
|
||
var configuration = HttpContext.RequestServices.GetRequiredService<IConfiguration>();
|
||
var viewsPath = configuration["Frontend:ViewsPath"] ?? "../adminSystem/src/views";
|
||
|
||
// 组件路径格式: /test/page -> test/page.vue
|
||
// 统一使用正斜杠,然后转换为系统路径分隔符
|
||
var relativePath = componentPath.TrimStart('/').Replace('\\', '/');
|
||
|
||
// 构建完整路径,确保路径分隔符正确
|
||
var fullPath = Path.GetFullPath(Path.Combine(viewsPath, $"{relativePath}.vue"));
|
||
var directory = Path.GetDirectoryName(fullPath);
|
||
|
||
Console.WriteLine($"[CreateVueComponent] viewsPath: {viewsPath}");
|
||
Console.WriteLine($"[CreateVueComponent] relativePath: {relativePath}");
|
||
Console.WriteLine($"[CreateVueComponent] fullPath: {fullPath}");
|
||
Console.WriteLine($"[CreateVueComponent] directory: {directory}");
|
||
|
||
// 如果文件已存在,不覆盖
|
||
if (System.IO.File.Exists(fullPath))
|
||
{
|
||
Console.WriteLine($"[CreateVueComponent] 组件文件已存在: {fullPath}");
|
||
return;
|
||
}
|
||
|
||
// 创建目录
|
||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||
{
|
||
Console.WriteLine($"[CreateVueComponent] 创建目录: {directory}");
|
||
Directory.CreateDirectory(directory);
|
||
}
|
||
|
||
// 生成组件名称 (PascalCase)
|
||
var componentName = string.Join("", relativePath.Split('/', '-', '_')
|
||
.Where(s => !string.IsNullOrEmpty(s))
|
||
.Select(s => char.ToUpper(s[0]) + s.Substring(1)));
|
||
|
||
// 用于 CSS 类名的路径(使用连字符)
|
||
var cssClassName = relativePath.Replace('/', '-').Replace('_', '-');
|
||
|
||
// 生成 Vue 组件模板
|
||
var template = $@"<template>
|
||
<div class=""{cssClassName}-page"">
|
||
<ElCard>
|
||
<template #header>
|
||
<span>{title}</span>
|
||
</template>
|
||
<p>这是 {title} 页面</p>
|
||
<p>组件路径:{componentPath}</p>
|
||
</ElCard>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang=""ts"">
|
||
defineOptions({{ name: '{componentName}' }})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.{cssClassName}-page {{
|
||
padding: 20px;
|
||
}}
|
||
</style>
|
||
";
|
||
|
||
await System.IO.File.WriteAllTextAsync(fullPath, template, System.Text.Encoding.UTF8);
|
||
Console.WriteLine($"[CreateVueComponent] 组件文件已创建: {fullPath}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新菜单
|
||
/// </summary>
|
||
[Authorize]
|
||
[HttpPut("api/menu/{id}")]
|
||
public async Task<ActionResult<ApiResponse<Menu>>> UpdateMenu(int id, [FromBody] UpdateMenuRequest request)
|
||
{
|
||
var menu = await _context.Menus.FindAsync(id);
|
||
if (menu == null)
|
||
{
|
||
return Ok(ApiResponse<Menu>.Fail(404, "菜单不存在"));
|
||
}
|
||
|
||
if (request.ParentId.HasValue) menu.ParentId = request.ParentId;
|
||
if (!string.IsNullOrEmpty(request.Name)) menu.Name = request.Name;
|
||
if (!string.IsNullOrEmpty(request.Path)) menu.Path = request.Path;
|
||
if (request.Component != null) menu.Component = request.Component;
|
||
if (!string.IsNullOrEmpty(request.Title)) menu.Title = request.Title;
|
||
if (request.Icon != null) menu.Icon = request.Icon;
|
||
if (request.Sort.HasValue) menu.Sort = request.Sort.Value;
|
||
if (request.IsHide.HasValue) menu.IsHide = request.IsHide.Value;
|
||
if (request.KeepAlive.HasValue) menu.KeepAlive = request.KeepAlive.Value;
|
||
if (request.Link != null) menu.Link = request.Link;
|
||
if (request.IsIframe.HasValue) menu.IsIframe = request.IsIframe.Value;
|
||
if (request.Roles != null) menu.Roles = string.Join(",", request.Roles);
|
||
|
||
await _context.SaveChangesAsync();
|
||
|
||
return Ok(ApiResponse<Menu>.Success(menu, "菜单更新成功"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除菜单
|
||
/// </summary>
|
||
[Authorize]
|
||
[HttpDelete("api/menu/{id}")]
|
||
public async Task<ActionResult<ApiResponse<object>>> DeleteMenu(int id)
|
||
{
|
||
var menu = await _context.Menus.FindAsync(id);
|
||
if (menu == null)
|
||
{
|
||
return Ok(ApiResponse<object>.Fail(404, "菜单不存在"));
|
||
}
|
||
|
||
// 检查是否为系统内置菜单
|
||
if (menu.IsSystem)
|
||
{
|
||
return Ok(ApiResponse<object>.Fail(400, "系统内置菜单不能删除"));
|
||
}
|
||
|
||
// 检查是否有子菜单
|
||
var hasChildren = await _context.Menus.AnyAsync(m => m.ParentId == id);
|
||
if (hasChildren)
|
||
{
|
||
return Ok(ApiResponse<object>.Fail(400, "请先删除子菜单"));
|
||
}
|
||
|
||
// 删除角色菜单关联
|
||
var roleMenus = await _context.RoleMenus.Where(rm => rm.MenuId == id).ToListAsync();
|
||
_context.RoleMenus.RemoveRange(roleMenus);
|
||
|
||
_context.Menus.Remove(menu);
|
||
await _context.SaveChangesAsync();
|
||
|
||
return Ok(ApiResponse<object>.Success(null, "菜单删除成功"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重置菜单为默认配置
|
||
/// </summary>
|
||
[Authorize]
|
||
[HttpPost("api/menu/reset")]
|
||
public async Task<ActionResult<ApiResponse<object>>> ResetMenus()
|
||
{
|
||
// 清空现有菜单和角色菜单关联
|
||
_context.RoleMenus.RemoveRange(_context.RoleMenus);
|
||
_context.Menus.RemoveRange(_context.Menus);
|
||
await _context.SaveChangesAsync();
|
||
|
||
// 重新创建默认菜单
|
||
var menus = new List<Menu>
|
||
{
|
||
// 仪表盘菜单(系统内置)
|
||
new() { Id = 1, Name = "Dashboard", Path = "/dashboard", Component = "/index/index", Title = "menus.dashboard.title", Icon = "ri:pie-chart-line", Sort = 1, Roles = "R_SUPER,R_ADMIN,R_USER", IsSystem = true },
|
||
new() { Id = 2, ParentId = 1, Name = "Console", Path = "console", Component = "/dashboard/console", Title = "menus.dashboard.console", KeepAlive = false, Sort = 1, Roles = "R_SUPER,R_ADMIN,R_USER", IsSystem = true },
|
||
|
||
// AMT 设备管理菜单(系统内置)
|
||
new() { Id = 5, Name = "AmtManage", Path = "/amt", Component = "/index/index", Title = "设备管理", Icon = "ri:computer-line", Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||
new() { Id = 6, ParentId = 5, Name = "AmtScan", Path = "scan", Component = "/amt/scan", Title = "网络扫描", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||
new() { Id = 7, ParentId = 5, Name = "AmtDevices", Path = "devices", Component = "/amt/devices", Title = "设备列表", KeepAlive = true, Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||
new() { Id = 8, ParentId = 5, Name = "AmtCredentials", Path = "credentials", Component = "/amt/credentials", Title = "AMT凭据", KeepAlive = true, Sort = 3, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||
new() { Id = 9, ParentId = 5, Name = "WindowsCredentials", Path = "windows-credentials", Component = "/amt/windows-credentials", Title = "Windows凭据", KeepAlive = true, Sort = 4, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||
|
||
// 系统管理菜单(系统内置)
|
||
new() { Id = 10, Name = "System", Path = "/system", Component = "/index/index", Title = "menus.system.title", Icon = "ri:user-3-line", Sort = 99, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||
new() { Id = 11, ParentId = 10, Name = "User", Path = "user", Component = "/system/user", Title = "menus.system.user", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||
new() { Id = 12, ParentId = 10, Name = "Role", Path = "role", Component = "/system/role", Title = "menus.system.role", KeepAlive = true, Sort = 2, Roles = "R_SUPER", IsSystem = true },
|
||
new() { Id = 13, ParentId = 10, Name = "UserCenter", Path = "user-center", Component = "/system/user-center", Title = "menus.system.userCenter", IsHide = true, KeepAlive = true, Sort = 3, Roles = "R_SUPER,R_ADMIN,R_USER", IsSystem = true },
|
||
new() { Id = 14, ParentId = 10, Name = "Menus", Path = "menu", Component = "/system/menu", Title = "menus.system.menu", KeepAlive = true, Sort = 4, Roles = "R_SUPER", IsSystem = true }
|
||
};
|
||
|
||
_context.Menus.AddRange(menus);
|
||
await _context.SaveChangesAsync();
|
||
|
||
// 重新分配角色菜单
|
||
var superRole = await _context.Roles.FirstAsync(r => r.RoleCode == "R_SUPER");
|
||
var adminRole = await _context.Roles.FirstAsync(r => r.RoleCode == "R_ADMIN");
|
||
var userRole = await _context.Roles.FirstAsync(r => r.RoleCode == "R_USER");
|
||
|
||
var allMenuIds = await _context.Menus.Select(m => m.Id).ToListAsync();
|
||
var adminMenuIds = await _context.Menus
|
||
.Where(m => m.Roles != null && (m.Roles.Contains("R_ADMIN") || m.Roles.Contains("R_USER")))
|
||
.Select(m => m.Id).ToListAsync();
|
||
var userMenuIds = await _context.Menus
|
||
.Where(m => m.Roles != null && m.Roles.Contains("R_USER"))
|
||
.Select(m => m.Id).ToListAsync();
|
||
|
||
var roleMenus = new List<RoleMenu>();
|
||
foreach (var menuId in allMenuIds)
|
||
roleMenus.Add(new RoleMenu { RoleId = superRole.Id, MenuId = menuId });
|
||
foreach (var menuId in adminMenuIds)
|
||
roleMenus.Add(new RoleMenu { RoleId = adminRole.Id, MenuId = menuId });
|
||
foreach (var menuId in userMenuIds)
|
||
roleMenus.Add(new RoleMenu { RoleId = userRole.Id, MenuId = menuId });
|
||
|
||
_context.RoleMenus.AddRange(roleMenus);
|
||
await _context.SaveChangesAsync();
|
||
|
||
return Ok(ApiResponse<object>.Success(null, "菜单已重置为默认配置"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加 AMT 设备管理菜单(如果不存在)
|
||
/// </summary>
|
||
[Authorize]
|
||
[HttpPost("api/menu/seed-amt")]
|
||
public async Task<ActionResult<ApiResponse<object>>> SeedAmtMenus()
|
||
{
|
||
// 检查是否已存在 AMT 菜单
|
||
if (await _context.Menus.AnyAsync(m => m.Name == "AmtManage"))
|
||
{
|
||
return Ok(ApiResponse<object>.Success(null, "AMT菜单已存在"));
|
||
}
|
||
|
||
// 获取当前最大 ID
|
||
var maxId = await _context.Menus.MaxAsync(m => (int?)m.Id) ?? 0;
|
||
var startId = Math.Max(maxId + 1, 5);
|
||
|
||
var amtMenus = new List<Menu>
|
||
{
|
||
new() { Name = "AmtManage", Path = "/amt", Component = "/index/index", Title = "设备管理", Icon = "ri:computer-line", Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||
};
|
||
|
||
_context.Menus.AddRange(amtMenus);
|
||
await _context.SaveChangesAsync();
|
||
|
||
// 获取刚创建的目录菜单 ID
|
||
var amtManageId = amtMenus[0].Id;
|
||
|
||
// 添加子菜单
|
||
var childMenus = new List<Menu>
|
||
{
|
||
new() { ParentId = amtManageId, Name = "AmtScan", Path = "scan", Component = "/amt/scan", Title = "网络扫描", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||
new() { ParentId = amtManageId, Name = "AmtDevices", Path = "devices", Component = "/amt/devices", Title = "设备列表", KeepAlive = true, Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||
new() { ParentId = amtManageId, Name = "AmtCredentials", Path = "credentials", Component = "/amt/credentials", Title = "AMT凭据", KeepAlive = true, Sort = 3, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||
new() { ParentId = amtManageId, Name = "WindowsCredentials", Path = "windows-credentials", Component = "/amt/windows-credentials", Title = "Windows凭据", KeepAlive = true, Sort = 4, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||
};
|
||
|
||
_context.Menus.AddRange(childMenus);
|
||
await _context.SaveChangesAsync();
|
||
|
||
// 为超级管理员和管理员分配菜单权限
|
||
var superRole = await _context.Roles.FirstOrDefaultAsync(r => r.RoleCode == "R_SUPER");
|
||
var adminRole = await _context.Roles.FirstOrDefaultAsync(r => r.RoleCode == "R_ADMIN");
|
||
|
||
var allNewMenuIds = new List<int> { amtManageId };
|
||
allNewMenuIds.AddRange(childMenus.Select(m => m.Id));
|
||
|
||
var roleMenus = new List<RoleMenu>();
|
||
foreach (var menuId in allNewMenuIds)
|
||
{
|
||
if (superRole != null)
|
||
roleMenus.Add(new RoleMenu { RoleId = superRole.Id, MenuId = menuId });
|
||
if (adminRole != null)
|
||
roleMenus.Add(new RoleMenu { RoleId = adminRole.Id, MenuId = menuId });
|
||
}
|
||
|
||
_context.RoleMenus.AddRange(roleMenus);
|
||
await _context.SaveChangesAsync();
|
||
|
||
return Ok(ApiResponse<object>.Success(null, "AMT菜单已添加成功"));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建菜单请求
|
||
/// </summary>
|
||
public class CreateMenuRequest
|
||
{
|
||
public int? ParentId { get; set; }
|
||
public string Name { get; set; } = string.Empty;
|
||
public string Path { get; set; } = string.Empty;
|
||
public string? Component { get; set; }
|
||
public string Title { get; set; } = string.Empty;
|
||
public string? Icon { get; set; }
|
||
public int Sort { get; set; }
|
||
public bool IsHide { get; set; }
|
||
public bool KeepAlive { get; set; }
|
||
public string? Link { get; set; }
|
||
public bool IsIframe { get; set; }
|
||
public List<string>? Roles { get; set; }
|
||
public bool AutoCreateComponent { get; set; } = true; // 是否自动创建组件文件
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新菜单请求
|
||
/// </summary>
|
||
public class UpdateMenuRequest
|
||
{
|
||
public int? ParentId { get; set; }
|
||
public string? Name { get; set; }
|
||
public string? Path { get; set; }
|
||
public string? Component { get; set; }
|
||
public string? Title { get; set; }
|
||
public string? Icon { get; set; }
|
||
public int? Sort { get; set; }
|
||
public bool? IsHide { get; set; }
|
||
public bool? KeepAlive { get; set; }
|
||
public string? Link { get; set; }
|
||
public bool? IsIframe { get; set; }
|
||
public List<string>? Roles { get; set; }
|
||
}
|