上传标签 限制最大数量之后 多选图片还是会出现上传情况
问题描述:ant desgin vue 的上传组件,限制最大上传五个,一开始选择七张图片还是会上传成功,如果在上传之前函数拦截,会出现七个白框占位。
解决方案:用input 代替上传组件
<template>
<div class="upload-container">
<input
ref="fileInput"
type="file"
class="hidden-input"
:multiple="multiple"
accept="image/*, video/*"
@change="handleFileSelect"
>
<AUpload
v-model:fileList="fileList"
listType="picture-card"
:multiple="multiple"
:beforeUpload="beforeUpload"
:showUploadList="{ showPreviewIcon: false, showRemoveIcon: false }"
:customRequest="customRequest"
accept="image/*, video/*"
@preview="handlePreview"
>
<div v-if="fileList.length < maxCount">
<PlusOutlined style="font-size: 28px;" />
<div class="ant-upload-text">
<!-- 上传文件 ({{ fileList.length }}/{{ maxCount }}) -->
上传文件
</div>
</div>
<template #itemRender="{file}">
<div
:key="file.uid"
class="ant-upload-list-item"
>
<div class="ant-upload-list-item-info">
<div
class="ant-upload-list-item-thumbnail"
style="display: flex;"
>
<LoadingOutlined
v-if="file.status === 'uploading'"
style="font-size: 40px; color: #1890ff;"
/>
<img
v-if="file.status === 'done' && file.url"
:src="file.cover"
alt="图片缩略图"
>
</div>
<img
v-if="file.status === 'done' && file.type?.startsWith('video/')"
class="video-thumbnail-container"
src="../../assets/play.png"
alt=""
>
<div
v-if="file.status === 'uploading'"
class="upload-progress"
>
<div
class="progress-bar"
:style="{ width: `${file.progress || 0}%` }"
/>
</div>
</div>
<div
v-if="!isAtlas"
class="ant-upload-list-item-actions"
>
<span
class="ant-upload-list-item-action"
@click="handlePreview(file)"
>
<EyeOutlined style="font-size: 20px;width: 20px;" />
</span>
<span
class="ant-upload-list-item-action"
@click="removeFile(file)"
>
<DeleteOutlined style="font-size: 20px;width: 20px;" />
</span>
</div>
<div
v-if="isAtlas"
class="ant-upload-list-item-actions"
>
<div>
<AButton
type="primary"
shape="round"
size="small"
@click="handleTitle(file)"
>
标题
</AButton>
</div>
<div style="margin-top: 8px;">
<AButton
type="primary"
shape="round"
size="small"
danger
@click="removeFile(file)"
>
删除
</AButton>
</div>
</div>
</div>
<span style="line-height: 20px;">{{ file.title }}</span>
</template>
</AUpload>
<slot name="tip" />
<AModal
:open="previewVisible"
title="预览"
@cancel="previewVisible = false"
>
<div v-if="previewType == 'image'">
<img
:src="previewUrl"
alt="预览图片"
class="preview-image"
>
</div>
<div v-else-if="previewType == 'video'">
<video
controls
class="preview-video"
>
<source
:src="previewUrl"
type="video/mp4"
>
您的浏览器不支持视频播放
</video>
</div>
<template #footer>
<AButton @click="previewVisible = false">
关闭
</AButton>
</template>
</AModal>
<AModal
:open="titleModal"
title="标题"
@cancel="closeTitle"
@ok="saveTitle"
>
<AInput
v-model:value="picTitle"
placeholder="请输入标题"
/>
</AModal>
</div>
</template>
<script lang="ts" setup>
import { ref, defineProps, defineEmits, getCurrentInstance, watch, onMounted } from 'vue'
import type { UploadFile } from 'ant-design-vue/es/upload/interface'
import { message } from 'ant-design-vue'
const { proxy } = getCurrentInstance()
interface CustomFile extends UploadFile {
uid: string;
name: string;
thumbnail?: string;
type?: string;
progress?: number;
cover?: string;
title?: string;
id?: string;
status?: string;
url?: string;
}
const props = defineProps({
multiple: {
type: Boolean,
default: true,
},
maxCount: {
type: Number,
default: 5,
},
isAtlas: {
type: Boolean,
default: false,
},
modelValue: { type: [String, Array, Object], default: () => {} },
})
const emit = defineEmits(['update:modelValue', 'update:fileList'])
const fileList = ref<CustomFile[]>([])
const previewVisible = ref(false)
const previewUrl = ref('')
const previewType = ref('image')
const titleModal = ref(false)
const picTitle = ref('')
const selectedFile = ref<CustomFile | null>(null)
const pendingFiles = ref<File[]>([])
const maxCount = ref(props.maxCount)
const fileInput = ref<HTMLInputElement | null>(null)
// 初始化文件列表
if (Array.isArray(props.modelValue)) {
fileList.value = props.modelValue.map((item: any) => ({
...item,
status: 'done',
}))
} else if (props.modelValue && typeof props.modelValue === 'object') {
fileList.value = [{
...props.modelValue,
status: 'done',
uid: (props.modelValue.uid || Date.now().toString()),
name: (props.modelValue.name || '文件'),
}]
}
watch(
() => props.modelValue,
(newVal: any) => {
if (Array.isArray(newVal)) {
fileList.value = newVal.map((item: any) => ({
...item,
status: 'done',
}))
} else if (newVal && typeof newVal === 'object') {
fileList.value = [{ ...newVal, status: 'done' }]
}
},
{ deep: true },
)
const updateModelValue = (newList: any) => {
emit('update:modelValue', newList)
}
const beforeUpload = (file: File) => {
const isImage = file.type.startsWith('image/')
const isVideo = file.type.startsWith('video/')
if (!isImage && !isVideo) {
message.error('只能上传图片或视频文件!')
return false
}
return true
}
const handleFileSelect = (e: Event) => {
const input = e.target as HTMLInputElement
if (!input.files) return
const availableSlots = maxCount.value - fileList.value.length
if (availableSlots <= 0) {
message.error(`最多只能上传 ${maxCount.value} 个文件!`)
input.value = ''
return
}
const files = Array.from(input.files)
const filesToUpload = files.slice(0, availableSlots)
if (files.length > availableSlots) {
message.error(`只能选择 ${availableSlots} 个文件,已自动筛选`)
}
pendingFiles.value = [...pendingFiles.value, ...filesToUpload]
processPendingFiles()
input.value = ''
}
const processPendingFiles = async() => {
while (pendingFiles.value.length > 0 && fileList.value.length < maxCount.value) {
const file = pendingFiles.value[0]
const previewFile: CustomFile = {
uid: Date.now().toString(),
name: file.name,
status: 'uploading',
url: '',
type: file.type,
cover: '',
title: '',
progress: 0,
}
fileList.value = [...fileList.value, previewFile]
updateModelValue(fileList.value)
try {
await new Promise<void>((resolve, reject) => {
proxy.$ossApi.upload('1011', file.name.replace(/\s+/g, ''), file,
(res: any) => {
const data = res.result
const index = fileList.value.findIndex((f: any) => f.uid === previewFile.uid)
if (index !== -1) {
fileList.value[index] = {
...previewFile,
status: 'done',
url: data.url,
cover: file.type.indexOf('image') >= 0
? data.url
: data.url + '?x-oss-process=video/snapshot,t_2000,m_fast',
id: data.id,
}
updateModelValue(fileList.value)
}
resolve()
},
(err: any) => {
fileList.value = fileList.value.filter((f: any) => f.uid !== previewFile.uid)
updateModelValue(fileList.value)
message.error(`${file.name} 上传失败`)
reject(err)
},
(progress: number) => {
const index = fileList.value.findIndex((f: any) => f.uid === previewFile.uid)
if (index !== -1) {
fileList.value[index].progress = progress
}
},
)
})
} catch (error) {
console.error('上传失败:', error)
} finally {
pendingFiles.value = pendingFiles.value.slice(1)
}
}
}
const customRequest = () => {}
const handlePreview = (file: CustomFile) => {
previewUrl.value = file.url || ''
previewType.value = file.type?.startsWith('video/') ? 'video' : 'image'
previewVisible.value = true
}
const removeFile = (file: CustomFile) => {
fileList.value = fileList.value.filter((item: any) => item.uid !== file.uid)
updateModelValue(fileList.value)
}
const handleTitle = (file: CustomFile) => {
selectedFile.value = file
picTitle.value = file.title || ''
titleModal.value = true
}
const saveTitle = () => {
if (selectedFile.value) {
selectedFile.value.title = picTitle.value
fileList.value.forEach((file: any) => {
if (file.uid === selectedFile.value!.uid) {
file.title = picTitle.value
}
})
updateModelValue(fileList.value)
titleModal.value = false
}
}
const closeTitle = () => {
titleModal.value = false
picTitle.value = ''
}
</script>
<style scoped lang="less">
.upload-container {
position: relative;
padding: 10px;
border: 1px solid #d9d9d9;
border-radius: 4px;
.hidden-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
z-index: 10;
}
}
.ant-upload-list-item-info {
width: 100%;
height: 100%;
position: relative;
}
:deep(.ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item-container) {
margin-block: 0 24px !important;
}
:deep(.ant-upload-wrapper .ant-upload-list.ant-upload-list-picture .ant-upload-list-item),
:deep(.ant-upload-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item) {
padding: 0 !important;
}
:deep(.ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item::before) {
width: 100% !important;
height: 100% !important;
}
.ant-upload-list-item-thumbnail {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
}
.preview-image {
max-width: 100%;
max-height: 400px;
object-fit: contain;
}
.preview-video {
max-width: 100%;
max-height: 400px;
}
.video-thumbnail-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 18px !important;
height: 18px !important;
}
.upload-progress {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 4px;
background: rgba(0,0,0,0.1);
.progress-bar {
height: 100%;
background: #1890ff;
transition: width 0.3s;
}
}
</style>

浙公网安备 33010602011771号