im-ui-mobile 0.1.34 → 0.1.35

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.
@@ -1,123 +1,568 @@
1
1
  <template>
2
- <view class="avatar none-pointer-events" @click="showUserInfo" :title="title">
3
- <image v-if="url" class="avatar-image" :src="url" :style="avatarImageStyle" lazy-load="true"
4
- mode="aspectFill" />
5
- <view v-else-if="title" class="avatar-text" :style="avatarTextStyle">
6
- {{ title?.substring(0, 1).toUpperCase() }}
7
- </view>
8
- <view v-if="online" class="online" title="用户当前在线" />
9
- <slot />
10
- </view>
2
+ <!-- 角标 -->
3
+ <im-badge v-if="badge && url" :value="badgeText" :dot="badgeDot">
4
+ <view class="im-avatar" :class="[
5
+ `im-avatar--${size}`,
6
+ {
7
+ 'im-avatar--square': shape === 'square',
8
+ 'im-avatar--rounded': shape === 'rounded',
9
+ 'im-avatar--circle': shape === 'circle',
10
+ 'im-avatar--with-border': border,
11
+ 'im-avatar--clickable': clickable
12
+ }
13
+ ]" :style="[customStyle, avatarStyle]" @click="handleClick" @longpress="handleLongPress">
14
+ <!-- 图片头像 -->
15
+ <image v-if="url" :class="['im-avatar__image',
16
+ {
17
+ 'im-avatar__image--square': shape === 'square',
18
+ 'im-avatar__image--rounded': shape === 'rounded',
19
+ 'im-avatar__image--circle': shape === 'circle',
20
+ 'im-avatar__image--with-border': border,
21
+ }
22
+ ]" :src="url" :mode="imageMode" :show-menu-by-longpress="showMenuByLongpress" @error="handleImageError" />
23
+ </view>
24
+ </im-badge>
25
+ <view v-else class="im-avatar" :class="[
26
+ `im-avatar--${size}`,
27
+ {
28
+ 'im-avatar--square': shape === 'square',
29
+ 'im-avatar--rounded': shape === 'rounded',
30
+ 'im-avatar--circle': shape === 'circle',
31
+ 'im-avatar--with-border': border,
32
+ 'im-avatar--clickable': clickable
33
+ }
34
+ ]" :style="[customStyle, avatarStyle]" @click="handleClick" @longpress="handleLongPress">
35
+ <!-- 图片头像 -->
36
+ <image v-if="url" :class="['im-avatar__image',
37
+ {
38
+ 'im-avatar__image--square': shape === 'square',
39
+ 'im-avatar__image--rounded': shape === 'rounded',
40
+ 'im-avatar__image--circle': shape === 'circle',
41
+ 'im-avatar__image--with-border': border,
42
+ }
43
+ ]" :src="url" :mode="imageMode" :show-menu-by-longpress="showMenuByLongpress" @error="handleImageError" />
44
+
45
+ <!-- 文字头像(无图片时显示) -->
46
+ <text v-else-if="title" class="im-avatar__text" :style="textStyle">
47
+ {{ displayText }}
48
+ </text>
49
+
50
+ <!-- 默认图标(无图片和文字时显示) -->
51
+ <text v-else-if="icon" class="im-avatar__icon">
52
+ <im-icon :name="icon" :color="iconColor" :size="iconSize" />
53
+ </text>
54
+
55
+ <!-- 在线状态指示器 -->
56
+ <view v-if="showStatus && status" class="im-avatar__status" :class="`im-avatar__status--${status}`" />
57
+
58
+ <!-- 角标 -->
59
+ <im-badge v-if="badge" :value="badgeText" :dot="badgeDot" :class="[`im-avatar__badge`]" :size="badgeSize" />
60
+
61
+ <!-- 加载状态 -->
62
+ <view v-if="loading" class="im-avatar__loading">
63
+ <im-loading v-if="loading" :size="32" text="" :mask="true" :class="[
64
+ {
65
+ 'im-avatar__loading--square': shape === 'square',
66
+ 'im-avatar__loading--rounded': shape === 'rounded',
67
+ 'im-avatar__loading--circle': shape === 'circle'
68
+ }
69
+ ]" />
70
+ </view>
71
+ </view>
11
72
  </template>
12
73
 
13
74
  <script setup lang="ts">
14
75
  import { computed } from 'vue'
76
+ import ImIcon from '../im-icon/im-icon.vue'
77
+ import { validator } from '../../index'
15
78
 
79
+ // 定义组件Props
16
80
  interface Props {
17
- id?: number;
18
- size?: number | string;
19
- url?: string;
20
- title?: string;
21
- radius?: string;
22
- online?: boolean;
81
+ // 基础属性
82
+ id?: string | number
83
+ url?: string // 头像图片地址
84
+ title?: string // 显示的文字(无图片时)
85
+ size?: 'mini' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' // 尺寸
86
+ shape?: 'circle' | 'square' | 'rounded' // 形状
87
+ border?: boolean // 是否显示边框
88
+ borderColor?: string // 边框颜色
89
+
90
+ // 样式控制
91
+ bgColor?: string // 背景颜色(无图片时)
92
+ textColor?: string // 文字颜色(无图片时)
93
+ fontSize?: number // 文字大小(无图片时)
94
+ fontWeight?: number | string // 文字粗细
95
+
96
+ // 图标设置
97
+ icon?: string // 图标字符(无图片和文字时)
98
+ iconColor?: string // 图标颜色
99
+ iconSize?: number // 图标大小
100
+
101
+ // 状态指示
102
+ status?: 'online' | 'offline' | 'busy' | 'away' // 在线状态
103
+ showStatus?: boolean // 是否显示状态指示器
104
+ badge?: string | number | boolean // 角标内容
105
+ badgeDot?: boolean // 是否显示圆点角标
106
+ badgeMax?: number // 角标最大值
107
+ badgeColor?: string // 角标背景色
108
+ badgeTextColor?: string // 角标文字颜色
109
+
110
+ // 交互功能
111
+ clickable?: boolean // 是否可点击
112
+ loading?: boolean // 加载状态
113
+ showMenuByLongpress?: boolean // 是否允许长按显示菜单
114
+
115
+ // 图片相关
116
+ imageMode?: 'scaleToFill' | 'aspectFit' | 'aspectFill' | 'widthFix' | 'heightFix' | 'top' | 'bottom' | 'center' | 'left' | 'right' | 'top left' | 'top right' | 'bottom left' | 'bottom right'
117
+
118
+ // 自定义样式
119
+ customStyle?: Record<string, string | number>
23
120
  }
24
121
 
122
+ // 默认Props
25
123
  const props = withDefaults(defineProps<Props>(), {
26
- size: 'default',
27
- title: '',
28
- radius: "50%",
29
- online: false
30
- });
31
-
32
- interface Emits {
33
- (e: 'click', id: number): void
124
+ size: 'medium',
125
+ shape: 'circle',
126
+ border: false,
127
+ borderColor: '#dcdfe6',
128
+ bgColor: '#409eff',
129
+ textColor: '#ffffff',
130
+ fontSize: undefined,
131
+ fontWeight: 500,
132
+ icon: '👤',
133
+ iconColor: '#909399',
134
+ iconSize: undefined,
135
+ status: 'offline',
136
+ showStatus: false,
137
+ badge: undefined,
138
+ badgeDot: false,
139
+ badgeMax: 99,
140
+ badgeColor: '#ff4d4f',
141
+ badgeTextColor: '#ffffff',
142
+ clickable: false,
143
+ loading: false,
144
+ showMenuByLongpress: false,
145
+ imageMode: 'aspectFill',
146
+ customStyle: () => ({})
147
+ })
148
+
149
+ // 定义事件
150
+ const emit = defineEmits<{
151
+ click: [id?: string | number]
152
+ longpress: [id?: string | number]
153
+ error: [error: Error]
154
+ }>()
155
+
156
+ // 计算属性
157
+ const displayText = computed(() => {
158
+ if (!props.title) return ''
159
+ // 取最后一个字符或前两个字符
160
+ if (props.title.length <= 2) {
161
+ return props.title
162
+ }
163
+ // 中文取最后两个字符,英文取首字母
164
+ const isChinese = /[\u4e00-\u9fa5]/.test(props.title)
165
+ if (isChinese) {
166
+ return props.title.slice(-2)
167
+ } else {
168
+ const words = props.title.split(' ').filter(word => word.length > 0)
169
+ if (words.length >= 2) {
170
+ return words[0][0] + words[1][0]
171
+ } else {
172
+ return props.title.slice(0, 2)
173
+ }
174
+ }
175
+ })
176
+
177
+ const avatarStyle = computed(() => {
178
+ const style: Record<string, string> = {}
179
+
180
+ if (props.bgColor && !props.url) {
181
+ style.backgroundColor = props.bgColor
182
+ }
183
+
184
+ if (props.border) {
185
+ style.border = `2rpx solid ${props.borderColor}`
186
+ }
187
+
188
+ return style
189
+ })
190
+
191
+ const textStyle = computed(() => {
192
+ const style: Record<string, string> = {
193
+ color: props.textColor,
194
+ fontWeight: String(props.fontWeight)
195
+ }
196
+
197
+ // 根据尺寸设置字体大小
198
+ if (props.fontSize) {
199
+ style.fontSize = `${props.fontSize}rpx`
200
+ }
201
+
202
+ return style
203
+ })
204
+
205
+ // const iconStyle = computed(() => {
206
+ // const style: Record<string, string> = {
207
+ // color: props.iconColor
208
+ // }
209
+
210
+ // if (props.iconSize) {
211
+ // style.fontSize = `${props.iconSize}rpx`
212
+ // }
213
+
214
+ // return style
215
+ // })
216
+
217
+ // const badgeClass = computed(() => {
218
+ // if (props.badgeColor) {
219
+ // return ''
220
+ // }
221
+ // return 'im-avatar__badge--default'
222
+ // })
223
+
224
+ const badgeSize = computed(() => {
225
+ switch (props.size) {
226
+ case 'mini':
227
+ return 'small'
228
+ case 'small':
229
+ case 'medium':
230
+ case 'large':
231
+ return props.size
232
+ case 'xlarge':
233
+ case 'xxlarge':
234
+ return 'large'
235
+ }
236
+ })
237
+
238
+ const badgeText = computed(() => {
239
+ if (!props.badge) return ''
240
+
241
+ if (validator.isNumber(String(props.badge))) {
242
+ const badgeNum = Number(props.badge)
243
+
244
+ if (badgeNum > props.badgeMax) {
245
+ return `${props.badgeMax}+`
246
+ }
247
+
248
+ return String(props.badge)
249
+ }
250
+
251
+ // const badgeNum = Number(props.badge)
252
+
253
+ // if (isNaN(badgeNum)) {
254
+ // return String(props.badge)
255
+ // }
256
+
257
+ // if (badgeNum > props.badgeMax) {
258
+ // return `${props.badgeMax}+`
259
+ // }
260
+
261
+ return String(props.badge)
262
+ })
263
+
264
+ // 事件处理
265
+ const handleClick = () => {
266
+ if (props.clickable && !props.loading) {
267
+ emit('click', props.id)
268
+ }
269
+ }
270
+
271
+ const handleLongPress = () => {
272
+ if (props.clickable && !props.loading) {
273
+ emit('longpress', props.id)
274
+ }
275
+ }
276
+
277
+ const handleImageError = (error: any) => {
278
+ emit('error', error)
34
279
  }
35
- const emit = defineEmits<Emits>()
36
-
37
- const colors = [
38
- "#5daa31", "#c7515a", "#e03697", "#85029b",
39
- "#c9b455", "#326eb6"
40
- ];
41
-
42
- const showUserInfo = () => {
43
- if (props.id && props.id > 0) {
44
- // TODO:组件化
45
- // uni.navigateTo({ url: "/pages/common/user-info?id=" + props.id });
46
- emit('click', props.id)
47
- }
48
- };
49
-
50
- const _size = computed(() => {
51
- if (typeof props.size === 'number') {
52
- return props.size;
53
- } else if (typeof props.size === 'string') {
54
- const sizeMap: Record<string, number> = {
55
- 'default': 96,
56
- 'small': 84,
57
- 'smaller': 72,
58
- 'mini': 60,
59
- 'minier': 48,
60
- 'lage': 108,
61
- 'lager': 120,
62
- };
63
- return sizeMap[props.size] || 96;
64
- }
65
- return 96;
66
- });
67
-
68
- const avatarImageStyle = computed(() => {
69
- return `width:${_size.value}rpx;height:${_size.value}rpx;`;
70
- });
71
-
72
- const avatarTextStyle = computed(() => {
73
- return `width: ${_size.value}rpx;
74
- height:${_size.value}rpx;
75
- background: linear-gradient(145deg,#ffffff20 25%,#00000060),${textColor.value};
76
- font-size:${_size.value * 0.45}rpx;
77
- border-radius: ${props.radius};`;
78
- });
79
-
80
- const textColor = computed(() => {
81
- if (!props.title) {
82
- return '#fff';
83
- }
84
- let hash = 0;
85
- for (let i = 0; i < props.title.length; i++) {
86
- hash += props.title.charCodeAt(i);
87
- }
88
- return colors[hash % colors.length];
89
- });
90
280
  </script>
91
281
 
92
- <style scoped lang="scss">
93
- .avatar {
94
- position: relative;
95
- cursor: pointer;
96
-
97
- .avatar-image {
98
- position: relative;
99
- overflow: hidden;
100
- border-radius: 50%;
101
- vertical-align: bottom;
102
- }
103
-
104
- .avatar-text {
105
- color: white;
106
- border-radius: 50%;
107
- display: flex;
108
- align-items: center;
109
- justify-content: center;
110
- }
111
-
112
- .online {
113
- position: absolute;
114
- right: -10%;
115
- bottom: 0;
116
- width: 24rpx;
117
- height: 24rpx;
118
- background: rgb(108, 198, 108);
119
- border-radius: 50%;
120
- border: 6rpx solid white;
121
- }
282
+ <style lang="scss" scoped>
283
+ @import '../../styles/variables.scss';
284
+
285
+ .im-avatar {
286
+ position: relative;
287
+ display: inline-flex;
288
+ align-items: center;
289
+ justify-content: center;
290
+ box-sizing: border-box;
291
+ vertical-align: middle;
292
+ transition: all 0.3s ease;
293
+ user-select: none;
294
+ background-color: #ffffff;
295
+
296
+ // 形状
297
+ &--circle {
298
+ border-radius: 50%;
299
+ }
300
+
301
+ &--square {
302
+ border-radius: 8rpx;
303
+ }
304
+
305
+ &--rounded {
306
+ border-radius: 20%;
307
+ }
308
+
309
+ &--with-border {
310
+ box-shadow: 0 6rpx 12rpx rgba(0, 0, 0, 0.3);
311
+ }
312
+
313
+ &--clickable {
314
+ cursor: pointer;
315
+
316
+ &:active {
317
+ opacity: 0.8;
318
+ transform: scale(0.95);
319
+ }
320
+ }
321
+
322
+ // 尺寸
323
+ &--mini {
324
+ width: 64rpx;
325
+ height: 64rpx;
326
+ font-size: 24rpx;
327
+
328
+ .im-avatar__text {
329
+ font-size: 24rpx;
330
+ }
331
+
332
+ .im-avatar__icon {
333
+ font-size: 32rpx;
334
+ }
335
+ }
336
+
337
+ &--small {
338
+ width: 80rpx;
339
+ height: 80rpx;
340
+ font-size: 28rpx;
341
+
342
+ .im-avatar__text {
343
+ font-size: 28rpx;
344
+ }
345
+
346
+ .im-avatar__icon {
347
+ font-size: 40rpx;
348
+ }
349
+ }
350
+
351
+ &--medium {
352
+ width: 100rpx;
353
+ height: 100rpx;
354
+ font-size: 32rpx;
355
+
356
+ .im-avatar__text {
357
+ font-size: 32rpx;
358
+ }
359
+
360
+ .im-avatar__icon {
361
+ font-size: 48rpx;
362
+ }
363
+ }
364
+
365
+ &--large {
366
+ width: 120rpx;
367
+ height: 120rpx;
368
+ font-size: 36rpx;
369
+
370
+ .im-avatar__text {
371
+ font-size: 36rpx;
372
+ }
373
+
374
+ .im-avatar__icon {
375
+ font-size: 56rpx;
376
+ }
377
+ }
378
+
379
+ &--xlarge {
380
+ width: 160rpx;
381
+ height: 160rpx;
382
+ font-size: 48rpx;
383
+
384
+ .im-avatar__text {
385
+ font-size: 48rpx;
386
+ }
387
+
388
+ .im-avatar__icon {
389
+ font-size: 72rpx;
390
+ }
391
+ }
392
+
393
+ &--xxlarge {
394
+ width: 200rpx;
395
+ height: 200rpx;
396
+ font-size: 60rpx;
397
+
398
+ .im-avatar__text {
399
+ font-size: 60rpx;
400
+ }
401
+
402
+ .im-avatar__icon {
403
+ font-size: 88rpx;
404
+ }
405
+ }
406
+
407
+ // 子元素
408
+ &__image {
409
+ width: 100%;
410
+ height: 100%;
411
+ object-fit: cover;
412
+
413
+ // 形状
414
+ &--circle {
415
+ border-radius: 50%;
416
+ }
417
+
418
+ &--square {
419
+ border-radius: 8rpx;
420
+ }
421
+
422
+ &--rounded {
423
+ border-radius: 20%;
424
+ }
425
+
426
+ &--with-border {
427
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
428
+ }
429
+ }
430
+
431
+ &__text {
432
+ font-weight: 500;
433
+ line-height: 1;
434
+ text-transform: uppercase;
435
+ }
436
+
437
+ &__icon {
438
+ line-height: 1;
439
+ }
440
+
441
+ // 状态指示器
442
+ &__status {
443
+ position: absolute;
444
+ right: 0;
445
+ bottom: 0;
446
+ width: 20rpx;
447
+ height: 20rpx;
448
+ border: 2rpx solid white;
449
+ border-radius: 50%;
450
+
451
+ &--online {
452
+ background-color: #07c160;
453
+ }
454
+
455
+ &--offline {
456
+ background-color: #c8c9cc;
457
+ }
458
+
459
+ &--busy {
460
+ background-color: #ff976a;
461
+ }
462
+
463
+ &--away {
464
+ background-color: #ffd700;
465
+ }
466
+
467
+ // 根据尺寸调整位置和大小
468
+ .im-avatar--mini &,
469
+ .im-avatar--small & {
470
+ width: 16rpx;
471
+ height: 16rpx;
472
+ right: -2rpx;
473
+ bottom: -2rpx;
474
+ }
475
+
476
+ .im-avatar--large &,
477
+ .im-avatar--xlarge &,
478
+ .im-avatar--xxlarge & {
479
+ width: 24rpx;
480
+ height: 24rpx;
481
+ right: 4rpx;
482
+ bottom: 4rpx;
483
+ }
484
+ }
485
+
486
+ // 角标
487
+ &__badge {
488
+
489
+ // 尺寸
490
+ .im-avatar--mini & {
491
+ top: -30rpx;
492
+ right: -6rpx;
493
+ }
494
+
495
+ .im-avatar--small & {
496
+ top: -38rpx;
497
+ right: -8rpx;
498
+ }
499
+
500
+ .im-avatar--medium & {
501
+ top: -48rpx;
502
+ right: -14rpx;
503
+ }
504
+
505
+ .im-avatar--large & {
506
+ top: -56rpx;
507
+ right: -20rpx;
508
+ }
509
+
510
+ .im-avatar--xlarge & {
511
+ top: -72rpx;
512
+ right: -25rpx;
513
+ }
514
+
515
+ .im-avatar--xxlarge & {
516
+ top: -88rpx;
517
+ right: -30rpx;
518
+ }
519
+ }
520
+
521
+ // 加载状态
522
+ &__loading {
523
+ position: absolute;
524
+ top: 0;
525
+ left: 0;
526
+ width: 100%;
527
+ height: 100%;
528
+ display: flex;
529
+ align-items: center;
530
+ justify-content: center;
531
+ z-index: 2;
532
+
533
+ // 形状
534
+ &--circle {
535
+ border-radius: 50%;
536
+ }
537
+
538
+ &--square {
539
+ border-radius: 8rpx;
540
+ }
541
+
542
+ &--rounded {
543
+ border-radius: 20%;
544
+ }
545
+ }
546
+ }
547
+
548
+ // 暗黑模式适配
549
+ @media (prefers-color-scheme: dark) {
550
+ .im-avatar {
551
+ &--with-border {
552
+ border-color: #5c5c5c;
553
+ }
554
+
555
+ &__status {
556
+ border-color: #2c2c2c;
557
+ }
558
+
559
+ &__badge {
560
+
561
+ &--dot,
562
+ &--number {
563
+ border-color: #2c2c2c;
564
+ }
565
+ }
566
+ }
122
567
  }
123
568
  </style>
@@ -0,0 +1,123 @@
1
+ <template>
2
+ <view class="avatar none-pointer-events" @click="showUserInfo" :title="title">
3
+ <image v-if="url" class="avatar-image" :src="url" :style="avatarImageStyle" lazy-load="true"
4
+ mode="aspectFill" />
5
+ <view v-else-if="title" class="avatar-text" :style="avatarTextStyle">
6
+ {{ title?.substring(0, 1).toUpperCase() }}
7
+ </view>
8
+ <view v-if="online" class="online" title="用户当前在线" />
9
+ <slot />
10
+ </view>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { computed } from 'vue'
15
+
16
+ interface Props {
17
+ id?: number;
18
+ size?: number | string;
19
+ url?: string;
20
+ title?: string;
21
+ radius?: string;
22
+ online?: boolean;
23
+ }
24
+
25
+ const props = withDefaults(defineProps<Props>(), {
26
+ size: 'default',
27
+ title: '',
28
+ radius: "50%",
29
+ online: false
30
+ });
31
+
32
+ interface Emits {
33
+ (e: 'click', id: number): void
34
+ }
35
+ const emit = defineEmits<Emits>()
36
+
37
+ const colors = [
38
+ "#5daa31", "#c7515a", "#e03697", "#85029b",
39
+ "#c9b455", "#326eb6"
40
+ ];
41
+
42
+ const showUserInfo = () => {
43
+ if (props.id && props.id > 0) {
44
+ // TODO:组件化
45
+ // uni.navigateTo({ url: "/pages/common/user-info?id=" + props.id });
46
+ emit('click', props.id)
47
+ }
48
+ };
49
+
50
+ const _size = computed(() => {
51
+ if (typeof props.size === 'number') {
52
+ return props.size;
53
+ } else if (typeof props.size === 'string') {
54
+ const sizeMap: Record<string, number> = {
55
+ 'default': 96,
56
+ 'small': 84,
57
+ 'smaller': 72,
58
+ 'mini': 60,
59
+ 'minier': 48,
60
+ 'lage': 108,
61
+ 'lager': 120,
62
+ };
63
+ return sizeMap[props.size] || 96;
64
+ }
65
+ return 96;
66
+ });
67
+
68
+ const avatarImageStyle = computed(() => {
69
+ return `width:${_size.value}rpx;height:${_size.value}rpx;`;
70
+ });
71
+
72
+ const avatarTextStyle = computed(() => {
73
+ return `width: ${_size.value}rpx;
74
+ height:${_size.value}rpx;
75
+ background: linear-gradient(145deg,#ffffff20 25%,#00000060),${textColor.value};
76
+ font-size:${_size.value * 0.45}rpx;
77
+ border-radius: ${props.radius};`;
78
+ });
79
+
80
+ const textColor = computed(() => {
81
+ if (!props.title) {
82
+ return '#fff';
83
+ }
84
+ let hash = 0;
85
+ for (let i = 0; i < props.title.length; i++) {
86
+ hash += props.title.charCodeAt(i);
87
+ }
88
+ return colors[hash % colors.length];
89
+ });
90
+ </script>
91
+
92
+ <style scoped lang="scss">
93
+ .avatar {
94
+ position: relative;
95
+ cursor: pointer;
96
+
97
+ .avatar-image {
98
+ position: relative;
99
+ overflow: hidden;
100
+ border-radius: 50%;
101
+ vertical-align: bottom;
102
+ }
103
+
104
+ .avatar-text {
105
+ color: white;
106
+ border-radius: 50%;
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ }
111
+
112
+ .online {
113
+ position: absolute;
114
+ right: -10%;
115
+ bottom: 0;
116
+ width: 24rpx;
117
+ height: 24rpx;
118
+ background: rgb(108, 198, 108);
119
+ border-radius: 50%;
120
+ border: 6rpx solid white;
121
+ }
122
+ }
123
+ </style>
@@ -3,7 +3,7 @@
3
3
  <!--rich-text中的表情包会屏蔽事件,所以这里用一个遮罩层捕获点击事件 -->
4
4
  <view class="mask" @tap="showChatBox()"></view>
5
5
  <view class="left">
6
- <im-avatar :url="chat.avatar" :title="chat.displayName" :online="online" />
6
+ <im-avatar :url="chat.avatar" :title="chat.displayName" :status="online ? 'online' : 'offline'" :show-status="online" />
7
7
  </view>
8
8
  <view class="chat-right">
9
9
  <view class="chat-name">
@@ -1,6 +1,7 @@
1
1
  <template>
2
2
  <view class="friend-item" @click="onClick()">
3
- <im-avatar :title="friend.nickName" :online="friend.online" :url="friend.avatar" size="small" />
3
+ <im-avatar :title="friend.nickName" :url="friend.avatar" size="small"
4
+ :status="friend.online ? 'online' : 'offline'" :show-status="friend.online" />
4
5
  <view class="friend-info">
5
6
  <view class="friend-name">{{ friend.nickName }}</view>
6
7
  <view class="friend-online">
@@ -2,7 +2,7 @@
2
2
  <view class="loading" :style="loadingStyle">
3
3
  <view :style="icontStyle">
4
4
  <im-icon name="loading" :color="iconColor" :size="size" spin :spin-speed="2" />
5
- <text class="loading-text">{{ text }}</text>
5
+ <text v-if="text" class="loading-text">{{ text }}</text>
6
6
  </view>
7
7
  <slot></slot>
8
8
  </view>
@@ -21,8 +21,8 @@
21
21
  <im-virtual-list :data="memberItems">
22
22
  <template v-slot="{ item }">
23
23
  <view class="member-item" @click="onSwitchChecked(item)">
24
- <im-avatar :title="item.showNickName" :online="item.online" :url="item.avatar"
25
- size="small" />
24
+ <im-avatar :title="item.showNickName" :url="item.avatar" size="small"
25
+ :status="item.online ? 'online' : 'offline'" :show-status="item.online" />
26
26
  <view class="member-name">{{ item.showNickName }}</view>
27
27
  <radio :checked="item.checked" :disabled="item.locked"
28
28
  @click.stop="onSwitchChecked(item)" />
@@ -2,15 +2,15 @@
2
2
  <im-popup ref="popup" position="bottom">
3
3
  <view class="read-receipt">
4
4
  <view class="uni-padding-wrap uni-common-mt">
5
- <im-tabs v-model="current" :items="items" @change="onClickItem" />
5
+ <im-tabs v-model="current" :items="items" @change="onClickItem" />
6
6
  </view>
7
7
  <view class="content">
8
8
  <view v-if="current === 0">
9
9
  <im-virtual-list :data="readedMembers">
10
10
  <template v-slot="{ item }">
11
11
  <view class="member-item">
12
- <im-avatar :title="item.showNickName" :online="item.online" :url="item.avatar"
13
- :size="90" />
12
+ <im-avatar :title="item.showNickName" :url="item.avatar" :size="90"
13
+ :status="item.online ? 'online' : 'offline'" :show-status="item.online" />
14
14
  <view class="member-name">{{ item.showNickName }}</view>
15
15
  </view>
16
16
  </template>
@@ -20,8 +20,8 @@
20
20
  <im-virtual-list :data="unreadMembers">
21
21
  <template v-slot="{ item }">
22
22
  <view class="member-item">
23
- <im-avatar :title="item.showNickName" :online="item.online" :url="item.avatar"
24
- :size="90" />
23
+ <im-avatar :title="item.showNickName" :url="item.avatar" :size="90"
24
+ :status="item.online ? 'online' : 'offline'" :show-status="item.online" />
25
25
  <view class="member-name">{{ item.showNickName }}</view>
26
26
  </view>
27
27
  </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "im-ui-mobile",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "A Vue3.0 + Typescript instant messaging component library for Uniapp",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,17 +1,72 @@
1
1
  import { AllowedComponentProps, VNodeProps } from '../common'
2
2
 
3
3
  declare interface AvatarProps {
4
- id?: number
5
- size?: number | string
4
+ // 基础属性
5
+ id?: string | number
6
+ size?: 'mini' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' | number | string
7
+ shape?: 'circle' | 'square' | 'rounded'
8
+
9
+ // 头像内容
6
10
  url?: string
7
- name?: string
8
- radius?: string
9
- online?: boolean
11
+ title?: string
12
+ name?: string // 兼容 name 属性
13
+ icon?: string
14
+
15
+ // 样式控制
16
+ bgColor?: string
17
+ textColor?: string
18
+ fontSize?: number | string
19
+ fontWeight?: number | string
20
+ iconColor?: string
21
+ iconSize?: number | string
22
+
23
+ // 边框
24
+ border?: boolean
25
+ borderColor?: string
26
+ radius?: string | number // 兼容 radius 属性
27
+
28
+ // 状态指示
29
+ status?: 'online' | 'offline' | 'busy' | 'away'
30
+ showStatus?: boolean
31
+ online?: boolean // 兼容 online 属性
32
+
33
+ // 角标
34
+ badge?: string | number
35
+ badgeDot?: boolean
36
+ badgeMax?: number
37
+ badgeColor?: string
38
+ badgeTextColor?: string
39
+
40
+ // 交互
41
+ clickable?: boolean
42
+ loading?: boolean
43
+ showMenuByLongpress?: boolean
44
+
45
+ // 图片设置
46
+ imageMode?: 'scaleToFill' | 'aspectFit' | 'aspectFill' | 'widthFix' | 'heightFix' |
47
+ 'top' | 'bottom' | 'center' | 'left' | 'right' |
48
+ 'top left' | 'top right' | 'bottom left' | 'bottom right'
49
+
50
+ // 自定义样式
51
+ customStyle?: Record<string, string | number> | string
52
+ }
53
+
54
+ declare interface AvatarSlots {
55
+ default?: never
56
+ // 未来可能支持插槽
57
+ }
58
+
59
+ declare interface AvatarEmits {
60
+ (e: 'click', id?: string | number): void
61
+ (e: 'longpress', id?: string | number): void
62
+ (e: 'error', error: Error): void
10
63
  }
11
64
 
12
65
  declare interface _Avatar {
13
66
  new(): {
14
67
  $props: AllowedComponentProps & VNodeProps & AvatarProps
68
+ $slots: AvatarSlots
69
+ $emit: AvatarEmits
15
70
  }
16
71
  }
17
72
 
@@ -20,5 +75,7 @@ export declare const Avatar: _Avatar
20
75
  export default Avatar
21
76
 
22
77
  export type {
23
- AvatarProps
78
+ AvatarProps,
79
+ AvatarEmits,
80
+ AvatarSlots
24
81
  }
@@ -0,0 +1,24 @@
1
+ import { AllowedComponentProps, VNodeProps } from '../common'
2
+
3
+ declare interface AvatarProps {
4
+ id?: number
5
+ size?: number | string
6
+ url?: string
7
+ name?: string
8
+ radius?: string
9
+ online?: boolean
10
+ }
11
+
12
+ declare interface _Avatar {
13
+ new(): {
14
+ $props: AllowedComponentProps & VNodeProps & AvatarProps
15
+ }
16
+ }
17
+
18
+ export declare const Avatar: _Avatar
19
+
20
+ export default Avatar
21
+
22
+ export type {
23
+ AvatarProps
24
+ }
@@ -1,7 +1,6 @@
1
1
  declare module 'vue' {
2
2
  export interface GlobalComponents {
3
3
  ['im-cell']: typeof import('./components/cell')['Cell']
4
- ['im-cell-switch']: typeof import('./components/cell-switch')['CellSwitch']
5
4
  ['im-cell-group']: typeof import('./components/cell-group')['CellGroup']
6
5
  ['im-button']: typeof import('./components/button')['Button']
7
6
  ['im-badge']: typeof import('./components/badge')['Badge']
@@ -1,225 +0,0 @@
1
- <template>
2
- <ImCell :type="type" :size="size" :disabled="disabled || switchDisabled" :border="border" :hover="hover"
3
- :clickable="clickable" :icon="icon" :iconPosition="iconPosition" :avatar="avatar" :title="title"
4
- :titleSize="titleSize" :description="description" :descriptionSize="descriptionSize" :value="value"
5
- :label="label" :badge="badge" :padding="padding" :margin="margin" :bgColor="bgColor" :textColor="textColor"
6
- @click="handleCellClick" @longpress="handleLongPress">
7
- <!-- 传递所有插槽 -->
8
- <!-- <template v-if="icon || $slots.icon" #icon>
9
- <slot name="icon" />
10
- </template>
11
-
12
- <template v-if="avatar || $slots.avatar" #avatar>
13
- <slot name="avatar" />
14
- </template>
15
-
16
- <template v-if="title || $slots.title" #title>
17
- <slot name="title" />
18
- </template>
19
-
20
- <template v-if="description || $slots.description" #description>
21
- <slot name="description" />
22
- </template>
23
-
24
- <template v-if="arrow || $slots.arrow" #arrow>
25
- <slot name="arrow" />
26
- </template>
27
-
28
- <template v-if="label || $slots.label" #label>
29
- <slot name="label" />
30
- </template>
31
-
32
- <template v-if="badge || $slots.badge" #badge>
33
- <slot name="badge" />
34
- </template> -->
35
-
36
- <!-- 自定义右侧内容,添加 Switch -->
37
- <template #value>
38
- <view class="im-cell-switch__wrapper">
39
- <switch v-if="showSwitch" :checked="checked" :disabled="disabled || switchDisabled" :color="switchColor"
40
- :backgroundColor="switchBackgroundColor" @change="handleSwitchChange" />
41
- <text v-else class="im-cell-switch__loading">加载中...</text>
42
- </view>
43
- </template>
44
- </ImCell>
45
- </template>
46
-
47
- <script setup lang="ts">
48
- import { ref, computed, watch } from 'vue'
49
- import ImCell from '../im-cell/im-cell.vue'
50
-
51
- // 定义 Props 接口
52
- interface Props {
53
- // 继承自 ImCell 的 Props
54
- type?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info'
55
- size?: 'small' | 'medium' | 'large'
56
- disabled?: boolean
57
- border?: boolean
58
- hover?: boolean
59
- clickable?: boolean
60
- icon?: string
61
- iconPosition?: 'left' | 'right'
62
- avatar?: string
63
- title?: string
64
- titleSize?: 'small' | 'medium' | 'large'
65
- description?: string
66
- descriptionSize?: 'small' | 'medium' | 'large'
67
- value?: string
68
- label?: string
69
- badge?: boolean | number | string
70
- padding?: string
71
- margin?: string
72
- bgColor?: string
73
- textColor?: string
74
- arrow?: boolean
75
-
76
- // Switch 特有 Props
77
- checked?: boolean
78
- switchDisabled?: boolean
79
- switchColor?: string
80
- switchBackgroundColor?: string
81
- loading?: boolean
82
- async?: boolean
83
- beforeChange?: (newValue: boolean) => Promise<boolean> | boolean
84
- }
85
-
86
- // 定义 Emits 接口
87
- interface Emits {
88
- (e: 'update:checked', value: boolean): void
89
- (e: 'change', value: boolean): void
90
- (e: 'click', event: PointerEvent): void
91
- (e: 'longpress', event: TouchEvent): void
92
- }
93
-
94
- // Props 和 Emits
95
- const props = withDefaults(defineProps<Props>(), {
96
- type: 'default',
97
- size: 'medium',
98
- disabled: false,
99
- border: true,
100
- hover: true,
101
- clickable: true,
102
- iconPosition: 'left',
103
- titleSize: 'medium',
104
- descriptionSize: 'small',
105
- switchDisabled: false,
106
- switchColor: '#409EFF',
107
- switchBackgroundColor: '#DCDFE6',
108
- loading: false,
109
- async: false,
110
- arrow: false
111
- })
112
-
113
- const emit = defineEmits<Emits>()
114
-
115
- // 响应式数据
116
- const isLoading = ref(false)
117
- const internalValue = ref(props.checked)
118
-
119
- // 计算属性
120
- const showSwitch = computed(() => {
121
- return !props.loading && !isLoading.value
122
- })
123
-
124
- // 监听外部值变化
125
- watch(() => props.checked, (newValue) => {
126
- internalValue.value = newValue
127
- })
128
-
129
- // 事件处理
130
- const handleSwitchChange = async (event: any) => {
131
- const newValue = event.detail.value
132
-
133
- // 如果设置了异步切换
134
- if (props.async || props.beforeChange) {
135
- await handleAsyncChange(newValue)
136
- } else {
137
- updateValue(newValue)
138
- }
139
- }
140
-
141
- const handleAsyncChange = async (newValue: boolean) => {
142
- isLoading.value = true
143
-
144
- try {
145
- let canChange = true
146
-
147
- // 执行 beforeChange 回调
148
- if (props.beforeChange) {
149
- const result = props.beforeChange(newValue)
150
- if (result instanceof Promise) {
151
- canChange = await result
152
- } else {
153
- canChange = result
154
- }
155
- }
156
-
157
- if (canChange) {
158
- updateValue(newValue)
159
- } else {
160
- // 恢复原值
161
- internalValue.value = !newValue
162
- }
163
- } catch (error) {
164
- console.error('切换失败:', error)
165
- // 恢复原值
166
- internalValue.value = !newValue
167
- } finally {
168
- isLoading.value = false
169
- }
170
- }
171
-
172
- const updateValue = (value: boolean) => {
173
- internalValue.value = value
174
- emit('update:checked', value)
175
- emit('change', value)
176
- }
177
-
178
- const handleCellClick = (event: PointerEvent) => {
179
- if (props.disabled || props.switchDisabled) return
180
- emit('click', event)
181
- }
182
-
183
- const handleLongPress = (event: TouchEvent) => {
184
- if (props.disabled || props.switchDisabled) return
185
- emit('longpress', event)
186
- }
187
- </script>
188
-
189
- <style lang="scss" scoped>
190
- .im-cell-switch {
191
- &__wrapper {
192
- display: flex;
193
- align-items: center;
194
- justify-content: center;
195
- min-width: 80rpx;
196
- margin-left: 8rpx;
197
- }
198
-
199
- &__loading {
200
- font-size: 24rpx;
201
- color: #909399;
202
- }
203
- }
204
-
205
- // 覆盖 Switch 样式
206
- :deep(switch) {
207
- transform: scale(0.8);
208
- transform-origin: center;
209
-
210
- &.wx-switch-input {
211
- width: 80rpx;
212
- height: 40rpx;
213
-
214
- &::before {
215
- width: 76rpx;
216
- height: 36rpx;
217
- }
218
-
219
- &::after {
220
- width: 34rpx;
221
- height: 34rpx;
222
- }
223
- }
224
- }
225
- </style>
@@ -1,38 +0,0 @@
1
- import { AllowedComponentProps, VNodeProps } from 'vue'
2
- import { ImCellProps } from './im-cell'
3
-
4
- export interface CellSwitchProps extends Omit<ImCellProps, 'arrow'> {
5
- // 必须的属性
6
- checked?: boolean
7
-
8
- // Switch 特有属性
9
- switchDisabled?: boolean
10
- switchColor?: string
11
- switchBackgroundColor?: string
12
- loading?: boolean
13
- async?: boolean
14
- beforeChange?: (newValue: boolean) => Promise<boolean> | boolean
15
- }
16
-
17
- export interface CellSwitchEmits {
18
- (e: 'update:checked', value: boolean): void
19
- (e: 'change', value: boolean): void
20
- (e: 'click', event: TouchEvent): void
21
- (e: 'longpress', event: TouchEvent): void
22
- }
23
-
24
- declare interface _CellSwitch {
25
- new(): {
26
- $props: AllowedComponentProps & VNodeProps & CellSwitchProps
27
- $emit: CellSwitchEmits
28
- }
29
- }
30
-
31
- export declare const CellSwitch: _CellSwitch
32
-
33
- export default CellSwitch
34
-
35
- export type {
36
- CellSwitchProps,
37
- CellSwitchEmits
38
- }