This commit is contained in:
oneao committed 2025-09-18 22:11:18 +08:00
1 parent 35bc29f356
commit d917b0f6e3
2 files changed
+123 -124

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 { computed, defineComponent, onMounted, ref, Teleport, Transition, watch } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls' import { getPrefixCls } from '../_utils/getPrefixCls'
import Button from '../button/index' import Button from '../button/index'
@@ -7,90 +7,43 @@ import Icon from '../icon/index'
export default defineComponent({ export default defineComponent({
name: 'Modal', name: 'Modal',
props: { props: {
visible: { visible: { type: Boolean, default: undefined },
type: Boolean, defaultVisible: { type: Boolean, default: false },
default: undefined, mask: { type: Boolean, default: true },
}, maskClosable: { type: Boolean, default: true },
defaultVisible: { position: { type: String as PropType<'top' | 'center' | 'bottom'>, default: 'center' },
type: Boolean, type: { type: String as PropType<'default' | 'info' | 'success' | 'warning' | 'danger' | 'question'>, default: 'default' },
default: false, showClose: { type: Boolean, default: true },
}, confirmBtn: { type: null, default: () => ({ label: '确认', type: 'solid', status: 'primary', size: 'small' }) },
mask: { cancelBtn: { type: null, default: () => ({ label: '取消', type: 'solid', status: 'default', size: 'small' }) },
type: Boolean, onBeforeConfirm: { type: Function as PropType<(done: (ok: boolean) => void) => void | boolean | Promise<boolean>> },
default: true, onBeforeCancel: { type: Function as PropType<(done: (ok: boolean) => void) => void | boolean | Promise<boolean>> },
}, destroyOnClose: { type: Boolean, default: false },
maskClosable: { appendTo: { type: [String, Object] as PropType<string | HTMLElement>, default: 'body' },
type: Boolean, fullscreen: { type: Boolean, default: false },
default: true, draggable: { type: Boolean, default: false },
}, draggableBoundary: { type: Boolean, default: false },
position: { draggableRef: { type: Object as PropType<Ref<HTMLElement | null> | null>, default: null },
type: String as PropType<'top' | 'center' | 'bottom'>, class: { type: [String, Array, Object], default: '' },
default: 'center', style: { type: Object, default: () => ({}) },
},
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: () => ({}),
},
}, },
emits: ['update:visible', 'confirm', 'cancel', 'open', 'close', 'beforeOpen', 'beforeClose'], emits: ['update:visible', 'confirm', 'cancel', 'open', 'close', 'beforeOpen', 'beforeClose'],
setup(props, { slots, emit }) { setup(props, { slots, emit }) {
const prefixCls = getPrefixCls('modal') const prefixCls = getPrefixCls('modal')
const targetEl = ref<HTMLElement>(document.body) const targetEl = ref<HTMLElement>(document.body)
const confirmLoading = ref(false) const confirmLoading = ref(false)
const modalRef = ref<HTMLElement>()
const _visible = ref(props.defaultVisible) const _visible = ref(props.defaultVisible)
const isVisible = computed(() => props.visible ?? _visible.value) const isVisible = computed(() => props.visible ?? _visible.value)
const isMounted = ref(isVisible.value) const isMounted = ref(isVisible.value)
const isRootVisible = ref(isVisible.value) const isRootVisible = ref(isVisible.value)
watch(isVisible, (value) => { watch(isVisible, (value) => {
if (_visible.value !== value) { if (_visible.value !== value)
_visible.value = value _visible.value = value
}
if (value) { if (value) {
emit('beforeOpen') emit('beforeOpen')
isMounted.value = true isMounted.value = true
isRootVisible.value = true isRootVisible.value = true
} }
@@ -111,10 +64,9 @@ export default defineComponent({
}) })
const handleMaskClick = () => { const handleMaskClick = () => {
if (props.maskClosable) { if (props.maskClosable)
emit('update:visible', false) emit('update:visible', false)
} }
}
const handleCancel = async () => { const handleCancel = async () => {
let allow = true let allow = true
@@ -128,11 +80,8 @@ export default defineComponent({
result.then(resolve).catch(() => resolve(false)) result.then(resolve).catch(() => resolve(false))
}) })
} }
catch { catch { allow = false }
allow = false
} }
}
if (allow) { if (allow) {
emit('update:visible', false) emit('update:visible', false)
emit('cancel') emit('cancel')
@@ -162,18 +111,77 @@ export default defineComponent({
confirmLoading.value = false confirmLoading.value = false
} }
const handleAfterEnter = () => { const handleAfterEnter = () => emit('open')
emit('open')
}
const handleAfterLeave = () => { const handleAfterLeave = () => {
isRootVisible.value = false isRootVisible.value = false
if (props.destroyOnClose) { if (props.destroyOnClose)
isMounted.value = false isMounted.value = false
}
emit('close') 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> = { const typeIconMap: Record<string, string> = {
success: 'ix:success-filled', success: 'ix:success-filled',
danger: 'ix:error-filled', danger: 'ix:error-filled',
@@ -182,8 +190,7 @@ export default defineComponent({
question: 'ix:question-filled', question: 'ix:question-filled',
} }
return () => { return () => (
return (
<Teleport to={targetEl.value}> <Teleport to={targetEl.value}>
{isMounted.value && ( {isMounted.value && (
<div v-show={isRootVisible.value} class={`${prefixCls}-container`} style={{ position: 'fixed', zIndex: 1000 }}> <div v-show={isRootVisible.value} class={`${prefixCls}-container`} style={{ position: 'fixed', zIndex: 1000 }}>
@@ -198,13 +205,22 @@ export default defineComponent({
onAfterEnter={handleAfterEnter} onAfterEnter={handleAfterEnter}
onAfterLeave={handleAfterLeave} onAfterLeave={handleAfterLeave}
> >
<div v-show={isVisible.value} class={[`${prefixCls}-wrapper`, `${prefixCls}-position-${props.position}`, props.class]} style={{ ...props.style }}> <div
<div class={`${prefixCls} ${props.fullscreen ? `${prefixCls}-fullscreen` : ''}`}> v-show={isVisible.value}
<div class={`${prefixCls}-header`}> class={[`${prefixCls}-wrapper`, `${prefixCls}-position-${props.position}`, props.class]}
style={{ ...props.style }}
>
<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]} />} {props.type !== 'default' && <Icon class={[`${prefixCls}-icon`, `${prefixCls}-icon-${props.type}`]} icon={typeIconMap[props.type]} />}
<span class={`${prefixCls}-title`}>{slots.title?.()}</span> <span class={`${prefixCls}-title`}>{slots.title?.()}</span>
{props.showClose && <Icon class={`${prefixCls}-close`} icon="ix:close" />} {props.showClose && <Icon class={`${prefixCls}-close`} icon="ix:close" />}
</div> </div>
<div class={`${prefixCls}-body`}>{slots.default?.()}</div> <div class={`${prefixCls}-body`}>{slots.default?.()}</div>
<div class={`${prefixCls}-footer`}> <div class={`${prefixCls}-footer`}>
{slots.footer?.() ?? ( {slots.footer?.() ?? (
@@ -221,6 +237,5 @@ export default defineComponent({
)} )}
</Teleport> </Teleport>
) )
}
}, },
}) })
+21 -37
View File
@@ -1,50 +1,34 @@
<script setup lang="ts"> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
const AModal1Visible = ref(false) const visible1 = ref(false)
const AModal2Visible = ref(false) const visible2 = ref(false)
const AModal3Visible = ref(false) const dragHandle = ref<HTMLElement | null>(null)
</script> </script>
<template> <template>
<button @click="AModal1Visible = true"> <!-- 默认 header 拖拽 -->
打开第 1 个 AModal <AModal v-model:visible="visible1" :draggable="true">
</button>
<!-- 第 1 个 AModal -->
<AModal v-model:visible="AModal1Visible" mask-closable >
<template #title> <template #title>
第 1 个 AModal Header 拖拽示例
</template> </template>
<p>这是第 1 个弹窗内容</p> <p>这是 header 拖拽</p>
</AModal> </AModal>
<AButton @click="visible1 = true">
打开 Header 拖拽
</AButton>
<!-- 第 2 个 AModal --> <!-- ref 拖拽 -->
<AModal v-model:visible="AModal2Visible" mask-closable> <AModal v-model:visible="visible2" :draggable="true" :draggable-ref="dragHandle">
<template #title> <template #title>
第 2 个 AModal Ref 拖拽示例
</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>
</template> </template>
<div ref="dragHandle" style="padding:10px; background:#ffdede; cursor:move;">
拖拽这个区域 (ref)
</div>
<p>Modal 内容</p>
</AModal> </AModal>
<AButton @click="visible2 = true">
打开 Ref 拖拽
</AButton>
</template> </template>