diff --git a/apps/vben5/apps/app-antd/package.json b/apps/vben5/apps/app-antd/package.json index be1116864..adfaf25a1 100644 --- a/apps/vben5/apps/app-antd/package.json +++ b/apps/vben5/apps/app-antd/package.json @@ -29,6 +29,7 @@ "@abp/account": "workspace:*", "@abp/auditing": "workspace:*", "@abp/core": "workspace:*", + "@abp/features": "workspace:*", "@abp/identity": "workspace:*", "@abp/notifications": "workspace:*", "@abp/openiddict": "workspace:*", diff --git a/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json b/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json index af1be1952..b5b1400f0 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json @@ -30,6 +30,11 @@ "groups": "Groups", "definitions": "Definitions" }, + "features": { + "title": "Features", + "groups": "Groups", + "definitions": "Definitions" + }, "settings": { "title": "Settings", "definitions": "Definitions", diff --git a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json index 1c4595dbb..6cc746f9c 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json @@ -30,6 +30,11 @@ "groups": "权限分组", "definitions": "权限定义" }, + "features": { + "title": "功能管理", + "groups": "功能分组", + "definitions": "功能定义" + }, "settings": { "title": "设置管理", "definitions": "设置定义", diff --git a/apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts b/apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts index f737035c3..b0c6ea5fc 100644 --- a/apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts +++ b/apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts @@ -117,6 +117,35 @@ const routes: RouteRecordRaw[] = [ }, ], }, + { + meta: { + title: $t('abp.manage.features.title'), + icon: 'ant-design:gold-outlined', + }, + name: 'FeatureManagement', + path: '/manage/features', + children: [ + { + meta: { + title: $t('abp.manage.features.groups'), + icon: 'lucide:group', + }, + name: 'FeatureGroupDefinitions', + path: '/manage/features/groups', + component: () => import('#/views/features/groups/index.vue'), + }, + { + meta: { + title: $t('abp.manage.features.definitions'), + icon: 'pajamas:feature-flag', + }, + name: 'FeatureDefinitions', + path: '/manage/features/definitions', + component: () => + import('#/views/features/definitions/index.vue'), + }, + ], + }, { meta: { title: $t('abp.manage.settings.title'), diff --git a/apps/vben5/apps/app-antd/src/views/features/definitions/index.vue b/apps/vben5/apps/app-antd/src/views/features/definitions/index.vue new file mode 100644 index 000000000..637b08c87 --- /dev/null +++ b/apps/vben5/apps/app-antd/src/views/features/definitions/index.vue @@ -0,0 +1,15 @@ + + + + + + + diff --git a/apps/vben5/apps/app-antd/src/views/features/groups/index.vue b/apps/vben5/apps/app-antd/src/views/features/groups/index.vue new file mode 100644 index 000000000..caa12d1e8 --- /dev/null +++ b/apps/vben5/apps/app-antd/src/views/features/groups/index.vue @@ -0,0 +1,15 @@ + + + + + + + diff --git a/apps/vben5/packages/@abp/core/src/hooks/useValidation.ts b/apps/vben5/packages/@abp/core/src/hooks/useValidation.ts index afb24569e..4e18afba0 100644 --- a/apps/vben5/packages/@abp/core/src/hooks/useValidation.ts +++ b/apps/vben5/packages/@abp/core/src/hooks/useValidation.ts @@ -129,7 +129,7 @@ export function useValidation(): RuleCreator { required?: boolean, ): Rule { const message = field.name - ? L(useNameEnum, [_getFieldName(field), field.minimum, field.maximum]) + ? L(useNameEnum, [_getFieldName(field), field.maximum, field.minimum]) : L(notNameEnum, [field.minimum, field.maximum]); return { message, @@ -397,6 +397,12 @@ export function useValidation(): RuleCreator { ), ]; }, + mapEnumValidMessage( + enumName: string, + args?: any[] | Record | undefined, + ) { + return L(enumName, args); + }, }; return ruleCreator; diff --git a/apps/vben5/packages/@abp/core/src/types/rules.ts b/apps/vben5/packages/@abp/core/src/types/rules.ts index aef6ed573..3737cc5fb 100644 --- a/apps/vben5/packages/@abp/core/src/types/rules.ts +++ b/apps/vben5/packages/@abp/core/src/types/rules.ts @@ -45,6 +45,11 @@ interface RuleCreator { fieldOnlyAcceptsFilesExtensions(field: FieldContains): Rule[]; /** 字段{0}不可为空 */ fieldRequired(field: Field): Rule[]; + /** 获取一个错误枚举验证消息 */ + mapEnumValidMessage( + enumName: string, + args?: any[] | Record | undefined, + ): string; } export type { RuleCreator }; diff --git a/apps/vben5/packages/@abp/core/src/utils/index.ts b/apps/vben5/packages/@abp/core/src/utils/index.ts index 2109114a1..1219f2544 100644 --- a/apps/vben5/packages/@abp/core/src/utils/index.ts +++ b/apps/vben5/packages/@abp/core/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './date'; +export * from './is'; export * from './mitt'; export * from './regex'; export * from './string'; diff --git a/apps/vben5/packages/@abp/features/package.json b/apps/vben5/packages/@abp/features/package.json index 67751a202..c44657431 100644 --- a/apps/vben5/packages/@abp/features/package.json +++ b/apps/vben5/packages/@abp/features/package.json @@ -20,7 +20,6 @@ } }, "dependencies": { - "@abp/auditing": "workspace:*", "@abp/core": "workspace:*", "@abp/request": "workspace:*", "@abp/ui": "workspace:*", @@ -30,6 +29,7 @@ "@vben/hooks": "workspace:*", "@vben/icons": "workspace:*", "@vben/locales": "workspace:*", + "@vben/utils": "workspace:*", "ant-design-vue": "catalog:", "dayjs": "catalog:", "vue": "catalog:*", diff --git a/apps/vben5/packages/@abp/features/src/api/index.ts b/apps/vben5/packages/@abp/features/src/api/index.ts index 0d11e3ec4..4287d4768 100644 --- a/apps/vben5/packages/@abp/features/src/api/index.ts +++ b/apps/vben5/packages/@abp/features/src/api/index.ts @@ -1 +1,3 @@ +export { useFeatureDefinitionsApi } from './useFeatureDefinitionsApi'; +export { useFeatureGroupDefinitionsApi } from './useFeatureGroupDefinitionsApi'; export { useFeaturesApi } from './useFeaturesApi'; diff --git a/apps/vben5/packages/@abp/features/src/api/useFeatureDefinitionsApi.ts b/apps/vben5/packages/@abp/features/src/api/useFeatureDefinitionsApi.ts new file mode 100644 index 000000000..165af3aff --- /dev/null +++ b/apps/vben5/packages/@abp/features/src/api/useFeatureDefinitionsApi.ts @@ -0,0 +1,100 @@ +import type { ListResultDto } from '@abp/core'; + +import type { + FeatureDefinitionCreateDto, + FeatureDefinitionDto, + FeatureDefinitionGetListInput, + FeatureDefinitionUpdateDto, +} from '../types/definitions'; + +import { useRequest } from '@abp/request'; + +export function useFeatureDefinitionsApi() { + const { cancel, request } = useRequest(); + + /** + * 删除功能定义 + * @param name 功能名称 + */ + function deleteApi(name: string): Promise { + return request(`/api/feature-management/definitions/${name}`, { + method: 'DELETE', + }); + } + + /** + * 查询功能定义 + * @param name 功能名称 + * @returns 功能定义数据传输对象 + */ + function getApi(name: string): Promise { + return request( + `/api/feature-management/definitions/${name}`, + { + method: 'GET', + }, + ); + } + + /** + * 查询功能定义列表 + * @param input 功能过滤条件 + * @returns 功能定义数据传输对象列表 + */ + function getListApi( + input?: FeatureDefinitionGetListInput, + ): Promise> { + return request>( + `/api/feature-management/definitions`, + { + method: 'GET', + params: input, + }, + ); + } + + /** + * 创建功能定义 + * @param input 功能定义参数 + * @returns 功能定义数据传输对象 + */ + function createApi( + input: FeatureDefinitionCreateDto, + ): Promise { + return request( + '/api/feature-management/definitions', + { + data: input, + method: 'POST', + }, + ); + } + + /** + * 更新功能定义 + * @param name 功能名称 + * @param input 功能定义参数 + * @returns 功能定义数据传输对象 + */ + function updateApi( + name: string, + input: FeatureDefinitionUpdateDto, + ): Promise { + return request( + `/api/feature-management/definitions/${name}`, + { + data: input, + method: 'PUT', + }, + ); + } + + return { + cancel, + createApi, + deleteApi, + getApi, + getListApi, + updateApi, + }; +} diff --git a/apps/vben5/packages/@abp/features/src/components/definitions/features/FeatureDefinitionModal.vue b/apps/vben5/packages/@abp/features/src/components/definitions/features/FeatureDefinitionModal.vue new file mode 100644 index 000000000..4b812dfa3 --- /dev/null +++ b/apps/vben5/packages/@abp/features/src/components/definitions/features/FeatureDefinitionModal.vue @@ -0,0 +1,467 @@ + + + + + + + + + + onGroupChange(e?.toString())" + /> + + + + + + + + + + + + + + + + + + + (formModel.defaultValue = String( + e.target.checked, + ).toLowerCase()) + " + > + {{ $t('AbpFeatureManagement.DisplayName:DefaultValue') }} + + + + + {{ $t('AbpFeatureManagement.DisplayName:IsVisibleToClients') }} + + + + + {{ $t('AbpFeatureManagement.DisplayName:IsAvailableToHost') }} + + + + + + + + + + + + + + + + + + diff --git a/apps/vben5/packages/@abp/features/src/components/definitions/features/FeatureDefinitionTable.vue b/apps/vben5/packages/@abp/features/src/components/definitions/features/FeatureDefinitionTable.vue new file mode 100644 index 000000000..2ad85a924 --- /dev/null +++ b/apps/vben5/packages/@abp/features/src/components/definitions/features/FeatureDefinitionTable.vue @@ -0,0 +1,346 @@ + + + + + + + {{ $t('AbpFeatureManagement.FeatureDefinitions:AddNew') }} + + + + + + + + + + + + + + + + + + + + {{ $t('AbpUi.Edit') }} + + + {{ $t('AbpUi.Delete') }} + + + + + + + onGet()" /> + + + diff --git a/apps/vben5/packages/@abp/features/src/components/definitions/groups/FeatureGroupDefinitionTable.vue b/apps/vben5/packages/@abp/features/src/components/definitions/groups/FeatureGroupDefinitionTable.vue index fec1f118a..4534a8fd7 100644 --- a/apps/vben5/packages/@abp/features/src/components/definitions/groups/FeatureGroupDefinitionTable.vue +++ b/apps/vben5/packages/@abp/features/src/components/definitions/groups/FeatureGroupDefinitionTable.vue @@ -1,11 +1,14 @@ + + + + + + + + + + {{ $t('component.value_type_nput.type.name') }} + + + + + + {{ + $t('component.value_type_nput.validator.name') + }} + + + + + handleValueTypeChange(value?.toString())" + > + + {{ $t('component.value_type_nput.type.FREE_TEXT.name') }} + + + {{ $t('component.value_type_nput.type.TOGGLE.name') }} + + + {{ $t('component.value_type_nput.type.SELECTION.name') }} + + + + + + + + handleValidatorChange(value?.toString())" + > + + {{ $t('component.value_type_nput.validator.NULL.name') }} + + + {{ $t('component.value_type_nput.validator.BOOLEAN.name') }} + + + {{ $t('component.value_type_nput.validator.NUMERIC.name') }} + + + {{ $t('component.value_type_nput.validator.STRING.name') }} + + + + + + + + + + + + + + {{ + $t( + 'component.value_type_nput.validator.NUMERIC.minValue', + ) + }} + + + + + + {{ + $t( + 'component.value_type_nput.validator.NUMERIC.maxValue', + ) + }} + + + + + + + + + + + + + + + + + + + {{ + $t( + 'component.value_type_nput.validator.STRING.allowNull', + ) + }} + + + + + + {{ + $t( + 'component.value_type_nput.validator.STRING.regularExpression', + ) + }} + + + + + + + + + + {{ + $t( + 'component.value_type_nput.validator.STRING.minLength', + ) + }} + + + + + + {{ + $t( + 'component.value_type_nput.validator.STRING.maxLength', + ) + }} + + + + + + + + + + + + + + + + + + + + + + {{ + $t( + 'component.value_type_nput.type.SELECTION.itemsNotBeEmpty', + ) + }} + + + + + + {{ + $t( + 'component.value_type_nput.type.SELECTION.actions.create', + ) + }} + + + {{ + $t( + 'component.value_type_nput.type.SELECTION.actions.clean', + ) + }} + + + + + + + + + {{ getDisplayName(record.displayText) }} + + + + handleEdit(record)" + > + + + + {{ + $t( + 'component.value_type_nput.type.SELECTION.actions.update', + ) + }} + + handleDelete(record)" + danger + > + + + + {{ + $t( + 'component.value_type_nput.type.SELECTION.actions.delete', + ) + }} + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/vben5/packages/@abp/ui/src/components/string-value-type/index.ts b/apps/vben5/packages/@abp/ui/src/components/string-value-type/index.ts new file mode 100644 index 000000000..7560b8843 --- /dev/null +++ b/apps/vben5/packages/@abp/ui/src/components/string-value-type/index.ts @@ -0,0 +1,4 @@ +export * from './interface'; +export { default as StringValueTypeInput } from './StringValueTypeInput.vue'; +export * from './validator'; +export * from './valueType'; diff --git a/apps/vben5/packages/@abp/ui/src/components/string-value-type/interface.ts b/apps/vben5/packages/@abp/ui/src/components/string-value-type/interface.ts new file mode 100644 index 000000000..b28d11292 --- /dev/null +++ b/apps/vben5/packages/@abp/ui/src/components/string-value-type/interface.ts @@ -0,0 +1,3 @@ +export interface StringValueTypeInstance { + validate(value: any): Promise; +} diff --git a/apps/vben5/packages/@abp/ui/src/components/string-value-type/validator.ts b/apps/vben5/packages/@abp/ui/src/components/string-value-type/validator.ts new file mode 100644 index 000000000..e97848cc9 --- /dev/null +++ b/apps/vben5/packages/@abp/ui/src/components/string-value-type/validator.ts @@ -0,0 +1,143 @@ +import type { Dictionary } from '@abp/core'; + +import { isBoolean, isNumber } from '@vben/utils'; + +import { isNullOrUnDef, isNullOrWhiteSpace } from '@abp/core'; + +export interface ValueValidator { + isValid(value?: any): boolean; + name: string; + + properties: Dictionary; +} + +export class AlwaysValidValueValidator implements ValueValidator { + name = 'NULL'; + properties: Dictionary; + constructor() { + this.properties = {}; + } + isValid(_value?: any): boolean { + return true; + } +} + +export class BooleanValueValidator implements ValueValidator { + name = 'BOOLEAN'; + properties: Dictionary; + constructor() { + this.properties = {}; + } + isValid(value?: any): boolean { + if (isNullOrUnDef(value)) return true; + if (isBoolean(value)) return true; + const bolString = String(value).toLowerCase(); + if (bolString === 'true' || bolString === 'false') return true; + return false; + } +} + +export class NumericValueValidator implements ValueValidator { + name = 'NUMERIC'; + properties: Dictionary; + get maxValue(): number | undefined { + return Number(this.properties.MaxValue); + } + + set maxValue(value: number) { + this.properties.MaxValue = value; + } + + get minValue(): number | undefined { + return Number(this.properties.MinValue); + } + + set minValue(value: number) { + this.properties.MinValue = value; + } + + constructor() { + this.properties = {}; + } + + _isValidInternal(value: number): boolean { + if (this.minValue && value < this.minValue) return false; + if (this.maxValue && value > this.maxValue) return false; + return true; + } + + isValid(value?: any): boolean { + if (isNullOrUnDef(value)) return true; + if (isNumber(value)) return this._isValidInternal(value); + const numString = String(value); + if (!isNullOrUnDef(numString)) { + const num = Number(numString); + if (num) return this._isValidInternal(num); + } + return false; + } +} + +export class StringValueValidator implements ValueValidator { + name = 'STRING'; + properties: Dictionary; + get allowNull(): boolean { + return ( + String(this.properties.AllowNull ?? 'true')?.toLowerCase() === 'true' + ); + } + + set allowNull(value: boolean) { + this.properties.AllowNull = value; + } + + get maxLength(): number | undefined { + return Number(this.properties.MaxLength); + } + + set maxLength(value: number) { + this.properties.MaxLength = value; + } + + get minLength(): number | undefined { + return Number(this.properties.MinLength); + } + + set minLength(value: number) { + this.properties.MinLength = value; + } + + get regularExpression(): string { + return String(this.properties.RegularExpression ?? ''); + } + + set regularExpression(value: string) { + this.properties.RegularExpression = value; + } + + constructor() { + this.properties = {}; + } + + isValid(value?: any): boolean { + if (!this.allowNull && isNullOrUnDef(value)) return false; + const valueString = String(value); + if (!this.allowNull && isNullOrWhiteSpace(valueString.trim())) return false; + if ( + this.minLength && + this.minLength > 0 && + valueString.length < this.minLength + ) + return false; + if ( + this.maxLength && + this.maxLength > 0 && + valueString.length > this.maxLength + ) + return false; + if (!isNullOrWhiteSpace(this.regularExpression)) { + return new RegExp(this.regularExpression).test(valueString); + } + return true; + } +} diff --git a/apps/vben5/packages/@abp/ui/src/components/string-value-type/valueType.ts b/apps/vben5/packages/@abp/ui/src/components/string-value-type/valueType.ts new file mode 100644 index 000000000..1d4cdab5e --- /dev/null +++ b/apps/vben5/packages/@abp/ui/src/components/string-value-type/valueType.ts @@ -0,0 +1,120 @@ +import type { Dictionary, LocalizableStringInfo } from '@abp/core'; + +import type { ValueValidator } from './validator'; + +import { + AlwaysValidValueValidator, + BooleanValueValidator, + NumericValueValidator, + StringValueValidator, +} from './validator'; + +export interface StringValueType { + name: string; + properties: Dictionary; + validator: ValueValidator; +} + +export interface SelectionStringValueItem { + displayText: LocalizableStringInfo; + value: string; +} + +export interface SelectionStringValueItemSource { + items: SelectionStringValueItem[]; +} + +export class FreeTextStringValueType implements StringValueType { + name = 'FreeTextStringValueType'; + properties: Dictionary; + validator: ValueValidator; + constructor(validator?: ValueValidator) { + this.properties = {}; + this.validator = validator ?? new AlwaysValidValueValidator(); + } +} + +export class ToggleStringValueType implements StringValueType { + name = 'ToggleStringValueType'; + properties: Dictionary; + validator: ValueValidator; + constructor(validator?: ValueValidator) { + this.properties = {}; + this.validator = validator ?? new BooleanValueValidator(); + } +} + +export class SelectionStringValueType implements StringValueType { + itemSource: SelectionStringValueItemSource; + name = 'SelectionStringValueType'; + properties: Dictionary; + validator: ValueValidator; + constructor(validator?: ValueValidator) { + this.properties = {}; + this.itemSource = { + items: [], + }; + this.validator = validator ?? new AlwaysValidValueValidator(); + } +} + +class StringValueTypeSerializer { + _deserializeValidator(validator: any): ValueValidator { + let convertValidator: ValueValidator = new AlwaysValidValueValidator(); + if (validator.name) { + switch (validator.name) { + case 'BOOLEAN': { + convertValidator = new BooleanValueValidator(); + break; + } + case 'NULL': { + convertValidator = new AlwaysValidValueValidator(); + break; + } + case 'NUMERIC': { + convertValidator = new NumericValueValidator(); + break; + } + case 'STRING': { + convertValidator = new StringValueValidator(); + break; + } + } + } + convertValidator.properties = validator.properties; + return convertValidator; + } + + deserialize(value: string): StringValueType { + let valueType: StringValueType; + const valueTypeObj = JSON.parse(value); + switch (valueTypeObj.name) { + case 'SELECTION': + case 'SelectionStringValueType': { + valueType = new SelectionStringValueType(); + (valueType as SelectionStringValueType).itemSource = + valueTypeObj.itemSource; + break; + } + case 'TOGGLE': + case 'ToggleStringValueType': { + valueType = new ToggleStringValueType(); + break; + } + default: { + valueType = new FreeTextStringValueType(); + break; + } + } + valueType.properties = valueTypeObj.properties; + valueType.validator = this._deserializeValidator(valueTypeObj.validator); + return valueType; + } + + serialize(value: StringValueType): string { + const valueTypeString = JSON.stringify(value); + return valueTypeString; + } +} + +export const valueTypeSerializer = new StringValueTypeSerializer();