This commit is contained in:
oneao committed 2025-09-19 17:29:19 +08:00
1 parent d917b0f6e3
commit d37d92996a
3 files changed
+113 -106

No files matched your search

@@ -40,6 +40,7 @@ $btn-prefix: '#{$prefix}-btn';
justify-content: center; justify-content: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: visible;
.#{$modal-prefix} { .#{$modal-prefix} {
background: #fff; background: #fff;
@@ -59,7 +60,6 @@ $btn-prefix: '#{$prefix}-btn';
width: 100%; width: 100%;
font-size: 16px; font-size: 16px;
padding: 12px 16px; padding: 12px 16px;
border-bottom: 1px solid $separator-color;
word-break: break-all; word-break: break-all;
} }
@@ -111,7 +111,6 @@ $btn-prefix: '#{$prefix}-btn';
&-footer { &-footer {
padding: 12px 16px; padding: 12px 16px;
flex-shrink: 0; flex-shrink: 0;
border-top: 1px solid $separator-color;
text-align: right; text-align: right;
.#{$btn-prefix}:not(:first-child) { .#{$btn-prefix}:not(:first-child) {
margin-left: 12px; margin-left: 12px;
@@ -1,4 +1,4 @@
import type { PropType, Ref } from 'vue' import type { PropType } 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,32 +7,81 @@ import Icon from '../icon/index'
export default defineComponent({ export default defineComponent({
name: 'Modal', name: 'Modal',
props: { props: {
visible: { type: Boolean, default: undefined }, visible: {
defaultVisible: { type: Boolean, default: false }, type: Boolean,
mask: { type: Boolean, default: true }, default: undefined,
maskClosable: { type: Boolean, default: true }, },
position: { type: String as PropType<'top' | 'center' | 'bottom'>, default: 'center' }, defaultVisible: {
type: { type: String as PropType<'default' | 'info' | 'success' | 'warning' | 'danger' | 'question'>, default: 'default' }, type: Boolean,
showClose: { type: Boolean, default: true }, default: false,
confirmBtn: { type: null, default: () => ({ label: '确认', type: 'solid', status: 'primary', size: 'small' }) }, },
cancelBtn: { type: null, default: () => ({ label: '取消', type: 'solid', status: 'default', size: 'small' }) }, mask: {
onBeforeConfirm: { type: Function as PropType<(done: (ok: boolean) => void) => void | boolean | Promise<boolean>> }, type: Boolean,
onBeforeCancel: { type: Function as PropType<(done: (ok: boolean) => void) => void | boolean | Promise<boolean>> }, default: true,
destroyOnClose: { type: Boolean, default: false }, },
appendTo: { type: [String, Object] as PropType<string | HTMLElement>, default: 'body' }, maskClosable: {
fullscreen: { type: Boolean, default: false }, type: Boolean,
draggable: { type: Boolean, default: false }, default: true,
draggableBoundary: { type: Boolean, default: false }, },
draggableRef: { type: Object as PropType<Ref<HTMLElement | null> | null>, default: null }, position: {
class: { type: [String, Array, Object], default: '' }, type: String as PropType<'top' | 'center' | 'bottom'>,
style: { type: Object, default: () => ({}) }, 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' }),
},
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,
},
draggableSelector: {
type: String,
default: '',
}, // 拖拽区域选择器
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 modalRef = ref<HTMLElement>() const modalRef = ref<HTMLElement>()
const confirmLoading = ref(false)
const _visible = ref(props.defaultVisible) const _visible = ref(props.defaultVisible)
const isVisible = computed(() => props.visible ?? _visible.value) const isVisible = computed(() => props.visible ?? _visible.value)
@@ -69,48 +118,15 @@ export default defineComponent({
} }
const handleCancel = async () => { const handleCancel = async () => {
let allow = true emit('update:visible', false)
if (props.onBeforeCancel) { emit('cancel')
try { confirmLoading.value = false
allow = await new Promise<boolean>((resolve) => {
const result = props.onBeforeCancel!(ok => resolve(ok))
if (typeof result === 'boolean')
resolve(result)
else if (result instanceof Promise)
result.then(resolve).catch(() => resolve(false))
})
}
catch { allow = false }
}
if (allow) {
emit('update:visible', false)
emit('cancel')
confirmLoading.value = false
}
} }
const handleConfirm = async () => { const handleConfirm = async () => {
let allow = true emit('update:visible', false)
if (props.onBeforeConfirm) { emit('confirm')
confirmLoading.value = true
try {
allow = await new Promise<boolean>((resolve) => {
const result = props.onBeforeConfirm!(ok => resolve(ok))
if (typeof result === 'boolean')
resolve(result)
else if (result instanceof Promise)
result.then(resolve).catch(() => resolve(false))
})
}
catch { allow = false }
}
if (allow) {
emit('update:visible', false)
emit('confirm')
}
confirmLoading.value = false confirmLoading.value = false
} }
const handleAfterEnter = () => emit('open') const handleAfterEnter = () => emit('open')
const handleAfterLeave = () => { const handleAfterLeave = () => {
isRootVisible.value = false isRootVisible.value = false
@@ -125,28 +141,21 @@ export default defineComponent({
const handleMouseMove = (e: MouseEvent) => { const handleMouseMove = (e: MouseEvent) => {
if (!dragData.dragging || !modalRef.value) if (!dragData.dragging || !modalRef.value)
return return
const modal = modalRef.value const rect = modalRef.value.getBoundingClientRect()
const rect = modal.getBoundingClientRect() let left = e.clientX - dragData.offsetX
let newLeft = e.clientX - dragData.offsetX let top = e.clientY - dragData.offsetY
let newTop = e.clientY - dragData.offsetY
if (props.draggableBoundary) { if (props.draggableBoundary) {
const winWidth = window.innerWidth const w = window.innerWidth
const winHeight = window.innerHeight const h = window.innerHeight
if (newLeft < 0) left = Math.min(Math.max(0, left), w - rect.width)
newLeft = 0 top = Math.min(Math.max(0, top), h - rect.height)
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` modalRef.value.style.left = `${left}px`
modal.style.top = `${newTop}px` modalRef.value.style.top = `${top}px`
modal.style.position = 'absolute' modalRef.value.style.position = 'absolute'
modal.style.transform = '' modalRef.value.style.transform = ''
} }
const handleMouseUp = () => { const handleMouseUp = () => {
@@ -156,21 +165,23 @@ export default defineComponent({
} }
const handleMouseDown = (e: MouseEvent) => { const handleMouseDown = (e: MouseEvent) => {
if (!modalRef.value) if (!props.draggable || !modalRef.value)
return return
let dragTarget: HTMLElement | null = null // 自动选择拖拽区域
let dragEl: HTMLElement | null = null
// 如果传了 ref if (props.draggableSelector) {
if (props.draggableRef?.value) { dragEl = modalRef.value.querySelector(props.draggableSelector)
dragTarget = props.draggableRef.value
} }
// 否则默认 header
else { else {
dragTarget = modalRef.value.querySelector(`.${prefixCls}-header`) dragEl = modalRef.value.querySelector(`.${prefixCls}-header`)
} }
// 如果没找到,默认拖动整个 modal
if (!dragEl)
dragEl = modalRef.value
if (!dragTarget || !dragTarget.contains(e.target as Node)) if (!dragEl.contains(e.target as Node))
return return
dragData.dragging = true dragData.dragging = true
@@ -199,22 +210,22 @@ export default defineComponent({
<div v-show={isVisible.value} class={`${prefixCls}-mask`} onClick={handleMaskClick} /> <div v-show={isVisible.value} class={`${prefixCls}-mask`} onClick={handleMaskClick} />
</Transition> </Transition>
)} )}
<Transition <Transition name={`${prefixCls}-content-fade`} appear onAfterEnter={handleAfterEnter} onAfterLeave={handleAfterLeave}>
name={`${prefixCls}-content-fade`}
appear
onAfterEnter={handleAfterEnter}
onAfterLeave={handleAfterLeave}
>
<div <div
v-show={isVisible.value} v-show={isVisible.value}
class={[`${prefixCls}-wrapper`, `${prefixCls}-position-${props.position}`, props.class]} class={[`${prefixCls}-wrapper`, `${prefixCls}-position-${props.position}`, props.class]}
style={{ ...props.style }} style={{ ...props.style }}
> >
<div ref={modalRef} class={`${prefixCls} ${props.fullscreen ? `${prefixCls}-fullscreen` : ''}`}> <div
ref={modalRef}
onMousedown={handleMouseDown}
class={`${prefixCls} ${props.fullscreen ? `${prefixCls}-fullscreen` : ''}`}
>
<div <div
class={`${prefixCls}-header`} class={`${prefixCls}-header`}
onMousedown={handleMouseDown} style={{
style={{ cursor: props.draggable ? 'move' : undefined }} cursor: props.draggable && (!props.draggableSelector || props.draggableSelector === `${prefixCls}-header`) ? '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>
+7 -10
View File
@@ -18,16 +18,13 @@ const dragHandle = ref<HTMLElement | null>(null)
打开 Header 拖拽 打开 Header 拖拽
</AButton> </AButton>
<!-- ref 拖拽 --> <AModal v-model:visible="visible2" :draggable="true" draggable-selector=".drag-area">
<AModal v-model:visible="visible2" :draggable="true" :draggable-ref="dragHandle"> <div class="drag-area" style="padding:10px; background:#ffdede; cursor:move">
<template #title> 拖拽这里
Ref 拖拽示例 </div>
</template> <p>Modal 内容</p>
<div ref="dragHandle" style="padding:10px; background:#ffdede; cursor:move;"> </AModal>
拖拽这个区域 (ref)
</div>
<p>Modal 内容</p>
</AModal>
<AButton @click="visible2 = true"> <AButton @click="visible2 = true">
打开 Ref 拖拽 打开 Ref 拖拽
</AButton> </AButton>