| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- <script setup lang="ts">
- import type { MenuOption } from 'naive-ui'
- import { NIcon, useMessage } from 'naive-ui'
- import { BookOutline, LogoUsd, MenuOutline, PeopleOutline } from '@vicons/ionicons5'
- import RouteTabBar from '@/components/RouteTabBar.vue'
- import { setToken } from '@/api/request'
- import { useAppStore } from '@/stores/app'
- import { useTabStore } from '@/stores/tabs'
- const app = useAppStore()
- const tabStore = useTabStore()
- const { sidebarCollapsed } = storeToRefs(app)
- const route = useRoute()
- const router = useRouter()
- const message = useMessage()
- watch(
- () => route.fullPath,
- () => {
- tabStore.syncFromRoute(route)
- },
- { immediate: true },
- )
- function renderIcon(icon: typeof BookOutline) {
- return () => h(NIcon, { size: 18 }, { default: () => h(icon) })
- }
- const SUBMENU_COURSES = 'submenu-courses'
- const SUBMENU_CUSTOMERS = 'submenu-customers'
- const SUBMENU_FINANCE = 'submenu-finance'
- const menuOptions: MenuOption[] = [
- {
- label: '课程管理',
- key: SUBMENU_COURSES,
- icon: renderIcon(BookOutline),
- children: [
- { label: '常见问题', key: '/courses/common-questions' },
- { label: '有奖问答', key: '/courses/reward-questions' },
- { label: '教学内容', key: '/courses/goods' },
- ],
- },
- {
- label: '客户管理',
- key: SUBMENU_CUSTOMERS,
- icon: renderIcon(PeopleOutline),
- children: [{ label: '客户列表', key: '/customers/list' }],
- },
- {
- label: '财务管理',
- key: SUBMENU_FINANCE,
- icon: renderIcon(LogoUsd),
- children: [
- { label: '取款申请', key: '/finance/withdraw-apply' },
- { label: '订单列表', key: '/finance/order-list' },
- ],
- },
- ]
- const expandedKeys = ref<string[]>([])
- watch(
- () => route.path,
- (p) => {
- if (p.startsWith('/courses')) {
- expandedKeys.value = [SUBMENU_COURSES]
- return
- }
- if (p.startsWith('/customers')) {
- expandedKeys.value = [SUBMENU_CUSTOMERS]
- return
- }
- if (p.startsWith('/finance')) {
- expandedKeys.value = [SUBMENU_FINANCE]
- return
- }
- expandedKeys.value = []
- },
- { immediate: true },
- )
- const activeKey = computed(() => route.path)
- const pageTitle = computed(() => (route.meta.title as string) ?? '后台')
- const userOptions = [
- { label: '修改密码', key: 'account' },
- { type: 'divider', key: 'd' },
- { label: '退出登录', key: 'logout' },
- ]
- function handleUserSelect(key: string) {
- if (key === 'logout') {
- setToken(null)
- message.success('已退出登录')
- router.replace({ path: '/login' })
- }
- }
- function goHome() {
- router.push('/courses/common-questions')
- }
- /** NMenu 无内置 router 集成;受控 :value 时需自行跳转,否则点击不会改变路由 */
- function handleMenuSelect(key: string | number) {
- const path = String(key)
- if (path.startsWith('/')) void router.push(path)
- }
- </script>
- <template>
- <NLayout has-sider class="admin-root" position="absolute">
- <NLayoutSider
- v-model:collapsed="sidebarCollapsed"
- bordered
- collapse-mode="width"
- :collapsed-width="72"
- :width="248"
- :native-scrollbar="false"
- show-trigger
- trigger-style="right: -14px; border-radius: 0 8px 8px 0;"
- class="admin-sider"
- >
- <div class="sider-inner" :class="{ 'is-collapsed': sidebarCollapsed }">
- <div class="brand" @click="goHome">
- <div class="brand-mark" aria-hidden="true" />
- <span v-if="!sidebarCollapsed" class="admin-logo">课程后台</span>
- </div>
- <NScrollbar class="sider-menu-scroll">
- <NMenu
- v-model:expanded-keys="expandedKeys"
- :value="activeKey"
- :options="menuOptions"
- :collapsed="sidebarCollapsed"
- :collapsed-width="72"
- :collapsed-icon-size="22"
- :indent="22"
- inverted
- @update:value="handleMenuSelect"
- />
- </NScrollbar>
- </div>
- </NLayoutSider>
- <!-- 使用独立列 + 内部滚动,避免顶栏与内容在同一 Layout 滚动容器内一起滚动 -->
- <div class="admin-right">
- <NLayoutHeader bordered class="admin-header">
- <div class="header-left">
- <NButton quaternary circle @click="app.toggleSidebar">
- <template #icon>
- <NIcon :component="MenuOutline" />
- </template>
- </NButton>
- <NBreadcrumb separator="/">
- <NBreadcrumbItem>首页</NBreadcrumbItem>
- <NBreadcrumbItem>{{ pageTitle }}</NBreadcrumbItem>
- </NBreadcrumb>
- </div>
- <div class="header-right">
- <NDropdown :options="userOptions" @select="handleUserSelect">
- <NAvatar
- round
- size="small"
- :style="{ cursor: 'pointer', background: 'linear-gradient(135deg, #6366f1, #8b5cf6)' }"
- >
- 管
- </NAvatar>
- </NDropdown>
- </div>
- </NLayoutHeader>
- <div class="admin-route-tabs">
- <RouteTabBar />
- </div>
- <div class="admin-main">
- <div class="content-wrap page-enter">
- <RouterView />
- </div>
- </div>
- </div>
- </NLayout>
- </template>
- <style scoped>
- .admin-root {
- height: 100%;
- min-height: 100vh;
- }
- /* 与 NMenu 的 collapsedWidth 对齐:收起时去掉水平内边距,否则菜单按 72px 居中图标会偏右 */
- .sider-inner {
- padding: 20px 12px 24px;
- min-height: 100vh;
- box-sizing: border-box;
- }
- .sider-inner.is-collapsed {
- padding-left: 0;
- padding-right: 0;
- }
- .sider-menu-scroll {
- max-height: calc(100vh - 88px);
- }
- .brand {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 0 8px 24px;
- cursor: pointer;
- user-select: none;
- }
- .sider-inner.is-collapsed .brand {
- justify-content: center;
- padding-left: 0;
- padding-right: 0;
- }
- .brand-mark {
- width: 36px;
- height: 36px;
- border-radius: 10px;
- flex-shrink: 0;
- background: linear-gradient(135deg, #6366f1 0%, #a855f7 55%, #ec4899 100%);
- box-shadow: 0 8px 24px rgba(99, 102, 241, 0.35);
- }
- .admin-right {
- flex: 1;
- min-width: 0;
- min-height: 0;
- height: 100%;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- background: var(--admin-bg, #f4f5f8);
- }
- .admin-header {
- flex-shrink: 0;
- position: sticky;
- top: 0;
- z-index: 200;
- display: flex;
- align-items: center;
- justify-content: space-between;
- height: 64px;
- padding: 0 24px;
- background: rgba(255, 255, 255, 0.92);
- backdrop-filter: blur(12px);
- box-shadow: 0 1px 0 var(--admin-border);
- }
- .admin-route-tabs {
- flex-shrink: 0;
- padding: 0 20px;
- background: rgba(255, 255, 255, 0.96);
- border-bottom: 1px solid var(--admin-border);
- box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
- }
- .admin-main {
- flex: 1;
- min-height: 0;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- }
- .header-left,
- .header-right {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .content-wrap {
- box-sizing: border-box;
- width: 100%;
- flex: 1;
- min-height: 0;
- overflow: auto;
- padding: 20px;
- max-width: 100%;
- display: flex;
- flex-direction: column;
- }
- </style>
|