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

运行界面

一条记录覆盖卡片、详情与路线

BeastItem 不把“神兽”简化为标题和一段简介。regionIdhallId 为图鉴与场馆路线提供连接点,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[];
}
字段组 图鉴页的职责 详情页与后续模块的职责
idnonamealias 标识卡片并支持关键字匹配 id 解析当前对象,保留统一身份
regionIdhallId 显示出没地并参与地域排序 跳转展厅、组织导览路线
summarydetail 卡片只读取摘要 详情页展开完整说明
source 提供简短出处提示 展开章节、定位链接和阅读摘录
relations 不在卡片上堆叠关系 交给关联推荐与图谱面板

数据模型的边界也很明确:列表不保存一份可编辑的详情副本,页面状态也不把筛选结果回写到原始目录。只要目录本身不被筛选过程改写,切换分类、搜索或排序后仍然可以回到完整图鉴。

可见列表是从原始目录计算出来的视图

图鉴的筛选状态被收敛到 BeastArchiveQuery。分类、搜索词、排序方式和发现筛选都是输入;getVisibleBeastsbeasts.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 数组
输入搜索词 名称、别名、摘要与地域名 覆盖详情中的 detailsource
选择“已发现” 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 获取最新的 summarysource 与关系数据。

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 和字段约定。对于文化知识类应用,这比在每个页面复制一份展示对象更容易持续校对来源、路线与关联关系。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐