776 lines
19 KiB
Vue
776 lines
19 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"
|
||
: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: "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 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/square/square",
|
||
];
|
||
|
||
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 → px:96rpx 在不同屏幕宽度下换算
|
||
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: 240rpx 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: 64rpx;
|
||
}
|
||
|
||
.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 1;JS 通过 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>
|