feat: 完善图片编辑功能和UI优化
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

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

View File

@ -12,15 +12,14 @@ export async function generateMetadata(): Promise<Metadata> {
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",
},
};
}

View File

@ -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<Vote[]>(
messages.length >= 2 ? `/api/vote?chatId=${id}` : null,
fetcher

View File

@ -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
>
{/* 顶部标题栏 */}
<div className="flex items-center justify-between border-b px-4 py-3">
<span className="text-base font-medium">图片标注</span>
<span className="text-base font-medium">图片编辑</span>
<div className="flex items-center gap-2">
<Button
variant="ghost"
@ -717,18 +733,19 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel }: ImageAnnotator
{/* 处理后的图片展示区域 */}
{processedImageBase64 && (
<div className="flex flex-1 flex-col items-center justify-center">
<div className="mb-2 flex items-center gap-2">
<div className="mb-2 flex items-center justify-between gap-2 w-full">
<span className="text-xs text-muted-foreground">
{processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : "拓展结果"}
</span>
<Button
variant="ghost"
variant="default"
size="sm"
onClick={handleDownloadProcessed}
className="h-7 px-2"
className="h-8 px-3 bg-primary text-primary-foreground hover:bg-primary/90 shadow-md flex items-center gap-1.5 font-medium"
title="下载处理后的图片"
>
<Download className="h-3.5 w-3.5" />
<Download className="h-4 w-4" />
<span className="text-xs">下载</span>
</Button>
</div>
<div className="relative rounded-lg border-2 border-primary/50 bg-background p-2 shadow-lg">

View File

@ -471,14 +471,14 @@ export function ImagePreview({
</div>
)}
{/* 图片标注器 */}
{/* 图片编辑器 */}
{showAnnotator && onDrawingComplete && (
<ImageAnnotator
imageUrl={original}
onConfirm={(annotatedImageUrl) => {
onDrawingComplete(annotatedImageUrl);
setShowAnnotator(false);
toast.success("标注图片已添加到聊天");
toast.success("编辑图片已添加到聊天");
}}
onCancel={() => setShowAnnotator(false)}
/>

View File

@ -363,14 +363,14 @@ export const PreviewAttachment = ({
</div>
)}
{/* 图片标注器 */}
{/* 图片编辑器 */}
{showAnnotator && onDrawingComplete && (
<ImageAnnotator
imageUrl={url}
onConfirm={(annotatedImageUrl) => {
onDrawingComplete(annotatedImageUrl);
setShowAnnotator(false);
toast.success("标注图片已添加到聊天");
toast.success("编辑图片已添加到聊天");
}}
onCancel={() => setShowAnnotator(false)}
/>

View File

@ -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<RankingItem[]>([]);
const [rankingOpen, setRankingOpen] = useState(false);
const [showImageEditor, setShowImageEditor] = useState(false);
const [editorImageUrl, setEditorImageUrl] = useState<string>("");
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
setMounted(true);
@ -94,6 +98,59 @@ export function TopNavbar({
// 那么在黑色导航栏上,应该优先使用 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 + 网站名称 */}
@ -213,6 +270,28 @@ export function TopNavbar({
</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"
@ -245,6 +324,15 @@ export function TopNavbar({
</Button>
)}
</div>
{/* 图片编辑器 */}
{showImageEditor && editorImageUrl && (
<ImageAnnotator
imageUrl={editorImageUrl}
onConfirm={handleImageEditorConfirm}
onCancel={handleImageEditorCancel}
/>
)}
</nav>
);
}