676 lines
15 KiB
Vue
676 lines
15 KiB
Vue
<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('调用 getAssetDetailApi,assetId:', 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>
|