u
This commit is contained in:
1 parent
35bc29f356
commit
d917b0f6e3
2 files changed
+154
-155
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import type { PropType } from 'vue'
|
||||
import type { PropType, Ref } from 'vue'
|
||||
import { computed, defineComponent, onMounted, ref, Teleport, Transition, watch } from 'vue'
|
||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||
import Button from '../button/index'
|
||||
@@ -7,90 +7,43 @@ import Icon from '../icon/index'
|
||||
export default defineComponent({
|
||||
name: 'Modal',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
defaultVisible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
mask: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
maskClosable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
position: {
|
||||
type: String as PropType<'top' | 'center' | 'bottom'>,
|
||||
default: 'center',
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'default' | 'info' | 'success' | 'warning' | 'danger' | 'question'>,
|
||||
default: 'default',
|
||||
},
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
confirmBtn: {
|
||||
type: null,
|
||||
default: () => ({ label: '确认', type: 'solid', status: 'primary', size: 'small' }),
|
||||
},
|
||||
cancelBtn: {
|
||||
type: null,
|
||||
default: () => ({ label: '取消', type: 'solid', status: 'default', size: 'small' }),
|
||||
},
|
||||
onBeforeConfirm: {
|
||||
type: Function as PropType<(done: (ok: boolean) => void) => void | boolean | Promise<boolean>>,
|
||||
},
|
||||
onBeforeCancel: {
|
||||
type: Function as PropType<(done: (ok: boolean) => void) => void | boolean | Promise<boolean>>,
|
||||
},
|
||||
destroyOnClose: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
appendTo: {
|
||||
type: [String, Object] as PropType<string | HTMLElement>,
|
||||
default: 'body',
|
||||
},
|
||||
fullscreen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
class: {
|
||||
type: [String, Array, Object],
|
||||
default: '',
|
||||
},
|
||||
style: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
visible: { type: Boolean, default: undefined },
|
||||
defaultVisible: { type: Boolean, default: false },
|
||||
mask: { type: Boolean, default: true },
|
||||
maskClosable: { type: Boolean, default: true },
|
||||
position: { type: String as PropType<'top' | 'center' | 'bottom'>, default: 'center' },
|
||||
type: { type: String as PropType<'default' | 'info' | 'success' | 'warning' | 'danger' | 'question'>, default: 'default' },
|
||||
showClose: { type: Boolean, default: true },
|
||||
confirmBtn: { type: null, default: () => ({ label: '确认', type: 'solid', status: 'primary', size: 'small' }) },
|
||||
cancelBtn: { type: null, default: () => ({ label: '取消', type: 'solid', status: 'default', size: 'small' }) },
|
||||
onBeforeConfirm: { type: Function as PropType<(done: (ok: boolean) => void) => void | boolean | Promise<boolean>> },
|
||||
onBeforeCancel: { type: Function as PropType<(done: (ok: boolean) => void) => void | boolean | Promise<boolean>> },
|
||||
destroyOnClose: { type: Boolean, default: false },
|
||||
appendTo: { type: [String, Object] as PropType<string | HTMLElement>, default: 'body' },
|
||||
fullscreen: { type: Boolean, default: false },
|
||||
draggable: { type: Boolean, default: false },
|
||||
draggableBoundary: { type: Boolean, default: false },
|
||||
draggableRef: { type: Object as PropType<Ref<HTMLElement | null> | null>, default: null },
|
||||
class: { type: [String, Array, Object], default: '' },
|
||||
style: { type: Object, default: () => ({}) },
|
||||
},
|
||||
emits: ['update:visible', 'confirm', 'cancel', 'open', 'close', 'beforeOpen', 'beforeClose'],
|
||||
setup(props, { slots, emit }) {
|
||||
const prefixCls = getPrefixCls('modal')
|
||||
const targetEl = ref<HTMLElement>(document.body)
|
||||
const confirmLoading = ref(false)
|
||||
const modalRef = ref<HTMLElement>()
|
||||
|
||||
const _visible = ref(props.defaultVisible)
|
||||
|
||||
const isVisible = computed(() => props.visible ?? _visible.value)
|
||||
|
||||
const isMounted = ref(isVisible.value)
|
||||
const isRootVisible = ref(isVisible.value)
|
||||
|
||||
watch(isVisible, (value) => {
|
||||
if (_visible.value !== value) {
|
||||
if (_visible.value !== value)
|
||||
_visible.value = value
|
||||
}
|
||||
|
||||
if (value) {
|
||||
emit('beforeOpen')
|
||||
|
||||
isMounted.value = true
|
||||
isRootVisible.value = true
|
||||
}
|
||||
@@ -111,9 +64,8 @@ export default defineComponent({
|
||||
})
|
||||
|
||||
const handleMaskClick = () => {
|
||||
if (props.maskClosable) {
|
||||
if (props.maskClosable)
|
||||
emit('update:visible', false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = async () => {
|
||||
@@ -128,11 +80,8 @@ export default defineComponent({
|
||||
result.then(resolve).catch(() => resolve(false))
|
||||
})
|
||||
}
|
||||
catch {
|
||||
allow = false
|
||||
}
|
||||
catch { allow = false }
|
||||
}
|
||||
|
||||
if (allow) {
|
||||
emit('update:visible', false)
|
||||
emit('cancel')
|
||||
@@ -162,18 +111,77 @@ export default defineComponent({
|
||||
confirmLoading.value = false
|
||||
}
|
||||
|
||||
const handleAfterEnter = () => {
|
||||
emit('open')
|
||||
}
|
||||
|
||||
const handleAfterEnter = () => emit('open')
|
||||
const handleAfterLeave = () => {
|
||||
isRootVisible.value = false
|
||||
if (props.destroyOnClose) {
|
||||
if (props.destroyOnClose)
|
||||
isMounted.value = false
|
||||
}
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 拖拽逻辑
|
||||
const dragData = { dragging: false, offsetX: 0, offsetY: 0 }
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!dragData.dragging || !modalRef.value)
|
||||
return
|
||||
const modal = modalRef.value
|
||||
const rect = modal.getBoundingClientRect()
|
||||
let newLeft = e.clientX - dragData.offsetX
|
||||
let newTop = e.clientY - dragData.offsetY
|
||||
|
||||
if (props.draggableBoundary) {
|
||||
const winWidth = window.innerWidth
|
||||
const winHeight = window.innerHeight
|
||||
if (newLeft < 0)
|
||||
newLeft = 0
|
||||
if (newTop < 0)
|
||||
newTop = 0
|
||||
if (newLeft + rect.width > winWidth)
|
||||
newLeft = winWidth - rect.width
|
||||
if (newTop + rect.height > winHeight)
|
||||
newTop = winHeight - rect.height
|
||||
}
|
||||
|
||||
modal.style.left = `${newLeft}px`
|
||||
modal.style.top = `${newTop}px`
|
||||
modal.style.position = 'absolute'
|
||||
modal.style.transform = ''
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
dragData.dragging = false
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
if (!modalRef.value)
|
||||
return
|
||||
|
||||
let dragTarget: HTMLElement | null = null
|
||||
|
||||
// 如果传了 ref
|
||||
if (props.draggableRef?.value) {
|
||||
dragTarget = props.draggableRef.value
|
||||
}
|
||||
// 否则默认 header
|
||||
else {
|
||||
dragTarget = modalRef.value.querySelector(`.${prefixCls}-header`)
|
||||
}
|
||||
|
||||
if (!dragTarget || !dragTarget.contains(e.target as Node))
|
||||
return
|
||||
|
||||
dragData.dragging = true
|
||||
const rect = modalRef.value.getBoundingClientRect()
|
||||
dragData.offsetX = e.clientX - rect.left
|
||||
dragData.offsetY = e.clientY - rect.top
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
const typeIconMap: Record<string, string> = {
|
||||
success: 'ix:success-filled',
|
||||
danger: 'ix:error-filled',
|
||||
@@ -182,45 +190,52 @@ export default defineComponent({
|
||||
question: 'ix:question-filled',
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<Teleport to={targetEl.value}>
|
||||
{isMounted.value && (
|
||||
<div v-show={isRootVisible.value} class={`${prefixCls}-container`} style={{ position: 'fixed', zIndex: 1000 }}>
|
||||
{props.mask && (
|
||||
<Transition name={`${prefixCls}-mask-fade`} appear>
|
||||
<div v-show={isVisible.value} class={`${prefixCls}-mask`} onClick={handleMaskClick} />
|
||||
</Transition>
|
||||
)}
|
||||
<Transition
|
||||
name={`${prefixCls}-content-fade`}
|
||||
appear
|
||||
onAfterEnter={handleAfterEnter}
|
||||
onAfterLeave={handleAfterLeave}
|
||||
return () => (
|
||||
<Teleport to={targetEl.value}>
|
||||
{isMounted.value && (
|
||||
<div v-show={isRootVisible.value} class={`${prefixCls}-container`} style={{ position: 'fixed', zIndex: 1000 }}>
|
||||
{props.mask && (
|
||||
<Transition name={`${prefixCls}-mask-fade`} appear>
|
||||
<div v-show={isVisible.value} class={`${prefixCls}-mask`} onClick={handleMaskClick} />
|
||||
</Transition>
|
||||
)}
|
||||
<Transition
|
||||
name={`${prefixCls}-content-fade`}
|
||||
appear
|
||||
onAfterEnter={handleAfterEnter}
|
||||
onAfterLeave={handleAfterLeave}
|
||||
>
|
||||
<div
|
||||
v-show={isVisible.value}
|
||||
class={[`${prefixCls}-wrapper`, `${prefixCls}-position-${props.position}`, props.class]}
|
||||
style={{ ...props.style }}
|
||||
>
|
||||
<div v-show={isVisible.value} class={[`${prefixCls}-wrapper`, `${prefixCls}-position-${props.position}`, props.class]} style={{ ...props.style }}>
|
||||
<div class={`${prefixCls} ${props.fullscreen ? `${prefixCls}-fullscreen` : ''}`}>
|
||||
<div class={`${prefixCls}-header`}>
|
||||
{props.type !== 'default' && <Icon class={[`${prefixCls}-icon`, `${prefixCls}-icon-${props.type}`]} icon={typeIconMap[props.type]} />}
|
||||
<span class={`${prefixCls}-title`}>{slots.title?.()}</span>
|
||||
{props.showClose && <Icon class={`${prefixCls}-close`} icon="ix:close" />}
|
||||
</div>
|
||||
<div class={`${prefixCls}-body`}>{slots.default?.()}</div>
|
||||
<div class={`${prefixCls}-footer`}>
|
||||
{slots.footer?.() ?? (
|
||||
<>
|
||||
<Button {...props.cancelBtn} onClick={handleCancel}>{props.cancelBtn.label}</Button>
|
||||
<Button loading={confirmLoading.value} {...props.confirmBtn} onClick={handleConfirm}>{props.confirmBtn.label}</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div ref={modalRef} class={`${prefixCls} ${props.fullscreen ? `${prefixCls}-fullscreen` : ''}`}>
|
||||
<div
|
||||
class={`${prefixCls}-header`}
|
||||
onMousedown={handleMouseDown}
|
||||
style={{ cursor: props.draggable ? 'move' : undefined }}
|
||||
>
|
||||
{props.type !== 'default' && <Icon class={[`${prefixCls}-icon`, `${prefixCls}-icon-${props.type}`]} icon={typeIconMap[props.type]} />}
|
||||
<span class={`${prefixCls}-title`}>{slots.title?.()}</span>
|
||||
{props.showClose && <Icon class={`${prefixCls}-close`} icon="ix:close" />}
|
||||
</div>
|
||||
|
||||
<div class={`${prefixCls}-body`}>{slots.default?.()}</div>
|
||||
<div class={`${prefixCls}-footer`}>
|
||||
{slots.footer?.() ?? (
|
||||
<>
|
||||
<Button {...props.cancelBtn} onClick={handleCancel}>{props.cancelBtn.label}</Button>
|
||||
<Button loading={confirmLoading.value} {...props.confirmBtn} onClick={handleConfirm}>{props.confirmBtn.label}</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Teleport>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Teleport>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -1,50 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const AModal1Visible = ref(false)
|
||||
const AModal2Visible = ref(false)
|
||||
const AModal3Visible = ref(false)
|
||||
const visible1 = ref(false)
|
||||
const visible2 = ref(false)
|
||||
const dragHandle = ref<HTMLElement | null>(null)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button @click="AModal1Visible = true">
|
||||
打开第 1 个 AModal
|
||||
</button>
|
||||
|
||||
<!-- 第 1 个 AModal -->
|
||||
<AModal v-model:visible="AModal1Visible" mask-closable >
|
||||
<!-- 默认 header 拖拽 -->
|
||||
<AModal v-model:visible="visible1" :draggable="true">
|
||||
<template #title>
|
||||
第 1 个 AModal
|
||||
Header 拖拽示例
|
||||
</template>
|
||||
<p>这是第 1 个弹窗内容</p>
|
||||
<p>这是 header 拖拽</p>
|
||||
</AModal>
|
||||
<AButton @click="visible1 = true">
|
||||
打开 Header 拖拽
|
||||
</AButton>
|
||||
|
||||
<!-- 第 2 个 AModal -->
|
||||
<AModal v-model:visible="AModal2Visible" mask-closable>
|
||||
<!-- ref 拖拽 -->
|
||||
<AModal v-model:visible="visible2" :draggable="true" :draggable-ref="dragHandle">
|
||||
<template #title>
|
||||
第 2 个 AModal
|
||||
</template>
|
||||
<p>这是第 2 个弹窗内容</p>
|
||||
<template #footer>
|
||||
<button @click="AModal3Visible = true">
|
||||
打开第 3 个 AModal
|
||||
</button>
|
||||
<button @click="AModal2Visible = false">
|
||||
关闭
|
||||
</button>
|
||||
</template>
|
||||
</AModal>
|
||||
|
||||
<!-- 第 3 个 AModal -->
|
||||
<AModal v-model:visible="AModal3Visible" mask-closable position="top">
|
||||
<template #title>
|
||||
第 3 个 AModal
|
||||
</template>
|
||||
<p>这是第 3 个弹窗内容</p>
|
||||
<template #footer>
|
||||
<button @click="AModal3Visible = false">
|
||||
关闭
|
||||
</button>
|
||||
Ref 拖拽示例
|
||||
</template>
|
||||
<div ref="dragHandle" style="padding:10px; background:#ffdede; cursor:move;">
|
||||
拖拽这个区域 (ref)
|
||||
</div>
|
||||
<p>Modal 内容</p>
|
||||
</AModal>
|
||||
<AButton @click="visible2 = true">
|
||||
打开 Ref 拖拽
|
||||
</AButton>
|
||||
</template>
|
||||
Reference in new issue
Block a user