
【观止·诗史汇 HarmonyOS 实战系列 07】兴替明鉴四维总览与六类分析的朝代洞察模型前六篇已经把《观止·诗史汇》的两条主线铺出来了前四篇解决工程骨架、首页和诗文内容包第五篇把一首诗的详情页拆成正文、译注、简析、作者、朗读、收藏、笔记和统计第六篇把历史事件放进时间轴让用户能顺着朝代顺序理解关键节点。第七篇继续往“史”的一侧推进。时间轴解决的是“先后顺序”但只知道一个朝代从哪一年到哪一年还不够。学习者真正需要的是这个朝代为什么兴起为什么衰落政治、经济、文化、科技和对外关系分别扮演什么角色关键事件又如何把这些判断落到具体节点上。这就是兴替明鉴模块要解决的问题。它不是朝代表的详情页也不是一段朝代介绍文案而是一个朝代洞察页顶部可以切换朝代中间有基础信息和跨模块入口主体用六类详细分析解释兴衰再用关键节点把抽象分析接回事件详情。上图来自本机 DevEco 模拟器中打开《观止·诗史汇》的“兴替明鉴”模块。第七篇的截图重点不是首页入口而是DynastyInsightPage本体朝代选择、基础信息、古今地理/朝代详情跳转、六类详细分析和后续关键节点。本篇要解决什么问题兴替明鉴至少要回答六个工程问题问题当前实现朝代从哪里来DynastyService.list() 读取朝代时间线默认进入哪个朝代Navigator.getParams() 读取 dynastyId否则回落到 d_ancient深度分析从哪里来DynastyAnalysisService 读取 HistoryAnalysis.EXTRA_DETAILS六类分析如何固定结构DynastyDetail 拆成 gaikuo / zhengzhi / jingji / wenhua / keji / duiwai关键节点如何生成合并 HistoryEvent.nodeType、turningPoints 和 keyEvents如何连接其他模块跳转 GEO_DETAIL、DYNASTY_OVERVIEW、TIMELINE_EVENT_DETAIL这几个点决定了兴替明鉴的定位它不是孤立页面而是时间轴、朝代详情、古今地理、事件详情之间的解释层。源码对象总览文件职责features/src/main/ets/dynasty/DynastyInsightPage.ets兴替明鉴页负责朝代切换、基础卡、六类分析、关键节点和跨模块跳转features/src/main/ets/dynasty/DynastyOverviewPage.ets朝代详情页展示时期概述、重大事件、人物、发明、转折点和收藏笔记features/src/main/ets/services/DynastyAnalysisService.ets朝代分析服务读取四维总览和六类详细分析features/src/main/ets/services/HistoryAnalysis.ets朝代兴衰内容包维护 EXTRA_DETAILSfeatures/src/main/ets/services/HistoryEvents.ets历史事件扩展数据为关键节点提供 nodeType、摘要和详情commons/src/main/ets/router/RouteNames.ets路由常量DYNASTY_INSIGHT、DYNASTY_OVERVIEW、GEO_DETAIL、TIMELINE_EVENT_DETAIL这篇文章会重点拆DynastyInsightPage和DynastyAnalysisService再补充它与DynastyOverviewPage、事件详情和地理详情的关系。页面不是从文案开始而是从状态模型开始DynastyInsightPage的状态定义比较克制interface InsightState { loading: boolean; dynasties: Dynasty[]; current: Dynasty | null; overview: DynastyOverview | null; detail: DynastyDetail | null; modules: DynModule[]; nodes: KeyNodeRow[]; }这组状态可以分成三层状态说明dynasties、current当前朝代上下文overview、detail、modules朝代分析内容nodes可点击的历史关键节点这种拆法有两个好处。第一页面不会把全部内容都塞进一个巨大对象。朝代本体仍然来自DynastyService深度分析来自DynastyAnalysisService事件节点来自EventService。第二页面可以清晰处理内容缺口。比如overview当前为空页面可以隐藏“四维总览”detail不存在六类分析就显示空态事件没有摘要也不影响朝代基础信息展示。默认朝代与路由参数兴替明鉴既可以从首页直接打开也可以从时间轴、文脉、地理等模块带着dynastyId打开。因此它不能假设入口唯一。async aboutToAppear() { const dyn: Dynasty[] await this.dynastySvc.list(); const params: NavigateParams Navigator.getParams(); let target: string params.dynastyId ?? DEFAULT_DYNASTY; if (!dyn.some((d: Dynasty) d.id target)) { target dyn.length 0 ? dyn[0].id : DEFAULT_DYNASTY; } this.state { loading: true, dynasties: dyn, current: null, overview: null, detail: null, modules: [], nodes: [] }; await this.loadDynasty(target); }这里有一个小但重要的防御如果传入的dynastyId不存在页面会回退到列表中的第一个朝代而不是直接空白或崩溃。对一个内容型 App 来说路由参数不可靠是常态。用户可能从收藏回跳可能从旧版本笔记进入也可能从未来新增的文脉入口进入。页面先校验参数再加载内容是比“入口保证正确”更稳的做法。loadDynasty一次装配四类信息loadDynasty(id)是这个页面最核心的方法。它不是只加载一个朝代而是把四类信息装成页面可渲染状态当前朝代cur四维总览ov六类详细分析dt关键节点nodesasync loadDynasty(id: string) { const cur: Dynasty | null await this.dynastySvc.getById(id); const ov: DynastyOverview | null await this.analysisSvc.getOverview(id); const dt: DynastyDetail | null await this.analysisSvc.getDetail(id); const allEvents: HistoryEvent[] await this.eventSvc.list(); const evs: HistoryEvent[] allEvents .filter((e: HistoryEvent) e.dynastyId id e.nodeType e.nodeType.length 0) .sort((a: HistoryEvent, b: HistoryEvent) a.year - b.year); }这一段有两个选择很值得注意。第一关键节点不取所有事件而是只取nodeType存在的事件。也就是说不是每条史事都适合出现在兴衰节点里。nodeType把事件分成建立、兴盛、转折、危机、衰亡等类型让页面可以从“事件列表”升级为“兴衰结构”。第二事件按year排序。兴替分析不是按录入顺序展示而是回到历史时间顺序。这和第六篇时间轴的设计是同一条原则历史内容的第一秩序是时间。六类详细分析固定字段比自由标题更稳定当前DynastyDetail不是一个自由数组而是固定六个字段const modules: DynModule[] dt ? [ { key: ${id}_gaikuo, title: 兴衰关键, body: dt.gaikuo }, { key: ${id}_zhengzhi, title: 政治, body: dt.zhengzhi }, { key: ${id}_jingji, title: 经济, body: dt.jingji }, { key: ${id}_wenhua, title: 文化, body: dt.wenhua }, { key: ${id}_keji, title: 科技, body: dt.keji }, { key: ${id}_duiwai, title: 对外交流, body: dt.duiwai } ] : [];这比“后端给一个标题数组页面照着渲染”更朴素但更适合当前项目。原因是本项目仍处于本地内容包阶段内容结构需要稳定。六类字段固定后写文章、写数据、写 UI、做验收都能对齐同一套口径字段页面标题解决的问题gaikuo兴衰关键一句话或一段话解释核心兴衰线索zhengzhi政治权力结构、制度设计、治理能力jingji经济农业、商业、财政、交通、资源wenhua文化思想、文学、教育、艺术与价值秩序keji科技技术、工程、历法、医学、制造duiwai对外交流边疆、战争、贸易、外交和世界联系对读者来说这种结构也更容易横向比较。看完秦、汉、唐、宋、明、清之后用户自然会意识到每个朝代都可以从同一组维度观察。空内容过滤不要为了完整而展示空壳HistoryAnalysis.ets里有些朝代的gaikuo可能为空。页面没有强行展示空卡片而是做了过滤const nonEmptyModules: DynModule[] modules.filter((m: DynModule) m.body m.body.length 0);这个细节很像内容产品里的“温柔退让”数据还没有补齐时页面不要把空白暴露给用户。模块是否出现由内容决定而不是由模板强行撑出来。这也解释了为什么当前截图里直接从“六类详细分析”的政治、经济等模块开始。页面不是漏了内容而是尊重了当前内容包状态。DynastyAnalysisService服务层负责 ID 兼容朝代分析服务很短但它承担了一个重要职责把历史遗留 ID 差异收束在服务层。export class DynastyAnalysisService { getOverview(dynastyId: string): PromiseDynastyOverview | null { const o: DynastyOverview | undefined EXTRA_OVERVIEWS.find((it: DynastyOverview) it.dynastyId dynastyId); return Promise.resolve(o ?? null); } getDetail(dynastyId: string): PromiseDynastyDetail | null { const normalizedId: string normalizeDynastyAnalysisId(dynastyId); const d: DynastyDetail | undefined EXTRA_DETAILS.find((it: DynastyDetail) it.dynastyId normalizedId); return Promise.resolve(d ?? null); } }兼容函数如下function normalizeDynastyAnalysisId(dynastyId: string): string { if (dynastyId d_w_han) { return d_west_han; } if (dynastyId d_three) { return d_sanguo; } return dynastyId; }为什么不在页面里写兼容因为页面不应该知道d_w_han和d_west_han是历史包 ID 差异也不应该到处出现if dynastyId ...。页面只关心“我要某个朝代的分析”服务层负责把不同数据源的 ID 归一。这是内容工程里非常常见的小问题一开始几个文档、几个 mock、几个页面各自命名后来统一时如果没有服务层兜住就会出现页面分支蔓延。越早把归一逻辑放到 service后面越省心。四维总览当前为什么为空HistoryAnalysis.ets顶部有一段说明/** * - EXTRA_OVERVIEWS四维总览文治/商业/科技/积弱。当前按产品需求留空 * 页面在无数据时会自动隐藏 四维总览 区块。 * - EXTRA_DETAILS六模块详细分析但按最新产品口径将首模块 gaikuo * 改为承载 兴衰关键 叙事段其余五项对应政治 / 经济 / 文化 / 科技 / 对外。 */ export const EXTRA_OVERVIEWS: DynastyOverview[] [];这说明当前产品口径发生过一次调整四维总览的模型还在但数据暂时留空重点先落在六类详细分析上。这不是坏事。工程上保留overview的状态和 UI 分支意味着未来要恢复四维卡片时不需要重写页面结构。只要补齐EXTRA_OVERVIEWS页面就会自动出现“四维总览”。关键节点从事件、转折点和 keyEvents 合成兴替明鉴的关键节点不是单一来源。页面会先读取事件列表中已经带nodeType的事件const nodes: KeyNodeRow[] evs.map((e: HistoryEvent) { const r: KeyNodeRow { id: e.id, eventId: e.id, year: e.year, title: e.title, nodeType: e.nodeType ?? , summary: this.eventContent(e), hasContent: this.hasEventContent(e) }; return r; });接着又把Dynasty.turningPoints并入if (cur cur.turningPoints) { for (let i 0; i cur.turningPoints.length; i) { const tp: DynastyTurningPoint cur.turningPoints[i]; const related: HistoryEvent | null this.findRelatedEvent(tp.title, evs); const r: KeyNodeRow { id: ${id}_turning_${i}, eventId: related ? related.id : , year: related ? related.year : cur.startYear, title: tp.title, nodeType: related related.nodeType ? related.nodeType : this.turningPointType(i, cur.turningPoints.length), summary: related ? this.eventContent(related) : , hasContent: related ? this.hasEventContent(related) : false }; nodes.push(r); } }最后还会把keyEvents兜底合进去。这种做法的价值在于兴替明鉴可以尽量利用已有内容包。事件详情足够完整时节点点击后可以进入TimelineEventDetailPage事件详情还没补齐时转折点仍然可以在页面上作为简要节点出现。标题匹配用 normalizeEventTitle 做轻量关联turningPoints和HistoryEvent之间没有强绑定 ID页面通过标题做近似匹配private findRelatedEvent(title: string, events: HistoryEvent[]): HistoryEvent | null { const key: string this.normalizeEventTitle(title); for (let i 0; i events.length; i) { const eventKey: string this.normalizeEventTitle(events[i].title); if (key eventKey || key.indexOf(eventKey) 0 || eventKey.indexOf(key) 0) { return events[i]; } } return null; }归一函数会去掉括号、数字、公元年、标点和一些常见差异private normalizeEventTitle(title: string): string { return title .replace(/[^]*/g, ) .replace(/\([^)]*\)/g, ) .replace(/[0-9]/g, ) .replace(/[前后约公元年\s·、。]/g, ) .replace(/之变/g, ) .replace(/之耻/g, ) .replace(/首下/g, 下) .replace(/开创科举制/g, 科举) .replace(/开创科举/g, 科举) .replace(/开科举设进士/g, 科举) .replace(/修建大运河/g, 大运河) .replace(/开凿大运河/g, 大运河); }这不是一个完美的语义匹配算法但在本地内容包阶段足够实用。它解决的是“同一件事在不同文档里写法略不同”的现实问题。后续如果内容包继续扩大更稳的方案是给turningPoints增加eventId字段让内容编辑阶段就建立显式关联。当前的标题归一可以作为过渡层。节点类型让历史事件带上结构意义nodeType的渲染逻辑很直接private nodeLabel(t: DynastyNodeType): string { if (t found) return 建立; if (t prosper) return 兴盛; if (t turn) return 转折; if (t crisis) return 危机; if (t fall) return 衰亡; return ; }配套颜色也按语义分组private nodeColor(t: DynastyNodeType): ResourceColor { if (t found) return AppColors.accent; if (t prosper) return AppColors.success; if (t turn) return AppColors.warning; if (t crisis) return AppColors.warning; if (t fall) return AppColors.danger; return AppColors.textTertiary; }这让关键节点不只是列表。用户能一眼看出这件事是在“建立”“兴盛”“转折”“危机”还是“衰亡”阶段。对于历史学习来说这种类型提示很有价值因为它把事件放回王朝生命线里。与古今地理的关系兴替明鉴里有一个“古今地理”入口Text(${this.state.current!.name}·古今地理) .onClick(() { const p: NavigateParams { dynastyId: this.state.current!.id }; Navigator.push(AppRoutes.GEO_DETAIL, p); })这个入口带的是dynastyId不是geoId。也就是说它打开的不是某一个地名详情而是古今地理列表并按当前朝代筛选。这和第八篇要讲的GeoPage正好衔接地理模块既可以作为一级入口展示全部地名也可以从某个朝代进入只展示该朝代相关的古今地理。与朝代详情的关系兴替明鉴也能跳到朝代详情Text(${this.state.current!.name}·朝代详情) .onClick(() { const p: NavigateParams { dynastyId: this.state.current!.id }; Navigator.push(AppRoutes.DYNASTY_OVERVIEW, p); })DynastyOverviewPage更像资料页时期概述、重大事件、著名人物、发明创造、历史转折点以及收藏和笔记。兴替明鉴则更像分析页围绕兴衰逻辑展开六类解释并用节点把分析接回事件。两者分工可以这样理解页面角色DynastyOverviewPage一个朝代有什么概述、事件、人物、发明、转折点DynastyInsightPage一个朝代为什么这样兴衰政治、经济、文化、科技、对外和关键节点这也是系列文章里要特别强调的工程设计不要把所有朝代信息都堆在一个页面里。资料页和分析页分开用户路径会更清楚。当前实现的边界这个模块已经完成了兴替分析的核心链路但还有几个边界值得记录。第一EXTRA_OVERVIEWS当前为空。页面保留了四维总览能力但数据还没有填充。后续如果恢复“文治昌明、商业繁荣、科技发达、积赏积弱”的四维卡片需要优先补内容包而不是改页面。第二关键节点存在重复风险。因为HistoryEvent、turningPoints、keyEvents会合并到同一个nodes数组如果标题匹配没有命中就可能出现语义相近的节点重复。后续可以用规范化标题做去重。第三事件标题匹配是启发式的。它能处理“科举”“大运河”等常见差异但不能替代显式 ID。长期看内容包应该把turningPoints与eventId绑定起来。第四六类分析当前是纯文本。未来如果要做高亮、引用、图表、参考出处或折叠展开DynastyDetail可能需要从字符串升级为结构化段落数组。本地验收命令本篇使用真实模拟器截图不使用封面加工图作为正文图。基本验收路径如下git status --short 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/guanzhi_07_dynasty_insight.png -w 1080 -h 2400 -t png D:\Program Files\HuaWei\DevEco Studio\sdk\default\openharmony\toolchains\hdc.exe file recv /data/local/tmp/guanzhi_07_dynasty_insight.png .\screenshots\07_dynasty_insight_emulator.png页面人工验收重点首页点击“兴替明鉴”可以进入DynastyInsightPage。顶部朝代 chip 可以横向切换。当前朝代基础信息显示正确。“古今地理”“朝代详情”能带dynastyId跳转。六类详细分析不展示空模块。关键节点点击后能进入事件详情。常见问题复盘1. 为什么不把兴替明鉴做成时间轴的一部分时间轴负责“顺序”兴替明鉴负责“解释”。如果把六类分析塞进时间轴时间轴会变成很长的知识百科失去快速浏览朝代顺序的能力。两个页面分开后用户可以先看顺序再进入某个朝代深读。2. 为什么六类分析不用动态标题因为当前阶段更需要统一口径。固定的政治、经济、文化、科技、对外和兴衰关键方便横向比较也方便内容包补齐。等内容更丰富后再考虑让部分朝代出现特有模块。3. 为什么 ID 归一要放在服务层页面不应该知道数据历史包的命名差异。d_w_han和d_west_han是内容层的兼容问题放在DynastyAnalysisService更稳。4. 为什么关键节点要有 nodeType因为历史事件本身不等于兴衰节点。nodeType让事件具备结构意义建立、兴盛、转折、危机、衰亡。用户看到的就不是散点而是一条朝代生命线。5. 为什么现在四维总览为空也保留代码这是一个可扩展位。当前产品口径先落六类详细分析四维总览暂不展示但保留overview状态和 UI 分支后续补内容时成本很低。本章小结第七篇的核心是兴替明鉴不是“朝代介绍页”而是把朝代数据、深度分析、历史节点、地理入口和朝代详情页串起来的解释层。从工程角度看这个模块最值得借鉴的地方有三点页面状态按朝代上下文、分析内容、关键节点拆开。服务层吸收历史 ID 差异页面只面向稳定接口。六类分析和节点类型让历史内容具备可比较、可跳转、可扩展的结构。下一篇会顺着兴替明鉴里的“古今地理”入口继续拆解一个地名如何同时关联朝代、事件和诗文为什么GeoPlace应该先稳定关系模型再考虑地图展示。