feat: 移动端自适应布局

- 768px 断点: 侧边栏变为抽屉式滑出, 汉堡按钮切换
- 移动端顶栏: 显示当前组件名称
- 移动端 Tab: 配置表单/代码预览一键切换
- 遮罩层: 点击遮罩自动收起侧边栏
- 切换组件后自动收起侧边栏并回到表单 tab
- 平板端 (1024px) 表单区域适当缩窄
This commit is contained in:
cnbugs
2026-07-19 16:35:29 +08:00
parent b951e29c00
commit c45a59c6d1
2 changed files with 201 additions and 5 deletions
+199 -3
View File
@@ -1,14 +1,57 @@
<template>
<div class="app-layout">
<Sidebar v-model="activeSchemaId" />
<div class="main-content">
<!-- 移动端顶部栏 -->
<div class="mobile-header">
<button class="hamburger" @click="sidebarOpen = !sidebarOpen">
<el-icon :size="22"><Expand /></el-icon>
</button>
<div class="mobile-logo">
<h1>ConfTemplate</h1>
</div>
<el-tag size="small" type="info">{{ currentSchema.name }}</el-tag>
</div>
<!-- 遮罩层 -->
<div
v-if="sidebarOpen"
class="sidebar-overlay"
@click="sidebarOpen = false"
/>
<!-- 侧边栏 -->
<div class="sidebar-wrapper" :class="{ open: sidebarOpen }">
<Sidebar
v-model="activeSchemaId"
@select="sidebarOpen = false"
/>
</div>
<!-- 移动端 Tab 切换 -->
<div class="mobile-tabs">
<button
:class="{ active: activeTab === 'form' }"
@click="activeTab = 'form'"
>
<el-icon><Edit /></el-icon> 配置表单
</button>
<button
:class="{ active: activeTab === 'preview' }"
@click="activeTab = 'preview'"
>
<el-icon><Document /></el-icon> 代码预览
</button>
</div>
<!-- 主内容区 -->
<div class="main-content" :class="{ 'mobile-hidden': activeTab !== 'form' }">
<ConfigForm
:key="activeSchemaId"
:schema="currentSchema"
@update:config="onConfigUpdate"
/>
</div>
<div class="preview-content">
<div class="preview-content" :class="{ 'mobile-hidden': activeTab !== 'preview' }">
<CodePreview :code="generatedCode" :schema="currentSchema" />
</div>
</div>
@@ -16,6 +59,7 @@
<script setup>
import { ref, computed, watch } from 'vue'
import { Expand, Edit, Document } from '@element-plus/icons-vue'
import Sidebar from './components/Sidebar.vue'
import ConfigForm from './components/ConfigForm.vue'
import CodePreview from './components/CodePreview.vue'
@@ -23,12 +67,16 @@ import { schemas, generators, getDefaultValues } from './schemas'
const activeSchemaId = ref('nginx')
const currentConfig = ref({})
const sidebarOpen = ref(false)
const activeTab = ref('form')
const currentSchema = computed(() => schemas[activeSchemaId.value])
// 当 schema 切换时,立即用新 schema 的默认值重置 config
watch(activeSchemaId, (newId) => {
currentConfig.value = getDefaultValues(schemas[newId])
// 移动端切换组件后自动切到表单 tab
activeTab.value = 'form'
})
const generatedCode = computed(() => {
@@ -59,6 +107,7 @@ html, body, #app {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
/* ===== 桌面端布局 ===== */
.app-layout {
display: flex;
height: 100vh;
@@ -80,6 +129,21 @@ html, body, #app {
overflow: hidden;
}
/* 移动端顶栏 - 默认隐藏 */
.mobile-header {
display: none;
}
/* 遮罩层 - 默认隐藏 */
.sidebar-overlay {
display: none;
}
/* 移动端 Tab - 默认隐藏 */
.mobile-tabs {
display: none;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
@@ -111,4 +175,136 @@ html, body, #app {
.el-select {
width: 100%;
}
/* ===== 平板端 (<= 1024px) ===== */
@media (max-width: 1024px) {
.main-content {
flex: 0 0 360px;
min-width: 320px;
max-width: 400px;
}
}
/* ===== 移动端 (<= 768px) ===== */
@media (max-width: 768px) {
.app-layout {
flex-direction: column;
position: relative;
}
/* 移动端顶栏 */
.mobile-header {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: #1a1a2e;
color: #fff;
flex-shrink: 0;
z-index: 100;
}
.hamburger {
background: none;
border: none;
color: #fff;
cursor: pointer;
padding: 6px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.hamburger:hover {
background: rgba(255, 255, 255, 0.1);
}
.mobile-logo h1 {
font-size: 16px;
font-weight: 700;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* 遮罩层 */
.sidebar-overlay {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 199;
}
/* 侧边栏变为抽屉 */
.sidebar-wrapper {
position: fixed;
top: 0;
left: -280px;
bottom: 0;
z-index: 200;
transition: left 0.3s ease;
}
.sidebar-wrapper.open {
left: 0;
}
/* 移动端 Tab */
.mobile-tabs {
display: flex;
background: #fff;
border-bottom: 1px solid #ebeef5;
flex-shrink: 0;
z-index: 50;
}
.mobile-tabs button {
flex: 1;
padding: 10px;
border: none;
background: none;
font-size: 13px;
font-weight: 500;
color: #909399;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.mobile-tabs button.active {
color: #667eea;
border-bottom-color: #667eea;
background: rgba(102, 126, 234, 0.04);
}
/* 主内容区全宽 */
.main-content {
flex: 1;
min-width: 0;
max-width: none;
border-right: none;
overflow: hidden;
}
.preview-content {
flex: 1;
min-width: 0;
overflow: hidden;
}
/* 隐藏非活跃 tab */
.mobile-hidden {
display: none !important;
}
}
</style>
+2 -2
View File
@@ -41,7 +41,7 @@
:key="itemId"
class="nav-item"
:class="{ active: modelValue === itemId }"
@click="$emit('update:modelValue', itemId)"
@click="$emit('update:modelValue', itemId); $emit('select')"
>
<el-icon><component :is="schemas[itemId].icon" /></el-icon>
<div class="nav-item-info">
@@ -74,7 +74,7 @@ const props = defineProps({
modelValue: String,
})
defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'select'])
const searchQuery = ref('')