<template>
<div class="uploadBox ">
<el-upload
ref="uploadRef"
class="avatar-uploader"
:action="uploadAction"
:headers="initData.headers"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:on-error="handleUploadError"
:before-upload="beforeAvatarUpload"
:disabled="disabled || !!imageUrl"
>
<!-- 已上传的图片:点击预览 -->
<div v-if="imageUrl" class="image-preview-wrapper" @click.stop="handlePreview">
<img :src="imageUrl" class="avatar" :style="{ objectFit: fitMode }" />
<div class="preview-mask">
<el-icon class="preview-icon"><ZoomIn /></el-icon>
<span class="preview-text">{{ $t('components.clickPreview') }}</span>
</div>
</div>
<!-- 未上传:显示上传区域 -->
<div v-else class="upload-img">
<div class="img-box" :style="imageStyle"></div>
<div class="upload-text">
<icon-add class="iconColor" />
<span class="upload-title">{{ title || $t('components.image') }}</span>
</div>
</div>
</el-upload>
<!-- 删除按钮 -->
<el-button
v-if="imageUrl && !disabled"
class="delete-btn "
type="danger"
size="small"
circle
@click.stop="handleDelete"
>
<el-icon><Delete /></el-icon>
</el-button>
<!-- 图片预览器 -->
<el-image-viewer
v-if="showViewer"
:url-list="[imageUrl]"
:initial-index="0"
@close="closeViewer"
:z-index="3000"
hide-on-click-modal
/>
</div>
</template>
<script setup>
import notification from '@/utils/notification'
import { ElMessageBox } from 'element-plus'
import { ref, computed, watch, onMounted } from 'vue'
import { Delete, ZoomIn, Upload } from '@element-plus/icons-vue'
import { ElImageViewer } from 'element-plus'
import { getToken } from '@/utils/auth'
import uploadImg from '@/assets/images/upload-img.png'
import licenImg from '@/assets/images/licen.png'
import obverseImg from '@/assets/images/obverselmg.png'
import { getVisitorId } from '@/utils/fingerprintjs.js'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
// 定义 props:接受父组件传递的参数
const props = defineProps({
// 标题
title: {
type: String,
default: ''
},
// 类型:front-正面, obverse-反面, enterprise-企业
type: {
type: String,
default: ''
},
index: {
type: Number,
default: 0
},
// 上传接口地址
action: {
type: String,
default: '/csc/member/upload'
},
// 最大文件大小(MB)
maxSize: {
type: Number,
default: 20
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 回显图片URL
modelValue: {
type: String,
default: ''
},
// 是否检查文件名包含身份证关键词
checkIdCardKeyword: {
type: Boolean,
default: false
},
// 图片显示模式:contain-完整显示, cover-填充裁剪
fitMode: {
type: String,
default: 'fill',
validator: (value) => ['contain', 'cover', 'fill', 'scale-down'].includes(value)
}
})
// 定义事件
const emit = defineEmits(['update:modelValue', 'uploadSuccess', 'uploadError', 'delete'])
// 上传 ref
const uploadRef = ref(null)
// 图片 URL
const imageUrl = ref(props.modelValue || '')
// 上传状态
const isUploaded = ref(false)
// 图片预览器显示状态
const showViewer = ref(false)
// 监听 modelValue 变化(回显)
watch(
() => props.modelValue,
(newVal) => {
imageUrl.value = newVal || ''
},
{ immediate: true }
)
// 计算背景图片样式
const imageStyle = computed(() => {
if (props.type === 'business_license') {
return { 'background-image': `url(${licenImg})` }
}
if (props.type === 'front') {
return { 'background-image': `url(${uploadImg})` }
}
if (props.type === 'obverse') {
return { 'background-image': `url(${obverseImg})` }
}
return {}
})
// 上传接口地址
const uploadAction = computed(() => props.action)
// 上传请求头
// const uploadHeaders = computed(() => ({
// Authorization: `Bearer ${getToken()}`
// }))
const initData = ref({
maskVisible: false,
headers: {
Authorization: 'Bearer ' + getToken()
},
uploadImg: '',
progressVisible: false,
progressPercentage: 0,
progressStatus: ''
})
// 上传前验证
const beforeAvatarUpload = (file) => {
console.log('开始上传文件:', file)
// 验证文件类型(修复:使用 === 而不是 =)
const isJPG = file.type === 'image/jpeg'
const isPNG = file.type === 'image/png'
const isJPEG = file.type === 'image/jpeg'
const isWebP = file.type === 'image/webp'
if (!isJPG && !isPNG && !isJPEG && !isWebP) {
notification.error(t('components.uploadFailed') + ',' + t('components.onlySupportJpgPngJpegWebp'))
return false
}
// 验证文件大小
const isLtMaxSize = file.size / 1024 / 1024 < props.maxSize
if (!isLtMaxSize) {
notification.error(`${t('components.uploadFailed')},${t('components.imageSizeCannotExceed')} ${props.maxSize}MB`)
return false
}
// 可选:验证文件名包含身份证关键词
if (props.checkIdCardKeyword) {
const fileName = file.name.toLowerCase()
const idCardKeywords = ['身份证', 'idcard', 'id-card', '身份证电子版']
const hasKeyword = idCardKeywords.some(keyword => fileName.includes(keyword))
if (hasKeyword) {
notification.error(t('components.uploadFailed') + ',' + t('components.noElectronicIdCard'))
return false
}
}
return true
}
// 上传成功处理
const handleAvatarSuccess = (response, uploadFile) => {
console.log('上传成功:', response, uploadFile)
// 获取图片 URL(根据实际接口返回格式调整)
const url = response.data?.url || response.url || URL.createObjectURL(uploadFile.raw)
imageUrl.value = url
isUploaded.value = true
// 通知父组件
emit('update:modelValue', url)
emit('uploadSuccess', {
type: props.type,
url: url,
file: uploadFile
},props.index)
notification.success(t('components.imageUploadSuccess'))
}
// 上传失败处理
const handleUploadError = (error, uploadFile) => {
console.error('上传失败:', error)
emit('uploadError', {
type: props.type,
error: error,
file: uploadFile
})
notification.error(t('components.imageUploadFailed'))
}
// 删除图片
const handleDelete = async () => {
try {
await ElMessageBox.confirm(t('components.confirmDeleteImage'), t('common.info'), {
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning',
})
imageUrl.value = ''
isUploaded.value = false
// 通知父组件更新 v-model
emit('update:modelValue', '')
emit('delete', props.type,props.index)
notification.success(t('components.deleteSuccess'))
} catch {
// 用户取消删除
}
}
// 手动触发上传
const triggerUpload = () => {
uploadRef.value?.$refs?.['upload-inner']?.$refs?.input?.click()
}
// 清空上传
const clearUpload = () => {
imageUrl.value = ''
isUploaded.value = false
emit('update:modelValue', '')
}
// 预览图片
const handlePreview = () => {
if (imageUrl.value) {
showViewer.value = true
}
}
// 关闭预览
const closeViewer = () => {
showViewer.value = false
}
const getvisitorData = async () => {
let visitorId = localStorage.getItem('X-VISITOR-ID')
if (!visitorId) {
visitorId = await getVisitorId() // 安全调用 async
}
if (visitorId) {
initData.value.headers['X-VISITOR-ID'] = visitorId
} else {
initData.value.headers['X-VISITOR-ID'] = 'default-visitor-id'
}
}
// 暴露方法给父组件
defineExpose({
uploadRef,
isUploaded,
imageUrl,
triggerUpload,
clearUpload
})
onMounted(() => {
getvisitorData()
})
</script>
<style lang="scss" scoped>
.uploadBox {
position: relative;
width: 200px;
height: 180px;
background: #ffffff0c;
border: 1px solid #ffffff19;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
&:hover {
border-color: #1D5CCC;
box-shadow: 0 2px 12px rgba(29, 92, 204, 0.15);
}
.avatar-uploader {
width: 100%;
height: 100%;
}
.avatar {
width: 100%;
height: 100%;
display: block;
/* // object-fit 通过 props.fitMode 动态设置
// 默认 contain:完整显示图片,保持比例
// cover:填充容器,可能裁剪
// fill:拉伸填充
// scale-down:取 none 或 contain 中较小者 */
}
/* // 图片预览包装器 */
.image-preview-wrapper {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
overflow: hidden;
background: #f5f7fa; /* 添加背景色,避免透明区域不清晰*/
.avatar {
width: 100%;
height: 100%;
display: block;
transition: transform 0.3s ease;
/* // object-fit 通过 props.fitMode 动态设置 */
}
/* // 预览遮罩层 */
.preview-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
.preview-icon {
font-size: 32px;
color: #fff;
margin-bottom: 8px;
}
.preview-text {
color: #fff;
font-size: 14px;
font-weight: 500;
}
}
&:hover {
.preview-mask {
opacity: 1;
}
.avatar {
transform: scale(1.05);
}
}
}
.upload-img {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
cursor: pointer;
.img-box {
width: 90px;
height: 62px;
margin: 28px 0;
background-repeat: no-repeat;
background-size: 100% 100%;
background-position: center;
}
.upload-text {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
margin-top: 10px;
gap: 6px;
.upload-title {
color: #808080;
font-size: 14px;
transition: color 0.3s ease;
}
}
&:hover .upload-title {
color: #1D5CCC;
}
}
/* // 删除按钮 */
.delete-btn {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
background: rgba(255, 77, 79, 0.9);
border: none;
transition: all 0.3s ease;
&:hover {
background: rgba(255, 77, 79, 1);
transform: scale(1.1);
}
}
/* // 重新上传按钮 */
.reupload-btn {
position: absolute;
top: 8px;
right: 48px;
z-index: 10;
background: rgba(29, 92, 204, 0.9);
border: none;
transition: all 0.3s ease;
&:hover {
background: rgba(29, 92, 204, 1);
transform: scale(1.1);
}
}
:deep(.el-upload) {
width: 100%;
height: 100%;
}
:deep(.el-upload--text) {
width: 100%;
height: 100%;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
:deep(.el-upload.is-disabled) {
cursor: not-allowed;
opacity: 0.6;
}
}
.iconColor {
width: 16px;
height: 16px;
fill: #1D5CCC;
transition: fill 0.3s ease;
}
.upload-img:hover .iconColor {
fill: #1D5CCC;
}
</style>当点击的时候怎么设置背景颜色
最新发布