408 lines
9.9 KiB
Vue
408 lines
9.9 KiB
Vue
<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"
|
||
/>
|
||
|
||
<!-- 全局引导遮罩 -->
|
||
<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 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 { clearSubStepProgress, shouldShowGuideStartModal, resetGuide } 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 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
|
||
// 使用 componentMode 判断,因为 isActive 可能在 END_GUIDE 后已变为 false
|
||
if (!visible && store.state.guide.componentMode) {
|
||
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.navigateTo({
|
||
url: routes[newTab]
|
||
})
|
||
}
|
||
}
|
||
|
||
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()) {
|
||
uni.navigateTo({
|
||
url: '/pages/tasks/guide'
|
||
})
|
||
}
|
||
})
|
||
|
||
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] 调试模式已关闭')
|
||
}
|
||
}
|
||
|
||
// 处理引导跳转参数:如果传递了 guide_key,则继续该引导
|
||
if (options && options.guide_key) {
|
||
console.log('[Guide] 收到引导跳转参数, guide_key:', options.guide_key, 'guide_step:', options.guide_step)
|
||
// 使用 resumeGuide 继续引导(不会检查 shouldShowGuide)
|
||
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')
|
||
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>
|