Browse Source

Merge pull request #1134 from colinin/vben5-features

Vben5 features
pull/1149/head
yx lin 11 months ago
committed by GitHub
parent
commit
ac82f3d463
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      apps/vben5/apps/app-antd/package.json
  2. 5
      apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json
  3. 5
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json
  4. 29
      apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts
  5. 15
      apps/vben5/apps/app-antd/src/views/features/definitions/index.vue
  6. 15
      apps/vben5/apps/app-antd/src/views/features/groups/index.vue
  7. 8
      apps/vben5/packages/@abp/core/src/hooks/useValidation.ts
  8. 5
      apps/vben5/packages/@abp/core/src/types/rules.ts
  9. 1
      apps/vben5/packages/@abp/core/src/utils/index.ts
  10. 39
      apps/vben5/packages/@abp/features/package.json
  11. 3
      apps/vben5/packages/@abp/features/src/api/index.ts
  12. 100
      apps/vben5/packages/@abp/features/src/api/useFeatureDefinitionsApi.ts
  13. 100
      apps/vben5/packages/@abp/features/src/api/useFeatureGroupDefinitionsApi.ts
  14. 62
      apps/vben5/packages/@abp/features/src/api/useFeaturesApi.ts
  15. 467
      apps/vben5/packages/@abp/features/src/components/definitions/features/FeatureDefinitionModal.vue
  16. 346
      apps/vben5/packages/@abp/features/src/components/definitions/features/FeatureDefinitionTable.vue
  17. 150
      apps/vben5/packages/@abp/features/src/components/definitions/groups/FeatureGroupDefinitionModal.vue
  18. 261
      apps/vben5/packages/@abp/features/src/components/definitions/groups/FeatureGroupDefinitionTable.vue
  19. 262
      apps/vben5/packages/@abp/features/src/components/features/FeatureModal.vue
  20. 3
      apps/vben5/packages/@abp/features/src/components/index.ts
  21. 1
      apps/vben5/packages/@abp/features/src/constants/index.ts
  22. 20
      apps/vben5/packages/@abp/features/src/constants/permissions.ts
  23. 4
      apps/vben5/packages/@abp/features/src/index.ts
  24. 47
      apps/vben5/packages/@abp/features/src/types/definitions.ts
  25. 63
      apps/vben5/packages/@abp/features/src/types/features.ts
  26. 31
      apps/vben5/packages/@abp/features/src/types/groups.ts
  27. 3
      apps/vben5/packages/@abp/features/src/types/index.ts
  28. 6
      apps/vben5/packages/@abp/features/tsconfig.json
  29. 2
      apps/vben5/packages/@abp/gdpr/src/api/index.ts
  30. 4
      apps/vben5/packages/@abp/platform/src/api/index.ts
  31. 1
      apps/vben5/packages/@abp/saas/package.json
  32. 4
      apps/vben5/packages/@abp/saas/src/api/index.ts
  33. 14
      apps/vben5/packages/@abp/saas/src/components/tenants/TenantModal.vue
  34. 21
      apps/vben5/packages/@abp/saas/src/components/tenants/TenantTable.vue
  35. 1
      apps/vben5/packages/@abp/settings/package.json
  36. 23
      apps/vben5/packages/@abp/settings/src/components/settings/SettingForm.vue
  37. 23
      apps/vben5/packages/@abp/settings/src/components/settings/SystemSetting.vue
  38. 1
      apps/vben5/packages/@abp/ui/src/components/index.ts
  39. 765
      apps/vben5/packages/@abp/ui/src/components/string-value-type/StringValueTypeInput.vue
  40. 4
      apps/vben5/packages/@abp/ui/src/components/string-value-type/index.ts
  41. 3
      apps/vben5/packages/@abp/ui/src/components/string-value-type/interface.ts
  42. 143
      apps/vben5/packages/@abp/ui/src/components/string-value-type/validator.ts
  43. 120
      apps/vben5/packages/@abp/ui/src/components/string-value-type/valueType.ts

1
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:*",

5
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",

5
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": "设置定义",

29
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'),

15
apps/vben5/apps/app-antd/src/views/features/definitions/index.vue

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { FeatureDefinitionTable } from '@abp/features';
defineOptions({
name: 'FeatureDefinitions',
});
</script>
<template>
<Page>
<FeatureDefinitionTable />
</Page>
</template>

15
apps/vben5/apps/app-antd/src/views/features/groups/index.vue

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { FeatureGroupDefinitionTable } from '@abp/features';
defineOptions({
name: 'FeatureGroupDefinitions',
});
</script>
<template>
<Page>
<FeatureGroupDefinitionTable />
</Page>
</template>

8
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<string, string> | undefined,
) {
return L(enumName, args);
},
};
return ruleCreator;

5
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<string, string> | undefined,
): string;
}
export type { RuleCreator };

1
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';

39
apps/vben5/packages/@abp/features/package.json

@ -0,0 +1,39 @@
{
"name": "@abp/features",
"version": "9.0.4",
"homepage": "https://github.com/colinin/abp-next-admin",
"bugs": "https://github.com/colinin/abp-next-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/colinin/abp-next-admin.git",
"directory": "packages/@abp/features"
},
"license": "MIT",
"type": "module",
"sideEffects": [
"**/*.css"
],
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"dependencies": {
"@abp/core": "workspace:*",
"@abp/request": "workspace:*",
"@abp/ui": "workspace:*",
"@ant-design/icons-vue": "catalog:",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/utils": "workspace:*",
"ant-design-vue": "catalog:",
"dayjs": "catalog:",
"vue": "catalog:*",
"vxe-table": "catalog:"
},
"devDependencies": {}
}

3
apps/vben5/packages/@abp/features/src/api/index.ts

@ -0,0 +1,3 @@
export { useFeatureDefinitionsApi } from './useFeatureDefinitionsApi';
export { useFeatureGroupDefinitionsApi } from './useFeatureGroupDefinitionsApi';
export { useFeaturesApi } from './useFeaturesApi';

100
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<void> {
return request(`/api/feature-management/definitions/${name}`, {
method: 'DELETE',
});
}
/**
*
* @param name
* @returns
*/
function getApi(name: string): Promise<FeatureDefinitionDto> {
return request<FeatureDefinitionDto>(
`/api/feature-management/definitions/${name}`,
{
method: 'GET',
},
);
}
/**
*
* @param input
* @returns
*/
function getListApi(
input?: FeatureDefinitionGetListInput,
): Promise<ListResultDto<FeatureDefinitionDto>> {
return request<ListResultDto<FeatureDefinitionDto>>(
`/api/feature-management/definitions`,
{
method: 'GET',
params: input,
},
);
}
/**
*
* @param input
* @returns
*/
function createApi(
input: FeatureDefinitionCreateDto,
): Promise<FeatureDefinitionDto> {
return request<FeatureDefinitionDto>(
'/api/feature-management/definitions',
{
data: input,
method: 'POST',
},
);
}
/**
*
* @param name
* @param input
* @returns
*/
function updateApi(
name: string,
input: FeatureDefinitionUpdateDto,
): Promise<FeatureDefinitionDto> {
return request<FeatureDefinitionDto>(
`/api/feature-management/definitions/${name}`,
{
data: input,
method: 'PUT',
},
);
}
return {
cancel,
createApi,
deleteApi,
getApi,
getListApi,
updateApi,
};
}

100
apps/vben5/packages/@abp/features/src/api/useFeatureGroupDefinitionsApi.ts

@ -0,0 +1,100 @@
import type { ListResultDto } from '@abp/core';
import type {
FeatureGroupDefinitionCreateDto,
FeatureGroupDefinitionDto,
FeatureGroupDefinitionGetListInput,
FeatureGroupDefinitionUpdateDto,
} from '../types/groups';
import { useRequest } from '@abp/request';
export function useFeatureGroupDefinitionsApi() {
const { cancel, request } = useRequest();
/**
*
* @param name
*/
function deleteApi(name: string): Promise<void> {
return request(`/api/feature-management/definitions/groups/${name}`, {
method: 'DELETE',
});
}
/**
*
* @param name
* @returns
*/
function getApi(name: string): Promise<FeatureGroupDefinitionDto> {
return request<FeatureGroupDefinitionDto>(
`/api/feature-management/definitions/groups/${name}`,
{
method: 'GET',
},
);
}
/**
*
* @param input
* @returns
*/
function getListApi(
input?: FeatureGroupDefinitionGetListInput,
): Promise<ListResultDto<FeatureGroupDefinitionDto>> {
return request<ListResultDto<FeatureGroupDefinitionDto>>(
`/api/feature-management/definitions/groups`,
{
method: 'GET',
params: input,
},
);
}
/**
*
* @param input
* @returns
*/
function createApi(
input: FeatureGroupDefinitionCreateDto,
): Promise<FeatureGroupDefinitionDto> {
return request<FeatureGroupDefinitionDto>(
'/api/feature-management/definitions/groups',
{
data: input,
method: 'POST',
},
);
}
/**
*
* @param name
* @param input
* @returns
*/
function updateApi(
name: string,
input: FeatureGroupDefinitionUpdateDto,
): Promise<FeatureGroupDefinitionDto> {
return request<FeatureGroupDefinitionDto>(
`/api/feature-management/definitions/groups/${name}`,
{
data: input,
method: 'PUT',
},
);
}
return {
cancel,
createApi,
deleteApi,
getApi,
getListApi,
updateApi,
};
}

62
apps/vben5/packages/@abp/features/src/api/useFeaturesApi.ts

@ -0,0 +1,62 @@
import type {
FeatureProvider,
GetFeatureListResultDto,
UpdateFeaturesDto,
} from '../types/features';
import { useRequest } from '@abp/request';
export function useFeaturesApi() {
const { cancel, request } = useRequest();
/**
*
* @param {FeatureProvider} provider
* @returns {Promise<void>}
*/
function deleteApi(provider: FeatureProvider): Promise<void> {
return request(`/api/feature-management/features`, {
method: 'DELETE',
params: provider,
});
}
/**
*
* @param {FeatureProvider} provider
* @returns {Promise<GetFeatureListResultDto>}
*/
function getApi(provider: FeatureProvider): Promise<GetFeatureListResultDto> {
return request<GetFeatureListResultDto>(
`/api/feature-management/features`,
{
method: 'GET',
params: provider,
},
);
}
/**
*
* @param {FeatureProvider} provider
* @param {UpdateFeaturesDto} input
* @returns {Promise<void>}
*/
function updateApi(
provider: FeatureProvider,
input: UpdateFeaturesDto,
): Promise<void> {
return request(`/api/feature-management/features`, {
data: input,
method: 'PUT',
params: provider,
});
}
return {
cancel,
deleteApi,
getApi,
updateApi,
};
}

467
apps/vben5/packages/@abp/features/src/components/definitions/features/FeatureDefinitionModal.vue

@ -0,0 +1,467 @@
<script setup lang="ts">
import type {
PropertyInfo,
SelectionStringValueItem,
StringValueTypeInstance,
} from '@abp/ui';
import type { FormInstance } from 'ant-design-vue';
import type { FeatureDefinitionDto } from '../../../types/definitions';
import type { FeatureGroupDefinitionDto } from '../../../types/groups';
import {
defineEmits,
defineOptions,
reactive,
ref,
toValue,
unref,
useTemplateRef,
} from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { isBoolean, isString } from '@vben/utils';
import {
listToTree,
useLocalization,
useLocalizationSerializer,
useValidation,
ValidationEnum,
} from '@abp/core';
import { LocalizableInput, PropertyTable, StringValueTypeInput } from '@abp/ui';
import {
Checkbox,
Form,
Input,
InputNumber,
message,
Select,
Tabs,
Textarea,
TreeSelect,
} from 'ant-design-vue';
import { useFeatureDefinitionsApi } from '../../../api/useFeatureDefinitionsApi';
import { useFeatureGroupDefinitionsApi } from '../../../api/useFeatureGroupDefinitionsApi';
defineOptions({
name: 'FeatureDefinitionModal',
});
const emits = defineEmits<{
(event: 'change', data: FeatureDefinitionDto): void;
}>();
const FormItem = Form.Item;
const TabPane = Tabs.TabPane;
interface DefinitionTreeVo {
children: DefinitionTreeVo[];
displayName: string;
groupName: string;
name: string;
}
type TabKeys = 'basic' | 'props';
const defaultModel = {
allowedProviders: [],
displayName: '',
extraProperties: {},
groupName: '',
isAvailableToHost: true,
isEnabled: true,
isStatic: false,
isVisibleToClients: false,
name: '',
valueType: '',
} as FeatureDefinitionDto;
const isEditModel = ref(false);
const validatorNameRef = ref<string>('NULL');
const valueTypeNameRef = ref<string>('FreeTextStringValueType');
const activeTab = ref<TabKeys>('basic');
const form = useTemplateRef<FormInstance>('form');
const valueTypeInput = useTemplateRef<StringValueTypeInstance>('valueType');
const formModel = ref<FeatureDefinitionDto>({ ...defaultModel });
const availableGroups = ref<FeatureGroupDefinitionDto[]>([]);
const availableDefinitions = ref<DefinitionTreeVo[]>([]);
const selectionDataSource = ref<{ label: string; value: string }[]>([]);
const { Lr } = useLocalization();
const { deserialize, validate } = useLocalizationSerializer();
const { getListApi: getGroupsApi } = useFeatureGroupDefinitionsApi();
const { defineValidator, fieldRequired, mapEnumValidMessage } = useValidation();
const {
createApi,
getApi,
getListApi: getDefinitionsApi,
updateApi,
} = useFeatureDefinitionsApi();
const formRules = reactive({
defaultValue: defineValidator({
trigger: 'change',
validator(_rule, value) {
const valueType = unref(valueTypeInput);
if (valueType) {
return valueType.validate(value);
}
return Promise.resolve();
},
}),
// description: defineValidator({
// trigger: 'blur',
// validator(_rule, value) {
// if (!validate(value, { required: false })) {
// return Promise.reject(
// $t(ValidationEnum.FieldRequired, [$t('DisplayName:Description')]),
// );
// }
// return Promise.resolve();
// },
// }),
displayName: defineValidator({
required: true,
trigger: 'blur',
validator(_rule, value) {
if (!validate(value)) {
return Promise.reject(
mapEnumValidMessage(ValidationEnum.FieldRequired, [
$t('AbpFeatureManagement.DisplayName:DisplayName'),
]),
);
}
return Promise.resolve();
},
}),
groupName: fieldRequired({
name: 'GroupName',
prefix: 'DisplayName',
resourceName: 'AbpFeatureManagement',
trigger: 'blur',
}),
name: fieldRequired({
name: 'Name',
prefix: 'DisplayName',
resourceName: 'AbpFeatureManagement',
trigger: 'blur',
}),
});
const [Modal, modalApi] = useVbenModal({
class: 'w-1/2',
draggable: true,
fullscreenButton: false,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
await form.value?.validate();
const input = toValue(formModel);
if (input.defaultValue && !isString(input.defaultValue)) {
input.defaultValue = String(input.defaultValue);
}
const api = isEditModel.value
? updateApi(formModel.value.name, input)
: createApi(input);
modalApi.setState({ confirmLoading: true, loading: true });
api
.then((res) => {
message.success($t('AbpUi.SavedSuccessfully'));
emits('change', res);
modalApi.close();
})
.finally(() => {
modalApi.setState({ confirmLoading: false, loading: false });
});
},
onOpenChange: async (isOpen: boolean) => {
if (isOpen) {
isEditModel.value = false;
activeTab.value = 'basic';
formModel.value = { ...defaultModel };
availableDefinitions.value = [];
availableGroups.value = [];
modalApi.setState({
showConfirmButton: true,
title: $t('AbpFeatureManagement.FeatureDefinitions:AddNew'),
});
try {
modalApi.setState({ loading: true });
const { groupName, name } = modalApi.getData<FeatureDefinitionDto>();
name && (await onGet(name));
await onInitGroups(groupName);
} finally {
modalApi.setState({ loading: false });
}
}
},
title: $t('AbpFeatureManagement.FeatureDefinitions:AddNew'),
});
async function onInitGroups(name?: string) {
const { items } = await getGroupsApi({ filter: name });
availableGroups.value = items.map((group) => {
const localizableGroup = deserialize(group.displayName);
return {
...group,
displayName: Lr(localizableGroup.resourceName, localizableGroup.name),
};
});
if (name) {
formModel.value.groupName = name;
await onGroupChange(name);
}
}
async function onGet(name: string) {
isEditModel.value = true;
const dto = await getApi(name);
formModel.value = dto;
modalApi.setState({
showConfirmButton: !dto.isStatic,
title: `${$t('AbpFeatureManagement.FeatureDefinitions')} - ${dto.name}`,
});
}
async function onGroupChange(name?: string) {
const { items } = await getDefinitionsApi({
groupName: name,
});
const features = items.map((item) => {
const displayName = deserialize(item.displayName);
const description = deserialize(item.description);
return {
...item,
description: Lr(description.resourceName, description.name),
disabled: item.name === formModel.value.name,
displayName: Lr(displayName.resourceName, displayName.name),
};
});
availableDefinitions.value = listToTree(features, {
id: 'name',
pid: 'parentName',
});
}
function onPropChange(prop: PropertyInfo) {
formModel.value.extraProperties ??= {};
formModel.value.extraProperties[prop.key] = prop.value;
}
function onPropDelete(prop: PropertyInfo) {
formModel.value.extraProperties ??= {};
delete formModel.value.extraProperties[prop.key];
}
function onValueTypeNameChange(valueTypeName: string) {
valueTypeNameRef.value = valueTypeName;
switch (valueTypeName) {
case 'ToggleStringValueType': {
if (!isBoolean(formModel.value.defaultValue)) {
formModel.value.defaultValue ??= 'false';
}
break;
}
default: {
formModel.value.defaultValue ??= undefined;
break;
}
}
form.value?.clearValidate();
}
function onValidatorNameChange(validatorName: string) {
validatorNameRef.value = validatorName;
}
function onSelectionChange(items: SelectionStringValueItem[]) {
if (items.length === 0) {
formModel.value.defaultValue = undefined;
selectionDataSource.value = [];
return;
}
selectionDataSource.value = items.map((item) => {
return {
label: Lr(item.displayText.resourceName, item.displayText.name),
value: item.value,
};
});
if (!items.some((item) => item.value === formModel.value.defaultValue)) {
formModel.value.defaultValue = undefined;
}
}
</script>
<template>
<Modal>
<Form
ref="form"
:model="formModel"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Tabs v-model:active-key="activeTab">
<!-- 基本信息 -->
<TabPane key="basic" :tab="$t('AbpFeatureManagement.BasicInfo')">
<FormItem
:label="$t('AbpFeatureManagement.DisplayName:GroupName')"
name="groupName"
>
<Select
v-model:value="formModel.groupName"
:allow-clear="true"
:disabled="formModel.isStatic"
:field-names="{
label: 'displayName',
value: 'name',
}"
:options="availableGroups"
@change="(e) => onGroupChange(e?.toString())"
/>
</FormItem>
<FormItem
v-if="availableDefinitions.length > 0"
:label="$t('AbpFeatureManagement.DisplayName:ParentName')"
name="parentName"
>
<TreeSelect
v-model:value="formModel.parentName"
:allow-clear="true"
:disabled="formModel.isStatic"
:field-names="{
label: 'displayName',
value: 'name',
children: 'children',
}"
:tree-data="availableDefinitions"
/>
</FormItem>
<FormItem
:label="$t('AbpFeatureManagement.DisplayName:Name')"
name="name"
>
<Input
v-model:value="formModel.name"
:disabled="formModel.isStatic"
autocomplete="off"
/>
</FormItem>
<FormItem
:label="$t('AbpFeatureManagement.DisplayName:DisplayName')"
name="displayName"
>
<LocalizableInput
v-model:value="formModel.displayName"
:disabled="formModel.isStatic"
/>
</FormItem>
<FormItem
:label="$t('AbpFeatureManagement.DisplayName:Description')"
name="description"
>
<LocalizableInput
v-model:value="formModel.description"
:disabled="formModel.isStatic"
/>
</FormItem>
<FormItem
name="defaultValue"
:label="$t('AbpFeatureManagement.DisplayName:DefaultValue')"
>
<Select
v-if="valueTypeNameRef === 'SelectionStringValueType'"
:disabled="formModel.isStatic"
:allow-clear="true"
v-model:value="formModel.defaultValue"
:options="selectionDataSource"
/>
<Textarea
v-else-if="
valueTypeNameRef === 'FreeTextStringValueType' &&
(validatorNameRef === 'NULL' || validatorNameRef === 'STRING')
"
:disabled="formModel.isStatic"
:allow-clear="true"
:auto-size="{ minRows: 3 }"
v-model:value="formModel.defaultValue"
/>
<InputNumber
v-else-if="
valueTypeNameRef === 'FreeTextStringValueType' &&
validatorNameRef === 'NUMERIC'
"
style="width: 100%"
:disabled="formModel.isStatic"
v-model:value="formModel.defaultValue"
/>
<Checkbox
v-else-if="
valueTypeNameRef === 'ToggleStringValueType' &&
validatorNameRef === 'BOOLEAN'
"
:disabled="formModel.isStatic"
:checked="formModel.defaultValue === 'true'"
@change="
(e) =>
(formModel.defaultValue = String(
e.target.checked,
).toLowerCase())
"
>
{{ $t('AbpFeatureManagement.DisplayName:DefaultValue') }}
</Checkbox>
</FormItem>
<FormItem
name="isVisibleToClients"
:label="$t('AbpFeatureManagement.DisplayName:IsVisibleToClients')"
:extra="$t('AbpFeatureManagement.Description:IsVisibleToClients')"
>
<Checkbox v-model:checked="formModel.isVisibleToClients">
{{ $t('AbpFeatureManagement.DisplayName:IsVisibleToClients') }}
</Checkbox>
</FormItem>
<FormItem
name="isAvailableToHost"
:label="$t('AbpFeatureManagement.DisplayName:IsAvailableToHost')"
:extra="$t('AbpFeatureManagement.Description:IsAvailableToHost')"
>
<Checkbox v-model:checked="formModel.isAvailableToHost">
{{ $t('AbpFeatureManagement.DisplayName:IsAvailableToHost') }}
</Checkbox>
</FormItem>
</TabPane>
<TabPane
key="valueType"
:tab="$t('AbpFeatureManagement.ValueValidator')"
force-render
>
<FormItem
name="valueType"
label=""
:label-col="{ span: 0 }"
:wrapper-col="{ span: 24 }"
>
<StringValueTypeInput
ref="valueType"
:disabled="formModel.isStatic"
:allow-delete="true"
:allow-edit="true"
v-model:value="formModel.valueType"
@change:value-type="onValueTypeNameChange"
@change:validator="onValidatorNameChange"
@change:selection="onSelectionChange"
/>
</FormItem>
</TabPane>
<!-- 属性 -->
<TabPane key="props" :tab="$t('AbpFeatureManagement.Properties')">
<PropertyTable
:data="formModel.extraProperties"
:disabled="formModel.isStatic"
@change="onPropChange"
@delete="onPropDelete"
/>
</TabPane>
</Tabs>
</Form>
</Modal>
</template>
<style scoped></style>

346
apps/vben5/packages/@abp/features/src/components/definitions/features/FeatureDefinitionTable.vue

@ -0,0 +1,346 @@
<script setup lang="ts">
import type { VbenFormProps, VxeGridListeners, VxeGridProps } from '@abp/ui';
import type { FeatureDefinitionDto } from '../../../types/definitions';
import type { FeatureGroupDefinitionDto } from '../../../types/groups';
import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
listToTree,
useLocalization,
useLocalizationSerializer,
} from '@abp/core';
import { useVbenVxeGrid } from '@abp/ui';
import {
CheckOutlined,
CloseOutlined,
DeleteOutlined,
EditOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import { Button, message, Modal } from 'ant-design-vue';
import { VxeGrid } from 'vxe-table';
import { useFeatureDefinitionsApi } from '../../../api/useFeatureDefinitionsApi';
import { useFeatureGroupDefinitionsApi } from '../../../api/useFeatureGroupDefinitionsApi';
import { GroupDefinitionsPermissions } from '../../../constants/permissions';
defineOptions({
name: 'FeatureDefinitionTable',
});
interface PermissionVo {
children: PermissionVo[];
displayName: string;
groupName: string;
isEnabled: boolean;
isStatic: boolean;
name: string;
parentName?: string;
providers: string[];
stateCheckers: string[];
}
interface PermissionGroupVo {
displayName: string;
name: string;
permissions: PermissionVo[];
}
const permissionGroups = ref<PermissionGroupVo[]>([]);
const pageState = reactive({
current: 1,
size: 10,
total: 0,
});
const { Lr } = useLocalization();
const { deserialize } = useLocalizationSerializer();
const { getListApi: getGroupsApi } = useFeatureGroupDefinitionsApi();
const { deleteApi, getListApi: getPermissionsApi } = useFeatureDefinitionsApi();
const formOptions: VbenFormProps = {
//
collapsed: false,
handleReset: onReset,
async handleSubmit(params) {
pageState.current = 1;
await onGet(params);
},
schema: [
{
component: 'Input',
fieldName: 'filter',
formItemClass: 'col-span-2 items-baseline',
label: $t('AbpUi.Search'),
},
],
//
showCollapseButton: true,
//
submitOnEnter: true,
};
const gridOptions: VxeGridProps<FeatureGroupDefinitionDto> = {
columns: [
{
align: 'center',
type: 'seq',
width: 50,
},
{
align: 'left',
field: 'group',
slots: { content: 'group' },
type: 'expand',
width: 50,
},
{
align: 'left',
field: 'name',
minWidth: 150,
title: $t('AbpFeatureManagement.DisplayName:Name'),
},
{
align: 'left',
field: 'displayName',
minWidth: 150,
title: $t('AbpFeatureManagement.DisplayName:DisplayName'),
},
],
expandConfig: {
padding: true,
trigger: 'row',
},
exportConfig: {},
keepSource: true,
toolbarConfig: {
custom: true,
export: true,
refresh: false,
zoom: true,
},
};
const subGridColumns: VxeGridProps<FeatureDefinitionDto>['columns'] = [
{
align: 'center',
type: 'seq',
width: 50,
},
{
align: 'left',
field: 'name',
minWidth: 150,
title: $t('AbpFeatureManagement.DisplayName:Name'),
treeNode: true,
},
{
align: 'left',
field: 'displayName',
minWidth: 120,
title: $t('AbpFeatureManagement.DisplayName:DisplayName'),
},
{
align: 'left',
field: 'description',
minWidth: 120,
title: $t('AbpFeatureManagement.DisplayName:Description'),
},
{
align: 'left',
field: 'isVisibleToClients',
minWidth: 120,
slots: { default: 'isVisibleToClients' },
title: $t('AbpFeatureManagement.DisplayName:IsVisibleToClients'),
},
{
align: 'left',
field: 'isAvailableToHost',
minWidth: 120,
slots: { default: 'isAvailableToHost' },
title: $t('AbpFeatureManagement.DisplayName:IsAvailableToHost'),
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: $t('AbpUi.Actions'),
width: 180,
},
];
const gridEvents: VxeGridListeners<FeatureGroupDefinitionDto> = {
pageChange(params) {
pageState.current = params.currentPage;
pageState.size = params.pageSize;
onPageChange();
},
};
const [GroupGrid, gridApi] = useVbenVxeGrid({
formOptions,
gridEvents,
gridOptions,
});
const [FeatureDefinitionModal, modalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(
() => import('./FeatureDefinitionModal.vue'),
),
});
async function onGet(input?: Record<string, string>) {
try {
gridApi.setLoading(true);
const groupRes = await getGroupsApi(input);
const permissionRes = await getPermissionsApi(input);
pageState.total = groupRes.items.length;
permissionGroups.value = groupRes.items.map((group) => {
const localizableGroup = deserialize(group.displayName);
const permissions = permissionRes.items
.filter((permission) => permission.groupName === group.name)
.map((permission) => {
const displayName = deserialize(permission.displayName);
const description = deserialize(permission.displayName);
return {
...permission,
description: Lr(description.resourceName, description.name),
displayName: Lr(displayName.resourceName, displayName.name),
};
});
return {
...group,
displayName: Lr(localizableGroup.resourceName, localizableGroup.name),
permissions: listToTree(permissions, {
id: 'name',
pid: 'parentName',
}),
};
});
onPageChange();
} finally {
gridApi.setLoading(false);
}
}
async function onReset() {
await gridApi.formApi.resetForm();
const input = await gridApi.formApi.getValues();
await onGet(input);
}
function onPageChange() {
const items = permissionGroups.value.slice(
(pageState.current - 1) * pageState.size,
pageState.current * pageState.size,
);
gridApi.setGridOptions({
data: items,
pagerConfig: {
currentPage: pageState.current,
pageSize: pageState.size,
total: pageState.total,
},
});
}
function onCreate() {
modalApi.setData({});
modalApi.open();
}
function onUpdate(row: FeatureDefinitionDto) {
modalApi.setData(row);
modalApi.open();
}
function onDelete(row: FeatureDefinitionDto) {
Modal.confirm({
centered: true,
content: `${$t('AbpUi.ItemWillBeDeletedMessageWithFormat', [row.name])}`,
onOk: async () => {
await deleteApi(row.name);
message.success($t('AbpUi.SuccessfullyDeleted'));
onGet();
},
title: $t('AbpUi.AreYouSure'),
});
}
onMounted(onGet);
</script>
<template>
<GroupGrid :table-title="$t('AbpFeatureManagement.FeatureDefinitions')">
<template #toolbar-tools>
<Button
:icon="h(PlusOutlined)"
type="primary"
v-access:code="[GroupDefinitionsPermissions.Create]"
@click="onCreate"
>
{{ $t('AbpFeatureManagement.FeatureDefinitions:AddNew') }}
</Button>
</template>
<template #group="{ row: group }">
<VxeGrid
:columns="subGridColumns"
:data="group.permissions"
:tree-config="{
trigger: 'row',
rowField: 'name',
childrenField: 'children',
}"
>
<template #isVisibleToClients="{ row }">
<div class="flex flex-row justify-center">
<CheckOutlined
v-if="row.isVisibleToClients"
class="text-green-500"
/>
<CloseOutlined v-else class="text-red-500" />
</div>
</template>
<template #isAvailableToHost="{ row }">
<div class="flex flex-row justify-center">
<CheckOutlined
v-if="row.isAvailableToHost"
class="text-green-500"
/>
<CloseOutlined v-else class="text-red-500" />
</div>
</template>
<template #action="{ row: permission }">
<div class="flex flex-row">
<Button
:icon="h(EditOutlined)"
block
type="link"
v-access:code="[GroupDefinitionsPermissions.Update]"
@click="onUpdate(permission)"
>
{{ $t('AbpUi.Edit') }}
</Button>
<Button
v-if="!permission.isStatic"
:icon="h(DeleteOutlined)"
block
danger
type="link"
v-access:code="[GroupDefinitionsPermissions.Delete]"
@click="onDelete(permission)"
>
{{ $t('AbpUi.Delete') }}
</Button>
</div>
</template>
</VxeGrid>
</template>
</GroupGrid>
<FeatureDefinitionModal @change="() => onGet()" />
</template>
<style scoped></style>

150
apps/vben5/packages/@abp/features/src/components/definitions/groups/FeatureGroupDefinitionModal.vue

@ -0,0 +1,150 @@
<script setup lang="ts">
import type { PropertyInfo } from '@abp/ui';
import type { FormInstance } from 'ant-design-vue';
import type { FeatureGroupDefinitionDto } from '../../../types/groups';
import { defineEmits, defineOptions, ref, toValue, useTemplateRef } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { LocalizableInput, PropertyTable } from '@abp/ui';
import { Form, Input, message, Tabs } from 'ant-design-vue';
import { useFeatureGroupDefinitionsApi } from '../../../api/useFeatureGroupDefinitionsApi';
defineOptions({
name: 'FeatureGroupDefinitionModal',
});
const emits = defineEmits<{
(event: 'change', data: FeatureGroupDefinitionDto): void;
}>();
const FormItem = Form.Item;
const TabPane = Tabs.TabPane;
type TabKeys = 'basic' | 'props';
const defaultModel = {} as FeatureGroupDefinitionDto;
const isEditModel = ref(false);
const activeTab = ref<TabKeys>('basic');
const form = useTemplateRef<FormInstance>('form');
const formModel = ref<FeatureGroupDefinitionDto>({ ...defaultModel });
const { cancel, createApi, getApi, updateApi } =
useFeatureGroupDefinitionsApi();
const [Modal, modalApi] = useVbenModal({
class: 'w-1/2',
draggable: true,
fullscreenButton: false,
onCancel() {
modalApi.close();
},
onClosed() {
cancel('FeatureGroupDefinitionModal has closed!');
},
onConfirm: async () => {
await form.value?.validate();
const api = isEditModel.value
? updateApi(formModel.value.name, toValue(formModel))
: createApi(toValue(formModel));
modalApi.setState({ confirmLoading: true, loading: true });
api
.then((res) => {
message.success($t('AbpUi.SavedSuccessfully'));
emits('change', res);
modalApi.close();
})
.finally(() => {
modalApi.setState({ confirmLoading: false, loading: false });
});
},
onOpenChange: async (isOpen: boolean) => {
if (isOpen) {
isEditModel.value = false;
activeTab.value = 'basic';
formModel.value = { ...defaultModel };
modalApi.setState({
showConfirmButton: true,
title: $t('AbpFeatureManagement.GroupDefinitions:AddNew'),
});
try {
modalApi.setState({ loading: true });
const { name } = modalApi.getData<FeatureGroupDefinitionDto>();
name && (await onGet(name));
} finally {
modalApi.setState({ loading: false });
}
}
},
title: $t('AbpFeatureManagement.GroupDefinitions:AddNew'),
});
async function onGet(name: string) {
isEditModel.value = true;
const dto = await getApi(name);
formModel.value = dto;
modalApi.setState({
showConfirmButton: !dto.isStatic,
title: `${$t('AbpFeatureManagement.GroupDefinitions')} - ${dto.name}`,
});
}
function onPropChange(prop: PropertyInfo) {
formModel.value.extraProperties ??= {};
formModel.value.extraProperties[prop.key] = prop.value;
}
function onPropDelete(prop: PropertyInfo) {
formModel.value.extraProperties ??= {};
delete formModel.value.extraProperties[prop.key];
}
</script>
<template>
<Modal>
<Form
ref="form"
:label-col="{ span: 6 }"
:model="formModel"
:wrapper-col="{ span: 18 }"
>
<Tabs v-model:active-key="activeTab">
<!-- 基本信息 -->
<TabPane key="basic" :tab="$t('AbpFeatureManagement.BasicInfo')">
<FormItem
:label="$t('AbpFeatureManagement.DisplayName:Name')"
name="name"
required
>
<Input
v-model:value="formModel.name"
:disabled="formModel.isStatic"
autocomplete="off"
/>
</FormItem>
<FormItem
:label="$t('AbpFeatureManagement.DisplayName:DisplayName')"
name="displayName"
required
>
<LocalizableInput
v-model:value="formModel.displayName"
:disabled="formModel.isStatic"
/>
</FormItem>
</TabPane>
<!-- 属性 -->
<TabPane key="props" :tab="$t('AbpFeatureManagement.Properties')">
<PropertyTable
:data="formModel.extraProperties"
:disabled="formModel.isStatic"
@change="onPropChange"
@delete="onPropDelete"
/>
</TabPane>
</Tabs>
</Form>
</Modal>
</template>
<style scoped></style>

261
apps/vben5/packages/@abp/features/src/components/definitions/groups/FeatureGroupDefinitionTable.vue

@ -0,0 +1,261 @@
<script setup lang="ts">
import type { VbenFormProps, VxeGridListeners, VxeGridProps } from '@abp/ui';
import type { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import type { FeatureGroupDefinitionDto } from '../../../types/groups';
import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue';
import { useAccess } from '@vben/access';
import { useVbenModal } from '@vben/common-ui';
import { createIconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { useLocalization, useLocalizationSerializer } from '@abp/core';
import { useVbenVxeGrid } from '@abp/ui';
import {
DeleteOutlined,
EditOutlined,
EllipsisOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import { Button, Dropdown, Menu, message, Modal } from 'ant-design-vue';
import { useFeatureGroupDefinitionsApi } from '../../../api/useFeatureGroupDefinitionsApi';
import {
FeatureDefinitionsPermissions,
GroupDefinitionsPermissions,
} from '../../../constants/permissions';
defineOptions({
name: 'FeatureGroupDefinitionTable',
});
const MenuItem = Menu.Item;
const FeaturesOutlined = createIconifyIcon('pajamas:feature-flag');
const permissionGroups = ref<FeatureGroupDefinitionDto[]>([]);
const pageState = reactive({
current: 1,
size: 10,
total: 0,
});
const { Lr } = useLocalization();
const { hasAccessByCodes } = useAccess();
const { deserialize } = useLocalizationSerializer();
const { deleteApi, getListApi } = useFeatureGroupDefinitionsApi();
const formOptions: VbenFormProps = {
//
collapsed: false,
handleReset: onReset,
async handleSubmit(params) {
pageState.current = 1;
await onGet(params);
},
schema: [
{
component: 'Input',
fieldName: 'filter',
formItemClass: 'col-span-2 items-baseline',
label: $t('AbpUi.Search'),
},
],
//
showCollapseButton: true,
//
submitOnEnter: true,
};
const gridOptions: VxeGridProps<FeatureGroupDefinitionDto> = {
columns: [
{
align: 'left',
field: 'name',
minWidth: 150,
title: $t('AbpFeatureManagement.DisplayName:Name'),
},
{
align: 'left',
field: 'displayName',
minWidth: 150,
title: $t('AbpFeatureManagement.DisplayName:DisplayName'),
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: $t('AbpUi.Actions'),
width: 220,
},
],
exportConfig: {},
keepSource: true,
toolbarConfig: {
custom: true,
export: true,
refresh: false,
zoom: true,
},
};
const gridEvents: VxeGridListeners<FeatureGroupDefinitionDto> = {
pageChange(params) {
pageState.current = params.currentPage;
pageState.size = params.pageSize;
onPageChange();
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridEvents,
gridOptions,
});
const [FeatureGroupDefinitionModal, groupModalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(
() => import('./FeatureGroupDefinitionModal.vue'),
),
});
const [FeatureDefinitionModal, defineModalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(
() => import('../features/FeatureDefinitionModal.vue'),
),
});
async function onGet(input?: Record<string, string>) {
try {
gridApi.setLoading(true);
const { items } = await getListApi(input);
pageState.total = items.length;
permissionGroups.value = items.map((item) => {
const localizableString = deserialize(item.displayName);
return {
...item,
displayName: Lr(localizableString.resourceName, localizableString.name),
};
});
onPageChange();
} finally {
gridApi.setLoading(false);
}
}
async function onReset() {
await gridApi.formApi.resetForm();
const input = await gridApi.formApi.getValues();
await onGet(input);
}
function onPageChange() {
const items = permissionGroups.value.slice(
(pageState.current - 1) * pageState.size,
pageState.current * pageState.size,
);
gridApi.setGridOptions({
data: items,
pagerConfig: {
currentPage: pageState.current,
pageSize: pageState.size,
total: pageState.total,
},
});
}
function onCreate() {
groupModalApi.setData({});
groupModalApi.open();
}
function onUpdate(row: FeatureGroupDefinitionDto) {
groupModalApi.setData(row);
groupModalApi.open();
}
function onMenuClick(row: FeatureGroupDefinitionDto, info: MenuInfo) {
switch (info.key) {
case 'features': {
defineModalApi.setData({
groupName: row.name,
});
defineModalApi.open();
break;
}
}
}
function onDelete(row: FeatureGroupDefinitionDto) {
Modal.confirm({
centered: true,
content: `${$t('AbpUi.ItemWillBeDeletedMessageWithFormat', [row.name])}`,
onOk: async () => {
await deleteApi(row.name);
message.success($t('AbpUi.SuccessfullyDeleted'));
onGet();
},
title: $t('AbpUi.AreYouSure'),
});
}
onMounted(onGet);
</script>
<template>
<Grid :table-title="$t('AbpFeatureManagement.GroupDefinitions')">
<template #toolbar-tools>
<Button
:icon="h(PlusOutlined)"
type="primary"
v-access:code="[GroupDefinitionsPermissions.Create]"
@click="onCreate"
>
{{ $t('AbpFeatureManagement.GroupDefinitions:AddNew') }}
</Button>
</template>
<template #action="{ row }">
<div class="flex flex-row">
<Button
:icon="h(EditOutlined)"
block
type="link"
v-access:code="[GroupDefinitionsPermissions.Update]"
@click="onUpdate(row)"
>
{{ $t('AbpUi.Edit') }}
</Button>
<Button
v-if="!row.isStatic"
:icon="h(DeleteOutlined)"
block
danger
type="link"
v-access:code="[GroupDefinitionsPermissions.Delete]"
@click="onDelete(row)"
>
{{ $t('AbpUi.Delete') }}
</Button>
<Dropdown v-if="!row.isStatic">
<template #overlay>
<Menu @click="(info) => onMenuClick(row, info)">
<MenuItem
v-if="hasAccessByCodes([FeatureDefinitionsPermissions.Create])"
key="features"
:icon="h(FeaturesOutlined)"
>
{{ $t('AbpFeatureManagement.GroupDefinitions:AddNew') }}
</MenuItem>
</Menu>
</template>
<Button :icon="h(EllipsisOutlined)" type="link" />
</Dropdown>
</div>
</template>
</Grid>
<FeatureGroupDefinitionModal @change="() => onGet()" />
<FeatureDefinitionModal />
</template>
<style scoped></style>

262
apps/vben5/packages/@abp/features/src/components/features/FeatureModal.vue

@ -0,0 +1,262 @@
<script setup lang="ts">
import type { SelectionStringValueType, Validator } from '@abp/core';
import type { FormInstance } from 'ant-design-vue/es/form/Form';
import type { FeatureGroupDto, UpdateFeaturesDto } from '../../types/features';
import { computed, ref, toValue, useTemplateRef } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useValidation } from '@abp/core';
import {
Checkbox,
Form,
Input,
InputNumber,
message,
Select,
Tabs,
} from 'ant-design-vue';
import { useFeaturesApi } from '../../api/useFeaturesApi';
const FormItem = Form.Item;
const TabPane = Tabs.TabPane;
interface ModalState {
displayName?: string;
providerKey?: string;
providerName: string;
readonly?: boolean;
}
interface FormModel {
groups: FeatureGroupDto[];
}
const activeTabKey = ref('');
const modelState = ref<ModalState>();
const formModel = ref<FormModel>({ groups: [] });
const form = useTemplateRef<FormInstance>('form');
const getModalTitle = computed(() => {
if (modelState.value?.displayName) {
return `${$t('AbpFeatureManagement.Features')} - ${modelState.value.displayName}`;
}
return $t('AbpFeatureManagement.Features');
});
const { getApi, updateApi } = useFeaturesApi();
const {
fieldMustBeetWeen,
fieldMustBeStringWithMinimumLengthAndMaximumLength,
fieldRequired,
} = useValidation();
const [Modal, modalApi] = useVbenModal({
centered: true,
class: 'w-1/2',
async onConfirm() {
await form.value?.validate();
await onSubmit();
},
async onOpenChange(isOpen) {
if (isOpen) {
formModel.value = { groups: [] };
await onGet();
if (formModel.value?.groups.length > 0) {
activeTabKey.value = formModel.value.groups[0]?.name!;
}
}
},
});
function mapFeatures(groups: FeatureGroupDto[]) {
groups.forEach((group) => {
group.features.forEach((feature) => {
if (feature.valueType.name === 'SelectionStringValueType') {
const valueType =
feature.valueType as unknown as SelectionStringValueType;
valueType.itemSource.items.forEach((valueItem) => {
if (valueItem.displayText.resourceName === 'Fixed') {
valueItem.displayName = valueItem.displayText.name;
return;
}
valueItem.displayName = $t(
`${valueItem.displayText.resourceName}.${valueItem.displayText.name}`,
);
});
} else {
switch (feature.valueType?.validator.name) {
case 'BOOLEAN': {
feature.value =
String(feature.value).toLocaleLowerCase() === 'true';
break;
}
case 'NUMERIC': {
feature.value = Number(feature.value);
break;
}
}
}
});
});
return groups;
}
function getFeatureInput(groups: FeatureGroupDto[]): UpdateFeaturesDto {
const input: UpdateFeaturesDto = {
features: [],
};
groups.forEach((g) => {
g.features.forEach((f) => {
if (f.value !== null) {
input.features.push({
name: f.name,
value: String(f.value),
});
}
});
});
return input;
}
function createRules(field: string, validator: Validator) {
const featureRules: { [key: string]: any }[] = [];
if (validator.properties) {
switch (validator.name) {
case 'NUMERIC': {
featureRules.push(
...fieldMustBeetWeen({
end: Number(validator.properties.MaxValue),
name: field,
start: Number(validator.properties.MinValue),
trigger: 'change',
}),
);
break;
}
case 'STRING': {
if (
validator.properties.AllowNull &&
validator.properties.AllowNull.toLowerCase() === 'true'
) {
featureRules.push(
...fieldRequired({
name: field,
trigger: 'blur',
}),
);
}
featureRules.push(
...fieldMustBeStringWithMinimumLengthAndMaximumLength({
maximum: Number(validator.properties.MaxLength),
minimum: Number(validator.properties.MinLength),
name: field,
trigger: 'blur',
}),
);
break;
}
default: {
break;
}
}
}
return featureRules;
}
async function onGet() {
try {
modalApi.setState({ loading: true });
const state = modalApi.getData<ModalState>();
const { groups } = await getApi({
providerKey: state.providerKey,
providerName: state.providerName,
});
formModel.value = {
groups: mapFeatures(groups),
};
modelState.value = state;
} finally {
modalApi.setState({ loading: false });
}
}
async function onSubmit() {
try {
modalApi.setState({ submitting: true });
const model = toValue(formModel);
const state = modalApi.getData<ModalState>();
const input = getFeatureInput(model.groups);
await updateApi(
{
providerKey: state.providerKey,
providerName: state.providerName,
},
input,
);
message.success($t('AbpUi.SavedSuccessfully'));
await onGet();
} finally {
modalApi.setState({ submitting: false });
}
}
</script>
<template>
<Modal :title="getModalTitle">
<Form :model="formModel" ref="form">
<Tabs tab-position="left" v-model:active-key="activeTabKey">
<TabPane
v-for="(group, gi) in formModel.groups"
:key="group.name"
:tab="group.displayName"
>
<template v-for="(feature, fi) in group.features" :key="feature.name">
<FormItem
v-if="feature.valueType !== null"
:name="['groups', gi, 'features', fi, 'value']"
:label="feature.displayName"
:extra="feature.description"
:rules="
createRules(feature.displayName, feature.valueType.validator)
"
>
<Checkbox
v-if="
feature.valueType.name === 'ToggleStringValueType' &&
feature.valueType.validator.name === 'BOOLEAN'
"
v-model:checked="feature.value"
>
{{ feature.displayName }}
</Checkbox>
<div
v-else-if="feature.valueType.name === 'FreeTextStringValueType'"
>
<InputNumber
v-if="feature.valueType.validator.name === 'NUMERIC'"
style="width: 100%"
v-model:value="feature.value"
/>
<Input
v-else
v-model:value="feature.value"
autocomplete="off"
/>
</div>
<Select
v-else-if="
feature.valueType.name === 'SelectionStringValueType'
"
v-model:value="feature.value"
:options="feature.valueType.itemSource.items"
:field-names="{ label: 'displayName', value: 'value' }"
/>
</FormItem>
</template>
</TabPane>
</Tabs>
</Form>
</Modal>
</template>
<style scoped></style>

3
apps/vben5/packages/@abp/features/src/components/index.ts

@ -0,0 +1,3 @@
export { default as FeatureDefinitionTable } from './definitions/features/FeatureDefinitionTable.vue';
export { default as FeatureGroupDefinitionTable } from './definitions/groups/FeatureGroupDefinitionTable.vue';
export { default as FeatureModal } from './features/FeatureModal.vue';

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

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

20
apps/vben5/packages/@abp/features/src/constants/permissions.ts

@ -0,0 +1,20 @@
/** 分组权限 */
export const GroupDefinitionsPermissions = {
/** 新增 */
Create: 'FeatureManagement.GroupDefinitions.Create',
Default: 'FeatureManagement.GroupDefinitions',
/** 删除 */
Delete: 'FeatureManagement.GroupDefinitions.Delete',
/** 更新 */
Update: 'FeatureManagement.GroupDefinitions.Update',
};
/** 功能定义权限 */
export const FeatureDefinitionsPermissions = {
/** 新增 */
Create: 'FeatureManagement.Definitions.Create',
Default: 'FeatureManagement.Definitions',
/** 删除 */
Delete: 'FeatureManagement.Definitions.Delete',
/** 更新 */
Update: 'FeatureManagement.Definitions.Update',
};

4
apps/vben5/packages/@abp/features/src/index.ts

@ -0,0 +1,4 @@
export * from './api';
export * from './components';
export * from './constants';
export * from './types';

47
apps/vben5/packages/@abp/features/src/types/definitions.ts

@ -0,0 +1,47 @@
import type { IHasConcurrencyStamp, IHasExtraProperties } from '@abp/core';
interface FeatureDefinitionDto extends IHasExtraProperties {
allowedProviders: string[];
defaultValue?: string;
description?: string;
displayName: string;
groupName: string;
isAvailableToHost: boolean;
isStatic: boolean;
isVisibleToClients: boolean;
name: string;
parentName?: string;
valueType: string;
}
interface FeatureDefinitionGetListInput {
filter?: string;
groupName?: string;
}
interface FeatureDefinitionCreateOrUpdateDto extends IHasExtraProperties {
allowedProviders: string[];
defaultValue?: string;
description?: string;
displayName: string;
isAvailableToHost: boolean;
isVisibleToClients: boolean;
parentName?: string;
valueType: string;
}
interface FeatureDefinitionUpdateDto
extends FeatureDefinitionCreateOrUpdateDto,
IHasConcurrencyStamp {}
interface FeatureDefinitionCreateDto
extends FeatureDefinitionCreateOrUpdateDto {
groupName: string;
name: string;
}
export type {
FeatureDefinitionCreateDto,
FeatureDefinitionDto,
FeatureDefinitionGetListInput,
FeatureDefinitionUpdateDto,
};

63
apps/vben5/packages/@abp/features/src/types/features.ts

@ -0,0 +1,63 @@
import type { Dictionary, NameValue } from '@abp/core';
interface FeatureProvider {
providerKey?: string;
providerName: string;
}
interface IValueValidator {
[key: string]: any;
isValid(value?: any): boolean;
name: string;
properties: Dictionary<string, any>;
}
interface IStringValueType {
[key: string]: any;
name: string;
properties: Dictionary<string, any>;
validator: IValueValidator;
}
interface FeatureProviderDto {
key: string;
name: string;
}
interface FeatureDto {
depth: number;
description?: string;
displayName: string;
name: string;
parentName?: string;
provider: FeatureProviderDto;
value?: any;
valueType: IStringValueType;
}
interface FeatureGroupDto {
displayName: string;
features: FeatureDto[];
name: string;
}
interface GetFeatureListResultDto {
groups: FeatureGroupDto[];
}
type UpdateFeatureDto = NameValue<string>;
interface UpdateFeaturesDto {
features: UpdateFeatureDto[];
}
export type {
FeatureDto,
FeatureGroupDto,
FeatureProvider,
GetFeatureListResultDto,
IStringValueType,
IValueValidator,
UpdateFeatureDto,
UpdateFeaturesDto,
};

31
apps/vben5/packages/@abp/features/src/types/groups.ts

@ -0,0 +1,31 @@
import type { IHasConcurrencyStamp, IHasExtraProperties } from '@abp/core';
interface FeatureGroupDefinitionDto extends IHasExtraProperties {
displayName: string;
isStatic: boolean;
name: string;
}
interface FeatureGroupDefinitionGetListInput {
filter?: string;
}
interface FeatureGroupDefinitionCreateOrUpdateDto extends IHasExtraProperties {
displayName: string;
}
interface FeatureGroupDefinitionUpdateDto
extends FeatureGroupDefinitionCreateOrUpdateDto,
IHasConcurrencyStamp {}
interface FeatureGroupDefinitionCreateDto
extends FeatureGroupDefinitionCreateOrUpdateDto {
name: string;
}
export type {
FeatureGroupDefinitionCreateDto,
FeatureGroupDefinitionDto,
FeatureGroupDefinitionGetListInput,
FeatureGroupDefinitionUpdateDto,
};

3
apps/vben5/packages/@abp/features/src/types/index.ts

@ -0,0 +1,3 @@
export * from './definitions';
export * from './features';
export * from './groups';

6
apps/vben5/packages/@abp/features/tsconfig.json

@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"include": ["src"],
"exclude": ["node_modules"]
}

2
apps/vben5/packages/@abp/gdpr/src/api/index.ts

@ -1 +1 @@
export * from './useGdprRequestsApi';
export { useGdprRequestsApi } from './useGdprRequestsApi';

4
apps/vben5/packages/@abp/platform/src/api/index.ts

@ -1,2 +1,2 @@
export * from './useEmailMessagesApi';
export * from './useSmsMessagesApi';
export { useEmailMessagesApi } from './useEmailMessagesApi';
export { useSmsMessagesApi } from './useSmsMessagesApi';

1
apps/vben5/packages/@abp/saas/package.json

@ -22,6 +22,7 @@
"dependencies": {
"@abp/auditing": "workspace:*",
"@abp/core": "workspace:*",
"@abp/features": "workspace:*",
"@abp/request": "workspace:*",
"@abp/ui": "workspace:*",
"@ant-design/icons-vue": "catalog:",

4
apps/vben5/packages/@abp/saas/src/api/index.ts

@ -1,2 +1,2 @@
export * from './useEditionsApi';
export * from './useTenantsApi';
export { useEditionsApi } from './useEditionsApi';
export { useTenantsApi } from './useTenantsApi';

14
apps/vben5/packages/@abp/saas/src/components/tenants/TenantModal.vue

@ -214,6 +214,13 @@ onMounted(onSearchEditions);
{{ $t('AbpSaas.DisplayName:IsActive') }}
</Checkbox>
</FormItem>
<FormItem name="name" :label="$t('AbpSaas.DisplayName:TenantName')">
<Input
v-model:value="tenant.name"
@change="(e) => onNameChange(e.target.value)"
autocomplete="off"
/>
</FormItem>
<FormItem
v-if="!tenant.id"
name="adminEmailAddress"
@ -237,13 +244,6 @@ onMounted(onSearchEditions);
autocomplete="off"
/>
</FormItem>
<FormItem name="name" :label="$t('AbpSaas.DisplayName:TenantName')">
<Input
v-model:value="tenant.name"
@change="(e) => onNameChange(e.target.value)"
autocomplete="off"
/>
</FormItem>
<FormItem name="editionId" :label="$t('AbpSaas.DisplayName:EditionName')">
<Select
:options="editions"

21
apps/vben5/packages/@abp/saas/src/components/tenants/TenantTable.vue

@ -13,6 +13,7 @@ import { $t } from '@vben/locales';
import { AuditLogPermissions, EntityChangeDrawer } from '@abp/auditing';
import { useFeatures } from '@abp/core';
import { FeatureModal } from '@abp/features';
import { useVbenVxeGrid } from '@abp/ui';
import {
DeleteOutlined,
@ -33,6 +34,7 @@ const CheckIcon = createIconifyIcon('ant-design:check-outlined');
const CloseIcon = createIconifyIcon('ant-design:close-outlined');
const AuditLogIcon = createIconifyIcon('fluent-mdl2:compliance-audit');
const ConnectionIcon = createIconifyIcon('mdi:connection');
const FeatureIcon = createIconifyIcon('pajamas:feature-flag');
const { isEnabled } = useFeatures();
const { hasAccessByCodes } = useAccess();
@ -131,6 +133,9 @@ const [TenantConnectionStringsModal, connectionStringsModalApi] = useVbenModal({
const [TenantChangeDrawer, entityChangeDrawerApi] = useVbenDrawer({
connectedComponent: EntityChangeDrawer,
});
const [TenantFeatureModal, tenantFeatureModalApi] = useVbenModal({
connectedComponent: FeatureModal,
});
const [Grid, { query }] = useVbenVxeGrid({
formOptions,
gridEvents,
@ -179,6 +184,14 @@ const onMenuClick = (row: TenantDto, info: MenuInfo) => {
entityChangeDrawerApi.open();
break;
}
case 'features': {
tenantFeatureModalApi.setData({
displayName: row.name,
providerKey: row.id,
providerName: 'T',
});
tenantFeatureModalApi.open();
}
}
};
</script>
@ -233,6 +246,13 @@ const onMenuClick = (row: TenantDto, info: MenuInfo) => {
>
{{ $t('AbpSaas.ConnectionStrings') }}
</MenuItem>
<MenuItem
v-if="hasAccessByCodes([TenantsPermissions.ManageFeatures])"
key="features"
:icon="h(FeatureIcon)"
>
{{ $t('AbpSaas.ManageFeatures') }}
</MenuItem>
<MenuItem
v-if="
isEnabled('AbpAuditing.Logging.AuditLog') &&
@ -252,6 +272,7 @@ const onMenuClick = (row: TenantDto, info: MenuInfo) => {
</Grid>
<TenantModal @change="() => query()" />
<TenantConnectionStringsModal />
<TenantFeatureModal />
<TenantChangeDrawer />
</template>

1
apps/vben5/packages/@abp/settings/package.json

@ -21,6 +21,7 @@
},
"dependencies": {
"@abp/core": "workspace:*",
"@abp/features": "workspace:*",
"@abp/request": "workspace:*",
"@abp/ui": "workspace:*",
"@ant-design/icons-vue": "catalog:",

23
apps/vben5/packages/@abp/settings/src/components/settings/SettingForm.vue

@ -110,16 +110,19 @@ onMounted(onGet);
<template>
<Card :title="$t('AbpSettingManagement.Settings')">
<template #extra>
<Button
v-if="settingsUpdateInput.settings.length > 0"
:loading="submiting"
class="w-[100px]"
post-icon="ant-design:setting-outlined"
type="primary"
@click="onSubmit"
>
{{ $t('AbpUi.Submit') }}
</Button>
<div class="flex flex-row gap-1">
<slot name="toolbar"></slot>
<Button
v-if="settingsUpdateInput.settings.length > 0"
:loading="submiting"
class="w-[100px]"
post-icon="ant-design:setting-outlined"
type="primary"
@click="onSubmit"
>
{{ $t('AbpUi.Submit') }}
</Button>
</div>
</template>
<Form :label-col="{ span: 5 }" :wrapper-col="{ span: 15 }">
<Tabs v-model="activeTab">

23
apps/vben5/packages/@abp/settings/src/components/settings/SystemSetting.vue

@ -3,9 +3,11 @@ import type { SettingsUpdateInput } from '../../types';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { isEmail, useAbpStore } from '@abp/core';
import { FeatureModal } from '@abp/features';
import { Button, Form, InputSearch, message, Modal } from 'ant-design-vue';
import {
@ -24,6 +26,9 @@ defineOptions({
const FormItem = Form.Item;
const abpStore = useAbpStore();
const [HostFeatureModal, featureModalApi] = useVbenModal({
connectedComponent: FeatureModal,
});
const sending = ref(false);
@ -61,10 +66,27 @@ async function onSendMail(email: string) {
sending.value = false;
}
}
function onFeatureManage() {
featureModalApi.setData({
providerName: 'T',
});
featureModalApi.open();
}
</script>
<template>
<SettingForm :get-api="onGet" :submit-api="onSubmit">
<template #toolbar>
<Button
ghost
post-icon="ant-design:setting-outlined"
type="primary"
@click="onFeatureManage"
>
{{ $t('AbpFeatureManagement.ManageHostFeatures') }}
</Button>
</template>
<template #send-test-email="{ detail }">
<FormItem
:extra="detail.description"
@ -86,6 +108,7 @@ async function onSendMail(email: string) {
</FormItem>
</template>
</SettingForm>
<HostFeatureModal />
</template>
<style scoped></style>

1
apps/vben5/packages/@abp/ui/src/components/index.ts

@ -1,4 +1,5 @@
export { default as LocalizableInput } from './localizable-input/LocalizableInput.vue';
export { default as PropertyTable } from './properties/PropertyTable.vue';
export * from './properties/types';
export * from './string-value-type';
export type * from './vxe-table';

765
apps/vben5/packages/@abp/ui/src/components/string-value-type/StringValueTypeInput.vue

@ -0,0 +1,765 @@
<!-- eslint-disable vue/custom-event-name-casing -->
<script setup lang="ts">
import type { LocalizableStringInfo } from '@abp/core';
import type { RuleObject } from 'ant-design-vue/lib/form';
import type { ColumnsType } from 'ant-design-vue/lib/table';
import type { StringValueTypeInstance } from './interface';
import type { SelectionStringValueItem, StringValueType } from './valueType';
import { computed, reactive, ref, unref, watch } from 'vue';
import { createIconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import {
isNullOrWhiteSpace,
useLocalization,
useLocalizationSerializer,
} from '@abp/core';
import {
Button,
Card,
Checkbox,
Col,
Form,
Input,
InputNumber,
Modal,
Row,
Select,
Table,
} from 'ant-design-vue';
import LocalizableInput from '../localizable-input/LocalizableInput.vue';
import {
AlwaysValidValueValidator,
BooleanValueValidator,
NumericValueValidator,
StringValueValidator,
} from './validator';
import {
FreeTextStringValueType,
SelectionStringValueType,
ToggleStringValueType,
valueTypeSerializer,
} from './valueType';
interface Props {
allowDelete: boolean;
allowEdit: boolean;
disabled: boolean;
value: string;
}
const props = withDefaults(defineProps<Props>(), {
allowDelete: false,
allowEdit: false,
disabled: false,
value: '{}',
});
const emits = defineEmits<{
(event: 'update:value', data: string | undefined): void;
(event: 'change', data: string | undefined): void;
(event: 'change:valueType', data: string): void;
(event: 'change:validator', data: string): void;
(event: 'change:selection', data: SelectionStringValueItem[]): void;
}>();
const FormItemRest = Form.ItemRest;
const FormItem = Form.Item;
const Option = Select.Option;
const DeleteOutlined = createIconifyIcon('ant-design:delete-outlined');
const EditOutlined = createIconifyIcon('ant-design:edit-outlined');
const { Lr } = useLocalization();
const {
deserialize,
serialize,
validate: validateLocalizer,
} = useLocalizationSerializer();
interface Selection {
form: {
editFlag: boolean;
model: any;
rules?: Record<string, RuleObject>;
};
modal: {
maskClosable?: boolean;
minHeight?: number;
onCancel?: (e: MouseEvent) => void;
onOk?: (e: MouseEvent) => void;
title?: string;
visible?: boolean;
width?: number;
};
}
interface State {
selection: Selection;
value?: string;
valueType: StringValueType;
}
const formRef = ref<any>();
const state = reactive<State>({
selection: {
form: {
editFlag: false,
model: {},
rules: {
displayText: {
validator: (_rule, value) => {
if (!validateLocalizer(value)) {
return Promise.reject(
$t(
'component.value_type_nput.type.SELECTION.displayTextNotBeEmpty',
),
);
}
const items = (state.valueType as SelectionStringValueType)
.itemSource.items;
if (items.some((x) => serialize(x.displayText) === value)) {
return Promise.reject(
$t(
'component.value_type_nput.type.SELECTION.duplicateKeyOrValue',
),
);
}
return Promise.resolve();
},
},
value: {
validator: (_rule, value) => {
const items = (state.valueType as SelectionStringValueType)
.itemSource.items;
if (items.some((x) => x.value === value)) {
return Promise.reject(
$t(
'component.value_type_nput.type.SELECTION.duplicateKeyOrValue',
),
);
}
return Promise.resolve();
},
},
},
},
modal: {
maskClosable: false,
minHeight: 400,
onCancel: handleCancel,
onOk: handleSubmit,
title: $t('component.value_type_nput.type.SELECTION.modal.title'),
visible: false,
width: 600,
},
},
valueType: new FreeTextStringValueType(),
});
const getTableColumns = computed(() => {
const columns: ColumnsType = [
{
align: 'left',
dataIndex: 'displayText',
fixed: 'left',
key: 'displayText',
title: $t('component.value_type_nput.type.SELECTION.displayText'),
width: 180,
},
{
align: 'left',
dataIndex: 'value',
fixed: 'left',
key: 'value',
title: $t('component.value_type_nput.type.SELECTION.value'),
width: 200,
},
];
if (!props.disabled) {
columns.push({
align: 'center',
dataIndex: 'action',
fixed: 'right',
key: 'action',
title: $t('component.value_type_nput.type.SELECTION.actions.title'),
width: 220,
});
}
return columns;
});
watch(
() => props.value,
(value) => {
if (isNullOrWhiteSpace(value) || value === '{}') {
state.valueType = new FreeTextStringValueType();
} else {
_formatValueType(value);
}
},
{
immediate: true,
},
);
watch(
() => state.valueType,
(valueType) => {
const isSelection = valueType.name === 'SelectionStringValueType';
if (
isSelection &&
(valueType as SelectionStringValueType).itemSource.items.length < 0
) {
return;
}
state.value = valueTypeSerializer.serialize(valueType);
emits('change:valueType', state.valueType.name);
emits('change:validator', state.valueType.validator.name);
if (isSelection) {
emits(
'change:selection',
(valueType as SelectionStringValueType).itemSource.items,
);
}
},
{
deep: true,
immediate: false,
},
);
watch(
() => state.value,
(value) => {
emits('change', value);
emits('update:value', value);
},
);
const getDisplayName = (displayName: LocalizableStringInfo) => {
return Lr(displayName.resourceName, displayName.name);
};
function validate(value: any) {
if (state.valueType.name === 'SelectionStringValueType') {
const items = (state.valueType as SelectionStringValueType).itemSource
.items;
if (items.length === 0) {
return Promise.reject(
$t('component.value_type_nput.type.SELECTION.itemsNotBeEmpty'),
);
}
if (value && !items.some((item) => item.value === value)) {
return Promise.reject(
$t('component.value_type_nput.type.SELECTION.itemsNotFound'),
);
}
}
if (!state.valueType.validator.isValid(value)) {
return Promise.reject(
$t('component.value_type_nput.validator.isInvalidValue', [
$t(
`component.value_type_nput.validator.${state.valueType.validator.name}.name`,
),
]),
);
}
return Promise.resolve(value);
}
function _formatValueType(valueTypeString: string) {
try {
state.valueType = valueTypeSerializer.deserialize(valueTypeString);
} catch {
console.warn(
'Unable to serialize validator example, check that "valueType" value is valid',
);
}
}
function handleValueTypeChange(type?: string) {
switch (type) {
case 'SELECTION':
case 'SelectionStringValueType': {
state.valueType = new SelectionStringValueType();
break;
}
case 'TOGGLE':
case 'ToggleStringValueType': {
state.valueType = new ToggleStringValueType();
break;
}
default: {
state.valueType = new FreeTextStringValueType();
break;
}
}
}
function handleValidatorChange(validator?: string) {
switch (validator) {
case 'BOOLEAN': {
state.valueType.validator = new BooleanValueValidator();
break;
}
case 'NULL': {
state.valueType.validator = new AlwaysValidValueValidator();
break;
}
case 'NUMERIC': {
state.valueType.validator = new NumericValueValidator();
break;
}
default: {
state.valueType.validator = new StringValueValidator();
break;
}
}
emits('change:validator', state.valueType.validator.name);
}
function handleAddNew() {
state.selection.form.model = {};
state.selection.form.editFlag = false;
state.selection.modal.visible = true;
}
function handleClean() {
const items = (state.valueType as SelectionStringValueType).itemSource.items;
items.length = 0;
emits('change:selection', items);
}
function handleEdit(record: Record<string, any>) {
state.selection.form.model = {
displayText: serialize(record.displayText),
value: record.value,
};
state.selection.form.editFlag = true;
state.selection.modal.visible = true;
}
function handleCancel() {
state.selection.form.model = {};
state.selection.form.editFlag = false;
state.selection.modal.visible = false;
}
function handleSubmit() {
const form = unref(formRef);
form?.validate().then(() => {
const displayText = state.selection.form.model.displayText;
const items = (state.valueType as SelectionStringValueType).itemSource
.items;
if (state.selection.form.editFlag) {
const findIndex = items.findIndex(
(x) => serialize(x.displayText) === displayText,
);
items[findIndex] &&
(items[findIndex].value = state.selection.form.model.value);
} else {
items.push({
displayText: deserialize(displayText),
value: state.selection.form.model.value,
});
}
form.resetFields();
state.selection.modal.visible = false;
emits('change:selection', items);
});
}
function handleDelete(record: Record<string, any>) {
const displayText = serialize(record.displayText);
const items = (state.valueType as SelectionStringValueType).itemSource.items;
const findIndex = items.findIndex(
(x) => serialize(x.displayText) === displayText,
);
items.splice(findIndex, 1);
emits('change:selection', items);
}
defineExpose<StringValueTypeInstance>({
validate,
});
</script>
<template>
<div class="container">
<FormItemRest>
<Card>
<template #title>
<div class="type">
<Row>
<Col :span="11">
<span>{{ $t('component.value_type_nput.type.name') }}</span>
</Col>
<Col :span="2">
<div class="w-full"></div>
</Col>
<Col :span="11">
<span>{{
$t('component.value_type_nput.validator.name')
}}</span>
</Col>
</Row>
<Row>
<Col :span="11">
<Select
:disabled="props.disabled"
:value="state.valueType.name"
@change="(value) => handleValueTypeChange(value?.toString())"
>
<Option value="FreeTextStringValueType">
{{ $t('component.value_type_nput.type.FREE_TEXT.name') }}
</Option>
<Option value="ToggleStringValueType">
{{ $t('component.value_type_nput.type.TOGGLE.name') }}
</Option>
<Option value="SelectionStringValueType">
{{ $t('component.value_type_nput.type.SELECTION.name') }}
</Option>
</Select>
</Col>
<Col :span="2">
<div class="w-full"></div>
</Col>
<Col :span="11">
<Select
:disabled="props.disabled"
:value="state.valueType.validator.name"
@change="(value) => handleValidatorChange(value?.toString())"
>
<Option value="NULL">
{{ $t('component.value_type_nput.validator.NULL.name') }}
</Option>
<Option
value="BOOLEAN"
:disabled="state.valueType.name !== 'ToggleStringValueType'"
>
{{ $t('component.value_type_nput.validator.BOOLEAN.name') }}
</Option>
<Option
value="NUMERIC"
:disabled="
state.valueType.name !== 'FreeTextStringValueType'
"
>
{{ $t('component.value_type_nput.validator.NUMERIC.name') }}
</Option>
<Option
value="STRING"
:disabled="
state.valueType.name !== 'FreeTextStringValueType'
"
>
{{ $t('component.value_type_nput.validator.STRING.name') }}
</Option>
</Select>
</Col>
</Row>
</div>
</template>
<div class="wrap">
<Row>
<Col :span="24">
<div v-if="state.valueType.name === 'FreeTextStringValueType'">
<div
v-if="state.valueType.validator.name === 'NUMERIC'"
class="numeric"
>
<Row>
<Col :span="11">
<span>{{
$t(
'component.value_type_nput.validator.NUMERIC.minValue',
)
}}</span>
</Col>
<Col :span="2">
<div class="w-full"></div>
</Col>
<Col :span="11">
<span>{{
$t(
'component.value_type_nput.validator.NUMERIC.maxValue',
)
}}</span>
</Col>
</Row>
<Row>
<Col :span="11">
<InputNumber
:disabled="props.disabled"
class="w-full"
v-model:value="
(state.valueType.validator as NumericValueValidator)
.minValue
"
/>
</Col>
<Col :span="2">
<div class="w-full"></div>
</Col>
<Col :span="11">
<InputNumber
:disabled="props.disabled"
class="w-full"
v-model:value="
(state.valueType.validator as NumericValueValidator)
.maxValue
"
/>
</Col>
</Row>
</div>
<div
v-else-if="state.valueType.validator.name === 'STRING'"
class="string"
>
<Row style="margin-top: 10px">
<Col :span="24">
<Checkbox
:disabled="props.disabled"
class="w-full"
v-model:checked="
(state.valueType.validator as StringValueValidator)
.allowNull
"
>
{{
$t(
'component.value_type_nput.validator.STRING.allowNull',
)
}}
</Checkbox>
</Col>
</Row>
<Row style="margin-top: 10px">
<Col :span="24">
<span>{{
$t(
'component.value_type_nput.validator.STRING.regularExpression',
)
}}</span>
</Col>
</Row>
<Row>
<Col :span="24">
<Input
:disabled="props.disabled"
class="w-full"
v-model:value="
(state.valueType.validator as StringValueValidator)
.regularExpression
"
/>
</Col>
</Row>
<Row style="margin-top: 10px">
<Col :span="11">
<span>{{
$t(
'component.value_type_nput.validator.STRING.minLength',
)
}}</span>
</Col>
<Col :span="2">
<div class="w-full"></div>
</Col>
<Col :span="11">
<span>{{
$t(
'component.value_type_nput.validator.STRING.maxLength',
)
}}</span>
</Col>
</Row>
<Row>
<Col :span="11">
<InputNumber
:disabled="props.disabled"
class="w-full"
v-model:value="
(state.valueType.validator as StringValueValidator)
.minLength
"
/>
</Col>
<Col :span="2">
<div class="w-full"></div>
</Col>
<Col :span="11">
<InputNumber
:disabled="props.disabled"
class="w-full"
v-model:value="
(state.valueType.validator as StringValueValidator)
.maxLength
"
/>
</Col>
</Row>
</div>
</div>
<div
v-else-if="state.valueType.name === 'SelectionStringValueType'"
>
<Card class="selection">
<template #title>
<Row>
<Col :span="12">
<div
class="valid"
v-if="
(state.valueType as SelectionStringValueType)
.itemSource.items.length <= 0
"
>
<span>{{
$t(
'component.value_type_nput.type.SELECTION.itemsNotBeEmpty',
)
}}</span>
</div>
</Col>
<Col :span="12">
<div class="toolbar" v-if="!props.disabled">
<Button type="primary" @click="handleAddNew">
{{
$t(
'component.value_type_nput.type.SELECTION.actions.create',
)
}}
</Button>
<Button danger @click="handleClean">
{{
$t(
'component.value_type_nput.type.SELECTION.actions.clean',
)
}}
</Button>
</div>
</Col>
</Row>
</template>
<Table
:columns="getTableColumns"
:data-source="
(state.valueType as SelectionStringValueType).itemSource
.items
"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'displayText'">
<span>{{ getDisplayName(record.displayText) }}</span>
</template>
<template v-else-if="column.key === 'action'">
<div class="flex flex-row items-center gap-1">
<Button
v-if="props.allowEdit"
type="link"
@click="() => handleEdit(record)"
>
<template #icon>
<EditOutlined />
</template>
{{
$t(
'component.value_type_nput.type.SELECTION.actions.update',
)
}}
</Button>
<Button
v-if="props.allowDelete"
type="link"
@click="() => handleDelete(record)"
danger
>
<template #icon>
<DeleteOutlined />
</template>
{{
$t(
'component.value_type_nput.type.SELECTION.actions.delete',
)
}}
</Button>
</div>
</template>
</template>
</Table>
</Card>
</div>
</Col>
</Row>
</div>
</Card>
</FormItemRest>
<Modal class="modal" v-bind="state.selection.modal">
<Form
ref="formRef"
class="form"
v-bind="state.selection.form"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<FormItem
name="displayText"
required
:label="$t('component.value_type_nput.type.SELECTION.displayText')"
>
<LocalizableInput
:disabled="props.disabled || state.selection.form.editFlag"
v-model:value="state.selection.form.model.displayText"
/>
</FormItem>
<FormItem
name="value"
required
:label="$t('component.value_type_nput.type.SELECTION.value')"
>
<Input
:disabled="props.disabled"
v-model:value="state.selection.form.model.value"
/>
</FormItem>
</Form>
</Modal>
</div>
</template>
<style lang="less" scoped>
.container {
.type {
width: 100% !important;
min-height: 70px;
}
.wrap {
width: 100% !important;
.selection {
.valid {
color: red;
font-size: 14;
}
.toolbar {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 8px;
> * {
margin-right: 8px;
}
}
}
}
}
.modal {
height: 500px;
.form {
margin: 10px;
}
}
</style>

4
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';

3
apps/vben5/packages/@abp/ui/src/components/string-value-type/interface.ts

@ -0,0 +1,3 @@
export interface StringValueTypeInstance {
validate(value: any): Promise<any>;
}

143
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<string, any>;
}
export class AlwaysValidValueValidator implements ValueValidator {
name = 'NULL';
properties: Dictionary<string, any>;
constructor() {
this.properties = {};
}
isValid(_value?: any): boolean {
return true;
}
}
export class BooleanValueValidator implements ValueValidator {
name = 'BOOLEAN';
properties: Dictionary<string, any>;
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<string, any>;
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<string, any>;
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;
}
}

120
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<string, any>;
validator: ValueValidator;
}
export interface SelectionStringValueItem {
displayText: LocalizableStringInfo;
value: string;
}
export interface SelectionStringValueItemSource {
items: SelectionStringValueItem[];
}
export class FreeTextStringValueType implements StringValueType {
name = 'FreeTextStringValueType';
properties: Dictionary<string, any>;
validator: ValueValidator;
constructor(validator?: ValueValidator) {
this.properties = {};
this.validator = validator ?? new AlwaysValidValueValidator();
}
}
export class ToggleStringValueType implements StringValueType {
name = 'ToggleStringValueType';
properties: Dictionary<string, any>;
validator: ValueValidator;
constructor(validator?: ValueValidator) {
this.properties = {};
this.validator = validator ?? new BooleanValueValidator();
}
}
export class SelectionStringValueType implements StringValueType {
itemSource: SelectionStringValueItemSource;
name = 'SelectionStringValueType';
properties: Dictionary<string, any>;
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();
Loading…
Cancel
Save