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