2025-12-10 20:52:44 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<Teleport to="body">
|
2025-12-12 16:15:07 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="isOpen"
|
2025-12-17 19:15:08 +08:00
|
|
|
|
class="fixed inset-0 overflow-y-auto pointer-events-none"
|
2025-12-12 16:15:07 +08:00
|
|
|
|
:style="{ zIndex: containerZIndex }"
|
|
|
|
|
|
>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<!-- 背景遮罩 -->
|
|
|
|
|
|
<Transition
|
|
|
|
|
|
enter-active-class="duration-200 ease-out"
|
|
|
|
|
|
enter-from-class="opacity-0"
|
|
|
|
|
|
enter-to-class="opacity-100"
|
|
|
|
|
|
leave-active-class="duration-200 ease-in"
|
|
|
|
|
|
leave-from-class="opacity-100"
|
|
|
|
|
|
leave-to-class="opacity-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="isOpen"
|
2025-12-17 19:15:08 +08:00
|
|
|
|
class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity pointer-events-auto"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
:style="{ zIndex: backdropZIndex }"
|
|
|
|
|
|
@click="handleClose"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
|
2025-12-18 10:57:31 +08:00
|
|
|
|
<div class="relative flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0 pointer-events-none">
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<!-- 对话框内容 -->
|
|
|
|
|
|
<Transition
|
|
|
|
|
|
enter-active-class="duration-300 ease-out"
|
|
|
|
|
|
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
|
|
|
|
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
|
|
|
|
|
|
leave-active-class="duration-200 ease-in"
|
|
|
|
|
|
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
|
|
|
|
|
|
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="isOpen"
|
2025-12-17 19:15:08 +08:00
|
|
|
|
class="relative transform rounded-lg bg-background text-left shadow-2xl transition-all sm:my-8 sm:w-full border border-border pointer-events-auto"
|
2025-12-10 20:52:44 +08:00
|
|
|
|
:style="{ zIndex: contentZIndex }"
|
|
|
|
|
|
:class="maxWidthClass"
|
2025-12-12 16:15:07 +08:00
|
|
|
|
@click.stop
|
2025-12-10 20:52:44 +08:00
|
|
|
|
>
|
|
|
|
|
|
<!-- Header 区域:优先使用 slot,否则使用 title prop -->
|
|
|
|
|
|
<slot name="header">
|
2025-12-12 16:15:07 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="title"
|
|
|
|
|
|
class="border-b border-border px-6 py-4"
|
|
|
|
|
|
>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
<div class="flex items-center gap-3">
|
2025-12-12 16:15:07 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="icon"
|
|
|
|
|
|
class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 flex-shrink-0"
|
|
|
|
|
|
:class="iconClass"
|
|
|
|
|
|
>
|
|
|
|
|
|
<component
|
|
|
|
|
|
:is="icon"
|
|
|
|
|
|
class="h-5 w-5 text-primary"
|
|
|
|
|
|
/>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex-1 min-w-0">
|
2025-12-12 16:15:07 +08:00
|
|
|
|
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
|
|
|
|
|
{{ title }}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p
|
|
|
|
|
|
v-if="description"
|
|
|
|
|
|
class="text-xs text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ description }}
|
|
|
|
|
|
</p>
|
2025-12-10 20:52:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</slot>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 内容区域:统一添加 padding -->
|
|
|
|
|
|
<div class="px-6 py-3">
|
|
|
|
|
|
<slot />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Footer 区域:如果有 footer 插槽,自动添加样式 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="slots.footer"
|
|
|
|
|
|
class="border-t border-border px-6 py-4 bg-muted/10 flex flex-row-reverse gap-3"
|
|
|
|
|
|
>
|
|
|
|
|
|
<slot name="footer" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Teleport>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, useSlots, type Component } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
// Props 定义
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
open?: boolean
|
|
|
|
|
|
modelValue?: boolean
|
|
|
|
|
|
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | '7xl'
|
|
|
|
|
|
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | '7xl'
|
|
|
|
|
|
title?: string
|
|
|
|
|
|
description?: string
|
|
|
|
|
|
icon?: Component // Lucide icon component
|
|
|
|
|
|
iconClass?: string // Custom icon color class
|
|
|
|
|
|
zIndex?: number // Custom z-index for nested dialogs (default: 60)
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
// Emits 定义
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
'update:open': [value: boolean]
|
|
|
|
|
|
'update:modelValue': [value: boolean]
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
// 获取 slots 以便在模板中使用
|
|
|
|
|
|
const slots = useSlots()
|
|
|
|
|
|
|
|
|
|
|
|
// 统一处理 open 状态
|
|
|
|
|
|
const isOpen = computed(() => {
|
|
|
|
|
|
if (props.modelValue === true) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
if (props.open === true) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 统一处理关闭事件
|
|
|
|
|
|
function handleClose() {
|
|
|
|
|
|
if (props.open !== undefined) {
|
|
|
|
|
|
emit('update:open', false)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (props.modelValue !== undefined) {
|
|
|
|
|
|
emit('update:modelValue', false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const maxWidthClass = computed(() => {
|
|
|
|
|
|
const sizeValue = props.maxWidth || props.size || 'md'
|
|
|
|
|
|
const sizes = {
|
|
|
|
|
|
sm: 'sm:max-w-sm',
|
|
|
|
|
|
md: 'sm:max-w-md',
|
|
|
|
|
|
lg: 'sm:max-w-lg',
|
|
|
|
|
|
xl: 'sm:max-w-xl',
|
|
|
|
|
|
'2xl': 'sm:max-w-2xl',
|
|
|
|
|
|
'3xl': 'sm:max-w-3xl',
|
|
|
|
|
|
'4xl': 'sm:max-w-4xl',
|
|
|
|
|
|
'5xl': 'sm:max-w-5xl',
|
|
|
|
|
|
'6xl': 'sm:max-w-6xl',
|
|
|
|
|
|
'7xl': 'sm:max-w-7xl'
|
|
|
|
|
|
}
|
|
|
|
|
|
return sizes[sizeValue]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Z-index computed values for nested dialogs support
|
|
|
|
|
|
const containerZIndex = computed(() => props.zIndex || 60)
|
|
|
|
|
|
const backdropZIndex = computed(() => props.zIndex || 60)
|
|
|
|
|
|
const contentZIndex = computed(() => (props.zIndex || 60) + 10)
|
|
|
|
|
|
</script>
|