Your Name c7ffcda87b
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add infinite canvas feature with AI image generation
- Add canvas project and node database tables
- Create canvas workspace with drag, resize, zoom and pan
- Add ImageNode with background removal, enhance, extend, angle edit, scene fusion
- Add VideoNode with playback controls
- Add TextNode for text editing
- Integrate AI image generation API (Gemini/Alibaba)
- Implement viewport culling for performance optimization
- Add canvas entry in top navigation bar
2026-01-24 19:48:13 +08:00

100 lines
2.7 KiB
TypeScript

"use client";
import React, { useState, useRef, useEffect } from "react";
import { useCanvasStore } from "@/store/canvas";
import type { CanvasNode, TextNodeData } from "@/types/canvas";
import { cn } from "@/lib/utils";
interface TextNodeProps {
node: CanvasNode;
isSelected: boolean;
}
export function TextNode({ node, isSelected }: TextNodeProps) {
const data = node.data as TextNodeData;
const [isEditing, setIsEditing] = useState(false);
const [editContent, setEditContent] = useState(data.content);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const updateNode = useCanvasStore((state) => state.updateNode);
// 进入编辑模式时聚焦
useEffect(() => {
if (isEditing && textareaRef.current) {
textareaRef.current.focus();
textareaRef.current.select();
}
}, [isEditing]);
// 保存编辑
const handleSave = () => {
updateNode(node.id, {
data: { ...data, content: editContent },
});
setIsEditing(false);
};
// 取消编辑
const handleCancel = () => {
setEditContent(data.content);
setIsEditing(false);
};
// 双击进入编辑
const handleDoubleClick = (e: React.MouseEvent) => {
e.stopPropagation();
setIsEditing(true);
};
return (
<div
className={cn(
"relative w-full h-full bg-card rounded-lg overflow-hidden border shadow-sm p-3",
isSelected && "border-primary"
)}
onDoubleClick={handleDoubleClick}
>
{isEditing ? (
<textarea
ref={textareaRef}
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
onBlur={handleSave}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
handleSave();
}
if (e.key === 'Escape') {
handleCancel();
}
}}
className="w-full h-full resize-none bg-transparent outline-none text-sm"
style={{
fontSize: data.fontSize || 14,
color: data.color || 'inherit',
fontWeight: data.fontWeight || 'normal',
}}
/>
) : (
<div
className="w-full h-full overflow-auto text-sm whitespace-pre-wrap"
style={{
fontSize: data.fontSize || 14,
color: data.color || 'inherit',
fontWeight: data.fontWeight || 'normal',
}}
>
{data.content || '双击编辑文本'}
</div>
)}
{/* 编辑提示 */}
{!isEditing && isSelected && (
<div className="absolute bottom-1 right-1 text-[10px] text-muted-foreground">
双击编辑
</div>
)}
</div>
);
}