topfans/frontend/pages/square/square.vue
2026-04-13 11:30:05 +08:00

411 lines
9.6 KiB
Vue
Raw Permalink 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"
@touchstart="onBgTouchStart"
@touchmove="onBgTouchMove"
@touchend="onBgTouchEnd"
@touchcancel="onBgTouchCancel"
>
<!-- 横向无限滚动背景条 -->
<view class="background-strip" :style="backgroundStripStyle">
<image
v-for="i in 3"
:key="i"
class="background-tile"
:style="{ width: tileWidth + 'px', height: '100%' }"
src="/static/background/mainbg.png"
/>
</view>
<!-- Cabin 图标层(与背景同步移动) -->
<view class="cabin-layer" :style="cabinLayerStyle">
<CabinItem
v-for="cabin in visibleCabins"
:key="cabin.key"
:cabin="cabin"
:currentUserNickname="currentUserNickname"
@click="handleCabinClick"
/>
</view>
<!-- 翻页箭头按钮 -->
<NavArrows @scroll="scrollPage" />
<!-- 调试按钮(开发环境) -->
<view v-if="isDev" class="debug-btn" @click="openDebugGrid">
<text class="debug-text">调试</text>
</view>
<!-- Header组件 -->
<Header
:showGuideIcon="true"
:showTaskIcon="true"
:showStarActivityIcon="true"
backIconColor="#e6e6e6"
/>
<!-- 轮播图 + 应援活动列表 -->
<BannerCarousel
:bannerActivities="bannerActivities"
@activityClick="handleActivityClick"
@top3Click="showRankingModal = true"
/>
<!-- 蒙层 - 导航栏展开时显示 -->
<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="0"
:isExpanded="navExpanded"
@update:activeTab="handleTabChange"
@update:isExpanded="navExpanded = $event"
/>
<!-- 新手引导开始弹窗 -->
<GuideStartModal
:visible="showGuideStartModal"
@start="handleGuideStart"
@close="handleGuideStartModalClose"
/>
<!-- 全局引导遮罩 -->
<GuideOverlay />
</view>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { useStore } from 'vuex'
import Header from '../components/Header.vue'
import BottomNav from '../components/BottomNav.vue'
import GuideStartModal from '@/components/GuideStartModal.vue'
import GuideOverlay from '@/components/GuideOverlay.vue'
import RankingModal from '../components/RankingModal.vue'
import CabinItem from './components/CabinItem.vue'
import BannerCarousel from './components/BannerCarousel.vue'
import NavArrows from './components/NavArrows.vue'
import { shouldShowGuideStartModal } from '@/utils/guideConfig.js'
import { IMAGE_W, IMAGE_H } from './config/cabin.js'
import { useSwipe } from './composables/useSwipe.js'
import { useCabin } from './composables/useCabin.js'
import { useBanner } from './composables/useBanner.js'
import { useDialogRotation } from './composables/useDialogRotation.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 navExpanded = ref(false)
const showRankingModal = ref(false)
const showGuideStartModal = ref(false)
const isDev = ref(false) // 开发模式,显示调试按钮
// ========== Screen Info ==========
const tileWidth = ref(375)
const screenWidth = ref(375)
const screenHeight = ref(812)
// ========== Composables ==========
const {
cabinLayerStyle,
backgroundStripStyle,
scrollPage,
initSwipe,
reset: resetSwipe,
onBgTouchStart,
onBgTouchMove,
onBgTouchEnd,
onBgTouchCancel,
velocity,
} = useSwipe()
const {
visibleCabins,
currentPage,
ensurePages,
initCabin,
updateCurrentUserNickname,
resetSquare: resetCabinSquare,
wrapPage,
} = useCabin()
const {
bannerActivities,
loadBannerActivities,
} = useBanner()
const {
initDialogRotation,
startDialogRotation,
stopDialogRotation,
} = useDialogRotation()
// ========== Handlers ==========
const handleActivityClick = (item) => {
uni.navigateTo({
url: `/pages/support-activity/index?id=${item.id}`,
})
}
const handleRankingVisit = (userId) => {
showRankingModal.value = false
uni.navigateTo({
url: `/pages/exhibition/exhibition?target_uid=${userId}`,
})
}
const handleRankingModalClose = (visible) => {
showRankingModal.value = visible
if (!visible && store.state.guide.isActive) {
uni.$emit('guide:closeComponent')
}
}
const handleCabinClick = (cabin) => {
// 惯性速度残留时说明是滑动结束,不是真实点击
if (Math.abs(velocity) > 0.5) return
// 无用户、或被 banner 遮住showNickname=false的小屋不响应点击
if (!cabin.userId || !cabin.showNickname) return
uni.navigateTo({
url: (cabin.isMine || cabin.nickname === currentUserNickname.value)
? '/pages/exhibition/exhibition'
: `/pages/exhibition/exhibition?target_uid=${cabin.userId}`,
})
}
const handleTabChange = (newTab) => {
if (newTab === 0) {
navExpanded.value = false
return
}
const routes = [
'/pages/square/square',
'/pages/starbook/index',
'/pages/castlove/mall',
'/pages/starcity/index',
'/pages/friends/index'
]
if (newTab >= 0 && newTab < routes.length) {
uni.redirectTo({
url: routes[newTab]
})
}
}
const handleGuideStart = () => {
showGuideStartModal.value = false
}
const handleGuideStartModalClose = () => {
showGuideStartModal.value = false
}
const openDebugGrid = () => {
uni.navigateTo({
url: '/pages/square/debug-grid'
})
}
// ========== Tile Change Callback ==========
const handleTileChange = (delta, isInertia) => {
const newPage = wrapPage(currentPage.value + delta)
currentPage.value = newPage
if (isInertia) {
// 惯性阶段防抖
let ensureTimer = null
if (ensureTimer) clearTimeout(ensureTimer)
ensureTimer = setTimeout(() => {
ensurePages(newPage)
ensureTimer = null
}, 100)
} else {
// 手动触摸阶段立即触发
ensurePages(newPage)
}
}
// ========== Reset Square ==========
const resetSquare = async () => {
resetSwipe()
await resetCabinSquare()
}
// ========== Watch visibleCabins for Dialog Rotation ==========
watch(visibleCabins, () => {
if (visibleCabins.value.some(c => c.nickname && c.sharedBoothSlotsRemaining !== null)) {
startDialogRotation(visibleCabins.value)
} else {
stopDialogRotation()
}
}, { immediate: true })
// ========== Watch currentUserNickname ==========
watch(currentUserNickname, (nickname) => {
if (nickname) {
updateCurrentUserNickname(nickname)
}
}, { immediate: true })
// ========== Lifecycle ==========
onMounted(() => {
const info = uni.getSystemInfoSync()
screenWidth.value = info.windowWidth
screenHeight.value = info.windowHeight
tileWidth.value = Math.round(info.windowHeight * (IMAGE_W / IMAGE_H))
// 初始化各模块
initSwipe({
screenW: screenWidth.value,
tileW: tileWidth.value,
onTileChangeCallback: handleTileChange,
})
initCabin({
screenW: screenWidth.value,
tileW: tileWidth.value,
imageH: screenHeight.value,
currentUserNickname: currentUserNickname.value,
})
initDialogRotation({
screenW: screenWidth.value,
screenH: screenHeight.value,
})
// 数据加载在后台进行,不阻塞渲染
resetSquare()
loadBannerActivities()
})
onShow(() => {
// 检查是否需要显示引导开始弹窗
if (shouldShowGuideStartModal()) {
showGuideStartModal.value = true
}
})
onLoad((options) => {
// 调试模式:读取 guide_debug 参数并设置存储
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] 调试模式已关闭')
}
}
})
// 监听引导打开组件事件
uni.$on('guide:openComponent', (componentName) => {
if (componentName === 'RankingModal') {
showRankingModal.value = true
}
})
onUnmounted(() => {
uni.$off('guide:openComponent')
stopDialogRotation()
})
</script>
<style scoped>
.square-container {
position: relative;
width: 100vw;
height: 100vh;
min-height: 100vh;
overflow: hidden;
}
.background-strip {
position: absolute;
top: 0;
left: 50%;
height: 150%;
display: flex;
z-index: 0;
will-change: transform;
}
.background-tile {
flex-shrink: 0;
height: 100%;
}
.cabin-layer {
position: absolute;
top: 0;
left: 50%;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: none;
}
.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;
}
}
.debug-btn {
position: fixed;
bottom: 200rpx;
right: 20rpx;
width: 100rpx;
height: 100rpx;
background: rgba(255, 0, 0, 0.8);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.3);
}
.debug-text {
font-size: 24rpx;
color: #ffffff;
font-weight: bold;
}
</style>