im-ui-mobile 0.1.1 → 0.1.3

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,489 @@
1
+ <!-- components/im-tabs/tabs-navigation.vue -->
2
+ <template>
3
+ <view ref="navRef" :class="[
4
+ 'im-tabs-nav',
5
+ navClass,
6
+ {
7
+ 'im-tabs-nav-scrollable': scrollable,
8
+ 'im-tabs-nav-centered': centered && !scrollable
9
+ }
10
+ ]" :style="navStyle" :scroll-x="scrollable" :scroll-with-animation="true" :scroll-left="scrollLeft"
11
+ :scroll-into-view="scrollIntoView" @scroll="onScroll">
12
+ <!-- 标签项 -->
13
+ <view v-for="(item, index) in items" :key="item.id || index" :id="`tab-${index}`" :class="[
14
+ 'im-tabs-tab',
15
+ item.class,
16
+ `im-tabs-tab-icon-${iconPosition}`,
17
+ {
18
+ 'im-tabs-tab-active': currentIndex === index,
19
+ 'im-tabs-tab-disabled': item.disabled
20
+ }
21
+ ]" :style="[tabStyle, item.style]" :data-index="index" @click="handleTabClick(index, item)"
22
+ @touchstart="onTouchStart">
23
+ <!-- 自定义渲染 -->
24
+ <template v-if="item.renderTitle">
25
+ <view class="im-tabs-tab-content">
26
+ {{ item.renderTitle(item, currentIndex === index) }}
27
+ </view>
28
+ </template>
29
+
30
+ <template v-else>
31
+ <view class="im-tabs-tab-content"
32
+ :class="{ 'im-tabs-tab-content-badge': item.badge || item.badgeType === 'dot' }">
33
+ <!-- 图标 -->
34
+ <im-icon v-if="showIcon && (item.icon || item.activeIcon)"
35
+ :name="currentIndex === index && item.activeIcon ? item.activeIcon : item.icon"
36
+ class="im-tabs-tab-icon" :color="getIconColor(item, index)" />
37
+
38
+ <!-- 徽标 -->
39
+ <template v-if="item.badge || item.badgeType === 'dot'">
40
+ <im-badge v-if="item.badgeType === 'dot'" size="small" dot>
41
+ <!-- 标题 -->
42
+ <text class="im-tabs-tab-title">
43
+ {{ item.title }}
44
+ </text>
45
+ </im-badge>
46
+ <im-badge v-else-if="item.badgeType === 'number' && item.badge" size="small"
47
+ :value="item.badge">
48
+ <!-- 标题 -->
49
+ <text class="im-tabs-tab-title">
50
+ {{ item.title }}
51
+ </text>
52
+ </im-badge>
53
+ <im-badge v-else-if="item.badgeType === 'text' && item.badge" size="small" :value="item.badge">
54
+ <!-- 标题 -->
55
+ <text class="im-tabs-tab-title">
56
+ {{ item.title }}
57
+ </text>
58
+ </im-badge>
59
+ </template>
60
+ <!-- 标题 -->
61
+ <text v-else class="im-tabs-tab-title">
62
+ {{ item.title }}
63
+ </text>
64
+ </view>
65
+ </template>
66
+ </view>
67
+
68
+ <!-- 滑动条 -->
69
+ <view v-if="showSlider && type === 'line'" class="im-tabs-slider" :style="sliderStyle" />
70
+ </view>
71
+ </template>
72
+
73
+ <script lang="ts" setup>
74
+ import { ref, computed, watch, inject } from 'vue'
75
+ import type { TabItem } from '../../types/components/tabs'
76
+
77
+ // 注入父组件数据
78
+ const tabsData = inject<any>('tabs')
79
+
80
+ if (!tabsData) {
81
+ console.error('tabs-navigation: 未找到父组件注入的 tabs 数据')
82
+ }
83
+
84
+ const {
85
+ items = [],
86
+ type = 'line',
87
+ showSlider = true,
88
+ sliderStyle = {},
89
+ scrollable = false,
90
+ centered = false,
91
+ gutter = 0,
92
+ showIcon = false,
93
+ iconPosition = 'left',
94
+ minTabWidth = 0,
95
+ maxTabWidth = 0,
96
+ onTabClick,
97
+ navClass = ''
98
+ } = tabsData || {}
99
+
100
+ const navRef = ref<any>(null)
101
+ const scrollLeft = ref(0)
102
+ const scrollIntoView = ref('')
103
+
104
+ // 定义属性
105
+ const props = defineProps<{
106
+ currentIndex: 0
107
+ }>()
108
+
109
+ // 监听 currentIndex 变化,更新 scroll-into-view
110
+ watch(() => props.currentIndex, (newVal) => {
111
+ if (scrollable && newVal >= 0) {
112
+ scrollIntoView.value = `tab-${newVal}`
113
+
114
+ // 在小程序环境中,延迟更新以确保生效
115
+ setTimeout(() => {
116
+ scrollIntoView.value = `tab-${newVal}`
117
+ }, 50)
118
+ }
119
+ }, { immediate: true })
120
+
121
+ // 计算属性
122
+ const navStyle = computed(() => {
123
+ const style: Record<string, any> = {}
124
+
125
+ if (gutter > 0) {
126
+ style.gap = `${gutter}px`
127
+ }
128
+
129
+ // 如果是可滚动模式,设置滚动相关样式
130
+ if (scrollable) {
131
+ style.whiteSpace = 'nowrap'
132
+ style.display = 'flex'
133
+ style.flexWrap = 'nowrap'
134
+ }
135
+
136
+ return style
137
+ })
138
+
139
+ const tabStyle = computed(() => {
140
+ const style: Record<string, any> = {}
141
+
142
+ if (minTabWidth > 0) {
143
+ style.minWidth = `${minTabWidth}px`
144
+ }
145
+
146
+ if (maxTabWidth > 0) {
147
+ style.maxWidth = `${maxTabWidth}px`
148
+ }
149
+
150
+ if (centered && !scrollable) {
151
+ style.flex = 1
152
+ }
153
+
154
+ // 如果是可滚动模式,设置为行内块元素
155
+ if (scrollable) {
156
+ style.display = 'inline-flex'
157
+ style.flexShrink = 0
158
+ }
159
+
160
+ return style
161
+ })
162
+
163
+ // 获取图标颜色
164
+ const getIconColor = (item: TabItem, index: number | string) => {
165
+ if (item.disabled) {
166
+ return '#c8c9cc'
167
+ }
168
+
169
+ if (props.currentIndex === index) {
170
+ return '#1989fa'
171
+ }
172
+
173
+ return '#646566'
174
+ }
175
+
176
+ // 触摸开始
177
+ const onTouchStart = () => {
178
+ // 可以在这里添加触摸反馈
179
+ }
180
+
181
+ // 滚动事件
182
+ const onScroll = (event: any) => {
183
+ if (scrollable) {
184
+ scrollLeft.value = event.detail?.scrollLeft || 0
185
+ }
186
+ }
187
+
188
+ // 标签点击
189
+ const handleTabClick = (index: number | string, tab: TabItem) => {
190
+ if (onTabClick) {
191
+ onTabClick(index, tab)
192
+ }
193
+ }
194
+
195
+ // 暴露方法供父组件调用
196
+ defineExpose({
197
+ getNavRef: () => navRef.value,
198
+ getScrollLeft: () => scrollLeft.value
199
+ })
200
+ </script>
201
+
202
+ <style lang="scss" scoped>
203
+ .im-tabs-nav {
204
+ display: flex;
205
+ position: relative;
206
+ flex-shrink: 0;
207
+ z-index: 1;
208
+ overflow: hidden;
209
+
210
+ // 水平布局
211
+ .im-tabs-top &,
212
+ .im-tabs-bottom & {
213
+ flex-direction: row;
214
+ }
215
+
216
+ // 垂直布局
217
+ .im-tabs-left &,
218
+ .im-tabs-right & {
219
+ flex-direction: column;
220
+ width: auto;
221
+ min-width: 80px;
222
+ max-width: 200px;
223
+ }
224
+
225
+ // 可滚动模式
226
+ &-scrollable {
227
+ flex-wrap: nowrap;
228
+ overflow-x: auto;
229
+ -webkit-overflow-scrolling: touch;
230
+
231
+ &::-webkit-scrollbar {
232
+ display: none;
233
+ }
234
+
235
+ .im-tabs-tab {
236
+ flex-shrink: 0;
237
+ }
238
+ }
239
+
240
+ // 居中模式(非滚动时)
241
+ &-centered {
242
+ justify-content: center;
243
+ }
244
+ }
245
+
246
+ .im-tabs-tab {
247
+ position: relative;
248
+ display: flex;
249
+ align-items: center;
250
+ justify-content: center;
251
+ flex-shrink: 0;
252
+ cursor: pointer;
253
+ transition: all 0.3s;
254
+ user-select: none;
255
+ padding: 0 12px;
256
+ box-sizing: border-box;
257
+
258
+ // 水平布局
259
+ .im-tabs-top &,
260
+ .im-tabs-bottom & {
261
+ height: 100%;
262
+ }
263
+
264
+ // 垂直布局
265
+ .im-tabs-left &,
266
+ .im-tabs-right & {
267
+ width: 100%;
268
+ padding: 12px;
269
+ justify-content: flex-start;
270
+ }
271
+
272
+ // 禁用状态
273
+ &-disabled {
274
+ cursor: not-allowed;
275
+ opacity: 0.5;
276
+
277
+ &:active {
278
+ opacity: 0.5;
279
+ }
280
+ }
281
+
282
+ // 激活状态
283
+ &-active {
284
+ font-weight: 600;
285
+ }
286
+
287
+ // 图标位置
288
+ &-icon-left {
289
+ flex-direction: row;
290
+
291
+ .im-tabs-tab-icon {
292
+ margin-right: 4px;
293
+ }
294
+ }
295
+
296
+ &-icon-right {
297
+ flex-direction: row-reverse;
298
+
299
+ .im-tabs-tab-icon {
300
+ margin-left: 4px;
301
+ }
302
+ }
303
+
304
+ &-icon-top {
305
+ flex-direction: column;
306
+
307
+ .im-tabs-tab-icon {
308
+ margin-bottom: 4px;
309
+ }
310
+ }
311
+
312
+ &-icon-bottom {
313
+ flex-direction: column-reverse;
314
+
315
+ .im-tabs-tab-icon {
316
+ margin-top: 4px;
317
+ }
318
+ }
319
+ }
320
+
321
+ .im-tabs-tab-content {
322
+ display: flex;
323
+ align-items: center;
324
+ justify-content: center;
325
+ position: relative;
326
+ white-space: nowrap;
327
+
328
+ &-badge {
329
+ height: 77rpx;
330
+ }
331
+ }
332
+
333
+ .im-tabs-tab-title {
334
+ font-size: 14px;
335
+ line-height: 1.4;
336
+ transition: color 0.3s;
337
+
338
+ .im-tabs-line & {
339
+ color: #646566;
340
+ }
341
+
342
+ .im-tabs-tab-active & {
343
+ color: #1989fa;
344
+ }
345
+
346
+ .im-tabs-card &,
347
+ .im-tabs-button &,
348
+ .im-tabs-segment & {
349
+ color: #323233;
350
+ }
351
+
352
+ .im-tabs-card &-active,
353
+ .im-tabs-button &-active,
354
+ .im-tabs-segment &-active {
355
+ color: #1989fa;
356
+ }
357
+ }
358
+
359
+ .im-tabs-tab-icon {
360
+ display: flex;
361
+ align-items: center;
362
+ justify-content: center;
363
+ font-size: 18px;
364
+ transition: color 0.3s;
365
+
366
+ .im-tabs-line & {
367
+ color: #646566;
368
+ }
369
+
370
+ .im-tabs-tab-active & {
371
+ color: #1989fa;
372
+ }
373
+ }
374
+
375
+ // 滑动条
376
+ .im-tabs-slider {
377
+ position: absolute;
378
+ bottom: 0;
379
+ left: 0;
380
+ z-index: 1;
381
+ border-radius: 2px;
382
+
383
+ .im-tabs-bottom & {
384
+ top: 0;
385
+ bottom: auto;
386
+ }
387
+
388
+ .im-tabs-left & {
389
+ right: 0;
390
+ left: auto;
391
+ top: 0;
392
+ width: 2px;
393
+ height: auto;
394
+ }
395
+
396
+ .im-tabs-right & {
397
+ left: 0;
398
+ top: 0;
399
+ width: 2px;
400
+ height: auto;
401
+ }
402
+ }
403
+
404
+ // 不同类型
405
+ .im-tabs-nav {
406
+ .im-tabs-line & {
407
+ .im-tabs-tab {
408
+ background-color: transparent;
409
+ }
410
+ }
411
+
412
+ .im-tabs-card & {
413
+ background-color: #f2f3f5;
414
+ padding: 4px;
415
+ border-radius: 8px;
416
+
417
+ .im-tabs-tab {
418
+ background-color: transparent;
419
+ border-radius: 6px;
420
+ margin: 0 2px;
421
+
422
+ &-active {
423
+ background-color: #ffffff;
424
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
425
+ }
426
+ }
427
+ }
428
+
429
+ .im-tabs-button & {
430
+ .im-tabs-tab {
431
+ border: 1px solid #ebedf0;
432
+ margin: 0 4px;
433
+ border-radius: 20px;
434
+
435
+ &:first-child {
436
+ margin-left: 0;
437
+ }
438
+
439
+ &:last-child {
440
+ margin-right: 0;
441
+ }
442
+
443
+ &-active {
444
+ border-color: #1989fa;
445
+ background-color: #1989fa;
446
+
447
+ .im-tabs-tab-title,
448
+ .im-tabs-tab-icon {
449
+ color: #ffffff;
450
+ }
451
+ }
452
+ }
453
+ }
454
+
455
+ .im-tabs-segment & {
456
+ background-color: #f2f3f5;
457
+ border-radius: 20px;
458
+ padding: 2px;
459
+
460
+ .im-tabs-tab {
461
+ flex: 1;
462
+ border-radius: 18px;
463
+
464
+ &-active {
465
+ background-color: #ffffff;
466
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
467
+ }
468
+ }
469
+ }
470
+ }
471
+
472
+ // 响应式
473
+ @media (max-width: 768px) {
474
+ .im-tabs-tab-title {
475
+ font-size: 13px;
476
+ }
477
+
478
+ .im-tabs-tab-icon {
479
+ font-size: 16px;
480
+ }
481
+
482
+ .im-tabs-left,
483
+ .im-tabs-right {
484
+ .im-tabs-nav {
485
+ min-width: 70px;
486
+ }
487
+ }
488
+ }
489
+ </style>
@@ -0,0 +1,181 @@
1
+ import type { TabItem } from '../../../types/components/tabs'
2
+
3
+ /**
4
+ * 验证标签页配置
5
+ */
6
+ export function validateTabsConfig(config: any) {
7
+ const errors: string[] = []
8
+
9
+ if (!Array.isArray(config.items)) {
10
+ errors.push('items 必须是数组')
11
+ }
12
+
13
+ if (config.height && config.height < 30) {
14
+ errors.push('高度不能小于30px')
15
+ }
16
+
17
+ if (config.duration && config.duration < 0) {
18
+ errors.push('动画持续时间不能为负数')
19
+ }
20
+
21
+ return errors
22
+ }
23
+
24
+ /**
25
+ * 计算标签宽度
26
+ */
27
+ export function calculateTabWidth(
28
+ tabs: TabItem[],
29
+ options: {
30
+ containerWidth?: number
31
+ minTabWidth?: number
32
+ maxTabWidth?: number
33
+ gutter?: number
34
+ centered?: boolean
35
+ }
36
+ ): number {
37
+ if (!tabs.length) return 0
38
+
39
+ const { containerWidth = 0, minTabWidth = 0, maxTabWidth = 0, gutter = 0, centered = false } = options
40
+
41
+ // 如果有容器宽度,计算合适的宽度
42
+ if (containerWidth > 0) {
43
+ const availableWidth = containerWidth - gutter * (tabs.length - 1)
44
+ let calculatedWidth = availableWidth / tabs.length
45
+
46
+ // 应用最小和最大宽度限制
47
+ if (minTabWidth > 0) {
48
+ calculatedWidth = Math.max(calculatedWidth, minTabWidth)
49
+ }
50
+
51
+ if (maxTabWidth > 0) {
52
+ calculatedWidth = Math.min(calculatedWidth, maxTabWidth)
53
+ }
54
+
55
+ return calculatedWidth
56
+ }
57
+
58
+ // 如果没有容器宽度,返回0(使用内容自适应)
59
+ return 0
60
+ }
61
+
62
+ /**
63
+ * 获取激活的标签索引
64
+ */
65
+ export function getActiveIndex(
66
+ items: TabItem[],
67
+ modelValue?: number,
68
+ activeKey?: string | number
69
+ ): number {
70
+ if (activeKey !== undefined) {
71
+ const index = items.findIndex(item =>
72
+ (item.id !== undefined && item.id === activeKey) ||
73
+ item.title === activeKey
74
+ )
75
+ if (index !== -1) return index
76
+ }
77
+
78
+ if (modelValue !== undefined) {
79
+ return Math.max(0, Math.min(modelValue, items.length - 1))
80
+ }
81
+
82
+ return 0
83
+ }
84
+
85
+ /**
86
+ * 更新标签徽标
87
+ */
88
+ export function updateTabBadge(
89
+ items: TabItem[],
90
+ index: number,
91
+ badge: number | string,
92
+ type: 'number' | 'text' = 'number'
93
+ ): TabItem[] {
94
+ return items.map((item, i) => {
95
+ if (i === index) {
96
+ return {
97
+ ...item,
98
+ badge: typeof badge === 'number' ? badge : undefined,
99
+ badgeText: typeof badge === 'string' ? badge : undefined,
100
+ badgeType: type
101
+ }
102
+ }
103
+ return item
104
+ })
105
+ }
106
+
107
+ /**
108
+ * 更新标签红点状态
109
+ */
110
+ export function updateTabRedDot(
111
+ items: TabItem[],
112
+ index: number,
113
+ showRedDot: boolean
114
+ ): TabItem[] {
115
+ return items.map((item, i) => {
116
+ if (i === index) {
117
+ return {
118
+ ...item,
119
+ showRedDot
120
+ }
121
+ }
122
+ return item
123
+ })
124
+ }
125
+
126
+ /**
127
+ * 禁用/启用标签
128
+ */
129
+ export function toggleTabDisabled(
130
+ items: TabItem[],
131
+ index: number,
132
+ disabled: boolean
133
+ ): TabItem[] {
134
+ return items.map((item, i) => {
135
+ if (i === index) {
136
+ return {
137
+ ...item,
138
+ disabled
139
+ }
140
+ }
141
+ return item
142
+ })
143
+ }
144
+
145
+ /**
146
+ * 生成标签样式
147
+ */
148
+ export function generateTabStyles(options: {
149
+ type?: string
150
+ position?: string
151
+ height?: number
152
+ background?: string
153
+ border?: boolean
154
+ }): Record<string, any> {
155
+ const styles: Record<string, any> = {}
156
+
157
+ // 基础样式
158
+ if (options.height) {
159
+ styles.height = `${options.height}px`
160
+ }
161
+
162
+ if (options.background) {
163
+ styles.backgroundColor = options.background
164
+ }
165
+
166
+ // 位置相关样式
167
+ if (options.position === 'left' || options.position === 'right') {
168
+ styles.flexDirection = 'column'
169
+ }
170
+
171
+ // 边框样式
172
+ if (options.border) {
173
+ if (options.position === 'top') {
174
+ styles.borderBottom = '1px solid #ebedf0'
175
+ } else if (options.position === 'bottom') {
176
+ styles.borderTop = '1px solid #ebedf0'
177
+ }
178
+ }
179
+
180
+ return styles
181
+ }