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>
This commit is contained in:
parent
d6573efcdd
commit
83999995f5
@ -1,12 +1,15 @@
|
||||
import { ref } from 'vue'
|
||||
import { estimateMintCostApi } from '@/utils/api.js'
|
||||
import { submitCraftMintFromPath } from '@/utils/craftMintSubmit.js'
|
||||
import { getStoredUser } from '@/utils/getStoredUser.js'
|
||||
|
||||
function updateLocalBalance(newBalance) {
|
||||
try {
|
||||
const userStr = uni.getStorageSync('user')
|
||||
if (!userStr) return
|
||||
const user = typeof userStr === 'string' ? JSON.parse(userStr) : { ...userStr }
|
||||
// ★ 2026-07-27 refactor:旧写法 `typeof userStr === 'string' ? JSON.parse(userStr) : {...}`
|
||||
// 在 userStr 是 "" 时 typeof 仍是 'string',仍会撞到 JSON.parse crash。
|
||||
// 统一用 @/utils/getStoredUser,任何异常路径都已封装,这里只管写回。
|
||||
const user = getStoredUser()
|
||||
if (!user) return
|
||||
user.crystal_balance = Number(newBalance) || 0
|
||||
uni.setStorageSync('user', JSON.stringify(user))
|
||||
uni.$emit('balanceUpdated', { crystal_balance: user.crystal_balance })
|
||||
|
||||
@ -8,6 +8,7 @@ import { composeShareImage, computeComposeKey } from '@/utils/image-compositor.j
|
||||
import { pickRandomSlogan } from '@/utils/brand-slogans.js';
|
||||
import { getShareQrcodeApi, trackShareApi } from '@/utils/api.js';
|
||||
import { useAliveGuard } from '@/composables/useAliveGuard.js';
|
||||
import { getStoredUser } from '@/utils/getStoredUser.js';
|
||||
|
||||
const APP_PACKAGES = {
|
||||
weixin_friend: { pname: 'com.tencent.mm', bundleid: 'com.tencent.xinWeChat' },
|
||||
@ -77,10 +78,9 @@ export function useShare(props) {
|
||||
const l1Cache = new Map();
|
||||
|
||||
// ============ Helpers ============
|
||||
// ★ 2026-07-27 refactor:统一用 @/utils/getStoredUser 取代本地 if/try 守卫
|
||||
function getCurrentUser() {
|
||||
const userStr = uni.getStorageSync('user');
|
||||
if (!userStr) return null;
|
||||
try { return JSON.parse(userStr); } catch { return null; }
|
||||
return getStoredUser();
|
||||
}
|
||||
|
||||
// 后端 TrackShareRequest 字段是 int64,前端 props 是 string,转 number
|
||||
|
||||
@ -266,6 +266,7 @@ import {
|
||||
} from '@/utils/castloveGenerationFlow.js';
|
||||
import { submitCraftMintFromPath } from '@/utils/craftMintSubmit.js';
|
||||
import { composeStickers, relationsToStickers } from '@/utils/sticker-compositor.js';
|
||||
import { getStoredUser } from '@/utils/getStoredUser.js';
|
||||
// 页面参数
|
||||
const assetIdParam = ref('');
|
||||
const orderIdParam = ref('');
|
||||
@ -335,12 +336,11 @@ const currentUserNickname = ref('');
|
||||
// 加载当前用户信息
|
||||
const loadCurrentUser = () => {
|
||||
try {
|
||||
const userStr = uni.getStorageSync('user');
|
||||
if (!userStr) return;
|
||||
const userInfo = typeof userStr === 'string' ? JSON.parse(userStr) : userStr;
|
||||
userAvatarUrl.value = userInfo?.avatar_url || '';
|
||||
currentUserId.value = String(userInfo?.uid || userInfo?.user_id || '');
|
||||
currentUserNickname.value = userInfo?.nickname || '';
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一 JSON.parse 守卫
|
||||
const userInfo = getStoredUser() || {};
|
||||
userAvatarUrl.value = userInfo.avatar_url || '';
|
||||
currentUserId.value = String(userInfo.uid || userInfo.user_id || '');
|
||||
currentUserNickname.value = userInfo.nickname || '';
|
||||
} catch (e) {
|
||||
console.error('解析用户信息失败:', e);
|
||||
}
|
||||
|
||||
@ -139,6 +139,7 @@ import {
|
||||
STUDIO_LENTICULAR,
|
||||
} from "@/utils/castloveGenerationFlow.js";
|
||||
import { submitCraftMintFromPath } from "@/utils/craftMintSubmit.js";
|
||||
import { getStoredUser } from "@/utils/getStoredUser.js";
|
||||
|
||||
const generatedImages = ref([]);
|
||||
const selectedIndex = ref(-1);
|
||||
@ -192,14 +193,13 @@ const handleRegenerate = () => {
|
||||
// 更新本地存储的余额
|
||||
function updateLocalBalance(newBalance) {
|
||||
try {
|
||||
const userStr = uni.getStorageSync("user");
|
||||
if (userStr) {
|
||||
const user =
|
||||
typeof userStr === "string" ? JSON.parse(userStr) : { ...userStr };
|
||||
user.crystal_balance = Number(newBalance) || 0;
|
||||
uni.setStorageSync("user", JSON.stringify(user));
|
||||
uni.$emit("balanceUpdated", { crystal_balance: user.crystal_balance });
|
||||
}
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,消除 `typeof === 'string' ? JSON.parse : spread`
|
||||
// 在 userStr === "" 时仍会 JSON.parse 抛错的隐患
|
||||
const user = getStoredUser();
|
||||
if (!user) return;
|
||||
user.crystal_balance = Number(newBalance) || 0;
|
||||
uni.setStorageSync("user", JSON.stringify(user));
|
||||
uni.$emit("balanceUpdated", { crystal_balance: user.crystal_balance });
|
||||
} catch (e) {
|
||||
console.warn("[lenticular-result] 更新本地余额失败:", e);
|
||||
}
|
||||
|
||||
@ -159,6 +159,7 @@ import { reportEvent } from "@/utils/task-api.js";
|
||||
import { getEarningsSummaryApi } from "@/utils/api.js";
|
||||
import { getPreloadApi } from "@/utils/preloadApi/index";
|
||||
import { onScanResult } from "@/utils/scanLaunch.js";
|
||||
import { getStoredUser } from "@/utils/getStoredUser.js";
|
||||
|
||||
// 获取星援活动数据(复用 square 的 useBanner)
|
||||
const { bannerActivities, loadBannerActivities } = useBanner();
|
||||
@ -210,10 +211,8 @@ const showGuideModal = ref(false);
|
||||
// 从本地存储读取用户信息
|
||||
const loadUserInfo = () => {
|
||||
try {
|
||||
const userStr = uni.getStorageSync("user");
|
||||
if (userStr) {
|
||||
userInfo.value = JSON.parse(userStr);
|
||||
}
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
userInfo.value = getStoredUser();
|
||||
} catch (e) {
|
||||
console.error("解析用户信息失败:", e);
|
||||
userInfo.value = null;
|
||||
@ -295,14 +294,8 @@ function checkAndReportDailyLogin() {
|
||||
if (!starId) return;
|
||||
|
||||
// 获取用户ID
|
||||
const userStr = uni.getStorageSync("user");
|
||||
let userId = null;
|
||||
if (userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
userId = user?.uid || null;
|
||||
} catch (e) {}
|
||||
}
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
const userId = getStoredUser()?.uid ?? null;
|
||||
if (!userId) return;
|
||||
|
||||
// 每个用户每个明星身份有独立的每日登录状态
|
||||
|
||||
@ -56,6 +56,7 @@ import { useShare } from '@/composables/useShare.js';
|
||||
import { useAliveGuard } from '@/composables/useAliveGuard.js';
|
||||
import { LANDING_BASE, SHARE_TARGETS } from '@/utils/constants.js';
|
||||
import { trackShareApi } from '@/utils/api.js';
|
||||
import { getStoredUser } from '@/utils/getStoredUser.js';;
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
@ -75,7 +76,8 @@ const emit = defineEmits(['close']);
|
||||
const visibleLocal = ref(props.visible);
|
||||
watch(() => props.visible, (v) => { visibleLocal.value = v; });
|
||||
const isLoggedIn = computed(() => {
|
||||
try { return !!JSON.parse(uni.getStorageSync('user') || '{}').uid; } catch { return false; }
|
||||
// ★ 2026-07-27 refactor:统一用 @/utils/getStoredUser(原写法 `JSON.parse(...||'{}')`+try/catch 也安全,这里是为单点真理)
|
||||
try { return !!getStoredUser()?.uid; } catch { return false; }
|
||||
});
|
||||
|
||||
const { state, pick, currentSlogan, systemType } = useShare({
|
||||
@ -109,13 +111,13 @@ function copyLink() {
|
||||
// 兜底:系统类型未就绪时给个 'other'(与后端 QR 接口校验一致)
|
||||
const os = systemType.value || 'other';
|
||||
// 当前用户 uid(分享归因 from 参数),从本地存储读取;未登录则不附带
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
let from = '';
|
||||
let uid = 0;
|
||||
try {
|
||||
const userStr = uni.getStorageSync('user');
|
||||
if (userStr) {
|
||||
const userInfo = JSON.parse(userStr);
|
||||
const rawUid = userInfo?.uid || userInfo?.user_id;
|
||||
const userInfo = getStoredUser();
|
||||
if (userInfo) {
|
||||
const rawUid = userInfo.uid || userInfo.user_id;
|
||||
from = String(rawUid || '');
|
||||
uid = Number(rawUid) || 0;
|
||||
}
|
||||
|
||||
@ -40,6 +40,7 @@ import ShareModal from './ShareModal.vue';
|
||||
import ReportModal from './ReportModal.vue';
|
||||
import { getShareQrcodeApi } from '@/utils/api.js';
|
||||
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
|
||||
import { getStoredUser } from '@/utils/getStoredUser.js';;
|
||||
|
||||
const props = defineProps({
|
||||
// 资产拥有者的昵称
|
||||
@ -77,11 +78,9 @@ const props = defineProps({
|
||||
// 当前用户昵称
|
||||
const currentNickname = computed(() => {
|
||||
try {
|
||||
const userStr = uni.getStorageSync('user');
|
||||
if (userStr) {
|
||||
const userInfo = JSON.parse(userStr);
|
||||
return userInfo?.nickname || '';
|
||||
}
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
const userInfo = getStoredUser();
|
||||
return userInfo?.nickname || '';
|
||||
} catch (e) {
|
||||
console.error('获取用户信息失败:', e);
|
||||
}
|
||||
@ -105,8 +104,8 @@ const showReportModal = ref(false);
|
||||
// 分享
|
||||
const handleShare = async () => {
|
||||
// 登录门槛:未登录不打开弹窗(spec § 3.4)
|
||||
const userStr = uni.getStorageSync('user');
|
||||
if (!userStr || !JSON.parse(userStr)?.uid) {
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
if (!getStoredUser()?.uid) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' });
|
||||
setTimeout(() => uni.navigateTo({ url: '/pages/login/login' }), 800);
|
||||
return;
|
||||
@ -132,15 +131,11 @@ const handleShare = async () => {
|
||||
}
|
||||
shareCoverUrl.value = cover;
|
||||
|
||||
// 分享者头像:从本地存储读取当前登录用户的头像
|
||||
// 分享者头像:从本地存储读取当前登录用户的头像
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
try {
|
||||
const userStr2 = uni.getStorageSync('user');
|
||||
if (userStr2) {
|
||||
const userInfo = JSON.parse(userStr2);
|
||||
shareAvatarUrl.value = userInfo?.avatar_url || '/static/square/gerenzhongxinkangpinkuang.png';
|
||||
} else {
|
||||
shareAvatarUrl.value = '/static/square/gerenzhongxinkangpinkuang.png';
|
||||
}
|
||||
const userInfo = getStoredUser();
|
||||
shareAvatarUrl.value = userInfo?.avatar_url || '/static/square/gerenzhongxinkangpinkuang.png';
|
||||
} catch (e) {
|
||||
console.error('获取用户头像失败:', e);
|
||||
shareAvatarUrl.value = '/static/square/gerenzhongxinkangpinkuang.png';
|
||||
@ -153,9 +148,9 @@ const handleShare = async () => {
|
||||
|
||||
// 在 showShareModal = true 后(或 watch 回调里),异步拉真实二维码 URL(spec § 3)
|
||||
async function refreshQrcode() {
|
||||
const userStr = uni.getStorageSync('user');
|
||||
if (!userStr) return;
|
||||
const user = JSON.parse(userStr);
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
const user = getStoredUser();
|
||||
if (!user) return;
|
||||
try {
|
||||
// 用异步版本(spec § 3.3 推荐 — 不阻塞 UI 线程)
|
||||
const { platform } = await uni.getSystemInfo();
|
||||
|
||||
@ -377,6 +377,7 @@ export default {
|
||||
import NftCard from '../components/NftCard.vue';
|
||||
import { getMyGalleriesApi, placeAssetToGalleryApi, getMyAssetsApi, removeAssetFromGalleryApi, getUserGalleriesApi, getRandomGalleryApi } from '@/utils/api.js';
|
||||
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
|
||||
import { getStoredUser } from '@/utils/getStoredUser.js';
|
||||
import GuideOverlay from "@/components/GuideOverlay.vue";
|
||||
|
||||
const store = useStore();
|
||||
@ -827,16 +828,10 @@ export default {
|
||||
} else {
|
||||
// 访问他人展馆,返回自己的展馆
|
||||
// 先确保 currentUserUid 有值
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
if (!currentUserUid.value) {
|
||||
const userStr = uni.getStorageSync('user');
|
||||
if (userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
currentUserUid.value = user.uid;
|
||||
} catch (e) {
|
||||
console.error('解析用户信息失败:', e);
|
||||
}
|
||||
}
|
||||
const user = getStoredUser();
|
||||
if (user?.uid) currentUserUid.value = user.uid;
|
||||
}
|
||||
|
||||
// 先清空状态,确保 UI 先更新
|
||||
@ -1237,7 +1232,9 @@ export default {
|
||||
|
||||
let currentUserUid;
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
const user = getStoredUser();
|
||||
if (!user) return;
|
||||
currentUserUid = user.uid;
|
||||
} catch (e) {
|
||||
console.error('解析用户信息失败:', e);
|
||||
@ -1488,15 +1485,9 @@ export default {
|
||||
onLoad((options) => {
|
||||
|
||||
// 获取当前用户UID
|
||||
const userStr = uni.getStorageSync('user');
|
||||
if (userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
currentUserUid.value = user.uid;
|
||||
} catch (e) {
|
||||
console.error('解析用户信息失败:', e);
|
||||
}
|
||||
}
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
const userOnLoad = getStoredUser();
|
||||
if (userOnLoad?.uid) currentUserUid.value = userOnLoad.uid;
|
||||
|
||||
// 如果传入了target_uid参数,则访问指定用户的展馆
|
||||
if (options && options.target_uid) {
|
||||
@ -1539,16 +1530,10 @@ export default {
|
||||
calculateTopPadding();
|
||||
|
||||
// 确保 currentUserUid 有值
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
if (!currentUserUid.value) {
|
||||
const userStr = uni.getStorageSync('user');
|
||||
if (userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
currentUserUid.value = user.uid;
|
||||
} catch (e) {
|
||||
console.error('解析用户信息失败:', e);
|
||||
}
|
||||
}
|
||||
const user = getStoredUser();
|
||||
if (user?.uid) currentUserUid.value = user.uid;
|
||||
}
|
||||
|
||||
// 如果 visitingGalleryOwnerUid 没有值,设置为当前用户
|
||||
|
||||
@ -119,9 +119,13 @@
|
||||
import { ref } from "vue";
|
||||
import LoginBackground from "@/components/LoginBackground.vue";
|
||||
import { AGREEMENT_CONTENT } from "@/utils/agreement";
|
||||
import { getStoredUser } from "@/utils/getStoredUser.js";
|
||||
|
||||
// 用户信息(从本地缓存读取 user 信息)
|
||||
const userInfo = JSON.parse(uni.getStorageSync("user")) || {};
|
||||
// 用户信息(从本地缓存读取 user 信息)
|
||||
// ★ 修复:uni.getStorageSync('user') 在 key 未写过时返回 ""或 undefined,
|
||||
// JSON.parse("") 会抛 SyntaxError: Unexpected end of JSON input,导致 setup 崩溃。
|
||||
// 统一委托 @/utils/getStoredUser.js(标准守卫写法),失败降级为 {}。
|
||||
const userInfo = getStoredUser() || {};
|
||||
// 本地脱敏兜底:服务端若返回原始手机号,前端做脱敏
|
||||
const maskPhone = (phone) => {
|
||||
if (!phone) return "";
|
||||
|
||||
@ -618,6 +618,7 @@ import GuideModal from "@/pages/tasks/GuideModal.vue";
|
||||
import GuideOverlay from "@/components/GuideOverlay.vue";
|
||||
import FeedbackModal from "../components/FeedbackModal.vue";
|
||||
import { getClaimableRewardCount } from "@/utils/guideConfig.js";
|
||||
import { getStoredUser } from "@/utils/getStoredUser.js";
|
||||
|
||||
const store = useStore();
|
||||
|
||||
@ -1545,9 +1546,9 @@ const handleAvatarUpdateSuccess = async (newAvatarUrl) => {
|
||||
// URL 字符串天然不同,<image> 会重新拉取,无需再追加 ?t= 兜底
|
||||
// 1. 更新本地缓存
|
||||
userAvatarUrl.value = newAvatarUrl;
|
||||
const userStr = uni.getStorageSync("user");
|
||||
if (userStr) {
|
||||
const user = JSON.parse(userStr);
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
const user = getStoredUser();
|
||||
if (user) {
|
||||
user.avatar_url = newAvatarUrl;
|
||||
uni.setStorageSync("user", JSON.stringify(user));
|
||||
}
|
||||
|
||||
@ -128,6 +128,7 @@ import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { purchaseItem } from "@/utils/activity-config";
|
||||
import { getEarningsSummaryApi } from "@/utils/api";
|
||||
import DiamondConfirmModal from "./DiamondConfirmModal.vue";
|
||||
import { getStoredUser } from "@/utils/getStoredUser.js";
|
||||
|
||||
const props = defineProps({
|
||||
activityId: {
|
||||
@ -274,11 +275,8 @@ const crystalBalance = ref(0);
|
||||
|
||||
async function loadUserInfo() {
|
||||
try {
|
||||
const userStr = uni.getStorageSync("user");
|
||||
if (userStr) {
|
||||
userInfo.value =
|
||||
typeof userStr === "string" ? JSON.parse(userStr) : userStr;
|
||||
}
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
userInfo.value = getStoredUser();
|
||||
|
||||
// 从API获取真实余额
|
||||
const res = await getEarningsSummaryApi();
|
||||
@ -623,17 +621,10 @@ function addToPendingQueue(item) {
|
||||
// 乐观扣除本地余额(离线入队时使用)
|
||||
function deductLocalBalance(cost) {
|
||||
try {
|
||||
const userStr = uni.getStorageSync("user");
|
||||
if (userStr) {
|
||||
// 确保正确解析用户对象
|
||||
let user;
|
||||
if (typeof userStr === "string") {
|
||||
user = JSON.parse(userStr);
|
||||
} else {
|
||||
user = { ...userStr };
|
||||
}
|
||||
|
||||
// 扣除余额,确保不为负数
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
const user = getStoredUser();
|
||||
if (user) {
|
||||
// 扣除余额,确保不为负数
|
||||
user.crystal_balance = Math.max(
|
||||
0,
|
||||
(Number(user.crystal_balance) || 0) - cost,
|
||||
@ -650,19 +641,12 @@ function deductLocalBalance(cost) {
|
||||
}
|
||||
}
|
||||
|
||||
// 退还本地余额(同步彻底失败时使用)
|
||||
// 退还本地余额(同步彻底失败时使用)
|
||||
function refundLocalBalance(cost) {
|
||||
try {
|
||||
const userStr = uni.getStorageSync("user");
|
||||
if (userStr) {
|
||||
// 确保正确解析用户对象
|
||||
let user;
|
||||
if (typeof userStr === "string") {
|
||||
user = JSON.parse(userStr);
|
||||
} else {
|
||||
user = { ...userStr };
|
||||
}
|
||||
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
const user = getStoredUser();
|
||||
if (user) {
|
||||
// 退还余额
|
||||
user.crystal_balance = (Number(user.crystal_balance) || 0) + cost;
|
||||
|
||||
@ -678,21 +662,13 @@ function refundLocalBalance(cost) {
|
||||
}
|
||||
async function updateLocalBalanceFromResult(newBalance) {
|
||||
try {
|
||||
const userStr = uni.getStorageSync("user");
|
||||
if (userStr) {
|
||||
// 确保正确解析用户对象
|
||||
let user;
|
||||
if (typeof userStr === "string") {
|
||||
user = JSON.parse(userStr);
|
||||
} else {
|
||||
// 如果已经是对象,创建一个新副本避免引用问题
|
||||
user = { ...userStr };
|
||||
}
|
||||
|
||||
// 更新余额,确保是数字类型
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
const user = getStoredUser();
|
||||
if (user) {
|
||||
// 更新余额,确保是数字类型
|
||||
user.crystal_balance = Number(newBalance) || 0;
|
||||
|
||||
// 保存回存储,确保是字符串格式
|
||||
// 保存回存储,确保是字符串格式
|
||||
uni.setStorageSync("user", JSON.stringify(user));
|
||||
userInfo.value = user;
|
||||
crystalBalance.value = user.crystal_balance;
|
||||
|
||||
@ -50,6 +50,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { getActivityTopRankingApi } from "@/utils/api.js";
|
||||
import { getStoredUser } from "@/utils/getStoredUser.js";
|
||||
|
||||
const props = defineProps({
|
||||
activityId: {
|
||||
@ -74,14 +75,12 @@ const myInfo = ref({
|
||||
gapToPrev: 0,
|
||||
});
|
||||
|
||||
// 兜底:从未登录态 / 本地缓存读取用户头像
|
||||
// 兜底:从未登录态 / 本地缓存读取用户头像
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
function getFallbackAvatar() {
|
||||
try {
|
||||
const userStr = uni.getStorageSync("user");
|
||||
if (userStr) {
|
||||
const u = typeof userStr === "string" ? JSON.parse(userStr) : userStr;
|
||||
return u?.avatar_url || u?.avatar || "";
|
||||
}
|
||||
const u = getStoredUser() || {};
|
||||
return u.avatar_url || u.avatar || "";
|
||||
} catch (e) {
|
||||
// 忽略解析错误
|
||||
}
|
||||
|
||||
@ -204,6 +204,7 @@ import StageArea from "./components/StageArea.vue";
|
||||
import FloatingBubbles from "./components/FloatingBubbles.vue";
|
||||
import ActivityRankingModal from "./components/ActivityRankingModal.vue";
|
||||
import ActionBar from "./components/ActionBar.vue";
|
||||
import { getStoredUser } from '@/utils/getStoredUser.js';
|
||||
import TopRanking from "./components/TopRanking.vue";
|
||||
import MessageBoard from "./components/MessageBoard.vue";
|
||||
import MessageInput from "./components/MessageInput.vue";
|
||||
@ -428,14 +429,13 @@ async function handleContribute(itemType, remainingBalance) {
|
||||
// 购买成功后刷新左上角水晶余额
|
||||
if (remainingBalance !== null && remainingBalance !== undefined) {
|
||||
exhibitionRevenue.value = Number(remainingBalance) || 0;
|
||||
// 同步本地用户存储,保持和 ActionBar 内部逻辑一致
|
||||
const userStr = uni.getStorageSync("user");
|
||||
if (userStr) {
|
||||
const user =
|
||||
typeof userStr === "string" ? JSON.parse(userStr) : { ...userStr };
|
||||
// 同步本地用户存储,保持和 ActionBar 内部逻辑一致
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,消除 typeof/JSON.parse 隐患
|
||||
const user = getStoredUser();
|
||||
if (user) {
|
||||
user.crystal_balance = Number(remainingBalance) || 0;
|
||||
uni.setStorageSync("user", JSON.stringify(user));
|
||||
// 通知其他订阅 balanceUpdated 的组件(如 Header)
|
||||
// 通知其他订阅 balanceUpdated 的组件(如 Header)
|
||||
uni.$emit("balanceUpdated", { crystal_balance: user.crystal_balance });
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,6 +107,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { getOnboardingStatus, claimOnboardingReward, completeGuide } from '@/utils/task-api.js'
|
||||
import { getStoredUser } from '@/utils/getStoredUser.js'
|
||||
import {
|
||||
getGuideConfig,
|
||||
getGuideStatusList,
|
||||
@ -393,9 +394,9 @@ async function handleClaimReward(key) {
|
||||
// 更新本地存储的余额并通知 Header 组件
|
||||
if (res.data?.crystal_balance !== undefined) {
|
||||
try {
|
||||
const userStr = uni.getStorageSync('user')
|
||||
if (userStr) {
|
||||
const user = JSON.parse(userStr)
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
const user = getStoredUser()
|
||||
if (user) {
|
||||
user.crystal_balance = parseInt(res.data.crystal_balance)
|
||||
uni.setStorageSync('user', JSON.stringify(user))
|
||||
}
|
||||
|
||||
@ -131,6 +131,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { getDailyTasks, claimDailyTask, claimAllDailyTasks } from '@/utils/task-api.js'
|
||||
import { getStoredUser } from '@/utils/getStoredUser.js'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@ -342,21 +343,14 @@ async function handleClaim(task) {
|
||||
tasks.value[index].status = 'claimed'
|
||||
tasks.value[index].can_claim = false
|
||||
}
|
||||
const userStr = uni.getStorageSync('user')
|
||||
if (userStr) {
|
||||
// 确保正确解析用户对象
|
||||
let user
|
||||
if (typeof userStr === 'string') {
|
||||
user = JSON.parse(userStr)
|
||||
} else {
|
||||
// 如果已经是对象,创建一个新副本避免引用问题
|
||||
user = { ...userStr }
|
||||
}
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
const user = getStoredUser()
|
||||
if (user) {
|
||||
emit('updated')
|
||||
// 更新余额,确保是数字类型
|
||||
// 更新余额,确保是数字类型
|
||||
user.crystal_balance = Number(res.data?.crystal_balance) || 0
|
||||
|
||||
// 保存回存储,确保是字符串格式
|
||||
// 保存回存储,确保是字符串格式
|
||||
uni.setStorageSync('user', JSON.stringify(user))
|
||||
uni.$emit('balanceUpdated', { crystal_balance: user.crystal_balance })
|
||||
}
|
||||
@ -384,21 +378,14 @@ async function handleClaimAll() {
|
||||
})
|
||||
|
||||
emit('updated')
|
||||
const userStr = uni.getStorageSync('user')
|
||||
if (userStr) {
|
||||
// 确保正确解析用户对象
|
||||
let user
|
||||
if (typeof userStr === 'string') {
|
||||
user = JSON.parse(userStr)
|
||||
} else {
|
||||
// 如果已经是对象,创建一个新副本避免引用问题
|
||||
user = { ...userStr }
|
||||
}
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||||
const user = getStoredUser()
|
||||
if (user) {
|
||||
emit('updated')
|
||||
// 更新余额,确保是数字类型
|
||||
// 更新余额,确保是数字类型
|
||||
user.crystal_balance = Number(res.data?.crystal_balance) || 0
|
||||
|
||||
// 保存回存储,确保是字符串格式
|
||||
// 保存回存储,确保是字符串格式
|
||||
uni.setStorageSync('user', JSON.stringify(user))
|
||||
uni.$emit('balanceUpdated', { crystal_balance: user.crystal_balance })
|
||||
}
|
||||
|
||||
43
frontend/utils/getStoredUser.js
Normal file
43
frontend/utils/getStoredUser.js
Normal file
@ -0,0 +1,43 @@
|
||||
// frontend/utils/getStoredUser.js
|
||||
// 安全读取 uni.getStorageSync('user') 并 JSON.parse —— 单点封装,消除全库 ~20 处
|
||||
// 重复且不安全的 `JSON.parse(userStr)` 裸调用。
|
||||
//
|
||||
// 已知坑(踩过 2 次,均以红色 stack trace 形式出现):
|
||||
// 1. `uni.getStorageSync('user')` 在 key 从未写过的 UniApp 平台下默认返回 ""
|
||||
// (空串)而不是 null/undefined,旧式 `JSON.parse(getStorageSync('user')) || {}`
|
||||
// 会在 parse 阶段直接抛 `SyntaxError: Unexpected end of JSON input`,setup 崩溃 →
|
||||
// Vue 触发 `[Vue warn]: Invalid vnode type when creating vnode: undefined`
|
||||
// 二次告警(同一个根因)。最低成本修复是 `|| '{}'` 兜底,但仍无法防 JSON 损坏场景。
|
||||
// 2. 上线后曾遇到"token 有效但 user 缓存被清"的中间态
|
||||
// (升级包 / 跨设备 tab 登出 / 部分页面单独清缓存),彼时 setup 抛错导致整页白屏。
|
||||
//
|
||||
// 使用规范:
|
||||
// import { getStoredUser } from '@/utils/getStoredUser'
|
||||
// const user = getStoredUser() // 失败返回 null
|
||||
// const user = getStoredUser() || {} // 失败降级为空对象
|
||||
// const uid = getStoredUser()?.uid // 失败时为 undefined
|
||||
//
|
||||
// 与 store/modules/user.js 初始化块保持同源语义;后续如需支持多 key (e.g. 'adminUser')
|
||||
// 再扩成泛型 getStoredJSON(key) 即可。
|
||||
|
||||
/**
|
||||
* 安全读取本地缓存中的 user(JSON) 并解析为对象。
|
||||
*
|
||||
* 失败场景(return null):
|
||||
* - 'user' key 不存在 → uni.getStorageSync 返回 "" 或 undefined
|
||||
* - 'user' 缓存损坏 / 不是合法 JSON → JSON.parse 抛错
|
||||
* - 'user' 是合法 JSON 但解析后是 null(如 'null' 字符串)→ 兜底返回 null
|
||||
*
|
||||
* @returns {object|null} 解析成功返回 user 对象;失败返回 null
|
||||
*/
|
||||
export function getStoredUser() {
|
||||
const userStr = uni.getStorageSync('user');
|
||||
if (!userStr) return null;
|
||||
try {
|
||||
return JSON.parse(userStr) || null;
|
||||
} catch (e) {
|
||||
// warn 级别即可,不要 error —— 这是可预期的 cache miss / 损坏,不是真错
|
||||
console.warn('[getStoredUser] failed to parse user storage:', e?.message || e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,8 @@
|
||||
* - 关闭调试:访问 pages/square/square?guide_debug=0(会清除调试状态和 is_new_user,恢复正式流程)
|
||||
*/
|
||||
|
||||
import { getStoredUser } from '@/utils/getStoredUser.js'
|
||||
|
||||
/**
|
||||
* 引导配置结构
|
||||
* {
|
||||
@ -473,16 +475,8 @@ export function getAllGuideKeys() {
|
||||
* @returns {number|null}
|
||||
*/
|
||||
function getCurrentUserId() {
|
||||
const userStr = uni.getStorageSync('user')
|
||||
if (userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr)
|
||||
return user?.uid || null
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
// ★ 2026-07-27 refactor:委托给 @/utils/getStoredUser.js,统一空值/损坏 JSON 的安全读取
|
||||
return getStoredUser()?.uid ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -11,6 +11,8 @@ import {
|
||||
evictOldest
|
||||
} from './storage'
|
||||
|
||||
import { getStoredUser } from '@/utils/getStoredUser.js'
|
||||
|
||||
// ── 常量 ──
|
||||
const NAMESPACE = 'preload'
|
||||
const DEFAULT_MAX_MEMORY_ENTRIES = 100
|
||||
@ -45,13 +47,10 @@ let _config = {
|
||||
let _fetchers = {}
|
||||
|
||||
// userId 获取函数(由外部注入)
|
||||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser.js,统一空值/损坏 JSON 的安全读取
|
||||
let _getUserId = () => {
|
||||
try {
|
||||
const userStr = uni.getStorageSync('user')
|
||||
if (userStr) {
|
||||
const user = JSON.parse(userStr)
|
||||
return user?.uid || null
|
||||
}
|
||||
return getStoredUser()?.uid ?? null
|
||||
} catch (e) { /* ignore */ }
|
||||
return null
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user