项目页面截图

ArkTS 严格模式对类型非常敏感。项目里一开始如果到处写临时对象、动态字段和 Record<string, ...>,后面很容易遇到编译错误,或者页面之间字段对不上。

这个桌面卡片工具项目把共享模型统一放在 AppModels.ets,再由服务层输出页面需要的视图模型。这样页面只关心“展示什么”,不关心底层数据从模板、本地卡片还是回收站来。

为什么先建模型

项目里有很多页面都在展示卡片:

  • 首页的“我的卡片”
  • 分类页的分类概览和热门卡片
  • 卡片详情页的摘要卡
  • 样式页的样式卡
  • 管理页的卡片列表
  • 桌面 Form 的摘要数据

如果每个页面自己定义字段,很快会出现字段不一致:

title
subtitle
value
badge
categoryId
templateId
cardId
imageKey

这些字段有些用于 UI,有些用于路由,有些用于资源映射。项目选择把它们抽成固定 interface。

ShowcaseCardModel:通用卡片展示模型

ShowcaseCardModel 是页面复用最高的模型:

export interface ShowcaseCardModel {
  id: string;
  title: string;
  subtitle: string;
  tone: ToneName;
  value?: string;
  footer?: string;
  badge?: string;
  route?: string;
  imageKey?: string;
  cardId?: string;
  templateId?: string;
  categoryId?: CardCategoryId;
}

它同时服务展示和跳转:

  • titlesubtitlevaluefooter 负责文案。
  • tone 负责颜色主题。
  • imageKey 负责图片资源。
  • cardIdtemplateIdcategoryId 负责点击后的语义。

这里没有把所有字段都做成必填。因为不同页面使用同一个卡片组件时,信息密度不同:详情页有完整卡片,分类概览可能只需要分类入口,样式页则更关注 imageKey

MenuRowModel:列表行也需要明确目标参数

列表项模型和卡片模型类似,但更适合单行结构:

export interface MenuRowModel {
  id: string;
  mark: string;
  title: string;
  subtitle: string;
  tone: ToneName;
  value?: string;
  route?: string;
  tabId?: string;
  imageKey?: string;
  cardId?: string;
  templateId?: string;
}

项目里曾经出现过“点击列表进详情但参数丢失”的问题。修复后的关键是:只要列表项会跳转详情,就必须带明确的 cardIdtemplateId

页面处理时也按优先级判断:

const cardId: string = item.cardId ? item.cardId : '';
if (cardId.length > 0) {
  router.pushUrl({
    url: RoutePaths.cardDetail,
    params: { cardId: cardId }
  });
  return;
}

const templateId: string = item.templateId ? item.templateId : item.id;

这样用户卡片和内置模板可以共用同一个详情页,但不会混成“空白 fallback”。

CardDraftModel 和 CardRecordModel:草稿和真实记录分开

编辑页使用的是 CardDraftModel,保存后的数据才是 CardRecordModel

export interface CardDraftModel {
  id: string;
  templateId: string;
  title: string;
  subtitle: string;
  detail: string;
  value: string;
  footer: string;
  badge: string;
  tone: ToneName;
  categoryId: CardCategoryId;
  favorite: boolean;
}

export interface CardRecordModel extends CardDraftModel {
  active: boolean;
  usageCount: number;
  createdAt: string;
  updatedAt: string;
  lastUsedAt: string;
  sceneTags: string[];
}

这个拆法有两个实际价值:

  1. 编辑页不需要伪造 createdAtusageCount 这类字段。
  2. 服务层保存时可以统一补齐运行时状态,页面不会乱写元数据。

枚举类型不要直接展示给用户

项目中的分类 ID 是稳定内部 key:

export type CardCategoryId =
  'daily' | 'countdown' | 'health' | 'tool' |
  'life' | 'study' | 'fun' | 'system';

这些 key 适合做持久化、筛选和路由参数,但不适合直接显示。详情页展示“类别”时没有直接渲染 countdown,而是调用:

appDataService.getCategoryLabel(this.card.categoryId)

这样用户看到的是“倒计时”“健康”“工具”这类中文文案。内部 key 和用户文案分离,是多语言、改文案和保持数据兼容的基础。

ArkTS 严格模式下的几个实践

这个项目里形成了几条比较稳定的规则:

  1. ForEach 的 item 和 key 函数都显式标注类型。
  2. 共享常量优先用静态类或具名 interface。
  3. 自定义组件回调字段避免叫 onClickonChange 这类容易和内置属性混淆的名字。
  4. 避免在 UI Builder 中声明局部 constlet,需要中间值时抽 helper。
  5. 不直接渲染内部枚举 key,服务层负责转成用户文案。

例如首页分类卡片:

ForEach(appDataService.getCategoryCards('recommend', this.categoryQuery),
  (item: ShowcaseCardModel) => {
    GridItem() {
      ShowcaseCard({
        item: item,
        compactBadge: true
      })
    }
  },
  (item: ShowcaseCardModel) => item.id
)

这比省略类型更啰嗦,但在严格模式下更稳。

视图模型比原始数据更适合页面

服务层可以把原始卡片、模板目录、分类元信息统一转换成页面模型。以详情页为例:

export interface CardDetailViewModel {
  card: CardRecordModel;
  isTemplate: boolean;
}

页面只需要根据 isTemplate 决定按钮:

  • 模板:显示“添加到我的卡片”
  • 我的卡片:显示“编辑当前卡片”
  • 空白 fallback:显示“新建卡片”

底层是模板还是用户数据,页面不直接判断。

小结

ArkTS 项目里,类型模型不是可有可无的“文档”,而是页面契约。这个桌面卡片工具项目用 AppModels.ets 把卡片、列表、草稿、记录、统计、备份、提醒都统一建模,再由 AppDataService 输出页面需要的视图模型。

这样写的直接收益是:页面少猜字段,路由少丢参数,资源映射更稳定,严格模式下的编译错误也更容易定位。

Logo

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

更多推荐