@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,440 @@
1
+ <template>
2
+ <button
3
+ :class="buttonClass"
4
+ :disabled="disabled || loading"
5
+ @click="handleClick"
6
+ @mousedown="handleMouseDown"
7
+ ref="buttonRef"
8
+ >
9
+ <!-- 波纹效果 -->
10
+ <span
11
+ v-for="ripple in ripples"
12
+ :key="ripple.id"
13
+ class="x-btn-ripple"
14
+ :style="ripple.style"
15
+ ></span>
16
+
17
+ <!-- 左侧图标 -->
18
+ <span v-if="icon && !loading" class="x-btn-icon">
19
+ <Icon :icon="icon" />
20
+ </span>
21
+
22
+ <!-- 加载图标 -->
23
+ <span v-if="loading" class="x-btn-loading">
24
+ <Icon icon="eos-icons:loading" />
25
+ </span>
26
+
27
+ <!-- 按钮内容 -->
28
+ <span class="x-btn-content">
29
+ <slot />
30
+ </span>
31
+
32
+ <!-- 右侧图标 -->
33
+ <span v-if="iconRight && !loading" class="x-btn-icon">
34
+ <Icon :icon="iconRight" />
35
+ </span>
36
+ </button>
37
+ </template>
38
+
39
+ <script setup lang="ts">
40
+ import { computed, ref } from 'vue'
41
+ import { Icon } from '@iconify/vue'
42
+
43
+ /**
44
+ * Button 按钮组件
45
+ * @displayName XButton
46
+ */
47
+
48
+ type ButtonType = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'text'
49
+ type ButtonSize = 'small' | 'medium' | 'large'
50
+
51
+ interface Props {
52
+ /**
53
+ * 按钮类型
54
+ * @values default, primary, success, warning, danger, text
55
+ */
56
+ type?: ButtonType
57
+ /**
58
+ * 按钮尺寸
59
+ * @values small, medium, large
60
+ */
61
+ size?: ButtonSize
62
+ /**
63
+ * 左侧图标(使用 iconify 图标名)
64
+ * @example "mdi:cart"
65
+ */
66
+ icon?: string
67
+ /**
68
+ * 右侧图标(使用 iconify 图标名)
69
+ * @example "mdi:arrow-right"
70
+ */
71
+ iconRight?: string
72
+ /**
73
+ * 是否禁用
74
+ */
75
+ disabled?: boolean
76
+ /**
77
+ * 是否加载中
78
+ */
79
+ loading?: boolean
80
+ /**
81
+ * 是否为块级按钮
82
+ */
83
+ block?: boolean
84
+ /**
85
+ * 是否为圆形按钮
86
+ */
87
+ round?: boolean
88
+ /**
89
+ * 是否为朴素按钮
90
+ */
91
+ plain?: boolean
92
+ }
93
+
94
+ const props = withDefaults(defineProps<Props>(), {
95
+ type: 'default',
96
+ size: 'medium',
97
+ disabled: false,
98
+ loading: false,
99
+ block: false,
100
+ round: false,
101
+ plain: false
102
+ })
103
+
104
+ const emit = defineEmits<{
105
+ /**
106
+ * 点击按钮时触发
107
+ */
108
+ (e: 'click', event: MouseEvent): void
109
+ }>()
110
+
111
+ // 波纹效果
112
+ interface Ripple {
113
+ id: number
114
+ style: {
115
+ left: string
116
+ top: string
117
+ }
118
+ }
119
+
120
+ const buttonRef = ref<HTMLButtonElement>()
121
+ const ripples = ref<Ripple[]>([])
122
+ let rippleId = 0
123
+
124
+ const buttonClass = computed(() => {
125
+ return [
126
+ 'x-btn',
127
+ `x-btn-${props.type}`,
128
+ `x-btn-${props.size}`,
129
+ {
130
+ 'x-btn-disabled': props.disabled,
131
+ 'x-btn-loading': props.loading,
132
+ 'x-btn-block': props.block,
133
+ 'x-btn-round': props.round,
134
+ 'x-btn-plain': props.plain,
135
+ }
136
+ ]
137
+ })
138
+
139
+ function handleClick(event: MouseEvent) {
140
+ if (!props.disabled && !props.loading) {
141
+ emit('click', event)
142
+ }
143
+ }
144
+
145
+ function handleMouseDown(event: MouseEvent) {
146
+ if (props.disabled || props.loading) return
147
+
148
+ const button = buttonRef.value
149
+ if (!button) return
150
+
151
+ const rect = button.getBoundingClientRect()
152
+ const ripple: Ripple = {
153
+ id: rippleId++,
154
+ style: {
155
+ left: `${event.clientX - rect.left}px`,
156
+ top: `${event.clientY - rect.top}px`,
157
+ }
158
+ }
159
+
160
+ ripples.value.push(ripple)
161
+
162
+ setTimeout(() => {
163
+ const index = ripples.value.findIndex(r => r.id === ripple.id)
164
+ if (index > -1) {
165
+ ripples.value.splice(index, 1)
166
+ }
167
+ }, 600)
168
+ }
169
+ </script>
170
+
171
+ <style scoped>
172
+ .x-btn {
173
+ position: relative;
174
+ display: inline-flex;
175
+ align-items: center;
176
+ justify-content: center;
177
+ gap: 6px;
178
+ font-weight: 500;
179
+ border: 1px solid transparent;
180
+ cursor: pointer;
181
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
182
+ user-select: none;
183
+ white-space: nowrap;
184
+ outline: none;
185
+ overflow: hidden;
186
+ transform: translateY(0);
187
+ }
188
+
189
+ .x-btn:active:not(.x-btn-disabled):not(.x-btn-loading) {
190
+ transform: translateY(1px);
191
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
192
+ }
193
+
194
+ /* 波纹效果 */
195
+ .x-btn-ripple {
196
+ position: absolute;
197
+ border-radius: 50%;
198
+ background-color: rgba(255, 255, 255, 0.4);
199
+ width: 0;
200
+ height: 0;
201
+ pointer-events: none;
202
+ animation: ripple-animation 0.6s ease-out;
203
+ transform: translate(-50%, -50%);
204
+ }
205
+
206
+ @keyframes ripple-animation {
207
+ to {
208
+ width: 300px;
209
+ height: 300px;
210
+ opacity: 0;
211
+ }
212
+ }
213
+
214
+ /* 图标和内容布局 */
215
+ .x-btn-icon {
216
+ display: inline-flex;
217
+ align-items: center;
218
+ font-size: 1.2em;
219
+ animation: icon-in 0.3s ease-out;
220
+ }
221
+
222
+ .x-btn-content {
223
+ display: inline-flex;
224
+ align-items: center;
225
+ }
226
+
227
+ @keyframes icon-in {
228
+ from {
229
+ opacity: 0;
230
+ transform: scale(0.8);
231
+ }
232
+ to {
233
+ opacity: 1;
234
+ transform: scale(1);
235
+ }
236
+ }
237
+
238
+ /* 尺寸 */
239
+ .x-btn-small {
240
+ padding: 4px 12px;
241
+ font-size: 14px;
242
+ border-radius: 4px;
243
+ height: 28px;
244
+ }
245
+
246
+ .x-btn-medium {
247
+ padding: 8px 16px;
248
+ font-size: 14px;
249
+ border-radius: 6px;
250
+ height: 36px;
251
+ }
252
+
253
+ .x-btn-large {
254
+ padding: 12px 20px;
255
+ font-size: 16px;
256
+ border-radius: 8px;
257
+ height: 44px;
258
+ }
259
+
260
+ /* 类型 - default */
261
+ .x-btn-default {
262
+ background-color: var(--x-color-white, #ffffff);
263
+ border-color: var(--x-color-gray-300, #cacaca);
264
+ color: var(--x-color-black, #1a1a1a);
265
+ box-shadow: var(--x-shadow-sm, 0 1px 2px rgba(26, 26, 26, 0.05));
266
+ }
267
+
268
+ .x-btn-default:hover:not(.x-btn-disabled):not(.x-btn-loading) {
269
+ border-color: var(--x-color-primary, #1a1a1a);
270
+ color: var(--x-color-primary, #1a1a1a);
271
+ box-shadow: var(--x-shadow-primary, 0 4px 12px rgba(26, 26, 26, 0.3));
272
+ transform: translateY(-2px);
273
+ }
274
+
275
+ /* 类型 - primary */
276
+ .x-btn-primary {
277
+ background: var(--x-gradient-primary, linear-gradient(135deg, #ff6b35 0%, #ff8c61 100%));
278
+ border-color: var(--x-color-primary, #ff6b35);
279
+ color: var(--x-color-white, #ffffff);
280
+ box-shadow: var(--x-shadow-primary, 0 4px 12px rgba(255, 107, 53, 0.3));
281
+ }
282
+
283
+ .x-btn-primary:hover:not(.x-btn-disabled):not(.x-btn-loading) {
284
+ background: var(--x-gradient-primary-hover, linear-gradient(135deg, #ff8c61 0%, #ffad8d 100%));
285
+ box-shadow: var(--x-shadow-primary-lg, 0 8px 24px rgba(255, 107, 53, 0.4));
286
+ transform: translateY(-2px);
287
+ }
288
+
289
+ /* 类型 - success */
290
+ .x-btn-success {
291
+ background: var(--x-gradient-success, linear-gradient(135deg, #10b981 0%, #059669 100%));
292
+ border-color: var(--x-color-success, #10b981);
293
+ color: var(--x-color-white, #ffffff);
294
+ box-shadow: var(--x-shadow-success, 0 4px 12px rgba(16, 185, 129, 0.3));
295
+ }
296
+
297
+ .x-btn-success:hover:not(.x-btn-disabled):not(.x-btn-loading) {
298
+ background: linear-gradient(135deg, var(--x-color-success-light, #34d399) 0%, var(--x-color-success, #10b981) 100%);
299
+ box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
300
+ transform: translateY(-2px);
301
+ }
302
+
303
+ /* 类型 - warning */
304
+ .x-btn-warning {
305
+ background: var(--x-gradient-warning, linear-gradient(135deg, #f59e0b 0%, #d97706 100%));
306
+ border-color: var(--x-color-warning, #f59e0b);
307
+ color: var(--x-color-white, #ffffff);
308
+ box-shadow: var(--x-shadow-warning, 0 4px 12px rgba(245, 158, 11, 0.3));
309
+ }
310
+
311
+ .x-btn-warning:hover:not(.x-btn-disabled):not(.x-btn-loading) {
312
+ background: linear-gradient(135deg, var(--x-color-warning-light, #fbbf24) 0%, var(--x-color-warning, #f59e0b) 100%);
313
+ box-shadow: 0 6px 16px rgba(245, 158, 11, 0.4);
314
+ transform: translateY(-2px);
315
+ }
316
+
317
+ /* 类型 - danger */
318
+ .x-btn-danger {
319
+ background: var(--x-gradient-danger, linear-gradient(135deg, #ef4444 0%, #dc2626 100%));
320
+ border-color: var(--x-color-danger, #ef4444);
321
+ color: var(--x-color-white, #ffffff);
322
+ box-shadow: var(--x-shadow-danger, 0 4px 12px rgba(239, 68, 68, 0.3));
323
+ }
324
+
325
+ .x-btn-danger:hover:not(.x-btn-disabled):not(.x-btn-loading) {
326
+ background: linear-gradient(135deg, var(--x-color-danger-light, #f87171) 0%, var(--x-color-danger, #ef4444) 100%);
327
+ box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
328
+ transform: translateY(-2px);
329
+ }
330
+
331
+ /* 类型 - text */
332
+ .x-btn-text {
333
+ background-color: transparent;
334
+ border-color: transparent;
335
+ color: var(--x-color-black, #1a1a1a);
336
+ }
337
+
338
+ .x-btn-text:hover:not(.x-btn-disabled):not(.x-btn-loading) {
339
+ background-color: var(--x-color-gray-100, #f5f5f5);
340
+ color: var(--x-color-primary, #ff6b35);
341
+ }
342
+
343
+ /* 朴素按钮 */
344
+ .x-btn-plain.x-btn-primary {
345
+ background-color: rgba(26, 26, 26, 0.1);
346
+ border-color: var(--x-color-primary-lighter, #ffad8d);
347
+ color: white;
348
+ }
349
+
350
+ .x-btn-plain.x-btn-primary:hover:not(.x-btn-disabled):not(.x-btn-loading) {
351
+ background-color: rgba(26, 26, 26, 0.2);
352
+ border-color: var(--x-color-primary, #1a1a1a);
353
+ color: white;
354
+ }
355
+
356
+ .x-btn-plain.x-btn-success {
357
+ background-color: rgba(16, 185, 129, 0.1);
358
+ border-color: var(--x-color-success-lighter, #6ee7b7);
359
+ color: white;
360
+ }
361
+
362
+ .x-btn-plain.x-btn-success:hover:not(.x-btn-disabled):not(.x-btn-loading) {
363
+ background-color: rgba(16, 185, 129, 0.2);
364
+ border-color: var(--x-color-success, #10b981);
365
+ color: white;
366
+ }
367
+
368
+ .x-btn-plain.x-btn-warning {
369
+ background-color: rgba(245, 158, 11, 0.1);
370
+ border-color: var(--x-color-warning-lighter, #fcd34d);
371
+ color: white;
372
+ }
373
+
374
+ .x-btn-plain.x-btn-warning:hover:not(.x-btn-disabled):not(.x-btn-loading) {
375
+ background-color: rgba(245, 158, 11, 0.2);
376
+ border-color: var(--x-color-warning, #f59e0b);
377
+ color: white;
378
+ }
379
+
380
+ .x-btn-plain.x-btn-danger {
381
+ background-color: rgba(239, 68, 68, 0.1);
382
+ border-color: var(--x-color-danger-lighter, #fca5a5);
383
+ color: white;
384
+ }
385
+
386
+ .x-btn-plain.x-btn-danger:hover:not(.x-btn-disabled):not(.x-btn-loading) {
387
+ background-color: rgba(239, 68, 68, 0.2);
388
+ border-color: var(--x-color-danger, #ef4444);
389
+ color: white;
390
+ }
391
+
392
+ /* 圆角按钮 */
393
+ .x-btn-round.x-btn-small {
394
+ border-radius: 14px;
395
+ }
396
+
397
+ .x-btn-round.x-btn-medium {
398
+ border-radius: 18px;
399
+ }
400
+
401
+ .x-btn-round.x-btn-large {
402
+ border-radius: 22px;
403
+ }
404
+
405
+ /* 块级按钮 */
406
+ .x-btn-block {
407
+ display: flex;
408
+ width: 100%;
409
+ }
410
+
411
+ /* 禁用状态 */
412
+ .x-btn-disabled {
413
+ cursor: not-allowed;
414
+ opacity: 0.6;
415
+ }
416
+
417
+ /* 加载状态 */
418
+ .x-btn-loading {
419
+ cursor: wait;
420
+ opacity: 0.8;
421
+ }
422
+
423
+ .x-btn.x-btn-loading {
424
+ pointer-events: none;
425
+ }
426
+
427
+ .x-btn-loading .x-btn-icon {
428
+ font-size: 1em;
429
+ animation: spin 1s linear infinite;
430
+ }
431
+
432
+ @keyframes spin {
433
+ from {
434
+ transform: rotate(0deg);
435
+ }
436
+ to {
437
+ transform: rotate(360deg);
438
+ }
439
+ }
440
+ </style>