topfans/frontend/pages/scan/verify.vue
2026-07-21 16:41:42 +08:00

268 lines
6.5 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="verify-page">
<image class="bg-image" src="/static/square/squearbj.png" mode="aspectFill"></image>
<view class="nav-bar">
<view class="nav-back" @tap="goBack"></view>
<text class="nav-title">周边验真</text>
</view>
<view v-if="loading" class="loading">
<text>加载中…</text>
</view>
<view v-else-if="error" class="error">
<text>{{ error }}</text>
<button @tap="loadData">重试</button>
</view>
<view v-else-if="data" class="content">
<view class="header">
<image class="thumb" :src="data.image || '/static/nft/collection.png'" mode="aspectFit" />
<view class="badge"><text>✓ 已通过验真</text></view>
</view>
<view class="info-card">
<view v-for="row in infoRows" :key="row.label" class="info-row">
<text class="info-label">{{ row.label }}</text>
<text class="info-value" :class="{ 'info-hash': row.hash }">{{ row.value || '—' }}</text>
</view>
</view>
<view class="tip-row">
<text class="tip-icon">💡</text>
<text class="tip-text">加入前请对比实物,确认一致后再添加</text>
</view>
<button class="mint-btn" :loading="submitting" :disabled="submitting" @tap="handleAddToCollection">
加入我的藏品
</button>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getPeripheralByHashApi, mintToMyCollectionByHashApi } from '@/utils/api.js'
// stage 1 强制新版:扫码 URL 形如 /verify/{encrypted_code}?sign=xxx
// encrypted_code(32 hex)+ sign(32 hex)由 QR 码自带,不再支持老版明文 code
const HEX32_RE = /^[0-9a-f]{32}$/i
const encryptedCode = ref('')
const sign = ref('')
const data = ref(null)
const loading = ref(true)
const error = ref('')
const submitting = ref(false)
onLoad((options) => {
// 强校验:必须同时有 encrypted_code + sign
if (HEX32_RE.test(options.encryptedCode || '') && options.sign) {
encryptedCode.value = String(options.encryptedCode)
sign.value = String(options.sign)
loadData()
return
}
// 老入口(明文 code / asset_id)不再支持
error.value = '请使用最新版本扫描周边二维码'
loading.value = false
})
const infoRows = computed(() => {
if (!data.value) return []
const d = data.value
console.log('infoRows data:', d)
return [
{ label: '周边编号', value: d.code || '—' },
{ label: '品牌', value: d.brand },
{ label: '公司', value: d.company },
{ label: '链上哈希', value: d.hash, hash: true },
{ label: '验证次数', value: `${d.verify_count}` },
{ label: '验证人', value: d.verifier },
{ label: '首次验证', value: formatDate(d.verified_at) },
// 周边素材类型(后端 omitempty,空时 d.material_type 为 undefined)
// { label: '素材类型', value: (d.material_type && d.material_type.trim()) || 'new' }
]
})
const goBack = () => {
// 获取页面栈
const pages = getCurrentPages();
if (pages.length > 1) {
// 有上一页,执行返回
uni.navigateBack();
} else {
// 没有上一页跳转到square页面
uni.reLaunch({
url: '/pages/square/square'
});
}
};
async function loadData() {
loading.value = true
error.value = ''
try {
// stage 1:按加密 code 验真(详见 design §5.1.3)
const res = await getPeripheralByHashApi(encryptedCode.value, sign.value)
// stage 1 新增业务码 50013(sign 验证失败)—— 引导用户重新扫码
if (res?.code === 50013) {
throw new Error('验签失败,二维码可能已损坏或被篡改,请重新扫码')
}
if (!res || !res.data) throw new Error(res?.message || '此物品暂无验真信息')
data.value = res.data
} catch (e) {
error.value = e.message || '加载失败'
} finally {
loading.value = false
}
}
async function handleAddToCollection() {
if (submitting.value) return
submitting.value = true
try {
const res = await mintToMyCollectionByHashApi(encryptedCode.value, sign.value)
uni.showToast({ title: '已加入我的藏品', icon: 'success' })
setTimeout(() => {
uni.navigateTo({
url: `/pages/asset-detail/asset-detail?asset_id=${res.data?.asset_id}`
})
}, 800)
} catch (e) {
uni.showToast({ title: e.message || '添加失败', icon: 'none' })
} finally {
submitting.value = false
}
}
function formatDate(ts) {
if (!ts) return ''
const d = new Date(ts * 1000)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
</script>
<style scoped>
.verify-page {
/* background: #0a0a0a; */
min-height: 100vh;
padding: 24rpx;
}
/* 背景图片 */
.bg-image {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 120%;
z-index: 0;
}
.nav-bar {
display: flex;
align-items: center;
height: 88rpx;
padding: 0 24rpx;
color: #fff;
position: relative;
top: 96rpx;
z-index: 1;
margin-bottom: 24rpx;
}
.nav-back {
font-size: 40rpx;
padding-right: 24rpx;
position: relative;
z-index: 1;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 600;
}
.loading,
.error {
color: #aaa;
text-align: center;
padding: 80rpx 0;
position: relative;
z-index: 1;
}
.content {
position: relative;
z-index: 1;
}
.header {
/* display: flex;
align-items: center;
justify-content: center; */
margin-top: 64rpx;
}
.info-card {
/* background: #1a1a1a; */
border-radius: 16rpx;
/* padding: 24rpx; */
margin-top: 24rpx;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 16rpx 0;
/* border-bottom: 1rpx solid #2a2a2a; */
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: rgba(255, 255, 255, 0.7);
font-size: 26rpx;
}
.info-value {
color: #fff;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.7);
font-size: 28rpx;
width: 60%;
word-break: break-all;
text-align: left;
}
.info-value.info-hash {
font-family: monospace;
font-size: 22rpx;
color: #3ddc84;
}
.tip-row {
display: flex;
align-items: center;
gap: 8rpx;
padding: 24rpx 8rpx;
color: #888;
font-size: 26rpx;
}
.mint-btn {
background: linear-gradient(135deg, rgba(255, 222, 8, 0.49) 0%, rgba(252, 100, 102, 1) 61.1%, rgba(244, 88, 104, 1) 100%);
box-shadow: inset 0 0.03125rem 0 rgba(255, 255, 255, 0.45), 2px 2px 4px 0px #f2151578;
color: #fff;
border-radius: 100rpx;
margin-top: 32rpx;
font-weight: 600;
}
</style>