82a19101a8
- 创建 rui-frontend 前端仓库 - 迁移 admin-ui 管理后台 - 创建 cashier-mobile 和 customer-mobile 占位项目 - 配置 pnpm workspace
215 lines
8.6 KiB
Vue
215 lines
8.6 KiB
Vue
<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>
|