131 lines
4.0 KiB
TypeScript
131 lines
4.0 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);
|
|
};
|
|
|
|
const containerStyle = data.transparent
|
|
? undefined
|
|
: {
|
|
backgroundColor: data.backgroundColor || undefined,
|
|
borderColor: data.borderColor || undefined,
|
|
borderWidth:
|
|
typeof data.borderWidth === "number" ? `${data.borderWidth}px` : undefined,
|
|
borderStyle:
|
|
typeof data.borderWidth === "number" && data.borderWidth > 0 ? "solid" : undefined,
|
|
borderRadius:
|
|
typeof data.borderRadius === "number" ? `${data.borderRadius}px` : undefined,
|
|
padding: typeof data.padding === "number" ? `${data.padding}px` : undefined,
|
|
boxShadow: data.boxShadow || undefined,
|
|
backdropFilter:
|
|
typeof data.backdropBlur === "number" && data.backdropBlur > 0
|
|
? `blur(${data.backdropBlur}px)`
|
|
: undefined,
|
|
WebkitBackdropFilter:
|
|
typeof data.backdropBlur === "number" && data.backdropBlur > 0
|
|
? `blur(${data.backdropBlur}px)`
|
|
: undefined,
|
|
};
|
|
|
|
const textStyle = {
|
|
fontSize: data.fontSize || 14,
|
|
fontFamily: data.fontFamily || undefined,
|
|
color: data.color || "inherit",
|
|
fontWeight: data.fontWeight || "normal",
|
|
lineHeight: data.lineHeight || 1.35,
|
|
letterSpacing:
|
|
typeof data.letterSpacing === "number" ? `${data.letterSpacing}px` : undefined,
|
|
textAlign: data.textAlign || "left",
|
|
textTransform: data.textTransform || "none",
|
|
} as const;
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"relative w-full h-full overflow-hidden p-3",
|
|
data.transparent
|
|
? "rounded-none border-0 bg-transparent p-0 shadow-none"
|
|
: "rounded-lg border bg-card shadow-sm",
|
|
isSelected && (data.transparent ? "ring-1 ring-primary/50" : "border-primary")
|
|
)}
|
|
onDoubleClick={handleDoubleClick}
|
|
style={containerStyle}
|
|
>
|
|
{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={textStyle}
|
|
/>
|
|
) : (
|
|
<div
|
|
className="w-full h-full overflow-auto text-sm whitespace-pre-wrap"
|
|
style={textStyle}
|
|
>
|
|
{data.content || '双击编辑文本'}
|
|
</div>
|
|
)}
|
|
|
|
{/* 编辑提示 */}
|
|
{!isEditing && isSelected && (
|
|
<div className="absolute bottom-1 right-1 text-[10px] text-muted-foreground">
|
|
双击编辑
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|