fix: 修改任务系统的bug
This commit is contained in:
parent
9e4cbab9b4
commit
bebdc13c80
@ -53,7 +53,7 @@
|
|||||||
```
|
```
|
||||||
ssh root@101.132.250.62
|
ssh root@101.132.250.62
|
||||||
|
|
||||||
>n73qBnCja-,#VF+Wq
|
R251Y>Y8inL_BM=W
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1.1 更新系统包
|
### 1.1 更新系统包
|
||||||
|
|||||||
@ -290,6 +290,7 @@ func (ctrl *TaskController) CompleteGuide(c *gin.Context) {
|
|||||||
|
|
||||||
var req struct {
|
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 {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
response.Error(c, http.StatusBadRequest, "请求参数错误")
|
response.Error(c, http.StatusBadRequest, "请求参数错误")
|
||||||
@ -303,6 +304,7 @@ func (ctrl *TaskController) CompleteGuide(c *gin.Context) {
|
|||||||
logger.Logger.Info("CompleteGuide request",
|
logger.Logger.Info("CompleteGuide request",
|
||||||
zap.Int64("user_id", userID.(int64)),
|
zap.Int64("user_id", userID.(int64)),
|
||||||
zap.String("task_key", req.TaskKey),
|
zap.String("task_key", req.TaskKey),
|
||||||
|
zap.Int("stages_count", len(req.Stages)),
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
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{
|
resp, err := ctrl.taskMobileService.CompleteGuide(ctx, &pbTask.CompleteGuideRequest{
|
||||||
TaskKey: req.TaskKey,
|
TaskKey: req.TaskKey,
|
||||||
|
Stages: req.Stages,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -477,6 +480,8 @@ func (ctrl *TaskController) ClaimOnboardingReward(c *gin.Context) {
|
|||||||
|
|
||||||
response.Success(c, map[string]interface{}{
|
response.Success(c, map[string]interface{}{
|
||||||
"success": resp.Success,
|
"success": resp.Success,
|
||||||
|
"crystal_balance": resp.CrystalBalance,
|
||||||
|
"experience": resp.Experience,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -604,6 +604,7 @@ type OnboardingStage struct {
|
|||||||
ExpReward int64 `protobuf:"varint,5,opt,name=exp_reward,json=expReward,proto3" json:"exp_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
|
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"`
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -687,9 +688,17 @@ func (x *OnboardingStage) GetIsCurrent() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *OnboardingStage) GetAllTasksCompleted() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.AllTasksCompleted
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type CompleteGuideRequest struct {
|
type CompleteGuideRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
TaskKey string `protobuf:"bytes,1,opt,name=task_key,json=taskKey,proto3" json:"task_key,omitempty"`
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -731,6 +740,13 @@ func (x *CompleteGuideRequest) GetTaskKey() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *CompleteGuideRequest) GetStages() []*OnboardingStage {
|
||||||
|
if x != nil {
|
||||||
|
return x.Stages
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type CompleteGuideResponse struct {
|
type CompleteGuideResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
|
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
|
||||||
@ -1079,6 +1095,8 @@ type ClaimOnboardingRewardResponse struct {
|
|||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
|
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"`
|
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
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -1127,6 +1145,20 @@ func (x *ClaimOnboardingRewardResponse) GetSuccess() bool {
|
|||||||
return false
|
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 {
|
type ExhibitionRevenueItem struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
@ -1910,7 +1942,7 @@ const file_task_proto_rawDesc = "" +
|
|||||||
"\n" +
|
"\n" +
|
||||||
"experience\x18\x04 \x01(\x03R\n" +
|
"experience\x18\x04 \x01(\x03R\n" +
|
||||||
"experience\x12*\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" +
|
"\x0fOnboardingStage\x12\x14\n" +
|
||||||
"\x05stage\x18\x01 \x01(\x05R\x05stage\x12\x12\n" +
|
"\x05stage\x18\x01 \x01(\x05R\x05stage\x12\x12\n" +
|
||||||
"\x04name\x18\x02 \x01(\tR\x04name\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" +
|
"exp_reward\x18\x05 \x01(\x03R\texpReward\x12\x16\n" +
|
||||||
"\x06status\x18\x06 \x01(\tR\x06status\x12\x1d\n" +
|
"\x06status\x18\x06 \x01(\tR\x06status\x12\x1d\n" +
|
||||||
"\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" +
|
"\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" +
|
"\x15CompleteGuideResponse\x120\n" +
|
||||||
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x17\n" +
|
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x17\n" +
|
||||||
"\auser_id\x18\x02 \x01(\x03R\x06userId\x12#\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" +
|
"\x06status\x18\x03 \x01(\tR\x06status\x125\n" +
|
||||||
"\x06stages\x18\x04 \x03(\v2\x1d.topfans.task.OnboardingStageR\x06stages\"4\n" +
|
"\x06stages\x18\x04 \x03(\v2\x1d.topfans.task.OnboardingStageR\x06stages\"4\n" +
|
||||||
"\x1cClaimOnboardingRewardRequest\x12\x14\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" +
|
"\x1dClaimOnboardingRewardResponse\x120\n" +
|
||||||
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x18\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" +
|
"\x15ExhibitionRevenueItem\x12\x0e\n" +
|
||||||
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x17\n" +
|
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x17\n" +
|
||||||
"\astar_id\x18\x02 \x01(\x03R\x06starId\x12#\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, // 2: topfans.task.ReportEventResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
29, // 3: topfans.task.ClaimDailyTaskResponse.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, // 4: topfans.task.ClaimAllDailyTasksResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
29, // 5: topfans.task.CompleteGuideResponse.base:type_name -> topfans.common.BaseResponse
|
9, // 5: topfans.task.CompleteGuideRequest.stages:type_name -> topfans.task.OnboardingStage
|
||||||
9, // 6: topfans.task.CompleteGuideResponse.stages:type_name -> topfans.task.OnboardingStage
|
29, // 6: topfans.task.CompleteGuideResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
29, // 7: topfans.task.GetOnboardingStatusResponse.base:type_name -> topfans.common.BaseResponse
|
9, // 7: topfans.task.CompleteGuideResponse.stages:type_name -> topfans.task.OnboardingStage
|
||||||
9, // 8: topfans.task.GetOnboardingStatusResponse.stages:type_name -> topfans.task.OnboardingStage
|
29, // 8: topfans.task.GetOnboardingStatusResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
29, // 9: topfans.task.AdvanceStageResponse.base:type_name -> topfans.common.BaseResponse
|
9, // 9: topfans.task.GetOnboardingStatusResponse.stages:type_name -> topfans.task.OnboardingStage
|
||||||
9, // 10: topfans.task.AdvanceStageResponse.stages:type_name -> topfans.task.OnboardingStage
|
29, // 10: topfans.task.AdvanceStageResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
29, // 11: topfans.task.ClaimOnboardingRewardResponse.base:type_name -> topfans.common.BaseResponse
|
9, // 11: topfans.task.AdvanceStageResponse.stages:type_name -> topfans.task.OnboardingStage
|
||||||
29, // 12: topfans.task.GetExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse
|
29, // 12: topfans.task.ClaimOnboardingRewardResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
18, // 13: topfans.task.GetExhibitionRevenueResponse.items:type_name -> topfans.task.ExhibitionRevenueItem
|
29, // 13: topfans.task.GetExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
29, // 14: topfans.task.ClaimExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse
|
18, // 14: topfans.task.GetExhibitionRevenueResponse.items:type_name -> topfans.task.ExhibitionRevenueItem
|
||||||
29, // 15: topfans.task.ClaimAllExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse
|
29, // 15: topfans.task.ClaimExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
29, // 16: topfans.task.InitUserTasksResponse.base:type_name -> topfans.common.BaseResponse
|
29, // 16: topfans.task.ClaimAllExhibitionRevenueResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
29, // 17: topfans.task.OnExhibitionCompletedResponse.base:type_name -> topfans.common.BaseResponse
|
29, // 17: topfans.task.InitUserTasksResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
1, // 18: topfans.task.TaskMobileService.GetDailyTasks:input_type -> topfans.task.GetDailyTasksRequest
|
29, // 18: topfans.task.OnExhibitionCompletedResponse.base:type_name -> topfans.common.BaseResponse
|
||||||
3, // 19: topfans.task.TaskMobileService.ReportEvent:input_type -> topfans.task.ReportEventRequest
|
1, // 19: topfans.task.TaskMobileService.GetDailyTasks:input_type -> topfans.task.GetDailyTasksRequest
|
||||||
5, // 20: topfans.task.TaskMobileService.ClaimDailyTask:input_type -> topfans.task.ClaimDailyTaskRequest
|
3, // 20: topfans.task.TaskMobileService.ReportEvent:input_type -> topfans.task.ReportEventRequest
|
||||||
7, // 21: topfans.task.TaskMobileService.ClaimAllDailyTasks:input_type -> topfans.task.ClaimAllDailyTasksRequest
|
5, // 21: topfans.task.TaskMobileService.ClaimDailyTask:input_type -> topfans.task.ClaimDailyTaskRequest
|
||||||
10, // 22: topfans.task.TaskMobileService.CompleteGuide:input_type -> topfans.task.CompleteGuideRequest
|
7, // 22: topfans.task.TaskMobileService.ClaimAllDailyTasks:input_type -> topfans.task.ClaimAllDailyTasksRequest
|
||||||
12, // 23: topfans.task.TaskMobileService.GetOnboardingStatus:input_type -> topfans.task.GetOnboardingStatusRequest
|
10, // 23: topfans.task.TaskMobileService.CompleteGuide:input_type -> topfans.task.CompleteGuideRequest
|
||||||
14, // 24: topfans.task.TaskMobileService.AdvanceStage:input_type -> topfans.task.AdvanceStageRequest
|
12, // 24: topfans.task.TaskMobileService.GetOnboardingStatus:input_type -> topfans.task.GetOnboardingStatusRequest
|
||||||
16, // 25: topfans.task.TaskMobileService.ClaimOnboardingReward:input_type -> topfans.task.ClaimOnboardingRewardRequest
|
14, // 25: topfans.task.TaskMobileService.AdvanceStage:input_type -> topfans.task.AdvanceStageRequest
|
||||||
19, // 26: topfans.task.TaskMobileService.GetExhibitionRevenue:input_type -> topfans.task.GetExhibitionRevenueRequest
|
16, // 26: topfans.task.TaskMobileService.ClaimOnboardingReward:input_type -> topfans.task.ClaimOnboardingRewardRequest
|
||||||
21, // 27: topfans.task.TaskMobileService.ClaimExhibitionRevenue:input_type -> topfans.task.ClaimExhibitionRevenueRequest
|
19, // 27: topfans.task.TaskMobileService.GetExhibitionRevenue:input_type -> topfans.task.GetExhibitionRevenueRequest
|
||||||
23, // 28: topfans.task.TaskMobileService.ClaimAllExhibitionRevenue:input_type -> topfans.task.ClaimAllExhibitionRevenueRequest
|
21, // 28: topfans.task.TaskMobileService.ClaimExhibitionRevenue:input_type -> topfans.task.ClaimExhibitionRevenueRequest
|
||||||
25, // 29: topfans.task.TaskInternalService.InitUserTasks:input_type -> topfans.task.InitUserTasksRequest
|
23, // 29: topfans.task.TaskMobileService.ClaimAllExhibitionRevenue:input_type -> topfans.task.ClaimAllExhibitionRevenueRequest
|
||||||
27, // 30: topfans.task.TaskInternalService.OnExhibitionCompleted:input_type -> topfans.task.OnExhibitionCompletedRequest
|
25, // 30: topfans.task.TaskInternalService.InitUserTasks:input_type -> topfans.task.InitUserTasksRequest
|
||||||
2, // 31: topfans.task.TaskMobileService.GetDailyTasks:output_type -> topfans.task.GetDailyTasksResponse
|
27, // 31: topfans.task.TaskInternalService.OnExhibitionCompleted:input_type -> topfans.task.OnExhibitionCompletedRequest
|
||||||
4, // 32: topfans.task.TaskMobileService.ReportEvent:output_type -> topfans.task.ReportEventResponse
|
2, // 32: topfans.task.TaskMobileService.GetDailyTasks:output_type -> topfans.task.GetDailyTasksResponse
|
||||||
6, // 33: topfans.task.TaskMobileService.ClaimDailyTask:output_type -> topfans.task.ClaimDailyTaskResponse
|
4, // 33: topfans.task.TaskMobileService.ReportEvent:output_type -> topfans.task.ReportEventResponse
|
||||||
8, // 34: topfans.task.TaskMobileService.ClaimAllDailyTasks:output_type -> topfans.task.ClaimAllDailyTasksResponse
|
6, // 34: topfans.task.TaskMobileService.ClaimDailyTask:output_type -> topfans.task.ClaimDailyTaskResponse
|
||||||
11, // 35: topfans.task.TaskMobileService.CompleteGuide:output_type -> topfans.task.CompleteGuideResponse
|
8, // 35: topfans.task.TaskMobileService.ClaimAllDailyTasks:output_type -> topfans.task.ClaimAllDailyTasksResponse
|
||||||
13, // 36: topfans.task.TaskMobileService.GetOnboardingStatus:output_type -> topfans.task.GetOnboardingStatusResponse
|
11, // 36: topfans.task.TaskMobileService.CompleteGuide:output_type -> topfans.task.CompleteGuideResponse
|
||||||
15, // 37: topfans.task.TaskMobileService.AdvanceStage:output_type -> topfans.task.AdvanceStageResponse
|
13, // 37: topfans.task.TaskMobileService.GetOnboardingStatus:output_type -> topfans.task.GetOnboardingStatusResponse
|
||||||
17, // 38: topfans.task.TaskMobileService.ClaimOnboardingReward:output_type -> topfans.task.ClaimOnboardingRewardResponse
|
15, // 38: topfans.task.TaskMobileService.AdvanceStage:output_type -> topfans.task.AdvanceStageResponse
|
||||||
20, // 39: topfans.task.TaskMobileService.GetExhibitionRevenue:output_type -> topfans.task.GetExhibitionRevenueResponse
|
17, // 39: topfans.task.TaskMobileService.ClaimOnboardingReward:output_type -> topfans.task.ClaimOnboardingRewardResponse
|
||||||
22, // 40: topfans.task.TaskMobileService.ClaimExhibitionRevenue:output_type -> topfans.task.ClaimExhibitionRevenueResponse
|
20, // 40: topfans.task.TaskMobileService.GetExhibitionRevenue:output_type -> topfans.task.GetExhibitionRevenueResponse
|
||||||
24, // 41: topfans.task.TaskMobileService.ClaimAllExhibitionRevenue:output_type -> topfans.task.ClaimAllExhibitionRevenueResponse
|
22, // 41: topfans.task.TaskMobileService.ClaimExhibitionRevenue:output_type -> topfans.task.ClaimExhibitionRevenueResponse
|
||||||
26, // 42: topfans.task.TaskInternalService.InitUserTasks:output_type -> topfans.task.InitUserTasksResponse
|
24, // 42: topfans.task.TaskMobileService.ClaimAllExhibitionRevenue:output_type -> topfans.task.ClaimAllExhibitionRevenueResponse
|
||||||
28, // 43: topfans.task.TaskInternalService.OnExhibitionCompleted:output_type -> topfans.task.OnExhibitionCompletedResponse
|
26, // 43: topfans.task.TaskInternalService.InitUserTasks:output_type -> topfans.task.InitUserTasksResponse
|
||||||
31, // [31:44] is the sub-list for method output_type
|
28, // 44: topfans.task.TaskInternalService.OnExhibitionCompleted:output_type -> topfans.task.OnExhibitionCompletedResponse
|
||||||
18, // [18:31] is the sub-list for method input_type
|
32, // [32:45] is the sub-list for method output_type
|
||||||
18, // [18:18] is the sub-list for extension type_name
|
19, // [19:32] is the sub-list for method input_type
|
||||||
18, // [18:18] is the sub-list for extension extendee
|
19, // [19:19] is the sub-list for extension type_name
|
||||||
0, // [0:18] is the sub-list for field 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() }
|
func init() { file_task_proto_init() }
|
||||||
|
|||||||
@ -77,10 +77,12 @@ message OnboardingStage {
|
|||||||
int64 exp_reward = 5;
|
int64 exp_reward = 5;
|
||||||
string status = 6; // pending/completed/in_progress
|
string status = 6; // pending/completed/in_progress
|
||||||
bool is_current = 7;
|
bool is_current = 7;
|
||||||
|
bool all_tasks_completed = 8; // 该阶段所有任务是否完成
|
||||||
}
|
}
|
||||||
|
|
||||||
message CompleteGuideRequest {
|
message CompleteGuideRequest {
|
||||||
string task_key = 1;
|
string task_key = 1;
|
||||||
|
repeated OnboardingStage stages = 2; // 前端传入的阶段配置,首次调用时存储
|
||||||
}
|
}
|
||||||
|
|
||||||
message CompleteGuideResponse {
|
message CompleteGuideResponse {
|
||||||
@ -119,6 +121,8 @@ message ClaimOnboardingRewardRequest {
|
|||||||
message ClaimOnboardingRewardResponse {
|
message ClaimOnboardingRewardResponse {
|
||||||
topfans.common.BaseResponse base = 1;
|
topfans.common.BaseResponse base = 1;
|
||||||
bool success = 2;
|
bool success = 2;
|
||||||
|
string crystal_balance = 3; // 使用 string 避免 Dubbo int64 序列化 bug
|
||||||
|
string experience = 4; // 使用 string 避免 Dubbo int64 序列化 bug
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 展示收益 ====================
|
// ==================== 展示收益 ====================
|
||||||
|
|||||||
@ -118,8 +118,9 @@ func (r *rankingRepository) getHotRankingByDimension(starID int64, dimension str
|
|||||||
Where("exhibitions.expire_at > ?", now).
|
Where("exhibitions.expire_at > ?", now).
|
||||||
Where("fan_profiles.star_id = ?", starID)
|
Where("fan_profiles.star_id = ?", starID)
|
||||||
case "month":
|
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":
|
case "total":
|
||||||
// 全部:直接使用 assets 表的 like_count,无需额外条件
|
// 全部:直接使用 assets 表的 like_count,无需额外条件
|
||||||
}
|
}
|
||||||
@ -140,7 +141,9 @@ func (r *rankingRepository) getHotRankingByDimension(starID int64, dimension str
|
|||||||
Where("exhibitions.expire_at > ?", now).
|
Where("exhibitions.expire_at > ?", now).
|
||||||
Where("host_fp.star_id = ?", starID)
|
Where("host_fp.star_id = ?", starID)
|
||||||
case "month":
|
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 {
|
if err := countDB.Count(&total).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@ -190,8 +193,9 @@ func (r *rankingRepository) GetMyBestRanking(userID, starID int64, dimension str
|
|||||||
Where("exhibitions.expire_at > ?", now).
|
Where("exhibitions.expire_at > ?", now).
|
||||||
Where("host_fp.star_id = ?", starID)
|
Where("host_fp.star_id = ?", starID)
|
||||||
case "month":
|
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下点赞数最高的藏品
|
// 获取用户在该star下点赞数最高的藏品
|
||||||
@ -232,7 +236,9 @@ func (r *rankingRepository) GetMyBestRanking(userID, starID int64, dimension str
|
|||||||
Where("exhibitions.expire_at > ?", now).
|
Where("exhibitions.expire_at > ?", now).
|
||||||
Where("host_fp.star_id = ?", starID)
|
Where("host_fp.star_id = ?", starID)
|
||||||
case "month":
|
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 {
|
if err := rankingDB.Count(&rank).Error; err != nil {
|
||||||
|
|||||||
@ -159,11 +159,12 @@ func (p *TaskMobileProvider) CompleteGuide(ctx context.Context, req *pb.Complete
|
|||||||
return &pb.CompleteGuideResponse{}, nil
|
return &pb.CompleteGuideResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Logger.Debug("CompleteGuide",
|
logger.Logger.Info("CompleteGuide",
|
||||||
zap.Int64("user_id", userID),
|
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) {
|
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
|
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)
|
return p.onboardingSvc.ClaimOnboardingReward(ctx, userID, starID, req.Stage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import (
|
|||||||
type OnboardingRepository interface {
|
type OnboardingRepository interface {
|
||||||
GetOnboardingStatus(userID int64, starID int64) (*model.UserOnboardingStatus, error)
|
GetOnboardingStatus(userID int64, starID int64) (*model.UserOnboardingStatus, error)
|
||||||
GetOrCreateOnboardingStatus(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
|
UpdateOnboardingStatus(status *model.UserOnboardingStatus) error
|
||||||
UpdateOnboardingProgress(progress *model.UserOnboardingProgress) error
|
UpdateOnboardingProgress(progress *model.UserOnboardingProgress) error
|
||||||
GetUserOnboardingProgress(userID int64, taskKey string) (*model.UserOnboardingProgress, error)
|
GetUserOnboardingProgress(userID int64, taskKey string) (*model.UserOnboardingProgress, error)
|
||||||
@ -20,6 +21,8 @@ type OnboardingRepository interface {
|
|||||||
ListActiveStageConfigs() ([]*model.OnboardingStageConfig, error)
|
ListActiveStageConfigs() ([]*model.OnboardingStageConfig, error)
|
||||||
ListUserOnboardingProgressByUser(userID int64) ([]*model.UserOnboardingProgress, error)
|
ListUserOnboardingProgressByUser(userID int64) ([]*model.UserOnboardingProgress, error)
|
||||||
GetStageConfig(stage int64) (*model.OnboardingStageConfig, error)
|
GetStageConfig(stage int64) (*model.OnboardingStageConfig, error)
|
||||||
|
SaveStageConfigs(configs []*model.OnboardingStageConfig) error
|
||||||
|
CountStageConfigs() (int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type onboardingRepository struct {
|
type onboardingRepository struct {
|
||||||
@ -65,6 +68,16 @@ func (r *onboardingRepository) GetOrCreateOnboardingStatus(userID int64, starID
|
|||||||
return &status, nil
|
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 {
|
func (r *onboardingRepository) UpdateOnboardingStatus(status *model.UserOnboardingStatus) error {
|
||||||
status.UpdatedAt = time.Now().Unix()
|
status.UpdatedAt = time.Now().Unix()
|
||||||
if err := r.db.Save(status).Error; err != nil {
|
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) {
|
func (r *onboardingRepository) GetStageConfig(stage int64) (*model.OnboardingStageConfig, error) {
|
||||||
var config model.OnboardingStageConfig
|
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
|
err := r.db.Where("stage = ? AND is_active = ?", stage, true).First(&config).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Logger.Error("GetStageConfig: query failed",
|
||||||
|
zap.Int64("stage", stage),
|
||||||
|
zap.Error(err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
logger.Logger.Info("GetStageConfig: found",
|
||||||
|
zap.Int64("stage", stage),
|
||||||
|
zap.String("name", config.Name))
|
||||||
return &config, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -55,13 +55,27 @@ import { useStore } from 'vuex'
|
|||||||
const store = useStore()
|
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 isComponentMode = computed(() => store.state.guide.componentMode)
|
||||||
const targetRect = computed(() => store.state.guide.targetRect)
|
const targetRect = computed(() => store.state.guide.targetRect)
|
||||||
const stepConfig = computed(() => store.getters['guide/currentStepConfig'])
|
const stepConfig = computed(() => store.getters['guide/currentStepConfig'])
|
||||||
const isFirst = computed(() => store.getters['guide/isFirst'])
|
const isFirst = computed(() => store.getters['guide/isFirst'])
|
||||||
const isLast = computed(() => store.getters['guide/isLast'])
|
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 highlightStyle = computed(() => {
|
||||||
const config = stepConfig.value || {}
|
const config = stepConfig.value || {}
|
||||||
@ -271,6 +285,7 @@ watch(() => store.state.guide.currentStep, async () => {
|
|||||||
|
|
||||||
// 组件挂载时初始化
|
// 组件挂载时初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
console.log('[GuideOverlay] mounted, isActive:', store.state.guide.isActive, 'currentGuide:', store.state.guide.currentGuide?.key, 'currentStep:', store.state.guide.currentStep)
|
||||||
if (isActive.value) {
|
if (isActive.value) {
|
||||||
// 延迟执行,确保 DOM 已渲染完成
|
// 延迟执行,确保 DOM 已渲染完成
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -288,16 +303,20 @@ onMounted(() => {
|
|||||||
// 监听组件关闭事件,关闭后执行下一步
|
// 监听组件关闭事件,关闭后执行下一步
|
||||||
let closeComponentHandled = false // 防止重复处理的标志
|
let closeComponentHandled = false // 防止重复处理的标志
|
||||||
uni.$on('guide:closeComponent', () => {
|
uni.$on('guide:closeComponent', () => {
|
||||||
|
console.log('[GuideOverlay] 收到 guide:closeComponent 事件, componentMode:', store.state.guide.componentMode)
|
||||||
// 如果已经处理过或者 componentMode 已经是 false,忽略
|
// 如果已经处理过或者 componentMode 已经是 false,忽略
|
||||||
if (closeComponentHandled || !store.state.guide.componentMode) {
|
if (closeComponentHandled || !store.state.guide.componentMode) {
|
||||||
|
console.log('[GuideOverlay] guide:closeComponent 忽略, closeComponentHandled:', closeComponentHandled, 'componentMode:', store.state.guide.componentMode)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
closeComponentHandled = true
|
closeComponentHandled = true
|
||||||
store.commit('guide/RESET_COMPONENT_MODE')
|
store.commit('guide/RESET_COMPONENT_MODE')
|
||||||
// component action 后需要手动前进到下一步
|
// component action 后需要手动前进到下一步
|
||||||
if (store.getters['guide/hasNext']) {
|
if (store.getters['guide/hasNext']) {
|
||||||
|
console.log('[GuideOverlay] 有下一步,前进')
|
||||||
store.commit('guide/NEXT_STEP')
|
store.commit('guide/NEXT_STEP')
|
||||||
} else {
|
} else {
|
||||||
|
console.log('[GuideOverlay] 没有下一步,结束引导')
|
||||||
store.commit('guide/END_GUIDE')
|
store.commit('guide/END_GUIDE')
|
||||||
}
|
}
|
||||||
// 重置标志,允许未来再次处理
|
// 重置标志,允许未来再次处理
|
||||||
@ -356,8 +375,10 @@ function handleTooltipClick() {
|
|||||||
// 点击高亮区域
|
// 点击高亮区域
|
||||||
function handleHighlightClick() {
|
function handleHighlightClick() {
|
||||||
const stepConfigVal = stepConfig.value
|
const stepConfigVal = stepConfig.value
|
||||||
|
console.log('[GuideOverlay] handleHighlightClick, stepConfig:', stepConfigVal)
|
||||||
if (stepConfigVal && stepConfigVal.action) {
|
if (stepConfigVal && stepConfigVal.action) {
|
||||||
const { action } = stepConfigVal
|
const { action } = stepConfigVal
|
||||||
|
console.log('[GuideOverlay] action:', action)
|
||||||
|
|
||||||
if (action.type === 'navigate' && action.url) {
|
if (action.type === 'navigate' && action.url) {
|
||||||
// 路由跳转
|
// 路由跳转
|
||||||
@ -482,6 +503,7 @@ defineExpose({
|
|||||||
z-index: 10001;
|
z-index: 10001;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-content {
|
.tooltip-content {
|
||||||
|
|||||||
@ -26,6 +26,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/starbook/items",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"app-plus": {
|
||||||
|
"bounce": "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/starcity/index",
|
"path": "pages/starcity/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@ -195,15 +195,22 @@ onMounted(() => {
|
|||||||
uni.$on('userInfoUpdated', handleUserInfoUpdate);
|
uni.$on('userInfoUpdated', handleUserInfoUpdate);
|
||||||
uni.$on('balanceUpdated', handleBalanceUpdate);
|
uni.$on('balanceUpdated', handleBalanceUpdate);
|
||||||
|
|
||||||
// 上报每日首次登录事件(每天首次挂载时)
|
// 上报每日首次登录事件(每日5点重置后首次挂载时)
|
||||||
const today = new Date().toISOString().split('T')[0]
|
// 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}`
|
const dailyLoginKey = `daily_login_completed_${today}`
|
||||||
// 先判断是否处于登录状态(是否有 star_id)
|
// 先判断是否处于登录状态(是否有 star_id)
|
||||||
const starId = uni.getStorageSync('star_id')
|
const starId = uni.getStorageSync('star_id')
|
||||||
if (starId && !uni.getStorageSync(dailyLoginKey)) {
|
if (starId && !uni.getStorageSync(dailyLoginKey)) {
|
||||||
reportEvent('daily_login', starId).then(() => {
|
reportEvent('daily_login', starId).then(() => {
|
||||||
// 报告成功后标记今日已完成
|
// 报告成功后标记今日已完成,并清除昨日的记录
|
||||||
uni.setStorageSync(dailyLoginKey, true)
|
uni.setStorageSync(dailyLoginKey, true)
|
||||||
|
const yesterday = new Date(now.getTime() - 86400000).toISOString().split('T')[0]
|
||||||
|
uni.removeStorageSync(`daily_login_completed_${yesterday}`)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('上报登录事件失败:', err)
|
console.error('上报登录事件失败:', err)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -5,28 +5,182 @@
|
|||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<view class="content-wrapper">
|
<view class="content-wrapper">
|
||||||
<!-- NFT卡片网格容器 -->
|
<!-- 类型Tab -->
|
||||||
<view class="nft-grid-container">
|
<view class="type-tabs">
|
||||||
<view
|
<view
|
||||||
v-for="(item, index) in nftList"
|
class="tab-item"
|
||||||
:key="index"
|
:class="{ active: currentType === 'regular' }"
|
||||||
|
@click="switchType('regular')"
|
||||||
|
>
|
||||||
|
<text>普通</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: currentType === 'collection' }"
|
||||||
|
@click="switchType('collection')"
|
||||||
|
>
|
||||||
|
<text>典藏</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: currentType === 'activity' }"
|
||||||
|
@click="switchType('activity')"
|
||||||
|
>
|
||||||
|
<text>活动</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<view v-if="loading" class="loading-container">
|
||||||
|
<text class="loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-else-if="!hasData" class="empty-container">
|
||||||
|
<text class="empty-text">暂无藏品</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 藏品列表 -->
|
||||||
|
<view v-else class="nft-list-container">
|
||||||
|
<!-- 普通藏品:按 grade 分组 -->
|
||||||
|
<view v-if="currentType === 'regular'">
|
||||||
|
<view v-for="group in regularGroups" :key="group.category" class="nft-group">
|
||||||
|
<!-- 分组标题 -->
|
||||||
|
<view class="group-header">
|
||||||
|
<text class="group-title">{{ group.category_name }} · {{ formatGrade(group.grade) }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 分组内容 -->
|
||||||
|
<view class="nft-row">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in group.items"
|
||||||
|
:key="item.asset_id"
|
||||||
class="nft-grid-item"
|
class="nft-grid-item"
|
||||||
|
@click="handleCardClick(item)"
|
||||||
>
|
>
|
||||||
<NftCard
|
<NftCard
|
||||||
:cover-image="item.image"
|
:cover-image="item.cover_url_signed"
|
||||||
:width="cardSize"
|
:width="cardSize"
|
||||||
:height="cardSize"
|
:height="cardSize"
|
||||||
:locked="item.locked"
|
:locked="false"
|
||||||
:show-add-button="!item.locked && !item.image"
|
|
||||||
operation="place"
|
|
||||||
:custom-style="cardCustomStyle"
|
:custom-style="cardCustomStyle"
|
||||||
@add="handleAddNft"
|
|
||||||
@click="handleCardClick(item, index)"
|
|
||||||
/>
|
/>
|
||||||
|
<view class="nft-info">
|
||||||
|
<text class="nft-name">{{ item.name }}</text>
|
||||||
|
<text class="nft-likes">★{{ item.like_count }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 更多按钮 -->
|
||||||
|
<view v-if="group.has_more" class="nft-grid-item more-item" @click="goToMore(group)">
|
||||||
|
<NftCard
|
||||||
|
:cover-image="''"
|
||||||
|
:width="cardSize"
|
||||||
|
:height="cardSize"
|
||||||
|
:locked="false"
|
||||||
|
:show-add-button="false"
|
||||||
|
operation="none"
|
||||||
|
:custom-style="cardCustomStyle"
|
||||||
|
/>
|
||||||
|
<view class="more-overlay">
|
||||||
|
<text class="more-text">更多></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 典藏藏品:按 category 分组 -->
|
||||||
|
<view v-if="currentType === 'collection'">
|
||||||
|
<view v-for="group in collectionGroups" :key="group.category" class="nft-group">
|
||||||
|
<!-- 分组标题 -->
|
||||||
|
<view class="group-header">
|
||||||
|
<text class="group-title">{{ group.category_name }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 分组内容 -->
|
||||||
|
<view class="nft-row">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in group.items"
|
||||||
|
:key="item.asset_id"
|
||||||
|
class="nft-grid-item"
|
||||||
|
@click="handleCardClick(item)"
|
||||||
|
>
|
||||||
|
<NftCard
|
||||||
|
:cover-image="item.cover_url_signed"
|
||||||
|
:width="cardSize"
|
||||||
|
:height="cardSize"
|
||||||
|
:locked="false"
|
||||||
|
:custom-style="cardCustomStyle"
|
||||||
|
/>
|
||||||
|
<view class="nft-info">
|
||||||
|
<text class="nft-name">{{ item.name }}</text>
|
||||||
|
<text class="nft-likes">★{{ item.like_count }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 更多按钮 -->
|
||||||
|
<view v-if="group.has_more" class="nft-grid-item more-item" @click="goToMore(group)">
|
||||||
|
<NftCard
|
||||||
|
:cover-image="''"
|
||||||
|
:width="cardSize"
|
||||||
|
:height="cardSize"
|
||||||
|
:locked="false"
|
||||||
|
:show-add-button="false"
|
||||||
|
operation="none"
|
||||||
|
:custom-style="cardCustomStyle"
|
||||||
|
/>
|
||||||
|
<view class="more-overlay">
|
||||||
|
<text class="more-text">更多></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 活动藏品:按 activity_type 分组 -->
|
||||||
|
<view v-if="currentType === 'activity'">
|
||||||
|
<view v-for="group in activityGroups" :key="group.category" class="nft-group">
|
||||||
|
<!-- 分组标题 -->
|
||||||
|
<view class="group-header">
|
||||||
|
<text class="group-title">{{ group.category_name }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 分组内容 -->
|
||||||
|
<view class="nft-row">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in group.items"
|
||||||
|
:key="item.asset_id"
|
||||||
|
class="nft-grid-item"
|
||||||
|
@click="handleCardClick(item)"
|
||||||
|
>
|
||||||
|
<NftCard
|
||||||
|
:cover-image="item.cover_url_signed"
|
||||||
|
:width="cardSize"
|
||||||
|
:height="cardSize"
|
||||||
|
:locked="false"
|
||||||
|
:custom-style="cardCustomStyle"
|
||||||
|
/>
|
||||||
|
<view class="nft-info">
|
||||||
|
<text class="nft-name">{{ item.name }}</text>
|
||||||
|
<text class="nft-likes">★{{ item.like_count }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 更多按钮 -->
|
||||||
|
<view v-if="group.has_more" class="nft-grid-item more-item" @click="goToMore(group)">
|
||||||
|
<NftCard
|
||||||
|
:cover-image="''"
|
||||||
|
:width="cardSize"
|
||||||
|
:height="cardSize"
|
||||||
|
:locked="false"
|
||||||
|
:show-add-button="false"
|
||||||
|
operation="none"
|
||||||
|
:custom-style="cardCustomStyle"
|
||||||
|
/>
|
||||||
|
<view class="more-overlay">
|
||||||
|
<text class="more-text">更多></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -34,8 +188,7 @@
|
|||||||
import { ref, computed, onMounted, onActivated, watch } from 'vue';
|
import { ref, computed, onMounted, onActivated, watch } from 'vue';
|
||||||
import { onShow } from '@dcloudio/uni-app';
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
import NftCard from './NftCard.vue';
|
import NftCard from './NftCard.vue';
|
||||||
import { getMyAssetsApi } from '@/utils/api.js';
|
import { getStarbookHomeApi } from '@/utils/api.js';
|
||||||
import { getAssetCoverRealUrl } from '@/utils/assetImageHelper.js';
|
|
||||||
|
|
||||||
// 屏幕宽度
|
// 屏幕宽度
|
||||||
const screenWidth = ref(0);
|
const screenWidth = ref(0);
|
||||||
@ -43,20 +196,21 @@ const screenWidth = ref(0);
|
|||||||
// 加载状态
|
// 加载状态
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
// NFT列表(初始为15个卡片,将通过API填充)
|
// 当前选中的类型
|
||||||
const nftList = ref([]);
|
const currentType = ref('regular');
|
||||||
|
|
||||||
|
// 星册首页数据
|
||||||
|
const starbookData = ref([]);
|
||||||
|
|
||||||
|
// 上次加载时间(用于防抖)
|
||||||
|
let lastLoadedAt = 0;
|
||||||
|
|
||||||
// 计算卡片尺寸
|
// 计算卡片尺寸
|
||||||
const cardSize = computed(() => {
|
const cardSize = computed(() => {
|
||||||
if (screenWidth.value === 0) return 200;
|
if (screenWidth.value === 0) return 200;
|
||||||
// 每行3个卡片,留出padding和间距
|
|
||||||
// rpx转px:1rpx = screenWidth / 750
|
|
||||||
const rpxToPx = screenWidth.value / 750;
|
const rpxToPx = screenWidth.value / 750;
|
||||||
const padding = 40 * rpxToPx; // 左右各40rpx
|
const padding = 30 * rpxToPx; // 左右各30rpx
|
||||||
const gap = 15 * rpxToPx; // 卡片间距15rpx(3列有2个间距)
|
const gap = 15 * rpxToPx; // 卡片间距15rpx
|
||||||
// 可用宽度 = 屏幕宽度 - 左右padding - 2个间距
|
|
||||||
const availableWidth = screenWidth.value - (padding * 2) - (gap * 2);
|
const availableWidth = screenWidth.value - (padding * 2) - (gap * 2);
|
||||||
return Math.floor(availableWidth / 3);
|
return Math.floor(availableWidth / 3);
|
||||||
});
|
});
|
||||||
@ -68,63 +222,57 @@ const cardCustomStyle = {
|
|||||||
left: '0'
|
left: '0'
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加NFT处理 - 跳转到铸爱页面
|
// grade 中文转换
|
||||||
const handleAddNft = () => {
|
const gradeMap = { 1: '一', 2: '二', 3: '三', 4: '四', 5: '五' };
|
||||||
// 跳转到铸爱商城页面
|
function formatGrade(grade) {
|
||||||
uni.navigateTo({
|
return `等级${gradeMap[grade] || grade}`;
|
||||||
url: '/pages/castlove/mall'
|
}
|
||||||
});
|
|
||||||
|
// 判断是否有数据
|
||||||
|
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;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await getMyAssetsApi(1, 20);
|
const response = await getStarbookHomeApi();
|
||||||
if (response.code === 200 && response.data && response.data.items) {
|
if (response.code === 200 && response.data && response.data.groups) {
|
||||||
// 映射后端数据并解析封面URL
|
starbookData.value = response.data.groups;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取藏品列表失败:', error);
|
console.error('获取星册数据失败:', error);
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: error.message || '获取藏品列表失败',
|
title: error.message || '获取星册数据失败',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
duration: 2000
|
duration: 2000
|
||||||
});
|
});
|
||||||
|
starbookData.value = [];
|
||||||
// 失败时使用默认布局(1个空白卡片 + 14个锁定卡片)
|
|
||||||
const defaultList = [{ image: '', locked: false }];
|
|
||||||
for (let i = 0; i < 14; i++) {
|
|
||||||
defaultList.push({ image: '', locked: true });
|
|
||||||
}
|
|
||||||
nftList.value = defaultList;
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@ -132,14 +280,31 @@ const loadAssetsList = async () => {
|
|||||||
|
|
||||||
// 点击卡片跳转到详情页
|
// 点击卡片跳转到详情页
|
||||||
const handleCardClick = (item) => {
|
const handleCardClick = (item) => {
|
||||||
if (item.image && !item.locked && item.asset_id) {
|
if (item.asset_id) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/asset-detail/asset-detail?asset_id=${item.asset_id}`
|
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({
|
const props = defineProps({
|
||||||
isActive: {
|
isActive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -150,25 +315,25 @@ const props = defineProps({
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const systemInfo = uni.getSystemInfoSync();
|
const systemInfo = uni.getSystemInfoSync();
|
||||||
screenWidth.value = systemInfo.windowWidth;
|
screenWidth.value = systemInfo.windowWidth;
|
||||||
|
loadStarbookData();
|
||||||
// 加载藏品列表
|
|
||||||
loadAssetsList();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 每次组件激活时重新加载数据(keep-alive场景)
|
// 每次组件激活时重新加载数据
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
loadAssetsList();
|
loadStarbookData();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听页面显示事件(页面级切换场景)
|
// 监听页面显示事件
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
loadAssetsList();
|
if (props.isActive) {
|
||||||
|
loadStarbookData();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听 isActive prop 变化(tab切换场景)
|
// 监听 isActive prop 变化
|
||||||
watch(() => props.isActive, (newValue, oldValue) => {
|
watch(() => props.isActive, (newVal) => {
|
||||||
if (newValue && !oldValue) {
|
if (newVal) {
|
||||||
loadAssetsList();
|
loadStarbookData();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -188,7 +353,7 @@ watch(() => props.isActive, (newValue, oldValue) => {
|
|||||||
background: #0d0820;
|
background: #0d0820;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 滚动条样式 - 自动隐藏 */
|
/* 滚动条样式 */
|
||||||
.starbook-content::-webkit-scrollbar {
|
.starbook-content::-webkit-scrollbar {
|
||||||
width: 6rpx;
|
width: 6rpx;
|
||||||
}
|
}
|
||||||
@ -226,31 +391,133 @@ watch(() => props.isActive, (newValue, oldValue) => {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
padding: 250rpx 30rpx;
|
padding: 224rpx 30rpx 120rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* NFT网格容器 */
|
/* 类型Tab */
|
||||||
.nft-grid-container {
|
.type-tabs {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
justify-content: center;
|
||||||
/* 列间距(与计算逻辑保持一致) */
|
gap: 40rpx;
|
||||||
column-gap: 15rpx;
|
margin-bottom: 30rpx;
|
||||||
/* 行间距 */
|
padding: 0 20rpx;
|
||||||
row-gap: 10rpx;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
/* 确保网格项靠上对齐 */
|
|
||||||
align-items: start;
|
|
||||||
justify-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
.nft-grid-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 210rpx;
|
||||||
/* 保持3:4比例 */
|
flex-shrink: 0;
|
||||||
padding-top: 133.33%;
|
}
|
||||||
/* 确保内容靠上对齐 */
|
|
||||||
|
.nft-grid-item.more-item {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 藏品信息 */
|
||||||
|
.nft-info {
|
||||||
|
padding: 8rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nft-name {
|
||||||
display: block;
|
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;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -688,15 +688,16 @@ export default {
|
|||||||
// 加载展馆槽位信息
|
// 加载展馆槽位信息
|
||||||
// 将 API 响应解析并填充到展馆数据中(供 loadGallerySlots 和 loadRandomGallery 共用)
|
// 将 API 响应解析并填充到展馆数据中(供 loadGallerySlots 和 loadRandomGallery 共用)
|
||||||
const parseGalleryResponse = async (data) => {
|
const parseGalleryResponse = async (data) => {
|
||||||
// 如果当前是查看自己的展馆,强制使用 currentUserUid
|
console.log('parseGalleryResponse called', {
|
||||||
// 防止 API 返回的 gallery_owner_id 与当前用户 UID 不一致
|
isViewingOthers: isViewingOthers.value,
|
||||||
if (!isViewingOthers.value && currentUserUid.value) {
|
dataGalleryOwnerId: data.gallery_owner_id,
|
||||||
visitingGalleryOwnerUid.value = currentUserUid.value;
|
currentUserUid: currentUserUid.value
|
||||||
galleryOwnerNickname.value = '';
|
});
|
||||||
} else {
|
|
||||||
|
// 只更新展馆所有者信息,不要修改 isViewingOthers
|
||||||
|
// isViewingOthers 只在 onLoad 时设置,避免状态混乱
|
||||||
visitingGalleryOwnerUid.value = data.gallery_owner_id;
|
visitingGalleryOwnerUid.value = data.gallery_owner_id;
|
||||||
galleryOwnerNickname.value = data.nickname || '';
|
galleryOwnerNickname.value = data.nickname || '';
|
||||||
}
|
|
||||||
|
|
||||||
galleryOwnerId.value = data.gallery_owner_id;
|
galleryOwnerId.value = data.gallery_owner_id;
|
||||||
uni.setStorageSync('gallery_owner_id', data.gallery_owner_id);
|
uni.setStorageSync('gallery_owner_id', data.gallery_owner_id);
|
||||||
@ -743,10 +744,16 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadGallerySlots = async () => {
|
const loadGallerySlots = async () => {
|
||||||
|
console.log('loadGallerySlots called', {
|
||||||
|
isViewingOthers: isViewingOthers.value,
|
||||||
|
visitingGalleryOwnerUid: visitingGalleryOwnerUid.value,
|
||||||
|
currentUserUid: currentUserUid.value
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
// 使用 isViewingOthers 判断,因为它在 onLoad 中同步设置,比 isMyGallery 更可靠
|
// 根据 visitingGalleryOwnerUid 是否等于 currentUserUid 来判断是否调用自己的展馆 API
|
||||||
if (!isViewingOthers.value) {
|
// 这样可以避免 isViewingOthers 状态错误导致的 API 调用错误
|
||||||
|
if (visitingGalleryOwnerUid.value === currentUserUid.value) {
|
||||||
response = await getMyGalleriesApi();
|
response = await getMyGalleriesApi();
|
||||||
} else {
|
} else {
|
||||||
response = await getUserGalleriesApi(visitingGalleryOwnerUid.value);
|
response = await getUserGalleriesApi(visitingGalleryOwnerUid.value);
|
||||||
@ -1217,6 +1224,17 @@ export default {
|
|||||||
visitingGalleryOwnerUid.value = currentUserUid.value;
|
visitingGalleryOwnerUid.value = currentUserUid.value;
|
||||||
isViewingOthers.value = false;
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理点赞数实时更新
|
// 处理点赞数实时更新
|
||||||
|
|||||||
@ -252,13 +252,13 @@
|
|||||||
@claim-success="handleClaimSuccess" @close="showGuideListModal = false" />
|
@claim-success="handleClaimSuccess" @close="showGuideListModal = false" />
|
||||||
|
|
||||||
<!-- 全局引导遮罩 -->
|
<!-- 全局引导遮罩 -->
|
||||||
<!-- <GuideOverlay /> -->
|
<GuideOverlay />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { onShow } from '@dcloudio/uni-app';
|
import { onShow, onLoad } from '@dcloudio/uni-app';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import { onReady } from "@dcloudio/uni-app";
|
import { onReady } from "@dcloudio/uni-app";
|
||||||
import Header from '../components/Header.vue';
|
import Header from '../components/Header.vue';
|
||||||
@ -1217,6 +1217,18 @@ onReady(() => {
|
|||||||
// 不再自动触发引导,由用户从引导列表选择后触发
|
// 不再自动触发引导,由用户从引导列表选择后触发
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 处理引导跳转参数
|
||||||
|
onLoad((options) => {
|
||||||
|
if (options && options.guide_key) {
|
||||||
|
console.log('[Guide] profile 收到引导跳转参数, guide_key:', options.guide_key, 'guide_step:', options.guide_step)
|
||||||
|
store.dispatch('guide/resumeGuide', options.guide_key).then(res => {
|
||||||
|
console.log('[Guide] profile resumeGuide 结果:', res)
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[Guide] profile resumeGuide 失败:', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
// 每次页面显示时都刷新用户信息并通知 Header 更新余额
|
// 每次页面显示时都刷新用户信息并通知 Header 更新余额
|
||||||
fetchUserInfo().then(() => {
|
fetchUserInfo().then(() => {
|
||||||
|
|||||||
@ -71,13 +71,6 @@
|
|||||||
@update:isExpanded="navExpanded = $event"
|
@update:isExpanded="navExpanded = $event"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 新手引导开始弹窗 -->
|
|
||||||
<GuideStartModal
|
|
||||||
:visible="showGuideStartModal"
|
|
||||||
@start="handleGuideStart"
|
|
||||||
@close="handleGuideStartModalClose"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 全局引导遮罩 -->
|
<!-- 全局引导遮罩 -->
|
||||||
<GuideOverlay />
|
<GuideOverlay />
|
||||||
</view>
|
</view>
|
||||||
@ -89,13 +82,12 @@ import { onLoad, onShow } from '@dcloudio/uni-app'
|
|||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import Header from '../components/Header.vue'
|
import Header from '../components/Header.vue'
|
||||||
import BottomNav from '../components/BottomNav.vue'
|
import BottomNav from '../components/BottomNav.vue'
|
||||||
import GuideStartModal from '@/components/GuideStartModal.vue'
|
|
||||||
import GuideOverlay from '@/components/GuideOverlay.vue'
|
import GuideOverlay from '@/components/GuideOverlay.vue'
|
||||||
import RankingModal from '../components/RankingModal.vue'
|
import RankingModal from '../components/RankingModal.vue'
|
||||||
import CabinItem from './components/CabinItem.vue'
|
import CabinItem from './components/CabinItem.vue'
|
||||||
import BannerCarousel from './components/BannerCarousel.vue'
|
import BannerCarousel from './components/BannerCarousel.vue'
|
||||||
import NavArrows from './components/NavArrows.vue'
|
import NavArrows from './components/NavArrows.vue'
|
||||||
import { shouldShowGuideStartModal } from '@/utils/guideConfig.js'
|
import { clearSubStepProgress, shouldShowGuideStartModal, resetGuide } from '@/utils/guideConfig.js'
|
||||||
import { IMAGE_W, IMAGE_H } from './config/cabin.js'
|
import { IMAGE_W, IMAGE_H } from './config/cabin.js'
|
||||||
import { useSwipe } from './composables/useSwipe.js'
|
import { useSwipe } from './composables/useSwipe.js'
|
||||||
import { useCabin } from './composables/useCabin.js'
|
import { useCabin } from './composables/useCabin.js'
|
||||||
@ -110,7 +102,6 @@ const currentStarId = ref(uni.getStorageSync('star_id') || null)
|
|||||||
// ========== UI State ==========
|
// ========== UI State ==========
|
||||||
const navExpanded = ref(false)
|
const navExpanded = ref(false)
|
||||||
const showRankingModal = ref(false)
|
const showRankingModal = ref(false)
|
||||||
const showGuideStartModal = ref(false)
|
|
||||||
const isDev = ref(false) // 开发模式,显示调试按钮
|
const isDev = ref(false) // 开发模式,显示调试按钮
|
||||||
|
|
||||||
// ========== Screen Info ==========
|
// ========== Screen Info ==========
|
||||||
@ -169,7 +160,8 @@ const handleRankingVisit = (userId) => {
|
|||||||
|
|
||||||
const handleRankingModalClose = (visible) => {
|
const handleRankingModalClose = (visible) => {
|
||||||
showRankingModal.value = visible
|
showRankingModal.value = visible
|
||||||
if (!visible && store.state.guide.isActive) {
|
// 使用 componentMode 判断,因为 isActive 可能在 END_GUIDE 后已变为 false
|
||||||
|
if (!visible && store.state.guide.componentMode) {
|
||||||
uni.$emit('guide:closeComponent')
|
uni.$emit('guide:closeComponent')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,14 +200,6 @@ const handleTabChange = (newTab) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGuideStart = () => {
|
|
||||||
showGuideStartModal.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGuideStartModalClose = () => {
|
|
||||||
showGuideStartModal.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const openDebugGrid = () => {
|
const openDebugGrid = () => {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages/square/debug-grid'
|
url: '/pages/square/debug-grid'
|
||||||
@ -295,9 +279,11 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
// 检查是否需要显示引导开始弹窗
|
// 检查是否需要显示引导,如果需要则跳转到引导页面
|
||||||
if (shouldShowGuideStartModal()) {
|
if (shouldShowGuideStartModal()) {
|
||||||
showGuideStartModal.value = true
|
uni.navigateTo({
|
||||||
|
url: '/pages/tasks/guide'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -316,6 +302,17 @@ onLoad((options) => {
|
|||||||
console.log('[Guide] 调试模式已关闭')
|
console.log('[Guide] 调试模式已关闭')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理引导跳转参数:如果传递了 guide_key,则继续该引导
|
||||||
|
if (options && options.guide_key) {
|
||||||
|
console.log('[Guide] 收到引导跳转参数, guide_key:', options.guide_key, 'guide_step:', options.guide_step)
|
||||||
|
// 使用 resumeGuide 继续引导(不会检查 shouldShowGuide)
|
||||||
|
store.dispatch('guide/resumeGuide', options.guide_key).then(res => {
|
||||||
|
console.log('[Guide] resumeGuide 结果:', res)
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[Guide] resumeGuide 失败:', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听引导打开组件事件
|
// 监听引导打开组件事件
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page-container">
|
<view class="page-container">
|
||||||
<Header :showBack="true" backIconColor="#e6e6e6" />
|
<Header :showGuideIcon="false" :showTaskIcon="false" :showStarActivityIcon="false" :showBack="true" backIconColor="#e6e6e6" />
|
||||||
<StarbookContent :isActive="true" />
|
<StarbookContent :isActive="true" />
|
||||||
|
|
||||||
<!-- 蒙层 - 导航栏展开时显示 -->
|
<!-- 蒙层 - 导航栏展开时显示 -->
|
||||||
|
|||||||
287
frontend/pages/starbook/items.vue
Normal file
287
frontend/pages/starbook/items.vue
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container">
|
||||||
|
<Header :showBack="true" backIconColor="#e6e6e6" :title="pageTitle" />
|
||||||
|
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<view v-if="loading && page === 1" class="loading-container">
|
||||||
|
<text class="loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-else-if="!hasData" class="empty-container">
|
||||||
|
<text class="empty-text">暂无藏品</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 藏品网格 -->
|
||||||
|
<view v-else class="nft-grid-container">
|
||||||
|
<view
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.asset_id"
|
||||||
|
class="nft-grid-item"
|
||||||
|
@click="handleCardClick(item)"
|
||||||
|
>
|
||||||
|
<NftCard
|
||||||
|
:cover-image="item.cover_url_signed"
|
||||||
|
:width="cardSize"
|
||||||
|
:height="cardSize"
|
||||||
|
:locked="false"
|
||||||
|
:custom-style="cardCustomStyle"
|
||||||
|
/>
|
||||||
|
<view class="nft-info">
|
||||||
|
<text class="nft-name">{{ item.name }}</text>
|
||||||
|
<text class="nft-likes">★{{ item.like_count }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 分页提示 -->
|
||||||
|
<view v-if="hasMore && !loading" class="load-more" @click="loadMore">
|
||||||
|
<text class="load-more-text">加载更多</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载中(加载更多) -->
|
||||||
|
<view v-if="loading && page > 1" class="loading-more">
|
||||||
|
<text class="loading-more-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 没有更多了 -->
|
||||||
|
<view v-if="!hasMore && items.length > 0" class="no-more">
|
||||||
|
<text class="no-more-text">— 没有更多了 —</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<BottomNav :activeTab="1" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import Header from "../components/Header.vue";
|
||||||
|
import BottomNav from "../components/BottomNav.vue";
|
||||||
|
import NftCard from "../components/NftCard.vue";
|
||||||
|
import { getStarbookItemsApi } from '@/utils/api.js';
|
||||||
|
|
||||||
|
// 屏幕宽度
|
||||||
|
const screenWidth = ref(0);
|
||||||
|
|
||||||
|
// 路由参数
|
||||||
|
const type = ref('regular');
|
||||||
|
const category = ref('castlove');
|
||||||
|
const grade = ref(null);
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = ref(20);
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
|
// 藏品列表
|
||||||
|
const items = ref([]);
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 计算卡片尺寸
|
||||||
|
const cardSize = computed(() => {
|
||||||
|
if (screenWidth.value === 0) return 200;
|
||||||
|
const rpxToPx = screenWidth.value / 750;
|
||||||
|
const padding = 30 * rpxToPx; // 左右各30rpx
|
||||||
|
const gap = 15 * rpxToPx; // 卡片间距15rpx(3列有2个间距)
|
||||||
|
const availableWidth = screenWidth.value - (padding * 2) - (gap * 2);
|
||||||
|
return Math.floor(availableWidth / 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 卡片自定义样式
|
||||||
|
const cardCustomStyle = {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '0',
|
||||||
|
left: '0'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面标题
|
||||||
|
const pageTitle = computed(() => {
|
||||||
|
if (type.value === 'regular') {
|
||||||
|
return `普通 · ${formatGrade(grade.value)}`;
|
||||||
|
}
|
||||||
|
return category.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 判断是否有数据
|
||||||
|
const hasData = computed(() => {
|
||||||
|
return items.value.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 判断是否有更多
|
||||||
|
const hasMore = computed(() => {
|
||||||
|
return items.value.length < total.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// grade 中文转换
|
||||||
|
const gradeMap = { 1: '一', 2: '二', 3: '三', 4: '四', 5: '五' };
|
||||||
|
function formatGrade(g) {
|
||||||
|
return `等级${gradeMap[g] || g}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async (append = false) => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getStarbookItemsApi(
|
||||||
|
type.value,
|
||||||
|
category.value,
|
||||||
|
grade.value,
|
||||||
|
page.value,
|
||||||
|
pageSize.value
|
||||||
|
);
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
if (append) {
|
||||||
|
items.value = [...items.value, ...(response.data.items || [])];
|
||||||
|
} else {
|
||||||
|
items.value = response.data.items || [];
|
||||||
|
}
|
||||||
|
total.value = response.data.total || 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取藏品列表失败:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: error.message || '获取藏品列表失败',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
const loadMore = () => {
|
||||||
|
if (!hasMore.value || loading.value) return;
|
||||||
|
page.value++;
|
||||||
|
loadData(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击卡片跳转到详情页
|
||||||
|
const handleCardClick = (item) => {
|
||||||
|
if (item.asset_id) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/asset-detail/asset-detail?asset_id=${item.asset_id}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听页面滚动到底部
|
||||||
|
onMounted(() => {
|
||||||
|
const systemInfo = uni.getSystemInfoSync();
|
||||||
|
screenWidth.value = systemInfo.windowWidth;
|
||||||
|
|
||||||
|
// 获取路由参数
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
const currentPage = pages[pages.length - 1];
|
||||||
|
const options = currentPage.options || currentPage.$page?.options || {};
|
||||||
|
|
||||||
|
type.value = options.type || 'regular';
|
||||||
|
category.value = options.category || 'castlove';
|
||||||
|
grade.value = options.grade ? parseInt(options.grade) : null;
|
||||||
|
page.value = parseInt(options.page) || 1;
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 触底加载更多
|
||||||
|
onReachBottom(() => {
|
||||||
|
if (hasMore.value && !loading.value) {
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #0d0820;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container,
|
||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 300rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text,
|
||||||
|
.empty-text {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 藏品网格容器 */
|
||||||
|
.nft-grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
column-gap: 15rpx;
|
||||||
|
row-gap: 20rpx;
|
||||||
|
padding: 250rpx 30rpx 180rpx;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nft-grid-item {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 133.33%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nft-grid-item:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 藏品信息 */
|
||||||
|
.nft-info {
|
||||||
|
padding: 10rpx 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载更多 */
|
||||||
|
.load-more {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more,
|
||||||
|
.no-more {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more-text,
|
||||||
|
.no-more-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -75,14 +75,32 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部进度区域和一键领取 -->
|
||||||
|
<view v-if="!loading && !errorMessage" class="bottom-section">
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<view class="progress-section">
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view
|
||||||
|
v-for="(milestone, index) in milestones"
|
||||||
|
:key="index"
|
||||||
|
class="progress-node"
|
||||||
|
:class="{ 'active': index < completedCount }"
|
||||||
|
>
|
||||||
|
<view class="node-circle"></view>
|
||||||
|
<text class="node-label">{{ milestone }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 一键领取按钮 -->
|
<!-- 一键领取按钮 -->
|
||||||
<view v-if="!loading && !errorMessage && hasClaimableTasks" class="claim-all-bar">
|
<view v-if="hasClaimableTasks" class="claim-all-bar">
|
||||||
<button class="claim-all-btn" :loading="claimingAll" @click="handleClaimAll">
|
<button class="claim-all-btn" :loading="claimingAll" @click="handleClaimAll">
|
||||||
一键领取 ({{ claimableCount }})
|
一键领取 ({{ claimableCount }})
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
</transition>
|
</transition>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@ -110,6 +128,15 @@ const starId = ref(1)
|
|||||||
const hasClaimableTasks = computed(() => tasks.value.some(t => t.can_claim))
|
const hasClaimableTasks = computed(() => tasks.value.some(t => t.can_claim))
|
||||||
const claimableCount = computed(() => tasks.value.filter(t => t.can_claim).length)
|
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 变化,自动加载数据
|
// 监听 visible 变化,自动加载数据
|
||||||
watch(() => props.visible, (newVal) => {
|
watch(() => props.visible, (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
@ -485,6 +512,67 @@ const handleCloseClick = (e) => {
|
|||||||
margin-top: 20rpx;
|
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 {
|
.claim-all-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 24rpx 0;
|
padding: 24rpx 0;
|
||||||
|
|||||||
@ -11,96 +11,103 @@
|
|||||||
<!-- 页面标题 -->
|
<!-- 页面标题 -->
|
||||||
<view class="page-title">新手引导</view>
|
<view class="page-title">新手引导</view>
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
<!-- 引导列表 -->
|
||||||
<view v-if="loading" class="loading-state">
|
<view class="guide-list">
|
||||||
<view class="loading-spinner"></view>
|
<view
|
||||||
<text class="loading-text">加载中...</text>
|
v-for="item in guideList"
|
||||||
</view>
|
:key="item.key"
|
||||||
|
class="guide-item"
|
||||||
<!-- 错误状态 -->
|
:class="{ 'in-progress': item.status === 'in_progress' }"
|
||||||
<view v-else-if="errorMessage" class="error-state">
|
|
||||||
<text class="error-text">{{ errorMessage }}</text>
|
|
||||||
<button class="retry-btn" @click="loadStatus">重试</button>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 引导内容 -->
|
|
||||||
<view v-else class="guide-content">
|
|
||||||
<!-- 当前阶段信息 -->
|
|
||||||
<view class="current-stage-card">
|
|
||||||
<view class="stage-header">
|
|
||||||
<text class="stage-label">当前阶段</text>
|
|
||||||
<text class="stage-number">第 {{ currentStage }} 阶段</text>
|
|
||||||
</view>
|
|
||||||
<view class="stage-name">{{ currentStageName }}</view>
|
|
||||||
<view class="stage-reward" v-if="currentStageReward > 0">
|
|
||||||
<text class="reward-icon">💎</text>
|
|
||||||
<text class="reward-value">{{ currentStageReward }}</text>
|
|
||||||
<text class="reward-sep">|</text>
|
|
||||||
<text class="reward-icon">⭐</text>
|
|
||||||
<text class="reward-value">{{ currentStageExp }} 经验</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 阶段列表 -->
|
|
||||||
<view class="stages-list">
|
|
||||||
<view v-for="stage in stages" :key="stage.stage" class="stage-item" :class="{ 'is-current': stage.is_current, 'is-completed': stage.status === 'completed' }">
|
|
||||||
<view class="stage-item-header">
|
|
||||||
<view class="stage-item-name">
|
|
||||||
<text v-if="stage.status === 'completed'" class="check-icon">✓</text>
|
|
||||||
<text>{{ stage.name }}</text>
|
|
||||||
</view>
|
|
||||||
<view v-if="stage.is_current" class="current-tag">进行中</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 该阶段的任务列表 -->
|
|
||||||
<view class="task-list-mini">
|
|
||||||
<view v-for="taskKey in stage.required_task_keys" :key="taskKey" class="task-key-item">
|
|
||||||
<text class="task-check" :class="{ 'is-done': isTaskCompleted(stage, taskKey) }">
|
|
||||||
{{ isTaskCompleted(stage, taskKey) ? '✓' : '○' }}
|
|
||||||
</text>
|
|
||||||
<text class="task-key-name">{{ taskKey }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
|
||||||
<view class="action-bar">
|
|
||||||
<!-- 进入下一阶段按钮 -->
|
|
||||||
<button
|
|
||||||
v-if="canAdvance"
|
|
||||||
class="advance-btn"
|
|
||||||
:loading="advancing"
|
|
||||||
@click="handleAdvance"
|
|
||||||
>
|
>
|
||||||
进入下一阶段
|
<view class="guide-item-header">
|
||||||
</button>
|
<view class="guide-status-badge" :class="item.status">
|
||||||
<view v-else-if="!allCurrentTasksDone" class="advance-hint">
|
{{ item.statusText }}
|
||||||
完成当前阶段所有任务后可进入下一阶段
|
</view>
|
||||||
|
<text class="guide-name">{{ item.name }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 领取奖励按钮 -->
|
<view class="guide-item-body">
|
||||||
<button
|
<text class="guide-desc">{{ item.desc }}</text>
|
||||||
v-if="canClaimReward"
|
</view>
|
||||||
class="claim-btn"
|
|
||||||
:loading="claimingReward"
|
<!-- 进度条(进行中状态显示) -->
|
||||||
@click="handleClaimReward"
|
<view v-if="item.status === 'in_progress'" class="guide-progress">
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view
|
||||||
|
class="progress-fill"
|
||||||
|
:style="{ width: item.progress.percentage + '%' }"
|
||||||
|
></view>
|
||||||
|
</view>
|
||||||
|
<text class="progress-text">{{ item.progress.completed }}/{{ item.progress.total }} 步骤</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="guide-item-footer">
|
||||||
|
<!-- 未开始 -->
|
||||||
|
<view
|
||||||
|
v-if="item.buttonType === 'start'"
|
||||||
|
class="guide-btn start-btn"
|
||||||
|
@click="handleStartGuide(item.key)"
|
||||||
>
|
>
|
||||||
领取奖励
|
{{ item.buttonText }}
|
||||||
</button>
|
</view>
|
||||||
|
<!-- 进行中 -->
|
||||||
|
<view
|
||||||
|
v-else-if="item.buttonType === 'continue'"
|
||||||
|
class="guide-btn continue-btn"
|
||||||
|
@click="handleContinueGuide(item.key)"
|
||||||
|
>
|
||||||
|
{{ item.buttonText }}
|
||||||
|
</view>
|
||||||
|
<!-- 已完成(可领取) -->
|
||||||
|
<view
|
||||||
|
v-else-if="item.buttonType === 'claim'"
|
||||||
|
class="guide-btn claim-btn"
|
||||||
|
@click="handleClaimReward(item.key)"
|
||||||
|
>
|
||||||
|
{{ item.buttonText }}
|
||||||
|
</view>
|
||||||
|
<!-- 已领取 -->
|
||||||
|
<view
|
||||||
|
v-else
|
||||||
|
class="guide-btn disabled-btn"
|
||||||
|
>
|
||||||
|
{{ item.buttonText }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 下拉刷新 -->
|
<!-- 底部统计 -->
|
||||||
<view v-if="!loading" class="pull-to-refresh" @click="handleRefresh">
|
<view class="guide-footer">
|
||||||
<!-- <text class="refresh-text">{{ refreshing ? '刷新中...' : '下拉刷新' }}</text> -->
|
<view class="footer-stats">
|
||||||
|
已完成 {{ doneCount }}/{{ totalCount }}
|
||||||
|
</view>
|
||||||
|
<view v-if="claimableCount > 0" class="footer-claim-tip">
|
||||||
|
有 {{ claimableCount }} 个奖励可领取
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { getOnboardingStatus, advanceStage, claimOnboardingReward } from '@/utils/task-api.js'
|
import { getOnboardingStatus, advanceStage, claimOnboardingReward, completeGuide } from '@/utils/task-api.js'
|
||||||
|
import {
|
||||||
|
guideConfig,
|
||||||
|
getGuideConfig,
|
||||||
|
getGuideStatusList,
|
||||||
|
getStepProgress,
|
||||||
|
hasGuideProgress,
|
||||||
|
getNextIncompleteStep,
|
||||||
|
getStepPage,
|
||||||
|
clearSubStepProgress,
|
||||||
|
resetGuide,
|
||||||
|
claimGuideReward,
|
||||||
|
markGuideDone,
|
||||||
|
isGuideDone,
|
||||||
|
isGuideRewardClaimed,
|
||||||
|
onboardingStages
|
||||||
|
} from '@/utils/guideConfig.js'
|
||||||
|
|
||||||
const isAndroid = ref(false)
|
const isAndroid = ref(false)
|
||||||
|
|
||||||
@ -116,106 +123,222 @@ const handleBack = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const refreshing = ref(false)
|
const guideList = ref([])
|
||||||
const errorMessage = ref('')
|
|
||||||
const currentStage = ref(0)
|
|
||||||
const currentStatus = ref('')
|
|
||||||
const stages = ref([])
|
|
||||||
const advancing = ref(false)
|
|
||||||
const claimingReward = ref(false)
|
|
||||||
|
|
||||||
// 当前阶段配置
|
// 统计数据
|
||||||
const currentStageConfig = computed(() => stages.value.find(s => s.is_current))
|
const totalCount = computed(() => guideList.value.length)
|
||||||
const currentStageName = computed(() => currentStageConfig.value?.name || '新手引导')
|
const doneCount = computed(() => guideList.value.filter(item =>
|
||||||
const currentStageReward = computed(() => currentStageConfig.value?.crystal_reward || 0)
|
item.status === 'completed' || item.status === 'reward_claimed'
|
||||||
const currentStageExp = computed(() => currentStageConfig.value?.exp_reward || 0)
|
).length)
|
||||||
|
const claimableCount = computed(() => guideList.value.filter(item => item.status === 'completed').length)
|
||||||
|
|
||||||
// 当前阶段所有任务是否完成
|
// 刷新引导列表
|
||||||
const currentTasksDone = computed(() => {
|
function refreshList() {
|
||||||
if (!currentStageConfig.value) return false
|
const rawList = getGuideStatusList()
|
||||||
return currentStageConfig.value.required_task_keys.every(key =>
|
guideList.value = rawList.map(item => {
|
||||||
isTaskCompleted(currentStageConfig.value, key)
|
const progress = getStepProgress(item.key)
|
||||||
)
|
const status = calculateStatus(item.key, item.done, item.claimed)
|
||||||
})
|
|
||||||
|
|
||||||
const allCurrentTasksDone = computed(() => currentTasksDone.value)
|
return {
|
||||||
const canAdvance = computed(() => currentStatus.value === 'in_progress' && currentTasksDone.value)
|
...item,
|
||||||
const canClaimReward = computed(() => currentStatus.value === 'in_progress' && currentTasksDone.value)
|
progress,
|
||||||
|
status,
|
||||||
// 检查某个阶段的任务是否完成
|
statusText: getStatusText(status, progress),
|
||||||
// 优先使用后端返回的 AllTasksCompleted 字段
|
buttonText: getButtonText(status),
|
||||||
function isTaskCompleted(stage, taskKey) {
|
buttonType: getButtonType(status)
|
||||||
// 后端返回的 stage.all_tasks_completed 标识该阶段所有任务是否完成
|
|
||||||
if (stage.all_tasks_completed === true) {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
// 如果后端没有返回 all_tasks_completed,尝试从 status 判断
|
})
|
||||||
if (stage.status === 'completed') {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadStatus() {
|
function calculateStatus(key, done, claimed) {
|
||||||
|
if (claimed) return 'reward_claimed'
|
||||||
|
if (done && !claimed) return 'completed'
|
||||||
|
if (hasGuideProgress(key)) return 'in_progress'
|
||||||
|
return 'not_started'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusText(status, progress) {
|
||||||
|
if (status === 'in_progress') {
|
||||||
|
return `${progress.completed}/${progress.total} 步骤`
|
||||||
|
}
|
||||||
|
const map = {
|
||||||
|
not_started: '未开始',
|
||||||
|
completed: '已完成',
|
||||||
|
reward_claimed: '已领取'
|
||||||
|
}
|
||||||
|
return map[status] || '未开始'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getButtonText(status) {
|
||||||
|
const map = {
|
||||||
|
not_started: '开始',
|
||||||
|
in_progress: '继续',
|
||||||
|
completed: '领取奖励',
|
||||||
|
reward_claimed: '已领取'
|
||||||
|
}
|
||||||
|
return map[status] || '开始'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getButtonType(status) {
|
||||||
|
const map = {
|
||||||
|
not_started: 'start',
|
||||||
|
in_progress: 'continue',
|
||||||
|
completed: 'claim',
|
||||||
|
reward_claimed: 'claimed'
|
||||||
|
}
|
||||||
|
return map[status] || 'start'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始引导
|
||||||
|
function handleStartGuide(key) {
|
||||||
|
console.log('[guide] handleStartGuide:', key)
|
||||||
|
// 清除进度从头开始
|
||||||
|
clearSubStepProgress(key)
|
||||||
|
// 重置引导"已显示"状态,允许重新开始
|
||||||
|
resetGuide(key)
|
||||||
|
const targetPage = getStepPage(key, 0)
|
||||||
|
|
||||||
|
if (targetPage) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: targetPage + '?guide_key=' + key + '&guide_step=0'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继续引导
|
||||||
|
function handleContinueGuide(key) {
|
||||||
|
console.log('[guide] handleContinueGuide:', key)
|
||||||
|
// 从第一个未完成步骤继续
|
||||||
|
const resumeStep = getNextIncompleteStep(key)
|
||||||
|
const targetPage = getStepPage(key, resumeStep)
|
||||||
|
console.log('[guide] handleContinueGuide, resumeStep:', resumeStep, 'targetPage:', targetPage)
|
||||||
|
|
||||||
|
if (targetPage) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: targetPage + '?guide_key=' + key + '&guide_step=' + resumeStep
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成引导并同步后端
|
||||||
|
async function completeGuideAndSync(key) {
|
||||||
|
try {
|
||||||
|
// 标记本地已完成
|
||||||
|
markGuideDone(key)
|
||||||
|
// 同步到后端
|
||||||
|
await completeGuide(key, onboardingStages)
|
||||||
|
console.log('[guide] completeGuideAndSync success:', key)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[guide] completeGuideAndSync error:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查并同步所有完成的引导到后端
|
||||||
|
async function checkAndSyncCompletedGuides() {
|
||||||
|
const rawList = getGuideStatusList()
|
||||||
|
for (const item of rawList) {
|
||||||
|
if (item.done && !item.claimed) {
|
||||||
|
// 引导已完成但后端未同步
|
||||||
|
await completeGuideAndSync(item.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 领取奖励
|
||||||
|
async function handleClaimReward(key) {
|
||||||
|
// 调用后端接口领取奖励
|
||||||
|
try {
|
||||||
|
// 找到对应的 stage
|
||||||
|
const config = getGuideConfig(key)
|
||||||
|
if (!config) return
|
||||||
|
|
||||||
|
// 从 onboardingStages 找到对应 stage
|
||||||
|
let stageNum = -1
|
||||||
|
for (const s of onboardingStages) {
|
||||||
|
if (s.required_task_keys.includes(key)) {
|
||||||
|
stageNum = s.stage
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stageNum >= 0) {
|
||||||
|
const res = await claimOnboardingReward(stageNum)
|
||||||
|
// 更新本地存储的余额并通知 Header 组件
|
||||||
|
if (res.data?.crystal_balance !== undefined) {
|
||||||
|
try {
|
||||||
|
const userStr = uni.getStorageSync('user')
|
||||||
|
if (userStr) {
|
||||||
|
const user = JSON.parse(userStr)
|
||||||
|
user.crystal_balance = parseInt(res.data.crystal_balance)
|
||||||
|
uni.setStorageSync('user', JSON.stringify(user))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('更新本地存储失败:', e)
|
||||||
|
}
|
||||||
|
uni.$emit('balanceUpdated', { crystal_balance: res.data.crystal_balance, experience: res.data.experience })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本地标记已领取
|
||||||
|
claimGuideReward(key)
|
||||||
|
|
||||||
|
// uni.showToast({
|
||||||
|
// title: `领取成功!${config.reward?.exp || 0}经验 ${config.reward?.diamond || 0}钻`,
|
||||||
|
// icon: 'none'
|
||||||
|
// })
|
||||||
|
|
||||||
|
refreshList()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('handleClaimReward error:', err)
|
||||||
|
uni.showToast({ title: '领取失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化时同步后端
|
||||||
|
async function initBackend() {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
errorMessage.value = ''
|
|
||||||
|
|
||||||
|
// 检查后端是否有配置
|
||||||
const res = await getOnboardingStatus()
|
const res = await getOnboardingStatus()
|
||||||
const data = res.data || {}
|
const data = res.data || {}
|
||||||
|
|
||||||
currentStage.value = data.current_stage || 0
|
if (!data.stages || data.stages.length === 0) {
|
||||||
currentStatus.value = data.status || 'pending'
|
// 后端没有配置,用前端配置初始化
|
||||||
stages.value = data.stages || []
|
await completeGuide('init', onboardingStages)
|
||||||
|
} else {
|
||||||
|
// 后端有配置,根据后端的 stage 状态更新本地存储
|
||||||
|
// 遍历后端返回的 stages,根据 allTasksCompleted 状态更新本地
|
||||||
|
for (const stage of data.stages) {
|
||||||
|
if (stage.allTasksCompleted && stage.required_task_keys) {
|
||||||
|
for (const taskKey of stage.required_task_keys) {
|
||||||
|
// 如果后端显示该任务已完成,标记本地为已完成
|
||||||
|
if (!isGuideDone(taskKey)) {
|
||||||
|
console.log('[Guide] 从后端同步任务完成状态:', taskKey)
|
||||||
|
markGuideDone(taskKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
refreshList()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('loadStatus error:', err)
|
console.error('initBackend error:', err)
|
||||||
errorMessage.value = err.message || '加载失败'
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
refreshing.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAdvance() {
|
|
||||||
if (advancing.value) return
|
|
||||||
try {
|
|
||||||
advancing.value = true
|
|
||||||
const nextStage = currentStage.value + 1
|
|
||||||
await advanceStage(nextStage)
|
|
||||||
await loadStatus()
|
|
||||||
uni.showToast({ title: '已进入下一阶段', icon: 'success' })
|
|
||||||
} catch (err) {
|
|
||||||
console.error('handleAdvance error:', err)
|
|
||||||
uni.showToast({ title: err.message || '进入下一阶段失败', icon: 'none' })
|
|
||||||
} finally {
|
|
||||||
advancing.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleClaimReward() {
|
|
||||||
if (claimingReward.value) return
|
|
||||||
try {
|
|
||||||
claimingReward.value = true
|
|
||||||
await claimOnboardingReward(currentStage.value)
|
|
||||||
await loadStatus()
|
|
||||||
uni.showToast({ title: '奖励领取成功', icon: 'success' })
|
|
||||||
} catch (err) {
|
|
||||||
console.error('handleClaimReward error:', err)
|
|
||||||
uni.showToast({ title: err.message || '领取失败', icon: 'none' })
|
|
||||||
} finally {
|
|
||||||
claimingReward.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRefresh() {
|
|
||||||
refreshing.value = true
|
|
||||||
loadStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const systemInfo = uni.getSystemInfoSync()
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
isAndroid.value = systemInfo.platform === 'android'
|
isAndroid.value = systemInfo.platform === 'android'
|
||||||
loadStatus()
|
initBackend().then(() => {
|
||||||
|
// 初始化后检查并同步已完成的引导
|
||||||
|
checkAndSyncCompletedGuides()
|
||||||
|
refreshList()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -223,7 +346,8 @@ onMounted(() => {
|
|||||||
.guide-container {
|
.guide-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding-bottom: 120rpx;
|
padding: 20rpx;
|
||||||
|
padding-bottom: 200rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.background-image {
|
.background-image {
|
||||||
@ -264,222 +388,142 @@ onMounted(() => {
|
|||||||
padding: 20rpx 0;
|
padding: 20rpx 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-state,
|
.guide-list {
|
||||||
.error-state {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 100rpx 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-spinner {
|
.guide-item {
|
||||||
width: 60rpx;
|
|
||||||
height: 60rpx;
|
|
||||||
border: 4rpx solid #e0e0e0;
|
|
||||||
border-top-color: #6c5ce7;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text,
|
|
||||||
.error-text {
|
|
||||||
color: #999;
|
|
||||||
font-size: 28rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-btn {
|
|
||||||
margin-top: 20rpx;
|
|
||||||
padding: 16rpx 40rpx;
|
|
||||||
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 50rpx;
|
|
||||||
font-size: 28rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guide-content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
padding: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-stage-card {
|
|
||||||
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 40rpx;
|
|
||||||
margin-bottom: 30rpx;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-label {
|
|
||||||
font-size: 28rpx;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-number {
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-name {
|
|
||||||
font-size: 40rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-reward {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8rpx;
|
|
||||||
font-size: 28rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reward-icon {
|
|
||||||
font-size: 28rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reward-value {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reward-sep {
|
|
||||||
opacity: 0.5;
|
|
||||||
margin: 0 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stages-list {
|
|
||||||
margin-bottom: 30rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-item {
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
padding: 30rpx;
|
padding: 24rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-item.is-current {
|
.guide-item.in-progress {
|
||||||
border: 2rpx solid #6c5ce7;
|
border-left: 4rpx solid #4a90e2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-item.is-completed {
|
.guide-item-header {
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-item-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-item-name {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10rpx;
|
margin-bottom: 12rpx;
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-icon {
|
.guide-status-badge {
|
||||||
color: #52c41a;
|
padding: 4rpx 12rpx;
|
||||||
font-size: 32rpx;
|
border-radius: 8rpx;
|
||||||
}
|
|
||||||
|
|
||||||
.current-tag {
|
|
||||||
padding: 6rpx 16rpx;
|
|
||||||
background: #6c5ce7;
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
|
margin-right: 12rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-list-mini {
|
.guide-status-badge.not_started {
|
||||||
display: flex;
|
background: #f0f0f0;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-key-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8rpx;
|
|
||||||
padding: 8rpx 16rpx;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
font-size: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-check {
|
|
||||||
color: #ddd;
|
|
||||||
font-size: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-check.is-done {
|
|
||||||
color: #52c41a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-key-name {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-bar {
|
|
||||||
padding: 20rpx 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.advance-btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 24rpx 0;
|
|
||||||
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 50rpx;
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.advance-hint {
|
|
||||||
text-align: center;
|
|
||||||
color: #999;
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-status-badge.in_progress {
|
||||||
|
background: #e6f0ff;
|
||||||
|
color: #4a90e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-status-badge.completed {
|
||||||
|
background: #fff3e6;
|
||||||
|
color: #ff9500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-status-badge.reward_claimed {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-name {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-desc {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
padding: 20rpx 0;
|
color: #666;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-progress {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 8rpx;
|
||||||
|
background: #eee;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 4rpx;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-item-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-btn {
|
||||||
|
padding: 12rpx 32rpx;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.continue-btn {
|
||||||
|
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.claim-btn {
|
.claim-btn {
|
||||||
width: 100%;
|
background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
|
||||||
padding: 24rpx 0;
|
|
||||||
background: linear-gradient(135deg, #ffd700, #ffb347);
|
|
||||||
color: #333;
|
|
||||||
border-radius: 50rpx;
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pull-to-refresh {
|
.disabled-btn {
|
||||||
position: relative;
|
background: #ddd;
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 30rpx 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-text {
|
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: 24rpx;
|
}
|
||||||
|
|
||||||
|
.guide-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1rpx solid #eee;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-stats {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-claim-tip {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #ff6b6b;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -39,11 +39,14 @@ const mutations = {
|
|||||||
* @param {number} [guideConfig.startFromStep] 从指定步骤开始(可选,默认从保存的进度继续)
|
* @param {number} [guideConfig.startFromStep] 从指定步骤开始(可选,默认从保存的进度继续)
|
||||||
*/
|
*/
|
||||||
START_GUIDE(state, { guideConfig, startFromStep }) {
|
START_GUIDE(state, { guideConfig, startFromStep }) {
|
||||||
|
console.log('[Guide] START_GUIDE mutation called, key:', guideConfig.key, 'startFromStep:', startFromStep)
|
||||||
state.currentGuide = guideConfig
|
state.currentGuide = guideConfig
|
||||||
// 如果指定了起始步骤,从指定步骤开始;否则恢复之前保存的进度
|
// 如果指定了起始步骤,从指定步骤开始;否则恢复之前保存的进度
|
||||||
state.currentStep = startFromStep !== undefined ? startFromStep : (getGuideCurrentStep(guideConfig.key) || 0)
|
state.currentStep = startFromStep !== undefined ? startFromStep : (getGuideCurrentStep(guideConfig.key) || 0)
|
||||||
|
console.log('[Guide] START_GUIDE, currentStep set to:', state.currentStep)
|
||||||
state.isActive = true
|
state.isActive = true
|
||||||
state.isNavigating = false
|
state.isNavigating = false
|
||||||
|
console.log('[Guide] START_GUIDE, isActive set to:', state.isActive)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -236,6 +239,7 @@ const actions = {
|
|||||||
}
|
}
|
||||||
// 获取第一个未完成的步骤
|
// 获取第一个未完成的步骤
|
||||||
const resumeStep = getNextIncompleteStep(guideKey)
|
const resumeStep = getNextIncompleteStep(guideKey)
|
||||||
|
console.log('[Guide] resumeGuide:', guideKey, 'resumeStep:', resumeStep, 'guide:', guide)
|
||||||
commit('START_GUIDE', { guideConfig: guide, startFromStep: resumeStep })
|
commit('START_GUIDE', { guideConfig: guide, startFromStep: resumeStep })
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|||||||
@ -542,3 +542,25 @@ export function getActivityRankingApi(activityId, starId = null, page = 1, pageS
|
|||||||
method: 'GET'
|
method: 'GET'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 星册相关接口 ====================
|
||||||
|
|
||||||
|
// 获取星册首页数据
|
||||||
|
export function getStarbookHomeApi() {
|
||||||
|
return request({
|
||||||
|
url: '/api/v1/starbook/home',
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取星册藏品列表(分页)
|
||||||
|
export function getStarbookItemsApi(type, category, grade = null, page = 1, pageSize = 20) {
|
||||||
|
let url = `/api/v1/starbook/items?type=${type}&category=${category}&page=${page}&page_size=${pageSize}`
|
||||||
|
if (grade !== null) {
|
||||||
|
url += `&grade=${grade}`
|
||||||
|
}
|
||||||
|
return request({
|
||||||
|
url: url,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -52,6 +52,47 @@
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// ==================== 新手引导阶段配置 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新手引导阶段配置
|
||||||
|
* 用于后端存储和双重校验
|
||||||
|
*
|
||||||
|
* 结构:
|
||||||
|
* {
|
||||||
|
* stage: 0, // 阶段号
|
||||||
|
* name: '初识平台', // 阶段名称
|
||||||
|
* required_task_keys: [], // 该阶段必须完成的任务key列表
|
||||||
|
* crystal_reward: 100, // 水晶奖励
|
||||||
|
* exp_reward: 50 // 经验奖励
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const onboardingStages = [
|
||||||
|
{
|
||||||
|
stage: 0,
|
||||||
|
name: '初识平台',
|
||||||
|
required_task_keys: ['square_home', 'browse_exhibition'],
|
||||||
|
crystal_reward: 100,
|
||||||
|
exp_reward: 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: 1,
|
||||||
|
name: '互动入门',
|
||||||
|
required_task_keys: ['follow_star', 'send_gift'],
|
||||||
|
crystal_reward: 200,
|
||||||
|
exp_reward: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: 2,
|
||||||
|
name: '社交达人',
|
||||||
|
required_task_keys: ['add_friend', 'share_content'],
|
||||||
|
crystal_reward: 300,
|
||||||
|
exp_reward: 150
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// ==================== 引导配置文件 ====================
|
||||||
|
|
||||||
export const guideConfig = {
|
export const guideConfig = {
|
||||||
// 广场页面引导
|
// 广场页面引导
|
||||||
square_home: {
|
square_home: {
|
||||||
|
|||||||
@ -43,10 +43,11 @@ export const claimAllDailyTasks = (starId) =>
|
|||||||
/**
|
/**
|
||||||
* 完成引导步骤
|
* 完成引导步骤
|
||||||
* @param {string} taskKey - 引导任务key(如 square_home, profile_edit)
|
* @param {string} taskKey - 引导任务key(如 square_home, profile_edit)
|
||||||
|
* @param {Array} stages - 前端配置的阶段列表(首次调用时传,后续不传)
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export const completeGuide = (taskKey) =>
|
export const completeGuide = (taskKey, stages = []) =>
|
||||||
request({ url: '/api/v1/tasks/guide/complete', method: 'POST', data: { task_key: taskKey } })
|
request({ url: '/api/v1/tasks/guide/complete', method: 'POST', data: { task_key: taskKey, stages } })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取引导状态
|
* 获取引导状态
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 856 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
Loading…
Reference in New Issue
Block a user