功能变化: 更新组件
parent
e1071f1381
commit
03878eec5a
|
@ -4,6 +4,6 @@ COPY web/. .
|
||||||
RUN npm install --registry=https://registry.npm.taobao.org
|
RUN npm install --registry=https://registry.npm.taobao.org
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM nginx:alpine
|
FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/nginx:alpine
|
||||||
COPY ./docker_env/nginx/my.conf /etc/nginx/conf.d/my.conf
|
COPY ./docker_env/nginx/my.conf /etc/nginx/conf.d/my.conf
|
||||||
COPY --from=0 /web/dist /usr/share/nginx/html
|
COPY --from=0 /web/dist /usr/share/nginx/html
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import D2pImagesFormat from './lib/images-format'
|
||||||
|
import D2pFilesFormat from './lib/files-format'
|
||||||
|
import 'cropperjs/dist/cropper.css'
|
||||||
|
import D2pUploader from 'd2p-extends/src/uploader'
|
||||||
|
|
||||||
|
function install (Vue, options) {
|
||||||
|
Vue.component('d2p-file-uploader', () => import('./lib/file-uploader'))
|
||||||
|
Vue.component('d2p-images-format', D2pImagesFormat)
|
||||||
|
Vue.component('d2p-cropper-uploader', () => import('./lib/cropper-uploader'))
|
||||||
|
Vue.component('d2p-cropper', () => import('./lib/cropper'))
|
||||||
|
Vue.component('d2p-files-format', D2pFilesFormat)
|
||||||
|
|
||||||
|
if (options != null) {
|
||||||
|
Vue.use(D2pUploader, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createAllUploadedValidator = (getFormComponentRef) => {
|
||||||
|
return (rule, value, callback) => {
|
||||||
|
const ref = getFormComponentRef(rule.fullField)
|
||||||
|
if (ref && ref.isHasUploadingItem()) {
|
||||||
|
callback(new Error('还有未上传完成的文件'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install,
|
||||||
|
createAllUploadedValidator
|
||||||
|
}
|
|
@ -0,0 +1,377 @@
|
||||||
|
<template>
|
||||||
|
<div class="d2p-cropper-uploader" :class="{'is-disabled':disabled}" >
|
||||||
|
<div class="image-list">
|
||||||
|
<div class="image-item" v-for="(item,index) in list" :key="index">
|
||||||
|
<el-image class="image"
|
||||||
|
:src="item.dataUrl?item.dataUrl:item.url"
|
||||||
|
:data-src="item.url"
|
||||||
|
:preview-src-list="_urlList"
|
||||||
|
fit="contain" >
|
||||||
|
<div slot="placeholder" class="image-slot">
|
||||||
|
<img src="./loading-spin.svg">
|
||||||
|
</div>
|
||||||
|
</el-image>
|
||||||
|
<div class="delete" v-if="!disabled"><i class="el-icon-delete" @click="removeImage(index,item)"></i></div>
|
||||||
|
<div class="status-uploading" v-if="item.status==='uploading'">
|
||||||
|
<el-progress type="circle" :percentage="item.progress" :width="70"/>
|
||||||
|
</div>
|
||||||
|
<div class="status-done" v-else-if="item.status==='done'">
|
||||||
|
<i class="el-icon-upload-success el-icon-check"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="limit <=0 || limit>list.length" class="image-item image-plus" @click="addNewImage">
|
||||||
|
<i class="el-icon-plus cropper-uploader-icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<d2p-cropper ref="cropper"
|
||||||
|
:title="title"
|
||||||
|
:cropperHeight="cropperHeight"
|
||||||
|
:dialogWidth="dialogWidth"
|
||||||
|
:accept="accept"
|
||||||
|
:uploadTip="uploadTip"
|
||||||
|
:maxSize="maxSize"
|
||||||
|
:cropper="cropper"
|
||||||
|
output="all"
|
||||||
|
@done="cropComplete"
|
||||||
|
></d2p-cropper>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import D2pCropper from './cropper'
|
||||||
|
import D2pUploader from 'd2p-extends/src/uploader'
|
||||||
|
import { d2CrudPlus } from 'd2-crud-plus'
|
||||||
|
import log from 'd2p-extends/src/utils/util.log'
|
||||||
|
/**
|
||||||
|
* 图片裁剪上传组件,封装了d2p-cropper, d2p-cropper内部封装了cropperjs
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'd2p-cropper-uploader',
|
||||||
|
mixins: [d2CrudPlus.inputBase],
|
||||||
|
components: {
|
||||||
|
D2pCropper
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
// 初始图片url
|
||||||
|
value: {
|
||||||
|
type: [String, Array]
|
||||||
|
},
|
||||||
|
// 上传后端类型,[form, cos, qiniu , alioss]
|
||||||
|
type: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
// 上传提示
|
||||||
|
uploadTip: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
// 对话框标题
|
||||||
|
title: String,
|
||||||
|
// cropper的高度,默认为浏览器可视窗口高度的40%,最小270
|
||||||
|
cropperHeight: {
|
||||||
|
type: [String, Number]
|
||||||
|
},
|
||||||
|
// 对话框宽度,默认50%
|
||||||
|
dialogWidth: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '50%'
|
||||||
|
},
|
||||||
|
// 图片大小限制,单位MB
|
||||||
|
maxSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 5
|
||||||
|
},
|
||||||
|
// 图片数量限制,0为不限制
|
||||||
|
limit: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
// 可接收的文件后缀
|
||||||
|
accept: {
|
||||||
|
type: String,
|
||||||
|
default: '.jpg, .jpeg, .png, .gif, .webp'
|
||||||
|
},
|
||||||
|
// [cropperjs的参数](https://github.com/fengyuanchen/cropperjs)
|
||||||
|
cropper: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
// 上传参数,会临时覆盖全局上传配置参数[d2p-uploader](/guide/extends/uploader.html)
|
||||||
|
uploader: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
// 构建下载url方法,不影响提交的value
|
||||||
|
buildUrl: {
|
||||||
|
type: Function,
|
||||||
|
default: function (value, item) { return (typeof value === 'object') ? item.url : value }
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
index: undefined,
|
||||||
|
list: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value (val) {
|
||||||
|
this.$emit('change', val)
|
||||||
|
if (val === this.emitValue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.initValue(val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.emitValue = this.value
|
||||||
|
this.initValue(this.value)
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
_urlList () {
|
||||||
|
const urlList = []
|
||||||
|
if (this.list) {
|
||||||
|
for (const item of this.list) {
|
||||||
|
urlList.push(item.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urlList
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initValue (value) {
|
||||||
|
const list = []
|
||||||
|
if (value == null || value === '') {
|
||||||
|
this.$set(this, 'list', list)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof (value) === 'string') {
|
||||||
|
list.push({ url: this.buildUrl(value), value: value, status: 'done' })
|
||||||
|
} else {
|
||||||
|
for (const item of value) {
|
||||||
|
list.push({ url: this.buildUrl(item), value: item, status: 'done' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$set(this, 'list', list)
|
||||||
|
},
|
||||||
|
addNewImage () {
|
||||||
|
if (this.disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.index = undefined
|
||||||
|
this.$refs.cropper.clear()
|
||||||
|
this.$refs.cropper.open()
|
||||||
|
},
|
||||||
|
removeImage (index, item) {
|
||||||
|
this.list.splice(index, 1)
|
||||||
|
this.emit()
|
||||||
|
},
|
||||||
|
isHasUploadingItem () {
|
||||||
|
const fileList = this.list
|
||||||
|
if (fileList && fileList.length > 0) {
|
||||||
|
for (const item of fileList) {
|
||||||
|
if (item.status === 'uploading') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
async cropComplete (ret) {
|
||||||
|
const blob = ret.blob
|
||||||
|
const dataUrl = ret.dataUrl
|
||||||
|
const file = ret.file
|
||||||
|
// 开始上传
|
||||||
|
const item = {
|
||||||
|
url: undefined,
|
||||||
|
dataUrl: dataUrl,
|
||||||
|
status: 'uploading',
|
||||||
|
progress: 0
|
||||||
|
}
|
||||||
|
const onProgress = (e) => {
|
||||||
|
item.progress = e.percent
|
||||||
|
}
|
||||||
|
const onError = (e) => {
|
||||||
|
item.status = 'error'
|
||||||
|
item.message = '文件上传出错:' + e.message
|
||||||
|
log.debug(e)
|
||||||
|
}
|
||||||
|
log.debug('blob:', blob)
|
||||||
|
const option = {
|
||||||
|
file: blob,
|
||||||
|
fileName: file.name,
|
||||||
|
onProgress,
|
||||||
|
onError
|
||||||
|
}
|
||||||
|
this.list.push(item)
|
||||||
|
const upload = await this.doUpload(option)
|
||||||
|
item.url = this.buildUrl(upload.url)
|
||||||
|
item.value = upload.url
|
||||||
|
item.status = 'done'
|
||||||
|
this.emit()
|
||||||
|
},
|
||||||
|
doUpload (option) {
|
||||||
|
option.config = this.uploader
|
||||||
|
return this.getUploader().then(uploader => {
|
||||||
|
return uploader.upload(option).then(ret => {
|
||||||
|
if (this.suffix != null) {
|
||||||
|
ret.url += this.suffix
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getUploader () {
|
||||||
|
let type = this.type
|
||||||
|
if (this.uploader != null && this.uploader.type != null) {
|
||||||
|
type = this.uploader.type
|
||||||
|
}
|
||||||
|
return D2pUploader.getUploader(type)
|
||||||
|
},
|
||||||
|
emit () {
|
||||||
|
const list = []
|
||||||
|
for (const item of this.list) {
|
||||||
|
if (item.status != null && item.status !== 'done') {
|
||||||
|
// 全部上传完再发通知
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof (item) === 'string') {
|
||||||
|
list.push(item)
|
||||||
|
} else {
|
||||||
|
list.push(item.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ret = list
|
||||||
|
if (this.limit === 1) {
|
||||||
|
ret = ((list && list.length > 0) ? list[0] : undefined)
|
||||||
|
}
|
||||||
|
this.emitValue = ret
|
||||||
|
this.$emit('input', ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" >
|
||||||
|
.d2p-cropper-uploader{
|
||||||
|
.el-image-viewer__close{color:#fff}
|
||||||
|
&.is-disabled {
|
||||||
|
.image-list{
|
||||||
|
.image-item{
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i{cursor: not-allowed}
|
||||||
|
}
|
||||||
|
.image-list{
|
||||||
|
display: flex;
|
||||||
|
justify-content: left;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
.image-item{
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #fbfdff;
|
||||||
|
border: 1px solid #c0ccda;
|
||||||
|
border-radius: 6px;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
&.image-plus{
|
||||||
|
border: 1px dashed #c0ccda;
|
||||||
|
}
|
||||||
|
.cropper-uploader-icon {
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
}
|
||||||
|
.image{
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete{
|
||||||
|
border-radius: 6px;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
cursor: default;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
background-color: rgba(0,0,0,.9);
|
||||||
|
-webkit-transition: opacity .3s;
|
||||||
|
transition: opacity .3s;
|
||||||
|
&:hover{
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
i{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.status-uploading{
|
||||||
|
border-radius: 6px;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
cursor: default;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
opacity: 1;
|
||||||
|
font-size: 20px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
-webkit-transition: opacity .3s;
|
||||||
|
transition: opacity .3s;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
.el-progress{
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
.el-progress__text {
|
||||||
|
color:#fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.status-done{
|
||||||
|
position: absolute;
|
||||||
|
right: -15px;
|
||||||
|
top: -6px;
|
||||||
|
width: 40px;
|
||||||
|
height: 24px;
|
||||||
|
background: #13ce66;
|
||||||
|
text-align: center;
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
-webkit-box-shadow: 0 0 1pc 1px rgba(0,0,0,.2);
|
||||||
|
box-shadow: 0 0 1pc 1px rgba(0,0,0,.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
i{
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 11px;
|
||||||
|
color: #fff;
|
||||||
|
-webkit-transform: rotate(-45deg);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,420 @@
|
||||||
|
<template>
|
||||||
|
<el-dialog class="cropper-uploader quying-dialog" :title="title" :visible.sync="dialogVisible" append-to-body
|
||||||
|
:before-close="handleClose" :close-on-click-modal="true" ref="editAvatar" :width="_dialogWidth" >
|
||||||
|
<div class="cropper-uploader-wrap" >
|
||||||
|
<input type="file" v-show="false" ref="fileinput" :accept="accept" @change="handleChange">
|
||||||
|
<!-- step1 -->
|
||||||
|
<div class="cropper-uploader__choose cropper-uploader_left" v-show="!isLoaded">
|
||||||
|
<el-button round @click="handleClick">+选择图片</el-button>
|
||||||
|
<p>{{_uploadTip}}</p>
|
||||||
|
</div>
|
||||||
|
<!-- step2 -->
|
||||||
|
<div class="cropper-uploader__edit cropper-uploader_left" v-show="isLoaded">
|
||||||
|
<div class="cropper-uploader__edit-area" >
|
||||||
|
<vue-cropper
|
||||||
|
ref="cropper"
|
||||||
|
:src="imgSrc"
|
||||||
|
preview=".preview"
|
||||||
|
:style="{height:_cropperHeight}"
|
||||||
|
v-bind="_cropper"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="tool-bar">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button round size="mini" icon="el-icon-edit" @click="handleClick">重新选择</el-button>
|
||||||
|
<el-button round size="mini" type="" @click="flipX">左右翻转</el-button>
|
||||||
|
<el-button round size="mini" type="" @click="flipY">上下翻转</el-button>
|
||||||
|
<el-button round size="mini" type="" icon="el-icon-zoom-in" @click="zoom(0.1)"></el-button>
|
||||||
|
<el-button round size="mini" type="" icon="el-icon-zoom-out" @click="zoom(-0.1)"></el-button>
|
||||||
|
<el-button round size="mini" type="" icon="el-icon-refresh-right" @click="rotate(90)">旋转</el-button>
|
||||||
|
<el-button round size="mini" type="" icon="el-icon-refresh" @click="reset">重置</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cropper-uploader__preview">
|
||||||
|
<span class="cropper-uploader__preview-title">预览</span>
|
||||||
|
<div class="cropper-uploader__preview-120 preview"></div>
|
||||||
|
<div class="cropper-uploader__preview-65 preview" :class="{'round': _cropper.aspectRatio===1}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="handleClose" size="mini">取 消</el-button>
|
||||||
|
<el-button type="primary" size="mini" @click="doCropper()">确 定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import VueCropper from './vue-cropper'
|
||||||
|
import log from 'd2p-extends/src/utils/util.log'
|
||||||
|
// 图片裁剪对话框,封装cropperjs
|
||||||
|
export default {
|
||||||
|
name: 'd2p-cropper',
|
||||||
|
components: {
|
||||||
|
VueCropper
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
// 对话框标题
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '图片裁剪'
|
||||||
|
},
|
||||||
|
// cropper的高度,默认为浏览器可视窗口高度的40%,最小270
|
||||||
|
cropperHeight: {
|
||||||
|
type: [String, Number]
|
||||||
|
},
|
||||||
|
// 对话框宽度,默认50%
|
||||||
|
dialogWidth: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '50%'
|
||||||
|
},
|
||||||
|
// 图片大小限制,单位MB,0为不限制
|
||||||
|
maxSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 5
|
||||||
|
},
|
||||||
|
// 上传提示
|
||||||
|
uploadTip: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
// cropperjs的参数,详见:https://github.com/fengyuanchen/cropperjs
|
||||||
|
cropper: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
// 可接收的文件后缀
|
||||||
|
accept: {
|
||||||
|
type: String,
|
||||||
|
default: '.jpg, .jpeg, .png, .gif, .webp'
|
||||||
|
},
|
||||||
|
// 输出类型,blob,dataUrl,all
|
||||||
|
output: {
|
||||||
|
type: String,
|
||||||
|
default: 'blob'// blob
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
dialogVisible: false,
|
||||||
|
isLoaded: false,
|
||||||
|
imgSrc: '',
|
||||||
|
data: null,
|
||||||
|
file: undefined,
|
||||||
|
scale: {
|
||||||
|
x: 1,
|
||||||
|
y: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
_uploadTip () {
|
||||||
|
if (this.uploadTip != null && this.uploadTip !== '') {
|
||||||
|
return this.uploadTip
|
||||||
|
}
|
||||||
|
if (this.maxSize > 0) {
|
||||||
|
return `只支持${this.accept.replace(/,/g, '、')},大小不超过${this.maxSize}M`
|
||||||
|
} else {
|
||||||
|
return `只支持${this.accept},大小无限制`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_cropper () {
|
||||||
|
const def = {
|
||||||
|
aspectRatio: 1,
|
||||||
|
ready: this.ready
|
||||||
|
}
|
||||||
|
if (this.cropper == null) {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
const assign = Object.assign(def, this.cropper)
|
||||||
|
log.debug('cropper options:', assign)
|
||||||
|
return assign
|
||||||
|
},
|
||||||
|
_cropperHeight () {
|
||||||
|
let height = this.cropperHeight
|
||||||
|
if (height == null) {
|
||||||
|
height = document.documentElement.clientHeight * 0.55
|
||||||
|
if (height < 270) {
|
||||||
|
height = 270
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof (height) === 'number') {
|
||||||
|
height += 'px'
|
||||||
|
}
|
||||||
|
return height
|
||||||
|
},
|
||||||
|
_dialogWidth () {
|
||||||
|
let width = this.dialogWidth
|
||||||
|
if (width == null) {
|
||||||
|
width = '50%'
|
||||||
|
}
|
||||||
|
if (typeof (width) === 'number') {
|
||||||
|
width += 'px'
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
open (url) {
|
||||||
|
this.dialogVisible = true
|
||||||
|
if (url != null && url !== '') {
|
||||||
|
this.imgSrc = url
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close () {
|
||||||
|
this.dialogVisible = false
|
||||||
|
},
|
||||||
|
clear () {
|
||||||
|
this.isLoaded = false
|
||||||
|
if (this.$refs.fileinput != null) {
|
||||||
|
this.$refs.fileinput.value = null
|
||||||
|
}
|
||||||
|
if (this.$refs.cropper != null) {
|
||||||
|
this.$refs.cropper.clear()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 获取vue-cropper组件对象
|
||||||
|
getCropper () {
|
||||||
|
return this.$refs.cropper
|
||||||
|
},
|
||||||
|
ready (event) {
|
||||||
|
log.debug('cropper ready:', event)
|
||||||
|
// this.zoom(-0.3)
|
||||||
|
},
|
||||||
|
preventDefault (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
// 点击按钮打开文件资源窗口
|
||||||
|
handleClick (e) {
|
||||||
|
this.$refs.fileinput.click()
|
||||||
|
},
|
||||||
|
// 检测选择的文件是否合适
|
||||||
|
checkFile (file) {
|
||||||
|
// 仅限图片
|
||||||
|
if (file.type.indexOf('image') === -1) {
|
||||||
|
this.$message({
|
||||||
|
message: '请选择合适的文件类型',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 超出大小
|
||||||
|
if (this.maxSize > 0 && file.size / 1024 / 1024 > this.maxSize) {
|
||||||
|
this.$message({
|
||||||
|
message: '图片大小超出最大限制(' + this.maxSize + 'MB),请重新选择',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
// 触发input框的change事件选择图片
|
||||||
|
handleChange (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const files = e.target.files || e.dataTransfer.files
|
||||||
|
this.isLoaded = true
|
||||||
|
const file = files[0]
|
||||||
|
if (this.checkFile(file)) {
|
||||||
|
this.file = file
|
||||||
|
this.setImage(e)
|
||||||
|
// setTimeout(() => {
|
||||||
|
// this.zoom(-0.3)
|
||||||
|
// }, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 点击关闭弹窗
|
||||||
|
handleClose () {
|
||||||
|
this.dialogVisible = false
|
||||||
|
this.$emit('cancel')
|
||||||
|
},
|
||||||
|
doCropper () {
|
||||||
|
if (!this.isLoaded) {
|
||||||
|
this.$message('请先选择图片')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.dialogVisible = false
|
||||||
|
this.doOutput(this.file)
|
||||||
|
},
|
||||||
|
doOutput (file) {
|
||||||
|
log.debug('output this:', this)
|
||||||
|
const ret = { file }
|
||||||
|
if (this.output === 'all') {
|
||||||
|
this.getCropImageBlob((blob) => {
|
||||||
|
const dataUrl = this.cropImageDataUrl()
|
||||||
|
ret.blob = blob
|
||||||
|
ret.dataUrl = dataUrl
|
||||||
|
this.$emit('done', ret)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.output === 'blob') {
|
||||||
|
this.getCropImageBlob((blob) => {
|
||||||
|
ret.blob = blob
|
||||||
|
this.$emit('done', ret)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.output === 'dataUrl') {
|
||||||
|
ret.dataUrl = this.cropImageDataUrl()
|
||||||
|
this.$emit('done', ret)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emit (result) {
|
||||||
|
this.$emit('done', result)
|
||||||
|
},
|
||||||
|
cropImageDataUrl () {
|
||||||
|
// get image data for post processing, e.g. upload or setting image src
|
||||||
|
return this.$refs.cropper.getCroppedCanvas().toDataURL()
|
||||||
|
},
|
||||||
|
getCropImageBlob (callback, type, quality) {
|
||||||
|
this.$refs.cropper.getCroppedCanvas().toBlob(callback, type, quality)
|
||||||
|
},
|
||||||
|
flipX () {
|
||||||
|
this.$refs.cropper.scaleX(this.scale.x *= -1)
|
||||||
|
},
|
||||||
|
flipY () {
|
||||||
|
this.$refs.cropper.scaleY(this.scale.y *= -1)
|
||||||
|
},
|
||||||
|
getCropBoxData () {
|
||||||
|
this.data = JSON.stringify(this.$refs.cropper.getCropBoxData(), null, 4)
|
||||||
|
},
|
||||||
|
getData () {
|
||||||
|
this.data = JSON.stringify(this.$refs.cropper.getData(), null, 4)
|
||||||
|
},
|
||||||
|
move (offsetX, offsetY) {
|
||||||
|
this.$refs.cropper.move(offsetX, offsetY)
|
||||||
|
},
|
||||||
|
reset () {
|
||||||
|
this.$refs.cropper.reset()
|
||||||
|
},
|
||||||
|
rotate (deg) {
|
||||||
|
this.$refs.cropper.rotate(deg)
|
||||||
|
},
|
||||||
|
setCropBoxData () {
|
||||||
|
if (!this.data) return
|
||||||
|
this.$refs.cropper.setCropBoxData(JSON.parse(this.data))
|
||||||
|
},
|
||||||
|
setData () {
|
||||||
|
if (!this.data) return
|
||||||
|
this.$refs.cropper.setData(JSON.parse(this.data))
|
||||||
|
},
|
||||||
|
setImage (e) {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (file.type.indexOf('image/') === -1) {
|
||||||
|
this.$message('Please select an image file')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof FileReader === 'function') {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (event) => {
|
||||||
|
this.imgSrc = event.target.result
|
||||||
|
// rebuild cropperjs with the updated source
|
||||||
|
this.$refs.cropper.replace(event.target.result)
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
} else {
|
||||||
|
this.$message('Sorry, FileReader API not supported')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showFileChooser () {
|
||||||
|
this.$refs.input.click()
|
||||||
|
},
|
||||||
|
zoom (percent) {
|
||||||
|
this.$refs.cropper.relativeZoom(percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$area-height: 280px;
|
||||||
|
.cropper-uploader {
|
||||||
|
&-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_left {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999999;
|
||||||
|
position: relative;
|
||||||
|
background: #ecf2f6;
|
||||||
|
flex-grow:5;
|
||||||
|
margin:10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column
|
||||||
|
}
|
||||||
|
|
||||||
|
&__choose {
|
||||||
|
p {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__edit {
|
||||||
|
&-area {
|
||||||
|
width:100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&-img {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tool-bar{
|
||||||
|
margin:10px;position: absolute;bottom: -50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$top: 30px;
|
||||||
|
|
||||||
|
&__preview {
|
||||||
|
background: #ecf2f6;
|
||||||
|
text-align: center;
|
||||||
|
width: 200px;
|
||||||
|
padding-top: $top;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
margin:10px;
|
||||||
|
padding:10px;
|
||||||
|
&-title {
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
.preview{
|
||||||
|
overflow: hidden;
|
||||||
|
margin:10px;
|
||||||
|
border: 1px #cacaca solid;
|
||||||
|
}
|
||||||
|
.round{
|
||||||
|
border-radius: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
background: #fff;
|
||||||
|
margin-top: 5px;
|
||||||
|
border-radius: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-120 {
|
||||||
|
height: 120px;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-65 {
|
||||||
|
height: 65px;
|
||||||
|
width: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-40 {
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,367 @@
|
||||||
|
import Cropper from 'cropperjs'
|
||||||
|
|
||||||
|
const previewPropType = typeof window === 'undefined'
|
||||||
|
? [String, Array]
|
||||||
|
: [String, Array, Element, NodeList]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
render (h) {
|
||||||
|
return h('div', { style: this.containerStyle }, [
|
||||||
|
h('img', {
|
||||||
|
ref: 'img',
|
||||||
|
attrs: {
|
||||||
|
src: this.src,
|
||||||
|
alt: this.alt || 'image',
|
||||||
|
style: 'max-width: 100%'
|
||||||
|
},
|
||||||
|
style: this.imgStyle
|
||||||
|
})
|
||||||
|
])
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
// Library props
|
||||||
|
containerStyle: Object,
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
alt: String,
|
||||||
|
imgStyle: Object,
|
||||||
|
|
||||||
|
// CropperJS props
|
||||||
|
viewMode: Number,
|
||||||
|
dragMode: String,
|
||||||
|
initialAspectRatio: Number,
|
||||||
|
aspectRatio: Number,
|
||||||
|
data: Object,
|
||||||
|
preview: previewPropType,
|
||||||
|
responsive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
restore: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
checkCrossOrigin: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
checkOrientation: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
guides: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
autoCrop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
autoCropArea: Number,
|
||||||
|
movable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
rotatable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
scalable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
zoomable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
zoomOnTouch: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
zoomOnWheel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
wheelZoomRatio: Number,
|
||||||
|
cropBoxMovable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
cropBoxResizable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
toggleDragModeOnDblclick: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
|
||||||
|
// Size limitation
|
||||||
|
minCanvasWidth: Number,
|
||||||
|
minCanvasHeight: Number,
|
||||||
|
minCropBoxWidth: Number,
|
||||||
|
minCropBoxHeight: Number,
|
||||||
|
minContainerWidth: Number,
|
||||||
|
minContainerHeight: Number,
|
||||||
|
|
||||||
|
// callbacks
|
||||||
|
ready: Function,
|
||||||
|
cropstart: Function,
|
||||||
|
cropmove: Function,
|
||||||
|
cropend: Function,
|
||||||
|
crop: Function,
|
||||||
|
zoom: Function
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
const { containerStyle, src, alt, imgStyle, ...data } = this.$options.props
|
||||||
|
const props = {}
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
if (this[key] !== undefined) {
|
||||||
|
props[key] = this[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cropper = new Cropper(this.$refs.img, props)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// Reset the image and crop box to their initial states
|
||||||
|
reset () {
|
||||||
|
return this.cropper.reset()
|
||||||
|
},
|
||||||
|
|
||||||
|
// Clear the crop box
|
||||||
|
clear () {
|
||||||
|
return this.cropper.clear()
|
||||||
|
},
|
||||||
|
|
||||||
|
// Init crop box manually
|
||||||
|
initCrop () {
|
||||||
|
return this.cropper.crop()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the image's src and rebuild the cropper
|
||||||
|
* @param {string} url - The new URL.
|
||||||
|
* @param {boolean} [onlyColorChanged] - Indicate if the new image only changed color.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
replace (url, onlyColorChanged = false) {
|
||||||
|
return this.cropper.replace(url, onlyColorChanged)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enable (unfreeze) the cropper
|
||||||
|
enable () {
|
||||||
|
return this.cropper.enable()
|
||||||
|
},
|
||||||
|
|
||||||
|
// Disable (freeze) the cropper
|
||||||
|
disable () {
|
||||||
|
return this.cropper.disable()
|
||||||
|
},
|
||||||
|
|
||||||
|
// Destroy the cropper and remove the instance from the image
|
||||||
|
destroy () {
|
||||||
|
return this.cropper.destroy()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the canvas with relative offsets
|
||||||
|
* @param {number} offsetX - The relative offset distance on the x-axis.
|
||||||
|
* @param {number} offsetY - The relative offset distance on the y-axis.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
move (offsetX, offsetY) {
|
||||||
|
return this.cropper.move(offsetX, offsetY)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the canvas to an absolute point
|
||||||
|
* @param {number} x - The x-axis coordinate.
|
||||||
|
* @param {number} [y=x] - The y-axis coordinate.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
moveTo (x, y = x) {
|
||||||
|
return this.cropper.moveTo(x, y)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zoom the canvas with a relative ratio
|
||||||
|
* @param {number} ratio - The target ratio.
|
||||||
|
* @param {Event} _originalEvent - The original event if any.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
relativeZoom (ratio, _originalEvent) {
|
||||||
|
return this.cropper.zoom(ratio, _originalEvent)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zoom the canvas to an absolute ratio
|
||||||
|
* @param {number} ratio - The target ratio.
|
||||||
|
* @param {Event} _originalEvent - The original event if any.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
zoomTo (ratio, _originalEvent) {
|
||||||
|
return this.cropper.zoomTo(ratio, _originalEvent)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate the canvas with a relative degree
|
||||||
|
* @param {number} degree - The rotate degree.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
rotate (degree) {
|
||||||
|
return this.cropper.rotate(degree)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate the canvas to an absolute degree
|
||||||
|
* @param {number} degree - The rotate degree.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
rotateTo (degree) {
|
||||||
|
return this.cropper.rotateTo(degree)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale the image on the x-axis.
|
||||||
|
* @param {number} scaleX - The scale ratio on the x-axis.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
scaleX (scaleX) {
|
||||||
|
return this.cropper.scaleX(scaleX)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale the image on the y-axis.
|
||||||
|
* @param {number} scaleY - The scale ratio on the y-axis.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
scaleY (scaleY) {
|
||||||
|
return this.cropper.scaleY(scaleY)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale the image
|
||||||
|
* @param {number} scaleX - The scale ratio on the x-axis.
|
||||||
|
* @param {number} [scaleY=scaleX] - The scale ratio on the y-axis.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
scale (scaleX, scaleY = scaleX) {
|
||||||
|
return this.cropper.scale(scaleX, scaleY)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cropped area position and size data (base on the original image)
|
||||||
|
* @param {boolean} [rounded=false] - Indicate if round the data values or not.
|
||||||
|
* @returns {Object} The result cropped data.
|
||||||
|
*/
|
||||||
|
getData (rounded = false) {
|
||||||
|
return this.cropper.getData(rounded)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the cropped area position and size with new data
|
||||||
|
* @param {Object} data - The new data.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
setData (data) {
|
||||||
|
return this.cropper.setData(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the container size data.
|
||||||
|
* @returns {Object} The result container data.
|
||||||
|
*/
|
||||||
|
getContainerData () {
|
||||||
|
return this.cropper.getContainerData()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the image position and size data.
|
||||||
|
* @returns {Object} The result image data.
|
||||||
|
*/
|
||||||
|
getImageData () {
|
||||||
|
return this.cropper.getImageData()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the canvas position and size data.
|
||||||
|
* @returns {Object} The result canvas data.
|
||||||
|
*/
|
||||||
|
getCanvasData () {
|
||||||
|
return this.cropper.getCanvasData()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the canvas position and size with new data.
|
||||||
|
* @param {Object} data - The new canvas data.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
setCanvasData (data) {
|
||||||
|
return this.cropper.setCanvasData(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the crop box position and size data.
|
||||||
|
* @returns {Object} The result crop box data.
|
||||||
|
*/
|
||||||
|
getCropBoxData () {
|
||||||
|
return this.cropper.getCropBoxData()
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the crop box position and size with new data.
|
||||||
|
* @param {Object} data - The new crop box data.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
setCropBoxData (data) {
|
||||||
|
return this.cropper.setCropBoxData(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a canvas drawn the cropped image.
|
||||||
|
* @param {Object} [options={}] - The config options.
|
||||||
|
* @returns {HTMLCanvasElement} - The result canvas.
|
||||||
|
*/
|
||||||
|
getCroppedCanvas (options = {}) {
|
||||||
|
return this.cropper.getCroppedCanvas(options)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the aspect ratio of the crop box.
|
||||||
|
* @param {number} aspectRatio - The new aspect ratio.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
setAspectRatio (aspectRatio) {
|
||||||
|
return this.cropper.setAspectRatio(aspectRatio)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the drag mode.
|
||||||
|
* @param {string} mode - The new drag mode.
|
||||||
|
* @returns {Object} this
|
||||||
|
*/
|
||||||
|
setDragMode (mode) {
|
||||||
|
return this.cropper.setDragMode(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,666 @@
|
||||||
|
<template>
|
||||||
|
<div class="d2p-file-uploader" :class="{'is-disabled':disabled}" >
|
||||||
|
<el-upload :class="uploadClass"
|
||||||
|
:file-list="fileList"
|
||||||
|
:disabled="disabled"
|
||||||
|
:http-request="httpRequest"
|
||||||
|
:on-exceed="onExceed"
|
||||||
|
:on-remove="handleUploadFileRemove"
|
||||||
|
:on-success="handleUploadFileSuccess"
|
||||||
|
:on-error="handleUploadFileError"
|
||||||
|
:on-progress="handleUploadProgress"
|
||||||
|
@blur="handleBlur"
|
||||||
|
ref="fileUploader"
|
||||||
|
v-bind="_elProps"
|
||||||
|
>
|
||||||
|
<el-button :disabled="disabled" :size="btnSize" type="primary" v-if="_elProps.listType === 'text' || this._elProps.listType === 'picture'">{{btnName}}</el-button>
|
||||||
|
<div class="avatar-item-wrapper" v-else-if="this._elProps.listType === 'picture-card'">
|
||||||
|
<i class="el-icon-plus avatar-uploader-icon" />
|
||||||
|
</div>
|
||||||
|
<template v-else-if="_elProps.listType === 'avatar'">
|
||||||
|
<div class="avatar-item-wrapper">
|
||||||
|
<div class="status-uploading" v-if="avatarLoading!=null">
|
||||||
|
<el-progress type="circle" :percentage="avatarLoading" :width="70"/>
|
||||||
|
</div>
|
||||||
|
<div v-if="avatarUrl!=null" class="avatar">
|
||||||
|
<el-image :src="avatarUrl">
|
||||||
|
<div slot="placeholder" class="image-slot">
|
||||||
|
<img src="./loading-spin.svg">
|
||||||
|
</div>
|
||||||
|
</el-image>
|
||||||
|
<div class="preview" @click.stop="" >
|
||||||
|
<i class="el-icon-zoom-in" @click="previewAvatar"></i>
|
||||||
|
<i class="el-icon-delete" v-if="!disabled" @click="removeAvatar"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<i class="el-icon-plus avatar-uploader-icon" v-else/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
<el-dialog :visible.sync="dialogVisible" v-bind="preview" append-to-body >
|
||||||
|
<div style="text-align: center">
|
||||||
|
<img style="max-width: 100%;" :src="dialogImageUrl" alt="">
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SparkMD5 from 'spark-md5'
|
||||||
|
import D2pUploader from 'd2p-extends/src/uploader'
|
||||||
|
import lodash from 'lodash'
|
||||||
|
import { d2CrudPlus } from 'd2-crud-plus'
|
||||||
|
import log from 'd2p-extends/src/utils/util.log'
|
||||||
|
import util from '@/libs/util'
|
||||||
|
// 文件上传组件,依赖D2pUploader
|
||||||
|
export default {
|
||||||
|
name: 'd2p-file-uploader',
|
||||||
|
mixins: [d2CrudPlus.inputBase],
|
||||||
|
props: {
|
||||||
|
// 选择文件按钮的大小
|
||||||
|
btnSize: { default: 'small' },
|
||||||
|
// 选择文件按钮的名称
|
||||||
|
btnName: { default: '选择文件' },
|
||||||
|
// 可选哪些类型的文件
|
||||||
|
accept: {},
|
||||||
|
// 上传后端类型,[cos,qiniu,alioss,form]
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: undefined // 上传类型:form cos qiniu alioss
|
||||||
|
},
|
||||||
|
// 值:url<br/>
|
||||||
|
// 或 [url1,url2]<br/>
|
||||||
|
// 或 {url:'url',md5:'',size:number}<br/>
|
||||||
|
// 或 [{url:'url',md5:'',size:number}]<br/>
|
||||||
|
// <br/>
|
||||||
|
// limit=1 时 input事件返回 {url:'url',md5:'',size:number}<br/>
|
||||||
|
// limit>1 时 input事件返回 数组<br/>
|
||||||
|
value: {
|
||||||
|
type: [String, Array, Object]
|
||||||
|
},
|
||||||
|
// 样式后缀 追加到url的后面,进行图片处理,需要到对象存储平台配置样式
|
||||||
|
suffix: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
// 返回类型: url=仅返回链接, object=包含md5和size , key=仅返回文件key
|
||||||
|
returnType: {
|
||||||
|
type: String,
|
||||||
|
default: 'url'
|
||||||
|
},
|
||||||
|
// 自定义参数
|
||||||
|
custom: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
// 内部封装[el-upload](https://element.eleme.cn/#/zh-CN/component/upload)组件的属性参数<br/>
|
||||||
|
// 注意,form方式上传的action、name、headers等参数不在此设置
|
||||||
|
elProps: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
// 预览对话框的配置
|
||||||
|
preview: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
// 文件大小限制 <br/>
|
||||||
|
// 如果传入{limit,tip(fileSize,limit){vm.$message('可以自定义提示')}}
|
||||||
|
sizeLimit: {
|
||||||
|
type: Number, Object
|
||||||
|
},
|
||||||
|
// 构建下载url方法
|
||||||
|
buildUrl: {
|
||||||
|
type: Function,
|
||||||
|
default: function (value, item) { return (typeof value === 'object') ? item.url : value }
|
||||||
|
},
|
||||||
|
// 上传组件参数,会临时覆盖全局上传配置参数[d2p-uploader](/guide/extends/uploader.html)
|
||||||
|
uploader: {
|
||||||
|
type: Object,
|
||||||
|
default () { return {} }
|
||||||
|
},
|
||||||
|
// 与el-upload一致
|
||||||
|
beforeUpload: {
|
||||||
|
type: Function
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
fileList: [],
|
||||||
|
context: {},
|
||||||
|
dialogImageUrl: '',
|
||||||
|
dialogVisible: false,
|
||||||
|
avatarLoading: undefined,
|
||||||
|
baseURL: util.baseURL()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.emitValue = this.value
|
||||||
|
this.initValue(this.value)
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value (value) {
|
||||||
|
if (this.dispatch) {
|
||||||
|
this.dispatch('ElFormItem', 'el.form.blur')
|
||||||
|
}
|
||||||
|
this.$emit('change', value)
|
||||||
|
if (this.emitValue === value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.emitValue = value
|
||||||
|
this.initValue(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
_elProps () {
|
||||||
|
const defaultElProps = this.getDefaultElProps()
|
||||||
|
Object.assign(defaultElProps, this.elProps)
|
||||||
|
return defaultElProps
|
||||||
|
},
|
||||||
|
avatarUrl () {
|
||||||
|
if (this.fileList.length > 0) {
|
||||||
|
const file = this.fileList[0]
|
||||||
|
log.debug('file,', file, file.status)
|
||||||
|
if (file.response != null && file.response.url != null) {
|
||||||
|
return file.response.url
|
||||||
|
} else if (file.url != null) {
|
||||||
|
return file.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
uploadClass () {
|
||||||
|
if (this._elProps.listType === 'avatar') {
|
||||||
|
return 'avatar-uploader'
|
||||||
|
} else if (this._elProps.listType === 'picture-card') {
|
||||||
|
if (this.fileList && this._elProps.limit !== 0 && this.fileList.length >= this._elProps.limit) {
|
||||||
|
return 'image-uploader hide-plus'
|
||||||
|
}
|
||||||
|
return 'image-uploader'
|
||||||
|
}
|
||||||
|
return 'file-uploader'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleBlur () {
|
||||||
|
console.log('blur')
|
||||||
|
},
|
||||||
|
getDefaultElProps () {
|
||||||
|
return {
|
||||||
|
limit: 0,
|
||||||
|
listType: 'text',
|
||||||
|
showFileList: true,
|
||||||
|
action: '',
|
||||||
|
onPreview: (file) => {
|
||||||
|
if (this._elProps.listType === 'picture-card' || this._elProps.listType === 'picture' || this._elProps.listType === 'avatar') {
|
||||||
|
this.dialogImageUrl = file.url
|
||||||
|
this.dialogVisible = true
|
||||||
|
} else {
|
||||||
|
window.open(file.url)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUpload: async (file) => {
|
||||||
|
if (this.beforeUpload) {
|
||||||
|
const ret = await this.beforeUpload(file, { vm: this })
|
||||||
|
if (ret === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.sizeLimit == null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
let limit = this.sizeLimit
|
||||||
|
let showMessage = null
|
||||||
|
if (typeof limit === 'number') {
|
||||||
|
limit = this.sizeLimit
|
||||||
|
showMessage = (fileSize, limit) => {
|
||||||
|
if (this.$message) {
|
||||||
|
const limitTip = this.computeFileSize(limit)
|
||||||
|
const fileSizeTip = this.computeFileSize(file.size)
|
||||||
|
this.$message({ message: '文件大小不能超过' + limitTip + ',当前文件大小:' + fileSizeTip, type: 'warning' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
limit = this.sizeLimit.limit
|
||||||
|
showMessage = this.sizeLimit.tip
|
||||||
|
}
|
||||||
|
if (file.size > limit) {
|
||||||
|
log.debug('文件大小超过限制:', file.size)
|
||||||
|
showMessage(file.size, limit)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setValue (value) {
|
||||||
|
this.initValue(value)
|
||||||
|
// this.$emit('change', this.fileList)
|
||||||
|
},
|
||||||
|
getUploader () {
|
||||||
|
let type = this.type
|
||||||
|
if (this.uploader != null && this.uploader.type != null) {
|
||||||
|
type = this.uploader.type
|
||||||
|
}
|
||||||
|
return D2pUploader.getUploader(type)
|
||||||
|
},
|
||||||
|
initValue (value) {
|
||||||
|
let fileList = []
|
||||||
|
if (value == null) {
|
||||||
|
|
||||||
|
} else if (typeof (value) === 'string') {
|
||||||
|
if (value !== '') {
|
||||||
|
const fileName = value.substring(value.lastIndexOf('/') + 1)
|
||||||
|
fileList = [{ value: value, name: fileName }]
|
||||||
|
}
|
||||||
|
} else if (value instanceof Array) {
|
||||||
|
if (value.length > 0 && typeof (value[0]) === 'string') {
|
||||||
|
const tmp = []
|
||||||
|
value.forEach(item => {
|
||||||
|
const fileName = item.substring(item.lastIndexOf('/') + 1)
|
||||||
|
tmp.push({ value: item, name: fileName })
|
||||||
|
})
|
||||||
|
fileList = tmp
|
||||||
|
} else {
|
||||||
|
fileList = value
|
||||||
|
}
|
||||||
|
} else if (value instanceof Object) {
|
||||||
|
fileList = [value]
|
||||||
|
}
|
||||||
|
for (const item of fileList) {
|
||||||
|
if (item.value == null) {
|
||||||
|
item.value = item.url
|
||||||
|
}
|
||||||
|
item.url = this.buildUrl(item.value, item)
|
||||||
|
}
|
||||||
|
this.resetFileList(fileList)
|
||||||
|
},
|
||||||
|
computeFileSize (fileSize) {
|
||||||
|
let sizeTip = fileSize
|
||||||
|
if (fileSize > (1024 * 1024 * 1024)) {
|
||||||
|
sizeTip = (fileSize / (1024 * 1024 * 1024)).toFixed(2) + 'G'
|
||||||
|
} else if (fileSize > (1024 * 1024)) {
|
||||||
|
sizeTip = (fileSize / (1024 * 1024)).toFixed(2) + 'M'
|
||||||
|
} else {
|
||||||
|
sizeTip = Math.round(fileSize / (1024)) + 'K'
|
||||||
|
}
|
||||||
|
return sizeTip
|
||||||
|
},
|
||||||
|
resetFileList (fileList) {
|
||||||
|
this.$set(this, 'fileList', fileList)
|
||||||
|
},
|
||||||
|
handleUploadProgress (event, file, fileList) {
|
||||||
|
if (this._elProps.listType === 'avatar') {
|
||||||
|
log.debug('progress', event, file)
|
||||||
|
this.avatarLoading = Math.floor(event.percent)
|
||||||
|
if (event.percent === 100) {
|
||||||
|
this.avatarLoading = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$emit('progress', event, { file, fileList, vm: this })
|
||||||
|
},
|
||||||
|
handleUploadFileSuccess (res, file, fileList) {
|
||||||
|
res.size = res.size != null ? res.size : file.size
|
||||||
|
res.name = res.name != null ? res.name : file.name
|
||||||
|
res.value = this.getReturnValue(res)
|
||||||
|
const value = this.returnType === 'object' ? res.url : res.value
|
||||||
|
const url = this.buildUrl(value, res)
|
||||||
|
file.url = res.url = url
|
||||||
|
this.resetFileList(fileList)
|
||||||
|
this.$emit('success', res, file)
|
||||||
|
const list = []
|
||||||
|
for (const item of fileList) {
|
||||||
|
// if (item.status === 'uploading') {
|
||||||
|
// log.debug('当前文件上传完成,等待剩下的文件全部上传成功后再更新value')
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
item.url = this.baseURL + url
|
||||||
|
if (item.response != null && item.response.url != null) {
|
||||||
|
list.push({ ...item.response })
|
||||||
|
} else {
|
||||||
|
list.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug('handleUploadFileSuccess list', list, res)
|
||||||
|
this.emit(res, list)
|
||||||
|
},
|
||||||
|
isHasUploadingItem () {
|
||||||
|
const fileList = this.$refs.fileUploader.uploadFiles
|
||||||
|
if (fileList && fileList.length > 0) {
|
||||||
|
for (const item of fileList) {
|
||||||
|
if (item.status === 'uploading') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
handleUploadFileRemove (file, fileList) {
|
||||||
|
this.fileList = fileList
|
||||||
|
console.log('remove', fileList)
|
||||||
|
this.emitList(fileList)
|
||||||
|
this.$emit('remove', file, fileList)
|
||||||
|
},
|
||||||
|
handleUploadFileError (err, file, fileList) {
|
||||||
|
console.error('文件上传失败', err, file, fileList)
|
||||||
|
this.$message({ type: 'error', message: '文件上传失败' })
|
||||||
|
this.$emit('error', err, file, fileList)
|
||||||
|
},
|
||||||
|
previewAvatar ($event) {
|
||||||
|
$event.stopPropagation()
|
||||||
|
this._elProps.onPreview(this.fileList[0])
|
||||||
|
},
|
||||||
|
removeAvatar ($event) {
|
||||||
|
$event.stopPropagation()
|
||||||
|
this.resetFileList([])
|
||||||
|
this.emit() // 返回undefined,相当于清空已有的值
|
||||||
|
},
|
||||||
|
emit (res, list) {
|
||||||
|
if (this._elProps.limit === 1) {
|
||||||
|
const value = res ? res.value : undefined
|
||||||
|
this.emitValue = value
|
||||||
|
this.$emit('input', value)
|
||||||
|
} else {
|
||||||
|
this.emitList(list)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emitList (list) {
|
||||||
|
if (list) {
|
||||||
|
const tmp = []
|
||||||
|
list.forEach(item => {
|
||||||
|
tmp.push(this.getReturnValue(item))
|
||||||
|
})
|
||||||
|
list = tmp
|
||||||
|
}
|
||||||
|
this.emitValue = list
|
||||||
|
this.$emit('input', list)
|
||||||
|
},
|
||||||
|
getReturnValue (item) {
|
||||||
|
const value = item[this.returnType]
|
||||||
|
if (value != null) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
},
|
||||||
|
httpRequest (option) {
|
||||||
|
Promise.all([
|
||||||
|
this.doUpload(option),
|
||||||
|
this.computeMd5(option.file)
|
||||||
|
]).then((ret) => {
|
||||||
|
// 得到组合结果, size,md5
|
||||||
|
const result = ret[0]
|
||||||
|
result.md5 = ret[1]
|
||||||
|
option.onSuccess(result)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
doUpload (option) {
|
||||||
|
let config = this.uploader
|
||||||
|
if (config == null) {
|
||||||
|
config = {}
|
||||||
|
}
|
||||||
|
if (!lodash.isEmpty(this._elProps.action)) {
|
||||||
|
config.action = this._elProps.action
|
||||||
|
}
|
||||||
|
if (!lodash.isEmpty(this._elProps.name)) {
|
||||||
|
config.name = this._elProps.name
|
||||||
|
}
|
||||||
|
if (!lodash.isEmpty(this._elProps.data)) {
|
||||||
|
config.data = this._elProps.data
|
||||||
|
}
|
||||||
|
if (!lodash.isEmpty(this._elProps.headers)) {
|
||||||
|
config.headers = this._elProps.headers
|
||||||
|
}
|
||||||
|
if (!lodash.isEmpty(this.custom)) {
|
||||||
|
config.custom = this.custom
|
||||||
|
}
|
||||||
|
const uploadOption = {
|
||||||
|
file: option.file,
|
||||||
|
fileName: option.file.name,
|
||||||
|
onProgress: option.onProgress,
|
||||||
|
onError: option.onError,
|
||||||
|
config: config
|
||||||
|
}
|
||||||
|
this.$emit('start', uploadOption)
|
||||||
|
return this.getUploader().then(uploader => {
|
||||||
|
return uploader.upload(uploadOption)
|
||||||
|
}).then(ret => {
|
||||||
|
if (this.suffix != null) {
|
||||||
|
ret.url += this.suffix
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onExceed (files, fileList) {
|
||||||
|
log.debug('文件数量超出限制')
|
||||||
|
if (this._elProps.limit === 1) {
|
||||||
|
this.clearFiles()
|
||||||
|
this.$refs.fileUploader.handleStart(files[0])
|
||||||
|
this.$refs.fileUploader.submit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$message({
|
||||||
|
showClose: true,
|
||||||
|
message: '已达最大限制数量,请删除一个文件后再上传',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clearFiles () {
|
||||||
|
if (this.$refs.fileUploader != null) {
|
||||||
|
this.$refs.fileUploader.clearFiles()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getFileList () {
|
||||||
|
return this.fileList
|
||||||
|
},
|
||||||
|
computeMd5 (file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
|
||||||
|
const chunkSize = 2097152 // Read in chunks of 2MB
|
||||||
|
const chunks = Math.ceil(file.size / chunkSize)
|
||||||
|
let currentChunk = 0
|
||||||
|
const spark = new SparkMD5.ArrayBuffer()
|
||||||
|
const fileReader = new FileReader()
|
||||||
|
|
||||||
|
fileReader.onload = (e) => {
|
||||||
|
spark.append(e.target.result) // Append array buffer
|
||||||
|
currentChunk++
|
||||||
|
|
||||||
|
if (currentChunk < chunks) {
|
||||||
|
loadNext()
|
||||||
|
} else {
|
||||||
|
const md5 = spark.end()
|
||||||
|
log.debug('computed hash', md5) // Compute hash
|
||||||
|
|
||||||
|
resolve(md5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileReader.onerror = function (error) {
|
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
reject('md5 computer error', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadNext () {
|
||||||
|
const start = currentChunk * chunkSize
|
||||||
|
const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize
|
||||||
|
|
||||||
|
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNext()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.d2p-file-uploader{
|
||||||
|
&.is-disabled{
|
||||||
|
.avatar-item-wrapper{
|
||||||
|
background-color: #F5F7FA;
|
||||||
|
border-color: #E4E7ED;
|
||||||
|
color: #C0C4CC;
|
||||||
|
cursor: not-allowed
|
||||||
|
}
|
||||||
|
li{
|
||||||
|
cursor: not-allowed
|
||||||
|
}
|
||||||
|
.el-upload-list__item-actions{
|
||||||
|
cursor: not-allowed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hide-plus{
|
||||||
|
.el-upload--picture-card{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.avatar-uploader{
|
||||||
|
display: flex;
|
||||||
|
.el-upload{
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
/*display: flex;*/
|
||||||
|
/*justify-content: center;*/
|
||||||
|
/*align-items: center;*/
|
||||||
|
background-color: #fbfdff;
|
||||||
|
border: 1px dashed #c0ccda;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.el-upload img{
|
||||||
|
max-width: 100px;
|
||||||
|
max-height: 100px;
|
||||||
|
}
|
||||||
|
.el-icon-plus.avatar-uploader-icon {
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
line-height: 100px;
|
||||||
|
}
|
||||||
|
.status-uploading{
|
||||||
|
border-radius: 6px;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
cursor: default;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
opacity: 1;
|
||||||
|
font-size: 20px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
-webkit-transition: opacity .3s;
|
||||||
|
transition: opacity .3s;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
.el-progress{
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
.el-progress__text {
|
||||||
|
color:#fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-upload--picture-card .el-icon-plus.avatar-uploader-icon {
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
line-height: 100px;
|
||||||
|
}
|
||||||
|
.image-uploader .el-upload-list--picture-card .el-upload-list__item-thumbnail {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
width:auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.el-upload-list--picture .el-upload-list__item-thumbnail {
|
||||||
|
max-height: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.image-uploader .el-upload-list--picture-card .el-upload-list__item {
|
||||||
|
/*display: flex;*/
|
||||||
|
/*justify-content: center;*/
|
||||||
|
/*align-items: center;*/
|
||||||
|
text-align: center;
|
||||||
|
line-height: 125px;
|
||||||
|
}
|
||||||
|
.image-uploader{
|
||||||
|
/*display: flex;flex-wrap: wrap;*/
|
||||||
|
.el-upload-list--picture-card .el-upload-list__item-actions{
|
||||||
|
line-height: 100px;
|
||||||
|
}
|
||||||
|
.el-upload-list--picture-card {
|
||||||
|
/*display: flex;*/
|
||||||
|
/*flex-wrap: wrap;*/
|
||||||
|
}
|
||||||
|
.el-upload-list__item-status-label{
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload--picture-card {
|
||||||
|
background-color: #fbfdff;
|
||||||
|
border: 1px dashed #c0ccda;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
cursor: pointer;
|
||||||
|
/*display: flex;*/
|
||||||
|
/*justify-content: center;*/
|
||||||
|
/*align-items: center;*/
|
||||||
|
}
|
||||||
|
.el-upload-list--picture-card {
|
||||||
|
.el-upload-list__item{
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
.el-progress-circle{
|
||||||
|
width: 70px !important;
|
||||||
|
height: 70px !important;
|
||||||
|
}
|
||||||
|
.el-progress{
|
||||||
|
width: 70px !important;
|
||||||
|
height: 70px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.avatar-item-wrapper{
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
.avatar{
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview{
|
||||||
|
border-radius: 6px;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
cursor: default;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
background-color: rgba(0,0,0,.9);
|
||||||
|
-webkit-transition: opacity .3s;
|
||||||
|
transition: opacity .3s;
|
||||||
|
&:hover{
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
i{
|
||||||
|
margin: 0 7px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,96 @@
|
||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<template v-if="type === 'text'">
|
||||||
|
<span v-for="(item) in items" :key="item.url" >
|
||||||
|
<el-link type="primary" size="mini" :underline="false" :href="item.url" target="_blank"> {{item.name}} </el-link>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else >
|
||||||
|
<el-tag class='tag-item d2-mr-5 d2-mb-2 d2-mt-2' v-for="(item) in items" :key="item.url" size="small" :type="item.color" >
|
||||||
|
<el-link type="primary" :underline="false" :href="item.url" target="_blank">{{item.name}}</el-link>
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 文件格式化展示组件
|
||||||
|
export default {
|
||||||
|
name: 'd2p-files-format',
|
||||||
|
props: {
|
||||||
|
// 值
|
||||||
|
value: {
|
||||||
|
require: true
|
||||||
|
},
|
||||||
|
// 颜色,【primary, success, warning, danger ,info】
|
||||||
|
color: {
|
||||||
|
require: false,
|
||||||
|
default: 'primary'
|
||||||
|
},
|
||||||
|
// 展示类型【text, tag】
|
||||||
|
type: {
|
||||||
|
default: 'tag' // 可选【text,tag】
|
||||||
|
},
|
||||||
|
// 构建下载url方法
|
||||||
|
buildUrl: {
|
||||||
|
type: Function,
|
||||||
|
default: function (value, item) { return value }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
items () {
|
||||||
|
if (this.value == null || this.value === '') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let valueArr = []
|
||||||
|
if (typeof (this.value) === 'string') {
|
||||||
|
valueArr = [this.getItem(this.value)]
|
||||||
|
} else if (this.value instanceof Array) {
|
||||||
|
// 本来就是数组的
|
||||||
|
valueArr = []
|
||||||
|
for (const val of this.value) {
|
||||||
|
valueArr.push(this.getItem(val))
|
||||||
|
}
|
||||||
|
} else if (this.value instanceof Object) {
|
||||||
|
valueArr = []
|
||||||
|
valueArr.push(this.getItem(this.value))
|
||||||
|
}
|
||||||
|
return valueArr
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getFileName (url) {
|
||||||
|
if (url && url.lastIndexOf('/') >= 0) {
|
||||||
|
return url.substring(url.lastIndexOf('/') + 1)
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
},
|
||||||
|
getItem (value) {
|
||||||
|
const url = this.buildUrl(value)
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
value: value,
|
||||||
|
name: this.getFileName(url),
|
||||||
|
color: this.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style >
|
||||||
|
.d2-mb-2{margin-bottom: 2px}
|
||||||
|
.d2-mt-2{margin-top: 2px;}
|
||||||
|
.d2-mr-5{margin-right: 5px;}
|
||||||
|
.tag-item{
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.el-divider__text, .el-link {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,133 @@
|
||||||
|
<template>
|
||||||
|
<span class="d2p-image-format">
|
||||||
|
<el-image
|
||||||
|
:style="{width:imgWidth,height:imgHeight,'margin-right':'10px',border:'1px solid #eee'}"
|
||||||
|
v-for="url in urls" :key="url" :src="url"
|
||||||
|
v-bind="_elProps" >
|
||||||
|
<div slot="placeholder" class="image-slot">
|
||||||
|
<img style="max-width:50%" src="./loading-spin.svg">
|
||||||
|
</div>
|
||||||
|
<template v-if="error==='slot'">
|
||||||
|
<div slot="error" class="image-slot">
|
||||||
|
<slot name="error"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="error">
|
||||||
|
<div slot="error" class="image-slot">
|
||||||
|
<img :src="error" width="50%"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 图片行展示组件
|
||||||
|
export default {
|
||||||
|
name: 'd2p-images-format',
|
||||||
|
props: {
|
||||||
|
// 图片的url
|
||||||
|
// 'url' 或 ['url1','url2']
|
||||||
|
value: {
|
||||||
|
type: [String, Array],
|
||||||
|
require: true
|
||||||
|
},
|
||||||
|
// 图片的宽度设置
|
||||||
|
width: {
|
||||||
|
require: false,
|
||||||
|
default: 30
|
||||||
|
},
|
||||||
|
// 图片的高度设置
|
||||||
|
height: {
|
||||||
|
require: false,
|
||||||
|
default: 30
|
||||||
|
},
|
||||||
|
fit: {
|
||||||
|
default: 'contain'
|
||||||
|
},
|
||||||
|
// 内部封装[el-image](https://element.eleme.cn/#/zh-CN/component/image)组件的属性参数<br/>
|
||||||
|
elProps: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
// 构建下载url方法
|
||||||
|
buildUrl: {
|
||||||
|
type: Function,
|
||||||
|
default: function (value, item) { return value }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
urls () {
|
||||||
|
const urls = []
|
||||||
|
if (this.value == null || this.value === '') {
|
||||||
|
return urls
|
||||||
|
}
|
||||||
|
if (typeof (this.value) === 'string') {
|
||||||
|
urls.push(this.value)
|
||||||
|
} else if (this.value instanceof Array) {
|
||||||
|
for (const item of this.value) {
|
||||||
|
if (item == null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (item.url != null) {
|
||||||
|
urls.push(item.url)
|
||||||
|
} else {
|
||||||
|
urls.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
urls.push(this.value.url)
|
||||||
|
}
|
||||||
|
const arr = []
|
||||||
|
for (const url of urls) {
|
||||||
|
arr.push(this.buildUrl(url))
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
},
|
||||||
|
imgHeight () {
|
||||||
|
if (typeof (this.height) === 'number') {
|
||||||
|
return this.height + 'px'
|
||||||
|
}
|
||||||
|
return this.height
|
||||||
|
},
|
||||||
|
imgWidth () {
|
||||||
|
if (typeof (this.width) === 'number') {
|
||||||
|
return this.width + 'px'
|
||||||
|
}
|
||||||
|
return this.width
|
||||||
|
},
|
||||||
|
_elProps () {
|
||||||
|
const defaultElProps = { fit: this.fit, previewSrcList: this.urls }
|
||||||
|
Object.assign(defaultElProps, this.elProps)
|
||||||
|
return defaultElProps
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClick () {
|
||||||
|
// this.$emit('input', !this.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.d2p-image-format{
|
||||||
|
.image-slot{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.el-image-viewer__close {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="#999">
|
||||||
|
<path opacity=".25" d="M16 0 A16 16 0 0 0 16 32 A16 16 0 0 0 16 0 M16 4 A12 12 0 0 1 16 28 A12 12 0 0 1 16 4"/>
|
||||||
|
<path d="M16 0 A16 16 0 0 1 32 16 L28 16 A12 12 0 0 0 16 4z">
|
||||||
|
<animateTransform attributeName="transform" type="rotate" from="0 16 16" to="360 16 16" dur="0.8s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 422 B |
|
@ -2,7 +2,7 @@
|
||||||
<div ref="selectedTableRef">
|
<div ref="selectedTableRef">
|
||||||
<el-popover
|
<el-popover
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
width="400"
|
width="600"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
@show="visibleChange">
|
@show="visibleChange">
|
||||||
<div class="option">
|
<div class="option">
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
size="mini"
|
size="mini"
|
||||||
border
|
border
|
||||||
:row-key="dict.value"
|
:row-key="dict.value"
|
||||||
style="width: 400px"
|
style="width: 600px"
|
||||||
max-height="200"
|
max-height="200"
|
||||||
height="200"
|
height="200"
|
||||||
:highlight-current-row="!_elProps.tableConfig.multiple"
|
:highlight-current-row="!_elProps.tableConfig.multiple"
|
||||||
|
@ -25,8 +25,10 @@
|
||||||
>
|
>
|
||||||
<el-table-column v-if="_elProps.tableConfig.multiple" fixed type="selection" reserve-selection width="55"/>
|
<el-table-column v-if="_elProps.tableConfig.multiple" fixed type="selection" reserve-selection width="55"/>
|
||||||
<el-table-column fixed type="index" label="#" width="50"/>
|
<el-table-column fixed type="index" label="#" width="50"/>
|
||||||
|
<span v-for="(item,index) in _elProps.tableConfig.columns" :key="index" >
|
||||||
<el-table-column :prop="item.prop" :label="item.label" :width="item.width"
|
<el-table-column :prop="item.prop" :label="item.label" :width="item.width"
|
||||||
v-for="(item,index) in _elProps.tableConfig.columns" :key="index"/>
|
v-if="item.show !== false"/>
|
||||||
|
</span>
|
||||||
</el-table>
|
</el-table>
|
||||||
<el-pagination style="margin-top: 10px;max-width: 200px" background
|
<el-pagination style="margin-top: 10px;max-width: 200px" background
|
||||||
small
|
small
|
||||||
|
@ -206,7 +208,11 @@ export default {
|
||||||
// 在这里对 传入的value值做处理
|
// 在这里对 传入的value值做处理
|
||||||
const { url, value, label } = this.dict
|
const { url, value, label } = this.dict
|
||||||
params[value] = val
|
params[value] = val
|
||||||
params.query = `{${label},${value}}`
|
const queryList = ['id', label, value]
|
||||||
|
this._elProps.tableConfig.columns.map(res => {
|
||||||
|
queryList.push(res.prop)
|
||||||
|
})
|
||||||
|
params.query = `{${Array.from(new Set(queryList)).join(',')}}`
|
||||||
return request({
|
return request({
|
||||||
url: url,
|
url: url,
|
||||||
params: params,
|
params: params,
|
||||||
|
@ -241,10 +247,14 @@ export default {
|
||||||
if (that.dict.params) {
|
if (that.dict.params) {
|
||||||
dictParams = { ...that.dict.params }
|
dictParams = { ...that.dict.params }
|
||||||
}
|
}
|
||||||
|
const queryList = ['id', label, value]
|
||||||
|
this._elProps.tableConfig.columns.map(res => {
|
||||||
|
queryList.push(res.prop)
|
||||||
|
})
|
||||||
const params = {
|
const params = {
|
||||||
page: that.pageConfig.page,
|
page: that.pageConfig.page,
|
||||||
limit: that.pageConfig.limit,
|
limit: that.pageConfig.limit,
|
||||||
query: `{${label},${value}}`
|
query: `{${Array.from(new Set(queryList)).join(',')}}`
|
||||||
}
|
}
|
||||||
if (that.search) {
|
if (that.search) {
|
||||||
params.search = that.search
|
params.search = that.search
|
||||||
|
|
Loading…
Reference in New Issue