topfans/frontend/pages/square/hot-category-more.vue

537 lines
12 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="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>
<!-- 加载中 -->
<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 { getHotRankingApi } from '@/utils/api.js'
import { doubleTapLike } from '@/utils/likeHelper.js'
// ========== 路由参数 ==========
const dimension = ref('displaying')
const pageTitle = ref('')
// ========== 状态 ==========
const items = ref([])
const loading = ref(false)
const noMore = ref(false)
const activeSubTab = ref('all')
const likingMap = ref({})
const cardTapTimers = {}
// ========== 子标签配置(仅星卡) ==========
const starCardSubTabs = [
{ label: '全部', value: 'all' },
{ label: '光栅卡', value: 'raster' },
{ label: '镭射卡', value: 'holographic' },
{ label: '撕拉卡', value: 'tear_off' },
{ label: '拍立得', value: 'polaroid' }
]
// ========== 组件通信 ==========
const emit = defineEmits(['cardClick', 'scroll'])
// ========== 计算属性 ==========
const isStarCard = computed(() => false)
// ========== 工具函数 ==========
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.dimension) {
dimension.value = options.dimension
}
if (options.title) {
pageTitle.value = decodeURIComponent(options.title)
}
})
// ========== 数据加载 ==========
const loadData = async (reset = false) => {
if (loading.value) return
if (reset) {
noMore.value = false
items.value = []
}
loading.value = true
try {
const res = await getHotRankingApi(dimension.value, null, 1, 20)
if (res.code === 200 && res.data?.items) {
const newItems = res.data.items.map((item) => {
return {
...item,
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.like_count || 0,
is_liked: item.is_liked || false,
}
})
items.value = reset ? newItems : [...items.value, ...newItems]
if (res.data.items.length < 20) {
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 = (card) => {
if (cardTapTimers[card.id]) {
// 第二次点击,双击点赞
clearTimeout(cardTapTimers[card.id]);
delete cardTapTimers[card.id];
// 触发动画
likingMap.value = { ...likingMap.value, [card.id]: true };
setTimeout(() => {
likingMap.value = { ...likingMap.value, [card.id]: false };
}, 600);
doubleTapLike(card.id, card.exhibition_id || 0, (success) => {
if (success) {
uni.showToast({ title: '点赞成功', icon: 'success' })
}
});
} else {
// 第一次点击,单击跳转
if (card.id) {
cardTapTimers[card.id] = setTimeout(() => {
delete cardTapTimers[card.id];
uni.navigateTo({ url: `/pages/asset-detail/asset-detail?asset_id=${card.id}` });
}, 300);
}
}
}
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.asset_id || c.id) === asset_id)
if (idx !== -1) {
if (data && typeof data.new_like_count === 'number') {
items.value[idx].like_count = data.new_like_count
}
if (data && typeof data.is_liked === 'boolean') {
items.value[idx].is_liked = data.is_liked
}
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;
}
/* 点赞角标 */
.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;
}
/* 光波动画 */
.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>