feat:修改活动页面
@ -258,6 +258,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/support-activity/center",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"app-plus": {
|
||||||
|
"bounce": "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/asset-detail/asset-detail",
|
"path": "pages/asset-detail/asset-detail",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@ -80,11 +80,10 @@
|
|||||||
</view>
|
</view>
|
||||||
</view> -->
|
</view> -->
|
||||||
|
|
||||||
<!-- 星援活动列表 -->
|
<!-- 星援活动 -->
|
||||||
<view v-if="showStarActivityIcon" class="star-activity-list">
|
<view v-if="showStarActivityIcon" class="star-activity-list">
|
||||||
<view
|
<view
|
||||||
v-for="activity in starActivities"
|
|
||||||
:key="activity.id"
|
|
||||||
class="daily-task-group"
|
class="daily-task-group"
|
||||||
@click="handleActivityClick(activity)"
|
@click="handleActivityClick(activity)"
|
||||||
>
|
>
|
||||||
@ -92,7 +91,7 @@
|
|||||||
<view class="task-icon-box">
|
<view class="task-icon-box">
|
||||||
<image
|
<image
|
||||||
class="task-icon-img"
|
class="task-icon-img"
|
||||||
:src="activity.icon || '/static/icon/bus-icon.png'"
|
src="/static/icon/bus-icon.png"
|
||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
></image>
|
></image>
|
||||||
<image
|
<image
|
||||||
@ -104,9 +103,7 @@
|
|||||||
|
|
||||||
<!-- 下层:文字背景块 -->
|
<!-- 下层:文字背景块 -->
|
||||||
<view class="task-text-box">
|
<view class="task-text-box">
|
||||||
<text class="task-text-label">{{
|
<text class="task-text-label">星援活动</text>
|
||||||
activity.theme || "星援活动"
|
|
||||||
}}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -318,7 +315,7 @@ const starActivities = computed(() => {
|
|||||||
|
|
||||||
// 组件挂载时加载用户信息和星援活动数据
|
// 组件挂载时加载用户信息和星援活动数据
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadUserInfo();
|
loadUserInfo();
|
||||||
await loadBannerActivities();
|
await loadBannerActivities();
|
||||||
// await loadEarningsSummary();
|
// await loadEarningsSummary();
|
||||||
uni.$on("avatarUpdated", handleAvatarUpdate);
|
uni.$on("avatarUpdated", handleAvatarUpdate);
|
||||||
@ -378,13 +375,15 @@ const handleTaskClick = () => {
|
|||||||
showTaskModal.value = true;
|
showTaskModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理星援活动图标点击
|
// 处理星援活动图标点击 → 跳到星援活动中心
|
||||||
const handleActivityClick = (activity) => {
|
const handleActivityClick = (activity) => {
|
||||||
if (activity) {
|
// 始终先进入活动中心列表(不论是否传了单个 activity 对象)
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/support-activity/index?id=${activity.id}`,
|
url: "/pages/support-activity/center",
|
||||||
});
|
fail: (err) => {
|
||||||
}
|
console.error("[Header] 跳转活动中心失败:", err);
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 点击新手引导
|
// 点击新手引导
|
||||||
|
|||||||
@ -6,9 +6,9 @@
|
|||||||
align-items: center;">
|
align-items: center;">
|
||||||
<BannerTop3 @dataLoaded="onTop3DataLoaded" @top3Click="$emit('top3Click')" />
|
<BannerTop3 @dataLoaded="onTop3DataLoaded" @top3Click="$emit('top3Click')" />
|
||||||
</swiper-item> -->
|
</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" />
|
<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)">
|
<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>
|
<image class="banner-image" :src="banner.image_url" mode="widthFill"></image>
|
||||||
<!-- <view class="banner-overlay">
|
<!-- <view class="banner-overlay">
|
||||||
|
|||||||
875
frontend/pages/support-activity/center.vue
Normal 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>
|
||||||
BIN
frontend/static/rank/centerbj.png
Normal file
|
After Width: | Height: | Size: 293 KiB |
BIN
frontend/static/rank/centerhbj.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
frontend/static/rank/hd.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
frontend/static/rank/infobj.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
frontend/static/rank/lsph.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
frontend/static/rank/ph.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/static/rank/textbj.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |