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

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

Sisyphus Agent 2 месяцев назад
Родитель
Сommit
89bc2c391c
31 измененных файлов с 3216 добавлено и 191 удалено
  1. 1 1
      cfc-frontend/App.vue
  2. 129 0
      cfc-frontend/components/AIFloatingAvatar.vue
  3. 1 1
      cfc-frontend/components/DimensionActivities.vue
  4. 1 1
      cfc-frontend/components/tab-transition.vue
  5. 712 0
      cfc-frontend/docs/superpowers/plans/2026-07-02-activity-home-page-implementation-plan.md
  6. 241 0
      cfc-frontend/docs/superpowers/specs/2026-07-02-activity-home-page-design.md
  7. 13 0
      cfc-frontend/pages.json
  8. 1 1
      cfc-frontend/pages/action-detail/member-action-detail.vue
  9. 14 28
      cfc-frontend/pages/action/index.vue
  10. 1 1
      cfc-frontend/pages/action/member-action-detail.vue
  11. 866 0
      cfc-frontend/pages/activity/index.vue
  12. 1 0
      cfc-frontend/pages/ai/chat.vue
  13. 1 1
      cfc-frontend/pages/body-detail/member-body-detail.vue
  14. 13 27
      cfc-frontend/pages/body/index.vue
  15. 1 1
      cfc-frontend/pages/body/member-body-detail.vue
  16. 1 1
      cfc-frontend/pages/index/child-index.vue
  17. 264 29
      cfc-frontend/pages/index/index.vue
  18. 1 1
      cfc-frontend/pages/index/parent-index.vue
  19. 1 1
      cfc-frontend/pages/mind-detail/member-mind-detail.vue
  20. 12 30
      cfc-frontend/pages/mind/index.vue
  21. 1 1
      cfc-frontend/pages/mind/member-mind-detail.vue
  22. 1 1
      cfc-frontend/pages/profile/profile.vue
  23. 5 59
      cfc-frontend/pages/splash/splash.vue
  24. 3 1
      cfc-frontend/pages/wisdom-detail/member-wisdom-detail.vue
  25. 2 2
      cfc-frontend/pages/wisdom/index.vue
  26. 3 1
      cfc-frontend/pages/wisdom/member-wisdom-detail.vue
  27. 3 1
      cfc-frontend/pages/wisdom/wisdom_temp/index.vue
  28. 1 1
      cfc-frontend/pages/wisdom/wisdom_temp/member-wisdom-detail.vue
  29. 564 0
      docs/superpowers/plans/2026-07-02-login-left-panel-redesign.md
  30. 184 0
      docs/superpowers/specs/2026-07-02-login-left-panel-redesign.md
  31. 174 0
      docs/superpowers/specs/2026-07-03-article-center-design.md

+ 1 - 1
cfc-frontend/App.vue

@@ -43,7 +43,7 @@ export default {
             this.saveLoginInfo(res.data)
             console.log('openid自动登录成功')
             // 刷新页面到登录态
-            uni.reLaunch({ url: '/pages/index/index' })
+            uni.switchTab({ url: '/pages/index/index' })
             return
           }
         }

+ 129 - 0
cfc-frontend/components/AIFloatingAvatar.vue

@@ -0,0 +1,129 @@
+<template>
+  <view class="ai-float-wrapper" @click="goToChat">
+    <view class="ai-float-avatar" :class="mascotCode">
+      <text class="ai-float-icon">{{ mascotIcon }}</text>
+    </view>
+    <view class="ai-float-pulse" v-if="showPulse"></view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'AIFloatingAvatar',
+  data() {
+    return {
+      mascotCode: 'xibao',
+      mascotIcon: '🌟',
+      showPulse: true
+    }
+  },
+  mounted() {
+    this.loadMascot()
+  },
+  onShow() {
+    this.loadMascot()
+  },
+  methods: {
+    loadMascot() {
+      var userInfo = uni.getStorageSync('userInfo')
+      if (userInfo && userInfo.mascot) {
+        if (userInfo.mascot === 'xibao') {
+          this.mascotCode = 'xibao'
+          this.mascotIcon = '🌟'
+        } else if (userInfo.mascot === 'fubao') {
+          this.mascotCode = 'fubao'
+          this.mascotIcon = '💪'
+        }
+      } else {
+        // 默认浠宝
+        this.mascotCode = 'xibao'
+        this.mascotIcon = '🌟'
+      }
+    },
+    goToChat() {
+      // 获取当前页面路径作为来源标识
+      var pages = getCurrentPages()
+      var currentPage = pages[pages.length - 1]
+      var fromPage = ''
+      if (currentPage) {
+        fromPage = currentPage.route || ''
+      }
+      var url = '/pages/ai/chat'
+      if (fromPage) {
+        url += '?fromPage=' + encodeURIComponent(fromPage)
+      }
+      uni.navigateTo({ url: url })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.ai-float-wrapper {
+  position: fixed;
+  right: 32rpx;
+  bottom: calc(160rpx + constant(safe-area-inset-bottom));
+  bottom: calc(160rpx + env(safe-area-inset-bottom));
+  z-index: 999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 112rpx;
+  height: 112rpx;
+}
+
+.ai-float-avatar {
+  width: 96rpx;
+  height: 96rpx;
+  border-radius: 50%;
+  background: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15), 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+  animation: floatBounce 3s ease-in-out infinite;
+  position: relative;
+  z-index: 2;
+  transition: transform 0.2s;
+}
+
+.ai-float-avatar:active {
+  transform: scale(0.92);
+}
+
+.ai-float-avatar.xibao {
+  background: linear-gradient(135deg, #FFF7ED 0%, #FFE4CC 100%);
+  border: 3rpx solid #FF8C42;
+}
+
+.ai-float-avatar.fubao {
+  background: linear-gradient(135deg, #FFFBEB 0%, #FEF3C7 100%);
+  border: 3rpx solid #F59E0B;
+}
+
+.ai-float-icon {
+  font-size: 48rpx;
+  line-height: 1;
+}
+
+/* 脉冲动画 */
+.ai-float-pulse {
+  position: absolute;
+  width: 96rpx;
+  height: 96rpx;
+  border-radius: 50%;
+  background: rgba(249, 115, 22, 0.25);
+  animation: pulseRing 2s ease-out infinite;
+  z-index: 1;
+}
+
+@keyframes floatBounce {
+  0%, 100% { transform: translateY(0); }
+  50% { transform: translateY(-8rpx); }
+}
+
+@keyframes pulseRing {
+  0% { transform: scale(1); opacity: 0.6; }
+  100% { transform: scale(1.6); opacity: 0; }
+}
+</style>

+ 1 - 1
cfc-frontend/components/DimensionActivities.vue

@@ -2,7 +2,7 @@
   <view class="section">
     <view class="section-header">
       <text class="section-title">🔥 推荐活动</text>
-      <text class="section-more" v-if="displayActivities && displayActivities.length > 0" @click="$emit('moreActivities')">更多 ›</text>
+      <text class="section-more" @click="$emit('moreActivities')">更多 ›</text>
     </view>
     <view v-if="!displayActivities || displayActivities.length === 0" class="empty-state">
       <text class="empty-tip">{{ isLoggedIn ? '暂无相关活动' : '登录后查看更多活动' }}</text>

+ 1 - 1
cfc-frontend/components/tab-transition.vue

@@ -148,4 +148,4 @@ export default {
     width: 100%;
   }
 }
-</style>
+</style>

+ 712 - 0
cfc-frontend/docs/superpowers/plans/2026-07-02-activity-home-page-implementation-plan.md

@@ -0,0 +1,712 @@
+# 活动主页实现计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 创建独立活动主页 `/pages/activity/index`,优先展示用户报名的活动,支持发现活动浏览和历史记录查看。
+
+**Architecture:** 活动主页为独立 subPackage(`pages/activity`),三 Tab 结构(我的活动/发现活动/历史记录),数据通过现有 API 获取,无需后端改动。16 个页面的"更多"按钮跳转目标需更新。
+
+**Tech Stack:** Vue 2 Options API / uni-app / pages.json / 现有 API (`getMyActivityRegistrations`, `getActivityList`, `activityCheckin`, `registerActivity`, `cancelActivityRegistration`)
+
+---
+
+## 文件结构
+
+```
+cfc-frontend/
+├── pages/activity/                              # 新建独立 subPackage
+│   ├── index.vue                                 # 活动主页(核心文件)
+│   └── activity-detail/activity-detail.vue      # 活动详情(可复用现有 discover-detail 版本)
+├── pages.json                                    # 注册 pages/activity subPackage
+├── pages/index/parent-index.vue                  # 更新 goMoreActivities
+├── pages/index/child-index.vue                   # 更新 goMoreActivities
+├── pages/body/index.vue                          # 更新 goMoreActivities
+├── pages/body/member-body-detail.vue             # 更新 goMoreActivities
+├── pages/body-detail/member-body-detail.vue      # 更新 goMoreActivities
+├── pages/mind/index.vue                          # 更新 goMoreActivities
+├── pages/mind/member-mind-detail.vue             # 更新 goMoreActivities
+├── pages/mind-detail/member-mind-detail.vue      # 更新 goMoreActivities
+├── pages/wisdom/index.vue                        # 更新 goMoreActivities
+├── pages/wisdom/member-wisdom-detail.vue         # 更新 goMoreActivities
+├── pages/wisdom/wisdom_temp/index.vue            # 更新 goMoreActivities
+├── pages/wisdom/wisdom_temp/member-wisdom-detail.vue  # 更新 goMoreActivities
+├── pages/wisdom-detail/member-wisdom-detail.vue  # 更新 goMoreActivities
+├── pages/action/index.vue                        # 更新 goMoreActivities
+├── pages/action/member-action-detail.vue         # 更新 goMoreActivities
+├── pages/action-detail/member-action-detail.vue  # 更新 goMoreActivities
+```
+
+---
+
+## Task 1: 注册 pages/activity SubPackage
+
+**Files:**
+- Modify: `cfc-frontend/pages.json`
+
+- [ ] **Step 1: 在 pages.json 中添加 activity subPackage**
+
+在 `subPackages` 数组末尾添加:
+
+```json
+{
+  "root": "pages/activity",
+  "pages": [
+    {
+      "path": "index",
+      "style": { "navigationBarTitleText": "活动中心" }
+    },
+    {
+      "path": "activity-detail/activity-detail",
+      "style": { "navigationBarTitleText": "活动详情" }
+    }
+  ]
+}
+```
+
+验证:运行 `grep -n '"root": "pages/activity"' cfc-frontend/pages.json` 确认存在。
+
+- [ ] **Step 2: 提交**
+
+```bash
+git add cfc-frontend/pages.json
+git commit -m "feat(activity): register pages/activity subPackage"
+```
+
+---
+
+## Task 2: 创建活动主页 index.vue
+
+**Files:**
+- Create: `cfc-frontend/pages/activity/index.vue`
+
+- [ ] **Step 1: 创建文件结构**
+
+```vue
+<template>
+  <view class="activity-container">
+    <!-- 导航栏 -->
+    <view class="nav-bar">
+      <text class="nav-title">活动中心</text>
+    </view>
+
+    <!-- Tab 栏 -->
+    <view class="tab-bar">
+      <view
+        v-for="tab in tabs"
+        :key="tab.key"
+        :class="['tab-item', currentTab === tab.key ? 'active' : '']"
+        @click="onTabChange(tab.key)"
+      >
+        {{ tab.label }}
+      </view>
+    </view>
+
+    <!-- 内容区 -->
+    <scroll-view scroll-y class="content" @scrolltolower="onLoadMore">
+
+      <!-- 我的活动 Tab -->
+      <view v-if="currentTab === 'mine'" class="tab-content">
+        <!-- 状态筛选 -->
+        <scroll-view scroll-x enable-flex show-scrollbar="false" class="filter-scroll">
+          <view
+            v-for="filter in mineFilters"
+            :key="filter.value"
+            :class="['filter-chip', currentMineFilter === filter.value ? 'active' : '']"
+            @click="onMineFilterChange(filter.value)"
+          >
+            {{ filter.label }}
+          </view>
+        </scroll-view>
+
+        <!-- 加载中 -->
+        <view v-if="mineLoading" class="loading-state">
+          <text class="loading-text">加载中...</text>
+        </view>
+
+        <!-- 空状态 -->
+        <view v-else-if="myActivities.length === 0" class="empty-state">
+          <text class="empty-icon">📋</text>
+          <text class="empty-title">暂无报名的活动</text>
+          <text class="empty-desc">去发现页探索精彩活动吧</text>
+          <button class="empty-btn" @click="switchToDiscover">去发现活动</button>
+        </view>
+
+        <!-- 活动列表 -->
+        <view v-else class="activity-list">
+          <view
+            v-for="act in myActivities"
+            :key="act.id"
+            class="activity-card"
+            @click="goDetail(act)"
+          >
+            <!-- 卡片内容,见 Task 3 -->
+          </view>
+        </view>
+      </view>
+
+      <!-- 发现活动 Tab -->
+      <view v-if="currentTab === 'discover'" class="tab-content">
+        <!-- 维度筛选 -->
+        <scroll-view scroll-x enable-flex show-scrollbar="false" class="filter-scroll">
+          <view
+            v-for="dim in dimensionFilters"
+            :key="dim.value"
+            :class="['filter-chip', currentDimension === dim.value ? 'active' : '']"
+            @click="onDimensionChange(dim.value)"
+          >
+            {{ dim.label }}
+          </view>
+        </scroll-view>
+
+        <!-- 加载中/空状态/列表 -->
+        <view v-if="discoverLoading" class="loading-state">...</view>
+        <view v-else-if="discoverActivities.length === 0" class="empty-state">...</view>
+        <view v-else class="activity-list">...</view>
+      </view>
+
+      <!-- 历史记录 Tab -->
+      <view v-if="currentTab === 'history'" class="tab-content">
+        <!-- 历史筛选 + 列表 -->
+        <!-- 结构同我的活动 Tab -->
+      </view>
+
+      <!-- 上拉加载 -->
+      <view v-if="hasMore && !loading" class="load-more" @click="onLoadMore">
+        <text>加载更多</text>
+      </view>
+
+    </scroll-view>
+  </view>
+</template>
+```
+
+- [ ] **Step 2: 实现 data()**
+
+```javascript
+data() {
+  return {
+    tabs: [
+      { key: 'mine', label: '我的活动' },
+      { key: 'discover', label: '发现活动' },
+      { key: 'history', label: '历史记录' }
+    ],
+    currentTab: 'mine',
+    mineFilters: [
+      { label: '全部', value: '' },
+      { label: '待开始', value: 'upcoming' },
+      { label: '待审核', value: 'pending' },
+      { label: '已签到', value: 'checked_in' }
+    ],
+    dimensionFilters: [
+      { label: '全部', value: '' },
+      { label: '身', value: 'body' },
+      { label: '心', value: 'mind' },
+      { label: '智', value: 'wisdom' },
+      { label: '行', value: 'action' },
+      { label: '富', value: 'wealth' }
+    ],
+    currentMineFilter: '',
+    currentDimension: '',
+    myActivities: [],
+    discoverActivities: [],
+    historyActivities: [],
+    mineLoading: false,
+    discoverLoading: false,
+    minePage: 1,
+    discoverPage: 1,
+    historyPage: 1,
+    pageSize: 10,
+    hasMore: true
+  }
+}
+```
+
+- [ ] **Step 3: 实现 onLoad**
+
+```javascript
+onLoad(options) {
+  // 如果带 tab 参数,切换到对应 Tab
+  if (options && options.tab) {
+    this.currentTab = options.tab
+  }
+  this.loadData()
+}
+```
+
+- [ ] **Step 4: 实现 onShow**
+
+```javascript
+onShow() {
+  // 每次可见时刷新我的活动(用户可能刚报名/签到)
+  if (this.currentTab === 'mine') {
+    this.loadMyActivities(true)
+  }
+}
+```
+
+- [ ] **Step 5: 实现 methods — Tab 切换**
+
+```javascript
+onTabChange(key) {
+  this.currentTab = key
+  this.loadData()
+},
+loadData() {
+  if (this.currentTab === 'mine') {
+    this.loadMyActivities(true)
+  } else if (this.currentTab === 'discover') {
+    this.loadDiscoverActivities(true)
+  } else if (this.currentTab === 'history') {
+    this.loadHistoryActivities(true)
+  }
+}
+```
+
+- [ ] **Step 6: 实现 loadMyActivities**
+
+```javascript
+loadMyActivities(refresh) {
+  if (refresh) {
+    this.minePage = 1
+    this.myActivities = []
+    this.hasMore = true
+  }
+  if (!this.hasMore) return
+  this.mineLoading = true
+  const childId = uni.getStorageSync('currentChildId')
+  if (!childId) {
+    this.mineLoading = false
+    return
+  }
+  getMyActivityRegistrations({
+    childId: childId,
+    page: this.minePage,
+    size: this.pageSize,
+    status: this.currentMineFilter || undefined
+  }).then(res => {
+    this.mineLoading = false
+    if (res.code === 200 && res.data) {
+      const records = res.data.records || []
+      if (refresh) {
+        this.myActivities = records
+      } else {
+        this.myActivities = this.myActivities.concat(records)
+      }
+      this.hasMore = records.length >= this.pageSize
+      this.minePage++
+    }
+  }).catch(() => {
+    this.mineLoading = false
+  })
+}
+```
+
+- [ ] **Step 7: 实现 loadDiscoverActivities**
+
+```javascript
+loadDiscoverActivities(refresh) {
+  if (refresh) {
+    this.discoverPage = 1
+    this.discoverActivities = []
+    this.hasMore = true
+  }
+  if (!this.hasMore) return
+  this.discoverLoading = true
+  getActivityList({
+    dimensionCode: this.currentDimension || undefined,
+    page: this.discoverPage,
+    size: this.pageSize
+  }).then(res => {
+    this.discoverLoading = false
+    if (res.code === 200 && res.data) {
+      const records = res.data.records || []
+      if (refresh) {
+        this.discoverActivities = records
+      } else {
+        this.discoverActivities = this.discoverActivities.concat(records)
+      }
+      this.hasMore = records.length >= this.pageSize
+      this.discoverPage++
+    }
+  }).catch(() => {
+    this.discoverLoading = false
+  })
+}
+```
+
+- [ ] **Step 8: 实现 loadHistoryActivities(类似 loadMyActivities)**
+
+```javascript
+loadHistoryActivities(refresh) {
+  // 同 loadMyActivities,但 API 参数可能需要 status=ended
+  // 或直接复用 loadMyActivities 通过 filter 状态过滤
+}
+```
+
+- [ ] **Step 9: 实现筛选切换**
+
+```javascript
+onMineFilterChange(value) {
+  this.currentMineFilter = value
+  this.loadMyActivities(true)
+},
+onDimensionChange(value) {
+  this.currentDimension = value
+  this.loadDiscoverActivities(true)
+},
+onLoadMore() {
+  if (this.currentTab === 'mine') {
+    this.loadMyActivities(false)
+  } else if (this.currentTab === 'discover') {
+    this.loadDiscoverActivities(false)
+  } else if (this.currentTab === 'history') {
+    this.loadHistoryActivities(false)
+  }
+}
+```
+
+- [ ] **Step 10: 实现 goDetail / switchToDiscover**
+
+```javascript
+goDetail(act) {
+  uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + act.id })
+},
+switchToDiscover() {
+  this.currentTab = 'discover'
+  this.loadDiscoverActivities(true)
+}
+```
+
+- [ ] **Step 11: 提交**
+
+```bash
+git add cfc-frontend/pages/activity/index.vue
+git commit -m "feat(activity): create activity home page with 3-tab structure"
+```
+
+---
+
+## Task 3: 实现活动卡片组件(可内联在 index.vue 中)
+
+**Files:**
+- Already created in: `cfc-frontend/pages/activity/index.vue`
+
+- [ ] **Step 1: 卡片模板(内联在 index.vue 的 activity-card div 内)**
+
+```vue
+<view class="activity-card" @click="goDetail(act)">
+  <image
+    class="card-cover"
+    :src="act.coverImage || '/static/default-activity.png'"
+    mode="aspectFill"
+  />
+  <view class="card-info">
+    <text class="card-title">{{ act.title }}</text>
+    <text class="card-meta">{{ act.startTime }}</text>
+    <text class="card-meta" v-if="act.location">📍 {{ act.location }}</text>
+    <view class="card-bottom">
+      <text class="card-status" :class="'status-' + getStatusClass(act)">
+        {{ getStatusText(act) }}
+      </text>
+      <text class="card-price" v-if="act.price > 0">{{ formatPrice(act.price) }}</text>
+      <text class="card-price free" v-else>免费</text>
+    </view>
+  </view>
+</view>
+```
+
+- [ ] **Step 2: 卡片样式(scoped CSS)**
+
+```css
+.activity-card {
+  display: flex;
+  background: #fff;
+  border-radius: 12px;
+  overflow: hidden;
+  margin-bottom: 12px;
+  box-shadow: 0 1px 4px rgba(0,0,0,0.06);
+}
+.card-cover {
+  width: 72px;
+  height: 72px;
+  flex-shrink: 0;
+}
+.card-info {
+  flex: 1;
+  padding: 10px 12px;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+}
+.card-title {
+  font-size: 13px;
+  font-weight: bold;
+  color: #333;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.card-meta {
+  font-size: 10px;
+  color: #999;
+  margin-top: 2px;
+}
+.card-bottom {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 4px;
+}
+.card-status {
+  font-size: 9px;
+  padding: 2px 6px;
+  border-radius: 3px;
+}
+.status-upcoming { background: #FFF7ED; color: #F97316; }
+.status-pending { background: #FEF3C7; color: #D97706; }
+.status-approved { background: #F0FDF4; color: #22C55E; }
+.status-checked_in { background: #DCFCE7; color: #16A34A; }
+.status-ended { background: #F5F5F5; color: #999; }
+.status-rejected { background: #FEE2E2; color: #EF4444; }
+.card-price {
+  font-size: 11px;
+  color: #F97316;
+  font-weight: 500;
+}
+.card-price.free { color: #22C55E; }
+```
+
+- [ ] **Step 3: 辅助方法**
+
+```javascript
+getStatusClass(act) {
+  // 根据 act.status 和时间判断
+  // pending/approved/rejected/cancelled/checked_in
+  return act.status || 'upcoming'
+},
+getStatusText(act) {
+  const map = {
+    pending: '待审核',
+    approved: '报名成功',
+    rejected: '已拒绝',
+    cancelled: '已取消',
+    checked_in: '已签到'
+  }
+  // 如果没有 status 但有 endTime 且已过,显示已结束
+  if (!act.status && act.endTime && new Date(act.endTime) < new Date()) {
+    return '已结束'
+  }
+  return map[act.status] || '即将开始'
+},
+formatPrice(price) {
+  return '¥' + (price / 100).toFixed(2)
+}
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-frontend/pages/activity/index.vue
+git commit -m "feat(activity): add activity card component with status display"
+```
+
+---
+
+## Task 4: 实现报名/签到操作按钮
+
+**Files:**
+- Already modified in: `cfc-frontend/pages/activity/index.vue`
+
+- [ ] **Step 1: 在我的活动 Tab 的卡片底部添加操作按钮**
+
+在 `activity-card` 底部添加(需区分当前 Tab):
+
+```vue
+<!-- 我的活动 Tab 的卡片有操作按钮 -->
+<view v-if="currentTab === 'mine' && act.status === 'approved'" class="card-actions">
+  <button class="btn-cancel" @click.stop="onCancel(act)">取消报名</button>
+  <button class="btn-checkin" @click.stop="onCheckin(act)">签到</button>
+</view>
+```
+
+- [ ] **Step 2: 实现 onCheckin**
+
+```javascript
+onCheckin(act) {
+  const childId = uni.getStorageSync('currentChildId')
+  if (!childId) {
+    uni.showToast({ title: '请先选择孩子身份', icon: 'none' })
+    return
+  }
+  activityCheckin({ id: act.id, childId: childId }).then(res => {
+    if (res.code === 200) {
+      uni.showToast({ title: '签到成功', icon: 'success' })
+      this.loadMyActivities(true)
+    } else {
+      uni.showToast({ title: res.message || '签到失败', icon: 'none' })
+    }
+  })
+}
+```
+
+- [ ] **Step 3: 实现 onCancel**
+
+```javascript
+onCancel(act) {
+  uni.showModal({
+    title: '提示',
+    content: '确定要取消报名吗?',
+    success: res => {
+      if (res.confirm) {
+        const childId = uni.getStorageSync('currentChildId')
+        cancelActivityRegistration({ id: act.id, childId: childId }).then(res => {
+          if (res.code === 200) {
+            uni.showToast({ title: '已取消报名', icon: 'success' })
+            this.loadMyActivities(true)
+          } else {
+            uni.showToast({ title: res.message || '取消失败', icon: 'none' })
+          }
+        })
+      }
+    }
+  })
+}
+```
+
+- [ ] **Step 4: 添加操作按钮样式**
+
+```css
+.card-actions {
+  display: flex;
+  justify-content: flex-end;
+  gap: 6px;
+  padding-top: 6px;
+  border-top: 1px solid #f5f5f5;
+  margin-top: 6px;
+}
+.btn-cancel {
+  background: #fff;
+  border: 1px solid #ddd;
+  color: #666;
+  padding: 4px 12px;
+  border-radius: 14px;
+  font-size: 10px;
+  line-height: 1.5;
+}
+.btn-checkin {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  border: none;
+  color: #fff;
+  padding: 4px 12px;
+  border-radius: 14px;
+  font-size: 10px;
+  line-height: 1.5;
+}
+```
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-frontend/pages/activity/index.vue
+git commit -m "feat(activity): add checkin and cancel registration actions"
+```
+
+---
+
+## Task 5: 更新 16 个页面的 goMoreActivities 跳转目标
+
+**Files:**
+- Modify: 16个 Vue 文件(见文件结构列表)
+
+- [ ] **Step 1: 批量更新 parent-index.vue**
+
+在 `pages/index/parent-index.vue` 中找到 `goMoreActivities` 方法:
+
+```javascript
+// 修改前
+goMoreActivities() {
+  uni.switchTab({ url: '/pages/discover/index' })
+}
+
+// 修改后
+goMoreActivities() {
+  uni.navigateTo({ url: '/pages/activity/index' })
+}
+```
+
+- [ ] **Step 2: 批量更新其余 15 个文件**
+
+对以下每个文件执行相同修改(将 `goMoreActivities` 中的跳转目标从 `discover/index` 或 `switchTab` 改为 `/pages/activity/index`):
+
+```
+pages/index/child-index.vue
+pages/body/index.vue
+pages/body/member-body-detail.vue
+pages/body-detail/member-body-detail.vue
+pages/mind/index.vue
+pages/mind/member-mind-detail.vue
+pages/mind-detail/member-mind-detail.vue
+pages/wisdom/index.vue
+pages/wisdom/member-wisdom-detail.vue
+pages/wisdom/wisdom_temp/index.vue
+pages/wisdom/wisdom_temp/member-wisdom-detail.vue
+pages/wisdom-detail/member-wisdom-detail.vue
+pages/action/index.vue
+pages/action/member-action-detail.vue
+pages/action-detail/member-action-detail.vue
+```
+
+验证:每个文件中应该只有一处 `goMoreActivities` 方法,搜索确认后修改。
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add pages/index/parent-index.vue pages/index/child-index.vue pages/body/index.vue pages/body/member-body-detail.vue pages/body-detail/member-body-detail.vue pages/mind/index.vue pages/mind/member-mind-detail.vue pages/mind-detail/member-mind-detail.vue pages/wisdom/index.vue pages/wisdom/member-wisdom-detail.vue pages/wisdom/wisdom_temp/index.vue pages/wisdom/wisdom_temp/member-wisdom-detail.vue pages/wisdom-detail/member-wisdom-detail.vue pages/action/index.vue pages/action/member-action-detail.vue pages/action-detail/member-action-detail.vue
+git commit -m "refactor(activity): update goMoreActivities to navigate to /pages/activity/index"
+```
+
+---
+
+## Task 6: 创建 activity-detail 页面(可选,复用现有)
+
+**Files:**
+- Create: `cfc-frontend/pages/activity/activity-detail/activity-detail.vue`
+
+- [ ] **Step 1: 复制现有 activity-detail 作为基础**
+
+将 `pages/discover-detail/activity-detail/activity-detail.vue` 复制到 `pages/activity/activity-detail/activity-detail.vue`。
+
+此步骤可选,因为活动详情已有两个版本,路由复用已足够。此任务可跳过。
+
+---
+
+## Task 7: 整体验证
+
+- [ ] **Step 1: 编译验证**
+
+```bash
+cd cfc-frontend && npm run dev:mp-weixin 2>&1 | head -50
+```
+
+- [ ] **Step 2: 注册验证**
+
+确认 `pages.json` 中 activity subPackage 正确注册。
+
+- [ ] **Step 3: 路由验证**
+
+在微信开发者工具中测试:
+1. 从任意维度页点击"更多 ›",应跳转 `/pages/activity/index`
+2. 活动主页三个 Tab 切换正常
+3. 点击活动卡片,跳转详情页
+
+---
+
+## 执行方式选择
+
+**两个执行选项:**
+
+**1. Subagent-Driven (recommended)** — 每个 Task 由独立 subagent 执行,Task 间有检查点
+
+**2. Inline Execution** — 在当前 session 中顺序执行所有 Task,带检查点
+
+请选择执行方式,我随后通过 `executing-plans` 或 `subagent-driven-development` 技能执行计划。

+ 241 - 0
cfc-frontend/docs/superpowers/specs/2026-07-02-activity-home-page-design.md

@@ -0,0 +1,241 @@
+# 活动主页设计文档
+
+**日期**: 2026-07-02
+**状态**: 进行中
+**负责人**: Sisyphus
+
+---
+
+## 1. 背景与目标
+
+### 背景
+
+目前小程序中各维度页面(身/心/智/行/富)的 `DimensionActivities` 组件点击"更多"按钮后统一跳转到 `discover/index?type=activity`,该页面以商品发现为主,活动仅为筛选类型之一,用户体验不专注。
+
+### 目标
+
+设计独立的**活动主页** `/pages/activity/index`,作为活动聚合入口,统一承接所有维度页"更多"来的流量,优先展示用户报名的活动。
+
+---
+
+## 2. 用户场景
+
+1. **家长/孩子** 从某个维度详情页点击"更多 ›"进入活动主页,查看自己报名的活动或发现新活动
+2. **服务商(规划师)** 进入活动主页,浏览/管理自己发布的活动
+3. **未登录用户** 进入活动主页,仅能浏览公开活动,不能报名/签到
+
+---
+
+## 3. 页面结构
+
+### 路由
+
+```
+/pages/activity/index   (新建,独立subPackage: pages/activity)
+```
+
+### Tab 结构(三 Tab 并列)
+
+| Tab | 内容 | 数据来源 |
+|-----|------|----------|
+| 我的活动 | 用户报名的活动(含状态筛选) | `getMyActivityRegistrations` |
+| 发现活动 | 所有公开活动(含维度筛选) | `getActivityList` |
+| 历史记录 | 已结束/已签到的活动 | `getMyActivityRegistrations(status=ended)` |
+
+**注意**: 未登录用户默认展示"发现活动"Tab,"我的活动"和"历史记录"显示登录引导。
+
+---
+
+## 4. 各 Tab 详细设计
+
+### 4.1 我的活动 Tab(默认激活)
+
+**状态筛选 Chips**(横向滚动):
+- 全部 / 🟢 待开始 / 🟡 待审核 / ✅ 已签到 / ❌ 已取消
+
+**活动卡片展示**:
+
+```
+┌────────────────────────────────────────┐
+│ [封面图]  │  活动标题                     │
+│ 72×72    │  🕐 时间 / 📍 地点              │
+│          │  [状态标签] [倒计时标签]          │
+│          ├────────────────────────────────┤
+│          │  [取消报名]        [签到]        │
+└────────────────────────────────────────┘
+```
+
+**操作按钮逻辑**:
+- `报名成功 + 未到时间` → 显示"签到"按钮
+- `报名成功 + 已到时间 + 未签到` → 显示"签到"按钮
+- `报名待审核` → 显示"取消报名"按钮(灰色)
+- `已签到` → 不显示操作按钮
+- `已结束` → 归入"历史记录"Tab
+
+**空状态**:
+```
+📋 暂无报名的活动
+去发现页探索精彩活动吧
+[去发现活动] 按钮
+```
+
+---
+
+### 4.2 发现活动 Tab
+
+**维度筛选 Chips**(横向滚动):
+- 全部 / 身 / 心 / 智 / 行 / 富
+
+**活动卡片展示**:
+
+```
+┌────────────────────────────────────────┐
+│ [封面图]  │  活动标题                     │
+│ 72×72    │  🕐 时间 / 📍 地点              │
+│          │  [状态标签]  价格               │
+└────────────────────────────────────────┘
+```
+
+**点击卡片** → 跳转 `/pages/discover/activity-detail/activity-detail?id=xxx`
+
+**空状态**:
+```
+🎯 暂无相关活动
+试试其他维度筛选
+```
+
+---
+
+### 4.3 历史记录 Tab
+
+**筛选 Chips**: 全部 / 已结束 / 已签到
+
+**卡片样式同"我的活动"**,但操作按钮均不显示。
+
+---
+
+## 5. API 集成
+
+### 现有 API(无需后端改动)
+
+| 前端函数 | 端点 | 参数 |
+|----------|------|------|
+| `getMyActivityRegistrations(data)` | `POST /api/activity/my-registrations` | `{ childId, page, size, status? }` |
+| `getActivityList(data)` | `POST /api/activity/list` | `{ dimensionCode?, page, size, status? }` |
+| `getActivityDetail(id)` | `POST /api/activity/detail` | `{ id }` |
+| `registerActivity(data)` | `POST /api/activity/register` | `{ id, childId }` |
+| `cancelActivityRegistration(data)` | `POST /api/activity/cancel-registration` | `{ id, childId }` |
+| `activityCheckin(data)` | `POST /api/activity/checkin` | `{ id, childId }` |
+
+### 状态映射
+
+| 后端 status | 前端标签 | 说明 |
+|-------------|----------|------|
+| `pending` | 🟡 待审核 | 报名待审核 |
+| `approved` | 🟢 报名成功 | 已通过审核 |
+| `rejected` | ❌ 已拒绝 | 报名被拒绝 |
+| `cancelled` | ⚪ 已取消 | 用户取消 |
+| `checked_in` | ✅ 已签到 | 签到完成 |
+
+---
+
+## 6. 入口整合
+
+### 需要更新的页面(共16个文件)
+
+所有使用 `DimensionActivities` 组件的页面,其 `goMoreActivities` 方法需改为:
+
+```javascript
+goMoreActivities: function() {
+  uni.navigateTo({ url: '/pages/activity/index' })
+}
+```
+
+**受影响文件**:
+- `pages/index/parent-index.vue`
+- `pages/index/child-index.vue`
+- `pages/body/index.vue`
+- `pages/body/member-body-detail.vue`
+- `pages/body-detail/member-body-detail.vue`
+- `pages/mind/index.vue`
+- `pages/mind/member-mind-detail.vue`
+- `pages/mind-detail/member-mind-detail.vue`
+- `pages/wisdom/index.vue`
+- `pages/wisdom/member-wisdom-detail.vue`
+- `pages/wisdom/wisdom_temp/member-wisdom-detail.vue`
+- `pages/wisdom-detail/member-wisdom-detail.vue`
+- `pages/action/index.vue`
+- `pages/action/member-action-detail.vue`
+- `pages/action-detail/member-action-detail.vue`
+- `pages/wisdom/wisdom_temp/index.vue`
+
+---
+
+## 7. 文件清单
+
+### 新增文件
+
+| 文件 | 说明 |
+|------|------|
+| `cfc-frontend/pages/activity/index.vue` | 活动主页主页面 |
+| `cfc-frontend/pages/activity/activity-detail/activity-detail.vue` | 活动详情页(可复用现有,但需迁移路由注册) |
+
+### 需修改文件
+
+| 文件 | 修改内容 |
+|------|----------|
+| `cfc-frontend/pages.json` | 注册 `pages/activity` subPackage |
+| 16个使用 DimensionActivities 的页面 | 更新 `goMoreActivities` 跳转目标 |
+
+### 可复用的现有组件/样式
+
+- `DimensionActivities.vue` 卡片样式参考(但不完全复用,因为是新页面自己管理数据)
+- `pages/discover/activity-detail/activity-detail.vue` 详情页逻辑参考
+- `uni.scss` 全局样式变量
+
+---
+
+## 8. 路由注册(pages.json)
+
+```json
+{
+  "root": "pages/activity",
+  "pages": [
+    {
+      "path": "index",
+      "style": { "navigationBarTitleText": "活动中心" }
+    },
+    {
+      "path": "activity-detail/activity-detail",
+      "style": { "navigationBarTitleText": "活动详情" }
+    }
+  ]
+}
+```
+
+**注意**: `pages/discover/activity-detail/activity-detail.vue` 仍保留(discover-detail subPackage),新路由为独立 activity subPackage。
+
+---
+
+## 9. 待确认事项(TBD)
+
+以下内容需用户确认后才能最终确定:
+
+1. **「历史记录」Tab** — 是否需要?如不需要可简化为两 Tab
+2. **服务商视角** — 是否需要在活动主页展示"我发布的活动"?(如需要,Tab 调整为:我参加的活动 / 我发布的活动 / 发现活动)
+3. **维度筛选 Chips** — 发现活动 Tab 的维度筛选是否需要按五维(身/心/智/行/富)?
+
+---
+
+## 10. 设计风格
+
+- **主色调**: `#F97316`(暖橙,同小程序主色)
+- **卡片风格**: 白色背景 + 轻阴影 + 圆角12px
+- **字体**: 系统默认
+- **图片**: 封面图 72×72 正方形,圆角8px
+- **状态标签色**:
+  - 待开始: `#FFF7ED` + `#F97316`
+  - 待审核: `#FEF3C7` + `#D97706`
+  - 已签到: `#F0FDF4` + `#22C55E`
+  - 已拒绝: `#FEE2E2` + `#EF4444`
+  - 已取消: `#F5F5F5` + `#999`

+ 13 - 0
cfc-frontend/pages.json

@@ -754,6 +754,19 @@
           "style": { "navigationBarTitleText": "加入家庭", "navigationStyle": "custom" }
         }
       ]
+    },
+    {
+      "root": "pages/activity",
+      "pages": [
+        {
+          "path": "index",
+          "style": { "navigationBarTitleText": "活动中心" }
+        },
+        {
+          "path": "activity-detail/activity-detail",
+          "style": { "navigationBarTitleText": "活动详情" }
+        }
+      ]
     }
   ],
   "globalStyle": {

+ 1 - 1
cfc-frontend/pages/action-detail/member-action-detail.vue

@@ -202,7 +202,7 @@ export default {
       if (act && act.id) uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
     },
     goMoreActivities: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=activity&dimension=action' })
+      uni.navigateTo({ url: '/pages/activity/index' })
     },
     goProductDetail: function(prod) {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })

+ 14 - 28
cfc-frontend/pages/action/index.vue

@@ -68,31 +68,12 @@
       @moreProducts="goMoreProducts" />
 
     <!-- 推荐阅读 -->
-    <view class="section" v-if="actionArticles.length > 0">
-      <view class="section-header">
-        <text class="section-title">推荐阅读</text>
-      </view>
-      <view class="article-list">
-        <view class="article-card" v-for="article in actionArticles" :key="article.id" @click="goArticleDetail(article.id)">
-          <view class="article-category" :style="{ background: article.categoryColor }">
-            <text class="article-category-text">{{ article.category }}</text>
-          </view>
-          <text class="article-title">{{ article.title }}</text>
-          <text class="article-summary">{{ article.summary }}</text>
-          <view class="article-meta">
-            <view class="meta-item">
-              <text class="meta-icon">&#x1F4D6;</text>
-              <text class="meta-text">{{ article.views }}</text>
-            </view>
-            <view class="meta-item">
-              <text class="meta-icon">&#x1F44D;</text>
-              <text class="meta-text">{{ article.likes }}</text>
-            </view>
-            <text class="article-date">{{ article.date }}</text>
-          </view>
-        </view>
-      </view>
-    </view>
+    <DimensionArticles
+      dimensionCode="action"
+      :articles="articles"
+      :isLoggedIn="isLoggedIn"
+      @articleClick="goArticleDetail"
+      @moreArticles="goMoreArticles" />
 
     <!-- ===== 重要关系维护 ===== -->
     <view class="contact-section" v-if="isLoggedIn">
@@ -199,13 +180,14 @@ import UserQuickEntry from '../../components/UserQuickEntry.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
+import DimensionArticles from '../../components/DimensionArticles.vue'
 import ContactCard from '../../components/ContactCard.vue'
 import ContactImport from '../../components/ContactImport.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import { getVisibleSections, getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getContactList, getVisibleFamilyMembers, getFeaturedArticles, getHealthAlerts, getMilestones } from '../../utils/api.js'
 
 export default {
-  components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, ContactCard, ContactImport, FamilyRelationGraph },
+  components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, ContactCard, ContactImport, FamilyRelationGraph },
   data() {
     return {
       showTabTransition: true,
@@ -456,7 +438,7 @@ export default {
       uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
     },
     goMoreActivities: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=activity' })
+      uni.navigateTo({ url: '/pages/activity/index' })
     },
     goProductDetail: function(prod) {
       uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
@@ -518,9 +500,13 @@ export default {
         }
       }).catch(function() {})
     },
-    goArticleDetail: function(id) {
+    goArticleDetail: function(article) {
+      var id = article.id || article
       uni.navigateTo({ url: '/pages/mind-detail/article-detail?id=' + id })
     },
+    goMoreArticles: function() {
+      uni.navigateTo({ url: '/pages/mind-detail/articles' })
+    },
     loadRelationshipHealth: function() {
       var self = this
       getHealthAlerts({}).then(function(res) {

+ 1 - 1
cfc-frontend/pages/action/member-action-detail.vue

@@ -202,7 +202,7 @@ export default {
       if (act && act.id) uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + act.id })
     },
     goMoreActivities: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=activity&dimension=action' })
+      uni.navigateTo({ url: '/pages/activity/index' })
     },
     goProductDetail: function(prod) {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })

+ 866 - 0
cfc-frontend/pages/activity/index.vue

@@ -0,0 +1,866 @@
+<template>
+  <view class="activity-container">
+    <!-- Nav bar with title "活动中心" -->
+    <view class="nav-bar">
+      <text class="nav-title">活动中心</text>
+    </view>
+
+    <!-- Tab bar (3 tabs) -->
+    <view class="tab-bar">
+      <view
+        class="tab-item"
+        :class="{ active: currentTab === tab.key }"
+        v-for="tab in tabs"
+        :key="tab.key"
+        @click="onTabChange(tab.key)"
+      >
+        <text class="tab-label">{{ tab.label }}</text>
+      </view>
+    </view>
+
+    <!-- ScrollView content area -->
+    <scroll-view
+      class="content-scroll"
+      scroll-y
+      :refresher-enabled="true"
+      :refresher-triggered="isRefreshing"
+      @refresherrefresh="onRefresh"
+      @scrolltolower="onLoadMore"
+    >
+      <!-- Tab 1: mine -->
+      <view v-if="currentTab === 'mine'">
+        <!-- Filter chips -->
+        <scroll-view class="filter-scroll" scroll-x enable-flex>
+          <view class="filter-chips">
+            <view
+              class="filter-chip"
+              :class="{ active: currentMineFilter === filter.value }"
+              v-for="filter in mineFilters"
+              :key="filter.value"
+              @click="onMineFilterChange(filter.value)"
+            >
+              <text class="filter-label">{{ filter.label }}</text>
+            </view>
+          </view>
+        </scroll-view>
+
+        <!-- Activity cards -->
+        <view class="card-list" v-if="myActivities.length > 0">
+          <view
+            class="activity-card"
+            v-for="act in myActivities"
+            :key="act.id"
+            @click="goDetail(act)"
+          >
+            <image
+              class="card-cover"
+              :src="act.coverImage || '/static/default-activity.png'"
+              mode="aspectFill"
+            />
+            <view class="card-info">
+              <text class="card-title">{{ act.title }}</text>
+              <text class="card-meta">{{ act.startTime }}</text>
+              <text class="card-meta" v-if="act.location">📍 {{ act.location }}</text>
+              <view class="card-bottom">
+                <text class="card-status" :class="getStatusClass(act)">
+                  {{ getStatusText(act) }}
+                </text>
+                <text class="card-price" v-if="act.price > 0">¥{{ formatPrice(act.price) }}</text>
+                <text class="card-price free" v-else>免费</text>
+              </view>
+              <!-- Action buttons for approved status -->
+              <view
+                v-if="act.status === 'approved'"
+                class="card-actions"
+              >
+                <button class="btn-cancel" @click.stop="onCancel(act)">取消报名</button>
+                <button class="btn-checkin" @click.stop="onCheckin(act)">签到</button>
+              </view>
+            </view>
+          </view>
+        </view>
+
+        <!-- Empty state for mine -->
+        <view class="empty-state" v-else-if="!mineLoading">
+          <text class="empty-icon">📋</text>
+          <text class="empty-title">暂无报名的活动</text>
+          <text class="empty-desc">去发现页探索精彩活动吧</text>
+          <button class="empty-btn" @click="switchToDiscover">去发现活动</button>
+        </view>
+
+        <!-- Loading state -->
+        <view class="loading-state" v-if="mineLoading">
+          <view class="loading-spinner"></view>
+          <text class="loading-text">加载中...</text>
+        </view>
+
+        <!-- Load more tip -->
+        <view class="load-more-tip" v-if="myActivities.length > 0 && !hasMore && currentTab === 'mine'">
+          <text class="load-more-text">没有更多了</text>
+        </view>
+      </view>
+
+      <!-- Tab 2: discover -->
+      <view v-if="currentTab === 'discover'">
+        <!-- Filter chips (dimension) -->
+        <scroll-view class="filter-scroll" scroll-x enable-flex>
+          <view class="filter-chips">
+            <view
+              class="filter-chip"
+              :class="{ active: currentDimension === filter.value }"
+              v-for="filter in dimensionFilters"
+              :key="filter.value"
+              @click="onDimensionChange(filter.value)"
+            >
+              <text class="filter-label">{{ filter.label }}</text>
+            </view>
+          </view>
+        </scroll-view>
+
+        <!-- Activity cards -->
+        <view class="card-list" v-if="discoverActivities.length > 0">
+          <view
+            class="activity-card"
+            v-for="act in discoverActivities"
+            :key="act.id"
+            @click="goDetail(act)"
+          >
+            <image
+              class="card-cover"
+              :src="act.coverImage || '/static/default-activity.png'"
+              mode="aspectFill"
+            />
+            <view class="card-info">
+              <text class="card-title">{{ act.title }}</text>
+              <text class="card-meta">{{ act.startTime }}</text>
+              <text class="card-meta" v-if="act.location">📍 {{ act.location }}</text>
+              <view class="card-bottom">
+                <text class="card-status status-published">进行中</text>
+                <text class="card-price" v-if="act.price > 0">¥{{ formatPrice(act.price) }}</text>
+                <text class="card-price free" v-else>免费</text>
+              </view>
+            </view>
+          </view>
+        </view>
+
+        <!-- Empty state for discover -->
+        <view class="empty-state" v-else-if="!discoverLoading">
+          <text class="empty-icon">🔍</text>
+          <text class="empty-title">暂无活动</text>
+          <text class="empty-desc">稍后再来看看吧</text>
+        </view>
+
+        <!-- Loading state -->
+        <view class="loading-state" v-if="discoverLoading">
+          <view class="loading-spinner"></view>
+          <text class="loading-text">加载中...</text>
+        </view>
+
+        <!-- Load more tip -->
+        <view class="load-more-tip" v-if="discoverActivities.length > 0 && !hasMore && currentTab === 'discover'">
+          <text class="load-more-text">没有更多了</text>
+        </view>
+      </view>
+
+      <!-- Tab 3: history -->
+      <view v-if="currentTab === 'history'">
+        <!-- Filter chips (same as mine) -->
+        <scroll-view class="filter-scroll" scroll-x enable-flex>
+          <view class="filter-chips">
+            <view
+              class="filter-chip"
+              :class="{ active: currentMineFilter === filter.value }"
+              v-for="filter in mineFilters"
+              :key="filter.value"
+              @click="onMineFilterChange(filter.value)"
+            >
+              <text class="filter-label">{{ filter.label }}</text>
+            </view>
+          </view>
+        </scroll-view>
+
+        <!-- Activity cards -->
+        <view class="card-list" v-if="historyActivities.length > 0">
+          <view
+            class="activity-card"
+            v-for="act in historyActivities"
+            :key="act.id"
+            @click="goDetail(act)"
+          >
+            <image
+              class="card-cover"
+              :src="act.coverImage || '/static/default-activity.png'"
+              mode="aspectFill"
+            />
+            <view class="card-info">
+              <text class="card-title">{{ act.title }}</text>
+              <text class="card-meta">{{ act.startTime }}</text>
+              <text class="card-meta" v-if="act.location">📍 {{ act.location }}</text>
+              <view class="card-bottom">
+                <text class="card-status status-ended">已结束</text>
+                <text class="card-price" v-if="act.price > 0">¥{{ formatPrice(act.price) }}</text>
+                <text class="card-price free" v-else>免费</text>
+              </view>
+            </view>
+          </view>
+        </view>
+
+        <!-- Empty state for history -->
+        <view class="empty-state" v-else-if="!historyLoading">
+          <text class="empty-icon">📜</text>
+          <text class="empty-title">暂无历史活动</text>
+          <text class="empty-desc">参加过的活动会显示在这里</text>
+        </view>
+
+        <!-- Loading state -->
+        <view class="loading-state" v-if="historyLoading">
+          <view class="loading-spinner"></view>
+          <text class="loading-text">加载中...</text>
+        </view>
+
+        <!-- Load more tip -->
+        <view class="load-more-tip" v-if="historyActivities.length > 0 && !hasMore && currentTab === 'history'">
+          <text class="load-more-text">没有更多了</text>
+        </view>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import { getActivityList, getMyActivityRegistrations, activityCheckin, cancelActivityRegistration } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      tabs: [
+        { key: 'mine', label: '我的活动' },
+        { key: 'discover', label: '发现活动' },
+        { key: 'history', label: '历史记录' }
+      ],
+      currentTab: 'mine',
+      mineFilters: [
+        { label: '全部', value: '' },
+        { label: '待开始', value: 'upcoming' },
+        { label: '待审核', value: 'pending' },
+        { label: '已签到', value: 'checked_in' }
+      ],
+      dimensionFilters: [
+        { label: '全部', value: '' },
+        { label: '身', value: 'body' },
+        { label: '心', value: 'mind' },
+        { label: '智', value: 'wisdom' },
+        { label: '行', value: 'action' },
+        { label: '富', value: 'wealth' }
+      ],
+      currentMineFilter: '',
+      currentDimension: '',
+      myActivities: [],
+      discoverActivities: [],
+      historyActivities: [],
+      mineLoading: false,
+      discoverLoading: false,
+      historyLoading: false,
+      minePage: 1,
+      discoverPage: 1,
+      historyPage: 1,
+      pageSize: 10,
+      hasMore: true,
+      isRefreshing: false
+    }
+  },
+  onLoad: function(options) {
+    if (options && options.tab) {
+      this.currentTab = options.tab
+      this.loadData()
+    }
+  },
+  onShow: function() {
+    if (this.currentTab === 'mine') {
+      this.loadMyActivities(true)
+    }
+  },
+  methods: {
+    onTabChange: function(key) {
+      this.currentTab = key
+      this.hasMore = true
+      if (key === 'mine') {
+        this.minePage = 1
+        this.myActivities = []
+      } else if (key === 'discover') {
+        this.discoverPage = 1
+        this.discoverActivities = []
+      } else if (key === 'history') {
+        this.historyPage = 1
+        this.historyActivities = []
+      }
+      this.loadData()
+    },
+    loadData: function() {
+      if (this.currentTab === 'mine') {
+        this.loadMyActivities()
+      } else if (this.currentTab === 'discover') {
+        this.loadDiscoverActivities()
+      } else if (this.currentTab === 'history') {
+        this.loadHistoryActivities()
+      }
+    },
+    loadMyActivities: function(refresh) {
+      var self = this
+      if (refresh) {
+        this.minePage = 1
+        this.myActivities = []
+      }
+      this.mineLoading = true
+      var childId = uni.getStorageSync('currentChildId')
+      if (!childId) {
+        this.mineLoading = false
+        return
+      }
+      var params = {
+        childId: childId,
+        page: this.minePage,
+        size: this.pageSize
+      }
+      if (this.currentMineFilter && this.currentMineFilter !== 'upcoming') {
+        params.status = this.currentMineFilter
+      }
+      getMyActivityRegistrations(params).then(function(res) {
+        self.mineLoading = false
+        if (res && res.code === 200 && res.data) {
+          var records = res.data.records || []
+          if (self.currentMineFilter === 'upcoming') {
+            var now = new Date()
+            var filtered = []
+            for (var i = 0; i < records.length; i++) {
+              var act = records[i]
+              if (act.startTime) {
+                var startTime = new Date(act.startTime.replace(/-/g, '/'))
+                if (startTime > now && act.status === 'approved') {
+                  filtered.push(act)
+                }
+              }
+            }
+            records = filtered
+          }
+          if (refresh) {
+            self.myActivities = records
+          } else {
+            self.myActivities = self.myActivities.concat(records)
+          }
+          self.hasMore = records.length >= self.pageSize
+        }
+      }).catch(function() {
+        self.mineLoading = false
+      })
+    },
+    loadDiscoverActivities: function(refresh) {
+      var self = this
+      if (refresh) {
+        this.discoverPage = 1
+        this.discoverActivities = []
+      }
+      this.discoverLoading = true
+      var params = {
+        page: this.discoverPage,
+        size: this.pageSize
+      }
+      if (this.currentDimension) {
+        params.dimensionCode = this.currentDimension
+      }
+      getActivityList(params).then(function(res) {
+        self.discoverLoading = false
+        if (res && res.code === 200 && res.data) {
+          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
+          if (refresh) {
+            self.discoverActivities = list
+          } else {
+            self.discoverActivities = self.discoverActivities.concat(list)
+          }
+          self.hasMore = list.length >= self.pageSize
+        }
+      }).catch(function() {
+        self.discoverLoading = false
+      })
+    },
+    loadHistoryActivities: function(refresh) {
+      var self = this
+      if (refresh) {
+        this.historyPage = 1
+        this.historyActivities = []
+      }
+      this.historyLoading = true
+      var childId = uni.getStorageSync('currentChildId')
+      if (!childId) {
+        this.historyLoading = false
+        return
+      }
+      var params = {
+        childId: childId,
+        page: this.historyPage,
+        size: this.pageSize
+      }
+      getMyActivityRegistrations(params).then(function(res) {
+        self.historyLoading = false
+        if (res && res.code === 200 && res.data) {
+          var records = res.data.records || []
+          var now = new Date()
+          var ended = []
+          for (var i = 0; i < records.length; i++) {
+            var act = records[i]
+            if (act.endTime) {
+              var endTime = new Date(act.endTime.replace(/-/g, '/'))
+              if (endTime < now || act.status === 'cancelled' || act.status === 'rejected') {
+                ended.push(act)
+              }
+            } else {
+              ended.push(act)
+            }
+          }
+          if (refresh) {
+            self.historyActivities = ended
+          } else {
+            self.historyActivities = self.historyActivities.concat(ended)
+          }
+          self.hasMore = records.length >= self.pageSize
+        }
+      }).catch(function() {
+        self.historyLoading = false
+      })
+    },
+    onMineFilterChange: function(value) {
+      this.currentMineFilter = value
+      this.minePage = 1
+      this.myActivities = []
+      this.hasMore = true
+      if (this.currentTab === 'mine') {
+        this.loadMyActivities()
+      } else if (this.currentTab === 'history') {
+        this.loadHistoryActivities()
+      }
+    },
+    onDimensionChange: function(value) {
+      this.currentDimension = value
+      this.discoverPage = 1
+      this.discoverActivities = []
+      this.hasMore = true
+      this.loadDiscoverActivities()
+    },
+    onLoadMore: function() {
+      if (!this.hasMore) return
+      if (this.currentTab === 'mine' && !this.mineLoading) {
+        this.minePage++
+        this.loadMyActivities()
+      } else if (this.currentTab === 'discover' && !this.discoverLoading) {
+        this.discoverPage++
+        this.loadDiscoverActivities()
+      } else if (this.currentTab === 'history' && !this.historyLoading) {
+        this.historyPage++
+        this.loadHistoryActivities()
+      }
+    },
+    onRefresh: function() {
+      var self = this
+      this.isRefreshing = true
+      if (this.currentTab === 'mine') {
+        this.loadMyActivities(true).finally(function() {
+          self.isRefreshing = false
+        })
+      } else if (this.currentTab === 'discover') {
+        this.loadDiscoverActivities(true).finally(function() {
+          self.isRefreshing = false
+        })
+      } else if (this.currentTab === 'history') {
+        this.loadHistoryActivities(true).finally(function() {
+          self.isRefreshing = false
+        })
+      } else {
+        this.isRefreshing = false
+      }
+    },
+    goDetail: function(act) {
+      uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
+    },
+    switchToDiscover: function() {
+      this.currentTab = 'discover'
+      this.loadData()
+    },
+    getStatusClass: function(act) {
+      if (!act.status) {
+        if (act.endTime) {
+          var endTime = new Date(act.endTime.replace(/-/g, '/'))
+          if (endTime < new Date()) {
+            return 'status-ended'
+          }
+        }
+        return 'status-upcoming'
+      }
+      return 'status-' + act.status
+    },
+    getStatusText: function(act) {
+      var statusMap = {
+        pending: '待审核',
+        approved: '报名成功',
+        rejected: '已拒绝',
+        cancelled: '已取消',
+        checked_in: '已签到'
+      }
+      if (!act.status) {
+        if (act.endTime) {
+          var endTime = new Date(act.endTime.replace(/-/g, '/'))
+          if (endTime < new Date()) {
+            return '已结束'
+          }
+        }
+        return '待开始'
+      }
+      return statusMap[act.status] || act.status
+    },
+    formatPrice: function(price) {
+      return (price / 100).toFixed(2)
+    },
+    onCheckin: function(act) {
+      var self = this
+      var childId = uni.getStorageSync('currentChildId')
+      if (!childId) {
+        uni.showToast({ title: '请先选择孩子身份', icon: 'none' })
+        return
+      }
+      activityCheckin({ id: act.activityId || act.id, childId: childId }).then(function(res) {
+        if (res && res.code === 200) {
+          uni.showToast({ title: '签到成功', icon: 'success' })
+          self.loadMyActivities(true)
+        } else {
+          uni.showToast({ title: (res && res.message) || '签到失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.showToast({ title: '网络异常', icon: 'none' })
+      })
+    },
+    onCancel: function(act) {
+      var self = this
+      uni.showModal({
+        title: '提示',
+        content: '确定要取消报名吗?',
+        success: function(res) {
+          if (res.confirm) {
+            var childId = uni.getStorageSync('currentChildId')
+            if (!childId) {
+              uni.showToast({ title: '请先选择孩子身份', icon: 'none' })
+              return
+            }
+            cancelActivityRegistration({ id: act.activityId || act.id, childId: childId }).then(function(res) {
+              if (res && res.code === 200) {
+                uni.showToast({ title: '已取消报名', icon: 'success' })
+                self.loadMyActivities(true)
+              } else {
+                uni.showToast({ title: (res && res.message) || '取消失败', icon: 'none' })
+              }
+            }).catch(function() {
+              uni.showToast({ title: '网络异常', icon: 'none' })
+            })
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.activity-container {
+  min-height: 100vh;
+  background: #F5F5F5;
+  display: flex;
+  flex-direction: column;
+}
+
+.nav-bar {
+  background: #1A0F00;
+  padding: 20rpx 0;
+  text-align: center;
+}
+
+.nav-title {
+  color: #fff;
+  font-size: 32rpx;
+  font-weight: 600;
+}
+
+.tab-bar {
+  background: #fff;
+  display: flex;
+  flex-direction: row;
+  border-bottom: 1rpx solid #eee;
+}
+
+.tab-item {
+  flex: 1;
+  padding: 24rpx 0;
+  text-align: center;
+  border-bottom: 4rpx solid transparent;
+}
+
+.tab-item.active {
+  border-bottom-color: #F97316;
+}
+
+.tab-label {
+  font-size: 28rpx;
+  color: #666;
+}
+
+.tab-item.active .tab-label {
+  color: #F97316;
+  font-weight: 600;
+}
+
+.content-scroll {
+  flex: 1;
+  height: calc(100vh - 200rpx);
+}
+
+.filter-scroll {
+  padding: 20rpx 24rpx;
+  background: #fff;
+}
+
+.filter-chips {
+  display: flex;
+  flex-direction: row;
+  gap: 16rpx;
+}
+
+.filter-chip {
+  padding: 12rpx 24rpx;
+  border-radius: 16px;
+  background: #fff;
+  border: 1rpx solid #ddd;
+  flex-shrink: 0;
+}
+
+.filter-chip.active {
+  background: #FFF7ED;
+  border-color: #F97316;
+}
+
+.filter-label {
+  font-size: 24rpx;
+  color: #666;
+}
+
+.filter-chip.active .filter-label {
+  color: #F97316;
+  font-weight: 500;
+}
+
+.card-list {
+  padding: 0 24rpx 24rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+
+.activity-card {
+  background: #fff;
+  border-radius: 12rpx;
+  padding: 20rpx;
+  display: flex;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+}
+
+.card-cover {
+  width: 144rpx;
+  height: 144rpx;
+  border-radius: 8rpx;
+  flex-shrink: 0;
+  background: #f0f0f0;
+}
+
+.card-info {
+  flex: 1;
+  margin-left: 20rpx;
+  display: flex;
+  flex-direction: column;
+}
+
+.card-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  line-height: 1.4;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.card-meta {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+
+.card-bottom {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: auto;
+  padding-top: 12rpx;
+}
+
+.card-status {
+  font-size: 22rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 6rpx;
+}
+
+.status-pending {
+  background: #FFF7ED;
+  color: #F97316;
+}
+
+.status-approved {
+  background: #F0FDF4;
+  color: #22C55E;
+}
+
+.status-rejected {
+  background: #FEF2F2;
+  color: #EF4444;
+}
+
+.status-cancelled {
+  background: #F5F5F5;
+  color: #999;
+}
+
+.status-checked_in {
+  background: #DBEAFE;
+  color: #3B82F6;
+}
+
+.status-upcoming {
+  background: #FFF7ED;
+  color: #F97316;
+}
+
+.status-ended {
+  background: #F5F5F5;
+  color: #999;
+}
+
+.status-published {
+  background: #F0FDF4;
+  color: #22C55E;
+}
+
+.card-price {
+  font-size: 28rpx;
+  color: #F97316;
+  font-weight: 600;
+}
+
+.card-price.free {
+  color: #22C55E;
+}
+
+.card-actions {
+  display: flex;
+  flex-direction: row;
+  gap: 12rpx;
+  margin-top: 12rpx;
+}
+
+.btn-cancel {
+  padding: 8rpx 20rpx;
+  font-size: 22rpx;
+  color: #999;
+  background: #fff;
+  border: 1rpx solid #ddd;
+  border-radius: 20rpx;
+  line-height: 1.5;
+}
+
+.btn-checkin {
+  padding: 8rpx 20rpx;
+  font-size: 22rpx;
+  color: #fff;
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  border: none;
+  border-radius: 20rpx;
+  line-height: 1.5;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 40rpx;
+}
+
+.empty-icon {
+  font-size: 80rpx;
+  margin-bottom: 20rpx;
+}
+
+.empty-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 12rpx;
+}
+
+.empty-desc {
+  font-size: 26rpx;
+  color: #999;
+  margin-bottom: 40rpx;
+}
+
+.empty-btn {
+  padding: 16rpx 48rpx;
+  font-size: 28rpx;
+  color: #fff;
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  border: none;
+  border-radius: 40rpx;
+}
+
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 80rpx 40rpx;
+}
+
+.loading-spinner {
+  width: 60rpx;
+  height: 60rpx;
+  border: 4rpx solid #FDE68A;
+  border-top-color: #F97316;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+  margin-bottom: 20rpx;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+.loading-text {
+  font-size: 26rpx;
+  color: #999;
+}
+
+.load-more-tip {
+  text-align: center;
+  padding: 30rpx;
+}
+
+.load-more-text {
+  font-size: 24rpx;
+  color: #999;
+}
+</style>

+ 1 - 0
cfc-frontend/pages/ai/chat.vue

@@ -154,6 +154,7 @@ export default {
     this.reportId = options.reportId ? parseInt(options.reportId) : null
     this.surveyId = options.surveyId ? parseInt(options.surveyId) : null
     this.mode = options.mode || ''
+    this.fromPage = options.fromPage ? decodeURIComponent(options.fromPage) : ''
     if (options.firstMessage) {
       this.pendingFirstMessage = decodeURIComponent(options.firstMessage)
     }

+ 1 - 1
cfc-frontend/pages/body-detail/member-body-detail.vue

@@ -202,7 +202,7 @@ export default {
       if (act && act.id) uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
     },
     goMoreActivities: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=activity&dimension=body' })
+      uni.navigateTo({ url: '/pages/activity/index' })
     },
     goProductDetail: function(prod) {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })

+ 13 - 27
cfc-frontend/pages/body/index.vue

@@ -149,29 +149,13 @@
       @activityClick="goActivityDetail"
       @moreActivities="goMoreActivities" />
 
-    <!-- 推荐阅读(无条件展示) -->
-    <view class="section" v-if="bodyArticles.length > 0">
-      <view class="section-header">
-        <text class="section-title">📖 推荐阅读</text>
-        <text class="section-more" @click="goMoreArticles">更多 ›</text>
-      </view>
-      <view class="article-list">
-        <view class="article-card" v-for="article in bodyArticles" :key="article.id" @click="goArticleDetail(article.id)">
-          <view class="article-category" :style="{ background: article.categoryColor }">
-            <text class="article-category-text">{{ article.category }}</text>
-          </view>
-          <text class="article-title">{{ article.title }}</text>
-          <text class="article-summary">{{ article.summary }}</text>
-          <view class="article-meta">
-            <view class="meta-item">
-              <text class="meta-icon">&#x1F4D6;</text>
-              <text class="meta-text">{{ article.views }}</text>
-            </view>
-            <text class="article-date">{{ article.date }}</text>
-          </view>
-        </view>
-      </view>
-    </view>
+    <!-- 推荐阅读 -->
+    <DimensionArticles
+      dimensionCode="body"
+      :articles="bodyArticles"
+      :isLoggedIn="isLoggedIn"
+      @articleClick="goArticleDetail"
+      @moreArticles="goMoreArticles" />
 
     <!-- 健康小贴士 -->
     <view class="section" v-if="sectionVisible('health_tips')">
@@ -217,6 +201,7 @@ import UserQuickEntry from '../../components/UserQuickEntry.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
+import DimensionArticles from '../../components/DimensionArticles.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getVisibleFamilyMembers, getDimensionOverview, getFeaturedArticles } from '../../utils/api.js'
@@ -250,7 +235,7 @@ const healthRequest = function(url, data) {
 }
 
 export default {
-  components: { TabTransition, PageBanner, LoginGuideCard, HealthTips, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, FamilyRelationGraph, RadarChart },
+  components: { TabTransition, PageBanner, LoginGuideCard, HealthTips, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, RadarChart },
   data() {
     return {
       isLoggedIn: false,
@@ -517,15 +502,16 @@ export default {
       uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
     },
     goMoreActivities: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=activity' })
+      uni.navigateTo({ url: '/pages/activity/index' })
     },
     goProductDetail: function(prod) {
       uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/discover/index' })
+      uni.navigateTo({ url: '/pages/discover/index?type=product' })
     },
-    goArticleDetail: function(id) {
+    goArticleDetail: function(article) {
+      var id = article.id || article
       uni.navigateTo({ url: '/pages/mind-detail/article-detail?id=' + id })
     },
     goMoreArticles: function() {

+ 1 - 1
cfc-frontend/pages/body/member-body-detail.vue

@@ -202,7 +202,7 @@ export default {
       if (act && act.id) uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + act.id })
     },
     goMoreActivities: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=activity&dimension=body' })
+      uni.navigateTo({ url: '/pages/activity/index' })
     },
     goProductDetail: function(prod) {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })

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

@@ -683,7 +683,7 @@ export default {
       if (act && act.id) uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + act.id })
     },
     goMoreActivities: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=activity&dimension=child' })
+      uni.navigateTo({ url: '/pages/activity/index' })
     },
     goProductDetail: function(prod) {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })

+ 264 - 29
cfc-frontend/pages/index/index.vue

@@ -1,5 +1,34 @@
 <template>
 <view>
+  <!-- 首次进入的 splash loading 蒙板 — 覆盖首页直到数据加载完毕 -->
+  <view class="splash-overlay" :class="{'fade-out': splashOverlayFading}" v-if="splashOverlay || splashOverlayFading">
+    <view class="splash-overlay-bg">
+      <view class="splash-overlay-circle c1"></view>
+      <view class="splash-overlay-circle c2"></view>
+      <view class="splash-overlay-circle c3"></view>
+    </view>
+    <view class="splash-overlay-body">
+      <view class="splash-overlay-logo-wrap">
+        <image class="splash-overlay-logo" src="/static/logo.png" mode="aspectFit" />
+      </view>
+      <text class="splash-overlay-brand">浠艾福</text>
+      <text class="splash-overlay-en">Care Family</text>
+      <view class="splash-overlay-dims">
+        <view class="splash-overlay-dim"><text class="splash-overlay-dim-icon">🌏</text><text class="splash-overlay-dim-label">身</text></view>
+        <view class="splash-overlay-dim"><text class="splash-overlay-dim-icon">🔥</text><text class="splash-overlay-dim-label">心</text></view>
+        <view class="splash-overlay-dim"><text class="splash-overlay-dim-icon">⚡</text><text class="splash-overlay-dim-label">智</text></view>
+        <view class="splash-overlay-dim"><text class="splash-overlay-dim-icon">🌿</text><text class="splash-overlay-dim-label">行</text></view>
+        <view class="splash-overlay-dim"><text class="splash-overlay-dim-icon">💧</text><text class="splash-overlay-dim-label">富</text></view>
+      </view>
+      <text class="splash-overlay-tagline">给全家的一站式幸福提案</text>
+      <view class="splash-overlay-loading">
+        <view class="splash-overlay-dot"></view>
+        <view class="splash-overlay-dot"></view>
+        <view class="splash-overlay-dot"></view>
+      </view>
+    </view>
+  </view>
+
   <!-- Tabbar 切换过渡页 -->
   <tab-transition v-if="showTabTransition" dimCode="action" />
 
@@ -82,12 +111,18 @@
     </view>
 
     <!-- 推荐阅读(无条件展示) -->
-    <view class="section" v-if="indexArticles.length > 0">
+    <view class="section">
       <view class="section-header">
         <text class="section-title">📖 推荐阅读</text>
         <text class="section-more" @click="goArticles">更多 ›</text>
       </view>
-      <view class="article-list">
+      <view v-if="articlesLoading" class="loading-state">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <view v-else-if="indexArticles.length === 0" class="empty-state" style="padding: 40rpx 0;">
+        <text class="empty-text">暂无文章</text>
+      </view>
+      <view class="article-list" v-else>
         <view class="article-card" v-for="article in indexArticles" :key="article.id" @click="goArticleDetail(article.id)">
           <view class="article-category" :style="{ background: article.categoryColor }">
             <text class="article-category-text">{{ article.category }}</text>
@@ -194,6 +229,10 @@ export default {
       currentRole: _currentRole,
       showTabTransition: true,
 
+      // splash loading ovarlay(首次进入时展示,数据加载完关闭)
+      splashOverlay: false,
+      splashOverlayFading: false,
+
       typeList: [
         { label: '全部', value: '' },
         { label: '活动', value: 'activity' },
@@ -206,29 +245,75 @@ export default {
       products: [],
       loading: false,
       dimensionActivities: [],
-      indexArticles: []
+      indexArticles: [],
+      articlesLoading: false
     }
   },
   onLoad(options) {
     this._doInit(options)
   },
   onShow() {
-    // Tabbar 切换过渡动画:从 splash 首次进入时跳过(splash 本身就是过渡)
-    var fromSplash = uni.getStorageSync('_fromSplash')
-    if (fromSplash === '1') {
-      uni.removeStorageSync('_fromSplash')
-    } else {
-      this.showTabTransition = true
-      var self = this
-      setTimeout(function() {
-        self.showTabTransition = false
-      }, 1100)
+    // 从 splash 首次进入:展示 splash overlay,数据加载完后自动关闭
+    var showOverlay = uni.getStorageSync('_showSplashOverlay')
+    if (showOverlay === '1') {
+      uni.removeStorageSync('_showSplashOverlay')
+      this._showSplashOverlayMask()
     }
 
+    // Tabbar 切换过渡动画
+    this.showTabTransition = true
+    var self = this
+    setTimeout(function() {
+      self.showTabTransition = false
+    }, 1800)
+
     // 始终执行初始化,确保登录态变更后及时切换视图
     this._doInit()
   },
   methods: {
+    // ===== Splash overlay (首次进入蒙板) =====
+    _showSplashOverlayMask() {
+      this.splashOverlay = true
+      this._splashLoadCount = 0
+      this._splashMinPassed = false
+
+      // 最小展示 800ms,让 overlay 有足够时间呈现
+      var self = this
+      setTimeout(function() {
+        self._splashMinPassed = true
+        self._tryHideSplash()
+      }, 800)
+
+      // 最长 6 秒强制关闭,防止 API 卡死导致蒙板不消失
+      setTimeout(function() {
+        if (self.splashOverlay) {
+          self._hideSplashOverlay()
+        }
+      }, 6000)
+    },
+    _checkSplashReady() {
+      this._splashLoadCount++
+      this._tryHideSplash()
+    },
+    _tryHideSplash() {
+      if (this.splashOverlayFading) return
+      // 最小展示时间未到 → 等待
+      if (!this._splashMinPassed) return
+      // 已登录用户只需最小时间;游客需要等待 3 个 API 全部返回
+      if (this.isLoggedIn() || this._splashLoadCount >= 3) {
+        this._hideSplashOverlay()
+      }
+    },
+    _hideSplashOverlay() {
+      if (this.splashOverlayFading) return
+      this.splashOverlayFading = true
+      var self = this
+      setTimeout(function() {
+        self.splashOverlay = false
+        self.splashOverlayFading = false
+      }, 400)
+    },
+
     _doInit(options) {
       options = options || {}
       if (!this.isLoggedIn()) {
@@ -279,34 +364,45 @@ export default {
       uni.setStorageSync('currentRole', this.currentRole)
     },
     // ===== 发现页 / 未登录态方法 =====
-    loadProducts() {
+    loadProducts: function() {
       this.loading = true
       var params = { page: 1, size: 10 }
       if (this.currentType) {
         params.productType = this.currentType
       }
+      var self = this
       productList(params).then(function(res) {
-        this.loading = false
-        if (res.code === 200 && res.data) {
-          this.products = res.data.records || []
+        self.loading = false
+        if (res && res.code === 200) {
+          // res.data is the response wrapper, products are in res.data.data or res.data.records
+          var rawData = res.data && (Array.isArray(res.data) ? res.data : (res.data.data || res.data.records || []))
+          self.products = Array.isArray(rawData) ? rawData : []
         }
-      }.bind(this)).catch(function() {
-        this.loading = false
-      }.bind(this))
+        self._checkSplashReady()
+      }).catch(function() {
+        self.loading = false
+        self._checkSplashReady()
+      })
     },
     loadDimensionActivities: function() {
       var self = this
       getActivityList({ dimensionCode: '', page: 1, size: 3 }).then(function(res) {
-        if (res && res.data) {
-          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
-          self.dimensionActivities = list.slice(0, 3)
+        if (res && res.code === 200) {
+          // res.data is the response wrapper, activities are in res.data.data or res.data.records
+          var rawData = res.data && (Array.isArray(res.data) ? res.data : (res.data.data || res.data.records || []))
+          self.dimensionActivities = Array.isArray(rawData) ? rawData.slice(0, 3) : []
         }
-      }.bind(this)).catch(function() {})
+        self._checkSplashReady()
+      }).catch(function() {
+        self._checkSplashReady()
+      })
     },
     loadFeaturedArticles: function() {
       var self = this
+      this.articlesLoading = true
       getFeaturedArticles({ size: 5 }).then(function(res) {
-        if (res.code === 200 && res.data) {
+        self.articlesLoading = false
+        if (res && res.code === 200) {
           var gradientColors = [
             'linear-gradient(135deg, #10B981, #34D399)',
             'linear-gradient(135deg, #5B9BD5, #8FC5E8)',
@@ -314,11 +410,13 @@ export default {
             'linear-gradient(135deg, #F97316, #FB923C)',
             'linear-gradient(135deg, #6366F1, #818CF8)'
           ]
-          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
+          // res.data is the response wrapper, articles are in res.data.data
+          var rawList = res.data && (Array.isArray(res.data) ? res.data : (res.data.data || res.data.records || []))
+          var list = Array.isArray(rawList) ? rawList : []
           self.indexArticles = list.map(function(item, index) {
             return {
               id: item.id,
-              category: item.categoryName || '热门文章',
+              category: item.categoryName || item.category || '热门文章',
               categoryColor: gradientColors[index % gradientColors.length],
               title: item.title || '',
               summary: item.summary || '',
@@ -327,7 +425,11 @@ export default {
             }
           })
         }
-      }.bind(this)).catch(function() {})
+        self._checkSplashReady()
+      }).catch(function() {
+        self.articlesLoading = false
+        self._checkSplashReady()
+      })
     },
     goActivities: function() {
       uni.navigateTo({ url: '/pages/discover/index?type=activity' })
@@ -383,7 +485,7 @@ export default {
                   uni.showToast({ title: '加入成功', icon: 'success' })
                   // 刷新页面
                   setTimeout(() => {
-                    uni.reLaunch({ url: '/pages/index/index' })
+                    uni.switchTab({ url: '/pages/index/index' })
                   }, 1500)
                 }
               } catch (e) {
@@ -813,4 +915,137 @@ export default {
   font-size: 22rpx;
   color: #bbb;
 }
+
+/* ===== Splash Loading Overlay(首次进入蒙板) ===== */
+.splash-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 9999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(160deg, #FFF7ED 0%, #FFEDD5 50%, #FED7AA 100%);
+  overflow: hidden;
+  opacity: 1;
+  transition: opacity 0.4s ease;
+}
+.splash-overlay.fade-out {
+  opacity: 0;
+  pointer-events: none;
+}
+.splash-overlay-bg {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  pointer-events: none;
+}
+.splash-overlay-circle {
+  position: absolute;
+  border-radius: 50%;
+  opacity: 0.15;
+}
+.splash-overlay-circle.c1 {
+  width: 400rpx; height: 400rpx;
+  background: #FF8C42;
+  top: -100rpx; right: -100rpx;
+  animation: splash-float 6s ease-in-out infinite;
+}
+.splash-overlay-circle.c2 {
+  width: 300rpx; height: 300rpx;
+  background: #F59E0B;
+  bottom: 100rpx; left: -80rpx;
+  animation: splash-float 8s ease-in-out infinite reverse;
+}
+.splash-overlay-circle.c3 {
+  width: 200rpx; height: 200rpx;
+  background: #FF6B9D;
+  top: 300rpx; right: 50rpx;
+  animation: splash-float 5s ease-in-out infinite 1s;
+}
+@keyframes splash-float {
+  0%, 100% { transform: translateY(0) scale(1); }
+  50% { transform: translateY(-20rpx) scale(1.05); }
+}
+
+.splash-overlay-body {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  animation: splash-overlay-fade-in 0.8s ease forwards;
+}
+@keyframes splash-overlay-fade-in {
+  from { opacity: 0; transform: translateY(30rpx); }
+  to   { opacity: 1; transform: translateY(0); }
+}
+
+.splash-overlay-logo-wrap {
+  width: 160rpx; height: 160rpx;
+  border-radius: 40rpx;
+  background: rgba(255,255,255,0.95);
+  display: flex; align-items: center; justify-content: center;
+  margin-bottom: 24rpx;
+  box-shadow: 0 8rpx 32rpx rgba(249,115,22,0.25);
+}
+.splash-overlay-logo {
+  width: 100rpx; height: 100rpx;
+}
+
+.splash-overlay-brand {
+  font-size: 60rpx; font-weight: 700;
+  color: #7C2D12;
+  letter-spacing: 8rpx; margin-bottom: 4rpx;
+  text-shadow: 0 2rpx 12rpx rgba(0,0,0,0.1);
+}
+.splash-overlay-en {
+  font-size: 22rpx;
+  color: rgba(124,45,18,0.6);
+  letter-spacing: 4rpx; margin-bottom: 20rpx;
+}
+
+.splash-overlay-dims {
+  display: flex; flex-direction: row;
+  align-items: center; justify-content: center;
+  gap: 20rpx; margin: 24rpx 0;
+}
+.splash-overlay-dim {
+  display: flex; flex-direction: column;
+  align-items: center; gap: 6rpx;
+}
+.splash-overlay-dim-icon { font-size: 36rpx; line-height: 1; }
+.splash-overlay-dim-label { font-size: 20rpx; color: rgba(124,45,18,0.7); line-height: 1; font-weight: 500; }
+
+.splash-overlay-tagline {
+  font-size: 28rpx;
+  color: rgba(124,45,18,0.8);
+  letter-spacing: 2rpx; font-weight: 500;
+}
+
+/* Loading dots */
+.splash-overlay-loading {
+  position: absolute;
+  bottom: 120rpx;
+  left: 0; right: 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: 12rpx;
+}
+.splash-overlay-dot {
+  width: 14rpx; height: 14rpx;
+  border-radius: 50%;
+  background: rgba(249,115,22,0.6);
+  animation: splash-dot-bounce 1.4s ease-in-out infinite both;
+}
+.splash-overlay-dot:nth-child(1) { animation-delay: -0.32s; }
+.splash-overlay-dot:nth-child(2) { animation-delay: -0.16s; }
+.splash-overlay-dot:nth-child(3) { animation-delay: 0s; }
+@keyframes splash-dot-bounce {
+  0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
+  40%           { transform: scale(1);   opacity: 1; }
+}
 </style>

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

@@ -728,7 +728,7 @@ export default {
       }
     },
     goMoreActivities() {
-      uni.switchTab({ url: '/pages/discover/index' })
+      uni.navigateTo({ url: '/pages/activity/index' })
     },
     goProductDetail(prod) {
       if (prod && prod.id) {

+ 1 - 1
cfc-frontend/pages/mind-detail/member-mind-detail.vue

@@ -202,7 +202,7 @@ export default {
       if (act && act.id) uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + act.id })
     },
     goMoreActivities: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=activity&dimension=mind' })
+      uni.navigateTo({ url: '/pages/activity/index' })
     },
     goProductDetail: function(prod) {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })

+ 12 - 30
cfc-frontend/pages/mind/index.vue

@@ -156,31 +156,12 @@
     </view>
 
     <!-- 推荐阅读 -->
-    <view class="section" v-if="sectionVisible('recommended_reading')">
-      <view class="section-header">
-        <text class="section-title">推荐阅读</text>
-      </view>
-      <view class="article-list">
-        <view class="article-card" v-for="article in emotionArticles" :key="article.id" @click="goArticleDetail(article.id)">
-          <view class="article-category" :style="{ background: article.categoryColor }">
-            <text class="article-category-text">{{ article.category }}</text>
-          </view>
-          <text class="article-title">{{ article.title }}</text>
-          <text class="article-summary">{{ article.summary }}</text>
-          <view class="article-meta">
-            <view class="meta-item">
-              <text class="meta-icon">&#x1F4D6;</text>
-              <text class="meta-text">{{ article.views }}</text>
-            </view>
-            <view class="meta-item">
-              <text class="meta-icon">&#x1F44D;</text>
-              <text class="meta-text">{{ article.likes }}</text>
-            </view>
-            <text class="article-date">{{ article.date }}</text>
-          </view>
-        </view>
-      </view>
-    </view>
+    <DimensionArticles
+      dimensionCode="mind"
+      :articles="emotionArticles"
+      :isLoggedIn="isLoggedIn"
+      @articleClick="goArticleDetail"
+      @moreArticles="goArticles" />
 
     <!-- ===== 传统文化(八字/五行/星座/灵数) ===== -->
     <view class="culture-section" v-if="isLoggedIn && traditionalData">
@@ -298,13 +279,14 @@ import UserQuickEntry from '../../components/UserQuickEntry.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
+import DimensionArticles from '../../components/DimensionArticles.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import PsychCrisisBanner from '../../components/PsychCrisisBanner.vue'
 import { getVisibleSections, getFeaturedArticles, getEmiReport, getDailyTip, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getVisibleFamilyMembers, getEnergyOverview } from '../../utils/api.js'
 import config from '../../config.js'
 
 export default {
-  components: { TabTransition, PageBanner, RadarChart, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, FamilyRelationGraph, PsychCrisisBanner },
+  components: { TabTransition, PageBanner, RadarChart, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, PsychCrisisBanner },
   data() {
     return {
       isLoggedIn: false,
@@ -649,9 +631,9 @@ export default {
       }
       if (item.page) uni.navigateTo({ url: item.page })
     },
-    goArticleDetail: function(id) {
-      var url = '/pages/mind-detail/article-detail?id=' + id
-      uni.navigateTo({ url: url })
+    goArticleDetail: function(article) {
+      var id = article.id || article
+      uni.navigateTo({ url: '/pages/mind-detail/article-detail?id=' + id })
     },
     // ===== 孩子列表与维度数据 =====
     loadChildren: function() {
@@ -791,7 +773,7 @@ export default {
       if (act && act.id) uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
     },
     goMoreActivities: function() {
-      uni.navigateTo({ url: '/pages/discover/index' })
+      uni.navigateTo({ url: '/pages/activity/index' })
     },
     goProductDetail: function(prod) {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })

+ 1 - 1
cfc-frontend/pages/mind/member-mind-detail.vue

@@ -202,7 +202,7 @@ export default {
       if (act && act.id) uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + act.id })
     },
     goMoreActivities: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=activity&dimension=mind' })
+      uni.navigateTo({ url: '/pages/activity/index' })
     },
     goProductDetail: function(prod) {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })

+ 1 - 1
cfc-frontend/pages/profile/profile.vue

@@ -102,7 +102,7 @@ export default {
     var self = this
     setTimeout(function() {
       self.showTabTransition = false
-    }, 1100)
+    }, 1800)
 
     if (!uni.getStorageSync('token')) return
     this.loadMascot()

+ 5 - 59
cfc-frontend/pages/splash/splash.vue

@@ -53,13 +53,6 @@
 
       <!-- Tagline -->
       <text class="splash-tagline">给全家的一站式幸福提案</text>
-
-      <!-- Loading dots at bottom -->
-      <view class="splash-loading">
-        <view class="splash-dot splash-dot--1"></view>
-        <view class="splash-dot splash-dot--2"></view>
-        <view class="splash-dot splash-dot--3"></view>
-      </view>
     </view>
   </view>
 </template>
@@ -67,23 +60,11 @@
 <script>
 export default {
   name: 'SplashPage',
-  data() {
-    return {
-      ready: false
-    }
-  },
-  onLoad() {
-    this.ready = true
-  },
   onReady() {
-    // 标记来自 splash 页,首页首次加载跳过 tab-transition
-    uni.setStorageSync('_fromSplash', '1')
-    // 至少展示 1 秒后跳转
-    setTimeout(function() {
-      uni.switchTab({
-        url: '/pages/index/index'
-      })
-    }, 1200)
+    // 标记需要在首页显示 splash overlay(仅在首次进入时)
+    uni.setStorageSync('_showSplashOverlay', '1')
+    // 立即跳转首页,首页会展示 splash overlay 并加载数据
+    uni.switchTab({ url: '/pages/index/index' })
   }
 }
 </script>
@@ -214,41 +195,6 @@ export default {
   font-weight: 500;
 }
 
-/* ===== Loading Dots ===== */
-.splash-loading {
-  position: absolute;
-  bottom: 120rpx;
-  left: 0;
-  right: 0;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  gap: 12rpx;
-}
-
-.splash-dot {
-  width: 14rpx;
-  height: 14rpx;
-  border-radius: 50%;
-  background: rgba(249, 115, 22, 0.6);
-  animation: splash-dot-bounce 1.4s ease-in-out infinite both;
-}
-
-.splash-dot--1 { animation-delay: -0.32s; }
-.splash-dot--2 { animation-delay: -0.16s; }
-.splash-dot--3 { animation-delay: 0s; }
-
-@keyframes splash-dot-bounce {
-  0%, 80%, 100% {
-    transform: scale(0.6);
-    opacity: 0.4;
-  }
-  40% {
-    transform: scale(1);
-    opacity: 1;
-  }
-}
-
 /* ===== 五维图标行 ===== */
 .splash-dims-row {
   display: flex;
@@ -306,4 +252,4 @@ export default {
     transform: translateY(0);
   }
 }
-</style>
+</style>

+ 3 - 1
cfc-frontend/pages/wisdom-detail/member-wisdom-detail.vue

@@ -141,7 +141,9 @@ export default {
     onTaskClick: function(task) {},
     goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
     goActivityDetail: function(id) {},
-    goMoreActivities: function() {},
+    goMoreActivities: function() {
+      uni.navigateTo({ url: '/pages/activity/index' })
+    },
     goProductDetail: function(id) {
       var token = uni.getStorageSync('token')
       if (token) {

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

@@ -411,7 +411,7 @@ export default {
     goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
     goActivityDetail: function(id) {},
     goMoreActivities: function() {
-      uni.navigateTo({ url: '/pages/discover/index' })
+      uni.navigateTo({ url: '/pages/activity/index' })
     },
     goActivityDetail: function(act) {
       if (act && act.id) uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
@@ -427,7 +427,7 @@ export default {
       }
     },
     goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/discover/index' })
+      uni.navigateTo({ url: '/pages/discover/index?type=product' })
     },
     loadFeaturedArticles: function() {
       var self = this

+ 3 - 1
cfc-frontend/pages/wisdom/member-wisdom-detail.vue

@@ -141,7 +141,9 @@ export default {
     onTaskClick: function(task) {},
     goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
     goActivityDetail: function(id) {},
-    goMoreActivities: function() {},
+    goMoreActivities: function() {
+      uni.navigateTo({ url: '/pages/activity/index' })
+    },
     goProductDetail: function(id) {
       var token = uni.getStorageSync('token')
       if (token) {

+ 3 - 1
cfc-frontend/pages/wisdom/wisdom_temp/index.vue

@@ -173,7 +173,9 @@ export default {
         }
       }
     },
-    goMoreActivities() {},
+    goMoreActivities() {
+      uni.navigateTo({ url: '/pages/activity/index' })
+    },
     goActivityDetail(id) {},
     goMoreProducts() {},
     goProductDetail(id) {

+ 1 - 1
cfc-frontend/pages/wisdom/wisdom_temp/member-wisdom-detail.vue

@@ -205,7 +205,7 @@ export default {
       if (act && act.id) uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + act.id })
     },
     goMoreActivities: function() {
-      uni.navigateTo({ url: '/pages/discover/index?type=activity&dimension=wisdom' })
+      uni.navigateTo({ url: '/pages/activity/index' })
     },
     goProductDetail: function(prod) {
       if (prod && prod.id) uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })

+ 564 - 0
docs/superpowers/plans/2026-07-02-login-left-panel-redesign.md

@@ -0,0 +1,564 @@
+# Login 左侧品牌区重新设计 实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 将 cfc-web 登录页左侧品牌区从五行沙盘 SVG 重构为五维能量轮盘动画,使用五维品牌色系
+
+**Architecture:** 单文件修改(`Login.vue`),只改 template 和 scoped style,不涉及 script。保留右侧表单区域不变。
+
+**Tech Stack:** Vue 2 Options API, CSS3 Animations, SVG
+
+**设计文档:** `docs/superpowers/specs/2026-07-02-login-left-panel-redesign.md`
+
+---
+
+## 文件结构
+
+| 文件 | 操作 | 说明 |
+|------|------|------|
+| `cfc-web/src/views/Login.vue` | 修改 | 替换全部 brand-panel 模板和样式 |
+
+---
+
+### Task 1: 更新品牌区背景 + 整体布局
+
+**Files:**
+- Modify: `cfc-web/src/views/Login.vue:1-884`
+
+- [ ] **Step 1: 替换背景渐变色和布局微调**
+
+修改 `.brand-panel` 的背景和 `.brand-content` 的布局:
+
+```css
+/* 替换现有 .brand-panel 背景 (line ~413-419) */
+.brand-panel {
+  flex: 0 0 55%;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(160deg,
+    #1A0F00 0%,
+    #2D1810 30%,
+    #3D2B1F 60%,
+    #4A3020 100%);
+  overflow: hidden;
+}
+
+/* 替换 .brand-panel::before 背景纹理 (line ~423-433) */
+.brand-panel::before {
+  content: '';
+  position: absolute;
+  top: 0; left: 0; right: 0; bottom: 0;
+  background:
+    radial-gradient(ellipse 100% 40% at 50% 30%, rgba(255, 215, 0, 0.04) 0%, transparent 70%),
+    radial-gradient(ellipse 80% 60% at 80% 80%, rgba(255, 140, 66, 0.05) 0%, transparent 70%),
+    radial-gradient(circle 50% 50% at 20% 80%, rgba(99, 102, 241, 0.03) 0%, transparent 60%);
+  pointer-events: none;
+  z-index: 0;
+}
+
+/* 调整 .brand-content 布局 (line ~435-448) */
+.brand-content {
+  position: relative;
+  z-index: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  max-width: 480px;
+  width: 100%;
+  padding: 32px 36px;
+  color: #fff;
+  min-height: 100vh;
+  box-sizing: border-box;
+}
+```
+
+- [ ] **Step 2: 更新品牌头部样式**
+
+```css
+/* 替换 .brand-header (line ~451-457) */
+.brand-header {
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  margin-bottom: 6px;
+}
+
+/* .logo-img 保持不变 (line ~459-464) */
+
+/* .brand-titles 不变 (line ~466-470) */
+
+/* 替换 .brand-cn (line ~472-480) */
+.brand-cn {
+  font-size: 24px;
+  font-weight: 700;
+  letter-spacing: 4px;
+  line-height: 1.3;
+  background: linear-gradient(135deg, #FFE066 0%, #F59E0B 50%, #FFE066 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+}
+
+/* .brand-dot 不变 (line ~482-486) */
+
+/* .brand-en 不变 (line ~488-494) */
+```
+
+- [ ] **Step 3: 更新品牌主张区域样式**
+
+```css
+/* 替换 .brand-mission 相关样式 (line ~497-524) */
+.brand-mission {
+  flex-shrink: 0;
+  text-align: center;
+  margin-bottom: 8px;
+}
+
+.mission-title {
+  font-size: 20px;
+  font-weight: 700;
+  letter-spacing: 2px;
+  margin: 0 0 4px;
+  line-height: 1.4;
+}
+
+.mission-title .highlight {
+  background: linear-gradient(135deg, #FFE066, #F59E0B);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+}
+
+.mission-desc {
+  font-size: 13px;
+  opacity: 0.55;
+  letter-spacing: 1px;
+  margin: 0;
+  font-weight: 300;
+}
+```
+
+- [ ] **Step 4: 更新底部统计 + 角色标签样式**
+
+替换 `.brand-values` 部分为 `.brand-footer`:
+
+```css
+/* 替换 .brand-values (line ~608-647) */
+.brand-footer {
+  flex-shrink: 0;
+  width: 100%;
+  margin-top: 4px;
+}
+
+.stats-row {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 0;
+  margin-bottom: 10px;
+}
+
+.stat-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 0 16px;
+}
+
+.stat-num {
+  font-size: 22px;
+  font-weight: 700;
+  background: linear-gradient(135deg, #FFE066, #F59E0B);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  line-height: 1.2;
+}
+
+.stat-label {
+  font-size: 10px;
+  opacity: 0.5;
+  letter-spacing: 1px;
+  margin-top: 1px;
+}
+
+.stat-divider {
+  width: 1px;
+  height: 28px;
+  background: rgba(255, 255, 255, 0.12);
+}
+
+.role-tags {
+  display: flex;
+  gap: 10px;
+  justify-content: center;
+}
+
+.role-tag {
+  display: inline-block;
+  padding: 3px 14px;
+  font-size: 11px;
+  border: 1px solid;
+  border-radius: 20px;
+  opacity: 0.5;
+  transition: opacity 0.3s;
+  letter-spacing: 1px;
+}
+
+.role-tag:hover {
+  opacity: 0.9;
+}
+```
+
+---
+
+### Task 2: 替换模板中的五维沙盘为能量轮盘
+
+**Files:**
+- Modify: `cfc-web/src/views/Login.vue:1-293`
+
+- [ ] **Step 1: 替换 brand-panel 的 template 内容**
+
+把 template 中的 `.brand-panel` 内部(lines 4-219)全部替换为:
+
+```html
+<div class="login-page">
+  <!-- 左侧品牌展示区 — 五维能量轮盘 -->
+  <div class="brand-panel">
+    <div class="brand-content">
+
+      <!-- ===== Logo + 品牌标识 ===== -->
+      <div class="brand-header">
+        <img src="/logo.png" alt="浠艾福" class="logo-img" />
+        <div class="brand-titles">
+          <span class="brand-cn">亿图腾<span class="brand-dot">·</span>浠艾福</span>
+          <span class="brand-en">Care Family</span>
+        </div>
+      </div>
+
+      <!-- ===== 五维能量轮盘 ===== -->
+      <div class="orbit-section">
+        <div class="orbit-container">
+          <!-- 中心"家"徽章 -->
+          <div class="orbit-center-node">
+            <span class="center-text">家</span>
+          </div>
+
+          <!-- 轨道装饰环 -->
+          <svg class="orbit-ring-svg" viewBox="0 0 340 340" xmlns="http://www.w3.org/2000/svg">
+            <circle cx="170" cy="170" r="148" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="1" />
+            <circle cx="170" cy="170" r="144" fill="none" stroke="rgba(255,215,0,0.08)" stroke-width="0.5" stroke-dasharray="2,4" class="orbital-dash" />
+
+            <!-- 相生弧线 (身→智→富→行→心→身) -->
+            <defs>
+              <marker id="orbitArrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
+                <polygon points="0,0 10,3.5 0,7" fill="#FFD700" opacity="0.6"/>
+              </marker>
+            </defs>
+
+            <!-- 五边形弧线连接 -->
+            <path d="M 170,26 A 144,144 0 0,1 293,95" fill="none" stroke="#FFD700" stroke-width="1.5" stroke-opacity="0.35" class="arc-gen" marker-end="url(#orbitArrow)" />
+            <path d="M 293,95 A 144,144 0 0,1 256,293" fill="none" stroke="#FFD700" stroke-width="1.5" stroke-opacity="0.35" class="arc-gen" marker-end="url(#orbitArrow)" />
+            <path d="M 256,293 A 144,144 0 0,1 84,293" fill="none" stroke="#FFD700" stroke-width="1.5" stroke-opacity="0.35" class="arc-gen" marker-end="url(#orbitArrow)" />
+            <path d="M 84,293 A 144,144 0 0,1 47,95" fill="none" stroke="#FFD700" stroke-width="1.5" stroke-opacity="0.35" class="arc-gen" marker-end="url(#orbitArrow)" />
+            <path d="M 47,95 A 144,144 0 0,1 170,26" fill="none" stroke="#FFD700" stroke-width="1.5" stroke-opacity="0.35" class="arc-gen" marker-end="url(#orbitArrow)" />
+
+            <!-- 轨道粒子 -->
+            <circle cx="170" cy="22" r="2" fill="#FFE066" opacity="0.5" class="orbit-particle" />
+            <circle cx="297" cy="92" r="2" fill="#FFE066" opacity="0.5" class="orbit-particle" />
+            <circle cx="262" cy="298" r="2" fill="#FFE066" opacity="0.5" class="orbit-particle" />
+            <circle cx="78" cy="298" r="2" fill="#FFE066" opacity="0.5" class="orbit-particle" />
+            <circle cx="43" cy="92" r="2" fill="#FFE066" opacity="0.5" class="orbit-particle" />
+          </svg>
+
+          <!-- 五维节点 (绝对定位公转) -->
+          <!-- 身(橙) -->
+          <div class="orbit-node" style="--node-color:#FF8C42; --orbit-angle:0deg; --orbit-delay:0s;">
+            <span class="node-icon">🌏</span>
+            <span class="node-label">身</span>
+          </div>
+          <!-- 智(紫) -->
+          <div class="orbit-node" style="--node-color:#6366F1; --orbit-angle:72deg; --orbit-delay:-3s;">
+            <span class="node-icon">⚔️</span>
+            <span class="node-label">智</span>
+          </div>
+          <!-- 富(金) -->
+          <div class="orbit-node" style="--node-color:#F59E0B; --orbit-angle:144deg; --orbit-delay:-6s;">
+            <span class="node-icon">💧</span>
+            <span class="node-label">富</span>
+          </div>
+          <!-- 行(绿) -->
+          <div class="orbit-node" style="--node-color:#10B981; --orbit-angle:216deg; --orbit-delay:-9s;">
+            <span class="node-icon">🌿</span>
+            <span class="node-label">行</span>
+          </div>
+          <!-- 心(粉) -->
+          <div class="orbit-node" style="--node-color:#FF6B9D; --orbit-angle:288deg; --orbit-delay:-12s;">
+            <span class="node-icon">🔥</span>
+            <span class="node-label">心</span>
+          </div>
+
+          <!-- 图例 -->
+          <div class="orbit-legend">
+            <span class="legend-item">
+              <svg width="20" height="6" viewBox="0 0 20 6"><line x1="0" y1="3" x2="14" y2="3" stroke="#FFD700" stroke-width="1.5"/><polygon points="14,3 18,1 18,5" fill="#FFD700"/></svg>
+              <span>相生</span>
+            </span>
+          </div>
+        </div>
+      </div>
+
+      <!-- ===== 核心主张 ===== -->
+      <div class="brand-mission">
+        <h1 class="mission-title">给全家的<span class="highlight">一站式幸福提案</span></h1>
+        <p class="mission-desc">
+          AI 智慧家庭成长平台 · 五维能量驱动全家持续成长
+        </p>
+      </div>
+
+      <!-- ===== 数据 + 角色标签 ===== -->
+      <div class="brand-footer">
+        <div class="stats-row">
+          <div class="stat-item">
+            <span class="stat-num">120,000+</span>
+            <span class="stat-label">家庭信赖</span>
+          </div>
+          <div class="stat-divider"></div>
+          <div class="stat-item">
+            <span class="stat-num">1,000+</span>
+            <span class="stat-label">专业人员</span>
+          </div>
+          <div class="stat-divider"></div>
+          <div class="stat-item">
+            <span class="stat-num">5</span>
+            <span class="stat-label">幸福维度</span>
+          </div>
+        </div>
+        <div class="role-tags">
+          <span class="role-tag" style="border-color:#6366F1;color:#818CF8">家庭客户</span>
+          <span class="role-tag" style="border-color:#FF6B9D;color:#FF8CB3">服务商</span>
+          <span class="role-tag" style="border-color:#10B981;color:#34D399">平台运营</span>
+        </div>
+      </div>
+
+    </div>
+  </div>
+
+  <!-- 右侧登录表单区 (不变) -->
+  ...
+```
+
+---
+
+### Task 3: 添加五维能量轮盘 CSS 动画
+
+**Files:**
+- Modify: `cfc-web/src/views/Login.vue:526-607` (样式区域)
+
+- [ ] **Step 1: 替换 sand-table 相关样式为 orbit 样式**
+
+删除旧沙盘相关CSS(lines 527-606),替换为:
+
+```css
+/* ==================== 五维能量轮盘 ==================== */
+.orbit-section {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  min-height: 0;
+  margin: -6px 0;
+}
+
+.orbit-container {
+  position: relative;
+  width: 300px;
+  height: 300px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* 轨道SVG装饰 */
+.orbit-ring-svg {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 1;
+  overflow: visible;
+}
+
+/* 虚线轨道动画 */
+.orbital-dash {
+  animation: rotateDash 20s linear infinite;
+  transform-origin: 170px 170px;
+}
+
+@keyframes rotateDash {
+  0%   { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+/* 相生弧线流光 */
+.arc-gen {
+  stroke-dasharray: 8 6;
+  animation: flowArc 1.2s linear infinite;
+}
+
+@keyframes flowArc {
+  0%   { stroke-dashoffset: 0; }
+  100% { stroke-dashoffset: -14; }
+}
+
+/* 轨道粒子公转 */
+.orbit-particle {
+  animation: orbitParticle 8s linear infinite;
+  transform-origin: 170px 170px;
+}
+
+@keyframes orbitParticle {
+  0%   { opacity: 0;   transform: rotate(0deg)   translateX(148px) rotate(0deg); }
+  12%  { opacity: 0.6; }
+  88%  { opacity: 0.6; }
+  100% { opacity: 0;   transform: rotate(360deg) translateX(148px) rotate(-360deg); }
+}
+
+/* 中心节点 */
+.orbit-center-node {
+  position: relative;
+  z-index: 3;
+  width: 68px;
+  height: 68px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #FFD700, #F59E0B);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow:
+    0 0 20px rgba(245, 158, 11, 0.3),
+    0 0 60px rgba(245, 158, 11, 0.1);
+  animation: pulseCenter 3s ease-in-out infinite;
+}
+
+.center-text {
+  font-size: 24px;
+  font-weight: 700;
+  color: #fff;
+  letter-spacing: 2px;
+  text-shadow: 0 1px 4px rgba(0,0,0,0.15);
+}
+
+@keyframes pulseCenter {
+  0%, 100% { box-shadow: 0 0 20px rgba(245, 158, 11, 0.3), 0 0 60px rgba(245, 158, 11, 0.1); transform: scale(1); }
+  50%      { box-shadow: 0 0 30px rgba(245, 158, 11, 0.5), 0 0 80px rgba(245, 158, 11, 0.2); transform: scale(1.05); }
+}
+
+/* 五维节点公转 */
+.orbit-node {
+  position: absolute;
+  z-index: 2;
+  width: 50px;
+  height: 50px;
+  border-radius: 50%;
+  background: var(--node-color);
+  border: 2px solid rgba(255, 255, 255, 0.2);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 0 12px rgba(0,0,0,0.15), inset 0 1px 2px rgba(255,255,255,0.15);
+  animation: orbitRotate 15s linear infinite;
+  animation-delay: var(--orbit-delay);
+  left: calc(50% - 25px);
+  top: calc(50% - 25px);
+  transform-origin: center;
+}
+
+/* 轨道公转 */
+@keyframes orbitRotate {
+  0%   { transform: rotate(var(--orbit-angle)) translateX(125px) rotate(calc(-1 * var(--orbit-angle))); }
+  100% { transform: rotate(calc(var(--orbit-angle) + 360deg)) translateX(125px) rotate(calc(-1 * var(--orbit-angle) - 360deg)); }
+}
+
+.node-icon {
+  font-size: 14px;
+  line-height: 1;
+  margin-bottom: 1px;
+}
+
+.node-label {
+  font-size: 11px;
+  font-weight: 700;
+  color: #fff;
+  line-height: 1;
+  text-shadow: 0 1px 2px rgba(0,0,0,0.1);
+}
+
+/* 图例 */
+.orbit-legend {
+  position: absolute;
+  bottom: -10px;
+  left: 50%;
+  transform: translateX(-50%);
+  z-index: 2;
+}
+
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 9px;
+  opacity: 0.4;
+  color: #fff;
+  letter-spacing: 1px;
+}
+
+.legend-item svg {
+  display: block;
+}
+```
+
+- [ ] **Step 2: 移除旧的沙盘相关 CSS**
+
+删除以下不再使用的 CSS 规则(找到并移除):
+- `.sand-table-wrap` 及其所有子规则
+- `.brand-values` 及其所有子规则
+- `.value-item`, `.value-icon`, `.value-label`, `.value-arrow`, `.value-subtitle`
+- `.sand-table-svg` 相关动画
+- 删除 `@keyframes` 中不再使用的:`nodeBreathe`(保留旧名但不再引用也可删除)
+
+- [ ] **Step 3: 更新响应式 CSS 微调**
+
+在 `@media screen and (max-width: 900px)` 中,确保 `.orbit-section` 在隐藏时随 `.brand-panel` 一起隐藏(已有 `.brand-panel { display: none; }` 处理了)。
+
+---
+
+### Task 4: 验证
+
+**Files:**
+- Verify: `cfc-web/src/views/Login.vue`
+
+- [ ] **Step 1: LSP 诊断检查**
+
+Run: LSP diagnostics on Login.vue
+Expected: 无错误,无警告
+
+- [ ] **Step 2: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-web && npx vue-cli-service build --mode production 2>&1 | tail -20
+```
+
+Expected: Build 成功,无错误
+
+- [ ] **Step 3: 最终清理**
+
+确认已删除所有旧的沙盘 SVG 标记和不再使用的 CSS 规则。
+确认右侧表单区域未受影响。

+ 184 - 0
docs/superpowers/specs/2026-07-02-login-left-panel-redesign.md

@@ -0,0 +1,184 @@
+# 登录页左侧品牌区重新设计
+
+**日期:** 2026-07-02
+**状态:** 设计已确认
+
+## 概述
+
+重新设计 cfc-web 管理端登录页(Login.vue)的左侧品牌展示区。以「五维能量轮盘」为核心视觉元素,采用五维品牌色系,替换当前的五行沙盘 SVG。
+
+## 设计方向
+
+- **参考来源:** `web/` 企业官网的视觉语言(dimension-orbit 动画、卡片化风格、色彩运用)
+- **色彩体系:** 五维品牌色(身橙/智紫/富金/行绿/心粉)
+- **整体风格:** 热忱现代,品牌感强,动画流畅
+
+## 布局结构
+
+```
+┌──────────────────────────────────┐
+│ 品牌 Logo + 标题                  │  顶部固定
+├──────────────────────────────────┤
+│                                  │
+│  五维能量轮盘 (主视觉元素)          │  弹性居中
+│  - 中心: "家" 金色徽章            │  max 300×300px
+│  - 五维节点公转 (72°等分)          │
+│  - 相生连接线 + 流光动画           │
+│                                  │
+├──────────────────────────────────┤
+│  "给全家的 一站式幸福提案"          │  品牌主张
+│  "AI 智慧家庭成长平台..."          │
+├──────────────────────────────────┤
+│  统计: 120,000+ / 1,000+ / 5     │  底部信息
+│  角色标签: 家庭客户/服务商/平台运营  │
+└──────────────────────────────────┘
+```
+
+## 配色方案
+
+### 背景渐变
+
+```
+#1A0F00 (#top)
+  ↓  径向光晕 + 极淡暖金粒子
+#2D1810 (#mid)
+  ↓
+#3D2B1F (#bottom)
+```
+
+### 五维色系
+
+| 维度 | 颜色 | HEX | 用途 |
+|:----:|:----:|:----:|------|
+| 身(土) | 橙 | `#FF8C42` | 节点1 + 标签边框 |
+| 智(金) | 紫 | `#6366F1` | 节点2 + 标签边框 |
+| 富(水) | 金 | `#F59E0B` | 节点3 + 标签边框 |
+| 行(木) | 绿 | `#10B981` | 节点4 + 标签边框 |
+| 心(火) | 粉 | `#FF6B9D` | 节点5 + 标签边框 |
+
+### 文字色
+
+| 用途 | 样式 |
+|------|------|
+| 品牌名(中文) | 金色渐变 `#FFE066 → #F59E0B` |
+| 品牌名(英文) | 白色 opacity 0.5 |
+| 核心标题 | 金色渐变 `#FFE066 → #F59E0B` |
+| 副标题 | 白色 opacity 0.55 |
+| 统计数字 | 金色渐变 |
+| 统计标签 | 白色 opacity 0.5 |
+| 角色标签 | 对应维度色,opacity 0.5 |
+
+## 组件结构
+
+### 1. 品牌头部(brand-header)
+
+```
+  [logo.png]  亿图腾·浠艾福
+               Care Family
+```
+
+- 与当前布局一致,微调字距
+- `margin-bottom: 12px`
+
+### 2. 五维能量轮盘(dimension-orbit-section)
+
+容器:`300×300px`,`position: relative`,`margin: 0 auto`
+
+#### 中心节点(center-node)
+- 72×72px 正圆
+- 背景:`linear-gradient(135deg, #FFD700, #F59E0B)`
+- 文字:"家" 白色 24px 700
+- 动画:`pulseCenter` 3s ease-in-out infinite
+
+#### 轨道节点(orbit-nodes)
+5 个节点通过 CSS `transform: rotate(Ndeg) translateX(Rpx)` 实现公转:
+
+| 节点 | 初始角度 | 颜色 | 内容 |
+|:----:|:--------:|:----:|:----:|
+| 身 | 0° | `#FF8C42` | 🌏 + 身 |
+| 智 | 72° | `#6366F1` | ⚔️ + 智 |
+| 富 | 144° | `#F59E0B` | 💧 + 富 |
+| 行 | 216° | `#10B981` | 🌿 + 行 |
+| 心 | 288° | `#FF6B9D` | 🔥 + 心 |
+
+- 节点尺寸:52×52px 正圆
+- 边框:2px solid color,微弱内阴影
+- 文字颜色:白色
+- 动画:`orbit 15s linear infinite`
+- 通过 `animation-delay` 错开 5 个节点的起始相位(每个延迟 0s, 3s, 6s, 9s, 12s)
+
+#### 轨道 && 连接线
+- 外层装饰轨道:半径 130px,细线 `stroke: rgba(255,255,255,0.06)`,虚线点缀
+- 相生弧线:连接相邻维度节点,组成五边形
+  - 使用 SVG `<path>` 绘制弧线
+  - 颜色:`#FFD700` 透明度 0.6
+  - 动画:流光效果 `flowArc 1.2s linear infinite`
+- 相克线(简化):用较淡的细虚线从中心向外辐射,或省略
+- 图例:底部中央显示"相生" + "相克" 的小图例
+
+#### 粒子装饰
+- 5 颗微小粒子沿轨道外缘缓慢公转
+- 尺寸 2px,金色
+- 动画:`orbitParticle 8s linear infinite`
+
+### 3. 品牌主张(brand-mission)
+
+```
+h1: 给全家的 <金色高亮>一站式幸福提案</金色高亮>
+p:  AI 智慧家庭成长平台 · 五维能量驱动全家持续成长
+```
+
+- 标题 20px 700
+- 描述 13px 300
+
+### 4. 底部信息(brand-footer)
+
+#### 统计行
+```
+120,000+  |  1,000+  |  5
+家庭信赖   |  专业人员 |  幸福维度
+```
+
+- 数字 22px 700 金色渐变
+- 标签 10px opacity 0.5
+- 竖线分隔 1px 白色 opacity 0.1, 高 28px
+
+#### 角色标签
+```
+[家庭客户]  [服务商]  [平台运营]
+```
+
+- 圆角 20px,字号 11px
+- 边框色 + 文字色分别对应五维色系中最接近的三种颜色
+- 家庭客户 → 智(紫) `#6366F1`
+- 服务商 → 心(粉) `#FF6B9D`
+- 平台运营 → 行(绿) `#10B981`
+
+## 动画清单
+
+| 动画 | 目标 | 周期 | 说明 |
+|------|------|:----:|------|
+| `pulseCenter` | 中心"家"节点 | 3s | 呼吸缩放 + 发光强弱变化 |
+| `nodeOrbit` | 五维节点 | 15s | 围绕中心公转(匀速) |
+| `flowArc` | 相生弧线 | 1.2s | 虚线偏移产生流动感 |
+| `pulseLine` | 相克线 | 2.2s | 透明度脉冲 |
+| `orbitParticle` | 轨道粒子 | 8s | 沿轨道公转 |
+| `nodeBreathe` | 节点自身 | 3.2s | 节点呼吸放大效果 |
+
+## 响应式
+
+| 断点 | 行为 |
+|:----:|------|
+| >900px | 完整双栏布局,左侧 55% |
+| ≤900px | 隐藏左侧品牌区,表单全屏(与当前一致) |
+
+## 重用说明
+
+此设计仅修改 `Login.vue` 中的 `.brand-panel` 相关模板和样式。
+- 不修改右侧 `.form-panel` 区域
+- 不修改 Script 部分
+- 不修改组件结构(SliderCaptcha 等)
+
+## 修改文件
+
+- `cfc-web/src/views/Login.vue` — 模板(<template> 中左侧 brand-panel 区域)+ 样式(<style> scoped 中 brand-panel 相关样式)

+ 174 - 0
docs/superpowers/specs/2026-07-03-article-center-design.md

@@ -0,0 +1,174 @@
+# 小程序文章主页设计方案
+
+**日期**:2026-07-03
+**状态**:已确认
+
+---
+
+## 一、页面入口与导航
+
+**路径**:`/pages/article-center/index`(子包新建)
+
+**入口**:
+- 发现页"更多 ›" → `/pages/article-center/index`(修复现有 bug)
+- 个人中心 ProfileMenu → 新增"成长文章"菜单入口
+
+**Tab 导航**(顶部横向滚动):
+```
+全部推荐 | 身 | 智 | 心 | 行 | 富
+```
+
+- **全部推荐** = 个性化流,按当前访问用户的综合画像(年龄、发育阶段、兴趣等)排序
+- **五维 Tab** = 该维度的文章分类列表
+
+**浮动按钮**:右下角 📝 写成长记录
+
+---
+
+## 二、文章列表
+
+**卡片结构**:
+- 封面图 16:9
+- 分类标签 + 阅读时长
+- 标题(最多2行)
+- 摘要(最多2行)
+- 作者 + 日期 + 收藏数
+- 底部:五维能量彩条(颜色对应维度)
+
+**列表交互**:
+- 下拉刷新、上拉加载(分页)
+- 点击 → 进入文章详情
+
+**列表状态**:骨架屏 / 空状态插画 / 加载失败重试
+
+---
+
+## 三、文章详情
+
+**路径**:`/pages/article-center/article-detail`(子包新建)
+
+**页面结构**:
+- 封面图(widthFix)
+- 分类 · 阅读时长
+- 标题
+- 作者 · 日期
+- 五维权重彩条
+- 正文(rich-text)
+- 底部悬浮:阅读完成按钮
+
+**阅读完成流程**:
+1. 用户点击/滚动到底部 → 显示 loading "AI 正在根据你的情况出题..."
+2. 后端根据**文章内容 + 用户综合画像**生成 2 道题
+3. 展示题目 → 用户提交答案
+4. 计分成功 → toast "获得 N 积分"
+5. 生成阅读记录,推送至家长/孩子报告
+
+---
+
+## 四、轻量发布 — 写成长记录
+
+**入口**:右下角浮动按钮
+
+**弹出页面**:`/pages/article-center/article-edit`(新页面)
+
+**字段**:
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| 标题 | 是 | 最多 50 字 |
+| 封面图 | 否 | 上传或拍照 |
+| 正文 | 是 | 富文本,最多 2000 字 |
+| 可见范围 | 否 | 默认"仅家庭";可选"公开" |
+| 孩子标签 | 否 | 家长写孩子动态时绑定 |
+
+**维度补充**:关联维度可选;不选则后端自动分析内容补充
+
+**草稿**:自动本地缓存草稿,退出页面提示"是否保存草稿"
+
+**发布状态**:
+- **仅家庭**:直接发布,无需审核,立即对家庭可见
+- **公开**:提交后进审核队列,通过后全量展示
+
+---
+
+## 五、发布记录
+
+**路径**:`/pages/article-center/my-posts`(新页面)
+
+**列表展示**:标题 + 封面图 + 发布时间 + 状态标签
+
+**状态**:
+- 草稿(未发布)
+- 审核中(公开文章)
+- 已发布
+- 已拒绝(含拒绝原因)
+
+**右上角入口**:发表页"历史"按钮
+
+---
+
+## 六、积分与能量激励
+
+**发布积分**:
+| 类型 | 积分 |
+|------|------|
+| 家庭内部可见发布并通过 | 10 分 |
+| 公开文章发布并通过审核 | 30 分 |
+| 被点赞/评论 | +2 分/次 |
+
+**阅读积分**:
+- AI 出题回答完成:+5 分
+
+**能量分配**:
+- 按文章关联维度的能量比计算(非均分)
+- 假设文章维度权重为 [身:20%, 智:30%, 富:10%, 行:15%, 心:25%],总能量 +10
+- 各维度能量 = 10 × 各维度百分比
+
+---
+
+## 七、消息通知
+
+- 管理员审核通过 → 消息通知作者
+- 被点赞/评论 → 消息通知
+- AI 出题完成 → 静默
+
+---
+
+## 八、孩子端 vs 家长端
+
+- **孩子访问**:内容按年龄过滤 + 行/富维度内容隐藏
+- **家长访问**:全量内容 + 商业内容
+- 孩子读完后 → 家长可在报告里看到阅读记录和 AI 答题情况
+
+---
+
+## 九、需要后端新增接口
+
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| `/api/articles/ai-questions` | POST | 根据文章内容+用户画像生成2道题 |
+| `/api/articles/submit-answers` | POST | 提交阅读答题,更新积分 |
+| `/api/articles/publish` | POST | 发布成长记录(家庭/公开) |
+| `/api/articles/my-posts` | POST | 我的发布列表 |
+| `/api/articles/approve` | POST | 管理员审核(web端) |
+
+---
+
+## 十、文件清单
+
+| 文件 | 说明 |
+|------|------|
+| `pages/article-center/index.vue` | 文章主页(列表+Tab) |
+| `pages/article-center/article-detail.vue` | 文章详情 |
+| `pages/article-center/article-edit.vue` | 写成长记录 |
+| `pages/article-center/my-posts.vue` | 我的发布 |
+| `pages.json` | 注册以上页面 |
+
+---
+
+## 十一、依赖现有能力
+
+- 现有 `pages/mind-detail/articles.vue` 的列表 UI 模式
+- 现有 `DimensionWeightPicker.vue` 组件
+- 现有五维能量 API
+- 现有 Dify AI 集成
+- 现有积分/能量系统