feat:增加top3的动画
This commit is contained in:
parent
d4e26337de
commit
23862e36ae
@ -48,9 +48,9 @@ const onTop3DataLoaded = (items) => {
|
||||
<style scoped>
|
||||
.banner-carousel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
/* width: 100%; */
|
||||
z-index: 100;
|
||||
/* padding: 0 16rpx; */
|
||||
padding: 0 16rpx;
|
||||
box-sizing: border-box;
|
||||
/* top:16rpx; */
|
||||
}
|
||||
@ -74,11 +74,11 @@ const onTop3DataLoaded = (items) => {
|
||||
|
||||
.banner-image {
|
||||
width: 100%;
|
||||
height: 356rpx;
|
||||
height: 376rpx;
|
||||
display: block;
|
||||
border-radius: 24rpx;
|
||||
position: relative;
|
||||
bottom: 8rpx;
|
||||
bottom: 16rpx;
|
||||
}
|
||||
|
||||
.banner-overlay {
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
<template>
|
||||
<view class="creation-grid-wrapper">
|
||||
<!-- 区域 A:主Tab(星卡/吧唧/海报) -->
|
||||
<view ref="mainTabsRef" class="main-tab-section">
|
||||
<view 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">
|
||||
@ -31,7 +30,7 @@
|
||||
ref="categoryRef"
|
||||
id="category-section"
|
||||
class="category-section"
|
||||
:class="{ fixed: isFixed, 'just-fixed': justFixed }"
|
||||
:class="{ fixed: isFixed }"
|
||||
>
|
||||
<scroll-view class="category-scroll" scroll-x :show-scrollbar="false">
|
||||
<view
|
||||
@ -57,7 +56,6 @@
|
||||
<view
|
||||
v-for="item in creationList"
|
||||
:key="item.id"
|
||||
:ref="(el) => setCardRef(el, item)"
|
||||
class="creation-card"
|
||||
@click="handleCardClick(item)"
|
||||
>
|
||||
@ -163,8 +161,7 @@ const categories = [
|
||||
{ label: '海报', value: 'poster' },
|
||||
]
|
||||
|
||||
// ========== 模板 ref(暴露给父组件 spotlight 系统 + 占位元素) ==========
|
||||
const mainTabsRef = ref(null)
|
||||
// ========== 模板 ref(categoryRef 用于内部测量分类标签位置) ==========
|
||||
const categoryRef = ref(null)
|
||||
|
||||
// ========== 滚动 fixed 行为(内部状态) ==========
|
||||
@ -172,16 +169,6 @@ const categoryOffsetTop = ref(null)
|
||||
const categoryHeight = ref(0)
|
||||
const fixedTopPx = ref(50) // 近似对应 CSS top: 96rpx
|
||||
const isFixed = ref(false)
|
||||
const justFixed = ref(false)
|
||||
|
||||
// 状态变化瞬间加 justFixed class,动画播完摘掉(产生回弹效果)
|
||||
watch(isFixed, (val) => {
|
||||
if (!val) return
|
||||
justFixed.value = true
|
||||
setTimeout(() => {
|
||||
justFixed.value = false
|
||||
}, 450)
|
||||
})
|
||||
|
||||
// ========== 内部事件处理(不再冒泡给父组件) ==========
|
||||
|
||||
@ -232,16 +219,6 @@ const formatCount = (count) => {
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@ -381,13 +358,10 @@ onUnmounted(() => {
|
||||
uni.$off('assetLiked')
|
||||
})
|
||||
|
||||
// ========== 对外暴露(仅 spotlight / 滚动需要的方法) ==========
|
||||
// ========== 对外暴露(仅滚动需要的方法) ==========
|
||||
defineExpose({
|
||||
loadMore,
|
||||
getCardRefs,
|
||||
mainTabsRef,
|
||||
categoryRef,
|
||||
categoryHeight,
|
||||
updateScroll,
|
||||
update,
|
||||
remeasure,
|
||||
|
||||
@ -48,6 +48,7 @@ function handleClick() {
|
||||
top: 400rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
animation: podium-float-center 3s ease-in-out infinite;
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@ -69,12 +70,17 @@ function handleClick() {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.top-label{
|
||||
left: 58%;
|
||||
}
|
||||
}
|
||||
|
||||
/* TOP 2: 左上 */
|
||||
.podium-2 {
|
||||
top: 184rpx;
|
||||
left: -96rpx;
|
||||
animation: podium-float 3.4s ease-in-out infinite;
|
||||
animation-delay: -1.2s;
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@ -101,6 +107,8 @@ function handleClick() {
|
||||
.podium-3 {
|
||||
top: 152rpx;
|
||||
right: -96rpx;
|
||||
animation: podium-float 3.2s ease-in-out infinite;
|
||||
animation-delay: -2s;
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@ -113,7 +121,7 @@ function handleClick() {
|
||||
transform: scale(0.85);
|
||||
}
|
||||
.cover-wrap {
|
||||
width: 176rpx;
|
||||
width: 160rpx;
|
||||
height: 180rpx;
|
||||
bottom: 72rpx;
|
||||
.podium-frame {
|
||||
@ -168,4 +176,25 @@ function handleClick() {
|
||||
z-index: 7;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
/* 上下浮动动画:podium-1 需保留 translateX(-50%) 居中,单独一组 keyframes */
|
||||
@keyframes podium-float-center {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-50%) translateY(-12rpx);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes podium-float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10rpx);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
:style="ringItemStyle(p)"
|
||||
@click="handleClick(items[i])"
|
||||
>
|
||||
<!-- 相框(最底层) -->
|
||||
<!-- 相框(中间层) -->
|
||||
<view class="top-label">{{ formatLabel(p.rank) }}</view>
|
||||
<image class="ring-frame" :src="ringFrameSrc(p.rank)" mode="aspectFit" />
|
||||
<image
|
||||
@ -35,6 +35,13 @@
|
||||
:src="items[i]?.cover_url || items[i]?.cover_image || ''"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<!-- 底座(最底层) -->
|
||||
<image
|
||||
v-if="baseSrc(p.rank)"
|
||||
class="base-image"
|
||||
:src="baseSrc(p.rank)"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@ -91,6 +98,14 @@ function ringFrameSrc(rank) {
|
||||
return `/static/square/galaxy/LV${rank}.png`;
|
||||
}
|
||||
|
||||
function baseSrc(rank) {
|
||||
// 不同名次区间使用不同的底座:4-6 → dizuo1, 7-9 → dizuo2, 10-12 → dizuo3
|
||||
if (rank >= 4 && rank <= 6) return "/static/square/galaxy/dizuo1.png";
|
||||
if (rank >= 7 && rank <= 9) return "/static/square/galaxy/dizuo2.png";
|
||||
if (rank >= 10 && rank <= 12) return "/static/square/galaxy/dizuo3.png";
|
||||
return "";
|
||||
}
|
||||
|
||||
function handleClick(item) {
|
||||
if (item) emit("cardClick", item);
|
||||
}
|
||||
@ -125,6 +140,7 @@ function handleClick(item) {
|
||||
cursor: pointer;
|
||||
/* display: flex; */
|
||||
/* flex-direction: column; */
|
||||
will-change: transform;
|
||||
animation: orbit 36s linear infinite;
|
||||
}
|
||||
|
||||
@ -169,6 +185,16 @@ function handleClick(item) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.base-image {
|
||||
position: absolute;
|
||||
bottom: -24rpx;
|
||||
left: -8rpx;
|
||||
width: 96rpx; /* 比 cover (84rpx) 略宽,呈现"承托"感 */
|
||||
height: 32rpx;
|
||||
z-index: 0; /* 位于 cover 之下 */
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
@ -217,10 +243,10 @@ function handleClick(item) {
|
||||
}
|
||||
|
||||
/* 可访问性:减少动画 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
/* @media (prefers-reduced-motion: reduce) {
|
||||
.ring-item,
|
||||
.crown {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
} */
|
||||
</style>
|
||||
|
||||
@ -1,156 +0,0 @@
|
||||
/**
|
||||
* useSpotlight - 滚动 spotlight / 边缘渐隐
|
||||
*
|
||||
* 行为:
|
||||
* - opacity 等于「元素在视口中可见的比例」
|
||||
* - 元素完全在视口内 → 1
|
||||
* - 元素被滚出一半 → 0.5
|
||||
* - 元素完全在视口外 → 0
|
||||
* - 元素比视口高、且完全覆盖视口 → 1
|
||||
*
|
||||
* 关键修复(针对 app-plus):
|
||||
* - app-plus 的 <scroll-view> 是原生 UIScrollView,WebView 内容本身不滚动。
|
||||
* 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(),作为双保险
|
||||
*/
|
||||
import { getCurrentInstance } from 'vue'
|
||||
|
||||
const SEEN_THRESHOLD = 0.5 // 透明度高于此值记为"首次出现"
|
||||
|
||||
export function useSpotlight(options = {}) {
|
||||
const { getElements = () => [] } = options
|
||||
|
||||
// app-plus / Vue3 组件内 createSelectorQuery 必须挂 in(public proxy),否则 selectAll 在 app 端返回空、opacity 从不生效
|
||||
const pageProxy = getCurrentInstance()?.proxy || null
|
||||
|
||||
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 = pageProxy ? uni.createSelectorQuery().in(pageProxy) : 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 ≈ 20fps,SelectorQuery 开销小但不为零,频率不用太高
|
||||
}
|
||||
|
||||
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",
|
||||
}
|
||||
}
|
||||
@ -45,17 +45,14 @@
|
||||
<!-- 内容区域 -->
|
||||
<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">
|
||||
<view class="banner-section">
|
||||
<BannerCarousel
|
||||
:bannerActivities="bannerActivities"
|
||||
:banners="banners"
|
||||
@ -66,7 +63,6 @@
|
||||
</view>
|
||||
|
||||
<ContentTabs
|
||||
ref="contentTabsRef"
|
||||
class="tabs"
|
||||
:modelValue="activeContentTab"
|
||||
@update:modelValue="activeContentTab = $event"
|
||||
@ -80,7 +76,6 @@
|
||||
<!-- 在线榜单区块 - 仅在 星榜 时显示 -->
|
||||
<view
|
||||
v-if="activeContentTab === 'xingbang'"
|
||||
ref="hotCategoryRef"
|
||||
class="hot-category-wrapper"
|
||||
>
|
||||
<HotCategoryBlock @cardClick="handleCardClick" />
|
||||
@ -90,7 +85,6 @@
|
||||
<CreationGrid
|
||||
v-if="activeContentTab === 'guangchang'"
|
||||
@cardClick="handleCardClick"
|
||||
@loaded="onGridLoaded"
|
||||
ref="creationGridRef"
|
||||
/>
|
||||
</scroll-view>
|
||||
@ -98,7 +92,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onUnmounted, nextTick } from "vue";
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { useStore } from "vuex";
|
||||
import Header from "../components/Header.vue";
|
||||
@ -112,7 +106,6 @@ import CreationGrid from "./components/CreationGrid.vue";
|
||||
import StarGalaxy from "./components/StarGalaxy/index.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 ==========
|
||||
@ -128,65 +121,13 @@ const creationGridRef = ref(null);
|
||||
const cardTapTimers = {};
|
||||
const likingMap = ref({});
|
||||
|
||||
// ========== 各区块的 template ref ==========
|
||||
const bannerSectionRef = ref(null);
|
||||
const contentTabsRef = ref(null);
|
||||
const hotCategoryRef = ref(null);
|
||||
// mainTabsRef / categoryRef 已迁入 CreationGrid 内部(通过 creationGridRef.value.mainTabsRef / categoryRef 拿)
|
||||
// - 4 个区块
|
||||
// - CreationGrid 里的所有卡片(通过 defineExpose 暴露的 getCardRefs 拿到)
|
||||
const allSpotlightRefs = () => {
|
||||
const sections = [
|
||||
bannerSectionRef.value,
|
||||
contentTabsRef.value?.$el || contentTabsRef.value,
|
||||
// hotCategoryRef.value,
|
||||
creationGridRef.value?.mainTabsRef?.value,
|
||||
creationGridRef.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 + 驱动 CreationGrid 切 fixed
|
||||
// 统一 scroll 处理:驱动 CreationGrid 切 fixed
|
||||
const onScroll = (e) => {
|
||||
bindScroll(); // 兼容路径:scroll 事件触发时也跑一次(主驱动是 rAF 轮询)
|
||||
const scrollTop = (e && e.detail && e.detail.scrollTop) || 0;
|
||||
// 通知 CreationGrid:根据 scrollTop 决定分类标签是否切到 fixed
|
||||
creationGridRef.value?.updateScroll?.(scrollTop);
|
||||
};
|
||||
|
||||
// 移动端 rAF 在用户滚动时会被节流,opacity 跟不上。
|
||||
// 用 touchstart / touchmove 同步触发 update(),绕开 rAF 节流。
|
||||
const onTouchStart = () => {
|
||||
update();
|
||||
};
|
||||
const onTouchMove = () => {
|
||||
update();
|
||||
};
|
||||
|
||||
// tab 变化 → 新内容可能进入视口,重做一次 spotlight 计算
|
||||
watch(activeContentTab, () => {
|
||||
nextTick(() => setTimeout(update, 50));
|
||||
});
|
||||
|
||||
// 卡片加载到位 → 重新算一次 spotlight(卡片异步加载,不在初始 200ms update 范围内)
|
||||
const onGridLoaded = (count) => {
|
||||
if (count > 0) {
|
||||
nextTick(() => update());
|
||||
}
|
||||
};
|
||||
|
||||
// ========== Composables ==========
|
||||
const { bannerActivities, banners, loadBannerActivities, loadBanners } =
|
||||
useBanner();
|
||||
@ -302,19 +243,10 @@ onMounted(() => {
|
||||
resetSquare();
|
||||
loadBannerActivities();
|
||||
loadBanners();
|
||||
|
||||
// 等首屏 DOM 稳定后,启动 spotlight 轮询
|
||||
// 分类标签的位置/高度测量已迁入 CreationGrid 内部(onMounted 自动测)
|
||||
nextTick(() => {
|
||||
startSpotlight();
|
||||
});
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
activeContentTab.value = "xinghe";
|
||||
nextTick(() => {
|
||||
setTimeout(update, 80);
|
||||
});
|
||||
});
|
||||
|
||||
// onLoad((options) => {
|
||||
@ -350,7 +282,6 @@ uni.$on("guide:openComponent", (componentName) => {
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopSpotlight();
|
||||
uni.$off("guide:openComponent");
|
||||
});
|
||||
</script>
|
||||
@ -412,9 +343,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
/* 区域二(主Tab)+ 区域三(分类标签)+ 占位 + 回弹动效
|
||||
已迁入 CreationGrid 组件内部(创建于 components/CreationGrid.vue)
|
||||
这里的 .main-tab-section / .category-section / .tab-item / 等选择器被保留
|
||||
是因为 .spotlight-ready 选择器需要它们,CSS specificity 才能匹配(容器外层应用 spotlight-ready)。 */
|
||||
已迁入 CreationGrid 组件内部(创建于 components/CreationGrid.vue)。 */
|
||||
|
||||
/* 热门分类区块 */
|
||||
.hot-category-wrapper {
|
||||
@ -445,84 +374,4 @@ 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 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>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
Loading…
Reference in New Issue
Block a user