HarmonyOs应用《重要日》开发第12篇 - 月视图日历组件详解
2026/7/18 14:55:45 网站建设 项目流程

本文深入解析 ImportantDays 项目中最复杂的 UI 组件MonthWeekCalendar,包括其组件参数设计、月视图网格渲染、日期项的选中/今天/重要日标记逻辑、周视图渲染、无限滚动的联动处理,以及日期文字颜色的多级优先策略。

一、组件定位

MonthWeekCalendar是日历模块的核心 UI 组件,负责:

  1. 渲染月视图(42 格网格)和周视图(7 天横排)
  2. 标记今天、选中日期
  3. 在日期上显示重要日颜色圆点和标题预览
  4. 处理日期点击事件
  5. 联动 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();}

参数说明

参数类型默认值说明
vmCalendarVM单例日历视图模型
onChangeDay(date: Date) => void空函数日期选择回调
selectedColorstring'#FF6B6B'选中日期的高亮颜色
startWeekdaynumber0一周起始日(0=周日)
isShowHeaderbooleantrue是否显示星期标题行

设计考量

  1. vm 通过 @Param 传入:虽然 vm 是单例,但通过@Param传入使组件更灵活(理论上可传入不同的 VM 实例)
  2. 回调函数模式onChangeDay让父组件决定如何处理日期选择
  3. manager 直接引用private manager直接获取单例,因为不需要外部控制
  4. 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 (标题预览)

选中与今天的视觉区分

状态视觉表现
选中日期实心圆背景 + 白色粗体数字
今天(未选中)空心圆边框 + 红色数字
今天(已选中)实心圆背景 + 白色粗体数字
普通日期无装饰 + 正常颜色数字

重要日标记设计

  1. 颜色圆点:最多显示 3 个圆点,每个圆点使用重要日的自定义颜色
  2. 标题预览:显示第一个重要日的标题,超出宽度时省略
  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 填充无(仅加粗文字)
重要日标记圆点 + 标题预览仅圆点
高度52vp56vp

周视图更简洁,适合快速浏览一周概况。

九、日期点击处理

privateonDateClick(dateModel:DateModel):void{this.vm.changeDate(dateModel.dayjsObj);this.onChangeDay(dateModel.dayjsObj.toDate());}

点击日期时:

  1. 更新 VM 中的选中日期
  2. 触发外部回调通知父组件

十、Swiper 切换联动

privatehandleSwiperChange(index:number):void{this.vm.currentSwiperIndex=index;this.vm.handleHorizontalSwiper(index);constcurrentData=this.vm.monthWeekDataSource.getData(index);if(currentData&&currentData.length>15){constmiddleDate=currentData[15].dayjsObj;if(!this.vm.selectDate.isSame(middleDate,'month')){this.vm.selectDate=middleDate;}}}

联动逻辑

  1. 更新当前 Swiper 索引
  2. 调用 VM 处理边界追加
  3. 更新选中日期到当前月份(取索引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,但由于dayMapMap结构,查找复杂度为 O(1),42 个日期格的查询总开销可忽略。

十二、总结

MonthWeekCalendar是整个项目中 UI 逻辑最密集的组件:

  1. 双视图渲染:月视图和周视图通过条件渲染切换
  2. 多层视觉:选中背景、今天边框、重要日标记层层叠加
  3. 颜色优先级:5 级颜色策略确保信息层次清晰
  4. 事件联动:点击和滑动事件与 VM 双向同步
  5. 性能优化:Map 查询 + LazyForEach 保证渲染效率

这个组件完美展示了 ArkUI 声明式 UI 在复杂交互场景下的表达能力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询