UserAvatar.vue 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. <template>
  2. <view class="user-avatar" :style="wrapStyle" @click="$emit('click')">
  3. <image v-if="src" class="user-avatar-img" :src="src" mode="aspectFill" :style="imgStyle" />
  4. <view v-else class="user-avatar-placeholder" :class="'gradient-' + gradientIndex" :style="placeholderStyle">
  5. <text class="user-avatar-text" :style="textStyle">{{ displayText }}</text>
  6. </view>
  7. </view>
  8. </template>
  9. <script setup>
  10. import { computed } from 'vue'
  11. /**
  12. * 文字提取:中文 ≤2 全量,>2 取最后两位;英文取前两词首字母大写,一词取前两位
  13. */
  14. function getAvatarText(name) {
  15. const n = String(name || '').trim()
  16. if (!n) return '?'
  17. // 是否主要为中文(CJK):非空字符里 CJK 占多数则按中文处理
  18. const cjkCount = (n.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length
  19. const letterCount = n.replace(/\s/g, '').length
  20. const isCJK = letterCount > 0 && cjkCount >= letterCount / 2
  21. if (isCJK) {
  22. if (n.length <= 2) return n
  23. return n.slice(-2)
  24. }
  25. // 英文/其他:按空格分词
  26. const words = n.split(/\s+/).filter(Boolean)
  27. if (words.length >= 2) {
  28. const a = (words[0][0] || '').toUpperCase()
  29. const b = (words[1][0] || '').toUpperCase()
  30. return (a + b) || '?'
  31. }
  32. if (words.length === 1 && words[0].length >= 2) return words[0].slice(0, 2).toUpperCase()
  33. if (words.length === 1 && words[0].length === 1) return words[0].toUpperCase()
  34. return '?'
  35. }
  36. /** 渐变色库 [左上深, 右下浅],135deg,共 20 组 */
  37. const AVATAR_GRADIENT_PAIRS = [
  38. ['#1e3a8a', '#93c5fd'], ['#166534', '#86efac'], ['#c2410c', '#fdba74'], ['#b91c1c', '#fca5a5'], ['#5b21b6', '#c4b5fd'],
  39. ['#0f766e', '#5eead4'], ['#3730a3', '#a5b4fc'], ['#0d9488', '#2dd4bf'], ['#b45309', '#fcd34d'], ['#be123c', '#fda4af'],
  40. ['#0369a1', '#7dd3fc'], ['#4d7c0f', '#bef264'], ['#86198f', '#e879f9'], ['#475569', '#cbd5e1'], ['#047857', '#6ee7b7'],
  41. ['#6d28d9', '#ddd6fe'], ['#1e40af', '#93c5fd'], ['#ea580c', '#fed7aa'], ['#0e7490', '#99f6e4'], ['#881337', '#fbcfe8'],
  42. ]
  43. /**
  44. * 根据 id 或 name 哈希得到固定索引 0~19,同一 id/name 对应同一种渐变
  45. * id 为空时用 name,确保无 id 时也有稳定渐变
  46. */
  47. function getGradientIndex(id, name) {
  48. const s = String(id || '').trim() || String(name || '').trim()
  49. let hash = 0
  50. for (let i = 0; i < s.length; i++) {
  51. hash = ((hash << 5) - hash + s.charCodeAt(i)) | 0
  52. }
  53. return Math.abs(hash) % AVATAR_GRADIENT_PAIRS.length
  54. }
  55. const props = defineProps({
  56. /** 显示名称,用于占位文字 */
  57. name: { type: String, default: '' },
  58. /** 用户唯一标识,用于占位背景色 */
  59. id: { type: String, default: '' },
  60. /** 头像图片地址,有则显示图片,无则显示占位 */
  61. src: { type: String, default: '' },
  62. /** 尺寸数值,默认 48 */
  63. size: { type: Number, default: 48 },
  64. /** 尺寸单位:'px' | 'rpx',默认 px;rpx 便于在 uni-app 中与页面 rpx 一致 */
  65. unit: { type: String, default: 'px' },
  66. })
  67. defineEmits(['click'])
  68. const displayText = computed(() => getAvatarText(props.name))
  69. const gradientIndex = computed(() => getGradientIndex(props.id, props.name))
  70. const su = computed(() => props.unit || 'px')
  71. const wrapStyle = computed(() => {
  72. const u = su.value
  73. const w = props.size + u
  74. return {
  75. width: w,
  76. height: w,
  77. borderRadius: '50%',
  78. }
  79. })
  80. const imgStyle = computed(() => {
  81. const u = su.value
  82. const w = props.size + u
  83. return {
  84. width: w,
  85. height: w,
  86. borderRadius: '50%',
  87. }
  88. })
  89. const placeholderStyle = computed(() => {
  90. const u = su.value
  91. const w = props.size + u
  92. return {
  93. width: w,
  94. height: w,
  95. borderRadius: '50%',
  96. }
  97. })
  98. const textStyle = computed(() => ({
  99. fontSize: (props.size * 0.4) + su.value,
  100. }))
  101. </script>
  102. <style scoped>
  103. .user-avatar {
  104. overflow: hidden;
  105. flex-shrink: 0;
  106. }
  107. .user-avatar-img {
  108. display: block;
  109. }
  110. .user-avatar-placeholder {
  111. display: flex;
  112. align-items: center;
  113. justify-content: center;
  114. }
  115. .user-avatar-text {
  116. color: #ffffff;
  117. font-weight: 500;
  118. font-family: 'PingFang SC', 'Roboto', -apple-system, sans-serif;
  119. }
  120. </style>
  121. <style>
  122. /* 无 scoped,避免编译后动态 class 与 data-v 不匹配导致渐变不生效;用 .user-avatar 限定避免污染;135deg 左上深→右下浅 */
  123. .user-avatar .user-avatar-placeholder.gradient-0 { background-color: #1e3a8a; background-image: linear-gradient(135deg, #1e3a8a, #93c5fd); }
  124. .user-avatar .user-avatar-placeholder.gradient-1 { background-color: #166534; background-image: linear-gradient(135deg, #166534, #86efac); }
  125. .user-avatar .user-avatar-placeholder.gradient-2 { background-color: #c2410c; background-image: linear-gradient(135deg, #c2410c, #fdba74); }
  126. .user-avatar .user-avatar-placeholder.gradient-3 { background-color: #b91c1c; background-image: linear-gradient(135deg, #b91c1c, #fca5a5); }
  127. .user-avatar .user-avatar-placeholder.gradient-4 { background-color: #5b21b6; background-image: linear-gradient(135deg, #5b21b6, #c4b5fd); }
  128. .user-avatar .user-avatar-placeholder.gradient-5 { background-color: #0f766e; background-image: linear-gradient(135deg, #0f766e, #5eead4); }
  129. .user-avatar .user-avatar-placeholder.gradient-6 { background-color: #3730a3; background-image: linear-gradient(135deg, #3730a3, #a5b4fc); }
  130. .user-avatar .user-avatar-placeholder.gradient-7 { background-color: #0d9488; background-image: linear-gradient(135deg, #0d9488, #2dd4bf); }
  131. .user-avatar .user-avatar-placeholder.gradient-8 { background-color: #b45309; background-image: linear-gradient(135deg, #b45309, #fcd34d); }
  132. .user-avatar .user-avatar-placeholder.gradient-9 { background-color: #be123c; background-image: linear-gradient(135deg, #be123c, #fda4af); }
  133. .user-avatar .user-avatar-placeholder.gradient-10 { background-color: #0369a1; background-image: linear-gradient(135deg, #0369a1, #7dd3fc); }
  134. .user-avatar .user-avatar-placeholder.gradient-11 { background-color: #4d7c0f; background-image: linear-gradient(135deg, #4d7c0f, #bef264); }
  135. .user-avatar .user-avatar-placeholder.gradient-12 { background-color: #86198f; background-image: linear-gradient(135deg, #86198f, #e879f9); }
  136. .user-avatar .user-avatar-placeholder.gradient-13 { background-color: #475569; background-image: linear-gradient(135deg, #475569, #cbd5e1); }
  137. .user-avatar .user-avatar-placeholder.gradient-14 { background-color: #047857; background-image: linear-gradient(135deg, #047857, #6ee7b7); }
  138. .user-avatar .user-avatar-placeholder.gradient-15 { background-color: #6d28d9; background-image: linear-gradient(135deg, #6d28d9, #ddd6fe); }
  139. .user-avatar .user-avatar-placeholder.gradient-16 { background-color: #1e40af; background-image: linear-gradient(135deg, #1e40af, #93c5fd); }
  140. .user-avatar .user-avatar-placeholder.gradient-17 { background-color: #ea580c; background-image: linear-gradient(135deg, #ea580c, #fed7aa); }
  141. .user-avatar .user-avatar-placeholder.gradient-18 { background-color: #0e7490; background-image: linear-gradient(135deg, #0e7490, #99f6e4); }
  142. .user-avatar .user-avatar-placeholder.gradient-19 { background-color: #881337; background-image: linear-gradient(135deg, #881337, #fbcfe8); }
  143. </style>