Explorar el Código

feat: 实现小游戏作为任务类型功能

- 后端:添加minigameCode字段支持小游戏任务
- TaskService增加completeMinigameTask方法处理小游戏任务完成
- TaskController增加minigame-options和complete-minigame接口
- 前端:创建任务页面支持选择小游戏类
- 任务列表显示小游戏任务标识
- 小游戏页面支持从任务跳转并调用任务完成接口
User hace 6 meses
padre
commit
ccbf3761a8

+ 15 - 0
zxyj-backend/src/main/java/com/zxyj/config/DatabaseInitializer.java

@@ -836,6 +836,21 @@ log.info("已添加template_id列到tasks表");
             // 列已存在,忽略错误
         }
 
+        // 迁移24: tasks表添加minigame_code字段(小游戏任务)
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN minigame_code VARCHAR(50) DEFAULT NULL COMMENT '小游戏代码: schulte/1a2b/sudoku'");
+            log.info("已添加minigame_code列到tasks表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE tasks ADD INDEX idx_minigame (minigame_code) WHERE minigame_code IS NOT NULL");
+            log.info("已添加idx_minigame索引到tasks表");
+        } catch (Exception e) {
+            // 索引已存在,忽略错误
+        }
+
         log.info("数据库迁移完成");
     }
 

+ 35 - 0
zxyj-backend/src/main/java/com/zxyj/controller/auth/AuthController.java

@@ -3,6 +3,7 @@ package com.zxyj.controller.auth;
 import com.zxyj.common.Result;
 import com.zxyj.dto.*;
 import com.zxyj.dto.RegisterWithIdCardDTO;
+import com.zxyj.dto.RegisterWithInviteCodeDTO;
 import com.zxyj.entity.User;
 import com.zxyj.service.UserService;
 import com.zxyj.service.VerificationCodeService;
@@ -196,6 +197,40 @@ public class AuthController {
         return Result.success(result);
     }
 
+    /**
+     * 使用邀请码注册并加入家庭
+     */
+    @Operation(summary = "使用邀请码注册")
+    @PostMapping("/register-with-invite")
+    public Result<LoginResultDTO> registerWithInvite(@RequestBody RegisterWithInviteCodeDTO dto) {
+        if (dto.getPhone() == null || dto.getPhone().isEmpty()) {
+            return Result.error("手机号不能为空");
+        }
+        if (dto.getInviteCode() == null || dto.getInviteCode().isEmpty()) {
+            return Result.error("邀请码不能为空");
+        }
+        if (dto.getCode() == null || dto.getCode().isEmpty()) {
+            return Result.error("验证码不能为空");
+        }
+        
+        // 验证验证码
+        if (!verificationCodeService.verifyCode(dto.getPhone(), dto.getCode(), dto.getCodeId())) {
+            return Result.error("验证码错误或已过期");
+        }
+        
+        // 检查手机号是否已注册
+        if (userService.isPhoneRegistered(dto.getPhone())) {
+            return Result.error("该手机号已注册,请使用验证码登录");
+        }
+        
+        try {
+            LoginResultDTO result = userService.registerWithInviteCode(dto);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
     private Long getUserId(javax.servlet.http.HttpServletRequest request) {
         Object userIdObj = request.getAttribute("userId");
         if (userIdObj != null) {

+ 135 - 0
zxyj-backend/src/main/java/com/zxyj/controller/family/FamilyController.java

@@ -0,0 +1,135 @@
+package com.zxyj.controller.family;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zxyj.common.Result;
+import com.zxyj.entity.Family;
+import com.zxyj.entity.User;
+import com.zxyj.mapper.FamilyMapper;
+import com.zxyj.mapper.UserMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.util.DigestUtils;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.UUID;
+
+/**
+ * 家庭管理控制器
+ */
+@Tag(name = "家庭管理", description = "家庭相关接口")
+@RestController
+@RequestMapping("/api/family")
+public class FamilyController {
+
+    @Resource
+    private FamilyMapper familyMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    /**
+     * 获取当前家庭的邀请码
+     */
+    @Operation(summary = "获取邀请码")
+    @GetMapping("/invite-code")
+    public Result<String> getInviteCode(@RequestHeader("X-User-Id") Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getFamilyId() == null) {
+            return Result.error("用户没有家庭");
+        }
+
+        Family family = familyMapper.selectById(user.getFamilyId());
+        if (family == null) {
+            return Result.error("家庭不存在");
+        }
+
+        // 如果没有邀请码,生成一个
+        if (family.getInviteCode() == null || family.getInviteCode().isEmpty()) {
+            family.setInviteCode(generateInviteCode());
+            family.setUpdatedAt(new Date());
+            familyMapper.updateById(family);
+        }
+
+        return Result.success(family.getInviteCode());
+    }
+
+    /**
+     * 生成/重置邀请码
+     */
+    @Operation(summary = "生成邀请码")
+    @PostMapping("/invite-code/generate")
+    public Result<String> generateInviteCode(@RequestHeader("X-User-Id") Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getFamilyId() == null) {
+            return Result.error("用户没有家庭");
+        }
+
+        Family family = familyMapper.selectById(user.getFamilyId());
+        if (family == null) {
+            return Result.error("家庭不存在");
+        }
+
+        // 生成新的邀请码
+        String newCode = generateInviteCode();
+        family.setInviteCode(newCode);
+        family.setUpdatedAt(new Date());
+        familyMapper.updateById(family);
+
+        return Result.success(newCode);
+    }
+
+    /**
+     * 指导师绑定家庭
+     */
+    @Operation(summary = "指导师绑定家庭")
+    @PostMapping("/guide-bind")
+    public Result<String> guideBindFamily(@RequestHeader("X-User-Id") Long userId, 
+                                          @RequestBody Map<String, String> params) {
+        String inviteCode = params.get("inviteCode");
+        if (inviteCode == null || inviteCode.isEmpty()) {
+            return Result.error("邀请码不能为空");
+        }
+
+        User guide = userMapper.selectById(userId);
+        if (guide == null || !"teacher".equals(guide.getRole())) {
+            return Result.error("只有指导师才能使用此功能");
+        }
+
+        // 查找家庭
+        Family family = familyMapper.selectOne(new LambdaQueryWrapper<Family>()
+                .eq(Family::getInviteCode, inviteCode));
+        
+        if (family == null) {
+            return Result.error("邀请码无效");
+        }
+
+        // 更新家庭的指导师ID
+        family.setTeacherId(userId);
+        family.setUpdatedAt(new Date());
+        familyMapper.updateById(family);
+
+        // 更新指导师的 teacherFamilyIds
+        String currentIds = guide.getTeacherFamilyIds();
+        String newFamilyId = String.valueOf(family.getId());
+        if (currentIds == null || !currentIds.contains(newFamilyId)) {
+            String updatedIds = currentIds == null ? newFamilyId : currentIds + "," + newFamilyId;
+            guide.setTeacherFamilyIds(updatedIds);
+            guide.setUpdatedAt(new Date());
+            userMapper.updateById(guide);
+        }
+
+        return Result.success("绑定成功");
+    }
+
+    /**
+     * 生成随机邀请码
+     */
+    private String generateInviteCode() {
+        // 生成8位随机邀请码
+        String uuid = UUID.randomUUID().toString().replace("-", "").toUpperCase();
+        return uuid.substring(0, 8);
+    }
+}

+ 35 - 4
zxyj-backend/src/main/java/com/zxyj/controller/task/TaskController.java

@@ -5,7 +5,9 @@ import com.zxyj.common.Result;
 import com.zxyj.dto.CompleteTaskDTO;
 import com.zxyj.dto.CreateTaskDTO;
 import com.zxyj.dto.TaskReviewDTO;
+import com.zxyj.entity.MiniGame;
 import com.zxyj.entity.Task;
+import com.zxyj.service.MiniGameService;
 import com.zxyj.service.TaskService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -23,6 +25,16 @@ public class TaskController {
     @Resource
     private TaskService taskService;
 
+    @Resource
+    private MiniGameService miniGameService;
+
+    @Operation(summary = "获取可选择的小游戏列表")
+    @PostMapping("/minigame-options")
+    public Result<List<MiniGame>> getMinigameOptions() {
+        List<MiniGame> games = miniGameService.getEnabledGames();
+        return Result.success(games);
+    }
+
     @Operation(summary = "创建任务")
     @PostMapping
     public Result<Long> createTask(@RequestAttribute("userId") Long userId,
@@ -91,8 +103,27 @@ public class TaskController {
     @Operation(summary = "删除任务")
     @DeleteMapping("/{id}")
     public Result<Boolean> deleteTask(@PathVariable Long id,
-                                      @RequestAttribute("userId") Long userId) {
-    boolean success = taskService.deleteTask(id, userId);
-    return Result.success(success);
-  }
+            @RequestAttribute("userId") Long userId) {
+        boolean success = taskService.deleteTask(id, userId);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "完成小游戏任务")
+    @PostMapping("/{id}/complete-minigame")
+    public Result<Map<String, Object>> completeMinigameTask(
+            @PathVariable Long id,
+            @RequestBody Map<String, Object> body) {
+        Long childId = Long.valueOf(body.get("childId").toString());
+        Integer completionTime = body.get("completionTime") != null ?
+            Integer.valueOf(body.get("completionTime").toString()) : null;
+        Integer score = body.get("score") != null ?
+            Integer.valueOf(body.get("score").toString()) : null;
+
+        try {
+            Map<String, Object> result = taskService.completeMinigameTask(id, childId, completionTime, score);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error(e.getMessage());
+        }
+    }
 }

+ 4 - 1
zxyj-backend/src/main/java/com/zxyj/dto/CreateTaskDTO.java

@@ -37,7 +37,10 @@ public class CreateTaskDTO {
     private Long executorId;
     // 兼容旧版 - 指定孩子ID
     private Long childId;
-    
+
+    // 小游戏代码(当category为'小游戏类'时必填): schulte/1a2b/sudoku
+    private String minigameCode;
+
     // 重复类型(兼容旧版)
     private String repeatType;
     // 截止时间(兼容旧版)

+ 24 - 0
zxyj-backend/src/main/java/com/zxyj/dto/InviteCodeDTO.java

@@ -0,0 +1,24 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+/**
+ * 邀请码DTO
+ */
+@Data
+public class InviteCodeDTO {
+    /**
+     * 邀请码
+     */
+    private String inviteCode;
+    
+    /**
+     * 邀请类型: family (家庭成员) / guide (指导师)
+     */
+    private String inviteType;
+    
+    /**
+     * 家庭ID (指导师邀请时需要)
+     */
+    private Long familyId;
+}

+ 39 - 0
zxyj-backend/src/main/java/com/zxyj/dto/RegisterWithInviteCodeDTO.java

@@ -0,0 +1,39 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+/**
+ * 使用邀请码注册DTO
+ */
+@Data
+public class RegisterWithInviteCodeDTO {
+    /**
+     * 手机号
+     */
+    private String phone;
+    
+    /**
+     * 验证码
+     */
+    private String code;
+    
+    /**
+     * 邀请码
+     */
+    private String inviteCode;
+    
+    /**
+     * 角色: parent / child (指导师通过另一个流程)
+     */
+    private String role;
+    
+    /**
+     * 昵称 (可选)
+     */
+    private String nickname;
+    
+    /**
+     * 验证码ID (用于验证)
+     */
+    private String codeId;
+}

+ 1 - 1
zxyj-backend/src/main/java/com/zxyj/entity/Family.java

@@ -17,7 +17,7 @@ public class Family implements Serializable {
 
     private String name;
 
-    private String inviteCode;
+    private String inviteCode; // 邀请码 (唯一索引)
 
     private Long streetId;        // 街道ID(关联streets表)
 

+ 7 - 4
zxyj-backend/src/main/java/com/zxyj/entity/Task.java

@@ -81,10 +81,13 @@ private String completeTypes;
 // 最早开始时间
 private Date earliestStart;
 
-// 最晚结束时间
-private Date latestEnd;
+    // 最晚结束时间
+    private Date latestEnd;
 
-private Date createdAt;
+    // 小游戏代码(当category为'小游戏类'时使用): schulte/1a2b/sudoku
+    private String minigameCode;
 
-private Date updatedAt;
+    private Date createdAt;
+
+    private Date updatedAt;
 }

+ 119 - 17
zxyj-backend/src/main/java/com/zxyj/service/TaskService.java

@@ -35,7 +35,13 @@ public class TaskService {
     @Resource
     private StreakService streakService;
 
-    private static final int EARLY_BONUS = 1;       // 提前完成奖励
+    @Resource
+    private MiniGameService miniGameService;
+
+    @Resource
+    private MiniGameMapper miniGameMapper;
+
+    private static final int EARLY_BONUS = 1; // 提前完成奖励
     private static final int PENALTY_MAX_DAILY = 5; // 每日最多扣分
     private static final int LATE_GRACE_MINUTES = 10; // 10分钟内不算迟到
 
@@ -105,7 +111,26 @@ public class TaskService {
         task.setTitle(dto.getTitle());
         task.setDescription(dto.getDescription());
         task.setPoints(dto.getPoints() != null ? dto.getPoints() : 2);
-        
+
+        // 处理小游戏任务
+        task.setMinigameCode(dto.getMinigameCode());
+        if ("小游戏类".equals(dto.getCategory()) && dto.getMinigameCode() != null) {
+            // 小游戏任务:从MiniGame获取默认积分
+            MiniGame miniGame = miniGameMapper.selectOne(
+                new LambdaQueryWrapper<MiniGame>()
+                    .eq(MiniGame::getGameCode, dto.getMinigameCode())
+                    .eq(MiniGame::getStatus, 1)
+            );
+            if (miniGame != null) {
+                if (dto.getPoints() == null) {
+                    task.setPoints(miniGame.getDefaultPoints() != null ? miniGame.getDefaultPoints() : 5);
+                }
+                if (dto.getTitle() == null || dto.getTitle().isEmpty()) {
+                    task.setTitle(miniGame.getGameName());
+                }
+            }
+        }
+
         // 如果没有指定截止时间,默认今天23:59
         if (dto.getDeadline() == null) {
             Calendar cal = Calendar.getInstance();
@@ -179,19 +204,28 @@ return taskMapper.selectList(new LambdaQueryWrapper<Task>()
 .orderByAsc(Task::getDeadline));
 }
 
-@Transactional
-public Map<String, Object> completeTask(Long taskId, Long childId, String photoUrl) {
-Task task = taskMapper.selectById(taskId);
-if (task == null || !task.getChildId().equals(childId)) {
-return null;
-}
+    @Transactional
+    public Map<String, Object> completeTask(Long taskId, Long childId, String photoUrl) {
+        Task task = taskMapper.selectById(taskId);
+        if (task == null || !task.getChildId().equals(childId)) {
+            return null;
+        }
 
-if ("completed".equals(task.getStatus())) {
-return null; // 任务已完成,不能重复提交
-}
+        if ("completed".equals(task.getStatus())) {
+            return null; // 任务已完成,不能重复提交
+        }
 
-Date now = new Date();
-int pointsEarned = task.getPoints();
+        Date now = new Date();
+        int pointsEarned;
+
+        // 检查是否为小游戏任务
+        if ("小游戏类".equals(task.getCategory()) && task.getMinigameCode() != null) {
+            // 小游戏任务不通过此接口完成,需要通过小游戏界面完成
+            // 这里只返回null,实际完成逻辑在completeMinigameTask方法中
+            return null;
+        }
+
+        pointsEarned = task.getPoints();
 boolean isEarly = now.before(task.getDeadline());
 boolean isLateGrace = false;
 
@@ -594,8 +628,76 @@ return -penalty;
 } else {
 return 0; // 达到每日扣分上限
 }
-} else {
-return 0; // 扣分未开启
-}
-}
+        } else {
+            return 0; // 扣分未开启
+        }
+    }
+
+    /**
+     * 完成小游戏任务
+     * @param taskId 任务ID
+     * @param childId 孩子ID
+     * @param completionTime 完成用时(秒)
+     * @param score 得分(可选)
+     * @return 积分信息
+     */
+    @Transactional
+    public Map<String, Object> completeMinigameTask(Long taskId, Long childId, Integer completionTime, Integer score) {
+        Task task = taskMapper.selectById(taskId);
+        if (task == null || !task.getChildId().equals(childId)) {
+            throw new RuntimeException("任务不存在或不属于该孩子");
+        }
+
+        if ("completed".equals(task.getStatus())) {
+            throw new RuntimeException("任务已完成");
+        }
+
+        if (!"小游戏类".equals(task.getCategory()) || task.getMinigameCode() == null) {
+            throw new RuntimeException("该任务不是小游戏任务");
+        }
+
+        // 调用MiniGameService计算积分
+        Map<String, Object> gameResult = miniGameService.completeGame(childId, task.getMinigameCode(), completionTime, score);
+        int pointsEarned = (Integer) gameResult.get("pointsEarned");
+
+        // 更新任务状态
+        task.setStatus("completed");
+        task.setCompletedAt(new Date());
+        task.setUpdatedAt(new Date());
+        taskMapper.updateById(task);
+
+        // 更新连续打卡
+        Child child = childMapper.selectById(childId);
+        Date lastTaskDate = child.getLastTaskDate();
+        Date today = new Date();
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.DAY_OF_MONTH, -1);
+        Date yesterday = cal.getTime();
+
+        if (lastTaskDate == null || lastTaskDate.before(yesterday)) {
+            child.setStreakDays(1); // 重新开始
+        } else if (lastTaskDate != null && isSameDay(lastTaskDate, today)) {
+            // 今天已经完成过任务,不增加天数
+        } else {
+            child.setStreakDays(child.getStreakDays() + 1);
+        }
+        child.setLastTaskDate(today);
+        child.setUpdatedAt(new Date());
+        childMapper.updateById(child);
+
+        // 检查并发放打卡里程碑奖励
+        int milestoneReward = streakService.checkAndAwardMilestone(childId, child.getStreakDays());
+        if (milestoneReward > 0) {
+            pointsEarned += milestoneReward;
+            log.info("孩子{}获得打卡里程碑奖励{}积分", childId, milestoneReward);
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("pointsEarned", pointsEarned);
+        result.put("newBalance", child.getTotalPoints());
+        result.put("gameName", gameResult.get("gameName"));
+        result.put("needReview", task.getNeedReview() == 1);
+
+        return result;
+    }
 }

+ 77 - 0
zxyj-backend/src/main/java/com/zxyj/service/UserService.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.zxyj.config.JwtConfig;
 import com.zxyj.dto.*;
 import com.zxyj.dto.RegisterWithIdCardDTO;
+import com.zxyj.dto.RegisterWithInviteCodeDTO;
 import com.zxyj.entity.Child;
 import com.zxyj.entity.Family;
 import com.zxyj.entity.User;
@@ -895,4 +896,80 @@ private WechatService wechatService;
         userMapper.updateById(user);
         return family.getId();
     }
+
+    /**
+     * 使用邀请码注册并加入家庭
+     */
+    public LoginResultDTO registerWithInviteCode(RegisterWithInviteCodeDTO dto) {
+        String phone = dto.getPhone();
+        String inviteCode = dto.getInviteCode();
+        String role = dto.getRole() != null ? dto.getRole() : "parent";
+        String nickname = dto.getNickname() != null ? dto.getNickname() : "用户" + phone.substring(7);
+        
+        // 查找家庭
+        Family family = familyMapper.selectOne(new LambdaQueryWrapper<Family>()
+                .eq(Family::getInviteCode, inviteCode));
+        
+        User user;
+        boolean isNewUser = false;
+        
+        if (family == null) {
+            throw new RuntimeException("邀请码无效");
+        }
+        
+        // 检查手机号是否已注册
+        User existingUser = userMapper.selectOne(new LambdaQueryWrapper<User>()
+                .eq(User::getPhone, phone));
+        
+        if (existingUser != null) {
+            // 手机号已注册,加入家庭
+            user = existingUser;
+            user.setFamilyId(family.getId());
+            user.setUpdatedAt(new Date());
+            userMapper.updateById(user);
+        } else {
+            // 创建新用户并加入家庭
+            isNewUser = true;
+            user = new User();
+            user.setOpenid("phone:" + phone);
+            user.setUnionid("");
+            user.setFamilyId(family.getId());
+            user.setRole(role);
+            user.setNickname(nickname);
+            user.setPhone(phone);
+            user.setPassword("");
+            user.setCreatedAt(new Date());
+            user.setUpdatedAt(new Date());
+            userMapper.insert(user);
+        }
+        
+        // 如果家庭绑定了指导师,自动将家庭ID添加到指导师的 teacherFamilyIds
+        if (family.getTeacherId() != null) {
+            User teacher = userMapper.selectById(family.getTeacherId());
+            if (teacher != null && "teacher".equals(teacher.getRole())) {
+                String currentIds = teacher.getTeacherFamilyIds();
+                String newFamilyId = String.valueOf(family.getId());
+                if (currentIds == null || !currentIds.contains(newFamilyId)) {
+                    String updatedIds = currentIds == null ? newFamilyId : currentIds + "," + newFamilyId;
+                    teacher.setTeacherFamilyIds(updatedIds);
+                    teacher.setUpdatedAt(new Date());
+                    userMapper.updateById(teacher);
+                }
+            }
+        }
+        
+        // 生成token
+        String token = jwtConfig.generateToken(user.getId(), user.getRole());
+        
+        LoginResultDTO result = new LoginResultDTO();
+        result.setToken(token);
+        result.setUserId(user.getId());
+        result.setRole(user.getRole());
+        result.setRoles(user.getRoles());
+        result.setFamilyId(user.getFamilyId());
+        result.setNickname(user.getNickname());
+        result.setIsNewUser(isNewUser);
+        
+        return result;
+    }
 }

+ 10 - 4
zxyj-frontend/pages.json

@@ -200,10 +200,16 @@
 }
 },
 {
-"path": "pages/parent/wishes/index",
-"style": {
-"navigationBarTitleText": "心愿管理"
-}
+  "path": "pages/parent/wishes/index",
+  "style": {
+    "navigationBarTitleText": "心愿管理"
+  }
+},
+{
+  "path": "pages/parent/invite/index",
+  "style": {
+    "navigationBarTitleText": "邀请加入"
+  }
 }
 ],
   "globalStyle": {

+ 37 - 8
zxyj-frontend/pages/child/tasks.vue

@@ -8,15 +8,24 @@
       
       <view class="task-list" v-if="tasks.length > 0">
         <view v-for="task in tasks" :key="task.id" class="task-item" :class="{ completed: task.status === 'completed' }">
-          <view class="task-left">
-            <view class="task-title">{{ task.title }}</view>
-            <view class="task-meta">
-              <text class="points">+{{ task.points }}分</text>
-              <text class="deadline">截止 {{ formatTime(task.deadline) }}</text>
+            <view class="task-left">
+              <view class="task-title">
+                {{ task.title }}
+                <text v-if="task.category === '小游戏类'" class="minigame-badge">🎮</text>
+              </view>
+              <view class="task-meta">
+                <text class="points">+{{ task.points }}分</text>
+                <text class="deadline">截止 {{ formatTime(task.deadline) }}</text>
+              </view>
             </view>
-          </view>
-          <view class="task-right">
-            <button v-if="task.status === 'pending'" class="btn-complete" @click="completeChildTask(task.id)">完成</button>
+            <view class="task-right">
+              <button 
+                v-if="task.status === 'pending'" 
+                class="btn-complete" 
+                @click="handleTaskClick(task)"
+              >
+                {{ task.category === '小游戏类' ? '开始游戏' : '完成' }}
+              </button>
             <view v-else class="completed-badge">已完成</view>
           </view>
         </view>
@@ -74,6 +83,25 @@ export default {
         console.error('完成任务失败', e)
       }
     },
+    handleTaskClick(task) {
+      // 如果是小游戏任务,跳转到对应的小游戏页面
+      if (task.category === '小游戏类' && task.minigameCode) {
+        const gamePages = {
+          'schulte': '/pages/games/schulte',
+          '1a2b': '/pages/games/1a2b',
+          'sudoku': '/pages/games/sudoku'
+        }
+        const gamePage = gamePages[task.minigameCode]
+        if (gamePage) {
+          uni.navigateTo({
+            url: `${gamePage}?taskId=${task.id}&childId=${this.currentChildId}`
+          })
+        }
+      } else {
+        // 普通任务,直接完成
+        this.completeChildTask(task.id)
+      }
+    },
     formatTime(dateStr) {
       if (!dateStr) return ''
       const date = new Date(dateStr)
@@ -130,5 +158,6 @@ export default {
 .deadline { color: #999; font-size: 24rpx; }
 .btn-complete { background: #FF6B6B; color: #fff; font-size: 26rpx; padding: 10rpx 30rpx; border-radius: 30rpx; }
 .completed-badge { color: #52C41A; font-size: 26rpx; }
+.minigame-badge { font-size: 28rpx; margin-left: 10rpx; }
 .empty { text-align: center; color: #999; padding: 100rpx; }
 </style>

+ 72 - 93
zxyj-frontend/pages/games/1a2b.vue

@@ -1,7 +1,7 @@
 <template>
   <view class="game-container">
     <view class="game-header">
-      <text class="game-title">🔢 猜数字</text>
+      <text class="game-title">猜数字</text>
       <view class="game-stats">
         <text>剩余次数: {{ remainingGuesses }}</text>
       </view>
@@ -45,17 +45,17 @@
     
     <view class="result-modal" v-if="gameOver">
       <view class="result-content">
-        <text class="result-title">{{ isWin ? '🎉 恭喜猜对!' : '😢 游戏结束' }}</text>
+        <text class="result-title">{{ isWin ? '猜对了!' : '游戏结束' }}</text>
         <text class="result-info">答案是: {{ answer }}</text>
         <text class="result-score">获得积分: {{ score }}</text>
-        <button class="restart-btn" @click="resetGame">再来一次</button>
+        <button class="restart-btn" @click="resetGame">再玩一次</button>
       </view>
     </view>
   </view>
 </template>
 
 <script>
-import { completeMiniGame, getChildren } from '../../utils/api.js'
+import { completeMiniGame, getChildren } from '@/utils/api.js'
 
 export default {
   data() {
@@ -72,12 +72,18 @@ export default {
       time: 0,
       timer: null,
       childId: null,
+      taskId: null,
       pointsEarned: 0,
-      newBalance: 0,
-      totalGuesses: 0
+      newBalance: 0
     }
   },
-  onLoad() {
+  onLoad(options) {
+    if (options.taskId) {
+      this.taskId = parseInt(options.taskId)
+    }
+    if (options.childId) {
+      this.childId = parseInt(options.childId)
+    }
     this.loadChildId()
   },
   onUnload() {
@@ -86,9 +92,11 @@ export default {
   methods: {
     async loadChildId() {
       try {
-        const res = await getChildren()
-        if (res.data && res.data.length > 0) {
-          this.childId = res.data[0].id
+        if (!this.childId) {
+          const res = await getChildren()
+          if (res.data && res.data.length > 0) {
+            this.childId = res.data[0].id
+          }
         }
       } catch (e) {
         console.error('获取孩子ID失败', e)
@@ -106,32 +114,16 @@ export default {
     },
     startGame() {
       if (this.userGuess.length !== 4 || !this.isUnique(this.userGuess)) {
-        uni.showToast({ title: '请输入4位不重复数字', icon: 'none' })
+        uni.showToast({ title: '请输入4位不重��数字', icon: 'none' })
         return
       }
-      
       this.answer = this.generateAnswer()
       this.isPlaying = true
       this.gameOver = false
       this.remainingGuesses = this.maxGuesses
       this.history = []
       this.score = 0
-      
-      // 检查第一次猜测是否直接猜对
-      const result = this.checkGuess(this.userGuess)
-      if (result === '4A0B') {
-        clearInterval(this.timer)
-        this.gameOver = true
-        this.isWin = true
-        this.score = 100 // 第一次猜对满分
-        this.submitScore()
-        return
-      }
-      
-      // 启动计时器
-      this.timer = setInterval(() => {
-        this.time++
-      }, 1000)
+      this.timer = setInterval(() => { this.time++ }, 1000)
     },
     isUnique(str) {
       return new Set(str).size === str.length
@@ -152,39 +144,45 @@ export default {
         uni.showToast({ title: '请输入4位不重复数字', icon: 'none' })
         return
       }
-      
       const result = this.checkGuess(this.userGuess)
       this.history.push({ guess: this.userGuess, result })
       this.remainingGuesses--
-      
       if (result === '4A0B') {
-        // 猜对了,计算分数:尝试次数越少分数越高
-        const guessesUsed = this.maxGuesses - this.remainingGuesses
-        this.score = Math.max(100 - guessesUsed * 8, 60) // 最低60分
-        
+        this.score = Math.max(100 - (this.maxGuesses - this.remainingGuesses) * 8, 60)
         this.gameOver = true
         this.isWin = true
+        clearInterval(this.timer)
         this.submitScore()
       } else if (this.remainingGuesses === 0) {
         this.gameOver = true
         this.isWin = false
-        this.score = 30 // 失败给保底分
+        this.score = 30
         this.submitScore()
       }
-      
       this.userGuess = ''
     },
     async submitScore() {
       if (!this.childId) return
       try {
+        if (this.taskId) {
+          const token = uni.getStorageSync('token')
+          await uni.request({
+            url: `http://localhost:8080/api/tasks/${this.taskId}/complete-minigame`,
+            method: 'POST',
+            header: {
+              'Authorization': token ? `Bearer ${token}` : '',
+              'Content-Type': 'application/json'
+            },
+            data: {
+              childId: this.childId,
+              completionTime: this.time,
+              score: this.score
+            }
+          })
+        }
         const res = await completeMiniGame(this.childId, '1a2b', this.time, this.score)
         if (res.code === 200) {
-          this.pointsEarned = res.data.pointsEarned
-          this.newBalance = res.data.newBalance
-          uni.showToast({
-            title: `获得${this.pointsEarned}积分!`,
-            icon: 'success'
-          })
+          uni.showToast({ title: `获得${res.data.pointsEarned}积分!`, icon: 'success' })
         }
       } catch (e) {
         console.error('提交成绩失败', e)
@@ -195,6 +193,8 @@ export default {
       this.gameOver = false
       this.userGuess = ''
       this.history = []
+      this.time = 0
+      if (this.timer) clearInterval(this.timer)
     }
   }
 }
@@ -206,84 +206,69 @@ export default {
   background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
   padding: 30rpx;
 }
-
 .game-header {
   text-align: center;
   margin-bottom: 40rpx;
 }
-
 .game-title {
-  display: block;
   font-size: 48rpx;
-  font-weight: bold;
-  color: #FFE66D;
+  color: #fff;
+  display: block;
+  margin-bottom: 20rpx;
 }
-
 .game-stats {
-  margin-top: 20rpx;
-  color: #fff;
-  font-size: 32rpx;
+  color: #888;
+  font-size: 28rpx;
 }
-
 .instructions {
-  background: #2a2a4a;
+  background: rgba(255,255,255,0.1);
   padding: 30rpx;
-  border-radius: 20rpx;
-  margin-bottom: 40rpx;
+  border-radius: 16rpx;
+  margin-bottom: 30rpx;
 }
-
 .info-text {
-  display: block;
-  color: rgba(255,255,255,0.7);
+  color: #aaa;
   font-size: 28rpx;
+  display: block;
   margin-bottom: 10rpx;
 }
-
 .input-area {
-  display: flex;
-  gap: 20rpx;
-  margin-bottom: 40rpx;
+  margin: 30rpx 0;
 }
-
 .guess-input {
-  flex: 1;
-  background: #2a2a4a;
-  color: #fff;
+  background: rgba(255,255,255,0.1);
+  border: 1rpx solid rgba(255,255,255,0.2);
+  border-radius: 12rpx;
   padding: 20rpx;
-  border-radius: 10rpx;
+  color: #fff;
   font-size: 36rpx;
   text-align: center;
+  margin-bottom: 20rpx;
 }
-
 .start-btn, .guess-btn {
-  background: linear-gradient(135deg, #2196F3, #4CAF50);
-  color: #fff;
-  padding: 20rpx 40rpx;
-  border-radius: 50rpx;
-  border: none;
+  background: #4ECDC4;
+  color: #1a1a2e;
+  border-radius: 12rpx;
+  padding: 20rpx;
 }
-
 .history {
-  background: #2a2a4a;
-  padding: 30rpx;
-  border-radius: 20rpx;
-  margin-bottom: 40rpx;
+  background: rgba(255,255,255,0.05);
+  padding: 20rpx;
+  border-radius: 12rpx;
+  margin: 30rpx 0;
 }
-
 .history-title {
+  color: #888;
+  font-size: 28rpx;
   display: block;
-  color: #4ECDC4;
-  font-size: 32rpx;
-  margin-bottom: 20rpx;
+  margin-bottom: 16rpx;
 }
-
 .history-item {
-  color: rgba(255,255,255,0.7);
+  color: #ccc;
   font-size: 28rpx;
   padding: 10rpx 0;
   border-bottom: 1rpx solid rgba(255,255,255,0.1);
 }
-
 .result-modal {
   position: fixed;
   top: 0;
@@ -295,34 +280,28 @@ export default {
   align-items: center;
   justify-content: center;
 }
-
 .result-content {
   background: #2a2a4a;
   padding: 60rpx;
   border-radius: 20rpx;
   text-align: center;
 }
-
 .result-title {
-  display: block;
   font-size: 48rpx;
   color: #FFE66D;
+  display: block;
   margin-bottom: 30rpx;
 }
-
 .result-info, .result-score {
-  display: block;
   color: #fff;
   font-size: 32rpx;
+  display: block;
   margin-bottom: 20rpx;
 }
-
 .restart-btn {
   background: #4ECDC4;
   color: #1a1a2e;
   padding: 20rpx 60rpx;
-  border-radius: 50rpx;
-  border: none;
   margin-top: 30rpx;
 }
 </style>

+ 48 - 10
zxyj-frontend/pages/games/schulte.vue

@@ -50,11 +50,19 @@ export default {
       score: 0,
       showResult: false,
       childId: null,
+      taskId: null,
       pointsEarned: 0,
       newBalance: 0
     }
   },
-  onLoad() {
+  onLoad(options) {
+    // 支持从任务跳转过来
+    if (options.taskId) {
+      this.taskId = parseInt(options.taskId)
+    }
+    if (options.childId) {
+      this.childId = parseInt(options.childId)
+    }
     this.loadChildId()
   },
   onUnload() {
@@ -74,16 +82,18 @@ export default {
       }
       this.numbers = nums
     },
-    async loadChildId() {
-      try {
+  async loadChildId() {
+    try {
+      if (!this.childId) {
         const res = await getChildren()
         if (res.data && res.data.length > 0) {
           this.childId = res.data[0].id
         }
-      } catch (e) {
-        console.error('获取孩子ID失败', e)
       }
-    },
+    } catch (e) {
+      console.error('获取孩子ID失败', e)
+    }
+  },
     startGame() {
       this.currentNumber = 1
       this.isPlaying = true
@@ -121,8 +131,35 @@ export default {
         this.submitScore()
       }
     },
-    async submitScore() {
-      try {
+  async submitScore() {
+    try {
+      const BASE_URL = 'http://localhost:8080'
+      const token = uni.getStorageSync('token')
+      // 如果有taskId,说明是从任务跳转来的,使用任务完成接口
+      if (this.taskId) {
+        const res = await uni.request({
+          url: `${BASE_URL}/api/tasks/${this.taskId}/complete-minigame`,
+          method: 'POST',
+          header: {
+            'Authorization': token ? `Bearer ${token}` : '',
+            'Content-Type': 'application/json'
+          },
+          data: {
+            childId: this.childId,
+            completionTime: this.time,
+            score: this.score
+          }
+        })
+        if (res.data && res.data.code === 200) {
+          this.pointsEarned = res.data.data.pointsEarned
+          this.newBalance = res.data.data.newBalance
+          uni.showToast({
+            title: `获得${this.pointsEarned}积分!`,
+            icon: 'success'
+          })
+        }
+      } else {
+        // 普通小游戏完成
         const res = await completeMiniGame(this.childId, 'schulte', this.time, this.score)
         if (res.code === 200) {
           this.pointsEarned = res.data.pointsEarned
@@ -132,11 +169,12 @@ export default {
             icon: 'success'
           })
         }
-      } catch (e) {
-        console.error('提交成绩失败', e)
       }
+    } catch (e) {
+      console.error('提交成绩失败', e)
     }
   }
+  }
 }
 </script>
 

+ 48 - 11
zxyj-frontend/pages/games/sudoku.vue

@@ -176,11 +176,18 @@ export default {
         { label: '困难', value: 'hard' }
       ],
       childId: null,
+      taskId: null,
       pointsEarned: 0,
       newBalance: 0
     }
   },
-  onLoad() {
+  onLoad(options) {
+    if (options.taskId) {
+      this.taskId = parseInt(options.taskId)
+    }
+    if (options.childId) {
+      this.childId = parseInt(options.childId)
+    }
     this.loadChildId()
   },
   onUnload() {
@@ -189,9 +196,11 @@ export default {
   methods: {
     async loadChildId() {
       try {
-        const res = await getChildren()
-        if (res.data && res.data.length > 0) {
-          this.childId = res.data[0].id
+        if (!this.childId) {
+          const res = await getChildren()
+          if (res.data && res.data.length > 0) {
+            this.childId = res.data[0].id
+          }
         }
       } catch (e) {
         console.error('获取孩子ID失败', e)
@@ -259,14 +268,42 @@ export default {
     },
     async submitScore() {
       try {
-        const res = await completeMiniGame(this.childId, 'sudoku', this.time, this.score)
-        if (res.code === 200) {
-          this.pointsEarned = res.data.pointsEarned
-          this.newBalance = res.data.newBalance
-          uni.showToast({
-            title: `获得${this.pointsEarned}积分!`,
-            icon: 'success'
+        const BASE_URL = 'http://localhost:8080'
+        const token = uni.getStorageSync('token')
+        // 如果有taskId,说明是从任务跳转来的,使用任务完成接口
+        if (this.taskId) {
+          const res = await uni.request({
+            url: `${BASE_URL}/api/tasks/${this.taskId}/complete-minigame`,
+            method: 'POST',
+            header: {
+              'Authorization': token ? `Bearer ${token}` : '',
+              'Content-Type': 'application/json'
+            },
+            data: {
+              childId: this.childId,
+              completionTime: this.time,
+              score: this.score
+            }
           })
+          if (res.data && res.data.code === 200) {
+            this.pointsEarned = res.data.data.pointsEarned
+            this.newBalance = res.data.data.newBalance
+            uni.showToast({
+              title: `获得${this.pointsEarned}积分!`,
+              icon: 'success'
+            })
+          }
+        } else {
+          // 普通小游戏完成
+          const res = await completeMiniGame(this.childId, 'sudoku', this.time, this.score)
+          if (res.code === 200) {
+            this.pointsEarned = res.data.pointsEarned
+            this.newBalance = res.data.newBalance
+            uni.showToast({
+              title: `获得${this.pointsEarned}积分!`,
+              icon: 'success'
+            })
+          }
         }
       } catch (e) {
         console.error('提交成绩失败', e)

+ 16 - 4
zxyj-frontend/pages/index/parent-index.vue

@@ -15,7 +15,7 @@
         <view 
           class="child-card" 
           v-for="(member, index) in familyMembers" 
-          :key="member && member.id ? (member.role + '-' + member.id) : index"
+          :key="getMemberKey(member, index)"
           @click="selectMember(member)"
           @longpress="handleMemberLongPress(member)"
         >
@@ -71,9 +71,9 @@
           <text class="icon">👨‍👩‍👧</text>
           <text class="label">孩子管理</text>
         </view>
-        <view class="action-item" @click="switchMode">
-          <text class="icon">🔄</text>
-          <text class="label">切换模式</text>
+        <view class="action-item" @click="inviteFriend">
+          <text class="icon">📤</text>
+          <text class="label">邀请家人</text>
         </view>
       </view>
     </view>
@@ -169,6 +169,12 @@ export default {
         this.switchToChildMode(member)
       }
     },
+    getMemberKey(member, index) {
+      if (member && member.id) {
+        return member.role + '-' + member.id
+      }
+      return 'member-' + index
+    },
     // 保留旧方法以兼容
     selectChild(child) {
       this.selectMember({ ...child, role: 'child' })
@@ -264,6 +270,12 @@ export default {
         url: '/pages/tasks/create-task'
       })
     },
+    // 邀请家人
+    inviteFriend() {
+      uni.navigateTo({
+        url: '/pages/parent/invite/index'
+      })
+    },
     // 添加孩子
     addChild() {
       uni.navigateTo({

+ 6 - 1
zxyj-frontend/pages/login/login.vue

@@ -84,7 +84,7 @@ export default {
       avatar: ''
     }
   },
-  onLoad() {
+  onLoad(options) {
     // 获取用户信息
     uni.getUserProfile({
       desc: '用于完善用户资料',
@@ -93,6 +93,11 @@ export default {
         this.avatar = res.userInfo.avatarUrl
       }
     })
+    // 处理邀请码
+    if (options.inviteCode) {
+      uni.setStorageSync('inviteCode', options.inviteCode)
+      uni.setStorageSync('inviteType', options.inviteType || 'family')
+    }
   },
   methods: {
     handleImageError(e) {

+ 256 - 0
zxyj-frontend/pages/parent/invite/index.vue

@@ -0,0 +1,256 @@
+<template>
+  <view class="container">
+    <view class="header">
+      <text class="title">邀请家人加入</text>
+      <text class="desc">分享给好友,好友注册后自动成为家庭成员</text>
+    </view>
+
+    <!-- 邀请码展示 -->
+    <view class="invite-card" v-if="inviteCode">
+      <view class="invite-code">{{ inviteCode }}</view>
+      <text class="invite-tip">好友也可输入邀请码加入</text>
+    </view>
+
+    <!-- 分享按钮区域 -->
+    <view class="share-section">
+      <button class="btn-share-large" open-type="share">
+        <text class="share-icon">📤</text>
+        <text class="share-text">发送给好友</text>
+      </button>
+      <text class="share-hint">点击上方按钮,分享到微信好友</text>
+    </view>
+
+    <!-- 其他方式 -->
+    <view class="other-methods">
+      <view class="method-title">其他方式</view>
+      <view class="method-item" @click="copyCode">
+        <text class="method-icon">📋</text>
+        <text class="method-text">复制邀请码</text>
+      </view>
+      <view class="method-item" @click="generatePoster">
+        <text class="method-icon">🖼️</text>
+        <text class="method-text">生成邀请海报</text>
+      </view>
+    </view>
+
+    <!-- 指导师邀请说明 -->
+    <view class="guide-tip" v-if="role === 'teacher'">
+      <text class="tip-title">指导师权限说明</text>
+      <text class="tip-text">通过邀请码绑定家庭后,您将获得:</text>
+      <text class="tip-item">• 查看家庭成员任务完成情况</text>
+      <text class="tip-item">• 审核任务执行</text>
+      <text class="tip-item">• 制定任务</text>
+    </view>
+
+    <!-- 指导师绑定家庭 -->
+    <view class="section" v-if="role === 'teacher'">
+      <view class="section-title">绑定已有家庭</view>
+      <input 
+        class="input" 
+        v-model="familyCode" 
+        placeholder="输入家庭邀请码" 
+      />
+      <button class="btn-primary" @click="bindFamily">绑定家庭</button>
+    </view>
+
+    <!-- 海报弹窗 -->
+    <view class="poster-mask" v-if="showPoster" @click="showPoster = false">
+      <view class="poster-content" @click.stop>
+        <canvas canvas-id="posterCanvas" class="poster-canvas"></canvas>
+        <button class="btn-save" @click="savePoster">保存到相册</button>
+        <text class="poster-hint">保存图片后发送给好友</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getInviteCode, generateInviteCode, bindGuideFamily } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      inviteCode: '',
+      familyCode: '',
+      role: 'parent',
+      showPoster: false,
+      familyName: ''
+    }
+  },
+  onLoad() {
+    this.role = uni.getStorageSync('role') || 'parent'
+    this.loadInviteCode()
+    this.loadFamilyName()
+  },
+  onShareAppMessage() {
+    const title = this.role === 'teacher' 
+      ? '指导师邀请 - 加入我的服务家庭' 
+      : '邀请您加入我的家庭'
+    return {
+      title: title,
+      path: `/pages/login/login?inviteCode=${this.inviteCode}&inviteType=${this.role === 'teacher' ? 'guide' : 'family'}`,
+      imageUrl: '/static/share-card.png' // 可选:自定义分享卡片图片
+    }
+  },
+  // 分享到朋友圈
+  onShareTimeline() {
+    return {
+      title: this.role === 'teacher' 
+        ? '指导师邀请 - 加入我的服务家庭' 
+        : '邀请您加入我的家庭',
+      query: `inviteCode=${this.inviteCode}&inviteType=${this.role === 'teacher' ? 'guide' : 'family'}`
+    }
+  },
+  methods: {
+    async loadInviteCode() {
+      try {
+        const res = await getInviteCode()
+        if (res.data) {
+          this.inviteCode = res.data
+        } else {
+          const genRes = await generateInviteCode()
+          this.inviteCode = genRes.data
+        }
+      } catch (e) {
+        console.error('获取邀请码失败', e)
+      }
+    },
+    async loadFamilyName() {
+      try {
+        const userInfo = uni.getStorageSync('userInfo')
+        if (userInfo && userInfo.nickname) {
+          this.familyName = userInfo.nickname + '的家庭'
+        } else {
+          this.familyName = '我的家庭'
+        }
+      } catch (e) {
+        this.familyName = '我的家庭'
+      }
+    },
+    copyCode() {
+      uni.setClipboardData({
+        data: this.inviteCode,
+        success: () => {
+          uni.showToast({ title: '已复制邀请码', icon: 'success' })
+        }
+      })
+    },
+    generatePoster() {
+      this.showPoster = true
+      this.$nextTick(() => {
+        this.drawPoster()
+      })
+    },
+    drawPoster() {
+      const ctx = uni.createCanvasContext('posterCanvas')
+      const width = 300
+      const height = 450
+      
+      // 背景
+      ctx.setFillStyle('#667eea')
+      ctx.fillRect(0, 0, width, height)
+      
+      // 标题
+      ctx.setFillStyle('#ffffff')
+      ctx.setFontSize(20)
+      ctx.setTextAlign('center')
+      ctx.fillText('邀请加入家庭', width / 2, 40)
+      
+      // 家庭名
+      ctx.setFontSize(14)
+      ctx.fillText(this.familyName, width / 2, 70)
+      
+      // 邀请码
+      ctx.setFillStyle('#ffffff')
+      ctx.setFontSize(36)
+      ctx.setFontWeight('bold')
+      ctx.fillText(this.inviteCode, width / 2, 140)
+      
+      // 提示
+      ctx.setFontSize(12)
+      ctx.setFillStyle('rgba(255,255,255,0.8)')
+      ctx.fillText('长按识别二维码或输入邀请码', width / 2, 180)
+      
+      // 二维码占位 (实际项目中应该生成真实二维码)
+      ctx.setFillStyle('#ffffff')
+      ctx.fillRect(75, 220, 150, 150)
+      ctx.setFillStyle('#667eea')
+      ctx.setFontSize(10)
+      ctx.fillText('扫码注册', width / 2, 300)
+      
+      ctx.draw()
+    },
+    savePoster() {
+      uni.canvasToTempFilePath({
+        canvasId: 'posterCanvas',
+        success: (res) => {
+          uni.saveImageToPhotosAlbum({
+            filePath: res.tempFilePath,
+            success: () => {
+              uni.showToast({ title: '已保存到相册', icon: 'success' })
+              this.showPoster = false
+            },
+            fail: () => {
+              uni.showToast({ title: '保存失败', icon: 'none' })
+            }
+          })
+        },
+        fail: () => {
+          uni.showToast({ title: '生成失败', icon: 'none' })
+        }
+      })
+    },
+    async bindFamily() {
+      if (!this.familyCode) {
+        uni.showToast({ title: '请输入家庭邀请码', icon: 'none' })
+        return
+      }
+      try {
+        await bindGuideFamily(this.familyCode)
+        uni.showToast({ title: '绑定成功', icon: 'success' })
+      } catch (e) {
+        uni.showToast({ title: e.message || '绑定失败', icon: 'none' })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container { padding: 30rpx; min-height: 100vh; background: #f5f5f5; }
+.header { text-align: center; padding: 40rpx 0; }
+.title { font-size: 40rpx; font-weight: bold; display: block; margin-bottom: 16rpx; color: #333; }
+.desc { font-size: 28rpx; color: #666; }
+.invite-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 20rpx; padding: 50rpx; text-align: center; margin: 30rpx 0; }
+.invite-code { font-size: 56rpx; font-weight: bold; color: #fff; letter-spacing: 8rpx; margin-bottom: 16rpx; }
+.invite-tip { font-size: 24rpx; color: rgba(255,255,255,0.8); }
+
+.share-section { background: #fff; border-radius: 20rpx; padding: 40rpx; margin: 30rpx 0; text-align: center; }
+.btn-share-large { background: #07c160; color: #fff; border-radius: 60rpx; padding: 24rpx 60rpx; display: inline-flex; align-items: center; justify-content: center; }
+.btn-share-large::after { border: none; }
+.share-icon { font-size: 36rpx; margin-right: 16rpx; }
+.share-text { font-size: 32rpx; font-weight: bold; }
+.share-hint { display: block; margin-top: 20rpx; font-size: 24rpx; color: #999; }
+
+.other-methods { background: #fff; border-radius: 20rpx; padding: 30rpx; margin: 30rpx 0; }
+.method-title { font-size: 28rpx; color: #999; margin-bottom: 20rpx; }
+.method-item { display: flex; align-items: center; padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0; }
+.method-item:last-child { border-bottom: none; }
+.method-icon { font-size: 40rpx; margin-right: 20rpx; }
+.method-text { font-size: 28rpx; color: #333; }
+
+.guide-tip { background: #fff3e0; border-radius: 16rpx; padding: 30rpx; margin: 30rpx 0; }
+.tip-title { font-size: 28rpx; font-weight: bold; color: #ff9800; display: block; margin-bottom: 16rpx; }
+.tip-text { font-size: 24rpx; color: #666; display: block; margin-bottom: 12rpx; }
+.tip-item { font-size: 24rpx; color: #666; display: block; margin-left: 20rpx; line-height: 1.8; }
+
+.section { background: #fff; border-radius: 20rpx; padding: 30rpx; margin: 30rpx 0; }
+.section-title { font-size: 28rpx; font-weight: bold; margin-bottom: 20rpx; }
+.input { border: 1rpx solid #ddd; border-radius: 10rpx; padding: 20rpx; margin-bottom: 20rpx; font-size: 28rpx; }
+
+.poster-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 999; }
+.poster-content { background: #fff; border-radius: 20rpx; padding: 30rpx; text-align: center; }
+.poster-canvas { width: 300px; height: 450px; margin: 0 auto; }
+.btn-save { background: #07c160; color: #fff; border-radius: 40rpx; padding: 20rpx; margin: 20rpx 0; }
+.poster-hint { font-size: 24rpx; color: #999; display: block; }
+</style>

+ 121 - 67
zxyj-frontend/pages/tasks/create-task.vue

@@ -25,16 +25,26 @@
 <input class="input" type="number" v-model="form.points" placeholder="请输入积分 (1-10)" />
 </view>
 
-<view class="form-item">
-<text class="label">分类</text>
-<picker :range="categories" @change="onCategoryChange">
-<view class="picker">
-{{ form.category || '请选择分类' }}
-</view>
-</picker>
-</view>
+          <view class="form-item">
+            <text class="label">分类</text>
+            <picker :range="categories" @change="onCategoryChange">
+              <view class="picker">
+                {{ form.category || '请选择分类' }}
+              </view>
+            </picker>
+          </view>
+
+          <!-- 小游戏选择(当选择"小游戏类"时显示) -->
+          <view class="form-item" v-if="form.category === '小游戏类'">
+            <text class="label">选择小游戏</text>
+            <picker :range="miniGames" range-key="gameName" @change="onMinigameChange">
+              <view class="picker">
+                {{ selectedMinigame ? selectedMinigame.gameName : '请选择小游戏' }}
+              </view>
+            </picker>
+          </view>
 
-<!-- TASK-003: 截止时间设置 -->
+          <!-- TASK-003: 截止时间设置 -->
 <view class="form-item">
 <text class="label">截止时间</text>
 <picker mode="multiSelector" :range="dateTimeRange" :value="dateTimeIndex" @change="onDateTimeChange" @columnchange="onColumnChange">
@@ -70,19 +80,22 @@ import { getChildren, createTask } from '../../utils/api.js'
 export default {
 data() {
 return {
-form: {
-title: '',
-description: '',
-childId: '',
-points: 2,
-category: '',
-deadline: null,
-repeatType: 'none',
-needReview: false
-},
-children: [],
-selectedChild: null,
-categories: ['学习类', '生活类', '运动类', '其他'],
+        form: {
+          title: '',
+          description: '',
+          childId: '',
+          points: 2,
+          category: '',
+          minigameCode: '',
+          deadline: null,
+          repeatType: 'none',
+          needReview: false
+        },
+        children: [],
+        selectedChild: null,
+        categories: ['学习类', '生活类', '运动类', '小游戏类', '其他'],
+        miniGames: [],
+        selectedMinigame: null,
 repeatTypes: [
 { value: 'none', label: '不重复' },
 { value: 'daily', label: '每天' },
@@ -93,19 +106,37 @@ dateTimeIndex: [0, 0, 0, 0],
 deadlineText: ''
 }
 },
-onLoad() {
-this.loadChildren()
-this.initDateTimePicker()
-},
+  onLoad() {
+    this.loadChildren()
+    this.loadMiniGames()
+    this.initDateTimePicker()
+  },
 methods: {
-async loadChildren() {
-try {
-const res = await getChildren()
-this.children = res.data || []
-} catch (e) {
-console.error(e)
-}
-},
+    async loadChildren() {
+      try {
+        const res = await getChildren()
+        this.children = res.data || []
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    async loadMiniGames() {
+      try {
+        const BASE_URL = 'http://localhost:8080'
+        const res = await uni.request({
+          url: `${BASE_URL}/api/tasks/minigame-options`,
+          method: 'POST',
+          header: {
+            'Authorization': `Bearer ${uni.getStorageSync('token')}`
+          }
+        })
+        if (res.data && res.data.code === 200) {
+          this.miniGames = res.data.data || []
+        }
+      } catch (e) {
+        console.error(e)
+      }
+    },
 initDateTimePicker() {
 // 初始化日期时间选择器
 const now = new Date()
@@ -152,9 +183,26 @@ onChildChange(e) {
 this.selectedChild = this.children[e.detail.value]
 this.form.childId = this.selectedChild.id
 },
-onCategoryChange(e) {
-this.form.category = this.categories[e.detail.value]
-},
+    onCategoryChange(e) {
+      this.form.category = this.categories[e.detail.value]
+      // 清空小游戏选择
+      if (this.form.category !== '小游戏类') {
+        this.form.minigameCode = ''
+        this.selectedMinigame = null
+      }
+    },
+    onMinigameChange(e) {
+      this.selectedMinigame = this.miniGames[e.detail.value]
+      this.form.minigameCode = this.selectedMinigame.gameCode
+      // 自动填充任务名称
+      if (!this.form.title) {
+        this.form.title = this.selectedMinigame.gameName
+      }
+      // 自动填充积分
+      if (this.selectedMinigame.defaultPoints) {
+        this.form.points = this.selectedMinigame.defaultPoints
+      }
+    },
 onDateTimeChange(e) {
 const vals = e.detail.value
 this.dateTimeIndex = vals
@@ -190,37 +238,43 @@ this.form.repeatType = selected.value
 onNeedReviewChange(e) {
 this.form.needReview = e.detail.value
 },
-async submit() {
-if (!this.form.title) {
-uni.showToast({ title: '请输入任务名称', icon: 'none' })
-return
-}
-if (!this.form.childId) {
-uni.showToast({ title: '请选择孩子', icon: 'none' })
-return
-}
-if (!this.form.deadline) {
-uni.showToast({ title: '请选择截止时间', icon: 'none' })
-return
-}
+    async submit() {
+      if (!this.form.title) {
+        uni.showToast({ title: '请输入任务名称', icon: 'none' })
+        return
+      }
+      if (!this.form.childId) {
+        uni.showToast({ title: '请选择孩子', icon: 'none' })
+        return
+      }
+      if (!this.form.deadline) {
+        uni.showToast({ title: '请选择截止时间', icon: 'none' })
+        return
+      }
+      // 小游戏类必须选择具体游戏
+      if (this.form.category === '小游戏类' && !this.form.minigameCode) {
+        uni.showToast({ title: '请选择具体小游戏', icon: 'none' })
+        return
+      }
 
-try {
-await createTask({
-title: this.form.title,
-description: this.form.description,
-childId: this.form.childId,
-points: parseInt(this.form.points) || 2,
-category: this.form.category || '其他',
-deadline: this.form.deadline.toISOString(),
-repeatType: this.form.repeatType,
-needReview: this.form.needReview ? 1 : 0
-})
-uni.showToast({ title: '创建成功', icon: 'success' })
-setTimeout(() => uni.navigateBack(), 1500)
-} catch (e) {
-uni.showToast({ title: '创建失败', icon: 'none' })
-}
-}
+      try {
+        await createTask({
+          title: this.form.title,
+          description: this.form.description,
+          childId: this.form.childId,
+          points: parseInt(this.form.points) || 2,
+          category: this.form.category || '其他',
+          minigameCode: this.form.minigameCode,
+          deadline: this.form.deadline.toISOString(),
+          repeatType: this.form.repeatType,
+          needReview: this.form.needReview ? 1 : 0
+        })
+        uni.showToast({ title: '创建成功', icon: 'success' })
+        setTimeout(() => uni.navigateBack(), 1500)
+      } catch (e) {
+        uni.showToast({ title: '创建失败', icon: 'none' })
+      }
+    }
 }
 }
 </script>

+ 15 - 7
zxyj-frontend/pages/tasks/tasks.vue

@@ -10,7 +10,10 @@
         :class="{ completed: task.status === 'completed' }"
       >
         <view class="task-left">
-          <view class="task-title">{{ task.title }}</view>
+          <view class="task-title">
+            {{ task.title }}
+            <text v-if="task.category === '小游戏类'" class="minigame-badge">🎮</text>
+          </view>
           <view class="task-meta">
             <text class="points">+{{ task.points }}分</text>
             <text class="deadline">截止 {{ formatTime(task.deadline) }}</text>
@@ -198,7 +201,7 @@ export default {
         earliestStart: '',
         latestEnd: ''
       },
-      categories: ['学习类', '生活类', '运动类', '其他'],
+      categories: ['学习类', '生活类', '运动类', '小游戏类', '其他'],
       completeTypes: [
         { value: 'checkin', label: '打卡' },
         { value: 'text', label: '文本' },
@@ -473,12 +476,17 @@ export default {
   border-radius: 30rpx;
 }
 
-.completed-badge {
-  color: #52C41A;
-  font-size: 26rpx;
-}
+  .completed-badge {
+    color: #52C41A;
+    font-size: 26rpx;
+  }
+
+  .minigame-badge {
+    font-size: 28rpx;
+    margin-left: 10rpx;
+  }
 
-.add-btn {
+  .add-btn {
   position: fixed;
   bottom: 30rpx;
   left: 30rpx;

+ 17 - 0
zxyj-frontend/utils/api.js

@@ -475,3 +475,20 @@ export const completeMiniGame = (childId, gameCode, completionTime, score) => {
     score 
   })
 }
+
+// 邀请码相关
+export const getInviteCode = () => {
+  return request('/api/family/invite-code', 'GET')
+}
+
+export const generateInviteCode = () => {
+  return request('/api/family/invite-code/generate', 'POST')
+}
+
+export const bindGuideFamily = (inviteCode) => {
+  return request('/api/family/guide-bind', 'POST', { inviteCode })
+}
+
+export const registerWithInviteCode = (data) => {
+  return request('/api/auth/register-with-invite', 'POST', data)
+}