前言
最近开始学习 HarmonyOS 鸿蒙应用开发,为了练手,我写了一个简单的“游戏每日签到”小 Demo。虽然功能简单,但涵盖了鸿蒙开发中核心的 ArkTS 语法、状态管理 (@State) 以及 UI 布局。本文将分享我的开发思路和核心代码,希望能给同样在入门鸿蒙的朋友一点参考。
🛠️ 开发环境
IDE:DevEco Studio (Next版本)
语言:ArkTS (基于 TypeScript)
SDK:HarmonyOS SDK 5.0+
✨ 功能展示
先来看一下最终的运行效果。界面采用深色背景风格,包含累计签到天数显示、签到按钮以及底部的统计信息。

项目运行预览效果如下:

1. 页面结构与状态定义
我们使用 @Entry 和 @Component 装饰器来定义页面入口。为了实现点击按钮后数字自动变化,我们需要使用 @State 装饰器来定义响应式变量。

 * ── ArkTS 状态装饰器说明(本文件及鸿蒙开发常用) ──
 *
 * @Entry  : 标记页面入口组件,每个页面有且仅有一个,系统通过它加载并渲染页面。
 * @Component : 标记自定义 UI 组件,配合 struct 使用,使结构体具备 UI 构建能力。
 *
 * @State  : 组件内部私有状态。变量变化时,仅刷新持有该状态的组件,驱动 UI 自动重绘。
 *           本页面用 @State 管理签到次数、今日签到状态、按钮缩放等 UI 相关数据。
 *
 * @Prop   : 父组件向子组件单向传递数据(父 → 子)。子组件修改 @Prop 不会影响父组件。
 *           适用于展示型子组件,如签到统计卡片接收 totalCount 仅用于显示。
 *
 * @Link   : 父组件与子组件双向绑定(父 ↔ 子)。子组件修改 @Link 变量会同步回父组件。
 *           适用于表单输入、开关等需要父子联动的场景。
 *
 * @Provide / @Consume : 跨多层级的祖先后代组件状态共享,无需逐层传递参数。
 *
 * @StorageLink : 与 AppStorage 双向绑定,修改会同步到全局 AppStorage 并通知所有关联组件。
 * @StorageProp  : 与 AppStorage 单向绑定,仅读取全局状态,本地修改不写回 AppStorage。
 *
 * @Watch : 监听 @State/@Prop/@Link 等变量的变化,变化时自动执行指定回调方法。
 *
 * 本页面采用 @State + Preferences 本地持久化方案,未使用 AppStorage,
 * 数据在 aboutToAppear 生命周期中从磁盘加载,签到成功后写回磁盘。
 */
// ═══════════════════════════════════════════════════════════
  //  区域一:状态定义
  // ═══════════════════════════════════════════════════════════

  /**
   * @State 累计签到总天数(同时也是累计总次数,每日限签一次)
   * 变化时自动触发 build() 重绘,更新顶部与底部的数字展示
   */
  @State totalCount: number = 0;

  /**
   * @State 今日是否已完成签到
   * true  → 按钮显示"今日已签到"并禁用
   * false → 按钮显示"立即签到"并可点击
   */
  @State hasCheckedInToday: boolean = false;

  /**
   * @State 签到按钮缩放比例,用于实现点击时的按压动效
   * 点击瞬间缩小至 0.92,松开后弹回 1.0,营造游戏按钮手感
   */
  @State buttonScale: number = 1.0;

  /**
   * @State 数据是否已从 Preferences 加载完毕
   * 加载完成前隐藏主内容,避免闪烁默认值
   */
  @State isDataLoaded: boolean = false;

  /** 上次签到日期(YYYY-MM-DD),不直接驱动 UI,无需 @State */
  private lastCheckInDate: string = '';

  /** UIAbility 上下文,用于 Preferences 读写,在 aboutToAppear 中初始化 */
  private uiContext: common.UIAbilityContext | null = null;

2. UI 布局构建
在 build() 方法中,我们使用了 Column 容器进行垂直布局。为了让界面好看,我设置了一个深色的背景颜色,并使用了 Stack 组件来叠加显示“今日已签到”的遮罩层。

build() {
    Stack({ alignContent: Alignment.Center }) {
      // ── 深色游戏风渐变背景层 ──
      Column()
        .width('100%')
        .height('100%')
        .linearGradient({
          angle: 160,
          colors: [
            ['#0f0c29', 0.0],
            ['#302b63', 0.45],
            ['#24243e', 1.0]
          ]
        })

      if (this.isDataLoaded) {
        Column() {
          // ── 顶部:累计签到总天数 ──
          this.buildTopSection()

          Blank()

          // ── 中间:大号游戏风签到按钮 ──
          this.buildCheckInButton()

          Blank()

          // ── 底部:累计总次数 ──
          this.buildBottomSection()
        }
        .width('100%')
        .height('100%')
        .padding({ left: 32, right: 32, top: 60, bottom: 60 })
      } else {
        // 数据加载中提示
        Text('加载中...')
          .fontSize(18)
          .fontColor('#8899BB')
      }
    }
    .width('100%')
    .height('100%')
  }

3. 交互逻辑
当用户点击“计算”或“签到”按钮时,我们需要更新状态。

// ═══════════════════════════════════════════════════════════
  //  区域三:业务逻辑
  // ═══════════════════════════════════════════════════════════

  /**
   * 页面即将显示时触发(生命周期钩子)
   * 在此初始化上下文并从 Preferences 加载历史签到数据
   */
  aboutToAppear(): void {
    this.uiContext = getContext(this) as common.UIAbilityContext;
    this.loadCheckInData();
  }

  /**
   * 从 Preferences 异步加载签到数据,并根据日期判断是否跨天
   */
  async loadCheckInData(): Promise<void> {
    if (this.uiContext === null) {
      return;
    }
    try {
      const data: CheckInData = await CheckInStorage.load(this.uiContext);
      this.totalCount = data.totalCount;
      this.lastCheckInDate = data.lastCheckInDate;

      const today: string = this.getTodayDateString();
      // 若上次签到日期等于今天,说明今日已签;若不等于(含空字符串),则允许签到
      this.hasCheckedInToday = (this.lastCheckInDate === today);
    } catch (err) {
      console.error('加载签到数据异常: ' + JSON.stringify(err));
    } finally {
      this.isDataLoaded = true;
    }
  }

  /**
   * 处理签到按钮点击:播放动效 → 校验状态 → 计数 +1 → 持久化
   */
  handleCheckInClick(): void {
    if (this.hasCheckedInToday) {
      return;
    }

    // 按压动效:先缩小再弹回,模拟游戏按钮点击反馈
    animateTo({ duration: 120, curve: Curve.EaseOut }, () => {
      this.buttonScale = 0.92;
    });
    animateTo({ duration: 120, delay: 120, curve: Curve.EaseIn }, () => {
      this.buttonScale = 1.0;
    });

    this.performCheckIn();
  }

  /**
   * 执行签到核心逻辑:更新状态并写入 Preferences
   */
  async performCheckIn(): Promise<void> {
    if (this.uiContext === null || this.hasCheckedInToday) {
      return;
    }

    const today: string = this.getTodayDateString();

    // 累计次数 +1(@State 赋值会自动刷新 UI)
    this.totalCount = this.totalCount + 1;
    this.lastCheckInDate = today;
    this.hasCheckedInToday = true;

    await CheckInStorage.save(this.uiContext, this.totalCount, this.lastCheckInDate);
  }

🚀 遇到的挑战与解决 (这部分可以让文章更真实,你可以简单写一两句)

问题:一开始不知道如何让点击按钮后数字自动变。 解决:查阅文档发现必须使用 @State 装饰器,普通的变量改变不会触发 UI 刷新。

 📝 总结

通过这个小小的签到 Demo,我初步掌握了鸿蒙 ArkTS 的声明式 UI 开发范式。接下来我计划加入数据持久化 (Preferences),这样即使关闭应用,签到记录也不会丢失。 如果你也对鸿蒙开发感兴趣,欢迎在评论区交流!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐