持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 30 天,点击查看活动详情。
为采用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()方法中。后期还优化了日期选择,为了方便,没有使用日历插件,而是结合弹框和选择框,做了个简易的日期选择,如下图所示。
当只有一种排期表时,复杂度并不高,而当有多种排期表时,许多逻辑就要调整,要么新增,要么适配,并且服务器也要跟着做相应的修改。
由于数据嵌套了多层,因此在遍历数据转换格式时会比较麻烦。当整合缓存和服务器数据中的单元格样式时,以缓存中的为准,例如缓存中未选中,而服务器是选中,那么忽略服务器中的状态。