😎1、增加前端form字段验证 2、优化接口重复请求 3、优化前端字典、下拉框组件 4、调整代码生成及其命名空间 5、其它优化
This commit is contained in:
parent
0a414a06c5
commit
9d4f3c20bd
@ -14,99 +14,41 @@ namespace Admin.NET.Core;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[SuppressSniffer]
|
[SuppressSniffer]
|
||||||
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)]
|
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)]
|
||||||
public class IdempotentAttribute : Attribute, IAsyncActionFilter
|
public class IdempotentAttribute(int intervalTime = 5, bool throwBah = true, string message = "您的操作过于频繁,请稍后再试!") : Attribute, IAsyncActionFilter
|
||||||
{
|
{
|
||||||
/// <summary>
|
private static readonly Lazy<SysCacheService> SysCacheService = new(() => App.GetService<SysCacheService>());
|
||||||
/// 请求间隔时间/秒
|
|
||||||
/// </summary>
|
|
||||||
public int IntervalTime { get; set; } = 5;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 错误提示内容
|
|
||||||
/// </summary>
|
|
||||||
public string Message { get; set; } = "你操作频率过快,请稍后重试!";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 缓存前缀: Key+请求路由+用户Id+请求参数
|
|
||||||
/// </summary>
|
|
||||||
public string CacheKey { get; set; } = CacheConst.KeyIdempotent;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 是否直接抛出异常:Ture是,False返回上次请求结果
|
|
||||||
/// </summary>
|
|
||||||
public bool ThrowBah { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 锁前缀
|
|
||||||
/// </summary>
|
|
||||||
public string LockPrefix { get; set; } = "lock_";
|
|
||||||
|
|
||||||
public IdempotentAttribute()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||||
{
|
{
|
||||||
var httpContext = context.HttpContext;
|
var httpContext = context.HttpContext;
|
||||||
var path = httpContext.Request.Path.Value.ToString();
|
var path = httpContext.Request.Path.Value;
|
||||||
var userId = httpContext.User?.FindFirstValue(ClaimConst.UserId);
|
var userId = httpContext.User.FindFirstValue(ClaimConst.UserId);
|
||||||
var cacheExpireTime = TimeSpan.FromSeconds(IntervalTime);
|
|
||||||
|
|
||||||
var parameters = JsonConvert.SerializeObject(context.ActionArguments, Formatting.None, new JsonSerializerSettings
|
var parameters = JsonConvert.SerializeObject(context.ActionArguments, Formatting.None, new JsonSerializerSettings
|
||||||
{
|
{
|
||||||
NullValueHandling = NullValueHandling.Include,
|
NullValueHandling = NullValueHandling.Include,
|
||||||
DefaultValueHandling = DefaultValueHandling.Include
|
DefaultValueHandling = DefaultValueHandling.Include
|
||||||
});
|
});
|
||||||
|
|
||||||
var cacheKey = CacheKey + MD5Encryption.Encrypt($"{path}{userId}{parameters}");
|
// 分布式锁
|
||||||
var sysCacheService = httpContext.RequestServices.GetService<SysCacheService>();
|
var md5Key = MD5Encryption.Encrypt($"{path}{userId}{parameters}");
|
||||||
try
|
using var distributedLock = SysCacheService.Value.BeginCacheLock(CacheConst.KeyIdempotent + md5Key);
|
||||||
|
if (distributedLock == null)
|
||||||
{
|
{
|
||||||
// 分布式锁
|
if (throwBah) throw Oops.Oh(message);
|
||||||
using var distributedLock = sysCacheService.BeginCacheLock($"{LockPrefix}{cacheKey}") ?? throw Oops.Oh(Message);
|
return;
|
||||||
|
|
||||||
var cacheValue = sysCacheService.Get<ResponseData>(cacheKey);
|
|
||||||
if (cacheValue != null)
|
|
||||||
{
|
|
||||||
if (ThrowBah) throw Oops.Oh(Message);
|
|
||||||
context.Result = new ObjectResult(cacheValue.Value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var resultContext = await next();
|
|
||||||
// 缓存请求结果 null 值不缓存
|
|
||||||
if (resultContext.Result is ObjectResult { Value: { } } objectResult)
|
|
||||||
{
|
|
||||||
var typeName = objectResult.Value.GetType().Name;
|
|
||||||
var responseData = new ResponseData
|
|
||||||
{
|
|
||||||
Type = typeName,
|
|
||||||
Value = objectResult.Value
|
|
||||||
};
|
|
||||||
sysCacheService.Set(cacheKey, responseData, cacheExpireTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
|
// 判断是否存在重复请求
|
||||||
|
var cacheKey = CacheConst.KeyIdempotent + "cache:" + md5Key;
|
||||||
|
var isExist = SysCacheService.Value.ExistKey(cacheKey);
|
||||||
|
if (isExist)
|
||||||
{
|
{
|
||||||
throw Oops.Oh($"{Message}-{ex}");
|
if (throwBah) throw Oops.Oh(message);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
// 标记请求
|
||||||
/// 请求结果数据
|
SysCacheService.Value.Set(cacheKey, 1, TimeSpan.FromSeconds(intervalTime));
|
||||||
/// </summary>
|
await next();
|
||||||
private class ResponseData
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 结果类型
|
|
||||||
/// </summary>
|
|
||||||
public string Type { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 请求结果
|
|
||||||
/// </summary>
|
|
||||||
public dynamic Value { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 代码生成策略工厂
|
/// 代码生成策略工厂
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 自定义模板引擎
|
/// 自定义模板引擎
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
public class TableEntityEngine : ViewEngineModel
|
public class TableEntityEngine : ViewEngineModel
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
public class TableSeedDataEngine : ViewEngineModel
|
public class TableSeedDataEngine : ViewEngineModel
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 代码生成详细配置参数
|
/// 代码生成详细配置参数
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 模板输出上下文
|
/// 模板输出上下文
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 种子数据策略基类(处理TableSeedDataEngine类型)
|
/// 种子数据策略基类(处理TableSeedDataEngine类型)
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 基础策略接口
|
/// 基础策略接口
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表策略基类(处理SysCodeGen类型)
|
/// 表策略基类(处理SysCodeGen类型)
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表实体代码生成策略类
|
/// 表实体代码生成策略类
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表种子数据代码生成策略类
|
/// 表种子数据代码生成策略类
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 主从表代码生成策略类
|
/// 主从表代码生成策略类
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 单表代码生成策略类
|
/// 单表代码生成策略类
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 关系对照代码生成策略类
|
/// 关系对照代码生成策略类
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 主从明细带树组件代码生成策略类
|
/// 主从明细带树组件代码生成策略类
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 单表带树组件代码生成策略类
|
/// 单表带树组件代码生成策略类
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
namespace Admin.NET.Core.CodeGen;
|
namespace Admin.NET.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 关系对照带树组件代码生成策略类
|
/// 关系对照带树组件代码生成策略类
|
||||||
|
|||||||
@ -112,9 +112,9 @@ public class CacheConst
|
|||||||
public const string KeyDict = "sys_dict:";
|
public const string KeyDict = "sys_dict:";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 重复请求(幂等)字典缓存
|
/// 重复请求(幂等)缓存
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string KeyIdempotent = "sys_idempotent:";
|
public const string KeyIdempotent = "sys_idempotent_lock:";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Excel临时文件缓存
|
/// Excel临时文件缓存
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
using Admin.NET.Core.CodeGen;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
//
|
//
|
||||||
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
using Admin.NET.Core.CodeGen;
|
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
|
|
||||||
namespace Admin.NET.Core;
|
namespace Admin.NET.Core;
|
||||||
|
|||||||
@ -18,16 +18,19 @@ public class SysDatabaseService : IDynamicApiController, ITransient
|
|||||||
private readonly ISqlSugarClient _db;
|
private readonly ISqlSugarClient _db;
|
||||||
private readonly IViewEngine _viewEngine;
|
private readonly IViewEngine _viewEngine;
|
||||||
private readonly CodeGenOptions _codeGenOptions;
|
private readonly CodeGenOptions _codeGenOptions;
|
||||||
|
private readonly CodeGenStrategyFactory _codeGenStrategyFactory;
|
||||||
|
|
||||||
public SysDatabaseService(UserManager userManager,
|
public SysDatabaseService(UserManager userManager,
|
||||||
ISqlSugarClient db,
|
ISqlSugarClient db,
|
||||||
IViewEngine viewEngine,
|
IViewEngine viewEngine,
|
||||||
IOptions<CodeGenOptions> codeGenOptions)
|
IOptions<CodeGenOptions> codeGenOptions,
|
||||||
|
CodeGenStrategyFactory codeGenStrategyFactory)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_db = db;
|
_db = db;
|
||||||
_viewEngine = viewEngine;
|
_viewEngine = viewEngine;
|
||||||
_codeGenOptions = codeGenOptions.Value;
|
_codeGenOptions = codeGenOptions.Value;
|
||||||
|
_codeGenStrategyFactory = codeGenStrategyFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -363,33 +366,10 @@ public class SysDatabaseService : IDynamicApiController, ITransient
|
|||||||
/// <param name="input"></param>
|
/// <param name="input"></param>
|
||||||
[ApiDescriptionSettings(Name = "CreateEntity"), HttpPost]
|
[ApiDescriptionSettings(Name = "CreateEntity"), HttpPost]
|
||||||
[DisplayName("创建实体")]
|
[DisplayName("创建实体")]
|
||||||
public void CreateEntity(CreateEntityInput input)
|
public async Task CreateEntity(CreateEntityInput input)
|
||||||
{
|
{
|
||||||
var tResult = GenerateEntity(input);
|
var template = await GenerateEntity(input);
|
||||||
var targetPath = GetEntityTargetPath(input);
|
await File.WriteAllTextAsync(template.OutPath, template.Context, Encoding.UTF8);
|
||||||
File.WriteAllText(targetPath, tResult, Encoding.UTF8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 创建实体文件内容
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ConfigId"></param>
|
|
||||||
/// <param name="TableName"></param>
|
|
||||||
/// <param name="Position"></param>
|
|
||||||
/// <param name="BaseClassName"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[DisplayName("创建实体文件内容")]
|
|
||||||
public string GenerateEntity(string ConfigId, string TableName, string Position, string BaseClassName)
|
|
||||||
{
|
|
||||||
var input = new CreateEntityInput
|
|
||||||
{
|
|
||||||
TableName = TableName,
|
|
||||||
EntityName = TableName.ToFirstLetterUpperCase(),
|
|
||||||
ConfigId = ConfigId,
|
|
||||||
Position = string.IsNullOrWhiteSpace(Position) ? "Admin.NET.Application" : Position,
|
|
||||||
BaseClassName = string.IsNullOrWhiteSpace(BaseClassName) ? "EntityBaseId" : BaseClassName
|
|
||||||
};
|
|
||||||
return GenerateEntity(input);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -397,53 +377,12 @@ public class SysDatabaseService : IDynamicApiController, ITransient
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input"></param>
|
/// <param name="input"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
[ApiDescriptionSettings(Name = "GenerateEntity"), HttpPost]
|
||||||
[DisplayName("创建实体文件内容")]
|
[DisplayName("创建实体文件内容")]
|
||||||
public string GenerateEntity(CreateEntityInput input)
|
public async Task<TemplateContextOutput> GenerateEntity(CreateEntityInput input)
|
||||||
{
|
{
|
||||||
var config = App.GetOptions<DbConnectionOptions>().ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == input.ConfigId);
|
var strategy = _codeGenStrategyFactory.GetStrategy<CreateEntityInput>(CodeGenSceneEnum.TableEntity);
|
||||||
input.Position = string.IsNullOrWhiteSpace(input.Position) ? "Admin.NET.Application" : input.Position;
|
return (await strategy.GenerateCode(input))?.FirstOrDefault();
|
||||||
input.EntityName = string.IsNullOrWhiteSpace(input.EntityName)
|
|
||||||
? (config.DbSettings.EnableUnderLine ? CodeGenHelper.CamelColumnName(input.TableName, null) : input.TableName)
|
|
||||||
: input.EntityName;
|
|
||||||
string[] dbColumnNames = [];
|
|
||||||
// Entity.cs.vm中是允许创建没有基类的实体的,所以这里也要做出相同的判断
|
|
||||||
if (!string.IsNullOrWhiteSpace(input.BaseClassName))
|
|
||||||
{
|
|
||||||
_codeGenOptions.EntityBaseColumn.TryGetValue(input.BaseClassName, out dbColumnNames);
|
|
||||||
if (dbColumnNames is null || dbColumnNames is { Length: 0 })
|
|
||||||
throw Oops.Oh("基类配置文件不存在此类型");
|
|
||||||
}
|
|
||||||
|
|
||||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
|
||||||
var dbColumnInfos = db.DbMaintenance.GetColumnInfosByTableName(input.TableName, false);
|
|
||||||
dbColumnInfos.ForEach(u =>
|
|
||||||
{
|
|
||||||
// 禁止字段全是大写的
|
|
||||||
if (u.DbColumnName.ToUpper() == u.DbColumnName)
|
|
||||||
throw new Exception($"字段命名规范错误:{u.DbColumnName} 字段全是大写字母,请用大驼峰式命名规范!");
|
|
||||||
|
|
||||||
u.PropertyName = config.DbSettings.EnableUnderLine ? CodeGenHelper.CamelColumnName(u.DbColumnName, dbColumnNames) : u.DbColumnName; // 转下划线后的列名需要再转回来
|
|
||||||
u.DataType = CodeGenHelper.ConvertDataType(u, config.DbType);
|
|
||||||
});
|
|
||||||
if (_codeGenOptions.BaseEntityNames.Contains(input.BaseClassName, StringComparer.OrdinalIgnoreCase))
|
|
||||||
dbColumnInfos = dbColumnInfos.Where(u => !dbColumnNames.Contains(u.PropertyName, StringComparer.OrdinalIgnoreCase)).ToList();
|
|
||||||
|
|
||||||
var dbTableInfo = db.DbMaintenance.GetTableInfoList(false).FirstOrDefault(u => u.Name == input.TableName || u.Name == input.TableName.ToLower()) ?? throw Oops.Oh(ErrorCodeEnum.db1001);
|
|
||||||
var templatePath = GetEntityTemplatePath();
|
|
||||||
var tContent = File.ReadAllText(templatePath);
|
|
||||||
var tResult = _viewEngine.RunCompileFromCached(tContent, new
|
|
||||||
{
|
|
||||||
NameSpace = $"{input.Position}.Entity",
|
|
||||||
TableName = input.TableName,
|
|
||||||
EntityName = input.EntityName,
|
|
||||||
BaseClassName = string.IsNullOrWhiteSpace(input.BaseClassName) ? "" : $": {input.BaseClassName}",
|
|
||||||
ConfigId = input.ConfigId,
|
|
||||||
Description = string.IsNullOrWhiteSpace(dbTableInfo.Description) ? input.EntityName + "业务表" : dbTableInfo.Description,
|
|
||||||
TableFields = dbColumnInfos,
|
|
||||||
AuthorName = "Admin.NET",
|
|
||||||
Email = "Admin.NET@qq.com"
|
|
||||||
});
|
|
||||||
return tResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -454,191 +393,9 @@ public class SysDatabaseService : IDynamicApiController, ITransient
|
|||||||
[DisplayName("创建种子数据")]
|
[DisplayName("创建种子数据")]
|
||||||
public async Task CreateSeedData(CreateSeedDataInput input)
|
public async Task CreateSeedData(CreateSeedDataInput input)
|
||||||
{
|
{
|
||||||
var config = App.GetOptions<DbConnectionOptions>().ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == input.ConfigId);
|
var strategy = _codeGenStrategyFactory.GetStrategy<CreateSeedDataInput>(CodeGenSceneEnum.TableSeedData);
|
||||||
input.Position = string.IsNullOrWhiteSpace(input.Position) ? "Admin.NET.Core" : input.Position;
|
var result = (await strategy.GenerateCode(input))?.FirstOrDefault();
|
||||||
|
await File.WriteAllTextAsync(result!.OutPath, result!.Context, Encoding.UTF8);
|
||||||
var templatePath = GetSeedDataTemplatePath();
|
|
||||||
var db = _db.AsTenant().GetConnectionScope(input.ConfigId);
|
|
||||||
var tableInfo = db.DbMaintenance.GetTableInfoList(false).First(u => u.Name == input.TableName); // 表名
|
|
||||||
List<DbColumnInfo> dbColumnInfos = db.DbMaintenance.GetColumnInfosByTableName(input.TableName, false); // 所有字段
|
|
||||||
IEnumerable<EntityInfo> entityInfos = await GetEntityInfos();
|
|
||||||
Type entityType = null;
|
|
||||||
foreach (var item in entityInfos)
|
|
||||||
{
|
|
||||||
if (tableInfo.Name.ToLower() != (config.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(item.DbTableName) : item.DbTableName).ToLower()) continue;
|
|
||||||
entityType = item.Type;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entityType == null) throw Oops.Oh(ErrorCodeEnum.db1003);
|
|
||||||
|
|
||||||
input.EntityName = entityType.Name;
|
|
||||||
input.SeedDataName = entityType.Name + "SeedData";
|
|
||||||
if (!string.IsNullOrWhiteSpace(input.Suffix)) input.SeedDataName += input.Suffix;
|
|
||||||
|
|
||||||
// 查询所有数据
|
|
||||||
var query = db.QueryableByObject(entityType);
|
|
||||||
// 如果 entityType.Name=="SysDictType"或"SysDictData"或"SysDictDataTenant" 加入查询条件 u.IsEnum!=1
|
|
||||||
if (entityType.Name == "SysDictType" || entityType.Name == "SysDictData" || entityType.Name == "SysDictDataTenant")
|
|
||||||
{
|
|
||||||
if (config.DbSettings.EnableUnderLine)
|
|
||||||
query = query.Where("is_enum != 1");
|
|
||||||
else
|
|
||||||
query = query.Where("IsEnum != 1");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优先用创建时间排序
|
|
||||||
DbColumnInfo orderField = dbColumnInfos.FirstOrDefault(u => u.DbColumnName.ToLower() == "create_time" || u.DbColumnName.ToLower() == "createtime");
|
|
||||||
if (orderField != null) query = query.OrderBy(orderField.DbColumnName);
|
|
||||||
// 优先用创建时间排序,再使用第一个主键排序
|
|
||||||
if (dbColumnInfos.Any(u => u.IsPrimarykey))
|
|
||||||
query = query.OrderBy(dbColumnInfos.First(u => u.IsPrimarykey).DbColumnName);
|
|
||||||
var records = ((IEnumerable)await query.ToListAsync()).ToDynamicList();
|
|
||||||
|
|
||||||
// 过滤已存在的数据
|
|
||||||
if (input.FilterExistingData && records.Count != 0)
|
|
||||||
{
|
|
||||||
// 获取实体类型-所有种数据数据类型
|
|
||||||
var entityTypes = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(SugarTable), false) && u.FullName.EndsWith("." + input.EntityName))
|
|
||||||
.Where(u => !u.GetCustomAttributes<IgnoreTableAttribute>().Any()).ToList();
|
|
||||||
if (entityTypes.Count == 1) // 只有一个实体匹配才能过滤
|
|
||||||
{
|
|
||||||
// 获取实体的主键对应的属性名称
|
|
||||||
var pkInfo = entityTypes[0].GetProperties().FirstOrDefault(u => u.GetCustomAttribute<SugarColumn>()?.IsPrimaryKey == true);
|
|
||||||
if (pkInfo != null)
|
|
||||||
{
|
|
||||||
var seedDataTypes = SeedDataHelper.GetSeedDataTypeList(entityTypes[0]);
|
|
||||||
// 可能会重名的种子数据不作为过滤项
|
|
||||||
string doNotFilterFullName1 = $"{input.Position}.SeedData.{input.SeedDataName}";
|
|
||||||
string doNotFilterFullName2 = $"{input.Position}.{input.SeedDataName}"; // Core中的命名空间没有SeedData
|
|
||||||
|
|
||||||
PropertyInfo idPropertySeedData = records[0].GetType().GetProperty("Id");
|
|
||||||
|
|
||||||
for (int i = seedDataTypes.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
string fullName = seedDataTypes[i].FullName;
|
|
||||||
if ((fullName == doNotFilterFullName1) || (fullName == doNotFilterFullName2)) continue;
|
|
||||||
|
|
||||||
// 删除重复数据
|
|
||||||
var instance = Activator.CreateInstance(seedDataTypes[i]);
|
|
||||||
var hasDataMethod = seedDataTypes[i].GetMethod("HasData");
|
|
||||||
var seedData = ((IEnumerable)hasDataMethod?.Invoke(instance, null))?.Cast<object>();
|
|
||||||
if (seedData == null) continue;
|
|
||||||
|
|
||||||
List<object> recordsToRemove = [];
|
|
||||||
foreach (var record in records)
|
|
||||||
{
|
|
||||||
object recordId = pkInfo.GetValue(record);
|
|
||||||
if (seedData.Select(d1 => idPropertySeedData.GetValue(d1)).Any(dataId => recordId != null && dataId != null && recordId.Equals(dataId)))
|
|
||||||
{
|
|
||||||
recordsToRemove.Add(record);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var itemToRemove in recordsToRemove)
|
|
||||||
{
|
|
||||||
records.Remove(itemToRemove);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查有没有 System.Text.Json.Serialization.JsonIgnore 或 Newtonsoft.Json.JsonIgnore 的属性
|
|
||||||
// 如果 JsonIgnore 和 SugarColumn 都存在,那么后成序更化时就生成这了这些字段,就需要在这里另外补充(以处理用户表SysUser中的Password为例)
|
|
||||||
var jsonIgnoreProperties = entityType.GetProperties().Where(p => (p.GetAttribute<System.Text.Json.Serialization.JsonIgnoreAttribute>() != null ||
|
|
||||||
p.GetAttribute<Newtonsoft.Json.JsonIgnoreAttribute>() != null) && p.GetAttribute<SugarColumn>() != null).ToList();
|
|
||||||
var jsonIgnoreInfo = new List<List<JsonIgnoredPropertyData>>();
|
|
||||||
if (jsonIgnoreProperties.Count > 0)
|
|
||||||
{
|
|
||||||
int recordIndex = 0;
|
|
||||||
foreach (var r in (IEnumerable)records)
|
|
||||||
{
|
|
||||||
List<JsonIgnoredPropertyData> record = [];
|
|
||||||
foreach (var item in jsonIgnoreProperties)
|
|
||||||
{
|
|
||||||
object v = item.GetValue(r);
|
|
||||||
string strValue = "null";
|
|
||||||
if (v != null)
|
|
||||||
{
|
|
||||||
strValue = v.ToString();
|
|
||||||
if (v.GetType() == typeof(string))
|
|
||||||
strValue = "\"" + strValue + "\"";
|
|
||||||
else if (v.GetType() == typeof(DateTime))
|
|
||||||
strValue = "DateTime.Parse(\"" + ((DateTime)v).ToString("yyyy-MM-dd HH:mm:ss") + "\")";
|
|
||||||
}
|
|
||||||
|
|
||||||
record.Add(new JsonIgnoredPropertyData { RecordIndex = recordIndex, Name = item.Name, Value = strValue });
|
|
||||||
}
|
|
||||||
|
|
||||||
recordIndex++;
|
|
||||||
jsonIgnoreInfo.Add(record);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有字段信息
|
|
||||||
var propertyList = entityType.GetProperties().Where(x => false == (x.GetCustomAttribute<SugarColumn>()?.IsIgnore ?? false)).ToList();
|
|
||||||
for (var i = 0; i < propertyList.Count; i++)
|
|
||||||
{
|
|
||||||
if (propertyList[i].Name != nameof(EntityBaseId.Id) || !(propertyList[i].GetCustomAttribute<SugarColumn>()?.IsPrimaryKey ?? true)) continue;
|
|
||||||
var temp = propertyList[i];
|
|
||||||
for (var j = i; j > 0; j--) propertyList[j] = propertyList[j - 1];
|
|
||||||
propertyList[0] = temp;
|
|
||||||
}
|
|
||||||
// var timeConverter = new IsoDateTimeConverter { DateTimeFormat = "yyyy-MM-dd HH:mm:ss" };
|
|
||||||
// var recordList = JsonConvert.SerializeObject(records, Formatting.Indented, timeConverter);
|
|
||||||
|
|
||||||
// 拼接种子数据
|
|
||||||
var recordList = records.Select(obj => string.Join(", ", propertyList.Select(prop =>
|
|
||||||
{
|
|
||||||
var propType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
|
||||||
object value = prop.GetValue(obj);
|
|
||||||
if (value == null) value = "null";
|
|
||||||
else if (propType == typeof(string))
|
|
||||||
{
|
|
||||||
value = $"@\"{value.ToString().Replace("\"", "\"\"")}\"";
|
|
||||||
}
|
|
||||||
else if (propType.IsEnum)
|
|
||||||
{
|
|
||||||
value = $"{propType.Name}.{value}";
|
|
||||||
}
|
|
||||||
else if (propType == typeof(bool))
|
|
||||||
{
|
|
||||||
value = (bool)value ? "true" : "false";
|
|
||||||
}
|
|
||||||
else if (propType == typeof(DateTime))
|
|
||||||
{
|
|
||||||
value = $"DateTime.Parse(\"{((DateTime)value):yyyy-MM-dd HH:mm:ss.fff}\")";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"{prop.Name}={value}";
|
|
||||||
}))).ToList();
|
|
||||||
|
|
||||||
var tContent = await File.ReadAllTextAsync(templatePath);
|
|
||||||
var data = new
|
|
||||||
{
|
|
||||||
NameSpace = $"{input.Position}.SeedData",
|
|
||||||
EntityNameSpace = entityType.Namespace,
|
|
||||||
input.TableName,
|
|
||||||
input.EntityName,
|
|
||||||
input.SeedDataName,
|
|
||||||
input.ConfigId,
|
|
||||||
tableInfo.Description,
|
|
||||||
JsonIgnoreInfo = jsonIgnoreInfo,
|
|
||||||
RecordList = recordList
|
|
||||||
};
|
|
||||||
var tResult = await _viewEngine.RunCompileAsync(tContent, data, builderAction: builder =>
|
|
||||||
{
|
|
||||||
builder.AddAssemblyReferenceByName("System.Linq");
|
|
||||||
builder.AddAssemblyReferenceByName("System.Collections");
|
|
||||||
builder.AddAssemblyReferenceByName("System.Text.RegularExpressions");
|
|
||||||
builder.AddUsing("System.Text.RegularExpressions");
|
|
||||||
builder.AddUsing("System.Collections.Generic");
|
|
||||||
builder.AddUsing("System.Linq");
|
|
||||||
});
|
|
||||||
|
|
||||||
var targetPath = GetSeedDataTargetPath(input);
|
|
||||||
await File.WriteAllTextAsync(targetPath, tResult, Encoding.UTF8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
56
Admin.NET/Admin.NET.Test/Attribute/IdempotentTest.cs
Normal file
56
Admin.NET/Admin.NET.Test/Attribute/IdempotentTest.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
|
||||||
|
//
|
||||||
|
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
|
||||||
|
//
|
||||||
|
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
|
||||||
|
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Admin.NET.Test.Attribute;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 防止重复请求测试
|
||||||
|
/// </summary>
|
||||||
|
public class IdempotentTest
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _testOutputHelper;
|
||||||
|
private const string BaseAddress = "http://localhost:5005";
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
|
||||||
|
public IdempotentTest(ITestOutputHelper testOutputHelper)
|
||||||
|
{
|
||||||
|
_testOutputHelper = testOutputHelper;
|
||||||
|
_client = new HttpClient();
|
||||||
|
_client.BaseAddress = new Uri(BaseAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> PostAsync(string action)
|
||||||
|
{
|
||||||
|
var result = await _client.PostAsync(action, null);
|
||||||
|
return await result.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task IdempotentTest1()
|
||||||
|
{
|
||||||
|
// 模拟重复请求50次
|
||||||
|
var tasks = Task.WhenAll(Enumerable.Range(0, 50).Select(u => PostAsync("/api/test/idempotentTest1?input=1")));
|
||||||
|
|
||||||
|
var resultList = await tasks;
|
||||||
|
_testOutputHelper.WriteLine(string.Join('\n', resultList));
|
||||||
|
Assert.Equal(1, resultList.Count(u => u == "请求成功"));
|
||||||
|
Assert.Equal(resultList.Length - 1, resultList.Count(u => u == "\"您的操作过于频繁,请稍后再试!\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task IdempotentTest2()
|
||||||
|
{
|
||||||
|
// 模拟重复请求50次
|
||||||
|
var tasks = Task.WhenAll(Enumerable.Range(0, 50).Select(u => PostAsync("/api/test/idempotentTest2?input=1")));
|
||||||
|
|
||||||
|
var resultList = await tasks;
|
||||||
|
_testOutputHelper.WriteLine(string.Join('\n', resultList));
|
||||||
|
Assert.Equal(1, resultList.Count(u => u == "请求成功"));
|
||||||
|
Assert.Equal(resultList.Length - 1, resultList.Count(string.IsNullOrWhiteSpace));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PropType, reactive, watch } from 'vue';
|
import { PropType, reactive, watch } from 'vue';
|
||||||
|
import { NullableNumber, NullableString } from '/@/types/global';
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'change']);
|
const emit = defineEmits(['update:modelValue', 'change']);
|
||||||
|
|
||||||
@ -105,7 +106,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-tabs v-model="state.active" class="ml5 mt5" v-bind="$attrs">
|
<el-tabs v-model="state.active" class="ml5 mt5 container" v-bind="$attrs">
|
||||||
<el-tab-pane v-for="(item, index) in state.data" :key="index" :label="item.label" :name="item.value" :disabled="item.disabled" v-show="item.visible === undefined || item.visible">
|
<el-tab-pane v-for="(item, index) in state.data" :key="index" :label="item.label" :name="item.value" :disabled="item.disabled" v-show="item.visible === undefined || item.visible">
|
||||||
<template #label v-if="(props.hideZero && item.count) || !props.hideZero">
|
<template #label v-if="(props.hideZero && item.count) || !props.hideZero">
|
||||||
<el-badge :value="item.count ?? 0" :max="props.max" :offset="props.offset">{{ item.label }}</el-badge>
|
<el-badge :value="item.count ?? 0" :max="props.max" :offset="props.offset">{{ item.label }}</el-badge>
|
||||||
@ -114,4 +115,11 @@ watch(
|
|||||||
<slot></slot>
|
<slot></slot>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</template>
|
</template>
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
361
Web/src/components/cameraDialog/cameraDialog.vue
Normal file
361
Web/src/components/cameraDialog/cameraDialog.vue
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="state.isShowDialog" width="900px" draggable :close-on-click-modal="false">
|
||||||
|
<template #header>
|
||||||
|
<div style="color: #fff">
|
||||||
|
<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-Edit /> </el-icon>
|
||||||
|
<span> 拍照 </span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-scrollbar class="camera-container">
|
||||||
|
<div v-if="state.cameraError" class="error-container">
|
||||||
|
<el-alert :title="state.cameraError" type="error" show-icon />
|
||||||
|
<!-- 提示不支持访问摄像头的指导 -->
|
||||||
|
<div style="margin-top: 16px; text-align: left; background: #fdf6ec; border-radius: 4px; padding: 10px; font-size: 13px">
|
||||||
|
<h3>提示不支持访问摄像头的配置方法:</h3>
|
||||||
|
<ol style="margin: 8px 0 0 18px; padding: 0">
|
||||||
|
<li class="mb15">
|
||||||
|
在浏览器地址栏输入
|
||||||
|
<a style="color: #409eff" href="javascript:void(0);" @click="() => comFunc.copyText(state.configUrl)">{{ state.configUrl }}</a>
|
||||||
|
并回车
|
||||||
|
</li>
|
||||||
|
<li class="mb15">
|
||||||
|
启用配置项,并在输入框粘贴当前站点地址:<a style="color: #409eff" href="javascript:void(0);" @click="() => comFunc.copyText(state.origin)">{{ state.origin }}</a>
|
||||||
|
<el-image :src="cameraConfig" style="max-width: 100%; margin-top: 8px" />
|
||||||
|
</li>
|
||||||
|
<li class="mb15">重启浏览器</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提示无权访问摄像头的指导 -->
|
||||||
|
<div style="margin-top: 16px; text-align: left; background: #f6f6f6; border-radius: 4px; padding: 10px; font-size: 13px">
|
||||||
|
<h3>提示无权访问摄像头的配置方法:</h3>
|
||||||
|
<ol style="margin: 8px 0 0 18px; padding: 0">
|
||||||
|
<li class="mb15">点击浏览器地址栏左侧图标</li>
|
||||||
|
<li class="mb15">允许浏览器访问权限</li>
|
||||||
|
<el-image :src="cameraConfig2" style="max-width: 100%; margin-top: 8px" />
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="!state.cameraError" class="content-container">
|
||||||
|
<!-- 左侧摄像头区域 -->
|
||||||
|
<div class="camera-panel">
|
||||||
|
<video ref="videoRef" autoplay playsinline muted width="400" height="300" style="background: #000"></video>
|
||||||
|
<canvas ref="canvasRef" style="display: none"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧预览区域 -->
|
||||||
|
<div class="preview-panel" style="width: 400px; height: 300px">
|
||||||
|
<div v-if="state.capturedImage" class="image-preview" style="width: 100%; height: 100%">
|
||||||
|
<el-image :src="state.capturedImage" fit="contain" :style="{ width: '100%', height: '100%' }" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="placeholder">
|
||||||
|
<i class="el-icon-picture-outline" style="font-size: 48px; color: #c0c4cc"></i>
|
||||||
|
<p style="color: #909399; margin-top: 10px">暂无照片</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="captureImage" v-if="!state.cameraError">拍照</el-button>
|
||||||
|
<el-button type="primary" :disabled="!state.capturedImage" @click="confirmImage" v-if="!state.cameraError">确认</el-button>
|
||||||
|
<el-button @click="closeDialog">取消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup name="CameraDialog">
|
||||||
|
import { reactive, ref, onUnmounted } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import commonFunction from '/@/utils/commonFunction';
|
||||||
|
import cameraConfig from '/@/assets/camera-config.png';
|
||||||
|
import cameraConfig2 from '/@/assets/camera-config2.png';
|
||||||
|
import { log } from 'console';
|
||||||
|
|
||||||
|
// 定义组件属性
|
||||||
|
interface Props {
|
||||||
|
imageWidth?: number;
|
||||||
|
imageHeight?: number;
|
||||||
|
watermarkText?: string; // 水印文字
|
||||||
|
watermarkFontSize?: number; // 水印字体大小
|
||||||
|
watermarkColor?: string; // 水印颜色
|
||||||
|
watermarkDensity?: number; // 水印密度
|
||||||
|
showWatermark?: boolean; // 是否显示水印
|
||||||
|
watermarkAngle?: number; // 水印角度
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
imageWidth: 600,
|
||||||
|
imageHeight: 400,
|
||||||
|
watermarkText: '水印',
|
||||||
|
watermarkFontSize: 20,
|
||||||
|
watermarkColor: 'rgba(255, 255, 255, 0.3)',
|
||||||
|
watermarkDensity: 0.5,
|
||||||
|
showWatermark: false,
|
||||||
|
watermarkAngle: -30,
|
||||||
|
});
|
||||||
|
|
||||||
|
const comFunc = commonFunction();
|
||||||
|
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||||
|
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||||
|
const streamRef = ref<MediaStream | null>(null);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
configUrl: 'chrome://flags/#unsafely-treat-insecure-origin-as-secure',
|
||||||
|
origin: window.location.origin,
|
||||||
|
isShowDialog: false,
|
||||||
|
cameraError: '',
|
||||||
|
capturedImage: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置面板尺寸
|
||||||
|
const panelWidth = ref(400);
|
||||||
|
const panelHeight = ref(300);
|
||||||
|
|
||||||
|
const emits = defineEmits(['confirm']);
|
||||||
|
|
||||||
|
// 打开对话框
|
||||||
|
const openDialog = () => {
|
||||||
|
state.isShowDialog = true;
|
||||||
|
state.capturedImage = '';
|
||||||
|
state.cameraError = '';
|
||||||
|
|
||||||
|
// 根据指定的图片尺寸计算面板高度,保持宽高比
|
||||||
|
const aspectRatio = props.imageWidth / props.imageHeight;
|
||||||
|
panelHeight.value = Math.round(panelWidth.value / aspectRatio);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
initCamera();
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化摄像头
|
||||||
|
const initCamera = async () => {
|
||||||
|
try {
|
||||||
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||||
|
state.cameraError = '当前浏览器不支持访问摄像头';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const constraints = {
|
||||||
|
video: {
|
||||||
|
facingMode: 'environment',
|
||||||
|
width: { ideal: props.imageWidth },
|
||||||
|
height: { ideal: props.imageHeight },
|
||||||
|
},
|
||||||
|
audio: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
streamRef.value = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
|
||||||
|
if (videoRef.value) {
|
||||||
|
videoRef.value.srcObject = streamRef.value;
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('获取摄像头权限失败:', err);
|
||||||
|
state.cameraError = '无法访问摄像头: ' + (err.message || '未知错误');
|
||||||
|
ElMessage.error('无法访问摄像头,请检查设备连接和权限设置');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拍照
|
||||||
|
const captureImage = () => {
|
||||||
|
if (!videoRef.value || !canvasRef.value) return;
|
||||||
|
|
||||||
|
const video = videoRef.value;
|
||||||
|
const canvas = canvasRef.value;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
if (!context) {
|
||||||
|
ElMessage.error('无法获取画布上下文');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目标宽高(最终图片尺寸)
|
||||||
|
const targetWidth = props.imageWidth;
|
||||||
|
const targetHeight = props.imageHeight;
|
||||||
|
canvas.width = targetWidth;
|
||||||
|
canvas.height = targetHeight;
|
||||||
|
|
||||||
|
// 视频流实际宽高
|
||||||
|
const videoWidth = video.videoWidth;
|
||||||
|
const videoHeight = video.videoHeight;
|
||||||
|
|
||||||
|
// 按组件参数宽高比例裁剪最大区域,居中裁剪
|
||||||
|
const targetRatio = targetWidth / targetHeight;
|
||||||
|
const videoRatio = videoWidth / videoHeight;
|
||||||
|
|
||||||
|
let cropWidth, cropHeight, sx, sy;
|
||||||
|
|
||||||
|
if (videoRatio > targetRatio) {
|
||||||
|
// 视频比目标宽,裁掉左右
|
||||||
|
cropHeight = videoHeight;
|
||||||
|
cropWidth = cropHeight * targetRatio;
|
||||||
|
sx = (videoWidth - cropWidth) / 2;
|
||||||
|
sy = 0;
|
||||||
|
} else {
|
||||||
|
// 视频比目标窄,裁掉上下
|
||||||
|
cropWidth = videoWidth;
|
||||||
|
cropHeight = cropWidth / targetRatio;
|
||||||
|
sx = 0;
|
||||||
|
sy = (videoHeight - cropHeight) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 居中裁剪并缩放到目标尺寸
|
||||||
|
context.drawImage(video, sx, sy, cropWidth, cropHeight, 0, 0, targetWidth, targetHeight);
|
||||||
|
|
||||||
|
// 如果启用水印功能,则绘制水印
|
||||||
|
if (props.showWatermark) {
|
||||||
|
drawWatermark(context, targetWidth, targetHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// 转换为base64图片
|
||||||
|
state.capturedImage = canvas.toDataURL('image/jpeg', 0.9);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制优化排列的水印
|
||||||
|
const drawWatermark = (context: CanvasRenderingContext2D, width: number, height: number) => {
|
||||||
|
const { watermarkText, watermarkFontSize, watermarkColor, watermarkAngle, watermarkDensity } = props;
|
||||||
|
|
||||||
|
// 保存当前绘图状态
|
||||||
|
context.save();
|
||||||
|
|
||||||
|
// 设置水印样式
|
||||||
|
context.font = `bold ${watermarkFontSize}px Arial`;
|
||||||
|
context.fillStyle = watermarkColor;
|
||||||
|
context.textAlign = 'left';
|
||||||
|
context.textBaseline = 'middle';
|
||||||
|
|
||||||
|
// 计算水印文本的尺寸
|
||||||
|
const textMetrics = context.measureText(watermarkText);
|
||||||
|
const textWidth = textMetrics.width;
|
||||||
|
const textHeight = watermarkFontSize;
|
||||||
|
|
||||||
|
// 根据密度参数计算水印间距
|
||||||
|
const density = Math.max(0.1, Math.min(1.0, watermarkDensity));
|
||||||
|
|
||||||
|
// 优化间距计算,使布局更友好
|
||||||
|
// 当密度为0.1时,间距为文本宽度的4倍
|
||||||
|
// 当密度为1.0时,间距为文本宽度的1.5倍
|
||||||
|
const spacingFactor = 5 - density * 3;
|
||||||
|
const spacingX = textWidth * spacingFactor;
|
||||||
|
const spacingY = textHeight * spacingFactor * 1.2;
|
||||||
|
|
||||||
|
// 将角度转换为弧度
|
||||||
|
const angleInRadians = (watermarkAngle * Math.PI) / 180;
|
||||||
|
|
||||||
|
// 旋转画布到指定角度
|
||||||
|
context.translate(width / 2, height / 2);
|
||||||
|
context.rotate(angleInRadians);
|
||||||
|
|
||||||
|
// 计算绘制范围
|
||||||
|
const diagonal = Math.sqrt(width * width + height * height);
|
||||||
|
const startX = -diagonal / 2;
|
||||||
|
const endX = diagonal / 2;
|
||||||
|
const startY = -diagonal / 2;
|
||||||
|
const endY = diagonal / 2;
|
||||||
|
|
||||||
|
// 在旋转后的坐标系中绘制网格状水印
|
||||||
|
for (let y = startY; y < endY; y += spacingY) {
|
||||||
|
for (let x = startX; x < endX; x += spacingX) {
|
||||||
|
// 创建交错网格效果,使布局更友好
|
||||||
|
const adjustedX = x + (Math.floor((y - startY) / spacingY) % 2 === 0 ? 0 : spacingX / 2);
|
||||||
|
context.fillText(watermarkText, adjustedX, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复绘图状态
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认使用照片
|
||||||
|
const confirmImage = () => {
|
||||||
|
if (state.capturedImage) {
|
||||||
|
emits('confirm', state.capturedImage);
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭对话框
|
||||||
|
const closeDialog = () => {
|
||||||
|
state.isShowDialog = false;
|
||||||
|
stopCamera();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 停止摄像头
|
||||||
|
const stopCamera = () => {
|
||||||
|
if (streamRef.value) {
|
||||||
|
const tracks = streamRef.value.getTracks();
|
||||||
|
tracks.forEach((track) => track.stop());
|
||||||
|
streamRef.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoRef.value) {
|
||||||
|
videoRef.value.srcObject = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.capturedImage = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件卸载时停止摄像头
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopCamera();
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ openDialog });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.camera-container {
|
||||||
|
text-align: center;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 400px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-panel {
|
||||||
|
width: 400px;
|
||||||
|
height: 300px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel {
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -160,10 +160,6 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* 查询字符输入字符醒目展示
|
|
||||||
*/
|
|
||||||
allowCreate: Boolean,
|
|
||||||
/**
|
/**
|
||||||
* 是否禁用
|
* 是否禁用
|
||||||
*/
|
*/
|
||||||
@ -301,10 +297,10 @@ const selectVisibleChange = (visible: boolean) => {
|
|||||||
// 设置默认选项
|
// 设置默认选项
|
||||||
const setDefaultOptions = (options: any[]) => {
|
const setDefaultOptions = (options: any[]) => {
|
||||||
const list = [] as any[];
|
const list = [] as any[];
|
||||||
for(const item of [...options ?? [], ...state.defaultOptions]) {
|
for (const item of [...(options ?? []), ...state.defaultOptions]) {
|
||||||
const value = item?.[props.valueProp];
|
const value = item?.[props.valueProp];
|
||||||
const label = props.labelFormat?.(item) || item?.[props.labelProp];
|
const label = props.labelFormat?.(item) || item?.[props.labelProp];
|
||||||
if (value && label) list.push({ [props.valueProp]: value, [props.labelProp]: label })
|
if (value && label) list.push({ [props.valueProp]: value, [props.labelProp]: label });
|
||||||
}
|
}
|
||||||
state.defaultOptions = Array.from(new Set(list));
|
state.defaultOptions = Array.from(new Set(list));
|
||||||
};
|
};
|
||||||
@ -332,8 +328,16 @@ const setValue = (option: any | any[], row: any) => {
|
|||||||
emit('change', state.selectedValues, row);
|
emit('change', state.selectedValues, row);
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(() => props.modelValue, (val: any) => state.selectedValues = val, { immediate: true });
|
watch(
|
||||||
watch(() => props.defaultOptions, (val: any) => setDefaultOptions(val), { immediate: true });
|
() => props.modelValue,
|
||||||
|
(val: any) => (state.selectedValues = val),
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => props.defaultOptions,
|
||||||
|
(val: any) => setDefaultOptions(val),
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
setValue,
|
setValue,
|
||||||
@ -345,21 +349,19 @@ defineExpose({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="state.selectedValues"
|
v-model="state.selectedValues"
|
||||||
:clearable="clearable"
|
:clearable="clearable"
|
||||||
:multiple="multiple"
|
:multiple="multiple"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:allow-create="allowCreate"
|
:remote-method="remoteMethod"
|
||||||
:remote-method="remoteMethod"
|
@visible-change="selectVisibleChange"
|
||||||
:default-first-option="allowCreate"
|
:style="{ width: dropdownWidth }"
|
||||||
@visible-change="selectVisibleChange"
|
popper-class="popper-class"
|
||||||
:style="{ width: dropdownWidth }"
|
ref="selectRef"
|
||||||
popper-class="popper-class"
|
remote-show-suffix
|
||||||
ref="selectRef"
|
filterable
|
||||||
remote-show-suffix
|
remote
|
||||||
filterable
|
|
||||||
remote
|
|
||||||
>
|
>
|
||||||
<!-- 隐藏的选项,用于占位 -->
|
<!-- 隐藏的选项,用于占位 -->
|
||||||
<el-option style="width: 0; height: 0" />
|
<el-option style="width: 0; height: 0" />
|
||||||
@ -383,11 +385,11 @@ defineExpose({
|
|||||||
|
|
||||||
<!-- 数据表格 -->
|
<!-- 数据表格 -->
|
||||||
<el-table
|
<el-table
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
@row-click="handleChange"
|
@row-click="handleChange"
|
||||||
:data="state.tableData?.items ?? []"
|
:data="state.tableData?.items ?? []"
|
||||||
:height="`calc(${dropdownHeight} - 175px${$slots.queryForm ? ` - ${queryHeightOffset}px` : ''}${state.tableQuery[keywordProp] && allowCreate ? ` - ${queryHeightOffset}px` : ''})`"
|
:height="`calc(${dropdownHeight} - 175px${$slots.queryForm ? ` - ${queryHeightOffset}px` : ''}${state.tableQuery[keywordProp] && allowCreate ? ` - ${queryHeightOffset}px` : ''})`"
|
||||||
highlight-current-row
|
highlight-current-row
|
||||||
>
|
>
|
||||||
<template #empty><el-empty :image-size="25" /></template>
|
<template #empty><el-empty :image-size="25" /></template>
|
||||||
<slot name="columns"></slot>
|
<slot name="columns"></slot>
|
||||||
@ -395,22 +397,23 @@ defineExpose({
|
|||||||
|
|
||||||
<!-- 分页组件 -->
|
<!-- 分页组件 -->
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-if="props.pagination"
|
v-if="props.pagination"
|
||||||
:disabled="state.loading"
|
:disabled="state.loading"
|
||||||
:currentPage="state.tableQuery.page"
|
:currentPage="state.tableQuery.page"
|
||||||
:page-size="state.tableQuery.pageSize"
|
:page-size="state.tableQuery.pageSize"
|
||||||
:total="state.tableData.total"
|
:total="state.tableData.total"
|
||||||
:pager-count="4"
|
:pager-count="4"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
layout="prev, pager, next"
|
layout="prev, pager, next"
|
||||||
size="small"
|
size="small"
|
||||||
background
|
background
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
|
||||||
|
<style scoped lang="scss">
|
||||||
.query-form {
|
.query-form {
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
@ -427,8 +430,7 @@ defineExpose({
|
|||||||
:deep(.popper-class) :deep(.el-select-dropdown__wrap) {
|
:deep(.popper-class) :deep(.el-select-dropdown__wrap) {
|
||||||
max-height: 600px !important;
|
max-height: 600px !important;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
<style>
|
|
||||||
.popper-class .el-select-dropdown__wrap {
|
.popper-class .el-select-dropdown__wrap {
|
||||||
max-height: 450px !important;
|
max-height: 450px !important;
|
||||||
}
|
}
|
||||||
@ -437,4 +439,4 @@ defineExpose({
|
|||||||
.el-select-dropdown__wrap[max-height] {
|
.el-select-dropdown__wrap[max-height] {
|
||||||
max-height: 450px !important;
|
max-height: 450px !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -36,13 +36,13 @@ type TagType = (typeof TAG_TYPES)[number];
|
|||||||
* @property {string|number} [value] - 值
|
* @property {string|number} [value] - 值
|
||||||
*/
|
*/
|
||||||
interface DictItem {
|
interface DictItem {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
tagType?: TagType;
|
tagType?: TagType;
|
||||||
styleSetting?: string;
|
styleSetting?: string;
|
||||||
classSetting?: string;
|
classSetting?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
value?: string | number;
|
value?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,8 +52,8 @@ interface DictItem {
|
|||||||
* @property {Array<string|number>} excludes - 被互斥的选项值列表
|
* @property {Array<string|number>} excludes - 被互斥的选项值列表
|
||||||
*/
|
*/
|
||||||
interface MutexConfig {
|
interface MutexConfig {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
excludes: (string | number)[];
|
excludes: (string | number)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,8 +63,8 @@ interface MutexConfig {
|
|||||||
* @property {string} Comma - 逗号分隔模式,如'1,2,3'
|
* @property {string} Comma - 逗号分隔模式,如'1,2,3'
|
||||||
*/
|
*/
|
||||||
const MultipleModel = {
|
const MultipleModel = {
|
||||||
Array: 'array',
|
Array: 'array',
|
||||||
Comma: 'comma',
|
Comma: 'comma',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// 多选值模式枚举类型
|
// 多选值模式枚举类型
|
||||||
@ -77,7 +77,7 @@ type MultipleModelType = (typeof MultipleModel)[keyof typeof MultipleModel];
|
|||||||
* @returns {value is RenderType} - 是否为合法的渲染类型
|
* @returns {value is RenderType} - 是否为合法的渲染类型
|
||||||
*/
|
*/
|
||||||
function isRenderType(value: any): value is RenderType {
|
function isRenderType(value: any): value is RenderType {
|
||||||
return RENDER_TYPES.includes(value);
|
return RENDER_TYPES.includes(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -87,7 +87,7 @@ function isRenderType(value: any): value is RenderType {
|
|||||||
* @returns {value is MultipleModel} - 是否为合法的多选模式
|
* @returns {value is MultipleModel} - 是否为合法的多选模式
|
||||||
*/
|
*/
|
||||||
function isMultipleModel(value: any): value is MultipleModelType {
|
function isMultipleModel(value: any): value is MultipleModelType {
|
||||||
return Object.values(MultipleModel).includes(value);
|
return Object.values(MultipleModel).includes(value);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -103,147 +103,147 @@ const emit = defineEmits(['update:modelValue', 'change']);
|
|||||||
* 组件属性定义
|
* 组件属性定义
|
||||||
*/
|
*/
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/**
|
/**
|
||||||
* 绑定值,支持多种类型
|
* 绑定值,支持多种类型
|
||||||
* @type {string|number|boolean|Array|null}
|
* @type {string|number|boolean|Array|null}
|
||||||
* @required
|
* @required
|
||||||
* @example
|
* @example
|
||||||
* // 单选选择器
|
* // 单选选择器
|
||||||
* <g-sys-dict v-model="selectedValue" code="gender" renderAs="select" />
|
* <g-sys-dict v-model="selectedValue" code="gender" renderAs="select" />
|
||||||
*
|
*
|
||||||
* // 多选选择器(数组模式)
|
* // 多选选择器(数组模式)
|
||||||
* <g-sys-dict v-model="selectedValues" code="roles" renderAs="select" multiple />
|
* <g-sys-dict v-model="selectedValues" code="roles" renderAs="select" multiple />
|
||||||
*
|
*
|
||||||
* // 多选选择器(逗号模式)
|
* // 多选选择器(逗号模式)
|
||||||
* <g-sys-dict v-model="selectedValues" code="roles" renderAs="select" multiple multiple-model="comma" />
|
* <g-sys-dict v-model="selectedValues" code="roles" renderAs="select" multiple multiple-model="comma" />
|
||||||
*/
|
*/
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: [String, Number, Boolean, Array, null] as PropType<string | number | boolean | any[] | Nullable>,
|
type: [String, Number, Boolean, Array, null] as PropType<string | number | boolean | any[] | Nullable>,
|
||||||
default: null,
|
default: null,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 字典编码,用于从字典中获取数据
|
* 字典编码,用于从字典中获取数据
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @required
|
* @required
|
||||||
* @example 'gender'
|
* @example 'gender'
|
||||||
*/
|
*/
|
||||||
code: {
|
code: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 直接传入的字典数据源(优先级高于code)
|
* 直接传入的字典数据源(优先级高于code)
|
||||||
* @type {DictItem[]}
|
* @type {DictItem[]}
|
||||||
* @example [{ label: '选项1', value: '1' }, { label: '选项2', value: '2' }]
|
* @example [{ label: '选项1', value: '1' }, { label: '选项2', value: '2' }]
|
||||||
*/
|
*/
|
||||||
data: {
|
data: {
|
||||||
type: Array as PropType<DictItem[]>,
|
type: Array as PropType<DictItem[]>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 是否为常量字典(true从常量列表获取,false从字典列表获取)
|
* 是否为常量字典(true从常量列表获取,false从字典列表获取)
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @default false
|
* @default false
|
||||||
* @example true
|
* @example true
|
||||||
*/
|
*/
|
||||||
isConst: {
|
isConst: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 字典项中用于显示的字段名
|
* 字典项中用于显示的字段名
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @default 'label'
|
* @default 'label'
|
||||||
* @example 'name'
|
* @example 'name'
|
||||||
*/
|
*/
|
||||||
propLabel: {
|
propLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'label',
|
default: 'label',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 字典项中用于取值的字段名
|
* 字典项中用于取值的字段名
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @default 'value'
|
* @default 'value'
|
||||||
* @example 'id'
|
* @example 'id'
|
||||||
*/
|
*/
|
||||||
propValue: {
|
propValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'value',
|
default: 'value',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 字典项过滤函数
|
* 字典项过滤函数
|
||||||
* @type {Function}
|
* @type {Function}
|
||||||
* @param {DictItem} dict - 当前字典项
|
* @param {DictItem} dict - 当前字典项
|
||||||
* @returns {boolean} - 是否保留该项
|
* @returns {boolean} - 是否保留该项
|
||||||
* @default (dict) => true
|
* @default (dict) => true
|
||||||
* @example
|
* @example
|
||||||
* // 只显示启用的字典项
|
* // 只显示启用的字典项
|
||||||
* :onItemFilter="(dict) => dict.status === 1"
|
* :onItemFilter="(dict) => dict.status === 1"
|
||||||
*/
|
*/
|
||||||
onItemFilter: {
|
onItemFilter: {
|
||||||
type: Function as PropType<(dict: DictItem) => boolean>,
|
type: Function as PropType<(dict: DictItem) => boolean>,
|
||||||
default: () => true,
|
default: () => true,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 字典项显示内容格式化函数
|
* 字典项显示内容格式化函数
|
||||||
* @type {Function}
|
* @type {Function}
|
||||||
* @param {DictItem} dict - 当前字典项
|
* @param {DictItem} dict - 当前字典项
|
||||||
* @returns {string|undefined|null} - 格式化后的显示内容
|
* @returns {string|undefined|null} - 格式化后的显示内容
|
||||||
* @default () => undefined
|
* @default () => undefined
|
||||||
* @example
|
* @example
|
||||||
* // 在标签前添加图标
|
* // 在标签前添加图标
|
||||||
* :onItemFormatter="(dict) => `${dict.label} <icon-user />`"
|
* :onItemFormatter="(dict) => `${dict.label} <icon-user />`"
|
||||||
*/
|
*/
|
||||||
onItemFormatter: {
|
onItemFormatter: {
|
||||||
type: Function as PropType<(dict: DictItem) => string | undefined | null>,
|
type: Function as PropType<(dict: DictItem) => string | undefined | null>,
|
||||||
default: () => undefined,
|
default: () => undefined,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 组件渲染方式
|
* 组件渲染方式
|
||||||
* @type {'tag'|'select'|'radio'|'checkbox'|'radio-button'}
|
* @type {'tag'|'select'|'radio'|'checkbox'|'radio-button'}
|
||||||
* @default 'tag'
|
* @default 'tag'
|
||||||
* @example 'select'
|
* @example 'select'
|
||||||
*/
|
*/
|
||||||
renderAs: {
|
renderAs: {
|
||||||
type: String as PropType<RenderType>,
|
type: String as PropType<RenderType>,
|
||||||
default: 'tag',
|
default: 'tag',
|
||||||
validator: isRenderType,
|
validator: isRenderType,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 是否多选(仅在renderAs为select/checkbox时有效)
|
* 是否多选(仅在renderAs为select/checkbox时有效)
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @default false
|
* @default false
|
||||||
* @example true
|
* @example true
|
||||||
*/
|
*/
|
||||||
multiple: {
|
multiple: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 多选值模式(仅在multiple为true时有效)
|
* 多选值模式(仅在multiple为true时有效)
|
||||||
* @type {'array'|'comma'}
|
* @type {'array'|'comma'}
|
||||||
* @default 'array'
|
* @default 'array'
|
||||||
* @example 'comma'
|
* @example 'comma'
|
||||||
*/
|
*/
|
||||||
multipleModel: {
|
multipleModel: {
|
||||||
type: String as PropType<MultipleModelType>,
|
type: String as PropType<MultipleModelType>,
|
||||||
default: MultipleModel.Array,
|
default: MultipleModel.Array,
|
||||||
validator: isMultipleModel,
|
validator: isMultipleModel,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 互斥配置项(仅在多选模式下有效)
|
* 互斥配置项(仅在多选模式下有效)
|
||||||
* @type {Array<MutexConfig>}
|
* @type {Array<MutexConfig>}
|
||||||
* @example
|
* @example
|
||||||
* :mutex-configs="[
|
* :mutex-configs="[
|
||||||
* { value: 'all', excludes: ['1', '2', '3'] },
|
* { value: 'all', excludes: ['1', '2', '3'] },
|
||||||
* { value: '1', excludes: ['all'] }
|
* { value: '1', excludes: ['all'] }
|
||||||
* ]"
|
* ]"
|
||||||
*/
|
*/
|
||||||
mutexConfigs: {
|
mutexConfigs: {
|
||||||
type: Array as PropType<MutexConfig[]>,
|
type: Array as PropType<MutexConfig[]>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -252,9 +252,9 @@ const props = defineProps({
|
|||||||
* @property {any} value - 当前值
|
* @property {any} value - 当前值
|
||||||
*/
|
*/
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
dictData: [] as DictItem[],
|
dictData: [] as DictItem[],
|
||||||
value: props.modelValue,
|
value: props.modelValue,
|
||||||
conversion: false,
|
conversion: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -263,26 +263,26 @@ const state = reactive({
|
|||||||
* @returns {DictItem[]} - 过滤并格式化后的字典数据
|
* @returns {DictItem[]} - 过滤并格式化后的字典数据
|
||||||
*/
|
*/
|
||||||
const formattedDictData = computed(() => {
|
const formattedDictData = computed(() => {
|
||||||
const baseData = state.dictData.filter(props.onItemFilter).map((item) => ({
|
const baseData = state.dictData.filter(props.onItemFilter).map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
label: item[props.propLabel],
|
label: item[props.propLabel],
|
||||||
value: item[props.propValue],
|
value: item[props.propValue],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 如果没有互斥配置或多选模式,直接返回基础数据
|
// 如果没有互斥配置或多选模式,直接返回基础数据
|
||||||
if (!props.multiple || !props.mutexConfigs || props.mutexConfigs.length === 0) {
|
if (!props.multiple || !props.mutexConfigs || props.mutexConfigs.length === 0) {
|
||||||
return baseData;
|
return baseData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理互斥逻辑,设置禁用状态
|
// 处理互斥逻辑,设置禁用状态
|
||||||
return baseData.map((item) => {
|
return baseData.map((item) => {
|
||||||
// 检查当前项是否应该被禁用
|
// 检查当前项是否应该被禁用
|
||||||
const isDisabled = isItemDisabled(item.value, state.value, props.mutexConfigs);
|
const isDisabled = isItemDisabled(item.value, state.value, props.mutexConfigs);
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
disabled: isDisabled || item.disabled, // 保持原有的disabled状态
|
disabled: isDisabled || item.disabled, // 保持原有的disabled状态
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -291,15 +291,15 @@ const formattedDictData = computed(() => {
|
|||||||
* @returns {DictItem|DictItem[]|null} - 当前选中的字典项或字典项数组
|
* @returns {DictItem|DictItem[]|null} - 当前选中的字典项或字典项数组
|
||||||
*/
|
*/
|
||||||
const currentDictItems = computed(() => {
|
const currentDictItems = computed(() => {
|
||||||
if (!state.value) return null;
|
if (!state.value) return null;
|
||||||
|
|
||||||
if (Array.isArray(state.value)) {
|
if (Array.isArray(state.value)) {
|
||||||
// 去重
|
// 去重
|
||||||
const uniqueValues = [...new Set(state.value)];
|
const uniqueValues = [...new Set(state.value)];
|
||||||
return formattedDictData.value.filter((item) => uniqueValues.includes(item.value));
|
return formattedDictData.value.filter((item) => uniqueValues.includes(item.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
return formattedDictData.value.find((item) => item.value == state.value) || null;
|
return formattedDictData.value.find((item) => item.value == state.value) || null;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -309,34 +309,34 @@ const currentDictItems = computed(() => {
|
|||||||
* @throws {Error} - 获取数据失败时抛出错误
|
* @throws {Error} - 获取数据失败时抛出错误
|
||||||
*/
|
*/
|
||||||
const getDataList = (): DictItem[] => {
|
const getDataList = (): DictItem[] => {
|
||||||
try {
|
try {
|
||||||
// 如果提供了 data 数据源,优先使用
|
// 如果提供了 data 数据源,优先使用
|
||||||
if (props.data && props.data.length > 0) {
|
if (props.data && props.data.length > 0) {
|
||||||
return props.data.map((item: any) => ({
|
return props.data.map((item: any) => ({
|
||||||
...item,
|
...item,
|
||||||
label: item[props.propLabel] ?? [item.name, item.desc].filter((x) => x).join('-'),
|
label: item[props.propLabel] ?? [item.name, item.desc].filter((x) => x).join('-'),
|
||||||
value: item[props.propValue] ?? item.code,
|
value: item[props.propValue] ?? item.code,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.code) {
|
if (!props.code) {
|
||||||
console.error('[g-sys-dict] code和data不能同时为空');
|
console.error('[g-sys-dict] code和data不能同时为空');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = props.isConst ? userStore.constList : userStore.dictList;
|
const source = props.isConst ? userStore.constList : userStore.dictList;
|
||||||
const data = props.isConst ? (source?.find((x: any) => x.code === props.code)?.data?.result ?? []) : (source[props.code] ?? []);
|
const data = props.isConst ? (source?.find((x: any) => x.code === props.code)?.data?.result ?? []) : (source[props.code] ?? []);
|
||||||
data.sort((a: number, b: number) => a - b);
|
data.sort((a: number, b: number) => a - b);
|
||||||
|
|
||||||
return data.map((item: any) => ({
|
return data.map((item: any) => ({
|
||||||
...item,
|
...item,
|
||||||
label: item[props.propLabel] ?? [item.name, item.desc].filter((x) => x).join('-'),
|
label: item[props.propLabel] ?? [item.name, item.desc].filter((x) => x).join('-'),
|
||||||
value: item[props.propValue] ?? item.code,
|
value: item[props.propValue] ?? item.code,
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[g-sys-dict] 获取字典[${props.code}]数据失败:`, error);
|
console.error(`[g-sys-dict] 获取字典[${props.code}]数据失败:`, error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -345,13 +345,13 @@ const getDataList = (): DictItem[] => {
|
|||||||
* @param {any} value - 待处理的值
|
* @param {any} value - 待处理的值
|
||||||
*/
|
*/
|
||||||
const processNumericValues = (value: any) => {
|
const processNumericValues = (value: any) => {
|
||||||
if (typeof value === 'number' || (Array.isArray(value) && typeof value[0] === 'number')) {
|
if (typeof value === 'number' || (Array.isArray(value) && typeof value[0] === 'number')) {
|
||||||
state.dictData.forEach((item) => {
|
state.dictData.forEach((item) => {
|
||||||
if (item.value) {
|
if (item.value) {
|
||||||
item.value = Number(item.value);
|
item.value = Number(item.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -361,48 +361,48 @@ const processNumericValues = (value: any) => {
|
|||||||
* @returns {any} - 解析后的值
|
* @returns {any} - 解析后的值
|
||||||
*/
|
*/
|
||||||
const parseMultipleValue = (value: any): any => {
|
const parseMultipleValue = (value: any): any => {
|
||||||
// 处理空值情况
|
// 处理空值情况
|
||||||
if (value === null || value === undefined || value === '') {
|
if (value === null || value === undefined || value === '') {
|
||||||
return props.multiple ? [] : value;
|
return props.multiple ? [] : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理数字
|
// 处理数字
|
||||||
if (typeof value === 'number' && !state.conversion) {
|
if (typeof value === 'number' && !state.conversion) {
|
||||||
try {
|
try {
|
||||||
state.dictData.forEach((item) => {
|
state.dictData.forEach((item) => {
|
||||||
if (item.value) item.value = Number(item.value);
|
if (item.value) item.value = Number(item.value);
|
||||||
});
|
});
|
||||||
state.conversion = true;
|
state.conversion = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[g-sys-dict] 数字转换失败:', error);
|
console.warn('[g-sys-dict] 数字转换失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理字符串值
|
// 处理字符串值
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
const trimmedValue = value.trim();
|
const trimmedValue = value.trim();
|
||||||
|
|
||||||
// 处理JSON数组格式
|
// 处理JSON数组格式
|
||||||
if (trimmedValue.startsWith('[') && trimmedValue.endsWith(']')) {
|
if (trimmedValue.startsWith('[') && trimmedValue.endsWith(']')) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(trimmedValue);
|
return JSON.parse(trimmedValue);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[g-sys-dict] 解析多选值失败:', error);
|
console.warn('[g-sys-dict] 解析多选值失败:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理逗号分隔格式
|
// 处理逗号分隔格式
|
||||||
if (props.multipleModel === MultipleModel.Comma && trimmedValue.includes(',')) {
|
if (props.multipleModel === MultipleModel.Comma && trimmedValue.includes(',')) {
|
||||||
return trimmedValue.split(',');
|
return trimmedValue.split(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理单个值情况
|
// 处理单个值情况
|
||||||
return props.multiple ? [trimmedValue] : trimmedValue;
|
return props.multiple ? [trimmedValue] : trimmedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 其他情况直接返回
|
// 其他情况直接返回
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -414,28 +414,28 @@ const parseMultipleValue = (value: any): any => {
|
|||||||
* @returns {boolean} - 是否应该禁用
|
* @returns {boolean} - 是否应该禁用
|
||||||
*/
|
*/
|
||||||
const isItemDisabled = (itemValue: string | number, currentValue: any, mutexConfigs: MutexConfig[]): boolean => {
|
const isItemDisabled = (itemValue: string | number, currentValue: any, mutexConfigs: MutexConfig[]): boolean => {
|
||||||
// 如果没有配置互斥规则,不禁用任何项
|
// 如果没有配置互斥规则,不禁用任何项
|
||||||
if (!mutexConfigs || mutexConfigs.length === 0) {
|
if (!mutexConfigs || mutexConfigs.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前选中的值数组
|
// 获取当前选中的值数组
|
||||||
const selectedValues = Array.isArray(currentValue) ? currentValue : currentValue ? [currentValue] : [];
|
const selectedValues = Array.isArray(currentValue) ? currentValue : currentValue ? [currentValue] : [];
|
||||||
|
|
||||||
// 检查每个互斥配置
|
// 检查每个互斥配置
|
||||||
for (const config of mutexConfigs) {
|
for (const config of mutexConfigs) {
|
||||||
// 如果互斥触发项已被选中,且当前项是被互斥项,则禁用
|
// 如果互斥触发项已被选中,且当前项是被互斥项,则禁用
|
||||||
if (selectedValues.includes(config.value) && config.excludes.includes(itemValue)) {
|
if (selectedValues.includes(config.value) && config.excludes.includes(itemValue)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果当前项是互斥触发项,且有被互斥项被选中,则禁用
|
// 如果当前项是互斥触发项,且有被互斥项被选中,则禁用
|
||||||
if (itemValue == config.value && config.excludes.some((exclude) => selectedValues.includes(exclude))) {
|
if (itemValue == config.value && config.excludes.some((exclude) => selectedValues.includes(exclude))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -446,20 +446,20 @@ const isItemDisabled = (itemValue: string | number, currentValue: any, mutexConf
|
|||||||
* @returns {any} - 处理后的值
|
* @returns {any} - 处理后的值
|
||||||
*/
|
*/
|
||||||
const handleMutex = (newValue: any, mutexConfigs: MutexConfig[]): any => {
|
const handleMutex = (newValue: any, mutexConfigs: MutexConfig[]): any => {
|
||||||
// 如果没有配置互斥规则,直接返回原值
|
// 如果没有配置互斥规则,直接返回原值
|
||||||
if (!mutexConfigs || mutexConfigs.length === 0) return newValue;
|
if (!mutexConfigs || mutexConfigs.length === 0) return newValue;
|
||||||
|
|
||||||
// 如果是单选模式,直接返回
|
// 如果是单选模式,直接返回
|
||||||
if (!props.multiple) return newValue;
|
if (!props.multiple) return newValue;
|
||||||
|
|
||||||
// 对于禁用模式,我们只需要确保新值是有效的(即没有违反互斥规则)
|
// 对于禁用模式,我们只需要确保新值是有效的(即没有违反互斥规则)
|
||||||
// 实际的禁用逻辑在formattedDictData中处理
|
// 实际的禁用逻辑在formattedDictData中处理
|
||||||
let resultValue = Array.isArray(newValue) ? [...newValue] : newValue ? [newValue] : [];
|
let resultValue = Array.isArray(newValue) ? [...newValue] : newValue ? [newValue] : [];
|
||||||
|
|
||||||
// 过滤掉无效的值(可能由于异步更新导致的无效选择)
|
// 过滤掉无效的值(可能由于异步更新导致的无效选择)
|
||||||
const validValues = formattedDictData.value.filter((item) => !item.disabled).map((item) => item.value);
|
const validValues = formattedDictData.value.filter((item) => !item.disabled).map((item) => item.value);
|
||||||
|
|
||||||
return resultValue.filter((val) => validValues.includes(val));
|
return resultValue.filter((val) => validValues.includes(val));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -468,30 +468,31 @@ const handleMutex = (newValue: any, mutexConfigs: MutexConfig[]): any => {
|
|||||||
* @param {any} newValue - 新值
|
* @param {any} newValue - 新值
|
||||||
*/
|
*/
|
||||||
const updateValue = (newValue: any) => {
|
const updateValue = (newValue: any) => {
|
||||||
// 如果有互斥配置,先处理互斥
|
// 如果有互斥配置,先处理互斥
|
||||||
let processedValue = newValue;
|
let processedValue = newValue;
|
||||||
if (props.mutexConfigs && props.mutexConfigs.length > 0) {
|
if (props.mutexConfigs && props.mutexConfigs.length > 0) {
|
||||||
processedValue = handleMutex(newValue, props.mutexConfigs);
|
processedValue = handleMutex(newValue, props.mutexConfigs);
|
||||||
}
|
}
|
||||||
|
|
||||||
let emitValue = processedValue;
|
let emitValue = processedValue;
|
||||||
if (props.multipleModel === MultipleModel.Comma) {
|
if (props.multipleModel === MultipleModel.Comma) {
|
||||||
if (Array.isArray(processedValue)) {
|
if (Array.isArray(processedValue)) {
|
||||||
emitValue = processedValue.length > 0 ? processedValue.sort().join(',') : [];
|
emitValue = processedValue.length > 0 ? processedValue.sort().join(',') : [];
|
||||||
} else if (processedValue === null || processedValue === undefined) {
|
} else if (processedValue === null || processedValue === undefined) {
|
||||||
emitValue = undefined;
|
emitValue = undefined;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (Array.isArray(processedValue)) {
|
if (Array.isArray(processedValue)) {
|
||||||
emitValue = processedValue.length > 0 ? processedValue.sort() : [];
|
emitValue = processedValue.length > 0 ? processedValue.sort() : [];
|
||||||
} else if (processedValue === null || processedValue === undefined) {
|
} else if (processedValue === null || processedValue === undefined) {
|
||||||
emitValue = undefined;
|
emitValue = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log('更新值:', { newValue, processedValue, emitValue });
|
||||||
|
|
||||||
state.value = processedValue;
|
state.value = processedValue;
|
||||||
emit('update:modelValue', emitValue === '' || emitValue.length === 0 ? undefined : emitValue);
|
emit('update:modelValue', emitValue === '' || emitValue?.length === 0 ? undefined : emitValue);
|
||||||
emit('change', state.value, currentDictItems, state.dictData);
|
emit('change', state.value, currentDictItems, state.dictData);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -501,7 +502,7 @@ const updateValue = (newValue: any) => {
|
|||||||
* @returns {TagType} - 合法的标签类型
|
* @returns {TagType} - 合法的标签类型
|
||||||
*/
|
*/
|
||||||
const ensureTagType = (item: DictItem): TagType => {
|
const ensureTagType = (item: DictItem): TagType => {
|
||||||
return TAG_TYPES.includes(item.tagType as TagType) ? (item.tagType as TagType) : 'primary';
|
return TAG_TYPES.includes(item.tagType as TagType) ? (item.tagType as TagType) : 'primary';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -511,9 +512,9 @@ const ensureTagType = (item: DictItem): TagType => {
|
|||||||
* @returns {string} - 显示文本
|
* @returns {string} - 显示文本
|
||||||
*/
|
*/
|
||||||
const getDisplayText = (dict?: DictItem): string => {
|
const getDisplayText = (dict?: DictItem): string => {
|
||||||
if (!dict) return String(state.value || '');
|
if (!dict) return String(state.value || '');
|
||||||
const formattedText = props.onItemFormatter?.(dict);
|
const formattedText = props.onItemFormatter?.(dict);
|
||||||
return formattedText ?? dict[props.propLabel] ?? '';
|
return formattedText ?? dict[props.propLabel] ?? '';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -521,20 +522,20 @@ const getDisplayText = (dict?: DictItem): string => {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
const initData = () => {
|
const initData = () => {
|
||||||
// 验证 code 和 data 不能同时为空
|
// 验证 code 和 data 不能同时为空
|
||||||
if (!props.code && (!props.data || props.data.length === 0)) {
|
if (!props.code && (!props.data || props.data.length === 0)) {
|
||||||
console.error('[g-sys-dict] code和data不能同时为空');
|
console.error('[g-sys-dict] code和data不能同时为空');
|
||||||
state.dictData = [];
|
state.dictData = [];
|
||||||
state.value = props.multiple ? [] : null;
|
state.value = props.multiple ? [] : null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.dictData = getDataList();
|
state.dictData = getDataList();
|
||||||
processNumericValues(props.modelValue);
|
processNumericValues(props.modelValue);
|
||||||
const initialValue = parseMultipleValue(props.modelValue);
|
const initialValue = parseMultipleValue(props.modelValue);
|
||||||
if (initialValue !== state.value) {
|
if (initialValue !== state.value) {
|
||||||
state.value = initialValue;
|
state.value = initialValue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -542,78 +543,78 @@ const initData = () => {
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
const validateInitialValue = () => {
|
const validateInitialValue = () => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (props.renderAs === 'tag' || !state.value) return resolve(undefined);
|
if (props.renderAs === 'tag' || !state.value) return resolve(undefined);
|
||||||
if (Array.isArray(state.value)) {
|
if (Array.isArray(state.value)) {
|
||||||
const errorValues = state.value.filter((val) => state.dictData.find((e) => e[props.propValue] == val) === undefined);
|
const errorValues = state.value.filter((val) => state.dictData.find((e) => e[props.propValue] == val) === undefined);
|
||||||
if (errorValues && errorValues.length > 0) {
|
if (errorValues && errorValues.length > 0) {
|
||||||
reject(`[g-sys-dict] 未匹配到选项值:${JSON.stringify(errorValues)}`);
|
reject(`[g-sys-dict] 未匹配到选项值:${JSON.stringify(errorValues)}`);
|
||||||
}
|
}
|
||||||
} else if (state.value) {
|
} else if (state.value) {
|
||||||
if (!state.dictData.find((e) => e[props.propValue] == state.value)) {
|
if (!state.dictData.find((e) => e[props.propValue] == state.value)) {
|
||||||
reject(`[g-sys-dict] 未匹配到选项值:${state.value}`);
|
reject(`[g-sys-dict] 未匹配到选项值:${state.value}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve(undefined);
|
resolve(undefined);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听数据变化
|
// 监听数据变化
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
state.value = parseMultipleValue(newValue);
|
state.value = parseMultipleValue(newValue);
|
||||||
validateInitialValue();
|
validateInitialValue();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
watch(() => [userStore.dictList, userStore.constList, props.data, state], initData, { immediate: true });
|
watch(() => [userStore.dictList, userStore.constList, props.data, state], initData, { immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 渲染标签 -->
|
<!-- 渲染标签 -->
|
||||||
<template v-if="props.renderAs === 'tag'">
|
<template v-if="props.renderAs === 'tag'">
|
||||||
<template v-if="Array.isArray(currentDictItems)">
|
<template v-if="Array.isArray(currentDictItems)">
|
||||||
<el-tag v-for="(item, index) in currentDictItems" :key="index" v-bind="$attrs" :type="ensureTagType(item)" :style="item.styleSetting" :class="item.classSetting" class="mr2">
|
<el-tag v-for="(item, index) in currentDictItems" :key="index" v-bind="$attrs" :type="ensureTagType(item)" :style="item.styleSetting" :class="item.classSetting" class="mr2">
|
||||||
{{ getDisplayText(item) }}
|
{{ getDisplayText(item) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-tag v-if="currentDictItems" v-bind="$attrs" :type="ensureTagType(currentDictItems)" :style="currentDictItems.styleSetting" :class="currentDictItems.classSetting">
|
<el-tag v-if="currentDictItems" v-bind="$attrs" :type="ensureTagType(currentDictItems)" :style="currentDictItems.styleSetting" :class="currentDictItems.classSetting">
|
||||||
{{ getDisplayText(currentDictItems) }}
|
{{ getDisplayText(currentDictItems) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span v-else>{{ getDisplayText() }}</span>
|
<span v-else>{{ getDisplayText() }}</span>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 渲染选择器 -->
|
<!-- 渲染选择器 -->
|
||||||
<el-select v-else-if="props.renderAs === 'select'" v-model="state.value" v-bind="$attrs" :multiple="props.multiple" @change="updateValue" filterable allow-create default-first-option clearable>
|
<el-select v-else-if="props.renderAs === 'select'" v-model="state.value" v-bind="$attrs" :multiple="props.multiple" @change="updateValue" filterable allow-create default-first-option clearable>
|
||||||
<el-option v-for="(item, index) in formattedDictData" :key="index" :label="getDisplayText(item)" :value="item.value" :disabled="item.disabled" />
|
<el-option v-for="(item, index) in formattedDictData" :key="index" :label="getDisplayText(item)" :value="item.value" :disabled="item.disabled" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
<!-- 多选框(多选) -->
|
<!-- 多选框(多选) -->
|
||||||
<el-checkbox-group v-else-if="props.renderAs === 'checkbox'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
<el-checkbox-group v-else-if="props.renderAs === 'checkbox'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
||||||
<el-checkbox v-for="(item, index) in formattedDictData" :key="index" :value="item.value" :label="getDisplayText(item)" :disabled="item.disabled" />
|
<el-checkbox v-for="(item, index) in formattedDictData" :key="index" :value="item.value" :label="getDisplayText(item)" :disabled="item.disabled" />
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
|
|
||||||
<!-- 多选框-按钮(多选) -->
|
<!-- 多选框-按钮(多选) -->
|
||||||
<el-checkbox-group v-else-if="props.renderAs === 'checkbox-button'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
<el-checkbox-group v-else-if="props.renderAs === 'checkbox-button'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
||||||
<el-checkbox-button v-for="(item, index) in formattedDictData" :key="index" :value="item.value" :disabled="item.disabled">
|
<el-checkbox-button v-for="(item, index) in formattedDictData" :key="index" :value="item.value" :disabled="item.disabled">
|
||||||
{{ getDisplayText(item) }}
|
{{ getDisplayText(item) }}
|
||||||
</el-checkbox-button>
|
</el-checkbox-button>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
|
|
||||||
<!-- 渲染单选框 -->
|
<!-- 渲染单选框 -->
|
||||||
<el-radio-group v-else-if="props.renderAs === 'radio'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
<el-radio-group v-else-if="props.renderAs === 'radio'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
||||||
<el-radio v-for="(item, index) in formattedDictData" :key="index" :value="item.value">
|
<el-radio v-for="(item, index) in formattedDictData" :key="index" :value="item.value">
|
||||||
{{ getDisplayText(item) }}
|
{{ getDisplayText(item) }}
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
|
|
||||||
<!-- 渲染单选框按钮 -->
|
<!-- 渲染单选框按钮 -->
|
||||||
<el-radio-group v-else-if="props.renderAs === 'radio-button'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
<el-radio-group v-else-if="props.renderAs === 'radio-button'" v-model="state.value" v-bind="$attrs" @change="updateValue">
|
||||||
<el-radio-button v-for="(item, index) in formattedDictData" :key="index" :value="item.value">
|
<el-radio-button v-for="(item, index) in formattedDictData" :key="index" :value="item.value">
|
||||||
{{ getDisplayText(item) }}
|
{{ getDisplayText(item) }}
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</template>
|
</template>
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss"></style>
|
||||||
|
|||||||
@ -56,6 +56,7 @@ import { Session } from '/@/utils/storage';
|
|||||||
import { isObjectValueEqual } from '/@/utils/arrayOperation';
|
import { isObjectValueEqual } from '/@/utils/arrayOperation';
|
||||||
import other from '/@/utils/other';
|
import other from '/@/utils/other';
|
||||||
import mittBus from '/@/utils/mitt';
|
import mittBus from '/@/utils/mitt';
|
||||||
|
import { RefType, RouteItem, RouteItems, RouteToFrom, WheelEventType } from '/@/types/global';
|
||||||
|
|
||||||
// 引入组件
|
// 引入组件
|
||||||
const Contextmenu = defineAsyncComponent(() => import('/@/layout/navBars/tagsView/contextmenu.vue'));
|
const Contextmenu = defineAsyncComponent(() => import('/@/layout/navBars/tagsView/contextmenu.vue'));
|
||||||
|
|||||||
@ -364,39 +364,41 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.el-tree {
|
.el-tree {
|
||||||
--el-tree-node-content-height: 30px;
|
--el-tree-node-content-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-table .el-table__cell {
|
.el-table .el-table__cell {
|
||||||
&:has(.cell .el-button, .el-tag, .el-switch, .el-avatar) {
|
&:has(.cell .el-button, .el-tag, .el-switch, .el-avatar) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
.cell { text-overflow: clip; }
|
.cell {
|
||||||
}
|
text-overflow: clip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.el-button.is-text {
|
.el-button.is-text {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-text--large {
|
.el-text--large {
|
||||||
font-size: var(--el-font-size-base);;
|
font-size: var(--el-font-size-base);
|
||||||
}
|
}
|
||||||
.el-button--large {
|
.el-button--large {
|
||||||
--el-button-size: 32px;
|
--el-button-size: 32px;
|
||||||
}
|
}
|
||||||
.el-tag--large {
|
.el-tag--large {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
.el-button--default {
|
.el-button--default {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
.el-button [class*=el-icon]+span{
|
.el-button [class*='el-icon'] + span {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.el-table [class*=el-table__row--level] .el-table__expand-icon {
|
.el-table [class*='el-table__row--level'] .el-table__expand-icon {
|
||||||
height: 14px;
|
height: 14px;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card 卡片
|
/* Card 卡片
|
||||||
@ -418,6 +420,8 @@
|
|||||||
.el-table {
|
.el-table {
|
||||||
// 表头背景色
|
// 表头背景色
|
||||||
--el-table-header-bg-color: var(--next-bg-main-color);
|
--el-table-header-bg-color: var(--next-bg-main-color);
|
||||||
|
// 当前行背景色
|
||||||
|
// --el-table-current-row-bg-color: #cbd8e4;
|
||||||
|
|
||||||
.el-button.is-text {
|
.el-button.is-text {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
402
Web/src/utils/formRule.ts
Normal file
402
Web/src/utils/formRule.ts
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
import type { FormItemRule } from 'element-plus';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const TRIGGER_INPUT: FormItemRule['trigger'] = 'change';
|
||||||
|
const TRIGGER_BLUR: FormItemRule['trigger'] = 'blur';
|
||||||
|
|
||||||
|
export function useFormRulePresets() {
|
||||||
|
const required = (label = '此项', trigger: FormItemRule['trigger'] = TRIGGER_BLUR): FormItemRule => ({
|
||||||
|
required: true,
|
||||||
|
message: `${label}为必填项`,
|
||||||
|
trigger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const requiredIf = (required?: boolean, label = '此项', trigger: FormItemRule['trigger'] = TRIGGER_BLUR): FormItemRule => ({
|
||||||
|
required: required ?? false,
|
||||||
|
message: `${label}为必填项`,
|
||||||
|
trigger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxLen = (len: number, label = '此项', trigger = TRIGGER_INPUT): FormItemRule => ({
|
||||||
|
max: len,
|
||||||
|
message: `${label}长度不能超过 ${len} 个字符`,
|
||||||
|
trigger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const minLen = (len: number, label = '此项', trigger = TRIGGER_INPUT): FormItemRule => ({
|
||||||
|
min: len,
|
||||||
|
message: `${label}长度不能少于 ${len} 个字符`,
|
||||||
|
trigger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pattern = (re: RegExp, msg: string, trigger = TRIGGER_BLUR): FormItemRule => ({
|
||||||
|
pattern: re,
|
||||||
|
message: msg,
|
||||||
|
trigger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const validator = (fn: (value: any) => true | string | Promise<true | string>, trigger = TRIGGER_BLUR): FormItemRule => ({
|
||||||
|
trigger,
|
||||||
|
validator: (_rule, value, callback) => {
|
||||||
|
Promise.resolve(fn(value))
|
||||||
|
.then((res) => {
|
||||||
|
if (res === true) return callback();
|
||||||
|
return callback(new Error(res));
|
||||||
|
})
|
||||||
|
.catch((err) => callback(err instanceof Error ? err : new Error(String(err))));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const phone = () => pattern(/^1[3,4,5,6,7,8,9][0-9]{9}$/, '请输入有效的手机号');
|
||||||
|
|
||||||
|
const idCard = () =>
|
||||||
|
validator((value) => {
|
||||||
|
if (!value) return true; // 如果为空,让required规则处理
|
||||||
|
|
||||||
|
const idCard = String(value).trim();
|
||||||
|
|
||||||
|
// 15位身份证号验证(老身份证)
|
||||||
|
if (idCard.length === 15) {
|
||||||
|
const reg15 = /^[1-9]\d{5}\d{2}((0[1-9])|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$/;
|
||||||
|
if (!reg15.test(idCard)) {
|
||||||
|
return '请输入有效的15位身份证号';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 18位身份证号验证(新身份证)
|
||||||
|
if (idCard.length === 18) {
|
||||||
|
const reg18 = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}(\d|X|x)$/;
|
||||||
|
if (!reg18.test(idCard)) {
|
||||||
|
return '请输入有效的18位身份证号';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证校验码
|
||||||
|
const factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||||||
|
const parity = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
||||||
|
let sum = 0;
|
||||||
|
let ai = 0;
|
||||||
|
let wi = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < 17; i++) {
|
||||||
|
ai = parseInt(idCard[i]);
|
||||||
|
wi = factor[i];
|
||||||
|
sum += ai * wi;
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = parity[sum % 11];
|
||||||
|
if (last !== idCard[17].toUpperCase()) {
|
||||||
|
return '身份证号校验码错误';
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '身份证号必须是15位或18位';
|
||||||
|
}, TRIGGER_BLUR);
|
||||||
|
|
||||||
|
const email = () => pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, '请输入有效的邮箱地址');
|
||||||
|
|
||||||
|
const digits = (label = '此项', trigger = TRIGGER_INPUT) => pattern(/^\d+$/, `${label}需为纯数字`, trigger);
|
||||||
|
|
||||||
|
const intRange = (min: number, max: number, label = '数值') =>
|
||||||
|
validator((v) => {
|
||||||
|
if (v === '' || v === undefined || v === null) return `请输入${label}`;
|
||||||
|
if (!Number.isInteger(Number(v))) return `${label}需为整数`;
|
||||||
|
const n = Number(v);
|
||||||
|
if (n < min || n > max) return `${label}需在 ${min}~${max} 之间`;
|
||||||
|
return true;
|
||||||
|
}, TRIGGER_INPUT);
|
||||||
|
|
||||||
|
const confirmSame = (getOther: () => any, label = '两次输入') => validator((v) => (v === getOther() ? true : `${label}不一致`), TRIGGER_INPUT);
|
||||||
|
|
||||||
|
const toDate = (v: any): Date | null => {
|
||||||
|
if (v instanceof Date && !isNaN(v.getTime())) return v;
|
||||||
|
if (typeof v === 'number') {
|
||||||
|
const d = new Date(v);
|
||||||
|
return isNaN(d.getTime()) ? null : d;
|
||||||
|
}
|
||||||
|
if (typeof v === 'string' && v) {
|
||||||
|
const d = new Date(v);
|
||||||
|
return isNaN(d.getTime()) ? null : d;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateAfterToday = (label = '日期', trigger = TRIGGER_INPUT) =>
|
||||||
|
validator((v) => {
|
||||||
|
if (v === '' || v === undefined || v === null) return true;
|
||||||
|
const d = toDate(v);
|
||||||
|
if (!d) return `请选择有效的${label}`;
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
return d.getTime() > today.getTime() ? true : `${label}必须在今天之后`;
|
||||||
|
}, trigger);
|
||||||
|
|
||||||
|
const dateBeforeToday = (label = '日期', trigger = TRIGGER_INPUT) =>
|
||||||
|
validator((v) => {
|
||||||
|
if (v === '' || v === undefined || v === null) return true;
|
||||||
|
const d = toDate(v);
|
||||||
|
if (!d) return `请选择有效的${label}`;
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
return d.getTime() < today.getTime() ? true : `${label}必须在今天之前`;
|
||||||
|
}, trigger);
|
||||||
|
|
||||||
|
const numberPrecision = (maxIntDigits: number | null, maxFracDigits: number | null, label = '此项') =>
|
||||||
|
validator((v) => {
|
||||||
|
if (v === '' || v === undefined || v === null) return true;
|
||||||
|
|
||||||
|
const str = String(v).trim();
|
||||||
|
|
||||||
|
// 允许 0、整数或小数(正数),不允许负数与多余小数点
|
||||||
|
if (!/^\d+(?:\.\d+)?$/.test(str)) {
|
||||||
|
return `${label}必须为数字`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [intPart, fracPart = ''] = str.split('.');
|
||||||
|
if (maxIntDigits !== null) {
|
||||||
|
if (intPart.length > maxIntDigits) {
|
||||||
|
return `${label}整数位不能超过${maxIntDigits}位`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fracPart && maxFracDigits !== null && fracPart.length > maxFracDigits) {
|
||||||
|
if (maxFracDigits === 0) {
|
||||||
|
return `${label}必须为整数`;
|
||||||
|
}
|
||||||
|
return `${label}小数位不能超过${maxFracDigits}位`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, TRIGGER_BLUR);
|
||||||
|
|
||||||
|
// ========== 直接显示错误的验证函数(直接用于元素)==========
|
||||||
|
|
||||||
|
// 长度验证函数
|
||||||
|
const validateLength = (value: any, maxLength: number, fieldName = '字段') => {
|
||||||
|
if (!value) return true;
|
||||||
|
if (String(value).length > maxLength) {
|
||||||
|
ElMessage.error(`${fieldName}长度不能超过${maxLength}个字符`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 必填验证函数
|
||||||
|
const validateRequired = (value: any, fieldName = '字段') => {
|
||||||
|
if (!value || String(value).trim() === '') {
|
||||||
|
ElMessage.error(`${fieldName}不能为空`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 数字验证函数
|
||||||
|
const validateNumber = (value: any, fieldName = '字段') => {
|
||||||
|
if (!value) return true;
|
||||||
|
if (!/^\d+$/.test(String(value))) {
|
||||||
|
ElMessage.error(`${fieldName}必须为纯数字`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 整数范围验证函数
|
||||||
|
const validateIntRange = (value: any, min: number, max: number, fieldName = '字段') => {
|
||||||
|
if (!value) return true;
|
||||||
|
const num = Number(value);
|
||||||
|
if (!Number.isInteger(num)) {
|
||||||
|
ElMessage.error(`${fieldName}必须为整数`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (num < min || num > max) {
|
||||||
|
ElMessage.error(`${fieldName}必须在${min}~${max}之间`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 中文验证函数
|
||||||
|
const validateChinese = (value: any, fieldName = '字段') => {
|
||||||
|
if (!value) return true;
|
||||||
|
if (!/^[\u4e00-\u9fa5]+$/.test(String(value))) {
|
||||||
|
ElMessage.error(`${fieldName}必须为中文`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 邮箱验证函数
|
||||||
|
const validateEmail = (value: any, fieldName = '字段') => {
|
||||||
|
if (!value) return true;
|
||||||
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))) {
|
||||||
|
ElMessage.error(`${fieldName}格式不正确`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 手机号验证函数
|
||||||
|
const validatePhone = (value: any, fieldName = '字段') => {
|
||||||
|
if (!value) return true;
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(String(value))) {
|
||||||
|
ElMessage.error(`${fieldName}格式不正确`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 身份证号验证函数
|
||||||
|
const validateIdCard = (value: any, fieldName = '字段') => {
|
||||||
|
if (!value) return true;
|
||||||
|
const idCard = String(value).trim();
|
||||||
|
|
||||||
|
if (idCard.length === 15) {
|
||||||
|
const reg15 = /^[1-9]\d{5}\d{2}((0[1-9])|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$/;
|
||||||
|
if (!reg15.test(idCard)) {
|
||||||
|
ElMessage.error(`${fieldName}格式不正确`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idCard.length === 18) {
|
||||||
|
const reg18 = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}(\d|X|x)$/;
|
||||||
|
if (!reg18.test(idCard)) {
|
||||||
|
ElMessage.error(`${fieldName}格式不正确`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证校验码
|
||||||
|
const factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||||||
|
const parity = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < 17; i++) {
|
||||||
|
sum += parseInt(idCard[i]) * factor[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = parity[sum % 11];
|
||||||
|
if (last !== idCard[17].toUpperCase()) {
|
||||||
|
ElMessage.error(`${fieldName}校验码错误`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.error(`${fieldName}必须是15位或18位`);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自定义正则验证函数
|
||||||
|
const validatePattern = (value: any, pattern: RegExp, message: string) => {
|
||||||
|
if (!value) return true;
|
||||||
|
if (!pattern.test(String(value))) {
|
||||||
|
ElMessage.error(message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组合验证函数(可同时验证多个规则)
|
||||||
|
const validateMultiple = (value: any, validators: Array<(value: any) => boolean>) => {
|
||||||
|
for (const validator of validators) {
|
||||||
|
if (!validator(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证小数和整数位数的函数
|
||||||
|
const validateDecimalPrecision = (value: any, maxIntDigits: number, maxFracDigits: number, fieldName = '字段') => {
|
||||||
|
if (!value) return true;
|
||||||
|
|
||||||
|
const str = String(value).trim();
|
||||||
|
|
||||||
|
// 允许 0、整数或小数(正数),不允许负数与多余小数点
|
||||||
|
if (!/^\d+(?:\.\d+)?$/.test(str)) {
|
||||||
|
ElMessage.error(`${fieldName}必须为数字`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [intPart, fracPart = ''] = str.split('.');
|
||||||
|
|
||||||
|
// 验证整数位数
|
||||||
|
if (intPart.length > maxIntDigits) {
|
||||||
|
ElMessage.error(`${fieldName}整数位不能超过${maxIntDigits}位`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证小数位数
|
||||||
|
if (fracPart && fracPart.length > maxFracDigits) {
|
||||||
|
if (maxFracDigits === 0) {
|
||||||
|
ElMessage.error(`${fieldName}必须为整数`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ElMessage.error(`${fieldName}小数位不能超过${maxFracDigits}位`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 通用验证函数(根据类型自动选择验证规则)
|
||||||
|
const validateField = (value: any, type: string, fieldName: string, options?: any) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'required':
|
||||||
|
return validateRequired(value, fieldName);
|
||||||
|
case 'length':
|
||||||
|
return validateLength(value, options.maxLength, fieldName);
|
||||||
|
case 'number':
|
||||||
|
return validateNumber(value, fieldName);
|
||||||
|
case 'range':
|
||||||
|
return validateIntRange(value, options.min, options.max, fieldName);
|
||||||
|
case 'chinese':
|
||||||
|
return validateChinese(value, fieldName);
|
||||||
|
case 'email':
|
||||||
|
return validateEmail(value, fieldName);
|
||||||
|
case 'phone':
|
||||||
|
return validatePhone(value, fieldName);
|
||||||
|
case 'idcard':
|
||||||
|
return validateIdCard(value, fieldName);
|
||||||
|
case 'pattern':
|
||||||
|
return validatePattern(value, options.pattern, options.message);
|
||||||
|
case 'decimal':
|
||||||
|
return validateDecimalPrecision(value, options.maxIntDigits, options.maxFracDigits, fieldName);
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
required,
|
||||||
|
requiredIf,
|
||||||
|
maxLen,
|
||||||
|
minLen,
|
||||||
|
pattern,
|
||||||
|
validator,
|
||||||
|
phone,
|
||||||
|
idCard,
|
||||||
|
email,
|
||||||
|
digits,
|
||||||
|
intRange,
|
||||||
|
confirmSame,
|
||||||
|
dateAfterToday,
|
||||||
|
dateBeforeToday,
|
||||||
|
numberPrecision,
|
||||||
|
validateRequired,
|
||||||
|
validateNumber,
|
||||||
|
validateIntRange,
|
||||||
|
validateChinese,
|
||||||
|
validateLength,
|
||||||
|
validateEmail,
|
||||||
|
validatePhone,
|
||||||
|
validateIdCard,
|
||||||
|
validatePattern,
|
||||||
|
validateMultiple,
|
||||||
|
validateField,
|
||||||
|
validateDecimalPrecision,
|
||||||
|
};
|
||||||
|
}
|
||||||
436
Web/src/utils/formValidator.ts
Normal file
436
Web/src/utils/formValidator.ts
Normal file
@ -0,0 +1,436 @@
|
|||||||
|
// 验证规则类型定义
|
||||||
|
export type ValidationRule = {
|
||||||
|
type: string;
|
||||||
|
message: string;
|
||||||
|
validator?: (value: any) => boolean;
|
||||||
|
pattern?: RegExp;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
required?: boolean;
|
||||||
|
minLength?: number;
|
||||||
|
maxLength?: number;
|
||||||
|
value?: any;
|
||||||
|
trigger?: 'blur' | 'change' | 'submit' | string | string[]; // 补齐 trigger 属性
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证器配置接口
|
||||||
|
export interface ValidatorConfig {
|
||||||
|
prop?: string;
|
||||||
|
label?: string;
|
||||||
|
customMessages?: Record<string, string>;
|
||||||
|
trigger?: 'blur' | 'change' | 'submit' | string | string[]; // 补齐 trigger 属性
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证器建造者类
|
||||||
|
class ValidationBuilder {
|
||||||
|
private readonly prop?: string;
|
||||||
|
private readonly label?: string;
|
||||||
|
private readonly customMessages: Record<string, string>;
|
||||||
|
private readonly rules: ValidationRule[] = [];
|
||||||
|
private readonly defaultTrigger?: string | string[]; // 存储默认触发方式
|
||||||
|
|
||||||
|
constructor(config: ValidatorConfig) {
|
||||||
|
this.prop = config.prop;
|
||||||
|
this.label = config.label || config.prop;
|
||||||
|
this.customMessages = config.customMessages || {};
|
||||||
|
this.defaultTrigger = config.trigger || 'blur'; // 设置默认触发方式
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取自定义消息或默认消息
|
||||||
|
private getMessage(key: string, defaultMessage: string): string {
|
||||||
|
return this.customMessages[key] || defaultMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加基于正则表达式的验证规则
|
||||||
|
private addPatternRule(type: string, pattern: RegExp, message: string): ValidationBuilder {
|
||||||
|
const rule: ValidationRule = {
|
||||||
|
type: '',
|
||||||
|
message,
|
||||||
|
pattern,
|
||||||
|
};
|
||||||
|
this.rules.push(rule);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加基于自定义函数的验证规则
|
||||||
|
private addValidatorRule(type: string, validator: (value: any) => boolean, message: string): ValidationBuilder {
|
||||||
|
const rule: ValidationRule = {
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
validator,
|
||||||
|
};
|
||||||
|
this.rules.push(rule);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 必填验证
|
||||||
|
required(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}不能为空`;
|
||||||
|
const rule: ValidationRule = {
|
||||||
|
type: 'required', // 添加 type 字段
|
||||||
|
message: message || this.getMessage('required', defaultMessage),
|
||||||
|
required: true,
|
||||||
|
};
|
||||||
|
this.rules.push(rule);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最小长度验证
|
||||||
|
min(len: number, message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}最少输入${len}个字符`;
|
||||||
|
const rule: ValidationRule = {
|
||||||
|
type: 'min', // 添加 type 字段
|
||||||
|
message: message || this.getMessage('min', defaultMessage),
|
||||||
|
minLength: len,
|
||||||
|
};
|
||||||
|
this.rules.push(rule);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最大长度验证
|
||||||
|
max(len: number, message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}最多输入${len}个字符`;
|
||||||
|
const rule: ValidationRule = {
|
||||||
|
type: 'max', // 添加 type 字段
|
||||||
|
message: message || this.getMessage('max', defaultMessage),
|
||||||
|
maxLength: len,
|
||||||
|
};
|
||||||
|
this.rules.push(rule);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 长度范围验证
|
||||||
|
range(min: number, max: number, message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}长度应在${min}-${max}个字符之间`;
|
||||||
|
const rule: ValidationRule = {
|
||||||
|
type: 'range', // 添加 type 字段
|
||||||
|
message: message || this.getMessage('range', defaultMessage),
|
||||||
|
minLength: min,
|
||||||
|
maxLength: max,
|
||||||
|
};
|
||||||
|
this.rules.push(rule);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正则表达式验证
|
||||||
|
pattern(regex: RegExp, message: string): ValidationBuilder {
|
||||||
|
const rule: ValidationRule = {
|
||||||
|
type: 'pattern', // 添加 type 字段
|
||||||
|
message,
|
||||||
|
pattern: regex,
|
||||||
|
};
|
||||||
|
this.rules.push(rule);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 邮箱验证
|
||||||
|
email(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}格式不正确`;
|
||||||
|
return this.addPatternRule('email', /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message || this.getMessage('email', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机号验证
|
||||||
|
mobile(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}格式不正确`;
|
||||||
|
return this.addPatternRule('mobile', /^1[3-9]\d{9}$/, message || this.getMessage('mobile', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 固定电话验证
|
||||||
|
telephone(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}格式不正确`;
|
||||||
|
return this.addPatternRule('telephone', /^(\d{3,4}-?)?\d{7,8}(-\d{1,6})?$/, message || this.getMessage('telephone', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数字验证
|
||||||
|
number(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}必须为数字`;
|
||||||
|
return this.addPatternRule('number', /^-?\d+(\.\d+)?$/, message || this.getMessage('number', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 整数验证
|
||||||
|
integer(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}必须为整数`;
|
||||||
|
return this.addPatternRule('integer', /^-?[1-9]\d*$|^0$/, message || this.getMessage('integer', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正整数验证
|
||||||
|
positiveInteger(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}必须为正整数`;
|
||||||
|
return this.addPatternRule('positiveInteger', /^[1-9]\d*$/, message || this.getMessage('positiveInteger', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 身份证号码验证
|
||||||
|
idCard(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}格式不正确`;
|
||||||
|
return this.addValidatorRule(
|
||||||
|
'idCard',
|
||||||
|
(value) => {
|
||||||
|
if (!value) return true;
|
||||||
|
|
||||||
|
// 基本格式验证
|
||||||
|
const reg = /(^\d{15}$)|(^\d{17}(\d|X|x)$)/;
|
||||||
|
if (!reg.test(value)) return false;
|
||||||
|
|
||||||
|
// 15位身份证直接返回true(因为没有校验位)
|
||||||
|
if (value.length === 15) return true;
|
||||||
|
|
||||||
|
// 18位身份证需要验证校验位和出生日期
|
||||||
|
// 验证出生日期
|
||||||
|
const birth = value.substring(6, 14);
|
||||||
|
const year = parseInt(birth.substring(0, 4));
|
||||||
|
const month = parseInt(birth.substring(4, 6));
|
||||||
|
const day = parseInt(birth.substring(6, 8));
|
||||||
|
|
||||||
|
// 简单的日期有效性检查
|
||||||
|
if (month < 1 || month > 12) return false;
|
||||||
|
if (day < 1 || day > 31) return false;
|
||||||
|
|
||||||
|
// 验证校验位
|
||||||
|
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||||||
|
const checks = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
||||||
|
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < 17; i++) {
|
||||||
|
sum += parseInt(value.charAt(i)) * weights[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const mod = sum % 11;
|
||||||
|
const checkBit = checks[mod];
|
||||||
|
|
||||||
|
return value.charAt(17).toUpperCase() === checkBit;
|
||||||
|
},
|
||||||
|
message || this.getMessage('idCard', defaultMessage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一社会信用代码验证
|
||||||
|
unifiedSocialCreditCode(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}格式不正确`;
|
||||||
|
return this.addValidatorRule(
|
||||||
|
'unifiedSocialCreditCode',
|
||||||
|
(value) => {
|
||||||
|
if (!value) return true;
|
||||||
|
|
||||||
|
// 基本格式验证:18位,前17位为数字或大写字母,最后1位为数字或大写字母
|
||||||
|
const reg = /^[0-9A-Z]{18}$/;
|
||||||
|
if (!reg.test(value)) return false;
|
||||||
|
|
||||||
|
// 统一社会信用代码校验规则
|
||||||
|
const code = value.toUpperCase();
|
||||||
|
const weights = [1, 3, 9, 27, 19, 17, 5, 33, 25, 11, 31, 7, 13, 37, 29, 41, 15];
|
||||||
|
const codes = '0123456789ABCDEFGHJKLMNPQRTUWXY';
|
||||||
|
const sum = weights.reduce((acc, weight, index) => {
|
||||||
|
const char = code.charAt(index);
|
||||||
|
const codeIndex = codes.indexOf(char);
|
||||||
|
if (codeIndex === -1) return acc; // 非法字符
|
||||||
|
return acc + weight * codeIndex;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const mod = sum % 31;
|
||||||
|
const checkBitIndex = 31 - mod;
|
||||||
|
const checkBit = codes[checkBitIndex];
|
||||||
|
|
||||||
|
return code.charAt(17) === checkBit;
|
||||||
|
},
|
||||||
|
message || this.getMessage('unifiedSocialCreditCode', defaultMessage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 银行卡号验证
|
||||||
|
bankCard(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}格式不正确`;
|
||||||
|
return this.addValidatorRule(
|
||||||
|
'bankCard',
|
||||||
|
(value) => {
|
||||||
|
if (!value) return true;
|
||||||
|
|
||||||
|
// 基本格式验证:16-19位数字
|
||||||
|
const reg = /^\d{16,19}$/;
|
||||||
|
if (!reg.test(value)) return false;
|
||||||
|
|
||||||
|
// Luhn算法验证
|
||||||
|
let sum = 0;
|
||||||
|
let isEven = false;
|
||||||
|
|
||||||
|
// 从右向左遍历
|
||||||
|
for (let i = value.length - 1; i >= 0; i--) {
|
||||||
|
let digit = parseInt(value.charAt(i));
|
||||||
|
|
||||||
|
if (isEven) {
|
||||||
|
digit *= 2;
|
||||||
|
if (digit > 9) {
|
||||||
|
digit -= 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sum += digit;
|
||||||
|
isEven = !isEven;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum % 10 === 0;
|
||||||
|
},
|
||||||
|
message || this.getMessage('bankCard', defaultMessage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL验证
|
||||||
|
url(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}格式不正确`;
|
||||||
|
return this.addPatternRule('url', /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/, message || this.getMessage('url', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP地址验证
|
||||||
|
ip(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}格式不正确`;
|
||||||
|
return this.addPatternRule('ip', /^(\d{1,3}\.){3}\d{1,3}$/, message || this.getMessage('ip', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 邮政编码验证
|
||||||
|
postalCode(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}格式不正确`;
|
||||||
|
return this.addPatternRule('postalCode', /^\d{6}$/, message || this.getMessage('postalCode', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期格式验证 (YYYY-MM-DD)
|
||||||
|
date(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}格式不正确,应为YYYY-MM-DD格式`;
|
||||||
|
return this.addPatternRule('date', /^\d{4}-\d{2}-\d{2}$/, message || this.getMessage('date', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间格式验证 (HH:mm)
|
||||||
|
time(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}格式不正确,应为HH:mm格式`;
|
||||||
|
return this.addPatternRule('time', /^([01]\d|2[0-3]):[0-5]\d$/, message || this.getMessage('time', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期时间格式验证 (YYYY-MM-DD HH:mm)
|
||||||
|
datetime(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}格式不正确,应为YYYY-MM-DD HH:mm格式`;
|
||||||
|
return this.addPatternRule('datetime', /^\d{4}-\d{2}-\d{2} ([01]\d|2[0-3]):[0-5]\d$/, message || this.getMessage('datetime', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字母验证
|
||||||
|
letter(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}只能包含字母`;
|
||||||
|
return this.addPatternRule('letter', /^[a-zA-Z]+$/, message || this.getMessage('letter', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字母和数字验证
|
||||||
|
alphanumeric(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}只能包含字母和数字`;
|
||||||
|
return this.addPatternRule('alphanumeric', /^[a-zA-Z0-9]+$/, message || this.getMessage('alphanumeric', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中文验证
|
||||||
|
chinese(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}只能包含中文字符`;
|
||||||
|
return this.addPatternRule('chinese', /^[\u4e00-\u9fa5]+$/, message || this.getMessage('chinese', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不包含空格验证
|
||||||
|
noSpace(message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}不能包含空格`;
|
||||||
|
return this.addPatternRule('noSpace', /^[^\s]*$/, message || this.getMessage('noSpace', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最小值验证(用于数字)
|
||||||
|
minValue(min: number, message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}不能小于${min}`;
|
||||||
|
return this.addValidatorRule(
|
||||||
|
'minValue',
|
||||||
|
(value) => {
|
||||||
|
if (value === '' || value === null || value === undefined) return true;
|
||||||
|
const num = Number(value);
|
||||||
|
return !isNaN(num) && num >= min;
|
||||||
|
},
|
||||||
|
message || this.getMessage('minValue', defaultMessage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最大值验证(用于数字)
|
||||||
|
maxValue(max: number, message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}不能大于${max}`;
|
||||||
|
return this.addValidatorRule(
|
||||||
|
'maxValue',
|
||||||
|
(value) => {
|
||||||
|
if (value === '' || value === null || value === undefined) return true;
|
||||||
|
const num = Number(value);
|
||||||
|
return !isNaN(num) && num <= max;
|
||||||
|
},
|
||||||
|
message || this.getMessage('maxValue', defaultMessage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数值范围验证
|
||||||
|
rangeValue(min: number, max: number, message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}必须在${min}到${max}之间`;
|
||||||
|
return this.addValidatorRule(
|
||||||
|
'rangeValue',
|
||||||
|
(value) => {
|
||||||
|
if (value === '' || value === null || value === undefined) return true;
|
||||||
|
const num = Number(value);
|
||||||
|
return !isNaN(num) && num >= min && num <= max;
|
||||||
|
},
|
||||||
|
message || this.getMessage('rangeValue', defaultMessage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等于某个值验证
|
||||||
|
equals(compareValue: any, message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}值不正确`;
|
||||||
|
return this.addValidatorRule('equals', (value) => value === compareValue, message || this.getMessage('equals', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 与另一字段相等验证(常用于确认密码)
|
||||||
|
equalsTo(getCompareValue: () => any, message?: string): ValidationBuilder {
|
||||||
|
const defaultMessage = `${this.label}与确认字段不一致`;
|
||||||
|
return this.addValidatorRule('equalsTo', (value) => value === getCompareValue(), message || this.getMessage('equalsTo', defaultMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义验证器
|
||||||
|
custom(validator: (value: any) => boolean, message: string): ValidationBuilder {
|
||||||
|
return this.addValidatorRule('custom', validator, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建并返回规则数组
|
||||||
|
build(): ValidationRule[] {
|
||||||
|
return this.rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空规则
|
||||||
|
clear(): ValidationBuilder {
|
||||||
|
this.rules.length = 0; // 更简洁的清空方式
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工厂函数,创建验证建造者实例
|
||||||
|
* @param propOrConfig
|
||||||
|
* @param trigger
|
||||||
|
* @example 示例1: 必填 + 手机号校验
|
||||||
|
* createValid('手机号码').required().mobile().build();
|
||||||
|
* @example 示例2:必填 + 身份证号 示例
|
||||||
|
* createValid('身份证号').required().idCard().build();
|
||||||
|
* @example 示例3:设置默认触发方式
|
||||||
|
* createValid('邮箱', 'blur').required().email().build();
|
||||||
|
*/
|
||||||
|
const createValid = (propOrConfig: string | ValidatorConfig, trigger?: 'blur' | 'change' | 'submit' | string | string[]): ValidationBuilder => {
|
||||||
|
// 判断第一个参数是字符串还是配置对象
|
||||||
|
if (typeof propOrConfig === 'string') {
|
||||||
|
// 第一个参数是字符串,表示 prop
|
||||||
|
const config: ValidatorConfig = {
|
||||||
|
prop: propOrConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果提供了 trigger 参数,则添加到配置中
|
||||||
|
if (trigger !== undefined) config.trigger = trigger;
|
||||||
|
|
||||||
|
return new ValidationBuilder(config);
|
||||||
|
} else {
|
||||||
|
// 第一个参数是配置对象,按原来的方式处理
|
||||||
|
return new ValidationBuilder(propOrConfig);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ValidationBuilder, createValid };
|
||||||
Loading…
Reference in New Issue
Block a user