山海万灵 HarmonyOS 文化知识实战(04):探索证据板的页面状态组织

当图鉴从“看一张卡片”升级到“沿线索探索”时,页面不能只放一段说明文字。读者需要知道内容来自哪里、哪些事实已定位、哪些属于本馆导览,以及当前神兽和哪些节点相连。山海万灵把这些信息组织为证据板:详情页与数字馆长都读取同一个神兽、来源与关系上下文。

证据板围绕当前目标组装

页面先确定selectedBeastId,再解析神兽、来源、知识卡和关联关系。来源信息不是页面临时字符串,而是数据模型中的SourceInfo;关系也不是图片上的装饰线,而是带类型、目标和说明的记录。

interface EvidenceBoardState { beast: BeastItem source: SourceInfo learningCards: BeastLearningCard[] relations: CuratorRelationItem[] resultState: CuratorResultState } type CuratorResultState = 'IDLE' | 'LOADING' | 'AI' | 'FALLBACK' | 'FAILED'

同一个目标在详情页里显示出处与知识卡,在馆长页里显示当前讲解和下一步入口。两处都不自行拼接历史事实,从而避免一页写“东海路线”、另一页把它当作原典地理。

来源、核验与导览分层呈现

应龙详情页把原典条目、数字文本核验、异文说明和人工校注状态放进来源面板;馆长模块在此基础上生成导览文字。前者回答“内容从哪里来”,后者回答“这条线索怎样继续看”。

private sourceLabel(source: SourceInfo): string { return source.name + ' · ' + source.verificationLabel } private relationText(item: CuratorRelationItem): string { return item.label + ':' + item.detail } private canRenderGuide(state: CuratorResultState): boolean { return state === 'AI' || state === 'FALLBACK' }
面板输入输出给用户的内容
出处SourceInfo条目、定位、数字核验与复核状态
知识与故事学习卡集合已登记的最小事实和导览说明
图谱关系关系集合与当前神兽相关的区域、展厅或叙事线索
馆长结果CuratorResultState加载、讲解、降级或失败反馈

选择主题后再请求结果

馆长页让用户选择神兽、故事、地图或展厅主题,再发出请求。请求期间按钮进入同步状态,结果回到同一个状态对象;失败时保留当前神兽和出处板,不把已读内容清空。

async function requestGuide(topic: CuratorGuideTopic): Promise<void> { this.resultState = 'LOADING' try { this.selectedGuide = await this.viewModel.explainBeast(this.selectedBeastId, topic) this.resultState = this.selectedGuide.fallback ? 'FALLBACK' : 'AI' } catch (_) { this.resultState = 'FAILED' } }

用单一状态源约束卡片的更新顺序

页面的关键不是把信息卡片排在同一屏,而是让每次主题切换都遵循同一条状态链:先锁定目标神兽,再切换结果状态,最后提交导览、出处与关系集合。这样,卡片拿到的是同一轮请求的结果;旧请求晚到时不会覆盖新目标。对于“应龙”这样的多关系对象,读者也不会看到已经切到其他神兽、但关系区仍残留应龙路线的错位内容。

private beginGuideRequest(beastId: string, topic: CuratorGuideTopic): number { this.curatorRequestSequence += 1 this.curatorTargetBeastId = beastId this.curatorResultState = 'LOADING' this.recommendations = [] this.learningCards = [] return this.curatorRequestSequence } private shouldApplyGuide(sequence: number, beastId: string): boolean { return sequence === this.curatorRequestSequence && beastId === this.curatorTargetBeastId } private restoreGuideActions(): void { this.curatorResultState = 'IDLE' this.notice = '可重新选择主题后继续导览。' }

这组字段把“谁是当前对象”“本轮是否仍有效”“结果处于哪个阶段”拆开保存。curatorRequestSequence只负责淘汰过期请求,curatorTargetBeastId决定页面展示对象,curatorResultState决定结果卡的视觉分支。三者不相互代替,状态切换才不会把网络时序错误伪装成内容错误。

事件必须更新的状态页面可见结果不能发生的副作用
选择新的神兽目标 ID、请求序号基础档案与出处先对齐新对象继续显示旧对象的关系列表
提交主题结果状态为LOADING结果卡显示进行中,证据板仍可阅读清空已确认的出处字段
返回导览结果导览、学习卡、关系集合同一对象的讲解和关系同时更新让过期请求覆盖新选择
返回降级结果结果状态为FALLBACK标明本地可用的导览内容把降级内容标为 AI 返回
请求失败结果状态为FAILED给出可恢复提示与再次选择入口以空白卡片隐藏失败原因

结果卡与关系区的组合边界

结果卡负责说明“本轮导览给了什么”,关系区负责说明“这些内容怎样连接到区域、展厅和叙事线索”。两个组件通过同一份馆长页面数据组合,而不是在各自内部再次查询。这样在窄屏时可以纵向排布,在宽屏时可以并列显示,数据边界不会随布局变化而漂移。

ShanhaiCuratorResultCard({ resultState: this.resultState, guide: this.selectedGuide, selectedBeast: this.selectedBeast, onRetry: () => this.requestGuide(this.selectedTopic) }) ShanhaiCuratorSourceRelations({ source: this.selectedBeast.sourceInfo, learningCards: this.learningCards, relations: this.recommendations, onOpenRelation: (target) => this.openRouteTarget(target) })

组合时应保持三个边界。第一,结果卡不拼接出处文本,它只消费已准备好的导览结果和状态。第二,关系区不决定请求成功与否,它只展示当前对象可用的来源、学习卡和连接线索。第三,页面容器集中处理重试、切换对象和进入下一站,子组件通过回调表达用户动作。这样的拆分让状态分支集中在页面层,组件可以围绕各自的阅读任务演进。

从回读结果验收证据板

验收不以“看见一个按钮”作为结论,而是检查一次完整动作之后的内容是否互相一致。选择应龙主题并进入馆长模块后,导览段落、出处条目、核验状态和“与应龙相关”的关系区应同时归属于应龙;切换到另一个主题后,旧关系和旧讲解不能继续停留在新对象的结果区。加载、降级和失败分支则分别保留可读的基础档案,避免读者在等待或恢复时失去已确认的来源线索。

function assertEvidenceBoard(result: EvidenceBoardState): boolean { const sameBeast = result.beast.id === result.source.beastId const hasGuide = result.resultState === 'AI' || result.resultState === 'FALLBACK' || result.resultState === 'LOADING' const relationTargetsValid = result.relations .every((item) => item.fromBeastId === result.beast.id) const fallbackReadable = result.resultState !== 'FALLBACK' || result.guide.content.length > 0 return sameBeast && hasGuide && relationTargetsValid && fallbackReadable } function canOpenNextStop(target: CuratorRouteTarget): boolean { return target.kind === 'REGION' || target.kind === 'HALL' || target.kind === 'BEAST' }

运行时选择“神兽”并打开模块后,界面会展示应龙的导览段落、出处条目和“与应龙相关”的关系区。该结构把选择、来源与结果放入明确状态,而不是让多个卡片各自保存一份文本。更多 ArkUI 状态模型可参考 HarmonyOS 官方说明。

状态切换的失败出口

讲解请求进入加载状态后,证据板仍保留当前神兽与出处;结果可用时更新导览卡,失败时显示可恢复反馈。页面不以空白替代失败,也不把上一次神兽的关系结果混入新的选择。