34 changed files with 1453 additions and 84 deletions
@ -0,0 +1 @@ |
|||
export * from './validation'; |
|||
@ -0,0 +1,46 @@ |
|||
export const ValidationEnum = { |
|||
DoNotMatch: "'{0}' and '{1}' do not match.", |
|||
FieldDoNotValidCreditCardNumber: |
|||
'The {0} field is not a valid credit card number.', |
|||
FieldDoNotValidEmailAddress: 'The {0} field is not a valid e-mail address.', |
|||
FieldDoNotValidFullyQualifiedUrl: |
|||
'The {0} field is not a valid fully-qualified http, https, or ftp URL.', |
|||
FieldDoNotValidPhoneNumber: 'The {0} field is not a valid phone number.', |
|||
FieldInvalid: 'The field {0} is invalid.', |
|||
FieldIsNotValid: '{0} is not valid.', |
|||
FieldMustBeetWeen: 'The field {0} must be between {1} and {2}.', |
|||
FieldMustBeStringOrArrayWithMaximumLength: |
|||
"The field {0} must be a string or array type with a maximum length of '{1}'.", |
|||
FieldMustBeStringOrArrayWithMinimumLength: |
|||
"The field {0} must be a string or array type with a minimum length of '{1}'.", |
|||
FieldMustBeStringWithMaximumLength: |
|||
'The field {0} must be a string with a maximum length of {1}.', |
|||
FieldMustBeStringWithMinimumLengthAndMaximumLength: |
|||
'The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.', |
|||
FieldMustMatchRegularExpression: |
|||
"The field {0} must match the regular expression '{1}'.", |
|||
FieldOnlyAcceptsFilesExtensions: |
|||
'The {0} field only accepts files with the following extensions: {1}', |
|||
FieldRequired: 'The {0} field is required.', |
|||
ThisFieldIsInvalid: 'ThisFieldIsInvalid.', |
|||
ThisFieldIsNotAValidCreditCardNumber: 'ThisFieldIsNotAValidCreditCardNumber.', |
|||
ThisFieldIsNotAValidEmailAddress: 'ThisFieldIsNotAValidEmailAddress.', |
|||
ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl: |
|||
'ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl', |
|||
ThisFieldIsNotAValidPhoneNumber: 'ThisFieldIsNotAValidPhoneNumber.', |
|||
ThisFieldIsNotValid: 'ThisFieldIsNotValid.', |
|||
ThisFieldIsRequired: 'ThisFieldIsRequired.', |
|||
ThisFieldMustBeAStringOrArrayTypeWithAMaximumLength: |
|||
'ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthOf{0}', |
|||
ThisFieldMustBeAStringOrArrayTypeWithAMinimumLength: |
|||
'ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}', |
|||
ThisFieldMustBeAStringWithAMaximumLength: |
|||
'ThisFieldMustBeAStringWithAMaximumLengthOf{0}', |
|||
ThisFieldMustBeAStringWithAMinimumLengthAndAMaximumLength: |
|||
'ThisFieldMustBeAStringWithAMinimumLengthOf{1}AndAMaximumLengthOf{0}', |
|||
ThisFieldMustBeBetween: 'ThisFieldMustBeBetween{0}And{1}', |
|||
ThisFieldMustMatchTheRegularExpression: |
|||
'ThisFieldMustMatchTheRegularExpression{0}', |
|||
ThisFieldOnlyAcceptsFilesWithTheFollowingExtensions: |
|||
'ThisFieldOnlyAcceptsFilesWithTheFollowingExtensions:{0}', |
|||
}; |
|||
@ -0,0 +1,3 @@ |
|||
export * from './useLocalization'; |
|||
export * from './useSettings'; |
|||
export * from './useValidation'; |
|||
@ -0,0 +1,70 @@ |
|||
import type { Dictionary, StringLocalizer } from '../types'; |
|||
|
|||
import { computed } from 'vue'; |
|||
|
|||
import { merge } from 'lodash'; |
|||
|
|||
import { useAbpStore } from '../store/abp'; |
|||
import { format } from '../utils/string'; |
|||
|
|||
export function useLocalization(resourceNames?: string | string[]) { |
|||
const abpStore = useAbpStore(); |
|||
const getResource = computed(() => { |
|||
if (!abpStore.application) { |
|||
return {}; |
|||
} |
|||
const { values } = abpStore.application.localization; |
|||
|
|||
let resource: { [key: string]: string } = {}; |
|||
if (resourceNames) { |
|||
if (Array.isArray(resourceNames)) { |
|||
resourceNames.forEach((name) => { |
|||
resource = merge(resource, values[name]); |
|||
}); |
|||
} else { |
|||
resource = merge(resource, values[resourceNames]); |
|||
} |
|||
} else { |
|||
Object.keys(values).forEach((rs) => { |
|||
resource = merge(resource, values[rs]); |
|||
}); |
|||
} |
|||
|
|||
return resource; |
|||
}); |
|||
const getResourceByName = computed(() => { |
|||
return (resource: string): Dictionary<string, string> => { |
|||
if (!abpStore.application) { |
|||
return {}; |
|||
} |
|||
const { values } = abpStore.application.localization; |
|||
return values[resource] ?? {}; |
|||
}; |
|||
}); |
|||
|
|||
function L(key: string, args?: any[] | Record<string, string> | undefined) { |
|||
if (!key) return ''; |
|||
if (!getResource.value) return key; |
|||
if (!getResource.value[key]) return key; |
|||
return format(getResource.value[key], args ?? []); |
|||
} |
|||
|
|||
function Lr( |
|||
resource: string, |
|||
key: string, |
|||
args?: any[] | Record<string, string> | undefined, |
|||
) { |
|||
if (!key) return ''; |
|||
const findResource = getResourceByName.value(resource); |
|||
if (!findResource) return key; |
|||
if (!findResource[key]) return key; |
|||
return format(findResource[key], args ?? []); |
|||
} |
|||
|
|||
const localizer: StringLocalizer = { |
|||
L, |
|||
Lr, |
|||
}; |
|||
|
|||
return { L, localizer, Lr }; |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
import type { ISettingProvider, SettingValue } from '../types/settings'; |
|||
|
|||
import { computed } from 'vue'; |
|||
|
|||
import { useAbpStore } from '../store'; |
|||
|
|||
export function useSettings(): ISettingProvider { |
|||
const getSettings = computed(() => { |
|||
const abpStore = useAbpStore(); |
|||
if (!abpStore.application) { |
|||
return []; |
|||
} |
|||
const { values: settings } = abpStore.application.setting; |
|||
const settingValues = Object.keys(settings).map((key): SettingValue => { |
|||
return { |
|||
name: key, |
|||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|||
value: settings[key]!, |
|||
}; |
|||
}); |
|||
return settingValues; |
|||
}); |
|||
|
|||
function get(name: string): SettingValue | undefined { |
|||
return getSettings.value.find((setting) => name === setting.name); |
|||
} |
|||
|
|||
function getAll(...names: string[]): SettingValue[] { |
|||
if (names) { |
|||
return getSettings.value.filter((setting) => |
|||
names.includes(setting.name), |
|||
); |
|||
} |
|||
return getSettings.value; |
|||
} |
|||
|
|||
function getOrDefault<T>(name: string, defaultValue: T): string | T { |
|||
const setting = get(name); |
|||
if (!setting) { |
|||
return defaultValue; |
|||
} |
|||
return setting.value; |
|||
} |
|||
|
|||
const settingProvider: ISettingProvider = { |
|||
getAll(...names: string[]) { |
|||
return getAll(...names); |
|||
}, |
|||
getNumber(name: string, defaultValue: number = 0) { |
|||
const value = getOrDefault(name, defaultValue); |
|||
const num = Number(value); |
|||
return Number.isNaN(num) ? defaultValue : num; |
|||
}, |
|||
getOrEmpty(name: string) { |
|||
return getOrDefault(name, ''); |
|||
}, |
|||
isTrue(name: string) { |
|||
const value = getOrDefault(name, 'false'); |
|||
return value.toLowerCase() === 'true'; |
|||
}, |
|||
}; |
|||
|
|||
return settingProvider; |
|||
} |
|||
@ -0,0 +1,405 @@ |
|||
import type { RuleCreator } from '../types/rules'; |
|||
import type { |
|||
Field, |
|||
FieldBeetWeen, |
|||
FieldContains, |
|||
FieldDefineValidator, |
|||
FieldLength, |
|||
FieldMatch, |
|||
FieldRange, |
|||
FieldRegular, |
|||
FieldValidator, |
|||
Rule, |
|||
RuleType, |
|||
} from '../types/validations'; |
|||
|
|||
import { ValidationEnum } from '../constants'; |
|||
import { isEmail, isPhone } from '../utils/regex'; |
|||
import { useLocalization } from './useLocalization'; |
|||
|
|||
export function useValidation() { |
|||
const { L } = useLocalization(['AbpValidation']); |
|||
function _getFieldName(field: Field) { |
|||
return __getFieldName( |
|||
field.name ?? '', |
|||
field.resourceName, |
|||
field.prefix, |
|||
field.connector, |
|||
); |
|||
} |
|||
|
|||
function __getFieldName( |
|||
fieldName: string, |
|||
resourceName?: string, |
|||
prefix?: string, |
|||
connector?: string, |
|||
) { |
|||
if (fieldName && resourceName) { |
|||
fieldName = prefix |
|||
? `${prefix}${connector ?? ':'}${fieldName}` |
|||
: fieldName; |
|||
const { L: l } = useLocalization(resourceName); |
|||
return l(fieldName); |
|||
} |
|||
return fieldName; |
|||
} |
|||
|
|||
function _createRule(options: { |
|||
len?: number; |
|||
max?: number; |
|||
message?: string; |
|||
min?: number; |
|||
required?: boolean; |
|||
trigger?: 'blur' | 'change' | ['change', 'blur']; |
|||
type?: 'array' | RuleType; |
|||
validator?: ( |
|||
rule: any, |
|||
value: any, |
|||
callback: any, |
|||
source?: any, |
|||
options?: any, |
|||
) => Promise<void> | void; |
|||
}): Rule[] { |
|||
return [ |
|||
{ |
|||
len: options.len, |
|||
max: options.max, |
|||
message: options.message, |
|||
min: options.min, |
|||
required: options.required, |
|||
trigger: options.trigger, |
|||
type: options.type, |
|||
validator: options.validator, |
|||
}, |
|||
]; |
|||
} |
|||
|
|||
function _createValidator( |
|||
field: Field, |
|||
useNameEnum: string, |
|||
notNameEnum: string, |
|||
required?: boolean, |
|||
): Rule { |
|||
const message = field.name |
|||
? L(useNameEnum, [_getFieldName(field)]) |
|||
: L(notNameEnum); |
|||
return { |
|||
message, |
|||
required, |
|||
trigger: field.trigger, |
|||
type: field.type, |
|||
}; |
|||
} |
|||
|
|||
function _createLengthValidator( |
|||
field: FieldLength, |
|||
checkMaximum: boolean, |
|||
useNameEnum: string, |
|||
notNameEnum: string, |
|||
required?: boolean, |
|||
): Rule { |
|||
const message = field.name |
|||
? L(useNameEnum, [_getFieldName(field), field.length]) |
|||
: L(notNameEnum, [field.length]); |
|||
|
|||
function checkLength(value: any[] | string) { |
|||
return checkMaximum |
|||
? field.length > value.length |
|||
: value.length > field.length; |
|||
} |
|||
|
|||
return { |
|||
message, |
|||
required, |
|||
trigger: field.trigger, |
|||
type: field.type, |
|||
validator: (_: any, value: string) => { |
|||
if (!checkLength(value)) { |
|||
return Promise.reject(message); |
|||
} |
|||
return Promise.resolve(); |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
function _createLengthRangValidator( |
|||
field: FieldRange, |
|||
useNameEnum: string, |
|||
notNameEnum: string, |
|||
required?: boolean, |
|||
): Rule { |
|||
const message = field.name |
|||
? L(useNameEnum, [_getFieldName(field), field.minimum, field.maximum]) |
|||
: L(notNameEnum, [field.minimum, field.maximum]); |
|||
return { |
|||
message, |
|||
required, |
|||
trigger: field.trigger, |
|||
type: field.type, |
|||
validator: (_: any, value: string) => { |
|||
if (value.length < field.minimum || value.length > field.maximum) { |
|||
return Promise.reject(message); |
|||
} |
|||
return Promise.resolve(); |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
function _createBeetWeenValidator(field: FieldBeetWeen): Rule { |
|||
const message = field.name |
|||
? L(ValidationEnum.FieldMustBeetWeen, [ |
|||
_getFieldName(field), |
|||
field.start, |
|||
field.end, |
|||
]) |
|||
: L(ValidationEnum.ThisFieldMustBeBetween, [field.start, field.end]); |
|||
return { |
|||
message, |
|||
trigger: field.trigger, |
|||
validator: (_: any, value: number) => { |
|||
// beetween不在进行必输检查, 改为数字有效性检查
|
|||
if (Number.isNaN(value)) { |
|||
return Promise.reject(message); |
|||
} |
|||
return value < field.start || value > field.end |
|||
? Promise.reject(message) |
|||
: Promise.resolve(); |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
function _createRegularExpressionValidator( |
|||
field: FieldRegular, |
|||
required?: boolean, |
|||
): Rule { |
|||
const message = field.name |
|||
? L(ValidationEnum.FieldMustMatchRegularExpression, [ |
|||
_getFieldName(field), |
|||
field.expression, |
|||
]) |
|||
: L(ValidationEnum.ThisFieldMustMatchTheRegularExpression, [ |
|||
field.expression, |
|||
]); |
|||
return { |
|||
message, |
|||
pattern: new RegExp(field.expression), |
|||
required, |
|||
trigger: field.trigger, |
|||
type: field.type, |
|||
}; |
|||
} |
|||
|
|||
function _createEmailValidator(field: Field, required?: boolean): Rule { |
|||
const message = field.name |
|||
? L(ValidationEnum.FieldDoNotValidEmailAddress, [_getFieldName(field)]) |
|||
: L(ValidationEnum.ThisFieldIsNotAValidEmailAddress); |
|||
return { |
|||
message, |
|||
required, |
|||
trigger: field.trigger, |
|||
type: field.type, |
|||
validator: (_: any, value: string) => { |
|||
if (!isEmail(value)) { |
|||
return Promise.reject(message); |
|||
} |
|||
return Promise.resolve(); |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
function _createPhoneValidator(field: Field, required?: boolean): Rule { |
|||
const message = field.name |
|||
? L(ValidationEnum.FieldDoNotValidPhoneNumber, [_getFieldName(field)]) |
|||
: L(ValidationEnum.ThisFieldIsNotAValidPhoneNumber); |
|||
return { |
|||
message, |
|||
required, |
|||
trigger: field.trigger, |
|||
type: field.type, |
|||
validator: (_: any, value: string) => { |
|||
if (!isPhone(value)) { |
|||
return Promise.reject(message); |
|||
} |
|||
return Promise.resolve(); |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
const ruleCreator: RuleCreator = { |
|||
defineValidator(field: FieldDefineValidator) { |
|||
return _createRule(field); |
|||
}, |
|||
doNotMatch(field: FieldMatch) { |
|||
const message = L(ValidationEnum.DoNotMatch, [ |
|||
__getFieldName(field.name, field.resourceName, field.prefix), |
|||
__getFieldName(field.matchField, field.resourceName, field.prefix), |
|||
]); |
|||
return _createRule({ |
|||
message, |
|||
required: field.required, |
|||
trigger: field.trigger, |
|||
type: field.type, |
|||
validator: (_, value: string) => { |
|||
if (value !== field.matchValue) { |
|||
return Promise.reject(message); |
|||
} |
|||
return Promise.resolve(); |
|||
}, |
|||
}); |
|||
}, |
|||
fieldDoNotValidCreditCardNumber(field: Field) { |
|||
if (field.name) { |
|||
return _createRule({ |
|||
message: L(ValidationEnum.FieldDoNotValidCreditCardNumber, [ |
|||
_getFieldName(field), |
|||
]), |
|||
trigger: field.trigger, |
|||
type: field.type, |
|||
}); |
|||
} |
|||
return _createRule({ |
|||
message: L(ValidationEnum.ThisFieldIsNotAValidCreditCardNumber), |
|||
trigger: field.trigger, |
|||
type: field.type, |
|||
}); |
|||
}, |
|||
fieldDoNotValidEmailAddress(field: Field) { |
|||
return [_createEmailValidator(field)]; |
|||
}, |
|||
fieldDoNotValidFullyQualifiedUrl(field: Field) { |
|||
if (field.name) { |
|||
return _createRule({ |
|||
message: L(ValidationEnum.FieldDoNotValidFullyQualifiedUrl, [ |
|||
_getFieldName(field), |
|||
]), |
|||
trigger: field.trigger, |
|||
type: field.type, |
|||
}); |
|||
} |
|||
return _createRule({ |
|||
message: L( |
|||
ValidationEnum.ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl, |
|||
), |
|||
trigger: field.trigger, |
|||
type: field.type, |
|||
}); |
|||
}, |
|||
fieldDoNotValidPhoneNumber(field: Field) { |
|||
return [_createPhoneValidator(field)]; |
|||
}, |
|||
fieldInvalid(field: FieldValidator) { |
|||
const message = field.name |
|||
? L(ValidationEnum.FieldInvalid, [_getFieldName(field)]) |
|||
: L(ValidationEnum.ThisFieldIsInvalid); |
|||
return _createRule({ |
|||
message, |
|||
required: field.required, |
|||
trigger: field.trigger, |
|||
type: field.type, |
|||
validator: (_, value: any) => { |
|||
if (!field.validator(value)) { |
|||
return Promise.reject(message); |
|||
} |
|||
return Promise.resolve(); |
|||
}, |
|||
}); |
|||
}, |
|||
fieldIsNotValid(field: FieldValidator) { |
|||
const message = field.name |
|||
? L(ValidationEnum.FieldIsNotValid, [_getFieldName(field)]) |
|||
: L(ValidationEnum.ThisFieldIsNotValid); |
|||
return _createRule({ |
|||
message, |
|||
required: field.required, |
|||
trigger: field.trigger, |
|||
type: field.type, |
|||
validator: (_, value: any) => { |
|||
if (field.validator(value)) { |
|||
return Promise.reject(message); |
|||
} |
|||
return Promise.resolve(); |
|||
}, |
|||
}); |
|||
}, |
|||
fieldMustBeetWeen(field: FieldBeetWeen) { |
|||
return [_createBeetWeenValidator(field)]; |
|||
}, |
|||
fieldMustBeStringOrArrayWithMaximumLength(field: FieldLength) { |
|||
return [ |
|||
_createLengthValidator( |
|||
field, |
|||
true, |
|||
ValidationEnum.FieldMustBeStringOrArrayWithMaximumLength, |
|||
ValidationEnum.ThisFieldMustBeAStringOrArrayTypeWithAMaximumLength, |
|||
), |
|||
]; |
|||
}, |
|||
fieldMustBeStringOrArrayWithMinimumLength(field: FieldLength) { |
|||
return [ |
|||
_createLengthValidator( |
|||
field, |
|||
false, |
|||
ValidationEnum.FieldMustBeStringOrArrayWithMinimumLength, |
|||
ValidationEnum.ThisFieldMustBeAStringOrArrayTypeWithAMinimumLength, |
|||
), |
|||
]; |
|||
}, |
|||
fieldMustBeStringWithMaximumLength(field: FieldLength) { |
|||
return [ |
|||
_createLengthValidator( |
|||
field, |
|||
true, |
|||
ValidationEnum.FieldMustBeStringWithMaximumLength, |
|||
ValidationEnum.ThisFieldMustBeAStringWithAMaximumLength, |
|||
), |
|||
]; |
|||
}, |
|||
fieldMustBeStringWithMinimumLengthAndMaximumLength(field: FieldRange) { |
|||
return [ |
|||
_createLengthRangValidator( |
|||
field, |
|||
ValidationEnum.FieldMustBeStringWithMinimumLengthAndMaximumLength, |
|||
ValidationEnum.ThisFieldMustBeAStringWithAMinimumLengthAndAMaximumLength, |
|||
), |
|||
]; |
|||
}, |
|||
fieldMustMatchRegularExpression(field: FieldRegular) { |
|||
return [_createRegularExpressionValidator(field)]; |
|||
}, |
|||
fieldOnlyAcceptsFilesExtensions(field: FieldContains) { |
|||
const message = field.name |
|||
? L(ValidationEnum.FieldOnlyAcceptsFilesExtensions, [ |
|||
_getFieldName(field), |
|||
field.value, |
|||
]) |
|||
: L(ValidationEnum.ThisFieldMustMatchTheRegularExpression, [ |
|||
field.value, |
|||
]); |
|||
return _createRule({ |
|||
message, |
|||
trigger: field.trigger, |
|||
type: field.type, |
|||
validator: (_, value: string) => { |
|||
if (!field.value.includes(value)) { |
|||
return Promise.reject(message); |
|||
} |
|||
return Promise.resolve(); |
|||
}, |
|||
}); |
|||
}, |
|||
fieldRequired(field: Field) { |
|||
return [ |
|||
_createValidator( |
|||
field, |
|||
ValidationEnum.FieldRequired, |
|||
ValidationEnum.ThisFieldIsRequired, |
|||
true, |
|||
), |
|||
]; |
|||
}, |
|||
}; |
|||
|
|||
return { |
|||
ruleCreator, |
|||
}; |
|||
} |
|||
@ -1,3 +1,5 @@ |
|||
export * from './constants'; |
|||
export * from './hooks'; |
|||
export * from './store'; |
|||
export * from './types'; |
|||
export * from './utils'; |
|||
|
|||
@ -1,2 +1,6 @@ |
|||
export * from './dto'; |
|||
export * from './global'; |
|||
export * from './localization'; |
|||
export * from './rules'; |
|||
export * from './settings'; |
|||
export * from './validations'; |
|||
|
|||
@ -0,0 +1,10 @@ |
|||
interface StringLocalizer { |
|||
L(key: string, args?: any[] | Record<string, any> | undefined): string; |
|||
Lr( |
|||
resource: string, |
|||
key: string, |
|||
args?: any[] | Record<string, any> | undefined, |
|||
): string; |
|||
} |
|||
|
|||
export type { StringLocalizer }; |
|||
@ -0,0 +1,50 @@ |
|||
import type { |
|||
Field, |
|||
FieldBeetWeen, |
|||
FieldContains, |
|||
FieldDefineValidator, |
|||
FieldLength, |
|||
FieldMatch, |
|||
FieldRange, |
|||
FieldRegular, |
|||
FieldValidator, |
|||
Rule, |
|||
} from './validations'; |
|||
|
|||
/** 规则创建器 */ |
|||
interface RuleCreator { |
|||
/** 自定义验证器 */ |
|||
defineValidator(field: FieldDefineValidator): Rule[]; |
|||
/** input 与 value 是否匹配 */ |
|||
doNotMatch(field: FieldMatch): Rule[]; |
|||
/** 字段{0}不是有效的信用卡号码 */ |
|||
fieldDoNotValidCreditCardNumber(field: Field): Rule[]; |
|||
/** 字段{0}不是有效的邮箱地址 */ |
|||
fieldDoNotValidEmailAddress(field: Field): Rule[]; |
|||
/** 字段{0}不是有效的完全限定的http,https或ftp URL. */ |
|||
fieldDoNotValidFullyQualifiedUrl(field: Field): Rule[]; |
|||
/** 字段{0}不是有效的手机号码 */ |
|||
fieldDoNotValidPhoneNumber(field: Field): Rule[]; |
|||
/** 字段是无效值 */ |
|||
fieldInvalid(field: FieldValidator): Rule[]; |
|||
/** 验证未通过 */ |
|||
fieldIsNotValid(field: FieldValidator): Rule[]; |
|||
/** 字段{0}值必须在{1}和{2}范围内 */ |
|||
fieldMustBeetWeen(field: FieldBeetWeen): Rule[]; |
|||
/** 字段{0}必须是最大长度为'{1}'的字符串或数组 */ |
|||
fieldMustBeStringOrArrayWithMaximumLength(field: FieldLength): Rule[]; |
|||
/** 字段{0}必须是最小长度为'{1}'的字符串或数组 */ |
|||
fieldMustBeStringOrArrayWithMinimumLength(field: FieldLength): Rule[]; |
|||
/** 字段{0}必须是最大长度为{1}的字符串 */ |
|||
fieldMustBeStringWithMaximumLength(field: FieldLength): Rule[]; |
|||
/** 字段{0}必须是最小长度为{2}并且最大长度{1}的字符串 */ |
|||
fieldMustBeStringWithMinimumLengthAndMaximumLength(field: FieldRange): Rule[]; |
|||
/** 字段{0}与正则表达式不匹配 */ |
|||
fieldMustMatchRegularExpression(field: FieldRegular): Rule[]; |
|||
/** {0}字段只允许以下扩展名的文件: {1} */ |
|||
fieldOnlyAcceptsFilesExtensions(field: FieldContains): Rule[]; |
|||
/** 字段{0}不可为空 */ |
|||
fieldRequired(field: Field): Rule[]; |
|||
} |
|||
|
|||
export type { RuleCreator }; |
|||
@ -0,0 +1,31 @@ |
|||
import type { NameValue } from './global'; |
|||
|
|||
type SettingValue = NameValue<string>; |
|||
/** |
|||
* 设置接口 |
|||
*/ |
|||
interface ISettingProvider { |
|||
/** |
|||
* 获取设定值结合 |
|||
* @param names 过滤的设置名称 |
|||
*/ |
|||
getAll(...names: string[]): SettingValue[]; |
|||
/** |
|||
* 查询 number 类型设定值 |
|||
* @param name 设置名称 |
|||
* @returns 返回类型为 number 的设定值, 默认0 |
|||
*/ |
|||
getNumber(name: string): number; |
|||
/** |
|||
* 获取设定值,如果为空返回空字符串 |
|||
* @param name 设置名称 |
|||
*/ |
|||
getOrEmpty(name: string): string; |
|||
/** |
|||
* 查询 boolean 类型设定值 |
|||
* @param name 设置名称 |
|||
*/ |
|||
isTrue(name: string): boolean; |
|||
} |
|||
|
|||
export type { ISettingProvider, SettingValue }; |
|||
@ -0,0 +1,112 @@ |
|||
type RuleType = |
|||
| 'boolean' |
|||
| 'date' |
|||
| 'email' |
|||
| 'enum' |
|||
| 'float' |
|||
| 'hex' |
|||
| 'integer' |
|||
| 'method' |
|||
| 'number' |
|||
| 'object' |
|||
| 'regexp' |
|||
| 'string' |
|||
| 'url'; |
|||
|
|||
interface Rule { |
|||
[key: string]: any; |
|||
trigger?: 'blur' | 'change' | ['change', 'blur']; |
|||
type?: 'array' | RuleType; |
|||
} |
|||
|
|||
interface Field extends Rule { |
|||
/** 连接符 |
|||
* @description 用于本地化字段名称时的连接字符 |
|||
* @example . => L('ResourceName.DisplayName.Field') |
|||
* @example : => L('ResourceName.DisplayName:Field') |
|||
*/ |
|||
connector?: string; |
|||
/** 字段名称 */ |
|||
name?: string; |
|||
/** 字段前缀 |
|||
* @description 用于本地化字段名称 |
|||
* @example DisplayName => L('ResourceName.DisplayName:Field') |
|||
*/ |
|||
prefix?: string; |
|||
/** 本地化资源 */ |
|||
resourceName?: string; |
|||
} |
|||
|
|||
interface FieldRequired extends Field { |
|||
/** 是否必须 */ |
|||
required?: boolean; |
|||
} |
|||
|
|||
interface FieldBeetWeen extends Field { |
|||
/** 结束值 */ |
|||
end: number; |
|||
/** 起始值 */ |
|||
start: number; |
|||
} |
|||
|
|||
interface FieldLength extends Field { |
|||
/** 长度 */ |
|||
length: number; |
|||
} |
|||
|
|||
interface FieldRange extends Field { |
|||
/** 最大数值 */ |
|||
maximum: number; |
|||
/** 最小数值 */ |
|||
minimum: number; |
|||
} |
|||
|
|||
interface FieldRegular extends Field { |
|||
/** 正则表达式 */ |
|||
expression: string; |
|||
} |
|||
|
|||
interface FieldMatch extends FieldRequired { |
|||
/** 对比字段 */ |
|||
matchField: string; |
|||
/** 对比字段值 */ |
|||
matchValue: string; |
|||
/** 字段名称 */ |
|||
name: string; |
|||
} |
|||
|
|||
interface FieldContains extends Field { |
|||
/** 验证的值中是否包含在定义的值中 */ |
|||
value: string; |
|||
} |
|||
|
|||
interface FieldValidator extends FieldRequired { |
|||
/** 值是否有效验证器 */ |
|||
validator: (value: any) => boolean; |
|||
} |
|||
|
|||
interface FieldDefineValidator extends FieldRequired { |
|||
message?: string; |
|||
validator: ( |
|||
rule: any, |
|||
value: any, |
|||
callback: any, |
|||
source?: any, |
|||
options?: any, |
|||
) => Promise<void> | void; |
|||
} |
|||
|
|||
export type { |
|||
Field, |
|||
FieldBeetWeen, |
|||
FieldContains, |
|||
FieldDefineValidator, |
|||
FieldLength, |
|||
FieldMatch, |
|||
FieldRange, |
|||
FieldRegular, |
|||
FieldRequired, |
|||
FieldValidator, |
|||
Rule, |
|||
RuleType, |
|||
}; |
|||
@ -1,2 +1,4 @@ |
|||
export * from './date'; |
|||
export * from './regex'; |
|||
export * from './string'; |
|||
export * from './tree'; |
|||
|
|||
@ -0,0 +1,47 @@ |
|||
/* eslint-disable regexp/no-unused-capturing-group */ |
|||
export function isRegMatch(reg: RegExp, val: string) { |
|||
return reg.test(val); |
|||
} |
|||
|
|||
export function isEmail(val: string) { |
|||
const reg = /^\w+((-\w+)|(\.\w+))*@[A-Z0-9]+((\.|-)[A-Z0-9]+)*\.[A-Z0-9]+$/i; |
|||
return isRegMatch(reg, val); |
|||
} |
|||
|
|||
export function isPhone(val: string) { |
|||
const reg = /^(13\d|14[5|7]|15\d|17\d|18\d|19\d)\d{8}$/; |
|||
return isRegMatch(reg, val); |
|||
} |
|||
|
|||
export function isDigit(val: string) { |
|||
return [...val].some((element) => _isDigit(element)); |
|||
} |
|||
|
|||
export function isLower(val: string) { |
|||
return [...val].some((element) => _isLower(element)); |
|||
} |
|||
|
|||
export function isUpper(val: string) { |
|||
return [...val].some((element) => _isUpper(element)); |
|||
} |
|||
|
|||
export function isLetterOrDigit(val: string) { |
|||
const arr = [...val]; |
|||
return !arr.some((element) => _isLetterOrDigit(element)); |
|||
} |
|||
|
|||
function _isDigit(char: string) { |
|||
return isRegMatch(/\d/g, char); |
|||
} |
|||
|
|||
function _isLower(char: string) { |
|||
return isRegMatch(/[a-z]/g, char); |
|||
} |
|||
|
|||
function _isUpper(char: string) { |
|||
return isRegMatch(/[A-Z]/g, char); |
|||
} |
|||
|
|||
function _isLetterOrDigit(char: string) { |
|||
return isRegMatch(/[^ \w]/g, char); |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
/** |
|||
* |
|||
* @param str 字符串是否为空或空格 |
|||
*/ |
|||
export function isNullOrWhiteSpace(str?: string) { |
|||
return str === undefined || str === null || str === '' || str === ' '; |
|||
} |
|||
|
|||
/** |
|||
* 格式化字符串 |
|||
* @param formatted 需要处理的字符串 |
|||
* @param args 参数列表,可以是数组,也可以是对象 |
|||
* @returns 返回格式化的字符串 |
|||
* @example format('Hello, {0}!', ['World']) |
|||
* @example format('Hello, {name}!', {name: 'World'}) |
|||
*/ |
|||
export function format(formatted: string, args: any[] | object) { |
|||
if (Array.isArray(args)) { |
|||
for (const [i, arg] of args.entries()) { |
|||
const regexp = new RegExp(String.raw`\{` + i + String.raw`\}`, 'gi'); |
|||
formatted = formatted.replace(regexp, arg); |
|||
} |
|||
} else if (typeof args === 'object') { |
|||
Object.keys(args).forEach((key) => { |
|||
const regexp = new RegExp(String.raw`\{` + key + String.raw`\}`, 'gi'); |
|||
const param = (args as any)[key]; |
|||
formatted = formatted.replace(regexp, param); |
|||
}); |
|||
} |
|||
return formatted; |
|||
} |
|||
|
|||
export function getUnique(val: string) { |
|||
const arr = [...val]; |
|||
const newArr = [...new Set(arr)]; |
|||
return newArr.join(''); |
|||
} |
|||
@ -0,0 +1,91 @@ |
|||
<!-- eslint-disable no-unused-vars --> |
|||
<script setup lang="ts"> |
|||
import { h } from 'vue'; |
|||
|
|||
import { useVbenForm, useVbenModal } from '@vben/common-ui'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { Button, message } from 'ant-design-vue'; |
|||
|
|||
import { changePasswordApi } from '../../api/users'; |
|||
import { useRandomPassword } from '../../hooks'; |
|||
|
|||
defineOptions({ |
|||
name: 'UserPasswordModal', |
|||
}); |
|||
const emits = defineEmits<{ |
|||
(event: 'change'): void; |
|||
}>(); |
|||
|
|||
const { generatePassword } = useRandomPassword(); |
|||
|
|||
const [Form, formApi] = useVbenForm({ |
|||
commonConfig: { |
|||
// 所有表单项 |
|||
componentProps: { |
|||
class: 'w-full', |
|||
}, |
|||
}, |
|||
handleSubmit: onSubmit, |
|||
schema: [ |
|||
{ |
|||
component: 'InputSearch', |
|||
componentProps: (_, actions) => { |
|||
return { |
|||
allowClear: false, |
|||
enterButton: h( |
|||
Button, |
|||
{ |
|||
type: 'primary', |
|||
}, |
|||
() => $t('AbpIdentity.RandomPassword'), |
|||
), |
|||
onSearch: () => { |
|||
actions.resetForm(); |
|||
actions.setFieldValue('password', generatePassword()); |
|||
}, |
|||
}; |
|||
}, |
|||
fieldName: 'password', |
|||
label: $t('AbpIdentity.Password'), |
|||
rules: 'required', |
|||
}, |
|||
], |
|||
showDefaultActions: false, |
|||
}); |
|||
|
|||
const [Modal, modalApi] = useVbenModal({ |
|||
closeOnClickModal: false, |
|||
closeOnPressEscape: false, |
|||
draggable: true, |
|||
fullscreenButton: false, |
|||
onCancel() { |
|||
modalApi.close(); |
|||
}, |
|||
onConfirm: async () => { |
|||
try { |
|||
modalApi.setState({ confirmLoading: true }); |
|||
await formApi.validateAndSubmitForm(); |
|||
} finally { |
|||
modalApi.setState({ confirmLoading: false }); |
|||
} |
|||
}, |
|||
title: $t('AbpIdentity.SetPassword'), |
|||
}); |
|||
|
|||
async function onSubmit(input: Record<string, any>) { |
|||
const { id } = modalApi.getData<Record<string, any>>(); |
|||
await changePasswordApi(id, { password: input.password }); |
|||
message.success($t('AbpUi.SavedSuccessfully')); |
|||
emits('change'); |
|||
modalApi.close(); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<Modal> |
|||
<Form /> |
|||
</Modal> |
|||
</template> |
|||
|
|||
<style scoped></style> |
|||
@ -0,0 +1,2 @@ |
|||
export * from './usePasswordValidator'; |
|||
export * from './useRandomPassword'; |
|||
@ -0,0 +1,80 @@ |
|||
import { computed, unref } from 'vue'; |
|||
|
|||
import { |
|||
getUnique, |
|||
isDigit, |
|||
isLetterOrDigit, |
|||
isLower, |
|||
isNullOrWhiteSpace, |
|||
isUpper, |
|||
useLocalization, |
|||
useSettings, |
|||
ValidationEnum, |
|||
} from '@abp/core'; |
|||
|
|||
export function usePasswordValidator() { |
|||
const { settingProvider } = useSettings(); |
|||
const { getNumber, isTrue } = settingProvider; |
|||
const { L } = useLocalization(['AbpIdentity', 'AbpUi']); |
|||
|
|||
const passwordSetting = computed(() => { |
|||
return { |
|||
requiredDigit: isTrue('Abp.Identity.Password.RequireDigit'), |
|||
requiredLength: getNumber('Abp.Identity.Password.RequiredLength'), |
|||
requiredLowercase: isTrue('Abp.Identity.Password.RequireLowercase'), |
|||
requiredUniqueChars: getNumber( |
|||
'Abp.Identity.Password.RequiredUniqueChars', |
|||
), |
|||
requireNonAlphanumeric: isTrue( |
|||
'Abp.Identity.Password.RequireNonAlphanumeric', |
|||
), |
|||
requireUppercase: isTrue('Abp.Identity.Password.RequireUppercase'), |
|||
}; |
|||
}); |
|||
|
|||
function validate(password: string): Promise<void> { |
|||
return new Promise((resolve, reject) => { |
|||
if (isNullOrWhiteSpace(password)) { |
|||
return reject( |
|||
L(ValidationEnum.FieldRequired, [L('DisplayName:Password')]), |
|||
); |
|||
} |
|||
const setting = unref(passwordSetting); |
|||
if ( |
|||
setting.requiredLength > 0 && |
|||
password.length < setting.requiredLength |
|||
) { |
|||
return reject( |
|||
L('Volo.Abp.Identity:PasswordTooShort', [setting.requiredLength]), |
|||
); |
|||
} |
|||
if (setting.requireNonAlphanumeric && isLetterOrDigit(password)) { |
|||
return reject(L('Volo.Abp.Identity:PasswordRequiresNonAlphanumeric')); |
|||
} |
|||
if (setting.requiredDigit && !isDigit(password)) { |
|||
return reject(L('Volo.Abp.Identity:PasswordRequiresDigit')); |
|||
} |
|||
if (setting.requiredLowercase && !isLower(password)) { |
|||
return reject(L('Volo.Abp.Identity:PasswordRequiresLower')); |
|||
} |
|||
if (setting.requireUppercase && !isUpper(password)) { |
|||
return reject(L('Volo.Abp.Identity:PasswordRequiresUpper')); |
|||
} |
|||
if ( |
|||
setting.requiredUniqueChars >= 1 && |
|||
getUnique(password).length < setting.requiredUniqueChars |
|||
) { |
|||
return reject( |
|||
L('Volo.Abp.Identity:PasswordRequiredUniqueChars', [ |
|||
setting.requiredUniqueChars, |
|||
]), |
|||
); |
|||
} |
|||
return resolve(); |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
validate, |
|||
}; |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
import { useSettings } from '@abp/core'; |
|||
|
|||
/** |
|||
* 摘自 https://www.html5tricks.com/demo/js-passwd-generator/index.html
|
|||
* 侵权请联系删除 |
|||
*/ |
|||
export function useRandomPassword() { |
|||
const randomFunc: { [key: string]: () => string } = { |
|||
defaultNumber: getRandomNumber, |
|||
lower: getRandomLower, |
|||
number: getRandomNumber, |
|||
symbol: getRandomSymbol, |
|||
upper: getRandomUpper, |
|||
}; |
|||
|
|||
function getRandomLower() { |
|||
return String.fromCodePoint(Math.floor(Math.random() * 26) + 97); |
|||
} |
|||
|
|||
function getRandomUpper() { |
|||
return String.fromCodePoint(Math.floor(Math.random() * 26) + 65); |
|||
} |
|||
|
|||
function getRandomNumber() { |
|||
return String.fromCodePoint(Math.floor(Math.random() * 10) + 48); |
|||
} |
|||
|
|||
function getRandomSymbol() { |
|||
const symbols = '~!@#$%^&*()_+{}":?><;.,'; |
|||
return symbols[Math.floor(Math.random() * symbols.length)] ?? ''; |
|||
} |
|||
|
|||
function generatePassword() { |
|||
const { settingProvider } = useSettings(); |
|||
// 根据配置项生成随机密码
|
|||
// 密码长度
|
|||
const length = settingProvider.getNumber( |
|||
'Abp.Identity.Password.RequiredLength', |
|||
); |
|||
// 需要小写字母
|
|||
const lower = settingProvider.isTrue( |
|||
'Abp.Identity.Password.RequireLowercase', |
|||
); |
|||
// 需要大写字母
|
|||
const upper = settingProvider.isTrue( |
|||
'Abp.Identity.Password.RequireUppercase', |
|||
); |
|||
// 需要数字
|
|||
const number = settingProvider.isTrue('Abp.Identity.Password.RequireDigit'); |
|||
// 需要符号
|
|||
const symbol = settingProvider.isTrue( |
|||
'Abp.Identity.Password.RequireNonAlphanumeric', |
|||
); |
|||
// 默认生成数字
|
|||
const defaultNumber = !lower && !upper && !number && !symbol; |
|||
|
|||
let generatedPassword = ''; |
|||
const typesArr = [ |
|||
{ lower }, |
|||
{ upper }, |
|||
{ number }, |
|||
{ symbol }, |
|||
{ defaultNumber }, |
|||
].filter((item) => Object.values(item)[0]); |
|||
for (let i = 0; i < length; i++) { |
|||
typesArr.forEach((type) => { |
|||
const funcName = Object.keys(type)[0]; |
|||
if (funcName && randomFunc[funcName]) { |
|||
generatedPassword += randomFunc[funcName](); |
|||
} |
|||
}); |
|||
} |
|||
return generatedPassword.slice(0, length); |
|||
} |
|||
|
|||
return { generatePassword }; |
|||
} |
|||
Loading…
Reference in new issue