diff --git a/README.md b/README.md
index c1114b8..4a8893e 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
# TopFans
+密码:R251Y>Y8inL_BM=W
\ No newline at end of file
diff --git a/backend/docs/服务器部署指南.md b/backend/docs/服务器部署指南.md
index 7817a75..43423c2 100644
--- a/backend/docs/服务器部署指南.md
+++ b/backend/docs/服务器部署指南.md
@@ -53,7 +53,7 @@
```
ssh root@101.132.250.62
->n73qBnCja-,#VF+Wq
+R251Y>Y8inL_BM=W
```
### 1.1 更新系统包
diff --git a/backend/gateway/controller/task_controller.go b/backend/gateway/controller/task_controller.go
index 38d6e88..17d4d9b 100644
--- a/backend/gateway/controller/task_controller.go
+++ b/backend/gateway/controller/task_controller.go
@@ -289,7 +289,8 @@ func (ctrl *TaskController) CompleteGuide(c *gin.Context) {
userID, _ := c.Get("user_id")
var req struct {
- TaskKey string `json:"task_key"`
+ TaskKey string `json:"task_key"`
+ Stages []*pbTask.OnboardingStage `json:"stages"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "请求参数错误")
@@ -303,6 +304,7 @@ func (ctrl *TaskController) CompleteGuide(c *gin.Context) {
logger.Logger.Info("CompleteGuide request",
zap.Int64("user_id", userID.(int64)),
zap.String("task_key", req.TaskKey),
+ zap.Int("stages_count", len(req.Stages)),
)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -315,6 +317,7 @@ func (ctrl *TaskController) CompleteGuide(c *gin.Context) {
resp, err := ctrl.taskMobileService.CompleteGuide(ctx, &pbTask.CompleteGuideRequest{
TaskKey: req.TaskKey,
+ Stages: req.Stages,
})
if err != nil {
@@ -476,7 +479,9 @@ func (ctrl *TaskController) ClaimOnboardingReward(c *gin.Context) {
}
response.Success(c, map[string]interface{}{
- "success": resp.Success,
+ "success": resp.Success,
+ "crystal_balance": resp.CrystalBalance,
+ "experience": resp.Experience,
})
}
diff --git a/backend/pkg/proto/task/task.pb.go b/backend/pkg/proto/task/task.pb.go
index 30ecf69..ed997ca 100644
--- a/backend/pkg/proto/task/task.pb.go
+++ b/backend/pkg/proto/task/task.pb.go
@@ -596,16 +596,17 @@ func (x *ClaimAllDailyTasksResponse) GetClaimedTaskKeys() []string {
}
type OnboardingStage struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Stage int32 `protobuf:"varint,1,opt,name=stage,proto3" json:"stage,omitempty"`
- Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
- RequiredTaskKeys []string `protobuf:"bytes,3,rep,name=required_task_keys,json=requiredTaskKeys,proto3" json:"required_task_keys,omitempty"`
- CrystalReward int64 `protobuf:"varint,4,opt,name=crystal_reward,json=crystalReward,proto3" json:"crystal_reward,omitempty"`
- ExpReward int64 `protobuf:"varint,5,opt,name=exp_reward,json=expReward,proto3" json:"exp_reward,omitempty"`
- 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"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Stage int32 `protobuf:"varint,1,opt,name=stage,proto3" json:"stage,omitempty"`
+ Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
+ RequiredTaskKeys []string `protobuf:"bytes,3,rep,name=required_task_keys,json=requiredTaskKeys,proto3" json:"required_task_keys,omitempty"`
+ CrystalReward int64 `protobuf:"varint,4,opt,name=crystal_reward,json=crystalReward,proto3" json:"crystal_reward,omitempty"`
+ ExpReward int64 `protobuf:"varint,5,opt,name=exp_reward,json=expReward,proto3" json:"exp_reward,omitempty"`
+ 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"` // 该阶段所有任务是否完成
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
func (x *OnboardingStage) Reset() {
@@ -687,9 +688,17 @@ func (x *OnboardingStage) GetIsCurrent() bool {
return false
}
+func (x *OnboardingStage) GetAllTasksCompleted() bool {
+ if x != nil {
+ return x.AllTasksCompleted
+ }
+ 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"`
+ Stages []*OnboardingStage `protobuf:"bytes,2,rep,name=stages,proto3" json:"stages,omitempty"` // 前端传入的阶段配置,首次调用时存储
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -731,6 +740,13 @@ func (x *CompleteGuideRequest) GetTaskKey() string {
return ""
}
+func (x *CompleteGuideRequest) GetStages() []*OnboardingStage {
+ if x != nil {
+ return x.Stages
+ }
+ return nil
+}
+
type CompleteGuideResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
@@ -1076,11 +1092,13 @@ func (x *ClaimOnboardingRewardRequest) GetStage() int32 {
}
type ClaimOnboardingRewardResponse struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
- Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
+ Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"`
+ CrystalBalance string `protobuf:"bytes,3,opt,name=crystal_balance,json=crystalBalance,proto3" json:"crystal_balance,omitempty"` // 使用 string 避免 Dubbo int64 序列化 bug
+ Experience string `protobuf:"bytes,4,opt,name=experience,proto3" json:"experience,omitempty"` // 使用 string 避免 Dubbo int64 序列化 bug
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
func (x *ClaimOnboardingRewardResponse) Reset() {
@@ -1127,6 +1145,20 @@ func (x *ClaimOnboardingRewardResponse) GetSuccess() bool {
return false
}
+func (x *ClaimOnboardingRewardResponse) GetCrystalBalance() string {
+ if x != nil {
+ return x.CrystalBalance
+ }
+ return ""
+}
+
+func (x *ClaimOnboardingRewardResponse) GetExperience() string {
+ if x != nil {
+ return x.Experience
+ }
+ return ""
+}
+
type ExhibitionRevenueItem struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
@@ -1910,7 +1942,7 @@ const file_task_proto_rawDesc = "" +
"\n" +
"experience\x18\x04 \x01(\x03R\n" +
"experience\x12*\n" +
- "\x11claimed_task_keys\x18\x05 \x03(\tR\x0fclaimedTaskKeys\"\xe6\x01\n" +
+ "\x11claimed_task_keys\x18\x05 \x03(\tR\x0fclaimedTaskKeys\"\x96\x02\n" +
"\x0fOnboardingStage\x12\x14\n" +
"\x05stage\x18\x01 \x01(\x05R\x05stage\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12,\n" +
@@ -1920,9 +1952,11 @@ const file_task_proto_rawDesc = "" +
"exp_reward\x18\x05 \x01(\x03R\texpReward\x12\x16\n" +
"\x06status\x18\x06 \x01(\tR\x06status\x12\x1d\n" +
"\n" +
- "is_current\x18\a \x01(\bR\tisCurrent\"1\n" +
+ "is_current\x18\a \x01(\bR\tisCurrent\x12.\n" +
+ "\x13all_tasks_completed\x18\b \x01(\bR\x11allTasksCompleted\"h\n" +
"\x14CompleteGuideRequest\x12\x19\n" +
- "\btask_key\x18\x01 \x01(\tR\ataskKey\"\xd6\x01\n" +
+ "\btask_key\x18\x01 \x01(\tR\ataskKey\x125\n" +
+ "\x06stages\x18\x02 \x03(\v2\x1d.topfans.task.OnboardingStageR\x06stages\"\xd6\x01\n" +
"\x15CompleteGuideResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x17\n" +
"\auser_id\x18\x02 \x01(\x03R\x06userId\x12#\n" +
@@ -1944,10 +1978,14 @@ const file_task_proto_rawDesc = "" +
"\x06status\x18\x03 \x01(\tR\x06status\x125\n" +
"\x06stages\x18\x04 \x03(\v2\x1d.topfans.task.OnboardingStageR\x06stages\"4\n" +
"\x1cClaimOnboardingRewardRequest\x12\x14\n" +
- "\x05stage\x18\x01 \x01(\x05R\x05stage\"k\n" +
+ "\x05stage\x18\x01 \x01(\x05R\x05stage\"\xb4\x01\n" +
"\x1dClaimOnboardingRewardResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x18\n" +
- "\asuccess\x18\x02 \x01(\bR\asuccess\"\xe2\x02\n" +
+ "\asuccess\x18\x02 \x01(\bR\asuccess\x12'\n" +
+ "\x0fcrystal_balance\x18\x03 \x01(\tR\x0ecrystalBalance\x12\x1e\n" +
+ "\n" +
+ "experience\x18\x04 \x01(\tR\n" +
+ "experience\"\xe2\x02\n" +
"\x15ExhibitionRevenueItem\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x17\n" +
"\astar_id\x18\x02 \x01(\x03R\x06starId\x12#\n" +
@@ -2071,50 +2109,51 @@ var file_task_proto_depIdxs = []int32{
29, // 2: topfans.task.ReportEventResponse.base:type_name -> topfans.common.BaseResponse
29, // 3: topfans.task.ClaimDailyTaskResponse.base:type_name -> topfans.common.BaseResponse
29, // 4: topfans.task.ClaimAllDailyTasksResponse.base:type_name -> topfans.common.BaseResponse
- 29, // 5: topfans.task.CompleteGuideResponse.base:type_name -> topfans.common.BaseResponse
- 9, // 6: topfans.task.CompleteGuideResponse.stages:type_name -> topfans.task.OnboardingStage
- 29, // 7: topfans.task.GetOnboardingStatusResponse.base:type_name -> topfans.common.BaseResponse
- 9, // 8: topfans.task.GetOnboardingStatusResponse.stages:type_name -> topfans.task.OnboardingStage
- 29, // 9: topfans.task.AdvanceStageResponse.base:type_name -> topfans.common.BaseResponse
- 9, // 10: topfans.task.AdvanceStageResponse.stages:type_name -> topfans.task.OnboardingStage
- 29, // 11: topfans.task.ClaimOnboardingRewardResponse.base:type_name -> topfans.common.BaseResponse
- 29, // 12: topfans.task.GetExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse
- 18, // 13: topfans.task.GetExhibitionRevenueResponse.items:type_name -> topfans.task.ExhibitionRevenueItem
- 29, // 14: topfans.task.ClaimExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse
- 29, // 15: topfans.task.ClaimAllExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse
- 29, // 16: topfans.task.InitUserTasksResponse.base:type_name -> topfans.common.BaseResponse
- 29, // 17: topfans.task.OnExhibitionCompletedResponse.base:type_name -> topfans.common.BaseResponse
- 1, // 18: topfans.task.TaskMobileService.GetDailyTasks:input_type -> topfans.task.GetDailyTasksRequest
- 3, // 19: topfans.task.TaskMobileService.ReportEvent:input_type -> topfans.task.ReportEventRequest
- 5, // 20: topfans.task.TaskMobileService.ClaimDailyTask:input_type -> topfans.task.ClaimDailyTaskRequest
- 7, // 21: topfans.task.TaskMobileService.ClaimAllDailyTasks:input_type -> topfans.task.ClaimAllDailyTasksRequest
- 10, // 22: topfans.task.TaskMobileService.CompleteGuide:input_type -> topfans.task.CompleteGuideRequest
- 12, // 23: topfans.task.TaskMobileService.GetOnboardingStatus:input_type -> topfans.task.GetOnboardingStatusRequest
- 14, // 24: topfans.task.TaskMobileService.AdvanceStage:input_type -> topfans.task.AdvanceStageRequest
- 16, // 25: topfans.task.TaskMobileService.ClaimOnboardingReward:input_type -> topfans.task.ClaimOnboardingRewardRequest
- 19, // 26: topfans.task.TaskMobileService.GetExhibitionRevenue:input_type -> topfans.task.GetExhibitionRevenueRequest
- 21, // 27: topfans.task.TaskMobileService.ClaimExhibitionRevenue:input_type -> topfans.task.ClaimExhibitionRevenueRequest
- 23, // 28: topfans.task.TaskMobileService.ClaimAllExhibitionRevenue:input_type -> topfans.task.ClaimAllExhibitionRevenueRequest
- 25, // 29: topfans.task.TaskInternalService.InitUserTasks:input_type -> topfans.task.InitUserTasksRequest
- 27, // 30: topfans.task.TaskInternalService.OnExhibitionCompleted:input_type -> topfans.task.OnExhibitionCompletedRequest
- 2, // 31: topfans.task.TaskMobileService.GetDailyTasks:output_type -> topfans.task.GetDailyTasksResponse
- 4, // 32: topfans.task.TaskMobileService.ReportEvent:output_type -> topfans.task.ReportEventResponse
- 6, // 33: topfans.task.TaskMobileService.ClaimDailyTask:output_type -> topfans.task.ClaimDailyTaskResponse
- 8, // 34: topfans.task.TaskMobileService.ClaimAllDailyTasks:output_type -> topfans.task.ClaimAllDailyTasksResponse
- 11, // 35: topfans.task.TaskMobileService.CompleteGuide:output_type -> topfans.task.CompleteGuideResponse
- 13, // 36: topfans.task.TaskMobileService.GetOnboardingStatus:output_type -> topfans.task.GetOnboardingStatusResponse
- 15, // 37: topfans.task.TaskMobileService.AdvanceStage:output_type -> topfans.task.AdvanceStageResponse
- 17, // 38: topfans.task.TaskMobileService.ClaimOnboardingReward:output_type -> topfans.task.ClaimOnboardingRewardResponse
- 20, // 39: topfans.task.TaskMobileService.GetExhibitionRevenue:output_type -> topfans.task.GetExhibitionRevenueResponse
- 22, // 40: topfans.task.TaskMobileService.ClaimExhibitionRevenue:output_type -> topfans.task.ClaimExhibitionRevenueResponse
- 24, // 41: topfans.task.TaskMobileService.ClaimAllExhibitionRevenue:output_type -> topfans.task.ClaimAllExhibitionRevenueResponse
- 26, // 42: topfans.task.TaskInternalService.InitUserTasks:output_type -> topfans.task.InitUserTasksResponse
- 28, // 43: topfans.task.TaskInternalService.OnExhibitionCompleted:output_type -> topfans.task.OnExhibitionCompletedResponse
- 31, // [31:44] is the sub-list for method output_type
- 18, // [18:31] 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
+ 9, // 5: topfans.task.CompleteGuideRequest.stages:type_name -> topfans.task.OnboardingStage
+ 29, // 6: topfans.task.CompleteGuideResponse.base:type_name -> topfans.common.BaseResponse
+ 9, // 7: topfans.task.CompleteGuideResponse.stages:type_name -> topfans.task.OnboardingStage
+ 29, // 8: topfans.task.GetOnboardingStatusResponse.base:type_name -> topfans.common.BaseResponse
+ 9, // 9: topfans.task.GetOnboardingStatusResponse.stages:type_name -> topfans.task.OnboardingStage
+ 29, // 10: topfans.task.AdvanceStageResponse.base:type_name -> topfans.common.BaseResponse
+ 9, // 11: topfans.task.AdvanceStageResponse.stages:type_name -> topfans.task.OnboardingStage
+ 29, // 12: topfans.task.ClaimOnboardingRewardResponse.base:type_name -> topfans.common.BaseResponse
+ 29, // 13: topfans.task.GetExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse
+ 18, // 14: topfans.task.GetExhibitionRevenueResponse.items:type_name -> topfans.task.ExhibitionRevenueItem
+ 29, // 15: topfans.task.ClaimExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse
+ 29, // 16: topfans.task.ClaimAllExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse
+ 29, // 17: topfans.task.InitUserTasksResponse.base:type_name -> topfans.common.BaseResponse
+ 29, // 18: topfans.task.OnExhibitionCompletedResponse.base:type_name -> topfans.common.BaseResponse
+ 1, // 19: topfans.task.TaskMobileService.GetDailyTasks:input_type -> topfans.task.GetDailyTasksRequest
+ 3, // 20: topfans.task.TaskMobileService.ReportEvent:input_type -> topfans.task.ReportEventRequest
+ 5, // 21: topfans.task.TaskMobileService.ClaimDailyTask:input_type -> topfans.task.ClaimDailyTaskRequest
+ 7, // 22: topfans.task.TaskMobileService.ClaimAllDailyTasks:input_type -> topfans.task.ClaimAllDailyTasksRequest
+ 10, // 23: topfans.task.TaskMobileService.CompleteGuide:input_type -> topfans.task.CompleteGuideRequest
+ 12, // 24: topfans.task.TaskMobileService.GetOnboardingStatus:input_type -> topfans.task.GetOnboardingStatusRequest
+ 14, // 25: topfans.task.TaskMobileService.AdvanceStage:input_type -> topfans.task.AdvanceStageRequest
+ 16, // 26: topfans.task.TaskMobileService.ClaimOnboardingReward:input_type -> topfans.task.ClaimOnboardingRewardRequest
+ 19, // 27: topfans.task.TaskMobileService.GetExhibitionRevenue:input_type -> topfans.task.GetExhibitionRevenueRequest
+ 21, // 28: topfans.task.TaskMobileService.ClaimExhibitionRevenue:input_type -> topfans.task.ClaimExhibitionRevenueRequest
+ 23, // 29: topfans.task.TaskMobileService.ClaimAllExhibitionRevenue:input_type -> topfans.task.ClaimAllExhibitionRevenueRequest
+ 25, // 30: topfans.task.TaskInternalService.InitUserTasks:input_type -> topfans.task.InitUserTasksRequest
+ 27, // 31: topfans.task.TaskInternalService.OnExhibitionCompleted:input_type -> topfans.task.OnExhibitionCompletedRequest
+ 2, // 32: topfans.task.TaskMobileService.GetDailyTasks:output_type -> topfans.task.GetDailyTasksResponse
+ 4, // 33: topfans.task.TaskMobileService.ReportEvent:output_type -> topfans.task.ReportEventResponse
+ 6, // 34: topfans.task.TaskMobileService.ClaimDailyTask:output_type -> topfans.task.ClaimDailyTaskResponse
+ 8, // 35: topfans.task.TaskMobileService.ClaimAllDailyTasks:output_type -> topfans.task.ClaimAllDailyTasksResponse
+ 11, // 36: topfans.task.TaskMobileService.CompleteGuide:output_type -> topfans.task.CompleteGuideResponse
+ 13, // 37: topfans.task.TaskMobileService.GetOnboardingStatus:output_type -> topfans.task.GetOnboardingStatusResponse
+ 15, // 38: topfans.task.TaskMobileService.AdvanceStage:output_type -> topfans.task.AdvanceStageResponse
+ 17, // 39: topfans.task.TaskMobileService.ClaimOnboardingReward:output_type -> topfans.task.ClaimOnboardingRewardResponse
+ 20, // 40: topfans.task.TaskMobileService.GetExhibitionRevenue:output_type -> topfans.task.GetExhibitionRevenueResponse
+ 22, // 41: topfans.task.TaskMobileService.ClaimExhibitionRevenue:output_type -> topfans.task.ClaimExhibitionRevenueResponse
+ 24, // 42: topfans.task.TaskMobileService.ClaimAllExhibitionRevenue:output_type -> topfans.task.ClaimAllExhibitionRevenueResponse
+ 26, // 43: topfans.task.TaskInternalService.InitUserTasks:output_type -> topfans.task.InitUserTasksResponse
+ 28, // 44: topfans.task.TaskInternalService.OnExhibitionCompleted:output_type -> topfans.task.OnExhibitionCompletedResponse
+ 32, // [32:45] is the sub-list for method output_type
+ 19, // [19:32] is the sub-list for method input_type
+ 19, // [19:19] is the sub-list for extension type_name
+ 19, // [19:19] is the sub-list for extension extendee
+ 0, // [0:19] is the sub-list for field type_name
}
func init() { file_task_proto_init() }
diff --git a/backend/proto/task.proto b/backend/proto/task.proto
index 6ba5c48..79e0354 100644
--- a/backend/proto/task.proto
+++ b/backend/proto/task.proto
@@ -77,10 +77,12 @@ message OnboardingStage {
int64 exp_reward = 5;
string status = 6; // pending/completed/in_progress
bool is_current = 7;
+ bool all_tasks_completed = 8; // 该阶段所有任务是否完成
}
message CompleteGuideRequest {
string task_key = 1;
+ repeated OnboardingStage stages = 2; // 前端传入的阶段配置,首次调用时存储
}
message CompleteGuideResponse {
@@ -119,6 +121,8 @@ message ClaimOnboardingRewardRequest {
message ClaimOnboardingRewardResponse {
topfans.common.BaseResponse base = 1;
bool success = 2;
+ string crystal_balance = 3; // 使用 string 避免 Dubbo int64 序列化 bug
+ string experience = 4; // 使用 string 避免 Dubbo int64 序列化 bug
}
// ==================== 展示收益 ====================
diff --git a/backend/services/assetService/repository/ranking_repository.go b/backend/services/assetService/repository/ranking_repository.go
index ad08223..88b8355 100644
--- a/backend/services/assetService/repository/ranking_repository.go
+++ b/backend/services/assetService/repository/ranking_repository.go
@@ -118,8 +118,9 @@ func (r *rankingRepository) getHotRankingByDimension(starID int64, dimension str
Where("exhibitions.expire_at > ?", now).
Where("fan_profiles.star_id = ?", starID)
case "month":
- // 本月:统计本月内的点赞数
- db = db.Where("assets.id IN (SELECT asset_id FROM asset_likes WHERE star_id = ? AND created_at >= ? GROUP BY asset_id)", starID, startOfMonth)
+ // 本月:本月内开始的展览,按点赞数排序
+ db = db.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id").
+ Where("exhibitions.start_time >= ?", startOfMonth)
case "total":
// 全部:直接使用 assets 表的 like_count,无需额外条件
}
@@ -140,7 +141,9 @@ func (r *rankingRepository) getHotRankingByDimension(starID int64, dimension str
Where("exhibitions.expire_at > ?", now).
Where("host_fp.star_id = ?", starID)
case "month":
- countDB = countDB.Where("assets.id IN (SELECT asset_id FROM asset_likes WHERE star_id = ? AND created_at >= ? GROUP BY asset_id)", starID, startOfMonth)
+ // 本月:本月内开始的展览
+ countDB = countDB.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id").
+ Where("exhibitions.start_time >= ?", startOfMonth)
}
if err := countDB.Count(&total).Error; err != nil {
return nil, 0, err
@@ -190,8 +193,9 @@ func (r *rankingRepository) GetMyBestRanking(userID, starID int64, dimension str
Where("exhibitions.expire_at > ?", now).
Where("host_fp.star_id = ?", starID)
case "month":
- // 本月:关联 asset_likes 表,筛选本月有点赞的藏品
- db = db.Where("assets.id IN (SELECT asset_id FROM asset_likes WHERE star_id = ? AND created_at >= ? GROUP BY asset_id)", starID, startOfMonth)
+ // 本月:本月内开始的展览
+ db = db.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id").
+ Where("exhibitions.start_time >= ?", startOfMonth)
}
// 获取用户在该star下点赞数最高的藏品
@@ -232,7 +236,9 @@ func (r *rankingRepository) GetMyBestRanking(userID, starID int64, dimension str
Where("exhibitions.expire_at > ?", now).
Where("host_fp.star_id = ?", starID)
case "month":
- rankingDB = rankingDB.Where("assets.id IN (SELECT asset_id FROM asset_likes WHERE star_id = ? AND created_at >= ? GROUP BY asset_id)", starID, startOfMonth)
+ // 本月:本月内开始的展览
+ rankingDB = rankingDB.Joins("INNER JOIN exhibitions ON exhibitions.asset_id = assets.id").
+ Where("exhibitions.start_time >= ?", startOfMonth)
}
if err := rankingDB.Count(&rank).Error; err != nil {
diff --git a/backend/services/taskService/provider/task_mobile_provider.go b/backend/services/taskService/provider/task_mobile_provider.go
index 35a5639..8a38f58 100644
--- a/backend/services/taskService/provider/task_mobile_provider.go
+++ b/backend/services/taskService/provider/task_mobile_provider.go
@@ -159,11 +159,12 @@ func (p *TaskMobileProvider) CompleteGuide(ctx context.Context, req *pb.Complete
return &pb.CompleteGuideResponse{}, nil
}
- logger.Logger.Debug("CompleteGuide",
+ logger.Logger.Info("CompleteGuide",
zap.Int64("user_id", userID),
- zap.String("task_key", req.TaskKey))
+ zap.String("task_key", req.TaskKey),
+ zap.Int("stages_count", len(req.Stages)))
- return p.onboardingSvc.CompleteGuide(ctx, userID, req.TaskKey)
+ return p.onboardingSvc.CompleteGuide(ctx, userID, req.TaskKey, req.Stages)
}
func (p *TaskMobileProvider) GetOnboardingStatus(ctx context.Context, req *pb.GetOnboardingStatusRequest) (*pb.GetOnboardingStatusResponse, error) {
@@ -202,11 +203,6 @@ func (p *TaskMobileProvider) ClaimOnboardingReward(ctx context.Context, req *pb.
return &pb.ClaimOnboardingRewardResponse{Success: false}, nil
}
- logger.Logger.Debug("ClaimOnboardingReward",
- zap.Int64("user_id", userID),
- zap.Int64("star_id", starID),
- zap.Int32("stage", req.Stage))
-
return p.onboardingSvc.ClaimOnboardingReward(ctx, userID, starID, req.Stage)
}
diff --git a/backend/services/taskService/repository/onboarding_repo.go b/backend/services/taskService/repository/onboarding_repo.go
index 2893801..b6dd042 100644
--- a/backend/services/taskService/repository/onboarding_repo.go
+++ b/backend/services/taskService/repository/onboarding_repo.go
@@ -13,6 +13,7 @@ import (
type OnboardingRepository interface {
GetOnboardingStatus(userID int64, starID int64) (*model.UserOnboardingStatus, error)
GetOrCreateOnboardingStatus(userID int64, starID int64) (*model.UserOnboardingStatus, error)
+ GetUserOnboardingStatuses(userID int64) ([]*model.UserOnboardingStatus, error)
UpdateOnboardingStatus(status *model.UserOnboardingStatus) error
UpdateOnboardingProgress(progress *model.UserOnboardingProgress) error
GetUserOnboardingProgress(userID int64, taskKey string) (*model.UserOnboardingProgress, error)
@@ -20,6 +21,8 @@ type OnboardingRepository interface {
ListActiveStageConfigs() ([]*model.OnboardingStageConfig, error)
ListUserOnboardingProgressByUser(userID int64) ([]*model.UserOnboardingProgress, error)
GetStageConfig(stage int64) (*model.OnboardingStageConfig, error)
+ SaveStageConfigs(configs []*model.OnboardingStageConfig) error
+ CountStageConfigs() (int64, error)
}
type onboardingRepository struct {
@@ -65,6 +68,16 @@ func (r *onboardingRepository) GetOrCreateOnboardingStatus(userID int64, starID
return &status, nil
}
+func (r *onboardingRepository) GetUserOnboardingStatuses(userID int64) ([]*model.UserOnboardingStatus, error) {
+ var statuses []*model.UserOnboardingStatus
+ err := r.db.Where("user_id = ?", userID).Order("star_id DESC").Find(&statuses).Error
+ if err != nil {
+ logger.Logger.Error("Failed to GetUserOnboardingStatuses", zap.Int64("user_id", userID), zap.Error(err))
+ return nil, err
+ }
+ return statuses, nil
+}
+
func (r *onboardingRepository) UpdateOnboardingStatus(status *model.UserOnboardingStatus) error {
status.UpdatedAt = time.Now().Unix()
if err := r.db.Save(status).Error; err != nil {
@@ -139,9 +152,84 @@ func (r *onboardingRepository) ListUserOnboardingProgressByUser(userID int64) ([
func (r *onboardingRepository) GetStageConfig(stage int64) (*model.OnboardingStageConfig, error) {
var config model.OnboardingStageConfig
+ logger.Logger.Info("GetStageConfig: querying",
+ zap.Int64("stage", stage))
err := r.db.Where("stage = ? AND is_active = ?", stage, true).First(&config).Error
if err != nil {
+ logger.Logger.Error("GetStageConfig: query failed",
+ zap.Int64("stage", stage),
+ zap.Error(err))
return nil, err
}
+ logger.Logger.Info("GetStageConfig: found",
+ zap.Int64("stage", stage),
+ zap.String("name", config.Name))
return &config, nil
}
+
+func (r *onboardingRepository) SaveStageConfigs(configs []*model.OnboardingStageConfig) error {
+ if len(configs) == 0 {
+ logger.Logger.Warn("SaveStageConfigs: configs is empty, returning")
+ return nil
+ }
+ now := time.Now().Unix()
+
+ logger.Logger.Info("SaveStageConfigs: starting save",
+ zap.Int("count", len(configs)))
+
+ for _, cfg := range configs {
+ logger.Logger.Info("SaveStageConfigs: processing config",
+ zap.Int("stage", cfg.Stage),
+ zap.String("name", cfg.Name),
+ zap.Strings("required_task_keys", cfg.RequiredTaskKeys),
+ zap.Int64("crystal_reward", cfg.CrystalReward),
+ zap.Int64("exp_reward", cfg.ExpReward))
+
+ cfg.UpdatedAt = now
+
+ // First try to update existing record
+ // Use Select to specify fields so GORM properly handles JSON serialization
+ result := r.db.Model(&model.OnboardingStageConfig{}).
+ Where("stage = ?", cfg.Stage).
+ Select("name", "required_task_keys", "crystal_reward", "exp_reward", "is_active", "updated_at").
+ Updates(cfg)
+
+ if result.Error != nil {
+ logger.Logger.Error("SaveStageConfigs: failed to update config",
+ zap.Int("stage", cfg.Stage),
+ zap.Error(result.Error))
+ return result.Error
+ }
+
+ logger.Logger.Info("SaveStageConfigs: update result",
+ zap.Int("stage", cfg.Stage),
+ zap.Int64("rows_affected", result.RowsAffected))
+
+ // If no rows affected, create new record
+ if result.RowsAffected == 0 {
+ cfg.CreatedAt = now
+ if err := r.db.Create(cfg).Error; err != nil {
+ logger.Logger.Error("SaveStageConfigs: failed to create config",
+ zap.Int("stage", cfg.Stage),
+ zap.Error(err))
+ return err
+ }
+ logger.Logger.Info("SaveStageConfigs: created config",
+ zap.Int("stage", cfg.Stage),
+ zap.String("name", cfg.Name))
+ } else {
+ logger.Logger.Info("SaveStageConfigs: updated config",
+ zap.Int("stage", cfg.Stage),
+ zap.String("name", cfg.Name))
+ }
+ }
+
+ logger.Logger.Info("SaveStageConfigs: all configs saved successfully")
+ return nil
+}
+
+func (r *onboardingRepository) CountStageConfigs() (int64, error) {
+ var count int64
+ err := r.db.Model(&model.OnboardingStageConfig{}).Where("is_active = ?", true).Count(&count).Error
+ return count, err
+}
diff --git a/frontend/components/GuideOverlay.vue b/frontend/components/GuideOverlay.vue
index 4edb16b..4988094 100644
--- a/frontend/components/GuideOverlay.vue
+++ b/frontend/components/GuideOverlay.vue
@@ -55,13 +55,27 @@ import { useStore } from 'vuex'
const store = useStore()
// 状态映射
-const isActive = computed(() => store.state.guide.isActive)
+const isActive = computed(() => {
+ const val = store.state.guide.isActive
+ console.log('[GuideOverlay] isActive computed:', val)
+ return val
+})
const isComponentMode = computed(() => store.state.guide.componentMode)
const targetRect = computed(() => store.state.guide.targetRect)
const stepConfig = computed(() => store.getters['guide/currentStepConfig'])
const isFirst = computed(() => store.getters['guide/isFirst'])
const isLast = computed(() => store.getters['guide/isLast'])
+// 监听引导激活状态
+watch(() => store.state.guide.isActive, (newVal) => {
+ console.log('[GuideOverlay] isActive changed to:', newVal)
+}, { immediate: true })
+
+// 组件挂载时记录当前状态
+onMounted(() => {
+ console.log('[GuideOverlay] mounted, isActive:', store.state.guide.isActive)
+})
+
// 高亮区域样式
const highlightStyle = computed(() => {
const config = stepConfig.value || {}
@@ -271,6 +285,7 @@ watch(() => store.state.guide.currentStep, async () => {
// 组件挂载时初始化
onMounted(() => {
+ console.log('[GuideOverlay] mounted, isActive:', store.state.guide.isActive, 'currentGuide:', store.state.guide.currentGuide?.key, 'currentStep:', store.state.guide.currentStep)
if (isActive.value) {
// 延迟执行,确保 DOM 已渲染完成
setTimeout(() => {
@@ -288,16 +303,20 @@ onMounted(() => {
// 监听组件关闭事件,关闭后执行下一步
let closeComponentHandled = false // 防止重复处理的标志
uni.$on('guide:closeComponent', () => {
+ console.log('[GuideOverlay] 收到 guide:closeComponent 事件, componentMode:', store.state.guide.componentMode)
// 如果已经处理过或者 componentMode 已经是 false,忽略
if (closeComponentHandled || !store.state.guide.componentMode) {
+ console.log('[GuideOverlay] guide:closeComponent 忽略, closeComponentHandled:', closeComponentHandled, 'componentMode:', store.state.guide.componentMode)
return
}
closeComponentHandled = true
store.commit('guide/RESET_COMPONENT_MODE')
// component action 后需要手动前进到下一步
if (store.getters['guide/hasNext']) {
+ console.log('[GuideOverlay] 有下一步,前进')
store.commit('guide/NEXT_STEP')
} else {
+ console.log('[GuideOverlay] 没有下一步,结束引导')
store.commit('guide/END_GUIDE')
}
// 重置标志,允许未来再次处理
@@ -356,8 +375,10 @@ function handleTooltipClick() {
// 点击高亮区域
function handleHighlightClick() {
const stepConfigVal = stepConfig.value
+ console.log('[GuideOverlay] handleHighlightClick, stepConfig:', stepConfigVal)
if (stepConfigVal && stepConfigVal.action) {
const { action } = stepConfigVal
+ console.log('[GuideOverlay] action:', action)
if (action.type === 'navigate' && action.url) {
// 路由跳转
@@ -482,6 +503,7 @@ defineExpose({
z-index: 10001;
display: flex;
align-items: center;
+ flex-direction: column;
}
.tooltip-content {
diff --git a/frontend/pages.json b/frontend/pages.json
index 2bd74a3..359f876 100644
--- a/frontend/pages.json
+++ b/frontend/pages.json
@@ -26,6 +26,15 @@
}
}
},
+ {
+ "path": "pages/starbook/items",
+ "style": {
+ "navigationStyle": "custom",
+ "app-plus": {
+ "bounce": "none"
+ }
+ }
+ },
{
"path": "pages/starcity/index",
"style": {
diff --git a/frontend/pages/components/Header.vue b/frontend/pages/components/Header.vue
index aed36dc..167fab3 100644
--- a/frontend/pages/components/Header.vue
+++ b/frontend/pages/components/Header.vue
@@ -195,15 +195,22 @@ onMounted(() => {
uni.$on('userInfoUpdated', handleUserInfoUpdate);
uni.$on('balanceUpdated', handleBalanceUpdate);
- // 上报每日首次登录事件(每天首次挂载时)
- const today = new Date().toISOString().split('T')[0]
+ // 上报每日首次登录事件(每日5点重置后首次挂载时)
+ // 5点之前属于昨日周期,不触发登录事件
+ const now = new Date()
+ const currentHour = now.getHours()
+ if (currentHour < 5) return // 5点之前不触发
+
+ const today = now.toISOString().split('T')[0]
const dailyLoginKey = `daily_login_completed_${today}`
// 先判断是否处于登录状态(是否有 star_id)
const starId = uni.getStorageSync('star_id')
if (starId && !uni.getStorageSync(dailyLoginKey)) {
reportEvent('daily_login', starId).then(() => {
- // 报告成功后标记今日已完成
+ // 报告成功后标记今日已完成,并清除昨日的记录
uni.setStorageSync(dailyLoginKey, true)
+ const yesterday = new Date(now.getTime() - 86400000).toISOString().split('T')[0]
+ uni.removeStorageSync(`daily_login_completed_${yesterday}`)
}).catch(err => {
console.error('上报登录事件失败:', err)
})
diff --git a/frontend/pages/components/StarbookContent.vue b/frontend/pages/components/StarbookContent.vue
index d3ae1cd..a52f467 100644
--- a/frontend/pages/components/StarbookContent.vue
+++ b/frontend/pages/components/StarbookContent.vue
@@ -2,31 +2,185 @@
-
+
-
-
-
+
+
-
+ 普通
+
+
+ 典藏
+
+
+ 活动
+
+
+
+
+
+ 加载中...
+
+
+
+
+ 暂无藏品
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+ ★{{ item.like_count }}
+
+
+
+
+
+
+ 更多>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+ ★{{ item.like_count }}
+
+
+
+
+
+
+ 更多>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+ ★{{ item.like_count }}
+
+
+
+
+
+
+ 更多>
+
+
+
+
-
@@ -34,8 +188,7 @@
import { ref, computed, onMounted, onActivated, watch } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import NftCard from './NftCard.vue';
-import { getMyAssetsApi } from '@/utils/api.js';
-import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
+import { getStarbookHomeApi } from '@/utils/api.js';
// 屏幕宽度
const screenWidth = ref(0);
@@ -43,20 +196,21 @@ const screenWidth = ref(0);
// 加载状态
const loading = ref(false);
-// NFT列表(初始为15个卡片,将通过API填充)
-const nftList = ref([]);
+// 当前选中的类型
+const currentType = ref('regular');
+// 星册首页数据
+const starbookData = ref([]);
+// 上次加载时间(用于防抖)
+let lastLoadedAt = 0;
// 计算卡片尺寸
const cardSize = computed(() => {
if (screenWidth.value === 0) return 200;
- // 每行3个卡片,留出padding和间距
- // rpx转px:1rpx = screenWidth / 750
const rpxToPx = screenWidth.value / 750;
- const padding = 40 * rpxToPx; // 左右各40rpx
- const gap = 15 * rpxToPx; // 卡片间距15rpx(3列有2个间距)
- // 可用宽度 = 屏幕宽度 - 左右padding - 2个间距
+ const padding = 30 * rpxToPx; // 左右各30rpx
+ const gap = 15 * rpxToPx; // 卡片间距15rpx
const availableWidth = screenWidth.value - (padding * 2) - (gap * 2);
return Math.floor(availableWidth / 3);
});
@@ -68,63 +222,57 @@ const cardCustomStyle = {
left: '0'
};
-// 添加NFT处理 - 跳转到铸爱页面
-const handleAddNft = () => {
- // 跳转到铸爱商城页面
- uni.navigateTo({
- url: '/pages/castlove/mall'
- });
+// grade 中文转换
+const gradeMap = { 1: '一', 2: '二', 3: '三', 4: '四', 5: '五' };
+function formatGrade(grade) {
+ return `等级${gradeMap[grade] || grade}`;
+}
+
+// 判断是否有数据
+const hasData = computed(() => {
+ return starbookData.value.length > 0;
+});
+
+// 根据当前类型获取分组数据
+const regularGroups = computed(() => {
+ const group = starbookData.value.find(g => g.type === 'regular');
+ if (!group) return [];
+ return (group.grades || []).sort((a, b) => b.grade - a.grade); // grade大的在上
+});
+
+const collectionGroups = computed(() => {
+ return starbookData.value.filter(g => g.type === 'collection');
+});
+
+const activityGroups = computed(() => {
+ return starbookData.value.filter(g => g.type === 'activity');
+});
+
+// 切换类型
+const switchType = (type) => {
+ currentType.value = type;
};
-// 加载藏品列表
-const loadAssetsList = async () => {
+// 加载星册数据
+const loadStarbookData = async () => {
+ const now = Date.now();
+ if (now - lastLoadedAt < 1000) return; // 1秒内不重复加载
+ lastLoadedAt = now;
+
loading.value = true;
try {
- const response = await getMyAssetsApi(1, 20);
- if (response.code === 200 && response.data && response.data.items) {
- // 映射后端数据并解析封面URL
- const assetsPromises = response.data.items.map(async item => {
- const realCoverUrl = await getAssetCoverRealUrl(item.cover_url);
- return {
- asset_id: item.asset_id,
- name: item.name,
- image: realCoverUrl,
- cover_url: item.cover_url || '/static/nft/collection.png',
- tx_hash: item.tx_hash,
- like_count: item.like_count,
- status: item.status,
- locked: false
- };
- });
-
- const assets = await Promise.all(assetsPromises);
-
- // 添加一个空白卡片用于添加新藏品
- assets.push({ image: '', locked: false });
-
- // 填充剩余位置为锁定卡片(总共15个卡片)
- const totalCards = 15;
- const remainingCards = totalCards - assets.length;
- for (let i = 0; i < remainingCards; i++) {
- assets.push({ image: '', locked: true });
- }
-
- nftList.value = assets;
+ const response = await getStarbookHomeApi();
+ if (response.code === 200 && response.data && response.data.groups) {
+ starbookData.value = response.data.groups;
}
} catch (error) {
- console.error('获取藏品列表失败:', error);
+ console.error('获取星册数据失败:', error);
uni.showToast({
- title: error.message || '获取藏品列表失败',
+ title: error.message || '获取星册数据失败',
icon: 'none',
duration: 2000
});
-
- // 失败时使用默认布局(1个空白卡片 + 14个锁定卡片)
- const defaultList = [{ image: '', locked: false }];
- for (let i = 0; i < 14; i++) {
- defaultList.push({ image: '', locked: true });
- }
- nftList.value = defaultList;
+ starbookData.value = [];
} finally {
loading.value = false;
}
@@ -132,14 +280,31 @@ const loadAssetsList = async () => {
// 点击卡片跳转到详情页
const handleCardClick = (item) => {
- if (item.image && !item.locked && item.asset_id) {
+ if (item.asset_id) {
uni.navigateTo({
url: `/pages/asset-detail/asset-detail?asset_id=${item.asset_id}`
});
}
};
-// 定义 props 用于接收父组件的显示状态
+// 点击更多跳转到查看更多页面
+const goToMore = (group) => {
+ const params = {
+ type: currentType.value,
+ category: group.category
+ };
+ if (currentType.value === 'regular' && group.grade) {
+ params.grade = group.grade;
+ }
+ const queryString = Object.entries(params)
+ .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
+ .join('&');
+ uni.navigateTo({
+ url: `/pages/starbook/items?${queryString}`
+ });
+};
+
+// 定义 props
const props = defineProps({
isActive: {
type: Boolean,
@@ -150,25 +315,25 @@ const props = defineProps({
onMounted(() => {
const systemInfo = uni.getSystemInfoSync();
screenWidth.value = systemInfo.windowWidth;
-
- // 加载藏品列表
- loadAssetsList();
+ loadStarbookData();
});
-// 每次组件激活时重新加载数据(keep-alive场景)
+// 每次组件激活时重新加载数据
onActivated(() => {
- loadAssetsList();
+ loadStarbookData();
});
-// 监听页面显示事件(页面级切换场景)
+// 监听页面显示事件
onShow(() => {
- loadAssetsList();
+ if (props.isActive) {
+ loadStarbookData();
+ }
});
-// 监听 isActive prop 变化(tab切换场景)
-watch(() => props.isActive, (newValue, oldValue) => {
- if (newValue && !oldValue) {
- loadAssetsList();
+// 监听 isActive prop 变化
+watch(() => props.isActive, (newVal) => {
+ if (newVal) {
+ loadStarbookData();
}
});
@@ -188,7 +353,7 @@ watch(() => props.isActive, (newValue, oldValue) => {
background: #0d0820;
}
-/* 滚动条样式 - 自动隐藏 */
+/* 滚动条样式 */
.starbook-content::-webkit-scrollbar {
width: 6rpx;
}
@@ -226,31 +391,133 @@ watch(() => props.isActive, (newValue, oldValue) => {
z-index: 1;
width: 100%;
min-height: 100%;
- padding: 250rpx 30rpx;
+ padding: 224rpx 30rpx 120rpx;
box-sizing: border-box;
}
-/* NFT网格容器 */
-.nft-grid-container {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- /* 列间距(与计算逻辑保持一致) */
- column-gap: 15rpx;
- /* 行间距 */
- row-gap: 10rpx;
- width: 100%;
- max-width: 100%;
- /* 确保网格项靠上对齐 */
- align-items: start;
- justify-items: center;
+/* 类型Tab */
+.type-tabs {
+ display: flex;
+ justify-content: center;
+ gap: 40rpx;
+ margin-bottom: 30rpx;
+ padding: 0 20rpx;
}
+.tab-item {
+ padding: 12rpx 30rpx;
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+ border-bottom: 4rpx solid transparent;
+ transition: all 0.3s ease;
+}
+
+.tab-item.active {
+ color: #ffffff;
+ border-bottom-color: #ffffff;
+}
+
+/* 加载中 */
+.loading-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding-top: 200rpx;
+}
+
+.loading-text {
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 28rpx;
+}
+
+/* 空状态 */
+.empty-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding-top: 200rpx;
+}
+
+.empty-text {
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 28rpx;
+}
+
+/* 藏品列表容器 */
+.nft-list-container {
+ width: 100%;
+}
+
+/* 藏品分组 */
+.nft-group {
+ margin-bottom: 40rpx;
+}
+
+/* 分组标题 */
+.group-header {
+ margin-bottom: 20rpx;
+}
+
+.group-title {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.8);
+}
+
+/* 藏品行(横向滚动) */
+.nft-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15rpx;
+}
+
+/* 藏品网格项 */
.nft-grid-item {
position: relative;
- width: 100%;
- /* 保持3:4比例 */
- padding-top: 133.33%;
- /* 确保内容靠上对齐 */
+ width: 210rpx;
+ flex-shrink: 0;
+}
+
+.nft-grid-item.more-item {
+ cursor: pointer;
+}
+
+/* 藏品信息 */
+.nft-info {
+ padding: 8rpx 0;
+ text-align: center;
+}
+
+.nft-name {
display: block;
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.9);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.nft-likes {
+ font-size: 20rpx;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+/* 更多覆盖层 */
+.more-overlay {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0, 0, 0, 0.5);
+ border-radius: 16rpx;
+}
+
+.more-text {
+ font-size: 26rpx;
+ color: #ffffff;
}
diff --git a/frontend/pages/exhibition/exhibition.vue b/frontend/pages/exhibition/exhibition.vue
index 6f2bf56..afa1680 100644
--- a/frontend/pages/exhibition/exhibition.vue
+++ b/frontend/pages/exhibition/exhibition.vue
@@ -688,15 +688,16 @@ export default {
// 加载展馆槽位信息
// 将 API 响应解析并填充到展馆数据中(供 loadGallerySlots 和 loadRandomGallery 共用)
const parseGalleryResponse = async (data) => {
- // 如果当前是查看自己的展馆,强制使用 currentUserUid
- // 防止 API 返回的 gallery_owner_id 与当前用户 UID 不一致
- if (!isViewingOthers.value && currentUserUid.value) {
- visitingGalleryOwnerUid.value = currentUserUid.value;
- galleryOwnerNickname.value = '';
- } else {
- visitingGalleryOwnerUid.value = data.gallery_owner_id;
- galleryOwnerNickname.value = data.nickname || '';
- }
+ console.log('parseGalleryResponse called', {
+ isViewingOthers: isViewingOthers.value,
+ dataGalleryOwnerId: data.gallery_owner_id,
+ currentUserUid: currentUserUid.value
+ });
+
+ // 只更新展馆所有者信息,不要修改 isViewingOthers
+ // isViewingOthers 只在 onLoad 时设置,避免状态混乱
+ visitingGalleryOwnerUid.value = data.gallery_owner_id;
+ galleryOwnerNickname.value = data.nickname || '';
galleryOwnerId.value = data.gallery_owner_id;
uni.setStorageSync('gallery_owner_id', data.gallery_owner_id);
@@ -743,10 +744,16 @@ export default {
};
const loadGallerySlots = async () => {
+ console.log('loadGallerySlots called', {
+ isViewingOthers: isViewingOthers.value,
+ visitingGalleryOwnerUid: visitingGalleryOwnerUid.value,
+ currentUserUid: currentUserUid.value
+ });
try {
let response;
- // 使用 isViewingOthers 判断,因为它在 onLoad 中同步设置,比 isMyGallery 更可靠
- if (!isViewingOthers.value) {
+ // 根据 visitingGalleryOwnerUid 是否等于 currentUserUid 来判断是否调用自己的展馆 API
+ // 这样可以避免 isViewingOthers 状态错误导致的 API 调用错误
+ if (visitingGalleryOwnerUid.value === currentUserUid.value) {
response = await getMyGalleriesApi();
} else {
response = await getUserGalleriesApi(visitingGalleryOwnerUid.value);
@@ -1217,6 +1224,17 @@ export default {
visitingGalleryOwnerUid.value = currentUserUid.value;
isViewingOthers.value = false;
}
+
+ // 处理引导跳转参数:如果传递了 guide_key,则继续该引导
+ if (options && options.guide_key) {
+ console.log('[Guide] exhibition 收到引导跳转参数, guide_key:', options.guide_key, 'guide_step:', options.guide_step)
+ // 使用 resumeGuide 继续引导(不会检查 shouldShowGuide)
+ store.dispatch('guide/resumeGuide', options.guide_key).then(res => {
+ console.log('[Guide] exhibition resumeGuide 结果:', res)
+ }).catch(err => {
+ console.error('[Guide] exhibition resumeGuide 失败:', err)
+ })
+ }
});
// 处理点赞数实时更新
diff --git a/frontend/pages/profile/profile.vue b/frontend/pages/profile/profile.vue
index a0fc279..a95ae31 100644
--- a/frontend/pages/profile/profile.vue
+++ b/frontend/pages/profile/profile.vue
@@ -252,13 +252,13 @@
@claim-success="handleClaimSuccess" @close="showGuideListModal = false" />
-
+
+
+
diff --git a/frontend/pages/tasks/daily-tasks.vue b/frontend/pages/tasks/daily-tasks.vue
index 2f2d513..4ca1949 100644
--- a/frontend/pages/tasks/daily-tasks.vue
+++ b/frontend/pages/tasks/daily-tasks.vue
@@ -75,11 +75,29 @@
-
-
-
+
+
+
+
+
+
+
+ {{ milestone }}
+
+
+
+
+
+
+
+
@@ -110,6 +128,15 @@ const starId = ref(1)
const hasClaimableTasks = computed(() => tasks.value.some(t => t.can_claim))
const claimableCount = computed(() => tasks.value.filter(t => t.can_claim).length)
+// 进度里程碑
+const milestones = ref(['任务1', '任务2', '任务3', '任务4'])
+
+// 已完成的里程碑数量
+const completedCount = computed(() => {
+ // 计算已领取的任务数量
+ return tasks.value.filter(t => t.status === 'claimed').length
+})
+
// 监听 visible 变化,自动加载数据
watch(() => props.visible, (newVal) => {
if (newVal) {
@@ -485,6 +512,67 @@ const handleCloseClick = (e) => {
margin-top: 20rpx;
}
+.bottom-section {
+ display: flex;
+ flex-direction: column;
+ gap: 20rpx;
+}
+
+.progress-section {
+ width: 100%;
+}
+
+.progress-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ position: relative;
+ height: 60rpx;
+ padding: 0 30rpx;
+}
+
+.progress-bar::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50rpx;
+ right: 50rpx;
+ height: 4rpx;
+ background: rgba(255, 255, 255, 0.2);
+ transform: translateY(-50%);
+ z-index: 0;
+}
+
+.progress-node {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ position: relative;
+ z-index: 1;
+}
+
+.node-circle {
+ width: 24rpx;
+ height: 24rpx;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.3);
+ border: 4rpx solid rgba(255, 255, 255, 0.3);
+ transition: all 0.3s ease;
+}
+
+.progress-node.active .node-circle {
+ background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
+ border-color: #F08399;
+}
+
+.node-label {
+ font-size: 22rpx;
+ color: rgba(230, 230, 230, 0.7);
+ margin-top: 8rpx;
+ font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
+ text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
+}
+
.claim-all-btn {
width: 100%;
padding: 24rpx 0;
diff --git a/frontend/pages/tasks/guide.vue b/frontend/pages/tasks/guide.vue
index 741e0bd..c8f0766 100644
--- a/frontend/pages/tasks/guide.vue
+++ b/frontend/pages/tasks/guide.vue
@@ -11,96 +11,103 @@
新手引导
-
-
-
- 加载中...
-
-
-
-
- {{ errorMessage }}
-
-
-
-
-
-
-
-
- {{ currentStageName }}
-
- 💎
- {{ currentStageReward }}
- |
- ⭐
- {{ currentStageExp }} 经验
-
-
-
-
-
-
-