lovart3/components/top-navbar.tsx
Your Name 7526a0159c feat: 完善图片编辑功能和UI优化
- 修复浏览器favicon未更新问题(使用settings.favicon)
- 将'图片标注'组件名改为'图片编辑'(所有相关文本和注释)
- 在顶部导航栏添加'图片编辑'按钮,支持上传图片进行编辑
- 图片编辑完成后自动添加到聊天输入框(通过sessionStorage传递)
- 优化图片编辑组件下载按钮UI(更明显的样式,添加文字标签)
- 图片处理结果(变清晰/抠图/图像拓展)自动保存到相册和生图记录
- 优化图片编辑确认逻辑,优先使用处理后的图片
2025-12-29 11:24:47 +08:00

339 lines
12 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 { useRouter } from "next/navigation";
import Link from "next/link";
import { Settings, ExternalLink, Briefcase, Images, Sparkles, Trophy, Building2, Crown, Medal, Award, Edit } 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>("");
const fileInputRef = useRef<HTMLInputElement>(null);
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
// 导航栏背景固定为黑色,理论上应该始终显示适合深色背景的 LOGO
// 但如果有专门配置了 "Dark Mode Logo" (通常用于深色背景),我们优先使用它
// 如果没配置 Dark Mode Logo,则使用默认 Logo
// 注意:这里的背景是黑色的,所以通常需要一个浅色的 LOGO(即 Dark Mode 下使用的 LOGO)
// 如果用户配置逻辑是:siteLogo(Light Mode/白底用), siteLogoDark(Dark Mode/黑底用)
// 那么在黑色导航栏上,应该优先使用 siteLogoDark
const displayLogo = (siteLogoDark && siteLogoDark.trim() !== "") ? siteLogoDark : siteLogo;
// 处理图片上传
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// 检查文件类型
if (!file.type.startsWith("image/")) {
return;
}
// 转换为 data URL
const reader = new FileReader();
reader.onloadend = () => {
const result = reader.result as string;
setEditorImageUrl(result);
setShowImageEditor(true);
};
reader.onerror = () => {
console.error("图片读取失败");
};
reader.readAsDataURL(file);
// 清空文件输入,以便可以再次选择同一文件
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
// 打开图片编辑器
const handleOpenImageEditor = () => {
fileInputRef.current?.click();
};
// 处理图片编辑完成
const handleImageEditorConfirm = (annotatedImageUrl: string) => {
// 将编辑后的图片存储到 sessionStorage,以便在聊天页面中使用
try {
sessionStorage.setItem("pending-edited-image", annotatedImageUrl);
// 导航到新聊天页面
router.push("/");
} catch (error) {
console.error("保存图片数据失败:", error);
}
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 + 网站名称 */}
<Link href="/" className="flex items-center gap-2 transition-opacity hover:opacity-80">
{displayLogo && (
<img
src={displayLogo}
alt={siteName}
className="h-6 w-6 object-contain"
/>
)}
<span className="text-sm font-semibold">{siteName}</span>
</Link>
{/* 右侧区域:自定义链接 + 系统设置 */}
<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智能精选 */}
<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={() => router.push("/featured")}
type="button"
title="AI智能精选"
>
<Sparkles className="h-4 w-4 text-yellow-400" />
<span className="text-sm font-medium">AI精选</span>
</Button>
{/* 精选榜单 */}
<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>
{/* 隐藏的文件输入 */}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageUpload}
className="hidden"
/>
{/* 我的图册 */}
<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={() => router.push("/gallery")}
type="button"
title="我的图册"
>
<Images className="h-4 w-4" />
<span className="text-sm font-medium">我的图册</span>
</Button>
{/* 分隔线 (如果有链接或图册按钮,且是管理员) */}
{isAdmin && (
<div className="hidden h-4 w-px bg-white/20 mx-1 sm:block" />
)}
{/* 系统设置(仅管理员可见)*/}
{isAdmin && (
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-white hover:bg-white/15 hover:text-white rounded-full"
onClick={() => router.push("/settings")}
type="button"
title="系统设置"
>
<Settings className="h-4 w-4" />
</Button>
)}
</div>
{/* 图片编辑器 */}
{showImageEditor && editorImageUrl && (
<ImageAnnotator
imageUrl={editorImageUrl}
onConfirm={handleImageEditorConfirm}
onCancel={handleImageEditorCancel}
/>
)}
</nav>
);
}