前言

「岁时·中国节」最初只是一个很朴素的想法:打开元服务就能知道下一个传统节日还有多久,不必再去日历里翻日期。真正开始写之后,事情很快变得复杂起来。农历怎么换算,除夕怎么算,节日资料放在哪里,桌面卡片又该展示什么,这些都不是生成一个页面就能解决的。

这次我没有从空白工程直接开写,而是先整理项目规划书、数据结构、页面要求和参考物料,再让 HarmonyOS 开发助手参与代码生成和修改。它确实省下了不少搭骨架、查错误的时间,但需求边界、代码核对和真机验收仍然要由开发者把关。

下面记录的是这个项目从规划到上架的实际过程,代码和目录也都来自最终工程。

先看成品

打开首页,最近节日和倒计时会直接出现在眼前,继续向下可以查看近期节日与全年节日。进入详情页后,节日由来、习俗和诗词会集中展示,祝福语则可以按长辈、亲友、恋人、同事客户等对象筛选。遇到合适的文案,可以直接复制、收藏,或者生成粒子祝福卡。桌面上的 2*2 万能卡片也会显示最近节日,点击后直接拉起对应页面。

元服务首页效果

这些功能看起来分散,底层其实围绕两件事展开:一套可离线运行的节日日期计算,以及一份统一维护的节日内容数据。页面、卡片和祝福语功能都建立在这两层之上。

开发助手在这个项目里做了什么

我使用的是集成在 DevEco Studio 中的 HarmonyOS 开发助手。在这个项目里,它主要帮我搭页面和模块骨架、根据编译日志定位问题,以及在真机验证后做局部修改。代码生成只是其中一环,真正有价值的是它能持续读取工程上下文,不必每次都从头解释目录和模块关系。

安装和能力说明可以直接查看华为的安装文档与开发助手介绍。如果需要查看插件版本和兼容信息,可以再到 JetBrains Marketplace 确认。

在 DevEco Studio 中安装

在 macOS 上,打开 DevEco Studio 后进入:

DevEco Studio > Preferences > Plugins

搜索 HarmonyOS Dev Assistant,安装后重启 IDE。如果使用离线插件包,则在插件设置中选择从磁盘安装。

首次打开助手时,需要使用已实名认证的华为开发者账号登录,并根据提示完成相关协议确认。之后先配置 AI 模型,再开始生成代码。助手支持通过供应商添加模型,也支持通过 URL、API Key 和模型名称手动配置。

配置完成后建议先执行一次模型测试。模型没有测试通过时就直接开始生成工程,后面遇到的问题很难判断究竟来自提示词、模型配置,还是工程本身。

我给助手划了一条边界

“做一个节日元服务”并不是一条合格的开发指令。它没有说明农历日期怎么处理,也没有交代除夕跨年、节日数据结构和万能卡片的限制。助手可以很快生成一个看起来完整的页面,但这种完整往往只停留在界面层。

所以我的分工很明确:助手负责搭骨架、读日志和改代码,我负责定义需求、判断方案并验收结果。这个边界一旦模糊,后面就会花更多时间修补看似能用、实际经不起验证的实现。

先把需求写成助手能读懂的物料

如果直接从一句需求开始生成,最常见的结果是功能边界飘忽不定:不同节日各写一套页面,规划能力被当成已实现能力,页面结构也会随着提示词不断变化。

我先写了一份项目规划书,把项目定位、MVP 范围、节日数据模型、页面路由和工程目录放在一起,同时写清 HarmonyOS Kit 与 API 约束、万能卡片配置、验收标准以及能力不可用时的降级方案。规划书不需要追求篇幅,真正重要的是让助手回答三个问题:要创建哪些文件,每个文件负责什么,这一版明确不做什么。

物料目录怎么组织

在项目的 entry/src/main/ets 目录下,我新建了 物料 文件夹,放入规划书、节日数据参考和提示词资料:

entry/src/main/ets/物料/
├── 岁时中国节_元服务项目规划书.md
├── Festival参考.md
├── 岁时中国节_App图标提示词.md
└── 待补充物料.md

我把这些物料直接放进工程,助手读取上下文时就能一起看到。需求有变化时,规划、代码和素材也在同一个地方,不必来回翻找散落的文档。

项目没有采用“一个节日一个页面”的写法。所有节日信息统一放在 model/festivals.ets,页面根据节日 ID 读取数据。以后新增节日时主要补充数据,不需要再复制一套页面逻辑。

让助手分阶段开发

物料准备完成后,我给助手的第一条提示词是:

需求如下:该元服务项目的项目说明文档以及相关的参考格式及内容已经引入。
请根据项目规划书和物料中的其他文件,对当前项目进行开发。
请先读取相关物料,输出开发计划,确认目录结构后再分阶段修改代码。

我没有让助手一次性生成所有功能,而是按下面的顺序推进:

阶段主要内容验收重点
数据层Festival 接口、节日数据、祝福语分类类型正确,节日信息可查询
核心页面首页、详情页、祝福语列表页面能打开,路由和数据绑定正常
日期能力农历转公历、最近节日、倒计时春节、端午、中秋等日期抽样核对
元服务入口FormExtensionAbility、桌面卡片卡片能刷新,点击能跳转
交互增强收藏、复制、粒子祝福卡、深链真机操作完整,失败有降级
发布准备签名、构建、AGC 信息构建成功,发布资料完整

助手生成计划后,我会逐项检查待办内容。涉及文件创建、删除、重命名和系统能力调用时,先确认计划,再让它执行,能够明显减少后续返工。

关键实现一:离线农历与节日倒计时

节日提醒是这个项目的核心功能。工程没有依赖网络请求,而是在 adapters/LunarCalendar.ets 中内置 1900—2100 年的农历信息表,提供农历转公历、公历转农历和日期差计算。

节日服务再基于这个日期引擎计算最近节日:

export interface FestivalCountdown {
  festival: Festival;
  solarDate: Date;
  daysLeft: number;
}

static getNearest(from: Date): FestivalCountdown {
  let nearest: FestivalCountdown | null = null;
  for (const f of FESTIVALS) {
    const d: Date = FestivalService.getNextSolarDate(f, from);
    const left: number = LunarCalendar.daysBetween(from, d);
    if (nearest === null || left < nearest.daysLeft) {
      nearest = { festival: f, solarDate: d, daysLeft: left };
    }
  }
  const fallback: Festival = FESTIVALS[0];
  const fallbackDate: Date = FestivalService.getNextSolarDate(fallback, from);
  return nearest ?? { festival: fallback, solarDate: fallbackDate, daysLeft: 0 };
}

这段计算真正麻烦的地方在跨年。某个节日如果今年已经过去,目标日期必须切换到下一年,否则首页会出现负数倒计时。除夕还要单独处理,因为它并不固定对应某个农历日期,工程里按下一年春节的前一天反推。

首页加载时会同时计算最近节日、今天的节日、未来 60 天节日和全年节日列表。页面只消费结果,不直接碰农历算法。后续即使替换日期实现,受影响的也主要是服务层。

关键实现二:数据驱动的详情页

节日数据统一使用 Festival 接口描述,字段包括节日 ID、农历或公历日期、主题色、简介、由来、习俗、诗词和祝福语。

当前工程的目录结构如下:

entry/src/main/ets/
├── adapters/
│   └── LunarCalendar.ets
├── common/
│   ├── AppConfig.ets
│   ├── PreferencesUtil.ets
│   └── viewmodels/
├── components/
│   ├── FestivalCard.ets
│   ├── BlessingList.ets
│   └── ParticleBlessing.ets
├── entryability/
│   └── EntryAbility.ets
├── entryformability/
│   └── EntryFormAbility.ets
├── features/
│   ├── blessing/BlessingService.ets
│   ├── festival/FestivalService.ets
│   └── share/ShareService.ets
├── form/
│   └── FestivalFormCard.ets
├── model/
│   ├── Festival.ets
│   └── festivals.ets
├── pages/
│   ├── Index.ets
│   └── BlessingCard.ets
└── 物料/

首页 Index.ets 只负责组合导航、列表和详情入口;FestivalService 负责日期和节日查询;BlessingService 负责分类、随机抽取和收藏;PreferencesUtil 负责本地持久化。这种分层比把所有逻辑写在页面的 build() 方法里更容易调试。

祝福语筛选逻辑很简单,但它体现了数据驱动页面的好处:

static filter(f: Festival, category: BlessingCategory): Blessing[] {
  if (category === 'general') {
    return f.blessings;
  }
  return f.blessings.filter((b: Blessing): boolean => b.category === category);
}

页面不需要知道每个节日有哪些祝福语,只需要把当前节日和分类传给服务层即可。

关键实现三:万能卡片与元服务入口

元服务的 module.json5 中开启了免安装能力,并注册 EntryFormAbility:

{
  "module": {
    "type": "entry",
    "mainElement": "EntryAbility",
    "installationFree": true,
    "extensionAbilities": [
      {
        "name": "EntryFormAbility",
        "srcEntry": "./ets/entryformability/EntryFormAbility.ets",
        "type": "form",
        "exported": true,
        "metadata": [
          {
            "name": "ohos.extension.form",
            "resource": "$profile:form_config"
          }
        ]
      }
    ]
  }
}

EntryFormAbility 每次生成卡片数据时,都会调用 FestivalService.getNearest(),因此卡片显示的是当前最近节日,而不是写死的内容:

private buildData(): FormCardData {
  const now: Date = new Date();
  const nearest = FestivalService.getNearest(now);
  const dateText =
    (nearest.solarDate.getMonth() + 1) + '月' + nearest.solarDate.getDate() + '日';
  const countdownText = nearest.daysLeft === 0
    ? '就是今天'
    : '还有 ' + nearest.daysLeft + ' 天';

  return {
    formName: nearest.festival.name,
    formDate: dateText,
    formCountdown: countdownText,
    formSummary: nearest.festival.summary,
    formPrimary: nearest.festival.theme.primary,
    formSecondary: nearest.festival.theme.secondary,
    formAction: ShareService.buildDeepLink(nearest.festival.id, '')
  };
}

卡片 UI 使用基础组件和主题色渐变,没有把 Canvas 粒子动画等复杂内容放进卡片。卡片的职责是快速展示和拉起元服务,复杂交互仍然放在主页面中,这也是元服务卡片开发时需要遵守的边界。

当前卡片配置为 2*2,每天 09:00 更新:

{
  "name": "festivalCard",
  "src": "./ets/form/FestivalFormCard.ets",
  "uiSyntax": "arkts",
  "updateEnabled": true,
  "scheduledUpdateTime": "09:00",
  "defaultDimension": "2*2",
  "supportDimensions": ["2*2"]
}

关键实现四:深链与分享降级

当前版本没有把系统分享能力当作必选前提,而是统一生成 festival:// 深链。用户复制祝福语时,可以同时复制祝福内容和深链;对端打开深链后,由 EntryAbility.onNewWant() 解析节日 ID,再交给页面消费。

深链格式示例:

festival://festival?id=zhongqiu&blessing=%E4%B8%AD%E7%A7%8B%E5%AE%89%E5%BA%B7

生成深链的代码如下:

static buildDeepLink(festivalId: string, blessing: string): string {
  if (blessing.length === 0) {
    return AppConfig.FES_DEEP_LINK_PREFIX + festivalId;
  }
  return AppConfig.FES_DEEP_LINK_PREFIX + festivalId
    + '&blessing=' + encodeURIComponent(blessing);
}

祝福语写进 URI 前必须经过 encodeURIComponent,否则空格、中文和特殊字符都可能破坏链接。接收端也不能默认外部 URI 永远正确,解析失败时直接返回空结果,至少不要让一条错误链接把页面带进白屏。

这套方案并不复杂,但降级效果很实际:即使设备、系统版本或相关权益不满足,核心的“复制祝福语”和“打开节日详情”仍然可用。

真机报错与修复方法

代码生成完成后,我先在 DevEco Studio 中构建,再连接真机运行。这个步骤不能省略,因为元服务卡片、签名、深链和部分系统能力在模拟环境中的表现可能与真机不同。

遇到问题时,我给助手的提示词不是简单地说“帮我修复”,而是把错误信息、触发步骤和期望结果一起提供:

当前工程在真机运行时出现以下问题:
1. 触发路径:从桌面卡片点击进入详情页。
2. 实际现象:页面未打开/出现错误提示。
3. 日志或编译错误如下:
<粘贴完整错误信息>

请先定位根因,再只修改必要文件。修改后说明:
- 修改了哪些文件;
- 为什么这样修改;
- 如何在真机上复现和验证。

让助手修复代码后,仍然要重新构建并走完整操作路径。编译通过只能说明语法和类型基本正确,不能代替功能验收。

打包与上架

功能确认后,可以自己完成打包,也可以让开发助手辅助执行构建和发布准备。无论使用哪种方式,签名和 AGC 配置都需要开发者自己确认。

上架前我检查了以下内容:

  1. AGC 项目、元服务名称和包名对应一致。
  2. 调试签名与发布签名没有混用。
  3. installationFree、卡片扩展能力和深链配置符合工程实际。
  4. 元服务图标、简介、隐私声明和截图资料完整。
  5. 构建产物在真机上重新安装或体验过。
  6. 卡片点击、详情页跳转、复制、收藏和祝福卡操作均可用。
  7. 关闭网络后,核心节日查询功能仍能正常工作。

签名配置完成并且项目验证通过后,再生成发布版本并上传 AGC。不要把签名信息直接写进提示词或公开文章,模型只需要知道工程中哪些配置需要检查,不需要接触敏感凭据。

AGC 配置

打包发布

审核结果

「岁时·中国节」首次提交后,我在一天左右收到了审核通过的邮件。

审核通过

审核速度会受到资料完整度、应用类型和平台审核节奏影响,这个时间只能作为本次项目的记录,不能当作固定承诺。

写在最后

这个项目做完后,我对开发助手的判断反而更具体了。物料越清楚,生成结果越容易收敛;开发过程拆得越细,出了问题越容易定位。先跑通数据和页面,再接卡片、深链和交互,比一开始就追求“全部生成”稳得多。

还有一点不能混淆:规划书里写过的能力,不等于工程里已经落地。规划书怎么写、发布前怎么验收,都应该回到实际代码和真机结果上。哪些功能已经可用,哪些做了降级,需要逐项确认。

HarmonyOS 开发助手很适合做开发过程中的协作工具。它能加快代码生成、问题定位和重复修改,但架构怎么取舍、API 是否用对、真机是否跑通,最后仍然是开发者的责任。

Logo

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

更多推荐