【时光清单|05】HarmonyOS ArkTS 倒计时卡片实战:适配 2x2、2x4 与 4x4 多尺寸

桌面卡片不是把应用页面按比例缩小。2x2 只够放一个强数字,2x4 可以补充标题和说明,4x4 才有空间建立更完整的阅读层级。如果三种尺寸共用一棵拥挤的 UI 树,最小尺寸会被迫塞入过多信息;如果每种尺寸各自读取数据,又容易出现日期算法、默认内容和选中事项不一致。

时光清单 的真实源码采用了更清晰的结构:CountdownFormAbility 只负责从纪念日仓库生成统一 FormData,2x2、2x4、4x4 三个 ArkTS 卡片页面分别通过 @LocalStorageProp 读取数据并独立排版。form_config.json 明确注册三个表单,分别声明支持尺寸、默认尺寸、颜色模式和更新时间。

本文以当前仓库中可读取的真实文件为主线,拆解多尺寸卡片的工程方法:先统一数据契约,再按面积分配信息优先级;把更新与降级逻辑放在 FormExtensionAbility;最后检查深浅色、长标题、空数据、点击事件和多卡片实例。本文不据此推断未在源码和历史记录中出现的系统版本、运行结果或发布效果。

时光清单倒计时卡片封面

本文会完成这些复核:

  1. 解释应用内 CountdownCard 与桌面 Form 卡片的职责差异。
  2. 对比 2x2、2x4、4x4 三种尺寸的真实布局。
  3. 说明统一 FormData 如何连接仓库和 @LocalStorageProp
  4. 检查添加、定时更新、选择事项与空数据降级链路。
  5. 找出点击文案未绑定事件、深色模式和多实例选择的改进点。

本文唯一标记:CSDN-SERIES:ALL-163202768

阅读前先划清证据边界

这篇文章把结论分成三类,避免把“代码里写了”误写成“设备上已经成功运行”。

当前事实来自 D:\huawei\one8 的静态源码复核。可以确认的是:模块注册了一个导出的 CountdownFormAbilityform_config.json 配置了 CountdownSmallCountdownMediumCountdownLarge 三个独立 ArkTS Form;它们分别绑定 2x2、2x4、4x4 页面;能力层通过同一 getFormData() 生成标题、天数、备注和更新时间;三个页面按面积选择不同字段。模块的 deviceTypes 当前只声明 phone

历史证据来自项目错误记录。记录表明,2026 年 5 月 20 日曾出现“桌面卡片不能选择并显示已有纪念日”的问题,原因是组件选择结果没有持久化;当时的修复是保存选中 ID,并让 Form 读取同一个键。记录还写有当时 assembleHap 通过,但那只是历史检查结果,不代表本文写作时重新构建成功,更不代表真机、商店审核或线上用户数据已经验证。

建议实现是本文在现状之上提出的演进方向,包括按 formId 保存多实例选择、增加日期方向、补齐真实点击事件、使用主题资源、在数据保存后主动请求更新。它们不是当前源码已有能力,文中会明确使用“建议”“可以”“如果实现”等表述。

下面这份最小清单用于限定本文实际核对的对象:

entry/src/main/module.json5
entry/src/main/resources/base/profile/form_config.json
entry/src/main/ets/forms/CountdownFormAbility.ets
entry/src/main/ets/forms/pages/CountdownCard2x2.ets
entry/src/main/ets/forms/pages/CountdownCard2x4.ets
entry/src/main/ets/forms/pages/CountdownCard4x4.ets
entry/src/main/ets/views/WidgetView.ets
entry/src/main/ets/components/CountdownCard.ets

本文没有执行新的 HAP 构建、安装、桌面添加、06:00 调度、点击跳转或多实例真机测试,因此这些项目都保留为待验证项。

一、先区分两类卡片:应用组件与桌面 Form

项目中有两套“倒计时卡片”:

components/CountdownCard.ets
  应用页面内部复用组件

forms/pages/CountdownCard2x2.ets
forms/pages/CountdownCard2x4.ets
forms/pages/CountdownCard4x4.ets
  系统桌面卡片页面

应用内组件接收完整 Anniversary

@Component
export struct CountdownCard {
  @Prop item: Anniversary;
  onCardClick?: () => void;

  build() {
    Column() {
      // 图标、标题、备注、置顶状态和天数
    }
    .onClick(() => {
      if (this.onCardClick) {
        this.onCardClick();
      }
    })
  }
}

它能访问主题色、资源映射、完整类型和页面回调。桌面 Form 则只消费少量字符串,不应把整个领域对象复制进去。两者共享的是日期语义,不是 UI 树。

当前数据与更新链路

二、统一 FormData:三种尺寸只拿必要字段

CountdownFormAbility 定义了窄数据契约:

interface FormData {
  title: string;
  days: string;
  subtitle: string;
  updateTime: string;
}

四个字段分别服务于:

字段 2x2 2x4 4x4 作用
title 当前事项名称
days 强视觉倒计时数字
subtitle 备注或降级说明
updateTime 不展示 不展示 不展示 让绑定数据每次带新值

2x2 不渲染 subtitle,但继续接收同一数据对象并没有问题。能力层无需判断尺寸,只负责给出完整、稳定、可降级的数据。

如果以后 4x4 要展示日期或图标,可以扩展可选字段,但要保证旧尺寸忽略新字段后仍能正常渲染。

三、添加卡片:同步读取是为了首帧可用

卡片添加时走 onAddForm()

onAddForm(
  want: Want
): formBindingData.FormBindingData {
  hilog.info(0x0000, TAG, 'onAddForm');
  const formData = this.getFormData();
  return formBindingData
    .createFormBindingData(formData);
}

getFormData() 使用仓库同步入口:

const repo =
  AnniversaryRepository.getInstance();
repo.initSync(this.context);
const items = repo.getAllSync();

这与应用页面的异步读取不同。Form 添加回调需要立即返回绑定数据,项目通过 DataStore.initSync(context) 读取 Preferences,再由仓库执行统一排序。桌面卡片因此不会自行解析 JSON,也不会复制“置顶优先、日期升序”的规则。

同步读取适合这种小型本地数组。若数据量扩大或迁移到 RDB,应评估回调时延,并考虑缓存一份专供 Form 的轻量快照。

四、选择事项:选中 ID 优先,否则使用排序第一项

能力层读取 WIDGET_ANNIVERSARY_ID

const selectedId =
  DataStore.getInstance()
    .getJsonSync<string>(
      DataKeys.WIDGET_ANNIVERSARY_ID,
      ''
    );
const selected = items.find(
  (item: Anniversary) =>
    item.id === selectedId
);
const top = selected ?? items[0];

这条降级链很实用:

  1. 用户选中的事项仍存在时,展示它。
  2. 选中事项被删除时,回退到排序后的第一项。
  3. 没有任何事项时,走空数据默认内容。

真实仓库的 getAllSync() 会返回置顶优先、目标日期升序的副本,所以 items[0] 不是随机记录。

当前 WIDGET_ANNIVERSARY_ID 是全局单值。如果用户在桌面添加两个倒计时卡片并希望各自展示不同事项,这个模型不够。多实例方案应按 formId 保存选择:

export interface FormSelectionMap {
  [formId: string]: string;
}

export function getSelectedId(
  formId: string,
  selections: FormSelectionMap
): string {
  return selections[formId] ?? '';
}

onRemoveForm(formId) 中清理对应映射,避免已删除卡片留下无效状态。

五、日期语义:当前统一显示绝对天数

能力层计算:

const days = Math.abs(
  calcDaysRemaining(top.targetDate)
);

const fd: FormData = {
  title: top.title,
  days: `${days}`,
  subtitle:
    top.remark ?? '记录重要时刻',
  updateTime: Date.now().toString()
};

Math.abs() 让未来三天和过去三天都显示数字 3。应用内卡片会另外显示“还剩”或“已过去”,而桌面 Form 的 subtitle 来自备注,并不包含方向信息。因此当前桌面卡片在目标日期过后可能产生歧义。

更完整的数据契约可以增加:

export type CountdownDirection =
  'future' | 'today' | 'past';

interface FormDataV2 {
  title: string;
  days: string;
  direction: CountdownDirection;
  label: string;
  subtitle: string;
  updateTime: string;
}

构造时明确:

const rawDays =
  calcDaysRemaining(top.targetDate);
const direction: CountdownDirection =
  rawDays === 0
    ? 'today'
    : rawDays > 0
      ? 'future'
      : 'past';
const label = direction === 'today'
  ? '就是今天'
  : direction === 'future'
    ? '还剩'
    : '已过去';

2x2 可以只展示数字与“天”,2x4 和 4x4 则加上方向标签。数据语义统一,尺寸只决定展示多少。

六、2x2:数字优先,标题退到第三层

真实 2x2 页面只读取标题和天数:

@Entry
@Component
struct CountdownCard2x2 {
  @LocalStorageProp('title')
  title: string = '倒计时';

  @LocalStorageProp('days')
  days: string = '0';

  build() {
    Column() {
      Text(this.days)
        .fontSize(36)
        .fontWeight(FontWeight.Bold)
        .fontColor('#2C5F7C')

      Text('天')
        .fontSize(12)
        .fontColor('#6B6B6B')

      Text(this.title)
        .fontSize(11)
        .fontColor('#666666')
        .maxLines(1)
        .textOverflow({
          overflow: TextOverflow.Ellipsis
        })
    }
    .justifyContent(FlexAlign.Center)
  }
}

这个层级适合小尺寸:用户第一眼看到天数,再确认单位,最后识别事项。标题限制一行并使用省略,避免长文本把数字挤出卡片。

2x2 不适合展示备注、品牌、副标题、按钮和多个事项。小卡片的目标不是信息完整,而是“一眼读懂一个值”。

七、2x4:横向分栏,左内容右数字

2x4 使用 Row

Row() {
  Column() {
    Text(this.title)
      .fontSize(14)
      .fontWeight(FontWeight.Medium)
      .maxLines(1)

    Text(this.subtitle)
      .fontSize(11)
      .margin({ top: 4 })
      .maxLines(1)
  }
  .layoutWeight(1)

  Column() {
    Text(this.days)
      .fontSize(40)
      .fontWeight(FontWeight.Bold)

    Text('天')
      .fontSize(12)
  }
}

左侧 layoutWeight(1) 吸收剩余宽度,右侧数字保持强视觉。相比写死左侧宽度,这种布局更能适应不同字体和系统缩放。

标题和副标题虽然都限制一行,但源码没有为标题设置 textOverflow。长标题可能在不同桌面网格下截断不一致,建议补上省略策略,并给右侧数字加 constraintSize,防止极大天数挤压左栏。

八、4x4:增加品牌和操作暗示

4x4 的信息层级最完整:

Column() {
  Row() {
    Text(this.title)
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .layoutWeight(1)

    Text('时光清单')
      .fontSize(10)
  }

  Blank()

  Row() {
    Text(this.days)
      .fontSize(56)
      .fontWeight(FontWeight.Bold)

    Text(' 天')
      .fontSize(18)
  }

  Text(this.subtitle)
    .fontSize(13)
    .maxLines(2)

  Blank()

  Text('点击查看详情 >')
    .fontSize(11)
}

两个 Blank() 把标题、核心数字、说明和底部操作暗示分散到纵向空间,避免内容都堆在顶部。

但这里存在真实的一致性问题:源码显示“点击查看详情 >”,页面本身没有绑定点击事件,CountdownFormAbility.onFormEvent() 也只写日志。因此这段文案当前是不可兑现的交互承诺。修复有两种方向:

  1. 接入 Form 支持的点击事件与应用跳转,再保留文案。
  2. 暂不支持点击时,删除操作暗示,换成纯状态文案。

不能只因为视觉上像按钮,就让用户以为一定可点击。

九、三种尺寸不是缩放,而是信息裁剪

可以把当前设计整理成一张优先级表:

信息 2x2 2x4 4x4
天数 最强
单位
标题 一行、小字号 一行 一行、较强
备注 一行 两行
品牌
点击暗示 当前有文案但无事件

这种“按尺寸裁剪”的方式比统一缩放更符合多尺寸设计。新增字段时先判断它属于哪一级信息,再决定从哪个尺寸开始展示。

多尺寸信息层级与建议能力边界

十、配置文件把尺寸和页面一一绑定

form_config.json 注册三个表单:

{
  "forms": [
    {
      "name": "CountdownSmall",
      "src":
        "./ets/forms/pages/CountdownCard2x2.ets",
      "uiSyntax": "arkts",
      "colorMode": "auto",
      "supportDimensions": ["2*2"],
      "defaultDimension": "2*2",
      "updateEnabled": true,
      "scheduledUpdateTime": "06:00",
      "updateDuration": 1
    }
  ]
}

中、大尺寸分别绑定 CountdownCard2x4.etsCountdownCard4x4.ets。每个表单只声明一个支持尺寸,因此系统选择尺寸时不会让同一个页面被强行拉伸到完全不同的结构。

配置中 colorModeauto,三个页面却都硬编码 #FFFFFF 背景和固定灰色文字。系统切换深色模式时,卡片仍可能保持白底,或与桌面主题显得割裂。若要真正支持 auto,应使用可适配资源或明确的深浅色分支;如果产品决定固定浅色,也要验证系统深色桌面上的边界和对比度。

十一、更新链路:统一回到 getFormData

系统请求更新时:

onUpdateForm(formId: string): void {
  hilog.info(
    0x0000,
    TAG,
    `onUpdateForm: ${formId}`
  );
  const data =
    formBindingData.createFormBindingData(
      this.getFormData()
    );
  formProvider.updateForm(
    formId,
    data
  ).catch((err: Error) => {
    hilog.error(
      0x0000,
      TAG,
      `updateForm error: ${err.message}`
    );
  });
}

添加与更新都调用 getFormData(),保证选择逻辑、天数算法和空态一致。formId 当前只用于更新目标卡片,没有参与数据选择,因此所有实例会拿到同一选中事项。

配置声明每天 06:00 更新,同时开启周期更新字段。具体调度频率、最小周期和设备策略应以目标 HarmonyOS SDK 的 Form Kit 文档及真机表现为准,不能把配置时间理解成绝对准点保证。系统可能基于资源和策略调整执行。

应用内修改纪念日后,如需立即刷新桌面卡片,还应在保存成功后请求对应 Form 更新,而不是只等下一次系统调度。

十二、空数据与异常降级

没有事项或读取异常时,真实能力层返回:

const fd: FormData = {
  title: '时光清单',
  days: '0',
  subtitle: '添加事项开始倒计时',
  updateTime: Date.now().toString()
};

这个降级不会让卡片白屏,但 days: '0' 可能被误解成“今天就是某个日子”。更明确的设计可以增加 state

type FormState =
  'content' | 'empty' | 'error';

interface FormDataV3 {
  state: FormState;
  title: string;
  days: string;
  subtitle: string;
  updateTime: string;
}

空态时 2x2 可以显示加号或“添加”,2x4/4x4 展示引导文案;错误态使用“暂时无法读取”,避免把系统故障伪装成零天。

异常日志当前使用 JSON.stringify(e)。生产环境应避免把私密标题或完整业务对象写入日志,只记录错误类型和必要诊断码。

十三、点击事件与路由契约

onFormEvent() 当前只是:

onFormEvent(
  formId: string,
  message: string
): void {
  hilog.info(
    0x0000,
    TAG,
    `onFormEvent: ${formId}, msg: ${message}`
  );
}

若要支持“点击查看详情”,应让卡片发送窄消息,例如:

interface FormActionMessage {
  version: 1;
  action: 'open_detail';
  anniversaryId: string;
}

能力层解析后,只把实体 ID交给应用路由;应用重新查询最新记录。记录已删除时回退列表。不要把完整实体 JSON 永久写进卡片事件,因为桌面卡片可能在数据更新后仍保留旧快照。

多实例时消息还应包含或结合 formId,确认点击的是哪一张卡片。

十四、长文本与极值适配

需要主动测试这些内容:

  1. 1 个汉字标题。
  2. 30 个汉字标题。
  3. 中英数字混合且没有自然断点。
  4. 空备注与两行长备注。
  5. 天数为 0、9、99、999、9999。
  6. 系统字体放大。

2x2 的 36fp 和 4x4 的 56fp 在四位数时可能过宽。可以动态分级:

function daysFontSize(
  days: string,
  large: boolean
): number {
  const length = days.length;
  if (large) {
    if (length >= 5) return 36;
    if (length >= 4) return 44;
    return 56;
  }
  if (length >= 4) return 28;
  return 36;
}

动态字号应按数字长度或容器约束计算,不依赖屏幕宽度。目标是让核心数字始终在卡片内部,而不是追求所有状态同字号。

十五、性能边界:卡片回调只做必要工作

getFormData() 当前读取一个小型 Preferences 数组、排序并选一项,成本可控。仍应遵守:

  1. 不在回调中访问网络。
  2. 不加载大图或解码媒体。
  3. 不扫描无关数据键。
  4. 不执行复杂迁移。
  5. 异常快速返回降级数据。

updateTime 每次变化会让绑定数据具有新值,但不要把它渲染到 UI,也不要用高频更新追求秒级倒计时。这个卡片的业务单位是“天”,每日或数据变化时更新即可,能耗和用户价值更匹配。

十六、真机验证矩阵

配置和布局必须在真实桌面卡片环境验证:

  1. 三种表单都能在卡片选择器中出现,名称和描述正确。
  2. 2x2、2x4、4x4 添加后分别加载对应页面。
  3. 空数据时不白屏、不误导为真实“0天”事项。
  4. 有选中 ID时展示选中事项。
  5. 选中事项删除后回退排序第一项。
  6. 未来日期和过去日期的方向文案正确。
  7. 当天日期显示符合产品定义。
  8. 长标题、长备注和四位天数不溢出。
  9. 系统字体放大后核心数字和标题仍可读。
  10. 浅色与深色桌面下背景、文字、边界对比清楚。
  11. 06:00 计划更新在真机上实际触发,记录执行时间。
  12. 应用内修改事项后,主动刷新路径生效。
  13. 添加两张卡片时,确认是否应共享同一事项。
  14. 若实现点击,冷启动和热启动都进入正确详情。
  15. 删除桌面卡片后清理按 formId 保存的选择状态。

当前模块只声明 phone,因此这里验证的是手机桌面卡片。没有经过平板或其他设备注册与材料验证前,不应宣称已完成那些设备适配。

十七、常见故障与排查顺序

现象 常见原因 修复方向
三种尺寸内容不一致 各页面自行读数据 统一由 FormAbility 生成 FormData
添加后显示默认值 同步仓库未初始化 在回调中完成同步轻量读取
删除选中事项后白屏 没有回退项 selected ?? items[0]
过去日期只显示数字 使用 Math.abs() 丢方向 增加 direction 与 label
4x4 写着可点击但没反应 没有事件绑定 接入 Form 事件或删除提示
深色桌面仍是突兀白卡 auto 配合硬编码白色 使用主题资源或明确固定模式
两张卡片总显示同一事项 选择 ID为全局单值 按 formId 保存映射
天数变了但卡片没刷新 只依赖计划更新 数据保存后请求更新 Form
四位天数挤出容器 字号固定 按长度分级并加约束

排查时按“配置是否注册、能力层是否生成数据、绑定键是否匹配、页面是否渲染、系统是否更新”五层进行。不要一开始就修改字号,因为很多“显示默认值”其实是数据绑定或仓库初始化问题。

十八、历史问题复盘:选择状态为什么必须成为数据契约

历史错误记录提供了一个很有价值的边界:应用内预览选中了某个纪念日,不等于桌面 Form 自动知道这个选择。应用页面、桌面卡片和 FormExtensionAbility 处在不同的界面与生命周期中,只有把选择写入双方都能读取的持久化键,能力层才能在添加或更新卡片时恢复同一个业务对象。

当前 WidgetView 会加载纪念日列表,读取 WIDGET_ANNIVERSARY_ID;没有已有选择时,它会把第一条记录的 ID 写进去。用户在列表中切换事项时,也会更新这个全局键。AddView 在第一次保存记录且尚无选择时,同样会把新记录 ID 写为默认选择。CountdownFormAbility 再读取这个键,从仓库结果中查找对应记录。这里可以确认的是静态数据链已经闭合;不能仅凭代码断言跨进程可见性、写入时序和桌面刷新时机已经在本文环境中验证。

这段历史也说明,组件预览与真实 Form 必须分开验收。WidgetView 中的小、中、大预览由应用页面自己的 Builder 和主题令牌绘制;真正的桌面内容由三个 forms/pages 页面绘制。预览正确只能证明应用内布局状态合理,不能替代系统桌面上的 Form 加载、数据绑定和更新测试。

建议为这条链路建立一份窄而明确的存储契约,而不是让页面随意拼接键名:

// 建议实现,当前源码仍使用单个全局键。
export interface WidgetSelectionSnapshot {
  version: 1;
  globalSelectedId: string;
  selectedByFormId: Record<string, string>;
  updatedAt: number;
}

version 用于将来迁移,globalSelectedId 可保留为旧数据或默认值,selectedByFormId 才承担多实例差异。迁移时先读取旧键,再在用户真正为某个实例做选择时写入映射,避免升级后突然清空已有选择。

十九、建议把“数字”升级成可判定的业务状态

当前能力层对 calcDaysRemaining() 使用 Math.abs(),因此它输出的是距离绝对值,而不是完整倒计时语义。对视觉稿来说,数字 3 很干净;对用户来说,“还有 3 天”和“已经过去 3 天”完全不同。当天、空数据和读取失败也不应都借用数字 0 表达。

建议先在能力层构造离散状态,再让不同尺寸决定展示多少字段:

// 建议实现,不是当前 FormData 的真实定义。
type CountdownState = 'future' | 'today' | 'past' | 'empty' | 'error';

interface CountdownFormSnapshot {
  state: CountdownState;
  title: string;
  days: string;
  directionLabel: string;
  subtitle: string;
  anniversaryId: string;
  updatedAt: string;
}

2x2 可以在 futurepast 状态下仍突出绝对天数,但至少用“还剩”“已过”或颜色、图标之一提供方向;today 应显示“今天”而不是模糊的 0empty 应引导添加事项;error 应说明暂时无法读取。2x4 可以增加一行方向或备注,4x4 再展示更完整说明。这样三种尺寸共用同一语义,不会由三个页面各自猜测业务状态。

状态转换函数也应保持纯净,便于单元测试边界日期:

// 建议实现:rawDays 的正负含义必须与项目日期工具保持一致。
function resolveCountdownState(rawDays: number): CountdownState {
  if (rawDays === 0) return 'today';
  return rawDays > 0 ? 'future' : 'past';
}

这里特别要避免直接照抄正负判断。实施前应先用项目当前 calcDaysRemaining() 对“昨天、今天、明天”各跑一个固定样例,确认正数究竟代表未来还是过去,再写断言。本文只能确认调用处用了绝对值,不能在未运行样例时把方向规则当成已证实事实。

二十、多实例不是换一个键名,而是完整生命周期

如果产品只允许所有桌面卡片共享同一个事项,当前全局选择 ID 已经足够,界面还应明确告诉用户“所有卡片同步”。如果希望两张卡片分别显示生日和纪念日,就需要让 formId 进入选择、更新、点击和删除四条链路。

建议的读取优先级是“实例选择、全局默认、排序第一项、空态”。伪代码如下:

// 建议实现。
const selectedId = selectedByFormId[formId] ?? globalSelectedId;
const selected = items.find((item) => item.id === selectedId);
const target = selected ?? items[0];

写入时要知道用户正在配置哪一个 formId;更新时只刷新受影响实例,或者在数据变化影响多个实例时枚举对应卡片;删除时在 onRemoveForm(formId) 清理映射。若被选事项已删除,不要只删除选择键,还应按既定降级规则生成下一帧数据。否则同一问题会以“卡片突然回默认值”的形式再次出现。

多实例还会影响隐私与日志。日志可以记录 formId 的脱敏片段、状态枚举和错误码,但不应输出纪念日标题、备注或完整持久化快照。调试便利不能成为长期暴露用户内容的理由。

二十一、点击与主动更新必须用真实闭环验收

当前 4x4 页面展示“点击查看详情”,但所检查页面没有看到对应动作绑定,能力层 onFormEvent() 也只记录日志。建议先定义窄动作,再接入目标 SDK 支持的 Form 交互 API。消息只携带动作、版本和实体 ID,详情页启动后重新从仓库查询最新数据:

// 建议契约;具体动作绑定 API 以目标 SDK 官方文档为准。
interface OpenDetailAction {
  version: 1;
  action: 'open_detail';
  anniversaryId: string;
  formId: string;
}

验收时至少覆盖冷启动、热启动、记录已删除、ID 非法、重复点击和返回桌面。只有文字、动作绑定、能力回调和应用路由都走通,才可以保留“点击查看详情”。在此之前,删除这句提示比留下无法兑现的操作更诚实。

更新也应区分两类触发。计划更新用于跨天后刷新天数,数据变更更新用于新增、编辑、删除、置顶或切换选择后尽快同步。form_config.json 中出现 scheduledUpdateTimeupdateDurationupdateEnabled,只能证明配置字段存在;是否按预期触发、触发精度以及系统资源策略,需要目标设备记录实际回调时间。

建议的数据变更路径是:仓库写入确认成功,计算受影响的卡片实例,请求更新,更新失败时保留持久化结果并记录可诊断错误。不能在持久化尚未确认时先刷新卡片,否则内存、磁盘与桌面可能短暂分叉。也不能因为 updateTime 每次变化,就宣称系统一定会主动拉取新数据;它只是当前绑定数据中的一个变化字段。

二十二、把验收结果分级,避免一次通过代表全部通过

这类功能适合把证据分成五级。第一级是静态源码:注册项、页面路径、字段名和降级逻辑存在。第二级是本地构建:目标命令真实执行并通过。第三级是设备运行:安装、添加三种尺寸、更新、空态、长文本和深浅色实际可见。第四级是生命周期:跨天、重启、删除事项、删除卡片、多实例、冷启动点击都成立。第五级才是发布侧证据:包体、材料、审核与线上状态。

本文当前只完成第一级复核,并引用了一条明确标注日期的历史构建记录。下列结果都没有在本文环境中重新获得,因此不写成完成:当前构建成功、真机添加成功、06:00 准点触发、点击详情成功、多实例独立选择成功、发布审核通过、用户使用量或平台评分。

建议后续每次验证都保留“命令或动作、时间、设备或环境、预期、实际、日志证据”六项。比如计划更新不要只写“等到第二天看看”,而应记录卡片添加时间、设备电量与网络状态、预期窗口、onUpdateForm 实际日志时间和渲染结果。这样遇到系统调度偏差时,才能判断是配置、系统策略、进程状态还是数据生成问题。

当以上证据逐层补齐后,再把建议代码转成当前事实。这个顺序虽然比直接宣布“已适配”更慢,却能让文章、源码和真实设备始终保持一致。

总结

时光清单 的倒计时 Form 已经建立了可迁移的多尺寸骨架:三种尺寸各自拥有独立 ArkTS 页面,统一通过 FormData 接收同一仓库结果,添加与更新复用同一数据生成函数,空数据也有降级内容。

多尺寸适配的核心不是三份相似代码,而是两条稳定边界:数据语义只定义一次,信息密度按尺寸递增。接下来应补齐方向标签、按实例选择、主动更新、深浅色资源和真实点击事件。把这些边界验证清楚后,2x2、2x4 与 4x4 才不是三个静态截图,而是同一业务在不同桌面面积上的一致表达。

AI 辅助声明:本文由 AI 辅助整理,结论与代码路径基于 D:\huawei\one8CountdownCard.ets、三个 Form 页面、CountdownFormAbility.ets、仓库和 form_config.json 的真实源码复核;扩展模型与交互方案需结合目标 HarmonyOS SDK 和真机测试后采用。

Logo

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

更多推荐