LayoutSwitch.vue是布局切换组件,提供用户切换不同布局模式的功能,支持管理布局、混合布局、顶部布局等多种布局模式。
文件位置: src/components/common/LayoutSwitch.vue
- 支持多种布局模式切换
- 实时预览布局效果
- 状态持久化保存
- 响应式布局适配
- 布局配置管理
<template>
<div class="layout-switch">
<el-dropdown
@command="handleCommand"
trigger="click"
placement="bottom-end"
>
<div class="layout-trigger">
<el-icon class="layout-icon">
<component :is="currentLayoutIcon" />
</el-icon>
<span class="layout-text">{{ currentLayoutText }}</span>
<el-icon class="dropdown-icon">
<ArrowDown />
</el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="layout in layoutOptions"
:key="layout.value"
:command="layout.value"
:class="{ 'is-active': currentLayout === layout.value }"
>
<div class="layout-option">
<div class="layout-preview">
<div :class="['preview-container', `preview-${layout.value}`]">
<div class="preview-header"></div>
<div class="preview-body">
<div v-if="layout.showSidebar" class="preview-sidebar"></div>
<div class="preview-main"></div>
</div>
</div>
</div>
<div class="layout-info">
<span class="layout-name">{{ layout.label }}</span>
<small class="layout-desc">{{ layout.description }}</small>
</div>
<el-icon v-if="currentLayout === layout.value" class="check-icon">
<Check />
</el-icon>
</div>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template><script setup>
import { ref, computed, onMounted } from 'vue'
import { useLayoutStore } from '@/stores/layout'
import { ElMessage } from 'element-plus'
import {
ArrowDown,
Check,
Grid,
Menu,
TopRight
} from '@element-plus/icons-vue'
// 状态管理
const layoutStore = useLayoutStore()
// 布局选项配置
const layoutOptions = ref([
{
value: 'admin',
label: '管理布局',
description: '经典后台管理布局',
icon: 'Menu',
showSidebar: true,
showHeader: true,
features: ['侧边栏导航', '顶部工具栏', '面包屑导航']
},
{
value: 'mix',
label: '混合布局',
description: '顶部+侧边混合布局',
icon: 'Grid',
showSidebar: true,
showHeader: true,
features: ['顶部主导航', '侧边子导航', '灵活切换']
},
{
value: 'top',
label: '顶部布局',
description: '顶部水平导航布局',
icon: 'TopRight',
showSidebar: false,
showHeader: true,
features: ['水平导航', '简洁界面', '移动友好']
}
])
// 当前布局
const currentLayout = computed(() => layoutStore.layoutMode)
// 当前布局信息
const currentLayoutInfo = computed(() => {
return layoutOptions.value.find(layout => layout.value === currentLayout.value) || layoutOptions.value[0]
})
// 当前布局图标
const currentLayoutIcon = computed(() => currentLayoutInfo.value.icon)
// 当前布局文本
const currentLayoutText = computed(() => currentLayoutInfo.value.label)
</script>// 处理命令
const handleCommand = (layoutMode) => {
if (layoutMode === currentLayout.value) {
return
}
setLayoutMode(layoutMode)
}
// 设置布局模式
const setLayoutMode = (mode) => {
// 更新布局状态
layoutStore.setLayoutMode(mode)
// 应用布局变化
applyLayoutChanges(mode)
// 显示切换提示
showLayoutChangeNotification(mode)
// 触发布局变化事件
emitLayoutChange(mode)
}
// 应用布局变化
const applyLayoutChanges = (mode) => {
const layoutInfo = layoutOptions.value.find(l => l.value === mode)
if (!layoutInfo) return
// 根据布局模式调整相关设置
switch (mode) {
case 'admin':
layoutStore.setShowSidebar(true)
layoutStore.setShowHeader(true)
break
case 'mix':
layoutStore.setShowSidebar(true)
layoutStore.setShowHeader(true)
break
case 'top':
layoutStore.setShowSidebar(false)
layoutStore.setShowHeader(true)
break
}
// 移动端适配
if (layoutStore.isMobile) {
adaptMobileLayout(mode)
}
// 更新CSS类
updateLayoutClasses(mode)
}
// 移动端布局适配
const adaptMobileLayout = (mode) => {
if (mode === 'admin' || mode === 'mix') {
// 移动端自动折叠侧边栏
layoutStore.setSidebarCollapse(true)
}
}
// 更新布局CSS类
const updateLayoutClasses = (mode) => {
const body = document.body
// 移除旧的布局类
body.classList.remove('layout-admin', 'layout-mix', 'layout-top')
// 添加新的布局类
body.classList.add(`layout-${mode}`)
// 更新根元素属性
document.documentElement.setAttribute('data-layout', mode)
}
// 显示布局切换通知
const showLayoutChangeNotification = (mode) => {
const layoutInfo = layoutOptions.value.find(l => l.value === mode)
if (!layoutInfo) return
ElMessage({
message: `已切换到${layoutInfo.label}`,
type: 'success',
duration: 2000,
showClose: false
})
}
// 触发布局变化事件
const emitLayoutChange = (mode) => {
window.dispatchEvent(new CustomEvent('layout-change', {
detail: {
mode,
layoutInfo: layoutOptions.value.find(l => l.value === mode)
}
}))
}// 布局预览配置
const previewConfig = {
admin: {
headerHeight: '12px',
sidebarWidth: '20px',
mainPadding: '4px'
},
mix: {
headerHeight: '12px',
sidebarWidth: '16px',
mainPadding: '4px'
},
top: {
headerHeight: '12px',
sidebarWidth: '0px',
mainPadding: '4px'
}
}
// 生成预览样式
const generatePreviewStyle = (layoutType) => {
const config = previewConfig[layoutType]
if (!config) return {}
return {
'--preview-header-height': config.headerHeight,
'--preview-sidebar-width': config.sidebarWidth,
'--preview-main-padding': config.mainPadding
}
}// 获取布局配置
const getLayoutConfig = (mode) => {
const configs = {
admin: {
showSidebar: true,
showHeader: true,
sidebarPosition: 'left',
headerPosition: 'top',
menuMode: 'vertical',
contentPadding: '20px'
},
mix: {
showSidebar: true,
showHeader: true,
sidebarPosition: 'left',
headerPosition: 'top',
menuMode: 'horizontal-vertical',
contentPadding: '20px'
},
top: {
showSidebar: false,
showHeader: true,
sidebarPosition: 'none',
headerPosition: 'top',
menuMode: 'horizontal',
contentPadding: '20px'
}
}
return configs[mode] || configs.admin
}
// 应用布局配置
const applyLayoutConfig = (mode) => {
const config = getLayoutConfig(mode)
// 应用配置到布局store
Object.keys(config).forEach(key => {
if (typeof layoutStore[`set${capitalize(key)}`] === 'function') {
layoutStore[`set${capitalize(key)}`](config[key])
}
})
}
// 首字母大写
const capitalize = (str) => {
return str.charAt(0).toUpperCase() + str.slice(1)
}// 监听设备变化
const handleDeviceChange = () => {
const device = layoutStore.device
// 根据设备类型调整布局
if (device === 'mobile') {
handleMobileLayout()
} else if (device === 'tablet') {
handleTabletLayout()
} else {
handleDesktopLayout()
}
}
// 移动端布局处理
const handleMobileLayout = () => {
// 移动端推荐使用顶部布局
if (currentLayout.value === 'admin' || currentLayout.value === 'mix') {
// 自动折叠侧边栏
layoutStore.setSidebarCollapse(true)
}
// 调整布局参数
layoutStore.setSidebarWidth(200) // 移动端侧边栏更窄
}
// 平板端布局处理
const handleTabletLayout = () => {
// 平板端适中的布局参数
layoutStore.setSidebarWidth(220)
}
// 桌面端布局处理
const handleDesktopLayout = () => {
// 桌面端完整的布局参数
layoutStore.setSidebarWidth(240)
// 恢复侧边栏状态
const savedCollapse = localStorage.getItem('sidebarCollapse')
if (savedCollapse !== null) {
layoutStore.setSidebarCollapse(savedCollapse === 'true')
}
}
// 监听设备变化
onMounted(() => {
// 初始化设备处理
handleDeviceChange()
// 监听设备变化事件
window.addEventListener('resize', handleDeviceChange)
})
onUnmounted(() => {
window.removeEventListener('resize', handleDeviceChange)
})// 布局切换动画
const animateLayoutChange = (fromMode, toMode) => {
const duration = 300
// 添加切换动画类
document.body.classList.add('layout-switching')
// 动画完成后移除类
setTimeout(() => {
document.body.classList.remove('layout-switching')
}, duration)
// 触发重新计算布局
setTimeout(() => {
window.dispatchEvent(new Event('resize'))
}, duration / 2)
}.layout-switch {
display: inline-block;
}
.layout-trigger {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
&:hover {
background: var(--el-fill-color-light);
}
}
.layout-icon {
margin-right: 6px;
font-size: 16px;
color: var(--el-text-color-regular);
}
.layout-text {
font-size: 13px;
color: var(--el-text-color-regular);
margin-right: 4px;
}
.dropdown-icon {
font-size: 12px;
color: var(--el-text-color-placeholder);
transition: transform 0.2s ease;
}
.layout-trigger:hover .dropdown-icon {
transform: rotate(180deg);
}.layout-option {
display: flex;
align-items: center;
width: 280px;
padding: 12px;
&:hover {
background: var(--el-fill-color-light);
}
&.is-active {
background: var(--el-color-primary-light-9);
.layout-name {
color: var(--el-color-primary);
font-weight: 500;
}
}
}
.layout-preview {
margin-right: 12px;
flex-shrink: 0;
}
.preview-container {
width: 60px;
height: 40px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
overflow: hidden;
background: var(--el-bg-color);
}
.preview-header {
height: var(--preview-header-height, 12px);
background: var(--el-color-primary-light-7);
}
.preview-body {
display: flex;
height: calc(100% - var(--preview-header-height, 12px));
}
.preview-sidebar {
width: var(--preview-sidebar-width, 20px);
background: var(--el-color-primary-light-8);
}
.preview-main {
flex: 1;
background: var(--el-fill-color-lighter);
margin: var(--preview-main-padding, 4px);
border-radius: 2px;
}
/* 不同布局的预览样式 */
.preview-admin {
--preview-header-height: 12px;
--preview-sidebar-width: 20px;
--preview-main-padding: 4px;
}
.preview-mix {
--preview-header-height: 12px;
--preview-sidebar-width: 16px;
--preview-main-padding: 4px;
}
.preview-top {
--preview-header-height: 12px;
--preview-sidebar-width: 0px;
--preview-main-padding: 4px;
}.layout-info {
flex: 1;
min-width: 0;
}
.layout-name {
display: block;
font-size: 14px;
color: var(--el-text-color-primary);
margin-bottom: 4px;
}
.layout-desc {
display: block;
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.4;
}
.check-icon {
margin-left: 8px;
font-size: 16px;
color: var(--el-color-primary);
flex-shrink: 0;
}/* 布局切换动画 */
.layout-switching {
.layout-container {
transition: all 0.3s ease;
}
.sidebar {
transition: width 0.3s ease, transform 0.3s ease;
}
.main-content {
transition: margin-left 0.3s ease, padding 0.3s ease;
}
}
/* 不同布局模式的样式 */
.layout-admin {
.sidebar {
position: fixed;
left: 0;
top: 60px;
}
.main-content {
margin-left: var(--sidebar-width);
}
}
.layout-mix {
.sidebar {
position: fixed;
left: 0;
top: 60px;
}
.main-content {
margin-left: var(--sidebar-width);
}
.header-menu {
display: flex;
}
}
.layout-top {
.sidebar {
display: none;
}
.main-content {
margin-left: 0;
}
.header-menu {
display: flex;
justify-content: center;
}
}// 保存布局模板
const saveLayoutTemplate = (name, config) => {
const templates = getLayoutTemplates()
templates[name] = {
...config,
createTime: Date.now(),
updateTime: Date.now()
}
localStorage.setItem('layoutTemplates', JSON.stringify(templates))
ElMessage.success('布局模板保存成功')
}
// 获取布局模板
const getLayoutTemplates = () => {
try {
const templates = localStorage.getItem('layoutTemplates')
return templates ? JSON.parse(templates) : {}
} catch (error) {
console.error('获取布局模板失败:', error)
return {}
}
}
// 应用布局模板
const applyLayoutTemplate = (templateName) => {
const templates = getLayoutTemplates()
const template = templates[templateName]
if (template) {
Object.keys(template).forEach(key => {
if (key !== 'createTime' && key !== 'updateTime') {
layoutStore[`set${capitalize(key)}`]?.(template[key])
}
})
ElMessage.success(`已应用布局模板:${templateName}`)
}
}// 布局快捷键处理
const handleKeyboard = (event) => {
// Ctrl + 1: 管理布局
if (event.ctrlKey && event.key === '1') {
event.preventDefault()
setLayoutMode('admin')
}
// Ctrl + 2: 混合布局
if (event.ctrlKey && event.key === '2') {
event.preventDefault()
setLayoutMode('mix')
}
// Ctrl + 3: 顶部布局
if (event.ctrlKey && event.key === '3') {
event.preventDefault()
setLayoutMode('top')
}
}
// 绑定快捷键
onMounted(() => {
document.addEventListener('keydown', handleKeyboard)
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeyboard)
})<template>
<div class="header-tools">
<LayoutSwitch />
</div>
</template>
<script setup>
import LayoutSwitch from '@/components/common/LayoutSwitch.vue'
</script><script setup>
import { onMounted, onUnmounted } from 'vue'
const handleLayoutChange = (event) => {
const { mode, layoutInfo } = event.detail
console.log(`布局已切换到: ${layoutInfo.label}`)
// 执行相关操作
handleLayoutChangeEffect(mode)
}
const handleLayoutChangeEffect = (mode) => {
// 重新计算图表大小
nextTick(() => {
window.dispatchEvent(new Event('resize'))
})
}
onMounted(() => {
window.addEventListener('layout-change', handleLayoutChange)
})
onUnmounted(() => {
window.removeEventListener('layout-change', handleLayoutChange)
})
</script><script setup>
import { ref } from 'vue'
// 自定义布局选项
const customLayoutOptions = ref([
{
value: 'custom',
label: '自定义布局',
description: '个性化布局配置',
icon: 'Setting',
showSidebar: true,
showHeader: true
}
])
</script>- 响应式: 确保在不同设备上的布局切换效果
- 性能: 布局切换时避免频繁的DOM操作
- 兼容性: 考虑不同浏览器的CSS支持
- 用户体验: 提供平滑的切换动画
- 状态管理: 正确保存和恢复布局状态
最后更新时间:2025-09-19