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>
217 lines
6.1 KiB
Vue
217 lines
6.1 KiB
Vue
<template>
|
||
<view class="share-report-btns">
|
||
<!-- 分享按钮 -->
|
||
<view class="action-btn share-btn" @tap="handleShare">
|
||
<image class="btn-icon" src="/static/assetDetail/fenxiang.png" mode="aspectFit"></image>
|
||
<text class="btn-text">分享</text>
|
||
</view>
|
||
<!-- 举报按钮 - 只在不是自己的资产时显示 -->
|
||
<view v-if="showReport" class="action-btn report-btn" @tap="handleReport">
|
||
<image class="btn-icon" src="/static/assetDetail/jubao.png" mode="aspectFit"></image>
|
||
<text class="btn-text">举报</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 分享弹窗 -->
|
||
<ShareModal
|
||
:visible="showShareModal"
|
||
:coverUrl="shareCoverUrl"
|
||
:qrcodeUrl="shareQrcodeUrl"
|
||
:avatarUrl="shareAvatarUrl"
|
||
:nickname="ownerNickname"
|
||
:assetId="assetId"
|
||
:displayTxHash="displayTxHash"
|
||
:externalCanvasId="externalCanvasId"
|
||
@close="showShareModal = false"
|
||
/>
|
||
|
||
<!-- 举报弹窗 -->
|
||
<ReportModal
|
||
:visible="showReportModal"
|
||
:assetId="assetId"
|
||
@close="showReportModal = false"
|
||
@submit="handleReportSubmit"
|
||
/>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed } from 'vue';
|
||
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({
|
||
// 资产拥有者的昵称
|
||
ownerNickname: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// 资产ID,用于分享
|
||
assetId: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// 封面图片(已解析的 CDN URL)
|
||
coverUrl: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// 封面原始 OSS key(异步 coverUrl 还没解析完时,ShareReportButtons 自己 resolve)
|
||
coverKey: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// 外部 page 级 canvasId(app-plus 组件内 canvas 不稳,推荐父页面提供)
|
||
externalCanvasId: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// 链上哈希(原文,由父组件 asset-detail 提供;image-compositor 内部截断渲染)
|
||
displayTxHash: {
|
||
type: String,
|
||
default: ''
|
||
}
|
||
});
|
||
|
||
// 当前用户昵称
|
||
const currentNickname = computed(() => {
|
||
try {
|
||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||
const userInfo = getStoredUser();
|
||
return userInfo?.nickname || '';
|
||
} catch (e) {
|
||
console.error('获取用户信息失败:', e);
|
||
}
|
||
return '';
|
||
});
|
||
|
||
// 是否显示举报按钮(不是自己的资产时显示)
|
||
const showReport = computed(() => {
|
||
return props.ownerNickname && currentNickname.value && props.ownerNickname !== currentNickname.value;
|
||
});
|
||
|
||
// 分享弹窗状态
|
||
const showShareModal = ref(false);
|
||
const shareCoverUrl = ref('');
|
||
const shareQrcodeUrl = ref('');
|
||
const shareAvatarUrl = ref('');
|
||
|
||
// 举报弹窗状态
|
||
const showReportModal = ref(false);
|
||
|
||
// 分享
|
||
const handleShare = async () => {
|
||
// 登录门槛:未登录不打开弹窗(spec § 3.4)
|
||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||
if (!getStoredUser()?.uid) {
|
||
uni.showToast({ title: '请先登录', icon: 'none' });
|
||
setTimeout(() => uni.navigateTo({ url: '/pages/login/login' }), 800);
|
||
return;
|
||
}
|
||
|
||
// 关键:coverUrl 是异步加载的(props.coverUrl 可能还没解析完),如果空就用 coverKey 自己 resolve
|
||
// 优先用 props.coverKey(原始 OSS key) + getAssetCoverRealUrl 拼接真实 URL,
|
||
// 因为 coverUrl 可能只缓存了短期签名 URL,share 流程需要长期可用的 URL
|
||
const sourceKey = props.coverKey || props.coverUrl;
|
||
let cover = '';
|
||
console.log('[ShareReportButtons] handleShare coverUrl =', props.coverUrl, 'coverKey =', props.coverKey);
|
||
try {
|
||
cover = await getAssetCoverRealUrl(sourceKey);
|
||
console.log('[ShareReportButtons] resolved cover =', cover);
|
||
} catch (e) {
|
||
console.warn('[ShareReportButtons] resolve cover failed, fallback raw:', e);
|
||
// 兜底:用原始值
|
||
cover = props.coverUrl || props.coverKey;
|
||
}
|
||
if (!cover) {
|
||
uni.showToast({ title: '封面加载中,请稍后再试', icon: 'none' });
|
||
return;
|
||
}
|
||
shareCoverUrl.value = cover;
|
||
|
||
// 分享者头像:从本地存储读取当前登录用户的头像
|
||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||
try {
|
||
const userInfo = getStoredUser();
|
||
shareAvatarUrl.value = userInfo?.avatar_url || '/static/square/gerenzhongxinkangpinkuang.png';
|
||
} catch (e) {
|
||
console.error('获取用户头像失败:', e);
|
||
shareAvatarUrl.value = '/static/square/gerenzhongxinkangpinkuang.png';
|
||
}
|
||
// 先用 mock 占位,后端真实接口返回后会替换
|
||
shareQrcodeUrl.value = '/static/share/mock_qrcode.png';
|
||
showShareModal.value = true;
|
||
refreshQrcode();
|
||
};
|
||
|
||
// 在 showShareModal = true 后(或 watch 回调里),异步拉真实二维码 URL(spec § 3)
|
||
async function refreshQrcode() {
|
||
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
|
||
const user = getStoredUser();
|
||
if (!user) return;
|
||
try {
|
||
// 用异步版本(spec § 3.3 推荐 — 不阻塞 UI 线程)
|
||
const { platform } = await uni.getSystemInfo();
|
||
const data = await getShareQrcodeApi(props.assetId, user.uid, platform);
|
||
if (data?.qrcode_url) {
|
||
shareQrcodeUrl.value = data.qrcode_url;
|
||
console.log(data.qrcode_url)
|
||
}
|
||
} catch (e) {
|
||
// 失败保持 mock 占位图,不打断用户
|
||
}
|
||
}
|
||
|
||
// 举报 - 打开举报弹窗
|
||
const handleReport = () => {
|
||
if (!props.assetId) {
|
||
uni.showToast({ title: '藏品信息缺失', icon: 'none' });
|
||
return;
|
||
}
|
||
showReportModal.value = true;
|
||
};
|
||
|
||
// 举报提交完成回调(埋点/埋日志可用)
|
||
const handleReportSubmit = (payload) => {
|
||
console.log('[ShareReportButtons] report submitted', payload);
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.share-report-btns {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.action-btn {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
border-radius: 24rpx;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.action-btn:active {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.btn-icon {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.5));
|
||
}
|
||
|
||
.btn-text {
|
||
font-size: 24rpx;
|
||
color: #fff;
|
||
font-family: 'yt', sans-serif;
|
||
text-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.6);
|
||
}
|
||
</style> |