feat: 修改藏品展示等级和藏品没有同步问题
This commit is contained in:
parent
bfae6b9851
commit
3f58a82a91
@ -11,9 +11,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
BlacklistKeyPrefix = "blacklist:token:"
|
||||
InspirationFlowKeyPrefix = "inspiration_flow:"
|
||||
AssetLikersKeyPrefix = "asset_likers:"
|
||||
BlacklistKeyPrefix = "blacklist:token:"
|
||||
InspirationFlowKeyPrefix = "inspiration_flow:"
|
||||
AssetLikersKeyPrefix = "asset_likers:"
|
||||
ExpiringAssetsKey = "expiring_assets"
|
||||
ExpiringAssetsPerStarKey = "expiring_assets:star:"
|
||||
)
|
||||
|
||||
// AssetLikersCache 缓存数据结构
|
||||
@ -320,3 +322,113 @@ func InvalidateAssetLikersCache(ctx context.Context, assetID int64) error {
|
||||
key := AssetLikersKey(assetID)
|
||||
return RedisClient.Del(ctx, key).Err()
|
||||
}
|
||||
|
||||
// AddExpiringAsset 添加到过期资产 ZSET (score = expire_at)
|
||||
func AddExpiringAsset(ctx context.Context, assetID int64, expireAt int64) error {
|
||||
if RedisClient == nil {
|
||||
return nil
|
||||
}
|
||||
// 使用 ZADD,score 为过期时间戳
|
||||
return RedisClient.ZAdd(ctx, ExpiringAssetsKey, redis.Z{
|
||||
Score: float64(expireAt),
|
||||
Member: assetID,
|
||||
}).Err()
|
||||
}
|
||||
|
||||
// AddExpiringAssetToStar 添加到指定 star 的过期资产 ZSET
|
||||
func AddExpiringAssetToStar(ctx context.Context, starID int64, assetID int64, expireAt int64) error {
|
||||
if RedisClient == nil {
|
||||
return nil
|
||||
}
|
||||
key := fmt.Sprintf("%s%d", ExpiringAssetsPerStarKey, starID)
|
||||
return RedisClient.ZAdd(ctx, key, redis.Z{
|
||||
Score: float64(expireAt),
|
||||
Member: assetID,
|
||||
}).Err()
|
||||
}
|
||||
|
||||
// RemoveExpiringAsset 从过期资产 ZSET 移除
|
||||
func RemoveExpiringAsset(ctx context.Context, assetID int64) error {
|
||||
if RedisClient == nil {
|
||||
return nil
|
||||
}
|
||||
return RedisClient.ZRem(ctx, ExpiringAssetsKey, assetID).Err()
|
||||
}
|
||||
|
||||
// RemoveExpiringAssetFromStar 从指定 star 的过期资产 ZSET 移除
|
||||
func RemoveExpiringAssetFromStar(ctx context.Context, starID int64, assetID int64) error {
|
||||
if RedisClient == nil {
|
||||
return nil
|
||||
}
|
||||
key := fmt.Sprintf("%s%d", ExpiringAssetsPerStarKey, starID)
|
||||
return RedisClient.ZRem(ctx, key, assetID).Err()
|
||||
}
|
||||
|
||||
// GetExpiredAssets 获取已过期的资产ID列表 (score <= now)
|
||||
func GetExpiredAssets(ctx context.Context, now int64) ([]int64, error) {
|
||||
if RedisClient == nil {
|
||||
return nil, fmt.Errorf("redis client not initialized")
|
||||
}
|
||||
// ZRANGEBYSCORE 获取 score 在 [0, now] 范围内的成员
|
||||
results, err := RedisClient.ZRangeByScore(ctx, ExpiringAssetsKey, &redis.ZRangeBy{
|
||||
Min: "0",
|
||||
Max: fmt.Sprintf("%d", now),
|
||||
}).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assetIDs := make([]int64, 0, len(results))
|
||||
for _, r := range results {
|
||||
var id int64
|
||||
if _, err := fmt.Sscanf(r, "%d", &id); err == nil {
|
||||
assetIDs = append(assetIDs, id)
|
||||
}
|
||||
}
|
||||
return assetIDs, nil
|
||||
}
|
||||
|
||||
// GetExpiredAssetsFromStar 获取指定 star 已过期的资产ID列表
|
||||
func GetExpiredAssetsFromStar(ctx context.Context, starID int64, now int64) ([]int64, error) {
|
||||
if RedisClient == nil {
|
||||
return nil, fmt.Errorf("redis client not initialized")
|
||||
}
|
||||
key := fmt.Sprintf("%s%d", ExpiringAssetsPerStarKey, starID)
|
||||
results, err := RedisClient.ZRangeByScore(ctx, key, &redis.ZRangeBy{
|
||||
Min: "0",
|
||||
Max: fmt.Sprintf("%d", now),
|
||||
}).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assetIDs := make([]int64, 0, len(results))
|
||||
for _, r := range results {
|
||||
var id int64
|
||||
if _, err := fmt.Sscanf(r, "%d", &id); err == nil {
|
||||
assetIDs = append(assetIDs, id)
|
||||
}
|
||||
}
|
||||
return assetIDs, nil
|
||||
}
|
||||
|
||||
// GetAllExpiringAssets 获取所有待处理的资产ID(未过期的也返回,用于校验)
|
||||
func GetAllExpiringAssets(ctx context.Context) ([]int64, error) {
|
||||
if RedisClient == nil {
|
||||
return nil, fmt.Errorf("redis client not initialized")
|
||||
}
|
||||
results, err := RedisClient.ZRange(ctx, ExpiringAssetsKey, 0, -1).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assetIDs := make([]int64, 0, len(results))
|
||||
for _, r := range results {
|
||||
var id int64
|
||||
if _, err := fmt.Sscanf(r, "%d", &id); err == nil {
|
||||
assetIDs = append(assetIDs, id)
|
||||
}
|
||||
}
|
||||
return assetIDs, nil
|
||||
}
|
||||
|
||||
|
||||
@ -157,15 +157,23 @@ func (r *assetRepository) GetGradeByAssetID(assetID int64) (int32, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// UpdateGradeByAssetID 根据asset_id更新藏品等级(用于同步AssetLevelRecord.CurrentLevel到AssetRegistry.Grade)
|
||||
// UpdateGradeByAssetID 根据asset_id更新藏品等级(用于同步AssetLevelRecord.CurrentLevel到AssetRegistry.Grade和Assets.Grade)
|
||||
func (r *assetRepository) UpdateGradeByAssetID(assetID int64, grade int32) error {
|
||||
if assetID <= 0 {
|
||||
return errors.New("asset_id must be greater than 0")
|
||||
}
|
||||
|
||||
return r.db.Table("public.asset_registry").
|
||||
Where("asset_id = ?", assetID).
|
||||
Update("grade", grade).Error
|
||||
// 同时更新 assets.grade 和 asset_registry.grade
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&models.Asset{}).
|
||||
Where("id = ?", assetID).
|
||||
Update("grade", grade).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Table("public.asset_registry").
|
||||
Where("asset_id = ?", assetID).
|
||||
Update("grade", grade).Error
|
||||
})
|
||||
}
|
||||
|
||||
// GetByIDs 批量查询资产
|
||||
|
||||
@ -23,6 +23,7 @@ type GalleryRepository interface {
|
||||
|
||||
// 展品相关
|
||||
GetExhibitionByAsset(assetID int64) (*models.Exhibition, error)
|
||||
GetExhibitionByAssetID(assetID int64) (*models.Exhibition, error)
|
||||
GetExhibitionBySlot(slotID int64) (*models.Exhibition, error)
|
||||
GetActiveExhibitionBySlot(slotID, now int64) (*models.Exhibition, error)
|
||||
GetExhibitionsByUser(userID, starID int64) ([]*models.Exhibition, error)
|
||||
@ -228,6 +229,19 @@ func (r *galleryRepository) GetExhibitionByAsset(assetID int64) (*models.Exhibit
|
||||
return &exhibition, nil
|
||||
}
|
||||
|
||||
// GetExhibitionByAssetID 根据资产ID获取活跃展品展示记录(未删除且未处理)
|
||||
func (r *galleryRepository) GetExhibitionByAssetID(assetID int64) (*models.Exhibition, error) {
|
||||
var exhibition models.Exhibition
|
||||
err := r.db.Where("asset_id = ? AND deleted_at IS NULL AND is_processed = false", assetID).First(&exhibition).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &exhibition, nil
|
||||
}
|
||||
|
||||
// GetExhibitionBySlot 根据展位ID获取展品展示记录(不含已删除)
|
||||
func (r *galleryRepository) GetExhibitionBySlot(slotID int64) (*models.Exhibition, error) {
|
||||
var exhibition models.Exhibition
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/topfans/backend/pkg/database"
|
||||
"github.com/topfans/backend/pkg/logger"
|
||||
"github.com/topfans/backend/services/galleryService/client"
|
||||
"github.com/topfans/backend/services/galleryService/repository"
|
||||
@ -37,9 +38,9 @@ func NewCleanupWorker(repo repository.GalleryRepository, assetClient client.Asse
|
||||
|
||||
// Start 启动清理Worker
|
||||
func (w *CleanupWorker) Start() {
|
||||
log.Println("清理Worker已启动,每分钟扫描一次过期展品")
|
||||
log.Println("清理Worker已启动,每小时扫描一次过期展品")
|
||||
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
// 立即执行一次清理
|
||||
@ -72,37 +73,46 @@ func (w *CleanupWorker) cleanup() {
|
||||
w.cleanupInvalidDisplayStatus()
|
||||
}
|
||||
|
||||
// cleanupExpiredExhibitions 清理过期的展品展示记录
|
||||
// cleanupExpiredExhibitions 清理过期的展品展示记录(使用 ZSET 驱动)
|
||||
func (w *CleanupWorker) cleanupExpiredExhibitions(now int64) {
|
||||
// 获取过期的展品展示记录
|
||||
expired, err := w.repo.GetExpiredExhibitions(now)
|
||||
ctx := context.Background()
|
||||
|
||||
// 使用 ZSET 获取已过期的 asset_id 列表
|
||||
expiredAssetIDs, err := database.GetExpiredAssets(ctx, now)
|
||||
if err != nil {
|
||||
log.Printf("获取过期展品失败: %v", err)
|
||||
log.Printf("从 ZSET 获取过期展品失败: %v", err)
|
||||
// 降级:从数据库查询(兼容未初始化 Redis 的情况)
|
||||
w.cleanupExpiredExhibitionsFromDB(now)
|
||||
return
|
||||
}
|
||||
|
||||
if len(expired) == 0 {
|
||||
if len(expiredAssetIDs) == 0 {
|
||||
log.Println("没有过期的展品需要清理")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("发现 %d 个过期展品,开始清理", len(expired))
|
||||
log.Printf("ZSET 发现 %d 个过期展品,开始清理", len(expiredAssetIDs))
|
||||
|
||||
// 批量删除过期记录
|
||||
successCount := 0
|
||||
failedCount := 0
|
||||
|
||||
for _, e := range expired {
|
||||
for _, assetID := range expiredAssetIDs {
|
||||
// 从数据库查询该 asset_id 对应的有效展览
|
||||
e, err := w.repo.GetExhibitionByAssetID(assetID)
|
||||
if err != nil || e == nil {
|
||||
// 展览不存在或已处理,从 ZSET 移除
|
||||
database.RemoveExpiringAsset(ctx, assetID)
|
||||
continue
|
||||
}
|
||||
|
||||
// 1. 获取点赞数用于计算收益
|
||||
likeCount := 0
|
||||
if w.assetClient != nil {
|
||||
likeCount = w.assetClient.GetAssetLikeCount(e.AssetID)
|
||||
}
|
||||
|
||||
// 2. 计算展示收益(使用 Buff 公式)
|
||||
// R1 = R0 × T × [100% + Buff(n)]
|
||||
// R0 = 5 水晶/小时
|
||||
// Buff(n): n<5→0%, 5≤n<10→10%, 10≤n<30→20%, n≥30→30%
|
||||
// 2. 计算展示收益
|
||||
revenue := calculateExhibitionRevenue(likeCount, e.StartTime, now)
|
||||
|
||||
logger.Logger.Info("计算展出收益",
|
||||
@ -114,10 +124,8 @@ func (w *CleanupWorker) cleanupExpiredExhibitions(now int64) {
|
||||
zap.Int64("revenue", revenue))
|
||||
|
||||
// 3. 调用 TaskService 记录收益
|
||||
// 注意:仅当展位所有者和占位者不同时才创建收益记录
|
||||
if w.taskClient != nil {
|
||||
// 获取展位所有者的实际用户ID(而非profile_id)
|
||||
slotOwnerUID := e.HostProfileID // 默认使用HostProfileID
|
||||
slotOwnerUID := e.HostProfileID
|
||||
if ownerUID, err := w.repo.GetSlotOwnerUserID(e.SlotID); err == nil {
|
||||
slotOwnerUID = ownerUID
|
||||
}
|
||||
@ -129,37 +137,116 @@ func (w *CleanupWorker) cleanupExpiredExhibitions(now int64) {
|
||||
OccupierUid: e.OccupierUID,
|
||||
OccupierStarId: e.OccupierStarID,
|
||||
SlotOwnerUid: slotOwnerUID,
|
||||
StartTime: e.StartTime,
|
||||
ExpireAt: now,
|
||||
StartTime: e.StartTime,
|
||||
ExpireAt: now,
|
||||
CrystalAmount: revenue,
|
||||
LikeCount: int32(likeCount),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Logger.Error("调用TaskService记录收益失败,跳过标记已处理以便重试",
|
||||
logger.Logger.Error("调用TaskService记录收益失败",
|
||||
zap.Int64("exhibition_id", e.ID),
|
||||
zap.Error(err))
|
||||
failedCount++
|
||||
continue // 不标记为已处理,让下次重试
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 不再删除展品 - 用户领取收益时才会下架
|
||||
// 此处只更新展品状态为过期(可选),展品记录保留供用户领取
|
||||
|
||||
successCount++
|
||||
|
||||
// 4.5 标记展品已处理,防止重复生成收益记录
|
||||
// 4. 标记展品已处理
|
||||
if err := w.repo.SetExhibitionProcessed(e.ID, true); err != nil {
|
||||
logger.Logger.Error("标记展品已处理失败",
|
||||
zap.Int64("exhibition_id", e.ID),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
// 5. 从 ZSET 移除
|
||||
database.RemoveExpiringAsset(ctx, assetID)
|
||||
database.RemoveExpiringAssetFromStar(ctx, e.OccupierStarID, assetID)
|
||||
|
||||
log.Printf("展品已到期并生成领取记录: ExhibitionID=%d, AssetID=%d, SlotID=%d, OccupierUID=%d, Revenue=%d",
|
||||
e.ID, e.AssetID, e.SlotID, e.OccupierUID, revenue)
|
||||
}
|
||||
|
||||
// 5. 保留点赞记录,允许用户在下次展出时再次点赞(每次展览可点赞一次)
|
||||
// 注意:点赞记录现在按 (asset_id, user_id, exhibition_id) 去重,不会与历史点赞冲突
|
||||
log.Printf("过期展品清理完成: 成功 %d 个, 失败 %d 个", successCount, failedCount)
|
||||
}
|
||||
|
||||
// cleanupExpiredExhibitionsFromDB 降级方案:从数据库查询过期展览
|
||||
func (w *CleanupWorker) cleanupExpiredExhibitionsFromDB(now int64) {
|
||||
expired, err := w.repo.GetExpiredExhibitions(now)
|
||||
if err != nil {
|
||||
log.Printf("从数据库获取过期展品失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(expired) == 0 {
|
||||
log.Println("没有过期的展品需要清理")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("数据库发现 %d 个过期展品,开始清理", len(expired))
|
||||
|
||||
successCount := 0
|
||||
failedCount := 0
|
||||
ctx := context.Background()
|
||||
|
||||
for _, e := range expired {
|
||||
likeCount := 0
|
||||
if w.assetClient != nil {
|
||||
likeCount = w.assetClient.GetAssetLikeCount(e.AssetID)
|
||||
}
|
||||
|
||||
revenue := calculateExhibitionRevenue(likeCount, e.StartTime, now)
|
||||
|
||||
logger.Logger.Info("计算展出收益",
|
||||
zap.Int64("exhibition_id", e.ID),
|
||||
zap.Int64("asset_id", e.AssetID),
|
||||
zap.Int("like_count", likeCount),
|
||||
zap.Int64("start_time", e.StartTime),
|
||||
zap.Int64("end_time", now),
|
||||
zap.Int64("revenue", revenue))
|
||||
|
||||
if w.taskClient != nil {
|
||||
slotOwnerUID := e.HostProfileID
|
||||
if ownerUID, err := w.repo.GetSlotOwnerUserID(e.SlotID); err == nil {
|
||||
slotOwnerUID = ownerUID
|
||||
}
|
||||
|
||||
_, err := w.taskClient.OnExhibitionCompleted(context.Background(), &client.OnExhibitionCompletedRequest{
|
||||
ExhibitionId: e.ID,
|
||||
AssetId: e.AssetID,
|
||||
SlotId: e.SlotID,
|
||||
OccupierUid: e.OccupierUID,
|
||||
OccupierStarId: e.OccupierStarID,
|
||||
SlotOwnerUid: slotOwnerUID,
|
||||
StartTime: e.StartTime,
|
||||
ExpireAt: now,
|
||||
CrystalAmount: revenue,
|
||||
LikeCount: int32(likeCount),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Logger.Error("调用TaskService记录收益失败",
|
||||
zap.Int64("exhibition_id", e.ID),
|
||||
zap.Error(err))
|
||||
failedCount++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
successCount++
|
||||
|
||||
if err := w.repo.SetExhibitionProcessed(e.ID, true); err != nil {
|
||||
logger.Logger.Error("标记展品已处理失败",
|
||||
zap.Int64("exhibition_id", e.ID),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
// 降级方案也需要清理 ZSET
|
||||
database.RemoveExpiringAsset(ctx, e.AssetID)
|
||||
database.RemoveExpiringAssetFromStar(ctx, e.OccupierStarID, e.AssetID)
|
||||
|
||||
log.Printf("展品已到期并生成领取记录: ExhibitionID=%d, AssetID=%d, SlotID=%d, OccupierUID=%d, Revenue=%d",
|
||||
e.ID, e.AssetID, e.SlotID, e.OccupierUID, revenue)
|
||||
}
|
||||
|
||||
log.Printf("过期展品清理完成: 成功 %d 个, 失败 %d 个", successCount, failedCount)
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/topfans/backend/pkg/database"
|
||||
"github.com/topfans/backend/pkg/logger"
|
||||
"github.com/topfans/backend/pkg/models"
|
||||
pb "github.com/topfans/backend/pkg/proto/gallery"
|
||||
@ -109,6 +110,11 @@ func (s *exhibitionService) PlaceAsset(userID, starID int64, req *pb.PlaceAssetR
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 6.5 添加到 ZSET 用于过期清理
|
||||
ctx := context.Background()
|
||||
database.AddExpiringAsset(ctx, exhibition.AssetID, exhibition.ExpireAt)
|
||||
database.AddExpiringAssetToStar(ctx, starID, exhibition.AssetID, exhibition.ExpireAt)
|
||||
|
||||
// 7. 发布事件(不再强依赖同步更新 Asset Service 状态)
|
||||
// TODO: 实现事件发布逻辑
|
||||
// go s.publishEvent("gallery.exhibit", exhibition)
|
||||
@ -152,6 +158,11 @@ func (s *exhibitionService) RemoveFromSlot(userID, starID int64, req *pb.RemoveF
|
||||
return err
|
||||
}
|
||||
|
||||
// 4.5 从 ZSET 移除
|
||||
ctx := context.Background()
|
||||
database.RemoveExpiringAsset(ctx, exhibition.AssetID)
|
||||
database.RemoveExpiringAssetFromStar(ctx, exhibition.OccupierStarID, exhibition.AssetID)
|
||||
|
||||
// 5. 保留点赞记录,允许下次展出时用户重新点赞
|
||||
// 点赞记录用于历史查询,每次展出通过 exhibition_id 区分
|
||||
// 6. 发布事件
|
||||
@ -303,6 +314,10 @@ func (s *exhibitionService) RemoveExhibitionByAsset(ctx context.Context, assetID
|
||||
return err
|
||||
}
|
||||
|
||||
// 2.5 从 ZSET 移除
|
||||
database.RemoveExpiringAsset(ctx, exhibition.AssetID)
|
||||
database.RemoveExpiringAssetFromStar(ctx, exhibition.OccupierStarID, exhibition.AssetID)
|
||||
|
||||
// 3. 保留点赞记录,允许下次展出时用户重新点赞
|
||||
// 点赞记录用于历史查询,每次展出通过 exhibition_id 区分
|
||||
|
||||
|
||||
180
docs/figma-analysis-topfans-ranking.md
Normal file
180
docs/figma-analysis-topfans-ranking.md
Normal file
@ -0,0 +1,180 @@
|
||||
# TOPFANS 排行榜页面结构分析
|
||||
|
||||
## 页面概述
|
||||
- **页面名称**: HOME
|
||||
- **尺寸**: 375 x 1588 (移动端)
|
||||
- **风格**: 梦幻甜美、饭圈审美、粉色系渐变
|
||||
|
||||
---
|
||||
|
||||
## 一、页面布局结构(从上到下)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ 1. 状态栏 (iPhone Status Bar) │ 高度: ~20px
|
||||
├─────────────────────────────────┤
|
||||
│ 2. 顶部用户区 │ y: 45px
|
||||
│ - 积分: 2713 │
|
||||
│ - 每日任务 | 巴士应援 图标 │
|
||||
├─────────────────────────────────┤
|
||||
│ 3. Banner 横幅区域 │ y: 264px
|
||||
│ - 背景装饰 (模糊渐变) │
|
||||
│ - "TOPFANS 排行榜" 主标题 │
|
||||
│ - "日榜" 标签 │
|
||||
├─────────────────────────────────┤
|
||||
│ 4. 藏品内容区 (3x4 Grid) │ y: 293px
|
||||
│ - 12 个藏品卡片 │
|
||||
│ - 每个卡片带 TOP 1-12 排名 │
|
||||
│ - 点赞数和文案 │
|
||||
│ - "查看更多" 按钮 │
|
||||
├─────────────────────────────────┤
|
||||
│ 5. 分类导航区 │ y: 950px
|
||||
│ - 星卡 | 吧唧 | 海报 │
|
||||
│ - 3 个大图标导航 │
|
||||
├─────────────────────────────────┤
|
||||
│ 6. 筛选 Tab 栏 │
|
||||
│ - 热门作品 | 最新作品 │
|
||||
├─────────────────────────────────┤
|
||||
│ 7. 瀑布流作品展示区 │
|
||||
│ - 多张卡片,含图片+点赞+文案 │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、核心组件详解
|
||||
|
||||
### 2.1 状态栏 (1:29 -> 1:7)
|
||||
- iPhone 状态栏组件,包含时间、信号、电池图标
|
||||
|
||||
### 2.2 顶部用户区 (活动栏位)
|
||||
| 元素 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| 每日任务 | 文字 | 粉水晶样式 |
|
||||
| 巴士应援 | 文字 | 粉水晶样式 |
|
||||
| 用户头像 | 图片 | 圆形 |
|
||||
| 积分显示 | 文字 (2713) | 白色带阴影 |
|
||||
|
||||
### 2.3 Banner 横幅 (banner条)
|
||||
```
|
||||
- 背景: 模糊渐变 (粉红到透明)
|
||||
- "TOPFANS 排行榜" 主标题
|
||||
- "日榜" 标签 (右上角)
|
||||
- 装饰性圆形图案
|
||||
```
|
||||
|
||||
### 2.4 藏品内容区 (Grid)
|
||||
- **布局**: 3列 x 4行
|
||||
- **卡片结构**:
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 图片 │ ← 带圆角的主图
|
||||
│ ❤️ 9653 │ ← 点赞图标 + 数字
|
||||
├─────────────┤
|
||||
│ 新铸爱的小卡也太绝了 │ ← 文案
|
||||
├─────────────┤
|
||||
│ TOP 1 │ ← 排名标签 (金色)
|
||||
└─────────────┘
|
||||
```
|
||||
- **卡片样式**:
|
||||
- 图片圆角: 8px
|
||||
- 背景: 白色圆角卡片带阴影
|
||||
- 点赞数: 带文字阴影
|
||||
- TOP 标签: 金色/红色渐变
|
||||
|
||||
### 2.5 分类导航区 (顶部金刚区)
|
||||
| 图标 | 名称 | 样式 |
|
||||
|------|------|------|
|
||||
| 星卡 | 星卡 | 粉色水晶风格,opacity 0.75 |
|
||||
| 吧唧 | 吧唧 | 同上 |
|
||||
| 海报 | 海报 | 同上 |
|
||||
|
||||
### 2.6 筛选 Tab 栏
|
||||
- **热门作品**: 选中状态 (opacity 0.75)
|
||||
- **最新作品**: 未选中状态 (opacity 0.52)
|
||||
- 样式: 胶囊形状,半透明背景
|
||||
|
||||
### 2.7 瀑布流展示区
|
||||
- 每张卡片包含:
|
||||
- 图片 (带圆角)
|
||||
- 点赞按钮 + 数字
|
||||
- 文案标题
|
||||
|
||||
---
|
||||
|
||||
## 三、颜色主题
|
||||
|
||||
| 用途 | 颜色 |
|
||||
|------|------|
|
||||
| 主背景渐变 | linear-gradient(126deg, #FFE5E5, #FF9C9C, #6AC5D3) |
|
||||
| 卡片背景 | #FFFFFF |
|
||||
| 强调/排名 | #FC6466, #F45668, #FFF60F |
|
||||
| 点赞红心 | #FF5A5D |
|
||||
| 文字主色 | #FFFFFF |
|
||||
| 文字深色 | #000000 |
|
||||
| Tab 未选中 | opacity 0.52 |
|
||||
| Tab 选中 | opacity 0.75 |
|
||||
|
||||
---
|
||||
|
||||
## 四、阴影与效果
|
||||
|
||||
| 效果 | 参数 |
|
||||
|------|------|
|
||||
| 粉色卡片阴影 | box-shadow: 2px 2px 4px rgba(242, 21, 21, 0.47) |
|
||||
| 应援图标阴影 | box-shadow: 0px 4px 4px rgba(238, 38, 38, 0.33) |
|
||||
| 模糊背景 | blur(4px), blur(9.3px) |
|
||||
| 状态栏背景 | blur(4px) |
|
||||
|
||||
---
|
||||
|
||||
## 五、文字样式
|
||||
|
||||
| 用途 | 样式 |
|
||||
|------|------|
|
||||
| TOP 排名 | 金色带阴影 (style_GT3LFV) |
|
||||
| 日榜标签 | 白色带阴影 |
|
||||
| 藏品文案 | 白色带阴影 (style_S0XHUK) |
|
||||
| 瀑布发文案 | 同上 |
|
||||
| 分类名称 | 白色带阴影 (style_0R0H6H) |
|
||||
| 积分数字 | 白色 (style_F5RO1M) |
|
||||
|
||||
---
|
||||
|
||||
## 六、数据内容示例
|
||||
|
||||
### 排行榜藏品 (12个)
|
||||
| 排名 | 点赞数 | 文案 |
|
||||
|------|--------|------|
|
||||
| TOP 1 | 9653 | 新铸爱的小卡也太绝了 |
|
||||
| TOP 2 | 9430 | 今日翻牌:这张海报… |
|
||||
| TOP 3 | 7933 | 细节控狂喜 |
|
||||
| TOP 4 | 8034 | 海报墙又添新成员了 |
|
||||
| TOP 5 | 9223 | 这张小卡光影感绝了 |
|
||||
| TOP 6 | 6420 | 展开那一刻,忍不住… |
|
||||
| TOP 7 | 6342 | 必须单独发一条 |
|
||||
| TOP 8 | 9070 | 这个角度我直接封神 |
|
||||
| TOP 9 | 5943 | 这套效果每张都想单独说一次 |
|
||||
| TOP 10 | 5270 | 新进一位门面 |
|
||||
| TOP 11 | 4766 | 压箱底的海报 |
|
||||
| TOP 12 | 4326 | 铸爱进度又往前了 |
|
||||
|
||||
### 瀑布流内容 (示例)
|
||||
| 点赞数 | 文案 |
|
||||
|--------|------|
|
||||
| 3244 | 来给大家看看我新到手的宝贝呀~~~ |
|
||||
| 7189 | マイドラだゆううううう |
|
||||
| 147 | 咕咕嘎嘎 |
|
||||
|
||||
---
|
||||
|
||||
## 七、结论
|
||||
|
||||
这是一个**粉丝应援类排行榜页面**,核心功能:
|
||||
1. 展示用户积分和应援任务入口
|
||||
2. TOP 12 藏品排行榜 (3x4 grid)
|
||||
3. 分类导航 (星卡/吧唧/海报)
|
||||
4. 热门/最新作品筛选
|
||||
5. 瀑布流UGC内容展示
|
||||
|
||||
**开发优先级建议**: 高 (核心排行榜功能)
|
||||
Loading…
Reference in New Issue
Block a user