feat(admin-ui): implement RuoYi sidebar shell
This commit is contained in:
125
WonderQ-Admin-UI-Vue/src/components/layout/Sidebar.vue
Normal file
125
WonderQ-Admin-UI-Vue/src/components/layout/Sidebar.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { visibleNavigableMenus } from "@/lib/menu-display";
|
||||
import type { NavigationType, SideTheme } from "@/lib/layout-settings";
|
||||
import type { AdminMenu } from "@/types";
|
||||
import SidebarItem from "@/components/layout/SidebarItem.vue";
|
||||
|
||||
type ThemeValue = SideTheme | "dark" | "light";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
collapsed?: boolean;
|
||||
sideTheme?: ThemeValue;
|
||||
sidebarLogo?: boolean;
|
||||
showLogo?: boolean;
|
||||
navigationType?: NavigationType;
|
||||
}>(), {
|
||||
collapsed: false,
|
||||
sideTheme: "theme-dark",
|
||||
sidebarLogo: true,
|
||||
navigationType: "left",
|
||||
});
|
||||
|
||||
const auth = useAuthStore();
|
||||
const layout = useLayoutStore();
|
||||
const route = useRoute();
|
||||
|
||||
const activePath = computed(() => route.path);
|
||||
const isMobile = computed(() => layout.device === "mobile");
|
||||
const collapsedMenu = computed(() => !isMobile.value && props.collapsed);
|
||||
const isDark = computed(() => props.sideTheme === "theme-dark" || props.sideTheme === "dark");
|
||||
const isVisible = computed(() => props.navigationType !== "top");
|
||||
const showLogo = computed(() => props.showLogo ?? props.sidebarLogo);
|
||||
const menus = computed(() => visibleNavigableMenus<AdminMenu>(auth.menus));
|
||||
const menuBackgroundColor = computed(() => isDark.value ? "#304156" : "#ffffff");
|
||||
const menuTextColor = computed(() => isDark.value ? "#bfcbd9" : "#303133");
|
||||
const menuActiveTextColor = computed(() => isDark.value ? "#ffffff" : "#409eff");
|
||||
|
||||
const sidebarStyle = computed(() => {
|
||||
const width = isMobile.value || !props.collapsed ? "200px" : "64px";
|
||||
return {
|
||||
width,
|
||||
minWidth: width,
|
||||
transform: isMobile.value && !layout.sidebarOpened ? "translateX(-200px)" : "translateX(0)",
|
||||
backgroundColor: menuBackgroundColor.value,
|
||||
position: isMobile.value ? "fixed" : "relative",
|
||||
top: isMobile.value ? "0" : undefined,
|
||||
bottom: isMobile.value ? "0" : undefined,
|
||||
left: isMobile.value ? "0" : undefined,
|
||||
zIndex: isMobile.value ? "1001" : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
function handleMenuSelect() {
|
||||
if (isMobile.value) layout.closeSidebar();
|
||||
}
|
||||
|
||||
function closeSidebar() {
|
||||
layout.closeSidebar();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="isVisible"
|
||||
:class="[
|
||||
'sidebar-shell',
|
||||
`sidebar-shell--${props.navigationType}`,
|
||||
{ 'sidebar-shell--mobile': isMobile, 'sidebar-shell--collapsed': collapsedMenu },
|
||||
]"
|
||||
>
|
||||
<button
|
||||
v-if="isMobile && layout.sidebarOpened"
|
||||
type="button"
|
||||
class="sidebar-overlay"
|
||||
aria-label="关闭侧边栏"
|
||||
@click="closeSidebar"
|
||||
/>
|
||||
|
||||
<aside
|
||||
:class="[
|
||||
'sidebar-drawer',
|
||||
isDark ? 'sidebar-drawer--dark' : 'sidebar-drawer--light',
|
||||
{ 'sidebar-drawer--mobile': isMobile, 'sidebar-drawer--collapsed': collapsedMenu },
|
||||
]"
|
||||
:style="sidebarStyle"
|
||||
:aria-hidden="isMobile && !layout.sidebarOpened ? 'true' : undefined"
|
||||
>
|
||||
<div v-if="showLogo" class="sidebar-logo" :class="{ 'sidebar-logo--collapsed': collapsedMenu }">
|
||||
<div class="sidebar-logo__mark" aria-hidden="true">WQ</div>
|
||||
<div v-if="!collapsedMenu" class="sidebar-logo__copy">
|
||||
<strong>WonderQ</strong>
|
||||
<span>内容运营后台</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-scrollbar class="sidebar-scrollbar">
|
||||
<el-menu
|
||||
:default-active="activePath"
|
||||
:collapse="collapsedMenu"
|
||||
:collapse-transition="false"
|
||||
:collapse-tooltip="true"
|
||||
:unique-opened="true"
|
||||
:router="true"
|
||||
:background-color="menuBackgroundColor"
|
||||
:text-color="menuTextColor"
|
||||
:active-text-color="menuActiveTextColor"
|
||||
class="sidebar-menu"
|
||||
@select="handleMenuSelect"
|
||||
>
|
||||
<SidebarItem
|
||||
v-for="menu in menus"
|
||||
:key="menu.id"
|
||||
:menu="menu"
|
||||
base-path=""
|
||||
:collapsed="collapsedMenu"
|
||||
:navigation-type="props.navigationType"
|
||||
/>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
59
WonderQ-Admin-UI-Vue/src/components/layout/SidebarItem.vue
Normal file
59
WonderQ-Admin-UI-Vue/src/components/layout/SidebarItem.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import LayoutIcon from "@/components/layout/LayoutIcon.vue";
|
||||
import { getMenuIconName } from "@/lib/menu-display";
|
||||
import type { NavigationType } from "@/lib/layout-settings";
|
||||
import type { AdminMenu } from "@/types";
|
||||
|
||||
type SidebarMenu = AdminMenu & { isVisible?: boolean };
|
||||
|
||||
withDefaults(defineProps<{
|
||||
menu: SidebarMenu;
|
||||
basePath?: string;
|
||||
collapsed?: boolean;
|
||||
navigationType?: NavigationType;
|
||||
}>(), {
|
||||
basePath: "",
|
||||
collapsed: false,
|
||||
navigationType: "left",
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-sub-menu
|
||||
v-if="menu.type === 'directory'"
|
||||
:index="menu.path || menu.id"
|
||||
:aria-label="menu.name"
|
||||
:class="['sidebar-item', `sidebar-item--${navigationType}`]"
|
||||
>
|
||||
<template #title>
|
||||
<el-tooltip :content="menu.name" :disabled="!collapsed" placement="right">
|
||||
<span class="sidebar-item__content">
|
||||
<LayoutIcon :name="getMenuIconName(menu)" :title="collapsed ? menu.name : undefined" size="18" />
|
||||
<span v-if="!collapsed" class="sidebar-item__label">{{ menu.name }}</span>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<SidebarItem
|
||||
v-for="child in menu.children || []"
|
||||
:key="child.id"
|
||||
:menu="child"
|
||||
:base-path="basePath"
|
||||
:collapsed="collapsed"
|
||||
:navigation-type="navigationType"
|
||||
/>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item
|
||||
v-else-if="menu.type === 'page' && menu.path"
|
||||
:index="menu.path"
|
||||
:aria-label="menu.name"
|
||||
:class="['sidebar-item', `sidebar-item--${navigationType}`]"
|
||||
>
|
||||
<el-tooltip :content="menu.name" :disabled="!collapsed" placement="right">
|
||||
<span class="sidebar-item__content">
|
||||
<LayoutIcon :name="getMenuIconName(menu)" :title="collapsed ? menu.name : undefined" size="18" />
|
||||
<span v-if="!collapsed" class="sidebar-item__label">{{ menu.name }}</span>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
43
WonderQ-Admin-UI-Vue/src/components/layout/TopMenu.vue
Normal file
43
WonderQ-Admin-UI-Vue/src/components/layout/TopMenu.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { visibleNavigableMenus } from "@/lib/menu-display";
|
||||
import type { NavigationType } from "@/lib/layout-settings";
|
||||
import type { AdminMenu } from "@/types";
|
||||
import SidebarItem from "@/components/layout/SidebarItem.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
menus?: AdminMenu[];
|
||||
navType?: NavigationType;
|
||||
navigationType?: NavigationType;
|
||||
}>();
|
||||
|
||||
const auth = useAuthStore();
|
||||
const route = useRoute();
|
||||
const navigationType = computed(() => props.navType ?? props.navigationType ?? "left");
|
||||
const activePath = computed(() => route.path);
|
||||
const menus = computed(() => visibleNavigableMenus<AdminMenu>(props.menus ?? auth.menus));
|
||||
const isVisible = computed(() => navigationType.value === "top" || navigationType.value === "mix");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav v-if="isVisible" class="top-menu" aria-label="顶部导航">
|
||||
<el-menu
|
||||
mode="horizontal"
|
||||
:default-active="activePath"
|
||||
:router="true"
|
||||
:unique-opened="true"
|
||||
class="top-menu__bar"
|
||||
>
|
||||
<SidebarItem
|
||||
v-for="menu in menus"
|
||||
:key="menu.id"
|
||||
:menu="menu"
|
||||
base-path=""
|
||||
:collapsed="false"
|
||||
:navigation-type="navigationType"
|
||||
/>
|
||||
</el-menu>
|
||||
</nav>
|
||||
</template>
|
||||
Reference in New Issue
Block a user