u
This commit is contained in:
1 parent
8eedf281fd
commit
35bc29f356
13 files changed
+675
-171
No files matched your search
@@ -102,6 +102,8 @@ $border-color-component: var(--ao-border-color-component); // 组件边框(Inp
|
|||||||
$bg-color-secondarycomponent: var(--ao-bg-color-secondarycomponent);
|
$bg-color-secondarycomponent: var(--ao-bg-color-secondarycomponent);
|
||||||
$bg-color-secondarycomponent-hover: var(--ao-bg-color-secondarycomponent-hover);
|
$bg-color-secondarycomponent-hover: var(--ao-bg-color-secondarycomponent-hover);
|
||||||
|
|
||||||
|
$bg-color-mask: var(--ao-bg-color-mask);
|
||||||
|
|
||||||
// shadow
|
// shadow
|
||||||
// 基础/下层 投影 hover 使用的组件包括:表格 /
|
// 基础/下层 投影 hover 使用的组件包括:表格 /
|
||||||
$shadow-surface: var(--ao-shadow-surface);
|
$shadow-surface: var(--ao-shadow-surface);
|
||||||
|
|||||||
@@ -79,6 +79,8 @@
|
|||||||
--ao-bg-color-secondarycomponent: #4b4b4b;
|
--ao-bg-color-secondarycomponent: #4b4b4b;
|
||||||
--ao-bg-color-secondarycomponent-hover: #5e5e5e;
|
--ao-bg-color-secondarycomponent-hover: #5e5e5e;
|
||||||
|
|
||||||
|
--ao-bg-color-mask: rgba(23, 23, 26, 0.6);
|
||||||
|
|
||||||
// shadow阴影
|
// shadow阴影
|
||||||
// 基础/下层 投影 hover 使用的组件包括:表格 /
|
// 基础/下层 投影 hover 使用的组件包括:表格 /
|
||||||
--ao-shadow-surface: 0 4px 6px rgba(0, 0, 0, 6%), 0 1px 10px rgba(0, 0, 0, 8%), 0 2px 4px rgba(0, 0, 0, 12%);
|
--ao-shadow-surface: 0 4px 6px rgba(0, 0, 0, 6%), 0 1px 10px rgba(0, 0, 0, 8%), 0 2px 4px rgba(0, 0, 0, 12%);
|
||||||
|
|||||||
@@ -80,6 +80,8 @@
|
|||||||
--ao-bg-color-secondarycomponent: #ddd;
|
--ao-bg-color-secondarycomponent: #ddd;
|
||||||
--ao-bg-color-secondarycomponent-hover: #c6c6c6;
|
--ao-bg-color-secondarycomponent-hover: #c6c6c6;
|
||||||
|
|
||||||
|
--ao-bg-color-mask: rgba(29, 33, 41, 0.6);
|
||||||
|
|
||||||
// shadow阴影
|
// shadow阴影
|
||||||
// 基础/下层 投影 hover 使用的组件包括:表格 /
|
// 基础/下层 投影 hover 使用的组件包括:表格 /
|
||||||
--ao-shadow-surface: 0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
|
--ao-shadow-surface: 0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
|
||||||
|
|||||||
@@ -37,9 +37,10 @@ export default defineComponent({
|
|||||||
const map = { small: 14, medium: 16, large: 16 }
|
const map = { small: 14, medium: 16, large: 16 }
|
||||||
return map[props.size]
|
return map[props.size]
|
||||||
})
|
})
|
||||||
const hasContent = !!slots.default?.().length
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
const hasContent = !!slots.default?.().length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
class={cls.value}
|
class={cls.value}
|
||||||
|
|||||||
@@ -38,9 +38,10 @@ export default defineComponent({
|
|||||||
type: null,
|
type: null,
|
||||||
default: () => ({ label: '取消', type: 'solid', status: 'default' }),
|
default: () => ({ label: '取消', type: 'solid', status: 'default' }),
|
||||||
},
|
},
|
||||||
asyncConfirm: {
|
beforeConfirm: {
|
||||||
type: Boolean,
|
type: Function as PropType<
|
||||||
default: false,
|
(done: (ok: boolean) => void) => void | boolean | Promise<boolean>
|
||||||
|
>,
|
||||||
},
|
},
|
||||||
maskClose: {
|
maskClose: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -83,17 +84,36 @@ export default defineComponent({
|
|||||||
return `${defaultPx}px`
|
return `${defaultPx}px`
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = async () => {
|
||||||
if (props.asyncConfirm) {
|
let allow = true
|
||||||
|
|
||||||
|
if (props.beforeConfirm) {
|
||||||
confirmLoading.value = true
|
confirmLoading.value = true
|
||||||
emit('confirm', close, () => {
|
|
||||||
confirmLoading.value = false
|
try {
|
||||||
})
|
allow = await new Promise<boolean>((resolve) => {
|
||||||
|
const result = props.beforeConfirm!(ok => resolve(ok))
|
||||||
|
|
||||||
|
if (typeof result === 'boolean') {
|
||||||
|
resolve(result)
|
||||||
|
}
|
||||||
|
else if (result instanceof Promise) {
|
||||||
|
result.then(ok => resolve(ok)).catch(() => resolve(false))
|
||||||
|
}
|
||||||
|
// 如果是用 done 回调,就等调用方触发 resolve
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
allow = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
|
if (allow) {
|
||||||
close()
|
close()
|
||||||
emit('confirm')
|
emit('confirm')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
confirmLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
|
|||||||
@@ -1,67 +1,156 @@
|
|||||||
@use '../_styles/_variables.scss' as *;
|
@use '../_styles/_variables.scss' as *;
|
||||||
|
|
||||||
$modal-prefix: '#{$prefix}-modal';
|
$modal-prefix: '#{$prefix}-modal';
|
||||||
|
$btn-prefix: '#{$prefix}-btn';
|
||||||
|
|
||||||
.#{$modal-prefix}-wrapper {
|
.#{$modal-prefix}-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
display: flex;
|
pointer-events: auto;
|
||||||
align-items: center;
|
outline: none;
|
||||||
justify-content: center;
|
width: 100%;
|
||||||
z-index: 1000;
|
height: 100%;
|
||||||
|
.#{$modal-prefix}-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: $bg-color-mask;
|
||||||
|
pointer-events: auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$modal-prefix}-position-top {
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-top: 20vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$modal-prefix}-position-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$modal-prefix}-position-bottom {
|
||||||
|
align-items: flex-end;
|
||||||
|
padding-bottom: 20vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$modal-prefix}-wrapper {
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.#{$modal-prefix} {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
width: 480px;
|
||||||
|
height: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid $separator-color;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-right: 6px;
|
||||||
|
&-info {
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
&-success {
|
||||||
|
color: $success-color;
|
||||||
|
}
|
||||||
|
&-warning {
|
||||||
|
color: $warning-color;
|
||||||
|
}
|
||||||
|
&-danger {
|
||||||
|
color: $danger-color;
|
||||||
|
}
|
||||||
|
&-question {
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
color: $text-color-assist;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
&:hover {
|
||||||
|
background-color: $default-color-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding: 12px 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-footer {
|
||||||
|
padding: 12px 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-top: 1px solid $separator-color;
|
||||||
|
text-align: right;
|
||||||
|
.#{$btn-prefix}:not(:first-child) {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$modal-prefix}-fullscreen {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 内容动画 - 修复版 */
|
||||||
|
.#{$modal-prefix}-content-fade-enter-active,
|
||||||
|
.#{$modal-prefix}-content-fade-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.3s ease,
|
||||||
|
transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.#{$modal-prefix}-mask {
|
.#{$modal-prefix}-content-fade-enter-from,
|
||||||
position: absolute;
|
.#{$modal-prefix}-content-fade-leave-to {
|
||||||
inset: 0;
|
opacity: 0;
|
||||||
background: rgba(0, 0, 0, 0.45);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
.#{$modal-prefix} {
|
.#{$modal-prefix}-content-fade-enter-to,
|
||||||
position: relative;
|
.#{$modal-prefix}-content-fade-leave-from {
|
||||||
z-index: 1001;
|
opacity: 1;
|
||||||
background: #fff;
|
transform: scale(1);
|
||||||
border-radius: 8px;
|
}
|
||||||
min-width: 300px;
|
/* 遮罩动画 - 修复版 */
|
||||||
max-width: 90%;
|
.#{$modal-prefix}-mask-fade-enter-active,
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
.#{$modal-prefix}-mask-fade-leave-active {
|
||||||
display: flex;
|
transition: opacity 0.3s ease;
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.#{$modal-prefix}-header {
|
.#{$modal-prefix}-mask-fade-enter-from,
|
||||||
padding: 12px 16px;
|
.#{$modal-prefix}-mask-fade-leave-to {
|
||||||
font-weight: 600;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.#{$modal-prefix}-close {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.#{$modal-prefix}-body {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.#{$modal-prefix}-footer {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.#{$modal-prefix}-fade-enter-active,
|
|
||||||
.#{$modal-prefix}-fade-leave-active {
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.#{$modal-prefix}-fade-enter-from,
|
|
||||||
.#{$modal-prefix}-fade-leave-to {
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@@ -1,85 +1,226 @@
|
|||||||
import { defineComponent, ref, Teleport, Transition, watch } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
|
import { computed, defineComponent, onMounted, ref, Teleport, Transition, watch } from 'vue'
|
||||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||||
import { addModal, getMaskZIndex, removeModal } from './modalManager'
|
import Button from '../button/index'
|
||||||
|
import Icon from '../icon/index'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Modal',
|
name: 'Modal',
|
||||||
props: {
|
props: {
|
||||||
visible: { type: Boolean, default: false },
|
visible: {
|
||||||
mask: { type: Boolean, default: true },
|
type: Boolean,
|
||||||
maskClosable: { type: Boolean, default: true },
|
default: undefined,
|
||||||
class: { type: [String, Array, Object], default: '' },
|
},
|
||||||
style: { type: Object, default: () => ({}) },
|
defaultVisible: {
|
||||||
lazy: { type: Boolean, default: true }, // 是否懒加载
|
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: () => ({}),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ['update:visible', 'close'],
|
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 zIndex = ref(0)
|
const targetEl = ref<HTMLElement>(document.body)
|
||||||
const isMounted = ref(false) // DOM 是否挂载
|
const confirmLoading = ref(false)
|
||||||
|
|
||||||
// 打开 Modal
|
const _visible = ref(props.defaultVisible)
|
||||||
const openModal = () => {
|
|
||||||
zIndex.value = addModal()
|
|
||||||
isMounted.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭 Modal
|
const isVisible = computed(() => props.visible ?? _visible.value)
|
||||||
const closeModal = () => {
|
|
||||||
removeModal(zIndex.value)
|
|
||||||
zIndex.value = 0
|
|
||||||
// 不卸载 DOM,仍保持 isMounted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
const isMounted = ref(isVisible.value)
|
||||||
() => props.visible,
|
const isRootVisible = ref(isVisible.value)
|
||||||
(val) => {
|
|
||||||
if (val)
|
watch(isVisible, (value) => {
|
||||||
openModal()
|
if (_visible.value !== value) {
|
||||||
else closeModal()
|
_visible.value = value
|
||||||
},
|
}
|
||||||
{ immediate: true },
|
|
||||||
)
|
if (value) {
|
||||||
|
emit('beforeOpen')
|
||||||
|
|
||||||
|
isMounted.value = true
|
||||||
|
isRootVisible.value = true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
emit('beforeClose')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.appendTo instanceof HTMLElement) {
|
||||||
|
targetEl.value = props.appendTo
|
||||||
|
}
|
||||||
|
else if (typeof props.appendTo === 'string') {
|
||||||
|
const el = document.querySelector(props.appendTo)
|
||||||
|
if (el)
|
||||||
|
targetEl.value = el as HTMLElement
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const handleMaskClick = () => {
|
const handleMaskClick = () => {
|
||||||
if (props.maskClosable) {
|
if (props.maskClosable) {
|
||||||
emit('update:visible', false)
|
emit('update:visible', false)
|
||||||
emit('close')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleCancel = async () => {
|
||||||
emit('update:visible', false)
|
let allow = true
|
||||||
|
if (props.onBeforeCancel) {
|
||||||
|
try {
|
||||||
|
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 () => {
|
||||||
|
let allow = true
|
||||||
|
if (props.onBeforeConfirm) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAfterEnter = () => {
|
||||||
|
emit('open')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAfterLeave = () => {
|
||||||
|
isRootVisible.value = false
|
||||||
|
if (props.destroyOnClose) {
|
||||||
|
isMounted.value = false
|
||||||
|
}
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => (
|
const typeIconMap: Record<string, string> = {
|
||||||
<Teleport to="body">
|
success: 'ix:success-filled',
|
||||||
{/* DOM 永远挂载,只用 v-show 控制显示 */}
|
danger: 'ix:error-filled',
|
||||||
<div
|
warning: 'ix:warning-filled',
|
||||||
class={`${prefixCls}-wrapper`}
|
info: 'ix:info-filled',
|
||||||
style={{
|
question: 'ix:question-filled',
|
||||||
zIndex: zIndex.value,
|
}
|
||||||
display: props.visible ? 'flex' : 'none',
|
|
||||||
}}
|
return () => {
|
||||||
>
|
return (
|
||||||
{props.mask && (
|
<Teleport to={targetEl.value}>
|
||||||
<div
|
{isMounted.value && (
|
||||||
class={`${prefixCls}-mask`}
|
<div v-show={isRootVisible.value} class={`${prefixCls}-container`} style={{ position: 'fixed', zIndex: 1000 }}>
|
||||||
style={{ zIndex: getMaskZIndex() }}
|
{props.mask && (
|
||||||
onClick={handleMaskClick}
|
<Transition name={`${prefixCls}-mask-fade`} appear>
|
||||||
/>
|
<div v-show={isVisible.value} class={`${prefixCls}-mask`} onClick={handleMaskClick} />
|
||||||
)}
|
</Transition>
|
||||||
<div class={[prefixCls, props.class]} style={{ ...props.style, zIndex: zIndex.value }}>
|
)}
|
||||||
<div class={`${prefixCls}-header`}>
|
<Transition
|
||||||
<span class={`${prefixCls}-title`}>{slots.title?.()}</span>
|
name={`${prefixCls}-content-fade`}
|
||||||
<button class={`${prefixCls}-close`} onClick={handleClose}>×</button>
|
appear
|
||||||
|
onAfterEnter={handleAfterEnter}
|
||||||
|
onAfterLeave={handleAfterLeave}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
<div class={`${prefixCls}-body`}>{slots.default?.()}</div>
|
)}
|
||||||
{slots.footer && <div class={`${prefixCls}-footer`}>{slots.footer()}</div>}
|
</Teleport>
|
||||||
</div>
|
)
|
||||||
</div>
|
}
|
||||||
</Teleport>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1,15 +1,8 @@
|
|||||||
let zIndexBase = 1000
|
let zIndexBase = 1000
|
||||||
const modalStack: number[] = [] // 当前打开的 Modal zIndex
|
const modalStack: number[] = [] // 当前打开的 Modal zIndex
|
||||||
const freeZIndex: number[] = [] // 已关闭 Modal 可复用 zIndex
|
|
||||||
|
|
||||||
export function addModal() {
|
export function addModal() {
|
||||||
let zIndex: number
|
const zIndex = ++zIndexBase // 每次直接递增
|
||||||
if (freeZIndex.length > 0) {
|
|
||||||
zIndex = freeZIndex.pop()!
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
zIndex = ++zIndexBase
|
|
||||||
}
|
|
||||||
modalStack.push(zIndex)
|
modalStack.push(zIndex)
|
||||||
return zIndex
|
return zIndex
|
||||||
}
|
}
|
||||||
@@ -17,8 +10,7 @@ export function addModal() {
|
|||||||
export function removeModal(zIndex: number) {
|
export function removeModal(zIndex: number) {
|
||||||
const idx = modalStack.indexOf(zIndex)
|
const idx = modalStack.indexOf(zIndex)
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
modalStack.splice(idx, 1)
|
modalStack.splice(idx, 1) // 只移出 stack,不回收 zIndex
|
||||||
freeZIndex.push(zIndex) // 回收 zIndex
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
import type { PropType } from 'vue'
|
||||||
|
import { computed, defineComponent, onMounted, ref, Teleport, Transition, watch } from 'vue'
|
||||||
|
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||||
|
import Button from '../button/index'
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
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 _visible = ref(props.defaultVisible)
|
||||||
|
const _rendered = ref(props.defaultVisible)
|
||||||
|
const _mounted = ref(props.defaultVisible)
|
||||||
|
|
||||||
|
watch(() => props.visible, (val) => {
|
||||||
|
if (val !== undefined) {
|
||||||
|
_visible.value = val
|
||||||
|
if (val) {
|
||||||
|
_mounted.value = true
|
||||||
|
_rendered.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.appendTo instanceof HTMLElement) {
|
||||||
|
targetEl.value = props.appendTo
|
||||||
|
}
|
||||||
|
else if (typeof props.appendTo === 'string') {
|
||||||
|
const el = document.querySelector(props.appendTo)
|
||||||
|
if (el)
|
||||||
|
targetEl.value = el as HTMLElement
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleMaskClick = () => {
|
||||||
|
if (props.maskClosable) {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = async () => {
|
||||||
|
let allow = true
|
||||||
|
if (props.onBeforeCancel) {
|
||||||
|
try {
|
||||||
|
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 () => {
|
||||||
|
let allow = true
|
||||||
|
if (props.onBeforeConfirm) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBeforeEnter = () => {
|
||||||
|
emit('beforeOpen')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAfterEnter = () => {
|
||||||
|
emit('open')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBeforeLeave = () => {
|
||||||
|
emit('beforeClose')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAfterLeave = () => {
|
||||||
|
_mounted.value = false
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeIconMap: Record<string, string> = {
|
||||||
|
success: 'ix:success-filled',
|
||||||
|
danger: 'ix:error-filled',
|
||||||
|
warning: 'ix:warning-filled',
|
||||||
|
info: 'ix:info-filled',
|
||||||
|
question: 'ix:question-filled',
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const shouldRenderPopup = props.destroyOnClose
|
||||||
|
? _visible.value
|
||||||
|
: _visible.value || _rendered.value
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Teleport to={targetEl.value}>
|
||||||
|
{shouldRenderPopup && (
|
||||||
|
<div v-show={_mounted.value} class={`${prefixCls}-container`} style={{ position: 'fixed', zIndex: 1000 }}>
|
||||||
|
{props.mask && (
|
||||||
|
<Transition name={`${prefixCls}-mask-fade`} appear>
|
||||||
|
<div v-show={_visible.value} class={`${prefixCls}-mask`} onClick={handleMaskClick} />
|
||||||
|
</Transition>
|
||||||
|
)}
|
||||||
|
<Transition
|
||||||
|
name={`${prefixCls}-content-fade`}
|
||||||
|
appear
|
||||||
|
onBeforeEnter={handleBeforeEnter}
|
||||||
|
onAfterEnter={handleAfterEnter}
|
||||||
|
onBeforeLeave={handleBeforeLeave}
|
||||||
|
onAfterLeave={handleAfterLeave}
|
||||||
|
>
|
||||||
|
<div v-show={_visible.value} class={[`${prefixCls}-wrapper`, `${prefixCls}-position-${props.position}`, props.class]} style={{ ...props.style }}>
|
||||||
|
<div class={`${prefixCls}`}>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Teleport>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -21,7 +21,9 @@ export default defineComponent({
|
|||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
beforeConfirm: {
|
beforeConfirm: {
|
||||||
type: Function as PropType<() => boolean | Promise<boolean>>,
|
type: Function as PropType<
|
||||||
|
(done: (ok: boolean) => void) => void | boolean | Promise<boolean>
|
||||||
|
>,
|
||||||
},
|
},
|
||||||
confirmBtn: {
|
confirmBtn: {
|
||||||
type: null,
|
type: null,
|
||||||
@@ -68,11 +70,27 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
confirmLoading.value = true
|
|
||||||
let allow = true
|
let allow = true
|
||||||
|
|
||||||
if (props.beforeConfirm) {
|
if (props.beforeConfirm) {
|
||||||
allow = await Promise.resolve(props.beforeConfirm())
|
confirmLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
allow = await new Promise<boolean>((resolve) => {
|
||||||
|
const result = props.beforeConfirm!(ok => resolve(ok))
|
||||||
|
|
||||||
|
if (typeof result === 'boolean') {
|
||||||
|
resolve(result)
|
||||||
|
}
|
||||||
|
else if (result instanceof Promise) {
|
||||||
|
result.then(ok => resolve(ok)).catch(() => resolve(false))
|
||||||
|
}
|
||||||
|
// 如果是用 done 回调,就等调用方触发 resolve
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
allow = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allow) {
|
if (allow) {
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ import { getPrefixCls } from '../_utils/getPrefixCls'
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'APopup',
|
name: 'APopup',
|
||||||
props: {
|
props: {
|
||||||
visible: { type: Boolean, default: false },
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
trigger: {
|
trigger: {
|
||||||
type: [String, Array] as PropType<
|
type: [String, Array] as PropType<
|
||||||
'hover' | 'click' | 'focus' | 'contextMenu' | Array<'hover' | 'click' | 'focus' | 'contextMenu'>
|
'hover' | 'click' | 'focus' | 'contextMenu' | Array<'hover' | 'click' | 'focus' | 'contextMenu'>
|
||||||
@@ -65,8 +67,14 @@ export default defineComponent({
|
|||||||
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
|
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
showDelay: { type: Number, default: 0 },
|
showDelay: {
|
||||||
hideDelay: { type: Number, default: 0 },
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
hideDelay: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ['update:visible'],
|
emits: ['update:visible'],
|
||||||
setup(props, { slots, emit }) {
|
setup(props, { slots, emit }) {
|
||||||
@@ -75,8 +83,8 @@ export default defineComponent({
|
|||||||
const triggerRef = ref<HTMLElement | null>(null) // 触发 引用
|
const triggerRef = ref<HTMLElement | null>(null) // 触发 引用
|
||||||
const floatingRef = ref<HTMLElement | null>(null) // float 引用
|
const floatingRef = ref<HTMLElement | null>(null) // float 引用
|
||||||
const arrowRef = ref<HTMLElement | null>(null) // 箭头引用
|
const arrowRef = ref<HTMLElement | null>(null) // 箭头引用
|
||||||
const localVisible = ref(props.visible)
|
const _visible = ref(props.visible)
|
||||||
const hasRendered = ref(props.visible)
|
const _rendered = ref(props.visible)
|
||||||
// 定时器
|
// 定时器
|
||||||
let showTimer: number | null = null
|
let showTimer: number | null = null
|
||||||
let hideTimer: number | null = null
|
let hideTimer: number | null = null
|
||||||
@@ -155,12 +163,12 @@ export default defineComponent({
|
|||||||
const showPopup = async () => {
|
const showPopup = async () => {
|
||||||
clearTimers()
|
clearTimers()
|
||||||
showTimer = window.setTimeout(async () => {
|
showTimer = window.setTimeout(async () => {
|
||||||
if (!hasRendered.value)
|
if (!_rendered.value)
|
||||||
hasRendered.value = true
|
_rendered.value = true
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
fitFloatingWidth()
|
fitFloatingWidth()
|
||||||
localVisible.value = true
|
_visible.value = true
|
||||||
setupAutoUpdate()
|
setupAutoUpdate()
|
||||||
emit('update:visible', true)
|
emit('update:visible', true)
|
||||||
}, props.showDelay)
|
}, props.showDelay)
|
||||||
@@ -169,7 +177,7 @@ export default defineComponent({
|
|||||||
const hidePopup = async () => {
|
const hidePopup = async () => {
|
||||||
clearTimers()
|
clearTimers()
|
||||||
hideTimer = window.setTimeout(() => {
|
hideTimer = window.setTimeout(() => {
|
||||||
localVisible.value = false
|
_visible.value = false
|
||||||
clearAutoUpdate()
|
clearAutoUpdate()
|
||||||
emit('update:visible', false)
|
emit('update:visible', false)
|
||||||
}, props.hideDelay)
|
}, props.hideDelay)
|
||||||
@@ -178,15 +186,15 @@ export default defineComponent({
|
|||||||
const togglePopup = async () => {
|
const togglePopup = async () => {
|
||||||
clearTimers()
|
clearTimers()
|
||||||
|
|
||||||
if (!hasRendered.value)
|
if (!_rendered.value)
|
||||||
hasRendered.value = true
|
_rendered.value = true
|
||||||
await nextTick()
|
await nextTick()
|
||||||
fitFloatingWidth()
|
fitFloatingWidth()
|
||||||
|
|
||||||
if (!localVisible.value) {
|
if (!_visible.value) {
|
||||||
// 需要显示
|
// 需要显示
|
||||||
showTimer = window.setTimeout(async () => {
|
showTimer = window.setTimeout(async () => {
|
||||||
localVisible.value = true
|
_visible.value = true
|
||||||
await nextTick()
|
await nextTick()
|
||||||
setupAutoUpdate()
|
setupAutoUpdate()
|
||||||
emit('update:visible', true)
|
emit('update:visible', true)
|
||||||
@@ -195,7 +203,7 @@ export default defineComponent({
|
|||||||
else {
|
else {
|
||||||
// 需要隐藏
|
// 需要隐藏
|
||||||
hideTimer = window.setTimeout(() => {
|
hideTimer = window.setTimeout(() => {
|
||||||
localVisible.value = false
|
_visible.value = false
|
||||||
clearAutoUpdate()
|
clearAutoUpdate()
|
||||||
emit('update:visible', false)
|
emit('update:visible', false)
|
||||||
}, props.hideDelay)
|
}, props.hideDelay)
|
||||||
@@ -230,7 +238,7 @@ export default defineComponent({
|
|||||||
const triggerEl = getDOM(triggerRef.value)
|
const triggerEl = getDOM(triggerRef.value)
|
||||||
const floatingEl = getDOM(floatingRef.value)
|
const floatingEl = getDOM(floatingRef.value)
|
||||||
if (!triggerEl?.contains(target) && !floatingEl?.contains(target)) {
|
if (!triggerEl?.contains(target) && !floatingEl?.contains(target)) {
|
||||||
localVisible.value = false
|
_visible.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +304,7 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(() => props.visible, (val) => {
|
watch(() => props.visible, (val) => {
|
||||||
localVisible.value = val
|
_visible.value = val
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
const renderContent = (content: string | VNode | (() => VNode)) => {
|
const renderContent = (content: string | VNode | (() => VNode)) => {
|
||||||
@@ -310,8 +318,8 @@ export default defineComponent({
|
|||||||
return () => {
|
return () => {
|
||||||
// 懒加载
|
// 懒加载
|
||||||
const shouldRenderPopup = props.destroyOnClose
|
const shouldRenderPopup = props.destroyOnClose
|
||||||
? localVisible.value // 需要销毁时,只在显示时渲染
|
? _visible.value // 需要销毁时,只在显示时渲染
|
||||||
: localVisible.value || hasRendered.value // 不销毁时保持已渲染
|
: _visible.value || _rendered.value // 不销毁时保持已渲染
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -332,7 +340,7 @@ export default defineComponent({
|
|||||||
appear
|
appear
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-show={localVisible.value}
|
v-show={_visible.value}
|
||||||
ref={floatingRef}
|
ref={floatingRef}
|
||||||
class={[prefixCls, props.class]}
|
class={[prefixCls, props.class]}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -31,13 +31,14 @@ export default defineComponent({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
beforeConfirm: {
|
beforeConfirm: {
|
||||||
type: Function as PropType<() => boolean | Promise<boolean>>,
|
type: Function as PropType<
|
||||||
|
(done: (ok: boolean) => void) => void | boolean | Promise<boolean>
|
||||||
|
>,
|
||||||
},
|
},
|
||||||
class: {
|
class: {
|
||||||
type: [String, Array, Object],
|
type: [String, Array, Object],
|
||||||
@@ -70,12 +71,24 @@ export default defineComponent({
|
|||||||
if (props.disabled || isLoading.value)
|
if (props.disabled || isLoading.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
_loading.value = true
|
|
||||||
let allow = true
|
let allow = true
|
||||||
|
|
||||||
if (props.beforeConfirm) {
|
if (props.beforeConfirm) {
|
||||||
|
_loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
allow = await Promise.resolve(props.beforeConfirm())
|
// 用一个统一的函数来兼容三种写法
|
||||||
|
allow = await new Promise<boolean>((resolve) => {
|
||||||
|
const result = props.beforeConfirm!(ok => resolve(ok))
|
||||||
|
|
||||||
|
if (typeof result === 'boolean') {
|
||||||
|
resolve(result)
|
||||||
|
}
|
||||||
|
else if (result instanceof Promise) {
|
||||||
|
result.then(ok => resolve(ok)).catch(() => resolve(false))
|
||||||
|
}
|
||||||
|
// 如果 beforeConfirm 里调用了 done,就等 done 触发
|
||||||
|
})
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
allow = false
|
allow = false
|
||||||
|
|||||||
@@ -12,19 +12,11 @@ const AModal3Visible = ref(false)
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 第 1 个 AModal -->
|
<!-- 第 1 个 AModal -->
|
||||||
<AModal v-model:visible="AModal1Visible" mask-closable>
|
<AModal v-model:visible="AModal1Visible" mask-closable >
|
||||||
<template #title>
|
<template #title>
|
||||||
第 1 个 AModal
|
第 1 个 AModal
|
||||||
</template>
|
</template>
|
||||||
<p>这是第 1 个弹窗内容</p>
|
<p>这是第 1 个弹窗内容</p>
|
||||||
<template #footer>
|
|
||||||
<button @click="AModal2Visible = true">
|
|
||||||
打开第 2 个 AModal
|
|
||||||
</button>
|
|
||||||
<button @click="AModal1Visible = false">
|
|
||||||
关闭
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</AModal>
|
</AModal>
|
||||||
|
|
||||||
<!-- 第 2 个 AModal -->
|
<!-- 第 2 个 AModal -->
|
||||||
@@ -44,7 +36,7 @@ const AModal3Visible = ref(false)
|
|||||||
</AModal>
|
</AModal>
|
||||||
|
|
||||||
<!-- 第 3 个 AModal -->
|
<!-- 第 3 个 AModal -->
|
||||||
<AModal v-model:visible="AModal3Visible" mask-closable>
|
<AModal v-model:visible="AModal3Visible" mask-closable position="top">
|
||||||
<template #title>
|
<template #title>
|
||||||
第 3 个 AModal
|
第 3 个 AModal
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in new issue
Block a user