feat: 修改square组件的页面
This commit is contained in:
parent
4f3bb7e3ab
commit
dc9f1a7b1e
@ -1,48 +1,115 @@
|
||||
<template>
|
||||
<view class="creation-grid">
|
||||
<view class="creation-grid-wrapper">
|
||||
<!-- 区域 A:主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>
|
||||
|
||||
<!-- 区域 B:分类标签 -->
|
||||
<view
|
||||
v-for="item in creationList"
|
||||
:key="item.id"
|
||||
:ref="(el) => setCardRef(el, item)"
|
||||
class="creation-card"
|
||||
@click="handleCardClick(item)"
|
||||
ref="categoryRef"
|
||||
id="category-section"
|
||||
class="category-section"
|
||||
:class="{ fixed: isFixed, 'just-fixed': justFixed }"
|
||||
>
|
||||
<image class="creation-image" :src="item.cover_image" mode="aspectFill"></image>
|
||||
<view class="like-badge">
|
||||
<view class="like-icon-wrapper">
|
||||
<image class="like-icon"
|
||||
:src="item.is_liked ? '/static/icon/heart-icon.png' : '/static/icon/heart-icon-false.png'" mode="aspectFit">
|
||||
</image>
|
||||
<text class="like-count">{{ formatCount(item.like_count) }}</text>
|
||||
<scroll-view class="category-scroll" scroll-x :show-scrollbar="false">
|
||||
<view
|
||||
v-for="(category, index) in categories"
|
||||
:key="index"
|
||||
class="category-item"
|
||||
:class="{ active: category === category.value }"
|
||||
@click="handleCategoryChange(category.value)"
|
||||
>
|
||||
<text class="category-text">{{ category.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="wf-like-wave wf-like-wave-outer" :class="{ 'wf-like-wave-active': likingMap[item.id] }" />
|
||||
<view class="wf-like-wave wf-like-wave-inner" :class="{ 'wf-like-wave-active': likingMap[item.id] }" />
|
||||
<view class="creation-info">
|
||||
<view class="creation-id">
|
||||
<text class="id-text">链上编号: #{{ item.certificate_id }}</text>
|
||||
</view>
|
||||
<view class="creation-meta">
|
||||
<view class="creator-info">
|
||||
<image class="creator-avatar" :src="item.creator_avatar" mode="aspectFill"></image>
|
||||
<text class="creator-name">{{ item.creator_name }}</text>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- fixed 时占位,避免下方内容跳变 -->
|
||||
<view
|
||||
v-if="isFixed && categoryHeight > 0"
|
||||
class="category-placeholder"
|
||||
:style="{ height: categoryHeight + 'px' }"
|
||||
></view>
|
||||
|
||||
<!-- 区域 C:创作网格列表(保留原功能) -->
|
||||
<view class="creation-grid">
|
||||
<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">
|
||||
<image
|
||||
class="like-icon"
|
||||
:src="item.is_liked ? '/static/icon/heart-icon.png' : '/static/icon/heart-icon-false.png'"
|
||||
mode="aspectFit"
|
||||
>
|
||||
</image>
|
||||
<text class="like-count">{{ formatCount(item.like_count) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="wf-like-wave wf-like-wave-outer"
|
||||
:class="{ 'wf-like-wave-active': likingMap[item.id] }"
|
||||
/>
|
||||
<view
|
||||
class="wf-like-wave wf-like-wave-inner"
|
||||
:class="{ 'wf-like-wave-active': likingMap[item.id] }"
|
||||
/>
|
||||
<view class="creation-info">
|
||||
<view class="creation-id">
|
||||
<text class="id-text">链上编号: #{{ item.certificate_id }}</text>
|
||||
</view>
|
||||
<view class="creation-meta">
|
||||
<view class="creator-info">
|
||||
<image
|
||||
class="creator-avatar"
|
||||
:src="item.creator_avatar"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<text class="creator-name">{{ item.creator_name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="isLoading" class="loading-more">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
<view v-if="noMore && creationList.length > 0" class="no-more">
|
||||
<text class="no-more-text">没有更多了</text>
|
||||
<view v-if="isLoading" class="loading-more">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
<view v-if="noMore && creationList.length > 0" class="no-more">
|
||||
<text class="no-more-text">没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { getInspirationFlowApi } from '@/utils/api.js'
|
||||
|
||||
@ -55,7 +122,7 @@ const props = defineProps({
|
||||
isActive: { type: Boolean, default: true },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['cardClick', 'scroll', 'loaded'])
|
||||
const emit = defineEmits(['cardClick', 'scroll', 'loaded', 'mainTabClick', 'categoryChange'])
|
||||
|
||||
const creationList = ref([])
|
||||
const cursor = ref('')
|
||||
@ -64,6 +131,100 @@ const noMore = ref(false)
|
||||
let isComponentMounted = false
|
||||
const likingMap = ref({});
|
||||
|
||||
// ========== 区域 A:主Tab 配置(内化) ==========
|
||||
const mainTabs = [
|
||||
{
|
||||
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,
|
||||
},
|
||||
]
|
||||
|
||||
// ========== 区域 B:分类配置(内化) ==========
|
||||
const categories = [
|
||||
{ label: '热门作品', value: 'hot' },
|
||||
{ label: '最新作品', value: 'new' },
|
||||
{ label: '星卡', value: 'star_card' },
|
||||
{ label: '吧唧', value: 'badge' },
|
||||
{ label: '海报', value: 'poster' },
|
||||
]
|
||||
|
||||
// ========== 模板 ref(expose 给父组件 spotlight 系统 + 占位元素) ==========
|
||||
const mainTabsRef = ref(null)
|
||||
const categoryRef = ref(null)
|
||||
|
||||
// ========== 滚动 fixed 行为(内部状态) ==========
|
||||
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)
|
||||
})
|
||||
|
||||
// ========== 内部事件 ==========
|
||||
const handleMainTabClick = (tab) => {
|
||||
emit('mainTabClick', tab)
|
||||
}
|
||||
|
||||
const handleCategoryChange = (value) => {
|
||||
if (props.category === value) return
|
||||
emit('categoryChange', value)
|
||||
}
|
||||
|
||||
// ========== 父级调用的方法(滚动 → fixed 切换) ==========
|
||||
/**
|
||||
* 父级在 onScroll 时调用本方法,根据 scrollTop 决定是否切到 fixed
|
||||
* @param {Number} scrollTop 当前页面滚动距离(px)
|
||||
*/
|
||||
function updateScroll(scrollTop) {
|
||||
if (categoryOffsetTop.value === null) return
|
||||
isFixed.value = scrollTop + fixedTopPx.value >= categoryOffsetTop.value
|
||||
}
|
||||
|
||||
// 兼容移动端 rAF 节流:父级可监听 touchstart / touchmove 后调用
|
||||
function update() {
|
||||
if (categoryOffsetTop.value === null) return
|
||||
// 预留 hook,行为同 updateScroll 但语义更轻
|
||||
}
|
||||
|
||||
// 重新测量(适用于页面尺寸变化 / 内容更新后)
|
||||
function remeasure() {
|
||||
nextTick(() => {
|
||||
if (categoryRef.value) {
|
||||
categoryOffsetTop.value = categoryRef.value.offsetTop
|
||||
categoryHeight.value = categoryRef.value.offsetHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 原有:创作网格逻辑(保留) ==========
|
||||
const formatCount = (count) => {
|
||||
if (!count) return '0'
|
||||
if (count >= 10000) return (count / 10000).toFixed(1) + 'w'
|
||||
@ -71,7 +232,6 @@ const formatCount = (count) => {
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
// 卡片 DOM 引用 map(key = item.id)。供父组件读取位置做 spotlight。
|
||||
const cardRefsMap = new Map()
|
||||
const setCardRef = (el, item) => {
|
||||
if (el && item && item.id != null) {
|
||||
@ -88,19 +248,15 @@ const handleCardClick = (item) => {
|
||||
|
||||
const loadUsers = async () => {
|
||||
if (!isComponentMounted) return Promise.resolve()
|
||||
|
||||
cursor.value = ''
|
||||
isLoading.value = true
|
||||
noMore.value = false
|
||||
|
||||
try {
|
||||
const res = await getInspirationFlowApi({ limit: 20, type: props.category, cursor: '' })
|
||||
if (!isComponentMounted) return
|
||||
|
||||
if (res.code === 200 && res.data?.items && res.data.items.length > 0) {
|
||||
const items = res.data.items
|
||||
cursor.value = res.data.cursor || ''
|
||||
|
||||
creationList.value = items.map((item) => {
|
||||
return {
|
||||
id: item.asset_id,
|
||||
@ -125,16 +281,13 @@ const loadUsers = async () => {
|
||||
|
||||
const loadMore = async () => {
|
||||
if (!isComponentMounted || isLoading.value || noMore.value) return
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await getInspirationFlowApi({ limit: 20, type: props.category, cursor: cursor.value })
|
||||
if (!isComponentMounted) return
|
||||
|
||||
if (res.code === 200 && res.data?.items && res.data.items.length > 0) {
|
||||
const items = res.data.items
|
||||
cursor.value = res.data.cursor || ''
|
||||
|
||||
const newItems = items.map((item) => {
|
||||
return {
|
||||
id: item.asset_id,
|
||||
@ -146,7 +299,6 @@ const loadMore = async () => {
|
||||
is_liked: item.is_liked || false,
|
||||
}
|
||||
})
|
||||
|
||||
creationList.value = [...creationList.value, ...newItems]
|
||||
} else {
|
||||
noMore.value = true
|
||||
@ -162,61 +314,178 @@ const loadMore = async () => {
|
||||
watch(
|
||||
() => props.category,
|
||||
() => {
|
||||
loadUsers();
|
||||
loadUsers()
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.isActive,
|
||||
(active) => {
|
||||
if (active && creationList.value.length === 0) {
|
||||
loadUsers();
|
||||
loadUsers()
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers();
|
||||
isComponentMounted = true;
|
||||
// 监听点赞事件,实时更新点赞数
|
||||
uni.$on("assetLiked", ({ asset_id, data }) => {
|
||||
// 触发动画
|
||||
likingMap.value = { ...likingMap.value, [asset_id]: true };
|
||||
loadUsers()
|
||||
isComponentMounted = true
|
||||
uni.$on('assetLiked', ({ asset_id, data }) => {
|
||||
likingMap.value = { ...likingMap.value, [asset_id]: true }
|
||||
setTimeout(() => {
|
||||
likingMap.value = { ...likingMap.value, [asset_id]: false };
|
||||
}, 600);
|
||||
const idx = creationList.value.findIndex((c) => c.id === asset_id);
|
||||
likingMap.value = { ...likingMap.value, [asset_id]: false }
|
||||
}, 600)
|
||||
const idx = creationList.value.findIndex((c) => c.id === asset_id)
|
||||
if (idx !== -1) {
|
||||
if (data && typeof data.new_like_count === "number") {
|
||||
creationList.value[idx].like_count = data.new_like_count;
|
||||
if (data && typeof data.new_like_count === 'number') {
|
||||
creationList.value[idx].like_count = data.new_like_count
|
||||
}
|
||||
if (data && typeof data.is_liked === "boolean") {
|
||||
creationList.value[idx].is_liked = data.is_liked;
|
||||
if (data && typeof data.is_liked === 'boolean') {
|
||||
creationList.value[idx].is_liked = data.is_liked
|
||||
}
|
||||
creationList.value = [...creationList.value];
|
||||
creationList.value = [...creationList.value]
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
onShow(()=>{
|
||||
// loadUsers();
|
||||
// 测量分类标签位置和高度(rpx → px 换算 + 启动时定位)
|
||||
const info = uni.getSystemInfoSync()
|
||||
const sw = info.windowWidth || 750
|
||||
fixedTopPx.value = (96 * sw) / 750
|
||||
nextTick(() => {
|
||||
if (categoryRef.value) {
|
||||
categoryOffsetTop.value = categoryRef.value.offsetTop
|
||||
categoryHeight.value = categoryRef.value.offsetHeight
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
// loadUsers()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
isComponentMounted = false;
|
||||
uni.$off("assetLiked");
|
||||
});
|
||||
isComponentMounted = false
|
||||
uni.$off('assetLiked')
|
||||
})
|
||||
|
||||
defineExpose({ loadMore, getCardRefs })
|
||||
// ========== 对外暴露 ==========
|
||||
defineExpose({
|
||||
// 原有的
|
||||
loadMore,
|
||||
getCardRefs,
|
||||
// 新增:spotlight 系统 + 滚动 fixed + 占位
|
||||
mainTabsRef,
|
||||
categoryRef,
|
||||
categoryHeight,
|
||||
updateScroll,
|
||||
update,
|
||||
remeasure,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 区域 A:主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);
|
||||
}
|
||||
|
||||
/* 区域 B:分类标签 */
|
||||
.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 {
|
||||
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;
|
||||
}
|
||||
|
||||
/* 区域 C:创作网格(原有) */
|
||||
.creation-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
/* padding: 24rpx; */
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
@ -368,4 +637,4 @@ defineExpose({ loadMore, getCardRefs })
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -101,6 +101,18 @@
|
||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { getHotRankingApi } from "@/utils/api.js";
|
||||
import { getAssetCoverRealUrl } from "@/utils/assetImageHelper.js";
|
||||
|
||||
// 把后端返回的 cover_url / cover_image 转成真实可访问的 URL
|
||||
// 处理 3 种形态:/static/... (本地)、相对路径 (需 presign)、完整 URL (可能过期)
|
||||
async function resolveItemUrls(item) {
|
||||
if (!item) return item
|
||||
const cover = item.cover_url || item.cover_image || ""
|
||||
if (cover) {
|
||||
item.cover_url = await getAssetCoverRealUrl(cover)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
@ -179,10 +191,15 @@ const loadData = async () => {
|
||||
try {
|
||||
const res = await getHotRankingApi(props.dimension, null, 1, 11);
|
||||
if (res.code === 200 && res.data?.items) {
|
||||
items.value = res.data.items.map((item) => ({
|
||||
...item,
|
||||
id: item.id || item.asset_id,
|
||||
}));
|
||||
// 逐个把 cover_url 转换成真实可访问 URL(OSS 预签名前端实现)
|
||||
items.value = await Promise.all(
|
||||
res.data.items.map(async (item) => {
|
||||
return await resolveItemUrls({
|
||||
...item,
|
||||
id: item.id || item.asset_id,
|
||||
})
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[HotCategoryBlock] 加载数据失败", e?.message ?? e);
|
||||
|
||||
@ -46,40 +46,11 @@
|
||||
</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 组件(主Tab + 分类标签 + 网格列表) -->
|
||||
<CreationGrid :screenWidth="screenWidth" :screenHeight="screenHeight" :bannerBottom="bannerBottomPx"
|
||||
:category="activeCategoryTab" :isActive="isActive" @cardClick="handleCardClick" @loaded="onGridLoaded"
|
||||
:category="activeCategoryTab" :isActive="isActive"
|
||||
@cardClick="handleCardClick" @loaded="onGridLoaded"
|
||||
@mainTabClick="handleMainTabClick" @categoryChange="handleCategoryChange"
|
||||
ref="creationGridRef" />
|
||||
</scroll-view>
|
||||
</view>
|
||||
@ -113,8 +84,6 @@ 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({});
|
||||
@ -123,18 +92,7 @@ const likingMap = 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:
|
||||
// mainTabsRef / categoryRef 已迁入 CreationGrid 内部(通过 creationGridRef.value.mainTabsRef / categoryRef 拿)
|
||||
// - 4 个区块
|
||||
// - CreationGrid 里的所有卡片(通过 defineExpose 暴露的 getCardRefs 拿到)
|
||||
const allSpotlightRefs = () => {
|
||||
@ -142,8 +100,8 @@ const allSpotlightRefs = () => {
|
||||
bannerSectionRef.value,
|
||||
contentTabsRef.value?.$el || contentTabsRef.value,
|
||||
hotCategoryRef.value,
|
||||
mainTabsRef.value,
|
||||
categoryRef.value,
|
||||
creationGridRef.value?.mainTabsRef?.value,
|
||||
creationGridRef.value?.categoryRef?.value,
|
||||
].filter(Boolean)
|
||||
|
||||
const cards = creationGridRef.value?.getCardRefs?.() || []
|
||||
@ -154,13 +112,12 @@ const { update, bindScroll, start: startSpotlight, stop: stopSpotlight, isH5 } =
|
||||
getElements: allSpotlightRefs,
|
||||
})
|
||||
|
||||
// 统一 scroll 处理:spotlight + isFixed
|
||||
// 统一 scroll 处理:spotlight + 驱动 CreationGrid 切 fixed
|
||||
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
|
||||
}
|
||||
// 通知 CreationGrid:根据 scrollTop 决定分类标签是否切到 fixed
|
||||
creationGridRef.value?.updateScroll?.(scrollTop)
|
||||
}
|
||||
|
||||
// 移动端 rAF 在用户滚动时会被节流,opacity 跟不上。
|
||||
@ -172,15 +129,6 @@ 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));
|
||||
@ -196,51 +144,6 @@ const onGridLoaded = (count) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 主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);
|
||||
@ -392,14 +295,9 @@ onMounted(() => {
|
||||
loadBannerActivities();
|
||||
loadBanners();
|
||||
|
||||
// 等首屏 DOM 稳定后,测量分类标签 section 的位置/高度 + 启动 spotlight 轮询
|
||||
// 等首屏 DOM 稳定后,启动 spotlight 轮询
|
||||
// 分类标签的位置/高度测量已迁入 CreationGrid 内部(onMounted 自动测)
|
||||
nextTick(() => {
|
||||
if (categoryRef.value) {
|
||||
categoryOffsetTop.value = categoryRef.value.offsetTop;
|
||||
categoryHeight.value = categoryRef.value.offsetHeight;
|
||||
}
|
||||
// rpx → px:96rpx 在不同屏幕宽度下换算
|
||||
fixedTopPx.value = (96 * screenWidth.value) / 750;
|
||||
startSpotlight();
|
||||
});
|
||||
});
|
||||
@ -505,103 +403,10 @@ onUnmounted(() => {
|
||||
/* 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;
|
||||
}
|
||||
/* 区域二(主Tab)+ 区域三(分类标签)+ 占位 + 回弹动效
|
||||
已迁入 CreationGrid 组件内部(创建于 components/CreationGrid.vue)
|
||||
这里的 .main-tab-section / .category-section / .tab-item / 等选择器被保留
|
||||
是因为 .spotlight-ready 选择器需要它们,CSS specificity 才能匹配(容器外层应用 spotlight-ready)。 */
|
||||
|
||||
/* 热门分类区块 */
|
||||
.hot-category-wrapper {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user