优化拖拽

This commit is contained in:
yml2213
2026-08-16 14:38:10 +08:00
parent 4dd94cde27
commit c4e7e67115
2 changed files with 134 additions and 10 deletions
@@ -1,7 +1,7 @@
import { PlusOutlined } from '@ant-design/icons' import { CloudUploadOutlined, PlusOutlined } from '@ant-design/icons'
import { App, Typography, Upload } from 'antd' import { App, Typography, Upload } from 'antd'
import type { UploadFile, UploadProps } from 'antd' import type { UploadFile, UploadProps } from 'antd'
import { useEffect, useRef } from 'react' import { useEffect, useRef, useState } from 'react'
import type { ClipboardEvent, DragEvent, FocusEvent, MouseEvent } from 'react' import type { ClipboardEvent, DragEvent, FocusEvent, MouseEvent } from 'react'
import { uploadFile } from '@/services/files' import { uploadFile } from '@/services/files'
@@ -23,6 +23,8 @@ type GlobalPasteTarget = {
uploadImages: (files: File[]) => void uploadImages: (files: File[]) => void
} }
type UploadSource = 'default' | 'drag'
let nextTargetId = 1 let nextTargetId = 1
const globalPasteTargets: GlobalPasteTarget[] = [] const globalPasteTargets: GlobalPasteTarget[] = []
let globalPasteListenerAttached = false let globalPasteListenerAttached = false
@@ -62,6 +64,8 @@ export default function ImageUpload({
captureGlobalPaste = false, captureGlobalPaste = false,
}: ImageUploadProps) { }: ImageUploadProps) {
const { message } = App.useApp() const { message } = App.useApp()
const [isDragging, setIsDragging] = useState(false)
const dragCounterRef = useRef(0)
const targetIdRef = useRef(0) const targetIdRef = useRef(0)
const valueRef = useRef(value) const valueRef = useRef(value)
valueRef.current = value valueRef.current = value
@@ -74,7 +78,10 @@ export default function ImageUpload({
thumbUrl: file.thumbnailUrl || file.url, thumbUrl: file.thumbnailUrl || file.url,
})) }))
async function uploadImages(images: File[]) { async function uploadImages(
images: File[],
source: UploadSource = 'default',
) {
const remaining = maxCount - valueRef.current.length const remaining = maxCount - valueRef.current.length
if (remaining <= 0) { if (remaining <= 0) {
message.warning(`最多上传 ${maxCount} 张图片`) message.warning(`最多上传 ${maxCount} 张图片`)
@@ -86,7 +93,7 @@ export default function ImageUpload({
const uploaded = await uploadFile(file, scene, scope) const uploaded = await uploadFile(file, scene, scope)
next.push(uploaded) next.push(uploaded)
} catch (error) { } catch (error) {
message.error(error instanceof Error ? error.message : '上传失败') message.error(resolveUploadErrorMessage(error, source))
} }
} }
if (next.length > valueRef.current.length) { if (next.length > valueRef.current.length) {
@@ -95,10 +102,13 @@ export default function ImageUpload({
} }
} }
async function uploadFiles(files: File[]) { async function uploadFiles(
files: File[],
source: UploadSource = 'default',
) {
const images = files.filter((file) => file.type.startsWith('image/')) const images = files.filter((file) => file.type.startsWith('image/'))
if (images.length === 0) return if (images.length === 0) return
await uploadImages(images) await uploadImages(images, source)
} }
function handlePaste(event: ClipboardEvent<HTMLDivElement>) { function handlePaste(event: ClipboardEvent<HTMLDivElement>) {
@@ -108,10 +118,42 @@ export default function ImageUpload({
void uploadFiles(files) void uploadFiles(files)
} }
function handleDragEnter(event: DragEvent<HTMLDivElement>) {
event.preventDefault()
event.stopPropagation()
dragCounterRef.current += 1
if (event.dataTransfer?.types?.includes('Files')) {
setIsDragging(true)
}
}
function handleDragOver(event: DragEvent<HTMLDivElement>) {
event.preventDefault()
event.stopPropagation()
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy'
}
}
function handleDragLeave(event: DragEvent<HTMLDivElement>) {
event.preventDefault()
event.stopPropagation()
dragCounterRef.current -= 1
if (dragCounterRef.current <= 0) {
dragCounterRef.current = 0
setIsDragging(false)
}
}
function handleDrop(event: DragEvent<HTMLDivElement>) { function handleDrop(event: DragEvent<HTMLDivElement>) {
event.preventDefault() event.preventDefault()
event.stopPropagation()
dragCounterRef.current = 0
setIsDragging(false)
const files = Array.from(event.dataTransfer?.files || []) const files = Array.from(event.dataTransfer?.files || [])
if (files.length > 0) void uploadFiles(files) if (files.length > 0) {
void uploadFiles(files, 'drag')
}
} }
function activateGlobalPasteTarget() { function activateGlobalPasteTarget() {
@@ -157,13 +199,22 @@ export default function ImageUpload({
return ( return (
<div <div
className="image-upload-droppable" className={`image-upload-droppable ${isDragging ? 'is-dragging' : ''}`}
onPaste={handlePaste} onPaste={handlePaste}
onDrop={handleDrop} onDropCapture={handleDrop}
onDragOver={(event) => event.preventDefault()} onDragEnterCapture={handleDragEnter}
onDragOverCapture={handleDragOver}
onDragLeaveCapture={handleDragLeave}
onFocusCapture={handleWrapperFocus} onFocusCapture={handleWrapperFocus}
onClickCapture={handleWrapperClick} onClickCapture={handleWrapperClick}
> >
{isDragging && (
<div className="image-upload-drag-overlay">
<CloudUploadOutlined className="image-upload-drag-icon" />
<span className="image-upload-drag-text"></span>
<span className="image-upload-drag-subtext"></span>
</div>
)}
<Upload <Upload
accept="image/jpeg,image/png,image/webp" accept="image/jpeg,image/png,image/webp"
multiple multiple
@@ -196,3 +247,11 @@ export default function ImageUpload({
</div> </div>
) )
} }
function resolveUploadErrorMessage(error: unknown, source: UploadSource) {
const errorMessage = error instanceof Error ? error.message : '上传失败'
if (source === 'drag' && /网络连接错误|Network Error/i.test(errorMessage)) {
return '拖拽图片读取失败,请复制图片后粘贴,或保存到本地后点击上传'
}
return errorMessage
}
+65
View File
@@ -232,9 +232,74 @@ select {
} }
.image-upload-droppable { .image-upload-droppable {
position: relative;
display: grid; display: grid;
gap: 8px; gap: 8px;
justify-items: start; justify-items: start;
border-radius: 10px;
padding: 6px;
margin: -6px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.image-upload-droppable.is-dragging {
background: var(--theme-primary-soft, #fff3e8);
}
.image-upload-drag-overlay {
position: absolute;
inset: 0;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(4px);
border: 2px dashed var(--theme-primary, #ff6a00);
border-radius: 10px;
color: var(--theme-primary, #ff6a00);
pointer-events: none;
animation: uploadFadeIn 0.15s ease-out;
box-shadow: 0 8px 24px var(--theme-primary-shadow, rgba(255, 106, 0, 0.14));
}
.image-upload-drag-icon {
font-size: 32px;
color: var(--theme-primary, #ff6a00);
animation: uploadBounce 0.8s infinite alternate ease-in-out;
}
.image-upload-drag-text {
font-size: 13px;
font-weight: 600;
color: #1e293b;
}
.image-upload-drag-subtext {
font-size: 11px;
color: #64748b;
}
@keyframes uploadFadeIn {
from {
opacity: 0;
transform: scale(0.98);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes uploadBounce {
from {
transform: translateY(0);
}
to {
transform: translateY(-4px);
}
} }
.image-upload-hint { .image-upload-hint {