feat:修改活动页面

This commit is contained in:
zheng020 2026-06-17 15:10:55 +08:00
parent ef0a4f6afd
commit 2d4fbc4a5b
11 changed files with 899 additions and 16 deletions

View File

@ -258,6 +258,15 @@
}
}
},
{
"path": "pages/support-activity/center",
"style": {
"navigationStyle": "custom",
"app-plus": {
"bounce": "none"
}
}
},
{
"path": "pages/asset-detail/asset-detail",
"style": {

View File

@ -80,11 +80,10 @@
</view>
</view> -->
<!-- 星援活动列表 -->
<!-- 星援活动 -->
<view v-if="showStarActivityIcon" class="star-activity-list">
<view
v-for="activity in starActivities"
:key="activity.id"
class="daily-task-group"
@click="handleActivityClick(activity)"
>
@ -92,7 +91,7 @@
<view class="task-icon-box">
<image
class="task-icon-img"
:src="activity.icon || '/static/icon/bus-icon.png'"
src="/static/icon/bus-icon.png"
mode="aspectFit"
></image>
<image
@ -104,9 +103,7 @@
<!-- 下层文字背景块 -->
<view class="task-text-box">
<text class="task-text-label">{{
activity.theme || "星援活动"
}}</text>
<text class="task-text-label">星援活动</text>
</view>
</view>
</view>
@ -318,7 +315,7 @@ const starActivities = computed(() => {
//
onMounted(async () => {
await loadUserInfo();
loadUserInfo();
await loadBannerActivities();
// await loadEarningsSummary();
uni.$on("avatarUpdated", handleAvatarUpdate);
@ -378,13 +375,15 @@ const handleTaskClick = () => {
showTaskModal.value = true;
};
//
//
const handleActivityClick = (activity) => {
if (activity) {
// activity
uni.navigateTo({
url: `/pages/support-activity/index?id=${activity.id}`,
url: "/pages/support-activity/center",
fail: (err) => {
console.error("[Header] 跳转活动中心失败:", err);
},
});
}
};
//

View File

@ -6,9 +6,9 @@
align-items: center;">
<BannerTop3 @dataLoaded="onTop3DataLoaded" @top3Click="$emit('top3Click')" />
</swiper-item> -->
<swiper-item v-for="item in bannerActivities" :key="item.id" @tap.stop="$emit('activityClick', item)">
<!-- <swiper-item v-for="item in bannerActivities" :key="item.id" @tap.stop="$emit('activityClick', item)">
<image class="banner-activity-img" :src="item.cover_image || '/static/avatar/1.jpeg'" mode="aspectFill" />
</swiper-item>
</swiper-item> -->
<swiper-item v-for="(banner, index) in banners" :key="banner.id || index" @click="emit('bannerClick', banner)">
<image class="banner-image" :src="banner.image_url" mode="widthFill"></image>
<!-- <view class="banner-overlay">

View File

@ -0,0 +1,875 @@
<template>
<view class="center-container">
<!-- 顶部导航 -->
<!-- <Header
:show-back="true"
backIconColor="#3a2540"
:showGuideIcon="false"
:showTaskIcon="false"
:showStarActivityIcon="false"
/> -->
<!-- 状态栏占位 -->
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<!-- 顶部毛绒装饰区直接用项目里现成的 centerhbj.png -->
<view class="hero-section">
<image
class="hero-bg-img"
src="/static/rank/centerhbj.png"
mode="aspectFill"
/>
</view>
<view class="main-box">
<!-- 统计条3 个独立渐变模块背景对应 Figma 108:567 / 108:579 / 108:581 -->
<view class="stats-strip">
<view class="stat-card stat-card-1">
<image
class="image-1"
src="/static/rank/hd.png"
mode="aspectFit"
></image>
<text class="stat-label">已参与活动</text>
<text class="stat-value">{{ stats.participated }}</text>
</view>
<view class="stat-card stat-card-2">
<image
class="image-2"
src="/static/icon/crystal.png"
mode="aspectFit"
></image>
<text class="stat-label">水晶贡献数</text>
<text class="stat-value">{{ formattedContributions }}</text>
</view>
<view class="stat-card stat-card-3">
<image
class="image-3"
src="/static/rank/lsph.png"
mode="aspectFit"
></image>
<text class="stat-label">历史最高排名</text>
<text class="stat-value">{{ formattedBestRank }}</text>
</view>
</view>
<!-- 标签切换 -->
<view class="tab-bar">
<view
v-for="tab in tabs"
:key="tab.key"
class="tab-item"
:class="{ 'tab-active': activeTab === tab.key }"
@click="activeTab = tab.key"
>
<text
class="tab-text"
:class="{ 'tab-text-active': activeTab === tab.key }"
>{{ tab.label }}</text
>
</view>
</view>
<!-- 活动列表 -->
<scroll-view
class="activity-scroll"
scroll-y
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onScrollToLower"
>
<view class="activity-list">
<view
v-for="item in filteredActivities"
:key="item.id"
class="activity-card"
@click="handleCardTap(item)"
>
<!-- 左侧封面 -->
<view class="card-cover">
<image
v-if="item.banner_image"
:src="item.banner_image"
class="cover-img"
mode="aspectFill"
:lazy-load="true"
/>
<view v-else class="cover-fallback">{{
item.title || "活动"
}}</view>
<view
v-if="item.status === 'expired' || item.status === 'completed'"
class="cover-ended"
>
<text class="ended-text">活动已结束</text>
</view>
</view>
<!-- 右侧信息 -->
<view class="card-info">
<view class="info-row">
<view class="year-pill">
<text class="year-text">{{ getYear(item) }}</text>
<text class="name-text">{{ getShortName(item) }}</text>
</view>
</view>
<!-- <view class="date-row">
<text class="date-text">{{ formatDateRange(item) }}</text>
<text v-if="!isEnded(item)" class="countdown-text">
距离活动结束还有{{ getDaysLeft(item) }}
</text>
</view> -->
<view class="stat-row">
<view class="stat-block">
<text class="block-value">{{
formatStatValue(item.my_contribution)
}}</text>
<text class="block-label">水晶贡献数</text>
<image
class="stat-image"
src="/static/icon/crystal.png"
mode="aspectFit"
></image>
</view>
<view class="stat-block">
<text class="block-value">{{
formatRank(item.my_rank)
}}</text>
<text class="block-label">排名</text>
<image
class="block-image"
src="/static/rank/ph.png"
mode="aspectFit"
></image>
</view>
</view>
</view>
</view>
<view
v-if="filteredActivities.length === 0 && !isLoading"
class="empty-block"
>
<text class="empty-text">暂无活动</text>
</view>
<view v-if="filteredActivities.length > 0" class="footer-end">
<text class="footer-text">已经到底啦</text>
</view>
</view>
</scroll-view>
<!-- 加载/错误遮罩 -->
<view v-if="isLoading" class="loading-mask">
<view class="loading-spinner"></view>
<text class="loading-text">加载中</text>
</view>
<view v-if="errorMessage" class="error-mask">
<text class="error-title">加载失败</text>
<text class="error-msg">{{ errorMessage }}</text>
<view class="retry-btn" @click="loadData">
<text class="retry-text">重试</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
import Header from "../components/Header.vue";
import { getActivityListApi, getActivityRankingApi } from "@/utils/api.js";
import screenCache from "@/utils/screen-cache";
const statusBarHeight = ref(0);
const isLoading = ref(true);
const refreshing = ref(false);
const errorMessage = ref("");
const activities = ref([]);
const activeTab = ref("all");
const tabs = [
{ key: "all", label: "全部" },
{ key: "active", label: "进行中" },
{ key: "ended", label: "已结束" },
];
// 3
const stats = ref({
participated: 0,
contributions: 0,
bestRank: 0,
});
const formattedContributions = computed(() =>
formatStatValue(stats.value.contributions),
);
const formattedBestRank = computed(() => {
const r = stats.value.bestRank;
if (!r) return "—";
if (r >= 10000) return `${Math.floor(r / 1000)}W+`;
return String(r);
});
const filteredActivities = computed(() => {
if (activeTab.value === "all") return activities.value;
if (activeTab.value === "active") {
return activities.value.filter((a) => a.status === "active");
}
return activities.value.filter(
(a) => a.status === "expired" || a.status === "completed",
);
});
function isEnded(item) {
return item.status === "expired" || item.status === "completed";
}
function getYear(item) {
const t = item.start_time || "";
return t.slice(0, 4) || "—";
}
function getShortName(item) {
// Figma "//" activity.theme
return item.theme || item.title || "活动";
}
function formatDateRange(item) {
const s = (item.start_time || "").slice(0, 10).replace(/-/g, ".");
const e = (item.end_time || "").slice(0, 10).replace(/-/g, ".");
if (s && e) return `${s}~${e}`;
if (s) return s;
return "—";
}
function getDaysLeft(item) {
if (!item.end_time) return 0;
const end = new Date(item.end_time.replace(/-/g, "/")).getTime();
const now = Date.now();
if (Number.isNaN(end) || end < now) return 0;
return Math.max(0, Math.ceil((end - now) / 86400000));
}
function formatStatValue(v) {
if (v === null || v === undefined || v === 0) return "0";
return String(v);
}
function formatRank(rank) {
if (rank === null || rank === undefined) return "—";
if (rank === 0) return "未上榜";
if (rank >= 10000) return "1W+";
return String(rank);
}
async function fetchWithRanking(list) {
// /
const starId = uni.getStorageSync("star_id") || null;
const tasks = list.map(async (a) => {
try {
const res = await getActivityRankingApi(a.id, starId, 1, 10);
if (res?.code === 0 && res.data) {
const mine = res.data.my_contribution;
return {
...a,
my_contribution: mine?.total_contribution ?? 0,
my_rank: mine?.rank ?? null,
};
}
} catch (e) {
console.error("[center] 拉取活动贡献失败", a.id, e?.message ?? e);
}
return { ...a, my_contribution: 0, my_rank: null };
});
return Promise.all(tasks);
}
function aggregate(merged) {
const participated = merged.length;
const contributions = merged.reduce(
(sum, a) => sum + (a.my_contribution || 0),
0,
);
const ranks = merged.map((a) => a.my_rank).filter((r) => r && r > 0);
const bestRank = ranks.length ? Math.min(...ranks) : 0;
stats.value = { participated, contributions, bestRank };
}
async function loadData() {
try {
isLoading.value = true;
errorMessage.value = "";
const starId = uni.getStorageSync("star_id");
if (!starId) {
throw new Error("缺少粉丝身份,请先选择明星");
}
// 1.
const res = await getActivityListApi(starId, 1, 50);
if (res?.code !== 0) {
throw new Error(res?.message || "获取活动列表失败");
}
const list = res.data?.activities || [];
// 2. /
const merged = await fetchWithRanking(list);
// 3.
merged.sort((a, b) => {
const ta = a.start_time ? new Date(a.start_time).getTime() : 0;
const tb = b.start_time ? new Date(b.start_time).getTime() : 0;
return tb - ta;
});
activities.value = merged;
aggregate(merged);
} catch (e) {
console.error("[center] loadData failed", e);
errorMessage.value = e?.message || "加载失败";
} finally {
isLoading.value = false;
refreshing.value = false;
}
}
function onRefresh() {
refreshing.value = true;
loadData();
}
function onScrollToLower() {
//
}
function handleCardTap(item) {
//
uni.navigateTo({
url: `/pages/support-activity/index?id=${item.id}`,
fail: (err) => {
console.error("[center] 跳转活动详情失败:", err);
},
});
}
onMounted(() => {
screenCache.init();
statusBarHeight.value = screenCache.getStatusBarHeight();
loadData();
});
onShow(() => {
//
if (activities.value.length > 0) {
loadData();
}
});
</script>
<style scoped lang="scss">
.center-container {
position: relative;
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(
180deg,
rgba(255, 229, 229, 0.25) -32.49%,
rgba(243, 160, 161, 0.25) -32.49%,
rgba(255, 156, 156, 0.25) 86.46%,
rgba(255, 32, 36, 0.25) 180.79%
);
// filter: blur(2px);
&::before {
content: "";
position: absolute;
inset: 0;
background: url("/static/rank/centerbj.png") center no-repeat;
background-size: 100% 100%;
}
}
.status-bar {
width: 100%;
background: transparent;
}
/* ============ 顶部毛绒装饰区 ============ */
.hero-section {
position: absolute;
top: 0;
width: 100%;
height: 360rpx;
overflow: hidden;
}
.main-box {
position: relative;
display: flex;
flex-direction: column;
margin: 328rpx 10rpx 0;
padding: 0 0 40rpx;
min-height: 1324rpx;
border-radius: 28rpx;
background: linear-gradient(
180deg,
#ffd8dc 0%,
rgba(255, 90, 93, 0.2) 38%,
transparent 81.31%,
transparent 128.86%
);
// box-shadow: 0 12rpx 24rpx rgba(179, 50, 50, 0.15);
box-sizing: border-box;
overflow: hidden;
}
.hero-bg-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
/* ============ 统计条 ============ */
.stats-strip {
position: relative;
margin: -80rpx 24rpx 0;
z-index: 5;
display: flex;
align-items: flex-end;
justify-content: space-between;
// gap: 18rpx;
height: 200rpx;
padding: 0 16rpx;
}
.stat-card {
position: relative;
width: 200rpx;
height: 81.92rpx;
// padding-bottom: 12rpx;
border-radius: 38rpx 20rpx 14rpx 14rpx;
background: linear-gradient(
274deg,
rgba(168, 166, 237, 0.49) -9.28%,
rgba(136, 200, 216, 0.49) 61.89%,
rgba(243, 128, 239, 0.49) 106.57%
);
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25);
z-index: 0;
box-sizing: border-box;
overflow: hidden;
}
/* 3 个模块各自的 ::before 背景图(按需替换路径) */
.image-1 {
width: 100%;
height: 100%;
position: absolute;
left: -32rpx;
bottom: -8rpx;
}
.image-2 {
width: 100%;
height: 100%;
position: absolute;
left: -32rpx;
bottom: -16rpx;
transform: rotate(-10deg);
opacity: 0.6;
}
.image-3 {
width: 100%;
height: 100%;
position: absolute;
left: -32rpx;
bottom: 0;
transform: rotate(-2deg);
}
.stat-label {
position: absolute;
top: 0;
left: 8rpx;
z-index: 2;
font-size: 16rpx;
font-weight: bold;
color: #fffabd;
text-shadow: -2rpx 2rpx 8rpx rgba(206, 9, 9, 0.84);
letter-spacing: 0.5rpx;
margin-top: 4rpx;
}
.stat-value {
position: absolute;
bottom: 4rpx;
right: 12rpx;
z-index: 2;
font-size: 32rpx;
font-weight: bold;
color: #fffabd;
text-shadow: -2rpx 2rpx 8rpx rgba(206, 9, 9, 0.84);
line-height: 1.1;
margin-top: 2rpx;
}
/* ============ 标签切换(对应 Figma 108:589 title组 ============ */
.tab-bar {
position: relative;
z-index: 5;
margin: 30rpx 128rpx 18rpx;
height: 64rpx;
border-radius: 32rpx;
background: rgba(217, 217, 217, 0.11);
box-shadow: 0 8rpx 8rpx 0 rgba(179, 50, 50, 0.25);
display: flex;
align-items: center;
justify-content: space-around;
padding: 0 8rpx;
}
.tab-item {
flex: 1;
height: 46rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 23rpx;
transition: all 0.2s ease;
}
.tab-active {
background: linear-gradient(
90deg,
rgba(255, 222, 8, 0.55) 0%,
rgba(252, 100, 102, 0.85) 64%,
rgba(244, 88, 104, 0.85) 100%
);
box-shadow: 4rpx 4rpx 8rpx 0 rgba(242, 21, 21, 0.47);
}
.tab-text {
font-size: 26rpx;
font-weight: bold;
color: #fff;
text-shadow: 0 4rpx 6rpx rgba(0, 0, 0, 0.55);
opacity: 0.85;
}
.tab-text-active {
opacity: 1;
}
/* ============ 活动列表 ============ */
.activity-scroll {
flex: 1;
width: 100%;
padding: 0 24rpx;
box-sizing: border-box;
}
.activity-list {
display: flex;
flex-direction: column;
gap: 36rpx;
padding-bottom: 60rpx;
}
.activity-card {
position: relative;
display: flex;
align-items: stretch;
gap: 24rpx;
min-height: 168rpx;
}
.card-cover {
position: relative;
width: 384rpx;
height: 168rpx;
border-radius: 24rpx;
overflow: hidden;
background: linear-gradient(135deg, #ffd6e0 0%, #ff9eb5 100%);
box-shadow: 0 8rpx 16rpx rgba(179, 50, 50, 0.25);
flex-shrink: 0;
z-index: 2;
}
.cover-img {
width: 100%;
height: 100%;
}
.cover-fallback {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 28rpx;
font-weight: bold;
text-align: center;
padding: 0 16rpx;
box-sizing: border-box;
}
.cover-ended {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
border-radius: 24rpx;
}
.ended-text {
color: #ffffff;
font-size: 24rpx;
font-weight: bold;
text-shadow: -2rpx 2rpx 8rpx rgba(206, 9, 9, 0.84);
letter-spacing: 1rpx;
}
.card-info {
width: 320rpx;
height: 150.4rpx;
display: flex;
margin: 16rpx 0;
flex-direction: column;
justify-content: space-between;
background: url("static/rank/infobj.png") center no-repeat;
background-size: 100% 100%;
position: absolute;
right: 0;
}
.info-row {
display: flex;
align-items: center;
justify-content: center;
position: relative;
top: -16rpx;
}
.year-pill {
min-width: 192rpx;
height: 40rpx;
padding: 0 20rpx;
border-radius: 12rpx;
// background: linear-gradient(
// 176deg,
// rgba(255, 90, 93, 0.6) 16.6%,
// rgba(76, 237, 255, 0.6) 48%,
// rgba(255, 122, 124, 0.6) 84%
// );
// box-shadow: 0 4rpx 6rpx rgba(201, 60, 159, 0.5);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: url("static/rank/textbj.png") center no-repeat;
background-size: 100%;
}
.year-text {
display: block;
color: #fffabd;
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84);
font-family: "Baloo Bhai";
font-size: 28rpx;
font-style: normal;
font-weight: 400;
line-height: normal;
margin-right: 8rpx;
}
.name-text {
display: block;
color: #fffabd;
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84);
font-family: "Abhaya Libre ExtraBold";
font-size: 20rpx;
font-style: normal;
font-weight: 800;
line-height: normal;
margin-left: 8rpx;
}
.date-row {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.date-text {
font-size: 18rpx;
color: #fffabd;
text-shadow: -2rpx 2rpx 8rpx rgba(206, 9, 9, 0.84);
font-weight: bold;
}
.countdown-text {
font-size: 14rpx;
color: #fffabd;
text-shadow: -2rpx 2rpx 8rpx rgba(206, 9, 9, 0.84);
opacity: 0.85;
}
.stat-row {
display: flex;
align-items: center;
margin-left: 64rpx;
overflow: hidden;
}
.stat-block {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 2rpx;
min-width: 0;
overflow: hidden;
}
.block-label {
font-size: 14rpx;
font-weight: bold;
color: #fffabd;
text-shadow: -2rpx 2rpx 8rpx rgba(206, 9, 9, 0.84);
display: block;
}
.block-image {
width: 96rpx;
height: 64rpx;
position: relative;
}
.block-value {
font-size: 30rpx;
font-weight: bold;
color: #fffabd;
text-shadow: -2rpx 2rpx 8rpx rgba(206, 9, 9, 0.84);
line-height: 1.1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
.stat-image {
width: 96rpx;
height: 64rpx;
position: relative;
bottom: -16rpx;
transform: scale(1.3) rotate(-15deg);
}
.empty-block {
padding: 80rpx 0;
display: flex;
align-items: center;
justify-content: center;
}
.empty-text {
color: rgba(255, 250, 189, 0.6);
font-size: 24rpx;
}
.footer-end {
padding: 40rpx 0 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.footer-text {
color: rgba(95, 95, 95, 0.66);
font-size: 24rpx;
font-weight: bold;
}
/* ============ 加载/错误遮罩 ============ */
.loading-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 200;
}
.loading-spinner {
width: 80rpx;
height: 80rpx;
border: 6rpx solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20rpx;
}
.loading-text {
color: #fff;
font-size: 28rpx;
}
.error-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 200;
padding: 0 60rpx;
}
.error-title {
color: #fff;
font-size: 36rpx;
font-weight: bold;
margin-bottom: 20rpx;
}
.error-msg {
color: rgba(255, 255, 255, 0.85);
font-size: 26rpx;
margin-bottom: 40rpx;
text-align: center;
line-height: 1.5;
}
.retry-btn {
padding: 16rpx 60rpx;
border-radius: 40rpx;
background: linear-gradient(135deg, #ff5a5d 0%, #fc6466 50%, #f45868 100%);
box-shadow: 0 4rpx 8rpx rgba(242, 21, 21, 0.5);
}
.retry-text {
color: #fff;
font-size: 28rpx;
font-weight: bold;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

BIN
frontend/static/rank/hd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
frontend/static/rank/ph.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB