Tempus Dominus自定义视图开发:从日视图到年视图扩展

Tempus Dominus自定义视图开发:从日视图到年视图扩展

【免费下载链接】tempus-dominus A powerful Date/time picker widget. 【免费下载链接】tempus-dominus 项目地址: https://gitcode.com/gh_mirrors/te/tempus-dominus

Tempus Dominus作为一款功能强大的日期时间选择器(Date/time picker widget),提供了灵活的视图扩展能力。本文将从现有视图实现入手,详细介绍如何从日视图扩展到年视图,并自定义视图组件。通过本文,你将掌握视图开发的核心模式、组件通信机制及样式定制方法,最终实现符合业务需求的定制化时间选择界面。

视图系统架构解析

Tempus Dominus的视图系统采用模块化设计,每个视图类型对应独立的实现类,统一继承自基础视图接口。核心视图实现位于src/js/display/calendar/目录,包含日视图(DateDisplay)、月视图(MonthDisplay)、年视图(YearDisplay)和十年视图(DecadeDisplay)四种基础实现。

核心视图类结构

所有日历视图均遵循相同的实现模式,包含构建DOM结构的getPicker()方法和更新视图内容的_update()方法:

// 日视图核心实现 [src/js/display/calendar/date-display.ts](https://link.gitcode.com/i/77f4d32f16384cd596b73f959fc330b4)
export default class DateDisplay {
  getPicker(): HTMLElement { /* 创建日历容器 */ }
  _update(widget: HTMLElement, paint: Paint): void { /* 更新日历内容 */ }
  private _daysOfTheWeek(): HTMLElement[] { /* 生成星期标题行 */ }
  private _handleCalendarWeeks(container: HTMLElement, innerDate: DateTime) { /* 处理周数显示 */ }
}

月视图实现与此类似,但采用12个月份格子的布局:

// 月视图实现 [src/js/display/calendar/month-display.ts](https://link.gitcode.com/i/c617bd7757329f862d8addd141664f31)
export default class MonthDisplay {
  getPicker(): HTMLElement {
    const container = document.createElement('div');
    container.classList.add(Namespace.css.monthsContainer);
    for (let i = 0; i < 12; i++) { /* 创建12个月份格子 */ }
    return container;
  }
}

视图切换机制

视图切换通过options.display.viewMode配置项控制,支持从日视图到十年视图的多级切换。官方文档src/docs/partials/options/display.html详细列出了视图相关配置:

{
  display: {
    viewMode: 'calendar', // 默认为日视图,可选值: 'clock' | 'calendar' | 'months' | 'years' | 'decades'
    components: {
      calendar: true,      // 总开关,控制所有日历视图可见性
      date: true,          // 日视图开关
      month: true,         // 月视图开关
      year: true,          // 年视图开关
      decades: true        // 十年视图开关
    }
  }
}

日视图实现详解

日视图(DateDisplay)是最常用的视图类型,负责显示月历网格并处理日期选择交互。其核心实现包含DOM结构构建、日期数据填充和交互事件处理三个部分。

DOM结构构建

getPicker()方法创建包含42个日期格子的容器(6行×7列),并预留周数列位置:

// [src/js/display/calendar/date-display.ts](https://link.gitcode.com/i/77f4d32f16384cd596b73f959fc330b4#L28-L70)
getPicker(): HTMLElement {
  const container = document.createElement('div');
  container.classList.add(Namespace.css.daysContainer);
  
  // 添加星期标题行
  container.append(...this._daysOfTheWeek());
  
  // 添加42个日期格子
  for (let i = 0; i < 42; i++) {
    const div = document.createElement('div');
    div.setAttribute('data-action', ActionTypes.selectDay);
    container.appendChild(div);
  }
  
  return container;
}

生成的日历容器结构如图所示,包含星期标题行和6行日期格子:

月历视图

日期数据填充

_update()方法负责将日期数据填充到DOM元素,并处理日期状态(选中、禁用、今天等):

// [src/js/display/calendar/date-display.ts](https://link.gitcode.com/i/77f4d32f16384cd596b73f959fc330b4#L76-L133)
_update(widget: HTMLElement, paint: Paint): void {
  const container = widget.getElementsByClassName(Namespace.css.daysContainer)[0] as HTMLElement;
  const innerDate = this.optionsStore.viewDate.clone.startOf(Unit.month);
  
  // 遍历所有日期格子并填充数据
  container.querySelectorAll(`[data-action="${ActionTypes.selectDay}"]`).forEach((element: HTMLElement) => {
    const classes = [Namespace.css.day];
    
    // 设置日期状态类
    if (innerDate.isBefore(this.optionsStore.viewDate, Unit.month)) classes.push(Namespace.css.old);
    if (innerDate.isAfter(this.optionsStore.viewDate, Unit.month)) classes.push(Namespace.css.new);
    if (this.dates.isPicked(innerDate, Unit.date)) classes.push(Namespace.css.active);
    if (!this.validation.isValid(innerDate, Unit.date)) classes.push(Namespace.css.disabled);
    if (innerDate.isSame(new DateTime(), Unit.date)) classes.push(Namespace.css.today);
    
    // 应用样式并设置日期文本
    element.classList.add(...classes);
    element.innerText = innerDate.parts(undefined, { day: 'numeric' }).day;
    innerDate.manipulate(1, Unit.date);
  });
}

周数显示功能

当日历配置display.calendarWeeks: true时,会在日历左侧显示周数列,实现逻辑位于_handleCalendarWeeks()方法:

// [src/js/display/calendar/date-display.ts](https://link.gitcode.com/i/77f4d32f16384cd596b73f959fc330b4#L332-L338)
private _handleCalendarWeeks(container: HTMLElement, innerDate: DateTime) {
  [...container.querySelectorAll(`.${Namespace.css.calendarWeeks}`)]
    .filter((e: HTMLElement) => e.innerText !== '#')
    .forEach((element: HTMLElement) => {
      element.innerText = `${innerDate.week}`;
      innerDate.manipulate(7, Unit.date);
    });
}

周数显示效果如图所示,左侧额外列显示当前周在全年中的序号:

周数显示

月视图与年视图扩展

月视图(MonthDisplay)和年视图(YearDisplay)采用与日视图相同的架构模式,但数据粒度和布局不同。理解这些视图的实现差异,是进行自定义扩展的基础。

月视图实现特点

月视图以年为单位,显示12个月份格子,核心实现位于src/js/display/calendar/month-display.ts

// 月视图日期填充逻辑
_update(widget: HTMLElement, paint: Paint): void {
  const innerDate = this.optionsStore.viewDate.clone.startOf(Unit.year);
  
  container.querySelectorAll(`[data-action="${ActionTypes.selectMonth}"]`).forEach((containerClone: HTMLElement) => {
    // 设置月份名称
    containerClone.innerText = innerDate.format({ month: 'short' });
    // 检查是否为选中状态
    if (this.dates.isPicked(innerDate, Unit.month)) {
      classes.push(Namespace.css.active);
    }
    innerDate.manipulate(1, Unit.month);
  });
}

月视图显示效果如图,以网格形式展示12个月份,当前选中月份高亮显示:

月视图

年视图实现特点

年视图以十年为单位,显示12个年份格子,核心实现位于src/js/display/calendar/year-display.ts

// 年视图构造函数
constructor() {
  this.optionsStore = serviceLocator.locate(OptionsStore);
  this.dates = serviceLocator.locate(Dates);
  this.validation = serviceLocator.locate(Validation);
}

// 年视图日期范围计算
_update(widget: HTMLElement, paint: Paint) {
  this._startYear = this.optionsStore.viewDate.clone.manipulate(-1, Unit.year);
  this._endYear = this.optionsStore.viewDate.clone.manipulate(10, Unit.year);
  
  // 生成年份标签
  switcher.setAttribute(
    Namespace.css.yearsContainer,
    `${this._startYear.format({ year: 'numeric' })}-${this._endYear.format({ year: 'numeric' })}`
  );
}

年视图显示效果如图,展示连续12年的年份选择界面:

年视图

自定义视图开发实战

自定义视图开发需遵循现有视图的实现规范,实现getPicker()_update()核心方法,并通过配置项启用自定义视图。以下以季度视图为例,详细说明实现步骤。

步骤1:创建季度视图类

src/js/display/calendar/目录下创建quarter-display.ts文件,实现季度视图类:

import { Unit } from '../../datetime';
import Namespace from '../../utilities/namespace';
import { serviceLocator } from '../../utilities/service-locator';
import ActionTypes from '../../utilities/action-types';
import { OptionsStore } from '../../utilities/optionsStore';

export default class QuarterDisplay {
  private optionsStore: OptionsStore;
  
  constructor() {
    this.optionsStore = serviceLocator.locate(OptionsStore);
  }
  
  // 创建季度选择器DOM
  getPicker(): HTMLElement {
    const container = document.createElement('div');
    container.classList.add(Namespace.css.quartersContainer);
    
    // 创建4个季度格子
    for (let i = 0; i < 4; i++) {
      const div = document.createElement('div');
      div.setAttribute('data-action', ActionTypes.selectQuarter);
      container.appendChild(div);
    }
    
    return container;
  }
  
  // 更新季度视图内容
  _update(widget: HTMLElement, paint: Paint): void {
    const container = widget.getElementsByClassName(Namespace.css.quartersContainer)[0];
    const innerDate = this.optionsStore.viewDate.clone.startOf(Unit.year);
    
    container.querySelectorAll(`[data-action="${ActionTypes.selectQuarter}"]`).forEach((element: HTMLElement, index) => {
      const classes = [Namespace.css.quarter];
      const quarterName = `Q${index + 1} ${innerDate.format({ year: 'numeric' })}`;
      
      // 设置选中状态
      if (this.isCurrentQuarter(innerDate, index)) {
        classes.push(Namespace.css.active);
      }
      
      element.classList.add(...classes);
      element.innerText = quarterName;
      innerDate.manipulate(3, Unit.month); // 移动到下一季度
    });
  }
  
  private isCurrentQuarter(date: DateTime, quarterIndex: number): boolean {
    // 实现季度选中逻辑
    const currentQuarter = Math.floor(date.month / 3);
    return currentQuarter === quarterIndex;
  }
}

步骤2:注册视图类型

在视图管理器中注册新创建的季度视图,修改src/js/display/index.ts

import QuarterDisplay from './calendar/quarter-display';

export const ViewTypes = {
  date: DateDisplay,
  month: MonthDisplay,
  year: YearDisplay,
  decade: DecadeDisplay,
  quarter: QuarterDisplay  // 添加季度视图
};

步骤3:添加配置选项

扩展配置选项以支持季度视图,修改src/js/utilities/options.ts

export interface DisplayOptions {
  viewMode: 'calendar' | 'months' | 'years' | 'decades' | 'quarters'; // 添加quarters选项
  components: {
    // ... 现有配置
    quarter: boolean; // 添加季度视图开关
  };
}

步骤4:实现视图切换逻辑

修改视图切换处理逻辑,支持季度视图的切换,修改src/js/datetime.ts

switch (newViewMode) {
  case 'quarters':
    this.currentView = new QuarterDisplay();
    break;
  // ... 其他视图case
}

步骤5:样式定制

在SCSS文件中添加季度视图样式,修改src/scss/tempus-dominus.scss

.quarters-container {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.25rem;
  
  .quarter {
    padding: 1rem;
    text-align: center;
    cursor: pointer;
    
    &.active {
      background-color: $primary;
      color: white;
    }
    
    &.disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }
  }
}

高级视图配置与交互

Tempus Dominus提供丰富的视图配置选项,可通过display配置对象自定义视图行为和外观。合理利用这些配置项,可在不修改源码的情况下实现多样化的视图效果。

视图组件控制

通过display.components配置可细粒度控制各视图组件的可见性:

{
  display: {
    components: {
      calendar: true,    // 总开关:控制所有日历视图
      date: true,        // 日视图开关
      month: true,       // 月视图开关
      year: true,        // 年视图开关
      decades: false,    // 禁用十年视图
      quarter: true      // 启用自定义季度视图
    }
  }
}

工具栏与按钮配置

视图工具栏包含导航按钮和视图切换控件,可通过display.buttons配置控制按钮显示:

{
  display: {
    buttons: {
      today: true,  // 显示"今天"按钮
      clear: true,  // 显示"清除"按钮
      close: true   // 显示"关闭"按钮
    },
    toolbarPlacement: 'top'  // 工具栏位置:top/bottom
  }
}

工具栏按钮布局如图所示,包含今天、清除和关闭三个功能按钮:

工具栏按钮

视图导航控制

视图导航按钮状态由_updateCalendarView()方法控制,可根据业务需求自定义导航逻辑:

// [src/js/display/calendar/date-display.ts](https://link.gitcode.com/i/77f4d32f16384cd596b73f959fc330b4#L263-L289)
private _updateCalendarView(container: Element) {
  const [previous, switcher, next] = container.parentElement
    .getElementsByClassName(Namespace.css.calendarHeader)[0]
    .getElementsByTagName('div');
  
  // 设置标题文本
  switcher.setAttribute(
    Namespace.css.daysContainer,
    this.optionsStore.viewDate.format(this.optionsStore.options.localization.dayViewHeaderFormat)
  );
  
  // 控制导航按钮状态
  this.validation.isValid(
    this.optionsStore.viewDate.clone.manipulate(-1, Unit.month),
    Unit.month
  ) ? previous.classList.remove(Namespace.css.disabled) 
    : previous.classList.add(Namespace.css.disabled);
}

视图扩展最佳实践

自定义视图开发需遵循一定的设计原则,以确保代码可维护性和与框架的兼容性。以下是视图扩展开发中的最佳实践指南。

代码组织原则

  1. 单一职责:每个视图类只负责一种视图类型的实现
  2. 依赖注入:通过serviceLocator获取依赖,避免硬编码依赖关系
  3. 状态管理:通过optionsStoredates服务管理视图状态,避免直接操作DOM
  4. 样式隔离:使用唯一的CSS类前缀,避免样式冲突

性能优化策略

  1. DOM复用:创建一次DOM结构,通过_update()方法复用DOM元素
  2. 批量操作:使用DocumentFragment批量处理DOM更新
  3. 事件委托:在父容器上委托处理子元素事件,减少事件监听器数量
  4. 懒加载:对不常用的视图类型采用动态导入

兼容性考虑

  1. 浏览器支持:确保使用的API在目标浏览器中受支持
  2. 响应式设计:使用CSS Grid和Flexbox实现自适应布局
  3. 无障碍访问:添加适当的ARIA属性,支持键盘导航

视图扩展实例:财务季度选择器

基于上述自定义视图开发方法,我们实现一个财务季度选择器,用于财务系统中的季度数据筛选。

需求分析

财务季度选择器需满足以下需求:

  • 显示当前年度的四个季度
  • 支持跨年度季度选择
  • 高亮显示当前季度
  • 支持季度范围选择

实现效果

财务季度选择器最终效果如图所示,以卡片式布局展示四个季度,当前季度高亮显示,支持单击选择和拖拽选择季度范围:

财务季度选择器

核心代码实现

季度范围选择功能实现:

// 季度视图中的范围选择实现
private _handleRangeSelection(element: HTMLElement) {
  if (this.optionsStore.options.dateRange) {
    const startDate = this.dates.picked[0];
    const endDate = DateTime.fromString(element.getAttribute('data-value'), { format: 'yyyy-MM' });
    
    if (startDate && !endDate) {
      // 绘制范围选择样式
      this._paintRange(startDate, endDate);
    }
  }
}

private _paintRange(start: DateTime, end: DateTime) {
  // 实现范围选择的样式绘制逻辑
}

总结与扩展

Tempus Dominus的视图系统通过模块化设计提供了强大的扩展能力,本文详细介绍了视图系统的架构设计、核心实现及自定义扩展方法。通过实现自定义视图类、注册视图类型、扩展配置选项和样式定制四个步骤,可快速开发符合业务需求的定制化视图。

关键知识点回顾

  1. 视图架构:所有视图遵循getPicker()+_update()的实现模式
  2. 核心目录:视图实现位于src/js/display/calendar/
  3. 配置系统:通过display配置对象控制视图行为
  4. 扩展步骤:创建视图类 → 注册视图 → 添加配置 → 样式定制

未来扩展方向

  1. 自定义单元格渲染:通过paint回调自定义单元格内容
  2. 多日历视图:实现同时显示多个月份的日历视图
  3. 任务日历:集成任务数据,实现任务日历视图
  4. 热图视图:基于数据密度的热图日历视图

通过掌握Tempus Dominus的视图扩展方法,开发者可以构建丰富多样的时间选择界面,满足不同业务场景的需求。官方文档src/docs/partials/options/display.html提供了更多视图配置选项的详细说明,建议进一步查阅以深入理解视图系统的 capabilities。

【免费下载链接】tempus-dominus A powerful Date/time picker widget. 【免费下载链接】tempus-dominus 项目地址: https://gitcode.com/gh_mirrors/te/tempus-dominus

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值