lvfengfree 5382685f21 fix: 修复远程桌面分享链接重定向问题
- 修复已登录用户访问 /remote/:token 路由被重定向到首页的问题
- 路由守卫优先检查静态路由,静态路由直接放行不走权限验证
- 后端生成的 accessUrl 使用 Hash 路由格式 (/#/remote/{token})
- 前端 remote-desktop-modal 中修正链接格式为 Hash 路由
- 新增远程桌面访问页面 /views/remote/index.vue
2026-01-20 19:52:37 +08:00

431 lines
18 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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; }
}