@xlui/xux-ui 0.2.0 → 0.2.1

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.
@@ -36,16 +36,16 @@
36
36
  <slot name="header">
37
37
  <h3 class="modal-title">{{ title }}</h3>
38
38
  </slot>
39
- <button
39
+ <XButton
40
40
  v-if="showClose"
41
+ size="small"
41
42
  class="modal-close"
43
+ icon="mdi:close"
42
44
  @click="handleClose"
43
45
  aria-label="关闭"
44
46
  >
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>
47
+ <Icon icon="mdi:close" />
48
+ </XButton>
49
49
  </div>
50
50
 
51
51
  <!-- 内容 -->
@@ -56,22 +56,28 @@
56
56
  <!-- 底部 -->
57
57
  <div v-if="showFooter" class="modal-footer">
58
58
  <slot name="footer">
59
- <button
59
+ <XButton
60
60
  v-if="showCancel"
61
- class="modal-btn modal-btn--cancel"
61
+ size="small"
62
+ ghost
63
+ type="danger"
64
+
62
65
  @click="handleCancel"
63
66
  >
64
67
  {{ cancelText }}
65
- </button>
66
- <button
68
+ </XButton>
69
+ <XButton
67
70
  v-if="showConfirm"
68
- class="modal-btn modal-btn--confirm"
71
+ size="small"
72
+ ghost
73
+ type="primary"
74
+
69
75
  @click="handleConfirm"
70
76
  :disabled="confirmLoading"
71
77
  >
72
78
  <span v-if="confirmLoading" class="loading-spinner"></span>
73
79
  {{ confirmText }}
74
- </button>
80
+ </XButton>
75
81
  </slot>
76
82
  </div>
77
83
  </div>
@@ -83,7 +89,7 @@
83
89
 
84
90
  <script setup lang="ts">
85
91
  import { watch } from 'vue'
86
-
92
+ import { Icon } from '@iconify/vue'
87
93
  /**
88
94
  * Modal 模态框组件
89
95
  * @displayName XModal
@@ -0,0 +1,620 @@
1
+ <template>
2
+ <!-- 标准版本 -->
3
+ <div v-if="variant === 'standard'" class="x-radio-group" :class="[`radio-${size}`, `radio-${direction}`, `radio-${mode}`, { 'radio-disabled': disabled }]">
4
+ <label
5
+ v-for="option in options"
6
+ :key="option.value"
7
+ class="x-radio-item"
8
+ :class="[
9
+ { 'radio-item-checked': modelValue === option.value },
10
+ { 'radio-item-disabled': disabled || option.disabled }
11
+ ]"
12
+ >
13
+ <input
14
+ type="radio"
15
+ :name="name"
16
+ :value="option.value"
17
+ :disabled="disabled || option.disabled"
18
+ :checked="modelValue === option.value"
19
+ @change="handleChange(option.value)"
20
+ class="radio-input"
21
+ />
22
+
23
+ <!-- 传统单选按钮模式 -->
24
+ <div v-if="mode === 'default'" class="radio-button">
25
+ <div class="radio-button-inner"></div>
26
+ </div>
27
+
28
+ <div class="radio-content">
29
+ <!-- 图标 -->
30
+ <div v-if="option.icon || showIcon" class="radio-icon">
31
+ <slot name="icon" :option="option">
32
+ <Icon v-if="option.icon" :icon="option.icon" />
33
+ </slot>
34
+ </div>
35
+
36
+ <!-- 内容 -->
37
+ <div class="radio-text">
38
+ <div class="radio-label">{{ option.label }}</div>
39
+ <div v-if="option.description" class="radio-description">{{ option.description }}</div>
40
+ </div>
41
+
42
+ <!-- 自定义内容插槽 -->
43
+ <slot name="content" :option="option"></slot>
44
+ </div>
45
+ </label>
46
+ </div>
47
+
48
+ <!-- 电商支付版本 -->
49
+ <div v-else class="payment-method-container">
50
+ <p class="payment-title">Payment method</p>
51
+ <p class="payment-subtitle">
52
+ This component is made by using
53
+ <span class="payment-highlight">`:has() pseudo-class`</span>
54
+ </p>
55
+
56
+ <label
57
+ v-for="option in paymentOptions"
58
+ :key="option.value"
59
+ class="payment-option"
60
+ :class="{ 'payment-option-checked': modelValue === option.value }"
61
+ >
62
+ <div class="payment-content">
63
+ <div class="payment-icon">
64
+ <Icon :icon="option.icon" />
65
+ </div>
66
+ <p class="payment-label">{{ option.label }}</p>
67
+ </div>
68
+ <input
69
+ type="radio"
70
+ :name="name"
71
+ :value="option.value"
72
+ :disabled="disabled"
73
+ :checked="modelValue === option.value"
74
+ @change="handleChange(option.value)"
75
+ class="payment-input"
76
+ />
77
+ </label>
78
+ </div>
79
+ </template>
80
+
81
+ <script setup lang="ts">
82
+ import { Icon } from '@iconify/vue'
83
+ export interface RadioOption {
84
+ /** 选项值 */
85
+ value: string | number
86
+ /** 选项标签 */
87
+ label: string
88
+ /** 选项描述 */
89
+ description?: string
90
+ /** 是否禁用 */
91
+ disabled?: boolean
92
+ /** 图标组件 */
93
+ icon?: any
94
+ }
95
+
96
+ export interface RadioProps {
97
+ /** 当前选中的值 */
98
+ modelValue?: string | number
99
+ /** 选项列表 */
100
+ options?: RadioOption[]
101
+ /** 组件尺寸 */
102
+ size?: 'small' | 'medium' | 'large'
103
+ /** 是否禁用 */
104
+ disabled?: boolean
105
+ /** 是否显示图标 */
106
+ showIcon?: boolean
107
+ /** 单选组名称 */
108
+ name?: string
109
+ /** 布局方向 */
110
+ direction?: 'horizontal' | 'vertical'
111
+ /** 组件变体 */
112
+ variant?: 'standard' | 'payment'
113
+ /** 显示模式 */
114
+ mode?: 'default' | 'list'
115
+ }
116
+
117
+ const props = withDefaults(defineProps<RadioProps>(), {
118
+ modelValue: '',
119
+ options: () => [],
120
+ size: 'medium',
121
+ disabled: false,
122
+ showIcon: false,
123
+ name: 'radio-group',
124
+ direction: 'vertical',
125
+ variant: 'standard',
126
+ mode: 'default'
127
+ })
128
+
129
+ const emit = defineEmits<{
130
+ 'update:modelValue': [value: string | number]
131
+ change: [value: string | number]
132
+ }>()
133
+
134
+ // 支付选项配置
135
+ const paymentOptions = [
136
+ {
137
+ value: 'google',
138
+ label: 'Google Pay',
139
+ icon: 'logos:google-pay'
140
+ },
141
+ {
142
+ value: 'apple',
143
+ label: 'Apple Pay',
144
+ icon: 'logos:apple-pay'
145
+ },
146
+ {
147
+ value: 'paypal',
148
+ label: 'Paypal',
149
+ icon: 'logos:paypal'
150
+ },
151
+ {
152
+ value: 'visa',
153
+ label: 'Credit Card',
154
+ icon: 'logos:visa'
155
+ }
156
+ ]
157
+
158
+ // 处理选项变化
159
+ const handleChange = (value: string | number) => {
160
+ if (props.disabled) return
161
+
162
+ emit('update:modelValue', value)
163
+ emit('change', value)
164
+ }
165
+ </script>
166
+
167
+ <style scoped>
168
+ /* ===== 标准版本样式 ===== */
169
+ .x-radio-group {
170
+ display: flex;
171
+ flex-direction: column;
172
+ gap: 0.75rem;
173
+ }
174
+
175
+ .x-radio-group.radio-horizontal {
176
+ flex-direction: row;
177
+ flex-wrap: wrap;
178
+ gap: 1.5rem;
179
+ }
180
+
181
+ .x-radio-item {
182
+ position: relative;
183
+ cursor: pointer;
184
+ display: flex;
185
+ align-items: center;
186
+ gap: 0.75rem;
187
+ padding: 0.5rem 0;
188
+ transition: all 0.2s ease;
189
+ }
190
+
191
+ /* 列表模式样式 */
192
+ .radio-list .x-radio-item {
193
+ border: 1px solid #e5e7eb;
194
+ border-radius: 0.5rem;
195
+ padding: 1rem;
196
+ background-color: #ffffff;
197
+ margin-bottom: 0.5rem;
198
+ }
199
+
200
+ .radio-list .x-radio-item:hover {
201
+ border-color: #1a1a1a;
202
+ background-color: #f9fafb;
203
+ }
204
+
205
+ .radio-list .x-radio-item.radio-item-checked {
206
+ border-color: #1a1a1a;
207
+ background-color: #f3f4f6;
208
+ box-shadow: 0 0 0 1px #1a1a1a;
209
+ }
210
+
211
+ .radio-list .x-radio-item.radio-item-disabled {
212
+ opacity: 0.5;
213
+ cursor: not-allowed;
214
+ background-color: #f9fafb;
215
+ border-color: #e5e7eb;
216
+ }
217
+
218
+ .radio-list .x-radio-item.radio-item-disabled:hover {
219
+ border-color: #e5e7eb;
220
+ background-color: #f9fafb;
221
+ }
222
+
223
+ .x-radio-item:hover .radio-button {
224
+ border-color: #1a1a1a;
225
+ }
226
+
227
+ .x-radio-item.radio-item-checked .radio-button {
228
+ border-color: #1a1a1a;
229
+ }
230
+
231
+ .x-radio-item.radio-item-checked .radio-button-inner {
232
+ background-color: #1a1a1a;
233
+ transform: translate(-50%, -50%) scale(1);
234
+ }
235
+
236
+ .x-radio-item.radio-item-disabled {
237
+ opacity: 0.5;
238
+ cursor: not-allowed;
239
+ }
240
+
241
+ .x-radio-item.radio-item-disabled:hover .radio-button {
242
+ border-color: #d1d5db;
243
+ }
244
+
245
+ .radio-input {
246
+ position: absolute;
247
+ opacity: 0;
248
+ pointer-events: none;
249
+ }
250
+
251
+ .radio-button {
252
+ position: relative;
253
+ width: 1.25rem;
254
+ height: 1.25rem;
255
+ border: 2px solid #d1d5db;
256
+ border-radius: 50%;
257
+ background-color: #ffffff;
258
+ transition: all 0.2s ease;
259
+ flex-shrink: 0;
260
+ }
261
+
262
+ .radio-button-inner {
263
+ position: absolute;
264
+ top: 50%;
265
+ left: 50%;
266
+ width: 0.5rem;
267
+ height: 0.5rem;
268
+ background-color: transparent;
269
+ border-radius: 50%;
270
+ transform: translate(-50%, -50%) scale(0);
271
+ transition: all 0.2s ease;
272
+ }
273
+
274
+ .radio-content {
275
+ display: flex;
276
+ align-items: center;
277
+ gap: 0.75rem;
278
+ flex: 1;
279
+ }
280
+
281
+ .radio-icon {
282
+ display: flex;
283
+ align-items: center;
284
+ justify-content: center;
285
+ width: 1.5rem;
286
+ height: 1.5rem;
287
+ color: #6b7280;
288
+ transition: color 0.2s ease;
289
+ }
290
+
291
+ .radio-item-checked .radio-icon {
292
+ color: #1a1a1a;
293
+ }
294
+
295
+ .radio-text {
296
+ flex: 1;
297
+ min-width: 0;
298
+ }
299
+
300
+ .radio-label {
301
+ font-weight: 500;
302
+ color: #1a1a1a;
303
+ transition: color 0.2s ease;
304
+ font-size: 0.875rem;
305
+ line-height: 1.5;
306
+ }
307
+
308
+ .radio-item-checked .radio-label {
309
+ color: #1a1a1a;
310
+ font-weight: 600;
311
+ }
312
+
313
+ .radio-description {
314
+ font-size: 0.75rem;
315
+ color: #6b7280;
316
+ margin-top: 0.25rem;
317
+ line-height: 1.4;
318
+ transition: color 0.2s ease;
319
+ }
320
+
321
+ .radio-item-checked .radio-description {
322
+ color: #4b5563;
323
+ }
324
+
325
+ /* 尺寸变体 */
326
+ .radio-small .x-radio-item {
327
+ padding: 0.25rem 0;
328
+ gap: 0.5rem;
329
+ }
330
+
331
+ .radio-small.radio-list .x-radio-item {
332
+ padding: 0.75rem;
333
+ }
334
+
335
+ .radio-small .radio-button {
336
+ width: 1rem;
337
+ height: 1rem;
338
+ }
339
+
340
+ .radio-small .radio-button-inner {
341
+ width: 0.375rem;
342
+ height: 0.375rem;
343
+ transform: translate(-50%, -50%) scale(0);
344
+ }
345
+
346
+ .radio-small .x-radio-item.radio-item-checked .radio-button-inner {
347
+ transform: translate(-50%, -50%) scale(1);
348
+ }
349
+
350
+ .radio-small .radio-content {
351
+ gap: 0.5rem;
352
+ }
353
+
354
+ .radio-small .radio-icon {
355
+ width: 1.25rem;
356
+ height: 1.25rem;
357
+ }
358
+
359
+ .radio-small .radio-label {
360
+ font-size: 0.75rem;
361
+ font-weight: 500;
362
+ }
363
+
364
+ .radio-small .radio-description {
365
+ font-size: 0.625rem;
366
+ margin-top: 0.125rem;
367
+ }
368
+
369
+ .radio-medium .x-radio-item {
370
+ padding: 0.5rem 0;
371
+ gap: 0.75rem;
372
+ }
373
+
374
+ .radio-medium.radio-list .x-radio-item {
375
+ padding: 1rem;
376
+ }
377
+
378
+ .radio-medium .radio-button {
379
+ width: 1.25rem;
380
+ height: 1.25rem;
381
+ }
382
+
383
+ .radio-medium .radio-button-inner {
384
+ width: 0.5rem;
385
+ height: 0.5rem;
386
+ transform: translate(-50%, -50%) scale(0);
387
+ }
388
+
389
+ .radio-medium .x-radio-item.radio-item-checked .radio-button-inner {
390
+ transform: translate(-50%, -50%) scale(1);
391
+ }
392
+
393
+ .radio-medium .radio-content {
394
+ gap: 0.75rem;
395
+ }
396
+
397
+ .radio-medium .radio-icon {
398
+ width: 1.5rem;
399
+ height: 1.5rem;
400
+ }
401
+
402
+ .radio-large .x-radio-item {
403
+ padding: 0.75rem 0;
404
+ gap: 1rem;
405
+ }
406
+
407
+ .radio-large.radio-list .x-radio-item {
408
+ padding: 1.25rem;
409
+ }
410
+
411
+ .radio-large .radio-button {
412
+ width: 1.5rem;
413
+ height: 1.5rem;
414
+ }
415
+
416
+ .radio-large .radio-button-inner {
417
+ width: 0.625rem;
418
+ height: 0.625rem;
419
+ transform: translate(-50%, -50%) scale(0);
420
+ }
421
+
422
+ .radio-large .x-radio-item.radio-item-checked .radio-button-inner {
423
+ transform: translate(-50%, -50%) scale(1);
424
+ }
425
+
426
+ .radio-large .radio-content {
427
+ gap: 1rem;
428
+ }
429
+
430
+ .radio-large .radio-icon {
431
+ width: 1.75rem;
432
+ height: 1.75rem;
433
+ }
434
+
435
+ .radio-large .radio-label {
436
+ font-size: 1rem;
437
+ font-weight: 600;
438
+ }
439
+
440
+ .radio-large .radio-description {
441
+ font-size: 0.875rem;
442
+ margin-top: 0.375rem;
443
+ }
444
+
445
+ /* 水平布局调整 */
446
+ .radio-horizontal .x-radio-item {
447
+ flex: 1;
448
+ min-width: 0;
449
+ max-width: 300px;
450
+ }
451
+
452
+ /* 禁用状态 */
453
+ .radio-disabled .x-radio-item {
454
+ opacity: 0.5;
455
+ cursor: not-allowed;
456
+ background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
457
+ border-color: #e5e7eb;
458
+ }
459
+
460
+ .radio-disabled .x-radio-item:hover {
461
+ border-color: #e5e7eb;
462
+ background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
463
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
464
+ transform: none;
465
+ }
466
+
467
+ .radio-disabled .radio-icon {
468
+ background: rgba(243, 244, 246, 0.8);
469
+ color: #9ca3af;
470
+ }
471
+
472
+ .radio-disabled .radio-label {
473
+ color: #9ca3af;
474
+ }
475
+
476
+ .radio-disabled .radio-description {
477
+ color: #9ca3af;
478
+ }
479
+
480
+ /* ===== 电商支付版本样式 ===== */
481
+ .payment-method-container {
482
+ width: 320px;
483
+ padding: 1.5rem;
484
+ aspect-ratio: 1;
485
+ border-radius: 1rem;
486
+ box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
487
+ display: flex;
488
+ flex-direction: column;
489
+ align-items: center;
490
+ justify-content: center;
491
+ gap: 1rem;
492
+ background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
493
+ backdrop-filter: blur(10px);
494
+ border: 1px solid rgba(255, 255, 255, 0.2);
495
+ }
496
+
497
+ .payment-title {
498
+ text-transform: capitalize;
499
+ font-weight: 700;
500
+ align-self: flex-start;
501
+ margin: 0;
502
+ color: #1e293b;
503
+ font-size: 1.125rem;
504
+ letter-spacing: -0.025em;
505
+ }
506
+
507
+ .payment-subtitle {
508
+ font-size: 0.75rem;
509
+ align-self: flex-start;
510
+ text-wrap: wrap;
511
+ color: #64748b;
512
+ padding-bottom: 0.5rem;
513
+ margin: 0;
514
+ line-height: 1.4;
515
+ }
516
+
517
+ .payment-highlight {
518
+ font-weight: 800;
519
+ text-decoration: underline;
520
+ color: #3b82f6;
521
+ }
522
+
523
+ .payment-option {
524
+ display: inline-flex;
525
+ justify-content: space-between;
526
+ width: 100%;
527
+ align-items: center;
528
+ z-index: 10;
529
+ border-radius: 0.75rem;
530
+ padding: 0.875rem;
531
+ border: 1px solid rgba(226, 232, 240, 0.8);
532
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
533
+ cursor: pointer;
534
+ position: relative;
535
+ overflow: hidden;
536
+ background: rgba(255, 255, 255, 0.8);
537
+ backdrop-filter: blur(8px);
538
+ }
539
+
540
+ .payment-option:hover {
541
+ background: rgba(240, 249, 255, 0.9);
542
+ border-color: rgba(59, 130, 246, 0.3);
543
+ transform: translateY(-1px);
544
+ box-shadow: 0 4px 12px 0 rgba(59, 130, 246, 0.15);
545
+ }
546
+
547
+ .payment-option.payment-option-checked {
548
+ border-color: #3b82f6;
549
+ color: #1e40af;
550
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(147, 197, 253, 0.1) 100%);
551
+ font-weight: 700;
552
+ box-shadow: 0 4px 16px 0 rgba(59, 130, 246, 0.25);
553
+ transform: translateY(-1px);
554
+ }
555
+
556
+ .payment-content {
557
+ display: inline-flex;
558
+ align-items: center;
559
+ justify-content: center;
560
+ gap: 0.75rem;
561
+ position: relative;
562
+ z-index: 10;
563
+ }
564
+
565
+ .payment-icon {
566
+ width: 2.5rem;
567
+ height: 2.5rem;
568
+ display: flex;
569
+ align-items: center;
570
+ justify-content: center;
571
+ border-radius: 0.5rem;
572
+ background: rgba(255, 255, 255, 0.9);
573
+ backdrop-filter: blur(8px);
574
+ transition: all 0.2s ease;
575
+ }
576
+
577
+ .payment-option.payment-option-checked .payment-icon {
578
+ background: rgba(59, 130, 246, 0.1);
579
+ transform: scale(1.05);
580
+ }
581
+
582
+ .payment-label {
583
+ font-weight: 600;
584
+ position: absolute;
585
+ inset: 0;
586
+ width: 100%;
587
+ white-space: nowrap;
588
+ transform: translateY(110%) translateX(100%);
589
+ top: 0.25rem;
590
+ left: 3.5rem;
591
+ transition: all 0.7s cubic-bezier(0.4, 0, 0.2, 1);
592
+ opacity: 0;
593
+ margin: 0;
594
+ font-size: 0.875rem;
595
+ letter-spacing: -0.025em;
596
+ }
597
+
598
+ .payment-option.payment-option-checked .payment-label {
599
+ transform: translateY(0);
600
+ opacity: 1;
601
+ }
602
+
603
+ .payment-input {
604
+ color: #3b82f6;
605
+ outline: none;
606
+ box-shadow: none;
607
+ }
608
+
609
+ .payment-input:focus {
610
+ outline: none;
611
+ box-shadow: none;
612
+ }
613
+
614
+ /* 支付图标组件样式 */
615
+ .payment-icon svg {
616
+ width: 1.5rem;
617
+ height: 1.5rem;
618
+ fill: currentColor;
619
+ }
620
+ </style>