前端四种布局模式切换实现方案

作者:忆笙智云官方 | 发布时间:2026-06-09 08:45 | 更新时间:2026-07-09 08:45

前端四种布局模式切换实现方案

引言

企业级后台管理系统中,不同用户对页面布局有不同的偏好:有人喜欢经典的左侧菜单+顶部导航,有人偏好横向菜单布局以获得更多纵向空间,还有人习惯分栏布局以便同时查看多个模块。支持多种布局模式并允许用户自由切换,是提升用户体验的重要手段。

本文分享在 Vue 3 项目中实现四种布局模式(默认/经典/横向/分栏)切换的完整方案,包括布局组件设计、动态切换机制、用户级配置持久化,以及无需刷新页面即可切换的技术实现。

核心内容

一、四种布局模式概览

flowchart TB
    A[四种布局模式] --> B[默认布局
Default]
    A --> C[经典布局
Classic]
    A --> D[横向布局
Transverse]
    A --> E[分栏布局
Columns]

    B --> B1[顶部导航栏
左侧菜单
右侧内容区]
    C --> C1[顶部Logo+导航
左侧菜单
右侧内容区]
    D --> D1[顶部导航+菜单
右侧内容区
无侧边栏]
    E --> E1[左侧分栏菜单
右侧内容区
分栏可折叠]

四种布局的结构差异:

布局模式 顶部栏 侧边栏 特点
默认 有(含Logo+操作区) 有(可折叠) 最常用,功能完整
经典 有(仅操作区) 有(含Logo+菜单) 传统后台风格
横向 有(含Logo+菜单+操作区) 纵向空间最大化
分栏 有(含操作区) 有(分栏常驻+子菜单) 多级菜单快速导航

二、布局组件架构

flowchart TB
    A[App.vue] --> B[LayoutContainer]
    B --> C{当前布局模式}

    C -->|default| D[DefaultLayout]
    C -->|classic| E[ClassicLayout]
    C -->|transverse| F[TransverseLayout]
    C -->|columns| G[ColumnsLayout]

    D --> H[LayoutHeader]
    D --> I[LayoutSidebar]
    D --> J[LayoutMain]

    E --> K[LayoutSidebar
含Logo]
    E --> L[LayoutHeader
不含Logo]
    E --> M[LayoutMain]

    F --> N[LayoutHeader
含菜单]
    F --> O[LayoutMain]

    G --> P[LayoutColumns
分栏菜单]
    G --> Q[LayoutMain]

    J --> R[LayoutTabs]
    M --> R
    O --> R
    Q --> R
    R --> S[RouterView]

三、布局状态管理

使用 Pinia 管理布局状态,实现响应式切换:

/**
 * 布局状态管理
 * 管理布局模式、侧边栏状态、主题等全局布局配置
 */
import { defineStore } from 'pinia'

export const useLayoutStore = defineStore('layout', {
  state: () => ({
    /** 当前布局模式 */
    layoutMode: 'default',

    /** 侧边栏是否折叠 */
    isCollapse: false,

    /** 分栏布局-左侧分栏是否折叠 */
    isColumnsCollapse: false,

    /** 侧边栏宽度(展开) */
    sidebarWidth: 210,

    /** 侧边栏宽度(折叠) */
    sidebarCollapseWidth: 64,

    /** 是否显示标签页 */
    showTabs: true,

    /** 是否显示页脚 */
    showFooter: true,
  }),

  getters: {
    /** 当前侧边栏实际宽度 */
    currentSidebarWidth: (state) => {
      return state.isCollapse ? state.sidebarCollapseWidth : state.sidebarWidth
    },

    /** 是否有侧边栏 */
    hasSidebar: (state) => {
      return state.layoutMode !== 'transverse'
    },
  },

  actions: {
    /** 切换布局模式 */
    setLayoutMode(mode) {
      const validModes = ['default', 'classic', 'transverse', 'columns']
      if (!validModes.includes(mode)) return

      this.layoutMode = mode

      // 横向布局时折叠侧边栏
      if (mode === 'transverse') {
        this.isCollapse = true
      }

      // 持久化到用户配置
      this.persistLayoutConfig()
    },

    /** 切换侧边栏折叠状态 */
    toggleCollapse() {
      this.isCollapse = !this.isCollapse
    },

    /** 持久化布局配置 */
    persistLayoutConfig() {
      const config = {
        layoutMode: this.layoutMode,
        isCollapse: this.isCollapse,
        showTabs: this.showTabs,
        showFooter: this.showFooter,
      }
      localStorage.setItem('layout-config', JSON.stringify(config))
    },

    /** 从本地存储恢复布局配置 */
    restoreLayoutConfig() {
      try {
        const config = JSON.parse(localStorage.getItem('layout-config'))
        if (config) {
          this.layoutMode = config.layoutMode || 'default'
          this.isCollapse = config.isCollapse || false
          this.showTabs = config.showTabs !== false
          this.showFooter = config.showFooter !== false
        }
      } catch (e) {
        console.warn('恢复布局配置失败', e)
      }
    },
  },
})

四、布局容器组件

布局容器是所有布局模式的入口,根据当前模式动态渲染对应布局:

<template>
  <component :is="layoutComponent" />
</template>

<script setup>
import { computed, onMounted } from 'vue'
import { useLayoutStore } from '@/store/modules/layout'
import DefaultLayout from './layouts/DefaultLayout.vue'
import ClassicLayout from './layouts/ClassicLayout.vue'
import TransverseLayout from './layouts/TransverseLayout.vue'
import ColumnsLayout from './layouts/ColumnsLayout.vue'

const layoutStore = useLayoutStore()

/** 布局模式与组件的映射 */
const layoutMap = {
  default: DefaultLayout,
  classic: ClassicLayout,
  transverse: TransverseLayout,
  columns: ColumnsLayout,
}

/** 当前布局组件 */
const layoutComponent = computed(() => {
  return layoutMap[layoutStore.layoutMode] || DefaultLayout
})

/** 应用启动时恢复用户布局配置 */
onMounted(() => {
  layoutStore.restoreLayoutConfig()
})
</script>

五、各布局模式实现

5.1 默认布局

<!-- DefaultLayout.vue -->
<template>
  <el-container class="layout-default">
    <!-- 顶部导航栏 -->
    <el-header class="layout-header" height="50px">
      <LayoutHeader />
    </el-header>

    <el-container>
      <!-- 左侧菜单 -->
      <el-aside :width="layoutStore.currentSidebarWidth + 'px'" class="layout-sidebar">
        <LayoutSidebar />
      </el-aside>

      <!-- 右侧内容区 -->
      <el-main class="layout-main">
        <LayoutTabs v-if="layoutStore.showTabs" />
        <router-view v-slot="{ Component }">
          <transition name="fade" mode="out-in">
            <component :is="Component" />
          </transition>
        </router-view>
      </el-main>
    </el-container>
  </el-container>
</template>

<script setup>
import { useLayoutStore } from '@/store/modules/layout'
import LayoutHeader from '../components/LayoutHeader.vue'
import LayoutSidebar from '../components/LayoutSidebar.vue'
import LayoutTabs from '../components/LayoutTabs.vue'

const layoutStore = useLayoutStore()
</script>

<style scoped>
.layout-default {
  height: 100vh;
  overflow: hidden;
}

.layout-header {
  padding: 0;
  border-bottom: 1px solid var(--el-border-color-light);
  transition: background-color 0.3s;
}

.layout-sidebar {
  transition: width 0.3s ease;
  overflow: hidden;
  border-right: 1px solid var(--el-border-color-light);
}

.layout-main {
  padding: 0;
  overflow: auto;
  background-color: var(--el-bg-color-page);
}
</style>

5.2 经典布局

<!-- ClassicLayout.vue -->
<template>
  <el-container class="layout-classic">
    <el-container>
      <!-- 左侧:Logo + 菜单 -->
      <el-aside :width="layoutStore.currentSidebarWidth + 'px'" class="layout-sidebar">
        <div class="sidebar-logo">
          <img src="@/assets/logo.png" alt="Logo" />
          <span v-show="!layoutStore.isCollapse" class="logo-text">管理系统</span>
        </div>
        <LayoutSidebar />
      </el-aside>

      <!-- 右侧:顶部栏 + 内容 -->
      <el-container>
        <el-header height="50px" class="layout-header">
          <LayoutHeader :show-logo="false" />
        </el-header>
        <el-main class="layout-main">
          <LayoutTabs v-if="layoutStore.showTabs" />
          <router-view />
        </el-main>
      </el-container>
    </el-container>
  </el-container>
</template>

5.3 横向布局

<!-- TransverseLayout.vue -->
<template>
  <el-container class="layout-transverse">
    <!-- 顶部:Logo + 菜单 + 操作区 -->
    <el-header height="50px" class="layout-header">
      <div class="header-left">
        <div class="header-logo">
          <img src="@/assets/logo.png" alt="Logo" />
          <span class="logo-text">管理系统</span>
        </div>
        <!-- 横向菜单 -->
        <LayoutHorizontalMenu />
      </div>
      <div class="header-right">
        <LayoutHeaderActions />
      </div>
    </el-header>

    <!-- 内容区 -->
    <el-main class="layout-main">
      <LayoutTabs v-if="layoutStore.showTabs" />
      <router-view />
    </el-main>
  </el-container>
</template>

<style scoped>
.layout-transverse .layout-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
}

.header-left {
  display: flex;
  align-items: center;
  gap: 20px;
}
</style>

5.4 分栏布局

<!-- ColumnsLayout.vue -->
<template>
  <el-container class="layout-columns">
    <!-- 左侧常驻分栏(一级菜单图标) -->
    <el-aside width="64px" class="layout-columns-aside">
      <div class="columns-logo">
        <img src="@/assets/logo.png" alt="Logo" width="32" />
      </div>
      <LayoutColumnsMenu :menu-list="rootMenuList" />
    </el-aside>

    <!-- 二级菜单 -->
    <el-aside
      v-show="!layoutStore.isColumnsCollapse"
      width="180px"
      class="layout-columns-sub"
    >
      <div class="sub-menu-title">{{ currentRootMenu?.meta?.title }}</div>
      <LayoutSidebar :menu-list="currentSubMenuList" />
    </el-aside>

    <!-- 右侧内容区 -->
    <el-container>
      <el-header height="50px" class="layout-header">
        <LayoutHeader />
      </el-header>
      <el-main class="layout-main">
        <LayoutTabs v-if="layoutStore.showTabs" />
        <router-view />
      </el-main>
    </el-container>
  </el-container>
</template>

<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useLayoutStore } from '@/store/modules/layout'
import { useMenuStore } from '@/store/modules/menu'

const route = useRoute()
const layoutStore = useLayoutStore()
const menuStore = useMenuStore()

/** 一级菜单列表 */
const rootMenuList = computed(() => menuStore.menuList)

/** 当前激活的一级菜单 */
const currentRootMenu = computed(() => {
  const matched = route.matched
  return rootMenuList.value.find(m => matched.some(r => r.path === m.path))
})

/** 当前一级菜单的子菜单列表 */
const currentSubMenuList = computed(() => {
  return currentRootMenu.value?.children || []
})
</script>

六、布局切换组件

提供可视化的布局切换入口,让用户一键切换:

<template>
  <div class="layout-switcher">
    <el-popover placement="bottom" :width="300" trigger="click">
      <template #reference>
        <el-button :icon="Grid" circle />
      </template>

      <div class="layout-options">
        <h4>布局模式</h4>
        <div class="layout-grid">
          <div
            v-for="item in layoutOptions"
            :key="item.value"
            class="layout-item"
            :class="{ active: layoutStore.layoutMode === item.value }"
            @click="switchLayout(item.value)"
          >
            <!-- 布局缩略图 -->
            <div class="layout-preview" :class="'preview-' + item.value">
              <div class="preview-header"></div>
              <div class="preview-body">
                <div v-if="item.hasSidebar" class="preview-sidebar"></div>
                <div class="preview-content"></div>
              </div>
            </div>
            <span class="layout-label">{{ item.label }}</span>
          </div>
        </div>
      </div>
    </el-popover>
  </div>
</template>

<script setup>
import { Grid } from '@element-plus/icons-vue'
import { useLayoutStore } from '@/store/modules/layout'

const layoutStore = useLayoutStore()

const layoutOptions = [
  { value: 'default', label: '默认', hasSidebar: true },
  { value: 'classic', label: '经典', hasSidebar: true },
  { value: 'transverse', label: '横向', hasSidebar: false },
  { value: 'columns', label: '分栏', hasSidebar: true },
]

const switchLayout = (mode) => {
  layoutStore.setLayoutMode(mode)
}
</script>

<style scoped>
.layout-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
}

.layout-item {
  cursor: pointer;
  padding: 8px;
  border-radius: 6px;
  border: 2px solid transparent;
  transition: all 0.2s;
}

.layout-item.active {
  border-color: var(--el-color-primary);
}

.layout-item:hover {
  background: var(--el-fill-color-light);
}

/* 布局缩略图样式 */
.layout-preview {
  height: 60px;
  border: 1px solid var(--el-border-color);
  border-radius: 4px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.preview-header {
  height: 12px;
  background: var(--el-color-primary-light-5);
}

.preview-body {
  flex: 1;
  display: flex;
}

.preview-sidebar {
  width: 20px;
  background: var(--el-color-primary-light-7);
}

.preview-content {
  flex: 1;
  background: var(--el-fill-color-lighter);
}

/* 横向布局缩略图 */
.preview-transverse .preview-body .preview-content {
  width: 100%;
}

.layout-label {
  display: block;
  text-align: center;
  margin-top: 4px;
  font-size: 12px;
}
</style>

七、用户级配置持久化

布局偏好属于用户个人设置,需要持久化到服务端,换设备也能恢复:

flowchart TB
    A[用户切换布局] --> B[更新 Pinia 状态]
    B --> C[localStorage 即时保存]
    C --> D[异步同步到服务端]

    D --> E[PUT /api/user/layout-config]
    E --> F[保存到用户配置表]

    subgraph 下次登录恢复
        G[用户登录] --> H[GET /api/user/layout-config]
        H --> I[恢复 Pinia 状态]
        I --> J[渲染对应布局]
    end

7.1 持久化服务

/**
 * 布局配置持久化服务
 * 本地缓存 + 服务端持久化双保险
 */
import { useLayoutStore } from '@/store/modules/layout'
import { getUserConfig, updateUserConfig } from '@/api/system/user-config'

const CACHE_KEY = 'layout-config'

export const LayoutPersistService = {
  /** 保存到本地缓存 */
  saveToLocal(config) {
    localStorage.setItem(CACHE_KEY, JSON.stringify(config))
  },

  /** 从本地缓存读取 */
  loadFromLocal() {
    try {
      return JSON.parse(localStorage.getItem(CACHE_KEY))
    } catch {
      return null
    }
  },

  /** 同步到服务端 */
  async syncToServer(config) {
    try {
      await updateUserConfig({ type: 'layout', content: config })
    } catch (e) {
      console.warn('布局配置同步服务端失败', e)
    }
  },

  /** 从服务端加载 */
  async loadFromServer() {
    try {
      const res = await getUserConfig({ type: 'layout' })
      return res.data?.content || null
    } catch {
      return null
    }
  },

  /** 初始化布局配置(优先服务端,降级本地) */
  async init() {
    const store = useLayoutStore()

    // 优先从服务端获取
    const serverConfig = await this.loadFromServer()
    if (serverConfig) {
      store.setLayoutMode(serverConfig.layoutMode)
      store.restoreFromConfig(serverConfig)
      this.saveToLocal(serverConfig)
      return
    }

    // 降级到本地缓存
    const localConfig = this.loadFromLocal()
    if (localConfig) {
      store.restoreFromConfig(localConfig)
    }
  },
}

八、布局切换过渡动画

切换布局时添加过渡动画,避免突兀的页面跳变:

/* 布局切换过渡 */
.layout-container {
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

/* 侧边栏宽度过渡 */
.layout-sidebar {
  transition: width 0.3s ease, transform 0.3s ease;
}

/* 内容区路由切换过渡 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.2s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

结论与建议

核心要点

  1. 组件化布局架构:每种布局模式独立组件,通过动态组件切换,互不干扰
  2. Pinia 集中管理状态:布局模式、侧边栏状态等统一管理,响应式更新
  3. 无需刷新页面切换:利用 Vue 的动态组件 <component :is> 实现无缝切换
  4. 用户级持久化:本地缓存 + 服务端存储双保险,换设备也能恢复偏好

最佳实践建议

建议 说明
布局组件职责单一 每种布局只负责结构编排,业务逻辑放在子组件中
公共组件复用 Header、Sidebar、Tabs 等组件在不同布局间复用
过渡动画适度 布局切换动画不宜过长,300ms 以内为佳
移动端适配 横向布局在移动端自动降级为默认布局
性能优化 布局组件使用 keep-alive 缓存,避免重复渲染
配置项可扩展 预留更多布局配置项(如侧边栏宽度、头部高度等)

相关资源