feat: 修改square组件

This commit is contained in:
zheng020 2026-06-09 21:09:57 +08:00
parent b14cc119b4
commit b00f7dabf0
5 changed files with 525 additions and 524 deletions

View File

@ -1,15 +1,21 @@
<template>
<view class="content-tabs">
<view v-for="(tab, index) in tabs" :key="tab.key" class="tab-item" :class="{ active: modelValue === tab.key }"
:data-key="tab.key" @click="handleTabClick">
<view
v-for="(tab, index) in tabs"
:key="tab.key"
class="tab-item"
:class="{ active: modelValue === tab.key }"
:data-key="tab.key"
@click="handleTabClick"
>
<!-- 背景图片 -->
<!-- <image class="tab-bg" :class="{ 'tab-bg-inactive': modelValue !== tab.key }"
src="/static/nft/dingbutubiao_liang.png" mode="scaleToFill" /> -->
<!-- 左侧图标绝对浮动覆盖背景左侧色块不影响文字布局 -->
<view class="tab-left" :class="{ 'tab-left-first': index === 2 }">
<text v-if="tab.emoji" class="tab-emoji">{{ tab.emoji }}</text>
<view class="tab-left">
<!-- <text v-if="tab.emoji" class="tab-emoji">{{ tab.emoji }}</text>
<image v-else class="tab-icon" :src="tab.icon" mode="aspectFill"
:style="{ width: tab.iconWidth + 'rpx', height: tab.iconHeight + 'rpx' }" />
:style="{ width: tab.iconWidth + 'rpx', height: tab.iconHeight + 'rpx' }" /> -->
<!-- 右侧文字 -->
<text class="tab-label">{{ tab.label }}</text>
</view>
@ -19,56 +25,75 @@
<script setup>
const props = defineProps({
modelValue: { type: String, default: 'hot' }
})
modelValue: { type: String, default: "hot" },
});
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(["update:modelValue"]);
const handleTabClick = (e) => {
const key = e.currentTarget.dataset.key
console.log(key)
emit('update:modelValue', key)
}
const key = e.currentTarget.dataset.key;
console.log(key);
emit("update:modelValue", key);
};
const tabs = [
{ key: 'displaying', label: '日榜', emoji: null, icon: '/static/square/1.png', iconWidth: 32, iconHeight: 40 },
// { key: 'week', label: '', emoji: null, icon: '/static/square/1.png', iconWidth: 32, iconHeight: 40 },
// { key: 'month', label: '', emoji: null, icon: '/static/square/1.png', iconWidth: 32, iconHeight: 40 },
]
{
key: "xinghe",
label: "星河",
emoji: null,
icon: "/static/square/1.png",
iconWidth: 32,
iconHeight: 40,
},
{
key: "xingbang",
label: "星榜",
emoji: null,
icon: "/static/square/1.png",
iconWidth: 32,
iconHeight: 40,
},
{
key: "guangchang",
label: "广场",
emoji: null,
icon: "/static/square/1.png",
iconWidth: 32,
iconHeight: 40,
},
];
</script>
<style scoped>
.content-tabs {
position: relative;
top: unset;
left: unset;
right: unset;
bottom: 16rpx;
left: 50%;
bottom: 0.25rem;
transform: translateX(-50%);
z-index: 100;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 8rpx 16rpx 0;
padding: 0 4rpx;
height: 56rpx;
background: transparent;
margin: 8rpx 16rpx;
padding: 0 8rpx;
width: 336rpx;
height: 64rpx;
border-radius: 16px;
overflow: visible;
background: #d9d9d91c;
box-shadow: 0px 4px 4px 0px #b3323240;
}
.tab-item {
max-width: 134rpx;
width: 112rpx;
height: 42rpx;
border-radius: 21rpx;
opacity: 0.78;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
overflow: visible;
background: linear-gradient(90deg, rgba(255, 222, 8, 0.2989) -17.54%, rgba(252, 100, 102, 0.61) 64.4%, rgba(244, 88, 104, 0.61) 116.67%);
box-shadow: 2px 2px 4px 0px #F2151578;
}
/* 背景图片铺满整个 tab */
@ -92,7 +117,6 @@ const tabs = [
display: flex;
align-items: center;
justify-content: center;
padding: 4rpx 30rpx 4rpx 20rpx ;
transition: transform 0.25s ease;
}
@ -123,7 +147,13 @@ const tabs = [
margin-left: 14rpx;
}
.tab-item.active .tab-label {
color: #ffffff;
.tab-item.active{
background: linear-gradient(
90deg,
rgba(255, 222, 8, 0.2989) -17.54%,
rgba(252, 100, 102, 0.61) 64.4%,
rgba(244, 88, 104, 0.61) 116.67%
);
box-shadow: 2px 2px 4px 0px #f2151578;
}
</style>

View File

@ -38,7 +38,7 @@
v-for="(category, index) in categories"
:key="index"
class="category-item"
:class="{ active: category === category.value }"
:class="{ active: category.value === activeCategory }"
@click="handleCategoryChange(category.value)"
>
<text class="category-text">{{ category.label }}</text>
@ -110,26 +110,21 @@
<script setup>
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { onShow } from "@dcloudio/uni-app";
import { onShow, onHide } from "@dcloudio/uni-app";
import { getInspirationFlowApi } from '@/utils/api.js'
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js'
const props = defineProps({
screenWidth: { type: Number, default: 375 },
screenHeight: { type: Number, default: 812 },
bannerBottom: { type: Number, default: 200 },
useMockData: { type: Boolean, default: false },
category: { type: String, default: '' },
isActive: { type: Boolean, default: true },
})
const emit = defineEmits(['cardClick', 'scroll', 'loaded', 'mainTabClick', 'categoryChange'])
// prop/
const emit = defineEmits(['cardClick', 'loaded'])
// ========== ==========
const creationList = ref([])
const cursor = ref('')
const isLoading = ref(false)
const noMore = ref(false)
let isComponentMounted = false
const likingMap = ref({});
const likingMap = ref({})
const activeCategory = ref('hot') //
const isComponentActive = ref(true) //
// ========== ATab ==========
const mainTabs = [
@ -168,7 +163,7 @@ const categories = [
{ label: '海报', value: 'poster' },
]
// ========== refexpose spotlight + ==========
// ========== ref spotlight + ==========
const mainTabsRef = ref(null)
const categoryRef = ref(null)
@ -188,14 +183,19 @@ watch(isFixed, (val) => {
}, 450)
})
// ========== ==========
// ========== ==========
// Tab -
const handleMainTabClick = (tab) => {
emit('mainTabClick', tab)
uni.navigateTo({
url: `/pages/castlove/mall?type=${encodeURIComponent(tab.type)}`,
})
}
// - activeCategory
const handleCategoryChange = (value) => {
if (props.category === value) return
emit('categoryChange', value)
if (activeCategory.value === value) return
activeCategory.value = value
}
// ========== fixed ==========
@ -224,7 +224,7 @@ function remeasure() {
})
}
// ========== ==========
// ========== ==========
const formatCount = (count) => {
if (!count) return '0'
if (count >= 10000) return (count / 10000).toFixed(1) + 'w'
@ -246,28 +246,40 @@ const handleCardClick = (item) => {
emit('cardClick', item)
}
// cover_url 访 URLOSS
async function resolveItemImage(item) {
if (!item) return item
const cover = item.cover_url || item.cover_image || ''
if (cover) {
item.cover_image = await getAssetCoverRealUrl(cover)
}
return 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
const res = await getInspirationFlowApi({ limit: 20, type: activeCategory.value, cursor: '' })
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,
certificate_id: item.asset_id,
cover_image: item.cover_url || '',
creator_avatar: item.owner_avatar || '',
creator_name: item.owner_nickname || item.name || '',
like_count: item.likes || item.like_count || 0,
is_liked: item.is_liked || false,
}
})
// cover_url 访 URL
creationList.value = await Promise.all(
items.map(async (item) => {
const resolved = await resolveItemImage({ ...item })
return {
id: item.asset_id,
certificate_id: item.asset_id,
cover_image: resolved.cover_image,
creator_avatar: item.owner_avatar || '',
creator_name: item.owner_nickname || item.name || '',
like_count: item.likes || item.like_count || 0,
is_liked: item.is_liked || false,
}
}),
)
} else {
noMore.value = true
}
@ -280,25 +292,27 @@ const loadUsers = async () => {
}
const loadMore = async () => {
if (!isComponentMounted || isLoading.value || noMore.value) return
if (isLoading.value || noMore.value) return
isLoading.value = true
try {
const res = await getInspirationFlowApi({ limit: 20, type: props.category, cursor: cursor.value })
if (!isComponentMounted) return
const res = await getInspirationFlowApi({ limit: 20, type: activeCategory.value, cursor: cursor.value })
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,
certificate_id: item.asset_id,
cover_image: item.cover_url || '',
creator_avatar: item.owner_avatar || '',
creator_name: item.owner_nickname || item.name || '',
like_count: item.likes || item.like_count || 0,
is_liked: item.is_liked || false,
}
})
const newItems = await Promise.all(
items.map(async (item) => {
const resolved = await resolveItemImage({ ...item })
return {
id: item.asset_id,
certificate_id: item.asset_id,
cover_image: resolved.cover_image,
creator_avatar: item.owner_avatar || '',
creator_name: item.owner_nickname || item.name || '',
like_count: item.likes || item.like_count || 0,
is_liked: item.is_liked || false,
}
}),
)
creationList.value = [...creationList.value, ...newItems]
} else {
noMore.value = true
@ -311,25 +325,21 @@ const loadMore = async () => {
}
}
watch(
() => props.category,
() => {
loadUsers()
},
)
//
watch(activeCategory, () => {
loadUsers()
})
watch(
() => props.isActive,
(active) => {
if (active && creationList.value.length === 0) {
loadUsers()
}
},
)
//
watch(isComponentActive, (active) => {
if (active && creationList.value.length === 0) {
loadUsers()
}
})
onMounted(() => {
loadUsers()
isComponentMounted = true
uni.$on('assetLiked', ({ asset_id, data }) => {
likingMap.value = { ...likingMap.value, [asset_id]: true }
setTimeout(() => {
@ -360,20 +370,21 @@ onMounted(() => {
})
onShow(() => {
// loadUsers()
isComponentActive.value = true
})
onHide(() => {
isComponentActive.value = false
})
onUnmounted(() => {
isComponentMounted = false
uni.$off('assetLiked')
})
// ========== ==========
// ========== spotlight / ==========
defineExpose({
//
loadMore,
getCardRefs,
// spotlight + fixed +
mainTabsRef,
categoryRef,
categoryHeight,

View File

@ -1,9 +1,28 @@
<template>
<view class="hot-category-block">
<!-- 标题 -->
<!-- <view class="block-title">
<text class="recommend-text"> {{ title }}</text>
</view> -->
<!-- Tab -->
<view class="ranking-tabs">
<view
v-for="tab in tabs"
:key="tab.key"
class="ranking-tab-item"
:class="{ active: activeTabKey === tab.key }"
:data-key="tab.key"
@click="handleTabClick"
>
<image
v-if="tab.icon"
class="ranking-tab-icon"
:src="tab.icon"
mode="aspectFit"
:style="{
width: (tab.iconWidth || 32) + 'rpx',
height: (tab.iconHeight || 40) + 'rpx',
}"
/>
<!-- <text class="ranking-tab-label">{{ tab.label }}</text> -->
</view>
</view>
<!-- 骨架屏 -->
<view v-if="loading" class="grid-skeleton">
@ -22,6 +41,10 @@
v-for="(item, index) in items"
:key="item.id || index"
class="grid-card"
:class="{
'grid-card-top': index < 3,
[`grid-card-top-${index + 1}`]: index < 3,
}"
@click="handleCardClick(item)"
>
<!-- 点赞动效波纹 -->
@ -33,15 +56,35 @@
class="wf-like-wave wf-like-wave-inner"
:class="{ 'wf-like-wave-active': likingMap[item.id] }"
/>
<!-- 底部信息模块独立于图片 -->
<view class="card-bottom" :class="`card-bottom-${index + 1}`">
<image
class="card-image"
:src="item.cover_url || item.cover_image || ''"
mode="aspectFill"
></image>
<view class="like-badge" :class="`like-badge-${index + 1}`">
<view class="like-icon-wrapper">
<!-- 单行布局藏品图片 + 头像 + 点赞数 + TOP 标签 -->
<view class="card-row">
<view class="card-image-wrap">
<image
class="card-image"
:src="item.cover_url || item.cover_image || ''"
mode="aspectFill"
/>
<!-- 3 名专属包裹整个卡片的边框图 -->
<image
v-if="index < 3"
class="frame-image"
:src="TOP_FRAME_MAP[index]"
mode="scaleToFill"
/>
<!-- 3 名专属左上角奖牌装饰 -->
<image
v-if="index < 3"
class="card-medal"
:src="MEDAL_MAP[index]"
mode="aspectFit"
/>
</view>
<view
class="like-info"
:class="{ [`like-info-top-${index + 1}`]: index < 3 }"
>
<view class="like-row">
<image
class="like-icon"
:src="
@ -50,47 +93,24 @@
: '/static/icon/heart-icon-false.png'
"
mode="aspectFit"
>
</image>
/>
<text class="like-count">{{ formatCount(item.like_count) }}</text>
</view>
<text class="user-name">{{
item.owner_nickname || item.creator_name || item.name || ""
}}</text>
<text class="user-number">No.{{ item.owner_uid || "" }}</text>
</view>
<!-- 用户信息 -->
<view class="card-info" :class="`card-info-${index + 1}`">
<view class="user-info">
<view v-if="index >= 3" class="top-badge">
<view class="badge-rank">
<image
class="user-avatar"
:src="item.owner_avatar || item.creator_avatar || ''"
mode="aspectFill"
>
</image>
<text class="user-name">{{
item.owner_nickname || item.creator_name || item.name || ""
}}</text>
class="badge-rank-icon"
src="/static/square/top/top.png"
mode="aspectFit"
/>
<text class="badge-rank-number">{{ index + 1 }}</text>
</view>
</view>
<!-- 前三名专属包裹 card-bottom 的边框 -->
<view v-if="index < 3" class="card-frame">
<image
class="frame-image"
:src="TOP_FRAME_MAP[index]"
mode="scaleToFill"
></image>
</view>
</view>
<!-- 前三名专属右上角装饰图位于 grid-card 层级避免被 card-bottom overflow 裁切 -->
<view v-if="index < 3" class="corner-decoration">
<image :src="TOP_ICON_MAP[index]" mode="aspectFit"></image>
</view>
<!-- Top 排名标签 -->
<view class="top-badge" :class="`top-badge-${index + 1}`">
<view
v-if="index < 3"
class="corner-decoration top-corner-decoration"
>
<image :src="TOP_ICON_MAP[index]" mode="aspectFit"></image>
</view>
<view class="badge-rank">TOP {{ index + 1 }}</view>
</view>
</view>
</view>
@ -98,7 +118,7 @@
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from "vue";
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { getHotRankingApi } from "@/utils/api.js";
import { getAssetCoverRealUrl } from "@/utils/assetImageHelper.js";
@ -106,51 +126,79 @@ 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 (!item) return item;
const cover = item.cover_url || item.cover_image || "";
if (cover) {
item.cover_url = await getAssetCoverRealUrl(cover)
item.cover_url = await getAssetCoverRealUrl(cover);
}
return item
return item;
}
const props = defineProps({
title: {
type: String,
default: "在线榜单",
},
dimension: {
type: String,
default: "displaying",
},
});
const emit = defineEmits(["cardClick"]);
const items = ref([]);
const loading = ref(false);
const likingMap = ref({});
const activeTabKey = ref("");
//
const TOP_FRAME_MAP = {
0: "/static/square/top/TOP1biankuang1.png",
1: "/static/square/top/TOP2biankuang2.png",
2: "/static/square/top/TOP3biankuangpng3.png",
};
const TOP_ICON_MAP = {
// Tab
// tab push { key, label, icon, iconWidth, iconHeight, fetch }
const tabs = [
{
key: "hot",
label: "热度榜",
icon: "/static/square/rementubiao.png",
iconWidth: 32,
iconHeight: 40,
fetch: () => getHotRankingApi("displaying", null, 1, 11),
},
];
// 3
const MEDAL_MAP = {
0: "/static/square/top/TOP1icon.png",
1: "/static/square/top/TOP2icon.png",
2: "/static/square/top/TOP3icon.png",
};
// dimension
watch(
() => props.dimension,
() => {
loadData();
},
// 3
const TOP_FRAME_MAP = {
0: "/static/square/top/TOP1biankuang1.png",
1: "/static/square/top/TOP2biankuang2.png",
2: "/static/square/top/TOP3biankuangpng3.png",
};
const activeTab = computed(
() => tabs.find((t) => t.key === activeTabKey.value) || tabs[0],
);
// activeTabKey tab
watch(
() => tabs,
(newTabs) => {
if (
newTabs.length > 0 &&
!newTabs.some((t) => t.key === activeTabKey.value)
) {
activeTabKey.value = newTabs[0].key;
}
},
{ immediate: true },
);
// tab
const handleTabClick = (e) => {
const key = e.currentTarget.dataset.key;
if (key && key !== activeTabKey.value) {
activeTabKey.value = key;
}
};
// activeTab
watch(activeTab, () => {
loadData();
});
//
const formatCount = (count) => {
if (!count) return "0";
@ -187,22 +235,31 @@ const onAssetLiked = ({ asset_id, data }) => {
//
const loadData = async () => {
const tab = activeTab.value;
if (!tab || typeof tab.fetch !== "function") {
console.warn("[HotCategoryBlock] 当前 tab 未配置 fetch:", tab);
items.value = [];
return;
}
loading.value = true;
try {
const res = await getHotRankingApi(props.dimension, null, 1, 11);
if (res.code === 200 && res.data?.items) {
const res = await tab.fetch();
if (res && res.code === 200 && res.data?.items) {
// cover_url 访 URLOSS
items.value = await Promise.all(
res.data.items.map(async (item) => {
return await resolveItemUrls({
...item,
id: item.id || item.asset_id,
})
})
});
}),
);
} else {
items.value = [];
}
} catch (e) {
console.error("[HotCategoryBlock] 加载数据失败", e?.message ?? e);
items.value = [];
} finally {
loading.value = false;
}
@ -224,9 +281,81 @@ onUnmounted(() => {
<style scoped>
.hot-category-block {
padding: 19rpx 9.5rpx;
padding: 0 9.5rpx;
border-radius: 24rpx;
/* opacity: 0.8; */
position: relative;
}
/* Tab 栏 */
.ranking-tabs {
display: flex;
flex-direction: row;
align-items: center;
padding: 0 16rpx;
position: relative;
left: 50%;
transform: translate(-50%);
opacity: 0.8;
border-top-left-radius: 14px;
border-top-right-radius: 13px;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 7px;
z-index: 1; /* 在内容网格之下 */
width: 480rpx;
height: 80rpx;
background: linear-gradient(183.58deg, #ff5a5d -36.55%, #c2ebff 121.2%);
backdrop-filter: blur(11.699999809265137px);
}
.ranking-tab-item {
height: 80rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
flex-direction: column;
transition: all 0.25s ease;
position: absolute;
top: 24rpx;
}
.ranking-tab-item.active {
background: linear-gradient(
185.9deg,
rgba(255, 90, 140, 0.98) 5.54%,
rgba(194, 235, 255, 0.98) 184.09%
);
backdrop-filter: blur(1.7999999523162842px);
height: 144rpx;
width: 88rpx;
}
.ranking-tab-label {
font-size: 22rpx;
font-weight: 600;
color: rgba(255, 255, 255, 0.85);
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.4);
line-height: 1;
white-space: nowrap;
}
.ranking-tab-icon {
display: block;
position: absolute;
top: -8rpx;
}
.ranking-tab-item.active .ranking-tab-label {
color: #ffffff;
}
/* 骨架屏 */
.grid-skeleton {
display: flex;
flex-direction: column; /* 与 items-grid 保持一致:纵向 */
position: relative;
z-index: 2;
border-radius: 12px;
background: linear-gradient(
161.28deg,
rgba(255, 90, 93, 0.2) 16.63%,
@ -234,98 +363,39 @@ onUnmounted(() => {
rgba(255, 122, 124, 0.2) 83.71%
);
backdrop-filter: blur(9.300000190734863px);
position: relative;
}
.block-title {
position: absolute;
top: -24rpx;
left: 4rpx;
width: 188rpx;
height: 56rpx;
border-top-left-radius: 44rpx;
border-top-right-radius: 8rpx;
border-bottom-right-radius: 44rpx;
border-bottom-left-radius: 4rpx;
background: linear-gradient(
90deg,
rgba(255, 0, 4, 0.73) -3.96%,
rgba(254, 141, 103, 0.73) 57.95%,
rgba(252, 228, 75, 0.73) 97%
);
box-shadow: 2px 2px 4px 0px #d9262640;
backdrop-filter: blur(7.599999904632568px);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.recommend-text {
font-size: 24rpx;
color: #fff;
text-shadow: 0px 2px 8px #00000074;
font-weight: 600;
line-height: 100%;
letter-spacing: 0%;
}
/* 骨架屏 */
.grid-skeleton {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.skeleton-card {
width: calc(25% - 12rpx);
margin-bottom: 16rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
overflow: hidden;
}
/* 骨架屏第一排3个大图 */
.skeleton-card:nth-child(-n + 3) {
width: calc(33.333% - 12rpx);
}
.skeleton-image {
.skeleton-card {
width: 100%;
height: 192rpx;
background: linear-gradient(90deg, #3a3a4a 25%, #4a4a5a 50%, #3a3a4a 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
/* 骨架屏:第一排图片更高 */
.skeleton-card:nth-child(-n + 3) .skeleton-image {
height: 236rpx;
border-top-left-radius: 8px;
border-top-right-radius: 24px;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 21px;
}
.skeleton-info {
height: 120rpx; /* 与新卡片 row 高度一致 */
background: rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
display: flex;
align-items: center;
padding: 16rpx;
padding: 12rpx 16rpx;
box-sizing: border-box;
}
.skeleton-avatar {
width: 40rpx;
height: 40rpx;
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: #3a3a4a;
margin-right: 8rpx;
flex-shrink: 0;
}
.skeleton-name {
width: 100rpx;
height: 24rpx;
flex: 1;
height: 28rpx;
background: #3a3a4a;
border-radius: 8rpx;
margin-left: 16rpx;
}
.skeleton-image,
.skeleton-info {
display: none; /* 新布局下不再使用 */
}
@keyframes shimmer {
@ -341,222 +411,182 @@ onUnmounted(() => {
/* 内容网格 */
.items-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
flex-direction: column; /* 改为纵向排列,每张卡片独占一行 */
position: relative;
z-index: 2; /* 在 tab 栏之上 */
/* 背景与父容器一致,看上去与 tab 栏融成一体 */
background: linear-gradient(
161.28deg,
rgba(255, 90, 93, 0.2) 16.63%,
rgba(76, 237, 255, 0.2) 48.19%,
rgba(255, 122, 124, 0.2) 83.71%
);
backdrop-filter: blur(9.300000190734863px);
border-radius: 12px;
padding: 40rpx 20rpx 0;
overflow: hidden;
}
.grid-card {
width: calc(25% - 12rpx);
width: 100%;
border-radius: 16rpx;
/* overflow: hidden; */
position: relative;
/* background: rgba(255, 255, 255, 0.15); */
/* box-shadow: 2px 2px 4.5px 0px #f04b4b40; */
box-shadow: 2px 4px 4px 0px #c92f2f5c;
margin-bottom: 48rpx;
}
/* 第一排3个大图突出显示 */
.grid-card:nth-child(-n + 3) {
width: calc(33% - 20rpx);
}
.grid-card:nth-child(-n + 3) .card-image {
height: 264rpx;
box-shadow: 3px 3px 4.5px 2px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(0px);
/* padding: 8rpx; */
/* 单行布局:藏品图片 + 头像 + 点赞信息 + TOP 标签 */
.card-row {
display: flex;
align-items: center;
padding: 0 16rpx;
width: 100%;
height: 104rpx;
box-sizing: border-box;
}
.card-image-wrap {
position: relative;
width: 90rpx;
height: 120rpx;
}
.card-image {
width: 100%;
height: 224rpx;
height: 100%;
border-radius: 12rpx;
display: block;
position: absolute;
top: -16rpx;
left: 32rpx;
}
/* 前三名专属:包裹 card-bottom 的边框 */
.card-frame {
/* 前 3 名专属:左上角奖牌装饰 */
.card-medal {
position: absolute;
top: 0;
left: 0;
top: -32rpx;
left: 96rpx;
width: 48rpx;
height: 48rpx;
z-index: 5;
pointer-events: none;
transform: rotate(45deg);
}
/* 前 3 名专属:卡片整体突出 */
.grid-card-top {
}
.grid-card-top-1 {
}
.grid-card-top-2 {
}
.grid-card-top-3 {
}
/* 前 3 名专属:包裹藏品图的边框图(叠加在 card-image 之上) */
.frame-image {
position: absolute;
top: -16rpx;
left: 32rpx;
width: 100%;
height: 100%;
z-index: 4;
pointer-events: none;
}
.frame-image {
width: 100%;
height: 100%;
display: block;
}
.corner-decoration {
position: absolute;
top: -16rpx;
right: -16rpx;
width: 64rpx;
height: 64rpx;
z-index: 6;
pointer-events: none;
transform: rotate(60deg);
}
.corner-decoration.top-corner-decoration {
width: 56rpx;
height: 56rpx;
left: -16rpx;
right: 0;
}
.corner-decoration image {
width: 100%;
height: 100%;
display: block;
}
/* 底部信息模块 - 独立模块,有背景色和圆角 */
.card-bottom {
background: rgba(255, 255, 255, 0.15);
border-radius: 16rpx;
overflow: hidden;
position: relative;
box-shadow: 2px 2px 4.5px 0px #F04B4B40;
}
.card-bottom-1,
.card-bottom-2,
.card-bottom-3 {
border-radius: 28rpx;
}
/* Top 排名标签 */
.top-badge {
width: 80rpx;
height: 32rpx;
border-radius: 16rpx;
margin: 16rpx auto;
background: linear-gradient(
93.1deg,
rgba(224, 180, 247, 0.71) -12.06%,
rgba(178, 246, 204, 0.71) 52.09%,
rgba(98, 178, 244, 0.71) 163.5%
);
backdrop-filter: blur(11.699999809265137px);
/* overflow: hidden; */
}
.top-badge-1,
.top-badge-2,
.top-badge-3 {
padding-left: 24rpx;
}
.badge-rank {
width: 80rpx;
height: 32rpx;
color: #fffabd;
font-size: 18rpx;
font-weight: 600;
border-radius: 16rpx;
.like-info {
display: flex;
align-items: center;
flex-direction: column; /* 从上往下:用户名 → 编号 → 点赞 */
justify-content: center;
text-shadow: -1px 1px 4px #ce0909d6;
}
/* 用户信息 */
.card-info {
width: 100%;
display: flex;
align-items: center;
padding: 8rpx;
border-top-left-radius: 7px;
border-top-right-radius: 7px;
position: absolute;
bottom: 0;
background: linear-gradient(
177.83deg,
rgba(235, 228, 219, 0) 11.38%,
rgba(138, 135, 131, 0.4) 23.67%,
rgba(255, 231, 231, 0.6) 43.04%,
rgba(255, 255, 255, 0.9) 67.52%,
#ffffff 98.2%
);
backdrop-filter: blur(0px);
}
.card-info-1,
.card-info-2,
.card-info-3 {
padding-bottom: 16rpx;
}
.user-info {
display: flex;
align-items: flex-end;
}
.user-avatar {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
margin-right: 8rpx;
margin-left: 64rpx; /* 距头像 10rpx */
flex: 1;
min-width: 0;
}
.user-name {
font-size: 18rpx;
font-weight: 400;
color: #554545;
max-width: 120rpx;
overflow: hidden;
font-size: 20rpx;
font-weight: 800;
color: #fffabd;
text-shadow: -1px 1px 4px #ce0909d6;
white-space: nowrap;
white-space: nowrap;
text-overflow: ellipsis;
}
.user-number {
font-size: 20rpx;
font-weight: 800;
line-height: 1.3;
color: #fffabd;
text-shadow: -1px 1px 4px #ce0909d6;
white-space: nowrap;
}
.like-badge {
position: absolute;
top: 0;
left: 0;
width: 80rpx;
height: 64rpx;
opacity: 1;
border-top-left-radius: 7px;
border-bottom-right-radius: 21.5px;
background: linear-gradient(
177.83deg,
rgba(83, 244, 211, 0.2) 2.52%,
rgba(15, 9, 0, 0) 69.07%
);
backdrop-filter: blur(0px);
z-index: 5;
}
.like-badge-1,
.like-badge-2,
.like-badge-3 {
padding: 10rpx 0 0 10rpx;
}
.like-icon-wrapper {
.like-row {
display: flex;
align-items: center;
padding: 8rpx;
margin-top: 6rpx;
}
.like-badge .like-icon {
.like-info .like-icon {
width: 24rpx;
height: 24rpx;
margin-right: 6rpx;
margin-right: 4rpx;
}
.like-badge .like-count {
font-size: 20rpx;
.like-info .like-count {
font-size: 28rpx;
font-weight: 400;
line-height: 100%;
letter-spacing: 0%;
color: #fffabd;
text-shadow:
-1px 1px 4px #ce0909d6,
0px 0px 10px #fffabd;
text-shadow: -1px 1px 4px #ce0909d6;
white-space: nowrap;
}
/* Top 排名标签(顶到右边) */
.top-badge {
margin-left: auto; /* 推到右侧 */
min-width: 100rpx;
height: 36rpx;
border-radius: 18rpx;
padding: 0 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.badge-rank {
display: flex;
align-items: baseline;
justify-content: center;
}
.badge-rank-icon {
width: 90.24rpx;
height: 40rpx;
margin-right: 5px;
}
.badge-rank-number {
font-size: 84rpx;
font-weight: 600;
/* 渐变填充到文字 */
background: linear-gradient(
181.98deg,
#fcfcf8 23.78%,
#ffb3eb 66.88%,
#ffeded 94.93%
);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
white-space: nowrap;
}
/* 点赞动效波纹 */

View File

@ -38,27 +38,29 @@
<ContentTabs ref="contentTabsRef" class="tabs" :modelValue="activeContentTab"
@update:modelValue="activeContentTab = $event" />
<!-- 在线榜单区块 -->
<view ref="hotCategoryRef" class="hot-category-wrapper">
<HotCategoryBlock :dimension="activeContentTab" @cardClick="handleCardClick" />
<view class="hot-more-btn" @click="goToHotCategoryMore">
<text class="hot-more-text">查看更多</text>
</view>
<!-- 在线榜单区块 - 仅在 星榜 时显示 -->
<view
v-if="activeContentTab === 'xingbang'"
ref="hotCategoryRef"
class="hot-category-wrapper"
>
<HotCategoryBlock @cardClick="handleCardClick" />
</view>
<!-- 区域二//四已合并到 CreationGrid 组件主Tab + 分类标签 + 网格列表 -->
<CreationGrid :screenWidth="screenWidth" :screenHeight="screenHeight" :bannerBottom="bannerBottomPx"
:category="activeCategoryTab" :isActive="isActive"
@cardClick="handleCardClick" @loaded="onGridLoaded"
@mainTabClick="handleMainTabClick" @categoryChange="handleCategoryChange"
ref="creationGridRef" />
<!-- CreationGrid 组件主Tab + 分类标签 + 网格列表 - 仅在 广场 时显示 -->
<CreationGrid
v-if="activeContentTab === 'guangchang'"
@cardClick="handleCardClick"
@loaded="onGridLoaded"
ref="creationGridRef"
/>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import { ref, watch, onMounted, onUnmounted, nextTick } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { useStore } from "vuex";
import Header from "../components/Header.vue";
import BottomNav from "../components/BottomNav.vue";
@ -79,11 +81,9 @@ const store = useStore();
const currentStarId = ref(uni.getStorageSync("star_id") || null);
// ========== UI State ==========
const activeContentTab = ref("displaying");
const activeCategoryTab = ref("hot");
const activeContentTab = ref("xinghe");
const navExpanded = ref(false);
const showRankingModal = ref(false);
const isActive = ref(true);
const creationGridRef = ref(null);
const cardTapTimers = {};
const likingMap = ref({});
@ -99,7 +99,7 @@ const allSpotlightRefs = () => {
const sections = [
bannerSectionRef.value,
contentTabsRef.value?.$el || contentTabsRef.value,
hotCategoryRef.value,
// hotCategoryRef.value,
creationGridRef.value?.mainTabsRef?.value,
creationGridRef.value?.categoryRef?.value,
].filter(Boolean)
@ -129,13 +129,10 @@ const onTouchMove = () => {
update()
}
// tab / spotlight
// tab spotlight
watch(activeContentTab, () => {
nextTick(() => setTimeout(update, 50));
});
watch(activeCategoryTab, () => {
nextTick(() => setTimeout(update, 50));
});
// spotlight 200ms update
const onGridLoaded = (count) => {
@ -144,18 +141,9 @@ const onGridLoaded = (count) => {
}
};
// ========== Screen Info ==========
const screenWidth = ref(375);
const screenHeight = ref(812);
// ========== Composables ==========
const { bannerActivities, banners, loadBannerActivities, loadBanners } = useBanner();
// banner(216+360rpx) + tab(16+80rpx) + (8rpx) 680rpx
const bannerBottomPx = computed(() =>
Math.round((screenWidth.value / 750) * 715),
);
// ========== Handlers ==========
const handleCardClick = (card) => {
@ -233,18 +221,6 @@ const handleRankingModalClose = (visible) => {
}
};
const goToHotCategoryMore = () => {
const title =
activeContentTab.value === "displaying"
? "日榜"
: activeContentTab.value === "week"
? "周榜"
: "月榜";
uni.navigateTo({
url: `/pages/square/hot-category-more?dimension=${activeContentTab.value}&title=${encodeURIComponent(title)}`,
});
};
const handleTabChange = (newTab) => {
// if (newTab === 4) {
// navExpanded.value = false;
@ -266,18 +242,7 @@ const handleTabChange = (newTab) => {
}
};
// Tab -
const handleMainTabClick = (tab) => {
// mall (mall craft-select ,), type
uni.navigateTo({
url: `/pages/castlove/mall?type=${encodeURIComponent(tab.type)}`,
});
};
const handleCategoryChange = (value) => {
if (activeCategoryTab.value === value) return;
activeCategoryTab.value = value;
};
// Tab / CreationGrid
// ========== Tile Change Callback ==========
const handleTileChange = () => { };
@ -287,10 +252,6 @@ const resetSquare = async () => { };
// ========== Lifecycle ==========
onMounted(() => {
const info = uni.getSystemInfoSync();
screenWidth.value = info.windowWidth;
screenHeight.value = info.windowHeight;
resetSquare();
loadBannerActivities();
loadBanners();
@ -303,18 +264,12 @@ onMounted(() => {
});
onShow(() => {
isActive.value = true;
activeContentTab.value = "displaying";
activeCategoryTab.value = "hot";
activeContentTab.value = "xinghe";
nextTick(() => {
setTimeout(update, 80);
});
});
onHide(() => {
isActive.value = false;
});
// onLoad((options) => {
// if (options && 'guide_debug' in options) {
// const debugValue = options.guide_debug
@ -414,31 +369,6 @@ onUnmounted(() => {
padding-bottom: 80rpx;
}
.hot-more-btn {
position: absolute;
bottom: 0;
right: 16rpx;
z-index: 10;
width: 104rpx;
height: 40rpx;
border-radius: 20rpx;
opacity: 0.66;
padding: 8rpx 20rpx;
background: linear-gradient(90deg,
rgba(255, 222, 8, 0.61) -17.54%,
rgba(255, 0, 25, 0.61) 116.67%);
box-shadow: 2px 2px 4px 0px #f2151578;
display: flex;
justify-content: center;
align-items: center;
}
.hot-more-text {
font-size: 22rpx;
color: #fff;
font-weight: 600;
}
/* 蒙层 */
.nav-mask {
position: fixed;

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB