【ABAC】最强实战:基于.net core + vue2 实现ABAC鉴权模型(附数据库设计和核心代码)
可能是史上最强的.net + vue 全栈实战ABAC,附数据库设计和核心代码
1、前言
最近公司重新开发老的oa系统,这边选择后端使用net core 前端使用vue2 重新开发。
在梳理需求时,发现很多菜单的权限判断,都是根据登录用户的职级、或者一些自定义的业务来判断。如果按照传统的RBAC(角色权限管理)来设计,也可以实现,比如根据职级形成多个角色,然后将人员定期同步到对应的角色下。但是这样做的后果是,随着系统业务逻辑的迭代,如后期可能根据其访问来源IP,内外网,是否试用期等等来判断其权限。这样的后果就是角色爆炸,难以维护,而且把人员往角色同步的工作也会随之增加。
所以我在架构设计时,考虑使用ABAC,根据属性来控制权限。这样,我仅需要关注和维护属性,以及属性的判断比较即可。而且这样设计带来的好处就是,在企业内部会比RBAC更适合企业的实际需求。
1.1后端代码实现要点
首先要把ABAC的概念理解并用编码的方式将其实现。
ABAC的实现核心在于基于属性组装成表达式语言,有使用过mybatis的同学都知道以下方式拼接成的sql语句
<if test=' testString != null and testString == "A" '>
AND 表字段 = #{testString}
</if>
<if test=" testString != null and testString == 'A'.toString() ">
AND 表字段 = #{testString}
</if>
ABAC的实现思路与此类似,根据不同属性,组装不同的表达式,通过表达式来得出有无权限。
实现的方式也有很多,JAVA中可以选择Spring Expression Language:SpEL来实现,也可通过规则引擎。
.net中也可选择规则引擎、Expression表达式树
本文中选择通过Expression表达式树 来实现
1.2、前端实现难点
在本系统设计及实现时,如果说前后端是分离的,那么就要要求前后端人员对ABAC有着相同的理解。
前端如何传递表达式参数?如何展示表达式树,如 age >1 && ( name != ‘张三’ || email != ‘lisi’ ) ,前端该如何形象且易懂的展示给用户,这也是难点所在。
表达式与子表达式如何展示,提示一下:利用vue组件的递归引用
2、系统授权分类
这里简要指出一般系统中授权的三级分类
3、RBAC(Role-Based Access Control)基础概念
基于角色的访问控制,用户绑定角色,角色关联功能,用户只有权访问自己角色所关联的功能。
是IT系统中使用较多的权限管控模型,也是比较简单易懂的一种模型
4、ABAC(Attribute-Based Access Control)基础概念
ABAC是基于属性的访问控制,相较于RBAC粒度更细。再判断用户是否有权限访问某一资源时,是通过其各种属性实时计算而来的。
ABAC中的属性又可细分为:
1、环境属性:访问来源内外网、事件、场景 。
– 如企业内某些系统、功能在未来仅支持内网访问
2、用户属性:职别、职等、部门、籍贯、直间接、签核权
– 实时计算用户属性的好处是,针对频繁的组织、人员异动,只需要修改对应资源的权限策略,无需关注组织人员异动
3、资源属性:资源状态、资源创建时间、资源热度等
– 以API接口资源为例。庞杂的业务系统,一定会衍生出较多的API接口。但随着业务的演变,有些API会被弃用但并未下架,而这些接口又可以访问到一些数据。在后期规划API平台中,就可以根据API的热度,限定在本次访问之前,指定时间内没有被访问过的API禁止访问。
5、RBAC与ABAC对比
5.1、实现方式
5.1.1 RBAC
RBAC实现方式相对较简单,主要需要考虑以下几个方面:
角色定义:定义不同的角色,根据不同的职责和权限进行划分。
角色分配:将用户分配到相应的角色中。
权限分配:将不同的权限分配给不同的角色,以实现权限控制。
5.1.2 ABAC
ABAC的实现过程相对复杂,需要考虑以下几个方面:
属性设计:设计适当的属性标识用户、资源、环境等。
访问策略:基于属性来制定访问策略。
实现机制:引入多个组件来实现属性访问控制,如规则引擎等。
5.2、适用场景
5.2.1 RBAC
适用于规模较小、角色划分较为静态的场景,比如一些中小型企业、少量管理员对应着少量功能的系统等,它的优点在于结构清晰,易于管理。但是如果角色不够细分,就不能对不同的用户进行详细的权限控制,也不太适用于复杂多层次的业务场景。
5.2.2 ABAC
适用于复杂多层次、动态变化的场景,它的优点在于非常灵活,可以应对不同的用户、不同的业务需求,更加符合实际场景的需求。但是ABAC需要对各种属性进行建模,实现更加复杂,需要的技术支持和投入也更大。
6、核心功能梳理及表结构设计
管理侧权限管理流程图:
用户侧登录鉴权流程图:
6.0、属性管理
ABAC中所有权限判定都是围绕属性来做判断,故将现有的属性抽象化,并进行管理,是整个权限管理的数据基础
6.0.1 Sys_Dev_Param (基础参数表)
create table Sys_Dev_Param
(
Id bigint not null
constraint PK_Sys_Dev_Param_Id
primary key,
param_name nvarchar(200) not null,
param_value nvarchar(500) not null,
param_desc nvarchar(255) not null,
value_type nvarchar(255) not null,
attr_type nvarchar(255) not null,
CreateTime datetime,
UpdateTime datetime,
CreateEmpNo nvarchar(255),
UpdateEmpNo nvarchar(255),
CreateEmp nvarchar(255),
UpdateEmp nvarchar(255),
CreateDeptNo nvarchar(255),
UpdateDeptNo nvarchar(255),
ExtJson nvarchar(max),
IsDelete bit
)
go
exec sp_addextendedproperty 'MS_Description', N'基础参数表', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param'
go
exec sp_addextendedproperty 'MS_Description', 'Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN', 'Id'
go
exec sp_addextendedproperty 'MS_Description', N'显示名称', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
'param_name'
go
exec sp_addextendedproperty 'MS_Description', N'参数key', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
'param_value'
go
exec sp_addextendedproperty 'MS_Description', N'参数描述', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
'param_desc'
go
exec sp_addextendedproperty 'MS_Description', N'参数值类型 如:string、int、datetime、list', 'SCHEMA', 'dbo', 'TABLE',
'Sys_Dev_Param', 'COLUMN', 'value_type'
go
exec sp_addextendedproperty 'MS_Description',
N'参数类型:1:用户基础属性;2:用户扩展属性;3:部门基础属性;4:部门扩展属性;5:环境属性;6:自定义属性 ', 'SCHEMA', 'dbo',
'TABLE', 'Sys_Dev_Param', 'COLUMN', 'attr_type'
go
exec sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
'CreateTime'
go
exec sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
'UpdateTime'
go
exec sp_addextendedproperty 'MS_Description', N'创建者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
'CreateEmpNo'
go
exec sp_addextendedproperty 'MS_Description', N'修改者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
'UpdateEmpNo'
go
exec sp_addextendedproperty 'MS_Description', N'创建人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
'CreateEmp'
go
exec sp_addextendedproperty 'MS_Description', N'更新人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
'UpdateEmp'
go
exec sp_addextendedproperty 'MS_Description', N'创建者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
'CreateDeptNo'
go
exec sp_addextendedproperty 'MS_Description', N'修改者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
'UpdateDeptNo'
go
exec sp_addextendedproperty 'MS_Description', N'扩展信息', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
'ExtJson'
go
exec sp_addextendedproperty 'MS_Description', N'软删除', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN', 'IsDelete'
go
6.0.2 Sys_Dev_Param_API (基础参数表-API参数表)
create table Sys_Dev_Param_API
(
Id bigint not null
constraint PK_Sys_Dev_Param_API_Id
primary key,
param_id bigint not null,
request_url nvarchar(255) not null,
request_method nvarchar(255) not null,
request_param nvarchar(4000) not null,
reponse_type nvarchar(255) not null,
CreateTime datetime,
UpdateTime datetime,
CreateEmpNo nvarchar(255),
UpdateEmpNo nvarchar(255),
CreateEmp nvarchar(255),
UpdateEmp nvarchar(255),
CreateDeptNo nvarchar(255),
UpdateDeptNo nvarchar(255),
ExtJson nvarchar(max),
IsDelete bit
)
go
exec sp_addextendedproperty 'MS_Description', N'基础参数-API参数表', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API'
go
exec sp_addextendedproperty 'MS_Description', 'Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN', 'Id'
go
exec sp_addextendedproperty 'MS_Description', N'参数ID', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
'param_id'
go
exec sp_addextendedproperty 'MS_Description', N'API请求地址', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
'request_url'
go
exec sp_addextendedproperty 'MS_Description', N'API请求方法', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
'request_method'
go
exec sp_addextendedproperty 'MS_Description', N'API请求参数', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
'request_param'
go
exec sp_addextendedproperty 'MS_Description', N'API请求返回类型', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API',
'COLUMN', 'reponse_type'
go
exec sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
'CreateTime'
go
exec sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
'UpdateTime'
go
exec sp_addextendedproperty 'MS_Description', N'创建者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
'CreateEmpNo'
go
exec sp_addextendedproperty 'MS_Description', N'修改者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
'UpdateEmpNo'
go
exec sp_addextendedproperty 'MS_Description', N'创建人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
'CreateEmp'
go
exec sp_addextendedproperty 'MS_Description', N'更新人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
'UpdateEmp'
go
exec sp_addextendedproperty 'MS_Description', N'创建者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
'CreateDeptNo'
go
exec sp_addextendedproperty 'MS_Description', N'修改者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
'UpdateDeptNo'
go
exec sp_addextendedproperty 'MS_Description', N'扩展信息', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
'ExtJson'
go
exec sp_addextendedproperty 'MS_Description', N'软删除', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
'IsDelete'
go
6.1、策略/规则管理
所有基于属性的判断条件都应当归属在规则或策略下
前端设计应当支持策略的复制功能,避免多个菜单,每个都要配置
6.1.1 Sys_Dev_Rule (策略/规则表)
数据库设计(所有表均需继承公共表,包含ID、创建时间、是否删除等公共属性)
-- auto-generated definition
create table Sys_Dev_Rule
(
Id bigint not null
constraint PK_Sys_Dev_Rule_Id
primary key,
rule_name nvarchar(500) not null,
rule_desc nvarchar(500) not null,
CreateTime datetime,
UpdateTime datetime,
CreateEmpNo nvarchar(255),
UpdateEmpNo nvarchar(255),
CreateEmp nvarchar(255),
UpdateEmp nvarchar(255),
CreateDeptNo nvarchar(255),
UpdateDeptNo nvarchar(255),
ExtJson nvarchar(max),
IsDelete bit
)
go
exec sp_addextendedproperty 'MS_Description', N'策略/规则', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule'
go
exec sp_addextendedproperty 'MS_Description', 'Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN', 'Id'
go
exec sp_addextendedproperty 'MS_Description', N'规则名称', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
'rule_name'
go
exec sp_addextendedproperty 'MS_Description', N'规则描述', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
'rule_desc'
go
exec sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
'CreateTime'
go
exec sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
'UpdateTime'
go
exec sp_addextendedproperty 'MS_Description', N'创建者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
'CreateEmpNo'
go
exec sp_addextendedproperty 'MS_Description', N'修改者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
'UpdateEmpNo'
go
exec sp_addextendedproperty 'MS_Description', N'创建人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN', 'CreateEmp'
go
exec sp_addextendedproperty 'MS_Description', N'更新人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN', 'UpdateEmp'
go
exec sp_addextendedproperty 'MS_Description', N'创建者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
'CreateDeptNo'
go
exec sp_addextendedproperty 'MS_Description', N'修改者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
'UpdateDeptNo'
go
exec sp_addextendedproperty 'MS_Description', N'扩展信息', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN', 'ExtJson'
go
exec sp_addextendedproperty 'MS_Description', N'软删除', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN', 'IsDelete'
go
6.2、条件管理
条件管理的唯一目的是获得返回的True/False
什么是条件?
如:EmpNo == ‘001’ 返回true
我们要做的就是把条件抽象化成结构化数据
6.1.1 Sys_Dev_Rule_Cond(条件表)
数据库设计(所有表均需继承公共表,包含ID、创建时间、是否删除等公共属性)
-- auto-generated definition
create table Sys_Dev_Rule_Cond
(
Id bigint not null
constraint PK_Sys_Dev_Rule_Cond_Id
primary key,
cond_group_id bigint not null,
left_type nvarchar(255) not null,
left_value nvarchar(255) not null,
left_value_type nvarchar(255) not null,
symbol nvarchar(255) not null,
right_type nvarchar(255),
right_value_type nvarchar(255),
right_value nvarchar(255) not null,
CreateTime datetime,
UpdateTime datetime,
CreateEmpNo nvarchar(255),
UpdateEmpNo nvarchar(255),
CreateEmp nvarchar(255),
UpdateEmp nvarchar(255),
CreateDeptNo nvarchar(255),
UpdateDeptNo nvarchar(255),
ExtJson nvarchar(max),
IsDelete bit
)
go
exec sp_addextendedproperty 'MS_Description', N'条件表', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond'
go
exec sp_addextendedproperty 'MS_Description', 'Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN', 'Id'
go
exec sp_addextendedproperty 'MS_Description', N'条件归属的条件组ID', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond',
'COLUMN', 'cond_group_id'
go
exec sp_addextendedproperty 'MS_Description', N'属性来源类型', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'left_type'
go
exec sp_addextendedproperty 'MS_Description', N'左值', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'left_value'
go
exec sp_addextendedproperty 'MS_Description', N'左值值类型', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'left_value_type'
go
exec sp_addextendedproperty 'MS_Description', N'关系', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN', 'symbol'
go
exec sp_addextendedproperty 'MS_Description', N'右值类型', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'right_type'
go
exec sp_addextendedproperty 'MS_Description', N'右值值类型', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'right_value_type'
go
exec sp_addextendedproperty 'MS_Description', N'右值', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'right_value'
go
exec sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'CreateTime'
go
exec sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'UpdateTime'
go
exec sp_addextendedproperty 'MS_Description', N'创建者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'CreateEmpNo'
go
exec sp_addextendedproperty 'MS_Description', N'修改者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'UpdateEmpNo'
go
exec sp_addextendedproperty 'MS_Description', N'创建人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'CreateEmp'
go
exec sp_addextendedproperty 'MS_Description', N'更新人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'UpdateEmp'
go
exec sp_addextendedproperty 'MS_Description', N'创建者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'CreateDeptNo'
go
exec sp_addextendedproperty 'MS_Description', N'修改者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'UpdateDeptNo'
go
exec sp_addextendedproperty 'MS_Description', N'扩展信息', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'ExtJson'
go
exec sp_addextendedproperty 'MS_Description', N'软删除', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
'IsDelete'
go
6.2、条件组管理
条件组管理是将多个条件组合在一起,作为一个操作单元,返回一个true或false
条件组的管理要实现,多个条件之间的逻辑关系,且、或
如:EmpNo == ‘001’ || EmpNo == ‘002’
条件组应当能够体现 两个条件的或关系
如:EmpNo == ‘001’ || (EmpNo == ‘002’ && DeptNo == ‘部门1’ )
由于涉及到子逻辑关系,这时新建条件组应当能够体现:
1、新建条件组应 包含 条件a:EmpNo == ‘001’
2、新建条件组应 包含条件组2, 条件组2包含条件b:EmpNo == ‘002’ 、条件c:Eamil == ‘cuizhexin’
3、新建条件组 应当是条件a 和条件组2的父级
4、条件a 和条件组2 应当是平级
如下图:
6.2.1 Sys_Dev_Cond_Group (条件组表)
数据库设计(所有表均需继承公共表,包含ID、创建时间、是否删除等公共属性)
create table Sys_Dev_Rule_Cond_Group
(
Id bigint not null
constraint PK_Sys_Dev_Rule_Cond_Group_Id
primary key,
rule_id bigint not null,
logic_symbol nvarchar(255) not null,
parent_id bigint,
CreateTime datetime,
UpdateTime datetime,
CreateEmpNo nvarchar(255),
UpdateEmpNo nvarchar(255),
CreateEmp nvarchar(255),
UpdateEmp nvarchar(255),
CreateDeptNo nvarchar(255),
UpdateDeptNo nvarchar(255),
ExtJson nvarchar(max),
IsDelete bit
)
go
exec sp_addextendedproperty 'MS_Description', N'条件组', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group'
go
exec sp_addextendedproperty 'MS_Description', 'Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group', 'COLUMN', 'Id'
go
exec sp_addextendedproperty 'MS_Description', N'策略ID', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group', 'COLUMN',
'rule_id'
go
exec sp_addextendedproperty 'MS_Description', N'逻辑操作符', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
'COLUMN', 'logic_symbol'
go
exec sp_addextendedproperty 'MS_Description', N'父条件配置ID,为空时,应为顶级条件组', 'SCHEMA', 'dbo', 'TABLE',
'Sys_Dev_Rule_Cond_Group', 'COLUMN', 'parent_id'
go
exec sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
'COLUMN', 'CreateTime'
go
exec sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
'COLUMN', 'UpdateTime'
go
exec sp_addextendedproperty 'MS_Description', N'创建者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
'COLUMN', 'CreateEmpNo'
go
exec sp_addextendedproperty 'MS_Description', N'修改者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
'COLUMN', 'UpdateEmpNo'
go
exec sp_addextendedproperty 'MS_Description', N'创建人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group', 'COLUMN',
'CreateEmp'
go
exec sp_addextendedproperty 'MS_Description', N'更新人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group', 'COLUMN',
'UpdateEmp'
go
exec sp_addextendedproperty 'MS_Description', N'创建者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
'COLUMN', 'CreateDeptNo'
go
exec sp_addextendedproperty 'MS_Description', N'修改者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
'COLUMN', 'UpdateDeptNo'
go
exec sp_addextendedproperty 'MS_Description', N'扩展信息', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
'COLUMN', 'ExtJson'
go
exec sp_addextendedproperty 'MS_Description', N'软删除', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group', 'COLUMN',
'IsDelete'
go
6.4、资源管理
资源管理包含菜单及操作的管理,为树形结构
6.4.1 Sys_Dev_Resource (资源表)
数据库设计(所有表均需继承公共表,包含ID、创建时间、是否删除等公共属性)
-- auto-generated definition
create table Sys_Dev_Resource
(
Id bigint not null
constraint PK_Sys_Dev_Resource_Id
primary key,
ParentId bigint,
Title nvarchar(200) not null,
Name nvarchar(200),
Code nvarchar(200),
Category nvarchar(200) not null,
Module bigint,
MenuType nvarchar(200),
Path nvarchar(255),
Component nvarchar(200),
Icon nvarchar(200),
Color nvarchar(200),
SortCode int,
HasTopBanner bit,
TopBannerComponent nvarchar(200),
HasSiderBar bit,
Affix bit,
IsActiveMenu bit,
IsHide bit,
IsDisable bit,
CreateTime datetime,
UpdateTime datetime,
CreateEmpNo nvarchar(255),
UpdateEmpNo nvarchar(255),
CreateEmp nvarchar(255),
UpdateEmp nvarchar(255),
CreateDeptNo nvarchar(255),
UpdateDeptNo nvarchar(255),
ExtJson nvarchar(max),
IsDelete bit
)
go
exec sp_addextendedproperty 'MS_Description', N'资源', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource'
go
exec sp_addextendedproperty 'MS_Description', 'Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN', 'Id'
go
exec sp_addextendedproperty 'MS_Description', N'父id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'ParentId'
go
exec sp_addextendedproperty 'MS_Description', N'标题', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN', 'Title'
go
exec sp_addextendedproperty 'MS_Description', N'别名', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN', 'Name'
go
exec sp_addextendedproperty 'MS_Description', N'编码', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN', 'Code'
go
exec sp_addextendedproperty 'MS_Description', N'分类', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'Category'
go
exec sp_addextendedproperty 'MS_Description', N'所属模块Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'Module'
go
exec sp_addextendedproperty 'MS_Description', N'菜单类型', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'MenuType'
go
exec sp_addextendedproperty 'MS_Description', N'路径', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN', 'Path'
go
exec sp_addextendedproperty 'MS_Description', N'组件', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'Component'
go
exec sp_addextendedproperty 'MS_Description', N'图标', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN', 'Icon'
go
exec sp_addextendedproperty 'MS_Description', N'颜色', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN', 'Color'
go
exec sp_addextendedproperty 'MS_Description', N'排序码', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'SortCode'
go
exec sp_addextendedproperty 'MS_Description', N'是否拥有头部Banner', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource',
'COLUMN', 'HasTopBanner'
go
exec sp_addextendedproperty 'MS_Description', N'头部Banner组件地址', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource',
'COLUMN', 'TopBannerComponent'
go
exec sp_addextendedproperty 'MS_Description', N'是否拥有侧边菜单', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource',
'COLUMN', 'HasSiderBar'
go
exec sp_addextendedproperty 'MS_Description', N'是否首页', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'Affix'
go
exec sp_addextendedproperty 'MS_Description', N'是否默认激活', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'IsActiveMenu'
go
exec sp_addextendedproperty 'MS_Description', N'是否隐藏', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'IsHide'
go
exec sp_addextendedproperty 'MS_Description', N'是否禁用', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'IsDisable'
go
exec sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'CreateTime'
go
exec sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'UpdateTime'
go
exec sp_addextendedproperty 'MS_Description', N'创建者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'CreateEmpNo'
go
exec sp_addextendedproperty 'MS_Description', N'修改者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'UpdateEmpNo'
go
exec sp_addextendedproperty 'MS_Description', N'创建人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'CreateEmp'
go
exec sp_addextendedproperty 'MS_Description', N'更新人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'UpdateEmp'
go
exec sp_addextendedproperty 'MS_Description', N'创建者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'CreateDeptNo'
go
exec sp_addextendedproperty 'MS_Description', N'修改者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'UpdateDeptNo'
go
exec sp_addextendedproperty 'MS_Description', N'扩展信息', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'ExtJson'
go
exec sp_addextendedproperty 'MS_Description', N'软删除', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
'IsDelete'
go
6.5、资源与策略关联管理
6.5.1 Sys_Dev_Res_Rule_Real (资源与策略关联表)
数据库设计(所有表均需继承公共表,包含ID、创建时间、是否删除等公共属性)
-- auto-generated definition
create table Sys_Dev_Res_Rule_Real
(
Id bigint not null
constraint PK_Sys_Dev_Res_Rule_Real_Id
primary key,
res_id bigint not null,
rule_id bigint not null,
ExtJson nvarchar(max),
IsDelete bit
)
go
exec sp_addextendedproperty 'MS_Description', N'资源策略关联表', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Res_Rule_Real'
go
exec sp_addextendedproperty 'MS_Description', 'Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Res_Rule_Real', 'COLUMN', 'Id'
go
exec sp_addextendedproperty 'MS_Description', N'资源Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Res_Rule_Real', 'COLUMN',
'res_id'
go
exec sp_addextendedproperty 'MS_Description', N'策略Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Res_Rule_Real', 'COLUMN',
'rule_id'
go
exec sp_addextendedproperty 'MS_Description', N'扩展信息', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Res_Rule_Real', 'COLUMN',
'ExtJson'
go
exec sp_addextendedproperty 'MS_Description', N'软删除', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Res_Rule_Real', 'COLUMN',
'IsDelete'
go
7、.net 核心代码实现
7.1 获取当前登录用户信息
也是用户基础属性的数据来源,通过UserManager.属性 获取到当前登录用户的动态信息。这里只介绍概念,具体实现视业务而定
/// <summary>
/// 当前登录用户信息
/// </summary>
public class UserManager
{
/// <summary>
/// 当前用户EmpNo
/// </summary>
public static string EmpNo => App.User?.FindFirst(ClaimConst.EmpNo)?.Value;
/// <summary>
/// 当前用户账号
/// </summary>
public static string Email => App.User?.FindFirst(ClaimConst.Email)?.Value;
/// <summary>
/// 当前用户名称
/// </summary>
public static string EmpName => App.User?.FindFirst(ClaimConst.Name)?.Value;
/// <summary>
/// 机构ID
/// </summary>
public static string DeptNo => App.User?.FindFirst(ClaimConst.DeptNo)?.Value;
/// <summary>
/// 签核权限等级
/// </summary>
public static string ApproveLevel => App.User?.FindFirst(ClaimConst.ApproveLevel)?.Value;
/// <summary>
/// OrgCode
/// </summary>
public static string OrgCode => App.User?.FindFirst(ClaimConst.OrgCode)?.Value;
/// <summary>
/// Extend 扩展字段
/// </summary>
public static string ExtJson => App.User?.FindFirst(ClaimConst.ExtJson)?.Value;
}
7.2 新增策略
/// <summary>
/// 新增策略
/// </summary>
/// <param name="rule"></param>
/// <exception cref="Exception"></exception>
public void AddRule(DevRuleDto rule)
{
try
{
// 1、开启事务
repository.BeginTran();
// 2、新增策略
var devRule = repository.InsertReturnEntity(new SysDevRule
{
RuleName = rule.RuleName,
RuleDesc = rule.RuleDesc,
}.ToCreate());
// 3、递归新增条件组
CreateSubConditionGroups(rule.RuleCondGroup, devRule.Id);
// 4、提交事务
repository.CommitTran();
}
catch (Exception ex)
{
repository.RollbackTran();
throw new Exception("新增策略出错,请联系管理员", ex);
}
}
7.3 递归新增条件组
/// <summary>
/// 递归新增条件组
/// </summary>
/// <param name="group"></param>
/// <param name="ruleId"></param>
private void CreateSubConditionGroups(DevRuleCondGroupDto group, long ruleId)
{
// 如果group为空,则直接返回
if (group == null)
{
return;
}
// 插入SysDevRuleCondGroup记录
var condGroupId = repository.Change<SysDevRuleCondGroup>().InsertReturnEntity(
new SysDevRuleCondGroup
{
LogicSymbol = group.LogicSymbol,
RuleId = ruleId,
ParentId = group.ParentId,
}.ToCreate()).Id;
// 插入SysDevRuleCond记录
foreach (var condDto in group.ConditionList)
{
repository.Change<SysDevRuleCond>().Insert(new SysDevRuleCond
{
CondGroupId = condGroupId,
LeftType = condDto.LeftType,
LeftValue = condDto.LeftValue,
LeftValueType = condDto.LeftValueType,
Symbol = condDto.Symbol,
RightType = condDto.RightType,
RightValueType = condDto.RightValueType,
RightValue = condDto.RightValue,
}.ToCreate());
}
// 递归插入子条件组
foreach (var subGroup in group.ConditionGroupList.Where(subGroup => subGroup != null))
{
subGroup.ParentId = condGroupId;
CreateSubConditionGroups(subGroup, ruleId);
}
}
7.4 递归组装Expression
/// <summary>
/// 获取根节点下的所有表达式
/// </summary>
/// <param name="ruleCondGroup"></param>
/// <returns></returns>
public Expression GetAllExpression(SysDevRuleCondGroup ruleCondGroup)
{
// 初始化条件列表表达式为true常量
Expression condListBody = Expression.Constant(true);
var condExpressions = new List<Expression>();
// 遍历所有条件,目前仅支持部分类型
foreach (var cond in ruleCondGroup.ConditionList)
{
condExpressions.Add(GetExpression(cond));
}
// 根据逻辑符号合并条件表达式
condListBody = MergeExpressions(ruleCondGroup.LogicSymbol, condListBody, condExpressions);
// 递归处理子条件组并合并到当前条件表达式
if (ruleCondGroup.ConditionGroupList != null)
{
foreach (var subGroup in ruleCondGroup.ConditionGroupList)
{
var subGroupExpression = GetAllExpression(subGroup);
condListBody = MergeExpressions(ruleCondGroup.LogicSymbol, condListBody,
new List<Expression> { subGroupExpression });
}
}
return condListBody;
}
7.5 单个条件 组装表达式
/// <summary>
/// 单个条件 组装表达式
/// </summary>
/// <param name="cond"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
private Expression GetExpression(SysDevRuleCond cond)
{
switch (cond.LeftType)
{
// 用户基础属性
case DevParamConst.EmpBaseAttr:
return GetEmpBaseExpression(cond);
// 用户组织基础属性
case DevParamConst.DeptBaseAttr:
// 可以添加异常处理或日志记录
break;
// 环境属性
case DevParamConst.EnvAttr:
// 可以添加异常处理或日志记录
break;
// 自定义属性
case DevParamConst.CustomAttr:
// 可以添加异常处理或日志记录
break;
default:
throw new ArgumentException($"Unsupported condition type: {cond.LeftType}");
}
return Expression.Constant(true); // 如果不支持该条件类型,默认返回true
}
7.6 根据逻辑符号合并条件表达式
private Expression MergeExpressions(string logicSymbol, Expression existingExpr,
IEnumerable<Expression> expressions)
{
return logicSymbol switch
{
"AND" => expressions.Aggregate(existingExpr, Expression.AndAlso),
"OR" => expressions.Aggregate(existingExpr, Expression.OrElse),
_ => throw new ArgumentException($"Unsupported logic symbol: {logicSymbol}")
};
}
7.7 构造属性表达式具体实现
public Expression GetEmpBaseExpression(SysDevRuleCond cond)
{
// 判断UserManager中是否有该属性
if (typeof(UserManager).GetProperties().All(x =>
!string.Equals(x.Name, cond.LeftValue, StringComparison.CurrentCultureIgnoreCase)))
{
return null;
}
Expression? constant = null;
//构造表达式左侧
var left = Expression.Constant(typeof(UserManager).GetProperties().First(x =>
string.Equals(x.Name, cond.LeftValue, StringComparison.CurrentCultureIgnoreCase)).GetValue(null));
//构造表达式右侧
ConstantExpression? right = null;
ConstantExpression? rightStart = null;
ConstantExpression? rightEnd = null;
var requestPart = (cond.RightValue + "?empNo={empNo}").SetTemplates(new
{
empNo = UserManager.EmpNo,
});
switch (cond.RightValueType)
{
case DevParamConst.DataType_STRING:
right = Expression.Constant(cond.RightValue);
break;
case DevParamConst.DataType_LIST:
right = Expression.Constant(cond.RightValue.Split(";"));
break;
case DevParamConst.DataType_NUMBER:
right = Expression.Constant(int.Parse(cond.RightValue));
break;
case DevParamConst.DataType_DATETIME:
right = Expression.Constant(DateTime.Parse(cond.RightValue));
break;
case DevParamConst.DataType_DATETIME_RANGE:
rightStart = Expression.Constant(DateTime.Parse(cond.RightValue.Split(";")[0]));
rightEnd = Expression.Constant(DateTime.Parse(cond.RightValue.Split(";")[0]));
break;
case DevParamConst.DataType_API_STRING:
try
{
var result = requestPart.GetAsAsync<string>().Result;
right = Expression.Constant(result);
break;
}
catch (Exception e)
{
break;
}
case DevParamConst.DataType_API_NUMBER:
try
{
var result = requestPart.GetAsAsync<int>().Result;
right = Expression.Constant(result);
break;
}
catch (Exception e)
{
break;
}
case DevParamConst.DataType_API_DATETIME:
try
{
var result = requestPart.GetAsAsync<DateTime>().Result;
right = Expression.Constant(result);
break;
}
catch (Exception e)
{
break;
}
case DevParamConst.DataType_API_DATETIME_RANGE:
try
{
var result = requestPart.GetAsAsync<List<DateTime>>().Result;
rightStart = Expression.Constant(result[0]);
rightEnd = Expression.Constant(result[1]);
break;
}
catch (Exception e)
{
break;
}
case DevParamConst.DataType_API_LIST_STRING:
try
{
var result = requestPart.GetAsAsync<List<string>>().Result;
right = Expression.Constant(result);
break;
}
catch (Exception e)
{
break;
}
case DevParamConst.DataType_API_LIST_NUMBER:
try
{
var result = requestPart.GetAsAsync<List<int>>().Result;
right = Expression.Constant(result);
break;
}
catch (Exception e)
{
break;
}
}
//构造运算表达式
constant = cond.Symbol switch
{
DevParamConst.Symbol_EQUAL => Expression.Equal(left, right),
DevParamConst.Symbol_NOT_EQUAL => Expression.NotEqual(left, right),
DevParamConst.Symbol_GT => Expression.GreaterThan(left, right),
DevParamConst.Symbol_GT_EQUAL => Expression.GreaterThanOrEqual(left, right),
DevParamConst.Symbol_LT => Expression.LessThan(left, right),
DevParamConst.Symbol_LT_EQUAL => Expression.LessThanOrEqual(left, right),
DevParamConst.Symbol_CONTAIN => cond.RightValueType switch
{
DevParamConst.DataType_STRING => Expression.Call(left,
typeof(string).GetMethod("Contains", new[] { typeof(string) }), right),
DevParamConst.DataType_LIST => Expression.Call(left,
typeof(List<string>).GetMethod("Contains", new[] { typeof(List<string>) }), right),
_ => constant
},
DevParamConst.Symbol_NOT_CONTAIN => cond.RightValueType switch
{
DevParamConst.DataType_STRING => Expression.Not(Expression.Call(left,
typeof(string).GetMethod("Contain", new[] { typeof(string) }), right)),
DevParamConst.DataType_LIST => Expression.Not(Expression.Call(left,
typeof(List<string>).GetMethod("Contain", new[] { typeof(List<string>) }), right)),
_ => constant
},
DevParamConst.Symbol_IN => cond.RightValueType switch
{
DevParamConst.DataType_STRING => Expression.Call(right,
typeof(string).GetMethod("Contains", new[] { typeof(string) }), left),
DevParamConst.DataType_LIST => Expression.Call(right,
typeof(List<string>).GetMethod("Contains", new[] { typeof(List<string>) }), left),
_ => constant
},
DevParamConst.Symbol_NOT_IN => cond.RightValueType switch
{
DevParamConst.DataType_STRING => Expression.Not(Expression.Call(right,
typeof(string).GetMethod("Contain", new[] { typeof(string) }), left)),
DevParamConst.DataType_LIST => Expression.Not(Expression.Call(right,
typeof(List<string>).GetMethod("Contain", new[] { typeof(List<string>) }), left)),
_ => constant
},
DevParamConst.Symbol_BETWEEN => Expression.AndAlso(Expression.GreaterThanOrEqual(left, rightStart),
Expression.LessThanOrEqual(left, rightEnd)),
_ => constant
};
return constant;
}
public List<Expression> GetEmpExtendExpression(SysDevRuleCond cond)
{
// todo 由于开发进度要求,暂时不做实现,等API管理平台时,如有需求再做实现
return null;
}
public List<Expression> GetDeptBaseExpression(SysDevRuleCond cond)
{
return null;
}
public List<Expression> GetDeptExtendExpression(SysDevRuleCond cond)
{
return null;
}
public List<Expression> GetEnvExpression(SysDevRuleCond cond)
{
return null;
}
public List<Expression> GetCustomExpression(SysDevRuleCond cond)
{
return null;
}
8、vue核心代码
8.1 条件组
如下图,条件配置为当前页面核心。通过vue组件实现
8.2 vue组件的目录结构:
8.3 核心文件说明
8.3.1 RuleBracket.vue
<template>
<!-- 左侧花括号列 -->
<el-col class="leftBracketCol" :span="1">
<!-- 上括号部分 -->
<el-row class="topBracket" :style="{ height: bracketHeight }"></el-row>
<!-- 中间逻辑符号部分 -->
<el-row class="middleBracketRow">
<div class="middleBracket">
<!-- 显示当前逻辑关系(且/或) -->
<span v-if="logicSymbol === 'AND'">且</span>
<span v-else>或</span>
<!-- 切换逻辑关系的图标按钮 -->
<i class="el-icon-d-caret symbolIcon" @click="changeLogicSymbol"></i>
</div>
</el-row>
<!-- 下括号部分 -->
<el-row class="bottomBracket" :style="{ height: bracketHeight }"></el-row>
</el-col>
</template>
<script>
/**
* 规则括号组件
* 用于显示条件之间的逻辑关系(AND/OR)
* 包含上括号、下括号和中间的逻辑符号
*/
export default {
name: 'RuleBracket',
props: {
// 括号高度
bracketHeight: {
type: String,
required: true
},
// 逻辑符号(AND/OR)
logicSymbol: {
type: String,
default: 'AND'
}
},
methods: {
/**
* 切换逻辑符号
* 在 AND 和 OR 之间切换
*/
changeLogicSymbol() {
this.$emit('update:logicSymbol', this.logicSymbol === 'AND' ? 'OR' : 'AND')
}
}
}
</script>
<style scoped>
/* 左侧括号列容器样式 */
.leftBracketCol {
display: flex;
flex-direction: column;
align-items: flex-end;
height: 100%;
margin-top: 15px;
}
/* 上括号样式 */
.topBracket {
border-top-left-radius: 8px;
border: 1px solid rgba(28, 28, 35, 0.2);
border-right: none;
border-bottom: none;
width: 24px;
}
/* 中间逻辑符号行样式 */
.middleBracketRow {
display: flex;
justify-content: flex-end;
width: 100%;
}
/* 中间逻辑符号容器样式 */
.middleBracket {
display: flex;
align-items: center;
justify-content: space-around;
height: 32px;
width: 42px;
padding: 8px 4px;
border-radius: 4px;
margin-right: 2px;
background-color: #fff;
}
/* 下括号样式 */
.bottomBracket {
border-bottom-left-radius: 8px;
border: 1px solid rgba(28, 28, 35, 0.2);
border-right: none;
border-top: none;
width: 24px;
}
/* 逻辑符号图标样式 */
.symbolIcon {
cursor: pointer;
transition: color 0.3s;
}
/* 逻辑符号图标悬停样式 */
.symbolIcon:hover {
color: #1890ff;
}
</style>
8.3.2 RuleConditionGroup.vue
<template>
<!-- 条件组容器 -->
<el-row
class="condition-group"
type="flex"
align="middle"
style="
display: flex;
flex: 1 1;
margin-top: 15px;
border-radius: 6px;
"
>
<!-- 条件组内容区域 -->
<el-col
v-if="conditionList.length > 0"
:span="23"
style="border: 1px solid rgba(28, 28, 35, 0.08)"
>
<!-- 使用动态组件加载 RuleConfigComp,实现递归渲染 -->
<component
style="margin-top: 15px;"
:is="ruleConfigComp"
:ruleConfigData.sync="localConfig"
:attrOptions="attrOptions"
@update:ruleConfigData="handleConfigUpdate"
></component>
</el-col>
<!-- 删除按钮区域 -->
<el-col
v-if="conditionList.length > 0"
:span="1"
style="
display: inline-block;
text-align: center;
vertical-align: middle;
"
>
<el-tooltip
class="item"
effect="dark"
content="删除条件组"
placement="top"
>
<i
class="el-icon-remove-outline removeCond"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@click="handleRemove"
></i>
</el-tooltip>
</el-col>
</el-row>
</template>
<script>
/**
* 条件组组件
* 用于渲染一组条件,支持嵌套条件组
* 通过动态组件实现递归渲染
* 主要功能:
* 1. 条件组容器渲染
* 2. 递归渲染子条件
* 3. 条件组删除
*/
export default {
name: 'RuleConditionGroup',
props: {
// 当前条件组的条件列表
conditionList: {
type: Array,
default: () => []
},
// 当前条件组的子条件组列表
conditionGroupList: {
type: Array,
default: () => []
},
// 当前条件组的逻辑关系(AND/OR)
logicSymbol: {
type: String,
default: 'AND'
},
// 当前条件组在父组件中的索引
index: {
type: Number,
required: true
},
// 属性选项
attrOptions: {
type: Object,
required: true
}
},
data() {
return {
// 动态加载的 RuleConfigComp 组件
ruleConfigComp: null,
// 本地配置数据
localConfig: {
conditionList: this.conditionList,
conditionGroupList: this.conditionGroupList,
logicSymbol: this.logicSymbol
}
}
},
watch: {
// 监听props变化,更新本地配置
conditionList: {
handler(val) {
this.localConfig.conditionList = val;
},
deep: true
},
conditionGroupList: {
handler(val) {
this.localConfig.conditionGroupList = val;
},
deep: true
},
logicSymbol(val) {
this.localConfig.logicSymbol = val;
}
},
created() {
// 动态导入 RuleConfigComp 组件,避免循环依赖
import('../RuleConfigComp.vue').then(module => {
this.ruleConfigComp = module.default
})
},
methods: {
/**
* 处理配置更新
* @param {Object} newConfig - 新的配置数据
*/
handleConfigUpdate(newConfig) {
this.localConfig = newConfig;
this.handleConfigChange(newConfig);
},
/**
* 处理配置变更
* @param {Object} newConfig - 新的配置数据
*/
handleConfigChange(newConfig) {
// 向父组件发送更新事件
this.$emit('update:conditionList', newConfig.conditionList)
this.$emit('update:conditionGroupList', newConfig.conditionGroupList)
this.$emit('update:logicSymbol', newConfig.logicSymbol)
// 同时发送一个统一的change事件
this.$emit('change', {
index: this.index,
config: newConfig
})
},
/**
* 删除当前条件组
*/
handleRemove() {
this.$emit('remove', this.index)
},
/**
* 鼠标移入事件处理
* @param {Event} e - 事件对象
*/
handleMouseEnter(e) {
this.$emit('mouse-enter', e)
},
/**
* 鼠标移出事件处理
* @param {Event} e - 事件对象
*/
handleMouseLeave(e) {
this.$emit('mouse-leave', e)
}
}
}
</script>
<style scoped>
/* 删除按钮样式 */
.removeCond {
color: #606266;
margin: 0;
font-size: 30px;
vertical-align: middle;
cursor: pointer;
}
.condition-group {
margin-top: 15px;
}
/* 删除按钮悬停样式 */
.removeCond:hover {
color: #f56c6c;
}
</style>
8.3.3 RuleConditionItem.vue
<template>
<!-- 条件项容器 -->
<el-row class="condition-item" :gutter="5">
<!-- 属性分类选择 -->
<el-col :span="6">
<el-select
@change="handleAttrTypeChange"
size="small"
style="width: 100%"
v-model="condition.leftType"
clearable
placeholder="请选择属性分类"
>
<el-option
v-for="dict in attrTypeOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-col>
<!-- 属性选择 -->
<el-col :span="5">
<el-select
:disabled="!condition.leftType"
size="small"
style="width: 100%"
clearable
v-model="condition.leftValue"
@change="handleLeftValueChange"
placeholder="请选择属性"
>
<el-option
v-for="dict in currentAttrOptions"
:key="dict.paramValue"
:label="dict.paramName"
:value="dict.paramValue"
/>
</el-select>
</el-col>
<!-- 运算符选择 -->
<el-col :span="3">
<el-select
:disabled="!condition.leftValueType"
size="small"
style="width: 100%"
v-model="condition.symbol"
placeholder="请选择关系"
@change="handleSymbolChange"
>
<el-option
v-for="dict in symbolOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-col>
<!-- 右值类型选择 -->
<el-col :span="4">
<el-select
:disabled="!condition.symbol"
size="small"
style="width: 100%"
v-model="condition.rightValueType"
placeholder="选择值类型"
@change="handleRightValueTypeChange"
>
<el-option
v-for="type in rightValueTypes"
:key="type.value"
:label="type.label"
:value="type.value"
/>
</el-select>
</el-col>
<!-- 右值输入框(未选择类型时显示) -->
<el-col v-show="!condition.rightValueType" :span="5">
<el-input size="small" disabled></el-input>
</el-col>
<!-- 静态值输入区域 -->
<el-col v-show="isStaticValueType" :span="5">
<!-- 字符串输入框 -->
<el-input
v-if="isStringType"
size="small"
v-model="condition.rightValue"
placeholder="请输入字符串值"
@input="handleInputChange($event, condition)"
></el-input>
<!-- 数字输入框 -->
<el-input-number
v-if="isNumberType"
size="small"
v-model="condition.rightValue"
placeholder="请输入数字"
@change="handleInputChange($event, condition)"
></el-input-number>
<!-- 日期时间选择器 -->
<el-date-picker
v-if="isDateTimeType"
size="small"
v-model="dateValue"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="选择日期时间"
@change="handleInputChange($event, condition)"
/>
<!-- 日期区间选择器 -->
<el-date-picker
v-if="isDateTimeRangeType"
size="small"
v-model="dateRangeValue"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:style="{ width: '100%' }"
:picker-options="{
shortcuts: [{
text: '最近一周',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近一个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
}
}, {
text: '最近三个月',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
picker.$emit('pick', [start, end]);
}
}]
}"
@change="handleInputChange($event, condition)"
/>
<!-- 列表值输入框 -->
<el-input
v-if="isListType"
size="small"
v-model="condition.rightValue"
placeholder="多个值请用英文分号分隔"
@input="handleInputChange($event, condition)"
></el-input>
<!-- 布尔值选择器 -->
<el-select
v-if="isBooleanType"
size="small"
style="width: 100%"
v-model="condition.rightValue"
placeholder="请选择"
@change="handleInputChange($event, condition)"
>
<el-option label="true" value="true"></el-option>
<el-option label="false" value="false"></el-option>
</el-select>
<!-- 禁用状态输入框 -->
<el-input
v-if="!condition.leftValueType"
size="small"
disabled
></el-input>
</el-col>
<!-- API返回值输入区域 -->
<el-col v-show="isApiValueType" :span="5">
<el-input
size="small"
v-model="condition.rightValue"
placeholder="请输入GET方式API地址"
@input="handleInputChange($event, condition)"
></el-input>
</el-col>
<!-- 删除按钮 -->
<el-col :span="1" style="text-align: center">
<el-tooltip class="item" effect="dark" content="删除条件" placement="top">
<i
class="el-icon-remove-outline remove-cond"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@click="handleRemove"
></i>
</el-tooltip>
</el-col>
</el-row>
</template>
<script>
/**
* 条件项组件
* 用于配置单个条件的具体内容
* 包含以下功能:
* 1. 属性分类选择
* 2. 属性选择
* 3. 运算符选择
* 4. 右值类型选择
* 5. 右值输入(支持多种类型)
* 6. 条件删除
*/
export default {
name: "RuleConditionItem",
props: {
// 当前条件项数据
condition: {
type: Object,
required: true,
},
// 属性选项数据
attrOptions: {
type: Object,
required: true,
},
// 当前条件项索引
index: {
type: Number,
required: true,
},
},
data() {
return {
// 用于存储转换后的日期值
dateValue: null,
dateRangeValue: null,
};
},
watch: {
"condition.rightValue": {
immediate: true,
deep: true,
handler(val) {
if (!val) {
this.dateValue = this.dateRangeValue = null;
return;
}
// 处理单个日期时间
if (this.condition.rightValueType === "DATETIME") {
try {
// 如果已经是目标格式,直接使用
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(val)) {
this.dateValue = val;
return;
}
// 统一处理日期转换
const date = this.parseDate(val);
this.dateValue = date ? this.formatDate(date) : null;
} catch (error) {
console.warn("日期解析错误:", error.message);
this.dateValue = null;
}
}
// 处理日期时间范围
else if (this.condition.rightValueType === "DATETIME_RANGE") {
try {
const dateRange = typeof val === "string" ? val.trim().split(";") : Array.isArray(val) ? val : null;
if (!dateRange?.length === 2) {
throw new Error("无效的日期范围格式");
}
this.dateRangeValue = dateRange.map(dateStr => {
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(dateStr)) {
return dateStr;
}
const date = this.parseDate(dateStr);
return date ? this.formatDate(date) : null;
});
// 如果任一日期解析失败,清空整个范围
if (this.dateRangeValue.includes(null)) {
this.dateRangeValue = null;
}
} catch (error) {
console.warn("日期区间解析错误:", error.message);
this.dateRangeValue = null;
}
}
}
},
},
// 字典数据配置
dicts: [
"AttrType",
"STRING_SYMBOL",
"NUMBER_SYMBOL",
"DATETIME_SYMBOL",
"LIST_SYMBOL",
"BOOLEAN_SYMBOL",
],
computed: {
// 属性类型选项
attrTypeOptions() {
return this.dict.type.AttrType || [];
},
// 当前属性类型的选项
currentAttrOptions() {
return this.attrOptions[this.condition.leftType] || [];
},
// 运算符选项
symbolOptions() {
return this.dict.type[this.condition.leftValueType + "_SYMBOL"] || [];
},
// 右值类型选项
rightValueTypes() {
return [
{ label: "字符串", value: "STRING" },
{ label: "数字", value: "NUMBER" },
{ label: "日期", value: "DATETIME" },
{ label: "日期区间", value: "DATETIME_RANGE" },
{ label: "LIST", value: "LIST" },
{ label: "BOOLEAN", value: "BOOLEAN" },
{ label: "API返回:STRING", value: "API_STRING" },
{ label: "API返回:NUMBER", value: "API_NUMBER" },
{ label: "API返回:BOOLEAN", value: "API_BOOLEAN" },
{ label: "API返回:LIST<STRING>", value: "API_LIST<STRING>" },
{ label: "API返回:LIST<NUMBER>", value: "API_LIST<NUMBER>" },
];
},
// 是否为字符串类型
isStringType() {
return this.condition.rightValueType === "STRING";
},
// 是否为数字类型
isNumberType() {
return this.condition.rightValueType === "NUMBER";
},
// 是否为日期时间类型
isDateTimeType() {
return this.condition.rightValueType === "DATETIME";
},
// 是否为日期区间类型
isDateTimeRangeType() {
return this.condition.rightValueType === "DATETIME_RANGE";
},
// 是否为列表类型
isListType() {
return this.condition.rightValueType === "LIST";
},
// 是否为布尔类型
isBooleanType() {
return this.condition.rightValueType === "BOOLEAN";
},
// 是否为静态值类型
isStaticValueType() {
return [
"STRING",
"NUMBER",
"DATETIME",
"DATETIME_RANGE",
"LIST",
"BOOLEAN",
].includes(this.condition.rightValueType);
},
// 是否为API返回值类型
isApiValueType() {
return this.condition.rightValueType?.startsWith("API_");
},
},
methods: {
/**
* 属性类型变更处理
* @param {string} val - 新的属性类型值
*/
handleAttrTypeChange(val) {
this.$emit("attr-type-change", val, this.condition);
},
/**
* 属性值变更处理
* @param {string} val - 新的属性值
*/
handleLeftValueChange(val) {
this.$emit("left-value-change", val, this.condition);
},
/**
* 运算符变更处理
* @param {string} val - 新的运算符值
*/
handleSymbolChange(val) {
this.$emit("symbol-change", val, this.condition);
},
/**
* 右值类型变更处理
* @param {string} val - 新的右值类型
*/
handleRightValueTypeChange(val) {
// 先清空日期相关的值
this.dateValue = null;
this.dateRangeValue = null;
// 清空 rightValue
this.$set(this.condition, "rightValue", null);
// 设置新的类型
this.$set(this.condition, "rightValueType", val);
// 触发父组件更新
this.$emit("right-value-type-change", val, this.condition);
},
/**
* 处理输入值变更
* @param {any} value - 新的输入值
* @param {Object} condition - 当前条件对象
*/
handleInputChange(value, condition) {
let finalValue = value;
// 处理日期时间类型的值
if (condition.rightValueType === "DATETIME") {
if (value) {
// 使用 value-format 指定的格式,不需要额外转换
finalValue = value;
this.dateValue = value;
} else {
finalValue = null;
this.dateValue = null;
}
} else if (condition.rightValueType === "DATETIME_RANGE") {
if (Array.isArray(value) && value.length === 2) {
// 使用 value-format 指定的格式,直接用分号连接
finalValue = value.join(";");
this.dateRangeValue = value;
} else {
finalValue = null;
this.dateRangeValue = null;
}
}
// 更新本地数据
this.$set(condition, "rightValue", finalValue);
// 触发父组件更新
this.$emit("input-change", finalValue, condition);
},
/**
* 删除条件处理
*/
handleRemove() {
this.$emit("remove", this.index);
},
/**
* 鼠标移入事件处理
* @param {Event} e - 事件对象
*/
handleMouseEnter(e) {
this.$emit("mouse-enter", e);
},
/**
* 鼠标移出事件处理
* @param {Event} e - 事件对象
*/
handleMouseLeave(e) {
this.$emit("mouse-leave", e);
},
/**
* 格式化日期为 'yyyy-MM-dd HH:mm:ss' 格式
* @param {Date} date - 日期对象
* @returns {string} 格式化后的日期字符串
*/
formatDate(date) {
const pad = (num) => String(num).padStart(2, "0");
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
const seconds = pad(date.getSeconds());
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
/**
* 解析各种格式的日期字符串或时间戳为Date对象
* @param {string|number} value - 要解析的日期值
* @returns {Date|null} 解析后的Date对象,解析失败返回null
*/
parseDate(value) {
try {
if (typeof value === "string") {
if (value.includes("T")) {
// 处理ISO格式,自动处理时区转换
const utcDate = new Date(value.trim());
return new Date(utcDate.getTime() + utcDate.getTimezoneOffset() * 60000);
}
// 处理时间戳字符串
if (!isNaN(value)) {
return new Date(parseInt(value));
}
}
// 处理数字类型时间戳
if (typeof value === "number") {
return new Date(value);
}
return null;
} catch {
return null;
}
},
},
};
</script>
<style scoped>
.condition-item {
/* 第一个不用margin-top */
&:first-child {
margin-top: 0;
}
margin-top: 15px;
}
/* 日期选择器样式 */
:deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__inner) {
width: 100%;
}
:deep(.el-date-editor .el-range-separator) {
padding: 0;
width: 20px;
}
:deep(.el-date-editor .el-range__icon) {
margin-left: -5px;
}
:deep(.el-range-editor.el-input__inner) {
padding: 3px 5px;
}
/* 删除按钮样式 */
.remove-cond {
color: #606266;
margin: 0;
font-size: 30px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
}
/* 删除按钮悬停样式 */
.remove-cond:hover {
color: #f56c6c;
}
</style>
8.3.4 RuleConditionItem.vue
<template>
<div>
<!-- 规则配置主容器 -->
<el-row ref="configRef" :gutter="5" style="padding-left: 10px">
<!-- 左侧花括号区域,显示逻辑关系 -->
<rule-bracket
ref="leftBracket"
v-if="showLeftBracket"
:bracketHeight="bracketHeight"
:logicSymbol="ruleConfig.logicSymbol"
@update:logicSymbol="changeLogicSymbol"
/>
<!-- 右侧条件区域 -->
<el-col ref="conditionCol" :span="showLeftBracket ? 23 : 24">
<!-- 条件列表渲染 -->
<rule-condition-item
v-for="(item, index) in ruleConfig.conditionList"
:key="index"
:condition="item"
:attrOptions="attrOptions"
:index="index"
class="rule-config__item"
@attr-type-change="changeAttrType"
@left-value-change="changeLeftValueAndType"
@symbol-change="changeSymbol"
@right-value-type-change="changeRightValueType"
@input-change="changeInput"
@remove="remove_click"
@mouse-enter="remove_enter"
@mouse-leave="remove_leave"
/>
<!-- 条件组列表渲染 -->
<rule-condition-group
v-for="(item, index) in ruleConfig.conditionGroupList"
:key="'group-' + index"
:conditionList="item.conditionList"
:conditionGroupList="item.conditionGroupList"
:logicSymbol="item.logicSymbol"
:index="index"
:attrOptions="attrOptions"
class="rule-config__item"
@remove="remove_group_click"
@mouse-enter="remove_enter"
@mouse-leave="remove_leave"
@change="handleGroupChange"
@update:conditionList="(val) => updateGroupConditionList(index, val)"
@update:conditionGroupList="(val) => updateGroupConditionGroupList(index, val)"
@update:logicSymbol="(val) => updateGroupLogicSymbol(index, val)"
/>
<!-- 操作按钮区域 -->
<el-row class="button-area" style="margin-top: 10px">
<el-button type="text" @click="addCond">新增条件</el-button>
<el-divider direction="vertical"></el-divider>
<el-dropdown @command="handleCommand" size="small" trigger="click">
<span class="el-dropdown-link">
<el-button type="text">
<i class="el-icon-caret-bottom"></i>
</el-button>
</span>
<el-dropdown-menu slot="dropdown" style="margin: 0 !important">
<el-dropdown-item command="addGroup">添加条件组</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-row>
</el-col>
</el-row>
</div>
</template>
<script>
import { GetParamDictGroupByAttrType } from "@/api/system/dev/param";
import { debounce } from "lodash";
import RuleBracket from "./components/RuleBracket.vue";
import RuleConditionGroup from "./components/RuleConditionGroup.vue";
import RuleConditionItem from "./components/RuleConditionItem.vue";
/**
* 规则配置组件
* @component
* @example
* <rule-config-comp
* v-model="ruleConfig"
* @change="handleConfigChange"
* />
*
* @property {Object} ruleConfigData - 规则配置数据
* @property {Array} ruleConfigData.conditionList - 条件列表
* @property {Array} ruleConfigData.conditionGroupList - 条件组列表
* @property {String} ruleConfigData.logicSymbol - 逻辑符号(AND/OR)
*
* @event {Function} change - 配置变更事件
* @event {Function} error - 错误事件
*/
export default {
name: "RuleConfigComp",
components: {
RuleBracket,
RuleConditionItem,
RuleConditionGroup,
},
// 字典数据配置
dicts: [
"AttrType",
"STRING_SYMBOL",
"NUMBER_SYMBOL",
"DATETIME_SYMBOL",
"LIST_SYMBOL",
"BOOLEAN_SYMBOL",
],
props: {
// 规则配置数据
ruleConfigData: {
type: Object,
required: true,
},
},
data() {
return {
// 属性选项数据
attrOptions: {},
// 左侧括号高度
bracketHeight: "",
// 当前规则配置数据
ruleConfig: null,
defaultCondition: {
leftValue: null,
leftType: null,
leftValueType: null,
symbol: null,
rightValue: null,
rightValueType: null,
},
defaultGroup: {
conditionList: [this.defaultCondition],
logicSymbol: "AND",
conditionGroupList: [],
},
debouncedCalcHeight: null,
};
},
computed: {
/**
* 计算是否显示左侧括号
* 当条件数量大于1或存在条件组时显示
*/
showLeftBracket() {
return (
this.ruleConfig.conditionList.length > 1 ||
(this.ruleConfig.conditionList.length === 1 &&
this.ruleConfig.conditionGroupList?.length > 0)
);
},
buttonAreaHeight() {
const buttonArea = this.$refs.conditionCol?.$el.querySelector(
":scope > .button-area"
);
if (!buttonArea) return 42;
const style = window.getComputedStyle(buttonArea);
return (
buttonArea.offsetHeight +
parseInt(style.marginTop) +
parseInt(style.marginBottom)
);
},
leftBracketButtonHeight() {
const area = this.$refs.leftBracket?.$el.querySelector(
":scope > .middleBracketRow"
);
if (!area) return 0;
const style = window.getComputedStyle(area);
return (
area.offsetHeight +
parseInt(style.marginTop) +
parseInt(style.marginBottom)
);
},
},
watch: {
ruleConfigData: {
handler(val) {
this.ruleConfig = JSON.parse(JSON.stringify(val)); // 深拷贝避免直接修改 prop
},
immediate: true,
},
},
created() {
// 初始化属性字典数据
this.initParamDict();
this.debouncedCalcHeight = debounce(this.calcLeftBracketHeight, 200);
},
mounted() {
this.$nextTick(() => {
this.calcLeftBracketHeight();
// 添加窗口resize监听
window.addEventListener("resize", this.debouncedCalcHeight);
});
},
beforeDestroy() {
// 清理防抖函数
this.debouncedCalcHeight.cancel();
// 清理其他可能的定时器或监听器
},
methods: {
/**
* 初始化属性字典数据
* 从后端获取属性选项数据
*/
initParamDict() {
GetParamDictGroupByAttrType().then((res) => {
if (res.code === 200) {
this.attrOptions = res.data;
}
});
},
/**
* 处理值变更的通用方法
* @param {Object} item - 当前条件项
* @param {string} field - 要修改的字段
* @param {any} value - 新的值
* @param {Object} additionalFields - 其他需要同时修改的字段
*/
handleValueChange(item, field, value, additionalFields = {}) {
// 使用 Vue 的响应式方法设置值
this.$set(item, field, value);
// 设置额外的字段
Object.entries(additionalFields).forEach(([key, val]) => {
this.$set(item, key, val);
});
// 触发更新
this.$emit('update:ruleConfigData', this.ruleConfig);
},
/**
* 属性类型变更处理
* @param {string} val - 新的属性类型值
* @param {Object} item - 当前条件项
*/
changeAttrType(val, item) {
this.handleValueChange(item, "leftType", val, {
leftValue: "",
leftValueType: "",
});
},
/**
* 属性值变更处理
* @param {string} val - 新的属性值
* @param {Object} item - 当前条件项
*/
changeLeftValueAndType(val, item) {
this.handleValueChange(item, "leftValue", val, {
leftValueType: _.find(this.attrOptions[item.leftType], [
"paramValue",
val,
]).valueType,
});
},
/**
* 运算符变更处理
* @param {string} val - 新的运算符值
* @param {Object} item - 当前条件项
*/
changeSymbol(val, item) {
this.handleValueChange(item, "symbol", val, {
rightValueType: "",
});
},
/**
* 右值类型变更处理
* @param {string} val - 新的右值类型
* @param {Object} item - 当前条件项
*/
changeRightValueType(val, item) {
// 使用 Vue 的响应式方法设置值
this.$set(item, 'rightValueType', val);
this.$set(item, 'rightValue', null);
// 触发更新
this.$emit('update:ruleConfigData', this.ruleConfig);
},
/**
* 输入值变更处理
* @param {string} val - 新的输入值
* @param {Object} item - 当前条件项
*/
changeInput(val, item) {
if (item) {
// 确保 rightValueType 已设置
if (!item.rightValueType) {
let valueType = 'STRING';
if (typeof val === 'number') {
valueType = 'NUMBER';
} else if (val instanceof Date) {
valueType = 'DATETIME';
} else if (typeof val === 'boolean') {
valueType = 'BOOLEAN';
}
this.$set(item, 'rightValueType', valueType);
}
// 设置值
this.$set(item, 'rightValue', val);
// 触发更新
this.$emit('update:ruleConfigData', this.ruleConfig);
}
},
/**
* 计算左侧花括号高度
* 计算规则:
* 1. 获取条件列表容器的总高度
* 2. 计算所有条件项和条件组的实际内容高度
* 3. 减去边距和按钮区域后计算花括号实际需要的高度
* @returns {void}
*/
calcLeftBracketHeight() {
this.$nextTick(() => {
// 如果条件列表容器不存在,直接返回
if (!this.$refs.conditionCol) {
console.warn("条件列表容器未找到");
return;
}
try {
// 获取关键元素
const conditionCol = this.$refs.conditionCol.$el;
const leftBracket = this.$refs.leftBracket?.$el;
// 获取所有条件项和条件组
// 只查询第一级的条件项和条件组
// '> .condition-item' 表示只选择直接子元素
const conditionItems = conditionCol.querySelectorAll(
":scope > .condition-item"
);
const conditionGroups = conditionCol.querySelectorAll(
":scope > .condition-group"
);
// 如果没有条件项,直接返回
if (!conditionItems.length) {
console.warn("没有找到条件项");
return;
}
// 计算条件项和条件组的总高度
let totalContentHeight = 0;
// 计算条件项高度
conditionItems.forEach((item) => {
// 条件项的边框和内边距
const itemStyle = window.getComputedStyle(item);
const itemMarginTop = parseInt(itemStyle.marginTop) || 0;
const itemMarginBottom = 0;
totalContentHeight +=
item.offsetHeight + itemMarginTop + itemMarginBottom;
});
// 计算条件组高度
conditionGroups.forEach((group) => {
// 条件组的边框和内边距
const groupStyle = window.getComputedStyle(group);
const groupMarginTop = parseInt(groupStyle.marginTop) || 0;
const groupMarginBottom = parseInt(groupStyle.marginBottom) || 0;
totalContentHeight +=
group.offsetHeight + groupMarginTop + groupMarginBottom;
});
// 获取底部按钮区域高度
const buttonArea = conditionCol.querySelector(
":scope > .button-area"
);
const buttonAreaStyle = window.getComputedStyle(buttonArea);
const buttonAreaHeight = buttonArea
? buttonArea.offsetHeight +
parseInt(buttonAreaStyle.marginTop) +
parseInt(buttonAreaStyle.marginBottom)
: 42; // 10px 是按钮上边距
// 计算花括号实际需要的高度
// 总内容高度 - 上下边距 - 按钮区域高度
// 花括号 中间的且或高度
let leftBracketButtonAreaHeight = 0;
const leftBracketButtonArea = leftBracket?.querySelector(
":scope > .middleBracketRow"
);
if (leftBracketButtonArea) {
const leftBracketButtonAreaStyle = window.getComputedStyle(
leftBracketButtonArea
);
leftBracketButtonAreaHeight =
leftBracketButtonArea.offsetHeight +
parseInt(leftBracketButtonAreaStyle.marginTop) +
parseInt(leftBracketButtonAreaStyle.marginBottom);
} else {
leftBracketButtonAreaHeight = 0;
}
const bracketHeight = Math.max(
0,
(totalContentHeight -
buttonAreaHeight -
leftBracketButtonAreaHeight +
15) /
2
);
// 设置括号高度
this.bracketHeight = `${bracketHeight}px`;
} catch (error) {
console.error("计算括号高度时发生错误:", error);
// 设置一个默认值,确保界面不会完全崩溃
this.bracketHeight = "100px";
}
});
},
/**
* 下拉菜单命令处理
* @param {string} cmditem - 命令项
*/
handleCommand(cmditem) {
if (cmditem === "addGroup") {
this.addGroup();
}
},
/**
* 添加条件
*/
addCond() {
if (!this.ruleConfig.conditionList) {
this.ruleConfig.conditionList = [];
}
// 使用 Vue 的响应式方法添加新条件
this.$set(
this.ruleConfig.conditionList,
this.ruleConfig.conditionList.length,
{
leftValue: null,
leftType: null,
leftValueType: null,
symbol: null,
rightValue: null,
rightValueType: null // 确保初始化时设置为 null
}
);
// 触发更新
this.$emit('update:ruleConfigData', this.ruleConfig);
this.$nextTick(() => {
this.calcLeftBracketHeight();
});
},
/**
* 添加条件组
*/
addGroup() {
if (
!this.ruleConfig.conditionList ||
this.ruleConfig.conditionList.length === 0
) {
this.addCond();
}
if (!this.ruleConfig.conditionGroupList) {
this.ruleConfig.conditionGroupList = [];
}
this.$set(
this.ruleConfig.conditionGroupList,
this.ruleConfig.conditionGroupList.length,
{
conditionList: [
{
leftValue: null,
leftType: null,
leftValueType: null,
symbol: null,
rightValue: null,
rightValueType: null,
},
],
logicSymbol: "AND",
conditionGroupList: [],
}
);
this.$forceUpdate();
// 触发更新
this.$emit('update:ruleConfigData', this.ruleConfig);
this.$nextTick(() => {
this.calcLeftBracketHeight();
});
},
/**
* 删除条件
* @param {number} index - 条件索引
*/
remove_click(index) {
if (
this.ruleConfig.conditionList.length <= 1 &&
this.ruleConfig.conditionGroupList.length > 0
) {
this.$message({
type: "warning",
message: "至少需要保留一条条件!",
});
return;
}
this.$delete(this.ruleConfig.conditionList, index);
this.$forceUpdate();
// 触发更新
this.$emit('update:ruleConfigData', this.ruleConfig);
this.calcLeftBracketHeight();
},
/**
* 删除条件组
* @param {number} index - 条件组索引
*/
remove_group_click(index) {
this.$delete(this.ruleConfig.conditionGroupList, index);
this.$forceUpdate();
// 触发更新
this.$emit('update:ruleConfigData', this.ruleConfig);
this.calcLeftBracketHeight();
},
/**
* 切换逻辑关系(AND/OR)
*/
changeLogicSymbol() {
this.ruleConfig.logicSymbol =
this.ruleConfig.logicSymbol === "AND" ? "OR" : "AND";
// 触发更新
this.$emit('update:ruleConfigData', this.ruleConfig);
},
/**
* 鼠标移入删除按钮处理
* @param {Event} e - 事件对象
*/
remove_enter(e) {
// 直接获取当前元素
var parentElement = e.currentTarget.parentElement.parentElement;
// 给父元素添加class
parentElement.classList.add("styles_danger");
},
/**
* 鼠标移出删除按钮处理
* @param {Event} e - 事件对象
*/
remove_leave(e) {
// 直接获取当前元素
var parentElement = e.currentTarget.parentElement.parentElement;
// 给父元素添加class
parentElement.classList.remove("styles_danger");
},
updateConfig() {
this.$emit("change", this.ruleConfig);
},
/**
* 处理条件组数据变更
* @param {Object} data - 变更数据,包含索引和新的配置
*/
handleGroupChange(data) {
const { index, config } = data;
// 更新对应索引的条件组数据
this.$set(this.ruleConfig.conditionGroupList[index], 'conditionList', config.conditionList);
this.$set(this.ruleConfig.conditionGroupList[index], 'conditionGroupList', config.conditionGroupList);
this.$set(this.ruleConfig.conditionGroupList[index], 'logicSymbol', config.logicSymbol);
// 触发更新
this.$emit('update:ruleConfigData', this.ruleConfig);
this.$nextTick(() => {
this.calcLeftBracketHeight();
});
},
/**
* 更新条件组的条件列表
* @param {number} index - 条件组索引
* @param {Array} val - 新的条件列表
*/
updateGroupConditionList(index, val) {
this.$set(this.ruleConfig.conditionGroupList[index], 'conditionList', val);
this.$emit('update:ruleConfigData', this.ruleConfig);
},
/**
* 更新条件组的子条件组列表
* @param {number} index - 条件组索引
* @param {Array} val - 新的子条件组列表
*/
updateGroupConditionGroupList(index, val) {
this.$set(this.ruleConfig.conditionGroupList[index], 'conditionGroupList', val);
this.$emit('update:ruleConfigData', this.ruleConfig);
},
/**
* 更新条件组的逻辑符号
* @param {number} index - 条件组索引
* @param {string} val - 新的逻辑符号
*/
updateGroupLogicSymbol(index, val) {
this.$set(this.ruleConfig.conditionGroupList[index], 'logicSymbol', val);
this.$emit('update:ruleConfigData', this.ruleConfig);
},
},
model: {
prop: "ruleConfigData",
event: "change",
},
};
</script>
<style scoped>
/* 变量定义 */
:root {
--danger-bg: #fff1ec;
--primary-color: #1890ff;
--border-color: rgba(28, 28, 35, 0.08);
}
/* 基础样式 */
.rule-config__item {
margin-bottom: 10px;
}
.styles_danger {
input {
background: #fff1ec !important;
border-color: transparent !important;
}
}
/* 按钮区域样式 */
.rule-config__button-area {
margin-top: 10px;
display: flex;
align-items: center;
}
/* 组样式 */
.rule-config__group {
border: 1px solid var(--border-color);
border-radius: 6px;
margin-top: 15px;
}
/* 下拉链接样式 */
.el-dropdown-link {
cursor: pointer;
color: var(--primary-color);
}
</style>
8.4 组件调用说明
<template>
<div id="app" class="app-container backTop" style="height: 100%;overflow-y: scroll;">
<div class="app-content" style="height: 100%;">
<div style="width: 90%;margin: 0 auto; ">
<!-- 将 el-page-header 和 保存按钮 放在同一行 -->
<el-row>
<el-col :span="20">
<el-page-header @back="goBack" :content="title">
</el-page-header>
</el-col>
<el-col :span="4">
<el-button @click="submitRule" style="float:right" v-waves type="primary" size="mini">保存</el-button>
</el-col>
</el-row>
<div style="margin-top: 20px">
<el-card class="box-card" shadow="never">
<div>
<el-form ref="form" label-position="top" :model="form" size="small" :rules="rules"
:label-width="formLabelWidth">
<el-row class="form-row" :gutter="5">
<el-col :span="24">
<el-form-item label="策略名称" prop="ruleName">
<el-input v-model:value="form.ruleName"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="策略描述" prop="ruleDesc">
<el-input type="textarea" v-model:value="form.ruleDesc"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="条件配置">
<div style="background-color: #f8f8f8">
<el-card class="box-card" shadow="never">
<div style="margin:5px;">
<RuleConfigComp :ruleConfigData.sync="ruleConfig"></RuleConfigComp>
</div>
</el-card>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</el-card>
</div>
</div>
</div>
</div>
</template>
<script>
import RuleConfigComp from './RuleConfigComp.vue';
import {AddRule, EditRule, GetRule} from '@/api/system/dev/rule';
export default {
name: 'ruleAdd',
components: {RuleConfigComp},
data() {
return {
isEdit: false,
title: this.isEdit ? '编辑策略' : '新增策略',
formLabelWidth: '100px',
form: {
ruleName: null,
ruleDesc: null
},
ruleConfig: {
conditionList: [],
logicSymbol: 'AND',
conditionGroupList: []
},
rules: {
ruleName: [
{required: true, message: '请输入策略名称', trigger: 'blur'}
],
ruleDesc: [
{required: true, message: '请输入策略描述', trigger: 'blur'}
]
}
};
},
watch: {
// 监听整个 ruleConfig 对象的变化
ruleConfig: {
handler(newVal) {
console.log('规则配置发生变化:', newVal);
// 每次配置变化时,更新 form 中的 ruleCondGroup
this.form.ruleCondGroup = newVal;
},
deep: true // 深度监听,确保能监听到嵌套对象的变化
}
},
created() {
if (this.$route.params.id) {
this.isEdit = true;
this.title = '编辑策略'; // 修复标题不更新的问题
GetRule({id: this.$route.params.id}).then((res) => {
if (res.code === 200) {
this.form = res.data;
this.ruleConfig = res.data.ruleCondGroup;
} else {
this.$message({type: 'error', message: res.msg});
}
});
}
},
methods: {
goBack() {
// 返回策略管理列表页
this.$router.push({path: '/system/7161234204494139392/rule'});
},
submitRule() {
var _this = this
this.$refs.form.validate((valid) => {
if (valid) {
console.log(_this.ruleConfig);
// 校验 ruleConfig
if (_this.ruleConfig.conditionList.length === 0) {
this.$message({
type: 'error',
message: '请添加条件',
})
return false;
} else {
if (_this.validateRuleConfig(_this.ruleConfig)) {
_this.form.ruleCondGroup = _this.ruleConfig
if (_this.isEdit) {
EditRule(_this.form).then((res) => {
if (res.code === 200) {
_this.$message({
type: 'success',
message: '修改成功',
})
_this.$router.push({path: '/system/7161234204494139392/rule'});
} else {
_this.$message({
type: 'error',
message: res.data.msg,
})
}
}).catch()
} else {
AddRule(_this.form).then((res) => {
if (res.code === 200) {
_this.$message({
type: 'success',
message: '保存成功',
})
_this.$router.push({path: '/system/7161234204494139392/rule'});
} else {
_this.$message({
type: 'error',
message: res.data.msg,
})
}
}).catch()
}
}
}
} else {
return false;
}
})
},
validateRuleConfig(ruleConfig) {
// 验证conditionGroupList
if (null != ruleConfig.conditionGroupList && ruleConfig.conditionGroupList.length > 0) {
var isAllValid = true;
ruleConfig.conditionGroupList.forEach((item) => {
// 递归验证子条件组
if (!this.validateRuleConfig(item)) {
isAllValid = false;
return;
}
});
if (!isAllValid) {
return false;
}
} else {
// 验证conditionList
var isAllValid = true;
ruleConfig.conditionList.forEach((item) => {
// 验证属性分类、属性、关系、右值类型和右值
if (!this.validateConditionItem(item)) {
isAllValid = false;
return;
}
});
if (!isAllValid) {
return false;
}
}
// 所有元素都满足条件时,递归到达基本情况,返回true
return true;
},
validateConditionItem(item) {
console.log(item);
if (null == item.leftType || '' === item.leftType) {
this.$message({
type: 'error',
message: '请选择属性分类',
});
return false;
}
if (null == item.leftValue || '' === item.leftValue) {
this.$message({
type: 'error',
message: '请选择属性',
});
return false;
}
if (null == item.symbol || '' === item.symbol) {
this.$message({
type: 'error',
message: '请选择关系',
});
return false;
}
if (null == item.rightValueType || '' === item.rightValueType) {
this.$message({
type: 'error',
message: '请选择右值类型',
});
return false;
}
if (null == item.rightValue || '' === item.rightValue) {
this.$message({
type: 'error',
message: '右值不可为空',
});
return false;
}
return true;
}
}
};
</script>
<style scoped>
::v-deep .box-card {
box-sizing: border-box;
margin: 0;
padding: 0;
font-size: 14px;
background: #fff;
border: none;
margin-bottom: 20px;
}
::v-deep .box-card .el-card__body {
padding: 0;
}
::v-deep .el-card__header {
color: #293350;
border-bottom: none;
padding: 0;
margin-bottom: 10px;
}
.form-row {
display: flex;
flex-wrap: wrap;
}
.el-dropdown-link {
cursor: pointer;
color: #1890ff;
}
.topBracket {
border-top-left-radius: 8px;
border: 1px solid rgba(28, 28, 35, .08);
margin-top: 15px;
border-right: none;
border-bottom: none;
width: 24px;
}
.middleBracket {
align-items: center;
border-radius: 4px;
display: flex;
flex-direction: row;
height: 32px;
justify-content: space-around;
margin-right: 2px;
width: 42px;
padding: 8px 4px;
}
.bottomBracket {
border-bottom-left-radius: 8px;
border: 1px solid rgba(28, 28, 35, .08);
border-right: none;
width: 24px;
border-top: none;
}
.leftBracketCol {
align-items: flex-end;
display: flex;
flex-direction: column;
margin-top: 15px;
height: 100%
}
.symbolIcon {
cursor: pointer;
&:hover {
color: #1890ff;
}
}
.removeCond {
color: #606266;
margin: 0;
font-size: 30px;
vertical-align: middle;
&:hover {
color: #f56c6c;
}
}
.styles_danger {
input {
background: #fff1ec !important;
border-color: transparent !important;
}
}
</style>
更多推荐
所有评论(0)