feat: 完善画布功能按钮与后端 API 对接
1. 修复 CanvasChatPanel 中的图片上传 - 使用 parts 数组而非 attachments 2. 扩展 chat schema 支持更多图片格式(gif, bmp, svg) 3. 实现尺寸调整功能 - 添加对话框选择尺寸 4. 更新 imageGenerationService.resizeImage 使用 canvas API 调整尺寸 5. 所有图片编辑功能已对接:抠图、变清晰、扩展、角度编辑、场景裂变、尺寸调整 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a779555f3f
commit
057014900c
@ -7,7 +7,7 @@ const textPartSchema = z.object({
|
||||
|
||||
const filePartSchema = z.object({
|
||||
type: z.enum(["file"]),
|
||||
mediaType: z.enum(["image/jpeg", "image/png", "image/webp"]),
|
||||
mediaType: z.enum(["image/jpeg", "image/png", "image/webp", "image/gif", "image/jpg", "image/bmp", "image/svg+xml"]),
|
||||
name: z.string().min(1).max(100),
|
||||
url: z.string().url(),
|
||||
});
|
||||
|
||||
@ -252,16 +252,32 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
return;
|
||||
}
|
||||
|
||||
const messageAttachments = attachments.map(a => ({
|
||||
contentType: a.contentType,
|
||||
url: a.url,
|
||||
name: a.name,
|
||||
}));
|
||||
// 构建 parts 数组:文本 + 文件
|
||||
const parts: any[] = [];
|
||||
|
||||
// 添加文本部分
|
||||
if (input.trim()) {
|
||||
parts.push({ type: "text", text: input });
|
||||
}
|
||||
|
||||
// 添加文件部分(作为 file 类型的 part)
|
||||
attachments.forEach(a => {
|
||||
parts.push({
|
||||
type: "file",
|
||||
mediaType: a.contentType,
|
||||
name: a.name,
|
||||
url: a.url,
|
||||
});
|
||||
});
|
||||
|
||||
// 如果没有文本但有附件,添加一个空的文本提示
|
||||
if (!input.trim() && attachments.length > 0) {
|
||||
parts.unshift({ type: "text", text: "请处理这张图片" });
|
||||
}
|
||||
|
||||
sendMessage({
|
||||
role: "user" as const,
|
||||
parts: [{ type: "text", text: input }],
|
||||
attachments: messageAttachments,
|
||||
parts: parts,
|
||||
});
|
||||
|
||||
setInput("");
|
||||
|
||||
@ -10,6 +10,15 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { toast } from "sonner";
|
||||
import { useCanvasStore } from "@/store/canvas";
|
||||
import { imageGenerationService } from "@/services/canvas";
|
||||
@ -29,6 +38,9 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [angleDialogOpen, setAngleDialogOpen] = useState(false);
|
||||
const [sceneFusionDialogOpen, setSceneFusionDialogOpen] = useState(false);
|
||||
const [resizeDialogOpen, setResizeDialogOpen] = useState(false);
|
||||
const [resizeWidth, setResizeWidth] = useState(1024);
|
||||
const [resizeHeight, setResizeHeight] = useState(1024);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
// 文件输入的 ref
|
||||
@ -131,7 +143,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
return;
|
||||
}
|
||||
if (action === 'resize') {
|
||||
toast.info("尺寸调整功能即将上线");
|
||||
setResizeDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -237,6 +249,67 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
});
|
||||
};
|
||||
|
||||
// 尺寸调整执行
|
||||
const handleResize = async () => {
|
||||
if (!data.src) return;
|
||||
|
||||
setResizeDialogOpen(false);
|
||||
|
||||
// 创建处理中节点
|
||||
const newNodeId = addNode({
|
||||
type: 'image',
|
||||
position: {
|
||||
x: node.position.x + node.size.width + 20,
|
||||
y: node.position.y
|
||||
},
|
||||
size: { width: resizeWidth / 3, height: resizeHeight / 3 }, // 按比例显示
|
||||
data: {
|
||||
src: '',
|
||||
status: 'generating',
|
||||
prompt: `尺寸调整 ${resizeWidth}x${resizeHeight}`,
|
||||
originalSrc: data.src,
|
||||
},
|
||||
connections: [node.id],
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await imageGenerationService.resizeImage(data.src, resizeWidth, resizeHeight);
|
||||
|
||||
if (result?.success && result.data?.url) {
|
||||
updateNode(newNodeId, {
|
||||
data: {
|
||||
src: result.data.url,
|
||||
status: 'completed',
|
||||
prompt: `尺寸调整 ${resizeWidth}x${resizeHeight}`,
|
||||
originalSrc: data.src,
|
||||
},
|
||||
});
|
||||
toast.success('尺寸调整完成');
|
||||
} else {
|
||||
updateNode(newNodeId, {
|
||||
data: {
|
||||
src: '',
|
||||
status: 'failed',
|
||||
prompt: '尺寸调整失败',
|
||||
originalSrc: data.src,
|
||||
},
|
||||
});
|
||||
toast.error(result?.error || '尺寸调整失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('尺寸调整失败:', error);
|
||||
updateNode(newNodeId, {
|
||||
data: {
|
||||
src: '',
|
||||
status: 'failed',
|
||||
prompt: '尺寸调整失败',
|
||||
originalSrc: data.src,
|
||||
},
|
||||
});
|
||||
toast.error('尺寸调整失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 下载图片
|
||||
const handleDownload = () => {
|
||||
if (data.src) {
|
||||
@ -470,6 +543,55 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
open={sceneFusionDialogOpen}
|
||||
onOpenChange={setSceneFusionDialogOpen}
|
||||
/>
|
||||
|
||||
{/* 尺寸调整对话框 */}
|
||||
<Dialog open={resizeDialogOpen} onOpenChange={setResizeDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Maximize2 className="h-5 w-5" />
|
||||
尺寸调整
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="width">宽度 (px)</Label>
|
||||
<Input
|
||||
id="width"
|
||||
type="number"
|
||||
value={resizeWidth}
|
||||
onChange={(e) => setResizeWidth(parseInt(e.target.value) || 1024)}
|
||||
min={100}
|
||||
max={4096}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="height">高度 (px)</Label>
|
||||
<Input
|
||||
id="height"
|
||||
type="number"
|
||||
value={resizeHeight}
|
||||
onChange={(e) => setResizeHeight(parseInt(e.target.value) || 1024)}
|
||||
min={100}
|
||||
max={4096}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => { setResizeWidth(512); setResizeHeight(512); }}>512x512</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => { setResizeWidth(1024); setResizeHeight(1024); }}>1024x1024</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => { setResizeWidth(1920); setResizeHeight(1080); }}>1920x1080</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => { setResizeWidth(1080); setResizeHeight(1920); }}>1080x1920</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => { setResizeWidth(2048); setResizeHeight(2048); }}>2048x2048</Button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setResizeDialogOpen(false)}>取消</Button>
|
||||
<Button onClick={handleResize}>确定</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -217,17 +217,58 @@ export const imageGenerationService = {
|
||||
}
|
||||
},
|
||||
|
||||
// 尺寸调整
|
||||
// 尺寸调整 - 通过 URL 下载图片,调整大小后上传到 S3
|
||||
async resizeImage(imageUrl: string, width: number, height: number): Promise<ApiResponse<{ url: string }>> {
|
||||
try {
|
||||
const response = await fetch('/api/resize', {
|
||||
// 1. 下载原图
|
||||
const response = await fetch(imageUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error('下载图片失败');
|
||||
}
|
||||
const blob = await response.blob();
|
||||
|
||||
// 2. 使用 canvas 调整尺寸
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('无法创建 canvas context');
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => reject(new Error('图片加载失败'));
|
||||
img.src = URL.createObjectURL(blob);
|
||||
});
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
// 3. 转换为 base64
|
||||
const base64 = canvas.toDataURL('image/png').split(',')[1];
|
||||
|
||||
// 4. 上传到 S3
|
||||
const uploadResponse = await fetch('/api/resize', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ imageUrl, width, height }),
|
||||
body: JSON.stringify({
|
||||
image_base64: base64,
|
||||
canvasWidth: width,
|
||||
canvasHeight: height,
|
||||
prompt: '尺寸调整',
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
|
||||
const data = await uploadResponse.json();
|
||||
if (data.success && data.original_url) {
|
||||
return { success: true, data: { url: data.original_url } };
|
||||
}
|
||||
return { success: false, error: data.error || '尺寸调整失败' };
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 尺寸调整失败:', error);
|
||||
return { success: false, error: '尺寸调整失败' };
|
||||
}
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user