Просмотр исходного кода

resolve merge conflict: use convertFieldList for purchase field save

asus 1 месяц назад
Родитель
Сommit
874011df03

+ 47 - 8
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -2939,7 +2939,9 @@ log.info("已添加template_id列到tasks表");
 
     private void migrateHealthDimensionsTable() {
         try {
-            jdbcTemplate.execute("RENAME TABLE IF EXISTS `health_dimension_score` TO `health_dimension_score_old`");
+            if (tableExists("health_dimension_score")) {
+                jdbcTemplate.execute("RENAME TABLE `health_dimension_score` TO `health_dimension_score_old`");
+            }
         } catch (Exception e) {
             log.warn("health_dimension_score rename skipped: {}", e.getMessage());
         }
@@ -2965,7 +2967,9 @@ log.info("已添加template_id列到tasks表");
         }
 
         try {
-            jdbcTemplate.execute("RENAME TABLE IF EXISTS `health_data_source_record` TO `health_data_source_record_old`");
+            if (tableExists("health_data_source_record")) {
+                jdbcTemplate.execute("RENAME TABLE `health_data_source_record` TO `health_data_source_record_old`");
+            }
         } catch (Exception e) {
             log.warn("health_data_source_record rename skipped: {}", e.getMessage());
         }
@@ -2987,7 +2991,9 @@ log.info("已添加template_id列到tasks表");
         }
 
         try {
-            jdbcTemplate.execute("RENAME TABLE IF EXISTS `health_norm_reference` TO `health_norm_reference_old`");
+            if (tableExists("health_norm_reference")) {
+                jdbcTemplate.execute("RENAME TABLE `health_norm_reference` TO `health_norm_reference_old`");
+            }
         } catch (Exception e) {
             log.warn("health_norm_reference rename skipped: {}", e.getMessage());
         }
@@ -4693,6 +4699,28 @@ try {
         log.info("食品相关表初始化完成");
     }
 
+    private boolean tableExists(String tableName) {
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?",
+                Integer.class, tableName);
+            return count != null && count > 0;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    private boolean columnExists(String table, String column) {
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?",
+                Integer.class, table, column);
+            return count != null && count > 0;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
     private void ensureColumn(String table, String column, String definition) {
         try {
             jdbcTemplate.execute("ALTER TABLE " + table + " ADD COLUMN " + column + " " + definition);
@@ -6042,11 +6070,13 @@ try {
     ensureColumn("points_exchange_records", "family_member_id", "BIGINT COMMENT '家庭成员ID'");
 
     // 迁移133: products表列对齐 — rename min_quantity→purchase_count_threshold, add missing columns
-    try {
-        jdbcTemplate.execute("ALTER TABLE products CHANGE COLUMN min_quantity purchase_count_threshold INT DEFAULT 0 COMMENT '最小购买数量限制'");
-        log.info("已重命名products.min_quantity→purchase_count_threshold");
-    } catch (Exception e) {
-        log.warn("重命名products列可能已处理: {}", e.getMessage());
+    if (columnExists("products", "min_quantity")) {
+        try {
+            jdbcTemplate.execute("ALTER TABLE products CHANGE COLUMN min_quantity purchase_count_threshold INT DEFAULT 0 COMMENT '最小购买数量限制'");
+            log.info("已重命名products.min_quantity→purchase_count_threshold");
+        } catch (Exception e) {
+            log.warn("重命名products列失败(可能已处理): {}", e.getMessage());
+        }
     }
     ensureColumn("products", "recommendation_tags", "VARCHAR(500) DEFAULT NULL COMMENT '推荐标签'");
     ensureColumn("products", "repurchase_interval_days", "INT DEFAULT 30 COMMENT '复购间隔天数'");
@@ -8304,5 +8334,14 @@ private void runMigration100() {
 
 		// 迁移194: family_join_requests 添加 family_role 列(保存申请人角色)
 		ensureColumn("family_join_requests", "family_role", "VARCHAR(20) DEFAULT NULL COMMENT '申请人在家庭中的角色:爸爸/妈妈/爷爷/奶奶等'");
+
+		// 迁移195: tasks 表新增 accept_deadline 列 + 扩展 status 枚举(支持 assigned/in_progress/rejected)
+		ensureColumn("tasks", "accept_deadline", "DATETIME DEFAULT NULL COMMENT '接受截止时间(分配给其他家庭成员的任务)'");
+		try {
+			jdbcTemplate.execute("ALTER TABLE tasks MODIFY COLUMN `status` ENUM('pending','assigned','in_progress','completed','rejected','overdue','cancelled') DEFAULT 'pending' COMMENT '状态: pending=待审核 assigned=待接受 in_progress=进行中 completed=已完成 rejected=已拒绝 overdue=已过期 cancelled=已取消'");
+			log.info("已更新tasks.status枚举值");
+		} catch (Exception e) {
+			log.warn("tasks.status枚举修改失败(可能已修改): " + e.getMessage());
+		}
 	}
 }

+ 27 - 7
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminAuthController.java

@@ -2,6 +2,7 @@ package com.etotem.cfc.controller.admin;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.common.Result;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import com.etotem.cfc.config.JwtConfig;
 import com.etotem.cfc.dto.AdminLoginDTO;
 import com.etotem.cfc.dto.SendCodeDTO;
@@ -27,6 +28,8 @@ import java.util.Map;
 @RequestMapping("/api/admin-auth")
 public class AdminAuthController {
 
+    private static final BCryptPasswordEncoder BC_PASSWORD_ENCODER = new BCryptPasswordEncoder();
+
     @Resource
     private UserMapper userMapper;
 
@@ -268,13 +271,8 @@ public class AdminAuthController {
             return Result.error("用户不存在");
         }
 
-        // 验证密码(兼容两种hash方式)
-        java.nio.charset.Charset utf8 = java.nio.charset.StandardCharsets.UTF_8;
-        String hash1 = org.springframework.util.DigestUtils.md5DigestAsHex(
-                ("xzyj_salt_" + dto.getPassword()).getBytes(utf8));
-        String hash2 = org.springframework.util.DigestUtils.md5DigestAsHex(
-                ("xzyj_admin_salt_" + dto.getPassword()).getBytes(utf8));
-        if (!hash1.equals(user.getPassword()) && !hash2.equals(user.getPassword())) {
+        // 验证密码(兼容 MD5 与 BCrypt 两种哈希方式)
+        if (!verifyLoginPassword(dto.getPassword(), user.getPassword())) {
             loginAttemptService.recordFailure(lockKey);
             int remaining = loginAttemptService.getRemainingAttempts(lockKey);
             return Result.error("密码错误,剩余" + remaining + "次尝试机会");
@@ -331,4 +329,26 @@ public class AdminAuthController {
         }
         return ip;
     }
+
+    /**
+     * 验证登录密码:兼容旧 MD5 哈希与新 BCrypt 哈希
+     * MD5 格式:xzyj_salt_{password} 或 xzyj_admin_salt_{password}
+     * BCrypt 格式:$2a$...$...
+     */
+    private boolean verifyLoginPassword(String rawPassword, String storedHash) {
+        if (storedHash == null || storedHash.isEmpty()) {
+            return false;
+        }
+        // BCrypt 哈希
+        if (storedHash.startsWith("$2a$") || storedHash.startsWith("$2b$")) {
+            return BC_PASSWORD_ENCODER.matches(rawPassword, storedHash);
+        }
+        // 旧 MD5 哈希(兼容两种盐值)
+        java.nio.charset.Charset utf8 = java.nio.charset.StandardCharsets.UTF_8;
+        String hash1 = org.springframework.util.DigestUtils.md5DigestAsHex(
+                ("xzyj_salt_" + rawPassword).getBytes(utf8));
+        String hash2 = org.springframework.util.DigestUtils.md5DigestAsHex(
+                ("xzyj_admin_salt_" + rawPassword).getBytes(utf8));
+        return hash1.equals(storedHash) || hash2.equals(storedHash);
+    }
 }

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java

@@ -715,7 +715,7 @@ public class AdminController {
             admin.setNickname(user.getNickname());
             admin.setRole("teacher");
             admin.setStatus(1);
-            admin.setPassword(DigestUtils.md5DigestAsHex(("xzyj_admin_salt_" + user.getPhone()).getBytes()));
+            admin.setPassword(new org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder().encode(user.getPhone()));
             admin.setCreatedAt(new Date());
             admin.setUpdatedAt(new Date());
             adminMapper.insert(admin);

+ 28 - 11
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminPurchaseFieldController.java

@@ -8,8 +8,6 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import javax.annotation.Resource;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
@@ -23,9 +21,6 @@ public class AdminPurchaseFieldController {
     @Resource
     private ProductPurchaseFieldService purchaseFieldService;
 
-    @Resource
-    private ObjectMapper objectMapper;
-
     @PostMapping("/predefined")
     public Result<List<Map<String, String>>> predefined() {
         List<Map<String, String>> list = new ArrayList<>();
@@ -47,11 +42,33 @@ public class AdminPurchaseFieldController {
     @PostMapping("/save")
     public Result<Void> save(@RequestBody Map<String, Object> params) {
         Long productId = Long.parseLong(params.get("productId").toString());
-        List<?> rawFields = (List<?>) params.get("fields");
-        List<ProductPurchaseField> fields = rawFields != null
-                ? objectMapper.convertValue(rawFields,
-                        objectMapper.getTypeFactory().constructCollectionType(List.class, ProductPurchaseField.class))
-                : new ArrayList<>();
+List<ProductPurchaseField> fields = convertFieldList(params.get("fields"));
         return purchaseFieldService.save(productId, fields);
     }
-}
+
+    @SuppressWarnings("unchecked")
+    private List<ProductPurchaseField> convertFieldList(Object raw) {
+        if (raw == null || !(raw instanceof List)) {
+            return new ArrayList<>();
+        }
+        List<Map<String, Object>> mapList = (List<Map<String, Object>>) raw;
+        List<ProductPurchaseField> list = new ArrayList<>();
+        for (Map<String, Object> m : mapList) {
+            ProductPurchaseField f = new ProductPurchaseField();
+            f.setFieldKey(strVal(m.get("fieldKey")));
+            f.setFieldName(strVal(m.get("fieldName")));
+            f.setIsRequired(intVal(m.get("isRequired")));
+            f.setSortOrder(intVal(m.get("sortOrder")));
+            list.add(f);
+        }
+        return list;
+    }
+
+    private String strVal(Object v) {
+        return v != null ? v.toString() : null;
+    }
+
+    private Integer intVal(Object v) {
+        return v != null ? Integer.parseInt(v.toString()) : null;
+    }
+}

+ 3 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyInviteController.java

@@ -268,7 +268,9 @@ public class FamilyInviteController {
         }
 
         String qrBase64;
-        qrBase64 = wechatService.generateWxacode("invite=" + token, "pages/invite/join");
+        // scene 必须是合法 JSON 字符串(getwxacodeunlimit 限制 64 字节,token 32 字符 → ~46 字节安全)
+        String scene = "{\"t\":\"" + token + "\"}";
+        qrBase64 = wechatService.generateWxacode(scene, "pages/invite/join");
 
         Family family = familyMapper.selectById(user.getFamilyId());
 

+ 38 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/task/TaskController.java

@@ -175,4 +175,42 @@ public class TaskController {
             return Result.error("批量操作失败: " + e.getMessage());
         }
     }
+
+    @Operation(summary = "获取我收到的待接受任务列表")
+    @PostMapping("/my-pending")
+    public Result<List<Task>> getMyPendingTasks(@RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId,
+                                                @RequestBody Map<String, Object> params) {
+        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        if (memberId == null) {
+            return Result.error("memberId不能为空");
+        }
+        List<Task> tasks = taskService.getMyPendingTasks(memberId);
+        return Result.success(tasks);
+    }
+
+    @Operation(summary = "接受任务")
+    @PostMapping("/accept")
+    public Result<Boolean> acceptTask(@PathVariable Long id,
+                                      @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId,
+                                      @RequestBody Map<String, Object> params) {
+        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        if (memberId == null) {
+            return Result.error("memberId不能为空");
+        }
+        boolean success = taskService.acceptTask(id, memberId);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "拒绝任务")
+    @PostMapping("/reject/{id}")
+    public Result<Boolean> rejectTask(@PathVariable Long id,
+                                      @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId,
+                                      @RequestBody Map<String, Object> params) {
+        Long memberId = params.get("memberId") != null ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
+        if (memberId == null) {
+            return Result.error("memberId不能为空");
+        }
+        boolean success = taskService.rejectTask(id, memberId);
+        return Result.success(success);
+    }
 }

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Task.java

@@ -89,6 +89,9 @@ private Date earliestStart;
 // 最晚结束时间
 private Date latestEnd;
 
+/** 接受截止时间(分配给其他家庭成员的任务使用) */
+private Date acceptDeadline;
+
     // 小游戏代码(当category为'小游戏类'时使用): schulte/1a2b/sudoku
     private String minigameCode;
 

+ 115 - 5
cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java

@@ -48,6 +48,9 @@ public class TaskService implements TaskServiceInterface {
     @Resource
     private OnboardingService onboardingService;
 
+    @Resource
+    private FamilyRelationshipScoreMapper familyRelationshipScoreMapper;
+
     @Resource
     private GrowthTaskService growthTaskService;
 
@@ -59,10 +62,9 @@ public class TaskService implements TaskServiceInterface {
     public Long createTask(Long userId, CreateTaskDTO dto) {
         User currentUser = userService.getUserInfo(userId);
         
-        // 处理执行者类型 - 支持孩子任务和家长任务
+        // 处理执行者类型 - 支持孩子任务、家长任务、家庭成员任务
         String executorType = dto.getExecutorType();
         if (executorType == null || executorType.isEmpty()) {
-            // 兼容旧版:默认给孩子创建任务
             executorType = "child";
         }
         
@@ -109,11 +111,14 @@ public class TaskService implements TaskServiceInterface {
         task.setExecutorType(executorType);
         
         if ("child".equals(executorType)) {
-            // 给孩子安排的任务
             task.setExecutorId(memberId);
             task.setChildId(memberId);
+        } else if ("member".equals(executorType)) {
+            task.setExecutorId(memberId);
+            task.setChildId(null);
+            task.setStatus("assigned");
+            task.setAcceptDeadline(dto.getDeadline());
         } else {
-            // 给家长安排的任务
             task.setExecutorId(dto.getExecutorId() != null ? dto.getExecutorId() : userId);
             task.setChildId(null);
         }
@@ -189,7 +194,9 @@ public class TaskService implements TaskServiceInterface {
         }
 
 task.setReviewByCategory(dto.getReviewByCategory() != null ? dto.getReviewByCategory() : 0);
-        task.setStatus("pending");
+        if (!"assigned".equals(task.getStatus())) {
+            task.setStatus("pending");
+        }
         task.setCreatedAt(new Date());
         task.setUpdatedAt(new Date());
 
@@ -893,4 +900,107 @@ return 0; // 达到每日扣分上限
         wrapper.orderByDesc(Task::getCreatedAt);
         return taskMapper.selectPage(new Page<>(page, size), wrapper);
     }
+
+    /**
+     * 获取当前用户收到的待接受任务列表(executor_type='member' AND status='assigned')
+     */
+    public List<Task> getMyPendingTasks(Long memberId) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+        Date startOfDay = calendar.getTime();
+
+        calendar.add(Calendar.DAY_OF_MONTH, 1);
+        Date endOfDay = calendar.getTime();
+
+        return taskMapper.selectList(new LambdaQueryWrapper<Task>()
+                .eq(Task::getExecutorType, "member")
+                .eq(Task::getExecutorId, memberId)
+                .eq(Task::getStatus, "assigned")
+                .ge(Task::getDeadline, startOfDay)
+                .lt(Task::getDeadline, endOfDay)
+                .or()
+                .and(w -> w.eq(Task::getExecutorType, "member")
+                        .eq(Task::getExecutorId, memberId)
+                        .eq(Task::getStatus, "assigned")
+                        .isNull(Task::getDeadline))
+                .orderByAsc(Task::getDeadline));
+    }
+
+    /**
+     * 接受任务:status 从 assigned 改为 in_progress
+     */
+    @Transactional
+    public boolean acceptTask(Long taskId, Long memberId) {
+        Task task = taskMapper.selectById(taskId);
+        if (task == null || !task.getExecutorId().equals(memberId) || !"assigned".equals(task.getStatus())) {
+            return false;
+        }
+        task.setStatus("in_progress");
+        task.setAcceptDeadline(null);
+        task.setUpdatedAt(new Date());
+        taskMapper.updateById(task);
+        return true;
+    }
+
+    /**
+     * 拒绝任务:status 改为 rejected,调整 creator→assignee 和 assignee→creator 的关系分数
+     */
+    @Transactional
+    public boolean rejectTask(Long taskId, Long memberId) {
+        Task task = taskMapper.selectById(taskId);
+        if (task == null || !task.getExecutorId().equals(memberId) || !"assigned".equals(task.getStatus())) {
+            return false;
+        }
+        task.setStatus("rejected");
+        task.setUpdatedAt(new Date());
+        taskMapper.updateById(task);
+
+        Long creatorId = task.getCreatorId();
+        Long assigneeId = memberId;
+        Long familyId = task.getFamilyId();
+        if (creatorId == null || assigneeId == null || familyId == null) return true;
+
+        int trustDelta = -1;
+        int intimacyDelta = -1;
+        int commDelta = -1;
+
+        adjustRelationshipScore(familyId, creatorId, assigneeId, trustDelta, intimacyDelta, commDelta);
+        adjustRelationshipScore(familyId, assigneeId, creatorId, 0, 0, 1);
+        return true;
+    }
+
+    private void adjustRelationshipScore(Long familyId, Long fromId, Long toId, int trustDelta, int intimacyDelta, int commDelta) {
+        FamilyRelationshipScore score = familyRelationshipScoreMapper.selectOne(
+                new LambdaQueryWrapper<FamilyRelationshipScore>()
+                        .eq(FamilyRelationshipScore::getFamilyId, familyId)
+                        .eq(FamilyRelationshipScore::getFromMemberId, fromId)
+                        .eq(FamilyRelationshipScore::getToMemberId, toId));
+        if (score == null) {
+            FamilyMember from = familyMemberMapper.selectById(fromId);
+            score = new FamilyRelationshipScore();
+            score.setFamilyId(familyId);
+            score.setFromMemberId(fromId);
+            score.setToMemberId(toId);
+            score.setTrustScore(from != null && from.getTrustScore() != null ? from.getTrustScore() : new java.math.BigDecimal("60.00"));
+            score.setIntimacyScore(from != null && from.getIntimacyScore() != null ? from.getIntimacyScore() : new java.math.BigDecimal("60.00"));
+            score.setCommunicationScore(from != null && from.getCommunicationScore() != null ? from.getCommunicationScore() : new java.math.BigDecimal("60.00"));
+            try {
+                familyRelationshipScoreMapper.insert(score);
+            } catch (Exception e) {
+                score = familyRelationshipScoreMapper.selectOne(
+                        new LambdaQueryWrapper<FamilyRelationshipScore>()
+                                .eq(FamilyRelationshipScore::getFamilyId, familyId)
+                                .eq(FamilyRelationshipScore::getFromMemberId, fromId)
+                                .eq(FamilyRelationshipScore::getToMemberId, toId));
+            }
+        }
+        if (score == null) return;
+        if (trustDelta != 0) score.setTrustScore(score.getTrustScore().add(java.math.BigDecimal.valueOf(trustDelta)));
+        if (intimacyDelta != 0) score.setIntimacyScore(score.getIntimacyScore().add(java.math.BigDecimal.valueOf(intimacyDelta)));
+        if (commDelta != 0) score.setCommunicationScore(score.getCommunicationScore().add(java.math.BigDecimal.valueOf(commDelta)));
+        familyRelationshipScoreMapper.updateById(score);
+    }
 }

+ 5 - 3
cfc-backend/src/main/resources/schema.sql

@@ -69,17 +69,18 @@ CREATE TABLE IF NOT EXISTS tasks (
     family_id BIGINT NOT NULL,
     creator_id BIGINT NOT NULL,
     child_id BIGINT COMMENT '执行者ID(child任务时必填,parent任务时可空)',
-    executor_type VARCHAR(20) COMMENT '执行者类型: child/parent',
+    executor_type VARCHAR(20) COMMENT '执行者类型: child/parent/member',
     executor_id BIGINT COMMENT '执行者ID',
     title VARCHAR(200) NOT NULL,
     description TEXT,
     points TINYINT DEFAULT 2 COMMENT '任务积分1-10',
     deadline DATETIME NOT NULL,
+    accept_deadline DATETIME DEFAULT NULL COMMENT '接受截止时间(分配给其他家庭成员的任务)',
     repeat_type ENUM('none', 'daily', 'weekly') DEFAULT 'none',
     category VARCHAR(32),
     need_review TINYINT DEFAULT 0 COMMENT '是否需要审核',
     review_by_category TINYINT DEFAULT 0 COMMENT '是否按分类审核',
-    status ENUM('pending', 'completed', 'overdue', 'cancelled') DEFAULT 'pending',
+    status ENUM('pending', 'assigned', 'in_progress', 'completed', 'rejected', 'overdue', 'cancelled') DEFAULT 'pending',
     completed_at DATETIME,
     bonus_rule JSON COMMENT '积分奖励规则',
     points_expire_days INT DEFAULT 0 COMMENT '积分有效期天数',
@@ -91,7 +92,8 @@ CREATE TABLE IF NOT EXISTS tasks (
     INDEX idx_status (status),
     INDEX idx_category (category),
     INDEX idx_executor (executor_type, executor_id),
-    INDEX idx_dimension_code (dimension_code)
+    INDEX idx_dimension_code (dimension_code),
+    INDEX idx_executor_assigned (executor_type, executor_id, status)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 -- 积分流水表

+ 14 - 0
cfc-frontend/components/ProfileMenu.vue

@@ -33,6 +33,14 @@
       <!-- ===== 服务 ===== -->
       <view class="menu-group" v-if="showServices">
         <view class="menu-group-title">── 服务 ──</view>
+        <view class="menu-item" @click="goToShop">
+          <text>🛍️ 商城</text>
+          <text class="arrow">›</text>
+        </view>
+        <view class="menu-item" @click="goToCart">
+          <text>🛒 购物车</text>
+          <text class="arrow">›</text>
+        </view>
         <view class="menu-item" @click="goToMembership">
           <text>👑 会员中心</text>
           <text class="arrow">›</text>
@@ -172,6 +180,12 @@ export default {
         uni.showToast({ title: '操作失败', icon: 'none' })
       }
     },
+    goToShop() {
+      uni.navigateTo({ url: '/pages/shop/index/index' })
+    },
+    goToCart() {
+      uni.navigateTo({ url: '/pages/shop/cart/cart' })
+    },
     goToMembership() {
       uni.navigateTo({ url: '/pages/membership/index' })
     },

+ 11 - 2
cfc-frontend/pages/invite/join.vue

@@ -171,13 +171,22 @@ export default {
     }
   },
   onLoad(query) {
-    // token:优先从 query 参数,其次从场景值(invite=xxx)
+    // token:优先从 query 参数,其次从场景值
     var token = query.token || query.inviteToken || ''
     if (!token) {
       var scene = query.scene
       if (scene) {
         var sceneDecoded = decodeURIComponent(scene)
-        if (sceneDecoded.indexOf('invite=') === 0) {
+        // 新格式:JSON 场景值 {"t":"TOKEN"}
+        if (sceneDecoded.indexOf('{') === 0) {
+          try {
+            var sceneObj = JSON.parse(sceneDecoded)
+            token = sceneObj.t || sceneObj.token || ''
+          } catch (e) {
+            token = ''
+          }
+        } else if (sceneDecoded.indexOf('invite=') === 0) {
+          // 旧格式兼容
           token = sceneDecoded.substring(7)
         }
       }

+ 3 - 9
cfc-frontend/pages/profile-extra/family-members.vue

@@ -31,9 +31,6 @@
       <button class="empty-invite-btn" @click="onInvite">📨 邀请家人</button>
     </view>
 
-    <!-- ===== 分享邀请卡海报 ===== -->
-    <view v-else class="empty-tip">暂无家庭成员,正在为您创建家庭...</view>
-
     <!-- ===== 成员详情弹窗 ===== -->
     <view class="modal-mask" v-if="showMemberDetailModal" @click="closeMemberDetail">
       <view class="member-detail-card" @click.stop>
@@ -108,7 +105,6 @@ import {
   listFamilyMembers,
   switchToFamilyMember,
   kickFamilyMemberById,
-  generateFamilyInviteQrCode,
   generateFamilyInvite,
   getFamilyMembers,
   updateFamilyMember
@@ -280,26 +276,24 @@ export default {
         var res = await generateFamilyInvite()
         if (res.code === 200 && res.data) {
           var token = res.data.token
-          var qrCode = res.data.qrCodeBase64
-          // 分享小程序码
           uni.share({
             provider: 'weixin',
             scene: 'WXSceneSession',
             type: 5,
-            imageUrl: qrCode ? qrCode.replace('data:image/png;base64,', '') : '',
+            title: '加入「' + (this.familyName || '我的家庭') + '」',
             miniProgram: {
               id: '',
               path: 'pages/invite/join?token=' + token,
               type: 0,
               webUrl: 'https://cfc.etotem.com.cn/invite/join?token=' + token
             },
-            success: function(shareRes) {
+            success: function() {
               uni.hideLoading()
               uni.showToast({ title: '分享成功', icon: 'success' })
             },
             fail: function(err) {
               uni.hideLoading()
-              // 如果分享失败,复制链接
+              if (err.errMsg.indexOf('cancel') >= 0) return
               uni.setClipboardData({
                 data: 'https://cfc.etotem.com.cn/invite/join?token=' + token,
                 success: function() {

+ 721 - 772
cfc-frontend/pages/tasks/tasks.vue

@@ -1,772 +1,721 @@
-<template>
-  <PlayfulCard elevation="1">
-  <view class="container">
-    <!-- 批量操作栏 -->
-    <view class="batch-bar" v-if="batchMode">
-      <checkbox :checked="isAllSelected" @click="toggleSelectAll" />
-      <text class="batch-label">全选</text>
-      <view class="batch-actions">
-        <button class="batch-btn complete" @click="batchComplete" :disabled="selectedTasks.length === 0">批量完成</button>
-        <button class="batch-btn delete" @click="batchDelete" :disabled="selectedTasks.length === 0">批量删除</button>
-      </view>
-      <button class="batch-exit" @click="exitBatchMode">退出</button>
-    </view>
-
-    <!-- 正常模式顶部栏 -->
-    <view class="header-bar" v-else>
-      <text class="header-title">今日任务</text>
-      <button class="btn-batch" @click="enterBatchMode" v-if="tasks.length > 0">批量操作</button>
-    </view>
-
-    <!-- 任务列表 -->
-    <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', selected: selectedTasks.includes(task.id) }"
-      >
-        <view class="task-checkbox" v-if="batchMode" @click="toggleTaskSelect(task.id)">
-          <checkbox :checked="selectedTasks.includes(task.id)" />
-        </view>
-        <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 class="task-right" v-if="!batchMode">
-          <button 
-            v-if="task.status === 'pending'" 
-            class="btn-complete" 
-            @click="isParent ? completeParentTask(task.id) : completeTask(task.id)"
-          >
-            完成
-          </button>
-          <view v-else class="completed-badge">已完成</view>
-        </view>
-      </view>
-    </view>
-
-    <view class="empty" v-else>
-      <text>暂无今日任务</text>
-    </view>
-
-    <!-- 家长模式:添加任务按钮 -->
-    <button 
-      v-if="isParent && !batchMode" 
-      class="btn-primary add-btn" 
-      @click="showAddModal = true"
-    >
-      + 添加任务
-    </button>
-
-    <!-- 添加任务弹窗 -->
-    <view class="modal-mask" v-if="showAddModal" @click="showAddModal = false">
-      <view class="modal modal-large" @click.stop>
-        <view class="modal-title">创建任务</view>
-        
-        <!-- 任务模板选择 -->
-        <view class="form-item">
-          <view class="form-label">从模板选择(可选)</view>
-          <picker mode="selector" :range="taskTemplates" range-key="title" @change="onTemplateChange">
-            <view class="picker">{{ selectedTemplate ? selectedTemplate.title : '选择任务模板' }}</view>
-          </picker>
-        </view>
-        
-        <!-- 必填:标题 -->
-        <view class="form-item">
-          <view class="form-label">任务标题 <text class="required">*</text></view>
-          <input v-model="newTask.title" placeholder="请输入任务标题" />
-        </view>
-        
-        <!-- 内容 -->
-        <view class="form-item">
-          <view class="form-label">任务内容</view>
-          <textarea v-model="newTask.description" placeholder="请输入任务描述" class="textarea" />
-        </view>
-        
-        <!-- 类型 -->
-        <view class="form-item">
-          <view class="form-label">类型 <text class="required">*</text></view>
-          <picker mode="selector" :range="categories" @change="onCategoryChange">
-            <view class="picker">{{ newTask.category || '请选择类型' }}</view>
-          </picker>
-        </view>
-        
-        <!-- 完成方式 -->
-        <view class="form-item">
-          <view class="form-label">完成方式 <text class="required">*</text></view>
-          <checkbox-group @change="onCompleteTypeChange">
-            <label v-for="type in completeTypes" :key="type.value">
-              <checkbox :value="type.value" :checked="newTask.completeTypes.includes(type.value)" />
-              <text>{{ type.label }}</text>
-            </label>
-          </checkbox-group>
-        </view>
-        
-        <!-- 积分和时长 -->
-        <view class="form-row">
-          <view class="form-item half">
-            <view class="form-label">积分 <text class="required">*</text></view>
-            <input v-model="newTask.points" type="number" placeholder="1-10" />
-          </view>
-          <view class="form-item half">
-            <view class="form-label">时长(分钟)</view>
-            <input v-model="newTask.duration" type="number" placeholder="时长" />
-          </view>
-        </view>
-        
-        <!-- 完成孩子(多选) -->
-        <view class="form-item">
-          <view class="form-label">完成孩子 <text class="required">*</text></view>
-          <checkbox-group @change="onChildSelectChange">
-            <label v-for="child in children" :key="child.id">
-              <checkbox :value="child.id" :checked="newTask.memberIds.includes(child.id)" />
-              <text>{{ child.nickname }}</text>
-            </label>
-          </checkbox-group>
-        </view>
-        
-        <!-- 任务性质 -->
-        <view class="form-item">
-          <view class="form-label">任务性质 <text class="required">*</text></view>
-          <radio-group @change="onTaskTypeChange">
-            <label><radio value="onetime" :checked="newTask.taskType === 'onetime'" />一次性</label>
-            <label><radio value="recurring" :checked="newTask.taskType === 'recurring'" />循环任务</label>
-          </radio-group>
-        </view>
-        
-        <!-- 循环任务:频率和次数 -->
-        <view class="form-item" v-if="newTask.taskType === 'recurring'">
-          <view class="form-label">频率</view>
-          <picker mode="selector" :range="frequencies" @change="onFrequencyChange">
-            <view class="picker">{{ newTask.frequency || '请选择频率' }}</view>
-          </picker>
-        </view>
-        <view class="form-item" v-if="newTask.taskType === 'recurring'">
-          <view class="form-label">周期内最多完成次数</view>
-          <input v-model="newTask.maxFrequency" type="number" placeholder="超过次数不记积分" />
-        </view>
-        
-        <!-- 审核设置 -->
-        <view class="form-item">
-          <view class="form-label">需要审核</view>
-          <switch :checked="newTask.needReview === 1" @change="onNeedReviewChange" />
-        </view>
-        <view class="form-item" v-if="newTask.needReview === 1">
-          <view class="form-label">审核人</view>
-          <picker mode="selector" :range="reviewTypes" range-key="label" @change="onReviewTypeChange">
-            <view class="picker">{{ getReviewTypeLabel(newTask.reviewType) }}</view>
-          </picker>
-        </view>
-        
-        <!-- 非必填:时间 -->
-        <view class="form-item">
-          <view class="form-label">最早开始时间</view>
-          <picker mode="datetime" :value="newTask.earliestStart" @change="onEarliestStartChange">
-            <view class="picker">{{ newTask.earliestStart || '选择时间(不填表示不限时)' }}</view>
-          </picker>
-        </view>
-        <view class="form-item">
-          <view class="form-label">最晚结束时间</view>
-          <picker mode="datetime" :value="newTask.latestEnd" @change="onLatestEndChange">
-            <view class="picker">{{ newTask.latestEnd || '选择时间(不填表示不限时)' }}</view>
-          </picker>
-        </view>
-        
-        <view class="modal-btns">
-          <button @click="showAddModal = false">取消</button>
-          <button class="btn-primary" @click="createTask">创建任务</button>
-        </view>
-      </view>
-    </view>
-  </view>
-  </PlayfulCard>
-</template>
-
-<script>
-import { getTodayTasks, completeTask as completeTaskApi, createTask as createTaskApi, getChildren, getTodayParentTasks, completeParentTask as completeParentTaskApi, batchCompleteTasks, batchDeleteTasks } from '../../utils/api.js'
-import { parseDate } from '../../utils/format.js'
-import PlayfulCard from '@/components/PlayfulCard.vue'
-
-export default {
-  components: { PlayfulCard },
-  data() {
-    return {
-      tasks: [],
-      isParent: false,
-      currentRole: 'parent',  // 当前角色
-      currentChildId: '',
-      children: [],  // 孩子列表
-      taskTemplates: [],  // 任务模板
-      selectedTemplate: null,
-      showAddModal: false,
-      batchMode: false,
-      selectedTasks: [],
-      newTask: {
-        title: '',
-        description: '',
-        category: '',
-        points: 2,
-        duration: '',
-        memberIds: [],
-        taskType: 'onetime',
-        frequency: '',
-        maxFrequency: '',
-        completeTypes: ['checkin'],
-        needReview: 0,
-        reviewType: 'parent',
-        earliestStart: '',
-        latestEnd: ''
-      },
-      categories: ['学习类', '生活类', '运动类', '小游戏类', '其他'],
-      completeTypes: [
-        { value: 'checkin', label: '打卡' },
-        { value: 'text', label: '文本' },
-        { value: 'audio', label: '语音' },
-        { value: 'image', label: '图片' },
-        { value: 'video', label: '视频' }
-      ],
-      frequencies: ['每天', '每周', '每月'],
-      reviewTypes: [
-        { value: 'creator', label: '创建人' },
-        { value: 'parent', label: '家长' },
-        { value: 'teacher', label: '成长规划师' },
-        { value: 'ai', label: 'AI自动' }
-      ]
-    }
-  },
-  onShow() {
-    // 规划师角色跳转到家庭管理页
-    const role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
-    if (role === 'teacher') {
-      uni.redirectTo({ url: '/pages/teacher/families' })
-      return
-    }
-    // 加载任务数据
-    this.loadParentTasks()
-  },
-  methods: {
-    async loadParentTasks() {
-      try {
-        const currentUserId = uni.getStorageSync('userId')
-        const res = await getTodayParentTasks()
-        this.tasks = (res.data || []).slice(0, 5)
-      } catch (e) {
-        console.error('加载任务失败', e)
-      }
-    },
-    async loadChildren() {
-      try {
-        const res = await getChildren()
-        this.children = res.data || []
-        // 默认选中第一个孩子
-        if (this.children.length > 0) {
-          this.newTask.memberIds = [this.children[0].id]
-        }
-      } catch (e) {
-        console.error('加载孩子失败', e)
-      }
-    },
-    // 加载家长任务(分配给家长的任务)
-    async loadParentTasks() {
-      try {
-        const res = await getTodayParentTasks()
-        this.tasks = res.data || []
-      } catch (e) {
-        console.error('加载家长任务失败', e)
-      }
-    },
-    async loadTasks() {
-      try {
-        const childrenRes = await getChildren()
-        const children = childrenRes.data
-        if (children && children.length > 0) {
-          this.currentChildId = children[0].id
-          const res = await getTodayTasks(this.currentChildId)
-          this.tasks = res.data || []
-        }
-      } catch (e) {
-        console.error('加载任务失败', e)
-      }
-    },
-    // 完成任务(孩子模式)
-    async completeTask(taskId) {
-      try {
-        const res = await completeTaskApi(taskId, this.currentChildId, '')
-        uni.showToast({
-          title: `获得 ${res.data.pointsEarned} 积分`,
-          icon: 'success'
-        })
-        this.loadTasks()
-      } catch (e) {
-        console.error('完成任务失败', e)
-      }
-    },
-    // 完成任务(家长模式)
-    async completeParentTask(taskId) {
-      try {
-        const res = await completeParentTaskApi(taskId)
-        uni.showToast({
-          title: `获得 ${res.data.pointsEarned} 积分`,
-          icon: 'success'
-        })
-        this.loadParentTasks()
-      } catch (e) {
-        console.error('完成任务失败', e)
-      }
-    },
-    // 模板选择
-    onTemplateChange(e) {
-      if (e.detail.value >= 0) {
-        this.selectedTemplate = this.taskTemplates[e.detail.value]
-        this.applyTemplate(this.selectedTemplate)
-      }
-    },
-    applyTemplate(template) {
-      this.newTask.title = template.title || ''
-      this.newTask.description = template.description || ''
-      this.newTask.category = template.category || ''
-      this.newTask.points = template.points || 2
-      this.newTask.duration = template.duration || ''
-      this.newTask.completeTypes = template.completeTypes ? template.completeTypes.split(',') : ['checkin']
-      this.newTask.needReview = template.needReview || 0
-      this.newTask.reviewType = template.reviewType || 'parent'
-    },
-    // 类型选择
-    onCategoryChange(e) {
-      this.newTask.category = this.categories[e.detail.value]
-    },
-    // 完成方式选择
-    onCompleteTypeChange(e) {
-      this.newTask.completeTypes = e.detail.value
-    },
-    // 孩子多选
-    onChildSelectChange(e) {
-      this.newTask.memberIds = e.detail.value.map(id => parseInt(id))
-    },
-    // 任务性质
-    onTaskTypeChange(e) {
-      this.newTask.taskType = e.detail.value
-    },
-    // 频率选择
-    onFrequencyChange(e) {
-      const freq = this.frequencies[e.detail.value]
-      this.newTask.frequency = freq === '每天' ? 'daily' : (freq === '每周' ? 'weekly' : 'monthly')
-    },
-    // 是否需要审核
-    onNeedReviewChange(e) {
-      this.newTask.needReview = e.detail.value ? 1 : 0
-    },
-    // 审核人选择
-    onReviewTypeChange(e) {
-      this.newTask.reviewType = this.reviewTypes[e.detail.value].value
-    },
-    getReviewTypeLabel(value) {
-      const found = this.reviewTypes.find(r => r.value === value)
-      return found ? found.label : '请选择审核人'
-    },
-    // 时间选择
-    onEarliestStartChange(e) {
-      this.newTask.earliestStart = e.detail.value
-    },
-    onLatestEndChange(e) {
-      this.newTask.latestEnd = e.detail.value
-    },
-    async createTask() {
-      // 验证必填
-      if (!this.newTask.title) {
-        uni.showToast({ title: '请输入任务标题', icon: 'none' })
-        return
-      }
-      if (!this.newTask.category) {
-        uni.showToast({ title: '请选择类型', icon: 'none' })
-        return
-      }
-      if (!this.newTask.completeTypes || this.newTask.completeTypes.length === 0) {
-        uni.showToast({ title: '请选择完成方式', icon: 'none' })
-        return
-      }
-      if (!this.newTask.memberIds || this.newTask.memberIds.length === 0) {
-        uni.showToast({ title: '请选择完成孩子', icon: 'none' })
-        return
-      }
-
-      try {
-        await createTaskApi({
-          title: this.newTask.title,
-          description: this.newTask.description,
-          category: this.newTask.category,
-          points: parseInt(this.newTask.points) || 2,
-          duration: parseInt(this.newTask.duration) || null,
-          memberIds: this.newTask.memberIds,
-          taskType: this.newTask.taskType,
-          frequency: this.newTask.frequency,
-          maxFrequency: parseInt(this.newTask.maxFrequency) || null,
-          completeTypes: this.newTask.completeTypes,
-          needReview: this.newTask.needReview,
-          reviewType: this.newTask.reviewType,
-          earliestStart: this.newTask.earliestStart || null,
-          latestEnd: this.newTask.latestEnd || null
-        })
-        uni.showToast({ title: '创建成功', icon: 'success' })
-        this.showAddModal = false
-        this.resetNewTask()
-        this.loadTasks()
-      } catch (e) {
-        console.error('创建任务失败', e)
-        uni.showToast({ title: '创建失败', icon: 'none' })
-      }
-    },
-    resetNewTask() {
-      this.newTask = {
-        title: '',
-        description: '',
-        category: '',
-        points: 2,
-        duration: '',
-        memberIds: this.children.length > 0 ? [this.children[0].id] : [],
-        taskType: 'onetime',
-        frequency: '',
-        maxFrequency: '',
-        completeTypes: ['checkin'],
-        needReview: 0,
-        reviewType: 'parent',
-        earliestStart: '',
-        latestEnd: ''
-      }
-      this.selectedTemplate = null
-    },
-    formatTime(dateStr) {
-      if (!dateStr) return ''
-      const date = parseDate(dateStr)
-      if (!date) return ''
-      return `${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
-    },
-    enterBatchMode() {
-      this.batchMode = true
-      this.selectedTasks = []
-    },
-    exitBatchMode() {
-      this.batchMode = false
-      this.selectedTasks = []
-    },
-    toggleTaskSelect(taskId) {
-      const index = this.selectedTasks.indexOf(taskId)
-      if (index > -1) {
-        this.selectedTasks.splice(index, 1)
-      } else {
-        this.selectedTasks.push(taskId)
-      }
-    },
-    toggleSelectAll() {
-      if (this.isAllSelected) {
-        this.selectedTasks = []
-      } else {
-        this.selectedTasks = this.tasks.filter(t => t.status === 'pending').map(t => t.id)
-      }
-    },
-    async batchComplete() {
-      if (this.selectedTasks.length === 0) {
-        uni.showToast({ title: '请选择任务', icon: 'none' })
-        return
-      }
-      try {
-        await batchCompleteTasks(this.selectedTasks)
-        uni.showToast({ title: '批量完成成功', icon: 'success' })
-        this.exitBatchMode()
-        this.loadParentTasks()
-      } catch (e) {
-        console.error('批量完成失败', e)
-        uni.showToast({ title: '批量完成失败', icon: 'none' })
-      }
-    },
-    async batchDelete() {
-      if (this.selectedTasks.length === 0) {
-        uni.showToast({ title: '请选择任务', icon: 'none' })
-        return
-      }
-      uni.showModal({
-        title: '确认删除',
-        content: `确定要删除选中的 ${this.selectedTasks.length} 个任务吗?`,
-        success: async (res) => {
-          if (res.confirm) {
-            try {
-              await batchDeleteTasks(this.selectedTasks)
-              uni.showToast({ title: '批量删除成功', icon: 'success' })
-              this.exitBatchMode()
-              this.loadParentTasks()
-            } catch (e) {
-              console.error('批量删除失败', e)
-              uni.showToast({ title: '批量删除失败', icon: 'none' })
-            }
-          }
-        }
-      })
-    }
-  },
-  computed: {
-    isAllSelected() {
-      if (this.selectedTasks.length === 0) return false
-      const pendingTasks = this.tasks.filter(t => t.status === 'pending')
-      return pendingTasks.length > 0 && pendingTasks.every(t => this.selectedTasks.includes(t.id))
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container {
-  padding: 30rpx;
-  padding-bottom: 120rpx;
-}
-
-.header-bar {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 30rpx;
-}
-
-.header-title {
-  font-size: 32rpx;
-  font-weight: bold;
-  color: #333;
-}
-
-.btn-batch {
-  background: #F97316;
-  color: #fff;
-  font-size: 24rpx;
-  padding: 10rpx 30rpx;
-  border-radius: 30rpx;
-}
-
-.batch-bar {
-  display: flex;
-  align-items: center;
-  background: #f5f5f5;
-  padding: 20rpx 30rpx;
-  border-radius: 16rpx;
-  margin-bottom: 30rpx;
-}
-
-.batch-label {
-  font-size: 28rpx;
-  color: #333;
-  margin-left: 10rpx;
-  margin-right: auto;
-}
-
-.batch-actions {
-  display: flex;
-  gap: 20rpx;
-}
-
-.batch-btn {
-  font-size: 24rpx;
-  padding: 10rpx 30rpx;
-  border-radius: 30rpx;
-  background: #F97316;
-  color: #fff;
-}
-
-.batch-btn.complete {
-  background: #52C41A;
-}
-
-.batch-btn.delete {
-  background: #FF4D4F;
-}
-
-.batch-btn[disabled] {
-  background: #ccc;
-}
-
-.batch-exit {
-  font-size: 24rpx;
-  color: #999;
-  margin-left: 20rpx;
-}
-
-.task-item {
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 30rpx;
-  margin-bottom: 20rpx;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.task-item.selected {
-  background: #FFF1F0;
-  border: 2rpx solid #F97316;
-}
-
-.task-checkbox {
-  margin-right: 20rpx;
-}
-
-.task-title {
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 30rpx;
-  margin-bottom: 20rpx;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.task-item.completed {
-  opacity: 0.6;
-}
-
-.task-title {
-  font-size: 30rpx;
-  color: #333;
-  margin-bottom: 10rpx;
-}
-
-.task-meta {
-  display: flex;
-  gap: 20rpx;
-}
-
-.points {
-  color: #F97316;
-  font-size: 24rpx;
-}
-
-.deadline {
-  color: #999;
-  font-size: 24rpx;
-}
-
-.btn-complete {
-  background: #F97316;
-  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;
-  }
-
-  .add-btn {
-  position: fixed;
-  bottom: 30rpx;
-  left: 30rpx;
-  right: 30rpx;
-}
-
-.empty {
-  text-align: center;
-  color: #999;
-  padding: 100rpx;
-}
-
-.modal-mask {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0, 0, 0, 0.5);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.modal {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 40rpx;
-  width: 80%;
-  max-height: 80vh;
-  overflow-y: auto;
-}
-
-.modal-large {
-  width: 90%;
-}
-
-.modal-title {
-  font-size: 32rpx;
-  font-weight: bold;
-  margin-bottom: 30rpx;
-  text-align: center;
-}
-
-.form-item {
-  margin-bottom: 20rpx;
-}
-
-.form-label {
-  font-size: 28rpx;
-  color: #333;
-  margin-bottom: 10rpx;
-}
-
-.form-label .required {
-  color: #F97316;
-}
-
-.form-row {
-  display: flex;
-  gap: 20rpx;
-}
-
-.form-item.half {
-  flex: 1;
-}
-
-.form-item input, .picker, .textarea {
-  border: 1rpx solid #ddd;
-  border-radius: 10rpx;
-  padding: 20rpx;
-  font-size: 28rpx;
-  width: 100%;
-  box-sizing: border-box;
-}
-
-.textarea {
-  min-height: 100rpx;
-}
-
-.form-item checkbox-group {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 20rpx;
-}
-
-.form-item label {
-  display: flex;
-  align-items: center;
-  gap: 10rpx;
-  font-size: 28rpx;
-}
-
-.modal-btns {
-  display: flex;
-  gap: 20rpx;
-  margin-top: 30rpx;
-}
-
-.modal-btns button {
-  flex: 1;
-}
-</style>
+<template>
+  <view class="container">
+    <!-- 顶部 Tab 切换 -->
+    <view class="tab-bar">
+      <view class="tab-item" :class="{ active: activeTab === 'pending' }" @click="switchTab('pending')">
+        <text class="tab-text">待接受</text>
+        <text class="tab-badge" v-if="pendingTasks.length > 0">{{ pendingTasks.length }}</text>
+      </view>
+      <view class="tab-item" :class="{ active: activeTab === 'today' }" @click="switchTab('today')">
+        <text class="tab-text">今日任务</text>
+      </view>
+      <view class="tab-item" :class="{ active: activeTab === 'created' }" @click="switchTab('created')">
+        <text class="tab-text">我创建的</text>
+      </view>
+    </view>
+
+    <!-- 待接受任务 -->
+    <view class="task-list" v-if="activeTab === 'pending'">
+      <view class="task-item assigned" v-for="task in pendingTasks" :key="task.id">
+        <view class="task-left">
+          <view class="task-title">{{ task.title }}</view>
+          <view class="task-meta">
+            <text class="points">+{{ task.points }}分</text>
+            <text class="creator">创建人:{{ task.creatorName || '家人' }}</text>
+            <text class="deadline">截止 {{ formatTime(task.deadline) }}</text>
+          </view>
+        </view>
+        <view class="task-actions">
+          <button class="btn-accept" @click="acceptTask(task.id)">接受</button>
+          <button class="btn-reject" @click="showRejectModal(task)">拒绝</button>
+        </view>
+      </view>
+      <view class="empty" v-if="!loading && pendingTasks.length === 0">
+        <text>暂无待接受任务</text>
+      </view>
+    </view>
+
+    <!-- 今日任务 -->
+    <view class="task-list" v-if="activeTab === 'today'">
+      <view class="task-item" v-for="task in todayTasks" :key="task.id" :class="{ completed: task.status === 'completed' }">
+        <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 class="task-right" v-if="task.status === 'pending'">
+          <button class="btn-complete" @click="completeTask(task.id)">完成</button>
+        </view>
+        <view class="completed-badge" v-else>已完成</view>
+      </view>
+      <view class="empty" v-if="!loading && todayTasks.length === 0">
+        <text>暂无今日任务</text>
+      </view>
+    </view>
+
+    <!-- 我创建的任务 -->
+    <view class="task-list" v-if="activeTab === 'created'">
+      <view class="task-item" v-for="task in createdTasks" :key="task.id">
+        <view class="task-left">
+          <view class="task-title">{{ task.title }}</view>
+          <view class="task-meta">
+            <text class="points">+{{ task.points }}分</text>
+            <text class="assignee">{{ task.executorName || '未分配' }}</text>
+            <text class="status-tag" :class="'status-' + task.status">{{ statusLabel(task.status) }}</text>
+          </view>
+        </view>
+      </view>
+      <view class="empty" v-if="!loading && createdTasks.length === 0">
+        <text>暂无创建的任务</text>
+      </view>
+    </view>
+
+    <!-- 添加任务按钮 -->
+    <button v-if="activeTab === 'today' && isParent" class="add-btn" @click="showAddModal = true">
+      + 添加任务
+    </button>
+
+    <!-- 拒绝确认弹窗 -->
+    <view class="modal-mask" v-if="showRejectModalFlag" @click="showRejectModalFlag = false">
+      <view class="modal" @click.stop>
+        <view class="modal-title">确认拒绝</view>
+        <view class="modal-content">
+          <text>拒绝后,创建人对你的信任度和亲密度将各减1分,沟通质量各加1分。</text>
+        </view>
+        <view class="modal-btns">
+          <button @click="showRejectModalFlag = false">取消</button>
+          <button class="btn-primary" @click="confirmReject">确认拒绝</button>
+        </view>
+      </view>
+    </view>
+
+    <!-- 创建任务弹窗 -->
+    <view class="modal-mask" v-if="showAddModal" @click="showAddModal = false">
+      <view class="modal modal-large" @click.stop>
+        <view class="modal-title">创建任务</view>
+
+        <view class="form-item">
+          <view class="form-label">任务标题 <text class="required">*</text></view>
+          <input v-model="newTask.title" placeholder="请输入任务标题" />
+        </view>
+
+        <view class="form-item">
+          <view class="form-label">任务内容</view>
+          <textarea v-model="newTask.description" placeholder="请输入任务描述" class="textarea" />
+        </view>
+
+        <view class="form-item">
+          <view class="form-label">类型 <text class="required">*</text></view>
+          <picker mode="selector" :range="categories" @change="onCategoryChange">
+            <view class="picker">{{ newTask.category || '请选择类型' }}</view>
+          </picker>
+        </view>
+
+        <view class="form-item">
+          <view class="form-label">积分 <text class="required">*</text></view>
+          <input v-model="newTask.points" type="number" placeholder="1-10" />
+        </view>
+
+        <view class="form-item">
+          <view class="form-label">截止时间 <text class="required">*</text></view>
+          <picker mode="datetime" :value="newTask.deadline" @change="onDeadlineChange">
+            <view class="picker">{{ newTask.deadline || '选择截止时间' }}</view>
+          </picker>
+        </view>
+
+        <view class="form-item">
+          <view class="form-label">分配给 <text class="required">*</text></view>
+          <view class="assignee-tabs">
+            <view class="assignee-tab" :class="{ active: newTask.executorType === 'child' }" @click="newTask.executorType = 'child'">
+              <text>孩子</text>
+            </view>
+            <view class="assignee-tab" :class="{ active: newTask.executorType === 'member' }" @click="newTask.executorType = 'member'">
+              <text>家庭成员</text>
+            </view>
+          </view>
+        </view>
+
+        <view class="form-item" v-if="newTask.executorType === 'child'">
+          <view class="form-label">选择孩子 <text class="required">*</text></view>
+          <checkbox-group @change="onChildSelectChange">
+            <label v-for="child in children" :key="child.id">
+              <checkbox :value="String(child.id)" :checked="newTask.memberIds.includes(String(child.id))" />
+              <text>{{ child.nickname }}</text>
+            </label>
+          </checkbox-group>
+        </view>
+
+        <view class="form-item" v-if="newTask.executorType === 'member'">
+          <view class="form-label">选择家庭成员 <text class="required">*</text></view>
+          <checkbox-group @change="onMemberSelectChange">
+            <label v-for="member in familyMembers" :key="member.id">
+              <checkbox :value="String(member.id)" :checked="newTask.memberIds.includes(String(member.id))" />
+              <text>{{ member.nickname }}</text>
+            </label>
+          </checkbox-group>
+        </view>
+
+        <view class="modal-btns">
+          <button @click="showAddModal = false">取消</button>
+          <button class="btn-primary" @click="createTask">创建</button>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getMyPendingTasks, acceptTask as acceptTaskApi, rejectTask as rejectTaskApi, getTodayTasks, completeTask as completeTaskApi, createTask as createTaskApi, getChildren, getVisibleFamilyMembers } from '../../utils/api.js'
+import { parseDate } from '../../utils/format.js'
+
+export default {
+  data() {
+    return {
+      activeTab: 'today',
+      loading: false,
+      pendingTasks: [],
+      todayTasks: [],
+      createdTasks: [],
+      isParent: false,
+      currentMemberId: null,
+      currentUserId: null,
+      children: [],
+      familyMembers: [],
+      showAddModal: false,
+      showRejectModalFlag: false,
+      rejectTaskId: null,
+      newTask: {
+        title: '',
+        description: '',
+        category: '',
+        points: 2,
+        deadline: '',
+        executorType: 'child',
+        memberIds: []
+      },
+      categories: ['学习类', '生活类', '运动类', '其他']
+    }
+  },
+  onShow() {
+    const role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
+    this.isParent = role === 'parent'
+    this.currentUserId = uni.getStorageSync('userId')
+    this.currentMemberId = uni.getStorageSync('currentChildId') || this.currentUserId
+    this.loadInitialData()
+  },
+  methods: {
+    switchTab(tab) {
+      this.activeTab = tab
+      if (tab === 'pending') this.loadPendingTasks()
+      else if (tab === 'today') this.loadTodayTasks()
+      else if (tab === 'created') this.loadCreatedTasks()
+    },
+    async loadInitialData() {
+      this.loading = true
+      try {
+        const [childrenRes, membersRes] = await Promise.all([
+          getChildren(),
+          getVisibleFamilyMembers()
+        ])
+        this.children = childrenRes.data || []
+        this.familyMembers = membersRes.data || []
+        this.loadTodayTasks()
+        this.loadPendingTasks()
+        this.loadCreatedTasks()
+      } catch (e) {
+        console.error('加载数据失败', e)
+      } finally {
+        this.loading = false
+      }
+    },
+    async loadPendingTasks() {
+      try {
+        const res = await getMyPendingTasks(this.currentMemberId)
+        this.pendingTasks = res.data || []
+      } catch (e) {
+        console.error('加载待接受任务失败', e)
+      }
+    },
+    async loadTodayTasks() {
+      try {
+        const res = await getTodayTasks(this.currentMemberId)
+        this.todayTasks = res.data || []
+      } catch (e) {
+        console.error('加载今日任务失败', e)
+      }
+    },
+    async loadCreatedTasks() {
+      try {
+        const res = await getTodayTasks(this.currentUserId)
+        this.createdTasks = res.data || []
+      } catch (e) {
+        console.error('加载创建的任务失败', e)
+      }
+    },
+    async completeTask(taskId) {
+      try {
+        const res = await completeTaskApi(taskId, this.currentMemberId, '')
+        uni.showToast({ title: `获得 ${res.data.pointsEarned} 积分`, icon: 'success' })
+        this.loadTodayTasks()
+      } catch (e) {
+        console.error('完成任务失败', e)
+      }
+    },
+    acceptTask(taskId) {
+      uni.showLoading({ title: '接受中...' })
+      acceptTaskApi(taskId, this.currentMemberId).then(res => {
+        uni.hideLoading()
+        if (res.data) {
+          uni.showToast({ title: '已接受任务', icon: 'success' })
+          this.loadPendingTasks()
+          this.loadTodayTasks()
+        } else {
+          uni.showToast({ title: '接受失败', icon: 'none' })
+        }
+      }).catch(e => {
+        uni.hideLoading()
+        uni.showToast({ title: '接受失败', icon: 'none' })
+      })
+    },
+    showRejectModal(task) {
+      this.rejectTaskId = task.id
+      this.showRejectModalFlag = true
+    },
+    confirmReject() {
+      uni.showLoading({ title: '处理中...' })
+      rejectTaskApi(this.rejectTaskId, this.currentMemberId).then(res => {
+        uni.hideLoading()
+        if (res.data) {
+          uni.showToast({ title: '已拒绝', icon: 'success' })
+          this.loadPendingTasks()
+        } else {
+          uni.showToast({ title: '操作失败', icon: 'none' })
+        }
+      }).catch(e => {
+        uni.hideLoading()
+        uni.showToast({ title: '操作失败', icon: 'none' })
+      })
+      this.showRejectModalFlag = false
+    },
+    async createTask() {
+      if (!this.newTask.title) {
+        uni.showToast({ title: '请输入任务标题', icon: 'none' })
+        return
+      }
+      if (!this.newTask.category) {
+        uni.showToast({ title: '请选择类型', icon: 'none' })
+        return
+      }
+      if (this.newTask.memberIds.length === 0) {
+        uni.showToast({ title: '请选择分配对象', icon: 'none' })
+        return
+      }
+      try {
+        await createTaskApi({
+          title: this.newTask.title,
+          description: this.newTask.description,
+          category: this.newTask.category,
+          points: parseInt(this.newTask.points) || 2,
+          deadline: this.newTask.deadline ? new Date(this.newTask.deadline).getTime() : null,
+          executorType: this.newTask.executorType,
+          executorId: this.newTask.executorType === 'member' ? Number(this.newTask.memberIds[0]) : null,
+          memberId: this.newTask.executorType === 'child' ? Number(this.newTask.memberIds[0]) : null,
+          memberIds: this.newTask.executorType === 'child' ? this.newTask.memberIds.map(id => Number(id)) : []
+        })
+        uni.showToast({ title: '创建成功', icon: 'success' })
+        this.showAddModal = false
+        this.resetNewTask()
+        this.loadCreatedTasks()
+        this.loadPendingTasks()
+      } catch (e) {
+        console.error('创建任务失败', e)
+        uni.showToast({ title: '创建失败', icon: 'none' })
+      }
+    },
+    resetNewTask() {
+      this.newTask = {
+        title: '',
+        description: '',
+        category: '',
+        points: 2,
+        deadline: '',
+        executorType: 'child',
+        memberIds: []
+      }
+    },
+    onCategoryChange(e) {
+      this.newTask.category = this.categories[e.detail.value]
+    },
+    onDeadlineChange(e) {
+      this.newTask.deadline = e.detail.value
+    },
+    onChildSelectChange(e) {
+      this.newTask.memberIds = e.detail.value
+    },
+    onMemberSelectChange(e) {
+      this.newTask.memberIds = e.detail.value
+    },
+    formatTime(dateStr) {
+      if (!dateStr) return ''
+      const date = parseDate(dateStr)
+      if (!date) return ''
+      return `${date.getMonth()+1}月${date.getDate()}日 ${String(date.getHours()).padStart(2,'0')}:${String(date.getMinutes()).padStart(2,'0')}`
+    },
+    statusLabel(status) {
+      const map = {
+        'pending': '待审核',
+        'assigned': '待接受',
+        'in_progress': '进行中',
+        'completed': '已完成',
+        'rejected': '已拒绝',
+        'overdue': '已过期',
+        'cancelled': '已取消'
+      }
+      return map[status] || status
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  padding-bottom: 120rpx;
+  background: #f5f7fa;
+  min-height: 100vh;
+}
+
+.tab-bar {
+  display: flex;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 10rpx;
+  margin-bottom: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+
+.tab-item {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 20rpx 0;
+  border-radius: 12rpx;
+  position: relative;
+}
+
+.tab-item.active {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+}
+
+.tab-text {
+  font-size: 28rpx;
+  color: #666;
+  font-weight: 500;
+}
+
+.tab-item.active .tab-text {
+  color: #fff;
+}
+
+.tab-badge {
+  position: absolute;
+  top: 8rpx;
+  right: 30rpx;
+  background: #FF4D4F;
+  color: #fff;
+  font-size: 20rpx;
+  padding: 2rpx 10rpx;
+  border-radius: 20rpx;
+  min-width: 32rpx;
+  text-align: center;
+}
+
+.task-list {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+
+.task-item {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.task-item.assigned {
+  border: 2rpx solid #F97316;
+}
+
+.task-item.completed {
+  opacity: 0.6;
+}
+
+.task-left {
+  flex: 1;
+}
+
+.task-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 10rpx;
+}
+
+.task-meta {
+  display: flex;
+  gap: 20rpx;
+  flex-wrap: wrap;
+}
+
+.points {
+  color: #F97316;
+  font-size: 24rpx;
+  font-weight: 500;
+}
+
+.creator, .assignee {
+  color: #999;
+  font-size: 24rpx;
+}
+
+.deadline {
+  color: #999;
+  font-size: 24rpx;
+}
+
+.status-tag {
+  font-size: 22rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 12rpx;
+  background: #f0f0f0;
+  color: #666;
+}
+
+.status-assigned {
+  background: #FFF7ED;
+  color: #F97316;
+}
+
+.status-in_progress {
+  background: #ECFDF5;
+  color: #10B981;
+}
+
+.status-rejected {
+  background: #FEF2F2;
+  color: #EF4444;
+}
+
+.task-right {
+  margin-left: 20rpx;
+}
+
+.btn-complete, .btn-accept {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+  font-size: 26rpx;
+  padding: 12rpx 30rpx;
+  border-radius: 30rpx;
+  border: none;
+}
+
+.btn-reject {
+  background: #fff;
+  color: #EF4444;
+  font-size: 26rpx;
+  padding: 12rpx 30rpx;
+  border-radius: 30rpx;
+  border: 2rpx solid #EF4444;
+  margin-left: 16rpx;
+}
+
+.task-actions {
+  display: flex;
+  align-items: center;
+}
+
+.completed-badge {
+  color: #52C41A;
+  font-size: 26rpx;
+}
+
+.minigame-badge {
+  font-size: 28rpx;
+  margin-left: 10rpx;
+}
+
+.empty {
+  text-align: center;
+  color: #999;
+  padding: 100rpx 0;
+  font-size: 28rpx;
+}
+
+.add-btn {
+  position: fixed;
+  bottom: 40rpx;
+  left: 30rpx;
+  right: 30rpx;
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+  font-size: 30rpx;
+  font-weight: bold;
+  padding: 24rpx 0;
+  border-radius: 40rpx;
+  border: none;
+  box-shadow: 0 8rpx 24rpx rgba(249,115,22,0.3);
+}
+
+.modal-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.modal {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 40rpx;
+  width: 85%;
+  max-height: 80vh;
+  overflow-y: auto;
+}
+
+.modal-large {
+  width: 90%;
+}
+
+.modal-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  margin-bottom: 30rpx;
+  text-align: center;
+  color: #333;
+}
+
+.modal-content {
+  font-size: 28rpx;
+  color: #666;
+  line-height: 1.6;
+  margin-bottom: 30rpx;
+  text-align: center;
+}
+
+.modal-btns {
+  display: flex;
+  gap: 20rpx;
+  margin-top: 30rpx;
+}
+
+.modal-btns button {
+  flex: 1;
+  height: 80rpx;
+  line-height: 80rpx;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+  border: none;
+}
+
+.modal-btns button:first-child {
+  background: #f5f5f5;
+  color: #666;
+}
+
+.form-item {
+  margin-bottom: 24rpx;
+}
+
+.form-label {
+  font-size: 28rpx;
+  color: #333;
+  margin-bottom: 12rpx;
+  font-weight: 500;
+}
+
+.form-label .required {
+  color: #F97316;
+}
+
+.form-item input, .form-item .picker {
+  border: 1rpx solid #ddd;
+  border-radius: 10rpx;
+  padding: 20rpx;
+  font-size: 28rpx;
+  width: 100%;
+  box-sizing: border-box;
+  background: #fafafa;
+}
+
+.form-item .picker {
+  color: #333;
+}
+
+.textarea {
+  min-height: 120rpx;
+  width: 100%;
+  box-sizing: border-box;
+  font-size: 28rpx;
+  padding: 20rpx;
+  border: 1rpx solid #ddd;
+  border-radius: 10rpx;
+  background: #fafafa;
+}
+
+.assignee-tabs {
+  display: flex;
+  gap: 20rpx;
+}
+
+.assignee-tab {
+  flex: 1;
+  padding: 20rpx;
+  text-align: center;
+  border-radius: 10rpx;
+  border: 2rpx solid #ddd;
+  font-size: 28rpx;
+  color: #666;
+  background: #fafafa;
+}
+
+.assignee-tab.active {
+  border-color: #F97316;
+  background: #FFF7ED;
+  color: #F97316;
+  font-weight: bold;
+}
+
+.form-item checkbox-group {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20rpx;
+}
+
+.form-item label {
+  display: flex;
+  align-items: center;
+  gap: 10rpx;
+  font-size: 28rpx;
+  color: #333;
+  padding: 16rpx 24rpx;
+  border: 1rpx solid #ddd;
+  border-radius: 10rpx;
+  background: #fafafa;
+}
+</style>

+ 1 - 1
cfc-frontend/pages/user-edit/user-edit.vue

@@ -43,7 +43,7 @@
 		</view>
 
 		<!-- 详细信息填写(选择角色后显示) -->
-		<view v-if="isEdit && hasFamily">
+		<view v-if="isEdit && (!isNewUserFlow || hasFamily)">
 		<view class="form-section">
 			<!-- 头像 -->
 			<view class="form-item avatar-item">

+ 15 - 0
cfc-frontend/utils/api.js

@@ -536,6 +536,21 @@ export const completeParentTask = (taskId) => {
   return request(`/api/tasks/${taskId}/complete-parent`, 'POST', {})
 }
 
+// ===
+// 家庭成员任务分配模块
+// ===
+export const getMyPendingTasks = (memberId) => {
+  return request('/api/tasks/my-pending', 'POST', { memberId })
+}
+
+export const acceptTask = (taskId, memberId) => {
+  return request('/api/tasks/accept', 'POST', { taskId, memberId })
+}
+
+export const rejectTask = (taskId, memberId) => {
+  return request(`/api/tasks/reject/${taskId}`, 'POST', { memberId })
+}
+
 // ===
 // 家长心愿单模块
 // ===

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-65e79f8c6aaa4dc924acfc50b657fa9d8e416d61
+115f80296c1237af90d4c4b23502ca21dbddfc68

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.880",
+  "version": "1.0.886",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.880",
+      "version": "1.0.886",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.881",
+  "version": "1.0.887",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 69 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,75 @@
 
 ---
 
+## v1.0.887 (2026-08-09)
+
+### Bug 修复
+- 修复DBI启动日志WARN(RENAME语法+不存在表/列的迁移)
+
+### 其他
+-   → 改用 tableExists() 前置检查 + RENAME TABLE(无 IF EXISTS)
+- - 修复 1 处 CHANGE COLUMN min_quantity(列已不存在时报错)
+-   → 改用 columnExists() 前置检查,不存在则跳过
+- - 新增 tableExists() + columnExists() 辅助方法
+- - 不影响业务,仅消除启动日志噪声
+- 
+
+
+## v1.0.886 (2026-08-09)
+
+### 新功能
+- 任务首页重设计 - 三Tab布局 + 家庭成员任务分配 + 接受/拒绝 + 关系分数调整
+
+
+## v1.0.885 (2026-08-09)
+
+### 新功能
+- 任务首页重设计 - 支持分配任务给家庭成员,接受/拒绝机制及关系分数调整
+
+
+## v1.0.883 (2026-08-09)
+
+### 新功能
+- 我的页面服务区添加商城和购物车入口
+
+### 其他
+- 🛒 购物车 (pages/shop/cart/cart) 快捷入口。
+- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- 当 store familyId 为空时显示 FamilyEmptyState(暂无数据)。
+- 编辑个人资料不应依赖是否有家庭,改为老用户 (isNewUserFlow=false) 始终显示表单。
+- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+### Bug 修复
+- 完善信息页面移除 hasFamily 门控 — 老用户编辑时始终显示表单
+
+
+## v1.0.882 (2026-08-08)
+
+### 重构
+- 首页重新设计 — 对齐新TabBar页面模式
+
+### 其他
+- - 移除 FamilyEnergyBar(沙盘已含能量信息)
+- - 移除 DimensionTasks/圈子模块,移入功能入口
+- - 新增 3列功能入口网格(对齐健康/成长页 grid-3col)
+- - 移除未登录态五维全景幸福矩阵
+- - 重构结构:Banner → FamilyMemberStrip → 沙盘 → 网格 → 挑战 → 推荐
+- - 清理冗余 JS 数据/方法/API调用和 CSS 样式
+- 
+- 导致 ProfileHeader/Badges/Growth/Menu 加载失败,页面仅显示内联个人信息卡片。
+- 将 4 个 Profile 组件复制到全局 components/ 目录,并更新两处 import 路径。
+- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+### Bug 修复
+- 修复我的页面主包引用分包组件导致内容不显示
+
+
 ## v1.0.881 (2026-08-08)
 
 ### 重构

+ 70 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.881
+> 当前版本: v1.0.887
 
 ## 历史版本
 
@@ -8,6 +8,75 @@
 
 ---
 
+## v1.0.887 (2026-08-09)
+
+### Bug 修复
+- 修复DBI启动日志WARN(RENAME语法+不存在表/列的迁移)
+
+### 其他
+-   → 改用 tableExists() 前置检查 + RENAME TABLE(无 IF EXISTS)
+- - 修复 1 处 CHANGE COLUMN min_quantity(列已不存在时报错)
+-   → 改用 columnExists() 前置检查,不存在则跳过
+- - 新增 tableExists() + columnExists() 辅助方法
+- - 不影响业务,仅消除启动日志噪声
+- 
+
+
+## v1.0.886 (2026-08-09)
+
+### 新功能
+- 任务首页重设计 - 三Tab布局 + 家庭成员任务分配 + 接受/拒绝 + 关系分数调整
+
+
+## v1.0.885 (2026-08-09)
+
+### 新功能
+- 任务首页重设计 - 支持分配任务给家庭成员,接受/拒绝机制及关系分数调整
+
+
+## v1.0.883 (2026-08-09)
+
+### 新功能
+- 我的页面服务区添加商城和购物车入口
+
+### 其他
+- 🛒 购物车 (pages/shop/cart/cart) 快捷入口。
+- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+- 当 store familyId 为空时显示 FamilyEmptyState(暂无数据)。
+- 编辑个人资料不应依赖是否有家庭,改为老用户 (isNewUserFlow=false) 始终显示表单。
+- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+### Bug 修复
+- 完善信息页面移除 hasFamily 门控 — 老用户编辑时始终显示表单
+
+
+## v1.0.882 (2026-08-08)
+
+### 重构
+- 首页重新设计 — 对齐新TabBar页面模式
+
+### 其他
+- - 移除 FamilyEnergyBar(沙盘已含能量信息)
+- - 移除 DimensionTasks/圈子模块,移入功能入口
+- - 新增 3列功能入口网格(对齐健康/成长页 grid-3col)
+- - 移除未登录态五维全景幸福矩阵
+- - 重构结构:Banner → FamilyMemberStrip → 沙盘 → 网格 → 挑战 → 推荐
+- - 清理冗余 JS 数据/方法/API调用和 CSS 样式
+- 
+- 导致 ProfileHeader/Badges/Growth/Menu 加载失败,页面仅显示内联个人信息卡片。
+- 将 4 个 Profile 组件复制到全局 components/ 目录,并更新两处 import 路径。
+- Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
+- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
+- 
+
+### Bug 修复
+- 修复我的页面主包引用分包组件导致内容不显示
+
+
 ## v1.0.881 (2026-08-08)
 
 ### 重构