feat: 移除oss预url的鉴权
This commit is contained in:
parent
1b82e4c3bc
commit
75893b1f42
@ -263,15 +263,15 @@ const handleLike = async () => {
|
||||
if (liking.value || !assetData.value.asset_id) return;
|
||||
liking.value = true;
|
||||
try {
|
||||
if (isLiked.value) {
|
||||
await unlikeAssetApi(assetData.value.asset_id);
|
||||
isLiked.value = false;
|
||||
likeCount.value = Math.max(0, likeCount.value - 1);
|
||||
} else {
|
||||
// if (isLiked.value) {
|
||||
// await unlikeAssetApi(assetData.value.asset_id);
|
||||
// isLiked.value = false;
|
||||
// likeCount.value = Math.max(0, likeCount.value - 1);
|
||||
// } else {
|
||||
await likeAssetApi(assetData.value.asset_id);
|
||||
isLiked.value = true;
|
||||
likeCount.value += 1;
|
||||
}
|
||||
// }
|
||||
// 通知展馆页面更新点赞数
|
||||
uni.$emit('assetLikeChanged', {
|
||||
asset_id: assetData.value.asset_id,
|
||||
|
||||
@ -11,9 +11,6 @@
|
||||
|
||||
<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({
|
||||
@ -59,67 +56,30 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 本地头像路径或预签名URL
|
||||
const ossPresignedUrl = ref('');
|
||||
|
||||
// 获取用户自己的头像(需要缓存)
|
||||
const fetchOwnAvatarWithCache = async () => {
|
||||
// 本地头像路径
|
||||
const localAvatarUrl = ref('');
|
||||
|
||||
// 获取用户自己的头像(直接使用后端返回的URL)
|
||||
const fetchOwnAvatar = () => {
|
||||
if (!props.avatarUrl) {
|
||||
ossPresignedUrl.value = '';
|
||||
localAvatarUrl.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
|
||||
localAvatarUrl.value = props.avatarUrl;
|
||||
};
|
||||
|
||||
// 获取好友头像(不缓存,直接使用传入的URL)
|
||||
const fetchFriendAvatarWithoutCache = () => {
|
||||
// 好友头像已经在FriendsContent中解析完成,直接使用
|
||||
ossPresignedUrl.value = props.avatarUrl || '';
|
||||
|
||||
// 获取好友头像(直接使用传入的URL)
|
||||
const fetchFriendAvatar = () => {
|
||||
localAvatarUrl.value = props.avatarUrl || '';
|
||||
};
|
||||
|
||||
|
||||
// 根据enableCache决定使用哪种获取方式
|
||||
const fetchAvatar = () => {
|
||||
if (props.enableCache) {
|
||||
// 用户自己的头像:需要缓存
|
||||
fetchOwnAvatarWithCache();
|
||||
fetchOwnAvatar();
|
||||
} else {
|
||||
// 好友头像:不缓存,直接使用传入的URL
|
||||
fetchFriendAvatarWithoutCache();
|
||||
fetchFriendAvatar();
|
||||
}
|
||||
};
|
||||
|
||||
@ -160,13 +120,11 @@
|
||||
return `/static/avatar/${index}.jpeg`;
|
||||
};
|
||||
|
||||
// 头像图片源(优先使用OSS头像,否则使用默认头像)
|
||||
// 头像图片源(优先使用头像URL,否则使用默认头像)
|
||||
const avatarImage = computed(() => {
|
||||
// 如果有OSS预签名URL,使用它
|
||||
if (ossPresignedUrl.value) {
|
||||
return ossPresignedUrl.value;
|
||||
if (localAvatarUrl.value) {
|
||||
return localAvatarUrl.value;
|
||||
}
|
||||
// 否则使用默认头像
|
||||
return getDefaultAvatar();
|
||||
});
|
||||
|
||||
|
||||
@ -7,18 +7,13 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { getHotRankingApi, getOssPresignedUrlApi } from '@/utils/api.js';
|
||||
import { getHotRankingApi } from '@/utils/api.js';
|
||||
|
||||
const emit = defineEmits(['dataLoaded']);
|
||||
|
||||
const resolveOssUrl = async (fileName, type) => {
|
||||
if (!fileName) return '';
|
||||
try {
|
||||
const res = await getOssPresignedUrlApi(fileName, 3600, type);
|
||||
if (res?.code === 200 && res.data?.url) return res.data.url;
|
||||
} catch (e) {
|
||||
console.warn('[BannerTop3] OSS URL 获取失败', fileName, e?.message);
|
||||
}
|
||||
// 直接使用后端返回的图片URL
|
||||
return fileName;
|
||||
};
|
||||
|
||||
@ -27,13 +22,9 @@ const loadTop3 = async () => {
|
||||
const res = await getHotRankingApi('total', null, 1, 3);
|
||||
if (res.code === 200 && res.data?.items) {
|
||||
const items = res.data.items.slice(0, 3);
|
||||
const resolved = await Promise.all(items.map(async (item) => {
|
||||
const [coverUrl, avatarUrl] = await Promise.all([
|
||||
resolveOssUrl(item.cover_url || '', 'asset'),
|
||||
resolveOssUrl(item.avatar_url || '', 'avatar'),
|
||||
]);
|
||||
return { ...item, cover_url: coverUrl, avatar_url: avatarUrl };
|
||||
}));
|
||||
const resolved = items.map(item => {
|
||||
return { ...item, cover_url: item.cover_url || '', avatar_url: item.avatar_url || '' };
|
||||
});
|
||||
emit('dataLoaded', resolved);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@ -137,8 +137,7 @@ import {
|
||||
getHotRankingApi,
|
||||
getOriginalRankingApi,
|
||||
getActivityRankingApi,
|
||||
getActivityListApi,
|
||||
getOssPresignedUrlApi
|
||||
getActivityListApi
|
||||
} from '@/utils/api.js';
|
||||
import {
|
||||
ACTIVITY_TYPES
|
||||
@ -367,19 +366,11 @@ const fetchActivityList = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 获取OSS图片的预签名URL
|
||||
// 获取OSS图片URL - 直接使用后端返回的URL
|
||||
const getOssImageUrl = async (fileName, type = 'avatar') => {
|
||||
if (!fileName || fileName === '') return;
|
||||
try {
|
||||
const response = await getOssPresignedUrlApi(fileName, 3600, type);
|
||||
if (response && response.code === 200 && response.data && response.data.url) {
|
||||
return response.data.url;
|
||||
}
|
||||
return fileName; // 如果获取失败,返回原始文件名
|
||||
} catch (error) {
|
||||
console.error('Failed to get OSS presigned URL:', error);
|
||||
return fileName; // 如果出错,返回原始文件名
|
||||
}
|
||||
// 直接使用后端返回的图片URL
|
||||
return fileName;
|
||||
};
|
||||
|
||||
// 转换活动排名数据
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<image class="bg-image" src="/static/square/beijingban.png" mode="aspectFill"></image>
|
||||
|
||||
<!-- 顶部导航 -->
|
||||
<view class="nav-bar ">
|
||||
<view class="nav-bar">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<image class="nav-back-icon" src="/static/icon/back.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
@ -51,7 +51,9 @@
|
||||
|
||||
<!-- 空状态占位:显示剩余空展位卡片 -->
|
||||
<view v-if="exhibitionWorks.length < 2" class="empty-exhibition">
|
||||
<view class="empty-card empty-card-left" @tap="openAssetSelector(0)">
|
||||
<!-- 根据已展出数量决定显示几个空卡片 -->
|
||||
<view v-if="exhibitionWorks.length === 0" class="empty-card empty-card-left"
|
||||
@tap="openAssetSelector(0)">
|
||||
<image class="empty-cover" src="/static/nft/placeholder.png" mode="aspectFill"></image>
|
||||
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png"
|
||||
mode="aspectFill"></image>
|
||||
@ -83,7 +85,7 @@
|
||||
<view v-for="(item, index) in likedWorks" :key="item.id" class="liked-row"
|
||||
@tap="goToAssetDetail(item.id)">
|
||||
<!-- 排名图标,绝对定位在卡片左侧 -->
|
||||
<image v-if="index < 3" :src="rankIcons[index]" class="rank-icon-img" mode="aspectFit"></image>
|
||||
<!-- <image v-if="index < 3" :src="rankIcons[index]" class="rank-icon-img" mode="aspectFit"></image> -->
|
||||
|
||||
<!-- 卡片主体 -->
|
||||
<view class="liked-item" :class="index === 0 ? 'liked-item-first' : ''">
|
||||
@ -424,7 +426,7 @@ onShow(() => {
|
||||
|
||||
}
|
||||
|
||||
.section-block {
|
||||
.section-block{
|
||||
background: rgb(249 159 192 / 45%);
|
||||
border-radius: 48rpx;
|
||||
padding: 16rpx;
|
||||
|
||||
@ -263,9 +263,9 @@ import { useStore } from 'vuex';
|
||||
import { onReady } from "@dcloudio/uni-app";
|
||||
import Header from '../components/Header.vue';
|
||||
import Avatar from '../components/Avatar.vue';
|
||||
import { getUserProfileApi, deleteAccountApi, updateNicknameApi, updatePasswordApi, getFanIdentitiesApi, addFanIdentityApi, getMyFanIdentitiesApi, switchFanIdentityApi, getOssSignatureApi, updateAvatarApi, getOssPresignedUrlApi } from '@/utils/api';
|
||||
import { getUserProfileApi, deleteAccountApi, updateNicknameApi, updatePasswordApi, getFanIdentitiesApi, addFanIdentityApi, getMyFanIdentitiesApi, switchFanIdentityApi, getOssSignatureApi, updateAvatarApi } from '@/utils/api';
|
||||
import { validateNickname } from '@/utils/validator.js';
|
||||
import { clearAvatarCache, downloadAndCacheAvatar } from '@/utils/avatarCache';
|
||||
import { clearAvatarCache } from '@/utils/avatarCache';
|
||||
import GuideListModal from '@/components/GuideListModal.vue';
|
||||
import GuideOverlay from '@/components/GuideOverlay.vue';
|
||||
import {
|
||||
@ -1032,22 +1032,9 @@ const closeAvatarModal = () => {
|
||||
showAvatarModal.value = false;
|
||||
};
|
||||
|
||||
// 处理头像更新后的操作(下载缓存、更新本地存储、刷新UI)
|
||||
// 处理头像更新后的操作(更新本地存储、刷新UI)
|
||||
const handleAvatarUpdateSuccess = async (newAvatarUrl) => {
|
||||
try {
|
||||
// 1. 获取新头像的真实URL并下载缓存
|
||||
const fileName = 'avatar.png';
|
||||
const urlRes = await getOssPresignedUrlApi(fileName, 3600, 'avatar');
|
||||
if (urlRes.code === 200 && urlRes.data && urlRes.data.url) {
|
||||
// 立即下载并缓存新头像
|
||||
await downloadAndCacheAvatar(newAvatarUrl, urlRes.data.url);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('预下载头像失败:', error);
|
||||
// 不影响主流程,继续
|
||||
}
|
||||
|
||||
// 2. 更新本地缓存
|
||||
// 1. 更新本地缓存
|
||||
userAvatarUrl.value = newAvatarUrl;
|
||||
const userStr = uni.getStorageSync('user');
|
||||
if (userStr) {
|
||||
|
||||
@ -33,16 +33,17 @@
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelValue: { type: String, default: 'hot' }
|
||||
modelValue: { type: String, default: 'new' }
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
|
||||
const tabs = [
|
||||
{ key: 'hot', label: '人气王者', emoji: null, icon: '/static/square/rementubiao.png', iconWidth: 104, iconHeight: 104 },
|
||||
{ key: 'xingka', label: '潜力之星', emoji: null, icon: '/static/square/xingka.png', iconWidth: 80, iconHeight: 80 },
|
||||
{ key: 'baji', label: '新鲜上架', emoji: null, icon: '/static/square/baji.png', iconWidth: 80, iconHeight: 80 },
|
||||
{ key: 'haibao', label: '随机寻宝', emoji: null, icon: '/static/square/haibao.png', iconWidth: 96, iconHeight: 96 },]
|
||||
{ key: 'hot', label: '人气王者', emoji: null, icon: '/static/square/rementubiao.png', iconWidth: 104, iconHeight: 104 },
|
||||
{ key: 'new', label: '新鲜上架', emoji: null, icon: '/static/square/baji.png', iconWidth: 80, iconHeight: 80 },
|
||||
{ key: 'potential', label: '潜力之星', emoji: null, icon: '/static/square/xingka.png', iconWidth: 80, iconHeight: 80 },
|
||||
{ key: 'random', label: '随机寻宝', emoji: null, icon: '/static/square/haibao.png', iconWidth: 96, iconHeight: 96 },
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -66,7 +66,8 @@ const props = defineProps({
|
||||
screenHeight: { type: Number, default: 812 },
|
||||
bannerBottom: { type: Number, default: 200 },
|
||||
useMockData: { type: Boolean, default: false }, // 是否使用模拟数据
|
||||
category: { type: String, default: 'hot' }, // 当前分类
|
||||
category: { type: String, default: '' }, // 当前分类
|
||||
isActive: { type: Boolean, default: true }, // 是否处于激活状态
|
||||
})
|
||||
|
||||
const emit = defineEmits(['cardClick'])
|
||||
@ -91,6 +92,8 @@ let currentScrollLeft = 0
|
||||
let idCounter = 0
|
||||
let isComponentMounted = false // 标记组件是否已卸载
|
||||
let mockDataOffset = 0 // 模拟数据循环偏移量
|
||||
let appendFailed = false // 标记追加是否已失败
|
||||
let isInitialLoading = true // 标记是否在初始加载中
|
||||
|
||||
// ========== RAF 兼容 ==========
|
||||
const rafFn = (cb) => {
|
||||
@ -112,17 +115,26 @@ let appendTimer = null // 防抖定时器
|
||||
|
||||
const startAutoScroll = () => {
|
||||
if (!isComponentMounted) {
|
||||
|
||||
return
|
||||
}
|
||||
if (rafId) cancelAnimationFrame(rafId)
|
||||
// 确保没有重复的 RAF 在运行
|
||||
if (rafId) {
|
||||
cafFn(rafId)
|
||||
rafId = null
|
||||
}
|
||||
// 重置滚动状态
|
||||
userInteracting = false
|
||||
|
||||
let lastScrollLeft = 0 // 用于检测手动滚动
|
||||
|
||||
const step = () => {
|
||||
if (!isComponentMounted) {
|
||||
rafId = null
|
||||
|
||||
return
|
||||
}
|
||||
// 每次 RAF 循环开始时也检查一次
|
||||
if (!isComponentMounted) {
|
||||
rafId = null
|
||||
return
|
||||
}
|
||||
|
||||
@ -143,7 +155,8 @@ const startAutoScroll = () => {
|
||||
const remainingScroll = totalWidth.value - currentScrollLeft - props.screenWidth
|
||||
if (remainingScroll < Math.max(totalWidth.value / 2, props.screenWidth)) {
|
||||
// 直接调用 appendMore(不需要防抖)
|
||||
if (!isLoadingMore) {
|
||||
// 注意:真实接口模式下不应该自动追加数据,由 has_more 控制
|
||||
if (!isLoadingMore && !appendFailed && !isInitialLoading && props.useMockData) {
|
||||
appendMore()
|
||||
}
|
||||
}
|
||||
@ -178,14 +191,14 @@ const pauseForUser = () => {
|
||||
|
||||
// 防抖:延迟 500ms 后再执行追加,避免频繁调用
|
||||
const scheduleAppend = () => {
|
||||
if (!isComponentMounted) return
|
||||
if (!isComponentMounted || appendFailed) return
|
||||
if (appendTimer) clearTimeout(appendTimer)
|
||||
appendTimer = setTimeout(() => {
|
||||
appendTimer = null
|
||||
if (isComponentMounted) {
|
||||
if (isComponentMounted && !appendFailed) {
|
||||
appendMore()
|
||||
}
|
||||
}, 500)
|
||||
}, 800)
|
||||
}
|
||||
|
||||
// ========== 触摸 / 滚动事件 ==========
|
||||
@ -198,7 +211,10 @@ const onScroll = (e) => {
|
||||
// 预加载:剩余可滚动距离小于一半屏幕宽度时,触发追加
|
||||
const remainingScroll = totalWidth.value - currentScrollLeft - props.screenWidth
|
||||
if (remainingScroll < Math.max(totalWidth.value / 2, props.screenWidth)) {
|
||||
scheduleAppend()
|
||||
// 注意:真实接口模式下不应该自动追加数据,由 has_more 控制
|
||||
if (!isLoadingMore && !appendFailed && !isInitialLoading && props.useMockData) {
|
||||
scheduleAppend()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,7 +236,7 @@ const scrollStyle = computed(() => ({
|
||||
// 1. 后端传 span:直接按 span 分组成列
|
||||
// 2. 后端不传 span:使用分类特定的 span 阈值计算
|
||||
class WaterfallLayout {
|
||||
constructor(containerH, category = 'hot', gap = GAP) {
|
||||
constructor(containerH, category = 'random', gap = GAP) {
|
||||
this.containerH = containerH
|
||||
this.category = category
|
||||
this.gap = gap
|
||||
@ -408,15 +424,9 @@ const randomLikes = () => {
|
||||
// 本地素材图,循环使用
|
||||
const MOCK_IMAGES = Array.from({ length: 16 }, (_, i) => `/static/sucai/image-${String(i + 1).padStart(2, '0')}.png`)
|
||||
|
||||
const mapUser = async (u) => {
|
||||
// 优先用后端图,没有则用本地素材循环
|
||||
let coverUrl = MOCK_IMAGES[idCounter % MOCK_IMAGES.length]
|
||||
if (u.cover_url) {
|
||||
try {
|
||||
const r = await getOssPresignedUrlApi(u.cover_url, 3600, 'asset')
|
||||
if (r?.code === 200 && r.data?.url) coverUrl = r.data.url
|
||||
} catch (_) {}
|
||||
}
|
||||
const mapUser = (u) => {
|
||||
// 直接使用后端返回的图片URL
|
||||
let coverUrl = u.cover_url || MOCK_IMAGES[idCounter % MOCK_IMAGES.length]
|
||||
// 后端可以返回 span 字段(1~4)来控制卡片大小,不传则前端随机模板决定
|
||||
return {
|
||||
id: idCounter++,
|
||||
@ -451,6 +461,7 @@ const loadUsers = async () => {
|
||||
|
||||
// 切换分类时重置偏移量
|
||||
mockDataOffset = 0
|
||||
isLoadingMore = true
|
||||
|
||||
try {
|
||||
let items
|
||||
@ -465,7 +476,7 @@ const loadUsers = async () => {
|
||||
|
||||
} else {
|
||||
// 使用真实API
|
||||
const res = await getInspirationFlowApi({ limit: 40, type: props.category })
|
||||
const res = await getInspirationFlowApi({ limit: 20, type: props.category })
|
||||
|
||||
if (!isComponentMounted) return
|
||||
if (res.code === 200 && res.data?.items) {
|
||||
@ -498,18 +509,9 @@ const loadUsers = async () => {
|
||||
|
||||
const appendMore = async () => {
|
||||
if (!isComponentMounted) {
|
||||
|
||||
return
|
||||
}
|
||||
if (isLoadingMore) {
|
||||
|
||||
// 等待一下再试
|
||||
setTimeout(() => {
|
||||
if (isComponentMounted && !isLoadingMore) {
|
||||
|
||||
appendMore()
|
||||
}
|
||||
}, 500)
|
||||
return
|
||||
}
|
||||
isLoadingMore = true
|
||||
@ -522,8 +524,6 @@ const appendMore = async () => {
|
||||
const allItems = mockData.items
|
||||
const batchSize = 20
|
||||
|
||||
|
||||
|
||||
// 从 mockDataOffset 位置开始取 batchSize 个
|
||||
const itemsToAdd = []
|
||||
for (let i = 0; i < batchSize; i++) {
|
||||
@ -542,18 +542,22 @@ const appendMore = async () => {
|
||||
mockDataOffset = mockDataOffset + batchSize
|
||||
|
||||
items = itemsToAdd
|
||||
|
||||
|
||||
} else {
|
||||
// 使用真实API
|
||||
const res = await getInspirationFlowApi({ limit: 20, type: props.category })
|
||||
if (!isComponentMounted) return
|
||||
if (res.code === 200 && res.data?.items) {
|
||||
items = res.data.items
|
||||
// 真实接口根据 has_more 决定是否继续加载
|
||||
if (!res.data.has_more) {
|
||||
appendFailed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (items && items.length > 0) {
|
||||
|
||||
|
||||
const withData = items.map((item) => {
|
||||
return {
|
||||
id: item.asset_id, // 直接使用 asset_id(已经在上面保证唯一)
|
||||
@ -564,17 +568,20 @@ const appendMore = async () => {
|
||||
span: item.span ?? null,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const placed = layout.addCards(withData)
|
||||
|
||||
|
||||
cards.value = [...cards.value, ...placed]
|
||||
totalWidth.value = layout.getTotalWidth()
|
||||
|
||||
|
||||
} else {
|
||||
// 没有可追加的数据,标记失败停止
|
||||
appendFailed = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[WaterfallGrid] 追加用户失败', e?.message ?? e)
|
||||
appendFailed = true
|
||||
} finally {
|
||||
|
||||
isLoadingMore = false
|
||||
}
|
||||
}
|
||||
@ -615,20 +622,57 @@ const handleCardClick = (card) => {
|
||||
// ========== 初始化 ==========
|
||||
onMounted(() => {
|
||||
isComponentMounted = true
|
||||
const containerH = props.screenHeight - props.bannerBottom
|
||||
layout = new WaterfallLayout(containerH, props.category)
|
||||
isInitialLoading = true
|
||||
appendFailed = false
|
||||
userInteracting = false
|
||||
currentScrollLeft = 0
|
||||
scrollLeft.value = 0
|
||||
const containerH = props.screenHeight - props.bannerBottom
|
||||
layout = new WaterfallLayout(containerH, props.category)
|
||||
loadUsers().then(() => {
|
||||
isInitialLoading = false
|
||||
nextTick(() => startAutoScroll())
|
||||
})
|
||||
})
|
||||
|
||||
// 监听页面可见性变化
|
||||
const handleVisibilityChange = () => {
|
||||
if (!isComponentMounted) return
|
||||
if (document.hidden) {
|
||||
userInteracting = true
|
||||
} else {
|
||||
userInteracting = false
|
||||
if (!rafId) {
|
||||
startAutoScroll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
}
|
||||
|
||||
// 监听 isActive 属性变化(父组件控制)
|
||||
watch(() => props.isActive, (active) => {
|
||||
if (!isComponentMounted) return
|
||||
if (active) {
|
||||
userInteracting = false
|
||||
if (!rafId) {
|
||||
startAutoScroll()
|
||||
}
|
||||
} else {
|
||||
userInteracting = true
|
||||
}
|
||||
}, { immediate: false })
|
||||
|
||||
onUnmounted(() => {
|
||||
isComponentMounted = false
|
||||
stopAutoScroll()
|
||||
clearTimeout(resumeTimer)
|
||||
clearTimeout(appendTimer)
|
||||
if (typeof document !== 'undefined') {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => [props.screenHeight, props.bannerBottom], () => {
|
||||
@ -662,6 +706,8 @@ watch(() => props.category, (newCategory) => {
|
||||
allUsers.value = []
|
||||
totalWidth.value = 0
|
||||
mockDataOffset = 0
|
||||
appendFailed = false
|
||||
isInitialLoading = false
|
||||
|
||||
// 先停止 RAF,确保在 loadUsers 完成前不会有滚动逻辑
|
||||
if (rafId) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ref } from 'vue'
|
||||
import { getActivityListApi, getOssPresignedUrlApi } from '@/utils/api.js'
|
||||
import { getActivityListApi } from '@/utils/api.js'
|
||||
|
||||
export function useBanner() {
|
||||
const bannerActivities = ref([])
|
||||
@ -8,24 +8,11 @@ export function useBanner() {
|
||||
try {
|
||||
const starId = uni.getStorageSync('star_id') || null
|
||||
const res = await getActivityListApi(starId, 1, 10)
|
||||
|
||||
|
||||
if (res.code === 200 && res.data?.activities) {
|
||||
const activities = res.data.activities
|
||||
// 并行获取所有 OSS URL
|
||||
const urlPromises = activities.map(async (item) => {
|
||||
if (!item.cover_image) return { item, url: null }
|
||||
try {
|
||||
const r = await getOssPresignedUrlApi(item.cover_image, 3600, 'avatar')
|
||||
return { item, url: r?.code === 200 && r.data?.url ? r.data.url : null }
|
||||
} catch (e) {
|
||||
console.error('[useBanner] 获取 OSS URL 失败', e)
|
||||
return { item, url: null }
|
||||
}
|
||||
})
|
||||
const results = await Promise.all(urlPromises)
|
||||
bannerActivities.value = results.map(({ item, url }) =>
|
||||
url ? { ...item, cover_image: url } : item
|
||||
)
|
||||
// 直接使用后端返回的图片URL
|
||||
bannerActivities.value = activities
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[useBanner] 加载 banner 活动失败', e?.message ?? e)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// ========== 模拟数据配置 ==========
|
||||
|
||||
// 是否使用模拟数据(开发调试时设为 true,上线后改为 false)
|
||||
export const USE_MOCK_DATA = true
|
||||
export const USE_MOCK_DATA = false
|
||||
|
||||
// 模拟图片列表
|
||||
const MOCK_IMAGES = [
|
||||
@ -43,8 +43,16 @@ export const SPAN_CONFIG = {
|
||||
{ max: Infinity, span: 4 }, // 10w+ → span 4
|
||||
]
|
||||
},
|
||||
// 新鲜上架:低点赞,小卡片为主
|
||||
new: {
|
||||
thresholds: [
|
||||
{ max: 200, span: 1 }, // 200 以下 → span 1
|
||||
{ max: 500, span: 2 }, // 200-500 → span 2
|
||||
{ max: Infinity, span: 3 },// 500+ → span 3
|
||||
]
|
||||
},
|
||||
// 潜力之星:中等点赞,中小卡片
|
||||
xingka: {
|
||||
potential: {
|
||||
thresholds: [
|
||||
{ max: 5000, span: 1 }, // 5k 以下 → span 1
|
||||
{ max: 10000, span: 2 }, // 5k-1w → span 2
|
||||
@ -52,16 +60,8 @@ export const SPAN_CONFIG = {
|
||||
{ max: Infinity, span: 4 },// 1.5w+ → span 4
|
||||
]
|
||||
},
|
||||
// 新鲜上架:低点赞,小卡片为主
|
||||
baji: {
|
||||
thresholds: [
|
||||
{ max: 200, span: 1 }, // 200 以下 → span 1
|
||||
{ max: 500, span: 2 }, // 200-500 → span 2
|
||||
{ max: Infinity, span: 3 },// 500+ → span 3
|
||||
]
|
||||
},
|
||||
// 随机寻宝:混合
|
||||
haibao: {
|
||||
random: {
|
||||
thresholds: [
|
||||
{ max: 10000, span: 1 }, // 1w 以下 → span 1
|
||||
{ max: 30000, span: 2 }, // 1w-3w → span 2
|
||||
@ -95,42 +95,12 @@ export const MOCK_RENQIWANG = {
|
||||
{ asset_id: 10009, name: '粉红泡泡', cover_url: MOCK_IMAGES[8], like_count: 45600, owner_nickname: '小迷糊' },
|
||||
{ asset_id: 10010, name: '爱的力量', cover_url: MOCK_IMAGES[9], like_count: 42300, owner_nickname: '小幸运' },
|
||||
{ asset_id: 10011, name: '璀璨星河', cover_url: MOCK_IMAGES[10], like_count: 39800, owner_nickname: '小浪漫' },
|
||||
// { asset_id: 10012, name: '甜蜜日常', cover_url: MOCK_IMAGES[11], like_count: 37500, owner_nickname: '小清新' },
|
||||
// { asset_id: 10013, name: '爱心发射', cover_url: MOCK_IMAGES[12], like_count: 35200, owner_nickname: '小活力' },
|
||||
// { asset_id: 10014, name: '超级喜欢', cover_url: MOCK_IMAGES[13], like_count: 32900, owner_nickname: '小呆萌' },
|
||||
// { asset_id: 10015, name: '温暖拥抱', cover_url: MOCK_IMAGES[14], like_count: 30600, owner_nickname: '小棉花' },
|
||||
// { asset_id: 10016, name: '今日份心动', cover_url: MOCK_IMAGES[15], like_count: 28500, owner_nickname: '小牛奶' },
|
||||
],
|
||||
cursor: 'renqiwang_cursor_001',
|
||||
has_more: true,
|
||||
session_id: 'renqiwang_session',
|
||||
}
|
||||
|
||||
// ========== 潜力之星 - 中等点赞有潜力的作品 ==========
|
||||
export const MOCK_QIANLIXING = {
|
||||
items: [
|
||||
{ asset_id: 20001, name: '初露锋芒', cover_url: MOCK_IMAGES[0], like_count: 12800, owner_nickname: '小新芽' },
|
||||
{ asset_id: 20002, name: '蓄势待发', cover_url: MOCK_IMAGES[1], like_count: 11500, owner_nickname: '小嫩草' },
|
||||
{ asset_id: 20003, name: '冉冉升起', cover_url: MOCK_IMAGES[2], like_count: 10200, owner_nickname: '小泡泡' },
|
||||
{ asset_id: 20004, name: '明日之星', cover_url: MOCK_IMAGES[3], like_count: 9800, owner_nickname: '小火苗' },
|
||||
{ asset_id: 20005, name: '潜力无限', cover_url: MOCK_IMAGES[4], like_count: 8900, owner_nickname: '小萌芽' },
|
||||
{ asset_id: 20006, name: '闪耀新星', cover_url: MOCK_IMAGES[5], like_count: 8200, owner_nickname: '小水滴' },
|
||||
{ asset_id: 20007, name: '小荷才露', cover_url: MOCK_IMAGES[6], like_count: 7600, owner_nickname: '小竹笋' },
|
||||
{ asset_id: 20008, name: '锋芒初现', cover_url: MOCK_IMAGES[7], like_count: 7100, owner_nickname: '小鸽子' },
|
||||
{ asset_id: 20009, name: '闪闪发光', cover_url: MOCK_IMAGES[8], like_count: 6500, owner_nickname: '小萤火' },
|
||||
{ asset_id: 20010, name: '未来可期', cover_url: MOCK_IMAGES[9], like_count: 5900, owner_nickname: '小芽芽' },
|
||||
{ asset_id: 20011, name: '新秀登场', cover_url: MOCK_IMAGES[10], like_count: 5400, owner_nickname: '小藤蔓' },
|
||||
// { asset_id: 20012, name: '蒸蒸日上', cover_url: MOCK_IMAGES[11], like_count: 4900, owner_nickname: '小葵花' },
|
||||
// { asset_id: 20013, name: '茁壮成长', cover_url: MOCK_IMAGES[12], like_count: 4500, owner_nickname: '小苗苗' },
|
||||
// { asset_id: 20014, name: '初绽光芒', cover_url: MOCK_IMAGES[13], like_count: 4100, owner_nickname: '小花花' },
|
||||
// { asset_id: 20015, name: '星火燎原', cover_url: MOCK_IMAGES[14], like_count: 3800, owner_nickname: '小豆芽' },
|
||||
// { asset_id: 20016, name: '蓄力中...', cover_url: MOCK_IMAGES[15], like_count: 3500, owner_nickname: '小冰晶' },
|
||||
],
|
||||
cursor: 'qianlixing_cursor_001',
|
||||
has_more: true,
|
||||
session_id: 'qianlixing_session',
|
||||
}
|
||||
|
||||
// ========== 新鲜上架 - 新发布作品,点赞较低 ==========
|
||||
export const MOCK_XINXIANSHANG = {
|
||||
items: [
|
||||
@ -145,17 +115,32 @@ export const MOCK_XINXIANSHANG = {
|
||||
{ asset_id: 30009, name: '新鲜出炉', cover_url: MOCK_IMAGES[8], like_count: 98, owner_nickname: '小创作者' },
|
||||
{ asset_id: 30010, name: '首发作品', cover_url: MOCK_IMAGES[9], like_count: 267, owner_nickname: '小练手' },
|
||||
{ asset_id: 30011, name: '全新上线', cover_url: MOCK_IMAGES[10], like_count: 189, owner_nickname: '新起步' },
|
||||
// { asset_id: 30012, name: '今日份新', cover_url: MOCK_IMAGES[11], like_count: 156, owner_nickname: '小萌娃' },
|
||||
// { asset_id: 30013, name: '新新人类', cover_url: MOCK_IMAGES[12], like_count: 223, owner_nickname: '小新潮' },
|
||||
// { asset_id: 30014, name: '新鲜血液', cover_url: MOCK_IMAGES[13], like_count: 134, owner_nickname: '小白白' },
|
||||
// { asset_id: 30015, name: '新晋选手', cover_url: MOCK_IMAGES[14], like_count: 278, owner_nickname: '小小新' },
|
||||
// { asset_id: 30016, name: '首发新动态', cover_url: MOCK_IMAGES[15], like_count: 201, owner_nickname: '小清新' },
|
||||
],
|
||||
cursor: 'xinxianshang_cursor_001',
|
||||
has_more: true,
|
||||
session_id: 'xinxianshang_session',
|
||||
}
|
||||
|
||||
// ========== 潜力之星 - 中等点赞有潜力的作品 ==========
|
||||
export const MOCK_QIANLIXING = {
|
||||
items: [
|
||||
{ asset_id: 20001, name: '初露锋芒', cover_url: MOCK_IMAGES[0], like_count: 12800, owner_nickname: '小新芽' },
|
||||
{ asset_id: 20002, name: '蓄势待发', cover_url: MOCK_IMAGES[1], like_count: 11500, owner_nickname: '小嫩草' },
|
||||
{ asset_id: 20003, name: '冉冉升起', cover_url: MOCK_IMAGES[2], like_count: 10200, owner_nickname: '小泡泡' },
|
||||
{ asset_id: 20004, name: '明日之星', cover_url: MOCK_IMAGES[3], like_count: 9800, owner_nickname: '小火苗' },
|
||||
{ asset_id: 20005, name: '潜力无限', cover_url: MOCK_IMAGES[4], like_count: 8900, owner_nickname: '小萌芽' },
|
||||
{ asset_id: 20006, name: '闪耀新星', cover_url: MOCK_IMAGES[5], like_count: 8200, owner_nickname: '小水滴' },
|
||||
{ asset_id: 20007, name: '小荷才露', cover_url: MOCK_IMAGES[6], like_count: 7600, owner_nickname: '小竹笋' },
|
||||
{ asset_id: 20008, name: '锋芒初现', cover_url: MOCK_IMAGES[7], like_count: 7100, owner_nickname: '小鸽子' },
|
||||
{ asset_id: 20009, name: '闪闪发光', cover_url: MOCK_IMAGES[8], like_count: 6500, owner_nickname: '小萤火' },
|
||||
{ asset_id: 20010, name: '未来可期', cover_url: MOCK_IMAGES[9], like_count: 5900, owner_nickname: '小芽芽' },
|
||||
{ asset_id: 20011, name: '新秀登场', cover_url: MOCK_IMAGES[10], like_count: 5400, owner_nickname: '小藤蔓' },
|
||||
],
|
||||
cursor: 'qianlixing_cursor_001',
|
||||
has_more: true,
|
||||
session_id: 'qianlixing_session',
|
||||
}
|
||||
|
||||
// ========== 随机寻宝 - 随机混合数据 ==========
|
||||
export const MOCK_SUIJIXUNBAO = {
|
||||
items: [
|
||||
@ -170,11 +155,6 @@ export const MOCK_SUIJIXUNBAO = {
|
||||
{ asset_id: 40009, name: '神秘宝藏9', cover_url: MOCK_IMAGES[8], like_count: 62000, owner_nickname: '寻宝奇兵' },
|
||||
{ asset_id: 40010, name: '神秘宝藏10', cover_url: MOCK_IMAGES[9], like_count: 800, owner_nickname: '淘宝猎人' },
|
||||
{ asset_id: 40011, name: '神秘宝藏11', cover_url: MOCK_IMAGES[10], like_count: 9500, owner_nickname: '挖宝小分队' },
|
||||
// { asset_id: 40012, name: '神秘宝藏12', cover_url: MOCK_IMAGES[11], like_count: 72000, owner_nickname: '淘宝小能手' },
|
||||
// { asset_id: 40013, name: '神秘宝藏13', cover_url: MOCK_IMAGES[12], like_count: 350, owner_nickname: '宝藏猎人' },
|
||||
// { asset_id: 40014, name: '神秘宝藏14', cover_url: MOCK_IMAGES[13], like_count: 48000, owner_nickname: '寻宝小天才' },
|
||||
// { asset_id: 40015, name: '神秘宝藏15', cover_url: MOCK_IMAGES[14], like_count: 11000, owner_nickname: '淘宝小精灵' },
|
||||
// { asset_id: 40016, name: '神秘宝藏16', cover_url: MOCK_IMAGES[15], like_count: 600, owner_nickname: '挖宝小专家' },
|
||||
],
|
||||
cursor: 'suijixunbao_cursor_001',
|
||||
has_more: true,
|
||||
@ -184,9 +164,9 @@ export const MOCK_SUIJIXUNBAO = {
|
||||
// ========== 分类映射 ==========
|
||||
export const MOCK_DATA_MAP = {
|
||||
hot: MOCK_RENQIWANG,
|
||||
xingka: MOCK_QIANLIXING,
|
||||
baji: MOCK_XINXIANSHANG,
|
||||
haibao: MOCK_SUIJIXUNBAO,
|
||||
new: MOCK_XINXIANSHANG,
|
||||
potential: MOCK_QIANLIXING,
|
||||
random: MOCK_SUIJIXUNBAO,
|
||||
}
|
||||
|
||||
// 根据分类获取模拟数据
|
||||
@ -207,13 +187,13 @@ export function generateMockItems(category = 'hot', count = 20, startId = 50000)
|
||||
case 'hot':
|
||||
like_count = 20000 + Math.floor(Math.random() * 100000) // 2w-12w
|
||||
break
|
||||
case 'xingka':
|
||||
like_count = 3000 + Math.floor(Math.random() * 15000) // 3k-18k
|
||||
break
|
||||
case 'baji':
|
||||
case 'new':
|
||||
like_count = 50 + Math.floor(Math.random() * 500) // 50-550
|
||||
break
|
||||
case 'haibao':
|
||||
case 'potential':
|
||||
like_count = 3000 + Math.floor(Math.random() * 15000) // 3k-18k
|
||||
break
|
||||
case 'random':
|
||||
like_count = Math.floor(Math.random() * 80000) // 0-8w
|
||||
break
|
||||
default:
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
:bannerBottom="bannerBottomPx"
|
||||
:useMockData="USE_MOCK_DATA"
|
||||
:category="activeContentTab"
|
||||
:isActive="isActive"
|
||||
@cardClick="handleCardClick"
|
||||
class="fall-bg"
|
||||
/>
|
||||
@ -59,7 +60,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { onLoad, onShow, onHide } from '@dcloudio/uni-app'
|
||||
import { useStore } from 'vuex'
|
||||
import Header from '../components/Header.vue'
|
||||
import BottomNav from '../components/BottomNav.vue'
|
||||
@ -78,9 +79,10 @@ const currentUserNickname = computed(() => store.state.user?.userInfo?.nickname
|
||||
const currentStarId = ref(uni.getStorageSync('star_id') || null)
|
||||
|
||||
// ========== UI State ==========
|
||||
const activeContentTab = ref('hot')
|
||||
const activeContentTab = ref('random')
|
||||
const navExpanded = ref(false)
|
||||
const showRankingModal = ref(false)
|
||||
const isActive = ref(true)
|
||||
|
||||
// ========== Screen Info ==========
|
||||
const screenWidth = ref(375)
|
||||
@ -163,6 +165,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
isActive.value = true
|
||||
// 检查是否需要显示引导,如果需要则跳转到引导页面
|
||||
if (shouldShowGuideStartModal()) {
|
||||
uni.navigateTo({
|
||||
@ -171,6 +174,10 @@ onShow(() => {
|
||||
}
|
||||
})
|
||||
|
||||
onHide(() => {
|
||||
isActive.value = false
|
||||
})
|
||||
|
||||
onLoad((options) => {
|
||||
// 调试模式:读取 guide_debug 参数并设置存储
|
||||
if (options && 'guide_debug' in options) {
|
||||
|
||||
@ -83,11 +83,10 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { onLoad, onUnload, onShow, onHide } from '@dcloudio/uni-app'
|
||||
import {
|
||||
fetchActivityDetail,
|
||||
import {
|
||||
fetchActivityDetail,
|
||||
fetchActivityProgress,
|
||||
} from '@/utils/activity-config'
|
||||
import { getOssPresignedUrlApi } from '@/utils/api'
|
||||
import { getProgressManager, releaseProgressManager, cleanupProgressManager } from '@/utils/progress-manager'
|
||||
import screenCache from '@/utils/screen-cache'
|
||||
import performanceMonitor from '@/utils/performance-monitor'
|
||||
@ -222,52 +221,23 @@ async function handleContribute(itemType) {
|
||||
function onAnimationComplete() {
|
||||
}
|
||||
|
||||
// 预签名 URL 缓存,同一路径只请求一次
|
||||
const presignedUrlCache = new Map()
|
||||
|
||||
/**
|
||||
* 转换图片路径为OSS预签名URL
|
||||
*/
|
||||
async function convertImageToPresignedUrl(imagePath) {
|
||||
if (!imagePath) return null
|
||||
if (imagePath === '') return ''
|
||||
|
||||
if (presignedUrlCache.has(imagePath)) {
|
||||
return presignedUrlCache.get(imagePath)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getOssPresignedUrlApi(imagePath, 3600, 'avatar')
|
||||
const url = response.data?.url || imagePath
|
||||
presignedUrlCache.set(imagePath, url)
|
||||
return url
|
||||
} catch (error) {
|
||||
console.error('图片URL转换失败:', imagePath, error)
|
||||
return imagePath
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量转换图片路径
|
||||
*/
|
||||
// 批量转换图片路径 - 直接使用后端返回的URL
|
||||
async function convertActivityImages(activityData) {
|
||||
try {
|
||||
// 并发转换所有图片
|
||||
const [bannerImage, coverImage, stageBackground] = await Promise.all([
|
||||
convertImageToPresignedUrl(activityData.banner_image),
|
||||
convertImageToPresignedUrl(activityData.cover_image),
|
||||
convertImageToPresignedUrl(activityData.current_stage_background)
|
||||
Promise.resolve(activityData.banner_image),
|
||||
Promise.resolve(activityData.cover_image),
|
||||
Promise.resolve(activityData.current_stage_background)
|
||||
])
|
||||
|
||||
|
||||
// 转换道具图标
|
||||
const actionItems = activityData.items || []
|
||||
const convertedItems = await Promise.all(
|
||||
actionItems.map(async (item) => ({
|
||||
...item,
|
||||
icon: await convertImageToPresignedUrl(item.icon)
|
||||
}))
|
||||
)
|
||||
|
||||
const convertedItems = actionItems.map(item => ({
|
||||
...item,
|
||||
icon: item.icon
|
||||
}))
|
||||
|
||||
return {
|
||||
bannerImage,
|
||||
coverImage,
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { loginApi, registerApi, getOssPresignedUrlApi } from '@/utils/api'
|
||||
import { downloadAndCacheAvatar } from '@/utils/avatarCache'
|
||||
import { loginApi, registerApi } from '@/utils/api'
|
||||
import { resetAllGuides } from '@/utils/guideConfig'
|
||||
|
||||
const state = {
|
||||
@ -103,31 +102,7 @@ const actions = {
|
||||
|
||||
uni.setStorageSync('user', JSON.stringify(user))
|
||||
commit('SET_USER_INFO', user)
|
||||
|
||||
// 处理头像缓存
|
||||
if (user.avatar_url) {
|
||||
try {
|
||||
// 获取真实的预签名URL
|
||||
const urlRes = await getOssPresignedUrlApi(user.avatar_url, 3600, 'avatar')
|
||||
if (urlRes.code === 200 && urlRes.data && urlRes.data.url) {
|
||||
// 下载并缓存头像
|
||||
const localPath = await downloadAndCacheAvatar(user.avatar_url, urlRes.data.url)
|
||||
|
||||
// 触发头像更新事件,通知Header刷新
|
||||
setTimeout(() => {
|
||||
uni.$emit('avatarUpdated', {
|
||||
uid: user.uid,
|
||||
avatarUrl: user.avatar_url,
|
||||
localPath: localPath
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
} catch (avatarError) {
|
||||
console.error('头像缓存失败,但不影响登录:', avatarError)
|
||||
// 头像缓存失败不影响登录流程
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return Promise.resolve(res)
|
||||
} else {
|
||||
// 返回错误信息,包含 message
|
||||
@ -162,31 +137,7 @@ const actions = {
|
||||
|
||||
uni.setStorageSync('user', JSON.stringify(user))
|
||||
commit('SET_USER_INFO', user)
|
||||
|
||||
// 处理头像缓存
|
||||
if (user.avatar_url) {
|
||||
try {
|
||||
// 获取真实的预签名URL
|
||||
const urlRes = await getOssPresignedUrlApi(user.avatar_url, 3600, 'avatar')
|
||||
if (urlRes.code === 200 && urlRes.data && urlRes.data.url) {
|
||||
// 下载并缓存头像
|
||||
const localPath = await downloadAndCacheAvatar(user.avatar_url, urlRes.data.url)
|
||||
|
||||
// 触发头像更新事件,通知Header刷新
|
||||
setTimeout(() => {
|
||||
uni.$emit('avatarUpdated', {
|
||||
uid: user.uid,
|
||||
avatarUrl: user.avatar_url,
|
||||
localPath: localPath
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
} catch (avatarError) {
|
||||
console.error('头像缓存失败,但不影响注册:', avatarError)
|
||||
// 头像缓存失败不影响注册流程
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return Promise.resolve(res)
|
||||
} else {
|
||||
// 处理昵称已存在的情况
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// API 基础配置
|
||||
const baseURL = 'http://101.132.250.62:8080'
|
||||
// const baseURL = 'http://101.132.250.62:8080'
|
||||
// const baseURL = 'http://192.168.110.60:8080'
|
||||
// const baseURL = 'http://localhost:8080'
|
||||
const baseURL = 'http://localhost:8080'
|
||||
|
||||
// 是否使用模拟数据(开发调试时设为 true,后端API准备好后改为 false)
|
||||
const USE_MOCK_API = false
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { getOssPresignedUrlApi } from './api.js';
|
||||
|
||||
/**
|
||||
* 从URL提取文件名
|
||||
* @param {String} url - URL字符串
|
||||
@ -9,10 +7,10 @@ export function extractFileNameFromUrl(url) {
|
||||
if (!url || url.startsWith('/static/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
let urlPath = url;
|
||||
|
||||
|
||||
// 如果是完整URL,提取路径部分
|
||||
if (url.includes('://')) {
|
||||
// 找到协议后的第一个 / 开始的路径
|
||||
@ -22,7 +20,7 @@ export function extractFileNameFromUrl(url) {
|
||||
urlPath = url.substring(pathStartIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 提取最后一个 / 之后的内容作为文件名
|
||||
const fileName = urlPath.substring(urlPath.lastIndexOf('/') + 1);
|
||||
return fileName || null;
|
||||
@ -66,7 +64,7 @@ export function extractOssObjectPath(url) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取藏品封面的真实URL
|
||||
* 获取藏品封面的真实URL - 直接使用后端返回的URL
|
||||
* @param {String} coverUrl - 后端返回的cover_url
|
||||
* @returns {Promise<String>} 真实可访问的URL
|
||||
*/
|
||||
@ -79,63 +77,21 @@ export async function getAssetCoverRealUrl(coverUrl) {
|
||||
return coverUrl || DEFAULT_IMAGE;
|
||||
}
|
||||
|
||||
try {
|
||||
// 提取完整OSS对象路径(保留 asset/13/87/filename.jpg 这样的前缀)
|
||||
let objectPath = extractOssObjectPath(coverUrl);
|
||||
if (!objectPath) {
|
||||
console.warn('无法提取OSS对象路径,使用默认图片');
|
||||
return DEFAULT_IMAGE;
|
||||
}
|
||||
|
||||
// URL解码(处理 %2F -> / 等编码)
|
||||
let decodedPath = decodeURIComponent(objectPath);
|
||||
|
||||
// 去掉 query string(?及其后面的内容)
|
||||
const queryIndex = decodedPath.indexOf('?');
|
||||
if (queryIndex !== -1) {
|
||||
decodedPath = decodedPath.substring(0, queryIndex);
|
||||
}
|
||||
|
||||
|
||||
// 调用API获取预签名URL(type='asset')
|
||||
const res = await getOssPresignedUrlApi(decodedPath, 3600, 'asset');
|
||||
|
||||
if (res.code === 200 && res.data && res.data.url) {
|
||||
return res.data.url;
|
||||
} else {
|
||||
console.error('获取OSS预签名URL失败:', res.message);
|
||||
return DEFAULT_IMAGE;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取藏品封面URL失败:', error);
|
||||
return DEFAULT_IMAGE;
|
||||
}
|
||||
// 直接使用后端返回的 URL
|
||||
return coverUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取好友头像的真实URL
|
||||
* 获取好友头像的真实URL - 直接使用后端返回的URL
|
||||
* @param {String} avatarUrl - 后端返回的avatar_url(完整URL)
|
||||
* @returns {Promise<String>} 真实可访问的URL,失败返回空字符串(让Avatar组件使用默认头像)
|
||||
* @returns {Promise<String>} 真实可访问的URL,失败返回空字符串
|
||||
*/
|
||||
export async function getFriendAvatarRealUrl(avatarUrl) {
|
||||
|
||||
// 如果是本地静态资源或为空,直接返回
|
||||
if (!avatarUrl || avatarUrl.startsWith('/static/')) {
|
||||
return avatarUrl || '';
|
||||
}
|
||||
|
||||
try {
|
||||
// 直接将完整URL作为filename传递给预签名API
|
||||
const res = await getOssPresignedUrlApi(avatarUrl, 3600, 'avatar');
|
||||
|
||||
if (res.code === 200 && res.data && res.data.url) {
|
||||
return res.data.url;
|
||||
} else {
|
||||
console.error('获取头像预签名URL失败:', res.message);
|
||||
return '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取好友头像URL失败:', error);
|
||||
return '';
|
||||
}
|
||||
|
||||
// 直接使用后端返回的 URL
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user