Files
zgty-mas-m/sheep/components/s-avatar/s-avatar.vue
2025-09-30 00:08:23 +08:00

203 lines
3.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
默认头像组件
参考 zt-vue-element CropperAvatar 实现
支持默认头像逻辑头像URL优先否则显示用户昵称首字母
-->
<template>
<view class="avatar-container" @click="handleClick">
<!-- 有头像时显示图片 -->
<image
v-if="avatarUrl"
:src="avatarUrl"
:style="avatarStyle"
class="avatar-image"
mode="aspectFill"
@error="handleImageError"
/>
<!-- 无头像时显示首字母头像 -->
<view
v-else
class="avatar-initial"
:style="[avatarStyle, initialStyle]"
>
{{ initialText }}
</view>
<!-- 编辑按钮 -->
<view v-if="editable && !readonly" class="edit-mask">
<text class="edit-icon">+</text>
</view>
</view>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
// 组件属性
const props = defineProps({
// 头像URL
src: {
type: String,
default: ''
},
// 用户昵称,用于生成首字母
nickname: {
type: String,
default: ''
},
// 头像尺寸
size: {
type: [String, Number],
default: 80
},
// 是否可编辑
editable: {
type: Boolean,
default: false
},
// 是否只读
readonly: {
type: Boolean,
default: false
},
// 背景色(用于首字母头像)
bgColor: {
type: String,
default: '#0055A2'
},
// 文字色(用于首字母头像)
textColor: {
type: String,
default: '#ffffff'
},
// 自定义初始字符
initial: {
type: String,
default: ''
}
})
// 事件定义
const emit = defineEmits(['click', 'error'])
// 响应式数据
const avatarUrl = ref(props.src)
const imageError = ref(false)
// 计算属性
const avatarStyle = computed(() => {
const size = typeof props.size === 'number' ? `${props.size}rpx` : props.size
return {
width: size,
height: size,
borderRadius: '50%'
}
})
const initialStyle = computed(() => ({
backgroundColor: props.bgColor,
color: props.textColor,
fontSize: `${Math.floor(props.size * 0.4)}rpx`,
fontWeight: 'bold'
}))
// 生成初始字符逻辑
const initialText = computed(() => {
// 如果提供了自定义初始字符,使用它
if (props.initial) {
return props.initial.charAt(0).toUpperCase()
}
// 如果有昵称,取首字母
if (props.nickname) {
// 处理中文和英文
const firstChar = props.nickname.charAt(0)
// 如果是中文,直接使用
if (/[\u4e00-\u9fa5]/.test(firstChar)) {
return firstChar
}
// 如果是英文,转大写
return firstChar.toUpperCase()
}
// 默认返回用户图标
return '用'
})
// 监听src变化
watch(() => props.src, (newSrc) => {
avatarUrl.value = newSrc
imageError.value = false
}, { immediate: true })
// 方法
const handleClick = () => {
if (!props.readonly) {
emit('click')
}
}
const handleImageError = () => {
imageError.value = true
avatarUrl.value = '' // 清空URL显示首字母头像
emit('error')
}
// 暴露方法给父组件
defineExpose({
refresh: () => {
avatarUrl.value = props.src
imageError.value = false
}
})
</script>
<style lang="scss" scoped>
.avatar-container {
position: relative;
display: inline-block;
overflow: hidden;
&:hover .edit-mask {
opacity: 1;
}
}
.avatar-image {
display: block;
object-fit: cover;
}
.avatar-initial {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--theme-primary, #0055A2);
color: #ffffff;
font-weight: bold;
text-align: center;
user-select: none;
}
.edit-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.edit-icon {
color: #ffffff;
font-size: 48rpx;
font-weight: bold;
}
</style>