feat:修改星榜卡顿问题

This commit is contained in:
zerosaturation 2026-06-11 13:56:34 +08:00
parent 2c8ce4d586
commit 3ec096ecd9
3 changed files with 132 additions and 23 deletions

View File

@ -78,6 +78,7 @@
class="card-image"
:src="item.cover_url || item.cover_image || ''"
mode="aspectFill"
lazy-load
/>
<!-- 3 名专属包裹整个卡片的边框图 -->
<image
@ -85,6 +86,7 @@
class="frame-image"
:src="TOP_FRAME_MAP[index]"
mode="scaleToFill"
lazy-load
/>
<!-- 3 名专属左上角奖牌装饰 -->
<image
@ -92,6 +94,7 @@
class="card-medal"
:src="MEDAL_MAP[index]"
mode="aspectFit"
lazy-load
/>
</view>
<view
@ -156,7 +159,10 @@
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { getHotRankingApi } from "@/utils/api.js";
import { getAssetCoverRealUrl } from "@/utils/assetImageHelper.js";
import {
getAssetCoverRealUrl,
getInstantAssetCoverUrl,
} from "@/utils/assetImageHelper.js";
// cover_url / cover_image 访 URL
// 3 /static/... () ( presign) URL ()
@ -325,6 +331,12 @@ const resetAndLoad = () => {
//
// append=false itemsappend=true items
//
// await
// 1) getInstantAssetCoverUrl URL/URL/
// 2) set items.value 线
// 3) getAssetCoverRealUrl URL patch items.value[i].cover_url
// Vue 3 ref reactive
const loadData = async ({ append = false } = {}) => {
const tab = activeTab.value;
if (!tab || typeof tab.fetch !== "function") {
@ -341,30 +353,56 @@ const loadData = async ({ append = false } = {}) => {
try {
const res = await tab.fetch(currentPage.value);
if (res && res.code === 200 && res.data?.items) {
// cover_url 访 URLOSS
const newItems = await Promise.all(
res.data.items.map(async (item) => {
return await resolveItemUrls({
...item,
id: item.id || item.asset_id,
// items
const rawItems = res.data.items.map((it) => {
const id = it.id || it.asset_id;
const rawCover = it.cover_url || it.cover_image || "";
return {
...it,
id,
_rawCover: rawCover, // URL
cover_url: getInstantAssetCoverUrl(rawCover),
};
});
}),
);
//
const baseOffset = append ? items.value.length : 0;
if (append) {
items.value = [...items.value, ...newItems];
items.value = [...items.value, ...rawItems];
} else {
items.value = newItems;
items.value = rawItems;
}
//
// 1) total
// 2)
// URL patch
// await
rawItems.forEach((it, idx) => {
const raw = it._rawCover;
if (!raw) return;
// URL /
const instant = it.cover_url;
if (instant && instant !== PLACEHOLDER_IMAGE && instant === getInstantAssetCoverUrl(raw)) {
// URLgetAssetCoverRealUrl
}
getAssetCoverRealUrl(raw)
.then((realUrl) => {
const targetIdx = baseOffset + idx;
const target = items.value[targetIdx];
// tab / patch
if (target && target.id === it.id && realUrl) {
target.cover_url = realUrl;
}
})
.catch(() => {
/* 单张失败不影响其它图 */
});
});
//
const total = Number(res.data.total ?? 0);
if (total > 0) {
hasMore.value = items.value.length < total;
} else {
hasMore.value = newItems.length >= PAGE_SIZE;
hasMore.value = rawItems.length >= PAGE_SIZE;
}
} else if (!append) {
items.value = [];
@ -379,6 +417,9 @@ const loadData = async ({ append = false } = {}) => {
}
};
// helper DEFAULT_IMAGE
const PLACEHOLDER_IMAGE = "/static/nft/collection.png";
//
const handleScrollToLower = () => {
if (loading.value || loadingMore.value || !hasMore.value) return;
@ -434,7 +475,7 @@ onUnmounted(() => {
inset: 0;
background: linear-gradient(184deg, #ff5a5d -36.55%, #c2ebff 121.2%);
opacity: 0.8;
backdrop-filter: blur(5.85px);
// backdrop-filter: blur(5.85px);
border-top-left-radius: 14px;
border-top-right-radius: 13px;
border-bottom-right-radius: 8px;
@ -467,7 +508,7 @@ onUnmounted(() => {
rgba(194, 235, 255, 0.98) 184.09%
);
opacity: 0.5;
backdrop-filter: blur(0.9px);
// backdrop-filter: blur(0.9px);
border-radius: 20rpx;
}
}
@ -508,7 +549,7 @@ onUnmounted(() => {
rgba(76, 237, 255, 0.2) 48.19%,
rgba(255, 122, 124, 0.2) 83.71%
);
backdrop-filter: blur(9.300000190734863px);
// backdrop-filter: blur(9.300000190734863px);
overflow: hidden;
}
@ -566,7 +607,7 @@ onUnmounted(() => {
rgba(76, 237, 255, 0.2) 48.19%,
rgba(255, 122, 124, 0.2) 83.71%
);
backdrop-filter: blur(4.65px);
// backdrop-filter: blur(4.65px);
border-top-left-radius: 13px;
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
@ -626,14 +667,17 @@ onUnmounted(() => {
.grid-card-top-1 {
background: url("/static/square/galaxy/TOP.png") no-repeat center;
background-size: 100% 100%;
}
.grid-card-top-2 {
background: url("/static/square/galaxy/TOP2.png") no-repeat center;
background-size: 100% 100%;
}
.grid-card-top-3 {
background: url("/static/square/galaxy/TOP3.png") no-repeat center;
background-size: 100% 100%;
}
.grid-card-top-4 {
position: relative;

View File

@ -265,10 +265,10 @@ function handleClick(item) {
/* 可访问性:减少动画 */
/* @media (prefers-reduced-motion: reduce) {
@media (prefers-reduced-motion: reduce) {
.ring-item,
.crown {
animation: none !important;
}
} */
} */ */
</style> -->

View File

@ -1,5 +1,37 @@
import { getOssPresignedUrlApi } from '@/utils/api.js'
/**
* 预签名 URL 内存缓存
* key = 原始 cover_url相对路径 / 完整带 Expires URL
* value = 已签名的完整 URL
*
* 命中规则
* - 命中后再检查一次 isExpiredPresignedUrl过期则丢弃重签
* - 同一进程内页面 reload 会清上限 500 超过随机清一半
*/
const coverUrlCache = new Map()
const COVER_CACHE_LIMIT = 500
function setCoverCache(rawUrl, signedUrl) {
if (!rawUrl || !signedUrl) return
if (coverUrlCache.size >= COVER_CACHE_LIMIT) {
// 简单容量控制:删一半旧的,避免无限增长
const keys = Array.from(coverUrlCache.keys()).slice(0, COVER_CACHE_LIMIT / 2)
keys.forEach(k => coverUrlCache.delete(k))
}
coverUrlCache.set(rawUrl, signedUrl)
}
function getCoverCache(rawUrl) {
const v = coverUrlCache.get(rawUrl)
if (!v) return null
if (isExpiredPresignedUrl(v)) {
coverUrlCache.delete(rawUrl)
return null
}
return v
}
/**
* 从URL提取文件名
* @param {String} url - URL字符串
@ -106,6 +138,32 @@ function extractOssObjectPathFromUrl(url) {
}
}
/**
* 同步获取立即可用的 cover URL绝不发起网络请求
* 用于先用这个 URL 立即渲染列表再异步调 getAssetCoverRealUrl 拿到精确的预签名 URL 并替换
*
* 优先级
* 1) /static/... 或空 兜底占位图
* 2) 命中缓存 缓存内的预签名 URL
* 3) 完整 URL 未过期 原样返回
* 4) 其它相对路径 / 过期 URL 占位图等异步预签名补上
*
* @param {String} coverUrl
* @param {String} [placeholder='/static/nft/collection.png']
* @returns {String}
*/
export function getInstantAssetCoverUrl(coverUrl, placeholder = '/static/nft/collection.png') {
if (!coverUrl || coverUrl.startsWith('/static/')) {
return coverUrl || placeholder
}
const cached = getCoverCache(coverUrl)
if (cached) return cached
if (coverUrl.includes('://') && !isExpiredPresignedUrl(coverUrl)) {
return coverUrl
}
return placeholder
}
/**
* 获取藏品封面的真实URL - 使用 OSS 预签名 URL
* 处理 3 coverUrl 形态
@ -124,6 +182,10 @@ export async function getAssetCoverRealUrl(coverUrl) {
return coverUrl || DEFAULT_IMAGE;
}
// 命中缓存:直接返回(已在 getCoverCache 内做过期检查)
const cached = getCoverCache(coverUrl)
if (cached) return cached
// 2. 完整 URL带 ://
if (coverUrl.includes('://')) {
// 检查是否带 Expires 且已过期 → 提取对象路径重签
@ -132,6 +194,7 @@ export async function getAssetCoverRealUrl(coverUrl) {
try {
const res = await getOssPresignedUrlApi(objectPath, 3600, 'asset')
if (res && res.data && res.data.url) {
setCoverCache(coverUrl, res.data.url)
return res.data.url
}
} catch (e) {
@ -139,7 +202,8 @@ export async function getAssetCoverRealUrl(coverUrl) {
}
return objectPath // 兜底:返回相对路径
}
// 未过期:原样返回
// 未过期:原样返回 + 顺手缓存
setCoverCache(coverUrl, coverUrl)
return coverUrl
}
@ -147,6 +211,7 @@ export async function getAssetCoverRealUrl(coverUrl) {
try {
const res = await getOssPresignedUrlApi(coverUrl, 3600, 'asset');
if (res && res.data && res.data.url) {
setCoverCache(coverUrl, res.data.url)
return res.data.url;
}
return coverUrl;