mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-07 14:08:09 +08:00
244 lines
5.5 KiB
Vue
244 lines
5.5 KiB
Vue
<script setup lang="ts">
|
|
import type { DrawerEmits, DrawerProps } from '.'
|
|
import { VisuallyHidden } from 'reka-ui'
|
|
import { cn } from '@/utils'
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetDescription,
|
|
SheetFooter,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from './sheet'
|
|
|
|
defineOptions({
|
|
name: 'FaDrawer',
|
|
})
|
|
|
|
const props = withDefaults(
|
|
defineProps<DrawerProps>(),
|
|
{
|
|
modelValue: false,
|
|
zIndex: 2000,
|
|
side: 'right',
|
|
loading: false,
|
|
closable: true,
|
|
centered: false,
|
|
bordered: true,
|
|
overlay: true,
|
|
overlayBlur: false,
|
|
showConfirmButton: true,
|
|
showCancelButton: false,
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
confirmButtonDisabled: false,
|
|
confirmButtonLoading: false,
|
|
header: true,
|
|
footer: true,
|
|
closeOnClickOverlay: true,
|
|
closeOnPressEscape: true,
|
|
destroyOnClose: true,
|
|
openAutoFocus: false,
|
|
},
|
|
)
|
|
|
|
const emits = defineEmits<DrawerEmits>()
|
|
|
|
const slots = defineSlots<{
|
|
header?: () => VNode
|
|
default?: () => VNode
|
|
footer?: () => VNode
|
|
}>()
|
|
|
|
const drawerId = shallowRef(props.id ?? useId())
|
|
const isOpen = ref(props.modelValue)
|
|
|
|
watch(() => props.modelValue, (newValue) => {
|
|
isOpen.value = newValue
|
|
})
|
|
|
|
const hasOpened = ref(false)
|
|
const isClosed = ref(true)
|
|
|
|
watch(isOpen, (val) => {
|
|
emits('update:modelValue', val)
|
|
if (val) {
|
|
emits('open')
|
|
}
|
|
else {
|
|
emits('close')
|
|
}
|
|
isClosed.value = false
|
|
if (val && !hasOpened.value) {
|
|
hasOpened.value = true
|
|
}
|
|
}, {
|
|
immediate: true,
|
|
})
|
|
|
|
const forceMount = computed(() => !props.destroyOnClose && hasOpened.value)
|
|
|
|
async function updateOpen(value: boolean) {
|
|
if (value) {
|
|
isOpen.value = value
|
|
emits('open')
|
|
}
|
|
else {
|
|
if (props.beforeClose) {
|
|
await props.beforeClose(
|
|
'close',
|
|
() => {
|
|
isOpen.value = value
|
|
emits('close')
|
|
},
|
|
)
|
|
}
|
|
else {
|
|
isOpen.value = value
|
|
emits('close')
|
|
}
|
|
}
|
|
}
|
|
|
|
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')
|
|
}
|
|
}
|
|
|
|
async function onCancel() {
|
|
if (props.beforeClose) {
|
|
await props.beforeClose(
|
|
'cancel',
|
|
() => {
|
|
isOpen.value = false
|
|
emits('cancel')
|
|
},
|
|
)
|
|
}
|
|
else {
|
|
isOpen.value = false
|
|
emits('cancel')
|
|
}
|
|
}
|
|
|
|
function handleOpenAutoFocus(e: Event) {
|
|
if (!props.openAutoFocus) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
}
|
|
}
|
|
|
|
function handleFocusOutside(e: Event) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
}
|
|
|
|
function handleClickOutside(e: Event) {
|
|
if (!props.closeOnClickOverlay || (e.target as HTMLElement).dataset.drawerId !== drawerId.value) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
}
|
|
}
|
|
|
|
function handleEscapeKeyDown(e: KeyboardEvent) {
|
|
if (!props.closeOnPressEscape) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
}
|
|
}
|
|
|
|
function handleAnimationEnd() {
|
|
if (isOpen.value) {
|
|
emits('opened')
|
|
}
|
|
else {
|
|
emits('closed')
|
|
isClosed.value = true
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Sheet :modal="false" :open="isOpen" @update:open="updateOpen">
|
|
<SheetContent
|
|
:drawer-id="drawerId"
|
|
:open="isOpen"
|
|
:z-index="props.zIndex"
|
|
:closable="props.closable"
|
|
:overlay="props.overlay"
|
|
:overlay-blur="props.overlayBlur"
|
|
:class="cn('w-full flex flex-col gap-0 p-0', props.contentClass, {
|
|
hidden: isClosed,
|
|
})"
|
|
:side="props.side"
|
|
:force-mount="forceMount"
|
|
@open-auto-focus="handleOpenAutoFocus"
|
|
@close-auto-focus="handleFocusOutside"
|
|
@focus-outside="handleFocusOutside"
|
|
@pointer-down-outside="handleClickOutside"
|
|
@interact-outside="handleClickOutside"
|
|
@escape-key-down="handleEscapeKeyDown"
|
|
@animation-end="handleAnimationEnd"
|
|
>
|
|
<SheetHeader
|
|
v-if="header" :class="cn('p-4 gap-y-1', props.headerClass, {
|
|
'border-b': props.bordered,
|
|
})"
|
|
>
|
|
<VisuallyHidden v-if="!!slots.header">
|
|
<SheetTitle />
|
|
<SheetDescription />
|
|
</VisuallyHidden>
|
|
<slot name="header">
|
|
<SheetTitle :class="{ 'text-center': props.centered }">
|
|
{{ title }}
|
|
</SheetTitle>
|
|
<SheetDescription class="empty:hidden" :class="{ 'text-center': props.centered }">
|
|
{{ description }}
|
|
</SheetDescription>
|
|
</slot>
|
|
</SheetHeader>
|
|
<VisuallyHidden v-else>
|
|
<SheetTitle />
|
|
<SheetDescription />
|
|
</VisuallyHidden>
|
|
<div v-if="!!slots.default" class="relative m-0 flex-1 overflow-y-auto 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>
|
|
<SheetFooter
|
|
v-if="footer" :class="cn('p-3 gap-y-2', props.footerClass, {
|
|
'sm:justify-center': props.centered,
|
|
'border-t': props.bordered,
|
|
})"
|
|
>
|
|
<slot name="footer">
|
|
<FaButton v-if="showCancelButton" variant="outline" @click="onCancel">
|
|
{{ cancelButtonText }}
|
|
</FaButton>
|
|
<FaButton v-if="showConfirmButton" :disabled="confirmButtonDisabled" :loading="confirmButtonLoading" @click="onConfirm">
|
|
{{ confirmButtonText }}
|
|
</FaButton>
|
|
</slot>
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
</template>
|