mirror of https://github.com/halo-dev/halo
feat: add extension points for user detail tab (#5763)
#### What type of PR is this? /area ui /kind feature /milestone 2.15.x #### What this PR does / why we need it: 为 Console 的用户详情页面的选项卡和个人中心的个人资料页面选项卡添加扩展点,支持通过插件扩展选项卡。 todo: - [x] 完善 docs.halo.run 的开发文档 https://github.com/halo-dev/docs/pull/340 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/5745 #### Special notes for your reviewer: 可以使用 [plugin-starter-1.3.2-SNAPSHOT.jar.zip](https://github.com/halo-dev/halo/files/15059291/plugin-starter-1.3.2-SNAPSHOT.jar.zip) 进行测试。 #### Does this PR introduce a user-facing change? ```release-note 为 Console 的用户详情页面的选项卡和个人中心的个人资料页面选项卡添加扩展点 ```pull/5794/head
parent
e4b2e07cc8
commit
fbf2b06432
|
@ -2,40 +2,36 @@
|
|||
import { apiClient } from "@/utils/api-client";
|
||||
import {
|
||||
VButton,
|
||||
VTabbar,
|
||||
VDropdown,
|
||||
VDropdownItem,
|
||||
VTabbar,
|
||||
} from "@halo-dev/components";
|
||||
import { computed, provide, ref, type Ref } from "vue";
|
||||
import {
|
||||
computed,
|
||||
markRaw,
|
||||
onMounted,
|
||||
provide,
|
||||
type Ref,
|
||||
ref,
|
||||
toRaw,
|
||||
} from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import type { DetailedUser } from "@halo-dev/api-client";
|
||||
import UserEditingModal from "./components/UserEditingModal.vue";
|
||||
import UserPasswordChangeModal from "./components/UserPasswordChangeModal.vue";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import UserAvatar from "@/components/user-avatar/UserAvatar.vue";
|
||||
import type { Raw } from "vue";
|
||||
import type { Component } from "vue";
|
||||
import { markRaw } from "vue";
|
||||
import DetailTab from "./tabs/Detail.vue";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import type { PluginModule, UserTab } from "@halo-dev/console-shared";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
const { currentUser } = useUserStore();
|
||||
|
||||
interface UserTab {
|
||||
id: string;
|
||||
label: string;
|
||||
component: Raw<Component>;
|
||||
props?: Record<string, unknown>;
|
||||
permissions?: string[];
|
||||
priority: number;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
const editingModal = ref(false);
|
||||
const passwordChangeModal = ref(false);
|
||||
|
||||
|
@ -56,25 +52,44 @@ const {
|
|||
enabled: computed(() => !!params.name),
|
||||
});
|
||||
|
||||
provide<Ref<DetailedUser | undefined>>("user", user);
|
||||
|
||||
const tabs: UserTab[] = [
|
||||
const tabs = ref<UserTab[]>([
|
||||
{
|
||||
id: "detail",
|
||||
label: t("core.user.detail.tabs.detail"),
|
||||
component: markRaw(DetailTab),
|
||||
priority: 10,
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
const activeTab = useRouteQuery<string>("tab", tabs[0].id, {
|
||||
// Collect user:detail:tabs:create extension points
|
||||
onMounted(() => {
|
||||
const { pluginModules } = usePluginModuleStore();
|
||||
|
||||
pluginModules.forEach((pluginModule: PluginModule) => {
|
||||
const { extensionPoints } = pluginModule;
|
||||
if (!extensionPoints?.["user:detail:tabs:create"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const providers = extensionPoints["user:detail:tabs:create"]() as UserTab[];
|
||||
|
||||
tabs.value.push(...providers);
|
||||
});
|
||||
});
|
||||
|
||||
const activeTab = useRouteQuery<string>("tab", tabs.value[0].id, {
|
||||
mode: "push",
|
||||
});
|
||||
|
||||
provide<Ref<string>>("activeTab", activeTab);
|
||||
|
||||
const tabbarItems = computed(() => {
|
||||
return tabs.map((tab) => ({ id: tab.id, label: tab.label }));
|
||||
return toRaw(tabs)
|
||||
.value.sort((a, b) => a.priority - b.priority)
|
||||
.map((tab) => ({
|
||||
id: tab.id,
|
||||
label: tab.label,
|
||||
}));
|
||||
});
|
||||
|
||||
function handleRouteToUC() {
|
||||
|
@ -142,7 +157,8 @@ function handleRouteToUC() {
|
|||
<template v-for="tab in tabs" :key="tab.id">
|
||||
<component
|
||||
:is="tab.component"
|
||||
v-if="activeTab === tab.id && !tab.hidden"
|
||||
v-if="activeTab === tab.id"
|
||||
:user="user"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -5,13 +5,13 @@ import {
|
|||
VDescriptionItem,
|
||||
VTag,
|
||||
} from "@halo-dev/components";
|
||||
import type { Ref } from "vue";
|
||||
import { inject } from "vue";
|
||||
import type { DetailedUser } from "@halo-dev/api-client";
|
||||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
|
||||
const user = inject<Ref<DetailedUser | undefined>>("user");
|
||||
withDefaults(defineProps<{ user?: DetailedUser }>(), {
|
||||
user: undefined,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="border-t border-gray-100">
|
||||
|
|
|
@ -11,3 +11,4 @@ export * from "./states/plugin-installation-tabs";
|
|||
export * from "./states/entity";
|
||||
export * from "./states/theme-list-tabs";
|
||||
export * from "./states/operation";
|
||||
export * from "./states/user-tab";
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import type { Component, Raw } from "vue";
|
||||
|
||||
export interface UserTab {
|
||||
id: string;
|
||||
label: string;
|
||||
component: Raw<Component>;
|
||||
permissions?: string[];
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface UserProfileTab {
|
||||
id: string;
|
||||
label: string;
|
||||
component: Raw<Component>;
|
||||
permissions?: string[];
|
||||
priority: number;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import type { Component, Ref } from "vue";
|
||||
import type { RouteRecordRaw, RouteRecordName } from "vue-router";
|
||||
import type { RouteRecordName, RouteRecordRaw } from "vue-router";
|
||||
import type { FunctionalPage } from "../states/pages";
|
||||
import type { AttachmentSelectProvider } from "../states/attachment-selector";
|
||||
import type { EditorProvider, PluginTab } from "..";
|
||||
|
@ -17,6 +17,7 @@ import type {
|
|||
Plugin,
|
||||
Theme,
|
||||
} from "@halo-dev/api-client";
|
||||
import type { UserProfileTab, UserTab } from "@/states/user-tab";
|
||||
|
||||
export interface RouteRecordAppend {
|
||||
parentName: RouteRecordName;
|
||||
|
@ -76,6 +77,12 @@ export interface ExtensionPoint {
|
|||
"theme:list-item:operation:create"?: (
|
||||
theme: Ref<Theme>
|
||||
) => OperationItem<Theme>[] | Promise<OperationItem<Theme>[]>;
|
||||
|
||||
"user:detail:tabs:create"?: () => UserTab[] | Promise<UserTab[]>;
|
||||
|
||||
"uc:user:profile:tabs:create"?: () =>
|
||||
| UserProfileTab[]
|
||||
| Promise<UserProfileTab[]>;
|
||||
}
|
||||
|
||||
export interface PluginModule {
|
||||
|
|
|
@ -2,38 +2,35 @@
|
|||
import { apiClient } from "@/utils/api-client";
|
||||
import {
|
||||
VButton,
|
||||
VTabbar,
|
||||
VDropdown,
|
||||
VDropdownItem,
|
||||
VTabbar,
|
||||
} from "@halo-dev/components";
|
||||
import { computed, provide, ref, type Ref } from "vue";
|
||||
import {
|
||||
computed,
|
||||
markRaw,
|
||||
onMounted,
|
||||
provide,
|
||||
type Ref,
|
||||
ref,
|
||||
toRaw,
|
||||
} from "vue";
|
||||
import type { DetailedUser } from "@halo-dev/api-client";
|
||||
import ProfileEditingModal from "./components/ProfileEditingModal.vue";
|
||||
import PasswordChangeModal from "./components/PasswordChangeModal.vue";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import UserAvatar from "@/components/user-avatar/UserAvatar.vue";
|
||||
import type { Raw } from "vue";
|
||||
import type { Component } from "vue";
|
||||
import { markRaw } from "vue";
|
||||
import DetailTab from "./tabs/Detail.vue";
|
||||
import PersonalAccessTokensTab from "./tabs/PersonalAccessTokens.vue";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import NotificationPreferences from "./tabs/NotificationPreferences.vue";
|
||||
import TwoFactor from "./tabs/TwoFactor.vue";
|
||||
import type { PluginModule, UserProfileTab } from "@halo-dev/console-shared";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface UserTab {
|
||||
id: string;
|
||||
label: string;
|
||||
component: Raw<Component>;
|
||||
props?: Record<string, unknown>;
|
||||
permissions?: string[];
|
||||
priority: number;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
const editingModal = ref(false);
|
||||
const passwordChangeModal = ref(false);
|
||||
|
||||
|
@ -51,7 +48,7 @@ const {
|
|||
|
||||
provide<Ref<DetailedUser | undefined>>("user", user);
|
||||
|
||||
const tabs: UserTab[] = [
|
||||
const tabs = ref<UserProfileTab[]>([
|
||||
{
|
||||
id: "detail",
|
||||
label: t("core.uc_profile.tabs.detail"),
|
||||
|
@ -76,13 +73,36 @@ const tabs: UserTab[] = [
|
|||
component: markRaw(TwoFactor),
|
||||
priority: 40,
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
const tabbarItems = computed(() => {
|
||||
return tabs.map((tab) => ({ id: tab.id, label: tab.label }));
|
||||
// Collect uc:profile:tabs:create extension points
|
||||
onMounted(() => {
|
||||
const { pluginModules } = usePluginModuleStore();
|
||||
|
||||
pluginModules.forEach((pluginModule: PluginModule) => {
|
||||
const { extensionPoints } = pluginModule;
|
||||
if (!extensionPoints?.["uc:user:profile:tabs:create"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const providers = extensionPoints[
|
||||
"uc:user:profile:tabs:create"
|
||||
]() as UserProfileTab[];
|
||||
|
||||
tabs.value.push(...providers);
|
||||
});
|
||||
});
|
||||
|
||||
const activeTab = useRouteQuery<string>("tab", tabs[0].id, {
|
||||
const tabbarItems = computed(() => {
|
||||
return toRaw(tabs)
|
||||
.value.sort((a, b) => a.priority - b.priority)
|
||||
.map((tab) => ({
|
||||
id: tab.id,
|
||||
label: tab.label,
|
||||
}));
|
||||
});
|
||||
|
||||
const activeTab = useRouteQuery<string>("tab", tabs.value[0].id, {
|
||||
mode: "push",
|
||||
});
|
||||
</script>
|
||||
|
@ -140,7 +160,8 @@ const activeTab = useRouteQuery<string>("tab", tabs[0].id, {
|
|||
<template v-for="tab in tabs" :key="tab.id">
|
||||
<component
|
||||
:is="tab.component"
|
||||
v-if="activeTab === tab.id && !tab.hidden"
|
||||
v-if="activeTab === tab.id"
|
||||
:user="user"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -8,8 +8,7 @@ import {
|
|||
VDescriptionItem,
|
||||
VTag,
|
||||
} from "@halo-dev/components";
|
||||
import type { Ref } from "vue";
|
||||
import { inject, computed } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import type { DetailedUser, ListedAuthProvider } from "@halo-dev/api-client";
|
||||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
|
@ -18,9 +17,8 @@ import { apiClient } from "@/utils/api-client";
|
|||
import axios from "axios";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import EmailVerifyModal from "../components/EmailVerifyModal.vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
const user = inject<Ref<DetailedUser | undefined>>("user");
|
||||
withDefaults(defineProps<{ user?: DetailedUser }>(), { user: undefined });
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
|
|
@ -1,34 +1,36 @@
|
|||
<script lang="ts" setup>
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||
import type { DetailedUser } from "@halo-dev/api-client";
|
||||
import type {
|
||||
DetailedUser,
|
||||
ReasonTypeNotifierRequest,
|
||||
} from "@halo-dev/api-client";
|
||||
import { VLoading, VSwitch } from "@halo-dev/components";
|
||||
import type { Ref } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { inject } from "vue";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import type { ReasonTypeNotifierRequest } from "@halo-dev/api-client";
|
||||
import HasPermission from "@/components/permission/HasPermission.vue";
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const props = withDefaults(defineProps<{ user?: DetailedUser }>(), {
|
||||
user: undefined,
|
||||
});
|
||||
|
||||
const user = inject<Ref<DetailedUser | undefined>>("user");
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["notification-preferences"],
|
||||
queryFn: async () => {
|
||||
if (!user?.value) {
|
||||
return;
|
||||
if (!props.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { data } =
|
||||
await apiClient.notification.listUserNotificationPreferences({
|
||||
username: user?.value?.user.metadata.name,
|
||||
username: props.user?.user.metadata.name,
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
enabled: computed(() => !!user?.value),
|
||||
enabled: computed(() => !!props.user),
|
||||
});
|
||||
|
||||
const {
|
||||
|
@ -48,7 +50,7 @@ const {
|
|||
}) => {
|
||||
const preferences = cloneDeep(data.value);
|
||||
|
||||
if (!user?.value || !preferences) {
|
||||
if (!props.user || !preferences) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -78,7 +80,7 @@ const {
|
|||
.filter(Boolean) as Array<ReasonTypeNotifierRequest>;
|
||||
|
||||
return await apiClient.notification.saveUserNotificationPreferences({
|
||||
username: user?.value?.user.metadata.name,
|
||||
username: props.user.user.metadata.name,
|
||||
reasonTypeNotifierCollectionRequest: {
|
||||
reasonTypeNotifiers,
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue