修改页面结构
This commit is contained in:
@@ -1,32 +1,33 @@
|
||||
<template>
|
||||
<view class="layout-root">
|
||||
<!-- 主侧边栏 -->
|
||||
<!-- 主侧边栏 (CRMEB风格) -->
|
||||
<AdminAside
|
||||
:collapsed="isCollapsed"
|
||||
:menuList="menuList"
|
||||
:activeMenuId="activeMenuId"
|
||||
@toggle="toggleCollapse"
|
||||
@menu-click="onMenuClick"
|
||||
:asideWidth='ASIDE_W'
|
||||
:collapsed="isMainAsideCollapsed"
|
||||
:topMenus="topMenus"
|
||||
:activeTopMenuId="activeTopMenuId"
|
||||
@toggle="toggleMainAsideCollapse"
|
||||
@menu-click="onTopMenuClick"
|
||||
:asideWidth="ASIDE_W"
|
||||
/>
|
||||
|
||||
<!-- 二级侧边栏:固定在内容区左侧(独立层级) -->
|
||||
<!-- 二级侧边栏 (CRMEB风格 - 内容区左侧) -->
|
||||
<AdminSubSider
|
||||
v-if="activeGroups.length > 0"
|
||||
:activeMenuTitle="activeMenuTitle"
|
||||
v-if="showSubSider"
|
||||
:topMenuTitle="activeTopMenuTitle"
|
||||
:groups="activeGroups"
|
||||
:activeSubId="activeSubId"
|
||||
:activeMenuId="activeMenuId || 'home'"
|
||||
:routes="activeRoutes"
|
||||
:activeRouteId="activeRouteId"
|
||||
:asideWidth="ASIDE_W"
|
||||
:siderWidth="SUB_W"
|
||||
@sub-click="onSubClick"
|
||||
@route-click="onRouteClick"
|
||||
/>
|
||||
|
||||
<!-- 右侧内容区(Header + Tags + 内容展示区 + Footer) -->
|
||||
<!-- 右侧内容区 -->
|
||||
<view
|
||||
class="main"
|
||||
:style="{ marginLeft: mainLeft }"
|
||||
>
|
||||
<!-- 顶部导航栏 -->
|
||||
<AdminHeader
|
||||
:breadcrumb="breadcrumb"
|
||||
:hasNotification="hasNotification"
|
||||
@@ -35,17 +36,20 @@
|
||||
@notify="onNotify"
|
||||
/>
|
||||
|
||||
<!-- 标签页 (CRMEB风格) -->
|
||||
<AdminTagsView
|
||||
:tabs="tabs"
|
||||
:activeTabId="activeTabId"
|
||||
:activeTabId="activeRouteId"
|
||||
@tab-click="onTabClick"
|
||||
@tab-close="onTabClose"
|
||||
@close-other="onCloseOther"
|
||||
@close-all="onCloseAll"
|
||||
/>
|
||||
|
||||
<!-- 展示区:只渲染 slot 内容(你的页面内容都在这里展示) -->
|
||||
<!-- 内容展示区 (内部路由渲染) -->
|
||||
<scroll-view class="content" scroll-y="true">
|
||||
<view class="content-inner">
|
||||
<slot></slot>
|
||||
<component :is="currentComponent" />
|
||||
</view>
|
||||
<AdminFooter />
|
||||
</scroll-view>
|
||||
@@ -54,209 +58,169 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed } from 'vue'
|
||||
import AdminAside from './components/AdminAside.uvue'
|
||||
import AdminSubSider from './components/AdminSubSider.uvue'
|
||||
import AdminHeader from './components/AdminHeader.uvue'
|
||||
import AdminTagsView from './components/AdminTagsView.uvue'
|
||||
import AdminFooter from './components/AdminFooter.uvue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import AdminAside from '@/layouts/admin/components/AdminAside.uvue'
|
||||
import AdminSubSider from '@/layouts/admin/components/AdminSubSider.uvue'
|
||||
import AdminHeader from '@/layouts/admin/components/AdminHeader.uvue'
|
||||
import AdminTagsView from '@/layouts/admin/components/AdminTagsView.uvue'
|
||||
import AdminFooter from '@/layouts/admin/components/AdminFooter.uvue'
|
||||
|
||||
import { menuList as menuConst } from './utils/menu.uts'
|
||||
import { findActiveByCurrentPage, getCurrentRoutePath } from './utils/nav.uts'
|
||||
import { makeTabFromPath, upsertTab, removeTab } from './utils/tabs.uts'
|
||||
import type { MenuItem, TabItem , MenuChild } from './types.uts'
|
||||
import {
|
||||
getTopMenus,
|
||||
getGroupsByTopMenu,
|
||||
getRoutesByGroup,
|
||||
findRouteById,
|
||||
getBreadcrumb
|
||||
} from '@/layouts/admin/router/adminRoutes.uts'
|
||||
import type { TopMenu, MenuGroup, RouteRecord } from '@/layouts/admin/router/adminRoutes.uts'
|
||||
|
||||
import { tabs, activeTabId, isCollapsed, hasNotification } from './state.uts'
|
||||
import {
|
||||
activeTopMenuId,
|
||||
activeRouteId,
|
||||
tabs,
|
||||
isMainAsideCollapsed,
|
||||
showSubSider,
|
||||
openRoute,
|
||||
closeTab,
|
||||
closeOtherTabs,
|
||||
closeAllTabs,
|
||||
toggleMainAsideCollapse as storeToggleCollapse,
|
||||
initNavState
|
||||
} from '@/layouts/admin/store/adminNavStore.uts'
|
||||
import type { TabItem } from '@/layouts/admin/store/adminNavStore.uts'
|
||||
|
||||
import { getComponent } from '@/layouts/admin/router/adminComponentMap.uts'
|
||||
|
||||
// 侧边栏宽度配置
|
||||
const ASIDE_W = 96 // 主侧边栏宽度
|
||||
const SUB_W = 180 // 二级侧边栏宽度
|
||||
|
||||
// 你页面传进来的 currentPage:可能是顶级 id,也可能是子页面 id(user-list)
|
||||
const props = defineProps<{ currentPage: string }>()
|
||||
const hasNotification = ref<boolean>(false)
|
||||
|
||||
const menuList = ref<MenuItem[]>(menuConst)
|
||||
|
||||
|
||||
// active states
|
||||
const activeMenuId = ref('home')
|
||||
const activeSubId = ref('')
|
||||
|
||||
// 二级侧边栏
|
||||
const ASIDE_W = 96
|
||||
const SUB_W = 200 // 你想更像 CRMEB,就把这改小:160~180 都行
|
||||
|
||||
const mainLeft = computed(() => {
|
||||
return (activeGroups.value.length > 0 ? (ASIDE_W + SUB_W) : ASIDE_W) + 'px'
|
||||
// 计算主内容区左边距
|
||||
const mainLeft = computed<string>(() => {
|
||||
const asideWidth = isMainAsideCollapsed.value ? 0 : ASIDE_W
|
||||
const subWidth = showSubSider.value ? SUB_W : 0
|
||||
return (asideWidth + subWidth) + 'px'
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 每次 layout 渲染时,同步高亮(靠 currentPage)
|
||||
const syncActiveByCurrentPage = () => {
|
||||
const r = findActiveByCurrentPage(menuList.value, props.currentPage)
|
||||
activeMenuId.value = r.activeMenuId
|
||||
activeSubId.value = r.activeSubId
|
||||
}
|
||||
|
||||
// 同步 tabs(靠当前 route)
|
||||
const syncTabsByRoute = () => {
|
||||
const path = getCurrentRoutePath()
|
||||
if (!path) return
|
||||
|
||||
const tab = makeTabFromPath(menuList.value, path)
|
||||
tabs.value = upsertTab(tabs.value, tab)
|
||||
activeTabId.value = tab.id
|
||||
}
|
||||
|
||||
// 初始化同步(setup 执行一次)
|
||||
syncActiveByCurrentPage()
|
||||
syncTabsByRoute()
|
||||
|
||||
// computed
|
||||
const activeMenu = computed(() => menuList.value.find(m => m.id === activeMenuId.value))
|
||||
const activeMenuTitle = computed(() => activeMenu.value?.title || '商城后台')
|
||||
const activeGroups = computed(() => {
|
||||
const m = menuList.value.find(it => it.id === activeMenuId.value)
|
||||
return m?.groups ?? [] // ✅ 永远是数组
|
||||
// 获取一级菜单列表
|
||||
const topMenus = computed<TopMenu[]>(() => {
|
||||
return getTopMenus()
|
||||
})
|
||||
|
||||
|
||||
const breadcrumb = computed(() => {
|
||||
let subTitle = ''
|
||||
const groups = activeGroups.value
|
||||
for (const g of groups) {
|
||||
const cs = g.children ?? []
|
||||
const hit = cs.find(c => c.id === activeSubId.value)
|
||||
if (hit) { subTitle = hit.title; break }
|
||||
}
|
||||
return subTitle ? `${activeMenuTitle.value} / ${subTitle}` : `${activeMenuTitle.value}`
|
||||
// 当前选中一级菜单的标题
|
||||
const activeTopMenuTitle = computed<string>(() => {
|
||||
const menu = topMenus.value.find(m => m.id === activeTopMenuId.value)
|
||||
return menu ? menu.title : ''
|
||||
})
|
||||
|
||||
// 当前一级菜单的分组列表
|
||||
const activeGroups = computed<MenuGroup[]>(() => {
|
||||
return getGroupsByTopMenu(activeTopMenuId.value)
|
||||
})
|
||||
|
||||
// handlers
|
||||
const toggleCollapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
}
|
||||
// 当前一级菜单的所有路由
|
||||
const activeRoutes = computed<Map<string, RouteRecord[]>>(() => {
|
||||
const result = new Map<string, RouteRecord[]>()
|
||||
activeGroups.value.forEach(group => {
|
||||
result.set(group.id, getRoutesByGroup(group.id))
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
// 递归取第一个 leaf(你要求的“递归默认打开第一个页面”)
|
||||
const firstLeafOfMenu = (m: MenuItem): MenuChild | null => {
|
||||
if (!m.groups || m.groups.length === 0) return null
|
||||
const g0 = m.groups[0]
|
||||
if (!g0.children || g0.children.length === 0) return null
|
||||
// 面包屑导航
|
||||
const breadcrumb = computed<Array<{id: string, title: string}>>(() => {
|
||||
return getBreadcrumb(activeRouteId.value)
|
||||
})
|
||||
|
||||
const c0: any = g0.children[0] as any
|
||||
// 兼容未来 children 还能再嵌套
|
||||
if (c0.children && (c0.children as MenuChild[]).length > 0) {
|
||||
const walk = (list: MenuChild[]): MenuChild | null => {
|
||||
if (!list || list.length === 0) return null
|
||||
const n: any = list[0] as any
|
||||
if (n.children && (n.children as MenuChild[]).length > 0) return walk(n.children as MenuChild[])
|
||||
return list[0]
|
||||
}
|
||||
return walk(c0.children as MenuChild[])
|
||||
}
|
||||
return g0.children[0]
|
||||
}
|
||||
// 当前渲染的组件
|
||||
const currentComponent = computed<any>(() => {
|
||||
const route = findRouteById(activeRouteId.value)
|
||||
if (!route) return null
|
||||
return getComponent(route.componentKey)
|
||||
})
|
||||
|
||||
let navigating = false
|
||||
// ============================================
|
||||
// 事件处理
|
||||
// ============================================
|
||||
|
||||
// ✅ 改:使用 redirectTo(防止页面栈越堆越深)
|
||||
// 此方法用于主导航:菜单点击、二级菜单点击、tabs 点击
|
||||
const go = async (url?: string | null) => {
|
||||
if (!url || url.length === 0) return
|
||||
if (navigating) return
|
||||
navigating = true
|
||||
try {
|
||||
await uni.redirectTo({ url })
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setTimeout(() => { navigating = false }, 80)
|
||||
function onTopMenuClick(menu: TopMenu): void {
|
||||
activeTopMenuId.value = menu.id
|
||||
if (menu.groups.length === 0) {
|
||||
openRoute(menu.id + '_index')
|
||||
}
|
||||
}
|
||||
|
||||
// // ✅ 新增:navigateTo 用于详情页等非主导航场景(允许用户返回)
|
||||
// const navigateToDetail = async (url?: string | null) => {
|
||||
// if (!url || url.length === 0) return
|
||||
// try {
|
||||
// await uni.navigateTo({ url })
|
||||
// } catch (e) {
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
const onMenuClick = (menuId: string) => {
|
||||
const m = menuList.value.find(x => x.id === menuId)
|
||||
if (!m) return
|
||||
|
||||
activeMenuId.value = m.id
|
||||
|
||||
const leaf = firstLeafOfMenu(m)
|
||||
activeSubId.value = leaf ? leaf.id : ''
|
||||
|
||||
// ✅ 优先 leaf.path,其次才考虑 m.path
|
||||
go(leaf?.path ?? m.path ?? '')
|
||||
function onRouteClick(routeId: string): void {
|
||||
openRoute(routeId)
|
||||
}
|
||||
|
||||
const firstLeafInChildren = (list?: MenuChild[] | null): MenuChild | null => {
|
||||
const arr = list ?? []
|
||||
if (arr.length === 0) return null
|
||||
const n = arr[0]
|
||||
const deep = n.children ?? []
|
||||
return deep.length > 0 ? firstLeafInChildren(deep) : n
|
||||
function onTabClick(tab: TabItem): void {
|
||||
openRoute(tab.id, false)
|
||||
}
|
||||
|
||||
const onSubClick = (c: MenuChild) => {
|
||||
activeSubId.value = c.id
|
||||
if (c.path && c.path.length > 0) {
|
||||
go(c.path)
|
||||
} else {
|
||||
const leaf = firstLeafInChildren(c.children)
|
||||
go(leaf?.path ?? '')
|
||||
}
|
||||
function onTabClose(tabId: string): void {
|
||||
closeTab(tabId)
|
||||
}
|
||||
|
||||
|
||||
const onTabClick = (tab: TabItem) => {
|
||||
activeTabId.value = tab.id
|
||||
go(tab.path)
|
||||
function onCloseOther(tabId: string): void {
|
||||
closeOtherTabs(tabId)
|
||||
}
|
||||
|
||||
const onTabClose = (tabId: string) => {
|
||||
// 关闭当前 tab:删除后回到最后一个 tab
|
||||
const wasActive = activeTabId.value === tabId
|
||||
tabs.value = removeTab(tabs.value, tabId)
|
||||
if (wasActive) {
|
||||
const last = tabs.value[tabs.value.length - 1]
|
||||
if (last) {
|
||||
activeTabId.value = last.id
|
||||
go(last.path)
|
||||
}
|
||||
}
|
||||
function onCloseAll(): void {
|
||||
closeAllTabs()
|
||||
}
|
||||
|
||||
const onSearch = () => uni.showToast({ title: '搜索', icon: 'none' })
|
||||
const onRefresh = () => uni.showToast({ title: '刷新', icon: 'none' })
|
||||
const onNotify = () => uni.showToast({ title: '通知', icon: 'none' })
|
||||
function toggleMainAsideCollapse(): void {
|
||||
storeToggleCollapse()
|
||||
}
|
||||
|
||||
function onSearch(): void {
|
||||
uni.showToast({ title: '搜索', icon: 'none' })
|
||||
}
|
||||
|
||||
function onRefresh(): void {
|
||||
uni.showToast({ title: '刷新', icon: 'none' })
|
||||
}
|
||||
|
||||
function onNotify(): void {
|
||||
uni.showToast({ title: '通知', icon: 'none' })
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 生命周期
|
||||
// ============================================
|
||||
|
||||
onMounted(() => {
|
||||
initNavState()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.layout-root{
|
||||
<style scoped lang="scss">
|
||||
.layout-root {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background:#f3f4f6;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
/* 右侧主区域:左边距由 template 动态控制(96 或 336) */
|
||||
.main{
|
||||
min-height: 100vh;
|
||||
display:flex;
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
transition: margin-left 0.3s ease;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
/* 展示区 */
|
||||
.content{
|
||||
height: calc(100vh - 56px - 44px);
|
||||
.content {
|
||||
flex: 1;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
.content-inner{
|
||||
padding:5px;
|
||||
|
||||
.content-inner {
|
||||
min-height: calc(100vh - 120px);
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user