feat: 修改square组件的页面

This commit is contained in:
zerosaturation 2026-06-09 11:21:04 +08:00
parent 4f3bb7e3ab
commit dc9f1a7b1e
3 changed files with 376 additions and 285 deletions

View File

@ -1,48 +1,115 @@
<template> <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 <view
v-for="item in creationList" ref="categoryRef"
:key="item.id" id="category-section"
:ref="(el) => setCardRef(el, item)" class="category-section"
class="creation-card" :class="{ fixed: isFixed, 'just-fixed': justFixed }"
@click="handleCardClick(item)"
> >
<image class="creation-image" :src="item.cover_image" mode="aspectFill"></image> <scroll-view class="category-scroll" scroll-x :show-scrollbar="false">
<view class="like-badge"> <view
<view class="like-icon-wrapper"> v-for="(category, index) in categories"
<image class="like-icon" :key="index"
:src="item.is_liked ? '/static/icon/heart-icon.png' : '/static/icon/heart-icon-false.png'" mode="aspectFit"> class="category-item"
</image> :class="{ active: category === category.value }"
<text class="like-count">{{ formatCount(item.like_count) }}</text> @click="handleCategoryChange(category.value)"
>
<text class="category-text">{{ category.label }}</text>
</view> </view>
</view> </scroll-view>
<view class="wf-like-wave wf-like-wave-outer" :class="{ 'wf-like-wave-active': likingMap[item.id] }" /> </view>
<view class="wf-like-wave wf-like-wave-inner" :class="{ 'wf-like-wave-active': likingMap[item.id] }" /> <!-- fixed 时占位避免下方内容跳变 -->
<view class="creation-info"> <view
<view class="creation-id"> v-if="isFixed && categoryHeight > 0"
<text class="id-text">链上编号: #{{ item.certificate_id }}</text> class="category-placeholder"
</view> :style="{ height: categoryHeight + 'px' }"
<view class="creation-meta"> ></view>
<view class="creator-info">
<image class="creator-avatar" :src="item.creator_avatar" mode="aspectFill"></image> <!-- 区域 C创作网格列表保留原功能 -->
<text class="creator-name">{{ item.creator_name }}</text> <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> </view>
</view>
<view v-if="isLoading" class="loading-more"> <view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text> <text class="loading-text">加载中...</text>
</view> </view>
<view v-if="noMore && creationList.length > 0" class="no-more"> <view v-if="noMore && creationList.length > 0" class="no-more">
<text class="no-more-text">没有更多了</text> <text class="no-more-text">没有更多了</text>
</view>
</view> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue' import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { onShow } from "@dcloudio/uni-app"; import { onShow } from "@dcloudio/uni-app";
import { getInspirationFlowApi } from '@/utils/api.js' import { getInspirationFlowApi } from '@/utils/api.js'
@ -55,7 +122,7 @@ const props = defineProps({
isActive: { type: Boolean, default: true }, isActive: { type: Boolean, default: true },
}) })
const emit = defineEmits(['cardClick', 'scroll', 'loaded']) const emit = defineEmits(['cardClick', 'scroll', 'loaded', 'mainTabClick', 'categoryChange'])
const creationList = ref([]) const creationList = ref([])
const cursor = ref('') const cursor = ref('')
@ -64,6 +131,100 @@ const noMore = ref(false)
let isComponentMounted = false let isComponentMounted = false
const likingMap = ref({}); const likingMap = ref({});
// ========== ATab ==========
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' },
]
// ========== refexpose 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) => { const formatCount = (count) => {
if (!count) return '0' if (!count) return '0'
if (count >= 10000) return (count / 10000).toFixed(1) + 'w' if (count >= 10000) return (count / 10000).toFixed(1) + 'w'
@ -71,7 +232,6 @@ const formatCount = (count) => {
return count.toString() return count.toString()
} }
// DOM mapkey = item.id spotlight
const cardRefsMap = new Map() const cardRefsMap = new Map()
const setCardRef = (el, item) => { const setCardRef = (el, item) => {
if (el && item && item.id != null) { if (el && item && item.id != null) {
@ -88,19 +248,15 @@ const handleCardClick = (item) => {
const loadUsers = async () => { const loadUsers = async () => {
if (!isComponentMounted) return Promise.resolve() if (!isComponentMounted) return Promise.resolve()
cursor.value = '' cursor.value = ''
isLoading.value = true isLoading.value = true
noMore.value = false noMore.value = false
try { try {
const res = await getInspirationFlowApi({ limit: 20, type: props.category, cursor: '' }) const res = await getInspirationFlowApi({ limit: 20, type: props.category, cursor: '' })
if (!isComponentMounted) return if (!isComponentMounted) return
if (res.code === 200 && res.data?.items && res.data.items.length > 0) { if (res.code === 200 && res.data?.items && res.data.items.length > 0) {
const items = res.data.items const items = res.data.items
cursor.value = res.data.cursor || '' cursor.value = res.data.cursor || ''
creationList.value = items.map((item) => { creationList.value = items.map((item) => {
return { return {
id: item.asset_id, id: item.asset_id,
@ -125,16 +281,13 @@ const loadUsers = async () => {
const loadMore = async () => { const loadMore = async () => {
if (!isComponentMounted || isLoading.value || noMore.value) return if (!isComponentMounted || isLoading.value || noMore.value) return
isLoading.value = true isLoading.value = true
try { try {
const res = await getInspirationFlowApi({ limit: 20, type: props.category, cursor: cursor.value }) const res = await getInspirationFlowApi({ limit: 20, type: props.category, cursor: cursor.value })
if (!isComponentMounted) return if (!isComponentMounted) return
if (res.code === 200 && res.data?.items && res.data.items.length > 0) { if (res.code === 200 && res.data?.items && res.data.items.length > 0) {
const items = res.data.items const items = res.data.items
cursor.value = res.data.cursor || '' cursor.value = res.data.cursor || ''
const newItems = items.map((item) => { const newItems = items.map((item) => {
return { return {
id: item.asset_id, id: item.asset_id,
@ -146,7 +299,6 @@ const loadMore = async () => {
is_liked: item.is_liked || false, is_liked: item.is_liked || false,
} }
}) })
creationList.value = [...creationList.value, ...newItems] creationList.value = [...creationList.value, ...newItems]
} else { } else {
noMore.value = true noMore.value = true
@ -162,61 +314,178 @@ const loadMore = async () => {
watch( watch(
() => props.category, () => props.category,
() => { () => {
loadUsers(); loadUsers()
}, },
); )
watch( watch(
() => props.isActive, () => props.isActive,
(active) => { (active) => {
if (active && creationList.value.length === 0) { if (active && creationList.value.length === 0) {
loadUsers(); loadUsers()
} }
}, },
); )
onMounted(() => { onMounted(() => {
loadUsers(); loadUsers()
isComponentMounted = true; isComponentMounted = true
// uni.$on('assetLiked', ({ asset_id, data }) => {
uni.$on("assetLiked", ({ asset_id, data }) => { likingMap.value = { ...likingMap.value, [asset_id]: true }
//
likingMap.value = { ...likingMap.value, [asset_id]: true };
setTimeout(() => { setTimeout(() => {
likingMap.value = { ...likingMap.value, [asset_id]: false }; likingMap.value = { ...likingMap.value, [asset_id]: false }
}, 600); }, 600)
const idx = creationList.value.findIndex((c) => c.id === asset_id); const idx = creationList.value.findIndex((c) => c.id === asset_id)
if (idx !== -1) { if (idx !== -1) {
if (data && typeof data.new_like_count === "number") { if (data && typeof data.new_like_count === 'number') {
creationList.value[idx].like_count = data.new_like_count; creationList.value[idx].like_count = data.new_like_count
} }
if (data && typeof data.is_liked === "boolean") { if (data && typeof data.is_liked === 'boolean') {
creationList.value[idx].is_liked = data.is_liked; creationList.value[idx].is_liked = data.is_liked
} }
creationList.value = [...creationList.value]; creationList.value = [...creationList.value]
} }
}); })
});
onShow(()=>{ // rpx px +
// loadUsers(); 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(() => { onUnmounted(() => {
isComponentMounted = false; isComponentMounted = false
uni.$off("assetLiked"); uni.$off('assetLiked')
}); })
defineExpose({ loadMore, getCardRefs }) // ========== ==========
defineExpose({
//
loadMore,
getCardRefs,
// spotlight + fixed +
mainTabsRef,
categoryRef,
categoryHeight,
updateScroll,
update,
remeasure,
})
</script> </script>
<style scoped> <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 { .creation-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
/* padding: 24rpx; */
padding-bottom: 120rpx; padding-bottom: 120rpx;
} }
@ -368,4 +637,4 @@ defineExpose({ loadMore, getCardRefs })
font-size: 24rpx; font-size: 24rpx;
color: rgba(255, 255, 255, 0.6); color: rgba(255, 255, 255, 0.6);
} }
</style> </style>

View File

@ -101,6 +101,18 @@
import { ref, watch, onMounted, onUnmounted } from "vue"; import { ref, watch, onMounted, onUnmounted } from "vue";
import { onShow } from "@dcloudio/uni-app"; import { onShow } from "@dcloudio/uni-app";
import { getHotRankingApi } from "@/utils/api.js"; 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({ const props = defineProps({
title: { title: {
@ -179,10 +191,15 @@ const loadData = async () => {
try { try {
const res = await getHotRankingApi(props.dimension, null, 1, 11); const res = await getHotRankingApi(props.dimension, null, 1, 11);
if (res.code === 200 && res.data?.items) { if (res.code === 200 && res.data?.items) {
items.value = res.data.items.map((item) => ({ // cover_url 访 URLOSS
...item, items.value = await Promise.all(
id: item.id || item.asset_id, res.data.items.map(async (item) => {
})); return await resolveItemUrls({
...item,
id: item.id || item.asset_id,
})
})
);
} }
} catch (e) { } catch (e) {
console.error("[HotCategoryBlock] 加载数据失败", e?.message ?? e); console.error("[HotCategoryBlock] 加载数据失败", e?.message ?? e);

View File

@ -46,40 +46,11 @@
</view> </view>
</view> </view>
<!-- 区域二主Tab标签区星卡/吧唧/海报 --> <!-- 区域二//四已合并到 CreationGrid 组件主Tab + 分类标签 + 网格列表 -->
<view ref="mainTabsRef" class="main-tab-section">
<view v-for="(tab, index) in mainTabs" :key="index" class="tab-item"
:style="{ '--tab-delay': index * 0.06 + 's' }" @click="handleMainTabClick(tab)">
<view class="tab-icon-wrap">
<image class="tab-icon" :src="tab.icon" mode="aspectFit" :style="{
width: tab.width + 'rpx',
height: tab.height + 'rpx',
borderRadius: tab.type === 'badge' ? '50%' : '0',
transform: 'rotate(' + tab.rotate + 'deg)',
}">
</image>
</view>
<text class="tab-name">{{ tab.name }}</text>
</view>
</view>
<!-- 区域三分类标签区 -->
<view ref="categoryRef" id="category-section" class="category-section"
:class="{ fixed: isFixed, 'just-fixed': justFixed }">
<scroll-view class="category-scroll" scroll-x :show-scrollbar="false">
<view v-for="(category, index) in categories" :key="index" class="category-item"
:class="{ active: activeCategoryTab === category.value }" @click="handleCategoryChange(category.value)">
<text class="category-text">{{ category.label }}</text>
</view>
</scroll-view>
</view>
<!-- fixed 时占位避免下方内容跳变 -->
<view v-if="isFixed && categoryHeight > 0" class="category-placeholder"
:style="{ height: categoryHeight + 'px' }"></view>
<!-- 区域四创作网格列表 -->
<CreationGrid :screenWidth="screenWidth" :screenHeight="screenHeight" :bannerBottom="bannerBottomPx" <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" /> ref="creationGridRef" />
</scroll-view> </scroll-view>
</view> </view>
@ -113,8 +84,6 @@ const activeCategoryTab = ref("hot");
const navExpanded = ref(false); const navExpanded = ref(false);
const showRankingModal = ref(false); const showRankingModal = ref(false);
const isActive = ref(true); const isActive = ref(true);
const isFixed = ref(false);
const justFixed = ref(false);
const creationGridRef = ref(null); const creationGridRef = ref(null);
const cardTapTimers = {}; const cardTapTimers = {};
const likingMap = ref({}); const likingMap = ref({});
@ -123,18 +92,7 @@ const likingMap = ref({});
const bannerSectionRef = ref(null); const bannerSectionRef = ref(null);
const contentTabsRef = ref(null); const contentTabsRef = ref(null);
const hotCategoryRef = ref(null); const hotCategoryRef = ref(null);
const mainTabsRef = ref(null); // mainTabsRef / categoryRef CreationGrid creationGridRef.value.mainTabsRef / categoryRef
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 // - 4
// - CreationGrid defineExpose getCardRefs // - CreationGrid defineExpose getCardRefs
const allSpotlightRefs = () => { const allSpotlightRefs = () => {
@ -142,8 +100,8 @@ const allSpotlightRefs = () => {
bannerSectionRef.value, bannerSectionRef.value,
contentTabsRef.value?.$el || contentTabsRef.value, contentTabsRef.value?.$el || contentTabsRef.value,
hotCategoryRef.value, hotCategoryRef.value,
mainTabsRef.value, creationGridRef.value?.mainTabsRef?.value,
categoryRef.value, creationGridRef.value?.categoryRef?.value,
].filter(Boolean) ].filter(Boolean)
const cards = creationGridRef.value?.getCardRefs?.() || [] const cards = creationGridRef.value?.getCardRefs?.() || []
@ -154,13 +112,12 @@ const { update, bindScroll, start: startSpotlight, stop: stopSpotlight, isH5 } =
getElements: allSpotlightRefs, getElements: allSpotlightRefs,
}) })
// scroll spotlight + isFixed // scroll spotlight + CreationGrid fixed
const onScroll = (e) => { const onScroll = (e) => {
bindScroll() // scroll rAF bindScroll() // scroll rAF
const scrollTop = (e && e.detail && e.detail.scrollTop) || 0 const scrollTop = (e && e.detail && e.detail.scrollTop) || 0
if (categoryOffsetTop.value !== null) { // CreationGrid scrollTop fixed
isFixed.value = scrollTop + fixedTopPx.value >= categoryOffsetTop.value creationGridRef.value?.updateScroll?.(scrollTop)
}
} }
// rAF opacity // rAF opacity
@ -172,15 +129,6 @@ const onTouchMove = () => {
update() update()
} }
// fixed class
watch(isFixed, (val) => {
if (!val) return;
justFixed.value = true;
setTimeout(() => {
justFixed.value = false;
}, 500);
});
// tab / spotlight // tab / spotlight
watch(activeContentTab, () => { watch(activeContentTab, () => {
nextTick(() => setTimeout(update, 50)); 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 ========== // ========== Screen Info ==========
const screenWidth = ref(375); const screenWidth = ref(375);
const screenHeight = ref(812); const screenHeight = ref(812);
@ -392,14 +295,9 @@ onMounted(() => {
loadBannerActivities(); loadBannerActivities();
loadBanners(); loadBanners();
// DOM section / + spotlight // DOM spotlight
// / CreationGrid onMounted
nextTick(() => { nextTick(() => {
if (categoryRef.value) {
categoryOffsetTop.value = categoryRef.value.offsetTop;
categoryHeight.value = categoryRef.value.offsetHeight;
}
// rpx px96rpx
fixedTopPx.value = (96 * screenWidth.value) / 750;
startSpotlight(); startSpotlight();
}); });
}); });
@ -505,103 +403,10 @@ onUnmounted(() => {
/* margin-bottom: 32rpx; */ /* margin-bottom: 32rpx; */
} }
/* 区域二主Tab */ /* Tab+ + +
.main-tab-section { 已迁入 CreationGrid 组件内部创建于 components/CreationGrid.vue
display: flex; 这里的 .main-tab-section / .category-section / .tab-item / 等选择器被保留
justify-content: space-around; 是因为 .spotlight-ready 选择器需要它们CSS specificity 才能匹配容器外层应用 spotlight-ready */
align-items: center;
padding: 24rpx 0;
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
width: 200rpx;
height: 200rpx;
background: linear-gradient(135deg,
rgba(240, 228, 177, 0.3),
rgba(240, 131, 153, 0.3));
border-radius: 24rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 157, 0.2);
transition: all 0.3s;
}
.tab-item:active {
transform: scale(0.95);
opacity: 0.8;
}
.tab-icon-wrap {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
}
.tab-icon {
margin-bottom: 0;
object-fit: cover;
box-shadow: 8rpx 8rpx 16rpx rgba(229, 76, 93, 0.9);
}
.tab-name {
font-size: 28rpx;
color: #fff;
font-weight: 600;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}
/* 区域三:分类标签 */
.category-section {
margin-bottom: 24rpx;
transition: all 0.3s ease;
will-change: transform;
}
.category-section.fixed {
position: fixed;
top: 96rpx;
left: 24rpx;
right: 24rpx;
z-index: 100;
padding: 16rpx 0;
}
.category-placeholder {
/* 仅占位,无内容;高度由内联 style 写入 */
margin-bottom: 24rpx;
}
.category-scroll {
white-space: nowrap;
}
.category-item {
display: inline-block;
padding: 16rpx 32rpx;
margin-right: 16rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 40rpx;
backdrop-filter: blur(10rpx);
transition: all 0.3s;
}
.category-item.active {
background: linear-gradient(135deg, #f0e4b1, #f08399);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.4);
}
.category-text {
font-size: 26rpx;
color: #fff;
font-weight: 500;
}
.category-item.active .category-text {
font-weight: 600;
}
/* 热门分类区块 */ /* 热门分类区块 */
.hot-category-wrapper { .hot-category-wrapper {