jiapu пре 2 месеци
родитељ
комит
4d220e83ef

+ 76 - 11
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -9303,6 +9303,67 @@ public class DatabaseInitializer implements CommandLineRunner {
         } catch (Exception e) {
             log.warn("扩展 tasks.status 枚举失败(可能已修改): {}", e.getMessage());
         }
+
+        // 迁移267: 方案中心菜单(插在家庭运营与任务中心之间)
+        migratePlanCenterMenu();
+    }
+
+    /**
+     * 迁移267: 存量库插入「方案中心」一级菜单及子菜单(幂等)
+     * 位置:家庭运营(sort=1) 与 任务中心 之间;任务中心及之后顶级菜单 sort+1
+     */
+    private void migratePlanCenterMenu() {
+        try {
+            Long existed = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=0 AND title='方案中心'");
+            if (existed == null) {
+                // 为任务中心及之后的顶级菜单腾出 sort=2
+                jdbcTemplate.update(
+                        "UPDATE sys_menu SET sort = sort + 1 WHERE parent_id=0 AND sort >= 2 " +
+                        "AND title <> '方案中心'");
+                jdbcTemplate.update(
+                        "INSERT INTO sys_menu (parent_id, title, label, path, icon, perm, sort, visible) " +
+                        "VALUES (0, '方案中心', NULL, NULL, 'el-icon-notebook-2', 'plan:*', 2, 1)");
+                existed = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
+                log.info("迁移267: 已插入方案中心一级菜单");
+            }
+            if (existed == null) {
+                return;
+            }
+            Long healthPlanId = insertPlanCenterChild(existed, "健康方案管理", "/health-plan-manage",
+                    "el-icon-first-aid-kit", "plan:health", 0);
+            Long assessmentPlanId = insertPlanCenterChild(existed, "测评方案管理", "/assessment-plan-manage",
+                    "el-icon-edit", "plan:assessment", 1);
+            Integer grantCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM sys_role_menu", Integer.class);
+            if (grantCount != null && grantCount > 0) {
+                String[] roles = {"admin", "butler", "nutritionist", "operator"};
+                Long[] menuIds = {existed, healthPlanId, assessmentPlanId};
+                for (String role : roles) {
+                    for (Long menuId : menuIds) {
+                        grantMenuToRole(role, menuId);
+                    }
+                }
+            }
+            log.info("迁移267: 方案中心菜单挂载完成");
+        } catch (Exception e) {
+            log.warn("迁移267失败: {}", e.getMessage());
+        }
+    }
+
+    private Long insertPlanCenterChild(Long parentId, String label, String path, String icon, String perm, int sort) {
+        try {
+            Long existed = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=" + parentId + " AND path='" + path + "'");
+            if (existed != null) {
+                return existed;
+            }
+            jdbcTemplate.update(
+                    "INSERT INTO sys_menu (parent_id, title, label, path, icon, perm, sort, visible) VALUES (?, NULL, ?, ?, ?, ?, ?, 1)",
+                    parentId, label, path, icon, perm, sort
+            );
+            return jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
+        } catch (Exception e) {
+            log.warn("迁移267: 挂载子菜单「{}」失败: {}", label, e.getMessage());
+            return null;
+        }
     }
 
     /**
@@ -9553,20 +9614,24 @@ public class DatabaseInitializer implements CommandLineRunner {
                 {"operation", "pointsLog", "", "积分记录", "/points-log", "el-icon-document", "family:points,family:bound", "3"},
                 {"operation", "wishes", "", "心愿管理", "/wishes", "el-icon-star-off", "family:wishes", "4"},
                 {"operation", "rewards", "", "奖励管理", "/rewards", "el-icon-s-goods", "reward:list", "5"},
+                // 方案中心(plan:*,插在家庭运营与任务中心之间)
+                {"", "planCenter", "方案中心", "", "", "el-icon-notebook-2", "plan:*", "2"},
+                {"planCenter", "healthPlanManage", "", "健康方案管理", "/health-plan-manage", "el-icon-first-aid-kit", "plan:health", "0"},
+                {"planCenter", "assessmentPlanManage", "", "测评方案管理", "/assessment-plan-manage", "el-icon-edit", "plan:assessment", "1"},
                 // 任务中心(task:*)
-                {"", "taskCenter", "任务中心", "", "", "el-icon-s-order", "task:*", "2"},
+                {"", "taskCenter", "任务中心", "", "", "el-icon-s-order", "task:*", "3"},
                 {"taskCenter", "tasks", "", "任务列表", "/tasks", "el-icon-s-order", "task:list", "0"},
                 {"taskCenter", "taskTemplates", "", "任务模板", "/task-templates", "el-icon-document", "task:templates", "1"},
                 {"taskCenter", "growthTask", "", "成长任务管理", "/growth-task", "el-icon-s-management", "task:growth", "2"},
                 // 审核中心(audit,归集各审核页:服务角色申请/活动审核/退款/提现)
-                {"", "reviewCenterGroup", "审核中心", "", "", "el-icon-s-check", "audit", "3"},
+                {"", "reviewCenterGroup", "审核中心", "", "", "el-icon-s-check", "audit", "4"},
                 {"reviewCenterGroup", "reviewCenter", "", "审核中心总览", "/review-center", "el-icon-s-check", "audit", "0"},
                 {"reviewCenterGroup", "serviceRoleApp", "", "服务角色申请审核", "/service-role-applications", "el-icon-s-check", "audit:service-role", "1"},
                 {"reviewCenterGroup", "activityReview", "", "活动审核", "/activity-review", "el-icon-document-checked", "audit:activity", "2"},
                 {"reviewCenterGroup", "pendingRefund", "", "待退款管理", "/pending-refund", "el-icon-warning", "audit:refund", "3"},
                 {"reviewCenterGroup", "earningWithdraw", "", "收益提现审核", "/family-earnings-withdraw", "el-icon-document-checked", "audit:withdraw", "4"},
                 // 商城营销(commerce:*)
-                {"", "commerce", "商城营销", "", "", "el-icon-s-goods", "commerce:*", "4"},
+                {"", "commerce", "商城营销", "", "", "el-icon-s-goods", "commerce:*", "5"},
                 {"commerce", "productManage", "", "商品管理", "/product-manage", "el-icon-s-goods", "commerce:products,commerce:self", "0"},
                 {"commerce", "inventory", "", "库存管理", "/inventory", "el-icon-document", "inventory:self", "1"},
                 {"commerce", "orderManage", "", "订单管理", "/order-manage", "el-icon-s-order", "commerce:orders,order:self", "2"},
@@ -9577,7 +9642,7 @@ public class DatabaseInitializer implements CommandLineRunner {
                 {"commerce", "promotion", "", "推广管理", "/promotion", "el-icon-s-marketing", "commerce:promotion", "7"},
                 {"commerce", "familyEarnings", "", "家庭收益", "/family-earnings", "el-icon-s-money", "finance:family-earnings", "8"},
                 // 知识中心(articles:*)
-                {"", "knowledge", "知识中心", "", "", "el-icon-document", "articles:*", "5"},
+                {"", "knowledge", "知识中心", "", "", "el-icon-document", "articles:*", "6"},
                 {"knowledge", "articleCategories", "", "知识分类", "/article-categories", "el-icon-folder", "articles:categories", "0"},
                 {"knowledge", "articleManage", "", "知识管理", "/article-manage", "el-icon-document", "articles:manage", "1"},
                 {"knowledge", "commentReview", "", "评论审核", "/comment-review", "el-icon-chat-dot-round", "articles:manage", "2"},
@@ -9585,11 +9650,11 @@ public class DatabaseInitializer implements CommandLineRunner {
                 {"knowledge", "knowledgeBase", "", "知识库", "/knowledge-base", "el-icon-reading", "knowledge:base", "4"},
                 {"knowledge", "healthKnowledge", "", "健康知识库", "/health-knowledge", "el-icon-first-aid-kit", "knowledge:base", "5"},
                 // 活动管理(activity:*,活动审核已移入审核中心)
-                {"", "activity", "活动管理", "", "", "el-icon-date", "activity:*", "6"},
+                {"", "activity", "活动管理", "", "", "el-icon-date", "activity:*", "7"},
                 {"activity", "activities", "", "活动列表", "/activities", "el-icon-date", "activity:list,activity:self", "0"},
                 {"activity", "activityRegReview", "", "活动报名审核", "/activity-registration-review", "el-icon-document-checked", "activity:registration", "1"},
                 // 健康饮食(health:*,能量规则/行为/周期性服务已移入系统配置·健康配置,LangGraph已移入报告解析系统;报告管理/报告审核/应季食材按方案挂载)
-                {"", "health", "健康饮食", "", "", "el-icon-first-aid-kit", "health:*", "7"},
+                {"", "health", "健康饮食", "", "", "el-icon-first-aid-kit", "health:*", "8"},
                 {"health", "healthReports", "", "健康报告", "/health-reports", "el-icon-document", "health:reports", "0"},
                 {"health", "reportManagement", "", "报告管理", "/report-management", "el-icon-document", "health:reports", "1"},
                 {"health", "reportAudit", "", "报告审核", "/report-audit", "el-icon-document-checked", "report:audit", "2"},
@@ -9603,22 +9668,22 @@ public class DatabaseInitializer implements CommandLineRunner {
                 {"health", "nutritionProducts", "", "营养产品", "/nutrition-products", "el-icon-first-aid-kit", "health:products", "10"},
                 {"health", "energySandbox", "", "五维能量", "/energy-sandbox", "el-icon-data-line", "health:energy", "11"},
                 // 报告解析系统(config:report-parser,LangGraph 管理归入)
-                {"", "reportParser", "报告解析系统", "", "", "el-icon-cpu", "config:report-parser", "8"},
+                {"", "reportParser", "报告解析系统", "", "", "el-icon-cpu", "config:report-parser", "9"},
                 {"reportParser", "reportTypes", "", "报告类型管理", "/report-types", "el-icon-collection", "config:report-parser", "0"},
                 {"reportParser", "reportParserImport", "", "解析器导入", "/report-parser-import", "el-icon-upload2", "config:report-parser", "1"},
                 {"reportParser", "reportUnknownClusters", "", "未知报告审核", "/report-unknown-clusters", "el-icon-question", "config:report-parser", "2"},
                 {"reportParser", "reportAutoLearn", "", "报告自学习", "/report-auto-learn", "el-icon-magic-stick", "config:report-parser", "3"},
                 {"reportParser", "langgraphAdmin", "", "LangGraph 管理", "/langgraph-admin", "el-icon-cpu", "config:report-parser", "4"},
                 // 家庭服务(service:*)
-                {"", "familyService", "家庭服务", "", "", "el-icon-s-custom", "service:*", "9"},
+                {"", "familyService", "家庭服务", "", "", "el-icon-s-custom", "service:*", "10"},
                 {"familyService", "myFamilies", "", "我的家庭", "/my-families", "el-icon-s-custom", "service:family", "0"},
                 {"familyService", "teacherTeam", "", "我的团队", "/teacher-team", "el-icon-s-custom", "service:team", "1"},
                 // 业务管理(biz:*)
-                {"", "biz", "业务管理", "", "", "el-icon-s-marketing", "biz:*", "10"},
+                {"", "biz", "业务管理", "", "", "el-icon-s-marketing", "biz:*", "11"},
                 {"biz", "teacherPackages", "", "任务模板管理", "/teacher-packages", "el-icon-s-goods", "biz:packages", "0"},
                 {"biz", "teacherOrders", "", "订单佣金", "/teacher-orders", "el-icon-s-order", "biz:orders", "1"},
                 // 测评咨询(测评管理/测评订单/待分配规划师已从系统配置移入)
-                {"", "assessment", "测评咨询", "", "", "el-icon-edit", "assessment:*", "11"},
+                {"", "assessment", "测评咨询", "", "", "el-icon-edit", "assessment:*", "12"},
                 {"assessment", "assessmentAdmin", "", "测评管理", "/assessment-admin", "el-icon-edit", "assessment:manage", "0"},
                 {"assessment", "assessmentOrders", "", "测评订单", "/assessment-orders", "el-icon-s-order", "assessment:orders", "1"},
                 {"assessment", "assessmentAssign", "", "待分配规划师", "/assessment-assign", "el-icon-user", "assessment:assign", "2"},
@@ -9627,7 +9692,7 @@ public class DatabaseInitializer implements CommandLineRunner {
                 {"assessment", "growthRecords", "", "成长记录", "/growth-records", "el-icon-document", "growth:records", "5"},
                 {"assessment", "growthPlans", "", "成长计划", "/growth-plans", "el-icon-document", "growth:plans", "6"},
                 // 系统配置(system:*,权限拆分 system:base / config:member / config:dimension / config:health)
-                {"", "system", "系统配置", "", "", "el-icon-s-tools", "system:*", "12"},
+                {"", "system", "系统配置", "", "", "el-icon-s-tools", "system:*", "13"},
                 {"system", "baseManage", "基础管理", "", "", "el-icon-s-tools", "system:base", "0"},
                 {"baseManage", "sysConfig", "", "系统配置", "/sys-config", "el-icon-s-tools", "system:base", "0"},
                 {"baseManage", "users", "", "用户管理", "/users", "el-icon-user", "system:users", "1"},

+ 13 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/task/TaskController.java

@@ -80,7 +80,19 @@ public class TaskController {
     @Operation(summary = "获取今日任务")
     @PostMapping("/today")
     public Result<List<Task>> getTodayTasks(@RequestBody Map<String, Object> params,
-                                            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+                                            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId,
+                                            @RequestAttribute(value = "familyId", required = false) Long familyId) {
+        // familyWide=true:按家庭聚合今日任务(家长查看方案分解到其他成员的任务)
+        boolean familyWide = params.get("familyWide") != null
+                && ("true".equalsIgnoreCase(String.valueOf(params.get("familyWide")))
+                    || Boolean.TRUE.equals(params.get("familyWide"))
+                    || "1".equals(String.valueOf(params.get("familyWide"))));
+        if (familyWide) {
+            if (familyId == null) {
+                return Result.error("familyId不能为空");
+            }
+            return Result.success(taskService.getTodayTasksByFamily(familyId));
+        }
         Long memberId = params.get("memberId") != null ? ParamUtils.getLong(params.get("memberId")) : currentMemberId;
         String category = params.get("category") != null ? params.get("category").toString() : null;
         String dimensionCode = params.get("dimensionCode") != null ? params.get("dimensionCode").toString() : null;

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthPlan.java

@@ -32,4 +32,8 @@ public class HealthPlan implements Serializable {
     /** 方案分解任务(非数据库字段,detail 接口填充) */
     @TableField(exist = false)
     private List<Task> tasks;
+
+    /** 家庭名称(非数据库字段,管理端列表填充) */
+    @TableField(exist = false)
+    private String familyName;
 }

+ 19 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/DailyOverviewService.java

@@ -44,7 +44,7 @@ public class DailyOverviewService {
         vo.setMemberId(memberId);
         vo.setDate(today);
         vo.setCheckins(buildCheckins(userId, memberId, today));
-        vo.setTasks(buildTasks(userId, memberId));
+        vo.setTasks(buildTasks(userId, memberId, familyId));
         vo.setActivities(buildActivities(memberId));
         vo.setAssessments(buildAssessments(memberId, familyId));
         vo.setHasMore(false);
@@ -96,18 +96,33 @@ public class DailyOverviewService {
 
     // ---------- 任务类 ----------
 
-    private DailyOverviewVO.TaskSummary buildTasks(Long userId, Long memberId) {
+    private DailyOverviewVO.TaskSummary buildTasks(Long userId, Long memberId, Long familyId) {
         DailyOverviewVO.TaskSummary s = new DailyOverviewVO.TaskSummary();
         s.setPendingAccept(0);
 
         List<Task> pending = taskService.getMyPendingTasks(memberId);
         s.setPendingAccept(pending == null ? 0 : pending.size());
 
+        // 有 familyId 时按家庭聚合(方案任务可能挂在任意成员);否则按当前成员
+        int todo = 0;
+        int done = 0;
+        List<Task> todayTasks = familyId != null
+                ? taskService.getTodayTasksByFamily(familyId)
+                : taskService.getTodayTasks(memberId);
+        if (todayTasks != null) {
+            for (Task t : todayTasks) {
+                if ("completed".equals(t.getStatus())) {
+                    done++;
+                } else if ("pending".equals(t.getStatus()) || "pending_review".equals(t.getStatus())
+                        || "in_progress".equals(t.getStatus()) || "locked".equals(t.getStatus())) {
+                    todo++;
+                }
+            }
+        }
+
         Map<String, String> params = new HashMap<>();
         params.put("type", "DAILY");
         List<Map<String, Object>> dailyList = growthTaskService.getTaskList(userId, params);
-        int todo = 0;
-        int done = 0;
         if (dailyList != null) {
             for (Map<String, Object> t : dailyList) {
                 Object c = t.get("completed");

+ 6 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/HealthPlanService.java

@@ -1,5 +1,6 @@
 package com.etotem.cfc.service;
 
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.entity.HealthPlan;
 import com.etotem.cfc.entity.Task;
 import java.util.List;
@@ -10,6 +11,8 @@ public interface HealthPlanService {
     Long confirmPlan(Long planId);
     List<HealthPlan> getPlanList(Long familyId);
     HealthPlan getPlanDetail(Long planId);
+    /** 管理端跨家庭分页查询健康方案 */
+    Page<HealthPlan> adminListPlans(Long familyId, String status, String keyword, int page, int size);
     String generatePlan(Long familyId, String memberIds, String dimensions, String goal);
     String regenerateSection(Long familyId, String section, String feedback, String planJsonStr);
     /** 规划师查看待审核方案列表 */
@@ -24,8 +27,9 @@ public interface HealthPlanService {
     List<Map<String, Object>> getAvailableTeachers(Long familyId);
 
     /**
-     * 报告确认入库后自动生成并发布健康方案(无需规划师审核)
-     * @return 已发布方案 ID,生成失败时返回 null
+     * 报告确认入库后自动生成健康方案(status=draft,不分解任务;
+     * 用户在方案页点击「确定」后由 confirmPlan 发布并分解任务)
+     * @return 方案 ID,生成失败时返回 null
      */
     Long autoGenerateAndPublish(Long familyId, Long memberId, String reportType);
 

+ 27 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java

@@ -336,6 +336,33 @@ public List<Task> getTodayTasks(Long memberId, String dimensionCode) {
         return filterLockedChildren(all);
     }
 
+    /**
+     * 家庭维度今日任务(家长端总览:方案分解任务可能挂在任意家庭成员上)
+     */
+    public List<Task> getTodayTasksByFamily(Long familyId) {
+        if (familyId == null) {
+            return new ArrayList<>();
+        }
+        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();
+
+        List<Task> all = taskMapper.selectList(new LambdaQueryWrapper<Task>()
+                .eq(Task::getFamilyId, familyId)
+                .ge(Task::getDeadline, startOfDay)
+                .lt(Task::getDeadline, endOfDay)
+                .in(Task::getStatus, Arrays.asList("pending", "completed", "locked", "pending_review"))
+                .orderByAsc(Task::getDeadline)
+                .orderByAsc(Task::getId));
+        return filterLockedChildren(all);
+    }
+
     /**
      * 移除"父任务未完成"的子任务(购买任务→每日使用子任务链)。
      * 子任务在父购买任务完成(status=completed)前保持锁定,不显示在今日任务中。

+ 45 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthPlanServiceImpl.java

@@ -1,6 +1,7 @@
 package com.etotem.cfc.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.HealthPlan;
 import com.etotem.cfc.entity.Task;
@@ -126,9 +127,49 @@ public class HealthPlanServiceImpl implements HealthPlanService {
         return healthPlanMapper.selectList(wrapper);
     }
 
+    @Override
+    public Page<HealthPlan> adminListPlans(Long familyId, String status, String keyword, int page, int size) {
+        Page<HealthPlan> pageParam = new Page<>(page, size);
+        QueryWrapper<HealthPlan> wrapper = new QueryWrapper<>();
+        if (familyId != null) {
+            wrapper.eq("family_id", familyId);
+        }
+        if (status != null && !status.trim().isEmpty()) {
+            wrapper.eq("status", status.trim());
+        }
+        if (keyword != null && !keyword.trim().isEmpty()) {
+            String kw = keyword.trim();
+            wrapper.and(w -> w.like("member_name", kw).or().like("goal", kw));
+        }
+        wrapper.orderByDesc("created_at");
+        Page<HealthPlan> result = healthPlanMapper.selectPage(pageParam, wrapper);
+        if (result.getRecords() != null && !result.getRecords().isEmpty()) {
+            for (HealthPlan plan : result.getRecords()) {
+                fillFamilyName(plan);
+            }
+        }
+        return result;
+    }
+
+    private void fillFamilyName(HealthPlan plan) {
+        if (plan == null || plan.getFamilyId() == null) {
+            return;
+        }
+        try {
+            Family family = familyMapper.selectById(plan.getFamilyId());
+            if (family != null) {
+                plan.setFamilyName(family.getName());
+            }
+        } catch (Exception e) {
+            log.warn("填充家庭名称失败 planId={}: {}", plan.getId(), e.getMessage());
+        }
+    }
+
     @Override
     public HealthPlan getPlanDetail(Long planId) {
-        return healthPlanMapper.selectById(planId);
+        HealthPlan plan = healthPlanMapper.selectById(planId);
+        fillFamilyName(plan);
+        return plan;
     }
 
     @Override
@@ -633,9 +674,10 @@ public class HealthPlanServiceImpl implements HealthPlanService {
         plan.setPlanContent(planContent);
         plan.setPlanJson(planJson);
         plan.setMemberName(memberName);
-        plan.setStatus("published");
+        // draft:方案已生成,待用户在方案页点击「确定」后再分解任务(不自动同步任务)
+        plan.setStatus("draft");
         Long planId = savePlan(plan);
-        log.info("报告确认后自动生成方案完成 planId={}, familyId={}, memberId={}, reportType={}",
+        log.info("报告确认后自动生成方案完成(待用户确认分解任务) planId={}, familyId={}, memberId={}, reportType={}",
                 planId, familyId, memberId, reportType);
         return planId;
     }

+ 107 - 15
cfc-frontend/pages/health-main/index.vue

@@ -251,8 +251,15 @@
             <text class="plan-title">📋 健康方案</text>
             <button class="plan-btn" @click="goPlanSummary">{{ lastPlan ? '查看方案' : '生成新方案' }}</button>
           </view>
+          <FamilyMemberStrip
+            v-if="familyMembers.length > 0"
+            :members="familyMembers"
+            :selectedMemberId="planMemberId"
+            :isParent="true"
+            @select="onPlanMemberSelect"
+          />
           <text v-if="lastPlan" class="plan-goal">{{ lastPlan }}</text>
-          <text v-else class="plan-hint">选维度 + 选成员 + 设定目标 → 生成</text>
+          <text v-else class="plan-hint">{{ planEmptyHint }}</text>
         </view>
       </view>
 
@@ -266,16 +273,20 @@ import LoginGuideCard from '../../components/LoginGuideCard.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import ReportUploadCard from '../../components/report-upload-card.vue'
+import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import { parseDate } from '../../utils/format.js'
 
 export default {
-  components: { LoginGuideCard, RadarChart, ArticleBookshelf, ReportUploadCard },
+  components: { LoginGuideCard, RadarChart, ArticleBookshelf, ReportUploadCard, FamilyMemberStrip },
   data() {
     return {
       isLoggedIn: false,
       currentTab: 'body',
       selectedMemberName: '',
       selectedMemberId: '',
+      planMemberId: null,
+      familyMembers: [],
+      allPlans: [],
       reports: [],
       lastPlan: '',
       lastPlanId: null,
@@ -325,6 +336,12 @@ export default {
     }
   },
   computed: {
+    planEmptyHint: function() {
+      if (this.planMemberId) {
+        return '该成员暂无健康方案,点击「生成新方案」开始'
+      }
+      return '选维度 + 选成员 + 设定目标 → 生成'
+    },
     currentTabLabel: function() {
       var tab = this.tabs.filter(function(t) { return t.code === this.currentTab }.bind(this))[0]
       return tab ? tab.label : ''
@@ -460,10 +477,15 @@ export default {
     if (this.isLoggedIn) {
       var stored = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || ''
       if (stored) {
-        this.selectedMemberId = stored
+        var n = parseInt(stored, 10)
+        this.selectedMemberId = isNaN(n) ? stored : n
         this.selectedMemberName = uni.getStorageSync('currentChildName') || '成员'
+        if (this.planMemberId == null) {
+          this.planMemberId = isNaN(n) ? null : n
+        }
       }
       this.loadHealthData()
+      this.loadFamilyMembersForPlan()
       this.loadPlanList()
       this.loadPendingSurveys()
     } else {
@@ -471,6 +493,71 @@ export default {
     }
   },
   methods: {
+    onPlanMemberSelect: function(member) {
+      if (!member || member.id == null) return
+      var id = typeof member.id === 'number' ? member.id : parseInt(member.id, 10)
+      if (isNaN(id)) id = member.id
+      if (this.planMemberId === id) return
+      this.planMemberId = id
+      this.selectedMemberId = id
+      this.selectedMemberName = member.nickname || '成员'
+      uni.setStorageSync('currentMemberId', id)
+      uni.setStorageSync('currentChildId', id)
+      this.applyPlanForMember()
+    },
+    loadFamilyMembersForPlan: function() {
+      var self = this
+      var api = require('../../utils/api.js')
+      api.getFamilyMemberList({ visibleOnly: true }).then(function(res) {
+        var list = (res && res.code === 200 && res.data) ? res.data : []
+        self.familyMembers = Array.isArray(list) ? list : []
+        if (self.planMemberId == null && self.familyMembers.length > 0) {
+          var first = self.familyMembers[0]
+          var fid = first && first.id != null ? first.id : null
+          if (fid != null) {
+            var pn = parseInt(fid, 10)
+            self.planMemberId = isNaN(pn) ? fid : pn
+          }
+        }
+        self.applyPlanForMember()
+      }).catch(function() {
+        self.familyMembers = []
+      })
+    },
+    /** memberIds 可能是 "1020" 或 "1020,1021",按成员匹配方案 */
+    planMatchesMember: function(plan, memberId) {
+      if (!plan || memberId == null) return false
+      var ids = String(plan.memberIds || '')
+      if (!ids) return false
+      var target = String(memberId)
+      var parts = ids.split(',')
+      for (var i = 0; i < parts.length; i++) {
+        if (String(parts[i]).trim() === target) return true
+      }
+      return false
+    },
+    applyPlanForMember: function() {
+      var list = this.allPlans || []
+      var memberId = this.planMemberId
+      var matched = null
+      if (memberId != null) {
+        for (var i = 0; i < list.length; i++) {
+          if (this.planMatchesMember(list[i], memberId)) {
+            matched = list[i]
+            break
+          }
+        }
+      } else if (list.length > 0) {
+        matched = list[0]
+      }
+      if (matched) {
+        this.lastPlan = matched.goal || matched.memberName || '已有健康方案'
+        this.lastPlanId = matched.id || null
+      } else {
+        this.lastPlan = ''
+        this.lastPlanId = null
+      }
+    },
     _cognitiveScore: function(key) {
       if (!this.cognitiveData) return 0
       var v = this.cognitiveData[key]
@@ -565,8 +652,13 @@ export default {
     },
     goPlanSummary: function() {
       var url = '/pages/health/health-plan-summary'
-      if (this.lastPlanId) url += '?planId=' + this.lastPlanId
-      if (this.selectedMemberId) url += (url.indexOf('?') >= 0 ? '&' : '?') + 'memberId=' + this.selectedMemberId
+      if (this.lastPlanId) {
+        url += '?planId=' + this.lastPlanId
+      }
+      var mid = this.planMemberId || this.selectedMemberId
+      if (mid) {
+        url += (url.indexOf('?') >= 0 ? '&' : '?') + 'memberId=' + mid
+      }
       uni.navigateTo({ url: url })
     },
     loadHealthData: function() {
@@ -635,15 +727,15 @@ export default {
       api.getHealthPlanList({}).then(function(res) {
         if (res && res.code === 200 && res.data) {
           var list = res.data.list || res.data
-          if (Array.isArray(list) && list.length > 0) {
-            self.lastPlan = list[0].goal || ''
-            self.lastPlanId = list[0].id || null
-          } else {
-            self.lastPlan = ''
-            self.lastPlanId = null
-          }
+          self.allPlans = Array.isArray(list) ? list : []
+        } else {
+          self.allPlans = []
         }
-      }).catch(function() {})
+        self.applyPlanForMember()
+      }).catch(function() {
+        self.allPlans = []
+        self.applyPlanForMember()
+      })
     },
     goArticleDetail: function(article) {
       var id = article && article.id || article
@@ -743,8 +835,8 @@ export default {
 .plan-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12rpx; }
 .plan-title { font-size: 28rpx; font-weight: bold; color: #333; }
 .plan-btn { background: linear-gradient(135deg, #F97316, #FB923C); color: #fff; font-size: 24rpx; border: none; border-radius: 20rpx; padding: 10rpx 28rpx; height: 56rpx; line-height: 36rpx; margin: 0; }
-.plan-goal { font-size: 24rpx; color: #666; line-height: 1.5; display: block; }
-.plan-hint { font-size: 22rpx; color: #999; display: block; }
+.plan-goal { font-size: 24rpx; color: #666; line-height: 1.5; display: block; margin-top: 12rpx; }
+.plan-hint { font-size: 22rpx; color: #999; display: block; margin-top: 12rpx; }
 /* ===== 游客态样式 ===== */
 .brand-strap { margin: 0 24rpx 16rpx; background: linear-gradient(135deg, #FFF7ED, rgba(249,115,22,0.08)); border-radius: 16rpx; padding: 20rpx 24rpx; }
 .brand-strap-text { font-size: 26rpx; color: #F97316; font-weight: 600; }

+ 38 - 304
cfc-frontend/pages/health/health-plan-summary.vue

@@ -233,6 +233,7 @@
         <text class="plan-meta-item" v-if="planDetail.memberName">成员:{{ planDetail.memberName }}</text>
         <text class="plan-meta-item" v-if="planDetail.createdAt">制定于 {{ formatTime(planDetail.createdAt) }}</text>
         <text class="plan-meta-item" v-if="planDetail.status === 'confirmed' || planDetail.status === 'published'">状态:已发布</text>
+        <text class="plan-meta-item" v-if="planDetail.status === 'draft'">状态:待确认</text>
         <text class="plan-meta-item" v-if="planDetail.status === 'pending_review'">状态:待规划师审核</text>
         <text class="plan-meta-item" v-if="planDetail.status === 'rejected'">状态:已驳回</text>
         <text class="plan-meta-item" v-if="planDetail.reviewComment && planDetail.status !== 'draft'">审核备注:{{ planDetail.reviewComment }}</text>
@@ -241,41 +242,10 @@
         <text class="plan-preview-text">{{ planDetail.planContent }}</text>
       </view>
 
-      <!-- 方案分解任务 -->
-      <view class="plan-tasks-section">
-        <view class="plan-tasks-header">
-          <text class="plan-tasks-title">📋 分解任务</text>
-          <text class="plan-tasks-count" v-if="planTasks.length > 0">{{ planTaskCompletedCount }}/{{ planTasks.length }} 已完成</text>
-        </view>
-        <view class="plan-tasks-loading" v-if="planTasksLoading">
-          <text class="plan-tasks-loading-text">加载任务中...</text>
-        </view>
-        <view class="plan-task-list" v-else-if="planTasks.length > 0">
-          <view class="plan-task-item" v-for="item in planTasks" :key="item.id">
-            <view class="plan-task-main">
-              <text class="plan-task-title">{{ item.title }}</text>
-              <view class="plan-task-tags">
-                <text class="plan-task-tag" v-if="item.category">{{ item.category }}</text>
-                <text class="plan-task-tag plan-task-tag-freq">{{ getTaskFreqText(item) }}</text>
-              </view>
-            </view>
-            <view class="plan-task-right">
-              <text class="plan-task-status" :class="'plan-task-status-' + getTaskStatusClass(item.status)">{{ getTaskStatusText(item.status) }}</text>
-              <view class="plan-task-actions" v-if="item.status !== 'completed' && item.status !== 'pending_review'">
-                <text class="plan-task-btn plan-task-btn-complete" @click.stop="completePlanTask(item)">打卡完成</text>
-                <text class="plan-task-btn plan-task-btn-go" @click.stop="goToTaskCenter">任务中心</text>
-              </view>
-            </view>
-          </view>
-        </view>
-        <view class="plan-tasks-empty" v-else>
-          <text class="plan-tasks-empty-text">{{ getPlanTasksEmptyText() }}</text>
-        </view>
-      </view>
-
-      <view class="plan-actions">
-        <button class="btn-edit" @click="reCreate" v-if="planDetail.status !== 'confirmed' && planDetail.status !== 'published' && planDetail.status !== 'pending_review'">重新制定</button>
-        <button class="btn-done plan-done-btn" @click="goBack">返回</button>
+      <!-- 待确认:点「确定」分解任务(任务在首页/任务中心查看,不在方案下方展示);「申请规划师」预留 -->
+      <view class="btn-pair plan-confirm-actions" v-if="needsTaskConfirm">
+        <button class="btn-confirm" :disabled="confirmingTasks" @click="confirmPlanTasks">{{ confirmingTasks ? '处理中...' : '确定' }}</button>
+        <button class="btn-teacher" @click="applyForTeacher">申请规划师</button>
       </view>
     </view>
 
@@ -292,7 +262,7 @@
 </template>
 
 <script>
-import { getFamilyMemberList, generateHealthPlan, regenerateSection, confirmPlan, saveHealthPlan, getHealthPlanDetail, getHealthPlanTasks, approvePlan, completeTask } from '../../utils/api'
+import { getFamilyMemberList, generateHealthPlan, regenerateSection, confirmPlan, saveHealthPlan, getHealthPlanDetail, approvePlan } from '../../utils/api'
 import { getHealthStatus, saveHealthStatus } from '../../utils/api'
 
 // v2: 预置疾病清单(菌群报告病症风险 ★ + 互联网统计常见病 两源合并去重,8 组 54 项)
@@ -408,8 +378,7 @@ export default {
       viewMode: false,
       planDetail: null,
       planId: null,
-      planTasks: [],
-      planTasksLoading: false,
+      confirmingTasks: false,
       // UI 状态
       loading: false,
       taskSubmitted: false
@@ -422,6 +391,9 @@ export default {
     allConfirmed: function() {
       return this.sections.nutrition.confirmed && this.sections.diet.confirmed && this.sections.exercise.confirmed
     },
+    needsTaskConfirm: function() {
+      return !!(this.planDetail && this.planDetail.status === 'draft')
+    },
     steps: function() { return ['选人', '问卷', '方案'] },
     sectionKeys: function() { return ['nutrition', 'diet', 'exercise'] },
     sectionTitles: function() { return ['营养补充建议', '饮食建议', '运动计划'] },
@@ -445,13 +417,6 @@ export default {
     },
     selectedNames: function() {
       return this.selectedDiseases.map(function(d) { return d.name })
-    },
-    planTaskCompletedCount: function() {
-      var count = 0
-      for (var i = 0; i < this.planTasks.length; i++) {
-        if (this.planTasks[i] && this.planTasks[i].status === 'completed') count++
-      }
-      return count
     }
   },
   onLoad: function(options) {
@@ -907,72 +872,45 @@ export default {
               self.planData.overview = res.data.planContent.substring(0, 200)
             }
           }
-          self.loadPlanTasksFromDetail(res.data)
         }
       }).catch(function() {
         uni.showToast({ title: '方案加载失败', icon: 'none' })
         setTimeout(function() { uni.navigateBack() }, 1500)
       })
     },
-    loadPlanTasksFromDetail: function(planData) {
-      if (planData && planData.tasks instanceof Array) {
-        this.planTasks = planData.tasks
-        this.planTasksLoading = false
-        return
-      }
-      this.loadPlanTasks()
-    },
-    loadPlanTasks: function() {
+    confirmPlanTasks: function() {
       var self = this
-      if (!self.planId) {
-        self.planTasks = []
-        return
-      }
-      self.planTasksLoading = true
-      getHealthPlanTasks({ planId: self.planId }).then(function(res) {
-        if (res && res.code === 200 && res.data instanceof Array) {
-          self.planTasks = res.data
+      if (!self.planId || self.confirmingTasks) return
+      self.confirmingTasks = true
+      uni.showLoading({ title: '正在分解任务...' })
+      confirmPlan({ planId: self.planId }).then(function(res) {
+        uni.hideLoading()
+        self.confirmingTasks = false
+        if (res && res.code === 200) {
+          // 同步当前查看成员为方案执行成员,便于首页/任务中心看到新任务
+          var planMemberId = self.getPlanMemberId(null)
+          if (planMemberId) {
+            uni.setStorageSync('currentMemberId', planMemberId)
+            uni.setStorageSync('currentChildId', planMemberId)
+          }
+          self.loadPlanDetail()
+          uni.showModal({
+            title: '提示',
+            content: '任务已生成,请前往首页-任务中打卡',
+            showCancel: false,
+            confirmText: '知道了'
+          })
         } else {
-          self.planTasks = []
+          uni.showToast({ title: (res && res.message) || '操作失败', icon: 'none' })
         }
       }).catch(function() {
-        self.planTasks = []
-      }).finally(function() {
-        self.planTasksLoading = false
+        uni.hideLoading()
+        self.confirmingTasks = false
+        uni.showToast({ title: '请求失败', icon: 'none' })
       })
     },
-    getTaskStatusText: function(status) {
-      var map = {
-        pending: '待完成',
-        in_progress: '进行中',
-        pending_review: '待审核',
-        completed: '已完成',
-        expired: '已过期',
-        rejected: '已驳回'
-      }
-      return map[status] || '待完成'
-    },
-    getTaskStatusClass: function(status) {
-      if (status === 'completed') return 'done'
-      if (status === 'pending_review') return 'review'
-      if (status === 'expired' || status === 'rejected') return 'warn'
-      return 'pending'
-    },
-    getTaskFreqText: function(task) {
-      if (!task) return '任务'
-      if (task.repeatType === 'none' || task.taskType === 'onetime') return '一次性'
-      if (task.frequency === 'weekly' || task.repeatType === 'weekly') return '每周'
-      if (task.frequency === 'monthly' || task.repeatType === 'monthly') return '每月'
-      return '每日'
-    },
-    getPlanTasksEmptyText: function() {
-      if (this.planDetail && (this.planDetail.status === 'published' || this.planDetail.status === 'confirmed')) {
-        return '方案已发布,暂未解析出可执行任务'
-      }
-      if (this.planDetail && this.planDetail.status === 'pending_review') {
-        return '方案待规划师审核,审核通过后将自动分解任务'
-      }
-      return '方案发布后,系统将自动分解为每日/每周任务'
+    applyForTeacher: function() {
+      // 预留:申请规划师接口与业务待定
     },
     getPlanMemberId: function(item) {
       if (item && item.familyMemberId) {
@@ -987,68 +925,6 @@ export default {
       var stored = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId')
       return stored || null
     },
-    completePlanTask: function(item) {
-      var self = this
-      if (!item || !item.id || item.status === 'completed' || item.status === 'pending_review') {
-        return
-      }
-      var memberId = self.getPlanMemberId(item)
-      if (!memberId) {
-        uni.showToast({ title: '未找到执行成员', icon: 'none' })
-        return
-      }
-      uni.showModal({
-        title: '完成任务',
-        content: '确定完成「' + item.title + '」吗?',
-        success: function(res) {
-          if (!res.confirm) return
-          completeTask(item.id, memberId, '').then(function(r) {
-            if (r && r.code === 200) {
-              var needReview = r.data && r.data.needReview
-              if (needReview) {
-                uni.showToast({ title: '已提交,待审核', icon: 'none' })
-              } else {
-                var points = (r.data && r.data.pointsEarned) || item.points || 0
-                uni.showToast({ title: '获得 ' + points + ' 积分', icon: 'success' })
-              }
-              self.loadPlanTasks()
-            } else {
-              uni.showToast({ title: (r && r.message) || '完成失败', icon: 'none' })
-            }
-          }).catch(function() {
-            uni.showToast({ title: '完成失败', icon: 'none' })
-          })
-        }
-      })
-    },
-    goToTaskCenter: function() {
-      uni.navigateTo({ url: '/pages/tasks/tasks' })
-    },
-    reCreate: function() {
-      this.step = 1
-      this.viewMode = false
-      this.planId = null
-      this.planDetail = null
-      this.taskSubmitted = false
-      this.editing = false
-      this.sectionFeedback = ''
-      this.selectedMemberIds = []
-      this.userGoal = ''
-      this.planData = {}
-      this.sections = {
-        nutrition: { content: '', confirmed: false },
-        diet: { content: '', confirmed: false },
-        exercise: { content: '', confirmed: false }
-      }
-      // 重置健康问卷
-      this.form = { heightCm: '', weightKg: '', bloodPressure: '', bloodGlucose: '', notes: '' }
-      this.lipidCholesterol = ''
-      this.selectedDiseases = []
-      this.diseaseInput = ''
-      this.medRows = [{ name: '', startDate: '', durationText: '', time: '' }]
-      this.expandedGroups = this.diseaseGroups.slice(0, 3).map(function(g) { return g.key })
-      this.loadExecutors()
-    },
     goBack: function() {
       uni.navigateBack()
     }
@@ -1425,150 +1301,8 @@ export default {
   line-height: 1.8;
   white-space: pre-wrap;
 }
-.plan-tasks-section {
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 24rpx;
+.plan-confirm-actions {
   margin-bottom: 20rpx;
-  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
-}
-.plan-tasks-header {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 16rpx;
-}
-.plan-tasks-title {
-  font-size: 30rpx;
-  font-weight: bold;
-  color: #333;
-}
-.plan-tasks-count {
-  font-size: 22rpx;
-  color: #64748B;
-}
-.plan-tasks-loading {
-  padding: 24rpx 0;
-  display: flex;
-  justify-content: center;
-}
-.plan-tasks-loading-text {
-  font-size: 24rpx;
-  color: #999;
-}
-.plan-task-list {
-  display: flex;
-  flex-direction: column;
-  gap: 12rpx;
-}
-.plan-task-item {
-  display: flex;
-  flex-direction: row;
-  align-items: flex-start;
-  justify-content: space-between;
-  padding: 20rpx;
-  background: #F8FAFC;
-  border-radius: 12rpx;
-  border: 1rpx solid #E2E8F0;
-}
-.plan-task-right {
-  display: flex;
-  flex-direction: column;
-  align-items: flex-end;
-  gap: 12rpx;
-  flex-shrink: 0;
-  margin-left: 12rpx;
-}
-.plan-task-actions {
-  display: flex;
-  flex-direction: column;
-  align-items: flex-end;
-  gap: 8rpx;
-}
-.plan-task-btn {
-  font-size: 22rpx;
-  padding: 8rpx 18rpx;
-  border-radius: 20rpx;
-}
-.plan-task-btn-complete {
-  color: #fff;
-  background: #F97316;
-}
-.plan-task-btn-go {
-  color: #0369A1;
-  background: #E0F2FE;
-}
-.plan-task-main {
-  flex: 1;
-  min-width: 0;
-  padding-right: 16rpx;
-}
-.plan-task-title {
-  font-size: 26rpx;
-  color: #1E293B;
-  line-height: 1.5;
-}
-.plan-task-tags {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  gap: 8rpx;
-  margin-top: 8rpx;
-}
-.plan-task-tag {
-  font-size: 20rpx;
-  color: #0369A1;
-  background: #E0F2FE;
-  padding: 4rpx 12rpx;
-  border-radius: 16rpx;
-}
-.plan-task-tag-freq {
-  color: #92400E;
-  background: #FEF3C7;
-}
-.plan-task-status {
-  flex-shrink: 0;
-  font-size: 22rpx;
-  padding: 6rpx 16rpx;
-  border-radius: 20rpx;
-}
-.plan-task-status-pending {
-  color: #B45309;
-  background: #FEF3C7;
-}
-.plan-task-status-done {
-  color: #047857;
-  background: #D1FAE5;
-}
-.plan-task-status-review {
-  color: #92400E;
-  background: #FFEDD5;
-}
-.plan-task-status-warn {
-  color: #B91C1C;
-  background: #FEE2E2;
-}
-.plan-tasks-empty {
-  padding: 24rpx 0 8rpx;
-}
-.plan-tasks-empty-text {
-  font-size: 24rpx;
-  color: #94A3B8;
-  line-height: 1.6;
-  text-align: center;
-}
-.plan-actions { display: flex; gap: 16rpx; }
-.plan-actions .btn-edit { flex: 1; margin-bottom: 0; }
-.plan-done-btn {
-  flex: 1;
-  height: 80rpx;
-  line-height: 80rpx;
-  background: #F97316;
-  color: #fff;
-  font-size: 28rpx;
-  border-radius: 40rpx;
-  border: none;
 }
 
 /* 成功遮罩 */

+ 16 - 1
cfc-frontend/pages/index-home/index.vue

@@ -668,6 +668,20 @@ export default {
     },
     loadTodayTasks: function() {
       var self = this
+      var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
+      // 家长:家庭维度拉取(方案任务可能挂在任意成员,不只是当前孩子)
+      if (role === 'parent') {
+        getTodayTasks(null, { familyWide: true }).then(function(res) {
+          if (res.code === 200 && Array.isArray(res.data)) {
+            self.pendingTasks = res.data.filter(function(t) {
+              return t.status === 'pending'
+            })
+          }
+        }).catch(function() {
+          self.pendingTasks = []
+        })
+        return
+      }
       if (!self.currentChildId) {
         self.pendingTasks = []
         return
@@ -777,7 +791,8 @@ export default {
     },
     quickCompleteTask: function(task) {
       var self = this
-      completeTaskApi(task.id, self.currentChildId, '').then(function(res) {
+      var memberId = (task && task.familyMemberId) || self.currentChildId || uni.getStorageSync('currentMemberId')
+      completeTaskApi(task.id, memberId, '').then(function(res) {
         if (res.code === 200) {
           uni.showToast({ title: '获得 ' + (res.data && res.data.pointsEarned || 0) + ' 积分', icon: 'success' })
           self.loadTodayTasks()

+ 294 - 224
cfc-frontend/pages/tasks/tasks.vue

@@ -1,6 +1,15 @@
 <template>
   <view class="container">
-    <!-- 顶部 Tab 切换 -->
+    <!-- 顶部:家庭成员选择(选择谁就展示谁的任务) -->
+    <FamilyMemberStrip
+      v-if="familyMembers.length > 0"
+      :members="familyMembers"
+      :selectedMemberId="currentMemberId"
+      :isParent="isParent"
+      @select="onFamilyMemberSelect"
+    />
+
+    <!-- Tab 切换 -->
     <view class="tab-bar">
       <view class="tab-item" :class="{ active: activeTab === 'pending' }" @click="switchTab('pending')">
         <text class="tab-text">待接受</text>
@@ -186,8 +195,10 @@
 <script>
 import { getMyPendingTasks, acceptTask as acceptTaskApi, rejectTask as rejectTaskApi, getTodayTasks, completeTask as completeTaskApi, startTask as startTaskApi, createTask as createTaskApi, getFamilyMemberList, getMyMembership } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
+import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 
 export default {
+  components: { FamilyMemberStrip },
   data() {
     return {
       activeTab: 'today',
@@ -217,15 +228,47 @@ export default {
       categories: ['学习类', '生活类', '运动类', '其他']
     }
   },
+  onLoad(options) {
+    if (options && options.memberId) {
+      var mid = parseInt(options.memberId, 10)
+      if (!isNaN(mid)) {
+        this.currentMemberId = mid
+        uni.setStorageSync('currentMemberId', mid)
+        uni.setStorageSync('currentChildId', mid)
+      }
+    }
+  },
   onShow() {
     const role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
     this.isParent = role === 'parent'
     this.currentUserId = uni.getStorageSync('userId')
-    this.currentMemberId = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId')
+    if (this.currentMemberId == null) {
+      var cached = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId')
+      if (cached !== '' && cached != null) {
+        var n = parseInt(cached, 10)
+        this.currentMemberId = isNaN(n) ? cached : n
+      }
+    }
     this.checkMembership()
     this.loadInitialData()
   },
   methods: {
+    onFamilyMemberSelect(member) {
+      if (!member || member.id == null) return
+      var id = typeof member.id === 'number' ? member.id : parseInt(member.id, 10)
+      if (isNaN(id)) id = member.id
+      if (this.currentMemberId === id) return
+      this.currentMemberId = id
+      uni.setStorageSync('currentMemberId', id)
+      uni.setStorageSync('currentChildId', id)
+      this.loadTodayTasks()
+      this.loadPendingTasks()
+    },
+    resolveTaskMemberId(task) {
+      if (task && task.familyMemberId) return task.familyMemberId
+      if (task && task.childId) return task.childId
+      return this.currentMemberId
+    },
     checkMembership() {
       getMyMembership().then(res => {
         const m = res.data || {}
@@ -242,13 +285,16 @@ export default {
       this.loading = true
       try {
         const membersRes = await getFamilyMemberList({ visibleOnly: true })
-        this.familyMembers = membersRes.data || []
-        if (!this.currentMemberId && this.familyMembers.length > 0) {
-          var first = this.familyMembers[0]
-          this.currentMemberId = first && first.id ? first.id : null
+        var raw = (membersRes && membersRes.data) || []
+        this.familyMembers = Array.isArray(raw) ? raw : []
+        this.ensureSelectedMemberFromRaw(this.familyMembers)
+        if (!this.currentMemberId) {
+          this.todayTasks = []
+          this.pendingTasks = []
+        } else {
+          this.loadTodayTasks()
+          this.loadPendingTasks()
         }
-        this.loadTodayTasks()
-        this.loadPendingTasks()
         this.loadCreatedTasks()
       } catch (e) {
         console.error('加载数据失败', e)
@@ -256,7 +302,39 @@ export default {
         this.loading = false
       }
     },
+    ensureSelectedMemberFromRaw(raw) {
+      var members = raw || []
+      if (members.length === 0) {
+        this.currentMemberId = null
+        return
+      }
+      if (this.currentMemberId != null) {
+        var cur = String(this.currentMemberId)
+        for (var i = 0; i < members.length; i++) {
+          if (members[i] && String(members[i].id) === cur) {
+            // 统一成数字,保证 FamilyMemberStrip 选中态 === 比较成立
+            var n = parseInt(members[i].id, 10)
+            this.currentMemberId = isNaN(n) ? members[i].id : n
+            return
+          }
+        }
+      }
+      var first = members[0]
+      var fid = first && first.id != null ? first.id : null
+      if (fid != null) {
+        var pn = parseInt(fid, 10)
+        this.currentMemberId = isNaN(pn) ? fid : pn
+        uni.setStorageSync('currentMemberId', this.currentMemberId)
+        uni.setStorageSync('currentChildId', this.currentMemberId)
+      } else {
+        this.currentMemberId = null
+      }
+    },
     async loadPendingTasks() {
+      if (!this.currentMemberId) {
+        this.pendingTasks = []
+        return
+      }
       try {
         const res = await getMyPendingTasks(this.currentMemberId)
         this.pendingTasks = res.data || []
@@ -265,6 +343,10 @@ export default {
       }
     },
     async loadTodayTasks() {
+      if (!this.currentMemberId) {
+        this.todayTasks = []
+        return
+      }
       try {
         const res = await getTodayTasks(this.currentMemberId)
         this.todayTasks = res.data || []
@@ -295,7 +377,8 @@ export default {
         return
       }
       try {
-        const res = await completeTaskApi(task.id, this.currentMemberId, '')
+        var memberId = this.resolveTaskMemberId(task)
+        const res = await completeTaskApi(task.id, memberId, '')
         if (res.data && res.data.needReview) {
           uni.showToast({ title: '已提交,待审核', icon: 'none' })
         } else {
@@ -330,7 +413,7 @@ export default {
       // 两阶段:先开始,再按 action_type 跳转联动页面
       if (task.startRequired === 1 && task.status !== 'in_progress') {
         uni.showLoading({ title: '开始中...' })
-        startTaskApi(task.id, this.currentMemberId).then(function(res) {
+        startTaskApi(task.id, this.resolveTaskMemberId(task)).then(function(res) {
           uni.hideLoading()
           if (res && res.code === 200 && res.data) {
             uni.showToast({ title: '已开始', icon: 'success' })
@@ -360,22 +443,18 @@ export default {
       var at = task.actionType
       var taskId = task.id
       if (at === 'minigame') {
-        var code = 'schulte'
-        try {
-          if (task.actionConfig) {
-            var cfg = JSON.parse(task.actionConfig)
-            if (cfg.gameCode) code = cfg.gameCode
-          }
-        } catch (e) {}
-        uni.navigateTo({ url: '/pages/games/' + code + '?taskId=' + taskId })
+        var cfg = {}
+        try { cfg = typeof task.actionConfig === 'string' ? JSON.parse(task.actionConfig || '{}') : (task.actionConfig || {}) } catch (e) {}
+        var gameId = cfg.gameId || ''
+        uni.navigateTo({ url: '/pages/games/index?taskId=' + taskId + (gameId ? '&gameId=' + gameId : '') })
       } else if (at === 'focus') {
-        uni.navigateTo({ url: '/pages/focus/index?taskId=' + taskId })
+        uni.navigateTo({ url: '/pages/games/focus-timer?taskId=' + taskId })
       } else if (at === 'survey') {
-        uni.navigateTo({ url: '/pages/assessment/survey-questionnaire?taskId=' + taskId })
+        uni.navigateTo({ url: '/pages/survey/index?taskId=' + taskId })
       } else if (at === 'article') {
-        uni.navigateTo({ url: '/pages/article-center/article-detail?taskId=' + taskId })
+        uni.navigateTo({ url: '/pages/article-center/index?taskId=' + taskId })
       } else if (at === 'activity') {
-        uni.navigateTo({ url: '/pages/activity/activity-list?taskId=' + taskId })
+        uni.navigateTo({ url: '/pages/activity/list?taskId=' + taskId })
       } else if (at === 'cart' || at === 'order') {
         uni.navigateTo({ url: '/pages/shop/cart?taskId=' + taskId })
       }
@@ -389,7 +468,7 @@ export default {
       var task = this.activeInputTask
       if (!task || !this.inputContent) return
       try {
-        const res = await completeTaskApi(task.id, this.currentMemberId, '', this.inputContent, 'text')
+        const res = await completeTaskApi(task.id, this.resolveTaskMemberId(task), '', this.inputContent, 'text')
         if (res.data && res.data.needReview) {
           uni.showToast({ title: '已提交,待审核', icon: 'none' })
         } else {
@@ -419,15 +498,12 @@ export default {
       uni.showLoading({ title: '接受中...' })
       acceptTaskApi(task.id, this.currentMemberId).then(res => {
         uni.hideLoading()
-        if (res.data) {
-          uni.showToast({ title: '已接受任务', icon: 'success' })
-          this.loadPendingTasks()
-          this.loadTodayTasks()
-        } else {
-          uni.showToast({ title: '接受失败', icon: 'none' })
-        }
+        uni.showToast({ title: '已接受', icon: 'success' })
+        this.loadPendingTasks()
+        this.loadTodayTasks()
       }).catch(e => {
         uni.hideLoading()
+        console.error('接受失败', e)
         uni.showToast({ title: '接受失败', icon: 'none' })
       })
     },
@@ -436,24 +512,46 @@ export default {
       this.showRejectModalFlag = true
     },
     confirmReject() {
-      uni.showLoading({ title: '处理中...' })
+      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' })
-        }
+        this.showRejectModalFlag = false
+        uni.showToast({ title: '已拒绝', icon: 'success' })
+        this.loadPendingTasks()
       }).catch(e => {
         uni.hideLoading()
-        uni.showToast({ title: '操作失败', icon: 'none' })
+        console.error('拒绝失败', e)
+        uni.showToast({ title: '拒绝失败', icon: 'none' })
       })
-      this.showRejectModalFlag = false
+    },
+    statusLabel(status) {
+      const map = {
+        pending: '待完成',
+        assigned: '待接受',
+        in_progress: '进行中',
+        completed: '已完成',
+        pending_review: '待审核',
+        rejected: '已拒绝',
+        overdue: '已过期',
+        cancelled: '已取消',
+        locked: '未解锁'
+      }
+      return map[status] || status
+    },
+    formatTime(t) {
+      if (!t) return ''
+      var d = parseDate(t)
+      if (!d) return String(t).substring(0, 16)
+      var y = d.getFullYear()
+      var m = String(d.getMonth() + 1).padStart(2, '0')
+      var day = String(d.getDate()).padStart(2, '0')
+      var h = String(d.getHours()).padStart(2, '0')
+      var min = String(d.getMinutes()).padStart(2, '0')
+      return y + '-' + m + '-' + day + ' ' + h + ':' + min
     },
     async createTask() {
       if (!this.newTask.title) {
-        uni.showToast({ title: '请输入任务标题', icon: 'none' })
+        uni.showToast({ title: '请输入标题', icon: 'none' })
         return
       }
       if (!this.newTask.category) {
@@ -501,25 +599,7 @@ export default {
       this.newTask.deadline = e.detail.value
     },
     onFamilyMemberSelectChange(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
+      this.newTask.memberIds = e.detail.value || []
     }
   }
 }
@@ -527,13 +607,13 @@ export default {
 
 <style scoped>
 .container {
-  padding: 30rpx;
-  padding-bottom: 120rpx;
+  padding: 20rpx 30rpx 120rpx;
   background: #f5f7fa;
   min-height: 100vh;
 }
 
 .tab-bar {
+  margin-top: 16rpx;
   display: flex;
   background: #fff;
   border-radius: 16rpx;
@@ -559,24 +639,26 @@ export default {
 .tab-text {
   font-size: 28rpx;
   color: #666;
-  font-weight: 500;
 }
 
 .tab-item.active .tab-text {
   color: #fff;
+  font-weight: 600;
 }
 
 .tab-badge {
   position: absolute;
   top: 8rpx;
-  right: 30rpx;
-  background: #FF4D4F;
+  right: 20rpx;
+  background: #EF4444;
   color: #fff;
   font-size: 20rpx;
-  padding: 2rpx 10rpx;
-  border-radius: 20rpx;
-  min-width: 32rpx;
+  min-width: 28rpx;
+  height: 28rpx;
+  line-height: 28rpx;
   text-align: center;
+  border-radius: 14rpx;
+  padding: 0 6rpx;
 }
 
 .task-list {
@@ -586,187 +668,181 @@ export default {
 }
 
 .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;
+  justify-content: space-between;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
 }
 
-.task-item.assigned {
-  border: 2rpx solid #F97316;
+.task-item.completed {
+  opacity: 0.7;
 }
 
-.task-item.completed {
-  opacity: 0.6;
+.task-item.locked {
+  opacity: 0.55;
+}
+
+.task-item.assigned {
+  border-left: 6rpx solid #F97316;
 }
 
 .task-left {
   flex: 1;
+  min-width: 0;
+  padding-right: 20rpx;
 }
 
 .task-title {
   font-size: 30rpx;
-  font-weight: bold;
   color: #333;
-  margin-bottom: 10rpx;
+  font-weight: 500;
+  margin-bottom: 12rpx;
+}
+
+.member-badge {
+  font-size: 20rpx;
+  color: #F97316;
+  background: #FFF7ED;
+  padding: 2rpx 10rpx;
+  border-radius: 8rpx;
+  margin-left: 8rpx;
+}
+
+.minigame-badge {
+  margin-left: 8rpx;
+  font-size: 24rpx;
 }
 
 .task-meta {
   display: flex;
-  gap: 20rpx;
   flex-wrap: wrap;
+  gap: 16rpx;
+  font-size: 24rpx;
+  color: #999;
 }
 
 .points {
   color: #F97316;
-  font-size: 24rpx;
   font-weight: 500;
 }
 
-.creator, .assignee {
+.deadline, .creator, .assignee {
   color: #999;
-  font-size: 24rpx;
 }
 
-.deadline {
-  color: #999;
-  font-size: 24rpx;
-}
-
-.status-tag {
+.lock-tip {
+  display: block;
+  margin-top: 8rpx;
   font-size: 22rpx;
-  padding: 4rpx 12rpx;
-  border-radius: 12rpx;
-  background: #f0f0f0;
-  color: #666;
+  color: #9CA3AF;
 }
 
-.status-assigned {
-  background: #FFF7ED;
-  color: #F97316;
-}
-
-.status-in_progress {
-  background: #ECFDF5;
-  color: #10B981;
+.task-right {
+  flex-shrink: 0;
 }
 
-.status-rejected {
-  background: #FEF2F2;
-  color: #EF4444;
+.btn-complete {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+  font-size: 26rpx;
+  padding: 0 28rpx;
+  height: 64rpx;
+  line-height: 64rpx;
+  border-radius: 32rpx;
+  border: none;
 }
 
-.task-right {
-  margin-left: 20rpx;
+.task-actions {
+  display: flex;
+  gap: 12rpx;
+  flex-shrink: 0;
 }
 
-.btn-complete, .btn-accept {
-  background: linear-gradient(135deg, #F97316, #FB923C);
+.btn-accept {
+  background: #10B981;
   color: #fff;
-  font-size: 26rpx;
-  padding: 12rpx 30rpx;
-  border-radius: 30rpx;
+  font-size: 24rpx;
+  padding: 0 24rpx;
+  height: 56rpx;
+  line-height: 56rpx;
+  border-radius: 28rpx;
   border: none;
 }
 
 .btn-reject {
-  background: #fff;
-  color: #EF4444;
-  font-size: 26rpx;
-  padding: 12rpx 30rpx;
-  border-radius: 30rpx;
-  border: 2rpx solid #EF4444;
-  margin-left: 16rpx;
+  background: #F3F4F6;
+  color: #666;
+  font-size: 24rpx;
+  padding: 0 24rpx;
+  height: 56rpx;
+  line-height: 56rpx;
+  border-radius: 28rpx;
+  border: none;
 }
 
-.task-actions {
-  display: flex;
-  align-items: center;
+.completed-badge, .review-badge, .lock-badge {
+  font-size: 24rpx;
+  padding: 8rpx 16rpx;
+  border-radius: 12rpx;
+  flex-shrink: 0;
 }
 
 .completed-badge {
-  color: #52C41A;
-  font-size: 26rpx;
+  background: #ECFDF5;
+  color: #10B981;
 }
 
 .review-badge {
-  color: #D97706;
-  font-size: 26rpx;
+  background: #FFF7ED;
+  color: #F97316;
 }
 
 .lock-badge {
-  color: #B0B0B0;
-  font-size: 26rpx;
+  background: #F3F4F6;
+  color: #9CA3AF;
 }
 
-.lock-tip {
-  display: block;
-  margin-top: 8rpx;
+.status-tag {
   font-size: 22rpx;
-  color: #B0B0B0;
-}
-
-.task-item.locked {
-  opacity: 0.6;
-}
-
-.input-tip {
-  display: block;
-  font-size: 26rpx;
+  padding: 2rpx 10rpx;
+  border-radius: 8rpx;
+  background: #F3F4F6;
   color: #666;
-  margin-bottom: 16rpx;
 }
 
-.input-textarea {
-  width: 100%;
-  height: 240rpx;
-  background: #F7F7F7;
-  border-radius: 16rpx;
-  padding: 20rpx;
-  font-size: 26rpx;
-  box-sizing: border-box;
-}
-
-.minigame-badge {
-  font-size: 28rpx;
-  margin-left: 10rpx;
-}
-
-.member-badge {
-  display: inline-block;
-  font-size: 20rpx;
-  color: #B45309;
-  background: #FEF3C7;
-  border: 1rpx solid #F59E0B;
-  border-radius: 8rpx;
-  padding: 2rpx 12rpx;
-  margin-left: 12rpx;
-  vertical-align: middle;
-}
+.status-pending { background: #FFF7ED; color: #F97316; }
+.status-completed { background: #ECFDF5; color: #10B981; }
+.status-pending_review { background: #FEF3C7; color: #D97706; }
+.status-assigned { background: #EEF2FF; color: #6366F1; }
+.status-in_progress { background: #DBEAFE; color: #2563EB; }
+.status-rejected { background: #FEE2E2; color: #EF4444; }
+.status-locked { background: #F3F4F6; color: #9CA3AF; }
 
 .empty {
-  text-align: center;
-  color: #999;
-  padding: 100rpx 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 80rpx 0;
+  color: #9CA3AF;
   font-size: 28rpx;
 }
 
 .add-btn {
   position: fixed;
+  left: 40rpx;
+  right: 40rpx;
   bottom: 40rpx;
-  left: 30rpx;
-  right: 30rpx;
+  height: 88rpx;
+  line-height: 88rpx;
   background: linear-gradient(135deg, #F97316, #FB923C);
   color: #fff;
   font-size: 30rpx;
-  font-weight: bold;
-  padding: 24rpx 0;
-  border-radius: 40rpx;
+  border-radius: 44rpx;
   border: none;
-  box-shadow: 0 8rpx 24rpx rgba(249,115,22,0.3);
+  box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.35);
 }
 
 .modal-mask {
@@ -775,7 +851,7 @@ export default {
   left: 0;
   right: 0;
   bottom: 0;
-  background: rgba(0, 0, 0, 0.5);
+  background: rgba(0, 0, 0, 0.45);
   display: flex;
   align-items: center;
   justify-content: center;
@@ -812,6 +888,23 @@ export default {
   text-align: center;
 }
 
+.input-tip {
+  display: block;
+  margin-bottom: 16rpx;
+  color: #333;
+  font-weight: 500;
+}
+
+.input-textarea {
+  width: 100%;
+  min-height: 180rpx;
+  padding: 20rpx;
+  background: #F9FAFB;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+}
+
 .modal-btns {
   display: flex;
   gap: 20rpx;
@@ -833,6 +926,11 @@ export default {
   color: #666;
 }
 
+.btn-primary {
+  background: linear-gradient(135deg, #F97316, #FB923C) !important;
+  color: #fff !important;
+}
+
 .form-item {
   margin-bottom: 28rpx;
 }
@@ -848,6 +946,21 @@ export default {
   color: #F97316;
 }
 
+.form-item input,
+.form-item .picker,
+.form-item .textarea {
+  width: 100%;
+  padding: 20rpx;
+  background: #F9FAFB;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+}
+
+.form-item .textarea {
+  min-height: 140rpx;
+}
+
 .form-item checkbox-group {
   display: flex;
   flex-wrap: wrap;
@@ -857,58 +970,15 @@ export default {
 .form-item label {
   display: flex;
   align-items: center;
-  gap: 10rpx;
-  font-size: 28rpx;
-  color: #333;
-  padding: 18rpx 28rpx;
-  border: 1rpx solid #ddd;
-  border-radius: 16rpx;
-  background: #fafafa;
-  transition: all 0.25s ease;
+  gap: 8rpx;
+  padding: 12rpx 20rpx;
+  background: #F9FAFB;
+  border-radius: 12rpx;
+  font-size: 26rpx;
 }
 
 .form-item label.label-checked {
-  border-color: #F97316;
   background: #FFF7ED;
   color: #F97316;
 }
-
-.form-item label:active {
-  transform: scale(0.97);
-}
-
-.form-item input, .form-item .picker {
-  border: 1rpx solid #ddd;
-  border-radius: 16rpx;
-  padding: 24rpx 28rpx;
-  font-size: 28rpx;
-  width: 100%;
-  box-sizing: border-box;
-  background: #fafafa;
-  min-height: 88rpx;
-  transition: all 0.25s ease;
-}
-
-.form-item input:focus {
-  border-color: #F97316;
-  background: #fff;
-  box-shadow: 0 0 0 4rpx rgba(249, 115, 22, 0.1);
-}
-
-.form-item .picker {
-  color: #333;
-}
-
-.textarea {
-  min-height: 160rpx;
-  width: 100%;
-  box-sizing: border-box;
-  font-size: 28rpx;
-  padding: 24rpx 28rpx;
-  border: 1rpx solid #ddd;
-  border-radius: 16rpx;
-  background: #fafafa;
-  transition: all 0.25s ease;
-}
-
 </style>

+ 9 - 2
cfc-frontend/utils/api.js

@@ -407,8 +407,15 @@ export const createTask = (data) => {
   return request('/api/tasks', 'POST', data)
 }
 
-export const getTodayTasks = (memberId) => {
-  return request('/api/tasks/today', 'POST', { memberId })
+export const getTodayTasks = (memberId, options) => {
+  var body = {}
+  if (memberId != null && memberId !== '') {
+    body.memberId = memberId
+  }
+  if (options && options.familyWide) {
+    body.familyWide = true
+  }
+  return request('/api/tasks/today', 'POST', body)
 }
 
 export const getPendingReviewTasks = () => {

+ 18 - 0
cfc-web/src/api/healthPlan.js

@@ -39,3 +39,21 @@ export function getHealthPlanDetail(data) {
     data
   })
 }
+
+/** 管理端:跨家庭健康方案分页列表 */
+export function adminListHealthPlans(data) {
+  return request({
+    url: '/api/admin/health-plan/list',
+    method: 'post',
+    data
+  })
+}
+
+/** 管理端:健康方案详情 */
+export function adminGetHealthPlanDetail(data) {
+  return request({
+    url: '/api/admin/health-plan/detail',
+    method: 'post',
+    data
+  })
+}

+ 13 - 0
cfc-web/src/router/index.js

@@ -72,6 +72,19 @@ const routes = [
         component: () => import('@/views/admin/MembershipCenter.vue'),
         meta: { title: '会员中心', perm: 'config:member' }
       },
+      // 方案中心
+      {
+        path: 'health-plan-manage',
+        name: 'HealthPlanManage',
+        component: () => import('@/views/admin/HealthPlanManage.vue'),
+        meta: { title: '健康方案管理', perm: 'plan:health' }
+      },
+      {
+        path: 'assessment-plan-manage',
+        name: 'AssessmentPlanManage',
+        component: () => import('@/views/admin/AssessmentPlanManage.vue'),
+        meta: { title: '测评方案管理', perm: 'plan:assessment' }
+      },
       // 任务管理
       {
         path: 'tasks',

+ 4 - 4
cfc-web/src/utils/permissions.js

@@ -10,18 +10,18 @@ const ROLE_PERMISSIONS = {
   // 超管:全部权限
   admin: ['*'],
   // 运营:审核全量、活动全量、任务全量、家庭/商城/知识/测评只读
-  operator: ['dashboard', 'audit:*', 'activity:*', 'task:*', 'assessment:read', 'family:read', 'commerce:read', 'articles:read', 'knowledge:read'],
+  operator: ['dashboard', 'audit:*', 'activity:*', 'task:*', 'assessment:read', 'family:read', 'commerce:read', 'articles:read', 'knowledge:read', 'plan:*'],
   // 财务:财务/退款/提现、订单只读、CF值、商城营销、业务管理
   finance: ['dashboard', 'finance:*', 'audit:withdraw', 'audit:refund', 'commerce:read', 'commerce:orders:read', 'biz:*', 'cf-value:*'],
   // 管家:家庭客服 + 规划师 + 营养师权限并集(getEffectivePermissions 中自动并入 teacher/nutritionist)
   butler: ['dashboard', 'family:*', 'task:*', 'reward:*', 'service:*', 'growth:*', 'biz:*',
-           'assessment:dan', 'health:*', 'diet:*', 'report:audit'],
+           'assessment:dan', 'health:*', 'diet:*', 'report:audit', 'plan:*'],
   // 知识管理员
   knowledge_admin: ['dashboard', 'articles:*', 'knowledge:*'],
   // 成长规划师
-  teacher: ['dashboard', 'service:*', 'growth:*', 'biz:*', 'assessment:dan', 'family:bound', 'messages'],
+  teacher: ['dashboard', 'service:*', 'growth:*', 'biz:*', 'assessment:dan', 'family:bound', 'messages', 'plan:health'],
   // 营养师
-  nutritionist: ['dashboard', 'health:*', 'diet:*', 'report:audit', 'service:family', 'biz:orders', 'messages'],
+  nutritionist: ['dashboard', 'health:*', 'diet:*', 'report:audit', 'service:family', 'biz:orders', 'messages', 'plan:*'],
   // 供应商(仅自己的商品/库存/订单/结算)+ 业务管理
   supplier: ['dashboard', 'commerce:self', 'inventory:self', 'order:self', 'settlement:self', 'supply:member', 'biz:*'],
   // 活动管理员(仅自己的活动)+ 业务管理

+ 9 - 2
cfc-web/src/views/Layout.vue

@@ -178,7 +178,14 @@ export default {
             { path: '/rewards', label: '奖励管理', icon: 'el-icon-s-goods', perm: 'reward:list' },
           ]},
 
-        // ===== 3. 任务中心 (task:*) =====
+        // ===== 3. 方案中心 (plan:*) =====
+        { title: '方案中心', icon: 'el-icon-notebook-2', perm: 'plan:*',
+          children: [
+            { path: '/health-plan-manage', label: '健康方案管理', icon: 'el-icon-first-aid-kit', perm: 'plan:health' },
+            { path: '/assessment-plan-manage', label: '测评方案管理', icon: 'el-icon-edit', perm: 'plan:assessment' },
+          ]},
+
+        // ===== 4. 任务中心 (task:*) =====
         { title: '任务中心', icon: 'el-icon-s-order', perm: 'task:*,task:read',
           children: [
             { path: '/tasks', label: '任务列表', icon: 'el-icon-s-order', perm: 'task:list' },
@@ -186,7 +193,7 @@ export default {
             { path: '/growth-task', label: '成长任务管理', icon: 'el-icon-s-management', perm: 'task:growth' },
           ]},
 
-        // ===== 4. 审核中心(归集:服务角色申请/活动审核/退款/提现) =====
+        // ===== 5. 审核中心(归集:服务角色申请/活动审核/退款/提现) =====
         { title: '审核中心', icon: 'el-icon-s-check', perm: 'audit',
           children: [
             { path: '/review-center', label: '审核中心总览', icon: 'el-icon-s-check', perm: 'audit' },

+ 2 - 1
docs/superpowers/api/API_REFERENCE.md

@@ -182,7 +182,7 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 |------|------|
 | `POST /api/tasks/minigame-options` | 小游戏选项 |
 | `POST /api/tasks/create` | 创建任务 |
-| `POST /api/tasks/today` | 今日任务 |
+| `POST /api/tasks/today` | 今日任务(`memberId` 查单成员;`familyWide=true` 查全家庭今日任务) |
 | `POST /api/tasks/{id}/start` | 开始任务(两阶段入口,含购物任务自动加购/下单) |
 | `POST /api/tasks/{id}/complete` | 完成任务 |
 | `POST /api/tasks/{id}/review` | 审核任务 |
@@ -403,6 +403,7 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 | `/api/admin/ecom-supplier/*` | 电商供应商管理 |
 | `/api/admin/guide/*` | 成长规划师管理(申请/套餐) |
 | `/api/admin/growth-archive-product/*` | 成长档案关联商品 |
+| `/api/admin/health-plan/*` | 健康方案管理(跨家庭列表/详情) |
 | `/api/admin/knowledge-base/*` | 知识库管理 |
 | `/api/admin/notices/*` | 公告管理 |
 | `/api/admin/package-templates/*` | 套餐模板 |