Bladeren bron

Merge branch 'cfclub' of http://git.iwintrue.com/liaoxg/cfc into cfclub

E2E Test Bot 4 dagen geleden
bovenliggende
commit
bfc8d2723a
92 gewijzigde bestanden met toevoegingen van 867 en 1708 verwijderingen
  1. 2 0
      .gitignore
  2. 9 1
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  3. 16 10
      cfc-backend/src/main/java/com/etotem/cfc/controller/auth/AuthController.java
  4. 7 1
      cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
  5. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/service/api/UserServiceInterface.java
  6. 5 0
      cfc-backend/src/main/java/com/etotem/cfc/task/OrderTimeoutCancelTask.java
  7. 5 0
      cfc-backend/src/main/java/com/etotem/cfc/task/RefundScheduledTasks.java
  8. 5 0
      cfc-backend/src/main/java/com/etotem/cfc/task/RepeatTaskGenerator.java
  9. 5 0
      cfc-backend/src/main/java/com/etotem/cfc/task/VirtualPayTimeoutTask.java
  10. 5 1
      cfc-backend/src/main/resources/application.yml
  11. 1 1
      cfc-backend/src/main/resources/schema.sql
  12. 55 74
      cfc-frontend/App.vue
  13. 146 7
      cfc-frontend/__tests__/api/api.test.js
  14. 15 8
      cfc-frontend/__tests__/config/config.test.js
  15. 8 0
      cfc-frontend/__tests__/setup.js
  16. 13 1
      cfc-frontend/__tests__/store/store.test.js
  17. 0 1052
      cfc-frontend/build_log.txt
  18. 0 226
      cfc-frontend/components/ProfileHeader.vue
  19. 0 45
      cfc-frontend/fix-hbuilderx-scoped.js
  20. 3 0
      cfc-frontend/manifest.json
  21. 0 0
      cfc-frontend/pages/action-detail/components/AbilityDiagram.vue
  22. 0 0
      cfc-frontend/pages/action-detail/components/OctopusDiagram.vue
  23. 1 1
      cfc-frontend/pages/action-detail/components/PearlDiagram.vue
  24. 3 3
      cfc-frontend/pages/action-detail/index.vue
  25. 3 3
      cfc-frontend/pages/ai/chat.vue
  26. 0 0
      cfc-frontend/pages/ai/components/AITaskCard.vue
  27. 0 0
      cfc-frontend/pages/ai/components/QuotaProgressBar.vue
  28. 0 0
      cfc-frontend/pages/ai/components/UpgradeGuideModal.vue
  29. 2 2
      cfc-frontend/pages/discover/circles.vue
  30. 0 0
      cfc-frontend/pages/discover/components/CircleCard.vue
  31. 0 0
      cfc-frontend/pages/discover/components/CircleDetail.vue
  32. 1 1
      cfc-frontend/pages/family/add-member.vue
  33. 1 1
      cfc-frontend/pages/family/components/GenerationPicker.vue
  34. 19 9
      cfc-frontend/pages/family/self-check-entry.vue
  35. 1 156
      cfc-frontend/pages/family/self-check-result.vue
  36. 1 1
      cfc-frontend/pages/health/components/MemberChipPicker.vue
  37. 1 1
      cfc-frontend/pages/health/components/feedback-popup.vue
  38. 0 0
      cfc-frontend/pages/health/components/health-knowledge-popup.vue
  39. 2 2
      cfc-frontend/pages/health/gut-flora-detail.vue
  40. 1 1
      cfc-frontend/pages/health/gut-flora-risks-detail.vue
  41. 1 1
      cfc-frontend/pages/health/gut-flora-species-detail.vue
  42. 1 1
      cfc-frontend/pages/health/health-plan-summary.vue
  43. 1 1
      cfc-frontend/pages/health/report-confirm.vue
  44. 1 1
      cfc-frontend/pages/health/report-detail.vue
  45. 2 2
      cfc-frontend/pages/home-pages/child-index.vue
  46. 0 0
      cfc-frontend/pages/home-pages/components/AdBanner.vue
  47. 0 0
      cfc-frontend/pages/home-pages/components/CircleRanking.vue
  48. 0 0
      cfc-frontend/pages/home-pages/components/ConfettiCelebration.vue
  49. 0 0
      cfc-frontend/pages/home-pages/components/ContactCard.vue
  50. 1 1
      cfc-frontend/pages/home-pages/components/ContactImport.vue
  51. 0 0
      cfc-frontend/pages/home-pages/components/DailySummary.vue
  52. 0 0
      cfc-frontend/pages/home-pages/components/FamilyHealthRanking.vue
  53. 0 0
      cfc-frontend/pages/home-pages/components/FeedbackToast.vue
  54. 0 0
      cfc-frontend/pages/home-pages/components/HealthAlertBanner.vue
  55. 0 0
      cfc-frontend/pages/home-pages/components/MicroActionCard.vue
  56. 0 0
      cfc-frontend/pages/home-pages/components/MilestoneCelebration.vue
  57. 0 0
      cfc-frontend/pages/home-pages/components/gate-guide-bar.vue
  58. 11 11
      cfc-frontend/pages/home-pages/parent-index.vue
  59. 2 2
      cfc-frontend/pages/member-detail/components/member-detail-sections/action-section.vue
  60. 3 3
      cfc-frontend/pages/member-detail/components/member-detail-sections/body-section.vue
  61. 1 1
      cfc-frontend/pages/member-detail/components/member-detail-sections/mind-section.vue
  62. 2 2
      cfc-frontend/pages/member-detail/components/member-detail-sections/wisdom-section.vue
  63. 4 4
      cfc-frontend/pages/member-detail/member-detail.vue
  64. 0 0
      cfc-frontend/pages/mind-detail/components/FamilyTianpanCard.vue
  65. 2 2
      cfc-frontend/pages/mind-detail/components/PsychCrisisBanner.vue
  66. 1 1
      cfc-frontend/pages/mind-detail/emotion-checkin.vue
  67. 1 1
      cfc-frontend/pages/mind-detail/emotion-trend.vue
  68. 2 2
      cfc-frontend/pages/mind-detail/index.vue
  69. 66 31
      cfc-frontend/pages/profile/components/ProfileHeader.vue
  70. 1 1
      cfc-frontend/pages/profile/components/ProfileMenu.vue
  71. 2 2
      cfc-frontend/pages/profile/index.vue
  72. 0 0
      cfc-frontend/pages/promotion/components/SharePoster.vue
  73. 1 1
      cfc-frontend/pages/promotion/index.vue
  74. 1 1
      cfc-frontend/pages/promotion/invite.vue
  75. 1 1
      cfc-frontend/pages/shop/checkout/checkout.vue
  76. 0 0
      cfc-frontend/pages/shop/components/countdown-tag.vue
  77. 0 0
      cfc-frontend/pages/wealth/components/BodyWealthAlert.vue
  78. 0 0
      cfc-frontend/pages/wealth/components/WealthEnergyRing.vue
  79. 0 0
      cfc-frontend/pages/wealth/components/WealthSubScores.vue
  80. 3 3
      cfc-frontend/pages/wealth/index.vue
  81. 108 19
      cfc-frontend/utils/api.js
  82. 1 1
      cfc-web/.last_build_commit
  83. 2 2
      cfc-web/package-lock.json
  84. 1 1
      cfc-web/package.json
  85. 76 0
      cfc-web/public/CHANGELOG-v1.0.md
  86. 77 1
      cfc-web/public/CHANGELOG.md
  87. BIN
      docs/参考资料/益生元商品/瓜尔主推产品图标及介绍9.1.xlsx
  88. BIN
      工作文稿/抗衰促进会/2026《浠艾福青少年素质教育与家庭健康成长陪伴工程》立项申报书.docx
  89. BIN
      工作文稿/抗衰促进会/2026《老年心理健康筛查与陪伴工程》立项申报书.docx
  90. BIN
      工作文稿/抗衰促进会/2026《青少年主动心身健康与家庭健康成长陪伴工程》立项申报书.docx
  91. 74 0
      运营文案/健康理念/血管优先抗衰_视频逐字稿_主动健康版.md
  92. 74 0
      运营文案/健康理念/血管优先抗衰_视频逐字稿_浠艾福版.md

+ 2 - 0
.gitignore

@@ -14,6 +14,8 @@
 # Logs
 *.log
 logs/
+# HBuilderX / uni-app CLI 构建日志(.txt 后缀,不会被上面的 *.log 覆盖)
+**/build_log.txt
 
 # OS files
 .DS_Store

+ 9 - 1
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -29,6 +29,13 @@ public class DatabaseInitializer implements CommandLineRunner {
 
     private static final Logger log = LoggerFactory.getLogger(DatabaseInitializer.class);
 
+    /** 迁移是否已全部完成(供定时任务判断,避免启动期时序竞态) */
+    private static volatile boolean migrationsComplete = false;
+
+    public static boolean isMigrationsComplete() {
+        return migrationsComplete;
+    }
+
     @Resource
     private JdbcTemplate jdbcTemplate;
 
@@ -63,6 +70,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.info("init-test-data=false,跳过测试/种子数据初始化(生产模式)");
         }
 
+        migrationsComplete = true;
         log.info("数据库初始化完成!");
     }
 
@@ -11074,7 +11082,7 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
                     "popup_id BIGINT NOT NULL COMMENT '关联 guide_popup.id', " +
                     "step_order INT NOT NULL DEFAULT 0 COMMENT '步骤顺序', " +
                     "title VARCHAR(128) NOT NULL DEFAULT '' COMMENT '步骤标题', " +
-                    "desc TEXT COMMENT '步骤描述', " +
+                    "`desc` TEXT COMMENT '步骤描述', " +
                     "icon VARCHAR(256) NOT NULL DEFAULT '' COMMENT '步骤图标', " +
                     "icon_type TINYINT NOT NULL DEFAULT 0 COMMENT '图标类型:0=emoji 1=图片', " +
                     "is_intro TINYINT NOT NULL DEFAULT 0 COMMENT '是否为开场身份定义页', " +

+ 16 - 10
cfc-backend/src/main/java/com/etotem/cfc/controller/auth/AuthController.java

@@ -114,19 +114,25 @@ public class AuthController {
     }
 
     /**
-     * 通过openid自动登录
+     * 通过openid自动登录 —— 已废弃(410 Gone)
+     *
+     * <p>废弃原因(安全漏洞,2026-09 修复):本接口只凭请求参数里的明文 openid 就签发完整 JWT,
+     * 不做任何持有性证明。而 openid 并非密钥——它会随登录响应返回、明文存于小程序本地 storage、
+     * 并可能经日志/截图泄露。任何拿到他人 openid 的人都能直接换取该账号的合法 token,
+     * 等同于完全冒充。
+     *
+     * <p>替代方案:{@code /api/auth/silent-login}。前端调 {@code uni.login()} 拿微信签发的
+     * 一次性 code,服务端 code2Session 换 openid。code 单次使用、5 分钟过期,且能证明调用方
+     * 持有该微信设备,因此不可伪造,同样零交互静默完成。
+     *
+     * <p>按项目规范(AGENTS.md「废弃接口标记 410 Gone 并注释说明,不直接删除」):
+     * 保留路由与注释,待线上旧版本小程序全部升级后再移除路由。
      */
-    @Operation(summary = "openid自动登录")
+    @Operation(summary = "openid自动登录(已废弃,返回 410,请改用 /api/auth/silent-login)")
     @PostMapping("/auto-login")
     public Result<LoginResultDTO> autoLogin(@RequestBody WechatLoginDTO dto) {
-        if (dto.getOpenid() == null) {
-            return Result.error("openid不能为空");
-        }
-        LoginResultDTO result = userService.autoLoginByOpenid(dto.getOpenid());
-        if (result == null) {
-            return Result.error("用户不存在,请先登录");
-        }
-        return Result.success(result);
+        log.warn("/api/auth/auto-login 已被调用(已废弃接口):该接口仅凭 openid 签发 token 存在越权风险,已拒绝。请前端改用 /api/auth/silent-login");
+        return Result.error(410, "该接口已废弃,请改用 /api/auth/silent-login");
     }
 
     /**

+ 7 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java

@@ -307,8 +307,14 @@ private FamilyInvitationService familyInvitationService;
     }
 
     /**
-     * 通过openid自动登录(前端已缓存openid时调用)
+     * 通过openid自动登录
+     *
+     * @deprecated 仅凭明文 openid 签发 token,无任何持有性证明,构成越权风险,禁止调用。
+     * 对外接口 /api/auth/auto-login 已改为 410 Gone。保留实现仅为过渡期兼容,
+     * 待线上旧版本小程序全部升级后连同路由一并移除。
+     * 请改用 {@link #silentLogin(String)}(微信 code2Session,可证明设备持有)。
      */
+    @Deprecated
     public LoginResultDTO autoLoginByOpenid(String openid) {
         User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
                 .eq(User::getOpenid, openid));

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/api/UserServiceInterface.java

@@ -9,6 +9,13 @@ public interface UserServiceInterface {
     LoginResultDTO phoneLogin(PhoneLoginDTO dto);
     LoginResultDTO wechatPhoneLogin(WechatPhoneLoginDTO dto);
     LoginResultDTO silentLogin(String code);
+
+    /**
+     * @deprecated 仅凭明文 openid 签发 token,无任何持有性证明,构成越权风险。
+     * 对外接口 /api/auth/auto-login 已改为 410 Gone。请改用 {@link #silentLogin(String)},
+     * 由前端 uni.login() 提供微信签发的一次性 code 走 code2Session。
+     */
+    @Deprecated
     LoginResultDTO autoLoginByOpenid(String openid);
     boolean setPassword(Long userId, String password);
     boolean setSwitchPassword(Long userId, String password);

+ 5 - 0
cfc-backend/src/main/java/com/etotem/cfc/task/OrderTimeoutCancelTask.java

@@ -14,6 +14,7 @@ import java.util.Calendar;
 import java.util.Date;
 import java.util.List;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.config.DatabaseInitializer;
 
 @Component
 public class OrderTimeoutCancelTask {
@@ -28,6 +29,10 @@ public class OrderTimeoutCancelTask {
 
     @Scheduled(fixedRate = 60000) // 每分钟执行一次
     public void cancelTimeoutOrders() {
+        if (!DatabaseInitializer.isMigrationsComplete()) {
+            log.info("数据库迁移尚未完成,跳过本轮订单超时检查");
+            return;
+        }
         // 计算超时时间点
         Calendar cal = Calendar.getInstance();
         cal.add(Calendar.MINUTE, -paymentTimeoutMinutes);

+ 5 - 0
cfc-backend/src/main/java/com/etotem/cfc/task/RefundScheduledTasks.java

@@ -21,6 +21,7 @@ import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.config.DatabaseInitializer;
 
 /**
  * 退款定时任务
@@ -55,6 +56,10 @@ public class RefundScheduledTasks {
     @Scheduled(cron = "0 */5 * * * ?")
     @Transactional(rollbackFor = Exception.class)
     public void processPendingRefunds() {
+        if (!DatabaseInitializer.isMigrationsComplete()) {
+            log.info("数据库迁移尚未完成,跳过本轮退款处理");
+            return;
+        }
         log.info("开始执行待退款处理定时任务");
         try {
             // 获取待处理的退款记录(最多 10 条)

+ 5 - 0
cfc-backend/src/main/java/com/etotem/cfc/task/RepeatTaskGenerator.java

@@ -11,6 +11,7 @@ import org.springframework.stereotype.Component;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.List;
+import com.etotem.cfc.config.DatabaseInitializer;
 
 /**
  * TASK-SYS-248: 重复任务定时生成器(频率分派版)
@@ -34,6 +35,10 @@ public class RepeatTaskGenerator {
      */
     @Scheduled(cron = "0 5 * * * ?")
     public void generateDailyTasks() {
+        if (!DatabaseInitializer.isMigrationsComplete()) {
+            log.info("数据库迁移尚未完成,跳过本轮重复任务生成");
+            return;
+        }
         log.info("开始按频率生成重复任务...");
 
         try {

+ 5 - 0
cfc-backend/src/main/java/com/etotem/cfc/task/VirtualPayTimeoutTask.java

@@ -27,6 +27,7 @@ import java.util.Calendar;
 import java.util.Date;
 import java.util.List;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.config.DatabaseInitializer;
 
 /**
  * 虚拟支付超时兜底任务(spec §7:success 回调丢失防丢单)。
@@ -72,6 +73,10 @@ public class VirtualPayTimeoutTask {
 
     @Scheduled(fixedRate = 60000) // 每分钟执行一次
     public void handleTimeoutOrders() {
+        if (!DatabaseInitializer.isMigrationsComplete()) {
+            log.info("数据库迁移尚未完成,跳过本轮虚拟支付超时检查");
+            return;
+        }
         Calendar cal = Calendar.getInstance();
         cal.add(Calendar.MINUTE, -paymentTimeoutMinutes);
         Date timeoutTime = cal.getTime();

+ 5 - 1
cfc-backend/src/main/resources/application.yml

@@ -60,7 +60,11 @@ mybatis-plus:
 
 jwt:
   secret: ${JWT_SECRET:cfc-prod-jwt-secret-2026-keep-it-safe!}
-  expiration: 86400000
+  # 30 天(2592000000ms)。配合小程序端 wx.login 静默续期(/api/auth/silent-login),
+  # 用户几乎无需手动重新登录。
+  # 安全前提:续期只能走 code2Session(微信签发的单次 code,能证明设备持有),
+  # 严禁恢复「裸 openid 换 token」——那等于任何拿到 openid 的人都能冒充该账号。
+  expiration: 2592000000
 
 zkxz:
   enabled: false

+ 1 - 1
cfc-backend/src/main/resources/schema.sql

@@ -5904,7 +5904,7 @@ CREATE TABLE IF NOT EXISTS guide_popup_step (
     popup_id BIGINT NOT NULL COMMENT '关联 guide_popup.id',
     step_order INT NOT NULL DEFAULT 0 COMMENT '步骤顺序',
     title VARCHAR(128) NOT NULL DEFAULT '' COMMENT '步骤标题',
-    desc TEXT COMMENT '步骤描述',
+    `desc` TEXT COMMENT '步骤描述',
     icon VARCHAR(256) NOT NULL DEFAULT '' COMMENT '步骤图标',
     icon_type TINYINT NOT NULL DEFAULT 0 COMMENT '图标类型:0=emoji 1=图片',
     is_intro TINYINT NOT NULL DEFAULT 0 COMMENT '是否为开场身份定义页',

+ 55 - 74
cfc-frontend/App.vue

@@ -1,7 +1,7 @@
 <script>
 import config from '@/config.js'
 import Vue from 'vue'
-import { checkUserFamily, verifyToken, bindReferralIfNotSelf, request } from './utils/api.js'
+import { checkUserFamily, verifyToken, bindReferralIfNotSelf, silentRelogin } from './utils/api.js'
 Vue.prototype.$config = config
 
 export default {
@@ -9,7 +9,13 @@ export default {
     console.log('App Launch', options)
     // 捕获分享/邀请参数(普通分享链接 ?inviteCode=xxx):未登录写入 storage 供注册登录后绑定推荐人,已登录直接绑定
     this.captureReferralParams(options)
-// 分享/深链冷启动(携带分享参数)放行到落地页,不套 splash,避免分享的文章/小程序页面打不开
+    // 恢复登录态:必须放在 splash 分支之前。
+    // 原实现在普通冷启动(无深链)时于 splash 跳转处 return,导致 verifyStoredToken / tryAutoLogin
+    // 只在「分享/小程序码进入」时才执行,普通用户冷启动拿不到任何续期机会。
+    // 现在冷启动即异步校验/静默续期,与 2 秒 splash 动画并行,用户落地即已是有效登录态。
+    // 本方法不做任何页面跳转,导航交由 splash 或当前页面处理,避免与 splash 的 reLaunch 抢路由。
+    this.restoreSession()
+    // 分享/深链冷启动(携带分享参数)放行到落地页,不套 splash,避免分享的文章/小程序页面打不开
     // inviteCode=推荐码(文章/活动/商品分享)| refCode=推广码 | token/familyInviteCode=家庭邀请令牌 | id=内容详情
     var hasDeepLink = options && options.query && (
       options.query.inviteCode || options.query.refCode || options.query.token ||
@@ -23,25 +29,6 @@ export default {
     }
     // 处理小程序码场景值(从 wxacode 扫码进入,跳过 splash)
     this.handleScene(options)
-    
-    // TabBar 始终显示 5 个底签,不因登录状态隐藏
-    const token = uni.getStorageSync('token')
-    if (token) {
-      this.$store.commit('setToken', token)
-      // 启动时校验 token 是否仍有效:过期则清理本地登录态并引导重新登录
-      this.verifyStoredToken()
-      // 冷启动恢复 familyId(登录时已持久化,hasFamily getter 依赖)
-      var storedFamilyId = uni.getStorageSync('familyId')
-      if (storedFamilyId) {
-        this.$store.commit('setFamilyId', storedFamilyId)
-      } else {
-        // 历史用户(8ddc5b55 后未重新登录):本地无 familyId,从接口恢复
-        this.restoreFamilyIdFromApi()
-      }
-    } else {
-      // 无token时尝试自动登录
-      this.tryAutoLogin()
-    }
   },
   onShow: function(options) {
     console.log('App Show', options)
@@ -88,20 +75,40 @@ bindReferralIfNotSelf(code).then(function(res) {
         uni.setStorageSync('inviteType', options.query.inviteType)
       }
     },
-    // 校验本地 token 是否有效(开发环境跳过,避免无后端时误报)
-    verifyStoredToken() {
-      try {
-        var envInfo = uni.getAccountInfoSync()
-        if (envInfo && envInfo.miniProgram && envInfo.miniProgram.envVersion === 'develop') {
-          return
+    // 冷启动恢复登录态(异步执行,不做任何页面跳转)
+    // 1) 有 token:先同步到 Vuex。若 token 已过期/失效,/api/auth/verify 会返回 code=401,
+    //    由 request 封装统一走 _handle401 → silentRelogin 静默续期并重放,全程用户无感。
+    // 2) 无 token 但本地有 openid(此前登录过、token 已被清理):走静默续期。
+    // 3) 两者都没有:本设备从未登录,属新用户,保持未登录态,由登录页引导。
+    restoreSession() {
+      // TabBar 始终显示 5 个底签,不因登录状态隐藏
+      const token = uni.getStorageSync('token')
+      if (token) {
+        this.$store.commit('setToken', token)
+        // 冷启动恢复 familyId(登录时已持久化,hasFamily getter 依赖)
+        var storedFamilyId = uni.getStorageSync('familyId')
+        if (storedFamilyId) {
+          this.$store.commit('setFamilyId', storedFamilyId)
+        } else {
+          // 历史用户(8ddc5b55 后未重新登录):本地无 familyId,从接口恢复
+          this.restoreFamilyIdFromApi()
         }
-      } catch (e) { /* 非小程序环境 */ }
-      var self = this
-      request('/api/auth/verify', 'POST').then(function() {
+        // 启动时校验 token 是否仍有效,过期则触发静默续期
+        this.verifyStoredToken()
+      } else {
+        this.tryAutoLogin()
+      }
+    },
+    // 校验本地 token 是否有效。
+    // 注意:/api/auth/verify 在 JwtInterceptor 的白名单里,不会被拦截器判 401,而是由
+    // AuthController 自行解析后返回 code=401 的响应体 —— 同样会被 request 封装识别并续期。
+    // 早期版本在 develop 环境直接 return,导致开发版完全无法验证续期逻辑,已移除该屏蔽。
+    verifyStoredToken() {
+      verifyToken().then(function() {
         // token 有效,无需处理
       }).catch(function(e) {
-        // 401:由 request 封装统一触发 _handle401(静默 auto-login 续期或清空重登录)
-        // _handle401 内部已有完整逻辑:用缓存 openid auto-login → 成功则重放原请求;失败则清空并跳登录页
+        // 401:由 request 封装统一触发 _handle401(silentRelogin 静默续期后重放原请求);
+        //      只有续期也失败时才由 _clearAuthAndRedirect 清空登录态并跳登录页。
         // 其它错误(如网络失败)静默忽略,不打扰用户
         console.log('[App] token 校验未通过:', e && e.code)
       })
@@ -118,32 +125,25 @@ bindReferralIfNotSelf(code).then(function(res) {
         console.log('[App] familyId 接口恢复失败(页面按无家庭态渲染)', e)
       })
     },
+    // 无本地 token 时尝试静默续期。
+    // 走 uni.login() → /api/auth/silent-login(服务端 code2Session):微信签发的一次性 code
+    // 能证明调用方持有该微信设备,因此不可伪造;本地 openid 仅用于判断「本设备曾登录过」,
+    // 绝不作为凭证上送(旧的 autoLogin(openid) 已被后端废弃为 410 Gone)。
+    // 早期版本在 develop 环境直接 return,开发版无法验证续期,已移除该屏蔽。
+    // 不做页面跳转:冷启动由 splash 在 2 秒后统一 reLaunch 到首页,
+    // 此处若再 switchTab 会与其抢路由(splash 先落地时 switchTab 落在已销毁页面上)。
     async tryAutoLogin() {
-      // 开发环境下跳过自动登录,避免无后端时 10s 超时阻塞
-      try {
-        var envInfo = uni.getAccountInfoSync()
-        if (envInfo && envInfo.miniProgram && envInfo.miniProgram.envVersion === 'develop') {
-          return
-        }
-      } catch (e) { /* 非小程序环境 */ }
       try {
-        // 1. 先尝试用缓存的openid自动登录
-        const cachedOpenid = uni.getStorageSync('openid')
-        if (cachedOpenid) {
-          const res = await this.request('/api/auth/auto-login', 'POST', { openid: cachedOpenid })
-          if (res && res.data) {
-            this.saveLoginInfo(res.data)
-            console.log('openid自动登录成功')
-            // 刷新页面到登录态
-            uni.switchTab({ url: '/pages/index-home/index' })
-            return
-          }
+        var loginData = await silentRelogin()
+        if (loginData) {
+          this.saveLoginInfo(loginData)
+          console.log('[App] 静默续期成功,已恢复登录态')
+        } else {
+          // 本设备从未登录过,或续期失败:保持未登录态,由登录页引导
+          console.log('[App] 静默续期未成功,需手动登录')
         }
-
-        // 2. openid自动登录失败,不自动调用uni.login(避免DevTools环境超时阻塞渲染层)
-        //    用户会看到登录页,手动选择登录方式
       } catch (e) {
-        console.log('自动登录失败,需要手动登录', e)
+        console.log('[App] 静默续期异常,需手动登录', e)
       }
     },
     saveLoginInfo(data) {
@@ -164,28 +164,9 @@ bindReferralIfNotSelf(code).then(function(res) {
         familyId: data.familyId
       })
       this.$store.commit('setToken', data.token)
-      // 同步 familyId 到 store(hasFamily getter 依赖;auto-login 路径)
+      // 同步 familyId 到 store(hasFamily getter 依赖;静默续期路径)
       this.$store.commit('setFamilyId', data.familyId)
     },
-    request(url, method, data) {
-      return new Promise((resolve, reject) => {
-        uni.request({
-          url: config.api(url),
-          method: method,
-          data: data,
-          timeout: 10000,
-          header: { 'Content-Type': 'application/json' },
-          success: (res) => {
-            if (res.data.code === 200) {
-              resolve(res.data)
-            } else {
-              reject(res.data)
-            }
-          },
-          fail: reject
-        })
-      })
-    },
     // 处理小程序码场景值,跳转到邀请页
     handleScene(options) {
       if (!options || !options.query) return

+ 146 - 7
cfc-frontend/__tests__/api/api.test.js

@@ -46,10 +46,18 @@ beforeEach(() => {
   global.uni._storage = {}
   global.uni.request.mockReset()
   global.uni.showToast.mockReset()
+  global.uni.reLaunch.mockReset()
+  global.uni.login.mockReset()
+  global.getCurrentPages.mockReset()
+  global.getCurrentPages.mockReturnValue([{ route: 'pages/index-home/index' }])
   global.uni.getAccountInfoSync.mockReset()
   global.uni.getAccountInfoSync.mockReturnValue({
     miniProgram: { envVersion: 'develop' }
   })
+  // api.js 的 3 秒请求去重缓存是模块级单例,跨用例残留会让后续用例拿到上一个用例缓存的
+  // Promise(同 URL+method+body),表现为「本该 401 却是 200」「lastRequest() 为 undefined」。
+  // 每个用例前显式清空,保证用例间隔离。
+  api.clearApiDedup()
 })
 
 /** Capture the last uni.request call options */
@@ -99,10 +107,16 @@ describe('auth API', () => {
     expect(lastRequest().data.code).toBe('wxcode')
   })
 
-  test('autoLogin sends POST with openid', async () => {
-    mockSuccess({ code: 200 })
-    await api.autoLogin('openid_xxx')
-    expect(lastRequest().data.openid).toBe('openid_xxx')
+  // 已废弃:后端 /api/auth/auto-login 已改为 410 Gone(裸 openid 换 token 属越权漏洞)。
+  // 导出仅为排查历史调用保留,不得在新代码中使用;续期请走 silentRelogin()。
+  test('autoLogin (deprecated) still targets /api/auth/auto-login', async () => {
+    mockSuccess({ code: 410, message: '该接口已废弃,请改用 /api/auth/silent-login' })
+    await expect(api.autoLogin('openid_xxx')).rejects.toEqual(
+      { code: 410, message: '该接口已废弃,请改用 /api/auth/silent-login' }
+    )
+    const opts = lastRequest()
+    expect(opts.url).toMatch(/\/api\/auth\/auto-login$/)
+    expect(opts.data.openid).toBe('openid_xxx')
   })
 
   test('setPassword and verifyPassword send correct endpoints', async () => {
@@ -166,11 +180,22 @@ describe('auth API', () => {
     jest.useRealTimers()
   })
 
-  test('401 with cached openid silently re-logins and retries original request', async () => {
+  // 静默续期 = uni.login() 取微信签发的一次性 code → /api/auth/silent-login(服务端 code2Session)。
+  // 本地 openid 只作为「本设备曾登录过」的触发条件,不作为凭证上送。
+  function mockUniLoginCode(code) {
+    global.uni.login.mockImplementation((opts) => {
+      process.nextTick(() => {
+        if (opts.success) opts.success({ code: code || 'wxcode' })
+      })
+    })
+  }
+
+  test('401 silently re-logins via wx.login + code2Session and retries original request', async () => {
     global.uni.reLaunch.mockReset()
     global.uni.showToast.mockReset()
     global.uni._storage.openid = 'openid_abc'
     global.uni._storage.token = 'expired-token'
+    mockUniLoginCode('fresh-wxcode')
     mockSequence([
       { data: { code: 401, message: 'Token无效或已过期', data: null } },
       { data: { code: 200, data: { token: 'new-jwt', userId: 42, role: 'parent', familyId: 1, openid: 'openid_abc' } } },
@@ -179,17 +204,86 @@ describe('auth API', () => {
     const res = await api.verifyToken()
     expect(res.data.userId).toBe(42)
     expect(global.uni._storage.token).toBe('new-jwt')
+    // 续期必须走 code2Session,且不得把 openid 当凭证发送
+    const calls = global.uni.request.mock.calls.map((c) => c[0])
+    const renewReq = calls.find((c) => /\/api\/auth\/silent-login/.test(c.url))
+    expect(renewReq).toBeDefined()
+    expect(renewReq.data.code).toBe('fresh-wxcode')
+    expect(renewReq.data.openid).toBeUndefined()
+    // 已废弃的裸 openid 换 token 接口不得被调用
+    expect(calls.find((c) => /\/api\/auth\/auto-login/.test(c.url))).toBeUndefined()
     expect(global.uni.reLaunch).not.toHaveBeenCalled()
     expect(global.uni.showToast).not.toHaveBeenCalled()
   })
 
-  test('401 with cached openid falls back to login when auto-login fails', async () => {
+  test('401 retry does not duplicate query params in the replayed request URL', async () => {
+    global.uni._storage.openid = 'openid_abc'
+    global.uni._storage.token = 'expired-token'
+    mockUniLoginCode('fresh-wxcode')
+    mockSequence([
+      { data: { code: 401, message: 'Token无效或已过期', data: null } },
+      { data: { code: 200, data: { token: 'new-jwt', userId: 42, role: 'parent', familyId: 1, openid: 'openid_abc' } } },
+      { data: { code: 200, data: { ok: true } } }
+    ])
+    // publishPackage 通过 options 传 query(?publish=true),是最容易暴露重复拼接的调用形态
+    await api.publishPackage(7, true)
+    const calls = global.uni.request.mock.calls.map((c) => c[0])
+    const hits = calls.filter((c) => /\/api\/packages\/7\/publish/.test(c.url))
+    expect(hits.length).toBe(2) // 原始请求 1 次 + 401 续期后重放 1 次
+    // 修复前:_handle401 收到的是已拼过 query 的 url,重放时 options 又拼一次 → ?publish=true&publish=true
+    hits.forEach((c) => {
+      expect(c.url).toBe(c.url.split('?')[0] + '?publish=true')
+      expect((c.url.match(/publish=/g) || []).length).toBe(1)
+    })
+  })
+
+  test('401 during app launch window clears auth but does NOT navigate to login subpackage', async () => {
+    jest.useFakeTimers()
+    global.uni._storage.openid = 'openid_abc'
+    global.uni._storage.token = 'expired-token'
+    // 仍停在 splash:pages/login 是分包,此刻尚未下载,
+    // reLaunch 过去会报 'Page "pages/login/login" has not been registered yet'
+    global.getCurrentPages.mockReturnValue([{ route: 'pages/splash/index' }])
+    mockUniLoginCode()
+    mockSequence([
+      { data: { code: 401, message: 'Token无效或已过期', data: null } },
+      { data: { code: 500, message: '用户不存在,请先登录', data: null } }
+    ])
+    await expect(api.verifyToken()).rejects.toEqual({ code: 401, message: 'Token无效或已过期', data: null })
+    jest.runAllTimers()
+    // 登录态必须清掉
+    expect(global.uni._storage.token).toBeUndefined()
+    // 但启动窗口内绝不跳登录页,交由 splash 落地首页(主包)
+    expect(global.uni.reLaunch).not.toHaveBeenCalled()
+    jest.useRealTimers()
+  })
+
+  test('401 during app launch window (no page registered yet) also skips navigation', async () => {
     jest.useFakeTimers()
     global.uni._storage.openid = 'openid_abc'
     global.uni._storage.token = 'expired-token'
+    // 首屏尚未注册,getCurrentPages() 为空
+    global.getCurrentPages.mockReturnValue([])
+    mockUniLoginCode()
     mockSequence([
       { data: { code: 401, message: 'Token无效或已过期', data: null } },
-      { data: { code: 401, message: '用户不存在', data: null } }
+      { data: { code: 500, message: '用户不存在,请先登录', data: null } }
+    ])
+    await expect(api.verifyToken()).rejects.toEqual({ code: 401, message: 'Token无效或已过期', data: null })
+    jest.runAllTimers()
+    expect(global.uni._storage.token).toBeUndefined()
+    expect(global.uni.reLaunch).not.toHaveBeenCalled()
+    jest.useRealTimers()
+  })
+
+  test('401 falls back to login page when silent re-login is refused', async () => {
+    jest.useFakeTimers()
+    global.uni._storage.openid = 'openid_abc'
+    global.uni._storage.token = 'expired-token'
+    mockUniLoginCode()
+    mockSequence([
+      { data: { code: 401, message: 'Token无效或已过期', data: null } },
+      { data: { code: 500, message: '用户不存在,请先登录', data: null } }
     ])
     await expect(api.verifyToken()).rejects.toEqual({ code: 401, message: 'Token无效或已过期', data: null })
     jest.runAllTimers()
@@ -198,6 +292,51 @@ describe('auth API', () => {
     jest.useRealTimers()
   })
 
+  test('401 with no cached openid (never logged in on this device) goes straight to login', async () => {
+    jest.useFakeTimers()
+    global.uni._storage.token = 'expired-token'
+    mockSequence([
+      { data: { code: 401, message: 'Token无效或已过期', data: null } }
+    ])
+    await expect(api.verifyToken()).rejects.toEqual({ code: 401, message: 'Token无效或已过期', data: null })
+    // 本设备从未登录过 → 不应调用 uni.login 浪费一次 code2Session
+    expect(global.uni.login).not.toHaveBeenCalled()
+    jest.runAllTimers()
+    expect(global.uni.reLaunch).toHaveBeenCalledWith({ url: '/pages/login/login' })
+    jest.useRealTimers()
+  })
+
+  test('silentRelogin returns null when wx.login yields no code', async () => {
+    global.uni._storage.openid = 'openid_abc'
+    global.uni.login.mockImplementation((opts) => {
+      process.nextTick(() => { if (opts.success) opts.success({}) })
+    })
+    await expect(api.silentRelogin()).resolves.toBeNull()
+    expect(global.uni.request).not.toHaveBeenCalled()
+  })
+
+  test('silentRelogin returns null when wx.login fails', async () => {
+    global.uni._storage.openid = 'openid_abc'
+    global.uni.login.mockImplementation((opts) => {
+      process.nextTick(() => { if (opts.fail) opts.fail({ errMsg: 'login:fail' }) })
+    })
+    await expect(api.silentRelogin()).resolves.toBeNull()
+    expect(global.uni.request).not.toHaveBeenCalled()
+  })
+
+  test('silentRelogin does not issue a renew request on 401 (no recursive renewal)', async () => {
+    global.uni._storage.openid = 'openid_abc'
+    global.uni._storage.token = 'expired-token'
+    mockUniLoginCode()
+    // 续期接口自身返回 401:必须直接失败,不能再触发一次 silentRelogin,否则会递归续期/误清登录态
+    mockSuccess({ code: 401, message: 'Token无效或已过期', data: null })
+    await expect(api.silentRelogin()).resolves.toBeNull()
+    expect(global.uni.request.mock.calls.length).toBe(1)
+    // 续期失败不得清空登录态(那是「主动登出」才做的事)
+    expect(global.uni._storage.token).toBe('expired-token')
+    expect(global.uni.reLaunch).not.toHaveBeenCalled()
+  })
+
   test('Authorization header includes Bearer token', async () => {
     mockSuccess({ code: 200 })
     global.uni._storage.token = 'my-jwt-token'

+ 15 - 8
cfc-frontend/__tests__/config/config.test.js

@@ -13,34 +13,41 @@ beforeEach(() => {
   })
 })
 
+// 与 config.js 保持一致:develop / trial / release 三端统一指向同一生产域名;
+// 非小程序环境、env 缺失或未知 envVersion 时,回落到模块顶部的默认值。
+// 换域名时只需改这两行(对应 config.js 的 switch 分支与顶部默认值)。
+const PROD_HOST = 'https://cfc.etotem.com.cn'
+const FALLBACK_HOST = 'http://cfc.iwintrue.com'
+
 describe('config.js', () => {
   beforeEach(() => {
     // Reset the module registry so each test gets a fresh config
+    // (config.js 在模块加载时就求值 API_BASE_URL,必须重新 require 才会重跑这段逻辑)
     jest.resetModules()
   })
 
-  test('uses develop defaults when getAccountInfoSync returns develop', () => {
+  test('uses production host when getAccountInfoSync returns develop', () => {
     global.uni.getAccountInfoSync.mockReturnValue({
       miniProgram: { envVersion: 'develop' }
     })
     const cfg = require('@/config').default
-    expect(cfg.API_BASE_URL).toBe('http://localhost:9082')
+    expect(cfg.API_BASE_URL).toBe(PROD_HOST)
   })
 
-  test('uses trial URLs when envVersion is trial', () => {
+  test('uses production host when envVersion is trial', () => {
     global.uni.getAccountInfoSync.mockReturnValue({
       miniProgram: { envVersion: 'trial' }
     })
     const cfg = require('@/config').default
-    expect(cfg.API_BASE_URL).toBe('https://cfc.iwintrue.com')
+    expect(cfg.API_BASE_URL).toBe(PROD_HOST)
   })
 
-  test('uses release URLs when envVersion is release', () => {
+  test('uses production host when envVersion is release', () => {
     global.uni.getAccountInfoSync.mockReturnValue({
       miniProgram: { envVersion: 'release' }
     })
     const cfg = require('@/config').default
-    expect(cfg.API_BASE_URL).toBe('https://cfc.etotem.com.cn')
+    expect(cfg.API_BASE_URL).toBe(PROD_HOST)
   })
 
   test('uses defaults when getAccountInfoSync throws', () => {
@@ -48,7 +55,7 @@ describe('config.js', () => {
       throw new Error('not in mini-program')
     })
     const cfg = require('@/config').default
-    expect(cfg.API_BASE_URL).toBe('http://localhost:9082')
+    expect(cfg.API_BASE_URL).toBe(FALLBACK_HOST)
   })
 
   test('uses defaults for unknown envVersion', () => {
@@ -57,7 +64,7 @@ describe('config.js', () => {
     })
     const cfg = require('@/config').default
     // Falls through the switch without matching, keeps defaults
-    expect(cfg.API_BASE_URL).toBe('http://localhost:9082')
+    expect(cfg.API_BASE_URL).toBe(FALLBACK_HOST)
   })
 
   test('api(path) returns full URL joining API_BASE_URL + path', () => {

+ 8 - 0
cfc-frontend/__tests__/setup.js

@@ -49,6 +49,14 @@ global.uni = {
   }))
 }
 
+// ---- Mock getCurrentPages ----
+// 小程序运行时全局(不在 uni 上)。utils/api.js 的 _inLaunchWindow() 用它判断
+// 是否仍处于启动窗口(splash 未切走),因为 pages/login 是分包,
+// 启动窗口内 reLaunch 到未下载的分包页会报 "Page has not been registered yet"。
+// 默认返回一个「已渲染的非 splash 页面」,即正常交互态(此时应允许跳登录页)。
+// 用例内可覆盖 global.getCurrentPages 返回 [] 或 splash 页面来测启动窗口分支。
+global.getCurrentPages = jest.fn(() => [{ route: 'pages/index-home/index' }])
+
 // ---- Mock Vue ----
 jest.mock('vue', () => {
   const Vue = jest.requireActual('vue')

+ 13 - 1
cfc-frontend/__tests__/store/store.test.js

@@ -55,12 +55,24 @@ describe('Vuex Store', () => {
       expect(store.state.isSwitchedChild).toBe(false)
 
       // Verify storage sync
+      // 注意:familyId 的持久化由 setFamilyId 负责(见下一个用例),
+      // setUserInfo 只负责 state.familyId,不写 storage
       expect(global.uni._storage.userId).toBe('100')
       expect(global.uni._storage.role).toBe('parent')
-      expect(global.uni._storage.familyId).toBe('1')
       expect(global.uni._storage.isSwitchedChild).toBe(false)
     })
 
+    test('setFamilyId persists familyId to storage for cold start', () => {
+      store.commit('setFamilyId', '42')
+      expect(store.state.familyId).toBe('42')
+      expect(global.uni._storage.familyId).toBe('42')
+
+      // 无家庭时写入空串而非 undefined,保证冷启动 getStorageSync 能取到键
+      store.commit('setFamilyId', '')
+      expect(store.state.familyId).toBe('')
+      expect(global.uni._storage.familyId).toBe('')
+    })
+
     test('setUserInfo clears switched-child flag', () => {
       global.uni._storage.isSwitchedChild = true
       store.commit('setUserInfo', {

+ 0 - 1052
cfc-frontend/build_log.txt

@@ -1,1052 +0,0 @@
-
-> cfc-frontend@1.0.0 build:mp-weixin
-> cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin UNI_INPUT_DIR=. UNI_CLI_CONTEXT=. vue-cli-service uni-build
-
-该应用之前可能是非自定义组件模式,目前以自定义组件模式运行。非自定义组件已于2019年11月1日起停止支持。详见:https://ask.dcloud.net.cn/article/36385
-Please note that in running mode, due to log output, sourcemap, and uncompressed source code, the performance and package size are not as good as release mode.To officially release, please click the release menu or use the cli release command to release
-There are differences in the implementation mechanism of the browser kernels and custom components of each mini-program, and there may be compatibility issues with styles and layouts, please refer to: https://uniapp.dcloud.io/matter?id=mp
-
- WARNING  Compiled with 260 warnings23:48:31
-
- warning  in ./pages/games/1a2b.vue
-
-"export 'components' was not found in './1a2b.vue?vue&type=template&id=19434987&scoped=true&'
-
- warning  in ./components/AIFloatingAvatar.vue
-
-"export 'components' was not found in './AIFloatingAvatar.vue?vue&type=template&id=025fa5fc&'
-
- warning  in ./components/AITaskCard.vue
-
-"export 'components' was not found in './AITaskCard.vue?vue&type=template&id=271b2f78&scoped=true&'
-
- warning  in ./components/ArticleBookshelf.vue
-
-"export 'components' was not found in './ArticleBookshelf.vue?vue&type=template&id=465aa734&scoped=true&'
-
- warning  in ./components/BaseBadge.vue
-
-"export 'components' was not found in './BaseBadge.vue?vue&type=template&id=05421b87&scoped=true&'
-
- warning  in ./components/BaseEmpty.vue
-
-"export 'components' was not found in './BaseEmpty.vue?vue&type=template&id=32b12031&scoped=true&'
-
- warning  in ./components/BaseLoading.vue
-
-"export 'components' was not found in './BaseLoading.vue?vue&type=template&id=358769a0&scoped=true&'
-
- warning  in ./components/BodyWealthAlert.vue
-
-"export 'components' was not found in './BodyWealthAlert.vue?vue&type=template&id=22332ba2&scoped=true&'
-
- warning  in ./components/CircleCard.vue
-
-"export 'components' was not found in './CircleCard.vue?vue&type=template&id=b794b0ca&scoped=true&'
-
- warning  in ./components/CircleDetail.vue
-
-"export 'components' was not found in './CircleDetail.vue?vue&type=template&id=47d60b9c&scoped=true&'
-
- warning  in ./components/CircleRanking.vue
-
-"export 'components' was not found in './CircleRanking.vue?vue&type=template&id=bc366e0a&scoped=true&'
-
- warning  in ./components/ConfettiCelebration.vue
-
-"export 'components' was not found in './ConfettiCelebration.vue?vue&type=template&id=4afced92&scoped=true&'
-
- warning  in ./components/ContactCard.vue
-
-"export 'components' was not found in './ContactCard.vue?vue&type=template&id=74337d45&scoped=true&'
-
- warning  in ./components/ContactImport.vue
-
-"export 'components' was not found in './ContactImport.vue?vue&type=template&id=67b29fcc&scoped=true&'
-
- warning  in ./components/ContentSharePoster.vue
-
-"export 'components' was not found in './ContentSharePoster.vue?vue&type=template&id=7260c2ee&scoped=true&'
-
- warning  in ./components/DailySummary.vue
-
-"export 'components' was not found in './DailySummary.vue?vue&type=template&id=556943a8&scoped=true&'
-
- warning  in ./components/DimensionActivities.vue
-
-"export 'components' was not found in './DimensionActivities.vue?vue&type=template&id=2c184168&scoped=true&'
-
- warning  in ./components/DimensionProducts.vue
-
-"export 'components' was not found in './DimensionProducts.vue?vue&type=template&id=ca237fc2&scoped=true&'
-
- warning  in ./components/DimensionTasks.vue
-
-"export 'components' was not found in './DimensionTasks.vue?vue&type=template&id=5d5fd83a&scoped=true&'
-
- warning  in ./components/FamilyChallengeCard.vue
-
-"export 'components' was not found in './FamilyChallengeCard.vue?vue&type=template&id=292ece84&scoped=true&'
-
- warning  in ./components/FamilyEmptyState/FamilyEmptyState.vue
-
-"export 'components' was not found in './FamilyEmptyState.vue?vue&type=template&id=19999e04&scoped=true&'
-
- warning  in ./components/FamilyEnergyBar.vue
-
-"export 'components' was not found in './FamilyEnergyBar.vue?vue&type=template&id=1496595c&scoped=true&'
-
- warning  in ./components/FamilyHealthRanking.vue
-
-"export 'components' was not found in './FamilyHealthRanking.vue?vue&type=template&id=9b49b9aa&scoped=true&'
-
- warning  in ./components/FamilyMemberCard.vue
-
-"export 'components' was not found in './FamilyMemberCard.vue?vue&type=template&id=e3dd34ae&scoped=true&'
-
- warning  in ./components/FamilyMemberStrip.vue
-
-"export 'components' was not found in './FamilyMemberStrip.vue?vue&type=template&id=3a2229cf&scoped=true&'
-
- warning  in ./components/FamilyRelationGraph.vue
-
-"export 'components' was not found in './FamilyRelationGraph.vue?vue&type=template&id=298cccba&scoped=true&'
-
- warning  in ./components/FamilyTianpanCard.vue
-
-"export 'components' was not found in './FamilyTianpanCard.vue?vue&type=template&id=437aef48&scoped=true&'
-
- warning  in ./components/FeedbackToast.vue
-
-"export 'components' was not found in './FeedbackToast.vue?vue&type=template&id=501cc357&scoped=true&'
-
- warning  in ./components/GenerationPicker.vue
-
-"export 'components' was not found in './GenerationPicker.vue?vue&type=template&id=214423be&scoped=true&'
-
- warning  in ./components/HealthAlertBanner.vue
-
-"export 'components' was not found in './HealthAlertBanner.vue?vue&type=template&id=3c74f43e&scoped=true&'
-
- warning  in ./components/HealthBadge.vue
-
-"export 'components' was not found in './HealthBadge.vue?vue&type=template&id=c3f08dc8&scoped=true&'
-
- warning  in ./components/HealthTimeline.vue
-
-"export 'components' was not found in './HealthTimeline.vue?vue&type=template&id=83f3bb50&scoped=true&'
-
- warning  in ./components/LoginGuideCard.vue
-
-"export 'components' was not found in './LoginGuideCard.vue?vue&type=template&id=4c87535e&scoped=true&'
-
- warning  in ./components/MicroActionCard.vue
-
-"export 'components' was not found in './MicroActionCard.vue?vue&type=template&id=f3d96282&scoped=true&'
-
- warning  in ./components/MilestoneCelebration.vue
-
-"export 'components' was not found in './MilestoneCelebration.vue?vue&type=template&id=7bfa7235&scoped=true&'
-
- warning  in ./components/OnboardingGuide.vue
-
-"export 'components' was not found in './OnboardingGuide.vue?vue&type=template&id=774ca096&scoped=true&'
-
- warning  in ./components/PageBanner.vue
-
-"export 'components' was not found in './PageBanner.vue?vue&type=template&id=258384b6&scoped=true&'
-
- warning  in ./components/PlayfulButton.vue
-
-"export 'components' was not found in './PlayfulButton.vue?vue&type=template&id=3bdb3790&scoped=true&'
-
- warning  in ./components/PlayfulCard.vue
-
-"export 'components' was not found in './PlayfulCard.vue?vue&type=template&id=47e53424&scoped=true&'
-
- warning  in ./pages/profile/components/ProfileBadges.vue
-
-"export 'components' was not found in './ProfileBadges.vue?vue&type=template&id=79a245ff&scoped=true&'
-
- warning  in ./pages/profile/components/ProfileGrowth.vue
-
-"export 'components' was not found in './ProfileGrowth.vue?vue&type=template&id=14a9a9d4&scoped=true&'
-
- warning  in ./pages/profile/components/ProfileHeader.vue
-
-"export 'components' was not found in './ProfileHeader.vue?vue&type=template&id=a3ef4e48&scoped=true&'
-
- warning  in ./pages/profile/components/ProfileMenu.vue
-
-"export 'components' was not found in './ProfileMenu.vue?vue&type=template&id=0e59f46e&scoped=true&'
-
- warning  in ./components/PsychCrisisBanner.vue
-
-"export 'components' was not found in './PsychCrisisBanner.vue?vue&type=template&id=a6bf0b42&scoped=true&'
-
- warning  in ./components/RadarChart.vue
-
-"export 'components' was not found in './RadarChart.vue?vue&type=template&id=38aa9fda&scoped=true&'
-
- warning  in ./components/RecommendedFeed.vue
-
-"export 'components' was not found in './RecommendedFeed.vue?vue&type=template&id=57b9498e&scoped=true&'
-
- warning  in ./components/SharePoster.vue
-
-"export 'components' was not found in './SharePoster.vue?vue&type=template&id=577d5201&scoped=true&'
-
- warning  in ./components/WealthSubScores.vue
-
-"export 'components' was not found in './WealthSubScores.vue?vue&type=template&id=0a9ea26e&scoped=true&'
-
- warning  in ./pages/activity/activity-detail/activity-detail.vue
-
-"export 'components' was not found in './activity-detail.vue?vue&type=template&id=60ba5a88&scoped=true&'
-
- warning  in ./pages/family/add-member.vue
-
-"export 'components' was not found in './add-member.vue?vue&type=template&id=3b80b928&scoped=true&'
-
- warning  in ./pages/shop/address-edit/address-edit.vue
-
-"export 'components' was not found in './address-edit.vue?vue&type=template&id=0f74ff22&scoped=true&'
-
- warning  in ./components/address-picker.vue
-
-"export 'components' was not found in './address-picker.vue?vue&type=template&id=4e2c8f02&scoped=true&'
-
- warning  in ./pages/shop/address/address.vue
-
-"export 'components' was not found in './address.vue?vue&type=template&id=3069208e&scoped=true&'
-
- warning  in ./pages/shop/after-sales-apply/after-sales-apply.vue
-
-"export 'components' was not found in './after-sales-apply.vue?vue&type=template&id=1ff07989&scoped=true&'
-
- warning  in ./pages/shop/after-sales-detail/after-sales-detail.vue
-
-"export 'components' was not found in './after-sales-detail.vue?vue&type=template&id=8a2b7922&scoped=true&'
-
- warning  in ./pages/shop/after-sales/after-sales.vue
-
-"export 'components' was not found in './after-sales.vue?vue&type=template&id=301350f2&scoped=true&'
-
- warning  in ./pages/policy/agreement.vue
-
-"export 'components' was not found in './agreement.vue?vue&type=template&id=726fe366&scoped=true&'
-
- warning  in ./pages/tianpan/annual-energy.vue
-
-"export 'components' was not found in './annual-energy.vue?vue&type=template&id=40d2290e&scoped=true&'
-
- warning  in ./pages/nutritionist/apply.vue
-
-"export 'components' was not found in './apply.vue?vue&type=template&id=3478f8ca&'
-
- warning  in ./pages/butler/apply.vue
-
-"export 'components' was not found in './apply.vue?vue&type=template&id=400f6810&'
-
- warning  in ./pages/vendor/apply.vue
-
-"export 'components' was not found in './apply.vue?vue&type=template&id=6138bd40&'
-
- warning  in ./pages/assessment/apply.vue
-
-"export 'components' was not found in './apply.vue?vue&type=template&id=bdabec4c&scoped=true&'
-
- warning  in ./pages/parent/appointments.vue
-
-"export 'components' was not found in './appointments.vue?vue&type=template&id=7d56dae0&scoped=true&'
-
- warning  in ./pages/article-center/article-detail.vue
-
-"export 'components' was not found in './article-detail.vue?vue&type=template&id=50c02542&scoped=true&'
-
- warning  in ./pages/article-center/article-edit.vue
-
-"export 'components' was not found in './article-edit.vue?vue&type=template&id=7ffe4e8a&scoped=true&'
-
- warning  in ./pages/mind-extra/articles.vue
-
-"export 'components' was not found in './articles.vue?vue&type=template&id=471f69ce&scoped=true&'
-
- warning  in ./pages/mind-detail/articles.vue
-
-"export 'components' was not found in './articles.vue?vue&type=template&id=4c12eab9&scoped=true&'
-
- warning  in ./pages/growth/assessment-result.vue
-
-"export 'components' was not found in './assessment-result.vue?vue&type=template&id=cb5b6a8e&scoped=true&'
-
- warning  in ./pages/rewards/badge.vue
-
-"export 'components' was not found in './badge.vue?vue&type=template&id=c2652562&scoped=true&'
-
- warning  in ./pages/membership/benefits.vue
-
-"export 'components' was not found in './benefits.vue?vue&type=template&id=56cdc76c&scoped=true&'
-
- warning  in ./pages/shop/cart/cart.vue
-
-"export 'components' was not found in './cart.vue?vue&type=template&id=36f8ce8f&scoped=true&'
-
- warning  in ./pages/vendor/center.vue
-
-"export 'components' was not found in './center.vue?vue&type=template&id=5f9ec553&'
-
- warning  in ./pages/health/challenge-manage.vue
-
-"export 'components' was not found in './challenge-manage.vue?vue&type=template&id=11ce3c0e&scoped=true&'
-
- warning  in ./pages/ai/chat.vue
-
-"export 'components' was not found in './chat.vue?vue&type=template&id=1edb40f6&scoped=true&'
-
- warning  in ./pages/teacher/chat.vue
-
-"export 'components' was not found in './chat.vue?vue&type=template&id=4da1e044&scoped=true&'
-
- warning  in ./pages/wealth-sub/checkin.vue
-
-"export 'components' was not found in './checkin.vue?vue&type=template&id=70076e77&'
-
- warning  in ./pages/body-detail/checkin.vue
-
-"export 'components' was not found in './checkin.vue?vue&type=template&id=8240f7de&scoped=true&'
-
- warning  in ./pages/shop/checkout/checkout.vue
-
-"export 'components' was not found in './checkout.vue?vue&type=template&id=bbf0dc62&scoped=true&'
-
- warning  in ./pages/parent/child-detail.vue
-
-"export 'components' was not found in './child-detail.vue?vue&type=template&id=3517e8fe&scoped=true&'
-
- warning  in ./pages/home-pages/child-index.vue
-
-"export 'components' was not found in './child-index.vue?vue&type=template&id=7d36c961&scoped=true&'
-
- warning  in ./pages/profile-extra/children.vue
-
-"export 'components' was not found in './children.vue?vue&type=template&id=03fbd87b&scoped=true&'
-
- warning  in ./pages/discover/circles.vue
-
-"export 'components' was not found in './circles.vue?vue&type=template&id=f3dee114&scoped=true&'
-
- warning  in ./pages/wisdom-detail/cognitive-report.vue
-
-"export 'components' was not found in './cognitive-report.vue?vue&type=template&id=df56c59c&scoped=true&'
-
- warning  in ./pages/promotion/commission.vue
-
-"export 'components' was not found in './commission.vue?vue&type=template&id=5d447e50&scoped=true&'
-
- warning  in ./pages/stats/completion-rate.vue
-
-"export 'components' was not found in './completion-rate.vue?vue&type=template&id=0f801b98&scoped=true&'
-
- warning  in ./pages/shop/consignee-edit/consignee-edit.vue
-
-"export 'components' was not found in './consignee-edit.vue?vue&type=template&id=4bb748cf&scoped=true&'
-
- warning  in ./pages/shop/consignee-list/consignee-list.vue
-
-"export 'components' was not found in './consignee-list.vue?vue&type=template&id=62608e4f&scoped=true&'
-
- warning  in ./pages/action-detail/contact-detail.vue
-
-"export 'components' was not found in './contact-detail.vue?vue&type=template&id=34f1ff84&scoped=true&'
-
- warning  in ./pages/profile-extra/coupons.vue
-
-"export 'components' was not found in './coupons.vue?vue&type=template&id=1797957e&scoped=true&'
-
- warning  in ./pages/profile-extra/create-child.vue
-
-"export 'components' was not found in './create-child.vue?vue&type=template&id=1b0abf87&scoped=true&'
-
- warning  in ./pages/tasks/create-task.vue
-
-"export 'components' was not found in './create-task.vue?vue&type=template&id=5322290c&scoped=true&'
-
- warning  in ./pages/activity/create.vue
-
-"export 'components' was not found in './create.vue?vue&type=template&id=30ad9a93&scoped=true&'
-
- warning  in ./pages/wishes/create.vue
-
-"export 'components' was not found in './create.vue?vue&type=template&id=423de20d&scoped=true&'
-
- warning  in ./pages/guide/packages/create.vue
-
-"export 'components' was not found in './create.vue?vue&type=template&id=67818722&scoped=true&'
-
- warning  in ./pages/growth/create.vue
-
-"export 'components' was not found in './create.vue?vue&type=template&id=c14586ca&scoped=true&'
-
- warning  in ./pages/health/daily-checkin.vue
-
-"export 'components' was not found in './daily-checkin.vue?vue&type=template&id=6ff1ceff&scoped=true&'
-
- warning  in ./pages/tianpan/daily-fortune.vue
-
-"export 'components' was not found in './daily-fortune.vue?vue&type=template&id=357f4360&scoped=true&'
-
- warning  in ./pages/tasks/daily-tasks.vue
-
-"export 'components' was not found in './daily-tasks.vue?vue&type=template&id=4896ba30&scoped=true&'
-
- warning  in ./pages/growth/detail.vue
-
-"export 'components' was not found in './detail.vue?vue&type=template&id=0ffc0170&scoped=true&'
-
- warning  in ./pages/shop/detail/detail.vue
-
-"export 'components' was not found in './detail.vue?vue&type=template&id=4bde69af&scoped=true&'
-
- warning  in ./pages/guide/packages/detail.vue
-
-"export 'components' was not found in './detail.vue?vue&type=template&id=4fbf6812&scoped=true&'
-
- warning  in ./pages/guide/activities/detail.vue
-
-"export 'components' was not found in './detail.vue?vue&type=template&id=510de9f7&scoped=true&'
-
- warning  in ./pages/parent/market/detail.vue
-
-"export 'components' was not found in './detail.vue?vue&type=template&id=5635a158&scoped=true&'
-
- warning  in ./pages/energy/detail.vue
-
-"export 'components' was not found in './detail.vue?vue&type=template&id=629e5e0f&scoped=true&'
-
- warning  in ./pages/parent/plans/detail.vue
-
-"export 'components' was not found in './detail.vue?vue&type=template&id=7c37ac72&scoped=true&'
-
- warning  in ./pages/wishes/detail.vue
-
-"export 'components' was not found in './detail.vue?vue&type=template&id=9a46b23c&scoped=true&'
-
- warning  in ./pages/health/diet-index.vue
-
-"export 'components' was not found in './diet-index.vue?vue&type=template&id=4cdcc3ba&scoped=true&'
-
- warning  in ./pages/body-detail/dimension-detail.vue
-
-"export 'components' was not found in './dimension-detail.vue?vue&type=template&id=a225ba38&scoped=true&'
-
- warning  in ./pages/body-detail/dimension-questionnaire.vue
-
-"export 'components' was not found in './dimension-questionnaire.vue?vue&type=template&id=79fadde0&scoped=true&'
-
- warning  in ./pages/mind-detail/duo-compatibility.vue
-
-"export 'components' was not found in './duo-compatibility.vue?vue&type=template&id=261d2615&scoped=true&'
-
- warning  in ./pages/profile-extra/edit-child.vue
-
-"export 'components' was not found in './edit-child.vue?vue&type=template&id=11461c15&scoped=true&'
-
- warning  in ./pages/mind-detail/emotion-checkin.vue
-
-"export 'components' was not found in './emotion-checkin.vue?vue&type=template&id=00673e22&scoped=true&'
-
- warning  in ./pages/mind-detail/emotion-report.vue
-
-"export 'components' was not found in './emotion-report.vue?vue&type=template&id=ccad583c&scoped=true&'
-
- warning  in ./pages/mind-detail/emotion-trend.vue
-
-"export 'components' was not found in './emotion-trend.vue?vue&type=template&id=d26b0642&scoped=true&'
-
- warning  in ./pages/assessment/execution.vue
-
-"export 'components' was not found in './execution.vue?vue&type=template&id=4153baf8&scoped=true&'
-
- warning  in ./pages/health/exercise-index.vue
-
-"export 'components' was not found in './exercise-index.vue?vue&type=template&id=e35de3f2&scoped=true&'
-
- warning  in ./pages/teacher/families.vue
-
-"export 'components' was not found in './families.vue?vue&type=template&id=0102d388&scoped=true&'
-
- warning  in ./pages/mind-detail/family-dashboard.vue
-
-"export 'components' was not found in './family-dashboard.vue?vue&type=template&id=2bcd2e32&scoped=true&'
-
- warning  in ./pages/profile-extra/family-members.vue
-
-"export 'components' was not found in './family-members.vue?vue&type=template&id=7904faa8&'
-
- warning  in ./components/feedback-popup.vue
-
-"export 'components' was not found in './feedback-popup.vue?vue&type=template&id=90bfd742&scoped=true&'
-
- warning  in ./pages/diet/food-query.vue
-
-"export 'components' was not found in './food-query.vue?vue&type=template&id=a73083aa&scoped=true&'
-
- warning  in ./pages/health/gut-flora-detail.vue
-
-"export 'components' was not found in './gut-flora-detail.vue?vue&type=template&id=202aad77&scoped=true&'
-
- warning  in ./pages/health/gut-flora-foods-detail.vue
-
-"export 'components' was not found in './gut-flora-foods-detail.vue?vue&type=template&id=fa0a6022&scoped=true&'
-
- warning  in ./pages/health/gut-flora-risks-detail.vue
-
-"export 'components' was not found in './gut-flora-risks-detail.vue?vue&type=template&id=07b90ce0&scoped=true&'
-
- warning  in ./pages/health/gut-flora-species-detail.vue
-
-"export 'components' was not found in './gut-flora-species-detail.vue?vue&type=template&id=a29fe6b0&scoped=true&'
-
- warning  in ./pages/health/gut-index.vue
-
-"export 'components' was not found in './gut-index.vue?vue&type=template&id=3e0993de&scoped=true&'
-
- warning  in ./pages/body-detail/health-dimensions.vue
-
-"export 'components' was not found in './health-dimensions.vue?vue&type=template&id=4be9fc42&scoped=true&'
-
- warning  in ./components/health-knowledge-popup.vue
-
-"export 'components' was not found in './health-knowledge-popup.vue?vue&type=template&id=38c94627&scoped=true&'
-
- warning  in ./pages/body-detail/health-report.vue
-
-"export 'components' was not found in './health-report.vue?vue&type=template&id=104c86ae&scoped=true&'
-
- warning  in ./pages/discover/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=024a9de5&scoped=true&'
-
- warning  in ./pages/tianpan/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=09a2c1d6&'
-
- warning  in ./pages/splash/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=1144df23&scoped=true&'
-
- warning  in ./pages/profile/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=14bc1b43&scoped=true&'
-
- warning  in ./pages/body-detail/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=17928536&scoped=true&'
-
- warning  in ./pages/mind-detail/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=1d0a5086&scoped=true&'
-
- warning  in ./pages/parent/wishes/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=2760bc98&scoped=true&'
-
- warning  in ./pages/health-main/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=2c8bf364&scoped=true&'
-
- warning  in ./pages/membership/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=2f11355c&scoped=true&'
-
- warning  in ./pages/article-center/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=3092faf0&scoped=true&'
-
- warning  in ./custom-tab-bar/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=47b4a9d0&scoped=true&'
-
- warning  in ./pages/wealth/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=4dea08e9&scoped=true&'
-
- warning  in ./pages/shop/index/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=5931e635&scoped=true&'
-
- warning  in ./pages/wisdom-detail/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=5cb66b37&scoped=true&'
-
- warning  in ./pages/guide/hierarchy/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=60721388&scoped=true&'
-
- warning  in ./pages/wishes/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=61627a5e&scoped=true&'
-
- warning  in ./pages/stats/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=6198fe79&scoped=true&'
-
- warning  in ./pages/diet/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=619bcea0&scoped=true&'
-
- warning  in ./pages/activity/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=6362e18b&scoped=true&'
-
- warning  in ./pages/promotion/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=642729fd&scoped=true&'
-
- warning  in ./pages/guide/packages/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=6527a71c&scoped=true&'
-
- warning  in ./pages/parent/market/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=675cbcca&scoped=true&'
-
- warning  in ./pages/guide/activities/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=690e491c&scoped=true&'
-
- warning  in ./pages/guide/register/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=6cde5552&scoped=true&'
-
- warning  in ./pages/parent/plans/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=72b4cb41&scoped=true&'
-
- warning  in ./pages/teacher/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=82beab88&scoped=true&'
-
- warning  in ./pages/index-home/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=c50b3d14&'
-
- warning  in ./pages/share/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=d1630c8e&scoped=true&'
-
- warning  in ./pages/growth-main/index.vue
-
-"export 'components' was not found in './index.vue?vue&type=template&id=d67e1ece&scoped=true&'
-
- warning  in ./pages/wealth-sub/insurance-add.vue
-
-"export 'components' was not found in './insurance-add.vue?vue&type=template&id=3440df78&'
-
- warning  in ./pages/wealth-sub/insurance-list.vue
-
-"export 'components' was not found in './insurance-list.vue?vue&type=template&id=1f4ce152&'
-
- warning  in ./pages/wealth-sub/insurance-planning.vue
-
-"export 'components' was not found in './insurance-planning.vue?vue&type=template&id=734f5616&scoped=true&'
-
- warning  in ./pages/action-detail/interaction-log.vue
-
-"export 'components' was not found in './interaction-log.vue?vue&type=template&id=e922fbce&scoped=true&'
-
- warning  in ./pages/guide/bind/invite.vue
-
-"export 'components' was not found in './invite.vue?vue&type=template&id=1b5b8942&scoped=true&'
-
- warning  in ./pages/promotion/invite.vue
-
-"export 'components' was not found in './invite.vue?vue&type=template&id=3e3f1fee&scoped=true&'
-
- warning  in ./pages/invite/join.vue
-
-"export 'components' was not found in './join.vue?vue&type=template&id=01794487&scoped=true&'
-
- warning  in ./pages/promotion/leaderboard.vue
-
-"export 'components' was not found in './leaderboard.vue?vue&type=template&id=bc2e3e30&scoped=true&'
-
- warning  in ./pages/games/list.vue
-
-"export 'components' was not found in './list.vue?vue&type=template&id=21432265&scoped=true&'
-
- warning  in ./pages/meal/log.vue
-
-"export 'components' was not found in './log.vue?vue&type=template&id=5d6ca71e&scoped=true&'
-
- warning  in ./pages/login/login.vue
-
-"export 'components' was not found in './login.vue?vue&type=template&id=b237504c&scoped=true&'
-
- warning  in ./pages/shop/logistics/logistics.vue
-
-"export 'components' was not found in './logistics.vue?vue&type=template&id=06030e12&scoped=true&'
-
- warning  in ./pages/promotion/material.vue
-
-"export 'components' was not found in './material.vue?vue&type=template&id=5aac97cc&scoped=true&'
-
- warning  in ./pages/diet/meal-config.vue
-
-"export 'components' was not found in './meal-config.vue?vue&type=template&id=1902422c&scoped=true&'
-
- warning  in ./pages/health/meditation-index.vue
-
-"export 'components' was not found in './meditation-index.vue?vue&type=template&id=0a9bfd8d&scoped=true&'
-
- warning  in ./pages/action-detail/member-action-detail.vue
-
-"export 'components' was not found in './member-action-detail.vue?vue&type=template&id=a38ae416&scoped=true&'
-
- warning  in ./pages/body-detail/member-body-detail.vue
-
-"export 'components' was not found in './member-body-detail.vue?vue&type=template&id=1f95da16&scoped=true&'
-
- warning  in ./pages/tianpan/member-detail.vue
-
-"export 'components' was not found in './member-detail.vue?vue&type=template&id=218c0db2&'
-
- warning  in ./pages/home-pages/member-home-detail.vue
-
-"export 'components' was not found in './member-home-detail.vue?vue&type=template&id=27a9cf7c&scoped=true&'
-
- warning  in ./pages/mind-detail/member-mind-detail.vue
-
-"export 'components' was not found in './member-mind-detail.vue?vue&type=template&id=7914b0f5&scoped=true&'
-
- warning  in ./pages/wisdom-detail/member-wisdom-detail.vue
-
-"export 'components' was not found in './member-wisdom-detail.vue?vue&type=template&id=0bdae5d6&scoped=true&'
-
- warning  in ./pages/teacher/messages.vue
-
-"export 'components' was not found in './messages.vue?vue&type=template&id=2e5cbcd2&scoped=true&'
-
- warning  in ./components/mp-html/mp-html.vue
-
-"export 'components' was not found in './mp-html.vue?vue&type=template&id=35fd0930&'
-
- warning  in ./pages/article-center/my-posts.vue
-
-"export 'components' was not found in './my-posts.vue?vue&type=template&id=a1abcda8&scoped=true&'
-
- warning  in ./components/mp-html/node/node.vue
-
-"export 'components' was not found in './node.vue?vue&type=template&id=1d2f0fca&'
-
- warning  in ./pages/health/nutrition-profile.vue
-
-"export 'components' was not found in './nutrition-profile.vue?vue&type=template&id=1a8ada7a&scoped=true&'
-
- warning  in ./pages/profile-extra/onboarding.vue
-
-"export 'components' was not found in './onboarding.vue?vue&type=template&id=6c900cd2&scoped=true&'
-
- warning  in ./pages/shop/order-detail/order-detail.vue
-
-"export 'components' was not found in './order-detail.vue?vue&type=template&id=7872a98f&scoped=true&'
-
- warning  in ./pages/shop/order-list/order-list.vue
-
-"export 'components' was not found in './order-list.vue?vue&type=template&id=98c351a2&scoped=true&'
-
- warning  in ./pages/activity/order-pay/order-pay.vue
-
-"export 'components' was not found in './order-pay.vue?vue&type=template&id=28dcfb3c&scoped=true&'
-
- warning  in ./pages/teacher/orders.vue
-
-"export 'components' was not found in './orders.vue?vue&type=template&id=19ad8d6b&scoped=true&'
-
- warning  in ./pages/vendor/orders/orders.vue
-
-"export 'components' was not found in './orders.vue?vue&type=template&id=744f10c1&scoped=true&'
-
- warning  in ./pages/home-pages/parent-index.vue
-
-"export 'components' was not found in './parent-index.vue?vue&type=template&id=7902e8c2&scoped=true&'
-
- warning  in ./pages/shop/payment/payment.vue
-
-"export 'components' was not found in './payment.vue?vue&type=template&id=589cb7dd&'
-
- warning  in ./pages/health/physical-exam-detail.vue
-
-"export 'components' was not found in './physical-exam-detail.vue?vue&type=template&id=003af7b3&scoped=true&'
-
- warning  in ./pages/meal/preference.vue
-
-"export 'components' was not found in './preference.vue?vue&type=template&id=489b0fbe&scoped=true&'
-
- warning  in ./pages/diet/preferences.vue
-
-"export 'components' was not found in './preferences.vue?vue&type=template&id=7801ff56&scoped=true&'
-
- warning  in ./pages/policy/privacy.vue
-
-"export 'components' was not found in './privacy.vue?vue&type=template&id=0558e6e4&scoped=true&'
-
- warning  in ./pages/discover-detail/product-detail/product-detail.vue
-
-"export 'components' was not found in './product-detail.vue?vue&type=template&id=74d483e8&scoped=true&'
-
- warning  in ./pages/vendor/product-edit/product-edit.vue
-
-"export 'components' was not found in './product-edit.vue?vue&type=template&id=bde8d1be&scoped=true&'
-
- warning  in ./pages/vendor/products/products.vue
-
-"export 'components' was not found in './products.vue?vue&type=template&id=2c061021&scoped=true&'
-
- warning  in ./pages/profile-main/profile.vue
-
-"export 'components' was not found in './profile.vue?vue&type=template&id=2efc2970&scoped=true&'
-
- warning  in ./pages/policy/provider-agreement.vue
-
-"export 'components' was not found in './provider-agreement.vue?vue&type=template&id=58f6b582&scoped=true&'
-
- warning  in ./pages/assessment/purchase.vue
-
-"export 'components' was not found in './purchase.vue?vue&type=template&id=f2239876&scoped=true&'
-
- warning  in ./pages/assessment/quota.vue
-
-"export 'components' was not found in './quota.vue?vue&type=template&id=7775a038&scoped=true&'
-
- warning  in ./pages/assessment/rating.vue
-
-"export 'components' was not found in './rating.vue?vue&type=template&id=03f7253e&scoped=true&'
-
- warning  in ./pages/meal/recipe-detail.vue
-
-"export 'components' was not found in './recipe-detail.vue?vue&type=template&id=77f6e346&scoped=true&'
-
- warning  in ./pages/meal/recommend.vue
-
-"export 'components' was not found in './recommend.vue?vue&type=template&id=49091a29&scoped=true&'
-
- warning  in ./pages/diet/recommendation.vue
-
-"export 'components' was not found in './recommendation.vue?vue&type=template&id=46b9b36a&scoped=true&'
-
- warning  in ./pages/diet/record-detail.vue
-
-"export 'components' was not found in './record-detail.vue?vue&type=template&id=68165dab&scoped=true&'
-
- warning  in ./pages/diet/records.vue
-
-"export 'components' was not found in './records.vue?vue&type=template&id=09815ca0&scoped=true&'
-
- warning  in ./pages/games/records.vue
-
-"export 'components' was not found in './records.vue?vue&type=template&id=12463f4b&scoped=true&'
-
- warning  in ./pages/tianpan/related-items.vue
-
-"export 'components' was not found in './related-items.vue?vue&type=template&id=ec943e3e&scoped=true&'
-
- warning  in ./pages/tianpan/relation-detail.vue
-
-"export 'components' was not found in './relation-detail.vue?vue&type=template&id=07bcb9c5&'
-
- warning  in ./pages/action-detail/relationship-questionnaire.vue
-
-"export 'components' was not found in './relationship-questionnaire.vue?vue&type=template&id=9b125724&scoped=true&'
-
- warning  in ./pages/health/report-confirm.vue
-
-"export 'components' was not found in './report-confirm.vue?vue&type=template&id=2762e791&scoped=true&'
-
- warning  in ./pages/profile/report-detail.vue
-
-"export 'components' was not found in './report-detail.vue?vue&type=template&id=3b08d87b&scoped=true&'
-
- warning  in ./pages/member-detail/report-detail.vue
-
-"export 'components' was not found in './report-detail.vue?vue&type=template&id=774601b6&'
-
- warning  in ./pages/health/report-list.vue
-
-"export 'components' was not found in './report-list.vue?vue&type=template&id=44d8eec6&scoped=true&'
-
- warning  in ./pages/profile/report-management.vue
-
-"export 'components' was not found in './report-management.vue?vue&type=template&id=75de966d&scoped=true&'
-
- warning  in ./pages/health/report-survey.vue
-
-"export 'components' was not found in './report-survey.vue?vue&type=template&id=704978ce&scoped=true&'
-
- warning  in ./pages/health/report-upload.vue
-
-"export 'components' was not found in './report-upload.vue?vue&type=template&id=3cec0280&scoped=true&'
-
- warning  in ./pages/dan-assessment/report-upload.vue
-
-"export 'components' was not found in './report-upload.vue?vue&type=template&id=b7c61d3c&'
-
- warning  in ./pages/assessment/results.vue
-
-"export 'components' was not found in './results.vue?vue&type=template&id=e3f24d3c&scoped=true&'
-
- warning  in ./pages/tasks/review.vue
-
-"export 'components' was not found in './review.vue?vue&type=template&id=28405d1c&scoped=true&'
-
- warning  in ./pages/rewards/rewards.vue
-
-"export 'components' was not found in './rewards.vue?vue&type=template&id=441c2960&scoped=true&'
-
- warning  in ./pages/child/rewards.vue
-
-"export 'components' was not found in './rewards.vue?vue&type=template&id=a4ccedf0&scoped=true&'
-
- warning  in ./pages/games/schulte.vue
-
-"export 'components' was not found in './schulte.vue?vue&type=template&id=1f4fb52e&scoped=true&'
-
- warning  in ./pages/mind-detail/screening.vue
-
-"export 'components' was not found in './screening.vue?vue&type=template&id=538777aa&scoped=true&'
-
- warning  in ./pages/shop/search/search.vue
-
-"export 'components' was not found in './search.vue?vue&type=template&id=90520c62&scoped=true&'
-
- warning  in ./pages/family/self-check-result.vue
-
-"export 'components' was not found in './self-check-result.vue?vue&type=template&id=cdc3fa1e&scoped=true&'
-
- warning  in ./pages/family/self-check.vue
-
-"export 'components' was not found in './self-check.vue?vue&type=template&id=6e9f8aae&scoped=true&'
-
- warning  in ./pages/wisdom-detail/self-quiz.vue
-
-"export 'components' was not found in './self-quiz.vue?vue&type=template&id=179ee61b&scoped=true&'
-
- warning  in ./pages/member-detail/service-role-apply.vue
-
-"export 'components' was not found in './service-role-apply.vue?vue&type=template&id=bc1e201a&'
-
- warning  in ./pages/teacher/service-tasks.vue
-
-"export 'components' was not found in './service-tasks.vue?vue&type=template&id=05a1d7e0&scoped=true&'
-
- warning  in ./pages/guide/packages/share.vue
-
-"export 'components' was not found in './share.vue?vue&type=template&id=36fd7fa9&scoped=true&'
-
- warning  in ./pages/guide/invite/share.vue
-
-"export 'components' was not found in './share.vue?vue&type=template&id=b775a876&scoped=true&'
-
- warning  in ./pages/health/sleep-index.vue
-
-"export 'components' was not found in './sleep-index.vue?vue&type=template&id=5d67aee2&scoped=true&'
-
- warning  in ./pages/mind-detail/soothe-toolbox.vue
-
-"export 'components' was not found in './soothe-toolbox.vue?vue&type=template&id=8dfbcb40&scoped=true&'
-
- warning  in ./pages/games/stats.vue
-
-"export 'components' was not found in './stats.vue?vue&type=template&id=51aed130&scoped=true&'
-
- warning  in ./pages/games/sudoku.vue
-
-"export 'components' was not found in './sudoku.vue?vue&type=template&id=7117a3c4&scoped=true&'
-
- warning  in ./pages/growth/supplement.vue
-
-"export 'components' was not found in './supplement.vue?vue&type=template&id=56227258&scoped=true&'
-
- warning  in ./components/tab-transition.vue
-
-"export 'components' was not found in './tab-transition.vue?vue&type=template&id=1ab24bf0&scoped=true&'
-
- warning  in ./pages/child/tasks.vue
-
-"export 'components' was not found in './tasks.vue?vue&type=template&id=32d1e91c&scoped=true&'
-
- warning  in ./pages/tasks/tasks.vue
-
-"export 'components' was not found in './tasks.vue?vue&type=template&id=9828a938&scoped=true&'
-
- warning  in ./pages/teacher/teacher-profile.vue
-
-"export 'components' was not found in './teacher-profile.vue?vue&type=template&id=4436c3a8&scoped=true&'
-
- warning  in ./pages/teacher/team.vue
-
-"export 'components' was not found in './team.vue?vue&type=template&id=200db463&scoped=true&'
-
- warning  in ./pages/promotion/team.vue
-
-"export 'components' was not found in './team.vue?vue&type=template&id=9c708efc&scoped=true&'
-
- warning  in ./pages/health/tongue-index.vue
-
-"export 'components' was not found in './tongue-index.vue?vue&type=template&id=5be29cb3&'
-
- warning  in ./pages/mind-detail/traditional-mirror.vue
-
-"export 'components' was not found in './traditional-mirror.vue?vue&type=template&id=008673be&scoped=true&'
-
- warning  in ./pages/wisdom-detail/training-hub.vue
-
-"export 'components' was not found in './training-hub.vue?vue&type=template&id=7df4ddad&scoped=true&'
-
- warning  in ./pages/mind-detail/training.vue
-
-"export 'components' was not found in './training.vue?vue&type=template&id=b908ea54&scoped=true&'
-
- warning  in ./pages/membership/upgrade.vue
-
-"export 'components' was not found in './upgrade.vue?vue&type=template&id=2922079c&scoped=true&'
-
- warning  in ./pages/assessment/upload-report.vue
-
-"export 'components' was not found in './upload-report.vue?vue&type=template&id=293eb7ec&scoped=true&'
-
- warning  in ./pages/growth/upload.vue
-
-"export 'components' was not found in './upload.vue?vue&type=template&id=3f439f20&scoped=true&'
-
- warning  in ./pages/user-edit/user-edit.vue
-
-"export 'components' was not found in './user-edit.vue?vue&type=template&id=64c38fe0&scoped=true&'
-
- warning  in ./pages/health/water-index.vue
-
-"export 'components' was not found in './water-index.vue?vue&type=template&id=33c47522&scoped=true&'
-
- warning  in ./pages/promotion/withdraw.vue
-
-"export 'components' was not found in './withdraw.vue?vue&type=template&id=39f2942f&scoped=true&'
-
- warning  in ./components/wuxing-sandbox.vue
-
-"export 'components' was not found in './wuxing-sandbox.vue?vue&type=template&id=d4ff2986&scoped=true&'
-
- warning  in ./store/health-alert.js
-
-"export 'default' (imported as 'api') was not found in '@/utils/api.js'
-
- warning  in ./pages/home-pages/parent-index.vue?vue&type=script&lang=js&
-
-"export 'default' (imported as 'feedbackEngine') was not found in '../../utils/feedback-engine.js'
-
- warning  in ./store/feedback.js
-
-"export 'default' (imported as 'feedbackEngine') was not found in '../utils/feedback-engine.js'
-
- DONE  Build complete. The dist\dev\mp-weixin directory is ready to be deployed.
-Run method: open Weixin Mini Program Devtools, import dist\dev\mp-weixin run.

+ 0 - 226
cfc-frontend/components/ProfileHeader.vue

@@ -1,226 +0,0 @@
-<template>
-  <view>
-    <!-- 退出切换按钮(仅家长切换到孩子状态时显示) -->
-    <view class="exit-switch" v-if="isSwitchedChild" @click="onExitSwitch">
-      <text>🔄 退出切换</text>
-    </view>
-
-    <view class="user-card">
-      <view class="avatar" @click="$emit('avatar-click')">👤</view>
-      <view class="user-info">
-        <view class="nickname">{{ nickname || '未设置昵称' }}</view>
-        <view class="role">{{ role === 'parent' ? '家长模式' : (role === 'child' ? '孩子模式' : '成长规划师模式') }}</view>
-        <view class="system-points" v-if="role === 'parent' && children.length > 0">🏅 系统积分: {{ children[0].systemPoints || 0 }}</view>
-      </view>
-      <!-- 切换按钮仅在非切换状态下显示 -->
-      <button class="btn-switch" v-if="!isSwitchedChild" @click="onSwitchMode">切换</button>
-    </view>
-  </view>
-</template>
-
-<script>
-import { getChildren, verifyPassword, switchToFamilyMember } from '../utils/api.js'
-
-export default {
-  name: 'ProfileHeader',
-  data() {
-    return {
-      nickname: '',
-      role: 'parent',
-      children: [],
-      isSwitchedChild: false
-    }
-  },
-  computed: {
-    isLoggedIn() {
-      return !!uni.getStorageSync('token')
-    }
-  },
-  onShow() {
-    if (!uni.getStorageSync('token')) return
-    const currentRole = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
-    const userInfo = uni.getStorageSync('userInfo')
-    this.nickname = this.$store.state.nickname || (userInfo && userInfo.nickname) || ''
-    this.role = currentRole
-    this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
-    this.loadChildren()
-  },
-  methods: {
-    async loadChildren() {
-      try {
-        const res = await getChildren()
-        this.children = res.data || []
-      } catch (e) {
-        console.error('获取孩子列表失败', e)
-      }
-    },
-    onSwitchMode() {
-      if (this.role === 'child') {
-        uni.showModal({
-          title: '验证密码',
-          content: '请输入家长密码',
-          editable: true,
-          success: (res) => {
-            if (res.confirm && res.content) {
-              verifyPassword(res.content).then((result) => {
-                if (result.code === 200) {
-                  this.doSwitchMode()
-                } else {
-                  uni.showToast({ title: '密码错误', icon: 'none' })
-                }
-              }).catch(() => {
-                uni.showToast({ title: '验证失败', icon: 'none' })
-              })
-            }
-          }
-        })
-      } else {
-        this.doSwitchMode()
-      }
-    },
-    async doSwitchMode() {
-      // 成员视角(已切换到不可登录成员)下不可再切换其他成员,只能退出切换
-      if (this.$store.state.currentView === 'member') {
-        uni.showToast({ title: '当前为成员视角,请先退出切换', icon: 'none' })
-        return
-      }
-      if (this.children.length === 0) {
-        this.loadChildren()
-        if (this.children.length === 0) {
-          uni.showModal({
-            title: '提示',
-            content: '您还没有添加成员,请先在家庭成员中添加',
-            success: (res) => {
-              if (res.confirm) {
-                uni.navigateTo({ url: '/pages/profile-extra/family-members' })
-              }
-            }
-          })
-          return
-        }
-      }
-      // 优先显示可切换成员(不可登录成员),过滤掉可通过微信登录的成员
-      var switchable = this.children.filter(function(m) { return m.isSwitchable })
-      if (switchable.length === 0) {
-        uni.showToast({ title: '暂无可切换成员', icon: 'none' })
-        return
-      }
-      if (switchable.length === 1) {
-        var member = switchable[0]
-        uni.showModal({
-          title: '切换确认',
-          content: '切换到"' + member.nickname + '"后,退出时需要输入您的密码。是否继续?',
-          success: async (res) => {
-            if (!res.confirm) return
-            try {
-              var result = await switchToFamilyMember(member.id, member.source || 'family_member')
-              if (result.code === 200 && result.data) {
-                this.$store.commit('switchToMember', {
-                  memberId: result.data.memberId,
-                  effectiveRole: result.data.effectiveRole
-                })
-                this.isSwitchedChild = true
-                this.role = result.data.effectiveRole
-                uni.showToast({ title: '已切换', icon: 'success' })
-              } else {
-                uni.showToast({ title: result.message || '切换失败', icon: 'none' })
-              }
-            } catch (e) {
-              uni.showToast({ title: '切换失败', icon: 'none' })
-            }
-          }
-        })
-        return
-      }
-      uni.showActionSheet({
-        itemList: switchable.map(function(m) { return m.nickname }),
-        success: (res) => {
-          if (res.tapIndex < 0) return
-          var member = switchable[res.tapIndex]
-          this.$store.commit('switchToMember', {
-            memberId: member.id,
-            effectiveRole: member.effectiveRole
-          })
-          this.isSwitchedChild = true
-          this.role = member.effectiveRole
-          uni.showToast({ title: '已切换', icon: 'success' })
-        }
-      })
-    },
-    async onExitSwitch() {
-      if (!this.isSwitchedChild) return
-      var self = this
-      uni.showModal({
-        title: '退出切换',
-        content: '请输入原用户密码以确认退出',
-        editable: true,
-        success: async function(res) {
-          if (!res.confirm || !res.content) return
-          try {
-            var ok = await self.$store.dispatch('switchBackWithPassword', res.content)
-            if (ok) {
-              self.isSwitchedChild = false
-              self.role = 'parent'
-              uni.showToast({ title: '已退出切换', icon: 'success' })
-              uni.reLaunch({ url: '/pages/index-home/index' })
-            } else {
-              uni.showToast({ title: '密码错误', icon: 'none' })
-            }
-          } catch (e) {
-            uni.showToast({ title: '验证失败', icon: 'none' })
-          }
-        }
-      })
-    }
-  }
-}
-</script>
-
-<style scoped>
-.exit-switch {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  margin: 0 30rpx 16rpx;
-  background: linear-gradient(135deg, #667eea, #764ba2);
-  color: #fff;
-  padding: 16rpx;
-  border-radius: 12rpx;
-  font-size: 26rpx;
-}
-
-.user-card {
-  display: flex;
-  align-items: center;
-  background: #fff;
-  margin: 0 30rpx 20rpx;
-  border-radius: 20rpx;
-  padding: 30rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-}
-.avatar {
-  width: 100rpx;
-  height: 100rpx;
-  border-radius: 50%;
-  background: linear-gradient(135deg, #667eea, #764ba2);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 48rpx;
-  margin-right: 24rpx;
-  flex-shrink: 0;
-}
-.user-info { flex: 1; }
-.nickname { font-size: 34rpx; font-weight: bold; color: #333; margin-bottom: 8rpx; }
-.role { font-size: 24rpx; color: #999; margin-bottom: 6rpx; }
-.system-points { font-size: 24rpx; color: #F97316; margin-top: 4rpx; }
-
-.btn-switch {
-  background: #667eea;
-  color: #fff;
-  font-size: 24rpx;
-  padding: 10rpx 30rpx;
-  border-radius: 30rpx;
-  line-height: 1.8;
-}
-</style>

+ 0 - 45
cfc-frontend/fix-hbuilderx-scoped.js

@@ -1,45 +0,0 @@
-#!/usr/bin/env node
-
-const fs = require('fs');
-const path = require('path');
-
-// HBuilderX 内部的 scoped.js 路径
-const SCOPED_FILE = '/Applications/HBuilderX.app/Contents/HBuilderX/plugins/uniapp-cli/node_modules/@dcloudio/vue-cli-plugin-uni/packages/@vue/component-compiler-utils/dist/stylePlugins/scoped.js';
-
-if (!fs.existsSync(SCOPED_FILE)) {
-  console.error('错误:找不到 HBuilderX 的 scoped.js 文件');
-  console.error(`路径:${SCOPED_FILE}`);
-  process.exit(1);
-}
-
-console.log(`正在修复 ${SCOPED_FILE}...`);
-
-let content = fs.readFileSync(SCOPED_FILE, 'utf8');
-
-// 修复 1: n.spaces.before = n.spaces.after = ''; (第 40 行)
-content = content.replace(
-  /n\.value = ' ';[\s\n]*n\.spaces\.before = n\.spaces\.after = '';/,
-  `n.value = ' ';\n            n.spaces = n.spaces || {}; n.spaces.before = ''; n.spaces.after = '';`
-);
-
-// 修复 2: n.value = n.spaces.before = n.spaces.after = ''; (::v-deep)
-content = content.replace(
-  /if \(n\.type === '::v-deep' \) \{[\s\n]*n\.value = n\.spaces\.before = n\.spaces\.after = '';[\s\n]*return false;[\s\n]*\}/,
-  `if (n.type === '::v-deep') {\n            n.value = '';\n            n.spaces = { before: '', after: '' };\n            return false;\n          }`
-);
-
-// 修复 3: node.spaces.after = '';
-content = content.replace(
-  /if \(node\) \{[\s\n]*node\.spaces\.after = '';[\s\n]\}/,
-  `if (node) {\n          node.spaces = node.spaces || {}; node.spaces.after = '';`
-);
-
-// 修复 4: selector.first.spaces.before = '';
-content = content.replace(
-  /selector\.first\.spaces\.before = '';/,
-  `if (selector.first && selector.first.spaces) { selector.first.spaces.before = ''; }`
-);
-
-fs.writeFileSync(SCOPED_FILE, content, 'utf8');
-
-console.log('✅ 修复完成!请重启 HBuilderX 并重新编译。');

+ 3 - 0
cfc-frontend/manifest.json

@@ -47,6 +47,9 @@
       "es6": true,
       "postcss": true,
       "minified": true
+    },
+    "optimization": {
+      "subPackages": true
     }
   },
   "mp-alipay": {},

+ 0 - 0
cfc-frontend/components/AbilityDiagram.vue → cfc-frontend/pages/action-detail/components/AbilityDiagram.vue


+ 0 - 0
cfc-frontend/components/OctopusDiagram.vue → cfc-frontend/pages/action-detail/components/OctopusDiagram.vue


+ 1 - 1
cfc-frontend/components/PearlDiagram.vue → cfc-frontend/pages/action-detail/components/PearlDiagram.vue

@@ -58,7 +58,7 @@
 </template>
 
 <script>
-import { addPearlInteraction, updatePearlPriority } from '../utils/api.js'
+import { addPearlInteraction, updatePearlPriority } from '@/utils/api.js'
 
 /**
  * PearlDiagram - 珍珠图 Canvas 组件

+ 3 - 3
cfc-frontend/pages/action-detail/index.vue

@@ -209,9 +209,9 @@ import TabTransition from '../../components/tab-transition.vue'
 import PageBanner from '../../components/PageBanner.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
-import PearlDiagram from '../../components/PearlDiagram.vue'
-import AbilityDiagram from '../../components/AbilityDiagram.vue'
-import OctopusDiagram from '../../components/OctopusDiagram.vue'
+import PearlDiagram from '@/pages/action-detail/components/PearlDiagram.vue'
+import AbilityDiagram from '@/pages/action-detail/components/AbilityDiagram.vue'
+import OctopusDiagram from '@/pages/action-detail/components/OctopusDiagram.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'

+ 3 - 3
cfc-frontend/pages/ai/chat.vue

@@ -165,9 +165,9 @@ import { aiSendMessage, aiGetConversations, aiGetMessages, aiDeleteConversation,
 } from '../../utils/api.js'
 import { sttTranscribe } from '../../utils/api.js'
 import BaseEmpty from '../../components/BaseEmpty.vue'
-import TaskCard from '../../components/AITaskCard.vue'
-import QuotaProgressBar from '../../components/QuotaProgressBar.vue'
-import UpgradeGuideModal from '../../components/UpgradeGuideModal.vue'
+import TaskCard from '@/pages/ai/components/AITaskCard.vue'
+import QuotaProgressBar from '@/pages/ai/components/QuotaProgressBar.vue'
+import UpgradeGuideModal from '@/pages/ai/components/UpgradeGuideModal.vue'
 import config from '@/config.js'
 
 export default {

+ 0 - 0
cfc-frontend/components/AITaskCard.vue → cfc-frontend/pages/ai/components/AITaskCard.vue


+ 0 - 0
cfc-frontend/components/QuotaProgressBar.vue → cfc-frontend/pages/ai/components/QuotaProgressBar.vue


+ 0 - 0
cfc-frontend/components/UpgradeGuideModal.vue → cfc-frontend/pages/ai/components/UpgradeGuideModal.vue


+ 2 - 2
cfc-frontend/pages/discover/circles.vue

@@ -141,8 +141,8 @@
 </template>
 
 <script>
-import CircleCard from '../../components/CircleCard.vue'
-import CircleDetail from '../../components/CircleDetail.vue'
+import CircleCard from '@/pages/discover/components/CircleCard.vue'
+import CircleDetail from '@/pages/discover/components/CircleDetail.vue'
 import { discoverCircles, getMyCircles, joinGeneralCircle, leaveGeneralCircle, getPendingCircleMembers, approveCircleMember, updateCircle, reportAppEnter
 } from '../../utils/api.js'
 import { parseDate } from '@/utils/format.js'

+ 0 - 0
cfc-frontend/components/CircleCard.vue → cfc-frontend/pages/discover/components/CircleCard.vue


+ 0 - 0
cfc-frontend/components/CircleDetail.vue → cfc-frontend/pages/discover/components/CircleDetail.vue


+ 1 - 1
cfc-frontend/pages/family/add-member.vue

@@ -108,7 +108,7 @@
 
 <script>
 import { addFamilyMember, updateFamilyMember, recordConsent, checkPhone } from '../../utils/api.js'
-import GenerationPicker from '../../components/GenerationPicker.vue'
+import GenerationPicker from '@/pages/family/components/GenerationPicker.vue'
 import { parseDate } from '../../utils/format.js'
 
 export default {

+ 1 - 1
cfc-frontend/components/GenerationPicker.vue → cfc-frontend/pages/family/components/GenerationPicker.vue

@@ -33,7 +33,7 @@
 </template>
 
 <script>
-import genLevel from '../utils/generationLevel.js'
+import genLevel from '@/utils/generationLevel.js'
 var GENERATION_LEVELS = genLevel.GENERATION_LEVELS
 var PEER_TYPES = genLevel.PEER_TYPES
 var getGenerationLabel = genLevel.getGenerationLabel

+ 19 - 9
cfc-frontend/pages/family/self-check-entry.vue

@@ -19,6 +19,14 @@
       </view>
 
       <!-- 维度分数条 -->
+      <!-- 自检简易说明 -->
+      <view class="sce-card">
+        <view class="sce-card-title">
+          <text class="sce-card-title-text">自检简易说明</text>
+        </view>
+        <text class="sce-info-text">五维自检从「身·智·富·行·心」五个维度评估家庭状态,每维满分 9 分,总分 45 分。建议每 15 天完成一次自检,重点关注分数的变化趋势,而不是单次绝对值。</text>
+      </view>
+
       <view class="sce-card" v-if="lastResult && lastResult.dimensions && lastResult.dimensions.length > 0">
         <view class="sce-card-title">
           <text class="sce-card-title-text">五维得分</text>
@@ -51,7 +59,7 @@
       </view>
 
       <!-- 趋势分析(P1-1) -->
-      <view class="sce-trend" v-if="trendLoaded">
+      <view class="sce-trend" v-if="trendLoaded && (aiInsight || trendSummary)">
         <view class="sce-trend-header">
           <text class="sce-trend-title">📈 自检趋势</text>
           <text class="sce-trend-summary" v-if="trendSummary">{{ trendSummary }}</text>
@@ -311,17 +319,20 @@ export default {
 }
 .sce-footer {
   display: flex;
-  flex-direction: column;
-  gap: 16rpx;
+  flex-direction: row;
+  gap: 20rpx;
   margin-top: 20rpx;
 }
 .sce-btn {
+  flex: 1;
   height: 88rpx;
   line-height: 88rpx;
   font-size: 30rpx;
   border-radius: 44rpx;
   text-align: center;
   border: none;
+  margin-left: 0;
+  margin-right: 0;
 }
 .sce-btn-primary {
   background: linear-gradient(135deg, #F97316, #FF8C42);
@@ -367,11 +378,10 @@ export default {
   color: #555;
   line-height: 1.6;
 }
-.sce-trend-loading {
-  padding: 10rpx 0;
-}
-.sce-trend-loading text {
-  font-size: 24rpx;
-  color: #999;
+.sce-info-text {
+  display: block;
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.6;
 }
 </style>

+ 1 - 156
cfc-frontend/pages/family/self-check-result.vue

@@ -1,4 +1,4 @@
-<template>
+<template>
   <view class="scr-page">
     <!-- Loading -->
     <view class="scr-loading" v-if="loading">
@@ -21,22 +21,6 @@
         <text class="scr-hero-date" v-if="result.createdAt">自检时间:{{ formatDate(result.createdAt) }}</text>
       </view>
 
-      <!-- 雷达图 -->
-      <view class="scr-card">
-        <view class="scr-card-title">
-          <text class="scr-card-title-text">五维能量雷达</text>
-          <text class="scr-card-sub">身·智·富·行·心</text>
-        </view>
-        <view class="scr-canvas-wrap">
-          <canvas
-            type="2d"
-            id="selfCheckRadar"
-            class="scr-canvas"
-            @ready="onCanvasReady"
-          ></canvas>
-        </view>
-      </view>
-
       <!-- 维度得分 -->
       <view class="scr-card">
         <view class="scr-card-title">
@@ -130,13 +114,6 @@ export default {
       loading: true,
       result: null,
       selfCheckId: '',
-      canvasContext: null,
-      canvasWidth: 340,
-      canvasHeight: 340,
-      // 雷达图轴序(对应 tianpan 木火土金水,顺时针)
-      axes: ['木', '火', '土', '金', '水'],
-      elementKeys: ['action', 'mind', 'body', 'wisdom', 'wealth'],
-      elementColors: ['#10B981', '#FF6B9D', '#FF8C42', '#6366F1', '#F59E0B'],
       levelMap: {
         healthy: '健康',
         attention: '留意',
@@ -176,14 +153,12 @@ export default {
       if (cached && cached.id) {
         this.result = cached
         this.loading = false
-        this.drawRadar()
         return
       }
       getSelfCheckLatest().then(function(res) {
         self.loading = false
         if (res.code === 200 && res.data) {
           self.result = res.data
-          self.drawRadar()
         } else {
           uni.showToast({ title: (res && res.message) || '暂无自检记录', icon: 'none' })
         }
@@ -192,126 +167,6 @@ export default {
         uni.showToast({ title: '加载失败', icon: 'none' })
       })
     },
-    onCanvasReady: function(res) {
-      if (!res || !res[0] || !res[0].node) {
-        return
-      }
-      var ctx = res[0].node.getContext('2d')
-      var dpr = uni.getWindowInfo().pixelRatio
-      this.canvasContext = ctx
-      this.canvasContext.scale(dpr, dpr)
-      this.drawRadar()
-    },
-    drawRadar: function() {
-      if (!this.canvasContext || !this.result) {
-        return
-      }
-      var ctx = this.canvasContext
-      var w = this.canvasWidth
-      var h = this.canvasHeight
-      var cx = w / 2
-      var cy = h / 2
-      var r = w * 0.34
-      var angleStep = (Math.PI * 2) / 5
-
-      // 清空画布
-      ctx.clearRect(0, 0, w, h)
-      ctx.fillStyle = '#FFF8F0'
-      ctx.fillRect(0, 0, w, h)
-
-      // 维度分数映射
-      var scores = {}
-      var dims = this.result.dimensions || []
-      for (var i = 0; i < dims.length; i++) {
-        scores[dims[i].dimension] = dims[i].score || 0
-      }
-
-      // 背景五边形(三层网格)
-      for (var layer = 1; layer <= 3; layer++) {
-        ctx.beginPath()
-        for (var g = 0; g < 5; g++) {
-          var gAngle = g * angleStep - Math.PI / 2
-          var gx = cx + r * (layer / 3) * Math.cos(gAngle)
-          var gy = cy + r * (layer / 3) * Math.sin(gAngle)
-          if (g === 0) {
-            ctx.moveTo(gx, gy)
-          } else {
-            ctx.lineTo(gx, gy)
-          }
-        }
-        ctx.closePath()
-        ctx.strokeStyle = 'rgba(249, 114, 22, 0.15)'
-        ctx.lineWidth = 1
-        ctx.stroke()
-      }
-
-      // 轴线 + 标签
-      for (var j = 0; j < 5; j++) {
-        var a2 = j * angleStep - Math.PI / 2
-        var lx = cx + r * Math.cos(a2)
-        var ly = cy + r * Math.sin(a2)
-        ctx.beginPath()
-        ctx.moveTo(cx, cy)
-        ctx.lineTo(lx, ly)
-        ctx.strokeStyle = this.elementColors[j] + '44'
-        ctx.lineWidth = 1
-        ctx.stroke()
-
-        var labelX = cx + (r + 18) * Math.cos(a2)
-        var labelY = cy + (r + 18) * Math.sin(a2)
-        ctx.fillStyle = this.elementColors[j]
-        ctx.font = 'bold 13px sans-serif'
-        ctx.textAlign = 'center'
-        ctx.textBaseline = 'middle'
-        ctx.fillText(this.axes[j], labelX, labelY)
-      }
-
-      // 数据驱动多边形
-      ctx.beginPath()
-      for (var m = 0; m < 5; m++) {
-        var ang = m * angleStep - Math.PI / 2
-        var key = this.elementKeys[m]
-        var ratio = (scores[key] || 0) / 9
-        var dataR = r * ratio
-        var dx = cx + dataR * Math.cos(ang)
-        var dy = cy + dataR * Math.sin(ang)
-        if (m === 0) {
-          ctx.moveTo(dx, dy)
-        } else {
-          ctx.lineTo(dx, dy)
-        }
-      }
-      ctx.closePath()
-      ctx.fillStyle = 'rgba(249, 114, 22, 0.2)'
-      ctx.fill()
-      ctx.strokeStyle = '#F97316'
-      ctx.lineWidth = 2
-      ctx.stroke()
-
-      // 数据节点圆点 + 分数标签
-      for (var n = 0; n < 5; n++) {
-        var ang2 = n * angleStep - Math.PI / 2
-        var key2 = this.elementKeys[n]
-        var ratio2 = (scores[key2] || 0) / 9
-        var dataR2 = r * ratio2
-        var px = cx + dataR2 * Math.cos(ang2)
-        var py = cy + dataR2 * Math.sin(ang2)
-
-        ctx.beginPath()
-        ctx.arc(px, py, 4, 0, Math.PI * 2)
-        ctx.fillStyle = this.elementColors[n]
-        ctx.fill()
-
-        var pctLabelR = dataR2 + (ratio2 > 0.35 ? -16 : 18)
-        var pLabelX = cx + pctLabelR * Math.cos(ang2)
-        var pLabelY = cy + pctLabelR * Math.sin(ang2)
-        ctx.fillStyle = this.elementColors[n]
-        ctx.font = 'bold 11px sans-serif'
-        ctx.textAlign = 'center'
-        ctx.textBaseline = 'middle'
-        ctx.fillText(String(scores[key2] || 0), pLabelX, pLabelY)
-      }
-    },
     formatDate: function(dateStr) {
       if (!dateStr) {
         return ''
@@ -504,16 +359,6 @@ export default {
   color: #999;
 }
 
-/* 雷达图 */
-.scr-canvas-wrap {
-  display: flex;
-  justify-content: center;
-}
-.scr-canvas {
-  width: 340px;
-  height: 340px;
-}
-
 /* 维度得分 */
 .scr-dim-list {
   display: flex;

+ 1 - 1
cfc-frontend/components/MemberChipPicker.vue → cfc-frontend/pages/health/components/MemberChipPicker.vue

@@ -25,7 +25,7 @@
 </template>
 
 <script>
-import { getFamilyMemberList } from '../utils/api.js'
+import { getFamilyMemberList } from '@/utils/api.js'
 
 export default {
   name: 'MemberChipPicker',

+ 1 - 1
cfc-frontend/components/feedback-popup.vue → cfc-frontend/pages/health/components/feedback-popup.vue

@@ -47,7 +47,7 @@
 </template>
 
 <script>
-import { submitFeedback } from '../utils/api.js'
+import { submitFeedback } from '@/utils/api.js'
 
 export default {
   props: {

+ 0 - 0
cfc-frontend/components/health-knowledge-popup.vue → cfc-frontend/pages/health/components/health-knowledge-popup.vue


+ 2 - 2
cfc-frontend/pages/health/gut-flora-detail.vue

@@ -333,8 +333,8 @@
 
 <script>
 import { getReportDetail, editHealthReport, queryKnowledgeBase, queryIndicatorKnowledge, getGutFloraAnalysis, confirmReportPreview, discardReportDraft, parseReportDraft, getHealthFoods, getFoodCautionList, getFamilyMemberList, addFamilyMember } from '../../utils/api.js'
-import HealthKnowledgePopup from '../../components/health-knowledge-popup.vue'
-import FeedbackPopup from '../../components/feedback-popup.vue'
+import HealthKnowledgePopup from '@/pages/health/components/health-knowledge-popup.vue'
+import FeedbackPopup from '@/pages/health/components/feedback-popup.vue'
 import { parseDate } from '../../utils/format.js'
 
 var INDICATOR_TYPE_MAP = {

+ 1 - 1
cfc-frontend/pages/health/gut-flora-risks-detail.vue

@@ -113,7 +113,7 @@
 
 <script>
 import { getReportDetail, editHealthReport, queryKnowledgeBase, queryIndicatorKnowledge, queryDiseaseKnowledge } from '../../utils/api.js'
-import HealthKnowledgePopup from '../../components/health-knowledge-popup.vue'
+import HealthKnowledgePopup from '@/pages/health/components/health-knowledge-popup.vue'
 
 export default {
   components: { HealthKnowledgePopup },

+ 1 - 1
cfc-frontend/pages/health/gut-flora-species-detail.vue

@@ -51,7 +51,7 @@
 
 <script>
 import { getReportDetail, editHealthReport, queryKnowledgeBase, queryBacteriaKnowledge } from '../../utils/api.js'
-import HealthKnowledgePopup from '../../components/health-knowledge-popup.vue'
+import HealthKnowledgePopup from '@/pages/health/components/health-knowledge-popup.vue'
 
 export default {
   components: { HealthKnowledgePopup },

+ 1 - 1
cfc-frontend/pages/health/health-plan-summary.vue

@@ -271,7 +271,7 @@
 <script>
 import { generateHealthPlan, regenerateSection, confirmPlan, saveHealthPlan, getHealthPlanDetail, approvePlan } from '../../utils/api'
 import { getHealthStatus, saveHealthStatus } from '../../utils/api'
-import MemberChipPicker from '../../components/MemberChipPicker.vue'
+import MemberChipPicker from '@/pages/health/components/MemberChipPicker.vue'
 
 // v2: 预置疾病清单(菌群报告病症风险 ★ + 互联网统计常见病 两源合并去重,8 组 54 项)
 var DISEASE_GROUPS = [

+ 1 - 1
cfc-frontend/pages/health/report-confirm.vue

@@ -358,7 +358,7 @@
 <script>
 import { confirmReportPreview, discardReportDraft, confirmTongue, discardTongue, parseReportDraft } from '../../utils/api.js'
 import { addFamilyMember, getFamilyMemberList } from '../../utils/api.js'
-import FeedbackPopup from '../../components/feedback-popup.vue'
+import FeedbackPopup from '@/pages/health/components/feedback-popup.vue'
 
 export default {
   components: { FeedbackPopup },

+ 1 - 1
cfc-frontend/pages/health/report-detail.vue

@@ -406,7 +406,7 @@
 
 <script>
 import ReportBlocksRenderer from '../../components/report-blocks-renderer.vue'
-import FeedbackPopup from '../../components/feedback-popup.vue'
+import FeedbackPopup from '@/pages/health/components/feedback-popup.vue'
 import { getReportDetail, getDanReportDetail, getReportPayload, editHealthReport, parseReportDraft, confirmReportPreview, discardReportDraft, confirmTongue, discardTongue, getFamilyMemberList, addFamilyMember, queryIndicatorKnowledge, queryBacteriaKnowledge } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 

+ 2 - 2
cfc-frontend/pages/home-pages/child-index.vue

@@ -325,10 +325,10 @@ import BaseBadge from '../../components/BaseBadge.vue'
 import BaseEmpty from '../../components/BaseEmpty.vue'
 import BaseLoading from '../../components/BaseLoading.vue'
 import GateLock from '../../components/gate-lock.vue'
-import GateGuideBar from '../../components/gate-guide-bar.vue'
+import GateGuideBar from '@/pages/home-pages/components/gate-guide-bar.vue'
 import OnboardingGuide from '../../components/OnboardingGuide.vue'
 import ConfigurableGuide from '../../components/ConfigurableGuide.vue'
-import ConfettiCelebration from '../../components/ConfettiCelebration.vue'
+import ConfettiCelebration from '@/pages/home-pages/components/ConfettiCelebration.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 
 /**

+ 0 - 0
cfc-frontend/components/AdBanner.vue → cfc-frontend/pages/home-pages/components/AdBanner.vue


+ 0 - 0
cfc-frontend/components/CircleRanking.vue → cfc-frontend/pages/home-pages/components/CircleRanking.vue


+ 0 - 0
cfc-frontend/components/ConfettiCelebration.vue → cfc-frontend/pages/home-pages/components/ConfettiCelebration.vue


+ 0 - 0
cfc-frontend/components/ContactCard.vue → cfc-frontend/pages/home-pages/components/ContactCard.vue


+ 1 - 1
cfc-frontend/components/ContactImport.vue → cfc-frontend/pages/home-pages/components/ContactImport.vue

@@ -49,7 +49,7 @@
 </template>
 
 <script>
-import { importPhoneContact } from '../utils/api.js'
+import { importPhoneContact } from '@/utils/api.js'
 
 var DEFAULT_RELATIONSHIP_TYPES = [
   { value: 'family', label: '家人' },

+ 0 - 0
cfc-frontend/components/DailySummary.vue → cfc-frontend/pages/home-pages/components/DailySummary.vue


+ 0 - 0
cfc-frontend/components/FamilyHealthRanking.vue → cfc-frontend/pages/home-pages/components/FamilyHealthRanking.vue


+ 0 - 0
cfc-frontend/components/FeedbackToast.vue → cfc-frontend/pages/home-pages/components/FeedbackToast.vue


+ 0 - 0
cfc-frontend/components/HealthAlertBanner.vue → cfc-frontend/pages/home-pages/components/HealthAlertBanner.vue


+ 0 - 0
cfc-frontend/components/MicroActionCard.vue → cfc-frontend/pages/home-pages/components/MicroActionCard.vue


+ 0 - 0
cfc-frontend/components/MilestoneCelebration.vue → cfc-frontend/pages/home-pages/components/MilestoneCelebration.vue


+ 0 - 0
cfc-frontend/components/gate-guide-bar.vue → cfc-frontend/pages/home-pages/components/gate-guide-bar.vue


+ 11 - 11
cfc-frontend/pages/home-pages/parent-index.vue

@@ -500,23 +500,23 @@ import BaseEmpty from '../../components/BaseEmpty.vue'
 import BaseLoading from '../../components/BaseLoading.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import WuxingSandbox from '../../components/wuxing-sandbox.vue'
-import ContactCard from '../../components/ContactCard.vue'
-import FamilyHealthRanking from '../../components/FamilyHealthRanking.vue'
+import ContactCard from '@/pages/home-pages/components/ContactCard.vue'
+import FamilyHealthRanking from '@/pages/home-pages/components/FamilyHealthRanking.vue'
 import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
-import ContactImport from '../../components/ContactImport.vue'
+import ContactImport from '@/pages/home-pages/components/ContactImport.vue'
 import RecommendedFeed from '../../components/RecommendedFeed.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
-import MicroActionCard from '../../components/MicroActionCard.vue'
-import FeedbackToast from '../../components/FeedbackToast.vue'
-import MilestoneCelebration from '../../components/MilestoneCelebration.vue'
-import DailySummary from '../../components/DailySummary.vue'
-import HealthAlertBanner from '../../components/HealthAlertBanner.vue'
-import CircleRanking from '../../components/CircleRanking.vue'
-import AdBanner from '../../components/AdBanner.vue'
+import MicroActionCard from '@/pages/home-pages/components/MicroActionCard.vue'
+import FeedbackToast from '@/pages/home-pages/components/FeedbackToast.vue'
+import MilestoneCelebration from '@/pages/home-pages/components/MilestoneCelebration.vue'
+import DailySummary from '@/pages/home-pages/components/DailySummary.vue'
+import HealthAlertBanner from '@/pages/home-pages/components/HealthAlertBanner.vue'
+import CircleRanking from '@/pages/home-pages/components/CircleRanking.vue'
+import AdBanner from '@/pages/home-pages/components/AdBanner.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import OnboardingProgressBar from '../../components/OnboardingProgressBar.vue'
 import GateLock from '../../components/gate-lock.vue'
-import GateGuideBar from '../../components/gate-guide-bar.vue'
+import GateGuideBar from '@/pages/home-pages/components/gate-guide-bar.vue'
 import feedbackEngine from '../../utils/feedback-engine.js'
 
 export default {

+ 2 - 2
cfc-frontend/components/member-detail-sections/action-section.vue → cfc-frontend/pages/member-detail/components/member-detail-sections/action-section.vue

@@ -63,8 +63,8 @@
 </template>
 
 <script>
-import { getTodayTasksByCategory, getCheckinStats, getActivityList } from '../../utils/api.js'
-import { parseDate } from '../../utils/format.js'
+import { getTodayTasksByCategory, getCheckinStats, getActivityList } from '@/utils/api.js'
+import { parseDate } from '@/utils/format.js'
 
 export default {
   props: {

+ 3 - 3
cfc-frontend/components/member-detail-sections/body-section.vue → cfc-frontend/pages/member-detail/components/member-detail-sections/body-section.vue

@@ -59,9 +59,9 @@
 </template>
 
 <script>
-import RadarChart from '../RadarChart.vue'
-import { getHealthAnalysis, getDimensionOverview, getCheckinList } from '../../utils/api.js'
-import { parseDate } from '../../utils/format.js'
+import RadarChart from '@/components/RadarChart.vue'
+import { getHealthAnalysis, getDimensionOverview, getCheckinList } from '@/utils/api.js'
+import { parseDate } from '@/utils/format.js'
 
 export default {
   components: { RadarChart },

+ 1 - 1
cfc-frontend/components/member-detail-sections/mind-section.vue → cfc-frontend/pages/member-detail/components/member-detail-sections/mind-section.vue

@@ -58,7 +58,7 @@
 </template>
 
 <script>
-import { getRelationshipScores, getContactList, getHealthAlerts, getMilestones } from '../../utils/api.js'
+import { getRelationshipScores, getContactList, getHealthAlerts, getMilestones } from '@/utils/api.js'
 
 export default {
   props: {

+ 2 - 2
cfc-frontend/components/member-detail-sections/wisdom-section.vue → cfc-frontend/pages/member-detail/components/member-detail-sections/wisdom-section.vue

@@ -43,8 +43,8 @@
 </template>
 
 <script>
-import { getAssessmentLatestResult, getAssessmentHistory } from '../../utils/api.js'
-import { parseDate } from '../../utils/format.js'
+import { getAssessmentLatestResult, getAssessmentHistory } from '@/utils/api.js'
+import { parseDate } from '@/utils/format.js'
 
 export default {
   props: {

+ 4 - 4
cfc-frontend/pages/member-detail/member-detail.vue

@@ -101,10 +101,10 @@ import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import RecommendedFeed from '../../components/RecommendedFeed.vue'
-import BodySection from '../../components/member-detail-sections/body-section.vue'
-import MindSection from '../../components/member-detail-sections/mind-section.vue'
-import WisdomSection from '../../components/member-detail-sections/wisdom-section.vue'
-import ActionSection from '../../components/member-detail-sections/action-section.vue'
+import BodySection from '@/pages/member-detail/components/member-detail-sections/body-section.vue'
+import MindSection from '@/pages/member-detail/components/member-detail-sections/mind-section.vue'
+import WisdomSection from '@/pages/member-detail/components/member-detail-sections/wisdom-section.vue'
+import ActionSection from '@/pages/member-detail/components/member-detail-sections/action-section.vue'
 import { getFamilyEnergySandbox, getActivityList, getProductsByDomain, getChildren } from '../../utils/api.js'
 
 // 维度配置

+ 0 - 0
cfc-frontend/components/FamilyTianpanCard.vue → cfc-frontend/pages/mind-detail/components/FamilyTianpanCard.vue


+ 2 - 2
cfc-frontend/components/PsychCrisisBanner.vue → cfc-frontend/pages/mind-detail/components/PsychCrisisBanner.vue

@@ -20,8 +20,8 @@
 </template>
 
 <script>
-import config from '../config'
-import { parseDate } from '../utils/format'
+import config from '@/config.js'
+import { parseDate } from '@/utils/format.js'
 
 export default {
   props: {

+ 1 - 1
cfc-frontend/pages/mind-detail/emotion-checkin.vue

@@ -201,7 +201,7 @@
 
 <script>
 import config from '../../config'
-import PsychCrisisBanner from '../../components/PsychCrisisBanner.vue'
+import PsychCrisisBanner from '@/pages/mind-detail/components/PsychCrisisBanner.vue'
 
 export default {
   components: { PsychCrisisBanner },

+ 1 - 1
cfc-frontend/pages/mind-detail/emotion-trend.vue

@@ -124,7 +124,7 @@
 
 <script>
 import config from '../../config'
-import PsychCrisisBanner from '../../components/PsychCrisisBanner.vue'
+import PsychCrisisBanner from '@/pages/mind-detail/components/PsychCrisisBanner.vue'
 import { parseDate } from '../../utils/format.js'
 
 export default {

+ 2 - 2
cfc-frontend/pages/mind-detail/index.vue

@@ -192,9 +192,9 @@ import PageBanner from '../../components/PageBanner.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
-import PsychCrisisBanner from '../../components/PsychCrisisBanner.vue'
+import PsychCrisisBanner from '@/pages/mind-detail/components/PsychCrisisBanner.vue'
 import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
-import FamilyTianpanCard from '../../components/FamilyTianpanCard.vue'
+import FamilyTianpanCard from '@/pages/mind-detail/components/FamilyTianpanCard.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import { getVisibleSections, getEmiReport, getFamilyEnergySandbox, getEnergySandbox, getChildren, getFamilyMemberList, getEnergyOverview, getContactList, getHealthAlerts, getMilestones, getDanReportByChild, getFamilyFortune, getLatestIndicators, getRadarData } from '../../utils/api.js'
 import nav from '../../utils/nav.js'

+ 66 - 31
cfc-frontend/pages/profile/components/ProfileHeader.vue

@@ -10,7 +10,7 @@
       <view class="user-info">
         <view class="nickname">{{ nickname || '未设置昵称' }}</view>
         <view class="role">{{ role === 'parent' ? '家长模式' : (role === 'child' ? '孩子模式' : '成长规划师模式') }}</view>
-        <view class="system-points" v-if="role === 'parent' && children.length > 0">💎 CF值: {{ children[0].systemPoints || 0 }}</view>
+        <view class="system-points" v-if="role === 'parent' && children.length > 0">🏅 系统积分: {{ children[0].systemPoints || 0 }}</view>
       </view>
       <!-- 切换按钮仅在非切换状态下显示 -->
       <button class="btn-switch" v-if="!isSwitchedChild" @click="onSwitchMode">切换</button>
@@ -19,7 +19,7 @@
 </template>
 
 <script>
-import { getChildren, verifyPassword } from '../../../utils/api.js'
+import { getChildren, verifyPassword, switchToFamilyMember } from '@/utils/api.js'
 
 export default {
   name: 'ProfileHeader',
@@ -78,13 +78,18 @@ export default {
         this.doSwitchMode()
       }
     },
-    doSwitchMode() {
+    async doSwitchMode() {
+      // 成员视角(已切换到不可登录成员)下不可再切换其他成员,只能退出切换
+      if (this.$store.state.currentView === 'member') {
+        uni.showToast({ title: '当前为成员视角,请先退出切换', icon: 'none' })
+        return
+      }
       if (this.children.length === 0) {
         this.loadChildren()
         if (this.children.length === 0) {
           uni.showModal({
             title: '提示',
-            content: '您还没有添加孩子,请先在家庭成员中添加',
+            content: '您还没有添加成员,请先在家庭成员中添加',
             success: (res) => {
               if (res.confirm) {
                 uni.navigateTo({ url: '/pages/profile-extra/family-members' })
@@ -94,45 +99,75 @@ export default {
           return
         }
       }
-      if (this.children.length === 1) {
-        this.$store.commit('switchToChild', this.children[0].id)
-        this.role = 'child'
-        this.isSwitchedChild = true
-        uni.showToast({ title: '已切换为孩子模式', icon: 'success' })
+      // 优先显示可切换成员(不可登录成员),过滤掉可通过微信登录的成员
+      var switchable = this.children.filter(function(m) { return m.isSwitchable })
+      if (switchable.length === 0) {
+        uni.showToast({ title: '暂无可切换成员', icon: 'none' })
+        return
+      }
+      if (switchable.length === 1) {
+        var member = switchable[0]
+        uni.showModal({
+          title: '切换确认',
+          content: '切换到"' + member.nickname + '"后,退出时需要输入您的密码。是否继续?',
+          success: async (res) => {
+            if (!res.confirm) return
+            try {
+              var result = await switchToFamilyMember(member.id, member.source || 'family_member')
+              if (result.code === 200 && result.data) {
+                this.$store.commit('switchToMember', {
+                  memberId: result.data.memberId,
+                  effectiveRole: result.data.effectiveRole
+                })
+                this.isSwitchedChild = true
+                this.role = result.data.effectiveRole
+                uni.showToast({ title: '已切换', icon: 'success' })
+              } else {
+                uni.showToast({ title: result.message || '切换失败', icon: 'none' })
+              }
+            } catch (e) {
+              uni.showToast({ title: '切换失败', icon: 'none' })
+            }
+          }
+        })
         return
       }
       uni.showActionSheet({
-        itemList: this.children.map(c => c.nickname),
+        itemList: switchable.map(function(m) { return m.nickname }),
         success: (res) => {
-          const selectedChild = this.children[res.tapIndex]
-          this.$store.commit('switchToChild', selectedChild.id)
-          this.role = 'child'
+          if (res.tapIndex < 0) return
+          var member = switchable[res.tapIndex]
+          this.$store.commit('switchToMember', {
+            memberId: member.id,
+            effectiveRole: member.effectiveRole
+          })
           this.isSwitchedChild = true
-          uni.showToast({ title: `已切换为${selectedChild.nickname}模式`, icon: 'success' })
+          this.role = member.effectiveRole
+          uni.showToast({ title: '已切换', icon: 'success' })
         }
       })
     },
-    onExitSwitch() {
+    async onExitSwitch() {
       if (!this.isSwitchedChild) return
+      var self = this
       uni.showModal({
         title: '退出切换',
-        content: '请输入家长密码以确认退出',
+        content: '请输入原用户密码以确认退出',
         editable: true,
-        success: (res) => {
-          if (res.confirm && res.content) {
-            verifyPassword(res.content).then((result) => {
-              if (result.code === 200) {
-                this.$store.commit('switchBackToParent')
-                this.isSwitchedChild = false
-                this.role = 'parent'
-                uni.showToast({ title: '已退出切换', icon: 'success' })
-                uni.reLaunch({ url: '/pages/index-home/index' })
-              } else {
-                uni.showToast({ title: '密码错误', icon: 'none' })
-              }
-            }).catch(() => {
-              uni.showToast({ title: '验证失败', icon: 'none' })
-            })
+        success: async function(res) {
+          if (!res.confirm || !res.content) return
+          try {
+            var ok = await self.$store.dispatch('switchBackWithPassword', res.content)
+            if (ok) {
+              self.isSwitchedChild = false
+              self.role = 'parent'
+              uni.showToast({ title: '已退出切换', icon: 'success' })
+              uni.reLaunch({ url: '/pages/index-home/index' })
+            } else {
+              uni.showToast({ title: '密码错误', icon: 'none' })
+            }
+          } catch (e) {
+            uni.showToast({ title: '验证失败', icon: 'none' })
           }
         }
       })

+ 1 - 1
cfc-frontend/components/ProfileMenu.vue → cfc-frontend/pages/profile/components/ProfileMenu.vue

@@ -110,7 +110,7 @@
 </template>
 
 <script>
-import { setPassword, verifyPassword, updateMemberVisibility, getFamilyMemberList, vendorStatus, ensureFamily, getReferralCode, extractReferralCode } from '../utils/api.js'
+import { setPassword, verifyPassword, updateMemberVisibility, getFamilyMemberList, vendorStatus, ensureFamily, getReferralCode, extractReferralCode } from '@/utils/api.js'
 
 export default {
   name: 'ProfileMenu',

+ 2 - 2
cfc-frontend/pages/profile/index.vue

@@ -19,8 +19,8 @@
 </template>
 
 <script>
-import ProfileHeader from '../../components/ProfileHeader.vue'
-import ProfileMenu from '../../components/ProfileMenu.vue'
+import ProfileHeader from '@/pages/profile/components/ProfileHeader.vue'
+import ProfileMenu from '@/pages/profile/components/ProfileMenu.vue'
 import { getReferralCode, extractReferralCode } from '../../utils/api.js'
 
 export default {

+ 0 - 0
cfc-frontend/components/SharePoster.vue → cfc-frontend/pages/promotion/components/SharePoster.vue


+ 1 - 1
cfc-frontend/pages/promotion/index.vue

@@ -207,7 +207,7 @@
 
 <script>
 import { getReferralSummary, getCfSummary, getFamilyEarningsSummary, getFamilyEarningsMembers, getFamilyEarningsRecords, distributeToMember, toggleMemberBalance, withdrawFamilyEarnings, getInviteQrCode, getFamilyMemberList, getReferralCode, extractReferralCode } from '../../utils/api.js'
-import SharePoster from '@/components/SharePoster.vue'
+import SharePoster from '@/pages/promotion/components/SharePoster.vue'
 
 export default {
   components: { SharePoster },

+ 1 - 1
cfc-frontend/pages/promotion/invite.vue

@@ -90,7 +90,7 @@
 
 <script>
 import { getReferralCode, bindReferral, getReferralList, getReferralSummary, getInviteQrCode, getInviteMilestones, claimInviteMilestone, extractReferralCode } from '../../utils/api.js'
-import SharePoster from '@/components/SharePoster.vue'
+import SharePoster from '@/pages/promotion/components/SharePoster.vue'
 import { parseDate } from '../../utils/format.js'
 
 export default {

+ 1 - 1
cfc-frontend/pages/shop/checkout/checkout.vue

@@ -459,7 +459,7 @@
 import config from '@/config.js'
 import { getCouponList, getCouponCheckoutList, addressList, getProductRequiredFields, getMyMembership, getGiftMembershipProgress, productDetail, getFamilyPlatformBalance, exchangeCouponByCf, shopDiscountCalculate } from '../../../utils/api.js'
 import { parseDate } from '@/utils/format.js'
-import CountdownTag from '@/components/countdown-tag.vue'
+import CountdownTag from '@/pages/shop/components/countdown-tag.vue'
 
 export default {
   components: {

+ 0 - 0
cfc-frontend/components/countdown-tag.vue → cfc-frontend/pages/shop/components/countdown-tag.vue


+ 0 - 0
cfc-frontend/components/BodyWealthAlert.vue → cfc-frontend/pages/wealth/components/BodyWealthAlert.vue


+ 0 - 0
cfc-frontend/components/WealthEnergyRing.vue → cfc-frontend/pages/wealth/components/WealthEnergyRing.vue


+ 0 - 0
cfc-frontend/components/WealthSubScores.vue → cfc-frontend/pages/wealth/components/WealthSubScores.vue


+ 3 - 3
cfc-frontend/pages/wealth/index.vue

@@ -228,9 +228,9 @@
 
 <script>
 import TabTransition from '../../components/tab-transition.vue'
-import WealthEnergyRing from '../../components/WealthEnergyRing.vue'
-import WealthSubScores from '../../components/WealthSubScores.vue'
-import BodyWealthAlert from '../../components/BodyWealthAlert.vue'
+import WealthEnergyRing from '@/pages/wealth/components/WealthEnergyRing.vue'
+import WealthSubScores from '@/pages/wealth/components/WealthSubScores.vue'
+import BodyWealthAlert from '@/pages/wealth/components/BodyWealthAlert.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'

+ 108 - 19
cfc-frontend/utils/api.js

@@ -46,6 +46,30 @@ function _clearDedup() {
 }
 export { _clearDedup as clearApiDedup }
 
+// 判断当前是否仍处于「小程序启动窗口」(首屏 splash 尚未切走)。
+//
+// 背景:pages/login 是分包(pages.json subPackages,root=pages/login),
+// 而 pages/splash/index、pages/index-home/index 都在主包。分包页面在首次访问前
+// 需要下载并注册;若在 App.onLaunch 阶段就 reLaunch 到未下载的分包,微信会报
+// 「Page "pages/login/login" has not been registered yet」。
+// 其它 28 处跳登录都是「已渲染页面上的用户点击」,框架已可下载分包,故不受影响。
+function _inLaunchWindow() {
+  try {
+    var pages = typeof getCurrentPages === 'function' ? getCurrentPages() : null
+    // 首屏尚未注册:仍在启动窗口
+    if (!pages || pages.length === 0) return true
+    var cur = pages[pages.length - 1]
+    var route = ''
+    if (cur) {
+      route = cur.route || (cur.$page && cur.$page.route) || ''
+    }
+    // 还停在 splash:启动窗口内(约 2 秒),不与 splash 自己的 reLaunch 抢路由
+    return route === 'pages/splash/index'
+  } catch (e) {
+    return false
+  }
+}
+
 function _clearAuthAndRedirect() {
   if (_redirectingToLogin) return
   _redirectingToLogin = true
@@ -58,6 +82,13 @@ function _clearAuthAndRedirect() {
   uni.removeStorageSync('currentChildId')
   uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' })
   setTimeout(function() {
+    // 启动窗口内只清登录态、不跳转:让 splash 正常落地首页(主包),
+    // 首页按未登录态渲染登录引导,与「首次进入的新用户」路径一致。
+    if (_inLaunchWindow()) {
+      console.warn('[Auth] 启动窗口内不跳转登录页,交由 splash 落地首页(避免未注册分包页报错)')
+      _redirectingToLogin = false
+      return
+    }
     uni.reLaunch({ url: '/pages/login/login' })
     _redirectingToLogin = false
   }, 1500)
@@ -94,7 +125,7 @@ function _applyLoginData(data) {
   } catch (e) { /* 非页面环境无 Vuex,静默忽略 */ }
 }
 
-// 401 处理:优先用缓存 openid 静默续期并重放原请求;续期失败或无 openid 时回退跳登录页
+// 401 处理:优先静默续期(wx.login code2Session)并重放原请求;续期失败则回退跳登录页
 function _handle401(url, method, data, options, resolve, reject, resData) {
   // 续期接口自身 401 / 重试仍 401 时,避免递归续期死循环
   if (_reloginLock) {
@@ -102,17 +133,11 @@ function _handle401(url, method, data, options, resolve, reject, resData) {
     reject(resData)
     return
   }
-  var openid = uni.getStorageSync('openid')
-  if (!openid) {
-    _clearAuthAndRedirect()
-    reject(resData)
-    return
-  }
   _reloginLock = true
-  autoLogin(openid).then(function(res) {
-    if (res && res.data && res.data.token) {
+  silentRelogin().then(function(loginData) {
+    if (loginData && loginData.token) {
       console.warn('[API] 401 静默续期成功,重放原请求 ' + url)
-      _applyLoginData(res.data)
+      _applyLoginData(loginData)
       // 重放原请求;重试期间保持锁,防止重试仍 401 时再次触发续期
       // 401 重放请求跳过去重(原请求已 reject,缓存中的 promise 已过期)
       var _retryOpts = Object.assign({}, options, { _skipDedup: true })
@@ -135,13 +160,65 @@ function _handle401(url, method, data, options, resolve, reject, resData) {
   })
 }
 
+// 静默续期:uni.login() 取微信签发的一次性 code,服务端 code2Session 换 openid 并签发新 token。
+//
+// 为什么不能拿本地 openid 直接换 token:openid 是明文标识符而非密钥,它会随登录响应返回、
+// 明文存于本地 storage、并可能经日志/截图泄露;凭它签发 token 等同于任何拿到 openid 的人
+// 都能完全冒充该账号(旧的 /api/auth/auto-login 就是这么做的,已废弃返回 410)。
+// code 则由微信签发、单次使用、5 分钟过期,且能证明调用方持有该微信设备,不可伪造,
+// 同样零交互静默完成 —— 这才是「用小程序缓存完成自动登录」的安全实现方式。
+//
+// 本地 openid 在这里仅作为「本设备此前登录过」的判断依据(决定是否尝试续期),绝不作为凭证上送。
+//
+// 返回:成功返回 LoginResultDTO 数据对象;本设备从未登录过 / 续期失败返回 null(由调用方决定兜底)
+export function silentRelogin() {
+  return new Promise(function(resolve) {
+    // 无 openid 缓存 = 本设备从未成功登录过,属新用户,不做静默续期(直接引导手动登录)
+    var cachedOpenid = uni.getStorageSync('openid')
+    if (!cachedOpenid) {
+      resolve(null)
+      return
+    }
+    uni.login({
+      provider: 'weixin',
+      success: function(loginRes) {
+        if (!loginRes || !loginRes.code) {
+          console.warn('[Auth] uni.login 未返回 code,静默续期跳过')
+          resolve(null)
+          return
+        }
+        // _noRelogin:本次请求就是续期请求本身,不能再触发 _handle401 造成递归续期
+        silentLogin(loginRes.code, { _noRelogin: true, _skipDedup: true }).then(function(res) {
+          if (res && res.data && res.data.token) {
+            resolve(res.data)
+          } else {
+            console.warn('[Auth] 静默续期未返回 token')
+            resolve(null)
+          }
+        }).catch(function(e) {
+          console.warn('[Auth] 静默续期请求失败', e && e.code)
+          resolve(null)
+        })
+      },
+      fail: function(e) {
+        console.warn('[Auth] uni.login 调用失败,静默续期跳过', e)
+        resolve(null)
+      }
+    })
+  })
+}
+
 const request = (url, method = 'POST', data = {}, options = {}) => {
+  // 401 重放时 request(url, method, data, options) 会用同一份 options 再次拼 query,
+  // 所以这里必须保存未拼接 query 的原始 path 传给 _handle401,否则重放请求的 URL 会
+  // 出现 ?a=1&a=1(query 重复)。fullUrl 仍用拼接后的 url,日志和去重粒度不变。
+  var basePath = url
   // Append query params from options to URL if provided (e.g., { guideId } for GET requests)
-  // `timeout`/`_skipDedup` 是请求配置项,不是 query 参数,需排除
+  // `timeout`/`_skipDedup`/`_noRelogin` 是请求配置项,不是 query 参数,需排除
   if (options && Object.keys(options).length > 0) {
     const separator = url.includes('?') ? '&' : '?'
     const query = Object.entries(options)
-      .filter(([k, v]) => k !== 'timeout' && k !== '_skipDedup' && v !== undefined && v !== null && v !== '')
+      .filter(([k, v]) => k !== 'timeout' && k !== '_skipDedup' && k !== '_noRelogin' && v !== undefined && v !== null && v !== '')
       .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
       .join('&')
     if (query) url = url + separator + query
@@ -187,8 +264,16 @@ const request = (url, method = 'POST', data = {}, options = {}) => {
       },
       success: (res) => {
         if (res.statusCode === 401 || res.data.code === 401) {
+          // _noRelogin:本次请求就是静默续期请求本身(或上层已明确不走自动续期),
+          // 直接 reject。否则会再次进入 _handle401 → 递归续期,甚至误清登录态把用户踢出。
+          if (options._noRelogin) {
+            console.warn('[API] 401 ' + method + ' ' + fullUrl + '(已禁用自动续期,直接失败)')
+            reject(res.data)
+            return
+          }
           console.warn('[API] 401 ' + method + ' ' + fullUrl)
-          _handle401(url, method, data, options, resolve, reject, res.data)
+          // 传 basePath(未拼 query)而非 url:_handle401 重放时会用 options 再拼一次
+          _handle401(basePath, method, data, options, resolve, reject, res.data)
           return
         }
         // 5001: 用户无家庭 — resolve 带标记,不 reject;由组件层各自渲染 FamilyEmptyState
@@ -322,14 +407,18 @@ export const getNotices = () => {
   return request('/api/admin/notices/all', 'POST', {})
 }
 
-// 微信静默登录
-export const silentLogin = (code) => {
-  return request('/api/auth/silent-login', 'POST', { code })
+// 微信静默登录:code 由 uni.login() 签发,服务端 code2Session 换 openid 并签发新 token。
+// 这是自动续期的唯一安全通道(可证明设备持有,openid 本身不可作为凭证)。
+export const silentLogin = (code, options) => {
+  return request('/api/auth/silent-login', 'POST', { code }, options)
 }
 
-// 通过openid自动登录
+// 【已废弃 · 不要使用】通过裸 openid 换取 token。
+// 后端 /api/auth/auto-login 已改为 410 Gone:openid 是明文标识符而非密钥,
+// 凭它签发 token 等同于任何拿到 openid 的人都能完全冒充该账号。
+// 保留导出仅为便于排查历史调用,请改用 silentRelogin()(内部走 silentLogin)。
 export const autoLogin = (openid) => {
-  return request('/api/auth/auto-login', 'POST', { openid })
+  return request('/api/auth/auto-login', 'POST', { openid }, { _noRelogin: true })
 }
 
 export const setPassword = (password) => {
@@ -3175,4 +3264,4 @@ export function getNodeTemplateList(params = {}) {
   return request('/api/admin/task-templates', 'POST', params)
 }
 
-export const getHomeBanners = () => request('/api/home-banner/list', 'POST', {})
+export const getHomeBanners = () => request('/api/home-banner/list', 'POST', {})

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-87119319d22c7cf18d7a83d933cd822e4aa62e9c
+c7b8fd74b2afb6efd56d28b43228a3a597e72301

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

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1482",
+  "version": "1.0.1485",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1482",
+      "version": "1.0.1485",
       "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.1483",
+  "version": "1.0.1486",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

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

@@ -4,6 +4,82 @@
 
 ---
 
+## v1.0.1486 (2026-10-01)
+
+### 重构
+- 组件下沉到页面级目录
+
+### 其他
+- components/ 子目录(action-detail / ai / discover / family / health /
+- home-pages / member-detail / mind-detail / profile / promotion / shop /
+- wealth),并同步更新引用方与 manifest.json。
+- 目的:让组件跟随页面一起进入对应分包,避免被提升进主包,缩减主包体积。
+- 组件解析已校验:所有模板标签均可通过显式 import 或同名 .vue 解析,
+- 无悬空引用。audit-duplicate-api-calls.js 通过(0 error / 87 warning,
+- 与重构前基线一致)。
+- 
+
+
+## v1.0.1485 (2026-10-01)
+
+### Bug 修复
+- 废弃裸 openid 换 token 接口,静默续期改走 code2Session
+
+### 其他
+- 证明。而 openid 并非密钥:它会随登录响应返回、明文存于小程序本地 storage、并可能
+- 经日志或截图泄露。任何拿到他人 openid 的人都能直接换取该账号的合法 token,等同
+- 于完全冒充该账号。
+- 按 AGENTS.md「废弃接口标记 410 Gone 并注释说明,不直接删除」,保留路由与注释并
+- 返回 410,待线上旧版本小程序全部升级后再移除路由。UserService#autoLoginByOpenid
+- 标记 @Deprecated。
+- 替代路径:前端 uni.login() 取微信签发的一次性 code,服务端 code2Session 换
+- openid。code 单次使用、5 分钟过期,且能证明调用方持有该微信设备,不可伪造,同样
+- 零交互静默完成。本地 openid 仅用于判断「本设备曾登录过」,绝不作为凭证上送。
+- 其他修复:
+- - App.vue 冷启动即恢复登录态:原先在 splash 分支提前 return,导致普通冷启动(非
+-   分享/小程序码进入)完全拿不到任何续期机会
+- - 移除 develop 环境下对 verifyStoredToken/tryAutoLogin 的屏蔽,否则开发版无法验证
+-   续期逻辑
+- - 401 重放改传未拼接 query 的原始路径,避免 URL 出现 ?a=1&a=1 重复参数
+- - 新增 _noRelogin:续期请求自身返回 401 时直接失败,防止递归续期/误清登录态
+- - 启动窗口内(pages/login 为分包,首次访问前尚未下载注册)只清登录态不跳转,避免
+-   「Page has not been registered yet」
+- - JWT 有效期 1 天 -> 30 天,配合静默续期用户几乎无需手动重新登录
+- 验证:前端 jest 176 项全部通过;后端 mvn clean compile(release 8)BUILD SUCCESS
+- 
+
+
+## v1.0.1484 (2026-09-30)
+
+### Bug 修复
+- schema.sql guide_popup_step desc 保留字加反引号 [skip ci]
+
+
+## v1.0.1484 (2026-09-30)
+
+### Bug 修复
+- 迁移334 desc 保留字报错 + 启动期定时任务竞态保护 [skip ci]
+
+
+## v1.0.1484 (2026-09-30)
+
+### 其他
+-   从 Git 取消跟踪但保留本地文件,并新增 .gitignore 规则防止被再次添加
+- - fix-hbuilderx-scoped.js: 硬编码 macOS 路径的死代码,Windows 环境下从不可用,无任何代码引用
+- - 本 commit 不含任何页面源码改动:排查 14 个页面退化为 13 字节 Component({}) 的问题时,
+-   已确认 pages.json 注册与全部 264 个页面源码均正常,根因是 unpackage/ 下残留的
+-   webpack 增量缓存(optimization.noEmitOnErrors=false 导致构建错误被静默吞掉),
+-   清空 unpackage 后重新编译即全部恢复
+- Co-Authored-By: Sisyphus (Anthropic) <noreply@anthropic.com>
+- 
+
+
+## v1.0.1484 (2026-09-30)
+
+### 新功能
+- 首页广告栏配置功能 - 新增 home_banners 表及配套后端接口、管理端页面、小程序组件
+
+
 ## v1.0.1483 (2026-09-29)
 
 ### Bug 修复

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

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1483
+> 当前版本: v1.0.1486
 
 ## 历史版本
 
@@ -8,6 +8,82 @@
 
 ---
 
+## v1.0.1486 (2026-10-01)
+
+### 重构
+- 组件下沉到页面级目录
+
+### 其他
+- components/ 子目录(action-detail / ai / discover / family / health /
+- home-pages / member-detail / mind-detail / profile / promotion / shop /
+- wealth),并同步更新引用方与 manifest.json。
+- 目的:让组件跟随页面一起进入对应分包,避免被提升进主包,缩减主包体积。
+- 组件解析已校验:所有模板标签均可通过显式 import 或同名 .vue 解析,
+- 无悬空引用。audit-duplicate-api-calls.js 通过(0 error / 87 warning,
+- 与重构前基线一致)。
+- 
+
+
+## v1.0.1485 (2026-10-01)
+
+### Bug 修复
+- 废弃裸 openid 换 token 接口,静默续期改走 code2Session
+
+### 其他
+- 证明。而 openid 并非密钥:它会随登录响应返回、明文存于小程序本地 storage、并可能
+- 经日志或截图泄露。任何拿到他人 openid 的人都能直接换取该账号的合法 token,等同
+- 于完全冒充该账号。
+- 按 AGENTS.md「废弃接口标记 410 Gone 并注释说明,不直接删除」,保留路由与注释并
+- 返回 410,待线上旧版本小程序全部升级后再移除路由。UserService#autoLoginByOpenid
+- 标记 @Deprecated。
+- 替代路径:前端 uni.login() 取微信签发的一次性 code,服务端 code2Session 换
+- openid。code 单次使用、5 分钟过期,且能证明调用方持有该微信设备,不可伪造,同样
+- 零交互静默完成。本地 openid 仅用于判断「本设备曾登录过」,绝不作为凭证上送。
+- 其他修复:
+- - App.vue 冷启动即恢复登录态:原先在 splash 分支提前 return,导致普通冷启动(非
+-   分享/小程序码进入)完全拿不到任何续期机会
+- - 移除 develop 环境下对 verifyStoredToken/tryAutoLogin 的屏蔽,否则开发版无法验证
+-   续期逻辑
+- - 401 重放改传未拼接 query 的原始路径,避免 URL 出现 ?a=1&a=1 重复参数
+- - 新增 _noRelogin:续期请求自身返回 401 时直接失败,防止递归续期/误清登录态
+- - 启动窗口内(pages/login 为分包,首次访问前尚未下载注册)只清登录态不跳转,避免
+-   「Page has not been registered yet」
+- - JWT 有效期 1 天 -> 30 天,配合静默续期用户几乎无需手动重新登录
+- 验证:前端 jest 176 项全部通过;后端 mvn clean compile(release 8)BUILD SUCCESS
+- 
+
+
+## v1.0.1484 (2026-09-30)
+
+### Bug 修复
+- schema.sql guide_popup_step desc 保留字加反引号 [skip ci]
+
+
+## v1.0.1484 (2026-09-30)
+
+### Bug 修复
+- 迁移334 desc 保留字报错 + 启动期定时任务竞态保护 [skip ci]
+
+
+## v1.0.1484 (2026-09-30)
+
+### 其他
+-   从 Git 取消跟踪但保留本地文件,并新增 .gitignore 规则防止被再次添加
+- - fix-hbuilderx-scoped.js: 硬编码 macOS 路径的死代码,Windows 环境下从不可用,无任何代码引用
+- - 本 commit 不含任何页面源码改动:排查 14 个页面退化为 13 字节 Component({}) 的问题时,
+-   已确认 pages.json 注册与全部 264 个页面源码均正常,根因是 unpackage/ 下残留的
+-   webpack 增量缓存(optimization.noEmitOnErrors=false 导致构建错误被静默吞掉),
+-   清空 unpackage 后重新编译即全部恢复
+- Co-Authored-By: Sisyphus (Anthropic) <noreply@anthropic.com>
+- 
+
+
+## v1.0.1484 (2026-09-30)
+
+### 新功能
+- 首页广告栏配置功能 - 新增 home_banners 表及配套后端接口、管理端页面、小程序组件
+
+
 ## v1.0.1483 (2026-09-29)
 
 ### Bug 修复

BIN
docs/参考资料/益生元商品/瓜尔主推产品图标及介绍9.1.xlsx


BIN
工作文稿/抗衰促进会/2026《浠艾福青少年素质教育与家庭健康成长陪伴工程》立项申报书.docx


BIN
工作文稿/抗衰促进会/2026《老年心理健康筛查与陪伴工程》立项申报书.docx


BIN
工作文稿/抗衰促进会/2026《青少年主动心身健康与家庭健康成长陪伴工程》立项申报书.docx


+ 74 - 0
运营文案/健康理念/血管优先抗衰_视频逐字稿_主动健康版.md

@@ -0,0 +1,74 @@
+# 血管,比脸更早老十岁|抗衰全景图谱视频逐字稿(主动健康版)
+
+## 开场钩子
+人体最先衰老的器官是哪儿?
+不是皮肤,也不是大脑。
+这个答案,像是给所有拼命抗衰的人,结结实实扇了一耳光。
+
+今年 6 月 11 日,国际权威期刊 Cell 杂志,用一期封面文章宣布:人类第一张"衰老全景图谱",已经绘制完成。
+研究团队对 14 种组织、数万个细胞,做了单细胞测序,再加上空间组学分析——就是想揪出一件事:衰老,到底最早从哪儿开始。
+
+## 过去十年的盲区
+说句实话,过去十年,我们对付衰老的打法,本质上像在黑屋子里盲开枪。
+吃一种保健品、试一种疗法,但压根不知道——身体到底哪儿先老、怎么老、又是什么时候开始加速的。
+而这一张图谱,第一次把答案,画清楚了。
+
+## 发现一:血管,是优先起点
+研究发现,衰老根本不是均匀发生的。它有"优先起点"。
+其中,血管系统,被锁定为最早期的变故区域之一。
+
+其实这不算秘密。我一直在讲"微循环优先"——很多人当初听着,像无稽之谈。
+现在图谱告诉我们:一旦血管功能率先受损,氧气输送、营养输送、免疫细胞的调度,全都会被打乱。
+于是你看到的现象是——大脑供氧往下掉、心脏供血跟不上、皮肤状态也垮。
+这也解释了,为什么高血压、动脉硬化的人,往往一身上下,都更明显地加速衰老。
+
+## 发现二:不是线性,是两个拐点
+第二个发现,更扎心。
+衰老,不是匀速进行的。
+在蛋白质和代谢层面,研究观察到两个明显的加速拐点——一个在 50 岁上下,一个在 63 岁上下。
+
+50 岁那道坎,是一道变化窗口:大量衰老相关细胞,开始在血管和免疫系统里堆积,慢性炎症水平,往上抬。
+到了 63 岁左右,是第二个关键窗口:脑细胞的累积,冲到一个更高的台阶,多个器官功能,开始出现系统性的往下走。
+
+研究同时指出,在实验层面,清除衰老细胞的策略,和健康寿命的延缓之间,存在一定关联。
+换句话说——精准抗衰,正从概念,一步步走向可量化的科研方向。
+
+## 主动健康视角:衰老是"五维"的事
+但这里,我想多说一句。
+我们一直倡导"主动健康"——衰老,不只是身体维度的事。
+身体、心理、认知、社交、成就,这五个维度,各有各的老化轨迹。
+血管一老,先拖累的是认知和情绪;慢性炎症一高,心理状态和睡眠,也跟着遭殃。
+所以真正的抗衰,是五个维度一起管,而不是只盯着脸。
+
+## 普通人能做什么
+那咱们普通人,能干什么?
+
+第一,守住血管功能。
+规律的有氧运动,是目前证据最一致支持的方向之一。
+
+第二,压住慢性炎症。
+以地中海饮食为代表的吃法——大量生鲜蔬菜、优质脂肪,是实打实有数据支撑的。
+
+第三,盯紧年龄窗口。
+50 岁和 63 岁前后,重点查血管弹性、微循环,还有炎症相关指标。
+说到微循环,我又得啰嗦一句:它,才是很多慢性问题的根。
+
+第四,把健康当"主动管理",别等出问题再补救。
+定期看指标,比等身体报警,强一百倍。
+
+## 收尾金句 + 互动
+有研究者,一句话总结得狠:
+你的血管,可能比你的脸,早老十岁。
+50 岁和 63 岁,不是终点——是提醒你,该认真看看身体变化的信号了。
+
+评论区聊聊:
+有多少人,看着比身份证年龄,年轻十岁以上的?举个手,让我看看。
+
+---
+
+### 改写说明(发布前请过目)
+1. **品牌名处理**:按你定下的"文档禁出现浠艾福字样"铁律,全篇用"主动健康理念 / 微循环学说 / 五维主动健康"承载,未出现"浠艾福"三字。若本次想在视频里口播品牌名,说一声,我另出一版带名的。
+2. **原文纠错**:①"赛杂志"按发音修正为"Cell 杂志"(实际期刊/日期发前请核对);②"空间主学分析"→"空间组学分析";③"微液循环"→"微循环";④"三岁左右"明显口误→"63 岁左右",与后文第二个拐点一致。
+3. **理念融合**:把原本纯身体的叙事,接入"五维主动健康"(身体/心理/认知/社交/成就),点明血管老化同时拖累认知与情绪,呼应"主动健康、主动管理"而非被动治疗。
+4. **合规边界**:未出现任何食品/水治疗宣称、未碰氢气水治疗口径;纯科普+生活方式建议,符合对外发布边界。
+5. **结构**:保留原文"钩子—盲区—两大发现—实操—金句互动"节奏,口语化、可直录。

+ 74 - 0
运营文案/健康理念/血管优先抗衰_视频逐字稿_浠艾福版.md

@@ -0,0 +1,74 @@
+# 血管,比脸更早老十岁|抗衰全景图谱视频逐字稿(浠艾福主动健康版)
+
+## 开场钩子
+人体最先衰老的器官是哪儿?
+不是皮肤,也不是大脑。
+这个答案,像是给所有拼命抗衰的人,结结实实扇了一耳光。
+
+今年 6 月 11 日,国际权威期刊 Cell 杂志,用一期封面文章宣布:人类第一张"衰老全景图谱",已经绘制完成。
+研究团队对 14 种组织、数万个细胞,做了单细胞测序,再加上空间组学分析——就是想揪出一件事:衰老,到底最早从哪儿开始。
+
+## 过去十年的盲区
+说句实话,过去十年,我们对付衰老的打法,本质上像在黑屋子里盲开枪。
+吃一种保健品、试一种疗法,但压根不知道——身体到底哪儿先老、怎么老、又是什么时候开始加速的。
+而这一张图谱,第一次把答案,画清楚了。
+
+## 发现一:血管,是优先起点
+研究发现,衰老根本不是均匀发生的。它有"优先起点"。
+其中,血管系统,被锁定为最早期的变故区域之一。
+
+其实这不算秘密。我们浠艾福主动健康研究院,一直把"微循环优先"放在健康管理的第一位——很多人当初听着,像无稽之谈。
+现在图谱告诉我们:一旦血管功能率先受损,氧气输送、营养输送、免疫细胞的调度,全都会被打乱。
+于是你看到的现象是——大脑供氧往下掉、心脏供血跟不上、皮肤状态也垮。
+这也解释了,为什么高血压、动脉硬化的人,往往一身上下,都更明显地加速衰老。
+
+## 发现二:不是线性,是两个拐点
+第二个发现,更扎心。
+衰老,不是匀速进行的。
+在蛋白质和代谢层面,研究观察到两个明显的加速拐点——一个在 50 岁上下,一个在 63 岁上下。
+
+50 岁那道坎,是一道变化窗口:大量衰老相关细胞,开始在血管和免疫系统里堆积,慢性炎症水平,往上抬。
+到了 63 岁左右,是第二个关键窗口:脑细胞的累积,冲到一个更高的台阶,多个器官功能,开始出现系统性的往下走。
+
+研究同时指出,在实验层面,清除衰老细胞的策略,和健康寿命的延缓之间,存在一定关联。
+换句话说——精准抗衰,正从概念,一步步走向可量化的科研方向。
+
+## 浠艾福主动健康视角:衰老是"五维"的事
+但这里,我想多说一句。
+浠艾福一直倡导"主动健康"——衰老,不只是身体维度的事。
+身体、心理、认知、社交、成就,这五个维度,各有各的老化轨迹。
+血管一老,先拖累的是认知和情绪;慢性炎症一高,心理状态和睡眠,也跟着遭殃。
+所以真正的抗衰,是五个维度一起管,而不是只盯着脸。
+
+## 普通人能做什么
+那咱们普通人,能干什么?
+
+第一,守住血管功能。
+规律的有氧运动,是目前证据最一致支持的方向之一。
+
+第二,压住慢性炎症。
+以地中海饮食为代表的吃法——大量生鲜蔬菜、优质脂肪,是实打实有数据支撑的。
+
+第三,盯紧年龄窗口。
+50 岁和 63 岁前后,重点查血管弹性、微循环,还有炎症相关指标。
+说到微循环,我又得啰嗦一句:它,才是很多慢性问题的根——这也是浠艾福这么多年一直在讲的事。
+
+第四,把健康当"主动管理",别等出问题再补救。
+定期看指标,比等身体报警,强一百倍。
+
+## 收尾金句 + 互动
+有研究者,一句话总结得狠:
+你的血管,可能比你的脸,早老十岁。
+50 岁和 63 岁,不是终点——是提醒你,该认真看看身体变化的信号了。
+
+我是浠艾福主动健康研究院的[讲述人],关注我们,一起把"主动健康"真正落到日常。
+评论区聊聊:有多少人,看着比身份证年龄,年轻十岁以上的?举个手,让我看看。
+
+---
+
+### 改写说明(发布前请过目)
+1. **品牌处理**:按你确认——本视频可带品牌,已自然融入"浠艾福主动健康研究院 / 浠艾福微循环学说 / 浠艾福主动健康理念",未使用"中国抗衰老促进会精神与心理分会"及任何副主任头衔落款(遵循"本人不署名"口径)。
+2. **原文纠错**:①"赛杂志"→"Cell 杂志"(期刊/日期发前请核对);②"空间主学分析"→"空间组学分析";③"微液循环"→"微循环";④"三岁左右"→"63 岁左右"。
+3. **理念融合**:接入"五维主动健康"(身体/心理/认知/社交/成就),点明血管老化同时拖累认知与情绪。
+4. **合规边界**:未出现食品/水治疗宣称、未碰氢气水治疗口径;纯科普+生活方式建议。
+5. **待填**:[讲述人] 处填入实际出镜人;若不想用"关注我们"这种轻引流口播,可改为纯理念收尾。