@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xlui/xux-ui",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "VUE3 电商组件库",
5
5
  "author": "leheya",
6
6
  "license": "MIT",
@@ -10,12 +10,16 @@
10
10
  >
11
11
  <div class="relative p-2">
12
12
  <!-- 瀑布流保持自然高度 -->
13
- <img
14
- :src="product.image"
15
- :alt="product.name"
16
- loading="lazy"
17
- class="w-full h-auto rounded-lg bg-gray-100 object-cover transition-transform duration-300 group-hover:scale-[1.01]"
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 group-hover:scale-105 transition-transform duration-300"
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'