Files
jiang13-bbs/frontend/components/Header.tsx
freefire 0c2b0d2d6a 首次提交:姜十三论坛
后端 Go+Gin:认证/CSRF/限流、板块、帖子、评论、点赞、通知、用户资料、置顶推荐;前端 Next.js 16:发帖/编辑/删除、搜索、分页、点赞、通知中心、设置;基础设施 docker-compose 与配置模板;添加 .gitignore 与专有许可证(保留所有权利)
2026-09-12 01:11:05 +08:00

152 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState, useEffect } from "react";
import { Menu, X, Sun, Moon, LogIn, UserPlus, LogOut, PenSquare, Search, Settings } from "lucide-react";
import { apiMe, apiLogout, type User } from "@/lib/api";
import NotificationBell from "./NotificationBell";
export default function Header() {
const router = useRouter();
const [menuOpen, setMenuOpen] = useState(false);
const [theme, setTheme] = useState<"light" | "dark">("light");
const [user, setUser] = useState<User | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
const q = searchQuery.trim();
if (q) {
router.push(`/?q=${encodeURIComponent(q)}`);
}
};
useEffect(() => {
// 读取主题
const savedTheme = document.documentElement.classList.contains("dark") ? "dark" : "light";
setTheme(savedTheme);
// 从后端获取登录态(HttpOnly cookie 自动携带)
apiMe()
.then((res) => setUser(res.user || null))
.catch(() => setUser(null));
}, []);
const toggleTheme = () => {
const newTheme = theme === "dark" ? "light" : "dark";
setTheme(newTheme);
document.documentElement.classList.toggle("dark", newTheme === "dark");
document.cookie = `j13-theme=${newTheme}; path=/; max-age=31536000`;
};
const handleLogout = async () => {
try {
await apiLogout();
} catch {
/* ignore */
}
setUser(null);
setMenuOpen(false);
router.refresh();
};
return (
<header className="sticky top-0 z-50 border-b" style={{ borderColor: "var(--border)", background: "var(--bg)" }}>
<div className="max-w-5xl mx-auto px-4 h-14 flex items-center justify-between">
<Link href="/" className="font-bold text-lg" style={{ color: "var(--primary)" }}>
姜十三论坛
</Link>
{/* 桌面端导航 */}
<nav className="hidden md:flex items-center gap-4">
<form onSubmit={handleSearch} className="relative">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2" style={{ color: "var(--text-secondary)" }} />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索帖子..."
className="pl-9 pr-3 py-1.5 rounded-lg text-sm w-44 focus:outline-none focus:w-56 transition-all"
style={{ background: "var(--bg-secondary)", color: "var(--text)", border: "1px solid var(--border)" }}
/>
</form>
<Link href="/" className="hover:opacity-80" style={{ color: "var(--text)" }}>首页</Link>
{user && (
<Link href="/compose" className="flex items-center gap-1 hover:opacity-80" style={{ color: "var(--text)" }}>
<PenSquare size={16} /> 发帖
</Link>
)}
<button onClick={toggleTheme} className="p-2 rounded-lg hover:opacity-80" style={{ color: "var(--text)" }}>
{theme === "dark" ? <Sun size={18} /> : <Moon size={18} />}
</button>
{user && <NotificationBell />}
{user ? (
<div className="flex items-center gap-3">
<span className="text-sm" style={{ color: "var(--text-secondary)" }}>{user.nickname}</span>
<Link href="/settings" className="flex items-center gap-1 text-sm hover:opacity-80" style={{ color: "var(--text-secondary)" }}>
<Settings size={14} /> 设置
</Link>
<button onClick={handleLogout} className="flex items-center gap-1 text-sm hover:opacity-80" style={{ color: "var(--text-secondary)" }}>
<LogOut size={14} /> 退出
</button>
</div>
) : (
<div className="flex items-center gap-2">
<Link href="/login" className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-sm" style={{ background: "var(--bg-secondary)", color: "var(--text)" }}>
<LogIn size={14} /> 登录
</Link>
<Link href="/register" className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-sm text-white" style={{ background: "var(--primary)" }}>
<UserPlus size={14} /> 注册
</Link>
</div>
)}
</nav>
{/* 移动端菜单按钮 */}
<button onClick={() => setMenuOpen(!menuOpen)} className="md:hidden p-2" style={{ color: "var(--text)" }}>
{menuOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</div>
{/* 移动端下拉菜单 */}
{menuOpen && (
<div className="md:hidden border-t px-4 py-3 space-y-2" style={{ borderColor: "var(--border)", background: "var(--bg)" }}>
<form onSubmit={(e) => { handleSearch(e); setMenuOpen(false); }} className="relative">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2" style={{ color: "var(--text-secondary)" }} />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索帖子..."
className="w-full pl-9 pr-3 py-2 rounded-lg text-sm"
style={{ background: "var(--bg-secondary)", color: "var(--text)", border: "1px solid var(--border)" }}
/>
</form>
<Link href="/" onClick={() => setMenuOpen(false)} className="block py-2" style={{ color: "var(--text)" }}>首页</Link>
{user && (
<Link href="/compose" onClick={() => setMenuOpen(false)} className="block py-2" style={{ color: "var(--text)" }}>发帖</Link>
)}
<button onClick={toggleTheme} className="block py-2 w-full text-left" style={{ color: "var(--text)" }}>
{theme === "dark" ? "切换亮色主题" : "切换暗色主题"}
</button>
{user ? (
<Link href="/notifications" onClick={() => setMenuOpen(false)} className="block py-2" style={{ color: "var(--text)" }}>通知中心</Link>
) : null}
{user ? (
<>
<Link href="/settings" onClick={() => setMenuOpen(false)} className="block py-2" style={{ color: "var(--text)" }}>账号设置</Link>
<button onClick={handleLogout} className="block py-2 w-full text-left" style={{ color: "var(--text)" }}>退出登录</button>
</>
) : (
<>
<Link href="/login" onClick={() => setMenuOpen(false)} className="block py-2" style={{ color: "var(--text)" }}>登录</Link>
<Link href="/register" onClick={() => setMenuOpen(false)} className="block py-2" style={{ color: "var(--text)" }}>注册</Link>
</>
)}
</div>
)}
</header>
);
}