612d68f56f
Go + Vue 管理台,SQLite 持久化;支持隧道/域名穿透、域名转发插件链、访客认证、 IP 规则、API Key、Docker 单镜像部署;配置通过 Web 系统设置管理,无需 YAML 文件。 Co-authored-by: Cursor <cursoragent@cursor.com>
144 lines
3.8 KiB
Vue
144 lines
3.8 KiB
Vue
<template>
|
|
<div class="layout">
|
|
<aside class="sidebar custom-scrollbar">
|
|
<div class="sidebar-brand">
|
|
<div class="brand-icon">
|
|
<AppLogo :size="20" />
|
|
</div>
|
|
<span class="brand-name">{{ t('app.name') }}</span>
|
|
</div>
|
|
|
|
<template v-for="group in navGroups" :key="group.labelKey">
|
|
<template v-if="group.visible">
|
|
<div class="nav-group-label">{{ t(group.labelKey) }}</div>
|
|
<router-link
|
|
v-for="item in group.items"
|
|
:key="item.path"
|
|
:to="item.path"
|
|
class="nav-item"
|
|
:class="{ active: route.path === item.path }"
|
|
>
|
|
<el-icon :size="16"><component :is="item.icon" /></el-icon>
|
|
{{ t(item.labelKey) }}
|
|
</router-link>
|
|
</template>
|
|
</template>
|
|
</aside>
|
|
|
|
<div class="main-area">
|
|
<header class="topbar">
|
|
<span class="topbar-title">{{ pageTitle }}</span>
|
|
<div class="topbar-user">
|
|
<el-select
|
|
:model-value="locale"
|
|
class="toolbar-select"
|
|
size="small"
|
|
@change="onLocaleChange"
|
|
>
|
|
<el-option :label="t('common.zh')" value="zh-CN" />
|
|
<el-option :label="t('common.en')" value="en" />
|
|
</el-select>
|
|
<span>{{ user?.username }}</span>
|
|
<el-button text @click="logout">{{ t('common.logout') }}</el-button>
|
|
</div>
|
|
</header>
|
|
<main class="main-content custom-scrollbar">
|
|
<router-view />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import {
|
|
Odometer,
|
|
Link,
|
|
Connection,
|
|
Share,
|
|
Monitor,
|
|
Lock,
|
|
User,
|
|
Setting,
|
|
} from '@element-plus/icons-vue'
|
|
import AppLogo from '@/components/AppLogo.vue'
|
|
import { setLocale } from '@/i18n'
|
|
|
|
const { t, locale } = useI18n()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const topbarKeys = {
|
|
'/dashboard': 'topbar.dashboard',
|
|
'/clients': 'topbar.clients',
|
|
'/tunnels': 'topbar.tunnels',
|
|
'/hosts': 'topbar.hosts',
|
|
'/domain-forwards': 'topbar.domainForwards',
|
|
'/security/ip-rules': 'topbar.ipRules',
|
|
'/security/request-ips': 'topbar.requestIPs',
|
|
'/users': 'topbar.users',
|
|
'/settings': 'topbar.settings',
|
|
}
|
|
|
|
const user = computed(() => {
|
|
try {
|
|
return JSON.parse(localStorage.getItem('user') || '{}')
|
|
} catch {
|
|
return {}
|
|
}
|
|
})
|
|
|
|
const isAdmin = computed(() => user.value?.role === 'admin')
|
|
const pageTitle = computed(() => t(topbarKeys[route.path] || 'topbar.fallback'))
|
|
|
|
const navGroups = computed(() => [
|
|
{
|
|
labelKey: 'nav.groups.overview',
|
|
visible: true,
|
|
items: [{ path: '/dashboard', labelKey: 'nav.dashboard', icon: Odometer }],
|
|
},
|
|
{
|
|
labelKey: 'nav.groups.forward',
|
|
visible: isAdmin.value,
|
|
items: [{ path: '/domain-forwards', labelKey: 'nav.domainForwards', icon: Link }],
|
|
},
|
|
{
|
|
labelKey: 'nav.groups.tunnel',
|
|
visible: true,
|
|
items: [
|
|
{ path: '/clients', labelKey: 'nav.clients', icon: Connection },
|
|
{ path: '/tunnels', labelKey: 'nav.tunnels', icon: Share },
|
|
{ path: '/hosts', labelKey: 'nav.hosts', icon: Monitor },
|
|
],
|
|
},
|
|
{
|
|
labelKey: 'nav.groups.security',
|
|
visible: true,
|
|
items: [
|
|
{ path: '/security/ip-rules', labelKey: 'nav.ipRules', icon: Lock },
|
|
{ path: '/security/request-ips', labelKey: 'nav.requestIPs', icon: Monitor },
|
|
],
|
|
},
|
|
{
|
|
labelKey: 'nav.groups.system',
|
|
visible: isAdmin.value,
|
|
items: [
|
|
{ path: '/users', labelKey: 'nav.users', icon: User },
|
|
{ path: '/settings', labelKey: 'nav.settings', icon: Setting },
|
|
],
|
|
},
|
|
])
|
|
|
|
function onLocaleChange(value) {
|
|
setLocale(value)
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem('token')
|
|
localStorage.removeItem('user')
|
|
router.push('/login')
|
|
}
|
|
</script>
|