mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-07 11:34:10 +08:00
新增 FaFileUpload 组件,移除 FileUpload 组件
This commit is contained in:
@@ -1,143 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadProps, UploadUserFile } from 'element-plus'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
defineOptions({
|
||||
name: 'FileUpload',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
action: UploadProps['action']
|
||||
headers?: UploadProps['headers']
|
||||
data?: UploadProps['data']
|
||||
name?: UploadProps['name']
|
||||
size?: number
|
||||
max?: number
|
||||
files?: UploadUserFile[]
|
||||
notip?: boolean
|
||||
ext?: string[]
|
||||
httpRequest?: UploadProps['httpRequest']
|
||||
}>(),
|
||||
{
|
||||
name: 'file',
|
||||
size: 2,
|
||||
max: 3,
|
||||
files: () => [],
|
||||
notip: false,
|
||||
ext: () => ['zip', 'rar'],
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
onSuccess: [
|
||||
res: any,
|
||||
file: UploadUserFile,
|
||||
fileList: UploadUserFile[],
|
||||
]
|
||||
onRemove: [
|
||||
file: UploadUserFile,
|
||||
fileList: UploadUserFile[],
|
||||
]
|
||||
}>()
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const fileName = file.name.split('.')
|
||||
const fileExt = fileName.at(-1) ?? ''
|
||||
const isTypeOk = props.ext.includes(fileExt)
|
||||
const isSizeOk = file.size / 1024 / 1024 < props.size
|
||||
if (!isTypeOk) {
|
||||
toast.error(`上传文件只支持 ${props.ext.join(' / ')} 格式!`)
|
||||
}
|
||||
if (!isSizeOk) {
|
||||
toast.error(`上传文件大小不能超过 ${props.size}MB!`)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
}
|
||||
|
||||
const onExceed: UploadProps['onExceed'] = () => {
|
||||
toast.warning('文件上传超过限制')
|
||||
}
|
||||
|
||||
const onSuccess: UploadProps['onSuccess'] = (res, file, fileList) => {
|
||||
emits('onSuccess', res, file, fileList)
|
||||
}
|
||||
|
||||
const onRemove: UploadProps['onRemove'] = (file, fileList) => {
|
||||
emits('onRemove', file, fileList)
|
||||
}
|
||||
|
||||
const onPreview: UploadProps['onPreview'] = (e) => {
|
||||
const getBlob = (url: string) => new Promise((resolve) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', url, true)
|
||||
xhr.responseType = 'blob'
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
resolve(xhr.response)
|
||||
}
|
||||
}
|
||||
xhr.send()
|
||||
})
|
||||
getBlob(e.url!).then((blob: any) => {
|
||||
const a = document.createElement('a')
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const event = new MouseEvent('click')
|
||||
a.target = '_blank'
|
||||
a.download = e.name
|
||||
a.href = url
|
||||
a.dispatchEvent(event)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElUpload
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-exceed="onExceed"
|
||||
:on-success="onSuccess"
|
||||
:on-remove="onRemove"
|
||||
:on-preview="onPreview"
|
||||
:http-request="httpRequest"
|
||||
:file-list="files"
|
||||
:limit="max"
|
||||
drag
|
||||
>
|
||||
<div class="slot">
|
||||
<FaIcon name="i-ep:upload-filled" class="el-icon--upload" />
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<ElAlert :title="`上传文件支持 ${ext.join(' / ')} 格式,单个文件大小不超过 ${size}MB,且文件数量不超过 ${max} 个`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-upload.is-drag) {
|
||||
display: inline-block;
|
||||
|
||||
.el-upload-dragger {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.slot {
|
||||
width: 300px;
|
||||
padding: 40px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { defineFakeRoute } from 'vite-plugin-fake-server/client'
|
||||
|
||||
export default defineFakeRoute([
|
||||
{
|
||||
url: '/mock/upload/image',
|
||||
url: '/mock/upload',
|
||||
method: 'post',
|
||||
response: () => {
|
||||
return {
|
||||
|
||||
@@ -93,6 +93,14 @@ const routes: RouteRecordRaw = {
|
||||
title: '下拉菜单',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'fileupload',
|
||||
name: 'componentExampleBuiltInFileUpload',
|
||||
component: () => import('@/views/component_built_in_example/fileupload.vue'),
|
||||
meta: {
|
||||
title: '文件上传',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'fixedactionbar',
|
||||
name: 'componentExampleBuiltInFixedactionbar',
|
||||
|
||||
Vendored
+1
-1
@@ -21,6 +21,7 @@ declare module 'vue' {
|
||||
FaDivider: typeof import('./../ui/components/FaDivider/index.vue')['default']
|
||||
FaDrawer: typeof import('./../ui/components/FaDrawer/index.vue')['default']
|
||||
FaDropdown: typeof import('./../ui/components/FaDropdown/index.vue')['default']
|
||||
FaFileUpload: typeof import('./../ui/components/FaFileUpload/index.vue')['default']
|
||||
FaFixedActionBar: typeof import('./../ui/components/FaFixedActionBar/index.vue')['default']
|
||||
FaHoverCard: typeof import('./../ui/components/FaHoverCard/index.vue')['default']
|
||||
FaIcon: typeof import('./../ui/components/FaIcon/index.vue')['default']
|
||||
@@ -48,7 +49,6 @@ declare module 'vue' {
|
||||
FaTabs: typeof import('./../ui/components/FaTabs/index.vue')['default']
|
||||
FaToast: typeof import('./../ui/components/FaToast/index.vue')['default']
|
||||
FaTooltip: typeof import('./../ui/components/FaTooltip/index.vue')['default']
|
||||
FileUpload: typeof import('./../components/FileUpload/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
<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
|
||||
method?: string
|
||||
headers?: Headers | Record<string, any>
|
||||
data?: Record<string, any>
|
||||
name?: string
|
||||
afterUpload?: (response: any) => string | Promise<string>
|
||||
multiple?: boolean
|
||||
ext?: string[]
|
||||
size?: number
|
||||
max?: number
|
||||
hideTips?: boolean
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
method: 'post',
|
||||
headers: () => ({}),
|
||||
data: () => ({}),
|
||||
name: 'file',
|
||||
multiple: false,
|
||||
ext: () => [],
|
||||
size: 10 * 1024 * 1024,
|
||||
max: 0,
|
||||
hideTips: false,
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
onSuccess: [response: any, file: File]
|
||||
onClick: [fileItem: FileItem, index: number]
|
||||
}>()
|
||||
|
||||
const fileList = defineModel<FileItem[]>('modelValue', { required: true })
|
||||
|
||||
export interface FileItem {
|
||||
name: string
|
||||
size: number
|
||||
url?: string
|
||||
status?: 'uploading' | 'success' | 'error'
|
||||
progress?: number
|
||||
file?: File
|
||||
}
|
||||
|
||||
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
|
||||
const isDragging = ref(false)
|
||||
|
||||
function handleDragOver(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
function handleDragLeave(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
function handleDrop(e: DragEvent) {
|
||||
e.preventDefault()
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
isDragging.value = false
|
||||
if (e.dataTransfer?.files) {
|
||||
onSelectFile(e.dataTransfer.files)
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectFile(files: FileList | File[] | null) {
|
||||
if (!files) {
|
||||
return
|
||||
}
|
||||
const selectedFiles = Array.from(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(`上传文件只支持 ${props.ext.join(' / ')} 格式`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 大小校验
|
||||
if (props.size > 0) {
|
||||
if (file.size > props.size) {
|
||||
toast.error(`上传文件大小不能超过 ${filesize(props.size, { standard: 'jedec' })}`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
filesToAdd.push(file)
|
||||
}
|
||||
fileInputRef.value!.value = ''
|
||||
filesToAdd.forEach(file => uploadFile(file))
|
||||
}
|
||||
|
||||
function uploadFile(file: File, index?: number) {
|
||||
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 }
|
||||
}
|
||||
if (index === undefined) {
|
||||
fileList.value.push({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
status: 'uploading',
|
||||
progress: 0,
|
||||
file,
|
||||
})
|
||||
}
|
||||
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'
|
||||
})
|
||||
}
|
||||
|
||||
function removeFile(idx: number) {
|
||||
fileList.value.splice(idx, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
class="h-40 w-full flex flex-col cursor-pointer items-center justify-center border border-2 rounded-lg border-dashed bg-transparent p-4 transition-all"
|
||||
:class="{
|
||||
'border-primary bg-primary/5': isDragging,
|
||||
'opacity-50 cursor-not-allowed': props.disabled || (props.max > 0 && fileList.length >= props.max),
|
||||
}"
|
||||
:disabled="props.disabled || (props.max > 0 && fileList.length >= props.max)"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop"
|
||||
@click="fileInputRef?.click()"
|
||||
>
|
||||
<FaIcon name="i-icon-park-outline:upload" class="mb-2 text-2xl text-card-foreground/50" />
|
||||
<div class="text-sm text-card-foreground/70">
|
||||
将文件拖到此处,或<span class="cursor-pointer text-primary font-bold">点击上传</span>
|
||||
</div>
|
||||
<input ref="fileInputRef" type="file" :multiple="props.multiple" :disabled="props.disabled" class="hidden" @change="e => onSelectFile((e.target as HTMLInputElement).files)">
|
||||
</button>
|
||||
<div v-if="!props.hideTips && !props.disabled" class="flex flex-wrap gap-1 text-xs text-card-foreground/50 empty:hidden">
|
||||
<div v-if="props.ext.length > 0" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ `支持 ${props.ext.join(' / ')} 格式` }}
|
||||
</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="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2">
|
||||
<div
|
||||
v-for="(item, index) in fileList"
|
||||
:key="item.name + index"
|
||||
class="group/file-upload-item relative flex items-center gap-2 border rounded-lg py-2 pe-2 ps-3"
|
||||
:class="[
|
||||
item.status === 'error' ? 'border-red-500 bg-red-500/10' : '',
|
||||
]"
|
||||
@click="emits('onClick', item, index)"
|
||||
>
|
||||
<div class="flex-1 truncate">
|
||||
<div class="truncate text-sm font-medium">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="text-xs text-card-foreground/50">
|
||||
{{ filesize(item.size, { standard: 'jedec' }) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.status === 'uploading'" class="pointer-events-none absolute inset-0 z-0 bg-primary/5" :style="{ width: `${item.progress}%` }" />
|
||||
<div v-else-if="item.status === 'success'" class="size-10 flex-center">
|
||||
<FaIcon name="i-ix:upload-success" class="text-lg text-green-600" />
|
||||
</div>
|
||||
<div v-else-if="item.status === 'error'" class="size-10 flex-center">
|
||||
<FaIcon name="i-ix:upload-fail" class="text-lg text-red-600" />
|
||||
</div>
|
||||
<FaButtonGroup v-if="!props.disabled" class="absolute inset-e-2 top-1/2 opacity-0 transition-opacity -translate-y-1/2 group-hover/file-upload-item:opacity-100">
|
||||
<FaButton v-if="item.status === 'error'" variant="outline" size="icon" @click.stop="uploadFile(item.file!, index)">
|
||||
<FaIcon name="i-icon-park-outline:upload" class="cursor-pointer text-lg" />
|
||||
</FaButton>
|
||||
<FaButton v-if="item.status !== 'uploading'" variant="outline" size="icon" @click.stop="removeFile(index)">
|
||||
<FaIcon name="i-icon-park-outline:delete" class="cursor-pointer text-lg text-red-500" />
|
||||
</FaButton>
|
||||
</FaButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,52 @@
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
enabled: false
|
||||
</route>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FileItem } from '@/ui/components/FaFileUpload/index.vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const files1 = ref<FileItem[]>([])
|
||||
const files2 = ref<FileItem[]>([
|
||||
{ name: 'logo.svg', size: 1024 * 1024, url: 'https://fantastic-admin.hurui.me/logo.svg' },
|
||||
])
|
||||
|
||||
function handleSuccess() {
|
||||
toast.success('模拟上传成功')
|
||||
}
|
||||
|
||||
function handleClick(fileItem: FileItem) {
|
||||
toast.info(fileItem.name, {
|
||||
description: fileItem.url,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FaPageHeader title="文件上传" description="FaFileUpload" />
|
||||
<FaPageMain>
|
||||
<FaFileUpload
|
||||
v-model="files1"
|
||||
action="/mock/upload"
|
||||
:after-upload="(response) => response.data.url"
|
||||
multiple
|
||||
@on-success="handleSuccess"
|
||||
/>
|
||||
</FaPageMain>
|
||||
<FaPageMain title="限制上传数量、大小、类型、点击事件">
|
||||
<FaFileUpload
|
||||
v-model="files2"
|
||||
action="/mock/upload"
|
||||
:after-upload="(response) => response.data.url"
|
||||
multiple
|
||||
:ext="['png']"
|
||||
:size="200 * 1024"
|
||||
:max="5"
|
||||
@on-success="handleSuccess"
|
||||
@on-click="handleClick"
|
||||
/>
|
||||
</FaPageMain>
|
||||
</div>
|
||||
</template>
|
||||
@@ -21,7 +21,7 @@ function handleSuccess() {
|
||||
<FaPageMain>
|
||||
<FaImageUpload
|
||||
v-model="files1"
|
||||
action="/mock/upload/image"
|
||||
action="/mock/upload"
|
||||
:after-upload="(response) => response.data.url"
|
||||
@on-success="handleSuccess"
|
||||
/>
|
||||
@@ -29,7 +29,7 @@ function handleSuccess() {
|
||||
<FaPageMain title="多图上传">
|
||||
<FaImageUpload
|
||||
v-model="files2"
|
||||
action="/mock/upload/image"
|
||||
action="/mock/upload"
|
||||
:after-upload="(response) => response.data.url"
|
||||
:max="2"
|
||||
@on-success="handleSuccess"
|
||||
@@ -38,7 +38,7 @@ function handleSuccess() {
|
||||
<FaPageMain title="自定义默认内容、容器大小、建议尺寸、限制类型、不限制数量">
|
||||
<FaImageUpload
|
||||
v-model="files3"
|
||||
action="/mock/upload/image"
|
||||
action="/mock/upload"
|
||||
:after-upload="(response) => response.data.url"
|
||||
:width="200"
|
||||
:height="130"
|
||||
|
||||
Reference in New Issue
Block a user