<template>
<transition :name="animation">
<div v-if="currentVisible" v-show="!currentMinimized" class="my-dialog__wrapper" :style="wrapperStyle">
<div class="my-dialog__modal" v-if="modal"></div>
<MyDrag ref="dialog"
@mousedown.native="handleMousedown"
v-clickoutside="handleClickOutside"
v-bind="dragOptions"
:class="dialogClass"
:style="dialogStyle"
@stop="handleDragStop"
@drag="handleDrag"
@start="handleDragStart">
<MyResize ref="resize"
v-bind="resizeOptions"
@start="handleResizeStart"
@stop="handleResizeStop"
@resize="handleResize">
<Panel ref="panel"
v-bind="$attrs"
:title="title"
:icon="iconOptions"
:width="dialogWidth"
:height="dialogHeight"
:submit-text="submitText"
:cancel-text="cancelText"
:submit-loading="submitLoading"
:footer="footer"
:theme="theme"
:closable="closable"
:before-close="beforeClose"
:maximizable="maximizable"
:maximized.sync="currentMaximized"
:minimizable="minimizable"
:minimized.sync="currentMinimized"
@submit="handleSubmit"
@cancel="handleCancel"
@close="handleClose">
<template v-if="$slots.icon" v-slot:icon>
<slot name="icon"></slot>
</template>
<template v-if="$slots.title" v-slot:title>
<slot name="title"></slot>
</template>
<template v-if="$slots.tool" v-slot:tool>
<slot name="tool"></slot>
</template>
<template v-if="$slots.footer" v-slot:footer>
<slot name="footer"></slot>
</template>
<MySpin fit :tip="loadingTip" :loading="loading" ref="mySpin">
<SrcFrame v-if="src" :src="src" @load="handleSrcLoad"></SrcFrame>
<slot v-else></slot>
</MySpin>
</Panel>
</MyResize>
</MyDrag>
</div>
</transition>
</template>
<script>
/**
* 弹窗组件
* @module $ui/components/my-dialog
*/
import {MyDrag, MyResize, MySpin} from '$ui'
import {addResizeListener, removeResizeListener} from 'element-ui/lib/utils/resize-event'
import clickoutside from 'element-ui/lib/utils/clickoutside'
import {addClass, removeClass} from 'element-ui/lib/utils/dom'
import Panel from './Panel'
import SrcFrame from './SrcFrame'
let Z_INDEX = 100
/**
* 适配百分比或像素单位
* @private
* @param {number} max 最大的长度
* @param {string} value 要转换的长度值
* @return {number}
*/
function getLength(max, value) {
if (!value) return 0
const percent = value.includes('%')
const val = Number.parseFloat(value)
return percent ? max * val / 100 : val
}
/**
* 插槽
* @member slots
* @property {string} default 默认插槽,弹窗显示的内容
* @property {string} icon 定义标题前的图标
* @property {string} title 定义标题
* @property {string} tool 定义头部的工具按钮操作区
* @property {string} footer 定义底部, 定义底部将会导致确定、取消按钮失效
*/
export default {
name: 'MyDialog',
components: {
Panel,
MyDrag,
MyResize,
MySpin,
SrcFrame
},
directives: {
clickoutside
},
/**
* 属性参数
* @member props
* @property {boolean} [visible=true] 显示弹窗,支持sync修饰符
* @property {string} [title] 标题文本,复杂内容可通过插槽定义
* @property {string|object} [icon] 标题前的图标,可以是字体图标或svg
* @property {string} [width] 弹窗宽度
* @property {string} [height] 高度
* @property {array} [position] 弹窗默认位置
* @property {boolean} [modal] 显示遮罩层
* @property {string} [theme] 主题风格,可选值:'primary', 'dark', 'light'
* @property {boolean|object} [draggable] 拖拽配置,参考MyDrag组件
* @property {boolean|object} [resizable] resize配置,参考MyResize组件
* @property {string} [animation=el-fade-in] 显示动画
* @property {boolean} [footer=true] 显示底部
* @property {string} [submitText=确定] 确定按钮文本
* @property {string} [cancelText=取消] 取消按钮文本
* @property {boolean} [submitLoading] 确定按钮显示loading,防止重复提交
* @property {boolean} [closable=true] 窗体可关闭
* @property {function} [beforeClose] 窗体关闭前进行的操作,必须要返回Promise
* @property {boolean} [maximizable] 可最大化
* @property {boolean} [maximized] 初始是否最大化
* @property {boolean} [minimizable] 可最小化,最小化只隐藏,不销毁组件
* @property {boolean} [minimized] 初始是否最小化
* @property {boolean} [cancelClose] 点击取消按钮关闭窗体
* @property {boolean} [loading] 显示loading
* @property {boolean} [loadingTip=正在拼命加载...] loading提示文本
* @property {string} [src] 用iframe加载的页面地址
* @property {string} [bodyClass] dialog内容容器className
* @property {string} [target] 窗体加载到容器的html选择器
* @property {boolean} [closeOnClickOutside] 点击窗体外部关闭
*/
props: {
// 显示
visible: {
type: Boolean,
default: true
},
title: String,
icon: [String, Object],
width: String,
height: String,
position: {
type: Array,
validator(val) {
return !val || (val && val.length === 2 && !isNaN(val[0]) && !isNaN(val[1]))
}
},
modal: Boolean,
theme: {
type: String,
default: 'light',
validator(val) {
return ['primary', 'dark', 'light'].includes(val)
}
},
draggable: [Boolean, Object],
resizable: [Boolean, Object],
animation: {
type: String,
default: 'el-fade-in'
},
footer: {
type: Boolean,
default: true
},
submitText: {
type: String,
default: '确定'
},
submitLoading: Boolean,
cancelText: {
type: String,
default: '取消'
},
closable: {
type: Boolean,
default: true
},
beforeClose: Function,
maximizable: Boolean,
maximized: Boolean,
minimizable: Boolean,
minimized: Boolean,
cancelClose: {
type: Boolean,
default: true
},
loading: Boolean,
loadingTip: {
type: String,
default: '正在拼命加载...'
},
src: String,
bodyClass: String,
// 窗体加载到容器的html选择器
target: String,
closeOnClickOutside: Boolean
},
data() {
return {
currentVisible: this.visible,
viewWidth: 0,
viewHeight: 0,
viewLeft: 0,
viewTop: 0,
originalWidth: 0,
originalHeight: 0,
dialogWidth: 0,
dialogHeight: 0,
zIndex: ++Z_INDEX, // Z_INDEX,
currentMaximized: this.maximized,
currentMinimized: false,
positionProxy: this.position ? [...this.position] : null
}
},
computed: {
wrapperStyle() {
return {
zIndex: this.zIndex,
left: `${this.viewLeft}px`,
top: `${this.viewTop}px`
}
},
dialogClass() {
return {
'my-dialog': true,
'my-dialog--src': this.src,
[this.bodyClass]: !!this.bodyClass
}
},
dialogStyle() {
let left, top
if (!this.positionProxy) {
left = Math.max((this.viewWidth - this.originalWidth) / 2, 0)
top = Math.max((this.viewHeight - this.originalHeight) / 2, 0)
} else {
left = Math.max(Math.min(this.positionProxy[0], this.viewWidth - this.originalWidth), 0)
top = Math.max(Math.min(this.positionProxy[1], this.viewHeight - this.originalHeight), 0)
}
return {
left: `${left}px`,
top: `${top}px`
}
},
iconOptions() {
if (!this.icon) return null
return typeof this.icon === 'object'
? {
...this.icon
}
: {
name: this.icon
}
},
resizeOptions() {
return {
minWidth: 300,
minHeight: 150,
maxWidth: this.viewWidth,
maxHeight: this.viewHeight,
...(this.resizable || {}),
disabled: this.currentMaximized ? true : !this.resizable
}
},
dragOptions() {
return {
handle: () => {
if (this.$refs.panel) {
if (this.$refs.panel.$refs.header) {
return this.$refs.panel.$refs.header
}
return this.$refs.panel
}
return null
},
range: () => {
return {
left: -this.dialogWidth + 10,
top: 0,
width: this.viewWidth + this.dialogWidth - 20,
height: this.viewHeight - 10
}
},
disabled: this.currentMaximized ? true : !this.draggable,
...this.draggable
}
}
},
watch: {
width() {
this.$nextTick(() => {
this.updateView()
})
},
height() {
this.$nextTick(() => {
this.updateView()
})
},
visible(val) {
this.currentVisible = val
// 如果不可见,最小化已经无意义,需要重置
if (!val) {
this.currentMinimized = false
}
},
currentVisible(val) {
this.dispose()
if (val) {
this.zIndex = ++Z_INDEX
this.$nextTick(this.init)
}
},
maximized: {
immediate: true,
handler(val) {
this.maximizable && (this.currentMaximized = val)
}
},
currentMaximized(val) {
if (!this.currentVisible) return
/**
* 最大化变化时触发
* @event maximize
* @param {boolean} 是否最大化
*/
this.$emit('maximize', val)
this.$nextTick(() => {
this.maximize(val)
})
},
minimized: {
immediate: true,
handler(val) {
this.minimizable && (this.currentMinimized = val)
}
},
currentMinimized(val) {
// 如果组件不可见,最小化失效
if (!this.currentVisible) return
this.$nextTick(() => {
val ? this.hide() : this.show()
this.setBodyHidden(!val)
})
}
},
methods: {
init() {
if (!this.$el || this.$el.nodeType !== 1) return
if (this.target) {
this.targetDOM = document.querySelector(this.target)
this.targetDOM && this.targetDOM.appendChild(this.$el)
}
if (this.$el.parentNode) {
addResizeListener(this.$el.parentNode, this.updateView)
}
if (!this.draggable && !this.resizable) {
addResizeListener(this.$el, this.updateView)
}
this.updateView()
/**
* 窗体打开时触发
* @event open
*/
this.$emit('open')
this.setBodyHidden(true)
},
dispose() {
if (this.$el && this.$el.parentNode) {
removeResizeListener(this.$el.parentNode, this.updateView)
if (this.target) {
this.$el.parentNode.removeChild(this.$el)
}
}
if (!this.draggable && !this.resizable) {
removeResizeListener(this.$el, this.updateView)
}
this.setBodyHidden(false)
this.dialogWidth = null
this.dialogHeight = null
this.currentMinimized = this.minimized
this.currentMaximized = this.maximized
this.targetDOM = null
this.$emit('dispose')
},
updateView() {
if (!this.$el || !this.$refs.dialog) return
const rect = this.$el.getBoundingClientRect()
this.viewHeight = rect.height
this.viewWidth = rect.width
this.viewLeft = Math.max(document.documentElement.scrollLeft, document.body.scrollLeft)
this.viewTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop)
if (this.currentMaximized) {
this.maximize(true)
}
if (!this.dialogWidth || !this.dialogHeight) {
const dialogRect = this.$refs.dialog.$el.getBoundingClientRect()
this.originalWidth = this.dialogWidth = Math.min(getLength(this.viewWidth, this.width) || dialogRect.width, this.viewWidth - 20)
this.originalHeight = this.dialogHeight = Math.min(getLength(this.viewHeight, this.height) || dialogRect.height, this.viewHeight - 20)
}
},
// 重置窗体宽高方法:参数为
redoLayout(opt = {width: null, height: null}) {
const hHeight = this.$refs.panel.headerHeight
const fHeight = this.footer ? this.$refs.panel.footerHeight : 10
const innerNodes = this.$slots.default
const innerNodeHeight = innerNodes.reduce((total, node) => {
const nodeHeight = node.elm.offsetHeight || 0
total += nodeHeight
return total
}, 0)
const newDialogHeight = innerNodeHeight + hHeight + fHeight + 24
const resizeObj = {
height: Math.min(getLength(this.viewHeight, opt.h) || newDialogHeight, this.viewHeight - 20),
width: Math.min(getLength(this.viewWidth, opt.w), this.viewWidth - 20)
}
this.$nextTick(() => {
this.originalHeight = Math.min(resizeObj.height || this.dialogHeight, this.viewHeight - 20)
this.originalWidth = Math.min(resizeObj.width || this.dialogWidth, this.viewWidth - 20)
this.handleResize(resizeObj)
})
},
handleResizeStart(e) {
/**
* 开始改变尺寸时触发
* @event resize-start
* @param {Object} e
*/
this.$emit('resize-start', e)
},
handleResizeStop(e) {
/**
* 停止改变尺寸时触发
* @event resize-stop
* @param {Object} e
*/
this.$emit('resize-stop', e)
},
handleResize(e) {
const {width, height} = e
this.dialogWidth = width || this.dialogWidth
this.dialogHeight = height || this.dialogHeight
/**
* 改变尺寸时触发
* @event resize
* @param {Object} e
*/
this.$emit('resize', e)
},
handleDragStart(e) {
/**
* 开始拖拽时触发
* @event drag-start
* @param {Object} e
*/
this.$emit('drag-start', e)
},
handleDrag(e) {
/**
* 拖拽时触发
* @event drag
* @param {Object} e
*/
this.$emit('drag', e)
},
handleDragStop(e) {
if (this.positionProxy) { // 有定义position, 更新position
const dialogRect = e.$el.getBoundingClientRect()
this.positionProxy = [dialogRect.left, dialogRect.top]
}
/**
* 停止拖拽时触发
* @event drag-stop
* @param {Object} e
*/
this.$emit('drag-stop', e)
},
handleMousedown() {
this.zIndex = ++Z_INDEX
},
handleClickOutside() {
if (this.closeOnClickOutside) {
this.handleClose()
}
},
handleClose() {
this.currentVisible = false
this.$emit('update:visible', false)
/**
* 窗体关闭时触发
* @event close
*/
this.$emit('close')
},
handleSubmit() {
/**
* 点击确定按钮时触发
* @event submit
*/
this.$emit('submit')
},
handleCancel() {
/**
* 点击取消按钮时触发
* @event cancel
*/
this.$emit('cancel')
if (this.cancelClose) {
this.handleClose()
}
},
handleSrcLoad() {
/**
* iframe完成加载内容时触发
* @event load
*/
this.$emit('load')
},
maximize(maximized) {
if (maximized) {
// 记录全屏之前的窗口尺寸
if (!this.sizeCaches) {
this.sizeCaches = {
dialogWidth: this.dialogWidth,
dialogHeight: this.dialogHeight
}
}
this.originalWidth = this.dialogWidth = this.viewWidth
this.originalHeight = this.dialogHeight = this.viewHeight
} else {
if (this.sizeCaches) {
const {dialogWidth, dialogHeight} = this.sizeCaches
this.originalWidth = this.dialogWidth = dialogWidth
this.originalHeight = this.dialogHeight = dialogHeight
}
}
if (this.resizable) {
this.$refs.resize.width = this.originalWidth
this.$refs.resize.height = this.originalHeight
}
},
show() {
this.zIndex = ++Z_INDEX
this.currentMinimized = false
/**
* 显示时触发
* @event show
*/
this.$emit('show')
},
hide() {
this.currentMinimized = true
/**
* 隐藏时触发
* @event hide
*/
this.$emit('hide')
},
setBodyHidden(hidden) {
const name = 'my-dialog-hidden-' + this._uid
const targetDom = this.targetDOM || this.$el.parentNode
hidden ? addClass(targetDom, name) : removeClass(targetDom, name)
}
},
mounted() {
this.currentVisible && this.init()
},
beforeDestroy() {
this.dispose()
}
}
</script>