style: 藏品详细样式修改

This commit is contained in:
zheng020 2026-05-15 15:21:54 +08:00
parent f0b0df2ebf
commit 5edf153521
7 changed files with 435 additions and 127 deletions

View File

@ -33,7 +33,7 @@
</view>
<!-- 详情内容 -->
<scroll-view v-else scroll-y class="content-scroll">
<scroll-view v-else scroll-y class="content-scroll" :show-scrollbar="false">
<view class="content-wrapper">
<!-- 藏品卡片区域 -->
<view class="card-section">
@ -47,8 +47,9 @@
<!-- 点赞 + 收益 + 倒计时行 -->
<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"></image>
<view v-if="assetData.display_status === 1" class="like-area" @tap="handleLike">
<image :src="isLiked ? '/static/icon/like-after.png' : '/static/icon/like-before.png'"
class="like-icon" mode="aspectFit"></image>
<text class="like-num">{{ likeCount }}</text>
</view>
@ -59,7 +60,7 @@
</view>
<!-- 倒计时如有 -->
<view v-if="showCountdown" class="countdown-area">
<view v-if="assetData.display_status === 1 && countdownText" class="countdown-area">
<view class="countdown-pill">
<text class="countdown-val">{{ countdownText }}</text>
</view>
@ -69,10 +70,19 @@
<!-- 信息行创作者 + 铸造时间 + 来源 -->
<view class="info-row">
<view class="info-col">
<view class="info-item">
<text class="info-label">来源</text>
<text class="info-value">{{ assetData.source || '铸造' }}</text>
</view>
</view>
<view class="info-col">
<view class="info-item">
<text class="info-label">创作者</text>
<text class="info-value">{{ assetData.owner_nickname || '未知' }}</text>
<view class="info-nickname">
<image v-if="userAvatarUrl" class="info-avatar" :src="userAvatarUrl" mode="aspectFill"></image>
<text class="info-value">{{ assetData.owner_nickname || '未知' }}</text>
</view>
</view>
</view>
<view class="info-col">
@ -81,27 +91,16 @@
<text class="info-value">{{ formattedAcquireTime }}</text>
</view>
</view>
<view class="info-col">
<view class="info-item">
<text class="info-label">来源</text>
<text class="info-value">{{ assetData.source || '铸造' }}</text>
</view>
</view>
</view>
<!-- 创作者信息模块 -->
<view class="creator-section" @tap="showLikeUsersModal = true">
<!-- <view v-if="likeCount > 0" class="creator-section" @tap="showLikeUsersModal = true"> -->
<!-- 点赞用户头像区域 -->
<view class="liked-users-area">
<view
v-for="(user, index) in likedUsers"
:key="index"
class="liked-user-avatar"
:style="{
transform: `translate(${user.ellipseX || 0}rpx, ${user.ellipseY || 0}rpx) scale(${user.size || 1})`,
zIndex: index < 2 ? index + 1 : (index < 4 ? index + 3 : index + 1)
}"
>
<!-- <view class="liked-users-area">
<view v-for="(user, index) in likedUsers" :key="index" class="liked-user-avatar" :style="{
transform: `translate(${user.ellipseX || 0}rpx, ${user.ellipseY || 0}rpx) scale(${user.size || 1})`,
zIndex: index < 2 ? index + 1 : (index < 4 ? index + 3 : index + 1)
}">
<image class="liked-user-image" :src="user.avatar" mode="aspectFill"></image>
</view>
</view>
@ -109,10 +108,11 @@
<text class="creator-title">TA们最近为你点过赞</text>
<view class="creator-card">
<text class="creator-tip">点击查看</text>
<image class="creator-preview" src="/static/rank/activity-support-icon/tubiao.png" mode="aspectFill"></image>
<image class="creator-preview" src="/static/rank/activity-support-icon/tubiao.png"
mode="aspectFill"></image>
</view>
</view>
</view>
</view> -->
<!-- </view> -->
<!-- 链上数据 -->
<view class="chain-section">
@ -133,16 +133,18 @@
<view class="chain-row">
<text class="chain-label">区块链编号</text>
<view class="chain-value-wrap">
<text class="chain-value">{{ showBlockNumber ? (assetData.block_number || '未知') : hiddenBlockNumber }}</text>
<text class="chain-value">{{ showBlockNumber ? (assetData.block_number || '未知') :
hiddenBlockNumber }}</text>
<view class="toggle-btn" @tap="showBlockNumber = !showBlockNumber">
<text class="toggle-icon">{{ showBlockNumber ? '👁' : '👁‍🗨' }}</text>
</view>
</view>
</view>
<view class="chain-row">
<text class="chain-label">交易哈希</text>
<text class="chain-label">链上哈希</text>
<view class="chain-value-wrap">
<text class="chain-value chain-hash" @longpress="copyHash">{{ showTxHash ? displayTxHash : hiddenTxHash }}</text>
<text class="chain-value chain-hash" @longpress="copyHash">{{ showTxHash ? displayTxHash
: hiddenTxHash }}</text>
<view class="toggle-btn" @tap="showTxHash = !showTxHash">
<text class="toggle-icon">{{ showTxHash ? '👁' : '👁‍🗨' }}</text>
</view>
@ -154,30 +156,19 @@
</scroll-view>
<!-- 点赞用户弹窗 -->
<LikeUsersModal
:visible="showLikeUsersModal"
:tabs="['今日', '历史']"
@close="showLikeUsersModal = false"
@tab-change="handleLikeUsersTabChange"
>
<LikeUsersModal :visible="showLikeUsersModal" :tabs="['今日', '历史']" @close="showLikeUsersModal = false"
@tab-change="handleLikeUsersTabChange">
<template #content>
<view class="like-users-list">
<view v-for="(user, index) in displayedLikeUsers" :key="index" class="like-user-item" >
<view v-for="(user, index) in displayedLikeUsers" :key="index" class="like-user-item">
<image class="like-user-avatar" :src="user.avatar" mode="aspectFill"></image>
<view class="like-user-info">
<text class="like-user-name">{{ user.nickname }}</text>
</view>
<!-- 拜访按钮 -->
<view
class="visit-button"
@tap="handleVisit"
>
<image
class="visit-icon"
src="/static/square/dianjibaifang.png"
mode="aspectFit"
lazy-load
></image>
<view class="visit-button" @tap="handleVisit">
<image class="visit-icon" src="/static/square/dianjibaifang.png" mode="aspectFit" lazy-load>
</image>
<text class="visit-text">点击拜访</text>
</view>
</view>
@ -208,6 +199,22 @@ const isLiked = ref(false);
const likeCount = ref(0);
const liking = ref(false);
//
const userAvatarUrl = ref('');
//
const loadCurrentUser = () => {
try {
const userStr = uni.getStorageSync('user');
if (userStr) {
const userInfo = JSON.parse(userStr);
userAvatarUrl.value = userInfo?.avatar_url || '';
}
} catch (e) {
console.error('解析用户信息失败:', e);
}
};
// 6
// avatar: , ellipseX: X, ellipseY: Y, size:
const likedUsers = ref([
@ -215,8 +222,8 @@ const likedUsers = ref([
{ avatar: '/static/sucai/image-02.png', ellipseX: 40, ellipseY: -40, size: 0.85 },
{ avatar: '/static/sucai/image-03.png', ellipseX: -96, ellipseY: 0, size: 0.9 },
{ avatar: '/static/sucai/image-04.png', ellipseX: 120, ellipseY: -40, size: 0.9 },
{ avatar: '/static/sucai/image-05.png', ellipseX: 64, ellipseY: 32},
{ avatar: '/static/sucai/image-06.png', ellipseX: -16, ellipseY: 32 ,size:1.15 }
{ avatar: '/static/sucai/image-05.png', ellipseX: 64, ellipseY: 32 },
{ avatar: '/static/sucai/image-06.png', ellipseX: -16, ellipseY: 32, size: 1.15 }
]);
//
@ -254,15 +261,48 @@ const showBlockNumber = ref(false);
const showTxHash = ref(false);
//
const remainSeconds = ref(0);
let countdownTimer = null;
const showCountdown = computed(() => remainSeconds.value > 0);
//
const countdowns = ref({});
//
const calculateRemainingTime = (item) => {
if (item.remainSeconds !== undefined) {
if (item.remainSeconds <= 0) {
return { hours: 0, minutes: 0, seconds: 0, expired: true };
}
const hours = Math.floor(item.remainSeconds / 3600);
const minutes = Math.floor((item.remainSeconds % 3600) / 60);
const seconds = Math.floor(item.remainSeconds % 60);
return { hours, minutes, seconds, expired: false };
}
if (item.exhibition_expire_at) {
const now = Date.now();
const expireTime = new Date(item.exhibition_expire_at).getTime();
const remaining = expireTime - now;
if (remaining <= 0) {
return { hours: 0, minutes: 0, seconds: 0, expired: true };
}
const hours = Math.floor(remaining / (60 * 60 * 1000));
const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000));
const seconds = Math.floor((remaining % (60 * 1000)) / 1000);
return { hours, minutes, seconds, expired: false };
}
return { hours: 0, minutes: 0, seconds: 0, expired: true };
};
//
const updateCountdowns = () => {
countdowns.value['currentAsset'] = calculateRemainingTime({ exhibition_expire_at: assetData.value.exhibition_expire_at });
};
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');
const countdown = countdowns.value['currentAsset'];
if (!countdown || countdown.expired) return '';
const h = String(countdown.hours).padStart(2, '0');
const m = String(countdown.minutes).padStart(2, '0');
const s = String(countdown.seconds).padStart(2, '0');
return `${h}:${m}:${s}`;
});
@ -280,10 +320,7 @@ const formattedAcquireTime = computed(() => {
//
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)}`;
return assetData.value.tx_hash || '未知';
});
// 6 + 6*
@ -346,8 +383,7 @@ const loadData = async () => {
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;
if (asset.exhibition_expire_at) {
startCountdown();
}
console.log(res.data)
@ -375,13 +411,10 @@ const loadData = async () => {
//
const startCountdown = () => {
updateCountdowns(); // 1
if (countdownTimer) clearInterval(countdownTimer);
countdownTimer = setInterval(() => {
if (remainSeconds.value > 0) {
remainSeconds.value--;
} else {
clearInterval(countdownTimer);
}
updateCountdowns();
}, 1000);
};
@ -432,6 +465,7 @@ onLoad((options) => {
assetIdParam.value = options?.asset_id || '';
orderIdParam.value = options?.order_id || '';
fromParam.value = options?.from || '';
loadCurrentUser();
});
onShow(() => {
@ -636,10 +670,12 @@ onUnmounted(() => {
}
.content-wrapper {
min-height: 90%;
padding: 104rpx 40rpx 80rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
gap: 0;
}
@ -715,14 +751,15 @@ onUnmounted(() => {
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
padding: 10rpx 28rpx;
backdrop-filter: blur(12rpx);
}
.like-area:active {
opacity: 0.75;
}
.like-icon, .heart-icon, .crystal-icon {
.like-icon,
.heart-icon,
.crystal-icon {
width: 44rpx;
height: 44rpx;
}
@ -730,9 +767,10 @@ onUnmounted(() => {
.like-num {
font-size: 32rpx;
font-weight: bold;
color: #e6e6e6;
color: #fff;
font-family: 'yt', sans-serif;
font-variant-numeric: tabular-nums;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
}
/* 收益 */
@ -754,22 +792,28 @@ onUnmounted(() => {
.earnings-text {
font-size: 32rpx;
font-weight: bold;
color: #e6e6e6;
color: #fff;
font-family: 'yt', sans-serif;
font-variant-numeric: tabular-nums;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
}
/* 倒计时 */
.countdown-area {
display: flex;
align-items: center;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-radius: 999rpx;
padding: 10rpx 28rpx;
}
.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);
font-size: 32rpx;
font-weight: bold;
color: #fff;
font-family: 'yt', sans-serif;
font-variant-numeric: tabular-nums;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
}
.countdown-val {
@ -778,6 +822,7 @@ onUnmounted(() => {
color: #fff;
font-family: 'yt', sans-serif;
font-variant-numeric: tabular-nums;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
}
/* 信息行(持有人 + 铸造时间) */
@ -804,8 +849,8 @@ onUnmounted(() => {
display: flex;
gap: 8rpx;
width: 416rpx;
justify-content: space-between;
align-items: center;
justify-content: space-between;
align-items: center;
}
.info-label {
@ -822,6 +867,20 @@ onUnmounted(() => {
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.7);
}
.info-avatar {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
margin-left: 8rpx;
}
.info-nickname {
display: flex;
align-items: center;
gap: 8rpx;
border-radius: 24rpx;
}
/* 点赞用户头像区域 */
.liked-users-area {
position: relative;
@ -841,7 +900,7 @@ onUnmounted(() => {
overflow: hidden;
}
.liked-user-image{
.liked-user-image {
width: 100%;
height: 100%;
}
@ -910,7 +969,6 @@ onUnmounted(() => {
padding: 30rpx 16rpx;
display: flex;
flex-direction: row;
align-items: center;
gap: 24rpx;
}
@ -943,7 +1001,6 @@ onUnmounted(() => {
font-size: 28rpx;
color: #ffffff;
font-family: 'yt', sans-serif;
text-align: right;
flex: 1;
word-break: break-all;
display: flex
@ -980,7 +1037,7 @@ onUnmounted(() => {
display: flex;
flex-direction: row;
align-items: center;
padding:0 16rpx;
padding: 0 16rpx;
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
@ -1034,9 +1091,9 @@ onUnmounted(() => {
margin-bottom: 8rpx;
/* transform: scale(1.2); */
}
.visit-text{
color: #FFFFFF;
font-size:16rpx;
}
.visit-text {
color: #FFFFFF;
font-size: 16rpx;
}
</style>

View File

@ -6,13 +6,12 @@
<view class="cards-container">
<view v-for="(card, index) in cardList" :key="index" class="card-item"
:class="{ 'card-selected': selectedIndex === index }" :style="getCardStyle(index)">
<view
class="card-frame"
<view class="card-frame"
:class="{ 'no-border': card.comingSoon, 'card-frame--tappable': !card.comingSoon }"
@tap.stop="onCardFrameTap(index)"
>
@tap.stop="onCardFrameTap(index)">
<image class="card-image" :src="card.image" mode="aspectFill" />
<image v-if="card.comingSoon" class="coming-soon-badge" src="/static/castlove/jinqingqidai.png" mode="aspectFit" />
<image v-if="card.comingSoon" class="coming-soon-badge" src="/static/castlove/jinqingqidai.png"
mode="aspectFit" />
</view>
</view>
</view>
@ -21,7 +20,8 @@
<view class="text-panel">
<!-- 向上按钮 -->
<view class="arrow-btn arrow-up" @click="scrollUp">
<image class="arrow-icon" src="/static/castlove/jiantou.png" mode="aspectFit" style="transform: rotate(180deg);" />
<image class="arrow-icon" src="/static/castlove/jiantou.png" mode="aspectFit"
style="transform: rotate(180deg);" />
</view>
<!-- 文字列表 -->
@ -65,7 +65,14 @@ export default {
},
data() {
return {
selectedIndex: 2, //
selectedIndex: 2,
// 便
cardRoutes: {
'光栅卡': '/pages/castlove/create',
'拍立得': '/pages/castlove/create',
'镭射卡': '/pages/castlove/create',
'撕拉片': '/pages/castlove/create',
},
cardList: [
{ name: '镭射卡', image: '/static/castlove/leisheka.png', comingSoon: false },
{ name: '拍立得', image: '/static/castlove/pailide.png', comingSoon: false },
@ -130,15 +137,24 @@ export default {
return;
}
const pos = this.getCardStackPosition(index);
const LENTICULAR_INDEX = 2;
const goLenticular =
this.selectedIndex === LENTICULAR_INDEX &&
(pos === 2 || pos === 1);
const targetName = goLenticular ? '光栅卡' : card.name;
if (pos === 2 || goLenticular) {
uni.navigateTo({
url: `/pages/castlove/create?name=${encodeURIComponent(targetName)}`,
});
//
//
if (pos === 2) {
if (card.name === '撕拉片') {
const route = this.cardRoutes[card.name];
if (route) {
uni.navigateTo({
url: '/pages/castlove/mint/tear-card',
});
}
} else {
const route = this.cardRoutes[card.name];
if (route) {
uni.navigateTo({
url: `${route}?name=${encodeURIComponent(card.name)}`,
});
}
}
return;
}
this.selectCard(index);
@ -164,9 +180,12 @@ export default {
uni.showToast({ title: '请选择已开放的工艺', icon: 'none' })
return
}
uni.navigateTo({
url: `/pages/castlove/create?name=${encodeURIComponent(card.name)}`
})
const route = this.cardRoutes[card.name]
if (route) {
uni.navigateTo({
url: `${route}?name=${encodeURIComponent(card.name)}`
})
}
}
}
}
@ -217,6 +236,7 @@ export default {
background-image: url('/static/square/cangpinkuang1.png');
background-size: cover;
box-shadow: 0 0 0 rgba(0, 0, 0, 0.5);
&.no-border {
background-image: none;
}

View File

@ -34,7 +34,7 @@
</view>
</view>
<view class="upload-box upload-box--half" @click="chooseLenticularSubject">
<image v-if="uploadedSubject" class="uploaded-image" :src="uploadedSubject" mode="aspectFit"></image>
<image v-if="uploadedSubject" class="uploaded-image":src="uploadedSubject" mode="aspectFit"></image>
<view v-else class="upload-placeholder">
<image class="upload-icon" src="/static/icon/add.png" mode="aspectFit"></image>
<text class="upload-text">主体图</text>

View File

@ -23,8 +23,7 @@
<view class="exhibition-grid">
<view v-for="(item, index) in exhibitionWorks" :key="item.id" class="exhibition-card"
:class="index % 2 === 0 ? 'card-tilt-left' : 'card-tilt-right'"
@tap="goToAssetDetail(item.id)">
:class="index % 2 === 0 ? 'card-tilt-left' : 'card-tilt-right'" @tap="goToAssetDetail(item.id)">
<image class="card-image" :src="item.cover_url || '/static/nft/placeholder.png'"
mode="aspectFill"></image>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
@ -36,6 +35,13 @@
<text class="card-rate-text">{{ item.like_count || 0 }}</text>
</view>
</view>
<!-- 倒计时背景 -->
<view class="countdown-background" :style="getCountdownBackgroundStyle()">
<!-- 倒计时文字 -->
<text class="countdown-text">
{{ formatCountdown(item.id) }}
</text>
</view>
<!-- 图片下方收益 -->
<view class="card-income-row"
:class="index % 2 === 0 ? 'income-tilt-right' : 'income-tilt-left'">
@ -80,13 +86,14 @@
<view v-for="(item, index) in likedWorks" :key="item.id" class="liked-row"
@tap="goToAssetDetail(item.id)">
<!-- 排名图标绝对定位在卡片左侧 -->
<image v-if="index < 3" :src="rankIcons[index]" :class="'rank-icon rank-icon-' + (index + 1)" mode="aspectFit"></image>
<image v-if="index < 3" :src="rankIcons[index]" :class="'rank-icon rank-icon-' + (index + 1)"
mode="aspectFit"></image>
<!-- 卡片主体 -->
<!-- 卡片主体 -->
<view class="liked-item" :class="index === 0 ? 'liked-item-first' : ''">
<!-- 作品封面 -->
<view class="liked-cover-wrap" :class="index === 0 ? 'liked-cover-wrap-first' : ''" >
<view class="liked-item" :class="index === 0 ? 'liked-item-first' : ''">
<!-- 作品封面 -->
<view class="liked-cover-wrap" :class="index === 0 ? 'liked-cover-wrap-first' : ''">
<image class="liked-cover" :src="item.cover_url || '/static/nft/placeholder.png'"
mode="aspectFill"></image>
<image class="liked-cover-frame" src="/static/square/cangpinkuang1.png"
@ -105,7 +112,9 @@
<!-- 右侧奖励 -->
<view class="liked-reward">
<image class="reward-token-icon" :src="item.earnings > 10 ? '/static/square/shuijingtubiao.png' : '/static/icon/crystal.png'" mode="aspectFit">
<image class="reward-token-icon"
:src="item.earnings > 10 ? '/static/square/shuijingtubiao.png' : '/static/icon/crystal.png'"
mode="aspectFit">
</image>
<text class="reward-amount">+{{ item.reward }}</text>
</view>
@ -123,7 +132,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ref, onMounted, onUnmounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { getUserGalleriesApi, getUserLikedAssetsApi, getUserProfileApi } from '@/utils/api.js';
@ -156,6 +165,68 @@ const formatScore = (score) => {
return Number(score).toLocaleString();
};
//
const calculateRemainingTime = (item) => {
if (item.remainSeconds !== undefined) {
if (item.remainSeconds <= 0) {
return { hours: 0, minutes: 0, seconds: 0, expired: true };
}
const hours = Math.floor(item.remainSeconds / 3600);
const minutes = Math.floor((item.remainSeconds % 3600) / 60);
const seconds = Math.floor(item.remainSeconds % 60);
return { hours, minutes, seconds, expired: false };
}
if (item.expire_at) {
const now = Date.now();
const expireTime = new Date(item.expire_at).getTime();
const remaining = expireTime - now;
if (remaining <= 0) {
return { hours: 0, minutes: 0, seconds: 0, expired: true };
}
const hours = Math.floor(remaining / (60 * 60 * 1000));
const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000));
const seconds = Math.floor((remaining % (60 * 1000)) / 1000);
return { hours, minutes, seconds, expired: false };
}
return { hours: 0, minutes: 0, seconds: 0, expired: true };
};
//
const updateCountdowns = () => {
exhibitionWorks.value.forEach(item => {
if (item && item.id) {
if (item.remainSeconds !== undefined && item.remainSeconds > 0) {
item.remainSeconds--;
}
countdowns.value[item.id] = calculateRemainingTime(item);
}
});
};
//
const formatCountdown = (itemId) => {
const countdown = countdowns.value[itemId];
if (!countdown || countdown.expired) return '00:00:00';
const h = String(countdown.hours).padStart(2, '0');
const m = String(countdown.minutes).padStart(2, '0');
const s = String(countdown.seconds).padStart(2, '0');
return `${h}:${m}:${s}`;
};
//
const getCountdownBackgroundStyle = () => {
return {
position: 'absolute',
bottom: '20rpx',
right: '40%',
transform: 'translateX(50%)',
width: '140rpx',
height: '36rpx',
zIndex: 9
};
};
const rankIcons = [
'/static/square/icon1.png',
'/static/square/icon2.png',
@ -165,6 +236,9 @@ const rankIcons = [
//
const exhibitionWorks = ref([]);
//
const countdowns = ref({});
//
const likedWorks = ref([]);
@ -236,6 +310,19 @@ const loadLikedAssets = async () => {
onMounted(() => {
loadExhibitedAssets();
loadLikedAssets();
//
countdownTimer = setInterval(() => {
updateCountdowns();
}, 1000);
});
let countdownTimer = null;
onUnmounted(() => {
if (countdownTimer) {
clearInterval(countdownTimer);
}
});
</script>
@ -400,8 +487,8 @@ onMounted(() => {
.card-rate-badge {
position: absolute;
bottom: 16rpx;
left: 40%;
top: 16rpx;
left: 25%;
transform: translateX(-50%);
display: flex;
align-items: center;
@ -476,6 +563,35 @@ onMounted(() => {
font-weight: 700;
}
/* 倒计时背景 */
.countdown-background {
/* position: absolute;
bottom: 20rpx;
right: 30%;
transform: translateX(-50%); */
width: 140rpx;
height: 36rpx;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-radius: 999rpx;
z-index: 9;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.4);
opacity: 0.95;
}
/* 倒计时文字 */
.countdown-text {
font-size: 22rpx;
font-weight: bold;
color: #fff;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8);
z-index: 10;
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum";
display: flex;
justify-content: center;
}
.empty-exhibition {
display: flex;
align-items: center;
@ -527,7 +643,7 @@ onMounted(() => {
flex-shrink: 0;
/* margin-right: 8rpx; */
position: relative;
left: 32rpx;
left: 32rpx;
}
.rank-icon-1 {

View File

@ -39,6 +39,13 @@
<text class="card-rate-text">{{ item.like_count || 0 }}</text>
</view>
</view>
<!-- 倒计时背景 -->
<view class="countdown-background" :style="getCountdownBackgroundStyle(index)">
<!-- 倒计时文字 -->
<text class="countdown-text">
{{ formatCountdown(item.id) }}
</text>
</view>
<!-- 图片下方收益 -->
<view class="card-income-row"
:class="index % 2 === 0 ? 'income-tilt-right' : 'income-tilt-left'">
@ -101,13 +108,14 @@
<view v-for="(item, index) in likedWorks" :key="item.id" class="liked-row"
@tap="goToAssetDetail(item.id)">
<!-- 排名图标绝对定位在卡片左侧 -->
<image v-if="index < 3" :src="rankIcons[index]" :class="'rank-icon rank-icon-' + (index + 1)" mode="aspectFit"></image>
<image v-if="index < 3" :src="rankIcons[index]" :class="'rank-icon rank-icon-' + (index + 1)"
mode="aspectFit"></image>
<!-- 卡片主体 -->
<view class="liked-item" :class="index === 0 ? 'liked-item-first' : ''">
<!-- 作品封面 -->
<view class="liked-cover-wrap" :class="index === 0 ? 'liked-cover-wrap-first' : ''" >
<view class="liked-cover-wrap" :class="index === 0 ? 'liked-cover-wrap-first' : ''">
<image class="liked-cover" :src="item.cover_url || '/static/nft/placeholder.png'"
mode="aspectFill"></image>
<image class="liked-cover-frame" src="/static/square/cangpinkuang1.png"
@ -126,7 +134,9 @@
<!-- 右侧奖励 -->
<view class="liked-reward">
<image class="reward-token-icon" :src="item.reward > 10 ? '/static/square/shuijingtubiao.png' : '/static/icon/crystal.png'" mode="aspectFit">
<image class="reward-token-icon"
:src="item.reward > 10 ? '/static/square/shuijingtubiao.png' : '/static/icon/crystal.png'"
mode="aspectFit">
</image>
<text class="reward-amount">+{{ item.reward }}</text>
</view>
@ -302,9 +312,73 @@ const formatScore = (score) => {
return Number(score).toLocaleString();
};
//
const calculateRemainingTime = (item) => {
if (item.remainSeconds !== undefined) {
if (item.remainSeconds <= 0) {
return { hours: 0, minutes: 0, seconds: 0, expired: true };
}
const hours = Math.floor(item.remainSeconds / 3600);
const minutes = Math.floor((item.remainSeconds % 3600) / 60);
const seconds = Math.floor(item.remainSeconds % 60);
return { hours, minutes, seconds, expired: false };
}
if (item.expire_at) {
const now = Date.now();
const expireTime = new Date(item.expire_at).getTime();
const remaining = expireTime - now;
if (remaining <= 0) {
return { hours: 0, minutes: 0, seconds: 0, expired: true };
}
const hours = Math.floor(remaining / (60 * 60 * 1000));
const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000));
const seconds = Math.floor((remaining % (60 * 1000)) / 1000);
return { hours, minutes, seconds, expired: false };
}
return { hours: 0, minutes: 0, seconds: 0, expired: true };
};
//
const updateCountdowns = () => {
exhibitionWorks.value.forEach(item => {
if (item && item.id) {
if (item.remainSeconds !== undefined && item.remainSeconds > 0) {
item.remainSeconds--;
}
countdowns.value[item.id] = calculateRemainingTime(item);
}
});
};
//
const formatCountdown = (itemId) => {
const countdown = countdowns.value[itemId];
if (!countdown || countdown.expired) return '00:00:00';
const h = String(countdown.hours).padStart(2, '0');
const m = String(countdown.minutes).padStart(2, '0');
const s = String(countdown.seconds).padStart(2, '0');
return `${h}:${m}:${s}`;
};
//
const getCountdownBackgroundStyle = () => {
return {
position: 'absolute',
bottom: '20rpx',
right: '40%',
transform: 'translateX(50%)',
width: '140rpx',
height: '36rpx',
zIndex: 9
};
};
//
const exhibitionWorks = ref([]);
//
const countdowns = ref({});
//
const likedWorks = ref([]);
@ -380,6 +454,11 @@ onMounted(() => {
loadExhibitedAssets();
loadLikedAssets();
//
countdownTimer = setInterval(() => {
updateCountdowns();
}, 1000);
//
uni.$on('userInfoUpdated', () => {
loadExhibitedAssets();
@ -387,7 +466,12 @@ onMounted(() => {
});
});
let countdownTimer = null;
onUnmounted(() => {
if (countdownTimer) {
clearInterval(countdownTimer);
}
uni.$off('userInfoUpdated');
});
@ -560,13 +644,13 @@ onShow(() => {
.card-tilt-left {
transform: rotate(-4deg) translateY(10rpx);
margin-right: 32rpx;
border-radius: 32rpx;
border-radius: 32rpx;
box-shadow: -16rpx 16rpx 16rpx rgba(229, 76, 93, 0.9);
}
.card-tilt-right {
transform: rotate(4deg) translateY(10rpx);
border-radius: 32rpx;
border-radius: 32rpx;
box-shadow: 16rpx 16rpx 16rpx rgba(229, 76, 93, 0.9);
}
@ -617,8 +701,8 @@ onShow(() => {
.card-rate-badge {
position: absolute;
bottom: 16rpx;
left: 40%;
top: 16rpx;
left: 25%;
transform: translateX(-50%);
display: flex;
align-items: center;
@ -697,6 +781,35 @@ onShow(() => {
font-weight: 700;
}
/* 倒计时背景 */
.countdown-background {
/* position: absolute; */
/* top: 20rpx;
left: 30%; */
/* transform: translateX(-50%); */
width: 140rpx;
height: 36rpx;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-radius: 999rpx;
z-index: 9;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.4);
opacity: 0.95;
}
/* 倒计时文字 */
.countdown-text {
font-size: 22rpx;
font-weight: bold;
color: #fff;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8);
z-index: 10;
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum";
display: flex;
justify-content: center;
}
/* 空状态 */
.empty-exhibition {
display: flex;

View File

@ -1521,7 +1521,7 @@ onShow(() => {
.asset-card {
background-image: url(/static/rank/activity-support-icon/beijingkuang.png);
background-size: 100% 100%;
background-size: 100% 125%;
background-position: center;
border-radius: 20rpx;
padding: 0 16rpx;

View File

@ -3,19 +3,19 @@
// 不需要手动注释!
// #ifdef H5
// const baseURL = 'http://localhost:8080' // H5 开发用本机
const baseURL = 'http://101.132.250.62:8080' // H5 开发用本机
const baseURL = 'http://192.168.110.60:8080' // H5 开发用本机
// const baseURL = 'http://101.132.250.62:8080' // H5 开发用本机
// #endif
// #ifdef APP-PLUS
// 开发调试手机和电脑同一WiFi时用这个改成你电脑IP
// 上线后:改成实际服务器地址
// const baseURL = 'http://192.168.110.60:8080'
const baseURL = 'http://192.168.110.60:8080'
// #endif
// 服务器地址(正式上线用)
// #ifdef APP-PLUS
const baseURL = 'http://101.132.250.62:8080'
// const baseURL = 'http://101.132.250.62:8080'
// #endif
// 是否使用模拟数据(开发调试时设为 true后端API准备好后改为 false
@ -468,7 +468,9 @@ export function getBatchOssPresignedUrlsApi(files, expires = 3600, type = 'asset
* @returns {Promise<{ imageUrl: string, orderId?: string, data: object }>}
*/
export function uploadLocalFileToOss(filePath, options = {}) {
const { type = 'asset', orderId = '', fileName } = options
const {
type = 'asset', orderId = '', fileName
} = options
const objectName = fileName || `${Date.now()}.jpg`
return new Promise((resolve, reject) => {
getOssSignatureApi(type, orderId)