Browse Source

feat(vben5): Optimize the custom provider selection options

pull/1470/head
colin 5 months ago
parent
commit
657e57d92d
  1. 21
      apps/vben5/apps/app-antd/src/layouts/basic.vue
  2. 17
      apps/vben5/apps/app-antd/src/store/auth.ts
  3. 9
      apps/vben5/apps/app-antd/src/timezone-init.ts
  4. 18
      apps/vben5/packages/@abp/features/src/api/useFeatureDefinitionsApi.ts
  5. 32
      apps/vben5/packages/@abp/features/src/components/definitions/features/FeatureDefinitionModal.vue
  6. 56
      apps/vben5/packages/@abp/features/src/components/definitions/features/FeatureDefinitionTable.vue
  7. 32
      apps/vben5/packages/@abp/features/src/components/features/FeatureGroup.vue
  8. 133
      apps/vben5/packages/@abp/features/src/components/features/FeatureInput.vue
  9. 47
      apps/vben5/packages/@abp/features/src/components/features/FeatureTreeNode.vue
  10. 188
      apps/vben5/packages/@abp/features/src/components/features/FeaturesModal2.vue
  11. 9
      apps/vben5/packages/@abp/features/src/components/features/tree.ts
  12. 167
      apps/vben5/packages/@abp/features/src/components/features/useFeatureTree.ts
  13. 6
      apps/vben5/packages/@abp/features/src/types/definitions.ts
  14. 18
      apps/vben5/packages/@abp/permissions/src/api/usePermissionDefinitionsApi.ts
  15. 23
      apps/vben5/packages/@abp/permissions/src/components/definitions/permissions/PermissionDefinitionModal.vue
  16. 13
      apps/vben5/packages/@abp/permissions/src/components/definitions/permissions/PermissionDefinitionTable.vue
  17. 8
      apps/vben5/packages/@abp/permissions/src/components/definitions/permissions/types.ts
  18. 18
      apps/vben5/packages/@abp/settings/src/api/useDefinitionsApi.ts
  19. 29
      apps/vben5/packages/@abp/settings/src/components/definitions/SettingDefinitionModal.vue

21
apps/vben5/apps/app-antd/src/layouts/basic.vue

@ -95,14 +95,19 @@ watch(
<template> <template>
<BasicLayout @clear-preferences-and-logout="handleLogout"> <BasicLayout @clear-preferences-and-logout="handleLogout">
<template #user-dropdown> <template #user-dropdown>
<UserDropdown <div class="flex items-center pr-[4px]">
:avatar <UserDropdown
:description="description" :avatar
:menus :description="description"
:tag-text="userInfo?.email" :menus
:text="userInfo?.realName" :tag-text="userInfo?.email"
@logout="handleLogout" :text="userInfo?.realName"
/> @logout="handleLogout"
/>
<span class="mb-1 flex items-center text-sm font-medium">
{{ description }}
</span>
</div>
</template> </template>
<template #notification> <template #notification>
<Notification <Notification

17
apps/vben5/apps/app-antd/src/store/auth.ts

@ -37,6 +37,12 @@ export const useAuthStore = defineStore('auth', () => {
if (user?.refresh_token) { if (user?.refresh_token) {
accessStore.setRefreshToken(user.refresh_token); accessStore.setRefreshToken(user.refresh_token);
} }
try {
const userInfo = await fetchUserInfo();
userStore.setUserInfo(userInfo);
} catch (error) {
console.warn('refresh user iinfo error', error);
}
return newToken; return newToken;
} }
} }
@ -170,7 +176,6 @@ export const useAuthStore = defineStore('auth', () => {
userInfoRes = user.profile; userInfoRes = user.profile;
} }
const abpConfig = await getConfigApi(); const abpConfig = await getConfigApi();
const picture = await getPictureApi();
userInfo = { userInfo = {
userId: userInfoRes.sub ?? abpConfig.currentUser.id, userId: userInfoRes.sub ?? abpConfig.currentUser.id,
username: userInfoRes.uniqueName ?? abpConfig.currentUser.userName, username: userInfoRes.uniqueName ?? abpConfig.currentUser.userName,
@ -178,7 +183,7 @@ export const useAuthStore = defineStore('auth', () => {
userInfoRes.name ?? userInfoRes.name ??
abpConfig.currentUser.name ?? abpConfig.currentUser.name ??
abpConfig.currentUser.userName, abpConfig.currentUser.userName,
avatar: URL.createObjectURL(picture) ?? '', avatar: '',
desc: userInfoRes.uniqueName ?? userInfoRes.name, desc: userInfoRes.uniqueName ?? userInfoRes.name,
email: userInfoRes.email ?? userInfoRes.email, email: userInfoRes.email ?? userInfoRes.email,
emailVerified: emailVerified:
@ -191,6 +196,14 @@ export const useAuthStore = defineStore('auth', () => {
roles: abpConfig.currentUser.roles, roles: abpConfig.currentUser.roles,
homePath: '/', homePath: '/',
}; };
try {
const picture = await getPictureApi();
if (picture) {
userInfo.avatar = URL.createObjectURL(picture);
}
} catch (error) {
console.warn('Error in get user avatar:', error);
}
userStore.setUserInfo(userInfo); userStore.setUserInfo(userInfo);
abpStore.setApplication(abpConfig); abpStore.setApplication(abpConfig);
accessStore.setAccessCodes(Object.keys(abpConfig.auth.grantedPolicies)); accessStore.setAccessCodes(Object.keys(abpConfig.auth.grantedPolicies));

9
apps/vben5/apps/app-antd/src/timezone-init.ts

@ -1,16 +1,21 @@
import { setTimezoneHandler } from '@vben/stores'; import { setTimezoneHandler } from '@vben/stores';
import { useAbpStore } from '@abp/core';
import { useTimeZoneSettingsApi } from '@abp/settings'; import { useTimeZoneSettingsApi } from '@abp/settings';
/** /**
* 初始化时区处理,通过API保存时区设置 * 初始化时区处理,通过API保存时区设置
*/ */
export function initTimezone() { export function initTimezone() {
const abpStore = useAbpStore();
const { getMyTimezoneApi, getTimezonesApi, updateMyTimezoneApi } = const { getMyTimezoneApi, getTimezonesApi, updateMyTimezoneApi } =
useTimeZoneSettingsApi(); useTimeZoneSettingsApi();
setTimezoneHandler({ setTimezoneHandler({
getTimezone() { async getTimezone() {
return getMyTimezoneApi(); if (abpStore.application?.currentUser.isAuthenticated) {
return await getMyTimezoneApi();
}
return undefined;
}, },
setTimezone(timezone: string) { setTimezone(timezone: string) {
return updateMyTimezoneApi(timezone); return updateMyTimezoneApi(timezone);

18
apps/vben5/packages/@abp/features/src/api/useFeatureDefinitionsApi.ts

@ -1,4 +1,4 @@
import type { ListResultDto } from '@abp/core'; import type { ListResultDto, NameValue } from '@abp/core';
import type { import type {
FeatureDefinitionCreateDto, FeatureDefinitionCreateDto,
@ -89,11 +89,27 @@ export function useFeatureDefinitionsApi() {
); );
} }
/**
* 获取可用功能提供者列表
* @returns {Promise<ListResultDto<NameValue<string>>>} 功能提供者列表
*/
function getAssignableProvidersApi(): Promise<
ListResultDto<NameValue<string>>
> {
return request<ListResultDto<NameValue<string>>>(
'/api/feature-management/definitions/assignable-providers',
{
method: 'GET',
},
);
}
return { return {
cancel, cancel,
createApi, createApi,
deleteApi, deleteApi,
getApi, getApi,
getAssignableProvidersApi,
getListApi, getListApi,
updateApi, updateApi,
}; };

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

@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NameValue } from '@abp/core';
import type { import type {
PropertyInfo, PropertyInfo,
SelectionStringValueItem, SelectionStringValueItem,
@ -65,18 +66,17 @@ interface DefinitionTreeVo {
type TabKeys = 'basic' | 'props'; type TabKeys = 'basic' | 'props';
const defaultModel = { const defaultModel: FeatureDefinitionDto = {
allowedProviders: [], allowedProviders: [],
displayName: '', displayName: '',
extraProperties: {}, extraProperties: {},
groupName: '', groupName: '',
isAvailableToHost: true, isAvailableToHost: true,
isEnabled: true,
isStatic: false, isStatic: false,
isVisibleToClients: false, isVisibleToClients: false,
name: '', name: '',
valueType: '', valueType: '',
} as FeatureDefinitionDto; };
const isEditModel = ref(false); const isEditModel = ref(false);
const validatorNameRef = ref<string>('NULL'); const validatorNameRef = ref<string>('NULL');
@ -87,6 +87,7 @@ const valueTypeInput = useTemplateRef<StringValueTypeInstance>('valueType');
const formModel = ref<FeatureDefinitionDto>({ ...defaultModel }); const formModel = ref<FeatureDefinitionDto>({ ...defaultModel });
const availableGroups = ref<FeatureGroupDefinitionDto[]>([]); const availableGroups = ref<FeatureGroupDefinitionDto[]>([]);
const availableDefinitions = ref<DefinitionTreeVo[]>([]); const availableDefinitions = ref<DefinitionTreeVo[]>([]);
const availableProviders = ref<NameValue<string>[]>([]);
const selectionDataSource = ref<{ label: string; value: string }[]>([]); const selectionDataSource = ref<{ label: string; value: string }[]>([]);
const { Lr } = useLocalization(); const { Lr } = useLocalization();
@ -97,6 +98,7 @@ const {
createApi, createApi,
getApi, getApi,
getListApi: getDefinitionsApi, getListApi: getDefinitionsApi,
getAssignableProvidersApi,
updateApi, updateApi,
} = useFeatureDefinitionsApi(); } = useFeatureDefinitionsApi();
const formRules = reactive({ const formRules = reactive({
@ -189,9 +191,11 @@ const [Modal, modalApi] = useVbenModal({
}); });
try { try {
modalApi.setState({ loading: true }); modalApi.setState({ loading: true });
const { groupName, name } = modalApi.getData<FeatureDefinitionDto>(); const { groupName, name, parentName } =
modalApi.getData<FeatureDefinitionDto>();
formModel.value.parentName = parentName;
name && (await onGet(name)); name && (await onGet(name));
await onInitGroups(groupName); await Promise.all([onInitGroups(groupName), onInitProviders()]);
} finally { } finally {
modalApi.setState({ loading: false }); modalApi.setState({ loading: false });
} }
@ -199,6 +203,10 @@ const [Modal, modalApi] = useVbenModal({
}, },
title: $t('AbpFeatureManagement.FeatureDefinitions:AddNew'), title: $t('AbpFeatureManagement.FeatureDefinitions:AddNew'),
}); });
async function onInitProviders() {
const { items } = await getAssignableProvidersApi();
availableProviders.value = items;
}
async function onInitGroups(name?: string) { async function onInitGroups(name?: string) {
const { items } = await getGroupsApi({ filter: name }); const { items } = await getGroupsApi({ filter: name });
availableGroups.value = items.map((group) => { availableGroups.value = items.map((group) => {
@ -411,6 +419,20 @@ function onSelectionChange(items: SelectionStringValueItem[]) {
{{ $t('AbpFeatureManagement.DisplayName:DefaultValue') }} {{ $t('AbpFeatureManagement.DisplayName:DefaultValue') }}
</Checkbox> </Checkbox>
</FormItem> </FormItem>
<FormItem
name="providers"
:label="$t('AbpFeatureManagement.DisplayName:AllowedProviders')"
:extra="$t('AbpFeatureManagement.Description:AllowedProviders')"
>
<Select
v-model:value="formModel.allowedProviders"
:allow-clear="true"
:disabled="formModel.isStatic"
:field-names="{ label: 'name', value: 'value' }"
:options="availableProviders"
mode="multiple"
/>
</FormItem>
<FormItem <FormItem
name="isVisibleToClients" name="isVisibleToClients"
:label="$t('AbpFeatureManagement.DisplayName:IsVisibleToClients')" :label="$t('AbpFeatureManagement.DisplayName:IsVisibleToClients')"

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

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VxeGridListeners, VxeGridProps } from '@abp/ui'; import type { VxeGridListeners, VxeGridProps } from '@abp/ui';
import type { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import type { VbenFormProps } from '@vben/common-ui'; import type { VbenFormProps } from '@vben/common-ui';
@ -7,7 +8,9 @@ import type { FeatureDefinitionDto } from '../../../types/definitions';
import { defineAsyncComponent, h, onMounted, ref } from 'vue'; import { defineAsyncComponent, h, onMounted, ref } from 'vue';
import { useAccess } from '@vben/access';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { createIconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { import {
@ -22,18 +25,21 @@ import {
CloseOutlined, CloseOutlined,
DeleteOutlined, DeleteOutlined,
EditOutlined, EditOutlined,
EllipsisOutlined,
PlusOutlined, PlusOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { Button, message, Modal } from 'ant-design-vue'; import { Button, Dropdown, Menu, message, Modal } from 'ant-design-vue';
import { VxeGrid } from 'vxe-table'; import { VxeGrid } from 'vxe-table';
import { useFeatureDefinitionsApi } from '../../../api/useFeatureDefinitionsApi'; import { useFeatureDefinitionsApi } from '../../../api/useFeatureDefinitionsApi';
import { useFeatureGroupDefinitionsApi } from '../../../api/useFeatureGroupDefinitionsApi'; import { useFeatureGroupDefinitionsApi } from '../../../api/useFeatureGroupDefinitionsApi';
import { GroupDefinitionsPermissions } from '../../../constants/permissions'; import { FeatureDefinitionsPermissions } from '../../../constants/permissions';
defineOptions({ defineOptions({
name: 'FeatureDefinitionTable', name: 'FeatureDefinitionTable',
}); });
const FeaturesOutlined = createIconifyIcon('pajamas:feature-flag');
const MenuItem = Menu.Item;
interface FeatureVo { interface FeatureVo {
children: FeatureVo[]; children: FeatureVo[];
@ -55,6 +61,7 @@ interface FeatureGroupVo {
const featureGroups = ref<FeatureGroupVo[]>([]); const featureGroups = ref<FeatureGroupVo[]>([]);
const { Lr } = useLocalization(); const { Lr } = useLocalization();
const { hasAccessByCodes } = useAccess();
const { deserialize } = useLocalizationSerializer(); const { deserialize } = useLocalizationSerializer();
const { getListApi: getGroupsApi } = useFeatureGroupDefinitionsApi(); const { getListApi: getGroupsApi } = useFeatureGroupDefinitionsApi();
const { deleteApi, getListApi: getFeaturesApi } = useFeatureDefinitionsApi(); const { deleteApi, getListApi: getFeaturesApi } = useFeatureDefinitionsApi();
@ -197,7 +204,7 @@ const subGridColumns: VxeGridProps<FeatureGroupVo>['columns'] = [
fixed: 'right', fixed: 'right',
slots: { default: 'action' }, slots: { default: 'action' },
title: $t('AbpUi.Actions'), title: $t('AbpUi.Actions'),
width: 180, width: 220,
}, },
]; ];
@ -281,6 +288,19 @@ function onDelete(row: FeatureDefinitionDto) {
}); });
} }
function onMenuClick(row: FeatureDefinitionDto, info: MenuInfo) {
switch (info.key) {
case 'features': {
modalApi.setData({
groupName: row.groupName,
parentName: row.name,
});
modalApi.open();
break;
}
}
}
onMounted(onGet); onMounted(onGet);
</script> </script>
@ -290,7 +310,7 @@ onMounted(onGet);
<Button <Button
:icon="h(PlusOutlined)" :icon="h(PlusOutlined)"
type="primary" type="primary"
v-access:code="[GroupDefinitionsPermissions.Create]" v-access:code="[FeatureDefinitionsPermissions.Create]"
@click="onCreate" @click="onCreate"
> >
{{ $t('AbpFeatureManagement.FeatureDefinitions:AddNew') }} {{ $t('AbpFeatureManagement.FeatureDefinitions:AddNew') }}
@ -324,28 +344,44 @@ onMounted(onGet);
<CloseOutlined v-else class="text-red-500" /> <CloseOutlined v-else class="text-red-500" />
</div> </div>
</template> </template>
<template #action="{ row: permission }"> <template #action="{ row: feature }">
<div class="flex flex-row"> <div class="flex flex-row">
<Button <Button
:icon="h(EditOutlined)" :icon="h(EditOutlined)"
block block
type="link" type="link"
v-access:code="[GroupDefinitionsPermissions.Update]" v-access:code="[FeatureDefinitionsPermissions.Update]"
@click="onUpdate(permission)" @click="onUpdate(feature)"
> >
{{ $t('AbpUi.Edit') }} {{ $t('AbpUi.Edit') }}
</Button> </Button>
<Button <Button
v-if="!permission.isStatic" v-if="!feature.isStatic"
:icon="h(DeleteOutlined)" :icon="h(DeleteOutlined)"
block block
danger danger
type="link" type="link"
v-access:code="[GroupDefinitionsPermissions.Delete]" v-access:code="[FeatureDefinitionsPermissions.Delete]"
@click="onDelete(permission)" @click="onDelete(feature)"
> >
{{ $t('AbpUi.Delete') }} {{ $t('AbpUi.Delete') }}
</Button> </Button>
<Dropdown v-if="!feature.isStatic">
<template #overlay>
<Menu @click="(info) => onMenuClick(feature, info)">
<MenuItem
v-if="
hasAccessByCodes([FeatureDefinitionsPermissions.Create])
"
key="features"
:icon="h(FeaturesOutlined)"
>
{{ $t('AbpFeatureManagement.FeatureDefinitions:AddNew') }}
</MenuItem>
</Menu>
</template>
<Button :icon="h(EllipsisOutlined)" type="link" />
</Dropdown>
</div> </div>
</template> </template>
</VxeGrid> </VxeGrid>

32
apps/vben5/packages/@abp/features/src/components/features/FeatureGroup.vue

@ -0,0 +1,32 @@
<!-- components/FeatureGroup.vue -->
<script setup lang="ts">
import type { FeatureDto, FeatureGroupDto } from '../../types/features';
import type { TreeNode } from './tree';
import { Card } from 'ant-design-vue';
import FeatureTreeNode from './FeatureTreeNode.vue';
defineProps<{
baseIndentSize?: number;
group: FeatureGroupDto & { _treeRoots?: TreeNode[] };
groupIndex: number;
}>();
const emit = defineEmits<{
(event: 'change', feature: FeatureDto, groupIndex: number): void;
}>();
</script>
<template>
<Card :bordered="false" :title="group.displayName">
<div v-for="root in group._treeRoots" :key="root.feature.name">
<FeatureTreeNode
:node="root"
:group-index="groupIndex"
:base-indent-size="baseIndentSize"
@change="(feature, idx) => emit('change', feature, idx)"
/>
</div>
</Card>
</template>

133
apps/vben5/packages/@abp/features/src/components/features/FeatureInput.vue

@ -0,0 +1,133 @@
<script setup lang="ts">
import type { Validator } from '@abp/core';
import type { FeatureDto } from '../../types/features';
import { computed } from 'vue';
import { useValidation } from '@abp/core';
import { Checkbox, Form, Input, InputNumber, Select } from 'ant-design-vue';
const props = defineProps<{
feature: FeatureDto;
featureIndex: number;
groupIndex: number;
}>();
const emit = defineEmits<{
(event: 'change', feature: FeatureDto, groupIndex: number): void;
}>();
const FormItem = Form.Item;
const {
fieldMustBeetWeen,
fieldMustBeStringWithMinimumLengthAndMaximumLength,
fieldRequired,
} = useValidation();
function createRules(field: string, validator: Validator) {
const featureRules: 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?.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;
}
}
}
return featureRules;
}
function handleChange() {
emit('change', props.feature, props.groupIndex);
}
const isCheckbox = computed(
() =>
props.feature.valueType?.name === 'ToggleStringValueType' &&
props.feature.valueType?.validator?.name === 'BOOLEAN',
);
const isFreeText = computed(
() => props.feature.valueType?.name === 'FreeTextStringValueType',
);
const isNumeric = computed(
() => props.feature.valueType?.validator?.name === 'NUMERIC',
);
const isSelection = computed(
() => props.feature.valueType?.name === 'SelectionStringValueType',
);
</script>
<template>
<FormItem
v-if="feature.valueType !== null"
:name="['groups', groupIndex, 'features', featureIndex, 'value']"
:label="feature.displayName"
:extra="feature.description"
:rules="createRules(feature.displayName, feature.valueType.validator)"
>
<!-- 复选框类型 -->
<Checkbox
v-if="isCheckbox"
v-model:checked="feature.value"
@change="handleChange"
>
{{ feature.displayName }}
</Checkbox>
<!-- 文本/数字类型 -->
<div v-else-if="isFreeText">
<InputNumber
v-if="isNumeric"
style="width: 100%"
v-model:value="feature.value"
@change="handleChange"
/>
<Input
v-else
v-model:value="feature.value"
autocomplete="off"
@change="handleChange"
/>
</div>
<!-- 选择类型 -->
<Select
v-else-if="isSelection"
v-model:value="feature.value"
:options="feature.valueType.itemSource?.items"
:field-names="{ label: 'displayName', value: 'value' }"
@change="handleChange"
/>
</FormItem>
</template>

47
apps/vben5/packages/@abp/features/src/components/features/FeatureTreeNode.vue

@ -0,0 +1,47 @@
<!-- components/FeatureTreeNode.vue -->
<script setup lang="ts">
import type { FeatureDto } from '../../types/features';
import type { TreeNode } from './tree';
import { computed } from 'vue';
import FeatureInput from './FeatureInput.vue';
import { getIndentStyle } from './useFeatureTree';
const props = defineProps<{
baseIndentSize?: number;
groupIndex: number;
node: TreeNode;
}>();
const emit = defineEmits<{
(event: 'change', feature: FeatureDto, groupIndex: number): void;
}>();
const indentSize = computed(() => props.baseIndentSize || 8);
const indentStyle = computed(() =>
getIndentStyle(props.node.level, indentSize.value),
);
</script>
<template>
<div v-if="node.visible">
<div :style="{ marginLeft: indentStyle }">
<FeatureInput
:feature="node.feature"
:group-index="groupIndex"
:feature-index="0"
@change="(feature, idx) => emit('change', feature, idx)"
/>
</div>
<template v-for="child in node.children" :key="child.feature.name">
<FeatureTreeNode
:node="child"
:group-index="groupIndex"
:base-indent-size="baseIndentSize"
@change="(feature, idx) => emit('change', feature, idx)"
/>
</template>
</div>
</template>

188
apps/vben5/packages/@abp/features/src/components/features/FeaturesModal2.vue

@ -0,0 +1,188 @@
<script setup lang="ts">
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 { Form, message, Tabs } from 'ant-design-vue';
import { useFeaturesApi } from '../../api/useFeaturesApi';
import FeatureGroup from './FeatureGroup.vue';
import {
buildFeatureTree,
onFeatureValueChange,
processAllFeatures,
updateVisibility,
} from './useFeatureTree';
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 [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[]): FeatureGroupDto[] {
groups.forEach((group) => {
// 构建树形结构
const treeRoots = buildFeatureTree(group.features);
// 存储树形结构到 group 中供模板使用
(group as any)._treeRoots = treeRoots;
(group as any)._allFeatures = [...group.features];
// 初始化可见性
treeRoots.forEach((root) => updateVisibility(root, true));
// 处理特性值类型转换
processAllFeatures(treeRoots);
});
return groups;
}
function getFeatureInput(groups: FeatureGroupDto[]): UpdateFeaturesDto {
const input: UpdateFeaturesDto = {
features: [],
};
groups.forEach((g) => {
const features = (g as any)._allFeatures || g.features;
features.forEach((f: any) => {
if (f.value !== null && f.value !== undefined && f.value !== '') {
input.features.push({
name: f.name,
value: String(f.value),
});
}
});
});
return input;
}
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 });
}
}
function handleFeatureChange(feature: any, groupIndex: number) {
const group = formModel.value.groups[groupIndex];
const treeRoots = (group as any)._treeRoots;
if (treeRoots) {
onFeatureValueChange(feature, treeRoots);
// 强制更新视图
formModel.value = { ...formModel.value };
}
}
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" type="card" v-model:active-key="activeTabKey">
<TabPane
v-for="(group, gi) in formModel.groups"
:key="group.name"
:tab="group.displayName"
>
<FeatureGroup
:group="group"
:group-index="gi"
:base-indent-size="8"
@change="handleFeatureChange"
/>
</TabPane>
</Tabs>
</Form>
</Modal>
</template>
<style lang="scss" scoped>
:deep(.ant-tabs) {
height: 34rem;
.ant-tabs-nav {
width: 14rem;
}
.ant-tabs-content-holder {
overflow: hidden auto !important;
}
}
:deep(.ant-form-item) {
margin-bottom: 16px;
}
</style>

9
apps/vben5/packages/@abp/features/src/components/features/tree.ts

@ -0,0 +1,9 @@
import type { FeatureDto } from '../../types/features';
export interface TreeNode {
children: TreeNode[];
feature: FeatureDto;
level: number;
parent?: TreeNode;
visible: boolean;
}

167
apps/vben5/packages/@abp/features/src/components/features/useFeatureTree.ts

@ -0,0 +1,167 @@
import type { FeatureDto } from '../../types/features';
import type { TreeNode } from './tree';
import { $t } from '@vben/locales';
/**
* 将扁平的特性列表转换为树形结构
*/
export function buildFeatureTree(features: FeatureDto[]): TreeNode[] {
const nodeMap = new Map<string, TreeNode>();
const roots: TreeNode[] = [];
// 创建所有节点
features.forEach((feature) => {
const node: TreeNode = {
feature,
children: [],
level: 0,
visible: true,
};
nodeMap.set(feature.name, node);
});
// 建立父子关系
features.forEach((feature) => {
const node = nodeMap.get(feature.name);
if (node && feature.parentName && nodeMap.has(feature.parentName)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const parent = nodeMap.get(feature.parentName)!;
parent.children.push(node);
node.parent = parent;
node.level = parent.level + 1;
} else if (node && !feature.parentName) {
roots.push(node);
}
});
// 对子节点进行排序
const sortChildren = (nodes: TreeNode[]) => {
nodes.forEach((node) => {
if (node.children.length > 0) {
node.children.sort((a, b) =>
a.feature.displayName.localeCompare(b.feature.displayName),
);
sortChildren(node.children);
}
});
};
sortChildren(roots);
return roots;
}
/**
* 更新节点的可见性(基于父级状态)
*/
export function updateVisibility(
node: TreeNode,
parentVisible: boolean = true,
): void {
node.visible = parentVisible;
if (isCheckboxFeature(node.feature)) {
const isChecked = Boolean(node.feature.value);
node.children.forEach((child) => {
updateVisibility(child, parentVisible && isChecked);
});
} else {
node.children.forEach((child) => {
updateVisibility(child, parentVisible);
});
}
}
/**
* 检查是否为复选框类型的特性
*/
export function isCheckboxFeature(feature: FeatureDto): boolean {
return (
feature.valueType?.name === 'ToggleStringValueType' &&
feature.valueType?.validator?.name === 'BOOLEAN'
);
}
/**
* 当特性值改变时,更新其子节点的可见性
*/
export function onFeatureValueChange(
feature: FeatureDto,
treeNodes: TreeNode[],
): void {
const findNode = (nodes: TreeNode[]): null | TreeNode => {
for (const node of nodes) {
if (node.feature.name === feature.name) {
return node;
}
const found = findNode(node.children);
if (found) return found;
}
return null;
};
const node = findNode(treeNodes);
if (node && isCheckboxFeature(node.feature)) {
updateVisibility(node, true);
}
}
/**
* 处理特性值类型转换
*/
export function processFeatureValue(feature: FeatureDto): void {
if (feature.valueType?.name === 'SelectionStringValueType') {
const valueType = feature.valueType as any;
if (valueType.itemSource?.items) {
valueType.itemSource.items.forEach((valueItem: any) => {
if (valueItem.displayText?.resourceName === 'Fixed') {
valueItem.displayName = valueItem.displayText.name;
} else if (
valueItem.displayText?.resourceName &&
valueItem.displayText?.name
) {
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': {
if (
feature.value !== null &&
feature.value !== undefined &&
feature.value !== ''
) {
feature.value = Number(feature.value);
}
break;
}
}
}
}
/**
* 递归处理所有特性值
*/
export function processAllFeatures(treeNodes: TreeNode[]): void {
const traverse = (nodes: TreeNode[]) => {
nodes.forEach((node) => {
processFeatureValue(node.feature);
traverse(node.children);
});
};
traverse(treeNodes);
}
/**
* 获取缩进样式
*/
export function getIndentStyle(level: number, indentSize: number = 8): string {
return `${level * indentSize}px`;
}

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

@ -31,10 +31,8 @@ interface FeatureDefinitionCreateOrUpdateDto extends IHasExtraProperties {
} }
interface FeatureDefinitionUpdateDto interface FeatureDefinitionUpdateDto
extends FeatureDefinitionCreateOrUpdateDto, extends FeatureDefinitionCreateOrUpdateDto, IHasConcurrencyStamp {}
IHasConcurrencyStamp {} interface FeatureDefinitionCreateDto extends FeatureDefinitionCreateOrUpdateDto {
interface FeatureDefinitionCreateDto
extends FeatureDefinitionCreateOrUpdateDto {
groupName: string; groupName: string;
name: string; name: string;
} }

18
apps/vben5/packages/@abp/permissions/src/api/usePermissionDefinitionsApi.ts

@ -1,4 +1,4 @@
import type { ListResultDto } from '@abp/core'; import type { ListResultDto, NameValue } from '@abp/core';
import type { import type {
PermissionDefinitionCreateDto, PermissionDefinitionCreateDto,
@ -89,11 +89,27 @@ export function usePermissionDefinitionsApi() {
); );
} }
/**
* 获取可用权限提供者列表
* @returns {Promise<ListResultDto<NameValue<string>>>} 权限提供者列表
*/
function getAssignableProvidersApi(): Promise<
ListResultDto<NameValue<string>>
> {
return request<ListResultDto<NameValue<string>>>(
'/api/permission-management/definitions/assignable-providers',
{
method: 'GET',
},
);
}
return { return {
cancel, cancel,
createApi, createApi,
deleteApi, deleteApi,
getApi, getApi,
getAssignableProvidersApi,
getListApi, getListApi,
updateApi, updateApi,
}; };

23
apps/vben5/packages/@abp/permissions/src/components/definitions/permissions/PermissionDefinitionModal.vue

@ -1,5 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { IHasSimpleStateCheckers, ISimpleStateChecker } from '@abp/core'; import type {
IHasSimpleStateCheckers,
ISimpleStateChecker,
NameValue,
} from '@abp/core';
import type { PropertyInfo } from '@abp/ui'; import type { PropertyInfo } from '@abp/ui';
import type { FormInstance } from 'ant-design-vue'; import type { FormInstance } from 'ant-design-vue';
@ -41,8 +45,7 @@ const emits = defineEmits<{
const FormItem = Form.Item; const FormItem = Form.Item;
const TabPane = Tabs.TabPane; const TabPane = Tabs.TabPane;
interface PermissionStateChecker interface PermissionStateChecker extends IHasSimpleStateCheckers<PermissionStateChecker> {}
extends IHasSimpleStateCheckers<PermissionStateChecker> {}
class PermissionState implements PermissionStateChecker { class PermissionState implements PermissionStateChecker {
stateCheckers: ISimpleStateChecker<PermissionStateChecker>[] = []; stateCheckers: ISimpleStateChecker<PermissionStateChecker>[] = [];
@ -66,9 +69,10 @@ const isEditModel = ref(false);
const activeTab = ref<TabKeys>('basic'); const activeTab = ref<TabKeys>('basic');
const form = useTemplateRef<FormInstance>('form'); const form = useTemplateRef<FormInstance>('form');
const formModel = ref<PermissionDefinitionDto>({ ...defaultModel }); const formModel = ref<PermissionDefinitionDto>({ ...defaultModel });
const { multiTenancySideOptions, providerOptions } = useTypesMap(); const { multiTenancySideOptions } = useTypesMap();
const availableGroups = ref<PermissionGroupDefinitionDto[]>([]); const availableGroups = ref<PermissionGroupDefinitionDto[]>([]);
const availablePermissions = ref<PermissionTreeVo[]>([]); const availablePermissions = ref<PermissionTreeVo[]>([]);
const availableProviders = ref<NameValue<string>[]>([]);
const { Lr } = useLocalization(); const { Lr } = useLocalization();
const { deserialize } = useLocalizationSerializer(); const { deserialize } = useLocalizationSerializer();
@ -77,6 +81,7 @@ const {
createApi, createApi,
getApi, getApi,
getListApi: getPermissionsApi, getListApi: getPermissionsApi,
getAssignableProvidersApi,
updateApi, updateApi,
} = usePermissionDefinitionsApi(); } = usePermissionDefinitionsApi();
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
@ -118,7 +123,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.setState({ loading: true }); modalApi.setState({ loading: true });
const { groupName, name } = modalApi.getData<PermissionDefinitionDto>(); const { groupName, name } = modalApi.getData<PermissionDefinitionDto>();
name && (await onGet(name)); name && (await onGet(name));
await onInitGroups(groupName); await Promise.all([onInitGroups(groupName), onInitProviders()]);
} finally { } finally {
modalApi.setState({ loading: false }); modalApi.setState({ loading: false });
} }
@ -126,6 +131,10 @@ const [Modal, modalApi] = useVbenModal({
}, },
title: $t('AbpPermissionManagement.PermissionDefinitions:AddNew'), title: $t('AbpPermissionManagement.PermissionDefinitions:AddNew'),
}); });
async function onInitProviders() {
const { items } = await getAssignableProvidersApi();
availableProviders.value = items;
}
async function onInitGroups(name?: string) { async function onInitGroups(name?: string) {
const { items } = await getGroupsApi({ filter: name }); const { items } = await getGroupsApi({ filter: name });
availableGroups.value = items.map((group) => { availableGroups.value = items.map((group) => {
@ -268,13 +277,15 @@ function onPropDelete(prop: PropertyInfo) {
</FormItem> </FormItem>
<FormItem <FormItem
:label="$t('AbpPermissionManagement.DisplayName:Providers')" :label="$t('AbpPermissionManagement.DisplayName:Providers')"
:extra="$t('AbpPermissionManagement.Description:Providers')"
name="providers" name="providers"
> >
<Select <Select
v-model:value="formModel.providers" v-model:value="formModel.providers"
:allow-clear="true" :allow-clear="true"
:disabled="formModel.isStatic" :disabled="formModel.isStatic"
:options="providerOptions" :field-names="{ label: 'name', value: 'value' }"
:options="availableProviders"
mode="multiple" mode="multiple"
/> />
</FormItem> </FormItem>

13
apps/vben5/packages/@abp/permissions/src/components/definitions/permissions/PermissionDefinitionTable.vue

@ -3,9 +3,6 @@ import type { VxeGridListeners, VxeGridProps } from '@abp/ui';
import type { VbenFormProps } from '@vben/common-ui'; import type { VbenFormProps } from '@vben/common-ui';
import type { PermissionDefinitionDto } from '../../../types/definitions';
import type { PermissionGroupDefinitionDto } from '../../../types/groups';
import { defineAsyncComponent, h, onMounted, ref } from 'vue'; import { defineAsyncComponent, h, onMounted, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
@ -82,7 +79,7 @@ const formOptions: VbenFormProps = {
submitOnEnter: true, submitOnEnter: true,
}; };
const gridOptions: VxeGridProps<PermissionGroupDefinitionDto> = { const gridOptions: VxeGridProps<PermissionGroupVo> = {
columns: [ columns: [
{ {
align: 'center', align: 'center',
@ -150,7 +147,7 @@ const gridOptions: VxeGridProps<PermissionGroupDefinitionDto> = {
zoom: true, zoom: true,
}, },
}; };
const subGridColumns: VxeGridProps<PermissionDefinitionDto>['columns'] = [ const subGridColumns: VxeGridProps<PermissionVo>['columns'] = [
{ {
align: 'center', align: 'center',
type: 'seq', type: 'seq',
@ -200,7 +197,7 @@ const subGridColumns: VxeGridProps<PermissionDefinitionDto>['columns'] = [
}, },
]; ];
const gridEvents: VxeGridListeners<PermissionGroupDefinitionDto> = { const gridEvents: VxeGridListeners<PermissionGroupVo> = {
sortChange: () => { sortChange: () => {
gridApi.query(); gridApi.query();
}, },
@ -263,12 +260,12 @@ function onCreate() {
modalApi.open(); modalApi.open();
} }
function onUpdate(row: PermissionDefinitionDto) { function onUpdate(row: PermissionGroupVo) {
modalApi.setData(row); modalApi.setData(row);
modalApi.open(); modalApi.open();
} }
function onDelete(row: PermissionDefinitionDto) { function onDelete(row: PermissionGroupVo) {
Modal.confirm({ Modal.confirm({
centered: true, centered: true,
content: `${$t('AbpUi.ItemWillBeDeletedMessageWithFormat', [row.name])}`, content: `${$t('AbpUi.ItemWillBeDeletedMessageWithFormat', [row.name])}`,

8
apps/vben5/packages/@abp/permissions/src/components/definitions/permissions/types.ts

@ -37,17 +37,9 @@ export function useTypesMap() {
U: $t('AbpPermissionManagement.Providers:User'), U: $t('AbpPermissionManagement.Providers:User'),
}; };
const providerOptions = [
{ label: providersMap.R, value: 'R' },
{ label: providersMap.U, value: 'U' },
{ label: providersMap.O, value: 'O' },
{ label: providersMap.C, value: 'C' },
];
return { return {
multiTenancySideOptions, multiTenancySideOptions,
multiTenancySidesMap, multiTenancySidesMap,
providerOptions,
providersMap, providersMap,
}; };
} }

18
apps/vben5/packages/@abp/settings/src/api/useDefinitionsApi.ts

@ -1,4 +1,4 @@
import type { ListResultDto } from '@abp/core'; import type { ListResultDto, NameValue } from '@abp/core';
import type { import type {
SettingDefinitionCreateDto, SettingDefinitionCreateDto,
@ -88,11 +88,27 @@ export function useDefinitionsApi() {
); );
} }
/**
* 获取可用设置提供者列表
* @returns {Promise<ListResultDto<NameValue<string>>>} 设置提供者列表
*/
function getAssignableProvidersApi(): Promise<
ListResultDto<NameValue<string>>
> {
return request<ListResultDto<NameValue<string>>>(
'/api/permission-management/definitions/assignable-providers',
{
method: 'GET',
},
);
}
return { return {
cancel, cancel,
createApi, createApi,
deleteApi, deleteApi,
getApi, getApi,
getAssignableProvidersApi,
getListApi, getListApi,
updateApi, updateApi,
}; };

29
apps/vben5/packages/@abp/settings/src/components/definitions/SettingDefinitionModal.vue

@ -1,17 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NameValue } from '@abp/core';
import type { PropertyInfo } from '@abp/ui'; import type { PropertyInfo } from '@abp/ui';
import type { FormInstance } from 'ant-design-vue'; import type { FormInstance } from 'ant-design-vue';
import type { SettingDefinitionDto } from '../../types/definitions'; import type { SettingDefinitionDto } from '../../types/definitions';
import { import { defineEmits, defineOptions, ref, toValue, useTemplateRef } from 'vue';
defineEmits,
defineOptions,
reactive,
ref,
toValue,
useTemplateRef,
} from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
@ -47,15 +41,10 @@ const isEditModel = ref(false);
const activeTab = ref<TabKeys>('basic'); const activeTab = ref<TabKeys>('basic');
const form = useTemplateRef<FormInstance>('form'); const form = useTemplateRef<FormInstance>('form');
const formModel = ref<SettingDefinitionDto>({ ...defaultModel }); const formModel = ref<SettingDefinitionDto>({ ...defaultModel });
const providerOptions = reactive([ const availableProviders = ref<NameValue<string>[]>([]);
{ label: $t('AbpSettingManagement.Providers:Default'), value: 'D' },
{ label: $t('AbpSettingManagement.Providers:Configuration'), value: 'C' },
{ label: $t('AbpSettingManagement.Providers:Global'), value: 'G' },
{ label: $t('AbpSettingManagement.Providers:Tenant'), value: 'T' },
{ label: $t('AbpSettingManagement.Providers:User'), value: 'U' },
]);
const { createApi, getApi, updateApi } = useDefinitionsApi(); const { createApi, getApi, getAssignableProvidersApi, updateApi } =
useDefinitionsApi();
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
class: 'w-1/2', class: 'w-1/2',
@ -93,6 +82,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.setState({ loading: true }); modalApi.setState({ loading: true });
const { name } = modalApi.getData<SettingDefinitionDto>(); const { name } = modalApi.getData<SettingDefinitionDto>();
name && (await onGet(name)); name && (await onGet(name));
await onInitProviders();
} finally { } finally {
modalApi.setState({ loading: false }); modalApi.setState({ loading: false });
} }
@ -100,6 +90,10 @@ const [Modal, modalApi] = useVbenModal({
}, },
title: $t('AbpSettingManagement.Definition:AddNew'), title: $t('AbpSettingManagement.Definition:AddNew'),
}); });
async function onInitProviders() {
const { items } = await getAssignableProvidersApi();
availableProviders.value = items;
}
async function onGet(name: string) { async function onGet(name: string) {
isEditModel.value = true; isEditModel.value = true;
const dto = await getApi(name); const dto = await getApi(name);
@ -180,7 +174,8 @@ function onPropDelete(prop: PropertyInfo) {
v-model:value="formModel.providers" v-model:value="formModel.providers"
:allow-clear="true" :allow-clear="true"
:disabled="formModel.isStatic" :disabled="formModel.isStatic"
:options="providerOptions" :field-names="{ label: 'name', value: 'value' }"
:options="availableProviders"
mode="multiple" mode="multiple"
/> />
</FormItem> </FormItem>

Loading…
Cancel
Save