mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-11 03:37:57 +00:00
195 lines
7.6 KiB
TypeScript
195 lines
7.6 KiB
TypeScript
import { Suspense } from 'react'
|
|
import { Outlet, Link, useRouterState } from '@tanstack/react-router'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useAuth } from '@/features/auth/use-auth'
|
|
import { LanguageSwitcher } from '@/shared/components/language-switcher'
|
|
import { UserMenu } from '@/shared/components/user-menu'
|
|
|
|
export function Layout() {
|
|
const { t } = useTranslation()
|
|
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
|
const { user, isLoading } = useAuth()
|
|
|
|
if (pathname === '/registry/skill') {
|
|
return (
|
|
<Suspense fallback={null}>
|
|
<Outlet />
|
|
</Suspense>
|
|
)
|
|
}
|
|
|
|
const navItems: Array<{
|
|
label: string
|
|
to: string
|
|
exact?: boolean
|
|
auth?: boolean
|
|
}> = [
|
|
{ label: t('nav.landing'), to: '/', exact: true },
|
|
{ label: t('nav.publish'), to: '/dashboard/publish', auth: true },
|
|
{ label: t('nav.search'), to: '/search' },
|
|
{ label: t('nav.dashboard'), to: '/dashboard', auth: true },
|
|
{ label: t('nav.mySkills'), to: '/dashboard/skills', auth: true },
|
|
]
|
|
|
|
const isActive = (to: string, exact?: boolean) => {
|
|
if (exact) return pathname === to
|
|
// 精确匹配,避免父路径也被高亮
|
|
return pathname === to
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen flex flex-col relative overflow-x-hidden" style={{ background: 'var(--bg-page, hsl(var(--background)))' }}>
|
|
{/* Decorative gradient orb */}
|
|
<div
|
|
className="absolute top-0 right-0 w-[600px] h-[500px] rounded-full opacity-90 pointer-events-none z-0"
|
|
style={{
|
|
background: 'radial-gradient(ellipse at 70% 20%, rgba(184,94,255,0.25) 0%, rgba(106,109,255,0.15) 40%, transparent 70%)',
|
|
filter: 'blur(60px)',
|
|
}}
|
|
/>
|
|
|
|
{/* Header */}
|
|
<header className="relative z-50 flex items-center justify-between px-6 py-4 md:px-12 bg-white border-b" style={{ borderColor: 'hsl(var(--border))' }}>
|
|
<Link to="/" className="text-xl font-semibold tracking-tight text-brand-gradient">
|
|
SkillHub
|
|
</Link>
|
|
|
|
<nav className="hidden md:flex items-center gap-8 text-[15px] font-normal" style={{ color: 'hsl(var(--text-secondary))' }}>
|
|
{navItems.map((item) => {
|
|
if (item.auth && !user) return null
|
|
const active = isActive(item.to, item.exact)
|
|
|
|
return (
|
|
<Link
|
|
key={item.to}
|
|
to={item.to}
|
|
className={
|
|
active
|
|
? 'px-4 py-1.5 rounded-full bg-brand-gradient text-white shadow-sm'
|
|
: 'hover:opacity-80 transition-opacity'
|
|
}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
)
|
|
})}
|
|
</nav>
|
|
|
|
<div className="flex items-center gap-6 text-[15px] font-normal" style={{ color: 'hsl(var(--text-secondary))' }}>
|
|
<LanguageSwitcher />
|
|
{isLoading ? null : user ? (
|
|
<UserMenu user={user} />
|
|
) : (
|
|
<Link
|
|
to="/login"
|
|
search={{ returnTo: '' }}
|
|
className="hover:opacity-80 transition-opacity"
|
|
>
|
|
{t('nav.login')}
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main content */}
|
|
<main className={`flex-1 relative z-10${pathname === '/' ? '' : ' px-6 md:px-12 py-10'}`}>
|
|
<Suspense
|
|
fallback={
|
|
<div className="space-y-4 animate-fade-up">
|
|
<div className="h-10 w-48 animate-shimmer rounded-lg" />
|
|
<div className="h-5 w-72 animate-shimmer rounded-md" />
|
|
<div className="h-64 animate-shimmer rounded-xl" />
|
|
</div>
|
|
}
|
|
>
|
|
<Outlet />
|
|
</Suspense>
|
|
</main>
|
|
|
|
{/* Footer */}
|
|
<footer className="relative z-10 border-t rounded-t-2xl mt-auto" style={{ background: '#F1F5F9', borderColor: 'hsl(var(--border))' }}>
|
|
<div className="max-w-6xl mx-auto px-6 md:px-12 py-10">
|
|
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-10 md:gap-12">
|
|
<div className="flex-shrink-0">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<div className="w-9 h-9 rounded-lg flex items-center justify-center text-white text-sm font-bold shadow-sm bg-brand-gradient">
|
|
S
|
|
</div>
|
|
<span className="text-lg font-bold text-brand-gradient">SkillHub</span>
|
|
</div>
|
|
<p className="text-sm max-w-xs" style={{ color: 'hsl(var(--text-secondary))' }}>
|
|
{t('layout.footerDescription')}
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap gap-12 md:gap-16">
|
|
<div>
|
|
<h4 className="text-sm font-semibold mb-3" style={{ color: 'hsl(var(--foreground))' }}>
|
|
{t('nav.home')}
|
|
</h4>
|
|
<ul className="space-y-2 text-sm">
|
|
<li>
|
|
<Link to="/" className="hover:opacity-80 transition-opacity" style={{ color: 'hsl(var(--text-secondary))' }}>
|
|
{t('nav.home')}
|
|
</Link>
|
|
</li>
|
|
<li>
|
|
<Link
|
|
to="/search"
|
|
search={{ q: '', sort: 'relevance', page: 0, starredOnly: false }}
|
|
className="hover:opacity-80 transition-opacity"
|
|
style={{ color: 'hsl(var(--text-secondary))' }}
|
|
>
|
|
{t('nav.search')}
|
|
</Link>
|
|
</li>
|
|
<li>
|
|
<Link to="/dashboard" className="hover:opacity-80 transition-opacity" style={{ color: 'hsl(var(--text-secondary))' }}>
|
|
{t('nav.dashboard')}
|
|
</Link>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-semibold mb-3" style={{ color: 'hsl(var(--foreground))' }}>
|
|
{t('footer.resources')}
|
|
</h4>
|
|
<ul className="space-y-2 text-sm">
|
|
<li>
|
|
<a href="#" className="hover:opacity-80 transition-opacity" style={{ color: 'hsl(var(--text-secondary))' }}>
|
|
{t('footer.docs')}
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="#" className="hover:opacity-80 transition-opacity" style={{ color: 'hsl(var(--text-secondary))' }}>
|
|
{t('footer.api')}
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="#" className="hover:opacity-80 transition-opacity" style={{ color: 'hsl(var(--text-secondary))' }}>
|
|
{t('footer.community')}
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
className="mt-10 pt-6 border-t flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 text-xs"
|
|
style={{ borderColor: 'hsl(var(--border))', color: 'hsl(var(--muted-foreground))' }}
|
|
>
|
|
<span>{t('footer.copyright')}</span>
|
|
<div className="flex items-center gap-2">
|
|
<Link to="/privacy" className="hover:opacity-80 transition-opacity">
|
|
{t('footer.privacy')}
|
|
</Link>
|
|
<span>|</span>
|
|
<Link to="/terms" className="hover:opacity-80 transition-opacity">
|
|
{t('footer.terms')}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
)
|
|
}
|