收纳格卡片风:ArkUI 让鸿蒙物品清单像收纳盒贴标


实例:家庭物品清单(Household)|风格:收纳格卡片
一、设计理念:物品清单要「像给收纳盒贴标签」
家庭物品清单的 UI 目标是收纳盒/库存管理——每件物品一张「收纳格卡片」:emoji 图标 + 名称 + 分类·位置 + 状态(在家/借出)。实例 20 的风格「收纳格卡片」:分类筛选条(橙色主题)+ 物品卡(暖黄描边)+ 借用记录 + 借还登记。
页面信息架构:
- 标题栏:家庭物品清单 + 总数/借出数 + 刷新;
- 分类筛选:全部/厨房/家用/工具/数码/书籍/运动;
- 状态切换:在家/借出中(toggle);
- 物品列表:收纳格卡片(emoji + 名称 + 分类·位置 + 状态标签 + 借出/删除);
- 借用记录:最近 5 条借还流水(物品/借用人/时间/归还);
- 借出登记弹窗:借用人 + 备注。
二、页面骨架与核心状态
@State items: HouseholdItem[] = [];
@State borrows: BorrowRecord[] = [];
@State overview: HouseholdOverview = { total: 0, borrowed: 0, atHome: 0 };
@State categoryFilter: string = '全部';
@State statusFilter: number = -1;
@State borrowVisible: boolean = false;
@State borrowItem: HouseholdItem | null = null;
@State borrower: string = '';
@State note: string = '';
private readonly categories: string[] = ['厨房', '家用', '工具', '数码', '书籍', '运动'];
核心状态:items(物品列表)、borrows(借用记录)、overview(统计)、categoryFilter/statusFilter(双筛选)、borrowVisible/borrowItem/borrower/note(借出弹窗)。
build 骨架(无 Scroll 包裹——List 占主区 + 底部借用记录固定):
Column() {
// 标题栏
// 分类筛选
// 状态切换
// 物品列表(List)
// 借用记录(最近 5 条)
// 借出登记弹窗
}
三、分类筛选 + 状态切换:橙色主题
分类筛选(选中橙色——本实例主色):
Scroll() {
Row({ space: 8 }) {
ForEach(['全部'].concat(this.categories), (cat: string) => {
Text(cat)
.fontSize(13).padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.categoryFilter === cat ? '#F59E0B' : '#FFFFFF')
.fontColor(this.categoryFilter === cat ? Color.White : '#4B5563')
.onClick(() => this.switchCategory(cat))
}, (cat: string) => cat)
}.padding({ left: 16, right: 16, top: 10 })
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('100%')
状态切换(在家/借出中 toggle):
Row({ space: 8 }) {
Text(this.statusFilter === -1 ? '📍 全部状态' : '📍 在家')
.fontSize(12).padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(12).backgroundColor('#FFFFFF').fontColor('#6B7280')
.onClick(() => this.switchStatus(0))
Text('📤 借出中')
.fontSize(12).padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(12).backgroundColor(this.statusFilter === 1 ? '#FEE2E2' : '#FFFFFF')
.fontColor(this.statusFilter === 1 ? '#DC2626' : '#6B7280')
.onClick(() => this.switchStatus(1))
}.width('94%').margin({ top: 8 })
toggle 语义:点「在家」→ statusFilter=0(筛选在家)→ 再点 → -1(全部状态)——toggle 取消(与影音状态筛选同模式);「借出中」选中红底(借出是警示色)。
switchStatus:
async switchStatus(s: number): Promise<void> {
this.statusFilter = this.statusFilter === s ? -1 : s;
await this.applyFilter();
}
「再点取消」的 toggle 逻辑:statusFilter === s ? -1 : s。
四、物品收纳格卡片:核心视觉
每件物品一张「收纳格卡」:
List({ space: 10 }) {
ForEach(this.items, (item: HouseholdItem) => {
ListItem() {
Row({ space: 12 }) {
Text(item.emoji).fontSize(30)
.width(52).height(52).borderRadius(12)
.backgroundColor('#FFF7E6').textAlign(TextAlign.Center)
Column({ space: 3 }) {
Row({ space: 6 }) {
Text(item.name).fontSize(15).fontWeight(FontWeight.Bold)
Text(item.status === 1 ? '借出中' : '在家')
.fontSize(10).padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor(item.status === 1 ? '#FEE2E2' : '#E5F6EC')
.fontColor(item.status === 1 ? '#DC2626' : '#059669')
}
Text(`${item.category} · ${item.location}`).fontSize(11).fontColor('#9CA3AF')
if (item.note) {
Text(`📌 ${item.note}`).fontSize(11).fontColor('#B45309')
}
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
if (item.status === 0) {
Button('借出').height(30).fontSize(12).backgroundColor('#F59E0B')
.onClick(() => this.openBorrow(item))
}
Text('✕').fontSize(14).fontColor('#CCCCCC').onClick(() => this.deleteItem(item))
}
.width('100%').padding(12)
.backgroundColor(Color.White).borderRadius(10)
.border({ width: 1, color: '#FDE68A' })
}
}, (item: HouseholdItem) => `${item.id}-${item.name}`)
}
.width('94%').layoutWeight(1).margin({ top: 10 })
.scrollBar(BarState.Off)
收纳格卡五要素:
- emoji 图标:30fp 放 52×52 暖黄圆角块(
#FFF7E6)——收纳格图标位; - 名称 + 状态标签:在家绿 / 借出中红——状态可扫读;
- 分类 · 位置:
厨房 · 厨房橱柜灰字——「分类 + 收纳位置」双元信息(找物品的关键); - 备注:
📌 借给王阿姨棕色字(有备注才显示); - 操作:在家 → 「借出」橙色按钮(借出中不显示)+ ✕ 删除。
暖黄描边:border({ width: 1, color: '#FDE68A' })——「收纳格贴标」的暖黄色调贯穿(背景 #FFFBEB 米黄 + 图标块 #FFF7E6 + 描边 #FDE68A)。
五、借用记录:最近 5 条流水
列表下方「借用记录」(最近 5 条 + 归还按钮):
Column() {
Text('借用记录').fontSize(15).fontWeight(FontWeight.Bold)
ForEach(this.borrows.slice(0, 5), (b: BorrowRecord) => {
Row({ space: 8 }) {
Text(b.itemName).fontSize(13).fontWeight(FontWeight.Medium).layoutWeight(1)
Text(`${b.borrower} ${this.fmtDate(b.borrowTime)}`)
.fontSize(11).fontColor('#9CA3AF')
if (b.returnTime === 0) {
Text('归还').fontSize(11).padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(10).backgroundColor('#E5F6EC').fontColor('#059669')
.onClick(() => this.returnBorrow(b))
} else {
Text('已还').fontSize(11).fontColor('#9CA3AF')
}
}
.width('100%').padding({ top: 6, bottom: 6 })
}, (b: BorrowRecord) => `${b.id}-${b.itemId}`)
}
.width('94%').padding(12).backgroundColor(Color.White).borderRadius(12)
.margin({ top: 4, bottom: 20 })
.alignItems(HorizontalAlign.Start)
流水行三要素:物品名(来自 LEFT JOIN 的 itemName)+ 借用人·日期 + 归还按钮/已还状态——returnTime === 0 显示绿色「归还」按钮(点击 returnBorrow)、否则灰色「已还」。
slice(0, 5) 截取:只显示最近 5 条——「列表限制条数」(完整历史在真实应用分页查看)。
returnBorrow:
returnBorrow(b: BorrowRecord): void {
if (b.returnTime !== 0) {
return;
}
HouseholdDao.returnItem(this.context, b.id, b.itemId).then(async () => {
await this.refresh();
promptAction.showToast({ message: '📥 已归还' });
});
}
归还守卫:returnTime !== 0 直接 return(已还的记录不处理)——「状态守卫」防御重复点击。
六、借出登记弹窗
if (this.borrowVisible) {
Column() {
Text(`📤 借出「${this.borrowItem?.name ?? ''}」`).fontSize(18).fontWeight(FontWeight.Bold)
TextInput({ placeholder: '借用人 *', text: this.borrower })
.margin({ top: 12 }).onChange((v: string) => this.borrower = v)
TextInput({ placeholder: '备注(可选)', text: this.note })
.margin({ top: 8 }).onChange((v: string) => this.note = v)
Row({ space: 8 }) {
Button('取消').layoutWeight(1).backgroundColor('#EEF2F7').fontColor('#555555')
.onClick(() => this.borrowVisible = false)
Button('确认借出').layoutWeight(1).backgroundColor('#F59E0B')
.onClick(() => this.doBorrow())
}.margin({ top: 16 })
}
.padding(20).borderRadius(16).backgroundColor(Color.White).width('88%')
.position({ x: '6%', y: '30%' })
}
弹窗标题带物品名:借出「破壁机」——用户确认借的是哪件。doBorrow 校验:!borrower.trim() || !borrowItem → Toast「请输入借用人」——借用人必填校验。
七、UI 风格要素一览
| 风格项 | 取值 | 说明 |
|---|---|---|
| 页面背景 | #FFFBEB 米黄 |
收纳暖色 |
| 主色 | #F59E0B 橙 |
分类选中/借出 |
| 卡片描边 | #FDE68A 暖黄 |
贴标感 |
| 图标块 | #FFF7E6 52×52 |
emoji 收纳格 |
| 状态标签 | 在家绿 / 借出红 | 状态可辨 |
| 借用记录 | 最近 5 条 + 归还按钮 | 借还闭环 |
八、文章小结
本篇文章完成了实例 20 的 UI 层:分类筛选(橙色主题)+ 状态 toggle + 物品收纳格卡(emoji 图标 + 分类·位置 + 状态标签 + 借出/删除)+ 借用记录(最近 5 条 + 归还按钮)+ 借出登记弹窗。设计核心是「收纳贴标感」——暖黄主题贯穿(米黄背景 + 暖黄描边 + 橙主色);技术亮点是状态驱动 UI(在家显示借出按钮/借出中不显示)与归还守卫(returnTime 判断)。物品清单像给收纳盒贴标签。
下一篇(20-3)深入数据层,讲解事务借还与 LEFT JOIN 联表——borrowItem/returnItem 是本实例的技术核心。
九、补充:暖黄主题与借还交互
9.1 暖黄色系统一配色规则
| 角色 | 色值 | 使用位置 |
|---|---|---|
| 页面背景 | #FFFBEB 米黄 |
整页底色 |
| 卡片描边 | #FDE68A 暖黄 |
物品卡 border |
| 主色 | #F59E0B 橙 |
分类选中/借出按钮 |
| 图标块 | #FFF7E6 浅暖黄 |
emoji 收纳格 |
| 警示红 | #DC2626 |
借出中标签/筛选 |
配色规则:背景最浅(米黄)→ 图标块次之 → 描边加深 → 主色最深——「同一色系、三级递进」:暖黄色调贯穿不刺眼,橙主色在所有暖黄元素中自然突出,红色仅用于「借出」警示,语义不混淆。
9.2 状态驱动的借出按钮显隐
if (item.status === 0) {
Button('借出').height(30).fontSize(12).backgroundColor('#F59E0B')
.onClick(() => this.openBorrow(item))
}
状态驱动:status === 0(在家)才渲染「借出」按钮,借出中的物品按钮直接消失——UI 由数据状态单向驱动,用户不会对已借出的物品重复操作,也无需二次判断。
9.3 借用记录的 slice 截取
ForEach(this.borrows.slice(0, 5), (b: BorrowRecord) => { ... })
slice(0, 5):渲染前截取前 5 条——「渲染层限流」,借用记录只会增长、不会缩短,若全量渲染列表会无限变长;完整历史由真实应用的分页或「查看全部」承载。
9.4 FAQ
Q1:为什么借出按钮用 if 而不是 disabled?
借出中的物品连按钮都不渲染(if 条件渲染),比 disabled 置灰更彻底——列表更干净,避免「可点但报错」的歧义;同时按钮消失本身就是「已借出」的强视觉暗示。
Q2:slice(0, 5) 取的是最近 5 条吗?
是——borrows 由 SQL ORDER BY borrow_time DESC 排序返回,最新记录在前,slice(0, 5) 截取的正是最近 5 条;若查询漏了排序,slice 取到的就不是「最近」了,这是排序与截取配合的关键。
更多推荐




所有评论(0)