From fcebf5a107131431da732b8f97a8d54cdcaf3827 Mon Sep 17 00:00:00 2001 From: zerosaturation Date: Thu, 21 May 2026 18:51:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E5=88=86=E4=BA=AB?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=92=8C=E4=B8=BE=E6=8A=A5=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gateway/controller/social_controller.go | 1 + backend/pkg/proto/social/social.pb.go | 13 +- backend/proto/social.proto | 1 + .../repository/social_repository.go | 6 +- .../service/asset_like_service.go | 1 + frontend/pages/asset-detail/asset-detail.vue | 27 +- frontend/pages/components/BottomNav.vue | 2 +- frontend/pages/components/ShareModal.vue | 320 ++++++++++++++++++ .../pages/components/ShareReportButtons.vue | 129 +++++++ frontend/pages/profile/myWorks.vue | 138 ++++++-- frontend/pages/profile/profile.vue | 2 +- frontend/pages/square/square.vue | 4 +- 12 files changed, 615 insertions(+), 29 deletions(-) create mode 100644 frontend/pages/components/ShareModal.vue create mode 100644 frontend/pages/components/ShareReportButtons.vue diff --git a/backend/gateway/controller/social_controller.go b/backend/gateway/controller/social_controller.go index 175f979..05415de 100644 --- a/backend/gateway/controller/social_controller.go +++ b/backend/gateway/controller/social_controller.go @@ -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, }) } diff --git a/backend/pkg/proto/social/social.pb.go b/backend/pkg/proto/social/social.pb.go index 1e0b7ce..6010ef3 100644 --- a/backend/pkg/proto/social/social.pb.go +++ b/backend/pkg/proto/social/social.pb.go @@ -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" + diff --git a/backend/proto/social.proto b/backend/proto/social.proto index e7f65b9..85f228c 100644 --- a/backend/proto/social.proto +++ b/backend/proto/social.proto @@ -294,6 +294,7 @@ message LikedAssetItem { int64 earnings = 6; // 当前可领取收益 double hourly_earnings = 7; // 每小时收益 bool is_lenticular = 8; // 是否为光栅卡 + int64 expire_at = 9; // 展示结束时间(毫秒时间戳) } // 获取我今日点赞的作品列表请求(暂不实现) diff --git a/backend/services/socialService/repository/social_repository.go b/backend/services/socialService/repository/social_repository.go index c9f2bc7..1501dfd 100644 --- a/backend/services/socialService/repository/social_repository.go +++ b/backend/services/socialService/repository/social_repository.go @@ -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). diff --git a/backend/services/socialService/service/asset_like_service.go b/backend/services/socialService/service/asset_like_service.go index 96365b6..7e68fd7 100644 --- a/backend/services/socialService/service/asset_like_service.go +++ b/backend/services/socialService/service/asset_like_service.go @@ -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, }) } diff --git a/frontend/pages/asset-detail/asset-detail.vue b/frontend/pages/asset-detail/asset-detail.vue index deda401..5a8132e 100644 --- a/frontend/pages/asset-detail/asset-detail.vue +++ b/frontend/pages/asset-detail/asset-detail.vue @@ -8,6 +8,13 @@ + + + @@ -76,6 +83,7 @@ + {{ countdownText }} @@ -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 { diff --git a/frontend/pages/components/BottomNav.vue b/frontend/pages/components/BottomNav.vue index feb3ad4..76a9109 100644 --- a/frontend/pages/components/BottomNav.vue +++ b/frontend/pages/components/BottomNav.vue @@ -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' diff --git a/frontend/pages/components/ShareModal.vue b/frontend/pages/components/ShareModal.vue new file mode 100644 index 0000000..2cc278e --- /dev/null +++ b/frontend/pages/components/ShareModal.vue @@ -0,0 +1,320 @@ + + + + + \ No newline at end of file diff --git a/frontend/pages/components/ShareReportButtons.vue b/frontend/pages/components/ShareReportButtons.vue new file mode 100644 index 0000000..3440e29 --- /dev/null +++ b/frontend/pages/components/ShareReportButtons.vue @@ -0,0 +1,129 @@ + + + + + \ No newline at end of file diff --git a/frontend/pages/profile/myWorks.vue b/frontend/pages/profile/myWorks.vue index 83ce687..bb65cdb 100644 --- a/frontend/pages/profile/myWorks.vue +++ b/frontend/pages/profile/myWorks.vue @@ -10,9 +10,9 @@ - + @@ -29,16 +29,19 @@ - + + - 领取收益 + 领取收益 + @@ -71,8 +74,9 @@ @@ -81,7 +85,8 @@ - 领取收益 + 领取收益 + @@ -168,12 +173,20 @@ - - + + + + + {{ item.reward }} + + 领取收益 + + + - +{{ item.reward }} + {{ formatLikedCountdown(item.id) }} @@ -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; diff --git a/frontend/pages/profile/profile.vue b/frontend/pages/profile/profile.vue index ab16228..ea676b5 100644 --- a/frontend/pages/profile/profile.vue +++ b/frontend/pages/profile/profile.vue @@ -1299,7 +1299,7 @@ onShow(() => { width: 80rpx; height: 80rpx; position: relative; - top: 32rpx; + top: 96rpx; left: 32rpx; } diff --git a/frontend/pages/square/square.vue b/frontend/pages/square/square.vue index 27607b8..57bd034 100644 --- a/frontend/pages/square/square.vue +++ b/frontend/pages/square/square.vue @@ -288,7 +288,9 @@ onUnmounted(() => { .background-fixed { width: 300%; - height: 100%; + height: 110%; + position: relative; + bottom: 4rpx; } .nav-mask {