268 lines
6.5 KiB
Vue
268 lines
6.5 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' }
|
||
]
|
||
})
|
||
|
||
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> |