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>
<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({});
// ========== 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) => {
if (!count) return '0'
if (count >= 10000) return (count / 10000).toFixed(1) + 'w'
@ -71,7 +232,6 @@ const formatCount = (count) => {
return count.toString()
}
// DOM mapkey = item.id spotlight
const cardRefsMap = new Map()
const setCardRef = (el, item) => {
if (el && item && item.id != null) {
@ -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>

View File

@ -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 访 URLOSS
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);

View File

@ -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 px96rpx
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 {