专栏名称: 依韵宵音
前端工程师
目录
51好读  ›  专栏  ›  依韵宵音

带你开发一个二维周视图日历

依韵宵音  · 掘金  ·  · 2017-12-13 02:19

正文

即之前实现了一个月视图日历,我们今天来实现一个二维周视图的日历。

以下进行分析其中的关键部分。

结构准备

不同之处在于其在日历的基础上还有一个分类轴,用于展示不同的类目,主要用于一周内的日程安排、会议安排等。

二维则和之前单独的有所不同,二维日历再切换日期时不用全部重新渲染,分类是不用变的,仅仅改变显示的日期即可。

而且由于是二维的,插入的内容必定是同时属于一个分类和一个时间段的,内容肯定是可以跨越时间(即日期轴)的,因此不能直接将插入的内容像开始的日历一样直接放置在日历的格子中。而要进行单独的处理。

另外,只要分类不变,日期和分类构成的网格是不用重绘的。

考虑到以上情况,插入内容的和网格是需要分开来的,我将现成的日历弄成一下3D效果示意:

即插入内容的层是单独放置在时间和分类构成的网格上方的。

基于以上分析,先构建如下基本结构:

<div class="ep-weekcalendar border">
    <!-- 头部 -->
    <div class="ep-weekcalendar-header">
        <div class="ep-weekcalendar-header-left"></div>
        <div class="ep-weekcalendar-header-center">
            <span class="ep-weekcalendar-header-btn ep-weekcalendar-header-btn-prev"></span>
            <span class="ep-weekcalendar-title">2017年12月04日 - 10日</span>
            <span class="ep-weekcalendar-header-btn ep-weekcalendar-header-btn-next"></span>
        </div>
        <div class="ep-weekcalendar-header-right"></div>
    </div>
    <!-- 主体 -->
    <div class="ep-weekcalendar-body">
        <!-- 分类区域 -->
        <div class="ep-weekcalendar-category-area">
            <div class="ep-weekcalendar-category-header">
                <span class="ep-weekcalendar-category-title">车辆</span>
            </div>
            <ul class="ep-weekcalendar-category-list">
            </ul>
        </div>
        <!-- 内容区域 -->
        <div class="ep-weekcalendar-time-area">
            <!-- 每周日期渲染区域。切换日期时重新绘制内容 -->
            <div class="ep-weekcalendar-weeks"></div>
            <div class="ep-weekcalendar-main">
                <!-- 分类和内容构建的网格区域,仅在分类改变时进行调整 -->
                <div class="ep-weekcalendar-grid"> </div>
                <!-- 可插入任意内容的区域,日期切换时清空,根据使用需求插入内容 -->
                <div class="ep-weekcalendar-content"></div>
            </div>
        </div>
    </div>
    <!-- 底部 -->
    <div class="ep-weekcalendar-body"></div>
</div>

结构如上,实现代码就不用展示了。

绘制实现

初始好了必要的结构,我们接着进行日历的绘制工作。

分类绘制

首先要处理的是分类,周视图中,一周的天数是固定的,确定好分类才能绘制出主体部分的网格。

对于分类,暂时考虑如下必要数据格式:

{
    id: 'cate-1', // 分类ID
    name: '法拉利', // 分类名称
    content: '苏E00000' // 分类的具体描述
}

实现如下:

{
    // 设置分类数据
    setCategory: function (data) {
        if (!(data instanceof Array)) {
            this.throwError('分类数据必须是一个数组');
            return;
        }
        this._categoryData = data;

        // 绘制分类
        this._renderCatagories();
        // 绘制其他需要改变的部分
        this._renderChanged();
    },
    // 左侧分类渲染
    _renderCatagories: function () {
        this._categoryListEl.innerHTML = '';

        var i = 0,
            data = this._categoryData,
            node = document.createElement('li'),
            cataEl;
        node.className = 'ep-weekcalendar-category';

        // 用行作为下标记录当前分类id集合
        this._categoryIndexs = [];
        // id为键记录索引
        this._categoryReocrds = {};

        while (i < data.length) {
            this._categoryIndexs.push(data[i].id);
            this._categoryReocrds[data[i].id] = i;
            cataEl = node.cloneNode(true);
            this._rendercategory(data[i], cataEl);
            i++;
        }
    },
    _rendercategory: function (cate, cateEl) {
        cateEl.setAttribute('data-cateid', cate.id);

        var titleEl = document.createElement('span'),
            contentEl = document.createElement('span');
        titleEl.className = 'title';
        contentEl.className = 'content';

        titleEl.innerHTML = cate.name;
        contentEl.innerHTML = cate.content;
        cateEl.appendChild(titleEl);
        cateEl.appendChild(contentEl);

        this.fire('categoryRender', {
            categoryEl: cateEl,
            titleEl: titleEl,
            contentEl: contentEl
        });

        this._categoryListEl.appendChild(cateEl);

        this.fire('agterCategoryRender', {
            categoryEl: cateEl,
            titleEl: titleEl,
            contentEl: contentEl
        });
    }
}

上面通过设置分类数据 setCategory 作为入口,调用绘制分类方法,其中还调用了 _renderChanged 此方法用于重新绘制日历的可变部分,如标题、日期和其中的内容,会在之后进行介绍。

日期绘制

上面已经准备好了分类轴,还需要绘制出日期轴,对于周视图而言,一周的实现就非常简单了,根据一周的开始日期,依次渲染7天即可。 注意在绘制过程中提供日期的必要信息给相应事件,一遍使用者能够在事件中进行个性化处理。

{
    // 渲染日历的星期
    _renderWeeks: function () {
        this._weeksEl.innerHTML = '';
        var i = 0,
            currDate = this._startDate.clone(),
            node = document.createElement('div'),
            week;
        node.className = 'ep-weekcalendar-week';

        // 单元格列作为下标记录日期
        this._dateRecords = [];

        while (i++ < 7) {
            // 更新记录日期
            this._dateRecords.push(currDate.clone());

            week = node.cloneNode(true);
            this._renderWeek(currDate, week);
            currDate.add(1, 'day');
        }

        // 切换日期 需要重绘内容区域
        this._rednerContent();
    },

    _renderWeek: function (date, node) {
        var dateText = date.format('YYYY-MM-DD'),
            day = date.isoWeekday();

        if (day > 5) {
            node.className += ' weekend';
        }
        if (date.isSame(this.today, 'day')) {
            node.className += ' today';
        }

        node.setAttribute('data-date', dateText);
        node.setAttribute('date-isoweekday', day);

        var ev = this.fire('dateRender', {
            // 当前完整日期
            date: dateText,
            // iso星期
            isoWeekday: day,
            // 显示的文本
            dateText: '周' + this._WEEKSNAME[day - 1] + ' ' + date.format('MM-DD'),
            // classname
            dateCls: node.className,
            // 日历el
            el: this.el,
            // 当前el
            dateEl: node
        });

        // 处理事件的修改
        node.innerHTML = ev.dateText;
        node.className = ev.dateCls;

        this._weeksEl.appendChild(node);

        this.fire('afterDateRender', {
            // 当前完整日期
            date: dateText,
            // iso星期
            isoWeekday: day,
            // 显示的文本
            dateText: node.innerHTML,
            // classname
            dateCls: node.className,
            // 日历el
            el: this.el,
            // 当前el
            dateEl: node
        });
    }
}

网格和内容

上面已经准备好了二维视图中的两个轴,接着进行网格和内容层的绘制即可。

网格

此处以分类为Y方向(行),日期为X方向(列)来进行绘制:

{
    // 右侧网格
    _renderGrid: function () {
        this._gridEl.innerHTML = '';

        var rowNode = document.createElement('div'),
            itemNode = document.createElement('span'),
            rowsNum = this._categoryData.length,
            i = 0,
            j = 0,
            row, item;

        rowNode.className = 'ep-weekcalendar-grid-row';
        itemNode.className = 'ep-weekcalendar-grid-item';

        while (i < rowsNum) {
            row = rowNode.cloneNode();
            row.setAttribute('data-i', i);
            j = 0;

            while (j < 7) {
                item = itemNode.cloneNode();
                // 周末标识
                if (this.dayStartFromSunday) {
                    if (j === 0 || j === 6) {
                        item.className += ' weekend';
                    }
                } else {
                    if (j > 4) {
                        item.className += ' weekend';
                    }
                }

                item.setAttribute('data-i', i);
                item.setAttribute('data-j', j);
                row.appendChild(item);

                j++;
            }

            this._gridEl.appendChild(row);

            i++;
        }

        rowNode = itemNode = row = item = null;
    }
}

内容

理论上来说,二维要支持跨行、跨列两种情况,即内容区域应该为一整块元素。但是结合到实际情况,跨时间的需求普遍存在(一个东西在一段时间内被连续使用)。跨分类并没有多大的实际意义,本来就要分开以分类来管理,再跨分类,又变得复杂了。而且即使一定要实现一段时间内同时在使用多个东西,也是可以直接实现的(分类A在XX时间段内被使用,B在XX时间段内被使用,只是此时XX正好相同而已)。

因此此处仅处理跨时间情况,可将内容按行即分类进行绘制,这样在插入内容部件时,可以简化很多计算。

{
    // 右侧内容
    _rednerContent: function () {
        this._contentEl.innerHTML = '';

        var i = 0,
            node = document.createElement('div'),
            row;

        node.className = 'ep-weekcalendar-content-row';

        while (i < this._categoryData.length) {
            row = node.cloneNode();
            row.setAttribute('data-i', i);

            this._contentEl.appendChild(row);
            ++i;
        }

        row = node = null;

    },

    // 日期切换时清空内容
    _clearContent: function () {
        var rows = this._contentEl.childNodes,
            i = 0;

        while (i < rows.length) {
            rows[i].innerHTML && (rows[i].innerHTML = '');
            ++i;
        }

        // 部件数据清空
        this._widgetData = {};
    }
}

如果一定要实现跨行跨列的情况,直接将内容绘制成一整块元素即可,但是在点击事件和插入内容部件时,需要同时计算对应的分类和日期时间。

难点实现

内容部件插入

我们实现这个二维周视图日历的主要目的就是要支持插入任意的内容,上面已经准备好了插入内容的dom元素,这里要做的就是将数据绘制成dom放置在合适的位置。

考虑必要的内容部件数据结构如下:

{
    id: '数据标识',
    categoryId: '所属分类标识',
    title: '名称',
    content: '内容',
    start: '开始日期时间'
    end: '结束日期时间'
    bgColor: '展示的背景色'
}

由于上面在内容区域是直接按照分类作为绘制的,因此拿到数据后,对应的分类就已经存在了。重点要根据指定的开始和结束时间计算出开始和结束位置。

考虑如下:

  • 考虑响应式,位置计算按照百分比计算
  • 一周的总时间是固定的,开始日期时间和这周开始日期时间的差额占总时间的百分比即开始位置的百分比
  • 结束日期时间和开始时间的差额占总时间的百分比即为结束时间距离最左侧的百分比
  • 注意处理开始和结束时间溢出本周的情况

因此关于位置计算可以用如下代码处理:

{
    // 日期时间分隔符 默认为空 对应格式为 '2017-11-11 20:00'
    // 对于'2017-11-11T20:00' 这样的格式务必指定正确的日期和时间之间的分隔符T
    _dateTimeSplit:' ',
    // 一周分钟数
    _WEEKMINUTES: 7 * 24 * 60,
    // 一周秒数
    _WEEKSECONDS: 7 * 24 * 3600,
    // 一天的分钟数秒数
    _DAYMINUTES: 24 * 60,
    _DAYSCONDS: 24 * 3600,
    // 计算位置的精度 取值second 或 minute
    posUnit: 'second',
    // 计算指定日期的分钟或秒数
    _getNumByUnits: function (dateStr) {
        var temp = dateStr.split(this._dateTimeSplit),
            date = temp[0];

        // 处理左侧溢出
        if (this._startDate.isAfter(date, 'day')) {
            // 指定日期在开始日期之前
            return 0;
        }
        // 右侧溢出直接算作第7天即可
        var times = (temp[1] || '').split(':'),
            days = (function (startDate) {
                var currDate = startDate.clone(),
                    i = 0,
                    d = moment(date, 'YYYY-MM-DD');
                while (i < 7) {
                    if (currDate.isSame(d, 'day')) {
                        return i;
                    } else {
                        currDate.add(1, 'day');
                        ++i;
                    }
                }

                console && console.error && console.error('计算天数时出错!');
                return i;
            }(this._startDate)),






请到「今天看啥」查看全文