feat: 移除oss预url的鉴权

This commit is contained in:
zerosaturation 2026-05-07 14:12:07 +08:00
parent 1b82e4c3bc
commit 75893b1f42
15 changed files with 214 additions and 387 deletions

View File

@ -263,15 +263,15 @@ const handleLike = async () => {
if (liking.value || !assetData.value.asset_id) return; if (liking.value || !assetData.value.asset_id) return;
liking.value = true; liking.value = true;
try { try {
if (isLiked.value) { // if (isLiked.value) {
await unlikeAssetApi(assetData.value.asset_id); // await unlikeAssetApi(assetData.value.asset_id);
isLiked.value = false; // isLiked.value = false;
likeCount.value = Math.max(0, likeCount.value - 1); // likeCount.value = Math.max(0, likeCount.value - 1);
} else { // } else {
await likeAssetApi(assetData.value.asset_id); await likeAssetApi(assetData.value.asset_id);
isLiked.value = true; isLiked.value = true;
likeCount.value += 1; likeCount.value += 1;
} // }
// //
uni.$emit('assetLikeChanged', { uni.$emit('assetLikeChanged', {
asset_id: assetData.value.asset_id, asset_id: assetData.value.asset_id,

View File

@ -11,9 +11,6 @@
<script setup> <script setup>
import { computed, ref, watch, onMounted } from 'vue'; 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 // props
const props = defineProps({ const props = defineProps({
@ -59,67 +56,30 @@
} }
}); });
// URL //
const ossPresignedUrl = ref(''); const localAvatarUrl = ref('');
// // 使URL
const fetchOwnAvatarWithCache = async () => { const fetchOwnAvatar = () => {
if (!props.avatarUrl) { if (!props.avatarUrl) {
ossPresignedUrl.value = ''; localAvatarUrl.value = '';
return; return;
} }
// 使 URL
// 1. localAvatarUrl.value = props.avatarUrl;
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 // 使URL
const fetchFriendAvatarWithoutCache = () => { const fetchFriendAvatar = () => {
// FriendsContent使 localAvatarUrl.value = props.avatarUrl || '';
ossPresignedUrl.value = props.avatarUrl || '';
}; };
// enableCache使 // enableCache使
const fetchAvatar = () => { const fetchAvatar = () => {
if (props.enableCache) { if (props.enableCache) {
// fetchOwnAvatar();
fetchOwnAvatarWithCache();
} else { } else {
// 使URL fetchFriendAvatar();
fetchFriendAvatarWithoutCache();
} }
}; };
@ -160,13 +120,11 @@
return `/static/avatar/${index}.jpeg`; return `/static/avatar/${index}.jpeg`;
}; };
// 使OSS使 // 使URL使
const avatarImage = computed(() => { const avatarImage = computed(() => {
// OSSURL使 if (localAvatarUrl.value) {
if (ossPresignedUrl.value) { return localAvatarUrl.value;
return ossPresignedUrl.value;
} }
// 使
return getDefaultAvatar(); return getDefaultAvatar();
}); });

View File

@ -7,18 +7,13 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { getHotRankingApi, getOssPresignedUrlApi } from '@/utils/api.js'; import { getHotRankingApi } from '@/utils/api.js';
const emit = defineEmits(['dataLoaded']); const emit = defineEmits(['dataLoaded']);
const resolveOssUrl = async (fileName, type) => { const resolveOssUrl = async (fileName, type) => {
if (!fileName) return ''; if (!fileName) return '';
try { // 使URL
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);
}
return fileName; return fileName;
}; };
@ -27,13 +22,9 @@ const loadTop3 = async () => {
const res = await getHotRankingApi('total', null, 1, 3); const res = await getHotRankingApi('total', null, 1, 3);
if (res.code === 200 && res.data?.items) { if (res.code === 200 && res.data?.items) {
const items = res.data.items.slice(0, 3); const items = res.data.items.slice(0, 3);
const resolved = await Promise.all(items.map(async (item) => { const resolved = items.map(item => {
const [coverUrl, avatarUrl] = await Promise.all([ return { ...item, cover_url: item.cover_url || '', avatar_url: item.avatar_url || '' };
resolveOssUrl(item.cover_url || '', 'asset'), });
resolveOssUrl(item.avatar_url || '', 'avatar'),
]);
return { ...item, cover_url: coverUrl, avatar_url: avatarUrl };
}));
emit('dataLoaded', resolved); emit('dataLoaded', resolved);
} }
} catch (e) { } catch (e) {

View File

@ -137,8 +137,7 @@ import {
getHotRankingApi, getHotRankingApi,
getOriginalRankingApi, getOriginalRankingApi,
getActivityRankingApi, getActivityRankingApi,
getActivityListApi, getActivityListApi
getOssPresignedUrlApi
} from '@/utils/api.js'; } from '@/utils/api.js';
import { import {
ACTIVITY_TYPES ACTIVITY_TYPES
@ -367,19 +366,11 @@ const fetchActivityList = async () => {
} }
}; };
// OSSURL // OSSURL - 使URL
const getOssImageUrl = async (fileName, type = 'avatar') => { const getOssImageUrl = async (fileName, type = 'avatar') => {
if (!fileName || fileName === '') return; if (!fileName || fileName === '') return;
try { // 使URL
const response = await getOssPresignedUrlApi(fileName, 3600, type); return fileName;
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; // ,
}
}; };
// //

View File

@ -4,7 +4,7 @@
<image class="bg-image" src="/static/square/beijingban.png" mode="aspectFill"></image> <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"> <view class="nav-back" @tap="goBack">
<image class="nav-back-icon" src="/static/icon/back.png" mode="aspectFit"></image> <image class="nav-back-icon" src="/static/icon/back.png" mode="aspectFit"></image>
</view> </view>
@ -51,7 +51,9 @@
<!-- 空状态占位显示剩余空展位卡片 --> <!-- 空状态占位显示剩余空展位卡片 -->
<view v-if="exhibitionWorks.length < 2" class="empty-exhibition"> <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="empty-cover" src="/static/nft/placeholder.png" mode="aspectFill"></image>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" <image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png"
mode="aspectFill"></image> mode="aspectFill"></image>
@ -83,7 +85,7 @@
<view v-for="(item, index) in likedWorks" :key="item.id" class="liked-row" <view v-for="(item, index) in likedWorks" :key="item.id" class="liked-row"
@tap="goToAssetDetail(item.id)"> @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' : ''"> <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%); background: rgb(249 159 192 / 45%);
border-radius: 48rpx; border-radius: 48rpx;
padding: 16rpx; padding: 16rpx;

View File

@ -263,9 +263,9 @@ import { useStore } from 'vuex';
import { onReady } from "@dcloudio/uni-app"; import { onReady } from "@dcloudio/uni-app";
import Header from '../components/Header.vue'; import Header from '../components/Header.vue';
import Avatar from '../components/Avatar.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 { validateNickname } from '@/utils/validator.js';
import { clearAvatarCache, downloadAndCacheAvatar } from '@/utils/avatarCache'; import { clearAvatarCache } from '@/utils/avatarCache';
import GuideListModal from '@/components/GuideListModal.vue'; import GuideListModal from '@/components/GuideListModal.vue';
import GuideOverlay from '@/components/GuideOverlay.vue'; import GuideOverlay from '@/components/GuideOverlay.vue';
import { import {
@ -1032,22 +1032,9 @@ const closeAvatarModal = () => {
showAvatarModal.value = false; showAvatarModal.value = false;
}; };
// UI // UI
const handleAvatarUpdateSuccess = async (newAvatarUrl) => { const handleAvatarUpdateSuccess = async (newAvatarUrl) => {
try { // 1.
// 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.
userAvatarUrl.value = newAvatarUrl; userAvatarUrl.value = newAvatarUrl;
const userStr = uni.getStorageSync('user'); const userStr = uni.getStorageSync('user');
if (userStr) { if (userStr) {

View File

@ -33,16 +33,17 @@
<script setup> <script setup>
defineProps({ defineProps({
modelValue: { type: String, default: 'hot' } modelValue: { type: String, default: 'new' }
}) })
defineEmits(['update:modelValue']) defineEmits(['update:modelValue'])
const tabs = [ const tabs = [
{ key: 'hot', label: '人气王者', emoji: null, icon: '/static/square/rementubiao.png', iconWidth: 104, iconHeight: 104 }, { 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: 'new', label: '新鲜上架', emoji: null, icon: '/static/square/baji.png', iconWidth: 80, iconHeight: 80 },
{ key: 'baji', 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: 'haibao', label: '随机寻宝', emoji: null, icon: '/static/square/haibao.png', iconWidth: 96, iconHeight: 96 },] { key: 'random', label: '随机寻宝', emoji: null, icon: '/static/square/haibao.png', iconWidth: 96, iconHeight: 96 },
]
</script> </script>
<style scoped> <style scoped>

View File

@ -66,7 +66,8 @@ const props = defineProps({
screenHeight: { type: Number, default: 812 }, screenHeight: { type: Number, default: 812 },
bannerBottom: { type: Number, default: 200 }, bannerBottom: { type: Number, default: 200 },
useMockData: { type: Boolean, default: false }, // 使 useMockData: { type: Boolean, default: false }, // 使
category: { type: String, default: 'hot' }, // category: { type: String, default: '' }, //
isActive: { type: Boolean, default: true }, //
}) })
const emit = defineEmits(['cardClick']) const emit = defineEmits(['cardClick'])
@ -91,6 +92,8 @@ let currentScrollLeft = 0
let idCounter = 0 let idCounter = 0
let isComponentMounted = false // let isComponentMounted = false //
let mockDataOffset = 0 // let mockDataOffset = 0 //
let appendFailed = false //
let isInitialLoading = true //
// ========== RAF ========== // ========== RAF ==========
const rafFn = (cb) => { const rafFn = (cb) => {
@ -112,17 +115,26 @@ let appendTimer = null // 防抖定时器
const startAutoScroll = () => { const startAutoScroll = () => {
if (!isComponentMounted) { if (!isComponentMounted) {
return return
} }
if (rafId) cancelAnimationFrame(rafId) // RAF
if (rafId) {
cafFn(rafId)
rafId = null
}
//
userInteracting = false
let lastScrollLeft = 0 // let lastScrollLeft = 0 //
const step = () => { const step = () => {
if (!isComponentMounted) { if (!isComponentMounted) {
rafId = null rafId = null
return
}
// RAF
if (!isComponentMounted) {
rafId = null
return return
} }
@ -143,7 +155,8 @@ const startAutoScroll = () => {
const remainingScroll = totalWidth.value - currentScrollLeft - props.screenWidth const remainingScroll = totalWidth.value - currentScrollLeft - props.screenWidth
if (remainingScroll < Math.max(totalWidth.value / 2, props.screenWidth)) { if (remainingScroll < Math.max(totalWidth.value / 2, props.screenWidth)) {
// appendMore // appendMore
if (!isLoadingMore) { // has_more
if (!isLoadingMore && !appendFailed && !isInitialLoading && props.useMockData) {
appendMore() appendMore()
} }
} }
@ -178,14 +191,14 @@ const pauseForUser = () => {
// 500ms // 500ms
const scheduleAppend = () => { const scheduleAppend = () => {
if (!isComponentMounted) return if (!isComponentMounted || appendFailed) return
if (appendTimer) clearTimeout(appendTimer) if (appendTimer) clearTimeout(appendTimer)
appendTimer = setTimeout(() => { appendTimer = setTimeout(() => {
appendTimer = null appendTimer = null
if (isComponentMounted) { if (isComponentMounted && !appendFailed) {
appendMore() appendMore()
} }
}, 500) }, 800)
} }
// ========== / ========== // ========== / ==========
@ -198,7 +211,10 @@ const onScroll = (e) => {
// //
const remainingScroll = totalWidth.value - currentScrollLeft - props.screenWidth const remainingScroll = totalWidth.value - currentScrollLeft - props.screenWidth
if (remainingScroll < Math.max(totalWidth.value / 2, 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 // 1. span span
// 2. span使 span // 2. span使 span
class WaterfallLayout { class WaterfallLayout {
constructor(containerH, category = 'hot', gap = GAP) { constructor(containerH, category = 'random', gap = GAP) {
this.containerH = containerH this.containerH = containerH
this.category = category this.category = category
this.gap = gap 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 MOCK_IMAGES = Array.from({ length: 16 }, (_, i) => `/static/sucai/image-${String(i + 1).padStart(2, '0')}.png`)
const mapUser = async (u) => { const mapUser = (u) => {
// // 使URL
let coverUrl = MOCK_IMAGES[idCounter % MOCK_IMAGES.length] let coverUrl = u.cover_url || 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 (_) {}
}
// span 1~4 // span 1~4
return { return {
id: idCounter++, id: idCounter++,
@ -451,6 +461,7 @@ const loadUsers = async () => {
// //
mockDataOffset = 0 mockDataOffset = 0
isLoadingMore = true
try { try {
let items let items
@ -465,7 +476,7 @@ const loadUsers = async () => {
} else { } else {
// 使API // 使API
const res = await getInspirationFlowApi({ limit: 40, type: props.category }) const res = await getInspirationFlowApi({ limit: 20, type: props.category })
if (!isComponentMounted) return if (!isComponentMounted) return
if (res.code === 200 && res.data?.items) { if (res.code === 200 && res.data?.items) {
@ -498,18 +509,9 @@ const loadUsers = async () => {
const appendMore = async () => { const appendMore = async () => {
if (!isComponentMounted) { if (!isComponentMounted) {
return return
} }
if (isLoadingMore) { if (isLoadingMore) {
//
setTimeout(() => {
if (isComponentMounted && !isLoadingMore) {
appendMore()
}
}, 500)
return return
} }
isLoadingMore = true isLoadingMore = true
@ -522,8 +524,6 @@ const appendMore = async () => {
const allItems = mockData.items const allItems = mockData.items
const batchSize = 20 const batchSize = 20
// mockDataOffset batchSize // mockDataOffset batchSize
const itemsToAdd = [] const itemsToAdd = []
for (let i = 0; i < batchSize; i++) { for (let i = 0; i < batchSize; i++) {
@ -542,18 +542,22 @@ const appendMore = async () => {
mockDataOffset = mockDataOffset + batchSize mockDataOffset = mockDataOffset + batchSize
items = itemsToAdd items = itemsToAdd
} else { } else {
// 使API // 使API
const res = await getInspirationFlowApi({ limit: 20, type: props.category }) const res = await getInspirationFlowApi({ limit: 20, type: props.category })
if (!isComponentMounted) return if (!isComponentMounted) return
if (res.code === 200 && res.data?.items) { if (res.code === 200 && res.data?.items) {
items = res.data.items items = res.data.items
// has_more
if (!res.data.has_more) {
appendFailed = true
}
} }
} }
if (items && items.length > 0) { if (items && items.length > 0) {
const withData = items.map((item) => { const withData = items.map((item) => {
return { return {
id: item.asset_id, // 使 asset_id id: item.asset_id, // 使 asset_id
@ -564,17 +568,20 @@ const appendMore = async () => {
span: item.span ?? null, span: item.span ?? null,
} }
}) })
const placed = layout.addCards(withData) const placed = layout.addCards(withData)
cards.value = [...cards.value, ...placed] cards.value = [...cards.value, ...placed]
totalWidth.value = layout.getTotalWidth() totalWidth.value = layout.getTotalWidth()
} else {
//
appendFailed = true
} }
} catch (e) { } catch (e) {
console.error('[WaterfallGrid] 追加用户失败', e?.message ?? e) console.error('[WaterfallGrid] 追加用户失败', e?.message ?? e)
appendFailed = true
} finally { } finally {
isLoadingMore = false isLoadingMore = false
} }
} }
@ -615,20 +622,57 @@ const handleCardClick = (card) => {
// ========== ========== // ========== ==========
onMounted(() => { onMounted(() => {
isComponentMounted = true isComponentMounted = true
const containerH = props.screenHeight - props.bannerBottom isInitialLoading = true
layout = new WaterfallLayout(containerH, props.category) appendFailed = false
userInteracting = false
currentScrollLeft = 0 currentScrollLeft = 0
scrollLeft.value = 0 scrollLeft.value = 0
const containerH = props.screenHeight - props.bannerBottom
layout = new WaterfallLayout(containerH, props.category)
loadUsers().then(() => { loadUsers().then(() => {
isInitialLoading = false
nextTick(() => startAutoScroll()) 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(() => { onUnmounted(() => {
isComponentMounted = false isComponentMounted = false
stopAutoScroll() stopAutoScroll()
clearTimeout(resumeTimer) clearTimeout(resumeTimer)
clearTimeout(appendTimer) clearTimeout(appendTimer)
if (typeof document !== 'undefined') {
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}) })
watch(() => [props.screenHeight, props.bannerBottom], () => { watch(() => [props.screenHeight, props.bannerBottom], () => {
@ -662,6 +706,8 @@ watch(() => props.category, (newCategory) => {
allUsers.value = [] allUsers.value = []
totalWidth.value = 0 totalWidth.value = 0
mockDataOffset = 0 mockDataOffset = 0
appendFailed = false
isInitialLoading = false
// RAF loadUsers // RAF loadUsers
if (rafId) { if (rafId) {

View File

@ -1,5 +1,5 @@
import { ref } from 'vue' import { ref } from 'vue'
import { getActivityListApi, getOssPresignedUrlApi } from '@/utils/api.js' import { getActivityListApi } from '@/utils/api.js'
export function useBanner() { export function useBanner() {
const bannerActivities = ref([]) const bannerActivities = ref([])
@ -8,24 +8,11 @@ export function useBanner() {
try { try {
const starId = uni.getStorageSync('star_id') || null const starId = uni.getStorageSync('star_id') || null
const res = await getActivityListApi(starId, 1, 10) const res = await getActivityListApi(starId, 1, 10)
if (res.code === 200 && res.data?.activities) { if (res.code === 200 && res.data?.activities) {
const activities = res.data.activities const activities = res.data.activities
// 并行获取所有 OSS URL // 直接使用后端返回的图片URL
const urlPromises = activities.map(async (item) => { bannerActivities.value = activities
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
)
} }
} catch (e) { } catch (e) {
console.error('[useBanner] 加载 banner 活动失败', e?.message ?? e) console.error('[useBanner] 加载 banner 活动失败', e?.message ?? e)

View File

@ -1,7 +1,7 @@
// ========== 模拟数据配置 ========== // ========== 模拟数据配置 ==========
// 是否使用模拟数据(开发调试时设为 true上线后改为 false // 是否使用模拟数据(开发调试时设为 true上线后改为 false
export const USE_MOCK_DATA = true export const USE_MOCK_DATA = false
// 模拟图片列表 // 模拟图片列表
const MOCK_IMAGES = [ const MOCK_IMAGES = [
@ -43,8 +43,16 @@ export const SPAN_CONFIG = {
{ max: Infinity, span: 4 }, // 10w+ → span 4 { 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: [ thresholds: [
{ max: 5000, span: 1 }, // 5k 以下 → span 1 { max: 5000, span: 1 }, // 5k 以下 → span 1
{ max: 10000, span: 2 }, // 5k-1w → span 2 { max: 10000, span: 2 }, // 5k-1w → span 2
@ -52,16 +60,8 @@ export const SPAN_CONFIG = {
{ max: Infinity, span: 4 },// 1.5w+ → span 4 { 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: [ thresholds: [
{ max: 10000, span: 1 }, // 1w 以下 → span 1 { max: 10000, span: 1 }, // 1w 以下 → span 1
{ max: 30000, span: 2 }, // 1w-3w → span 2 { 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: 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: 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: 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', cursor: 'renqiwang_cursor_001',
has_more: true, has_more: true,
session_id: 'renqiwang_session', 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 = { export const MOCK_XINXIANSHANG = {
items: [ 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: 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: 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: 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', cursor: 'xinxianshang_cursor_001',
has_more: true, has_more: true,
session_id: 'xinxianshang_session', 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 = { export const MOCK_SUIJIXUNBAO = {
items: [ 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: 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: 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: 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', cursor: 'suijixunbao_cursor_001',
has_more: true, has_more: true,
@ -184,9 +164,9 @@ export const MOCK_SUIJIXUNBAO = {
// ========== 分类映射 ========== // ========== 分类映射 ==========
export const MOCK_DATA_MAP = { export const MOCK_DATA_MAP = {
hot: MOCK_RENQIWANG, hot: MOCK_RENQIWANG,
xingka: MOCK_QIANLIXING, new: MOCK_XINXIANSHANG,
baji: MOCK_XINXIANSHANG, potential: MOCK_QIANLIXING,
haibao: MOCK_SUIJIXUNBAO, random: MOCK_SUIJIXUNBAO,
} }
// 根据分类获取模拟数据 // 根据分类获取模拟数据
@ -207,13 +187,13 @@ export function generateMockItems(category = 'hot', count = 20, startId = 50000)
case 'hot': case 'hot':
like_count = 20000 + Math.floor(Math.random() * 100000) // 2w-12w like_count = 20000 + Math.floor(Math.random() * 100000) // 2w-12w
break break
case 'xingka': case 'new':
like_count = 3000 + Math.floor(Math.random() * 15000) // 3k-18k
break
case 'baji':
like_count = 50 + Math.floor(Math.random() * 500) // 50-550 like_count = 50 + Math.floor(Math.random() * 500) // 50-550
break 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 like_count = Math.floor(Math.random() * 80000) // 0-8w
break break
default: default:

View File

@ -10,6 +10,7 @@
:bannerBottom="bannerBottomPx" :bannerBottom="bannerBottomPx"
:useMockData="USE_MOCK_DATA" :useMockData="USE_MOCK_DATA"
:category="activeContentTab" :category="activeContentTab"
:isActive="isActive"
@cardClick="handleCardClick" @cardClick="handleCardClick"
class="fall-bg" class="fall-bg"
/> />
@ -59,7 +60,7 @@
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue' 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 { useStore } from 'vuex'
import Header from '../components/Header.vue' import Header from '../components/Header.vue'
import BottomNav from '../components/BottomNav.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) const currentStarId = ref(uni.getStorageSync('star_id') || null)
// ========== UI State ========== // ========== UI State ==========
const activeContentTab = ref('hot') const activeContentTab = ref('random')
const navExpanded = ref(false) const navExpanded = ref(false)
const showRankingModal = ref(false) const showRankingModal = ref(false)
const isActive = ref(true)
// ========== Screen Info ========== // ========== Screen Info ==========
const screenWidth = ref(375) const screenWidth = ref(375)
@ -163,6 +165,7 @@ onMounted(() => {
}) })
onShow(() => { onShow(() => {
isActive.value = true
// //
if (shouldShowGuideStartModal()) { if (shouldShowGuideStartModal()) {
uni.navigateTo({ uni.navigateTo({
@ -171,6 +174,10 @@ onShow(() => {
} }
}) })
onHide(() => {
isActive.value = false
})
onLoad((options) => { onLoad((options) => {
// guide_debug // guide_debug
if (options && 'guide_debug' in options) { if (options && 'guide_debug' in options) {

View File

@ -83,11 +83,10 @@
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onLoad, onUnload, onShow, onHide } from '@dcloudio/uni-app' import { onLoad, onUnload, onShow, onHide } from '@dcloudio/uni-app'
import { import {
fetchActivityDetail, fetchActivityDetail,
fetchActivityProgress, fetchActivityProgress,
} from '@/utils/activity-config' } from '@/utils/activity-config'
import { getOssPresignedUrlApi } from '@/utils/api'
import { getProgressManager, releaseProgressManager, cleanupProgressManager } from '@/utils/progress-manager' import { getProgressManager, releaseProgressManager, cleanupProgressManager } from '@/utils/progress-manager'
import screenCache from '@/utils/screen-cache' import screenCache from '@/utils/screen-cache'
import performanceMonitor from '@/utils/performance-monitor' import performanceMonitor from '@/utils/performance-monitor'
@ -222,52 +221,23 @@ async function handleContribute(itemType) {
function onAnimationComplete() { function onAnimationComplete() {
} }
// URL // - 使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
}
}
/**
* 批量转换图片路径
*/
async function convertActivityImages(activityData) { async function convertActivityImages(activityData) {
try { try {
// //
const [bannerImage, coverImage, stageBackground] = await Promise.all([ const [bannerImage, coverImage, stageBackground] = await Promise.all([
convertImageToPresignedUrl(activityData.banner_image), Promise.resolve(activityData.banner_image),
convertImageToPresignedUrl(activityData.cover_image), Promise.resolve(activityData.cover_image),
convertImageToPresignedUrl(activityData.current_stage_background) Promise.resolve(activityData.current_stage_background)
]) ])
// //
const actionItems = activityData.items || [] const actionItems = activityData.items || []
const convertedItems = await Promise.all( const convertedItems = actionItems.map(item => ({
actionItems.map(async (item) => ({ ...item,
...item, icon: item.icon
icon: await convertImageToPresignedUrl(item.icon) }))
}))
)
return { return {
bannerImage, bannerImage,
coverImage, coverImage,

View File

@ -1,5 +1,4 @@
import { loginApi, registerApi, getOssPresignedUrlApi } from '@/utils/api' import { loginApi, registerApi } from '@/utils/api'
import { downloadAndCacheAvatar } from '@/utils/avatarCache'
import { resetAllGuides } from '@/utils/guideConfig' import { resetAllGuides } from '@/utils/guideConfig'
const state = { const state = {
@ -103,31 +102,7 @@ const actions = {
uni.setStorageSync('user', JSON.stringify(user)) uni.setStorageSync('user', JSON.stringify(user))
commit('SET_USER_INFO', 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) return Promise.resolve(res)
} else { } else {
// 返回错误信息,包含 message // 返回错误信息,包含 message
@ -162,31 +137,7 @@ const actions = {
uni.setStorageSync('user', JSON.stringify(user)) uni.setStorageSync('user', JSON.stringify(user))
commit('SET_USER_INFO', 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) return Promise.resolve(res)
} else { } else {
// 处理昵称已存在的情况 // 处理昵称已存在的情况

View File

@ -1,7 +1,7 @@
// API 基础配置 // 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://192.168.110.60:8080'
// const baseURL = 'http://localhost:8080' const baseURL = 'http://localhost:8080'
// 是否使用模拟数据(开发调试时设为 true后端API准备好后改为 false // 是否使用模拟数据(开发调试时设为 true后端API准备好后改为 false
const USE_MOCK_API = false const USE_MOCK_API = false

View File

@ -1,5 +1,3 @@
import { getOssPresignedUrlApi } from './api.js';
/** /**
* 从URL提取文件名 * 从URL提取文件名
* @param {String} url - URL字符串 * @param {String} url - URL字符串
@ -9,10 +7,10 @@ export function extractFileNameFromUrl(url) {
if (!url || url.startsWith('/static/')) { if (!url || url.startsWith('/static/')) {
return null; return null;
} }
try { try {
let urlPath = url; let urlPath = url;
// 如果是完整URL提取路径部分 // 如果是完整URL提取路径部分
if (url.includes('://')) { if (url.includes('://')) {
// 找到协议后的第一个 / 开始的路径 // 找到协议后的第一个 / 开始的路径
@ -22,7 +20,7 @@ export function extractFileNameFromUrl(url) {
urlPath = url.substring(pathStartIndex); urlPath = url.substring(pathStartIndex);
} }
} }
// 提取最后一个 / 之后的内容作为文件名 // 提取最后一个 / 之后的内容作为文件名
const fileName = urlPath.substring(urlPath.lastIndexOf('/') + 1); const fileName = urlPath.substring(urlPath.lastIndexOf('/') + 1);
return fileName || null; return fileName || null;
@ -66,7 +64,7 @@ export function extractOssObjectPath(url) {
} }
/** /**
* 获取藏品封面的真实URL * 获取藏品封面的真实URL - 直接使用后端返回的URL
* @param {String} coverUrl - 后端返回的cover_url * @param {String} coverUrl - 后端返回的cover_url
* @returns {Promise<String>} 真实可访问的URL * @returns {Promise<String>} 真实可访问的URL
*/ */
@ -79,63 +77,21 @@ export async function getAssetCoverRealUrl(coverUrl) {
return coverUrl || DEFAULT_IMAGE; return coverUrl || DEFAULT_IMAGE;
} }
try { // 直接使用后端返回的 URL
// 提取完整OSS对象路径保留 asset/13/87/filename.jpg 这样的前缀) return coverUrl;
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获取预签名URLtype='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 * 获取好友头像的真实URL - 直接使用后端返回的URL
* @param {String} avatarUrl - 后端返回的avatar_url完整URL * @param {String} avatarUrl - 后端返回的avatar_url完整URL
* @returns {Promise<String>} 真实可访问的URL失败返回空字符串让Avatar组件使用默认头像 * @returns {Promise<String>} 真实可访问的URL失败返回空字符串
*/ */
export async function getFriendAvatarRealUrl(avatarUrl) { export async function getFriendAvatarRealUrl(avatarUrl) {
// 如果是本地静态资源或为空,直接返回 // 如果是本地静态资源或为空,直接返回
if (!avatarUrl || avatarUrl.startsWith('/static/')) { if (!avatarUrl || avatarUrl.startsWith('/static/')) {
return avatarUrl || ''; return avatarUrl || '';
} }
try { // 直接使用后端返回的 URL
// 直接将完整URL作为filename传递给预签名API return avatarUrl;
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 '';
}
} }