本文深入解析 ImportantDays 项目中最复杂的 UI 组件
MonthWeekCalendar,包括其组件参数设计、月视图网格渲染、日期项的选中/今天/重要日标记逻辑、周视图渲染、无限滚动的联动处理,以及日期文字颜色的多级优先策略。
一、组件定位
MonthWeekCalendar是日历模块的核心 UI 组件,负责:
- 渲染月视图(42 格网格)和周视图(7 天横排)
- 标记今天、选中日期
- 在日期上显示重要日颜色圆点和标题预览
- 处理日期点击事件
- 联动 Swiper 无限滚动
二、组件参数设计
@ComponentV2exportstruct MonthWeekCalendar{@Paramvm:CalendarVM=CalendarVM.getInstance();@ParamonChangeDay:(date:Date)=>void=()=>{};@ParamselectedColor:string='#FF6B6B';@ParamstartWeekday:number=0;@ParamisShowHeader:boolean=true;privatemanager:ImportantDayManager=ImportantDayManager.getInstance();privateswiperController:SwiperController=newSwiperController();}参数说明
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
vm | CalendarVM | 单例 | 日历视图模型 |
onChangeDay | (date: Date) => void | 空函数 | 日期选择回调 |
selectedColor | string | '#FF6B6B' | 选中日期的高亮颜色 |
startWeekday | number | 0 | 一周起始日(0=周日) |
isShowHeader | boolean | true | 是否显示星期标题行 |
设计考量
- vm 通过 @Param 传入:虽然 vm 是单例,但通过
@Param传入使组件更灵活(理论上可传入不同的 VM 实例) - 回调函数模式:
onChangeDay让父组件决定如何处理日期选择 - manager 直接引用:
private manager直接获取单例,因为不需要外部控制 - swiperController 私有:Swiper 控制器是组件内部状态
三、build 方法整体结构
build(){Column(){if(this.isShowHeader){// 星期标题行Row(){ForEach(this.getWeekNames(),(name:string,index:number)=>{Text(name).fontSize(14).fontColor(index===0||index===6?'#FF6B6B':'#666666').layoutWeight(1).textAlign(TextAlign.Center)})}.width('100%').padding({top:8,bottom:8})}// 日历主体 - Swiper + LazyForEachSwiper(this.swiperController){LazyForEach(this.vm.monthWeekDataSource,(data:DateModelList)=>{Column(){if(this.vm.viewType===ViewTypeEnum.MONTH){this.monthGrid(data)}else{this.weekRow(data)}}},(data:DateModelList,index:number)=>`month_${index}`)}.cachedCount(CALENDAR_SWIPER_COUNT).index(this.vm.currentSwiperIndex).loop(false).indicator(false).onChange((index:number)=>{this.handleSwiperChange(index);})}.width('100%')}结构层次
Column ├── Row (星期标题行) — 条件显示 └── Swiper └── LazyForEach └── Column ├── monthGrid (月视图) — 条件渲染 └── weekRow (周视图) — 条件渲染四、星期标题行
privategetWeekNames():string[]{constnames=[...WEEK_NAMES];// ['日', '一', '二', '三', '四', '五', '六']if(this.startWeekday===1){names.push(names.shift()!);// 周一起始:['一', '二', '三', '四', '五', '六', '日']}returnnames;}周日/周一起始处理
- 周日起始(默认):
['日', '一', '二', '三', '四', '五', '六'] - 周一起始:将 ‘日’ 移到末尾 →
['一', '二', '三', '四', '五', '六', '日']
周末颜色区分
.fontColor(index===0||index===6?'#FF6B6B':'#666666')第一个和最后一个(周末)显示红色,工作日显示灰色。
五、月视图网格:monthGrid
@BuildermonthGrid(data:DateModelList){Grid(){ForEach(data,(dateModel:DateModel,index:number)=>{GridItem(){this.monthDayItem(dateModel)}},(dateModel:DateModel,index:number)=>`${dateModel.dateStr}_${index}`)}.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr').rowsGap(4).width('100%')}网格配置
- 7 列:
columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr') - 行间距:
rowsGap(4)— 4vp 的行间距 - 数据:42 个
DateModel(6行×7列) - 键值:
${dateModel.dateStr}_${index}— 日期字符串 + 索引确保唯一
六、日期项渲染:monthDayItem
这是最复杂的 Builder,负责单个日期格的完整渲染。
@BuildermonthDayItem(dateModel:DateModel){Stack({alignContent:Alignment.Center}){// 1. 选中日期的背景圆if(this.vm.isDateSelect(dateModel.dayjsObj)){Circle({width:36,height:36}).fill(this.selectedColor)}// 2. 今天的高亮边框(非选中时)if(this.vm.isToday(dateModel.dayjsObj)&&!this.vm.isDateSelect(dateModel.dayjsObj)){Circle({width:36,height:36}).fill('#FFFFFF').stroke(this.selectedColor).strokeWidth(2)}// 3. 内容列Column(){// 日期数字Text(`${dateModel.day}`).fontSize(16).fontWeight(this.vm.isDateSelect(dateModel.dayjsObj)?FontWeight.Bold:FontWeight.Normal).fontColor(this.getDayTextColor(dateModel))// 重要日标记if(this.hasImportantDay(dateModel)){// 颜色圆点行(最多3个)Row(){ForEach(this.getImportantDays(dateModel).slice(0,3),(day:ImportantDay)=>{Circle({width:5,height:5}).fill(day.color).margin({right:2})})}.height(6).margin({top:2})// 第一个重要日的标题预览Text(this.getFirstDayShortText(dateModel)).fontSize(8).fontColor(this.getFirstDayColor(dateModel)).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis}).width(34).textAlign(TextAlign.Center).margin({top:1})}}}.width('100%').height(52).onClick(()=>{this.onDateClick(dateModel);})}渲染层次
Stack (居中对齐) ├── Circle (选中背景) — 条件 ├── Circle (今天边框) — 条件 └── Column ├── Text (日期数字) └── Row (重要日标记) — 条件 ├── Row (颜色圆点 × 最多3个) └── Text (标题预览)选中与今天的视觉区分
| 状态 | 视觉表现 |
|---|---|
| 选中日期 | 实心圆背景 + 白色粗体数字 |
| 今天(未选中) | 空心圆边框 + 红色数字 |
| 今天(已选中) | 实心圆背景 + 白色粗体数字 |
| 普通日期 | 无装饰 + 正常颜色数字 |
重要日标记设计
- 颜色圆点:最多显示 3 个圆点,每个圆点使用重要日的自定义颜色
- 标题预览:显示第一个重要日的标题,超出宽度时省略
- 字体大小:8vp,在有限的格子空间内显示更多信息
七、日期文字颜色策略
privategetDayTextColor(dateModel:DateModel):string{if(this.vm.isDateSelect(dateModel.dayjsObj)){return'#FFFFFF';// 选中 → 白色}if(!dateModel.isCurrentMonth){return'#CCCCCC';// 非当月 → 灰色}if(this.vm.isToday(dateModel.dayjsObj)){returnthis.selectedColor;// 今天 → 主题色}if(this.hasImportantDay(dateModel)){returnthis.getFirstDayColor(dateModel);// 有重要日 → 重要日颜色}return'#333333';// 普通 → 深灰}优先级链
选中 → 非当月 → 今天 → 有重要日 → 普通| 优先级 | 条件 | 颜色 |
|---|---|---|
| 1 | 选中日期 | #FFFFFF(白色) |
| 2 | 非当月 | #CCCCCC(浅灰) |
| 3 | 今天 | selectedColor(主题色) |
| 4 | 有重要日 | day.color(重要日颜色) |
| 5 | 普通 | #333333(深灰) |
设计考量:选中状态优先级最高(白色文字配彩色背景最清晰),非当月优先于今天/重要日(避免非当月的今天造成混淆)。
八、周视图:weekRow
@BuilderweekRow(data:DateModelList){Row(){ForEach(data,(dateModel:DateModel,index:number)=>{Column(){Text(`${dateModel.day}`).fontSize(16).fontWeight(this.vm.isDateSelect(dateModel.dayjsObj)?FontWeight.Bold:FontWeight.Normal).fontColor(this.getDayTextColor(dateModel))if(this.hasImportantDay(dateModel)){Circle({width:5,height:5}).fill(this.getFirstDayColor(dateModel)).margin({top:2})}}.layoutWeight(1).height(56).justifyContent(FlexAlign.Center).onClick(()=>{this.onDateClick(dateModel);})})}.width('100%')}与月视图的差异
| 特性 | 月视图 | 周视图 |
|---|---|---|
| 布局 | Grid 7列 | Row 等分 |
| 选中背景 | Circle 填充 | 无(仅加粗文字) |
| 重要日标记 | 圆点 + 标题预览 | 仅圆点 |
| 高度 | 52vp | 56vp |
周视图更简洁,适合快速浏览一周概况。
九、日期点击处理
privateonDateClick(dateModel:DateModel):void{this.vm.changeDate(dateModel.dayjsObj);this.onChangeDay(dateModel.dayjsObj.toDate());}点击日期时:
- 更新 VM 中的选中日期
- 触发外部回调通知父组件
十、Swiper 切换联动
privatehandleSwiperChange(index:number):void{this.vm.currentSwiperIndex=index;this.vm.handleHorizontalSwiper(index);constcurrentData=this.vm.monthWeekDataSource.getData(index);if(currentData&¤tData.length>15){constmiddleDate=currentData[15].dayjsObj;if(!this.vm.selectDate.isSame(middleDate,'month')){this.vm.selectDate=middleDate;}}}联动逻辑
- 更新当前 Swiper 索引
- 调用 VM 处理边界追加
- 更新选中日期到当前月份(取索引15的日期作为月份参考)
十一、重要日查询方法
privatehasImportantDay(dateModel:DateModel):boolean{returnthis.manager.hasImportantDay(dateModel.dateStr);}privategetImportantDays(dateModel:DateModel):ImportantDay[]{returnthis.manager.getDaysByDate(dateModel.dateStr);}privategetFirstDayColor(dateModel:DateModel):string{constdays=this.getImportantDays(dateModel);if(days.length>0){returndays[0].color;}return'#FF6B6B';}privategetFirstDayShortText(dateModel:DateModel):string{constdays=this.getImportantDays(dateModel);if(days.length>0){returndays[0].title;}return'';}这些方法封装了对ImportantDayManager的查询,提供组件级别的便捷访问。
性能考量
每次渲染日期项时都会查询dayMap,但由于dayMap是Map结构,查找复杂度为 O(1),42 个日期格的查询总开销可忽略。
十二、总结
MonthWeekCalendar是整个项目中 UI 逻辑最密集的组件:
- 双视图渲染:月视图和周视图通过条件渲染切换
- 多层视觉:选中背景、今天边框、重要日标记层层叠加
- 颜色优先级:5 级颜色策略确保信息层次清晰
- 事件联动:点击和滑动事件与 VM 双向同步
- 性能优化:Map 查询 + LazyForEach 保证渲染效率
这个组件完美展示了 ArkUI 声明式 UI 在复杂交互场景下的表达能力。