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:
Your Name 2026-02-01 12:18:16 +08:00
parent a779555f3f
commit 057014900c
4 changed files with 193 additions and 14 deletions

View File

@ -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(),
});

View File

@ -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("");

View File

@ -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>
</>
);
}

View File

@ -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: '尺寸调整失败' };
}
},