feat: 完善图片编辑功能和UI优化
- 修复浏览器favicon未更新问题(使用settings.favicon) - 将'图片标注'组件名改为'图片编辑'(所有相关文本和注释) - 在顶部导航栏添加'图片编辑'按钮,支持上传图片进行编辑 - 图片编辑完成后自动添加到聊天输入框(通过sessionStorage传递) - 优化图片编辑组件下载按钮UI(更明显的样式,添加文字标签) - 图片处理结果(变清晰/抠图/图像拓展)自动保存到相册和生图记录 - 优化图片编辑确认逻辑,优先使用处理后的图片
This commit is contained in:
parent
7c253e04a0
commit
7526a0159c
@ -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",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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)}
|
||||
/>
|
||||
|
||||
@ -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)}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user