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;
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,

View File

@ -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 localAvatarUrl = ref('');
//
const fetchOwnAvatarWithCache = async () => {
// 使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(() => {
// OSSURL使
if (ossPresignedUrl.value) {
return ossPresignedUrl.value;
if (localAvatarUrl.value) {
return localAvatarUrl.value;
}
// 使
return getDefaultAvatar();
});

View File

@ -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) {

View File

@ -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 () => {
}
};
// OSSURL
// OSSURL - 使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;
};
//

View File

@ -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' : ''">

View File

@ -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) {

View File

@ -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: '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>

View File

@ -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,9 +211,12 @@ const onScroll = (e) => {
//
const remainingScroll = totalWidth.value - currentScrollLeft - props.screenWidth
if (remainingScroll < Math.max(totalWidth.value / 2, props.screenWidth)) {
// has_more
if (!isLoadingMore && !appendFailed && !isInitialLoading && props.useMockData) {
scheduleAppend()
}
}
}
// ========== ==========
const scrollStyle = computed(() => ({
@ -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++) {
@ -549,6 +549,10 @@ const appendMore = async () => {
if (!isComponentMounted) return
if (res.code === 200 && res.data?.items) {
items = res.data.items
// has_more
if (!res.data.has_more) {
appendFailed = true
}
}
}
@ -570,11 +574,14 @@ const appendMore = async () => {
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) {

View File

@ -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([])
@ -11,21 +11,8 @@ export function useBanner() {
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)

View File

@ -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:

View File

@ -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) {

View File

@ -87,7 +87,6 @@ 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,51 +221,22 @@ 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) => ({
const convertedItems = actionItems.map(item => ({
...item,
icon: await convertImageToPresignedUrl(item.icon)
icon: item.icon
}))
)
return {
bannerImage,

View File

@ -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 = {
@ -104,30 +103,6 @@ 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
@ -163,30 +138,6 @@ 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 {
// 处理昵称已存在的情况

View File

@ -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

View File

@ -1,5 +1,3 @@
import { getOssPresignedUrlApi } from './api.js';
/**
* 从URL提取文件名
* @param {String} url - URL字符串
@ -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获取预签名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
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;
}