topfans/frontend/pages/square/square.vue

448 lines
11 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="square-container">
<!-- 背景图片 -->
<image class="bg-wrapper" src="/static/square/squearbj1.png" mode="aspectFill"></image>
<!-- Header组件 -->
<Header :showGuideIcon="true" :showTaskIcon="true" :showStarActivityIcon="true" backIconColor="#e6e6e6" />
<!-- 蒙层 - 导航栏展开时显示 -->
<view v-if="navExpanded" class="nav-mask" @click="navExpanded = false"></view>
<!-- 排行榜弹窗 -->
<RankingModal :visible="showRankingModal" :parent-active="true" :star-id="currentStarId"
@update:visible="handleRankingModalClose" @visit="handleRankingVisit" />
<!-- 底部导航栏 -->
<BottomNav :activeTab="4" :isExpanded="navExpanded" @update:activeTab="handleTabChange"
@update:isExpanded="navExpanded = $event" />
<!-- 全局引导遮罩 -->
<GuideOverlay />
<!-- 内容区域 -->
<scroll-view class="content-wrapper" scroll-y :show-scrollbar="false" :bounce="false"
@scrolltolower="handleScrollToLower">
<!-- 区域一:顶部运营轮播图 -->
<view class="banner-section">
<BannerCarousel :bannerActivities="bannerActivities" @activityClick="handleActivityClick"
@top3Click="showRankingModal = true" />
</view>
<!-- 在线榜单区块 -->
<view class="hot-category-wrapper">
<HotCategoryBlock :title="'日榜'" @cardClick="handleCardClick" />
</view>
<!-- 区域二主Tab标签区星卡/吧唧/海报) -->
<view class="main-tab-section">
<view v-for="(tab, index) in mainTabs" :key="index" class="tab-item" @click="handleMainTabClick(tab)">
<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>
<text class="tab-name">{{ tab.name }}</text>
</view>
</view>
<!-- 区域三:分类标签区 -->
<view 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: activeContentTab === category.value }" @click="handleCategoryChange(category.value)">
<text class="category-text">{{ category.label }}</text>
</view>
</scroll-view>
</view>
<!-- 区域四创作网格列表 -->
<CreationGrid :screenWidth="screenWidth" :screenHeight="screenHeight" :bannerBottom="bannerBottomPx"
:category="activeContentTab" :isActive="isActive" @cardClick="handleCardClick" ref="creationGridRef" />
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { onLoad, onShow, onHide } from '@dcloudio/uni-app'
import { useStore } from 'vuex'
import Header from '../components/Header.vue'
import BottomNav from '../components/BottomNav.vue'
import GuideOverlay from '@/components/GuideOverlay.vue'
import RankingModal from '../components/RankingModal.vue'
import BannerCarousel from './components/BannerCarousel.vue'
import HotCategoryBlock from './components/HotCategoryBlock.vue'
import CreationGrid from './components/CreationGrid.vue'
import { getHotRankingApi } from '@/utils/api.js'
// import { clearSubStepProgress, shouldShowGuideStartModal } from '@/utils/guideConfig.js'
import { useBanner } from './composables/useBanner.js'
import { doubleTapLike } from '@/utils/likeHelper.js'
// import { USE_MOCK_DATA } from './config/mockData.js'
// ========== Store & User Info ==========
const store = useStore()
// const currentUserNickname = computed(() => store.state.user?.userInfo?.nickname || '')
const currentStarId = ref(uni.getStorageSync('star_id') || null)
// ========== UI State ==========
const activeContentTab = ref('hot')
const navExpanded = ref(false)
const showRankingModal = ref(false)
const isActive = ref(true)
const isFixed = ref(false)
const creationGridRef = ref(null)
const cardTapTimers = {}
const likingMap = ref({})
// 主Tab配置
const mainTabs = ref([
{
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
}
]);
// ========== 分类配置 ==========
const categories = ref([
{ label: '热门作品', value: 'hot' },
{ label: '最新作品', value: 'latest' },
{ label: '星卡', value: 'star_card' },
{ label: '吧唧', value: 'badge' },
{ label: '海报', value: 'poster' }
])
// ========== Watch activeContentTab ==========
watch(activeContentTab, (newTab) => {
if (newTab === 'myworks') {
uni.navigateTo({ url: '/pages/profile/myWorks' })
return
}
})
// ========== Screen Info ==========
const screenWidth = ref(375)
const screenHeight = ref(812)
// ========== Composables ==========
const {
bannerActivities,
loadBannerActivities,
} = useBanner()
// banner(216+360rpx) + tab栏(16+80rpx) + 间距(8rpx) ≈ 680rpx
const bannerBottomPx = computed(() => Math.round(screenWidth.value / 750 * 715))
// ========== Handlers ==========
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 handleScrollToLower = () => {
if (creationGridRef.value) {
creationGridRef.value.loadMore()
}
}
const handleActivityClick = (item) => {
uni.navigateTo({
url: `/pages/support-activity/index?id=${item.id}`,
})
}
const handleRankingVisit = ({ userId, nickname }) => {
showRankingModal.value = false
uni.navigateTo({
url: `/pages/profile/hisWorks?userId=${userId}&nickname=${encodeURIComponent(nickname)}`
});
}
const handleRankingModalClose = (visible) => {
showRankingModal.value = visible
if (!visible && store.state.guide.componentMode) {
uni.$emit('guide:closeComponent')
}
}
const handleTabChange = (newTab) => {
if (newTab === 4) {
navExpanded.value = false
return
}
const routes = [
'/pages/ai-dazi/index',
'/pages/starbook/index',
'/pages/castlove/mall',
'/pages/starcity/index',
'/pages/square/square'
]
if (newTab >= 0 && newTab < routes.length) {
uni.navigateTo({
url: routes[newTab]
})
}
}
const handleCategoryChange = (value) => {
if (activeContentTab.value === value) return
activeContentTab.value = value
}
// ========== Tile Change Callback ==========
const handleTileChange = () => { }
// ========== Reset Square ==========
const resetSquare = async () => { }
// ========== Lifecycle ==========
onMounted(() => {
const info = uni.getSystemInfoSync()
screenWidth.value = info.windowWidth
screenHeight.value = info.windowHeight
resetSquare()
loadBannerActivities()
})
onShow(() => {
isActive.value = true
activeContentTab.value = 'hot'
})
onHide(() => {
isActive.value = false
})
// onLoad((options) => {
// if (options && 'guide_debug' in options) {
// const debugValue = options.guide_debug
// const isDebug = debugValue === '1' || debugValue === 'true'
// if (isDebug) {
// uni.setStorageSync('guide_debug_mode', true)
// uni.setStorageSync('is_new_user', true)
// console.log('[Guide] 调试模式已开启')
// } else {
// uni.setStorageSync('guide_debug_mode', false)
// uni.removeStorageSync('is_new_user')
// console.log('[Guide] 调试模式已关闭')
// }
// }
// if (options && options.guide_key) {
// console.log('[Guide] 收到引导跳转参数, guide_key:', options.guide_key, 'guide_step:', options.guide_step)
// store.dispatch('guide/resumeGuide', options.guide_key).then(res => {
// console.log('[Guide] resumeGuide 结果:', res)
// }).catch(err => {
// console.error('[Guide] resumeGuide 失败:', err)
// })
// }
// })
// 监听引导打开组件事件
uni.$on('guide:openComponent', (componentName) => {
if (componentName === 'RankingModal') {
showRankingModal.value = true
}
})
onUnmounted(() => {
uni.$off('guide:openComponent')
})
</script>
<style scoped>
.square-container {
position: relative;
width: 100vw;
height: 100vh;
min-height: 100vh;
overflow: hidden;
}
/* 背景图片 */
.bg-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
/* 内容区域 */
.content-wrapper {
position: relative;
z-index: 1;
width: 100%;
height: 100vh;
/* margin-top: 160rpx; */
padding: 240rpx 24rpx 0;
box-sizing: border-box;
}
/* 区域一:轮播图 */
.banner-section {
width: 100%;
/* margin-bottom: 32rpx; */
}
/* 区域二主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 {
margin-bottom: 12rpx;
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);
}
/* 区域三:分类标签 */
.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-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;
}
/* 热门分类区块 */
.hot-category-wrapper {
margin-bottom: 16rpx;
}
/* 蒙层 */
.nav-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
z-index: 999;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>