2026-07-02-activity-home-page-implementation-plan.md 20 KB

活动主页实现计划

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 数组末尾添加:

{
  "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: 提交

    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: 创建文件结构

    <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()

    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

    onLoad(options) {
    // 如果带 tab 参数,切换到对应 Tab
    if (options && options.tab) {
    this.currentTab = options.tab
    }
    this.loadData()
    }
    
  • [ ] Step 4: 实现 onShow

    onShow() {
    // 每次可见时刷新我的活动(用户可能刚报名/签到)
    if (this.currentTab === 'mine') {
    this.loadMyActivities(true)
    }
    }
    
  • [ ] Step 5: 实现 methods — Tab 切换

    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

    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

    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)

    loadHistoryActivities(refresh) {
    // 同 loadMyActivities,但 API 参数可能需要 status=ended
    // 或直接复用 loadMyActivities 通过 filter 状态过滤
    }
    
  • [ ] Step 9: 实现筛选切换

    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

    goDetail(act) {
    uni.navigateTo({ url: '/pages/discover/activity-detail/activity-detail?id=' + act.id })
    },
    switchToDiscover() {
    this.currentTab = 'discover'
    this.loadDiscoverActivities(true)
    }
    
  • [ ] Step 11: 提交

    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 内)

    <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)

    .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: 辅助方法

    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: 提交

    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):

<!-- 我的活动 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

    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

    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: 添加操作按钮样式

    .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: 提交

    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 方法:

// 修改前
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: 提交

    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: 编译验证

    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 技能执行计划。