HarmonyOs应用《重要日》开发第11篇 - 年视图组件
2026/7/18 15:25:17 网站建设 项目流程

本文解析 ImportantDays 项目中年视图的两个组件:YearCalendar负责整体年份网格布局和无限滚动,BaseCalendar负责单月小日历的渲染和重要日标记。两个组件协作呈现完整的年视图日历体验。

一、年视图整体设计

年视图将一整年的 12 个月以小日历网格的形式展示,用户可以快速浏览全年概况,并通过颜色圆点识别哪些日期有重要日。

组件协作关系

CalendarPage └── YearCalendar (年视图容器) ├── 年份标题 └── Swiper + LazyForEach └── Grid (3×4 月份网格) └── BaseCalendar × 12 (单月小日历)

二、YearCalendar 组件

组件定义

@ComponentV2exportstruct YearCalendar{@Paramvm:CalendarVM=CalendarVM.getInstance();@ParamonSelectDate:(date:Date)=>void=()=>{};@LocalyearDataSource:YearDataSource=newYearDataSource();@LocalcurrentYear:number=newDate().getFullYear();privateswiperController:SwiperController=newSwiperController();}

参数说明

参数类型说明
vmCalendarVM日历视图模型(通过 @Param 传入)
onSelectDate(date: Date) => void日期选择回调
yearDataSourceYearDataSource年份数据源(@Local 内部状态)
currentYearnumber当前显示年份
swiperControllerSwiperControllerSwiper 控制器

初始化

aboutToAppear():void{this.yearDataSource=this.vm.yearDataSource;this.currentYear=this.vm.selectDate.year();}

在组件即将出现时,从 VM 获取数据源和当前年份。

build 方法

build(){Column(){// 年份标题Row(){Text(`${this.currentYear}`).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#333333')}.width('100%').justifyContent(FlexAlign.Center).padding({top:8,bottom:12})// 月份网格 - Swiper + LazyForEachSwiper(this.swiperController){LazyForEach(this.yearDataSource,(year:number)=>{Grid(){ForEach([0,1,2,3,4,5,6,7,8,9,10,11],(month:number)=>{GridItem(){BaseCalendar({year:year,month:month,startWeekday:this.vm.startWeekday})}},(month:number)=>`${year}_${month}`)}.columnsTemplate('1fr 1fr 1fr').rowsGap(8).columnsGap(4).width('100%').height('100%').padding(4)},(year:number)=>`year_${year}`)}.cachedCount(CALENDAR_SWIPER_COUNT).index(1).loop(false).indicator(false).onChange((index:number)=>{this.handleYearSwiperChange(index);}).layoutWeight(1)}.width('100%').height('100%')}

布局结构

Column ├── Row (年份标题) └── Swiper └── LazyForEach └── Grid (3列) └── GridItem × 12 └── BaseCalendar

Grid 配置

  • 3 列 4 行columnsTemplate('1fr 1fr 1fr')— 每行 3 个月,共 4 行
  • 行间距rowsGap(8)— 月份之间的垂直间距
  • 列间距columnsGap(4)— 月份之间的水平间距

月份网格布局

┌────────┬────────┬────────┐ │ 1月 │ 2月 │ 3月 │ ├────────┼────────┼────────┤ │ 4月 │ 5月 │ 6月 │ ├────────┼────────┼────────┤ │ 7月 │ 8月 │ 9月 │ ├────────┼────────┼────────┤ │ 10月 │ 11月 │ 12月 │ └────────┴────────┴────────┘

三、年视图无限滚动

privatehandleYearSwiperChange(index:number):void{if(index===this.yearDataSource.totalCount()-1){// 追加下一年constlastYear=this.yearDataSource.getData(this.yearDataSource.totalCount()-1);this.yearDataSource.addData(lastYear+1);}elseif(index===0){// 前插上一年constfirstYear=this.yearDataSource.getData(0);this.yearDataSource.addDataFromHead(firstYear-1);}constcurrentYear=this.yearDataSource.getData(index);this.currentYear=currentYear;this.vm.selectDate=this.vm.selectDate.year(currentYear);}

滚动逻辑

  1. 滑到末尾:追加下一年(lastYear + 1
  2. 滑到开头:前插上一年(firstYear - 1
  3. 更新状态:同步currentYear和 VM 的selectDate

滚动示例

初始: [2025] [2026*] [2027] 滑到2027: [2025] [2026] [2027*] → 追加2028 → [2025] [2026] [2027*] [2028]

四、BaseCalendar 组件

BaseCalendar负责渲染单月的小日历,是年视图中每个月份的独立单元。

组件定义

@Componentexportstruct BaseCalendar{@Propyear:number=0;@Propmonth:number=0;@PropstartWeekday:number=0;privatevm:CalendarVM=CalendarVM.getInstance();privatemanager:ImportantDayManager=ImportantDayManager.getInstance();}

注意:使用 V1 装饰器

BaseCalendar使用@Component@Prop(V1),而非@ComponentV2@Param(V2)。这可能是因为:

  1. @Prop在 V1 中是值拷贝,适合简单类型参数
  2. 小日历组件不需要响应式更新(年视图切换时整体重渲染)
  3. 保持与可能存在的 V1 组件兼容

build 方法

build(){Column(){// 月份标题Text(`${this.month+1}`).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#333333').margin({bottom:4})// 星期标题行Row(){ForEach(WEEK_NAMES,(name:string)=>{Text(name).fontSize(10).fontColor('#999999').layoutWeight(1).textAlign(TextAlign.Center)})}.width('100%')// 日期网格Grid(){ForEach(this.getDateList(),(dateModel:DateModel,index:number)=>{GridItem(){this.dayItem(dateModel)}},(dateModel:DateModel,index:number)=>`${this.year}_${this.month}_${index}`)}.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr').rowsGap(2).width('100%')}.width('100%').padding(8)}

布局结构

Column ├── Text (月份标题,如 "3月") ├── Row (星期标题行) └── Grid (7列) └── GridItem × 42 └── dayItem

与 MonthWeekCalendar 的差异

特性MonthWeekCalendarBaseCalendar
装饰器@ComponentV2@Component
参数传递@Param(引用)@Prop(值拷贝)
字体大小16vp10-14vp
日期项高度52vp22vp
选中背景有(Circle 填充)
重要日标记圆点 + 标题预览仅圆点
点击交互

五、日期列表生成

privategetDateList():DateModel[]{constdate=dayjs().year(this.year).month(this.month);constlist:DateModel[]=[];constfirstDay=date.date(1);constdaysInMonth=date.daysInMonth();letfirstDayWeekday:number=firstDay.day();firstDayWeekday=(firstDayWeekday-this.startWeekday+WEEK_DAYS)%WEEK_DAYS;// 上月填充for(leti=firstDayWeekday-1;i>=0;i--){constprevDate=firstDay.subtract(i+1,'day');list.push(newDateModel(prevDate,false));}// 当月for(leti=1;i<=daysInMonth;i++){constdayDate=date.date(i);list.push(newDateModel(dayDate,true));}// 下月填充while(list.length<42){constlastDate=list[list.length-1].dayjsObj;list.push(newDateModel(lastDate.add(1,'day'),false));}returnlist;}

此方法与CalendarVM.getDateList()逻辑相同,但BaseCalendar独立实现以减少对 VM 的依赖。

六、日期项渲染

@BuilderdayItem(dateModel:DateModel){Stack({alignContent:Alignment.Center}){// 重要日标记点if(this.hasImportantDay(dateModel)){Circle({width:4,height:4}).fill(this.getFirstDayColor(dateModel)).position({x:'50%',y:2})}Text(`${dateModel.day}`).fontSize(11).fontColor(this.getDayColor(dateModel))}.width('100%').height(22)}

标记点位置

.position({x:'50%',y:2})

重要日标记圆点定位在日期格的顶部中央,4×4vp 的小圆点不影响日期数字的显示。

日期颜色策略

privategetDayColor(dateModel:DateModel):string{if(!dateModel.isCurrentMonth){return'#CCCCCC';// 非当月 → 灰色}if(this.vm.isToday(dateModel.dayjsObj)){return'#FF6B6B';// 今天 → 红色}if(this.hasImportantDay(dateModel)){returnthis.getFirstDayColor(dateModel);// 有重要日 → 重要日颜色}return'#333333';// 普通 → 深灰}

颜色优先级

非当月 → 今天 → 有重要日 → 普通

注意:年视图没有"选中"状态,因为年视图主要用于浏览,选中操作在月视图中进行。

七、重要日查询

privatehasImportantDay(dateModel:DateModel):boolean{returnthis.manager.hasImportantDay(dateModel.dateStr);}privategetFirstDayColor(dateModel:DateModel):string{constdays=this.manager.getDaysByDate(dateModel.dateStr);if(days.length>0){returndays[0].color;}return'#FF6B6B';}

MonthWeekCalendar相同,通过ImportantDayManagerdayMap查询。

八、从年视图到月视图的切换

CalendarPage中,年视图选择日期后会切换到月视图:

YearCalendar({vm:this.vm,onSelectDate:(date:Date)=>{this.vm.changeDate(dayjs(date));this.vm.changeViewType(ViewTypeEnum.MONTH);this.updateTitle();this.updateSelectedDays();}})

切换流程

用户在年视图中点击某月 ↓ BaseCalendar 的小日历不响应点击 (但 Grid 的整体可以扩展此功能) ↓ 实际上年视图通过 onSelectDate 回调 ↓ VM 切换到月视图 ↓ CalendarPage 重新渲染为月视图

九、性能分析

渲染开销

年视图一次渲染 12 个月的小日历,每个小日历有 42 个日期格:

  • 总日期格数:12 × 42 = 504 个
  • 每个日期格:1 个 Text + 可能 1 个 Circle
  • DOM 节点数:约 1000+

性能优化

  1. LazyForEach:年视图的 Swiper 只渲染可见页,预渲染页通过cachedCount控制
  2. 小字体小尺寸:10-11vp 字体、4vp 圆点,减少渲染像素
  3. 无交互:小日历不响应点击,减少事件处理开销
  4. Map 查询dayMap提供 O(1) 查询

十、总结

YearCalendarBaseCalendar的协作展示了组件分层设计:

  1. YearCalendar:管理年份级别的 Swiper 滚动和 Grid 布局
  2. BaseCalendar:专注于单月小日历的渲染和标记
  3. 职责分离:容器组件管布局和状态,叶子组件管渲染
  4. 性能可控:LazyForEach + 小尺寸渲染确保流畅
  5. V1/V2 混用:展示了 ArkUI V1 和 V2 组件可以共存

年视图为用户提供了全年重要日的鸟瞰视角,是月视图的有力补充。

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

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

立即咨询