mirror of https://github.com/halo-dev/halo
parent
96a01cccc7
commit
a951a34a1b
|
@ -1,129 +1,7 @@
|
|||
<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 { 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)
|
||||
);
|
||||
import BaseApp from "@/components/base-app/BaseApp.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
<BaseApp />
|
||||
</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,
|
||||
IconSearch,
|
||||
IconUserSettings,
|
||||
IconLogoutCircleRLine,
|
||||
VTag,
|
||||
VAvatar,
|
||||
Dialog,
|
||||
VDropdown,
|
||||
VDropdownItem,
|
||||
IconAccountCircleLine,
|
||||
} from "@halo-dev/components";
|
||||
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 {
|
||||
RouterView,
|
||||
useRoute,
|
||||
useRouter,
|
||||
type RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
import { RouterView, useRoute, useRouter } from "vue-router";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import axios from "axios";
|
||||
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
|
||||
import LoginModal from "@/components/login/LoginModal.vue";
|
||||
import { coreMenuGroups } from "@console/router/routes.config";
|
||||
import sortBy from "lodash.sortby";
|
||||
import { useRoleStore } from "@/stores/role";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
import { coreMenuGroups } from "@console/router/constant";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { defineStore, storeToRefs } from "pinia";
|
||||
|
@ -36,6 +27,7 @@ import {
|
|||
} from "overlayscrollbars-vue";
|
||||
import { isMac } from "@/utils/device";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
@ -79,105 +71,7 @@ useEventListener(document, "keydown", (e: KeyboardEvent) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Generate menus by routes
|
||||
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);
|
||||
const { menus, minimenus } = useRouteMenuGenerator(coreMenuGroups);
|
||||
|
||||
// aside scroll
|
||||
const navbarScroller = ref();
|
||||
|
@ -215,10 +109,6 @@ onMounted(() => {
|
|||
initialize({ target: navbarScroller.value });
|
||||
}
|
||||
});
|
||||
|
||||
function handleRouteToUC() {
|
||||
window.location.href = "/uc";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -262,12 +152,15 @@ function handleRouteToUC() {
|
|||
<VAvatar
|
||||
:src="currentUser?.spec.avatar"
|
||||
:alt="currentUser?.spec.displayName"
|
||||
size="md"
|
||||
size="sm"
|
||||
circle
|
||||
></VAvatar>
|
||||
</div>
|
||||
<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 }}
|
||||
</div>
|
||||
<div v-if="currentRoles?.[0]" class="flex">
|
||||
|
@ -283,19 +176,26 @@ function handleRouteToUC() {
|
|||
</VTag>
|
||||
</div>
|
||||
</div>
|
||||
<VDropdown
|
||||
class="profile-control cursor-pointer rounded p-1 transition-all hover:bg-gray-100"
|
||||
>
|
||||
<IconMore />
|
||||
<template #popper>
|
||||
<VDropdownItem @click="handleRouteToUC">
|
||||
{{ $t("core.sidebar.operations.profile.button") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem @click="handleLogout">
|
||||
{{ $t("core.sidebar.operations.logout.button") }}
|
||||
</VDropdownItem>
|
||||
</template>
|
||||
</VDropdown>
|
||||
|
||||
<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"
|
||||
>
|
||||
<IconAccountCircleLine
|
||||
class="h-5 w-5 text-gray-600 group-hover:text-gray-900"
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
class="group inline-block cursor-pointer rounded-full p-1.5 transition-all hover:bg-gray-100"
|
||||
@click="handleLogout"
|
||||
>
|
||||
<IconLogoutCircleRLine
|
||||
class="h-5 w-5 text-gray-600 group-hover:text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
@ -418,11 +318,8 @@ function handleRouteToUC() {
|
|||
|
||||
.profile-name {
|
||||
@apply flex-1
|
||||
self-center;
|
||||
}
|
||||
|
||||
.profile-control {
|
||||
@apply self-center;
|
||||
self-center
|
||||
overflow-hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
IconSearch,
|
||||
IconDatabase2Line,
|
||||
VCard,
|
||||
IconUserLine,
|
||||
IconAccountCircleLine,
|
||||
Dialog,
|
||||
Toast,
|
||||
} from "@halo-dev/components";
|
||||
|
@ -37,9 +37,9 @@ const { t } = useI18n();
|
|||
|
||||
const actions: Action[] = [
|
||||
{
|
||||
icon: markRaw(IconUserLine),
|
||||
icon: markRaw(IconAccountCircleLine),
|
||||
title: t(
|
||||
"core.dashboard.widgets.presets.quicklink.actions.user_profile.title"
|
||||
"core.dashboard.widgets.presets.quicklink.actions.user_center.title"
|
||||
),
|
||||
action: () => {
|
||||
window.location.href = "/uc/profile";
|
||||
|
|
|
@ -22,9 +22,11 @@ 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";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
const { currentUser } = useUserStore();
|
||||
|
||||
interface UserTab {
|
||||
id: string;
|
||||
|
@ -84,6 +86,10 @@ provide<Ref<string>>("activeTab", activeTab);
|
|||
const tabbarItems = computed(() => {
|
||||
return tabs.map((tab) => ({ id: tab.id, label: tab.label }));
|
||||
});
|
||||
|
||||
function handleRouteToUC() {
|
||||
window.location.href = "/uc";
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<UserEditingModal v-model:visible="editingModal" :user="user?.user" />
|
||||
|
@ -111,8 +117,15 @@ const tabbarItems = computed(() => {
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentUserHasPermission(['system:users:manage'])">
|
||||
<VDropdown>
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<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">
|
||||
{{ $t("core.common.buttons.edit") }}
|
||||
</VButton>
|
||||
|
|
|
@ -1,72 +1,17 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
Dialog,
|
||||
IconUserSettings,
|
||||
VButton,
|
||||
VDescription,
|
||||
VDescriptionItem,
|
||||
VTag,
|
||||
} from "@halo-dev/components";
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
import { inject, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import type { DetailedUser, ListedAuthProvider } from "@halo-dev/api-client";
|
||||
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";
|
||||
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 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>
|
||||
<template>
|
||||
<div class="border-t border-gray-100">
|
||||
|
@ -94,7 +39,7 @@ const handleBindAuth = (authProvider: ListedAuthProvider) => {
|
|||
v-for="(role, index) in user?.roles"
|
||||
:key="index"
|
||||
@click="
|
||||
router.push({
|
||||
$router.push({
|
||||
name: 'RoleDetail',
|
||||
params: { name: role.metadata.name },
|
||||
})
|
||||
|
@ -119,50 +64,6 @@ const handleBindAuth = (authProvider: ListedAuthProvider) => {
|
|||
:content="formatDatetime(user?.user.metadata?.creationTimestamp)"
|
||||
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>
|
||||
</div>
|
||||
</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 Setup from "@console/views/system/Setup.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";
|
||||
|
||||
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;
|
||||
|
|
|
@ -119,6 +119,7 @@
|
|||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.isequal": "^4.5.6",
|
||||
"@types/lodash.merge": "^4.6.7",
|
||||
"@types/lodash.sortby": "^4.7.9",
|
||||
"@types/node": "^18.11.19",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@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 IconRiUpload2Fill from "~icons/ri/upload-2-fill";
|
||||
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 {
|
||||
IconDashboard,
|
||||
|
@ -138,4 +141,7 @@ export {
|
|||
IconArrowUpDownLine,
|
||||
IconRiUpload2Fill,
|
||||
IconNotificationBadgeLine,
|
||||
IconLogoutCircleRLine,
|
||||
IconAccountCircleLine,
|
||||
IconSettings3Line,
|
||||
};
|
||||
|
|
|
@ -243,6 +243,9 @@ importers:
|
|||
'@types/lodash.merge':
|
||||
specifier: ^4.6.7
|
||||
version: 4.6.7
|
||||
'@types/lodash.sortby':
|
||||
specifier: ^4.7.9
|
||||
version: 4.7.9
|
||||
'@types/node':
|
||||
specifier: ^18.11.19
|
||||
version: 18.13.0
|
||||
|
@ -3952,6 +3955,12 @@ packages:
|
|||
'@types/lodash': 4.14.186
|
||||
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:
|
||||
resolution: {integrity: sha512-eHcVlLXP0c2FlMPm56ITode2AgLMSa6aJ05JTTbYbI+7EMkCEE5qk2E41d5g2lCVTqRe0GnnRFurmlCsDODrPw==}
|
||||
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:
|
||||
title: Quick Link
|
||||
actions:
|
||||
user_profile:
|
||||
title: User Profile
|
||||
user_center:
|
||||
title: User Center
|
||||
view_site:
|
||||
title: View Site
|
||||
new_post:
|
||||
|
|
|
@ -105,7 +105,7 @@ core:
|
|||
quicklink:
|
||||
title: Enlace Rápido
|
||||
actions:
|
||||
user_profile:
|
||||
user_center:
|
||||
title: Perfil de Usuario
|
||||
view_site:
|
||||
title: Ver Sitio
|
||||
|
|
|
@ -106,8 +106,8 @@ core:
|
|||
quicklink:
|
||||
title: 快捷访问
|
||||
actions:
|
||||
user_profile:
|
||||
title: 个人资料
|
||||
user_center:
|
||||
title: 个人中心
|
||||
view_site:
|
||||
title: 查看站点
|
||||
new_post:
|
||||
|
|
|
@ -106,8 +106,8 @@ core:
|
|||
quicklink:
|
||||
title: 快捷訪問
|
||||
actions:
|
||||
user_profile:
|
||||
title: 個人資料
|
||||
user_center:
|
||||
title: 個人中心
|
||||
view_site:
|
||||
title: 查看站點
|
||||
new_post:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { RouterView } from "vue-router";
|
||||
import BaseApp from "@/components/base-app/BaseApp.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
<BaseApp />
|
||||
</template>
|
||||
|
|
|
@ -5,25 +5,16 @@ import {
|
|||
VTag,
|
||||
VAvatar,
|
||||
Dialog,
|
||||
VDropdown,
|
||||
VDropdownItem,
|
||||
IconLogoutCircleRLine,
|
||||
IconSettings3Line,
|
||||
} from "@halo-dev/components";
|
||||
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 {
|
||||
RouterView,
|
||||
useRoute,
|
||||
useRouter,
|
||||
type RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
import { RouterView, useRoute, useRouter } from "vue-router";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import axios from "axios";
|
||||
import LoginModal from "@/components/login/LoginModal.vue";
|
||||
import { coreMenuGroups } from "@console/router/routes.config";
|
||||
import sortBy from "lodash.sortby";
|
||||
import { useRoleStore } from "@/stores/role";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
import { coreMenuGroups } from "@console/router/constant";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { defineStore, storeToRefs } from "pinia";
|
||||
|
@ -32,6 +23,7 @@ import {
|
|||
useOverlayScrollbars,
|
||||
type UseOverlayScrollbarsParams,
|
||||
} from "overlayscrollbars-vue";
|
||||
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
@ -65,105 +57,7 @@ const handleLogout = () => {
|
|||
});
|
||||
};
|
||||
|
||||
// Generate menus by routes
|
||||
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);
|
||||
const { menus, minimenus } = useRouteMenuGenerator(coreMenuGroups);
|
||||
|
||||
// aside scroll
|
||||
const navbarScroller = ref();
|
||||
|
@ -201,10 +95,6 @@ onMounted(() => {
|
|||
initialize({ target: navbarScroller.value });
|
||||
}
|
||||
});
|
||||
|
||||
function handleRouteToConsole() {
|
||||
window.location.href = "/console";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -253,19 +143,25 @@ function handleRouteToConsole() {
|
|||
</VTag>
|
||||
</div>
|
||||
</div>
|
||||
<VDropdown
|
||||
class="profile-control cursor-pointer rounded p-1 transition-all hover:bg-gray-100"
|
||||
>
|
||||
<IconMore />
|
||||
<template #popper>
|
||||
<VDropdownItem @click="handleRouteToConsole">
|
||||
管理控制台
|
||||
</VDropdownItem>
|
||||
<VDropdownItem @click="handleLogout">
|
||||
{{ $t("core.sidebar.operations.logout.button") }}
|
||||
</VDropdownItem>
|
||||
</template>
|
||||
</VDropdown>
|
||||
<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="/console"
|
||||
>
|
||||
<IconSettings3Line
|
||||
class="h-5 w-5 text-gray-600 group-hover:text-gray-900"
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
class="group inline-block cursor-pointer rounded-full p-1.5 transition-all hover:bg-gray-100"
|
||||
@click="handleLogout"
|
||||
>
|
||||
<IconLogoutCircleRLine
|
||||
class="h-5 w-5 text-gray-600 group-hover:text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
@ -387,11 +283,8 @@ function handleRouteToConsole() {
|
|||
|
||||
.profile-name {
|
||||
@apply flex-1
|
||||
self-center;
|
||||
}
|
||||
|
||||
.profile-control {
|
||||
@apply self-center;
|
||||
self-center
|
||||
overflow-hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { createApp, type DirectiveBinding } from "vue";
|
|||
import App from "./App.vue";
|
||||
import { setupVueQuery } from "@/setup/setupVueQuery";
|
||||
import { setupComponents } from "@/setup/setupComponents";
|
||||
import { setupI18n } from "@/locales";
|
||||
import { getBrowserLanguage, i18n, setupI18n } from "@/locales";
|
||||
import { createPinia } from "pinia";
|
||||
import { setupCoreModules, setupPluginModules } from "@uc/setup/setupModules";
|
||||
import router from "@uc/router";
|
||||
|
@ -11,6 +11,7 @@ import { useUserStore } from "@/stores/user";
|
|||
import { apiClient } from "@/utils/api-client";
|
||||
import { useRoleStore } from "@/stores/role";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
import { useGlobalInfoStore } from "@/stores/global-info";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
|
@ -55,19 +56,34 @@ async function loadUserPermissions() {
|
|||
})();
|
||||
|
||||
async function initApp() {
|
||||
setupCoreModules(app);
|
||||
|
||||
const userStore = useUserStore();
|
||||
await userStore.fetchCurrentUser();
|
||||
|
||||
loadUserPermissions();
|
||||
|
||||
try {
|
||||
await setupPluginModules(app);
|
||||
} catch (e) {
|
||||
console.error("Failed to load plugins", e);
|
||||
}
|
||||
setupCoreModules(app);
|
||||
|
||||
app.use(router);
|
||||
app.mount("#app");
|
||||
const userStore = useUserStore();
|
||||
await userStore.fetchCurrentUser();
|
||||
|
||||
// set locale
|
||||
i18n.global.locale.value =
|
||||
localStorage.getItem("locale") || getBrowserLanguage();
|
||||
|
||||
const globalInfoStore = useGlobalInfoStore();
|
||||
await globalInfoStore.fetchGlobalInfo();
|
||||
|
||||
if (userStore.isAnonymous) {
|
||||
return;
|
||||
}
|
||||
|
||||
await loadUserPermissions();
|
||||
|
||||
try {
|
||||
await setupPluginModules(app);
|
||||
} catch (e) {
|
||||
console.error("Failed to load plugins", e);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to init app", error);
|
||||
} finally {
|
||||
app.use(router);
|
||||
app.mount("#app");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@ const handleUpdateUser = async () => {
|
|||
|
||||
onVisibleChange(false);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["user-detail"] });
|
||||
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { definePlugin } from "@halo-dev/console-shared";
|
||||
import BasicLayout from "@uc/layouts/BasicLayout.vue";
|
||||
import { IconUserLine } from "@halo-dev/components";
|
||||
import { IconAccountCircleLine } from "@halo-dev/components";
|
||||
import { markRaw } from "vue";
|
||||
import Profile from "./Profile.vue";
|
||||
|
||||
|
@ -21,7 +21,7 @@ export default definePlugin({
|
|||
searchable: true,
|
||||
menu: {
|
||||
name: "我的",
|
||||
icon: markRaw(IconUserLine),
|
||||
icon: markRaw(IconAccountCircleLine),
|
||||
priority: 0,
|
||||
mobile: true,
|
||||
},
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
} from "@halo-dev/components";
|
||||
import type { Ref } from "vue";
|
||||
import { inject, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import type { DetailedUser, ListedAuthProvider } from "@halo-dev/api-client";
|
||||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
|
@ -20,7 +19,6 @@ import { useI18n } from "vue-i18n";
|
|||
|
||||
const user = inject<Ref<DetailedUser | undefined>>("user");
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { data: authProviders, isFetching } = useQuery<ListedAuthProvider[]>({
|
||||
|
@ -88,16 +86,7 @@ const handleBindAuth = (authProvider: ListedAuthProvider) => {
|
|||
:label="$t('core.user.detail.fields.roles')"
|
||||
class="!px-2"
|
||||
>
|
||||
<VTag
|
||||
v-for="(role, index) in user?.roles"
|
||||
:key="index"
|
||||
@click="
|
||||
router.push({
|
||||
name: 'RoleDetail',
|
||||
params: { name: role.metadata.name },
|
||||
})
|
||||
"
|
||||
>
|
||||
<VTag v-for="role in user?.roles" :key="role.metadata.name">
|
||||
<template #leftIcon>
|
||||
<IconUserSettings />
|
||||
</template>
|
||||
|
@ -118,7 +107,7 @@ const handleBindAuth = (authProvider: ListedAuthProvider) => {
|
|||
class="!px-2"
|
||||
/>
|
||||
<VDescriptionItem
|
||||
v-if="!isFetching && isCurrentUser && availableAuthProviders?.length"
|
||||
v-if="!isFetching && availableAuthProviders?.length"
|
||||
:label="$t('core.user.detail.fields.identity_authentication')"
|
||||
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,
|
||||
} from "vue-router";
|
||||
import routesConfig from "@uc/router/routes.config";
|
||||
import { setupAuthCheckGuard } from "./guards/auth-check";
|
||||
import { setupPermissionGuard } from "./guards/permission";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
@ -19,4 +21,7 @@ const router = createRouter({
|
|||
},
|
||||
});
|
||||
|
||||
setupAuthCheckGuard(router);
|
||||
setupPermissionGuard(router);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -2,7 +2,6 @@ import type { RouteRecordRaw } from "vue-router";
|
|||
import NotFound from "@/views/exceptions/NotFound.vue";
|
||||
import Forbidden from "@/views/exceptions/Forbidden.vue";
|
||||
import BasicLayout from "@uc/layouts/BasicLayout.vue";
|
||||
import type { MenuGroupType } from "@halo-dev/console-shared";
|
||||
|
||||
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;
|
||||
|
|
|
@ -21,7 +21,7 @@ export async function setupPluginModules(app: App) {
|
|||
const { load } = useScriptTag(
|
||||
`${
|
||||
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();
|
||||
|
@ -45,7 +45,7 @@ export async function setupPluginModules(app: App) {
|
|||
await loadStyle(
|
||||
`${
|
||||
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) {
|
||||
const message = i18n.global.t("core.plugin.loader.toast.style_load_failed");
|
||||
|
|
Loading…
Reference in New Issue