topfans/frontend/pages/asset-detail/asset-detail.vue
2026-05-07 14:12:07 +08:00

676 lines
15 KiB
Vue
Raw Permalink 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="detail-container">
<!-- 背景图片 -->
<image class="background-image" src="/static/background/starbook.jpg" mode="aspectFill"></image>
<!-- Header -->
<view class="header-bar">
<view class="back-btn" @tap="handleBack">
<text class="back-icon"></text>
</view>
</view>
<!-- 加载中 -->
<view v-if="loading" class="loading-wrapper">
<!-- 旋转光环 -->
<view class="loading-ring-outer">
<view class="loading-ring"></view>
</view>
<!-- 三颗脉冲粒子 -->
<view class="loading-dots">
<view class="loading-dot dot-1"></view>
<view class="loading-dot dot-2"></view>
<view class="loading-dot dot-3"></view>
</view>
<!-- 文字淡入淡出 -->
<text class="loading-text">正在加载藏品...</text>
</view>
<!-- 错误状态 -->
<view v-else-if="loadError" class="error-wrapper">
<text class="error-text">{{ loadError }}</text>
<button class="retry-btn" @tap="loadData">重试</button>
</view>
<!-- 详情内容 -->
<scroll-view v-else scroll-y class="content-scroll">
<view class="content-wrapper">
<!-- 藏品卡片区域 -->
<view class="card-section">
<view class="card-wrapper">
<NftCard
:cover-image="coverUrl"
:width="cardSize"
:height="cardSize"
:custom-style="nftCardStyle"
/>
</view>
<!-- 点赞 + 倒计时行 -->
<view class="card-meta-row">
<!-- 点赞 -->
<view class="like-area" @tap="handleLike">
<image
:src="isLiked ? '/static/icon/like-after.png' : '/static/icon/like-before.png'"
class="like-icon"
mode="aspectFit"
/>
<text class="like-num">{{ likeCount }}</text>
</view>
<!-- 倒计时(如有) -->
<view v-if="showCountdown" class="countdown-area">
<view class="countdown-pill">
<text class="countdown-val">{{ countdownText }}</text>
</view>
</view>
</view>
</view>
<!-- 持有人 + 获取时间 -->
<view class="owner-section">
<view class="owner-row">
<view class="owner-info">
<text class="owner-label">持有人</text>
<text class="owner-name">{{ assetData.owner_nickname || '未知' }}</text>
</view>
</view>
<view class="acquire-row">
<text class="acquire-label">获取时间</text>
<text class="acquire-value">{{ formattedAcquireTime }}</text>
</view>
</view>
<!-- 链上数据 -->
<view class="chain-section">
<view class="chain-row">
<text class="chain-label">数根名称</text>
<text class="chain-value">{{ assetData.name || '未知' }}</text>
</view>
<view class="chain-row">
<text class="chain-label">数根编号</text>
<text class="chain-value">{{ assetData.asset_id || '未知' }}</text>
</view>
<view class="chain-row">
<text class="chain-label">数根发行方</text>
<text class="chain-value">TOPFANS</text>
</view>
<view class="chain-row">
<text class="chain-label">区块链编号</text>
<text class="chain-value">{{ assetData.block_number || '未知' }}</text>
</view>
<view class="chain-row">
<text class="chain-label">交易哈希</text>
<text
class="chain-value chain-hash"
@longpress="copyHash"
>{{ displayTxHash }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import NftCard from '../components/NftCard.vue';
import { getAssetDetailApi, getMintOrderDetailApi, likeAssetApi, unlikeAssetApi } from '@/utils/api.js';
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
// 页面参数
const assetIdParam = ref('');
const orderIdParam = ref('');
const fromParam = ref('');
const lastLoadKey = ref(''); // 记录上次加载的key避免重复加载
// 数据状态
const loading = ref(true);
const loadError = ref('');
const assetData = ref({});
const coverUrl = ref('');
const isLiked = ref(false);
const likeCount = ref(0);
const liking = ref(false);
// 倒计时
const remainSeconds = ref(0);
let countdownTimer = null;
const showCountdown = computed(() => remainSeconds.value > 0);
const countdownText = computed(() => {
const h = String(Math.floor(remainSeconds.value / 3600)).padStart(2, '0');
const m = String(Math.floor((remainSeconds.value % 3600) / 60)).padStart(2, '0');
const s = String(Math.floor(remainSeconds.value % 60)).padStart(2, '0');
return `${h}:${m}:${s}`;
});
// 卡片尺寸(固定 px
const cardSize = 280;
const nftCardStyle = {
position: 'relative',
pointerEvents: 'none',
cursor: 'default',
transition: 'none'
};
// 格式化获取时间(字段名 created_at来自 getAssetDetailApi 响应)
const formattedAcquireTime = computed(() => {
const raw = assetData.value.created_at || '';
if (!raw) return '未知';
const d = new Date(raw);
if (isNaN(d.getTime())) return raw;
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${yyyy}.${mm}.${dd}`;
});
// 截断交易哈希
const displayTxHash = computed(() => {
const hash = assetData.value.tx_hash;
if (!hash) return '未知';
if (hash.length <= 20) return hash;
return `${hash.substring(0, 10)}...${hash.substring(hash.length - 8)}`;
});
// 复制完整哈希
const copyHash = () => {
const hash = assetData.value.tx_hash;
if (!hash) return;
uni.setClipboardData({
data: hash,
success: () => uni.showToast({ title: '已复制', icon: 'success', duration: 1500 })
});
};
// 加载数据
const loadData = async () => {
console.log('loadData 开始执行');
loading.value = true;
loadError.value = '';
try {
let assetId = assetIdParam.value;
console.log('assetIdParam:', assetIdParam.value, 'orderIdParam:', orderIdParam.value);
if (!assetId && orderIdParam.value) {
console.log('通过 order_id 获取资产信息');
const mintRes = await getMintOrderDetailApi(orderIdParam.value);
console.log('getMintOrderDetailApi 响应:', mintRes);
if (mintRes.code === 200 && mintRes.data?.asset?.asset_id) {
assetId = mintRes.data.asset.asset_id;
console.log('获取到 assetId:', assetId);
} else {
throw new Error('获取铸造订单详情失败');
}
}
if (!assetId) throw new Error('藏品信息不完整');
console.log('调用 getAssetDetailApiassetId:', assetId);
const res = await getAssetDetailApi(assetId);
console.log('getAssetDetailApi 响应:', res);
if (res.code === 200 && res.data?.asset) {
const asset = res.data.asset;
assetData.value = asset;
isLiked.value = res.data.asset.is_liked || res.data.is_liked || false;
likeCount.value = asset.like_count || 0;
if (asset.remain_time > 0) {
remainSeconds.value = asset.remain_time;
startCountdown();
}
console.log(res.data)
// 异步加载封面图片,不阻塞页面渲染
console.log('开始加载封面图片:', asset.cover_url);
getAssetCoverRealUrl(asset.cover_url).then(url => {
console.log('封面图片加载成功:', url);
coverUrl.value = url;
}).catch(err => {
console.error('加载封面图片失败:', err);
coverUrl.value = ''; // 失败时不显示默认图片
});
console.log('loadData 执行成功');
} else {
throw new Error(res.message || '获取藏品详情失败');
}
} catch (err) {
console.error('loadData 出错:', err);
loadError.value = err.message || '加载失败,请重试';
} finally {
loading.value = false;
console.log('loadData 执行完成loading:', loading.value);
}
};
// 倒计时
const startCountdown = () => {
if (countdownTimer) clearInterval(countdownTimer);
countdownTimer = setInterval(() => {
if (remainSeconds.value > 0) {
remainSeconds.value--;
} else {
clearInterval(countdownTimer);
}
}, 1000);
};
// 点赞
const handleLike = async () => {
if (liking.value || !assetData.value.asset_id) return;
liking.value = true;
try {
// if (isLiked.value) {
// await unlikeAssetApi(assetData.value.asset_id);
// isLiked.value = false;
// likeCount.value = Math.max(0, likeCount.value - 1);
// } else {
await likeAssetApi(assetData.value.asset_id);
isLiked.value = true;
likeCount.value += 1;
// }
// 通知展馆页面更新点赞数
uni.$emit('assetLikeChanged', {
asset_id: assetData.value.asset_id,
like_count: likeCount.value,
is_liked: isLiked.value
});
} catch (err) {
uni.showToast({ title: err.message || '操作失败', icon: 'none', duration: 2000 });
} finally {
liking.value = false;
}
};
// 返回逻辑
const handleBack = () => {
if (fromParam.value === 'castlove') {
uni.reLaunch({ url: '/pages/castlove/mall' });
} else {
uni.navigateBack();
}
};
onLoad((options) => {
console.log('onLoad 触发,参数:', options);
assetIdParam.value = options?.asset_id || '';
orderIdParam.value = options?.order_id || '';
fromParam.value = options?.from || '';
});
onShow(() => {
console.log('onShow 触发');
const currentKey = `${assetIdParam.value}_${orderIdParam.value}`;
console.log('当前key:', currentKey, '上次key:', lastLoadKey.value);
// 如果参数变化了,或者是首次加载,则重新加载数据
if ((assetIdParam.value || orderIdParam.value) && currentKey !== lastLoadKey.value) {
lastLoadKey.value = currentKey;
loadData();
}
});
onUnmounted(() => {
if (countdownTimer) clearInterval(countdownTimer);
});
</script>
<style scoped>
.detail-container {
position: relative;
width: 100vw;
min-height: 100vh;
overflow: hidden;
background-color: #0d0820;
}
.background-image {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 0;
}
/* Header */
.header-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 180rpx;
display: flex;
align-items: flex-end;
padding: 0 30rpx 20rpx;
z-index: 100;
}
.back-btn {
display: flex;
align-items: center;
justify-content: center;
}
.back-btn:active {
opacity: 0.6;
}
.back-icon {
font-size: 48rpx;
color: #1a1a1a;
line-height: 1;
text-shadow: 0 1rpx 4rpx rgba(255, 255, 255, 0.6);
}
/* 加载/错误 */
.loading-wrapper,
.error-wrapper {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
gap: 30rpx;
}
/* 旋转光环 */
.loading-ring-outer {
position: relative;
width: 160rpx;
height: 160rpx;
display: flex;
align-items: center;
justify-content: center;
}
.loading-ring {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
border: 6rpx solid transparent;
border-top-color: #F0E4B1;
border-right-color: #F08399;
border-bottom-color: #B94E73;
border-left-color: transparent;
box-shadow:
0 0 24rpx rgba(240, 131, 153, 0.5),
inset 0 0 16rpx rgba(183, 78, 115, 0.2);
animation: ring-spin 1.2s linear infinite;
}
@keyframes ring-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 三颗脉冲粒子 */
.loading-dots {
display: flex;
align-items: center;
gap: 24rpx;
}
.loading-dot {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
animation: dot-pulse 1.4s ease-in-out infinite;
}
.dot-1 {
background: #F0E4B1;
box-shadow: 0 0 12rpx rgba(240, 228, 177, 0.8);
animation-delay: 0s;
}
.dot-2 {
background: #F08399;
box-shadow: 0 0 12rpx rgba(240, 131, 153, 0.8);
animation-delay: 0.2s;
}
.dot-3 {
background: #834B9E;
box-shadow: 0 0 12rpx rgba(131, 75, 158, 0.8);
animation-delay: 0.4s;
}
@keyframes dot-pulse {
0%, 100% {
transform: scale(0.6);
opacity: 0.4;
}
50% {
transform: scale(1.3);
opacity: 1;
}
}
/* 加载文字 */
.loading-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.85);
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
letter-spacing: 4rpx;
animation: text-breathe 2s ease-in-out infinite;
}
@keyframes text-breathe {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.error-text {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.8);
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
}
.retry-btn {
padding: 16rpx 48rpx;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
color: #fff;
border-radius: 40rpx;
font-size: 28rpx;
border: none;
}
.retry-btn::after {
border: none;
}
/* 滚动区 */
.content-scroll {
position: relative;
z-index: 1;
height: 100vh;
}
.content-wrapper {
padding: 200rpx 40rpx 80rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
}
/* 卡片区域 */
.card-section {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 48rpx;
}
.card-wrapper {
margin-bottom: 28rpx;
}
.card-meta-row {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 24rpx;
}
/* 点赞 */
.like-area {
display: flex;
align-items: center;
gap: 10rpx;
background: rgba(0, 0, 0, 0.22);
border-radius: 999rpx;
padding: 10rpx 28rpx;
backdrop-filter: blur(12rpx);
}
.like-area:active {
opacity: 0.75;
}
.like-icon {
width: 44rpx;
height: 44rpx;
}
.like-num {
font-size: 32rpx;
font-weight: bold;
color: #e6e6e6;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
font-variant-numeric: tabular-nums;
}
/* 倒计时 */
.countdown-area {
display: flex;
align-items: center;
}
.countdown-pill {
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-radius: 999rpx;
padding: 10rpx 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.4);
}
.countdown-val {
font-size: 28rpx;
font-weight: bold;
color: #fff;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
font-variant-numeric: tabular-nums;
}
/* 持有人区域 */
.owner-section {
width: 100%;
margin-bottom: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
}
.owner-row {
display: flex;
align-items: center;
justify-content: center;
}
.owner-info {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 4rpx;
}
.owner-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
text-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.6);
}
.owner-name {
font-size: 36rpx;
font-weight: bold;
color: #ffffff;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.7);
}
.acquire-row {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 4rpx;
}
.acquire-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
text-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.6);
}
.acquire-value {
font-size: 32rpx;
color: #ffffff;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.7);
}
/* 链上数据 */
.chain-section {
width: calc(100% - 80rpx);
background: rgba(0, 0, 0, 0.22);
border-radius: 24rpx;
padding: 30rpx 48rpx;
backdrop-filter: blur(12rpx);
display: flex;
flex-direction: column;
gap: 0;
}
.chain-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 18rpx 0;
}
.chain-label {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
flex-shrink: 0;
margin-right: 20rpx;
}
.chain-value {
font-size: 28rpx;
color: #ffffff;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
text-align: right;
flex: 1;
word-break: break-all;
}
.chain-hash {
color: rgba(255, 255, 255, 0.75);
}
</style>