Skip to content

Commit 3685ef5

Browse files
committed
feat: implement global drag-and-drop file upload functionality with animations
1 parent eafb76d commit 3685ef5

File tree

4 files changed

+207
-3
lines changed

4 files changed

+207
-3
lines changed

src/App.jsx

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useCallback } from 'react';
1+
import React, { useState, useCallback, useEffect } from 'react';
22
import { FileUpload } from './components/FileUpload';
33
import { RegexControls } from './components/RegexControls';
44
import { FileList } from './components/FileList';
@@ -19,6 +19,8 @@ function App() {
1919
const [showGradNorm, setShowGradNorm] = useState(false);
2020
const [configModalOpen, setConfigModalOpen] = useState(false);
2121
const [configFile, setConfigFile] = useState(null);
22+
const [globalDragOver, setGlobalDragOver] = useState(false);
23+
const [dragCounter, setDragCounter] = useState(0);
2224

2325
const handleFilesUploaded = useCallback((files) => {
2426
const filesWithDefaults = files.map(file => ({
@@ -37,6 +39,39 @@ function App() {
3739
setUploadedFiles(prev => [...prev, ...filesWithDefaults]);
3840
}, []);
3941

42+
// 全局文件处理函数
43+
const processGlobalFiles = useCallback((files) => {
44+
const fileArray = Array.from(files).filter(file =>
45+
file.type === 'text/plain' || file.name.endsWith('.log') || file.name.endsWith('.txt')
46+
);
47+
48+
if (fileArray.length === 0) return;
49+
50+
const processedFiles = fileArray.map(file => ({
51+
file,
52+
name: file.name,
53+
id: Math.random().toString(36).substr(2, 9),
54+
data: null,
55+
content: null
56+
}));
57+
58+
// Read file contents
59+
Promise.all(
60+
processedFiles.map(fileObj =>
61+
new Promise((resolve) => {
62+
const reader = new FileReader();
63+
reader.onload = (e) => {
64+
fileObj.content = e.target.result;
65+
resolve(fileObj);
66+
};
67+
reader.readAsText(fileObj.file);
68+
})
69+
)
70+
).then(files => {
71+
handleFilesUploaded(files);
72+
});
73+
}, [handleFilesUploaded]);
74+
4075
const handleFileRemove = useCallback((index) => {
4176
setUploadedFiles(prev => prev.filter((_, i) => i !== index));
4277
}, []);
@@ -71,8 +106,110 @@ function App() {
71106
}
72107
}, []);
73108

109+
// 全局拖拽事件处理
110+
const handleGlobalDragEnter = useCallback((e) => {
111+
e.preventDefault();
112+
setDragCounter(prev => prev + 1);
113+
114+
// 检查是否包含文件
115+
if (e.dataTransfer.types.includes('Files')) {
116+
setGlobalDragOver(true);
117+
}
118+
}, []);
119+
120+
const handleGlobalDragOver = useCallback((e) => {
121+
e.preventDefault();
122+
// 设置拖拽效果
123+
e.dataTransfer.dropEffect = 'copy';
124+
}, []);
125+
126+
const handleGlobalDragLeave = useCallback((e) => {
127+
e.preventDefault();
128+
setDragCounter(prev => {
129+
const newCount = prev - 1;
130+
if (newCount === 0) {
131+
setGlobalDragOver(false);
132+
}
133+
return newCount;
134+
});
135+
}, []);
136+
137+
const handleGlobalDrop = useCallback((e) => {
138+
e.preventDefault();
139+
setGlobalDragOver(false);
140+
setDragCounter(0);
141+
142+
if (e.dataTransfer.files.length > 0) {
143+
processGlobalFiles(e.dataTransfer.files);
144+
}
145+
}, [processGlobalFiles]);
146+
147+
// 添加全局拖拽监听器
148+
useEffect(() => {
149+
const handleDragEnter = (e) => handleGlobalDragEnter(e);
150+
const handleDragOver = (e) => handleGlobalDragOver(e);
151+
const handleDragLeave = (e) => handleGlobalDragLeave(e);
152+
const handleDrop = (e) => handleGlobalDrop(e);
153+
154+
document.addEventListener('dragenter', handleDragEnter);
155+
document.addEventListener('dragover', handleDragOver);
156+
document.addEventListener('dragleave', handleDragLeave);
157+
document.addEventListener('drop', handleDrop);
158+
159+
return () => {
160+
document.removeEventListener('dragenter', handleDragEnter);
161+
document.removeEventListener('dragover', handleDragOver);
162+
document.removeEventListener('dragleave', handleDragLeave);
163+
document.removeEventListener('drop', handleDrop);
164+
};
165+
}, [handleGlobalDragEnter, handleGlobalDragOver, handleGlobalDragLeave, handleGlobalDrop]);
166+
74167
return (
75-
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
168+
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 relative">
169+
{/* 全页面拖拽覆盖层 */}
170+
{globalDragOver && (
171+
<div
172+
className="fixed inset-0 bg-blue-600 bg-opacity-95 z-50 flex items-center justify-center backdrop-blur-sm drag-overlay-fade-in"
173+
>
174+
<div
175+
className="bg-white rounded-xl shadow-2xl p-8 text-center max-w-md mx-4 border-4 border-dashed border-blue-300 drag-modal-scale-in"
176+
>
177+
<div className="mb-6">
178+
<div className="relative">
179+
<svg
180+
className="mx-auto h-20 w-20 text-blue-600 drag-icon-bounce"
181+
fill="none"
182+
viewBox="0 0 24 24"
183+
stroke="currentColor"
184+
aria-hidden="true"
185+
>
186+
<path
187+
strokeLinecap="round"
188+
strokeLinejoin="round"
189+
strokeWidth={1.5}
190+
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
191+
/>
192+
</svg>
193+
<div className="absolute -top-2 -right-2 w-6 h-6 bg-green-500 rounded-full flex items-center justify-center">
194+
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
195+
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
196+
</svg>
197+
</div>
198+
</div>
199+
</div>
200+
<h3 className="text-xl font-bold text-gray-900 mb-3">
201+
🎯 释放文件以上传
202+
</h3>
203+
<p className="text-sm text-gray-600 mb-2">
204+
支持 <span className="font-semibold text-blue-600">.log</span><span className="font-semibold text-blue-600">.txt</span> 格式
205+
</p>
206+
<p className="text-xs text-gray-500">
207+
拖拽到页面任意位置即可快速上传日志文件
208+
</p>
209+
</div>
210+
</div>
211+
)}
212+
76213
<div className="w-full px-3 py-3">
77214
<Header />
78215

src/components/FileUpload.jsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,27 @@ export function FileUpload({ onFilesUploaded }) {
3434
});
3535
}, [onFilesUploaded]);
3636

37+
const handleDragEnter = useCallback((e) => {
38+
e.preventDefault();
39+
e.stopPropagation(); // 阻止事件冒泡到全局处理器
40+
setIsDragOver(true);
41+
}, []);
42+
3743
const handleDragOver = useCallback((e) => {
3844
e.preventDefault();
45+
e.stopPropagation(); // 阻止事件冒泡到全局处理器
3946
setIsDragOver(true);
4047
}, []);
4148

4249
const handleDragLeave = useCallback((e) => {
4350
e.preventDefault();
51+
e.stopPropagation(); // 阻止事件冒泡到全局处理器
4452
setIsDragOver(false);
4553
}, []);
4654

4755
const handleDrop = useCallback((e) => {
4856
e.preventDefault();
57+
e.stopPropagation(); // 阻止事件冒泡到全局处理器
4958
setIsDragOver(false);
5059
processFiles(e.dataTransfer.files);
5160
}, [processFiles]);
@@ -67,6 +76,7 @@ export function FileUpload({ onFilesUploaded }) {
6776
className={`drag-area border-2 border-dashed rounded-lg p-4 text-center cursor-pointer ${
6877
isDragOver ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
6978
}`}
79+
onDragEnter={handleDragEnter}
7080
onDragOver={handleDragOver}
7181
onDragLeave={handleDragLeave}
7282
onDrop={handleDrop}

src/components/Header.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function Header() {
1212
role="img"
1313
/>
1414
<h1 className="text-2xl font-bold text-gray-800">
15-
🤖 ML Log Analyzer
15+
Log Analyzer
1616
</h1>
1717
</div>
1818
<p className="text-sm text-gray-600 mb-2" role="doc-subtitle">

src/index.css

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,60 @@ input[type="checkbox"]:focus {
167167
outline: 2px solid #3b82f6;
168168
outline-offset: 2px;
169169
}
170+
171+
/* 全页面拖拽动画 */
172+
@keyframes dragOverlayFadeIn {
173+
from {
174+
opacity: 0;
175+
}
176+
to {
177+
opacity: 1;
178+
}
179+
}
180+
181+
@keyframes dragModalScaleIn {
182+
from {
183+
opacity: 0;
184+
transform: scale(0.95) translateY(10px);
185+
}
186+
to {
187+
opacity: 1;
188+
transform: scale(1) translateY(0);
189+
}
190+
}
191+
192+
@keyframes dragIconBounce {
193+
0%, 20%, 53%, 80%, 100% {
194+
transform: translateY(0);
195+
}
196+
40%, 43% {
197+
transform: translateY(-8px);
198+
}
199+
70% {
200+
transform: translateY(-4px);
201+
}
202+
90% {
203+
transform: translateY(-2px);
204+
}
205+
}
206+
207+
.drag-overlay-fade-in {
208+
animation: dragOverlayFadeIn 0.1s ease-out;
209+
}
210+
211+
.drag-modal-scale-in {
212+
animation: dragModalScaleIn 0.15s ease-out;
213+
}
214+
215+
.drag-icon-bounce {
216+
animation: dragIconBounce 1s infinite;
217+
}
218+
219+
/* 在减少动画偏好设置下禁用拖拽动画 */
220+
@media (prefers-reduced-motion: reduce) {
221+
.drag-overlay-fade-in,
222+
.drag-modal-scale-in,
223+
.drag-icon-bounce {
224+
animation: none !important;
225+
}
226+
}

0 commit comments

Comments
 (0)