gaoluyang
2026-06-24 c0cb161bb52ce0fbdce5c66ec391d107c75e2452
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from 'reka-ui';
 
import type { HTMLAttributes } from 'vue';
 
import type { ClassType } from '..\..\..\..\..\base\typings\src';
 
import { computed, inject, ref } from 'vue';
 
import { useScrollLock } from '..\..\..\..\..\composables\src';
import { cn } from '..\..\..\..\..\base\shared\src\utils';
 
import { X } from '@lucide/vue';
import {
  DialogClose,
  DialogContent,
  DialogPortal,
  useForwardPropsEmits,
} from 'reka-ui';
 
defineOptions({
  inheritAttrs: false,
});
 
const props = withDefaults(
  defineProps<
    DialogContentProps & {
      animationType?: 'scale' | 'slide';
      appendTo?: HTMLElement | string;
      class?: HTMLAttributes['class'];
      closeClass?: ClassType;
      closeDisabled?: boolean;
      modal?: boolean;
      open?: boolean;
      overlayBlur?: number;
      showCloseButton?: boolean;
      zIndex?: number;
    }
  >(),
  {
    appendTo: 'body',
    animationType: 'slide',
    closeDisabled: false,
    showCloseButton: true,
  },
);
const emits = defineEmits<
  DialogContentEmits & { close: []; closed: []; opened: [] }
>();
 
const delegatedProps = computed(() => {
  const {
    class: _,
    modal: _modal,
    open: _open,
    showCloseButton: __,
    animationType: ___,
    ...delegated
  } = props;
 
  return delegated;
});
 
function isAppendToBody() {
  return (
    props.appendTo === 'body' ||
    props.appendTo === document.body ||
    !props.appendTo
  );
}
 
const position = computed(() => {
  return isAppendToBody() ? 'fixed' : 'absolute';
});
 
// reka-ui 的 Dialog 在 modal=false 时不会渲染遮罩,这里自行渲染一个遮罩层并锁定滚动,
// 既保留遮罩/滚动锁定能力,又避免 modal=true 时 body 被设置 pointer-events:none 导致
// 弹出层(如 Select 下拉框)无法点击的问题。
useScrollLock();
const dismissableModalId = inject('DISMISSABLE_MODAL_ID', undefined);
 
const forwarded = useForwardPropsEmits(delegatedProps, emits);
 
const contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
function onAnimationEnd(event: AnimationEvent) {
  // 只有在 contentRef 的动画结束时才触发 opened/closed 事件
  if (event.target === contentRef.value?.$el) {
    if (props.open) {
      emits('opened');
    } else {
      emits('closed');
    }
  }
}
defineExpose({
  getContentRef: () => contentRef.value,
});
</script>
 
<template>
  <DialogPortal :to="appendTo">
    <Transition name="fade">
      <div
        v-if="open && modal"
        :data-dismissable-modal="dismissableModalId"
        :style="{
          ...(zIndex ? { zIndex } : {}),
          position,
          backdropFilter:
            overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',
        }"
        :class="cn('z-popup bg-overlay inset-0 fixed')"
        @click="() => emits('close')"
      ></div>
    </Transition>
    <DialogContent
      ref="contentRef"
      :style="{ ...(zIndex ? { zIndex } : {}), position }"
      data-slot="dialog-content"
      @animationend="onAnimationEnd"
      v-bind="forwarded"
      :class="
        cn(
          'z-popup bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 w-full p-6 shadow-lg outline-hidden sm:rounded-xl',
          {
            'data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%]':
              animationType === 'slide',
          },
          props.class,
        )
      "
    >
      <slot></slot>
 
      <DialogClose
        v-if="showCloseButton"
        :disabled="closeDisabled"
        data-slot="dialog-close"
        :class="
          cn(
            'flex-center text-foreground/80 hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-3 right-3 h-6 w-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden disabled:pointer-events-none',
            props.closeClass,
          )
        "
        @click="() => emits('close')"
      >
        <X class="size-4" />
      </DialogClose>
    </DialogContent>
  </DialogPortal>
</template>