优化拖拽
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user