2级sidebar

This commit is contained in:
2025-12-13 15:56:12 +08:00
parent 3442f96214
commit b57a002de8
46 changed files with 1529 additions and 203 deletions

View File

@@ -149,33 +149,60 @@
// 用户信息
.user-section {
padding: 16px 20px;
padding: 16px;
border-top: 1px solid rgba(0, 0, 0, 0.08);
cursor: pointer;
transition: background 0.2s;
&:hover {
background: rgba(124, 58, 237, 0.05);
}
.user-info-wrapper {
display: flex;
align-items: center;
gap: 12px;
}
display: flex;
align-items: center;
gap: 12px;
.user-avatar {
width: 36px;
height: 36px;
cursor: pointer;
transition: transform 0.2s;
flex-shrink: 0;
&:hover {
transform: scale(1.1);
}
:deep(.el-avatar) {
width: 100%;
height: 100%;
}
}
.user-name {
flex: 1;
font-size: 14px;
font-weight: 500;
color: #333;
color: #303133;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.back-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
color: #606266;
transition: all 0.2s;
flex-shrink: 0;
&:hover {
color: #7c3aed;
background: rgba(124, 58, 237, 0.1);
}
.el-icon {
font-size: 18px;
}
}
}
// ==================== 主内容区 ====================

View File

@@ -143,13 +143,12 @@ function loadMenuFromStorage(): MenuItem[] {
const userViews = loginDomain.userViews || []
// SidebarLayout parentId platform admin
// AdminIframeSidebarLayout 3 iframe
const sidebarViews = userViews.filter((view: any) =>
view.layout === 'SidebarLayout' &&
!view.parentId &&
view.layout === 'AdminIframeSidebarLayout' && // AdminIframeSidebarLayout
view.viewType === 'iframe' && // iframe
view.type === 1 && // type 1
view.service === 'platform' && // platform
view.url?.startsWith('/admin') // admin AdminSidebar
view.service === 'platform' // platform
)
// orderNum
@@ -288,5 +287,5 @@ watch(
</script>
<style lang="scss" scoped>
@import url("./SidebarLayout.scss");
@import url("./AdminIframeSidebarLayout.scss");
</style>

View File

@@ -165,7 +165,7 @@ function loadMenuFromStorage(): MenuItem[] {
!view.url?.startsWith('/admin') // 排除 admin 路由(由 AdminSidebar 管理)
)
hasAdmin.value = userViews.filter((view: any) =>
view.layout === 'SidebarLayout' &&
view.layout === 'AdminIframeSidebarLayout' &&
!view.parentId &&
view.type === 1 && // type 1 是侧边栏菜单
view.service === 'platform' && // 只显示 platform 服务的视图
@@ -266,7 +266,7 @@ const handleUserCommand = (command: string) => {
const loginDomain = JSON.parse(loginDomainStr)
const userViews = loginDomain.userViews || []
const adminViews = userViews.filter((view: any) =>
view.service === 'platform' && view.url?.startsWith('/admin')
view.service === 'platform' && view.url?.startsWith('/admin') && view.viewType === 'iframe'
)
if (adminViews.length > 0) {
// 按 orderNum 排序,跳转到第一个

View File

@@ -1,3 +1,4 @@
export { default as SidebarLayout } from "./SidebarLayout/SidebarLayout.vue";
export { default as AdminIframeSidebarLayout } from "./AdminIframeSidebarLayout/AdminIframeSidebarLayout.vue";
// BlankLayout从shared导入
export { BlankLayout } from 'shared/layouts';
export { BlankLayout, AdminSidebarLayout } from 'shared/layouts';

View File

@@ -18,14 +18,15 @@ import {
import type { TbSysViewDTO } from 'shared/types'
import type { RouteRecordRaw } from 'vue-router'
import router from './index'
import { SidebarLayout, BlankLayout } from '@/layouts'
import { SidebarLayout, BlankLayout, AdminIframeSidebarLayout } from '@/layouts'
// Platform 布局组件映射
const platformLayoutMap: Record<string, () => Promise<any>> = {
'SidebarLayout': () => Promise.resolve({ default: SidebarLayout }),
'BlankLayout': () => Promise.resolve({ default: BlankLayout }),
'NavigationLayout': () => Promise.resolve({ default: SidebarLayout }),
'BasicLayout': () => Promise.resolve({ default: SidebarLayout })
'BasicLayout': () => Promise.resolve({ default: SidebarLayout }),
'AdminIframeSidebarLayout': () => Promise.resolve({ default: AdminIframeSidebarLayout })
}
// 视图组件加载器

View File

@@ -54,9 +54,34 @@ router.beforeEach((to, from, next) => {
dynamicRoutesLoaded = true
const loaded = loadRoutesFromStorage()
if (loaded && to.path !== '/') {
// 动态路由已加载,重新导航到目标路由
next({ ...to, replace: true })
if (loaded) {
if (to.path === '/') {
// 访问根路径,重定向到第一个可用路由
const firstRoute = getFirstAvailableRoute()
if (firstRoute && firstRoute !== '/') {
// 只有当第一个路由不是 / 时才重定向,避免无限循环
console.log('[Platform Router] 根路径重定向到:', firstRoute)
next({ path: firstRoute, replace: true })
return
} else {
// 第一个路由就是 /,直接放行
console.log('[Platform Router] 第一个路由就是根路径,直接放行')
}
} else {
// 动态路由已加载,重新导航到目标路由
next({ ...to, replace: true })
return
}
}
}
// 如果已登录且访问根路径,但动态路由已加载,重定向到第一个可用路由
if (hasToken && to.path === '/' && dynamicRoutesLoaded) {
const firstRoute = getFirstAvailableRoute()
if (firstRoute && firstRoute !== '/') {
// 只有当第一个路由不是 / 时才重定向,避免无限循环
console.log('[Platform Router] 已登录访问根路径,重定向到:', firstRoute)
next({ path: firstRoute, replace: true })
return
}
}
@@ -64,6 +89,52 @@ router.beforeEach((to, from, next) => {
next()
})
/**
* 获取第一个可用的路由路径
*/
function getFirstAvailableRoute(): string | null {
try {
console.log('[Platform Router] 开始获取第一个可用路由...')
const loginDomainStr = localStorage.getItem('loginDomain')
if (!loginDomainStr) {
console.warn('[Platform Router] localStorage 中没有 loginDomain')
return null
}
const loginDomain = JSON.parse(loginDomainStr)
const userViews = loginDomain.userViews || []
console.log('[Platform Router] 所有用户视图:', userViews.length)
// 过滤出 platform 服务的非 admin 视图
// 注意:不限制 type因为首页路由可能是 type=3路由类型而不是 type=1菜单类型
const platformViews = userViews.filter((view: any) =>
view.service === 'platform' &&
!view.url?.startsWith('/admin') &&
view.url // 必须有 url 字段
)
console.log('[Platform Router] Platform 服务视图:', platformViews)
if (platformViews.length === 0) {
console.warn('[Platform Router] 没有找到 platform 服务的视图')
return null
}
// 按 orderNum 排序
platformViews.sort((a: any, b: any) => (a.orderNum || 0) - (b.orderNum || 0))
const firstRoute = platformViews[0].url
console.log('[Platform Router] 第一个路由:', firstRoute, '视图:', platformViews[0].name)
return firstRoute
} catch (error) {
console.error('[Platform Router] 获取首页路由失败:', error)
return null
}
}
// 导出动态路由生成函数
export { addDynamicRoutes, loadRoutesFromStorage } from './dynamicRoute'

View File

@@ -181,4 +181,6 @@ declare module 'shared/layouts' {
import { DefineComponent } from 'vue'
export const BlankLayout: DefineComponent<{}, {}, any>
export const AdminSidebarLayout: DefineComponent<{}, {}, any>
}

View File

@@ -0,0 +1,11 @@
<template>
<div class="overview">
<!-- 数据概览视图 -->
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped>
@import url("./OverviewView.scss");
</style>

View File

@@ -0,0 +1,11 @@
<template>
<div class="user-management">
<!-- 用户管理视图 -->
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped>
@import url("./UserManagementView.scss");
</style>

View File

@@ -1,90 +0,0 @@
<template>
<div class="iframe-view">
<iframe
v-if="iframeUrl"
:src="iframeUrl"
class="iframe-content"
frameborder="0"
@load="handleLoad"
/>
<div v-else class="iframe-error">
<el-icon class="error-icon"><WarningFilled /></el-icon>
<p>无效的 iframe 地址</p>
</div>
<div v-if="loading" class="iframe-loading">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { Loading, WarningFilled } from '@element-plus/icons-vue'
const route = useRoute()
const loading = ref(true)
// 从路由 meta 中获取 iframe URL
const iframeUrl = computed(() => {
return route.meta.iframeUrl as string || ''
})
function handleLoad() {
loading.value = false
}
onMounted(() => {
console.log('[IframeView] 加载 iframe:', iframeUrl.value)
})
</script>
<style lang="scss" scoped>
.iframe-view {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.iframe-content {
width: 100%;
height: 100%;
border: none;
}
.iframe-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--el-text-color-secondary);
.error-icon {
font-size: 48px;
margin-bottom: 16px;
color: var(--el-color-warning);
}
}
.iframe-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--el-bg-color);
gap: 12px;
.el-icon {
font-size: 32px;
color: var(--el-color-primary);
}
}
</style>