mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-06 14:33:11 +08:00
feat: FaImageUpload / FaFileUpload 增加粘贴上传功能
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# FaFileUpload 文件上传组件
|
||||
|
||||
支持拖拽和点击上传的文件上传组件,带进度显示和文件管理功能。
|
||||
支持拖拽、点击和粘贴上传的文件上传组件,带进度显示和文件管理功能。
|
||||
|
||||
## 使用场景
|
||||
|
||||
@@ -60,6 +60,10 @@ interface FileItem {
|
||||
| `onSuccess` | `response: any, file: File` | 单个文件上传成功时触发 |
|
||||
| `onClick` | `fileItem: FileItem, index: number` | 点击文件项时触发 |
|
||||
|
||||
## 粘贴上传
|
||||
|
||||
将鼠标移入当前组件,或通过 `Tab` 聚焦当前组件后,可直接按 `Ctrl+V` / `Cmd+V` 粘贴剪贴板中的文件。粘贴上传会复用组件现有的数量、格式和大小校验逻辑。
|
||||
|
||||
## 示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
Executable → Regular
+142
-35
@@ -49,18 +49,83 @@ export interface FileItem {
|
||||
file?: File
|
||||
}
|
||||
|
||||
const containerRef = useTemplateRef<HTMLElement>('containerRef')
|
||||
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
|
||||
const isDragging = ref(false)
|
||||
const isHoveringContainer = ref(false)
|
||||
const isContainerFocused = ref(false)
|
||||
const isMaxReached = computed(() => props.max > 0 && fileList.value.length >= props.max)
|
||||
const canHandlePaste = computed(() =>
|
||||
!props.disabled
|
||||
&& !isMaxReached.value
|
||||
&& (isHoveringContainer.value || isContainerFocused.value),
|
||||
)
|
||||
const extAliasMap: Record<string, string[]> = {
|
||||
'jpeg': ['jpg', 'jpeg'],
|
||||
'jpg': ['jpg', 'jpeg'],
|
||||
'plain': ['txt'],
|
||||
'msword': ['doc'],
|
||||
'vnd.openxmlformats-officedocument.wordprocessingml.document': ['docx'],
|
||||
'vnd.ms-excel': ['xls'],
|
||||
'vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['xlsx'],
|
||||
'vnd.ms-powerpoint': ['ppt'],
|
||||
'vnd.openxmlformats-officedocument.presentationml.presentation': ['pptx'],
|
||||
'x-zip-compressed': ['zip'],
|
||||
'zip': ['zip'],
|
||||
'x-rar-compressed': ['rar'],
|
||||
'vnd.rar': ['rar'],
|
||||
'rar': ['rar'],
|
||||
'x-7z-compressed': ['7z'],
|
||||
}
|
||||
const acceptedExts = computed(() => new Set(props.ext.flatMap(ext => normalizeExtVariants(ext))))
|
||||
|
||||
function formatText(template: string, params: Record<string, string | number>) {
|
||||
return Object.entries(params).reduce((result, [key, value]) => {
|
||||
return result.replaceAll(`{${key}}`, String(value))
|
||||
}, template)
|
||||
function normalizeExt(ext?: string) {
|
||||
if (!ext) {
|
||||
return ''
|
||||
}
|
||||
return ext.toLowerCase().trim().replace(/^\./, '').split('+')[0] ?? ''
|
||||
}
|
||||
|
||||
function normalizeExtVariants(ext?: string) {
|
||||
const normalizedExt = normalizeExt(ext)
|
||||
if (!normalizedExt) {
|
||||
return []
|
||||
}
|
||||
return extAliasMap[normalizedExt] ?? [normalizedExt]
|
||||
}
|
||||
|
||||
function getFileExtVariants(file: File) {
|
||||
const extVariants = new Set<string>()
|
||||
const fileNameExt = file.name.split('.').pop()
|
||||
normalizeExtVariants(fileNameExt).forEach(ext => extVariants.add(ext))
|
||||
const mimeExt = file.type.split('/')[1]
|
||||
normalizeExtVariants(mimeExt).forEach(ext => extVariants.add(ext))
|
||||
return extVariants
|
||||
}
|
||||
|
||||
function validateFile(file: File) {
|
||||
if (acceptedExts.value.size > 0) {
|
||||
const fileExtVariants = getFileExtVariants(file)
|
||||
const isAccepted = [...fileExtVariants].some(ext => acceptedExts.value.has(ext))
|
||||
if (!isAccepted) {
|
||||
toast.error(`上传文件只支持 ${props.ext.join(' / ')} 格式`, {
|
||||
description: file.name || file.type,
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (props.size > 0 && file.size > props.size) {
|
||||
toast.error(`上传文件大小不能超过 ${filesize(props.size, { standard: 'jedec' })}`, {
|
||||
description: `${file.name || file.type} (${filesize(file.size, { standard: 'jedec' })})`,
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function handleDragOver(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
if (props.disabled) {
|
||||
if (props.disabled || isMaxReached.value) {
|
||||
return
|
||||
}
|
||||
isDragging.value = true
|
||||
@@ -73,43 +138,74 @@ function handleDragLeave(e: DragEvent) {
|
||||
|
||||
function handleDrop(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
if (props.disabled) {
|
||||
if (props.disabled || isMaxReached.value) {
|
||||
return
|
||||
}
|
||||
isDragging.value = false
|
||||
if (e.dataTransfer?.files) {
|
||||
onSelectFile(e.dataTransfer.files)
|
||||
handleFiles(e.dataTransfer.files)
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectFile(files: FileList | File[] | null) {
|
||||
if (!files) {
|
||||
function onPaste(e: ClipboardEvent) {
|
||||
if (e.defaultPrevented || !canHandlePaste.value) {
|
||||
return
|
||||
}
|
||||
const filesFromItems = [...(e.clipboardData?.items ?? [])]
|
||||
.filter(item => item.kind === 'file')
|
||||
.map(item => item.getAsFile())
|
||||
.filter((file): file is File => file !== null)
|
||||
const files = filesFromItems.length > 0
|
||||
? filesFromItems
|
||||
: [...(e.clipboardData?.files ?? [])]
|
||||
|
||||
if (files.length > 0 && handleFiles(files)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
function onContainerFocusIn() {
|
||||
isContainerFocused.value = true
|
||||
}
|
||||
|
||||
function onContainerFocusOut(e: FocusEvent) {
|
||||
const nextFocusedElement = e.relatedTarget
|
||||
if (nextFocusedElement instanceof Node && containerRef.value?.contains(nextFocusedElement)) {
|
||||
return
|
||||
}
|
||||
isContainerFocused.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('paste', onPaste)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('paste', onPaste)
|
||||
})
|
||||
|
||||
function onSelectFile(files: FileList | File[] | null) {
|
||||
handleFiles(files)
|
||||
}
|
||||
|
||||
function handleFiles(files: FileList | File[] | null) {
|
||||
if (!files || props.disabled || isMaxReached.value) {
|
||||
return false
|
||||
}
|
||||
const selectedFiles = [...files]
|
||||
// 数量限制
|
||||
const remain = props.max > 0 ? props.max - fileList.value.length : selectedFiles.length
|
||||
const filesToAdd: File[] = []
|
||||
for (const file of selectedFiles.slice(0, remain)) {
|
||||
// 类型校验
|
||||
if (props.ext.length > 0) {
|
||||
const ext = file.name.split('.').pop()?.toLowerCase()
|
||||
if (!props.ext.map(e => e.toLowerCase()).includes(ext || '')) {
|
||||
toast.error(formatText('上传文件只支持 {ext} 格式', { ext: props.ext.join(' / ') }))
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 大小校验
|
||||
if (props.size > 0) {
|
||||
if (file.size > props.size) {
|
||||
toast.error(formatText('上传文件大小不能超过 {size}', { size: filesize(props.size, { standard: 'jedec' }) }))
|
||||
continue
|
||||
}
|
||||
}
|
||||
filesToAdd.push(file)
|
||||
if (remain <= 0) {
|
||||
return false
|
||||
}
|
||||
if (fileInputRef.value) {
|
||||
fileInputRef.value.value = ''
|
||||
}
|
||||
const filesToAdd = selectedFiles.slice(0, remain).filter(validateFile)
|
||||
if (filesToAdd.length === 0) {
|
||||
return false
|
||||
}
|
||||
fileInputRef.value!.value = ''
|
||||
filesToAdd.forEach(file => uploadFile(file))
|
||||
return true
|
||||
}
|
||||
|
||||
function uploadFile(file: File, index?: number) {
|
||||
@@ -167,15 +263,23 @@ function removeFile(idx: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="space-y-2"
|
||||
:tabindex="props.disabled ? undefined : 0"
|
||||
@mouseenter="isHoveringContainer = true"
|
||||
@mouseleave="isHoveringContainer = false"
|
||||
@focusin="onContainerFocusIn"
|
||||
@focusout="onContainerFocusOut"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="p-4 border border-2 rounded-lg border-dashed bg-transparent flex flex-col h-40 w-full cursor-pointer transition-all items-center justify-center"
|
||||
:class="{
|
||||
'border-primary bg-primary/5': isDragging,
|
||||
'opacity-50 cursor-not-allowed': props.disabled || (props.max > 0 && fileList.length >= props.max),
|
||||
'opacity-50 cursor-not-allowed': props.disabled || isMaxReached,
|
||||
}"
|
||||
:disabled="props.disabled || (props.max > 0 && fileList.length >= props.max)"
|
||||
:disabled="props.disabled || isMaxReached"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop"
|
||||
@@ -191,13 +295,16 @@ function removeFile(idx: number) {
|
||||
</button>
|
||||
<div v-if="!props.hideTips && !props.disabled" class="text-xs text-card-foreground/50 flex flex-wrap gap-1 empty:hidden">
|
||||
<div v-if="props.ext.length > 0" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ formatText('支持 {ext} 格式', { ext: props.ext.join(' / ') }) }}
|
||||
支持 {{ props.ext.join(' / ') }} 格式
|
||||
</div>
|
||||
<div class="after:content-[';_'] last:after:content-empty">
|
||||
支持粘贴上传
|
||||
</div>
|
||||
<div v-if="props.size > 0" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ formatText('大小不超过 {size}', { size: filesize(props.size, { standard: 'jedec' }) }) }}
|
||||
大小不超过 {{ filesize(props.size, { standard: 'jedec' }) }}
|
||||
</div>
|
||||
<div v-if="props.max > 0" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ formatText('数量不超过 {max} 个', { max: props.max }) }}
|
||||
数量不超过 {{ props.max }} 个
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="fileList.length > 0" class="gap-2 grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))]">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# FaImageUpload 图片上传组件
|
||||
|
||||
专门用于图片上传的组件,支持预览、排序和拖拽上传。
|
||||
专门用于图片上传的组件,支持预览、排序和粘贴上传。
|
||||
|
||||
## 使用场景
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
|--------|------|------|
|
||||
| `onSuccess` | `response: any, file: File` | 单张图片上传成功时触发 |
|
||||
|
||||
## 粘贴上传
|
||||
|
||||
将鼠标移入当前组件,或通过 `Tab` 聚焦当前组件后,可直接按 `Ctrl+V` / `Cmd+V` 粘贴剪贴板中的图片。粘贴上传会复用组件现有的数量、格式和大小校验逻辑。
|
||||
|
||||
## 示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
Executable → Regular
+177
-78
@@ -49,88 +49,170 @@ const emits = defineEmits<{
|
||||
|
||||
const images = defineModel<string[]>('modelValue', { required: true })
|
||||
|
||||
const isUploading = ref(false)
|
||||
const activeUploadCount = ref(0)
|
||||
const uploadProgress = ref(0)
|
||||
const containerRef = useTemplateRef<HTMLElement>('containerRef')
|
||||
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
|
||||
const isHoveringContainer = ref(false)
|
||||
const isContainerFocused = ref(false)
|
||||
const isUploading = computed(() => activeUploadCount.value > 0)
|
||||
const acceptedExts = computed(() => new Set(props.ext.flatMap(ext => normalizeExtVariants(ext))))
|
||||
const canHandlePaste = computed(() =>
|
||||
!props.disabled
|
||||
&& !isUploading.value
|
||||
&& (isHoveringContainer.value || isContainerFocused.value),
|
||||
)
|
||||
|
||||
function formatText(template: string, params: Record<string, string | number>) {
|
||||
return Object.entries(params).reduce((result, [key, value]) => {
|
||||
return result.replaceAll(`{${key}}`, String(value))
|
||||
}, template)
|
||||
function normalizeExt(ext?: string) {
|
||||
if (!ext) {
|
||||
return ''
|
||||
}
|
||||
return ext.toLowerCase().trim().replace(/^\./, '').split('+')[0] ?? ''
|
||||
}
|
||||
|
||||
function normalizeExtVariants(ext?: string) {
|
||||
const normalizedExt = normalizeExt(ext)
|
||||
if (!normalizedExt) {
|
||||
return []
|
||||
}
|
||||
if (normalizedExt === 'jpg' || normalizedExt === 'jpeg') {
|
||||
return ['jpg', 'jpeg']
|
||||
}
|
||||
return [normalizedExt]
|
||||
}
|
||||
|
||||
function getFileExtVariants(file: File) {
|
||||
const extVariants = new Set<string>()
|
||||
const fileNameExt = file.name.split('.').pop()
|
||||
normalizeExtVariants(fileNameExt).forEach(ext => extVariants.add(ext))
|
||||
const mimeExt = file.type.split('/')[1]
|
||||
normalizeExtVariants(mimeExt).forEach(ext => extVariants.add(ext))
|
||||
return extVariants
|
||||
}
|
||||
|
||||
function validateFile(file: File) {
|
||||
if (acceptedExts.value.size > 0) {
|
||||
const fileExtVariants = getFileExtVariants(file)
|
||||
const isAccepted = [...fileExtVariants].some(ext => acceptedExts.value.has(ext))
|
||||
if (!isAccepted) {
|
||||
toast.error(`上传图片只支持 ${props.ext.join(' / ')} 格式`, {
|
||||
description: file.name || file.type,
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (props.size > 0 && file.size > props.size) {
|
||||
toast.error(`上传图片大小不能超过 ${filesize(props.size, { standard: 'jedec' })}`, {
|
||||
description: `${file.name || file.type} (${filesize(file.size, { standard: 'jedec' })})`,
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function uploadFile(file: File) {
|
||||
activeUploadCount.value += 1
|
||||
uploadProgress.value = 0
|
||||
const formData = new FormData()
|
||||
Object.entries(props.data).forEach(([key, value]) => {
|
||||
formData.append(key, value)
|
||||
})
|
||||
formData.append(props.name, file)
|
||||
let headersObj: Record<string, any> = {}
|
||||
if (props.headers instanceof Headers) {
|
||||
props.headers.forEach((value, key) => {
|
||||
headersObj[key] = value
|
||||
})
|
||||
}
|
||||
else {
|
||||
headersObj = { ...props.headers }
|
||||
}
|
||||
return axios({
|
||||
url: props.action,
|
||||
method: props.method,
|
||||
headers: headersObj,
|
||||
data: formData,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
uploadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||
}
|
||||
},
|
||||
}).then(async (response) => {
|
||||
const url = await props.afterUpload?.(response.data)
|
||||
if (url) {
|
||||
images.value.push(url)
|
||||
}
|
||||
emits('onSuccess', response.data, file)
|
||||
}).finally(() => {
|
||||
activeUploadCount.value -= 1
|
||||
if (!isUploading.value) {
|
||||
uploadProgress.value = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onSelectFile(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
if (!input.files) {
|
||||
handleFiles((e.target as HTMLInputElement).files)
|
||||
}
|
||||
|
||||
function onPaste(e: ClipboardEvent) {
|
||||
if (e.defaultPrevented || !canHandlePaste.value) {
|
||||
return
|
||||
}
|
||||
const selectedFiles = [...input.files]
|
||||
const clipboardItems = [...(e.clipboardData?.items ?? [])]
|
||||
const filesFromItems = clipboardItems
|
||||
.filter(item => item.kind === 'file' && item.type.startsWith('image/'))
|
||||
.map(item => item.getAsFile())
|
||||
.filter((file): file is File => file !== null)
|
||||
const files = filesFromItems.length > 0
|
||||
? filesFromItems
|
||||
: [...(e.clipboardData?.files ?? [])].filter(file => file.type.startsWith('image/'))
|
||||
|
||||
if (files.length > 0 && handleFiles(files)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
function onContainerFocusIn() {
|
||||
isContainerFocused.value = true
|
||||
}
|
||||
|
||||
function onContainerFocusOut(e: FocusEvent) {
|
||||
const nextFocusedElement = e.relatedTarget
|
||||
if (nextFocusedElement instanceof Node && containerRef.value?.contains(nextFocusedElement)) {
|
||||
return
|
||||
}
|
||||
isContainerFocused.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('paste', onPaste)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('paste', onPaste)
|
||||
})
|
||||
|
||||
function handleFiles(files: FileList | File[] | null) {
|
||||
if (!files || props.disabled || isUploading.value) {
|
||||
return false
|
||||
}
|
||||
const selectedFiles = [...files]
|
||||
const remain = props.max === 0 ? selectedFiles.length : props.max - images.value.length
|
||||
const filesToAdd: File[] = []
|
||||
for (const file of selectedFiles.slice(0, remain)) {
|
||||
// 类型校验
|
||||
if (props.ext.length > 0) {
|
||||
const ext = file.name.split('.').pop()?.toLowerCase()
|
||||
if (!props.ext.map(e => e.toLowerCase()).includes(ext || '')) {
|
||||
toast.error(formatText('上传图片只支持 {ext} 格式', { ext: props.ext.join(' / ') }), {
|
||||
description: file.name,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 大小校验
|
||||
if (props.size > 0) {
|
||||
if (file.size > props.size) {
|
||||
toast.error(formatText('上传图片大小不能超过 {size}', { size: filesize(props.size, { standard: 'jedec' }) }), {
|
||||
description: `${file.name} (${filesize(file.size, { standard: 'jedec' })})`,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
filesToAdd.push(file)
|
||||
if (remain <= 0) {
|
||||
return false
|
||||
}
|
||||
if (fileInputRef.value) {
|
||||
fileInputRef.value.value = ''
|
||||
}
|
||||
const filesToAdd = selectedFiles.slice(0, remain).filter(validateFile)
|
||||
if (filesToAdd.length === 0) {
|
||||
return false
|
||||
}
|
||||
fileInputRef.value!.value = ''
|
||||
// 上传每个文件
|
||||
filesToAdd.forEach((file) => {
|
||||
isUploading.value = true
|
||||
uploadProgress.value = 0
|
||||
const formData = new FormData()
|
||||
// 附加额外参数
|
||||
Object.entries(props.data).forEach(([key, value]) => {
|
||||
formData.append(key, value)
|
||||
})
|
||||
formData.append(props.name, file)
|
||||
// 处理 headers,确保为普通对象
|
||||
let headersObj: Record<string, any> = {}
|
||||
if (props.headers instanceof Headers) {
|
||||
props.headers.forEach((value, key) => {
|
||||
headersObj[key] = value
|
||||
})
|
||||
}
|
||||
else {
|
||||
headersObj = { ...props.headers }
|
||||
}
|
||||
return axios({
|
||||
url: props.action,
|
||||
method: props.method,
|
||||
headers: headersObj,
|
||||
data: formData,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
uploadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||
}
|
||||
},
|
||||
}).then(async (response) => {
|
||||
const url = await props.afterUpload?.(response.data)
|
||||
if (url) {
|
||||
images.value.push(url)
|
||||
}
|
||||
emits('onSuccess', response.data, file)
|
||||
}).finally(() => {
|
||||
isUploading.value = false
|
||||
uploadProgress.value = 0
|
||||
})
|
||||
void uploadFile(file)
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
function onRemove(idx: number) {
|
||||
@@ -152,7 +234,15 @@ function onMove(index: number, direction: 'forward' | 'backward') {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="space-y-2"
|
||||
:tabindex="props.disabled ? undefined : 0"
|
||||
@mouseenter="isHoveringContainer = true"
|
||||
@mouseleave="isHoveringContainer = false"
|
||||
@focusin="onContainerFocusIn"
|
||||
@focusout="onContainerFocusOut"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="(img, index) in images" :key="img" class="group/image-upload border rounded-lg flex items-center justify-center relative overflow-hidden" :style="{
|
||||
@@ -187,12 +277,18 @@ function onMove(index: number, direction: 'forward' | 'backward') {
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="images.length < props.max || props.max === 0" type="button" class="bg-transparent flex-center relative overflow-hidden" :class="{
|
||||
v-if="images.length < props.max || props.max === 0"
|
||||
type="button"
|
||||
class="bg-transparent flex-center relative overflow-hidden"
|
||||
:class="{
|
||||
'cursor-not-allowed': props.disabled || isUploading,
|
||||
}" :style="{
|
||||
}"
|
||||
:style="{
|
||||
width: `${props.width}px`,
|
||||
height: `${props.height}px`,
|
||||
}" :disabled="props.disabled || isUploading" @click="fileInputRef?.click()"
|
||||
}"
|
||||
:disabled="props.disabled || isUploading"
|
||||
@click="fileInputRef?.click()"
|
||||
>
|
||||
<slot>
|
||||
<div class="text-primary p-0 border border-2 rounded-lg border-dashed flex-center size-full">
|
||||
@@ -207,16 +303,19 @@ function onMove(index: number, direction: 'forward' | 'backward') {
|
||||
</div>
|
||||
<div v-if="!props.hideTips" class="text-xs text-card-foreground/50 flex flex-wrap gap-1 empty:hidden">
|
||||
<div v-if="props.dimension" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ formatText('建议尺寸为 {width}*{height}', { width: props.dimension.width, height: props.dimension.height }) }}
|
||||
建议尺寸为 {{ props.dimension.width }}*{{ props.dimension.height }}
|
||||
</div>
|
||||
<div v-if="props.ext.length > 0" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ formatText('支持 {ext} 格式', { ext: props.ext.join(' / ') }) }}
|
||||
支持 {{ props.ext.join(' / ') }} 格式
|
||||
</div>
|
||||
<div v-if="!props.disabled" class="after:content-[';_'] last:after:content-empty">
|
||||
支持粘贴上传
|
||||
</div>
|
||||
<div v-if="props.size > 0" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ formatText('大小不超过 {size}', { size: filesize(props.size, { standard: 'jedec' }) }) }}
|
||||
大小不超过 {{ filesize(props.size, { standard: 'jedec' }) }}
|
||||
</div>
|
||||
<div v-if="props.max > 1" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ formatText('数量不超过 {max} 个', { max: props.max }) }}
|
||||
数量不超过 {{ props.max }} 个
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user