Spaces:
Sleeping
Sleeping
| "use client"; | |
| import Link from "next/link"; | |
| import { useAuthStore, useConfigStore } from "@/lib/store"; | |
| import { useRouter } from "next/navigation"; | |
| import { useEffect, useState } from "react"; | |
| import { api } from "@/lib/api"; | |
| export default function ClientLayout({ children }: { children: React.ReactNode }) { | |
| const { user, logout } = useAuthStore(); | |
| const { uiConfig, setUiConfig } = useConfigStore(); | |
| const router = useRouter(); | |
| const [isInitializing, setIsInitializing] = useState(true); | |
| const [dropdownOpen, setDropdownOpen] = useState(false); | |
| const { setAuth, token } = useAuthStore(); | |
| useEffect(() => { | |
| let isMounted = true; | |
| const initApp = async () => { | |
| try { | |
| const configPromise = api.get('/api/config/ui').catch(e => { | |
| console.error('Failed to fetch UI config:', e); | |
| return null; | |
| }); | |
| const authPromise = (async () => { | |
| const storedToken = localStorage.getItem('token'); | |
| if (storedToken && !user) { | |
| try { | |
| const res = await api.get('/api/auth/profile'); | |
| if (res.success && isMounted) { | |
| setAuth(res.data, storedToken); | |
| } | |
| } catch (err) { | |
| console.error('Failed to restore auth:', err); | |
| localStorage.removeItem('token'); | |
| } | |
| } | |
| })(); | |
| const [configRes] = await Promise.all([configPromise, authPromise]); | |
| if (isMounted && configRes && configRes.success && configRes.data) { | |
| setUiConfig(configRes.data); | |
| } | |
| } finally { | |
| if (isMounted) { | |
| setIsInitializing(false); | |
| } | |
| } | |
| }; | |
| if (!uiConfig?.siteName || !user) { | |
| initApp(); | |
| } else { | |
| setIsInitializing(false); | |
| } | |
| return () => { | |
| isMounted = false; | |
| }; | |
| }, [setUiConfig, setAuth, uiConfig?.siteName, user]); | |
| const handleLogout = () => { | |
| logout(); | |
| router.push('/'); | |
| }; | |
| const handleUpgradeVip = async () => { | |
| try { | |
| const res = await api.post('/api/orders/create', { isVip: true }); | |
| if (res.success) { | |
| router.push(`/payment/${res.data.orderId}`); | |
| } | |
| } catch (err) { | |
| console.error(err); | |
| alert('创建会员订单失败,请稍后重试'); | |
| } | |
| }; | |
| const getAvatarColor = (name: string) => { | |
| let hash = 0; | |
| for (let i = 0; i < name.length; i++) { | |
| hash = name.charCodeAt(i) + ((hash << 5) - hash); | |
| } | |
| const hue = Math.abs(hash % 360); | |
| return `hsl(${hue}, 70%, 50%)`; | |
| }; | |
| if (isInitializing) { | |
| return ( | |
| <div className="min-h-screen flex items-center justify-center bg-gray-50"> | |
| <div className="flex flex-col items-center gap-3"> | |
| <div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div> | |
| <p className="text-gray-500 font-medium">应用加载中...</p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| const siteName = uiConfig?.siteName || '极简AI'; | |
| const footerText = uiConfig?.footerText || `© ${new Date().getFullYear()} 极简AI. 保留所有权利。`; | |
| const navLinks = uiConfig?.navLinks || []; | |
| return ( | |
| <> | |
| <header className="bg-white border-b sticky top-0 z-50"> | |
| <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between"> | |
| <Link href="/" className="text-xl font-bold text-blue-600"> | |
| {uiConfig?.logo ? ( | |
| // eslint-disable-next-line @next/next/no-img-element | |
| <img src={uiConfig.logo} alt={siteName} className="h-8 object-contain" /> | |
| ) : ( | |
| siteName | |
| )} | |
| </Link> | |
| <nav className="flex items-center gap-6"> | |
| <Link href="/" className="text-gray-600 hover:text-blue-600 font-medium">首页</Link> | |
| {navLinks.map((link, idx) => ( | |
| <Link | |
| key={idx} | |
| href={`/?category=${link.value}`} | |
| className="text-gray-600 hover:text-blue-600 font-medium" | |
| > | |
| {link.label} | |
| </Link> | |
| ))} | |
| {user ? ( | |
| <> | |
| {user.role !== 'admin' && ( | |
| <> | |
| <Link href="/user/stars" className="text-gray-600 hover:text-blue-600 font-medium">我的收藏</Link> | |
| <Link href="/user/courses" className="text-gray-600 hover:text-blue-600 font-medium">我的学习</Link> | |
| </> | |
| )} | |
| {user.role === 'admin' && ( | |
| <Link href="/admin" className="text-gray-600 hover:text-blue-600 font-medium">管理后台</Link> | |
| )} | |
| <div className="relative border-l pl-6 ml-2"> | |
| <div | |
| className="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold cursor-pointer hover:opacity-90 transition-opacity select-none relative" | |
| style={{ backgroundColor: getAvatarColor(user.nickname) }} | |
| onClick={() => setDropdownOpen(!dropdownOpen)} | |
| > | |
| {user.nickname.charAt(0).toUpperCase()} | |
| {user.isVip && ( | |
| <div className="absolute -bottom-1 -right-1 bg-yellow-400 text-xs text-white px-1.5 rounded-full border-2 border-white shadow-sm font-bold scale-75"> | |
| VIP | |
| </div> | |
| )} | |
| </div> | |
| {dropdownOpen && ( | |
| <> | |
| <div | |
| className="fixed inset-0 z-40" | |
| onClick={() => setDropdownOpen(false)} | |
| ></div> | |
| <div className="absolute right-0 mt-3 w-48 bg-white rounded-xl shadow-lg border border-gray-100 py-2 z-50 overflow-hidden"> | |
| <div className="px-4 py-2 border-b border-gray-50 mb-1"> | |
| <div className="text-sm font-medium text-gray-900 truncate">{user.nickname}</div> | |
| <div className="text-xs text-gray-500 truncate">{user.email}</div> | |
| </div> | |
| {!user.isVip && ( | |
| <button | |
| onClick={() => { | |
| setDropdownOpen(false); | |
| handleUpgradeVip(); | |
| }} | |
| className="w-full text-left px-4 py-2.5 text-sm text-yellow-600 hover:bg-yellow-50 font-medium flex items-center transition-colors" | |
| > | |
| <span className="mr-2">👑</span> | |
| 注册会员 (¥{uiConfig?.memberFee ?? 99}) | |
| </button> | |
| )} | |
| <button | |
| onClick={() => { | |
| setDropdownOpen(false); | |
| handleLogout(); | |
| }} | |
| className="w-full text-left px-4 py-2.5 text-sm text-gray-600 hover:bg-gray-50 transition-colors" | |
| > | |
| 退出登录 | |
| </button> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| </> | |
| ) : ( | |
| <Link href="/login" className="px-5 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm"> | |
| 登录 / 注册 | |
| </Link> | |
| )} | |
| </nav> | |
| </div> | |
| </header> | |
| <main className="flex-1 max-w-7xl mx-auto w-full px-4 py-8"> | |
| {children} | |
| </main> | |
| <footer className="bg-white border-t py-8 mt-auto"> | |
| <div className="max-w-7xl mx-auto px-4 text-center text-gray-500 text-sm"> | |
| {footerText} | |
| </div> | |
| </footer> | |
| </> | |
| ); | |
| } | |