chore: 初始化前端仓库并迁移 admin-ui
- 创建 rui-frontend 前端仓库 - 迁移 admin-ui 管理后台 - 创建 cashier-mobile 和 customer-mobile 占位项目 - 配置 pnpm workspace
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } 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 { topMenus, loadMenus, getSubMenus, getBreadcrumbs, getFirstPath } = useMenu()
|
||||
|
||||
const activeTop = ref('')
|
||||
|
||||
const sideMenus = computed(() => {
|
||||
if (!activeTop.value) return []
|
||||
return getSubMenus(activeTop.value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 面包屑数据
|
||||
*/
|
||||
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]
|
||||
})
|
||||
|
||||
function onTopChange(key: string) {
|
||||
activeTop.value = key
|
||||
const subItems = getSubMenus(key)
|
||||
if (subItems.length > 0) {
|
||||
const firstPath = getFirstPath(subItems[0])
|
||||
if (firstPath) router.push(firstPath)
|
||||
} else {
|
||||
const topItem = topMenus.value.find((m: any) => m.key === key)
|
||||
if (topItem?.path) router.push(topItem.path)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadMenus('admin').then(() => {
|
||||
if (topMenus.value.length > 0 && !activeTop.value) {
|
||||
activeTop.value = topMenus.value[0].key
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="h-screen">
|
||||
<div class="flex flex-col w-full">
|
||||
<!-- 顶部一级菜单 + 工具栏 -->
|
||||
<el-header class="top-header">
|
||||
<div class="top-header-nav">
|
||||
<div class="flex items-center gap-6 h-full">
|
||||
<!-- Logo -->
|
||||
<span class="text-lg font-bold whitespace-nowrap" :style="{ color: app.primaryColor }">{{ t('app.title') }}</span>
|
||||
<!-- 一级菜单 -->
|
||||
<div class="flex gap-0 flex-1">
|
||||
<div
|
||||
v-for="item in topMenus"
|
||||
:key="item.key"
|
||||
:class="[
|
||||
'px-3 py-2 text-sm cursor-pointer rounded transition-colors flex items-center gap-1',
|
||||
activeTop === item.key ? 'text-white' : 'hover:text-blue-500'
|
||||
]"
|
||||
:style="activeTop === item.key ? { background: app.primaryColor } : {}"
|
||||
@click="onTopChange(item.key)"
|
||||
>
|
||||
<RuiIcon v-if="item.icon" :icon="item.icon" :size="14" />
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧工具栏 -->
|
||||
<div class="flex items-center gap-3">
|
||||
<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><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>
|
||||
</div>
|
||||
<!-- 面包屑 + 标签栏 -->
|
||||
<div class="top-header-bottom">
|
||||
<div class="breadcrumb-wrap">
|
||||
<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>
|
||||
<TagsBar />
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<!-- 二级侧边 + 内容 -->
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<div v-if="sideMenus.length" class="side-sub">
|
||||
<el-scrollbar>
|
||||
<el-menu :default-active="route.path" router class="sub-menu">
|
||||
<template v-for="s in sideMenus" :key="s.id">
|
||||
<el-sub-menu v-if="s.children?.length" :index="String(s.id)">
|
||||
<template #title>
|
||||
<RuiIcon v-if="s.icon" :icon="s.icon" :size="16" class="menu-icon" />
|
||||
<span>{{ s.title }}</span>
|
||||
</template>
|
||||
<el-menu-item v-for="c in s.children" :key="c.id" :index="c.path">
|
||||
<RuiIcon v-if="c.icon" :icon="c.icon" :size="16" class="menu-icon" />
|
||||
<span>{{ c.title }}</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else :index="s.path">
|
||||
<RuiIcon v-if="s.icon" :icon="s.icon" :size="16" class="menu-icon" />
|
||||
<span>{{ s.title }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<el-main :class="['main', { 'fullscreen-main': app.pageFullscreen }]">
|
||||
<router-view />
|
||||
</el-main>
|
||||
</div>
|
||||
</div>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.top-header { padding: 0; height: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
|
||||
.top-header-nav { height: 52px; background: #fff; padding: 0 20px; }
|
||||
html.dark .top-header-nav { background: #1d1d1d; }
|
||||
|
||||
/* 面包屑区域 */
|
||||
.top-header-bottom { background: #fff; border-top: 1px solid var(--el-border-color-light); }
|
||||
html.dark .top-header-bottom { background: #1d1d1d; border-color: #2a2a2a; }
|
||||
.breadcrumb-wrap {
|
||||
display: flex; align-items: center;
|
||||
height: 28px; padding: 0 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.breadcrumb-wrap .el-breadcrumb { line-height: 1; }
|
||||
|
||||
/* 二级侧边栏 */
|
||||
.side-sub { width: 180px; background: #fff; border-right: 1px solid #eee; flex-shrink: 0; }
|
||||
.sub-menu { border-right: none !important; }
|
||||
.sub-menu .el-menu-item.is-active {
|
||||
background: rgba(0,0,0,0.04);
|
||||
border-right: 3px solid var(--el-color-primary);
|
||||
color: var(--el-color-primary); font-weight: 500;
|
||||
}
|
||||
.menu-icon { margin-right: 5px; }
|
||||
|
||||
/* 主内容区 */
|
||||
.main { background: #f5f6fa; flex: 1; padding: 20px; overflow: auto; }
|
||||
|
||||
/* 暗色模式适配 */
|
||||
html.dark .side-sub { background: #1d1d1d; border-color: #333; }
|
||||
html.dark .main { background: #111; }
|
||||
html.dark .sub-menu .el-menu-item.is-active { background: rgba(255,255,255,0.06); }
|
||||
html.dark .el-breadcrumb__inner { color: #ccc; }
|
||||
html.dark .el-breadcrumb__inner.is-link:hover { color: var(--el-color-primary); }
|
||||
html.dark .el-dropdown-menu .el-icon { margin-right: 6px; vertical-align: middle; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user