lvfengfree c53f658f91 feat: 菜单管理功能增强 - 自动创建Vue组件文件
后端改进:
- MenuController 添加自动创建 Vue 组件文件功能
- 创建菜单时自动生成对应的 .vue 文件模板
- 修复路径处理逻辑,确保子菜单使用相对路径
- 添加菜单名称唯一性检查,自动添加时间戳避免重复
- 修复 ViewsPath 配置路径
- 修复文件写入编码为 UTF-8

前端改进:
- 添加创建目录/子菜单的帮助说明
- 子菜单自动生成组件路径(如果用户未填写)
- 添加 autoCreateComponent 参数支持
- 优化菜单类型判断逻辑
2026-01-20 18:15:14 +08:00

362 lines
14 KiB
C#
Raw 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 },
// 系统管理菜单(系统内置)
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>
/// 创建菜单请求
/// </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; }
}