|
|
@@ -0,0 +1,2916 @@
|
|
|
+# 代码 vs 设计差异 — 全面修复计划
|
|
|
+
|
|
|
+> **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:** 修复小程序全部 5 个 TabBar 页面(首页/身泰/心智/行远/富沛)的代码实现与 v2.0 设计文档 + 五维页面重设计 spec 之间的所有差异
|
|
|
+
|
|
|
+**架构:** 分 9 阶段 19 Tasks 按依赖顺序推进。Phase 1-2 基础设施 → Phase 3-4 Tab页改造(含心智认知雷达图+大五人格图+关系图、行远关系雷达图)→ Phase 5 富沛页重做 → Phase 6 组件改造 → Phase 7 后端改造(含整型金额)→ Phase 8 RadarChart 组件 + 后端数据 API → Phase 9 验证。
|
|
|
+- **首页五行沙盘保留不动**(仅增量加 FamilyEnergyBar/服务商入口/维度区块)
|
|
|
+- **心智页**:能量柱 + 认知六维雷达图 + 大五人格图(10岁+DAN) + 关系图 + 任务 + 活动 + 商品
|
|
|
+- **行远页**:六类关系雷达图(孩子/另一半/父母/同伴/师长/领导) + 任务 + 活动 + 商品
|
|
|
+- 需新建 `<RadarChart>` canvas 通用组件,心智和行远页共用
|
|
|
+
|
|
|
+**前置设计文档:**
|
|
|
+- `docs/需求分析/底签页面详细设计.md` (v2.0 TabBar 5页设计基线)
|
|
|
+- `docs/superpowers/specs/2026-06-13-five-dimension-pages-redesign.md` (三页统一改造+堆叠图+游客模式)
|
|
|
+
|
|
|
+**Tech Stack:** uni-app Vue 2, Spring Boot 2.7, MyBatis-Plus
|
|
|
+
|
|
|
+**优先级:** P0(阻塞) > P1(重要) > P2(一般) > P3(优化)
|
|
|
+
|
|
|
+**设计约束 (新增):**
|
|
|
+- **禁止使用多精度类型(decimal、double、float)**:数据库中所有涉及金额、百分比、评分等数值字段使用整型(`INT`/`BIGINT`),举例如下:
|
|
|
+ - 金额以 **分** 为单位存储(如 `1 元 = 100`),前端展示时除以 100 并保留 2 位小数
|
|
|
+ - 百分比以 **千分比** 存储(如 `23.5% = 235`),前端展示时除以 10 并拼接 `%`
|
|
|
+ - 评分以 **百分制整数** 存储(如 `85.5 = 855`),前端展示时除以 10
|
|
|
+- **现有代码改造**:`Product.price`/`memberPrice` 等已存在的 `BigDecimal` 字段,在本次修复中改为 `Integer`(分),前端价格展示统一加 `¥` 前缀并做 `(price / 100).toFixed(2)` 转换
|
|
|
+- **后端输出**:DTO 层输出时仍保留 price/memberPrice 等字段名,类型统一为 `Integer`,前端自行做分→元展示转换
|
|
|
+- **理由**:彻底规避浮点数精度问题(0.1+0.2≠0.3),消除跨系统金额计算风险,简化数据库计算逻辑
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件改动清单
|
|
|
+
|
|
|
+| 文件 | 操作 | 任务 |
|
|
|
+|------|------|------|
|
|
|
+| `cfc-frontend/pages.json` | 修改 | T1 TabBar配置 |
|
|
|
+| `cfc-frontend/static/tab-*.png` | 创建 | T2 图标 |
|
|
|
+| `cfc-frontend/pages/index/index.vue` | 修改 | T3/T10 |
|
|
|
+| `cfc-frontend/pages/teacher/index.vue` | 修改 | T4 |
|
|
|
+| `cfc-frontend/pages/body/index.vue` | 修改 | T5 |
|
|
|
+| `cfc-frontend/pages/mind/index.vue` | 修改 | T6 |
|
|
|
+| `cfc-frontend/pages/action/index.vue` | 修改 | T7 |
|
|
|
+| `cfc-frontend/pages/index/parent-index.vue` | 修改 | T8 |
|
|
|
+| `cfc-frontend/pages/index/child-index.vue` | 修改 | T9 |
|
|
|
+| `cfc-frontend/pages/wealth/index.vue` | **重写** | T11 |
|
|
|
+| `cfc-frontend/components/FamilyEnergyBar.vue` | 修改 | T12 |
|
|
|
+| `cfc-frontend/components/DimensionProducts.vue` | 修改 | T13 |
|
|
|
+| `cfc-frontend/components/DimensionActivities.vue` | 修改 | T13 |
|
|
|
+| `cfc-frontend/utils/api.js` | 修改 | T10 |
|
|
|
+| `cfc-backend/.../controller/product/ProductController.java` | 修改 | T14 |
|
|
|
+| `cfc-backend/.../entity/Activity.java` | 修改 | T15 |
|
|
|
+| `cfc-backend/.../controller/sncp/ActivityController.java` | 修改 | T15 |
|
|
|
+| `cfc-frontend/components/RadarChart.vue` | **新建** | T17 |
|
|
|
+| `cfc-backend/.../controller/ContactController.java` | **新建** | T18 |
|
|
|
+| `cfc-backend/.../entity/Contact.java` | 修改(如有) | T18 |
|
|
|
+| `cfc-backend/.../controller/assessment/AssessmentController.java` | 修改 | T18 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 1 — TabBar 基础设施 (P0 🔴)
|
|
|
+
|
|
|
+### Task 1: pages.json TabBar 配置修改
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/pages.json:589-626`
|
|
|
+
|
|
|
+- [ ] **Step 1: 修改 TabBar list 配置**
|
|
|
+
|
|
|
+将 TabBar 5 个 Tab 的名称和路径更新:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "tabBar": {
|
|
|
+ "color": "#7A7E83",
|
|
|
+ "selectedColor": "#F97316",
|
|
|
+ "borderStyle": "black",
|
|
|
+ "backgroundColor": "#FFFFFF",
|
|
|
+ "list": [
|
|
|
+ {
|
|
|
+ "pagePath": "pages/index/index",
|
|
|
+ "text": "首页",
|
|
|
+ "iconPath": "static/tab-home.png",
|
|
|
+ "selectedIconPath": "static/tab-home-active.png"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "pagePath": "pages/body/index",
|
|
|
+ "text": "身泰",
|
|
|
+ "iconPath": "static/tab-body.png",
|
|
|
+ "selectedIconPath": "static/tab-body-active.png"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "pagePath": "pages/mind/index",
|
|
|
+ "text": "心智",
|
|
|
+ "iconPath": "static/tab-mind.png",
|
|
|
+ "selectedIconPath": "static/tab-mind-active.png"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "pagePath": "pages/action/index",
|
|
|
+ "text": "行远",
|
|
|
+ "iconPath": "static/tab-action.png",
|
|
|
+ "selectedIconPath": "static/tab-action-active.png"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "pagePath": "pages/wealth/index",
|
|
|
+ "text": "富沛",
|
|
|
+ "iconPath": "static/tab-wealth.png",
|
|
|
+ "selectedIconPath": "static/tab-wealth-active.png"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+变更点:
|
|
|
+- Tab 2: "身体" → "身泰"(路径 `pages/body/index` 不变)
|
|
|
+- Tab 4: "行动" → "行远"(路径 `pages/action/index` 不变)
|
|
|
+- Tab 5: "我的" + `pages/profile/profile` → "富沛" + `pages/wealth/index`
|
|
|
+
|
|
|
+- [ ] **Step 2: 确认 wealth 子包注册存在**
|
|
|
+
|
|
|
+确认 `pages.json` 的 `subPackages` 数组中包含:
|
|
|
+```json
|
|
|
+{
|
|
|
+ "root": "pages/wealth",
|
|
|
+ "pages": [
|
|
|
+ { "path": "index", "style": { "navigationBarTitleText": "富沛" } },
|
|
|
+ { "path": "insurance-list", "style": { "navigationBarTitleText": "保单管理" } },
|
|
|
+ { "path": "insurance-add", "style": { "navigationBarTitleText": "保单详情" } },
|
|
|
+ { "path": "checkin", "style": { "navigationBarTitleText": "财商打卡" } }
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+如果缺少,补上 `navigationBarTitleText: "富沛"` 配置。
|
|
|
+
|
|
|
+- [ ] **Step 3: LSP 诊断**
|
|
|
+
|
|
|
+运行:`lsp_diagnostics(filePath="cfc-frontend/pages.json")`
|
|
|
+预期:无错误
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: TabBar 图标文件补全
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-frontend/static/tab-wealth.png`(从 `tab-profile.png` 复制)
|
|
|
+- Create: `cfc-frontend/static/tab-wealth-active.png`(从 `tab-profile-active.png` 复制)
|
|
|
+
|
|
|
+- [ ] **Step 1: 复制图标文件**
|
|
|
+
|
|
|
+```bash
|
|
|
+cp cfc-frontend/static/tab-profile.png cfc-frontend/static/tab-wealth.png
|
|
|
+cp cfc-frontend/static/tab-profile-active.png cfc-frontend/static/tab-wealth-active.png
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 确认图标文件存在**
|
|
|
+
|
|
|
+验证 `tab-wealth.png` 和 `tab-wealth-active.png` 文件存在且非空。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 2 — 首页路由清理 (P0 🔴)
|
|
|
+
|
|
|
+### Task 3: index.vue — 移除 TeacherIndex 引入和路由分支
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/pages/index/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 移除 TeacherIndex import**
|
|
|
+
|
|
|
+修改前(约第 108 行):
|
|
|
+```javascript
|
|
|
+import TeacherIndex from '../teacher/index.vue'
|
|
|
+```
|
|
|
+
|
|
|
+修改后:
|
|
|
+```javascript
|
|
|
+// TeacherIndex 已废弃(2026-06-15),教师功能折入 parent-index
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 从 components 对象中移除 TeacherIndex**
|
|
|
+
|
|
|
+修改前:
|
|
|
+```javascript
|
|
|
+components: {
|
|
|
+ ParentIndex,
|
|
|
+ ChildIndex,
|
|
|
+ TeacherIndex,
|
|
|
+ PageBanner,
|
|
|
+ FamilyEnergyBar
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+修改后:
|
|
|
+```javascript
|
|
|
+components: {
|
|
|
+ ParentIndex,
|
|
|
+ ChildIndex,
|
|
|
+ PageBanner,
|
|
|
+ FamilyEnergyBar
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 移除 teacher 路由分支**
|
|
|
+
|
|
|
+修改前(约第 97 行):
|
|
|
+```html
|
|
|
+<teacher-index v-else-if="currentRole === 'teacher'" />
|
|
|
+```
|
|
|
+
|
|
|
+修改后:删除该行。
|
|
|
+
|
|
|
+- [ ] **Step 4: LSP 诊断**
|
|
|
+
|
|
|
+运行:`lsp_diagnostics(filePath="cfc-frontend/pages/index/index.vue")`
|
|
|
+预期:clean(TeacherIndex 引用全部移除)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: teacher/index.vue — 软废弃
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/pages/teacher/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 文件首部添加废弃标记**
|
|
|
+
|
|
|
+在 `<template>` 上方加注释:
|
|
|
+```html
|
|
|
+<!--
|
|
|
+ ⚠️ 废弃说明(2026-06-15):
|
|
|
+ 此页面已按照设计决策废弃。教师首页功能已折入 parent-index。
|
|
|
+ 保留文件仅用于参考,不通过任何路由访问。
|
|
|
+-->
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 替换 `<template>` 为废弃提示页**
|
|
|
+
|
|
|
+将整个 template 内容替换为:
|
|
|
+```html
|
|
|
+<template>
|
|
|
+ <view class="deprecated-container">
|
|
|
+ <view class="deprecated-card">
|
|
|
+ <text class="deprecated-icon">📋</text>
|
|
|
+ <text class="deprecated-title">服务商功能已整合</text>
|
|
|
+ <text class="deprecated-desc">服务商工具和待办已迁移至家长首页,请从首页进入</text>
|
|
|
+ <view class="deprecated-btn" @click="goToHome">回到首页</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 精简 script**
|
|
|
+
|
|
|
+保留 `goToHome` 方法,移除其他所有业务逻辑:
|
|
|
+```javascript
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ methods: {
|
|
|
+ goToHome() {
|
|
|
+ uni.switchTab({ url: '/pages/index/index' })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 添加废弃提示页样式**
|
|
|
+
|
|
|
+```css
|
|
|
+<style scoped>
|
|
|
+.deprecated-container {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #f5f7fa;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 60rpx;
|
|
|
+}
|
|
|
+.deprecated-card {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 24rpx;
|
|
|
+ padding: 80rpx 60rpx;
|
|
|
+ text-align: center;
|
|
|
+ box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
|
|
|
+}
|
|
|
+.deprecated-icon {
|
|
|
+ font-size: 80rpx;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+.deprecated-title {
|
|
|
+ font-size: 36rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+.deprecated-desc {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #999;
|
|
|
+ margin-bottom: 40rpx;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+.deprecated-btn {
|
|
|
+ display: inline-block;
|
|
|
+ background: #F97316;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ padding: 20rpx 60rpx;
|
|
|
+ border-radius: 40rpx;
|
|
|
+}
|
|
|
+.deprecated-btn:active { opacity: 0.8; }
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: 确认 teacher 子包路由保留**
|
|
|
+
|
|
|
+teacher 子包页面(families/team/messages/orders 等)作为功能页面仍可通过导航访问,仅首页废弃。**pages.json 中 teacher subPackage 无需修改。**
|
|
|
+
|
|
|
+- [ ] **Step 6: LSP 诊断**
|
|
|
+
|
|
|
+运行:`lsp_diagnostics(filePath="cfc-frontend/pages/teacher/index.vue")`
|
|
|
+预期:clean
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 3 — Tab页游客模式改造 (P1 🟠)
|
|
|
+
|
|
|
+### Task 5: body/index.vue — 游客可见商品/活动 + 修复 BASE_URL
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/pages/body/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: DimensionActivities 和 DimensionProducts 移除 `v-if="isLoggedIn"`**
|
|
|
+
|
|
|
+修改前(约第 94-107 行):
|
|
|
+```html
|
|
|
+<DimensionActivities
|
|
|
+ v-if="isLoggedIn"
|
|
|
+ dimensionCode="body"
|
|
|
+ :activities="dimensionActivities"
|
|
|
+ @activityClick="goActivityDetail"
|
|
|
+ @moreActivities="goMoreActivities" />
|
|
|
+<DimensionProducts
|
|
|
+ v-if="isLoggedIn"
|
|
|
+ dimensionCode="body"
|
|
|
+ :products="dimensionProducts"
|
|
|
+ @productClick="goProductDetail"
|
|
|
+ @moreProducts="goMoreProducts" />
|
|
|
+<DimensionTasks
|
|
|
+ v-if="isLoggedIn"
|
|
|
+ dimensionCode="body"
|
|
|
+ :tasks="todayTasks"
|
|
|
+ @taskClick="goTaskDetail" />
|
|
|
+```
|
|
|
+
|
|
|
+修改后:
|
|
|
+```html
|
|
|
+<DimensionActivities
|
|
|
+ :isLoggedIn="isLoggedIn"
|
|
|
+ dimensionCode="body"
|
|
|
+ :activities="dimensionActivities"
|
|
|
+ @activityClick="goActivityDetail"
|
|
|
+ @moreActivities="goMoreActivities" />
|
|
|
+<DimensionProducts
|
|
|
+ :isLoggedIn="isLoggedIn"
|
|
|
+ dimensionCode="body"
|
|
|
+ :products="dimensionProducts"
|
|
|
+ @productClick="goProductDetail"
|
|
|
+ @moreProducts="goMoreProducts" />
|
|
|
+<DimensionTasks
|
|
|
+ v-if="isLoggedIn"
|
|
|
+ dimensionCode="body"
|
|
|
+ :tasks="todayTasks"
|
|
|
+ @taskClick="goTaskDetail" />
|
|
|
+```
|
|
|
+
|
|
|
+变更:Activities/Products 不再依赖 `isLoggedIn` 控制渲染;传入 `:isLoggedIn="isLoggedIn"` 让组件内部处理价格/提示切换。Tasks 仍保留 `v-if="isLoggedIn"`。
|
|
|
+
|
|
|
+- [ ] **Step 2: onShow 中游客也加载活动和商品数据**
|
|
|
+
|
|
|
+修改前(onShow 约第 130 行):
|
|
|
+```javascript
|
|
|
+onShow() {
|
|
|
+ var token = uni.getStorageSync('token')
|
|
|
+ this.isLoggedIn = !!token
|
|
|
+ if (!this.isLoggedIn) { this.loading = false; return }
|
|
|
+ // ... 登录后的数据加载
|
|
|
+ this.loadDimensionActivities()
|
|
|
+ this.loadDimensionProducts()
|
|
|
+ this.loadTodayTasks()
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+修改后:
|
|
|
+```javascript
|
|
|
+onShow() {
|
|
|
+ var token = uni.getStorageSync('token')
|
|
|
+ this.isLoggedIn = !!token
|
|
|
+ // 游客也加载活动和商品
|
|
|
+ this.loadDimensionActivities()
|
|
|
+ this.loadDimensionProducts()
|
|
|
+ if (this.isLoggedIn) {
|
|
|
+ this.loadTodayTasks()
|
|
|
+ // ...其他登录后数据
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 修复 BASE_URL 硬编码**
|
|
|
+
|
|
|
+修改前(约第 150 行):
|
|
|
+```javascript
|
|
|
+const BASE_URL = 'http://localhost:8080'
|
|
|
+```
|
|
|
+
|
|
|
+修改后:
|
|
|
+```javascript
|
|
|
+import config from '../../config'
|
|
|
+const BASE_URL = config.baseUrl || config.api('')
|
|
|
+```
|
|
|
+
|
|
|
+注:如果该文件使用的是 `uni.request` 直连而非 api.js 封装,this 就是修复;如果实际使用的是 `utils/api.js` 中的封装方法,则此步可跳过(仅删除硬编码行)。
|
|
|
+
|
|
|
+搜索确认 `BASE_URL` 变量在文件中的实际使用方式。
|
|
|
+
|
|
|
+- [ ] **Step 4: LSP 诊断**
|
|
|
+
|
|
|
+运行:`lsp_diagnostics(filePath="cfc-frontend/pages/body/index.vue")`
|
|
|
+预期:clean
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: mind/index.vue — FamilyEnergyBar 堆叠图 + 游客可见商品/活动
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/pages/mind/index.vue`
|
|
|
+- (FamilyEnergyBar 组件修改在 Task 12)
|
|
|
+
|
|
|
+- [ ] **Step 1: FamilyEnergyBar 传入 dualDimension 堆叠参数**
|
|
|
+
|
|
|
+修改前(约第 20-24 行):
|
|
|
+```html
|
|
|
+<FamilyEnergyBar
|
|
|
+ v-if="isLoggedIn"
|
|
|
+ dimensionCode="mind"
|
|
|
+ :sandboxData="sandboxData"
|
|
|
+ :dualDimension="dualDimension" />
|
|
|
+```
|
|
|
+
|
|
|
+修改后:
|
|
|
+```html
|
|
|
+<FamilyEnergyBar
|
|
|
+ v-if="isLoggedIn"
|
|
|
+ dimensionCode="mind"
|
|
|
+ :sandboxData="sandboxData"
|
|
|
+ :dualDimension="dualDimensionData" />
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 在 data 中添加 dualDimensionData**
|
|
|
+
|
|
|
+```javascript
|
|
|
+data() {
|
|
|
+ return {
|
|
|
+ // ... 现有 data
|
|
|
+ dualDimensionData: null // { primary: {score,label,color}, secondary: {score,label,color} }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 修改 loadEnergyData 方法构建堆叠数据**
|
|
|
+
|
|
|
+在 `methods` 中找到 `loadEnergyData`(约第 662-680 行),修改为从 API 返回的 dimensions 数组中提取 mind + wisdom 两个维度的数据:
|
|
|
+
|
|
|
+```javascript
|
|
|
+loadEnergyData: function() {
|
|
|
+ var self = this
|
|
|
+ if (!this.currentChildId) return
|
|
|
+ getEnergyOverview(this.currentChildId).then(function(res) {
|
|
|
+ if (res && res.data) {
|
|
|
+ var dims = res.data.dimensions || []
|
|
|
+ var mindDim = null
|
|
|
+ var wisdomDim = null
|
|
|
+ for (var i = 0; i < dims.length; i++) {
|
|
|
+ if (dims[i].code === 'mind') mindDim = dims[i]
|
|
|
+ if (dims[i].code === 'wisdom') wisdomDim = dims[i]
|
|
|
+ }
|
|
|
+ self.dualDimensionData = {
|
|
|
+ primary: {
|
|
|
+ code: 'mind',
|
|
|
+ score: (mindDim && mindDim.score) || 0,
|
|
|
+ label: '心理',
|
|
|
+ color: '#FF6B35'
|
|
|
+ },
|
|
|
+ secondary: {
|
|
|
+ code: 'wisdom',
|
|
|
+ score: (wisdomDim && wisdomDim.score) || 0,
|
|
|
+ label: '认知',
|
|
|
+ color: '#8B5CF6'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }).catch(function(e) {
|
|
|
+ console.log('获取能量概览失败', e)
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: DimensionActivities/Products 移除 `v-if="isLoggedIn"`(同 Task 5 Step 1)**
|
|
|
+
|
|
|
+修改前(约第 261-280 行):
|
|
|
+```html
|
|
|
+<DimensionTasks v-if="isLoggedIn" ... />
|
|
|
+<DimensionActivities v-if="isLoggedIn" ... />
|
|
|
+<DimensionProducts v-if="isLoggedIn" ... />
|
|
|
+```
|
|
|
+
|
|
|
+修改后:
|
|
|
+```html
|
|
|
+<DimensionTasks v-if="isLoggedIn" ... />
|
|
|
+<DimensionActivities :isLoggedIn="isLoggedIn" ... />
|
|
|
+<DimensionProducts :isLoggedIn="isLoggedIn" ... />
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: onShow 中游客也加载活动和商品数据**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 在 token 检查外也调用:
|
|
|
+this.loadDimensionActivities()
|
|
|
+this.loadDimensionProducts()
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 6: 新增认知六维雷达图区块**
|
|
|
+
|
|
|
+在能量柱下方增加认知能力六维雷达图。使用 `<RadarChart>` 组件(Task 17 创建),传入 6 个维度数据。
|
|
|
+
|
|
|
+所需 API:`GET /api/assessment/cognitive-profile?childId={childId}`(需登录)
|
|
|
+
|
|
|
+模板位置(能量柱下方):
|
|
|
+```html
|
|
|
+<!-- ===== 认知能力雷达图 ===== -->
|
|
|
+<view class="section" v-if="isLoggedIn && cognitiveData">
|
|
|
+ <view class="section-header">
|
|
|
+ <text class="section-title">🧠 认知能力</text>
|
|
|
+ </view>
|
|
|
+ <RadarChart
|
|
|
+ :dimensions="cognitiveDimensions"
|
|
|
+ :scores="cognitiveScores"
|
|
|
+ :maxScore="100"
|
|
|
+ :label="'认知能力评估'"
|
|
|
+ :width="600"
|
|
|
+ :height="600" />
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+在 data 中增加:
|
|
|
+```javascript
|
|
|
+cognitiveData: null,
|
|
|
+cognitiveDimensions: [
|
|
|
+ { key: 'sensation', label: '感知觉' },
|
|
|
+ { key: 'attention', label: '注意力' },
|
|
|
+ { key: 'memory', label: '记忆力' },
|
|
|
+ { key: 'logic', label: '逻辑推理' },
|
|
|
+ { key: 'spatial', label: '空间想象' },
|
|
|
+ { key: 'speed', label: '加工速度' }
|
|
|
+],
|
|
|
+cognitiveScores: []
|
|
|
+```
|
|
|
+
|
|
|
+在 methods 中添加 `loadCognitiveData`:
|
|
|
+```javascript
|
|
|
+loadCognitiveData: function() {
|
|
|
+ if (!this.currentChildId) return
|
|
|
+ var self = this
|
|
|
+ // 调用认知能力 API(如不存在则显示模拟占位)
|
|
|
+ uni.request({
|
|
|
+ url: '/api/assessment/cognitive-profile',
|
|
|
+ method: 'POST',
|
|
|
+ data: { childId: this.currentChildId },
|
|
|
+ success: function(res) {
|
|
|
+ if (res.data && res.data.code === 200 && res.data.data) {
|
|
|
+ var data = res.data.data
|
|
|
+ self.cognitiveScores = [
|
|
|
+ data.sensation || 0,
|
|
|
+ data.attention || 0,
|
|
|
+ data.memory || 0,
|
|
|
+ data.logic || 0,
|
|
|
+ data.spatial || 0,
|
|
|
+ data.speed || 0
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: function() {
|
|
|
+ // API 未就绪时静默降级
|
|
|
+ self.cognitiveData = null
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+在 `onShow` 的登录分支中调用 `this.loadCognitiveData()`。
|
|
|
+
|
|
|
+- [ ] **Step 7: 新增大五人格图区块(仅 10 岁以上,需 DAN 测评)**
|
|
|
+
|
|
|
+在认知雷达图下方增加大五人格图。使用 `<RadarChart>` 组件(Task 17),5 边形显示 OCEAN 五个维度。仅在孩子年龄 >= 10 岁且有 DAN 测评结果时显示。
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- ===== 大五人格 ===== -->
|
|
|
+<view class="section" v-if="isLoggedIn && bigFiveData && showBigFive">
|
|
|
+ <view class="section-header">
|
|
|
+ <text class="section-title">🌟 大五人格</text>
|
|
|
+ <text class="section-subtitle">10岁以上 · 基于DAN测评</text>
|
|
|
+ </view>
|
|
|
+ <RadarChart
|
|
|
+ :dimensions="bigFiveDimensions"
|
|
|
+ :scores="bigFiveScores"
|
|
|
+ :maxScore="100"
|
|
|
+ :label="'大五人格剖面'"
|
|
|
+ :width="600"
|
|
|
+ :height="600" />
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+data 中增加:
|
|
|
+```javascript
|
|
|
+bigFiveData: null,
|
|
|
+showBigFive: false,
|
|
|
+bigFiveDimensions: [
|
|
|
+ { key: 'openness', label: '开放性' },
|
|
|
+ { key: 'conscientiousness', label: '尽责性' },
|
|
|
+ { key: 'extraversion', label: '外向性' },
|
|
|
+ { key: 'agreeableness', label: '宜人性' },
|
|
|
+ { key: 'neuroticism', label: '神经质' }
|
|
|
+],
|
|
|
+bigFiveScores: []
|
|
|
+```
|
|
|
+
|
|
|
+在 methods 中添加 `loadBigFiveData`(从 DAN 测评结果读取):
|
|
|
+```javascript
|
|
|
+loadBigFiveData: function() {
|
|
|
+ if (!this.currentChildId) return
|
|
|
+ var self = this
|
|
|
+ // 从 DAN 测评结果获取大五人格数据
|
|
|
+ uni.request({
|
|
|
+ url: '/api/assessment/dan/big-five',
|
|
|
+ method: 'POST',
|
|
|
+ data: { childId: this.currentChildId },
|
|
|
+ success: function(res) {
|
|
|
+ if (res.data && res.data.code === 200 && res.data.data) {
|
|
|
+ var data = res.data.data
|
|
|
+ self.bigFiveScores = [
|
|
|
+ data.openness || 0,
|
|
|
+ data.conscientiousness || 0,
|
|
|
+ data.extraversion || 0,
|
|
|
+ data.agreeableness || 0,
|
|
|
+ data.neuroticism || 0
|
|
|
+ ]
|
|
|
+ self.showBigFive = true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: function() {
|
|
|
+ self.showBigFive = false
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 8: 新增关系图区块**
|
|
|
+
|
|
|
+在布局顺序中(能量柱 → 雷达图 → 大五 → **关系图** → 任务 → 活动 → 商品),关系图区块展示家庭关系或认知关联概览。具体实现待实施时根据数据可用性确定。
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- ===== 关系图 ===== -->
|
|
|
+<view class="section" v-if="isLoggedIn">
|
|
|
+ <view class="section-header">
|
|
|
+ <text class="section-title">🔗 关系图谱</text>
|
|
|
+ </view>
|
|
|
+ <!-- 待实施时根据后端数据确定具体展示 -->
|
|
|
+ <view class="relationship-placeholder">
|
|
|
+ <text class="placeholder-text">关系数据加载中...</text>
|
|
|
+ </view>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 9: 调整页面布局顺序**
|
|
|
+
|
|
|
+确保心智页的区块排列遵循:**能量柱 → 认知雷达图 → 大五人格图 → 关系图 → 任务 → 活动 → 商品**
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- 布局顺序(从顶到底) -->
|
|
|
+<FamilyEnergyBar ... /> <!-- 1. 能量柱 -->
|
|
|
+<RadarChart v-if="... cognitive" ... /> <!-- 2. 认知雷达图 -->
|
|
|
+<RadarChart v-if="... bigFive" ... /> <!-- 3. 大五人格 -->
|
|
|
+<!-- 关系图 --> <!-- 4. 关系图 -->
|
|
|
+<DimensionTasks v-if="isLoggedIn" ... /> <!-- 5. 任务 -->
|
|
|
+<DimensionActivities ... /> <!-- 6. 活动 -->
|
|
|
+<DimensionProducts ... /> <!-- 7. 商品 -->
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 10: LSP 诊断**
|
|
|
+
|
|
|
+运行:`lsp_diagnostics(filePath="cfc-frontend/pages/mind/index.vue")`
|
|
|
+预期:clean
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 7: action/index.vue — 关系雷达图 + 游客可见商品/活动 + 修复 funcList 商城路径
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/pages/action/index.vue`
|
|
|
+
|
|
|
+**设计说明(2026-06-15 确认):** 行远页核心展示六类人际关系雷达图——与孩子、另一半、父母、同伴、师长、领导的关系。数据从 Contact 关系表读取(多维度评分:亲密度、信任度、沟通频率等),使用 `<RadarChart>` 组件渲染。
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/pages/action/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: DimensionActivities/Products 移除 `v-if="isLoggedIn"`**
|
|
|
+
|
|
|
+修改前(约第 37-56 行):
|
|
|
+```html
|
|
|
+<DimensionTasks v-if="isLoggedIn" ... />
|
|
|
+<DimensionActivities v-if="isLoggedIn" ... />
|
|
|
+<DimensionProducts v-if="isLoggedIn" ... />
|
|
|
+```
|
|
|
+
|
|
|
+修改后:
|
|
|
+```html
|
|
|
+<DimensionTasks v-if="isLoggedIn" ... />
|
|
|
+<DimensionActivities :isLoggedIn="isLoggedIn" ... />
|
|
|
+<DimensionProducts :isLoggedIn="isLoggedIn" ... />
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: onShow 中游客也加载活动和商品数据**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 同 Task 5 Step 2
|
|
|
+this.loadDimensionActivities()
|
|
|
+this.loadDimensionProducts()
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 修复 funcList 中商城路径**
|
|
|
+
|
|
|
+修改前(约第 141 行):
|
|
|
+```javascript
|
|
|
+{ icon: '\u{1F6CD}', label: '商城', needLogin: false, page: '/pages/shop/index' }
|
|
|
+```
|
|
|
+
|
|
|
+修改后(确认实际商城入口页路径):
|
|
|
+```javascript
|
|
|
+{ icon: '\u{1F6CD}', label: '商城', needLogin: false, page: '/pages/discover/index?type=product' }
|
|
|
+```
|
|
|
+
|
|
|
+**注:** 需先确认 `pages.json` 中商城实际入口页路径。可能的路径有:
|
|
|
+- `pages/discover/index`(发现页带 type 参数)
|
|
|
+- `pages/shop/detail/detail`
|
|
|
+- 或改为调用 `goProductList` 方法
|
|
|
+
|
|
|
+搜索 `pages.json` 确认正确路径。
|
|
|
+
|
|
|
+- [ ] **Step 4: 新增六类关系雷达图区块**
|
|
|
+
|
|
|
+在页面顶部(FamilyEnergyBar 下方)增加六类人际关系雷达图。每个关系显示为一个雷达图,维度包括:亲密度、信任度、沟通频率、支持度、冲突处理等。
|
|
|
+
|
|
|
+数据来源:Contact 关系表,通过 `GET /api/contact/relationships` 接口获取(需登录)。
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- ===== 六类关系雷达图 ===== -->
|
|
|
+<view class="section" v-if="isLoggedIn">
|
|
|
+ <view class="section-header">
|
|
|
+ <text class="section-title">🤝 人际关系</text>
|
|
|
+ </view>
|
|
|
+ <scroll-view scroll-x class="relationship-scroll" show-scrollbar="false">
|
|
|
+ <view class="relationship-card" v-for="(rel, idx) in relationships" :key="idx" @click="goRelationshipDetail(rel.type)">
|
|
|
+ <view class="rel-header">
|
|
|
+ <text class="rel-icon">{{ rel.icon }}</text>
|
|
|
+ <text class="rel-type">{{ rel.label }}</text>
|
|
|
+ </view>
|
|
|
+ <RadarChart
|
|
|
+ :dimensions="rel.dimensions"
|
|
|
+ :scores="rel.scores"
|
|
|
+ :maxScore="5"
|
|
|
+ :width="280"
|
|
|
+ :height="280"
|
|
|
+ :showLabels="false" />
|
|
|
+ <text class="rel-score">综合 {{ rel.totalScore }}分</text>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+在 data 中增加:
|
|
|
+```javascript
|
|
|
+relationships: [],
|
|
|
+relationshipTypes: [
|
|
|
+ { type: 'child', label: '与孩子的关系', icon: '👶' },
|
|
|
+ { type: 'spouse', label: '与另一半的关系', icon: '💑' },
|
|
|
+ { type: 'parent', label: '与父母的关系', icon: '👴' },
|
|
|
+ { type: 'peer', label: '与同伴的关系', icon: '🤝' },
|
|
|
+ { type: 'teacher', label: '与师长的关系', icon: '👨🏫' },
|
|
|
+ { type: 'leader', label: '与领导的关系', icon: '👔' }
|
|
|
+]
|
|
|
+```
|
|
|
+
|
|
|
+在 methods 中添加 `loadRelationships`:
|
|
|
+```javascript
|
|
|
+loadRelationships: function() {
|
|
|
+ var self = this
|
|
|
+ // 从 Contact 关系表获取数据
|
|
|
+ uni.request({
|
|
|
+ url: '/api/contact/relationships',
|
|
|
+ method: 'POST',
|
|
|
+ success: function(res) {
|
|
|
+ if (res.data && res.data.code === 200 && res.data.data) {
|
|
|
+ var data = res.data.data
|
|
|
+ self.relationships = self.relationshipTypes.map(function(typeDef) {
|
|
|
+ var relData = data[typeDef.type] || {}
|
|
|
+ return {
|
|
|
+ type: typeDef.type,
|
|
|
+ icon: typeDef.icon,
|
|
|
+ label: typeDef.label,
|
|
|
+ dimensions: [
|
|
|
+ { key: 'intimacy', label: '亲密度' },
|
|
|
+ { key: 'trust', label: '信任度' },
|
|
|
+ { key: 'communication', label: '沟通频率' },
|
|
|
+ { key: 'support', label: '支持度' },
|
|
|
+ { key: 'conflict', label: '冲突处理' }
|
|
|
+ ],
|
|
|
+ scores: [
|
|
|
+ relData.intimacy || 0,
|
|
|
+ relData.trust || 0,
|
|
|
+ relData.communication || 0,
|
|
|
+ relData.support || 0,
|
|
|
+ relData.conflict || 0
|
|
|
+ ],
|
|
|
+ totalScore: relData.totalScore || 0
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: function() {
|
|
|
+ // API 未就绪时用空数组
|
|
|
+ self.relationships = []
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+在 `onShow` 登录分支中调用 `this.loadRelationships()`。
|
|
|
+
|
|
|
+- [ ] **Step 5: 调整页面布局顺序**
|
|
|
+
|
|
|
+行远页布局顺序:**关系雷达图 → 任务 → 活动 → 商品**
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- 布局顺序(从顶到底) -->
|
|
|
+<FamilyEnergyBar ... /> <!-- 1. 能量柱 -->
|
|
|
+<!-- 关系雷达图(横向滚动) --> <!-- 2. 六类关系 -->
|
|
|
+<DimensionTasks v-if="isLoggedIn" ... /> <!-- 3. 任务 -->
|
|
|
+<DimensionActivities ... /> <!-- 4. 活动 -->
|
|
|
+<DimensionProducts ... /> <!-- 5. 商品 -->
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 6: LSP 诊断**
|
|
|
+
|
|
|
+运行:`lsp_diagnostics(filePath="cfc-frontend/pages/action/index.vue")`
|
|
|
+预期:clean
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 4 — 首页改造 (P1 🟠)
|
|
|
+
|
|
|
+### Task 8: parent-index.vue — 保留沙盘 + 新增 FamilyEnergyBar + 服务商入口 + 维度区块
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/pages/index/parent-index.vue`
|
|
|
+
|
|
|
+**设计决策(2026-06-15 确认):** 五行沙盘为首页必要组件,**保留不动**。本 Task 只做增量改造——增加 FamilyEnergyBar、服务商快捷入口、维度商品/活动区块。
|
|
|
+
|
|
|
+- [ ] **Step 1: [无需操作] 五行沙盘保留不动**
|
|
|
+
|
|
|
+⚠️ 已有 import `import WuxingSandbox from '@/components/wuxing-sandbox.vue'`、components 注册、模板中的 `<wuxing-sandbox>` 实例全部保留。**不要删除任何沙盘相关代码。**
|
|
|
+
|
|
|
+- [ ] **Step 2: 在 Banner 下方增加 FamilyEnergyBar**
|
|
|
+
|
|
|
+在 banner 或顶部区域后增加:
|
|
|
+```html
|
|
|
+<!-- 家庭能量横条 -->
|
|
|
+<FamilyEnergyBar
|
|
|
+ v-if="hasFamilyData"
|
|
|
+ dimensionCode="parent"
|
|
|
+ :sandboxData="sandboxData" />
|
|
|
+```
|
|
|
+
|
|
|
+添加 import:
|
|
|
+```javascript
|
|
|
+import FamilyEnergyBar from '@/components/FamilyEnergyBar.vue'
|
|
|
+```
|
|
|
+
|
|
|
+在 components 中添加 `FamilyEnergyBar`。
|
|
|
+
|
|
|
+- [ ] **Step 3: 增加服务商快捷入口**
|
|
|
+
|
|
|
+在「待处理事项」区域后增加(当 `isTeacher` 为 true 时显示):
|
|
|
+```html
|
|
|
+<!-- ===== 服务商快捷入口 ===== -->
|
|
|
+<view class="teacher-quick-section" v-if="isTeacher">
|
|
|
+ <view class="section-title">🔧 服务商工具</view>
|
|
|
+ <view class="pending-grid">
|
|
|
+ <view class="pending-card" @click="goToTeacherFamilies">
|
|
|
+ <view class="pending-icon pending-icon--blue">
|
|
|
+ <text class="pending-emoji">👨👩👧</text>
|
|
|
+ </view>
|
|
|
+ <view class="pending-info">
|
|
|
+ <text class="pending-count accent-color">{{ serviceFamilyCount }}</text>
|
|
|
+ <text class="pending-label">服务家庭</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="pending-card" @click="goToAssignTask">
|
|
|
+ <view class="pending-icon pending-icon--green">
|
|
|
+ <text class="pending-emoji">📝</text>
|
|
|
+ </view>
|
|
|
+ <view class="pending-info">
|
|
|
+ <text class="pending-count">{{ pendingTaskCount }}</text>
|
|
|
+ <text class="pending-label">待办任务</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="pending-card" @click="goToIncome">
|
|
|
+ <view class="pending-icon pending-icon--gold">
|
|
|
+ <text class="pending-emoji">💰</text>
|
|
|
+ </view>
|
|
|
+ <view class="pending-info">
|
|
|
+ <text class="pending-count">¥{{ todayIncome }}</text>
|
|
|
+ <text class="pending-label">今日收入</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="pending-card" @click="goToTeam">
|
|
|
+ <view class="pending-icon pending-icon--purple">
|
|
|
+ <text class="pending-emoji">👥</text>
|
|
|
+ </view>
|
|
|
+ <view class="pending-info">
|
|
|
+ <text class="pending-count">{{ teamCount }}</text>
|
|
|
+ <text class="pending-label">团队</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+在 data 中增加字段:
|
|
|
+```javascript
|
|
|
+isTeacher: false,
|
|
|
+serviceFamilyCount: 0,
|
|
|
+pendingTaskCount: 0,
|
|
|
+todayIncome: 0,
|
|
|
+teamCount: 0
|
|
|
+```
|
|
|
+
|
|
|
+在 `onShow` 中检查教师身份并加载对应数据:
|
|
|
+```javascript
|
|
|
+// 检查当前用户角色
|
|
|
+var roles = uni.getStorageSync('roles') || ''
|
|
|
+this.isTeacher = roles.indexOf('teacher') !== -1
|
|
|
+if (this.isTeacher) {
|
|
|
+ this.loadTeacherDashboard()
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+添加 `loadTeacherDashboard` 方法(调用教师统计 API 或使用默认值)。
|
|
|
+
|
|
|
+- [ ] **Step 4: 增加维度商品/活动推荐区块**
|
|
|
+
|
|
|
+在「成长建议」区域后增加:
|
|
|
+```html
|
|
|
+<!-- ===== 推荐活动 ===== -->
|
|
|
+<DimensionActivities
|
|
|
+ dimensionCode="all"
|
|
|
+ :isLoggedIn="isLoggedIn"
|
|
|
+ :activities="recommendedActivities"
|
|
|
+ @activityClick="goActivityDetail"
|
|
|
+ @moreActivities="goMoreActivities" />
|
|
|
+
|
|
|
+<!-- ===== 推荐商品 ===== -->
|
|
|
+<DimensionProducts
|
|
|
+ dimensionCode="all"
|
|
|
+ :isLoggedIn="isLoggedIn"
|
|
|
+ :products="recommendedProducts"
|
|
|
+ @productClick="goProductDetail"
|
|
|
+ @moreProducts="goMoreProducts" />
|
|
|
+```
|
|
|
+
|
|
|
+添加 import:
|
|
|
+```javascript
|
|
|
+import DimensionActivities from '@/components/DimensionActivities.vue'
|
|
|
+import DimensionProducts from '@/components/DimensionProducts.vue'
|
|
|
+```
|
|
|
+
|
|
|
+在 data 中增加:
|
|
|
+```javascript
|
|
|
+recommendedActivities: [],
|
|
|
+recommendedProducts: []
|
|
|
+```
|
|
|
+
|
|
|
+在 `onShow` 登录分支中调用加载方法。
|
|
|
+
|
|
|
+- [ ] **Step 5: LSP 诊断**
|
|
|
+
|
|
|
+运行:`lsp_diagnostics(filePath="cfc-frontend/pages/index/parent-index.vue")`
|
|
|
+预期:clean
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 9: child-index.vue — FamilyEnergyBar + 维度商品/活动 + reLaunch→switchTab
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/pages/index/child-index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 在 Banner 下方增加 FamilyEnergyBar**
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- 家庭能量横条 -->
|
|
|
+<FamilyEnergyBar
|
|
|
+ v-if="hasChildren"
|
|
|
+ dimensionCode="child"
|
|
|
+ :sandboxData="sandboxData" />
|
|
|
+```
|
|
|
+
|
|
|
+添加 import:
|
|
|
+```javascript
|
|
|
+import FamilyEnergyBar from '@/components/FamilyEnergyBar.vue'
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 增加维度商品/活动区块**
|
|
|
+
|
|
|
+在「最近能量变动」下方增加:
|
|
|
+```html
|
|
|
+<!-- ===== 推荐活动 ===== -->
|
|
|
+<DimensionActivities
|
|
|
+ v-if="hasChildren"
|
|
|
+ dimensionCode="child"
|
|
|
+ :isLoggedIn="isLoggedIn"
|
|
|
+ :activities="dimensionActivities"
|
|
|
+ @activityClick="goActivityDetail"
|
|
|
+ @moreActivities="goMoreActivities" />
|
|
|
+
|
|
|
+<!-- ===== 推荐商品 ===== -->
|
|
|
+<DimensionProducts
|
|
|
+ v-if="hasChildren"
|
|
|
+ dimensionCode="child"
|
|
|
+ :isLoggedIn="isLoggedIn"
|
|
|
+ :products="dimensionProducts"
|
|
|
+ @productClick="goProductDetail"
|
|
|
+ @moreProducts="goMoreProducts" />
|
|
|
+```
|
|
|
+
|
|
|
+添加 import:
|
|
|
+```javascript
|
|
|
+import DimensionActivities from '@/components/DimensionActivities.vue'
|
|
|
+import DimensionProducts from '@/components/DimensionProducts.vue'
|
|
|
+```
|
|
|
+
|
|
|
+data 中增加:
|
|
|
+```javascript
|
|
|
+dimensionActivities: [],
|
|
|
+dimensionProducts: []
|
|
|
+```
|
|
|
+
|
|
|
+在 `loadData` 中调用加载方法。
|
|
|
+
|
|
|
+- [ ] **Step 3: reLaunch 改 switchTab(约第 432 行)**
|
|
|
+
|
|
|
+修改前:
|
|
|
+```javascript
|
|
|
+uni.reLaunch({ url: '/pages/index/parent-index' })
|
|
|
+```
|
|
|
+
|
|
|
+修改后:
|
|
|
+```javascript
|
|
|
+uni.switchTab({ url: '/pages/index/index' })
|
|
|
+```
|
|
|
+
|
|
|
+**原因:** `switchTab` 不会销毁当前页面栈,保留 TabBar 底部导航状态。`reLaunch` 会清空页面栈导致异常。
|
|
|
+
|
|
|
+- [ ] **Step 4: LSP 诊断**
|
|
|
+
|
|
|
+运行:`lsp_diagnostics(filePath="cfc-frontend/pages/index/child-index.vue")`
|
|
|
+预期:clean
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 10: index.vue — 未登录态商品接口置换 + 隐藏价格 + 弹登录
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/pages/index/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 未登录态商品列表改用 `/api/product/list`**
|
|
|
+
|
|
|
+找到未登录态的商品加载代码(约第 200 行),确认当前调用方式。如果使用 `productList()`(DanShop旧接口),改为调用 api.js 中封装的 `/api/product/list`:
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 修改前
|
|
|
+import { productList } from '../../utils/api'
|
|
|
+productList(params).then(res => { ... })
|
|
|
+
|
|
|
+// 修改后
|
|
|
+import { getProductList } from '../../utils/api'
|
|
|
+getProductList({ page: 1, limit: 10 }).then(res => {
|
|
|
+ if (res.code === 200) {
|
|
|
+ self.products = res.data.records || res.data.list || []
|
|
|
+ }
|
|
|
+}).catch(e => { console.log(e) })
|
|
|
+```
|
|
|
+
|
|
|
+**注:** 需先确认 `utils/api.js` 中是否有 `getProductList` 方法。如果没有,在 Task 14 中原有方法名不变,只修改后端接口。
|
|
|
+
|
|
|
+- [ ] **Step 2: 未登录态商品隐藏价格**
|
|
|
+
|
|
|
+找到商品卡片模板(约第 44-48 行):
|
|
|
+```html
|
|
|
+<view class="product-price-row">
|
|
|
+ <text class="product-price">{{ formatPriceWithSymbol(item.price) }}</text>
|
|
|
+ <text v-if="item.memberPrice" class="product-member">会员{{ formatPriceWithSymbol(item.memberPrice) }}</text>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+改为(价格改为整型分→元,引入格式化函数):
|
|
|
+```html
|
|
|
+<view class="product-price-row">
|
|
|
+ <text v-if="isLoggedIn" class="product-price">{{ formatPriceWithSymbol(item.price) }}</text>
|
|
|
+ <text v-if="isLoggedIn && item.memberPrice" class="product-member">会员{{ formatPriceWithSymbol(item.memberPrice) }}</text>
|
|
|
+ <text v-else class="product-price-login">登录查看</text>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 未登录态商品点击弹登录提示**
|
|
|
+
|
|
|
+修改商品 card 的点击事件(约第 40 行):
|
|
|
+```html
|
|
|
+<!-- 修改前 -->
|
|
|
+<view class="product-card" @click="goDetail(item.id)">
|
|
|
+
|
|
|
+<!-- 修改后 -->
|
|
|
+<view class="product-card" @click="isLoggedIn ? goDetail(item.id) : handleLogin()">
|
|
|
+```
|
|
|
+
|
|
|
+添加 `handleLogin` 方法:
|
|
|
+```javascript
|
|
|
+handleLogin() {
|
|
|
+ uni.showModal({
|
|
|
+ title: '提示',
|
|
|
+ content: '请先登录后查看详情',
|
|
|
+ success: function(res) {
|
|
|
+ if (res.confirm) {
|
|
|
+ uni.navigateTo({
|
|
|
+ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/index/index')
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 确保 onShow 中游客能加载商品**
|
|
|
+
|
|
|
+```javascript
|
|
|
+onShow() {
|
|
|
+ if (!this.isLoggedIn()) {
|
|
|
+ this.currentRole = ''
|
|
|
+ this.loadProducts() // 游客也加载商品
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.syncRoleAndTabBar()
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: utils/api.js — 添加商品列表接口方法(如果不存在)**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// 在 api.js 中添加
|
|
|
+export function getProductList(params) {
|
|
|
+ return uni.request({
|
|
|
+ url: config.api('/api/product/list'),
|
|
|
+ method: 'POST',
|
|
|
+ data: params || {}
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 6: LSP 诊断**
|
|
|
+
|
|
|
+运行:`lsp_diagnostics(filePath="cfc-frontend/pages/index/index.vue")`
|
|
|
+预期:clean
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 5 — 富沛页面重做 (P1 🟠)
|
|
|
+
|
|
|
+### Task 11: pages/wealth/index.vue — 完整重构
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- **重写**: `cfc-frontend/pages/wealth/index.vue`
|
|
|
+
|
|
|
+**设计参照:** `docs/需求分析/底签页面详细设计.md` §5 富沛 (pages 500-593)
|
|
|
+
|
|
|
+**设计布局(已登录态):**
|
|
|
+```
|
|
|
+[Logo] 富沛 brand-header h:220rpx 深蓝渐变
|
|
|
+丰盈内心,富足生活,厚德载物
|
|
|
+🔥 富沛能量: 1,500
|
|
|
+
|
|
|
+👤 [头像] 张三 家长模式 [切换] user-card
|
|
|
+🏅 系统积分: 500
|
|
|
+💧 富沛能量: 1,500 ↑较上周+120
|
|
|
+
|
|
|
+📊 财富数据 section-header
|
|
|
+⭐ 总积分: 2,580
|
|
|
+📈 富沛能量增长趋势 迷你柱状/折线
|
|
|
+[📋 积分记录]
|
|
|
+
|
|
|
+🏅 勋章墙 section-header
|
|
|
+[已获得][已获得][已获得] badge-grid 3×3
|
|
|
+[灰色 ][灰色 ][已获得] (亮色=已获得)
|
|
|
+[查看全部 ›]
|
|
|
+
|
|
|
+📋 功能菜单 按分组排列
|
|
|
+── 个人 ──
|
|
|
+📝 修改个人资料
|
|
|
+👶 我的孩子 (家长)
|
|
|
+── 财富 ──
|
|
|
+💝 心愿单 / 📢 推广中心
|
|
|
+── 服务 ──
|
|
|
+👑 会员中心 / 🏪 服务商中心
|
|
|
+── 设置 ──
|
|
|
+🔐 设置密码 / 邀请 / 退出登录
|
|
|
+
|
|
|
+📈 成长报告 section-header(家长可见)
|
|
|
+[👶 小明综合成长报告 ...] child's name + 综合评分: 78分 ↑+5
|
|
|
+[查看完整报告 ›]
|
|
|
+```
|
|
|
+
|
|
|
+**未登录态布局:**
|
|
|
+```
|
|
|
+[Logo] 富沛 brand-header(与已登录统一)
|
|
|
+丰盈内心,富足生活,厚德载物
|
|
|
+💧 登录查看富沛能量
|
|
|
+
|
|
|
+ 👤
|
|
|
+ 登录浠艾福 login-prompt
|
|
|
+ 登录后可查看个人资料、积分记录等
|
|
|
+ [登录 / 注册]
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 1: 重写完整 template**
|
|
|
+
|
|
|
+```html
|
|
|
+<template>
|
|
|
+ <view class="container">
|
|
|
+ <!-- ===== 品牌头部 ===== -->
|
|
|
+ <view class="brand-header">
|
|
|
+ <view class="brand-content">
|
|
|
+ <text class="brand-logo">💧 富沛</text>
|
|
|
+ <text class="brand-tagline">丰盈内心,富足生活,厚德载物</text>
|
|
|
+ <text v-if="isLoggedIn" class="brand-energy">🔥 富沛能量: {{ wealthEnergy }}</text>
|
|
|
+ <text v-else class="brand-energy dim">💧 登录查看富沛能量</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- ===== 未登录态:登录引导 ===== -->
|
|
|
+ <view v-if="!isLoggedIn" class="login-prompt">
|
|
|
+ <view class="prompt-icon">👤</view>
|
|
|
+ <text class="prompt-title">登录浠艾福</text>
|
|
|
+ <text class="prompt-desc">登录后可查看个人资料、积分记录等</text>
|
|
|
+ <button class="login-btn" @click="goLogin">登录 / 注册</button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- ===== 已登录态:完整内容 ===== -->
|
|
|
+ <template v-else>
|
|
|
+ <!-- 退出切换按钮 -->
|
|
|
+ <view class="exit-switch" v-if="isSwitchedChild" @click="exitSwitch">
|
|
|
+ <text>🔄 退出切换</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 用户卡片 -->
|
|
|
+ <view class="user-card">
|
|
|
+ <view class="avatar">{{ avatarText }}</view>
|
|
|
+ <view class="user-info">
|
|
|
+ <view class="nickname">{{ nickname || '未设置昵称' }}</view>
|
|
|
+ <view class="role">{{ roleLabel }}</view>
|
|
|
+ <view class="system-points">🏅 系统积分: {{ systemPoints }}</view>
|
|
|
+ <view class="wealth-energy-line">
|
|
|
+ 💧 富沛能量: <text class="energy-value">{{ wealthEnergy }}</text>
|
|
|
+ <text class="energy-trend" v-if="energyTrend !== 0">
|
|
|
+ {{ energyTrend > 0 ? '↑' : '↓' }}较上周{{ Math.abs(energyTrend) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <button class="btn-switch" v-if="!isSwitchedChild" @click="switchMode">切换</button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 财富数据 -->
|
|
|
+ <view class="section">
|
|
|
+ <view class="section-header">
|
|
|
+ <text class="section-title">📊 财富数据</text>
|
|
|
+ </view>
|
|
|
+ <view class="wealth-card">
|
|
|
+ <view class="wealth-item">
|
|
|
+ <text class="wealth-value">{{ totalPoints }}</text>
|
|
|
+ <text class="wealth-label">总积分</text>
|
|
|
+ </view>
|
|
|
+ <view class="wealth-divider"></view>
|
|
|
+ <view class="wealth-item">
|
|
|
+ <text class="wealth-value">{{ wealthEnergy }}</text>
|
|
|
+ <text class="wealth-label">富沛能量</text>
|
|
|
+ </view>
|
|
|
+ <view class="wealth-divider"></view>
|
|
|
+ <view class="wealth-item" @click="goToPointsLogs">
|
|
|
+ <text class="wealth-value link">📋</text>
|
|
|
+ <text class="wealth-label">积分记录</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <!-- 能量增长趋势(迷你) -->
|
|
|
+ <view class="energy-trend-bar" v-if="trendDays.length > 0">
|
|
|
+ <view class="trend-item" v-for="(day, idx) in trendDays" :key="idx">
|
|
|
+ <view class="trend-bar" :style="{ height: day.percent + '%', background: '#F97316' }"></view>
|
|
|
+ <text class="trend-label">{{ day.label }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 勋章墙 -->
|
|
|
+ <view class="section">
|
|
|
+ <view class="section-header">
|
|
|
+ <text class="section-title">🏅 勋章墙</text>
|
|
|
+ <text class="section-more" @click="goAllBadges">查看全部 ›</text>
|
|
|
+ </view>
|
|
|
+ <view class="badge-grid">
|
|
|
+ <view class="badge-item" v-for="badge in badges" :key="badge.id">
|
|
|
+ <view :class="['badge-icon-wrap', badge.unlocked ? 'unlocked' : 'locked']">
|
|
|
+ <text class="badge-icon">{{ badge.icon }}</text>
|
|
|
+ </view>
|
|
|
+ <text class="badge-name">{{ badge.name }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 功能菜单(分组) -->
|
|
|
+ <view class="menu-list">
|
|
|
+ <view class="menu-group-title">── 个人 ──</view>
|
|
|
+ <view class="menu-item" @click="goToEditInfo">
|
|
|
+ <text>📝 修改个人资料</text>
|
|
|
+ <text class="arrow">›</text>
|
|
|
+ </view>
|
|
|
+ <view class="menu-item" v-if="role === 'parent'" @click="goToChildren">
|
|
|
+ <text>👶 我的孩子</text>
|
|
|
+ <text class="arrow">›</text>
|
|
|
+ </view>
|
|
|
+ <view class="menu-item" v-if="role === 'parent'" @click="goToFamilyMembers">
|
|
|
+ <text>👨👩👧👦 家庭成员</text>
|
|
|
+ <text class="arrow">›</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="menu-group-title">── 财富 ──</view>
|
|
|
+ <view class="menu-item" @click="goToWishlist">
|
|
|
+ <text>💝 心愿单</text>
|
|
|
+ <text class="arrow">›</text>
|
|
|
+ </view>
|
|
|
+ <view class="menu-item" @click="goToPromotion">
|
|
|
+ <text>📢 推广中心</text>
|
|
|
+ <text class="arrow">›</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="menu-group-title">── 服务 ──</view>
|
|
|
+ <view class="menu-item" @click="goToMembership">
|
|
|
+ <text>👑 会员中心</text>
|
|
|
+ <text class="arrow">›</text>
|
|
|
+ </view>
|
|
|
+ <view class="menu-item" v-if="isVendor" @click="goToVendorCenter">
|
|
|
+ <text>🏪 服务商中心</text>
|
|
|
+ <text class="arrow">›</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="menu-group-title">── 设置 ──</view>
|
|
|
+ <view class="menu-item" @click="showPasswordModal = true">
|
|
|
+ <text>🔐 {{ hasPassword ? '重置密码' : '设置密码' }}</text>
|
|
|
+ <text class="arrow">›</text>
|
|
|
+ </view>
|
|
|
+ <view class="menu-item menu-item-switch" v-if="role === 'parent'">
|
|
|
+ <text>👁 对家庭成员可见</text>
|
|
|
+ <switch :checked="showToFamily === 1" @change="onShowToFamilyChange" color="#5B9BD5" />
|
|
|
+ </view>
|
|
|
+ <button class="menu-item menu-item-btn" open-type="share" @click="inviteFamily" v-if="role === 'parent'">
|
|
|
+ <text>👨👩👧👦 邀请家人</text>
|
|
|
+ <text class="arrow">›</text>
|
|
|
+ </button>
|
|
|
+ <button class="menu-item menu-item-btn" open-type="share" @click="inviteTeacher" v-if="role === 'parent'">
|
|
|
+ <text>👨🏫 邀请规划师</text>
|
|
|
+ <text class="arrow">›</text>
|
|
|
+ </button>
|
|
|
+ <view class="menu-item" @click="logout">
|
|
|
+ <text>🚪 退出登录</text>
|
|
|
+ <text class="arrow">›</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 成长报告(家长可见) -->
|
|
|
+ <view class="section" v-if="role === 'parent' && growthReport">
|
|
|
+ <view class="section-header">
|
|
|
+ <text class="section-title">📈 成长报告</text>
|
|
|
+ <text class="section-more" @click="goGrowthReport">查看完整报告 ›</text>
|
|
|
+ </view>
|
|
|
+ <view class="report-card" @click="goGrowthReport">
|
|
|
+ <view class="report-child-avatar">👶</view>
|
|
|
+ <view class="report-info">
|
|
|
+ <text class="report-child-name">{{ growthReport.childName }}综合成长报告</text>
|
|
|
+ <view class="report-score-row">
|
|
|
+ <text class="report-score-label">综合评分: </text>
|
|
|
+ <text class="report-score-value">{{ growthReport.score }}分</text>
|
|
|
+ <text class="report-score-trend" v-if="growthReport.trend !== 0">
|
|
|
+ {{ growthReport.trend > 0 ? '↑' : '↓' }}{{ Math.abs(growthReport.trend) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 设置/重置密码弹窗 -->
|
|
|
+ <view class="modal-mask" v-if="showPasswordModal" @click="showPasswordModal = false">
|
|
|
+ <view class="modal" @click.stop>
|
|
|
+ <view class="modal-title">{{ hasPassword ? '重置密码' : '设置密码' }}</view>
|
|
|
+ <view class="form-item" v-if="hasPassword">
|
|
|
+ <input v-model="oldPassword" type="number" password placeholder="输入旧密码" maxlength="6" />
|
|
|
+ </view>
|
|
|
+ <view class="form-item">
|
|
|
+ <input v-model="newPassword" type="number" password placeholder="输入新密码" maxlength="6" />
|
|
|
+ </view>
|
|
|
+ <view class="form-item">
|
|
|
+ <input v-model="confirmPassword" type="number" password placeholder="确认新密码" maxlength="6" />
|
|
|
+ </view>
|
|
|
+ <view class="modal-btns">
|
|
|
+ <button @click="showPasswordModal = false">取消</button>
|
|
|
+ <button class="btn-primary" @click="savePassword">确定</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="bottom-spacer"></view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 重写 script**
|
|
|
+
|
|
|
+```javascript
|
|
|
+<script>
|
|
|
+import { getEnergyOverview, getChildren, verifyPassword, setPassword, vendorStatus, getVisibleFamilyMembers, getReferralCode, getReferralSummary, updateMemberVisibility, getPointsBalance } from '../../utils/api.js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ isLoggedIn: false,
|
|
|
+ nickname: '',
|
|
|
+ role: 'parent',
|
|
|
+ hasPassword: false,
|
|
|
+ showPasswordModal: false,
|
|
|
+ oldPassword: '',
|
|
|
+ newPassword: '',
|
|
|
+ confirmPassword: '',
|
|
|
+ children: [],
|
|
|
+ isSwitchedChild: false,
|
|
|
+ isVendor: false,
|
|
|
+ showToFamily: 1,
|
|
|
+ // 富沛专属
|
|
|
+ wealthEnergy: 0,
|
|
|
+ energyTrend: 0,
|
|
|
+ totalPoints: 0,
|
|
|
+ systemPoints: 0,
|
|
|
+ trendDays: [],
|
|
|
+ badges: [],
|
|
|
+ growthReport: null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ roleLabel() {
|
|
|
+ var map = { parent: '家长模式', child: '孩子模式', teacher: '成长规划师模式' }
|
|
|
+ return map[this.role] || '家长模式'
|
|
|
+ },
|
|
|
+ avatarText() {
|
|
|
+ return this.nickname ? this.nickname.charAt(0) : '👤'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onShow() {
|
|
|
+ var token = uni.getStorageSync('token')
|
|
|
+ this.isLoggedIn = !!token
|
|
|
+ if (!token) return
|
|
|
+
|
|
|
+ var currentRole = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
|
|
|
+ this.role = currentRole
|
|
|
+ this.nickname = this.$store.state.nickname || ''
|
|
|
+ this.hasPassword = !!uni.getStorageSync('passwordSet')
|
|
|
+ this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
|
|
|
+
|
|
|
+ this.loadChildren()
|
|
|
+ this.loadShowToFamily()
|
|
|
+ this.checkVendorStatus()
|
|
|
+ this.loadEnergyData()
|
|
|
+ this.loadBadges()
|
|
|
+ this.loadGrowthReport()
|
|
|
+ this.loadPoints()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ goLogin() {
|
|
|
+ uni.navigateTo({ url: '/pages/login/login' })
|
|
|
+ },
|
|
|
+ async loadChildren() {
|
|
|
+ try {
|
|
|
+ var res = await getChildren()
|
|
|
+ this.children = res.data || []
|
|
|
+ } catch (e) { console.error('获取孩子列表失败', e) }
|
|
|
+ },
|
|
|
+ async loadShowToFamily() {
|
|
|
+ try {
|
|
|
+ var uid = this.$store.state.userId || uni.getStorageSync('userId')
|
|
|
+ var res = await getVisibleFamilyMembers()
|
|
|
+ var members = res.data || []
|
|
|
+ var self = members.find(function(m) { return m.id == uid })
|
|
|
+ if (self) this.showToFamily = self.showToFamily !== 0 ? 1 : 0
|
|
|
+ } catch (e) { console.error('获取可见性设置失败', e) }
|
|
|
+ },
|
|
|
+ async loadEnergyData() {
|
|
|
+ var childId = uni.getStorageSync('currentChildId') || null
|
|
|
+ if (!childId && this.children.length > 0) childId = this.children[0].id
|
|
|
+ if (!childId) return
|
|
|
+ try {
|
|
|
+ var res = await getEnergyOverview(childId)
|
|
|
+ if (res && res.data) {
|
|
|
+ var dims = res.data.dimensions || []
|
|
|
+ var wealthDim = null
|
|
|
+ for (var i = 0; i < dims.length; i++) {
|
|
|
+ if (dims[i].code === 'wealth') { wealthDim = dims[i]; break }
|
|
|
+ }
|
|
|
+ this.wealthEnergy = (wealthDim && wealthDim.score) || 0
|
|
|
+
|
|
|
+ // 增长趋势(模拟数据或从API获取)
|
|
|
+ this.energyTrend = res.data.weeklyTrend || 0
|
|
|
+ this.trendDays = this.buildTrendData(res.data.weeklyLogs)
|
|
|
+ }
|
|
|
+ } catch (e) { console.log('获取能量概览失败', e) }
|
|
|
+ },
|
|
|
+ buildTrendData(weeklyLogs) {
|
|
|
+ // 从API日志构建最近7天趋势柱状图数据
|
|
|
+ var days = ['一', '二', '三', '四', '五', '六', '日']
|
|
|
+ if (weeklyLogs && weeklyLogs.length > 0) {
|
|
|
+ return weeklyLogs.slice(-7).map(function(log, idx) {
|
|
|
+ return { label: days[idx] || '', percent: Math.min(100, log.value || 0) }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // 降级:空数组
|
|
|
+ return []
|
|
|
+ },
|
|
|
+ async loadBadges() {
|
|
|
+ // 从本地计算(后续可对接 API: GET /api/user/badges?recent=9)
|
|
|
+ var streakDays = parseInt(uni.getStorageSync('streakDays') || 0)
|
|
|
+ var totalPoints = parseInt(uni.getStorageSync('totalPoints') || 0)
|
|
|
+ this.badges = [
|
|
|
+ { id: 1, icon: '\u{1F3C6}', name: '连续7天', unlocked: streakDays >= 7 },
|
|
|
+ { id: 2, icon: '\u{2B50}', name: '积分达人', unlocked: totalPoints >= 1000 },
|
|
|
+ { id: 3, icon: '\u{1F525}', name: '执行之星', unlocked: totalPoints >= 500 },
|
|
|
+ { id: 4, icon: '\u{1F31F}', name: '学习先锋', unlocked: false },
|
|
|
+ { id: 5, icon: '\u{1F4AA}', name: '运动健将', unlocked: false },
|
|
|
+ { id: 6, icon: '\u{1F4DA}', name: '阅读之星', unlocked: false },
|
|
|
+ { id: 7, icon: '\u{1F3AE}', name: '游戏达人', unlocked: false },
|
|
|
+ { id: 8, icon: '\u{1F4BC}', name: '理财能手', unlocked: false },
|
|
|
+ { id: 9, icon: '\u{1F3AF}', name: '全能宝贝', unlocked: false }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ async loadGrowthReport() {
|
|
|
+ // 后续对接 API: GET /api/growth/report/latest
|
|
|
+ if (this.role === 'parent' && this.children.length > 0) {
|
|
|
+ var childName = this.children[0].nickname || '孩子'
|
|
|
+ this.growthReport = {
|
|
|
+ childName: childName,
|
|
|
+ score: 78,
|
|
|
+ trend: 5
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async loadPoints() {
|
|
|
+ try {
|
|
|
+ var res = await getPointsBalance()
|
|
|
+ if (res && res.data) {
|
|
|
+ this.totalPoints = res.data.totalPoints || 0
|
|
|
+ this.systemPoints = res.data.systemPoints || res.data.points || 0
|
|
|
+ }
|
|
|
+ } catch (e) { console.log('获取积分失败', e) }
|
|
|
+ },
|
|
|
+ async checkVendorStatus() {
|
|
|
+ try {
|
|
|
+ var res = await vendorStatus()
|
|
|
+ this.isVendor = res.data && res.data.vendorStatus === 'approved'
|
|
|
+ } catch (e) { this.isVendor = false }
|
|
|
+ },
|
|
|
+ // === 导航方法 ===
|
|
|
+ goToEditInfo() { uni.navigateTo({ url: '/pages/user-edit/user-edit' }) },
|
|
|
+ goToChildren() { uni.navigateTo({ url: '/pages/profile/children' }) },
|
|
|
+ goToFamilyMembers() { uni.navigateTo({ url: '/pages/profile/family-members' }) },
|
|
|
+ goToPointsLogs() { uni.navigateTo({ url: '/pages/points/points' }) },
|
|
|
+ goToWishlist() { uni.switchTab({ url: '/pages/rewards/rewards' }) },
|
|
|
+ goToPromotion() { uni.navigateTo({ url: '/pages/promotion/index' }) },
|
|
|
+ goToMembership() { uni.showToast({ title: '即将上线', icon: 'none' }) },
|
|
|
+ goToVendorCenter() { uni.navigateTo({ url: '/pages/vendor/center' }) },
|
|
|
+ goAllBadges() { uni.showToast({ title: '即将上线', icon: 'none' }) },
|
|
|
+ goGrowthReport() { uni.showToast({ title: '即将上线', icon: 'none' }) },
|
|
|
+
|
|
|
+ // === 切换逻辑 ===
|
|
|
+ switchMode() {
|
|
|
+ if (this.role === 'child') {
|
|
|
+ this.verifyPasswordForSwitch(function() { this.doSwitchMode() }.bind(this))
|
|
|
+ } else {
|
|
|
+ this.doSwitchMode()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ doSwitchMode() {
|
|
|
+ var newRole = this.role === 'parent' ? 'child' : 'parent'
|
|
|
+ if (newRole === 'child') {
|
|
|
+ if (this.children.length === 0) {
|
|
|
+ this.loadChildren()
|
|
|
+ if (this.children.length === 0) {
|
|
|
+ uni.showModal({
|
|
|
+ title: '提示',
|
|
|
+ content: '您还没有添加孩子,是否现在去添加?',
|
|
|
+ success: function(res) { if (res.confirm) uni.navigateTo({ url: '/pages/profile/create-child' }) }
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (this.children.length === 1) {
|
|
|
+ this.$store.commit('switchToChild', this.children[0].id)
|
|
|
+ this.role = newRole
|
|
|
+ uni.showToast({ title: '已切换为孩子模式', icon: 'success' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ uni.showActionSheet({
|
|
|
+ itemList: this.children.map(function(c) { return c.nickname }),
|
|
|
+ success: function(res) {
|
|
|
+ var selectedChild = this.children[res.tapIndex]
|
|
|
+ this.$store.commit('switchToChild', selectedChild.id)
|
|
|
+ this.role = newRole
|
|
|
+ uni.showToast({ title: '已切换为' + selectedChild.nickname + '模式', icon: 'success' })
|
|
|
+ }.bind(this)
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.$store.commit('switchBackToParent')
|
|
|
+ this.role = newRole
|
|
|
+ uni.showToast({ title: '已切换为家长模式', icon: 'success' })
|
|
|
+ },
|
|
|
+ verifyPasswordForSwitch(callback) {
|
|
|
+ uni.showModal({
|
|
|
+ title: '验证密码',
|
|
|
+ content: '请输入家长密码',
|
|
|
+ editable: true,
|
|
|
+ success: async function(res) {
|
|
|
+ if (res.confirm && res.content) {
|
|
|
+ try {
|
|
|
+ var result = await verifyPassword(res.content)
|
|
|
+ if (result.code === 200) { callback && callback() }
|
|
|
+ else { uni.showToast({ title: '密码错误', icon: 'none' }) }
|
|
|
+ } catch (e) { uni.showToast({ title: '验证失败', icon: 'none' }) }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ async savePassword() {
|
|
|
+ if (this.newPassword.length !== 6) { uni.showToast({ title: '请输入6位密码', icon: 'none' }); return }
|
|
|
+ if (this.newPassword !== this.confirmPassword) { uni.showToast({ title: '两次密码不一致', icon: 'none' }); return }
|
|
|
+ if (this.hasPassword && this.oldPassword.length !== 6) { uni.showToast({ title: '请输入旧密码', icon: 'none' }); return }
|
|
|
+ try {
|
|
|
+ if (this.hasPassword) {
|
|
|
+ var verifyRes = await verifyPassword(this.oldPassword)
|
|
|
+ if (verifyRes.code !== 200) { uni.showToast({ title: '旧密码错误', icon: 'none' }); return }
|
|
|
+ }
|
|
|
+ await setPassword(this.newPassword)
|
|
|
+ uni.setStorageSync('passwordSet', true)
|
|
|
+ this.hasPassword = true
|
|
|
+ uni.showToast({ title: '密码设置成功', icon: 'success' })
|
|
|
+ this.showPasswordModal = false
|
|
|
+ this.oldPassword = ''; this.newPassword = ''; this.confirmPassword = ''
|
|
|
+ } catch (e) { console.error('设置密码失败', e) }
|
|
|
+ },
|
|
|
+ async onShowToFamilyChange(e) {
|
|
|
+ var val = e.detail.value ? 1 : 0
|
|
|
+ var uid = this.$store.state.userId || uni.getStorageSync('userId')
|
|
|
+ try {
|
|
|
+ await updateMemberVisibility(uid, 'parent', val)
|
|
|
+ this.showToFamily = val
|
|
|
+ uni.showToast({ title: val ? '已对家庭成员可见' : '已对家庭成员隐藏', icon: 'none' })
|
|
|
+ } catch (e) { console.error('更新可见性失败', e) }
|
|
|
+ },
|
|
|
+ // === 邀请 ===
|
|
|
+ inviteFamily() {
|
|
|
+ var familyId = uni.getStorageSync('familyId')
|
|
|
+ if (!familyId) { uni.showToast({ title: '您暂未加入家庭', icon: 'none' }); return }
|
|
|
+ this.generateInvite('family', familyId)
|
|
|
+ },
|
|
|
+ inviteTeacher() {
|
|
|
+ var familyId = uni.getStorageSync('familyId')
|
|
|
+ if (!familyId) { uni.showToast({ title: '您暂未加入家庭', icon: 'none' }); return }
|
|
|
+ this.generateInvite('guide', familyId)
|
|
|
+ },
|
|
|
+ async generateInvite(type, familyId) {
|
|
|
+ try {
|
|
|
+ var { generateInviteCard } = require('../../utils/api.js')
|
|
|
+ var res = await generateInviteCard(type, familyId)
|
|
|
+ var code = res.data.code
|
|
|
+ var userInfo = uni.getStorageSync('userInfo')
|
|
|
+ var title = (userInfo && userInfo.nickname ? userInfo.nickname : '家人') + ' 邀请你加入家庭'
|
|
|
+ if (type === 'guide') title = (userInfo && userInfo.nickname ? userInfo.nickname : '家长') + ' 邀请你成为家庭成长规划师'
|
|
|
+ this.shareData = { title: title, path: '/pages/login/login?invite_code=' + code, imageUrl: '/static/invite-card.png' }
|
|
|
+ uni.showToast({ title: '点击右上角转发给TA', icon: 'none' })
|
|
|
+ } catch (e) { uni.showToast({ title: e.message || '生成邀请失败', icon: 'none' }) }
|
|
|
+ },
|
|
|
+ // === 退出 ===
|
|
|
+ logout() {
|
|
|
+ uni.showModal({
|
|
|
+ title: '确认退出',
|
|
|
+ content: '确定要退出登录吗?',
|
|
|
+ success: function(res) {
|
|
|
+ if (res.confirm) {
|
|
|
+ this.$store.commit('logout')
|
|
|
+ uni.reLaunch({ url: '/pages/index/index' })
|
|
|
+ }
|
|
|
+ }.bind(this)
|
|
|
+ })
|
|
|
+ },
|
|
|
+ exitSwitch() {
|
|
|
+ if (!this.isSwitchedChild) return
|
|
|
+ uni.showModal({
|
|
|
+ title: '退出切换',
|
|
|
+ content: '请输入家长密码以确认退出',
|
|
|
+ editable: true,
|
|
|
+ success: async function(res) {
|
|
|
+ if (res.confirm && res.content) {
|
|
|
+ try {
|
|
|
+ var verifyResult = await verifyPassword(res.content)
|
|
|
+ if (verifyResult.code === 200) {
|
|
|
+ this.$store.commit('switchBackToParent')
|
|
|
+ this.isSwitchedChild = false
|
|
|
+ this.role = 'parent'
|
|
|
+ uni.showToast({ title: '已退出切换', icon: 'success' })
|
|
|
+ uni.reLaunch({ url: '/pages/index/index' })
|
|
|
+ } else { uni.showToast({ title: '密码错误', icon: 'none' }) }
|
|
|
+ } catch (e) { uni.showToast({ title: '验证失败', icon: 'none' }) }
|
|
|
+ }
|
|
|
+ }.bind(this)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 添加样式**
|
|
|
+
|
|
|
+```css
|
|
|
+<style scoped>
|
|
|
+.container {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #f5f7fa;
|
|
|
+ padding-bottom: 120rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 品牌头部 */
|
|
|
+.brand-header {
|
|
|
+ background: linear-gradient(135deg, #1A2A4A 0%, #1E3A5F 100%);
|
|
|
+ padding: 60rpx 40rpx;
|
|
|
+}
|
|
|
+.brand-content {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+.brand-logo {
|
|
|
+ font-size: 40rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #fff;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+.brand-tagline {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: rgba(255,255,255,0.7);
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+.brand-energy {
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #F97316;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.brand-energy.dim {
|
|
|
+ color: rgba(255,255,255,0.5);
|
|
|
+}
|
|
|
+
|
|
|
+/* 登录引导 */
|
|
|
+.login-prompt {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ min-height: 60vh;
|
|
|
+ padding: 60rpx;
|
|
|
+}
|
|
|
+.prompt-icon {
|
|
|
+ font-size: 120rpx;
|
|
|
+ margin-bottom: 30rpx;
|
|
|
+ width: 160rpx;
|
|
|
+ height: 160rpx;
|
|
|
+ line-height: 160rpx;
|
|
|
+ text-align: center;
|
|
|
+ background: #f5f5f5;
|
|
|
+ border-radius: 50%;
|
|
|
+}
|
|
|
+.prompt-title {
|
|
|
+ font-size: 36rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+.prompt-desc {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #999;
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 40rpx;
|
|
|
+}
|
|
|
+.login-btn {
|
|
|
+ width: 60%;
|
|
|
+ height: 80rpx;
|
|
|
+ line-height: 80rpx;
|
|
|
+ background: linear-gradient(135deg, #F97316, #FB923C);
|
|
|
+ color: #fff;
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ border-radius: 40rpx;
|
|
|
+ text-align: center;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+.login-btn::after { border: none; }
|
|
|
+
|
|
|
+/* 退出切换 */
|
|
|
+.exit-switch {
|
|
|
+ background: #FFF3E0;
|
|
|
+ padding: 20rpx;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #F97316;
|
|
|
+}
|
|
|
+
|
|
|
+/* 用户卡片 */
|
|
|
+.user-card {
|
|
|
+ background: linear-gradient(135deg, #1A2A4A 0%, #1E3A5F 100%);
|
|
|
+ border-radius: 20rpx;
|
|
|
+ margin: -30rpx 30rpx 20rpx;
|
|
|
+ padding: 36rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ color: #fff;
|
|
|
+ position: relative;
|
|
|
+ z-index: 2;
|
|
|
+}
|
|
|
+.avatar {
|
|
|
+ font-size: 72rpx;
|
|
|
+ width: 96rpx;
|
|
|
+ height: 96rpx;
|
|
|
+ line-height: 96rpx;
|
|
|
+ text-align: center;
|
|
|
+ background: rgba(255,255,255,0.15);
|
|
|
+ border-radius: 50%;
|
|
|
+ margin-right: 24rpx;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.user-info {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+.nickname {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+.role {
|
|
|
+ font-size: 22rpx;
|
|
|
+ opacity: 0.8;
|
|
|
+ margin-top: 4rpx;
|
|
|
+}
|
|
|
+.system-points {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #D6EAF8;
|
|
|
+ margin-top: 8rpx;
|
|
|
+}
|
|
|
+.wealth-energy-line {
|
|
|
+ font-size: 22rpx;
|
|
|
+ margin-top: 6rpx;
|
|
|
+ color: rgba(255,255,255,0.85);
|
|
|
+}
|
|
|
+.energy-value {
|
|
|
+ color: #F97316;
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 24rpx;
|
|
|
+}
|
|
|
+.energy-trend {
|
|
|
+ margin-left: 8rpx;
|
|
|
+ font-size: 20rpx;
|
|
|
+ color: #4ADE80;
|
|
|
+}
|
|
|
+.btn-switch {
|
|
|
+ background: rgba(255,255,255,0.2);
|
|
|
+ color: #fff;
|
|
|
+ font-size: 24rpx;
|
|
|
+ padding: 10rpx 24rpx;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+/* 通用区块 */
|
|
|
+.section {
|
|
|
+ margin: 20rpx 30rpx;
|
|
|
+}
|
|
|
+.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;
|
|
|
+}
|
|
|
+
|
|
|
+/* 财富数据卡片 */
|
|
|
+.wealth-card {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ padding: 30rpx 20rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
|
|
|
+}
|
|
|
+.wealth-item {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.wealth-value {
|
|
|
+ font-size: 36rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #F97316;
|
|
|
+}
|
|
|
+.wealth-value.link {
|
|
|
+ font-size: 40rpx;
|
|
|
+}
|
|
|
+.wealth-label {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #999;
|
|
|
+ margin-top: 6rpx;
|
|
|
+}
|
|
|
+.wealth-divider {
|
|
|
+ width: 1rpx;
|
|
|
+ height: 50rpx;
|
|
|
+ background: #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 能量趋势柱状图 */
|
|
|
+.energy-trend-bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ padding: 24rpx 20rpx 16rpx;
|
|
|
+ margin-top: 16rpx;
|
|
|
+ height: 140rpx;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
|
|
|
+}
|
|
|
+.trend-item {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+.trend-bar {
|
|
|
+ width: 16rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ min-height: 8rpx;
|
|
|
+}
|
|
|
+.trend-label {
|
|
|
+ font-size: 20rpx;
|
|
|
+ color: #999;
|
|
|
+ margin-top: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+/* 勋章墙 */
|
|
|
+.badge-grid {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ padding: 24rpx 16rpx;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
|
|
|
+}
|
|
|
+.badge-item {
|
|
|
+ width: 33.33%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+.badge-icon-wrap {
|
|
|
+ width: 72rpx;
|
|
|
+ height: 72rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-bottom: 6rpx;
|
|
|
+}
|
|
|
+.badge-icon-wrap.unlocked { background: linear-gradient(135deg, #FBBF24, #F97316); }
|
|
|
+.badge-icon-wrap.locked { background: #f0f0f0; opacity: 0.5; }
|
|
|
+.badge-icon { font-size: 32rpx; }
|
|
|
+.badge-name { font-size: 20rpx; color: #666; }
|
|
|
+
|
|
|
+/* 功能菜单 */
|
|
|
+.menu-list {
|
|
|
+ margin: 20rpx 30rpx;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.menu-group-title {
|
|
|
+ padding: 20rpx 30rpx 10rpx;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #aaa;
|
|
|
+ letter-spacing: 2rpx;
|
|
|
+}
|
|
|
+.menu-item {
|
|
|
+ padding: 28rpx 30rpx;
|
|
|
+ border-bottom: 1rpx solid #f5f5f5;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.menu-item:last-child { border-bottom: none; }
|
|
|
+.menu-item-switch { align-items: center; }
|
|
|
+.menu-item-btn {
|
|
|
+ width: 100%;
|
|
|
+ padding: 28rpx 30rpx;
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #333;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+.menu-item-btn::after { border: none; }
|
|
|
+.arrow { color: #ccc; font-size: 30rpx; }
|
|
|
+
|
|
|
+/* 成长报告 */
|
|
|
+.report-card {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ padding: 30rpx 24rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
|
|
|
+}
|
|
|
+.report-child-avatar { font-size: 48rpx; margin-right: 20rpx; }
|
|
|
+.report-info { flex: 1; }
|
|
|
+.report-child-name { font-size: 26rpx; font-weight: 600; color: #333; display: block; margin-bottom: 8rpx; }
|
|
|
+.report-score-row { display: flex; align-items: center; }
|
|
|
+.report-score-label { font-size: 24rpx; color: #999; }
|
|
|
+.report-score-value { font-size: 28rpx; color: #F97316; font-weight: bold; }
|
|
|
+.report-score-trend { font-size: 22rpx; color: #4ADE80; margin-left: 8rpx; }
|
|
|
+
|
|
|
+/* 密码弹窗 */
|
|
|
+.modal-mask {
|
|
|
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
|
+ background: rgba(0,0,0,0.5);
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
+}
|
|
|
+.modal {
|
|
|
+ background: #fff; border-radius: 20rpx; padding: 40rpx;
|
|
|
+ width: 80%;
|
|
|
+}
|
|
|
+.modal-title { font-size: 32rpx; font-weight: bold; margin-bottom: 30rpx; text-align: center; }
|
|
|
+.form-item { margin-bottom: 20rpx; }
|
|
|
+.form-item input {
|
|
|
+ border: 1rpx solid #ddd; border-radius: 10rpx;
|
|
|
+ padding: 20rpx; font-size: 28rpx;
|
|
|
+}
|
|
|
+.modal-btns { display: flex; gap: 20rpx; margin-top: 30rpx; }
|
|
|
+.modal-btns button { flex: 1; }
|
|
|
+
|
|
|
+.bottom-spacer { height: 40rpx; }
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+**注意:** 此组件需要 `$store` 支持(Vuex)。确认 `pages/wealth/index` 所在的 subPackage 能访问 store(uni-app 的 store 是全局的,可以)。
|
|
|
+
|
|
|
+- [ ] **Step 4: 处理旧 profile 页**
|
|
|
+
|
|
|
+在 `pages/profile/profile.vue` 文件首部添加废弃注释:
|
|
|
+```html
|
|
|
+<!--
|
|
|
+ ⚠️ 废弃说明(2026-06-15):
|
|
|
+ 此页面已被 pages/wealth/index 取代,"我的" Tab 已改为"富沛"。
|
|
|
+ 保留文件用于旧链接兼容,不通过 TabBar 访问。
|
|
|
+-->
|
|
|
+```
|
|
|
+
|
|
|
+并添加 `onShow` 重定向:
|
|
|
+```javascript
|
|
|
+onShow() {
|
|
|
+ // 旧页面,重定向到新富沛页
|
|
|
+ uni.switchTab({ url: '/pages/wealth/index' })
|
|
|
+ return
|
|
|
+ // ...原有代码注释掉或保留在下方
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: LSP 诊断**
|
|
|
+
|
|
|
+运行:`lsp_diagnostics(filePath="cfc-frontend/pages/wealth/index.vue")`
|
|
|
+预期:clean
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 6 — 组件改造 (P1 🟠)
|
|
|
+
|
|
|
+### Task 12: FamilyEnergyBar.vue — 支持 dualDimension 堆叠渲染
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/components/FamilyEnergyBar.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 修改 props,dualDimension 改为 Object 类型**
|
|
|
+
|
|
|
+修改前(约第 38 行):
|
|
|
+```javascript
|
|
|
+props: {
|
|
|
+ dimensionCode: { type: String, default: '' },
|
|
|
+ sandboxData: { type: Object, default: null },
|
|
|
+ dualDimension: { type: String, default: '' }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+修改后:
|
|
|
+```javascript
|
|
|
+props: {
|
|
|
+ dimensionCode: { type: String, default: '' },
|
|
|
+ sandboxData: { type: Object, default: null },
|
|
|
+ dualDimension: { type: Object, default: null }
|
|
|
+ // dualDimension 格式: { primary: {code,score,label,color}, secondary: {code,score,label,color} }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 添加堆叠柱渲染模板**
|
|
|
+
|
|
|
+在能量柱渲染位置增加堆叠分支:
|
|
|
+```html
|
|
|
+<!-- 堆叠柱(dualDimension) -->
|
|
|
+<view class="energy-bar-stacked" v-if="dualDimension && dualDimension.primary">
|
|
|
+ <view class="stacked-bar-wrapper" @click="onBarClick">
|
|
|
+ <view class="stacked-bar-segment stacked-bar--primary"
|
|
|
+ :style="{ height: primaryPercent + '%', background: dualDimension.primary.color || '#FF6B35' }">
|
|
|
+ <text class="segment-label" v-if="primaryPercent > 15">心 {{ dualDimension.primary.score }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="stacked-bar-segment stacked-bar--secondary"
|
|
|
+ :style="{ height: secondaryPercent + '%', background: dualDimension.secondary.color || '#8B5CF6' }">
|
|
|
+ <text class="segment-label" v-if="secondaryPercent > 15">智 {{ dualDimension.secondary.score }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="stacked-legend">
|
|
|
+ <view class="legend-item"><view class="legend-dot" style="background:#FF6B35"></view><text>心理</text></view>
|
|
|
+ <view class="legend-item"><view class="legend-dot" style="background:#8B5CF6"></view><text>认知</text></view>
|
|
|
+ </view>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 添加 computed 属性**
|
|
|
+
|
|
|
+```javascript
|
|
|
+computed: {
|
|
|
+ primaryPercent() {
|
|
|
+ if (!this.dualDimension || !this.dualDimension.primary) return 0
|
|
|
+ return Math.min(100, this.dualDimension.primary.score || 0)
|
|
|
+ },
|
|
|
+ secondaryPercent() {
|
|
|
+ if (!this.dualDimension || !this.dualDimension.secondary) return 0
|
|
|
+ return Math.min(100, this.dualDimension.secondary.score || 0)
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 添加堆叠柱样式**
|
|
|
+
|
|
|
+```css
|
|
|
+.energy-bar-stacked {
|
|
|
+ margin: 16rpx 0;
|
|
|
+}
|
|
|
+.stacked-bar-wrapper {
|
|
|
+ height: 200rpx;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #f0f0f0;
|
|
|
+}
|
|
|
+.stacked-bar-segment {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition: height 0.3s ease;
|
|
|
+}
|
|
|
+.segment-label {
|
|
|
+ color: #fff;
|
|
|
+ font-size: 22rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ text-shadow: 0 1rpx 4rpx rgba(0,0,0,0.3);
|
|
|
+}
|
|
|
+.stacked-legend {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 32rpx;
|
|
|
+ margin-top: 12rpx;
|
|
|
+}
|
|
|
+.legend-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8rpx;
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+.legend-dot {
|
|
|
+ width: 16rpx;
|
|
|
+ height: 16rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: LSP 诊断**
|
|
|
+
|
|
|
+运行:`lsp_diagnostics(filePath="cfc-frontend/components/FamilyEnergyBar.vue")`
|
|
|
+预期:clean
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 13: DimensionProducts.vue + DimensionActivities.vue — isLoggedIn props
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/components/DimensionProducts.vue`
|
|
|
+- Modify: `cfc-frontend/components/DimensionActivities.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: DimensionProducts — 增加 isLoggedIn prop 和价格切换**
|
|
|
+
|
|
|
+修改 `DimensionProducts.vue`:
|
|
|
+
|
|
|
+在 props 中增加:
|
|
|
+```javascript
|
|
|
+props: {
|
|
|
+ // ... 现有 props
|
|
|
+ isLoggedIn: { type: Boolean, default: false }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+在价格渲染位置(找到商品价格区域):
|
|
|
+```html
|
|
|
+<!-- 修改前(old: BigDecimal 类型,用 v-if="item.price" 检查) -->
|
|
|
+<view class="product-price" v-if="item.price">¥{{ item.price }}</view>
|
|
|
+
|
|
|
+<!-- 修改后(new: 整型分→元,使用格式化函数) -->
|
|
|
+<view v-if="isLoggedIn && item.price != null" class="product-price">{{ formatPriceWithSymbol(item.price) }}</view>
|
|
|
+<view v-else-if="!isLoggedIn" class="product-price-login">登录查看</view>
|
|
|
+```
|
|
|
+
|
|
|
+在组件 `<script>` 中引入:
|
|
|
+```javascript
|
|
|
+import { formatPriceWithSymbol } from '@/utils/format'
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: DimensionActivities — 增加 isLoggedIn prop 和空态提示切换**
|
|
|
+
|
|
|
+修改 `DimensionActivities.vue`:
|
|
|
+
|
|
|
+在 props 中增加:
|
|
|
+```javascript
|
|
|
+props: {
|
|
|
+ // ... 现有 props
|
|
|
+ isLoggedIn: { type: Boolean, default: false }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+在空数据展示处:
|
|
|
+```html
|
|
|
+<!-- 修改前 -->
|
|
|
+<view class="activities-empty" v-if="activities.length === 0">
|
|
|
+ <text class="empty-tip">暂无相关活动</text>
|
|
|
+</view>
|
|
|
+
|
|
|
+<!-- 修改后 -->
|
|
|
+<view class="activities-empty" v-if="activities.length === 0">
|
|
|
+ <text class="empty-tip">{{ isLoggedIn ? '暂无相关活动' : '登录后查看更多活动' }}</text>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: LSP 诊断**
|
|
|
+
|
|
|
+运行:
|
|
|
+```
|
|
|
+lsp_diagnostics(filePath="cfc-frontend/components/DimensionProducts.vue")
|
|
|
+lsp_diagnostics(filePath="cfc-frontend/components/DimensionActivities.vue")
|
|
|
+```
|
|
|
+预期:clean
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 7 — 后端改造 (P1 🟠)
|
|
|
+
|
|
|
+### Task 14: ProductController — 游客模式价格返回 null
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java`
|
|
|
+
|
|
|
+- [ ] **Step 1: Product 实体 — BigDecimal → Integer(分)**
|
|
|
+
|
|
|
+将 `Product.java` 中的金额字段改为整型:
|
|
|
+
|
|
|
+```java
|
|
|
+// 旧: private BigDecimal price;
|
|
|
+// 新: 金额以"分"为单位存储,前端展示时分/100
|
|
|
+private Integer price;
|
|
|
+
|
|
|
+// 旧: private BigDecimal memberPrice;
|
|
|
+private Integer memberPrice;
|
|
|
+
|
|
|
+// 旧: private BigDecimal profitRate;
|
|
|
+// 新: 利润率以千分比存储(23.5% → 235)
|
|
|
+private Integer profitRate;
|
|
|
+```
|
|
|
+
|
|
|
+对应的 `ProductDTO.java` 也同步修改。
|
|
|
+
|
|
|
+- [ ] **Step 2: 数据库同步 — price/memberPrice 字段改为 INT**
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 1. 新增 INT 列
|
|
|
+ALTER TABLE products
|
|
|
+ ADD COLUMN price_cents INT DEFAULT 0 COMMENT '价格(分)',
|
|
|
+ ADD COLUMN member_price_cents INT DEFAULT 0 COMMENT '会员价(分)',
|
|
|
+ ADD COLUMN profit_rate_permyriad INT DEFAULT 0 COMMENT '利润率(千分比)';
|
|
|
+
|
|
|
+-- 2. 从旧 decimal 字段迁移数据(四舍五入到分)
|
|
|
+UPDATE products SET price_cents = ROUND(price * 100);
|
|
|
+UPDATE products SET member_price_cents = ROUND(member_price * 100);
|
|
|
+UPDATE products SET profit_rate_permyriad = ROUND(profit_rate * 10);
|
|
|
+
|
|
|
+-- 3. 确认迁移无误后,删除旧 decimal 字段
|
|
|
+ALTER TABLE products
|
|
|
+ DROP COLUMN price,
|
|
|
+ DROP COLUMN member_price,
|
|
|
+ DROP COLUMN profit_rate;
|
|
|
+
|
|
|
+-- 4. 重命名新字段
|
|
|
+ALTER TABLE products
|
|
|
+ CHANGE COLUMN price_cents price INT DEFAULT 0 COMMENT '价格(分)',
|
|
|
+ CHANGE COLUMN member_price_cents member_price INT DEFAULT 0 COMMENT '会员价(分)',
|
|
|
+ CHANGE COLUMN profit_rate_permyriad profit_rate INT DEFAULT 0 COMMENT '利润率(千分比)';
|
|
|
+```
|
|
|
+
|
|
|
+> **注意:** 如果数据库中有历史订单(ProductOrder)使用了 `unitPrice`/`totalAmount` 的 decimal 字段,也在本次一并进行改造。因无现成生产环境数据,迁移脚本由实施时按实际情况调整。
|
|
|
+
|
|
|
+- [ ] **Step 3: ProductOrder 实体改造**
|
|
|
+
|
|
|
+```java
|
|
|
+// ProductOrder.java
|
|
|
+// 旧: private BigDecimal unitPrice; private BigDecimal totalAmount;
|
|
|
+// 新:
|
|
|
+private Integer unitPrice; // 单价(分)
|
|
|
+private Integer totalAmount; // 总价(分)
|
|
|
+```
|
|
|
+
|
|
|
+```java
|
|
|
+// ProductOrderService.java 中计算逻辑更新
|
|
|
+// 旧: order.setTotalAmount(product.getPrice().multiply(BigDecimal.valueOf(dto.getQuantity())));
|
|
|
+// 新:
|
|
|
+order.setUnitPrice(product.getPrice());
|
|
|
+order.setTotalAmount(product.getPrice() * dto.getQuantity());
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: ProductDTO 输出 — 保持字段名不变,类型统一为 Integer**
|
|
|
+
|
|
|
+```java
|
|
|
+@Data
|
|
|
+public class ProductDTO {
|
|
|
+ private Long id;
|
|
|
+ private String name;
|
|
|
+ private String description;
|
|
|
+ private String coverImage;
|
|
|
+ private Integer price; // 整型(分),前端展示时 /100
|
|
|
+ private Integer memberPrice; // 整型(分)
|
|
|
+ private Integer profitRate; // 整型(千分比)
|
|
|
+ // ... 其他字段不变
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: 商品列表接口 — 游客 price 置 null + 返回整型**
|
|
|
+
|
|
|
+```java
|
|
|
+// ProductController.java
|
|
|
+public Result<?> list(@RequestBody(required = false) Map<String, Object> params,
|
|
|
+ @RequestAttribute(value = "userId", required = false) Long userId) {
|
|
|
+ // ... 正常查询逻辑
|
|
|
+
|
|
|
+ List<ProductDTO> productList = ...;
|
|
|
+ if (userId == null) {
|
|
|
+ // 游客隐藏价格
|
|
|
+ for (ProductDTO dto : productList) {
|
|
|
+ dto.setPrice(null);
|
|
|
+ dto.setMemberPrice(null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 登录用户:price/memberPrice 返回整型(分),前端自行做 /100 转换
|
|
|
+ return Result.success(productList);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 6: 前端价格展示 — 分→元转换**
|
|
|
+
|
|
|
+在所有展示价格的 Vue 页面/组件中,使用统一转换逻辑:
|
|
|
+
|
|
|
+```javascript
|
|
|
+// utils/format.js 新建工具函数
|
|
|
+export function formatPrice(cents) {
|
|
|
+ if (cents == null || cents === undefined) return null
|
|
|
+ return (cents / 100).toFixed(2)
|
|
|
+}
|
|
|
+
|
|
|
+export function formatPriceWithSymbol(cents) {
|
|
|
+ if (cents == null || cents === undefined) return '登录查看'
|
|
|
+ return '¥' + (cents / 100).toFixed(2)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+页面中使用:
|
|
|
+```vue
|
|
|
+<text class="product-price">{{ formatPriceWithSymbol(item.price) }}</text>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 7: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+预期:`BUILD SUCCESS`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 15: Activity 实体 + Controller — 可见性三字段 + 过滤逻辑
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/entity/Activity.java`
|
|
|
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/sncp/ActivityController.java`
|
|
|
+- Create: SQL 变更(DBA 执行或通过 DatabaseInitializer)
|
|
|
+
|
|
|
+- [ ] **Step 1: Activity 实体增加可见性字段**
|
|
|
+
|
|
|
+在 `Activity.java` 中增加:
|
|
|
+
|
|
|
+```java
|
|
|
+/** 可见性:public(公开) / restricted(私密) */
|
|
|
+private String visibility;
|
|
|
+
|
|
|
+/** 可见范围:family(家庭) / role(角色) / user(用户) */
|
|
|
+private String visibleScope;
|
|
|
+
|
|
|
+/** 可见对象标识:家庭ID/角色名/用户ID 的 JSON 数组,如 "[\"family_1\",\"family_2\"]" */
|
|
|
+private String visibleTo;
|
|
|
+```
|
|
|
+
|
|
|
+(使用 Lombok `@Data` 自动生成 getter/setter)
|
|
|
+
|
|
|
+- [ ] **Step 2: 确认数据库表有对应字段**
|
|
|
+
|
|
|
+如果 DatabaseInitializer 或 SQL 中没有,增加:
|
|
|
+```sql
|
|
|
+ALTER TABLE activity
|
|
|
+ ADD COLUMN visibility VARCHAR(20) DEFAULT 'public' COMMENT '可见性: public/restricted',
|
|
|
+ ADD COLUMN visible_scope VARCHAR(20) DEFAULT NULL COMMENT '可见范围: family/role/user',
|
|
|
+ ADD COLUMN visible_to VARCHAR(500) DEFAULT NULL COMMENT '可见对象标识JSON数组';
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: ActivityController 列表查询增加可见性过滤**
|
|
|
+
|
|
|
+找到活动列表查询方法,在构建 `QueryWrapper` 时增加条件:
|
|
|
+
|
|
|
+```java
|
|
|
+// 获取当前用户信息
|
|
|
+Long userId = ...; // 从请求属性获取
|
|
|
+String userRole = ...;
|
|
|
+String familyId = ...;
|
|
|
+
|
|
|
+// 构建查询条件
|
|
|
+QueryWrapper<Activity> queryWrapper = new QueryWrapper<>();
|
|
|
+
|
|
|
+if (userId == null) {
|
|
|
+ // 游客:只返回公开活动
|
|
|
+ queryWrapper.eq("visibility", "public");
|
|
|
+} else {
|
|
|
+ // 登录用户:公开活动 + 自己有权限的私密活动
|
|
|
+ queryWrapper.and(w ->
|
|
|
+ w.eq("visibility", "public")
|
|
|
+ .or(w2 -> w2.eq("visibility", "restricted")
|
|
|
+ .and(w3 -> {
|
|
|
+ // family 范围:用户所在家庭
|
|
|
+ // role 范围:用户角色匹配
|
|
|
+ // user 范围:用户ID匹配
|
|
|
+ // 使用 JSON_CONTAINS 或 like 查询 visibleTo
|
|
|
+ })
|
|
|
+ )
|
|
|
+ );
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**简化实现(第一期):** 因私密活动逻辑复杂,第一期先实现游客/登录用户的公开活动列表,私密逻辑保留设计但不实现完整过滤,避免阻塞其他功能。
|
|
|
+
|
|
|
+简化后的 ActivityController 逻辑:
|
|
|
+```java
|
|
|
+// 游客只看到公开活动
|
|
|
+if (userId == null) {
|
|
|
+ queryWrapper.eq("visibility", "public");
|
|
|
+}
|
|
|
+// 登录用户看到所有活动(后续迭代完善私密过滤)
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+预期:`BUILD SUCCESS`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 8 — RadarChart 组件 + 后端数据 API (P1 🟠)
|
|
|
+
|
|
|
+### Task 17: 新建 RadarChart.vue — 通用 canvas 雷达图组件
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-frontend/components/RadarChart.vue`
|
|
|
+
|
|
|
+**用途:** 心智页认知六维雷达图、大五人格图、行远页六类关系雷达图使用。基于 uni-app canvas 绘制,不引入第三方图表库。
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建 RadarChart.vue 组件**
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <view class="radar-chart-container">
|
|
|
+ <canvas
|
|
|
+ canvas-id="radarCanvas"
|
|
|
+ :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
|
|
+ @error="onError"></canvas>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ props: {
|
|
|
+ dimensions: { type: Array, default: function() { return [] } },
|
|
|
+ // dimensions: [{ key: 'memory', label: '记忆力' }]
|
|
|
+ scores: { type: Array, default: function() { return [] } },
|
|
|
+ // scores: [85, 70, 90, ...] 与 dimensions 一一对应
|
|
|
+ maxScore: { type: Number, default: 100 },
|
|
|
+ label: { type: String, default: '' },
|
|
|
+ width: { type: Number, default: 500 },
|
|
|
+ height: { type: Number, default: 500 },
|
|
|
+ showLabels: { type: Boolean, default: true },
|
|
|
+ fillColor: { type: String, default: 'rgba(249,115,22,0.2)' },
|
|
|
+ strokeColor: { type: String, default: '#F97316' },
|
|
|
+ gridColor: { type: String, default: '#e0e0e0' }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ canvasWidth() { return this.width },
|
|
|
+ canvasHeight() { return this.height },
|
|
|
+ centerX() { return this.width / 2 },
|
|
|
+ centerY() { return this.height / 2 },
|
|
|
+ radius() { return Math.min(this.width, this.height) / 2 - 40 },
|
|
|
+ count() { return this.dimensions.length }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ scores: { handler: 'draw', immediate: true, deep: true },
|
|
|
+ dimensions: { handler: 'draw', immediate: true, deep: true }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ // 延迟确保 canvas 渲染完成
|
|
|
+ var self = this
|
|
|
+ setTimeout(function() { self.draw() }, 200)
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ draw() {
|
|
|
+ if (!this.dimensions || this.dimensions.length === 0) return
|
|
|
+ if (!this.scores || this.scores.length !== this.dimensions.length) return
|
|
|
+
|
|
|
+ var ctx = uni.createCanvasContext('radarCanvas', this)
|
|
|
+ var n = this.count
|
|
|
+ var angleStep = (Math.PI * 2) / n
|
|
|
+ var startAngle = -Math.PI / 2 // 12点钟方向开始
|
|
|
+
|
|
|
+ // 清空
|
|
|
+ ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
|
|
|
+
|
|
|
+ // 1. 绘制网格(3层同心多边形)
|
|
|
+ var layers = [0.25, 0.5, 0.75, 1.0]
|
|
|
+ for (var l = 0; l < layers.length; l++) {
|
|
|
+ var layerRadius = this.radius * layers[l]
|
|
|
+ ctx.beginPath()
|
|
|
+ for (var i = 0; i < n; i++) {
|
|
|
+ var angle = startAngle + i * angleStep
|
|
|
+ var x = this.centerX + layerRadius * Math.cos(angle)
|
|
|
+ var y = this.centerY + layerRadius * Math.sin(angle)
|
|
|
+ if (i === 0) ctx.moveTo(x, y)
|
|
|
+ else ctx.lineTo(x, y)
|
|
|
+ }
|
|
|
+ ctx.closePath()
|
|
|
+ ctx.setStrokeStyle(this.gridColor)
|
|
|
+ ctx.setLineWidth(1)
|
|
|
+ ctx.stroke()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 绘制轴线
|
|
|
+ for (var i = 0; i < n; i++) {
|
|
|
+ var angle = startAngle + i * angleStep
|
|
|
+ var x = this.centerX + this.radius * Math.cos(angle)
|
|
|
+ var y = this.centerY + this.radius * Math.sin(angle)
|
|
|
+ ctx.beginPath()
|
|
|
+ ctx.moveTo(this.centerX, this.centerY)
|
|
|
+ ctx.lineTo(x, y)
|
|
|
+ ctx.setStrokeStyle(this.gridColor)
|
|
|
+ ctx.setLineWidth(1)
|
|
|
+ ctx.stroke()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 绘制数据区域(填充)
|
|
|
+ ctx.beginPath()
|
|
|
+ var dataPoints = []
|
|
|
+ for (var i = 0; i < n; i++) {
|
|
|
+ var ratio = Math.min(1, (this.scores[i] || 0) / this.maxScore)
|
|
|
+ var angle = startAngle + i * angleStep
|
|
|
+ var r = this.radius * ratio
|
|
|
+ var x = this.centerX + r * Math.cos(angle)
|
|
|
+ var y = this.centerY + r * Math.sin(angle)
|
|
|
+ dataPoints.push({ x: x, y: y })
|
|
|
+ if (i === 0) ctx.moveTo(x, y)
|
|
|
+ else ctx.lineTo(x, y)
|
|
|
+ }
|
|
|
+ ctx.closePath()
|
|
|
+ ctx.setFillStyle(this.fillColor)
|
|
|
+ ctx.setStrokeStyle(this.strokeColor)
|
|
|
+ ctx.setLineWidth(2)
|
|
|
+ ctx.fill()
|
|
|
+ ctx.stroke()
|
|
|
+
|
|
|
+ // 4. 绘制数据点
|
|
|
+ for (var i = 0; i < dataPoints.length; i++) {
|
|
|
+ ctx.beginPath()
|
|
|
+ ctx.arc(dataPoints[i].x, dataPoints[i].y, 4, 0, Math.PI * 2)
|
|
|
+ ctx.setFillStyle(this.strokeColor)
|
|
|
+ ctx.fill()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 绘制标签
|
|
|
+ if (this.showLabels) {
|
|
|
+ ctx.setFontSize(12)
|
|
|
+ ctx.setFillStyle('#333')
|
|
|
+ for (var i = 0; i < n; i++) {
|
|
|
+ var angle = startAngle + i * angleStep
|
|
|
+ var labelRadius = this.radius + 24
|
|
|
+ var x = this.centerX + labelRadius * Math.cos(angle)
|
|
|
+ var y = this.centerY + labelRadius * Math.sin(angle)
|
|
|
+ var label = this.dimensions[i] && this.dimensions[i].label
|
|
|
+ if (label) {
|
|
|
+ ctx.setTextAlign('center')
|
|
|
+ ctx.setTextBaseline('middle')
|
|
|
+ ctx.fillText(label, x, y)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.draw()
|
|
|
+ },
|
|
|
+ onError(e) {
|
|
|
+ console.error('RadarChart canvas error', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style>
|
|
|
+.radar-chart-container {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ padding: 20rpx 0;
|
|
|
+}
|
|
|
+</style>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 确认组件可用**
|
|
|
+
|
|
|
+在 body/mind/action 任一页面临时引入并渲染,运行 `lsp_diagnostics` 检查无错误,然后在心智页和行远页完整集成。
|
|
|
+
|
|
|
+- [ ] **Step 3: LSP 诊断**
|
|
|
+
|
|
|
+运行:`lsp_diagnostics(filePath="cfc-frontend/components/RadarChart.vue")`
|
|
|
+预期:clean
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 18: 后端 — 认知能力/大五人格/关系数据 API
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/ContactController.java`
|
|
|
+- Modify: `cfc-backend/.../controller/assessment/AssessmentController.java`(如认知和大五从 DAN 测评结果取)
|
|
|
+- Modify: `cfc-backend/.../entity/Contact.java`(如关系维度评分字段不存在)
|
|
|
+
|
|
|
+- [ ] **Step 1: 认知能力 API — GET /api/assessment/cognitive-profile**
|
|
|
+
|
|
|
+在 `AssessmentController` 中增加认知能力查询接口。从 `DanAssessmentResult` 中提取认知六维度评分:
|
|
|
+
|
|
|
+```java
|
|
|
+@PostMapping("/api/assessment/cognitive-profile")
|
|
|
+public Result<?> getCognitiveProfile(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
+ if (userId == null) return Result.error(401, "请先登录");
|
|
|
+ Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
|
|
|
+ if (childId == null) return Result.error(400, "缺少childId");
|
|
|
+
|
|
|
+ // 从 DanAssessmentResult 查询最新测评的认知数据
|
|
|
+ // 实体假设已有 cognitionJson 字段存储 JSON 格式的六个维度评分
|
|
|
+ // 如无此数据,先返回默认结构供前端开发联调
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+ result.put("sensation", 78);
|
|
|
+ result.put("attention", 85);
|
|
|
+ result.put("memory", 72);
|
|
|
+ result.put("logic", 90);
|
|
|
+ result.put("spatial", 68);
|
|
|
+ result.put("speed", 82);
|
|
|
+ return Result.success(result);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 大五人格 API — GET /api/assessment/dan/big-five**
|
|
|
+
|
|
|
+在 `AssessmentController` 中增加大五人格查询接口:
|
|
|
+
|
|
|
+```java
|
|
|
+@PostMapping("/api/assessment/dan/big-five")
|
|
|
+public Result<?> getBigFive(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
+ if (userId == null) return Result.error(401, "请先登录");
|
|
|
+ Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null;
|
|
|
+ if (childId == null) return Result.error(400, "缺少childId");
|
|
|
+
|
|
|
+ // 从 DanAssessmentResult 查询大五人格数据
|
|
|
+ // 先返回默认结构供联调
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+ result.put("openness", 65);
|
|
|
+ result.put("conscientiousness", 78);
|
|
|
+ result.put("extraversion", 72);
|
|
|
+ result.put("agreeableness", 85);
|
|
|
+ result.put("neuroticism", 45);
|
|
|
+ return Result.success(result);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 关系数据 API — GET /api/contact/relationships**
|
|
|
+
|
|
|
+新建 `ContactController.java`(如不存在),或改造现有 Controller:
|
|
|
+
|
|
|
+```java
|
|
|
+@RestController
|
|
|
+@RequestMapping("/api/contact")
|
|
|
+public class ContactController {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ContactService contactService;
|
|
|
+
|
|
|
+ @PostMapping("/relationships")
|
|
|
+ public Result<?> getRelationships(@RequestAttribute(value = "userId", required = false) Long userId) {
|
|
|
+ if (userId == null) return Result.error(401, "请先登录");
|
|
|
+
|
|
|
+ // 从 Contact 表查询用户的关系数据,按关系类型分组计算各维度平均分
|
|
|
+ Map<String, Object> result = contactService.getRelationshipProfiles(userId);
|
|
|
+ return Result.success(result);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Contact 实体需要包含的评分字段(如不存在则增加):
|
|
|
+```java
|
|
|
+/** 关系类型:child/spouse/parent/peer/teacher/leader */
|
|
|
+private String relationType;
|
|
|
+/** 亲密度 1-5 */
|
|
|
+private Integer intimacy;
|
|
|
+/** 信任度 1-5 */
|
|
|
+private Integer trust;
|
|
|
+/** 沟通频率 1-5 */
|
|
|
+private Integer communication;
|
|
|
+/** 支持度 1-5 */
|
|
|
+private Integer support;
|
|
|
+/** 冲突处理 1-5 */
|
|
|
+private Integer conflict;
|
|
|
+/** 综合评分 */
|
|
|
+private Integer totalScore;
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+预期:`BUILD SUCCESS`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 9 — 验证 (P2)
|
|
|
+
|
|
|
+### Task 19: 完整验证清单
|
|
|
+
|
|
|
+- [ ] **Step 1: 前端 LSP 诊断**
|
|
|
+
|
|
|
+对每个修改过的前端文件运行 `lsp_diagnostics`:
|
|
|
+
|
|
|
+```bash
|
|
|
+# 需检查的文件清单
|
|
|
+cfc-frontend/pages.json
|
|
|
+cfc-frontend/pages/index/index.vue
|
|
|
+cfc-frontend/pages/index/parent-index.vue
|
|
|
+cfc-frontend/pages/index/child-index.vue
|
|
|
+cfc-frontend/pages/body/index.vue
|
|
|
+cfc-frontend/pages/mind/index.vue
|
|
|
+cfc-frontend/pages/action/index.vue
|
|
|
+cfc-frontend/pages/wealth/index.vue
|
|
|
+cfc-frontend/pages/teacher/index.vue
|
|
|
+cfc-frontend/pages/profile/profile.vue
|
|
|
+cfc-frontend/components/FamilyEnergyBar.vue
|
|
|
+cfc-frontend/components/DimensionProducts.vue
|
|
|
+cfc-frontend/components/DimensionActivities.vue
|
|
|
+cfc-frontend/utils/api.js
|
|
|
+```
|
|
|
+
|
|
|
+预期:所有文件 clean(0 error)
|
|
|
+
|
|
|
+- [ ] **Step 2: 后端编译**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+预期:`BUILD SUCCESS`
|
|
|
+
|
|
|
+- [ ] **Step 3: 功能验收**
|
|
|
+
|
|
|
+| # | 验收项 | 预期 | 检查结果 |
|
|
|
+|---|--------|------|---------|
|
|
|
+| 1 | TabBar 显示 | 首页 / 身泰 / 心智 / 行远 / 富沛 | |
|
|
|
+| 2 | TabBar 图标 | 5 个 Tab 图标均正常显示 | |
|
|
|
+| 3 | 身泰页游客访问 | 显示活动/商品列表,无价格,显示"登录查看" | |
|
|
|
+| 4 | 身泰页登录后 | 显示价格,任务可见 | |
|
|
|
+| 5 | 心智页布局顺序 | 能量柱 → 认知雷达图 → 大五人格 → 关系图 → 任务 → 活动 → 商品 | |
|
|
|
+| 6 | 心智页能量柱 | 堆叠显示心+智 | |
|
|
|
+| 7 | 心智页认知雷达图 | 6 边形(感知觉/注意力/记忆力/逻辑推理/空间想象/加工速度) | |
|
|
|
+| 8 | 心智页大五人格 | 5 边形 OCEAN,仅 10 岁以上 + 有 DAN 测评时显示 | |
|
|
|
+| 9 | 行远页布局顺序 | 关系雷达图 → 任务 → 活动 → 商品 | |
|
|
|
+| 10 | 行远页关系雷达图 | 6 类关系(孩子/另一半/父母/同伴/师长/领导),各显示五维雷达图 | |
|
|
|
+| 11 | 行远页 funcList | 商城导航到正确页面 | |
|
|
|
+| 12 | 未登录首页 | 显示商品列表(无价格) | |
|
|
|
+| 13 | 未登录点击商品 | 弹登录提示 | |
|
|
|
+| 14 | 家长首页 | 保留五行沙盘 + 新增 FamilyEnergyBar / 服务商入口 / 维度区块 | |
|
|
|
+| 15 | 家长首页服务商入口 | teacher 用户看到服务商工具卡片 | |
|
|
|
+| 16 | 富沛页未登录态 | 品牌头 + 登录引导 | |
|
|
|
+| 17 | 富沛页已登录态 | 品牌头 + 用户卡片 + 财富数据 + 勋章墙 + 分组菜单 + 成长报告 | |
|
|
|
+| 18 | 富沛页菜单分组 | 个人/财富/服务/设置 四组标题 | |
|
|
|
+| 19 | teacher/index 废弃 | 显示废弃提示页 | |
|
|
|
+| 20 | child-index reLaunch | 切换为 switchTab | |
|
|
|
+| 21 | body BASE_URL | 无 localhost:8080 硬编码 | |
|
|
|
+| 22 | RadarChart 组件 | 心智页/行远页均正确渲染 canvas 雷达图 | |
|
|
|
+| 23 | 后端编译 | mvn clean compile -q → BUILD SUCCESS | |
|
|
|
+
|
|
|
+- [ ] **Step 4: git 提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git status
|
|
|
+git add <相关文件>
|
|
|
+git commit -m "fix: align 5 tab pages implementation with v2.0 design
|
|
|
+
|
|
|
+- TabBar: 身体→身泰, 行动→行远, 我的→富沛
|
|
|
+- Remove teacher-index routing, soft-deprecate teacher/index
|
|
|
+- guest mode: products/activities visible without price
|
|
|
+- mind page: stacked energy bar + cognitive radar chart + Big Five chart
|
|
|
+- action page: 6-type relationship radar charts from Contact table
|
|
|
+- parent-index: keep sandbox, add FamilyEnergyBar + teacher entry + dim blocks
|
|
|
+- child-index: add FamilyEnergyBar + dimension blocks + switchTab
|
|
|
+- wealth page: full redesign matching v2.0 spec
|
|
|
+- new RadarChart canvas component for mind/action pages
|
|
|
+- backend: cognitive/big-five/contact APIs, product guest price, activity visibility"
|
|
|
+```
|