这一篇专门拆“内容底座”。离线旅行 App 看起来是页面项目,本质上先是内容项目:景点、美食、路线、城市和旅行贴士必须先被结构化,后面的首页推荐、探索筛选、详情页和收藏才能顺起来。

项目落点:

  • 类型定义:entry/src/main/ets/models/AtlasModels.ets
  • 离线内容:entry/src/main/ets/data/AtlasData.ets
  • 图片引用:entry/src/main/resources/base/media

为什么先做数据模型

做内容型 App 时,最容易犯的错误是先画页面,再在页面里临时拼字段。这样前期看起来快,后期一定会痛苦:详情页要字段,搜索页要字段,收藏页还要字段,最后每个页面都有一套自己的数据假设。

《疆域纪行》的做法是先建模,再写页面。项目里核心类型放在:

entry/src/main/ets/models/AtlasModels.ets

这个文件不长,但决定了整个 App 的内容表达能力。

景点和美食共用 AtlasItem

项目把景点和美食抽象成同一个 AtlasItem

export interface AtlasItem {
  id: string;
  kind: string;
  title: string;
  english: string;
  region: string;
  summary: string;
  tags: string[];
  meta: string;
  color: string;
  image: Resource;
  bestSeason: string;
  duration: string;
  cost: string;
  transport: string;
  highlights: string[];
  shops?: string[];
  tips: string[];
}

这个设计很实用。景点和美食虽然业务含义不同,但在列表、收藏、搜索、详情头部里需要的字段非常相似:

  • 标题:title
  • 英文名:english
  • 地区:region
  • 简介:summary
  • 标签:tags
  • 图片:image
  • 主题色:color
  • 注意事项:tips

共用模型后,很多 UI 组件可以复用,比如 ListCardFavoriteAtlasCardAtlasDetail。真正有差异的部分再用 kind 和可选字段控制,例如美食才会用到 shops

路线单独建模

路线和景点不一样。路线有天数、每日安排、住宿、餐食、行程亮点,所以项目定义了 RouteItem

export interface RouteItem {
  id: string;
  title: string;
  days: string;
  region: string;
  summary: string;
  tags: string[];
  color: string;
  image: Resource;
  bestSeason: string;
  transport: string;
  dailyPlans: string[];
  dayDetails: RouteDayDetail[];
  tips: string[];
}

export interface RouteDayDetail {
  title: string;
  meal: string;
  stay: string;
  highlight: string;
}

这里有一个不错的分层:dailyPlans 用于快速概览,dayDetails 用于路线详情页的时间轴。

也就是说,同一条路线在不同页面可以用不同的信息密度:

  • 首页卡片:标题、天数、简介、标签
  • 路线列表:封面、区域、适合季节、交通方式
  • 详情页:每日安排、住宿、餐食、亮点、提示

这就是内容建模的关键:不要让所有页面都被迫消费完整字段,也不要让详情页缺字段。

城市和贴士保持轻量

城市卡片模型:

export interface CityItem {
  id: string;
  name: string;
  region: string;
  summary: string;
  image: Resource;
  color: string;
  linkedRegion: string;
}

旅行贴士模型:

export interface TipItem {
  title: string;
  summary: string;
}

这两个类型都很克制。它们不追求“一步到位”,只满足当前页面需要。工程里建模不是字段越多越好,而是要匹配产品阶段。MVP 阶段只要城市能参与探索筛选、贴士能展示在“我的”页,就足够了。

本地状态也要建模

除了内容模型,项目还定义了本地状态:

export interface LocalAppState {
  favoriteIds: string[];
  searchHistory: string[];
  itineraryIds: string[];
}

这个模型看起来简单,却很重要。它把“用户数据”和“内容数据”分开了:

  • 内容数据:景点、路线、美食,随应用发布。
  • 用户数据:收藏、历史、行程,只存在本机。

这样做后,本地存储服务只需要负责 LocalAppState 的读写,页面只需要处理数组状态,不用到处关心 Preferences 的 key 和序列化格式。

离线内容库如何写

内容库放在:

entry/src/main/ets/data/AtlasData.ets

一个景点数据大概长这样:

{
  id: 'spot_sayram',
  kind: '景点',
  title: '赛里木湖',
  english: 'Sayram Lake',
  region: '北疆-伊犁地区',
  summary: '雪山环抱的高山湖泊,是进入伊犁河谷前最经典的湖泊目的地。',
  tags: ['湖泊', '雪山', '摄影', '自驾'],
  meta: '博尔塔拉 / 伊犁门户 · 5月-9月',
  color: '#2F6F9F',
  image: $r('app.media.img_sayram'),
  bestSeason: '5月-9月',
  duration: '3-5小时',
  cost: '门票以景区公告为准',
  transport: '自驾 / 包车',
  highlights: ['环湖公路', '雪山倒影', '日出日落', '松树头观景'],
  tips: ['湖区早晚温差大', '夏季也建议带外套', '环湖建议预留半天以上']
}

这里有几个值得学习的点。

第一,id 是稳定主键。收藏、行程、详情页查找都靠它,而不是靠标题。标题可能会改,id 不应该轻易改。

第二,image 使用 Resource

image: $r('app.media.img_sayram')

这样图片会走 HarmonyOS 的资源系统,打包、引用、预览都更稳定。

第三,color 跟着内容走。每个景点和美食都有自己的主题色,卡片加载图片前可以先显示色块,也能让同一套卡片组件有差异化气质。

第四,tips 不写成长字符串,而是数组。详情页可以逐条渲染,后续也方便做图标化或重要程度排序。

内容库规模

当前项目内置了:

  • 20 多个景点
  • 6 个美食条目
  • 7 条路线
  • 8 个城市区域
  • 5 条旅行贴士

它不是一个“空壳 Demo”,而是已经具备可浏览内容的离线 App。对旅行攻略类应用来说,内容量直接决定页面是否真实。只有数据足够多,搜索、筛选、空状态、卡片复用才有测试价值。

静态 TS 数据 vs JSON 文件

这个项目选择把内容写在 AtlasData.ets 中,而不是放到 rawfile JSON。两种方式各有取舍。

写在 TS 里的优点:

  • 字段有类型检查。
  • $r() 资源引用更直接。
  • 编译期就能发现字段缺失。
  • 小规模内容维护快。

JSON 的优点:

  • 内容和代码分离更彻底。
  • 后续可以做内容导入、更新包、远程配置。
  • 非开发人员也更容易参与维护。

我的建议是:MVP 和小规模内容库,用 TS 静态数据更高效;当内容增长到数百条,再考虑迁移到 JSON、SQLite 或可导入离线包。

数据驱动页面的第一步

Index.ets 里,页面直接引用这些内容:

private readonly spots: AtlasItem[] = SPOTS;
private readonly foods: AtlasItem[] = FOODS;
private readonly routes: RouteItem[] = ROUTES;
private readonly cities: CityItem[] = CITIES;
private readonly tips = TIPS;

private getAllItems(): AtlasItem[] {
  return [...this.spots, ...this.foods];
}

getAllItems() 是一个小函数,但很有用。搜索、收藏等功能不需要分别处理景点和美食,而是先拿到统一的 AtlasItem[]

这就是数据建模带来的收益:页面逻辑会变短,组件复用会变自然。

实战经验:内容模型要服务页面,不要炫技

做内容模型时,有两个常见误区:

第一个误区是过度抽象。比如一上来就设计 BaseEntity<T>ContentBlockMetaSchema。对 MVP 来说,这会增加理解成本。

第二个误区是完全不抽象。所有字段都散在页面里,列表一份、详情一份、收藏一份,后期改字段会非常痛。

《疆域纪行》的模型刚好处在中间:

  • 景点和美食共用 AtlasItem
  • 路线独立成 RouteItem
  • 城市和贴士轻量建模
  • 本地状态独立成 LocalAppState

这是一个很适合初中级 HarmonyOS 项目的数据模型范式。

本篇小结

这一篇我们完成了离线内容库的设计拆解:

  • 用 ArkTS interface 定义业务模型。
  • 用稳定 id 连接收藏、详情和行程。
  • Resource 引用本地图片。
  • 用数组字段支持标签、亮点、贴士等多值内容。
  • 把内容数据和用户本地状态分开。

下一篇进入 ArkUI 页面实战:如何用大图 Hero、横向卡片、底部导航,搭建一个有旅行杂志感的首页。

Logo

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

更多推荐