gongxiangp2/components/upload/drag-drop-utils.ts

126 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type React from 'react'
const FALLBACK_IMAGE_EXTENSIONS = new Set([
'avif',
'bmp',
'gif',
'jpeg',
'jpg',
'png',
'webp',
])
export interface ImageFileValidationOptions {
maxBytes?: number
maxCount?: number
existingCount?: number
/**
* 可选的业务格式白名单。浏览器的 image/* MIME 范围比实际处理能力更宽,
* 需要在特定上传链路中按扩展名提前拦截不兼容格式。
*/
allowedExtensions?: readonly string[]
}
export interface ImageFileValidationResult {
acceptedFiles: File[]
errors: string[]
}
export function hasDragLeftDropZone(event: React.DragEvent<Element>): boolean {
const nextTarget = event.relatedTarget as Node | null
if (nextTarget && nextTarget !== event.currentTarget) {
return !event.currentTarget.contains(nextTarget)
}
const rect = event.currentTarget.getBoundingClientRect()
const { clientX, clientY } = event
return clientX <= rect.left || clientX >= rect.right || clientY <= rect.top || clientY >= rect.bottom
}
export function prepareFileDrag(event: React.DragEvent<Element>, disabled = false): void {
event.preventDefault()
event.stopPropagation()
event.dataTransfer.dropEffect = disabled ? 'none' : 'copy'
}
/**
* 让拖拽上传区域也能通过键盘打开文件选择器,避免只能用鼠标点击。
*/
export function handleDropZoneKeyDown(
event: React.KeyboardEvent<HTMLElement>,
openFilePicker: () => void,
): void {
if (event.target !== event.currentTarget) return
if (event.key !== 'Enter' && event.key !== ' ') return
event.preventDefault()
openFilePicker()
}
export function isSupportedImageFile(file: File): boolean {
if (file.type) {
return file.type.startsWith('image/')
}
const extension = getFileExtension(file)
return FALLBACK_IMAGE_EXTENSIONS.has(extension)
}
export function getFileExtension(file: File): string {
return file.name.split('.').pop()?.toLowerCase() || ''
}
export function hasAllowedFileExtension(file: File, allowedExtensions: readonly string[]): boolean {
const extension = getFileExtension(file)
return allowedExtensions.some((allowedExtension) => allowedExtension.replace(/^\./, '').toLowerCase() === extension)
}
export function validateImageFiles(
files: FileList | File[],
options: ImageFileValidationOptions = {},
): ImageFileValidationResult {
const { maxBytes, maxCount, existingCount = 0, allowedExtensions } = options
const unsupportedNames: string[] = []
const oversizedNames: string[] = []
const validFiles: File[] = []
for (const file of Array.from(files)) {
const isValidImage = allowedExtensions
? hasAllowedFileExtension(file, allowedExtensions)
: isSupportedImageFile(file)
if (!isValidImage) {
unsupportedNames.push(file.name)
continue
}
if (maxBytes !== undefined && file.size > maxBytes) {
oversizedNames.push(file.name)
continue
}
validFiles.push(file)
}
const remainingCount = maxCount === undefined
? validFiles.length
: Math.max(0, maxCount - existingCount)
const acceptedFiles = validFiles.slice(0, remainingCount)
const ignoredByCount = Math.max(0, validFiles.length - acceptedFiles.length)
const errors: string[] = []
if (unsupportedNames.length > 0) {
const supportedLabel = allowedExtensions?.length
? `,请上传 ${allowedExtensions.map((extension) => extension.replace(/^\./, '').toUpperCase()).join('、')} 格式`
: ''
errors.push(`以下文件不是支持的图片格式:${unsupportedNames.join('、')}${supportedLabel}`)
}
if (oversizedNames.length > 0 && maxBytes !== undefined) {
const maxMegabytes = Math.round(maxBytes / 1024 / 1024)
errors.push(`以下图片超过 ${maxMegabytes}MB:${oversizedNames.join('、')}`)
}
if (ignoredByCount > 0 && maxCount !== undefined) {
errors.push(`最多只能选择 ${maxCount} 张图片,已忽略 ${ignoredByCount} 张`)
}
return { acceptedFiles, errors }
}