committed by
GitHub
43 changed files with 2072 additions and 134 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 './store'; |
||||
export * from './types'; |
export * from './types'; |
||||
export * from './utils'; |
export * from './utils'; |
||||
|
|||||
@ -1,2 +1,6 @@ |
|||||
export * from './dto'; |
export * from './dto'; |
||||
export * from './global'; |
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 './date'; |
||||
|
export * from './regex'; |
||||
|
export * from './string'; |
||||
export * from './tree'; |
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,123 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { IdentityUserClaimDto } from '../../types/users'; |
||||
|
|
||||
|
import { useVbenForm, useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { getAssignableClaimsApi } from '../../api/claim-types'; |
||||
|
import { createClaimApi, updateClaimApi } from '../../api/users'; |
||||
|
|
||||
|
interface IdentityUserClaimVto extends IdentityUserClaimDto { |
||||
|
userId: string; |
||||
|
} |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'UserClaimEditModal', |
||||
|
}); |
||||
|
|
||||
|
const emits = defineEmits<{ |
||||
|
(event: 'change', data: IdentityUserClaimDto): void; |
||||
|
}>(); |
||||
|
|
||||
|
const [Form, formApi] = useVbenForm({ |
||||
|
commonConfig: { |
||||
|
// 所有表单项 |
||||
|
componentProps: { |
||||
|
class: 'w-full', |
||||
|
}, |
||||
|
}, |
||||
|
handleSubmit: onSubmit, |
||||
|
schema: [ |
||||
|
{ |
||||
|
component: 'Select', |
||||
|
fieldName: 'claimType', |
||||
|
label: $t('AbpIdentity.DisplayName:ClaimType'), |
||||
|
rules: 'required', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Textarea', |
||||
|
fieldName: 'claimValue', |
||||
|
label: $t('AbpIdentity.DisplayName:ClaimValue'), |
||||
|
rules: 'required', |
||||
|
}, |
||||
|
], |
||||
|
showDefaultActions: false, |
||||
|
}); |
||||
|
const [Modal, modalApi] = useVbenModal({ |
||||
|
draggable: true, |
||||
|
fullscreenButton: false, |
||||
|
async onConfirm() { |
||||
|
await formApi.validateAndSubmitForm(); |
||||
|
}, |
||||
|
async onOpenChange(isOpen: boolean) { |
||||
|
if (isOpen) { |
||||
|
try { |
||||
|
modalApi.setState({ loading: true }); |
||||
|
const claimVto = modalApi.getData<IdentityUserClaimVto>(); |
||||
|
formApi.setValues({ |
||||
|
...claimVto, |
||||
|
newClaimValue: claimVto.claimValue, |
||||
|
}); |
||||
|
// 新增可选, 修改不可选 |
||||
|
formApi.updateSchema([ |
||||
|
{ |
||||
|
disabled: !!claimVto.id, |
||||
|
fieldName: 'claimType', |
||||
|
}, |
||||
|
]); |
||||
|
// 新增时初始化可选声明 |
||||
|
!claimVto.id && (await initAssignableClaims()); |
||||
|
} finally { |
||||
|
modalApi.setState({ loading: false }); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
title: $t('AbpIdentity.ManageClaim'), |
||||
|
}); |
||||
|
/** 初始化可用声明类型 */ |
||||
|
async function initAssignableClaims() { |
||||
|
const { items } = await getAssignableClaimsApi(); |
||||
|
formApi.updateSchema([ |
||||
|
{ |
||||
|
componentProps: { |
||||
|
fieldNames: { |
||||
|
label: 'name', |
||||
|
value: 'name', |
||||
|
}, |
||||
|
options: items, |
||||
|
}, |
||||
|
fieldName: 'claimType', |
||||
|
}, |
||||
|
]); |
||||
|
} |
||||
|
/** 提交声明类型变更 */ |
||||
|
async function onSubmit(values: Record<string, any>) { |
||||
|
try { |
||||
|
modalApi.setState({ confirmLoading: true }); |
||||
|
const claimVto = modalApi.getData<IdentityUserClaimVto>(); |
||||
|
const api = claimVto.id |
||||
|
? updateClaimApi(claimVto.userId, { |
||||
|
claimType: claimVto.claimType, |
||||
|
claimValue: claimVto.claimValue, |
||||
|
newClaimValue: values.claimValue, |
||||
|
}) |
||||
|
: createClaimApi(claimVto.userId, { |
||||
|
claimType: values.claimType, |
||||
|
claimValue: values.claimValue, |
||||
|
}); |
||||
|
await api; |
||||
|
emits('change', values as IdentityUserClaimDto); |
||||
|
modalApi.close(); |
||||
|
} finally { |
||||
|
modalApi.setState({ confirmLoading: false }); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal> |
||||
|
<Form /> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,163 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { VxeGridProps } from '@abp/ui'; |
||||
|
|
||||
|
import type { IdentityUserClaimDto, IdentityUserDto } from '../../types'; |
||||
|
|
||||
|
import { defineAsyncComponent, h } from 'vue'; |
||||
|
|
||||
|
import { useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { useVbenVxeGrid } from '@abp/ui'; |
||||
|
import { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue'; |
||||
|
import { Button, Popconfirm } from 'ant-design-vue'; |
||||
|
|
||||
|
import { deleteClaimApi, getClaimsApi } from '../../api/users'; |
||||
|
import { IdentityUserPermissions } from '../../constants/permissions'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'UserClaimModal', |
||||
|
}); |
||||
|
|
||||
|
const ClaimEditModal = defineAsyncComponent( |
||||
|
() => import('./UserClaimEditModal.vue'), |
||||
|
); |
||||
|
|
||||
|
const [Modal, modalApi] = useVbenModal({ |
||||
|
draggable: true, |
||||
|
fullscreenButton: false, |
||||
|
onCancel() { |
||||
|
modalApi.close(); |
||||
|
}, |
||||
|
onConfirm: async () => {}, |
||||
|
showCancelButton: false, |
||||
|
showConfirmButton: false, |
||||
|
title: $t('AbpIdentity.ManageClaim'), |
||||
|
}); |
||||
|
const gridOptions: VxeGridProps<IdentityUserClaimDto> = { |
||||
|
columns: [ |
||||
|
{ |
||||
|
field: 'claimType', |
||||
|
minWidth: 120, |
||||
|
title: $t('AbpIdentity.DisplayName:ClaimType'), |
||||
|
}, |
||||
|
{ |
||||
|
field: 'claimValue', |
||||
|
title: $t('AbpIdentity.DisplayName:ClaimValue'), |
||||
|
width: 'auto', |
||||
|
}, |
||||
|
{ |
||||
|
field: 'action', |
||||
|
fixed: 'right', |
||||
|
slots: { default: 'action' }, |
||||
|
title: $t('AbpUi.Actions'), |
||||
|
width: 180, |
||||
|
}, |
||||
|
], |
||||
|
exportConfig: {}, |
||||
|
keepSource: true, |
||||
|
pagerConfig: { |
||||
|
enabled: false, |
||||
|
}, |
||||
|
proxyConfig: { |
||||
|
ajax: { |
||||
|
query: async () => { |
||||
|
const { id } = modalApi.getData<IdentityUserDto>(); |
||||
|
return await getClaimsApi(id); |
||||
|
}, |
||||
|
}, |
||||
|
response: { |
||||
|
total: 'totalCount', |
||||
|
list: 'items', |
||||
|
}, |
||||
|
}, |
||||
|
toolbarConfig: { |
||||
|
custom: true, |
||||
|
export: true, |
||||
|
// import: true, |
||||
|
refresh: true, |
||||
|
zoom: true, |
||||
|
}, |
||||
|
}; |
||||
|
const [Grid, { query }] = useVbenVxeGrid({ |
||||
|
gridOptions, |
||||
|
}); |
||||
|
|
||||
|
const [UserClaimEditModal, editModalApi] = useVbenModal({ |
||||
|
connectedComponent: ClaimEditModal, |
||||
|
}); |
||||
|
function onCreate() { |
||||
|
const { id } = modalApi.getData<IdentityUserDto>(); |
||||
|
editModalApi.setData({ |
||||
|
userId: id, |
||||
|
}); |
||||
|
editModalApi.open(); |
||||
|
} |
||||
|
|
||||
|
async function onDelete(row: IdentityUserClaimDto) { |
||||
|
const { id } = modalApi.getData<IdentityUserDto>(); |
||||
|
await deleteClaimApi(id, { |
||||
|
claimType: row.claimType, |
||||
|
claimValue: row.claimValue, |
||||
|
}).then(() => query()); |
||||
|
} |
||||
|
|
||||
|
function onUpdate(row: IdentityUserClaimDto) { |
||||
|
const { id } = modalApi.getData<IdentityUserDto>(); |
||||
|
editModalApi.setData({ |
||||
|
userId: id, |
||||
|
...row, |
||||
|
}); |
||||
|
editModalApi.open(); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal> |
||||
|
<Grid> |
||||
|
<template #toolbar-tools> |
||||
|
<Button |
||||
|
type="primary" |
||||
|
v-access:code="[IdentityUserPermissions.ManageClaims]" |
||||
|
@click="onCreate" |
||||
|
> |
||||
|
{{ $t('AbpIdentity.AddClaim') }} |
||||
|
</Button> |
||||
|
</template> |
||||
|
<template #action="{ row }"> |
||||
|
<div class="flex flex-row"> |
||||
|
<div class="basis-1/2"> |
||||
|
<Button |
||||
|
:icon="h(EditOutlined)" |
||||
|
block |
||||
|
type="link" |
||||
|
v-access:code="[IdentityUserPermissions.ManageClaims]" |
||||
|
@click="onUpdate(row)" |
||||
|
> |
||||
|
{{ $t('AbpUi.Edit') }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
<div class="basis-1/2"> |
||||
|
<Popconfirm |
||||
|
:title="$t('AbpIdentity.WillDeleteClaim', [row.claimType])" |
||||
|
@confirm="onDelete(row)" |
||||
|
> |
||||
|
<Button |
||||
|
:icon="h(DeleteOutlined)" |
||||
|
block |
||||
|
danger |
||||
|
type="link" |
||||
|
v-access:code="[IdentityUserPermissions.Delete]" |
||||
|
> |
||||
|
{{ $t('AbpUi.Delete') }} |
||||
|
</Button> |
||||
|
</Popconfirm> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
</Grid> |
||||
|
<UserClaimEditModal @change="() => query()" /> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,117 @@ |
|||||
|
<!-- eslint-disable no-unused-vars --> |
||||
|
<script setup lang="ts"> |
||||
|
import { useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { useVbenForm } from '@abp/ui'; |
||||
|
|
||||
|
import { lockApi } from '../../api/users'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'UserLockModal', |
||||
|
}); |
||||
|
const emits = defineEmits<{ |
||||
|
(event: 'change'): void; |
||||
|
}>(); |
||||
|
|
||||
|
enum LockType { |
||||
|
Days = 86_400, |
||||
|
Hours = 3600, |
||||
|
Minutes = 60, |
||||
|
Months = 2_678_400, // 按31天计算 |
||||
|
Seconds = 1, |
||||
|
Years = 32_140_800, // 按31*12天计算 |
||||
|
} |
||||
|
|
||||
|
const [Form, formApi] = useVbenForm({ |
||||
|
commonConfig: { |
||||
|
// 所有表单项 |
||||
|
componentProps: { |
||||
|
class: 'w-full', |
||||
|
}, |
||||
|
}, |
||||
|
handleSubmit: onSubmit, |
||||
|
schema: [ |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
componentProps: { |
||||
|
style: { |
||||
|
display: 'none', |
||||
|
}, |
||||
|
}, |
||||
|
fieldName: 'userId', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'InputNumber', |
||||
|
fieldName: 'seconds', |
||||
|
label: $t('AbpIdentity.LockTime'), |
||||
|
rules: 'required', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Select', |
||||
|
componentProps: { |
||||
|
options: [ |
||||
|
// TODO: 本地化 |
||||
|
{ label: $t('LockType:Seconds'), value: LockType.Seconds }, |
||||
|
{ label: $t('LockType:Minutes'), value: LockType.Minutes }, |
||||
|
{ label: $t('LockType:Hours'), value: LockType.Hours }, |
||||
|
{ label: $t('LockType:Days'), value: LockType.Days }, |
||||
|
{ label: $t('LockType:Months'), value: LockType.Months }, |
||||
|
{ label: $t('LockType:Years'), value: LockType.Years }, |
||||
|
], |
||||
|
}, |
||||
|
defaultValue: LockType.Seconds, |
||||
|
fieldName: 'type', |
||||
|
label: $t('AbpIdentity.LockType'), |
||||
|
rules: 'required', |
||||
|
}, |
||||
|
], |
||||
|
showDefaultActions: false, |
||||
|
}); |
||||
|
|
||||
|
const [Modal, modalApi] = useVbenModal({ |
||||
|
closeOnClickModal: false, |
||||
|
closeOnPressEscape: false, |
||||
|
draggable: true, |
||||
|
fullscreenButton: false, |
||||
|
onCancel() { |
||||
|
modalApi.close(); |
||||
|
}, |
||||
|
onConfirm: async () => { |
||||
|
await formApi.validateAndSubmitForm(); |
||||
|
}, |
||||
|
async onOpenChange(isOpen: boolean) { |
||||
|
if (isOpen) { |
||||
|
const { id } = modalApi.getData<Record<string, any>>(); |
||||
|
formApi.setValues({ |
||||
|
userId: id, |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
title: $t('AbpIdentity.Lock'), |
||||
|
}); |
||||
|
|
||||
|
async function onSubmit(input: Record<string, any>) { |
||||
|
try { |
||||
|
modalApi.setState({ |
||||
|
confirmLoading: true, |
||||
|
}); |
||||
|
const lockSeconds = input.type * input.seconds; |
||||
|
await lockApi(input.userId, lockSeconds); |
||||
|
emits('change'); |
||||
|
modalApi.close(); |
||||
|
} finally { |
||||
|
modalApi.setState({ |
||||
|
confirmLoading: false, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal> |
||||
|
<Form /> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -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