topfans/frontend/pages/square/components/HotCategoryBlock.vue

636 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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 < 3,
[`grid-card-top-${index + 1}`]: index < 3,
}"
@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/rementubiao.png",
iconWidth: 32,
iconHeight: 40,
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 转换成真实可访问 URLOSS 预签名前端实现)
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>
.hot-category-block {
padding: 0 9.5rpx;
border-radius: 24rpx;
position: relative;
}
/* Tab 栏 */
.ranking-tabs {
display: flex;
flex-direction: row;
align-items: center;
padding: 0 16rpx;
position: relative;
left: 50%;
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: 80rpx;
background: linear-gradient(183.58deg, #ff5a5d -36.55%, #c2ebff 121.2%);
backdrop-filter: blur(11.699999809265137px);
}
.ranking-tab-item {
height: 80rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
flex-direction: column;
transition: all 0.25s ease;
position: absolute;
top: 24rpx;
}
.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;
width: 88rpx;
}
.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;
top: -8rpx;
}
.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);
}
/* 前 3 名专属:卡片整体突出 */
.grid-card-top {
}
.grid-card-top-1 {
}
.grid-card-top-2 {
}
.grid-card-top-3 {
}
/* 前 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>