feat: 添加去水印功能 - 图片工具栏新增去水印按钮,系统设置新增PicWish API配置
This commit is contained in:
parent
ff359fe03b
commit
f2d7aff7c6
@ -447,6 +447,11 @@ export function AdminSettingsClient() {
|
||||
yScale: "1.5", // Y轴放大倍数
|
||||
});
|
||||
|
||||
// 去水印API配置状态
|
||||
const [watermarkRemoveConfig, setWatermarkRemoveConfig] = useState({
|
||||
apiKey: "", // PicWish API密钥
|
||||
});
|
||||
|
||||
// 角度编辑API配置状态
|
||||
const [angleEditConfig, setAngleEditConfig] = useState({
|
||||
enabled: false,
|
||||
@ -531,6 +536,19 @@ export function AdminSettingsClient() {
|
||||
}
|
||||
}, [extendConfigData]);
|
||||
|
||||
// 加载去水印API配置
|
||||
const { data: watermarkRemoveConfigData } = useSWR("/api/admin/settings?key=watermark_remove_config", fetcher);
|
||||
useEffect(() => {
|
||||
if (watermarkRemoveConfigData?.setting?.value) {
|
||||
const config = watermarkRemoveConfigData.setting.value as {
|
||||
apiKey?: string;
|
||||
};
|
||||
setWatermarkRemoveConfig({
|
||||
apiKey: config.apiKey || "",
|
||||
});
|
||||
}
|
||||
}, [watermarkRemoveConfigData]);
|
||||
|
||||
// 加载角度编辑API配置
|
||||
const { data: angleEditConfigData } = useSWR("/api/admin/settings?key=angle_edit_config", fetcher);
|
||||
useEffect(() => {
|
||||
@ -1013,6 +1031,32 @@ export function AdminSettingsClient() {
|
||||
}
|
||||
};
|
||||
|
||||
// --- 去水印API配置逻辑 ---
|
||||
const handleSaveWatermarkRemoveConfig = async () => {
|
||||
try {
|
||||
if (!watermarkRemoveConfig.apiKey.trim()) {
|
||||
toast.error("请输入PicWish API密钥");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("/api/admin/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
key: "watermark_remove_config",
|
||||
value: {
|
||||
apiKey: watermarkRemoveConfig.apiKey.trim(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("保存失败");
|
||||
toast.success("去水印API配置保存成功");
|
||||
} catch (error) {
|
||||
toast.error("保存去水印API配置失败");
|
||||
}
|
||||
};
|
||||
|
||||
// --- 角度编辑API配置逻辑 ---
|
||||
const handleSaveAngleEditConfig = async () => {
|
||||
try {
|
||||
@ -4559,6 +4603,49 @@ export function AdminSettingsClient() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 去水印API配置 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z" />
|
||||
<line x1="4" y1="4" x2="20" y2="20" />
|
||||
</svg>
|
||||
去水印API配置
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
配置PicWish去水印API,用于自动去除图片中的水印
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 pt-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="watermarkRemoveApiKey">API密钥 *</Label>
|
||||
<Input
|
||||
id="watermarkRemoveApiKey"
|
||||
type="password"
|
||||
value={watermarkRemoveConfig.apiKey}
|
||||
onChange={(e) => setWatermarkRemoveConfig({ ...watermarkRemoveConfig, apiKey: e.target.value })}
|
||||
placeholder="输入PicWish API密钥"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
在 PicWish 开发者平台获取API密钥
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-purple-200 bg-purple-50 p-4 space-y-2">
|
||||
<div className="text-sm font-medium text-purple-900">PicWish API 说明</div>
|
||||
<div className="text-xs text-purple-800 space-y-1">
|
||||
<p>• 使用 PicWish 云服务进行智能去水印</p>
|
||||
<p>• 支持各种类型的水印去除(文字、图片水印)</p>
|
||||
<p>• API文档:https://picwish.com/developer</p>
|
||||
<p>• 处理时间:约10-30秒/张</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleSaveWatermarkRemoveConfig}>
|
||||
<Save className="mr-2 h-4 w-4" /> 保存配置
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 角度编辑API配置 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
219
app/api/watermark-remove/route.ts
Normal file
219
app/api/watermark-remove/route.ts
Normal file
@ -0,0 +1,219 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { systemSettings } from "@/lib/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
|
||||
// 获取去水印API配置
|
||||
async function getWatermarkRemoveConfig() {
|
||||
try {
|
||||
const setting = await db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, "watermark_remove_config"))
|
||||
.limit(1);
|
||||
|
||||
if (setting.length > 0 && setting[0].value) {
|
||||
return setting[0].value as { apiKey: string };
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("[WatermarkRemove] 获取配置失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 轮询获取处理结果
|
||||
async function pollForResult(taskId: string, apiKey: string): Promise<{ success: boolean; imageUrl?: string; error?: string }> {
|
||||
const maxAttempts = 30; // 最多轮询30次(30秒)
|
||||
const pollInterval = 1000; // 每次间隔1秒
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://techsz.aoscdn.com/api/tasks/visual/external/watermark-remove/${taskId}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-API-KEY": apiKey,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`[WatermarkRemove] 轮询失败 (attempt ${attempt + 1}):`, errorText);
|
||||
// 继续轮询
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log(`[WatermarkRemove] 轮询结果 (attempt ${attempt + 1}):`, JSON.stringify(result).substring(0, 200));
|
||||
|
||||
// 检查任务状态
|
||||
if (result.data?.state === 1) {
|
||||
// 任务完成
|
||||
const imageUrl = result.data?.image;
|
||||
if (imageUrl) {
|
||||
return { success: true, imageUrl };
|
||||
}
|
||||
return { success: false, error: "处理完成但未返回图片URL" };
|
||||
} else if (result.data?.state === -1) {
|
||||
// 任务失败
|
||||
return { success: false, error: result.data?.error || "去水印处理失败" };
|
||||
}
|
||||
// state === 0 表示处理中,继续轮询
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
} catch (error) {
|
||||
console.error(`[WatermarkRemove] 轮询错误 (attempt ${attempt + 1}):`, error);
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false, error: "处理超时,请稍后重试" };
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// 验证用户登录
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "请先登录" }, { status: 401 });
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// 获取请求参数
|
||||
const body = await request.json();
|
||||
const { image_url, image_base64 } = body;
|
||||
|
||||
if (!image_url && !image_base64) {
|
||||
return NextResponse.json(
|
||||
{ error: "请提供图片URL或Base64数据" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 获取API配置
|
||||
const config = await getWatermarkRemoveConfig();
|
||||
if (!config?.apiKey) {
|
||||
return NextResponse.json(
|
||||
{ error: "去水印服务未配置,请联系管理员在系统设置中配置API密钥" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 如果传入的是base64,需要先上传到S3获取URL
|
||||
let imageUrl = image_url;
|
||||
if (image_base64 && !image_url) {
|
||||
try {
|
||||
const buffer = Buffer.from(image_base64, "base64");
|
||||
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
||||
const filename = `watermark-temp-${Date.now()}.jpeg`;
|
||||
const uploadResult = await uploadFile(filename, arrayBuffer);
|
||||
imageUrl = uploadResult.url;
|
||||
console.log("[WatermarkRemove] Base64图片已上传到S3:", imageUrl);
|
||||
} catch (uploadError) {
|
||||
console.error("[WatermarkRemove] 上传图片失败:", uploadError);
|
||||
return NextResponse.json(
|
||||
{ error: "上传图片失败" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[WatermarkRemove] 开始处理图片:", imageUrl);
|
||||
|
||||
// 创建去水印任务
|
||||
const formData = new FormData();
|
||||
formData.append("sync", "0");
|
||||
formData.append("url", imageUrl);
|
||||
|
||||
const createResponse = await fetch(
|
||||
"https://techsz.aoscdn.com/api/tasks/visual/external/watermark-remove",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-API-KEY": config.apiKey,
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
if (!createResponse.ok) {
|
||||
const errorText = await createResponse.text();
|
||||
console.error("[WatermarkRemove] 创建任务失败:", errorText);
|
||||
return NextResponse.json(
|
||||
{ error: "创建去水印任务失败" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const createResult = await createResponse.json();
|
||||
console.log("[WatermarkRemove] 创建任务结果:", JSON.stringify(createResult).substring(0, 200));
|
||||
|
||||
const taskId = createResult.data?.task_id;
|
||||
if (!taskId) {
|
||||
return NextResponse.json(
|
||||
{ error: "未获取到任务ID" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 轮询获取结果
|
||||
const pollResult = await pollForResult(taskId, config.apiKey);
|
||||
|
||||
if (!pollResult.success) {
|
||||
return NextResponse.json(
|
||||
{ error: pollResult.error || "去水印处理失败" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 下载处理后的图片并上传到S3
|
||||
const processedImageUrl = pollResult.imageUrl!;
|
||||
console.log("[WatermarkRemove] 处理完成,结果URL:", processedImageUrl);
|
||||
|
||||
try {
|
||||
// 下载处理后的图片
|
||||
const imageResponse = await fetch(processedImageUrl);
|
||||
if (!imageResponse.ok) {
|
||||
throw new Error("下载处理后的图片失败");
|
||||
}
|
||||
const imageBuffer = await imageResponse.arrayBuffer();
|
||||
|
||||
// 上传到S3
|
||||
const filename = `watermark-removed-${Date.now()}.png`;
|
||||
const uploadResult = await uploadFile(filename, imageBuffer);
|
||||
|
||||
const processingTime = (Date.now() - startTime) / 1000;
|
||||
console.log(`[WatermarkRemove] 处理完成,耗时: ${processingTime}s`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
original_url: uploadResult.url,
|
||||
thumbnail_url: uploadResult.url,
|
||||
processing_time: processingTime,
|
||||
});
|
||||
} catch (uploadError) {
|
||||
console.error("[WatermarkRemove] 上传结果失败:", uploadError);
|
||||
// 如果上传失败,直接返回原始URL
|
||||
const processingTime = (Date.now() - startTime) / 1000;
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
original_url: processedImageUrl,
|
||||
thumbnail_url: processedImageUrl,
|
||||
processing_time: processingTime,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[WatermarkRemove] 处理错误:", error);
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "去水印处理失败" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -123,6 +123,7 @@ export function Chat({
|
||||
const [isProcessingRmbg, setIsProcessingRmbg] = useState(false);
|
||||
const [isProcessingExtend, setIsProcessingExtend] = useState(false);
|
||||
const [isProcessingAngleEdit, setIsProcessingAngleEdit] = useState(false);
|
||||
const [isProcessingWatermarkRemove, setIsProcessingWatermarkRemove] = useState(false);
|
||||
const currentModelIdRef = useRef(currentModelId);
|
||||
|
||||
// 🆕 视频参数状态
|
||||
@ -889,6 +890,110 @@ export function Chat({
|
||||
}
|
||||
};
|
||||
|
||||
// 处理图片去水印
|
||||
const handleImageWatermarkRemove = async (imageUrl: string) => {
|
||||
// 防止重复点击
|
||||
if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend || isProcessingWatermarkRemove) {
|
||||
toast.info("正在处理中,请稍候...");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessingWatermarkRemove(true);
|
||||
const messageId = generateUUID();
|
||||
|
||||
// 创建临时加载消息
|
||||
const loadingMessage: ChatMessage = {
|
||||
id: messageId,
|
||||
role: "assistant",
|
||||
parts: [{ type: "text", text: "正在去水印..." }],
|
||||
createdAt: new Date().toISOString(),
|
||||
} as ChatMessage;
|
||||
|
||||
setMessages((prev) => [...prev, loadingMessage]);
|
||||
|
||||
// 延迟滚动到底部,确保DOM已更新
|
||||
setTimeout(() => {
|
||||
const messagesContainer = document.querySelector('[role="log"]');
|
||||
if (messagesContainer) {
|
||||
messagesContainer.scrollTo({
|
||||
top: messagesContainer.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
|
||||
try {
|
||||
// 调用去水印API
|
||||
const watermarkRemoveResponse = await fetch("/api/watermark-remove", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
image_url: imageUrl,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!watermarkRemoveResponse.ok) {
|
||||
const error = await watermarkRemoveResponse.json();
|
||||
throw new Error(error.error || "去水印处理失败");
|
||||
}
|
||||
|
||||
const result = await watermarkRemoveResponse.json();
|
||||
|
||||
if (result.success && result.original_url) {
|
||||
// 构造图片Markdown(使用与Gemini生成相同的格式)
|
||||
const imageMarkdown = ``;
|
||||
|
||||
// 创建最终消息(前端显示用)
|
||||
const finalMessage: ChatMessage = {
|
||||
id: messageId,
|
||||
role: "assistant",
|
||||
parts: [{ type: "text", text: imageMarkdown }],
|
||||
createdAt: new Date().toISOString(),
|
||||
} as ChatMessage;
|
||||
|
||||
setMessages((prev) => {
|
||||
const filtered = prev.filter((msg) => msg.id !== messageId);
|
||||
return [...filtered, finalMessage];
|
||||
});
|
||||
|
||||
// 保存消息到数据库
|
||||
try {
|
||||
const saveResponse = await fetch("/api/messages", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
chatId: id,
|
||||
message: {
|
||||
id: messageId,
|
||||
role: "assistant",
|
||||
parts: [{ type: "text", text: imageMarkdown }],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!saveResponse.ok) {
|
||||
console.error("[Chat] Failed to save message:", await saveResponse.text());
|
||||
}
|
||||
} catch (saveError) {
|
||||
console.error("[Chat] Failed to save message:", saveError);
|
||||
}
|
||||
|
||||
toast.success(`去水印完成!处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`);
|
||||
} else {
|
||||
throw new Error(result.error || "去水印处理失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[去水印] 错误:", error);
|
||||
toast.error(error instanceof Error ? error.message : "去水印失败,请稍后重试");
|
||||
// 移除加载消息
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
} finally {
|
||||
setIsProcessingWatermarkRemove(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理图像拓展
|
||||
const handleImageExtend = async (imageUrl: string) => {
|
||||
// 防止重复点击
|
||||
@ -1157,10 +1262,12 @@ export function Chat({
|
||||
onImageRmbg={handleImageRmbg}
|
||||
onImageExtend={handleImageExtend}
|
||||
onImageAngleEdit={handleImageAngleEdit}
|
||||
onImageWatermarkRemove={handleImageWatermarkRemove}
|
||||
isProcessingEnhance={isProcessingEnhance}
|
||||
isProcessingRmbg={isProcessingRmbg}
|
||||
isProcessingExtend={isProcessingExtend}
|
||||
isProcessingAngleEdit={isProcessingAngleEdit}
|
||||
isProcessingWatermarkRemove={isProcessingWatermarkRemove}
|
||||
pendingGeneration={pendingGeneration}
|
||||
imageCount={imageParams?.imageCount || 1}
|
||||
isGeneratingImageFlag={isGeneratingImageFlag}
|
||||
|
||||
@ -13,10 +13,12 @@ type ResponseProps = ComponentProps<typeof Streamdown> & {
|
||||
onImageRmbg?: (imageUrl: string) => void;
|
||||
onImageExtend?: (imageUrl: string) => void;
|
||||
onImageAngleEdit?: (imageUrl: string) => void;
|
||||
onImageWatermarkRemove?: (imageUrl: string) => void;
|
||||
isProcessingEnhance?: boolean;
|
||||
isProcessingRmbg?: boolean;
|
||||
isProcessingExtend?: boolean;
|
||||
isProcessingAngleEdit?: boolean;
|
||||
isProcessingWatermarkRemove?: boolean;
|
||||
};
|
||||
|
||||
// 注意:移除了 memo 的自定义比较函数,确保 isProcessing* 状态变化时组件能正确重新渲染
|
||||
@ -29,10 +31,12 @@ export const Response = ({
|
||||
onImageRmbg,
|
||||
onImageExtend,
|
||||
onImageAngleEdit,
|
||||
onImageWatermarkRemove,
|
||||
isProcessingEnhance,
|
||||
isProcessingRmbg,
|
||||
isProcessingExtend,
|
||||
isProcessingAngleEdit,
|
||||
isProcessingWatermarkRemove,
|
||||
...props
|
||||
}: ResponseProps) => (
|
||||
<Streamdown
|
||||
@ -46,7 +50,7 @@ export const Response = ({
|
||||
return null;
|
||||
}
|
||||
// 使用 key 强制在处理状态变化时重新渲染组件
|
||||
const processingKey = `${isProcessingEnhance}-${isProcessingRmbg}-${isProcessingExtend}-${isProcessingAngleEdit}`;
|
||||
const processingKey = `${isProcessingEnhance}-${isProcessingRmbg}-${isProcessingExtend}-${isProcessingAngleEdit}-${isProcessingWatermarkRemove}`;
|
||||
return <ImagePreview
|
||||
key={`${src}-${processingKey}`}
|
||||
src={src}
|
||||
@ -57,10 +61,12 @@ export const Response = ({
|
||||
onImageRmbg={onImageRmbg}
|
||||
onImageExtend={onImageExtend}
|
||||
onImageAngleEdit={onImageAngleEdit}
|
||||
onImageWatermarkRemove={onImageWatermarkRemove}
|
||||
isProcessingEnhance={isProcessingEnhance}
|
||||
isProcessingRmbg={isProcessingRmbg}
|
||||
isProcessingExtend={isProcessingExtend}
|
||||
isProcessingAngleEdit={isProcessingAngleEdit}
|
||||
isProcessingWatermarkRemove={isProcessingWatermarkRemove}
|
||||
/>;
|
||||
},
|
||||
video: ({ src, alt }: { src?: string; alt?: string }) => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { X, Download, Edit, Target, Paintbrush, Sparkles, Scissors, Expand, Maximize2, Move3D } from "lucide-react";
|
||||
import { X, Download, Edit, Target, Paintbrush, Sparkles, Scissors, Expand, Maximize2, Move3D, Droplet } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { ImageAnnotator } from "./image-annotator";
|
||||
import { AngleEditDialog } from "./angle-edit-dialog";
|
||||
@ -16,10 +16,12 @@ export function ImagePreview({
|
||||
onImageRmbg,
|
||||
onImageExtend,
|
||||
onImageAngleEdit,
|
||||
onImageWatermarkRemove,
|
||||
isProcessingEnhance,
|
||||
isProcessingRmbg,
|
||||
isProcessingExtend,
|
||||
isProcessingAngleEdit,
|
||||
isProcessingWatermarkRemove,
|
||||
imageIndex
|
||||
}: {
|
||||
src: string;
|
||||
@ -35,10 +37,12 @@ export function ImagePreview({
|
||||
onImageRmbg?: (imageUrl: string) => void;
|
||||
onImageExtend?: (imageUrl: string) => void;
|
||||
onImageAngleEdit?: (imageUrl: string) => void;
|
||||
onImageWatermarkRemove?: (imageUrl: string) => void;
|
||||
isProcessingEnhance?: boolean;
|
||||
isProcessingRmbg?: boolean;
|
||||
isProcessingExtend?: boolean;
|
||||
isProcessingAngleEdit?: boolean;
|
||||
isProcessingWatermarkRemove?: boolean;
|
||||
imageIndex?: number;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@ -55,9 +59,10 @@ export function ImagePreview({
|
||||
isProcessingRmbg,
|
||||
isProcessingExtend,
|
||||
isProcessingAngleEdit,
|
||||
isProcessingWatermarkRemove,
|
||||
src: src?.substring(0, 50)
|
||||
});
|
||||
}, [isProcessingEnhance, isProcessingRmbg, isProcessingExtend, isProcessingAngleEdit, src]);
|
||||
}, [isProcessingEnhance, isProcessingRmbg, isProcessingExtend, isProcessingAngleEdit, isProcessingWatermarkRemove, src]);
|
||||
|
||||
// 解析 alt 文本,提取原图 URL 和宽度
|
||||
// 格式优先级:
|
||||
@ -326,11 +331,11 @@ export function ImagePreview({
|
||||
className="flex items-center gap-1 rounded-full bg-black/70 hover:bg-black/80 px-2 py-1 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend && !isProcessingAngleEdit) {
|
||||
if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend && !isProcessingAngleEdit && !isProcessingWatermarkRemove) {
|
||||
setShowAngleEditDialog(true);
|
||||
}
|
||||
}}
|
||||
disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend || isProcessingAngleEdit}
|
||||
disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend || isProcessingAngleEdit || isProcessingWatermarkRemove}
|
||||
type="button"
|
||||
aria-label="角度调整"
|
||||
title="角度调整"
|
||||
@ -339,6 +344,24 @@ export function ImagePreview({
|
||||
<span className="text-[10px] font-medium">角度调整</span>
|
||||
</button>
|
||||
)}
|
||||
{onImageWatermarkRemove && (
|
||||
<button
|
||||
className="flex items-center gap-1 rounded-full bg-black/70 hover:bg-black/80 px-2 py-1 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend && !isProcessingAngleEdit && !isProcessingWatermarkRemove) {
|
||||
onImageWatermarkRemove(original);
|
||||
}
|
||||
}}
|
||||
disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend || isProcessingAngleEdit || isProcessingWatermarkRemove}
|
||||
type="button"
|
||||
aria-label="去水印"
|
||||
title="去水印"
|
||||
>
|
||||
<Droplet size={12} />
|
||||
<span className="text-[10px] font-medium">去水印</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="flex items-center gap-1 rounded-full bg-black/70 hover:bg-black/80 px-2 py-1 text-white transition-colors shadow-lg whitespace-nowrap"
|
||||
onClick={(e) => {
|
||||
|
||||
@ -152,10 +152,12 @@ const PurePreviewMessage = ({
|
||||
onImageRmbg,
|
||||
onImageExtend,
|
||||
onImageAngleEdit,
|
||||
onImageWatermarkRemove,
|
||||
isProcessingEnhance,
|
||||
isProcessingRmbg,
|
||||
isProcessingExtend,
|
||||
isProcessingAngleEdit,
|
||||
isProcessingWatermarkRemove,
|
||||
}: {
|
||||
chatId: string;
|
||||
message: ChatMessage;
|
||||
@ -172,10 +174,12 @@ const PurePreviewMessage = ({
|
||||
onImageRmbg?: (imageUrl: string) => void;
|
||||
onImageExtend?: (imageUrl: string) => void;
|
||||
onImageAngleEdit?: (imageUrl: string) => void;
|
||||
onImageWatermarkRemove?: (imageUrl: string) => void;
|
||||
isProcessingEnhance?: boolean;
|
||||
isProcessingRmbg?: boolean;
|
||||
isProcessingExtend?: boolean;
|
||||
isProcessingAngleEdit?: boolean;
|
||||
isProcessingWatermarkRemove?: boolean;
|
||||
}) => {
|
||||
const [mode, setMode] = useState<"view" | "edit">("view");
|
||||
|
||||
@ -312,10 +316,12 @@ const PurePreviewMessage = ({
|
||||
onImageRmbg={onImageRmbg}
|
||||
onImageExtend={onImageExtend}
|
||||
onImageAngleEdit={onImageAngleEdit}
|
||||
onImageWatermarkRemove={onImageWatermarkRemove}
|
||||
isProcessingEnhance={isProcessingEnhance}
|
||||
isProcessingRmbg={isProcessingRmbg}
|
||||
isProcessingExtend={isProcessingExtend}
|
||||
isProcessingAngleEdit={isProcessingAngleEdit}
|
||||
isProcessingWatermarkRemove={isProcessingWatermarkRemove}
|
||||
>
|
||||
{sanitizeText(part.content)}
|
||||
</Response>
|
||||
|
||||
@ -42,10 +42,12 @@ type MessagesProps = {
|
||||
onImageRmbg?: (imageUrl: string) => void;
|
||||
onImageExtend?: (imageUrl: string) => void;
|
||||
onImageAngleEdit?: (imageUrl: string) => void;
|
||||
onImageWatermarkRemove?: (imageUrl: string) => void;
|
||||
isProcessingEnhance?: boolean;
|
||||
isProcessingRmbg?: boolean;
|
||||
isProcessingExtend?: boolean;
|
||||
isProcessingAngleEdit?: boolean;
|
||||
isProcessingWatermarkRemove?: boolean;
|
||||
pendingGeneration?: PendingGeneration | null; // 🆕 持久化的生成状态
|
||||
imageCount?: number; // 🆕 用户选择的图片数量
|
||||
isGeneratingImageFlag?: boolean; // 🆕 正在生成图片的标志(来自 Chat 组件)
|
||||
@ -67,10 +69,12 @@ function PureMessages({
|
||||
onImageRmbg,
|
||||
onImageExtend,
|
||||
onImageAngleEdit,
|
||||
onImageWatermarkRemove,
|
||||
isProcessingEnhance = false,
|
||||
isProcessingRmbg = false,
|
||||
isProcessingExtend = false,
|
||||
isProcessingAngleEdit = false,
|
||||
isProcessingWatermarkRemove = false,
|
||||
pendingGeneration,
|
||||
imageCount = 1,
|
||||
isGeneratingImageFlag = false,
|
||||
@ -402,10 +406,12 @@ function PureMessages({
|
||||
onImageRmbg={onImageRmbg}
|
||||
onImageExtend={onImageExtend}
|
||||
onImageAngleEdit={onImageAngleEdit}
|
||||
onImageWatermarkRemove={onImageWatermarkRemove}
|
||||
isProcessingEnhance={isProcessingEnhance}
|
||||
isProcessingRmbg={isProcessingRmbg}
|
||||
isProcessingExtend={isProcessingExtend}
|
||||
isProcessingAngleEdit={isProcessingAngleEdit}
|
||||
isProcessingWatermarkRemove={isProcessingWatermarkRemove}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user