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 { apiClient } from "@/utils/api-client";
|
||||||
import {
|
import {
|
||||||
VButton,
|
VButton,
|
||||||
VTabbar,
|
|
||||||
VDropdown,
|
VDropdown,
|
||||||
VDropdownItem,
|
VDropdownItem,
|
||||||
|
VTabbar,
|
||||||
} from "@halo-dev/components";
|
} 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 { useRoute } from "vue-router";
|
||||||
import type { DetailedUser } from "@halo-dev/api-client";
|
|
||||||
import UserEditingModal from "./components/UserEditingModal.vue";
|
import UserEditingModal from "./components/UserEditingModal.vue";
|
||||||
import UserPasswordChangeModal from "./components/UserPasswordChangeModal.vue";
|
import UserPasswordChangeModal from "./components/UserPasswordChangeModal.vue";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import UserAvatar from "@/components/user-avatar/UserAvatar.vue";
|
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 DetailTab from "./tabs/Detail.vue";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
|
import { usePluginModuleStore } from "@/stores/plugin";
|
||||||
|
import type { PluginModule, UserTab } from "@halo-dev/console-shared";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { currentUser } = useUserStore();
|
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 editingModal = ref(false);
|
||||||
const passwordChangeModal = ref(false);
|
const passwordChangeModal = ref(false);
|
||||||
|
|
||||||
|
@ -56,25 +52,44 @@ const {
|
||||||
enabled: computed(() => !!params.name),
|
enabled: computed(() => !!params.name),
|
||||||
});
|
});
|
||||||
|
|
||||||
provide<Ref<DetailedUser | undefined>>("user", user);
|
const tabs = ref<UserTab[]>([
|
||||||
|
|
||||||
const tabs: UserTab[] = [
|
|
||||||
{
|
{
|
||||||
id: "detail",
|
id: "detail",
|
||||||
label: t("core.user.detail.tabs.detail"),
|
label: t("core.user.detail.tabs.detail"),
|
||||||
component: markRaw(DetailTab),
|
component: markRaw(DetailTab),
|
||||||
priority: 10,
|
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",
|
mode: "push",
|
||||||
});
|
});
|
||||||
|
|
||||||
provide<Ref<string>>("activeTab", activeTab);
|
provide<Ref<string>>("activeTab", activeTab);
|
||||||
|
|
||||||
const tabbarItems = computed(() => {
|
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() {
|
function handleRouteToUC() {
|
||||||
|
@ -142,7 +157,8 @@ function handleRouteToUC() {
|
||||||
<template v-for="tab in tabs" :key="tab.id">
|
<template v-for="tab in tabs" :key="tab.id">
|
||||||
<component
|
<component
|
||||||
:is="tab.component"
|
:is="tab.component"
|
||||||
v-if="activeTab === tab.id && !tab.hidden"
|
v-if="activeTab === tab.id"
|
||||||
|
:user="user"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,13 +5,13 @@ import {
|
||||||
VDescriptionItem,
|
VDescriptionItem,
|
||||||
VTag,
|
VTag,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import type { Ref } from "vue";
|
|
||||||
import { inject } from "vue";
|
|
||||||
import type { DetailedUser } from "@halo-dev/api-client";
|
import type { DetailedUser } from "@halo-dev/api-client";
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
|
|
||||||
const user = inject<Ref<DetailedUser | undefined>>("user");
|
withDefaults(defineProps<{ user?: DetailedUser }>(), {
|
||||||
|
user: undefined,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="border-t border-gray-100">
|
<div class="border-t border-gray-100">
|
||||||
|
|
|
@ -11,3 +11,4 @@ export * from "./states/plugin-installation-tabs";
|
||||||
export * from "./states/entity";
|
export * from "./states/entity";
|
||||||
export * from "./states/theme-list-tabs";
|
export * from "./states/theme-list-tabs";
|
||||||
export * from "./states/operation";
|
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 { 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 { FunctionalPage } from "../states/pages";
|
||||||
import type { AttachmentSelectProvider } from "../states/attachment-selector";
|
import type { AttachmentSelectProvider } from "../states/attachment-selector";
|
||||||
import type { EditorProvider, PluginTab } from "..";
|
import type { EditorProvider, PluginTab } from "..";
|
||||||
|
@ -17,6 +17,7 @@ import type {
|
||||||
Plugin,
|
Plugin,
|
||||||
Theme,
|
Theme,
|
||||||
} from "@halo-dev/api-client";
|
} from "@halo-dev/api-client";
|
||||||
|
import type { UserProfileTab, UserTab } from "@/states/user-tab";
|
||||||
|
|
||||||
export interface RouteRecordAppend {
|
export interface RouteRecordAppend {
|
||||||
parentName: RouteRecordName;
|
parentName: RouteRecordName;
|
||||||
|
@ -76,6 +77,12 @@ export interface ExtensionPoint {
|
||||||
"theme:list-item:operation:create"?: (
|
"theme:list-item:operation:create"?: (
|
||||||
theme: Ref<Theme>
|
theme: Ref<Theme>
|
||||||
) => OperationItem<Theme>[] | Promise<OperationItem<Theme>[]>;
|
) => OperationItem<Theme>[] | Promise<OperationItem<Theme>[]>;
|
||||||
|
|
||||||
|
"user:detail:tabs:create"?: () => UserTab[] | Promise<UserTab[]>;
|
||||||
|
|
||||||
|
"uc:user:profile:tabs:create"?: () =>
|
||||||
|
| UserProfileTab[]
|
||||||
|
| Promise<UserProfileTab[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginModule {
|
export interface PluginModule {
|
||||||
|
|
|
@ -2,38 +2,35 @@
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import {
|
import {
|
||||||
VButton,
|
VButton,
|
||||||
VTabbar,
|
|
||||||
VDropdown,
|
VDropdown,
|
||||||
VDropdownItem,
|
VDropdownItem,
|
||||||
|
VTabbar,
|
||||||
} from "@halo-dev/components";
|
} 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 type { DetailedUser } from "@halo-dev/api-client";
|
||||||
import ProfileEditingModal from "./components/ProfileEditingModal.vue";
|
import ProfileEditingModal from "./components/ProfileEditingModal.vue";
|
||||||
import PasswordChangeModal from "./components/PasswordChangeModal.vue";
|
import PasswordChangeModal from "./components/PasswordChangeModal.vue";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import UserAvatar from "@/components/user-avatar/UserAvatar.vue";
|
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 DetailTab from "./tabs/Detail.vue";
|
||||||
import PersonalAccessTokensTab from "./tabs/PersonalAccessTokens.vue";
|
import PersonalAccessTokensTab from "./tabs/PersonalAccessTokens.vue";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import NotificationPreferences from "./tabs/NotificationPreferences.vue";
|
import NotificationPreferences from "./tabs/NotificationPreferences.vue";
|
||||||
import TwoFactor from "./tabs/TwoFactor.vue";
|
import TwoFactor from "./tabs/TwoFactor.vue";
|
||||||
|
import type { PluginModule, UserProfileTab } from "@halo-dev/console-shared";
|
||||||
|
import { usePluginModuleStore } from "@/stores/plugin";
|
||||||
|
|
||||||
const { t } = useI18n();
|
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 editingModal = ref(false);
|
||||||
const passwordChangeModal = ref(false);
|
const passwordChangeModal = ref(false);
|
||||||
|
|
||||||
|
@ -51,7 +48,7 @@ const {
|
||||||
|
|
||||||
provide<Ref<DetailedUser | undefined>>("user", user);
|
provide<Ref<DetailedUser | undefined>>("user", user);
|
||||||
|
|
||||||
const tabs: UserTab[] = [
|
const tabs = ref<UserProfileTab[]>([
|
||||||
{
|
{
|
||||||
id: "detail",
|
id: "detail",
|
||||||
label: t("core.uc_profile.tabs.detail"),
|
label: t("core.uc_profile.tabs.detail"),
|
||||||
|
@ -76,13 +73,36 @@ const tabs: UserTab[] = [
|
||||||
component: markRaw(TwoFactor),
|
component: markRaw(TwoFactor),
|
||||||
priority: 40,
|
priority: 40,
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
|
|
||||||
const tabbarItems = computed(() => {
|
// Collect uc:profile:tabs:create extension points
|
||||||
return tabs.map((tab) => ({ id: tab.id, label: tab.label }));
|
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",
|
mode: "push",
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -140,7 +160,8 @@ const activeTab = useRouteQuery<string>("tab", tabs[0].id, {
|
||||||
<template v-for="tab in tabs" :key="tab.id">
|
<template v-for="tab in tabs" :key="tab.id">
|
||||||
<component
|
<component
|
||||||
:is="tab.component"
|
:is="tab.component"
|
||||||
v-if="activeTab === tab.id && !tab.hidden"
|
v-if="activeTab === tab.id"
|
||||||
|
:user="user"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,8 +8,7 @@ import {
|
||||||
VDescriptionItem,
|
VDescriptionItem,
|
||||||
VTag,
|
VTag,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import type { Ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { inject, computed } from "vue";
|
|
||||||
import type { DetailedUser, ListedAuthProvider } from "@halo-dev/api-client";
|
import type { DetailedUser, ListedAuthProvider } from "@halo-dev/api-client";
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
|
@ -18,9 +17,8 @@ import { apiClient } from "@/utils/api-client";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import EmailVerifyModal from "../components/EmailVerifyModal.vue";
|
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();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,36 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
|
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 { VLoading, VSwitch } from "@halo-dev/components";
|
||||||
import type { Ref } from "vue";
|
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { inject } from "vue";
|
|
||||||
import { cloneDeep } from "lodash-es";
|
import { cloneDeep } from "lodash-es";
|
||||||
import type { ReasonTypeNotifierRequest } from "@halo-dev/api-client";
|
|
||||||
import HasPermission from "@/components/permission/HasPermission.vue";
|
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({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ["notification-preferences"],
|
queryKey: ["notification-preferences"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!user?.value) {
|
if (!props.user) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } =
|
const { data } =
|
||||||
await apiClient.notification.listUserNotificationPreferences({
|
await apiClient.notification.listUserNotificationPreferences({
|
||||||
username: user?.value?.user.metadata.name,
|
username: props.user?.user.metadata.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
enabled: computed(() => !!user?.value),
|
enabled: computed(() => !!props.user),
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -48,7 +50,7 @@ const {
|
||||||
}) => {
|
}) => {
|
||||||
const preferences = cloneDeep(data.value);
|
const preferences = cloneDeep(data.value);
|
||||||
|
|
||||||
if (!user?.value || !preferences) {
|
if (!props.user || !preferences) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +80,7 @@ const {
|
||||||
.filter(Boolean) as Array<ReasonTypeNotifierRequest>;
|
.filter(Boolean) as Array<ReasonTypeNotifierRequest>;
|
||||||
|
|
||||||
return await apiClient.notification.saveUserNotificationPreferences({
|
return await apiClient.notification.saveUserNotificationPreferences({
|
||||||
username: user?.value?.user.metadata.name,
|
username: props.user.user.metadata.name,
|
||||||
reasonTypeNotifierCollectionRequest: {
|
reasonTypeNotifierCollectionRequest: {
|
||||||
reasonTypeNotifiers,
|
reasonTypeNotifiers,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue