c45a59c6d1
- 768px 断点: 侧边栏变为抽屉式滑出, 汉堡按钮切换 - 移动端顶栏: 显示当前组件名称 - 移动端 Tab: 配置表单/代码预览一键切换 - 遮罩层: 点击遮罩自动收起侧边栏 - 切换组件后自动收起侧边栏并回到表单 tab - 平板端 (1024px) 表单区域适当缩窄
355 lines
7.3 KiB
Vue
355 lines
7.3 KiB
Vue
<template>
|
|
<div class="sidebar">
|
|
<div class="sidebar-header">
|
|
<div class="logo">
|
|
<el-icon :size="28"><Setting /></el-icon>
|
|
<h1>ConfTemplate</h1>
|
|
</div>
|
|
<p class="subtitle">云原生配置文件生成器</p>
|
|
</div>
|
|
|
|
<div class="search-box">
|
|
<el-input
|
|
v-model="searchQuery"
|
|
placeholder="搜索中间件..."
|
|
prefix-icon="Search"
|
|
clearable
|
|
size="default"
|
|
/>
|
|
</div>
|
|
|
|
<div class="nav-list">
|
|
<template v-for="cat in filteredCategories" :key="cat.name">
|
|
<div
|
|
class="nav-category"
|
|
:class="{ collapsed: !isExpanded(cat.name) }"
|
|
@click="toggle(cat.name)"
|
|
>
|
|
<div class="nav-category-left">
|
|
<el-icon><component :is="cat.icon" /></el-icon>
|
|
<span>{{ cat.name }}</span>
|
|
<span class="nav-category-count">{{ cat.items.length }}</span>
|
|
</div>
|
|
<el-icon class="nav-category-arrow" :class="{ open: isExpanded(cat.name) }">
|
|
<ArrowRight />
|
|
</el-icon>
|
|
</div>
|
|
<transition name="slide">
|
|
<div v-show="isExpanded(cat.name)" class="nav-category-items">
|
|
<div
|
|
v-for="itemId in cat.items"
|
|
:key="itemId"
|
|
class="nav-item"
|
|
:class="{ active: modelValue === itemId }"
|
|
@click="$emit('update:modelValue', itemId); $emit('select')"
|
|
>
|
|
<el-icon><component :is="schemas[itemId].icon" /></el-icon>
|
|
<div class="nav-item-info">
|
|
<span class="nav-item-name">{{ schemas[itemId].name }}</span>
|
|
<span class="nav-item-desc">{{ schemas[itemId].description }}</span>
|
|
</div>
|
|
<el-tag size="small" type="info">{{ schemas[itemId].format }}</el-tag>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</template>
|
|
|
|
<div v-if="filteredCategories.length === 0" class="no-results">
|
|
<el-empty description="未找到匹配的中间件" :image-size="60" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sidebar-footer">
|
|
<el-text size="small" type="info">v1.0.0 · 纯前端生成 · 59 个组件</el-text>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch } from 'vue'
|
|
import { ArrowRight } from '@element-plus/icons-vue'
|
|
import { schemas, categories } from '../schemas'
|
|
|
|
const props = defineProps({
|
|
modelValue: String,
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue', 'select'])
|
|
|
|
const searchQuery = ref('')
|
|
|
|
// Track which categories are expanded
|
|
const expandedSet = ref(new Set())
|
|
|
|
// Initialize: expand all categories, or auto-expand the one containing active item
|
|
function initExpanded() {
|
|
const activeCat = categories.find(cat => cat.items.includes(props.modelValue))
|
|
if (activeCat) {
|
|
expandedSet.value = new Set([activeCat.name])
|
|
} else {
|
|
expandedSet.value = new Set(categories.map(c => c.name))
|
|
}
|
|
}
|
|
initExpanded()
|
|
|
|
// When active item changes, auto-expand its category
|
|
watch(() => props.modelValue, (val) => {
|
|
const cat = categories.find(c => c.items.includes(val))
|
|
if (cat && !expandedSet.value.has(cat.name)) {
|
|
expandedSet.value.add(cat.name)
|
|
}
|
|
})
|
|
|
|
function isExpanded(name) {
|
|
return expandedSet.value.has(name)
|
|
}
|
|
|
|
function toggle(name) {
|
|
const s = new Set(expandedSet.value)
|
|
if (s.has(name)) {
|
|
s.delete(name)
|
|
} else {
|
|
s.add(name)
|
|
}
|
|
expandedSet.value = s
|
|
}
|
|
|
|
const filteredCategories = computed(() => {
|
|
const q = searchQuery.value.toLowerCase().trim()
|
|
if (!q) return categories
|
|
|
|
return categories
|
|
.map((cat) => ({
|
|
...cat,
|
|
items: cat.items.filter((id) => {
|
|
const s = schemas[id]
|
|
return (
|
|
s.name.toLowerCase().includes(q) ||
|
|
s.description.toLowerCase().includes(q) ||
|
|
s.category.toLowerCase().includes(q) ||
|
|
id.toLowerCase().includes(q)
|
|
)
|
|
}),
|
|
}))
|
|
.filter((cat) => cat.items.length > 0)
|
|
})
|
|
|
|
// When searching, auto-expand all matching categories
|
|
watch(searchQuery, (q) => {
|
|
if (q.trim()) {
|
|
expandedSet.value = new Set(filteredCategories.value.map(c => c.name))
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.sidebar {
|
|
width: 280px;
|
|
min-width: 280px;
|
|
height: 100vh;
|
|
background: #1a1a2e;
|
|
color: #e0e0e0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: 20px 16px 12px;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
}
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
color: #fff;
|
|
}
|
|
|
|
.logo h1 {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
margin: 0;
|
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 12px;
|
|
color: #888;
|
|
margin: 6px 0 0;
|
|
}
|
|
|
|
.search-box {
|
|
padding: 12px 16px;
|
|
}
|
|
|
|
.search-box :deep(.el-input__wrapper) {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
box-shadow: none;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.search-box :deep(.el-input__inner) {
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.search-box :deep(.el-input__inner::placeholder) {
|
|
color: #666;
|
|
}
|
|
|
|
.nav-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 4px 8px;
|
|
}
|
|
|
|
.nav-list::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
|
|
.nav-list::-webkit-scrollbar-thumb {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* Category header — clickable */
|
|
.nav-category {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 8px 4px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: #666;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: color 0.2s;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.nav-category:hover {
|
|
color: #999;
|
|
}
|
|
|
|
.nav-category-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.nav-category-count {
|
|
font-size: 10px;
|
|
background: rgba(102, 126, 234, 0.2);
|
|
color: #667eea;
|
|
padding: 1px 6px;
|
|
border-radius: 10px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.nav-category-arrow {
|
|
font-size: 12px;
|
|
transition: transform 0.25s ease;
|
|
color: #555;
|
|
}
|
|
|
|
.nav-category-arrow.open {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
/* Slide transition */
|
|
.nav-category-items {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.slide-enter-active,
|
|
.slide-leave-active {
|
|
transition: all 0.25s ease;
|
|
}
|
|
|
|
.slide-enter-from,
|
|
.slide-leave-to {
|
|
opacity: 0;
|
|
max-height: 0;
|
|
}
|
|
|
|
.slide-enter-to,
|
|
.slide-leave-from {
|
|
opacity: 1;
|
|
max-height: 1000px;
|
|
}
|
|
|
|
/* Nav item */
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 12px;
|
|
margin: 2px 0;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
position: relative;
|
|
}
|
|
|
|
.nav-item:hover {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
}
|
|
|
|
.nav-item.active {
|
|
background: rgba(102, 126, 234, 0.15);
|
|
color: #667eea;
|
|
}
|
|
|
|
.nav-item.active::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 3px;
|
|
height: 60%;
|
|
background: #667eea;
|
|
border-radius: 0 2px 2px 0;
|
|
}
|
|
|
|
.nav-item-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.nav-item-name {
|
|
display: block;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.nav-item.active .nav-item-name {
|
|
color: #667eea;
|
|
}
|
|
|
|
.nav-item-desc {
|
|
display: block;
|
|
font-size: 11px;
|
|
color: #666;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.no-results {
|
|
padding: 40px 0;
|
|
}
|
|
|
|
.sidebar-footer {
|
|
padding: 12px 16px;
|
|
text-align: center;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
}
|
|
</style>
|