在这里插入图片描述
在这里插入图片描述

实例:卡券管理(Coupon)|收官文章

一、文件清单

文件 职责 行数
database/CouponDao.ets 数据层:有效期比较、批量过期、状态统计、10 张种子 约 217 行
pages/samples/CouponPage.ets UI 层:撕边券面列表 + 状态筛选 + 使用/长按删除 约 199 行
resources/base/profile/main_pages.json 路由注册:pages/samples/CouponPage 追加一行
pages/Index.ets 首页入口按钮 追加一个按钮

本篇文章完整展示可编译运行的 CouponPage 代码,最后描述运行效果。

二、CouponPage 完整代码

import { common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { CouponDao, Coupon } from '../../database/CouponDao';

@Entry
@Component
struct CouponPage {
  @State coupons: Coupon[] = [];
  @State stats: Record<string, number> = {};
  @State statusFilter: number = -1;
  private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
  private readonly statusNames: string[] = ['未使用', '已使用', '已过期'];
  private readonly statusColors: string[] = ['#059669', '#9CA3AF', '#D1D5DB'];

  aboutToAppear(): void {
    this.refresh();
  }

  async refresh(): Promise<void> {
    try {
      await CouponDao.initSeedData(this.context);
      // 先执行过期检查(把到期未用标记为已过期)
      await CouponDao.markExpired(this.context);
      this.stats = await CouponDao.statusStats(this.context);
      if (this.statusFilter === -1) {
        this.coupons = await CouponDao.queryAll(this.context);
      } else {
        this.coupons = await CouponDao.queryByStatus(this.context, this.statusFilter);
      }
    } catch (e) {
      promptAction.showToast({ message: `加载失败: ${e}` });
    }
  }

  async switchFilter(f: number): Promise<void> {
    this.statusFilter = f;
    await this.refresh();
  }

  useCoupon(c: Coupon): void {
    promptAction.showDialog({
      title: '使用卡券',
      message: `确认使用「${c.title}」吗?`,
      buttons: [
        { text: '取消', color: '#808080' },
        { text: '使用', color: '#059669' },
      ],
    }).then((res: promptAction.ShowDialogSuccessResponse) => {
      if (res.index === 1) {
        CouponDao.useCoupon(this.context, c.id).then(async () => {
          await this.refresh();
          promptAction.showToast({ message: '✅ 已使用' });
        });
      }
    });
  }

  deleteCoupon(c: Coupon): void {
    promptAction.showDialog({
      title: '删除卡券',
      message: `删除「${c.title}」?`,
      buttons: [
        { text: '取消', color: '#808080' },
        { text: '删除', color: '#EF4444' },
      ],
    }).then((res: promptAction.ShowDialogSuccessResponse) => {
      if (res.index === 1) {
        CouponDao.delete(this.context, c.id).then(async () => {
          await this.refresh();
          promptAction.showToast({ message: '🗑 已删除' });
        });
      }
    });
  }

  private fmtDate(ts: number): string {
    const d = new Date(ts);
    return `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}`;
  }

  private daysLeft(c: Coupon): number {
    return Math.ceil((c.expireTime - Date.now()) / 86400000);
  }

  build() {
    Column() {
      // ===== 标题栏 =====
      Row() {
        Column() {
          Text('🎟️ 卡券管理').fontSize(22).fontWeight(FontWeight.Bold)
          Text('有效期比较 · 批量过期检测').fontSize(11).fontColor('#999999').margin({ top: 2 })
        }.alignItems(HorizontalAlign.Start).layoutWeight(1)
        Text('↻').fontSize(22).onClick(() => this.refresh())
      }.width('100%').padding({ left: 16, right: 16, top: 12 })

      // ===== 状态筛选 =====
      Scroll() {
        Row({ space: 8 }) {
          Text(`全部 ${this.totalCount()}`)
            .fontSize(13).padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .borderRadius(16)
            .backgroundColor(this.statusFilter === -1 ? '#111827' : '#FFFFFF')
            .fontColor(this.statusFilter === -1 ? Color.White : '#4B5563')
            .onClick(() => this.switchFilter(-1))
          ForEach(this.statusNames, (name: string, idx: number) => {
            Text(`${name} ${this.stats[String(idx)] ?? 0}`)
              .fontSize(13).padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .borderRadius(16)
              .backgroundColor(this.statusFilter === idx ? this.statusColors[idx] : '#FFFFFF')
              .fontColor(this.statusFilter === idx ? Color.White : '#4B5563')
              .onClick(() => this.switchFilter(idx))
          }, (name: string, idx: number) => `${idx}-${name}`)
        }.padding({ left: 16, right: 16, top: 10 })
      }.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('100%')

      // ===== 卡券列表 =====
      List({ space: 12 }) {
        ForEach(this.coupons, (c: Coupon) => {
          ListItem() {
            // 撕边券面卡片
            Stack({ alignContent: Alignment.Center }) {
              Row().width('100%').height(96).borderRadius(12)
                .linearGradient({ angle: 135, colors: [[c.color, 0], [c.color + 'CC', 1]] })
              // 左右半圆撕边
              Row()
                .width(24).height(24).borderRadius(12).backgroundColor('#F8FAFC')
                .position({ x: -12, y: 36 })
              Row()
                .width(24).height(24).borderRadius(12).backgroundColor('#F8FAFC')
                .position({ x: 'calc(100% - 12px)', y: 36 })

              Row() {
                // 面值区
                Column() {
                  Text(`¥${c.faceValue}`)
                    .fontSize(30).fontWeight(FontWeight.Bold).fontColor(Color.White)
                  Text(c.minSpend > 0 ? `${c.minSpend} 可用` : '无门槛')
                    .fontSize(10).fontColor('rgba(255,255,255,0.85)').margin({ top: 2 })
                }.width(90)
                // 分割虚线
                Row().width(1).height(56).backgroundColor('rgba(255,255,255,0.5)')
                // 信息区
                Column({ space: 3 }) {
                  Text(c.title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(Color.White).maxLines(1)
                  Text(`${c.type} · 有效期至 ${this.fmtDate(c.expireTime)}`)
                    .fontSize(10).fontColor('rgba(255,255,255,0.85)')
                  if (c.status === 0) {
                    Text(`${this.daysLeft(c)}`).fontSize(10).padding({ left: 6, right: 6, top: 2, bottom: 2 })
                      .borderRadius(8).backgroundColor('rgba(255,255,255,0.25)').fontColor(Color.White)
                  }
                }.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 10 })
                // 状态 / 操作
                if (c.status === 0) {
                  Text('使用').fontSize(13).padding({ left: 14, right: 14, top: 6, bottom: 6 })
                    .borderRadius(14).backgroundColor(Color.White).fontColor(c.color)
                    .onClick(() => this.useCoupon(c))
                } else {
                  Text(this.statusNames[c.status])
                    .fontSize(12).padding({ left: 10, right: 10, top: 4, bottom: 4 })
                    .borderRadius(12).backgroundColor('rgba(255,255,255,0.3)').fontColor(Color.White)
                }
              }
              .width('100%').padding({ left: 14, right: 14 })
            }
            .width('100%').height(96)
            .onClick(() => this.showDetail(c))
            .gesture(
              LongPressGesture().onAction(() => this.deleteCoupon(c))
            )
          }
        }, (c: Coupon) => `${c.id}-${c.title}`)
      }
      .width('94%').layoutWeight(1).margin({ top: 12 })
      .scrollBar(BarState.Off)
    }
    .width('100%').height('100%').backgroundColor('#F8FAFC')
  }

  showDetail(c: Coupon): void {
    promptAction.showDialog({
      title: c.title,
      message: `类型:${c.type}\n面值:¥${c.faceValue}\n门槛:${c.minSpend > 0 ? `${c.minSpend} 可用` : '无门槛'}\n生效:${this.fmtDate(c.startTime)}\n过期:${this.fmtDate(c.expireTime)}\n状态:${this.statusNames[c.status]}\n备注:${c.remark}`,
      buttons: [{ text: '知道了', color: '#3B82F6' }],
    });
  }

  private totalCount(): number {
    let sum = 0;
    Object.keys(this.stats).forEach((k: string) => {
      sum += this.stats[k];
    });
    return sum;
  }
}

三、注册与运行

  1. main_pages.json 追加 "pages/samples/CouponPage"
  2. Index.ets 追加:
    Button('🎟️ 18 卡券管理').fontSize(15).width('70%')
      .onClick(() => this.getUIContext().getRouter().pushUrl({ url: 'pages/samples/CouponPage' }))
    
  3. 构建验证 → BUILD SUCCESSFUL

四、运行效果描述

进入「🎟️ 18 卡券管理」:

第一屏:标题栏「🎟️ 卡券管理 · 有效期比较 · 批量过期检测」;状态筛选条(全部 10 / 未使用 5 / 已使用 2 / 已过期 3);下方 10 张撕边券面——未使用 5 张(红/绿/蓝/橙/靛渐变券面 + ¥20/¥10/¥80/¥5/¥100 面值 + 「满 100 可用」/「无门槛」 + 「剩 20 天/15 天/30 天/7 天/60 天」标签 + 白色「使用」按钮),中间已使用 2 张(粉/紫 + 灰色「已使用」标签),底部已过期 3 张(灰「已过期」标签)。

交互一(使用卡券):点新人券「使用」→ 确认框 → 确认 → Toast「✅ 已使用」→ 新人券变「已使用」灰标签、筛选条「未使用 4 / 已使用 3」更新。

交互二(状态筛选):点「已过期」→ 3 张灰券 → 点「未使用」→ 4 张 → 点「全部」恢复。

交互三(点券详情):点年终回馈 100 券面 → 弹窗「礼品卡 / 面值 ¥100 / 无门槛 / 生效 2024.06.24 / 过期 2024.08.23 / 状态 未使用 / 备注 积分兑换」。

交互四(长按删除):长按「洗车 5 折券」→ 确认框 → 删除 → 列表 9 张、筛选条「全部 9 / 已过期 2」。

交互五(过期自动标记):把一张未使用券的过期时间改为昨天(改种子或数据库)→ 刷新 → 该券自动变「已过期」(markExpired 批量检测生效)。

五、代码质量要点回顾

关注点 本实例做法
批量过期 UPDATE WHERE 双条件
有效期筛选 expire_time > now
状态排序 status + expire_time ASC
撕边券面 底色圆 + calc 定位
渐变 色 + 透明后缀
手势 点按详情 + 长按删除
状态统计 GROUP BY + 预置默认键

六、文章小结

实例 18「卡券管理」收官。五篇文章覆盖:时间戳字段与状态机建模(18-1)→ 撕边券面 UI(18-2)→ 有效期比较与批量过期(18-3)→ 10 张券三态种子(18-4)→ 全量代码(18-5)。核心技术是 start/expire_time 时间戳(时刻级比较)、markExpired 批量条件更新(一条 SQL 状态流转 + 幂等)、状态分组排序、撕边券面的零资源视觉(渐变 + 底色圆)。这是「有效期 + 状态」类应用的完整范式(会员、礼券、订阅同构)。

Logo

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

更多推荐