diff --git a/app/layout.tsx b/app/layout.tsx index a1f5613..0eb1299 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -12,15 +12,14 @@ export async function generateMetadata(): Promise { const settings = await getSiteSettings(); return { - metadataBase: settings.siteUrl ? new URL(settings.siteUrl) : undefined, title: { template: `%s | ${settings.siteName}`, default: settings.siteName, }, - description: settings.siteDescription || settings.siteName, + description: settings.siteName, icons: { - icon: settings.siteFavicon || "/favicon.ico", - apple: settings.siteFavicon || "/favicon.ico", + icon: settings.favicon || "/favicon.ico", + apple: settings.favicon || "/favicon.ico", }, }; } diff --git a/components/chat.tsx b/components/chat.tsx index ab586f5..3d72c34 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -394,6 +394,61 @@ export function Chat({ } }, [editImageUrl, hasLoadedEditImage, id, setInput]); + // 🆕 处理从导航栏图片编辑器跳转过来的图片编辑请求 + const [hasLoadedPendingImage, setHasLoadedPendingImage] = useState(false); + useEffect(() => { + if (!hasLoadedPendingImage) { + try { + const pendingImage = sessionStorage.getItem("pending-edited-image"); + if (pendingImage) { + // 清除 sessionStorage 中的数据,避免重复添加 + sessionStorage.removeItem("pending-edited-image"); + + // 判断是 base64 data URL 还是普通 URL + const fileName = pendingImage.startsWith("data:image/") + ? `edited-image-${Date.now()}.png` + : pendingImage.split('/').pop()?.split('?')[0] || `image-${Date.now()}.jpg`; + + // 推断图片类型 + let contentType = "image/png"; + if (pendingImage.startsWith("data:image/jpeg") || pendingImage.startsWith("data:image/jpg")) { + contentType = "image/jpeg"; + } else if (pendingImage.startsWith("data:image/webp")) { + contentType = "image/webp"; + } else if (pendingImage.startsWith("data:image/gif")) { + contentType = "image/gif"; + } else if (!pendingImage.startsWith("data:")) { + // 普通 URL,根据文件名推断 + if (fileName.toLowerCase().endsWith(".jpg") || fileName.toLowerCase().endsWith(".jpeg")) { + contentType = "image/jpeg"; + } else if (fileName.toLowerCase().endsWith(".webp")) { + contentType = "image/webp"; + } else if (fileName.toLowerCase().endsWith(".gif")) { + contentType = "image/gif"; + } + } + + setAttachments((prev) => [ + ...prev, + { + url: pendingImage, + name: fileName, + contentType: contentType, + }, + ]); + + // 自动填充提示词(如果输入框为空) + setInput((prev) => prev || "请帮我处理这张图片:"); + + setHasLoadedPendingImage(true); + } + } catch (error) { + console.error("读取待处理图片失败:", error); + setHasLoadedPendingImage(true); + } + } + }, [hasLoadedPendingImage, setInput]); + const { data: votes } = useSWR( messages.length >= 2 ? `/api/vote?chatId=${id}` : null, fetcher diff --git a/components/image-annotator.tsx b/components/image-annotator.tsx index db711a7..1b63807 100644 --- a/components/image-annotator.tsx +++ b/components/image-annotator.tsx @@ -461,7 +461,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator const result = await response.json(); if (result.success && result.base64) { - // 保存处理后的图片URL和Base64 + // 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图) if (result.original_url) { setProcessedImageUrl(result.original_url); } else { @@ -523,6 +523,22 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator // 确认 const handleConfirm = () => { + // 优先使用处理后的图片(如果有) + if (processedImageUrl) { + // 如果有处理后的图片URL,直接使用它 + onConfirm(processedImageUrl); + return; + } + + if (processedImageBase64) { + // 如果有处理后的图片Base64,转换为data URL + const mimeType = processType === "rmbg" ? "image/png" : "image/jpeg"; + const dataUrl = `data:${mimeType};base64,${processedImageBase64}`; + onConfirm(dataUrl); + return; + } + + // 否则,导出标注后的图片 const canvas = canvasRef.current; if (!canvas || !image) return; @@ -556,7 +572,7 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator > {/* 顶部标题栏 */}
- 图片标注 + 图片编辑
diff --git a/components/image-preview.tsx b/components/image-preview.tsx index b1d7cee..58ee38c 100644 --- a/components/image-preview.tsx +++ b/components/image-preview.tsx @@ -471,14 +471,14 @@ export function ImagePreview({
)} - {/* 图片标注器 */} + {/* 图片编辑器 */} {showAnnotator && onDrawingComplete && ( { onDrawingComplete(annotatedImageUrl); setShowAnnotator(false); - toast.success("标注图片已添加到聊天"); + toast.success("编辑图片已添加到聊天"); }} onCancel={() => setShowAnnotator(false)} /> diff --git a/components/preview-attachment.tsx b/components/preview-attachment.tsx index 84162da..cf82498 100644 --- a/components/preview-attachment.tsx +++ b/components/preview-attachment.tsx @@ -363,14 +363,14 @@ export const PreviewAttachment = ({
)} - {/* 图片标注器 */} + {/* 图片编辑器 */} {showAnnotator && onDrawingComplete && ( { onDrawingComplete(annotatedImageUrl); setShowAnnotator(false); - toast.success("标注图片已添加到聊天"); + toast.success("编辑图片已添加到聊天"); }} onCancel={() => setShowAnnotator(false)} /> diff --git a/components/top-navbar.tsx b/components/top-navbar.tsx index ed142fc..cba78bc 100644 --- a/components/top-navbar.tsx +++ b/components/top-navbar.tsx @@ -2,16 +2,17 @@ import { useRouter } from "next/navigation"; import Link from "next/link"; -import { Settings, ExternalLink, Briefcase, Images, Sparkles, Trophy, Building2, Crown, Medal, Award } from "lucide-react"; +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 } from "react"; +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; @@ -46,6 +47,9 @@ export function TopNavbar({ const [mounted, setMounted] = useState(false); const [ranking, setRanking] = useState([]); const [rankingOpen, setRankingOpen] = useState(false); + const [showImageEditor, setShowImageEditor] = useState(false); + const [editorImageUrl, setEditorImageUrl] = useState(""); + const fileInputRef = useRef(null); useEffect(() => { setMounted(true); @@ -94,6 +98,59 @@ export function TopNavbar({ // 那么在黑色导航栏上,应该优先使用 siteLogoDark const displayLogo = (siteLogoDark && siteLogoDark.trim() !== "") ? siteLogoDark : siteLogo; + // 处理图片上传 + const handleImageUpload = (e: React.ChangeEvent) => { + 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 ( ); }