Files
shanxia2.0/web-admin/src/components/ImageUpload/index.vue
T
2026-06-01 08:21:21 +08:00

167 lines
4.3 KiB
Vue

<template>
<div class="component-upload-image">
<el-upload
v-if="!disabled"
v-model:file-list="fileList"
list-type="picture-card"
:limit="limit"
:accept="fileAccept"
:http-request="handleUploadRequest"
:before-upload="handleBeforeUpload"
:on-remove="handleRemove"
:on-preview="handlePreview"
:class="{ hide: displayItems.length >= limit }"
>
<el-icon><Plus /></el-icon>
</el-upload>
<div v-else-if="displayItems.length" class="image-list">
<ImagePreview
v-for="(item, index) in displayItems"
:key="`${item}-${index}`"
:src="item"
:width="100"
:height="100"
:is-oss-id="!isDirectUrl(item)"
/>
</div>
<div v-else-if="placeholder" class="upload-empty">{{ placeholder }}</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import type { UploadFile, UploadRequestOptions } from 'element-plus'
import { ElMessage } from 'element-plus'
import ImagePreview from '@/components/ImagePreview/index.vue'
import { fetchUploadAttachment } from '@/api/admin'
const props = withDefaults(
defineProps<{
modelValue?: string | string[]
limit?: number
fileType?: string[]
disabled?: boolean
placeholder?: string
folder?: string
}>(),
{
modelValue: '',
limit: 15,
fileType: () => ['png', 'jpg', 'jpeg'],
disabled: false,
placeholder: '',
folder: 'crm/customer'
}
)
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const fileList = ref<UploadFile[]>([])
const isDirectUrl = (value: string) => /^https?:\/\//i.test(value)
const displayItems = computed(() => parseValue(props.modelValue))
const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','))
function parseValue(raw: string | string[] | undefined): string[] {
if (!raw) return []
if (Array.isArray(raw)) {
return raw.map((item) => String(item).trim()).filter(Boolean)
}
return String(raw)
.split(',')
.map((item) => item.trim())
.filter(Boolean)
}
function listToString(items: string[]) {
return items.filter(Boolean).join(',')
}
function syncFileListFromModel() {
fileList.value = displayItems.value.map((url, index) => ({
name: url.split('/').pop() || `file-${index + 1}`,
url,
status: 'success'
}))
}
watch(
() => props.modelValue,
() => {
if (props.disabled) return
syncFileListFromModel()
},
{ immediate: true }
)
const handleBeforeUpload = (file: File) => {
const ext = file.name.includes('.') ? file.name.slice(file.name.lastIndexOf('.') + 1).toLowerCase() : ''
const matched = props.fileType.some((type) => {
const normalized = type.toLowerCase()
return file.type.includes(normalized) || ext === normalized
})
if (!matched) {
ElMessage.error(`文件格式不正确,请上传 ${props.fileType.join('/')} 格式文件`)
return false
}
return true
}
async function handleUploadRequest(options: UploadRequestOptions) {
const formData = new FormData()
formData.append('file', options.file)
if (props.folder) {
formData.append('folder', props.folder)
}
try {
const res = await fetchUploadAttachment(formData)
const fileUrl = res?.fileUrl
if (!fileUrl) {
throw new Error('上传失败')
}
const next = [...displayItems.value, fileUrl]
emit('update:modelValue', listToString(next))
options.onSuccess?.(res)
} catch (error: any) {
ElMessage.error(error?.message || '上传失败')
options.onError?.(error)
}
}
function handleRemove(uploadFile: UploadFile) {
const removedUrl = uploadFile.url || (uploadFile.response as { fileUrl?: string })?.fileUrl
if (!removedUrl) return
const next = displayItems.value.filter((url) => url !== removedUrl)
emit('update:modelValue', listToString(next))
}
function handlePreview(uploadFile: UploadFile) {
const url = uploadFile.url || (uploadFile.response as { fileUrl?: string })?.fileUrl
if (url && isDirectUrl(url)) {
window.open(url, '_blank')
}
}
</script>
<style lang="scss" scoped>
.image-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.upload-empty {
color: var(--el-text-color-secondary);
font-size: 13px;
}
:deep(.hide .el-upload--picture-card) {
display: none;
}
</style>