JSP 技巧整理

文章目录

1 JSP 标准标签库(JSTL)

1.1 时间戳显示

  1. taglib 引用 fmt标签库
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
  1. 显示时间戳
<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"/> 
  1. formateDate属性
value要显示的日期
typeDATE, TIME, 或 BOTHdate
dateStyleFULL, LONG, MEDIUM, SHORT, 或 DEFAULTdefault
timeStyleFULL, LONG, MEDIUM, SHORT, 或 DEFAULTdefault
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>

默认校验规则说明

规则取值说明
requiredtrue必输字段
remotecheck.php使用 ajax 方法调用 check.php 验证输入值
emailtrue必须输入正确格式的电子邮件
`url``true必须输入正确格式的网址
daterue必须输入正确格式的日期
dateISOtrue必须输入正确格式的日期(ISO),例如:2009-06-23,1998/01/22 只验证格式,不验证有效性
numbertrue必须输入合法的数字(负数,小数)
digitstrue必须输入整数
creditcard必须输入合法的信用卡号
equalTo#field输入值必须和 #field 相同
accept输入拥有合法后缀名的字符串(上传文件的后缀)
maxlengthn输入长度最多是n的字符串(汉字算一个字符)
minlengthm输入长度最小是10的字符串(汉字算一个字符)
rangelength[n,m]输入长度必须介于 n 和 m 之间的字符串")(汉字算一个字符)
range[n,m]输入值必须介于 n和 m 之间
maxn输入值不能大于 n
minm输入值不能小于 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
ifUncheckedchecked 状态被移除
ifDisabled输入框状态变为 disabled
`ifEnableddisabled 状态被移除
ifCreated输入框被应用了 iCheck 样式
ifDestroyediCheck 样式被移除

使用 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>&nbsp;&nbsp;&nbsp;' +
                    '<a href="#" type="button" class="btn btn-sm btn-primary"><i class="fa fa-edit"></i> 编辑</a>&nbsp;&nbsp;&nbsp;' +
                    '<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>

接口

配置参数
参数取值类型说明
themestring主题,有两个选项:default、vsStyle. 默认:default
expandLevelint树表的展开层次. 默认:1
columnlint可控制列的序号. 默认:0,即第一列
onSelectlfunction拥有 controller 自定义属性的元素的点击事件,return false 则中止展开
beforeExpandlfunction展开子节点前触发的事件
属性说明
属性取值类型说明
idstring行的 id
pIdstring父行的 id
controllerbool指定某一个元素是否可以控制行的展开
hasChild:bool指定某一个 tr 元素是否有孩子(动态加载需用到)
isFirstOnebool指定某一个 tr 元素是否是第一个孩子(自动生成属性,只读)
isLastOnebool指定某一个 tr 元素是否是最后一个孩子(自动生成属性,只读)
prevIdstring前一个兄弟节点的 id(自动生成属性,只读)
depthstring当前行的深度(自动生成属性,只读)
使用方法
$(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 初始化的时候调用,可以用来添加自己的事件监听器
forceFallbackFallback 是一种机制,当浏览器不支持此插件时,提供一个备选方案。默认为 false。如果设为 true,则强制 fallback
fallback一个函数,如果浏览器不支持此插件则调用
国际化
参数说明
dictDefaultMessage没有任何文件被添加的时候的提示文本
dictFallbackMessageallback 情况下的提示文本
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');
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值