山海万灵 HarmonyOS 文化知识实战(03):神兽图鉴列表的数据模型
图鉴页同时承担浏览、筛选和进入阅读详情三种任务。若列表卡片只保存名称和配图,而详情页另存一套说明、出处与关系数据,筛选后很容易出现卡片与详情不一致的问题。山海万灵把 BeastItem 定为图鉴的稳定读模型:页面只传递 id,展示字段、来源信息和关联节点都从同一条记录解析。

一条记录覆盖卡片、详情与路线
BeastItem 不把“神兽”简化为标题和一段简介。regionId、hallId 为图鉴与场馆路线提供连接点,source 承担出处和阅读信息,relations 为后续关系推荐保留稳定的节点集合。卡片、详情、场馆和策展入口读取的是同一个对象,因此不会在不同页面复制文案。
export interface BeastItem {
id: string;
no: string;
name: string;
alias: string;
regionId: string;
hallId: string;
summary: string;
detail: string;
source: SourceInfo;
curatorRoute?: string;
relations: string[];
}
| 字段组 | 图鉴页的职责 | 详情页与后续模块的职责 |
|---|---|---|
id、no、name、alias |
标识卡片并支持关键字匹配 | 用 id 解析当前对象,保留统一身份 |
regionId、hallId |
显示出没地并参与地域排序 | 跳转展厅、组织导览路线 |
summary、detail |
卡片只读取摘要 | 详情页展开完整说明 |
source |
提供简短出处提示 | 展开章节、定位链接和阅读摘录 |
relations |
不在卡片上堆叠关系 | 交给关联推荐与图谱面板 |
数据模型的边界也很明确:列表不保存一份可编辑的详情副本,页面状态也不把筛选结果回写到原始目录。只要目录本身不被筛选过程改写,切换分类、搜索或排序后仍然可以回到完整图鉴。
可见列表是从原始目录计算出来的视图
图鉴的筛选状态被收敛到 BeastArchiveQuery。分类、搜索词、排序方式和发现筛选都是输入;getVisibleBeasts 从 beasts.slice() 开始计算,不改变 Repository 提供的原始数组。这样搜索框输入“应龙”后再清空,原始条目仍然完整可用。
export interface BeastArchiveQuery {
category: string;
searchText: string;
sortMode: string;
filterMode: string;
}
getVisibleBeasts(
beasts: BeastItem[],
regions: RegionItem[],
discoveredIds: string[],
query: BeastArchiveQuery
): BeastItem[] {
const keyword: string = query.searchText.trim();
let result: BeastItem[] = beasts.slice();
if (query.category !== BEAST_ARCHIVE_DEFAULT_CATEGORY) {
result = result.filter((beast: BeastItem): boolean => this.matchesCategory(beast, query.category));
}
if (keyword.length > 0) {
result = result.filter((beast: BeastItem): boolean => this.matchesKeyword(beast, regions, keyword));
}
return result.filter((beast: BeastItem): boolean =>
this.matchesFilter(beast, regions, discoveredIds, query.filterMode));
}
| 操作 | 参与计算的输入 | 不应发生的副作用 |
|---|---|---|
| 切换分类 | category 与神兽类别 |
修改原始 beasts 数组 |
| 输入搜索词 | 名称、别名、摘要与地域名 | 覆盖详情中的 detail 或 source |
| 选择“已发现” | discoveredIds |
改写探索进度 |
| 切换地域排序 | regionId 与 Region 名称 |
改变基础展示顺序 |
关键词匹配覆盖名称、别名、摘要和地域名。读者记得“东海”但记不清神兽全名时,仍可通过地域进入对应卡片;这种匹配只影响当前视图,不制造新的数据副本。
private matchesKeyword(beast: BeastItem, regions: RegionItem[], keyword: string): boolean {
return beast.name.indexOf(keyword) >= 0 ||
beast.alias.indexOf(keyword) >= 0 ||
beast.summary.indexOf(keyword) >= 0 ||
this.regionName(beast.regionId, regions).indexOf(keyword) >= 0;
}
发现状态作为独立输入参与筛选与排序
发现记录使用 discoveredIds 传入 ViewModel,而不是写入 BeastItem。模型仍描述神兽事实,探索进度属于用户状态;两者分开后,同一份内容目录能适配新访客和已完成探索的访客。页面要显示“已发现”或“待发现”时,通过 isVisuallyDiscovered 读取状态即可。
isVisuallyDiscovered(beastId: string, discoveredIds: string[]): boolean {
return discoveredIds.indexOf(beastId) >= 0;
}
private discoveryOrder(beast: BeastItem, discoveredIds: string[]): number {
const discoveredIndex: number = discoveredIds.indexOf(beast.id);
return discoveredIndex >= 0 ? discoveredIndex : 1000 + this.baseOrder(beast);
}
排序同样保留明确的兜底规则。按名称或地域排序时,baseOrder 用作并列时的稳定顺序;选择“未发现优先”时,尚未发现的条目先出现,再使用基础顺序保证列表不抖动。页面每次重新计算都得到可预期的卡片顺序。
| 排序方式 | 第一比较条件 | 并列时的处理 |
|---|---|---|
| 发现顺序 | discoveredIds 中的位置 |
未发现条目按基础顺序排在后面 |
| 名称首字母 | name.localeCompare |
使用现有稳定顺序 |
| 出没地排序 | Region 名称 | 回退到基础顺序 |
| 未发现优先 | 是否存在于 discoveredIds |
同组内保留基础顺序 |
详情入口只传递稳定 ID
用户点击卡片时,页面保存 selectedBeastId,随后从当前目录解析对象并同步探索记录。详情页、学习卡和策展入口由同一标识衔接,不需要在导航参数中复制完整神兽对象。目录刷新后,入口仍可通过 id 获取最新的 summary、source 与关系数据。
private openBeast(beastId: string): void {
this.selectedBeastId = beastId;
this.syncDiscovery(beastId);
this.refreshAiContext(beastId);
}
private getBeast(beastId: string): BeastItem {
return this.beasts.find((item: BeastItem) => item.id === beastId) ?? this.beasts[0];
}
图鉴页将 beasts、已发现 ID、筛选输入和 onOpenBeast 回调交给结果网格。结果网格负责渲染卡片,状态计算保留在 ViewModel,跳转协调留在页面入口,三者各自只管理一种职责。ArkTS 的状态管理方式可结合 HarmonyOS 官方状态管理指南 进一步阅读。
稳定排序避免筛选后的卡片跳动
同一批条目在重复筛选时如果顺序不断变化,读者很难判断某张卡片是被过滤掉,还是仅仅移动了位置。因此图鉴为目录维护一套基础展示顺序;任何排序分支在比较条件相同或找不到预设编号时,都回落到该顺序。新增条目不会因为缺少排序权重而丢失,仍会以可预期的位置显示在列表末尾。
private baseOrder(beast: BeastItem): number {
const orderIndex: number = BEAST_ARCHIVE_DISPLAY_ORDER.indexOf(beast.id);
return orderIndex < 0 ? 999 : orderIndex;
}
private compare(
left: BeastItem,
right: BeastItem,
regions: RegionItem[],
discoveredIds: string[],
sortMode: string
): number {
if (sortMode === BEAST_ARCHIVE_SORT_REGION) {
const regionCompare: number = this.regionName(left.regionId, regions)
.localeCompare(this.regionName(right.regionId, regions));
return regionCompare !== 0 ? regionCompare : this.baseOrder(left) - this.baseOrder(right);
}
return this.discoveryOrder(left, discoveredIds) - this.discoveryOrder(right, discoveredIds);
}
这项约束尤其适合文化知识图鉴:内容运营可以增补条目,读者的发现记录也会随探索积累而变化,但基础目录、筛选视图和详情入口始终通过同一个 id 对齐。页面不需要为了“记住位置”额外存储临时索引,重新进入图鉴时仍可按同一计算规则恢复可读的浏览顺序。
从筛选到详情的验收路径
验证图鉴数据模型时,可以先进入图鉴并观察分类栏、发现印章与神兽卡片;随后切换分类或输入关键词,确认结果集随条件变化;最后打开应龙等卡片,确认名称、摘要、出处和关联信息来自同一条 BeastItem。当筛选条件清空后,完整目录能够重新出现,发现状态也不会被列表操作改写。
这套模型让图鉴在内容增加后仍保持可维护:新增神兽主要补充目录记录与地域关系,卡片筛选、排序与详情解析继续复用统一的 id 和字段约定。对于文化知识类应用,这比在每个页面复制一份展示对象更容易持续校对来源、路线与关联关系。
更多推荐

所有评论(0)