mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-07 09:43:17 +08:00
feat!: 新增 FaImageUpload 组件,移除 ImageUpload 和 ImagesUpload 组件
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
"element-plus": "^2.10.2",
|
||||
"eruda": "^3.4.3",
|
||||
"es-toolkit": "^1.39.5",
|
||||
"filesize": "^10.1.6",
|
||||
"hotkeys-js": "^3.13.14",
|
||||
"lucide-vue-next": "^0.525.0",
|
||||
"mitt": "^3.0.1",
|
||||
|
||||
Generated
+3
@@ -47,6 +47,9 @@ importers:
|
||||
es-toolkit:
|
||||
specifier: ^1.39.5
|
||||
version: 1.39.5
|
||||
filesize:
|
||||
specifier: ^10.1.6
|
||||
version: 10.1.6
|
||||
hotkeys-js:
|
||||
specifier: ^3.13.14
|
||||
version: 3.13.14
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadProps } from 'element-plus'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
defineOptions({
|
||||
name: 'ImageUpload',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
action: UploadProps['action']
|
||||
headers?: UploadProps['headers']
|
||||
data?: UploadProps['data']
|
||||
name?: UploadProps['name']
|
||||
size?: number
|
||||
width?: number
|
||||
height?: number
|
||||
placeholder?: string
|
||||
notip?: boolean
|
||||
ext?: string[]
|
||||
httpRequest?: UploadProps['httpRequest']
|
||||
}>(),
|
||||
{
|
||||
name: 'file',
|
||||
size: 2,
|
||||
width: 150,
|
||||
height: 150,
|
||||
placeholder: '',
|
||||
notip: false,
|
||||
ext: () => ['jpg', 'png', 'gif', 'bmp'],
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
onSuccess: [
|
||||
res: any,
|
||||
]
|
||||
}>()
|
||||
|
||||
const url = defineModel<string>({
|
||||
default: '',
|
||||
})
|
||||
|
||||
const uploadData = ref({
|
||||
imageViewerVisible: false,
|
||||
progress: {
|
||||
preview: '',
|
||||
percent: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// 预览
|
||||
function preview() {
|
||||
uploadData.value.imageViewerVisible = true
|
||||
}
|
||||
// 关闭预览
|
||||
function previewClose() {
|
||||
uploadData.value.imageViewerVisible = false
|
||||
}
|
||||
// 移除
|
||||
function remove() {
|
||||
url.value = ''
|
||||
}
|
||||
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!`)
|
||||
}
|
||||
if (isTypeOk && isSizeOk) {
|
||||
uploadData.value.progress.preview = URL.createObjectURL(file)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
}
|
||||
const onProgress: UploadProps['onProgress'] = (file) => {
|
||||
uploadData.value.progress.percent = ~~file.percent
|
||||
}
|
||||
const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
uploadData.value.progress.preview = ''
|
||||
uploadData.value.progress.percent = 0
|
||||
emits('onSuccess', res)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<ElUpload
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-progress="onProgress"
|
||||
:on-success="onSuccess"
|
||||
:http-request="httpRequest"
|
||||
drag
|
||||
class="image-upload"
|
||||
>
|
||||
<ElImage v-if="url === ''" :src="url === '' ? placeholder : url" :style="`width:${width}px;height:${height}px;`" fit="fill">
|
||||
<template #error>
|
||||
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
|
||||
<FaIcon name="i-ep:plus" class="icon" />
|
||||
</div>
|
||||
</template>
|
||||
</ElImage>
|
||||
<div v-else class="image">
|
||||
<ElImage :src="url" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<div class="mask">
|
||||
<div class="actions">
|
||||
<span @click.stop="preview">
|
||||
<FaIcon name="i-ep:zoom-in" class="icon" />
|
||||
</span>
|
||||
<span @click.stop="remove">
|
||||
<FaIcon name="i-ep:delete" class="icon" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="url === '' && uploadData.progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
|
||||
<ElImage :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<ElProgress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
|
||||
</div>
|
||||
</ElUpload>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<ElAlert :title="`上传图片支持 ${ext.join(' / ')} 格式,且图片大小不超过 ${size}MB,建议图片尺寸为 ${width}*${height}`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
<ElImageViewer v-if="uploadData.imageViewerVisible" :url-list="[url]" teleported @close="previewClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.upload-container {
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.el-image {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
|
||||
span {
|
||||
width: 50%;
|
||||
color: var(--el-color-white);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.image-upload {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
.el-upload-dragger {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background-color: transparent;
|
||||
|
||||
.icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: "";
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
}
|
||||
|
||||
.el-progress {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 1;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
|
||||
.el-progress__text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,278 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadProps } from 'element-plus'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
defineOptions({
|
||||
name: 'ImagesUpload',
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
action: UploadProps['action']
|
||||
headers?: UploadProps['headers']
|
||||
data?: UploadProps['data']
|
||||
name?: UploadProps['name']
|
||||
size?: number
|
||||
max?: number
|
||||
width?: number
|
||||
height?: number
|
||||
placeholder?: string
|
||||
notip?: boolean
|
||||
ext?: string[]
|
||||
httpRequest?: UploadProps['httpRequest']
|
||||
}>(),
|
||||
{
|
||||
name: 'file',
|
||||
size: 2,
|
||||
max: 3,
|
||||
width: 150,
|
||||
height: 150,
|
||||
placeholder: '',
|
||||
notip: false,
|
||||
ext: () => ['jpg', 'png', 'gif', 'bmp'],
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
onSuccess: [
|
||||
res: any,
|
||||
]
|
||||
}>()
|
||||
|
||||
const url = defineModel<string[]>({
|
||||
default: [],
|
||||
})
|
||||
|
||||
const uploadData = ref({
|
||||
dialogImageIndex: 0,
|
||||
imageViewerVisible: false,
|
||||
progress: {
|
||||
preview: '',
|
||||
percent: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// 预览
|
||||
function preview(index: number) {
|
||||
uploadData.value.dialogImageIndex = index
|
||||
uploadData.value.imageViewerVisible = true
|
||||
}
|
||||
// 关闭预览
|
||||
function previewClose() {
|
||||
uploadData.value.imageViewerVisible = false
|
||||
}
|
||||
// 移除
|
||||
function remove(index: number) {
|
||||
url.value.splice(index, 1)
|
||||
}
|
||||
// 移动
|
||||
function move(index: number, type: 'left' | 'right') {
|
||||
if (type === 'left' && index !== 0) {
|
||||
url.value[index] = url.value.splice(index - 1, 1, url.value[index])[0]
|
||||
}
|
||||
if (type === 'right' && index !== url.value.length - 1) {
|
||||
url.value[index] = url.value.splice(index + 1, 1, url.value[index])[0]
|
||||
}
|
||||
}
|
||||
|
||||
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!`)
|
||||
}
|
||||
if (isTypeOk && isSizeOk) {
|
||||
uploadData.value.progress.preview = URL.createObjectURL(file)
|
||||
}
|
||||
return isTypeOk && isSizeOk
|
||||
}
|
||||
const onProgress: UploadProps['onProgress'] = (file) => {
|
||||
uploadData.value.progress.percent = ~~file.percent
|
||||
}
|
||||
const onSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
uploadData.value.progress.preview = ''
|
||||
uploadData.value.progress.percent = 0
|
||||
emits('onSuccess', res)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<div v-for="(item, index) in (url as string[])" :key="index" class="images">
|
||||
<ElImage v-if="index < max" :src="item" :style="`width:${width}px;height:${height}px;`" fit="cover" />
|
||||
<div class="mask">
|
||||
<div class="actions">
|
||||
<span @click="preview(index)">
|
||||
<FaIcon name="i-ep:zoom-in" class="icon" />
|
||||
</span>
|
||||
<span @click="remove(index)">
|
||||
<FaIcon name="i-ep:delete" class="icon" />
|
||||
</span>
|
||||
<span v-show="url.length > 1" :class="{ disabled: index === 0 }" @click="move(index, 'left')">
|
||||
<FaIcon name="i-ep:back" class="icon" />
|
||||
</span>
|
||||
<span v-show="url.length > 1" :class="{ disabled: index === url.length - 1 }" @click="move(index, 'right')">
|
||||
<FaIcon name="i-ep:right" class="icon" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ElUpload
|
||||
v-show="url.length < max"
|
||||
:show-file-list="false"
|
||||
:headers="headers"
|
||||
:action="action"
|
||||
:data="data"
|
||||
:name="name"
|
||||
:before-upload="beforeUpload"
|
||||
:on-progress="onProgress"
|
||||
:on-success="onSuccess"
|
||||
:http-request="httpRequest"
|
||||
drag
|
||||
class="images-upload"
|
||||
>
|
||||
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
|
||||
<FaIcon name="i-ep:plus" class="icon" />
|
||||
</div>
|
||||
<div v-show="uploadData.progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
|
||||
<ElImage :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
|
||||
<ElProgress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
|
||||
</div>
|
||||
</ElUpload>
|
||||
<div v-if="!notip" class="el-upload__tip">
|
||||
<div style="display: inline-block;">
|
||||
<ElAlert :title="`上传图片支持 ${ext.join(' / ')} 格式,单张图片大小不超过 ${size}MB,建议图片尺寸为 ${width}*${height},且图片数量不超过 ${max} 张`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
<ElImageViewer v-if="uploadData.imageViewerVisible" :url-list="url as string[]" :initial-index="uploadData.dialogImageIndex" teleported @close="previewClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.upload-container {
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.el-image {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.images {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-inline-end: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
|
||||
span {
|
||||
width: 50%;
|
||||
color: var(--el-color-white);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
|
||||
&.disabled {
|
||||
color: var(--el-text-color-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.images-upload {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
.el-upload-dragger {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
|
||||
&.is-dragover {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--el-text-color-placeholder);
|
||||
background-color: transparent;
|
||||
|
||||
.icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: "";
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
}
|
||||
|
||||
.el-progress {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 1;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
|
||||
.el-progress__text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Vendored
+1
-2
@@ -25,6 +25,7 @@ declare module 'vue' {
|
||||
FaHoverCard: typeof import('./../ui/components/FaHoverCard/index.vue')['default']
|
||||
FaIcon: typeof import('./../ui/components/FaIcon/index.vue')['default']
|
||||
FaImagePreview: typeof import('./../ui/components/FaImagePreview/index.vue')['default']
|
||||
FaImageUpload: typeof import('./../ui/components/FaImageUpload/index.vue')['default']
|
||||
FaInput: typeof import('./../ui/components/FaInput/index.vue')['default']
|
||||
FaKbd: typeof import('./../ui/components/FaKbd/index.vue')['default']
|
||||
FaModal: typeof import('./../ui/components/FaModal/index.vue')['default']
|
||||
@@ -47,8 +48,6 @@ declare module 'vue' {
|
||||
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']
|
||||
ImagesUpload: typeof import('./../components/ImagesUpload/index.vue')['default']
|
||||
ImageUpload: typeof import('./../components/ImageUpload/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
<script setup lang="ts">
|
||||
import axios from 'axios'
|
||||
import { filesize } from 'filesize'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { useFaImagePreview } from '@/ui/components/FaImagePreview'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
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[]
|
||||
max?: number
|
||||
width?: number
|
||||
height?: number
|
||||
dimension?: {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
size?: number
|
||||
hideTips?: boolean
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
method: 'post',
|
||||
headers: () => ({}),
|
||||
data: () => ({}),
|
||||
name: 'file',
|
||||
multiple: false,
|
||||
ext: () => [],
|
||||
max: 1,
|
||||
width: 100,
|
||||
height: 100,
|
||||
size: 5 * 1024 * 1024,
|
||||
hideTips: false,
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
onSuccess: [response: any, file: File]
|
||||
}>()
|
||||
|
||||
const images = defineModel<string[]>('modelValue', { required: true })
|
||||
|
||||
const isUploading = ref(false)
|
||||
const uploadProgress = ref(0)
|
||||
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
|
||||
|
||||
function onSelectFile(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
if (!input.files) {
|
||||
return
|
||||
}
|
||||
const selectedFiles = Array.from(input.files)
|
||||
const remain = props.max === 0 ? selectedFiles.length : props.max - images.value.length
|
||||
const filesToAdd: File[] = []
|
||||
for (const file of selectedFiles.slice(0, remain)) {
|
||||
const fileSize = file.size / 1024 / 1024
|
||||
if (fileSize > props.size) {
|
||||
toast.error(`上传图片大小不能超过 ${filesize(props.size, { standard: 'jedec' })}`)
|
||||
return
|
||||
}
|
||||
// 类型校验
|
||||
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(' / ')} 格式`, {
|
||||
description: file.name,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 大小校验
|
||||
if (props.size > 0) {
|
||||
if (file.size > props.size) {
|
||||
toast.error(`上传图片大小不能超过 ${filesize(props.size, { standard: 'jedec' })}`, {
|
||||
description: `${file.name} (${filesize(file.size, { standard: 'jedec' })})`,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
filesToAdd.push(file)
|
||||
}
|
||||
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
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function onRemove(idx: number) {
|
||||
images.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
function onPreview(index: number) {
|
||||
useFaImagePreview().open(images.value, index)
|
||||
}
|
||||
|
||||
function onMove(index: number, direction: 'forward' | 'backward') {
|
||||
if (direction === 'forward' && index !== 0) {
|
||||
images.value[index] = images.value.splice(index - 1, 1, images.value[index])[0]
|
||||
}
|
||||
if (direction === 'backward' && index !== images.value.length - 1) {
|
||||
images.value[index] = images.value.splice(index + 1, 1, images.value[index])[0]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="(img, index) in images" :key="img" class="group/image-upload relative flex items-center justify-center overflow-hidden border rounded-lg" :style="{
|
||||
width: `${props.width}px`,
|
||||
height: `${props.height}px`,
|
||||
}"
|
||||
>
|
||||
<img :src="img" class="h-full w-full object-contain">
|
||||
<div v-if="!props.disabled" class="absolute inset-0 grid grid-cols-2 place-items-center rounded-lg bg-black/50 p-2 text-white opacity-0 transition-opacity group-hover/image-upload:opacity-100">
|
||||
<div class="flex-center cursor-pointer opacity-60 transition-all hover:(scale-110 opacity-100)" @click.prevent="onPreview(index)">
|
||||
<FaIcon name="i-icon-park-outline:preview-open" class="size-6" />
|
||||
</div>
|
||||
<div class="flex-center cursor-pointer opacity-60 transition-all hover:(scale-110 opacity-100)" @click.prevent="onRemove(index)">
|
||||
<FaIcon name="i-icon-park-outline:delete" class="size-6" />
|
||||
</div>
|
||||
<div
|
||||
v-show="images.length > 1" :class="cn('flex-center cursor-pointer opacity-60 transition-all', {
|
||||
'hover:(scale-110 opacity-100)': index !== 0,
|
||||
'cursor-not-allowed': index === 0,
|
||||
})" @click="onMove(index, 'forward')"
|
||||
>
|
||||
<FaIcon name="i-icon-park-outline:arrow-circle-left" class="size-6" />
|
||||
</div>
|
||||
<div
|
||||
v-show="images.length > 1" :class="cn('flex-center cursor-pointer opacity-60 transition-all', {
|
||||
'hover:(scale-110 opacity-100)': index !== images.length - 1,
|
||||
'cursor-not-allowed': index === images.length - 1,
|
||||
})" @click="onMove(index, 'backward')"
|
||||
>
|
||||
<FaIcon name="i-icon-park-outline:arrow-circle-right" class="size-6" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="images.length < props.max || props.max === 0" class="relative flex-center overflow-hidden bg-transparent" :class="{
|
||||
'cursor-not-allowed': props.disabled || isUploading,
|
||||
}" :style="{
|
||||
width: `${props.width}px`,
|
||||
height: `${props.height}px`,
|
||||
}" :disabled="props.disabled || isUploading" @click="fileInputRef?.click()"
|
||||
>
|
||||
<slot>
|
||||
<div class="size-full flex-center border border-2 rounded-lg border-dashed p-0 text-primary">
|
||||
<FaIcon name="i-icon-park-outline:upload" class="text-2xl text-card-foreground/50" />
|
||||
</div>
|
||||
</slot>
|
||||
<template v-if="isUploading && uploadProgress > 0 && uploadProgress < 100">
|
||||
<FaProgress :model-value="uploadProgress" class="absolute inset-x-1 inset-b-1 h-1 w-auto" />
|
||||
</template>
|
||||
<input ref="fileInputRef" type="file" accept="image/*" :multiple="props.multiple" class="hidden" @change="onSelectFile">
|
||||
</button>
|
||||
</div>
|
||||
<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.dimension" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ `建议尺寸为 ${props.dimension?.width}*${props.dimension?.height}` }}
|
||||
</div>
|
||||
<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 > 1" class="after:content-[';_'] last:after:content-empty">
|
||||
{{ `数量不超过 ${props.max} 个` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user