From a6aef1f101c9a5b7342e04860d94077e785d14d1 Mon Sep 17 00:00:00 2001 From: colin Date: Sat, 7 Dec 2024 08:53:07 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(users):=20=E5=A2=9E=E5=8A=A0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E9=94=81=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../packages/@abp/identity/src/api/users.ts | 17 +++ .../src/components/users/UserLockModal.vue | 117 ++++++++++++++++++ .../src/components/users/UserTable.vue | 65 +++++++++- 3 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 apps/vben5/packages/@abp/identity/src/components/users/UserLockModal.vue diff --git a/apps/vben5/packages/@abp/identity/src/api/users.ts b/apps/vben5/packages/@abp/identity/src/api/users.ts index 42745cd90..dd51b9028 100644 --- a/apps/vben5/packages/@abp/identity/src/api/users.ts +++ b/apps/vben5/packages/@abp/identity/src/api/users.ts @@ -78,3 +78,20 @@ export function removeOrganizationUnit( `/api/identity/users/${id}/organization-units/${ouId}`, ); } + +/** + * 锁定用户 + * @param id 用户id + * @param seconds 锁定时长(秒) + */ +export function lockApi(id: string, seconds: number): Promise { + return requestClient.put(`/api/identity/users/${id}/lock/${seconds}`); +} + +/** + * 解锁用户 + * @param id 用户id + */ +export function unLockApi(id: string): Promise { + return requestClient.put(`/api/identity/users/${id}/unlock`); +} diff --git a/apps/vben5/packages/@abp/identity/src/components/users/UserLockModal.vue b/apps/vben5/packages/@abp/identity/src/components/users/UserLockModal.vue new file mode 100644 index 000000000..235a7c03b --- /dev/null +++ b/apps/vben5/packages/@abp/identity/src/components/users/UserLockModal.vue @@ -0,0 +1,117 @@ + + + + + + diff --git a/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue b/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue index 55939d133..a3eb444c6 100644 --- a/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue +++ b/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue @@ -4,7 +4,7 @@ import type { MenuInfo } from 'ant-design-vue/es/menu/src/interface'; import type { IdentityUserDto } from '../../types/users'; -import { defineAsyncComponent, h } from 'vue'; +import { computed, defineAsyncComponent, h } from 'vue'; import { useAccess } from '@vben/access'; import { useVbenModal } from '@vben/common-ui'; @@ -18,10 +18,12 @@ import { DeleteOutlined, EditOutlined, EllipsisOutlined, + LockOutlined, + UnlockOutlined, } from '@ant-design/icons-vue'; import { Button, Dropdown, Menu, Modal } from 'ant-design-vue'; -import { deleteApi, getPagedListApi } from '../../api/users'; +import { deleteApi, getPagedListApi, unLockApi } from '../../api/users'; import { IdentityUserPermissions } from '../../constants/permissions'; defineOptions({ @@ -29,6 +31,7 @@ defineOptions({ }); const UserModal = defineAsyncComponent(() => import('./UserModal.vue')); +const LockModal = defineAsyncComponent(() => import('./UserLockModal.vue')); const MenuItem = Menu.Item; const CheckIcon = createIconifyIcon('ant-design:check-outlined'); @@ -36,8 +39,19 @@ const CloseIcon = createIconifyIcon('ant-design:close-outlined'); const MenuOutlined = createIconifyIcon('heroicons-outline:menu-alt-3'); const ClaimOutlined = createIconifyIcon('la:id-card-solid'); const PermissionsOutlined = createIconifyIcon('icon-park-outline:permissions'); -const [UserPermissionModal, permissionModalApi] = useVbenModal({ - connectedComponent: PermissionModal, + +const getLockEnd = computed(() => { + return (row: IdentityUserDto) => { + if (row.lockoutEnd) { + const lockTime = new Date(row.lockoutEnd); + if (lockTime) { + // 锁定时间高于当前时间不显示 + const nowTime = new Date(); + return lockTime < nowTime; + } + } + return true; + }; }); const abpStore = useAbpStore(); @@ -127,6 +141,12 @@ const gridEvents: VxeGridListeners = { const [UserEditModal, userModalApi] = useVbenModal({ connectedComponent: UserModal, }); +const [UserLockModal, lockModalApi] = useVbenModal({ + connectedComponent: LockModal, +}); +const [UserPermissionModal, permissionModalApi] = useVbenModal({ + connectedComponent: PermissionModal, +}); const [Grid, { query }] = useVbenVxeGrid({ formOptions, gridEvents, @@ -156,8 +176,18 @@ const handleDelete = (row: IdentityUserDto) => { }); }; +const handleUnlock = async (row: IdentityUserDto) => { + await unLockApi(row.id); + await query(); +}; + const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => { switch (info.key) { + case 'lock': { + lockModalApi.setData(row); + lockModalApi.open(); + break; + } case 'permissions': { const userId = abpStore.application?.currentUser.id; permissionModalApi.setData({ @@ -169,6 +199,10 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => { permissionModalApi.open(); break; } + case 'unlock': { + handleUnlock(row); + break; + } } }; @@ -221,6 +255,28 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => { From c5cb3479d446cbfc388fb881c803026159334a36 Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 19 Dec 2024 14:33:22 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(users):=20=E5=A2=9E=E5=8A=A0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=A7=92=E8=89=B2=E7=BB=84=E7=BB=87=E6=9C=BA=E6=9E=84?= =?UTF-8?q?=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/vben5/apps/app-antd/package.json | 1 + .../app-antd/src/adapter/component/index.ts | 3 + apps/vben5/apps/app-antd/vite.config.mts | 4 +- apps/vben5/packages/@abp/core/package.json | 5 + .../packages/@abp/core/src/constants/index.ts | 1 + .../@abp/core/src/constants/validation.ts | 46 ++ .../packages/@abp/core/src/hooks/index.ts | 3 + .../@abp/core/src/hooks/useLocalization.ts | 70 +++ .../@abp/core/src/hooks/useSettings.ts | 64 +++ .../@abp/core/src/hooks/useValidation.ts | 405 ++++++++++++++++++ apps/vben5/packages/@abp/core/src/index.ts | 2 + .../vben5/packages/@abp/core/src/store/abp.ts | 97 +++-- .../packages/@abp/core/src/types/index.ts | 4 + .../@abp/core/src/types/localization.ts | 10 + .../packages/@abp/core/src/types/rules.ts | 50 +++ .../packages/@abp/core/src/types/settings.ts | 31 ++ .../@abp/core/src/types/validations.ts | 112 +++++ .../packages/@abp/core/src/utils/index.ts | 2 + .../packages/@abp/core/src/utils/regex.ts | 47 ++ .../packages/@abp/core/src/utils/string.ts | 37 ++ .../packages/@abp/identity/src/api/roles.ts | 2 +- .../packages/@abp/identity/src/api/users.ts | 56 ++- .../OrganizationUnitRoleTable.vue | 4 +- .../OrganizationUnitUserTable.vue | 4 +- .../src/components/users/UserModal.vue | 185 ++++++-- .../components/users/UserPasswordModal.vue | 91 ++++ .../src/components/users/UserTable.vue | 20 + .../packages/@abp/identity/src/hooks/index.ts | 2 + .../src/hooks/usePasswordValidator.ts | 80 ++++ .../identity/src/hooks/useRandomPassword.ts | 77 ++++ .../packages/@abp/identity/src/types/users.ts | 10 +- .../permissions/PermissionModal.vue | 7 +- .../@abp/ui/src/adapter/component/index.ts | 3 + apps/vben5/pnpm-workspace.yaml | 2 + 34 files changed, 1453 insertions(+), 84 deletions(-) create mode 100644 apps/vben5/packages/@abp/core/src/constants/index.ts create mode 100644 apps/vben5/packages/@abp/core/src/constants/validation.ts create mode 100644 apps/vben5/packages/@abp/core/src/hooks/index.ts create mode 100644 apps/vben5/packages/@abp/core/src/hooks/useLocalization.ts create mode 100644 apps/vben5/packages/@abp/core/src/hooks/useSettings.ts create mode 100644 apps/vben5/packages/@abp/core/src/hooks/useValidation.ts create mode 100644 apps/vben5/packages/@abp/core/src/types/localization.ts create mode 100644 apps/vben5/packages/@abp/core/src/types/rules.ts create mode 100644 apps/vben5/packages/@abp/core/src/types/settings.ts create mode 100644 apps/vben5/packages/@abp/core/src/types/validations.ts create mode 100644 apps/vben5/packages/@abp/core/src/utils/regex.ts create mode 100644 apps/vben5/packages/@abp/core/src/utils/string.ts create mode 100644 apps/vben5/packages/@abp/identity/src/components/users/UserPasswordModal.vue create mode 100644 apps/vben5/packages/@abp/identity/src/hooks/index.ts create mode 100644 apps/vben5/packages/@abp/identity/src/hooks/usePasswordValidator.ts create mode 100644 apps/vben5/packages/@abp/identity/src/hooks/useRandomPassword.ts diff --git a/apps/vben5/apps/app-antd/package.json b/apps/vben5/apps/app-antd/package.json index a4bc65408..f12c4b965 100644 --- a/apps/vben5/apps/app-antd/package.json +++ b/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:*", diff --git a/apps/vben5/apps/app-antd/src/adapter/component/index.ts b/apps/vben5/apps/app-antd/src/adapter/component/index.ts index 1afa62174..e237b52fc 100644 --- a/apps/vben5/apps/app-antd/src/adapter/component/index.ts +++ b/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 }) => { diff --git a/apps/vben5/apps/app-antd/vite.config.mts b/apps/vben5/apps/app-antd/vite.config.mts index cc4b2bef1..4ccb102a9 100644 --- a/apps/vben5/apps/app-antd/vite.config.mts +++ b/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, }, }, diff --git a/apps/vben5/packages/@abp/core/package.json b/apps/vben5/packages/@abp/core/package.json index 7ffa6d8e4..8beec67f0 100644 --- a/apps/vben5/packages/@abp/core/package.json +++ b/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:" } } diff --git a/apps/vben5/packages/@abp/core/src/constants/index.ts b/apps/vben5/packages/@abp/core/src/constants/index.ts new file mode 100644 index 000000000..4d5ffa36a --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/constants/index.ts @@ -0,0 +1 @@ +export * from './validation'; diff --git a/apps/vben5/packages/@abp/core/src/constants/validation.ts b/apps/vben5/packages/@abp/core/src/constants/validation.ts new file mode 100644 index 000000000..27680e687 --- /dev/null +++ b/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}', +}; diff --git a/apps/vben5/packages/@abp/core/src/hooks/index.ts b/apps/vben5/packages/@abp/core/src/hooks/index.ts new file mode 100644 index 000000000..1582c45c5 --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/hooks/index.ts @@ -0,0 +1,3 @@ +export * from './useLocalization'; +export * from './useSettings'; +export * from './useValidation'; diff --git a/apps/vben5/packages/@abp/core/src/hooks/useLocalization.ts b/apps/vben5/packages/@abp/core/src/hooks/useLocalization.ts new file mode 100644 index 000000000..4593a11e5 --- /dev/null +++ b/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 => { + if (!abpStore.application) { + return {}; + } + const { values } = abpStore.application.localization; + return values[resource] ?? {}; + }; + }); + + function L(key: string, args?: any[] | Record | 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 | 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 }; +} diff --git a/apps/vben5/packages/@abp/core/src/hooks/useSettings.ts b/apps/vben5/packages/@abp/core/src/hooks/useSettings.ts new file mode 100644 index 000000000..a573d3c23 --- /dev/null +++ b/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(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; +} diff --git a/apps/vben5/packages/@abp/core/src/hooks/useValidation.ts b/apps/vben5/packages/@abp/core/src/hooks/useValidation.ts new file mode 100644 index 000000000..8a04e63e7 --- /dev/null +++ b/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; + }): 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, + }; +} diff --git a/apps/vben5/packages/@abp/core/src/index.ts b/apps/vben5/packages/@abp/core/src/index.ts index 48ebf8d9a..067745944 100644 --- a/apps/vben5/packages/@abp/core/src/index.ts +++ b/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'; diff --git a/apps/vben5/packages/@abp/core/src/store/abp.ts b/apps/vben5/packages/@abp/core/src/store/abp.ts index 4134845b2..8d035181d 100644 --- a/apps/vben5/packages/@abp/core/src/store/abp.ts +++ b/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(); - const localization = ref(); - /** 获取 i18n 格式本地化文本 */ - function getI18nLocales() { - const abpLocales: Record = {}; - if (!localization.value) { +export const useAbpStore = defineStore( + 'abp', + () => { + const application = ref(); + const localization = ref(); + /** 获取 i18n 格式本地化文本 */ + function getI18nLocales() { + const abpLocales: Record = {}; + 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 = {}; + 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 = {}; - 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; diff --git a/apps/vben5/packages/@abp/core/src/types/index.ts b/apps/vben5/packages/@abp/core/src/types/index.ts index 56615902d..97fa56fd8 100644 --- a/apps/vben5/packages/@abp/core/src/types/index.ts +++ b/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'; diff --git a/apps/vben5/packages/@abp/core/src/types/localization.ts b/apps/vben5/packages/@abp/core/src/types/localization.ts new file mode 100644 index 000000000..19bebb7d9 --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/types/localization.ts @@ -0,0 +1,10 @@ +interface StringLocalizer { + L(key: string, args?: any[] | Record | undefined): string; + Lr( + resource: string, + key: string, + args?: any[] | Record | undefined, + ): string; +} + +export type { StringLocalizer }; diff --git a/apps/vben5/packages/@abp/core/src/types/rules.ts b/apps/vben5/packages/@abp/core/src/types/rules.ts new file mode 100644 index 000000000..aef6ed573 --- /dev/null +++ b/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 }; diff --git a/apps/vben5/packages/@abp/core/src/types/settings.ts b/apps/vben5/packages/@abp/core/src/types/settings.ts new file mode 100644 index 000000000..c00bdfc97 --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/types/settings.ts @@ -0,0 +1,31 @@ +import type { NameValue } from './global'; + +type SettingValue = NameValue; +/** + * 设置接口 + */ +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 }; diff --git a/apps/vben5/packages/@abp/core/src/types/validations.ts b/apps/vben5/packages/@abp/core/src/types/validations.ts new file mode 100644 index 000000000..4f4a10657 --- /dev/null +++ b/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; +} + +export type { + Field, + FieldBeetWeen, + FieldContains, + FieldDefineValidator, + FieldLength, + FieldMatch, + FieldRange, + FieldRegular, + FieldRequired, + FieldValidator, + Rule, + RuleType, +}; diff --git a/apps/vben5/packages/@abp/core/src/utils/index.ts b/apps/vben5/packages/@abp/core/src/utils/index.ts index 61cf91f96..202185507 100644 --- a/apps/vben5/packages/@abp/core/src/utils/index.ts +++ b/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'; diff --git a/apps/vben5/packages/@abp/core/src/utils/regex.ts b/apps/vben5/packages/@abp/core/src/utils/regex.ts new file mode 100644 index 000000000..fb047e06d --- /dev/null +++ b/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); +} diff --git a/apps/vben5/packages/@abp/core/src/utils/string.ts b/apps/vben5/packages/@abp/core/src/utils/string.ts new file mode 100644 index 000000000..cca6eb5aa --- /dev/null +++ b/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(''); +} diff --git a/apps/vben5/packages/@abp/identity/src/api/roles.ts b/apps/vben5/packages/@abp/identity/src/api/roles.ts index 8292bd7ae..06da49391 100644 --- a/apps/vben5/packages/@abp/identity/src/api/roles.ts +++ b/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 { diff --git a/apps/vben5/packages/@abp/identity/src/api/users.ts b/apps/vben5/packages/@abp/identity/src/api/users.ts index dd51b9028..c4cea8535 100644 --- a/apps/vben5/packages/@abp/identity/src/api/users.ts +++ b/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 { @@ -79,6 +81,18 @@ export function removeOrganizationUnit( ); } +/** + * 获取用户组织机构列表 + * @param id 用户id + */ +export function getOrganizationUnitsApi( + id: string, +): Promise> { + return requestClient.get>( + `/api/identity/users/${id}/organization-units`, + ); +} + /** * 锁定用户 * @param id 用户id @@ -95,3 +109,41 @@ export function lockApi(id: string, seconds: number): Promise { export function unLockApi(id: string): Promise { return requestClient.put(`/api/identity/users/${id}/unlock`); } + +/** + * 更改用户密码 + * @param id 用户id + * @param input 密码变更dto + */ +export function changePasswordApi( + id: string, + input: ChangeUserPasswordInput, +): Promise { + return requestClient.put( + `/api/identity/users/change-password?id=${id}`, + input, + ); +} + +/** + * 获取可用的角色列表 + */ +export function getAssignableRolesApi(): Promise< + ListResultDto +> { + return requestClient.get>( + `/api/identity/users/assignable-roles`, + ); +} + +/** + * 获取用户角色列表 + * @param id 用户id + */ +export function getRolesApi( + id: string, +): Promise> { + return requestClient.get>( + `/api/identity/users/${id}/roles`, + ); +} diff --git a/apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitRoleTable.vue b/apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitRoleTable.vue index 426a754a9..3f50eb0bc 100644 --- a/apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitRoleTable.vue +++ b/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)); }, diff --git a/apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitUserTable.vue b/apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitUserTable.vue index 275a8498a..250105026 100644 --- a/apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitUserTable.vue +++ b/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)); }, diff --git a/apps/vben5/packages/@abp/identity/src/components/users/UserModal.vue b/apps/vben5/packages/@abp/identity/src/components/users/UserModal.vue index 3029b621a..b57aab912 100644 --- a/apps/vben5/packages/@abp/identity/src/components/users/UserModal.vue +++ b/apps/vben5/packages/@abp/identity/src/components/users/UserModal.vue @@ -1,7 +1,8 @@ diff --git a/apps/vben5/packages/@abp/identity/src/hooks/index.ts b/apps/vben5/packages/@abp/identity/src/hooks/index.ts new file mode 100644 index 000000000..cf20b7cbc --- /dev/null +++ b/apps/vben5/packages/@abp/identity/src/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './usePasswordValidator'; +export * from './useRandomPassword'; diff --git a/apps/vben5/packages/@abp/identity/src/hooks/usePasswordValidator.ts b/apps/vben5/packages/@abp/identity/src/hooks/usePasswordValidator.ts new file mode 100644 index 000000000..93cf39b1f --- /dev/null +++ b/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 { + 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, + }; +} diff --git a/apps/vben5/packages/@abp/identity/src/hooks/useRandomPassword.ts b/apps/vben5/packages/@abp/identity/src/hooks/useRandomPassword.ts new file mode 100644 index 000000000..4d0a21e5c --- /dev/null +++ b/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 }; +} diff --git a/apps/vben5/packages/@abp/identity/src/types/users.ts b/apps/vben5/packages/@abp/identity/src/types/users.ts index 0b5cd0bf0..9c718cd88 100644 --- a/apps/vben5/packages/@abp/identity/src/types/users.ts +++ b/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, diff --git a/apps/vben5/packages/@abp/permission/src/components/permissions/PermissionModal.vue b/apps/vben5/packages/@abp/permission/src/components/permissions/PermissionModal.vue index 7fe29479b..fea114430 100644 --- a/apps/vben5/packages/@abp/permission/src/components/permissions/PermissionModal.vue +++ b/apps/vben5/packages/@abp/permission/src/components/permissions/PermissionModal.vue @@ -276,7 +276,7 @@ function getChildren(permissions: PermissionTree[]): PermissionTree[] { {{ $t('AbpPermissionManagement.SelectAllInThisTab') }} @@ -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" /> diff --git a/apps/vben5/packages/@abp/ui/src/adapter/component/index.ts b/apps/vben5/packages/@abp/ui/src/adapter/component/index.ts index 1afa62174..e237b52fc 100644 --- a/apps/vben5/packages/@abp/ui/src/adapter/component/index.ts +++ b/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 }) => { diff --git a/apps/vben5/pnpm-workspace.yaml b/apps/vben5/pnpm-workspace.yaml index cfbfa7f21..f0564f794 100644 --- a/apps/vben5/pnpm-workspace.yaml +++ b/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 From e6ccd8b629598f9fa87331549fdd01c4f58bc346 Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 19 Dec 2024 16:38:03 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(users):=20=E5=A2=9E=E5=8A=A0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=A3=B0=E6=98=8E=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@abp/identity/src/api/claim-types.ts | 13 +- .../packages/@abp/identity/src/api/users.ts | 54 ++++++ .../components/claim-types/ClaimTypeModal.vue | 27 ++- .../components/claim-types/ClaimTypeTable.vue | 4 +- .../organization-units/SelectMemberModal.vue | 8 +- .../src/components/roles/RoleModal.vue | 29 ++-- .../src/components/roles/RoleTable.vue | 4 +- .../components/users/UserClaimEditModal.vue | 119 +++++++++++++ .../src/components/users/UserClaimModal.vue | 159 ++++++++++++++++++ .../src/components/users/UserModal.vue | 20 ++- .../src/components/users/UserTable.vue | 16 +- .../packages/@abp/identity/src/types/users.ts | 17 ++ .../permissions/PermissionModal.vue | 2 + 13 files changed, 421 insertions(+), 51 deletions(-) create mode 100644 apps/vben5/packages/@abp/identity/src/components/users/UserClaimEditModal.vue create mode 100644 apps/vben5/packages/@abp/identity/src/components/users/UserClaimModal.vue diff --git a/apps/vben5/packages/@abp/identity/src/api/claim-types.ts b/apps/vben5/packages/@abp/identity/src/api/claim-types.ts index dbbc8d603..b42816c5c 100644 --- a/apps/vben5/packages/@abp/identity/src/api/claim-types.ts +++ b/apps/vben5/packages/@abp/identity/src/api/claim-types.ts @@ -1,4 +1,4 @@ -import type { PagedResultDto } from '@abp/core'; +import type { ListResultDto, PagedResultDto } from '@abp/core'; import type { GetIdentityClaimTypePagedListInput, @@ -72,3 +72,14 @@ export function getPagedListApi( }, ); } + +/** + * 获取可用的声明类型列表 + */ +export function getAssignableClaimsApi(): Promise< + ListResultDto +> { + return requestClient.get>( + `/api/identity/claim-types/actived-list`, + ); +} diff --git a/apps/vben5/packages/@abp/identity/src/api/users.ts b/apps/vben5/packages/@abp/identity/src/api/users.ts index c4cea8535..f73e7a2dc 100644 --- a/apps/vben5/packages/@abp/identity/src/api/users.ts +++ b/apps/vben5/packages/@abp/identity/src/api/users.ts @@ -4,6 +4,10 @@ import type { IdentityRoleDto, OrganizationUnitDto } from '../types'; import type { ChangeUserPasswordInput, GetUserPagedListInput, + IdentityUserClaimCreateDto, + IdentityUserClaimDeleteDto, + IdentityUserClaimDto, + IdentityUserClaimUpdateDto, IdentityUserCreateDto, IdentityUserDto, IdentityUserUpdateDto, @@ -147,3 +151,53 @@ export function getRolesApi( `/api/identity/users/${id}/roles`, ); } + +/** + * 获取用户声明列表 + * @param id 用户id + */ +export function getClaimsApi( + id: string, +): Promise> { + return requestClient.get>( + `/api/identity/users/${id}/claims`, + ); +} + +/** + * 删除用户声明 + * @param id 用户id + * @param input 用户声明dto + */ +export function deleteClaimApi( + id: string, + input: IdentityUserClaimDeleteDto, +): Promise { + return requestClient.delete(`/api/identity/users/${id}/claims`, { + params: input, + }); +} + +/** + * 创建用户声明 + * @param id 用户id + * @param input 用户声明dto + */ +export function createClaimApi( + id: string, + input: IdentityUserClaimCreateDto, +): Promise { + return requestClient.post(`/api/identity/users/${id}/claims`, input); +} + +/** + * 更新用户声明 + * @param id 用户id + * @param input 用户声明dto + */ +export function updateClaimApi( + id: string, + input: IdentityUserClaimUpdateDto, +): Promise { + return requestClient.put(`/api/identity/users/${id}/claims`, input); +} diff --git a/apps/vben5/packages/@abp/identity/src/components/claim-types/ClaimTypeModal.vue b/apps/vben5/packages/@abp/identity/src/components/claim-types/ClaimTypeModal.vue index 911a35e38..754e08fe7 100644 --- a/apps/vben5/packages/@abp/identity/src/components/claim-types/ClaimTypeModal.vue +++ b/apps/vben5/packages/@abp/identity/src/components/claim-types/ClaimTypeModal.vue @@ -79,24 +79,23 @@ const [Modal, modalApi] = useVbenModal({ }, onOpenChange: async (isOpen: boolean) => { if (isOpen) { - const { values } = modalApi.getData>(); - if (values?.id) { - modalApi.setState({ loading: true }); - return getApi(values.id) - .then((dto) => { - formModel.value = dto; - modalApi.setState({ - title: `${$t('AbpIdentity.DisplayName:ClaimType')} - ${dto.name}`, - }); - }) - .finally(() => { - modalApi.setState({ loading: false }); - }); - } formModel.value = { ...defaultModel }; modalApi.setState({ title: $t('AbpIdentity.IdentityClaim:New'), }); + const claimTypeDto = modalApi.getData(); + if (claimTypeDto?.id) { + modalApi.setState({ loading: true }); + try { + const dto = await getApi(claimTypeDto.id); + formModel.value = dto; + modalApi.setState({ + title: `${$t('AbpIdentity.DisplayName:ClaimType')} - ${dto.name}`, + }); + } finally { + modalApi.setState({ loading: false }); + } + } } }, title: 'ClaimType', diff --git a/apps/vben5/packages/@abp/identity/src/components/claim-types/ClaimTypeTable.vue b/apps/vben5/packages/@abp/identity/src/components/claim-types/ClaimTypeTable.vue index 97d2d3689..2f088c700 100644 --- a/apps/vben5/packages/@abp/identity/src/components/claim-types/ClaimTypeTable.vue +++ b/apps/vben5/packages/@abp/identity/src/components/claim-types/ClaimTypeTable.vue @@ -157,9 +157,7 @@ const handleAdd = () => { }; const handleEdit = (row: IdentityClaimTypeDto) => { - roleModalApi.setData({ - values: row, - }); + roleModalApi.setData(row); roleModalApi.open(); }; diff --git a/apps/vben5/packages/@abp/identity/src/components/organization-units/SelectMemberModal.vue b/apps/vben5/packages/@abp/identity/src/components/organization-units/SelectMemberModal.vue index e01ee5d52..7980d2edf 100644 --- a/apps/vben5/packages/@abp/identity/src/components/organization-units/SelectMemberModal.vue +++ b/apps/vben5/packages/@abp/identity/src/components/organization-units/SelectMemberModal.vue @@ -1,4 +1,6 @@ + + + + diff --git a/apps/vben5/packages/@abp/identity/src/components/users/UserClaimModal.vue b/apps/vben5/packages/@abp/identity/src/components/users/UserClaimModal.vue new file mode 100644 index 000000000..c9223b9db --- /dev/null +++ b/apps/vben5/packages/@abp/identity/src/components/users/UserClaimModal.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/apps/vben5/packages/@abp/identity/src/components/users/UserModal.vue b/apps/vben5/packages/@abp/identity/src/components/users/UserModal.vue index b57aab912..e0b7b3023 100644 --- a/apps/vben5/packages/@abp/identity/src/components/users/UserModal.vue +++ b/apps/vben5/packages/@abp/identity/src/components/users/UserModal.vue @@ -93,15 +93,18 @@ const [Modal, modalApi] = useVbenModal({ formModel.value = { ...defaultModel }; modalApi.setState({ loading: true, - title: $t('NewUser'), + title: $t('AbpIdentity.NewUser'), }); try { - const { values } = modalApi.getData>(); + const userDto = modalApi.getData(); const manageRolePolicy = checkManageRolePolicy(); - if (values?.id) { - await initUserInfo(values.id); - manageRolePolicy && (await initUserRoles(values.id)); - checkManageOuPolicy() && (await initOrganizationUnitTree(values.id)); + if (userDto?.id) { + await initUserInfo(userDto.id); + manageRolePolicy && (await initUserRoles(userDto.id)); + checkManageOuPolicy() && (await initOrganizationUnitTree(userDto.id)); + modalApi.setState({ + title: `${$t('AbpIdentity.Users')} - ${userDto.userName}`, + }); } manageRolePolicy && (await initAssignableRoles()); } finally { @@ -275,7 +278,10 @@ async function onLoadOuChildren(node: EventDataNode) { height: '338px', }" :render="(item) => item.title" - :titles="[$t('AbpIdentity.Assigned'), $t('AbpIdentity.Available')]" + :titles="[ + $t('AbpIdentityServer.Assigned'), + $t('AbpIdentityServer.Available'), + ]" class="tree-transfer" /> diff --git a/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue b/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue index 8a60c6f46..758e31ebd 100644 --- a/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue +++ b/apps/vben5/packages/@abp/identity/src/components/users/UserTable.vue @@ -19,6 +19,7 @@ import { EditOutlined, EllipsisOutlined, LockOutlined, + PlusOutlined, UnlockOutlined, } from '@ant-design/icons-vue'; import { Button, Dropdown, Menu, Modal } from 'ant-design-vue'; @@ -32,6 +33,7 @@ defineOptions({ const UserModal = defineAsyncComponent(() => import('./UserModal.vue')); const LockModal = defineAsyncComponent(() => import('./UserLockModal.vue')); +const ClaimModal = defineAsyncComponent(() => import('./UserClaimModal.vue')); const PasswordModal = defineAsyncComponent( () => import('./UserPasswordModal.vue'), ); @@ -151,6 +153,9 @@ const [UserLockModal, lockModalApi] = useVbenModal({ const [UserPasswordModal, pwdModalApi] = useVbenModal({ connectedComponent: PasswordModal, }); +const [UserClaimModal, claimModalApi] = useVbenModal({ + connectedComponent: ClaimModal, +}); const [UserPermissionModal, permissionModalApi] = useVbenModal({ connectedComponent: PermissionModal, }); @@ -166,9 +171,7 @@ const handleAdd = () => { }; const handleEdit = (row: IdentityUserDto) => { - userModalApi.setData({ - values: row, - }); + userModalApi.setData(row); userModalApi.open(); }; @@ -190,6 +193,11 @@ const handleUnlock = async (row: IdentityUserDto) => { const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => { switch (info.key) { + case 'claims': { + claimModalApi.setData(row); + claimModalApi.open(); + break; + } case 'lock': { lockModalApi.setData(row); lockModalApi.open(); @@ -223,6 +231,7 @@ const handleMenuClick = async (row: IdentityUserDto, info: MenuInfo) => { + diff --git a/apps/vben5/packages/@abp/identity/src/types/users.ts b/apps/vben5/packages/@abp/identity/src/types/users.ts index 9c718cd88..075564265 100644 --- a/apps/vben5/packages/@abp/identity/src/types/users.ts +++ b/apps/vben5/packages/@abp/identity/src/types/users.ts @@ -6,6 +6,8 @@ import type { PagedAndSortedResultRequestDto, } from '@abp/core'; +import type { IdentityClaimDto } from './claim-types'; + /** 用户对象接口 */ interface IUser { /** 邮件地址 */ @@ -90,10 +92,25 @@ interface GetUserPagedListInput extends PagedAndSortedResultRequestDto { type IdentityUserCreateDto = IdentityUserCreateOrUpdateDto; type IdentityUserUpdateDto = IdentityUserCreateOrUpdateDto; +interface IdentityUserClaimDto extends IdentityClaimDto { + id: string; +} + +interface IdentityUserClaimUpdateDto extends IdentityClaimDto { + newClaimValue: string; +} + +type IdentityUserClaimDeleteDto = IdentityClaimDto; +type IdentityUserClaimCreateDto = IdentityClaimDto; + export type { ChangeMyPasswordInput, ChangeUserPasswordInput, GetUserPagedListInput, + IdentityUserClaimCreateDto, + IdentityUserClaimDeleteDto, + IdentityUserClaimDto, + IdentityUserClaimUpdateDto, IdentityUserCreateDto, IdentityUserDto, IdentityUserOrganizationUnitUpdateDto, diff --git a/apps/vben5/packages/@abp/permission/src/components/permissions/PermissionModal.vue b/apps/vben5/packages/@abp/permission/src/components/permissions/PermissionModal.vue index fea114430..62cf75f3f 100644 --- a/apps/vben5/packages/@abp/permission/src/components/permissions/PermissionModal.vue +++ b/apps/vben5/packages/@abp/permission/src/components/permissions/PermissionModal.vue @@ -115,6 +115,7 @@ const [Modal, modalApi] = useVbenModal({ const state = modalApi.getData(); modelState.value = state; modalApi.setState({ + confirmLoading: true, loading: true, }); try { @@ -129,6 +130,7 @@ const [Modal, modalApi] = useVbenModal({ checkedNodeKeys.value = getGrantedPermissionKeys(permissionTree.value); } finally { modalApi.setState({ + confirmLoading: false, loading: false, }); } From c984666dbc55796c88f0be246355ee59e466332a Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 19 Dec 2024 16:42:41 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat(users):=20=E5=A2=9E=E5=8A=A0=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E5=90=8D=E7=A7=B0=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@abp/identity/src/components/users/UserClaimEditModal.vue | 4 ++++ .../@abp/identity/src/components/users/UserClaimModal.vue | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/apps/vben5/packages/@abp/identity/src/components/users/UserClaimEditModal.vue b/apps/vben5/packages/@abp/identity/src/components/users/UserClaimEditModal.vue index 160904116..94ce61037 100644 --- a/apps/vben5/packages/@abp/identity/src/components/users/UserClaimEditModal.vue +++ b/apps/vben5/packages/@abp/identity/src/components/users/UserClaimEditModal.vue @@ -11,6 +11,10 @@ interface IdentityUserClaimVto extends IdentityUserClaimDto { userId: string; } +defineOptions({ + name: 'UserClaimEditModal', +}); + const emits = defineEmits<{ (event: 'change', data: IdentityUserClaimDto): void; }>(); diff --git a/apps/vben5/packages/@abp/identity/src/components/users/UserClaimModal.vue b/apps/vben5/packages/@abp/identity/src/components/users/UserClaimModal.vue index c9223b9db..e29a306ad 100644 --- a/apps/vben5/packages/@abp/identity/src/components/users/UserClaimModal.vue +++ b/apps/vben5/packages/@abp/identity/src/components/users/UserClaimModal.vue @@ -15,6 +15,10 @@ 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'), );