@xlui/xux-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,360 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <Transition
4
+ enter-active-class="transition-opacity duration-300"
5
+ enter-from-class="opacity-0"
6
+ enter-to-class="opacity-100"
7
+ leave-active-class="transition-opacity duration-300"
8
+ leave-from-class="opacity-100"
9
+ leave-to-class="opacity-0"
10
+ >
11
+ <div
12
+ v-if="visible"
13
+ class="modal-mask"
14
+ @click="handleMaskClick"
15
+ >
16
+ <Transition
17
+ enter-active-class="transition-all duration-300"
18
+ enter-from-class="opacity-0 scale-95"
19
+ enter-to-class="opacity-100 scale-100"
20
+ leave-active-class="transition-all duration-300"
21
+ leave-from-class="opacity-100 scale-100"
22
+ leave-to-class="opacity-0 scale-95"
23
+ >
24
+ <div
25
+ v-if="visible"
26
+ class="modal-container"
27
+ :class="[
28
+ `modal-container--${size}`,
29
+ { 'modal-container--fullscreen': fullscreen }
30
+ ]"
31
+ :style="{ width: width }"
32
+ @click.stop
33
+ >
34
+ <!-- 头部 -->
35
+ <div v-if="showHeader" class="modal-header">
36
+ <slot name="header">
37
+ <h3 class="modal-title">{{ title }}</h3>
38
+ </slot>
39
+ <button
40
+ v-if="showClose"
41
+ class="modal-close"
42
+ @click="handleClose"
43
+ aria-label="关闭"
44
+ >
45
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
46
+ <path d="M18 6L6 18M6 6l12 12" />
47
+ </svg>
48
+ </button>
49
+ </div>
50
+
51
+ <!-- 内容 -->
52
+ <div class="modal-body">
53
+ <slot></slot>
54
+ </div>
55
+
56
+ <!-- 底部 -->
57
+ <div v-if="showFooter" class="modal-footer">
58
+ <slot name="footer">
59
+ <button
60
+ v-if="showCancel"
61
+ class="modal-btn modal-btn--cancel"
62
+ @click="handleCancel"
63
+ >
64
+ {{ cancelText }}
65
+ </button>
66
+ <button
67
+ v-if="showConfirm"
68
+ class="modal-btn modal-btn--confirm"
69
+ @click="handleConfirm"
70
+ :disabled="confirmLoading"
71
+ >
72
+ <span v-if="confirmLoading" class="loading-spinner"></span>
73
+ {{ confirmText }}
74
+ </button>
75
+ </slot>
76
+ </div>
77
+ </div>
78
+ </Transition>
79
+ </div>
80
+ </Transition>
81
+ </Teleport>
82
+ </template>
83
+
84
+ <script setup lang="ts">
85
+ import { watch } from 'vue'
86
+
87
+ /**
88
+ * Modal 模态框组件
89
+ * @displayName XModal
90
+ */
91
+
92
+ export interface ModalProps {
93
+ visible?: boolean
94
+ title?: string
95
+ width?: string
96
+ size?: 'small' | 'medium' | 'large'
97
+ fullscreen?: boolean
98
+ showHeader?: boolean
99
+ showFooter?: boolean
100
+ showClose?: boolean
101
+ showCancel?: boolean
102
+ showConfirm?: boolean
103
+ cancelText?: string
104
+ confirmText?: string
105
+ confirmLoading?: boolean
106
+ maskClosable?: boolean
107
+ escClosable?: boolean
108
+ }
109
+
110
+ const props = withDefaults(defineProps<ModalProps>(), {
111
+ visible: false,
112
+ title: '提示',
113
+ size: 'medium',
114
+ fullscreen: false,
115
+ showHeader: true,
116
+ showFooter: true,
117
+ showClose: true,
118
+ showCancel: true,
119
+ showConfirm: true,
120
+ cancelText: '取消',
121
+ confirmText: '确定',
122
+ confirmLoading: false,
123
+ maskClosable: true,
124
+ escClosable: true
125
+ })
126
+
127
+ const emit = defineEmits<{
128
+ 'update:visible': [visible: boolean]
129
+ 'close': []
130
+ 'cancel': []
131
+ 'confirm': []
132
+ 'opened': []
133
+ 'closed': []
134
+ }>()
135
+
136
+ const handleClose = () => {
137
+ emit('update:visible', false)
138
+ emit('close')
139
+ }
140
+
141
+ const handleCancel = () => {
142
+ emit('cancel')
143
+ emit('update:visible', false)
144
+ }
145
+
146
+ const handleConfirm = () => {
147
+ emit('confirm')
148
+ }
149
+
150
+ const handleMaskClick = () => {
151
+ if (props.maskClosable) {
152
+ handleClose()
153
+ }
154
+ }
155
+
156
+ // 键盘事件
157
+ const handleEsc = (e: KeyboardEvent) => {
158
+ if (e.key === 'Escape' && props.escClosable && props.visible) {
159
+ handleClose()
160
+ }
161
+ }
162
+
163
+ // 监听 visible 变化
164
+ watch(() => props.visible, (val) => {
165
+ if (val) {
166
+ document.addEventListener('keydown', handleEsc)
167
+ document.body.style.overflow = 'hidden'
168
+ emit('opened')
169
+ } else {
170
+ document.removeEventListener('keydown', handleEsc)
171
+ document.body.style.overflow = ''
172
+ emit('closed')
173
+ }
174
+ })
175
+
176
+ // 暴露方法
177
+ defineExpose({
178
+ close: handleClose
179
+ })
180
+ </script>
181
+
182
+ <style scoped>
183
+ .modal-mask {
184
+ position: fixed;
185
+ top: 0;
186
+ left: 0;
187
+ right: 0;
188
+ bottom: 0;
189
+ background-color: rgba(0, 0, 0, 0.5);
190
+ z-index: 9998;
191
+ display: flex;
192
+ align-items: center;
193
+ justify-content: center;
194
+ padding: 20px;
195
+ }
196
+
197
+ .modal-container {
198
+ position: relative;
199
+ background-color: white;
200
+ border-radius: var(--x-radius-lg, 12px);
201
+ box-shadow: var(--x-shadow-xl, 0 20px 25px rgba(0, 0, 0, 0.15));
202
+ max-width: 100%;
203
+ max-height: calc(100vh - 40px);
204
+ display: flex;
205
+ flex-direction: column;
206
+ overflow: hidden;
207
+ }
208
+
209
+ .modal-container--small {
210
+ width: 400px;
211
+ }
212
+
213
+ .modal-container--medium {
214
+ width: 600px;
215
+ }
216
+
217
+ .modal-container--large {
218
+ width: 800px;
219
+ }
220
+
221
+ .modal-container--fullscreen {
222
+ width: 100vw !important;
223
+ height: 100vh !important;
224
+ max-height: 100vh;
225
+ border-radius: 0;
226
+ }
227
+
228
+ .modal-header {
229
+ display: flex;
230
+ align-items: center;
231
+ justify-content: space-between;
232
+ padding: 20px 24px;
233
+ border-bottom: 1px solid var(--x-color-gray-200, #e5e7eb);
234
+ flex-shrink: 0;
235
+ }
236
+
237
+ .modal-title {
238
+ font-size: var(--x-font-size-lg, 18px);
239
+ font-weight: 600;
240
+ color: var(--x-color-gray-900, #1a1a1a);
241
+ margin: 0;
242
+ }
243
+
244
+ .modal-close {
245
+ display: flex;
246
+ align-items: center;
247
+ justify-content: center;
248
+ width: 32px;
249
+ height: 32px;
250
+ padding: 0;
251
+ border: none;
252
+ background: none;
253
+ color: var(--x-color-gray-500, #6b7280);
254
+ cursor: pointer;
255
+ border-radius: var(--x-radius-md, 6px);
256
+ transition: all var(--x-transition-fast, 0.15s ease);
257
+ }
258
+
259
+ .modal-close:hover {
260
+ background-color: var(--x-color-gray-100, #f3f4f6);
261
+ color: var(--x-color-gray-700, #374151);
262
+ }
263
+
264
+ .modal-body {
265
+ flex: 1;
266
+ padding: 24px;
267
+ overflow-y: auto;
268
+ color: var(--x-color-gray-700, #374151);
269
+ }
270
+
271
+ .modal-footer {
272
+ display: flex;
273
+ align-items: center;
274
+ justify-content: flex-end;
275
+ gap: 12px;
276
+ padding: 16px 24px;
277
+ border-top: 1px solid var(--x-color-gray-200, #e5e7eb);
278
+ flex-shrink: 0;
279
+ }
280
+
281
+ .modal-btn {
282
+ padding: 8px 16px;
283
+ font-size: var(--x-font-size-sm, 14px);
284
+ font-weight: 500;
285
+ border-radius: var(--x-radius-md, 6px);
286
+ cursor: pointer;
287
+ transition: all var(--x-transition-fast, 0.15s ease);
288
+ border: 1px solid;
289
+ display: inline-flex;
290
+ align-items: center;
291
+ gap: 8px;
292
+ }
293
+
294
+ .modal-btn--cancel {
295
+ background-color: white;
296
+ border-color: var(--x-color-gray-300, #d1d5db);
297
+ color: var(--x-color-gray-700, #374151);
298
+ }
299
+
300
+ .modal-btn--cancel:hover {
301
+ background-color: var(--x-color-gray-50, #f9fafb);
302
+ border-color: var(--x-color-gray-400, #9ca3af);
303
+ }
304
+
305
+ .modal-btn--confirm {
306
+ background-color: var(--x-color-primary, #1a1a1a);
307
+ border-color: var(--x-color-primary, #1a1a1a);
308
+ color: white;
309
+ }
310
+
311
+ .modal-btn--confirm:hover:not(:disabled) {
312
+ background-color: var(--x-color-primary-dark, #0d0d0d);
313
+ }
314
+
315
+ .modal-btn--confirm:disabled {
316
+ opacity: 0.6;
317
+ cursor: not-allowed;
318
+ }
319
+
320
+ .loading-spinner {
321
+ width: 16px;
322
+ height: 16px;
323
+ border: 2px solid rgba(255, 255, 255, 0.3);
324
+ border-top-color: white;
325
+ border-radius: 50%;
326
+ animation: spin 0.6s linear infinite;
327
+ }
328
+
329
+ @keyframes spin {
330
+ to {
331
+ transform: rotate(360deg);
332
+ }
333
+ }
334
+
335
+ /* 响应式 */
336
+ @media (max-width: 768px) {
337
+ .modal-mask {
338
+ padding: 10px;
339
+ }
340
+
341
+ .modal-container--small,
342
+ .modal-container--medium,
343
+ .modal-container--large {
344
+ width: 100%;
345
+ }
346
+
347
+ .modal-header {
348
+ padding: 16px;
349
+ }
350
+
351
+ .modal-body {
352
+ padding: 16px;
353
+ }
354
+
355
+ .modal-footer {
356
+ padding: 12px 16px;
357
+ }
358
+ }
359
+ </style>
360
+