生产模块拆分为前缫/后缫;修复菜单卡顿与重复请求;支持 IIS 单站点部署

1. 生产模块按工序拆分为两个一级菜单
   - 生产-前缫(落丝记录)、生产-后缫(纤度检验、黑板检验)
   - 旧的「生产管理」(/prod) 目录及子菜单由种子自动清理,页面组件与权限标识保持不变
   - 消息中心跳转路径、首页快捷入口路径同步更新

2. 职工管理独立为一级菜单(原挂在「基础数据」目录下),并清理历史子菜单

3. 修复侧边栏菜单展开卡顿(基础数据等子项较多的目录尤其明显)
   - 去掉 Element Plus 的 height 过渡动画(会逐帧重排全部子项)
   - 收起时改用 height:0 + visibility:hidden 而非 display:none,子项提前完成布局
   - 菜单区独立滚动并预留滚动条宽度(scrollbar-gutter),消除展开时的横向抖动
   - 菜单项增加 contain 布局隔离,淡入动画改为容器级

4. 修复页面首次打开重复请求
   - 分页组件挂载时会触发一次 current-change,页码未变化时不再重复加载(18 个页面)
   - 职工管理去掉 onMounted 与默认选中部门的重复加载
   - 工种管理、原料庄口的注册表下拉由按 key 多次请求改为一次全量拉取

5. 后端增加前端静态托管与 SPA 路由回退,支持 IIS 单站点部署(前后端同源)
6. 新增部署脚本 deploy-iis.ps1(首次部署)与 update-iis.ps1(更新发布)
This commit is contained in:
yanpeng200212
2026-10-12 14:09:59 +08:00
parent 2ffb83830c
commit c3e2665257
47 changed files with 59270 additions and 109 deletions
@@ -60,12 +60,13 @@ public static class DbSeeder
// 6.6 F9 风格 RBAC 菜单(用户/角色/菜单/部门管理,写入新 sys_rbac_menus 表)
SeedSysRbacMenus(fsql, snowflake);
// 6.65 基础数据(工种 + 注册表配置 + 原料庄口 + 物资 + 往来单位,F9 导出数据幂等导入)
// 6.65 基础数据(工种 + 注册表配置 + 原料庄口 + 物资 + 往来单位 + 职工,F9 导出数据幂等导入)
WorkTypeSeedData.Seed(fsql, snowflake);
RegistrySeedData.Seed(fsql);
YuanliaoZhuangkouSeedData.Seed(fsql);
WuziSeedData.Seed(fsql);
GongyingshangSeedData.Seed(fsql);
EmployeeSeedData.Seed(fsql);
// 6.66 基础数据菜单(工种管理、账期管理、注册表、原料庄口、物资目录、往来单位)
SeedWorkTypeMenus(fsql, snowflake);
@@ -75,6 +76,12 @@ public static class DbSeeder
SeedWuziMenus(fsql, snowflake);
SeedGongyingshangMenus(fsql, snowflake);
// 6.67 职工管理(一级菜单,与"基础数据"平级)
SeedEmployeeTopMenu(fsql, snowflake);
// 6.68 生产模块(按工序拆分:生产-前缫 / 生产-后缫)
SeedProdMenus(fsql, snowflake);
// 6.7 为菜单创建权限单元(Permission),并修正旧菜单权限关联(统一权限模型)
FixMenuPermissions(fsql, snowflake);
@@ -175,7 +182,7 @@ public static class DbSeeder
{
Code = "TeyeFound", Name = "发现特野", Description = "纤度检验发现特野时自动推送提醒",
RequiredPermission = "", IsActive = true, CreatedBy = 0, Sort = 1,
Category = NotificationCategory.Notification, Icon = "Warning", RouteTemplate = "/prod/xdjy"
Category = NotificationCategory.Notification, Icon = "Warning", RouteTemplate = "/prod/houxian/xdjy"
}).ExecuteAffrows();
}
@@ -186,7 +193,7 @@ public static class DbSeeder
{
Code = "TdcFound", Name = "发现特大糙", Description = "黑板检验发现特大糙时自动推送提醒",
RequiredPermission = "", IsActive = true, CreatedBy = 0, Sort = 2,
Category = NotificationCategory.Notification, Icon = "Warning", RouteTemplate = "/prod/hbjy"
Category = NotificationCategory.Notification, Icon = "Warning", RouteTemplate = "/prod/houxian/hbjy"
}).ExecuteAffrows();
}
@@ -572,6 +579,203 @@ public static class DbSeeder
}
}
/// <summary>
/// 职工管理菜单:独立的一级菜单(原作为"基础数据"的子菜单)。
/// 会清理历史遗留的 /base/employee 子菜单,并规范一级菜单排序,避免同一页面出现两个入口。
/// </summary>
private static void SeedEmployeeTopMenu(IFreeSql fsql, ISnowflakeIdGenerator snowflake)
{
try
{
const string menuPath = "/employee";
const string menuIcon = "Avatar";
// 1) 清理历史:曾是"基础数据"下的子菜单
var legacy = fsql.Select<SysMenu>().Where(m => m.Path == "/base/employee").First();
if (legacy != null)
{
fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {legacy.Id}");
fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_menus WHERE id = {legacy.Id}");
Console.WriteLine("[DbSeeder] 已清理历史子菜单 /base/employee");
}
// 2) 创建/校正一级菜单
var menu = fsql.Select<SysMenu>().Where(m => m.Path == menuPath).First();
if (menu == null)
{
menu = new SysMenu(snowflake.CreateId(), "职工管理", menuIcon, MenuType_Enum.页面, null, null, 0, 4, 0);
menu.SetWebInfo(menuPath, "base/employee/index", "base:employee:list");
fsql.Insert(menu).ExecuteAffrows();
}
else if (!menu.IsActive || menu.IconSvg != menuIcon || menu.ParentId != 0)
{
fsql.Update<SysMenu>()
.Set(x => x.IconSvg, menuIcon)
.Set(x => x.IsActive, true)
.Set(x => x.ParentId, 0)
.Set(x => x.Level, 1)
.Where(x => x.Id == menu.Id)
.ExecuteAffrows();
}
// 3) 规范一级菜单顺序:考勤管理 / 基础数据 / 职工管理 / 生产管理 / 消息中心 / 系统管理
foreach (var (path, sort) in new (string Path, int Sort)[]
{
("/attendance", 2), ("/base", 3), ("/employee", 4),
("/prod", 5), ("/notification", 6), ("/sys", 7)
})
{
fsql.Update<SysMenu>()
.Set(x => x.SortOrder, sort)
.Where(x => x.ParentId == 0 && x.Path == path && x.SortOrder != sort)
.ExecuteAffrows();
}
// 4) admin 角色授权(后续 FixMenuPermissions 会把 permission_id 修正为权限单元 Id)
var adminRole = fsql.Select<SysRole>().Where(r => r.Code == "admin").First();
if (adminRole != null)
{
var any = fsql.Select<SysRolePermission>()
.Where(x => x.RoleId == adminRole.Id && x.PermissionId == menu.Id)
.First();
if (any == null)
{
fsql.Insert(new SysRolePermission(snowflake.CreateId(), adminRole.Id, menu.Id)).ExecuteAffrows();
}
}
Console.WriteLine("[DbSeeder] 职工管理一级菜单已就绪");
}
catch (Exception ex)
{
Console.WriteLine($"[DbSeeder] 职工管理菜单种子跳过:{ex}");
}
}
/// <summary>
/// 生产模块菜单:按缫丝工序拆分为「生产-前缫」(落丝记录)与「生产-后缫」(纤度检验、黑板检验)两个一级菜单。
/// 会清理旧的「生产管理」(/prod) 目录及其子菜单;页面组件与权限标识保持不变。
/// </summary>
private static void SeedProdMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake)
{
try
{
// 1) 清理旧「生产管理」结构(精确匹配历史路径)
foreach (var legacyPath in new[] { "/prod", "/prod/lsjl", "/prod/xdjy", "/prod/hbjy" })
{
var legacy = fsql.Select<SysMenu>().Where(m => m.Path == legacyPath).First();
if (legacy == null) continue;
fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {legacy.Id}");
fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_menus WHERE id = {legacy.Id}");
Console.WriteLine($"[DbSeeder] 已清理旧生产菜单 {legacyPath}");
}
// 2) 两个一级目录 + 各自页面菜单
EnsureTopMenuWithChildren(fsql, snowflake, "/prod/qianxian", "生产-前缫", "Cpu", 5,
new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[]
{
("落丝记录", "/prod/qianxian/lsjl", "prod/lsjl/index", "prod:lsjl:list", "Document", 1)
});
EnsureTopMenuWithChildren(fsql, snowflake, "/prod/houxian", "生产-后缫", "DataLine", 6,
new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[]
{
("纤度检验", "/prod/houxian/xdjy", "prod/xdjy/index", "prod:xdjy:list", "EditPen", 1),
("黑板检验", "/prod/houxian/hbjy", "prod/hbjy/index", "prod:hbjy:list", "Notebook", 2)
});
// 3) 规范一级菜单顺序
foreach (var (path, sort) in new (string Path, int Sort)[]
{
("/attendance", 2), ("/base", 3), ("/employee", 4),
("/prod/qianxian", 5), ("/prod/houxian", 6), ("/notification", 7), ("/sys", 8)
})
{
fsql.Update<SysMenu>()
.Set(x => x.SortOrder, sort)
.Where(x => x.ParentId == 0 && x.Path == path && x.SortOrder != sort)
.ExecuteAffrows();
}
// 4) 消息跳转路径同步到新路由
fsql.Ado.ExecuteNonQuery("UPDATE NotificationTypes SET RouteTemplate = '/prod/houxian/xdjy' WHERE Code = 'TeyeFound'");
fsql.Ado.ExecuteNonQuery("UPDATE NotificationTypes SET RouteTemplate = '/prod/houxian/hbjy' WHERE Code = 'TdcFound'");
Console.WriteLine("[DbSeeder] 生产模块菜单已就绪(生产-前缫 / 生产-后缫)");
}
catch (Exception ex)
{
Console.WriteLine($"[DbSeeder] 生产菜单种子跳过:{ex}");
}
}
/// <summary>
/// 确保一个一级目录菜单及其下属页面菜单存在(含父级、图标、启用状态校正),并给 admin 角色授权。
/// 注意:图标名必须是 Element Plus 图标组件名。
/// </summary>
private static void EnsureTopMenuWithChildren(IFreeSql fsql, ISnowflakeIdGenerator snowflake,
string dirPath, string dirName, string dirIcon, int dirSort,
(string Name, string Path, string Component, string Permission, string Icon, int Sort)[] children)
{
var dir = fsql.Select<SysMenu>().Where(m => m.Path == dirPath).First();
if (dir == null)
{
dir = new SysMenu(snowflake.CreateId(), dirName, dirIcon, MenuType_Enum.分组, null, null, 0, dirSort, 0);
dir.SetWebInfo(dirPath, "", "");
fsql.Insert(dir).ExecuteAffrows();
}
else if (!dir.IsActive || dir.IconSvg != dirIcon || dir.Name != dirName || dir.ParentId != 0)
{
fsql.Update<SysMenu>()
.Set(x => x.Name, dirName)
.Set(x => x.IconSvg, dirIcon)
.Set(x => x.IsActive, true)
.Set(x => x.ParentId, 0)
.Set(x => x.Level, 1)
.Where(x => x.Id == dir.Id)
.ExecuteAffrows();
}
var adminRoleId = fsql.Select<SysRole>().Where(r => r.Code == "admin").First()?.Id ?? 0;
EnsureMenuPermission(fsql, snowflake, adminRoleId, dir.Id);
foreach (var (name, path, component, permission, icon, sort) in children)
{
var child = fsql.Select<SysMenu>().Where(m => m.Path == path).First();
if (child == null)
{
child = new SysMenu(snowflake.CreateId(), name, icon, MenuType_Enum.页面, null, null, dir.Id, sort, 1);
child.SetWebInfo(path, component, permission);
fsql.Insert(child).ExecuteAffrows();
}
else if (!child.IsActive || child.IconSvg != icon || child.ParentId != dir.Id)
{
fsql.Update<SysMenu>()
.Set(x => x.IconSvg, icon)
.Set(x => x.IsActive, true)
.Set(x => x.ParentId, dir.Id)
.Set(x => x.Level, 2)
.Where(x => x.Id == child.Id)
.ExecuteAffrows();
}
EnsureMenuPermission(fsql, snowflake, adminRoleId, child.Id);
}
}
/// <summary>确保指定菜单已授权给角色(后续 FixMenuPermissions 会把 permission_id 修正为权限单元 Id)</summary>
private static void EnsureMenuPermission(IFreeSql fsql, ISnowflakeIdGenerator snowflake, long roleId, long menuId)
{
if (roleId <= 0 || menuId <= 0) return;
var any = fsql.Select<SysRolePermission>()
.Where(x => x.RoleId == roleId && x.PermissionId == menuId)
.First();
if (any == null)
{
fsql.Insert(new SysRolePermission(snowflake.CreateId(), roleId, menuId)).ExecuteAffrows();
}
}
/// <summary>
/// 确保"基础数据"目录下的菜单存在(含启用状态与图标校正),并给 admin 角色授权。
/// 注意:图标名必须是 Element Plus 图标组件名,前端按图标名渲染菜单图标。