topfans/frontend/pages/square/hot-category-more.vue
zheng020 d45a2fb479 fix: remove assetLiked event listener on unmount to prevent memory leak
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:46:09 +08:00

464 lines
10 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="page-container">
<!-- 背景图片 -->
<image class="bg-image" src="/static/square/squearbj.png" mode="aspectFill"></image>
<!-- 顶部导航 -->
<view class="nav-bar">
<view class="nav-back" @tap="goBack">
<text class="nav-back-icon"></text>
</view>
<text class="nav-title">{{ pageTitle }}</text>
<view class="nav-placeholder"></view>
</view>
<!-- 子标签仅星卡显示 -->
<view v-if="isStarCard" class="sub-tabs">
<view
v-for="tab in starCardSubTabs"
:key="tab.value"
class="sub-tab-item"
:class="{ active: activeSubTab === tab.value }"
@tap="handleSubTabChange(tab.value)"
>
<text class="sub-tab-text">{{ tab.label }}</text>
</view>
</view>
<!-- 瀑布流网格 -->
<scroll-view
class="content-scroll"
scroll-y
:show-scrollbar="false"
@scrolltolower="handleLoadMore"
>
<view class="creation-grid">
<view
v-for="item in items"
:key="item.id"
class="creation-card"
@tap="handleCardClick(item)"
>
<image class="creation-image" :src="item.cover_image" mode="aspectFill"></image>
<!-- 光波动画层 - 外层 -->
<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 class="like-info">
<image class="like-icon" src="/static/icon/heart-icon.png" mode="aspectFit"></image>
<text class="like-count">{{ formatCount(item.like_count) }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 加载中 -->
<view v-if="loading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<!-- 没有更多 -->
<view v-if="noMore && items.length > 0" class="no-more">
<text class="no-more-text">没有更多了</text>
</view>
<!-- 空状态 -->
<view v-if="!loading && items.length === 0" class="empty-state">
<text class="empty-text">暂无作品</text>
</view>
<!-- 底部安全区 -->
<view class="safe-area-bottom"></view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getHotInspirationFlowMoreApi } from '@/utils/api.js'
// ========== 路由参数 ==========
const type = ref('')
const pageTitle = ref('')
// ========== 状态 ==========
const items = ref([])
const cursor = ref('')
const loading = ref(false)
const noMore = ref(false)
const activeSubTab = ref('all')
const likingMap = ref({})
// ========== 子标签配置(仅星卡) ==========
const starCardSubTabs = [
{ label: '全部', value: 'all' },
{ label: '光栅卡', value: 'raster' },
{ label: '镭射卡', value: 'holographic' },
{ label: '撕拉卡', value: 'tear_off' },
{ label: '拍立得', value: 'polaroid' }
]
// ========== 计算属性 ==========
const isStarCard = computed(() => type.value === 'hot_star_card')
// ========== 工具函数 ==========
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()
}
// ========== 页面加载 ==========
onLoad((options) => {
if (options.type) {
type.value = options.type
}
if (options.title) {
pageTitle.value = decodeURIComponent(options.title)
}
})
// ========== 数据加载 ==========
const loadData = async (reset = false) => {
if (loading.value) return
if (reset) {
cursor.value = ''
noMore.value = false
items.value = []
}
loading.value = true
try {
// 星卡使用子类型
const apiType = isStarCard.value && activeSubTab.value !== 'all'
? activeSubTab.value
: type.value
const res = await getHotInspirationFlowMoreApi(apiType, cursor.value, 20)
if (res.code === 200 && res.data?.items && res.data.items.length > 0) {
const newItems = res.data.items.map((item) => {
return {
id: item.asset_id,
certificate_id: item.asset_id,
cover_image: item.cover_url || '',
creator_avatar: item.owner_avatar || '',
creator_name: item.owner_nickname || item.name || '',
like_count: item.likes || item.like_count || 0,
}
})
cursor.value = res.data.cursor || ''
items.value = reset ? newItems : [...items.value, ...newItems]
if (!cursor.value) {
noMore.value = true
}
} else {
noMore.value = true
}
} catch (e) {
console.error('[HotCategoryMore] 加载数据失败', e?.message ?? e)
} finally {
loading.value = false
}
}
// ========== 事件处理 ==========
const handleSubTabChange = (value) => {
if (activeSubTab.value === value) return
activeSubTab.value = value
loadData(true)
}
const handleLoadMore = () => {
if (!loading.value && !noMore.value) {
loadData(false)
}
}
const handleCardClick = (item) => {
uni.navigateTo({
url: `/pages/asset-detail/asset-detail?asset_id=${item.id}`
})
}
const goBack = () => {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
uni.reLaunch({
url: '/pages/square/square'
})
}
}
// ========== 生命周期 ==========
onMounted(() => {
loadData(true)
// 监听点赞事件,实时更新点赞数
uni.$on('assetLiked', ({ asset_id, data }) => {
// 触发动画
likingMap.value = { ...likingMap.value, [asset_id]: true }
setTimeout(() => {
likingMap.value = { ...likingMap.value, [asset_id]: false }
}, 600)
const idx = items.value.findIndex(c => c.id === asset_id)
if (idx !== -1 && data && typeof data.new_like_count === 'number') {
items.value[idx].like_count = data.new_like_count
items.value = [...items.value]
}
})
})
onUnmounted(() => {
uni.$off('assetLiked')
})
</script>
<style scoped>
.page-container {
min-height: 100vh;
position: relative;
}
.bg-image {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 120%;
z-index: 0;
}
.nav-bar {
position: relative;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
padding: 80rpx 32rpx 16rpx;
}
.nav-back {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
}
.nav-back-icon {
font-size: 48rpx;
font-weight: bold;
color: #fff;
}
.nav-title {
font-size: 36rpx;
font-weight: 700;
color: #fff;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.9);
letter-spacing: 2rpx;
}
.nav-placeholder {
width: 80rpx;
}
.sub-tabs {
position: relative;
z-index: 10;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0 16rpx 16rpx;
gap: 16rpx;
}
.sub-tab-item {
padding: 12rpx 24rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 40rpx;
backdrop-filter: blur(10rpx);
transition: all 0.3s;
}
.sub-tab-item.active {
background: linear-gradient(135deg, #F0E4B1, #F08399);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.4);
}
.sub-tab-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
}
.sub-tab-item.active .sub-tab-text {
color: #fff;
font-weight: 600;
}
.content-scroll {
position: relative;
z-index: 1;
width: 100%;
height: calc(100vh - 160rpx);
}
.creation-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 24rpx;
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);
position: relative;
}
.creation-image {
width: 100%;
height: 400rpx;
}
/* 光波动画 */
.wf-like-wave {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 400rpx;
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-info {
display: flex;
align-items: center;
}
.like-icon {
width: 24rpx;
height: 24rpx;
margin-right: 4rpx;
}
.like-count {
font-size: 22rpx;
color: #fff;
}
.loading-more,
.no-more,
.empty-state {
width: 100%;
text-align: center;
padding: 32rpx 0;
}
.loading-text,
.no-more-text,
.empty-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
}
.safe-area-bottom {
height: 120rpx;
}
</style>