topfans/frontend/pages/components/Avatar.vue
2026-04-07 23:08:49 +08:00

268 lines
6.7 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.

<template>
<view class="avatar-wrapper" :style="wrapperStyle">
<view class="avatar-circle" :style="avatarStyle">
<image class="avatar-image" :src="avatarImage" mode="aspectFill"></image>
</view>
<view class="level-badge" v-if="showLevel && level" :style="badgeStyle">
<text class="level-text" :style="badgeTextStyle">LV{{ level }}</text>
</view>
</view>
</template>
<script setup>
import { computed, ref, watch, onMounted } from 'vue';
import { getOssPresignedUrlApi } from '@/utils/api.js';
import { getCachedAvatarPath, downloadAndCacheAvatar } from '@/utils/avatarCache.js';
import { extractFileNameFromUrl } from '@/utils/assetImageHelper.js';
// 定义 props
const props = defineProps({
// 用户ID优先使用
userId: {
type: [String, Number],
default: ''
},
// 用户昵称(作为备用)
nickname: {
type: String,
default: ''
},
// 自定义头像URL来自OSS或已解析的预签名URL
avatarUrl: {
type: String,
default: ''
},
// 头像尺寸单位rpx
size: {
type: Number,
default: 100
},
// 是否显示等级徽章
showLevel: {
type: Boolean,
default: false
},
// 等级
level: {
type: [String, Number],
default: 0
},
// 边框宽度单位rpx
borderWidth: {
type: Number,
default: 4
},
// 是否需要缓存true=用户自己的头像需要缓存false=好友头像不缓存)
enableCache: {
type: Boolean,
default: true
}
});
// 本地头像路径或预签名URL
const ossPresignedUrl = ref('');
// 获取用户自己的头像(需要缓存)
const fetchOwnAvatarWithCache = async () => {
if (!props.avatarUrl) {
ossPresignedUrl.value = '';
return;
}
// 1. 先尝试从本地缓存获取文件路径
const cachedPath = await getCachedAvatarPath(props.avatarUrl);
if (cachedPath) {
ossPresignedUrl.value = cachedPath;
return;
}
// 2. 本地缓存不存在从后端获取预签名URL
try {
// 从avatarUrl中提取真实文件名
const fileName = extractFileNameFromUrl(props.avatarUrl) || 'avatar.png';
const res = await getOssPresignedUrlApi(fileName, 3600, 'avatar');
if (res.code === 200 && res.data && res.data.url) {
const realUrl = res.data.url;
// 3. 下载并缓存头像文件到本地
const localPath = await downloadAndCacheAvatar(props.avatarUrl, realUrl);
if (localPath) {
// 使用本地文件路径
ossPresignedUrl.value = localPath;
} else {
// 下载失败临时使用预签名URL
console.warn('下载头像文件失败使用临时URL');
ossPresignedUrl.value = realUrl;
}
} else {
console.error('获取OSS预签名URL失败:', res.message);
ossPresignedUrl.value = '';
}
} catch (error) {
console.error('获取头像异常:', error);
ossPresignedUrl.value = '';
}
};
// 获取好友头像不缓存直接使用传入的URL
const fetchFriendAvatarWithoutCache = () => {
// 好友头像已经在FriendsContent中解析完成直接使用
ossPresignedUrl.value = props.avatarUrl || '';
};
// 根据enableCache决定使用哪种获取方式
const fetchAvatar = () => {
if (props.enableCache) {
// 用户自己的头像:需要缓存
fetchOwnAvatarWithCache();
} else {
// 好友头像不缓存直接使用传入的URL
fetchFriendAvatarWithoutCache();
}
};
// 监听avatarUrl变化
watch(() => props.avatarUrl, () => {
fetchAvatar();
});
// 监听enableCache变化
watch(() => props.enableCache, () => {
fetchAvatar();
});
// 组件挂载时获取头像
onMounted(() => {
if (props.avatarUrl) {
fetchAvatar();
}
});
// 根据用户ID或昵称生成默认头像路径保证同一用户头像一致
const getDefaultAvatar = () => {
const avatarCount = 7; // static/avatar 中有7张图片
let index = 1; // 默认使用第一张
if (props.userId) {
// 优先使用用户ID生成索引
index = (parseInt(props.userId) || 0) % avatarCount + 1;
} else if (props.nickname) {
// 如果没有ID使用昵称的字符码生成索引
let hash = 0;
for (let i = 0; i < props.nickname.length; i++) {
hash = props.nickname.charCodeAt(i) + ((hash << 5) - hash);
}
index = Math.abs(hash) % avatarCount + 1;
}
return `/static/avatar/${index}.jpeg`;
};
// 头像图片源优先使用OSS头像否则使用默认头像
const avatarImage = computed(() => {
// 如果有OSS预签名URL使用它
if (ossPresignedUrl.value) {
return ossPresignedUrl.value;
}
// 否则使用默认头像
return getDefaultAvatar();
});
// 包装器样式
const wrapperStyle = computed(() => ({
position: 'relative',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexShrink: 0
}));
// 头像圆圈样式
const avatarStyle = computed(() => {
// 根据尺寸调整阴影大小
const shadowSize = props.size >= 160 ? '0 8rpx 32rpx rgba(0, 0, 0, 0.2)' : '0 4rpx 16rpx rgba(0, 0, 0, 0.2)';
return {
width: `${props.size}rpx`,
height: `${props.size}rpx`,
borderWidth: `${props.borderWidth}rpx`,
boxShadow: shadowSize
};
});
// 徽章样式(根据头像大小自适应)
const badgeStyle = computed(() => {
// 根据头像大小调整徽章样式
if (props.size >= 160) {
// 大头像profile页面
return {
top: '-10rpx',
right: '-10rpx',
borderRadius: '20rpx',
padding: '6rpx 16rpx',
border: '3rpx solid rgba(255, 255, 255, 0.8)',
boxShadow: '0 4rpx 12rpx rgba(0, 0, 0, 0.3)'
};
} else {
// 小头像(好友列表等)
const offset = -props.size * 0.18; // 徽章偏移量
return {
top: `${offset}rpx`,
right: `${offset}rpx`,
borderRadius: '10rpx',
padding: '0 8rpx',
border: '2rpx solid rgba(255, 255, 255, 0.8)',
boxShadow: '0 2rpx 8rpx rgba(0, 0, 0, 0.3)'
};
}
});
// 徽章文字样式(根据头像大小自适应)
const badgeTextStyle = computed(() => {
// 根据头像大小调整文字大小
const fontSize = props.size >= 160 ? 22 : 18;
return {
fontSize: `${fontSize}rpx`
};
});
</script>
<style scoped>
.avatar-wrapper {
position: relative;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.avatar-circle {
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border-style: solid;
border-color: rgba(255, 255, 255, 0.3);
overflow: hidden;
}
.avatar-image {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.level-badge {
position: absolute;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
}
.level-text {
font-weight: bold;
color: #e6e6e6;
line-height: 1;
}
</style>