Browse Source

refactor: 删除成长计划页面并合并推荐商品与活动为统一信息流

1. 删除成长计划相关页面 (6个 Vue 文件):
   - pages/parent/plans/index.vue, detail.vue
   - pages/guide/plans/index.vue, detail.vue
   - pages/guide/training-plans/index.vue, detail.vue

2. pages.json 中移除对应的路由注册

3. 新增 components/RecommendedFeed.vue:
   - 将商品和活动混合为交替排列的信息流 (商品/活动/商品/活动...)
   - 统一展示:封面图 + 类型标签 + 标题 + 价格 + 状态
   - 点击统一跳转到对应详情页

4. parent-index.vue:
   - 替换 DimensionActivities + DimensionProducts 为 RecommendedFeed
   - 新增 onFeedItemClick 和 goMoreDiscover 方法

5. child-index.vue:
   - 替换 DimensionActivities + DimensionProducts 为 RecommendedFeed
   - 新增 onFeedItemClick 和 goMoreDiscover 方法

6. member-detail.vue:
   - 替换 DimensionActivities + DimensionProducts 为 RecommendedFeed
   - 新增 onFeedItemClick 和 goMoreDiscover 方法
openhands 2 months ago
parent
commit
6ceeff6bee

+ 265 - 0
cfc-frontend/components/RecommendedFeed.vue

@@ -0,0 +1,265 @@
+<template>
+  <view class="section">
+    <view class="section-header">
+      <text class="section-title">🔥 发现好物</text>
+      <text class="section-more" @click="$emit('more')">更多 ›</text>
+    </view>
+    <view class="feed-list">
+      <!-- 无数据时显示占位 -->
+      <template v-if="(!items || items.length === 0) && !loading">
+        <view class="feed-card feed-placeholder" v-for="i in 4" :key="'ph-' + i">
+          <view class="feed-img-placeholder"></view>
+          <view class="feed-info">
+            <text class="feed-title-placeholder line-clamp-2">精彩内容即将上线</text>
+            <text class="feed-sub-placeholder">--</text>
+          </view>
+        </view>
+      </template>
+      <!-- 混合信息流 -->
+      <template v-else>
+        <view
+          class="feed-card"
+          v-for="item in displayItems"
+          :key="item.id + '-' + item.type"
+          @click="$emit('itemClick', item)"
+        >
+          <!-- 商品卡片 -->
+          <template v-if="item.type === 'product'">
+            <image class="feed-img" :src="item.coverImage || '/static/default-product.png'" mode="aspectFill" />
+            <view class="feed-info">
+              <view class="feed-tag-row">
+                <text class="feed-type-badge product-badge">🛍️ 商品</text>
+              </view>
+              <text class="feed-title line-clamp-2">{{ item.name }}</text>
+              <text class="feed-price">{{ formatPrice(item) }}</text>
+            </view>
+          </template>
+          <!-- 活动卡片 -->
+          <template v-else-if="item.type === 'activity'">
+            <image class="feed-img" :src="item.coverImage || '/static/default-activity.png'" mode="aspectFill" />
+            <view class="feed-info">
+              <view class="feed-tag-row">
+                <text class="feed-type-badge activity-badge">🔥 活动</text>
+                <text v-if="item.statusText" class="feed-status-badge" :class="item.statusClass">{{ item.statusText }}</text>
+              </view>
+              <text class="feed-title line-clamp-2">{{ item.title }}</text>
+              <view class="feed-meta-row">
+                <text class="feed-price" v-if="item.priceLabel">{{ item.priceLabel }}</text>
+                <text class="feed-price" v-else-if="item.price && item.price > 0">{{ formatPrice(item) }}</text>
+                <text class="feed-price fee-free" v-else>免费</text>
+                <text class="feed-date" v-if="item.startTime">{{ item.startTime }}</text>
+              </view>
+            </view>
+          </template>
+        </view>
+      </template>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  props: {
+    products: { type: Array, default: function() { return [] } },
+    activities: { type: Array, default: function() { return [] } },
+    maxItems: { type: Number, default: 6 },
+    loading: { type: Boolean, default: false }
+  },
+  computed: {
+    displayItems: function() {
+      var productItems = (this.products || []).slice(0, 3).map(function(p) {
+        return {
+          id: p.id,
+          type: 'product',
+          name: p.name,
+          coverImage: p.coverImage,
+          price: p.price,
+          priceLabel: p.priceLabel,
+          memberPrice: p.memberPrice,
+          source: p
+        }
+      })
+      var activityItems = (this.activities || []).slice(0, 3).map(function(a) {
+        return {
+          id: a.id,
+          type: 'activity',
+          title: a.title,
+          coverImage: a.coverImage,
+          price: a.price,
+          priceLabel: a.priceLabel,
+          startTime: a.startTime,
+          statusText: this.getStatusText(a.status),
+          statusClass: this.getStatusClass(a.status),
+          source: a
+        }
+      }.bind(this))
+      // 交替合并:商品、活动、商品、活动...
+      var merged = []
+      var pi = 0, ai = 0
+      var toggle = 0
+      while ((pi < productItems.length || ai < activityItems.length) && merged.length < this.maxItems) {
+        if (toggle % 2 === 0 && pi < productItems.length) {
+          merged.push(productItems[pi++])
+        } else if (ai < activityItems.length) {
+          merged.push(activityItems[ai++])
+        } else if (pi < productItems.length) {
+          merged.push(productItems[pi++])
+        }
+        toggle++
+      }
+      return merged
+    }
+  },
+  methods: {
+    formatPrice: function(item) {
+      var price = item.memberPrice || item.price
+      if (price == null || price === 0) return '免费'
+      return '¥' + (Number(price) / 100).toFixed(0)
+    },
+    getStatusText: function(status) {
+      var map = { upcoming: '即将开始', ongoing: '进行中', ended: '已结束', cancelled: '已取消' }
+      return map[status] || '即将开始'
+    },
+    getStatusClass: function(status) {
+      var map = { upcoming: 'status-upcoming', ongoing: 'status-ongoing', ended: 'status-ended' }
+      return map[status] || ''
+    }
+  }
+}
+</script>
+
+<style scoped>
+.section {
+  margin: 20rpx 20rpx;
+}
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+}
+.section-more {
+  font-size: 24rpx;
+  color: #999;
+}
+.feed-list {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+.feed-card {
+  width: calc(50% - 8rpx);
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 16rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+  box-sizing: border-box;
+}
+.feed-card:active {
+  opacity: 0.8;
+}
+.feed-img {
+  width: 100%;
+  height: 200rpx;
+  border-radius: 12rpx;
+  background: #f0f0f0;
+  display: block;
+  margin-bottom: 12rpx;
+}
+.feed-img-placeholder {
+  width: 100%;
+  height: 200rpx;
+  border-radius: 12rpx;
+  background: linear-gradient(135deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
+  background-size: 200% 100%;
+  animation: shimmer 1.5s infinite;
+  margin-bottom: 12rpx;
+}
+@keyframes shimmer {
+  0% { background-position: 200% 0; }
+  100% { background-position: -200% 0; }
+}
+.feed-info {
+  display: flex;
+  flex-direction: column;
+}
+.feed-tag-row {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+  margin-bottom: 6rpx;
+}
+.feed-type-badge {
+  font-size: 20rpx;
+  padding: 2rpx 8rpx;
+  border-radius: 6rpx;
+  font-weight: 500;
+}
+.product-badge {
+  background: #FFF0E6;
+  color: #F97316;
+}
+.activity-badge {
+  background: #FEF2E6;
+  color: #EA580C;
+}
+.feed-status-badge {
+  font-size: 18rpx;
+  padding: 2rpx 8rpx;
+  border-radius: 6rpx;
+}
+.status-upcoming { background: #DBEAFE; color: #2563EB; }
+.status-ongoing { background: #D1FAE5; color: #16A34A; }
+.status-ended { background: #F3F4F6; color: #9CA3AF; }
+.feed-title {
+  font-size: 26rpx;
+  color: #333;
+  font-weight: 500;
+  display: block;
+  margin-bottom: 6rpx;
+  min-height: 36rpx;
+  line-height: 1.4;
+}
+.feed-title-placeholder {
+  font-size: 26rpx;
+  color: #ccc;
+  display: block;
+  margin-bottom: 6rpx;
+  min-height: 36rpx;
+}
+.feed-price {
+  font-size: 24rpx;
+  color: #F97316;
+  font-weight: 600;
+}
+.fee-free {
+  color: #22C55E;
+}
+.feed-meta-row {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  margin-top: 4rpx;
+}
+.feed-date {
+  font-size: 20rpx;
+  color: #bbb;
+}
+.feed-sub-placeholder {
+  font-size: 24rpx;
+  color: #ccc;
+}
+.line-clamp-2 {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+</style>

+ 20 - 68
cfc-frontend/pages.json

@@ -291,18 +291,6 @@
             "navigationBarTitleText": "下级成长规划师"
           }
         },
-        {
-          "path": "training-plans/index",
-          "style": {
-            "navigationBarTitleText": "训练方案"
-          }
-        },
-        {
-          "path": "training-plans/detail",
-          "style": {
-            "navigationBarTitleText": "方案详情"
-          }
-        },
         {
           "path": "activities/index",
           "style": {
@@ -314,18 +302,6 @@
           "style": {
             "navigationBarTitleText": "活动详情"
           }
-        },
-        {
-          "path": "plans/index",
-          "style": {
-            "navigationBarTitleText": "测评方案审核"
-          }
-        },
-        {
-          "path": "plans/detail",
-          "style": {
-            "navigationBarTitleText": "方案详情"
-          }
         }
       ]
     },
@@ -334,50 +310,26 @@
       "pages": [
         {
           "path": "market/index",
-          "style": {
-            "navigationBarTitleText": "任务模板市场"
-          }
-        },
-        {
-          "path": "market/detail",
-          "style": {
-            "navigationBarTitleText": "任务模板详情"
-          }
-        },
-        {
-          "path": "plans/index",
-          "style": {
-            "navigationBarTitleText": "我的计划"
-          }
-        },
-        {
-          "path": "plans/detail",
-          "style": {
-            "navigationBarTitleText": "计划详情"
-          }
-        },
-        {
-          "path": "wishes/index",
-          "style": {
-            "navigationBarTitleText": "心愿管理"
-          }
-        },
-        {
-          "path": "child-detail",
-          "style": {
-            "navigationBarTitleText": "孩子详情"
-          }
-        },
-        {
-          "path": "invite/index",
-          "style": {
-            "navigationBarTitleText": "邀请加入"
-          }
-        }
-      ]
-    },
-    {
-      "root": "pages/child",
+              "style": {
+                "navigationBarTitleText": "任务模板市场"
+              }
+            },
+            {
+              "path": "market/detail",
+              "style": {
+                "navigationBarTitleText": "任务模板详情"
+              }
+            },
+            {
+              "path": "child-detail",
+              "style": {
+                "navigationBarTitleText": "孩子详情"
+              }
+            }
+          ]
+        },
+        {
+          "root": "pages/child",
       "pages": [
         {
           "path": "tasks",

+ 0 - 113
cfc-frontend/pages/guide/plans/detail.vue

@@ -1,113 +0,0 @@
-<template>
-  <view class="container" v-if="plan">
-    <view class="card">
-      <view class="card-header">
-        <text class="card-title">{{ plan.name || '测评方案' }}</text>
-        <text class="status-tag" :class="'status-' + plan.status">{{ statusLabel(plan.status) }}</text>
-      </view>
-      <view class="card-body">
-        <view class="info-row"><text class="label">家庭ID</text><text>{{ plan.familyId }}</text></view>
-        <view class="info-row"><text class="label">孩子ID</text><text>{{ plan.childId }}</text></view>
-        <view class="info-row"><text class="label">周期</text><text>{{ plan.totalDays || '-' }} 天</text></view>
-        <view class="info-row"><text class="label">来源</text><text>{{ plan.source || '-' }}</text></view>
-        <view class="info-row" v-if="plan.sourceResultId"><text class="label">来源测评</text><text>{{ plan.sourceResultId }}</text></view>
-        <view class="info-row"><text class="label">创建时间</text><text>{{ formatDate(plan.createdAt) }}</text></view>
-        <view class="info-row" v-if="plan.reviewedAt"><text class="label">审核时间</text><text>{{ formatDate(plan.reviewedAt) }}</text></view>
-        <view class="info-row" v-if="plan.activatedAt"><text class="label">激活时间</text><text>{{ formatDate(plan.activatedAt) }}</text></view>
-      </view>
-      <view class="card-remark" v-if="plan.remark">
-        <text class="remark-title">方案说明</text>
-        <text class="remark-text">{{ plan.remark }}</text>
-      </view>
-    </view>
-
-    <view class="action-bar" v-if="plan.status === 'generated'">
-      <button class="btn-approve" @click="handleApprove">批准方案</button>
-      <button class="btn-reject" @click="handleReject">驳回方案</button>
-    </view>
-    <view class="action-bar" v-if="plan.status === 'reviewed'">
-      <button class="btn-activate" @click="handleActivate">激活方案</button>
-    </view>
-  </view>
-</template>
-
-<script>
-import { getGuidePlanDetail, reviewPlan, activatePlan } from '@/utils/api'
-
-export default {
-  data() {
-    return {
-      plan: null
-    }
-  },
-  onLoad(options) {
-    if (options.planId) this.loadPlan(options.planId)
-  },
-  methods: {
-    async loadPlan(planId) {
-      try {
-        const res = await getGuidePlanDetail({ planId })
-        if (res.code === 200) this.plan = res.data
-      } catch (e) { /* ignore */ }
-    },
-    statusLabel(s) {
-      const map = { generated: '待审核', reviewed: '已审核', active: '已激活', completed: '已完成', rejected: '已驳回' }
-      return map[s] || s
-    },
-    formatDate(d) {
-      if (!d) return '-'
-      const date = new Date(d)
-      return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate()
-    },
-    async handleApprove() {
-      const res = await reviewPlan({ planId: this.plan.id, approved: true })
-      if (res.code === 200) { uni.showToast({ title: '已批准' }); this.loadPlan(this.plan.id) }
-    },
-    async handleReject() {
-      uni.showModal({
-        title: '驳回方案',
-        content: '确定驳回该方案吗?',
-        success: async (r) => {
-          if (!r.confirm) return
-          const res = await reviewPlan({ planId: this.plan.id, approved: false })
-          if (res.code === 200) { uni.showToast({ title: '已驳回' }); this.loadPlan(this.plan.id) }
-        }
-      })
-    },
-    async handleActivate() {
-      uni.showModal({
-        title: '激活方案',
-        content: '激活后将生成任务,确定激活?',
-        success: async (r) => {
-          if (!r.confirm) return
-          const res = await activatePlan({ planId: this.plan.id })
-          if (res.code === 200) { uni.showToast({ title: '已激活' }); this.loadPlan(this.plan.id) }
-        }
-      })
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container { min-height: 100vh; background: #f5f5f5; padding: 30rpx; }
-.card { background: #fff; border-radius: 16rpx; padding: 30rpx; margin-bottom: 30rpx; }
-.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24rpx; }
-.card-title { font-size: 34rpx; font-weight: bold; }
-.status-tag { font-size: 24rpx; padding: 6rpx 16rpx; border-radius: 6rpx; }
-.status-generated { background: #fff3cd; color: #856404; }
-.status-reviewed { background: #d1ecf1; color: #0c5460; }
-.status-active { background: #d4edda; color: #155724; }
-.status-completed { background: #e2e3e5; color: #383d41; }
-.status-rejected { background: #f8d7da; color: #721c24; }
-.card-body { margin-bottom: 20rpx; }
-.info-row { display: flex; justify-content: space-between; padding: 16rpx 0; border-bottom: 2rpx solid #f5f5f5; font-size: 28rpx; }
-.label { color: #999; }
-.card-remark { background: #f8f9fa; border-radius: 12rpx; padding: 20rpx; }
-.remark-title { font-size: 28rpx; font-weight: bold; display: block; margin-bottom: 12rpx; }
-.remark-text { font-size: 26rpx; color: #666; white-space: pre-wrap; }
-.action-bar { display: flex; gap: 20rpx; padding: 0 30rpx; }
-.btn-approve { flex: 1; background: #27ae60; color: #fff; border-radius: 40rpx; }
-.btn-reject { flex: 1; background: #e74c3c; color: #fff; border-radius: 40rpx; }
-.btn-activate { flex: 1; background: #4A9BD7; color: #fff; border-radius: 40rpx; }
-</style>

+ 0 - 188
cfc-frontend/pages/guide/plans/index.vue

@@ -1,188 +0,0 @@
-<template>
-  <view class="container">
-    <view class="header">
-      <text class="title">测评方案审核</text>
-      <view class="header-actions">
-        <picker :range="['全部', '待审核(generated)', '已审核(reviewed)', '已激活(active)', '已完成(completed)']" @change="onStatusFilter">
-          <view class="filter-btn">
-            <text>{{ statusFilterText }}</text>
-          </view>
-        </picker>
-      </view>
-    </view>
-
-    <view class="family-section" v-for="group in planGroups" :key="group.familyId">
-      <view class="family-header">
-        <text class="family-name">家庭 ID: {{ group.familyId }}</text>
-        <text class="plan-count">{{ group.plans.length }} 个方案</text>
-      </view>
-      <view class="plan-card" v-for="plan in group.plans" :key="plan.id" @click="goDetail(plan)">
-        <view class="plan-header">
-          <text class="plan-title">{{ plan.name || '测评方案' }}</text>
-          <text class="plan-status" :class="'status-' + plan.status">{{ statusLabel(plan.status) }}</text>
-        </view>
-        <view class="plan-meta">
-          <text>孩子ID: {{ plan.childId }}</text>
-          <text>{{ formatDate(plan.createdAt) }}</text>
-        </view>
-        <view class="plan-desc" v-if="plan.remark">{{ plan.remark }}</view>
-        <view class="plan-actions">
-          <view class="action-btn primary" v-if="plan.status === 'generated'" @click.stop="handleReview(plan, true)">
-            <text>批准</text>
-          </view>
-          <view class="action-btn danger" v-if="plan.status === 'generated'" @click.stop="handleReview(plan, false)">
-            <text>驳回</text>
-          </view>
-          <view class="action-btn success" v-if="plan.status === 'reviewed'" @click.stop="handleActivate(plan)">
-            <text>激活</text>
-          </view>
-        </view>
-      </view>
-    </view>
-
-    <view class="empty" v-if="planGroups.length === 0">
-      <text>暂无方案</text>
-    </view>
-
-    <uni-popup ref="reviewPopup" type="dialog">
-      <uni-popup-dialog title="审核意见" :content="reviewComment" @confirm="submitReview" @close="reviewComment = ''">
-        <input class="review-input" v-model="reviewComment" placeholder="输入审核意见(可选)" />
-      </uni-popup-dialog>
-    </uni-popup>
-  </view>
-</template>
-
-<script>
-import { listFamilyPlans, reviewPlan, activatePlan, getGuidePlanDetail } from '@/utils/api'
-
-export default {
-  data() {
-    return {
-      plans: [],
-      statusFilter: '',
-      statusFilterText: '全部状态',
-      currentPlan: null,
-      reviewApproved: false,
-      reviewComment: '',
-      families: []
-    }
-  },
-  computed: {
-    planGroups() {
-      const groups = {}
-      for (const p of this.plans) {
-        const key = p.familyId
-        if (!groups[key]) groups[key] = { familyId: key, plans: [] }
-        groups[key].plans.push(p)
-      }
-      return Object.values(groups)
-    }
-  },
-  onShow() {
-    this.loadPlans()
-  },
-  methods: {
-    async loadPlans() {
-      try {
-        const res = await listFamilyPlans({ status: this.statusFilter || null })
-        if (res.code === 200) this.plans = res.data || []
-      } catch (e) { /* ignore */ }
-    },
-    onStatusFilter(e) {
-      const labels = ['全部', '待审核(generated)', '已审核(reviewed)', '已激活(active)', '已完成(completed)']
-      const vals = ['', 'generated', 'reviewed', 'active', 'completed']
-      const idx = e.detail.value
-      this.statusFilterText = labels[idx]
-      this.statusFilter = vals[idx]
-      this.loadPlans()
-    },
-    statusLabel(s) {
-      const map = { generated: '待审核', reviewed: '已审核', active: '已激活', completed: '已完成', rejected: '已驳回' }
-      return map[s] || s
-    },
-    formatDate(d) {
-      if (!d) return ''
-      const date = new Date(d)
-      return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate()
-    },
-    goDetail(plan) {
-      uni.navigateTo({ url: '/pages/guide/plans/detail?planId=' + plan.id })
-    },
-    handleReview(plan, approved) {
-      this.currentPlan = plan
-      this.reviewApproved = approved
-      this.reviewComment = ''
-      if (!approved) {
-        uni.showModal({
-          title: '驳回方案',
-          content: '确定驳回该方案吗?',
-          success: (res) => {
-            if (res.confirm) this.submitReview()
-          }
-        })
-      } else {
-        this.submitReview()
-      }
-    },
-    async submitReview() {
-      if (!this.currentPlan) return
-      try {
-        const res = await reviewPlan({
-          planId: this.currentPlan.id,
-          approved: this.reviewApproved,
-          comment: this.reviewComment
-        })
-        if (res.code === 200) {
-          uni.showToast({ title: '操作成功' })
-          this.loadPlans()
-        }
-      } catch (e) { /* ignore */ }
-    },
-    handleActivate(plan) {
-      uni.showModal({
-        title: '激活方案',
-        content: '激活后将生成任务,确定激活?',
-        success: async (res) => {
-          if (!res.confirm) return
-          try {
-            const r = await activatePlan({ planId: plan.id })
-            if (r.code === 200) {
-              uni.showToast({ title: '方案已激活' })
-              this.loadPlans()
-            }
-          } catch (e) { /* ignore */ }
-        }
-      })
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container { min-height: 100vh; background: #f5f5f5; padding: 30rpx; }
-.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30rpx; }
-.title { font-size: 36rpx; font-weight: bold; }
-.filter-btn { padding: 10rpx 20rpx; background: #fff; border-radius: 8rpx; font-size: 26rpx; }
-.family-section { margin-bottom: 30rpx; }
-.family-header { display: flex; justify-content: space-between; padding: 20rpx; background: #e8f4fd; border-radius: 12rpx 12rpx 0 0; }
-.family-name { font-weight: bold; font-size: 28rpx; }
-.plan-count { font-size: 24rpx; color: #999; }
-.plan-card { background: #fff; padding: 24rpx; border-bottom: 2rpx solid #f0f0f0; }
-.plan-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16rpx; }
-.plan-title { font-size: 30rpx; font-weight: bold; }
-.plan-status { font-size: 24rpx; padding: 4rpx 12rpx; border-radius: 6rpx; }
-.status-generated { background: #fff3cd; color: #856404; }
-.status-reviewed { background: #d1ecf1; color: #0c5460; }
-.status-active { background: #d4edda; color: #155724; }
-.status-completed { background: #e2e3e5; color: #383d41; }
-.status-rejected { background: #f8d7da; color: #721c24; }
-.plan-meta { font-size: 24rpx; color: #999; display: flex; justify-content: space-between; margin-bottom: 12rpx; }
-.plan-desc { font-size: 26rpx; color: #666; margin-bottom: 16rpx; }
-.plan-actions { display: flex; gap: 20rpx; }
-.action-btn { padding: 10rpx 24rpx; border-radius: 8rpx; font-size: 26rpx; }
-.action-btn.primary { background: #4A9BD7; color: #fff; }
-.action-btn.danger { background: #e74c3c; color: #fff; }
-.action-btn.success { background: #27ae60; color: #fff; }
-.empty { text-align: center; padding: 100rpx 0; color: #999; font-size: 30rpx; }
-.review-input { border: 2rpx solid #ddd; border-radius: 8rpx; padding: 16rpx; margin-top: 20rpx; }
-</style>

+ 0 - 968
cfc-frontend/pages/guide/training-plans/detail.vue

@@ -1,968 +0,0 @@
-<template>
-  <view class="container">
-    <!-- ===== 看板模式 ===== -->
-    <view class="board-section" v-if="mode === 'board' && boardData">
-      <view class="board-header">
-        <text class="board-title">{{ boardData.title }}</text>
-        <text class="board-progress">{{ boardData.progress }}%</text>
-      </view>
-      <view class="progress-bar-wrap">
-        <view class="progress-bar-fill" :style="{ width: boardData.progress + '%' }"></view>
-      </view>
-      <view class="board-summary">
-        <view class="summary-item">
-          <text class="summary-value">{{ boardData.completedItems }}</text>
-          <text class="summary-label">已完成</text>
-        </view>
-        <view class="summary-item">
-          <text class="summary-value">{{ boardData.totalItems }}</text>
-          <text class="summary-label">总任务</text>
-        </view>
-        <view class="summary-item">
-          <text class="summary-value">{{ boardData.totalItems - boardData.completedItems }}</text>
-          <text class="summary-label">进行中</text>
-        </view>
-      </view>
-
-      <view class="board-items" v-if="boardData.itemStatus">
-        <view class="board-item" v-for="item in boardData.itemStatus" :key="item.itemId">
-          <view class="bi-left">
-            <view class="bi-check" :class="item.status === 'completed' ? 'done' : ''">
-              <text v-if="item.status === 'completed'">&#x2714;</text>
-              <text v-else>&#x25CB;</text>
-            </view>
-            <view class="bi-info">
-              <text class="bi-dim">{{ dimensionLabel(item.dimensionKey) }}</text>
-              <text class="bi-title">{{ item.taskTitle }}</text>
-            </view>
-          </view>
-          <view class="bi-right">
-            <text class="bi-status" :class="'bi-' + item.status">{{ itemStatusText(item.status) }}</text>
-          </view>
-        </view>
-      </view>
-    </view>
-
-    <!-- ===== 编辑模式 ===== -->
-    <view class="edit-section" v-else>
-      <!-- 基本信息 -->
-      <view class="section-card">
-        <view class="section-title">方案信息</view>
-        <view class="form-item">
-          <text class="form-label">方案标题</text>
-          <input class="form-input" v-model="planForm.title" placeholder="请输入方案标题" :disabled="!editable" />
-        </view>
-        <view class="form-item">
-          <text class="form-label">方案描述</text>
-          <textarea class="form-textarea" v-model="planForm.description" placeholder="请输入方案描述" :disabled="!editable" />
-        </view>
-        <view class="form-item">
-          <text class="form-label">状态</text>
-          <text class="status-badge" :class="'status-' + planForm.status">{{ statusText(planForm.status) }}</text>
-        </view>
-        <view class="form-actions" v-if="editable && !isNew">
-          <view class="btn-save" @click="savePlan">
-            <text>保存</text>
-          </view>
-        </view>
-      </view>
-
-      <!-- 训练项目 -->
-      <view class="section-card">
-        <view class="section-header">
-          <text class="section-title">训练项目</text>
-          <view class="btn-add-item" v-if="editable" @click="showAddItem">
-            <text>+ 添加项目</text>
-          </view>
-        </view>
-
-        <view class="item-list" v-if="items.length > 0">
-          <view class="plan-item" v-for="(item, idx) in items" :key="item.id">
-            <view class="item-header">
-              <view class="item-seq">{{ idx + 1 }}</view>
-              <view class="item-dim-tag">{{ dimensionLabel(item.dimensionKey) }}</view>
-              <view class="item-status-badge" :class="'item-' + item.status">{{ itemStatusText(item.status) }}</view>
-            </view>
-            <view class="item-body">
-              <view class="item-field">
-                <text class="item-field-label">任务标题</text>
-                <input v-if="editable" class="item-input" v-model="item.taskTitle" @blur="updateItemField(item)" />
-                <text v-else class="item-field-value">{{ item.taskTitle }}</text>
-              </view>
-              <view class="item-field">
-                <text class="item-field-label">任务描述</text>
-                <input v-if="editable" class="item-input" v-model="item.taskDesc" @blur="updateItemField(item)" />
-                <text v-else class="item-field-value">{{ item.taskDesc }}</text>
-              </view>
-              <view class="item-field-row">
-                <view class="item-field half">
-                  <text class="item-field-label">积分</text>
-                  <input v-if="editable" class="item-input" type="number" v-model="item.taskPoints" @blur="updateItemField(item)" />
-                  <text v-else class="item-field-value">{{ item.taskPoints || 2 }}</text>
-                </view>
-                <view class="item-field half">
-                  <text class="item-field-label">截止天数</text>
-                  <input v-if="editable" class="item-input" type="number" v-model="item.deadlineDays" @blur="updateItemField(item)" />
-                  <text v-else class="item-field-value">{{ item.deadlineDays || 7 }}天</text>
-                </view>
-              </view>
-              <view class="item-field" v-if="item.minigameCode">
-                <text class="item-field-label">小游戏</text>
-                <text class="item-field-value">{{ item.minigameCode }}</text>
-              </view>
-            </view>
-            <view class="item-actions" v-if="editable">
-              <view class="item-action-btn delete" @click="deleteItem(item.id, idx)">
-                <text>删除</text>
-              </view>
-            </view>
-          </view>
-        </view>
-
-        <view class="empty-items" v-else>
-          <text class="empty-items-text">暂无训练项目,点击上方"添加项目"开始配置</text>
-        </view>
-      </view>
-
-      <!-- 操作栏 -->
-      <view class="action-bar" v-if="editable && !isNew">
-        <view class="btn-deploy" @click="handleDeploy" v-if="planForm.status === 'draft'">
-          <text>下发训练方案</text>
-        </view>
-      </view>
-    </view>
-
-    <!-- 添加项目弹窗 -->
-    <view class="modal-mask" v-if="showItemModal" @click="showItemModal = false">
-      <view class="modal-card" @click.stop>
-        <text class="modal-title">添加训练项目</text>
-        <view class="modal-form">
-          <view class="form-item">
-            <text class="form-label">认知维度</text>
-            <picker class="dim-picker" :value="dimPickerIdx" :range="dimensionOptions" range-key="label" @change="onDimChange">
-              <text class="picker-text">{{ newItem.dimensionKey ? dimensionLabel(newItem.dimensionKey) : '请选择维度' }}</text>
-            </picker>
-          </view>
-          <view class="form-item">
-            <text class="form-label">任务标题</text>
-            <input class="form-input" v-model="newItem.taskTitle" placeholder="请输入任务标题" />
-          </view>
-          <view class="form-item">
-            <text class="form-label">任务描述</text>
-            <textarea class="form-textarea" v-model="newItem.taskDesc" placeholder="请输入任务描述" />
-          </view>
-          <view class="form-item">
-            <text class="form-label">建议文案</text>
-            <textarea class="form-textarea" v-model="newItem.suggestion" placeholder="给家长的建议" />
-          </view>
-          <view class="form-item-row">
-            <view class="form-item half">
-              <text class="form-label">积分</text>
-              <input class="form-input" type="number" v-model="newItem.taskPoints" />
-            </view>
-            <view class="form-item half">
-              <text class="form-label">截止天数</text>
-              <input class="form-input" type="number" v-model="newItem.deadlineDays" />
-            </view>
-          </view>
-        </view>
-        <view class="modal-actions">
-          <view class="btn-cancel" @click="showItemModal = false"><text>取消</text></view>
-          <view class="btn-confirm" @click="addItemConfirm"><text>确认添加</text></view>
-        </view>
-      </view>
-    </view>
-
-    <!-- 新建模式: 选择孩子 -->
-    <view class="modal-mask" v-if="showChildModal" @click="showChildModal = false">
-      <view class="modal-card" @click.stop>
-        <text class="modal-title">选择孩子</text>
-        <view class="modal-child-list">
-          <view class="modal-child-item" v-for="child in bindedChildren" :key="child.id"
-            @click="selectCreateChild(child.id)">
-            <text>{{ child.name || '孩子' + child.id }}</text>
-          </view>
-          <view class="empty-small-text" v-if="bindedChildren.length === 0">暂无可用孩子</view>
-        </view>
-        <view class="modal-footer">
-          <view class="btn-cancel" @click="showChildModal = false"><text>取消</text></view>
-        </view>
-      </view>
-    </view>
-
-    <view class="loading" v-if="loading"><text class="loading-text">加载中...</text></view>
-  </view>
-</template>
-
-<script>
-import { getTrainingPlanDetail, createTrainingPlan, updateTrainingPlan, addTrainingPlanItem, updateTrainingPlanItem, deleteTrainingPlanItem, deployTrainingPlan, getExecutionBoard, getGuideBindedChildren } from '../../../utils/api.js'
-
-export default {
-  data() {
-    return {
-      planId: null,
-      isNew: false,
-      mode: 'edit',
-      planForm: { title: '', description: '', status: 'draft' },
-      items: [],
-      boardData: null,
-      loading: false,
-      editable: true,
-      showItemModal: false,
-      showChildModal: false,
-      bindedChildren: [],
-      newItem: { dimensionKey: '', taskTitle: '', taskDesc: '', suggestion: '', taskPoints: 2, deadlineDays: 7 },
-      dimPickerIdx: 0,
-      dimensionOptions: [
-        { key: 'perception', label: '感知能力' },
-        { key: 'focus', label: '专注力' },
-        { key: 'memory', label: '记忆力' },
-        { key: 'logic', label: '逻辑思维' },
-        { key: 'spatial', label: '空间思维' },
-        { key: 'processingSpeed', label: '加工速度' }
-      ]
-    }
-  },
-  onLoad(options) {
-    this.planId = options.planId ? parseInt(options.planId) : null
-    this.mode = options.mode || 'edit'
-
-    if (!this.planId || this.planId === 0) {
-      this.isNew = true
-      this.planForm = { title: '认知训练方案', description: '', status: 'draft' }
-      this.showChildModalForNew()
-    } else {
-      this.loadDetail()
-    }
-  },
-  methods: {
-    loadDetail() {
-      var self = this
-      self.loading = true
-      if (self.mode === 'board') {
-        getExecutionBoard(self.planId).then(function(res) {
-          self.loading = false
-          if (res.code === 200) {
-            self.boardData = res.data
-          }
-        }).catch(function() { self.loading = false })
-      } else {
-        getTrainingPlanDetail(self.planId).then(function(res) {
-          self.loading = false
-          if (res.code === 200 && res.data) {
-            self.planForm = res.data.plan || {}
-            self.items = res.data.items || []
-            self.editable = self.planForm.status === 'draft'
-          }
-        }).catch(function() { self.loading = false })
-      }
-    },
-    showChildModalForNew() {
-      getGuideBindedChildren().then(function(res) {
-        if (res.code === 200 && res.data) {
-          var children = []
-          var data = res.data
-          for (var i = 0; i < data.length; i++) {
-            var family = data[i]
-            if (family.children) {
-              for (var j = 0; j < family.children.length; j++) {
-                children.push(family.children[j])
-              }
-            }
-            if (family.members) {
-              for (var k = 0; k < family.members.length; k++) {
-                var m = family.members[k]
-                if (m.childId) children.push({ id: m.childId, name: m.name || m.childName })
-              }
-            }
-          }
-          self.bindedChildren = children
-        }
-      }).catch(function() {})
-      this.showChildModal = true
-    },
-    selectCreateChild(childId) {
-      var self = this
-      self.showChildModal = false
-      createTrainingPlan(childId, self.planForm.title, self.planForm.description).then(function(res) {
-        if (res.code === 200 && res.data) {
-          self.planId = res.data
-          self.isNew = false
-          uni.showToast({ title: '方案已创建', icon: 'success' })
-          setTimeout(function() {
-            uni.redirectTo({ url: '/pages/guide/training-plans/detail?planId=' + self.planId })
-          }, 1000)
-        } else {
-          uni.showToast({ title: res.message || '创建失败', icon: 'none' })
-        }
-      }).catch(function() {
-        uni.showToast({ title: '网络异常', icon: 'none' })
-      })
-    },
-    savePlan() {
-      var self = this
-      updateTrainingPlan(self.planId, self.planForm.title, self.planForm.description).then(function(res) {
-        if (res.code === 200) {
-          uni.showToast({ title: '保存成功', icon: 'success' })
-        } else {
-          uni.showToast({ title: res.message || '保存失败', icon: 'none' })
-        }
-      }).catch(function() {
-        uni.showToast({ title: '网络异常', icon: 'none' })
-      })
-    },
-    showAddItem() {
-      this.newItem = { dimensionKey: '', taskTitle: '', taskDesc: '', suggestion: '', taskPoints: 2, deadlineDays: 7 }
-      this.dimPickerIdx = 0
-      this.showItemModal = true
-    },
-    onDimChange(e) {
-      this.dimPickerIdx = e.detail.value
-      this.newItem.dimensionKey = this.dimensionOptions[this.dimPickerIdx].key
-    },
-    addItemConfirm() {
-      var self = this
-      if (!this.newItem.dimensionKey) {
-        uni.showToast({ title: '请选择维度', icon: 'none' })
-        return
-      }
-      if (!this.newItem.taskTitle) {
-        uni.showToast({ title: '请输入任务标题', icon: 'none' })
-        return
-      }
-      var payload = {
-        planId: this.planId,
-        dimensionKey: this.newItem.dimensionKey,
-        taskTitle: this.newItem.taskTitle,
-        taskDesc: this.newItem.taskDesc,
-        suggestion: this.newItem.suggestion,
-        taskPoints: parseInt(this.newItem.taskPoints) || 2,
-        deadlineDays: parseInt(this.newItem.deadlineDays) || 7,
-        sequence: this.items.length + 1
-      }
-      addTrainingPlanItem(payload).then(function(res) {
-        if (res.code === 200) {
-          self.showItemModal = false
-          uni.showToast({ title: '已添加', icon: 'success' })
-          self.loadDetail()
-        } else {
-          uni.showToast({ title: res.message || '添加失败', icon: 'none' })
-        }
-      }).catch(function() {
-        uni.showToast({ title: '网络异常', icon: 'none' })
-      })
-    },
-    updateItemField(item) {
-      updateTrainingPlanItem(item).catch(function() {
-        uni.showToast({ title: '保存失败', icon: 'none' })
-      })
-    },
-    deleteItem(itemId, idx) {
-      var self = this
-      if (!itemId) {
-        self.items.splice(idx, 1)
-        return
-      }
-      uni.showModal({
-        title: '确认删除',
-        content: '确定删除此项目?',
-        success: function(res) {
-          if (res.confirm) {
-            deleteTrainingPlanItem(itemId).then(function(r) {
-              if (r.code === 200) {
-                self.items.splice(idx, 1)
-                uni.showToast({ title: '已删除', icon: 'success' })
-              }
-            }).catch(function() {})
-          }
-        }
-      })
-    },
-    handleDeploy() {
-      var self = this
-      uni.showModal({
-        title: '确认下发',
-        content: '将一次性创建所有训练任务,是否继续?',
-        success: function(res) {
-          if (res.confirm) {
-            uni.showLoading({ title: '下发中...' })
-            deployTrainingPlan(self.planId).then(function(r) {
-              uni.hideLoading()
-              if (r.code === 200) {
-                uni.showToast({ title: '下发成功', icon: 'success' })
-                self.loadDetail()
-              } else {
-                uni.showToast({ title: r.message || '下发失败', icon: 'none' })
-              }
-            }).catch(function() {
-              uni.hideLoading()
-              uni.showToast({ title: '网络异常', icon: 'none' })
-            })
-          }
-        }
-      })
-    },
-    dimensionLabel(key) {
-      var map = { perception: '感知', focus: '专注', memory: '记忆', logic: '逻辑', spatial: '空间', processingSpeed: '加工速度' }
-      return map[key] || key || ''
-    },
-    statusText(status) {
-      var map = { draft: '草稿', published: '已下发', completed: '已完成' }
-      return map[status] || status || '草稿'
-    },
-    itemStatusText(status) {
-      var map = { pending: '待完成', completed: '已完成', skipped: '已跳过' }
-      return map[status] || status || '待完成'
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container {
-  min-height: 100vh;
-  background: #f5f7fa;
-  padding-bottom: 60rpx;
-}
-
-/* ===== 看板 ===== */
-.board-section {
-  padding: 30rpx;
-}
-
-.board-header {
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 16rpx;
-}
-
-.board-title {
-  font-size: 34rpx;
-  font-weight: bold;
-  color: #333;
-}
-
-.board-progress {
-  font-size: 40rpx;
-  font-weight: bold;
-  color: #667eea;
-}
-
-.progress-bar-wrap {
-  height: 12rpx;
-  background: #e5e7eb;
-  border-radius: 6rpx;
-  overflow: hidden;
-  margin-bottom: 24rpx;
-}
-
-.progress-bar-fill {
-  height: 100%;
-  background: linear-gradient(90deg, #667eea, #764ba2);
-  border-radius: 6rpx;
-  transition: width 0.3s;
-}
-
-.board-summary {
-  display: flex;
-  flex-direction: row;
-  gap: 20rpx;
-  margin-bottom: 24rpx;
-}
-
-.summary-item {
-  flex: 1;
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 24rpx;
-  text-align: center;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05);
-}
-
-.summary-value {
-  font-size: 40rpx;
-  font-weight: bold;
-  color: #667eea;
-  display: block;
-}
-
-.summary-label {
-  font-size: 24rpx;
-  color: #888;
-  display: block;
-  margin-top: 4rpx;
-}
-
-.board-items {
-  background: #fff;
-  border-radius: 20rpx;
-  overflow: hidden;
-}
-
-.board-item {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  padding: 24rpx;
-  border-bottom: 1rpx solid #f0f0f0;
-}
-
-.board-item:last-child { border-bottom: none; }
-
-.bi-left {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  flex: 1;
-  gap: 16rpx;
-}
-
-.bi-check {
-  width: 44rpx;
-  height: 44rpx;
-  border-radius: 50%;
-  border: 3rpx solid #e5e7eb;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 22rpx;
-  color: #d1d5db;
-  flex-shrink: 0;
-}
-
-.bi-check.done {
-  background: #22c55e;
-  border-color: #22c55e;
-  color: #fff;
-}
-
-.bi-info {
-  display: flex;
-  flex-direction: column;
-  gap: 4rpx;
-}
-
-.bi-dim {
-  font-size: 22rpx;
-  color: #888;
-}
-
-.bi-title {
-  font-size: 28rpx;
-  color: #333;
-  font-weight: 500;
-}
-
-.bi-right {}
-
-.bi-status {
-  font-size: 22rpx;
-  padding: 4rpx 16rpx;
-  border-radius: 16rpx;
-}
-
-.bi-pending { background: #f3f4f6; color: #666; }
-.bi-completed { background: #d1fae5; color: #059669; }
-.bi-skipped { background: #fee2e2; color: #dc2626; }
-
-/* ===== 编辑模式 ===== */
-.edit-section {
-  padding: 20rpx 30rpx;
-}
-
-.section-card {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 28rpx;
-  margin-bottom: 24rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
-}
-
-.section-header {
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 16rpx;
-}
-
-.section-title {
-  font-size: 30rpx;
-  font-weight: bold;
-  color: #333;
-  margin-bottom: 16rpx;
-}
-
-.section-header .section-title {
-  margin-bottom: 0;
-}
-
-.form-item {
-  margin-bottom: 20rpx;
-}
-
-.form-item:last-child { margin-bottom: 0; }
-
-.form-label {
-  font-size: 26rpx;
-  color: #888;
-  display: block;
-  margin-bottom: 8rpx;
-}
-
-.form-input {
-  width: 100%;
-  height: 72rpx;
-  background: #f9fafb;
-  border-radius: 12rpx;
-  padding: 0 20rpx;
-  font-size: 28rpx;
-  color: #333;
-  box-sizing: border-box;
-  border: 1rpx solid #e5e7eb;
-}
-
-.form-textarea {
-  width: 100%;
-  background: #f9fafb;
-  border-radius: 12rpx;
-  padding: 16rpx 20rpx;
-  font-size: 28rpx;
-  color: #333;
-  box-sizing: border-box;
-  border: 1rpx solid #e5e7eb;
-  min-height: 120rpx;
-  display: block;
-}
-
-.form-actions {
-  display: flex;
-  flex-direction: row;
-  justify-content: flex-end;
-  margin-top: 20rpx;
-}
-
-.btn-save {
-  background: linear-gradient(135deg, #667eea, #764ba2);
-  padding: 16rpx 48rpx;
-  border-radius: 32rpx;
-}
-
-.btn-save:active { opacity: 0.8; }
-
-.btn-save text {
-  color: #fff;
-  font-size: 28rpx;
-  font-weight: 500;
-}
-
-.status-badge {
-  display: inline-block;
-  font-size: 24rpx;
-  padding: 6rpx 20rpx;
-  border-radius: 16rpx;
-}
-
-.status-draft { background: #f3f4f6; color: #666; }
-.status-published { background: #dbeafe; color: #2563eb; }
-.status-completed { background: #d1fae5; color: #059669; }
-
-/* 添加项目按钮 */
-.btn-add-item {
-  background: #eef2ff;
-  padding: 8rpx 24rpx;
-  border-radius: 20rpx;
-}
-
-.btn-add-item:active { opacity: 0.7; }
-
-.btn-add-item text {
-  font-size: 26rpx;
-  color: #667eea;
-  font-weight: 500;
-}
-
-/* 项目列表 */
-.item-list {
-  display: flex;
-  flex-direction: column;
-  gap: 16rpx;
-}
-
-.plan-item {
-  background: #f9fafb;
-  border-radius: 16rpx;
-  padding: 20rpx;
-  border: 1rpx solid #e5e7eb;
-}
-
-.item-header {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  gap: 12rpx;
-  margin-bottom: 12rpx;
-}
-
-.item-seq {
-  width: 36rpx;
-  height: 36rpx;
-  background: #667eea;
-  border-radius: 50%;
-  color: #fff;
-  font-size: 22rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-weight: bold;
-}
-
-.item-dim-tag {
-  background: #eef2ff;
-  color: #667eea;
-  font-size: 22rpx;
-  padding: 4rpx 14rpx;
-  border-radius: 12rpx;
-  font-weight: 500;
-}
-
-.item-status-badge {
-  font-size: 22rpx;
-  padding: 4rpx 14rpx;
-  border-radius: 12rpx;
-}
-
-.item-pending { background: #f3f4f6; color: #666; }
-.item-completed { background: #d1fae5; color: #059669; }
-
-.item-body {
-  display: flex;
-  flex-direction: column;
-  gap: 10rpx;
-}
-
-.item-field {
-  display: flex;
-  flex-direction: column;
-  gap: 4rpx;
-}
-
-.item-field-row {
-  display: flex;
-  flex-direction: row;
-  gap: 12rpx;
-}
-
-.item-field.half {
-  flex: 1;
-}
-
-.item-field-label {
-  font-size: 22rpx;
-  color: #999;
-}
-
-.item-field-value {
-  font-size: 26rpx;
-  color: #333;
-}
-
-.item-input {
-  background: #fff;
-  border: 1rpx solid #e5e7eb;
-  border-radius: 8rpx;
-  padding: 8rpx 12rpx;
-  font-size: 26rpx;
-  color: #333;
-}
-
-.item-actions {
-  display: flex;
-  flex-direction: row;
-  justify-content: flex-end;
-  margin-top: 10rpx;
-  border-top: 1rpx solid #e5e7eb;
-  padding-top: 10rpx;
-}
-
-.item-action-btn {
-  padding: 8rpx 20rpx;
-  border-radius: 16rpx;
-  font-size: 24rpx;
-}
-
-.item-action-btn.delete {
-  background: #fee2e2;
-  color: #dc2626;
-}
-
-.item-action-btn.delete:active { opacity: 0.7; }
-
-.empty-items {
-  padding: 40rpx 0;
-  text-align: center;
-}
-
-.empty-items-text {
-  font-size: 26rpx;
-  color: #999;
-}
-
-/* 底部操作栏 */
-.action-bar {
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  background: #fff;
-  padding: 20rpx 30rpx;
-  box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.06);
-  display: flex;
-  flex-direction: row;
-  justify-content: center;
-}
-
-.btn-deploy {
-  background: linear-gradient(135deg, #667eea, #764ba2);
-  padding: 20rpx 80rpx;
-  border-radius: 40rpx;
-  box-shadow: 0 4rpx 16rpx rgba(102,126,234,0.3);
-}
-
-.btn-deploy:active { opacity: 0.85; }
-
-.btn-deploy text {
-  color: #fff;
-  font-size: 30rpx;
-  font-weight: bold;
-}
-
-/* 弹窗 */
-.modal-mask {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0,0,0,0.5);
-  z-index: 100;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.modal-card {
-  background: #fff;
-  border-radius: 24rpx;
-  padding: 40rpx;
-  width: 640rpx;
-  max-height: 75vh;
-  overflow-y: auto;
-}
-
-.modal-title {
-  font-size: 32rpx;
-  font-weight: bold;
-  color: #333;
-  display: block;
-  margin-bottom: 24rpx;
-}
-
-.modal-form {
-  margin-bottom: 24rpx;
-}
-
-.form-item-row {
-  display: flex;
-  flex-direction: row;
-  gap: 16rpx;
-}
-
-.form-item.half {
-  flex: 1;
-}
-
-.dim-picker {
-  background: #f9fafb;
-  border: 1rpx solid #e5e7eb;
-  border-radius: 12rpx;
-  padding: 16rpx 20rpx;
-}
-
-.picker-text {
-  font-size: 28rpx;
-  color: #333;
-}
-
-.modal-actions {
-  display: flex;
-  flex-direction: row;
-  gap: 16rpx;
-  justify-content: center;
-}
-
-.btn-cancel, .btn-confirm {
-  padding: 16rpx 48rpx;
-  border-radius: 32rpx;
-  font-size: 28rpx;
-}
-
-.btn-cancel {
-  background: #f3f4f6;
-  color: #666;
-}
-
-.btn-confirm {
-  background: linear-gradient(135deg, #667eea, #764ba2);
-  color: #fff;
-}
-
-.btn-cancel:active, .btn-confirm:active { opacity: 0.7; }
-
-.modal-child-list {
-  display: flex;
-  flex-direction: column;
-  gap: 12rpx;
-  margin-bottom: 20rpx;
-}
-
-.modal-child-item {
-  padding: 20rpx;
-  background: #f9fafb;
-  border-radius: 12rpx;
-}
-
-.modal-child-item:active { background: #eef2ff; }
-
-.modal-child-item text {
-  font-size: 28rpx;
-  color: #333;
-}
-
-.modal-footer {
-  display: flex;
-  flex-direction: row;
-  justify-content: center;
-}
-
-.empty-small-text {
-  font-size: 26rpx;
-  color: #999;
-  text-align: center;
-  padding: 20rpx 0;
-}
-
-.loading {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 80rpx;
-}
-
-.loading-text {
-  font-size: 28rpx;
-  color: #999;
-}
-</style>

+ 0 - 599
cfc-frontend/pages/guide/training-plans/index.vue

@@ -1,599 +0,0 @@
-<template>
-  <view class="container">
-    <view class="header">
-      <text class="title">训练方案</text>
-      <view class="header-actions">
-        <view class="btn-secondary" @click="showChildPicker">
-          <text class="btn-secondary-text">筛选孩子</text>
-        </view>
-        <view class="btn-primary" @click="goToCreate">
-          <text class="btn-primary-text">+ 新建方案</text>
-        </view>
-      </view>
-    </view>
-
-    <view class="child-filter-bar" v-if="selectedChildId">
-      <text class="filter-label">当前: {{ selectedChildName || '孩子' }}</text>
-      <view class="filter-clear" @click="clearChildFilter">
-        <text class="filter-clear-text">清除</text>
-      </view>
-    </view>
-
-    <view class="plan-list" v-if="plans.length > 0">
-      <view class="plan-card" v-for="plan in plans" :key="plan.id" @click="goToDetail(plan.id)">
-        <view class="plan-header">
-          <text class="plan-title">{{ plan.title || '未命名方案' }}</text>
-          <text class="plan-status" :class="'status-' + plan.status">{{ statusText(plan.status) }}</text>
-        </view>
-        <view class="plan-meta" v-if="plan.childId">
-          <text class="meta-text">孩子ID: {{ plan.childId }}</text>
-          <text class="meta-date">{{ formatDate(plan.createdAt) }}</text>
-        </view>
-        <view class="plan-desc" v-if="plan.description">{{ plan.description }}</view>
-        <view class="plan-actions" @click.stop>
-          <view class="action-btn outline" v-if="plan.status === 'draft'" @click="goToDetail(plan.id)">
-            <text>编辑</text>
-          </view>
-          <view class="action-btn outline" v-if="plan.status === 'published'" @click="goToBoard(plan.id)">
-            <text>执行看板</text>
-          </view>
-          <view class="action-btn danger" v-if="plan.status === 'draft'" @click="confirmDelete(plan.id)">
-            <text>删除</text>
-          </view>
-          <view class="action-btn primary" v-if="plan.status === 'draft'" @click="handleDeploy(plan.id)">
-            <text>下发</text>
-          </view>
-        </view>
-      </view>
-    </view>
-
-    <view class="empty" v-else>
-      <text class="empty-icon">&#x1F3EB;</text>
-      <text class="empty-title">暂无训练方案</text>
-      <text class="empty-desc">为绑定家庭创建认知训练方案,或从推荐结果自动生成</text>
-      <view class="empty-btns">
-        <view class="btn-secondary" @click="showGenerateDialog">
-          <text class="btn-secondary-text">从推荐生成</text>
-        </view>
-        <view class="btn-primary" @click="goToCreate">
-          <text class="btn-primary-text">新建方案</text>
-        </view>
-      </view>
-    </view>
-
-    <!-- 生成方案弹窗 -->
-    <view class="modal-mask" v-if="showGenerate" @click="showGenerate = false">
-      <view class="modal-card" @click.stop>
-        <text class="modal-title">从推荐结果生成方案</text>
-        <text class="modal-desc">将根据该孩子最新的认知评估结果,自动生成针对性训练方案</text>
-        <view class="modal-child-list">
-          <view class="modal-child-item" v-for="child in bindedChildren" :key="child.id"
-            @click="generateFromRec(child.id)">
-            <text class="modal-child-name">{{ child.name || '孩子' + child.id }}</text>
-            <text class="modal-child-id">ID: {{ child.id }}</text>
-          </view>
-          <view class="empty-small" v-if="bindedChildren.length === 0">
-            <text class="empty-small-text">暂无可用孩子,请先绑定家庭</text>
-          </view>
-        </view>
-        <view class="modal-footer">
-          <view class="btn-cancel" @click="showGenerate = false">
-            <text>取消</text>
-          </view>
-        </view>
-      </view>
-    </view>
-
-    <!-- 孩子选择器弹窗 -->
-    <view class="modal-mask" v-if="showPicker" @click="showPicker = false">
-      <view class="modal-card" @click.stop>
-        <text class="modal-title">选择孩子</text>
-        <view class="modal-child-list">
-          <view class="modal-child-item" v-for="child in bindedChildren" :key="child.id"
-            @click="selectChild(child.id, child.name)">
-            <text class="modal-child-name">{{ child.name || '孩子' + child.id }}</text>
-            <text class="modal-child-id">ID: {{ child.id }}</text>
-          </view>
-          <view class="modal-child-item" @click="selectChild(null, '全部')">
-            <text class="modal-child-name">全部</text>
-          </view>
-        </view>
-        <view class="modal-footer">
-          <view class="btn-cancel" @click="showPicker = false">
-            <text>取消</text>
-          </view>
-        </view>
-      </view>
-    </view>
-
-    <view class="loading" v-if="loading">
-      <text class="loading-text">加载中...</text>
-    </view>
-  </view>
-</template>
-
-<script>
-import { getTrainingPlanList, generatePlanFromRecommendations, deployTrainingPlan, deleteTrainingPlan } from '../../../utils/api.js'
-import { getGuideBindedChildren } from '../../../utils/api.js'
-
-export default {
-  data() {
-    return {
-      plans: [],
-      bindedChildren: [],
-      selectedChildId: null,
-      selectedChildName: null,
-      showGenerate: false,
-      showPicker: false,
-      loading: false
-    }
-  },
-  onShow() {
-    this.loadPlans()
-    this.loadBindedChildren()
-  },
-  methods: {
-    loadPlans() {
-      var self = this
-      self.loading = true
-      getTrainingPlanList(self.selectedChildId).then(function(res) {
-        self.loading = false
-        if (res.code === 200) {
-          self.plans = res.data || []
-        }
-      }).catch(function() {
-        self.loading = false
-      })
-    },
-    loadBindedChildren() {
-      var self = this
-      getGuideBindedChildren().then(function(res) {
-        if (res.code === 200) {
-          self.bindedChildren = res.data || []
-        }
-      }).catch(function() {})
-    },
-    goToDetail(planId) {
-      uni.navigateTo({ url: '/pages/guide/training-plans/detail?planId=' + planId })
-    },
-    goToBoard(planId) {
-      uni.navigateTo({ url: '/pages/guide/training-plans/detail?planId=' + planId + '&mode=board' })
-    },
-    goToCreate() {
-      uni.navigateTo({ url: '/pages/guide/training-plans/detail?planId=0' })
-    },
-    showChildPicker() {
-      this.showPicker = true
-    },
-    clearChildFilter() {
-      this.selectedChildId = null
-      this.selectedChildName = null
-      this.loadPlans()
-    },
-    selectChild(childId, childName) {
-      this.selectedChildId = childId
-      this.selectedChildName = childName
-      this.showPicker = false
-      this.loadPlans()
-    },
-    showGenerateDialog() {
-      this.showGenerate = true
-    },
-    generateFromRec(childId) {
-      var self = this
-      uni.showModal({
-        title: '确认生成',
-        content: '将根据该孩子的评估结果自动生成训练方案,是否继续?',
-        success: function(res) {
-          if (res.confirm) {
-            self.showGenerate = false
-            uni.showLoading({ title: '生成中...' })
-            generatePlanFromRecommendations(childId).then(function(res) {
-              uni.hideLoading()
-              if (res.code === 200 && res.data) {
-                uni.showToast({ title: '生成成功', icon: 'success' })
-                self.loadPlans()
-                setTimeout(function() {
-                  self.goToDetail(res.data)
-                }, 1000)
-              } else {
-                uni.showToast({ title: res.message || '生成失败', icon: 'none' })
-              }
-            }).catch(function() {
-              uni.hideLoading()
-              uni.showToast({ title: '网络异常', icon: 'none' })
-            })
-          }
-        }
-      })
-    },
-    handleDeploy(planId) {
-      var self = this
-      uni.showModal({
-        title: '确认下发',
-        content: '确定下发此方案?将一次性创建所有训练任务。',
-        success: function(res) {
-          if (res.confirm) {
-            uni.showLoading({ title: '下发中...' })
-            deployTrainingPlan(planId).then(function(res) {
-              uni.hideLoading()
-              if (res.code === 200) {
-                uni.showToast({ title: '下发成功', icon: 'success' })
-                self.loadPlans()
-              } else {
-                uni.showToast({ title: res.message || '下发失败', icon: 'none' })
-              }
-            }).catch(function() {
-              uni.hideLoading()
-              uni.showToast({ title: '网络异常', icon: 'none' })
-            })
-          }
-        }
-      })
-    },
-    confirmDelete(planId) {
-      var self = this
-      uni.showModal({
-        title: '确认删除',
-        content: '删除后无法恢复,是否确定删除?',
-        success: function(res) {
-          if (res.confirm) {
-            deleteTrainingPlan(planId).then(function(res) {
-              if (res.code === 200) {
-                uni.showToast({ title: '删除成功', icon: 'success' })
-                self.loadPlans()
-              } else {
-                uni.showToast({ title: res.message || '删除失败', icon: 'none' })
-              }
-            }).catch(function() {
-              uni.showToast({ title: '网络异常', icon: 'none' })
-            })
-          }
-        }
-      })
-    },
-    statusText(status) {
-      var map = { draft: '草稿', published: '已下发', completed: '已完成' }
-      return map[status] || status || '草稿'
-    },
-    formatDate(dateStr) {
-      if (!dateStr) return ''
-      var d = new Date(dateStr)
-      var y = d.getFullYear()
-      var m = ('0' + (d.getMonth() + 1)).slice(-2)
-      var day = ('0' + d.getDate()).slice(-2)
-      return y + '-' + m + '-' + day
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container {
-  min-height: 100vh;
-  background: #f5f7fa;
-  padding-bottom: 40rpx;
-}
-
-.header {
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  align-items: center;
-  padding: 30rpx;
-  background: #fff;
-}
-
-.title {
-  font-size: 36rpx;
-  font-weight: bold;
-  color: #333;
-}
-
-.header-actions {
-  display: flex;
-  flex-direction: row;
-  gap: 16rpx;
-}
-
-.btn-primary {
-  background: linear-gradient(135deg, #667eea, #764ba2);
-  padding: 12rpx 28rpx;
-  border-radius: 32rpx;
-}
-
-.btn-primary:active { opacity: 0.8; }
-
-.btn-primary-text {
-  color: #fff;
-  font-size: 26rpx;
-  font-weight: 500;
-}
-
-.btn-secondary {
-  background: #f0f0f0;
-  padding: 12rpx 28rpx;
-  border-radius: 32rpx;
-}
-
-.btn-secondary:active { opacity: 0.8; }
-
-.btn-secondary-text {
-  color: #666;
-  font-size: 26rpx;
-}
-
-.child-filter-bar {
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  align-items: center;
-  padding: 16rpx 30rpx;
-  background: #eef2ff;
-}
-
-.filter-label {
-  font-size: 26rpx;
-  color: #667eea;
-}
-
-.filter-clear {
-  padding: 4rpx 16rpx;
-  background: #fff;
-  border-radius: 16rpx;
-}
-
-.filter-clear-text {
-  font-size: 24rpx;
-  color: #999;
-}
-
-.plan-list {
-  padding: 20rpx 30rpx;
-  display: flex;
-  flex-direction: column;
-  gap: 20rpx;
-}
-
-.plan-card {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 28rpx;
-  box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.05);
-}
-
-.plan-header {
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  align-items: flex-start;
-  margin-bottom: 12rpx;
-}
-
-.plan-title {
-  font-size: 30rpx;
-  font-weight: bold;
-  color: #333;
-  flex: 1;
-}
-
-.plan-status {
-  font-size: 22rpx;
-  padding: 4rpx 16rpx;
-  border-radius: 16rpx;
-  margin-left: 16rpx;
-}
-
-.status-draft {
-  background: #f3f4f6;
-  color: #666;
-}
-
-.status-published {
-  background: #dbeafe;
-  color: #2563eb;
-}
-
-.status-completed {
-  background: #d1fae5;
-  color: #059669;
-}
-
-.plan-meta {
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  margin-bottom: 8rpx;
-}
-
-.meta-text, .meta-date {
-  font-size: 24rpx;
-  color: #999;
-}
-
-.plan-desc {
-  font-size: 26rpx;
-  color: #666;
-  line-height: 1.5;
-  margin-bottom: 16rpx;
-}
-
-.plan-actions {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  gap: 12rpx;
-  margin-top: 16rpx;
-  border-top: 1rpx solid #f0f0f0;
-  padding-top: 16rpx;
-}
-
-.action-btn {
-  padding: 10rpx 24rpx;
-  border-radius: 24rpx;
-  font-size: 24rpx;
-}
-
-.action-btn:active { opacity: 0.7; }
-
-.action-btn.outline {
-  background: #f9fafb;
-  color: #374151;
-  border: 1rpx solid #e5e7eb;
-}
-
-.action-btn.primary {
-  background: #667eea;
-  color: #fff;
-}
-
-.action-btn.danger {
-  background: #fee2e2;
-  color: #dc2626;
-}
-
-.empty {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  padding: 120rpx 40rpx 80rpx;
-}
-
-.empty-icon {
-  font-size: 100rpx;
-  margin-bottom: 20rpx;
-}
-
-.empty-title {
-  font-size: 32rpx;
-  color: #333;
-  font-weight: bold;
-  margin-bottom: 12rpx;
-}
-
-.empty-desc {
-  font-size: 26rpx;
-  color: #999;
-  text-align: center;
-  line-height: 1.6;
-  margin-bottom: 40rpx;
-}
-
-.empty-btns {
-  display: flex;
-  flex-direction: row;
-  gap: 16rpx;
-}
-
-.loading {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 80rpx;
-}
-
-.loading-text {
-  font-size: 28rpx;
-  color: #999;
-}
-
-/* 弹窗 */
-.modal-mask {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0,0,0,0.5);
-  z-index: 100;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.modal-card {
-  background: #fff;
-  border-radius: 24rpx;
-  padding: 40rpx;
-  width: 600rpx;
-  max-height: 70vh;
-  overflow-y: auto;
-}
-
-.modal-title {
-  font-size: 32rpx;
-  font-weight: bold;
-  color: #333;
-  display: block;
-  margin-bottom: 12rpx;
-}
-
-.modal-desc {
-  font-size: 26rpx;
-  color: #888;
-  display: block;
-  margin-bottom: 24rpx;
-  line-height: 1.5;
-}
-
-.modal-child-list {
-  display: flex;
-  flex-direction: column;
-  gap: 12rpx;
-  margin-bottom: 24rpx;
-}
-
-.modal-child-item {
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  align-items: center;
-  padding: 20rpx;
-  background: #f9fafb;
-  border-radius: 12rpx;
-}
-
-.modal-child-item:active {
-  background: #eef2ff;
-}
-
-.modal-child-name {
-  font-size: 28rpx;
-  color: #333;
-  font-weight: 500;
-}
-
-.modal-child-id {
-  font-size: 24rpx;
-  color: #999;
-}
-
-.empty-small {
-  padding: 24rpx 0;
-  text-align: center;
-}
-
-.empty-small-text {
-  font-size: 26rpx;
-  color: #999;
-}
-
-.modal-footer {
-  display: flex;
-  flex-direction: row;
-  justify-content: center;
-}
-
-.btn-cancel {
-  padding: 16rpx 48rpx;
-  background: #f3f4f6;
-  border-radius: 32rpx;
-}
-
-.btn-cancel:active { opacity: 0.7; }
-
-.btn-cancel text {
-  font-size: 28rpx;
-  color: #666;
-}
-</style>

+ 16 - 28
cfc-frontend/pages/index/child-index.vue

@@ -271,21 +271,13 @@ B) 积分卡片 — Claymorphism 暖橙黏土风格
         </view>
       </view>
 
-      <!-- ===== 推荐活动 ===== -->
-      <DimensionActivities
+      <!-- ===== 推荐商品+活动(混合信息流) ===== -->
+      <RecommendedFeed
         v-if="hasChildren"
-        dimensionCode="child"
-        :activities="dimensionActivities"
-        @activityClick="goActivityDetail"
-        @moreActivities="goMoreActivities" />
-
-      <!-- ===== 推荐商品 ===== -->
-      <DimensionProducts
-        v-if="hasChildren"
-        dimensionCode="child"
         :products="dimensionProducts"
-        @productClick="goProductDetail"
-        @moreProducts="goMoreProducts" />
+        :activities="dimensionActivities"
+        @itemClick="onFeedItemClick"
+        @more="goMoreDiscover" />
 
       <!-- 底部安全留白 -->
       <view class="bottom-spacer"></view>
@@ -298,8 +290,7 @@ import { getChildren, getTodayTasks, getEnergyOverview, getEnergyLogs, getActivi
 import PageBanner from '../../components/PageBanner.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import WuxingSandbox from '../../components/wuxing-sandbox.vue'
-import DimensionActivities from '../../components/DimensionActivities.vue'
-import DimensionProducts from '../../components/DimensionProducts.vue'
+import RecommendedFeed from '../../components/RecommendedFeed.vue'
 import PlayfulCard from '../../components/PlayfulCard.vue'
 import PlayfulButton from '../../components/PlayfulButton.vue'
 import BaseBadge from '../../components/BaseBadge.vue'
@@ -337,8 +328,7 @@ export default {
     PageBanner,
     FamilyEnergyBar,
     WuxingSandbox,
-    DimensionActivities,
-    DimensionProducts,
+    RecommendedFeed,
     PlayfulCard,
     PlayfulButton,
     BaseBadge,
@@ -679,18 +669,16 @@ export default {
       })
     },
 
-    // ======================== 维度导航 ========================
-    goActivityDetail: function(act) {
-      if (act && act.id) uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + act.id })
-    },
-    goMoreActivities: function() {
-      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 })
+    // ======================== 推荐信息流 ========================
+    onFeedItemClick: function(item) {
+      if (item.type === 'product') {
+        uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + item.id })
+      } else if (item.type === 'activity') {
+        uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + item.id })
+      }
     },
-    goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/shop/index' })
+    goMoreDiscover: function() {
+      uni.switchTab({ url: '/pages/discover/index' })
     },
 
     // ======================== 任务交互 ========================

+ 19 - 34
cfc-frontend/pages/index/parent-index.vue

@@ -227,25 +227,15 @@
         @close="onCloseImport"
         @success="onImportSuccess" />
 
-      <!-- ===== 推荐活动 ===== -->
-      <DimensionActivities
-        dimensionCode="all"
-        :activities="recommendedActivities"
-        :isLoggedIn="!!token"
-        @activityClick="goActivityDetail"
-        @moreActivities="goMoreActivities" />
-      <view class="error-state" v-if="errorActivities">
-        <text class="error-state-text">活动推荐加载失败</text>
-      </view>
-
-      <!-- ===== 推荐商品 ===== -->
-      <DimensionProducts
-        dimensionCode="all"
+      <!-- ===== 推荐商品+活动(混合信息流) ===== -->
+      <RecommendedFeed
         :products="recommendedProducts"
-        @productClick="goProductDetail"
-        @moreProducts="goMoreProducts" />
-      <view class="error-state" v-if="errorProducts">
-        <text class="error-state-text">商品推荐加载失败</text>
+        :activities="recommendedActivities"
+        :loading="loadingProducts || loadingActivities"
+        @itemClick="onFeedItemClick"
+        @more="goMoreDiscover" />
+      <view class="error-state" v-if="errorProducts || errorActivities">
+        <text class="error-state-text">推荐加载失败</text>
       </view>
 
       <!-- ===== 推荐阅读 ===== -->
@@ -359,8 +349,6 @@
 <script>
 import { getFamilyMembers, getChildren, getPendingReviewTasks, getPendingWishes, approveTask, rejectTask, getChildCompletionStats, getParentDashboard, getFamilyEnergySandbox, getEnergyOverview, getVisibleFamilyMembers, getActivityList, getProductsByDomain, getContactList, getFeaturedArticles } from '../../utils/api.js'
 import PageBanner from '../../components/PageBanner.vue'
-import DimensionActivities from '../../components/DimensionActivities.vue'
-import DimensionProducts from '../../components/DimensionProducts.vue'
 import PlayfulCard from '../../components/PlayfulCard.vue'
 import PlayfulButton from '../../components/PlayfulButton.vue'
 import BaseBadge from '../../components/BaseBadge.vue'
@@ -370,9 +358,10 @@ import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import WuxingSandbox from '../../components/wuxing-sandbox.vue'
 import ContactCard from '../../components/ContactCard.vue'
 import ContactImport from '../../components/ContactImport.vue'
+import RecommendedFeed from '../../components/RecommendedFeed.vue'
 
 export default {
-  components: { PageBanner, DimensionActivities, DimensionProducts, PlayfulCard, PlayfulButton, BaseBadge, BaseEmpty, BaseLoading, FamilyRelationGraph, WuxingSandbox, ContactCard, ContactImport },
+  components: { PageBanner, RecommendedFeed, PlayfulCard, PlayfulButton, BaseBadge, BaseEmpty, BaseLoading, FamilyRelationGraph, WuxingSandbox, ContactCard, ContactImport },
   data() {
     return {
       nickname: '',
@@ -408,6 +397,8 @@ export default {
       errorTasks: false,
       errorActivities: false,
       errorProducts: false,
+      loadingActivities: false,
+      loadingProducts: false,
       contactList: [],
       showImportModal: false,
       // 推荐阅读
@@ -764,21 +755,15 @@ export default {
     goToAssessmentReport() { uni.navigateTo({ url: '/pages/assessment/report' }) },
     goToActivities() { uni.switchTab({ url: '/pages/discover/index' }) },
     goToRewards() { uni.switchTab({ url: '/pages/rewards/rewards' }) },
-    goActivityDetail(act) {
-      if (act && act.id) {
-        uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + act.id })
-      }
-    },
-    goMoreActivities() {
-      uni.navigateTo({ url: '/pages/activity/index' })
-    },
-    goProductDetail(prod) {
-      if (prod && prod.id) {
-        uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + prod.id })
+    onFeedItemClick(item) {
+      if (item.type === 'product') {
+        uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + item.id })
+      } else if (item.type === 'activity') {
+        uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + item.id })
       }
     },
-    goMoreProducts() {
-      uni.navigateTo({ url: '/pages/shop/index' })
+    goMoreDiscover() {
+      uni.switchTab({ url: '/pages/discover/index' })
     },
     loadRecommendedActivities() {
       var self = this

+ 14 - 29
cfc-frontend/pages/member-detail/member-detail.vue

@@ -88,21 +88,13 @@
         @taskClick="onTaskClick"
         @moreTasks="goTasks" />
 
-      <!-- 推荐活动(非身卡片入口时显示) -->
-      <DimensionActivities
+      <!-- 推荐商品+活动(混合信息流,非身卡片入口时显示) -->
+      <RecommendedFeed
         v-if="showRecommendations"
-        dimensionCode="body"
-        :activities="dimensionActivities"
-        @activityClick="goActivityDetail"
-        @moreActivities="goMoreActivities" />
-
-      <!-- 推荐商品(非身卡片入口时显示) -->
-      <DimensionProducts
-        v-if="showRecommendations"
-        dimensionCode="body"
         :products="dimensionProducts"
-        @productClick="goProductDetail"
-        @moreProducts="goMoreProducts" />
+        :activities="dimensionActivities"
+        @itemClick="onFeedItemClick"
+        @more="goMoreDiscover" />
 
       <!-- 底部占位 -->
       <view class="bottom-spacer"></view>
@@ -114,8 +106,7 @@
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
-import DimensionActivities from '../../components/DimensionActivities.vue'
-import DimensionProducts from '../../components/DimensionProducts.vue'
+import RecommendedFeed from '../../components/RecommendedFeed.vue'
 import BodySection from '../../components/member-detail-sections/body-section.vue'
 import MindSection from '../../components/member-detail-sections/mind-section.vue'
 import WisdomSection from '../../components/member-detail-sections/wisdom-section.vue'
@@ -133,7 +124,7 @@ var dimensionConfig = {
 }
 
 export default {
-  components: { FamilyEnergyBar, FamilyRelationGraph, DimensionTasks, DimensionActivities, DimensionProducts, BodySection, MindSection, WisdomSection, ActionSection },
+  components: { FamilyEnergyBar, FamilyRelationGraph, DimensionTasks, RecommendedFeed, BodySection, MindSection, WisdomSection, ActionSection },
   data() {
     return {
       memberId: null,
@@ -373,21 +364,15 @@ export default {
     onTaskClick: function(task) {
       uni.navigateTo({ url: '/pages/tasks/tasks' })
     },
-    goActivityDetail: function(act) {
-      if (act && act.id) {
-        uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
-      }
-    },
-    goMoreActivities: function() {
-      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 })
+    onFeedItemClick: function(item) {
+      if (item.type === 'product') {
+        uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + item.id })
+      } else if (item.type === 'activity') {
+        uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + item.id })
       }
     },
-    goMoreProducts: function() {
-      uni.navigateTo({ url: '/pages/shop/index' })
+    goMoreDiscover: function() {
+      uni.switchTab({ url: '/pages/discover/index' })
     },
     goTasks: function() {
       uni.switchTab({ url: '/pages/tasks/tasks' })

+ 14 - 6
cfc-frontend/pages/mind-detail/family-dashboard.vue

@@ -110,7 +110,7 @@
               <view class="calendar-day" v-for="(day, index) in monthDays" :key="index" :class="day.dayClass">
                 <text class="day-number">{{ day.day }}</text>
                 <view class="day-fortune" v-if="day.fortune">
-                  <view class="fortune-bubble" :class="'fortune-' + day.fortune.fortuneLevel"></view>
+                  <view class="fortune-bubble" :class="'fortune-lv-' + getFortuneLevel(day.fortune)"></view>
                 </view>
               </view>
             </view>
@@ -627,7 +627,7 @@ export default {
       return names[element] || element
     },
     
-    // 获取运势颜色
+// 获取运势颜色
     getFortuneColor(level) {
       const colors = {
         '大吉': '#EF4444',
@@ -639,6 +639,14 @@ export default {
       return colors[level] || '#666666'
     },
     
+    // 获取运势等级数值(用于CSS class)
+    getFortuneLevel(dayFortune) {
+      if (!dayFortune) return 2
+      var levelMap = { '大吉': 0, '吉': 1, '平': 2, '凶': 3, '大凶': 4 }
+      var level = dayFortune.fortuneLevel || dayFortune.fortuneLevelText || ''
+      return levelMap[level] !== undefined ? levelMap[level] : 2
+    },
+
     // 获取日历日样式
     getDayClass(day) {
       const classes = []
@@ -655,7 +663,7 @@ export default {
       
       if (day.fortune) {
         classes.push('has-data')
-        classes.push('fortune-' + day.fortune.fortuneLevel)
+        classes.push('fortune-lv-' + this.getFortuneLevel(day.fortune))
       }
       
       return classes
@@ -951,9 +959,9 @@ export default {
   height: 16rpx;
   border-radius: 50%;
 }
-.fortune-大吉, .fortune-吉 { background-color: #EF4444; }
-.fortune- { background-color: #FBBF24; }
-.fortune-凶, .fortune-大凶 { background-color: #8B5CF6; }
+.fortune-lv-0, .fortune-lv-1 { background-color: #EF4444; }
+.fortune-lv-2 { background-color: #FBBF24; }
+.fortune-lv-3, .fortune-lv-4 { background-color: #8B5CF6; }
 
 /* ===== 历史周报 ===== */
 .report-list {

+ 0 - 56
cfc-frontend/pages/parent/plans/detail.vue

@@ -1,56 +0,0 @@
-<template>
-  <view class="container">
-    <view class="header">
-      <text class="title">计划详情</text>
-    </view>
-    
-    <view class="content">
-      <text class="placeholder">计划详情页面开发中...</text>
-    </view>
-  </view>
-</template>
-
-<script>
-export default {
-  data() {
-    return {}
-  },
-  onLoad(options) {
-    // 获取计划ID
-    const planId = options.id
-    console.log('计划ID:', planId)
-    this.loadPlanDetail(planId)
-  },
-  methods: {
-    async loadPlanDetail(planId) {
-      // TODO: 加载计划详情
-      console.log('加载计划详情:', planId)
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container {
-  padding: 30rpx;
-}
-
-.header {
-  padding: 20rpx 0;
-}
-
-.title {
-  font-size: 36rpx;
-  font-weight: bold;
-}
-
-.content {
-  padding: 40rpx;
-  text-align: center;
-}
-
-.placeholder {
-  color: #999;
-  font-size: 28rpx;
-}
-</style>

+ 0 - 130
cfc-frontend/pages/parent/plans/index.vue

@@ -1,130 +0,0 @@
-<template>
-  <view class="container">
-    <view class="header">
-      <text class="title">我的计划</text>
-    </view>
-
-    <!-- 计划列表 -->
-    <view class="plan-list" v-if="plans.length > 0">
-      <view 
-        class="plan-card" 
-        v-for="plan in plans" 
-        :key="plan.id"
-        @click="goToDetail(plan.id)"
-      >
-        <view class="plan-header">
-          <text class="plan-name">{{ plan.name }}</text>
-          <text class="plan-status" :class="plan.status">{{ getStatusText(plan.status) }}</text>
-        </view>
-        <view class="plan-progress">
-          <progress :percent="getProgress(plan)" :stroke-width="8" backgroundColor="#e0e0e0" activeColor="#4CAF50" />
-          <text class="progress-text">{{ plan.completedDays }} / {{ plan.totalDays }} 天</text>
-        </view>
-        <view class="plan-dates">
-          <text>{{ formatDate(plan.startDate) }} - {{ formatDate(plan.endDate) }}</text>
-        </view>
-        <view class="plan-actions" v-if="plan.status === 'active'">
-          <button class="btn-cancel" @click.stop="cancelPlan(plan.id)">取消</button>
-        </view>
-      </view>
-    </view>
-
-    <!-- 空状态 -->
-    <view class="empty" v-else>
-      <text class="empty-text">暂无计划,快去应用任务模板吧</text>
-      <button class="btn-go" @click="goToMarket">去市场</button>
-    </view>
-  </view>
-</template>
-
-<script>
-import { getMyPlans, cancelPlan as cancelPlanApi } from '@/utils/api.js'
-
-export default {
-  data() {
-    return {
-      plans: []
-    }
-  },
-  onShow() {
-    this.loadPlans()
-  },
-  methods: {
-    async loadPlans() {
-      try {
-        const res = await getMyPlans()
-        this.plans = res.data || []
-      } catch (e) {
-        console.error(e)
-      }
-    },
-    getStatusText(status) {
-      const map = { 'active': '进行中', 'completed': '已完成', 'cancelled': '已取消' }
-      return map[status] || status
-    },
-    getProgress(plan) {
-      if (!plan.totalDays) return 0
-      return Math.round(plan.completedDays / plan.totalDays * 100)
-    },
-    formatDate(date) {
-      if (!date) return ''
-      const d = new Date(date)
-      return `${d.getMonth() + 1}-${d.getDate()}`
-    },
-    goToDetail(id) {
-      uni.navigateTo({
-        url: `/pages/parent/plans/detail?id=${id}`
-      })
-    },
-    goToMarket() {
-      uni.navigateTo({
-        url: '/pages/parent/market/index'
-      })
-    },
-    async cancelPlan(id) {
-      uni.showModal({
-        title: '确认取消',
-        content: '确定要取消这个计划吗?',
-        success: async (res) => {
-          if (res.confirm) {
-            try {
-              await cancelPlanApi(id)
-              uni.showToast({ title: '已取消' })
-              this.loadPlans()
-            } catch (e) {
-              uni.showToast({ title: '取消失败', icon: 'none' })
-            }
-          }
-        }
-      })
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container { min-height: 100vh; background: #f5f5f5; padding: 30rpx; }
-.header { margin-bottom: 30rpx; }
-.title { font-size: 36rpx; font-weight: bold; color: #333; }
-
-.plan-list { display: flex; flex-direction: column; gap: 20rpx; }
-.plan-card { background: #fff; border-radius: 16rpx; padding: 30rpx; box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05); }
-.plan-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
-.plan-name { font-size: 32rpx; font-weight: bold; color: #333; }
-.plan-status { font-size: 24rpx; padding: 8rpx 16rpx; border-radius: 20rpx; }
-.plan-status.active { background: #e8f5e9; color: #4CAF50; }
-.plan-status.completed { background: #e3f2fd; color: #2196F3; }
-.plan-status.cancelled { background: #f5f5f5; color: #999; }
-
-.plan-progress { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
-.plan-progress progress { flex: 1; }
-.progress-text { font-size: 24rpx; color: #666; }
-
-.plan-dates { font-size: 24rpx; color: #999; margin-bottom: 16rpx; }
-.plan-actions { display: flex; justify-content: flex-end; }
-.btn-cancel { background: #f5f5f5; color: #666; font-size: 24rpx; padding: 12rpx 24rpx; border-radius: 24rpx; }
-
-.empty { display: flex; flex-direction: column; align-items: center; padding: 100rpx; }
-.empty-text { color: #999; font-size: 28rpx; margin-bottom: 30rpx; }
-.btn-go { background: #4CAF50; color: #fff; }
-</style>