feat:增加top3的动画
This commit is contained in:
parent
d4e26337de
commit
23862e36ae
@ -48,9 +48,9 @@ const onTop3DataLoaded = (items) => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.banner-carousel {
|
.banner-carousel {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
/* width: 100%; */
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
/* padding: 0 16rpx; */
|
padding: 0 16rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
/* top:16rpx; */
|
/* top:16rpx; */
|
||||||
}
|
}
|
||||||
@ -74,11 +74,11 @@ const onTop3DataLoaded = (items) => {
|
|||||||
|
|
||||||
.banner-image {
|
.banner-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 356rpx;
|
height: 376rpx;
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
position: relative;
|
position: relative;
|
||||||
bottom: 8rpx;
|
bottom: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner-overlay {
|
.banner-overlay {
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="creation-grid-wrapper">
|
<view class="creation-grid-wrapper">
|
||||||
<!-- 区域 A:主Tab(星卡/吧唧/海报) -->
|
<!-- 区域 A:主Tab(星卡/吧唧/海报) -->
|
||||||
<view ref="mainTabsRef" class="main-tab-section">
|
<view class="main-tab-section">
|
||||||
<view
|
<view
|
||||||
v-for="(tab, index) in mainTabs"
|
v-for="(tab, index) in mainTabs"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="tab-item"
|
class="tab-item"
|
||||||
:style="{ '--tab-delay': index * 0.06 + 's' }"
|
|
||||||
@click="handleMainTabClick(tab)"
|
@click="handleMainTabClick(tab)"
|
||||||
>
|
>
|
||||||
<view class="tab-icon-wrap">
|
<view class="tab-icon-wrap">
|
||||||
@ -31,7 +30,7 @@
|
|||||||
ref="categoryRef"
|
ref="categoryRef"
|
||||||
id="category-section"
|
id="category-section"
|
||||||
class="category-section"
|
class="category-section"
|
||||||
:class="{ fixed: isFixed, 'just-fixed': justFixed }"
|
:class="{ fixed: isFixed }"
|
||||||
>
|
>
|
||||||
<scroll-view class="category-scroll" scroll-x :show-scrollbar="false">
|
<scroll-view class="category-scroll" scroll-x :show-scrollbar="false">
|
||||||
<view
|
<view
|
||||||
@ -57,7 +56,6 @@
|
|||||||
<view
|
<view
|
||||||
v-for="item in creationList"
|
v-for="item in creationList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:ref="(el) => setCardRef(el, item)"
|
|
||||||
class="creation-card"
|
class="creation-card"
|
||||||
@click="handleCardClick(item)"
|
@click="handleCardClick(item)"
|
||||||
>
|
>
|
||||||
@ -163,8 +161,7 @@ const categories = [
|
|||||||
{ label: '海报', value: 'poster' },
|
{ label: '海报', value: 'poster' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// ========== 模板 ref(暴露给父组件 spotlight 系统 + 占位元素) ==========
|
// ========== 模板 ref(categoryRef 用于内部测量分类标签位置) ==========
|
||||||
const mainTabsRef = ref(null)
|
|
||||||
const categoryRef = ref(null)
|
const categoryRef = ref(null)
|
||||||
|
|
||||||
// ========== 滚动 fixed 行为(内部状态) ==========
|
// ========== 滚动 fixed 行为(内部状态) ==========
|
||||||
@ -172,16 +169,6 @@ const categoryOffsetTop = ref(null)
|
|||||||
const categoryHeight = ref(0)
|
const categoryHeight = ref(0)
|
||||||
const fixedTopPx = ref(50) // 近似对应 CSS top: 96rpx
|
const fixedTopPx = ref(50) // 近似对应 CSS top: 96rpx
|
||||||
const isFixed = ref(false)
|
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()
|
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) => {
|
const handleCardClick = (item) => {
|
||||||
emit('cardClick', item)
|
emit('cardClick', item)
|
||||||
}
|
}
|
||||||
@ -381,13 +358,10 @@ onUnmounted(() => {
|
|||||||
uni.$off('assetLiked')
|
uni.$off('assetLiked')
|
||||||
})
|
})
|
||||||
|
|
||||||
// ========== 对外暴露(仅 spotlight / 滚动需要的方法) ==========
|
// ========== 对外暴露(仅滚动需要的方法) ==========
|
||||||
defineExpose({
|
defineExpose({
|
||||||
loadMore,
|
loadMore,
|
||||||
getCardRefs,
|
|
||||||
mainTabsRef,
|
|
||||||
categoryRef,
|
categoryRef,
|
||||||
categoryHeight,
|
|
||||||
updateScroll,
|
updateScroll,
|
||||||
update,
|
update,
|
||||||
remeasure,
|
remeasure,
|
||||||
|
|||||||
@ -48,6 +48,7 @@ function handleClick() {
|
|||||||
top: 400rpx;
|
top: 400rpx;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
|
animation: podium-float-center 3s ease-in-out infinite;
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -69,12 +70,17 @@ function handleClick() {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.top-label{
|
||||||
|
left: 58%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TOP 2: 左上 */
|
/* TOP 2: 左上 */
|
||||||
.podium-2 {
|
.podium-2 {
|
||||||
top: 184rpx;
|
top: 184rpx;
|
||||||
left: -96rpx;
|
left: -96rpx;
|
||||||
|
animation: podium-float 3.4s ease-in-out infinite;
|
||||||
|
animation-delay: -1.2s;
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -101,6 +107,8 @@ function handleClick() {
|
|||||||
.podium-3 {
|
.podium-3 {
|
||||||
top: 152rpx;
|
top: 152rpx;
|
||||||
right: -96rpx;
|
right: -96rpx;
|
||||||
|
animation: podium-float 3.2s ease-in-out infinite;
|
||||||
|
animation-delay: -2s;
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -113,7 +121,7 @@ function handleClick() {
|
|||||||
transform: scale(0.85);
|
transform: scale(0.85);
|
||||||
}
|
}
|
||||||
.cover-wrap {
|
.cover-wrap {
|
||||||
width: 176rpx;
|
width: 160rpx;
|
||||||
height: 180rpx;
|
height: 180rpx;
|
||||||
bottom: 72rpx;
|
bottom: 72rpx;
|
||||||
.podium-frame {
|
.podium-frame {
|
||||||
@ -168,4 +176,25 @@ function handleClick() {
|
|||||||
z-index: 7;
|
z-index: 7;
|
||||||
padding: 0 16rpx;
|
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>
|
</style>
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
:style="ringItemStyle(p)"
|
:style="ringItemStyle(p)"
|
||||||
@click="handleClick(items[i])"
|
@click="handleClick(items[i])"
|
||||||
>
|
>
|
||||||
<!-- 相框(最底层) -->
|
<!-- 相框(中间层) -->
|
||||||
<view class="top-label">{{ formatLabel(p.rank) }}</view>
|
<view class="top-label">{{ formatLabel(p.rank) }}</view>
|
||||||
<image class="ring-frame" :src="ringFrameSrc(p.rank)" mode="aspectFit" />
|
<image class="ring-frame" :src="ringFrameSrc(p.rank)" mode="aspectFit" />
|
||||||
<image
|
<image
|
||||||
@ -35,6 +35,13 @@
|
|||||||
:src="items[i]?.cover_url || items[i]?.cover_image || ''"
|
:src="items[i]?.cover_url || items[i]?.cover_image || ''"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
|
<!-- 底座(最底层) -->
|
||||||
|
<image
|
||||||
|
v-if="baseSrc(p.rank)"
|
||||||
|
class="base-image"
|
||||||
|
:src="baseSrc(p.rank)"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@ -91,6 +98,14 @@ function ringFrameSrc(rank) {
|
|||||||
return `/static/square/galaxy/LV${rank}.png`;
|
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) {
|
function handleClick(item) {
|
||||||
if (item) emit("cardClick", item);
|
if (item) emit("cardClick", item);
|
||||||
}
|
}
|
||||||
@ -125,6 +140,7 @@ function handleClick(item) {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
/* display: flex; */
|
/* display: flex; */
|
||||||
/* flex-direction: column; */
|
/* flex-direction: column; */
|
||||||
|
will-change: transform;
|
||||||
animation: orbit 36s linear infinite;
|
animation: orbit 36s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,6 +185,16 @@ function handleClick(item) {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
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>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -217,10 +243,10 @@ function handleClick(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 可访问性:减少动画 */
|
/* 可访问性:减少动画 */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
/* @media (prefers-reduced-motion: reduce) {
|
||||||
.ring-item,
|
.ring-item,
|
||||||
.crown {
|
.crown {
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
</style>
|
</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
|
<scroll-view
|
||||||
class="content-wrapper"
|
class="content-wrapper"
|
||||||
:class="{ 'spotlight-ready': isH5 }"
|
|
||||||
scroll-y
|
scroll-y
|
||||||
:show-scrollbar="false"
|
:show-scrollbar="false"
|
||||||
:bounce="false"
|
:bounce="false"
|
||||||
@scroll="onScroll"
|
@scroll="onScroll"
|
||||||
@touchstart="onTouchStart"
|
|
||||||
@touchmove="onTouchMove"
|
|
||||||
@scrolltolower="handleScrollToLower"
|
@scrolltolower="handleScrollToLower"
|
||||||
>
|
>
|
||||||
<!-- 区域一:顶部运营轮播图 -->
|
<!-- 区域一:顶部运营轮播图 -->
|
||||||
<view ref="bannerSectionRef" class="banner-section">
|
<view class="banner-section">
|
||||||
<BannerCarousel
|
<BannerCarousel
|
||||||
:bannerActivities="bannerActivities"
|
:bannerActivities="bannerActivities"
|
||||||
:banners="banners"
|
:banners="banners"
|
||||||
@ -66,7 +63,6 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<ContentTabs
|
<ContentTabs
|
||||||
ref="contentTabsRef"
|
|
||||||
class="tabs"
|
class="tabs"
|
||||||
:modelValue="activeContentTab"
|
:modelValue="activeContentTab"
|
||||||
@update:modelValue="activeContentTab = $event"
|
@update:modelValue="activeContentTab = $event"
|
||||||
@ -80,7 +76,6 @@
|
|||||||
<!-- 在线榜单区块 - 仅在 星榜 时显示 -->
|
<!-- 在线榜单区块 - 仅在 星榜 时显示 -->
|
||||||
<view
|
<view
|
||||||
v-if="activeContentTab === 'xingbang'"
|
v-if="activeContentTab === 'xingbang'"
|
||||||
ref="hotCategoryRef"
|
|
||||||
class="hot-category-wrapper"
|
class="hot-category-wrapper"
|
||||||
>
|
>
|
||||||
<HotCategoryBlock @cardClick="handleCardClick" />
|
<HotCategoryBlock @cardClick="handleCardClick" />
|
||||||
@ -90,7 +85,6 @@
|
|||||||
<CreationGrid
|
<CreationGrid
|
||||||
v-if="activeContentTab === 'guangchang'"
|
v-if="activeContentTab === 'guangchang'"
|
||||||
@cardClick="handleCardClick"
|
@cardClick="handleCardClick"
|
||||||
@loaded="onGridLoaded"
|
|
||||||
ref="creationGridRef"
|
ref="creationGridRef"
|
||||||
/>
|
/>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
@ -98,7 +92,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onUnmounted, nextTick } from "vue";
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import Header from "../components/Header.vue";
|
import Header from "../components/Header.vue";
|
||||||
@ -112,7 +106,6 @@ import CreationGrid from "./components/CreationGrid.vue";
|
|||||||
import StarGalaxy from "./components/StarGalaxy/index.vue";
|
import StarGalaxy from "./components/StarGalaxy/index.vue";
|
||||||
// import { clearSubStepProgress, shouldShowGuideStartModal } from '@/utils/guideConfig.js'
|
// import { clearSubStepProgress, shouldShowGuideStartModal } from '@/utils/guideConfig.js'
|
||||||
import { useBanner } from "./composables/useBanner.js";
|
import { useBanner } from "./composables/useBanner.js";
|
||||||
import { useSpotlight } from "./composables/useSpotlight.js";
|
|
||||||
import { doubleTapLike } from "@/utils/likeHelper.js";
|
import { doubleTapLike } from "@/utils/likeHelper.js";
|
||||||
|
|
||||||
// ========== Store & User Info ==========
|
// ========== Store & User Info ==========
|
||||||
@ -128,65 +121,13 @@ const creationGridRef = ref(null);
|
|||||||
const cardTapTimers = {};
|
const cardTapTimers = {};
|
||||||
const likingMap = ref({});
|
const likingMap = ref({});
|
||||||
|
|
||||||
// ========== 各区块的 template ref ==========
|
// 统一 scroll 处理:驱动 CreationGrid 切 fixed
|
||||||
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
|
|
||||||
const onScroll = (e) => {
|
const onScroll = (e) => {
|
||||||
bindScroll(); // 兼容路径:scroll 事件触发时也跑一次(主驱动是 rAF 轮询)
|
|
||||||
const scrollTop = (e && e.detail && e.detail.scrollTop) || 0;
|
const scrollTop = (e && e.detail && e.detail.scrollTop) || 0;
|
||||||
// 通知 CreationGrid:根据 scrollTop 决定分类标签是否切到 fixed
|
// 通知 CreationGrid:根据 scrollTop 决定分类标签是否切到 fixed
|
||||||
creationGridRef.value?.updateScroll?.(scrollTop);
|
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 ==========
|
// ========== Composables ==========
|
||||||
const { bannerActivities, banners, loadBannerActivities, loadBanners } =
|
const { bannerActivities, banners, loadBannerActivities, loadBanners } =
|
||||||
useBanner();
|
useBanner();
|
||||||
@ -302,19 +243,10 @@ onMounted(() => {
|
|||||||
resetSquare();
|
resetSquare();
|
||||||
loadBannerActivities();
|
loadBannerActivities();
|
||||||
loadBanners();
|
loadBanners();
|
||||||
|
|
||||||
// 等首屏 DOM 稳定后,启动 spotlight 轮询
|
|
||||||
// 分类标签的位置/高度测量已迁入 CreationGrid 内部(onMounted 自动测)
|
|
||||||
nextTick(() => {
|
|
||||||
startSpotlight();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
activeContentTab.value = "xinghe";
|
activeContentTab.value = "xinghe";
|
||||||
nextTick(() => {
|
|
||||||
setTimeout(update, 80);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// onLoad((options) => {
|
// onLoad((options) => {
|
||||||
@ -350,7 +282,6 @@ uni.$on("guide:openComponent", (componentName) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopSpotlight();
|
|
||||||
uni.$off("guide:openComponent");
|
uni.$off("guide:openComponent");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -412,9 +343,7 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 区域二(主Tab)+ 区域三(分类标签)+ 占位 + 回弹动效
|
/* 区域二(主Tab)+ 区域三(分类标签)+ 占位 + 回弹动效
|
||||||
已迁入 CreationGrid 组件内部(创建于 components/CreationGrid.vue)
|
已迁入 CreationGrid 组件内部(创建于 components/CreationGrid.vue)。 */
|
||||||
这里的 .main-tab-section / .category-section / .tab-item / 等选择器被保留
|
|
||||||
是因为 .spotlight-ready 选择器需要它们,CSS specificity 才能匹配(容器外层应用 spotlight-ready)。 */
|
|
||||||
|
|
||||||
/* 热门分类区块 */
|
/* 热门分类区块 */
|
||||||
.hot-category-wrapper {
|
.hot-category-wrapper {
|
||||||
@ -445,84 +374,4 @@ onUnmounted(() => {
|
|||||||
opacity: 1;
|
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>
|
</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