925 lines
23 KiB
Vue
925 lines
23 KiB
Vue
<template>
|
||
<view class="hot-category-block">
|
||
<!-- Tab 栏 -->
|
||
<view class="ranking-tabs">
|
||
<view
|
||
v-for="tab in tabs"
|
||
:key="tab.key"
|
||
class="ranking-tab-item"
|
||
:class="{ active: activeTabKey === tab.key }"
|
||
:data-key="tab.key"
|
||
@click="handleTabClick"
|
||
>
|
||
<image
|
||
v-if="tab.icon"
|
||
class="ranking-tab-icon"
|
||
:src="tab.icon"
|
||
mode="aspectFit"
|
||
:style="{
|
||
width: (tab.iconWidth || 32) + 'rpx',
|
||
height: (tab.iconHeight || 40) + 'rpx',
|
||
}"
|
||
/>
|
||
<!-- <text class="ranking-tab-label">{{ tab.label }}</text> -->
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 内容区域: 内部 scroll -->
|
||
<scroll-view
|
||
class="content-scroll"
|
||
scroll-y
|
||
:show-scrollbar="false"
|
||
:bounce="false"
|
||
:lower-threshold="80"
|
||
:scroll-into-view="scrollIntoView"
|
||
:scroll-with-animation="false"
|
||
@scrolltolower="handleScrollToLower"
|
||
>
|
||
<!-- 顶部哨兵:切换 tab 时通过 scroll-into-view 把列表强制滚回顶部 -->
|
||
<view id="grid-top" class="scroll-anchor"></view>
|
||
|
||
<!-- 骨架屏 -->
|
||
<view v-if="loading" class="grid-skeleton">
|
||
<view v-for="i in 11" :key="i" class="skeleton-card">
|
||
<view class="skeleton-image"></view>
|
||
<view class="skeleton-info">
|
||
<view class="skeleton-avatar"></view>
|
||
<view class="skeleton-name"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 内容网格 -->
|
||
<view v-else class="items-grid">
|
||
<view
|
||
v-for="(item, index) in items"
|
||
:key="item.id || index"
|
||
class="grid-card"
|
||
:class="{
|
||
[`grid-card-top-${index + 1}`]: index < 5,
|
||
'grid-card-top-other': index >= 5,
|
||
}"
|
||
@click="handleCardClick(item)"
|
||
>
|
||
<!-- 点赞动效波纹 -->
|
||
<view
|
||
class="wf-like-wave wf-like-wave-outer"
|
||
:class="{ 'wf-like-wave-active': likingMap[item.id] }"
|
||
/>
|
||
<view
|
||
class="wf-like-wave wf-like-wave-inner"
|
||
:class="{ 'wf-like-wave-active': likingMap[item.id] }"
|
||
/>
|
||
|
||
<!-- 单行布局:藏品图片 + 头像 + 点赞数 + TOP 标签 -->
|
||
<view class="card-row">
|
||
<view class="card-image-wrap">
|
||
<image
|
||
class="card-image"
|
||
:src="item.cover_url || item.cover_image || ''"
|
||
mode="aspectFill"
|
||
lazy-load
|
||
/>
|
||
<!-- 前 3 名专属:包裹整个卡片的边框图 -->
|
||
<image
|
||
v-if="index < 3"
|
||
class="frame-image"
|
||
:src="TOP_FRAME_MAP[index]"
|
||
mode="scaleToFill"
|
||
lazy-load
|
||
/>
|
||
<!-- 前 3 名专属:左上角奖牌装饰 -->
|
||
<!-- <image
|
||
v-if="index < 3"
|
||
class="card-medal"
|
||
:src="MEDAL_MAP[index]"
|
||
mode="aspectFit"
|
||
lazy-load
|
||
/> -->
|
||
</view>
|
||
<view
|
||
class="like-info"
|
||
:class="{ [`like-info-top-${index + 1}`]: index < 3 }"
|
||
>
|
||
<view class="like-row">
|
||
<image
|
||
class="like-icon"
|
||
:src="
|
||
item.is_liked
|
||
? '/static/icon/heart-icon.png'
|
||
: '/static/icon/heart-icon-false.png'
|
||
"
|
||
mode="aspectFit"
|
||
/>
|
||
<text class="like-count">{{
|
||
formatCount(item.like_count)
|
||
}}</text>
|
||
</view>
|
||
<text class="user-name">{{
|
||
item.owner_nickname || item.creator_name || item.name || ""
|
||
}}</text>
|
||
<text class="user-number">No.{{ item.owner_uid || "" }}</text>
|
||
</view>
|
||
<view v-if="index >= 3" class="top-badge">
|
||
<view class="badge-rank">
|
||
<image
|
||
class="badge-rank-icon"
|
||
src="/static/square/top/top.png"
|
||
mode="aspectFit"
|
||
/>
|
||
<text class="badge-rank-number">{{ index + 1 }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 分页底部状态:加载中 / 没有更多了 / 暂无数据 -->
|
||
<view v-if="loadingMore" class="load-more-tip load-more-tip-loading">
|
||
<text class="load-more-text">加载中...</text>
|
||
</view>
|
||
<view v-else-if="!hasMore && items.length > 0" class="load-more-tip">
|
||
<text class="load-more-text">— 没有更多了 —</text>
|
||
</view>
|
||
<view v-else-if="!loading && items.length === 0" class="load-more-tip">
|
||
<text class="load-more-text">暂无数据</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
|
||
import { onShow } from "@dcloudio/uni-app";
|
||
import { getHotRankingApi } from "@/utils/api.js";
|
||
import {
|
||
getAssetCoverRealUrl,
|
||
getInstantAssetCoverUrl,
|
||
} from "@/utils/assetImageHelper.js";
|
||
|
||
// 把后端返回的 cover_url / cover_image 转成真实可访问的 URL
|
||
// 处理 3 种形态:/static/... (本地)、相对路径 (需 presign)、完整 URL (可能过期)
|
||
async function resolveItemUrls(item) {
|
||
if (!item) return item;
|
||
const cover = item.cover_url || item.cover_image || "";
|
||
if (cover) {
|
||
item.cover_url = await getAssetCoverRealUrl(cover);
|
||
}
|
||
return item;
|
||
}
|
||
|
||
const emit = defineEmits(["cardClick"]);
|
||
|
||
const items = ref([]);
|
||
const loading = ref(false);
|
||
const likingMap = ref({});
|
||
const activeTabKey = ref("");
|
||
|
||
// ===== 分页状态 =====
|
||
// currentPage : 已加载到的页码(从 1 开始)
|
||
// hasMore : 是否还有下一页(接口返回数量 < PAGE_SIZE 或累计已 >= total 时置 false)
|
||
// loadingMore : 是否正在加载下一页(防止 scrolltolower 重复触发)
|
||
const currentPage = ref(1);
|
||
const hasMore = ref(true);
|
||
const loadingMore = ref(false);
|
||
|
||
// scroll-into-view 目标元素 id —— 切换 tab 时设为 'grid-top' 把 scroll-view 滚回顶部
|
||
// 通过先置空 → nextTick 设回目标 id 来强制触发(即使上一次已经是同一个 id 也能生效)
|
||
const scrollIntoView = ref("");
|
||
|
||
// 每页数量
|
||
const PAGE_SIZE = 10;
|
||
|
||
// Tab 配置(直接写死在组件内)
|
||
// 新增 tab 在这里 push 一项即可:{ key, label, icon, iconWidth, iconHeight, fetch }
|
||
// fetch 接受 page 参数,由组件内部分页逻辑统一传入。
|
||
const tabs = [
|
||
{
|
||
key: "hot",
|
||
label: "点赞榜",
|
||
icon: "/static/square/galaxy/dianzanbang.png",
|
||
iconWidth: 96,
|
||
iconHeight: 88,
|
||
fetch: (page) => getHotRankingApi("displaying", null, page, PAGE_SIZE),
|
||
},
|
||
{
|
||
key: "new",
|
||
label: "活跃榜",
|
||
icon: "/static/square/galaxy/huoyuebang.png",
|
||
iconWidth: 96,
|
||
iconHeight: 88,
|
||
fetch: (page) => getHotRankingApi("displaying", null, page, PAGE_SIZE),
|
||
},
|
||
{
|
||
key: "trending",
|
||
label: "曝光榜",
|
||
icon: "/static/square/galaxy/baoguangbang.png",
|
||
iconWidth: 96,
|
||
iconHeight: 88,
|
||
fetch: (page) => getHotRankingApi("displaying", null, page, PAGE_SIZE),
|
||
},
|
||
{
|
||
key: "tongcheng",
|
||
label: "同城榜",
|
||
icon: "/static/square/galaxy/tongchengbang.png",
|
||
iconWidth: 96,
|
||
iconHeight: 88,
|
||
fetch: (page) => getHotRankingApi("displaying", null, page, PAGE_SIZE),
|
||
},
|
||
];
|
||
|
||
// 前 3 名奖牌图标
|
||
const MEDAL_MAP = {
|
||
0: "/static/square/top/TOP1icon.png",
|
||
1: "/static/square/top/TOP2icon.png",
|
||
2: "/static/square/top/TOP3icon.png",
|
||
};
|
||
|
||
// 前 3 名对应的边框图
|
||
const TOP_FRAME_MAP = {
|
||
0: "/static/square/top/TOP1biankuang1.png",
|
||
1: "/static/square/top/TOP2biankuang2.png",
|
||
2: "/static/square/top/TOP3biankuangpng3.png",
|
||
};
|
||
|
||
const activeTab = computed(
|
||
() => tabs.find((t) => t.key === activeTabKey.value) || tabs[0],
|
||
);
|
||
|
||
// 初始化 activeTabKey:默认选第一个 tab
|
||
watch(
|
||
() => tabs,
|
||
(newTabs) => {
|
||
if (
|
||
newTabs.length > 0 &&
|
||
!newTabs.some((t) => t.key === activeTabKey.value)
|
||
) {
|
||
activeTabKey.value = newTabs[0].key;
|
||
}
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
// 切换 tab
|
||
const handleTabClick = (e) => {
|
||
const key = e.currentTarget.dataset.key;
|
||
if (key && key !== activeTabKey.value) {
|
||
activeTabKey.value = key;
|
||
}
|
||
};
|
||
|
||
// activeTab 变化时重新加载数据(重置分页)
|
||
watch(activeTab, () => {
|
||
resetAndLoad();
|
||
});
|
||
|
||
// 格式化数量
|
||
const formatCount = (count) => {
|
||
if (!count) return "0";
|
||
if (count >= 10000) return (count / 10000).toFixed(1) + "w";
|
||
if (count >= 1000) return (count / 1000).toFixed(1) + "k";
|
||
return count.toString();
|
||
};
|
||
|
||
const handleCardClick = (item) => {
|
||
emit("cardClick", item);
|
||
};
|
||
|
||
// 监听全局点赞事件,更新状态
|
||
const onAssetLiked = ({ asset_id, data }) => {
|
||
const index = items.value.findIndex(
|
||
(item) => (item.asset_id || item.id) === asset_id,
|
||
);
|
||
if (index !== -1) {
|
||
const updatedItems = [...items.value];
|
||
updatedItems[index] = {
|
||
...updatedItems[index],
|
||
is_liked: data?.is_liked ?? true,
|
||
like_count:
|
||
data?.new_like_count ?? (updatedItems[index].like_count || 0) + 1,
|
||
};
|
||
items.value = updatedItems;
|
||
// 触发动画
|
||
likingMap.value = { ...likingMap.value, [asset_id]: true };
|
||
setTimeout(() => {
|
||
likingMap.value = { ...likingMap.value, [asset_id]: false };
|
||
}, 600);
|
||
}
|
||
};
|
||
|
||
// 重置分页并重新加载第 1 页
|
||
// 切换 tab 时调用:清空 items、把 currentPage 拉回 1、hasMore 设回 true、滚动到顶。
|
||
const resetAndLoad = () => {
|
||
currentPage.value = 1;
|
||
hasMore.value = true;
|
||
loadingMore.value = false;
|
||
items.value = [];
|
||
// 强制滚回顶部:先清空再设回 id,避免上一次值就是 'grid-top' 时不触发
|
||
scrollIntoView.value = "";
|
||
nextTick(() => {
|
||
scrollIntoView.value = "grid-top";
|
||
});
|
||
loadData({ append: false });
|
||
};
|
||
|
||
// 加载数据
|
||
// append=false:首页加载,覆盖 items;append=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") {
|
||
console.warn("[HotCategoryBlock] 当前 tab 未配置 fetch:", tab);
|
||
if (!append) items.value = [];
|
||
return;
|
||
}
|
||
// 首页用 loading 整屏骨架;分页用 loadingMore 底部小指示器
|
||
if (append) {
|
||
loadingMore.value = true;
|
||
} else {
|
||
loading.value = true;
|
||
}
|
||
try {
|
||
const res = await tab.fetch(currentPage.value);
|
||
if (res && res.code === 0 && res.data?.items) {
|
||
// ① 立刻准备好「能马上渲染的」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, ...rawItems];
|
||
} else {
|
||
items.value = rawItems;
|
||
}
|
||
|
||
// ③ 后台并行解析预签名 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)
|
||
) {
|
||
// 已是精确 URL;仍然异步校准一次以处理过期(getAssetCoverRealUrl 内部命中缓存就是同步快速返回)
|
||
}
|
||
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 = rawItems.length >= PAGE_SIZE;
|
||
}
|
||
} else if (!append) {
|
||
items.value = [];
|
||
hasMore.value = false;
|
||
}
|
||
} catch (e) {
|
||
console.error("[HotCategoryBlock] 加载数据失败", e?.message ?? e);
|
||
if (!append) items.value = [];
|
||
} finally {
|
||
loading.value = false;
|
||
loadingMore.value = false;
|
||
}
|
||
};
|
||
|
||
// 占位图常量(与 helper 内部 DEFAULT_IMAGE 一致)
|
||
const PLACEHOLDER_IMAGE = "/static/nft/collection.png";
|
||
|
||
// 滚动到底部触发加载下一页
|
||
const handleScrollToLower = () => {
|
||
if (loading.value || loadingMore.value || !hasMore.value) return;
|
||
currentPage.value += 1;
|
||
loadData({ append: true });
|
||
};
|
||
|
||
onMounted(() => {
|
||
uni.$on("assetLiked", onAssetLiked);
|
||
loadData();
|
||
});
|
||
|
||
onShow(() => {
|
||
// loadData();
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
uni.$off("assetLiked", onAssetLiked);
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.hot-category-block {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
// padding: 0 9.5rpx;
|
||
border-radius: 24rpx;
|
||
position: relative;
|
||
background: url("/static/square/galaxy/xbbj.png") center no-repeat;
|
||
background-size: 100% 100%;
|
||
}
|
||
|
||
.content-scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.ranking-tabs {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0 64rpx;
|
||
position: relative;
|
||
top: 16rpx;
|
||
margin: 0 24rpx;
|
||
// width: 480rpx;
|
||
height: 96rpx;
|
||
// clip-path: inset(-200rpx 0 16rpx 0);
|
||
&::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(
|
||
184deg,
|
||
rgba(255, 90, 93, 0.47) -36.55%,
|
||
rgba(194, 235, 255, 0.47) 121.2%
|
||
);
|
||
filter: blur(2.5px);
|
||
// opacity: 0.8;
|
||
// Figma 用的就是 filter: blur(图形模糊),不是 backdrop-filter(背景模糊)
|
||
// filter: blur(3.7px);
|
||
-webkit-filter: blur(2.5px);
|
||
border-top-left-radius: 14px;
|
||
border-top-right-radius: 13px;
|
||
border-bottom-right-radius: 8px;
|
||
border-bottom-left-radius: 7px;
|
||
}
|
||
}
|
||
|
||
.ranking-tab-item {
|
||
height: 80rpx;
|
||
width: 99.2rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
flex-direction: column;
|
||
transition: all 0.25s ease;
|
||
position: relative;
|
||
}
|
||
|
||
.ranking-tab-item.active {
|
||
width: 99.2rpx;
|
||
height: 160rpx;
|
||
top: 40rpx;
|
||
z-index: 1;
|
||
&::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(
|
||
190.37deg,
|
||
rgba(255, 90, 140, 0.98) 5.5378%,
|
||
rgba(194, 235, 255, 0.98) 184.09%
|
||
);
|
||
opacity: 0.5;
|
||
// backdrop-filter: blur(0.9px);
|
||
border-radius: 20rpx;
|
||
filter: blur(1.3px);
|
||
}
|
||
}
|
||
|
||
.ranking-tab-item.active .ranking-tab-icon {
|
||
top: -8rpx;
|
||
height: 96rpx !important;
|
||
}
|
||
|
||
.ranking-tab-label {
|
||
font-size: 22rpx;
|
||
font-weight: 600;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.4);
|
||
line-height: 1;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.ranking-tab-icon {
|
||
display: block;
|
||
position: absolute;
|
||
}
|
||
|
||
.ranking-tab-item.active .ranking-tab-label {
|
||
color: #ffffff;
|
||
}
|
||
|
||
/* 骨架屏 */
|
||
.grid-skeleton {
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
z-index: 2;
|
||
border-radius: 12px;
|
||
|
||
background: linear-gradient(
|
||
161.28deg,
|
||
rgba(255, 90, 93, 0.2) 16.63%,
|
||
rgba(76, 237, 255, 0.2) 48.19%,
|
||
rgba(255, 122, 124, 0.2) 83.71%
|
||
);
|
||
// backdrop-filter: blur(9.300000190734863px);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.skeleton-card {
|
||
width: 100%;
|
||
height: 120rpx; /* 与新卡片 row 高度一致 */
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12rpx 16rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.skeleton-avatar {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
border-radius: 50%;
|
||
background: #3a3a4a;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.skeleton-name {
|
||
flex: 1;
|
||
height: 28rpx;
|
||
background: #3a3a4a;
|
||
border-radius: 8rpx;
|
||
margin-left: 16rpx;
|
||
}
|
||
|
||
.skeleton-image,
|
||
.skeleton-info {
|
||
display: none; /* 新布局下不再使用 */
|
||
}
|
||
|
||
@keyframes shimmer {
|
||
0% {
|
||
background-position: 200% 0;
|
||
}
|
||
|
||
100% {
|
||
background-position: -200% 0;
|
||
}
|
||
}
|
||
|
||
/* 内容网格 */
|
||
.items-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
z-index: 2;
|
||
// background: linear-gradient(
|
||
// 145.83deg,
|
||
// rgba(255, 90, 93, 0.2) 16.63%,
|
||
// rgba(76, 237, 255, 0.2) 48.19%,
|
||
// rgba(255, 122, 124, 0.2) 83.71%
|
||
// );
|
||
|
||
// pointer-events: none;
|
||
// backdrop-filter: blur(4.65px);
|
||
border-top-left-radius: 13px;
|
||
border-top-right-radius: 12px;
|
||
border-bottom-right-radius: 12px;
|
||
border-bottom-left-radius: 12px;
|
||
// opacity: 0.8;
|
||
padding: 40rpx 20rpx 32rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.grid-card {
|
||
width: 100%;
|
||
border-radius: 16rpx;
|
||
position: relative;
|
||
/* background: rgba(255, 255, 255, 0.15); */
|
||
/* box-shadow: 2px 2px 4.5px 0px #f04b4b40; */
|
||
box-shadow: 2px 4px 4px 0px #c92f2f5c;
|
||
margin-bottom: 36.8rpx;
|
||
}
|
||
|
||
/* 单行布局:藏品图片 + 头像 + 点赞信息 + TOP 标签 */
|
||
.card-row {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 16rpx;
|
||
width: 100%;
|
||
height: 104rpx;
|
||
box-sizing: border-box;
|
||
position: relative;
|
||
}
|
||
|
||
.card-image-wrap {
|
||
position: relative;
|
||
width: 90rpx;
|
||
height: 120rpx;
|
||
}
|
||
.card-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 12rpx;
|
||
display: block;
|
||
position: absolute;
|
||
top: -16rpx;
|
||
left: 32rpx;
|
||
}
|
||
|
||
/* 前 3 名专属:左上角奖牌装饰 */
|
||
.card-medal {
|
||
position: absolute;
|
||
top: -32rpx;
|
||
left: 96rpx;
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
z-index: 5;
|
||
pointer-events: none;
|
||
transform: rotate(45deg);
|
||
}
|
||
|
||
.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;
|
||
// overflow: hidden;
|
||
// [方案3] 伪元素承载 bj.png,对图片单独设 opacity
|
||
&::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: url("/static/square/galaxy/TOP4.png") center no-repeat;
|
||
background-size: 100% 100%;
|
||
opacity: 0.8; // ⬅ 调这个数控制图片透明度(0=完全透明,1=完全不透明)
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
}
|
||
.grid-card-top-5 {
|
||
position: relative;
|
||
// overflow: hidden;
|
||
&::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: url("/static/square/galaxy/TOP4.png") center no-repeat;
|
||
background-size: 100% 100%;
|
||
opacity: 0.8;
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
}
|
||
|
||
.grid-card-top-other {
|
||
position: relative;
|
||
// overflow: hidden;
|
||
|
||
&::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: url("/static/square/galaxy/TOP6.png") center no-repeat;
|
||
background-size: 100% 100%;
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
/* 前 3 名专属:包裹藏品图的边框图(叠加在 card-image 之上) */
|
||
.frame-image {
|
||
position: absolute;
|
||
top: -16rpx;
|
||
left: 32rpx;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 4;
|
||
pointer-events: none;
|
||
display: block;
|
||
}
|
||
|
||
.like-info {
|
||
display: flex;
|
||
flex-direction: column; /* 从上往下:用户名 → 编号 → 点赞 */
|
||
justify-content: center;
|
||
margin-left: 64rpx; /* 距头像 10rpx */
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.user-name {
|
||
font-size: 20rpx;
|
||
font-weight: 800;
|
||
color: #fffabd;
|
||
text-shadow: -1px 1px 4px #ce0909d6;
|
||
white-space: nowrap;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.user-number {
|
||
font-size: 20rpx;
|
||
font-weight: 800;
|
||
line-height: 1.3;
|
||
color: #fffabd;
|
||
text-shadow: -1px 1px 4px #ce0909d6;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.like-row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 6rpx;
|
||
}
|
||
|
||
.like-info .like-icon {
|
||
width: 24rpx;
|
||
height: 24rpx;
|
||
margin-right: 4rpx;
|
||
}
|
||
|
||
.like-info .like-count {
|
||
font-size: 28rpx;
|
||
font-weight: 400;
|
||
color: #fffabd;
|
||
text-shadow: -1px 1px 4px #ce0909d6;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Top 排名标签(顶到右边) */
|
||
.top-badge {
|
||
margin-left: auto; /* 推到右侧 */
|
||
margin-right: 16rpx;
|
||
min-width: 100rpx;
|
||
height: 36rpx;
|
||
border-radius: 18rpx;
|
||
padding: 0 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.badge-rank {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: center;
|
||
}
|
||
|
||
.badge-rank-icon {
|
||
width: 90.24rpx;
|
||
height: 40rpx;
|
||
margin-right: 5px;
|
||
}
|
||
|
||
.badge-rank-number {
|
||
font-family: "Abyssinica SIL";
|
||
font-size: 84rpx;
|
||
font-weight: 600;
|
||
/* 渐变填充到文字 */
|
||
background: linear-gradient(
|
||
181.98deg,
|
||
#fcfcf8 23.78%,
|
||
#ffb3eb 66.88%,
|
||
#ffeded 94.93%
|
||
);
|
||
-webkit-background-clip: text;
|
||
background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
color: transparent;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 点赞动效波纹 */
|
||
.wf-like-wave {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
pointer-events: none;
|
||
opacity: 0;
|
||
z-index: 1;
|
||
}
|
||
|
||
.wf-like-wave-outer {
|
||
background: radial-gradient(
|
||
circle,
|
||
rgba(255, 107, 107, 0.8) 0%,
|
||
transparent 70%
|
||
);
|
||
}
|
||
|
||
.wf-like-wave-inner {
|
||
background: radial-gradient(
|
||
circle,
|
||
rgba(255, 184, 0, 0.6) 0%,
|
||
transparent 70%
|
||
);
|
||
}
|
||
|
||
.wf-like-wave-active {
|
||
animation: likeWave 0.6s ease-out forwards;
|
||
}
|
||
|
||
@keyframes likeWave {
|
||
0% {
|
||
opacity: 0.9;
|
||
transform: scale(0.8);
|
||
}
|
||
|
||
100% {
|
||
opacity: 0;
|
||
transform: scale(1.5);
|
||
}
|
||
}
|
||
|
||
/* 分页底部加载提示 */
|
||
.load-more-tip {
|
||
width: 100%;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 16rpx 0 8rpx;
|
||
position: relative;
|
||
z-index: 3;
|
||
}
|
||
|
||
/* 滚动哨兵:仅作为 scroll-into-view 的锚点,不占据视觉空间 */
|
||
.scroll-anchor {
|
||
width: 100%;
|
||
height: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.load-more-text {
|
||
font-size: 22rpx;
|
||
color: #fffabd;
|
||
text-shadow: -1px 1px 4px #ce0909d6;
|
||
opacity: 0.85;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
|
||
.load-more-tip-loading .load-more-text {
|
||
animation: loadMorePulse 1s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes loadMorePulse {
|
||
0%,
|
||
100% {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
50% {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
</style>
|