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>
|