merge: branch 'huangzhenbao-admin' into comclib-analytics, keeping local RPC integration versions
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
<template>
|
||||
<view class="layout-root">
|
||||
<!-- 主侧边栏 (CRMEB风格) -->
|
||||
<!-- 统一遮罩层 (复刻 CRMEB: 用于所有 Overlay 状态) -->
|
||||
<view
|
||||
class="mobile-mask"
|
||||
:class="{ 'mask-show': isOverlayVisible || (isMobile && isMobileMenuOpen) }"
|
||||
@click="closeAllMenu"
|
||||
></view>
|
||||
|
||||
<!-- 主侧边栏 (CRMEB风格: 70px) -->
|
||||
<AdminAside
|
||||
:collapsed="isMainAsideCollapsed"
|
||||
class="admin-sidebar"
|
||||
:class="{ 'mobile-aside-open': isMobileMenuOpen }"
|
||||
:collapsed="false"
|
||||
:topMenus="topMenus"
|
||||
:activeTopMenuId="activeTopMenuId"
|
||||
@toggle="toggleMainAsideCollapse"
|
||||
@@ -10,46 +19,52 @@
|
||||
:asideWidth="ASIDE_W"
|
||||
/>
|
||||
|
||||
<!-- 二级侧边栏 (CRMEB风格 - 内容区左侧) -->
|
||||
<!-- 二级侧边栏 (1:1 复刻 CRMEB 抽屉/Dock 平滑切换) -->
|
||||
<AdminSubSider
|
||||
v-if="showSubSider"
|
||||
:topMenuTitle="activeTopMenuTitle"
|
||||
:groups="activeGroups"
|
||||
:routes="activeRoutes"
|
||||
:activeRouteId="activeRouteId"
|
||||
:asideWidth="ASIDE_W"
|
||||
:siderWidth="SUB_W"
|
||||
@route-click="onRouteClick"
|
||||
:visible="isSubSiderVisible"
|
||||
:isOverlay="isOverlayVisible || layoutMode === 'mobile'"
|
||||
:layoutMode="layoutMode"
|
||||
:menuTitle="activeTopMenuTitle"
|
||||
:menuTree="activeMenuTree"
|
||||
:activeId="activeRouteId"
|
||||
:currentPath="currentRoutePath"
|
||||
:asideWidth="layoutMode === 'mobile' ? 0 : ASIDE_W"
|
||||
:width="SUB_W"
|
||||
@sub-click="onSubClick"
|
||||
/>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<view
|
||||
class="main"
|
||||
:style="{ marginLeft: mainLeft }"
|
||||
:style="{ marginLeft: isMobile ? '0' : mainLeft }"
|
||||
>
|
||||
<!-- 顶部导航栏 -->
|
||||
<AdminHeader
|
||||
:breadcrumb="breadcrumb"
|
||||
:hasNotification="hasNotification"
|
||||
:isMobile="isMobile"
|
||||
@search="onSearch"
|
||||
@refresh="onRefresh"
|
||||
@notify="onNotify"
|
||||
/>
|
||||
|
||||
<!-- 标签页 (CRMEB风格) -->
|
||||
<!-- 标签页 (CRMEB风格) - 移动端可以隐藏或滚动 -->
|
||||
<AdminTagsView
|
||||
v-if="!isMobile"
|
||||
:tabs="tabs"
|
||||
:activeTabId="activeRouteId"
|
||||
@tab-click="onTabClick"
|
||||
@tab-close="onTabClose"
|
||||
@close-other="onCloseOther"
|
||||
@close-all="onCloseAll"
|
||||
@refresh="onRefresh"
|
||||
/>
|
||||
|
||||
<!-- 内容展示区 (内部路由渲染) -->
|
||||
<view class="content-scroll">
|
||||
<view class="content-inner">
|
||||
<component :is="currentComponent" />
|
||||
<view class="content-inner" :class="{ 'is-mobile': isMobile }">
|
||||
<component :is="currentComponent" v-if="!isPageLoading" />
|
||||
<AdminPageLoading v-if="isPageLoading" />
|
||||
</view>
|
||||
<AdminFooter />
|
||||
</view>
|
||||
@@ -66,6 +81,7 @@ 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 AdminPageLoading from '@/layouts/admin/components/AdminPageLoading.uvue'
|
||||
|
||||
import {
|
||||
getTopMenus,
|
||||
@@ -76,34 +92,132 @@ import {
|
||||
} from '@/layouts/admin/router/adminRoutes.uts'
|
||||
import type { TopMenu, MenuGroup, RouteRecord } from '@/layouts/admin/router/adminRoutes.uts'
|
||||
|
||||
import { MenuNode, settingSubSiderMenu } from '@/layouts/admin/router/settingSubSiderMenu.uts'
|
||||
|
||||
import {
|
||||
activeTopMenuId,
|
||||
activeRouteId,
|
||||
lastSubIdByMenu,
|
||||
tabs,
|
||||
isMainAsideCollapsed,
|
||||
showSubSider,
|
||||
windowWidth,
|
||||
layoutMode,
|
||||
isMobile,
|
||||
isMobileMenuOpen,
|
||||
isOverlayVisible,
|
||||
openRoute,
|
||||
closeTab,
|
||||
closeOtherTabs,
|
||||
closeAllTabs,
|
||||
toggleMainAsideCollapse as storeToggleCollapse,
|
||||
toggleSubSider as storeToggleSubSider,
|
||||
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 // 二级侧边栏宽度
|
||||
// 侧边栏宽度配置 (CRMEB 1:1)
|
||||
const ASIDE_W = 70
|
||||
const SUB_W = 200
|
||||
|
||||
// 页面加载状态
|
||||
const isPageLoading = ref(false)
|
||||
|
||||
const hasNotification = ref<boolean>(false)
|
||||
|
||||
// 计算主内容区左边距
|
||||
/**
|
||||
* 核心逻辑:计算二级菜单是否应该以 Overlay (抽屉) 模式展示
|
||||
* CRMEB 规则:Tablet 屏 (768~1199) 为 Overlay
|
||||
*/
|
||||
const isSubSiderOverlay = computed<boolean>(() => {
|
||||
return layoutMode.value === 'tablet'
|
||||
})
|
||||
|
||||
// 当前路由的路径
|
||||
const currentRoutePath = computed<string>(() => {
|
||||
const route = findRouteById(activeRouteId.value)
|
||||
return route ? route.path : ''
|
||||
})
|
||||
|
||||
// 将旧的 Group + Routes 转换为新的 Tree 结构 (支持 3 级嵌套)
|
||||
const activeMenuTree = computed<MenuNode[]>(() => {
|
||||
if (activeTopMenuId.value === 'setting') {
|
||||
return settingSubSiderMenu
|
||||
}
|
||||
|
||||
const tree: MenuNode[] = []
|
||||
activeGroups.value.forEach(group => {
|
||||
const routes = activeRoutes.value.get(group.id)
|
||||
if (routes != null && routes.length > 0) {
|
||||
const children: MenuNode[] = []
|
||||
routes.forEach(route => {
|
||||
children.push({
|
||||
id: route.id,
|
||||
title: route.title,
|
||||
type: 'page',
|
||||
path: route.path
|
||||
} as MenuNode)
|
||||
})
|
||||
|
||||
// 1:1 复刻 CRMEB: 如果分组标题为空,直接将子菜单平铺在顶层,不渲染分组行
|
||||
if (group.title === '') {
|
||||
children.forEach(child => {
|
||||
tree.push(child)
|
||||
})
|
||||
} else {
|
||||
tree.push({
|
||||
id: group.id,
|
||||
title: group.title,
|
||||
type: 'group',
|
||||
children: children
|
||||
} as MenuNode)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return tree
|
||||
})
|
||||
|
||||
/**
|
||||
* 核心逻辑:二级菜单是否可见
|
||||
* 1. 如果有子菜单内容 (activeMenuTree > 0)
|
||||
* 2. 如果是 Desktop 且 showSubSider 为真 (Dock模式)
|
||||
* 3. 如果是 Tablet/Mobile 且 isOverlayVisible 为真 (Overlay模式)
|
||||
*/
|
||||
const isSubSiderVisible = computed<boolean>(() => {
|
||||
// 首页模块通常不显示 SubSider
|
||||
if (activeTopMenuId.value === 'home' || activeMenuTree.value.length === 0) return false
|
||||
|
||||
if (layoutMode.value === 'desktop') {
|
||||
return showSubSider.value
|
||||
}
|
||||
|
||||
// Tablet 和 Mobile 模式下,直接由 isOverlayVisible 控制
|
||||
return isOverlayVisible.value
|
||||
})
|
||||
|
||||
/**
|
||||
* 核心逻辑:计算主内容区左偏移量 (mainLeft)
|
||||
* 严格按照断点策略计算,防止遮挡
|
||||
* 1. Desktop: ASIDE_W + (SUB_W if dock)
|
||||
* 2. Tablet: ASIDE_W (sub-sider 是 overlay,不占位)
|
||||
* 3. Mobile: 0
|
||||
*/
|
||||
const mainLeft = computed<string>(() => {
|
||||
const asideWidth = isMainAsideCollapsed.value ? 0 : ASIDE_W
|
||||
const subWidth = showSubSider.value ? SUB_W : 0
|
||||
return (asideWidth + subWidth) + 'px'
|
||||
if (layoutMode.value === 'mobile') {
|
||||
return '0px'
|
||||
}
|
||||
|
||||
let left = ASIDE_W // 只要不是 Mobile,主侧栏 70px 始终 Dock
|
||||
|
||||
// 只有在 Desktop 模式且二级菜单处于 Dock 模式显示时,才累加宽度
|
||||
if (layoutMode.value === 'desktop' && showSubSider.value && activeMenuTree.value.length > 0) {
|
||||
left += SUB_W
|
||||
}
|
||||
|
||||
return left + 'px'
|
||||
})
|
||||
|
||||
// 获取一级菜单列表
|
||||
@@ -143,19 +257,77 @@ const currentComponent = computed<any>(() => {
|
||||
return getComponent(route.componentKey)
|
||||
})
|
||||
|
||||
// 监听路由变化,同步状态 (处理从 Tabs 或外部跳转的情况)
|
||||
watch(() => activeRouteId.value, (newId) => {
|
||||
// 触发页面加载动画
|
||||
isPageLoading.value = true
|
||||
setTimeout(() => {
|
||||
isPageLoading.value = false
|
||||
}, 400) // 给予足够的时间让异步组件加载
|
||||
|
||||
const route = findRouteById(newId)
|
||||
if (route && route.parentId) {
|
||||
// 同步一级菜单
|
||||
if (activeTopMenuId.value !== route.parentId) {
|
||||
activeTopMenuId.value = route.parentId
|
||||
}
|
||||
// 同步最后访问记录
|
||||
lastSubIdByMenu.value[route.parentId] = newId
|
||||
|
||||
// 如果是 Desktop 且 SubSider 关着,自动打开(除非用户手动关了)
|
||||
if (layoutMode.value === 'desktop' && !showSubSider.value) {
|
||||
showSubSider.value = true
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// ============================================
|
||||
// 事件处理
|
||||
// ============================================
|
||||
|
||||
function onTopMenuClick(menu: TopMenu): void {
|
||||
activeTopMenuId.value = menu.id
|
||||
if (menu.groups.length === 0) {
|
||||
|
||||
// 1:1 复刻 CRMEB 交互:点击 Aside 立即展示 SubSider
|
||||
if (layoutMode.value === 'desktop') {
|
||||
showSubSider.value = true
|
||||
} else {
|
||||
isOverlayVisible.value = true
|
||||
isMobileMenuOpen.value = false // 如果主侧栏开着,关掉它,开二级
|
||||
}
|
||||
|
||||
// 1:1 复刻 CRMEB 交互:点击一级菜单后,自动跳转到上一次记录的子页面或第一个子页面
|
||||
let targetId = lastSubIdByMenu.value[menu.id]
|
||||
if (!targetId && activeMenuTree.value.length > 0) {
|
||||
const firstNode = activeMenuTree.value[0]
|
||||
if (firstNode.type === 'page') {
|
||||
targetId = firstNode.id
|
||||
} else if (firstNode.children != null && firstNode.children!.length > 0) {
|
||||
targetId = firstNode.children![0].id
|
||||
}
|
||||
}
|
||||
|
||||
if (targetId) {
|
||||
openRoute(targetId)
|
||||
} else {
|
||||
// 兜底逻辑:如果没有二级菜单,跳转到 index
|
||||
openRoute(menu.id + '_index')
|
||||
}
|
||||
}
|
||||
|
||||
function onRouteClick(routeId: string): void {
|
||||
openRoute(routeId)
|
||||
function onSubClick(payload: { id: string, path: string }): void {
|
||||
// CRMEB 跳转逻辑
|
||||
openRoute(payload.id)
|
||||
|
||||
// 更新最后访问记录
|
||||
lastSubIdByMenu.value[activeTopMenuId.value] = payload.id
|
||||
|
||||
// 重要:1:1 复刻 CRMEB,点击二级菜单项后,SubSider 通常保持显示状态
|
||||
// 只有在 Mobile 模式下,用户如果是想“跳转并收起”,通常需要点击遮罩或关闭按钮,但这里我们按桌面版常驻逻辑
|
||||
// 如果需要移动端点击自动关闭,才解开下面注释
|
||||
// if (layoutMode.value === 'mobile') {
|
||||
// closeAllMenu()
|
||||
// }
|
||||
}
|
||||
|
||||
function onTabClick(tab: TabItem): void {
|
||||
@@ -178,6 +350,12 @@ function toggleMainAsideCollapse(): void {
|
||||
storeToggleCollapse()
|
||||
}
|
||||
|
||||
function closeAllMenu(): void {
|
||||
isMobileMenuOpen.value = false
|
||||
isOverlayVisible.value = false
|
||||
// 注意:在 Desktop 模式下,closeAllMenu 通常不隐藏 showSubSider,除非用户手动点汉堡按钮
|
||||
}
|
||||
|
||||
function onSearch(): void {
|
||||
uni.showToast({ title: '搜索', icon: 'none' })
|
||||
}
|
||||
@@ -194,6 +372,8 @@ function onNotify(): void {
|
||||
// 生命周期
|
||||
// ============================================
|
||||
|
||||
let resizeTid: any = null
|
||||
|
||||
onMounted(async () => {
|
||||
if (!ensureAnalyticsLogin({ toastTitle: '请先登录以访问管理后台' })) return
|
||||
|
||||
@@ -208,16 +388,84 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
initNavState()
|
||||
|
||||
// 初始化窗口宽度
|
||||
windowWidth.value = uni.getWindowInfo().windowWidth
|
||||
|
||||
// 监听窗口变化 (增加节流保护与跨断点状态重置)
|
||||
uni.onWindowResize((res) => {
|
||||
if (resizeTid != null) {
|
||||
cancelAnimationFrame(resizeTid)
|
||||
}
|
||||
|
||||
resizeTid = requestAnimationFrame(() => {
|
||||
const oldMode = layoutMode.value
|
||||
windowWidth.value = res.size.windowWidth
|
||||
const newMode = layoutMode.value
|
||||
|
||||
// 跨断点自动关闭所有 Overlay,防止状态残留遮挡内容
|
||||
if (oldMode != newMode) {
|
||||
isOverlayVisible.value = false
|
||||
isMobileMenuOpen.value = false
|
||||
// 如果切到桌面端,默认展开二级侧栏以符合 CRMEB 习惯
|
||||
if (newMode === 'desktop') {
|
||||
showSubSider.value = true
|
||||
}
|
||||
}
|
||||
|
||||
resizeTid = null
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-root {
|
||||
--admin-page-padding-desktop: 12px;
|
||||
--admin-page-padding-mobile: 8px;
|
||||
--admin-section-gap: 12px;
|
||||
--admin-card-padding: 16px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 移动端侧边栏样式 */
|
||||
.mobile-aside {
|
||||
position: absolute;
|
||||
left: -70px; /* 隐藏在左侧,匹配 ASIDE_W: 70 */
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1001;
|
||||
transition: transform 0.3s ease;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.mobile-aside-open {
|
||||
transform: translateX(70px); /* 移入视图 */
|
||||
}
|
||||
|
||||
.mobile-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 300ms ease, visibility 0s linear 300ms;
|
||||
}
|
||||
|
||||
.mask-show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: opacity 300ms ease, visibility 0s linear 0s;
|
||||
}
|
||||
|
||||
.main {
|
||||
@@ -225,18 +473,49 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
transition: margin-left 0.3s ease;
|
||||
transition: margin-left 300ms ease;
|
||||
background: #f0f2f5;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 响应式强制覆盖 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.main {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
/* 强行改变侧边栏布局模式 */
|
||||
.admin-sidebar {
|
||||
position: absolute !important;
|
||||
left: -70px !important; /* 隐藏在左侧 */
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1001;
|
||||
transition: transform 0.3s ease !important;
|
||||
}
|
||||
|
||||
/* 展开时的状态 */
|
||||
.mobile-aside-open {
|
||||
transform: translateX(70px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
overflow-x: auto; /* 允许横向滚动,兼容极端窄屏 */
|
||||
background: #f0f2f5;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
min-height: calc(100vh - 120px);
|
||||
padding: 16px;
|
||||
padding: var(--admin-page-padding-desktop);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content-inner.is-mobile {
|
||||
padding: var(--admin-page-padding-mobile);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user