mirror of https://github.com/halo-dev/halo
parent
96a01cccc7
commit
a951a34a1b
|
@ -1,129 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { RouterView, useRoute } from "vue-router";
|
import BaseApp from "@/components/base-app/BaseApp.vue";
|
||||||
import { computed, watch, reactive, onMounted, inject } from "vue";
|
|
||||||
import { useTitle } from "@vueuse/core";
|
|
||||||
import { useFavicon } from "@vueuse/core";
|
|
||||||
import { useSystemConfigMapStore } from "@console/stores/system-configmap";
|
|
||||||
import { storeToRefs } from "pinia";
|
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
import {
|
|
||||||
useOverlayScrollbars,
|
|
||||||
type UseOverlayScrollbarsParams,
|
|
||||||
} from "overlayscrollbars-vue";
|
|
||||||
import type { FormKitConfig } from "@formkit/core";
|
|
||||||
import { i18n } from "@/locales";
|
|
||||||
import { AppName } from "@/constants/app";
|
|
||||||
import { useGlobalInfoStore } from "@/stores/global-info";
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const { configMap } = storeToRefs(useSystemConfigMapStore());
|
|
||||||
const globalInfoStore = useGlobalInfoStore();
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const title = useTitle();
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.name,
|
|
||||||
() => {
|
|
||||||
const { title: routeTitle } = route.meta;
|
|
||||||
if (routeTitle) {
|
|
||||||
title.value = `${t(routeTitle)} - ${AppName}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
title.value = AppName;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Favicon
|
|
||||||
const defaultFavicon = "/console/favicon.ico";
|
|
||||||
|
|
||||||
const favicon = computed(() => {
|
|
||||||
if (configMap?.value?.data?.["basic"]) {
|
|
||||||
const basic = JSON.parse(configMap.value.data["basic"]);
|
|
||||||
|
|
||||||
if (basic.favicon) {
|
|
||||||
return basic.favicon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (globalInfoStore.globalInfo?.favicon) {
|
|
||||||
return globalInfoStore.globalInfo.favicon;
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultFavicon;
|
|
||||||
});
|
|
||||||
|
|
||||||
useFavicon(favicon);
|
|
||||||
|
|
||||||
// body scroll
|
|
||||||
const body = document.querySelector("body");
|
|
||||||
const reactiveParams = reactive<UseOverlayScrollbarsParams>({
|
|
||||||
options: {
|
|
||||||
scrollbars: {
|
|
||||||
autoHide: "scroll",
|
|
||||||
autoHideDelay: 600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defer: true,
|
|
||||||
});
|
|
||||||
const [initialize] = useOverlayScrollbars(reactiveParams);
|
|
||||||
onMounted(() => {
|
|
||||||
if (body) initialize({ target: body });
|
|
||||||
});
|
|
||||||
|
|
||||||
// setup formkit locale
|
|
||||||
// see https://formkit.com/essentials/internationalization
|
|
||||||
const formkitLocales = {
|
|
||||||
en: "en",
|
|
||||||
zh: "zh",
|
|
||||||
"en-US": "en",
|
|
||||||
"zh-CN": "zh",
|
|
||||||
};
|
|
||||||
const formkitConfig = inject(Symbol.for("FormKitConfig")) as FormKitConfig;
|
|
||||||
formkitConfig.locale = formkitLocales[i18n.global.locale.value] || "zh";
|
|
||||||
|
|
||||||
// Fix 100vh issue in ios devices
|
|
||||||
function setViewportProperty(doc: HTMLElement) {
|
|
||||||
let prevClientHeight: number;
|
|
||||||
const customVar = "--vh";
|
|
||||||
function handleResize() {
|
|
||||||
const clientHeight = doc.clientHeight;
|
|
||||||
if (clientHeight === prevClientHeight) return;
|
|
||||||
requestAnimationFrame(function updateViewportHeight() {
|
|
||||||
doc.style.setProperty(customVar, clientHeight * 0.01 + "px");
|
|
||||||
prevClientHeight = clientHeight;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
handleResize();
|
|
||||||
return handleResize;
|
|
||||||
}
|
|
||||||
window.addEventListener(
|
|
||||||
"resize",
|
|
||||||
setViewportProperty(document.documentElement)
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterView />
|
<BaseApp />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
body {
|
|
||||||
background: #eff4f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-popper__popper {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-popper--theme-tooltip {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -3,29 +3,20 @@ import {
|
||||||
IconMore,
|
IconMore,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconUserSettings,
|
IconUserSettings,
|
||||||
|
IconLogoutCircleRLine,
|
||||||
VTag,
|
VTag,
|
||||||
VAvatar,
|
VAvatar,
|
||||||
Dialog,
|
Dialog,
|
||||||
VDropdown,
|
IconAccountCircleLine,
|
||||||
VDropdownItem,
|
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
||||||
import type { MenuGroupType, MenuItemType } from "@halo-dev/console-shared";
|
|
||||||
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
||||||
import {
|
import { RouterView, useRoute, useRouter } from "vue-router";
|
||||||
RouterView,
|
|
||||||
useRoute,
|
|
||||||
useRouter,
|
|
||||||
type RouteRecordRaw,
|
|
||||||
} from "vue-router";
|
|
||||||
import { onMounted, reactive, ref } from "vue";
|
import { onMounted, reactive, ref } from "vue";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
|
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
|
||||||
import LoginModal from "@/components/login/LoginModal.vue";
|
import LoginModal from "@/components/login/LoginModal.vue";
|
||||||
import { coreMenuGroups } from "@console/router/routes.config";
|
import { coreMenuGroups } from "@console/router/constant";
|
||||||
import sortBy from "lodash.sortby";
|
|
||||||
import { useRoleStore } from "@/stores/role";
|
|
||||||
import { hasPermission } from "@/utils/permission";
|
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
import { defineStore, storeToRefs } from "pinia";
|
import { defineStore, storeToRefs } from "pinia";
|
||||||
|
@ -36,6 +27,7 @@ import {
|
||||||
} from "overlayscrollbars-vue";
|
} from "overlayscrollbars-vue";
|
||||||
import { isMac } from "@/utils/device";
|
import { isMac } from "@/utils/device";
|
||||||
import { useEventListener } from "@vueuse/core";
|
import { useEventListener } from "@vueuse/core";
|
||||||
|
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -79,105 +71,7 @@ useEventListener(document, "keydown", (e: KeyboardEvent) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate menus by routes
|
const { menus, minimenus } = useRouteMenuGenerator(coreMenuGroups);
|
||||||
const menus = ref<MenuGroupType[]>([] as MenuGroupType[]);
|
|
||||||
const minimenus = ref<MenuItemType[]>([] as MenuItemType[]);
|
|
||||||
|
|
||||||
const roleStore = useRoleStore();
|
|
||||||
const { uiPermissions } = roleStore.permissions;
|
|
||||||
|
|
||||||
const generateMenus = () => {
|
|
||||||
// sort by menu.priority and meta.core
|
|
||||||
const currentRoutes = sortBy(
|
|
||||||
router.getRoutes().filter((route) => {
|
|
||||||
const { meta } = route;
|
|
||||||
if (!meta?.menu) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (meta.permissions) {
|
|
||||||
return hasPermission(uiPermissions, meta.permissions as string[], true);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
(route: RouteRecordRaw) => !route.meta?.core,
|
|
||||||
(route: RouteRecordRaw) => route.meta?.menu?.priority || 0,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// group by menu.group
|
|
||||||
menus.value = currentRoutes.reduce((acc, route) => {
|
|
||||||
const { menu } = route.meta;
|
|
||||||
if (!menu) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
const group = acc.find((item) => item.id === menu.group);
|
|
||||||
const childRoute = route.children[0];
|
|
||||||
const childMetaMenu = childRoute?.meta?.menu;
|
|
||||||
|
|
||||||
// only support one level
|
|
||||||
const menuChildren = childMetaMenu
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
name: childMetaMenu.name,
|
|
||||||
path: childRoute.path,
|
|
||||||
icon: childMetaMenu.icon,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: undefined;
|
|
||||||
if (group) {
|
|
||||||
group.items?.push({
|
|
||||||
name: menu.name,
|
|
||||||
path: route.path,
|
|
||||||
icon: menu.icon,
|
|
||||||
mobile: menu.mobile,
|
|
||||||
children: menuChildren,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const menuGroup = coreMenuGroups.find((item) => item.id === menu.group);
|
|
||||||
let name = "";
|
|
||||||
if (!menuGroup) {
|
|
||||||
name = menu.group;
|
|
||||||
} else if (menuGroup.name) {
|
|
||||||
name = menuGroup.name;
|
|
||||||
}
|
|
||||||
acc.push({
|
|
||||||
id: menuGroup?.id || menu.group,
|
|
||||||
name: name,
|
|
||||||
priority: menuGroup?.priority || 0,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: menu.name,
|
|
||||||
path: route.path,
|
|
||||||
icon: menu.icon,
|
|
||||||
mobile: menu.mobile,
|
|
||||||
children: menuChildren,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, [] as MenuGroupType[]);
|
|
||||||
|
|
||||||
// sort by menu.priority
|
|
||||||
menus.value = sortBy(menus.value, [
|
|
||||||
(menu: MenuGroupType) => {
|
|
||||||
return coreMenuGroups.findIndex((item) => item.id === menu.id) < 0;
|
|
||||||
},
|
|
||||||
(menu: MenuGroupType) => menu.priority || 0,
|
|
||||||
]);
|
|
||||||
|
|
||||||
minimenus.value = menus.value
|
|
||||||
.reduce((acc, group) => {
|
|
||||||
if (group?.items) {
|
|
||||||
acc.push(...group.items);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, [] as MenuItemType[])
|
|
||||||
.filter((item) => item.mobile);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(generateMenus);
|
|
||||||
|
|
||||||
// aside scroll
|
// aside scroll
|
||||||
const navbarScroller = ref();
|
const navbarScroller = ref();
|
||||||
|
@ -215,10 +109,6 @@ onMounted(() => {
|
||||||
initialize({ target: navbarScroller.value });
|
initialize({ target: navbarScroller.value });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleRouteToUC() {
|
|
||||||
window.location.href = "/uc";
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -262,12 +152,15 @@ function handleRouteToUC() {
|
||||||
<VAvatar
|
<VAvatar
|
||||||
:src="currentUser?.spec.avatar"
|
:src="currentUser?.spec.avatar"
|
||||||
:alt="currentUser?.spec.displayName"
|
:alt="currentUser?.spec.displayName"
|
||||||
size="md"
|
size="sm"
|
||||||
circle
|
circle
|
||||||
></VAvatar>
|
></VAvatar>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-name">
|
<div class="profile-name">
|
||||||
<div class="flex text-sm font-medium">
|
<div
|
||||||
|
class="flex text-sm font-medium"
|
||||||
|
:title="currentUser?.spec.displayName"
|
||||||
|
>
|
||||||
{{ currentUser?.spec.displayName }}
|
{{ currentUser?.spec.displayName }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="currentRoles?.[0]" class="flex">
|
<div v-if="currentRoles?.[0]" class="flex">
|
||||||
|
@ -283,19 +176,26 @@ function handleRouteToUC() {
|
||||||
</VTag>
|
</VTag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<VDropdown
|
|
||||||
class="profile-control cursor-pointer rounded p-1 transition-all hover:bg-gray-100"
|
<div class="flex items-center gap-1">
|
||||||
|
<a
|
||||||
|
v-tooltip="'个人中心'"
|
||||||
|
class="group inline-block cursor-pointer rounded-full p-1.5 transition-all hover:bg-gray-100"
|
||||||
|
href="/uc"
|
||||||
>
|
>
|
||||||
<IconMore />
|
<IconAccountCircleLine
|
||||||
<template #popper>
|
class="h-5 w-5 text-gray-600 group-hover:text-gray-900"
|
||||||
<VDropdownItem @click="handleRouteToUC">
|
/>
|
||||||
{{ $t("core.sidebar.operations.profile.button") }}
|
</a>
|
||||||
</VDropdownItem>
|
<div
|
||||||
<VDropdownItem @click="handleLogout">
|
class="group inline-block cursor-pointer rounded-full p-1.5 transition-all hover:bg-gray-100"
|
||||||
{{ $t("core.sidebar.operations.logout.button") }}
|
@click="handleLogout"
|
||||||
</VDropdownItem>
|
>
|
||||||
</template>
|
<IconLogoutCircleRLine
|
||||||
</VDropdown>
|
class="h-5 w-5 text-gray-600 group-hover:text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
@ -418,11 +318,8 @@ function handleRouteToUC() {
|
||||||
|
|
||||||
.profile-name {
|
.profile-name {
|
||||||
@apply flex-1
|
@apply flex-1
|
||||||
self-center;
|
self-center
|
||||||
}
|
overflow-hidden;
|
||||||
|
|
||||||
.profile-control {
|
|
||||||
@apply self-center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconDatabase2Line,
|
IconDatabase2Line,
|
||||||
VCard,
|
VCard,
|
||||||
IconUserLine,
|
IconAccountCircleLine,
|
||||||
Dialog,
|
Dialog,
|
||||||
Toast,
|
Toast,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
|
@ -37,9 +37,9 @@ const { t } = useI18n();
|
||||||
|
|
||||||
const actions: Action[] = [
|
const actions: Action[] = [
|
||||||
{
|
{
|
||||||
icon: markRaw(IconUserLine),
|
icon: markRaw(IconAccountCircleLine),
|
||||||
title: t(
|
title: t(
|
||||||
"core.dashboard.widgets.presets.quicklink.actions.user_profile.title"
|
"core.dashboard.widgets.presets.quicklink.actions.user_center.title"
|
||||||
),
|
),
|
||||||
action: () => {
|
action: () => {
|
||||||
window.location.href = "/uc/profile";
|
window.location.href = "/uc/profile";
|
||||||
|
|
|
@ -22,9 +22,11 @@ import type { Component } from "vue";
|
||||||
import { markRaw } 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";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { currentUser } = useUserStore();
|
||||||
|
|
||||||
interface UserTab {
|
interface UserTab {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -84,6 +86,10 @@ provide<Ref<string>>("activeTab", activeTab);
|
||||||
const tabbarItems = computed(() => {
|
const tabbarItems = computed(() => {
|
||||||
return tabs.map((tab) => ({ id: tab.id, label: tab.label }));
|
return tabs.map((tab) => ({ id: tab.id, label: tab.label }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleRouteToUC() {
|
||||||
|
window.location.href = "/uc";
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<UserEditingModal v-model:visible="editingModal" :user="user?.user" />
|
<UserEditingModal v-model:visible="editingModal" :user="user?.user" />
|
||||||
|
@ -111,8 +117,15 @@ const tabbarItems = computed(() => {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="currentUserHasPermission(['system:users:manage'])">
|
<div class="inline-flex items-center gap-2">
|
||||||
<VDropdown>
|
<VButton
|
||||||
|
v-if="currentUser?.metadata.name === user?.user.metadata.name"
|
||||||
|
type="primary"
|
||||||
|
@click="handleRouteToUC"
|
||||||
|
>
|
||||||
|
个人中心
|
||||||
|
</VButton>
|
||||||
|
<VDropdown v-if="currentUserHasPermission(['system:users:manage'])">
|
||||||
<VButton type="default">
|
<VButton type="default">
|
||||||
{{ $t("core.common.buttons.edit") }}
|
{{ $t("core.common.buttons.edit") }}
|
||||||
</VButton>
|
</VButton>
|
||||||
|
|
|
@ -1,72 +1,17 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
Dialog,
|
|
||||||
IconUserSettings,
|
IconUserSettings,
|
||||||
VButton,
|
|
||||||
VDescription,
|
VDescription,
|
||||||
VDescriptionItem,
|
VDescriptionItem,
|
||||||
VTag,
|
VTag,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import type { ComputedRef, Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { inject, computed } from "vue";
|
import { inject } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import type { DetailedUser } 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";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
|
||||||
import { apiClient } from "@/utils/api-client";
|
|
||||||
import axios from "axios";
|
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
|
|
||||||
const user = inject<Ref<DetailedUser | undefined>>("user");
|
const user = inject<Ref<DetailedUser | undefined>>("user");
|
||||||
const isCurrentUser = inject<ComputedRef<boolean>>("isCurrentUser");
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const { data: authProviders, isFetching } = useQuery<ListedAuthProvider[]>({
|
|
||||||
queryKey: ["user-auth-providers"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await apiClient.authProvider.listAuthProviders();
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
enabled: isCurrentUser,
|
|
||||||
});
|
|
||||||
|
|
||||||
const availableAuthProviders = computed(() => {
|
|
||||||
return authProviders.value?.filter(
|
|
||||||
(authProvider) => authProvider.enabled && authProvider.supportsBinding
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleUnbindAuth = (authProvider: ListedAuthProvider) => {
|
|
||||||
Dialog.warning({
|
|
||||||
title: t("core.user.detail.operations.unbind.title", {
|
|
||||||
display_name: authProvider.displayName,
|
|
||||||
}),
|
|
||||||
confirmText: t("core.common.buttons.confirm"),
|
|
||||||
cancelText: t("core.common.buttons.cancel"),
|
|
||||||
onConfirm: async () => {
|
|
||||||
await axios.put(
|
|
||||||
`${import.meta.env.VITE_API_URL}${authProvider.unbindingUrl}`,
|
|
||||||
{
|
|
||||||
withCredentials: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
window.location.reload();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBindAuth = (authProvider: ListedAuthProvider) => {
|
|
||||||
if (!authProvider.bindingUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.location.href = `${
|
|
||||||
authProvider.bindingUrl
|
|
||||||
}?redirect_uri=${encodeURIComponent(window.location.href)}`;
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="border-t border-gray-100">
|
<div class="border-t border-gray-100">
|
||||||
|
@ -94,7 +39,7 @@ const handleBindAuth = (authProvider: ListedAuthProvider) => {
|
||||||
v-for="(role, index) in user?.roles"
|
v-for="(role, index) in user?.roles"
|
||||||
:key="index"
|
:key="index"
|
||||||
@click="
|
@click="
|
||||||
router.push({
|
$router.push({
|
||||||
name: 'RoleDetail',
|
name: 'RoleDetail',
|
||||||
params: { name: role.metadata.name },
|
params: { name: role.metadata.name },
|
||||||
})
|
})
|
||||||
|
@ -119,50 +64,6 @@ const handleBindAuth = (authProvider: ListedAuthProvider) => {
|
||||||
:content="formatDatetime(user?.user.metadata?.creationTimestamp)"
|
:content="formatDatetime(user?.user.metadata?.creationTimestamp)"
|
||||||
class="!px-2"
|
class="!px-2"
|
||||||
/>
|
/>
|
||||||
<VDescriptionItem
|
|
||||||
v-if="!isFetching && isCurrentUser && availableAuthProviders?.length"
|
|
||||||
:label="$t('core.user.detail.fields.identity_authentication')"
|
|
||||||
class="!px-2"
|
|
||||||
>
|
|
||||||
<ul class="space-y-2">
|
|
||||||
<template v-for="(authProvider, index) in authProviders">
|
|
||||||
<li
|
|
||||||
v-if="authProvider.supportsBinding && authProvider.enabled"
|
|
||||||
:key="index"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex w-full cursor-pointer flex-wrap justify-between gap-y-3 rounded border p-5 hover:border-primary sm:w-1/2"
|
|
||||||
>
|
|
||||||
<div class="inline-flex items-center gap-3">
|
|
||||||
<div>
|
|
||||||
<img class="h-7 w-7 rounded" :src="authProvider.logo" />
|
|
||||||
</div>
|
|
||||||
<div class="text-sm font-medium text-gray-900">
|
|
||||||
{{ authProvider.displayName }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="inline-flex items-center">
|
|
||||||
<VButton
|
|
||||||
v-if="authProvider.isBound"
|
|
||||||
size="sm"
|
|
||||||
@click="handleUnbindAuth(authProvider)"
|
|
||||||
>
|
|
||||||
{{ $t("core.user.detail.operations.unbind.button") }}
|
|
||||||
</VButton>
|
|
||||||
<VButton
|
|
||||||
v-else
|
|
||||||
size="sm"
|
|
||||||
type="secondary"
|
|
||||||
@click="handleBindAuth(authProvider)"
|
|
||||||
>
|
|
||||||
{{ $t("core.user.detail.operations.bind.button") }}
|
|
||||||
</VButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
</ul>
|
|
||||||
</VDescriptionItem>
|
|
||||||
</VDescription>
|
</VDescription>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import type { MenuGroupType } from "@halo-dev/console-shared";
|
||||||
|
|
||||||
|
export const coreMenuGroups: MenuGroupType[] = [
|
||||||
|
{
|
||||||
|
id: "dashboard",
|
||||||
|
name: undefined,
|
||||||
|
priority: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "content",
|
||||||
|
name: "core.sidebar.menu.groups.content",
|
||||||
|
priority: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "interface",
|
||||||
|
name: "core.sidebar.menu.groups.interface",
|
||||||
|
priority: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system",
|
||||||
|
name: "core.sidebar.menu.groups.system",
|
||||||
|
priority: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tool",
|
||||||
|
name: "core.sidebar.menu.groups.tool",
|
||||||
|
priority: 4,
|
||||||
|
},
|
||||||
|
];
|
|
@ -4,7 +4,6 @@ import Forbidden from "@/views/exceptions/Forbidden.vue";
|
||||||
import BasicLayout from "@console/layouts/BasicLayout.vue";
|
import BasicLayout from "@console/layouts/BasicLayout.vue";
|
||||||
import Setup from "@console/views/system/Setup.vue";
|
import Setup from "@console/views/system/Setup.vue";
|
||||||
import Redirect from "@console/views/system/Redirect.vue";
|
import Redirect from "@console/views/system/Redirect.vue";
|
||||||
import type { MenuGroupType } from "@halo-dev/console-shared";
|
|
||||||
import SetupInitialData from "@console/views/system/SetupInitialData.vue";
|
import SetupInitialData from "@console/views/system/SetupInitialData.vue";
|
||||||
|
|
||||||
export const routes: Array<RouteRecordRaw> = [
|
export const routes: Array<RouteRecordRaw> = [
|
||||||
|
@ -47,32 +46,4 @@ export const routes: Array<RouteRecordRaw> = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const coreMenuGroups: MenuGroupType[] = [
|
|
||||||
{
|
|
||||||
id: "dashboard",
|
|
||||||
name: undefined,
|
|
||||||
priority: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "content",
|
|
||||||
name: "core.sidebar.menu.groups.content",
|
|
||||||
priority: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "interface",
|
|
||||||
name: "core.sidebar.menu.groups.interface",
|
|
||||||
priority: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "system",
|
|
||||||
name: "core.sidebar.menu.groups.system",
|
|
||||||
priority: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tool",
|
|
||||||
name: "core.sidebar.menu.groups.tool",
|
|
||||||
priority: 4,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
|
|
@ -119,6 +119,7 @@
|
||||||
"@types/lodash.debounce": "^4.0.7",
|
"@types/lodash.debounce": "^4.0.7",
|
||||||
"@types/lodash.isequal": "^4.5.6",
|
"@types/lodash.isequal": "^4.5.6",
|
||||||
"@types/lodash.merge": "^4.6.7",
|
"@types/lodash.merge": "^4.6.7",
|
||||||
|
"@types/lodash.sortby": "^4.7.9",
|
||||||
"@types/node": "^18.11.19",
|
"@types/node": "^18.11.19",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@types/randomstring": "^1.1.8",
|
"@types/randomstring": "^1.1.8",
|
||||||
|
|
|
@ -67,6 +67,9 @@ import IconArrowLeftRightLine from "~icons/ri/arrow-left-right-line";
|
||||||
import IconArrowUpDownLine from "~icons/ri/arrow-up-down-line";
|
import IconArrowUpDownLine from "~icons/ri/arrow-up-down-line";
|
||||||
import IconRiUpload2Fill from "~icons/ri/upload-2-fill";
|
import IconRiUpload2Fill from "~icons/ri/upload-2-fill";
|
||||||
import IconNotificationBadgeLine from "~icons/ri/notification-badge-line";
|
import IconNotificationBadgeLine from "~icons/ri/notification-badge-line";
|
||||||
|
import IconLogoutCircleRLine from "~icons/ri/logout-circle-r-line";
|
||||||
|
import IconAccountCircleLine from "~icons/ri/account-circle-line";
|
||||||
|
import IconSettings3Line from "~icons/ri/settings-3-line";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
IconDashboard,
|
IconDashboard,
|
||||||
|
@ -138,4 +141,7 @@ export {
|
||||||
IconArrowUpDownLine,
|
IconArrowUpDownLine,
|
||||||
IconRiUpload2Fill,
|
IconRiUpload2Fill,
|
||||||
IconNotificationBadgeLine,
|
IconNotificationBadgeLine,
|
||||||
|
IconLogoutCircleRLine,
|
||||||
|
IconAccountCircleLine,
|
||||||
|
IconSettings3Line,
|
||||||
};
|
};
|
||||||
|
|
|
@ -243,6 +243,9 @@ importers:
|
||||||
'@types/lodash.merge':
|
'@types/lodash.merge':
|
||||||
specifier: ^4.6.7
|
specifier: ^4.6.7
|
||||||
version: 4.6.7
|
version: 4.6.7
|
||||||
|
'@types/lodash.sortby':
|
||||||
|
specifier: ^4.7.9
|
||||||
|
version: 4.7.9
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^18.11.19
|
specifier: ^18.11.19
|
||||||
version: 18.13.0
|
version: 18.13.0
|
||||||
|
@ -3952,6 +3955,12 @@ packages:
|
||||||
'@types/lodash': 4.14.186
|
'@types/lodash': 4.14.186
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/lodash.sortby@4.7.9:
|
||||||
|
resolution: {integrity: sha512-PDmjHnOlndLS59GofH0pnxIs+n9i4CWeXGErSB5JyNFHu2cmvW6mQOaUKjG8EDPkni14IgF8NsRW8bKvFzTm9A==}
|
||||||
|
dependencies:
|
||||||
|
'@types/lodash': 4.14.186
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/lodash@4.14.186:
|
/@types/lodash@4.14.186:
|
||||||
resolution: {integrity: sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==}
|
resolution: {integrity: sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { RouterView, useRoute } from "vue-router";
|
||||||
|
import { computed, watch, reactive, onMounted, inject } from "vue";
|
||||||
|
import { useTitle } from "@vueuse/core";
|
||||||
|
import { useFavicon } from "@vueuse/core";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import {
|
||||||
|
useOverlayScrollbars,
|
||||||
|
type UseOverlayScrollbarsParams,
|
||||||
|
} from "overlayscrollbars-vue";
|
||||||
|
import type { FormKitConfig } from "@formkit/core";
|
||||||
|
import { i18n } from "@/locales";
|
||||||
|
import { AppName } from "@/constants/app";
|
||||||
|
import { useGlobalInfoStore } from "@/stores/global-info";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const globalInfoStore = useGlobalInfoStore();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const title = useTitle();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.name,
|
||||||
|
() => {
|
||||||
|
const { title: routeTitle } = route.meta;
|
||||||
|
if (routeTitle) {
|
||||||
|
title.value = `${t(routeTitle)} - ${AppName}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
title.value = AppName;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Favicon
|
||||||
|
const defaultFavicon = "/console/favicon.ico";
|
||||||
|
const favicon = computed(() => {
|
||||||
|
return globalInfoStore.globalInfo?.favicon || defaultFavicon;
|
||||||
|
});
|
||||||
|
|
||||||
|
useFavicon(favicon);
|
||||||
|
|
||||||
|
// body scroll
|
||||||
|
const body = document.querySelector("body");
|
||||||
|
const reactiveParams = reactive<UseOverlayScrollbarsParams>({
|
||||||
|
options: {
|
||||||
|
scrollbars: {
|
||||||
|
autoHide: "scroll",
|
||||||
|
autoHideDelay: 600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defer: true,
|
||||||
|
});
|
||||||
|
const [initialize] = useOverlayScrollbars(reactiveParams);
|
||||||
|
onMounted(() => {
|
||||||
|
if (body) initialize({ target: body });
|
||||||
|
});
|
||||||
|
|
||||||
|
// setup formkit locale
|
||||||
|
// see https://formkit.com/essentials/internationalization
|
||||||
|
const formkitLocales = {
|
||||||
|
en: "en",
|
||||||
|
zh: "zh",
|
||||||
|
"en-US": "en",
|
||||||
|
"zh-CN": "zh",
|
||||||
|
};
|
||||||
|
const formkitConfig = inject(Symbol.for("FormKitConfig")) as FormKitConfig;
|
||||||
|
formkitConfig.locale = formkitLocales[i18n.global.locale.value] || "zh";
|
||||||
|
|
||||||
|
// Fix 100vh issue in ios devices
|
||||||
|
function setViewportProperty(doc: HTMLElement) {
|
||||||
|
let prevClientHeight: number;
|
||||||
|
const customVar = "--vh";
|
||||||
|
function handleResize() {
|
||||||
|
const clientHeight = doc.clientHeight;
|
||||||
|
if (clientHeight === prevClientHeight) return;
|
||||||
|
requestAnimationFrame(function updateViewportHeight() {
|
||||||
|
doc.style.setProperty(customVar, clientHeight * 0.01 + "px");
|
||||||
|
prevClientHeight = clientHeight;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleResize();
|
||||||
|
return handleResize;
|
||||||
|
}
|
||||||
|
window.addEventListener(
|
||||||
|
"resize",
|
||||||
|
setViewportProperty(document.documentElement)
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
body {
|
||||||
|
background: #eff4f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper__popper {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-popper--theme-tooltip {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { useRoleStore } from "@/stores/role";
|
||||||
|
import type { MenuGroupType, MenuItemType } from "@halo-dev/console-shared";
|
||||||
|
import { onMounted, ref, type Ref } from "vue";
|
||||||
|
import sortBy from "lodash.sortby";
|
||||||
|
import { hasPermission } from "@/utils/permission";
|
||||||
|
import {
|
||||||
|
useRouter,
|
||||||
|
type RouteRecordRaw,
|
||||||
|
type RouteRecordNormalized,
|
||||||
|
} from "vue-router";
|
||||||
|
|
||||||
|
interface useRouteMenuGeneratorReturn {
|
||||||
|
menus: Ref<MenuGroupType[]>;
|
||||||
|
minimenus: Ref<MenuItemType[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRouteMenuGenerator(
|
||||||
|
menuGroups: MenuGroupType[]
|
||||||
|
): useRouteMenuGeneratorReturn {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const menus = ref<MenuGroupType[]>([] as MenuGroupType[]);
|
||||||
|
const minimenus = ref<MenuItemType[]>([] as MenuItemType[]);
|
||||||
|
|
||||||
|
const roleStore = useRoleStore();
|
||||||
|
const { uiPermissions } = roleStore.permissions;
|
||||||
|
|
||||||
|
const generateMenus = () => {
|
||||||
|
// sort by menu.priority and meta.core
|
||||||
|
const currentRoutes = sortBy<RouteRecordNormalized>(
|
||||||
|
router.getRoutes().filter((route) => {
|
||||||
|
const { meta } = route;
|
||||||
|
if (!meta?.menu) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (meta.permissions) {
|
||||||
|
return hasPermission(
|
||||||
|
uiPermissions,
|
||||||
|
meta.permissions as string[],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
(route: RouteRecordRaw) => !route.meta?.core,
|
||||||
|
(route: RouteRecordRaw) => route.meta?.menu?.priority || 0,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// group by menu.group
|
||||||
|
menus.value = currentRoutes.reduce((acc, route) => {
|
||||||
|
const { menu } = route.meta;
|
||||||
|
if (!menu) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
const group = acc.find((item) => item.id === menu.group);
|
||||||
|
const childRoute = route.children[0];
|
||||||
|
const childMetaMenu = childRoute?.meta?.menu;
|
||||||
|
|
||||||
|
// only support one level
|
||||||
|
const menuChildren = childMetaMenu
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: childMetaMenu.name,
|
||||||
|
path: childRoute.path,
|
||||||
|
icon: childMetaMenu.icon,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: undefined;
|
||||||
|
if (group) {
|
||||||
|
group.items?.push({
|
||||||
|
name: menu.name,
|
||||||
|
path: route.path,
|
||||||
|
icon: menu.icon,
|
||||||
|
mobile: menu.mobile,
|
||||||
|
children: menuChildren,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const menuGroup = menuGroups.find((item) => item.id === menu.group);
|
||||||
|
let name = "";
|
||||||
|
if (!menuGroup) {
|
||||||
|
name = menu.group || "";
|
||||||
|
} else if (menuGroup.name) {
|
||||||
|
name = menuGroup.name;
|
||||||
|
}
|
||||||
|
acc.push({
|
||||||
|
id: menuGroup?.id || menu.group || "",
|
||||||
|
name: name,
|
||||||
|
priority: menuGroup?.priority || 0,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: menu.name,
|
||||||
|
path: route.path,
|
||||||
|
icon: menu.icon,
|
||||||
|
mobile: menu.mobile,
|
||||||
|
children: menuChildren,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as MenuGroupType[]);
|
||||||
|
|
||||||
|
// sort by menu.priority
|
||||||
|
menus.value = sortBy(menus.value, [
|
||||||
|
(menu: MenuGroupType) => {
|
||||||
|
return menuGroups.findIndex((item) => item.id === menu.id) < 0;
|
||||||
|
},
|
||||||
|
(menu: MenuGroupType) => menu.priority || 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
minimenus.value = menus.value
|
||||||
|
.reduce((acc, group) => {
|
||||||
|
if (group?.items) {
|
||||||
|
acc.push(...group.items);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as MenuItemType[])
|
||||||
|
.filter((item) => item.mobile);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(generateMenus);
|
||||||
|
|
||||||
|
return {
|
||||||
|
menus,
|
||||||
|
minimenus,
|
||||||
|
};
|
||||||
|
}
|
|
@ -106,8 +106,8 @@ core:
|
||||||
quicklink:
|
quicklink:
|
||||||
title: Quick Link
|
title: Quick Link
|
||||||
actions:
|
actions:
|
||||||
user_profile:
|
user_center:
|
||||||
title: User Profile
|
title: User Center
|
||||||
view_site:
|
view_site:
|
||||||
title: View Site
|
title: View Site
|
||||||
new_post:
|
new_post:
|
||||||
|
|
|
@ -105,7 +105,7 @@ core:
|
||||||
quicklink:
|
quicklink:
|
||||||
title: Enlace Rápido
|
title: Enlace Rápido
|
||||||
actions:
|
actions:
|
||||||
user_profile:
|
user_center:
|
||||||
title: Perfil de Usuario
|
title: Perfil de Usuario
|
||||||
view_site:
|
view_site:
|
||||||
title: Ver Sitio
|
title: Ver Sitio
|
||||||
|
|
|
@ -106,8 +106,8 @@ core:
|
||||||
quicklink:
|
quicklink:
|
||||||
title: 快捷访问
|
title: 快捷访问
|
||||||
actions:
|
actions:
|
||||||
user_profile:
|
user_center:
|
||||||
title: 个人资料
|
title: 个人中心
|
||||||
view_site:
|
view_site:
|
||||||
title: 查看站点
|
title: 查看站点
|
||||||
new_post:
|
new_post:
|
||||||
|
|
|
@ -106,8 +106,8 @@ core:
|
||||||
quicklink:
|
quicklink:
|
||||||
title: 快捷訪問
|
title: 快捷訪問
|
||||||
actions:
|
actions:
|
||||||
user_profile:
|
user_center:
|
||||||
title: 個人資料
|
title: 個人中心
|
||||||
view_site:
|
view_site:
|
||||||
title: 查看站點
|
title: 查看站點
|
||||||
new_post:
|
new_post:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { RouterView } from "vue-router";
|
import BaseApp from "@/components/base-app/BaseApp.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterView />
|
<BaseApp />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -5,25 +5,16 @@ import {
|
||||||
VTag,
|
VTag,
|
||||||
VAvatar,
|
VAvatar,
|
||||||
Dialog,
|
Dialog,
|
||||||
VDropdown,
|
IconLogoutCircleRLine,
|
||||||
VDropdownItem,
|
IconSettings3Line,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
||||||
import type { MenuGroupType, MenuItemType } from "@halo-dev/console-shared";
|
|
||||||
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
||||||
import {
|
import { RouterView, useRoute, useRouter } from "vue-router";
|
||||||
RouterView,
|
|
||||||
useRoute,
|
|
||||||
useRouter,
|
|
||||||
type RouteRecordRaw,
|
|
||||||
} from "vue-router";
|
|
||||||
import { onMounted, reactive, ref } from "vue";
|
import { onMounted, reactive, ref } from "vue";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import LoginModal from "@/components/login/LoginModal.vue";
|
import LoginModal from "@/components/login/LoginModal.vue";
|
||||||
import { coreMenuGroups } from "@console/router/routes.config";
|
import { coreMenuGroups } from "@console/router/constant";
|
||||||
import sortBy from "lodash.sortby";
|
|
||||||
import { useRoleStore } from "@/stores/role";
|
|
||||||
import { hasPermission } from "@/utils/permission";
|
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
import { defineStore, storeToRefs } from "pinia";
|
import { defineStore, storeToRefs } from "pinia";
|
||||||
|
@ -32,6 +23,7 @@ import {
|
||||||
useOverlayScrollbars,
|
useOverlayScrollbars,
|
||||||
type UseOverlayScrollbarsParams,
|
type UseOverlayScrollbarsParams,
|
||||||
} from "overlayscrollbars-vue";
|
} from "overlayscrollbars-vue";
|
||||||
|
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -65,105 +57,7 @@ const handleLogout = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate menus by routes
|
const { menus, minimenus } = useRouteMenuGenerator(coreMenuGroups);
|
||||||
const menus = ref<MenuGroupType[]>([] as MenuGroupType[]);
|
|
||||||
const minimenus = ref<MenuItemType[]>([] as MenuItemType[]);
|
|
||||||
|
|
||||||
const roleStore = useRoleStore();
|
|
||||||
const { uiPermissions } = roleStore.permissions;
|
|
||||||
|
|
||||||
const generateMenus = () => {
|
|
||||||
// sort by menu.priority and meta.core
|
|
||||||
const currentRoutes = sortBy(
|
|
||||||
router.getRoutes().filter((route) => {
|
|
||||||
const { meta } = route;
|
|
||||||
if (!meta?.menu) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (meta.permissions) {
|
|
||||||
return hasPermission(uiPermissions, meta.permissions as string[], true);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
(route: RouteRecordRaw) => !route.meta?.core,
|
|
||||||
(route: RouteRecordRaw) => route.meta?.menu?.priority || 0,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// group by menu.group
|
|
||||||
menus.value = currentRoutes.reduce((acc, route) => {
|
|
||||||
const { menu } = route.meta;
|
|
||||||
if (!menu) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
const group = acc.find((item) => item.id === menu.group);
|
|
||||||
const childRoute = route.children[0];
|
|
||||||
const childMetaMenu = childRoute?.meta?.menu;
|
|
||||||
|
|
||||||
// only support one level
|
|
||||||
const menuChildren = childMetaMenu
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
name: childMetaMenu.name,
|
|
||||||
path: childRoute.path,
|
|
||||||
icon: childMetaMenu.icon,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: undefined;
|
|
||||||
if (group) {
|
|
||||||
group.items?.push({
|
|
||||||
name: menu.name,
|
|
||||||
path: route.path,
|
|
||||||
icon: menu.icon,
|
|
||||||
mobile: menu.mobile,
|
|
||||||
children: menuChildren,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const menuGroup = coreMenuGroups.find((item) => item.id === menu.group);
|
|
||||||
let name = "";
|
|
||||||
if (!menuGroup) {
|
|
||||||
name = menu.group;
|
|
||||||
} else if (menuGroup.name) {
|
|
||||||
name = menuGroup.name;
|
|
||||||
}
|
|
||||||
acc.push({
|
|
||||||
id: menuGroup?.id || menu.group,
|
|
||||||
name: name,
|
|
||||||
priority: menuGroup?.priority || 0,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
name: menu.name,
|
|
||||||
path: route.path,
|
|
||||||
icon: menu.icon,
|
|
||||||
mobile: menu.mobile,
|
|
||||||
children: menuChildren,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, [] as MenuGroupType[]);
|
|
||||||
|
|
||||||
// sort by menu.priority
|
|
||||||
menus.value = sortBy(menus.value, [
|
|
||||||
(menu: MenuGroupType) => {
|
|
||||||
return coreMenuGroups.findIndex((item) => item.id === menu.id) < 0;
|
|
||||||
},
|
|
||||||
(menu: MenuGroupType) => menu.priority || 0,
|
|
||||||
]);
|
|
||||||
|
|
||||||
minimenus.value = menus.value
|
|
||||||
.reduce((acc, group) => {
|
|
||||||
if (group?.items) {
|
|
||||||
acc.push(...group.items);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, [] as MenuItemType[])
|
|
||||||
.filter((item) => item.mobile);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(generateMenus);
|
|
||||||
|
|
||||||
// aside scroll
|
// aside scroll
|
||||||
const navbarScroller = ref();
|
const navbarScroller = ref();
|
||||||
|
@ -201,10 +95,6 @@ onMounted(() => {
|
||||||
initialize({ target: navbarScroller.value });
|
initialize({ target: navbarScroller.value });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleRouteToConsole() {
|
|
||||||
window.location.href = "/console";
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -253,19 +143,25 @@ function handleRouteToConsole() {
|
||||||
</VTag>
|
</VTag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<VDropdown
|
<div class="flex items-center gap-1">
|
||||||
class="profile-control cursor-pointer rounded p-1 transition-all hover:bg-gray-100"
|
<a
|
||||||
|
v-tooltip="'管理控制台'"
|
||||||
|
class="group inline-block cursor-pointer rounded-full p-1.5 transition-all hover:bg-gray-100"
|
||||||
|
href="/console"
|
||||||
>
|
>
|
||||||
<IconMore />
|
<IconSettings3Line
|
||||||
<template #popper>
|
class="h-5 w-5 text-gray-600 group-hover:text-gray-900"
|
||||||
<VDropdownItem @click="handleRouteToConsole">
|
/>
|
||||||
管理控制台
|
</a>
|
||||||
</VDropdownItem>
|
<div
|
||||||
<VDropdownItem @click="handleLogout">
|
class="group inline-block cursor-pointer rounded-full p-1.5 transition-all hover:bg-gray-100"
|
||||||
{{ $t("core.sidebar.operations.logout.button") }}
|
@click="handleLogout"
|
||||||
</VDropdownItem>
|
>
|
||||||
</template>
|
<IconLogoutCircleRLine
|
||||||
</VDropdown>
|
class="h-5 w-5 text-gray-600 group-hover:text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
@ -387,11 +283,8 @@ function handleRouteToConsole() {
|
||||||
|
|
||||||
.profile-name {
|
.profile-name {
|
||||||
@apply flex-1
|
@apply flex-1
|
||||||
self-center;
|
self-center
|
||||||
}
|
overflow-hidden;
|
||||||
|
|
||||||
.profile-control {
|
|
||||||
@apply self-center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { createApp, type DirectiveBinding } from "vue";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import { setupVueQuery } from "@/setup/setupVueQuery";
|
import { setupVueQuery } from "@/setup/setupVueQuery";
|
||||||
import { setupComponents } from "@/setup/setupComponents";
|
import { setupComponents } from "@/setup/setupComponents";
|
||||||
import { setupI18n } from "@/locales";
|
import { getBrowserLanguage, i18n, setupI18n } from "@/locales";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
import { setupCoreModules, setupPluginModules } from "@uc/setup/setupModules";
|
import { setupCoreModules, setupPluginModules } from "@uc/setup/setupModules";
|
||||||
import router from "@uc/router";
|
import router from "@uc/router";
|
||||||
|
@ -11,6 +11,7 @@ import { useUserStore } from "@/stores/user";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useRoleStore } from "@/stores/role";
|
import { useRoleStore } from "@/stores/role";
|
||||||
import { hasPermission } from "@/utils/permission";
|
import { hasPermission } from "@/utils/permission";
|
||||||
|
import { useGlobalInfoStore } from "@/stores/global-info";
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
|
@ -55,19 +56,34 @@ async function loadUserPermissions() {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
async function initApp() {
|
async function initApp() {
|
||||||
|
try {
|
||||||
setupCoreModules(app);
|
setupCoreModules(app);
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
await userStore.fetchCurrentUser();
|
await userStore.fetchCurrentUser();
|
||||||
|
|
||||||
loadUserPermissions();
|
// set locale
|
||||||
|
i18n.global.locale.value =
|
||||||
|
localStorage.getItem("locale") || getBrowserLanguage();
|
||||||
|
|
||||||
|
const globalInfoStore = useGlobalInfoStore();
|
||||||
|
await globalInfoStore.fetchGlobalInfo();
|
||||||
|
|
||||||
|
if (userStore.isAnonymous) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadUserPermissions();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setupPluginModules(app);
|
await setupPluginModules(app);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load plugins", e);
|
console.error("Failed to load plugins", e);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to init app", error);
|
||||||
|
} finally {
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ const handleUpdateUser = async () => {
|
||||||
|
|
||||||
onVisibleChange(false);
|
onVisibleChange(false);
|
||||||
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
queryClient.invalidateQueries({ queryKey: ["user-detail"] });
|
||||||
|
|
||||||
Toast.success(t("core.common.toast.save_success"));
|
Toast.success(t("core.common.toast.save_success"));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { definePlugin } from "@halo-dev/console-shared";
|
import { definePlugin } from "@halo-dev/console-shared";
|
||||||
import BasicLayout from "@uc/layouts/BasicLayout.vue";
|
import BasicLayout from "@uc/layouts/BasicLayout.vue";
|
||||||
import { IconUserLine } from "@halo-dev/components";
|
import { IconAccountCircleLine } from "@halo-dev/components";
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
import Profile from "./Profile.vue";
|
import Profile from "./Profile.vue";
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export default definePlugin({
|
||||||
searchable: true,
|
searchable: true,
|
||||||
menu: {
|
menu: {
|
||||||
name: "我的",
|
name: "我的",
|
||||||
icon: markRaw(IconUserLine),
|
icon: markRaw(IconAccountCircleLine),
|
||||||
priority: 0,
|
priority: 0,
|
||||||
mobile: true,
|
mobile: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { inject, computed } from "vue";
|
import { inject, computed } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
|
||||||
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";
|
||||||
|
@ -20,7 +19,6 @@ import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const user = inject<Ref<DetailedUser | undefined>>("user");
|
const user = inject<Ref<DetailedUser | undefined>>("user");
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const { data: authProviders, isFetching } = useQuery<ListedAuthProvider[]>({
|
const { data: authProviders, isFetching } = useQuery<ListedAuthProvider[]>({
|
||||||
|
@ -88,16 +86,7 @@ const handleBindAuth = (authProvider: ListedAuthProvider) => {
|
||||||
:label="$t('core.user.detail.fields.roles')"
|
:label="$t('core.user.detail.fields.roles')"
|
||||||
class="!px-2"
|
class="!px-2"
|
||||||
>
|
>
|
||||||
<VTag
|
<VTag v-for="role in user?.roles" :key="role.metadata.name">
|
||||||
v-for="(role, index) in user?.roles"
|
|
||||||
:key="index"
|
|
||||||
@click="
|
|
||||||
router.push({
|
|
||||||
name: 'RoleDetail',
|
|
||||||
params: { name: role.metadata.name },
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<IconUserSettings />
|
<IconUserSettings />
|
||||||
</template>
|
</template>
|
||||||
|
@ -118,7 +107,7 @@ const handleBindAuth = (authProvider: ListedAuthProvider) => {
|
||||||
class="!px-2"
|
class="!px-2"
|
||||||
/>
|
/>
|
||||||
<VDescriptionItem
|
<VDescriptionItem
|
||||||
v-if="!isFetching && isCurrentUser && availableAuthProviders?.length"
|
v-if="!isFetching && availableAuthProviders?.length"
|
||||||
:label="$t('core.user.detail.fields.identity_authentication')"
|
:label="$t('core.user.detail.fields.identity_authentication')"
|
||||||
class="!px-2"
|
class="!px-2"
|
||||||
>
|
>
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import type { MenuGroupType } from "@halo-dev/console-shared";
|
||||||
|
|
||||||
|
export const coreMenuGroups: MenuGroupType[] = [
|
||||||
|
{
|
||||||
|
id: "dashboard",
|
||||||
|
name: undefined,
|
||||||
|
priority: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "content",
|
||||||
|
name: "core.sidebar.menu.groups.content",
|
||||||
|
priority: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "interface",
|
||||||
|
name: "core.sidebar.menu.groups.interface",
|
||||||
|
priority: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "system",
|
||||||
|
name: "core.sidebar.menu.groups.system",
|
||||||
|
priority: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tool",
|
||||||
|
name: "core.sidebar.menu.groups.tool",
|
||||||
|
priority: 4,
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
import type { Router } from "vue-router";
|
||||||
|
|
||||||
|
export function setupAuthCheckGuard(router: Router) {
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
if (userStore.isAnonymous) {
|
||||||
|
window.location.href = `/console/login?redirect_uri=${encodeURIComponent(
|
||||||
|
window.location.href
|
||||||
|
)}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { useRoleStore } from "@/stores/role";
|
||||||
|
import { hasPermission } from "@/utils/permission";
|
||||||
|
import type { Router } from "vue-router";
|
||||||
|
|
||||||
|
export function setupPermissionGuard(router: Router) {
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const roleStore = useRoleStore();
|
||||||
|
const { uiPermissions } = roleStore.permissions;
|
||||||
|
const { meta } = to;
|
||||||
|
if (meta && meta.permissions) {
|
||||||
|
const flag = hasPermission(
|
||||||
|
Array.from(uiPermissions),
|
||||||
|
meta.permissions as string[],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (!flag) {
|
||||||
|
next({ name: "Forbidden" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ import {
|
||||||
type RouteLocationNormalizedLoaded,
|
type RouteLocationNormalizedLoaded,
|
||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
import routesConfig from "@uc/router/routes.config";
|
import routesConfig from "@uc/router/routes.config";
|
||||||
|
import { setupAuthCheckGuard } from "./guards/auth-check";
|
||||||
|
import { setupPermissionGuard } from "./guards/permission";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
@ -19,4 +21,7 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setupAuthCheckGuard(router);
|
||||||
|
setupPermissionGuard(router);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -2,7 +2,6 @@ import type { RouteRecordRaw } from "vue-router";
|
||||||
import NotFound from "@/views/exceptions/NotFound.vue";
|
import NotFound from "@/views/exceptions/NotFound.vue";
|
||||||
import Forbidden from "@/views/exceptions/Forbidden.vue";
|
import Forbidden from "@/views/exceptions/Forbidden.vue";
|
||||||
import BasicLayout from "@uc/layouts/BasicLayout.vue";
|
import BasicLayout from "@uc/layouts/BasicLayout.vue";
|
||||||
import type { MenuGroupType } from "@halo-dev/console-shared";
|
|
||||||
|
|
||||||
export const routes: Array<RouteRecordRaw> = [
|
export const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
|
@ -23,32 +22,4 @@ export const routes: Array<RouteRecordRaw> = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const coreMenuGroups: MenuGroupType[] = [
|
|
||||||
{
|
|
||||||
id: "dashboard",
|
|
||||||
name: undefined,
|
|
||||||
priority: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "content",
|
|
||||||
name: "core.sidebar.menu.groups.content",
|
|
||||||
priority: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "interface",
|
|
||||||
name: "core.sidebar.menu.groups.interface",
|
|
||||||
priority: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "system",
|
|
||||||
name: "core.sidebar.menu.groups.system",
|
|
||||||
priority: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "tool",
|
|
||||||
name: "core.sidebar.menu.groups.tool",
|
|
||||||
priority: 4,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
|
|
@ -21,7 +21,7 @@ export async function setupPluginModules(app: App) {
|
||||||
const { load } = useScriptTag(
|
const { load } = useScriptTag(
|
||||||
`${
|
`${
|
||||||
import.meta.env.VITE_API_URL
|
import.meta.env.VITE_API_URL
|
||||||
}/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.js`
|
}/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.js?t=${Date.now()}`
|
||||||
);
|
);
|
||||||
|
|
||||||
await load();
|
await load();
|
||||||
|
@ -45,7 +45,7 @@ export async function setupPluginModules(app: App) {
|
||||||
await loadStyle(
|
await loadStyle(
|
||||||
`${
|
`${
|
||||||
import.meta.env.VITE_API_URL
|
import.meta.env.VITE_API_URL
|
||||||
}/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.css`
|
}/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.css?t=${Date.now()}`
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const message = i18n.global.t("core.plugin.loader.toast.style_load_failed");
|
const message = i18n.global.t("core.plugin.loader.toast.style_load_failed");
|
||||||
|
|
Loading…
Reference in New Issue