topfans/frontend/pages/square/square.vue
2026-06-03 01:21:28 +08:00

776 lines
19 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"
:class="{ 'spotlight-ready': isH5 }"
scroll-y
:show-scrollbar="false"
:bounce="false"
@scroll="onScroll"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@scrolltolower="handleScrollToLower"
>
<!-- 区域一:顶部运营轮播图 -->
<view ref="bannerSectionRef" class="banner-section">
<BannerCarousel
:bannerActivities="bannerActivities"
@activityClick="handleActivityClick"
@top3Click="showRankingModal = true"
/>
</view>
<ContentTabs
ref="contentTabsRef"
class="tabs"
:modelValue="activeContentTab"
@update:modelValue="activeContentTab = $event"
/>
<!-- 在线榜单区块 -->
<view ref="hotCategoryRef" class="hot-category-wrapper">
<HotCategoryBlock
:dimension="activeContentTab"
@cardClick="handleCardClick"
/>
<view class="hot-more-btn" @click="goToHotCategoryMore">
<text class="hot-more-text">查看更多</text>
</view>
</view>
<!-- 区域二主Tab标签区星卡/吧唧/海报) -->
<view ref="mainTabsRef" class="main-tab-section">
<view
v-for="(tab, index) in mainTabs"
:key="index"
class="tab-item"
:style="{ '--tab-delay': index * 0.06 + 's' }"
@click="handleMainTabClick(tab)"
>
<view class="tab-icon-wrap">
<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>
</view>
<text class="tab-name">{{ tab.name }}</text>
</view>
</view>
<!-- 区域三:分类标签区 -->
<view
ref="categoryRef"
id="category-section"
class="category-section"
:class="{ fixed: isFixed, 'just-fixed': justFixed }"
>
<scroll-view class="category-scroll" scroll-x :show-scrollbar="false">
<view
v-for="(category, index) in categories"
:key="index"
class="category-item"
:class="{ active: activeCategoryTab === category.value }"
@click="handleCategoryChange(category.value)"
>
<text class="category-text">{{ category.label }}</text>
</view>
</scroll-view>
</view>
<!-- fixed 时占位避免下方内容跳变 -->
<view
v-if="isFixed && categoryHeight > 0"
class="category-placeholder"
:style="{ height: categoryHeight + 'px' }"
></view>
<!-- 区域四:创作网格列表 -->
<CreationGrid
:screenWidth="screenWidth"
:screenHeight="screenHeight"
:bannerBottom="bannerBottomPx"
:category="activeCategoryTab"
:isActive="isActive"
@cardClick="handleCardClick"
@loaded="onGridLoaded"
ref="creationGridRef"
/>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted, nextTick } 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 ContentTabs from "./components/ContentTabs.vue";
import HotCategoryBlock from "./components/HotCategoryBlock.vue";
import CreationGrid from "./components/CreationGrid.vue";
// import { clearSubStepProgress, shouldShowGuideStartModal } from '@/utils/guideConfig.js'
import { useBanner } from "./composables/useBanner.js";
import { useSpotlight } from "./composables/useSpotlight.js";
import { doubleTapLike } from "@/utils/likeHelper.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("displaying");
const activeCategoryTab = ref("hot");
const navExpanded = ref(false);
const showRankingModal = ref(false);
const isActive = ref(true);
const isFixed = ref(false);
const justFixed = ref(false);
const creationGridRef = ref(null);
const cardTapTimers = {};
const likingMap = ref({});
// ========== 各区块的 template ref ==========
const bannerSectionRef = ref(null);
const contentTabsRef = ref(null);
const hotCategoryRef = ref(null);
const mainTabsRef = ref(null);
const categoryRef = ref(null);
// ========== 分类标签 fixed 触发的"占位 + 阈值" ==========
// 记录 section 在滚动内容中的初始位置offsetTop和自然高度
// 滚动时用 scrollTop + 阈值 对比,决定是否切到 fixed
const categoryOffsetTop = ref(null);
const categoryHeight = ref(0);
const fixedTopPx = ref(50); // 近似对应 CSS 的 top: 96rpx
// ========== 滚动 spotlight ==========
// 把所有"会随滚动淡出/淡入"的元素喂给 composable
// - 4 个区块
// - CreationGrid 里的所有卡片(通过 defineExpose 暴露的 getCardRefs 拿到)
const allSpotlightRefs = () => {
const sections = [
bannerSectionRef.value,
contentTabsRef.value?.$el || contentTabsRef.value,
hotCategoryRef.value,
mainTabsRef.value,
categoryRef.value,
].filter(Boolean)
const cards = creationGridRef.value?.getCardRefs?.() || []
return [...sections, ...cards]
}
const { update, bindScroll, start: startSpotlight, stop: stopSpotlight, isH5 } = useSpotlight({
getElements: allSpotlightRefs,
})
// 统一 scroll 处理spotlight + isFixed
const onScroll = (e) => {
bindScroll() // 兼容路径scroll 事件触发时也跑一次(主驱动是 rAF 轮询)
const scrollTop = (e && e.detail && e.detail.scrollTop) || 0
if (categoryOffsetTop.value !== null) {
isFixed.value = scrollTop + fixedTopPx.value >= categoryOffsetTop.value
}
}
// 移动端 rAF 在用户滚动时会被节流opacity 跟不上。
// 用 touchstart / touchmove 同步触发 update(),绕开 rAF 节流。
const onTouchStart = () => {
update()
}
const onTouchMove = () => {
update()
}
// 分类标签 fixed 触发的回弹:状态变化瞬间加 class动画播完摘掉
watch(isFixed, (val) => {
if (!val) return;
justFixed.value = true;
setTimeout(() => {
justFixed.value = false;
}, 500);
});
// tab / 分类变化 → 新内容可能进入视口,重做一次 spotlight 计算
watch(activeContentTab, () => {
nextTick(() => setTimeout(update, 50));
});
watch(activeCategoryTab, () => {
nextTick(() => setTimeout(update, 50));
});
// 卡片加载到位 → 重新算一次 spotlight卡片异步加载不在初始 200ms update 范围内)
const onGridLoaded = (count) => {
if (count > 0) {
nextTick(() => update());
}
};
// 主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: "new" },
{ 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 goToHotCategoryMore = () => {
const title =
activeContentTab.value === "displaying"
? "日榜"
: activeContentTab.value === "week"
? "周榜"
: "月榜";
uni.navigateTo({
url: `/pages/square/hot-category-more?dimension=${activeContentTab.value}&title=${encodeURIComponent(title)}`,
});
};
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/profile/myWorks",
];
if (newTab >= 0 && newTab < routes.length) {
uni.navigateTo({
url: routes[newTab],
});
}
};
// 主Tab点击 - 进入铸造页面
const handleMainTabClick = (tab) => {
// 跳转到 mall 页(mall 内嵌 craft-select 组件,带菜单),并带上 type
uni.navigateTo({
url: `/pages/castlove/mall?type=${encodeURIComponent(tab.type)}`,
});
};
const handleCategoryChange = (value) => {
if (activeCategoryTab.value === value) return;
activeCategoryTab.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();
// 等首屏 DOM 稳定后,测量分类标签 section 的位置/高度 + 启动 spotlight 轮询
nextTick(() => {
if (categoryRef.value) {
categoryOffsetTop.value = categoryRef.value.offsetTop;
categoryHeight.value = categoryRef.value.offsetHeight;
}
// rpx → px96rpx 在不同屏幕宽度下换算
fixedTopPx.value = (96 * screenWidth.value) / 750;
startSpotlight();
});
});
onShow(() => {
isActive.value = true;
activeContentTab.value = "displaying";
activeCategoryTab.value = "hot";
nextTick(() => {
setTimeout(update, 80);
});
});
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(() => {
stopSpotlight();
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: 208rpx 24rpx 0;
box-sizing: border-box;
}
/* 区域一:轮播图 */
.banner-section {
width: 100%;
height: 360rpx;
/* 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-wrap {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
}
.tab-icon {
margin-bottom: 0;
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-placeholder {
/* 仅占位,无内容;高度由内联 style 写入 */
margin-bottom: 24rpx;
}
.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 {
position: relative;
padding-bottom: 80rpx;
}
.hot-more-btn {
position: absolute;
bottom: 0;
right: 16rpx;
z-index: 10;
width: 104rpx;
height: 40rpx;
border-radius: 20rpx;
opacity: 0.66;
padding: 8rpx 20rpx;
background: linear-gradient(
90deg,
rgba(255, 222, 8, 0.61) -17.54%,
rgba(255, 0, 25, 0.61) 116.67%
);
box-shadow: 2px 2px 4px 0px #f2151578;
display: flex;
justify-content: center;
align-items: center;
}
.hot-more-text {
font-size: 22rpx;
color: #fff;
font-weight: 600;
}
/* 蒙层 */
.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;
}
}
/* ========== 滚动 spotlight仅 H5 ==========
* JS 把 --scroll-opacity 写到每个 .spotlight 元素上;
* 边缘渐隐由 CSS transition 平滑。
* 非 H5 不写变量,默认 1元素完全可见。 */
</style>
<style>
.spotlight,
.spotlight-ready .banner-section,
.spotlight-ready .tabs,
.spotlight-ready .hot-category-wrapper,
.spotlight-ready .main-tab-section,
.spotlight-ready .category-section,
.spotlight-ready .creation-card {
/* 默认 opacity 1JS 通过 node.style.opacity 行内样式直接覆盖 */
opacity: 1;
transition: opacity 0.22s ease-out;
will-change: opacity;
}
/* 主Tab 三个图标首次进入视野时 pop 一下first-seen 由 JS 写) */
.spotlight-ready .main-tab-section.first-seen .tab-item .tab-icon-wrap {
animation: tabIconPop 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both;
animation-delay: var(--tab-delay, 0s);
}
@keyframes tabIconPop {
0% {
transform: scale(0.5) rotate(-90deg);
opacity: 0;
}
60% {
transform: scale(1.08) rotate(0deg);
opacity: 1;
}
100% {
transform: scale(1) rotate(0);
opacity: 1;
}
}
/* 分类标签 fixed 时的回弹just-fixed 短暂挂载) */
.spotlight-ready .category-section.just-fixed {
animation: categoryBounce 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes categoryBounce {
0% {
transform: scale(1);
}
50% {
transform: scale(0.96);
}
100% {
transform: scale(1);
}
}
/* 动效减弱的可访问性兜底 */
@media (prefers-reduced-motion: reduce) {
.spotlight,
.spotlight-ready .banner-section,
.spotlight-ready .tabs,
.spotlight-ready .hot-category-wrapper,
.spotlight-ready .main-tab-section,
.spotlight-ready .category-section,
.spotlight-ready .creation-card,
.spotlight-ready .main-tab-section.first-seen .tab-item .tab-icon-wrap,
.spotlight-ready .category-section.just-fixed {
transition: none !important;
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
}
</style>