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; }它同时服务展示和跳转:
title、subtitle、value、footer负责文案。tone负责颜色主题。imageKey负责图片资源。cardId、templateId、categoryId负责点击后的语义。
这里没有把所有字段都做成必填。因为不同页面使用同一个卡片组件时,信息密度不同:详情页有完整卡片,分类概览可能只需要分类入口,样式页则更关注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; }项目里曾经出现过“点击列表进详情但参数丢失”的问题。修复后的关键是:只要列表项会跳转详情,就必须带明确的cardId或templateId。
页面处理时也按优先级判断:
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[]; }这个拆法有两个实际价值:
- 编辑页不需要伪造
createdAt、usageCount这类字段。 - 服务层保存时可以统一补齐运行时状态,页面不会乱写元数据。
枚举类型不要直接展示给用户
项目中的分类 ID 是稳定内部 key:
export type CardCategoryId = 'daily' | 'countdown' | 'health' | 'tool' | 'life' | 'study' | 'fun' | 'system';这些 key 适合做持久化、筛选和路由参数,但不适合直接显示。详情页展示“类别”时没有直接渲染countdown,而是调用:
appDataService.getCategoryLabel(this.card.categoryId)这样用户看到的是“倒计时”“健康”“工具”这类中文文案。内部 key 和用户文案分离,是多语言、改文案和保持数据兼容的基础。
ArkTS 严格模式下的几个实践
这个项目里形成了几条比较稳定的规则:
ForEach的 item 和 key 函数都显式标注类型。- 共享常量优先用静态类或具名 interface。
- 自定义组件回调字段避免叫
onClick、onChange这类容易和内置属性混淆的名字。 - 避免在 UI Builder 中声明局部
const或let,需要中间值时抽 helper。 - 不直接渲染内部枚举 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输出页面需要的视图模型。
这样写的直接收益是:页面少猜字段,路由少丢参数,资源映射更稳定,严格模式下的编译错误也更容易定位。