Browse Source

feat(users): 增加用户角色组织机构管理

pull/1051/head
colin 1 year ago
parent
commit
c5cb3479d4
  1. 1
      apps/vben5/apps/app-antd/package.json
  2. 3
      apps/vben5/apps/app-antd/src/adapter/component/index.ts
  3. 4
      apps/vben5/apps/app-antd/vite.config.mts
  4. 5
      apps/vben5/packages/@abp/core/package.json
  5. 1
      apps/vben5/packages/@abp/core/src/constants/index.ts
  6. 46
      apps/vben5/packages/@abp/core/src/constants/validation.ts
  7. 3
      apps/vben5/packages/@abp/core/src/hooks/index.ts
  8. 70
      apps/vben5/packages/@abp/core/src/hooks/useLocalization.ts
  9. 64
      apps/vben5/packages/@abp/core/src/hooks/useSettings.ts
  10. 405
      apps/vben5/packages/@abp/core/src/hooks/useValidation.ts
  11. 2
      apps/vben5/packages/@abp/core/src/index.ts
  12. 97
      apps/vben5/packages/@abp/core/src/store/abp.ts
  13. 4
      apps/vben5/packages/@abp/core/src/types/index.ts
  14. 10
      apps/vben5/packages/@abp/core/src/types/localization.ts
  15. 50
      apps/vben5/packages/@abp/core/src/types/rules.ts
  16. 31
      apps/vben5/packages/@abp/core/src/types/settings.ts
  17. 112
      apps/vben5/packages/@abp/core/src/types/validations.ts
  18. 2
      apps/vben5/packages/@abp/core/src/utils/index.ts
  19. 47
      apps/vben5/packages/@abp/core/src/utils/regex.ts
  20. 37
      apps/vben5/packages/@abp/core/src/utils/string.ts
  21. 2
      apps/vben5/packages/@abp/identity/src/api/roles.ts
  22. 56
      apps/vben5/packages/@abp/identity/src/api/users.ts
  23. 4
      apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitRoleTable.vue
  24. 4
      apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitUserTable.vue
  25. 185
      apps/vben5/packages/@abp/identity/src/components/users/UserModal.vue
  26. 91
      apps/vben5/packages/@abp/identity/src/components/users/UserPasswordModal.vue
  27. 20
      apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue
  28. 2
      apps/vben5/packages/@abp/identity/src/hooks/index.ts
  29. 80
      apps/vben5/packages/@abp/identity/src/hooks/usePasswordValidator.ts
  30. 77
      apps/vben5/packages/@abp/identity/src/hooks/useRandomPassword.ts
  31. 10
      apps/vben5/packages/@abp/identity/src/types/users.ts
  32. 7
      apps/vben5/packages/@abp/permission/src/components/permissions/PermissionModal.vue
  33. 3
      apps/vben5/packages/@abp/ui/src/adapter/component/index.ts
  34. 2
      apps/vben5/pnpm-workspace.yaml

1
apps/vben5/apps/app-antd/package.json

@ -30,6 +30,7 @@
"@abp/core": "workspace:*",
"@abp/identity": "workspace:*",
"@abp/request": "workspace:*",
"@abp/ui": "workspace:*",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",

3
apps/vben5/apps/app-antd/src/adapter/component/index.ts

@ -21,6 +21,7 @@ import {
Input,
InputNumber,
InputPassword,
InputSearch,
Mentions,
notification,
Radio,
@ -57,6 +58,7 @@ export type ComponentType =
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'InputSearch'
| 'Mentions'
| 'PrimaryButton'
| 'Radio'
@ -90,6 +92,7 @@ async function initComponentAdapter() {
Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
InputSearch,
Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {

4
apps/vben5/apps/app-antd/vite.config.mts

@ -10,14 +10,14 @@ export default defineConfig(async () => {
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ''),
// mock代理目标地址
target: 'http://81.68.64.105:30001/',
target: 'http://127.0.0.1:30001/',
ws: true,
},
'/connect': {
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ''),
// mock代理目标地址
target: 'http://81.68.64.105:30001/',
target: 'http://127.0.0.1:30001/',
ws: true,
},
},

5
apps/vben5/packages/@abp/core/package.json

@ -36,7 +36,12 @@
},
"dependencies": {
"dayjs": "catalog:",
"lodash": "catalog:",
"pinia": "catalog:",
"pinia-plugin-persistedstate": "catalog:",
"vue": "catalog:"
},
"devDependencies": {
"@types/lodash": "catalog:"
}
}

1
apps/vben5/packages/@abp/core/src/constants/index.ts

@ -0,0 +1 @@
export * from './validation';

46
apps/vben5/packages/@abp/core/src/constants/validation.ts

@ -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}',
};

3
apps/vben5/packages/@abp/core/src/hooks/index.ts

@ -0,0 +1,3 @@
export * from './useLocalization';
export * from './useSettings';
export * from './useValidation';

70
apps/vben5/packages/@abp/core/src/hooks/useLocalization.ts

@ -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 };
}

64
apps/vben5/packages/@abp/core/src/hooks/useSettings.ts

@ -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;
}

405
apps/vben5/packages/@abp/core/src/hooks/useValidation.ts

@ -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,
};
}

2
apps/vben5/packages/@abp/core/src/index.ts

@ -1,3 +1,5 @@
export * from './constants';
export * from './hooks';
export * from './store';
export * from './types';
export * from './utils';

97
apps/vben5/packages/@abp/core/src/store/abp.ts

@ -7,54 +7,63 @@ import { ref } from 'vue';
import { acceptHMRUpdate, defineStore } from 'pinia';
export const useAbpStore = defineStore('abp', () => {
const application = ref<ApplicationConfigurationDto>();
const localization = ref<ApplicationLocalizationDto>();
/** 获取 i18n 格式本地化文本 */
function getI18nLocales() {
const abpLocales: Record<string, any> = {};
if (!localization.value) {
export const useAbpStore = defineStore(
'abp',
() => {
const application = ref<ApplicationConfigurationDto>();
const localization = ref<ApplicationLocalizationDto>();
/** 获取 i18n 格式本地化文本 */
function getI18nLocales() {
const abpLocales: Record<string, any> = {};
if (!localization.value) {
return abpLocales;
}
const resources = localization.value.resources;
// AbpValidation.The field {0} is invalid.
Object.keys(resources).forEach((resource) => {
// resource --> AbpValidation
const resourceLocales: Record<string, any> = {};
const resourcesByName = resources[resource];
if (resourcesByName) {
Object.keys(resourcesByName.texts).forEach((key) => {
// The field {0} is invalid. --> The field {0} is invalid_
let localeKey = key.replaceAll('.', '_');
// The field {0} is invalid. --> The field {0} is invalid
localeKey.endsWith('_') &&
(localeKey = localeKey.slice(
0,
Math.max(0, localeKey.length - 1),
));
// _The field {0} is invalid --> The field {0} is invalid
localeKey.startsWith('_') &&
(localeKey = localeKey.slice(0, Math.max(1, localeKey.length)));
resourceLocales[localeKey] = resourcesByName.texts[key];
});
abpLocales[resource] = resourceLocales;
}
});
return abpLocales;
}
const resources = localization.value.resources;
// AbpValidation.The field {0} is invalid.
Object.keys(resources).forEach((resource) => {
// resource --> AbpValidation
const resourceLocales: Record<string, any> = {};
const resourcesByName = resources[resource];
if (resourcesByName) {
Object.keys(resourcesByName.texts).forEach((key) => {
// The field {0} is invalid. --> The field {0} is invalid_
let localeKey = key.replaceAll('.', '_');
// The field {0} is invalid. --> The field {0} is invalid
localeKey.endsWith('_') &&
(localeKey = localeKey.slice(0, Math.max(0, localeKey.length - 1)));
// _The field {0} is invalid --> The field {0} is invalid
localeKey.startsWith('_') &&
(localeKey = localeKey.slice(0, Math.max(1, localeKey.length)));
resourceLocales[localeKey] = resourcesByName.texts[key];
});
abpLocales[resource] = resourceLocales;
}
});
return abpLocales;
}
function setApplication(val: ApplicationConfigurationDto) {
application.value = val;
}
function setApplication(val: ApplicationConfigurationDto) {
application.value = val;
}
function setLocalization(val: ApplicationLocalizationDto) {
localization.value = val;
}
function setLocalization(val: ApplicationLocalizationDto) {
localization.value = val;
}
return {
application,
getI18nLocales,
localization,
setApplication,
setLocalization,
};
});
return {
application,
getI18nLocales,
localization,
setApplication,
setLocalization,
};
},
{
persist: true,
},
);
// 解决热更新问题
const hot = import.meta.hot;

4
apps/vben5/packages/@abp/core/src/types/index.ts

@ -1,2 +1,6 @@
export * from './dto';
export * from './global';
export * from './localization';
export * from './rules';
export * from './settings';
export * from './validations';

10
apps/vben5/packages/@abp/core/src/types/localization.ts

@ -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 };

50
apps/vben5/packages/@abp/core/src/types/rules.ts

@ -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 };

31
apps/vben5/packages/@abp/core/src/types/settings.ts

@ -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 };

112
apps/vben5/packages/@abp/core/src/types/validations.ts

@ -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,
};

2
apps/vben5/packages/@abp/core/src/utils/index.ts

@ -1,2 +1,4 @@
export * from './date';
export * from './regex';
export * from './string';
export * from './tree';

47
apps/vben5/packages/@abp/core/src/utils/regex.ts

@ -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);
}

37
apps/vben5/packages/@abp/core/src/utils/string.ts

@ -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('');
}

2
apps/vben5/packages/@abp/identity/src/api/roles.ts

@ -70,7 +70,7 @@ export function getPagedListApi(
* @param id id
* @param ouId id
*/
export function removeOrganizationUnit(
export function removeOrganizationUnitApi(
id: string,
ouId: string,
): Promise<void> {

56
apps/vben5/packages/@abp/identity/src/api/users.ts

@ -1,6 +1,8 @@
import type { PagedResultDto } from '@abp/core';
import type { ListResultDto, PagedResultDto } from '@abp/core';
import type { IdentityRoleDto, OrganizationUnitDto } from '../types';
import type {
ChangeUserPasswordInput,
GetUserPagedListInput,
IdentityUserCreateDto,
IdentityUserDto,
@ -70,7 +72,7 @@ export function getPagedListApi(
* @param id id
* @param ouId id
*/
export function removeOrganizationUnit(
export function removeOrganizationUnitApi(
id: string,
ouId: string,
): Promise<void> {
@ -79,6 +81,18 @@ export function removeOrganizationUnit(
);
}
/**
*
* @param id id
*/
export function getOrganizationUnitsApi(
id: string,
): Promise<ListResultDto<OrganizationUnitDto>> {
return requestClient.get<ListResultDto<OrganizationUnitDto>>(
`/api/identity/users/${id}/organization-units`,
);
}
/**
*
* @param id id
@ -95,3 +109,41 @@ export function lockApi(id: string, seconds: number): Promise<void> {
export function unLockApi(id: string): Promise<void> {
return requestClient.put(`/api/identity/users/${id}/unlock`);
}
/**
*
* @param id id
* @param input dto
*/
export function changePasswordApi(
id: string,
input: ChangeUserPasswordInput,
): Promise<void> {
return requestClient.put(
`/api/identity/users/change-password?id=${id}`,
input,
);
}
/**
*
*/
export function getAssignableRolesApi(): Promise<
ListResultDto<IdentityRoleDto>
> {
return requestClient.get<ListResultDto<IdentityRoleDto>>(
`/api/identity/users/assignable-roles`,
);
}
/**
*
* @param id id
*/
export function getRolesApi(
id: string,
): Promise<ListResultDto<IdentityRoleDto>> {
return requestClient.get<ListResultDto<IdentityRoleDto>>(
`/api/identity/users/${id}/roles`,
);
}

4
apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitRoleTable.vue

@ -16,7 +16,7 @@ import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
import { Button, Modal } from 'ant-design-vue';
import { addRoles, getRoleListApi } from '../../api/organization-units';
import { removeOrganizationUnit } from '../../api/roles';
import { removeOrganizationUnitApi } from '../../api/roles';
import { OrganizationUnitPermissions } from '../../constants/permissions';
defineOptions({
@ -111,7 +111,7 @@ const onDelete = (row: IdentityRoleDto) => {
]),
onOk: () => {
setLoading(true);
return removeOrganizationUnit(row.id, props.selectedKey!)
return removeOrganizationUnitApi(row.id, props.selectedKey!)
.then(onRefresh)
.finally(() => setLoading(false));
},

4
apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitUserTable.vue

@ -14,7 +14,7 @@ import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
import { Button, Modal } from 'ant-design-vue';
import { addMembers, getUserListApi } from '../../api/organization-units';
import { removeOrganizationUnit } from '../../api/users';
import { removeOrganizationUnitApi } from '../../api/users';
import { OrganizationUnitPermissions } from '../../constants/permissions';
defineOptions({
@ -113,7 +113,7 @@ const onDelete = (row: IdentityUserDto) => {
]),
onOk: () => {
setLoading(true);
return removeOrganizationUnit(row.id, props.selectedKey!)
return removeOrganizationUnitApi(row.id, props.selectedKey!)
.then(onRefresh)
.finally(() => setLoading(false));
},

185
apps/vben5/packages/@abp/identity/src/components/users/UserModal.vue

@ -1,7 +1,8 @@
<script setup lang="ts">
import type { FormInstance } from 'ant-design-vue';
import type { TransferItem } from 'ant-design-vue/es/transfer';
import type { DataNode, EventDataNode } from 'ant-design-vue/es/tree';
import type { IdentityRoleDto } from '../../types/roles';
import type { IdentityUserDto } from '../../types/users';
import { defineEmits, defineOptions, ref, toValue } from 'vue';
@ -10,6 +11,7 @@ import { useAccess } from '@vben/access';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useSettings } from '@abp/core';
import {
Checkbox,
Form,
@ -18,9 +20,18 @@ import {
message,
Tabs,
Transfer,
Tree,
} from 'ant-design-vue';
import { createApi, getApi, updateApi } from '../../api/users';
import { getChildrenApi, getRootListApi } from '../../api/organization-units';
import {
createApi,
getApi,
getAssignableRolesApi,
getOrganizationUnitsApi,
getRolesApi,
updateApi,
} from '../../api/users';
defineOptions({
name: 'UserModal',
@ -36,12 +47,20 @@ const defaultModel = {
isActive: true,
} as IdentityUserDto;
const assignableRoles = ref<IdentityRoleDto[]>([]);
const activedTab = ref('info');
const form = ref<FormInstance>();
/** 可分配的角色列表 */
const assignedRoles = ref<TransferItem[]>([]);
/** 组织机构 */
const organizationUnits = ref<DataNode[]>([]);
/** 已加载的组织机构Keys */
const loadedOuKeys = ref<string[]>([]);
/** 用户拥有的组织机构节点keys */
const checkedOuKeys = ref<string[]>([]);
/** 表单数据 */
const formModel = ref<IdentityUserDto>({ ...defaultModel });
const { isTrue } = useSettings();
const { hasAccessByCodes } = useAccess();
const [Modal, modalApi] = useVbenModal({
draggable: true,
@ -54,41 +73,124 @@ const [Modal, modalApi] = useVbenModal({
const api = formModel.value.id
? updateApi(formModel.value.id, toValue(formModel))
: createApi(toValue(formModel));
modalApi.setState({ loading: true });
modalApi.setState({ confirmLoading: true });
api
.then((res) => {
message.success($t('AbpUi.Success'));
message.success($t('AbpUi.SavedSuccessfully'));
emits('change', res);
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false });
modalApi.setState({ confirmLoading: false });
});
},
onOpenChange: async (isOpen: boolean) => {
if (isOpen) {
const { values } = modalApi.getData<Record<string, any>>();
if (values?.id) {
modalApi.setState({ loading: true });
return getApi(values.id)
.then((dto) => {
formModel.value = dto;
modalApi.setState({
title: `${$t('AbpIdentity.Users')} - ${dto.userName}`,
});
})
.finally(() => {
modalApi.setState({ loading: false });
});
}
loadedOuKeys.value = [];
assignedRoles.value = [];
activedTab.value = 'info';
organizationUnits.value = [];
formModel.value = { ...defaultModel };
modalApi.setState({
loading: true,
title: $t('NewUser'),
});
try {
const { values } = modalApi.getData<Record<string, any>>();
const manageRolePolicy = checkManageRolePolicy();
if (values?.id) {
await initUserInfo(values.id);
manageRolePolicy && (await initUserRoles(values.id));
checkManageOuPolicy() && (await initOrganizationUnitTree(values.id));
}
manageRolePolicy && (await initAssignableRoles());
} finally {
modalApi.setState({
loading: false,
});
}
}
},
title: $t('AbpIdentity.Users'),
});
/** 检查管理角色权限 */
function checkManageRolePolicy() {
return hasAccessByCodes(['AbpIdentity.Users.Update.ManageRoles']);
}
/** 检查管理组织机构权限 */
function checkManageOuPolicy() {
return hasAccessByCodes(['AbpIdentity.Users.ManageOrganizationUnits']);
}
/**
* 初始化用户信息
* @param userId 用户id
*/
async function initUserInfo(userId: string) {
const dto = await getApi(userId);
formModel.value = dto;
modalApi.setState({
title: `${$t('AbpIdentity.Users')} - ${dto.userName}`,
});
}
/**
* 初始化用户角色
* @param userId 用户id
*/
async function initUserRoles(userId: string) {
const { items } = await getRolesApi(userId);
formModel.value.roleNames = items.map((item) => item.name);
}
/** 初始化可用角色列表 */
async function initAssignableRoles() {
const { items } = await getAssignableRolesApi();
assignedRoles.value = items.map((item) => {
return {
key: item.name,
title: item.name,
...item,
};
});
}
/**
* 初始化组织机构树
* @param userId 用户id
*/
async function initOrganizationUnitTree(userId: string) {
const ouResult = await getRootListApi();
organizationUnits.value = ouResult.items.map((item) => {
return {
isLeaf: false,
key: item.id,
title: item.displayName,
children: [],
};
});
const userOuResult = await getOrganizationUnitsApi(userId);
checkedOuKeys.value = userOuResult.items.map((item) => item.id);
}
/** 加载组织机构树节点 */
async function onLoadOuChildren(node: EventDataNode) {
const nodeKey = String(node.key);
const { items } = await getChildrenApi({ id: nodeKey });
node.dataRef!.isLeaf = items.length === 0;
node.dataRef!.children = items.map((item): DataNode => {
return {
isLeaf: false,
key: item.id,
title: item.displayName,
children: [],
};
});
organizationUnits.value = [...organizationUnits.value];
loadedOuKeys.value.push(nodeKey);
}
</script>
<template>
@ -100,6 +202,7 @@ const [Modal, modalApi] = useVbenModal({
:wrapper-col="{ span: 18 }"
>
<Tabs v-model:active-key="activedTab">
<!-- 基本信息 -->
<TabPane key="info" :tab="$t('AbpIdentity.UserInformations')">
<FormItem :label="$t('AbpIdentity.DisplayName:IsActive')">
<Checkbox v-model:checked="formModel.isActive">
@ -111,7 +214,10 @@ const [Modal, modalApi] = useVbenModal({
name="userName"
required
>
<Input v-model:value="formModel.userName" />
<Input
v-model:value="formModel.userName"
:disabled="!isTrue('Abp.Identity.User.IsUserNameUpdateEnabled')"
/>
</FormItem>
<FormItem
v-if="!formModel.id"
@ -135,7 +241,10 @@ const [Modal, modalApi] = useVbenModal({
name="email"
required
>
<Input v-model:value="formModel.email" />
<Input
v-model:value="formModel.email"
:disabled="!isTrue('Abp.Identity.User.IsEmailUpdateEnabled')"
/>
</FormItem>
<FormItem
:label="$t('AbpIdentity.DisplayName:PhoneNumber')"
@ -152,19 +261,41 @@ const [Modal, modalApi] = useVbenModal({
</Checkbox>
</FormItem>
</TabPane>
<TabPane key="role" :tab="$t('Roles')">
<!-- 角色 -->
<TabPane
v-if="checkManageRolePolicy()"
key="role"
:tab="$t('AbpIdentity.Roles')"
>
<Transfer
:data-source="assignableRoles"
:disabled="hasAccessByCodes(['AbpIdentity.Users.ManageRoles'])"
v-model:target-keys="formModel.roleNames"
:data-source="assignedRoles"
:list-style="{
width: '47%',
height: '338px',
}"
:target-keys="formModel.roleNames"
:render="(item) => item.title"
:titles="[$t('AbpIdentity.Assigned'), $t('AbpIdentity.Available')]"
class="tree-transfer"
/>
</TabPane>
<!-- 组织机构 -->
<TabPane
v-if="formModel.id && checkManageOuPolicy()"
key="ou"
:tab="$t('AbpIdentity.OrganizationUnits')"
>
<Tree
:checked-keys="checkedOuKeys"
:load-data="onLoadOuChildren"
:loaded-keys="loadedOuKeys"
:tree-data="organizationUnits"
block-node
check-strictly
checkable
disabled
/>
</TabPane>
</Tabs>
</Form>
</Modal>

91
apps/vben5/packages/@abp/identity/src/components/users/UserPasswordModal.vue

@ -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>

20
apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue

@ -32,10 +32,14 @@ defineOptions({
const UserModal = defineAsyncComponent(() => import('./UserModal.vue'));
const LockModal = defineAsyncComponent(() => import('./UserLockModal.vue'));
const PasswordModal = defineAsyncComponent(
() => import('./UserPasswordModal.vue'),
);
const MenuItem = Menu.Item;
const CheckIcon = createIconifyIcon('ant-design:check-outlined');
const CloseIcon = createIconifyIcon('ant-design:close-outlined');
const PasswordIcon = createIconifyIcon('carbon:password');
const MenuOutlined = createIconifyIcon('heroicons-outline:menu-alt-3');
const ClaimOutlined = createIconifyIcon('la:id-card-solid');
const PermissionsOutlined = createIconifyIcon('icon-park-outline:permissions');
@ -144,6 +148,9 @@ const [UserEditModal, userModalApi] = useVbenModal({
const [UserLockModal, lockModalApi] = useVbenModal({
connectedComponent: LockModal,
});
const [UserPasswordModal, pwdModalApi] = useVbenModal({
connectedComponent: PasswordModal,
});
const [UserPermissionModal, permissionModalApi] = useVbenModal({
connectedComponent: PermissionModal,
});
@ -188,6 +195,11 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
lockModalApi.open();
break;
}
case 'password': {
pwdModalApi.setData(row);
pwdModalApi.open();
break;
}
case 'permissions': {
const userId = abpStore.application?.currentUser.id;
permissionModalApi.setData({
@ -297,6 +309,13 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
>
{{ $t('AbpIdentity.ManageClaim') }}
</MenuItem>
<MenuItem
v-if="hasAccessByCodes([IdentityUserPermissions.Update])"
key="password"
:icon="h(PasswordIcon)"
>
{{ $t('AbpIdentity.SetPassword') }}
</MenuItem>
<MenuItem
v-if="hasAccessByCodes(['Platform.Menu.ManageUsers'])"
key="menus"
@ -314,6 +333,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => {
</Grid>
<UserLockModal @change="query" />
<UserEditModal @change="() => query()" />
<UserPasswordModal @change="query" />
<UserPermissionModal />
</template>

2
apps/vben5/packages/@abp/identity/src/hooks/index.ts

@ -0,0 +1,2 @@
export * from './usePasswordValidator';
export * from './useRandomPassword';

80
apps/vben5/packages/@abp/identity/src/hooks/usePasswordValidator.ts

@ -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,
};
}

77
apps/vben5/packages/@abp/identity/src/hooks/useRandomPassword.ts

@ -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 };
}

10
apps/vben5/packages/@abp/identity/src/types/users.ts

@ -25,13 +25,18 @@ interface IUser {
}
/** 更改密码数据传输对象 */
interface ChangePasswordInput {
interface ChangeMyPasswordInput {
/** 当前密码 */
currentPassword?: string;
/** 新密码 */
newPassword: string;
}
interface ChangeUserPasswordInput {
/** 新密码 */
password: string;
}
/** 用户组织机构数据传输对象 */
interface IdentityUserOrganizationUnitUpdateDto {
/** 组织机构标识列表 */
@ -86,7 +91,8 @@ type IdentityUserCreateDto = IdentityUserCreateOrUpdateDto;
type IdentityUserUpdateDto = IdentityUserCreateOrUpdateDto;
export type {
ChangePasswordInput,
ChangeMyPasswordInput,
ChangeUserPasswordInput,
GetUserPagedListInput,
IdentityUserCreateDto,
IdentityUserDto,

7
apps/vben5/packages/@abp/permission/src/components/permissions/PermissionModal.vue

@ -276,7 +276,7 @@ function getChildren(permissions: PermissionTree[]): PermissionTree[] {
<Checkbox
:disabled="modelState?.readonly"
v-bind="getPermissionNodeState(permission)"
@change="(e) => onCheckNodeAll(e, permission)"
@change="(e: any) => onCheckNodeAll(e, permission)"
>
{{ $t('AbpPermissionManagement.SelectAllInThisTab') }}
</Checkbox>
@ -293,7 +293,10 @@ function getChildren(permissions: PermissionTree[]): PermissionTree[] {
children: 'children',
}"
:tree-data="permission.children"
@check="(keys, info) => onCheckNode(permission, keys, info)"
@check="
(keys: any, info: CheckInfo) =>
onCheckNode(permission, keys, info)
"
@expand="onExpandNode"
@select="onSelectNode"
/>

3
apps/vben5/packages/@abp/ui/src/adapter/component/index.ts

@ -21,6 +21,7 @@ import {
Input,
InputNumber,
InputPassword,
InputSearch,
Mentions,
notification,
Radio,
@ -57,6 +58,7 @@ export type ComponentType =
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'InputSearch'
| 'Mentions'
| 'PrimaryButton'
| 'Radio'
@ -90,6 +92,7 @@ async function initComponentAdapter() {
Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
InputSearch,
Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {

2
apps/vben5/pnpm-workspace.yaml

@ -44,6 +44,7 @@ catalog:
'@types/eslint': ^9.6.1
'@types/html-minifier-terser': ^7.0.2
'@types/jsonwebtoken': ^9.0.7
'@types/lodash': ^4.17.13
'@types/lodash.clonedeep': ^4.5.9
'@types/node': ^22.10.0
'@types/nprogress': ^0.2.3
@ -113,6 +114,7 @@ catalog:
jsonc-eslint-parser: ^2.4.0
jsonwebtoken: ^9.0.2
lint-staged: ^15.2.10
lodash: ^4.17.13
lodash.clonedeep: ^4.5.0
lucide-vue-next: ^0.461.0
medium-zoom: ^1.1.0

Loading…
Cancel
Save