starmusic-pc
1<script setup>
2/**
3 * UiDialog — 通用对话框
4 *
5 * 居中弹窗,支持自定义正文与底部按钮;Esc 关闭、点击遮罩关闭(可通过 maskClosable 控制)。
6 * 与 uni 兼容层的 showModal 不同,这个用于承载复杂表单(新建歌单、导入歌单等)。
7 */
8import { onMounted, onBeforeUnmount, watch } from 'vue'
9import Icon from './Icon.vue'
10
11const props = defineProps({
12 visible: { type: Boolean, default: false },
13 title: { type: String, default: '' },
14 subtitle: { type: String, default: '' },
15 width: { type: [Number, String], default: 460 },
16 maskClosable: { type: Boolean, default: true },
17 /** 关闭前拦截(返回 false 可阻止关闭,例如导入中) */
18 beforeClose: { type: Function, default: null },
19})
20
21const emit = defineEmits(['update:visible', 'close'])
22
23const requestClose = () => {
24 if (props.beforeClose && props.beforeClose() === false) return
25 emit('update:visible', false)
26 emit('close')
27}
28
29const onKey = (e) => {
30 if (!props.visible) return
31 if (e.key === 'Escape') {
32 e.stopPropagation()
33 requestClose()
34 }
35}
36
37onMounted(() => document.addEventListener('keydown', onKey))
38onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
39
40watch(
41 () => props.visible,
42 () => {
43 /* 供外部观察,无需额外处理 */
44 },
45)
46</script>
47
48<template>
49 <Teleport to="body">
50 <Transition name="dlg">
51 <div v-if="visible" class="dlg-mask" @click.self="maskClosable && requestClose()">
52 <div class="dlg" :style="{ width: typeof width === 'number' ? `${width}px` : width }">
53 <header class="dlg-head">
54 <div class="dlg-titles">
55 <h3 class="dlg-title">{{ title }}</h3>
56 <p v-if="subtitle" class="dlg-sub">{{ subtitle }}</p>
57 </div>
58 <button class="dlg-x" title="关闭" @click="requestClose">
59 <Icon name="close" :size="16" />
60 </button>
61 </header>
62
63 <div class="dlg-body scroll-y">
64 <slot />
65 </div>
66
67 <footer v-if="$slots.footer" class="dlg-foot">
68 <slot name="footer" />
69 </footer>
70 </div>
71 </div>
72 </Transition>
73 </Teleport>
74</template>
75
76<style scoped>
77.dlg-mask {
78 position: fixed;
79 inset: 0;
80 z-index: var(--z-modal);
81 display: grid;
82 place-items: center;
83 background: var(--surface-overlay);
84 backdrop-filter: blur(3px);
85 -webkit-backdrop-filter: blur(3px);
86 animation: kMaskIn var(--t-base) ease both;
87 padding: 24px;
88}
89
90.dlg {
91 max-width: calc(100vw - 48px);
92 max-height: calc(100vh - 64px);
93 display: flex;
94 flex-direction: column;
95 border-radius: var(--r-xl);
96 /* 弹窗用不透明底色:背景图模式下 --surface-1 半透明,遮罩下的内容会透上来 */
97 background: var(--surface-pop);
98 box-shadow: var(--sh-xl);
99 overflow: hidden;
100 animation: kPopIn var(--t-slow) var(--ease-overshoot) both;
101}
102
103.dlg-head {
104 flex: none;
105 display: flex;
106 align-items: flex-start;
107 justify-content: space-between;
108 gap: 12px;
109 padding: 20px 20px 14px;
110}
111
112.dlg-titles {
113 min-width: 0;
114}
115
116.dlg-title {
117 font-size: 16px;
118 font-weight: 600;
119 color: var(--text-1);
120}
121
122.dlg-sub {
123 margin-top: 4px;
124 font-size: 12px;
125 color: var(--text-4);
126 line-height: 1.6;
127}
128
129.dlg-x {
130 flex: none;
131 display: grid;
132 place-items: center;
133 width: 28px;
134 height: 28px;
135 border-radius: var(--r-sm);
136 color: var(--text-4);
137 transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
138}
139
140.dlg-x:hover {
141 background: var(--surface-3);
142 color: var(--text-1);
143}
144
145.dlg-body {
146 flex: 1;
147 min-height: 0;
148 padding: 0 20px 6px;
149}
150
151.dlg-foot {
152 flex: none;
153 display: flex;
154 justify-content: flex-end;
155 gap: 9px;
156 padding: 16px 20px 18px;
157}
158
159.dlg-enter-active,
160.dlg-leave-active {
161 transition: opacity var(--t-base) var(--ease-out);
162}
163
164.dlg-enter-from,
165.dlg-leave-to {
166 opacity: 0;
167}
168</style>