Initial commit: 网络验证平台
This commit is contained in:
@@ -0,0 +1,643 @@
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ThemePopover from '@/components/custom-theme/theme-popover.vue'
|
||||
import LanguageChange from '@/components/language-change.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const mode = useColorMode()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const isLoggedIn = ref(false)
|
||||
const currentUser = ref<any>(null)
|
||||
const mobileMenuOpen = ref(false)
|
||||
|
||||
function checkLoginStatus() {
|
||||
isLoggedIn.value = !!localStorage.getItem('token')
|
||||
}
|
||||
|
||||
const siteSettings = ref({
|
||||
siteName: window.__SITE_CONFIG__?.siteName || '',
|
||||
logoLight: window.__SITE_CONFIG__?.logoLight || '',
|
||||
logoDark: window.__SITE_CONFIG__?.logoDark || '',
|
||||
favicon: window.__SITE_CONFIG__?.favicon || '',
|
||||
})
|
||||
|
||||
const siteName = computed(() => siteSettings.value.siteName)
|
||||
|
||||
const userAvatar = computed(() => {
|
||||
const avatar = currentUser.value?.avatar
|
||||
if (!avatar)
|
||||
return ''
|
||||
if (avatar.startsWith('http'))
|
||||
return avatar
|
||||
return `${import.meta.env.VITE_API_BASE || 'http://localhost:8080'}${avatar}`
|
||||
})
|
||||
|
||||
function getAssetUrl(path: string) {
|
||||
if (!path)
|
||||
return ''
|
||||
if (path.startsWith('http'))
|
||||
return path
|
||||
return `${import.meta.env.VITE_API_BASE || 'http://localhost:8080'}${path}`
|
||||
}
|
||||
|
||||
const logoSrc = computed(() => {
|
||||
if (mode.value === 'dark') {
|
||||
if (siteSettings.value.logoDark) {
|
||||
return getAssetUrl(siteSettings.value.logoDark)
|
||||
}
|
||||
return '/logo.svg'
|
||||
}
|
||||
else {
|
||||
if (siteSettings.value.logoLight) {
|
||||
return getAssetUrl(siteSettings.value.logoLight)
|
||||
}
|
||||
return '/logo-black.svg'
|
||||
}
|
||||
})
|
||||
|
||||
const developerMenus = computed(() => ({
|
||||
main: {
|
||||
title: t('nav.mainFeatures'),
|
||||
items: [
|
||||
{ title: t('nav.console'), url: '/developer', icon: 'lucide:gauge' },
|
||||
{ title: t('nav.applications'), url: '/developer/applications', icon: 'lucide:boxes' },
|
||||
{ title: t('nav.cardTypes'), url: '/developer/card-types', icon: 'lucide:hash' },
|
||||
{ title: t('nav.cards'), url: '/developer/cards', icon: 'lucide:key' },
|
||||
{ title: t('nav.announcements'), url: '/developer/announcements', icon: 'lucide:megaphone' },
|
||||
{ title: t('nav.versions'), url: '/developer/versions', icon: 'lucide:git-branch' },
|
||||
{ title: t('nav.users'), url: '/developer/users', icon: 'lucide:users' },
|
||||
{ title: t('nav.devices'), url: '/developer/devices', icon: 'lucide:monitor' },
|
||||
{ title: t('nav.sessions'), url: '/developer/sessions', icon: 'lucide:wifi' },
|
||||
{ title: t('nav.agentApps'), url: '/developer/agent-apps', icon: 'lucide:share-2' },
|
||||
],
|
||||
},
|
||||
business: {
|
||||
title: t('nav.businessManagement'),
|
||||
items: [
|
||||
{ title: t('nav.finance'), url: '/developer/finance', icon: 'lucide:dollar-sign' },
|
||||
{ title: t('nav.logs'), url: '/developer/logs', icon: 'lucide:scroll-text' },
|
||||
{ title: t('nav.tickets'), url: '/developer/tickets', icon: 'lucide:message-square' },
|
||||
],
|
||||
},
|
||||
advanced: {
|
||||
title: t('nav.advancedFeatures'),
|
||||
items: [
|
||||
{ title: t('nav.cloudConstants'), url: '/developer/cloud-constants', icon: 'lucide:file-lock' },
|
||||
{ title: t('nav.cloudVariables'), url: '/developer/cloud-variables', icon: 'lucide:variable' },
|
||||
{ title: t('nav.cloudFunction'), url: '/developer/cloud-function', icon: 'lucide:code' },
|
||||
{ title: t('nav.riskControl'), url: '/developer/risk-control', icon: 'lucide:shield' },
|
||||
{ title: t('nav.extension'), url: '/developer/extension', icon: 'lucide:plug' },
|
||||
],
|
||||
},
|
||||
}))
|
||||
|
||||
const adminMenus = computed(() => ({
|
||||
main: {
|
||||
title: t('nav.mainFeatures'),
|
||||
items: [
|
||||
{ title: t('nav.console'), url: '/admin', icon: 'lucide:gauge' },
|
||||
{ title: t('nav.users'), url: '/admin/users', icon: 'lucide:users' },
|
||||
{ title: t('nav.orders'), url: '/admin/orders', icon: 'lucide:dollar-sign' },
|
||||
{ title: t('nav.statistics'), url: '/admin/statistics', icon: 'lucide:bar-chart-3' },
|
||||
],
|
||||
},
|
||||
content: {
|
||||
title: t('nav.contentManagement'),
|
||||
items: [
|
||||
{ title: t('nav.docCategories'), url: '/admin/doc-categories', icon: 'lucide:folder-open' },
|
||||
{ title: t('nav.docList'), url: '/admin/docs', icon: 'lucide:file-text' },
|
||||
{ title: t('nav.packages'), url: '/admin/packages', icon: 'lucide:package' },
|
||||
],
|
||||
},
|
||||
system: {
|
||||
title: t('nav.systemManagement'),
|
||||
items: [
|
||||
{ title: t('nav.tickets'), url: '/admin/tickets', icon: 'lucide:message-square' },
|
||||
{ title: t('nav.logs'), url: '/admin/logs', icon: 'lucide:scroll-text' },
|
||||
{ title: t('nav.basicSettings'), url: '/admin/settings/basic', icon: 'lucide:settings' },
|
||||
{ title: t('nav.emailSettings'), url: '/admin/settings/email', icon: 'lucide:mail' },
|
||||
{ title: t('nav.smsSettings'), url: '/admin/settings/sms', icon: 'lucide:message-square' },
|
||||
{ title: t('nav.paymentSettings'), url: '/admin/settings/payment', icon: 'lucide:credit-card' },
|
||||
],
|
||||
},
|
||||
}))
|
||||
|
||||
const publicMenus = computed(() => [
|
||||
{ title: t('nav.home'), url: '/', icon: 'lucide:home' },
|
||||
{ title: t('nav.docs'), url: '/docs', icon: 'lucide:book-open' },
|
||||
{ title: t('nav.pricing'), url: '/pricing', icon: 'lucide:credit-card' },
|
||||
])
|
||||
|
||||
function isMenuActive(items: { url: string }[]) {
|
||||
return items.some(item => route.path === item.url)
|
||||
}
|
||||
|
||||
function isActive(url: string) {
|
||||
return route.path === url
|
||||
}
|
||||
|
||||
function navigateTo(url: string) {
|
||||
router.push(url)
|
||||
mobileMenuOpen.value = false
|
||||
}
|
||||
|
||||
function updateFavicon() {
|
||||
if (siteSettings.value.favicon) {
|
||||
const link: HTMLLinkElement = document.querySelector('link[rel*=\'icon\']') || document.createElement('link')
|
||||
link.type = 'image/x-icon'
|
||||
link.rel = 'shortcut icon'
|
||||
link.href = getAssetUrl(siteSettings.value.favicon)
|
||||
document.getElementsByTagName('head')[0].appendChild(link)
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
currentUser.value = null
|
||||
isLoggedIn.value = false
|
||||
mobileMenuOpen.value = false
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkLoginStatus()
|
||||
const userStr = localStorage.getItem('user')
|
||||
if (userStr) {
|
||||
try {
|
||||
currentUser.value = JSON.parse(userStr)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to parse user data:', error)
|
||||
}
|
||||
}
|
||||
if (siteSettings.value.favicon) {
|
||||
updateFavicon()
|
||||
}
|
||||
if (siteSettings.value.siteName) {
|
||||
document.title = siteSettings.value.siteName
|
||||
}
|
||||
})
|
||||
|
||||
watch(mode, () => {
|
||||
updateFavicon()
|
||||
})
|
||||
|
||||
watch(route, () => {
|
||||
mobileMenuOpen.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div class="mx-auto max-w-6xl flex h-14 items-center px-4 sm:px-6 lg:px-8">
|
||||
<div class="mr-4 hidden md:flex items-center">
|
||||
<router-link to="/" class="mr-4 flex items-center space-x-2">
|
||||
<UiAvatar class="h-7 w-7">
|
||||
<UiAvatarImage :src="logoSrc" alt="Logo" />
|
||||
</UiAvatar>
|
||||
<span class="hidden font-bold sm:inline-block">{{ siteName }}</span>
|
||||
</router-link>
|
||||
<nav class="flex items-center space-x-1 text-sm font-medium">
|
||||
<router-link
|
||||
to="/"
|
||||
class="transition-colors hover:text-foreground/80 px-3 py-2 rounded-lg"
|
||||
:class="route.name === 'Home' ? 'text-foreground bg-accent' : 'text-foreground/60'"
|
||||
>
|
||||
{{ t('nav.home') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/docs"
|
||||
class="transition-colors hover:text-foreground/80 px-3 py-2 rounded-lg"
|
||||
:class="route.name === 'Docs' ? 'text-foreground bg-accent' : 'text-foreground/60'"
|
||||
>
|
||||
{{ t('nav.docs') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/pricing"
|
||||
class="transition-colors hover:text-foreground/80 px-3 py-2 rounded-lg"
|
||||
:class="route.name === 'Pricing' ? 'text-foreground bg-accent' : 'text-foreground/60'"
|
||||
>
|
||||
{{ t('nav.pricing') }}
|
||||
</router-link>
|
||||
|
||||
<template v-if="isLoggedIn && currentUser?.role === 'developer'">
|
||||
<div class="w-px h-4 bg-border mx-1" />
|
||||
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<button
|
||||
class="transition-colors hover:text-foreground/80 px-3 py-2 rounded-lg flex items-center gap-1"
|
||||
:class="isMenuActive(developerMenus.main.items) ? 'text-foreground bg-accent' : 'text-foreground/60'"
|
||||
>
|
||||
{{ developerMenus.main.title }}
|
||||
<Icon icon="lucide:chevron-down" class="h-4 w-4" />
|
||||
</button>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="start" class="w-40">
|
||||
<UiDropdownMenuItem
|
||||
v-for="item in developerMenus.main.items"
|
||||
:key="item.url"
|
||||
:class="route.path === item.url ? 'bg-accent' : ''"
|
||||
@click="router.push(item.url)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="mr-2 h-4 w-4" />
|
||||
{{ item.title }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<button
|
||||
class="transition-colors hover:text-foreground/80 px-3 py-2 rounded-lg flex items-center gap-1"
|
||||
:class="isMenuActive(developerMenus.business.items) ? 'text-foreground bg-accent' : 'text-foreground/60'"
|
||||
>
|
||||
{{ developerMenus.business.title }}
|
||||
<Icon icon="lucide:chevron-down" class="h-4 w-4" />
|
||||
</button>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="start" class="w-40">
|
||||
<UiDropdownMenuItem
|
||||
v-for="item in developerMenus.business.items"
|
||||
:key="item.url"
|
||||
:class="route.path === item.url ? 'bg-accent' : ''"
|
||||
@click="router.push(item.url)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="mr-2 h-4 w-4" />
|
||||
{{ item.title }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<button
|
||||
class="transition-colors hover:text-foreground/80 px-3 py-2 rounded-lg flex items-center gap-1"
|
||||
:class="isMenuActive(developerMenus.advanced.items) ? 'text-foreground bg-accent' : 'text-foreground/60'"
|
||||
>
|
||||
{{ developerMenus.advanced.title }}
|
||||
<Icon icon="lucide:chevron-down" class="h-4 w-4" />
|
||||
</button>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="start" class="w-40">
|
||||
<UiDropdownMenuItem
|
||||
v-for="item in developerMenus.advanced.items"
|
||||
:key="item.url"
|
||||
:class="route.path === item.url ? 'bg-accent' : ''"
|
||||
@click="router.push(item.url)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="mr-2 h-4 w-4" />
|
||||
{{ item.title }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</template>
|
||||
|
||||
<template v-if="isLoggedIn && currentUser?.role === 'admin'">
|
||||
<div class="w-px h-4 bg-border mx-1" />
|
||||
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<button
|
||||
class="transition-colors hover:text-foreground/80 px-3 py-2 rounded-lg flex items-center gap-1"
|
||||
:class="isMenuActive(adminMenus.main.items) ? 'text-foreground bg-accent' : 'text-foreground/60'"
|
||||
>
|
||||
{{ adminMenus.main.title }}
|
||||
<Icon icon="lucide:chevron-down" class="h-4 w-4" />
|
||||
</button>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="start" class="w-40">
|
||||
<UiDropdownMenuItem
|
||||
v-for="item in adminMenus.main.items"
|
||||
:key="item.url"
|
||||
:class="route.path === item.url ? 'bg-accent' : ''"
|
||||
@click="router.push(item.url)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="mr-2 h-4 w-4" />
|
||||
{{ item.title }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<button
|
||||
class="transition-colors hover:text-foreground/80 px-3 py-2 rounded-lg flex items-center gap-1"
|
||||
:class="isMenuActive(adminMenus.content.items) ? 'text-foreground bg-accent' : 'text-foreground/60'"
|
||||
>
|
||||
{{ adminMenus.content.title }}
|
||||
<Icon icon="lucide:chevron-down" class="h-4 w-4" />
|
||||
</button>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="start" class="w-40">
|
||||
<UiDropdownMenuItem
|
||||
v-for="item in adminMenus.content.items"
|
||||
:key="item.url"
|
||||
:class="route.path === item.url ? 'bg-accent' : ''"
|
||||
@click="router.push(item.url)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="mr-2 h-4 w-4" />
|
||||
{{ item.title }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<button
|
||||
class="transition-colors hover:text-foreground/80 px-3 py-2 rounded-lg flex items-center gap-1"
|
||||
:class="isMenuActive(adminMenus.system.items) ? 'text-foreground bg-accent' : 'text-foreground/60'"
|
||||
>
|
||||
{{ adminMenus.system.title }}
|
||||
<Icon icon="lucide:chevron-down" class="h-4 w-4" />
|
||||
</button>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="start" class="w-40">
|
||||
<UiDropdownMenuItem
|
||||
v-for="item in adminMenus.system.items"
|
||||
:key="item.url"
|
||||
:class="route.path === item.url ? 'bg-accent' : ''"
|
||||
@click="router.push(item.url)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="mr-2 h-4 w-4" />
|
||||
{{ item.title }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</template>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="md:hidden mr-2 flex items-center justify-center size-9 rounded-md hover:bg-accent"
|
||||
@click="mobileMenuOpen = true"
|
||||
>
|
||||
<Icon icon="lucide:menu" class="size-5" />
|
||||
</button>
|
||||
|
||||
<router-link to="/" class="mr-6 flex items-center space-x-2 md:hidden">
|
||||
<UiAvatar class="h-7 w-7">
|
||||
<UiAvatarImage :src="logoSrc" alt="Logo" />
|
||||
</UiAvatar>
|
||||
<span class="font-bold">{{ siteName }}</span>
|
||||
</router-link>
|
||||
|
||||
<div class="flex flex-1 items-center justify-end space-x-2">
|
||||
<LanguageChange />
|
||||
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-9 w-9"
|
||||
@click="mode = mode === 'dark' ? 'light' : 'dark'"
|
||||
>
|
||||
<Icon
|
||||
icon="lucide:sun"
|
||||
class="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
|
||||
/>
|
||||
<Icon
|
||||
icon="lucide:moon"
|
||||
class="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
|
||||
/>
|
||||
<span class="sr-only">切换主题</span>
|
||||
</UiButton>
|
||||
|
||||
<ThemePopover />
|
||||
|
||||
<template v-if="isLoggedIn">
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton variant="ghost" class="relative h-9 w-9 rounded-full">
|
||||
<UiAvatar class="h-9 w-9">
|
||||
<UiAvatarImage :src="userAvatar" alt="用户头像" />
|
||||
<UiAvatarFallback>{{ currentUser?.username?.charAt(0).toUpperCase() || 'U' }}</UiAvatarFallback>
|
||||
</UiAvatar>
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="end" class="w-48">
|
||||
<UiDropdownMenuLabel>
|
||||
<div class="flex flex-col space-y-1">
|
||||
<p class="text-sm font-medium leading-none">
|
||||
{{ currentUser?.username || '用户' }}
|
||||
</p>
|
||||
<p class="text-xs leading-none text-muted-foreground">
|
||||
{{ currentUser?.email || '' }}
|
||||
</p>
|
||||
</div>
|
||||
</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem @click="router.push('/profile')">
|
||||
<Icon icon="lucide:user" class="mr-2 h-4 w-4" />
|
||||
<span>{{ t('nav.profile') }}</span>
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem v-if="currentUser?.role === 'developer'" @click="router.push('/developer')">
|
||||
<Icon icon="lucide:layout-dashboard" class="mr-2 h-4 w-4" />
|
||||
<span>{{ t('nav.developerDashboard') }}</span>
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem v-if="currentUser?.role === 'agent'" @click="router.push('/agent/dashboard')">
|
||||
<Icon icon="lucide:layout-dashboard" class="mr-2 h-4 w-4" />
|
||||
<span>{{ t('nav.agentDashboard') }}</span>
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem v-if="currentUser?.role === 'admin'" @click="router.push('/admin')">
|
||||
<Icon icon="lucide:shield" class="mr-2 h-4 w-4" />
|
||||
<span>{{ t('nav.adminDashboard') }}</span>
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem @click="handleLogout">
|
||||
<Icon icon="lucide:log-out" class="mr-2 h-4 w-4" />
|
||||
<span>{{ t('nav.logout') }}</span>
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</template>
|
||||
<template v-else>
|
||||
<UiButton variant="ghost" size="sm" as-child>
|
||||
<router-link to="/login">
|
||||
{{ t('nav.login') }}
|
||||
</router-link>
|
||||
</UiButton>
|
||||
<UiButton size="sm" as-child class="hidden sm:flex">
|
||||
<router-link to="/register">
|
||||
{{ t('nav.register') }}
|
||||
</router-link>
|
||||
</UiButton>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UiSheet v-model:open="mobileMenuOpen">
|
||||
<UiSheetContent side="left" class="w-72 overflow-y-auto">
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center space-x-2">
|
||||
<UiAvatar class="h-7 w-7">
|
||||
<UiAvatarImage :src="logoSrc" alt="Logo" />
|
||||
</UiAvatar>
|
||||
<span class="font-bold">{{ siteName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 space-y-6">
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs font-medium text-muted-foreground px-3 mb-2">
|
||||
{{ t('nav.navigation') }}
|
||||
</p>
|
||||
<button
|
||||
v-for="item in publicMenus"
|
||||
:key="item.url"
|
||||
class="w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg transition-colors"
|
||||
:class="isActive(item.url) ? 'bg-accent text-foreground' : 'text-muted-foreground hover:bg-accent hover:text-foreground'"
|
||||
@click="navigateTo(item.url)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="size-4" />
|
||||
{{ item.title }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-if="isLoggedIn && currentUser?.role === 'developer'">
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs font-medium text-muted-foreground px-3 mb-2">
|
||||
{{ developerMenus.main.title }}
|
||||
</p>
|
||||
<button
|
||||
v-for="item in developerMenus.main.items"
|
||||
:key="item.url"
|
||||
class="w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg transition-colors"
|
||||
:class="isActive(item.url) ? 'bg-accent text-foreground' : 'text-muted-foreground hover:bg-accent hover:text-foreground'"
|
||||
@click="navigateTo(item.url)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="size-4" />
|
||||
{{ item.title }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs font-medium text-muted-foreground px-3 mb-2">
|
||||
{{ developerMenus.business.title }}
|
||||
</p>
|
||||
<button
|
||||
v-for="item in developerMenus.business.items"
|
||||
:key="item.url"
|
||||
class="w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg transition-colors"
|
||||
:class="isActive(item.url) ? 'bg-accent text-foreground' : 'text-muted-foreground hover:bg-accent hover:text-foreground'"
|
||||
@click="navigateTo(item.url)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="size-4" />
|
||||
{{ item.title }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs font-medium text-muted-foreground px-3 mb-2">
|
||||
{{ developerMenus.advanced.title }}
|
||||
</p>
|
||||
<button
|
||||
v-for="item in developerMenus.advanced.items"
|
||||
:key="item.url"
|
||||
class="w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg transition-colors"
|
||||
:class="isActive(item.url) ? 'bg-accent text-foreground' : 'text-muted-foreground hover:bg-accent hover:text-foreground'"
|
||||
@click="navigateTo(item.url)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="size-4" />
|
||||
{{ item.title }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="isLoggedIn && currentUser?.role === 'admin'">
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs font-medium text-muted-foreground px-3 mb-2">
|
||||
{{ adminMenus.main.title }}
|
||||
</p>
|
||||
<button
|
||||
v-for="item in adminMenus.main.items"
|
||||
:key="item.url"
|
||||
class="w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg transition-colors"
|
||||
:class="isActive(item.url) ? 'bg-accent text-foreground' : 'text-muted-foreground hover:bg-accent hover:text-foreground'"
|
||||
@click="navigateTo(item.url)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="size-4" />
|
||||
{{ item.title }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs font-medium text-muted-foreground px-3 mb-2">
|
||||
{{ adminMenus.content.title }}
|
||||
</p>
|
||||
<button
|
||||
v-for="item in adminMenus.content.items"
|
||||
:key="item.url"
|
||||
class="w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg transition-colors"
|
||||
:class="isActive(item.url) ? 'bg-accent text-foreground' : 'text-muted-foreground hover:bg-accent hover:text-foreground'"
|
||||
@click="navigateTo(item.url)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="size-4" />
|
||||
{{ item.title }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs font-medium text-muted-foreground px-3 mb-2">
|
||||
{{ adminMenus.system.title }}
|
||||
</p>
|
||||
<button
|
||||
v-for="item in adminMenus.system.items"
|
||||
:key="item.url"
|
||||
class="w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg transition-colors"
|
||||
:class="isActive(item.url) ? 'bg-accent text-foreground' : 'text-muted-foreground hover:bg-accent hover:text-foreground'"
|
||||
@click="navigateTo(item.url)"
|
||||
>
|
||||
<Icon :icon="item.icon" class="size-4" />
|
||||
{{ item.title }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</nav>
|
||||
|
||||
<div v-if="!isLoggedIn" class="pt-4 border-t mt-4 space-y-2">
|
||||
<UiButton variant="outline" class="w-full" as-child @click="mobileMenuOpen = false">
|
||||
<router-link to="/login">
|
||||
{{ t('nav.login') }}
|
||||
</router-link>
|
||||
</UiButton>
|
||||
<UiButton class="w-full" as-child @click="mobileMenuOpen = false">
|
||||
<router-link to="/register">
|
||||
{{ t('nav.register') }}
|
||||
</router-link>
|
||||
</UiButton>
|
||||
</div>
|
||||
|
||||
<div v-else class="pt-4 border-t mt-4">
|
||||
<div class="flex items-center gap-3 px-3 py-2">
|
||||
<UiAvatar class="h-9 w-9">
|
||||
<UiAvatarImage :src="userAvatar" alt="用户头像" />
|
||||
<UiAvatarFallback>{{ currentUser?.username?.charAt(0).toUpperCase() || 'U' }}</UiAvatarFallback>
|
||||
</UiAvatar>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium truncate">
|
||||
{{ currentUser?.username || '用户' }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground truncate">
|
||||
{{ currentUser?.email || '' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<UiButton variant="outline" class="w-full mt-2" @click="handleLogout">
|
||||
<Icon icon="lucide:log-out" class="mr-2 h-4 w-4" />
|
||||
{{ t('nav.logout') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</UiSheetContent>
|
||||
</UiSheet>
|
||||
</header>
|
||||
</template>
|
||||
Reference in New Issue
Block a user