瀏覽代碼

完善管理员页面逻辑

王育民 5 年之前
父節點
當前提交
59fd623d94

+ 7 - 0
pom.xml

@@ -69,6 +69,13 @@
             <artifactId>spring-boot-starter-thymeleaf</artifactId>
         </dependency>
 
+        <!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 -->
+        <dependency>
+            <groupId>org.thymeleaf.extras</groupId>
+            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
+            <version>3.0.4.RELEASE</version>
+        </dependency>
+
         <!-- 避免 layout:fragment 失效 -->
         <dependency>
             <groupId>nz.net.ultraq.thymeleaf</groupId>

+ 39 - 0
src/main/java/cn/minbb/job/controller/rest/AdminController.java

@@ -0,0 +1,39 @@
+package cn.minbb.job.controller.rest;
+
+import cn.minbb.job.data.ResponseResult;
+import cn.minbb.job.service.WebLogService;
+import cn.minbb.job.util.DateUtil;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Log4j2
+@RestController
+@RequestMapping(value = "/admin")
+public class AdminController {
+
+    private final WebLogService webLogService;
+
+    public AdminController(WebLogService webLogService) {
+        this.webLogService = webLogService;
+    }
+
+    @GetMapping(value = "/overview/pv-uv")
+    public ResponseResult<Serializable> pvUvData(
+            @RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date startDate,
+            @RequestParam(value = "endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date endDate
+    ) {
+        if (null == endDate) endDate = new Date();
+        if (null == startDate) startDate = DateUtil.addOrMinusDays(endDate, -30);
+        List<Map<String, Integer>> mapList = webLogService.selectPvUvByDateBetween(startDate, endDate);
+        return ResponseResult.ok(true).object(mapList).build();
+    }
+}

+ 67 - 3
src/main/java/cn/minbb/job/controller/web/AdminController.java

@@ -1,20 +1,84 @@
 package cn.minbb.job.controller.web;
 
 import cn.minbb.job.data.Const;
+import cn.minbb.job.model.Job;
+import cn.minbb.job.model.User;
+import cn.minbb.job.model.WebLog;
+import cn.minbb.job.service.JobService;
+import cn.minbb.job.service.WebLogService;
+import cn.minbb.job.system.UserSession;
+import cn.minbb.job.util.DateUtil;
+import com.alibaba.fastjson.JSON;
 import lombok.extern.log4j.Log4j2;
+import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.servlet.ModelAndView;
 
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 @Controller
 @Log4j2
 @RequestMapping("/admin")
 public class AdminController {
 
-    @GetMapping("")
-    public ModelAndView adminPage(ModelAndView modelAndView) {
-        modelAndView.setViewName(Const.ViewName.VIEW_ADMIN);
+    private final JobService jobService;
+    private final WebLogService webLogService;
+
+    public AdminController(JobService jobService, WebLogService webLogService) {
+        this.jobService = jobService;
+        this.webLogService = webLogService;
+    }
+
+    /**
+     * 博客概况
+     */
+    @GetMapping(value = {"", "/overview"})
+    public ModelAndView overviewPage(ModelAndView modelAndView) {
+        User user = UserSession.getUserAuthentication();
+        log.error(JSON.toJSONString(user));
+        Date now = new Date();
+        Map<String, Object> data = new HashMap<>();
+        data.put("countA", 200);
+        data.put("countB", 326);
+        data.put("countC", 0);
+        data.put("countD", webLogService.countByDateBetween(DateUtil.getDateOnlyByDateTime(now), now));
+        List<Job> jobList = jobService.findTop8();
+        modelAndView.addObject("jobList", jobList);
+        modelAndView.addObject(Const.Key.KEY_DATA, data);
+        modelAndView.addObject(Const.Key.KEY_ACTIVE, Const.ViewName.VIEW_ADMIN_OVERVIEW);
+        modelAndView.setViewName(Const.ViewName.VIEW_ADMIN_OVERVIEW);
+        return modelAndView;
+    }
+
+    /**
+     * 页面访问日志
+     */
+    @GetMapping(value = "/web/log")
+    public ModelAndView webLogPage(
+            @RequestParam(value = "startTime", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date startTime,
+            @RequestParam(value = "endTime", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date endTime,
+            @RequestParam(value = "method", required = false) String method,
+            @RequestParam(value = "uri", required = false) String uri,
+            @RequestParam(value = "ip", required = false) String ip,
+            @RequestParam(value = "location", required = false) String location,
+            @RequestParam(value = "remark", required = false) String remark,
+            @RequestParam(value = "page", defaultValue = "1", required = false) Integer page,
+            @RequestParam(value = "size", defaultValue = "12", required = false) Integer size,
+            ModelAndView modelAndView
+    ) {
+        if (null == endTime) endTime = new Date();
+        if (null == startTime) startTime = DateUtil.addOrMinusDays(endTime, -1);
+        List<WebLog> webLogList = webLogService.findAllByParamsIn(startTime, endTime, uri, method, null, null, ip, location, remark);
+        modelAndView.addObject(Const.Key.KEY_DATASET, webLogList);
+        modelAndView.addObject(Const.Key.KEY_DATA, method);
+        modelAndView.addObject(Const.Key.KEY_ACTIVE, Const.ViewName.VIEW_ADMIN_WEB_LOG);
+        modelAndView.setViewName(Const.ViewName.VIEW_ADMIN_WEB_LOG);
         return modelAndView;
     }
 }

+ 2 - 0
src/main/java/cn/minbb/job/data/Const.java

@@ -61,6 +61,8 @@ public class Const {
         String VIEW_SCHOOL_INFO = "school-info";
         String VIEW_FEEDBACK = "feedback";
         String VIEW_ADMIN = "admin";
+        String VIEW_ADMIN_OVERVIEW = "admin-overview";
+        String VIEW_ADMIN_WEB_LOG = "admin-web-log";
     }
 
     /**

+ 216 - 0
src/main/java/cn/minbb/job/util/DateUtil.java

@@ -0,0 +1,216 @@
+package cn.minbb.job.util;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+
+public class DateUtil {
+
+    public final static String DATE_FORMAT = "yyyy-MM-dd";
+    public final static String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
+
+    /**
+     * 字符串格式转日期
+     *
+     * @param date yyyy-MM-dd HH:mm:ss
+     */
+    public static Date stringToDate(String date) {
+        try {
+            return new SimpleDateFormat(DATE_TIME_FORMAT).parse(date);
+        } catch (ParseException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 日期格式转字符串
+     *
+     * @return yyyy-MM-dd HH:mm:ss
+     */
+    public static String dateToString(Date date) {
+        return null == date ? "" : new SimpleDateFormat(DATE_TIME_FORMAT).format(date);
+    }
+
+    /**
+     * 计算两个日期差的天数
+     *
+     * @param startTime 开始日期
+     * @param endTime   结束日期
+     * @return 相差天数(整数天)
+     */
+    public static int daysBetween(Date startTime, Date endTime) {
+        Calendar calendarStart = Calendar.getInstance();
+        calendarStart.setTime(endTime);
+        calendarStart.set(Calendar.HOUR_OF_DAY, 0);
+        calendarStart.set(Calendar.MINUTE, 0);
+        calendarStart.set(Calendar.SECOND, 0);
+        Calendar calendarEnd = Calendar.getInstance();
+        calendarEnd.setTime(startTime);
+        calendarEnd.set(Calendar.HOUR_OF_DAY, 0);
+        calendarEnd.set(Calendar.MINUTE, 0);
+        calendarEnd.set(Calendar.SECOND, 0);
+        long millisStart = calendarStart.getTimeInMillis();
+        long millisEnd = calendarEnd.getTimeInMillis();
+        long millisAbs = Math.abs(millisStart - millisEnd);
+        return (int) (millisAbs / (1000 * 86400));
+    }
+
+    /**
+     * 对指定的日期增加或减少指定的秒
+     *
+     * @param date    日期
+     * @param seconds 秒
+     * @return 日期
+     */
+    public static Date addOrMinusSeconds(Date date, int seconds) {
+        GregorianCalendar gregorianCalendar = new GregorianCalendar();
+        gregorianCalendar.setTime(date);
+        gregorianCalendar.add(Calendar.SECOND, seconds);
+        return gregorianCalendar.getTime();
+    }
+
+    /**
+     * 对指定的日期增加或减少指定的分钟
+     *
+     * @param date    日期
+     * @param minutes 分钟
+     * @return 日期
+     */
+    public static Date addOrMinusMinutes(Date date, int minutes) {
+        GregorianCalendar gregorianCalendar = new GregorianCalendar();
+        gregorianCalendar.setTime(date);
+        gregorianCalendar.add(Calendar.MINUTE, minutes);
+        return gregorianCalendar.getTime();
+    }
+
+    /**
+     * 对指定的日期增加或减少指定的小时数
+     *
+     * @param date  日期
+     * @param hours 小时
+     * @return 日期
+     */
+    public static Date addOrMinusHours(Date date, int hours) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.add(Calendar.HOUR_OF_DAY, hours);
+        return calendar.getTime();
+    }
+
+    /**
+     * 对指定的日期增加或减少指定的天数
+     *
+     * @param date 日期
+     * @param days 天数
+     * @return 日期
+     */
+    public static Date addOrMinusDays(Date date, int days) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.add(Calendar.DAY_OF_MONTH, days);
+        return calendar.getTime();
+    }
+
+    /**
+     * 对指定的日期增加或减少指定的月数
+     *
+     * @param date   日期
+     * @param months 月数
+     * @return 日期
+     */
+    public static Date addOrMinusMonths(Date date, int months) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.add(Calendar.MONTH, months);
+        return calendar.getTime();
+    }
+
+    /**
+     * 获取当前日期的字符串格式
+     *
+     * @return 当前日期字符串 yyyy-MM-dd HH:mm:ss
+     */
+    public static String getStringDateOfCurrent() {
+        return dateToString(new Date());
+    }
+
+    /**
+     * 获取日期,去除时间
+     *
+     * @return 日期 yyyy-MM-dd 00:00:00
+     */
+    public static Date getDateOnlyByDateTime(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        return calendar.getTime();
+    }
+
+    /**
+     * 获取当前日期的前一天
+     *
+     * @return 当前日期的前一天
+     */
+    public static Date getBeforeDayOfCurrent() {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(new Date());
+        calendar.add(Calendar.DAY_OF_MONTH, -1);
+        return calendar.getTime();
+    }
+
+    /**
+     * 获取当前日期的后一天
+     *
+     * @return 当前日期的后一天
+     */
+    public static Date getNextDayOfCurrent() {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(new Date());
+        calendar.add(Calendar.DAY_OF_MONTH, 1);
+        return calendar.getTime();
+    }
+
+    /**
+     * 获取当月的第一天
+     *
+     * @return 当月的第一天
+     */
+    public static Date getFirstDateOfMonth(Date date) {
+        if (null == date) date = new Date();
+        Calendar calendar = Calendar.getInstance(Locale.CHINA);
+        calendar.setTime(date);
+        calendar.add(Calendar.MONTH, 0);
+        calendar.set(Calendar.DAY_OF_MONTH, 1);
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        return calendar.getTime();
+    }
+
+    /**
+     * 获取当前月的最后一天
+     *
+     * @return 当前月的最后一天
+     */
+    public static Date getLastDateOfMonth(Date date) {
+        if (null == date) date = new Date();
+        Calendar calendar = Calendar.getInstance(Locale.CHINA);
+        calendar.setTime(date);
+        // 最后一天
+        int lastDate = calendar.getActualMaximum(Calendar.DATE);
+        calendar.set(Calendar.DATE, lastDate);
+        return calendar.getTime();
+
+    }
+
+    public static void main(String[] args) {
+        System.out.println(new SimpleDateFormat(DATE_TIME_FORMAT).format(getFirstDateOfMonth(null)));
+        System.err.println(dateToString(getDateOnlyByDateTime(new Date())));
+        System.err.println(System.currentTimeMillis());
+    }
+}

+ 154 - 0
src/main/resources/static/js/admin-common.js

@@ -0,0 +1,154 @@
+/**
+ * 保存成功
+ * @param title 标题
+ * @param message 提示信息
+ */
+function adminSaveSuccess(title, message) {
+    swal({
+        title: title,
+        text: message,
+        type: "success",
+        confirmButtonText: "好的",
+        confirmButtonColor: "#2B982B"
+    });
+}
+
+/**
+ * 删除失败
+ * @param error 错误信息
+ */
+function adminDeleteError(error) {
+    swal({
+        title: "删除失败",
+        text: error,
+        type: "error",
+        confirmButtonText: "好的",
+        confirmButtonColor: "#2196F3"
+    });
+}
+
+/**
+ * 通知
+ * @param message 提示信息
+ * @param colorName 通知样式
+ */
+function adminNotify(message, colorName) {
+    if (message === null || message === '') message = '';
+    if (colorName === null || colorName === '') colorName = 'bg-black';
+    $.notify({
+        message: message
+    }, {
+        type: colorName,
+        allow_dismiss: true,
+        newest_on_top: true,
+        timer: 1000,
+        placement: {from: 'top', align: 'right'},
+        animate: {enter: 'animated fadeInDown', exit: 'animated fadeOutUp'},
+        template: '' +
+            '<div class="bootstrap-notify-container alert alert-dismissible {0} p-r-35" role="alert" data-notify="container">' +
+            '    <button class="close" type="button" aria-hidden="true" data-notify="dismiss">×</button>' +
+            '    <span data-notify="icon"></span> ' +
+            '    <span data-notify="title">{1}</span> ' +
+            '    <span data-notify="message">{2}</span>' +
+            '    <div class="progress" data-notify="progressbar">' +
+            '        <div class="progress-bar progress-bar-{0}" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0;"></div>' +
+            '    </div>' +
+            '    <a href="{3}" target="{4}" data-notify="url"></a>' +
+            '</div>'
+    });
+}
+
+/**
+ * 内容编辑
+ * @param id 内容 ID
+ */
+function contentEdit(id) {
+    window.location.href = '/admin/content/edit?id=' + id;
+}
+
+/**
+ * 内容预览
+ * @param obj
+ */
+function contentPreview(obj) {
+    window.open('/admin/content/preview?id=' + obj.dataset.id);
+}
+
+$(function () {
+    skinChanger();
+    activateNotificationAndTasksScroll();
+
+    setSkinListHeightAndScroll(true);
+    setSettingListHeightAndScroll(true);
+    $(window).resize(function () {
+        setSkinListHeightAndScroll(false);
+        setSettingListHeightAndScroll(false);
+    });
+});
+
+// Skin changer
+function skinChanger() {
+    $('.right-sidebar .demo-choose-skin li').on('click', function () {
+        var $body = $('body');
+        var $this = $(this);
+
+        var existTheme = $('.right-sidebar .demo-choose-skin li.active').data('theme');
+        $('.right-sidebar .demo-choose-skin li').removeClass('active');
+        $body.removeClass('theme-' + existTheme);
+        $this.addClass('active');
+
+        $body.addClass('theme-' + $this.data('theme'));
+    });
+}
+
+// Skin tab content set height and show scroll
+function setSkinListHeightAndScroll(isFirstTime) {
+    var height = $(window).height() - ($('.navbar').innerHeight() + $('.right-sidebar .nav-tabs').outerHeight());
+    var $el = $('.demo-choose-skin');
+
+    if (!isFirstTime) {
+        $el.slimScroll({destroy: true}).height('auto');
+        $el.parent().find('.slimScrollBar, .slimScrollRail').remove();
+    }
+
+    $el.slimscroll({
+        height: height + 'px',
+        color: 'rgba(0,0,0,0.5)',
+        size: '6px',
+        alwaysVisible: false,
+        borderRadius: '0',
+        railBorderRadius: '0'
+    });
+}
+
+// Setting tab content set height and show scroll
+function setSettingListHeightAndScroll(isFirstTime) {
+    var height = $(window).height() - ($('.navbar').innerHeight() + $('.right-sidebar .nav-tabs').outerHeight());
+    var $el = $('.right-sidebar .demo-settings');
+
+    if (!isFirstTime) {
+        $el.slimScroll({destroy: true}).height('auto');
+        $el.parent().find('.slimScrollBar, .slimScrollRail').remove();
+    }
+
+    $el.slimscroll({
+        height: height + 'px',
+        color: 'rgba(0,0,0,0.5)',
+        size: '6px',
+        alwaysVisible: false,
+        borderRadius: '0',
+        railBorderRadius: '0'
+    });
+}
+
+// Activate notification and task dropdown on top right menu
+function activateNotificationAndTasksScroll() {
+    $('.navbar-right .dropdown-menu .body .menu').slimscroll({
+        height: '254px',
+        color: 'rgba(0,0,0,0.5)',
+        size: '4px',
+        alwaysVisible: false,
+        borderRadius: '0',
+        railBorderRadius: '0'
+    });
+}

+ 26 - 0
src/main/resources/static/js/date-util.js

@@ -0,0 +1,26 @@
+Date.prototype.format = function (format) {
+    var o = {
+        "M+": this.getMonth() + 1,      // 月份
+        "d+": this.getDate(),           // 日
+        "h+": this.getHours(),          // 小时
+        "m+": this.getMinutes(),        // 分
+        "s+": this.getSeconds(),        // 秒
+        "q+": Math.floor((this.getMonth() + 3) / 3),    // 季度
+        "S": this.getMilliseconds()     // 毫秒
+    };
+    if (/(y+)/.test(format)) {
+        format = format.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
+    }
+    for (var k in o) {
+        if (new RegExp("(" + k + ")").test(format)) {
+            format = format.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
+        }
+    }
+    return format;
+};
+
+function addOrMinusDays(date, days) {
+    if (date === undefined) date = new Date();
+    if (days === undefined) days = 0;
+    return new Date(date.getTime() + days * 86400000);
+}

+ 11 - 0
src/main/resources/static/js/util.js

@@ -0,0 +1,11 @@
+function getQueryVariable(variable) {
+    var query = window.location.search.substring(1);
+    var vars = query.split("&");
+    for (var i = 0; i < vars.length; i++) {
+        var pair = vars[i].split("=");
+        if (pair[0] === variable) {
+            return pair[1];
+        }
+    }
+    return "";
+}

+ 215 - 0
src/main/resources/templates/admin-overview.html

@@ -0,0 +1,215 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layouts/admin-layout}">
+<head>
+    <meta charset="UTF-8"/>
+    <title>系统概况</title>
+    <!-- Morris Css -->
+    <link href="https://files.minbb.cn/plugins/morrisjs/morris.css" rel="stylesheet"/>
+    <!--WaitMe Css -->
+    <link href="https://files.minbb.cn/plugins/waitme/waitMe.css" rel="stylesheet"/>
+
+    <!-- Jquery CountTo Plugin Js -->
+    <script src="https://files.minbb.cn/plugins/jquery-countto/jquery.countTo.js"></script>
+    <!-- Morris Plugin Js -->
+    <script src="https://files.minbb.cn/plugins/raphael/raphael.min.js"></script>
+    <script src="https://files.minbb.cn/plugins/morrisjs/morris.js"></script>
+    <!-- Wait Me Plugin Js -->
+    <script src="https://files.minbb.cn/plugins/waitme/waitMe.js"></script>
+</head>
+<body>
+<th:block layout:fragment="content">
+    <div class="container-fluid">
+        <div class="block-header">
+            <h2>系统概况</h2>
+        </div>
+
+        <div class="row clearfix">
+            <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
+                <div class="info-box bg-light-green hover-expand-effect">
+                    <div class="icon">
+                        <i class="material-icons">format_quote</i>
+                    </div>
+                    <div class="content">
+                        <div class="text">发布文章</div>
+                        <div class="number count-to" data-from="0" th:data-to="${DATA.countA}" data-speed="1000" data-fresh-interval="20" th:text="${DATA.countA}"></div>
+                    </div>
+                </div>
+            </div>
+            <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
+                <div class="info-box bg-cyan hover-expand-effect">
+                    <div class="icon">
+                        <i class="material-icons">forum</i>
+                    </div>
+                    <div class="content">
+                        <div class="text">收到留言</div>
+                        <div class="number count-to" data-from="0" th:data-to="${DATA.countB}" data-speed="1000" data-fresh-interval="20" th:text="${DATA.countB}"></div>
+                    </div>
+                </div>
+            </div>
+            <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
+                <div class="info-box bg-pink hover-zoom-effect">
+                    <div class="icon">
+                        <i class="material-icons">cloud_upload</i>
+                    </div>
+                    <div class="content">
+                        <div class="text">上传附件</div>
+                        <div class="number count-to" data-from="0" th:data-to="${DATA.countC}" data-speed="1000" data-fresh-interval="20" th:text="${DATA.countC}"></div>
+                    </div>
+                </div>
+            </div>
+            <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12">
+                <div class="info-box bg-deep-purple hover-zoom-effect">
+                    <div class="icon">
+                        <i class="material-icons">remove_red_eye</i>
+                    </div>
+                    <div class="content">
+                        <div class="text">今日浏览</div>
+                        <div class="number count-to" data-from="0" th:data-to="${DATA.countD}" data-speed="1000" data-fresh-interval="20" th:text="${DATA.countD}"></div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="block-header">
+            <h2>页面访问量</h2>
+        </div>
+
+        <div class="row clearfix">
+            <!-- Line Chart -->
+            <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
+                <div class="card">
+                    <div class="header">
+                        <h2>PV / UV</h2>
+                        <ul class="header-dropdown m-r-0">
+                            <li data-toggle="tooltip" data-placement="top" title="前30天">
+                                <a href="javascript: getData(1);"><i class="material-icons">arrow_back</i></a>
+                            </li>
+                            <li data-toggle="tooltip" data-placement="top" title="后30天">
+                                <a href="javascript: getData(2);"><i class="material-icons">arrow_forward</i></a>
+                            </li>
+                            <li data-toggle="tooltip" data-placement="top" title="往前查看30天">
+                                <a href="javascript: getData(3);" data-toggle="cardloading"><i class="material-icons">playlist_add</i></a>
+                            </li>
+                            <li data-toggle="tooltip" data-placement="top" title="刷新">
+                                <a href="javascript: getData(0);" data-toggle="cardloading"><i class="material-icons">loop</i></a>
+                            </li>
+                            <li data-toggle="tooltip" data-placement="left" title="PV(PageView):页面访问量 UV(UniqueVisitor):独立访问用户数">
+                                <a href="javascript: void(0);"><i class="material-icons">info_outline</i></a>
+                            </li>
+                        </ul>
+                    </div>
+                    <div class="body">
+                        <div class="graph" id="line_chart"></div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="block-header">
+            <h2>最新内容</h2>
+        </div>
+
+        <div class="row clearfix">
+            <div class="col-xs-12 col-sm-6 col-md-6 col-lg-6">
+                <div class="card">
+                    <div class="header"><h2>最新文章</h2></div>
+                    <div class="body">
+                        <div class="align-center" th:if="${jobList.size() == 0}">没有数据</div>
+                        <div class="list-group">
+                            <button class="list-group-item" type="button" th:each="job : ${jobList}"
+                                    th:onclick="'window.open(\'/job?id=' + ${job.getId()} + '\', \'_blank\');'">
+                                <span th:text="${job.getName()}"></span>
+                                <span class="badge bg-green">[[${job.getPriority()}]] 热度</span>
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="col-xs-12 col-sm-6 col-md-6 col-lg-6">
+                <div class="card">
+                    <div class="header"><h2>最新留言</h2></div>
+                    <div class="body">
+                        <div class="align-center" th:if="${jobList.size() == 0}">没有数据</div>
+                        <div class="list-group">
+                            <a class="list-group-item" href="javascript: void(0);" th:each="job : ${jobList}">
+                                <h4 class="list-group-item-heading">
+                                    [[${job.getName()}]] <small th:text="${job.getName()}"></small>
+                                </h4>
+                                <p class="list-group-item-text" th:text="${job.getName()}"></p>
+                            </a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script type="text/javascript">
+        var endDate = new Date(), startDate = addOrMinusDays(endDate, -30);
+        $(function () {
+            $('.count-to').countTo();
+            getData();
+        });
+
+        function getData(which) {
+            var $loading = $('.card').waitMe({
+                effect: 'timer',
+                text: '加载中···',
+                bg: 'rgba(255,255,255,0.90)',
+                color: $.AdminBSB.options.colors['lightBlue']
+            });
+            switch (which) {
+                case 0:
+                    endDate = new Date();
+                    startDate = addOrMinusDays(endDate, -30);
+                    break;
+                case 1:
+                    startDate = addOrMinusDays(startDate, -30);
+                    endDate = addOrMinusDays(endDate, -30);
+                    break;
+                case 2:
+                    startDate = addOrMinusDays(startDate, 30);
+                    endDate = addOrMinusDays(endDate, 30);
+                    break;
+                case 3:
+                    startDate = addOrMinusDays(startDate, -30);
+                    break;
+            }
+            $.ajax({
+                type: 'GET',
+                url: '/admin/overview/pv-uv?startDate=' + startDate.format("yyyy-MM-dd hh:mm:ss") + '&endDate=' + endDate.format("yyyy-MM-dd hh:mm:ss"),
+                contentType: 'application/json; charset=UTF-8',
+                async: true,
+                dataType: 'JSON',
+                success: function (result) {
+                    $loading.waitMe('hide');
+                    if (result && result.success && result.object.length !== 0) {
+                        $("#line_chart").empty();
+                        $("#line_chart svg").remove();
+                        Morris.Line({
+                            element: 'line_chart',
+                            data: result.object,
+                            xkey: 'date',
+                            ykeys: ['pv', 'uv'],
+                            labels: ['PV', 'UV'],
+                            lineColors: ['rgb(233, 30, 99)', 'rgb(0, 188, 212)'],
+                            hideHover: 'auto',
+                            resize: true,
+                            lineWidth: 3
+                        });
+                    } else {
+                    }
+                },
+                error: function (XMLHttpResponse, textStatus, errorThrown) {
+                    $loading.waitMe('hide');
+                }
+            });
+        }
+    </script>
+</th:block>
+</body>
+</html>

+ 259 - 0
src/main/resources/templates/admin-tags.html

@@ -0,0 +1,259 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layouts/admin-layout}">
+<head>
+    <meta charset="UTF-8"/>
+    <meta th:if="${_csrf}" th:name="${_csrf.parameterName}" th:content="${_csrf.token}"/>
+    <title>分类 / 标签</title>
+
+    <style>
+        .btn-group {
+            margin-bottom: 12px;
+        }
+    </style>
+</head>
+<body>
+<th:block layout:fragment="content">
+    <div class="container-fluid">
+        <div class="block-header">
+            <h2>分类 / 标签</h2>
+            <small>括号内数字为分类/标签被引用次数,有引用的分类/标签 <b>不可删除</b></small>
+        </div>
+
+        <div class="row clearfix">
+            <div class="col-xs-12 col-sm-12 col-md-6 col-lg-6">
+                <div class="card">
+                    <div class="header bg-blue">
+                        <h2>分类列表</h2>
+                        <ul class="header-dropdown m-r-0">
+                            <li><a href="javascript: addCategory();"><i class="material-icons">add</i></a></li>
+                        </ul>
+                    </div>
+                    <div class="body">
+                        <div class="row clearfix">
+                            <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="margin-bottom: 8px;">
+                                <div class="btn-group" th:each="categoryVo : ${categoryVoList}">
+                                    <button class="btn btn-info dropdown-toggle waves-effect" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                        [[${categoryVo.getName()}]] ([[${categoryVo.getUsage()}]])
+                                        <span class="caret"></span>
+                                    </button>
+                                    <ul class="dropdown-menu">
+                                        <li>
+                                            <a class="waves-effect waves-block" href="javascript: void(0);" onclick="updateCategory(this);" th:data-id="${categoryVo.getId()}"
+                                               th:data-name="${categoryVo.getName()}" th:data-description="${categoryVo.getDescription()}"
+                                               th:data-priority="${categoryVo.getPriority()}">修改</a>
+                                            <a class="waves-effect waves-block" href="javascript: void(0);" onclick="deleteCategory(this);" th:if="${categoryVo.getUsage() == 0}"
+                                               th:data-id="${categoryVo.getId()}" th:data-name="${categoryVo.getName()}">删除</a>
+                                        </li>
+                                    </ul>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="col-xs-12 col-sm-12 col-md-6 col-lg-6">
+                <div class="card">
+                    <div class="header bg-cyan">
+                        <h2>标签列表</h2>
+                    </div>
+                    <div class="body">
+                        <div class="row clearfix">
+                            <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="margin-bottom: 8px;">
+                                <div class="btn-group" th:each="tagVo : ${tagVoList}">
+                                    <button class="btn btn-info dropdown-toggle waves-effect" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                        [[${tagVo.getName()}]] ([[${tagVo.getUsage()}]])
+                                        <span class="caret" th:if="${tagVo.getUsage() == 0}"></span>
+                                    </button>
+                                    <ul class="dropdown-menu" th:if="${tagVo.getUsage() == 0}">
+                                        <li>
+                                            <a class="waves-effect waves-block" href="javascript: void(0);" onclick="deleteTag(this);" th:data-id="${tagVo.getId()}"
+                                               th:data-name="${tagVo.getName()}">删除</a>
+                                        </li>
+                                    </ul>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="modal fade" id="modal" tabindex="-1" role="dialog">
+        <div class="modal-dialog modal-lg" role="document">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h4 class="modal-title" id="modal-title">新增/修改 分类</h4>
+                </div>
+                <div class="modal-body">
+                    <form>
+                        <div class="form-group form-float">
+                            <div class="form-line">
+                                <label class="form-label" for="name">分类名称</label>
+                                <input class="form-control" id="name" type="text" maxlength="16"/>
+                            </div>
+                        </div>
+                        <div class="form-group form-float">
+                            <div class="form-line">
+                                <label class="form-label" for="description">描述</label>
+                                <textarea class="form-control no-resize" id="description" type="text" maxlength="200" rows="3"></textarea>
+                            </div>
+                        </div>
+                    </form>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-link waves-effect" data-dismiss="modal">取消</button>
+                    <button type="button" class="btn btn-link waves-effect" onclick="saveCategory();">保存</button>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script type="text/javascript">
+        // 获取 CSRF Token
+        var csrfToken = $('meta[name="_csrf"]').attr("content");
+        var modal, modalTitle = $('#modal-title'), categoryNameInput = $('#name'), categoryDescriptionInput = $('#description');
+        var categoryId = null;
+
+        $(function () {
+            modal = $('#modal');
+        });
+
+        function showModal(id, name, description, priority) {
+            modal.modal('show');
+            setTimeout(function () {
+                categoryNameInput.focus();
+            }, 800);
+        }
+
+        function addCategory() {
+            modalTitle.text('新增分类');
+            categoryId = null;
+            categoryNameInput.val('');
+            categoryDescriptionInput.val('');
+            showModal();
+        }
+
+        function updateCategory(obj) {
+            modalTitle.text('修改分类');
+            categoryId = obj.dataset.id;
+            // 赋值
+            var categoryName = obj.dataset.name, categoryDescription = obj.dataset.description;
+            categoryNameInput.val(categoryName);
+            categoryDescriptionInput.val(categoryDescription);
+            showModal(obj.dataset.id, categoryName, categoryDescription, obj.dataset.priority);
+        }
+
+        function saveCategory() {
+            modal.modal('hide');
+            $.ajax({
+                type: 'POST',
+                url: '/admin/category',
+                contentType: 'application/json; charset=UTF-8',
+                data: JSON.stringify({id: categoryId, name: categoryNameInput.val(), description: categoryDescriptionInput.val(), priority: null}),
+                async: false,
+                dataType: 'JSON',
+                beforeSend: function (request) {
+                    if (csrfToken) {
+                        // 添加 CSRF Token
+                        request.setRequestHeader("X-CSRF-TOKEN", csrfToken);
+                    }
+                },
+                success: function (result) {
+                    if (result && result.success) {
+                        window.location.reload();
+                    } else {
+                        adminDeleteError(result.message);
+                    }
+                },
+                error: function (XMLHttpResponse, textStatus, errorThrown) {
+                    adminDeleteError(errorThrown);
+                }
+            });
+        }
+
+        function deleteCategory(obj) {
+            swal({
+                title: "删除分类",
+                text: obj.dataset.name,
+                type: "error",
+                showCancelButton: true,
+                closeOnConfirm: false,
+                showLoaderOnConfirm: true,
+                confirmButtonText: "删除",
+                confirmButtonColor: "#F27474",
+                cancelButtonText: "取消",
+                cancelButtonColor: "#9E9E9E",
+            }, function () {
+                $.ajax({
+                    type: 'DELETE',
+                    url: '/admin/category?id=' + obj.dataset.id,
+                    contentType: 'application/json; charset=UTF-8',
+                    data: JSON.stringify({}),
+                    async: true,
+                    dataType: 'JSON',
+                    beforeSend: function (request) {
+                        if (csrfToken) {
+                            request.setRequestHeader("X-CSRF-TOKEN", csrfToken);
+                        }
+                    },
+                    success: function (result) {
+                        if (result && result.success) {
+                            window.location.reload();
+                        } else {
+                            adminDeleteError(result.message);
+                        }
+                    },
+                    error: function (XMLHttpResponse, textStatus, errorThrown) {
+                        adminDeleteError(errorThrown);
+                    }
+                });
+            });
+        }
+
+        function deleteTag(obj) {
+            swal({
+                title: "删除标签",
+                text: obj.dataset.name,
+                type: "error",
+                showCancelButton: true,
+                closeOnConfirm: false,
+                showLoaderOnConfirm: true,
+                confirmButtonText: "删除",
+                confirmButtonColor: "#F27474",
+                cancelButtonText: "取消",
+                cancelButtonColor: "#9E9E9E",
+            }, function () {
+                $.ajax({
+                    type: 'DELETE',
+                    url: '/admin/tag?id=' + obj.dataset.id,
+                    contentType: 'application/json; charset=UTF-8',
+                    data: JSON.stringify({}),
+                    async: true,
+                    dataType: 'JSON',
+                    beforeSend: function (request) {
+                        if (csrfToken) {
+                            request.setRequestHeader("X-CSRF-TOKEN", csrfToken);
+                        }
+                    },
+                    success: function (result) {
+                        if (result && result.success) {
+                            window.location.reload();
+                        } else {
+                            adminDeleteError(result.message);
+                        }
+                    },
+                    error: function (XMLHttpResponse, textStatus, errorThrown) {
+                        adminDeleteError(errorThrown);
+                    }
+                });
+            });
+        }
+    </script>
+</th:block>
+</body>
+</html>

+ 309 - 0
src/main/resources/templates/admin-web-log.html

@@ -0,0 +1,309 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layouts/admin-layout}">
+<head>
+    <meta charset="UTF-8"/>
+    <title>页面访问日志</title>
+
+    <!-- Bootstrap DatePicker Css -->
+    <link href="https://files.minbb.cn/plugins/bootstrap-datepicker/css/bootstrap-datepicker.css" rel="stylesheet"/>
+    <!-- Bootstrap Datetimepicker Plugin Css -->
+    <link href="https://files.minbb.cn/plugins/bootstrap-datetimepicker/4.17.47/css/bootstrap-datetimepicker-standalone.css" rel="stylesheet"/>
+    <link href="https://files.minbb.cn/plugins/bootstrap-datetimepicker/4.17.47/css/bootstrap-datetimepicker.css" rel="stylesheet"/>
+    <!-- Bootstrap Select Css -->
+    <link href="https://files.minbb.cn/plugins/bootstrap-select/css/bootstrap-select.css" rel="stylesheet"/>
+    <!-- JQuery DataTable Css -->
+    <link href="https://files.minbb.cn/plugins/jquery-datatable/skin/bootstrap/css/dataTables.bootstrap.css" rel="stylesheet"/>
+    <!-- Bootstrap Datepicker Plugin Js -->
+    <!-- <script src="https://files.minbb.cn/plugins/bootstrap-datepicker/js/bootstrap-datepicker.js"></script> -->
+    <!-- <script src="https://files.minbb.cn/plugins/bootstrap-datepicker/locales/bootstrap-datepicker.zh-CN.min.js"></script> -->
+    <!-- Bootstrap Datetimepicker Plugin Js -->
+    <script src="https://files.minbb.cn/plugins/moment/2.24.0/moment.js"></script>
+    <script src="https://files.minbb.cn/plugins/moment/2.24.0/locale/zh-cn.js"></script>
+    <script src="https://files.minbb.cn/plugins/bootstrap-datetimepicker/4.17.47/js/bootstrap-datetimepicker.min.js"></script>
+    <!-- Select Plugin Js -->
+    <script src="https://files.minbb.cn/plugins/bootstrap-select/js/bootstrap-select.js"></script>
+    <!-- Jquery DataTable Plugin Js -->
+    <script src="https://files.minbb.cn/plugins/jquery-datatable/jquery.dataTables.js"></script>
+    <script src="https://files.minbb.cn/plugins/jquery-datatable/skin/bootstrap/js/dataTables.bootstrap.js"></script>
+    <script src="https://files.minbb.cn/plugins/jquery-datatable/extensions/export/dataTables.buttons.min.js"></script>
+    <script src="https://files.minbb.cn/plugins/jquery-datatable/extensions/export/buttons.flash.min.js"></script>
+    <script src="https://files.minbb.cn/plugins/jquery-datatable/extensions/export/jszip.min.js"></script>
+    <script src="https://files.minbb.cn/plugins/jquery-datatable/extensions/export/pdfmake.min.js"></script>
+    <script src="https://files.minbb.cn/plugins/jquery-datatable/extensions/export/vfs_fonts.js"></script>
+    <script src="https://files.minbb.cn/plugins/jquery-datatable/extensions/export/buttons.html5.min.js"></script>
+    <script src="https://files.minbb.cn/plugins/jquery-datatable/extensions/export/buttons.print.min.js"></script>
+    <!-- Others -->
+    <script src="../static/js/util.js" th:src="@{/js/util.js}"></script>
+
+    <style>
+        .dt-buttons {
+            margin-top: 8px;
+        }
+
+        .dataTables_length {
+            float: right;
+        }
+
+        #DataTables_Table_0_paginate {
+            margin-top: 8px;
+        }
+    </style>
+</head>
+<body>
+<th:block layout:fragment="content">
+    <div class="container-fluid">
+        <div class="block-header">
+            <h2>页面访问日志</h2>
+            <small>默认查询 24H 内的访问日志,查询更多请输入查询条件</small>
+        </div>
+
+        <div class="row clearfix">
+            <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
+                <div class="card">
+                    <div class="header">
+                        <h2>日志列表</h2>
+                        <ul class="header-dropdown m-r-0">
+                            <li data-toggle="tooltip" data-placement="left" title="(?) 表示该查询条件为模糊查询">
+                                <a href="javascript: void(0);"><i class="material-icons">info_outline</i></a>
+                            </li>
+                        </ul>
+                    </div>
+                    <div class="body">
+                        <div class="row clearfix">
+                            <div class="col-xs-12 col-sm-6" style="height: 65px;">
+                                <h2 class="card-inside-title">请求时间段</h2>
+                                <div class="input-daterange input-group" id="datepicker_range_container">
+                                    <div class="form-line">
+                                        <input class="form-control" id="input-start-time" type="text" placeholder="开始时间..."/>
+                                    </div>
+                                    <span class="input-group-addon"> - </span>
+                                    <div class="form-line">
+                                        <input class="form-control" id="input-end-time" type="text" placeholder="结束时间..."/>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="col-xs-12 col-sm-3" style="height: 65px;">
+                                <h2 class="card-inside-title">请求方法</h2>
+                                <select class="form-control show-tick" id="select-method">
+                                    <option value="">-- 请选择 --</option>
+                                    <option value="GET" th:selected="${DATA == 'GET'}">GET</option>
+                                    <option value="POST" th:selected="${DATA == 'POST'}">POST</option>
+                                    <option value="HEAD" th:selected="${DATA == 'HEAD'}">HEAD</option>
+                                    <option value="PUT" th:selected="${DATA == 'PUT'}">PUT</option>
+                                    <option value="DELETE" th:selected="${DATA == 'DELETE'}">DELETE</option>
+                                    <option value="OPTIONS" th:selected="${DATA == 'OPTIONS'}">OPTIONS</option>
+                                </select>
+                            </div>
+                            <div class="col-xs-12 col-sm-3" style="height: 65px;">
+                                <h2 class="card-inside-title">备注</h2>
+                                <div class="form-group">
+                                    <div class="form-line">
+                                        <input class="form-control" id="input-remark" type="text" placeholder="备注 (?)" value=""/>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="col-xs-6 col-sm-3">
+                                <div class="form-group form-float">
+                                    <div class="form-line">
+                                        <label class="form-label" for="input-uri">路由 (?)</label>
+                                        <input class="form-control" id="input-uri" type="text"/>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="col-xs-6 col-sm-3">
+                                <div class="form-group form-float">
+                                    <div class="form-line">
+                                        <label class="form-label" for="input-ip">IP地址</label>
+                                        <input class="form-control" id="input-ip" type="text"/>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="col-xs-6 col-sm-3">
+                                <div class="form-group form-float">
+                                    <div class="form-line">
+                                        <label class="form-label" for="input-location">地理位置 (?)</label>
+                                        <input class="form-control" id="input-location" type="text"/>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="col-xs-6 col-sm-3">
+                                <button type="button" class="btn btn-primary m-l-20 waves-effect" onclick="search();">查询</button>
+                            </div>
+                        </div>
+
+                        <div class="table-responsive">
+                            <table class="table table-bordered table-striped table-hover dataTable js-exportable">
+                                <thead>
+                                <tr>
+                                    <th>序号</th>
+                                    <th>访问时间</th>
+                                    <th>请求方法</th>
+                                    <th>请求路由</th>
+                                    <th>协议</th>
+                                    <th>处理时长</th>
+                                    <th>IP地址</th>
+                                    <th>物理位置</th>
+                                    <th>更多</th>
+                                </tr>
+                                </thead>
+                                <tfoot>
+                                <tr>
+                                    <th>序号</th>
+                                    <th>访问时间</th>
+                                    <th>请求方法</th>
+                                    <th>请求路由</th>
+                                    <th>协议</th>
+                                    <th>处理时长</th>
+                                    <th>IP地址</th>
+                                    <th>物理位置</th>
+                                    <th>更多</th>
+                                </tr>
+                                </tfoot>
+                                <tbody>
+                                <tr th:each="webLog, iter : ${DATASET}">
+                                    <td th:text="${iter.count}"></td>
+                                    <td th:text="${#dates.format(webLog.getCreatedAt(), 'yyyy-MM-dd HH:mm:ss')}"></td>
+                                    <td th:text="${webLog.getMethod()}"></td>
+                                    <td th:text="${webLog.getUri().length() > 24 ? webLog.getUri().substring(0, 21) + '...' : webLog.getUri()}"></td>
+                                    <td th:text="${webLog.getProtocol()}"></td>
+                                    <td th:text="${webLog.getProcessTime()}"></td>
+                                    <td th:text="${webLog.getIp()}"></td>
+                                    <td th:text="${webLog.getLocation()}"></td>
+                                    <td class="align-center">
+                                        <a class="col-green" href="javascript: void(0);" onclick="detail(this);" style="text-decoration: none;"
+                                           th:class="${webLog.getRemark() == null}? 'col-blue' : 'col-green'" th:data-uri="${webLog.getUri()}"
+                                           th:data-agent="${webLog.getUserAgent()}" th:data-method="${webLog.getProcessMethod()}" th:data-remark="${webLog.getRemark()}">详情</a>
+                                    </td>
+                                </tr>
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- 详情模态框 -->
+    <div class="modal fade" id="modal" tabindex="-1" role="dialog">
+        <div class="modal-dialog modal-lg" role="document">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h4 class="modal-title">访问详情</h4>
+                </div>
+                <div class="modal-body">
+                    <h5>请求路由</h5>
+                    <p id="data-uri"></p>
+                    <h5>浏览器标识</h5>
+                    <p id="data-agent"></p>
+                    <h5>处理方法</h5>
+                    <p id="data-method"></p>
+                    <h5>备注</h5>
+                    <p id="data-remark"></p>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-link waves-effect" data-dismiss="modal">OK</button>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script th:inline="none">
+        var inputStartTime = $('#input-start-time'), inputEndTime = $('#input-end-time'), selectMethod = $('#select-method'), inputRemark = $('#input-remark'),
+            inputUri = $('#input-uri'), inputIp = $('#input-ip'), inputLocation = $('#input-location');
+        $(function () {
+            // Init Data
+            inputStartTime.val(getQueryVariable('startTime'));
+            inputEndTime.val(getQueryVariable('endTime'));
+            inputRemark.val(decodeURI(getQueryVariable('remark')));
+            inputUri.val(decodeURI(getQueryVariable('uri')));
+            inputIp.val(getQueryVariable('ip'));
+            inputLocation.val(decodeURI(getQueryVariable('location')));
+            // Init View
+            inputStartTime.datetimepicker({
+                format: 'YYYY-MM-DD HH:mm:00'
+            });
+            inputEndTime.datetimepicker({
+                format: 'YYYY-MM-DD HH:mm:59',
+                useCurrent: false
+            });
+            inputStartTime.on("dp.change", function (e) {
+                inputEndTime.data("DateTimePicker").minDate(e.date);
+            });
+            inputEndTime.on("dp.change", function (e) {
+                inputStartTime.data("DateTimePicker").maxDate(e.date);
+            });
+            // $('#datepicker_range_container').datepicker({
+            //     format: 'yyyy-mm-dd 00:00:00',
+            //     language: 'zh-CN',
+            //     clearBtn: true,
+            //     endDate: '1d',
+            //     todayHighlight: true,
+            //     autoclose: true,
+            //     container: '#datepicker_range_container'
+            // });
+            $('.js-exportable').DataTable({
+                dom: 'Bfrtlip',
+                responsive: true,
+                buttons: ['copy', 'csv', 'excel', 'pdf', 'print'],
+                processing: false,          // 是否显示加载中提示
+                autoWidth: false,           // 是否自动计算表格各列宽度
+                info: true,                 // 是否显示页数信息
+                pagingType: "full_numbers", // 分页样式
+                pageLength: 20,             // 默认表格长度
+                searching: true,            // 是否显示搜索框
+                ordering: true,             // 启用排序
+                ajax: '',                   // 异步请求地址
+                serverSide: false,          // 是否从服务器获取数据
+                stateSave: false,           // 页面重载后保持当前页
+                lengthChange: true,         // 表格长度是否可变更
+                lengthMenu: [[10, 25, 50, 100, -1], ["10条", "25条", "50条", "100条", "全部"]],
+                language: {
+                    lengthMenu: "每页显示 _MENU_ 记录",
+                    zeroRecords: "没有匹配的数据",
+                    info: "第 _PAGE_ 页 / 共 _PAGES_ 页 ( 共 _TOTAL_ 条记录 )",
+                    infoEmpty: "没有符合条件的记录",
+                    search: "搜索:",
+                    infoFiltered: "(从 _MAX_ 条记录中过滤)",
+                    paginate: {"first": "首页 ", "last": "末页", "next": "下一页", "previous": "上一页"},
+                    processing: "正在加载..."
+                },
+                scrollX: true,
+                columnDefs: [
+                    {orderable: false, targets: 8}
+                ]
+            });
+        });
+
+        function search() {
+            var url = '/admin/web/log?';
+            var params = {
+                startTime: inputStartTime.val(),
+                endTime: inputEndTime.val(),
+                method: selectMethod.val(),
+                remark: inputRemark.val(),
+                uri: inputUri.val(),
+                ip: inputIp.val(),
+                location: inputLocation.val()
+            };
+            for (var param in params) {
+                if (params[param] !== -1 && params[param] !== '') url += param + "=" + params[param] + "&";
+            }
+            url = url.substring(0, url.length - 1);
+            window.location.href = url;
+        }
+
+        function detail(obj) {
+            $('#data-uri').text(obj.dataset.uri);
+            $('#data-agent').text(obj.dataset.agent);
+            $('#data-method').text(obj.dataset.method);
+            $('#data-remark').text(obj.dataset.remark);
+            $('#modal').modal('show');
+        }
+    </script>
+</th:block>
+</body>
+</html>

+ 586 - 0
src/main/resources/templates/layouts/admin-layout.html

@@ -0,0 +1,586 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+<head>
+    <meta charset="UTF-8"/>
+    <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/>
+    <title></title>
+    <!-- Favicon -->
+    <link rel="icon" href="https://files.minbb.cn/blog/favicon.ico" type="image/x-icon"/>
+
+    <!-- Google Fonts -->
+    <link href="https://fonts.googleapis.com/css?family=Roboto:400,700&subset=latin,cyrillic-ext" rel="stylesheet" type="text/css"/>
+    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" type="text/css"/>
+
+    <!-- Bootstrap Core Css -->
+    <link href="https://files.minbb.cn/plugins/bootstrap/css/bootstrap.css" rel="stylesheet"/>
+    <!-- Waves Effect Css -->
+    <link href="https://files.minbb.cn/plugins/node-waves/waves.css" rel="stylesheet"/>
+    <!-- Animation Css -->
+    <link href="https://files.minbb.cn/plugins/animate-css/animate.css" rel="stylesheet"/>
+    <!-- Sweetalert Css -->
+    <link href="https://files.minbb.cn/plugins/sweetalert/sweetalert.css" rel="stylesheet"/>
+    <!-- Custom Css -->
+    <link href="https://files.minbb.cn/css/style.css" rel="stylesheet"/>
+    <!-- AdminBSB Themes. You can choose a theme from css/themes instead of get all themes -->
+    <link href="https://files.minbb.cn/css/themes/all-themes.css" rel="stylesheet"/>
+
+    <!-- Jquery Core Js -->
+    <script src="https://files.minbb.cn/plugins/jquery/jquery.min.js"></script>
+    <!-- Bootstrap Core Js -->
+    <script src="https://files.minbb.cn/plugins/bootstrap/js/bootstrap.js"></script>
+    <!-- Select Plugin Js -->
+    <script src="https://files.minbb.cn/plugins/bootstrap-select/js/bootstrap-select.js"></script>
+    <!-- Slimscroll Plugin Js -->
+    <script src="https://files.minbb.cn/plugins/jquery-slimscroll/jquery.slimscroll.js"></script>
+    <!-- Waves Effect Plugin Js -->
+    <script src="https://files.minbb.cn/plugins/node-waves/waves.js"></script>
+    <!-- SweetAlert Plugin Js -->
+    <script src="https://files.minbb.cn/plugins/sweetalert/sweetalert.min.js"></script>
+    <!-- Bootstrap Notify Plugin Js -->
+    <script src="https://files.minbb.cn/plugins/bootstrap-notify/bootstrap-notify.js"></script>
+    <!-- Custom Js -->
+    <script src="https://files.minbb.cn/js/admin.js"></script>
+    <script src="/js/date-util.js"></script>
+    <script src="/js/admin-common.js"></script>
+</head>
+<body class="theme-green" th:with="user=${#authentication.principal}">
+<!-- Page Loader -->
+<div class="page-loader-wrapper">
+    <div class="loader">
+        <div class="preloader">
+            <div class="spinner-layer pl-red">
+                <div class="circle-clipper left">
+                    <div class="circle"></div>
+                </div>
+                <div class="circle-clipper right">
+                    <div class="circle"></div>
+                </div>
+            </div>
+        </div>
+        <p>请稍等,加载中...</p>
+    </div>
+</div>
+<!-- #END# Page Loader -->
+<!-- Overlay For Sidebars -->
+<div class="overlay"></div>
+<!-- #END# Overlay For Sidebars -->
+<!-- Top Bar -->
+<nav class="navbar">
+    <div class="container-fluid">
+        <div class="navbar-header">
+            <a href="javascript:void(0);" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse" aria-expanded="false"></a>
+            <a href="javascript:void(0);" class="bars"></a>
+            <a class="navbar-brand" href="/admin/">[[${APP_NAME}]] - 系统管理</a>
+        </div>
+        <div class="collapse navbar-collapse" id="navbar-collapse">
+            <ul class="nav navbar-nav navbar-right">
+                <li data-toggle="tooltip" data-placement="bottom" title="进入博客"><a class="js-search" href="/" target="_blank"><i class="material-icons">web</i></a></li>
+                <!-- Notifications -->
+                <li class="dropdown">
+                    <a href="javascript:void(0);" class="dropdown-toggle" data-toggle="dropdown" role="button">
+                        <i class="material-icons">notifications</i>
+                        <span class="label-count">7</span>
+                    </a>
+                    <ul class="dropdown-menu">
+                        <li class="header">NOTIFICATIONS</li>
+                        <li class="body">
+                            <ul class="menu">
+                                <li>
+                                    <a href="javascript:void(0);">
+                                        <div class="icon-circle bg-light-green">
+                                            <i class="material-icons">person_add</i>
+                                        </div>
+                                        <div class="menu-info">
+                                            <h4>12 new members joined</h4>
+                                            <p>
+                                                <i class="material-icons">access_time</i> 14 mins ago
+                                            </p>
+                                        </div>
+                                    </a>
+                                </li>
+                                <li>
+                                    <a href="javascript:void(0);">
+                                        <div class="icon-circle bg-cyan">
+                                            <i class="material-icons">add_shopping_cart</i>
+                                        </div>
+                                        <div class="menu-info">
+                                            <h4>4 sales made</h4>
+                                            <p>
+                                                <i class="material-icons">access_time</i> 22 mins ago
+                                            </p>
+                                        </div>
+                                    </a>
+                                </li>
+                                <li>
+                                    <a href="javascript:void(0);">
+                                        <div class="icon-circle bg-red">
+                                            <i class="material-icons">delete_forever</i>
+                                        </div>
+                                        <div class="menu-info">
+                                            <h4><b>Nancy Doe</b> deleted account</h4>
+                                            <p>
+                                                <i class="material-icons">access_time</i> 3 hours ago
+                                            </p>
+                                        </div>
+                                    </a>
+                                </li>
+                                <li>
+                                    <a href="javascript:void(0);">
+                                        <div class="icon-circle bg-orange">
+                                            <i class="material-icons">mode_edit</i>
+                                        </div>
+                                        <div class="menu-info">
+                                            <h4><b>Nancy</b> changed name</h4>
+                                            <p>
+                                                <i class="material-icons">access_time</i> 2 hours ago
+                                            </p>
+                                        </div>
+                                    </a>
+                                </li>
+                                <li>
+                                    <a href="javascript:void(0);">
+                                        <div class="icon-circle bg-blue-grey">
+                                            <i class="material-icons">comment</i>
+                                        </div>
+                                        <div class="menu-info">
+                                            <h4><b>John</b> commented your post</h4>
+                                            <p>
+                                                <i class="material-icons">access_time</i> 4 hours ago
+                                            </p>
+                                        </div>
+                                    </a>
+                                </li>
+                                <li>
+                                    <a href="javascript:void(0);">
+                                        <div class="icon-circle bg-light-green">
+                                            <i class="material-icons">cached</i>
+                                        </div>
+                                        <div class="menu-info">
+                                            <h4><b>John</b> updated status</h4>
+                                            <p>
+                                                <i class="material-icons">access_time</i> 3 hours ago
+                                            </p>
+                                        </div>
+                                    </a>
+                                </li>
+                                <li>
+                                    <a href="javascript:void(0);">
+                                        <div class="icon-circle bg-purple">
+                                            <i class="material-icons">settings</i>
+                                        </div>
+                                        <div class="menu-info">
+                                            <h4>Settings updated</h4>
+                                            <p>
+                                                <i class="material-icons">access_time</i> Yesterday
+                                            </p>
+                                        </div>
+                                    </a>
+                                </li>
+                            </ul>
+                        </li>
+                        <li class="footer">
+                            <a href="javascript:void(0);">View All Notifications</a>
+                        </li>
+                    </ul>
+                </li>
+                <!-- #END# Notifications -->
+                <!-- Tasks -->
+                <li class="dropdown">
+                    <a href="javascript:void(0);" class="dropdown-toggle" data-toggle="dropdown" role="button">
+                        <i class="material-icons">flag</i>
+                        <span class="label-count">9</span>
+                    </a>
+                    <ul class="dropdown-menu">
+                        <li class="header">TASKS</li>
+                        <li class="body">
+                            <ul class="menu tasks">
+                                <li>
+                                    <a href="javascript:void(0);">
+                                        <h4>
+                                            Footer display issue
+                                            <small>32%</small>
+                                        </h4>
+                                        <div class="progress">
+                                            <div class="progress-bar bg-pink" role="progressbar" aria-valuenow="85" aria-valuemin="0" aria-valuemax="100" style="width: 32%">
+                                            </div>
+                                        </div>
+                                    </a>
+                                </li>
+                                <li>
+                                    <a href="javascript:void(0);">
+                                        <h4>
+                                            Make new buttons
+                                            <small>45%</small>
+                                        </h4>
+                                        <div class="progress">
+                                            <div class="progress-bar bg-cyan" role="progressbar" aria-valuenow="85" aria-valuemin="0" aria-valuemax="100" style="width: 45%">
+                                            </div>
+                                        </div>
+                                    </a>
+                                </li>
+                                <li>
+                                    <a href="javascript:void(0);">
+                                        <h4>
+                                            Create new dashboard
+                                            <small>54%</small>
+                                        </h4>
+                                        <div class="progress">
+                                            <div class="progress-bar bg-teal" role="progressbar" aria-valuenow="85" aria-valuemin="0" aria-valuemax="100" style="width: 54%">
+                                            </div>
+                                        </div>
+                                    </a>
+                                </li>
+                                <li>
+                                    <a href="javascript:void(0);">
+                                        <h4>
+                                            Solve transition issue
+                                            <small>65%</small>
+                                        </h4>
+                                        <div class="progress">
+                                            <div class="progress-bar bg-orange" role="progressbar" aria-valuenow="85" aria-valuemin="0" aria-valuemax="100" style="width: 65%">
+                                            </div>
+                                        </div>
+                                    </a>
+                                </li>
+                                <li>
+                                    <a href="javascript:void(0);">
+                                        <h4>
+                                            Answer GitHub questions
+                                            <small>92%</small>
+                                        </h4>
+                                        <div class="progress">
+                                            <div class="progress-bar bg-purple" role="progressbar" aria-valuenow="85" aria-valuemin="0" aria-valuemax="100" style="width: 92%">
+                                            </div>
+                                        </div>
+                                    </a>
+                                </li>
+                            </ul>
+                        </li>
+                        <li class="footer">
+                            <a href="javascript:void(0);">View All Tasks</a>
+                        </li>
+                    </ul>
+                </li>
+                <!-- #END# Tasks -->
+                <li class="pull-right"><a href="javascript:void(0);" class="js-right-sidebar" data-close="true"><i class="material-icons">more_vert</i></a></li>
+            </ul>
+        </div>
+    </div>
+</nav>
+<!-- #Top Bar -->
+<section>
+    <!-- Left Sidebar -->
+    <aside id="leftsidebar" class="sidebar">
+        <!-- User Info -->
+        <div class="user-info" style="background: #4CAF50;">
+            <div class="image">
+                <img alt="User" src="" width="56" height="56" th:alt="${user.name}" th:src="${user.avatar}"/>
+            </div>
+            <div class="info-container" style="top: 16px;">
+                <div class="name" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" th:text="${user.name}"></div>
+                <div class="email" th:text="${user.mail}"></div>
+                <div class="btn-group user-helper-dropdown">
+                    <i class="material-icons" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">keyboard_arrow_down</i>
+                    <ul class="dropdown-menu pull-right">
+                        <li><a href="javascript:void(0);"><i class="material-icons">person</i>我的主页</a></li>
+                        <li class="divider" role="separator"></li>
+                        <li><a href="javascript:void(0);" onclick="logout();"><i class="material-icons">input</i>退出登录</a></li>
+                    </ul>
+                </div>
+            </div>
+        </div>
+        <!-- #User Info -->
+        <!-- Menu -->
+        <div class="menu">
+            <ul class="list">
+                <li class="header">导航</li>
+                <li th:class="${ACTIVE == 'admin-overview'}? 'active'">
+                    <a href="/admin/overview">
+                        <i class="material-icons">assessment</i>
+                        <span>系统概况</span>
+                    </a>
+                </li>
+                <li th:class="${ACTIVE == 'admin-post-edit'}? 'active'">
+                    <a href="/admin/content/edit">
+                        <i class="material-icons">create</i>
+                        <span>发布内容</span>
+                    </a>
+                </li>
+                <li th:class="${ACTIVE == 'admin-post'}? 'active'">
+                    <a href="/admin/post">
+                        <i class="material-icons">view_list</i>
+                        <span>文章管理</span>
+                    </a>
+                </li>
+                <li th:class="${ACTIVE == 'admin-page'}? 'active'">
+                    <a href="/admin/page">
+                        <i class="material-icons">description</i>
+                        <span>页面管理</span>
+                    </a>
+                </li>
+                <li th:class="${ACTIVE == 'admin-comments'}? 'active'">
+                    <a href="/admin/comments">
+                        <i class="material-icons">forum</i>
+                        <span>评论管理</span>
+                    </a>
+                </li>
+                <li th:class="${ACTIVE == 'admin-attachments'}? 'active'">
+                    <a href="/admin/attachments">
+                        <i class="material-icons">cloud_upload</i>
+                        <span>附件管理</span>
+                    </a>
+                </li>
+                <li th:class="${ACTIVE == 'admin-tags'}? 'active'">
+                    <a href="/admin/tags">
+                        <i class="material-icons">bookmark</i>
+                        <span>分类 / 标签</span>
+                    </a>
+                </li>
+                <li th:class="${ACTIVE == 'admin-recycle-post' || ACTIVE  == 'admin-recycle-page' || ACTIVE  == 'admin-recycle-comment'}? 'active'">
+                    <a href="javascript:void(0);" class="menu-toggle">
+                        <i class="material-icons">delete_forever</i>
+                        <span>回收站</span>
+                    </a>
+                    <ul class="ml-menu">
+                        <li th:class="${ACTIVE == 'admin-recycle-post'}? 'active'"><a href="/admin/recycle/post">文章回收站</a></li>
+                        <li th:class="${ACTIVE == 'admin-recycle-page'}? 'active'"><a href="/admin/recycle/page">页面回收站</a></li>
+                        <li th:class="${ACTIVE == 'admin-recycle-comment'}? 'active'"><a href="/admin/recycle/comment">评论回收站</a></li>
+                    </ul>
+                </li>
+                <li th:class="${ACTIVE == 'quote-category' || ACTIVE == 'quote-tag' || ACTIVE  == 'admin-file-url'}? 'active'">
+                    <a href="javascript:void(0);" class="menu-toggle">
+                        <i class="material-icons">widgets</i>
+                        <span>其他</span>
+                    </a>
+                    <ul class="ml-menu">
+                        <li>
+                            <a href="javascript:void(0);" class="menu-toggle" th:classappend="${ACTIVE == 'quote-category' || ACTIVE == 'quote-tag'}? 'toggled'">分类 / 标签 引用</a>
+                            <ul class="ml-menu">
+                                <li th:class="${ACTIVE == 'quote-category'}? 'active'"><a href="/admin/quote/category">分类引用</a></li>
+                                <li th:class="${ACTIVE == 'quote-tag'}? 'active'"><a href="/admin/quote/tag">标签引用</a></li>
+                            </ul>
+                        </li>
+                        <li th:class="${ACTIVE == 'admin-file-url'}? 'active'"><a href="/admin/file/url">文件链接</a></li>
+                    </ul>
+                </li>
+                <li th:class="${ACTIVE == 'admin-system-config'}? 'active'">
+                    <a href="/admin/system/config">
+                        <i class="material-icons">settings</i>
+                        <span>系统设置</span>
+                    </a>
+                </li>
+                <li class="header">数据统计</li>
+                <li th:class="${ACTIVE == 'admin-web-log'}? 'active'">
+                    <a href="/admin/web/log">
+                        <i class="material-icons col-light-blue">donut_large</i>
+                        <span>页面访问日志</span>
+                    </a>
+                </li>
+                <li>
+                    <a href="javascript:void(0);">
+                        <i class="material-icons col-amber">donut_large</i>
+                        <span>告警信息</span>
+                    </a>
+                </li>
+                <li>
+                    <a href="javascript:void(0);">
+                        <i class="material-icons col-red">donut_large</i>
+                        <span>错误记录</span>
+                    </a>
+                </li>
+            </ul>
+        </div>
+        <!-- #Menu -->
+        <!-- Footer -->
+        <div class="legal">
+            <div class="copyright">
+                &copy; 2018 - [[${#dates.format(new java.util.Date(), 'yyyy')}]] <a href="/">[[${APP_NAME}]] - 渔民开发</a>.
+            </div>
+            <div class="version">
+                <strong>Version: </strong> 1.0.0
+            </div>
+        </div>
+        <!-- #Footer -->
+    </aside>
+    <!-- #END# Left Sidebar -->
+    <!-- Right Sidebar -->
+    <aside id="rightsidebar" class="right-sidebar">
+        <ul class="nav nav-tabs tab-nav-right" role="tablist">
+            <li role="presentation" class="active"><a href="#skins" data-toggle="tab">SKINS</a></li>
+            <li role="presentation"><a href="#settings" data-toggle="tab">SETTINGS</a></li>
+        </ul>
+        <div class="tab-content">
+            <div role="tabpanel" class="tab-pane fade in active in active" id="skins">
+                <ul class="demo-choose-skin">
+                    <li data-theme="red">
+                        <div class="red"></div>
+                        <span>Red</span>
+                    </li>
+                    <li data-theme="pink">
+                        <div class="pink"></div>
+                        <span>Pink</span>
+                    </li>
+                    <li data-theme="purple">
+                        <div class="purple"></div>
+                        <span>Purple</span>
+                    </li>
+                    <li data-theme="deep-purple">
+                        <div class="deep-purple"></div>
+                        <span>Deep Purple</span>
+                    </li>
+                    <li data-theme="indigo">
+                        <div class="indigo"></div>
+                        <span>Indigo</span>
+                    </li>
+                    <li data-theme="blue">
+                        <div class="blue"></div>
+                        <span>Blue</span>
+                    </li>
+                    <li data-theme="light-blue">
+                        <div class="light-blue"></div>
+                        <span>Light Blue</span>
+                    </li>
+                    <li data-theme="cyan">
+                        <div class="cyan"></div>
+                        <span>Cyan</span>
+                    </li>
+                    <li data-theme="teal">
+                        <div class="teal"></div>
+                        <span>Teal</span>
+                    </li>
+                    <li data-theme="green" class="active">
+                        <div class="green"></div>
+                        <span>Green</span>
+                    </li>
+                    <li data-theme="light-green">
+                        <div class="light-green"></div>
+                        <span>Light Green</span>
+                    </li>
+                    <li data-theme="lime">
+                        <div class="lime"></div>
+                        <span>Lime</span>
+                    </li>
+                    <li data-theme="yellow">
+                        <div class="yellow"></div>
+                        <span>Yellow</span>
+                    </li>
+                    <li data-theme="amber">
+                        <div class="amber"></div>
+                        <span>Amber</span>
+                    </li>
+                    <li data-theme="orange">
+                        <div class="orange"></div>
+                        <span>Orange</span>
+                    </li>
+                    <li data-theme="deep-orange">
+                        <div class="deep-orange"></div>
+                        <span>Deep Orange</span>
+                    </li>
+                    <li data-theme="brown">
+                        <div class="brown"></div>
+                        <span>Brown</span>
+                    </li>
+                    <li data-theme="grey">
+                        <div class="grey"></div>
+                        <span>Grey</span>
+                    </li>
+                    <li data-theme="blue-grey">
+                        <div class="blue-grey"></div>
+                        <span>Blue Grey</span>
+                    </li>
+                    <li data-theme="black">
+                        <div class="black"></div>
+                        <span>Black</span>
+                    </li>
+                </ul>
+            </div>
+            <div role="tabpanel" class="tab-pane fade" id="settings">
+                <div class="demo-settings">
+                    <p>GENERAL SETTINGS</p>
+                    <ul class="setting-list">
+                        <li>
+                            <span>Report Panel Usage</span>
+                            <div class="switch">
+                                <label><input type="checkbox" checked><span class="lever"></span></label>
+                            </div>
+                        </li>
+                        <li>
+                            <span>Email Redirect</span>
+                            <div class="switch">
+                                <label><input type="checkbox"><span class="lever"></span></label>
+                            </div>
+                        </li>
+                    </ul>
+                    <p>SYSTEM SETTINGS</p>
+                    <ul class="setting-list">
+                        <li>
+                            <span>Notifications</span>
+                            <div class="switch">
+                                <label><input type="checkbox" checked><span class="lever"></span></label>
+                            </div>
+                        </li>
+                        <li>
+                            <span>Auto Updates</span>
+                            <div class="switch">
+                                <label><input type="checkbox" checked><span class="lever"></span></label>
+                            </div>
+                        </li>
+                    </ul>
+                    <p>ACCOUNT SETTINGS</p>
+                    <ul class="setting-list">
+                        <li>
+                            <span>Offline</span>
+                            <div class="switch">
+                                <label><input type="checkbox"><span class="lever"></span></label>
+                            </div>
+                        </li>
+                        <li>
+                            <span>Location Permission</span>
+                            <div class="switch">
+                                <label><input type="checkbox" checked><span class="lever"></span></label>
+                            </div>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </aside>
+    <!-- #END# Right Sidebar -->
+</section>
+
+<section class="content">
+    <div class="container-fluid" layout:fragment="content">
+        <div class="block-header">
+            <h2>内容页</h2>
+        </div>
+    </div>
+</section>
+
+<script type="text/javascript">
+    $(function () {
+        $('[data-toggle="tooltip"]').tooltip({
+            container: 'body'
+        });
+    });
+
+    function logout() {
+        swal({
+            title: "退出登录",
+            text: "确定退出登录吗?",
+            type: "error",
+            showCancelButton: true,
+            closeOnConfirm: false,
+            showLoaderOnConfirm: true,
+            confirmButtonText: "退出",
+            confirmButtonColor: "#F27474",
+            cancelButtonText: "取消",
+            cancelButtonColor: "#9E9E9E",
+        }, function () {
+            window.location.href = '/logout';
+        });
+    }
+</script>
+</body>
+</html>