feat(im): 增加 UserAvatar.vue 通用用户头像组件

im
YunaiV 2026-04-26 17:32:47 +08:00
parent f929ebc184
commit 969d8237ce
1 changed files with 141 additions and 0 deletions

View File

@ -0,0 +1,141 @@
<template>
<!--
通用用户头像组件
- url 时展示图片 url 时展示色卡 + 首字母/首字
- 点击默认触发 UserInfoCardclickable
- previewable=true 时改为点头像直接放大预览用于名片 / 详情页等大头像位
-->
<div
class="relative inline-flex"
:style="{ cursor: clickable && !previewable ? 'pointer' : 'default' }"
@click="handleClick"
>
<el-image
v-if="url && previewable"
class="block overflow-hidden"
:src="url"
:preview-src-list="[url]"
:preview-teleported="true"
:style="imgStyle"
fit="cover"
/>
<img
v-else-if="url"
class="block overflow-hidden object-cover"
:src="url"
:style="imgStyle"
loading="lazy"
:alt="name || 'avatar'"
/>
<div
v-else
class="flex items-center justify-center text-white font-medium select-none"
:style="textStyle"
>
{{ avatarText }}
</div>
<!-- 允许外部插入装饰如群聊角标 -->
<slot></slot>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useImUiStore } from '../store/uiStore'
import type { UserInfo } from '../types'
defineOptions({ name: 'ImUserAvatar', inheritAttrs: false })
const props = withDefaults(
defineProps<{
id?: string | number // id
url?: string // URL
name?: string // +
size?: number // px width/height
radius?: string // CSS 15%
clickable?: boolean // UserInfoCard true
previewable?: boolean // clickable
user?: UserInfo //
}>(),
{
size: 42,
radius: '15%',
clickable: true,
previewable: false
}
)
const uiStore = useImUiStore()
const imgStyle = computed(() => ({
width: `${props.size}px`,
height: `${props.size}px`,
borderRadius: props.radius
}))
const textStyle = computed(() => ({
width: `${props.size}px`,
height: `${props.size}px`,
fontSize: `${Math.floor(props.size * (avatarText.value.length > 1 ? 0.34 : 0.42))}px`,
background: textColor.value,
borderRadius: props.radius
}))
/** 色卡首字:中文取 1 个字;英文/拉丁取前 2 个字母(跳过数字、空格、符号) */
const avatarText = computed(() => {
const trimmed = props.name?.trim()
if (!trimmed) {
return ''
}
const first = trimmed.charAt(0)
const code = first.charCodeAt(0)
if (code >= 0x4e00 && code <= 0x9fa5) {
return first
}
const letters = trimmed.match(/[A-Za-z]/g)
if (!letters || letters.length === 0) {
return first.toUpperCase()
}
return letters.slice(0, 2).join('').toUpperCase()
})
const colors = ['#07C160', '#1A95FF', '#FA9D3B', '#9163E0', '#F76760', '#1ABC9C'] //
const textColor = computed(() => {
if (!props.name) {
return '#909399'
}
let hash = 0
for (let i = 0; i < props.name.length; i++) {
hash += props.name.charCodeAt(i)
}
return colors[hash % colors.length]
})
/** 头像点击previewable 走 el-image 预览不弹名片;否则按 user / id 任一入参打开名片 */
function handleClick(e: MouseEvent) {
if (props.previewable) {
return
}
if (!props.clickable) {
return
}
// user
if (props.user) {
uiStore.openUserInfoCard(props.user, { x: e.clientX + 20, y: e.clientY })
return
}
// user id + +
if (props.id == null) {
return
}
uiStore.openUserInfoCard(
{
id: Number(props.id),
nickname: props.name,
avatar: props.url
},
{ x: e.clientX + 20, y: e.clientY }
)
}
</script>