2025-09-22 20:05:15 +08:00

173 lines
5.0 KiB
TypeScript

/**
* 编辑输入框组件
* 实现编辑描述输入功能,支持键盘操作和动画效果
*/
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Send, X, Loader2 } from 'lucide-react';
import { EditPoint, EditPointStatus } from './types';
interface EditInputProps {
/** 编辑点数据 */
editPoint: EditPoint;
/** 输入框位置 */
position: { x: number; y: number };
/** 是否显示 */
isVisible: boolean;
/** 是否正在提交 */
isSubmitting?: boolean;
/** 提交回调 */
onSubmit: (description: string) => void;
/** 取消回调 */
onCancel: () => void;
/** 输入框尺寸 */
size?: { width: number; height: number };
/** 占位符文本 */
placeholder?: string;
}
/**
* 编辑输入框组件
*/
export const EditInput: React.FC<EditInputProps> = ({
editPoint,
position,
isVisible,
isSubmitting = false,
onSubmit,
onCancel,
size = { width: 300, height: 50 },
placeholder = "Describe your edit..."
}) => {
const [description, setDescription] = useState(editPoint.description || '');
const textareaRef = useRef<HTMLTextAreaElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
// 自动聚焦
useEffect(() => {
if (isVisible && textareaRef.current) {
// 延迟聚焦,等待动画完成
const timer = setTimeout(() => {
textareaRef.current?.focus();
}, 300);
return () => clearTimeout(timer);
}
}, [isVisible]);
// 键盘事件处理
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
onCancel();
} else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
handleSubmit();
}
}, [onCancel]);
// 提交处理
const handleSubmit = useCallback(() => {
const trimmedDescription = description.trim();
if (trimmedDescription && !isSubmitting) {
onSubmit(trimmedDescription);
}
}, [description, isSubmitting, onSubmit]);
// 点击外部关闭
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
// 如果有内容,自动提交;否则取消
if (description.trim()) {
handleSubmit();
} else {
onCancel();
}
}
};
if (isVisible) {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}
}, [isVisible, description, handleSubmit, onCancel]);
// 自动调整高度
const adjustHeight = useCallback(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 120)}px`;
}
}, []);
useEffect(() => {
adjustHeight();
}, [description, adjustHeight]);
return (
<AnimatePresence>
{isVisible && (
<motion.div
ref={containerRef}
className="absolute z-30"
data-edit-input="true"
style={{
left: position.x,
top: position.y,
}}
initial={{
opacity: 0,
scale: 0.8,
y: -10
}}
animate={{
opacity: 1,
scale: 1,
y: 0
}}
exit={{
opacity: 0,
scale: 0.8,
y: -10
}}
transition={{
type: "spring",
stiffness: 300,
damping: 25,
duration: 0.3
}}
>
{/* Input interface - focused on input functionality only */}
<div className="flex items-center bg-white/90 backdrop-blur-sm rounded-lg px-3 py-2 shadow-lg">
<input
ref={textareaRef as any}
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={isSubmitting}
className="flex-1 bg-transparent text-gray-800 placeholder-gray-400 text-sm border-none outline-none min-w-[200px]"
autoFocus
/>
{/* Submit button */}
<button
onClick={handleSubmit}
disabled={!description.trim() || isSubmitting}
className="ml-2 w-8 h-8 rounded-full bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed text-white flex items-center justify-center flex-shrink-0"
>
{isSubmitting ? (
<div className="w-3 h-3 border border-white border-t-transparent rounded-full animate-spin"></div>
) : (
<span className="text-xs"></span>
)}
</button>
</div>
</motion.div>
)}
</AnimatePresence>
);
};