Files
rui-docs/admin-ui/src/layout/SideLayout.vue
T
vifo 82a19101a8 chore: 初始化前端仓库并迁移 admin-ui
- 创建 rui-frontend 前端仓库
- 迁移 admin-ui 管理后台
- 创建 cashier-mobile 和 customer-mobile 占位项目
- 配置 pnpm workspace
2026-06-04 05:14:11 +08:00

215 lines
8.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { useUserStore } from '@/stores/user'
import { useMenu } from '@/composables/useMenu'
import TagsBar from '@/components/TagsBar.vue'
import RuiIcon from '@/components/RuiIcon.vue'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const app = useAppStore()
const user = useUserStore()
const { menuItems, loadMenus, getBreadcrumbs } = useMenu()
const isCollapse = ref(false)
/**
* 加载菜单
*/
async function loadMenuData() {
try {
await loadMenus('admin')
} catch {
// 错误已由请求拦截器统一提示
}
}
/**
* 面包屑数据
*/
const breadcrumbs = computed(() => {
const crumbs = getBreadcrumbs(route.path)
if (crumbs.length === 0) {
return [{ title: t('common.breadcrumb.home'), path: '/dashboard' }]
}
return [{ title: t('common.breadcrumb.home'), path: '/dashboard' }, ...crumbs]
})
onMounted(() => {
loadMenuData()
})
</script>
<template>
<el-container class="h-screen">
<!-- 侧边栏 -->
<el-aside :width="isCollapse ? '64px' : '220px'" class="side-bar">
<div class="side-logo" :class="{ collapsed: isCollapse }">
<el-icon :size="22"><Setting /></el-icon>
<span v-show="!isCollapse" class="side-logo-text">{{ t('app.title') }}</span>
</div>
<el-scrollbar>
<el-menu :default-active="route.path" :collapse="isCollapse" router class="side-menu" :collapse-transition="false">
<template v-for="item in menuItems" :key="item.id">
<!-- 有子菜单 -->
<el-sub-menu v-if="item.children?.length" :index="String(item.id)">
<template #title>
<el-tooltip v-if="isCollapse" :content="item.title" placement="right">
<RuiIcon v-if="item.icon" :icon="item.icon" :size="16" class="menu-icon" />
</el-tooltip>
<RuiIcon v-else-if="item.icon" :icon="item.icon" :size="16" class="menu-icon" />
<span>{{ item.title }}</span>
</template>
<template v-for="child in item.children" :key="child.id">
<!-- 子菜单还有子菜单三级菜单 -->
<el-sub-menu v-if="child.children?.length" :index="String(child.id)">
<template #title>
<RuiIcon v-if="child.icon" :icon="child.icon" :size="16" class="menu-icon" />
<span>{{ child.title }}</span>
</template>
<el-menu-item v-for="sub in child.children" :key="sub.id" :index="sub.path">
<RuiIcon v-if="sub.icon" :icon="sub.icon" :size="16" class="menu-icon" />
<span>{{ sub.title }}</span>
</el-menu-item>
</el-sub-menu>
<!-- 二级菜单 -->
<el-menu-item v-else :index="child.path">
<RuiIcon v-if="child.icon" :icon="child.icon" :size="16" class="menu-icon" />
<span>{{ child.title }}</span>
</el-menu-item>
</template>
</el-sub-menu>
<!-- 无子菜单 -->
<el-menu-item v-else :index="item.path">
<el-tooltip v-if="isCollapse" :content="item.title" placement="right">
<RuiIcon v-if="item.icon" :icon="item.icon" :size="16" class="menu-icon" />
</el-tooltip>
<RuiIcon v-else-if="item.icon" :icon="item.icon" :size="16" class="menu-icon" />
<span>{{ item.title }}</span>
</el-menu-item>
</template>
</el-menu>
</el-scrollbar>
</el-aside>
<!-- 主内容区 -->
<el-container class="flex-1">
<!-- 顶部栏 -->
<el-header class="top-bar">
<div class="top-bar-inner">
<div class="flex items-center gap-3">
<el-icon :size="20" class="cursor-pointer hover:text-blue-500 transition-colors" @click="isCollapse = !isCollapse">
<Fold v-if="!isCollapse" /><Expand v-else />
</el-icon>
<!-- 面包屑 -->
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="(crumb, index) in breadcrumbs" :key="index" :to="crumb.path && index < breadcrumbs.length - 1 ? crumb.path : undefined">
{{ crumb.title }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="flex items-center gap-4">
<!-- 暗黑模式切换 -->
<el-tooltip :content="app.dark ? '切换亮色模式' : '切换暗黑模式'" placement="bottom">
<el-icon :size="18" class="cursor-pointer hover:text-blue-500 transition-colors" @click="app.toggleDark()">
<Moon v-if="!app.dark" /><Sunny v-else />
</el-icon>
</el-tooltip>
<!-- 主题配置 -->
<el-tooltip content="主题配置" placement="bottom">
<el-icon :size="18" class="cursor-pointer hover:text-blue-500 transition-colors" @click="app.themeVisible = true"><Brush /></el-icon>
</el-tooltip>
<!-- 通知 -->
<el-dropdown>
<el-badge :value="3" :offset="[-2, 2]">
<el-icon :size="18" class="cursor-pointer hover:text-blue-500 transition-colors"><Bell /></el-icon>
</el-badge>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item><el-text size="small" type="info">暂无通知</el-text></el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 用户信息 -->
<el-dropdown>
<span class="flex items-center gap-2 cursor-pointer hover:text-blue-500 transition-colors">
<el-avatar :size="28" :src="user.avatar" :icon="!user.avatar ? 'UserFilled' : undefined" />
<span class="text-sm">{{ user.username }}</span>
<el-icon :size="14"><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="router.push('/profile')">
<el-icon><User /></el-icon>{{ t('common.personal') }}
</el-dropdown-item>
<el-dropdown-item divided @click="app.logout()">
<el-icon><SwitchButton /></el-icon>{{ t('common.logout') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<TagsBar />
</el-header>
<!-- 主内容 -->
<el-main :class="['main', { 'fullscreen-main': app.pageFullscreen }]">
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<style>
/* 侧边栏暗色调 — 不受亮/暗模式影响 */
.side-bar {
background: #001529 !important;
--el-menu-bg-color: #001529;
--el-menu-text-color: #ffffffb3;
--el-menu-hover-bg-color: #ffffff0d;
--el-menu-active-color: #fff;
--el-menu-border-color: transparent;
--el-sub-menu-title-font-size: 14px;
transition: width 0.3s;
}
.side-logo {
display: flex; align-items: center; gap: 10px; padding: 0 18px; height: 56px;
border-bottom: 1px solid rgba(255,255,255,0.06); color: #fff;
transition: all 0.3s;
}
.side-logo.collapsed { justify-content: center; padding: 0; }
.side-logo-text { font-size: 16px; font-weight: 700; white-space: nowrap; }
.side-menu { border-right: none !important; }
.menu-icon { margin-right: 5px; }
/* 当前激活菜单项加背景色 */
.side-menu .el-menu-item.is-active { background: rgba(255,255,255,0.08) !important; }
.side-menu .el-sub-menu.is-active > .el-sub-menu__title { background: rgba(255,255,255,0.06); }
/* 顶部栏 */
.top-bar {
padding: 0; height: auto;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.top-bar-inner {
display: flex; align-items: center; justify-content: space-between;
height: 52px; background: #fff; padding: 0 20px;
transition: background 0.3s;
}
html.dark .top-bar-inner { background: #1d1d1d; }
/* 主内容区 */
.main { background: #f5f6fa; flex: 1; overflow: auto; padding: 20px; transition: background 0.3s; }
html.dark .main { background: #111; }
/* 面包屑样式优化 */
.el-breadcrumb { font-size: 13px; }
html.dark .el-breadcrumb__inner { color: #ccc; }
html.dark .el-breadcrumb__inner.is-link:hover { color: var(--el-color-primary); }
/* 下拉菜单图标对齐 */
.el-dropdown-menu .el-icon { margin-right: 6px; vertical-align: middle; }
</style>