feat:增加分享页面和举报按钮

This commit is contained in:
zerosaturation 2026-05-21 18:51:15 +08:00
parent 222c2cc1cc
commit fcebf5a107
12 changed files with 615 additions and 29 deletions

View File

@ -940,6 +940,7 @@ func (ctrl *SocialController) GetMyLikedAssets(c *gin.Context) {
"earnings": item.Earnings, "earnings": item.Earnings,
"hourly_earnings": item.HourlyEarnings, "hourly_earnings": item.HourlyEarnings,
"is_lenticular": item.IsLenticular, "is_lenticular": item.IsLenticular,
"expire_at": item.ExpireAt,
}) })
} }

View File

@ -2350,6 +2350,7 @@ type LikedAssetItem struct {
Earnings int64 `protobuf:"varint,6,opt,name=earnings,proto3" json:"earnings,omitempty"` // 当前可领取收益 Earnings int64 `protobuf:"varint,6,opt,name=earnings,proto3" json:"earnings,omitempty"` // 当前可领取收益
HourlyEarnings float64 `protobuf:"fixed64,7,opt,name=hourly_earnings,json=hourlyEarnings,proto3" json:"hourly_earnings,omitempty"` // 每小时收益 HourlyEarnings float64 `protobuf:"fixed64,7,opt,name=hourly_earnings,json=hourlyEarnings,proto3" json:"hourly_earnings,omitempty"` // 每小时收益
IsLenticular bool `protobuf:"varint,8,opt,name=is_lenticular,json=isLenticular,proto3" json:"is_lenticular,omitempty"` // 是否为光栅卡 IsLenticular bool `protobuf:"varint,8,opt,name=is_lenticular,json=isLenticular,proto3" json:"is_lenticular,omitempty"` // 是否为光栅卡
ExpireAt int64 `protobuf:"varint,9,opt,name=expire_at,json=expireAt,proto3" json:"expire_at,omitempty"` // 展示结束时间(毫秒时间戳)
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -2440,6 +2441,13 @@ func (x *LikedAssetItem) GetIsLenticular() bool {
return false return false
} }
func (x *LikedAssetItem) GetExpireAt() int64 {
if x != nil {
return x.ExpireAt
}
return 0
}
// 获取我今日点赞的作品列表请求(暂不实现) // 获取我今日点赞的作品列表请求(暂不实现)
type GetMyTodayLikedAssetsRequest struct { type GetMyTodayLikedAssetsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@ -2944,7 +2952,7 @@ const file_social_proto_rawDesc = "" +
"\x04page\x18\x02 \x01(\x05R\x04page\x12\x1b\n" + "\x04page\x18\x02 \x01(\x05R\x04page\x12\x1b\n" +
"\tpage_size\x18\x03 \x01(\x05R\bpageSize\x12\x14\n" + "\tpage_size\x18\x03 \x01(\x05R\bpageSize\x12\x14\n" +
"\x05total\x18\x04 \x01(\x03R\x05total\x12\x19\n" + "\x05total\x18\x04 \x01(\x03R\x05total\x12\x19\n" +
"\bhas_more\x18\x05 \x01(\bR\ahasMore\"\x80\x02\n" + "\bhas_more\x18\x05 \x01(\bR\ahasMore\"\x9d\x02\n" +
"\x0eLikedAssetItem\x12\x19\n" + "\x0eLikedAssetItem\x12\x19\n" +
"\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x12\n" + "\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" +
@ -2954,7 +2962,8 @@ const file_social_proto_rawDesc = "" +
"\bliked_at\x18\x05 \x01(\x03R\alikedAt\x12\x1a\n" + "\bliked_at\x18\x05 \x01(\x03R\alikedAt\x12\x1a\n" +
"\bearnings\x18\x06 \x01(\x03R\bearnings\x12'\n" + "\bearnings\x18\x06 \x01(\x03R\bearnings\x12'\n" +
"\x0fhourly_earnings\x18\a \x01(\x01R\x0ehourlyEarnings\x12#\n" + "\x0fhourly_earnings\x18\a \x01(\x01R\x0ehourlyEarnings\x12#\n" +
"\ris_lenticular\x18\b \x01(\bR\fisLenticular\"O\n" + "\ris_lenticular\x18\b \x01(\bR\fisLenticular\x12\x1b\n" +
"\texpire_at\x18\t \x01(\x03R\bexpireAt\"O\n" +
"\x1cGetMyTodayLikedAssetsRequest\x12\x12\n" + "\x1cGetMyTodayLikedAssetsRequest\x12\x12\n" +
"\x04page\x18\x01 \x01(\x05R\x04page\x12\x1b\n" + "\x04page\x18\x01 \x01(\x05R\x04page\x12\x1b\n" +
"\tpage_size\x18\x02 \x01(\x05R\bpageSize\"\x86\x01\n" + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\"\x86\x01\n" +

View File

@ -294,6 +294,7 @@ message LikedAssetItem {
int64 earnings = 6; // int64 earnings = 6; //
double hourly_earnings = 7; // double hourly_earnings = 7; //
bool is_lenticular = 8; // bool is_lenticular = 8; //
int64 expire_at = 9; //
} }
// //

View File

@ -148,6 +148,7 @@ type LikedAssetInfo struct {
Earnings int64 Earnings int64
HourlyEarnings float64 HourlyEarnings float64
IsLenticular bool // 是否为光栅卡 IsLenticular bool // 是否为光栅卡
ExpireAt int64 // 展出过期时间
} }
// RandomUserInfo 随机用户信息 // RandomUserInfo 随机用户信息
@ -610,12 +611,13 @@ func (r *socialRepositoryImpl) GetMyLikedAssets(userID, starID int64, page, page
err := r.db.Model(&models.AssetLike{}). err := r.db.Model(&models.AssetLike{}).
Select(`asset_likes.asset_id, a.name, a.cover_url, a.like_count, Select(`asset_likes.asset_id, a.name, a.cover_url, a.like_count,
asset_likes.created_at as liked_at, asset_likes.created_at as liked_at,
(a.tags @> '["craft:lenticular"]') as is_lenticular`). (a.tags @> '["craft:lenticular"]') as is_lenticular,
e.expire_at`).
Joins("JOIN assets a ON a.id = asset_likes.asset_id"). Joins("JOIN assets a ON a.id = asset_likes.asset_id").
Joins("JOIN exhibitions e ON e.id = asset_likes.exhibition_id AND e.deleted_at IS NULL"). Joins("JOIN exhibitions e ON e.id = asset_likes.exhibition_id AND e.deleted_at IS NULL").
Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID). Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID).
Where("a.deleted_at IS NULL AND a.is_active = ?", true). Where("a.deleted_at IS NULL AND a.is_active = ?", true).
Group("asset_likes.asset_id, a.name, a.cover_url, a.like_count, asset_likes.created_at, is_lenticular"). Group("asset_likes.asset_id, a.name, a.cover_url, a.like_count, asset_likes.created_at, is_lenticular, e.expire_at").
Order(orderClause). Order(orderClause).
Limit(pageSize). Limit(pageSize).
Offset(offset). Offset(offset).

View File

@ -279,6 +279,7 @@ func (s *AssetLikeService) GetMyLikedAssets(ctx context.Context, req *pb.GetMyLi
Earnings: item.Earnings, Earnings: item.Earnings,
HourlyEarnings: item.HourlyEarnings, HourlyEarnings: item.HourlyEarnings,
IsLenticular: item.IsLenticular, IsLenticular: item.IsLenticular,
ExpireAt: item.ExpireAt,
}) })
} }

View File

@ -8,6 +8,13 @@
<view class="back-btn" @tap="handleBack"> <view class="back-btn" @tap="handleBack">
<image class="back-icon" src="/static/starbookcontent/tuichu.png" mode="aspectFit"></image> <image class="back-icon" src="/static/starbookcontent/tuichu.png" mode="aspectFit"></image>
</view> </view>
<view class="header-actions">
<ShareReportButtons
:ownerNickname="assetData.owner_nickname || ''"
:assetId="assetIdParam"
:coverUrl="coverUrl"
/>
</view>
</view> </view>
@ -76,6 +83,7 @@
<!-- 倒计时如有 --> <!-- 倒计时如有 -->
<view v-if="assetData.display_status === 1 && countdownText" class="countdown-area"> <view v-if="assetData.display_status === 1 && countdownText" class="countdown-area">
<view class="countdown-pill"> <view class="countdown-pill">
<image class="crystal-icon" src="/static/assetDetail/time.png" mode="aspectFit"></image>
<text class="countdown-val">{{ countdownText }}</text> <text class="countdown-val">{{ countdownText }}</text>
</view> </view>
</view> </view>
@ -216,6 +224,8 @@ import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
import { getAssetDetailApi, getMintOrderDetailApi, likeAssetApi, unlikeAssetApi, getAssetMaterialsApi } from '@/utils/api.js'; import { getAssetDetailApi, getMintOrderDetailApi, likeAssetApi, unlikeAssetApi, getAssetMaterialsApi } from '@/utils/api.js';
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js'; import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
import LikeUsersModal from '@/pages/components/LikeUsersModal.vue'; import LikeUsersModal from '@/pages/components/LikeUsersModal.vue';
import ShareReportButtons from '@/pages/components/ShareReportButtons.vue';
import ShareModal from '@/pages/components/ShareModal.vue';
import LenticularCard from '@/components/lenticular/LenticularCard.vue'; import LenticularCard from '@/components/lenticular/LenticularCard.vue';
import { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview.js'; import { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview.js';
import { import {
@ -369,6 +379,9 @@ async function exportCompositeImage() {
// //
const showLikeUsersModal = ref(false); const showLikeUsersModal = ref(false);
const likeUsersActiveTab = ref(0); const likeUsersActiveTab = ref(0);
const showShareModal = ref(false);
const shareCoverUrl = ref('');
const shareQrcodeUrl = ref('');
// / // /
const todayLikeUsers = ref([ const todayLikeUsers = ref([
@ -781,7 +794,11 @@ onUnmounted(() => {
top: 16rpx; top: 16rpx;
/* #endif */ /* #endif */
left: 32rpx; left: 32rpx;
right: 32rpx;
z-index: 100; z-index: 100;
display: flex;
align-items: self-start;
justify-content: space-between;
} }
.back-btn { .back-btn {
@ -799,6 +816,12 @@ onUnmounted(() => {
height: 80rpx; height: 80rpx;
} }
.header-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
/* 加载/错误 */ /* 加载/错误 */
.loading-wrapper, .loading-wrapper,
.error-wrapper { .error-wrapper {
@ -1071,7 +1094,6 @@ onUnmounted(() => {
.like-area { .like-area {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10rpx;
background: linear-gradient(to bottom right, background: linear-gradient(to bottom right,
#F0E4B1 0%, #F0E4B1 0%,
#F08399 50%, #F08399 50%,
@ -1092,6 +1114,7 @@ onUnmounted(() => {
.crystal-icon { .crystal-icon {
width: 44rpx; width: 44rpx;
height: 44rpx; height: 44rpx;
margin-right: 10rpx;
} }
.like-num { .like-num {
@ -1107,7 +1130,6 @@ onUnmounted(() => {
.earnings-area { .earnings-area {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10rpx;
background: linear-gradient(to bottom right, background: linear-gradient(to bottom right,
#F0E4B1 0%, #F0E4B1 0%,
#F08399 50%, #F08399 50%,
@ -1144,6 +1166,7 @@ onUnmounted(() => {
font-family: 'yt', sans-serif; font-family: 'yt', sans-serif;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7); text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
display: flex;
} }
.countdown-val { .countdown-val {

View File

@ -41,7 +41,7 @@ const emit = defineEmits(['update:activeTab', 'update:isExpanded']);
// //
const navItems = [ const navItems = [
{ {
name: '搭子', name: 'AI搭子',
icon: '/static/icon/dazi.png', icon: '/static/icon/dazi.png',
angle: 122, // angle: 122, //
path: '/pages/ai-dazi/index' path: '/pages/ai-dazi/index'

View File

@ -0,0 +1,320 @@
<template>
<view class="share-modal" v-if="visible" @tap="handleClose">
<view class="modal-content" @tap.stop>
<!-- 标题 -->
<text class="modal-title">分享藏品</text>
<!-- 关闭按钮 -->
<view class="close-btn" @tap="handleClose">
<image class="close-icon" src="/static/starbookcontent/tuichu.png" mode="aspectFit"></image>
</view>
<!-- 图片区域 -->
<view class="share-image-wrapper">
<image class="share-image" :src="coverUrl" mode="aspectFill" @error="onImageError"></image>
<!-- 头像+ID 遮罩 -->
<view class="user-info-overlay">
<image class="user-avatar" :src="userAvatarUrl || '/static/square/gerenzhongxincangpinkuang.png'"
mode="aspectFill"></image>
<text class="user-id">ID: {{ ownerNickname }}</text>
</view>
<!-- 二维码 -->
<view class="qrcode-wrapper">
<image class="qrcode-image" :src="qrcodeUrl" mode="aspectFit" @error="onQrcodeError"></image>
</view>
<!-- 文字 -->
<view class="text-overlay">
<view class="brand-line">
<text class="brand-name">TOPFANS</text>
<text class="brand-slogan">让热爱被发现</text>
</view>
<text class="brand-desc">AI做周边应援全免费</text>
</view>
</view>
<!-- 保存图片 + 微信按钮 -->
<view class="action-buttons">
<view class="action-btn save-btn" @tap="handleSaveImage">
<image class="action-icon" src="/static/assetDetail/wenhao.png" mode="aspectFit"></image>
<text class="action-text">保存图片</text>
</view>
<view class="action-btn wechat-btn" @tap="handleShareToWeChat">
<image class="action-icon" src="/static/assetDetail/shangxiahuadong.png" mode="aspectFit"></image>
<text class="action-text">微信</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
visible: {
type: Boolean,
default: false
},
coverUrl: {
type: String,
default: ''
},
ownerNickname: {
type: String,
default: ''
},
qrcodeUrl: {
type: String,
default: ''
}
});
//
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);
}
};
loadCurrentUser();
const onImageError = (e) => {
console.log('图片加载失败', e);
};
const onQrcodeError = (e) => {
console.log('二维码加载失败', e);
};
//
const emit = defineEmits(['close']);
const handleClose = () => {
emit('close');
};
//
const handleSaveImage = () => {
if (!props.coverUrl) {
uni.showToast({ title: '图片加载中', icon: 'none' });
return;
}
uni.saveImageToPhotosAlbum({
filePath: props.coverUrl,
success: () => {
uni.showToast({ title: '保存成功', icon: 'success' });
},
fail: () => {
uni.showToast({ title: '保存失败', icon: 'none' });
}
});
};
//
const handleShareToWeChat = () => {
uni.share({
provider: 'weixin',
scene: 'WXSceneSession',
title: 'TOPFANS - 让热爱被发现',
summary: 'AI做周边应援全免费',
imageUrl: props.coverUrl,
success: () => {
uni.showToast({ title: '分享成功', icon: 'success' });
},
fail: () => {
uni.showToast({ title: '分享失败', icon: 'none' });
}
});
};
</script>
<style scoped>
.share-modal {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
position: relative;
top: 128rpx;
width: 80%;
display: flex;
flex-direction: column;
align-items: center;
gap: 40rpx;
}
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: #fff;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8);
margin-bottom: 16rpx;
}
.close-btn {
position: absolute;
top: -36rpx;
left: 0;
width: 64rpx;
height: 64rpx;
z-index: 10;
padding: 10rpx;
}
.close-icon {
width: 64rpx;
height: 64rpx;
}
.share-image-wrapper {
position: relative;
width: 100%;
border-radius: 24rpx;
overflow: hidden;
background: #1a1a2e;
min-height: 960rpx;
}
.share-image {
width: 100%;
height: 960rpx;
display: block;
background: rgba(255, 255, 255, 0.1);
}
.user-info-overlay {
position: absolute;
top: 58%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
gap: 12rpx;
}
.user-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
/* border: 4rpx solid #fff; */
background: rgba(255, 255, 255, 0.2);
}
.user-id {
font-size: 32rpx;
color: #fff;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8);
}
.qrcode-wrapper {
position: absolute;
bottom: 184rpx;
left: 50%;
transform: translateX(-50%);
width: 160rpx;
height: 160rpx;
background-image: url('@/static/rank/activity-support-icon/beijingkuang.png');
background-size: 105% 130%;
background-position: center;
}
.qrcode-image {
width: 144rpx;
height: 144rpx;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.text-overlay {
position: absolute;
bottom: 24rpx;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.brand-line {
display: flex;
align-items: center;
gap: 16rpx;
}
.brand-name {
font-size: 32rpx;
font-weight: bold;
color: #fff;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8);
}
.brand-slogan {
font-size: 40rpx;
font-weight: bold;
color: #fff;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8);
}
.brand-desc {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.9);
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.6);
}
.action-buttons {
display: flex;
gap: 32rpx;
width: 100%;
justify-content: center;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
padding: 16rpx 24rpx;
border-radius: 24rpx;
}
.action-btn:active {
opacity: 0.8;
}
.action-icon {
width: 64rpx;
height: 64rpx;
}
.action-text {
font-size: 28rpx;
color: #fff;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.5);
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<view class="share-report-btns">
<!-- 分享按钮 -->
<view class="action-btn share-btn" @tap="handleShare">
<image class="btn-icon" src="/static/assetDetail/fenxiang.png" mode="aspectFit"></image>
<text class="btn-text">分享</text>
</view>
<!-- 举报按钮 - 只在不是自己的资产时显示 -->
<view v-if="showReport" class="action-btn report-btn" @tap="handleReport">
<image class="btn-icon" src="/static/assetDetail/jubao.png" mode="aspectFit"></image>
<text class="btn-text">举报</text>
</view>
</view>
<!-- 分享弹窗 -->
<ShareModal
:visible="showShareModal"
:coverUrl="shareCoverUrl"
:ownerNickname="ownerNickname"
:qrcodeUrl="shareQrcodeUrl"
@close="showShareModal = false"
/>
</template>
<script setup>
import { ref, computed } from 'vue';
import ShareModal from './ShareModal.vue';
const props = defineProps({
//
ownerNickname: {
type: String,
default: ''
},
// ID
assetId: {
type: String,
default: ''
},
//
coverUrl: {
type: String,
default: ''
}
});
//
const currentNickname = computed(() => {
try {
const userStr = uni.getStorageSync('user');
if (userStr) {
const userInfo = JSON.parse(userStr);
return userInfo?.nickname || '';
}
} catch (e) {
console.error('获取用户信息失败:', e);
}
return '';
});
//
const showReport = computed(() => {
return props.ownerNickname && currentNickname.value && props.ownerNickname !== currentNickname.value;
});
//
const showShareModal = ref(false);
const shareCoverUrl = ref('');
const shareQrcodeUrl = ref('');
//
const handleShare = () => {
shareCoverUrl.value = props.coverUrl;
// TODO: URL API
shareQrcodeUrl.value = '/static/icon/qrcode-placeholder.png';
showShareModal.value = true;
};
//
const handleReport = () => {
uni.showModal({
title: '举报',
content: '确定要举报该藏品吗?',
success: (res) => {
if (res.confirm) {
// TODO: API
uni.showToast({ title: '举报成功', icon: 'success' });
}
}
});
};
</script>
<style scoped>
.share-report-btns {
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100rpx;
height: 100rpx;
border-radius: 24rpx;
gap: 8rpx;
}
.action-btn:active {
opacity: 0.7;
}
.btn-icon {
width: 56rpx;
height: 56rpx;
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.5));
}
.btn-text {
font-size: 24rpx;
color: #fff;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.6);
}
</style>

View File

@ -10,9 +10,9 @@
</view> </view>
<!-- <text class="nav-title">我的作品</text> --> <!-- <text class="nav-title">我的作品</text> -->
<view class="nav-placeholder"></view> <view class="nav-placeholder"></view>
<view class="nav-settings" @tap="goToSettings"> <!-- <view class="nav-settings" @tap="goToSettings">
<image class="nav-settings-icon" src="/static/icon/settings.png" mode="aspectFit"></image> <image class="nav-settings-icon" src="/static/icon/settings.png" mode="aspectFit"></image>
</view> </view> -->
</view> </view>
<view class="scroll-content"> <view class="scroll-content">
@ -29,16 +29,19 @@
<view v-if="exhibitionAtSlot[0]" class="exhibition-card card-tilt-left" <view v-if="exhibitionAtSlot[0]" class="exhibition-card card-tilt-left"
@tap="handleExhibitionCardTap(exhibitionAtSlot[0], 0)"> @tap="handleExhibitionCardTap(exhibitionAtSlot[0], 0)">
<LenticularCard v-if="exhibitionAtSlot[0].is_lenticular" class="card-lenticular" <LenticularCard v-if="exhibitionAtSlot[0].is_lenticular" class="card-lenticular"
:layers="getLenticularLayers(exhibitionAtSlot[0].id)" :transforms="getLenticularTransforms(exhibitionAtSlot[0].id)" :layers="getLenticularLayers(exhibitionAtSlot[0].id)"
:gyro-source="gyroSourceLabel" :skip-built-in-touch="false" :shimmer-mid-opacity="0.16" :transforms="getLenticularTransforms(exhibitionAtSlot[0].id)" :gyro-source="gyroSourceLabel"
:skip-built-in-touch="false" :shimmer-mid-opacity="0.16"
@simulate="(x, y) => onLenticularSimulate(exhibitionAtSlot[0].id, x, y)" /> @simulate="(x, y) => onLenticularSimulate(exhibitionAtSlot[0].id, x, y)" />
<image v-else class="card-image" :src="exhibitionAtSlot[0].cover_url || '/static/nft/placeholder.png'" <image v-else class="card-image"
mode="aspectFill"></image> :src="exhibitionAtSlot[0].cover_url || '/static/nft/placeholder.png'" mode="aspectFill">
</image>
<!-- 领取收益按钮 --> <!-- 领取收益按钮 -->
<view class="claim-reward-btn" v-if="isRewardClaimable(exhibitionAtSlot[0].id)"> <view class="claim-reward-btn" v-if="isRewardClaimable(exhibitionAtSlot[0].id)">
<image class="claim-crystal-icon" src="/static/square/shuijingtubiao.png" mode="aspectFit"> <image class="claim-crystal-icon" src="/static/square/shuijingtubiao.png" mode="aspectFit">
</image> </image>
<view @tap.stop="handleClaimReward(exhibitionAtSlot[0], 0)" class="claim-btn-text">领取收益</view> <view @tap.stop="handleClaimReward(exhibitionAtSlot[0], 0)" class="claim-btn-text">领取收益
</view>
</view> </view>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill"> <image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
</image> </image>
@ -71,8 +74,9 @@
<view v-if="exhibitionAtSlot[1]" class="exhibition-card card-tilt-right" <view v-if="exhibitionAtSlot[1]" class="exhibition-card card-tilt-right"
@tap="handleExhibitionCardTap(exhibitionAtSlot[1], 1)"> @tap="handleExhibitionCardTap(exhibitionAtSlot[1], 1)">
<LenticularCard v-if="exhibitionAtSlot[1].is_lenticular" class="card-lenticular" <LenticularCard v-if="exhibitionAtSlot[1].is_lenticular" class="card-lenticular"
:layers="getLenticularLayers(exhibitionAtSlot[1].id)" :transforms="getLenticularTransforms(exhibitionAtSlot[1].id)" :layers="getLenticularLayers(exhibitionAtSlot[1].id)"
:gyro-source="gyroSourceLabel" :skip-built-in-touch="false" :shimmer-mid-opacity="0.16" :transforms="getLenticularTransforms(exhibitionAtSlot[1].id)" :gyro-source="gyroSourceLabel"
:skip-built-in-touch="false" :shimmer-mid-opacity="0.16"
@simulate="(x, y) => onLenticularSimulate(exhibitionAtSlot[1].id, x, y)" /> @simulate="(x, y) => onLenticularSimulate(exhibitionAtSlot[1].id, x, y)" />
<image v-else class="card-image" <image v-else class="card-image"
:src="exhibitionAtSlot[1].cover_url || '/static/nft/placeholder.png'" mode="aspectFill"> :src="exhibitionAtSlot[1].cover_url || '/static/nft/placeholder.png'" mode="aspectFill">
@ -81,7 +85,8 @@
<view class="claim-reward-btn" v-if="isRewardClaimable(exhibitionAtSlot[1].id)"> <view class="claim-reward-btn" v-if="isRewardClaimable(exhibitionAtSlot[1].id)">
<image class="claim-crystal-icon" src="/static/square/shuijingtubiao.png" mode="aspectFit"> <image class="claim-crystal-icon" src="/static/square/shuijingtubiao.png" mode="aspectFit">
</image> </image>
<view @tap.stop="handleClaimReward(exhibitionAtSlot[1], 1)" class="claim-btn-text">领取收益</view> <view @tap.stop="handleClaimReward(exhibitionAtSlot[1], 1)" class="claim-btn-text">领取收益
</view>
</view> </view>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill"> <image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
</image> </image>
@ -168,12 +173,20 @@
</view> </view>
<!-- 右侧奖励 --> <!-- 右侧奖励 -->
<view class="liked-reward"> <view class="liked-reward" :class="{ 'reward-claimable': likedCountdowns[item.id]?.expired }" v-if="likedCountdowns[item.id]?.expired || (likedCountdowns[item.id] && showCountdownMode)">
<image class="reward-token-icon" <view class="liked-reward-box">
:src="item.reward > 10 ? '/static/square/shuijingtubiao.png' : '/static/icon/crystal.png'" <image class="reward-token-icon"
mode="aspectFit"> :src="item.reward > 10 ? '/static/square/shuijingtubiao.png' : '/static/icon/crystal.png'"
mode="aspectFit">
</image>
<text class="reward-amount">{{ item.reward }}</text>
</view>
<text v-if="likedCountdowns[item.id]?.expired" class="reward-text" @tap.stop="handleClaimReward(item)">领取收益</text>
</view>
<view class="liked-reward" v-else>
<image class="reward-token-icon" mode="aspectFit" src="/static/assetDetail/time.png">
</image> </image>
<text class="reward-amount">+{{ item.reward }}</text> <text class="liked-countdown">{{ formatLikedCountdown(item.id) }}</text>
</view> </view>
</view> </view>
</view> </view>
@ -501,6 +514,12 @@ const updateCountdowns = () => {
countdowns.value[item.id] = calculateRemainingTime(item); countdowns.value[item.id] = calculateRemainingTime(item);
} }
}); });
//
likedWorks.value.forEach(item => {
if (item && item.id) {
likedCountdowns.value[item.id] = calculateRemainingTime(item);
}
});
}; };
// //
@ -532,12 +551,38 @@ const getCountdownBackgroundStyle = () => {
}; };
}; };
//
const updateLikedCountdowns = () => {
likedWorks.value.forEach(item => {
if (item && item.id) {
likedCountdowns.value[item.id] = calculateRemainingTime(item);
}
});
};
//
const formatLikedCountdown = (itemId) => {
const countdown = likedCountdowns.value[itemId];
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}`;
};
// //
const exhibitionWorks = ref([]); const exhibitionWorks = ref([]);
// //
const countdowns = ref({}); const countdowns = ref({});
//
const likedCountdowns = ref({});
// /
const showCountdownMode = ref(false);
let displayModeTimer = null;
// //
const likedWorks = ref([]); const likedWorks = ref([]);
@ -805,6 +850,8 @@ const switchLikedTab = async (tab) => {
// //
likedLenticularLayersByAsset.value = {}; likedLenticularLayersByAsset.value = {};
likedLenticularTransformsMap.value = {}; likedLenticularTransformsMap.value = {};
//
likedCountdowns.value = {};
await loadLikedAssets(); await loadLikedAssets();
}; };
@ -866,6 +913,7 @@ const loadLikedAssets = async () => {
like_count: item.like_count, like_count: item.like_count,
earnings: item.earnings, earnings: item.earnings,
liked_at: item.liked_at, liked_at: item.liked_at,
expire_at: item.expire_at,
name: item.name, name: item.name,
is_lenticular: item.is_lenticular ?? false, is_lenticular: item.is_lenticular ?? false,
// //
@ -880,6 +928,9 @@ const loadLikedAssets = async () => {
loadLikedLenticularLayersForAsset(item.id); loadLikedLenticularLayersForAsset(item.id);
} }
} }
//
updateLikedCountdowns();
} }
} catch (err) { } catch (err) {
console.error('加载点赞作品失败:', err); console.error('加载点赞作品失败:', err);
@ -898,6 +949,11 @@ onMounted(() => {
updateCountdowns(); updateCountdowns();
}, 1000); }, 1000);
// 30
displayModeTimer = setInterval(() => {
showCountdownMode.value = !showCountdownMode.value;
}, 30000);
// //
uni.$on('userInfoUpdated', () => { uni.$on('userInfoUpdated', () => {
loadExhibitedAssets(); loadExhibitedAssets();
@ -911,6 +967,9 @@ onUnmounted(() => {
if (countdownTimer) { if (countdownTimer) {
clearInterval(countdownTimer); clearInterval(countdownTimer);
} }
if (displayModeTimer) {
clearInterval(displayModeTimer);
}
stopLenticularRenderLoop(); stopLenticularRenderLoop();
uni.$off('userInfoUpdated'); uni.$off('userInfoUpdated');
uni.$off('assetLiked'); uni.$off('assetLiked');
@ -944,7 +1003,7 @@ onShow(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 80rpx 32rpx 16rpx; padding: 96rpx 32rpx 16rpx;
} }
.nav-back { .nav-back {
@ -1390,8 +1449,6 @@ onShow(() => {
background: #ffffff50; background: #ffffff50;
border-radius: 48rpx; border-radius: 48rpx;
padding: 24rpx 20rpx; padding: 24rpx 20rpx;
gap: 16rpx;
overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
width: 80%; width: 80%;
padding-left: 13%; padding-left: 13%;
@ -1538,17 +1595,40 @@ onShow(() => {
/* 右侧奖励 */ /* 右侧奖励 */
.liked-reward { .liked-reward {
min-width: 160rpx;
padding: 8rpx 16rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 999rpx;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: center; /* gap: 8rpx; */
gap: 8rpx;
flex-shrink: 0; flex-shrink: 0;
} }
.liked-reward.reward-claimable {
flex-direction: column;
background: none;
}
.liked-reward.reward-claimable .liked-reward-box{
justify-content: center;
}
.liked-reward.reward-claimable .liked-reward-box .reward-amount {
color: #ff9500;
}
.liked-reward-box{
width: 100%;
display:flex;
align-items: center;
}
.reward-token-icon { .reward-token-icon {
width: 56rpx; width: 56rpx;
height: 56rpx; height: 56rpx;
margin-right: 8rpx;
} }
.reward-amount { .reward-amount {
@ -1558,6 +1638,24 @@ onShow(() => {
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.7); text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.7);
} }
.reward-text{
background:rgba(255, 255, 255, 0.3) ;
border-radius: 24rpx;
padding: 8rpx 20rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
font-size: 22rpx;
color: #fff;
}
.liked-countdown {
font-size: 22rpx;
color: #fff;
font-weight: 600;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.7);
}
/* 空状态 */ /* 空状态 */
.empty-liked { .empty-liked {
padding: 60rpx 0; padding: 60rpx 0;

View File

@ -1299,7 +1299,7 @@ onShow(() => {
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
position: relative; position: relative;
top: 32rpx; top: 96rpx;
left: 32rpx; left: 32rpx;
} }

View File

@ -288,7 +288,9 @@ onUnmounted(() => {
.background-fixed { .background-fixed {
width: 300%; width: 300%;
height: 100%; height: 110%;
position: relative;
bottom: 4rpx;
} }
.nav-mask { .nav-mask {