- 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
100 lines
2.7 KiB
TypeScript
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>
|
|
);
|
|
}
|