feat: 星册添加点赞数,修改样式

This commit is contained in:
zerosaturation 2026-05-08 20:20:52 +08:00
parent 86f32cd4df
commit b1c5bf13c8
22 changed files with 312 additions and 72 deletions

View File

@ -32,6 +32,11 @@ type GalleryRepository interface {
// 资产注册表相关 // 资产注册表相关
UpdateAssetRegistryDisplayStatus(assetID int64, displayStatus int32) error UpdateAssetRegistryDisplayStatus(assetID int64, displayStatus int32) error
// 事务性操作:创建展品并更新展示状态(原子操作)
PlaceExhibitionTx(exhibition *models.Exhibition, displayStatus int32) error
// 事务性操作:删除展品并更新展示状态(原子操作)
RemoveExhibitionTx(exhibitionID int64, assetID int64) error
// ========== 我的作品相关 ========== // ========== 我的作品相关 ==========
// GetMyExhibitedAssets 获取我展出的作品列表(只返回展出中且未过期的,含收益) // GetMyExhibitedAssets 获取我展出的作品列表(只返回展出中且未过期的,含收益)
@ -282,6 +287,59 @@ func (r *galleryRepository) UpdateAssetRegistryDisplayStatus(assetID int64, disp
Update("display_status", displayStatus).Error Update("display_status", displayStatus).Error
} }
// PlaceExhibitionTx 事务性创建展品并更新展示状态(原子操作)
func (r *galleryRepository) PlaceExhibitionTx(exhibition *models.Exhibition, displayStatus int32) error {
now := time.Now().UnixMilli()
exhibition.CreatedAt = now
exhibition.UpdatedAt = now
exhibition.DeletedAt = nil
return r.db.Transaction(func(tx *gorm.DB) error {
// 1. 物理删除已软删除的旧记录
if err := tx.Exec(`
DELETE FROM exhibitions
WHERE asset_id = ? AND deleted_at IS NOT NULL
`, exhibition.AssetID).Error; err != nil {
return err
}
// 2. 插入新记录
if err := tx.Create(exhibition).Error; err != nil {
return err
}
// 3. 更新展示状态(与展出操作在同一事务中)
if err := tx.Model(&models.AssetRegistry{}).
Where("asset_id = ?", exhibition.AssetID).
Update("display_status", displayStatus).Error; err != nil {
return err
}
return nil
})
}
// RemoveExhibitionTx 事务性删除展品并更新展示状态(原子操作)
func (r *galleryRepository) RemoveExhibitionTx(exhibitionID int64, assetID int64) error {
now := time.Now().UnixMilli()
return r.db.Transaction(func(tx *gorm.DB) error {
// 1. 软删除展品记录
if err := tx.Model(&models.Exhibition{}).
Where("id = ?", exhibitionID).
Updates(map[string]interface{}{
"deleted_at": now,
"updated_at": now,
}).Error; err != nil {
return err
}
// 2. 更新展示状态为未展示(与删除操作在同一事务中)
if err := tx.Model(&models.AssetRegistry{}).
Where("asset_id = ?", assetID).
Update("display_status", int32(0)).Error; err != nil {
return err
}
return nil
})
}
// ========== 我的作品相关实现 ========== // ========== 我的作品相关实现 ==========
// GetMyExhibitedAssets 获取我展出的作品列表(只返回展出中且未过期的,含收益) // GetMyExhibitedAssets 获取我展出的作品列表(只返回展出中且未过期的,含收益)
@ -304,15 +362,16 @@ func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pag
err = r.db.Model(&models.Exhibition{}). err = r.db.Model(&models.Exhibition{}).
Raw(` Raw(`
SELECT exhibitions.asset_id, a.name, a.cover_url, a.like_count, SELECT exhibitions.asset_id, a.name, a.cover_url, a.like_count,
exhibitions.start_time as exhibited_at, exhibitions.expire_at, exhibitions.start_time as exhibited_at, exhibitions.expire_at, bs.slot_index,
COALESCE(CAST(SUM(err.crystal_amount) / 10 AS bigint), 0) as earnings COALESCE(CAST(SUM(err.crystal_amount) / 10 AS bigint), 0) as earnings
FROM exhibitions FROM exhibitions
JOIN assets a ON a.id = exhibitions.asset_id JOIN assets a ON a.id = exhibitions.asset_id
JOIN booth_slots bs ON bs.slot_id = exhibitions.slot_id
LEFT JOIN exhibition_revenue_records err ON err.asset_id = a.id AND err.status = 'claimable' LEFT JOIN exhibition_revenue_records err ON err.asset_id = a.id AND err.status = 'claimable'
WHERE exhibitions.occupier_uid = ? AND exhibitions.occupier_star_id = ? WHERE exhibitions.occupier_uid = ? AND exhibitions.occupier_star_id = ?
AND exhibitions.deleted_at IS NULL AND exhibitions.expire_at > ? AND exhibitions.deleted_at IS NULL AND exhibitions.expire_at > ?
GROUP BY exhibitions.asset_id, a.name, a.cover_url, a.like_count, exhibitions.start_time, exhibitions.expire_at GROUP BY exhibitions.asset_id, a.name, a.cover_url, a.like_count, exhibitions.start_time, exhibitions.expire_at, bs.slot_index
ORDER BY exhibitions.start_time DESC ORDER BY bs.slot_index ASC
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
`, userID, starID, now, pageSize, offset).Scan(&items).Error `, userID, starID, now, pageSize, offset).Scan(&items).Error
@ -343,15 +402,16 @@ func (r *galleryRepository) GetUserExhibitedAssets(userID, starID int64, page, p
err = r.db.Model(&models.Exhibition{}). err = r.db.Model(&models.Exhibition{}).
Raw(` Raw(`
SELECT exhibitions.asset_id, a.name, a.cover_url, a.like_count, SELECT exhibitions.asset_id, a.name, a.cover_url, a.like_count,
exhibitions.start_time as exhibited_at, exhibitions.expire_at, exhibitions.start_time as exhibited_at, exhibitions.expire_at, bs.slot_index,
COALESCE(CAST(SUM(err.crystal_amount) / 10 AS bigint), 0) as earnings COALESCE(CAST(SUM(err.crystal_amount) / 10 AS bigint), 0) as earnings
FROM exhibitions FROM exhibitions
JOIN assets a ON a.id = exhibitions.asset_id JOIN assets a ON a.id = exhibitions.asset_id
JOIN booth_slots bs ON bs.slot_id = exhibitions.slot_id
LEFT JOIN exhibition_revenue_records err ON err.asset_id = a.id AND err.status = 'claimable' LEFT JOIN exhibition_revenue_records err ON err.asset_id = a.id AND err.status = 'claimable'
WHERE exhibitions.occupier_uid = ? AND exhibitions.occupier_star_id = ? WHERE exhibitions.occupier_uid = ? AND exhibitions.occupier_star_id = ?
AND exhibitions.deleted_at IS NULL AND exhibitions.expire_at > ? AND exhibitions.deleted_at IS NULL AND exhibitions.expire_at > ?
GROUP BY exhibitions.asset_id, a.name, a.cover_url, a.like_count, exhibitions.start_time, exhibitions.expire_at GROUP BY exhibitions.asset_id, a.name, a.cover_url, a.like_count, exhibitions.start_time, exhibitions.expire_at, bs.slot_index
ORDER BY exhibitions.start_time DESC ORDER BY bs.slot_index ASC
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
`, userID, starID, now, pageSize, offset).Scan(&items).Error `, userID, starID, now, pageSize, offset).Scan(&items).Error

View File

@ -276,6 +276,7 @@ func (s *starbookService) buildAssetItemsFromRegistries(registries []*models.Ass
var assetCoverMap map[int64]string // assetID -> coverURL var assetCoverMap map[int64]string // assetID -> coverURL
var assetNameMap map[int64]string // assetID -> name var assetNameMap map[int64]string // assetID -> name
var categoryMap map[int64]string // assetID -> category var categoryMap map[int64]string // assetID -> category
var assetLikeCountMap map[int64]int32 // assetID -> likeCount
switch assetType { switch assetType {
case models.AssetTypeRegular: case models.AssetTypeRegular:
@ -283,9 +284,11 @@ func (s *starbookService) buildAssetItemsFromRegistries(registries []*models.Ass
if err == nil && len(assets) > 0 { if err == nil && len(assets) > 0 {
assetCoverMap = make(map[int64]string) assetCoverMap = make(map[int64]string)
assetNameMap = make(map[int64]string) assetNameMap = make(map[int64]string)
assetLikeCountMap = make(map[int64]int32)
for _, asset := range assets { for _, asset := range assets {
assetCoverMap[asset.ID] = asset.CoverURL assetCoverMap[asset.ID] = asset.CoverURL
assetNameMap[asset.ID] = asset.Name assetNameMap[asset.ID] = asset.Name
assetLikeCountMap[asset.ID] = asset.LikeCount
} }
} }
case models.AssetTypeCollection: case models.AssetTypeCollection:
@ -345,6 +348,13 @@ func (s *starbookService) buildAssetItemsFromRegistries(registries []*models.Ass
item.Category = cat item.Category = cat
} }
// 从 assets 表获取点赞数regular 类型)
if assetType == models.AssetTypeRegular {
if likeCount, ok := assetLikeCountMap[reg.AssetID]; ok {
item.LikeCount = likeCount
}
}
items = append(items, item) items = append(items, item)
} }

View File

@ -55,11 +55,11 @@
> >
<image <image
class="visit-icon" class="visit-icon"
src="/static/icon/visit-house.png" src="/static/square/dianjibaifang.png"
mode="aspectFit" mode="aspectFit"
lazy-load lazy-load
></image> ></image>
<text class="visit-text">拜访小屋</text> <text class="visit-text">点击拜访</text>
</view> </view>
</view> </view>
@ -71,8 +71,8 @@
class="menu-item" class="menu-item"
@tap="handleMenuVisit" @tap="handleMenuVisit"
> >
<image class="menu-icon" src="/static/icon/visit-house.png" mode="aspectFit" lazy-load></image> <image class="menu-icon" src="/static/square/dianjibaifang.png" mode="aspectFit" lazy-load></image>
<text class="menu-text">拜访小屋</text> <text class="menu-text">点击拜访</text>
</view> </view>
<view class="menu-item" @tap="handleMenuViewProfile"> <view class="menu-item" @tap="handleMenuViewProfile">
<image class="menu-icon" src="/static/icon/character.png" mode="aspectFit" lazy-load></image> <image class="menu-icon" src="/static/icon/character.png" mode="aspectFit" lazy-load></image>
@ -207,14 +207,14 @@ const handleArtworkError = (e) => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 32rpx; margin-bottom: 24rpx;
gap: 20rpx; gap: 20rpx;
position:relative; position:relative;
} }
.rank-decoration { .rank-decoration {
position: absolute; position: absolute;
bottom: -24rpx; bottom: -8rpx;
left: 32rpx; left: 32rpx;
width: 92%; width: 92%;
height: 8rpx; height: 8rpx;
@ -225,14 +225,14 @@ const handleArtworkError = (e) => {
/* 排名编号 */ /* 排名编号 */
.rank-number { .rank-number {
min-width: 48rpx; min-width: 64rpx;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 8rpx; padding: 8rpx;
} }
.rank-text { .rank-text {
font-size: 48rpx; font-size: 32rpx;
font-weight: bold; font-weight: bold;
color: #FFFFFF; color: #FFFFFF;
text-shadow: text-shadow:
@ -269,8 +269,8 @@ const handleArtworkError = (e) => {
/* 作品图片 */ /* 作品图片 */
.artwork-image { .artwork-image {
width: 96rpx; width: 80rpx;
height: 120rpx; height: 104rpx;
border-radius: 16rpx; border-radius: 16rpx;
border: 3rpx solid rgba(255, 255, 255, 0.6); border: 3rpx solid rgba(255, 255, 255, 0.6);
/* 优化2层阴影简化为1层 */ /* 优化2层阴影简化为1层 */
@ -359,6 +359,7 @@ const handleArtworkError = (e) => {
} }
.popularity-container { .popularity-container {
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20rpx; gap: 20rpx;
@ -373,12 +374,16 @@ const handleArtworkError = (e) => {
} }
.fire-icon { .fire-icon {
width: 32rpx; width: 64rpx;
height: 40rpx; height: 72rpx;
position: absolute;
left: -32rpx;
top: -32rpx;
} }
.popularity-score { .popularity-score {
font-size: 24rpx; font-size: 24rpx;
padding: 0 16rpx 0 32rpx;
color: #FFFFFF; color: #FFFFFF;
text-shadow: text-shadow:
0 2rpx 4rpx rgba(0, 0, 0, 0.4), 0 2rpx 4rpx rgba(0, 0, 0, 0.4),
@ -401,7 +406,8 @@ const handleArtworkError = (e) => {
.visit-icon { .visit-icon {
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
transform: scale(1.2); margin-bottom: 8rpx;
/* transform: scale(1.2); */
} }
.visit-text{ .visit-text{
color: #FFFFFF; color: #FFFFFF;

View File

@ -1295,15 +1295,12 @@ const handleTouchEnd = (e) => {
.modal-container { .modal-container {
position: absolute; position: absolute;
width: 100%; width: calc(100% - 16rpx);
height: calc(100% - 224rpx); height: calc(100% - 224rpx);
bottom: 32rpx; bottom: 32rpx;
border-radius: 48rpx; border-radius: 48rpx;
overflow: hidden; overflow: hidden;
z-index: 10; z-index: 10;
/* 优化3层阴影简化为1层提升渲染性能 */
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.35);
border: 2rpx solid rgba(255, 255, 255, 0.2);
/* 添加过渡动画,使下拉关闭更流畅 */ /* 添加过渡动画,使下拉关闭更流畅 */
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
} }
@ -1319,9 +1316,10 @@ const handleTouchEnd = (e) => {
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
width: 100%; /* width: 100%;
height: 100%; height: 100%; */
background-image: url('/static/rank/rank-bg.png'); background-image: url('/static/rank/paihangbang.png');
background-size: 100% 100%;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
z-index: 0; z-index: 0;
@ -1356,6 +1354,7 @@ const handleTouchEnd = (e) => {
margin-bottom: 40rpx; margin-bottom: 40rpx;
padding: 0 15rpx; padding: 0 15rpx;
position: relative; position: relative;
top: 16rpx;
} }
.nav-arrow { .nav-arrow {
@ -1405,7 +1404,7 @@ const handleTouchEnd = (e) => {
.ranking-title-image.loaded { .ranking-title-image.loaded {
opacity: 1; opacity: 1;
transform: scale(2.5); transform: scale(2.3);
} }
@ -1625,7 +1624,7 @@ const handleTouchEnd = (e) => {
justify-content: space-between; justify-content: space-between;
gap: 60rpx; gap: 60rpx;
margin-bottom: 84rpx; margin-bottom: 84rpx;
padding: 0 15rpx; padding: 0 48rpx;
} }
/* 空数据提示 */ /* 空数据提示 */
@ -1652,7 +1651,7 @@ const handleTouchEnd = (e) => {
/* 排名列表区域 */ /* 排名列表区域 */
.ranking-list-section { .ranking-list-section {
margin-top: 20rpx; margin-top: 20rpx;
padding: 0 15rpx; padding: 0 48rpx;
} }
/* 加载更多容器 */ /* 加载更多容器 */
@ -1702,9 +1701,9 @@ const handleTouchEnd = (e) => {
/* 当前用户栏 - 固定在底部 */ /* 当前用户栏 - 固定在底部 */
.current-user-bar { .current-user-bar {
position: absolute; position: absolute;
bottom: 148rpx; bottom: 152rpx;
left: 20rpx; left: 84rpx;
right: 20rpx; right: 84rpx;
padding: 24rpx; padding: 24rpx;
/* 优化5色渐变简化为2色提升性能 */ /* 优化5色渐变简化为2色提升性能 */
background: linear-gradient(135deg, rgba(255, 107, 157, 0.9) 0%, rgba(255, 177, 153, 0.9) 100%); background: linear-gradient(135deg, rgba(255, 107, 157, 0.9) 0%, rgba(255, 177, 153, 0.9) 100%);
@ -1733,11 +1732,12 @@ const handleTouchEnd = (e) => {
/* gap: 34rpx; */ /* gap: 34rpx; */
position: relative; position: relative;
z-index: 1; z-index: 1;
height: 64rpx;
} }
.current-user-avatar { .current-user-avatar {
width: 88rpx; width: 88rpx;
height: 88rpx; height: 64rpx;
border-radius: 50%; border-radius: 50%;
border: 4rpx solid rgba(255, 255, 255, 0.9); border: 4rpx solid rgba(255, 255, 255, 0.9);
box-shadow: box-shadow:
@ -1746,7 +1746,7 @@ const handleTouchEnd = (e) => {
} }
.current-user-avatar.no-artwork { .current-user-avatar.no-artwork {
width: 64rpx; width: 56rpx;
border-radius: 0; border-radius: 0;
} }
@ -2103,7 +2103,7 @@ const handleTouchEnd = (e) => {
.nav-arrow { .nav-arrow {
min-width: 88rpx; min-width: 88rpx;
transform: scale(2) transform: scale(1.7)
} }
.tab-item { .tab-item {

View File

@ -1,7 +1,7 @@
<template> <template>
<view class="starbook-content"> <view class="starbook-content">
<!-- 背景图片 --> <!-- 背景图片 -->
<image class="background-image" src="/static/background/starbook.jpg" mode="aspectFill"></image> <image class="background-image" src="/static/starbookcontent/beijing.png" mode="aspectFill"></image>
<!-- 内容区域 --> <!-- 内容区域 -->
<view class="content-wrapper"> <view class="content-wrapper">
@ -53,6 +53,11 @@
<!-- category 下的所有 grades --> <!-- category 下的所有 grades -->
<view v-for="gradeItem in group.grades" :key="gradeItem.grade" class="grade-section"> <view v-for="gradeItem in group.grades" :key="gradeItem.grade" class="grade-section">
<view class="group-header"> <view class="group-header">
<image
class="group-header-bg"
:src="getGradeBackground(gradeItem.grade)"
mode="aspectFill"
></image>
<text class="group-title">{{ formatGrade(gradeItem.grade) }}</text> <text class="group-title">{{ formatGrade(gradeItem.grade) }}</text>
</view> </view>
<scroll-view class="nft-row" scroll-x :show-scrollbar="false" :enable-flex="true"> <scroll-view class="nft-row" scroll-x :show-scrollbar="false" :enable-flex="true">
@ -73,8 +78,9 @@
<view v-if="item.display_status === 1" class="status-overlay"> <view v-if="item.display_status === 1" class="status-overlay">
<text class="status-text-center">已展示</text> <text class="status-text-center">已展示</text>
</view> </view>
<view class="nft-info"> <view class="card-rate-badge-overlay">
<text class="nft-name">{{ item.name }}</text> <image class="heart-icon" src="/static/icon/heart-icon.png" mode="aspectFit"></image>
<text class="card-rate-text">{{ item.like_count || 0 }}</text>
</view> </view>
</view> </view>
<!-- 更多按钮 --> <!-- 更多按钮 -->
@ -213,10 +219,20 @@ const cardSize = computed(() => {
return Math.floor(availableWidth / 2.5); return Math.floor(availableWidth / 2.5);
}); });
// grade // grade
const gradeMap = { 1: '一', 2: '二', 3: '三', 4: '四', 5: '五' };
function formatGrade(grade) { function formatGrade(grade) {
return `等级${gradeMap[grade] || grade}`; return `V${grade}`;
}
//
function getGradeBackground(grade) {
if (grade <= 2) {
return '/static/starbookcontent/V1dengji.png';
} else if (grade <= 4) {
return '/static/starbookcontent/V2dengji.png';
} else {
return '/static/starbookcontent/V3dengji.png';
}
} }
// //
@ -495,14 +511,31 @@ watch(() => props.isActive, (newVal) => {
/* 分组标题 */ /* 分组标题 */
.group-header { .group-header {
position: relative;
margin-bottom: 16rpx; margin-bottom: 16rpx;
padding-bottom: 10rpx; padding: 16rpx 24rpx;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.1); display: inline-flex;
align-items: center;
justify-content: center;
width: 160rpx;
height: 32rpx;
}
.group-header-bg {
position: absolute;
top: 0;
left: 0;
width: 160rpx;
height: 64rpx;
z-index: 0;
} }
.group-title { .group-title {
font-size: 26rpx; position: relative;
z-index: 1;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
font-weight: 600;
} }
/* 藏品行 - 水平滚动 */ /* 藏品行 - 水平滚动 */
@ -510,13 +543,15 @@ watch(() => props.isActive, (newVal) => {
width: 100%; width: 100%;
height: 288rpx; height: 288rpx;
white-space: nowrap; white-space: nowrap;
background: #f0839960;
padding: 0.5rem;
} }
/* 藏品行内容容器 */ /* 藏品行内容容器 */
.nft-row-content { .nft-row-content {
display: inline-block; display: inline-block;
white-space: nowrap; white-space: nowrap;
padding-left: 24rpx; /* padding-left: 24rpx; */
height: 100%; height: 100%;
} }
@ -541,7 +576,7 @@ watch(() => props.isActive, (newVal) => {
/* NFT 图片 */ /* NFT 图片 */
.nft-image { .nft-image {
width: 192rpx; width: 192rpx;
height: 224rpx; height: 100%;
border-radius: 16rpx; border-radius: 16rpx;
/* background: rgba(255, 255, 255, 0.05); */ /* background: rgba(255, 255, 255, 0.05); */
display: block; display: block;
@ -549,7 +584,7 @@ watch(() => props.isActive, (newVal) => {
/* 已展示的图片 - 灰色滤镜 */ /* 已展示的图片 - 灰色滤镜 */
.nft-image-displayed { .nft-image-displayed {
filter: grayscale(23%); filter: grayscale(25%);
} }
/* 更多占位符 */ /* 更多占位符 */
@ -573,7 +608,7 @@ watch(() => props.isActive, (newVal) => {
top: 0; top: 0;
left: 0; left: 0;
width: 192rpx; width: 192rpx;
height: 224rpx; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -608,6 +643,41 @@ watch(() => props.isActive, (newVal) => {
padding: 16rpx; padding: 16rpx;
} }
/* 点赞数徽章 - 图片上覆盖层 */
.card-rate-badge-overlay {
position: absolute;
bottom: 12rpx;
left: 12rpx;
display: flex;
align-items: center;
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
#B94E73 100%);
border-radius: 999rpx;
padding: 8rpx 20rpx 8rpx 40rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
/* background: rgba(0, 0, 0, 0.5); */
border-radius: 20rpx;
padding: 6rpx 12rpx;
z-index: 2;
}
.card-rate-badge-overlay .heart-icon {
width: 24rpx;
height: 24rpx;
margin-right: 6rpx;
}
.card-rate-badge-overlay .card-rate-text {
font-size: 22rpx;
color: #fff;
font-weight: bold;
}
.nft-name { .nft-name {
display: block; display: block;
font-size: 22rpx; font-size: 22rpx;
@ -629,7 +699,7 @@ watch(() => props.isActive, (newVal) => {
left: 50%; left: 50%;
transform: translate(-50%, -50%); */ transform: translate(-50%, -50%); */
width: 192rpx; width: 192rpx;
height: 224rpx; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -97,8 +97,8 @@
class="menu-item" class="menu-item"
@tap="handleMenuVisit" @tap="handleMenuVisit"
> >
<image class="menu-icon" src="/static/icon/visit-house.png" mode="aspectFit" lazy-load></image> <image class="menu-icon" src="/static/square/dianjibaifang.png" mode="aspectFit" lazy-load></image>
<text class="menu-text">拜访小屋</text> <text class="menu-text">点击拜访</text>
</view> </view>
<view class="menu-item" @tap="handleMenuViewProfile"> <view class="menu-item" @tap="handleMenuViewProfile">
<image class="menu-icon" src="/static/icon/character.png" mode="aspectFit" lazy-load></image> <image class="menu-icon" src="/static/icon/character.png" mode="aspectFit" lazy-load></image>
@ -426,8 +426,8 @@
} }
.user-nickname { .user-nickname {
font-size: 14rpx; font-size: 12rpx;
margin-left: 12rpx; margin-left: 10rpx;
color: #FFFFFF; color: #FFFFFF;
text-align: center; text-align: center;
max-width: 100%; max-width: 100%;

View File

@ -103,7 +103,7 @@
<!-- 右侧奖励 --> <!-- 右侧奖励 -->
<view class="liked-reward"> <view class="liked-reward">
<image class="reward-token-icon" src="/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> </image>
<text class="reward-amount">+{{ item.reward }}</text> <text class="reward-amount">+{{ item.reward }}</text>
</view> </view>

View File

@ -104,9 +104,10 @@
<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-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'" <image class="liked-cover" :src="item.cover_url || '/static/nft/placeholder.png'"
mode="aspectFill"></image> mode="aspectFill"></image>
<image class="liked-cover-frame" src="/static/square/cangpinkuang1.png" <image class="liked-cover-frame" src="/static/square/cangpinkuang1.png"
@ -125,7 +126,7 @@
<!-- 右侧奖励 --> <!-- 右侧奖励 -->
<view class="liked-reward"> <view class="liked-reward">
<image class="reward-token-icon" src="/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> </image>
<text class="reward-amount">+{{ item.reward }}</text> <text class="reward-amount">+{{ item.reward }}</text>
</view> </view>
@ -323,7 +324,8 @@ const loadExhibitedAssets = async () => {
try { try {
const res = await getMyExhibitedAssetsApi(1, 20); const res = await getMyExhibitedAssetsApi(1, 20);
if (res.data && res.data.items) { if (res.data && res.data.items) {
exhibitionWorks.value = res.data.items.map(item => ({ exhibitionWorks.value = res.data.items
.map(item => ({
id: item.asset_id, id: item.asset_id,
cover_url: item.cover_url, cover_url: item.cover_url,
like_count: item.like_count, like_count: item.like_count,
@ -331,7 +333,10 @@ const loadExhibitedAssets = async () => {
exhibited_at: item.exhibited_at, exhibited_at: item.exhibited_at,
expire_at: item.expire_at, expire_at: item.expire_at,
name: item.name, name: item.name,
})); slot_index: item.slot_index ?? 0,
}))
.sort((a, b) => (a.slot_index ?? 0) - (b.slot_index ?? 0));
console.log('展出作品:', exhibitionWorks.value);
} }
} catch (err) { } catch (err) {
console.error('加载展出作品失败:', err); console.error('加载展出作品失败:', err);
@ -545,10 +550,14 @@ onShow(() => {
.card-tilt-left { .card-tilt-left {
transform: rotate(-4deg) translateY(10rpx); transform: rotate(-4deg) translateY(10rpx);
margin-right: 32rpx; margin-right: 32rpx;
border-radius: 32rpx;
box-shadow: -16rpx 16rpx 16rpx rgba(229, 76, 93, 0.9);
} }
.card-tilt-right { .card-tilt-right {
transform: rotate(4deg) translateY(10rpx); transform: rotate(4deg) translateY(10rpx);
border-radius: 32rpx;
box-shadow: 16rpx 16rpx 16rpx rgba(229, 76, 93, 0.9);
} }
.card-income-row.income-tilt-right { .card-income-row.income-tilt-right {
@ -611,7 +620,7 @@ onShow(() => {
/* 图片下方收益 */ /* 图片下方收益 */
.card-income-row { .card-income-row {
position: absolute; position: absolute;
bottom: -52rpx; bottom: -88rpx;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
display: flex; display: flex;
@ -758,7 +767,7 @@ onShow(() => {
display: flex; display: flex;
align-items: center; align-items: center;
background: #ffffff50; background: #ffffff50;
border-radius: 32rpx; border-radius: 48rpx;
padding: 16rpx 20rpx; padding: 16rpx 20rpx;
gap: 16rpx; gap: 16rpx;
overflow: hidden; overflow: hidden;
@ -771,6 +780,10 @@ onShow(() => {
padding: 28rpx 20rpx; padding: 28rpx 20rpx;
width: 90%; width: 90%;
padding-left: 20%; padding-left: 20%;
background-image: url(/static/square/diyi.png);
background-size: 102%;
background-position: center;
background-repeat: no-repeat;
} }
/* 排名图标 - 排名越靠前越大 */ /* 排名图标 - 排名越靠前越大 */

View File

@ -23,12 +23,20 @@
> >
<image <image
class="nft-cover" class="nft-cover"
:class="{ 'nft-image-displayed': item.display_status === 1 }"
:src="item.coverUrl || item.cover_url_signed" :src="item.coverUrl || item.cover_url_signed"
mode="aspectFill" mode="aspectFill"
/> />
<view class="nft-info"> <view v-if="item.display_status === 1" class="status-overlay">
<text class="nft-name">{{ item.name }}</text> <text class="status-text-center">已展示</text>
</view> </view>
<view class="card-rate-badge-overlay">
<image class="heart-icon" src="/static/icon/heart-icon.png" mode="aspectFit"></image>
<text class="card-rate-text">{{ item.like_count || 0 }}</text>
</view>
<!-- <view class="nft-info">
<text class="nft-name">{{ item.name }}</text>
</view> -->
</view> </view>
</view> </view>
@ -232,6 +240,7 @@ onMounted(() => {
} }
.nft-grid-item { .nft-grid-item {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -241,7 +250,7 @@ onMounted(() => {
.nft-cover { .nft-cover {
width: 192rpx; width: 192rpx;
height: 224rpx; height: 256rpx;
} }
.nft-grid-item:active { .nft-grid-item:active {
@ -264,6 +273,78 @@ onMounted(() => {
white-space: nowrap; white-space: nowrap;
} }
/* 已展示的图片 - 灰色滤镜 */
.nft-image-displayed {
filter: grayscale(25%);
}
/* 展示状态覆盖层 - 居中显示 */
.status-overlay {
position: absolute;
top: 0;
left: 0;
width: 192rpx;
height: 224rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16rpx;
z-index: 1;
}
.status-text-center {
font-size: 24rpx;
color: #fff;
font-weight: bold;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8);
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
#B94E73 100%
);
border-radius: 24rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4),
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05);
padding: 16rpx;
}
/* 点赞数徽章 - 图片上覆盖层 */
.card-rate-badge-overlay {
position: absolute;
bottom: 12rpx;
left: 12rpx;
display: flex;
align-items: center;
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
#B94E73 100%);
border-radius: 999rpx;
padding: 8rpx 20rpx 8rpx 40rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
border-radius: 20rpx;
padding: 6rpx 12rpx;
z-index: 2;
}
.card-rate-badge-overlay .heart-icon {
width: 24rpx;
height: 24rpx;
margin-right: 6rpx;
}
.card-rate-badge-overlay .card-rate-text {
font-size: 22rpx;
color: #fff;
font-weight: bold;
}
/* 加载更多 */ /* 加载更多 */
.load-more { .load-more {
display: flex; display: flex;

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@ -1,7 +1,7 @@
// API 基础配置 // API 基础配置
// const baseURL = 'http://101.132.250.62:8080' const baseURL = 'http://101.132.250.62:8080'
// const baseURL = 'http://192.168.110.60:8080' // const baseURL = 'http://192.168.110.60:8080'
const baseURL = 'http://localhost:8080' // const baseURL = 'http://localhost:8080'
// 是否使用模拟数据(开发调试时设为 true后端API准备好后改为 false // 是否使用模拟数据(开发调试时设为 true后端API准备好后改为 false
const USE_MOCK_API = false const USE_MOCK_API = false