文章目录
- 1 JSP 标准标签库(JSTL)
- 2 JavaScript 插件
1 JSP 标准标签库(JSTL)
1.1 时间戳显示
- 用
taglib
引用fmt
标签库
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
- 显示时间戳
<fmt:formatDate value="${user.created}" pattern="yyyy-MM-dd HH:mm:ss"/>
<fmt:formatDate value="Sat Aug 01 12:28:39 CST 2015" pattern="yyyy-MM-dd HH:mm:ss"/>
- formateDate属性
value | 要显示的日期 | 是 | 无 |
---|---|---|---|
type | DATE, TIME, 或 BOTH | 否 | date |
dateStyle | FULL, LONG, MEDIUM, SHORT, 或 DEFAULT | 否 | default |
timeStyle | FULL, LONG, MEDIUM, SHORT, 或 DEFAULT | 否 | default |
pattern | 自定义格式模式 | 否 | 无 |
timeZone | 显示日期的时区 | 否 | 默认时区 |
var | 存储格式化日期的变量名 | 否 | 显示在页面 |
scope | 存储格式化日志变量的范围 | 否 | 页面 |
1.2 引用其他jsp文件
<jsp:include page="includes/footer.jsp"/>
1.3 spring支持的表单标签
《走向单体地狱(八):Spring MVC 》
中的 Spring MVC 表单标签库
和 Spring MVC @ModelAttribute
联合应用
2 JavaScript 插件
2.1 jQuery Validation 表单验证框架
页面引用
<!-- jQuery Validation 1.14.0 -->
<script src="/static/assets/plugins/jquery-validation/js/jquery.validate.js"></script>
<script src="/static/assets/plugins/jquery-validation/js/additional-methods.js"></script>
<script src="/static/assets/plugins/jquery-validation/js/localization/messages_zh.js"></script>
默认校验规则说明
规则 | 取值 | 说明 |
---|---|---|
required | true | 必输字段 |
remote | check.php | 使用 ajax 方法调用 check.php 验证输入值 |
email | true | 必须输入正确格式的电子邮件 |
`url`` | true | 必须输入正确格式的网址 |
date | rue | 必须输入正确格式的日期 |
dateISO | true | 必须输入正确格式的日期(ISO),例如:2009-06-23,1998/01/22 只验证格式,不验证有效性 |
number | true | 必须输入合法的数字(负数,小数) |
digits | true | 必须输入整数 |
creditcard | 必须输入合法的信用卡号 | |
equalTo | #field | 输入值必须和 #field 相同 |
accept | 输入拥有合法后缀名的字符串(上传文件的后缀) | |
maxlength | n | 输入长度最多是n的字符串(汉字算一个字符) |
minlength | m | 输入长度最小是10的字符串(汉字算一个字符) |
rangelength | [n,m] | 输入长度必须介于 n 和 m 之间的字符串")(汉字算一个字符) |
range | [n,m] | 输入值必须介于 n和 m 之间 |
max | n | 输入值不能大于 n |
min | m | 输入值不能小于 m |
使用案例
初始化
給input元素的class 添加校验class
<input path="username" class="form-control required" placeholder="用户名" />
定义初始化方法并调用即可
/**
* 初始化 jquery validation
*/
var handlerInitValidate = function () {
$("#inputForm").validate({
errorElement: 'span', //定义错误元素为span 位于 #inputForm 表单中
errorClass: 'help-block', //定义错误样式的class为 help-block
errorPlacement: function (error, element) {
//定义触发错误后的action error是上面定义的错误元素 element是错误触发元素既input元素
element.parent().parent().attr("class", "form-group has-error"); //替换样式
error.insertAfter(element); //把error元素插入input元素后面
}
});
};
自定义校验规则
//手机号校验规则
$.validator.addMethod("mobile", function(value, element) {
var length = value.length;
var mobile = /^(((13[0-9]{1})|(15[0-9]{1}))+\d{8})$/;
return this.optional(element) || (length == 11 && mobile.test(value));
}, "手机号码格式错误");
封装校验器
/**
* 函数对象 之所以要声明对象,是利用了对象的闭包特性,保证安全性
*/
var Validate = function () {
/**
* 初始化 jquery validation
*/
var handlerInitValidate = function () {
$("#inputForm").validate({
errorElement: 'span', //定义错误元素为span 位于 #inputForm 表单中
errorClass: 'help-block', //定义错误样式的class为 help-block
errorPlacement: function (error, element) {
//定义触发错误后的action error是上面定义的错误元素 element是错误触发元素既input元素
element.parent().parent().attr("class", "form-group has-error"); //替换样式
error.insertAfter(element); //把error元素插入input元素后面
}
});
};
/**
* 增加自定义验证规则
*/
var handlerInitCustomValidate = function () {
$.validator.addMethod("mobile", function(value, element) {
var length = value.length;
var mobile = /^(((13[0-9]{1})|(15[0-9]{1}))+\d{8})$/;
return this.optional(element) || (length == 11 && mobile.test(value));
}, "手机号码格式错误");
};
/**
* return 外面的都是私有方法,return里面的都是公共方法
*/
return {
/**
* 初始化
*/
init: function () {
handlerInitCustomValidate();
handlerInitValidate();
}
}
}();
$(document).ready(function () {
Validate.init();
});
2.2 jQuery iCheck 单选/复选美化插件
页面引用
CSS 部分
<!-- iCheck for checkboxes and radio inputs -->
<link rel="stylesheet" href="/static/assets/plugins/iCheck/all.css">
JS 部分
<!-- iCheck 1.0.1 -->
<script src="/static/assets/plugins/iCheck/icheck.min.js"></script>
激活 iCheck
默认情况下 iCheck 是不生效的,需要使用 JS 代码激活,此过程可以指定 iCheck 的皮肤,案例代码如下:
CSS 部分
<input type="checkbox" class="minimal" />
JS 部分
// 激活 iCheck
$('input[type="checkbox"].minimal, input[type="radio"].minimal').iCheck({
checkboxClass: 'icheckbox_minimal-blue',
radioClass : 'iradio_minimal-blue'
});
回调事件
iCheck 提供了大量回调事件,都可以用来监听 change 事件
事件名称 | 使用时机 |
---|---|
ifClicked | 用户点击了自定义的输入框或与其相关联的 label |
ifChanged | 输入框的 checked 或 disabled 状态改变了 |
ifChecked | 输入框的状态变为 checked |
ifUnchecked | checked 状态被移除 |
ifDisabled | 输入框状态变为 disabled |
`ifEnabled | disabled 状态被移除 |
ifCreated | 输入框被应用了 iCheck 样式 |
ifDestroyed | iCheck 样式被移除 |
使用 on()
方法绑定事件:
$('input').on('ifChecked', function(event){
alert(event.type + ' callback');
});
方法
下面这些方法可以用来通过编程方式改变输入框状态(可以使用任何选择器):
- $(‘input’).iCheck(‘check’);:将输入框的状态设置为 checked
- $(‘input’).iCheck(‘uncheck’);:移除 checked 状态
- $(‘input’).iCheck(‘toggle’);:toggle checked state
- $(‘input’).iCheck(‘disable’);:将输入框的状态设置为 disabled
- $(‘input’).iCheck(‘enable’);:移除 disabled 状态
- $(‘input’).iCheck(‘update’);:apply input changes, which were done outside the plugin
- $(‘input’).iCheck(‘destroy’);:移除 iCheck 样式
案例代码
全选
_checkboxMaster = $(".checkbox-master");//主选框
_checkbox = $("tbody").find("[type='checkbox']").not("[disabled]");//所有子选框
_checkboxMaster.on("ifClicked", function (e) {//全选事件
// 当前状态已选中,点击后应取消选择
if (e.target.checked) {
_checkbox.iCheck("uncheck");
}
// 当前状态未选中,点击后应全选
else {
_checkbox.iCheck("check");
}
});
判断是否选中
_checkbox.each(function () {
// 判断是否选中
var delFlag = $(this).is(":checked");
if (delFlag) {
_idArray.push($(this).attr("id"));
}
});
2.3 jQuery Datatables 表格插件
中文网站:http://www.datatables.club/
实例索引:http://www.datatables.club/example/
参考手册:http://www.datatables.club/manual/
帮助文档:http://www.datatables.club/reference/
页面引用
CSS 部分
<!-- DataTables -->
<link rel="stylesheet" href="/static/assets/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css">
JS 部分
<!-- DataTables -->
<script src="/static/assets/bower_components/datatables.net/js/jquery.dataTables.min.js"></script>
<script src="/static/assets/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js"></script>
使用,启用 0 配置模式
$('#dataTable').DataTable();
分页查询案例
服务端
MyBatis 映射文件关键代码
DataTables 分页需要提供查询数据的总笔数,以下为查询总笔数的关键代码:
<select id="count" resultType="java.lang.Integer">
SELECT COUNT(*) FROM tb_user
</select>
这里我们使用 MySQL 作为数据库,由于需要传入分页参数,这里我们还使用了 Map 作为查询参数类型,以下为 MySQL 分页查询的关键代码:
<select id="page" resultType="TbUser" parameterType="java.util.Map">
SELECT
<include refid="tbUserColumns" />
FROM
tb_user AS a LIMIT #{page}, #{pageSize}
</select>
定义数据访问接口
/**
* 分页查询
* @param params
* @return
*/
List<TbUser> page(Map<String, Object> params);
/**
* 查询总数
* @return
*/
int count();
定义通用的分页展示对象
创建一个名为 PageInfo
的分页数据展示对象,代码如下:
package com.funtl.my.shop.commons.dto;
import com.funtl.my.shop.commons.persistence.BaseEntity;
import java.io.Serializable;
import java.util.List;
/**
* 分页展示对象
* <p>Title: PageInfo</p>
* <p>Description: </p>
*
* @author Lusifer
* @version 1.0.0
* @date 2018/6/21 5:17
*/
public class PageInfo<T extends BaseEntity> implements Serializable {
private int draw; //标识
private int recordsTotal; //未过滤数据总数
private int recordsFiltered; //过滤后的数据总数
private List<T> data; //返回数据
private String error; //错误提示
public int getDraw() {
return draw;
}
public void setDraw(int draw) {
this.draw = draw;
}
public int getRecordsTotal() {
return recordsTotal;
}
public void setRecordsTotal(int recordsTotal) {
this.recordsTotal = recordsTotal;
}
public int getRecordsFiltered() {
return recordsFiltered;
}
public void setRecordsFiltered(int recordsFiltered) {
this.recordsFiltered = recordsFiltered;
}
public List<T> getData() {
return data;
}
public void setData(List<T> data) {
this.data = data;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
Dao实现代码
@Override
public PageInfo<TbUser> page(Map<String, Object> params) {
PageInfo<TbUser> pageInfo = new PageInfo<>();
int count = tbUserDao.count();
List<TbUser> tbUsers = tbUserDao.page(params);
pageInfo.setRecordsTotal(count);
pageInfo.setRecordsFiltered(count);
pageInfo.setData(tbUsers);
return pageInfo;
}
Controller层关键代码
/**
* 分页查询
*
* @param request
* @return
*/
@ResponseBody
@RequestMapping(value = "page", method = RequestMethod.GET)
public PageInfo<TbUser> page(HttpServletRequest request) {
String draw = request.getParameter("draw");
int start = Integer.parseInt(request.getParameter("start"));
int length = Integer.parseInt(request.getParameter("length"));
Map<String, Object> params = new HashMap<>();
params.put("page", start);
params.put("pageSize", length);
PageInfo<TbUser> pageInfo = tbUserService.page(params);
pageInfo.setDraw(draw == null ? 0 : Integer.parseInt(draw));
return pageInfo;
}
客户端
使用 DataTables
分页功能,需要开启一些列的相关配置,分页的数据结果是由 Ajax
请求获取并解析 JSON
格式数据自动封装进表格的,代码如下:
$("#dataTable").DataTable({
// 是否开启本地分页
"paging": true,
// 是否开启本地排序
"ordering": false,
// 是否显示左下角信息
"info": true,
// 是否允许用户改变表格每页显示的记录数
"lengthChange": false,
// 是否显示处理状态(排序的时候,数据很多耗费时间长的话,也会显示这个)
"processing": true,
// 是否允许 DataTables 开启本地搜索
"searching": false,
// 是否开启服务器模式
"serverSide": true,
// 控制 DataTables 的延迟渲染,可以提高初始化的速度
"deferRender": true,
// 增加或修改通过 Ajax 提交到服务端的请求数据
"ajax": {
"url": "/user/page"
},
// 分页按钮显示选项
"pagingType": "full_numbers",
// 设置列的数据源
"columns": [
{
"data": function (row, type, val, meta) {
return '<input id="' + row.id + '" type="checkbox" class="minimal" />';
}
},
{"data": "id"},
{"data": "username"},
{"data": "phone"},
{"data": "email"},
{"data": "updated"},
{
"data": function (row, type, val, meta) {
return '<a href="#" type="button" class="btn btn-sm btn-default"><i class="fa fa-search"></i> 查看</a> ' +
'<a href="#" type="button" class="btn btn-sm btn-primary"><i class="fa fa-edit"></i> 编辑</a> ' +
'<a href="#" type="button" class="btn btn-sm btn-danger"><i class="fa fa-trash-o"></i> 删除</a>'
}
}
],
// 表格重绘的回调函数,激活iCheck插件
"drawCallback": function (settings) {
App.initCheckbox();
},
// 国际化
"language": {
"sProcessing": "处理中...",
"sLengthMenu": "显示 _MENU_ 项结果",
"sZeroRecords": "没有匹配结果",
"sInfo": "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项",
"sInfoEmpty": "显示第 0 至 0 项结果,共 0 项",
"sInfoFiltered": "(由 _MAX_ 项结果过滤)",
"sInfoPostFix": "",
"sSearch": "搜索:",
"sUrl": "",
"sEmptyTable": "表中数据为空",
"sLoadingRecords": "载入中...",
"sInfoThousands": ",",
"oPaginate": {
"sFirst": "首页",
"sPrevious": "上页",
"sNext": "下页",
"sLast": "末页"
},
"oAria": {
"sSortAscending": ": 以升序排列此列",
"sSortDescending": ": 以降序排列此列"
}
}
})
以上内容参考以下文档完成
配置选项:http://www.datatables.club/reference/option/
服务器处理:http://www.datatables.club/manual/server-side.html
设置列的数据源:https://datatables.net/reference/option/columns.data
国际化:http://www.datatables.club/manual/i18n.html
2.4 jQuery TreeTable 树形表格插件
treeTable 是跨浏览器、性能很高的 jQuery 的树表组件,它使用非常简单,只需要引用 jQuery 库和一个 js 文件,接口也很简单。
优点:
- 兼容主流浏览器:支持 IE6 和 IE6+, Firefox, chrome, Opera, Safari
- 接口简洁:在普通表格的基础上增加父子关系的自定义标签就可以
- 组件性能高:内部实现了只绑定了 table 的事件、使用了 css sprite 合并图片等
- 提供两种风格:通过参数来设置风格
页面引用
CSS 部分
<link rel="stylesheet" href="/static/assets/plugins/treeTable/themes/vsStyle/treeTable.min.css" />
JS 部分
<script src="/static/assets/plugins/treeTable/jquery.treeTable.min.js"></script>
接口
配置参数
参数 | 取值类型 | 说明 |
---|---|---|
theme | string | 主题,有两个选项:default、vsStyle. 默认:default |
expandLevel | int | 树表的展开层次. 默认:1 |
columnl | int | 可控制列的序号. 默认:0,即第一列 |
onSelectl | function | 拥有 controller 自定义属性的元素的点击事件,return false 则中止展开 |
beforeExpandl | function | 展开子节点前触发的事件 |
属性说明
属性 | 取值类型 | 说明 |
---|---|---|
id | string | 行的 id |
pId | string | 父行的 id |
controller | bool | 指定某一个元素是否可以控制行的展开 |
hasChild | :bool | 指定某一个 tr 元素是否有孩子(动态加载需用到) |
isFirstOne | bool | 指定某一个 tr 元素是否是第一个孩子(自动生成属性,只读) |
isLastOne | bool | 指定某一个 tr 元素是否是最后一个孩子(自动生成属性,只读) |
prevId | string | 前一个兄弟节点的 id(自动生成属性,只读) |
depth | string | 当前行的深度(自动生成属性,只读) |
使用方法
$(function () {
$("#treeTable").treeTable({
expandLevel: 2,
column: 1
});
});
HTML 结构代码
<table id="treeTable1" style="width:100%">
<tr>
<td style="width:200px;">标题</td>
<td>内容</td>
</tr>
<tr id="1">
<td><span controller="true">1</span></td>
<td>内容</td></tr>
<tr id="2" pId="1">
<td><span controller="true">2</span></td>
<td>内容</td></tr>
<tr id="3" pId="2">
<td>3</td>
<td>内容</td>
</tr>
<tr id="4" pId="2">
<td>4</td>
<td>内容</td>
</tr>
<tr id="5" pId="4">
<td>4.1</td>
<td>内容</td>
</tr>
<tr id="6" pId="1" hasChild="true">
<td>5</td>
<td>注意这个节点是动态加载的</td>
</tr>
<tr id="7">
<td>8</td>
<td>内容</td>
</tr>
</table>
注意事项
这里的 HTML 结构是经过排序的,每行数据必须紧跟其子类目的数据项,结构类似于:
类目 1
类目 1-1
类目 1-2
...
类目 2
类目 2-1
类目 3
类目 4
服务端排序代码如下:
// 调用方法,0 为约定好的根节点
sortList(sourceList, targetList, 0L);
/**
* 排序
* @param sourceList 数据源集合
* @param targetList 排序后的集合
* @param parentId 当前的父级类目 ID
*/
private void sortList(List<TbContentCategory> sourceList, List<TbContentCategory> targetList, Long parentId) {
for (TbContentCategory sourceContentCategory : sourceList) {
if (sourceContentCategory.getParentId().equals(parentId)) {
targetList.add(sourceContentCategory);
// 判断有没有子节点,有则继续追加
if (sourceContentCategory.getParent()) {
for (TbContentCategory tbContentCategory : sourceList) {
if (tbContentCategory.getParentId().equals(sourceContentCategory.getId())) {
sortList(sourceList, targetList, sourceContentCategory.getId());
break;
}
}
}
}
}
}
演示效果
2.4 jQuary DateTime 时间工具类
使用时只需要调用 DateTime函数对象中return的公共方法即可
/**
* 日期时间工具类
* @type {{dateFormat}}
*/
var DateTime = function () {
var patterns = {
PATTERN_ERA: 'G', // Era 标志符 Era strings. For example: "AD" and "BC"
PATTERN_YEAR: 'y', // 年
PATTERN_MONTH: 'M', // 月份
PATTERN_DAY_OF_MONTH: 'd', // 月份的天数
PATTERN_HOUR_OF_DAY1: 'k', // 一天中的小时数(1-24)
PATTERN_HOUR_OF_DAY0: 'H', // 24 小时制,一天中的小时数(0-23)
PATTERN_MINUTE: 'm', // 小时中的分钟数
PATTERN_SECOND: 's', // 秒
PATTERN_MILLISECOND: 'S', // 毫秒
PATTERN_DAY_OF_WEEK: 'E', // 一周中对应的星期,如星期一,周一
PATTERN_DAY_OF_YEAR: 'D', // 一年中的第几天
PATTERN_DAY_OF_WEEK_IN_MONTH: 'F', // 一月中的第几个星期(会把这个月总共过的天数除以7,不够准确,推荐用W)
PATTERN_WEEK_OF_YEAR: 'w', // 一年中的第几个星期
PATTERN_WEEK_OF_MONTH: 'W', // 一月中的第几星期(会根据实际情况来算)
PATTERN_AM_PM: 'a', // 上下午标识
PATTERN_HOUR1: 'h', // 12 小时制 ,am/pm 中的小时数(1-12)
PATTERN_HOUR0: 'K', // 和 h 类型
PATTERN_ZONE_NAME: 'z', // 时区名
PATTERN_ZONE_VALUE: 'Z', // 时区值
PATTERN_WEEK_YEAR: 'Y', // 和 y 类型
PATTERN_ISO_DAY_OF_WEEK: 'u',
PATTERN_ISO_ZONE: 'X'
};
var week = {
'ch': {
"0": "\u65e5",
"1": "\u4e00",
"2": "\u4e8c",
"3": "\u4e09",
"4": "\u56db",
"5": "\u4e94",
"6": "\u516d"
},
'en': {
"0": "Sunday",
"1": "Monday",
"2": "Tuesday",
"3": "Wednesday",
"4": "Thursday",
"5": "Friday",
"6": "Saturday"
}
};
/**
* 获取当前时间
* @returns {string}
*/
var handlerGetCurrentTime = function () {
var today = new Date();
var year = today.getFullYear();
var month = today.getMonth() + 1;
var day = today.getDate();
var hours = today.getHours();
var minutes = today.getMinutes();
var seconds = today.getSeconds();
var timeString = year + "-" + month + "-" + day + " " + hours + ":" + minutes + ":" + seconds;
return timeString;
};
/**
* 比较时间大小
* time1 > time2 return 1
* time1 < time2 return -1
* time1 == time2 return 0
* @param time1
* @param time2
* @returns {number}
*/
var handlerCompareTime = function (time1, time2) {
if (Date.parse(time1.replace(/-/g, "/")) > Date.parse(time2.replace(/-/g, "/"))) {
return 1;
} else if (Date.parse(time1.replace(/-/g, "/")) < Date.parse(time2.replace(/-/g, "/"))) {
return -1;
} else if (Date.parse(time1.replace(/-/g, "/")) == Date.parse(time2.replace(/-/g, "/"))) {
return 0;
}
};
/**
* 是否闰年
* @param year
* @returns {boolean}
*/
var handlerIsLeapYear = function (year) {
return ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0);
};
/**
* 获取某个月的天数,从 0 开始
* @param year
* @param month
* @returns {*}
*/
var handlerGetDaysOfMonth = function (year, month) {
return [31, (this.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
};
/**
* 获取某个月的天数,从 0 开始
* @param year
* @param month
* @returns {number}
*/
var handlerGetDaysOfMonth2 = function (year, month) {
// 将天置为 0,会获取其上个月的最后一天
month = parseInt(month) + 1;
var date = new Date(year, month, 0);
return date.getDate();
};
/**
* 距离现在几天的日期:负数表示今天之前的日期,0 表示今天,整数表示未来的日期
* 如 -1 表示昨天的日期,0 表示今天,2 表示后天
* @param days
* @returns {string}
*/
var handlerFromToday = function (days) {
var today = new Date();
today.setDate(today.getDate() + days);
var date = today.getFullYear() + "-" + (today.getMonth() + 1) + "-" + today.getDate();
return date;
};
/**
* 格式化日期时间
* @param dateTime 需要格式化的日期时间
* @param pattern 格式化的模式,如 yyyy-MM-dd hh(HH):mm:ss.S a k K E D F w W z Z
* @returns {*}
*/
var handlerFormat = function (dateTime, pattern) {
var date = new Date(dateTime);
if (pattern == null || pattern.length == 0) {
return date.toLocaleString();
}
return pattern.replace(/([a-z])\1*/ig, function (matchStr, group1) {
var replacement = "";
switch (group1) {
case patterns.PATTERN_ERA: //G
break;
case patterns.PATTERN_WEEK_YEAR: //Y
case patterns.PATTERN_YEAR: //y
replacement = date.getFullYear();
break;
case patterns.PATTERN_MONTH: //M
var month = date.getMonth() + 1;
replacement = (month < 10 && matchStr.length >= 2) ? "0" + month : month;
break;
case patterns.PATTERN_DAY_OF_MONTH: //d
var days = date.getDate();
replacement = (days < 10 && matchStr.length >= 2) ? "0" + days : days;
break;
case patterns.PATTERN_HOUR_OF_DAY1: //k(1~24)
var hours24 = date.getHours();
replacement = hours24;
break;
case patterns.PATTERN_HOUR_OF_DAY0: //H(0~23)
var hours24 = date.getHours();
replacement = (hours24 < 10 && matchStr.length >= 2) ? "0" + hours24 : hours24;
break;
case patterns.PATTERN_MINUTE: //m
var minutes = date.getMinutes();
replacement = (minutes < 10 && matchStr.length >= 2) ? "0" + minutes : minutes;
break;
case patterns.PATTERN_SECOND: //s
var seconds = date.getSeconds();
replacement = (seconds < 10 && matchStr.length >= 2) ? "0" + seconds : seconds;
break;
case patterns.PATTERN_MILLISECOND: //S
var milliSeconds = date.getMilliseconds();
replacement = milliSeconds;
break;
case patterns.PATTERN_DAY_OF_WEEK: //E
var day = date.getDay();
replacement = week['ch'][day];
break;
case patterns.PATTERN_DAY_OF_YEAR: //D
replacement = dayOfTheYear(date);
break;
case patterns.PATTERN_DAY_OF_WEEK_IN_MONTH: //F
var days = date.getDate();
replacement = Math.floor(days / 7);
break;
case patterns.PATTERN_WEEK_OF_YEAR: //w
var days = dayOfTheYear(date);
replacement = Math.ceil(days / 7);
break;
case patterns.PATTERN_WEEK_OF_MONTH: //W
var days = date.getDate();
replacement = Math.ceil(days / 7);
break;
case patterns.PATTERN_AM_PM: //a
var hours24 = date.getHours();
replacement = hours24 < 12 ? "\u4e0a\u5348" : "\u4e0b\u5348";
break;
case patterns.PATTERN_HOUR1: //h(1~12)
var hours12 = date.getHours() % 12 || 12; //0转为12
replacement = (hours12 < 10 && matchStr.length >= 2) ? "0" + hours12 : hours12;
break;
case patterns.PATTERN_HOUR0: //K(0~11)
var hours12 = date.getHours() % 12;
replacement = hours12;
break;
case patterns.PATTERN_ZONE_NAME: //z
replacement = handlerGetZoneNameValue(date)['name'];
break;
case patterns.PATTERN_ZONE_VALUE: //Z
replacement = handlerGetZoneNameValue(date)['value'];
break;
case patterns.PATTERN_ISO_DAY_OF_WEEK: //u
break;
case patterns.PATTERN_ISO_ZONE: //X
break;
default:
break;
}
return replacement;
});
};
/**
* 计算一个日期是当年的第几天
* @param date
* @returns {number}
*/
var handlerDayOfTheYear = function (date) {
var obj = new Date(date);
var year = obj.getFullYear();
var month = obj.getMonth(); //从0开始
var days = obj.getDate();
var daysArr = [31, (this.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
for (var i = 0; i < month; i++) {
days += daysArr[i];
}
return days;
};
/**
* 获得时区名和值
* @param dateObj
* @returns {{name: string, value: string}}
*/
var handlerGetZoneNameValue = function (dateObj) {
var date = new Date(dateObj);
date = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
var arr = date.toString().match(/([A-Z]+)([-+]\d+:?\d+)/);
var obj = {
'name': arr[1],
'value': arr[2]
};
return obj;
};
return {
getCurrentTime: function () {
return handlerGetCurrentTime();
},
compareTime: function (time1, time2) {
return handlerCompareTime(time1, time2);
},
isLeapYear: function (year) {
return handlerIsLeapYear(year);
},
getDaysOfMonth: function (year, month) {
return handlerGetDaysOfMonth(year, month);
},
getDaysOfMonth2: function (year, month) {
return handlerGetDaysOfMonth2(year, month);
},
fromToday: function (days) {
return handlerFromToday(days);
},
format: function (dateTime, pattern) {
return handlerFormat(dateTime, pattern);
},
dayOfTheYear: function (date) {
return handlerDayOfTheYear(date);
},
getZoneNameValue: function (dateObj) {
return handlerGetZoneNameValue(dateObj);
}
}
}();
2.5 jQuery zTree 树插件
zTree 是一个依靠 jQuery 实现的多功能 “树插件”。优异的性能、灵活的配置、多种功能的组合是 zTree 最大优点,具体用法请参考官方文档。
页面引用
CSS 部分
<link rel="stylesheet" href="/static/assets/plugins/jquery-ztree/css/zTreeStyle/zTreeStyle.min.css" />
JS 部分
<script src="/static/assets/plugins/jquery-ztree/js/jquery.ztree.core-3.5.min.js"></script>
使用方法
开启 zTree 异步加载数据的功能,案例代码如下:
var setting = {
view: {
// 禁止多选
selectedMulti: false
},
async: {
// 开启异步加载功能
enable: true,
// 远程访问地址
url: "/content/category/tree/data",
// 选择父节点时会自动将节点中的参数传回服务器再重新取结果
autoParam: ["id"]
}
};
// 初始化 zTree 控件
$.fn.zTree.init($("#myTree"), setting);
// 绑定事件
$("#btnModalOk").bind("click", function () {
// 获取 zTree 控件
var zTree = $.fn.zTree.getZTreeObj("myTree");
// 获取已选中的节点
var nodes = zTree.getSelectedNodes();
if (nodes.length == 0) {
alert("请先选择一个父节点");
}
else {
var node = nodes[0];
alert(node.id);
}
});
HTML 结构代码
<ul id="myTree" class="ztree"></ul>
服务器关键代码
Controller 层
@ResponseBody
@RequestMapping(value = "tree/data", method = RequestMethod.POST)
public List<TbContentCategory> treeData(String id) {
if (id == null) {
id = "0";
}
List<TbContentCategory> tbContentCategories = tbContentCategoryService.selectByPid(Long.parseLong(id));
return tbContentCategories;
}
Dao 层
查找pid为传入id的List
<select id="selectByPid" resultType="TbContentCategory">
SELECT
a.id,
a.name,
a.status,
a.sort_order AS sortOrder,
a.is_parent AS isParent,
a.created,
a.updated,
IFNULL (b.id, '0') AS "parent.id",
IFNULL (b.name, '/') AS "parent.name"
FROM
tb_content_category AS a
LEFT JOIN tb_content_category AS b ON a.parent_id = b.id
WHERE a.parent_id = #{parentId}
</select>
演示效果
2.6 jQuery Dropzone 拖拽上传插件
Dropzone 是一个开源的 JavaScript 库,提供文件的异步上传功能,并支持拖拽上传功能
页面引用
CSS 部分,其中 basic.min.css 提供了官网的炫酷上传效果
<link rel="stylesheet" href="/static/assets/plugins/dropzone/min/dropzone.min.css" />
<link rel="stylesheet" href="/static/assets/plugins/dropzone/min/basic.min.css" />
JS 部分
<script src="/static/assets/plugins/dropzone/min/dropzone.min.js"></script>
启用 Dropzone
只需要一个 div 元素,用 JavaScript 代码启用即可
HTML 结构如下:
<div id="dropz" class="dropzone"></div>
JavaScript 启用代码如下:
var myDropzone = new Dropzone("#dropz", {
url: "/upload",
dictDefaultMessage: '拖动文件至此或者点击上传', // 设置默认的提示语句
paramName: "dropzFile", // 传到后台的参数名称
init: function () {
this.on("success", function (file, data) {
// 上传成功触发的事件
});
}
});
其中 url 是必须的值,指明文件上传提交到哪个页面。其他的值都是可选的,如果使用默认值的话可以省略。
配置 Dropzone
此插件的特色就在于非常灵活,提供了许多可选项、事件等。下面分类介绍常用的配置项。
配置参数
参数 | 说明 |
---|---|
url | 最重要的参数,指明了文件提交到哪个页面 |
method | 默认为 post,如果需要,可以改为 put |
paramName | 相当于 元素的 name 属性,默认为 file |
maxFilesize | 最大文件大小,单位是 MB |
maxFiles | 默认为 null,可以指定为一个数值,限制最多文件数量 |
addRemoveLinks | 默认 false。如果设为 true,则会给文件添加一个删除链接 |
acceptedFiles | 指明允许上传的文件类型,格式是逗号分隔的 MIME type 或者扩展名。例如:image/*, application/pdf, .psd, .obj |
uploadMultiple | 指明是否允许 Dropzone 一次提交多个文件。默认为 false。如果设为 true,则相当于 HTML 表单添加 multiple 属性 |
headersl | 如果设定,则会作为额外的 header 信息发送到服务器。例如:{“custom-header”: “value”} |
initl | 一个函数,在 Dropzone 初始化的时候调用,可以用来添加自己的事件监听器 |
forceFallback | Fallback 是一种机制,当浏览器不支持此插件时,提供一个备选方案。默认为 false。如果设为 true,则强制 fallback |
fallback | 一个函数,如果浏览器不支持此插件则调用 |
国际化
参数 | 说明 |
---|---|
dictDefaultMessage | 没有任何文件被添加的时候的提示文本 |
dictFallbackMessage | allback 情况下的提示文本 |
dictInvalidInputType | 文件类型被拒绝时的提示文本 |
dictFileTooBig | 文件大小过大时的提示文本 |
dictCancelUpload | 取消上传链接的文本 |
dictCancelUploadConfirmation | 取消上传确认信息的文本 |
dictRemoveFile | 移除文件链接的文本 |
dictMaxFilesExceeded | 超过最大文件数量的提示文本 |
常用事件
以下事件接收 file 为第一个参数
事件 | 说明 |
---|---|
addedfile | 添加了一个文件时发生 |
removedfile | 一个文件被移除时发生。你可以监听这个事件并手动从服务器删除这个文件 |
uploadprogress | 上传时按一定间隔发生这个事件。第二个参数为一个整数,表示进度,从 0 到 100。第三个参数是一个整数,表示发送到服务器的字节数。当一个上传结束时,Dropzone 保证会把进度设为 100。注意:这个函数可能被以同一个进度调用多次 |
success | 文件成功上传之后发生,第二个参数为服务器响应 |
complete | 当文件上传成功或失败之后发生 |
canceled | 当文件在上传时被取消的时候发生 |
maxfilesreached | 当文件数量达到最大时发生 |
maxfilesexceeded | 当文件数量超过限制时发生 |
以下事件接收一个 file list 作为第一个参数(仅当 uploadMultiple 被设为 true 时才会发生)
事件 | 说明 |
---|---|
successmultiple | 成功上传 |
completemultiple | 上传完毕 |
cancelmultiple | 上传失败 |
特殊事件
事件 | 说明 |
---|---|
totaluploadprogress | 第一个参数为总上传进度,第二个参数为总字节数,第三个参数为总上传字节数。 |
使用案例
var myDropzone = new Dropzone("#dropz", {
url: "/upload", // 文件提交地址
method: "post", // 也可用put
paramName: "file", // 默认为file
maxFiles: 1,// 一次性上传的文件数量上限
maxFilesize: 2, // 文件大小,单位:MB
acceptedFiles: ".jpg,.gif,.png,.jpeg", // 上传的类型
addRemoveLinks: true,
parallelUploads: 1,// 一次上传的文件数量
//previewsContainer:"#preview", // 上传图片的预览窗口
dictDefaultMessage: '拖动文件至此或者点击上传',
dictMaxFilesExceeded: "您最多只能上传1个文件!",
dictResponseError: '文件上传失败!',
dictInvalidFileType: "文件类型只能是*.jpg,*.gif,*.png,*.jpeg。",
dictFallbackMessage: "浏览器不受支持",
dictFileTooBig: "文件过大上传文件最大支持.",
dictRemoveLinks: "删除",
dictCancelUpload: "取消",
init: function () {
this.on("addedfile", function (file) {
// 上传文件时触发的事件
});
this.on("success", function (file, data) {
// 上传成功触发的事件
});
this.on("error", function (file, data) {
// 上传失败触发的事件
});
this.on("removedfile", function (file) {
// 删除文件时触发的方法
});
}
});
服务端支持
前端工作做完后,后台需要提供文件上传支持,我们使用 Spring MVC 来接收上传的文件
POM
Spring MVC 上传文件需要 commons-fileupload:commons-fileupload
依赖支持,pom.xml
文件如下:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.2</version>
</dependency>
配置 spring-mvc.xml
需要 Spring 注入 multipartResolver
视图解析器实例,spring-mvc.xml
增加如下配置:
<!-- 上传文件拦截,设置最大上传文件大小 10M = 10*1024*1024(B) = 10485760 bytes -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="10485760"/>
</bean>
Controller关键代码
package com.funtl.my.shop.web.admin.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 文件上传控制器
* <p>Title: UploadController</p>
* <p>Description: </p>
*
* @author Lusifer
* @version 1.0.0
* @date 2018/6/27 0:42
*/
@Controller
public class UploadController {
@ResponseBody
@RequestMapping(value = "upload", method = RequestMethod.POST)
public Map<String, Object> upload(MultipartFile dropzFile, HttpServletRequest request) {
Map<String, Object> result = new HashMap<>();
// 获取上传的原始文件名
String fileName = dropzFile.getOriginalFilename();
// 设置文件上传路径
String filePath = request.getSession().getServletContext().getRealPath("/static/upload");
// 获取文件后缀
String fileSuffix = fileName.substring(fileName.lastIndexOf("."), fileName.length());
// 判断并创建上传用的文件夹
File file = new File(filePath);
if (!file.exists()) {
file.mkdir();
}
// 重新设置文件名为 UUID,以确保唯一
file = new File(filePath, UUID.randomUUID() + fileSuffix);
try {
// 写入文件
dropzFile.transferTo(file);
} catch (IOException e) {
e.printStackTrace();
}
// 返回 JSON 数据,这里只带入了文件名
result.put("fileName", file.getName());
return result;
}
}
2.7 jQuary wangEditor 富文本编辑器
轻量级 web 富文本编辑器,配置方便,使用简单。支持 IE10+ 浏览器。
官网:http://www.wangEditor.com
文档:http://www.kancloud.cn/wangfupeng/wangeditor3/332599
源码:http://github.com/wangfupeng1988/wangEditor
什么是富文本编辑器
富文本编辑器,Rich Text Editor, 简称 RTE, 它提供类似于 Microsoft Word 的编辑功能,容易被不会编写 HTML 的用户并需要设置各种文本格式的用户所喜爱。它的应用也越来越广泛。最先只有 IE 浏览器支持,其它浏览器相继跟进,在功能的丰富性来说,还是 IE 强些。虽然没有一个统一的标准,但对于最基本的功能,各浏览器提供的 API 基本一致,从而使编写一个跨浏览器的富文本编辑器成为可能。
页面引入
CSS 部分
<link rel="stylesheet" href="/static/assets/plugins/wangEditor/wangEditor.min.css" />
JS 部分
<script src="/static/assets/plugins/wangEditor/wangEditor.min.js"></script>
启用 wangEditor
只需要一个 div 元素,用 JavaScript 代码启用即可
HTML 结构如下:
<div id="editor"></div>
JavaScript 启用代码如下:
var E = window.wangEditor;
var editor = new E('#editor');
editor.create();
效果演示
配置完成后,在浏览器端显示如下:
服务端图片上传支持
配置方式同 Dropzone 图片上传插件,参考官方手册完成
控制器关键代码参考
package com.funtl.my.shop.web.admin.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 文件上传控制器
* <p>Title: UploadController</p>
* <p>Description: </p>
*
* @author Lusifer
* @version 1.0.0
* @date 2018/6/27 14:32
*/
@Controller
public class UploadController {
private static final String UPLOAD_PATH = "/static/upload/";
/**
* 文件上传
*
* @return
*/
@ResponseBody
@RequestMapping(value = "upload", method = RequestMethod.POST)
public Map<String, Object> upload(MultipartFile editorFile, HttpServletRequest request) {
Map<String, Object> result = new HashMap<>();
// 获取文件后缀
String fileName = editorFile.getOriginalFilename();
String fileSuffix = fileName.substring(fileName.lastIndexOf("."));
// 文件存放路径
String filePath = request.getSession().getServletContext().getRealPath(UPLOAD_PATH);
// 判断路径是否存在,不存在则创建文件夹
File file = new File(filePath);
if (!file.exists()) {
file.mkdir();
}
// 将文件写入目标
file = new File(filePath, UUID.randomUUID() + fileSuffix);
try {
editorFile.transferTo(file);
} catch (IOException e) {
e.printStackTrace();
}
// 获取服务端路径
String serverPath = String.format("%s://%s:%s%s%s", request.getScheme(), request.getServerName(), request.getServerPort(), request.getContextPath(), UPLOAD_PATH);
// 返回给 wangEditor 的数据格式
result.put("errno", 0);
result.put("data", new String[]{serverPath + file.getName()});
return result;
}
}
相比 Dropzone 图片上传插件 一节,控制器代码的主要差别在于接口返回的数据格式,官方要求的格式如下:
{
// errno 即错误代码,0 表示没有错误。
// 如果有错误,errno != 0,可通过下文中的监听函数 fail 拿到该错误码进行自定义处理
"errno": 0,
// data 是一个数组,返回若干图片的线上地址
"data": [
"图片1地址",
"图片2地址",
"……"
]
}
2.7 jQuery nth-tabs 多功能选项卡插件
2.7.1 其他依赖
滚动条:jquery.scrollbar
字体图标:font-awesome
2.7.2 使用说明
整体基于Bootstrap整合
2.7.2.1 CSS
<link href="https://cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/font-awesome/4.4.0/css/font-awesome.min.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/jquery.scrollbar/0.2.11/jquery.scrollbar.min.css" rel="stylesheet">
<link href="css/nth.tabs.min.css" rel="stylesheet">
2.7.2.2 JS
<script src="https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="https://cdn.bootcss.com/jquery.scrollbar/0.2.11/jquery.scrollbar.min.js"></script>
<script src="js/nth.tabs.min.js"></script>
2.7.2.3 html
<div class="nth-tabs" id="custom-id"></div>
2.7.2.4 初始化
nthTabs = $("#custom-id").nthTabs();
2.7.2.5 添加一个选项卡
nthTabs.addTab({
id:'a',
title:'孙悟空',
content:'看我七十二变',
});
2.7.2.6 添加一个不可关闭的选项卡
nthTabs.addTab({
id:'a',
title:'孙悟空',
content:'看我七十二变',
allowClose:false,
});
2.7.2.7 添加多个选项卡
nthTabs.addTab({
id:'a',
title:'孙悟空',
content:'看我七十二变',
}).addTab({
id:'b',
title:'孙悟空二号',
content:'看我七十三变',
});
2.7.2.8 删除一个选项卡
nthTabs.delTab('id');
2.7.2.9 删除其他选项卡
nthTabs.delOtherTab();
2.7.2.10 删除所有选项卡
nthTabs.delAllTab();
2.7.2.11 切换到指定选项卡
nthTabs.setActTab(id);
2.7.2.12 定位到当前选项卡
nthTabs.locationTab();
2.7.2.13 左滑动
$('.roll-nav-left').click();
2.7.2.14 右滑动
$('.roll-nav-right').click();
2.7.2.15 获取左右滑动步值
nthTabs.getMarginStep();
2.7.2.16 获取当前选项卡ID
nthTabs.getActiveId();
2.7.2.17 获取所有选项卡宽度
nthTabs.getAllTabWidth();
2.7.2.18 获取所有选项卡
nthTabs.nthTabs.getTabList();
2.7.3 附:群里提供的版本
群里提供的版本是为了适应 AdminLTE 而修改的版本,使用方式略有不同
2.7.3.1 CSS
<link href="https://cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/font-awesome/4.4.0/css/font-awesome.min.css" rel="stylesheet">
<link href="https://cdn.bootcss.com/jquery.scrollbar/0.2.11/jquery.scrollbar.min.css" rel="stylesheet">
<link href="css/nth.tabs.css" rel="stylesheet">
主要修改了 nth.tabs.css
部分样式以适应 AdminLTE
2.7.3.2 JS
<script src="https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="https://cdn.bootcss.com/jquery.scrollbar/0.2.11/jquery.scrollbar.min.js"></script>
<script src="js/nth.tabs.min.js"></script>
<script src="js/nth-tabs.js"></script>
主要增加了 nth-tabs.js
这个二次封装的自定义工具类
var NthTabs = function () {
var nthTabs;
var handleInit = function () {
nthTabs = $("#editor-tabs").nthTabs();
};
var handleHome = function (src) {
nthTabs.addTab({
id: "home",
title: "首页",
content: '<iframe name="iframe0" width="100%" height="737" src="' + src + '" frameborder="0"></iframe>',
allowClose: false,
active: true
});
};
var handleAddTab = function (id, title, src) {
// 判断选项卡是否存在
var hasTab = false;
var nthTabList = nthTabs.getTabList();
for (var i = 0 ; i < nthTabList.length ; i++) {
var nthTab = nthTabList[i];
if (nthTab.id == "#" + id) {
nthTabs.setActTab(id);
hasTab = true;
break;
}
}
// 选项卡已存在,返回
if (hasTab) {
return;
}
nthTabs.addTab({
id: id,
title: title,
content: '<iframe name="iframe' + id + '" width="100%" height="737" src="' + src + '" frameborder="0"></iframe>'
});
nthTabs.setActTab(id);
};
return {
init: function () {
handleInit();
},
home: function (src) {
handleHome(src)
},
addTab: function (id, title, src, allowClose, active) {
handleAddTab(id, title, src, allowClose, active);
}
};
}();
jQuery(document).ready(function () {
NthTabs.init();
});
2.7.3.3 html
<div class="nth-tabs common-bg" id="editor-tabs"></div>
2.7.3.4 初始化
$(function () {
NthTabs.home("/path");
});
2.7.3.5 添加一个选项卡
NthTabs.addTab('id', 'name', '/path');