topfans/frontend/pages/components/StarbookContent.vue
2026-04-24 18:04:55 +08:00

645 lines
15 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="starbook-content">
<!-- 背景图片 -->
<image class="background-image" src="/static/background/starbook.jpg" mode="aspectFill"></image>
<!-- 内容区域 -->
<view class="content-wrapper">
<!-- 类型Tab - 固定定位 -->
<view class="type-tabs">
<view
class="tab-item"
:class="{ active: currentType === 'regular' }"
@click="switchType('regular')"
>
<text>原创</text>
</view>
<view
class="tab-item"
:class="{ active: currentType === 'collection' }"
@click="switchType('collection')"
>
<text>典藏</text>
</view>
<view
class="tab-item"
:class="{ active: currentType === 'activity' }"
@click="switchType('activity')"
>
<text>活动</text>
</view>
</view>
<!-- 加载中 -->
<view v-if="loading" class="loading-container">
<text class="loading-text">加载中...</text>
</view>
<!-- 空状态 -->
<view v-else-if="!hasData" class="empty-container">
<text class="empty-text">暂无藏品</text>
</view>
<!-- 藏品列表 - 可滚动区域 -->
<scroll-view v-else class="nft-scroll-view" scroll-y :show-scrollbar="false">
<view class="nft-list-container">
<!-- 原创藏品:按 category > grade 分组 -->
<view v-if="currentType === 'regular'">
<view v-for="(group, gIndex) in regularGroups" :key="group.category" class="nft-group">
<!-- 分组标题category_name -->
<!-- <view class="group-header">
<text class="group-title">{{ group.category_name }}</text>
</view> -->
<!-- 该 category 下的所有 grades -->
<view v-for="gradeItem in group.grades" :key="gradeItem.grade" class="grade-section">
<view class="group-header">
<text class="group-title">{{ formatGrade(gradeItem.grade) }}</text>
</view>
<scroll-view class="nft-row" scroll-x :show-scrollbar="false" :enable-flex="true">
<view class="nft-row-content">
<view
v-for="item in gradeItem.items"
:key="item.asset_id"
class="nft-grid-item"
@click="handleCardClick(item)"
@touchstart.stop
>
<image
class="nft-image"
:class="{ 'nft-image-displayed': item.display_status === 1 }"
:src="item.coverUrl"
mode="aspectFill"
/>
<view v-if="item.display_status === 1" class="status-overlay">
<text class="status-text-center">已展示</text>
</view>
<view class="nft-info">
<text class="nft-name">{{ item.name }}</text>
</view>
</view>
<!-- 更多按钮 -->
<view v-if="gradeItem.has_more" class="nft-grid-item more-item" @click="goToMore(group, gradeItem.grade)" @touchstart.stop>
<!-- <view class="nft-image more-placeholder"></view> -->
<view class="more-overlay">
<text class="more-text">更多></text>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
<!-- 典藏藏品:按 category 分组 -->
<view v-if="currentType === 'collection'">
<view v-for="group in collectionGroups" :key="group.category" class="nft-group">
<!-- 分组标题 -->
<!-- <view class="group-header">
<text class="group-title">{{ group.category_name }}</text>
</view> -->
<!-- 分组内容 -->
<scroll-view class="nft-row" scroll-x :show-scrollbar="false" :enable-flex="true">
<view class="nft-row-content">
<view
v-for="(item, index) in group.items"
:key="item.asset_id"
class="nft-grid-item"
@click="handleCardClick(item)"
@touchstart.stop
>
<image
class="nft-image"
:class="{ 'nft-image-displayed': item.display_status === 1 }"
:src="item.coverUrl"
mode="aspectFill"
/>
<view v-if="item.display_status === 1" class="status-overlay">
<text class="status-text-center">已展示</text>
</view>
<view class="nft-info">
<text class="nft-name">{{ item.name }}</text>
<text class="nft-likes">★{{ item.like_count }}</text>
</view>
</view>
<!-- 更多按钮 -->
<view v-if="group.has_more" class="nft-grid-item more-item" @click="goToMore(group)" @touchstart.stop>
<!-- <view class="nft-image more-placeholder"></view> -->
<view class="more-overlay">
<text class="more-text">更多></text>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 活动藏品:按 activity_type 分组 -->
<view v-if="currentType === 'activity'">
<view v-for="group in activityGroups" :key="group.category" class="nft-group">
<!-- 分组标题 -->
<!-- <view class="group-header">
<text class="group-title">{{ group.category_name }}</text>
</view> -->
<!-- 分组内容 -->
<scroll-view class="nft-row" scroll-x :show-scrollbar="false" :enable-flex="true">
<view class="nft-row-content">
<view
v-for="(item, index) in group.items"
:key="item.asset_id"
class="nft-grid-item"
@click="handleCardClick(item)"
@touchstart.stop
>
<image
class="nft-image"
:class="{ 'nft-image-displayed': item.display_status === 1 }"
:src="item.coverUrl"
mode="aspectFill"
/>
<view v-if="item.display_status === 1" class="status-overlay">
<text class="status-text-center">已展示</text>
</view>
<view class="nft-info">
<text class="nft-name">{{ item.name }}</text>
<text class="nft-likes">★{{ item.like_count }}</text>
</view>
</view>
<!-- 更多按钮 -->
<view v-if="group.has_more" class="nft-grid-item more-item" @click="goToMore(group)" @touchstart.stop>
<!-- <view class="nft-image more-placeholder"></view> -->
<view class="more-overlay">
<text class="more-text">更多></text>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onActivated, watch } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { getStarbookHomeApi } from '@/utils/api.js';
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
// 屏幕宽度
const screenWidth = ref(0);
// 加载状态
const loading = ref(false);
// 当前选中的类型
const currentType = ref('regular');
// 星册首页数据
const starbookData = ref([]);
// 处理后的数据带有有效的封面URL
const processedData = ref([]);
// 上次加载时间(用于防抖)
let lastLoadedAt = 0;
// 计算卡片尺寸一行约2.5张卡片的宽度,让图片更大)
const cardSize = computed(() => {
const marginLeft = 24; // 左边距24rpx
const gap = 15; // 卡片间距15rpx
const availableWidth = 750 - marginLeft - (gap * 3.5);
return Math.floor(availableWidth / 2.5);
});
// grade 中文转换
const gradeMap = { 1: '一', 2: '二', 3: '三', 4: '四', 5: '五' };
function formatGrade(grade) {
return `等级${gradeMap[grade] || grade}`;
}
// 判断是否有数据
const hasData = computed(() => {
return processedData.value.length > 0;
});
// 根据当前类型获取分组数据(使用处理后的数据)
const regularGroups = computed(() => {
return processedData.value.filter(g => g.type === 'regular');
});
const collectionGroups = computed(() => {
return processedData.value.filter(g => g.type === 'collection');
});
const activityGroups = computed(() => {
return processedData.value.filter(g => g.type === 'activity');
});
// 切换类型
const switchType = (type) => {
currentType.value = type;
};
// 加载星册数据
const loadStarbookData = async () => {
const now = Date.now();
if (now - lastLoadedAt < 1000) return; // 1秒内不重复加载
lastLoadedAt = now;
loading.value = true;
try {
const response = await getStarbookHomeApi();
if (response.code === 200 && response.data && response.data.data.groups) {
// 处理数据获取有效的封面URL
await processGroupsWithValidUrls(response.data.data.groups);
}
} catch (error) {
console.error('获取星册数据失败:', error);
uni.showToast({
title: error.message || '获取星册数据失败',
icon: 'none',
duration: 2000
});
starbookData.value = [];
processedData.value = [];
} finally {
loading.value = false;
}
};
// 处理分组数据获取有效的封面URL
const processGroupsWithValidUrls = async (groups) => {
const processed = JSON.parse(JSON.stringify(groups)); // 深拷贝
// 遍历所有分组和藏品获取有效的封面URL
for (const group of processed) {
// 处理 regular 类型的 grades
if (group.grades) {
for (const grade of group.grades) {
for (const item of grade.items || []) {
item.coverUrl = await getAssetCoverRealUrl(item.cover_url_signed || '');
}
}
}
// 处理 collection/activity 类型的 items
if (group.items) {
for (const item of group.items) {
item.coverUrl = await getAssetCoverRealUrl(item.cover_url_signed || '');
}
}
}
processedData.value = processed;
};
// 点击卡片跳转到详情页
const handleCardClick = (item) => {
if (item.asset_id) {
uni.navigateTo({
url: `/pages/asset-detail/asset-detail?asset_id=${item.asset_id}`
});
}
};
// 点击更多跳转到查看更多页面
const goToMore = (group, grade) => {
const params = {
type: currentType.value,
category: group.category
};
if (currentType.value === 'regular' && grade) {
params.grade = grade;
}
const queryString = Object.entries(params)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('&');
uni.navigateTo({
url: `/pages/starbook/items?${queryString}`
});
};
// 定义 props
const props = defineProps({
isActive: {
type: Boolean,
default: false
}
});
onMounted(() => {
const systemInfo = uni.getSystemInfoSync();
screenWidth.value = systemInfo.windowWidth;
loadStarbookData();
});
// 每次组件激活时重新加载数据
onActivated(() => {
loadStarbookData();
});
// 监听页面显示事件
onShow(() => {
if (props.isActive) {
loadStarbookData();
}
});
// 监听 isActive prop 变化
watch(() => props.isActive, (newVal) => {
if (newVal) {
loadStarbookData();
}
});
</script>
<style scoped>
.starbook-content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 1;
/* overflow-y: auto; */
overflow: hidden;
background: #0d0820;
}
/* 滚动条样式 */
.starbook-content::-webkit-scrollbar {
width: 6rpx;
}
.starbook-content::-webkit-scrollbar-track {
background: transparent;
}
.starbook-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10rpx;
transition: background 0.3s ease;
}
.starbook-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
.background-image {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 0;
object-fit: cover;
min-width: 100%;
min-height: 100%;
}
.content-wrapper {
position: relative;
z-index: 1;
width: 100%;
min-height: 100%;
padding: 192rpx 30rpx 120rpx;
box-sizing: border-box;
}
/* 类型Tab - 固定在顶部 */
.type-tabs {
/* position: fixed;
top: 0;
left: 0;
right: 0; */
display: flex;
justify-content: center;
gap: 40rpx;
padding: 20rpx 30rpx;
z-index: 100;
}
.tab-item {
padding: 12rpx 30rpx;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
border-bottom: 4rpx solid transparent;
transition: all 0.3s ease;
}
.tab-item.active {
color: #ffffff;
border-bottom-color: #ffffff;
}
/* 可滚动区域 */
.nft-scroll-view {
height: calc(100vh - 320rpx);
}
/* 隐藏滚动条 */
.nft-scroll-view::-webkit-scrollbar {
display: none;
}
.nft-scroll-view {
scrollbar-width: none;
-ms-overflow-style: none;
}
/* 加载中 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
padding-top: 200rpx;
}
.loading-text {
color: rgba(255, 255, 255, 0.6);
font-size: 28rpx;
}
/* 空状态 */
.empty-container {
display: flex;
justify-content: center;
align-items: center;
padding-top: 200rpx;
}
.empty-text {
color: rgba(255, 255, 255, 0.6);
font-size: 28rpx;
}
/* 藏品列表容器 */
.nft-list-container {
width: 100%;
}
/* 藏品分组 */
.nft-group {
margin-bottom: 50rpx;
}
/* 等级区块 */
.grade-section {
background: rgba(255, 255, 255, 0.03);
border-radius: 16rpx;
padding: 20rpx;
}
/* 分组标题 */
.group-header {
margin-bottom: 16rpx;
padding-bottom: 10rpx;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
}
.group-title {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
}
/* 藏品行 - 水平滚动 */
.nft-row {
width: 100%;
height: 288rpx;
white-space: nowrap;
}
/* 藏品行内容容器 */
.nft-row-content {
display: inline-block;
white-space: nowrap;
padding-left: 24rpx;
height: 100%;
}
/* 隐藏滚动条 */
.nft-row::-webkit-scrollbar {
display: none;
}
.nft-row {
scrollbar-width: none;
-ms-overflow-style: none;
}
/* 藏品网格项 */
.nft-grid-item {
position: relative;
display: inline-block;
vertical-align: top;
margin-right: 32rpx;
height: 100%;
}
/* NFT 图片 */
.nft-image {
width: 192rpx;
height: 224rpx;
border-radius: 16rpx;
/* background: rgba(255, 255, 255, 0.05); */
display: block;
}
/* 已展示的图片 - 灰色滤镜 */
.nft-image-displayed {
filter: grayscale(23%);
}
/* 更多占位符 */
.more-placeholder {
background: rgba(255, 255, 255, 0.1);
}
.nft-grid-item.more-item {
cursor: pointer;
}
/* 藏品信息 */
.nft-info {
padding: 12rpx 0;
text-align: center;
}
/* 展示状态覆盖层 - 居中显示 */
.status-overlay {
position: absolute;
top: 0;
left: 0;
width: 192rpx;
height: 224rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16rpx;
z-index: 1;
}
.status-text-center {
font-size: 24rpx;
color: #fff;
font-weight: bold;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8);
/* 渐变:左浅橙粉 → 右柔粉红 */
background: linear-gradient(to bottom right,
#F0E4B1 0%, /* 左:浅橙粉 */
#F08399 50%,
#B94E73 100% /* 右:柔粉红 */
);
border-radius: 24rpx; /* 胶囊形 */
/* 立体感核心:多层阴影 + 内阴影模拟凸起 */
box-shadow:
/* 外层投影 - 让按钮浮起 */
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
/* 内阴影 - 模拟顶部受光 + 底部凹陷 */
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4), /* 顶部高光 */
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05); /* 底部暗部 */
padding: 16rpx;
}
.nft-name {
display: block;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.9);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nft-likes {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.5);
}
/* 更多覆盖层 */
.more-overlay {
/* position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); */
width: 192rpx;
height: 224rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
border-radius: 16rpx;
}
.more-text {
font-size: 26rpx;
color: #ffffff;
}
</style>