705 lines
16 KiB
Vue
705 lines
16 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>
|
||
|
||
<!-- 骨架屏 -->
|
||
<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"
|
||
/>
|
||
<!-- 前 3 名专属:包裹整个卡片的边框图 -->
|
||
<image
|
||
v-if="index < 3"
|
||
class="frame-image"
|
||
:src="TOP_FRAME_MAP[index]"
|
||
mode="scaleToFill"
|
||
/>
|
||
<!-- 前 3 名专属:左上角奖牌装饰 -->
|
||
<image
|
||
v-if="index < 3"
|
||
class="card-medal"
|
||
:src="MEDAL_MAP[index]"
|
||
mode="aspectFit"
|
||
/>
|
||
</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>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
||
import { onShow } from "@dcloudio/uni-app";
|
||
import { getHotRankingApi } from "@/utils/api.js";
|
||
import { getAssetCoverRealUrl } 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("");
|
||
|
||
// Tab 配置(直接写死在组件内)
|
||
// 新增 tab 在这里 push 一项即可:{ key, label, icon, iconWidth, iconHeight, fetch }
|
||
const tabs = [
|
||
{
|
||
key: "hot",
|
||
label: "热度榜",
|
||
icon: "/static/square/galaxy/dianzanbang.png",
|
||
iconWidth: 64,
|
||
iconHeight: 72,
|
||
fetch: () => getHotRankingApi("displaying", null, 1, 11),
|
||
},
|
||
{
|
||
key: "new",
|
||
label: "活跃榜",
|
||
icon: "/static/square/galaxy/huoyuebang.png",
|
||
iconWidth: 64,
|
||
iconHeight: 72,
|
||
fetch: () => getHotRankingApi("displaying", null, 1, 11),
|
||
},
|
||
{
|
||
key: "trending",
|
||
label: "曝光榜",
|
||
icon: "/static/square/galaxy/baoguangbang.png",
|
||
iconWidth: 64,
|
||
iconHeight: 72,
|
||
fetch: () => getHotRankingApi("displaying", null, 1, 11),
|
||
},
|
||
{
|
||
key: "trending",
|
||
label: "同城榜",
|
||
icon: "/static/square/galaxy/tongchengbang.png",
|
||
iconWidth: 64,
|
||
iconHeight: 72,
|
||
fetch: () => getHotRankingApi("displaying", null, 1, 11),
|
||
},
|
||
];
|
||
|
||
// 前 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, () => {
|
||
loadData();
|
||
});
|
||
|
||
// 格式化数量
|
||
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);
|
||
}
|
||
};
|
||
|
||
// 加载数据
|
||
const loadData = async () => {
|
||
const tab = activeTab.value;
|
||
if (!tab || typeof tab.fetch !== "function") {
|
||
console.warn("[HotCategoryBlock] 当前 tab 未配置 fetch:", tab);
|
||
items.value = [];
|
||
return;
|
||
}
|
||
loading.value = true;
|
||
try {
|
||
const res = await tab.fetch();
|
||
if (res && res.code === 200 && res.data?.items) {
|
||
// 逐个把 cover_url 转换成真实可访问 URL(OSS 预签名前端实现)
|
||
items.value = await Promise.all(
|
||
res.data.items.map(async (item) => {
|
||
return await resolveItemUrls({
|
||
...item,
|
||
id: item.id || item.asset_id,
|
||
});
|
||
}),
|
||
);
|
||
} else {
|
||
items.value = [];
|
||
}
|
||
} catch (e) {
|
||
console.error("[HotCategoryBlock] 加载数据失败", e?.message ?? e);
|
||
items.value = [];
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
uni.$on("assetLiked", onAssetLiked);
|
||
loadData();
|
||
});
|
||
|
||
onShow(() => {
|
||
// loadData();
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
uni.$off("assetLiked", onAssetLiked);
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.hot-category-block {
|
||
padding: 0 9.5rpx;
|
||
border-radius: 24rpx;
|
||
position: relative;
|
||
}
|
||
|
||
/* Tab 栏 */
|
||
.ranking-tabs {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0 16rpx;
|
||
position: relative;
|
||
left: 50%;
|
||
top: 16rpx;
|
||
transform: translate(-50%);
|
||
opacity: 0.8;
|
||
border-top-left-radius: 14px;
|
||
border-top-right-radius: 13px;
|
||
border-bottom-right-radius: 8px;
|
||
border-bottom-left-radius: 7px;
|
||
z-index: 1; /* 在内容网格之下 */
|
||
width: 480rpx;
|
||
height: 96rpx;
|
||
background: linear-gradient(183.58deg, #ff5a5d -36.55%, #c2ebff 121.2%);
|
||
backdrop-filter: blur(11.699999809265137px);
|
||
}
|
||
|
||
.ranking-tab-item {
|
||
height: 80rpx;
|
||
width: 88rpx;
|
||
border-radius: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
flex-direction: column;
|
||
transition: all 0.25s ease;
|
||
/* 保持 position: relative,让 .ranking-tab-icon 的 position: absolute 能以本元素为定位基准。
|
||
注意:不能写 position: absolute,否则 4 个 tab 会从 flex 流中抽离并全部堆在 left:0 重叠。 */
|
||
position: relative;
|
||
}
|
||
|
||
.ranking-tab-item.active {
|
||
background: linear-gradient(
|
||
185.9deg,
|
||
rgba(255, 90, 140, 0.98) 5.54%,
|
||
rgba(194, 235, 255, 0.98) 184.09%
|
||
);
|
||
backdrop-filter: blur(1.7999999523162842px);
|
||
height: 144rpx;
|
||
top: 40rpx;
|
||
}
|
||
|
||
.ranking-tab-item.active .ranking-tab-icon {
|
||
top: -24rpx;
|
||
}
|
||
|
||
.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; /* 与 items-grid 保持一致:纵向 */
|
||
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; /* 在 tab 栏之上 */
|
||
/* 背景与父容器一致,看上去与 tab 栏融成一体 */
|
||
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);
|
||
border-radius: 12px;
|
||
padding: 40rpx 20rpx 0;
|
||
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: 48rpx;
|
||
}
|
||
|
||
/* 单行布局:藏品图片 + 头像 + 点赞信息 + TOP 标签 */
|
||
.card-row {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 16rpx;
|
||
width: 100%;
|
||
height: 104rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.grid-card-top-2 {
|
||
background: url("/static/square/galaxy/TOP2.png") no-repeat center;
|
||
}
|
||
|
||
.grid-card-top-3 {
|
||
background: url("/static/square/galaxy/TOP3.png") no-repeat center;
|
||
}
|
||
.grid-card-top-4 {
|
||
position: relative;
|
||
overflow: hidden;
|
||
// [方案3] 伪元素承载 bj.png,对图片单独设 opacity
|
||
&::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: url("/static/dashboard/bj.png") center / cover no-repeat;
|
||
opacity: 0.8; // ⬅ 调这个数控制图片透明度(0=完全透明,1=完全不透明)
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
}
|
||
.grid-card-top-5 {
|
||
position: relative;
|
||
overflow: hidden;
|
||
// [方案3] 伪元素承载 bj.png,对图片单独设 opacity
|
||
&::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: url("/static/dashboard/bj.png") center / cover no-repeat;
|
||
opacity: 0.8; // ⬅ 调这个数控制图片透明度(0=完全透明,1=完全不透明)
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
}
|
||
|
||
.grid-card-top-other {
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
&::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: url("/static/square/top/bj.png") center / cover no-repeat;
|
||
opacity: 0.26;
|
||
}
|
||
}
|
||
|
||
/* 前 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; /* 推到右侧 */
|
||
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-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);
|
||
}
|
||
}
|
||
</style>
|