feat: 新增个人页

This commit is contained in:
zerosaturation 2026-04-30 21:21:12 +08:00
parent 36e8f251e2
commit f47a3b2ce4
26 changed files with 2293 additions and 1674 deletions

View File

@ -634,3 +634,86 @@ result := &dto.GetMyExhibitedAssetsResponseDTO{
response.Success(c, result)
}
// GetInspirationFlow 获取灵感瀑布藏品列表
// @Summary 获取灵感瀑布藏品列表
// @Description 获取灵感瀑布藏品列表,支持随机展示和双向滚动
// @Tags galleries
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param cursor query string false "游标"
// @Param direction query string false "滚动方向right加载新数据/ left加载历史" default(right)
// @Param limit query int false "每页数量" default(10)
// @Param type query string false "过滤类型badge/poster/original/all" default(all)
// @Param session_id query string false "会话ID"
// @Success 200 {object} response.Response{data=dto.GetInspirationFlowResponseDTO}
// @Router /api/v1/inspiration-flow [get]
func (ctrl *GalleryController) GetInspirationFlow(c *gin.Context) {
// 从上下文获取用户信息(中间件已验证)
userID, exists := c.Get("user_id")
if !exists {
response.Error(c, http.StatusUnauthorized, "用户未认证")
return
}
starID, exists := c.Get("star_id")
if !exists {
response.Error(c, http.StatusUnauthorized, "明星身份未设置")
return
}
// 解析请求参数
cursor := c.Query("cursor")
direction := c.DefaultQuery("direction", "right")
limitStr := c.DefaultQuery("limit", "10")
limit, _ := strconv.Atoi(limitStr)
materialType := c.DefaultQuery("type", "all")
sessionID := c.Query("session_id")
// 创建带有附加信息的contextTriple协议要求attachments值为string类型
ctx := context.WithValue(context.Background(), constant.AttachmentKey, map[string]interface{}{
"user_id": strconv.FormatInt(userID.(int64), 10),
"star_id": strconv.FormatInt(starID.(int64), 10),
})
// 调用RPC服务
resp, err := ctrl.galleryService.GetInspirationFlow(ctx, &pbGallery.GetInspirationFlowRequest{
Cursor: cursor,
Direction: direction,
Limit: int32(limit),
Type: materialType,
SessionId: sessionID,
})
if err != nil {
logger.Logger.Error("GetInspirationFlow RPC failed",
zap.Any("user_id", userID),
zap.Any("star_id", starID),
zap.Error(err),
)
_, msg := parseRPCError(err)
errorMsg := "获取灵感瀑布列表失败"
if msg != "" {
errorMsg += "" + msg
}
response.Error(c, http.StatusInternalServerError, errorMsg)
return
}
// 检查业务错误
if resp.Base.Code != pbCommon.StatusCode_STATUS_OK {
response.Error(c, http.StatusBadRequest, resp.Base.Message)
return
}
// 转换为DTO
flowDTO := dto.ConvertInspirationFlowData(resp.Data)
logger.Logger.Info("GetInspirationFlow success",
zap.Any("user_id", userID),
zap.Any("star_id", starID),
zap.Int("count", len(flowDTO.Items)),
)
response.Success(c, flowDTO)
}

View File

@ -150,3 +150,29 @@ func ConvertPlaceAssetRequest(dto *PlaceAssetRequestDTO) *pbGallery.PlaceAssetRe
SlotId: dto.SlotID,
}
}
// ConvertInspirationFlowData 转换灵感瀑布数据
func ConvertInspirationFlowData(pbData *pbGallery.InspirationFlowData) *GetInspirationFlowResponseDTO {
if pbData == nil {
return nil
}
dto := &GetInspirationFlowResponseDTO{
Cursor: pbData.Cursor,
HasMore: pbData.HasMore,
SessionID: pbData.SessionId,
Items: make([]*InspirationFlowItemDTO, 0, len(pbData.Items)),
}
for _, item := range pbData.Items {
dto.Items = append(dto.Items, &InspirationFlowItemDTO{
AssetID: item.AssetId,
Name: item.Name,
CoverURL: item.CoverUrl,
LikeCount: item.LikeCount,
OwnerNickname: item.OwnerNickname,
})
}
return dto
}

View File

@ -90,3 +90,22 @@ type GetMyExhibitedAssetsResponseDTO struct {
Total int64 `json:"total"` // 总数量
HasMore bool `json:"has_more"` // 是否有更多
}
// ========== 灵感瀑布相关 ==========
// InspirationFlowItemDTO 灵感瀑布藏品项
type InspirationFlowItemDTO struct {
AssetID int64 `json:"asset_id"` // 资产ID
Name string `json:"name"` // 藏品名称
CoverURL string `json:"cover_url"` // 封面图URL
LikeCount int32 `json:"like_count"` // 点赞数
OwnerNickname string `json:"owner_nickname"` // 展出者昵称
}
// GetInspirationFlowResponseDTO 获取灵感瀑布藏品列表响应
type GetInspirationFlowResponseDTO struct {
Items []*InspirationFlowItemDTO `json:"items"` // 藏品列表
Cursor string `json:"cursor"` // 下次请求的游标
HasMore bool `json:"has_more"` // 是否有更多
SessionID string `json:"session_id"` // 会话ID
}

View File

@ -203,6 +203,13 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl
galleries.POST("/slots_unlock", galleryCtrl.UnlockSlot) // 解锁/购买新展位
}
// 灵感瀑布相关路由(需要认证)
inspirationFlow := v1.Group("/inspiration-flow")
inspirationFlow.Use(middleware.AuthMiddleware())
{
inspirationFlow.GET("", galleryCtrl.GetInspirationFlow) // 获取灵感瀑布藏品列表
}
// 我的展馆路由(需要认证)
mygalleries := v1.Group("/mygalleries")
mygalleries.Use(middleware.AuthMiddleware())

View File

@ -0,0 +1,284 @@
// Code generated by protoc-gen-triple. DO NOT EDIT.
//
// Source: gallery.proto
package gallery
import (
"context"
)
import (
"dubbo.apache.org/dubbo-go/v3"
"dubbo.apache.org/dubbo-go/v3/client"
"dubbo.apache.org/dubbo-go/v3/common"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
"dubbo.apache.org/dubbo-go/v3/server"
)
// This is a compile-time assertion to ensure that this generated file and the Triple package
// are compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of Triple newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of Triple or updating the Triple
// version compiled into your binary.
const _ = triple_protocol.IsAtLeastVersion0_1_0
const (
// GalleryServiceName is the fully-qualified name of the GalleryService service.
GalleryServiceName = "topfans.gallery.GalleryService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// GalleryServiceGetMyGalleryProcedure is the fully-qualified name of the GalleryService's GetMyGallery RPC.
GalleryServiceGetMyGalleryProcedure = "/topfans.gallery.GalleryService/GetMyGallery"
// GalleryServiceGetUserGalleryProcedure is the fully-qualified name of the GalleryService's GetUserGallery RPC.
GalleryServiceGetUserGalleryProcedure = "/topfans.gallery.GalleryService/GetUserGallery"
// GalleryServicePlaceAssetProcedure is the fully-qualified name of the GalleryService's PlaceAsset RPC.
GalleryServicePlaceAssetProcedure = "/topfans.gallery.GalleryService/PlaceAsset"
// GalleryServiceUnlockSlotProcedure is the fully-qualified name of the GalleryService's UnlockSlot RPC.
GalleryServiceUnlockSlotProcedure = "/topfans.gallery.GalleryService/UnlockSlot"
// GalleryServiceRemoveFromSlotProcedure is the fully-qualified name of the GalleryService's RemoveFromSlot RPC.
GalleryServiceRemoveFromSlotProcedure = "/topfans.gallery.GalleryService/RemoveFromSlot"
// GalleryServiceGetMyExhibitedAssetsProcedure is the fully-qualified name of the GalleryService's GetMyExhibitedAssets RPC.
GalleryServiceGetMyExhibitedAssetsProcedure = "/topfans.gallery.GalleryService/GetMyExhibitedAssets"
// GalleryServiceGetInspirationFlowProcedure is the fully-qualified name of the GalleryService's GetInspirationFlow RPC.
GalleryServiceGetInspirationFlowProcedure = "/topfans.gallery.GalleryService/GetInspirationFlow"
)
var (
_ GalleryService = (*GalleryServiceImpl)(nil)
)
// GalleryService is a client for the topfans.gallery.GalleryService service.
type GalleryService interface {
GetMyGallery(ctx context.Context, req *GetMyGalleryRequest, opts ...client.CallOption) (*GetMyGalleryResponse, error)
GetUserGallery(ctx context.Context, req *GetUserGalleryRequest, opts ...client.CallOption) (*GetUserGalleryResponse, error)
PlaceAsset(ctx context.Context, req *PlaceAssetRequest, opts ...client.CallOption) (*PlaceAssetResponse, error)
UnlockSlot(ctx context.Context, req *UnlockSlotRequest, opts ...client.CallOption) (*UnlockSlotResponse, error)
RemoveFromSlot(ctx context.Context, req *RemoveFromSlotRequest, opts ...client.CallOption) (*RemoveFromSlotResponse, error)
GetMyExhibitedAssets(ctx context.Context, req *GetMyExhibitedAssetsRequest, opts ...client.CallOption) (*GetMyExhibitedAssetsResponse, error)
GetInspirationFlow(ctx context.Context, req *GetInspirationFlowRequest, opts ...client.CallOption) (*GetInspirationFlowResponse, error)
}
// NewGalleryService constructs a client for the gallery.GalleryService service.
func NewGalleryService(cli *client.Client, opts ...client.ReferenceOption) (GalleryService, error) {
conn, err := cli.DialWithInfo("topfans.gallery.GalleryService", &GalleryService_ClientInfo, opts...)
if err != nil {
return nil, err
}
return &GalleryServiceImpl{
conn: conn,
}, nil
}
func SetConsumerGalleryService(srv common.RPCService) {
dubbo.SetConsumerServiceWithInfo(srv, &GalleryService_ClientInfo)
}
// GalleryServiceImpl implements GalleryService.
type GalleryServiceImpl struct {
conn *client.Connection
}
func (c *GalleryServiceImpl) GetMyGallery(ctx context.Context, req *GetMyGalleryRequest, opts ...client.CallOption) (*GetMyGalleryResponse, error) {
resp := new(GetMyGalleryResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetMyGallery", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *GalleryServiceImpl) GetUserGallery(ctx context.Context, req *GetUserGalleryRequest, opts ...client.CallOption) (*GetUserGalleryResponse, error) {
resp := new(GetUserGalleryResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetUserGallery", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *GalleryServiceImpl) PlaceAsset(ctx context.Context, req *PlaceAssetRequest, opts ...client.CallOption) (*PlaceAssetResponse, error) {
resp := new(PlaceAssetResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "PlaceAsset", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *GalleryServiceImpl) UnlockSlot(ctx context.Context, req *UnlockSlotRequest, opts ...client.CallOption) (*UnlockSlotResponse, error) {
resp := new(UnlockSlotResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "UnlockSlot", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *GalleryServiceImpl) RemoveFromSlot(ctx context.Context, req *RemoveFromSlotRequest, opts ...client.CallOption) (*RemoveFromSlotResponse, error) {
resp := new(RemoveFromSlotResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "RemoveFromSlot", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *GalleryServiceImpl) GetMyExhibitedAssets(ctx context.Context, req *GetMyExhibitedAssetsRequest, opts ...client.CallOption) (*GetMyExhibitedAssetsResponse, error) {
resp := new(GetMyExhibitedAssetsResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetMyExhibitedAssets", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *GalleryServiceImpl) GetInspirationFlow(ctx context.Context, req *GetInspirationFlowRequest, opts ...client.CallOption) (*GetInspirationFlowResponse, error) {
resp := new(GetInspirationFlowResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetInspirationFlow", opts...); err != nil {
return nil, err
}
return resp, nil
}
var GalleryService_ClientInfo = client.ClientInfo{
InterfaceName: "topfans.gallery.GalleryService",
MethodNames: []string{"GetMyGallery", "GetUserGallery", "PlaceAsset", "UnlockSlot", "RemoveFromSlot", "GetMyExhibitedAssets", "GetInspirationFlow"},
ConnectionInjectFunc: func(dubboCliRaw interface{}, conn *client.Connection) {
dubboCli := dubboCliRaw.(*GalleryServiceImpl)
dubboCli.conn = conn
},
}
// GalleryServiceHandler is an implementation of the topfans.gallery.GalleryService service.
type GalleryServiceHandler interface {
GetMyGallery(context.Context, *GetMyGalleryRequest) (*GetMyGalleryResponse, error)
GetUserGallery(context.Context, *GetUserGalleryRequest) (*GetUserGalleryResponse, error)
PlaceAsset(context.Context, *PlaceAssetRequest) (*PlaceAssetResponse, error)
UnlockSlot(context.Context, *UnlockSlotRequest) (*UnlockSlotResponse, error)
RemoveFromSlot(context.Context, *RemoveFromSlotRequest) (*RemoveFromSlotResponse, error)
GetMyExhibitedAssets(context.Context, *GetMyExhibitedAssetsRequest) (*GetMyExhibitedAssetsResponse, error)
GetInspirationFlow(context.Context, *GetInspirationFlowRequest) (*GetInspirationFlowResponse, error)
}
func RegisterGalleryServiceHandler(srv *server.Server, hdlr GalleryServiceHandler, opts ...server.ServiceOption) error {
return srv.Register(hdlr, &GalleryService_ServiceInfo, opts...)
}
func SetProviderGalleryService(srv common.RPCService) {
dubbo.SetProviderServiceWithInfo(srv, &GalleryService_ServiceInfo)
}
var GalleryService_ServiceInfo = server.ServiceInfo{
InterfaceName: "topfans.gallery.GalleryService",
ServiceType: (*GalleryServiceHandler)(nil),
Methods: []server.MethodInfo{
{
Name: "GetMyGallery",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetMyGalleryRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetMyGalleryRequest)
res, err := handler.(GalleryServiceHandler).GetMyGallery(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "GetUserGallery",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetUserGalleryRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetUserGalleryRequest)
res, err := handler.(GalleryServiceHandler).GetUserGallery(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "PlaceAsset",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(PlaceAssetRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*PlaceAssetRequest)
res, err := handler.(GalleryServiceHandler).PlaceAsset(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "UnlockSlot",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(UnlockSlotRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*UnlockSlotRequest)
res, err := handler.(GalleryServiceHandler).UnlockSlot(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "RemoveFromSlot",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(RemoveFromSlotRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*RemoveFromSlotRequest)
res, err := handler.(GalleryServiceHandler).RemoveFromSlot(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "GetMyExhibitedAssets",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetMyExhibitedAssetsRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetMyExhibitedAssetsRequest)
res, err := handler.(GalleryServiceHandler).GetMyExhibitedAssets(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "GetInspirationFlow",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetInspirationFlowRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetInspirationFlowRequest)
res, err := handler.(GalleryServiceHandler).GetInspirationFlow(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
},
}

View File

@ -0,0 +1,554 @@
// Code generated by protoc-gen-triple. DO NOT EDIT.
//
// Source: social.proto
package social
import (
"context"
)
import (
"dubbo.apache.org/dubbo-go/v3"
"dubbo.apache.org/dubbo-go/v3/client"
"dubbo.apache.org/dubbo-go/v3/common"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
"dubbo.apache.org/dubbo-go/v3/server"
)
// This is a compile-time assertion to ensure that this generated file and the Triple package
// are compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of Triple newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of Triple or updating the Triple
// version compiled into your binary.
const _ = triple_protocol.IsAtLeastVersion0_1_0
const (
// SocialServiceName is the fully-qualified name of the SocialService service.
SocialServiceName = "topfans.social.SocialService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// SocialServiceSendFriendRequestProcedure is the fully-qualified name of the SocialService's SendFriendRequest RPC.
SocialServiceSendFriendRequestProcedure = "/topfans.social.SocialService/SendFriendRequest"
// SocialServiceGetFriendRequestsProcedure is the fully-qualified name of the SocialService's GetFriendRequests RPC.
SocialServiceGetFriendRequestsProcedure = "/topfans.social.SocialService/GetFriendRequests"
// SocialServiceHandleFriendRequestProcedure is the fully-qualified name of the SocialService's HandleFriendRequest RPC.
SocialServiceHandleFriendRequestProcedure = "/topfans.social.SocialService/HandleFriendRequest"
// SocialServiceGetFriendListProcedure is the fully-qualified name of the SocialService's GetFriendList RPC.
SocialServiceGetFriendListProcedure = "/topfans.social.SocialService/GetFriendList"
// SocialServiceDeleteFriendProcedure is the fully-qualified name of the SocialService's DeleteFriend RPC.
SocialServiceDeleteFriendProcedure = "/topfans.social.SocialService/DeleteFriend"
// SocialServiceSetFriendRemarkProcedure is the fully-qualified name of the SocialService's SetFriendRemark RPC.
SocialServiceSetFriendRemarkProcedure = "/topfans.social.SocialService/SetFriendRemark"
// SocialServiceCheckFriendshipProcedure is the fully-qualified name of the SocialService's CheckFriendship RPC.
SocialServiceCheckFriendshipProcedure = "/topfans.social.SocialService/CheckFriendship"
// SocialServiceGetFriendCountProcedure is the fully-qualified name of the SocialService's GetFriendCount RPC.
SocialServiceGetFriendCountProcedure = "/topfans.social.SocialService/GetFriendCount"
// SocialServiceSearchUserForFriendProcedure is the fully-qualified name of the SocialService's SearchUserForFriend RPC.
SocialServiceSearchUserForFriendProcedure = "/topfans.social.SocialService/SearchUserForFriend"
// SocialServiceGetRandomUsersProcedure is the fully-qualified name of the SocialService's GetRandomUsers RPC.
SocialServiceGetRandomUsersProcedure = "/topfans.social.SocialService/GetRandomUsers"
// SocialServiceGetUsersPagedProcedure is the fully-qualified name of the SocialService's GetUsersPaged RPC.
SocialServiceGetUsersPagedProcedure = "/topfans.social.SocialService/GetUsersPaged"
// SocialServiceLikeAssetProcedure is the fully-qualified name of the SocialService's LikeAsset RPC.
SocialServiceLikeAssetProcedure = "/topfans.social.SocialService/LikeAsset"
// SocialServiceUnlikeAssetProcedure is the fully-qualified name of the SocialService's UnlikeAsset RPC.
SocialServiceUnlikeAssetProcedure = "/topfans.social.SocialService/UnlikeAsset"
// SocialServiceCheckAssetLikeProcedure is the fully-qualified name of the SocialService's CheckAssetLike RPC.
SocialServiceCheckAssetLikeProcedure = "/topfans.social.SocialService/CheckAssetLike"
// SocialServiceGetMyLikedAssetsProcedure is the fully-qualified name of the SocialService's GetMyLikedAssets RPC.
SocialServiceGetMyLikedAssetsProcedure = "/topfans.social.SocialService/GetMyLikedAssets"
// SocialServiceGetMyTodayLikedAssetsProcedure is the fully-qualified name of the SocialService's GetMyTodayLikedAssets RPC.
SocialServiceGetMyTodayLikedAssetsProcedure = "/topfans.social.SocialService/GetMyTodayLikedAssets"
// SocialServiceGetMyWeekLikedAssetsProcedure is the fully-qualified name of the SocialService's GetMyWeekLikedAssets RPC.
SocialServiceGetMyWeekLikedAssetsProcedure = "/topfans.social.SocialService/GetMyWeekLikedAssets"
)
var (
_ SocialService = (*SocialServiceImpl)(nil)
)
// SocialService is a client for the topfans.social.SocialService service.
type SocialService interface {
SendFriendRequest(ctx context.Context, req *SendFriendRequestRequest, opts ...client.CallOption) (*SendFriendRequestResponse, error)
GetFriendRequests(ctx context.Context, req *GetFriendRequestsRequest, opts ...client.CallOption) (*GetFriendRequestsResponse, error)
HandleFriendRequest(ctx context.Context, req *HandleFriendRequestRequest, opts ...client.CallOption) (*HandleFriendRequestResponse, error)
GetFriendList(ctx context.Context, req *GetFriendListRequest, opts ...client.CallOption) (*GetFriendListResponse, error)
DeleteFriend(ctx context.Context, req *DeleteFriendRequest, opts ...client.CallOption) (*DeleteFriendResponse, error)
SetFriendRemark(ctx context.Context, req *SetFriendRemarkRequest, opts ...client.CallOption) (*SetFriendRemarkResponse, error)
CheckFriendship(ctx context.Context, req *CheckFriendshipRequest, opts ...client.CallOption) (*CheckFriendshipResponse, error)
GetFriendCount(ctx context.Context, req *GetFriendCountRequest, opts ...client.CallOption) (*GetFriendCountResponse, error)
SearchUserForFriend(ctx context.Context, req *SearchUserForFriendRequest, opts ...client.CallOption) (*SearchUserForFriendResponse, error)
GetRandomUsers(ctx context.Context, req *GetRandomUsersRequest, opts ...client.CallOption) (*GetRandomUsersResponse, error)
GetUsersPaged(ctx context.Context, req *GetUsersPagedRequest, opts ...client.CallOption) (*GetUsersPagedResponse, error)
LikeAsset(ctx context.Context, req *LikeAssetRequest, opts ...client.CallOption) (*LikeAssetResponse, error)
UnlikeAsset(ctx context.Context, req *UnlikeAssetRequest, opts ...client.CallOption) (*UnlikeAssetResponse, error)
CheckAssetLike(ctx context.Context, req *CheckAssetLikeRequest, opts ...client.CallOption) (*CheckAssetLikeResponse, error)
GetMyLikedAssets(ctx context.Context, req *GetMyLikedAssetsRequest, opts ...client.CallOption) (*GetMyLikedAssetsResponse, error)
GetMyTodayLikedAssets(ctx context.Context, req *GetMyTodayLikedAssetsRequest, opts ...client.CallOption) (*GetMyTodayLikedAssetsResponse, error)
GetMyWeekLikedAssets(ctx context.Context, req *GetMyWeekLikedAssetsRequest, opts ...client.CallOption) (*GetMyWeekLikedAssetsResponse, error)
}
// NewSocialService constructs a client for the social.SocialService service.
func NewSocialService(cli *client.Client, opts ...client.ReferenceOption) (SocialService, error) {
conn, err := cli.DialWithInfo("topfans.social.SocialService", &SocialService_ClientInfo, opts...)
if err != nil {
return nil, err
}
return &SocialServiceImpl{
conn: conn,
}, nil
}
func SetConsumerSocialService(srv common.RPCService) {
dubbo.SetConsumerServiceWithInfo(srv, &SocialService_ClientInfo)
}
// SocialServiceImpl implements SocialService.
type SocialServiceImpl struct {
conn *client.Connection
}
func (c *SocialServiceImpl) SendFriendRequest(ctx context.Context, req *SendFriendRequestRequest, opts ...client.CallOption) (*SendFriendRequestResponse, error) {
resp := new(SendFriendRequestResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "SendFriendRequest", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) GetFriendRequests(ctx context.Context, req *GetFriendRequestsRequest, opts ...client.CallOption) (*GetFriendRequestsResponse, error) {
resp := new(GetFriendRequestsResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetFriendRequests", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) HandleFriendRequest(ctx context.Context, req *HandleFriendRequestRequest, opts ...client.CallOption) (*HandleFriendRequestResponse, error) {
resp := new(HandleFriendRequestResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "HandleFriendRequest", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) GetFriendList(ctx context.Context, req *GetFriendListRequest, opts ...client.CallOption) (*GetFriendListResponse, error) {
resp := new(GetFriendListResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetFriendList", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) DeleteFriend(ctx context.Context, req *DeleteFriendRequest, opts ...client.CallOption) (*DeleteFriendResponse, error) {
resp := new(DeleteFriendResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "DeleteFriend", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) SetFriendRemark(ctx context.Context, req *SetFriendRemarkRequest, opts ...client.CallOption) (*SetFriendRemarkResponse, error) {
resp := new(SetFriendRemarkResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "SetFriendRemark", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) CheckFriendship(ctx context.Context, req *CheckFriendshipRequest, opts ...client.CallOption) (*CheckFriendshipResponse, error) {
resp := new(CheckFriendshipResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "CheckFriendship", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) GetFriendCount(ctx context.Context, req *GetFriendCountRequest, opts ...client.CallOption) (*GetFriendCountResponse, error) {
resp := new(GetFriendCountResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetFriendCount", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) SearchUserForFriend(ctx context.Context, req *SearchUserForFriendRequest, opts ...client.CallOption) (*SearchUserForFriendResponse, error) {
resp := new(SearchUserForFriendResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "SearchUserForFriend", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) GetRandomUsers(ctx context.Context, req *GetRandomUsersRequest, opts ...client.CallOption) (*GetRandomUsersResponse, error) {
resp := new(GetRandomUsersResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetRandomUsers", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) GetUsersPaged(ctx context.Context, req *GetUsersPagedRequest, opts ...client.CallOption) (*GetUsersPagedResponse, error) {
resp := new(GetUsersPagedResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetUsersPaged", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) LikeAsset(ctx context.Context, req *LikeAssetRequest, opts ...client.CallOption) (*LikeAssetResponse, error) {
resp := new(LikeAssetResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "LikeAsset", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) UnlikeAsset(ctx context.Context, req *UnlikeAssetRequest, opts ...client.CallOption) (*UnlikeAssetResponse, error) {
resp := new(UnlikeAssetResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "UnlikeAsset", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) CheckAssetLike(ctx context.Context, req *CheckAssetLikeRequest, opts ...client.CallOption) (*CheckAssetLikeResponse, error) {
resp := new(CheckAssetLikeResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "CheckAssetLike", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) GetMyLikedAssets(ctx context.Context, req *GetMyLikedAssetsRequest, opts ...client.CallOption) (*GetMyLikedAssetsResponse, error) {
resp := new(GetMyLikedAssetsResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetMyLikedAssets", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) GetMyTodayLikedAssets(ctx context.Context, req *GetMyTodayLikedAssetsRequest, opts ...client.CallOption) (*GetMyTodayLikedAssetsResponse, error) {
resp := new(GetMyTodayLikedAssetsResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetMyTodayLikedAssets", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *SocialServiceImpl) GetMyWeekLikedAssets(ctx context.Context, req *GetMyWeekLikedAssetsRequest, opts ...client.CallOption) (*GetMyWeekLikedAssetsResponse, error) {
resp := new(GetMyWeekLikedAssetsResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetMyWeekLikedAssets", opts...); err != nil {
return nil, err
}
return resp, nil
}
var SocialService_ClientInfo = client.ClientInfo{
InterfaceName: "topfans.social.SocialService",
MethodNames: []string{"SendFriendRequest", "GetFriendRequests", "HandleFriendRequest", "GetFriendList", "DeleteFriend", "SetFriendRemark", "CheckFriendship", "GetFriendCount", "SearchUserForFriend", "GetRandomUsers", "GetUsersPaged", "LikeAsset", "UnlikeAsset", "CheckAssetLike", "GetMyLikedAssets", "GetMyTodayLikedAssets", "GetMyWeekLikedAssets"},
ConnectionInjectFunc: func(dubboCliRaw interface{}, conn *client.Connection) {
dubboCli := dubboCliRaw.(*SocialServiceImpl)
dubboCli.conn = conn
},
}
// SocialServiceHandler is an implementation of the topfans.social.SocialService service.
type SocialServiceHandler interface {
SendFriendRequest(context.Context, *SendFriendRequestRequest) (*SendFriendRequestResponse, error)
GetFriendRequests(context.Context, *GetFriendRequestsRequest) (*GetFriendRequestsResponse, error)
HandleFriendRequest(context.Context, *HandleFriendRequestRequest) (*HandleFriendRequestResponse, error)
GetFriendList(context.Context, *GetFriendListRequest) (*GetFriendListResponse, error)
DeleteFriend(context.Context, *DeleteFriendRequest) (*DeleteFriendResponse, error)
SetFriendRemark(context.Context, *SetFriendRemarkRequest) (*SetFriendRemarkResponse, error)
CheckFriendship(context.Context, *CheckFriendshipRequest) (*CheckFriendshipResponse, error)
GetFriendCount(context.Context, *GetFriendCountRequest) (*GetFriendCountResponse, error)
SearchUserForFriend(context.Context, *SearchUserForFriendRequest) (*SearchUserForFriendResponse, error)
GetRandomUsers(context.Context, *GetRandomUsersRequest) (*GetRandomUsersResponse, error)
GetUsersPaged(context.Context, *GetUsersPagedRequest) (*GetUsersPagedResponse, error)
LikeAsset(context.Context, *LikeAssetRequest) (*LikeAssetResponse, error)
UnlikeAsset(context.Context, *UnlikeAssetRequest) (*UnlikeAssetResponse, error)
CheckAssetLike(context.Context, *CheckAssetLikeRequest) (*CheckAssetLikeResponse, error)
GetMyLikedAssets(context.Context, *GetMyLikedAssetsRequest) (*GetMyLikedAssetsResponse, error)
GetMyTodayLikedAssets(context.Context, *GetMyTodayLikedAssetsRequest) (*GetMyTodayLikedAssetsResponse, error)
GetMyWeekLikedAssets(context.Context, *GetMyWeekLikedAssetsRequest) (*GetMyWeekLikedAssetsResponse, error)
}
func RegisterSocialServiceHandler(srv *server.Server, hdlr SocialServiceHandler, opts ...server.ServiceOption) error {
return srv.Register(hdlr, &SocialService_ServiceInfo, opts...)
}
func SetProviderSocialService(srv common.RPCService) {
dubbo.SetProviderServiceWithInfo(srv, &SocialService_ServiceInfo)
}
var SocialService_ServiceInfo = server.ServiceInfo{
InterfaceName: "topfans.social.SocialService",
ServiceType: (*SocialServiceHandler)(nil),
Methods: []server.MethodInfo{
{
Name: "SendFriendRequest",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(SendFriendRequestRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*SendFriendRequestRequest)
res, err := handler.(SocialServiceHandler).SendFriendRequest(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "GetFriendRequests",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetFriendRequestsRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetFriendRequestsRequest)
res, err := handler.(SocialServiceHandler).GetFriendRequests(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "HandleFriendRequest",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(HandleFriendRequestRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*HandleFriendRequestRequest)
res, err := handler.(SocialServiceHandler).HandleFriendRequest(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "GetFriendList",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetFriendListRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetFriendListRequest)
res, err := handler.(SocialServiceHandler).GetFriendList(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "DeleteFriend",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(DeleteFriendRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*DeleteFriendRequest)
res, err := handler.(SocialServiceHandler).DeleteFriend(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "SetFriendRemark",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(SetFriendRemarkRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*SetFriendRemarkRequest)
res, err := handler.(SocialServiceHandler).SetFriendRemark(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "CheckFriendship",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(CheckFriendshipRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*CheckFriendshipRequest)
res, err := handler.(SocialServiceHandler).CheckFriendship(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "GetFriendCount",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetFriendCountRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetFriendCountRequest)
res, err := handler.(SocialServiceHandler).GetFriendCount(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "SearchUserForFriend",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(SearchUserForFriendRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*SearchUserForFriendRequest)
res, err := handler.(SocialServiceHandler).SearchUserForFriend(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "GetRandomUsers",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetRandomUsersRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetRandomUsersRequest)
res, err := handler.(SocialServiceHandler).GetRandomUsers(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "GetUsersPaged",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetUsersPagedRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetUsersPagedRequest)
res, err := handler.(SocialServiceHandler).GetUsersPaged(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "LikeAsset",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(LikeAssetRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*LikeAssetRequest)
res, err := handler.(SocialServiceHandler).LikeAsset(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "UnlikeAsset",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(UnlikeAssetRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*UnlikeAssetRequest)
res, err := handler.(SocialServiceHandler).UnlikeAsset(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "CheckAssetLike",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(CheckAssetLikeRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*CheckAssetLikeRequest)
res, err := handler.(SocialServiceHandler).CheckAssetLike(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "GetMyLikedAssets",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetMyLikedAssetsRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetMyLikedAssetsRequest)
res, err := handler.(SocialServiceHandler).GetMyLikedAssets(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "GetMyTodayLikedAssets",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetMyTodayLikedAssetsRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetMyTodayLikedAssetsRequest)
res, err := handler.(SocialServiceHandler).GetMyTodayLikedAssets(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "GetMyWeekLikedAssets",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetMyWeekLikedAssetsRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetMyWeekLikedAssetsRequest)
res, err := handler.(SocialServiceHandler).GetMyWeekLikedAssets(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
},
}

View File

@ -1215,6 +1215,282 @@ func (x *ExhibitedAssetItem) GetEarnings() int64 {
return 0
}
// 获取灵感瀑布藏品列表请求
type GetInspirationFlowRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Cursor string `protobuf:"bytes,1,opt,name=cursor,proto3" json:"cursor,omitempty"` // 游标(首次请求为空)
Direction string `protobuf:"bytes,2,opt,name=direction,proto3" json:"direction,omitempty"` // 滚动方向right加载新数据/ left加载历史
Limit int32 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` // 每页数量默认10最大20
Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` // 过滤类型badge/poster/original/all默认all
SessionId string `protobuf:"bytes,5,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // 会话ID首次请求时为空后端返回新的session_id
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetInspirationFlowRequest) Reset() {
*x = GetInspirationFlowRequest{}
mi := &file_gallery_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetInspirationFlowRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetInspirationFlowRequest) ProtoMessage() {}
func (x *GetInspirationFlowRequest) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetInspirationFlowRequest.ProtoReflect.Descriptor instead.
func (*GetInspirationFlowRequest) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{20}
}
func (x *GetInspirationFlowRequest) GetCursor() string {
if x != nil {
return x.Cursor
}
return ""
}
func (x *GetInspirationFlowRequest) GetDirection() string {
if x != nil {
return x.Direction
}
return ""
}
func (x *GetInspirationFlowRequest) GetLimit() int32 {
if x != nil {
return x.Limit
}
return 0
}
func (x *GetInspirationFlowRequest) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *GetInspirationFlowRequest) GetSessionId() string {
if x != nil {
return x.SessionId
}
return ""
}
// 获取灵感瀑布藏品列表响应
type GetInspirationFlowResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
Data *InspirationFlowData `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetInspirationFlowResponse) Reset() {
*x = GetInspirationFlowResponse{}
mi := &file_gallery_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetInspirationFlowResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetInspirationFlowResponse) ProtoMessage() {}
func (x *GetInspirationFlowResponse) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetInspirationFlowResponse.ProtoReflect.Descriptor instead.
func (*GetInspirationFlowResponse) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{21}
}
func (x *GetInspirationFlowResponse) GetBase() *common.BaseResponse {
if x != nil {
return x.Base
}
return nil
}
func (x *GetInspirationFlowResponse) GetData() *InspirationFlowData {
if x != nil {
return x.Data
}
return nil
}
// 灵感瀑布数据
type InspirationFlowData struct {
state protoimpl.MessageState `protogen:"open.v1"`
Items []*InspirationFlowItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` // 藏品列表
Cursor string `protobuf:"bytes,2,opt,name=cursor,proto3" json:"cursor,omitempty"` // 下次请求的游标
HasMore bool `protobuf:"varint,3,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` // 是否有更多
SessionId string `protobuf:"bytes,4,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` // 会话ID首次请求时返回
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *InspirationFlowData) Reset() {
*x = InspirationFlowData{}
mi := &file_gallery_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *InspirationFlowData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*InspirationFlowData) ProtoMessage() {}
func (x *InspirationFlowData) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use InspirationFlowData.ProtoReflect.Descriptor instead.
func (*InspirationFlowData) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{22}
}
func (x *InspirationFlowData) GetItems() []*InspirationFlowItem {
if x != nil {
return x.Items
}
return nil
}
func (x *InspirationFlowData) GetCursor() string {
if x != nil {
return x.Cursor
}
return ""
}
func (x *InspirationFlowData) GetHasMore() bool {
if x != nil {
return x.HasMore
}
return false
}
func (x *InspirationFlowData) GetSessionId() string {
if x != nil {
return x.SessionId
}
return ""
}
// 灵感瀑布藏品项
type InspirationFlowItem struct {
state protoimpl.MessageState `protogen:"open.v1"`
AssetId int64 `protobuf:"varint,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // 资产ID
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // 藏品名称
CoverUrl string `protobuf:"bytes,3,opt,name=cover_url,json=coverUrl,proto3" json:"cover_url,omitempty"` // 封面图URL
LikeCount int32 `protobuf:"varint,4,opt,name=like_count,json=likeCount,proto3" json:"like_count,omitempty"` // 点赞数
OwnerNickname string `protobuf:"bytes,5,opt,name=owner_nickname,json=ownerNickname,proto3" json:"owner_nickname,omitempty"` // 展出者昵称
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *InspirationFlowItem) Reset() {
*x = InspirationFlowItem{}
mi := &file_gallery_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *InspirationFlowItem) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*InspirationFlowItem) ProtoMessage() {}
func (x *InspirationFlowItem) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use InspirationFlowItem.ProtoReflect.Descriptor instead.
func (*InspirationFlowItem) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{23}
}
func (x *InspirationFlowItem) GetAssetId() int64 {
if x != nil {
return x.AssetId
}
return 0
}
func (x *InspirationFlowItem) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *InspirationFlowItem) GetCoverUrl() string {
if x != nil {
return x.CoverUrl
}
return ""
}
func (x *InspirationFlowItem) GetLikeCount() int32 {
if x != nil {
return x.LikeCount
}
return 0
}
func (x *InspirationFlowItem) GetOwnerNickname() string {
if x != nil {
return x.OwnerNickname
}
return ""
}
var File_gallery_proto protoreflect.FileDescriptor
const file_gallery_proto_rawDesc = "" +
@ -1310,7 +1586,30 @@ const file_gallery_proto_rawDesc = "" +
"like_count\x18\x04 \x01(\x05R\tlikeCount\x12!\n" +
"\fexhibited_at\x18\x05 \x01(\x03R\vexhibitedAt\x12\x1b\n" +
"\texpire_at\x18\x06 \x01(\x03R\bexpireAt\x12\x1a\n" +
"\bearnings\x18\a \x01(\x03R\bearnings2\xb4\x06\n" +
"\bearnings\x18\a \x01(\x03R\bearnings\"\x9a\x01\n" +
"\x19GetInspirationFlowRequest\x12\x16\n" +
"\x06cursor\x18\x01 \x01(\tR\x06cursor\x12\x1c\n" +
"\tdirection\x18\x02 \x01(\tR\tdirection\x12\x14\n" +
"\x05limit\x18\x03 \x01(\x05R\x05limit\x12\x12\n" +
"\x04type\x18\x04 \x01(\tR\x04type\x12\x1d\n" +
"\n" +
"session_id\x18\x05 \x01(\tR\tsessionId\"\x88\x01\n" +
"\x1aGetInspirationFlowResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x128\n" +
"\x04data\x18\x02 \x01(\v2$.topfans.gallery.InspirationFlowDataR\x04data\"\xa3\x01\n" +
"\x13InspirationFlowData\x12:\n" +
"\x05items\x18\x01 \x03(\v2$.topfans.gallery.InspirationFlowItemR\x05items\x12\x16\n" +
"\x06cursor\x18\x02 \x01(\tR\x06cursor\x12\x19\n" +
"\bhas_more\x18\x03 \x01(\bR\ahasMore\x12\x1d\n" +
"\n" +
"session_id\x18\x04 \x01(\tR\tsessionId\"\xa7\x01\n" +
"\x13InspirationFlowItem\x12\x19\n" +
"\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" +
"\tcover_url\x18\x03 \x01(\tR\bcoverUrl\x12\x1d\n" +
"\n" +
"like_count\x18\x04 \x01(\x05R\tlikeCount\x12%\n" +
"\x0eowner_nickname\x18\x05 \x01(\tR\rownerNickname2\xc6\a\n" +
"\x0eGalleryService\x12u\n" +
"\fGetMyGallery\x12$.topfans.gallery.GetMyGalleryRequest\x1a%.topfans.gallery.GetMyGalleryResponse\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/api/mygalleries\x12\x86\x01\n" +
"\x0eGetUserGallery\x12&.topfans.gallery.GetUserGalleryRequest\x1a'.topfans.gallery.GetUserGalleryResponse\"#\x82\xd3\xe4\x93\x02\x1d\x12\x1b/api/galleries/{target_uid}\x12v\n" +
@ -1319,7 +1618,8 @@ const file_gallery_proto_rawDesc = "" +
"\n" +
"UnlockSlot\x12\".topfans.gallery.UnlockSlotRequest\x1a#.topfans.gallery.UnlockSlotResponse\"&\x82\xd3\xe4\x93\x02 :\x01*\"\x1b/api/galleries/slots_unlock\x12\x8f\x01\n" +
"\x0eRemoveFromSlot\x12&.topfans.gallery.RemoveFromSlotRequest\x1a'.topfans.gallery.RemoveFromSlotResponse\",\x82\xd3\xe4\x93\x02&*$/api/galleries/slots/{slot_id}/asset\x12\x98\x01\n" +
"\x14GetMyExhibitedAssets\x12,.topfans.gallery.GetMyExhibitedAssetsRequest\x1a-.topfans.gallery.GetMyExhibitedAssetsResponse\"#\x82\xd3\xe4\x93\x02\x1d\x12\x1b/api/v1/me/exhibited-assetsB6Z4github.com/topfans/backend/pkg/proto/gallery;galleryb\x06proto3"
"\x14GetMyExhibitedAssets\x12,.topfans.gallery.GetMyExhibitedAssetsRequest\x1a-.topfans.gallery.GetMyExhibitedAssetsResponse\"#\x82\xd3\xe4\x93\x02\x1d\x12\x1b/api/v1/me/exhibited-assets\x12\x8f\x01\n" +
"\x12GetInspirationFlow\x12*.topfans.gallery.GetInspirationFlowRequest\x1a+.topfans.gallery.GetInspirationFlowResponse\" \x82\xd3\xe4\x93\x02\x1a\x12\x18/api/v1/inspiration-flowB6Z4github.com/topfans/backend/pkg/proto/gallery;galleryb\x06proto3"
var (
file_gallery_proto_rawDescOnce sync.Once
@ -1333,7 +1633,7 @@ func file_gallery_proto_rawDescGZIP() []byte {
return file_gallery_proto_rawDescData
}
var file_gallery_proto_msgTypes = make([]protoimpl.MessageInfo, 20)
var file_gallery_proto_msgTypes = make([]protoimpl.MessageInfo, 24)
var file_gallery_proto_goTypes = []any{
(*GetMyGalleryRequest)(nil), // 0: topfans.gallery.GetMyGalleryRequest
(*GetMyGalleryResponse)(nil), // 1: topfans.gallery.GetMyGalleryResponse
@ -1355,41 +1655,50 @@ var file_gallery_proto_goTypes = []any{
(*GetMyExhibitedAssetsResponse)(nil), // 17: topfans.gallery.GetMyExhibitedAssetsResponse
(*ExhibitedAssetsData)(nil), // 18: topfans.gallery.ExhibitedAssetsData
(*ExhibitedAssetItem)(nil), // 19: topfans.gallery.ExhibitedAssetItem
(*common.BaseResponse)(nil), // 20: topfans.common.BaseResponse
(*GetInspirationFlowRequest)(nil), // 20: topfans.gallery.GetInspirationFlowRequest
(*GetInspirationFlowResponse)(nil), // 21: topfans.gallery.GetInspirationFlowResponse
(*InspirationFlowData)(nil), // 22: topfans.gallery.InspirationFlowData
(*InspirationFlowItem)(nil), // 23: topfans.gallery.InspirationFlowItem
(*common.BaseResponse)(nil), // 24: topfans.common.BaseResponse
}
var file_gallery_proto_depIdxs = []int32{
20, // 0: topfans.gallery.GetMyGalleryResponse.base:type_name -> topfans.common.BaseResponse
24, // 0: topfans.gallery.GetMyGalleryResponse.base:type_name -> topfans.common.BaseResponse
8, // 1: topfans.gallery.GetMyGalleryResponse.data:type_name -> topfans.gallery.GalleryData
20, // 2: topfans.gallery.GetUserGalleryResponse.base:type_name -> topfans.common.BaseResponse
24, // 2: topfans.gallery.GetUserGalleryResponse.base:type_name -> topfans.common.BaseResponse
8, // 3: topfans.gallery.GetUserGalleryResponse.data:type_name -> topfans.gallery.GalleryData
20, // 4: topfans.gallery.PlaceAssetResponse.base:type_name -> topfans.common.BaseResponse
24, // 4: topfans.gallery.PlaceAssetResponse.base:type_name -> topfans.common.BaseResponse
12, // 5: topfans.gallery.PlaceAssetResponse.data:type_name -> topfans.gallery.PlaceAssetData
20, // 6: topfans.gallery.UnlockSlotResponse.base:type_name -> topfans.common.BaseResponse
24, // 6: topfans.gallery.UnlockSlotResponse.base:type_name -> topfans.common.BaseResponse
13, // 7: topfans.gallery.UnlockSlotResponse.data:type_name -> topfans.gallery.UnlockSlotData
9, // 8: topfans.gallery.GalleryData.slots:type_name -> topfans.gallery.SlotInfo
10, // 9: topfans.gallery.SlotInfo.asset:type_name -> topfans.gallery.AssetInfo
11, // 10: topfans.gallery.SlotInfo.unlock_condition:type_name -> topfans.gallery.UnlockCondition
20, // 11: topfans.gallery.RemoveFromSlotResponse.base:type_name -> topfans.common.BaseResponse
20, // 12: topfans.gallery.GetMyExhibitedAssetsResponse.base:type_name -> topfans.common.BaseResponse
24, // 11: topfans.gallery.RemoveFromSlotResponse.base:type_name -> topfans.common.BaseResponse
24, // 12: topfans.gallery.GetMyExhibitedAssetsResponse.base:type_name -> topfans.common.BaseResponse
18, // 13: topfans.gallery.GetMyExhibitedAssetsResponse.data:type_name -> topfans.gallery.ExhibitedAssetsData
19, // 14: topfans.gallery.ExhibitedAssetsData.items:type_name -> topfans.gallery.ExhibitedAssetItem
0, // 15: topfans.gallery.GalleryService.GetMyGallery:input_type -> topfans.gallery.GetMyGalleryRequest
2, // 16: topfans.gallery.GalleryService.GetUserGallery:input_type -> topfans.gallery.GetUserGalleryRequest
4, // 17: topfans.gallery.GalleryService.PlaceAsset:input_type -> topfans.gallery.PlaceAssetRequest
6, // 18: topfans.gallery.GalleryService.UnlockSlot:input_type -> topfans.gallery.UnlockSlotRequest
14, // 19: topfans.gallery.GalleryService.RemoveFromSlot:input_type -> topfans.gallery.RemoveFromSlotRequest
16, // 20: topfans.gallery.GalleryService.GetMyExhibitedAssets:input_type -> topfans.gallery.GetMyExhibitedAssetsRequest
1, // 21: topfans.gallery.GalleryService.GetMyGallery:output_type -> topfans.gallery.GetMyGalleryResponse
3, // 22: topfans.gallery.GalleryService.GetUserGallery:output_type -> topfans.gallery.GetUserGalleryResponse
5, // 23: topfans.gallery.GalleryService.PlaceAsset:output_type -> topfans.gallery.PlaceAssetResponse
7, // 24: topfans.gallery.GalleryService.UnlockSlot:output_type -> topfans.gallery.UnlockSlotResponse
15, // 25: topfans.gallery.GalleryService.RemoveFromSlot:output_type -> topfans.gallery.RemoveFromSlotResponse
17, // 26: topfans.gallery.GalleryService.GetMyExhibitedAssets:output_type -> topfans.gallery.GetMyExhibitedAssetsResponse
21, // [21:27] is the sub-list for method output_type
15, // [15:21] is the sub-list for method input_type
15, // [15:15] is the sub-list for extension type_name
15, // [15:15] is the sub-list for extension extendee
0, // [0:15] is the sub-list for field type_name
24, // 15: topfans.gallery.GetInspirationFlowResponse.base:type_name -> topfans.common.BaseResponse
22, // 16: topfans.gallery.GetInspirationFlowResponse.data:type_name -> topfans.gallery.InspirationFlowData
23, // 17: topfans.gallery.InspirationFlowData.items:type_name -> topfans.gallery.InspirationFlowItem
0, // 18: topfans.gallery.GalleryService.GetMyGallery:input_type -> topfans.gallery.GetMyGalleryRequest
2, // 19: topfans.gallery.GalleryService.GetUserGallery:input_type -> topfans.gallery.GetUserGalleryRequest
4, // 20: topfans.gallery.GalleryService.PlaceAsset:input_type -> topfans.gallery.PlaceAssetRequest
6, // 21: topfans.gallery.GalleryService.UnlockSlot:input_type -> topfans.gallery.UnlockSlotRequest
14, // 22: topfans.gallery.GalleryService.RemoveFromSlot:input_type -> topfans.gallery.RemoveFromSlotRequest
16, // 23: topfans.gallery.GalleryService.GetMyExhibitedAssets:input_type -> topfans.gallery.GetMyExhibitedAssetsRequest
20, // 24: topfans.gallery.GalleryService.GetInspirationFlow:input_type -> topfans.gallery.GetInspirationFlowRequest
1, // 25: topfans.gallery.GalleryService.GetMyGallery:output_type -> topfans.gallery.GetMyGalleryResponse
3, // 26: topfans.gallery.GalleryService.GetUserGallery:output_type -> topfans.gallery.GetUserGalleryResponse
5, // 27: topfans.gallery.GalleryService.PlaceAsset:output_type -> topfans.gallery.PlaceAssetResponse
7, // 28: topfans.gallery.GalleryService.UnlockSlot:output_type -> topfans.gallery.UnlockSlotResponse
15, // 29: topfans.gallery.GalleryService.RemoveFromSlot:output_type -> topfans.gallery.RemoveFromSlotResponse
17, // 30: topfans.gallery.GalleryService.GetMyExhibitedAssets:output_type -> topfans.gallery.GetMyExhibitedAssetsResponse
21, // 31: topfans.gallery.GalleryService.GetInspirationFlow:output_type -> topfans.gallery.GetInspirationFlowResponse
25, // [25:32] is the sub-list for method output_type
18, // [18:25] is the sub-list for method input_type
18, // [18:18] is the sub-list for extension type_name
18, // [18:18] is the sub-list for extension extendee
0, // [0:18] is the sub-list for field type_name
}
func init() { file_gallery_proto_init() }
@ -1403,7 +1712,7 @@ func file_gallery_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_gallery_proto_rawDesc), len(file_gallery_proto_rawDesc)),
NumEnums: 0,
NumMessages: 20,
NumMessages: 24,
NumExtensions: 0,
NumServices: 1,
},

View File

@ -48,6 +48,8 @@ const (
GalleryServiceRemoveFromSlotProcedure = "/topfans.gallery.GalleryService/RemoveFromSlot"
// GalleryServiceGetMyExhibitedAssetsProcedure is the fully-qualified name of the GalleryService's GetMyExhibitedAssets RPC.
GalleryServiceGetMyExhibitedAssetsProcedure = "/topfans.gallery.GalleryService/GetMyExhibitedAssets"
// GalleryServiceGetInspirationFlowProcedure is the fully-qualified name of the GalleryService's GetInspirationFlow RPC.
GalleryServiceGetInspirationFlowProcedure = "/topfans.gallery.GalleryService/GetInspirationFlow"
)
var (
@ -62,6 +64,7 @@ type GalleryService interface {
UnlockSlot(ctx context.Context, req *UnlockSlotRequest, opts ...client.CallOption) (*UnlockSlotResponse, error)
RemoveFromSlot(ctx context.Context, req *RemoveFromSlotRequest, opts ...client.CallOption) (*RemoveFromSlotResponse, error)
GetMyExhibitedAssets(ctx context.Context, req *GetMyExhibitedAssetsRequest, opts ...client.CallOption) (*GetMyExhibitedAssetsResponse, error)
GetInspirationFlow(ctx context.Context, req *GetInspirationFlowRequest, opts ...client.CallOption) (*GetInspirationFlowResponse, error)
}
// NewGalleryService constructs a client for the gallery.GalleryService service.
@ -132,9 +135,17 @@ func (c *GalleryServiceImpl) GetMyExhibitedAssets(ctx context.Context, req *GetM
return resp, nil
}
func (c *GalleryServiceImpl) GetInspirationFlow(ctx context.Context, req *GetInspirationFlowRequest, opts ...client.CallOption) (*GetInspirationFlowResponse, error) {
resp := new(GetInspirationFlowResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetInspirationFlow", opts...); err != nil {
return nil, err
}
return resp, nil
}
var GalleryService_ClientInfo = client.ClientInfo{
InterfaceName: "topfans.gallery.GalleryService",
MethodNames: []string{"GetMyGallery", "GetUserGallery", "PlaceAsset", "UnlockSlot", "RemoveFromSlot", "GetMyExhibitedAssets"},
MethodNames: []string{"GetMyGallery", "GetUserGallery", "PlaceAsset", "UnlockSlot", "RemoveFromSlot", "GetMyExhibitedAssets", "GetInspirationFlow"},
ConnectionInjectFunc: func(dubboCliRaw interface{}, conn *client.Connection) {
dubboCli := dubboCliRaw.(*GalleryServiceImpl)
dubboCli.conn = conn
@ -149,6 +160,7 @@ type GalleryServiceHandler interface {
UnlockSlot(context.Context, *UnlockSlotRequest) (*UnlockSlotResponse, error)
RemoveFromSlot(context.Context, *RemoveFromSlotRequest) (*RemoveFromSlotResponse, error)
GetMyExhibitedAssets(context.Context, *GetMyExhibitedAssetsRequest) (*GetMyExhibitedAssetsResponse, error)
GetInspirationFlow(context.Context, *GetInspirationFlowRequest) (*GetInspirationFlowResponse, error)
}
func RegisterGalleryServiceHandler(srv *server.Server, hdlr GalleryServiceHandler, opts ...server.ServiceOption) error {
@ -253,5 +265,20 @@ var GalleryService_ServiceInfo = server.ServiceInfo{
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "GetInspirationFlow",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(GetInspirationFlowRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*GetInspirationFlowRequest)
res, err := handler.(GalleryServiceHandler).GetInspirationFlow(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
},
}

View File

@ -605,6 +605,7 @@ type OnboardingStage struct {
Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"` // pending/completed/in_progress
IsCurrent bool `protobuf:"varint,7,opt,name=is_current,json=isCurrent,proto3" json:"is_current,omitempty"`
AllTasksCompleted bool `protobuf:"varint,8,opt,name=all_tasks_completed,json=allTasksCompleted,proto3" json:"all_tasks_completed,omitempty"` // 该阶段所有任务是否完成
IsRewardClaimed bool `protobuf:"varint,9,opt,name=is_reward_claimed,json=isRewardClaimed,proto3" json:"is_reward_claimed,omitempty"` // 该阶段奖励是否已领取
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -695,6 +696,13 @@ func (x *OnboardingStage) GetAllTasksCompleted() bool {
return false
}
func (x *OnboardingStage) GetIsRewardClaimed() bool {
if x != nil {
return x.IsRewardClaimed
}
return false
}
type CompleteGuideRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
TaskKey string `protobuf:"bytes,1,opt,name=task_key,json=taskKey,proto3" json:"task_key,omitempty"`
@ -1942,7 +1950,7 @@ const file_task_proto_rawDesc = "" +
"\n" +
"experience\x18\x04 \x01(\x03R\n" +
"experience\x12*\n" +
"\x11claimed_task_keys\x18\x05 \x03(\tR\x0fclaimedTaskKeys\"\x96\x02\n" +
"\x11claimed_task_keys\x18\x05 \x03(\tR\x0fclaimedTaskKeys\"\xc2\x02\n" +
"\x0fOnboardingStage\x12\x14\n" +
"\x05stage\x18\x01 \x01(\x05R\x05stage\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12,\n" +
@ -1953,7 +1961,8 @@ const file_task_proto_rawDesc = "" +
"\x06status\x18\x06 \x01(\tR\x06status\x12\x1d\n" +
"\n" +
"is_current\x18\a \x01(\bR\tisCurrent\x12.\n" +
"\x13all_tasks_completed\x18\b \x01(\bR\x11allTasksCompleted\"h\n" +
"\x13all_tasks_completed\x18\b \x01(\bR\x11allTasksCompleted\x12*\n" +
"\x11is_reward_claimed\x18\t \x01(\bR\x0fisRewardClaimed\"h\n" +
"\x14CompleteGuideRequest\x12\x19\n" +
"\btask_key\x18\x01 \x01(\tR\ataskKey\x125\n" +
"\x06stages\x18\x02 \x03(\v2\x1d.topfans.task.OnboardingStageR\x06stages\"\xd6\x01\n" +

View File

@ -54,6 +54,13 @@ service GalleryService {
get: "/api/v1/me/exhibited-assets"
};
}
//
rpc GetInspirationFlow(GetInspirationFlowRequest) returns (GetInspirationFlowResponse) {
option (google.api.http) = {
get: "/api/v1/inspiration-flow"
};
}
}
//
@ -181,3 +188,37 @@ message ExhibitedAssetItem {
int64 expire_at = 6; //
int64 earnings = 7; //
}
// ==================== ====================
//
message GetInspirationFlowRequest {
string cursor = 1; //
string direction = 2; // right/ left
int32 limit = 3; // 1020
string type = 4; // badge/poster/original/allall
string session_id = 5; // IDsession_id
}
//
message GetInspirationFlowResponse {
topfans.common.BaseResponse base = 1;
InspirationFlowData data = 2;
}
//
message InspirationFlowData {
repeated InspirationFlowItem items = 1; //
string cursor = 2; //
bool has_more = 3; //
string session_id = 4; // ID
}
//
message InspirationFlowItem {
int64 asset_id = 1; // ID
string name = 2; //
string cover_url = 3; // URL
int32 like_count = 4; //
string owner_nickname = 5; //
}

View File

@ -338,6 +338,64 @@ func (p *GalleryProvider) GetMyExhibitedAssets(ctx context.Context, req *pb.GetM
return p.exhibitionService.GetMyExhibitedAssets(ctx, userID, starID, req)
}
// GetInspirationFlow 获取灵感瀑布藏品列表
func (p *GalleryProvider) GetInspirationFlow(ctx context.Context, req *pb.GetInspirationFlowRequest) (*pb.GetInspirationFlowResponse, error) {
logger.Logger.Info("Received GetInspirationFlow request",
zap.String("cursor", req.Cursor),
zap.String("direction", req.Direction),
zap.Int32("limit", req.Limit),
zap.String("type", req.Type),
)
// 从 Dubbo attachments 获取用户信息(网关已验证并传递)
userID, starID, err := extractUserInfoFromDubboAttachments(ctx)
if err != nil {
logger.Logger.Error("Failed to extract user info from attachments",
zap.Error(err),
)
return &pb.GetInspirationFlowResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_UNAUTHORIZED,
Message: "user authentication required",
Timestamp: 0,
},
}, err
}
// 调用Service层
data, _, err := p.galleryService.GetInspirationFlow(userID, starID, req.Cursor, req.Direction, req.Limit, req.Type, req.SessionId)
if err != nil {
logger.Logger.Error("GetInspirationFlow failed",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.String("direction", req.Direction),
zap.Error(err),
)
return &pb.GetInspirationFlowResponse{
Base: &pbCommon.BaseResponse{
Code: appErrors.ToStatusCode(err),
Message: err.Error(),
Timestamp: 0,
},
}, err
}
logger.Logger.Info("GetInspirationFlow successful",
zap.Int64("user_id", userID),
zap.Int64("star_id", starID),
zap.Int("item_count", len(data.Items)),
)
return &pb.GetInspirationFlowResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "success",
Timestamp: 0,
},
Data: data,
}, nil
}
// ==================== 辅助函数 ====================
// extractUserInfoFromDubboAttachments 从Dubbo attachments提取用户信息

View File

@ -41,6 +41,30 @@ type GalleryRepository interface {
// pageSize: 每页数量
// 返回: 作品列表、总数量
GetMyExhibitedAssets(userID, starID int64, page, pageSize int) ([]*ExhibitedAssetInfo, int64, error)
// ========== 灵感瀑布相关 ==========
// CountValidExhibitions 统计有效展品数量
// starID: 明星ID
// materialType: 素材类型过滤(空字符串表示不过滤)
CountValidExhibitions(starID int64, materialType string) (int64, error)
// GetRandomExhibitions 获取随机展品列表
// starID: 明星ID
// materialType: 素材类型过滤(空字符串表示不过滤)
// excludeIDs: 排除的展品ID列表用于去重
// limit: 返回数量
// offset: 偏移量(随机生成)
GetRandomExhibitions(starID int64, materialType string, excludeIDs []int64, limit, offset int) ([]*InspirationFlowItem, error)
}
// InspirationFlowItem 灵感瀑布展品项
type InspirationFlowItem struct {
AssetID int64
Name string
CoverURL string
LikeCount int32
OwnerNickname string
}
// ExhibitedAssetInfo 我展出的作品信息
@ -255,18 +279,19 @@ func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pag
// 数据查询
offset := (page - 1) * pageSize
err = r.db.Model(&models.Exhibition{}).
Select(`exhibitions.asset_id, a.name, a.cover_url, a.like_count,
Raw(`
SELECT exhibitions.asset_id, a.name, a.cover_url, a.like_count,
exhibitions.start_time as exhibited_at, exhibitions.expire_at,
COALESCE(SUM(err.crystal_amount), 0) as earnings`).
Joins("JOIN assets a ON a.id = exhibitions.asset_id").
Joins("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 = ?", userID, starID).
Where("exhibitions.deleted_at IS NULL AND exhibitions.expire_at > ?", now).
Group("exhibitions.asset_id, a.name, a.cover_url, a.like_count, exhibitions.start_time, exhibitions.expire_at").
Order("exhibitions.start_time DESC").
Limit(pageSize).
Offset(offset).
Scan(&items).Error
COALESCE(CAST(SUM(err.crystal_amount) / 10 AS bigint), 0) as earnings
FROM exhibitions
JOIN assets a ON a.id = exhibitions.asset_id
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 = ?
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
ORDER BY exhibitions.start_time DESC
LIMIT ? OFFSET ?
`, userID, starID, now, pageSize, offset).Scan(&items).Error
if err != nil {
return nil, 0, err
@ -275,6 +300,62 @@ func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pag
return items, total, nil
}
// ========== 灵感瀑布相关实现 ==========
// CountValidExhibitions 统计有效展品数量
func (r *galleryRepository) CountValidExhibitions(starID int64, materialType string) (int64, error) {
var count int64
now := time.Now().UnixMilli()
query := r.db.Model(&models.Exhibition{}).
Where("occupier_star_id = ? AND expire_at > ? AND deleted_at IS NULL", starID, now)
if materialType != "" && materialType != "all" {
query = query.Joins("JOIN assets a ON a.id = exhibitions.asset_id").
Where("a.material_type = ?", materialType)
}
err := query.Count(&count).Error
return count, err
}
// GetRandomExhibitions 获取随机展品列表
func (r *galleryRepository) GetRandomExhibitions(starID int64, materialType string, excludeIDs []int64, limit, offset int) ([]*InspirationFlowItem, error) {
var items []*InspirationFlowItem
now := time.Now().UnixMilli()
// 构建基础查询
baseQuery := r.db.Model(&models.Exhibition{}).
Where("exhibitions.occupier_star_id = ? AND exhibitions.expire_at > ? AND exhibitions.deleted_at IS NULL", starID, now)
if materialType != "" && materialType != "all" {
baseQuery = baseQuery.Joins("JOIN assets a ON a.id = exhibitions.asset_id").
Where("a.material_type = ?", materialType)
}
// 排除已展示的ID
if len(excludeIDs) > 0 {
baseQuery = baseQuery.Where("exhibitions.id NOT IN ?", excludeIDs)
}
// 执行随机排序查询
err := baseQuery.
Select(`exhibitions.asset_id, a.name, a.cover_url, a.like_count, fp.nickname as owner_nickname`).
Joins("JOIN assets a ON a.id = exhibitions.asset_id").
Joins("JOIN fan_profiles fp ON exhibitions.occupier_uid = fp.user_id AND exhibitions.occupier_star_id = fp.star_id").
Where("a.status = 1 AND a.is_active = true").
Order("RANDOM()").
Limit(limit).
Offset(offset).
Scan(&items).Error
if err != nil {
return nil, err
}
return items, nil
}
// ==================== 辅助函数 ====================
// generateHostProfileID 生成 host_profile_id

View File

@ -695,3 +695,104 @@ onDataLoaded() {
| 2026-04-29 | 改用随机 offset 方案(方案 B每次请求都是独立随机数据变化无影响 |
| 2026-04-29 | 新增双向滚动支持:向右加载新数据,向左加载历史数据,后端维护会话级缓存 |
| 2026-04-29 | 修复:修正重复的 10.4 章节、RangeSamplingStrategy 签名、游标结构说明 |
| 2026-04-29 | 新增 Redis 会话级缓存实现方案:支持双向滚动去重 |
---
## 十三、Redis 会话级缓存实现
### 13.1 技术选型
- **客户端**: `github.com/redis/go-redis/v9`
- **连接信息**: `localhost:6379`,无密码
- **TTL**: 30分钟无操作自动清理
### 13.2 缓存结构
```
Key: inspiration_flow:{star_id}:{session_id}
Type: Hash
Fields:
- displayed_ids: ["id1", "id2", ...] # 已展示ID列表用于去重
- history: {"id1": json_data1, "id2": json_data2, ...} # 历史数据详情
TTL: 1800秒30分钟
```
### 13.3 环境变量配置
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| REDIS_HOST | Redis 主机地址 | 127.0.0.1 |
| REDIS_PORT | Redis 端口 | 6379 |
| REDIS_PASSWORD | Redis 密码 | (空) |
| REDIS_DB | Redis 数据库编号 | 0 |
### 13.4 核心逻辑
| 方向 | 行为 |
|------|------|
| `direction=right` | 随机查询新数据排除已展示ID返回并更新缓存 |
| `direction=left` | 从缓存的历史数据中分页返回 |
### 13.5 实现文件
| 文件 | 说明 |
|------|------|
| `backend/pkg/database/redis.go`(新建) | Redis 客户端初始化 |
| `backend/services/socialService/repository/social_repository.go` | 新增 `GetRandomUsersExcludeIDs` |
| `backend/services/socialService/service/friend_service.go` | 修改 `GetRandomUsers` 支持 direction + 缓存 |
| `backend/services/socialService/provider/social_provider.go` | 透传 direction 参数 |
| `backend/gateway/dto/social_converter.go` | 转换 exclude_ids |
| `backend/gateway/config/config.go` | 新增 Redis 配置 |
### 13.6 session_id 生成
- 由后端生成UUID
- 首次请求时返回给前端,前端后续请求携带
### 13.7 向左滚动实现
向左滚动时,后端从 Redis 缓存的 `history` 字段读取已展示数据,按 offset 分页返回。前端在左侧插入展示。
### 13.8 预签名 URL 批量获取优化
**问题**:前端逐个获取预签名 URLN 个卡片产生 N 次请求。
**解决方案**:新增批量接口,前端一次性获取所有 URL。
**接口设计**
```
POST /api/v1/assets/oss/batch-presigned-urls
Content-Type: application/json
Request:
{
"files": ["path/to/img1.png", "path/to/img2.png", ...],
"expires": 3600,
"type": "asset"
}
Response:
{
"code": 200,
"data": {
"urls": {
"path/to/img1.png": "https://xxx?signature=...",
"path/to/img2.png": "https://xxx?signature=..."
}
}
}
```
**前端逻辑**
1. 加载用户数据后,收集所有 `cover_url`
2. 批量调用接口获取全部预签名 URL
3. 存入 Map 缓存,后续直接使用
**实现文件**
| 文件 | 说明 |
|------|------|
| `backend/gateway/controller/asset_controller.go` | 新增 batch presigned urls 路由 |
| `backend/gateway/dto/asset_dto.go` | 新增 BatchPresignedUrlsRequest/Response |
| `frontend/utils/api.js` | 新增 `getBatchOssPresignedUrlsApi` |
| `frontend/pages/square/components/WaterfallGrid.vue` | 使用批量接口替代逐个调用 |

View File

@ -22,18 +22,18 @@
</view>
<!-- 新手引导 -->
<view v-if="showGuideIcon" class="daily-task-group" @click="handleGuideClick">
<!-- 1. 上层新手引导悬浮在上面 -->
<!-- <view v-if="showGuideIcon" class="daily-task-group" @click="handleGuideClick">
1. 上层新手引导悬浮在上面
<view class="task-icon-box">
<image class="task-icon-img" src="/static/icon/onboarding-bg.png" mode="aspectFit"></image>
<image class="task-red-dot" src="/static/square/tishi.png" mode="aspectFit"></image>
</view>
<!-- 2. 下层文字背景块 -->
2. 下层文字背景块
<view class="task-text-box">
<text class="task-text-label">新手引导</text>
</view>
</view>
</view> -->
<!-- 星援活动 -->
<view v-if="showStarActivityIcon" class="daily-task-group" @click="handleStarActivityClick">
@ -388,13 +388,13 @@ const handleAvatarClick = () => {
if (pages.length > 0) {
const currentPage = pages[pages.length - 1];
//
if (currentPage.route === 'pages/profile/profile') {
if (currentPage.route === 'pages/profile/myWorks') {
//
return;
}
}
uni.navigateTo({
url: '/pages/profile/profile'
url: '/pages/profile/myWorks'
});
};
@ -570,7 +570,7 @@ defineExpose({
}
.balance-number {
font-size: 22rpx;
font-size: 24rpx;
font-weight: bold;
color: #FFB800;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;

View File

@ -10,6 +10,9 @@
</view>
<!-- <text class="nav-title">我的作品</text> -->
<view class="nav-placeholder"></view>
<view class="nav-settings" @tap="goToSettings">
<text class="nav-settings-text">个人设置</text>
</view>
</view>
<view class="scroll-content">
@ -22,15 +25,13 @@
</view>
<view class="exhibition-grid">
<view
v-for="(item, index) in exhibitionWorks"
:key="item.id"
class="exhibition-card"
<view v-for="(item, index) in exhibitionWorks" :key="item.id" class="exhibition-card"
:class="index % 2 === 0 ? 'card-tilt-left' : 'card-tilt-right'"
@tap="handleExhibitionCardTap(item, index)"
>
<image class="card-image" :src="item.cover_url || '/static/nft/placeholder.png'" mode="aspectFill"></image>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill"></image>
@tap="handleExhibitionCardTap(item, index)">
<image class="card-image" :src="item.cover_url || '/static/nft/placeholder.png'"
mode="aspectFill"></image>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
</image>
<!-- 点赞数 -->
<view class="card-rate-badge">
<image class="heart-icon" src="/static/icon/heart-icon.png" mode="aspectFit"></image>
@ -39,10 +40,11 @@
</view>
</view>
<!-- 图片下方收益 -->
<view class="card-income-row" :class="index % 2 === 0 ? 'income-tilt-right' : 'income-tilt-left'">
<view class="card-income-row"
:class="index % 2 === 0 ? 'income-tilt-right' : 'income-tilt-left'">
<image class="topfans-icon" src="/static/icon/crystal.png" mode="aspectFit"></image>
<view class="card-income-text-wrap">
<text class="card-income-text">{{ item.earnings || 0 }}</text>
<text class="card-income-text">{{ item.earnings || 0 }}/</text>
</view>
</view>
</view>
@ -50,9 +52,11 @@
<!-- 空状态占位显示剩余空展位卡片 -->
<view v-if="exhibitionWorks.length < 2" class="empty-exhibition">
<!-- 根据已展出数量决定显示几个空卡片 -->
<view v-if="exhibitionWorks.length === 0" class="empty-card empty-card-left" @tap="openAssetSelector(0)">
<view v-if="exhibitionWorks.length === 0" class="empty-card empty-card-left"
@tap="openAssetSelector(0)">
<image class="empty-cover" src="/static/nft/placeholder.png" mode="aspectFill"></image>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill"></image>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png"
mode="aspectFill"></image>
<view class="empty-add-btn">
<text class="empty-add-icon">+</text>
</view>
@ -60,7 +64,8 @@
<view class="empty-card empty-card-right" @tap="openAssetSelector(1)">
<image class="empty-cover" src="/static/nft/placeholder.png" mode="aspectFill"></image>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill"></image>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png"
mode="aspectFill"></image>
<view class="empty-add-btn">
<text class="empty-add-icon">+</text>
</view>
@ -69,34 +74,27 @@
</view>
</view>
<!-- 今日点赞作品 -->
<!-- 当前点赞作品 -->
<view class="section-block">
<view class="section-label">
<image class="section-label-bg" src="/static/nft/dingbutubiao_liang.png" mode="aspectFill"></image>
<text class="section-label-text">今日点赞作品</text>
<text class="section-label-text">点赞作品</text>
</view>
<scroll-view class="liked-list" scroll-y="true" :show-scrollbar="false">
<view
v-for="(item, index) in likedWorks"
:key="item.id"
class="liked-row"
@tap="goToAssetDetail(item.id)"
>
<view v-for="(item, index) in likedWorks" :key="item.id" class="liked-row"
@tap="goToAssetDetail(item.id)">
<!-- 排名图标绝对定位在卡片左侧 -->
<image
v-if="index < 3"
:src="rankIcons[index]"
class="rank-icon-img"
mode="aspectFit"
></image>
<image v-if="index < 3" :src="rankIcons[index]" class="rank-icon-img" mode="aspectFit"></image>
<!-- 卡片主体 -->
<view class="liked-item" :class="index === 0 ? 'liked-item-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'" mode="aspectFill"></image>
<image class="liked-cover-frame" src="/static/square/cangpinkuang1.png" mode="aspectFill"></image>
<image class="liked-cover" :src="item.cover_url || '/static/nft/placeholder.png'"
mode="aspectFill"></image>
<image class="liked-cover-frame" src="/static/square/cangpinkuang1.png"
mode="aspectFill"></image>
</view>
<!-- 作品信息 -->
@ -104,13 +102,15 @@
<text class="liked-status">{{ item.status_text }}</text>
<view class="liked-score-row">
<text class="liked-score">{{ formatScore(item.score) }}</text>
<image class="fire-icon" src="/static/square/rementubiao.png" mode="aspectFit"></image>
<image class="fire-icon" src="/static/square/rementubiao.png" mode="aspectFit">
</image>
</view>
</view>
<!-- 右侧奖励 -->
<view class="liked-reward">
<image class="reward-token-icon" src="/static/icon/crystal.png" mode="aspectFit"></image>
<image class="reward-token-icon" src="/static/icon/crystal.png" mode="aspectFit">
</image>
<text class="reward-amount">+{{ item.reward }}</text>
</view>
</view>
@ -118,7 +118,7 @@
<!-- 空状态 -->
<view v-if="likedWorks.length === 0" class="empty-liked">
<text class="empty-text">今日暂无点赞作品</text>
<text class="empty-text">当前暂无点赞作品</text>
</view>
</scroll-view>
</view>
@ -127,12 +127,8 @@
</view>
<!-- 藏品选择器组件 -->
<AssetSelector
:visible="showAssetSelector"
:replace-asset="assetToReplace"
@close="closeAssetSelector"
@select="handleAssetSelect"
/>
<AssetSelector :visible="showAssetSelector" :replace-asset="assetToReplace" @close="closeAssetSelector"
@select="handleAssetSelect" />
</view>
</template>
@ -144,7 +140,24 @@ import { onShow } from '@dcloudio/uni-app';
import { doubleTapLike } from '@/utils/likeHelper.js';
const goBack = () => {
//
const pages = getCurrentPages();
if (pages.length > 1) {
//
uni.navigateBack();
} else {
// square
uni.reLaunch({
url: '/pages/square/square'
});
}
};
const goToSettings = () => {
uni.navigateTo({
url: '/pages/profile/profile'
});
};
const goToCastlove = () => {
@ -237,7 +250,7 @@ const handleExhibitionCardTap = (item, index) => {
if (success) {
//
exhibitionWorks.value[index].like_count = (exhibitionWorks.value[index].like_count || 0) + 1;
//
//
const likedItem = {
id: item.id,
cover_url: item.cover_url,
@ -275,7 +288,7 @@ const formatScore = (score) => {
//
const exhibitionWorks = ref([]);
//
//
const likedWorks = ref([]);
//
@ -383,6 +396,28 @@ onShow(() => {
width: 64rpx;
}
.nav-settings {
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
#B94E73 100%);
border-radius: 24rpx;
padding: 8rpx 20rpx 8rpx 20rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
}
.nav-settings-text {
font-size: 24rpx;
color: #fff;
font-weight: 400;
}
/* 内容区域 */
.scroll-content {
position: relative;
@ -532,11 +567,11 @@ onShow(() => {
}
.card-income-text-wrap {
width: 64rpx;
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
#B94E73 100%
);
#B94E73 100%);
border-radius: 999rpx;
padding: 8rpx 20rpx 8rpx 40rpx;
box-shadow:
@ -564,13 +599,13 @@ onShow(() => {
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
#B94E73 100%
);
#B94E73 100%);
border-radius: 999rpx;
padding: 2rpx 12rpx;
padding: 2rpx 16rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
display: flex;
}
.card-rate-text {
@ -643,7 +678,7 @@ onShow(() => {
color: #b09cc0;
}
/* 今日点赞列表 */
/* 当前点赞列表 */
.liked-list {
max-height: 732rpx;
}

View File

@ -1,117 +0,0 @@
<template>
<view
:id="'cabin-' + cabin.key"
class="cabin-wrapper"
:class="{
'cabin-nickname-mine': cabin.isMine || cabin.nickname === currentUserNickname,
'cabin-slots-zero': cabin.sharedBoothSlotsRemaining === 0
}"
:style="{ left: cabin.x + 'px', top: cabin.y + 'px', width: cabin.w + 'px' }"
@click="handleClick"
>
<image
class="cabin-icon"
:src="cabin.src"
:style="{ width: cabin.w + 'px', height: cabin.h + 'px' }"
mode="aspectFit"
/>
<text v-if="cabin.showNickname && cabin.nickname" class="cabin-nickname">
{{ cabin.nickname === currentUserNickname ? '我的小屋' : cabin.nickname }}
</text>
<text v-else class="cabin-nickname cabin-nickname--empty">小屋暂无人居住</text>
<view v-if="cabin.showDialog" class="cabin-slots-dialog">
<text class="cabin-slots-text text-white">
剩余 <text class="text-orange">{{ cabin.sharedBoothSlotsRemaining }}</text> 个展位
</text>
</view>
</view>
</template>
<script setup>
const props = defineProps({
cabin: {
type: Object,
required: true
},
currentUserNickname: {
type: String,
default: ''
}
})
const emit = defineEmits(['click'])
const handleClick = () => {
emit('click', props.cabin)
}
</script>
<style scoped>
.cabin-wrapper {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
pointer-events: auto;
}
.cabin-icon {
display: block;
}
.cabin-slots-zero .cabin-icon {
filter: grayscale(100%);
opacity: 0.6;
}
.cabin-nickname {
font-size: 20rpx;
color: #ffffff;
text-align: center;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.6);
margin-top: 4rpx;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cabin-nickname--empty {
color: rgba(255, 255, 255, 0.75);
font-style: italic;
}
.cabin-slots-dialog {
position: absolute;
top: -50rpx;
left: 50%;
transform: translateX(-50%);
background-image: url('/static/icon/tips-bg.png');
background-size: 100% 100%;
background-repeat: no-repeat;
padding: 8rpx 24rpx 18rpx;
width: max-content;
z-index: 10;
}
.cabin-slots-text {
font-size: 18rpx;
font-weight: 600;
white-space: nowrap;
text-align: center;
}
.text-white {
color: #ffffff;
}
.text-orange {
color: #FFB800;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
text-shadow:
0 0 10rpx rgba(255, 184, 0, 0.8),
0 2rpx 4rpx rgba(0, 0, 0, 0.5);
}
</style>

View File

@ -39,11 +39,10 @@ defineProps({
defineEmits(['update:modelValue'])
const tabs = [
{ key: 'hot', label: '热门作品', emoji: null, icon: '/static/square/rementubiao.png', iconWidth: 104, iconHeight: 104 },
{ key: 'xingka', label: '星卡', emoji: null, icon: '/static/square/xingka.png', iconWidth: 80, iconHeight: 80 },
{ key: 'baji', label: '把爱', emoji: null, icon: '/static/square/baji.png', iconWidth: 80, iconHeight: 80 },
{ key: 'haibao', label: '海报', emoji: null, icon: '/static/square/haibao.png', iconWidth: 96, iconHeight: 96 },
]
{ key: 'hot', label: '人气王者', emoji: null, icon: '/static/square/rementubiao.png', iconWidth: 104, iconHeight: 104 },
{ key: 'xingka', label: '潜力之星', emoji: null, icon: '/static/square/xingka.png', iconWidth: 80, iconHeight: 80 },
{ key: 'baji', label: '新鲜上架', emoji: null, icon: '/static/square/baji.png', iconWidth: 80, iconHeight: 80 },
{ key: 'haibao', label: '随机寻宝', emoji: null, icon: '/static/square/haibao.png', iconWidth: 96, iconHeight: 96 },]
</script>
<style scoped>

View File

@ -57,13 +57,16 @@
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { getRandomUsersApi, getOssPresignedUrlApi } from '@/utils/api.js'
import { getInspirationFlowApi, getBatchOssPresignedUrlsApi } from '@/utils/api.js'
import { doubleTapLike } from '@/utils/likeHelper.js'
import { USE_MOCK_DATA, getMockDataByCategory, generateMockItems, calcSpan } from '../config/mockData.js'
const props = defineProps({
screenWidth: { type: Number, default: 375 },
screenHeight: { type: Number, default: 812 },
bannerBottom: { type: Number, default: 200 },
useMockData: { type: Boolean, default: false }, // 使
category: { type: String, default: 'hot' }, //
})
const emit = defineEmits(['cardClick'])
@ -76,7 +79,7 @@ const SCALE = 0.9
const ROWS = 4
const AUTO_SCROLL_SPEED = 1.2
const AUTO_RESUME_DELAY = 2500
const PRELOAD_THRESHOLD = rpx2px(1200)
const PRELOAD_THRESHOLD = rpx2px(300)
// ========== ==========
const cards = ref([])
@ -86,6 +89,8 @@ const scrollLeft = ref(0)
const likingMap = ref({}) // ID
let currentScrollLeft = 0
let idCounter = 0
let isComponentMounted = false //
let mockDataOffset = 0 //
// ========== RAF ==========
const rafFn = (cb) => {
@ -103,16 +108,56 @@ const cafFn = (id) => {
let rafId = null
let userInteracting = false
let resumeTimer = null
let appendTimer = null //
const startAutoScroll = () => {
if (rafId) return
const step = () => {
if (!userInteracting) {
currentScrollLeft += AUTO_SCROLL_SPEED
scrollLeft.value = currentScrollLeft
if (totalWidth.value - currentScrollLeft - props.screenWidth < PRELOAD_THRESHOLD) {
if (!isComponentMounted) {
console.log('[WaterfallGrid] startAutoScroll blocked: not mounted')
return
}
if (rafId) cancelAnimationFrame(rafId)
console.log('[WaterfallGrid] startAutoScroll started')
// 使
if (!isLoadingMore && props.useMockData) {
console.log('[WaterfallGrid] startAutoScroll calling appendMore')
appendMore()
}
let lastScrollLeft = 0 //
const step = () => {
if (!isComponentMounted) {
rafId = null
console.log('[WaterfallGrid] RAF stopped: not mounted')
return
}
if (!userInteracting && !isLoadingMore) {
//
const actualScroll = scrollLeft.value
if (Math.abs(actualScroll - lastScrollLeft) > 5) {
//
currentScrollLeft = actualScroll
} else {
//
currentScrollLeft += AUTO_SCROLL_SPEED
scrollLeft.value = currentScrollLeft
lastScrollLeft = currentScrollLeft
}
//
const remainingScroll = totalWidth.value - currentScrollLeft - props.screenWidth
if (remainingScroll < Math.max(totalWidth.value / 2, props.screenWidth)) {
// appendMore
if (!isLoadingMore) {
appendMore()
}
}
} else {
//
lastScrollLeft = scrollLeft.value
currentScrollLeft = lastScrollLeft
}
rafId = rafFn(step)
}
@ -120,7 +165,16 @@ const startAutoScroll = () => {
}
const stopAutoScroll = () => {
if (rafId) { cafFn(rafId); rafId = null }
console.log('[WaterfallGrid] stopAutoScroll called')
if (rafId) {
cafFn(rafId)
rafId = null
}
clearTimeout(appendTimer)
appendTimer = null
clearTimeout(resumeTimer)
resumeTimer = null
userInteracting = false //
}
const pauseForUser = () => {
@ -129,14 +183,29 @@ const pauseForUser = () => {
resumeTimer = setTimeout(() => { userInteracting = false }, AUTO_RESUME_DELAY)
}
// 500ms
const scheduleAppend = () => {
if (!isComponentMounted) return
if (appendTimer) clearTimeout(appendTimer)
appendTimer = setTimeout(() => {
appendTimer = null
if (isComponentMounted) {
appendMore()
}
}, 500)
}
// ========== / ==========
const onTouchStart = () => pauseForUser()
const onTouchEnd = () => {}
const onScroll = (e) => {
if (!isComponentMounted) return
currentScrollLeft = e.detail.scrollLeft
if (totalWidth.value - currentScrollLeft - props.screenWidth < PRELOAD_THRESHOLD) {
appendMore()
//
const remainingScroll = totalWidth.value - currentScrollLeft - props.screenWidth
if (remainingScroll < Math.max(totalWidth.value / 2, props.screenWidth)) {
scheduleAppend()
}
}
@ -156,10 +225,11 @@ const scrollStyle = computed(() => ({
// = span=ROWS span
//
// 1. span span
// 2. span
// 2. span使 span
class WaterfallLayout {
constructor(containerH, gap = GAP) {
constructor(containerH, category = 'hot', gap = GAP) {
this.containerH = containerH
this.category = category
this.gap = gap
this.rowH = Math.floor((containerH - gap * (ROWS - 1)) / ROWS)
// = rowH × 9/16 9:16span
@ -173,30 +243,57 @@ class WaterfallLayout {
return { w, h }
}
// span
// span使
_span(likes) {
if (likes < 500) return 1
if (likes < 2000) return 2
if (likes < 8000) return 2
if (likes < 50000) return 3
return 4
return calcSpan(this.category, likes)
}
// span span = ROWS
_groupIntoColumns(users) {
//
const remaining = users.map((u, idx) => ({
...u,
originalIndex: idx,
span: u.span != null ? u.span : this._span(u.likes || 0)
}))
const columns = []
let i = 0
while (i < users.length) {
let colIndex = 0
while (remaining.length > 0) {
const col = []
let sum = 0
while (i < users.length && sum < ROWS) {
// span
const rawSpan = users[i].span != null ? users[i].span : this._span(users[i].likes || 0)
const span = Math.min(rawSpan, ROWS - sum)
col.push({ ...users[i], span })
sum += span
const colStart = users.length - remaining.length
//
let i = 0
while (i < remaining.length) {
const rawSpan = remaining[i].span
// ROWS
if (sum + rawSpan > ROWS) {
i++
continue
}
// remaining col
const item = remaining.splice(i, 1)[0]
col.push(item)
sum += rawSpan
// ROWS
if (sum >= ROWS) break
}
//
if (col.length === 0 && remaining.length > 0) {
const item = remaining.shift()
col.push(item)
sum = item.span
}
colIndex++
// _pad
while (sum < ROWS) {
col.push({ id: idCounter++, span: 1, _pad: true, likes: 0 })
@ -331,18 +428,71 @@ const mapUser = async (u) => {
return {
id: idCounter++,
userId: u.user_id,
nickname: u.nickname,
nickname: u.owner_nickname || u.nickname,
coverUrl,
likes: u.likes ?? randomLikes(),
likes: u.like_count ?? u.likes ?? randomLikes(),
span: u.span ?? null, // null =
}
}
const loadUsers = async () => {
// URL
const batchGetPresignedUrls = async (urls) => {
if (!urls || urls.length === 0) return {}
try {
const res = await getRandomUsersApi(1, 40)
if (res.code === 200 && res.data?.users) {
const withData = await Promise.all(res.data.users.map(mapUser))
const files = urls.filter(u => u)
const res = await getBatchOssPresignedUrlsApi(files, 3600, 'asset')
if (res?.code === 200 && res.data?.files) {
// key -> presigned_url
const map = {}
for (const file of res.data.files) {
map[file.key] = file.presigned_url
}
return map
}
} catch (_) {}
return {}
}
const loadUsers = async () => {
console.log('[WaterfallGrid] loadUsers called, isComponentMounted:', isComponentMounted, 'useMockData:', props.useMockData, 'category:', props.category)
if (!isComponentMounted) return
//
mockDataOffset = 0
try {
let items
if (props.useMockData) {
// 使
console.log('[WaterfallGrid] 使用模拟数据, category:', props.category)
const mockData = getMockDataByCategory(props.category)
items = mockData.items
//
mockDataOffset = items.length
console.log('[WaterfallGrid] loadUsers 模拟数据加载完成, items:', items.length, 'offset:', mockDataOffset)
} else {
// 使API
const res = await getInspirationFlowApi({ limit: 40, type: props.category })
console.log('[WaterfallGrid] loadUsers got response, isComponentMounted:', isComponentMounted)
if (!isComponentMounted) return
if (res.code === 200 && res.data?.items) {
items = res.data.items
}
}
if (items && items.length > 0) {
const withData = items.map((item) => {
return {
id: item.asset_id,
userId: item.asset_id,
nickname: item.owner_nickname || item.name,
coverUrl: item.cover_url || MOCK_IMAGES[idCounter % MOCK_IMAGES.length],
likes: item.likes || item.like_count || 0,
span: item.span ?? null,
}
})
console.log('[WaterfallGrid] loadUsers withData:', withData.length, 'sample likes:', withData.slice(0, 3).map(d => d.likes))
allUsers.value = withData
cards.value = layout.compute(withData)
totalWidth.value = layout.getTotalWidth()
@ -353,19 +503,92 @@ const loadUsers = async () => {
}
const appendMore = async () => {
if (isLoadingMore) return
console.log('[WaterfallGrid] appendMore called', {
isLoadingMore,
isComponentMounted,
useMockData: props.useMockData,
category: props.category,
cardsLength: cards.value.length,
totalWidth: totalWidth.value
})
if (!isComponentMounted) {
console.log('[WaterfallGrid] appendMore blocked: not mounted')
return
}
if (isLoadingMore) {
console.log('[WaterfallGrid] appendMore blocked: already loading, waiting...')
//
setTimeout(() => {
if (isComponentMounted && !isLoadingMore) {
console.log('[WaterfallGrid] appendMore retry after wait')
appendMore()
}
}, 500)
return
}
isLoadingMore = true
try {
const res = await getRandomUsersApi(1, 20)
if (res.code === 200 && res.data?.users) {
const withData = await Promise.all(res.data.users.map(mapUser))
let items
if (props.useMockData) {
// 使
const mockData = getMockDataByCategory(props.category)
const allItems = mockData.items
const batchSize = 20
console.log('[WaterfallGrid] 模拟数据追加, offset:', mockDataOffset, 'items:', allItems.length)
// mockDataOffset batchSize
const itemsToAdd = []
for (let i = 0; i < batchSize; i++) {
const sourceIndex = (mockDataOffset + i) % allItems.length
const sourceItem = allItems[sourceIndex]
// asset_id
const newItem = {
...sourceItem,
asset_id: sourceItem.asset_id * 100 + mockDataOffset + i, //
likes: sourceItem.like_count, // 使
}
itemsToAdd.push(newItem)
}
//
mockDataOffset = mockDataOffset + batchSize
items = itemsToAdd
console.log('[WaterfallGrid] 模拟数据追加完成, items:', items.length, 'next offset:', mockDataOffset)
} else {
// 使API
const res = await getInspirationFlowApi({ limit: 20, type: props.category })
if (!isComponentMounted) return
if (res.code === 200 && res.data?.items) {
items = res.data.items
}
}
if (items && items.length > 0) {
console.log('[WaterfallGrid] appendMore before:', 'cards.length:', cards.value.length, 'totalWidth:', totalWidth.value)
const withData = items.map((item) => {
return {
id: item.asset_id, // 使 asset_id
userId: item.asset_id,
nickname: item.owner_nickname || item.name,
coverUrl: item.cover_url || MOCK_IMAGES[idCounter % MOCK_IMAGES.length],
likes: item.likes || item.like_count || 0,
span: item.span ?? null,
}
})
console.log('[WaterfallGrid] appendMore withData:', withData.length)
const placed = layout.addCards(withData)
console.log('[WaterfallGrid] appendMore placed:', placed.length, 'layout.curX:', layout.curX)
cards.value = [...cards.value, ...placed]
totalWidth.value = layout.getTotalWidth()
console.log('[WaterfallGrid] appendMore after:', 'cards.length:', cards.value.length, 'totalWidth:', totalWidth.value)
}
} catch (e) {
console.error('[WaterfallGrid] 追加用户失败', e?.message ?? e)
} finally {
console.log('[WaterfallGrid] appendMore finally, resetting isLoadingMore')
isLoadingMore = false
}
}
@ -405,24 +628,56 @@ const handleCardClick = (card) => {
// ========== ==========
onMounted(() => {
isComponentMounted = true
const containerH = props.screenHeight - props.bannerBottom
layout = new WaterfallLayout(containerH)
layout = new WaterfallLayout(containerH, props.category)
console.log('[WaterfallGrid] onMounted, starting...')
loadUsers().then(() => startAutoScroll())
})
onUnmounted(() => {
console.log('[WaterfallGrid] onUnmounted called!')
isComponentMounted = false
stopAutoScroll()
clearTimeout(resumeTimer)
clearTimeout(appendTimer)
})
watch(() => [props.screenHeight, props.bannerBottom], () => {
const containerH = props.screenHeight - props.bannerBottom
layout = new WaterfallLayout(containerH)
layout = new WaterfallLayout(containerH, props.category)
if (allUsers.value.length) {
cards.value = layout.compute(allUsers.value)
totalWidth.value = layout.getTotalWidth()
}
})
//
watch(() => props.category, (newCategory) => {
console.log('[WaterfallGrid] category changed to:', newCategory)
if (isComponentMounted) {
//
stopAutoScroll()
//
if (appendTimer) {
clearTimeout(appendTimer)
appendTimer = null
}
isLoadingMore = false //
// 使 span
const containerH = props.screenHeight - props.bannerBottom
layout = new WaterfallLayout(containerH, newCategory)
cards.value = []
allUsers.value = []
totalWidth.value = 0
loadUsers().then(() => {
//
startAutoScroll()
})
}
})
</script>
<style scoped>

View File

@ -1,248 +0,0 @@
import { ref, reactive, watch, shallowRef } from 'vue'
import { generateGridCoordinates, CABIN_DEFS, cabinTypeByLevel } from '../config/cabin.js'
import { getRandomUsersApi } from '@/utils/api.js'
export function useCabin() {
const visibleCabins = ref([])
const currentPage = ref(1)
const totalUsers = ref(0)
const scaledCoords = ref([])
const pageCache = shallowRef(new Map())
const pageCacheVersion = ref(0)
const pendingPages = new Set()
let cabinRenderDefs = []
let tileWidth = 375
let screenWidth = 375
let bannerBottom = 0
const PAGE_SIZE = generateGridCoordinates().length
const maxPage = () => Math.max(1, Math.ceil(totalUsers.value / PAGE_SIZE))
const wrapPage = (p) => {
const max = maxPage()
return ((p - 1 + max) % max) + 1
}
const fetchPage = async (page) => {
if (pageCache.value.has(page) || pendingPages.has(page)) {
return
}
pendingPages.add(page)
try {
const res = await getRandomUsersApi(page, PAGE_SIZE)
if (res.code === 200 && res.data) {
if (totalUsers.value === 0) totalUsers.value = res.data.total || 0
const users = res.data.users || []
// 交换第 1 和第 3 个用户位置
if (users.length >= 3) {
[users[0], users[20]] = [users[20], users[0]]
}
pageCache.value.set(page, users)
pageCacheVersion.value++
}
} catch (e) {
console.error(`[useCabin] fetchPage: page=${page} 请求失败`, e?.message ?? e)
} finally {
pendingPages.delete(page)
}
}
const ensurePages = (center, isInertia = false) => {
const keep = new Set()
for (let n = 0; n <= 4; n++) {
keep.add(wrapPage(center + n - 1))
}
const pages = [...keep]
pages.forEach(fetchPage)
// 清除不在渲染范围内的页缓存
const evicted = []
for (const key of pageCache.value.keys()) {
if (!keep.has(key) && !pendingPages.has(key)) {
evicted.push(key)
pageCache.value.delete(key)
}
}
if (evicted.length) {
pageCacheVersion.value++
}
}
const buildVisibleCabins = (page, cache, coords) => {
if (!coords.length || !cabinRenderDefs.length) return []
const w = tileWidth
const result = []
// 第一遍:收集上方有用户数据的 cabin待移位以及下方空位坐标
const toRelocate = []
const emptySlots = []
for (let i = 0; i < coords.length; i++) {
const { sx, sy } = coords[i]
for (let n = 0; n <= 4; n++) {
const p = wrapPage(page + n - 1)
const users = cache.get(p) || []
const user = users[i] || null
const isAbove = sy < bannerBottom
if (isAbove && user) {
const typeIdx = cabinTypeByLevel(user.level)
toRelocate.push({ i, n, user, typeIdx, sx, sy })
} else if (!isAbove && !user) {
emptySlots.push({ sx, sy, n })
}
}
}
// 第二遍:正常渲染,跳过"被移位占用的空位"和"已移位的上方 cabin"
const occupiedSlots = new Set()
for (let k = 0; k < toRelocate.length && k < emptySlots.length; k++) {
occupiedSlots.add(k)
}
for (let i = 0; i < coords.length; i++) {
const { sx, sy } = coords[i]
for (let n = 0; n <= 4; n++) {
const p = wrapPage(page + n - 1)
const users = cache.get(p) || []
const user = users[i] || null
const isAbove = sy < bannerBottom
// 跳过上方有数据的(会被移位渲染)
if (isAbove && user) continue
// 检查这个空位是否被移位 cabin 占用
const slotIdx = emptySlots.findIndex(s => s.sx === sx && s.sy === sy && s.n === n)
if (slotIdx !== -1 && occupiedSlots.has(slotIdx)) continue
const typeIdx = cabinTypeByLevel(user ? user.level : 0)
const def = CABIN_DEFS[typeIdx]
const render = cabinRenderDefs[typeIdx]
const cabinY = sy - render.offsetY
result.push(reactive({
key: `${i}-${n}`,
x: sx + n * w - render.offsetX,
y: cabinY,
w: render.renderedW,
h: render.renderedH,
src: def.src,
userId: user ? user.user_id : null,
galleryOwnerId: user ? user.gallery_owner_id : null,
nickname: user ? user.nickname : null,
sharedBoothSlotsRemaining: user ? user.shared_booth_slots_remaining : null,
showNickname: !isAbove,
isMine: false,
showDialog: false,
}))
}
}
// 第三遍:把移位的 cabin 放到对应空位坐标上
for (let k = 0; k < toRelocate.length; k++) {
const { n, user, typeIdx } = toRelocate[k]
const def = CABIN_DEFS[typeIdx]
const render = cabinRenderDefs[typeIdx]
if (k >= emptySlots.length) continue
const targetSx = emptySlots[k].sx
const targetSy = emptySlots[k].sy
result.push(reactive({
key: `reloc-${k}-${n}`,
x: targetSx + n * w - render.offsetX,
y: targetSy - render.offsetY,
w: render.renderedW,
h: render.renderedH,
src: def.src,
userId: user.user_id,
galleryOwnerId: user.gallery_owner_id,
nickname: user.nickname,
sharedBoothSlotsRemaining: user.shared_booth_slots_remaining,
showNickname: true,
isMine: false,
showDialog: false,
}))
}
return result
}
// 翻页或坐标初始化时全量重建
watch([currentPage, scaledCoords], () => {
visibleCabins.value = buildVisibleCabins(currentPage.value, pageCache.value, scaledCoords.value)
}, { immediate: true })
// 数据加载完成时重建
watch(pageCacheVersion, () => {
visibleCabins.value = buildVisibleCabins(currentPage.value, pageCache.value, scaledCoords.value)
})
const initCabin = ({ screenW, tileW, imageH, currentUserNickname }) => {
screenWidth = screenW
tileWidth = tileW
// 计算 banner 底部边界
const rpxToPx = screenWidth / 750
bannerBottom = 496 * rpxToPx
// 计算 cabin 渲染尺寸
const baseH = Math.round(screenWidth * 0.2 * 1.3)
cabinRenderDefs = CABIN_DEFS.map(({ imgW, imgH, anchorX, anchorY }) => {
const renderedH = baseH
const renderedW = Math.round(renderedH * (imgW / imgH))
return {
renderedW,
renderedH,
offsetX: Math.round(renderedW * (anchorX / imgW)),
offsetY: Math.round(renderedH * (anchorY / imgH)),
}
})
// 生成并缩放坐标
const scale = imageH / 1918
const coords = generateGridCoordinates()
scaledCoords.value = coords.map(({ x, y }) => ({
sx: x * scale,
sy: y * scale,
}))
}
const updateCurrentUserNickname = (nickname) => {
visibleCabins.value.forEach(cabin => {
if (cabin.nickname === nickname) {
cabin.isMine = true
}
})
}
const resetSquare = async () => {
currentPage.value = 1
totalUsers.value = 0
pageCache.value = new Map()
pageCacheVersion.value++
await fetchPage(1)
ensurePages(1)
}
return {
visibleCabins,
currentPage,
scaledCoords,
cabinRenderDefs,
fetchPage,
ensurePages,
initCabin,
updateCurrentUserNickname,
resetSquare,
wrapPage,
}
}

View File

@ -1,84 +0,0 @@
import { ref } from 'vue'
export function useDialogRotation() {
let timer = null
let screenWidth = 375
let screenHeight = 812
const initDialogRotation = ({ screenW, screenH }) => {
screenWidth = screenW
screenHeight = screenH
}
const isCabinInViewport = (cabin) => {
return new Promise((resolve) => {
const query = uni.createSelectorQuery()
query.select(`#cabin-${cabin.key}`).boundingClientRect((rect) => {
if (!rect) {
resolve(false)
return
}
const visibleTop = Math.max(0, rect.top)
const visibleBottom = Math.min(screenHeight, rect.bottom)
const visibleLeft = Math.max(0, rect.left)
const visibleRight = Math.min(screenWidth, rect.right)
const visibleWidth = Math.max(0, visibleRight - visibleLeft)
const visibleHeight = Math.max(0, visibleBottom - visibleTop)
const visibleArea = visibleWidth * visibleHeight
const totalArea = rect.width * rect.height
const visiblePercent = totalArea > 0 ? visibleArea / totalArea : 0
resolve(visiblePercent > 0.7)
}).exec()
})
}
const rotateDialogVisibility = async (cabins) => {
if (!cabins.length) return
// 先全部重置为 false
cabins.forEach(c => c.showDialog = false)
// 筛选符合条件的 cabin有昵称、有展位剩余
const hasData = cabins.filter(c => c.nickname && c.sharedBoothSlotsRemaining !== null)
// 批量检查所有 cabin 是否在可视区域内
const viewportChecks = await Promise.all(hasData.map(c => isCabinInViewport(c)))
const eligible = hasData.filter((c, i) => viewportChecks[i])
if (eligible.length === 0) return
// 随机选择 2-3 个
const count = Math.min(Math.floor(Math.random() * 2) + 2, eligible.length, 3)
const shuffled = eligible.sort(() => Math.random() - 0.5)
for (let i = 0; i < count; i++) {
shuffled[i].showDialog = true
}
}
const startDialogRotation = (cabins) => {
stopDialogRotation()
const rotate = async () => {
await rotateDialogVisibility(cabins)
const interval = Math.floor(Math.random() * 1000) + 2000
timer = setTimeout(rotate, interval)
}
rotate()
}
const stopDialogRotation = () => {
if (timer) {
clearTimeout(timer)
timer = null
}
}
return {
initDialogRotation,
startDialogRotation,
stopDialogRotation,
}
}

View File

@ -1,181 +0,0 @@
import { ref, computed } from 'vue'
// RAF 封装
const rafFn = (cb) => uni.requestAnimationFrame ? uni.requestAnimationFrame(cb) : setTimeout(cb, 16)
const cafFn = (id) => uni.cancelAnimationFrame ? uni.cancelAnimationFrame(id) : clearTimeout(id)
export function useSwipe() {
const bgOffsetX = ref(0)
const screenWidth = ref(375)
const tileWidth = ref(375)
let rawOffsetX = 0
let touchStartX = 0
let lastMoveX = 0
let lastMoveTime = 0
let velocity = 0
let inertiaRaf = null
let isInertiaPhase = false
let touchInBanner = false
let onTileChange = null
const cabinLayerStyle = computed(() => ({
transform: `translateX(${-tileWidth.value + bgOffsetX.value}px)`
}))
const backgroundStripStyle = computed(() => ({
width: `${tileWidth.value * 3}px`,
transform: `translateX(${-tileWidth.value + bgOffsetX.value}px)`
}))
const clampOffset = (offset) => {
const w = tileWidth.value
return (((offset % w) + w) % w) - w
}
const normalizeOffset = (offset) => {
const w = tileWidth.value
const normalized = clampOffset(offset)
const prevTileN = Math.floor(-rawOffsetX / w)
rawOffsetX += offset - bgOffsetX.value
const nextTileN = Math.floor(-rawOffsetX / w)
const delta = nextTileN - prevTileN
if (delta !== 0 && onTileChange) {
onTileChange(delta, isInertiaPhase)
}
return normalized
}
const stopInertia = () => {
if (inertiaRaf) {
cafFn(inertiaRaf)
inertiaRaf = null
}
isInertiaPhase = false
}
const scrollPage = (direction) => {
stopInertia()
if (onTileChange) {
onTileChange(direction, false)
}
const DURATION = 300
const FRAME = 16
const totalFrames = Math.round(DURATION / FRAME)
const totalDelta = tileWidth.value * direction * -1
let frame = 0
const step = () => {
frame++
const progress = frame / totalFrames
const eased = 1 - Math.pow(1 - progress, 3)
const prevEased = frame === 1 ? 0 : 1 - Math.pow(1 - (frame - 1) / totalFrames, 3)
const delta = totalDelta * (eased - prevEased)
bgOffsetX.value = clampOffset(bgOffsetX.value + delta)
rawOffsetX += delta
if (frame < totalFrames) {
inertiaRaf = rafFn(step)
}
}
inertiaRaf = rafFn(step)
}
const getBannerBottom = () => (screenWidth.value / 750) * 632
const onBgTouchStart = (e) => {
const touchY = e.touches[0].clientY
touchInBanner = touchY < getBannerBottom()
if (touchInBanner) return
stopInertia()
touchStartX = e.touches[0].clientX
lastMoveX = touchStartX
lastMoveTime = Date.now()
velocity = 0
}
const onBgTouchMove = (e) => {
if (touchInBanner) return
e.preventDefault()
const currentX = e.touches[0].clientX
const now = Date.now()
const dt = now - lastMoveTime || 1
velocity = (currentX - lastMoveX) / dt
lastMoveX = currentX
lastMoveTime = now
bgOffsetX.value = normalizeOffset(bgOffsetX.value + (currentX - touchStartX))
touchStartX = currentX
}
const onBgTouchEnd = () => {
if (touchInBanner) {
touchInBanner = false
return
}
touchInBanner = false
isInertiaPhase = true
const FRICTION = 0.8
const MIN_VELOCITY = 0.2
const step = () => {
velocity *= FRICTION
if (Math.abs(velocity) < MIN_VELOCITY) {
isInertiaPhase = false
return
}
bgOffsetX.value = normalizeOffset(bgOffsetX.value + velocity * 16)
inertiaRaf = rafFn(step)
}
inertiaRaf = rafFn(step)
}
const onBgTouchCancel = () => {
touchInBanner = false
stopInertia()
velocity = 0
}
const initSwipe = ({ screenW, tileW, onTileChangeCallback }) => {
screenWidth.value = screenW
tileWidth.value = tileW
onTileChange = onTileChangeCallback
bgOffsetX.value = 0
rawOffsetX = 0
velocity = 0
}
const reset = () => {
stopInertia()
bgOffsetX.value = 0
rawOffsetX = 0
velocity = 0
}
return {
bgOffsetX,
rawOffsetX,
velocity,
cabinLayerStyle,
backgroundStripStyle,
scrollPage,
stopInertia,
initSwipe,
reset,
onBgTouchStart,
onBgTouchMove,
onBgTouchEnd,
onBgTouchCancel,
}
}

View File

@ -1,116 +0,0 @@
// ========== 图片原始尺寸 ==========
export const IMAGE_W = 2012
export const IMAGE_H = 1918
// ========== 小屋类型定义 ==========
export const CABIN_DEFS = [
{ src: '/static/components/cabin1.png', imgW: 1000, imgH: 1000, anchorX: 500, anchorY: 680 },
{ src: '/static/components/cabin2.png', imgW: 1000, imgH: 1000, anchorX: 500, anchorY: 680 },
{ src: '/static/components/cabin3.png', imgW: 1000, imgH: 1351, anchorX: 500, anchorY: 965 },
{ src: '/static/components/cabin4.png', imgW: 1000, imgH: 1223, anchorX: 500, anchorY: 875 },
]
// ========== 根据等级返回 cabin 类型索引 ==========
export const cabinTypeByLevel = (level) => {
if (level >= 7) return 3
if (level >= 5) return 2
if (level >= 3) return 1
return 0
}
// ========== 背景网格配置 ==========
export const GRID_CONFIG = {
backgroundWidth: 2012,
backgroundHeight: 1918,
grid: {
rows: 11,
cols: 4,
startX: -260,
startY: -20,
spacingX: 515,
spacingY: 180, // 减小垂直间距
staggered: true,
staggerOffsetX: 260,
cellWidth: 200,
cellHeight: 150,
excludeRows: [0, 1, 2],
},
// 手动微调个别位置
manualAdjustments: {
// 可以在这里添加需要微调的坐标
// 格式: index: { x: newX, y: newY }
16: { x: -255, y: 740 },
17: { x: 255, y: 740 },
18: { x: 750, y: 740 },
19: { x: 1265, y: 740 },
20: { x: 0, y: 900 },
21: { x: 505, y: 900 },
22: { x: 1010, y: 900 },
23: { x: 1515, y: 900 },
24: { x: -245, y: 1120 },
25: { x: 255, y: 1120 },
26: { x: 750, y: 1120 },
27: { x: 1265, y: 1120 },
28: { x: 0, y: 1300 },
29: { x: 505, y: 1300 },
30: { x: 1010, y: 1300 },
31: { x: 1515, y: 1300 },
32: { x: -245, y: 1520 },
33: { x: 255, y: 1520 },
34: { x: 750, y: 1520 },
35: { x: 1265, y: 1520 },
36: { x: 0, y: 1690 },
37: { x: 505, y: 1690 },
38: { x: 1010, y: 1690 },
39: { x: 1515, y: 1690 },
40: { x: -245, y: 1880 },
41: { x: 255, y: 1880 },
42: { x: 750, y: 1880 },
43: { x: 1265, y: 1880 },
}
}
// ========== 网格坐标生成函数 ==========
export function generateGridCoordinates(config = GRID_CONFIG) {
const { rows, cols, startX, startY, spacingX, spacingY, staggered, staggerOffsetX, excludeRows } = config.grid
const coords = []
// 先填充被排除行的占位符
for (let row = 0; row < rows; row++) {
if (excludeRows.includes(row)) {
for (let col = 0; col < cols; col++) {
coords.push({
x: 0,
y: 0,
row,
col,
index: coords.length
})
}
} else {
for (let col = 0; col < cols; col++) {
const offsetX = staggered && row % 2 === 1 ? staggerOffsetX : 0
coords.push({
x: startX + col * spacingX + offsetX,
y: startY + row * spacingY,
row,
col,
index: coords.length
})
}
}
}
// 应用手动微调
Object.entries(config.manualAdjustments || {}).forEach(([index, adjustment]) => {
const idx = parseInt(index)
if (coords[idx]) {
coords[idx].x = adjustment.x ?? coords[idx].x
coords[idx].y = adjustment.y ?? coords[idx].y
}
})
return coords
}

View File

@ -0,0 +1,231 @@
// ========== 模拟数据配置 ==========
// 是否使用模拟数据(开发调试时设为 true上线后改为 false
export const USE_MOCK_DATA = true
// 模拟图片列表
const MOCK_IMAGES = [
'/static/sucai/image-01.png',
'/static/sucai/image-02.png',
'/static/sucai/image-03.png',
'/static/sucai/image-04.png',
'/static/sucai/image-05.png',
'/static/sucai/image-06.png',
'/static/sucai/image-07.png',
'/static/sucai/image-08.png',
'/static/sucai/image-09.png',
'/static/sucai/image-10.png',
'/static/sucai/image-11.png',
'/static/sucai/image-12.png',
'/static/sucai/image-13.png',
'/static/sucai/image-14.png',
'/static/sucai/image-15.png',
'/static/sucai/image-16.png',
]
// 模拟昵称列表
const NICKNAMES = [
'小明', '小红', '小刚', '小芳', '小强', '小美', '小华', '小丽',
'小杰', '小婷', '小宇', '小雪', '小晨', '小曦', '小雷', '小雯',
'小风', '小月', '小星', '小云', '小河', '小涛', '小琳', '小瑶',
]
// ========== 分类 span 阈值配置 ==========
// 每个分类有不同的 span 计算规则
export const SPAN_CONFIG = {
// 人气王者:高点赞为主,大卡片多
hot: {
thresholds: [
{ max: 30000, span: 1 }, // 3w 以下 → span 1
{ max: 60000, span: 2 }, // 3w-6w → span 2
{ max: 100000, span: 3 }, // 6w-10w → span 3
{ max: Infinity, span: 4 }, // 10w+ → span 4
]
},
// 潜力之星:中等点赞,中小卡片
xingka: {
thresholds: [
{ max: 5000, span: 1 }, // 5k 以下 → span 1
{ max: 10000, span: 2 }, // 5k-1w → span 2
{ max: 15000, span: 3 }, // 1w-1.5w → span 3
{ max: Infinity, span: 4 },// 1.5w+ → span 4
]
},
// 新鲜上架:低点赞,小卡片为主
baji: {
thresholds: [
{ max: 200, span: 1 }, // 200 以下 → span 1
{ max: 500, span: 2 }, // 200-500 → span 2
{ max: Infinity, span: 3 },// 500+ → span 3
]
},
// 随机寻宝:混合
haibao: {
thresholds: [
{ max: 10000, span: 1 }, // 1w 以下 → span 1
{ max: 30000, span: 2 }, // 1w-3w → span 2
{ max: 60000, span: 3 }, // 3w-6w → span 3
{ max: Infinity, span: 4 },// 6w+ → span 4
]
},
}
// 根据分类和点赞数计算 span
export function calcSpan(category, likes) {
const config = SPAN_CONFIG[category] || SPAN_CONFIG.hot
for (const t of config.thresholds) {
if (likes < t.max) return t.span
}
return 1
}
// ========== 人气王者 - 高点赞热门作品 ==========
// span 由 WaterfallGrid._span() 根据 like_count 计算
export const MOCK_RENQIWANG = {
items: [
{ asset_id: 10001, name: '星光璀璨', cover_url: MOCK_IMAGES[0], like_count: 125800, owner_nickname: '小甜心' },
{ asset_id: 10002, name: '爱的绽放', cover_url: MOCK_IMAGES[1], like_count: 98600, owner_nickname: '爱豆粉' },
{ asset_id: 10003, name: '温暖守护', cover_url: MOCK_IMAGES[2], like_count: 87200, owner_nickname: '星星控' },
{ asset_id: 10004, name: '甜蜜暴击', cover_url: MOCK_IMAGES[3], like_count: 76800, owner_nickname: '追星族' },
{ asset_id: 10005, name: '闪耀舞台', cover_url: MOCK_IMAGES[4], like_count: 65400, owner_nickname: '小太阳' },
{ asset_id: 10006, name: '为你疯狂', cover_url: MOCK_IMAGES[5], like_count: 58900, owner_nickname: '小可爱' },
{ asset_id: 10007, name: '心动时刻', cover_url: MOCK_IMAGES[6], like_count: 52100, owner_nickname: '小天使' },
{ asset_id: 10008, name: '永相随', cover_url: MOCK_IMAGES[7], like_count: 48700, owner_nickname: '小甜豆' },
{ asset_id: 10009, name: '粉红泡泡', cover_url: MOCK_IMAGES[8], like_count: 45600, owner_nickname: '小迷糊' },
{ asset_id: 10010, name: '爱的力量', cover_url: MOCK_IMAGES[9], like_count: 42300, owner_nickname: '小幸运' },
{ asset_id: 10011, name: '璀璨星河', cover_url: MOCK_IMAGES[10], like_count: 39800, owner_nickname: '小浪漫' },
{ asset_id: 10012, name: '甜蜜日常', cover_url: MOCK_IMAGES[11], like_count: 37500, owner_nickname: '小清新' },
{ asset_id: 10013, name: '爱心发射', cover_url: MOCK_IMAGES[12], like_count: 35200, owner_nickname: '小活力' },
{ asset_id: 10014, name: '超级喜欢', cover_url: MOCK_IMAGES[13], like_count: 32900, owner_nickname: '小呆萌' },
{ asset_id: 10015, name: '温暖拥抱', cover_url: MOCK_IMAGES[14], like_count: 30600, owner_nickname: '小棉花' },
{ asset_id: 10016, name: '今日份心动', cover_url: MOCK_IMAGES[15], like_count: 28500, owner_nickname: '小牛奶' },
],
cursor: 'renqiwang_cursor_001',
has_more: true,
session_id: 'renqiwang_session',
}
// ========== 潜力之星 - 中等点赞有潜力的作品 ==========
export const MOCK_QIANLIXING = {
items: [
{ asset_id: 20001, name: '初露锋芒', cover_url: MOCK_IMAGES[0], like_count: 12800, owner_nickname: '小新芽' },
{ asset_id: 20002, name: '蓄势待发', cover_url: MOCK_IMAGES[1], like_count: 11500, owner_nickname: '小嫩草' },
{ asset_id: 20003, name: '冉冉升起', cover_url: MOCK_IMAGES[2], like_count: 10200, owner_nickname: '小泡泡' },
{ asset_id: 20004, name: '明日之星', cover_url: MOCK_IMAGES[3], like_count: 9800, owner_nickname: '小火苗' },
{ asset_id: 20005, name: '潜力无限', cover_url: MOCK_IMAGES[4], like_count: 8900, owner_nickname: '小萌芽' },
{ asset_id: 20006, name: '闪耀新星', cover_url: MOCK_IMAGES[5], like_count: 8200, owner_nickname: '小水滴' },
{ asset_id: 20007, name: '小荷才露', cover_url: MOCK_IMAGES[6], like_count: 7600, owner_nickname: '小竹笋' },
{ asset_id: 20008, name: '锋芒初现', cover_url: MOCK_IMAGES[7], like_count: 7100, owner_nickname: '小鸽子' },
{ asset_id: 20009, name: '闪闪发光', cover_url: MOCK_IMAGES[8], like_count: 6500, owner_nickname: '小萤火' },
{ asset_id: 20010, name: '未来可期', cover_url: MOCK_IMAGES[9], like_count: 5900, owner_nickname: '小芽芽' },
{ asset_id: 20011, name: '新秀登场', cover_url: MOCK_IMAGES[10], like_count: 5400, owner_nickname: '小藤蔓' },
{ asset_id: 20012, name: '蒸蒸日上', cover_url: MOCK_IMAGES[11], like_count: 4900, owner_nickname: '小葵花' },
{ asset_id: 20013, name: '茁壮成长', cover_url: MOCK_IMAGES[12], like_count: 4500, owner_nickname: '小苗苗' },
{ asset_id: 20014, name: '初绽光芒', cover_url: MOCK_IMAGES[13], like_count: 4100, owner_nickname: '小花花' },
{ asset_id: 20015, name: '星火燎原', cover_url: MOCK_IMAGES[14], like_count: 3800, owner_nickname: '小豆芽' },
{ asset_id: 20016, name: '蓄力中...', cover_url: MOCK_IMAGES[15], like_count: 3500, owner_nickname: '小冰晶' },
],
cursor: 'qianlixing_cursor_001',
has_more: true,
session_id: 'qianlixing_session',
}
// ========== 新鲜上架 - 新发布作品,点赞较低 ==========
export const MOCK_XINXIANSHANG = {
items: [
{ asset_id: 30001, name: '刚刚发布', cover_url: MOCK_IMAGES[0], like_count: 128, owner_nickname: '新手小白' },
{ asset_id: 30002, name: '今日新鲜', cover_url: MOCK_IMAGES[1], like_count: 256, owner_nickname: '小萌新' },
{ asset_id: 30003, name: '刚出锅', cover_url: MOCK_IMAGES[2], like_count: 89, owner_nickname: '新来的' },
{ asset_id: 30004, name: '热乎的', cover_url: MOCK_IMAGES[3], like_count: 167, owner_nickname: '小试牛刀' },
{ asset_id: 30005, name: '新品上市', cover_url: MOCK_IMAGES[4], like_count: 234, owner_nickname: '初来乍到' },
{ asset_id: 30006, name: '今日首发', cover_url: MOCK_IMAGES[5], like_count: 178, owner_nickname: '小透明' },
{ asset_id: 30007, name: '刚出炉', cover_url: MOCK_IMAGES[6], like_count: 145, owner_nickname: '新手村' },
{ asset_id: 30008, name: '最新创作', cover_url: MOCK_IMAGES[7], like_count: 312, owner_nickname: '小画师' },
{ asset_id: 30009, name: '新鲜出炉', cover_url: MOCK_IMAGES[8], like_count: 98, owner_nickname: '小创作者' },
{ asset_id: 30010, name: '首发作品', cover_url: MOCK_IMAGES[9], like_count: 267, owner_nickname: '小练手' },
{ asset_id: 30011, name: '全新上线', cover_url: MOCK_IMAGES[10], like_count: 189, owner_nickname: '新起步' },
{ asset_id: 30012, name: '今日份新', cover_url: MOCK_IMAGES[11], like_count: 156, owner_nickname: '小萌娃' },
{ asset_id: 30013, name: '新新人类', cover_url: MOCK_IMAGES[12], like_count: 223, owner_nickname: '小新潮' },
{ asset_id: 30014, name: '新鲜血液', cover_url: MOCK_IMAGES[13], like_count: 134, owner_nickname: '小白白' },
{ asset_id: 30015, name: '新晋选手', cover_url: MOCK_IMAGES[14], like_count: 278, owner_nickname: '小小新' },
{ asset_id: 30016, name: '首发新动态', cover_url: MOCK_IMAGES[15], like_count: 201, owner_nickname: '小清新' },
],
cursor: 'xinxianshang_cursor_001',
has_more: true,
session_id: 'xinxianshang_session',
}
// ========== 随机寻宝 - 随机混合数据 ==========
export const MOCK_SUIJIXUNBAO = {
items: [
{ asset_id: 40001, name: '神秘宝藏1', cover_url: MOCK_IMAGES[0], like_count: 45000, owner_nickname: '寻宝达人' },
{ asset_id: 40002, name: '神秘宝藏2', cover_url: MOCK_IMAGES[1], like_count: 32000, owner_nickname: '探险家' },
{ asset_id: 40003, name: '神秘宝藏3', cover_url: MOCK_IMAGES[2], like_count: 78000, owner_nickname: '淘宝高手' },
{ asset_id: 40004, name: '神秘宝藏4', cover_url: MOCK_IMAGES[3], like_count: 1500, owner_nickname: '捡漏王' },
{ asset_id: 40005, name: '神秘宝藏5', cover_url: MOCK_IMAGES[4], like_count: 8500, owner_nickname: '挖宝专家' },
{ asset_id: 40006, name: '神秘宝藏6', cover_url: MOCK_IMAGES[5], like_count: 55000, owner_nickname: '收藏家' },
{ asset_id: 40007, name: '神秘宝藏7', cover_url: MOCK_IMAGES[6], like_count: 200, owner_nickname: '淘宝达人' },
{ asset_id: 40008, name: '神秘宝藏8', cover_url: MOCK_IMAGES[7], like_count: 12000, owner_nickname: '猎奇者' },
{ asset_id: 40009, name: '神秘宝藏9', cover_url: MOCK_IMAGES[8], like_count: 62000, owner_nickname: '寻宝奇兵' },
{ asset_id: 40010, name: '神秘宝藏10', cover_url: MOCK_IMAGES[9], like_count: 800, owner_nickname: '淘宝猎人' },
{ asset_id: 40011, name: '神秘宝藏11', cover_url: MOCK_IMAGES[10], like_count: 9500, owner_nickname: '挖宝小分队' },
{ asset_id: 40012, name: '神秘宝藏12', cover_url: MOCK_IMAGES[11], like_count: 72000, owner_nickname: '淘宝小能手' },
{ asset_id: 40013, name: '神秘宝藏13', cover_url: MOCK_IMAGES[12], like_count: 350, owner_nickname: '宝藏猎人' },
{ asset_id: 40014, name: '神秘宝藏14', cover_url: MOCK_IMAGES[13], like_count: 48000, owner_nickname: '寻宝小天才' },
{ asset_id: 40015, name: '神秘宝藏15', cover_url: MOCK_IMAGES[14], like_count: 11000, owner_nickname: '淘宝小精灵' },
{ asset_id: 40016, name: '神秘宝藏16', cover_url: MOCK_IMAGES[15], like_count: 600, owner_nickname: '挖宝小专家' },
],
cursor: 'suijixunbao_cursor_001',
has_more: true,
session_id: 'suijixunbao_session',
}
// ========== 分类映射 ==========
export const MOCK_DATA_MAP = {
hot: MOCK_RENQIWANG,
xingka: MOCK_QIANLIXING,
baji: MOCK_XINXIANSHANG,
haibao: MOCK_SUIJIXUNBAO,
}
// 根据分类获取模拟数据
export function getMockDataByCategory(category) {
return MOCK_DATA_MAP[category] || MOCK_RENQIWANG
}
// 随机生成更多模拟数据(用于追加)
export function generateMockItems(category = 'hot', count = 20, startId = 50000) {
const items = []
for (let i = 0; i < count; i++) {
const id = startId + i
let like_count
// 根据分类生成不同的点赞数范围
switch (category) {
case 'hot':
like_count = 20000 + Math.floor(Math.random() * 100000) // 2w-12w
break
case 'xingka':
like_count = 3000 + Math.floor(Math.random() * 15000) // 3k-18k
break
case 'baji':
like_count = 50 + Math.floor(Math.random() * 500) // 50-550
break
case 'haibao':
like_count = Math.floor(Math.random() * 80000) // 0-8w
break
default:
like_count = Math.floor(Math.random() * 50000)
}
items.push({
asset_id: id,
name: `${category}_作品${id}`,
cover_url: MOCK_IMAGES[i % MOCK_IMAGES.length],
like_count,
owner_nickname: NICKNAMES[i % NICKNAMES.length],
})
}
return items
}

View File

@ -1,748 +0,0 @@
<template>
<view
class="debug-container"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>
<!-- 背景条3张图并排无限滚动 -->
<view class="background-strip" :style="backgroundStripStyle">
<image
v-for="i in 3"
:key="i"
class="background-tile"
:style="{ width: tileWidth + 'px', height: '100%' }"
src="/static/background/mainbg.png"
/>
</view>
<!-- 网格点层 -->
<view class="grid-layer" :style="gridLayerStyle">
<!-- 渲染 5 tile 的网格点n=0,1,2,3,4 -->
<view
v-for="point in visiblePoints"
:key="point.key"
class="grid-point"
:class="{
'grid-point--excluded': point.isExcluded,
'grid-point--selected': selectedIndex === point.originalIndex
}"
:style="{
left: point.x + 'px',
top: point.y + 'px',
transform: 'translate(-50%, -50%)'
}"
@click="selectPoint(point.originalIndex)"
>
<view class="grid-dot"></view>
<text class="grid-label">{{ point.originalIndex }}</text>
<text class="grid-coords">({{ Math.round(point.originalX) }}, {{ Math.round(point.originalY) }})</text>
</view>
</view>
<!-- 控制面板 -->
<view class="control-panel" @click.stop>
<view class="panel-header">
<text class="panel-title">网格调试工具</text>
<button class="btn-close" @click="goBack">返回</button>
</view>
<!-- 单点调试模式 -->
<view v-if="selectedIndex !== null" class="point-edit-section">
<view class="point-edit-header">
<text class="point-edit-title">编辑点位 #{{ selectedIndex }}</text>
<button class="btn-deselect" @click="deselectPoint">取消选择</button>
</view>
<view class="point-edit-controls">
<view class="point-edit-row">
<text class="point-edit-label">X: {{ currentPoint.x }}</text>
<view class="point-edit-buttons">
<button class="btn-adjust" @click="adjustPoint('x', -10)">-10</button>
<button class="btn-adjust" @click="adjustPoint('x', -1)">-1</button>
<button class="btn-adjust" @click="adjustPoint('x', 1)">+1</button>
<button class="btn-adjust" @click="adjustPoint('x', 10)">+10</button>
</view>
</view>
<view class="point-edit-row">
<text class="point-edit-label">Y: {{ currentPoint.y }}</text>
<view class="point-edit-buttons">
<button class="btn-adjust" @click="adjustPoint('y', -10)">-10</button>
<button class="btn-adjust" @click="adjustPoint('y', -1)">-1</button>
<button class="btn-adjust" @click="adjustPoint('y', 1)">+1</button>
<button class="btn-adjust" @click="adjustPoint('y', 10)">+10</button>
</view>
</view>
<view class="point-edit-row">
<button class="btn-save-point" @click="savePointAdjustment">保存此点位调整</button>
<button class="btn-reset-point" @click="resetPoint">重置此点位</button>
</view>
</view>
</view>
<!-- 全局参数调整 -->
<view v-else class="global-controls">
<view class="control-section">
<text class="control-label">起始Y (startY): {{ config.grid.startY }}</text>
<slider
:value="config.grid.startY"
@change="updateStartY"
min="-200"
max="100"
step="10"
show-value
/>
</view>
<view class="control-section">
<text class="control-label">垂直间距 (spacingY): {{ config.grid.spacingY }}</text>
<slider
:value="config.grid.spacingY"
@change="updateSpacingY"
min="150"
max="250"
step="5"
show-value
/>
</view>
<view class="control-section">
<text class="control-label">水平间距 (spacingX): {{ config.grid.spacingX }}</text>
<slider
:value="config.grid.spacingX"
@change="updateSpacingX"
min="450"
max="600"
step="5"
show-value
/>
</view>
<view class="control-section">
<text class="control-label">交错偏移 (staggerOffsetX): {{ config.grid.staggerOffsetX }}</text>
<slider
:value="config.grid.staggerOffsetX"
@change="updateStaggerOffsetX"
min="200"
max="350"
step="5"
show-value
/>
</view>
</view>
<view class="control-section">
<button class="btn-export" @click="exportConfig">导出配置</button>
<button class="btn-reset" @click="resetConfig">重置全部</button>
</view>
<view class="coords-list">
<text class="coords-title">生成的坐标列表点击点位编辑</text>
<scroll-view class="coords-scroll" scroll-y>
<view
v-for="(coord, i) in originalCoords"
:key="i"
class="coord-item"
:class="{ 'coord-item--selected': selectedIndex === i }"
@click="selectPoint(i)"
>
<text class="coord-index">{{ i }}:</text>
<text class="coord-value">[{{ coord.x }}, {{ coord.y }}]</text>
<text v-if="coord.x === 0 && coord.y === 0" class="coord-excluded">(排除)</text>
<text v-if="hasAdjustment(i)" class="coord-adjusted">(已调整)</text>
</view>
</scroll-view>
</view>
</view>
</view>
<!-- 通用确认弹窗 -->
<ConfirmModal
:visible="confirmModal.visible"
:title="confirmModal.title"
:content="confirmModal.content"
:confirmText="confirmModal.confirmText"
:cancelText="confirmModal.cancelText"
:showCancel="confirmModal.showCancel"
@confirm="onConfirmModal"
@cancel="onCancelModal"
/>
</template>
<script setup>
import { ref, computed } from 'vue'
import { GRID_CONFIG, generateGridCoordinates, IMAGE_W, IMAGE_H } from './config/cabin.js'
import ConfirmModal from '@/components/ConfirmModal.vue'
const config = ref(JSON.parse(JSON.stringify(GRID_CONFIG)))
const selectedIndex = ref(null)
//
const screenWidth = ref(375)
const screenHeight = ref(812)
const tileWidth = ref(375)
const scale = ref(1)
//
const bgOffsetX = ref(0)
let touchStartX = 0
//
const confirmModal = ref({
visible: false,
title: '',
content: '',
confirmText: '确认',
cancelText: '取消',
showCancel: true,
confirmCallback: null
})
//
const onConfirmModal = () => {
if (confirmModal.value.confirmCallback) {
confirmModal.value.confirmCallback({ confirm: true })
}
confirmModal.value.visible = false
}
//
const onCancelModal = () => {
if (confirmModal.value.confirmCallback) {
confirmModal.value.confirmCallback({ confirm: false })
}
confirmModal.value.visible = false
}
//
const showConfirmModal = (options) => {
confirmModal.value = {
visible: true,
title: options.title || '',
content: options.content || '',
confirmText: options.confirmText || '确认',
cancelText: options.cancelText || '取消',
showCancel: options.showCancel !== false,
confirmCallback: options.success || null
}
}
//
const originalCoords = computed(() => {
return generateGridCoordinates(config.value)
})
//
const scaledCoords = computed(() => {
return originalCoords.value.map(coord => ({
x: coord.x * scale.value,
y: coord.y * scale.value,
originalX: coord.x,
originalY: coord.y,
isExcluded: coord.x === 0 && coord.y === 0
}))
})
// 5tile
const visiblePoints = computed(() => {
const w = tileWidth.value
const points = []
for (let n = 0; n <= 4; n++) {
scaledCoords.value.forEach((coord, i) => {
points.push({
key: `${i}-${n}`,
x: coord.x + n * w,
y: coord.y,
originalX: coord.originalX,
originalY: coord.originalY,
originalIndex: i,
isExcluded: coord.isExcluded
})
})
}
return points
})
//
const backgroundStripStyle = computed(() => {
const centerOffset = (screenWidth.value - tileWidth.value) / 2
return {
width: `${tileWidth.value * 3}px`,
transform: `translateX(${-tileWidth.value + centerOffset + bgOffsetX.value}px)`
}
})
//
const gridLayerStyle = computed(() => {
const centerOffset = (screenWidth.value - tileWidth.value) / 2
return {
transform: `translateX(${-tileWidth.value + centerOffset + bgOffsetX.value}px)`
}
})
//
const currentPoint = computed(() => {
if (selectedIndex.value === null) return { x: 0, y: 0 }
return originalCoords.value[selectedIndex.value] || { x: 0, y: 0 }
})
//
const clampOffset = (offset) => {
const w = tileWidth.value
return (((offset % w) + w) % w) - w
}
//
const onTouchStart = (e) => {
touchStartX = e.touches[0].clientX
lastMoveX = touchStartX
}
const onTouchMove = (e) => {
e.preventDefault()
const currentX = e.touches[0].clientX
const delta = currentX - lastMoveX
lastMoveX = currentX
bgOffsetX.value = clampOffset(bgOffsetX.value + delta)
}
const onTouchEnd = () => {
//
}
const selectPoint = (index) => {
selectedIndex.value = index
console.log('选中点位:', index, originalCoords.value[index])
}
const deselectPoint = () => {
selectedIndex.value = null
}
const adjustPoint = (axis, delta) => {
if (selectedIndex.value === null) return
const idx = selectedIndex.value
if (!config.value.manualAdjustments) {
config.value.manualAdjustments = {}
}
const current = originalCoords.value[idx]
if (!config.value.manualAdjustments[idx]) {
config.value.manualAdjustments[idx] = { x: current.x, y: current.y }
}
if (axis === 'x') {
config.value.manualAdjustments[idx].x += delta
} else {
config.value.manualAdjustments[idx].y += delta
}
config.value = { ...config.value }
}
const savePointAdjustment = () => {
uni.showToast({
title: `点位 #${selectedIndex.value} 已保存`,
icon: 'success'
})
console.log('保存点位调整:', selectedIndex.value, config.value.manualAdjustments[selectedIndex.value])
}
const resetPoint = () => {
if (selectedIndex.value === null) return
if (config.value.manualAdjustments && config.value.manualAdjustments[selectedIndex.value]) {
delete config.value.manualAdjustments[selectedIndex.value]
config.value = { ...config.value }
uni.showToast({
title: '点位已重置',
icon: 'success'
})
}
}
const hasAdjustment = (index) => {
return config.value.manualAdjustments && config.value.manualAdjustments[index]
}
const updateStartY = (e) => {
config.value.grid.startY = e.detail.value
}
const updateSpacingY = (e) => {
config.value.grid.spacingY = e.detail.value
}
const updateSpacingX = (e) => {
config.value.grid.spacingX = e.detail.value
}
const updateStaggerOffsetX = (e) => {
config.value.grid.staggerOffsetX = e.detail.value
}
const exportConfig = () => {
const configStr = JSON.stringify(config.value, null, 2)
console.log('========== 当前配置 ==========')
console.log(configStr)
const coordsCode = originalCoords.value.map(c => ` [${c.x}, ${c.y}]`).join(',\n')
console.log('\n========== 坐标数组 ==========')
console.log('[\n' + coordsCode + '\n]')
if (config.value.manualAdjustments && Object.keys(config.value.manualAdjustments).length > 0) {
console.log('\n========== 手动调整 ==========')
console.log('manualAdjustments: {')
Object.entries(config.value.manualAdjustments).forEach(([idx, adj]) => {
console.log(` ${idx}: { x: ${adj.x}, y: ${adj.y} },`)
})
console.log('}')
}
showConfirmModal({
title: '配置已导出',
content: '请查看控制台输出,复制到 config/cabin.js',
showCancel: false
})
}
const resetConfig = () => {
showConfirmModal({
title: '确认重置',
content: '将重置所有参数和调整,是否继续?',
success: (res) => {
if (res.confirm) {
config.value = JSON.parse(JSON.stringify(GRID_CONFIG))
selectedIndex.value = null
uni.showToast({
title: '已重置',
icon: 'success'
})
}
}
})
}
const goBack = () => {
uni.navigateBack()
}
//
const init = () => {
const info = uni.getSystemInfoSync()
screenWidth.value = info.windowWidth
screenHeight.value = info.windowHeight
tileWidth.value = Math.round(info.windowHeight * (IMAGE_W / IMAGE_H))
scale.value = info.windowHeight / IMAGE_H
console.log('调试页面初始化:', {
screenWidth: screenWidth.value,
screenHeight: screenHeight.value,
tileWidth: tileWidth.value,
scale: scale.value
})
}
init()
</script>
<style scoped>
.debug-container {
position: relative;
width: 100vw;
min-height: calc(100vh + 650rpx);
overflow: hidden;
background: #1a1a1a;
padding-bottom: 650rpx;
}
.background-strip {
position: absolute;
top: 0;
left: 0;
height: 100vh;
display: flex;
z-index: 0;
will-change: transform;
}
.background-tile {
flex-shrink: 0;
height: 100%;
}
.grid-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: none;
}
.grid-point {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
z-index: 10;
pointer-events: auto;
}
.grid-point--excluded {
opacity: 0.3;
}
.grid-dot {
width: 20rpx;
height: 20rpx;
background: #ff0000;
border-radius: 50%;
border: 2rpx solid #ffffff;
box-shadow: 0 0 10rpx rgba(255, 0, 0, 0.8);
}
.grid-point--selected .grid-dot {
background: #00ff00;
width: 30rpx;
height: 30rpx;
box-shadow: 0 0 20rpx rgba(0, 255, 0, 1);
}
.grid-label {
font-size: 20rpx;
color: #ffffff;
background: rgba(0, 0, 0, 0.7);
padding: 2rpx 8rpx;
border-radius: 4rpx;
margin-top: 4rpx;
font-weight: bold;
}
.grid-point--selected .grid-label {
background: rgba(0, 255, 0, 0.9);
color: #000000;
font-size: 24rpx;
}
.grid-coords {
font-size: 18rpx;
color: #00ff00;
background: rgba(0, 0, 0, 0.7);
padding: 2rpx 6rpx;
border-radius: 4rpx;
margin-top: 2rpx;
}
.control-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #2a2a2a;
border-top: 2rpx solid #444;
padding: 20rpx;
max-height: 600rpx;
overflow-y: auto;
z-index: 100;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.panel-title {
font-size: 32rpx;
color: #ffffff;
font-weight: bold;
}
.btn-close {
padding: 10rpx 20rpx;
background: #ff4444;
color: #ffffff;
border: none;
border-radius: 8rpx;
font-size: 24rpx;
}
.control-section {
margin-bottom: 30rpx;
}
.control-label {
display: block;
font-size: 26rpx;
color: #ffffff;
margin-bottom: 10rpx;
}
.btn-export,
.btn-reset {
width: 48%;
padding: 20rpx;
background: #4CAF50;
color: #ffffff;
border: none;
border-radius: 8rpx;
font-size: 28rpx;
margin-right: 4%;
}
.btn-reset {
background: #ff9800;
margin-right: 0;
}
.coords-list {
margin-top: 30rpx;
background: #1a1a1a;
border-radius: 8rpx;
padding: 20rpx;
}
.coords-title {
display: block;
font-size: 28rpx;
color: #ffffff;
margin-bottom: 10rpx;
font-weight: bold;
}
.coords-scroll {
max-height: 400rpx;
}
.coord-item {
display: flex;
align-items: center;
padding: 8rpx 0;
border-bottom: 1rpx solid #333;
cursor: pointer;
}
.coord-item--selected {
background: rgba(0, 255, 0, 0.2);
}
.coord-index {
font-size: 24rpx;
color: #00ff00;
width: 80rpx;
font-weight: bold;
}
.coord-value {
font-size: 24rpx;
color: #ffffff;
flex: 1;
}
.coord-excluded {
font-size: 20rpx;
color: #ff0000;
margin-left: 10rpx;
}
.coord-adjusted {
font-size: 20rpx;
color: #00ff00;
margin-left: 10rpx;
}
.point-edit-section {
background: #1a1a1a;
border-radius: 8rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.point-edit-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.point-edit-title {
font-size: 28rpx;
color: #00ff00;
font-weight: bold;
}
.btn-deselect {
padding: 8rpx 16rpx;
background: #666;
color: #ffffff;
border: none;
border-radius: 6rpx;
font-size: 22rpx;
}
.point-edit-controls {
display: flex;
flex-direction: column;
gap: 15rpx;
}
.point-edit-row {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.point-edit-label {
font-size: 24rpx;
color: #ffffff;
font-weight: bold;
}
.point-edit-buttons {
display: flex;
gap: 10rpx;
}
.btn-adjust {
flex: 1;
padding: 15rpx;
background: #4CAF50;
color: #ffffff;
border: none;
border-radius: 6rpx;
font-size: 24rpx;
}
.btn-save-point,
.btn-reset-point {
width: 48%;
padding: 20rpx;
border: none;
border-radius: 8rpx;
font-size: 26rpx;
color: #ffffff;
}
.btn-save-point {
background: #2196F3;
margin-right: 4%;
}
.btn-reset-point {
background: #ff5722;
}
.global-controls {
margin-bottom: 20rpx;
}
</style>

View File

@ -8,6 +8,8 @@
:screenWidth="screenWidth"
:screenHeight="screenHeight"
:bannerBottom="bannerBottomPx"
:useMockData="USE_MOCK_DATA"
:category="activeContentTab"
@cardClick="handleCardClick"
class="fall-bg"
/>
@ -68,6 +70,7 @@ import WaterfallGrid from './components/WaterfallGrid.vue'
import ContentTabs from './components/ContentTabs.vue'
import { clearSubStepProgress, shouldShowGuideStartModal } from '@/utils/guideConfig.js'
import { useBanner } from './composables/useBanner.js'
import { USE_MOCK_DATA } from './config/mockData.js'
// ========== Store & User Info ==========
const store = useStore()
@ -78,7 +81,6 @@ const currentStarId = ref(uni.getStorageSync('star_id') || null)
const activeContentTab = ref('hot')
const navExpanded = ref(false)
const showRankingModal = ref(false)
const isDev = ref(false) //
// ========== Screen Info ==========
const screenWidth = ref(375)
@ -143,12 +145,6 @@ const handleTabChange = (newTab) => {
}
}
const openDebugGrid = () => {
uni.navigateTo({
url: '/pages/square/debug-grid'
})
}
// ========== Tile Change Callback ==========
const handleTileChange = () => {}
@ -282,25 +278,4 @@ onUnmounted(() => {
opacity: 1;
}
}
.debug-btn {
position: fixed;
bottom: 200rpx;
right: 20rpx;
width: 100rpx;
height: 100rpx;
background: rgba(255, 0, 0, 0.8);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.3);
}
.debug-text {
font-size: 24rpx;
color: #ffffff;
font-weight: bold;
}
</style>

View File

@ -1,6 +1,6 @@
// API 基础配置
const baseURL = 'http://101.132.250.62:8080'
// const baseURL = 'http://192.168.110.60:8080'
// const baseURL = 'http://101.132.250.62:8080'
const baseURL = 'http://192.168.110.60:8080'
// const baseURL = 'http://localhost:8080'
// 是否使用模拟数据(开发调试时设为 true后端API准备好后改为 false
@ -607,3 +607,22 @@ export function getMyLikedAssetsApi(page = 1, pageSize = 20) {
method: 'GET'
})
}
// ==================== 灵感瀑布相关接口 ====================
// 获取灵感瀑布藏品列表
export function getInspirationFlowApi(params) {
return request({
url: '/api/v1/inspiration-flow',
method: 'GET',
data: params
})
}
// 批量获取OSS预签名URL用于读取目录下的图片
export function getBatchOssPresignedUrlsApi(files, expires = 3600, type = 'asset') {
return request({
url: `/api/v1/assets/oss/batch-presigned-urls?files=${encodeURIComponent(JSON.stringify(files))}&expires=${expires}&type=${type}`,
method: 'GET'
})
}