系列第 4 篇。本文介绍三国人物详情页如何设计数据模型与页面结构,让阅读、收藏、笔记、听书形成统一体验。

人物详情页

一、人物详情页的信息结构

一个完整的人物详情页至少需要:

  • 头像与基础信息
  • 势力、年份、标签
  • 人物简介
  • 生平经历
  • 关键事迹
  • 评价与影响
  • 年表
  • 相关人物
  • 收藏、笔记、听书入口

如果把这些内容都塞进一个长页面,用户会很累。更好的方式是使用 Tab 分组。

二、本地内容模型

人物模型通常包含基础字段和富文本字段:

export class Person {
  id: string = '';
  name: string = '';
  courtesyName: string = '';
  factionId: string = '';
  years: string = '';
  summary: string = '';
  avatar: ResourceStr = '';
  tags: string[] = [];
  biography: string = '';
  achievements: string[] = [];
  comments: string[] = [];
  timeline: string[] = [];
  relatedIds: string[] = [];
}

实际项目中可以根据内容复杂度拆分更多字段,但核心原则是:详情页显示的文本,就是听书朗读的内容来源。

三、听书入口为什么不能只依赖播放列表

一开始很多人会把“播放列表”当作唯一音频来源。但用户如果清空播放列表,再从人物详情点“听书”,就会找不到音频。

正确做法是分离:

  • 音频模板目录:能力来源,不被用户删除
  • 用户播放队列:用户当前列表,可编辑可清空

示例:

private ensureAudioInList(targetId: string): AudioRecord {
  const existed = this.audios().find((item: AudioRecord) => item.targetId === targetId);
  if (existed) {
    return existed;
  }
  const template = this.audioCatalog().find((item: AudioRecord) => item.targetId === targetId);
  const next = new AudioRecord(
    template.id,
    template.targetId,
    template.title,
    template.durationText,
    0,
    template.durationSeconds
  );
  this.audioRecords = this.audioRecords.concat([next]);
  return next;
}

四、收藏与笔记的 targetId 设计

收藏和笔记不要直接绑定页面路径,而应绑定业务目标:

interface FavoriteJson {
  id: string;
  targetId: string;
  targetType: string;
  title: string;
  summary: string;
  createdAt: string;
  updatedAt: string;
}

这样人物、事件、文章、地图标记都可以复用收藏逻辑。

五、页面布局建议

人物详情页适合卡片式分层:

Column() {
  this.personHeader(person);
  this.personActionBar(person);
  this.personTabs(person);
  this.personTabContent(person);
}
.padding(AppSpacing.S20)
.backgroundColor(this.palette().pageBg)

视觉上,头像和姓名是焦点;生平正文是阅读核心;听书按钮是高频操作入口。

六、避免内容和功能脱节

在这个项目中,一个人物详情页如果有听书按钮,就必须满足:

  • Person.id 能匹配 AudioRecord.targetId
  • 传记文本不为空
  • 收藏 target 存在
  • 搜索能命中姓名、简介、关键词

这类覆盖校验越早做越好。

七、详情页状态流设计

从工程角度看,人物详情页不是“展示一份静态人物资料”,而是把三类状态拼到一起:

  • 人物模板:来自本地人物仓库,只读
  • 用户状态:收藏、笔记、听书进度,可变
  • 页面状态:当前选中的 Tab、滚动位置、是否展开摘要,短生命周期

如果这三类状态混写在一个对象里,后续最容易出现的问题是:

  • 页面刷新后收藏状态丢失
  • 编辑笔记时误改人物模板
  • 切换人物后保留上一位人物的 Tab 或音频状态

比较稳的做法是把详情页渲染状态显式收敛成组合对象:

interface PersonDetailViewState {
  person: PersonModel;
  favorite: FavoriteRecord | null;
  note: NoteRecord | null;
  audio: AudioRecord | null;
  selectedTab: PersonTab;
}

页面渲染永远依赖这个组合状态,而不是从多个 service 零散读值。这样后续要做平板双栏布局、返回态恢复、断点续读时,状态边界会清楚很多。

八、调试命令与联调方法

人物详情页最容易出错的不是排版,而是状态联动。开发阶段我会优先用以下命令验证安装、拉起和日志:

hdc list targets
hdc install -r .\entry\build\default\outputs\default\entry-default-signed.hap
hdc shell aa force-stop com.example.recordofthreekingdoms
hdc shell aa start -a EntryAbility -b com.example.recordofthreekingdoms
hdc shell hilog | Select-String -Pattern "Person|Favorite|Audio|Note"

如果发现“详情页能打开,但听书按钮点了没反应”,优先检查这三件事:

  • Person.id 是否和 AudioRecord.targetId 一致
  • 详情页动作栏点击后是否真的调用了 service
  • 页面状态更新后是否触发了 ArkUI 重建

例如在详情页动作栏里,可以先加最小日志确认链路:

private async handleListen(person: PersonModel) {
  hilog.info(0x0000, 'PersonPage', 'listen target=%{public}s', person.id);
  const audio = await this.audioService.ensureForTarget(person.id);
  this.currentAudio = audio;
}

这类日志看起来简单,但能很快区分“按钮事件没触发”和“service 返回了错误数据”。

九、常见坑位与修复策略

1. 详情页收藏状态延迟刷新

如果只在详情页本地翻转一个 boolean,收藏页或首页统计可能不会同步。更稳的方式是:

  • 先执行 service 层写入
  • 再重新读取当前 targetId 的收藏状态
  • 最后整体回写详情页 view state

2. 笔记 target 绑定错对象

很多内容型 App 初版会把笔记只挂到“当前页面路由”,这会导致从收藏页、搜索结果页进入人物详情时,笔记找不到。人物详情页必须始终以 personId 为主键,而不是页面入口。

3. 听书入口只依赖当前播放列表

如果 ensureAudioInList() 没有兜底模板目录,用户清空列表后详情页会出现“有按钮但不能播”的体验断层。这也是人物详情页和听书页最容易脱节的地方。

4. 平板布局把人物正文拉得过宽

平板上不要简单把手机布局放大。对人物正文这种长文本区域,建议限制正文容器最大宽度,否则阅读行长过长,信息密度会明显下降。

Column() {
  this.personBiography(person);
}
.constraintSize({ maxWidth: 960 })
.alignSelf(ItemAlign.Center)

十、工程实现与验收清单

人物详情页是内容型 App 的核心页面,它会同时连接静态内容、用户行为和音频能力。详情页可以使用组合模型,把人物静态信息和用户侧状态合并到一个渲染对象里。

interface PersonDetailState {
  person: PersonModel;
  isFavorite: boolean;
  note?: NoteRecord;
  audio?: AudioRecord;
}

页面初始化时按 personId 拉取人物模板,再并行读取收藏、笔记和听书进度:

private async loadPersonDetail(personId: string) {
  const person = PersonRepository.findById(personId);
  const isFavorite = await this.favoriteService.has(personId);
  const note = await this.noteService.findByTargetId(personId);
  const audio = await this.audioService.findByTargetId(personId);
  this.detailState = { person, isFavorite, note, audio };
}

验收用例建议覆盖:

用例 期望结果
进入人物详情后点击收藏 收藏按钮立即变为选中态
返回列表再进入同一人物 收藏状态保持
在详情页编辑笔记 笔记内容保存到对应 targetId
从收藏页进入人物详情 能恢复人物内容和笔记入口
删除笔记后重启 App 不显示旧笔记
清空听书列表后从详情页点听书 能重新创建音频记录
平板横屏阅读人物正文 行长不过宽,Tab 与正文关系清晰

人物详情页不是简单展示字段,而是内容、交互和能力的汇聚点。下一篇会讲收藏与笔记:如何用 Preferences 保存 JSON,并保证页面即时刷新。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐