Kaynağa Gözat

接口分组前缀功能

mxd 5 yıl önce
ebeveyn
işleme
e08a2c32a4

+ 39 - 0
src/main/java/org/ssssssss/magicapi/config/ApiInfo.java

@@ -39,6 +39,11 @@ public class ApiInfo {
 	 */
 	private String groupName;
 
+	/**
+	 * 分组前缀
+	 */
+	private String groupPrefix;
+
 	/**
 	 * 设置的请求参数
 	 */
@@ -49,6 +54,11 @@ public class ApiInfo {
 	 */
 	private String option;
 
+	/**
+	 * 输出结果
+	 */
+	private String output;
+
 	/**
 	 * 接口选项json->map
 	 */
@@ -110,10 +120,39 @@ public class ApiInfo {
 		this.parameter = parameter;
 	}
 
+	public String getOutput() {
+		return output;
+	}
+
+	public void setOutput(String output) {
+		this.output = output;
+	}
+
+	public String getGroupPrefix() {
+		return groupPrefix;
+	}
+
+	public void setGroupPrefix(String groupPrefix) {
+		this.groupPrefix = groupPrefix;
+	}
+
+
+	public Map getOptionMap() {
+		return optionMap;
+	}
+
+	public void setOptionMap(Map optionMap) {
+		this.optionMap = optionMap;
+	}
+
 	public String getOption() {
 		return option;
 	}
 
+	public void setOptionValue(String optionValue){
+		this.setOption(optionValue);
+	}
+
 	public void setOption(String option) {
 		this.option = option;
 		try {

+ 45 - 11
src/main/java/org/ssssssss/magicapi/config/MappingHandlerMapping.java

@@ -14,6 +14,8 @@ import org.ssssssss.magicapi.provider.ApiServiceProvider;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -54,6 +56,8 @@ public class MappingHandlerMapping {
 	 */
 	private String prefix;
 
+	private List<ApiInfo> apiInfos = Collections.synchronizedList(new ArrayList<>());
+
 	public MappingHandlerMapping() throws NoSuchMethodException {
 	}
 
@@ -81,8 +85,9 @@ public class MappingHandlerMapping {
 
 	/**
 	 * 构建缓存map的key
-	 * @param requestMethod	请求方法
-	 * @param requestMapping	请求路径
+	 *
+	 * @param requestMethod  请求方法
+	 * @param requestMapping 请求路径
 	 * @return
 	 */
 	public static String buildMappingKey(String requestMethod, String requestMapping) {
@@ -107,27 +112,40 @@ public class MappingHandlerMapping {
 	public void registerAllMapping() {
 		List<ApiInfo> list = magicApiService.listWithScript();
 		if (list != null) {
+			apiInfos.addAll(list);
 			for (ApiInfo info : list) {
-				registerMapping(info);
+				registerMapping(info, false);
 			}
 		}
 	}
 
 	/**
 	 * 根据请求方法和路径获取接口信息
-	 * @param method	请求方法
-	 * @param requestMapping	请求路径
+	 *
+	 * @param method         请求方法
+	 * @param requestMapping 请求路径
 	 */
 	public ApiInfo getApiInfo(String method, String requestMapping) {
 		return mappings.get(buildMappingKey(method, requestMapping));
 	}
 
+	public void updateGroupPrefix(String oldGroupName, String newGroupName, String prefix) {
+		for (ApiInfo info : apiInfos) {
+			if (oldGroupName.equals(info.getGroupName())) {
+				unregisterMapping(info.getId(), false);
+				info.setGroupName(newGroupName);
+				info.setGroupPrefix(prefix);
+				registerMapping(info, false);
+			}
+		}
+	}
+
 	/**
 	 * 注册请求映射
 	 *
 	 * @param info
 	 */
-	public void registerMapping(ApiInfo info) {
+	public void registerMapping(ApiInfo info, boolean delete) {
 		// 先判断是否已注册,如果已注册,则先取消注册在进行注册。
 		if (mappings.containsKey(info.getId())) {
 			ApiInfo oldInfo = mappings.get(info.getId());
@@ -142,17 +160,24 @@ public class MappingHandlerMapping {
 		mappings.put(info.getId(), info);
 		mappings.put(getMappingKey(info), info);
 		requestMappingHandlerMapping.registerMapping(requestMapping, handler, method);
+		if (delete) {
+			apiInfos.add(info);
+			apiInfos.removeIf(i -> i.getId().equalsIgnoreCase(info.getId()));
+		}
 	}
 
 	/**
 	 * 取消注册请求映射
 	 */
-	public void unregisterMapping(String id) {
+	public void unregisterMapping(String id, boolean delete) {
 		ApiInfo info = mappings.remove(id);
 		if (info != null) {
 			logger.info("取消注册接口:{}", info.getName());
 			mappings.remove(getMappingKey(info));
 			requestMappingHandlerMapping.unregisterMapping(getRequestMapping(info));
+			if (delete) {
+				apiInfos.removeIf(i -> i.getId().equalsIgnoreCase(info.getId()));
+			}
 		}
 	}
 
@@ -160,14 +185,23 @@ public class MappingHandlerMapping {
 	 * 根据接口信息获取绑定map的key
 	 */
 	private String getMappingKey(ApiInfo info) {
-		return buildMappingKey(info.getMethod(), getRequestPath(info.getPath()));
+		return buildMappingKey(info.getMethod(), getRequestPath(info.getGroupPrefix(), info.getPath()));
 	}
 
 	/**
 	 * 处理前缀
-	 * @param path	请求路径
+	 *
+	 * @param path 请求路径
 	 */
-	private String getRequestPath(String path) {
+	private String getRequestPath(String groupPrefix, String path) {
+		groupPrefix = groupPrefix == null ? "" : groupPrefix;
+		while (groupPrefix.endsWith("/")) {
+			groupPrefix = groupPrefix.substring(0, groupPrefix.length() - 1);
+		}
+		while (path.startsWith("/")) {
+			path = path.substring(1);
+		}
+		path = groupPrefix + "/" + path;
 		if (prefix != null) {
 			path = prefix + (path.startsWith("/") ? path.substring(1) : path);
 		}
@@ -178,7 +212,7 @@ public class MappingHandlerMapping {
 	 * 根据接口信息构建 RequestMappingInfo
 	 */
 	private RequestMappingInfo getRequestMapping(ApiInfo info) {
-		return RequestMappingInfo.paths(getRequestPath(info.getPath())).methods(RequestMethod.valueOf(info.getMethod().toUpperCase())).build();
+		return RequestMappingInfo.paths(getRequestPath(info.getGroupPrefix(), info.getPath())).methods(RequestMethod.valueOf(info.getMethod().toUpperCase())).build();
 	}
 
 }

+ 31 - 5
src/main/java/org/ssssssss/magicapi/config/WebUIController.java

@@ -95,6 +95,7 @@ public class WebUIController {
 
 	/**
 	 * 删除接口
+	 *
 	 * @param request
 	 * @param id      接口ID
 	 * @return
@@ -107,8 +108,8 @@ public class WebUIController {
 		}
 		try {
 			boolean success = this.magicApiService.delete(id);
-			if (success) {	//删除成功时在取消注册
-				mappingHandlerMapping.unregisterMapping(id);
+			if (success) {    //删除成功时在取消注册
+				mappingHandlerMapping.unregisterMapping(id, true);
 			}
 			return new JsonBean<>(success);
 		} catch (Exception e) {
@@ -133,12 +134,12 @@ public class WebUIController {
 		}
 		try {
 			boolean success = this.magicApiService.deleteGroup(groupName);
-			if (success) {	//删除成功时取消注册
+			if (success) {    //删除成功时取消注册
 				if (StringUtils.isNotBlank(apiIds)) {
 					String[] ids = apiIds.split(",");
 					if (ids != null && ids.length > 0) {
 						for (String id : ids) {
-							mappingHandlerMapping.unregisterMapping(id);
+							mappingHandlerMapping.unregisterMapping(id, true);
 						}
 					}
 				}
@@ -150,6 +151,31 @@ public class WebUIController {
 		}
 	}
 
+	/**
+	 * 修改分组
+	 *
+	 * @param groupName    分组名称
+	 * @param oldGroupName 原分组名称
+	 * @param prefix       分组前缀
+	 */
+	@RequestMapping("/group/update")
+	@ResponseBody
+	public JsonBean<Boolean> groupUpdate(String groupName, String oldGroupName, String prefix, HttpServletRequest request) {
+		if (!allowVisit(request, RequestInterceptor.Authorization.SAVE)) {
+			return new JsonBean<>(-10, "无权限执行删除方法");
+		}
+		try {
+			boolean success = magicApiService.updateGroup(oldGroupName, groupName, prefix);
+			if (success) {
+				mappingHandlerMapping.updateGroupPrefix(oldGroupName, groupName, prefix);
+			}
+			return new JsonBean<>(success);
+		} catch (Exception e) {
+			logger.error("修改分组出错", e);
+			return new JsonBean<>(-1, e.getMessage());
+		}
+	}
+
 	/**
 	 * 查询所有接口
 	 *
@@ -345,7 +371,7 @@ public class WebUIController {
 				magicApiService.update(info);
 			}
 			// 注册接口
-			mappingHandlerMapping.registerMapping(info);
+			mappingHandlerMapping.registerMapping(info, true);
 			return new JsonBean<>(info.getId());
 		} catch (Exception e) {
 			logger.error("保存接口出错", e);

+ 9 - 7
src/main/java/org/ssssssss/magicapi/provider/ApiServiceProvider.java

@@ -9,29 +9,25 @@ public interface ApiServiceProvider {
 	 * 删除接口
 	 *
 	 * @param id
-	 * @return
 	 */
 	boolean delete(String id);
 
 	/**
 	 * 根据组名删除接口
 	 *
-	 * @param groupName
-	 * @return
+	 * @param groupName 	分组名称
 	 */
 	boolean deleteGroup(String groupName);
 
 	/**
 	 * 查询所有接口(提供给页面,无需带script)
 	 *
-	 * @return
 	 */
 	List<ApiInfo> list();
 
 	/**
 	 * 查询所有接口(内部使用,需要带Script)
 	 *
-	 * @return
 	 */
 	List<ApiInfo> listWithScript();
 
@@ -39,7 +35,6 @@ public interface ApiServiceProvider {
 	 * 查询接口详情(主要给页面使用)
 	 *
 	 * @param id
-	 * @return
 	 */
 	ApiInfo get(String id);
 
@@ -48,10 +43,17 @@ public interface ApiServiceProvider {
 	 *
 	 * @param method 请求方法
 	 * @param path   请求路径
-	 * @return
 	 */
 	boolean exists(String method, String path);
 
+	/**
+	 * 修改分组信息
+	 * @param oldGroupName	旧分组名称
+	 * @param groupName	新分组名称
+	 * @param groupPrefix	分组前缀
+	 */
+	boolean updateGroup(String oldGroupName,String groupName, String groupPrefix);
+
 	/**
 	 * 判断接口是否存在
 	 *

+ 30 - 12
src/main/java/org/ssssssss/magicapi/provider/impl/DefaultApiServiceProvider.java

@@ -11,16 +11,19 @@ import java.util.UUID;
 
 public class DefaultApiServiceProvider implements ApiServiceProvider {
 
-	private final String deleteById = "delete from magic_api_info where id = ?";
-	private final String deleteByGroupName = "delete from magic_api_info where api_group_name = ?";
-	private final String selectList = "select id,api_name name,api_group_name group_name,api_path path,api_method method from magic_api_info order by api_update_time desc";
-	private final String selectListWithScript = "select id,api_name name,api_group_name group_name,api_path path,api_method method,api_script script,api_parameter parameter,api_option `option` from magic_api_info";
-	private final String selectOne = "select api_method method,api_script script,api_path path,api_name name,api_group_name group_name,api_parameter parameter,api_option `option` from magic_api_info where id = ?";
-	private final String exists = "select count(*) from magic_api_info where api_method = ? and api_path = ?";
-	private final String existsWithoutId = "select count(*) from magic_api_info where api_method = ? and api_path = ? and id !=?";
-	private final String insert = "insert into magic_api_info(id,api_method,api_path,api_script,api_name,api_group_name,api_parameter,api_option,api_create_time,api_update_time) values(?,?,?,?,?,?,?,?,?,?)";
-	private final String update = "update magic_api_info set api_method = ?,api_path = ?,api_script = ?,api_name = ?,api_group_name = ?,api_parameter = ?,api_option = ?,api_update_time = ? where id = ?";
+	private final String COMMON_COLUMNS = "id,\n" +
+			"api_name name,\n" +
+			"api_group_name group_name,\n" +
+			"api_group_prefix group_prefix,\n" +
+			"api_path path,\n" +
+			"api_method method";
+
+	private final String SCRIPT_COLUMNS = "api_script script,\n" +
+			"api_parameter parameter,\n" +
+			"api_option option_value\n";
+
 	private RowMapper<ApiInfo> rowMapper = new BeanPropertyRowMapper<>(ApiInfo.class);
+
 	private JdbcTemplate template;
 
 	public DefaultApiServiceProvider(JdbcTemplate template) {
@@ -28,20 +31,24 @@ public class DefaultApiServiceProvider implements ApiServiceProvider {
 	}
 
 	public boolean delete(String id) {
+		String deleteById = "delete from magic_api_info where id = ?";
 		return template.update(deleteById, id) > 0;
 	}
 
 	public boolean deleteGroup(String groupName) {
+		String deleteByGroupName = "delete from magic_api_info where api_group_name = ?";
 		return template.update(deleteByGroupName, groupName) > 0;
 	}
 
 	public List<ApiInfo> list() {
+		String selectList = "select " + COMMON_COLUMNS + " from magic_api_info order by api_update_time desc";
 		return template.query(selectList, rowMapper);
 	}
 
 	public List<ApiInfo> listWithScript() {
+		String selectListWithScript = "select " + COMMON_COLUMNS + "," + SCRIPT_COLUMNS + " from magic_api_info";
 		List<ApiInfo> infos = template.query(selectListWithScript, rowMapper);
-		if(infos != null){
+		if (infos != null) {
 			for (ApiInfo info : infos) {
 				unwrap(info);
 			}
@@ -50,16 +57,25 @@ public class DefaultApiServiceProvider implements ApiServiceProvider {
 	}
 
 	public ApiInfo get(String id) {
+		String selectOne = "select " + COMMON_COLUMNS + "," + SCRIPT_COLUMNS + " from magic_api_info where id = ?";
 		ApiInfo info = template.queryForObject(selectOne, rowMapper, id);
 		unwrap(info);
 		return info;
 	}
 
 	public boolean exists(String method, String path) {
+		String exists = "select count(*) from magic_api_info where api_method = ? and api_path = ?";
 		return template.queryForObject(exists, Integer.class, method, path) > 0;
 	}
 
+	@Override
+	public boolean updateGroup(String oldGroupName, String groupName, String groupPrefix) {
+		String updateGroup = "update magic_api_info set api_group_name = ?,api_group_prefix=?,api_update_time = ? where api_group_name = ?";
+		return template.update(updateGroup, groupName, groupPrefix, System.currentTimeMillis(), oldGroupName) > 0;
+	}
+
 	public boolean existsWithoutId(String method, String path, String id) {
+		String existsWithoutId = "select count(*) from magic_api_info where api_method = ? and api_path = ? and id !=?";
 		return template.queryForObject(existsWithoutId, Integer.class, method, path, id) > 0;
 	}
 
@@ -67,11 +83,13 @@ public class DefaultApiServiceProvider implements ApiServiceProvider {
 		info.setId(UUID.randomUUID().toString().replace("-", ""));
 		wrap(info);
 		long time = System.currentTimeMillis();
-		return template.update(insert, info.getId(), info.getMethod(), info.getPath(), info.getScript(), info.getName(), info.getGroupName(), info.getParameter(), info.getOption(), time, time) > 0;
+		String insert = "insert into magic_api_info(id,api_method,api_path,api_script,api_name,api_group_name,api_parameter,api_option,api_output,api_group_prefix,api_create_time,api_update_time) values(?,?,?,?,?,?,?,?,?,?,?,?)";
+		return template.update(insert, info.getId(), info.getMethod(), info.getPath(), info.getScript(), info.getName(), info.getGroupName(), info.getParameter(), info.getOption(), info.getOutput(), info.getGroupPrefix(), time, time) > 0;
 	}
 
 	public boolean update(ApiInfo info) {
 		wrap(info);
-		return template.update(update, info.getMethod(), info.getPath(), info.getScript(), info.getName(), info.getGroupName(), info.getParameter(), info.getOption(), System.currentTimeMillis(), info.getId()) > 0;
+		String update = "update magic_api_info set api_method = ?,api_path = ?,api_script = ?,api_name = ?,api_group_name = ?,api_parameter = ?,api_option = ?,api_output = ?,api_group_prefix = ?,api_update_time = ? where id = ?";
+		return template.update(update, info.getMethod(), info.getPath(), info.getScript(), info.getName(), info.getGroupName(), info.getParameter(), info.getOption(), info.getOutput(), info.getGroupPrefix(), System.currentTimeMillis(), info.getId()) > 0;
 	}
 }

+ 14 - 8
src/main/resources/magicapi-support/css/index.css

@@ -18,8 +18,8 @@ body {
     position: relative;
     width: 100%;
     height: 100%;
-    min-width: 1280px;
-    min-height: 768px;
+    min-width: 1200px;
+    min-height: 500px;
 }
 ul li {
     list-style: none;
@@ -166,6 +166,9 @@ ul li {
 .group-item .group-header i{
     color : #b3b3b3
 }
+.group-item .group-header span{
+    color : #999;
+}
 .group-item .group-header .icon-list{
     color : #aeb9c0;
     padding-left : 8px;
@@ -193,11 +196,11 @@ ul li {
     padding-right: 15px;
     font-size: 0;
 }
-.middle-container .editor-container-wrapper .properties-container label{
+.middle-container .editor-container-wrapper .properties-container label,.dialog label{
     font-size : 12px;
     margin:0 3px;
 }
-.properties-container input{
+.properties-container input,.dialog input{
     height: 22px;
     line-height: 22px;
     border-radius: 0;
@@ -354,13 +357,16 @@ ul li {
     border-top: 1px solid #919191;
     padding-left: 20px;
 }
-
 .main-container .bottom-container .bottom-content-container {
     border-bottom: 1px solid #c9c9c9;
     display: none;
     height: 300px;
 }
-
+@media screen and (max-height: 600px) {
+    .main-container .bottom-container .bottom-content-container {
+        height: 180px;
+    }
+}
 .main-container .bottom-container .bottom-content-container .bottom-content-item {
     display: none;
     background: #fff;
@@ -655,7 +661,7 @@ ul li {
 .skin-dark .main-container .bottom-container ul li i{
     color : #AFB1B3;
 }
-.skin-dark .group-item .group-list li span{
+.skin-dark .group-item .group-list li span,.skin-dark .group-item span{
     color : #787878
 }
 .skin-dark .main-container .bottom-container ul li.selected{
@@ -685,7 +691,7 @@ ul li {
 .skin-dark .main-container .middle-container{
     background: #313335;
 }
-.skin-dark .properties-container input{
+.skin-dark .properties-container input,.skin-dark .dialog input{
     border: 1px solid #646464;
     background: #45494A;
     color : #bbb;

+ 2 - 3
src/main/resources/magicapi-support/index.html

@@ -68,9 +68,7 @@
 				<div class="select input" style="width:120px">
 					<input type="text" value="未分组" name="group"/>
 					<ul class="not-select">
-						<li>未分组</li>
-						<li>组1</li>
-						<li>组2</li>
+						<li data-name="未分组" data-prefix="">未分组</li>
 					</ul>
 				</div>
 				<label>请求方法:</label>
@@ -87,6 +85,7 @@
 				<input type="text" style="width : 160px" name="name"/>
 				<label>接口地址:</label>
 				<input type="text" name="path"/>
+				<input type="hidden" name="prefix" />
 			</div>
 			<div class="editor-container"></div>
 		</div>

+ 201 - 29
src/main/resources/magicapi-support/js/index.js

@@ -4,6 +4,7 @@ var MagicEditor = {
         if(skin){
             $('body').addClass('skin-' + skin);
         }
+        this.addedGroups = {};
         this.apiId = null;
         this.apiList = [];
         this.debugSessionId = null;
@@ -88,16 +89,10 @@ var MagicEditor = {
     renderApiList : function(){
         var empty = true;
         var root = [];
-        var groups = {
-            "未分组" : {
-                id : '未分组',
-                children : [],
-                spread : true,
-                title : '未分组'
-            }
-        };
+        var groups = {};
         var apiList = this.apiList;
         if(apiList&&apiList.length > 0){
+            var $groupUL = $('input[name=group]').next();
             for(var i=0,len = apiList.length;i<len;i++){
                 var info = apiList[i];
                 info.groupName = info.groupName || '未分组';
@@ -106,13 +101,18 @@ var MagicEditor = {
                         id : info.groupName,
                         children : [],
                         spread : true,
+                        groupPrefix : info.groupPrefix,
                         title : info.groupName
                     }
+                    if($groupUL.find('[data-name='+info.groupName+']').length == 0){
+                        $groupUL.append($('<li data-name="'+info.groupName+'" data-prefix="'+(info.groupPrefix || '')+'"/>').append(info.groupName))
+                    }
                 }
                 if(info.show!==false){
                     groups[info.groupName].children.push({
                         id : info.id,
                         groupName : info.groupName,
+                        groupPrefix : info.groupPrefix,
                         name : info.name,
                         title : '<label style="padding-right: 4px;color:#000">' + info.name + "</label>" + info.path,
                         path : info.path
@@ -120,19 +120,31 @@ var MagicEditor = {
                 }
             }
         }
+        for(var key in this.addedGroups){
+            if(!groups[key]){
+                groups[key] = this.addedGroups[key];
+            }
+        }
         var $dom = $('.api-list-container').html('');
         for(var key in groups){
+            var group = groups[key];
             var $item = $('<div/>').addClass('group-item')
                 .addClass('opened')
-                .append($('<div/>').addClass('group-header').append('<i class="iconfont icon-arrow-bottom"></i><i class="iconfont icon-list"></i>').append(key));
-            var $ul = $('<ul/>').addClass('group-list');
-            for(var i =0,len = groups[key].children.length;i<len;i++){
-                var info = groups[key].children[i];
-                $ul.append($('<li/>').attr('data-id',info.id).append('<i class="iconfont icon-script"></i>')
-                    .append('<label>'+info.name+'</label>')
-                    .append('<span>('+info.path+')</span>'));
+                .append($('<div/>').addClass('group-header')
+                    .append('<i class="iconfont icon-arrow-bottom"></i><i class="iconfont icon-list"></i>')
+                    .append($('<label/>').append(key))
+                    .append(group.groupPrefix ? '<span>('+group.groupPrefix+')</span>': '')
+                );
+            if(group.children){
+                var $ul = $('<ul/>').addClass('group-list');
+                for(var i =0,len = group.children.length;i<len;i++){
+                    var info = group.children[i];
+                    $ul.append($('<li/>').attr('data-id',info.id).append('<i class="iconfont icon-script"></i>')
+                        .append('<label>'+info.name+'</label>')
+                        .append('<span>('+info.path+')</span>'));
+                }
+                $item.append($ul);
             }
-            $item.append($ul);
             $dom.append($item);
         }
     },
@@ -151,8 +163,9 @@ var MagicEditor = {
                     $('input[name=name]').val(info.name);
                     $('input[name=path]').val(info.path);
                     MagicEditor.setStatusBar('编辑接口:' + info.name + '(' + info.path + ')')
-                    $('select[name=method]').val(info.method);
-                    $('select[name=group]').val(info.groupName || '未分组');
+                    $('input[name=method]').val(info.method);
+                    $('input[name=group]').val(info.groupName || '未分组');
+                    $('input[name=prefix]').val(info.groupPrefix || '');
                     $('.button-run,.button-delete').removeClass('disabled');
                     _this.scriptEditor && _this.scriptEditor.setValue(info.script);
                     _this.requestEditor && _this.requestEditor.setValue(info.parameter || _this.defaultRequestValue);
@@ -201,29 +214,157 @@ var MagicEditor = {
         s.parentNode.insertBefore(mta, s);
         this.report('visit');
     },
-    deleteGroup : function($header){
-        var groupName = $header.text();
+    // 修改分组
+    updateGroup : function($header){
+        var _this = MagicEditor;
+        var oldGroupName = $header.find('label').text();
+        var oldPrefix = $header.find('span').text();
+        oldPrefix = oldPrefix ? oldPrefix.substring(1,oldPrefix.length - 1) : '';
+        _this.createDialog({
+            title : '修改分组:' + oldGroupName,
+            content : '<label>分组名称:</label><input type="text" name="name" value="'+oldGroupName+'" autocomplete="off"/><div style="height:2px;"></div><label>分组前缀:</label><input type="text" value="'+oldPrefix+'" name="prefix" autocomplete="off"/>',
+            replace : false,
+            buttons : [{
+                name : '修改',
+                click : function($dom){
+                    var groupName = $dom.find('input[name=name]').val();
+                    var groupPrefix = $dom.find('input[name=prefix]').val();
+                    if(!groupName){
+                        $dom.find('input[name=path]').focus();
+                        return false;
+                    }
+                    var exists = false;
+                    $('.group-header').each(function(){
+                        if(this !== $header[0]){
+                            var name = $(this).find('label').text();
+                            if(name == groupName){
+                                exists = true;
+                                return false;
+                            }
+                        }
+                    });
+                    if(exists){
+                        _this.createDialog({
+                            title : '创建分组',
+                            content : '分组已存在!',
+                            buttons : [{name : 'OK'}]
+                        })
+                        return false;
+                    }
+                    _this.ajax({
+                        url : 'group/update',
+                        data : {
+                            oldGroupName : oldGroupName,
+                            groupName : groupName,
+                            prefix : groupPrefix
+                        },
+                        success : function(){
+                            if(_this.addedGroups[oldGroupName]){
+                                delete _this.addedGroups[oldGroupName]
+                            }
+                            _this.addedGroups[groupName] = {
+                                groupName : groupName,
+                                groupPrefix : groupPrefix
+                            }
+                            var apiList = _this.apiList;
+                            if(apiList&&apiList.length > 0){
+                                for(var i=0,len = apiList.length;i<len;i++){
+                                    if(apiList[i].groupName == oldGroupName){
+                                        apiList[i].groupName = groupName;
+                                        apiList[i].groupPrefix = groupPrefix || '';
+                                    }
+                                }
+                            }
+                            var $group = $('input[name=group]');
+                            $group.next().find('li[data-name='+oldGroupName+']').attr('data-prefix',(groupPrefix || '')).attr('data-name',groupName).html(groupName);
+                            if($group.val() == oldGroupName){
+                                $group.val(groupName);
+                                $('input[name=prefix]').val(groupPrefix);
+                            }
+                            $header.find('label').html(groupName).next().html(groupPrefix ? '('+groupPrefix+')' : '');
+                            _this.renderApiList();
+                        }
+                    })
+                }
+            },{
+                name : '取消'
+            }]
+        })
+    },
+    // 创建分组
+    createGroup : function(){
+        var _this = MagicEditor;
+        _this.setStatusBar('创建分组..');
         MagicEditor.createDialog({
+            title : '创建分组',
+            content : '<label>分组名称:</label><input type="text" name="name" autocomplete="off"/><div style="height:2px;"></div><label>分组前缀:</label><input type="text" name="prefix" autocomplete="off"/>',
+            replace : false,
+            buttons : [{
+                name : '创建',
+                click : function($dom){
+                    var groupName = $dom.find('input[name=name]').val();
+                    var groupPrefix = $dom.find('input[name=prefix]').val();
+                    if(!groupName){
+                        $dom.find('input[name=path]').focus();
+                        return false;
+                    }
+                    var exists = false;
+                    $('.group-header').each(function(){
+                        var name = $(this).find('label').text();
+                        if(name == groupName){
+                            exists = true;
+                            return false;
+                        }
+                    });
+                    if(exists){
+                        _this.setStatusBar('分组「'+groupName + '」');
+                        MagicEditor.createDialog({
+                            title : '创建分组',
+                            content : '分组已存在!',
+                            buttons : [{name : 'OK'}]
+                        })
+                        return false;
+                    }
+                    _this.addedGroups[groupName] = {
+                        groupName : groupName,
+                        groupPrefix : groupPrefix
+                    }
+                    _this.setStatusBar('分组「'+groupName + '」创建成功');
+                    $('input[name=group]').next().append($('<li data-name="'+groupName+'" data-prefix="'+(groupPrefix || '')+'"/>').append(groupName));
+                    _this.renderApiList();
+                }
+            },{
+                name : '取消'
+            }]
+        })
+    },
+    // 删除分组
+    deleteGroup : function($header){
+        var groupName = $header.find('label').text();
+        _this.setStatusBar('准备删除分组「'+groupName + '」');
+        var _this = MagicEditor;
+        _this.createDialog({
             title : '删除接口分组',
             content : '是否要删除接口分组「'+groupName + '」',
             buttons : [{
                 name : '删除',
                 click : function(){
-                    MagicEditor.report('group_delete');
+                    _this.report('group_delete');
                     var ids = [];
                     $header.next().find('li').each(function(){
                         ids.push($(this).data('id'));
                     });
-                    MagicEditor.setStatusBar('准备删除接口分组「'+groupName + '」');
-                    MagicEditor.ajax({
+                    _this.setStatusBar('准备删除接口分组「'+groupName + '」');
+                    delete _this.addedGroups[groupName];
+                    _this.ajax({
                         url : 'group/delete',
                         data : {
                             apiIds : ids.join(','),
                             groupName : groupName
                         },
                         success : function(){
-                            MagicEditor.setStatusBar('接口分组「'+groupName + '」已删除');
-                            MagicEditor.loadAPI();  //重新加载
+                            _this.setStatusBar('接口分组「'+groupName + '」已删除');
+                            _this.loadAPI();  //重新加载
                         }
                     })
                 }
@@ -412,6 +553,7 @@ var MagicEditor = {
         var path = $('input[name=path]').val();
         var method = $('input[name=method]').val();
         var groupName = $('input[name=group]').val();
+        var groupPrefix = $('input[name=prefix]').val();
         this.setStatusBar('准备保存接口:' + name + "(" + path + ")");
         var _this = this;
         this.ajax({
@@ -422,6 +564,7 @@ var MagicEditor = {
                 method : method,
                 id : this.apiId,
                 groupName : groupName,
+                groupPrefix : groupPrefix,
                 parameter: this.requestEditor.getValue(),
                 option: this.optionsEditor.getValue(),
                 name : name
@@ -541,9 +684,12 @@ var MagicEditor = {
             }else if(e.keyCode == 83 && (e.metaKey || e.ctrlKey)){  //Ctrl + S
                 _this.doSave();
                 e.preventDefault();
-            }else if(e.keyCode == 78 && e.altKey){  //Ctrl + O
+            }else if(e.keyCode == 78 && e.altKey){  //Alt + N
                 _this.createNew();
                 e.preventDefault();
+            }else if(e.keyCode == 71 && e.altKey){  //Alt + G
+                _this.createGroup();
+                e.preventDefault();
             }
         })
     },
@@ -596,6 +742,14 @@ var MagicEditor = {
                 name : '删除组',
                 shortKey : '',
                 click : _this.deleteGroup
+            },{
+                name : '新建分组',
+                shortKey : 'Alt+G',
+                click : _this.createGroup
+            },{
+                name : '修改分组',
+                shortKey : '',
+                click : _this.updateGroup
             }],e.pageX,e.pageY,$(this));
             return false;
         }).on('contextmenu','.group-list li',function(e){
@@ -622,9 +776,18 @@ var MagicEditor = {
                 name : '删除接口',
                 shortKey : '',
                 click : _this.deleteApi
+            },{
+                name : '新建分组',
+                shortKey : 'Alt+G',
+                click : _this.createGroup
             }],e.pageX,e.pageY,$li)
             return false;
-        }).on('contextmenu',function(){
+        }).on('contextmenu',function(e){
+            _this.createContextMenu([{
+                name : '新建分组',
+                shortKey : 'Alt+G',
+                click : _this.createGroup
+            }],e.pageX,e.pageY,$(this));
             return false;
         })
     },
@@ -636,6 +799,10 @@ var MagicEditor = {
             return false;
         }).on('click','.select ul li',function(){
             var $this = $(this);
+            var prefix = $this.data('prefix');
+            if(prefix !== undefined){
+                $('input[name=prefix]').val(prefix || '');
+            }
             $this.parent().hide().parent().find('input').val($this.text());
             $this.addClass('selected').siblings().removeClass('selected');
             return false;
@@ -867,7 +1034,12 @@ var MagicEditor = {
             $wrapper.remove();
         })
         $dialog.append($header);
-        $dialog.append('<div class="dialog-content">' + options.content.replace(/\n/g,'<br>').replace(/ /g,'&nbsp;').replace(/\t/g,'&nbsp;&nbsp;&nbsp;&nbsp;') + '</div>');
+        var content = options.content;
+        if(options.replace !== false){
+            content = content.replace(/\n/g,'<br>').replace(/ /g,'&nbsp;').replace(/\t/g,'&nbsp;&nbsp;&nbsp;&nbsp;');
+        }
+
+        $dialog.append('<div class="dialog-content">' + content + '</div>');
         var buttons = options.buttons || [];
         var $buttons = $('<div/>').addClass('dialog-buttons').addClass('not-select');
         if(buttons.length > 1){
@@ -881,7 +1053,7 @@ var MagicEditor = {
         var $wrapper = $('<div/>').addClass('dialog-wrapper').append($dialog);
         $buttons.on('click','button',function(){
             var index = $(this).index();
-            if(buttons[index].click&&buttons[index].click() === false){
+            if(buttons[index].click&&buttons[index].click($dialog) === false){
                 return;
             }
             options.close&&options.close();