448 lines
11 KiB
Vue
448 lines
11 KiB
Vue
<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>
|