Compare commits

...

13 Commits
0.6.5 ... main

Author SHA1 Message Date
xuzeyu
5e032e3733 Merge pull request #136 from AIDotNet/feature_role
Feature role
2025-11-06 10:32:08 +08:00
xuzeyu
abed362ff2 重构和优化领域模型属性与关联类
重命名 `AntSK.Domain.Repositories.Kmss` 的多个属性为 `KmsDetails`,以更清晰地表达其含义,例如将 `Kmss.Icon` 替换为 `KmsDetails.FileName`,`Kmss.Name` 替换为 `KmsDetails.Url` 等。

新增 `RolePermissions` 类及其相关属性,用于表示角色权限关联表。

将 `RolePermissions` 替换为 `UserRoles`,并调整相关属性名称和描述以匹配用户角色关联表的语义。

删除了 `UserRoles` 类及其相关属性,移除冗余定义。

保留部分未修改的成员,确保现有功能的完整性。
2025-11-06 10:29:38 +08:00
copilot-swe-agent[bot]
1a3881e7a4 Fix role code consistency and add RBAC documentation
Co-authored-by: xuzeyu91 <26290929+xuzeyu91@users.noreply.github.com>
2025-11-06 02:08:14 +00:00
copilot-swe-agent[bot]
ea8dd21478 Add role management menu entry and update seed data
Co-authored-by: xuzeyu91 <26290929+xuzeyu91@users.noreply.github.com>
2025-11-06 02:05:38 +00:00
copilot-swe-agent[bot]
5ec918a2f9 Add role-based authorization entities, repositories and UI pages
Co-authored-by: xuzeyu91 <26290929+xuzeyu91@users.noreply.github.com>
2025-11-06 02:04:11 +00:00
copilot-swe-agent[bot]
08d3a0aa3d Initial analysis - Planning role-based authorization implementation
Co-authored-by: xuzeyu91 <26290929+xuzeyu91@users.noreply.github.com>
2025-11-06 01:57:38 +00:00
copilot-swe-agent[bot]
4044355ae7 Initial plan 2025-11-06 01:52:10 +00:00
zyxucp
f03649146f 调整聊天气泡尾部样式以优化外观
修改了`.chat-bubble.received::after`和`.chat-bubble.sent::after`伪元素的定位方式,将`bottom`替换为`top`,并调整了宽度从`16px`到`18px`。更新了`clip-path`属性以改进尾部形状,同时调整了`left`和`right`的偏移值以优化位置。
2025-10-05 16:11:24 +08:00
zyxucp
15fd59571f 优化 Chat 和 ChatView 样式及布局
重构了 `Chat.razor` 和 `ChatView.razor` 的样式:
- 替换内联样式为模块化 CSS 类(如 `chat-card`)。
- 优化卡片布局,新增容器类 `chat-card__content`。
- 改进滚动条样式,调整宽度、颜色和圆角。
- 为 `#chat` 添加模糊背景伪元素和阴影效果。
- 删除冗余样式规则,提升代码可维护性。
2025-10-05 16:03:18 +08:00
zyxucp
f8399887ce 优化 ChatView 组件的样式和功能
- 为 `<Image>` 组件新增 `Preview="false"` 属性,禁用头像图片预览。
- 调整输入框结构,新增 `<div class="input-bar__field">` 容器,优化布局。
- 更新 `.avatar` 样式,确保头像图片比例正确并裁剪为圆形。
- 优化 `.input-bar` 和 `.ant-input` 样式,提升输入框的视觉效果和交互体验。
- 新增 `.input-bar__field` 样式,增强输入框容器的布局和样式控制。
2025-10-05 15:40:56 +08:00
zyxucp
97548b0d2b 优化聊天界面样式与功能支持
更新聊天气泡样式,增加圆角、阴影及悬停动画,分离发送与接收消息的样式,改进消息元信息布局。
支持深色模式,优化滚动条样式,增强小屏设备的响应式设计。
调整文件上传组件样式,更新颜色变量,替换背景为单色设计,提升层次感。
改进Markdown与代码块样式,增加`.think`类样式调整,统一消息布局。
新增消息气泡弹出动画,优化输入栏样式,整体提升用户体验与视觉效果。
2025-10-05 15:31:49 +08:00
xuzeyu
d6b2c3a08b Update README.md 2025-10-04 23:34:00 +08:00
xuzeyu
3342378feb Update README.md 2025-09-02 15:23:03 +08:00
27 changed files with 1431 additions and 128 deletions

View File

@@ -386,7 +386,7 @@ version: '3.8'
services:
antsk:
container_name: antsk
image: registry.cn-hangzhou.aliyuncs.com/AIDotNet/antsk:v0.6.0
image: registry.cn-hangzhou.aliyuncs.com/AIDotNet/antsk:v0.6.5
ports:
- 5000:5000
networks:
@@ -604,7 +604,3 @@ services.AddSingleton<IChatCompletion, CustomChatCompletion>();
3. 满足以上要求
## ☎️联系我
如有任何问题或建议请通过以下方式关注我的公众号《许泽宇的技术分享》发消息与我联系我们也有AIDotnet交流群可以发送进群等消息然后我会拉你进交流群
另外您也可以通过邮箱与我联系antskpro@qq.com

140
docs/RBAC_README.md Normal file
View File

@@ -0,0 +1,140 @@
# 角色基础授权系统 (Role-Based Access Control)
## 概述
本系统实现了完整的角色基础授权功能,支持将权限绑定到角色,角色再绑定给用户,提供灵活的权限管理能力。
## 数据库结构
### 核心表
1. **Roles (角色表)**
- Id: 角色ID (主键)
- Name: 角色名称
- Code: 角色编码 (用于授权验证)
- Description: 角色描述
- IsEnabled: 是否启用
- CreateTime: 创建时间
2. **Permissions (权限表)**
- Id: 权限ID (主键)
- Name: 权限名称
- Code: 权限编码 (用于授权验证)
- Type: 权限类型 (Menu-菜单权限, Operation-操作权限)
- Description: 权限描述
- CreateTime: 创建时间
3. **RolePermissions (角色权限关联表)**
- Id: 关联ID (主键)
- RoleId: 角色ID
- PermissionId: 权限ID
- CreateTime: 创建时间
4. **UserRoles (用户角色关联表)**
- Id: 关联ID (主键)
- UserId: 用户ID
- RoleId: 角色ID
- CreateTime: 创建时间
## 默认角色和权限
系统首次运行时会自动初始化以下角色和权限:
### 默认角色
1. **AntSKAdmin (管理员)**
- 拥有系统所有权限
- 可以管理用户、角色、权限等
2. **AntSKUser (普通用户)**
- 拥有基本功能权限
- 包括:聊天、应用、知识库
### 默认权限
系统包含以下菜单权限:
- chat: 聊天
- app: 应用
- kms: 知识库
- plugins.apilist: API管理
- plugins.funlist: 函数管理
- modelmanager.modellist: 模型管理
- setting.user: 用户管理
- setting.role: 角色管理
- setting.chathistory: 聊天记录
- setting.delkms: 删除向量表
## 使用说明
### 1. 角色管理
访问 `/setting/rolelist` 可以进行角色管理:
- 查看所有角色
- 创建新角色
- 编辑角色信息
- 为角色分配权限
- 删除角色
### 2. 用户管理
访问 `/setting/userlist` 可以进行用户管理:
- 创建用户时可以分配角色
- 编辑用户时可以修改角色分配
- 用户可以拥有多个角色
### 3. 授权验证
系统支持两种授权方式:
**方式一:基于角色的授权**
```csharp
@attribute [Authorize(Roles = "AntSKAdmin")]
```
**方式二:基于多角色的授权**
```csharp
@attribute [Authorize(Roles = "AntSKAdmin,AntSKUser")]
```
## 技术实现
### 认证流程
1. 用户登录时,系统从数据库加载用户的角色和权限
2. 将所有角色添加到用户的Claims中
3. 将权限列表存储到UserSession中供后续使用
4. 支持多个角色同时验证
### 向后兼容
- 保留了原有的MenuRole字段支持逐步迁移
- 硬编码的管理员账号继续使用AntSKAdmin角色
- 新用户如果没有分配角色默认使用AntSKUser角色
## 安全性
1. **角色验证**: 只有AntSKAdmin角色可以访问角色和用户管理页面
2. **级联删除**: 删除角色时会自动删除相关的角色权限和用户角色关联
3. **启用状态**: 角色支持启用/禁用状态,禁用的角色不会加载权限
4. **多层级保护**: 数据库层、业务层、UI层都有权限验证
## 扩展建议
1. **操作权限**: 可以在Permissions表中添加Type为"Operation"的权限,用于控制具体操作
2. **权限缓存**: 可以考虑添加权限缓存机制,提高性能
3. **审计日志**: 可以添加角色和权限变更的审计日志
4. **批量操作**: 可以添加批量分配角色、批量分配权限的功能
## 常见问题
**Q: 如何添加新的权限?**
A: 可以通过代码在InitRolesAndPermissions方法中添加或者后续可以开发权限管理UI。
**Q: 用户可以有多个角色吗?**
A: 可以。系统支持一个用户拥有多个角色,最终拥有所有角色的权限并集。
**Q: 如何处理权限冲突?**
A: 系统采用"允许优先"策略,只要用户的任一角色拥有某项权限,用户就拥有该权限。
**Q: 删除角色会影响已登录的用户吗?**
A: 会的。用户下次登录时会重新加载角色和权限,已删除的角色将不再生效。

View File

@@ -836,6 +836,126 @@
部署名azure需要使用
</summary>
</member>
<member name="T:AntSK.Domain.Repositories.Permissions">
<summary>
权限表
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.Permissions.Id">
<summary>
权限ID
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.Permissions.Name">
<summary>
权限名称
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.Permissions.Code">
<summary>
权限编码
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.Permissions.Type">
<summary>
权限类型Menu-菜单权限, Operation-操作权限)
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.Permissions.Description">
<summary>
权限描述
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.Permissions.CreateTime">
<summary>
创建时间
</summary>
</member>
<member name="T:AntSK.Domain.Repositories.RolePermissions">
<summary>
角色权限关联表
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.RolePermissions.Id">
<summary>
关联ID
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.RolePermissions.RoleId">
<summary>
角色ID
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.RolePermissions.PermissionId">
<summary>
权限ID
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.RolePermissions.CreateTime">
<summary>
创建时间
</summary>
</member>
<member name="T:AntSK.Domain.Repositories.Roles">
<summary>
角色表
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.Roles.Id">
<summary>
角色ID
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.Roles.Name">
<summary>
角色名称
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.Roles.Code">
<summary>
角色编码
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.Roles.Description">
<summary>
角色描述
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.Roles.IsEnabled">
<summary>
是否启用
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.Roles.CreateTime">
<summary>
创建时间
</summary>
</member>
<member name="T:AntSK.Domain.Repositories.UserRoles">
<summary>
用户角色关联表
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.UserRoles.Id">
<summary>
关联ID
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.UserRoles.UserId">
<summary>
用户ID
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.UserRoles.RoleId">
<summary>
角色ID
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.UserRoles.CreateTime">
<summary>
创建时间
</summary>
</member>
<member name="P:AntSK.Domain.Repositories.Users.No">
<summary>
工号,用于登陆

View File

@@ -81,10 +81,98 @@ namespace AntSK.Domain.Common.DependencyInjection
llamafactoryStart.Value = "false";
_dic_Repository.Insert(llamafactoryStart);
}
// 初始化角色和权限
InitRolesAndPermissions(scope.ServiceProvider);
_logger.LogInformation("初始化数据库初始数据完成");
}
return app;
}
private static void InitRolesAndPermissions(IServiceProvider serviceProvider)
{
var _roles_Repository = serviceProvider.GetRequiredService<IRoles_Repositories>();
var _permissions_Repository = serviceProvider.GetRequiredService<IPermissions_Repositories>();
var _rolePermissions_Repository = serviceProvider.GetRequiredService<IRolePermissions_Repositories>();
// 检查是否已经初始化
if (_roles_Repository.IsAny(r => r.Code == "AntSKAdmin"))
{
return;
}
// 创建管理员角色
var adminRole = new Roles
{
Id = Guid.NewGuid().ToString(),
Name = "管理员",
Code = "AntSKAdmin",
Description = "系统管理员,拥有所有权限",
IsEnabled = true,
CreateTime = DateTime.Now
};
_roles_Repository.Insert(adminRole);
// 创建普通用户角色
var userRole = new Roles
{
Id = Guid.NewGuid().ToString(),
Name = "普通用户",
Code = "AntSKUser",
Description = "普通用户,拥有基本功能权限",
IsEnabled = true,
CreateTime = DateTime.Now
};
_roles_Repository.Insert(userRole);
// 创建菜单权限
var menuPermissions = new List<Permissions>
{
new Permissions { Id = Guid.NewGuid().ToString(), Name = "聊天", Code = "chat", Type = "Menu", Description = "聊天功能权限" },
new Permissions { Id = Guid.NewGuid().ToString(), Name = "应用", Code = "app", Type = "Menu", Description = "应用管理权限" },
new Permissions { Id = Guid.NewGuid().ToString(), Name = "知识库", Code = "kms", Type = "Menu", Description = "知识库管理权限" },
new Permissions { Id = Guid.NewGuid().ToString(), Name = "API管理", Code = "plugins.apilist", Type = "Menu", Description = "API管理权限" },
new Permissions { Id = Guid.NewGuid().ToString(), Name = "函数管理", Code = "plugins.funlist", Type = "Menu", Description = "函数管理权限" },
new Permissions { Id = Guid.NewGuid().ToString(), Name = "模型管理", Code = "modelmanager.modellist", Type = "Menu", Description = "模型管理权限" },
new Permissions { Id = Guid.NewGuid().ToString(), Name = "用户管理", Code = "setting.user", Type = "Menu", Description = "用户管理权限" },
new Permissions { Id = Guid.NewGuid().ToString(), Name = "角色管理", Code = "setting.role", Type = "Menu", Description = "角色管理权限" },
new Permissions { Id = Guid.NewGuid().ToString(), Name = "聊天记录", Code = "setting.chathistory", Type = "Menu", Description = "聊天记录权限" },
new Permissions { Id = Guid.NewGuid().ToString(), Name = "删除向量表", Code = "setting.delkms", Type = "Menu", Description = "删除向量表权限" }
};
foreach (var permission in menuPermissions)
{
_permissions_Repository.Insert(permission);
}
// 为管理员角色分配所有权限
foreach (var permission in menuPermissions)
{
_rolePermissions_Repository.Insert(new RolePermissions
{
Id = Guid.NewGuid().ToString(),
RoleId = adminRole.Id,
PermissionId = permission.Id,
CreateTime = DateTime.Now
});
}
// 为普通用户角色分配基本权限(聊天、应用、知识库)
var basicPermissions = menuPermissions.Where(p => p.Code == "chat" || p.Code == "app" || p.Code == "kms").ToList();
foreach (var permission in basicPermissions)
{
_rolePermissions_Repository.Insert(new RolePermissions
{
Id = Guid.NewGuid().ToString(),
RoleId = userRole.Id,
PermissionId = permission.Id,
CreateTime = DateTime.Now
});
}
_logger.LogInformation("初始化角色和权限完成");
}
/// <summary>
/// 加载数据库的插件
/// </summary>

View File

@@ -0,0 +1,8 @@
using AntSK.Domain.Repositories.Base;
namespace AntSK.Domain.Repositories
{
public interface IPermissions_Repositories : IRepository<Permissions>
{
}
}

View File

@@ -0,0 +1,46 @@
using SqlSugar;
using System.ComponentModel.DataAnnotations;
namespace AntSK.Domain.Repositories
{
/// <summary>
/// 权限表
/// </summary>
[SugarTable("Permissions")]
public partial class Permissions
{
/// <summary>
/// 权限ID
/// </summary>
[SugarColumn(IsPrimaryKey = true)]
public string Id { get; set; }
/// <summary>
/// 权限名称
/// </summary>
[Required]
public string Name { get; set; }
/// <summary>
/// 权限编码
/// </summary>
[Required]
public string Code { get; set; }
/// <summary>
/// 权限类型Menu-菜单权限, Operation-操作权限)
/// </summary>
[Required]
public string Type { get; set; }
/// <summary>
/// 权限描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; } = DateTime.Now;
}
}

View File

@@ -0,0 +1,10 @@
using AntSK.Domain.Common.DependencyInjection;
using AntSK.Domain.Repositories.Base;
namespace AntSK.Domain.Repositories
{
[ServiceDescription(typeof(IPermissions_Repositories), ServiceLifetime.Scoped)]
public class Permissions_Repositories : Repository<Permissions>, IPermissions_Repositories
{
}
}

View File

@@ -0,0 +1,8 @@
using AntSK.Domain.Repositories.Base;
namespace AntSK.Domain.Repositories
{
public interface IRoles_Repositories : IRepository<Roles>
{
}
}

View File

@@ -0,0 +1,45 @@
using SqlSugar;
using System.ComponentModel.DataAnnotations;
namespace AntSK.Domain.Repositories
{
/// <summary>
/// 角色表
/// </summary>
[SugarTable("Roles")]
public partial class Roles
{
/// <summary>
/// 角色ID
/// </summary>
[SugarColumn(IsPrimaryKey = true)]
public string Id { get; set; }
/// <summary>
/// 角色名称
/// </summary>
[Required]
public string Name { get; set; }
/// <summary>
/// 角色编码
/// </summary>
[Required]
public string Code { get; set; }
/// <summary>
/// 角色描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 是否启用
/// </summary>
public bool IsEnabled { get; set; } = true;
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; } = DateTime.Now;
}
}

View File

@@ -0,0 +1,10 @@
using AntSK.Domain.Common.DependencyInjection;
using AntSK.Domain.Repositories.Base;
namespace AntSK.Domain.Repositories
{
[ServiceDescription(typeof(IRoles_Repositories), ServiceLifetime.Scoped)]
public class Roles_Repositories : Repository<Roles>, IRoles_Repositories
{
}
}

View File

@@ -0,0 +1,8 @@
using AntSK.Domain.Repositories.Base;
namespace AntSK.Domain.Repositories
{
public interface IRolePermissions_Repositories : IRepository<RolePermissions>
{
}
}

View File

@@ -0,0 +1,32 @@
using SqlSugar;
namespace AntSK.Domain.Repositories
{
/// <summary>
/// 角色权限关联表
/// </summary>
[SugarTable("RolePermissions")]
public partial class RolePermissions
{
/// <summary>
/// 关联ID
/// </summary>
[SugarColumn(IsPrimaryKey = true)]
public string Id { get; set; }
/// <summary>
/// 角色ID
/// </summary>
public string RoleId { get; set; }
/// <summary>
/// 权限ID
/// </summary>
public string PermissionId { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; } = DateTime.Now;
}
}

View File

@@ -0,0 +1,10 @@
using AntSK.Domain.Common.DependencyInjection;
using AntSK.Domain.Repositories.Base;
namespace AntSK.Domain.Repositories
{
[ServiceDescription(typeof(IRolePermissions_Repositories), ServiceLifetime.Scoped)]
public class RolePermissions_Repositories : Repository<RolePermissions>, IRolePermissions_Repositories
{
}
}

View File

@@ -0,0 +1,8 @@
using AntSK.Domain.Repositories.Base;
namespace AntSK.Domain.Repositories
{
public interface IUserRoles_Repositories : IRepository<UserRoles>
{
}
}

View File

@@ -0,0 +1,32 @@
using SqlSugar;
namespace AntSK.Domain.Repositories
{
/// <summary>
/// 用户角色关联表
/// </summary>
[SugarTable("UserRoles")]
public partial class UserRoles
{
/// <summary>
/// 关联ID
/// </summary>
[SugarColumn(IsPrimaryKey = true)]
public string Id { get; set; }
/// <summary>
/// 用户ID
/// </summary>
public string UserId { get; set; }
/// <summary>
/// 角色ID
/// </summary>
public string RoleId { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; } = DateTime.Now;
}
}

View File

@@ -0,0 +1,10 @@
using AntSK.Domain.Common.DependencyInjection;
using AntSK.Domain.Repositories.Base;
namespace AntSK.Domain.Repositories
{
[ServiceDescription(typeof(IUserRoles_Repositories), ServiceLifetime.Scoped)]
public class UserRoles_Repositories : Repository<UserRoles>, IUserRoles_Repositories
{
}
}

View File

@@ -4,5 +4,7 @@
{
public string UserName { get; set; }
public string Role { get; set; }
public List<string>? Roles { get; set; }
public List<string>? Permissions { get; set; }
}
}

View File

@@ -10,7 +10,7 @@
<GridRow Gutter="(16, 16)">
<GridCol Span="14">
<Card Style="height:75vh;overflow: auto;">
<Card Class="chat-card">
<TitleTemplate>
<Icon Type="setting" /> 选择应用
<Select DataSource="@_list"
@@ -23,19 +23,21 @@
<a href="@( NavigationManager.BaseUri + "openchat/" + AppId)" target="_blank">分享使用</a>
</TitleTemplate>
<Body>
@if (!string.IsNullOrEmpty(AppId))
{
<Watermark Content="AntSK">
<ChatView AppId="@AppId" ShowTitle=false OnRelevantSources="OnRelevantSources"></ChatView>
</Watermark>
}
<div class="chat-card__content">
@if (!string.IsNullOrEmpty(AppId))
{
<Watermark Content="AntSK" Class="chat-card__watermark">
<ChatView AppId="@AppId" ShowTitle=false OnRelevantSources="OnRelevantSources"></ChatView>
</Watermark>
}
</div>
</Body>
</Card>
</GridCol>
<GridCol Span="10">
<Card Style="height: 75vh;overflow: auto;">
<TitleTemplate>
<Icon Type="search" /> 调试结果
<Icon Type="search" /> 知识溯源
</TitleTemplate>
<Extra>
@@ -62,13 +64,45 @@
</GridRow>
<style>
#chat {
height: calc(75vh - 120px);
.chat-card {
height: 75vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
margin: 0;
overflow: visible;
min-height: 0;
}
.chat-card .ant-card-body {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
padding: 5px;
box-sizing: border-box;
}
.chat-card__content {
position: relative;
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
min-height: 0;
}
.chat-card__watermark {
flex: 1 1 auto;
display: flex;
width: 100%;
height: 100%;
min-height: 0;
}
.chat-card__watermark > * {
flex: 1 1 auto;
min-width: 0;
height: 100%;
}
body {

View File

@@ -11,46 +11,54 @@
{
<PageHeader Class="site-page-header" Title="@app.Name" Subtitle="@app.Describe" />
}
<div id="scrollDiv" style="flex:1; width:100%; overflow-y:auto; overflow-x:hidden; padding:16px;">
<div id="scrollDiv" class="chat-scroll">
<Virtualize Items="@(MessageList.OrderBy(o => o.CreateTime).ToList())" Context="item">
@if (item.IsSend)
{
<GridRow>
<GridRow Class="message-row" Gutter="(8,8)">
<GridCol Span="23">
<div class="chat-bubble sent">
<Popover Title="@item.CreateTime.ToString()">
<Unbound>
<Flex Vertical RefBack="context">
@if (item.FileName != null)
{
<p class="message-file">
<Upload DefaultFileList="[new(){ FileName= item.FileName }]" />
</p>
}
<p class="bubble-text">@(item.Context)</p>
</Flex>
</Unbound>
</Popover>
<div class="message-shell sent">
<div class="chat-bubble sent">
<Popover Title="@item.CreateTime.ToString()">
<Unbound>
<Flex Vertical RefBack="context" Class="bubble-body">
@if (item.FileName != null)
{
<p class="message-file">
<Upload DefaultFileList="[new(){ FileName= item.FileName }]" />
</p>
}
<p class="bubble-text">@(item.Context)</p>
</Flex>
</Unbound>
</Popover>
</div>
<div class="message-meta meta-right">
<span>@item.CreateTime.ToString("HH:mm")</span>
<Icon Class="meta-action" Type="copy" Theme="outline" OnClick="async () =>await OnCopyAsync(item)" />
</div>
</div>
<div class="message-meta meta-right">@item.CreateTime.ToString("HH:mm")</div>
<Icon Style="float:right;margin-top:10px;" Type="copy" Theme="outline" OnClick="async () =>await OnCopyAsync(item)" />
</GridCol>
<GridCol Span="1">
<Image Class="avatar" Width="28px" Height="28px" Src="./assets/KDpgvguMpGfqaHPjicRK.svg" />
<GridCol Span="1" Class="avatar-col">
<Image Class="avatar" Width="32px" Height="32px" Src="./assets/KDpgvguMpGfqaHPjicRK.svg" Preview="false" />
</GridCol>
</GridRow>
}
else
{
<GridRow>
<GridCol Span="1">
<Image Class="avatar" Width="28px" Height="28px" Style="margin-top:10px;" Src="./assets/method-draw-image.svg" />
<GridRow Class="message-row" Gutter="(8,8)">
<GridCol Span="1" Class="avatar-col">
<Image Class="avatar" Width="32px" Height="32px" Src="./assets/method-draw-image.svg" Preview="false" />
</GridCol>
<GridCol Span="23">
<div class="chat-bubble received">
@((MarkupString)(item.Context))
<div class="message-shell received">
<div class="chat-bubble received">
@((MarkupString)(item.Context))
</div>
<div class="message-meta meta-left">
<span>@item.CreateTime.ToString("HH:mm")</span>
</div>
</div>
<div class="message-meta meta-left">@item.CreateTime.ToString("HH:mm")</div>
</GridCol>
</GridRow>
}
@@ -63,8 +71,10 @@
<Upload DefaultFileList="fileList" OnRemove="HandleFileRemove" />
</Flex>
}
<Flex Class="input-bar" Justify="end">
<AntDesign.Input @bind-Value="@(_messageInput)" DebounceMilliseconds="@(-1)" Placeholder="输入消息回车发送" OnPressEnter="@(async () => await OnSendAsync())" Disabled="@Sendding"></AntDesign.Input>
<Flex Class="input-bar" Justify="end" Align="center">
<div class="input-bar__field">
<AntDesign.Input @bind-Value="@(_messageInput)" DebounceMilliseconds="@(-1)" Placeholder="输入消息回车发送" OnPressEnter="@(async () => await OnSendAsync())" Disabled="@Sendding"></AntDesign.Input>
</div>
@if (app.EmbeddingModelID != null)
{
<Upload Action="@("api/File/UploadFile")"
@@ -83,130 +93,371 @@
<style>
:root {
--bg-start: #f7f9fc;
--bg-end: #eef2f7;
--surface-base: #ffffff;
--surface-elevated: #f5f7fb;
--border-subtle: rgba(15, 23, 42, 0.07);
--shadow-soft: 0 16px 28px rgba(15, 23, 42, 0.08);
--primary-color: var(--ant-primary-color, #1677ff);
--primary-soft: rgba(22, 119, 255, 0.12);
--bubble-recv: #ffffff;
--bubble-sent: #daf8cb;
--bubble-border: rgba(0,0,0,0.06);
--text-secondary: #8a8f98;
--bubble-border-sent: rgba(22, 119, 255, 0.25);
--text-primary: #1f2329;
--text-secondary: #7a808a;
}
@@supports (color: color-mix(in srgb, white 50%, black 50%)) {
:root {
--bubble-sent: color-mix(in srgb, var(--primary-color) 28%, white);
}
}
#chat {
position: relative;
isolation: isolate;
z-index: 0;
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
height: 100%;
min-height: 65vh;
background: linear-gradient(180deg, var(--bg-start), var(--bg-end));
border-radius: 8px;
padding: 8px 8px 0 8px;
width: 100%;
min-height: min(65vh, 100%);
max-height: 100%;
background: var(--surface-base);
border-radius: 16px 16px 16px 16px;
padding: 12px 12px 0 12px;
box-sizing: border-box;
border: 1px solid var(--border-subtle);
}
#scrollDiv {
background: transparent;
border-radius: 8px;
#chat::after {
content: "";
position: absolute;
left: 32px;
right: 32px;
bottom: -18px;
height: 40px;
background: rgba(15, 23, 42, 0.22);
filter: blur(24px);
border-radius: 50%;
pointer-events: none;
z-index: -1;
}
.chat-scroll {
flex: 1;
width: 100%;
min-height: 0;
max-height: 100%;
overflow-y: auto;
overflow-x: hidden;
padding: 12px 16px 20px;
background: var(--surface-base);
border-radius: 16px;
box-sizing: border-box;
scroll-behavior: smooth;
scrollbar-gutter: stable both-edges;
}
.chat-scroll::-webkit-scrollbar {
width: 6px;
}
.chat-scroll::-webkit-scrollbar-thumb {
background: rgba(22, 119, 255, 0.24);
border-radius: 8px;
}
.chat-scroll::-webkit-scrollbar-track {
background: rgba(15, 23, 42, 0.05);
border-radius: 8px;
}
.message-row {
margin-bottom: 4px;
}
.message-shell {
display: flex;
flex-direction: column;
gap: 6px;
}
.message-shell.sent {
align-items: flex-end;
}
.message-shell.received {
align-items: flex-start;
}
/* custom scrollbar */
#scrollDiv::-webkit-scrollbar { height: 8px; width: 8px; }
#scrollDiv::-webkit-scrollbar-thumb { background: #c7cbd1; border-radius: 8px; }
#scrollDiv::-webkit-scrollbar-track { background: transparent; }
.chat-bubble {
padding: 12px 14px;
margin: 6px 10px;
border-radius: 14px;
display: inline-flex;
flex-direction: column;
gap: 6px;
padding: 14px 16px;
border-radius: 16px;
max-width: 78%;
position: relative;
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
border: 1px solid var(--bubble-border);
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08);
border: none;
word-break: break-word;
line-height: 1.6;
animation: pop .18s ease-out;
line-height: 1.65;
animation: pop .2s ease-out;
transition: transform .2s ease, box-shadow .2s ease;
}
.chat-bubble .bubble-text { margin: 0; white-space: pre-wrap; }
.received {
background-color: var(--bubble-recv);
align-self: flex-start;
float: left;
.chat-bubble:hover {
transform: translateY(-1px);
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.12);
}
.sent {
background-color: var(--bubble-sent);
align-self: flex-end;
float: right;
.chat-bubble .bubble-text {
margin: 0;
white-space: pre-wrap;
color: var(--text-primary);
}
.chat-bubble .bubble-body {
gap: 10px;
}
.chat-bubble.received {
background: var(--bubble-recv);
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.06), inset 0 0 0 1px rgba(15, 23, 42, 0.05);
}
.chat-bubble.sent {
background: var(--bubble-sent);
color: #0c1a33;
box-shadow: 0 12px 26px rgba(22, 119, 255, 0.18);
}
.chat-bubble.sent .bubble-text {
color: #102347;
}
/* bubble tails */
.chat-bubble.received::after {
content: "";
position: absolute; left: -6px; bottom: 10px;
width: 10px; height: 10px; background: var(--bubble-recv);
border-left: 1px solid var(--bubble-border);
border-bottom: 1px solid var(--bubble-border);
transform: rotate(45deg);
border-bottom-left-radius: 2px;
}
.chat-bubble.received::after,
.chat-bubble.sent::after {
content: "";
position: absolute; right: -6px; bottom: 10px;
width: 10px; height: 10px; background: var(--bubble-sent);
border-right: 1px solid var(--bubble-border);
border-bottom: 1px solid var(--bubble-border);
transform: rotate(45deg);
border-bottom-right-radius: 2px;
position: absolute;
top: 16px;
bottom: auto;
width: 18px;
height: 18px;
transform: none;
border-radius: 0;
box-shadow: none !important;
filter: none !important;
}
.chat-bubble.received::after {
left: -14px;
background: var(--bubble-recv);
clip-path: polygon(100% 0, 0 0, 100% 100%);
}
.chat-bubble.sent::after {
right: -14px;
background: var(--bubble-sent);
clip-path: polygon(0 0, 100% 0, 0 100%);
}
.message-meta {
font-size: 12px;
color: var(--text-secondary);
margin: 2px 12px 4px;
clear: both;
display: flex;
align-items: center;
gap: 8px;
margin: 0;
}
@@keyframes pop {
from { transform: translateY(4px); opacity: .65; }
to { transform: translateY(0); opacity: 1; }
}
.meta-right { text-align: right; }
.meta-left { text-align: left; }
.avatar { border-radius: 50%; box-shadow: 0 0 0 1px #e6e8eb inset; }
.meta-right {
justify-content: flex-end;
}
.meta-left {
justify-content: flex-start;
padding-left: 4px;
}
.meta-action {
color: var(--text-secondary);
cursor: pointer;
transition: color .2s ease;
}
.meta-action:hover {
color: var(--primary-color);
}
.avatar-col {
display: flex;
justify-content: center;
}
.avatar {
width: 32px;
height: 32px;
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: 50%;
box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.12) inset, 0 6px 16px rgba(15, 23, 42, 0.12);
}
.input-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 8px 12px;
border-top: 1px solid #e6e8eb;
background: rgba(255,255,255,0.6);
backdrop-filter: blur(4px);
padding: 12px;
border-top: 1px solid rgba(15, 23, 42, 0.06);
background: var(--surface-base);
position: sticky;
bottom: 0;
box-shadow: 0 -12px 24px rgba(15, 23, 42, 0.06);
border-radius: 16px;
}
.input-bar .ant-input {
border-radius: 999px;
padding: 10px 16px;
}
.input-bar__field {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
padding: 2px;
border-radius: 999px;
background: var(--surface-elevated);
border: 1px solid var(--border-subtle);
}
.input-bar__field .ant-input {
width: 100%;
border: none;
background: transparent;
box-shadow: none;
height: 100%;
}
/* markdown & code inside bubbles */
.chat-bubble pre { background:#0b1021; color:#e6e6e6; padding:12px; border-radius:10px; overflow:auto; }
.chat-bubble code { background: rgba(27,31,35,0.06); padding: .15rem .35rem; border-radius: 6px; }
.chat-bubble pre code { background: transparent; padding: 0; }
.chat-bubble table { width: 100%; border-collapse: collapse; }
.chat-bubble table td, .chat-bubble table th { border: 1px solid #e6e8eb; padding: 6px 8px; }
.chat-bubble pre {
background: #0b1021;
color: #e6e6e6;
padding: 12px;
border-radius: 12px;
overflow: auto;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
.ant-card-body { height: 90% !important; }
.chat-bubble code {
background: rgba(27, 31, 35, 0.08);
padding: .15rem .35rem;
border-radius: 6px;
}
.chat-bubble pre code {
background: transparent;
padding: 0;
}
.chat-bubble table {
width: 100%;
border-collapse: collapse;
}
.chat-bubble table td,
.chat-bubble table th {
border: 1px solid rgba(15, 23, 42, 0.08);
padding: 6px 8px;
}
.think {
color: gray;
color: rgba(22, 119, 255, 0.9);
font-style: italic;
text-align: left !important;
display: block;
margin-top: 5px;
margin-left: 8px;
padding-left: 8px;
border-left: 2px solid #7F7FFF;
padding-left: 10px;
border-left: 3px solid rgba(22, 119, 255, 0.4);
}
.message-file { margin: 0 0 6px 0; }
.message-file {
margin: 0;
border-radius: 10px;
overflow: hidden;
}
.message-file .ant-upload-list-item {
background: rgba(22, 119, 255, 0.08);
border: none !important;
}
@@media (max-width: 992px) {
#chat {
border-radius: 12px;
padding: 10px 10px 0;
}
.chat-bubble {
max-width: 88%;
}
.input-bar {
flex-wrap: wrap;
}
}
@@media (prefers-color-scheme: dark) {
:root {
--surface-base: #111827;
--surface-elevated: #1f2937;
--border-subtle: rgba(148, 163, 184, 0.16);
--shadow-soft: 0 18px 34px rgba(0, 0, 0, 0.45);
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--bubble-recv: #1f2937;
}
#chat {
background: var(--surface-elevated);
box-shadow: 0 28px 48px -24px rgba(0, 0, 0, 0.68);
}
#chat::after {
background: rgba(15, 23, 42, 0.55);
}
.chat-scroll {
background: transparent;
}
.chat-bubble {
border: 1px solid rgba(148, 163, 184, 0.16);
}
.chat-bubble.received::after {
background: var(--bubble-recv);
border: none;
}
.message-file .ant-upload-list-item {
background: rgba(22, 119, 255, 0.16);
}
.input-bar {
background: var(--surface-elevated);
}
}
@@keyframes pop {
from { transform: translateY(6px); opacity: .55; }
to { transform: translateY(0); opacity: 1; }
}
</style>
@code {

View File

@@ -0,0 +1,61 @@
@namespace AntSK.Pages.Setting.Role
@using AntSK.Domain.Repositories
@using AntSK.Models
@page "/setting/role/add"
@page "/setting/role/add/{RoleId}"
@using AntSK.Services.Auth
@inherits AuthComponentBase
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Roles = "AntSKAdmin")]
<PageContainer Title="新增角色">
<ChildContent>
<Card>
<Form Model="@_roleModel"
Style="margin-top: 8px;"
OnFinish="HandleSubmit">
<FormItem Label="角色名称" LabelCol="LayoutModel._formItemLayout.LabelCol" WrapperCol="LayoutModel._formItemLayout.WrapperCol">
<Input Placeholder="请输入角色名称" @bind-Value="@context.Name" />
</FormItem>
<FormItem Label="角色编码" LabelCol="LayoutModel._formItemLayout.LabelCol" WrapperCol="LayoutModel._formItemLayout.WrapperCol">
<Input Placeholder="请输入角色编码" @bind-Value="@context.Code" />
</FormItem>
<FormItem Label="角色描述" LabelCol="LayoutModel._formItemLayout.LabelCol" WrapperCol="LayoutModel._formItemLayout.WrapperCol">
<Input Placeholder="请输入角色描述" @bind-Value="@context.Description" />
</FormItem>
<FormItem Label="是否启用" LabelCol="LayoutModel._formItemLayout.LabelCol" WrapperCol="LayoutModel._formItemLayout.WrapperCol">
<Switch @bind-Checked="@context.IsEnabled" />
</FormItem>
<FormItem Label="权限分配" LabelCol="LayoutModel._formItemLayout.LabelCol" WrapperCol="LayoutModel._formItemLayout.WrapperCol">
<Select Mode="multiple"
@bind-Values="_permissionIds"
Placeholder="选择权限"
TItemValue="string"
TItem="string"
Size="@AntSizeLDSType.Default">
<SelectOptions>
@foreach (var permission in _allPermissions)
{
<SelectOption TItem="string" TItemValue="string" Value="@permission.Id" Label="@permission.Name" />
}
</SelectOptions>
</Select>
</FormItem>
<FormItem Label=" " Style="margin-top:32px" WrapperCol="LayoutModel._submitFormLayout.WrapperCol">
<Button Type="primary" OnClick="HandleSubmit">
保存
</Button>
<Button OnClick="Back">
返回
</Button>
</FormItem>
</Form>
</Card>
</ChildContent>
</PageContainer>
@code {
}

View File

@@ -0,0 +1,103 @@
using AntDesign;
using AntSK.Domain.Repositories;
using Microsoft.AspNetCore.Components;
namespace AntSK.Pages.Setting.Role
{
public partial class AddRole
{
[Parameter]
public string RoleId { get; set; }
[Inject] protected IRoles_Repositories _roles_Repositories { get; set; }
[Inject] protected IPermissions_Repositories _permissions_Repositories { get; set; }
[Inject] protected IRolePermissions_Repositories _rolePermissions_Repositories { get; set; }
[Inject] protected MessageService? Message { get; set; }
private Roles _roleModel = new Roles();
private List<Permissions> _allPermissions = new List<Permissions>();
private IEnumerable<string> _permissionIds;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
// 加载所有权限
_allPermissions = _permissions_Repositories.GetList();
if (!string.IsNullOrEmpty(RoleId))
{
_roleModel = _roles_Repositories.GetFirst(p => p.Id == RoleId);
// 加载角色已有的权限
var rolePermissions = _rolePermissions_Repositories.GetList(p => p.RoleId == RoleId);
_permissionIds = rolePermissions.Select(rp => rp.PermissionId);
}
}
private void HandleSubmit()
{
if (string.IsNullOrEmpty(RoleId))
{
//新增
_roleModel.Id = Guid.NewGuid().ToString();
_roleModel.CreateTime = DateTime.Now;
if (_roles_Repositories.IsAny(p => p.Code == _roleModel.Code))
{
_ = Message.Error("角色编码已存在!", 2);
return;
}
_roles_Repositories.Insert(_roleModel);
// 添加角色权限关联
if (_permissionIds != null)
{
foreach (var permissionId in _permissionIds)
{
_rolePermissions_Repositories.Insert(new RolePermissions
{
Id = Guid.NewGuid().ToString(),
RoleId = _roleModel.Id,
PermissionId = permissionId,
CreateTime = DateTime.Now
});
}
}
}
else
{
//修改
_roles_Repositories.Update(_roleModel);
// 先删除旧的角色权限关联
var oldRolePermissions = _rolePermissions_Repositories.GetList(p => p.RoleId == RoleId);
foreach (var rp in oldRolePermissions)
{
_rolePermissions_Repositories.Delete(rp.Id);
}
// 添加新的角色权限关联
if (_permissionIds != null)
{
foreach (var permissionId in _permissionIds)
{
_rolePermissions_Repositories.Insert(new RolePermissions
{
Id = Guid.NewGuid().ToString(),
RoleId = _roleModel.Id,
PermissionId = permissionId,
CreateTime = DateTime.Now
});
}
}
}
Back();
}
private void Back()
{
NavigationManager.NavigateTo("/setting/rolelist");
}
}
}

View File

@@ -0,0 +1,72 @@
@namespace AntSK.Pages.Setting.Role
@using AntSK.Domain.Repositories
@page "/setting/rolelist"
@inject NavigationManager NavigationManager
@using AntSK.Services.Auth
@inherits AuthComponentBase
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Roles = "AntSKAdmin")]
<div>
<PageContainer Title="角色管理">
<ChildContent>
<div class="standardList">
<Card Class="listCard"
Title="角色列表"
Style="margin-top: 24px;"
BodyStyle="padding: 0 32px 40px 32px">
<Extra>
<div class="extraContent">
<Search Class="extraContentSearch" Placeholder="查询" @bind-Value="_searchKeyword" OnSearch="OnSearch" />
</div>
</Extra>
<ChildContent>
<Button Type="dashed"
Style="width: 100%; margin-bottom: 8px;"
OnClick="AddRole">
<Icon Type="plus" Theme="outline" />
新增角色
</Button>
<AntList TItem="Roles"
DataSource="_data"
ItemLayout="ListItemLayout.Horizontal">
<ListItem Actions="new[] {
edit(()=> Edit(context.Id)),
delete(async ()=>await Delete(context.Id))
}" Style="width:100%">
<div class="listContent" style="width:100%">
<div class="listContentItem" style="width:20%">
<b>角色名称</b>
<p>@context.Name</p>
</div>
<div class="listContentItem" style="width:20%">
<b>角色编码</b>
<p>@context.Code</p>
</div>
<div class="listContentItem" style="width:30%">
<b>描述</b>
<p>@context.Description</p>
</div>
<div class="listContentItem" style="width:15%">
<b>状态</b>
<p>@(context.IsEnabled ? "启用" : "禁用")</p>
</div>
</div>
</ListItem>
</AntList>
</ChildContent>
</Card>
</div>
</ChildContent>
</PageContainer>
</div>
@code
{
RenderFragment edit(Action clickAction) =>@<a key="edit" @onclick="@clickAction">修改</a>;
RenderFragment delete(Action clickAction) =>@<a key="delete" @onclick="@clickAction">删除</a>;
}

View File

@@ -0,0 +1,84 @@
using AntDesign;
using AntSK.Domain.Repositories;
using AntSK.Models;
using Microsoft.AspNetCore.Components;
namespace AntSK.Pages.Setting.Role
{
public partial class RoleList
{
private readonly BasicListFormModel _model = new BasicListFormModel();
private List<Roles> _data;
private string _searchKeyword;
[Inject]
protected IRoles_Repositories _roles_Repositories { get; set; }
[Inject]
protected IRolePermissions_Repositories _rolePermissions_Repositories { get; set; }
[Inject]
protected IUserRoles_Repositories _userRoles_Repositories { get; set; }
[Inject]
IConfirmService _confirmService { get; set; }
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await InitData();
}
private async Task InitData(string searchKey = null)
{
if (string.IsNullOrEmpty(searchKey))
{
_data = _roles_Repositories.GetList();
}
else
{
_data = _roles_Repositories.GetList(p => p.Name.Contains(searchKey) || p.Code.Contains(searchKey) || (p.Description != null && p.Description.Contains(searchKey)));
}
await InvokeAsync(StateHasChanged);
}
public async Task OnSearch()
{
await InitData(_searchKeyword);
}
public async Task AddRole()
{
NavigationManager.NavigateTo("/setting/role/add");
}
public void Edit(string roleid)
{
NavigationManager.NavigateTo("/setting/role/add/" + roleid);
}
public async Task Delete(string roleid)
{
var content = "是否确认删除此角色";
var title = "删除";
var result = await _confirmService.Show(content, title, ConfirmButtons.YesNo);
if (result == ConfirmResult.Yes)
{
// 删除角色权限关联
var rolePerms = _rolePermissions_Repositories.GetList(p => p.RoleId == roleid);
foreach (var rp in rolePerms)
{
await _rolePermissions_Repositories.DeleteAsync(rp.Id);
}
// 删除用户角色关联
var userRoles = _userRoles_Repositories.GetList(p => p.RoleId == roleid);
foreach (var ur in userRoles)
{
await _userRoles_Repositories.DeleteAsync(ur.Id);
}
// 删除角色
await _roles_Repositories.DeleteAsync(roleid);
await InitData("");
}
}
}
}

View File

@@ -43,6 +43,22 @@
</Select>
</FormItem>
<FormItem Label="角色分配" LabelCol="LayoutModel._formItemLayout.LabelCol" WrapperCol="LayoutModel._formItemLayout.WrapperCol">
<Select Mode="multiple"
@bind-Values="_roleIds"
Placeholder="选择角色"
TItemValue="string"
TItem="string"
Size="@AntSizeLDSType.Default">
<SelectOptions>
@foreach (var role in _allRoles)
{
<SelectOption TItem="string" TItemValue="string" Value="@role.Id" Label="@role.Name" />
}
</SelectOptions>
</Select>
</FormItem>
<FormItem Label=" " Style="margin-top:32px" WrapperCol="LayoutModel._submitFormLayout.WrapperCol">
<Button Type="primary" OnClick="HandleSubmit">
保存

View File

@@ -12,21 +12,34 @@ namespace AntSK.Pages.Setting.User
[Parameter]
public string UserId { get; set; }
[Inject] protected IUsers_Repositories _users_Repositories { get; set; }
[Inject] protected IRoles_Repositories _roles_Repositories { get; set; }
[Inject] protected IUserRoles_Repositories _userRoles_Repositories { get; set; }
[Inject] protected MessageService? Message { get; set; }
[Inject] public HttpClient HttpClient { get; set; }
private Users _userModel = new Users();
private string _password = "";
IEnumerable<string> _menuKeys;
IEnumerable<string> _roleIds;
private List<MenuDataItem> menuList = new List<MenuDataItem>();
private List<Roles> _allRoles = new List<Roles>();
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
// 加载所有角色
_allRoles = _roles_Repositories.GetList(r => r.IsEnabled);
if (!string.IsNullOrEmpty(UserId))
{
_userModel = _users_Repositories.GetFirst(p => p.Id == UserId);
_password = _userModel.Password;
// 加载用户已有的角色
var userRoles = _userRoles_Repositories.GetList(p => p.UserId == UserId);
_roleIds = userRoles.Select(ur => ur.RoleId);
}
menuList = (await HttpClient.GetFromJsonAsync<MenuDataItem[]>("data/menu.json")).ToList().Where(p => p.Key != "setting").ToList();
_menuKeys = _userModel.MenuRole?.Split(",");
@@ -52,6 +65,21 @@ namespace AntSK.Pages.Setting.User
}
_userModel.Password = PasswordUtil.HashPassword(_userModel.Password);
_users_Repositories.Insert(_userModel);
// 添加用户角色关联
if (_roleIds != null)
{
foreach (var roleId in _roleIds)
{
_userRoles_Repositories.Insert(new UserRoles
{
Id = Guid.NewGuid().ToString(),
UserId = _userModel.Id,
RoleId = roleId,
CreateTime = DateTime.Now
});
}
}
}
else
{
@@ -61,6 +89,28 @@ namespace AntSK.Pages.Setting.User
_userModel.Password = PasswordUtil.HashPassword(_userModel.Password);
}
_users_Repositories.Update(_userModel);
// 先删除旧的用户角色关联
var oldUserRoles = _userRoles_Repositories.GetList(p => p.UserId == UserId);
foreach (var ur in oldUserRoles)
{
_userRoles_Repositories.Delete(ur.Id);
}
// 添加新的用户角色关联
if (_roleIds != null)
{
foreach (var roleId in _roleIds)
{
_userRoles_Repositories.Insert(new UserRoles
{
Id = Guid.NewGuid().ToString(),
UserId = _userModel.Id,
RoleId = roleId,
CreateTime = DateTime.Now
});
}
}
}
Back();

View File

@@ -10,6 +10,10 @@ namespace AntSK.Services.Auth
{
public class AntSKAuthProvider(
IUsers_Repositories _users_Repositories,
IUserRoles_Repositories _userRoles_Repositories,
IRoles_Repositories _roles_Repositories,
IRolePermissions_Repositories _rolePermissions_Repositories,
IPermissions_Repositories _permissions_Repositories,
ProtectedSessionStorage _protectedSessionStore
) : AuthenticationStateProvider
{
@@ -29,13 +33,18 @@ namespace AntSK.Services.Auth
new Claim(ClaimTypes.Role, AdminRole)
};
identity = new ClaimsIdentity(claims, AdminRole);
await _protectedSessionStore.SetAsync("UserSession", new UserSession() { UserName = username, Role = AdminRole });
await _protectedSessionStore.SetAsync("UserSession", new UserSession()
{
UserName = username,
Role = AdminRole,
Roles = new List<string> { AdminRole },
Permissions = new List<string>()
});
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
return true;
}
else
{
string UserRole = "AntSKUser";
if (user.IsNull())
{
return false;
@@ -44,13 +53,42 @@ namespace AntSK.Services.Auth
{
return false;
}
// 获取用户的角色和权限
var userRoles = _userRoles_Repositories.GetList(p => p.UserId == user.Id);
var roleIds = userRoles.Select(ur => ur.RoleId).ToList();
var roles = _roles_Repositories.GetList(r => roleIds.Contains(r.Id) && r.IsEnabled);
// 获取角色的权限
var rolePermissions = _rolePermissions_Repositories.GetList(rp => roleIds.Contains(rp.RoleId));
var permissionIds = rolePermissions.Select(rp => rp.PermissionId).Distinct().ToList();
var permissions = _permissions_Repositories.GetList(p => permissionIds.Contains(p.Id));
// 如果没有角色,使用默认角色
string defaultRole = "AntSKUser";
var roleList = roles.Any() ? roles.Select(r => r.Code).ToList() : new List<string> { defaultRole };
var permissionList = permissions.Select(p => p.Code).ToList();
// 用户认证成功创建用户的ClaimsIdentity
var claims = new[] {
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, UserRole)
};
identity = new ClaimsIdentity(claims, UserRole);
await _protectedSessionStore.SetAsync("UserSession", new UserSession() { UserName = username, Role = UserRole });
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, username)
};
// 添加所有角色到Claims
foreach (var role in roleList)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
identity = new ClaimsIdentity(claims, roleList.FirstOrDefault() ?? defaultRole);
await _protectedSessionStore.SetAsync("UserSession", new UserSession()
{
UserName = username,
Role = roleList.FirstOrDefault() ?? defaultRole,
Roles = roleList,
Permissions = permissionList
});
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
return true;
}
@@ -68,9 +106,25 @@ namespace AntSK.Services.Auth
var userSession = userSessionStorageResult.Success ? userSessionStorageResult.Value : null;
if (userSession.IsNotNull())
{
var claims = new[] {
new Claim(ClaimTypes.Name, userSession.UserName),
new Claim( ClaimTypes.Role, userSession.Role) };
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, userSession.UserName)
};
// 添加所有角色到Claims
if (userSession.Roles != null && userSession.Roles.Any())
{
foreach (var role in userSession.Roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
}
else
{
// 向后兼容使用单个Role
claims.Add(new Claim(ClaimTypes.Role, userSession.Role));
}
identity = new ClaimsIdentity(claims, userSession.Role);
}
var user = new ClaimsPrincipal(identity);

View File

@@ -59,6 +59,11 @@
"name": "用户管理",
"key": "setting.user"
},
{
"path": "/setting/rolelist",
"name": "角色管理",
"key": "setting.role"
},
{
"path": "/setting/chathistory",
"name": "聊天记录",