功能变化: 更新组件

pull/103/head
李强 2023-06-18 10:02:38 +08:00
parent e1071f1381
commit 03878eec5a
10 changed files with 2114 additions and 7 deletions

View File

@ -4,6 +4,6 @@ COPY web/. .
RUN npm install --registry=https://registry.npm.taobao.org
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 --from=0 /web/dist /usr/share/nginx/html

View File

@ -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
}

View File

@ -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,
// cropper40%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>

View File

@ -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: '图片裁剪'
},
// cropper40%270
cropperHeight: {
type: [String, Number]
},
// 50%
dialogWidth: {
type: [String, Number],
default: '50%'
},
// MB0
maxSize: {
type: Number,
default: 5
},
//
uploadTip: {
type: String
},
// cropperjshttps://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
},
// inputchange
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>

View File

@ -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)
}
}
}

View File

@ -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=md5size , key=key
returnType: {
type: String,
default: 'url'
},
//
custom: {
type: Object
},
// [el-upload](https://element.eleme.cn/#/zh-CN/component/upload)<br/>
// formactionnameheaders
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) => {
// sizemd5
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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -2,7 +2,7 @@
<div ref="selectedTableRef">
<el-popover
placement="bottom"
width="400"
width="600"
trigger="click"
@show="visibleChange">
<div class="option">
@ -16,7 +16,7 @@
size="mini"
border
:row-key="dict.value"
style="width: 400px"
style="width: 600px"
max-height="200"
height="200"
: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 fixed type="index" label="#" width="50"/>
<el-table-column :prop="item.prop" :label="item.label" :width="item.width"
v-for="(item,index) in _elProps.tableConfig.columns" :key="index"/>
<span v-for="(item,index) in _elProps.tableConfig.columns" :key="index" >
<el-table-column :prop="item.prop" :label="item.label" :width="item.width"
v-if="item.show !== false"/>
</span>
</el-table>
<el-pagination style="margin-top: 10px;max-width: 200px" background
small
@ -206,7 +208,11 @@ export default {
// value
const { url, value, label } = this.dict
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({
url: url,
params: params,
@ -241,10 +247,14 @@ export default {
if (that.dict.params) {
dictParams = { ...that.dict.params }
}
const queryList = ['id', label, value]
this._elProps.tableConfig.columns.map(res => {
queryList.push(res.prop)
})
const params = {
page: that.pageConfig.page,
limit: that.pageConfig.limit,
query: `{${label},${value}}`
query: `{${Array.from(new Set(queryList)).join(',')}}`
}
if (that.search) {
params.search = that.search