Kaynağa Gözat

用户管理:支持在线维护系统用户

xuxueli 6 yıl önce
ebeveyn
işleme
b3b9ff80d4

+ 1 - 1
doc/XXL-JOB官方文档.md

@@ -1469,7 +1469,7 @@ Tips: 历史版本(V1.3.x)目前已经Release至稳定版本, 进入维护阶段
 - 1、[规划中] 移除quartz:精简底层实现,优化已知问题;
     - 触发:单节点周期性触发,运行事件如delayqueue;
     - 调度:集群竞争,负载方式协同处理,竞争-加入时间轮-释放-竞争;
-- 2、[规划中] 用户管理:支持在线维护系统用户;
+- 2、用户管理:支持在线维护系统用户;
 - 3、[规划中] 权限管理:执行器为粒度分配权限,核心操作校验权限,暂定管理员、普通用户两种角色;
 - 4、调度日志优化:支持设置日志保留天数,过期日志天维度记录报表,并清理;调度报表汇总实时数据和报表;
 - 5、调度线程池参数调优;

Dosya farkı çok büyük olduğundan ihmal edildi
+ 10 - 0
doc/db/tables_xxl_job.sql


+ 124 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java

@@ -0,0 +1,124 @@
+package com.xxl.job.admin.controller;
+
+import com.xxl.job.admin.core.model.XxlJobGroup;
+import com.xxl.job.admin.core.model.XxlJobUser;
+import com.xxl.job.admin.core.util.I18nUtil;
+import com.xxl.job.admin.dao.XxlJobGroupDao;
+import com.xxl.job.admin.dao.XxlJobUserDao;
+import com.xxl.job.core.biz.model.ReturnT;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.util.DigestUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author xuxueli 2019-05-04 16:39:50
+ */
+@Controller
+@RequestMapping("/user")
+public class UserController {
+
+    @Resource
+    private XxlJobUserDao xxlJobUserDao;
+    @Resource
+    private XxlJobGroupDao xxlJobGroupDao;
+
+    @RequestMapping
+    public String index(Model model) {
+
+        // 执行器列表
+        List<XxlJobGroup> groupList = xxlJobGroupDao.findAll();
+        model.addAttribute("groupList", groupList);
+
+        return "user/user.index";
+    }
+
+    @RequestMapping("/pageList")
+    @ResponseBody
+    public Map<String, Object> pageList(@RequestParam(required = false, defaultValue = "0") int start,
+                                        @RequestParam(required = false, defaultValue = "10") int length,
+                                        String username) {
+
+        // page list
+        List<XxlJobUser> list = xxlJobUserDao.pageList(start, length, username);
+        int list_count = xxlJobUserDao.pageListCount(start, length, username);
+
+        // package result
+        Map<String, Object> maps = new HashMap<String, Object>();
+        maps.put("recordsTotal", list_count);		// 总记录数
+        maps.put("recordsFiltered", list_count);	// 过滤后的总记录数
+        maps.put("data", list);  					// 分页列表
+        return maps;
+    }
+
+    @RequestMapping("/add")
+    @ResponseBody
+    public ReturnT<String> add(XxlJobUser xxlJobUser) {
+
+        // valid username
+        if (!StringUtils.hasText(xxlJobUser.getUsername())) {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_username") );
+        }
+        xxlJobUser.setUsername(xxlJobUser.getUsername().trim());
+        if (!(xxlJobUser.getUsername().length()>=4 && xxlJobUser.getUsername().length()<=20)) {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" );
+        }
+        // valid password
+        if (!StringUtils.hasText(xxlJobUser.getPassword())) {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_password") );
+        }
+        xxlJobUser.setPassword(xxlJobUser.getPassword().trim());
+        if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" );
+        }
+        // md5 password
+        xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes()));
+
+        // check repeat
+        XxlJobUser existUser = xxlJobUserDao.loadByUserName(xxlJobUser.getUsername());
+        if (existUser != null) {
+            return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("user_username_repeat") );
+        }
+
+        // write
+        xxlJobUserDao.save(xxlJobUser);
+        return ReturnT.SUCCESS;
+    }
+
+    @RequestMapping("/update")
+    @ResponseBody
+    public ReturnT<String> update(XxlJobUser xxlJobUser) {
+
+        // valid password
+        if (StringUtils.hasText(xxlJobUser.getPassword())) {
+            xxlJobUser.setPassword(xxlJobUser.getPassword().trim());
+            if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) {
+                return new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" );
+            }
+            // md5 password
+            xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes()));
+        } else {
+            xxlJobUser.setPassword(null);
+        }
+
+        // write
+        xxlJobUserDao.update(xxlJobUser);
+        return ReturnT.SUCCESS;
+    }
+
+    @RequestMapping("/remove")
+    @ResponseBody
+    public ReturnT<String> remove(int id) {
+        xxlJobUserDao.delete(id);
+        return ReturnT.SUCCESS;
+    }
+
+}

+ 54 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java

@@ -0,0 +1,54 @@
+package com.xxl.job.admin.core.model;
+
+/**
+ * @author xuxueli 2019-05-04 16:43:12
+ */
+public class XxlJobUser {
+	
+	private int id;
+	private String username;		// 账号
+	private String password;		// 密码
+	private int role;				// 角色:0-普通用户、1-管理员
+	private String permission;	// 权限:执行器ID列表,多个逗号分割
+
+	public int getId() {
+		return id;
+	}
+
+	public void setId(int id) {
+		this.id = id;
+	}
+
+	public String getUsername() {
+		return username;
+	}
+
+	public void setUsername(String username) {
+		this.username = username;
+	}
+
+	public String getPassword() {
+		return password;
+	}
+
+	public void setPassword(String password) {
+		this.password = password;
+	}
+
+	public int getRole() {
+		return role;
+	}
+
+	public void setRole(int role) {
+		this.role = role;
+	}
+
+	public String getPermission() {
+		return permission;
+	}
+
+	public void setPermission(String permission) {
+		this.permission = permission;
+	}
+
+}

+ 29 - 0
xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java

@@ -0,0 +1,29 @@
+package com.xxl.job.admin.dao;
+
+import com.xxl.job.admin.core.model.XxlJobUser;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import java.util.List;
+
+/**
+ * @author xuxueli 2019-05-04 16:44:59
+ */
+@Mapper
+public interface XxlJobUserDao {
+
+	public List<XxlJobUser> pageList(@Param("offset") int offset,
+                                     @Param("pagesize") int pagesize,
+                                     @Param("username") String username);
+	public int pageListCount(@Param("offset") int offset,
+							 @Param("pagesize") int pagesize,
+							 @Param("username") String username);
+
+	public XxlJobUser loadByUserName(@Param("username") String username);
+
+	public int save(XxlJobUser xxlJobUser);
+
+	public int update(XxlJobUser xxlJobUser);
+	
+	public int delete(@Param("id") int id);
+
+}

+ 14 - 0
xxl-job-admin/src/main/resources/i18n/message.properties

@@ -31,6 +31,7 @@ system_unvalid=非法
 system_not_found=不存在
 system_nav=导航
 system_digits=整数
+system_lengh_limit=长度限制
 
 ## daterangepicker
 daterangepicker_ranges_recent_hour=最近一小时
@@ -229,6 +230,19 @@ jobconf_trigger_type_parent=父任务触发
 jobconf_trigger_type_api=API触发
 jobconf_trigger_type_retry=失败重试触发
 
+## user
+user_manage=用户管理
+user_username=账号
+user_password=密码
+user_role=角色
+user_role_admin=管理员
+user_role_normal=普通用户
+user_permission=权限
+user_add=新增用户
+user_update=更新用户
+user_username_repeat=账号重复
+user_password_update_placeholder=请输入新密码,为空则不更新密码
+
 ## help
 job_help=使用教程
 job_help_document=官方文档

+ 14 - 0
xxl-job-admin/src/main/resources/i18n/message_en.properties

@@ -31,6 +31,7 @@ system_unvalid=illegal
 system_not_found=not exist
 system_nav=Navigation
 system_digits=digits
+system_lengh_limit=Length limit
 
 ## daterangepicker
 daterangepicker_ranges_recent_hour=recent one hour
@@ -229,6 +230,19 @@ jobconf_trigger_type_parent=Parent job trigger
 jobconf_trigger_type_api=Api trigger
 jobconf_trigger_type_retry=Fail retry trigger
 
+## user
+user_manage==User Manage
+user_username=Username
+user_password=Password
+user_role=Role
+user_role_admin=Admin User
+user_role_normal=Normal User
+user_permission=Permission
+user_add=Add User
+user_update=Edit User
+user_username_repeat=Username Repeat
+user_password_update_placeholder=Please input password, empty means not update
+
 ## help
 job_help=Tutorial
 job_help_document=Official Document

+ 81 - 0
xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml

@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.xxl.job.admin.dao.XxlJobUserDao">
+
+	<resultMap id="XxlJobUser" type="com.xxl.job.admin.core.model.XxlJobUser" >
+		<result column="id" property="id" />
+		<result column="username" property="username" />
+	    <result column="password" property="password" />
+	    <result column="role" property="role" />
+	    <result column="permission" property="permission" />
+	</resultMap>
+
+	<sql id="Base_Column_List">
+		t.id,
+		t.username,
+		t.password,
+		t.role,
+		t.permission
+	</sql>
+
+	<select id="pageList" parameterType="java.util.HashMap" resultMap="XxlJobUser">
+		SELECT <include refid="Base_Column_List" />
+		FROM XXL_JOB_QRTZ_USER AS t
+		<trim prefix="WHERE" prefixOverrides="AND | OR" >
+			<if test="username != null and username != ''">
+				AND t.username like CONCAT(CONCAT('%', #{username}), '%')
+			</if>
+		</trim>
+		ORDER BY username ASC
+		LIMIT #{offset}, #{pagesize}
+	</select>
+
+	<select id="pageListCount" parameterType="java.util.HashMap" resultType="int">
+		SELECT count(1)
+		FROM XXL_JOB_QRTZ_USER AS t
+		<trim prefix="WHERE" prefixOverrides="AND | OR" >
+			<if test="username != null and username != ''">
+				AND t.username like CONCAT(CONCAT('%', #{username}), '%')
+			</if>
+		</trim>
+	</select>
+
+	<select id="loadByUserName" parameterType="java.util.HashMap" resultMap="XxlJobUser">
+		SELECT <include refid="Base_Column_List" />
+		FROM XXL_JOB_QRTZ_USER AS t
+		WHERE t.username = #{username}
+	</select>
+
+	<insert id="save" parameterType="com.xxl.job.admin.core.model.XxlJobUser" useGeneratedKeys="true" keyProperty="id" >
+		INSERT INTO XXL_JOB_QRTZ_USER (
+			username,
+			password,
+			role,
+			permission
+		) VALUES (
+			#{username},
+			#{password},
+			#{role},
+			#{permission}
+		);
+	</insert>
+
+	<update id="update" parameterType="com.xxl.job.admin.core.model.XxlJobUser" >
+		UPDATE XXL_JOB_QRTZ_USER
+		SET
+			<if test="password != null and password != ''">
+				password = #{password},
+			</if>
+			role = #{role},
+			permission = #{permission}
+		WHERE id = #{id}
+	</update>
+
+	<delete id="delete" parameterType="java.util.HashMap">
+		DELETE
+		FROM XXL_JOB_QRTZ_USER
+		WHERE id = #{id}
+	</delete>
+
+</mapper>

+ 298 - 0
xxl-job-admin/src/main/resources/static/js/user.index.1.js

@@ -0,0 +1,298 @@
+$(function() {
+
+	// init date tables
+	var userListTable = $("#user_list").dataTable({
+		"deferRender": true,
+		"processing" : true, 
+	    "serverSide": true,
+		"ajax": {
+			url: base_url + "/user/pageList",
+			type:"post",
+	        data : function ( d ) {
+	        	var obj = {};
+                obj.username = $('#username').val();
+	        	obj.start = d.start;
+	        	obj.length = d.length;
+                return obj;
+            }
+	    },
+	    "searching": false,
+	    "ordering": false,
+	    //"scrollX": true,	// scroll x,close self-adaption
+	    "columns": [
+	                {
+	                	"data": 'id',
+						"visible" : false,
+						"width":'10%'
+					},
+	                {
+	                	"data": 'username',
+						"visible" : true,
+						"width":'20%'
+					},
+	                {
+	                	"data": 'password',
+						"visible" : true,
+                        "width":'20%'
+					},
+					{
+						"data": 'role',
+						"visible" : true,
+						"width":'10%',
+                        "render": function ( data, type, row ) {
+                            if (data == 1) {
+                                return I18n.user_role_admin
+                            } else {
+                                return I18n.user_role_normal
+                            }
+                        }
+					},
+	                {
+	                	"data": 'permission',
+						"width":'10%',
+	                	"visible" : true
+	                },
+	                {
+						"data": I18n.system_opt ,
+						"width":'15%',
+	                	"render": function ( data, type, row ) {
+	                		return function(){
+								// html
+                                tableData['key'+row.id] = row;
+								var html = '<p id="'+ row.id +'" >'+
+									'<button class="btn btn-warning btn-xs update" type="button">'+ I18n.system_opt_edit +'</button>  '+
+									'<button class="btn btn-danger btn-xs delete" type="button">'+ I18n.system_opt_del +'</button>  '+
+									'</p>';
+
+	                			return html;
+							};
+	                	}
+	                }
+	            ],
+		"language" : {
+			"sProcessing" : I18n.dataTable_sProcessing ,
+			"sLengthMenu" : I18n.dataTable_sLengthMenu ,
+			"sZeroRecords" : I18n.dataTable_sZeroRecords ,
+			"sInfo" : I18n.dataTable_sInfo ,
+			"sInfoEmpty" : I18n.dataTable_sInfoEmpty ,
+			"sInfoFiltered" : I18n.dataTable_sInfoFiltered ,
+			"sInfoPostFix" : "",
+			"sSearch" : I18n.dataTable_sSearch ,
+			"sUrl" : "",
+			"sEmptyTable" : I18n.dataTable_sEmptyTable ,
+			"sLoadingRecords" : I18n.dataTable_sLoadingRecords ,
+			"sInfoThousands" : ",",
+			"oPaginate" : {
+				"sFirst" : I18n.dataTable_sFirst ,
+				"sPrevious" : I18n.dataTable_sPrevious ,
+				"sNext" : I18n.dataTable_sNext ,
+				"sLast" : I18n.dataTable_sLast
+			},
+			"oAria" : {
+				"sSortAscending" : I18n.dataTable_sSortAscending ,
+				"sSortDescending" : I18n.dataTable_sSortDescending
+			}
+		}
+	});
+
+    // table data
+    var tableData = {};
+
+	// search btn
+	$('#searchBtn').on('click', function(){
+        userListTable.fnDraw();
+	});
+	
+	// job operate
+	$("#user_list").on('click', '.delete',function() {
+		var id = $(this).parent('p').attr("id");
+
+		layer.confirm( I18n.system_ok + I18n.system_opt_del + '?', {
+			icon: 3,
+			title: I18n.system_tips ,
+            btn: [ I18n.system_ok, I18n.system_cancel ]
+		}, function(index){
+			layer.close(index);
+
+			$.ajax({
+				type : 'POST',
+				url : base_url + "/user/remove",
+				data : {
+					"id" : id
+				},
+				dataType : "json",
+				success : function(data){
+					if (data.code == 200) {
+                        layer.msg( I18n.system_success );
+						userListTable.fnDraw(false);
+					} else {
+                        layer.msg( data.msg || I18n.system_opt_del + I18n.system_fail );
+					}
+				}
+			});
+		});
+	});
+
+	// add
+	$(".add").click(function(){
+		$('#addModal').modal({backdrop: false, keyboard: false}).modal('show');
+	});
+	var addModalValidate = $("#addModal .form").validate({
+		errorElement : 'span',  
+        errorClass : 'help-block',
+        focusInvalid : true,  
+        rules : {
+            username : {
+				required : true,
+                rangelength:[4, 20]
+			},
+            password : {
+                required : true,
+                rangelength:[4, 20]
+            }
+        }, 
+        messages : {
+            username : {
+            	required : I18n.system_please_input + I18n.user_username,
+                rangelength: I18n.system_lengh_limit + "[4-20]"
+            },
+            password : {
+                required : I18n.system_please_input + I18n.user_password,
+                rangelength: I18n.system_lengh_limit + "[4-20]"
+            }
+        },
+		highlight : function(element) {  
+            $(element).closest('.form-group').addClass('has-error');  
+        },
+        success : function(label) {  
+            label.closest('.form-group').removeClass('has-error');  
+            label.remove();  
+        },
+        errorPlacement : function(error, element) {  
+            element.parent('div').append(error);  
+        },
+        submitHandler : function(form) {
+
+            var permissionArr = [];
+            $("#addModal .form input[name=permission]:checked").each(function(){
+                permissionArr.push($(this).val());
+            });
+
+			var paramData = {
+				"username": $("#addModal .form input[name=username]").val(),
+                "password": $("#addModal .form input[name=password]").val(),
+                "role": $("#addModal .form input[name=role]:checked").val(),
+                "permission": permissionArr.join(',')
+			};
+
+        	$.post(base_url + "/user/add", paramData, function(data, status) {
+    			if (data.code == "200") {
+					$('#addModal').modal('hide');
+
+                    layer.msg( I18n.system_add_suc );
+                    userListTable.fnDraw();
+    			} else {
+					layer.open({
+						title: I18n.system_tips ,
+                        btn: [ I18n.system_ok ],
+						content: (data.msg || I18n.system_add_fail),
+						icon: '2'
+					});
+    			}
+    		});
+		}
+	});
+	$("#addModal").on('hide.bs.modal', function () {
+		$("#addModal .form")[0].reset();
+		addModalValidate.resetForm();
+		$("#addModal .form .form-group").removeClass("has-error");
+		$(".remote_panel").show();	// remote
+	});
+
+	// update
+	$("#user_list").on('click', '.update',function() {
+
+        var id = $(this).parent('p').attr("id");
+        var row = tableData['key'+id];
+
+		// base data
+		$("#updateModal .form input[name='id']").val( row.id );
+		$("#updateModal .form input[name='username']").val( row.username );
+		$("#updateModal .form input[name='password']").val( '' );
+		$("#updateModal .form input[name='role']").each(function () {
+			if($(this).val() == row.role) {
+                $(this).prop("checked",true);
+			} else {
+                $(this).prop("checked",false);
+			}
+        });
+        var permissionArr = [];
+        if (row.permission) {
+            permissionArr = row.permission.split(",");
+		}
+        $("#updateModal .form input[name='permission']").removeProp('checked');
+        $("#updateModal .form input[name='permission']").each(function () {
+            if($.inArray($(this).val(), permissionArr) > -1) {
+                $(this).prop("checked",true);
+            } else {
+                $(this).prop("checked",false);
+            }
+        });
+
+		// show
+		$('#updateModal').modal({backdrop: false, keyboard: false}).modal('show');
+	});
+	var updateModalValidate = $("#updateModal .form").validate({
+		errorElement : 'span',  
+        errorClass : 'help-block',
+        focusInvalid : true,
+		highlight : function(element) {
+            $(element).closest('.form-group').addClass('has-error');  
+        },
+        success : function(label) {  
+            label.closest('.form-group').removeClass('has-error');  
+            label.remove();  
+        },
+        errorPlacement : function(error, element) {  
+            element.parent('div').append(error);  
+        },
+        submitHandler : function(form) {
+
+            var permissionArr =[];
+            $("#updateModal .form input[name=permission]:checked").each(function(){
+                permissionArr.push($(this).val());
+            });
+
+            var paramData = {
+                "id": $("#updateModal .form input[name=id]").val(),
+                "username": $("#updateModal .form input[name=username]").val(),
+                "password": $("#updateModal .form input[name=password]").val(),
+                "role": $("#updateModal .form input[name=role]:checked").val(),
+                "permission": permissionArr.join(',')
+            };
+
+            $.post(base_url + "/user/update", paramData, function(data, status) {
+                if (data.code == "200") {
+                    $('#updateModal').modal('hide');
+
+                    layer.msg( I18n.system_update_suc );
+                    userListTable.fnDraw();
+                } else {
+                    layer.open({
+                        title: I18n.system_tips ,
+                        btn: [ I18n.system_ok ],
+                        content: (data.msg || I18n.system_update_fail),
+                        icon: '2'
+                    });
+                }
+            });
+		}
+	});
+	$("#updateModal").on('hide.bs.modal', function () {
+        $("#updateModal .form")[0].reset();
+        updateModalValidate.resetForm();
+        $("#updateModal .form .form-group").removeClass("has-error");
+        $(".remote_panel").show();	// remote
+	});
+
+});

+ 1 - 0
xxl-job-admin/src/main/resources/templates/common/common.macro.ftl

@@ -103,6 +103,7 @@
 				<li class="nav-click <#if pageName == "jobinfo">active</#if>" ><a href="${request.contextPath}/jobinfo"><i class="fa fa-circle-o text-yellow"></i><span>${I18n.jobinfo_name}</span></a></li>
 				<li class="nav-click <#if pageName == "joblog">active</#if>" ><a href="${request.contextPath}/joblog"><i class="fa fa-circle-o text-green"></i><span>${I18n.joblog_name}</span></a></li>
                 <li class="nav-click <#if pageName == "jobgroup">active</#if>" ><a href="${request.contextPath}/jobgroup"><i class="fa fa-circle-o text-red"></i><span>${I18n.jobgroup_name}</span></a></li>
+                <li class="nav-click <#if pageName == "user">active</#if>" ><a href="${request.contextPath}/user"><i class="fa fa-circle-o text-purple"></i><span>${I18n.user_manage}</span></a></li>
 				<li class="nav-click <#if pageName == "help">active</#if>" ><a href="${request.contextPath}/help"><i class="fa fa-circle-o text-gray"></i><span>${I18n.job_help}</span></a></li>
 			</ul>
 		</section>

+ 179 - 0
xxl-job-admin/src/main/resources/templates/user/user.index.ftl

@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<html>
+<head>
+  	<#import "../common/common.macro.ftl" as netCommon>
+	<@netCommon.commonStyle />
+	<!-- DataTables -->
+  	<link rel="stylesheet" href="${request.contextPath}/static/adminlte/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css">
+    <title>${I18n.admin_name}</title>
+</head>
+<body class="hold-transition skin-blue sidebar-mini <#if cookieMap?exists && cookieMap["xxljob_adminlte_settings"]?exists && "off" == cookieMap["xxljob_adminlte_settings"].value >sidebar-collapse</#if>">
+<div class="wrapper">
+	<!-- header -->
+	<@netCommon.commonHeader />
+	<!-- left -->
+	<@netCommon.commonLeft "user" />
+	
+	<!-- Content Wrapper. Contains page content -->
+	<div class="content-wrapper">
+		<!-- Content Header (Page header) -->
+		<section class="content-header">
+			<h1>${I18n.user_manage}</h1>
+		</section>
+		
+		<!-- Main content -->
+	    <section class="content">
+	    
+	    	<div class="row">
+                <div class="col-xs-3">
+                    <div class="input-group">
+                        <span class="input-group-addon">${I18n.user_username}</span>
+                        <input type="text" class="form-control" id="username" autocomplete="on" >
+                    </div>
+                </div>
+	            <div class="col-xs-1">
+	            	<button class="btn btn-block btn-info" id="searchBtn">${I18n.system_search}</button>
+	            </div>
+	            <div class="col-xs-2">
+	            	<button class="btn btn-block btn-success add" type="button">${I18n.user_add}</button>
+	            </div>
+          	</div>
+	    	
+			<div class="row">
+				<div class="col-xs-12">
+					<div class="box">
+			            <div class="box-body" >
+			              	<table id="user_list" class="table table-bordered table-striped" width="100%" >
+				                <thead>
+					            	<tr>
+                                        <th name="id" >ID</th>
+                                        <th name="username" >${I18n.user_username}</th>
+					                  	<th name="password" >${I18n.user_password}</th>
+                                        <th name="role" >${I18n.user_role}</th>
+					                  	<th name="permission" >${I18n.user_permission}</th>
+					                  	<th>${I18n.system_opt}</th>
+					                </tr>
+				                </thead>
+				                <tbody></tbody>
+				                <tfoot></tfoot>
+							</table>
+						</div>
+					</div>
+				</div>
+			</div>
+	    </section>
+	</div>
+	
+	<!-- footer -->
+	<@netCommon.commonFooter />
+</div>
+
+<!-- 新增.模态框 -->
+<div class="modal fade" id="addModal" tabindex="-1" role="dialog"  aria-hidden="true">
+	<div class="modal-dialog">
+		<div class="modal-content">
+			<div class="modal-header">
+            	<h4 class="modal-title" >${I18n.user_add}</h4>
+         	</div>
+         	<div class="modal-body">
+				<form class="form-horizontal form" role="form" >
+                    <div class="form-group">
+                        <label for="lastname" class="col-sm-2 control-label">${I18n.user_username}<font color="red">*</font></label>
+                        <div class="col-sm-8"><input type="text" class="form-control" name="username" placeholder="${I18n.system_please_input}${I18n.user_username}" maxlength="50" ></div>
+                    </div>
+                    <div class="form-group">
+                        <label for="lastname" class="col-sm-2 control-label">${I18n.user_password}<font color="red">*</font></label>
+                        <div class="col-sm-8"><input type="text" class="form-control" name="password" placeholder="${I18n.system_please_input}${I18n.user_password}" maxlength="50" ></div>
+                    </div>
+                    <div class="form-group">
+                        <label for="lastname" class="col-sm-2 control-label">${I18n.user_role}<font color="red">*</font></label>
+                        <div class="col-sm-10">
+                            <input type="radio" name="role" value="0" checked />${I18n.user_role_normal}
+                            &nbsp;&nbsp;&nbsp;&nbsp;
+                            <input type="radio" name="role" value="1" />${I18n.user_role_admin}
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="lastname" class="col-sm-2 control-label">${I18n.user_permission}<font color="black">*</font></label>
+                        <div class="col-sm-10">
+							<#if groupList?exists && groupList?size gt 0>
+								<#list groupList as item>
+                                    <input type="checkbox" name="permission" value="${item.id}" />${item.title}(${item.appName})<br>
+								</#list>
+							</#if>
+                        </div>
+                    </div>
+
+                    <hr>
+					<div class="form-group">
+						<div class="col-sm-offset-3 col-sm-6">
+							<button type="submit" class="btn btn-primary"  >${I18n.system_save}</button>
+							<button type="button" class="btn btn-default" data-dismiss="modal">${I18n.system_cancel}</button>
+						</div>
+					</div>
+
+				</form>
+         	</div>
+		</div>
+	</div>
+</div>
+
+<!-- 更新.模态框 -->
+<div class="modal fade" id="updateModal" tabindex="-1" role="dialog"  aria-hidden="true">
+	<div class="modal-dialog">
+		<div class="modal-content">
+			<div class="modal-header">
+            	<h4 class="modal-title" >${I18n.user_update}</h4>
+         	</div>
+         	<div class="modal-body">
+				<form class="form-horizontal form" role="form" >
+                    <div class="form-group">
+                        <label for="lastname" class="col-sm-2 control-label">${I18n.user_username}<font color="red">*</font></label>
+                        <div class="col-sm-8"><input type="text" class="form-control" name="username" placeholder="${I18n.system_please_input}${I18n.user_username}" maxlength="50" readonly ></div>
+                    </div>
+                    <div class="form-group">
+                        <label for="lastname" class="col-sm-2 control-label">${I18n.user_password}<font color="red">*</font></label>
+                        <div class="col-sm-8"><input type="text" class="form-control" name="password" placeholder="${I18n.user_password_update_placeholder}" maxlength="50" ></div>
+                    </div>
+                    <div class="form-group">
+                        <label for="lastname" class="col-sm-2 control-label">${I18n.user_role}<font color="red">*</font></label>
+                        <div class="col-sm-10">
+                            <input type="radio" name="role" value="0" />${I18n.user_role_normal}
+                            &nbsp;&nbsp;&nbsp;&nbsp;
+                            <input type="radio" name="role" value="1" />${I18n.user_role_admin}
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="lastname" class="col-sm-2 control-label">${I18n.user_permission}<font color="black">*</font></label>
+                        <div class="col-sm-10">
+						<#if groupList?exists && groupList?size gt 0>
+							<#list groupList as item>
+                                <input type="checkbox" name="permission" value="${item.id}" />${item.title}(${item.appName})<br>
+							</#list>
+						</#if>
+                        </div>
+                    </div>
+
+					<hr>
+					<div class="form-group">
+                        <div class="col-sm-offset-3 col-sm-6">
+							<button type="submit" class="btn btn-primary"  >${I18n.system_save}</button>
+							<button type="button" class="btn btn-default" data-dismiss="modal">${I18n.system_cancel}</button>
+                            <input type="hidden" name="id" >
+						</div>
+					</div>
+
+				</form>
+         	</div>
+		</div>
+	</div>
+</div>
+
+<@netCommon.commonScript />
+<!-- DataTables -->
+<script src="${request.contextPath}/static/adminlte/bower_components/datatables.net/js/jquery.dataTables.min.js"></script>
+<script src="${request.contextPath}/static/adminlte/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js"></script>
+<script src="${request.contextPath}/static/plugins/jquery/jquery.validate.min.js"></script>
+<script src="${request.contextPath}/static/js/user.index.1.js"></script>
+</body>
+</html>