✨ feat(im): 增加 UserAvatar.vue 通用用户头像组件
parent
f929ebc184
commit
969d8237ce
|
|
@ -0,0 +1,141 @@
|
|||
<template>
|
||||
<!--
|
||||
通用用户头像组件
|
||||
- 有 url 时展示图片;无 url 时展示色卡 + 首字母/首字
|
||||
- 点击默认触发 UserInfoCard(clickable)
|
||||
- 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>
|
||||
Loading…
Reference in New Issue