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

594 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="creation-grid-wrapper">
<!-- 区域 A主Tab星卡/吧唧/海报 -->
<view class="main-tab-section">
<view
v-for="(tab, index) in mainTabs"
:key="index"
class="tab-item"
@click="handleMainTabClick(tab)"
>
<view class="tab-icon-wrap">
<image
class="tab-icon"
:src="tab.icon"
mode="aspectFit"
:style="{
width: tab.width + 'rpx',
height: tab.height + 'rpx',
borderRadius: tab.type === 'badge' ? '50%' : '0',
transform: 'rotate(' + tab.rotate + 'deg)',
}"
></image>
</view>
<text class="tab-name">{{ tab.name }}</text>
</view>
</view>
<!-- 区域 B分类标签 -->
<!-- <view
ref="categoryRef"
id="category-section"
class="category-section"
:class="{ fixed: isFixed }"
>
<scroll-view class="category-scroll" scroll-x :show-scrollbar="false">
<view
v-for="(category, index) in categories"
:key="index"
class="category-item"
:class="{ active: category.value === activeCategory }"
@click="handleCategoryChange(category.value)"
>
<text class="category-text">{{ category.label }}</text>
</view>
</scroll-view>
</view> -->
<!-- fixed 时占位,避免下方内容跳变 -->
<view
v-if="isFixed && categoryHeight > 0"
class="category-placeholder"
:style="{ height: categoryHeight + 'px' }"
></view>
<!-- 区域 C创作网格列表保留原功能 -->
<view class="creation-grid">
<view
v-for="item in creationList"
:key="item.id"
class="creation-card"
@click="handleCardClick(item)"
>
<image class="creation-image" :src="item.cover_image" mode="aspectFill"></image>
<view class="like-badge">
<view class="like-icon-wrapper">
<image
class="like-icon"
:src="item.is_liked ? '/static/icon/heart-icon.png' : '/static/icon/heart-icon-false.png'"
mode="aspectFit"
>
</image>
<text class="like-count">{{ formatCount(item.like_count) }}</text>
</view>
</view>
<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] }"
/>
<view class="creation-info">
<view class="creation-id">
<text class="id-text">链上编号: #{{ item.certificate_id }}</text>
</view>
<view class="creation-meta">
<view class="creator-info">
<image
class="creator-avatar"
:src="item.creator_avatar"
mode="aspectFill"
></image>
<text class="creator-name">{{ item.creator_name }}</text>
</view>
</view>
</view>
</view>
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<view v-if="noMore && creationList.length > 0" class="no-more">
<text class="no-more-text">没有更多了</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { onShow, onHide } from "@dcloudio/uni-app";
import { getInspirationFlowApi } from '@/utils/api.js'
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js'
import { usePreload } from "@/composables/usePreload.js";
const emit = defineEmits(['cardClick', 'loaded'])
// ========== 内部状态 ==========
const creationList = ref([])
const cursor = ref('')
const isLoading = ref(true)
const noMore = ref(false)
const likingMap = ref({})
const activeCategory = ref('hot')
const isComponentActive = ref(true)
// ★ 首屏数据走 usePreload自动缓存 + 切换 tab 回来自动命中)
const { data: firstPageData, loading: firstPageLoading } = usePreload("inspiration.flow");
// ========== 区域 A主Tab 配置 ==========
const mainTabs = [
{ name: '星卡', type: 'star_card', icon: '/static/square/xingka.png', width: 96, height: 96, rotate: -15 },
{ name: '吧唧', type: 'badge', icon: '/static/square/baji.png', width: 100, height: 100, rotate: 0 },
{ name: '海报', type: 'poster', icon: '/static/square/haibao.png', width: 100, height: 108, rotate: -15 },
]
// ========== 区域 B分类配置 ==========
const categories = [
{ label: '热门作品', value: 'hot' },
{ label: '最新作品', value: 'new' },
{ label: '星卡', value: 'star_card' },
{ label: '吧唧', value: 'badge' },
{ label: '海报', value: 'poster' },
]
const categoryRef = ref(null)
const categoryOffsetTop = ref(null)
const categoryHeight = ref(0)
const fixedTopPx = ref(50)
const isFixed = ref(false)
// 主Tab点击
const handleMainTabClick = (tab) => {
uni.navigateTo({ url: `/pages/castlove/mall?type=${encodeURIComponent(tab.type)}` })
}
const handleCategoryChange = (value) => {
if (activeCategory.value === value) return
activeCategory.value = value
}
function updateScroll(scrollTop) {
if (categoryOffsetTop.value === null) return
isFixed.value = scrollTop + fixedTopPx.value >= categoryOffsetTop.value
}
function update() {}
function remeasure() {
nextTick(() => {
if (categoryRef.value) {
categoryOffsetTop.value = categoryRef.value.offsetTop
categoryHeight.value = categoryRef.value.offsetHeight
}
})
}
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)
}
async function resolveItemImage(item) {
if (!item) return item
const cover = item.cover_url || item.cover_image || ''
if (cover) {
item.cover_image = await getAssetCoverRealUrl(cover)
}
return item
}
// 把原始 items 转成渲染格式
async function transformItems(rawItems) {
return await Promise.all(
rawItems.map(async (item) => {
const resolved = await resolveItemImage({ ...item })
return {
id: item.asset_id,
certificate_id: item.asset_id,
cover_image: resolved.cover_image,
creator_avatar: item.owner_avatar || '',
creator_name: item.owner_nickname || item.name || '',
like_count: item.likes || item.like_count || 0,
is_liked: item.is_liked || false,
}
}),
)
}
const loadUsers = async () => {
cursor.value = ''
isLoading.value = true
noMore.value = false
try {
const res = await getInspirationFlowApi({ limit: 20, type: activeCategory.value, cursor: '' })
if (res.code === 0 && res.data?.items && res.data.items.length > 0) {
cursor.value = res.data.cursor || ''
creationList.value = await transformItems(res.data.items)
} else {
noMore.value = true
}
} catch (e) {
console.error('[CreationGrid] 加载数据失败', e?.message ?? e)
} finally {
isLoading.value = false
emit('loaded', creationList.value.length)
}
}
const loadMore = async () => {
if (isLoading.value || noMore.value) return
isLoading.value = true
try {
const res = await getInspirationFlowApi({ limit: 20, type: activeCategory.value, cursor: cursor.value })
if (res.code === 0 && res.data?.items && res.data.items.length > 0) {
cursor.value = res.data.cursor || ''
const newItems = await transformItems(res.data.items)
creationList.value = [...creationList.value, ...newItems]
} else {
noMore.value = true
}
} catch (e) {
console.error('[CreationGrid] 加载更多失败', e?.message ?? e)
} finally {
isLoading.value = false
emit('loaded', creationList.value.length)
}
}
// ★ 首屏数据watch usePreload 结果
let firstPageConsumed = false;
watch(firstPageLoading, (val) => { isLoading.value = val });
watch(firstPageData, async (newData) => {
if (!newData || newData.code !== 0 || firstPageConsumed) return;
firstPageConsumed = true;
isLoading.value = false;
try {
const items = newData.data?.items || [];
if (items.length > 0) {
creationList.value = await transformItems(items);
} else {
noMore.value = true;
}
} catch (e) {
console.error('[CreationGrid] transform 失败', e?.message ?? e);
}
emit('loaded', creationList.value.length);
}, { immediate: true });
// fallback1.5s 后如果 usePreload 还没数据
let fallbackTimer = null;
onMounted(() => {
fallbackTimer = setTimeout(() => {
if (firstPageConsumed) return;
loadUsers();
}, 1500);
});
// 分类变化 → 重新加载(不走缓存)
watch(activeCategory, () => {
loadUsers()
})
watch(isComponentActive, (active) => {
if (active && creationList.value.length === 0) {
loadUsers()
}
})
onMounted(() => {
uni.$on('assetLiked', ({ asset_id, data }) => {
likingMap.value = { ...likingMap.value, [asset_id]: true }
setTimeout(() => {
likingMap.value = { ...likingMap.value, [asset_id]: false }
}, 600)
const idx = creationList.value.findIndex((c) => c.id === asset_id)
if (idx !== -1) {
if (data && typeof data.new_like_count === 'number') {
creationList.value[idx].like_count = data.new_like_count
}
if (data && typeof data.is_liked === 'boolean') {
creationList.value[idx].is_liked = data.is_liked
}
creationList.value = [...creationList.value]
}
})
const info = uni.getSystemInfoSync()
const sw = info.windowWidth || 750
fixedTopPx.value = (96 * sw) / 750
nextTick(() => {
if (categoryRef.value) {
categoryOffsetTop.value = categoryRef.value.offsetTop
categoryHeight.value = categoryRef.value.offsetHeight
}
})
})
onShow(() => { isComponentActive.value = true })
onHide(() => { isComponentActive.value = false })
onUnmounted(() => {
uni.$off('assetLiked')
if (fallbackTimer) clearTimeout(fallbackTimer);
})
defineExpose({
loadMore, categoryRef, updateScroll, update, remeasure,
})
</script>
<style scoped>
/* 区域 A主Tab */
.main-tab-section {
display: flex;
justify-content: space-around;
align-items: center;
padding: 24rpx 0;
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
width: 200rpx;
height: 200rpx;
background: linear-gradient(135deg,
rgba(240, 228, 177, 0.3),
rgba(240, 131, 153, 0.3));
border-radius: 24rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 157, 0.2);
transition: all 0.3s;
}
.tab-item:active {
transform: scale(0.95);
opacity: 0.8;
}
.tab-icon-wrap {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
}
.tab-icon {
margin-bottom: 0;
object-fit: cover;
box-shadow: 8rpx 8rpx 16rpx rgba(229, 76, 93, 0.9);
}
.tab-name {
font-size: 28rpx;
color: #fff;
font-weight: 600;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}
/* 区域 B分类标签 */
.category-section {
margin-bottom: 24rpx;
transition: all 0.3s ease;
will-change: transform;
}
.category-section.fixed {
position: fixed;
top: 96rpx;
left: 24rpx;
right: 24rpx;
z-index: 100;
padding: 16rpx 0;
}
.category-placeholder {
margin-bottom: 24rpx;
}
.category-scroll {
white-space: nowrap;
}
.category-item {
display: inline-block;
padding: 16rpx 32rpx;
margin-right: 16rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 40rpx;
backdrop-filter: blur(10rpx);
transition: all 0.3s;
}
.category-item.active {
background: linear-gradient(135deg, #f0e4b1, #f08399);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.4);
}
.category-text {
font-size: 26rpx;
color: #fff;
font-weight: 500;
}
.category-item.active .category-text {
font-weight: 600;
}
/* 区域 C创作网格原有 */
.creation-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
padding-bottom: 120rpx;
}
.creation-card {
width: 48%;
margin-bottom: 24rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 20rpx;
overflow: hidden;
backdrop-filter: blur(10rpx);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
}
.creation-image {
width: 100%;
height: 400rpx;
}
.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);
}
}
.creation-info {
padding: 16rpx;
}
.creation-id {
margin-bottom: 12rpx;
}
.id-text {
font-size: 22rpx;
color: #FFB800;
font-weight: 600;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
}
.creation-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.creator-info {
display: flex;
align-items: center;
}
.creator-avatar {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
margin-right: 8rpx;
}
.creator-name {
font-size: 22rpx;
color: #fff;
}
.like-badge {
position: absolute;
top: 0;
left: 0;
width: 122rpx;
height: 140rpx;
opacity: 1;
border-top-left-radius: 7px;
border-bottom-right-radius: 21.5px;
background: linear-gradient(177.83deg,
rgba(83, 244, 211, 0.2) 2.52%,
rgba(15, 9, 0, 0) 69.07%);
backdrop-filter: blur(0px);
z-index: 5;
}
.like-icon-wrapper {
display: flex;
align-items: center;
padding: 8rpx;
}
.like-badge .like-icon {
width: 38rpx;
height: 38rpx;
margin-right: 6rpx;
}
.like-badge .like-count {
font-size: 32rpx;
font-weight: 400;
line-height: 100%;
letter-spacing: 0%;
color: #fffabd;
text-shadow:
-1px 1px 4px #ce0909d6,
0px 0px 10px #fffabd;
}
.loading-more,
.no-more {
width: 100%;
text-align: center;
padding: 32rpx 0;
}
.loading-text,
.no-more-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
}
</style>