选择图片预览如下:

组件代码
<template>
<div>
<!-- 管理图片按钮 -->
<el-button type="primary" @click="dialogVisible = true">管理图片</el-button>
<!-- 弹窗 -->
<el-dialog
title="管理商品图片"
:visible.sync="dialogVisible"
width="80%"
@close="handleClose"
>
<!-- 图片上传组件 -->
<image-upload
:file-size="maxFileSize"
:is-multiple="true"
:limit="maxImages"
:value="form.goods_images"
:file-type="allowedFileTypes"
@input="afterUpload"
class="upload-section"
></image-upload>
<!-- 预览已上传的图片 -->
<div v-if="form.goods_images.length" class="preview-section">
<el-row :gutter="18">
<el-col
:span="3"
v-for="(image, index) in form.goods_images"
:key="index"
>
<div class="preview-item">
<img :src="image" alt="商品图片预览" class="preview-image"/>
<el-button type="danger" icon="el-icon-delete" circle @click="removeImage(index)"></el-button>
</div>
</el-col>
</el-row>
</div>
<!-- 提示信息 -->
<span class="el-form-item__info">建议图片尺寸:400*400</span>
<!-- 操作按钮 -->
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="confirmChanges">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import ImageUpload from '@/components/UploadFile/image.vue'
export default {
components: {
ImageUpload
},
props: {
value: {
type: Array,
default: () => []
},
maxImages: {
type: Number,
default: 5
},
maxFileSize: {
type: Number,
default: 1
},
allowedFileTypes: {
type: Array,
default: () => ['png', 'jpg', 'jpeg']
}
},
data() {
return {
dialogVisible: false,
form: {
goods_images: this.value.slice() // 使用slice创建副本,避免直接修改props
}
}
},
watch: {
value(newValue) {
this.form.goods_images = newValue.slice() // 确保数据同步
}
},
methods: {
afterUpload(list) {
if (!list || !Array.isArray(list)) return
// 清空当前图片数组并添加新上传的图片
this.form.goods_images = list.map(file => file.url)
},
removeImage(index) {
this.form.goods_images.splice(index, 1)
},
confirmChanges() {
this.$emit('input', this.form.goods_images) // 同步到父组件
this.dialogVisible = false
},
handleClose() {
// 取消时可以选择是否保留更改或恢复原状
// 这里简单处理为不保存更改
this.form.goods_images = this.value.slice() // 恢复原始状态
}
}
}
</script>
<style scoped>
.upload-section {
margin-bottom: 10px;
}
.preview-section {
margin-bottom: 20px;
}
.preview-item {
position: relative;
text-align: center;
}
.preview-image {
max-width: 100%;
height: auto;
border: 1px solid #ccc;
margin-bottom: 5px;
}
.el-button--danger {
position: absolute;
top: -10px;
right: -10px;
}
.el-form-item__info {
font-size: 12px;
color: #999;
}
</style>上传组件:
<template>
<!--
用例:
<image-upload
:file-size="1"
:is-multiple="false"
:limit="1"
:value="form.mpic"
:file-type="['png', 'jpg', 'jpeg','gif']"
@input="afterUpload"
></image-upload>
afterUpload(list){
if (!list) return;
let file = list[0];
if (file){
this.form.mpic = file.url
}
}
-->
<div class="component-upload-image">
<el-upload
:action="uploadImgUrl"
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
name="file"
:on-remove="handleRemove"
:show-file-list="true"
:headers="headers"
:file-list="fileList"
:on-preview="handlePictureCardPreview"
:on-progress="handleOnProgress"
:class="{ hide: this.fileList.length >= this.limit }"
:multiple="isMultiple"
>
<i class="el-icon-plus"></i>
<p class="photo-text">点击上传</p>
</el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip">
请上传
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</template>
<template v-if="fileType">
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
</template>
的文件
</div>
<!-- 预览 -->
<el-dialog :visible.sync="dialogVisible" width="800" append-to-body custom-class="dialog-preview">
<template #title>
<div class="header">
<span>预览</span>
</div>
</template>
<img :src="dialogImageUrl" />
</el-dialog>
</div>
</template>
<script>
import { getToken } from '@/utils/auth'
import { cloneDeep } from 'lodash'
export default {
name: 'ImageUpload',
props: {
value: [String, Object, Array],
// 图片数量限制
limit: {
type: Number,
default: 5,
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5,
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ['png', 'jpg', 'jpeg'],
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true,
},
// 是否多选
isMultiple: {
type: Boolean,
default: true,
},
// 组件下标,如果一个动态表单渲染多个图片组件时使用
index: {
type: Number,
default: 0
}
},
data() {
return {
dialogImageUrl: '',
dialogVisible: false,
hideUpload: false,
baseUrl: '',
uploadImgUrl: process.env.VUE_APP_BASE_API + '/upload/image', // 上传的图片服务器地址
headers: {
Authorization: 'Bearer ' + getToken(),
},
fileList: [],
uploadList: [],
number: 0,
isUploading: false,
}
},
watch: {
value: {
handler(val) {
if (val) {
// 首先将值转为数组
const list = Array.isArray(val) ? val : this.value.split(',')
// 然后将数组转为对象数组
this.fileList = list.map((item) => {
if (typeof item === 'string') {
// console.log(item,this.baseUrl)
if (item.indexOf(this.baseUrl) === -1) {
item = { name: this.baseUrl + item, url: this.baseUrl + item }
} else {
item = { name: item, url: item }
}
}
return item
})
} else {
this.fileList = []
return []
}
},
deep: true,
immediate: true,
},
},
computed: {
// 是否显示提示
showTip() {
return this.isShowTip && (this.fileType || this.fileSize)
},
},
methods: {
// 删除图片
handleRemove(file, fileList) {
const findex = this.fileList.map((f) => f.name).indexOf(file.name)
if (findex > -1) {
this.fileList.splice(findex, 1)
this.$emit('input', cloneDeep(this.fileList))
}
},
// 上传成功回调
handleUploadSuccess(res) {
res = res.data;
this.isUploading = false
this.uploadList.push({name: res.name, url: res.full_url ? res.full_url : this.baseUrl + res.name})
if (this.uploadList.length === this.number) {
this.fileList = this.fileList.concat(this.uploadList)
this.uploadList = []
this.number = 0
this.$emit('input', cloneDeep(this.fileList), this.index)
}
},
// 上传前文件校验
handleBeforeUpload(file) {
let isImg = false
if (this.fileType.length) {
let fileExtension = ''
if (file.name.lastIndexOf('.') > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)
}
isImg = this.fileType.some((type) => {
if (file.type.indexOf(type) > -1) return true
if (fileExtension && fileExtension.indexOf(type) > -1) return true
return false
})
} else {
isImg = file.type.indexOf('image') > -1
}
if (!isImg) {
this.$message.error(`文件格式不正确, 请上传${this.fileType.join('/')}图片格式文件!`)
return false
}
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize
if (!isLt) {
this.$message.error(`上传头像图片大小不能超过 ${this.fileSize} MB!`)
return false
}
}
this.number++
},
// 文件个数超出
handleExceed() {
this.$message.error(`上传文件数量不能超过 ${this.limit} 个!`)
},
// 上传失败
handleUploadError() {
this.$message({
type: 'error',
message: '上传失败',
})
this.isUploading = false
},
// 预览
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url
this.dialogVisible = true
},
// 对象转成指定字符串分隔
listToString(list, separator) {
let strs = ''
separator = separator || ','
for (let i in list) {
strs += list[i].url.replace(this.baseUrl, '') + separator
}
return strs != '' ? strs.substr(0, strs.length - 1) : ''
},
handleOnProgress(event, file, fileList) {
this.isUploading = true
},
},
}
</script>
<style scoped lang="scss">
::v-deep .el-upload--picture-card {
border-radius: 2px;
}
// .el-upload--picture-card 控制加号部分
::v-deep.hide .el-upload--picture-card {
display: none;
}
// 去掉动画效果
::v-deep .el-list-enter-active,
::v-deep .el-list-leave-active {
transition: all 0s;
}
::v-deep .el-list-enter,
.el-list-leave-active {
opacity: 0;
transform: translateY(0);
}
::v-deep .el-upload-list--picture-card .el-upload-list__item-thumbnail {
object-fit: contain;
}
// 角标颜色
::v-deep .el-upload-list--picture-card .el-upload-list__item-status-label {
background-color: #18a8dc;
}
// 已上传图片
::v-deep .el-upload-list {
line-height: 1;
.el-upload-list__item {
margin: 0 16px 16px 0;
width: 104px;
height: 104px;
border: 1px solid #e5eaec;
border-radius: 2px;
.el-progress {
width: 102px !important;
.el-progress-circle {
width: 102px !important;
height: 102px !important;
}
}
}
}
// 上传按钮
::v-deep .el-upload {
position: relative;
width: 104px;
height: 104px;
line-height: 1;
background: #f7f8fb;
.el-icon-plus {
margin-top: 29px;
margin-bottom: 8px;
font-size: 16px;
}
.photo-text {
margin: 0;
font-weight: 400;
font-size: 14px;
line-height: 22px;
text-align: center;
color: #adb1b3;
}
}
// 预览
::v-deep .dialog-preview {
overflow: hidden;
.el-dialog__header {
position: relative;
padding: 11px 24px !important;
z-index: 2;
background: #f8faff;
border-radius: 4px 4px 0 0;
text-align: left;
.header {
display: flex;
align-items: center;
font-size: 14px;
height: 22px;
line-height: 22px;
color: #00244d;
font-weight: bold;
}
.el-dialog__headerbtn {
top: 15px;
right: 24px;
}
}
.el-dialog__body {
padding: 24px;
overflow-y: auto;
img {
display: block;
max-width: 100%;
margin: 0 auto;
}
}
}
</style>实操:
<template>
<div>
<!-- 显示图片区域 -->
<div v-if="productImages.length" class="display-images">
<el-row :gutter="18">
<el-col
:span="3"
v-for="(image, index) in productImages"
:key="index"
>
<div class="preview-item">
<img :src="image" alt="商品图片预览" class="preview-image"/>
</div>
</el-col>
</el-row>
</div>
<!-- 商品图片管理器 -->
<GoodsImageManager
v-model="productImages"
:max-images="6"
:max-file-size="2"
:allowed-file-types="['png', 'jpg']"
/>
</div>
</template>
<script>
import GoodsImageManager from './components/GoodsImageManager.vue';
export default {
components: {
GoodsImageManager,
},
data() {
return {
productImages: [
'http://example.com/image1.jpg',
'http://example.com/image2.jpg',
],
};
},
};
</script>
<style scoped>
.display-images {
margin-bottom: 20px;
}
.preview-item {
text-align: center;
}
.preview-image {
max-width: 100%;
height: auto;
border: 1px solid #ccc;
margin-bottom: 5px;
}
</style>