topfans/frontend/pages/support-activity/components/TopRanking.vue
zerosaturation 83999995f5 refactor(frontend): consolidate user storage read via getStoredUser helper
uni.getStorageSync('user') 在 key 从未写过的 UniApp 平台下默认返回 ""(空串)
而非 null/undefined,旧式 `JSON.parse(uni.getStorageSync('user')) || {}` 在
parse 阶段直接抛 `SyntaxError: Unexpected end of JSON input`,导致 quickLogin
setup 崩溃,并触发 Vue 二次告警 `Invalid vnode type: undefined`。

新增 utils/getStoredUser.js 单点封装,统一处理:
  - 空值/损坏 JSON 兜底(返回 null,不抛)
  - 与 store/modules/user.js 初始化块同源守卫
  - 失败 warn 级别日志,不影响业务

替换全库 16 处不安全 `JSON.parse(userStr)` 调用:
  composables/useLaserMint.js                  composables/useShare.js
  utils/guideConfig.js                         utils/preloadApi/core.js
  pages/castlove/lenticular/lenticular-result.vue
  pages/support-activity/index.vue             pages/support-activity/components/{TopRanking,ActionBar}.vue
  pages/components/{Header,ShareModal,ShareReportButtons}.vue
  pages/tasks/{GuideModal,daily-tasks}.vue
  pages/asset-detail/asset-detail.vue          pages/exhibition/exhibition.vue
  pages/profile/profile.vue                    pages/login/quickLogin.vue(原崩点)

净 -18 行,17 文件改动 + 1 文件新增。后续如需支持多 key(如 'adminUser'),
把 getStoredUser 扩成泛型 getStoredJSON(key) 即可。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 17:16:49 +08:00

249 lines
5.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="top-ranking" @tap="handleOpenRanking">
<!-- TOP3 头像组对应 Figma 110-671 -->
<view v-if="top3List.length > 0" class="top3-card">
<view v-for="item in top3List" :key="item.userId" class="top3-item">
<image
class="top3-avatar"
:src="item.avatar || '/static/avatar/1.jpeg'"
mode="aspectFill"
@error="handleAvatarError"
/>
<image
class="top3-medal"
:src="`/static/rank/activity-support-icon/pm${item.rank}.png`"
mode="aspectFit"
/>
</view>
</view>
<!-- 我的排名条(对应 Figma 110-662 -->
<view class="my-rank-card">
<view class="my-rank-row">
<image
class="my-avatar"
:src="myInfo.avatar || '/static/avatar/1.jpeg'"
mode="aspectFill"
@error="handleAvatarError"
/>
<text class="my-rank-label">当前排名</text>
<text class="my-rank-number">{{ myInfo.rank || '暂无排名' }}</text>
<image
class="my-rank-icon"
src="/static/rank/lsph.png"
mode="aspectFit"
/>
</view>
<view class="my-rank-row">
<text class="my-rank-label">距离上一名贡献值</text>
<text class="my-rank-number">{{ myInfo.gapToPrev }}</text>
<image
class="my-rank-icon"
src="/static/icon/crystal.png"
mode="aspectFit"
/>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { getActivityTopRankingApi } from "@/utils/api.js";
import { getStoredUser } from "@/utils/getStoredUser.js";
const props = defineProps({
activityId: {
type: [String, Number],
required: true,
},
starId: {
type: [String, Number],
default: null,
},
});
const emit = defineEmits(["open-ranking"]);
// TOP3 数据
const top3List = ref([]);
// 我的排名数据
const myInfo = ref({
rank: null,
avatar: "",
gapToPrev: 0,
});
// 兜底:从未登录态 / 本地缓存读取用户头像
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
function getFallbackAvatar() {
try {
const u = getStoredUser() || {};
return u.avatar_url || u.avatar || "";
} catch (e) {
// 忽略解析错误
}
return "";
}
// 头像加载失败兜底
function handleAvatarError(e) {
e.target.src = "/static/avatar/1.jpeg";
}
// 打开排行榜弹窗
function handleOpenRanking() {
emit("open-ranking");
}
// 加载排行数据(使用专用轻量接口 /top-ranking)
async function loadRanking() {
if (!props.activityId) return;
try {
const sid = props.starId || uni.getStorageSync("star_id");
const res = await getActivityTopRankingApi(props.activityId, sid);
if (res && res.code === 0 && res.data) {
// TOP3
const top3 = Array.isArray(res.data.top3) ? res.data.top3 : [];
top3List.value = top3
.filter((u) => u.rank >= 1 && u.rank <= 3)
.sort((a, b) => a.rank - b.rank)
.map((u) => ({
rank: u.rank,
userId: String(u.user_id),
avatar: u.avatar_url || "/static/avatar/1.jpeg",
}));
// 我的信息(后端已下发 gap_to_prev)
const my = res.data.my_info;
if (my && my.status === "ranked" && my.rank) {
myInfo.value = {
rank: my.rank,
avatar: my.avatar_url || "/static/avatar/1.jpeg",
gapToPrev: typeof my.gap_to_prev === "number" ? my.gap_to_prev : 0,
};
} else {
// 未上榜(status=unranked 或 my_info 缺失):仍展示卡片,排名显示"暂无排名"
myInfo.value = {
rank: null,
avatar: my && my.avatar_url ? my.avatar_url : getFallbackAvatar(),
gapToPrev: 0,
};
}
}
} catch (err) {
console.error("[TopRanking] 加载排行失败", err);
}
}
onMounted(() => {
loadRanking();
});
defineExpose({
refresh: loadRanking,
});
</script>
<style scoped>
.top-ranking {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12rpx;
}
/* TOP3 卡片(对应 110-671圆角深红半透明 + 3 个头像 + 奖牌) */
.top3-card {
display: flex;
align-items: center;
gap: 8rpx;
padding: 6rpx 12rpx;
background: rgba(42, 17, 17, 0.3);
border-radius: 22rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.15);
}
.top3-item {
position: relative;
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
}
.top3-avatar {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
/* border: 2rpx solid rgba(255, 255, 255, 0.6); */
/* background: #fff; */
box-shadow: 2px 2px 4px 0 rgba(174, 17, 17, 0.53);
}
.top3-medal {
position: absolute;
bottom: -10rpx;
left: 50%;
transform: translateX(-50%);
width: 32rpx;
height: 32rpx;
}
/* 我的排名卡片(对应 110-662圆角深红半透明 + 头像 + 文案 + 数字 + 图标) */
.my-rank-card {
display: flex;
flex-direction: column;
gap: 4rpx;
padding: 8rpx 14rpx;
background: rgba(42, 17, 17, 0.3);
border-radius: 22rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.15);
min-width: 208rpx;
}
.my-rank-row {
display: flex;
align-items: center;
gap: 8rpx;
}
.my-avatar {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
border: 2rpx solid rgba(255, 255, 255, 0.6);
box-shadow: 1px 1px 4px 0 rgba(181, 7, 7, 0.54);
background: #fff;
margin-right: 4rpx;
}
.my-rank-label {
font-size: 20rpx;
color: #fff;
font-weight: bold;
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.45);
flex: 1;
white-space: nowrap;
text-align: right;
}
.my-rank-number {
font-size: 28rpx;
color: #fffabd;
font-weight: bold;
font-family: "yt", "Baloo Bhai", sans-serif;
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.45);
margin: 0 8rpx;
white-space: nowrap;
}
.my-rank-icon {
width: 40rpx;
height: 32rpx;
transform: rotate(-10deg);
}
</style>