@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.
- package/README.md +55 -0
- package/dist/index.css +1 -0
- package/dist/index.js +128 -0
- package/dist/index.mjs +4819 -0
- package/package.json +57 -0
- package/src/components/Accordion/index.vue +355 -0
- package/src/components/Button/index.vue +440 -0
- package/src/components/Card/index.vue +386 -0
- package/src/components/Checkboxes/index.vue +416 -0
- package/src/components/CountrySelect/data/countries.json +2084 -0
- package/src/components/CountrySelect/index.vue +319 -0
- package/src/components/Input/index.vue +293 -0
- package/src/components/Modal/index.vue +360 -0
- package/src/components/Select/index.vue +411 -0
- package/src/components/Skeleton/index.vue +110 -0
- package/src/components/ThumbnailContainer/index.vue +451 -0
- package/src/composables/Msg.ts +349 -0
- package/src/index.ts +28 -0
- package/src/styles/theme.css +120 -0
@@ -0,0 +1,451 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="thumbnail-container-wrapper relative">
|
3
|
+
<!-- 左侧滚动按钮 -->
|
4
|
+
<button
|
5
|
+
v-if="showLeftArrow"
|
6
|
+
@click="scrollLeft"
|
7
|
+
class="scroll-arrow scroll-arrow-left"
|
8
|
+
aria-label="向左滚动"
|
9
|
+
>
|
10
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
11
|
+
<path d="M15 18l-6-6 6-6" />
|
12
|
+
</svg>
|
13
|
+
</button>
|
14
|
+
|
15
|
+
<!-- 缩略图容器 -->
|
16
|
+
<div
|
17
|
+
ref="thumbnailContainer"
|
18
|
+
@scroll="handleThumbnailScroll"
|
19
|
+
class="thumbnail-scroll-container"
|
20
|
+
>
|
21
|
+
<div
|
22
|
+
v-for="(img, index) in images"
|
23
|
+
:key="index"
|
24
|
+
:ref="el => setThumbnailRef(el, index)"
|
25
|
+
@mouseenter="handleImageHover(index)"
|
26
|
+
@mouseleave="handleImageLeave()"
|
27
|
+
@click="handleThumbnailClick(index)"
|
28
|
+
class="thumbnail-item"
|
29
|
+
:class="{
|
30
|
+
'thumbnail-item--active': currentIndex === index,
|
31
|
+
'thumbnail-item--inactive': currentIndex !== index
|
32
|
+
}"
|
33
|
+
>
|
34
|
+
<img
|
35
|
+
:src="img"
|
36
|
+
:alt="`缩略图 ${index + 1}`"
|
37
|
+
class="thumbnail-image"
|
38
|
+
:class="{
|
39
|
+
'thumbnail-image--active': currentIndex === index,
|
40
|
+
'thumbnail-image--inactive': currentIndex !== index
|
41
|
+
}"
|
42
|
+
>
|
43
|
+
|
44
|
+
<!-- 图片序号 -->
|
45
|
+
<div class="thumbnail-index">
|
46
|
+
{{ index + 1 }}
|
47
|
+
</div>
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
|
51
|
+
<!-- 右侧滚动按钮 -->
|
52
|
+
<button
|
53
|
+
v-if="showRightArrow"
|
54
|
+
@click="scrollRight"
|
55
|
+
class="scroll-arrow scroll-arrow-right"
|
56
|
+
aria-label="向右滚动"
|
57
|
+
>
|
58
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
59
|
+
<path d="M9 18l6-6-6-6" />
|
60
|
+
</svg>
|
61
|
+
</button>
|
62
|
+
</div>
|
63
|
+
</template>
|
64
|
+
|
65
|
+
<script setup lang="ts">
|
66
|
+
import { ref, onMounted, nextTick, watch } from 'vue'
|
67
|
+
import type { ComponentPublicInstance } from 'vue'
|
68
|
+
|
69
|
+
/**
|
70
|
+
* ThumbnailContainer 缩略图容器组件
|
71
|
+
* @displayName XThumbnailContainer
|
72
|
+
*/
|
73
|
+
|
74
|
+
export interface ThumbnailContainerProps {
|
75
|
+
images: string[]
|
76
|
+
modelValue?: number
|
77
|
+
autoScroll?: boolean
|
78
|
+
showIndex?: boolean
|
79
|
+
}
|
80
|
+
|
81
|
+
const props = withDefaults(defineProps<ThumbnailContainerProps>(), {
|
82
|
+
images: () => [],
|
83
|
+
modelValue: 0,
|
84
|
+
autoScroll: true,
|
85
|
+
showIndex: true
|
86
|
+
})
|
87
|
+
|
88
|
+
const emit = defineEmits<{
|
89
|
+
'update:modelValue': [index: number]
|
90
|
+
'change': [index: number]
|
91
|
+
'hover': [index: number]
|
92
|
+
}>()
|
93
|
+
|
94
|
+
// 响应式状态
|
95
|
+
const currentIndex = ref(props.modelValue)
|
96
|
+
const thumbnailContainer = ref<HTMLDivElement | null>(null)
|
97
|
+
const thumbnailRefs = ref<HTMLElement[]>([])
|
98
|
+
const canScrollLeft = ref(false)
|
99
|
+
const canScrollRight = ref(false)
|
100
|
+
const scrollProgress = ref(0)
|
101
|
+
const showLeftArrow = ref(false)
|
102
|
+
const showRightArrow = ref(false)
|
103
|
+
// 处理滚动事件
|
104
|
+
const handleThumbnailScroll = () => {
|
105
|
+
updateScrollState()
|
106
|
+
}
|
107
|
+
|
108
|
+
// 设置缩略图 ref
|
109
|
+
const setThumbnailRef = (el: Element | ComponentPublicInstance | null, index: number) => {
|
110
|
+
if (el && el instanceof HTMLElement) {
|
111
|
+
thumbnailRefs.value[index] = el
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
// 处理鼠标悬停
|
116
|
+
const handleImageHover = (index: number) => {
|
117
|
+
emit('hover', index)
|
118
|
+
}
|
119
|
+
|
120
|
+
// 处理鼠标离开
|
121
|
+
const handleImageLeave = () => {
|
122
|
+
// 可以在这里添加悬停结束的逻辑
|
123
|
+
}
|
124
|
+
|
125
|
+
// 处理缩略图点击
|
126
|
+
const handleThumbnailClick = (index: number) => {
|
127
|
+
currentIndex.value = index
|
128
|
+
emit('update:modelValue', index)
|
129
|
+
emit('change', index)
|
130
|
+
|
131
|
+
// 自动滚动到合适位置
|
132
|
+
if (props.autoScroll) {
|
133
|
+
nextTick(() => {
|
134
|
+
scrollThumbnailToThirdPosition(index)
|
135
|
+
setTimeout(() => {
|
136
|
+
updateScrollState()
|
137
|
+
}, 300)
|
138
|
+
})
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
// 左滚动
|
143
|
+
const scrollLeft = () => {
|
144
|
+
if (!thumbnailContainer.value) return
|
145
|
+
const container = thumbnailContainer.value
|
146
|
+
const scrollAmount = container.clientWidth * 0.8
|
147
|
+
container.scrollBy({
|
148
|
+
left: -scrollAmount,
|
149
|
+
behavior: 'smooth'
|
150
|
+
})
|
151
|
+
}
|
152
|
+
|
153
|
+
// 右滚动
|
154
|
+
const scrollRight = () => {
|
155
|
+
if (!thumbnailContainer.value) return
|
156
|
+
const container = thumbnailContainer.value
|
157
|
+
const scrollAmount = container.clientWidth * 0.8
|
158
|
+
container.scrollBy({
|
159
|
+
left: scrollAmount,
|
160
|
+
behavior: 'smooth'
|
161
|
+
})
|
162
|
+
}
|
163
|
+
|
164
|
+
|
165
|
+
// 滚动缩略图到第3个位置
|
166
|
+
const scrollThumbnailToThirdPosition = (clickedIndex: number) => {
|
167
|
+
if (!thumbnailContainer.value || thumbnailRefs.value.length === 0) return
|
168
|
+
|
169
|
+
const container = thumbnailContainer.value
|
170
|
+
const clickedItem = thumbnailRefs.value[clickedIndex]
|
171
|
+
|
172
|
+
if (!clickedItem) return
|
173
|
+
|
174
|
+
// 计算目标滚动位置:点击项移动到第3个位置(索引2)
|
175
|
+
const targetPosition = 2 // 第3个位置的索引
|
176
|
+
|
177
|
+
// 获取缩略图的宽度和间距
|
178
|
+
const itemWidth = clickedItem.offsetWidth
|
179
|
+
const gap = 8 // 默认间距为8px
|
180
|
+
|
181
|
+
// 计算每个项目的总宽度(包括间距)
|
182
|
+
const itemTotalWidth = itemWidth + gap
|
183
|
+
|
184
|
+
// 计算目标滚动距离
|
185
|
+
// 如果点击的是前3个项目,滚动到开始位置
|
186
|
+
// 否则,将点击项滚动到第3个位置
|
187
|
+
let targetScrollLeft = 0
|
188
|
+
|
189
|
+
if (clickedIndex >= targetPosition) {
|
190
|
+
// 将点击项滚动到第3个位置
|
191
|
+
targetScrollLeft = (clickedIndex - targetPosition) * itemTotalWidth
|
192
|
+
}
|
193
|
+
|
194
|
+
// 边界处理:确保不会滚动超出范围
|
195
|
+
const maxScrollLeft = container.scrollWidth - container.clientWidth
|
196
|
+
targetScrollLeft = Math.max(0, Math.min(targetScrollLeft, maxScrollLeft))
|
197
|
+
// 平滑滚动
|
198
|
+
container.scrollTo({
|
199
|
+
left: targetScrollLeft,
|
200
|
+
behavior: 'smooth'
|
201
|
+
})
|
202
|
+
}
|
203
|
+
// 更新滚动状态
|
204
|
+
const updateScrollState = () => {
|
205
|
+
if (!thumbnailContainer.value) return
|
206
|
+
|
207
|
+
const container = thumbnailContainer.value
|
208
|
+
const scrollLeft = container.scrollLeft
|
209
|
+
const maxScrollLeft = container.scrollWidth - container.clientWidth
|
210
|
+
|
211
|
+
canScrollLeft.value = scrollLeft > 0
|
212
|
+
canScrollRight.value = scrollLeft < maxScrollLeft
|
213
|
+
|
214
|
+
// 计算滚动进度
|
215
|
+
if (maxScrollLeft > 0) {
|
216
|
+
scrollProgress.value = (scrollLeft / maxScrollLeft) * 100
|
217
|
+
} else {
|
218
|
+
scrollProgress.value = 0
|
219
|
+
}
|
220
|
+
|
221
|
+
// 判断是否显示箭头
|
222
|
+
const visibleCount = getVisibleThumbnailCount()
|
223
|
+
showLeftArrow.value = props.images.length > visibleCount && canScrollLeft.value
|
224
|
+
showRightArrow.value = props.images.length > visibleCount && canScrollRight.value
|
225
|
+
}
|
226
|
+
|
227
|
+
// 获取可见缩略图数量
|
228
|
+
const getVisibleThumbnailCount = () => {
|
229
|
+
if (!thumbnailContainer.value || thumbnailRefs.value.length === 0) return 4
|
230
|
+
|
231
|
+
const container = thumbnailContainer.value
|
232
|
+
const containerWidth = container.clientWidth - 32
|
233
|
+
const itemWidth = thumbnailRefs.value[0]?.offsetWidth || 80
|
234
|
+
const gap = 12 // gap-3 对应12px
|
235
|
+
const itemTotalWidth = itemWidth + gap
|
236
|
+
|
237
|
+
return Math.floor(containerWidth / itemTotalWidth)
|
238
|
+
}
|
239
|
+
|
240
|
+
// 切换到指定索引
|
241
|
+
const setCurrentIndex = (index: number) => {
|
242
|
+
if (index >= 0 && index < props.images.length) {
|
243
|
+
handleThumbnailClick(index)
|
244
|
+
}
|
245
|
+
}
|
246
|
+
|
247
|
+
// 监听 modelValue 变化
|
248
|
+
watch(() => props.modelValue, (newValue) => {
|
249
|
+
if (newValue !== undefined && newValue !== currentIndex.value) {
|
250
|
+
currentIndex.value = newValue
|
251
|
+
if (props.autoScroll) {
|
252
|
+
nextTick(() => {
|
253
|
+
scrollThumbnailToThirdPosition(newValue)
|
254
|
+
})
|
255
|
+
}
|
256
|
+
}
|
257
|
+
})
|
258
|
+
|
259
|
+
// 监听 images 变化
|
260
|
+
watch(() => props.images, () => {
|
261
|
+
nextTick(() => {
|
262
|
+
updateScrollState()
|
263
|
+
})
|
264
|
+
}, { deep: true })
|
265
|
+
|
266
|
+
onMounted(() => {
|
267
|
+
nextTick(() => {
|
268
|
+
updateScrollState()
|
269
|
+
})
|
270
|
+
})
|
271
|
+
|
272
|
+
// 暴露方法
|
273
|
+
defineExpose({
|
274
|
+
setCurrentIndex,
|
275
|
+
scrollLeft,
|
276
|
+
scrollRight,
|
277
|
+
updateScrollState
|
278
|
+
})
|
279
|
+
</script>
|
280
|
+
|
281
|
+
<style scoped>
|
282
|
+
.thumbnail-container-wrapper {
|
283
|
+
position: relative;
|
284
|
+
width: 100%;
|
285
|
+
}
|
286
|
+
|
287
|
+
/* 滚动容器 */
|
288
|
+
.thumbnail-scroll-container {
|
289
|
+
display: flex;
|
290
|
+
overflow-x: auto;
|
291
|
+
gap: 0.75rem; /* 12px */
|
292
|
+
padding: 0.5rem 1rem;
|
293
|
+
scroll-behavior: smooth;
|
294
|
+
scrollbar-width: none;
|
295
|
+
-ms-overflow-style: none;
|
296
|
+
}
|
297
|
+
|
298
|
+
.thumbnail-scroll-container::-webkit-scrollbar {
|
299
|
+
display: none;
|
300
|
+
}
|
301
|
+
|
302
|
+
/* 缩略图项 */
|
303
|
+
.thumbnail-item {
|
304
|
+
position: relative;
|
305
|
+
width: 80px;
|
306
|
+
height: 80px;
|
307
|
+
border: 2px solid var(--x-color-gray-300, #d1d5db);
|
308
|
+
border-radius: var(--x-radius-lg, 12px);
|
309
|
+
cursor: pointer;
|
310
|
+
flex-shrink: 0;
|
311
|
+
transition: all var(--x-transition, 0.2s ease);
|
312
|
+
overflow: hidden;
|
313
|
+
}
|
314
|
+
|
315
|
+
/* 移除向下动画,只保留边框和阴影变化 */
|
316
|
+
.thumbnail-item:hover {
|
317
|
+
border-color: var(--x-color-gray-400, #9ca3af);
|
318
|
+
box-shadow: var(--x-shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));
|
319
|
+
}
|
320
|
+
|
321
|
+
.thumbnail-item--active {
|
322
|
+
border-color: var(--x-color-primary, #1a1a1a);
|
323
|
+
box-shadow: 0 0 0 1px var(--x-color-primary, #1a1a1a),
|
324
|
+
var(--x-shadow-lg, 0 10px 15px rgba(0, 0, 0, 0.1));
|
325
|
+
}
|
326
|
+
|
327
|
+
.thumbnail-item--inactive {
|
328
|
+
border-color: var(--x-color-gray-300, #d1d5db);
|
329
|
+
}
|
330
|
+
|
331
|
+
/* 缩略图图片 */
|
332
|
+
.thumbnail-image {
|
333
|
+
width: 100%;
|
334
|
+
height: 100%;
|
335
|
+
object-fit: cover;
|
336
|
+
pointer-events: none;
|
337
|
+
transition: opacity var(--x-transition, 0.2s ease);
|
338
|
+
}
|
339
|
+
|
340
|
+
.thumbnail-image--active {
|
341
|
+
opacity: 1;
|
342
|
+
}
|
343
|
+
|
344
|
+
.thumbnail-image--inactive {
|
345
|
+
opacity: 0.75;
|
346
|
+
}
|
347
|
+
|
348
|
+
/* 图片序号 */
|
349
|
+
.thumbnail-index {
|
350
|
+
position: absolute;
|
351
|
+
bottom: 0.25rem;
|
352
|
+
left: 0.25rem;
|
353
|
+
background-color: rgba(0, 0, 0, 0.6);
|
354
|
+
color: white;
|
355
|
+
font-size: var(--x-font-size-xs, 12px);
|
356
|
+
padding: 0.125rem 0.375rem;
|
357
|
+
border-radius: var(--x-radius-sm, 4px);
|
358
|
+
line-height: 1;
|
359
|
+
}
|
360
|
+
|
361
|
+
/* 滚动按钮 - 去掉 active 向下动画 */
|
362
|
+
.scroll-arrow {
|
363
|
+
position: absolute;
|
364
|
+
top: 50%;
|
365
|
+
transform: translateY(-50%); /* 只保持垂直居中,不再有 active 时的 translateY 变化 */
|
366
|
+
z-index: 10;
|
367
|
+
display: flex;
|
368
|
+
align-items: center;
|
369
|
+
justify-content: center;
|
370
|
+
width: 40px;
|
371
|
+
height: 40px;
|
372
|
+
background: rgba(255, 255, 255, 0.95);
|
373
|
+
border: 1px solid var(--x-color-gray-200, #e5e7eb);
|
374
|
+
border-radius: 50%;
|
375
|
+
cursor: pointer;
|
376
|
+
transition: all var(--x-transition-fast, 0.15s ease);
|
377
|
+
box-shadow: var(--x-shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.05));
|
378
|
+
}
|
379
|
+
|
380
|
+
.scroll-arrow:hover {
|
381
|
+
background: white;
|
382
|
+
border-color: var(--x-color-primary, #1a1a1a);
|
383
|
+
box-shadow: var(--x-shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));
|
384
|
+
}
|
385
|
+
|
386
|
+
/* 移除 active 状态的向下动画 */
|
387
|
+
.scroll-arrow:active {
|
388
|
+
/* 不再改变 transform,只改变背景色 */
|
389
|
+
background: var(--x-color-gray-100, #f3f4f6);
|
390
|
+
}
|
391
|
+
|
392
|
+
.scroll-arrow:focus-visible {
|
393
|
+
outline: 2px solid var(--x-color-primary, #1a1a1a);
|
394
|
+
outline-offset: 2px;
|
395
|
+
}
|
396
|
+
|
397
|
+
.scroll-arrow-left {
|
398
|
+
left: 0.5rem;
|
399
|
+
}
|
400
|
+
|
401
|
+
.scroll-arrow-right {
|
402
|
+
right: 0.5rem;
|
403
|
+
}
|
404
|
+
|
405
|
+
.scroll-arrow svg {
|
406
|
+
width: 24px;
|
407
|
+
height: 24px;
|
408
|
+
color: var(--x-color-gray-700, #374151);
|
409
|
+
transition: color var(--x-transition-fast, 0.15s ease);
|
410
|
+
}
|
411
|
+
|
412
|
+
.scroll-arrow:hover svg {
|
413
|
+
color: var(--x-color-primary, #1a1a1a);
|
414
|
+
}
|
415
|
+
|
416
|
+
/* 响应式设计 */
|
417
|
+
@media (min-width: 640px) {
|
418
|
+
.thumbnail-item {
|
419
|
+
width: 96px;
|
420
|
+
height: 96px;
|
421
|
+
}
|
422
|
+
}
|
423
|
+
|
424
|
+
@media (min-width: 1024px) {
|
425
|
+
.thumbnail-item {
|
426
|
+
width: 123px;
|
427
|
+
height: 122px;
|
428
|
+
}
|
429
|
+
}
|
430
|
+
|
431
|
+
@media (max-width: 640px) {
|
432
|
+
.scroll-arrow {
|
433
|
+
width: 36px;
|
434
|
+
height: 36px;
|
435
|
+
}
|
436
|
+
|
437
|
+
.scroll-arrow svg {
|
438
|
+
width: 20px;
|
439
|
+
height: 20px;
|
440
|
+
}
|
441
|
+
}
|
442
|
+
|
443
|
+
/* 优化动画性能 */
|
444
|
+
@media (prefers-reduced-motion: reduce) {
|
445
|
+
.thumbnail-item,
|
446
|
+
.thumbnail-image,
|
447
|
+
.scroll-arrow {
|
448
|
+
transition: none;
|
449
|
+
}
|
450
|
+
}
|
451
|
+
</style>
|