Initial commit

This commit is contained in:
liumangmang
2026-05-12 17:51:53 +08:00
commit b564ca4797
55 changed files with 6407 additions and 0 deletions
+287
View File
@@ -0,0 +1,287 @@
<template>
<div class="layout">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-logo">
<span class="logo-icon"></span>
<span class="logo-text">SmartUp</span>
</div>
<nav class="sidebar-nav">
<!-- Fixed nav -->
<div class="nav-group-label">监控</div>
<router-link to="/upstreams" class="nav-item" active-class="active">
<el-icon><Connection /></el-icon>
<span>上游管理</span>
</router-link>
<router-link to="/webhooks" class="nav-item" active-class="active">
<el-icon><Bell /></el-icon>
<span>Webhook 通知</span>
</router-link>
<router-link to="/logs" class="nav-item" active-class="active">
<el-icon><Document /></el-icon>
<span>通知日志</span>
</router-link>
<!-- Divider -->
<div class="nav-divider" />
<!-- Custom pages section -->
<div class="nav-group-label">
自定义页面
<router-link to="/custom-pages" class="nav-manage-link" title="管理自定义页面">
<el-icon :size="12"><Setting /></el-icon>
</router-link>
</div>
<template v-if="customPages.length > 0">
<router-link
v-for="page in customPages"
:key="page.id"
:to="`/page/${page.id}`"
class="nav-item"
active-class="active"
>
<el-icon><component :is="iconMap[page.icon] || LinkIcon" /></el-icon>
<span class="nav-custom-label">{{ page.name }}</span>
</router-link>
</template>
<div v-else class="nav-empty-pages">
<router-link to="/custom-pages" class="add-page-link">
<el-icon><Plus /></el-icon> 添加页面
</router-link>
</div>
</nav>
</aside>
<!-- Main -->
<div class="main-wrap">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-title">{{ pageTitle }}</div>
<div class="topbar-right">
<span class="user-email">
<el-icon><User /></el-icon>
{{ auth.email }}
</span>
<el-button size="small" text @click="handleLogout" style="color:var(--text-secondary)">
<el-icon><SwitchButton /></el-icon> 退出
</el-button>
</div>
</header>
<!-- Page content -->
<main class="page-content">
<router-view />
</main>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, markRaw } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import {
Link as LinkIcon, Plus, Setting,
Monitor, SetUp, Reading, Cpu, DataLine,
Grid, Ticket, Wallet, Key, Tools, Star, House,
} from '@element-plus/icons-vue'
import { customPagesApi, type CustomPageData } from '@/api'
const router = useRouter()
const route = useRoute()
const auth = useAuthStore()
// ---- icon map (must match CustomPages.vue) ----
const iconMap: Record<string, any> = {
Link: markRaw(LinkIcon), Monitor: markRaw(Monitor), SetUp: markRaw(SetUp),
Reading: markRaw(Reading), Cpu: markRaw(Cpu), DataLine: markRaw(DataLine),
Grid: markRaw(Grid), Ticket: markRaw(Ticket), Wallet: markRaw(Wallet),
Key: markRaw(Key), Tools: markRaw(Tools), Star: markRaw(Star), House: markRaw(House),
}
// ---- custom pages nav ----
const customPages = ref<CustomPageData[]>([])
async function loadCustomPages() {
try {
const res = await customPagesApi.list()
customPages.value = res.data.filter(p => p.enabled)
} catch { /* sidebar load failure is non-critical */ }
}
// Refresh when management page emits update event
function onPagesUpdated() { loadCustomPages() }
// ---- page title ----
const staticTitles: Record<string, string> = {
'/upstreams': '上游管理',
'/webhooks': 'Webhook 通知',
'/logs': '通知日志',
'/custom-pages': '自定义页面',
}
const pageTitle = computed(() => {
if (route.path.startsWith('/page/')) {
const id = Number(route.params.id)
const found = customPages.value.find(p => p.id === id)
return found ? found.name : '嵌入页面'
}
return staticTitles[route.path] || 'SmartUp'
})
function handleLogout() {
auth.clear()
router.push('/login')
}
onMounted(() => {
loadCustomPages()
window.addEventListener('custom-pages-updated', onPagesUpdated)
})
onUnmounted(() => {
window.removeEventListener('custom-pages-updated', onPagesUpdated)
})
</script>
<style scoped>
.layout {
display: flex;
height: 100vh;
overflow: hidden;
}
.sidebar {
width: var(--sidebar-width);
background: var(--bg-surface);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow-y: auto;
overflow-x: hidden;
}
.sidebar-logo {
height: var(--topbar-height);
display: flex;
align-items: center;
gap: 10px;
padding: 0 20px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
position: sticky;
top: 0;
background: var(--bg-surface);
z-index: 1;
}
.logo-icon { font-size: 22px; }
.logo-text {
font-size: 18px;
font-weight: 700;
background: linear-gradient(135deg, #6366f1, #818cf8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.sidebar-nav {
padding: 12px 8px;
display: flex;
flex-direction: column;
gap: 2px;
}
.nav-group-label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 8px 14px 4px;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-manage-link {
color: var(--text-muted);
text-decoration: none;
display: flex;
align-items: center;
padding: 2px;
border-radius: 4px;
transition: color 0.15s, background 0.15s;
}
.nav-manage-link:hover { color: var(--color-primary); background: var(--bg-elevated); }
.nav-divider {
height: 1px;
background: var(--border-color);
margin: 8px 10px;
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 14px;
border-radius: 8px;
color: var(--text-secondary);
text-decoration: none;
font-size: 13.5px;
font-weight: 500;
transition: all 0.15s ease;
}
.nav-item:hover { background: var(--bg-elevated); color: var(--text-primary); }
.nav-item.active { background: rgba(99,102,241,0.15); color: var(--color-primary); }
.nav-custom-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nav-empty-pages {
padding: 4px 8px;
}
.add-page-link {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border-radius: 8px;
color: var(--text-muted);
text-decoration: none;
font-size: 13px;
border: 1px dashed var(--border-color);
transition: all 0.15s;
}
.add-page-link:hover { border-color: var(--color-primary); color: var(--color-primary); }
/* ---- Main wrap ---- */
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.topbar {
height: var(--topbar-height);
background: var(--bg-surface);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
flex-shrink: 0;
}
.topbar-title { font-size: 15px; font-weight: 600; color: var(--text-primary); }
.topbar-right { display: flex; align-items: center; gap: 12px; }
.user-email { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-secondary); }
.page-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
</style>