@xlui/xux-ui 0.2.1 → 0.3.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.
- package/dist/index.css +1 -1
- package/dist/index.js +22 -22
- package/dist/index.mjs +989 -927
- package/package.json +1 -1
- package/src/components/Card/index.vue +44 -11
- package/src/components/Switch/index.vue +234 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
@@ -10,12 +10,16 @@
|
|
10
10
|
>
|
11
11
|
<div class="relative p-2">
|
12
12
|
<!-- 瀑布流保持自然高度 -->
|
13
|
-
<
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
13
|
+
<div class="image-container relative overflow-hidden rounded-lg bg-gray-100">
|
14
|
+
<img
|
15
|
+
:src="product.image"
|
16
|
+
:alt="product.name"
|
17
|
+
loading="lazy"
|
18
|
+
class="w-full h-auto object-cover transition-transform duration-300 group-hover:scale-[1.01]"
|
19
|
+
/>
|
20
|
+
<!-- 光效蒙版 -->
|
21
|
+
<div class="light-effect"></div>
|
22
|
+
</div>
|
19
23
|
|
20
24
|
<div v-if="product.label" class="absolute top-3 left-3">
|
21
25
|
<span class="label-badge">{{ product.label }}</span>
|
@@ -44,18 +48,20 @@
|
|
44
48
|
<div v-else :class="['grid', gridColsClass, 'gap-4 lg:gap-6']">
|
45
49
|
<div v-for="product in displayProducts" :key="product.id + '-grid'" class="h-full">
|
46
50
|
<div
|
47
|
-
class="group cursor-pointer product-card h-full flex flex-col"
|
51
|
+
class="py-2 px-2 group cursor-pointer product-card h-full flex flex-col"
|
48
52
|
@click="handleProductClick(product.id)"
|
49
53
|
>
|
50
54
|
<div class="relative flex-shrink-0 p-2">
|
51
55
|
<!-- 网格布局使用固定比例 -->
|
52
|
-
<div class="aspect-square bg-gray-100 rounded-lg overflow-hidden">
|
56
|
+
<div class="image-container aspect-square bg-gray-100 rounded-lg overflow-hidden relative">
|
53
57
|
<img
|
54
58
|
:src="product.image"
|
55
59
|
:alt="product.name"
|
56
|
-
class="w-full h-full object-cover
|
60
|
+
class="w-full h-full object-cover transition-transform duration-300"
|
57
61
|
loading="lazy"
|
58
62
|
/>
|
63
|
+
<!-- 光效蒙版 -->
|
64
|
+
<div class="light-effect"></div>
|
59
65
|
</div>
|
60
66
|
|
61
67
|
<div v-if="product.label" class="absolute top-3 left-3">
|
@@ -75,8 +81,8 @@
|
|
75
81
|
size="small"
|
76
82
|
class="like-button !min-w-0 !p-1"
|
77
83
|
>
|
78
|
-
<Icon v-if="!product.isWish" icon="bi:heart" class="bi bi-heart text-gray-500"></Icon>
|
79
|
-
<Icon v-else icon="bi:heart-fill" class="bi bi-heart-fill text-[#FE374F]"></Icon>
|
84
|
+
<Icon v-if="!product.isWish" icon="bi:heart" class="bi bi-heart text-gray-500 text-lg"></Icon>
|
85
|
+
<Icon v-else icon="bi:heart-fill" class="bi bi-heart-fill text-[#FE374F] text-lg"></Icon>
|
80
86
|
</XButton>
|
81
87
|
</div>
|
82
88
|
</div>
|
@@ -382,5 +388,32 @@ const handleLike = (product: Product) => {
|
|
382
388
|
break-inside: avoid;
|
383
389
|
page-break-inside: avoid;
|
384
390
|
}
|
391
|
+
|
392
|
+
/* 光效蒙版动画 */
|
393
|
+
.light-effect {
|
394
|
+
position: absolute;
|
395
|
+
top: 0;
|
396
|
+
left: 0;
|
397
|
+
width: 100%;
|
398
|
+
height: 100%;
|
399
|
+
background: linear-gradient(
|
400
|
+
135deg,
|
401
|
+
transparent 0%,
|
402
|
+
transparent 30%,
|
403
|
+
rgba(255, 255, 255, 0.1) 50%,
|
404
|
+
rgba(255, 255, 255, 0.3) 60%,
|
405
|
+
rgba(255, 255, 255, 0.1) 70%,
|
406
|
+
transparent 100%
|
407
|
+
);
|
408
|
+
transform: translateX(-100%) translateY(-100%);
|
409
|
+
transition: transform 0.3s ease-out;
|
410
|
+
pointer-events: none;
|
411
|
+
z-index: 1;
|
412
|
+
}
|
413
|
+
|
414
|
+
/* 只在悬停图片容器时触发光效动画 */
|
415
|
+
.image-container:hover .light-effect {
|
416
|
+
transform: translateX(100%) translateY(100%);
|
417
|
+
}
|
385
418
|
</style>
|
386
419
|
|
@@ -0,0 +1,234 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="switch" :class="[`size-${size}`, { 'disabled': disabled }]" :style="colorStyle">
|
3
|
+
<input
|
4
|
+
:id="switchId"
|
5
|
+
:checked="modelValue"
|
6
|
+
@change="handleChange"
|
7
|
+
:disabled="disabled"
|
8
|
+
type="checkbox"
|
9
|
+
>
|
10
|
+
<label :for="switchId" class="slider"></label>
|
11
|
+
</div>
|
12
|
+
</template>
|
13
|
+
|
14
|
+
<script setup lang="ts">
|
15
|
+
import { computed } from 'vue'
|
16
|
+
|
17
|
+
/**
|
18
|
+
* Switch 开关组件
|
19
|
+
* @displayName XSwitch
|
20
|
+
*/
|
21
|
+
|
22
|
+
interface Props {
|
23
|
+
modelValue: boolean
|
24
|
+
disabled?: boolean
|
25
|
+
size?: 'small' | 'medium' | 'large'
|
26
|
+
color?: string
|
27
|
+
shadowColor?: string
|
28
|
+
thumbColor?: string
|
29
|
+
}
|
30
|
+
|
31
|
+
const props = withDefaults(defineProps<Props>(), {
|
32
|
+
disabled: false,
|
33
|
+
size: 'medium',
|
34
|
+
color: '#0974f1',
|
35
|
+
shadowColor: '#0974f1',
|
36
|
+
thumbColor: '#ffffff'
|
37
|
+
})
|
38
|
+
|
39
|
+
const emit = defineEmits<{
|
40
|
+
(e: 'update:modelValue', value: boolean): void
|
41
|
+
}>()
|
42
|
+
|
43
|
+
// 生成唯一的 ID 避免冲突
|
44
|
+
const switchId = computed(() => `switch-${Math.random().toString(36).substr(2, 9)}`)
|
45
|
+
|
46
|
+
// 计算颜色相关的样式
|
47
|
+
const colorStyle = computed(() => {
|
48
|
+
const color = props.color || '#0974f1'
|
49
|
+
const shadowColor = props.shadowColor || color
|
50
|
+
const thumbColor = props.thumbColor || '#ffffff'
|
51
|
+
|
52
|
+
// 计算浅色背景
|
53
|
+
const lightBackgroundColor = lightenColor(color, 0.9)
|
54
|
+
// 计算激活状态的浅色版本
|
55
|
+
const activeColor = lightenColor(color, 0.2)
|
56
|
+
|
57
|
+
return {
|
58
|
+
'--switch-color': color,
|
59
|
+
'--switch-color-rgb': hexToRgb(color),
|
60
|
+
'--switch-shadow-color': shadowColor,
|
61
|
+
'--switch-shadow-color-rgb': hexToRgb(shadowColor),
|
62
|
+
'--switch-thumb-color': thumbColor,
|
63
|
+
'--switch-bg-color': lightBackgroundColor,
|
64
|
+
'--switch-active-color': activeColor
|
65
|
+
}
|
66
|
+
})
|
67
|
+
|
68
|
+
// 将十六进制颜色转换为 RGB
|
69
|
+
const hexToRgb = (hex: string) => {
|
70
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
71
|
+
if (result) {
|
72
|
+
const r = parseInt(result[1], 16)
|
73
|
+
const g = parseInt(result[2], 16)
|
74
|
+
const b = parseInt(result[3], 16)
|
75
|
+
return `${r}, ${g}, ${b}`
|
76
|
+
}
|
77
|
+
return '9, 117, 241' // 默认蓝色
|
78
|
+
}
|
79
|
+
|
80
|
+
// 将颜色变浅
|
81
|
+
const lightenColor = (hex: string, factor: number) => {
|
82
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
83
|
+
if (result) {
|
84
|
+
const r = parseInt(result[1], 16)
|
85
|
+
const g = parseInt(result[2], 16)
|
86
|
+
const b = parseInt(result[3], 16)
|
87
|
+
|
88
|
+
// 计算浅色版本
|
89
|
+
const lightR = Math.round(r + (255 - r) * factor)
|
90
|
+
const lightG = Math.round(g + (255 - g) * factor)
|
91
|
+
const lightB = Math.round(b + (255 - b) * factor)
|
92
|
+
|
93
|
+
return `#${lightR.toString(16).padStart(2, '0')}${lightG.toString(16).padStart(2, '0')}${lightB.toString(16).padStart(2, '0')}`
|
94
|
+
}
|
95
|
+
return '#e5e7eb' // 默认浅灰色
|
96
|
+
}
|
97
|
+
|
98
|
+
const handleChange = () => {
|
99
|
+
if (!props.disabled) {
|
100
|
+
emit('update:modelValue', !props.modelValue)
|
101
|
+
}
|
102
|
+
}
|
103
|
+
</script>
|
104
|
+
|
105
|
+
<style scoped>
|
106
|
+
/* The switch - the box around the slider */
|
107
|
+
.switch {
|
108
|
+
font-size: 17px;
|
109
|
+
position: relative;
|
110
|
+
display: inline-block;
|
111
|
+
width: 3.5em;
|
112
|
+
height: 2em;
|
113
|
+
}
|
114
|
+
|
115
|
+
/* Hide default HTML checkbox */
|
116
|
+
.switch input {
|
117
|
+
opacity: 0;
|
118
|
+
width: 0;
|
119
|
+
height: 0;
|
120
|
+
}
|
121
|
+
|
122
|
+
/* The slider */
|
123
|
+
.slider {
|
124
|
+
position: absolute;
|
125
|
+
cursor: pointer;
|
126
|
+
inset: 0;
|
127
|
+
border: none;
|
128
|
+
border-radius: 50px;
|
129
|
+
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
130
|
+
background-color: var(--switch-bg-color);
|
131
|
+
}
|
132
|
+
|
133
|
+
.slider:before {
|
134
|
+
position: absolute;
|
135
|
+
content: "";
|
136
|
+
height: 1.4em;
|
137
|
+
width: 1.4em;
|
138
|
+
left: 0.2em;
|
139
|
+
bottom: 0.2em;
|
140
|
+
background-color: var(--switch-thumb-color);
|
141
|
+
border-radius: inherit;
|
142
|
+
transition: all 0.4s cubic-bezier(0.23, 1, 0.320, 1);
|
143
|
+
}
|
144
|
+
|
145
|
+
.switch input:checked + .slider {
|
146
|
+
box-shadow: 0 0 20px rgba(var(--switch-shadow-color-rgb), 0.8);
|
147
|
+
border: 2px solid var(--switch-active-color);
|
148
|
+
background-color: var(--switch-active-color);
|
149
|
+
}
|
150
|
+
|
151
|
+
.switch input:checked + .slider:before {
|
152
|
+
transform: translateX(1.5em);
|
153
|
+
}
|
154
|
+
|
155
|
+
/* 尺寸变体 */
|
156
|
+
.switch.size-small {
|
157
|
+
width: 2.5em;
|
158
|
+
height: 1.4em;
|
159
|
+
font-size: 14px;
|
160
|
+
}
|
161
|
+
|
162
|
+
.switch.size-small .slider:before {
|
163
|
+
height: 1em;
|
164
|
+
width: 1em;
|
165
|
+
}
|
166
|
+
|
167
|
+
.switch.size-small input:checked + .slider:before {
|
168
|
+
transform: translateX(1.1em);
|
169
|
+
}
|
170
|
+
|
171
|
+
.switch.size-medium {
|
172
|
+
width: 3.5em;
|
173
|
+
height: 2em;
|
174
|
+
font-size: 17px;
|
175
|
+
}
|
176
|
+
|
177
|
+
.switch.size-large {
|
178
|
+
width: 4.5em;
|
179
|
+
height: 2.6em;
|
180
|
+
font-size: 20px;
|
181
|
+
}
|
182
|
+
|
183
|
+
.switch.size-large .slider:before {
|
184
|
+
height: 1.8em;
|
185
|
+
width: 1.8em;
|
186
|
+
}
|
187
|
+
|
188
|
+
.switch.size-large input:checked + .slider:before {
|
189
|
+
transform: translateX(1.9em);
|
190
|
+
}
|
191
|
+
|
192
|
+
/* 禁用状态 */
|
193
|
+
.switch.disabled .slider {
|
194
|
+
cursor: not-allowed;
|
195
|
+
opacity: 0.5;
|
196
|
+
border-color: #9ca3af;
|
197
|
+
}
|
198
|
+
|
199
|
+
.switch.disabled .slider:before {
|
200
|
+
background-color: #9ca3af;
|
201
|
+
}
|
202
|
+
|
203
|
+
.switch.disabled input:checked + .slider {
|
204
|
+
border-color: #9ca3af;
|
205
|
+
box-shadow: none;
|
206
|
+
}
|
207
|
+
|
208
|
+
/* 悬停效果 */
|
209
|
+
.switch:not(.disabled):hover .slider {
|
210
|
+
box-shadow: 0 0 20px rgba(var(--switch-shadow-color-rgb), 0.8);
|
211
|
+
}
|
212
|
+
|
213
|
+
.switch:not(.disabled) input:checked + .slider:hover {
|
214
|
+
box-shadow: 0 0 20px rgba(var(--switch-shadow-color-rgb), 0.8);
|
215
|
+
}
|
216
|
+
|
217
|
+
/* 点击后移除边框 */
|
218
|
+
.switch input:active + .slider {
|
219
|
+
box-shadow: none;
|
220
|
+
}
|
221
|
+
|
222
|
+
.switch input:checked:active + .slider {
|
223
|
+
box-shadow: 0 0 20px rgba(var(--switch-shadow-color-rgb), 0.8);
|
224
|
+
}
|
225
|
+
|
226
|
+
/* 焦点状态 */
|
227
|
+
.switch input:focus-visible + .slider {
|
228
|
+
box-shadow: 0 0 0 3px rgba(var(--switch-shadow-color-rgb), 0.3);
|
229
|
+
}
|
230
|
+
|
231
|
+
.switch input:checked:focus-visible + .slider {
|
232
|
+
box-shadow: 0 0 20px rgba(var(--switch-shadow-color-rgb), 0.8), 0 0 0 3px rgba(var(--switch-shadow-color-rgb), 0.3);
|
233
|
+
}
|
234
|
+
</style>
|
package/src/index.ts
CHANGED
@@ -13,6 +13,7 @@ export { default as XThumbnailContainer } from './components/ThumbnailContainer/
|
|
13
13
|
export { default as XSkeleton } from './components/Skeleton/index.vue'
|
14
14
|
export { default as XModal } from './components/Modal/index.vue'
|
15
15
|
export { default as XTooltips } from './components/Tooltips/index.vue'
|
16
|
+
export { default as XSwitch } from './components/Switch/index.vue'
|
16
17
|
export { default as XScore } from './components/Score/index.vue'
|
17
18
|
export { default as XRadio } from './components/Radio/index.vue'
|
18
19
|
export { default as XDateTimePicker } from './components/DateTimePicker/index.vue'
|