mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-07 16:28:49 +08:00
feat: FaModal 和 FaDrawer 分别增加命令式调用API
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import type { Component, HTMLAttributes } from 'vue'
|
||||
import { isVNode } from 'vue'
|
||||
import Drawer from './index.vue'
|
||||
|
||||
export interface DrawerProps {
|
||||
id?: string
|
||||
modelValue?: boolean
|
||||
side?: 'top' | 'bottom' | 'left' | 'right'
|
||||
title?: string
|
||||
description?: string
|
||||
loading?: boolean
|
||||
closable?: boolean
|
||||
centered?: boolean
|
||||
bordered?: boolean
|
||||
overlay?: boolean
|
||||
overlayBlur?: boolean
|
||||
showConfirmButton?: boolean
|
||||
showCancelButton?: boolean
|
||||
confirmButtonText?: string
|
||||
cancelButtonText?: string
|
||||
confirmButtonDisabled?: boolean
|
||||
confirmButtonLoading?: boolean
|
||||
beforeClose?: (
|
||||
action: 'confirm' | 'cancel' | 'close',
|
||||
done: () => void
|
||||
) => void
|
||||
header?: boolean
|
||||
footer?: boolean
|
||||
closeOnClickOverlay?: boolean
|
||||
closeOnPressEscape?: boolean
|
||||
destroyOnClose?: boolean
|
||||
contentClass?: HTMLAttributes['class']
|
||||
headerClass?: HTMLAttributes['class']
|
||||
footerClass?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
export interface DrawerEmits {
|
||||
'update:modelValue': [value: boolean]
|
||||
'open': []
|
||||
'opened': []
|
||||
'close': []
|
||||
'closed': []
|
||||
'confirm': []
|
||||
'cancel': []
|
||||
}
|
||||
|
||||
type BaseOptions = Omit<DrawerProps, 'modelValue'> & {
|
||||
content?: Component | VNode | string
|
||||
onOpen?: () => void
|
||||
onOpened?: () => void
|
||||
onClose?: () => void
|
||||
onClosed?: () => void
|
||||
onConfirm?: () => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
export function useFaDrawer() {
|
||||
function create(initialOptions: BaseOptions) {
|
||||
const container = document.createElement('div')
|
||||
const visible = ref(false)
|
||||
const options = reactive({ ...initialOptions })
|
||||
const app = createApp({
|
||||
render() {
|
||||
return h(Drawer, Object.assign({
|
||||
'id': useId(),
|
||||
'modelValue': visible.value,
|
||||
'onUpdate:modelValue': (val: boolean) => {
|
||||
visible.value = val
|
||||
},
|
||||
}, options), {
|
||||
default: () => {
|
||||
if (typeof options.content === 'string') {
|
||||
return options.content
|
||||
}
|
||||
else if (isVNode(options.content)) {
|
||||
return options.content
|
||||
}
|
||||
else if (options.content) {
|
||||
return h(options.content)
|
||||
}
|
||||
return null
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
// 继承主应用的上下文
|
||||
const instance = getCurrentInstance()
|
||||
if (instance && instance.appContext) {
|
||||
Object.assign(app._context, instance.appContext)
|
||||
}
|
||||
app.mount(container)
|
||||
const open = () => {
|
||||
visible.value = true
|
||||
}
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
}
|
||||
const update = (newOptions: BaseOptions) => {
|
||||
Object.assign(options, newOptions)
|
||||
}
|
||||
return {
|
||||
open,
|
||||
close,
|
||||
update,
|
||||
}
|
||||
}
|
||||
return {
|
||||
create,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { DrawerEmits, DrawerProps } from '.'
|
||||
import { cn } from '@/utils'
|
||||
import {
|
||||
Sheet,
|
||||
@@ -15,32 +15,7 @@ defineOptions({
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: boolean
|
||||
side?: 'top' | 'bottom' | 'left' | 'right'
|
||||
title: string
|
||||
description?: string
|
||||
loading?: boolean
|
||||
closable?: boolean
|
||||
centered?: boolean
|
||||
bordered?: boolean
|
||||
overlay?: boolean
|
||||
overlayBlur?: boolean
|
||||
showConfirmButton?: boolean
|
||||
showCancelButton?: boolean
|
||||
confirmButtonText?: string
|
||||
cancelButtonText?: string
|
||||
confirmButtonDisabled?: boolean
|
||||
confirmButtonLoading?: boolean
|
||||
header?: boolean
|
||||
footer?: boolean
|
||||
closeOnClickOverlay?: boolean
|
||||
closeOnPressEscape?: boolean
|
||||
destroyOnClose?: boolean
|
||||
contentClass?: HTMLAttributes['class']
|
||||
headerClass?: HTMLAttributes['class']
|
||||
footerClass?: HTMLAttributes['class']
|
||||
}>(),
|
||||
defineProps<DrawerProps>(),
|
||||
{
|
||||
modelValue: false,
|
||||
side: 'right',
|
||||
@@ -64,19 +39,9 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'open': []
|
||||
'opened': []
|
||||
'close': []
|
||||
'closed': []
|
||||
'confirm': []
|
||||
'cancel': []
|
||||
}>()
|
||||
|
||||
const id = useId()
|
||||
provide('DrawerId', id)
|
||||
const emits = defineEmits<DrawerEmits>()
|
||||
|
||||
const drawerId = shallowRef(props.id ?? useId())
|
||||
const isOpen = ref(props.modelValue)
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
@@ -86,9 +51,16 @@ watch(() => props.modelValue, (newValue) => {
|
||||
const hasOpened = ref(false)
|
||||
const isClosed = ref(true)
|
||||
|
||||
watch(() => isOpen.value, (value) => {
|
||||
watch(isOpen, (val) => {
|
||||
emits('update:modelValue', val)
|
||||
if (val) {
|
||||
emits('open')
|
||||
}
|
||||
else {
|
||||
emits('close')
|
||||
}
|
||||
isClosed.value = false
|
||||
if (value && !hasOpened.value) {
|
||||
if (val && !hasOpened.value) {
|
||||
hasOpened.value = true
|
||||
}
|
||||
}, {
|
||||
@@ -97,25 +69,62 @@ watch(() => isOpen.value, (value) => {
|
||||
|
||||
const forceMount = computed(() => !props.destroyOnClose && hasOpened.value)
|
||||
|
||||
function updateOpen(value: boolean) {
|
||||
isOpen.value = value
|
||||
emits('update:modelValue', value)
|
||||
async function updateOpen(value: boolean) {
|
||||
if (value) {
|
||||
isOpen.value = value
|
||||
emits('open')
|
||||
}
|
||||
else {
|
||||
emits('close')
|
||||
if (props.beforeClose) {
|
||||
await props.beforeClose(
|
||||
'close',
|
||||
() => {
|
||||
isOpen.value = value
|
||||
emits('close')
|
||||
},
|
||||
)
|
||||
}
|
||||
else {
|
||||
isOpen.value = value
|
||||
emits('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
updateOpen(false)
|
||||
emits('confirm')
|
||||
const isConfirmButtonLoading = ref(false)
|
||||
|
||||
async function onConfirm() {
|
||||
if (props.beforeClose) {
|
||||
isConfirmButtonLoading.value = true
|
||||
await props.beforeClose(
|
||||
'confirm',
|
||||
() => {
|
||||
isOpen.value = false
|
||||
emits('confirm')
|
||||
},
|
||||
)
|
||||
isConfirmButtonLoading.value = false
|
||||
}
|
||||
else {
|
||||
isOpen.value = false
|
||||
emits('confirm')
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
updateOpen(false)
|
||||
emits('cancel')
|
||||
async function onCancel() {
|
||||
if (props.beforeClose) {
|
||||
await props.beforeClose(
|
||||
'cancel',
|
||||
() => {
|
||||
isOpen.value = false
|
||||
emits('cancel')
|
||||
},
|
||||
)
|
||||
}
|
||||
else {
|
||||
isOpen.value = false
|
||||
emits('cancel')
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocusOutside(e: Event) {
|
||||
@@ -124,7 +133,7 @@ function handleFocusOutside(e: Event) {
|
||||
}
|
||||
|
||||
function handleClickOutside(e: Event) {
|
||||
if (!props.closeOnClickOverlay || (e.target as HTMLElement).dataset.drawerId !== id) {
|
||||
if (!props.closeOnClickOverlay || (e.target as HTMLElement).dataset.drawerId !== drawerId.value) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
@@ -151,8 +160,9 @@ function handleAnimationEnd() {
|
||||
<template>
|
||||
<Sheet :modal="false" :open="isOpen" @update:open="updateOpen">
|
||||
<SheetContent
|
||||
:closable="props.closable"
|
||||
:drawer-id="drawerId"
|
||||
:open="isOpen"
|
||||
:closable="props.closable"
|
||||
:overlay="props.overlay"
|
||||
:overlay-blur="props.overlayBlur"
|
||||
:class="cn('w-full flex flex-col gap-0 p-0', props.contentClass, {
|
||||
@@ -187,10 +197,10 @@ function handleAnimationEnd() {
|
||||
<div class="p-4">
|
||||
<slot />
|
||||
</div>
|
||||
<div v-show="props.loading" class="absolute inset-0 z-1000 size-full flex-center bg-popover/75">
|
||||
<FaIcon name="i-line-md:loading-twotone-loop" class="size-10" />
|
||||
</div>
|
||||
</FaScrollArea>
|
||||
<div v-show="props.loading" class="absolute inset-0 z-1000 size-full flex-center bg-popover/75">
|
||||
<FaIcon name="i-line-md:loading-twotone-loop" class="size-10" />
|
||||
</div>
|
||||
</div>
|
||||
<SheetFooter
|
||||
v-if="footer" :class="cn('p-2 gap-y-2', props.footerClass, {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { cn } from '@/utils'
|
||||
import { sheetVariants } from '.'
|
||||
|
||||
interface SheetContentProps extends DialogContentProps {
|
||||
drawerId: string
|
||||
class?: HTMLAttributes['class']
|
||||
open?: boolean
|
||||
side?: SheetVariants['side']
|
||||
@@ -51,8 +52,6 @@ watch(showOverlay, (val) => {
|
||||
isLocked.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const id = inject('DrawerId')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -70,7 +69,7 @@ const id = inject('DrawerId')
|
||||
>
|
||||
<div
|
||||
v-if="showOverlay"
|
||||
:data-drawer-id="id"
|
||||
:data-drawer-id="props.drawerId"
|
||||
:class="cn('fixed inset-0 z-2000 data-[state=closed]:animate-out data-[state=open]:animate-in bg-black/50 data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0', {
|
||||
'backdrop-blur-sm': props.overlayBlur,
|
||||
})"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { Component, HTMLAttributes } from 'vue'
|
||||
import { isVNode } from 'vue'
|
||||
import Modal from './index.vue'
|
||||
|
||||
export interface ModalProps {
|
||||
id?: string
|
||||
modelValue?: boolean
|
||||
title?: string
|
||||
description?: string
|
||||
@@ -46,125 +48,142 @@ export interface ModalEmits {
|
||||
'cancel': []
|
||||
}
|
||||
|
||||
type alertOptions = Pick<ModalProps, 'title' | 'description' | 'icon' | 'alignCenter' | 'overlay' | 'overlayBlur' | 'confirmButtonText' | 'confirmButtonDisabled' | 'confirmButtonLoading' | 'closeOnClickOverlay' | 'closeOnPressEscape' | 'class' | 'headerClass' | 'contentClass' | 'footerClass'> & {
|
||||
content: string
|
||||
onConfirm?: () => void
|
||||
}
|
||||
|
||||
type confirmOptions = Pick<ModalProps, 'title' | 'description' | 'alignCenter' | 'overlay' | 'overlayBlur' | 'confirmButtonText' | 'cancelButtonText' | 'confirmButtonDisabled' | 'confirmButtonLoading' | 'beforeClose' | 'closeOnClickOverlay' | 'closeOnPressEscape' | 'class' | 'headerClass' | 'contentClass' | 'footerClass'> & {
|
||||
content: string
|
||||
type BaseOptions = Omit<ModalProps, 'modelValue'> & {
|
||||
content?: Component | VNode | string
|
||||
onOpen?: () => void
|
||||
onOpened?: () => void
|
||||
onClose?: () => void
|
||||
onClosed?: () => void
|
||||
onConfirm?: () => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
type alertOptions = Pick<BaseOptions, 'title' | 'description' | 'icon' | 'alignCenter' | 'overlay' | 'overlayBlur' | 'confirmButtonText' | 'confirmButtonDisabled' | 'confirmButtonLoading' | 'closeOnClickOverlay' | 'closeOnPressEscape' | 'class' | 'headerClass' | 'contentClass' | 'footerClass' | 'content' | 'onConfirm'>
|
||||
|
||||
type confirmOptions = Pick<BaseOptions, 'title' | 'description' | 'alignCenter' | 'overlay' | 'overlayBlur' | 'confirmButtonText' | 'cancelButtonText' | 'confirmButtonDisabled' | 'confirmButtonLoading' | 'beforeClose' | 'closeOnClickOverlay' | 'closeOnPressEscape' | 'class' | 'headerClass' | 'contentClass' | 'footerClass' | 'content' | 'onConfirm' | 'onCancel'>
|
||||
|
||||
export function useFaModal() {
|
||||
function info(options: alertOptions) {
|
||||
function create(initialOptions: BaseOptions) {
|
||||
const container = document.createElement('div')
|
||||
const visible = ref(false)
|
||||
const options = reactive({ ...initialOptions })
|
||||
const app = createApp({
|
||||
render() {
|
||||
return h(Modal, Object.assign({
|
||||
modelValue: true,
|
||||
icon: 'info',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
onClosed: () => {
|
||||
app.unmount()
|
||||
'id': useId(),
|
||||
'modelValue': visible.value,
|
||||
'onUpdate:modelValue': (val: boolean) => {
|
||||
visible.value = val
|
||||
},
|
||||
}, options), () => options.content)
|
||||
}, options), {
|
||||
default: () => {
|
||||
if (typeof options.content === 'string') {
|
||||
return options.content
|
||||
}
|
||||
else if (isVNode(options.content)) {
|
||||
return options.content
|
||||
}
|
||||
else if (options.content) {
|
||||
return h(options.content)
|
||||
}
|
||||
return null
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
// 继承主应用的上下文
|
||||
const instance = getCurrentInstance()
|
||||
if (instance && instance.appContext) {
|
||||
Object.assign(app._context, instance.appContext)
|
||||
}
|
||||
app.mount(container)
|
||||
const open = () => {
|
||||
visible.value = true
|
||||
}
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
}
|
||||
const update = (newOptions: BaseOptions) => {
|
||||
Object.assign(options, newOptions)
|
||||
}
|
||||
return {
|
||||
open,
|
||||
close,
|
||||
update,
|
||||
}
|
||||
}
|
||||
function info(options: alertOptions) {
|
||||
const defaultOptions: BaseOptions = {
|
||||
icon: 'info',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
destroyOnClose: true,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
}
|
||||
const { open } = create(Object.assign(defaultOptions, options))
|
||||
open()
|
||||
}
|
||||
function success(options: alertOptions) {
|
||||
const container = document.createElement('div')
|
||||
const app = createApp({
|
||||
render() {
|
||||
return h(Modal, Object.assign({
|
||||
modelValue: true,
|
||||
icon: 'success',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
onClosed: () => {
|
||||
app.unmount()
|
||||
},
|
||||
}, options), () => options.content)
|
||||
},
|
||||
})
|
||||
app.mount(container)
|
||||
const defaultOptions: BaseOptions = {
|
||||
icon: 'success',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
destroyOnClose: true,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
}
|
||||
const { open } = create(Object.assign(defaultOptions, options))
|
||||
open()
|
||||
}
|
||||
function warning(options: alertOptions) {
|
||||
const container = document.createElement('div')
|
||||
const app = createApp({
|
||||
render() {
|
||||
return h(Modal, Object.assign({
|
||||
modelValue: true,
|
||||
icon: 'warning',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
onClosed: () => {
|
||||
app.unmount()
|
||||
},
|
||||
}, options), () => options.content)
|
||||
},
|
||||
})
|
||||
app.mount(container)
|
||||
const defaultOptions: BaseOptions = {
|
||||
icon: 'warning',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
destroyOnClose: true,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
}
|
||||
const { open } = create(Object.assign(defaultOptions, options))
|
||||
open()
|
||||
}
|
||||
function error(options: alertOptions) {
|
||||
const container = document.createElement('div')
|
||||
const app = createApp({
|
||||
render() {
|
||||
return h(Modal, Object.assign({
|
||||
modelValue: true,
|
||||
icon: 'error',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
onClosed: () => {
|
||||
app.unmount()
|
||||
},
|
||||
}, options), () => options.content)
|
||||
},
|
||||
})
|
||||
app.mount(container)
|
||||
const defaultOptions: BaseOptions = {
|
||||
icon: 'error',
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
destroyOnClose: true,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
}
|
||||
const { open } = create(Object.assign(defaultOptions, options))
|
||||
open()
|
||||
}
|
||||
function confirm(options: confirmOptions) {
|
||||
const container = document.createElement('div')
|
||||
const app = createApp({
|
||||
render() {
|
||||
return h(Modal, Object.assign({
|
||||
modelValue: true,
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
closeOnClickOverlay: false,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
showCancelButton: true,
|
||||
onClosed: () => {
|
||||
app.unmount()
|
||||
},
|
||||
}, options), () => options.content)
|
||||
},
|
||||
})
|
||||
app.mount(container)
|
||||
const defaultOptions: BaseOptions = {
|
||||
closable: false,
|
||||
border: false,
|
||||
alignCenter: true,
|
||||
showCancelButton: true,
|
||||
closeOnClickOverlay: false,
|
||||
destroyOnClose: true,
|
||||
contentClass: 'py-0 min-h-auto',
|
||||
footerClass: 'p-4',
|
||||
}
|
||||
const { open } = create(Object.assign(defaultOptions, options))
|
||||
open()
|
||||
}
|
||||
|
||||
return {
|
||||
create,
|
||||
info,
|
||||
success,
|
||||
warning,
|
||||
|
||||
@@ -61,7 +61,7 @@ defineExpose({
|
||||
areaRef: dialogAreaRef,
|
||||
})
|
||||
|
||||
const modalId = useId()
|
||||
const modalId = shallowRef(props.id ?? useId())
|
||||
const isOpen = ref(props.modelValue)
|
||||
const isMaximize = ref(props.maximize)
|
||||
|
||||
@@ -72,9 +72,16 @@ watch(() => props.modelValue, (newValue) => {
|
||||
const hasOpened = ref(false)
|
||||
const isClosed = ref(true)
|
||||
|
||||
watch(() => isOpen.value, (value) => {
|
||||
watch(isOpen, (val) => {
|
||||
emits('update:modelValue', val)
|
||||
if (val) {
|
||||
emits('open')
|
||||
}
|
||||
else {
|
||||
emits('close')
|
||||
}
|
||||
isClosed.value = false
|
||||
if (value && !hasOpened.value) {
|
||||
if (val && !hasOpened.value) {
|
||||
hasOpened.value = true
|
||||
}
|
||||
}, {
|
||||
@@ -98,7 +105,6 @@ function setTransform() {
|
||||
}
|
||||
|
||||
watch(isOpen, (val) => {
|
||||
emits('update:modelValue', val)
|
||||
if (val) {
|
||||
nextTick(() => {
|
||||
if (dialogContentRef.value) {
|
||||
@@ -106,10 +112,6 @@ watch(isOpen, (val) => {
|
||||
setTransform()
|
||||
}
|
||||
})
|
||||
emits('open')
|
||||
}
|
||||
else {
|
||||
emits('close')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -177,7 +179,7 @@ function handleFocusOutside(e: Event) {
|
||||
}
|
||||
|
||||
function handleClickOutside(e: Event) {
|
||||
if (!props.closeOnClickOverlay || (e.target as HTMLElement).dataset.modalId !== modalId) {
|
||||
if (!props.closeOnClickOverlay || (e.target as HTMLElement).dataset.modalId !== modalId.value) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user