268 lines
6.7 KiB
Vue
268 lines
6.7 KiB
Vue
<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>
|