在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

鸿蒙 Next 反向导师平台 App 开发实战:导师列表 + 申请系统 + 状态管理

作者:duluo
SDK 版本:HarmonyOS API 24 (Next)
开发工具:DevEco Studio
语言框架:ArkTS + ArkUI
字数:约 9200 字


目录

  1. 引言
  2. 产品概念与导师模型
  3. 三 Tab 架构设计
  4. 首页布局设计
  5. 导师卡片组件
  6. 详情弹窗与申请流程
  7. 状态管理系统
  8. Builder 参数传递与 const 约束
  9. 编译错误全记录
  10. 第二十九款 App 全景回顾
  11. 结语

1. 引言

1.1 什么是"反向导师"

传统导师制是"年长的导师指导年轻的学员"。反向导师制则相反——年轻的导师指导年长的学员

这个概念由普华永道在 2010 年代率先实践:让千禧一代员工反向指导高管团队,教授社交媒体、数字工具和新兴技术。结果出奇地好——高管们学到了新技能,年轻员工感受到了价值认可。

在中国,反向导师有更广阔的应用场景:

城市老人:2.8 亿 60 岁以上人口
智能手机普及率:超过 60%
能熟练使用手机挂号/购物/打车:不到 20%

这中间的"40% 的普及率差距 × 20% 的技能差距",就是反向导师的价值空间。年轻人花 2 小时教一位长辈用手机挂号,省去的是长辈去医院排队的半天时间。

“反向导师平台” App 将这个概念搬到了移动端:这里有 6 位年轻耐心的导师,教授手机使用、微信功能、短视频制作、线上购物等数字技能。学员(主要是长辈)可以浏览导师、申请指导、建立连接。

1.2 本 App 的定位

本 App 是系列中第二十九款,也是第二款"社交/平台类"App(第一款是 App 23 情绪漂流瓶回信)。但两款 App 的平台逻辑不同:

情绪漂流瓶(#23) 反向导师平台(#29)
连接方式 匿名随机匹配 实名双向选择
核心操作 扔出/捞取 浏览/申请
数据流 创建→匹配→回复 查看→申请→连接
状态管理 单条记录 isReplied 双数组 requests + connections
导师数据 6 位预置导师

1.3 本 App 的技术特色

  1. 卡片复用机制buildMentorCard 在首页和导师列表两个 Tab 中复用,减少代码重复
  2. 双状态数组requestsconnections 两个数组管理申请和连接状态
  3. Builder 参数传递buildMentorStatusCard(id, icon, text, color) 通过参数控制渲染内容
  4. 主题适配:蓝色主色调传达专业信赖感
  5. Grid 分类展示:6 大技能分类 2×3 Grid 展示

1.4 二十九款 App 全景

App 数量:    29
代码总行数:  ~17,000 行
编译错误数:  ~275 个
博客总字数:  ~291,000 字
技术博客数:  29 篇

2. 产品概念与导师模型

2.1 功能需求

用户故事 1:我想看看有哪些年轻导师可以教我
用户故事 2:我想知道每个导师会什么、教过多少人
用户故事 3:我想申请一位导师指导我
用户故事 4:我想看看我已经申请了哪些导师

功能清单:
├── F1: 首页展示(欢迎卡片 + 技能分类 + 推荐导师)
├── F2: 导师列表(全部 6 位导师)
├── F3: 导师详情弹窗(简介、技能标签、评分)
├── F4: 申请指导(发送请求)
├── F5: 我的导师(待确认 + 已连接分组)
└── F6: Grid 技能分类

2.2 数据模型

interface Mentor {
  id: number;
  name: string;
  age: number;
  avatar: string;
  title: string;
  skills: string[];
  intro: string;
  rating: number;
  sessions: number;
}

9 个字段,涵盖了导师的完整信息:

字段 类型 说明
id number 唯一标识
name string 导师姓名
age number 年龄,让学员了解导师背景
avatar string emoji 头像
title string 称号,一句话概括导师特色
skills string[] 可教技能列表
intro string 详细自我介绍
rating number 评分 1-5
sessions number 已指导次数

2.3 6 位导师的差异化设计

6 位导师覆盖了不同技能方向,避免重复:

年龄分布:20, 21, 22, 23, 24, 25
技能覆盖:基础操作、社交、购物、医疗、AI、硬件
评分分布:4.6 ~ 5.0
指导次数:67 ~ 312

每位导师的 title 和 intro 经过差异化设计——小周强调"零基础友好"(年龄最大 86 岁的学生),小吴侧重"AI 应用"(最新技术方向),小陈突出"生活办事"(使用频率最高的场景)。学员可以根据自己的需求选择最匹配的导师。


3. 三 Tab 架构设计

3.1 Tab 配置

build() {
  Stack() {
    Column().backgroundColor(C.bg)

    Column() {
      this.buildHeader()
      if (this.activeTab === 0) this.buildHomeTab()
      else if (this.activeTab === 1) this.buildBrowseTab()
      else this.buildMyTab()
      this.buildTabBar()
    }

    if (this.showDetail) this.buildDetailOverlay()
  }
}
Tab 图标 功能 用户意图
0 🏠 首页 — 推荐 + 分类 “有什么导师?”
1 👥 导师 — 全部列表 “看看所有导师”
2 📋 我的 — 申请/连接 “我申请了谁?”

弹窗设计:本 App 只有一个弹窗(导师详情),从首页和导师列表两个入口都能打开。详情弹窗是"单一事实来源"——修改状态后,所有 Tab 同时更新。

3.2 导师卡片的复用

// 首页调用
ForEach(MENTORS, (m: Mentor, idx: number) => {
  this.buildMentorCard(m, idx)
}, (m: Mentor) => m.id.toString())

// 导师列表调用(完全复用)
ForEach(MENTORS, (m: Mentor, idx: number) => {
  this.buildMentorCard(m, idx)
}, (m: Mentor) => 'b' + m.id.toString())

buildMentorCard 在首页和导师列表 Tab 中被完全复用。两个 Tab 的区别仅在于:

  1. 列表前缀:首页显示在推荐分类下方,导师列表显示"共 N 位导师可预约"
  2. ForEach key 前缀:首页用 m.id.toString(),导师列表用 'b' + m.id.toString()

为什么 key 要加前缀?两个 ForEach 如果使用相同的 key,ArkTS 可能会混淆它们的状态。不同 Tab 的 ForEach 使用不同的 key 前缀可以绝对避免这个问题。

3.3 数据流

                    ┌─→ 首页 Tab(读取 MENTORS 常量)
                    │
用户操作 → 点击卡片 → 详情弹窗 → 申请 → requests[ ]
                                                ↓
                    ┌─→ 首页:更新导师卡片状态(已申请/已连接)
                    │
                    ┌─→ 导师列表:同步更新
                    │
                    ┌─→ 我的 Tab:读取 requests + connections

设计要点:所有数据通过 @State 管理,不需要跨组件通信。requestsconnections 两个数组变更后,所有 Tab 自动刷新。


4. 首页布局设计

4.1 欢迎卡片

Column() {
  Text('欢迎来到反向导师平台 🌟').fontSize(18)
    .fontColor(C.text).fontWeight(FontWeight.Bold)
  Text('这里有年轻耐心的导师,\n教你使用手机和数字工具。\n选一位你喜欢的导师,开始学习吧!')
    .fontSize(14).fontColor(C.textLight).lineHeight(22)
}.padding(16).backgroundColor(C.bgCard).borderRadius(16)

欢迎卡片是首页的第一个元素,用三段式文案传达 App 的核心价值:

  1. “年轻耐心的导师” → 强调反向导师的特色(年轻)
  2. “教你使用手机和数字工具” → 明确教学范围
  3. “选一位你喜欢的导师” → 引导行动

三段文案用 \n 换行,在卡片中呈现三行阅读节奏。

4.2 技能分类 Grid

Grid() {
  ForEach(this.getCategories(), (cat: string[]) => {
    GridItem() {
      Column() {
        Text(cat[0]).fontSize(28)
        Text(cat[1]).fontSize(13).fontColor(C.text).fontWeight(FontWeight.Medium)
        Text(cat[2] + ' 位导师').fontSize(11).fontColor(C.textMuted)
      }.height(100).backgroundColor(C.bgCard).borderRadius(14)
    }
  }, (cat: string[]) => cat[1])
}.columnsTemplate('1fr 1fr').rowsGap(10).columnsGap(10)

6 个分类以 2×3 Grid 展示:📱手机基础、💬微信使用、📷拍照视频、🛒线上购物、🏥挂号就医、🤖AI工具。

每个分类卡片展示:大 emoji(28sp)→ 分类名称 → 导师数量。卡片高度固定 100px,2 列布局在手机屏幕上刚好填满宽度。

4.3 推荐导师列表

欢迎卡片和技能分类下方是完整的导师列表,与 Tab 1(导师列表)的卡片完全相同。这是有意为之——用户在首页浏览时就能看到所有导师,不需要切换到专门的 Tab。Tab 1 的作用是在"首页信息太多时"提供一个干净的列表视图。


5. 导师卡片组件

5.1 卡片布局

@Builder
buildMentorCard(m: Mentor, idx: number) {
  Column() {
    Row() {
      Text(m.avatar).fontSize(40)
      Column() {
        Text(m.name + ' · ' + m.age + '岁').fontSize(16).fontColor(C.text).fontWeight(FontWeight.Bold)
        Text(m.title).fontSize(12).fontColor(C.primary).margin({ top: 1 })
      }.margin({ left: 10 }).layoutWeight(1)

      Column() {
        Text('⭐ ' + m.rating).fontSize(13).fontColor(C.warm).fontWeight(FontWeight.Bold)
        Text(m.sessions + ' 次').fontSize(10).fontColor(C.textMuted)
      }
    }

    Text(m.intro).fontSize(13).fontColor(C.textLight).maxLines(2).margin({ top: 6 })

    Row() {
      ForEach(m.skills, (s: string) => {
        Text(s).fontSize(11).fontColor(C.primary)
          .backgroundColor(C.primaryDim)
          .padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8)
      })
      Blank()
      // 状态标签
    }
  }.onClick(() => { /* 打开详情 */ })
}

卡片从上到下的信息层级:

层级 内容 视觉权重
1 头像 + 姓名年龄 + 评分 最高(40sp emoji + 粗体)
2 称号 中等(12sp 蓝色)
3 简介(最多 2 行) 较低(13sp 灰色)
4 技能标签 + 状态 最低(11sp 小标签)

layoutWeight(1) 确保姓名列撑满剩余空间,评分列右对齐。

5.2 技能标签

ForEach(m.skills, (s: string) => {
  Text(s).fontSize(11).fontColor(C.primary)
    .backgroundColor(C.primaryDim)
    .padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8)
    .margin({ right: 4 })
}, (s: string) => s)

技能标签使用蓝色文字 + 透明蓝色背景(C.primaryDim),通过 borderRadius(8) 形成药丸形状。每个标签之间有 4px 右边距。

5.3 状态标签

if (this.isConnected(m.id)) {
  Text('已连接 ✓').fontSize(13).fontColor(C.accent)
} else if (this.isRequested(m.id)) {
  Text('待确认').fontSize(13).fontColor(C.warm)
}

卡片右下角显示当前状态:绿色"已连接"或橙色"待确认"。状态通过 isConnected()isRequested() 两个方法判断,这两个方法使用 Array.indexOf() 检测 id 是否存在于对应的状态数组中。

状态标签的视觉设计遵循"绿色=已完成、橙色=进行中、无色=未操作"的通用色彩语义,与市面上大多数社交平台的配色一致——用户不需要学习就能理解。

5.4 卡片交互与状态联动

卡片点击打开详情弹窗,在弹窗中执行"申请"操作后,状态的变更流程如下:

点击"申请指导" → requests 数组更新
     ↓
@State 检测到数组变化 → 重新渲染所有引用 requests 的组件
     ↓
首页导师卡片 → isRequested() 返回 true → 显示"待确认"
导师列表卡片 → 同步更新
我的 Tab → 新增一条待确认记录
详情弹窗 → 按钮从"申请指导"变为"已发送申请"

这个联动不需要手动触发刷新——@State 的响应式系统会自动完成。这也是 ArkUI 声明式编程的核心优势:你只需要管理数据,UI 会自动同步


6. 详情弹窗与申请流程

6.1 弹窗布局

详情弹窗展示导师的完整信息:

┌──────────────────────────────┐
│  🧑‍💻                         │
│  小林 · 22岁                  │
│  数字原生代 · 编程达人         │
│  ─────────────────────────   │
│  ⭐ 4.9   已指导 156 次        │
│                              │
│  📖 简介                      │
│  从高中就开始教爷爷奶奶用手机…  │
│                              │
│  🛠️ 可教技能                  │
│  [手机使用] [微信功能] [AI工具] │
│                              │
│      🙋 申请指导              │
└──────────────────────────────┘

弹窗高度为 72%,留出上下空间显示遮罩层。可滚动的内容区域让弹窗在信息多时也能完整展示。

6.2 申请流程

sendRequest(id: number): void {
  if (this.isRequested(id) || this.isConnected(id)) return;
  this.requests = [id, ...this.requests];
  this.showDetail = false;
  promptAction.showToast({ message: '📩 申请已发送!等待导师确认' });
}

三步流程:

用户点击"申请指导"
  ↓
check:是否已申请或已连接?→ 是 → 不操作
  ↓
否 → requests 头部插入导师 id
  ↓
关闭弹窗 → Toast 提示
  ↓
所有 Tab 自动更新(isRequested 返回 true)

这个流程模拟了"发送申请"的体验,但实际的"确认"操作(将 requests 中的 id 移到 connections)在本版本中由用户手动执行——这是"最小可行版本"的设计选择。

6.3 弹窗中的状态展示

if (this.isConnected(MENTORS[this.selectedMentor].id)) {
  Text('✅ 已连接')
} else if (this.isRequested(MENTORS[this.selectedMentor].id)) {
  Text('⏳ 已发送申请,等待确认')
} else {
  Text('🙋 申请指导').onClick(() => { this.sendRequest(...) })
}

弹窗底部的按钮有三种状态,与卡片上的状态标签一致。这个三重条件渲染使用了 ArkTS 标准的 if-else if-else 语法——在 @Builder 中,if-else 是允许的,只要条件表达式不包含变量声明。


7. 状态管理系统

7.1 双数组状态

@State connections: number[] = [];
@State requests: number[] = [];

这是本 App 最核心的状态设计。两个数组分别存储已连接和已申请的导师 ID。数组元素是 number(导师 id),而不是复杂的对象结构。

为什么用 id 而不是对象:两个原因。第一,导师数据存储在常量 MENTORS 中,不需要重复存储。第二,indexOf(id)find(m => m.id === id) 更简洁。

为什么不用 Map 或 Set:ArkTS 对 Map 和 Set 的支持有限(预览器中可能完全不可用)。数组 + indexOf 是最稳定、最兼容的方案。

为什么是两个数组而不是一个状态字段:如果给 Mentor 模型加一个 status 字段(如 'none' | 'requested' | 'connected'),有两种方案:

方案 A(状态字段):

// 修改 Mentor 模型,但 Mentor 是常量,不可修改
interface Mentor { status: string; } // ❌ 不能修改常量

方案 B(双数组):

@State requests: number[] = [];   // 存储申请的导师 id
@State connections: number[] = []; // 存储已连接的导师 id
// ✅ 常量不变,状态分离

方案 B 胜出。它保持了 Mentor 数据的纯净(纯常量,不包含运行时状态),同时通过两个数组的组合即可推导出完整的导师状态矩阵。

7.2 状态查询

isConnected(id: number): boolean {
  return this.connections.indexOf(id) >= 0;
}

isRequested(id: number): boolean {
  return this.requests.indexOf(id) >= 0;
}

两个方法用于检测导师的状态。Array.indexOf() 返回 -1 表示未找到,>= 0 表示找到。这个方法在 ForEach 的回调中被调用(每张卡片渲染时判断状态),时间复杂度 O(n) 对最多 6 位导师来说完全可接受。

7.3 状态更新

sendRequest(id: number): void {
  if (this.isRequested(id) || this.isConnected(id)) return;
  this.requests = [id, ...this.requests];
  this.showDetail = false;
  promptAction.showToast({ message: '📩 申请已发送!等待导师确认' });
}

this.requests = [id, ...this.requests] 创建新数组并触发 @State 更新。头部插入确保最新的申请在最前面。

@State 更新机制this.requests = [...] 创建一个新数组引用。ArkTS 的 @State 通过引用比较检测变化——只有新数组才会触发渲染更新。

7.4 状态持久化的缺失

本 App 没有实现数据持久化。重启 App 后,requests 和 connections 会清空。这是一个有意识的设计决策:在概念验证阶段,不引入持久化可以保持代码简洁,并确保预览器兼容性。


8. Builder 参数传递与 const 约束

8.1 问题:Builder 中不能有 const

在本 App 的开发中,遇到了一个"熟悉的陌生错误"——10905209(@Builder 中写逻辑)。

具体场景:在 buildMyTab() 的 ForEach 中,需要根据导师 id 查找导师信息。直接写 const mentor = this.findMentor(id) 触发了编译错误。

// ❌ 错误:@Builder 中的 ForEach 回调也不能使用 const
ForEach(this.requests, (id: number) => {
  const mentor = this.findMentor(id);  // 10905209
  Column() {
    Text(mentor.avatar).fontSize(32)
    Text(mentor.name).fontSize(15)
  }
})

这个错误的"熟悉"之处在于,10905209 已经在这个系列中出现了超过 100 次。但"陌生"之处在于,这次错误出现在 ForEach 的回调中,而不是 Builder 方法体的顶层。

ArkTS 的约束范围:@Builder 方法体中不允许变量声明,这个约束不仅适用于 Builder 方法体的顶层代码,也适用于 Builder 方法中所有嵌套闭包——包括 ForEach 的回调、事件处理器的闭包、条件渲染的分支。只要是 @Builder 调用链中的任何函数,都不能使用变量声明。

8.2 第一次修复:提取 Builder 方法

解决方案是创建一个专门渲染状态卡片的 @Builder 方法,通过参数传递需要的值:

// ✅ 修复:Builder 方法通过参数接收数据
@Builder
buildMentorStatusCard(id: number, statusIcon: string, statusText: string, statusColor: string) {
  Column() {
    Row() {
      Text(this.getMentorAvatar(id)).fontSize(32)
      Text(this.getMentorName(id)).fontSize(15)
      Text(statusText).fontSize(12).fontColor(statusColor)
      Text(statusIcon).fontSize(22)
    }
  }
}

这个方法接受 4 个参数:导师 id、状态图标、状态文字、状态颜色。通过参数控制渲染内容,避免了在 Builder 内部查找数据。

8.3 第二次修复:辅助方法

buildMentorStatusCard 内部仍然需要根据 id 获取导师的姓名和头像。直接在 @Builder 中写 const mentor = this.findMentor(id) 也不行。

最终方案:创建两个辅助方法:

getMentorAvatar(id: number): string {
  const m = this.findMentor(id);
  return m !== undefined ? m.avatar : '👤';
}

getMentorName(id: number): string {
  const m = this.findMentor(id);
  return m !== undefined ? m.name : '未知';
}

注意:const m 出现在辅助方法中而不是 @Builder 中——这是关键的区别。辅助方法是普通的实例方法,不受 @Builder 的语法约束。

8.4 教训总结

这是 29 款 App 中第 N 次遇到 10905209 错误。但这次的特殊之处在于:错误出现在 ForEach 的回调中,而不是直接出现在 Builder 的方法体中。

@Builder buildMethod() {
  // ❌ 直接写 const  → 错误 10905209
  ForEach(arr, (item) => {
    // ❌ 回调中写 const → 也是错误 10905209!
    const x = item.prop; 
  })
}

ArkTS 的规则是:@Builder 方法体及其所有嵌套的闭包(包括 ForEach 回调)中,都不允许变量声明。所有数据必须通过方法返回值或 Builder 参数传入。


9. 编译错误全记录

9.1 错误概览

本 App 共出现 6 个编译错误

# 错误代码 位置 原因 修复
1 10505001 L343 Row 不支持 wrap(true) 移除 wrap
2 10905209 L261 ForEach 回调中 const mentor = ... 提取 Builder 方法
3 10905209 L281 ForEach 回调中 const mentor = ... 提取 Builder 方法
4 10905209 L315 const m = MENTORS[...] 改为内联
5 10905209 Builder 内 const mentor 第二次出现 提取辅助方法
6 10905209 Builder 内 const mentor 第三次出现 提取辅助方法

9.2 错误分布

10505001(属性不存在):1 个 → 17%
10905209(Builder 逻辑):5 个 → 83%

10905209 占总数的 83%,再次验证了它是 ArkTS 开发中最高频的错误类型。

9.3 row.wrap(true) 错误

// ❌ 错误
Row() {
  ForEach(m.skills, (s: string) => {
    Text(s) // 技能标签
  })
}.width('100%').wrap(true)  // Row 不支持 wrap

在 ArkTS 中,Row 组件没有 wrap 属性。如果需要在行内换行显示子元素,应该使用 Flex 组件:

// ✅ 修复:移除 .wrap(true)
Row() {
  // 技能标签在 Row 中水平排列
}.width('100%')

技能标签通常 2-3 个,刚好在一行内显示,不需要换行。如果未来技能数量增加到 5 个以上,可以考虑使用 Flex 替代 Row。

9.4 29 款 App 的错误趋势

App 1:   16  ← 入门
App 10:  11  ← 模式形成
App 20:   2  ← 高效期
App 24:  48  ← AI 新领域(波峰)
App 25:   3  ← 回归基线
App 26:   8  ← 全部同一类型
App 27:   5  ← 全部低级错误
App 28:   8  ← 预览器兼容性
App 29:   6  ← Builder const 约束(83%)

App 29 的错误主要集中在一个类型(10905209),且全部可以通过"提取方法"或"直接内联"修复。没有出现新的错误类型,也没有触发"未知错误"的恐慌。


10. 第二十九款 App 全景回顾

10.1 数据总览

指标 数值
代码行数 373 行
编译错误数 6 个(修复后 0)
@State 变量 6 个
@Builder 方法 7 个
弹窗数 1 个
预置导师 6 位
外部依赖 0 个
页面数(Tab) 3 页

10.2 29 款 App 的 Builder 方法数量

App 1:   8 个 Builder
App 8:   12 个 Builder
App 16:  10 个 Builder
App 24:  14 个 Builder
App 25:  6 个 Builder
App 26:  6 个 Builder
App 27:  2 个 Builder
App 28:  6 个 Builder
App 29:  7 个 Builder

Builder 方法的数量从早期的 12-14 个下降到近期的 6-7 个。原因是:更多的 UI 直接内联到 ForEach 中,而不是提取为独立的 Builder 方法。

但 Builder 方法也不是越少越好——App 29 的 7 个 Builder 中,有 1 个(buildMentorStatusCard)是专门为了修复编译错误而创建的。这个 Builder 本身就是因为"不能在 ForEach 回调中写 const"才被提取出来的。

10.3 29 款 App 的经验沉淀

已经不会再犯的错误(第 29 次确认)

  • 颜色常量忘记加 interface ✅
  • @State 数组直接修改 ✅
  • ForEach key 函数作用域 ✅

偶尔还会犯的错误(第 29 次仍然出现)

  • @Builder 中写逻辑 ✅(10905209,几乎每款 App 都遇到)

新发现的错误类型(本 App)

  • Row 不支持 wrap

10.4 从 1 到 29

第 1 款:学习 @Builder 的基本语法
第 8 款:理解 @Builder 中不能写逻辑
第 16 款:掌握 ForEach 的正确用法
第 24 款:44 个错误中学会索引相关规则
第 29 款:Builder 回调中也不能写 const

29 款 App,每一款都贡献了至少一个"新教训"。这些教训中,大部分是 ArkTS 的语法约束(不能写逻辑、不能解构、不能索引签名),少部分是组件 API 的限制(Row 不支持 wrap、onLongClick 不存在)。


11. 结语

11.1 反向导师的社会意义

"反向导师平台"这个 App,技术上并不复杂——没有 AI、没有实时通信、没有复杂的匹配算法。但它的社会意义可能超过了许多技术更复杂的 App。

在中国,有超过 2.8 亿 60 岁以上人口。他们中的绝大多数人拥有智能手机,但真正能熟练使用的比例很低。不是因为"学不会",而是因为"没人耐心教"。

反向导师——让年轻一代教年长一代数字技能——是一个低成本、高收益的社会创新。年轻人花几小时教长辈用手机挂号,可能节省的是长辈跑一趟医院的大半天时间。

11.2 平台类 App 的起点

本 App 是系列中第一款"平台类"App——连接两端的用户。虽然目前只有预置数据(没有真实的导师注册和学员注册),但它的架构可以轻松扩展:

扩展一:导师注册系统
// 当前:预置数据
const MENTORS: Mentor[] = [ /* 6 条 */ ];

// 扩展后:用户注册 + 审核
@State registeredMentors: Mentor[] = [];
// 新增导师注册表单:姓名、年龄、技能、简介
// 新增审核状态:pending / approved / rejected

当前使用 const MENTORS 存储导师数据。扩展为注册系统后,只需要将存储方式从 const 改为 @State,并添加注册表单即可。导师卡片组件(buildMentorCard)完全不需要修改——它读取的是 Mentor 接口,不管数据来自预置常量还是用户注册。

扩展二:评价系统

当前导师卡片显示 ratingsessions 两个统计字段,目前是硬编码的。扩展为真实评价系统后:

@State reviews: Review[] = [];
// Review: { mentorId, userId, rating, comment, date }
// 统计:根据 reviews 计算每个导师的 rating 和 sessions

getMentorRating(id)getMentorSessions(id) 两个方法可以根据 reviews 动态计算,不需要修改 Mentor 模型。

扩展三:消息系统

当前"申请指导"只是将 id 加入 requests 数组。扩展为真实消息系统后:

@State messages: Message[] = [];
// Message: { id, from, to, content, time, type: 'request' | 'chat' }
// 申请 → 自动创建一条 request 类型消息
// 导师确认 → 消息状态更新为 accepted
// 之后可以自由聊天

从"状态数组 + Toast 提示"升级为"消息列表 + 实时通知",是平台类 App 的自然演进路径。

这三个扩展的共性是:当前的数据结构和组件设计可以无缝升级。Mentor 接口不需要改,导师卡片组件不需要改,三个 Tab 的架构不需要改——只需要添加新的 @State 和对应的 UI。

11.3 给开发者的建议

  1. @Builder 的 const 约束比你想象的更严格:不仅 Builder 方法体不能写 const,Builder 中 ForEach 的回调也不能写。所有数据都要通过方法调用获取
  2. 双数组状态管理适合"一对多"关系:一个用户对多个导师的关系,可以用两个 id 数组轻松管理
  3. 卡片复用从设计时就要考虑:如果同一个卡片在两个 Tab 中出现,一开始就设计成复用的 Builder 方法
  4. 技能标签的 Row 不需要 wrap:3 个以内的标签刚好一行,5 个以上才需要考虑换行
  5. 平台类 App 从预置数据开始:不需要一开始就做注册系统,6 条预置数据足够展示产品概念

11.4 感谢

29 款 App、29 篇博客、约 291,000 字。

从白噪音到反向导师平台,从工具到平台,从个人到连接——这个系列的 29 款 App 覆盖了鸿蒙 Next 应用开发的多个维度。

第 29 篇不是终点。系列还有更多 App、更多错误、更多教训等待记录。

现在,打开 DevEco Studio,写下你的第 1 款 App——也许它会是第 30 篇博客的主角。


附录 A:核心代码速查

导师模型

interface Mentor {
  id: number; name: string; age: number;
  avatar: string; title: string;
  skills: string[]; intro: string;
  rating: number; sessions: number;
}

发送申请

sendRequest(id: number): void {
  if (this.isRequested(id) || this.isConnected(id)) return;
  this.requests = [id, ...this.requests];
  this.showDetail = false;
}

状态查询

isConnected(id: number): boolean {
  return this.connections.indexOf(id) >= 0;
}
isRequested(id: number): boolean {
  return this.requests.indexOf(id) >= 0;
}

附录 B:色板

变量 用途
C.bg #F0F5FF 主背景(冰蓝)
C.bgCard #FFFFFF 卡片背景
C.primary #4A7CF7 主色(信赖蓝)
C.accent #34C759 已完成(绿色)
C.warm #FF8A4C 评分/待确认(暖橙)

附录 C:Builder 中禁止的操作速查

在 @Builder 中:
  ❌ const x = value     → 方法中获取
  ❌ let x = value       → 方法中获取
  ❌ for (...)           → 方法中计算
  ❌ if (x = get())      → 方法中获取
  ✅ if (condition) { Component() }   → 允许
  ✅ this.method()       → 允许(调用方法)
  ✅ inline expression   → 允许

在 @Builder → ForEach 回调中:
  ❌ const x = item.prop → 同上
  ✅ if (condition) { }  → 允许
  ✅ this.method(item)   → 允许

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐