325 lines
11 KiB
TypeScript
325 lines
11 KiB
TypeScript
"use client";
|
||
|
||
import { useRouter } from "next/navigation";
|
||
import Link from "next/link";
|
||
import { Settings, ExternalLink, Briefcase, Images, Sparkles, Trophy, Building2, Crown, Medal, Award, Edit, LayoutGrid } from "lucide-react";
|
||
import { Button } from "@/components/ui/button";
|
||
import { useTheme } from "next-themes";
|
||
import { useState, useEffect, useRef } from "react";
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuTrigger,
|
||
} from "@/components/ui/dropdown-menu";
|
||
import { ImageAnnotator } from "@/components/image-annotator";
|
||
|
||
type RankingItem = {
|
||
rank: number;
|
||
userId: string;
|
||
userName: string;
|
||
department: string;
|
||
featuredCount: number;
|
||
};
|
||
|
||
type TopNavbarProps = {
|
||
siteName?: string;
|
||
siteLogo?: string;
|
||
siteLogoDark?: string;
|
||
isAdmin?: boolean;
|
||
customLinks?: Array<{
|
||
id: string;
|
||
name: string;
|
||
url: string;
|
||
order: number;
|
||
}>;
|
||
};
|
||
|
||
export function TopNavbar({
|
||
siteName = "AI Chatbot",
|
||
siteLogo,
|
||
siteLogoDark,
|
||
isAdmin = false,
|
||
customLinks = [],
|
||
}: TopNavbarProps) {
|
||
const router = useRouter();
|
||
const { resolvedTheme } = useTheme();
|
||
const [mounted, setMounted] = useState(false);
|
||
const [ranking, setRanking] = useState<RankingItem[]>([]);
|
||
const [rankingOpen, setRankingOpen] = useState(false);
|
||
const [showImageEditor, setShowImageEditor] = useState(false);
|
||
const [editorImageUrl, setEditorImageUrl] = useState<string>("");
|
||
// 移除 fileInputRef,不再需要外部上传
|
||
|
||
useEffect(() => {
|
||
setMounted(true);
|
||
}, []);
|
||
|
||
// 获取精选榜单数据
|
||
const fetchRanking = async () => {
|
||
try {
|
||
const res = await fetch("/api/featured/ranking");
|
||
const data = await res.json();
|
||
if (data.ranking) {
|
||
setRanking(data.ranking);
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to fetch ranking:", error);
|
||
}
|
||
};
|
||
|
||
// 当下拉菜单打开时获取数据
|
||
useEffect(() => {
|
||
if (rankingOpen) {
|
||
fetchRanking();
|
||
}
|
||
}, [rankingOpen]);
|
||
|
||
// 获取排名图标
|
||
const getRankIcon = (rank: number) => {
|
||
switch (rank) {
|
||
case 1:
|
||
return <Crown className="h-4 w-4 text-yellow-500" />;
|
||
case 2:
|
||
return <Medal className="h-4 w-4 text-gray-400" />;
|
||
case 3:
|
||
return <Award className="h-4 w-4 text-amber-600" />;
|
||
default:
|
||
return <span className="w-4 h-4 text-center text-xs font-bold text-gray-500">{rank}</span>;
|
||
}
|
||
};
|
||
|
||
// 决定显示哪个 LOGO
|
||
const displayLogo = (siteLogoDark && siteLogoDark.trim() !== "") ? siteLogoDark : siteLogo;
|
||
|
||
// 打开图片编辑器 - 直接打开,不触发上传
|
||
const handleOpenImageEditor = () => {
|
||
setEditorImageUrl(""); // 确保初始为空
|
||
setShowImageEditor(true);
|
||
};
|
||
|
||
// 处理图片编辑完成
|
||
const handleImageEditorConfirm = (annotatedImageUrl: string) => {
|
||
// 将编辑后的图片存储到 sessionStorage,以便在聊天页面中使用
|
||
try {
|
||
sessionStorage.setItem("pending-edited-image", annotatedImageUrl);
|
||
} catch (error) {
|
||
// sessionStorage不可用(如隐私模式),使用URL参数作为备选方案
|
||
console.warn("sessionStorage不可用,尝试使用URL参数:", error);
|
||
// 如果sessionStorage不可用,我们可以将图片数据编码到URL中
|
||
// 但由于数据可能很大,这里只记录警告,让用户手动添加图片
|
||
// 或者可以显示提示信息
|
||
}
|
||
// 导航到新聊天页面(无论sessionStorage是否成功)
|
||
router.push("/");
|
||
setShowImageEditor(false);
|
||
setEditorImageUrl("");
|
||
};
|
||
|
||
// 取消图片编辑
|
||
const handleImageEditorCancel = () => {
|
||
setShowImageEditor(false);
|
||
setEditorImageUrl("");
|
||
};
|
||
|
||
return (
|
||
<nav className="fixed top-0 left-0 right-0 z-[100] flex h-12 items-center justify-between bg-black/90 backdrop-blur-sm px-4 text-white border-b border-white/10 shadow-sm">
|
||
{/* 左侧:LOGO + 网站名称 */}
|
||
<a
|
||
href="/"
|
||
className="flex items-center gap-2 transition-opacity hover:opacity-80"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
window.location.href = "/";
|
||
}}
|
||
>
|
||
{displayLogo && (
|
||
<img
|
||
src={displayLogo}
|
||
alt={siteName}
|
||
className="h-6 w-6 object-contain"
|
||
/>
|
||
)}
|
||
<span className="text-sm font-semibold">{siteName}</span>
|
||
</a>
|
||
|
||
{/* 右侧区域:自定义链接 + 系统设置 */}
|
||
<div className="flex items-center gap-2">
|
||
{/* 工具箱 (如果有自定义链接) */}
|
||
{customLinks.length > 0 && (
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors"
|
||
>
|
||
<Briefcase className="h-4 w-4" />
|
||
<span className="text-sm font-medium">工具箱</span>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="end" className="w-[320px] p-2">
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{customLinks
|
||
.sort((a, b) => a.order - b.order)
|
||
.map((link) => (
|
||
<DropdownMenuItem key={link.id} asChild className="cursor-pointer">
|
||
<a
|
||
href={link.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="flex flex-col items-center justify-center gap-2 rounded-md bg-muted/50 p-4 hover:bg-accent hover:text-accent-foreground text-center h-20 transition-colors"
|
||
>
|
||
<ExternalLink className="h-5 w-5 opacity-70" />
|
||
<span className="text-xs font-medium line-clamp-2">{link.name}</span>
|
||
</a>
|
||
</DropdownMenuItem>
|
||
))}
|
||
</div>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
)}
|
||
|
||
{/* AI智能精选 */}
|
||
<a
|
||
href="/featured"
|
||
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors text-sm font-medium"
|
||
title="AI智能精选"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
window.location.href = "/featured";
|
||
}}
|
||
>
|
||
<Sparkles className="h-4 w-4 text-yellow-400" />
|
||
<span>AI精选</span>
|
||
</a>
|
||
|
||
{/* 精选榜单 */}
|
||
<DropdownMenu open={rankingOpen} onOpenChange={setRankingOpen}>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors"
|
||
type="button"
|
||
>
|
||
<Trophy className="h-4 w-4 text-orange-400" />
|
||
<span className="text-sm font-medium">精选榜</span>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="end" className="w-[280px] p-3">
|
||
<div className="flex items-center gap-2 mb-3 pb-2 border-b">
|
||
<Trophy className="h-5 w-5 text-orange-500" />
|
||
<span className="font-semibold">精选榜单 TOP5</span>
|
||
</div>
|
||
{ranking.length === 0 ? (
|
||
<div className="text-center text-muted-foreground py-4 text-sm">
|
||
暂无精选数据
|
||
</div>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{ranking.map((item) => (
|
||
<div
|
||
key={item.userId}
|
||
className={`flex items-center gap-3 p-2 rounded-lg transition-colors ${
|
||
item.rank === 1 ? "bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/20 dark:to-orange-900/20" :
|
||
item.rank === 2 ? "bg-gray-50 dark:bg-gray-800/50" :
|
||
item.rank === 3 ? "bg-amber-50 dark:bg-amber-900/20" :
|
||
"hover:bg-muted"
|
||
}`}
|
||
>
|
||
<div className="flex items-center justify-center w-6">
|
||
{getRankIcon(item.rank)}
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<p className="font-medium text-sm truncate">{item.userName}</p>
|
||
<p className="text-xs text-muted-foreground flex items-center gap-1 truncate">
|
||
<Building2 className="h-3 w-3" />
|
||
{item.department}
|
||
</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<span className="text-lg font-bold text-orange-500">{item.featuredCount}</span>
|
||
<span className="text-xs text-muted-foreground ml-1">精选</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
{/* 图片编辑 */}
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors"
|
||
onClick={handleOpenImageEditor}
|
||
type="button"
|
||
title="图片编辑"
|
||
>
|
||
<Edit className="h-4 w-4" />
|
||
<span className="text-sm font-medium">图片编辑</span>
|
||
</Button>
|
||
|
||
{/* 无限画布 */}
|
||
<button
|
||
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors text-sm font-medium"
|
||
onClick={() => {
|
||
const newId = crypto.randomUUID();
|
||
window.location.href = `/canvas/${newId}`;
|
||
}}
|
||
type="button"
|
||
title="无限画布"
|
||
>
|
||
<LayoutGrid className="h-4 w-4 text-purple-400" />
|
||
<span>无限画布</span>
|
||
</button>
|
||
|
||
{/* 我的图册 */}
|
||
<a
|
||
href="/gallery"
|
||
className="flex items-center gap-1 h-8 px-3 text-white hover:bg-white/15 hover:text-white rounded-full transition-colors text-sm font-medium"
|
||
title="我的图册"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
window.location.href = "/gallery";
|
||
}}
|
||
>
|
||
<Images className="h-4 w-4" />
|
||
<span>我的图册</span>
|
||
</a>
|
||
|
||
{/* 分隔线 (如果有链接或图册按钮,且是管理员) */}
|
||
{isAdmin && (
|
||
<div className="hidden h-4 w-px bg-white/20 mx-1 sm:block" />
|
||
)}
|
||
|
||
{/* 系统设置(仅管理员可见)*/}
|
||
{isAdmin && (
|
||
<a
|
||
href="/settings"
|
||
className="flex items-center justify-center h-8 w-8 text-white hover:bg-white/15 hover:text-white rounded-full"
|
||
title="系统设置"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
window.location.href = "/settings";
|
||
}}
|
||
>
|
||
<Settings className="h-4 w-4" />
|
||
</a>
|
||
)}
|
||
</div>
|
||
|
||
{/* 图片编辑器 */}
|
||
{showImageEditor && (
|
||
<ImageAnnotator
|
||
imageUrl={editorImageUrl}
|
||
onConfirm={handleImageEditorConfirm}
|
||
onCancel={handleImageEditorCancel}
|
||
/>
|
||
)}
|
||
</nav>
|
||
);
|
||
}
|