topfans/frontend/pages/scan/verify.vue
2026-07-17 16:53:57 +08:00

258 lines
6.3 KiB
Vue

<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' }
]
})
function goBack() {
uni.navigateBack({ delta: 1, fail: () => uni.switchTab({ 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>