Files
nl-um-vue-ts/src/components/common/Avatar.vue

126 lines
3.0 KiB
Vue
Raw Normal View History

2025-12-03 12:56:57 +08:00
<template>
<div
2025-12-05 17:00:50 +08:00
class="flex items-center justify-center text-white font-bold shadow-md transition overflow-hidden"
2025-12-03 13:25:02 +08:00
:class="[sizeClass, roundedClass, { 'cursor-pointer hover:opacity-80': $attrs.onClick }]"
2025-12-08 14:56:02 +08:00
:style="customSizeStyle"
2025-12-03 12:56:57 +08:00
@click="$emit('click')"
>
2025-12-05 17:00:50 +08:00
<img
v-if="isImage"
2026-07-08 08:17:58 +08:00
:src="displayAvatar"
2025-12-05 17:00:50 +08:00
:alt="name || 'Avatar'"
class="w-full h-full object-cover"
@error="handleImageError"
/>
<span v-else>{{ avatar || name?.charAt(0).toUpperCase() || '?' }}</span>
2025-12-03 12:56:57 +08:00
</div>
</template>
<script setup lang="ts">
2025-12-05 17:00:50 +08:00
import { computed, ref } from 'vue'
2025-12-03 12:56:57 +08:00
import { generateColor } from '@/utils/format'
2026-07-08 08:17:58 +08:00
import { resolveImageUrl } from '@/utils/image'
2025-12-03 12:56:57 +08:00
interface Props {
avatar?: string
name?: string
color?: string
2025-12-08 14:56:02 +08:00
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'contact' | number
2025-12-03 13:25:02 +08:00
rounded?: 'full' | 'xl' | 'lg'
2025-12-03 12:56:57 +08:00
}
const props = withDefaults(defineProps<Props>(), {
size: 'md',
2025-12-03 13:25:02 +08:00
rounded: 'full',
2025-12-03 12:56:57 +08:00
})
2025-12-05 17:00:50 +08:00
const imageError = ref(false)
2026-07-08 08:17:58 +08:00
/** 解析后的头像地址 */
const displayAvatar = computed(() => resolveImageUrl(props.avatar))
2025-12-05 17:00:50 +08:00
// 判断 avatar 是否是图片URL
const isImage = computed(() => {
2026-07-08 08:17:58 +08:00
const avatar = props.avatar
if (!avatar || imageError.value) return false
2025-12-05 17:00:50 +08:00
// 检查是否是 base64 图片
2026-07-08 08:17:58 +08:00
if (avatar.startsWith('data:image/')) {
2025-12-05 17:00:50 +08:00
return true
}
2026-07-08 08:17:58 +08:00
if (avatar.startsWith('http://') || avatar.startsWith('https://')) {
2025-12-05 17:00:50 +08:00
return true
}
2026-07-08 08:17:58 +08:00
if (avatar.startsWith('/')) {
2025-12-05 17:00:50 +08:00
return true
}
2026-07-08 08:17:58 +08:00
2025-12-05 17:00:50 +08:00
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp']
2026-07-08 08:17:58 +08:00
const lowerAvatar = avatar.toLowerCase()
2025-12-05 17:00:50 +08:00
if (imageExtensions.some(ext => lowerAvatar.endsWith(ext))) {
return true
}
return false
})
2025-12-08 14:56:02 +08:00
// 预定义尺寸映射
const sizeMap: Record<string, string> = {
xs: 'w-7 h-7 text-[10px]',
sm: 'w-9 h-9 text-xs',
md: 'w-10 h-10 text-sm',
lg: 'w-12 h-12 text-base',
xl: 'w-16 h-16 text-xl',
'2xl': 'w-24 h-24 text-2xl',
contact: 'w-11 h-11 text-lg',
}
// 尺寸 class仅用于预定义尺寸
2025-12-03 12:56:57 +08:00
const sizeClass = computed(() => {
2025-12-08 14:56:02 +08:00
if (typeof props.size === 'number') {
return '' // 数字尺寸使用 style
2025-12-03 12:56:57 +08:00
}
2025-12-08 14:56:02 +08:00
return sizeMap[props.size] || sizeMap.md
})
// 自定义尺寸样式(用于数字尺寸和背景色)
const customSizeStyle = computed(() => {
const style: Record<string, string> = {}
// 背景色
if (!isImage.value) {
style.background = props.color || generateColor(props.name || props.avatar || '')
}
// 数字尺寸
if (typeof props.size === 'number') {
style.width = `${props.size}px`
style.height = `${props.size}px`
// 根据尺寸计算字体大小
style.fontSize = `${Math.max(10, props.size * 0.4)}px`
}
return style
2025-12-03 12:56:57 +08:00
})
2025-12-03 13:25:02 +08:00
const roundedClass = computed(() => {
const rounded = {
full: 'rounded-full',
xl: 'rounded-xl',
lg: 'rounded-lg',
}
return rounded[props.rounded]
})
2025-12-05 17:00:50 +08:00
// 图片加载失败时的处理
function handleImageError() {
imageError.value = true
}
2025-12-03 12:56:57 +08:00
defineEmits<{
click: []
}>()
</script>