Compare commits
2 Commits
14f02df58d
..
0.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| c45a59c6d1 | |||
| b951e29c00 |
+205
-4
@@ -1,21 +1,65 @@
|
|||||||
<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>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } 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,9 +67,18 @@ 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
|
||||||
|
watch(activeSchemaId, (newId) => {
|
||||||
|
currentConfig.value = getDefaultValues(schemas[newId])
|
||||||
|
// 移动端切换组件后自动切到表单 tab
|
||||||
|
activeTab.value = 'form'
|
||||||
|
})
|
||||||
|
|
||||||
const generatedCode = computed(() => {
|
const generatedCode = computed(() => {
|
||||||
const gen = generators[activeSchemaId.value]
|
const gen = generators[activeSchemaId.value]
|
||||||
if (!gen) return ''
|
if (!gen) return ''
|
||||||
@@ -54,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;
|
||||||
@@ -75,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;
|
||||||
@@ -106,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('')
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.config.errorHandler = (err, instance, info) => {
|
||||||
|
console.error('Vue Error:', err, info)
|
||||||
|
}
|
||||||
|
|
||||||
app.use(ElementPlus, { size: 'default' })
|
app.use(ElementPlus, { size: 'default' })
|
||||||
|
|
||||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
|||||||
+1
-1
@@ -174,7 +174,7 @@ export function generateBindConf(config) {
|
|||||||
lines.push(` recursion ${config.recursion ? 'yes' : 'no'};`)
|
lines.push(` recursion ${config.recursion ? 'yes' : 'no'};`)
|
||||||
lines.push(``)
|
lines.push(``)
|
||||||
lines.push(` forwarders {`)
|
lines.push(` forwarders {`)
|
||||||
const forwarders = config.forwarders.split(';').map((s) => s.trim()).filter(Boolean)
|
const forwarders = (config.forwarders || '').split(';').map((s) => s.trim()).filter(Boolean)
|
||||||
forwarders.forEach((f) => {
|
forwarders.forEach((f) => {
|
||||||
lines.push(` ${f};`)
|
lines.push(` ${f};`)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user