531 lines
14 KiB
Vue
531 lines
14 KiB
Vue
<template>
|
||
<section class="uploader">
|
||
<el-upload
|
||
action
|
||
multiple
|
||
ref="upload"
|
||
:class="{validError:!validateState}"
|
||
:http-request="submitUpload"
|
||
:on-remove="handleRemove"
|
||
:on-change="handleChange"
|
||
:before-upload="onBeforeUpload"
|
||
:file-list="fileList"
|
||
:limit="limit"
|
||
:disabled="disabled"
|
||
:list-type="isImg ? 'picture-card' : 'text'"
|
||
:accept="accept"
|
||
:show-file-list="!isSingle"
|
||
:on-preview="handlePictureCardPreview"
|
||
:auto-upload="isAutoUpload"
|
||
:on-exceed="handleExceed">
|
||
<template v-if="!disabled">
|
||
<template v-if="hasUploaded&&isSingle">
|
||
<div class="fileItem">
|
||
<div class="uploadFile" @click.stop>
|
||
<ai-icon type="svg" :icon="uploadFile.icon"/>
|
||
<div class="info">
|
||
<span v-text="uploadFile.name"/>
|
||
<span class="size" v-text="uploadFile.size"/>
|
||
</div>
|
||
</div>
|
||
<el-button>重新选择</el-button>
|
||
<el-button v-if="clearable" plain type="danger" @click.stop="handleClear">删除</el-button>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="limit > fileList.length">
|
||
<slot v-if="hasTriggerSlot" name="trigger"/>
|
||
<div v-else class="uploaderBox">
|
||
<span class="iconfont" :class="isImg ? 'iconPhoto' : 'iconAdd'"/>
|
||
<p>上传{{ isImg ? '图片' : '附件' }}</p>
|
||
</div>
|
||
</template>
|
||
<div slot="tip" class="el-upload__tip" v-if="showTips">
|
||
<p v-if="fileType === 'img' && !acceptType && !hasTipsSlot">最多上传{{
|
||
limit
|
||
}}张图片,单个文件最大10MB,支持jpg、jpeg、png格式</p>
|
||
<p v-if="fileType === 'file' && !acceptType && !hasTipsSlot">最多上传{{ limit }}个附件,单个文件最大10MB</p>
|
||
<p v-if="fileType === 'file' && !acceptType && !hasTipsSlot">
|
||
支持.zip、.rar、.doc、.docx、.xls、.xlsx、.ppt、.pptx、.pdf、.txt、.jpg、.png格式</p>
|
||
<p>
|
||
<slot name="tips" v-if="hasTipsSlot"></slot>
|
||
</p>
|
||
</div>
|
||
</template>
|
||
</el-upload>
|
||
<el-dialog :visible.sync="dialog" title="图片预览编辑器" :modal="false" :show-close="false" append-to-body>
|
||
<vue-cropper
|
||
ref="cropper"
|
||
style="height: 400px;"
|
||
:img="fileList.length ? fileList[0].url : ''" v-bind="crop"/>
|
||
<div style="text-align: center;margin-top: 10px;">
|
||
<el-radio-group v-if="crop.fixed" size="small" v-model="currFixedIndex" @change="onFixedChange"
|
||
style="margin-right: 8px;">
|
||
<el-radio-button
|
||
:label="0"
|
||
size="small">
|
||
1.6:1
|
||
</el-radio-button>
|
||
<el-radio-button
|
||
:label="1"
|
||
size="small">
|
||
4:3
|
||
</el-radio-button>
|
||
</el-radio-group>
|
||
<el-button size="small" circle icon="el-icon-refresh-right" @click="$refs.cropper.rotateRight()"></el-button>
|
||
<el-button size="small" circle icon="el-icon-refresh-left" @click="$refs.cropper.rotateLeft()"></el-button>
|
||
</div>
|
||
<div slot="footer">
|
||
<el-popconfirm title="是否关闭图片预览器,并上传图片?" @confirm="previewCrop">
|
||
<el-button slot="reference" type="primary">保存</el-button>
|
||
</el-popconfirm>
|
||
<el-button @click="onClose">关闭</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
<div class="images" v-viewer="{movable: true}" v-show="false">
|
||
<img v-for="(item, index) in imgList" :src="item" :key="index" alt="">
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<script>
|
||
import {VueCropper} from 'vue-cropper'
|
||
import 'viewerjs/dist/viewer.css'
|
||
import Viewer from 'v-viewer'
|
||
import Vue from "vue";
|
||
|
||
Viewer.setDefaults({
|
||
zIndex: 20170
|
||
})
|
||
Vue.use(Viewer)
|
||
export default {
|
||
name: 'AiUploader',
|
||
components: {VueCropper},
|
||
inject: {
|
||
elFormItem: {default: ""},
|
||
elForm: {default: ''},
|
||
},
|
||
model: {
|
||
prop: 'value',
|
||
event: 'change'
|
||
},
|
||
props: {
|
||
value: {default: () => []},
|
||
url: {
|
||
type: String,
|
||
default: '/admin/file/add'
|
||
},
|
||
isShowTip: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
isWechat: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
maxSize: {
|
||
type: Number,
|
||
default: 10
|
||
},
|
||
instance: Function,
|
||
acceptType: {type: String},
|
||
fileType: {type: String, default: 'img'},
|
||
limit: {type: Number, default: 9},
|
||
disabled: {type: Boolean, default: false},
|
||
isCrop: {type: Boolean, default: false},
|
||
cropOps: Object,
|
||
isImport: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
clearable: {default: true},
|
||
valueIsUrl: Boolean
|
||
},
|
||
data() {
|
||
return {
|
||
fileList: [],
|
||
dialog: false,
|
||
currFixedIndex: 0,
|
||
}
|
||
},
|
||
watch: {
|
||
value: {
|
||
handler(v) {
|
||
this.dispatch('ElFormItem', 'el.form.change', [v]);
|
||
if (v?.length > 0) {
|
||
this.fileList = this.valueIsUrl ? v?.split(",")?.map(url => ({url})) : [...v]
|
||
}
|
||
},
|
||
immediate: true,
|
||
deep: true
|
||
}
|
||
},
|
||
computed: {
|
||
isImg() {
|
||
return this.fileType === 'img'
|
||
},
|
||
validateState() {
|
||
return ['', 'success'].includes(this.elFormItem?.validateState)
|
||
},
|
||
isAutoUpload() {
|
||
return !(this.isCrop || this.isImport);
|
||
},
|
||
hasTipsSlot() {
|
||
return this.$slots.tips
|
||
},
|
||
hasTriggerSlot() {
|
||
return this.$slots.trigger
|
||
},
|
||
accept() {
|
||
if (this.acceptType) {
|
||
return this.acceptType
|
||
}
|
||
|
||
return this.isImg ? '.jpg,.png,.jpeg' : '.zip,.rar,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.txt,.jpg,.png,.mp4'
|
||
},
|
||
crop() {
|
||
return {
|
||
autoCrop: true,
|
||
outputType: 'png',
|
||
fixedBox: false,
|
||
fixed: true,
|
||
fixedNumber: [1.6, 1],
|
||
width: 0,
|
||
height: 0,
|
||
...this.cropOps
|
||
}
|
||
},
|
||
imgList() {
|
||
return this.fileList.map(v => v.url)
|
||
},
|
||
isSingle() {
|
||
return this.limit == 1 && this.isImport
|
||
},
|
||
showTips() {
|
||
return this.isShowTip || this.$slots.tips
|
||
},
|
||
hasUploaded() {
|
||
return this.fileList?.length > 0
|
||
},
|
||
uploadFile() {
|
||
let file = this.fileList?.[0],
|
||
size = Number(file.size),
|
||
icon = "iconTxt"
|
||
//显示大小
|
||
if (size > Math.pow(1024, 2)) {
|
||
size = (size / Math.pow(1024, 2)).toFixed(1) + 'MB'
|
||
} else {
|
||
size = (size / 1024).toFixed(1) + 'KB'
|
||
}
|
||
//显示图标
|
||
if (/\.(xls|xlsx)$/.test(file.name)) {
|
||
icon = "iconExcel"
|
||
} else if (/\.(zip)$/.test(file.name)) {
|
||
icon = "iconZip"
|
||
} else if (/\.(rar)$/.test(file.name)) {
|
||
icon = "iconRar"
|
||
} else if (/\.(png)$/.test(file.name)) {
|
||
icon = "iconPng"
|
||
} else if (/\.(pptx|ppt)$/.test(file.name)) {
|
||
icon = "iconPPT"
|
||
} else if (/\.(doc|docx)$/.test(file.name)) {
|
||
icon = "iconWord"
|
||
}
|
||
|
||
return {...file, size, icon}
|
||
}
|
||
},
|
||
methods: {
|
||
onFixedChange(e) {
|
||
this.fixedNumber = e === 0 ? [1.6, 1] : [4, 3]
|
||
|
||
this.$nextTick(() => {
|
||
this.$refs.cropper.goAutoCrop()
|
||
})
|
||
},
|
||
handleChange(file, fileList) {
|
||
if (this.isImport) {
|
||
if (!this.onOverSize(file)) {
|
||
this.fileList = []
|
||
return false
|
||
}
|
||
this.fileList = fileList
|
||
this.emitChange(fileList)
|
||
return false
|
||
}
|
||
if (this.isCrop) {
|
||
if (file.raw.type === 'image/gif') {
|
||
this.$message.error(`不支持gif格式的图片`)
|
||
this.fileList = []
|
||
|
||
return false
|
||
}
|
||
|
||
if (!this.onOverSize(file)) {
|
||
this.fileList = []
|
||
|
||
return false
|
||
}
|
||
this.dialog = true
|
||
this.fileList = fileList
|
||
} else {
|
||
this.fileList = fileList
|
||
}
|
||
},
|
||
handleExceed(files) {
|
||
if (this.isSingle && files[0]) {
|
||
this.$refs.upload?.clearFiles()
|
||
this.$refs.upload?.handleStart(files[0])
|
||
} else this.$message.warning(`最多上传${this.limit}个${this.isImg ? '图片' : '文件'}`)
|
||
},
|
||
handlePictureCardPreview(file) {
|
||
if (this.fileType !== 'img') return
|
||
|
||
const index = this.imgList.indexOf(file.url)
|
||
const viewer = this.$el.querySelector('.images').$viewer
|
||
viewer.view(index)
|
||
},
|
||
|
||
handleRemove(file, fileList) {
|
||
this.fileList = fileList
|
||
this.emitChange(fileList)
|
||
},
|
||
emitChange(files) {
|
||
this.$emit('change', this.valueIsUrl ? files?.map(e => e.url)?.toString() : files)
|
||
},
|
||
handleClear() {
|
||
this.fileList = []
|
||
},
|
||
getExtension(name) {
|
||
return name.substring(name.lastIndexOf('.'))
|
||
},
|
||
|
||
onOverSize(e) {
|
||
const isLt10M = e.size / 1024 / 1024 < this.maxSize
|
||
const suffixName = this.getExtension(e.name)
|
||
const suffixNameList = this.accept.split(',')
|
||
|
||
if (suffixNameList.indexOf(`${suffixName.toLowerCase()}`) === -1) {
|
||
this.$message.error(`不支持该格式`)
|
||
return false
|
||
}
|
||
|
||
if (!isLt10M) {
|
||
this.$message.error(`${this.isImg ? '图片' : '文件'}大小不超过${this.maxSize}MB!`)
|
||
return false
|
||
}
|
||
|
||
return true
|
||
},
|
||
|
||
onBeforeUpload(event) {
|
||
return this.onOverSize(event)
|
||
},
|
||
|
||
onClose() {
|
||
this.fileList = []
|
||
this.dialog = false
|
||
},
|
||
|
||
submitUpload(file) {
|
||
let formData = new FormData()
|
||
formData.append('file', file.file)
|
||
this.instance.post(this.url, formData, {
|
||
withCredentials: false
|
||
}).then(res => {
|
||
if (res?.code == 0) {
|
||
if (this.isWechat) {
|
||
this.emitChange([{
|
||
...res.data.file,
|
||
media: res.data.media
|
||
}])
|
||
this.fileList.forEach(item => {
|
||
if (item.uid === file.file.uid) {
|
||
item.id = res.data.file.id
|
||
item.path = res.data.file.url
|
||
item.url = res.data.file.url,
|
||
item.media = res.data.media
|
||
}
|
||
})
|
||
this.emitChange(this.fileList)
|
||
this.$message.success('上传成功')
|
||
return false
|
||
}
|
||
|
||
if (Object.prototype.toString.call(res.data) === '[object Object]') {
|
||
this.fileList.forEach(item => {
|
||
if (item.uid === file.file.uid) {
|
||
item.id = res.data.id
|
||
item.path = res.data.url
|
||
item.url = res.data.url
|
||
item.url = res.data.url
|
||
item.fileSizeStr = res.data.fileSizeStr
|
||
item.fileSize = res.data.size
|
||
item.postfix = res.data.postfix
|
||
}
|
||
})
|
||
} else {
|
||
let data = res.data[0].split(';')
|
||
this.fileList.forEach(item => {
|
||
if (item.uid === file.file.uid) {
|
||
item.id = data[1]
|
||
item.path = data[0]
|
||
item.url = data[0]
|
||
}
|
||
})
|
||
}
|
||
|
||
this.emitChange(this.fileList)
|
||
this.$message.success('上传成功')
|
||
}
|
||
})
|
||
},
|
||
previewCrop() {
|
||
this.$refs.cropper.getCropBlob(data => {
|
||
data.name = this.fileList[0].name;
|
||
this.fileList[0].file = new window.File([data], data.name, {type: data.type})
|
||
this.fileList[0].file.uid = this.fileList[0].uid
|
||
this.submitUpload(this.fileList[0])
|
||
this.dialog = false
|
||
})
|
||
},
|
||
/**
|
||
* 表单验证
|
||
* @param componentName
|
||
* @param eventName
|
||
* @param params
|
||
*/
|
||
dispatch(componentName, eventName, params) {
|
||
let parent = this.$parent || this.$root;
|
||
let name = parent.$options.componentName;
|
||
|
||
while (parent && (!name || name !== componentName)) {
|
||
parent = parent.$parent;
|
||
|
||
if (parent) {
|
||
name = parent.$options.componentName;
|
||
}
|
||
}
|
||
if (parent) {
|
||
parent.$emit.apply(parent, [eventName].concat(params));
|
||
}
|
||
},
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.uploader {
|
||
line-height: 1;
|
||
|
||
:deep(.el-upload ){
|
||
width: 100%;
|
||
text-align: start;
|
||
}
|
||
|
||
:deep(.validError ){
|
||
.el-button {
|
||
border-color: #f46;
|
||
color: #f46;
|
||
}
|
||
}
|
||
|
||
:deep(.el-upload--picture-card ){
|
||
border: none;
|
||
}
|
||
|
||
:deep( .el-list-leave-active), :deep( .el-upload-list__item ){
|
||
transition: all 0s !important;
|
||
}
|
||
|
||
:deep(.el-upload-list--picture-card .el-upload-list__item ){
|
||
width: 120px;
|
||
height: 120px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
:deep(.el-upload--picture-card ){
|
||
width: auto;
|
||
height: auto;
|
||
}
|
||
|
||
.el-upload__tip p {
|
||
color: #999;
|
||
line-height: 16px;
|
||
}
|
||
|
||
:deep(.fileItem ){
|
||
width: 100%;
|
||
height: 60px;
|
||
background: #FFFFFF;
|
||
border-radius: 2px;
|
||
border: 1px solid #D0D4DC;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 16px;
|
||
cursor: default;
|
||
|
||
.uploadFile {
|
||
text-align: start;
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
color: #222;
|
||
|
||
.AiIcon {
|
||
width: 40px;
|
||
height: 40px;
|
||
}
|
||
|
||
.info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-left: 8px;
|
||
margin-right: 40px;
|
||
line-height: 22px;
|
||
}
|
||
|
||
.size {
|
||
color: #888;
|
||
}
|
||
}
|
||
}
|
||
|
||
.uploaderBox {
|
||
width: 120px;
|
||
height: 120px;
|
||
line-height: 1;
|
||
background: #F3F4F7;
|
||
border-radius: 2px;
|
||
border: 1px solid #D0D4DC;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
|
||
&:hover {
|
||
opacity: 0.6;
|
||
}
|
||
|
||
span {
|
||
font-size: 32px;
|
||
color: #8899bb;
|
||
|
||
&:hover {
|
||
color: #8899bb;
|
||
}
|
||
}
|
||
|
||
p {
|
||
margin: 0;
|
||
padding-top: 4px;
|
||
color: #555;
|
||
font-size: 12px;
|
||
text-align: center;
|
||
line-height: 1;
|
||
}
|
||
}
|
||
}
|
||
</style>
|