feat: 移动端自适应布局
- 768px 断点: 侧边栏变为抽屉式滑出, 汉堡按钮切换 - 移动端顶栏: 显示当前组件名称 - 移动端 Tab: 配置表单/代码预览一键切换 - 遮罩层: 点击遮罩自动收起侧边栏 - 切换组件后自动收起侧边栏并回到表单 tab - 平板端 (1024px) 表单区域适当缩窄
This commit is contained in:
+199
-3
@@ -1,14 +1,57 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-layout">
|
<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
|
<ConfigForm
|
||||||
:key="activeSchemaId"
|
:key="activeSchemaId"
|
||||||
:schema="currentSchema"
|
:schema="currentSchema"
|
||||||
@update:config="onConfigUpdate"
|
@update:config="onConfigUpdate"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-content">
|
|
||||||
|
<div class="preview-content" :class="{ 'mobile-hidden': activeTab !== 'preview' }">
|
||||||
<CodePreview :code="generatedCode" :schema="currentSchema" />
|
<CodePreview :code="generatedCode" :schema="currentSchema" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,6 +59,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { Expand, Edit, Document } from '@element-plus/icons-vue'
|
||||||
import Sidebar from './components/Sidebar.vue'
|
import Sidebar from './components/Sidebar.vue'
|
||||||
import ConfigForm from './components/ConfigForm.vue'
|
import ConfigForm from './components/ConfigForm.vue'
|
||||||
import CodePreview from './components/CodePreview.vue'
|
import CodePreview from './components/CodePreview.vue'
|
||||||
@@ -23,12 +67,16 @@ import { schemas, generators, getDefaultValues } from './schemas'
|
|||||||
|
|
||||||
const activeSchemaId = ref('nginx')
|
const activeSchemaId = ref('nginx')
|
||||||
const currentConfig = ref({})
|
const currentConfig = ref({})
|
||||||
|
const sidebarOpen = ref(false)
|
||||||
|
const activeTab = ref('form')
|
||||||
|
|
||||||
const currentSchema = computed(() => schemas[activeSchemaId.value])
|
const currentSchema = computed(() => schemas[activeSchemaId.value])
|
||||||
|
|
||||||
// 当 schema 切换时,立即用新 schema 的默认值重置 config
|
// 当 schema 切换时,立即用新 schema 的默认值重置 config
|
||||||
watch(activeSchemaId, (newId) => {
|
watch(activeSchemaId, (newId) => {
|
||||||
currentConfig.value = getDefaultValues(schemas[newId])
|
currentConfig.value = getDefaultValues(schemas[newId])
|
||||||
|
// 移动端切换组件后自动切到表单 tab
|
||||||
|
activeTab.value = 'form'
|
||||||
})
|
})
|
||||||
|
|
||||||
const generatedCode = computed(() => {
|
const generatedCode = computed(() => {
|
||||||
@@ -59,6 +107,7 @@ html, body, #app {
|
|||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 桌面端布局 ===== */
|
||||||
.app-layout {
|
.app-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
@@ -80,6 +129,21 @@ html, body, #app {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端顶栏 - 默认隐藏 */
|
||||||
|
.mobile-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 遮罩层 - 默认隐藏 */
|
||||||
|
.sidebar-overlay {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端 Tab - 默认隐藏 */
|
||||||
|
.mobile-tabs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* Custom scrollbar */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
@@ -111,4 +175,136 @@ html, body, #app {
|
|||||||
.el-select {
|
.el-select {
|
||||||
width: 100%;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
:key="itemId"
|
:key="itemId"
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
:class="{ active: modelValue === itemId }"
|
: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>
|
<el-icon><component :is="schemas[itemId].icon" /></el-icon>
|
||||||
<div class="nav-item-info">
|
<div class="nav-item-info">
|
||||||
@@ -74,7 +74,7 @@ const props = defineProps({
|
|||||||
modelValue: String,
|
modelValue: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue', 'select'])
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user