feat: 重构广场页面
This commit is contained in:
parent
77ca675bd0
commit
4211108bd6
@ -92,6 +92,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/myWorks",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"app-plus": {
|
||||
"bounce": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/exhibition/exhibition",
|
||||
"style": {
|
||||
|
||||
@ -1,47 +1,7 @@
|
||||
<template>
|
||||
<view class="banner-top3">
|
||||
<image class="banner-bg" src="/static/rank/rank-bg.png" mode="aspectFill" />
|
||||
<image class="banner-bg banner-mask" src="/static/rank/bg-text.png" mode="aspectFill" />
|
||||
<view
|
||||
v-for="(item, index) in top3"
|
||||
:key="item.asset_id || index"
|
||||
class="top3-card"
|
||||
>
|
||||
<view class="artwork-container">
|
||||
<image class="artwork-image" :src="item.cover_url || '/static/avatar/1.jpeg'" mode="aspectFill" />
|
||||
|
||||
<!-- 人气值 -->
|
||||
<!--<view class="popularity-overlay">
|
||||
<image class="fire-icon" src="/static/rank/spark.png" mode="aspectFit" />
|
||||
<text class="popularity-score">{{ formatScore(item.like_count) }}</text>
|
||||
</view>-->
|
||||
|
||||
<!-- 排名徽章 -->
|
||||
<view class="rank-badge-bottom">
|
||||
<image class="rank-icon" :src="`/static/rank/charm-rank-icon${index + 1}.png`" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="rank-badge-bottom rank-badge-bottom2">
|
||||
<image class="rank-icon" :src="`/static/rank/rank-icon${index + 1}.png`" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<view class="user-info-with-artwork">
|
||||
<image
|
||||
class="user-avatar-small"
|
||||
:src="item.owner_avatar || '/static/avatar/1.jpeg'"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="user-nickname">用户 :
|
||||
<text class="user-nickname-name">{{ item.owner_nickname || '未知用户' }}</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="loading" class="skeleton-wrap">
|
||||
<view v-for="i in 3" :key="i" class="skeleton-card" />
|
||||
</view>
|
||||
<!-- 只渲染背景图,卡片由父组件 BannerCarousel 渲染在 swiper 外层 -->
|
||||
<view class="banner-top3-bg" @tap="onBannerTap">
|
||||
<image class="banner-bg" src="/static/square/paihangbang.png" mode="aspectFill" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -49,17 +9,8 @@
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { getHotRankingApi, getOssPresignedUrlApi } from '@/utils/api.js';
|
||||
|
||||
const top3 = ref([]);
|
||||
const loading = ref(true);
|
||||
const emit = defineEmits(['dataLoaded']);
|
||||
|
||||
const formatScore = (score) => {
|
||||
if (typeof score !== 'number' || isNaN(score) || score < 0) return '0';
|
||||
if (score >= 1000000) return (score / 1000000).toFixed(1) + 'M';
|
||||
if (score >= 1000) return (score / 1000).toFixed(1) + 'K';
|
||||
return score.toString();
|
||||
};
|
||||
|
||||
// 获取 OSS 预签名 URL,失败时降级返回原值
|
||||
const resolveOssUrl = async (fileName, type) => {
|
||||
if (!fileName) return '';
|
||||
try {
|
||||
@ -72,199 +23,45 @@ const resolveOssUrl = async (fileName, type) => {
|
||||
};
|
||||
|
||||
const loadTop3 = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getHotRankingApi('total', null, 1, 3);
|
||||
if (res.code === 200 && res.data?.items) {
|
||||
const items = res.data.items.slice(0, 3);
|
||||
// 并发解析所有封面图和头像的 OSS 预签名 URL
|
||||
top3.value = await Promise.all(items.map(async (item) => {
|
||||
const resolved = await Promise.all(items.map(async (item) => {
|
||||
const [coverUrl, avatarUrl] = await Promise.all([
|
||||
resolveOssUrl(item.cover_url || '', 'asset'),
|
||||
resolveOssUrl(item.avatar_url || '', 'avatar'),
|
||||
]);
|
||||
return { ...item, cover_url: coverUrl, avatar_url: avatarUrl };
|
||||
}));
|
||||
emit('dataLoaded', resolved);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[BannerTop3] 加载失败', e?.message ?? e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onBannerTap = () => {
|
||||
uni.navigateTo({ url: '/pages/rank/rank' });
|
||||
};
|
||||
|
||||
onMounted(loadTop3);
|
||||
defineExpose({ reload: loadTop3 });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.banner-top3 {
|
||||
.banner-top3-bg {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
justify-content: space-around;
|
||||
padding: 0 16rpx;
|
||||
box-sizing: border-box;
|
||||
min-height: 200rpx;
|
||||
height: 360rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.banner-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 105%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ===== 复用 TOP3Card 样式 ===== */
|
||||
.top3-card {
|
||||
width: 25%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
/* gap: 32rpx; */
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.rank-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
/* 有作品布局 */
|
||||
.artwork-container {
|
||||
width: 90%;
|
||||
height: 208rpx;
|
||||
margin-top: 16rpx;
|
||||
margin-bottom: 32rpx;
|
||||
background-image: url('/static/rank/frames/frame1.png');
|
||||
background-size: 130% 115%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.artwork-image {
|
||||
width: calc(100% - 16rpx);
|
||||
height: calc(100% - 24rpx);
|
||||
top:16rpx;
|
||||
left:8rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.popularity-overlay {
|
||||
position: absolute;
|
||||
top: 24rpx;
|
||||
left: 0;
|
||||
display: flex;
|
||||
gap: 4rpx;
|
||||
align-items: center;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.popularity-overlay .fire-icon {
|
||||
width: 32rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.popularity-overlay .popularity-score {
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
color: #FFFFFF;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.5), 0 1rpx 2rpx rgba(0, 0, 0, 0.3);
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
}
|
||||
|
||||
.rank-badge-bottom {
|
||||
position: absolute;
|
||||
bottom: -40rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scale(1.5);
|
||||
z-index: 10;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rank-badge-bottom2 {
|
||||
bottom: -50rpx;
|
||||
transform: translateX(-50%) scale(2);
|
||||
}
|
||||
|
||||
.user-info-with-artwork {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
/* gap: 8rpx; */
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
padding: 0 12rpx;
|
||||
}
|
||||
|
||||
.user-avatar-small {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
border: 3rpx solid rgba(255, 255, 255, 0.7);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-nickname {
|
||||
font-size: 14rpx;
|
||||
margin-left: 8rpx;
|
||||
color: #FFFFFF;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 3rpx 6rpx rgba(0, 0, 0, 0.4), 0 1rpx 3rpx rgba(0, 0, 0, 0.3);
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
}
|
||||
|
||||
.user-nickname-name {
|
||||
font-size: 14rpx;
|
||||
color: #FFA500;
|
||||
text-shadow: 0 3rpx 6rpx rgba(0, 0, 0, 0.4), 0 1rpx 3rpx rgba(0, 0, 0, 0.3);
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
}
|
||||
|
||||
/* 骨架屏 */
|
||||
.skeleton-wrap {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 0 16rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
width: 25%;
|
||||
height: 180rpx;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(255,255,255,0.06) 25%,
|
||||
rgba(255,255,255,0.14) 50%,
|
||||
rgba(255,255,255,0.06) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.4s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
<!-- 1. 上层:任务图标(悬浮在上面) -->
|
||||
<view class="task-icon-box">
|
||||
<image class="task-icon-img" src="/static/icon/task.png" mode="aspectFit"></image>
|
||||
<view class="task-red-dot"></view>
|
||||
<image class="task-red-dot" src="/static/square/tishi.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
||||
<!-- 2. 下层:文字背景块 -->
|
||||
@ -26,7 +26,7 @@
|
||||
<!-- 1. 上层:新手引导(悬浮在上面) -->
|
||||
<view class="task-icon-box">
|
||||
<image class="task-icon-img" src="/static/icon/onboarding-bg.png" mode="aspectFit"></image>
|
||||
<view class="task-red-dot"></view>
|
||||
<image class="task-red-dot" src="/static/square/tishi.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
||||
<!-- 2. 下层:文字背景块 -->
|
||||
@ -40,7 +40,7 @@
|
||||
<!-- 1. 上层:星援活动(悬浮在上面) -->
|
||||
<view class="task-icon-box">
|
||||
<image class="task-icon-img" src="/static/icon/bus-icon.png" mode="aspectFit"></image>
|
||||
<view class="task-red-dot"></view>
|
||||
<image class="task-red-dot" src="/static/square/tishi.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
||||
<!-- 2. 下层:文字背景块 -->
|
||||
@ -58,13 +58,14 @@
|
||||
|
||||
<!-- 2. 右侧容器:用于包裹背景和文字 -->
|
||||
<view class="crystal-info-container">
|
||||
<image class="crystal-bg-img" src="/static/square/shuijingzhanshikuang.png" mode="aspectFill"></image>
|
||||
|
||||
<!-- 上层:文字内容(层级中间,盖住背景) -->
|
||||
<!-- 上层:文字内容 -->
|
||||
<view class="crystal-text-layer">
|
||||
<text class="balance-number">{{ crystalBalance }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 下层:渐变背景(层级最低,被文字盖住,但盖住容器底色) -->
|
||||
<!-- 收益文字 -->
|
||||
<view class="crystal-bg-layer">
|
||||
<text class="balance-income">收益 27.1/H</text>
|
||||
</view>
|
||||
@ -454,14 +455,14 @@ defineExpose({
|
||||
/* --- 上层图标样式 --- */
|
||||
.task-icon-box {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: 8rpx;
|
||||
/* 固定在顶部 */
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
/* 水平居中 */
|
||||
width: 90rpx;
|
||||
width: 88rpx;
|
||||
/* 图标宽度 */
|
||||
height: 90rpx;
|
||||
height: 88rpx;
|
||||
/* 图标高度 */
|
||||
z-index: 10;
|
||||
/* 确保图标在文字块上面 */
|
||||
@ -475,13 +476,10 @@ defineExpose({
|
||||
/* 红点样式 */
|
||||
.task-red-dot {
|
||||
position: absolute;
|
||||
top: 6rpx;
|
||||
right: 6rpx;
|
||||
top: 8rpx;
|
||||
right: 12rpx;
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
background-color: red;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #fff;
|
||||
}
|
||||
|
||||
/* --- 下层文字背景块 --- */
|
||||
@ -493,42 +491,22 @@ defineExpose({
|
||||
transform: translateX(-50%);
|
||||
width: 88rpx;
|
||||
height: 80rpx;
|
||||
background: linear-gradient(to bottom right,
|
||||
#F0E4B1 0%,
|
||||
/* 左:浅橙粉 */
|
||||
#F08399 50%,
|
||||
#B94E73 100%
|
||||
/* 右:柔粉红 */
|
||||
);
|
||||
|
||||
|
||||
/* 立体感核心:多层阴影 + 内阴影模拟凸起 */
|
||||
box-shadow:
|
||||
/* 外层投影 - 让按钮浮起 */
|
||||
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
|
||||
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
|
||||
|
||||
/* 内阴影 - 模拟顶部受光 + 底部凹陷 */
|
||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4),
|
||||
/* 顶部高光 */
|
||||
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05);
|
||||
/* 底部暗部 */
|
||||
/* 粉色渐变背景,可微调 */
|
||||
background-image: url('/static/square/gerenzhongxincangpinkuang.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 5;
|
||||
/* 必须比图标小,才能被覆盖 */
|
||||
box-shadow: 0 4rpx 10rpx rgba(255, 107, 187, 0.3);
|
||||
/* 可选:增加一点阴影层次感 */
|
||||
}
|
||||
|
||||
.task-text-label {
|
||||
font-size: 18rpx;
|
||||
font-size: 16rpx;
|
||||
color: #fff;
|
||||
text-shadow: 1rpx 1rpx 2rpx rgba(255, 255, 255, 0.5);
|
||||
margin-top: 24rpx;
|
||||
margin-top: 32rpx;
|
||||
|
||||
}
|
||||
|
||||
@ -564,17 +542,23 @@ defineExpose({
|
||||
/* --- 右侧容器 --- */
|
||||
.crystal-info-container {
|
||||
position: relative;
|
||||
/* 自动撑开宽度 */
|
||||
height: 56rpx;
|
||||
width: 80rpx;
|
||||
/* 容器高度 */
|
||||
border-radius: 16rpx;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
bottom: 4rpx;
|
||||
padding-right: 28rpx;
|
||||
padding-left: 56rpx;
|
||||
}
|
||||
|
||||
.crystal-bg-img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* --- 上层:文字内容 --- */
|
||||
.crystal-text-layer {
|
||||
position: relative;
|
||||
@ -618,30 +602,19 @@ defineExpose({
|
||||
width: 100%;
|
||||
height: 65%;
|
||||
z-index: 1;
|
||||
/* 关键:层级低于文字,高于透明底色 */
|
||||
|
||||
background: linear-gradient(to bottom right,
|
||||
#F0E4B1 0%,
|
||||
/* 左:浅橙粉 */
|
||||
#F08399 50%,
|
||||
#B94E7399 100%
|
||||
/* 右:柔粉红 */
|
||||
);
|
||||
|
||||
|
||||
/* 立体感核心:多层阴影 + 内阴影模拟凸起 */
|
||||
box-shadow:
|
||||
/* 外层投影 - 让按钮浮起 */
|
||||
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
|
||||
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
|
||||
|
||||
/* 内阴影 - 模拟顶部受光 + 底部凹陷 */
|
||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4),
|
||||
/* 顶部高光 */
|
||||
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05);
|
||||
/* 底部暗部 */
|
||||
border-radius: 10rpx;
|
||||
|
||||
}
|
||||
|
||||
.icon-item {
|
||||
|
||||
589
frontend/pages/profile/myWorks.vue
Normal file
589
frontend/pages/profile/myWorks.vue
Normal file
@ -0,0 +1,589 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 背景图片 -->
|
||||
<image class="bg-image" src="/static/square/beijingban.png" mode="aspectFill"></image>
|
||||
|
||||
<!-- 顶部导航 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<image class="nav-back-icon" src="/static/icon/back.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<!-- <text class="nav-title">我的作品</text> -->
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<view class="scroll-content">
|
||||
|
||||
<!-- 我的在展作品 -->
|
||||
<view class="section-block section-1">
|
||||
<view class="section-label">
|
||||
<image class="section-label-bg" src="/static/nft/dingbutubiao_liang.png" mode="aspectFill"></image>
|
||||
<text class="section-label-text">我的在展作品</text>
|
||||
</view>
|
||||
|
||||
<view class="exhibition-grid">
|
||||
<view
|
||||
v-for="(item, index) in exhibitionWorks"
|
||||
:key="item.id"
|
||||
class="exhibition-card"
|
||||
:class="index % 2 === 0 ? 'card-tilt-left' : 'card-tilt-right'"
|
||||
@tap="goToAssetDetail(item.id)"
|
||||
>
|
||||
<image class="card-image" :src="item.cover_url || '/static/nft/placeholder.png'" mode="aspectFill"></image>
|
||||
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill"></image>
|
||||
<!-- 点赞数 -->
|
||||
<view class="card-rate-badge">
|
||||
<image class="heart-icon" src="/static/icon/heart-icon.png" mode="aspectFit"></image>
|
||||
<view class="card-rate-text-wrap">
|
||||
<text class="card-rate-text">{{ item.like_count || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 图片下方收益 -->
|
||||
<view class="card-income-row" :class="index % 2 === 0 ? 'income-tilt-right' : 'income-tilt-left'">
|
||||
<image class="topfans-icon" src="/static/icon/crystal.png" mode="aspectFit"></image>
|
||||
<view class="card-income-text-wrap">
|
||||
<text class="card-income-text">{{ item.rate || '0' }}/H</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态占位 -->
|
||||
<view v-if="exhibitionWorks.length === 0" class="empty-exhibition">
|
||||
<text class="empty-text">暂无在展作品</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日点赞作品 -->
|
||||
<view class="section-block">
|
||||
<view class="section-label">
|
||||
<image class="section-label-bg" src="/static/nft/dingbutubiao_liang.png" mode="aspectFill"></image>
|
||||
<text class="section-label-text">今日点赞作品</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="liked-list" scroll-y="true" :show-scrollbar="false">
|
||||
<view
|
||||
v-for="(item, index) in likedWorks"
|
||||
:key="item.id"
|
||||
class="liked-row"
|
||||
@tap="goToAssetDetail(item.id)"
|
||||
>
|
||||
<!-- 排名图标,绝对定位在卡片左侧 -->
|
||||
<image
|
||||
v-if="index < 3"
|
||||
:src="rankIcons[index]"
|
||||
class="rank-icon-img"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
|
||||
<!-- 卡片主体 -->
|
||||
<view class="liked-item" :class="index === 0 ? 'liked-item-first' : ''">
|
||||
<!-- 作品封面 -->
|
||||
<view class="liked-cover-wrap" :class="index === 0 ? 'liked-cover-wrap-first' : ''">
|
||||
<image class="liked-cover" :src="item.cover_url || '/static/nft/placeholder.png'" mode="aspectFill"></image>
|
||||
<image class="liked-cover-frame" src="/static/square/cangpinkuang1.png" mode="aspectFill"></image>
|
||||
</view>
|
||||
|
||||
<!-- 作品信息 -->
|
||||
<view class="liked-info">
|
||||
<text class="liked-status">{{ item.status_text }}</text>
|
||||
<view class="liked-score-row">
|
||||
<text class="liked-score">{{ formatScore(item.score) }}</text>
|
||||
<image class="fire-icon" src="/static/square/rementubiao.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧奖励 -->
|
||||
<view class="liked-reward">
|
||||
<image class="reward-token-icon" src="/static/icon/crystal.png" mode="aspectFit"></image>
|
||||
<text class="reward-amount">+{{ item.reward }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="likedWorks.length === 0" class="empty-liked">
|
||||
<text class="empty-text">今日暂无点赞作品</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- <view style="height: 60rpx;"></view> -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
const goToAssetDetail = (id) => {
|
||||
if (!id) return;
|
||||
uni.navigateTo({ url: `/pages/asset-detail/asset-detail?id=${id}` });
|
||||
};
|
||||
|
||||
const rankIcons = [
|
||||
'/static/rank/rank-icon1.png',
|
||||
'/static/rank/rank-icon2.png',
|
||||
'/static/rank/rank-icon3.png',
|
||||
];
|
||||
|
||||
const formatScore = (score) => {
|
||||
if (!score && score !== 0) return '0';
|
||||
return Number(score).toLocaleString();
|
||||
};
|
||||
|
||||
// 在展作品列表
|
||||
const exhibitionWorks = ref([]);
|
||||
|
||||
// 今日点赞作品列表
|
||||
const likedWorks = ref([]);
|
||||
|
||||
// 模拟数据(实际接入API时替换)
|
||||
const loadMockData = () => {
|
||||
exhibitionWorks.value = [
|
||||
{ id: '1', cover_url: '/static/sucai/image-08.png', owner_name: 'u585', like_count: 1234, rate: '0.7' },
|
||||
{ id: '2', cover_url: '/static/sucai/image-11.png', owner_name: 'u585', like_count: 856, rate: '0.6' },
|
||||
];
|
||||
|
||||
likedWorks.value = [
|
||||
{ id: '1', cover_url: '/static/sucai/image-03.png', status_text: '排名破100', score: 1354321, reward: 20 },
|
||||
{ id: '2', cover_url: '/static/sucai/image-04.png', status_text: '排名破300', score: 354321, reward: 17 },
|
||||
{ id: '3', cover_url: '/static/sucai/image-05.png', status_text: '热度飙升中', score: 14321, reward: 15 },
|
||||
{ id: '4', cover_url: '/static/sucai/image-06.png', status_text: '潜力待挖中', score: 321, reward: 8 },
|
||||
{ id: '5', cover_url: '/static/sucai/image-07.png', status_text: '潜力待挖中', score: 89, reward: 3 },
|
||||
];
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadMockData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 背景图片 */
|
||||
.bg-image {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* 导航栏 */
|
||||
.nav-bar {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 80rpx 32rpx 16rpx;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* background: rgba(255,255,255,0.5);
|
||||
border-radius: 50%; */
|
||||
}
|
||||
|
||||
.nav-back-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #5a2d82;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 64rpx;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.scroll-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
/* padding: 0 32rpx; */
|
||||
}
|
||||
|
||||
/* 区块 */
|
||||
.section-1 {
|
||||
margin-bottom: 8rpx;
|
||||
background: rgb(249 159 192 / 45%);
|
||||
border-radius: 48rpx;
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
/* 区块标签 */
|
||||
.section-label {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16rpx;
|
||||
height: 80rpx;
|
||||
min-width: 232rpx;
|
||||
}
|
||||
|
||||
.section-label-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.section-label-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
padding: 0 28rpx;
|
||||
}
|
||||
|
||||
/* 在展作品网格 */
|
||||
.exhibition-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
/* gap: 24rpx; */
|
||||
padding: 10rpx 10rpx 80rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.exhibition-card {
|
||||
width: 248rpx;
|
||||
height: 380rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-tilt-left {
|
||||
transform: rotate(-4deg) translateY(10rpx);
|
||||
margin-right: 32rpx;
|
||||
}
|
||||
|
||||
.card-tilt-right {
|
||||
transform: rotate(4deg) translateY(10rpx);
|
||||
}
|
||||
|
||||
.card-income-row.income-tilt-right {
|
||||
transform: translateX(-50%) rotate(4deg);
|
||||
}
|
||||
|
||||
.card-income-row.income-tilt-left {
|
||||
transform: translateX(-50%) rotate(-4deg);
|
||||
}
|
||||
|
||||
.card-image {
|
||||
width: 88%;
|
||||
height: 92%;
|
||||
border-radius: 80rpx;
|
||||
transform-origin: center center;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
padding: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-frame {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.card-user-tag {
|
||||
position: absolute;
|
||||
bottom: 56rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-user-text {
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
background: rgba(0,0,0,0.45);
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.card-rate-badge {
|
||||
position: absolute;
|
||||
bottom: 16rpx;
|
||||
left: 40%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
/* 图片下方收益 */
|
||||
.card-income-row {
|
||||
position: absolute;
|
||||
bottom: -52rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.topfans-icon {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-right: -16rpx;
|
||||
left: 20rpx;
|
||||
top: 8rpx
|
||||
}
|
||||
|
||||
.card-income-text-wrap {
|
||||
background: linear-gradient(to bottom right,
|
||||
#F0E4B1 0%,
|
||||
#F08399 50%,
|
||||
#B94E73 100%
|
||||
);
|
||||
border-radius: 999rpx;
|
||||
padding: 8rpx 20rpx 8rpx 40rpx;
|
||||
box-shadow:
|
||||
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
|
||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-income-text {
|
||||
font-size: 16rpx;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.heart-icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
}
|
||||
|
||||
.card-rate-text-wrap {
|
||||
background: linear-gradient(to bottom right,
|
||||
#F0E4B1 0%,
|
||||
#F08399 50%,
|
||||
#B94E73 100%
|
||||
);
|
||||
border-radius: 999rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
box-shadow:
|
||||
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
|
||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.card-rate-text {
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-exhibition {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #b09cc0;
|
||||
}
|
||||
|
||||
/* 今日点赞列表 */
|
||||
.liked-list {
|
||||
max-height: 732rpx;
|
||||
}
|
||||
|
||||
.liked-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.liked-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #ffffff50;
|
||||
border-radius: 32rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
gap: 16rpx;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
width: 80%;
|
||||
padding-left: 10%;
|
||||
}
|
||||
|
||||
.liked-item-first {
|
||||
padding: 28rpx 20rpx;
|
||||
width: 90%;
|
||||
padding-left: 20%;
|
||||
}
|
||||
|
||||
/* 排名徽章 */
|
||||
.rank-icon-img {
|
||||
width: 56rpx;
|
||||
height: 68rpx;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.rank-number-badge {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(180,140,220,0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rank-number-text {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 作品封面 */
|
||||
.liked-cover-wrap {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
flex-shrink: 0;
|
||||
margin-left: -18rpx;
|
||||
margin-right: 48rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* .liked-cover-wrap-first {
|
||||
width: 88rpx;
|
||||
height: 110rpx;
|
||||
} */
|
||||
|
||||
.liked-cover {
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
border-radius: 24rpx;
|
||||
transform: rotate(-22deg);
|
||||
transform-origin: center center;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.liked-cover-frame {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 110%;
|
||||
height: 110%;
|
||||
z-index: 2;
|
||||
transform: rotate(-22deg);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
|
||||
/* 作品信息 */
|
||||
.liked-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* gap: 8rpx; */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.liked-status {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.liked-score-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* gap: 6rpx; */
|
||||
}
|
||||
|
||||
.liked-score {
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.fire-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
align-self: flex-end;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* 右侧奖励 */
|
||||
.liked-reward {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reward-token-icon {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
}
|
||||
|
||||
.reward-amount {
|
||||
font-size: 28rpx;
|
||||
color: #c060e0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-liked {
|
||||
padding: 60rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@ -7,9 +7,10 @@
|
||||
:duration="400"
|
||||
:circular="true"
|
||||
:indicator-dots="false"
|
||||
@change="onSwiperChange"
|
||||
>
|
||||
<swiper-item @click.stop="$emit('top3Click')">
|
||||
<BannerTop3 />
|
||||
<BannerTop3 @dataLoaded="onTop3DataLoaded" />
|
||||
</swiper-item>
|
||||
<swiper-item
|
||||
v-for="item in bannerActivities"
|
||||
@ -23,28 +24,82 @@
|
||||
/>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<!-- 卡片层跟随第一个 swiper-item,切走时隐藏 -->
|
||||
<view
|
||||
class="cards-overlay"
|
||||
:style="{ opacity: currentIndex === 0 ? 1 : 0, pointerEvents: currentIndex === 0 ? 'auto' : 'none' }"
|
||||
@click.stop="$emit('top3Click')"
|
||||
>
|
||||
<view
|
||||
v-for="(item, index) in top3Items"
|
||||
:key="item.asset_id || index"
|
||||
class="card-wrapper"
|
||||
:class="`card-pos-${index}`"
|
||||
>
|
||||
<view class="card-frame">
|
||||
<view class="card-image-wrap">
|
||||
<image
|
||||
class="card-image"
|
||||
:src="item.cover_url || '/static/avatar/1.jpeg'"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<!-- 点赞数叠在图片底部 -->
|
||||
<view class="card-footer">
|
||||
<image class="card-heart" src="/static/icon/heart-icon.png" mode="aspectFit" />
|
||||
<view class="card-likes-wrap">
|
||||
<text class="card-likes">{{ formatLikes(item.like_count) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 边框叠在整张卡片上 -->
|
||||
<image class="card-frame-border" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="loading" class="skeleton-wrap">
|
||||
<view v-for="i in 3" :key="i" class="skeleton-card" :class="`skeleton-pos-${i - 1}`" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import BannerTop3 from '../../components/BannerTop3.vue'
|
||||
|
||||
defineProps({
|
||||
bannerActivities: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
bannerActivities: { type: Array, default: () => [] }
|
||||
})
|
||||
|
||||
defineEmits(['activityClick', 'top3Click'])
|
||||
|
||||
const top3Items = ref([])
|
||||
const loading = ref(true)
|
||||
const currentIndex = ref(0)
|
||||
|
||||
const onSwiperChange = (e) => {
|
||||
currentIndex.value = e.detail.current
|
||||
}
|
||||
|
||||
const onTop3DataLoaded = (items) => {
|
||||
const list = items.slice(0, 3)
|
||||
while (list.length < 3) list.push({ asset_id: `placeholder-${list.length}` })
|
||||
top3Items.value = list
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const formatLikes = (n) => {
|
||||
if (!n || isNaN(n)) return '0'
|
||||
if (n >= 10000) return (n / 10000).toFixed(1) + 'w'
|
||||
if (n >= 1000) return (n / 1000).toFixed(1) + 'k'
|
||||
return String(n)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.banner-carousel {
|
||||
position: fixed;
|
||||
top: 216rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
padding: 0 8rpx;
|
||||
@ -53,9 +108,8 @@ defineEmits(['activityClick', 'top3Click'])
|
||||
|
||||
.banner-swiper {
|
||||
width: 100%;
|
||||
height: 312rpx;
|
||||
height: 360rpx;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.banner-activity-img {
|
||||
@ -64,7 +118,151 @@ defineEmits(['activityClick', 'top3Click'])
|
||||
display: block;
|
||||
}
|
||||
|
||||
:deep(.uni-swiper-slide) {
|
||||
/* 卡片层:绝对定位叠在 swiper 上,可自由溢出 */
|
||||
.cards-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 8rpx;
|
||||
width: 420rpx;
|
||||
height: 360rpx;
|
||||
pointer-events: auto;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
position: absolute;
|
||||
width: 148rpx;
|
||||
height: 220rpx;
|
||||
}
|
||||
|
||||
.card-pos-0 {
|
||||
left: 50rpx;
|
||||
top: 40rpx;
|
||||
transform: rotate(-6deg);
|
||||
z-index: 3;
|
||||
}
|
||||
.card-pos-1 {
|
||||
left: 140rpx;
|
||||
top: -8rpx;
|
||||
transform: rotate(6deg);
|
||||
z-index: 4;
|
||||
}
|
||||
.card-pos-2 {
|
||||
left: 240rpx;
|
||||
top: 106rpx;
|
||||
transform: rotate(16deg);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.card-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 16rpx;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.card-image-wrap {
|
||||
width: 90%;
|
||||
height: 92%;
|
||||
position: relative;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
z-index: 5;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
.card-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.card-frame-border {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
position: absolute;
|
||||
bottom: 8rpx;
|
||||
left: 0;
|
||||
right: 64rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4rpx;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.card-bg-frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-heart {
|
||||
width: 22rpx;
|
||||
height: 22rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-likes-wrap {
|
||||
background: linear-gradient(to bottom right,
|
||||
#F0E4B1 0%,
|
||||
#F08399 50%,
|
||||
#B94E73 100%
|
||||
);
|
||||
border-radius: 8rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow:
|
||||
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
|
||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.card-likes {
|
||||
font-size: 12rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 骨架屏 */
|
||||
.skeleton-wrap {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
position: absolute;
|
||||
width: 148rpx;
|
||||
height: 220rpx;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(255,255,255,0.06) 25%,
|
||||
rgba(255,255,255,0.14) 50%,
|
||||
rgba(255,255,255,0.06) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.4s infinite;
|
||||
}
|
||||
|
||||
.skeleton-pos-0 { left: 50rpx; top: 40rpx; transform: rotate(-6deg); }
|
||||
.skeleton-pos-1 { left: 140rpx; top: -40rpx; transform: rotate(6deg); }
|
||||
.skeleton-pos-2 { left: 240rpx; top: 10rpx; transform: rotate(16deg); }
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
146
frontend/pages/square/components/ContentTabs.vue
Normal file
146
frontend/pages/square/components/ContentTabs.vue
Normal file
@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<view class="content-tabs" @click.stop>
|
||||
<view
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="tab.key"
|
||||
class="tab-item"
|
||||
:class="{ active: modelValue === tab.key }"
|
||||
@click="$emit('update:modelValue', tab.key)"
|
||||
>
|
||||
<!-- 背景图片 -->
|
||||
<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 === 0 }">
|
||||
<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' }"
|
||||
/>
|
||||
</view>
|
||||
<!-- 右侧文字 -->
|
||||
<text class="tab-label">{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelValue: { type: String, default: 'hot' }
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
|
||||
const tabs = [
|
||||
{ key: 'hot', label: '热门作品', emoji: null, icon: '/static/square/rementubiao.png', iconWidth: 104, iconHeight: 104 },
|
||||
{ key: 'xingka', label: '星卡', emoji: null, icon: '/static/square/xingka.png', iconWidth: 80, iconHeight: 80 },
|
||||
{ key: 'baji', label: '把爱', emoji: null, icon: '/static/square/baji.png', iconWidth: 80, iconHeight: 80 },
|
||||
{ key: 'haibao', label: '海报', emoji: null, icon: '/static/square/haibao.png', iconWidth: 96, iconHeight: 96 },
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-tabs {
|
||||
position: relative;
|
||||
top: unset;
|
||||
left: unset;
|
||||
right: unset;
|
||||
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;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
max-width: 160rpx;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 56rpx;
|
||||
margin: 0 4rpx;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* 背景图片铺满整个 tab */
|
||||
.tab-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 未选中时加透明度 */
|
||||
.tab-bg-inactive {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* 左侧图标,绝对浮动覆盖背景左侧色块,不影响文字布局 */
|
||||
.tab-left {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: -10rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
/* 第一个 tab 图标单独样式 */
|
||||
.tab-left-first {
|
||||
top: 40%;
|
||||
transform: translateY(-70%);
|
||||
|
||||
}
|
||||
|
||||
/* 选中时图标上移 */
|
||||
/* .tab-item.active .tab-left {
|
||||
transform: translateY(-70%);
|
||||
} */
|
||||
|
||||
.tab-emoji {
|
||||
font-size: 52rpx;
|
||||
line-height: 1;
|
||||
margin-top: -4rpx;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
white-space: nowrap;
|
||||
margin-left: 60rpx;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-label {
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
457
frontend/pages/square/components/WaterfallGrid.vue
Normal file
457
frontend/pages/square/components/WaterfallGrid.vue
Normal file
@ -0,0 +1,457 @@
|
||||
<template>
|
||||
<scroll-view
|
||||
class="waterfall-scroll"
|
||||
:style="scrollStyle"
|
||||
scroll-x
|
||||
:show-scrollbar="false"
|
||||
:scroll-left="scrollLeft"
|
||||
@scroll="onScroll"
|
||||
@touchstart="onTouchStart"
|
||||
@touchend="onTouchEnd"
|
||||
@touchcancel="onTouchEnd"
|
||||
>
|
||||
<view class="waterfall-inner" :style="{ width: totalWidth + 'px', height: '100%' }">
|
||||
<view
|
||||
v-for="card in cards"
|
||||
:key="card.id"
|
||||
class="wf-card"
|
||||
:style="cardStyle(card)"
|
||||
@click="handleCardClick(card)"
|
||||
>
|
||||
<!-- 渐变边框光效 -->
|
||||
<view class="wf-card-border" :style="borderStyle(card)" />
|
||||
|
||||
<!-- 封面图 -->
|
||||
<image
|
||||
v-if="card.coverUrl"
|
||||
class="wf-card-img"
|
||||
:src="card.coverUrl"
|
||||
mode="aspectFill"
|
||||
:style="{ width: card.w + 'px', height: card.h + 'px', borderRadius: card.radius + 'px' }"
|
||||
/>
|
||||
|
||||
<!-- 底部点赞数 -->
|
||||
<view class="wf-card-footer">
|
||||
<image class="wf-heart" src="/static/icon/heart-icon.png" mode="aspectFit" />
|
||||
<view class="wf-likes-wrap">
|
||||
<text class="wf-likes">{{ formatLikes(card.likes) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { getRandomUsersApi, getOssPresignedUrlApi } from '@/utils/api.js'
|
||||
|
||||
const props = defineProps({
|
||||
screenWidth: { type: Number, default: 375 },
|
||||
screenHeight: { type: Number, default: 812 },
|
||||
bannerBottom: { type: Number, default: 200 },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['cardClick'])
|
||||
|
||||
// ========== 布局常量 ==========
|
||||
const rpx2px = (rpx) => Math.round(uni.getSystemInfoSync().windowWidth / 750 * rpx)
|
||||
const GAP = rpx2px(16)
|
||||
const BORDER_W = rpx2px(2)
|
||||
const SCALE = 0.9
|
||||
const ROWS = 4
|
||||
const AUTO_SCROLL_SPEED = 1.2
|
||||
const AUTO_RESUME_DELAY = 2500
|
||||
const PRELOAD_THRESHOLD = rpx2px(1200)
|
||||
|
||||
// ========== 状态 ==========
|
||||
const cards = ref([])
|
||||
const allUsers = ref([])
|
||||
const totalWidth = ref(0)
|
||||
const scrollLeft = ref(0)
|
||||
let currentScrollLeft = 0
|
||||
let idCounter = 0
|
||||
|
||||
// ========== RAF 兼容 ==========
|
||||
const rafFn = (cb) => {
|
||||
if (typeof requestAnimationFrame !== 'undefined') return requestAnimationFrame(cb)
|
||||
if (uni.requestAnimationFrame) return uni.requestAnimationFrame(cb)
|
||||
return setTimeout(cb, 16)
|
||||
}
|
||||
const cafFn = (id) => {
|
||||
if (typeof cancelAnimationFrame !== 'undefined') return cancelAnimationFrame(id)
|
||||
if (uni.cancelAnimationFrame) return uni.cancelAnimationFrame(id)
|
||||
clearTimeout(id)
|
||||
}
|
||||
|
||||
// ========== 自动滚动 ==========
|
||||
let rafId = null
|
||||
let userInteracting = false
|
||||
let resumeTimer = null
|
||||
|
||||
const startAutoScroll = () => {
|
||||
if (rafId) return
|
||||
const step = () => {
|
||||
if (!userInteracting) {
|
||||
currentScrollLeft += AUTO_SCROLL_SPEED
|
||||
scrollLeft.value = currentScrollLeft
|
||||
if (totalWidth.value - currentScrollLeft - props.screenWidth < PRELOAD_THRESHOLD) {
|
||||
appendMore()
|
||||
}
|
||||
}
|
||||
rafId = rafFn(step)
|
||||
}
|
||||
rafId = rafFn(step)
|
||||
}
|
||||
|
||||
const stopAutoScroll = () => {
|
||||
if (rafId) { cafFn(rafId); rafId = null }
|
||||
}
|
||||
|
||||
const pauseForUser = () => {
|
||||
userInteracting = true
|
||||
clearTimeout(resumeTimer)
|
||||
resumeTimer = setTimeout(() => { userInteracting = false }, AUTO_RESUME_DELAY)
|
||||
}
|
||||
|
||||
// ========== 触摸 / 滚动事件 ==========
|
||||
const onTouchStart = () => pauseForUser()
|
||||
const onTouchEnd = () => {}
|
||||
|
||||
const onScroll = (e) => {
|
||||
currentScrollLeft = e.detail.scrollLeft
|
||||
if (totalWidth.value - currentScrollLeft - props.screenWidth < PRELOAD_THRESHOLD) {
|
||||
appendMore()
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 样式 ==========
|
||||
const scrollStyle = computed(() => ({
|
||||
position: 'absolute',
|
||||
top: props.bannerBottom + 'px',
|
||||
left: 0,
|
||||
width: props.screenWidth + 'px',
|
||||
height: (props.screenHeight - props.bannerBottom) + 'px',
|
||||
zIndex: 2,
|
||||
overflow: 'hidden',
|
||||
}))
|
||||
|
||||
// ========== 布局引擎 ==========
|
||||
// 核心思路:按列放置,每列的卡片 span 之和 = ROWS,保证零空缺
|
||||
// 同一列内所有卡片宽度统一 = 整列宽(span=ROWS 对应的宽),高度各自按 span 计算
|
||||
// 支持两种模式:
|
||||
// 1. 后端传 span:直接按 span 分组成列
|
||||
// 2. 后端不传 span:前端随机选列模板,每次打开都不同
|
||||
class WaterfallLayout {
|
||||
constructor(containerH, gap = GAP) {
|
||||
this.containerH = containerH
|
||||
this.gap = gap
|
||||
this.rowH = Math.floor((containerH - gap * (ROWS - 1)) / ROWS)
|
||||
// 列宽固定 = rowH × 9/16,严格竖长 9:16,span 只影响高度
|
||||
this.colW = Math.round(this.rowH * 9 / 16)
|
||||
this.curX = 0
|
||||
}
|
||||
|
||||
_cardSize(span) {
|
||||
const h = Math.round((span * this.rowH + (span - 1) * this.gap) * SCALE)
|
||||
const w = Math.round(h * 9 / 16)
|
||||
return { w, h }
|
||||
}
|
||||
|
||||
// 点赞数 → span
|
||||
_span(likes) {
|
||||
if (likes < 500) return 1
|
||||
if (likes < 2000) return 2
|
||||
if (likes < 8000) return 2
|
||||
if (likes < 50000) return 3
|
||||
return 4
|
||||
}
|
||||
|
||||
// 将用户列表按点赞数决定 span,分组成列,每列 span 之和 = ROWS
|
||||
_groupIntoColumns(users) {
|
||||
const columns = []
|
||||
let i = 0
|
||||
while (i < users.length) {
|
||||
const col = []
|
||||
let sum = 0
|
||||
while (i < users.length && sum < ROWS) {
|
||||
// 后端有 span 用后端的,否则按点赞数算
|
||||
const rawSpan = users[i].span != null ? users[i].span : this._span(users[i].likes || 0)
|
||||
const span = Math.min(rawSpan, ROWS - sum)
|
||||
col.push({ ...users[i], span })
|
||||
sum += span
|
||||
i++
|
||||
}
|
||||
// 凑不满则补 _pad
|
||||
while (sum < ROWS) {
|
||||
col.push({ id: idCounter++, span: 1, _pad: true, likes: 0 })
|
||||
sum++
|
||||
}
|
||||
columns.push(col)
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
_placeColumn(colUsers) {
|
||||
const result = []
|
||||
let curY = 0
|
||||
const colX = this.curX
|
||||
const maxSpan = Math.max(...colUsers.map(u => u.span || 1))
|
||||
const colW = this._cardSize(maxSpan).w
|
||||
|
||||
for (const u of colUsers) {
|
||||
const span = u.span || 1
|
||||
const { w, h } = this._cardSize(span)
|
||||
if (!u._pad) {
|
||||
result.push({ ...u, left: colX, top: curY, w, h, radius: 8 })
|
||||
}
|
||||
curY += h + this.gap
|
||||
}
|
||||
|
||||
this.curX += colW + this.gap
|
||||
return result
|
||||
}
|
||||
|
||||
// 计算全部卡片布局(重置状态)
|
||||
compute(users) {
|
||||
this.curX = 0
|
||||
const columns = this._groupIntoColumns(users)
|
||||
const result = []
|
||||
for (const col of columns) {
|
||||
result.push(...this._placeColumn(col))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 追加更多卡片(保持当前 X 继续)
|
||||
addCards(users) {
|
||||
const columns = this._groupIntoColumns(users)
|
||||
const result = []
|
||||
for (const col of columns) {
|
||||
result.push(...this._placeColumn(col))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
getTotalWidth() {
|
||||
return this.curX
|
||||
}
|
||||
}
|
||||
|
||||
let layout = null
|
||||
|
||||
// ========== 颜色主题 ==========
|
||||
const PALETTES = [
|
||||
'linear-gradient(135deg, #2d1b69, #5b21b6)',
|
||||
'linear-gradient(135deg, #0c4a6e, #0284c7)',
|
||||
'linear-gradient(135deg, #450a0a, #b91c1c)',
|
||||
'linear-gradient(135deg, #064e3b, #059669)',
|
||||
'linear-gradient(135deg, #78350f, #d97706)',
|
||||
'linear-gradient(135deg, #4a1942, #be185d)',
|
||||
]
|
||||
|
||||
const BORDER_COLORS = [
|
||||
'linear-gradient(135deg, #a78bfa, #60a5fa)',
|
||||
'linear-gradient(135deg, #38bdf8, #818cf8)',
|
||||
'linear-gradient(135deg, #f472b6, #fb923c)',
|
||||
'linear-gradient(135deg, #4ade80, #22d3ee)',
|
||||
'linear-gradient(135deg, #fbbf24, #f87171)',
|
||||
'linear-gradient(135deg, #c084fc, #f472b6)',
|
||||
]
|
||||
|
||||
// ========== 样式函数 ==========
|
||||
const cardStyle = (card) => ({
|
||||
position: 'absolute',
|
||||
left: card.left + 'px',
|
||||
top: card.top + 'px',
|
||||
width: card.w + 'px',
|
||||
height: card.h + 'px',
|
||||
borderRadius: card.radius + 'px',
|
||||
overflow: 'hidden',
|
||||
background: card.coverUrl ? 'transparent' : PALETTES[Math.abs(card.id) % PALETTES.length],
|
||||
})
|
||||
|
||||
const borderStyle = (card) => ({
|
||||
position: 'absolute',
|
||||
inset: `-${BORDER_W}px`,
|
||||
borderRadius: (card.radius + BORDER_W) + 'px',
|
||||
background: BORDER_COLORS[Math.abs(card.id) % BORDER_COLORS.length],
|
||||
zIndex: 0,
|
||||
opacity: 0.85,
|
||||
})
|
||||
|
||||
const formatLikes = (n) => {
|
||||
if (n >= 10000) return (n / 10000).toFixed(1) + 'w'
|
||||
if (n >= 1000) return (n / 1000).toFixed(1) + 'k'
|
||||
return String(n)
|
||||
}
|
||||
|
||||
// ========== 数据加载 ==========
|
||||
let isLoadingMore = false
|
||||
|
||||
// 后端不返回 likes 时,前端随机模拟
|
||||
const randomLikes = () => {
|
||||
const r = Math.random()
|
||||
if (r < 0.20) return Math.floor(Math.random() * 100)
|
||||
if (r < 0.40) return Math.floor(100 + Math.random() * 400)
|
||||
if (r < 0.60) return Math.floor(500 + Math.random() * 1500)
|
||||
if (r < 0.75) return Math.floor(2000 + Math.random() * 6000)
|
||||
if (r < 0.90) return Math.floor(8000 + Math.random() * 42000)
|
||||
return Math.floor(50000 + Math.random() * 950000)
|
||||
}
|
||||
|
||||
// 本地素材图,循环使用
|
||||
const MOCK_IMAGES = Array.from({ length: 16 }, (_, i) => `/static/sucai/image-${String(i + 1).padStart(2, '0')}.png`)
|
||||
|
||||
const mapUser = async (u) => {
|
||||
// 优先用后端图,没有则用本地素材循环
|
||||
let coverUrl = MOCK_IMAGES[idCounter % MOCK_IMAGES.length]
|
||||
if (u.cover_url) {
|
||||
try {
|
||||
const r = await getOssPresignedUrlApi(u.cover_url, 3600, 'asset')
|
||||
if (r?.code === 200 && r.data?.url) coverUrl = r.data.url
|
||||
} catch (_) {}
|
||||
}
|
||||
// 后端可以返回 span 字段(1~4)来控制卡片大小,不传则前端随机模板决定
|
||||
return {
|
||||
id: idCounter++,
|
||||
userId: u.user_id,
|
||||
nickname: u.nickname,
|
||||
coverUrl,
|
||||
likes: u.likes ?? randomLikes(),
|
||||
span: u.span ?? null, // 后端控制字段,null = 前端随机模板决定
|
||||
}
|
||||
}
|
||||
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
const res = await getRandomUsersApi(1, 40)
|
||||
if (res.code === 200 && res.data?.users) {
|
||||
const withData = await Promise.all(res.data.users.map(mapUser))
|
||||
allUsers.value = withData
|
||||
cards.value = layout.compute(withData)
|
||||
totalWidth.value = layout.getTotalWidth()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[WaterfallGrid] 加载用户失败', e?.message ?? e)
|
||||
}
|
||||
}
|
||||
|
||||
const appendMore = async () => {
|
||||
if (isLoadingMore) return
|
||||
isLoadingMore = true
|
||||
try {
|
||||
const res = await getRandomUsersApi(1, 20)
|
||||
if (res.code === 200 && res.data?.users) {
|
||||
const withData = await Promise.all(res.data.users.map(mapUser))
|
||||
const placed = layout.addCards(withData)
|
||||
cards.value = [...cards.value, ...placed]
|
||||
totalWidth.value = layout.getTotalWidth()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[WaterfallGrid] 追加用户失败', e?.message ?? e)
|
||||
} finally {
|
||||
isLoadingMore = false
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 点击处理 ==========
|
||||
const handleCardClick = (card) => {
|
||||
if (userInteracting) return
|
||||
emit('cardClick', card)
|
||||
}
|
||||
|
||||
// ========== 初始化 ==========
|
||||
onMounted(() => {
|
||||
const containerH = props.screenHeight - props.bannerBottom
|
||||
layout = new WaterfallLayout(containerH)
|
||||
loadUsers().then(() => startAutoScroll())
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoScroll()
|
||||
clearTimeout(resumeTimer)
|
||||
})
|
||||
|
||||
watch(() => [props.screenHeight, props.bannerBottom], () => {
|
||||
const containerH = props.screenHeight - props.bannerBottom
|
||||
layout = new WaterfallLayout(containerH)
|
||||
if (allUsers.value.length) {
|
||||
cards.value = layout.compute(allUsers.value)
|
||||
totalWidth.value = layout.getTotalWidth()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.waterfall-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.waterfall-inner {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.wf-card {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
will-change: transform;
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.wf-card:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.wf-card-border {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wf-card-img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wf-card-footer {
|
||||
position: absolute;
|
||||
bottom: 8rpx;
|
||||
left: 10rpx;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.wf-heart {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
}
|
||||
|
||||
.wf-likes-wrap {
|
||||
background: linear-gradient(to bottom right,
|
||||
#F0E4B1 0%,
|
||||
#F08399 50%,
|
||||
#B94E73 100%
|
||||
);
|
||||
border-radius: 999rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow:
|
||||
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
|
||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.wf-likes {
|
||||
font-size: 18rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
@ -88,7 +88,7 @@ export function useSwipe() {
|
||||
inertiaRaf = rafFn(step)
|
||||
}
|
||||
|
||||
const getBannerBottom = () => (screenWidth.value / 750) * 496
|
||||
const getBannerBottom = () => (screenWidth.value / 750) * 632
|
||||
|
||||
const onBgTouchStart = (e) => {
|
||||
const touchY = e.touches[0].clientY
|
||||
|
||||
@ -1,40 +1,16 @@
|
||||
<template>
|
||||
<view
|
||||
class="square-container"
|
||||
@touchstart="onBgTouchStart"
|
||||
@touchmove="onBgTouchMove"
|
||||
@touchend="onBgTouchEnd"
|
||||
@touchcancel="onBgTouchCancel"
|
||||
>
|
||||
<!-- 横向无限滚动背景条 -->
|
||||
<view class="background-strip" :style="backgroundStripStyle">
|
||||
<image
|
||||
v-for="i in 3"
|
||||
:key="i"
|
||||
class="background-tile"
|
||||
:style="{ width: tileWidth + 'px', height: '100%' }"
|
||||
src="/static/background/mainbg.png"
|
||||
/>
|
||||
</view>
|
||||
<view class="square-container">
|
||||
<!-- 固定背景 -->
|
||||
<image class="background-fixed" src="/static/square/squearbj.png" mode="aspectFill" />
|
||||
|
||||
<!-- Cabin 图标层(与背景同步移动) -->
|
||||
<view class="cabin-layer" :style="cabinLayerStyle">
|
||||
<CabinItem
|
||||
v-for="cabin in visibleCabins"
|
||||
:key="cabin.key"
|
||||
:cabin="cabin"
|
||||
:currentUserNickname="currentUserNickname"
|
||||
@click="handleCabinClick"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 翻页箭头按钮 -->
|
||||
<NavArrows @scroll="scrollPage" />
|
||||
|
||||
<!-- 调试按钮(开发环境) -->
|
||||
<view v-if="isDev" class="debug-btn" @click="openDebugGrid">
|
||||
<text class="debug-text">调试</text>
|
||||
</view>
|
||||
<!-- 横向瀑布流卡片层(内部自带横向滚动) -->
|
||||
<WaterfallGrid
|
||||
:screenWidth="screenWidth"
|
||||
:screenHeight="screenHeight"
|
||||
:bannerBottom="bannerBottomPx"
|
||||
@cardClick="handleCardClick"
|
||||
class="fall-bg"
|
||||
/>
|
||||
|
||||
<!-- Header组件 -->
|
||||
<Header
|
||||
@ -44,12 +20,15 @@
|
||||
backIconColor="#e6e6e6"
|
||||
/>
|
||||
|
||||
<!-- 轮播图 + 应援活动列表 -->
|
||||
<BannerCarousel
|
||||
:bannerActivities="bannerActivities"
|
||||
@activityClick="handleActivityClick"
|
||||
@top3Click="showRankingModal = true"
|
||||
/>
|
||||
<!-- 轮播图 + 内容分类 Tab -->
|
||||
<view class="banner-tabs-wrapper">
|
||||
<BannerCarousel
|
||||
:bannerActivities="bannerActivities"
|
||||
@activityClick="handleActivityClick"
|
||||
@top3Click="showRankingModal = true"
|
||||
/>
|
||||
<ContentTabs class="tabs" v-model="activeContentTab" />
|
||||
</view>
|
||||
|
||||
<!-- 蒙层 - 导航栏展开时显示 -->
|
||||
<view v-if="navExpanded" class="nav-mask" @click="navExpanded = false"></view>
|
||||
@ -77,22 +56,18 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } 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'
|
||||
import GuideOverlay from '@/components/GuideOverlay.vue'
|
||||
import RankingModal from '../components/RankingModal.vue'
|
||||
import CabinItem from './components/CabinItem.vue'
|
||||
import BannerCarousel from './components/BannerCarousel.vue'
|
||||
import NavArrows from './components/NavArrows.vue'
|
||||
import { clearSubStepProgress, shouldShowGuideStartModal, resetGuide } from '@/utils/guideConfig.js'
|
||||
import { IMAGE_W, IMAGE_H } from './config/cabin.js'
|
||||
import { useSwipe } from './composables/useSwipe.js'
|
||||
import { useCabin } from './composables/useCabin.js'
|
||||
import WaterfallGrid from './components/WaterfallGrid.vue'
|
||||
import ContentTabs from './components/ContentTabs.vue'
|
||||
import { clearSubStepProgress, shouldShowGuideStartModal } from '@/utils/guideConfig.js'
|
||||
import { useBanner } from './composables/useBanner.js'
|
||||
import { useDialogRotation } from './composables/useDialogRotation.js'
|
||||
|
||||
// ========== Store & User Info ==========
|
||||
const store = useStore()
|
||||
@ -100,51 +75,32 @@ const currentUserNickname = computed(() => store.state.user?.userInfo?.nickname
|
||||
const currentStarId = ref(uni.getStorageSync('star_id') || null)
|
||||
|
||||
// ========== UI State ==========
|
||||
const activeContentTab = ref('hot')
|
||||
const navExpanded = ref(false)
|
||||
const showRankingModal = ref(false)
|
||||
const isDev = ref(false) // 开发模式,显示调试按钮
|
||||
|
||||
// ========== Screen Info ==========
|
||||
const tileWidth = ref(375)
|
||||
const screenWidth = ref(375)
|
||||
const screenHeight = ref(812)
|
||||
|
||||
// ========== Composables ==========
|
||||
const {
|
||||
cabinLayerStyle,
|
||||
backgroundStripStyle,
|
||||
scrollPage,
|
||||
initSwipe,
|
||||
reset: resetSwipe,
|
||||
onBgTouchStart,
|
||||
onBgTouchMove,
|
||||
onBgTouchEnd,
|
||||
onBgTouchCancel,
|
||||
velocity,
|
||||
} = useSwipe()
|
||||
|
||||
const {
|
||||
visibleCabins,
|
||||
currentPage,
|
||||
ensurePages,
|
||||
initCabin,
|
||||
updateCurrentUserNickname,
|
||||
resetSquare: resetCabinSquare,
|
||||
wrapPage,
|
||||
} = useCabin()
|
||||
|
||||
const {
|
||||
bannerActivities,
|
||||
loadBannerActivities,
|
||||
} = useBanner()
|
||||
|
||||
const {
|
||||
initDialogRotation,
|
||||
startDialogRotation,
|
||||
stopDialogRotation,
|
||||
} = useDialogRotation()
|
||||
// banner(216+360rpx) + tab栏(16+80rpx) + 间距(8rpx) ≈ 680rpx
|
||||
const bannerBottomPx = computed(() => Math.round(screenWidth.value / 750 * 716))
|
||||
|
||||
// ========== Handlers ==========
|
||||
const handleCardClick = (card) => {
|
||||
if (!card.userId) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/exhibition/exhibition?target_uid=${card.userId}`,
|
||||
})
|
||||
}
|
||||
|
||||
const handleActivityClick = (item) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/support-activity/index?id=${item.id}`,
|
||||
@ -166,19 +122,6 @@ const handleRankingModalClose = (visible) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleCabinClick = (cabin) => {
|
||||
// 惯性速度残留时说明是滑动结束,不是真实点击
|
||||
if (Math.abs(velocity) > 0.5) return
|
||||
// 无用户、或被 banner 遮住(showNickname=false)的小屋不响应点击
|
||||
if (!cabin.userId || !cabin.showNickname) return
|
||||
|
||||
uni.navigateTo({
|
||||
url: (cabin.isMine || cabin.nickname === currentUserNickname.value)
|
||||
? '/pages/exhibition/exhibition'
|
||||
: `/pages/exhibition/exhibition?target_uid=${cabin.userId}`,
|
||||
})
|
||||
}
|
||||
|
||||
const handleTabChange = (newTab) => {
|
||||
if (newTab === 0) {
|
||||
navExpanded.value = false
|
||||
@ -210,71 +153,19 @@ const openDebugGrid = () => {
|
||||
}
|
||||
|
||||
// ========== Tile Change Callback ==========
|
||||
const handleTileChange = (delta, isInertia) => {
|
||||
const newPage = wrapPage(currentPage.value + delta)
|
||||
currentPage.value = newPage
|
||||
|
||||
if (isInertia) {
|
||||
// 惯性阶段防抖
|
||||
let ensureTimer = null
|
||||
if (ensureTimer) clearTimeout(ensureTimer)
|
||||
ensureTimer = setTimeout(() => {
|
||||
ensurePages(newPage)
|
||||
ensureTimer = null
|
||||
}, 100)
|
||||
} else {
|
||||
// 手动触摸阶段立即触发
|
||||
ensurePages(newPage)
|
||||
}
|
||||
}
|
||||
const handleTileChange = () => {}
|
||||
|
||||
// ========== Reset Square ==========
|
||||
const resetSquare = async () => {
|
||||
resetSwipe()
|
||||
await resetCabinSquare()
|
||||
}
|
||||
|
||||
// ========== Watch visibleCabins for Dialog Rotation ==========
|
||||
watch(visibleCabins, () => {
|
||||
if (visibleCabins.value.some(c => c.nickname && c.sharedBoothSlotsRemaining !== null)) {
|
||||
startDialogRotation(visibleCabins.value)
|
||||
} else {
|
||||
stopDialogRotation()
|
||||
}
|
||||
}, { immediate: true })
|
||||
const resetSquare = async () => {}
|
||||
|
||||
// ========== Watch currentUserNickname ==========
|
||||
watch(currentUserNickname, (nickname) => {
|
||||
if (nickname) {
|
||||
updateCurrentUserNickname(nickname)
|
||||
}
|
||||
}, { immediate: true })
|
||||
// (no-op, kept for future use)
|
||||
|
||||
// ========== Lifecycle ==========
|
||||
onMounted(() => {
|
||||
const info = uni.getSystemInfoSync()
|
||||
screenWidth.value = info.windowWidth
|
||||
screenHeight.value = info.windowHeight
|
||||
tileWidth.value = Math.round(info.windowHeight * (IMAGE_W / IMAGE_H))
|
||||
|
||||
// 初始化各模块
|
||||
initSwipe({
|
||||
screenW: screenWidth.value,
|
||||
tileW: tileWidth.value,
|
||||
onTileChangeCallback: handleTileChange,
|
||||
})
|
||||
|
||||
initCabin({
|
||||
screenW: screenWidth.value,
|
||||
tileW: tileWidth.value,
|
||||
imageH: screenHeight.value,
|
||||
currentUserNickname: currentUserNickname.value,
|
||||
})
|
||||
|
||||
initDialogRotation({
|
||||
screenW: screenWidth.value,
|
||||
screenH: screenHeight.value,
|
||||
})
|
||||
|
||||
// 数据加载在后台进行,不阻塞渲染
|
||||
resetSquare()
|
||||
@ -327,7 +218,6 @@ uni.$on('guide:openComponent', (componentName) => {
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off('guide:openComponent')
|
||||
stopDialogRotation()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -340,29 +230,38 @@ onUnmounted(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.background-strip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
height: 150%;
|
||||
/* .fall-bg{
|
||||
background: rgba(255, 180, 180, 0.25);
|
||||
border-radius: 48rpx;
|
||||
} */
|
||||
|
||||
.banner-tabs-wrapper {
|
||||
position: fixed;
|
||||
top: 216rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
z-index: 0;
|
||||
will-change: transform;
|
||||
flex-direction: column;
|
||||
min-height: 448rpx;
|
||||
justify-content: space-between;
|
||||
background: rgb(249 159 192 / 45%);;
|
||||
/* background: rgba(212, 127, 127, 0.8); */
|
||||
border-radius: 48rpx;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.background-tile {
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
}
|
||||
/* .tabs{
|
||||
margin-bottom: 8rpx;
|
||||
} */
|
||||
|
||||
.cabin-layer {
|
||||
.background-fixed {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.nav-mask {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user