feat: 增加主页的动画

This commit is contained in:
zheng020 2026-06-02 15:57:11 +08:00
parent e1829b2296
commit 7bc6c7e535
9 changed files with 615 additions and 87 deletions

View File

@ -1,7 +1,7 @@
<template>
<view class="ai-dazi-container">
<!-- 背景图 -->
<image class="bg-image" src="/static/AIimg/beijing.jpg" mode="aspectFill" />
<image class="bg-image" src="/static/AIimg/beijing.png" mode="aspectFill" />
<!-- 左上角关闭按钮 -->
<view class="close-btn" @click="handleClose">

View File

@ -3,19 +3,27 @@
<image class="bg-wrapper" src="/static/castlove/beijingban.png" mode="aspectFill"></image>
<view class="main-container">
<view class="cards-container" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd">
<view v-for="(card, index) in currentCardList" :key="index" class="card-item"
:class="{ 'no-transition': disableTransition }" :style="getCardStyle(index)">
<view class="card-frame"
:class="{ 'no-border': card.comingSoon, 'card-frame--tappable': !card.comingSoon }"
@tap.stop="onCardFrameTap(index)">
<image class="card-image" :src="card.image" mode="aspectFill" />
<image v-if="card.comingSoon" class="coming-soon-badge"
src="/static/castlove/jinqingqidai.png" />
<view v-if="currentCardList.length">
<view v-for="(card, index) in currentCardList" :key="index" class="card-item"
:class="{ 'no-transition': disableTransition }" :style="getCardStyle(index)">
<view class="card-frame"
:class="{ 'no-border': card.comingSoon, 'card-frame--tappable': !card.comingSoon }"
@tap.stop="onCardFrameTap(index)">
<image class="card-image" :src="card.image" mode="aspectFill" />
<image v-if="card.comingSoon" class="coming-soon-badge"
src="/static/castlove/jinqingqidai.png" />
</view>
</view>
</view>
<!-- 空态:当前分类暂无卡片时显示 -->
<!-- <view v-else class="empty-state">
<image class="empty-state-icon" src="/static/castlove/jinqingqidai.png" mode="aspectFit" />
<text class="empty-state-title">{{ currentCategoryName }}敬请期待</text>
<text class="empty-state-desc">该分类正在打磨中,马上与您见面</text>
</view> -->
</view>
<view class="text-panel">
<view v-if="showMenu" class="text-panel">
<view class="arrow-btn arrow-up" @click="scrollUp">
<image class="arrow-icon" src="/static/castlove/jiantou.png" style="transform:rotate(180deg)" />
</view>
@ -36,6 +44,39 @@
<script>
export default {
// mall.vue
// ( navigateTo , onLoad URL )
props: {
// :star_card | badge | poster
type: {
type: String,
default: '',
},
},
watch: {
// , type
// immediate + handler , H5
type: {
immediate: true,
handler(val) {
this.applyType(val)
},
},
},
created() {
// type, mall.vue onLoad initialType
// "" watch
this.applyType(this.type)
},
onLoad(options) {
// ,()
this.showMenu = false
// Tab(square.vue / CastloveContent.vue) type,
// type :star_card | badge | poster
if (options && options.type) {
this.applyType(options.type)
}
},
onShow() {
try {
uni.hideToast()
@ -44,7 +85,18 @@ export default {
},
data() {
return {
//
// - (mall.vue )使, true
// - ,onLoad false
showMenu: true,
selectedCategoryIndex: 0,
// Tab type categoryList
// square.vue / CastloveContent.vue mainTabs type
categoryTypeMap: {
star_card: 0,
badge: 1,
poster: 2,
},
categoryList: [{ name: '星卡' }, { name: '吧唧' }, { name: '海报' }],
cardListMap: {
'星卡': [
@ -81,12 +133,23 @@ export default {
}
},
computed: {
currentCategoryName() {
return this.categoryList[this.selectedCategoryIndex]?.name || ''
},
currentCardList() {
const name = this.categoryList[this.selectedCategoryIndex]?.name
return this.cardListMap[name] || this.cardList
}
},
methods: {
// type
applyType(type) {
if (!type) return
const idx = this.categoryTypeMap[type]
if (typeof idx === 'number') {
this.selectedCategoryIndex = idx
}
},
selectCategory(index) {
this.selectedCategoryIndex = index
this.selectedIndex = 1
@ -344,4 +407,39 @@ export default {
.font-mid {
font-size: 30rpx;
}
.empty-state {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 80%;
padding: 40rpx 0;
pointer-events: none;
}
.empty-state-icon {
width: 200rpx;
height: 200rpx;
opacity: 0.85;
margin-bottom: 32rpx;
}
.empty-state-title {
color: #fff;
font-size: 36rpx;
font-weight: 600;
margin-bottom: 16rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.35);
}
.empty-state-desc {
color: rgba(255, 255, 255, 0.7);
font-size: 26rpx;
text-align: center;
}
</style>

View File

@ -1,66 +1,124 @@
<template>
<view class="page-container">
<CastloveContent />
<view class="page-container">
<view class="nav-back" @tap="goBack">
<text class="nav-back-icon"></text>
</view>
<!-- 蒙层 - 导航栏展开时显示 -->
<view v-if="navExpanded" class="nav-mask" @click="navExpanded = false"></view>
<CastloveContent :type="initialType" />
<BottomNav :activeTab="2" :isExpanded="navExpanded" @update:activeTab="handleTabChange" @update:isExpanded="navExpanded = $event" />
</view>
<!-- 蒙层 - 导航栏展开时显示 -->
<view
v-if="navExpanded"
class="nav-mask"
@click="navExpanded = false"
></view>
<BottomNav
:activeTab="2"
:isExpanded="navExpanded"
@update:activeTab="handleTabChange"
@update:isExpanded="navExpanded = $event"
/>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import BottomNav from "../components/BottomNav.vue";
import CastloveContent from "./craft-select.vue";
const navExpanded = ref(false);
// URL type,
// (Tab square.vue / CastloveContent.vue query )
const initialType = ref("star_card");
const handleTabChange = (newTab) => {
const routes = [
'/pages/ai-dazi/index',
'/pages/starbook/index',
'/pages/castlove/mall',
'/pages/starcity/index',
'/pages/square/square'
];
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]
});
}
if (newTab >= 0 && newTab < routes.length) {
uni.navigateTo({
url: routes[newTab],
});
}
};
const goBack = () => {
//
const pages = getCurrentPages();
if (pages.length > 1) {
//
uni.navigateBack();
} else {
// square
uni.reLaunch({
url: "/pages/square/square",
});
}
};
// uni-app :
onLoad((options) => {
if (options && options.type) {
initialType.value = options.type;
}
});
</script>
<style scoped>
.page-container {
position: relative;
width: 100vw;
height: 100vh;
min-height: 100vh;
overflow: hidden;
position: relative;
width: 100vw;
height: 100vh;
min-height: 100vh;
overflow: hidden;
}
.nav-back {
width: 80rpx;
height: 80rpx;
position: fixed;
top: 96rpx;
left: 32rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 12;
/* background: rgba(255,255,255,0.5);
border-radius: 50%; */
}
.nav-back-icon {
font-size: 48rpx;
font-weight: bold;
color: #fff;
}
.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;
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;
}
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>

View File

@ -312,19 +312,10 @@ const handleBannerClick = (banner) => {
// Tab -
const handleMainTabClick = (tab) => {
console.log('点击主Tab:', tab);
//
if (tab.type === 'star_card') {
uni.navigateTo({
url: `/pages/castlove/craft-select`
});
} else {
//
uni.showToast({
title: `${tab.name}功能开发中`,
icon: 'none',
duration: 1500
});
}
// mall (mall craft-select ,), type
uni.navigateTo({
url: `/pages/castlove/mall?type=${encodeURIComponent(tab.type)}`
});
};
//

View File

@ -628,11 +628,13 @@ defineExpose({
left: 0;
width: 100%;
height: 100%;
border-radius: 120rpx;
z-index: 0;
}
/* --- 上层:文字内容 --- */
.crystal-text-layer {
height: 100%;
position: relative;
z-index: 2;
display: flex;
@ -647,7 +649,6 @@ defineExpose({
rgba(51, 241, 255, 0) 178.07%
);
padding: 0 24rpx;
}
.balance-number {

View File

@ -1,6 +1,12 @@
<template>
<view class="creation-grid">
<view v-for="(item, index) in creationList" :key="item.id" class="creation-card" @click="handleCardClick(item)">
<view
v-for="item in creationList"
:key="item.id"
:ref="(el) => setCardRef(el, item)"
class="creation-card"
@click="handleCardClick(item)"
>
<image class="creation-image" :src="item.cover_image" mode="aspectFill"></image>
<view class="like-badge">
<view class="like-icon-wrapper">
@ -49,7 +55,7 @@ const props = defineProps({
isActive: { type: Boolean, default: true },
})
const emit = defineEmits(['cardClick', 'scroll'])
const emit = defineEmits(['cardClick', 'scroll', 'loaded'])
const creationList = ref([])
const cursor = ref('')
@ -65,6 +71,17 @@ const formatCount = (count) => {
return count.toString()
}
// DOM mapkey = item.id spotlight
const cardRefsMap = new Map()
const setCardRef = (el, item) => {
if (el && item && item.id != null) {
cardRefsMap.set(item.id, el)
} else if (item && item.id != null) {
cardRefsMap.delete(item.id)
}
}
const getCardRefs = () => Array.from(cardRefsMap.values())
const handleCardClick = (item) => {
emit('cardClick', item)
}
@ -102,6 +119,7 @@ const loadUsers = async () => {
console.error('[CreationGrid] 加载数据失败', e?.message ?? e)
} finally {
isLoading.value = false
emit('loaded', creationList.value.length)
}
}
@ -137,6 +155,7 @@ const loadMore = async () => {
console.error('[CreationGrid] 加载更多失败', e?.message ?? e)
} finally {
isLoading.value = false
emit('loaded', creationList.value.length)
}
}
@ -188,7 +207,7 @@ onUnmounted(() => {
uni.$off("assetLiked");
});
defineExpose({ loadMore })
defineExpose({ loadMore, getCardRefs })
</script>
<style scoped>

View File

@ -0,0 +1,151 @@
/**
* useSpotlight - 滚动 spotlight / 边缘渐隐
*
* 行为
* - opacity 等于元素在视口中可见的比例
* - 元素完全在视口内 1
* - 元素被滚出一半 0.5
* - 元素完全在视口外 0
* - 元素比视口高且完全覆盖视口 1
*
* 关键修复针对 app-plus
* - app-plus <scroll-view> 是原生 UIScrollViewWebView 内容本身不滚动
* getBoundingClientRect() 永远返回初始 layout 位置不随原生滚动更新
* 所以基于 rAF/setTimeout + getBoundingClientRect 的方案在 app-plus 上完全无效
* - uni.createSelectorQuery() 取位置 uni-app 官方 API会桥到原生层
* app-plus / H5 / 小程序上都能拿到当前可视位置
* - 异步结果用 reqId 防止乱序连续触发时丢弃旧结果
*
* 用法square.vue
* const { start, stop } = useSpotlight({ getElements })
* onMounted(start); onUnmounted(stop)
* // @scroll 事件也调一次 update(),作为双保险
*/
const SEEN_THRESHOLD = 0.5 // 透明度高于此值记为"首次出现"
export function useSpotlight(options = {}) {
const { getElements = () => [] } = options
const seenSet = typeof WeakSet !== "undefined" ? new WeakSet() : new Set()
const computeOpacity = (rect, vh) => {
if (!rect) return 1
if (rect.bottom <= 0 || rect.top >= vh) return 0
if (rect.height >= vh && rect.top <= 0 && rect.bottom >= vh) return 1
const visibleTop = Math.max(0, rect.top)
const visibleBottom = Math.min(vh, rect.bottom)
const visibleHeight = Math.max(0, visibleBottom - visibleTop)
const totalHeight = rect.height
if (totalHeight <= 0) return 1
return Math.min(1, visibleHeight / totalHeight)
}
// 给每个 ref 对应的 DOM 节点加 data-spotlight-id方便 SelectorQuery 结果映射回节点
const idToNode = new Map()
let nextId = 1
const tagNodes = () => {
const elements = getElements()
elements.forEach((ref) => {
if (!ref) return
const node = ref.$el || ref
if (!node || node.nodeType !== 1) return
if (node.dataset && node.dataset.spotlightId) return
const id = `sl${nextId++}`
node.setAttribute("data-spotlight-id", id)
idToNode.set(id, node)
})
}
// SelectorQuery 异步:连续触发时用 reqId 丢弃旧结果
let reqId = 0
const applyRects = (rects, vh) => {
if (!rects || !rects.length) return
rects.forEach((rect) => {
const id = rect && rect.dataset && rect.dataset.spotlightId
if (!id) return
const node = idToNode.get(id) || (typeof document !== "undefined" && document.querySelector(`[data-spotlight-id="${id}"]`))
if (!node) return
const opacity = computeOpacity(rect, vh)
try {
node.style.opacity = opacity.toFixed(3)
} catch (e) {}
if (opacity > SEEN_THRESHOLD && !seenSet.has(node)) {
seenSet.add(node)
try { node.classList.add("first-seen") } catch (e) {}
}
})
}
// 测高度
const getVH = () => {
try {
if (typeof window !== "undefined" && window.innerHeight) return window.innerHeight
} catch (e) {}
try {
const info = uni.getSystemInfoSync()
return info.windowHeight || info.screenHeight || 667
} catch (e) {
return 667
}
}
const update = () => {
if (typeof uni === "undefined" || typeof uni.createSelectorQuery !== "function") return
tagNodes()
if (!idToNode.size) return
const myReq = ++reqId
const vh = getVH()
try {
const query = uni.createSelectorQuery()
query.selectAll("[data-spotlight-id]").boundingClientRect()
query.exec((res) => {
if (myReq !== reqId) return // 旧请求,丢弃
if (!res || !res[0]) return
applyRects(res[0], vh)
})
} catch (e) {
// SelectorQuery 不可用时不做事
}
}
// setTimeout 递归轮询:兜底,@scroll 漏触发时仍能跑
let timerId = null
const tick = () => {
update()
timerId = setTimeout(tick, 50) // 50ms ≈ 20fpsSelectorQuery 开销小但不为零,频率不用太高
}
const start = () => {
if (timerId) return
timerId = setTimeout(tick, 50)
}
const stop = () => {
if (timerId) {
clearTimeout(timerId)
timerId = null
}
}
// 兼容:@scroll 触发时直接调一次双保险scroll 事件比 50ms 轮询更密)
const bindScroll = () => {
update()
}
return {
update,
bindScroll,
start,
stop,
isH5: typeof window !== "undefined" && typeof document !== "undefined",
}
}

View File

@ -45,13 +45,17 @@
<!-- 内容区域 -->
<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 class="banner-section">
<view ref="bannerSectionRef" class="banner-section">
<BannerCarousel
:bannerActivities="bannerActivities"
@activityClick="handleActivityClick"
@ -60,13 +64,14 @@
</view>
<ContentTabs
ref="contentTabsRef"
class="tabs"
:modelValue="activeContentTab"
@update:modelValue="activeContentTab = $event"
/>
<!-- 在线榜单区块 -->
<view class="hot-category-wrapper">
<view ref="hotCategoryRef" class="hot-category-wrapper">
<HotCategoryBlock
:dimension="activeContentTab"
@cardClick="handleCardClick"
@ -77,34 +82,38 @@
</view>
<!-- 区域二主Tab标签区星卡/吧唧/海报 -->
<view class="main-tab-section">
<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)"
>
<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 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 }"
:class="{ fixed: isFixed, 'just-fixed': justFixed }"
>
<scroll-view class="category-scroll" scroll-x :show-scrollbar="false">
<view
@ -118,6 +127,12 @@
</view>
</scroll-view>
</view>
<!-- fixed 时占位避免下方内容跳变 -->
<view
v-if="isFixed && categoryHeight > 0"
class="category-placeholder"
:style="{ height: categoryHeight + 'px' }"
></view>
<!-- 区域四创作网格列表 -->
<CreationGrid
@ -127,6 +142,7 @@
:category="activeCategoryTab"
:isActive="isActive"
@cardClick="handleCardClick"
@loaded="onGridLoaded"
ref="creationGridRef"
/>
</scroll-view>
@ -134,7 +150,7 @@
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
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";
@ -147,6 +163,7 @@ 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 ==========
@ -161,10 +178,88 @@ 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([
{
@ -313,6 +408,14 @@ const handleTabChange = (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;
@ -332,12 +435,26 @@ onMounted(() => {
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(() => {
@ -377,6 +494,7 @@ uni.$on("guide:openComponent", (componentName) => {
});
onUnmounted(() => {
stopSpotlight();
uni.$off("guide:openComponent");
});
</script>
@ -448,8 +566,15 @@ onUnmounted(() => {
opacity: 0.8;
}
.tab-icon {
.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);
}
@ -477,6 +602,11 @@ onUnmounted(() => {
padding: 16rpx 0;
}
.category-placeholder {
/* 仅占位,无内容;高度由内联 style 写入 */
margin-bottom: 24rpx;
}
.category-scroll {
white-space: nowrap;
}
@ -562,4 +692,84 @@ onUnmounted(() => {
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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB