
本篇深入讲解 ImportantDays 项目中最核心的 UI 组件——DayCard。它展示单个重要日的完整信息包括标题、描述、日期、倒数天数、颜色标识和置顶标记。一、DayCard 设计目标DayCard 是重要日列表中每一条记录的卡片式展示设计目标信息密度适中一眼看到标题和倒数天数次要信息不抢眼视觉层级清晰标题 倒数 日期 描述颜色标识左侧色条快速区分不同重要日状态反馈今天的日期特殊处理响应式更新数据变化时自动刷新二、卡片布局设计┌─────────────────────────────────────────┐ │ ╆──────────────────────────────────────┐│ │ │ ││ │ │ 妈妈生日 30 ││ │ │ 记得买蛋糕 天后 ││ │ │ 2027年3月15日 每年 ││ │ │ ││ │ └──────────────────────────────────────┘│ └─────────────────────────────────────────┘ ↑ 色条6vp宽圆角对应颜色索引三、完整代码分析3.1 组件定义// components/DayCard.etsComponentV2exportstruct DayCard{Paramday:ImportantDaynewImportantDay(,,);privatevm:MainViewModelMainViewModel.instance;LocalcountValue:number0;LocalcountSuffix:string;LocalisToday:booleanfalse;LocaleffectiveDate:string;LocalrepeatLabel:string;aboutToAppear():void{this.refreshData();}Monitor(day.date,day.countMode,day.repeatType)onDayChange():void{this.refreshData();}privaterefreshData():void{constcountthis.vm.getCountText(this.day);this.countValuecount.value;this.countSuffixcount.suffix;this.isTodaycount.isToday;this.effectiveDatethis.vm.getEffectiveDate(this.day);if(this.day.repeatTypeRepeatType.YEARLY){this.repeatLabel 每年;}elseif(this.day.repeatTypeRepeatType.MONTHLY){this.repeatLabel 每月;}else{this.repeatLabel;}}// ...}3.2 Param 接收数据Paramday:ImportantDaynewImportantDay(,,);父组件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 字体层级元素字号颜色粗细标题16fpfont_primary (#E6000000)Medium描述12fpfont_secondary (#99000000)Normal日期12fpfont_tertiary (#66000000)Normal倒数数字32fp色条颜色Bold倒数后缀12fpfont_secondary (#99000000)Normal7.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管理派生状态实现了完全响应式的卡片展示。左侧色条 右侧大号倒数的布局设计在有限空间内传递了丰富的信息。