247 lines
7.8 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: 280, height: 120 },
placeholder = "Describe your edit request..."
}) => {
const [description, setDescription] = useState(editPoint.description || '');
const [isFocused, setIsFocused] = useState(false);
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,
width: size.width,
}}
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
}}
>
{/* 玻璃态背景容器 */}
<div className="bg-black/60 backdrop-blur-md rounded-lg border border-white/20 shadow-2xl overflow-hidden">
{/* 头部 */}
<div className="px-3 py-2 border-b border-white/10 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-400"></div>
<span className="text-xs text-white/80 font-medium">
Edit Request
</span>
<span className="text-xs text-white/50">
{Math.floor(editPoint.timestamp)}s
</span>
</div>
<button
onClick={onCancel}
className="w-5 h-5 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center text-white/70 hover:text-white transition-colors"
disabled={isSubmitting}
>
<X size={12} />
</button>
</div>
{/* 输入区域 */}
<div className="p-3">
<textarea
ref={textareaRef}
value={description}
onChange={(e) => setDescription(e.target.value)}
onKeyDown={handleKeyDown}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholder={placeholder}
disabled={isSubmitting}
className="w-full bg-transparent text-white placeholder-white/40 text-sm resize-none border-none outline-none min-h-[60px] max-h-[120px]"
style={{ lineHeight: '1.4' }}
/>
</div>
{/* 底部操作区 */}
<div className="px-3 py-2 border-t border-white/10 flex items-center justify-between">
<div className="flex items-center gap-2 text-xs text-white/50">
<span>Ctrl+Enter to submit</span>
<span></span>
<span>Esc to cancel</span>
</div>
<div className="flex items-center gap-2">
{/* 字符计数 */}
<span className={`text-xs ${
description.length > 500 ? 'text-red-400' : 'text-white/50'
}`}>
{description.length}/500
</span>
{/* 提交按钮 */}
<motion.button
onClick={handleSubmit}
disabled={!description.trim() || isSubmitting || description.length > 500}
className="flex items-center gap-1 px-3 py-1.5 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-500 disabled:cursor-not-allowed text-white text-xs rounded-md transition-colors"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{isSubmitting ? (
<>
<Loader2 size={12} className="animate-spin" />
<span>Submitting...</span>
</>
) : (
<>
<Send size={12} />
<span>Submit</span>
</>
)}
</motion.button>
</div>
</div>
{/* 聚焦时的发光边框 */}
<AnimatePresence>
{isFocused && (
<motion.div
className="absolute inset-0 rounded-lg border-2 border-blue-400/50 pointer-events-none"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
/>
)}
</AnimatePresence>
</div>
{/* 输入框指示箭头(可选) */}
<div
className="absolute w-3 h-3 bg-black/60 border-l border-t border-white/20 transform rotate-45"
style={{
left: 12,
top: -6,
}}
/>
</motion.div>
)}
</AnimatePresence>
);
};