feat(front): implement drawer component with customizable slots and state management for improved UI interactions

This commit is contained in:
keven1024
2025-04-14 23:01:07 +08:00
parent 07efc20ea2
commit a2c852064c
11 changed files with 232 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import {
Drawer,
DrawerContent,
} from '@/components/ui/drawer'
import { createVNode } from 'vue'
import useStore from '@/composables/useStore'
const store = useStore('drawer')
const drawer = computed(() => store?._get('drawer'))
const currentDrawer = computed(() => drawer?.value?.[drawer?.value?.length - 1])
const render = computed(() => currentDrawer?.value?.render)
const Children = () =>
createVNode(render.value, {
hide: () => store?._set('drawer', drawer?.value?.slice(0, -1)),
})
</script>
<template>
<Drawer :open="!!store?._get('drawer')?.[store?._get('drawer')?.length - 1]" @update:open="(open) => {
if (!open && drawer?.length > 0) {
store?._set('drawer', drawer?.slice(0, -1))
}
}">
<DrawerContent>
<div class="mx-auto w-full max-w-sm pb-10">
<Children />
</div>
</DrawerContent>
</Drawer>
</template>

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
import type { DrawerRootEmits, DrawerRootProps } from 'vaul-vue'
import { useForwardPropsEmits } from 'reka-ui'
import { DrawerRoot } from 'vaul-vue'
const props = withDefaults(defineProps<DrawerRootProps>(), {
shouldScaleBackground: true,
})
const emits = defineEmits<DrawerRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DrawerRoot
data-slot="drawer"
v-bind="forwarded"
>
<slot />
</DrawerRoot>
</template>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
import type { DrawerCloseProps } from 'vaul-vue'
import { DrawerClose } from 'vaul-vue'
const props = defineProps<DrawerCloseProps>()
</script>
<template>
<DrawerClose
data-slot="drawer-close"
v-bind="props"
>
<slot />
</DrawerClose>
</template>

View File

@@ -0,0 +1,34 @@
<script lang="ts" setup>
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { useForwardPropsEmits } from 'reka-ui'
import { DrawerContent, DrawerPortal } from 'vaul-vue'
import DrawerOverlay from './DrawerOverlay.vue'
const props = defineProps<DialogContentProps & { class?: HtmlHTMLAttributes['class'] }>()
const emits = defineEmits<DialogContentEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DrawerPortal>
<DrawerOverlay />
<DrawerContent
data-slot="drawer-content"
v-bind="forwarded"
:class="cn(
`group/drawer-content bg-background fixed z-50 flex h-auto flex-col`,
`data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg`,
`data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg`,
`data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:sm:max-w-sm`,
`data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm`,
props.class,
)"
>
<div class="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
<slot />
</DrawerContent>
</DrawerPortal>
</template>

View File

@@ -0,0 +1,24 @@
<script lang="ts" setup>
import type { DrawerDescriptionProps } from 'vaul-vue'
import { cn } from '@/lib/utils'
import { DrawerDescription } from 'vaul-vue'
import { computed, type HtmlHTMLAttributes } from 'vue'
const props = defineProps<DrawerDescriptionProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerDescription
data-slot="drawer-description"
v-bind="delegatedProps"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</DrawerDescription>
</template>

View File

@@ -0,0 +1,17 @@
<script lang="ts" setup>
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HtmlHTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="drawer-footer"
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script lang="ts" setup>
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HtmlHTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="drawer-header"
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
import type { DialogOverlayProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { DrawerOverlay } from 'vaul-vue'
import { computed, type HtmlHTMLAttributes } from 'vue'
const props = defineProps<DialogOverlayProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerOverlay
data-slot="drawer-overlay"
v-bind="delegatedProps"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
/>
</template>

View File

@@ -0,0 +1,24 @@
<script lang="ts" setup>
import type { DrawerTitleProps } from 'vaul-vue'
import { cn } from '@/lib/utils'
import { DrawerTitle } from 'vaul-vue'
import { computed, type HtmlHTMLAttributes } from 'vue'
const props = defineProps<DrawerTitleProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DrawerTitle
data-slot="drawer-title"
v-bind="delegatedProps"
:class="cn('text-foreground font-semibold', props.class)"
>
<slot />
</DrawerTitle>
</template>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
import type { DrawerTriggerProps } from 'vaul-vue'
import { DrawerTrigger } from 'vaul-vue'
const props = defineProps<DrawerTriggerProps>()
</script>
<template>
<DrawerTrigger
data-slot="drawer-trigger"
v-bind="props"
>
<slot />
</DrawerTrigger>
</template>

View File

@@ -0,0 +1,9 @@
export { default as Drawer } from './Drawer.vue'
export { default as DrawerClose } from './DrawerClose.vue'
export { default as DrawerContent } from './DrawerContent.vue'
export { default as DrawerDescription } from './DrawerDescription.vue'
export { default as DrawerFooter } from './DrawerFooter.vue'
export { default as DrawerHeader } from './DrawerHeader.vue'
export { default as DrawerOverlay } from './DrawerOverlay.vue'
export { default as DrawerTitle } from './DrawerTitle.vue'
export { default as DrawerTrigger } from './DrawerTrigger.vue'