im-ui-mobile 0.1.0 → 0.1.2

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.
Files changed (85) hide show
  1. package/components/im-avatar/im-avatar.vue +7 -7
  2. package/components/im-badge/im-badge.vue +326 -0
  3. package/components/im-button/im-button.vue +71 -34
  4. package/components/im-card/im-card.vue +563 -0
  5. package/components/im-chat-item/im-chat-item.vue +5 -4
  6. package/components/im-col/im-col.vue +191 -0
  7. package/components/im-dialog/im-dialog.vue +543 -0
  8. package/components/im-double-tap-view/im-double-tap-view.vue +93 -0
  9. package/components/im-emoji-picker/im-emoji-picker.vue +1143 -0
  10. package/components/im-friend-item/im-friend-item.vue +1 -1
  11. package/components/im-group-item/im-group-item.vue +1 -1
  12. package/components/im-group-member-selector/im-group-member-selector.vue +5 -5
  13. package/components/im-group-rtc-join/im-group-rtc-join.vue +8 -8
  14. package/components/im-icon/im-icon.vue +593 -0
  15. package/components/im-image-upload/im-image-upload.vue +0 -2
  16. package/components/im-link/im-link.vue +628 -0
  17. package/components/im-loading/im-loading.vue +13 -4
  18. package/components/im-mention-picker/im-mention-picker.vue +8 -7
  19. package/components/im-message-action/im-message-action.vue +678 -0
  20. package/components/im-message-item/im-message-item.vue +28 -26
  21. package/components/im-message-list/im-message-list.vue +1108 -0
  22. package/components/im-modal/im-modal.vue +373 -0
  23. package/components/im-nav-bar/im-nav-bar.vue +689 -75
  24. package/components/im-parse/im-parse.vue +1054 -0
  25. package/components/im-popup/im-popup.vue +467 -0
  26. package/components/im-read-receipt/im-read-receipt.vue +10 -10
  27. package/components/im-row/im-row.vue +189 -0
  28. package/components/im-search/im-search.vue +762 -0
  29. package/components/im-sku/im-sku.vue +720 -0
  30. package/components/im-sku/utils/helper.ts +182 -0
  31. package/components/im-stepper/im-stepper.vue +585 -0
  32. package/components/im-stepper/utils/helper.ts +167 -0
  33. package/components/im-tabs/im-tabs.vue +1022 -0
  34. package/components/im-tabs/tabs-navigation.vue +489 -0
  35. package/components/im-tabs/utils/helper.ts +181 -0
  36. package/components/im-tabs-tab-pane/im-tabs-tab-pane.vue +145 -0
  37. package/components/im-upload/im-upload.vue +1236 -0
  38. package/components/im-voice-input/im-voice-input.vue +1 -1
  39. package/index.js +3 -5
  40. package/index.scss +19 -0
  41. package/libs/emoji-data.ts +229 -0
  42. package/libs/index.ts +16 -16
  43. package/package.json +1 -2
  44. package/styles/button.scss +33 -33
  45. package/theme.scss +2 -2
  46. package/types/components/badge.d.ts +42 -0
  47. package/types/components/button.d.ts +2 -1
  48. package/types/components/card.d.ts +122 -0
  49. package/types/components/col.d.ts +37 -0
  50. package/types/components/dialog.d.ts +125 -0
  51. package/types/components/double-tap-view.d.ts +31 -0
  52. package/types/components/emoji-picker.d.ts +121 -0
  53. package/types/components/group-rtc-join.d.ts +1 -1
  54. package/types/components/icon.d.ts +77 -0
  55. package/types/components/link.d.ts +55 -0
  56. package/types/components/loading.d.ts +1 -0
  57. package/types/components/message-action.d.ts +96 -0
  58. package/types/components/message-item.d.ts +2 -2
  59. package/types/components/message-list.d.ts +136 -0
  60. package/types/components/modal.d.ts +106 -0
  61. package/types/components/nav-bar.d.ts +125 -0
  62. package/types/components/parse.d.ts +90 -0
  63. package/types/components/popup.d.ts +58 -0
  64. package/types/components/row.d.ts +31 -0
  65. package/types/components/search.d.ts +54 -0
  66. package/types/components/sku.d.ts +195 -0
  67. package/types/components/stepper.d.ts +99 -0
  68. package/types/components/tabs-tab-pane.d.ts +27 -0
  69. package/types/components/tabs.d.ts +117 -0
  70. package/types/components/upload.d.ts +137 -0
  71. package/types/components.d.ts +19 -1
  72. package/types/index.d.ts +38 -1
  73. package/types/libs/index.d.ts +10 -10
  74. package/types/utils/base64.d.ts +5 -0
  75. package/types/utils/dom.d.ts +3 -0
  76. package/types/utils/enums.d.ts +4 -5
  77. package/types/utils/validator.d.ts +74 -0
  78. package/utils/base64.js +18 -0
  79. package/utils/dom.js +353 -1
  80. package/utils/enums.js +4 -5
  81. package/utils/validator.js +230 -0
  82. package/components/im-file-upload/im-file-upload.vue +0 -309
  83. package/plugins/uview-plus.js +0 -29
  84. package/types/components/arrow-bar.d.ts +0 -14
  85. package/types/components/file-upload.d.ts +0 -58
@@ -0,0 +1,1022 @@
1
+ <!-- components/im-tabs/im-tabs.vue -->
2
+ <template>
3
+ <!-- 粘性容器 -->
4
+ <view v-if="sticky" ref="stickyRef" :class="['im-tabs-sticky', { 'im-tabs-sticky-fixed': isSticky }]"
5
+ :style="stickyStyle">
6
+ <tabs-navigation />
7
+ </view>
8
+
9
+ <!-- 主容器 -->
10
+ <view ref="containerRef" :class="[
11
+ 'im-tabs',
12
+ customClass,
13
+ `im-tabs-${position}`,
14
+ `im-tabs-${type}`,
15
+ {
16
+ 'im-tabs-border': border,
17
+ 'im-tabs-scrollable': scrollable,
18
+ 'im-tabs-centered': centered
19
+ }
20
+ ]" :style="containerStyle" @scroll="onContainerScroll">
21
+ <!-- 非粘性模式下的导航 -->
22
+ <tabs-navigation :currentIndex="currentIndex" v-if="!sticky" @scroll="onNavScroll" />
23
+
24
+ <!-- 标签内容区域 -->
25
+ <view v-if="position === 'top' || position === 'bottom'" ref="contentRef" class="im-tabs-content"
26
+ :class="contentClass" :style="contentStyle" @touchstart="onTouchStart" @touchmove="onTouchMove"
27
+ @touchend="onTouchEnd" @touchcancel="onTouchEnd">
28
+ <!-- 内容滑动容器 -->
29
+ <view ref="swipeRef" class="im-tabs-swipe" :class="{ 'im-tabs-swipe-transition': !isSwiping }"
30
+ :style="swipeStyle">
31
+ <slot />
32
+ </view>
33
+ </view>
34
+
35
+ <!-- 垂直布局的内容区域 -->
36
+ <view v-else ref="contentRef" class="im-tabs-content im-tabs-content-vertical" :class="contentClass"
37
+ :style="contentStyle">
38
+ <slot />
39
+ </view>
40
+ </view>
41
+ </template>
42
+
43
+ <script lang="ts" setup>
44
+ import {
45
+ ref,
46
+ computed,
47
+ watch,
48
+ onMounted,
49
+ onUnmounted,
50
+ provide,
51
+ reactive,
52
+ getCurrentInstance
53
+ } from 'vue'
54
+ import type { TabsProps, TabsEmits, TabItem } from '../../types/components/tabs'
55
+ import TabsNavigation from './tabs-navigation.vue'
56
+
57
+ const props = withDefaults(defineProps<TabsProps>(), {
58
+ modelValue: 0,
59
+ items: () => [],
60
+ position: 'top',
61
+ type: 'line',
62
+ swipeable: true,
63
+ showSlider: true,
64
+ sliderWidth: 'auto',
65
+ sliderHeight: 2,
66
+ sliderColor: '#1989fa',
67
+ border: true,
68
+ background: '#ffffff',
69
+ height: 44,
70
+ minTabWidth: 0,
71
+ maxTabWidth: 0,
72
+ sticky: false,
73
+ stickyOffsetTop: 0,
74
+ scrollable: false,
75
+ centered: false,
76
+ keepAlive: true,
77
+ lazy: false,
78
+ duration: 300,
79
+ scrollOnInit: true,
80
+ gutter: 0,
81
+ showIcon: false,
82
+ iconPosition: 'left',
83
+ animation: 'slide'
84
+ })
85
+
86
+ const emit = defineEmits<TabsEmits>()
87
+ const instance = getCurrentInstance()
88
+
89
+ // 响应式数据
90
+ const currentIndex = ref(props.modelValue)
91
+ const isSwiping = ref(false)
92
+ const startX = ref(0)
93
+ const deltaX = ref(0)
94
+ const sliderLeft = ref(0)
95
+ const sliderWidth = ref(0)
96
+ const isSticky = ref(false)
97
+ const scrollLeft = ref(0)
98
+ const changeTimer = ref<number | null>(null)
99
+ const updateTimer = ref<number | null>(null)
100
+
101
+ // 计算属性
102
+ const currentActiveKey = computed(() => {
103
+ const tab = props.items[currentIndex.value]
104
+ return tab?.id ?? currentIndex.value
105
+ })
106
+
107
+ const containerStyle = computed(() => {
108
+ const style: Record<string, any> = {}
109
+
110
+ if (props.background) {
111
+ style.backgroundColor = props.background
112
+ }
113
+
114
+ if (props.height) {
115
+ style.height = `${props.height}px`
116
+ }
117
+
118
+ if (props.position === 'left' || props.position === 'right') {
119
+ style.display = 'flex'
120
+ style.flexDirection = props.position === 'left' ? 'row' : 'row-reverse'
121
+ }
122
+
123
+ // 如果是可滚动模式,添加滚动相关样式
124
+ if (props.scrollable) {
125
+ style.overflowX = 'scroll'
126
+ style['-webkit-overflow-scrolling'] = 'touch'
127
+ style.whiteSpace = 'nowrap'
128
+ }
129
+
130
+ return style
131
+ })
132
+
133
+ const contentStyle = computed(() => {
134
+ const style: Record<string, any> = {}
135
+
136
+ if (props.position === 'top' || props.position === 'bottom') {
137
+ if (props.swipeable) {
138
+ style.overflow = 'hidden'
139
+ }
140
+ } else {
141
+ style.flex = 1
142
+ style.overflow = props.swipeable ? 'hidden' : 'auto'
143
+ }
144
+
145
+ return style
146
+ })
147
+
148
+ const swipeStyle = computed(() => {
149
+ const translateX = props.position === 'top' || props.position === 'bottom'
150
+ ? -currentIndex.value * 100 + '%'
151
+ : '0'
152
+
153
+ const translateY = props.position === 'left' || props.position === 'right'
154
+ ? -currentIndex.value * 100 + '%'
155
+ : '0'
156
+
157
+ const transition = isSwiping.value ? 'none' : `transform ${props.duration}ms`
158
+
159
+ return {
160
+ transform: `translate3d(${translateX}, ${translateY}, 0)`,
161
+ transition: transition,
162
+ flexDirection: props.position === 'top' || props.position === 'bottom' ? 'row' : 'column',
163
+ display: 'flex'
164
+ }
165
+ })
166
+
167
+ const stickyStyle = computed(() => {
168
+ return {
169
+ top: `${props.stickyOffsetTop}px`,
170
+ zIndex: 1000
171
+ }
172
+ })
173
+
174
+ const sliderStyle = computed(() => {
175
+ const style: Record<string, any> = {
176
+ backgroundColor: props.sliderColor,
177
+ height: `${props.sliderHeight}px`,
178
+ transform: `translate3d(${sliderLeft.value}px, 0, 0)`,
179
+ transition: isSwiping.value ? 'none' : `transform ${props.duration}ms, width ${props.duration}ms`,
180
+ width: `${sliderWidth.value}px`
181
+ }
182
+
183
+ if (props.sliderWidth !== 'auto') {
184
+ style.width = typeof props.sliderWidth === 'number'
185
+ ? `${props.sliderWidth}px`
186
+ : props.sliderWidth
187
+ }
188
+
189
+ return style
190
+ })
191
+
192
+
193
+
194
+ // 处理标签点击
195
+ const handleTabClick = async (index: number, tab: TabItem) => {
196
+ if (tab.disabled) {
197
+ emit('disabled', index, tab)
198
+ return
199
+ }
200
+
201
+ emit('click', index, tab)
202
+
203
+ // beforeChange 回调
204
+ if (props.beforeChange) {
205
+ try {
206
+ const result = await props.beforeChange(index, tab)
207
+ if (result === false) {
208
+ return
209
+ }
210
+ } catch (error) {
211
+ console.error('beforeChange 回调出错:', error)
212
+ return
213
+ }
214
+ }
215
+
216
+ // 切换标签
217
+ setActive(index)
218
+ }
219
+
220
+ // 提供数据给子组件
221
+ provide('tabs', reactive({
222
+ items: props.items,
223
+ currentIndex,
224
+ type: props.type,
225
+ showSlider: props.showSlider,
226
+ sliderStyle,
227
+ scrollable: props.scrollable,
228
+ centered: props.centered,
229
+ gutter: props.gutter,
230
+ showIcon: props.showIcon,
231
+ iconPosition: props.iconPosition,
232
+ minTabWidth: props.minTabWidth,
233
+ maxTabWidth: props.maxTabWidth,
234
+ onTabClick: handleTabClick
235
+ }))
236
+
237
+ // 设置激活标签
238
+ const setActive = (index: number) => {
239
+ if (index === currentIndex.value) {
240
+ return
241
+ }
242
+
243
+ const oldIndex = currentIndex.value
244
+ console.log('im-tabs: 切换标签', oldIndex, '->', index)
245
+
246
+ currentIndex.value = index
247
+
248
+ // 触发事件
249
+ emit('update:modelValue', index)
250
+ emit('update:activeKey', currentActiveKey.value)
251
+ emit('change', index, props.items[index])
252
+
253
+ // 延迟更新滑动条位置
254
+ if (updateTimer.value) {
255
+ clearTimeout(updateTimer.value)
256
+ }
257
+ updateTimer.value = Number(setTimeout(() => {
258
+ updateSliderPosition()
259
+ }, 50))
260
+
261
+ // 延迟滚动到激活标签
262
+ if (props.scrollable) {
263
+ setTimeout(() => {
264
+ scrollToActiveTab()
265
+ }, 100)
266
+ }
267
+ }
268
+
269
+ // 更新滑动条位置
270
+ const updateSliderPosition = () => {
271
+ if (!props.showSlider || props.items.length === 0) {
272
+ return
273
+ }
274
+
275
+ // 清除之前的定时器
276
+ if (updateTimer.value) {
277
+ clearTimeout(updateTimer.value)
278
+ }
279
+
280
+ // 延迟执行
281
+ updateTimer.value = Number(setTimeout(() => {
282
+ updateSliderPositionImpl()
283
+ }, 50))
284
+ }
285
+
286
+ // 实际的更新逻辑
287
+ const updateSliderPositionImpl = () => {
288
+ try {
289
+ const query = uni.createSelectorQuery().in(instance)
290
+
291
+ // 使用不同的选择器避免冲突
292
+ const selector = `.im-tabs-nav .im-tabs-tab:nth-child(${currentIndex.value + 1})`
293
+
294
+ query.select(selector).boundingClientRect((res: any) => {
295
+ if (!res) {
296
+ // 如果获取失败,设置默认值
297
+ sliderLeft.value = 0
298
+ sliderWidth.value = typeof props.sliderWidth === 'number'
299
+ ? props.sliderWidth
300
+ : 50
301
+ return
302
+ }
303
+
304
+ // 使用新的查询实例获取导航容器
305
+ const navQuery = uni.createSelectorQuery().in(instance)
306
+ navQuery.select('.im-tabs-nav').boundingClientRect((navRes: any) => {
307
+ if (!navRes) {
308
+ sliderLeft.value = res.left
309
+ sliderWidth.value = props.sliderWidth === 'auto' ? res.width :
310
+ (typeof props.sliderWidth === 'number' ? props.sliderWidth : res.width)
311
+ return
312
+ }
313
+
314
+
315
+
316
+ // 计算滑动条位置
317
+ const left = res.left - navRes.left
318
+ sliderLeft.value = left
319
+
320
+ // 计算滑动条宽度
321
+ if (props.sliderWidth === 'auto') {
322
+ sliderWidth.value = res.width
323
+ } else {
324
+ sliderWidth.value = typeof props.sliderWidth === 'number'
325
+ ? props.sliderWidth
326
+ : parseInt(props.sliderWidth as string) || res.width
327
+ }
328
+ }).exec()
329
+ }).exec()
330
+ } catch (error) {
331
+ console.error('更新滑动条位置失败:', error)
332
+ // 设置默认值
333
+ sliderLeft.value = 0
334
+ sliderWidth.value = 50
335
+ }
336
+ }
337
+
338
+ // 滚动到激活标签
339
+ const scrollToActiveTab = () => {
340
+ if (!props.scrollable) return
341
+
342
+ // 使用 requestAnimationFrame
343
+ requestAnimationFrame(() => {
344
+ scrollToActiveTabImpl()
345
+ })
346
+ }
347
+
348
+ const scrollToActiveTabImpl = () => {
349
+ try {
350
+ const query = uni.createSelectorQuery().in(instance)
351
+
352
+ query.select(`.im-tabs-nav .im-tabs-tab:nth-child(${currentIndex.value + 1})`).boundingClientRect((res) => {
353
+ if (!res) return
354
+
355
+ // 在小程序环境中,可以使用 scroll-into-view
356
+ // #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
357
+ // 这里可以设置 scroll-into-view 属性
358
+ // 导航组件中已经处理了
359
+ // #endif
360
+
361
+ // 在 H5 环境中
362
+ // #ifdef H5
363
+ // H5 中的滚动由 CSS 控制
364
+ // #endif
365
+ }).exec()
366
+ } catch (error) {
367
+ console.error('滚动到激活标签失败:', error)
368
+ }
369
+ }
370
+
371
+ // 触摸事件处理
372
+ const onTouchStart = (event: TouchEvent) => {
373
+ if (!props.swipeable || props.items.length <= 1) return
374
+
375
+ isSwiping.value = true
376
+ startX.value = event.touches[0].clientX
377
+ deltaX.value = 0
378
+ }
379
+
380
+ const onTouchMove = (event: TouchEvent) => {
381
+ if (!isSwiping.value || !props.swipeable) return
382
+
383
+ const touch = event.touches[0]
384
+ deltaX.value = touch.clientX - startX.value
385
+
386
+ // 阻止页面滚动
387
+ if (Math.abs(deltaX.value) > 10) {
388
+ event.preventDefault()
389
+ }
390
+ }
391
+
392
+ const onTouchEnd = () => {
393
+ if (!isSwiping.value || !props.swipeable) return
394
+
395
+ isSwiping.value = false
396
+
397
+ // 判断是否需要切换标签
398
+ const threshold = 50 // 使用固定阈值,避免依赖 contentWidth
399
+ const absDeltaX = Math.abs(deltaX.value)
400
+
401
+ if (absDeltaX > threshold) {
402
+ const direction = deltaX.value > 0 ? -1 : 1
403
+ const newIndex = currentIndex.value + direction
404
+
405
+ if (newIndex >= 0 && newIndex < props.items.length) {
406
+ const tab = props.items[newIndex]
407
+ if (!tab.disabled) {
408
+ handleTabClick(newIndex, tab)
409
+ }
410
+ }
411
+ }
412
+
413
+ deltaX.value = 0
414
+ }
415
+
416
+ // 导航滚动事件
417
+ const onNavScroll = (event: any) => {
418
+ if (props.scrollable) {
419
+ scrollLeft.value = event.detail?.scrollLeft || 0
420
+ emit('scroll', event)
421
+ }
422
+ }
423
+
424
+ // 容器滚动事件
425
+ const onContainerScroll = (event: any) => {
426
+ if (props.scrollable) {
427
+ scrollLeft.value = event.detail?.scrollLeft || 0
428
+ emit('scroll', event)
429
+ }
430
+ }
431
+
432
+ // 监听滚动事件(粘性布局)
433
+ const onPageScroll = (event: any) => {
434
+ if (!props.sticky) return
435
+
436
+ try {
437
+ const scrollTop = event.scrollTop || 0
438
+
439
+ requestAnimationFrame(() => {
440
+ const query = uni.createSelectorQuery().in(instance)
441
+
442
+ query.select('.im-tabs-sticky').boundingClientRect((res: any) => {
443
+ if (!res) return
444
+
445
+ const newIsSticky = scrollTop >= res.top - props.stickyOffsetTop
446
+ if (newIsSticky !== isSticky.value) {
447
+ isSticky.value = newIsSticky
448
+ emit('sticky', newIsSticky)
449
+ }
450
+ }).exec()
451
+ })
452
+ } catch (error) {
453
+ console.error('监听滚动事件失败:', error)
454
+ }
455
+ }
456
+
457
+ // 监听外部值变化
458
+ watch(() => props.modelValue, (newVal) => {
459
+ if (newVal !== currentIndex.value) {
460
+ if (changeTimer.value) {
461
+ clearTimeout(changeTimer.value)
462
+ }
463
+ changeTimer.value = Number(setTimeout(() => {
464
+ setActive(newVal)
465
+ }, 50))
466
+ }
467
+ })
468
+
469
+ watch(() => props.activeKey, (newVal) => {
470
+ const index = props.items.findIndex(item =>
471
+ (item.id !== undefined && item.id === newVal) ||
472
+ item.title === newVal
473
+ )
474
+
475
+ if (index !== -1 && index !== currentIndex.value) {
476
+ if (changeTimer.value) {
477
+ clearTimeout(changeTimer.value)
478
+ }
479
+ changeTimer.value = Number(setTimeout(() => {
480
+ setActive(index)
481
+ }, 50))
482
+ }
483
+ })
484
+
485
+ watch(() => props.items, () => {
486
+ // 确保当前索引在有效范围内
487
+ if (currentIndex.value >= props.items.length) {
488
+ const newIndex = Math.max(0, props.items.length - 1)
489
+ if (newIndex !== currentIndex.value) {
490
+ if (changeTimer.value) {
491
+ clearTimeout(changeTimer.value)
492
+ }
493
+ changeTimer.value = Number(setTimeout(() => {
494
+ setActive(newIndex)
495
+ }, 50))
496
+ }
497
+ }
498
+
499
+ // 延迟更新滑动条位置
500
+ setTimeout(() => {
501
+ updateSliderPosition()
502
+ }, 100)
503
+ }, { deep: true })
504
+
505
+ // 初始化
506
+ onMounted(() => {
507
+ // 延迟更新滑动条位置
508
+ setTimeout(() => {
509
+ updateSliderPosition()
510
+ }, 200)
511
+
512
+ // 监听页面滚动事件(粘性布局)
513
+ if (props.sticky) {
514
+ // #ifdef H5
515
+ window.addEventListener('scroll', onPageScroll, { passive: true })
516
+ // #endif
517
+
518
+ // #ifdef MP-WEIXIN
519
+ // 小程序中使用页面滚动事件
520
+ const pages = getCurrentPages()
521
+ if (pages.length) {
522
+ const page = pages[pages.length - 1]
523
+ if (page && page.onPageScroll) {
524
+ const originalOnPageScroll = page.onPageScroll
525
+ page.onPageScroll = function (e: any) {
526
+ originalOnPageScroll && originalOnPageScroll.call(this, e)
527
+ onPageScroll(e)
528
+ }
529
+ }
530
+ }
531
+ // #endif
532
+ }
533
+
534
+ // 初始化时滚动到激活标签
535
+ if (props.scrollOnInit) {
536
+ setTimeout(() => {
537
+ scrollToActiveTab()
538
+ }, 300)
539
+ }
540
+ })
541
+
542
+ // 清理
543
+ onUnmounted(() => {
544
+ // 清理定时器
545
+ if (changeTimer.value) {
546
+ clearTimeout(changeTimer.value)
547
+ }
548
+
549
+ if (updateTimer.value) {
550
+ clearTimeout(updateTimer.value)
551
+ }
552
+
553
+ if (props.sticky) {
554
+ // #ifdef H5
555
+ window.removeEventListener('scroll', onPageScroll)
556
+ // #endif
557
+ }
558
+ })
559
+
560
+ // 暴露方法
561
+ defineExpose({
562
+ setActive,
563
+ getCurrentIndex: () => currentIndex.value,
564
+ getCurrentKey: () => currentActiveKey.value,
565
+ scrollToActiveTab,
566
+ updateSliderPosition
567
+ })
568
+ </script>
569
+
570
+ <style lang="scss" scoped>
571
+ .im-tabs {
572
+ position: relative;
573
+ background-color: #ffffff;
574
+ overflow: hidden;
575
+
576
+ // 位置
577
+ &-top,
578
+ &-bottom {
579
+ display: flex;
580
+ flex-direction: column;
581
+ }
582
+
583
+ &-bottom {
584
+ flex-direction: column-reverse;
585
+ }
586
+
587
+ &-left,
588
+ &-right {
589
+ display: flex;
590
+ }
591
+
592
+ &-left {
593
+ .im-tabs-content {
594
+ margin-left: 0;
595
+ }
596
+ }
597
+
598
+ &-right {
599
+ .im-tabs-content {
600
+ margin-right: 0;
601
+ }
602
+ }
603
+
604
+ // 边框
605
+ &-border {
606
+ border-bottom: 1px solid #ebedf0;
607
+
608
+ &.im-tabs-bottom {
609
+ border-top: 1px solid #ebedf0;
610
+ border-bottom: none;
611
+ }
612
+
613
+ &.im-tabs-left {
614
+ border-right: 1px solid #ebedf0;
615
+ border-bottom: none;
616
+ }
617
+
618
+ &.im-tabs-right {
619
+ border-left: 1px solid #ebedf0;
620
+ border-bottom: none;
621
+ }
622
+ }
623
+
624
+ // 滚动模式
625
+ &-scrollable {
626
+ .im-tabs-nav {
627
+ flex-wrap: nowrap;
628
+ overflow-x: auto;
629
+ -webkit-overflow-scrolling: touch;
630
+
631
+ &::-webkit-scrollbar {
632
+ display: none;
633
+ }
634
+ }
635
+ }
636
+
637
+ // 居中模式
638
+ &-centered {
639
+ .im-tabs-nav {
640
+ justify-content: center;
641
+ }
642
+ }
643
+ }
644
+
645
+ .im-tabs-nav {
646
+ display: flex;
647
+ position: relative;
648
+ flex-shrink: 0;
649
+ z-index: 1;
650
+ overflow: hidden;
651
+
652
+ .im-tabs-top &,
653
+ .im-tabs-bottom & {
654
+ flex-direction: row;
655
+ }
656
+
657
+ .im-tabs-left &,
658
+ .im-tabs-right & {
659
+ flex-direction: column;
660
+ width: auto;
661
+ min-width: 80px;
662
+ max-width: 200px;
663
+ }
664
+ }
665
+
666
+ .im-tabs-content {
667
+ position: relative;
668
+ flex: 1;
669
+ min-height: 0;
670
+
671
+ .im-tabs-top &,
672
+ .im-tabs-bottom & {
673
+ width: 100%;
674
+ }
675
+
676
+ .im-tabs-left &,
677
+ .im-tabs-right & {
678
+ flex: 1;
679
+ overflow: auto;
680
+ }
681
+ }
682
+
683
+ .im-tabs-swipe {
684
+ width: 100%;
685
+ height: 100%;
686
+
687
+ &-transition {
688
+ transition-property: transform;
689
+ }
690
+
691
+ .im-tabs-top &,
692
+ .im-tabs-bottom & {
693
+ display: flex;
694
+ }
695
+
696
+ .im-tabs-left &,
697
+ .im-tabs-right & {
698
+ display: flex;
699
+ flex-direction: column;
700
+ }
701
+ }
702
+
703
+ // 标签项
704
+ .im-tabs-tab {
705
+ position: relative;
706
+ display: flex;
707
+ align-items: center;
708
+ justify-content: center;
709
+ flex-shrink: 0;
710
+ cursor: pointer;
711
+ transition: all 0.3s;
712
+ user-select: none;
713
+ padding: 0 12px;
714
+ box-sizing: border-box;
715
+
716
+ // 水平布局
717
+ .im-tabs-top &,
718
+ .im-tabs-bottom & {
719
+ height: 100%;
720
+ }
721
+
722
+ // 垂直布局
723
+ .im-tabs-left &,
724
+ .im-tabs-right & {
725
+ width: 100%;
726
+ padding: 12px;
727
+ }
728
+
729
+ // 禁用状态
730
+ &-disabled {
731
+ cursor: not-allowed;
732
+ opacity: 0.5;
733
+
734
+ &:active {
735
+ opacity: 0.5;
736
+ }
737
+ }
738
+
739
+ // 激活状态
740
+ &-active {
741
+ font-weight: 600;
742
+ }
743
+
744
+ // 图标位置
745
+ &-icon-left {
746
+ flex-direction: row;
747
+
748
+ .im-tabs-tab-icon {
749
+ margin-right: 4px;
750
+ }
751
+ }
752
+
753
+ &-icon-right {
754
+ flex-direction: row-reverse;
755
+
756
+ .im-tabs-tab-icon {
757
+ margin-left: 4px;
758
+ }
759
+ }
760
+
761
+ &-icon-top {
762
+ flex-direction: column;
763
+
764
+ .im-tabs-tab-icon {
765
+ margin-bottom: 4px;
766
+ }
767
+ }
768
+
769
+ &-icon-bottom {
770
+ flex-direction: column-reverse;
771
+
772
+ .im-tabs-tab-icon {
773
+ margin-top: 4px;
774
+ }
775
+ }
776
+ }
777
+
778
+ // 标签内容
779
+ .im-tabs-tab-content {
780
+ display: flex;
781
+ align-items: center;
782
+ justify-content: center;
783
+ position: relative;
784
+ white-space: nowrap;
785
+ }
786
+
787
+ .im-tabs-tab-title {
788
+ font-size: 14px;
789
+ line-height: 1.4;
790
+ transition: color 0.3s;
791
+
792
+ .im-tabs-line & {
793
+ color: #646566;
794
+ }
795
+
796
+ .im-tabs-tab-active & {
797
+ color: #1989fa;
798
+ }
799
+
800
+ .im-tabs-card &,
801
+ .im-tabs-button &,
802
+ .im-tabs-segment & {
803
+ color: #323233;
804
+ }
805
+
806
+ .im-tabs-card &-active,
807
+ .im-tabs-button &-active,
808
+ .im-tabs-segment &-active {
809
+ color: #1989fa;
810
+ }
811
+ }
812
+
813
+ .im-tabs-tab-icon {
814
+ display: flex;
815
+ align-items: center;
816
+ justify-content: center;
817
+ font-size: 18px;
818
+ transition: color 0.3s;
819
+
820
+ .im-tabs-line & {
821
+ color: #646566;
822
+ }
823
+
824
+ .im-tabs-tab-active & {
825
+ color: #1989fa;
826
+ }
827
+ }
828
+
829
+ // 徽标
830
+ .im-tabs-badge {
831
+ position: absolute;
832
+ top: 0;
833
+ right: 0;
834
+ transform: translate(50%, -50%);
835
+ z-index: 1;
836
+
837
+ &-dot {
838
+ width: 6px;
839
+ height: 6px;
840
+ background-color: #ee0a24;
841
+ border-radius: 50%;
842
+ }
843
+
844
+ &-number,
845
+ &-text {
846
+ padding: 0 4px;
847
+ font-size: 10px;
848
+ color: #ffffff;
849
+ background-color: #ee0a24;
850
+ border-radius: 10px;
851
+ line-height: 14px;
852
+ min-width: 14px;
853
+ text-align: center;
854
+ font-weight: 500;
855
+ }
856
+ }
857
+
858
+ // 滑动条
859
+ .im-tabs-slider {
860
+ position: absolute;
861
+ bottom: 0;
862
+ left: 0;
863
+ z-index: 1;
864
+ border-radius: 2px;
865
+
866
+ .im-tabs-bottom & {
867
+ top: 0;
868
+ bottom: auto;
869
+ }
870
+
871
+ .im-tabs-left & {
872
+ right: 0;
873
+ left: auto;
874
+ top: 0;
875
+ width: 2px;
876
+ height: auto;
877
+ }
878
+
879
+ .im-tabs-right & {
880
+ left: 0;
881
+ top: 0;
882
+ width: 2px;
883
+ height: auto;
884
+ }
885
+ }
886
+
887
+ // 不同类型
888
+ .im-tabs {
889
+ &-line {
890
+ .im-tabs-tab {
891
+ background-color: transparent;
892
+ }
893
+ }
894
+
895
+ &-card {
896
+ .im-tabs-nav {
897
+ background-color: #f2f3f5;
898
+ padding: 4px;
899
+ border-radius: 8px;
900
+ }
901
+
902
+ .im-tabs-tab {
903
+ background-color: transparent;
904
+ border-radius: 6px;
905
+ margin: 0 2px;
906
+
907
+ &-active {
908
+ background-color: #ffffff;
909
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
910
+ }
911
+ }
912
+ }
913
+
914
+ &-button {
915
+ .im-tabs-tab {
916
+ border: 1px solid #ebedf0;
917
+ margin: 0 4px;
918
+ border-radius: 20px;
919
+
920
+ &:first-child {
921
+ margin-left: 0;
922
+ }
923
+
924
+ &:last-child {
925
+ margin-right: 0;
926
+ }
927
+
928
+ &-active {
929
+ border-color: #1989fa;
930
+ background-color: #1989fa;
931
+ color: #ffffff;
932
+ }
933
+ }
934
+ }
935
+
936
+ &-segment {
937
+ .im-tabs-nav {
938
+ background-color: #f2f3f5;
939
+ border-radius: 20px;
940
+ padding: 2px;
941
+ }
942
+
943
+ .im-tabs-tab {
944
+ flex: 1;
945
+ border-radius: 18px;
946
+
947
+ &-active {
948
+ background-color: #ffffff;
949
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
950
+ }
951
+ }
952
+ }
953
+ }
954
+
955
+ // 粘性布局
956
+ .im-tabs-sticky {
957
+ position: relative;
958
+ z-index: 1000;
959
+
960
+ &-fixed {
961
+ position: fixed;
962
+ left: 0;
963
+ right: 0;
964
+ }
965
+ }
966
+
967
+ // 动画
968
+ @keyframes im-tabs-fade-in {
969
+ from {
970
+ opacity: 0;
971
+ }
972
+
973
+ to {
974
+ opacity: 1;
975
+ }
976
+ }
977
+
978
+ @keyframes im-tabs-fade-out {
979
+ from {
980
+ opacity: 1;
981
+ }
982
+
983
+ to {
984
+ opacity: 0;
985
+ }
986
+ }
987
+
988
+ .im-tabs-content {
989
+ &-fade {
990
+ .im-tabs-tab-pane {
991
+ animation-duration: 0.3s;
992
+ animation-fill-mode: both;
993
+
994
+ &-enter-active {
995
+ animation-name: im-tabs-fade-in;
996
+ }
997
+
998
+ &-leave-active {
999
+ animation-name: im-tabs-fade-out;
1000
+ }
1001
+ }
1002
+ }
1003
+ }
1004
+
1005
+ // 响应式
1006
+ @media (max-width: 768px) {
1007
+ .im-tabs-tab-title {
1008
+ font-size: 13px;
1009
+ }
1010
+
1011
+ .im-tabs {
1012
+ height: 40px;
1013
+ }
1014
+
1015
+ .im-tabs-left,
1016
+ .im-tabs-right {
1017
+ .im-tabs-nav {
1018
+ min-width: 70px;
1019
+ }
1020
+ }
1021
+ }
1022
+ </style>