卡券管理全量代码与效果:ArkTS 在 HarmonyOS 的电子券包


实例:卡券管理(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;
}
}
三、注册与运行
main_pages.json追加"pages/samples/CouponPage";Index.ets追加:Button('🎟️ 18 卡券管理').fontSize(15).width('70%') .onClick(() => this.getUIContext().getRouter().pushUrl({ url: 'pages/samples/CouponPage' }))- 构建验证 →
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 状态流转 + 幂等)、状态分组排序、撕边券面的零资源视觉(渐变 + 底色圆)。这是「有效期 + 状态」类应用的完整范式(会员、礼券、订阅同构)。
更多推荐



所有评论(0)