【观止·诗史汇 HarmonyOS 实战系列 06】时间轴与历史事件:把朝代、事件和兴衰节点串起来
【观止·诗史汇 HarmonyOS 实战系列 06】时间轴与历史事件:把朝代、事件和兴衰节点串起来
前五篇已经完成了从工程骨架到诗文阅读链路的铺垫:第一篇讲《观止·诗史汇》为什么是一个本地优先的诗文与历史学习 App,第二篇拆 entry / features / commons 三层边界,第三篇落地首页,第四篇把诗文内容包整理成可检索的本地诗库,第五篇把诗文详情页扩展成“读、听、收、记、统计”的学习节点。
第六篇开始,主线从“读一首诗”转向“理解一段历史”。诗文学习如果没有时间感,很多典故、作者处境和情绪来源都会悬空。杜甫的沉郁、陆游的北望、辛弃疾的豪放,都离不开朝代兴替和历史事件。时间轴模块要解决的不是“列出很多年份”,而是把朝代顺序、关键事件、兴衰节点和详情阅读串成一条可理解的路径。
本文就沿着真实源码拆解:TimelinePage 如何组织朝代筛选、兴衰入口和年份节点,HistoryPack 如何把 doc/lishi 的朝代内容注册进运行时数据,HistoryEvents 如何补充重大事件和兴衰节点,TimelineEventDetailPage 如何把事件背景、经过、影响、地点、相关诗文、收藏笔记和统计记录串起来。

上图应来自本机 DevEco 模拟器中打开《观止·诗史汇》的“时间轴”模块。第六篇的正文和封面图应该展示时间轴主体:顶部朝代筛选、兴衰入口卡、年份节点和朝代段,而不是首页,也不是其他模块的详情页。
本篇要解决什么问题
时间轴模块至少要解决四类问题:
| 问题 | 当前实现 |
|---|---|
| 朝代顺序从哪里来 | `HistoryPack.registerHistoryPack()` 注册 `HISTORY_PACK_DYNASTIES` |
| 页面如何展示时间线 | `TimelinePage` 中的 `YearNode()` + `DynastySegment()` |
| 如何筛选单个朝代 | `selectedDynasty` + 顶部横向 `FilterChip()` |
| 兴衰分析如何接入 | `XingShuaiCard()` 跳转 `AppRoutes.DYNASTY_INSIGHT` |
| 事件详情如何展示 | `TimelineEventDetailPage` 读取 `eventId`,展示背景、经过、影响 |
| 事件如何进入统计 | `StatsStore.recordEvent(eventId)` |
| 事件如何关联诗文和地理 | `relatedPoemIds`、`placeId`、`GeoService`、`PoemService` |
这和前三篇的“首页入口”也能对应起来:首页提供“今天看一件史事”,时间轴提供“按历史顺序理解这些史事”,事件详情页提供“深入读一个节点”。
源码对象总览
| 文件 | 职责 |
|---|---|
| `features/src/main/ets/timeline/TimelinePage.ets` | 时间轴首页,包含朝代筛选、兴衰入口、年份节点和朝代段 |
| `features/src/main/ets/timeline/TimelineEventDetailPage.ets` | 历史事件详情页,展示事件背景、经过、影响、地点和相关诗文 |
| `features/src/main/ets/services/HistoryPack.ets` | 历史内容包注册,把朝代数据和扩展事件并入运行时数据 |
| `features/src/main/ets/services/HistoryPackData.ets` | 19 个朝代的时间线数据源 |
| `features/src/main/ets/services/HistoryEvents.ets` | 扩展重大事件、转折节点和兴衰类型 |
| `features/src/main/ets/services/EventService.ets` | 事件查询服务 |
| `features/src/main/ets/services/DynastyService.ets` | 朝代查询服务 |
| `features/src/main/ets/state/AppStores.ets` | 事件阅读统计、收藏和笔记 |
| `commons/src/main/ets/router/RouteNames.ets` | `TIMELINE_EVENT_DETAIL`、`DYNASTY_OVERVIEW`、`DYNASTY_INSIGHT` 等路由 |
第六篇的核心并不是单个页面 UI,而是“历史内容包 → 时间轴 → 朝代详情/兴衰分析 → 事件详情 → 统计收藏笔记”的完整链路。
HistoryPack:先注册朝代,再展示时间轴
时间轴页面使用的是:
import { MOCK_DYNASTIES_TIMELINE } from '../services/HistoryPack';
HistoryPack 的职责是把基于 doc/lishi/*.md 的朝代内容写入运行时 mock 数据,并维护一个显式时间轴集合:
export const MOCK_DYNASTIES_TIMELINE: Dynasty[] = [];
export function registerHistoryPack(): void {
if (_registered) {
return;
}
_registered = true;
for (let i = 0; i < HISTORY_PACK_DYNASTIES.length; i++) {
const src: Dynasty = HISTORY_PACK_DYNASTIES[i];
const idx: number = MOCK_DYNASTIES.findIndex((d: Dynasty) => d.id === src.id);
if (idx >= 0) {
const old: Dynasty = MOCK_DYNASTIES[idx];
const merged: Dynasty = {
id: src.id,
name: src.name,
fullName: src.fullName,
startYear: src.startYear,
endYear: src.endYear,
capital: old.capital && old.capital !== '—' ? old.capital : src.capital,
founder: old.founder && old.founder !== '—' ? old.founder : src.founder,
brief: src.brief,
keyEvents: src.keyEvents,
inventions: src.inventions,
famousFigures: src.famousFigures,
turningPoints: src.turningPoints
};
MOCK_DYNASTIES[idx] = merged;
} else {
MOCK_DYNASTIES.push(src);
}
}
}
这里有一个阶段性工程取舍:项目还保留了 MOCK_DYNASTIES 这类运行时 mock 数据,但通过 HistoryPack.registerHistoryPack() 把文档整理后的历史内容并入进去。这样既不需要一次性推翻旧服务,也能让时间轴开始消费更完整的历史内容包。
注册后再构造时间轴显式集合:
for (let i = 0; i < HISTORY_PACK_DYNASTIES.length; i++) {
const id: string = HISTORY_PACK_DYNASTIES[i].id;
const d: Dynasty | undefined = MOCK_DYNASTIES.find((it: Dynasty) => it.id === id);
if (d) {
MOCK_DYNASTIES_TIMELINE.push(d);
}
}
MOCK_DYNASTIES_TIMELINE.sort((a: Dynasty, b: Dynasty) => a.startYear - b.startYear);
这说明时间轴不是随手把所有朝代数组拿来展示,而是显式挑选 HISTORY_PACK_DYNASTIES 中的 19 项,并按起始年升序排序。页面层因此可以假设数据已经按历史顺序就绪。
TimelinePage:页面状态很轻,只保留列表和筛选
时间轴页面状态很小:
interface TimelineState {
loading: boolean;
dynasties: Dynasty[];
}
@State state: TimelineState = { loading: true, dynasties: [] };
@State selectedDynasty: string = ALL_ID;
进入页面时直接复制时间轴数据:
async aboutToAppear() {
const dyn: Dynasty[] = MOCK_DYNASTIES_TIMELINE.slice();
this.state = { loading: false, dynasties: dyn };
}
这说明 TimelinePage 没有自己解析文档,也没有自己合并事件,更没有在页面里维护复杂缓存。它只消费已经注册好的朝代时间线。页面越轻,越容易把注意力放在布局和交互上。
页面骨架:筛选条、兴衰入口、时间轴主体
TimelinePage 的 build() 很直观:
Column() {
AppTopBar({ title: '时间轴', subtitle: '朝代兴替·古今事变' })
this.DynastyFilter()
this.XingShuaiCard()
if (this.state.loading) {
LoadingView({ message: '加载朝代时间轴…' }).layoutWeight(1)
} else if (this.state.dynasties.length === 0) {
EmptyView({ message: '暂无朝代数据', hint: '请检查内容包' }).layoutWeight(1)
} else {
Scroll() {
this.TimelineColumn()
}
}
}
可以拆成三层:
| 区域 | 作用 |
|---|---|
| `DynastyFilter()` | 横向筛选全部或单个朝代 |
| `XingShuaiCard()` | 进入兴替明鉴,查看当前朝代关键节点和归因分析 |
| `TimelineColumn()` | 用年份节点和朝代段串起历史顺序 |
这比一开始就展示事件列表更清晰。用户先看到朝代顺序,再进入某个朝代详情或兴衰分析;事件详情则是下一层。
朝代筛选:全部和单朝代共用同一套 chip
顶部筛选条:
@Builder
DynastyFilter() {
Scroll() {
Row({ space: AppDimens.spaceSm }) {
this.FilterChip(ALL_ID, '全部')
ForEach(this.state.dynasties, (d: Dynasty) => {
this.FilterChip(d.id, d.name)
}, (d: Dynasty) => d.id)
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.height(48)
.width('100%')
}
FilterChip() 只做一件事:根据 selectedDynasty 改变样式,并在点击时更新筛选值。
Text(label)
.fontSize(AppText.bodySm)
.fontColor(this.selectedDynasty === id ? '#FFFFFF' : AppColors.textPrimary)
.backgroundColor(this.selectedDynasty === id ? AppColors.accent : AppColors.cardBg)
.borderRadius(20)
.onClick(() => { this.selectedDynasty = id; })
筛选逻辑也很简单:
private filtered(): Dynasty[] {
if (this.selectedDynasty === ALL_ID) {
return this.state.dynasties;
}
return this.state.dynasties.filter((d: Dynasty) => d.id === this.selectedDynasty);
}
这里没有额外的搜索状态,也没有复制数组缓存。对于 19 个朝代的时间轴,直接过滤足够清楚。
兴衰入口:时间轴负责顺序,兴替明鉴负责归因
时间轴顶部保留了一个“兴衰”入口卡:
@Builder
XingShuaiCard() {
Row({ space: AppDimens.spaceMd }) {
Image($r('app.media.ic_milestone_turn'))
.width(36).height(36)
Column({ space: 4 }) {
Text('兴衰').fontSize(AppText.subtitle).fontWeight(FontWeight.Bold)
Text('查看' + this.currentDynastyLabel() + '的关键节点与归因分析')
}
Text('进入 →').fontSize(AppText.bodySm).fontColor(AppColors.accent)
}
.onClick(() => this.openDynastyInsight())
}
跳转时,如果当前选中“全部”,就默认用上古:
private openDynastyInsight(): void {
let id: string = this.selectedDynasty;
if (id === ALL_ID) {
id = 'd_ancient';
}
const params: NavigateParams = { dynastyId: id };
Navigator.push(AppRoutes.DYNASTY_INSIGHT, params);
}
这里的分工值得保留:
| 模块 | 职责 |
|---|---|
| 时间轴 | 讲顺序、年份、朝代段和入口 |
| 兴替明鉴 | 讲兴盛、危机、转折、衰亡的归因分析 |
如果把兴衰归因也塞进时间轴,页面会很快变重;如果时间轴只列朝代又没有兴衰入口,用户很难进一步理解“为什么变了”。当前设计让两者互相连接,但不互相吞并。
年份节点:用 startYear/endYear 串起朝代段
时间轴主体由 TimelineColumn() 生成:
Column() {
ForEach(this.filtered(), (d: Dynasty, idx: number) => {
this.YearNode(d.startYear, idx === 0)
this.DynastySegment(d)
if (idx === this.filtered().length - 1) {
this.YearNode(d.endYear, false)
}
}, (d: Dynasty) => d.id)
}
这里的展示思路是:
起始年份 → 朝代段 → 下一个朝代起始年份 → 朝代段 → 最后结束年份
YearNode() 负责把年份格式化:
private formatYear(y: number): string {
if (y < 0) return '前' + (-y);
return y.toString();
}
公元前年份用“前 xxx”展示,公元后直接展示数字。这个小处理让时间轴同时适配上古、夏商周和近现代。
朝代段:卡片展示 fullName 和 brief
朝代段 UI 使用 DynastySegment():
@Builder
DynastySegment(d: Dynasty) {
Row({ space: AppDimens.spaceSm }) {
Row() {
Column().width(2).height('100%').backgroundColor(AppColors.accent)
}
.width(56)
.justifyContent(FlexAlign.Center)
Column({ space: 4 }) {
Row({ space: AppDimens.spaceSm }) {
Text(d.fullName)
.fontSize(AppText.subtitle)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Text('详情 →')
.fontSize(AppText.captionXs)
.fontColor(AppColors.accent)
}
Text(d.brief)
.fontSize(AppText.bodySm)
.lineHeight(18)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.onClick(() => this.openDynasty(d.id))
}
}
它展示的不是“跨度多少年”,而是朝代名称和一句概述。源码注释里也写了这一点:新版时间轴“不使用跨度概念”,而是以“年份 → 朝代 → 年份 → 朝代”串联。
这个取舍适合学习 App。跨度是历史信息,但它不是用户理解历史顺序的第一需求。用户首先要知道“接下来是什么朝代”“这个朝代大致发生了什么”“点进去能看什么”。
点击朝代段:进入朝代概览
朝代段点击后进入朝代概览:
private openDynasty(id: string): void {
const params: NavigateParams = { dynastyId: id };
Navigator.push(AppRoutes.DYNASTY_OVERVIEW, params);
}
这让时间轴成为历史模块的导航页。它不负责把朝代详情、人物、制度、事件全部展示出来,而是提供可理解的路径:先看时间顺序,再进入朝代概览或兴替分析。
HistoryEvents:事件不只是日期,还带 nodeType
HistoryEvents.ets 中定义了扩展事件构造函数:
function ev(id: string, title: string, year: number, dynastyId: string,
category: string, summary: string, detail: string,
nodeType: DynastyNodeType, placeId: string): HistoryEvent {
return {
id, title, year, dynastyId, category, summary, detail,
relatedPoemIds: [], placeId: placeId || undefined,
nodeType: nodeType || undefined
};
}
每个事件至少包含:
| 字段 | 说明 |
|---|---|
| `id` | 稳定事件 ID |
| `title` | 事件标题 |
| `year` | 发生年份 |
| `dynastyId` | 所属朝代 |
| `category` | 政治、军事、文化、经济等 |
| `summary` | 摘要 |
| `detail` | 详细经过 |
| `nodeType` | 建立、兴盛、转折、危机、衰亡 |
| `placeId` | 可选地理位置 |
例如:
ev('e_qin_unify', '秦始皇统一六国', -221, 'd_qin', '政治',
'中国第一个中央集权大一统王朝建立。',
'前 230—前 221 年间秦先后灭韩赵魏楚燕齐六国;嬴政称"皇帝"...',
'found', 'g_changan')
nodeType 是第六篇很重要的字段。它让事件不仅是“某年发生了某事”,还能参与兴替分析:建立、兴盛、转折、危机、衰亡这些标签,后续可以被朝代洞察页、统计页和练习题复用。
TimelineEventDetailPage:事件详情不是单纯文本
事件详情页状态如下:
interface DetailState {
loading: boolean;
event: HistoryEvent | null;
dynasty: Dynasty | null;
place: GeoPlace | null;
poems: RelatedPoemRow[];
favored: boolean;
folders: FavoriteFolder[];
favoriteFolderId: string;
}
和诗文详情页类似,事件详情页也同时承接内容和行为:
| 状态 | 作用 |
|---|---|
| `event` | 事件主体 |
| `dynasty` | 补齐朝代名 |
| `place` | 补齐古今地理 |
| `poems` | 关联诗文 |
| `favored`、`folders`、`favoriteFolderId` | 收藏文件夹 |
进入页面时先读取路由参数:
const params: NavigateParams = Navigator.getParams();
const eid: string = params.eventId ?? '';
再读取事件、朝代、地点和相关诗文:
const e: HistoryEvent | null = await this.eventSvc.getById(eid);
const d: Dynasty | null = await this.dynastySvc.getById(e.dynastyId);
let p: GeoPlace | null = null;
if (e.placeId) p = await this.geoSvc.getById(e.placeId);
const poems: RelatedPoemRow[] = [];
for (let i = 0; i < e.relatedPoemIds.length; i++) {
const po: Poem | null = await this.poemSvc.getById(e.relatedPoemIds[i]);
if (po) poems.push({ id: po.id, title: po.title });
}
最后记录统计:
this.statsStore.recordEvent(eid);
这和第五篇的 recordPoem() 是同一套学习闭环:读诗记录诗文,读史记录事件。统计页最终可以展示用户的诗文阅读和历史阅读进度。
事件详情 HeaderCard:年份、朝代、类别、节点类型
事件详情顶部卡片展示关键信息:
Text(`${this.state.event!.year}`)
.fontSize(AppText.title)
.fontWeight(FontWeight.Bold)
.fontColor(AppColors.accent)
Text(this.state.dynasty ? this.state.dynasty.name : '')
.fontSize(AppText.bodySm)
.fontColor(AppColors.textSecondary)
Text(this.state.event!.category)
.fontSize(AppText.captionXs)
.fontColor(AppColors.accent)
如果事件有 nodeType,还会显示节点标签:
if (this.state.event!.nodeType && this.state.event!.nodeType!.length > 0) {
Text(this.nodeLabel(this.state.event!.nodeType!))
.fontSize(AppText.captionXs)
.fontColor('#FFFFFF')
.backgroundColor(AppColors.warning)
}
nodeLabel() 把内部枚举转成中文:
private nodeLabel(t: string): string {
if (t === 'found') return '建立';
if (t === 'prosper') return '兴盛';
if (t === 'turn') return '转折';
if (t === 'crisis') return '危机';
if (t === 'fall') return '衰亡';
return '';
}
这个标签让事件详情直接带上兴替语义。用户看到“秦始皇统一六国”是建立,看到“鸦片战争”是转折,看到“土木堡之变”是危机,理解会比单纯年份更快。
地点关联:事件可以跳到古今地理
如果事件有地点,详情页会展示古今地名:
if (this.state.place) {
Row({ space: 4 }) {
Text('发生地点:')
Text(`${this.state.place.ancientName}(${this.state.place.modernName})`)
.onClick(() => {
const p: NavigateParams = { geoId: this.state.place!.id };
Navigator.push(AppRoutes.GEO_DETAIL, p);
})
}
}
这就是第八篇“古今地理”的伏笔。历史事件不是孤立文本,它可以连接到地点;地点再反向连接到诗文、事件和朝代。时间轴因此不只是纵向时间线,也能和空间关系交织。
相关诗文:历史事件反向打开诗文
事件详情页还支持相关诗文:
@Builder
RelatedPoems() {
Column({ space: AppDimens.spaceSm }) {
ForEach(this.state.poems, (p: RelatedPoemRow) => {
Row() {
Text(p.title).layoutWeight(1)
Text('阅读 →')
}
.onClick(() => {
const params: NavigateParams = { poemId: p.id };
Navigator.push(AppRoutes.POEM_DETAIL, params);
})
}, (p: RelatedPoemRow) => p.id)
}
}
这里有一个待优化点:第五篇讲过诗文详情页最好同时拿到 poemId + poemShard。当前事件详情只传 poemId:
const params: NavigateParams = { poemId: p.id };
Navigator.push(AppRoutes.POEM_DETAIL, params);
这意味着如果相关诗文来自旧 PoemService,详情页可能需要兜底处理 poemShard 缺失;后续更好的做法是让事件关联诗文时携带内容包的 shard,或者在仓储里提供 getDetailById() 的索引兜底。这个问题放在文章里讲出来,比假装链路完美更有工程价值。
事件收藏和笔记:与诗文详情保持同一种交互
事件详情页也有收藏和笔记:
private onFavoriteTap(): void {
if (!this.state.event) return;
if (this.state.favored) {
this.favStore.removeFavorite('event', this.state.event.id);
this.choosingFolder = false;
this.updateFavoriteState(false, '');
return;
}
this.choosingFolder = !this.choosingFolder;
}
写入收藏时类型是 event:
this.favStore.addToFolder('event', this.state.event.id, this.state.event.title, folderId);
笔记也是按事件目标关联:
const t: FavoriteType = 'event';
let note = this.noteStore.getByTarget(t, this.state.event.id);
if (!note) {
note = this.noteStore.newDraft(t, this.state.event.id, this.state.event.title);
this.noteStore.upsert(note);
}
Navigator.push(AppRoutes.NOTE_DETAIL, { noteId: note.id });
这说明收藏和笔记不是诗文专用能力,而是围绕 FavoriteType 抽象出来的学习状态。诗文、事件、朝代、地点都可以成为收藏和笔记目标。
StatsStore:历史事件阅读也进入学习统计
StatsStore.recordEvent() 和 recordPoem() 是平行设计:
recordEvent(eventId: string): void {
if (!eventId || eventId.length === 0) return;
const t: DailyStat = this.ensureToday();
if (t.eventIds.indexOf(eventId) < 0) {
t.eventIds.push(eventId);
this.notifyChanged();
}
}
这让统计页能够区分:
| 行为 | 记录字段 |
|---|---|
| 阅读诗文 | `DailyStat.poemIds` |
| 阅读事件 | `DailyStat.eventIds` |
| 完成练习 | `practiceTotal/practiceRight/practiceWrong` |
| 活跃时长 | `durationSec` |
对学习 App 来说,时间轴和事件详情不是“内容浏览”,而是学习行为。只有把事件阅读记录下来,后续能力图谱、今日目标、勋章和学习趋势才有依据。
时间轴与首页的关系
第三篇讲首页时提到“一日一史”。它展示的是当天的一个史事,并通过:
Navigator.push(AppRoutes.TIMELINE_EVENT_DETAIL, { eventId });
进入事件详情。第六篇的时间轴则提供另一种路径:
时间轴 Tab → 朝代筛选 → 朝代段 → 朝代概览 / 兴替明鉴 → 事件详情
这两条路径面向不同场景:
| 路径 | 用户意图 |
|---|---|
| 首页一日一史 | 今天先了解一个事件 |
| 时间轴 | 从历史顺序理解朝代和事件 |
| 兴替明鉴 | 分析一个朝代为什么兴衰 |
| 事件详情 | 深入阅读某个节点 |
这就是《观止·诗史汇》的产品结构:不是每个模块各做各的,而是围绕学习路径互相导流。
当前实现的边界和后续优化
从源码看,时间轴模块已经完成主骨架,但也能看到几个清晰的后续方向:
| 当前边界 | 后续优化 |
|---|---|
| 时间轴只展示朝代段 | 可以在朝代段内展示 1 到 2 个关键事件预览 |
| 事件详情的相关诗文只传 `poemId` | 需要补齐 `poemShard` 或提供仓储级 ID 查询兜底 |
| `EXTRA_EVENTS` 在 ArkTS 中维护 | 后续可构建成 rawfile 历史事件包 |
| 事件影响文案部分仍较简化 | 可以从历史内容包中生成更完整的影响字段 |
| 时间轴筛选只支持朝代 | 后续可按政治、军事、文化、科技等事件类别筛选 |
这些不是缺陷,而是实战项目演进中的正常阶段。先把时间顺序和事件详情跑通,再逐步把事件包、地理关联、诗文关联和练习题生成做深。
本地验收命令
先检查历史内容和页面源码:
Get-Content -LiteralPath .\features\src\main\ets\timeline\TimelinePage.ets -Encoding UTF8
Get-Content -LiteralPath .\features\src\main\ets\timeline\TimelineEventDetailPage.ets -Encoding UTF8
Get-Content -LiteralPath .\features\src\main\ets\services\HistoryPack.ets -Encoding UTF8
Get-Content -LiteralPath .\features\src\main\ets\services\HistoryEvents.ets -Encoding UTF8
再检查模拟器和应用截图:
& "D:\Program Files\HuaWei\DevEco Studio\sdk\default\openharmony\toolchains\hdc.exe" list targets
& "D:\Program Files\HuaWei\DevEco Studio\sdk\default\openharmony\toolchains\hdc.exe" shell aa start -a EntryAbility -b com.example.app_project02
& "D:\Program Files\HuaWei\DevEco Studio\sdk\default\openharmony\toolchains\hdc.exe" shell snapshot_display -i 0 -f /data/local/tmp/timeline.png -w 1080 -h 2400 -t png
人工路径建议:
底部 Tab:时间轴 → 朝代筛选 → 兴衰入口 → 朝代详情 / 兴替明鉴
首页 → 一日一史 → 历史事件详情
页面验收清单
| 检查项 | 期望 |
|---|---|
| 时间轴入口 | 底部 Tab 能进入时间轴页 |
| 顶部标题 | 显示“时间轴 / 朝代兴替·古今事变” |
| 朝代筛选 | “全部”和各朝代 chip 横向滚动 |
| 筛选状态 | 选中某朝代后只展示该朝代段 |
| 兴衰入口 | 根据当前筛选展示当前朝代文案 |
| 兴衰跳转 | 携带 `dynastyId` 跳转 `DYNASTY_INSIGHT` |
| 年份节点 | 公元前年份显示“前 xxx” |
| 朝代段 | 展示 `fullName`、`brief` 和“详情 →” |
| 朝代详情 | 点击朝代段携带 `dynastyId` 跳转 |
| 事件详情 | 通过 `eventId` 加载事件、朝代、地点和相关诗文 |
| 节点标签 | `found/prosper/turn/crisis/fall` 显示为建立、兴盛、转折、危机、衰亡 |
| 事件统计 | 打开事件详情后当天 `eventIds` 去重记录 |
| 收藏笔记 | 事件可以收藏到文件夹,也可以打开/创建事件笔记 |
常见问题复盘
1. 为什么时间轴不直接显示所有事件?
因为时间轴的第一层任务是建立历史顺序。如果一上来塞满事件,用户会被信息量压住。当前实现先展示朝代段,再通过朝代详情、兴替明鉴和事件详情逐层展开,更适合移动端。
2. 为什么有“兴衰”入口,而不是只做朝代详情?
朝代详情回答“这个朝代是什么”,兴替明鉴回答“这个朝代为什么兴盛、为什么转折、为什么衰亡”。两者不是一类信息。时间轴保留兴衰入口,可以让用户从顺序进入分析。
3. 为什么 HistoryPack 还要合并到 MOCK_DYNASTIES?
这是项目演进阶段的兼容策略。旧服务仍然依赖 mock 数据,新的历史包通过注册函数合并进去,可以在不大改调用方的情况下让页面消费更完整的数据。
4. 为什么事件需要 nodeType?
如果事件只有标题和年份,它只能被阅读;有了 nodeType,事件就可以参与兴衰分析、练习题生成、统计分类和视觉标识。它让历史数据更结构化。
5. 为什么事件详情也要收藏和笔记?
诗文和事件都是学习对象。用户可能收藏一首诗,也可能收藏“靖康之耻”“商鞅变法”这类历史节点。统一用 FavoriteType 和 targetId 关联,后续收藏页、笔记页和统计页才能共用能力。
本章小结
第六篇的核心不是“画一条时间线”,而是把历史学习路径组织起来。
HistoryPack 负责把历史内容注册进运行时数据,TimelinePage 用朝代筛选、兴衰入口、年份节点和朝代段建立历史顺序,TimelineEventDetailPage 通过 eventId 展示事件背景、经过、影响、地点和相关诗文,StatsStore 把事件阅读沉淀为学习记录,FavoriteStore 和 NoteStore 让历史事件也能被长期整理。
这样,时间轴就不是一个孤立页面,而是从首页“一日一史”延展出来的历史主线。下一篇会继续深入“兴替明鉴”,拆解一个朝代如何被拆成四维总览和六类分析,让时间轴里的节点真正变成可理解、可复盘的历史洞察。
[#HarmonyOS](https://so.csdn.net/so/search/s.do?q=HarmonyOS&t=all&o=vip&s=&l=&f=&viparticle=&from_tracking_code=tag_word&from_code=app_blog_art) [#ArkTS](https://so.csdn.net/so/search/s.do?q=ArkTS&t=all&o=vip&s=&l=&f=&viparticle=&from_tracking_code=tag_word&from_code=app_blog_art) [#ArkUI](https://so.csdn.net/so/search/s.do?q=ArkUI&t=all&o=vip&s=&l=&f=&viparticle=&from_tracking_code=tag_word&from_code=app_blog_art) [#DevEco Studio](https://so.csdn.net/so/search/s.do?q=DevEco+Studio&t=all&o=vip&s=&l=&f=&viparticle=&from_code=app_blog_art) [#鸿蒙开发](https://so.csdn.net/so/search/s.do?q=%E9%B8%BF%E8%92%99%E5%BC%80%E5%8F%91&t=all&o=vip&s=&l=&f=&viparticle=&from_tracking_code=tag_word&from_code=app_blog_art)
更多推荐



所有评论(0)