mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-06 13:43:32 +08:00
feat(FaFileUpload): 增加文件夹上传和自定义上传请求功能
This commit is contained in:
Executable → Regular
+16
-2
@@ -5,6 +5,21 @@ const files = ref<FileItem[]>([
|
||||
{ name: 'logo.svg', size: 1024 * 1024, url: 'https://fantastic-admin.hurui.me/logo.svg' },
|
||||
])
|
||||
|
||||
function beforeUpload(file: File) {
|
||||
const isPng = file.type === 'image/png'
|
||||
const isLt200K = file.size <= 200 * 1024
|
||||
|
||||
if (!isPng) {
|
||||
faToast.error('只能上传 PNG 文件')
|
||||
return false
|
||||
}
|
||||
if (!isLt200K) {
|
||||
faToast.error('文件大小不能超过 200KB')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
faToast.success('模拟上传成功')
|
||||
}
|
||||
@@ -21,9 +36,8 @@ function handleClick(fileItem: FileItem) {
|
||||
v-model="files"
|
||||
action="/fake/upload"
|
||||
:after-upload="(response) => response.data.url"
|
||||
:before-upload="beforeUpload"
|
||||
multiple
|
||||
:ext="['png']"
|
||||
:size="200 * 1024"
|
||||
:max="5"
|
||||
@on-success="handleSuccess"
|
||||
@on-click="handleClick"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import type { FileItem, FileUploadRequestOptions } from '@fantastic-admin/components'
|
||||
|
||||
const files = ref<FileItem[]>([])
|
||||
|
||||
async function httpRequest({ file, onProgress }: FileUploadRequestOptions) {
|
||||
onProgress(30)
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
onProgress(100)
|
||||
|
||||
return {
|
||||
url: URL.createObjectURL(file),
|
||||
name: file.name,
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
faToast.success('自定义上传完成')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
v-model="files"
|
||||
:http-request="httpRequest"
|
||||
:after-upload="response => response.url"
|
||||
:max="3"
|
||||
@on-success="handleSuccess"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { FileItem, FileUploadRequestOptions } from '@fantastic-admin/components'
|
||||
|
||||
const files = ref<FileItem[]>([])
|
||||
|
||||
async function httpRequest({ file, onProgress }: FileUploadRequestOptions) {
|
||||
onProgress(50)
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
onProgress(100)
|
||||
|
||||
return {
|
||||
url: URL.createObjectURL(file),
|
||||
name: file.name,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
v-model="files"
|
||||
directory
|
||||
:max="0"
|
||||
:http-request="httpRequest"
|
||||
:after-upload="response => response.url"
|
||||
/>
|
||||
</template>
|
||||
@@ -2,6 +2,8 @@
|
||||
import Demo1 from './_demo1.vue'
|
||||
import Demo2 from './_demo2.vue'
|
||||
import Demo3 from './_demo3.vue'
|
||||
import Demo4 from './_demo4.vue'
|
||||
import Demo5 from './_demo5.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -16,5 +18,11 @@ import Demo3 from './_demo3.vue'
|
||||
<FaPageMain title="自定义显示" main-class="p-4">
|
||||
<Demo3 />
|
||||
</FaPageMain>
|
||||
<FaPageMain title="自定义上传请求" main-class="p-4">
|
||||
<Demo4 />
|
||||
</FaPageMain>
|
||||
<FaPageMain title="文件夹上传" main-class="p-4">
|
||||
<Demo5 />
|
||||
</FaPageMain>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -11,7 +11,7 @@ export * from './src/drawer/index'
|
||||
export { default as FaDrawer } from './src/drawer/index.vue'
|
||||
export { default as FaDropdown } from './src/dropdown/index.vue'
|
||||
export { default as FaFileUpload } from './src/file-upload/index.vue'
|
||||
export type { FileItem } from './src/file-upload/index.vue'
|
||||
export type { FileItem, FileUploadRequestOptions } from './src/file-upload/index.vue'
|
||||
export { default as FaFixedBar } from './src/fixed-bar/index.vue'
|
||||
export { default as FaHoverCard } from './src/hover-card/index.vue'
|
||||
export { default as FaIcon } from './src/icon/index.vue'
|
||||
|
||||
@@ -1,40 +1,52 @@
|
||||
# FaFileUpload 文件上传
|
||||
|
||||
支持拖拽、点击和粘贴上传的文件上传组件,带进度显示和文件管理功能。
|
||||
支持拖拽、点击、粘贴、自定义上传请求和文件夹上传的文件上传组件,带进度显示和文件管理功能。
|
||||
|
||||
## 使用场景
|
||||
|
||||
- 头像/图片上传
|
||||
- 附件文件上传
|
||||
- 批量文件上传
|
||||
- 资料文件上传
|
||||
- 证据/凭证上传
|
||||
- 文档上传
|
||||
- 文件夹批量上传
|
||||
|
||||
## Props
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `action` | `string` | **必需** | 上传接口地址 |
|
||||
| `method` | `string` | `'post'` | 请求方法 |
|
||||
| `headers` | `Headers \| Record<string, any>` | `{}` | 请求头 |
|
||||
| `data` | `Record<string, any>` | `{}` | 额外表单数据 |
|
||||
| `name` | `string` | `'file'` | 文件字段名 |
|
||||
| `afterUpload` | `(response: any) => string \| Promise<string>` | - | 上传成功后的处理函数,返回文件 URL |
|
||||
| `multiple` | `boolean` | `false` | 是否支持多选 |
|
||||
| `ext` | `string[]` | `[]` | 允许的文件扩展名 |
|
||||
| `size` | `number` | `10485760` (10MB) | 单个文件大小限制(字节) |
|
||||
| `max` | `number` | `0` (无限制) | 最大上传数量 |
|
||||
| `hideTips` | `boolean` | `false` | 是否隐藏提示信息 |
|
||||
| `action` | `string` | `''` | 默认上传请求地址,使用 `httpRequest` 时可不传 |
|
||||
| `method` | `string` | `'post'` | 默认上传请求方法 |
|
||||
| `headers` | `Headers \| Record<string, any>` | `{}` | 默认上传请求头 |
|
||||
| `data` | `Record<string, any>` | `{}` | 默认上传附加表单数据 |
|
||||
| `name` | `string` | `'file'` | 默认上传文件字段名 |
|
||||
| `afterUpload` | `(response: any) => string \| Promise<string>` | - | 上传成功后从响应中提取文件 URL |
|
||||
| `beforeUpload` | `(file: File) => boolean \| Promise<boolean>` | - | 上传前钩子,返回 `false` 时跳过该文件 |
|
||||
| `httpRequest` | `(options: FileUploadRequestOptions) => any \| Promise<any>` | - | 自定义上传请求,返回值会传给 `afterUpload` 和 `onSuccess` |
|
||||
| `multiple` | `boolean` | `false` | 是否支持多选文件 |
|
||||
| `max` | `number` | `0` | 最大上传数量,`0` 表示不限制 |
|
||||
| `directory` | `boolean` | `false` | 是否选择文件夹;启用后只能选择文件夹,文件夹内文件会扁平化上传 |
|
||||
| `disabled` | `boolean` | `false` | 是否禁用 |
|
||||
|
||||
```ts
|
||||
interface FileUploadRequestOptions {
|
||||
action: string
|
||||
method: string
|
||||
headers: Headers | Record<string, any>
|
||||
data: Record<string, any>
|
||||
name: string
|
||||
file: File
|
||||
onProgress: (percent: number) => void
|
||||
}
|
||||
```
|
||||
|
||||
### Model
|
||||
|
||||
| 名称 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `modelValue` | `FileItem[]` | 已上传的文件列表(必需) |
|
||||
|
||||
### FileItem 接口
|
||||
### FileItem
|
||||
|
||||
```ts
|
||||
interface FileItem {
|
||||
@@ -62,7 +74,7 @@ interface FileItem {
|
||||
|
||||
## 粘贴上传
|
||||
|
||||
将鼠标移入当前组件,或通过 `Tab` 聚焦当前组件后,可直接按 `Ctrl+V` / `Cmd+V` 粘贴剪贴板中的文件。粘贴上传会复用组件现有的数量、格式和大小校验逻辑。
|
||||
将鼠标移入当前组件,或通过 `Tab` 聚焦当前组件后,可直接按 `Ctrl+V` / `Cmd+V` 粘贴剪贴板中的文件。启用 `directory` 后,粘贴上传不可用。
|
||||
|
||||
## 示例
|
||||
|
||||
@@ -70,134 +82,17 @@ interface FileItem {
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const fileList = ref([])
|
||||
</script>
|
||||
import type { FileItem } from '@fantastic-admin/components'
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
v-model="fileList"
|
||||
action="/api/upload"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 基础上传
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const fileList = ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
v-model="fileList"
|
||||
action="/api/upload"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 限制文件类型
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const fileList = ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
v-model="fileList"
|
||||
action="/api/upload"
|
||||
:ext="['jpg', 'png', 'gif']"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 限制文件大小
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const fileList = ref([])
|
||||
const maxSize = 5 * 1024 * 1024 // 5MB
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
v-model="fileList"
|
||||
action="/api/upload"
|
||||
:size="maxSize"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 限制上传数量
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const fileList = ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
v-model="fileList"
|
||||
action="/api/upload"
|
||||
:max="3"
|
||||
multiple
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 多选上传
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const fileList = ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
v-model="fileList"
|
||||
action="/api/upload"
|
||||
multiple
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 带额外参数
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const fileList = ref([])
|
||||
const token = useToken()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
v-model="fileList"
|
||||
action="/api/upload"
|
||||
method="post"
|
||||
:headers="{ Authorization: token.value }"
|
||||
:data="{ userId: 123, type: 'avatar' }"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 处理上传结果
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const fileList = ref([])
|
||||
const fileList = ref<FileItem[]>([])
|
||||
|
||||
function afterUpload(response: any) {
|
||||
// 假设后端返回 { code: 200, data: { url: '...' } }
|
||||
if (response.code === 200) {
|
||||
return response.data.url
|
||||
}
|
||||
throw new Error('上传失败')
|
||||
return response.data.url
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
<FaFileUpload
|
||||
v-model="fileList"
|
||||
action="/api/upload"
|
||||
:after-upload="afterUpload"
|
||||
@@ -205,102 +100,100 @@ function afterUpload(response: any) {
|
||||
</template>
|
||||
```
|
||||
|
||||
### 监听上传成功
|
||||
### 上传前校验
|
||||
|
||||
组件不再内置文件格式或大小校验。如需校验,请通过 `beforeUpload` 实现。
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const fileList = ref([])
|
||||
import type { FileItem } from '@fantastic-admin/components'
|
||||
|
||||
function handleSuccess(response: any, file: File) {
|
||||
console.log('上传成功:', response)
|
||||
console.log('文件:', file)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
v-model="fileList"
|
||||
action="/api/upload"
|
||||
@on-success="handleSuccess"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 自定义上传区域
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const fileList = ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
v-model="fileList"
|
||||
action="/api/upload"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center h-48">
|
||||
<FaIcon name="i-lucide:cloud-upload" class="text-4xl text-primary mb-4" />
|
||||
<p class="text-sm text-muted-foreground">点击或拖拽文件到此处上传</p>
|
||||
</div>
|
||||
</FaFileUpload>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 头像上传
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const fileList = ref([])
|
||||
|
||||
function handleClick(fileItem: FileItem) {
|
||||
if (fileItem.url) {
|
||||
// 预览头像
|
||||
window.open(fileItem.url)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
v-model="fileList"
|
||||
action="/api/upload"
|
||||
:ext="['jpg', 'png']"
|
||||
:size="2 * 1024 * 1024"
|
||||
:max="1"
|
||||
@on-click="handleClick"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 附件上传列表
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const fileList = ref<FileItem[]>([])
|
||||
|
||||
function handleFileClick(fileItem: FileItem) {
|
||||
if (fileItem.url) {
|
||||
window.open(fileItem.url, '_blank')
|
||||
function beforeUpload(file: File) {
|
||||
const isLt2M = file.size <= 2 * 1024 * 1024
|
||||
const isPdf = file.type === 'application/pdf'
|
||||
|
||||
if (!isPdf) {
|
||||
faToast.error('只能上传 PDF 文件')
|
||||
return false
|
||||
}
|
||||
if (!isLt2M) {
|
||||
faToast.error('文件不能超过 2MB')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
<FaFileUpload
|
||||
v-model="fileList"
|
||||
action="/api/upload"
|
||||
multiple
|
||||
:max="10"
|
||||
@on-click="handleFileClick"
|
||||
:before-upload="beforeUpload"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 自定义上传请求
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { FileItem, FileUploadRequestOptions } from '@fantastic-admin/components'
|
||||
|
||||
const fileList = ref<FileItem[]>([])
|
||||
|
||||
async function httpRequest({ file, onProgress }: FileUploadRequestOptions) {
|
||||
onProgress(50)
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await fetch('/api/custom-upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
onProgress(100)
|
||||
return await response.json()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
v-model="fileList"
|
||||
:http-request="httpRequest"
|
||||
:after-upload="response => response.url"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 文件夹上传
|
||||
|
||||
启用 `directory` 后,文件选择器只能选择文件夹。选择文件夹后,文件夹内的文件会作为扁平文件列表逐个上传。
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { FileItem } from '@fantastic-admin/components'
|
||||
|
||||
const fileList = ref<FileItem[]>([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaFileUpload
|
||||
v-model="fileList"
|
||||
action="/api/upload"
|
||||
directory
|
||||
:max="0"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **文件列表**:`modelValue` 是必需的双向绑定属性
|
||||
2. **上传进度**:上传过程中会显示进度条
|
||||
3. **状态显示**:上传中/成功/失败状态会自动显示
|
||||
4. **重新上传**:失败的文件可以点击重试
|
||||
5. **删除文件**:上传成功的文件可以删除
|
||||
6. **拖拽上传**:支持拖拽文件到上传区域
|
||||
1. **文件列表**:`modelValue` 是必需的双向绑定属性。
|
||||
2. **上传进度**:默认上传请求会自动更新进度;自定义 `httpRequest` 可调用 `onProgress` 更新进度。
|
||||
3. **状态显示**:上传中、成功、失败状态会自动显示。
|
||||
4. **重新上传**:失败的文件可以点击重试。
|
||||
5. **删除文件**:上传成功的文件可以删除。
|
||||
6. **上传校验**:组件不再内置格式、大小校验,请使用 `beforeUpload` 自行处理。
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import axios from 'axios'
|
||||
import { filesize } from 'filesize'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
defineOptions({
|
||||
name: 'FaFileUpload',
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
action: string
|
||||
action?: string
|
||||
method?: string
|
||||
headers?: Headers | Record<string, any>
|
||||
data?: Record<string, any>
|
||||
name?: string
|
||||
afterUpload?: (response: any) => string | Promise<string>
|
||||
beforeUpload?: (file: File) => boolean | Promise<boolean>
|
||||
httpRequest?: (options: FileUploadRequestOptions) => any | Promise<any>
|
||||
multiple?: boolean
|
||||
ext?: string[]
|
||||
size?: number
|
||||
max?: number
|
||||
hideTips?: boolean
|
||||
directory?: boolean
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
action: '',
|
||||
method: 'post',
|
||||
headers: () => ({}),
|
||||
data: () => ({}),
|
||||
name: 'file',
|
||||
multiple: false,
|
||||
ext: () => [],
|
||||
size: 10 * 1024 * 1024,
|
||||
max: 0,
|
||||
hideTips: false,
|
||||
directory: false,
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
@@ -49,6 +47,16 @@ export interface FileItem {
|
||||
file?: File
|
||||
}
|
||||
|
||||
export interface FileUploadRequestOptions {
|
||||
action: string
|
||||
method: string
|
||||
headers: Headers | Record<string, any>
|
||||
data: Record<string, any>
|
||||
name: string
|
||||
file: File
|
||||
onProgress: (percent: number) => void
|
||||
}
|
||||
|
||||
const containerRef = useTemplateRef<HTMLElement>('containerRef')
|
||||
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
|
||||
const isDragging = ref(false)
|
||||
@@ -58,70 +66,9 @@ const isMaxReached = computed(() => props.max > 0 && fileList.value.length >= pr
|
||||
const canHandlePaste = computed(() =>
|
||||
!props.disabled
|
||||
&& !isMaxReached.value
|
||||
&& !props.directory
|
||||
&& (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 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()
|
||||
@@ -192,7 +139,7 @@ function handleFiles(files: FileList | File[] | null) {
|
||||
if (!files || props.disabled || isMaxReached.value) {
|
||||
return false
|
||||
}
|
||||
const selectedFiles = [...files]
|
||||
const selectedFiles = [...files].filter(file => file instanceof File)
|
||||
const remain = props.max > 0 ? props.max - fileList.value.length : selectedFiles.length
|
||||
if (remain <= 0) {
|
||||
return false
|
||||
@@ -200,29 +147,53 @@ function handleFiles(files: FileList | File[] | null) {
|
||||
if (fileInputRef.value) {
|
||||
fileInputRef.value.value = ''
|
||||
}
|
||||
const filesToAdd = selectedFiles.slice(0, remain).filter(validateFile)
|
||||
const filesToAdd = selectedFiles.slice(0, remain)
|
||||
if (filesToAdd.length === 0) {
|
||||
return false
|
||||
}
|
||||
filesToAdd.forEach(file => uploadFile(file))
|
||||
filesToAdd.forEach((file) => {
|
||||
void uploadFile(file)
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
function uploadFile(file: File, index?: number) {
|
||||
function getHeadersObject(headers: Headers | Record<string, any>) {
|
||||
if (!(headers instanceof Headers)) {
|
||||
return { ...headers }
|
||||
}
|
||||
const headersObj: Record<string, any> = {}
|
||||
headers.forEach((value, key) => {
|
||||
headersObj[key] = value
|
||||
})
|
||||
return headersObj
|
||||
}
|
||||
|
||||
async function defaultHttpRequest(options: FileUploadRequestOptions) {
|
||||
const formData = new FormData()
|
||||
Object.entries(props.data).forEach(([key, value]) => {
|
||||
Object.entries(options.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 }
|
||||
formData.append(options.name, options.file)
|
||||
const response = await axios({
|
||||
url: options.action,
|
||||
method: options.method,
|
||||
headers: getHeadersObject(options.headers),
|
||||
data: formData,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
options.onProgress(Math.round((progressEvent.loaded * 100) / progressEvent.total))
|
||||
}
|
||||
},
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
async function uploadFile(file: File, index?: number) {
|
||||
const canUpload = await props.beforeUpload?.(file)
|
||||
if (canUpload === false) {
|
||||
return
|
||||
}
|
||||
|
||||
if (index === undefined) {
|
||||
fileList.value.push({
|
||||
name: file.name,
|
||||
@@ -233,28 +204,28 @@ function uploadFile(file: File, index?: number) {
|
||||
})
|
||||
}
|
||||
const currentFileIndex = index ?? fileList.value.length - 1
|
||||
axios({
|
||||
url: props.action,
|
||||
method: props.method,
|
||||
headers: headersObj,
|
||||
data: formData,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
fileList.value[currentFileIndex].progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||
}
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
const url = await props.afterUpload?.(response.data)
|
||||
if (url) {
|
||||
fileList.value[currentFileIndex].url = url
|
||||
}
|
||||
emits('onSuccess', response.data, file)
|
||||
fileList.value[currentFileIndex].status = 'success'
|
||||
})
|
||||
.catch(() => {
|
||||
fileList.value[currentFileIndex].status = 'error'
|
||||
try {
|
||||
const response = await (props.httpRequest ?? defaultHttpRequest)({
|
||||
action: props.action,
|
||||
method: props.method,
|
||||
headers: props.headers,
|
||||
data: props.data,
|
||||
name: props.name,
|
||||
file,
|
||||
onProgress: (percent) => {
|
||||
fileList.value[currentFileIndex].progress = percent
|
||||
},
|
||||
})
|
||||
const url = await props.afterUpload?.(response)
|
||||
if (url) {
|
||||
fileList.value[currentFileIndex].url = url
|
||||
}
|
||||
emits('onSuccess', response, file)
|
||||
fileList.value[currentFileIndex].status = 'success'
|
||||
}
|
||||
catch {
|
||||
fileList.value[currentFileIndex].status = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
function removeFile(idx: number) {
|
||||
@@ -291,22 +262,17 @@ function removeFile(idx: number) {
|
||||
拖放或点击上传
|
||||
</div>
|
||||
</slot>
|
||||
<input ref="fileInputRef" type="file" :multiple="props.multiple" :disabled="props.disabled" class="hidden" @change="e => onSelectFile((e.target as HTMLInputElement).files)">
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
:multiple="props.directory || props.multiple"
|
||||
:webkitdirectory="props.directory || undefined"
|
||||
:directory="props.directory || undefined"
|
||||
:disabled="props.disabled"
|
||||
class="hidden"
|
||||
@change="e => onSelectFile((e.target as HTMLInputElement).files)"
|
||||
>
|
||||
</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">
|
||||
支持 {{ 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">
|
||||
大小不超过 {{ filesize(props.size, { standard: 'jedec' }) }}
|
||||
</div>
|
||||
<div v-if="props.max > 0" class="after:content-[';_'] last:after:content-empty">
|
||||
数量不超过 {{ props.max }} 个
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="fileList.length > 0" class="gap-2 grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))]">
|
||||
<div
|
||||
v-for="(item, index) in fileList"
|
||||
|
||||
Reference in New Issue
Block a user