为采用Bootstrap框架的后台管理系统制作一张点位排期的表格,基于jQuery封装了一个名为Calendar的插件,总共有3次大的迭代,代码从最初的200多行飙升至1000多行,包括一些工具函数,例如日期格式化、字符串模板、类型判断等。在封装插件前,先用CSS和HTML编写了静态的排期表,在此基础上通过JavaScript改成了动态渲染。

一、点位排期

  刚开始接到的需求比较简单,如下图所示。

  最上面的是日期过滤,可选择上一日或下一日。每页表格显示一个月的数据。第一行是日(天数),红底表示休息天。第一列是版位名称,点击版位名称可显示浮层,包含一张说明图。在单元格中可填入文字、图标或链接,其中白底是可点击的空闲排期,点击后显示一个勾,再点击就会取消;灰底是预排期,蓝底是下单排期,两者都不可点击。

1)服务器数据

  服务器返回的数据是前后端协商后的结构,字段比较多,包含多个数组,例如当月天数、休息天列表、已选中列表、版位信息等,具体如下所示,其中class属性是样式编号,原先是数字,在迭代的过程中改成了数组。

<?php
    $json = [
        'code'=>200,
        'msg'=>'操作成功',
        'data'=>[
            'prev'=>'2016-09',        //上一日
            'next'=>'2016-11',        //下一日
            //当月天数
            'month'=>[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31],
            'holiday'=>[1,2,8,9,15,16,22,23,29,30],        //休息天列表
            'check' => [                    //已选中列表
                '2016-09'=>[                //以日期为key
                    1 => [1,2,3,4,5,6],     //以版位ID为key,天数列表为value
                    2 => [1,2,5,8]
                ],
                '2016-11'=>[
                    1 => [3,4,5,6],
                    2 => [5,8]
                ]
            ],
            'list' =>[
                [
                    'label'=>[            //版位信息
                        'key'=>1,         //ID
                        'img'=>'image/demo.png',        //可弹出的说明图
                        'name'=>'首页文字链接'            //版位名称
                    ],
                    'check'=>[
                        [
                            'day'=>1,                    //天数
                            'class'=>[4],                //空闲排期 已选中 带跳转
                            'url'=>'http://www.pwstrick.com',
                            'price'=>100                 //价格
                        ],
                        [
                            'day'=>2,
                            'class'=>[4],                //空闲排期 已选中
                            'price'=>100
                        ],
                        [
                            'logo'=>'image/logo.gif',
                            'day'=>3,
                            'class'=>[5],                //空闲排期 已选中 带跳转和图标
                            'url'=>'http://www.pwstrick.com',
                            'price'=>100
                        ],
                        [
                            'logo'=>'image/logo.gif',
                            'day'=>4,
                            'class'=>[3],                //空闲排期
                            'price'=>100
                        ],
                        [
                            'day'=>5,
                            'class'=>[2],                //预排期
                            'price'=>100
                            
                        ],
                        [
                            'day'=>6,
                            'class'=>[1],                //下单排期
                            'price'=>100
                        ],
                        ....
                    ]
                ]
            ]
        ]
    ];
    echo json_encode($json);

  在插件中,定义了样式表字典,其含义与服务器返回的class属性一样,如下所列。

this.enums = {          //样式表字典
  1: "primary",         //蓝色 完成状态
  2: "info",            //灰色 审核状态
  3: "calendar-check",  //白色 可打勾
  4: "checked",         //已打勾
  5: "info-blue"        //跳转
};

2)排期渲染

  在初始化Calendar插件时,既可以传递自定义的参数,也可以传递“data-”为前缀的自定义属性,例如异步请求地址data-ajax、当前日期data-cur、控制单元格能否点击的data-disabled等属性。下面是Calendar插件的HTML结构和模板,以及带额外参数的初始化代码。

<script id="template" type="x-tmpl-mustache">
  {{#data}}
  <thead>
    <tr>
      <th></th>
      {{#month}}
      <th>{{.}}</th>
      {{/month}}
    </tr>
  </thead>
  <tbody>
    {{#list}}
    <tr>
      <td data-key="{{label.key}}" data-img="{{&label.img}}">{{label.name}}</td>
      {{#check}}
      <td data-day="{{day}}" class="{{class_name}}" data-remark="{{&remark_name}}" data-dialog="{{dialog}}" data-id="{{id}}" data-width="{{width}}" data-height="{{height}}" data-price="{{price}}">
        {{&img_str}}
      </td>
      {{/check}}
    </tr>
    {{/list}}
  </tbody>
  {{/data}}
</script>
<div id="calendar1" class="calendar-continer" data-loading="img/ajax-loader.gif" data-ajax="ajax/calendar.php" data-cur="2016-10">
  <div class="calendar-header">
    <div class="calendar-date">
      <i class="glyphicon glyphicon-chevron-left" data-date="2016-09"></i>
      <span name="cur-date">2016-10</span>
      <i class="glyphicon glyphicon-chevron-right" data-date="2016-11"></i>
    </div>
  </div>
  <div class="calendar-content">
    <table class="table table-bordered"></table>
  </div>
</div>
<script>
  var calendar1 = new Calendar('calendar1', {
    param: {                 //额外参数 用于ajax请求
      begin: '2017-01-01',
      end: '2017-11-01'
    }
  });
</script>

  Calendar插件包含一个空的table元素,通过mustcache.js模板渲染。在refresh()方法中完成了渲染逻辑,并且将选中的单元格信息缓存到本地。
  当单元格包含备注信息时,会配合tooltip插件,添加提示的效果,如下图所示。

3)缓存数据

  缓存的数据会在initChecked()方法中初始化,其JSON结构如下所示,对选中和未选中的单元格做单独记录。

{
  "2016-10": {            //日期(年月)
    "1": {                //版位ID
      checked: [          //选中列表 day表示年月日中的日,即天数
        { day: 1, class: [4, 3], url: "http://www.pwstrick.com", css: 4 },
        { day: 2, class: [4, 3], css: 4 }
      ],
      unchecked: [3]     //移除列表
    }
  }
};

  每次点击与取消都会触发缓存的修改,在私有的_set()方法中处理缓存数据。

4)优化

  在使用一段时间后,运营觉得第一列和第一行如果固定住,对他们的工作能更加的友好,于是就加了这个特效,具体细节可参考之前的《表格花式效果》一文。

二、版位互斥

  当一个版位和其它多个版位发生互斥时,如果这个版位是预留或下单状态,那么其它版位就无法点击,反之亦然。在服务器返回的数据中新增exclusions属性,如下所示。

<?php
    $json = [
        'code'=>200,
        'msg'=>'操作成功',
        'data'=>[
            'exclusions'=>[
                5 => [3,4]        //版位ID
            ]
        ]
    ];

  为了将插件修改最小化,互斥的逻辑封装到一个checkExclusions()函数中,需要判断的位置就调用该函数。

三、排期类型

  原先只有一个点位排期,后面新增了带购买、带点击数和素材三种排期。一开始没有想到会有多种排期类型,这次迭代修改很多,几乎增加了一倍的代码,并且服务器返回的数据结构也做了调整。

1)初始化

  在插件内部抽象出了_initWithType()方法,专门用于初始化不同类型的排期表,在初始化时还得传入排期类型,不传就渲染为普通的点位排期。

Calendar.prototype._initWithType = function(opts) {
  opts = opts || {};
  var _this = this;
  switch (opts.type) {
    case 'buy':
      this.checked = [4, 6];      //选中的样式
      this.enums = {              //样式表字典
        1: 'primary',
        2: 'info',
        3: 'calendar-check',      //可打勾
        4: 'buy',                 //购买 已打勾
        5: 'info-blue',
        6: 'send'                 //配送
      };
      this._get = function() {
        var type = _this.type;    //排期类型
        var data = {};
        data[type] = {};
        for(var key in _this.cache) {
          for(var key2 in _this.cache[key]) {
            data[type][key2] = data[type][key2] || [];        //每一行的key
            $.each(_this.cache[key][key2]['checked'], function(key3, value) {
              //传对象到服务器 日期和样式
              data[type][key2].push({date:key+'-'+value.day, css:value.css});
            });
          }
        }
        return data;
      };
      cellDbClick(_this);
      break;
    default:
      this.checked = 4;         //选中的样式
      this.enums = {            //样式表字典
        1: "primary",           //蓝色 完成状态
        2: "info",              //灰色 审核状态
        3: "calendar-check",    //白色 可打勾
        4: "checked",           //已打勾
        5: "info-blue"          //跳转
      };
      this._get = function() {
        var type = _this.type;  //排期类型
        var data = {};
        data[type] = {};
        for (var key in _this.cache) {                    //遍历日期,例如2016-10、2016-09
          for (var key2 in _this.cache[key]) {            //先取第一列的版位ID
            data[type][key2] = data[type][key2] || [];    //将表格类型下的版位ID初始化为数组
            $.each(_this.cache[key][key2]["checked"], function(key3, value) {
              //提取每月打勾的日期
              data[type][key2].push(key + "-" + value.day);     //只传日期
            });
          }
        }
        return data;
      };
      cellClick(_this);
      break;
  }
};

  不同的排期表,其样式表字典也是不同的。this._get()是内部用于获取缓存数据的方法,由于每种类型回传给服务器的数据格式不同,因此需要单独处理,但名称可以统一,便于调用。cellDbClick()和cellClick()都是用于绑定单元格点击事件的方法,只是两者内部的处理逻辑不同。

2)带点击数的排期表

  此类排期表需要与artDialog弹框插件配合使用,因为在点击单元格时需要出现两张表单,下图是其中的一张。

  初始化弹框和表单提交的逻辑都封装在showDialog()方法中。后期还优化了日期选择,为了方便,没有使用日历插件,而是结合弹框和选择框,做了个简易的日期选择,如下图所示。

  当只有一种排期表时,复杂度并不高,而当有多种排期表时,许多逻辑就要调整,要么新增,要么适配,并且服务器也要跟着做相应的修改。
  由于数据嵌套了多层,因此在遍历数据转换格式时会比较麻烦。当整合缓存和服务器数据中的单元格样式时,以缓存中的为准,例如缓存中未选中,而服务器是选中,那么忽略服务器中的状态。

Last modification:February 10th, 2020 at 09:42 pm