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 @@
+
+
+
+
+ 分享藏品
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ID: {{ ownerNickname }}
+
+
+
+
+
+
+
+
+
+
+ TOPFANS,
+ 让热爱被发现
+
+ AI做周边,应援全免费
+
+
+
+
+
+
+
+ 保存图片
+
+
+
+ 微信
+
+
+
+
+
+
+
+
+
\ 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 @@
onLenticularSimulate(exhibitionAtSlot[0].id, x, y)" />
-
+
+
- 领取收益
+ 领取收益
+
@@ -71,8 +74,9 @@
onLenticularSimulate(exhibitionAtSlot[1].id, x, y)" />
@@ -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 {