feat(chat): add paste and drag-drop file upload support
- Add clipboard paste handler for images and files (Ctrl+V / Cmd+V) - Add drag-and-drop zone with visual feedback - Update placeholder text to inform users about new features - Improve file upload UX with drop overlay Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2570e4add9
commit
8a39505ee6
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useRef, useEffect, KeyboardEvent, ChangeEvent } from 'react';
|
||||
import { Send, Paperclip, X, Image, FileText, Loader2 } from 'lucide-react';
|
||||
import { useState, useRef, useEffect, KeyboardEvent, ChangeEvent, ClipboardEvent, DragEvent } from 'react';
|
||||
import { Send, Paperclip, X, Image, FileText, Loader2, Upload } from 'lucide-react';
|
||||
import { clsx } from 'clsx';
|
||||
import { FileAttachment } from '../stores/chatStore';
|
||||
|
||||
|
|
@ -49,8 +49,10 @@ export function InputArea({
|
|||
uploadProgress = {},
|
||||
}: InputAreaProps) {
|
||||
const [message, setMessage] = useState('');
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const dropZoneRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Auto-resize textarea
|
||||
useEffect(() => {
|
||||
|
|
@ -105,6 +107,84 @@ export function InputArea({
|
|||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
// 处理文件验证和添加
|
||||
const processFiles = (files: File[]) => {
|
||||
const validFiles: File[] = [];
|
||||
files.forEach((file) => {
|
||||
if (!ALLOWED_TYPES.includes(file.type)) {
|
||||
console.warn(`不支持的文件类型: ${file.type}`);
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
console.warn(`文件过大: ${file.name}`);
|
||||
return;
|
||||
}
|
||||
validFiles.push(file);
|
||||
});
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
onFilesSelected?.(validFiles);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理粘贴事件
|
||||
const handlePaste = (e: ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
const items = e.clipboardData?.items;
|
||||
if (!items) return;
|
||||
|
||||
const files: File[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
e.preventDefault(); // 阻止默认粘贴行为
|
||||
processFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
// 拖放事件处理
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!disabled && !isUploading) {
|
||||
setIsDragging(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// 只有当离开整个drop zone时才取消高亮
|
||||
if (dropZoneRef.current && !dropZoneRef.current.contains(e.relatedTarget as Node)) {
|
||||
setIsDragging(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
|
||||
if (disabled || isUploading) return;
|
||||
|
||||
const files = Array.from(e.dataTransfer?.files || []);
|
||||
if (files.length > 0) {
|
||||
processFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
|
|
@ -112,7 +192,27 @@ export function InputArea({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div
|
||||
ref={dropZoneRef}
|
||||
className={clsx(
|
||||
'space-y-3 relative rounded-xl transition-all',
|
||||
isDragging && 'ring-2 ring-primary-500 ring-offset-2 bg-primary-50',
|
||||
)}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{/* 拖放提示层 */}
|
||||
{isDragging && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-primary-50/90 rounded-xl border-2 border-dashed border-primary-400">
|
||||
<div className="flex flex-col items-center gap-2 text-primary-600">
|
||||
<Upload className="w-8 h-8" />
|
||||
<span className="font-medium">释放以添加文件</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文件预览区域 */}
|
||||
{pendingFiles.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 px-1">
|
||||
|
|
@ -208,10 +308,11 @@ export function InputArea({
|
|||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
placeholder={
|
||||
pendingFiles.length > 0
|
||||
? '添加说明文字(可选),或直接发送...'
|
||||
: '输入您的问题...'
|
||||
: '输入您的问题,可粘贴或拖放图片...'
|
||||
}
|
||||
disabled={disabled || isUploading}
|
||||
rows={1}
|
||||
|
|
@ -244,7 +345,7 @@ export function InputArea({
|
|||
|
||||
{/* 提示文字 */}
|
||||
<p className="text-xs text-secondary-400 text-center">
|
||||
支持图片 (JPEG, PNG, GIF) 和文档 (PDF, Word, Excel, 文本),最大 50MB
|
||||
支持粘贴、拖放图片 (JPEG, PNG, GIF) 和文档 (PDF, Word, Excel),最大 50MB
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue