本篇深入讲解 ImportantDays 项目中最核心的 UI 组件——DayCard。它展示单个重要日的完整信息,包括标题、描述、日期、倒数天数、颜色标识和置顶标记。
一、DayCard 设计目标
DayCard 是重要日列表中每一条记录的卡片式展示,设计目标:
- 信息密度适中:一眼看到标题和倒数天数,次要信息不抢眼
- 视觉层级清晰:标题 > 倒数 > 日期 > 描述
- 颜色标识:左侧色条快速区分不同重要日
- 状态反馈:今天的日期特殊处理(🎉)
- 响应式更新:数据变化时自动刷新
二、卡片布局设计
┌─────────────────────────────────────────┐ │ ╆──────────────────────────────────────┐│ │ │ ││ │ │ 妈妈生日 30 ││ │ │ 记得买蛋糕 天后 ││ │ │ 2027年3月15日 每年 ││ │ │ ││ │ └──────────────────────────────────────┘│ └─────────────────────────────────────────┘ ↑ 色条(6vp宽,圆角,对应颜色索引)三、完整代码分析
3.1 组件定义
// components/DayCard.ets@ComponentV2exportstruct DayCard{@Paramday:ImportantDay=newImportantDay('','','');privatevm:MainViewModel=MainViewModel.instance;@LocalcountValue:number=0;@LocalcountSuffix:string='';@LocalisToday:boolean=false;@LocaleffectiveDate:string='';@LocalrepeatLabel:string='';aboutToAppear():void{this.refreshData();}@Monitor('day.date','day.countMode','day.repeatType')onDayChange():void{this.refreshData();}privaterefreshData():void{constcount=this.vm.getCountText(this.day);this.countValue=count.value;this.countSuffix=count.suffix;this.isToday=count.isToday;this.effectiveDate=this.vm.getEffectiveDate(this.day);if(this.day.repeatType===RepeatType.YEARLY){this.repeatLabel=' 每年';}elseif(this.day.repeatType===RepeatType.MONTHLY){this.repeatLabel=' 每月';}else{this.repeatLabel='';}}// ...}3.2 @Param 接收数据
@Paramday:ImportantDay=newImportantDay('','','');父组件ImportantDayListPage传递ImportantDay对象:
DayCard({day:day})@Param确保当父组件的数据变化时,DayCard 自动更新。
3.3 @Monitor 精确监听
@Monitor('day.date','day.countMode','day.repeatType')onDayChange():void{this.refreshData();}只监听影响倒数计算的三个属性:
day.date:日期变化需要重新计算day.countMode:计数模式变化(倒数/正数)day.repeatType:重复类型变化影响有效日期
其他属性(如title、description)变化不需要重新计算倒数,所以不监听,减少不必要的刷新。
四、UI 构建详解
4.1 整体结构
build(){Column(){Row(){// 左侧色条Column().width(6).height('100%').backgroundColor(AppConstants.COLOR_PALETTE[this.day.colorIndex%AppConstants.COLOR_PALETTE.length]).borderRadius(3).margin({right:12});// 中间内容区Column(){// 标题Text(this.day.title).fontSize($r('app.string.font_size_16')).fontColor($r('app.color.font_primary')).fontWeight(FontWeight.Medium).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis}).width('100%');// 描述(可选)if(this.day.description){Text(this.day.description).fontSize($r('app.string.font_size_12')).fontColor($r('app.color.font_secondary')).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis}).width('100%').margin({top:2});}// 日期和标签Row(){Text(DateUtil.formatDateChinese(this.effectiveDate)).fontSize($r('app.string.font_size_12')).fontColor($r('app.color.font_tertiary'));if(this.repeatLabel){Text(this.repeatLabel).fontSize($r('app.string.font_size_12')).fontColor($r('app.color.font_tertiary')).margin({left:6});}if(this.day.isPinned){Text('📌').fontSize($r('app.string.font_size_12')).margin({left:6});}}.width('100%').margin({top:4});}.layoutWeight(1).alignItems(HorizontalAlign.Start);// 右侧倒数显示Column(){if(this.isToday){Text('🎉').fontSize(32);Text('今天').fontSize($r('app.string.font_size_12')).fontColor(AppConstants.COLOR_PALETTE[this.day.colorIndex%AppConstants.COLOR_PALETTE.length]).fontWeight(FontWeight.Bold);}else{Text(`${this.countValue}`).fontSize($r('app.string.font_size_32')).fontColor(AppConstants.COLOR_PALETTE[this.day.colorIndex%AppConstants.COLOR_PALETTE.length]).fontWeight(FontWeight.Bold);Text(this.countSuffix).fontSize($r('app.string.font_size_12')).fontColor($r('app.color.font_secondary'));}}.alignItems(HorizontalAlign.Center).width(80);}.width('100%').padding(16).backgroundColor($r('app.color.card_background')).borderRadius(16);}.width('100%');}4.2 左侧色条
Column().width(6).height('100%').backgroundColor(AppConstants.COLOR_PALETTE[this.day.colorIndex%AppConstants.COLOR_PALETTE.length]).borderRadius(3).margin({right:12});色条设计:
- 宽度 6vp,圆角 3vp(半圆效果)
- 颜色从 16 色调色板中按
colorIndex选取 - 使用
% COLOR_PALETTE.length防止索引越界 height('100%')确保与卡片等高
4.3 文本溢出处理
Text(this.day.title).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis})标题限制一行,超出部分显示省略号(...)。这确保长标题不会破坏卡片布局。
4.4 倒数显示区域
// 今天的情况if(this.isToday){Text('🎉').fontSize(32);Text('今天').fontSize($r('app.string.font_size_12')).fontColor(AppConstants.COLOR_PALETTE[...]).fontWeight(FontWeight.Bold);}// 非今天的情况else{Text(`${this.countValue}`)// 大号数字.fontSize($r('app.string.font_size_32')).fontColor(AppConstants.COLOR_PALETTE[...]).fontWeight(FontWeight.Bold);Text(this.countSuffix)// 后缀(天后/天前/天).fontSize($r('app.string.font_size_12')).fontColor($r('app.color.font_secondary'));}右侧区域宽度固定 80vp,内容居中对齐:
- 今天:显示 🎉 emoji + "今天"文字
- 非今天:大号数字(32fp)+ 小号后缀(12fp)
数字颜色使用与色条相同的颜色,形成视觉关联。
4.5 条件渲染标签
Row(){// 日期(始终显示)Text(DateUtil.formatDateChinese(this.effectiveDate)).fontSize($r('app.string.font_size_12')).fontColor($r('app.color.font_tertiary'));// 重复标签(有重复时显示)if(this.repeatLabel){Text(this.repeatLabel).margin({left:6});}// 置顶标记(置顶时显示)if(this.day.isPinned){Text('📌').margin({left:6});}}底部信息行按需显示标签,保持简洁。
五、16 色调色板
// common/Constants.etspublicstaticreadonlyCOLOR_PALETTE:string[]=['#FF6B6B',// 珊瑚红'#FF8E53',// 橙红'#FFA94D',// 橙色'#FFD43B',// 金黄'#A9E34B',// 黄绿'#51CF66',// 翠绿'#20C997',// 青绿'#22B8CF',// 青色'#339AF0',// 天蓝'#5C7CFA',// 蓝紫'#845EF7',// 紫色'#CC5DE8',// 品红'#F06595',// 粉红'#FF6B6B',// 珊瑚红(重复)'#868E96',// 灰色'#495057',// 深灰];调色板设计特点:
- 16 种颜色覆盖红、橙、黄、绿、青、蓝、紫、粉、灰
- 按色环排列,相邻颜色有渐变关系
- 适合用
Grid8列布局展示
六、数据刷新机制
数据变化流程: 1. 用户在编辑页修改重要日 └── vm.updateDay(day) └── dayList[index] = day └── @Trace dayList 触发 └── ImportantDayListPage @Monitor('vm.dayList') └── refreshList() └── sortedDays = vm.getSortedDays() └── ForEach 重新渲染 └── DayCard({ day: newDay }) └── @Param 更新 day └── @Monitor('day.date', ...) └── refreshData() └── 更新 countValue 等 @Local └── UI 自动刷新七、样式细节
7.1 卡片外观
.padding(16)// 16vp 内边距.backgroundColor($r('app.color.card_background'))// 白色背景.borderRadius(16)// 16vp 圆角7.2 字体层级
| 元素 | 字号 | 颜色 | 粗细 |
|---|---|---|---|
| 标题 | 16fp | font_primary (#E6000000) | Medium |
| 描述 | 12fp | font_secondary (#99000000) | Normal |
| 日期 | 12fp | font_tertiary (#66000000) | Normal |
| 倒数数字 | 32fp | 色条颜色 | Bold |
| 倒数后缀 | 12fp | font_secondary (#99000000) | Normal |
7.3 间距设计
.margin({right:12})// 色条到内容的间距.margin({top:2})// 标题到描述的间距.margin({top:4})// 描述到日期行的间距.margin({left:6})// 标签之间的间距八、性能优化
8.1 精确的 @Monitor
只监听影响显示的属性,避免day.description变化时也触发refreshData()。
8.2 固定宽度的右侧区域
.width(80)// 固定宽度固定宽度避免了不同卡片之间因数字位数不同导致的布局抖动。
8.3 maxLines 限制
.maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis})限制标题和描述为一行,确保卡片高度一致,列表滚动更流畅。
九、小结
DayCard 是 ImportantDays 项目中最核心的展示组件。通过@Param接收数据、@Monitor精确监听变化、@Local管理派生状态,实现了完全响应式的卡片展示。左侧色条 + 右侧大号倒数的布局设计,在有限空间内传递了丰富的信息。