topfans/frontend/pages/components/ShareReportButtons.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

217 lines
6.1 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="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>