优化拖拽

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 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 { uploadFile } from '@/services/files'
@@ -23,6 +23,8 @@ type GlobalPasteTarget = {
uploadImages: (files: File[]) => void
}
type UploadSource = 'default' | 'drag'
let nextTargetId = 1
const globalPasteTargets: GlobalPasteTarget[] = []
let globalPasteListenerAttached = false
@@ -62,6 +64,8 @@ export default function ImageUpload({
captureGlobalPaste = false,
}: ImageUploadProps) {
const { message } = App.useApp()
const [isDragging, setIsDragging] = useState(false)
const dragCounterRef = useRef(0)
const targetIdRef = useRef(0)
const valueRef = useRef(value)
valueRef.current = value
@@ -74,7 +78,10 @@ export default function ImageUpload({
thumbUrl: file.thumbnailUrl || file.url,
}))
async function uploadImages(images: File[]) {
async function uploadImages(
images: File[],
source: UploadSource = 'default',
) {
const remaining = maxCount - valueRef.current.length
if (remaining <= 0) {
message.warning(`最多上传 ${maxCount} 张图片`)
@@ -86,7 +93,7 @@ export default function ImageUpload({
const uploaded = await uploadFile(file, scene, scope)
next.push(uploaded)
} catch (error) {
message.error(error instanceof Error ? error.message : '上传失败')
message.error(resolveUploadErrorMessage(error, source))
}
}
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/'))
if (images.length === 0) return
await uploadImages(images)
await uploadImages(images, source)
}
function handlePaste(event: ClipboardEvent<HTMLDivElement>) {
@@ -108,10 +118,42 @@ export default function ImageUpload({
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>) {
event.preventDefault()
event.stopPropagation()
dragCounterRef.current = 0
setIsDragging(false)
const files = Array.from(event.dataTransfer?.files || [])
if (files.length > 0) void uploadFiles(files)
if (files.length > 0) {
void uploadFiles(files, 'drag')
}
}
function activateGlobalPasteTarget() {
@@ -157,13 +199,22 @@ export default function ImageUpload({
return (
<div
className="image-upload-droppable"
className={`image-upload-droppable ${isDragging ? 'is-dragging' : ''}`}
onPaste={handlePaste}
onDrop={handleDrop}
onDragOver={(event) => event.preventDefault()}
onDropCapture={handleDrop}
onDragEnterCapture={handleDragEnter}
onDragOverCapture={handleDragOver}
onDragLeaveCapture={handleDragLeave}
onFocusCapture={handleWrapperFocus}
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
accept="image/jpeg,image/png,image/webp"
multiple
@@ -196,3 +247,11 @@ export default function ImageUpload({
</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 {
position: relative;
display: grid;
gap: 8px;
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 {