feat:增加分享页面和举报按钮
This commit is contained in:
parent
222c2cc1cc
commit
fcebf5a107
@ -940,6 +940,7 @@ func (ctrl *SocialController) GetMyLikedAssets(c *gin.Context) {
|
||||
"earnings": item.Earnings,
|
||||
"hourly_earnings": item.HourlyEarnings,
|
||||
"is_lenticular": item.IsLenticular,
|
||||
"expire_at": item.ExpireAt,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -2350,6 +2350,7 @@ type LikedAssetItem struct {
|
||||
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"` // 每小时收益
|
||||
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
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -2440,6 +2441,13 @@ func (x *LikedAssetItem) GetIsLenticular() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *LikedAssetItem) GetExpireAt() int64 {
|
||||
if x != nil {
|
||||
return x.ExpireAt
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 获取我今日点赞的作品列表请求(暂不实现)
|
||||
type GetMyTodayLikedAssetsRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@ -2944,7 +2952,7 @@ const file_social_proto_rawDesc = "" +
|
||||
"\x04page\x18\x02 \x01(\x05R\x04page\x12\x1b\n" +
|
||||
"\tpage_size\x18\x03 \x01(\x05R\bpageSize\x12\x14\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" +
|
||||
"\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x12\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" +
|
||||
"\bearnings\x18\x06 \x01(\x03R\bearnings\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" +
|
||||
"\x04page\x18\x01 \x01(\x05R\x04page\x12\x1b\n" +
|
||||
"\tpage_size\x18\x02 \x01(\x05R\bpageSize\"\x86\x01\n" +
|
||||
|
||||
@ -294,6 +294,7 @@ message LikedAssetItem {
|
||||
int64 earnings = 6; // 当前可领取收益
|
||||
double hourly_earnings = 7; // 每小时收益
|
||||
bool is_lenticular = 8; // 是否为光栅卡
|
||||
int64 expire_at = 9; // 展示结束时间(毫秒时间戳)
|
||||
}
|
||||
|
||||
// 获取我今日点赞的作品列表请求(暂不实现)
|
||||
|
||||
@ -148,6 +148,7 @@ type LikedAssetInfo struct {
|
||||
Earnings int64
|
||||
HourlyEarnings float64
|
||||
IsLenticular bool // 是否为光栅卡
|
||||
ExpireAt int64 // 展出过期时间
|
||||
}
|
||||
|
||||
// RandomUserInfo 随机用户信息
|
||||
@ -610,12 +611,13 @@ func (r *socialRepositoryImpl) GetMyLikedAssets(userID, starID int64, page, page
|
||||
err := r.db.Model(&models.AssetLike{}).
|
||||
Select(`asset_likes.asset_id, a.name, a.cover_url, a.like_count,
|
||||
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 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("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).
|
||||
Limit(pageSize).
|
||||
Offset(offset).
|
||||
|
||||
@ -279,6 +279,7 @@ func (s *AssetLikeService) GetMyLikedAssets(ctx context.Context, req *pb.GetMyLi
|
||||
Earnings: item.Earnings,
|
||||
HourlyEarnings: item.HourlyEarnings,
|
||||
IsLenticular: item.IsLenticular,
|
||||
ExpireAt: item.ExpireAt,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,13 @@
|
||||
<view class="back-btn" @tap="handleBack">
|
||||
<image class="back-icon" src="/static/starbookcontent/tuichu.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="header-actions">
|
||||
<ShareReportButtons
|
||||
:ownerNickname="assetData.owner_nickname || ''"
|
||||
:assetId="assetIdParam"
|
||||
:coverUrl="coverUrl"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@ -76,6 +83,7 @@
|
||||
<!-- 倒计时(如有) -->
|
||||
<view v-if="assetData.display_status === 1 && countdownText" class="countdown-area">
|
||||
<view class="countdown-pill">
|
||||
<image class="crystal-icon" src="/static/assetDetail/time.png" mode="aspectFit"></image>
|
||||
<text class="countdown-val">{{ countdownText }}</text>
|
||||
</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 { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
|
||||
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 { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview.js';
|
||||
import {
|
||||
@ -369,6 +379,9 @@ async function exportCompositeImage() {
|
||||
// 弹窗相关
|
||||
const showLikeUsersModal = ref(false);
|
||||
const likeUsersActiveTab = ref(0);
|
||||
const showShareModal = ref(false);
|
||||
const shareCoverUrl = ref('');
|
||||
const shareQrcodeUrl = ref('');
|
||||
|
||||
// 今日/历史点赞用户数据(模拟)
|
||||
const todayLikeUsers = ref([
|
||||
@ -781,7 +794,11 @@ onUnmounted(() => {
|
||||
top: 16rpx;
|
||||
/* #endif */
|
||||
left: 32rpx;
|
||||
right: 32rpx;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: self-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
@ -799,6 +816,12 @@ onUnmounted(() => {
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
/* 加载/错误 */
|
||||
.loading-wrapper,
|
||||
.error-wrapper {
|
||||
@ -1071,7 +1094,6 @@ onUnmounted(() => {
|
||||
.like-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
background: linear-gradient(to bottom right,
|
||||
#F0E4B1 0%,
|
||||
#F08399 50%,
|
||||
@ -1092,6 +1114,7 @@ onUnmounted(() => {
|
||||
.crystal-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.like-num {
|
||||
@ -1107,7 +1130,6 @@ onUnmounted(() => {
|
||||
.earnings-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
background: linear-gradient(to bottom right,
|
||||
#F0E4B1 0%,
|
||||
#F08399 50%,
|
||||
@ -1144,6 +1166,7 @@ onUnmounted(() => {
|
||||
font-family: 'yt', sans-serif;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.countdown-val {
|
||||
|
||||
@ -41,7 +41,7 @@ const emit = defineEmits(['update:activeTab', 'update:isExpanded']);
|
||||
// 导航项配置
|
||||
const navItems = [
|
||||
{
|
||||
name: '搭子',
|
||||
name: 'AI搭子',
|
||||
icon: '/static/icon/dazi.png',
|
||||
angle: 122, // 左上方
|
||||
path: '/pages/ai-dazi/index'
|
||||
|
||||
320
frontend/pages/components/ShareModal.vue
Normal file
320
frontend/pages/components/ShareModal.vue
Normal 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>
|
||||
129
frontend/pages/components/ShareReportButtons.vue
Normal file
129
frontend/pages/components/ShareReportButtons.vue
Normal 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>
|
||||
@ -10,9 +10,9 @@
|
||||
</view>
|
||||
<!-- <text class="nav-title">我的作品</text> -->
|
||||
<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>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
|
||||
<view class="scroll-content">
|
||||
@ -29,16 +29,19 @@
|
||||
<view v-if="exhibitionAtSlot[0]" class="exhibition-card card-tilt-left"
|
||||
@tap="handleExhibitionCardTap(exhibitionAtSlot[0], 0)">
|
||||
<LenticularCard v-if="exhibitionAtSlot[0].is_lenticular" class="card-lenticular"
|
||||
:layers="getLenticularLayers(exhibitionAtSlot[0].id)" :transforms="getLenticularTransforms(exhibitionAtSlot[0].id)"
|
||||
:gyro-source="gyroSourceLabel" :skip-built-in-touch="false" :shimmer-mid-opacity="0.16"
|
||||
:layers="getLenticularLayers(exhibitionAtSlot[0].id)"
|
||||
: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)" />
|
||||
<image v-else class="card-image" :src="exhibitionAtSlot[0].cover_url || '/static/nft/placeholder.png'"
|
||||
mode="aspectFill"></image>
|
||||
<image v-else class="card-image"
|
||||
:src="exhibitionAtSlot[0].cover_url || '/static/nft/placeholder.png'" mode="aspectFill">
|
||||
</image>
|
||||
<!-- 领取收益按钮 -->
|
||||
<view class="claim-reward-btn" v-if="isRewardClaimable(exhibitionAtSlot[0].id)">
|
||||
<image class="claim-crystal-icon" src="/static/square/shuijingtubiao.png" mode="aspectFit">
|
||||
</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>
|
||||
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
|
||||
</image>
|
||||
@ -71,8 +74,9 @@
|
||||
<view v-if="exhibitionAtSlot[1]" class="exhibition-card card-tilt-right"
|
||||
@tap="handleExhibitionCardTap(exhibitionAtSlot[1], 1)">
|
||||
<LenticularCard v-if="exhibitionAtSlot[1].is_lenticular" class="card-lenticular"
|
||||
:layers="getLenticularLayers(exhibitionAtSlot[1].id)" :transforms="getLenticularTransforms(exhibitionAtSlot[1].id)"
|
||||
:gyro-source="gyroSourceLabel" :skip-built-in-touch="false" :shimmer-mid-opacity="0.16"
|
||||
:layers="getLenticularLayers(exhibitionAtSlot[1].id)"
|
||||
: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)" />
|
||||
<image v-else class="card-image"
|
||||
: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)">
|
||||
<image class="claim-crystal-icon" src="/static/square/shuijingtubiao.png" mode="aspectFit">
|
||||
</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>
|
||||
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
|
||||
</image>
|
||||
@ -168,12 +173,20 @@
|
||||
</view>
|
||||
|
||||
<!-- 右侧奖励 -->
|
||||
<view class="liked-reward">
|
||||
<image class="reward-token-icon"
|
||||
:src="item.reward > 10 ? '/static/square/shuijingtubiao.png' : '/static/icon/crystal.png'"
|
||||
mode="aspectFit">
|
||||
<view class="liked-reward" :class="{ 'reward-claimable': likedCountdowns[item.id]?.expired }" v-if="likedCountdowns[item.id]?.expired || (likedCountdowns[item.id] && showCountdownMode)">
|
||||
<view class="liked-reward-box">
|
||||
<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>
|
||||
<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>
|
||||
<text class="reward-amount">+{{ item.reward }}</text>
|
||||
<text class="liked-countdown">{{ formatLikedCountdown(item.id) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -501,6 +514,12 @@ const updateCountdowns = () => {
|
||||
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 countdowns = ref({});
|
||||
|
||||
// 点赞作品倒计时状态
|
||||
const likedCountdowns = ref({});
|
||||
|
||||
// 定时切换显示模式(奖励/倒计时)
|
||||
const showCountdownMode = ref(false);
|
||||
let displayModeTimer = null;
|
||||
|
||||
// 当前点赞作品列表
|
||||
const likedWorks = ref([]);
|
||||
|
||||
@ -805,6 +850,8 @@ const switchLikedTab = async (tab) => {
|
||||
// 清理点赞作品光栅卡数据
|
||||
likedLenticularLayersByAsset.value = {};
|
||||
likedLenticularTransformsMap.value = {};
|
||||
// 清理点赞作品倒计时
|
||||
likedCountdowns.value = {};
|
||||
await loadLikedAssets();
|
||||
};
|
||||
|
||||
@ -866,6 +913,7 @@ const loadLikedAssets = async () => {
|
||||
like_count: item.like_count,
|
||||
earnings: item.earnings,
|
||||
liked_at: item.liked_at,
|
||||
expire_at: item.expire_at,
|
||||
name: item.name,
|
||||
is_lenticular: item.is_lenticular ?? false,
|
||||
// 暂时用排名模拟状态文字
|
||||
@ -880,6 +928,9 @@ const loadLikedAssets = async () => {
|
||||
loadLikedLenticularLayersForAsset(item.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化点赞作品倒计时
|
||||
updateLikedCountdowns();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载点赞作品失败:', err);
|
||||
@ -898,6 +949,11 @@ onMounted(() => {
|
||||
updateCountdowns();
|
||||
}, 1000);
|
||||
|
||||
// 启动显示模式切换定时器(30秒切换一次)
|
||||
displayModeTimer = setInterval(() => {
|
||||
showCountdownMode.value = !showCountdownMode.value;
|
||||
}, 30000);
|
||||
|
||||
// 监听身份切换事件,切换后刷新数据
|
||||
uni.$on('userInfoUpdated', () => {
|
||||
loadExhibitedAssets();
|
||||
@ -911,6 +967,9 @@ onUnmounted(() => {
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
}
|
||||
if (displayModeTimer) {
|
||||
clearInterval(displayModeTimer);
|
||||
}
|
||||
stopLenticularRenderLoop();
|
||||
uni.$off('userInfoUpdated');
|
||||
uni.$off('assetLiked');
|
||||
@ -944,7 +1003,7 @@ onShow(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 80rpx 32rpx 16rpx;
|
||||
padding: 96rpx 32rpx 16rpx;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
@ -1390,8 +1449,6 @@ onShow(() => {
|
||||
background: #ffffff50;
|
||||
border-radius: 48rpx;
|
||||
padding: 24rpx 20rpx;
|
||||
gap: 16rpx;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
width: 80%;
|
||||
padding-left: 13%;
|
||||
@ -1538,17 +1595,40 @@ onShow(() => {
|
||||
|
||||
/* 右侧奖励 */
|
||||
.liked-reward {
|
||||
min-width: 160rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
/* gap: 8rpx; */
|
||||
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 {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.reward-amount {
|
||||
@ -1558,6 +1638,24 @@ onShow(() => {
|
||||
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 {
|
||||
padding: 60rpx 0;
|
||||
|
||||
@ -1299,7 +1299,7 @@ onShow(() => {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
position: relative;
|
||||
top: 32rpx;
|
||||
top: 96rpx;
|
||||
left: 32rpx;
|
||||
}
|
||||
|
||||
|
||||
@ -288,7 +288,9 @@ onUnmounted(() => {
|
||||
|
||||
.background-fixed {
|
||||
width: 300%;
|
||||
height: 100%;
|
||||
height: 110%;
|
||||
position: relative;
|
||||
bottom: 4rpx;
|
||||
}
|
||||
|
||||
.nav-mask {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user