Browse Source

新增支持自定义选择接口推送和导出

mxd 4 năm trước cách đây
mục cha
commit
2145070729

+ 9 - 0
magic-api/src/main/java/org/ssssssss/magicapi/adapter/Resource.java

@@ -153,4 +153,13 @@ public interface Resource {
 	 */
 	String getAbsolutePath();
 
+	default String getFilePath() {
+		Resource parent = parent();
+		while (parent.parent() != null){
+			parent = parent.parent();
+		}
+		String path = this.getAbsolutePath().replace(parent.getAbsolutePath(), "");
+		return path.startsWith("/") ? path.substring(1) : path;
+	}
+
 }

+ 10 - 14
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicWorkbenchController.java

@@ -9,6 +9,8 @@ import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.util.ResourceUtils;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.multipart.MultipartFile;
@@ -204,13 +206,13 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 	@RequestMapping("/download")
 	@Valid(authorization = Authorization.DOWNLOAD)
 	@ResponseBody
-	public ResponseEntity<?> download(String groupId) throws IOException {
+	public ResponseEntity<?> download(String groupId, @RequestBody(required = false) List<SelectedResource> resources) throws IOException {
+		ByteArrayOutputStream os = new ByteArrayOutputStream();
+		magicApiService.download(groupId, resources, os);
 		if (StringUtils.isBlank(groupId)) {
-			return download(configuration.getWorkspace(), "magic-api-all.zip");
+			return ResponseModule.download(os.toByteArray(), "magic-api-group.zip");
 		} else {
-			Resource resource = configuration.getGroupServiceProvider().getGroupResource(groupId);
-			notNull(resource, GROUP_NOT_FOUND);
-			return download(resource, "magic-api-group.zip");
+			return ResponseModule.download(os.toByteArray(), "magic-api-all.zip");
 		}
 	}
 
@@ -225,8 +227,9 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 
 	@RequestMapping("/push")
 	@ResponseBody
-	public JsonBean<?> push(String target, String secretKey, String mode) {
-		return magicApiService.push(target, secretKey, mode);
+	public JsonBean<?> push(@RequestHeader("magic-push-target") String target, @RequestHeader("magic-push-secret-key")String secretKey,
+							@RequestHeader("magic-push-mode")String mode, @RequestBody List<SelectedResource> resources) {
+		return magicApiService.push(target, secretKey, mode, resources);
 	}
 
 	@ResponseBody
@@ -240,11 +243,4 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 		magicApiService.upload(new ByteArrayInputStream(bytes), mode);
 		return new JsonBean<>();
 	}
-
-	private ResponseEntity<?> download(Resource resource, String filename) throws IOException {
-		ByteArrayOutputStream os = new ByteArrayOutputStream();
-		resource.export(os, Constants.PATH_BACKUPS);
-		return ResponseModule.download(os.toByteArray(), filename);
-	}
-
 }

+ 24 - 0
magic-api/src/main/java/org/ssssssss/magicapi/model/SelectedResource.java

@@ -0,0 +1,24 @@
+package org.ssssssss.magicapi.model;
+
+public class SelectedResource {
+
+	private String id;
+
+	private String type;
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getType() {
+		return type;
+	}
+
+	public void setType(String type) {
+		this.type = type;
+	}
+}

+ 7 - 1
magic-api/src/main/java/org/ssssssss/magicapi/provider/MagicAPIService.java

@@ -5,6 +5,7 @@ import org.ssssssss.magicapi.model.*;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.List;
 import java.util.Map;
 
@@ -175,7 +176,12 @@ public interface MagicAPIService extends MagicModule {
 	 */
 	void upload(InputStream inputStream, String mode) throws IOException;
 
-	JsonBean<?> push(String target, String secretKey, String mode);
+	/**
+	 * 下载
+	 */
+	void download(String groupId, List<SelectedResource> resources, OutputStream os) throws IOException;
+
+	JsonBean<?> push(String target, String secretKey, String mode, List<SelectedResource> resources);
 
 	/**
 	 * 处理刷新通知

+ 53 - 8
magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/DefaultMagicAPIService.java

@@ -48,14 +48,15 @@ import org.ssssssss.script.parsing.ast.Expression;
 import javax.script.ScriptContext;
 import javax.script.SimpleScriptContext;
 import javax.sql.DataSource;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
+import java.io.*;
 import java.sql.Connection;
 import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static org.ssssssss.magicapi.model.Constants.GROUP_METABASE;
 
 public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstants {
 
@@ -510,7 +511,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		Set<FunctionInfo> functionInfos = new LinkedHashSet<>();
 		// 检查上传资源中是否有冲突
 		readPaths(groups, apiPaths, functionPaths, apiInfos, functionInfos, "/", root);
-		Resource item = root.getResource(Constants.GROUP_METABASE);
+		Resource item = root.getResource(GROUP_METABASE);
 		if (item.exists()) {
 			Group group = groupServiceProvider.readGroup(item);
 			// 检查分组是否存在
@@ -566,12 +567,56 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 	}
 
 	@Override
-	public JsonBean<?> push(String target, String secretKey, String mode) {
+	public void download(String groupId, List<SelectedResource> resources, OutputStream os) throws IOException {
+		if (StringUtils.isNotBlank(groupId)) {
+			Resource resource = groupServiceProvider.getGroupResource(groupId);
+			notNull(resource, GROUP_NOT_FOUND);
+			resource.parent().export(os);
+		} else {
+			ZipOutputStream zos = new ZipOutputStream(os);
+			for (SelectedResource item : resources) {
+				StoreServiceProvider storeServiceProvider = null;
+				if("root".equals(item.getType())){
+					zos.putNextEntry(new ZipEntry(item.getId() + "/"));
+					zos.closeEntry();
+				} else if ("group".equals(item.getType())) {
+					Resource resource = groupServiceProvider.getGroupResource(item.getId());
+					zos.putNextEntry(new ZipEntry(resource.getFilePath()));
+					zos.closeEntry();
+					resource = resource.getResource(GROUP_METABASE);
+					zos.putNextEntry(new ZipEntry(resource.getFilePath()));
+					zos.write(resource.read());
+					zos.closeEntry();
+				} else if("api".equals(item.getType())){
+					storeServiceProvider = apiServiceProvider;
+				} else if("function".equals(item.getType())){
+					storeServiceProvider = functionServiceProvider;
+				} else if("datasource".equals(item.getType())){
+					String filename = item.getId() + ".json";
+					Resource resource = datasourceResource.getResource(filename);
+					zos.putNextEntry(new ZipEntry(resource.getFilePath()));
+					zos.write(resource.read());
+					zos.closeEntry();
+				}
+				if(storeServiceProvider != null){
+					MagicEntity entity = storeServiceProvider.get(item.getId());
+					Resource resource = groupServiceProvider.getGroupResource(entity.getGroupId());
+					zos.putNextEntry(new ZipEntry(resource.getFilePath() + entity.getName() + ".ms"));
+					zos.write(storeServiceProvider.serialize(entity));
+					zos.closeEntry();
+				}
+			}
+			zos.close();
+		}
+	}
+
+	@Override
+	public JsonBean<?> push(String target, String secretKey, String mode, List<SelectedResource> resources) {
 		notBlank(target, TARGET_IS_REQUIRED);
 		notBlank(secretKey, SECRET_KEY_IS_REQUIRED);
 		ByteArrayOutputStream baos = new ByteArrayOutputStream();
 		try {
-			workspace.export(baos, Constants.PATH_BACKUPS);
+			download(null, resources, baos);
 		} catch (IOException e) {
 			return new JsonBean<>(-1, e.getMessage());
 		}
@@ -753,7 +798,7 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 	}
 
 	private void readPaths(Set<Group> groups, Set<String> apiPaths, Set<String> functionPaths, Set<ApiInfo> apiInfos, Set<FunctionInfo> functionInfos, String parentPath, Resource root) {
-		Resource resource = root.getResource(Constants.GROUP_METABASE);
+		Resource resource = root.getResource(GROUP_METABASE);
 		String path = "";
 		if (resource.exists()) {
 			Group group = JsonUtils.readValue(resource.read(), Group.class);

+ 10 - 2
magic-editor/src/console/src/components/common/magic-checkbox.vue

@@ -1,7 +1,7 @@
 <template>
-  <div class="ma-checkbox">
+  <div class="ma-checkbox" @click.stop="e=>$emit('click',e)">
     <input :id="cboId" ref="checkbox" type="checkbox" @change="onChange" :checked="value"/>
-    <label :for="cboId"/>
+    <label :for="cboId" :class="{ checkedHalf: checkedHalf&&value }"/>
   </div>
 </template>
 
@@ -10,6 +10,10 @@ export default {
   name: 'MagicCheckbox',
   props: {
     value: {
+      type: [Number,Boolean],
+      default: ()=> false
+    },
+    checkedHalf: {
       type: Boolean,
       default: false
     }
@@ -24,6 +28,7 @@ export default {
   methods: {
     onChange() {
       this.$emit('update:value', this.$refs.checkbox.checked);
+      this.$emit('change', this.$refs.checkbox.checked);
     }
   },
 }
@@ -70,4 +75,7 @@ export default {
   background-color: var(--checkbox-selected-background);
   border-color: var(--checkbox-selected-border);
 }
+.ma-checkbox input+ label.checkedHalf::after {
+  content: "\2501";
+}
 </style>

+ 12 - 2
magic-editor/src/console/src/components/common/modal/magic-dialog.vue

@@ -1,11 +1,11 @@
 <template>
-  <div v-show="value" :class="{ moveable,shade }" class="ma-dialog-wrapper">
+  <div v-show="value" :class="[{ moveable,shade },'ma-dialog-wrapper ' + className]">
     <div :style="{ position, top, left,width,height,'max-width': maxWidth }" class="ma-dialog">
       <div ref="title" class="ma-dialog-header not-select">
         {{ title }}
         <span v-if="showClose" @click="close"><i class="ma-icon ma-icon-close"/></span>
       </div>
-      <div :style="{padding}" class="ma-dialog-content">
+      <div :style="{padding,'max-height': maxHeight,height: contentHeight, overflow: 'auto'}" class="ma-dialog-content">
         <template v-if="content">
           {{ content }}
         </template>
@@ -22,6 +22,10 @@ export default {
   name: 'MagicDialog',
   props: {
     title: String,
+    className: {
+      type: String,
+      default: ''
+    },
     showClose: {
       type: Boolean,
       default: true,
@@ -54,6 +58,12 @@ export default {
     maxWidth: {
       type: String
     },
+    maxHeight: {
+      type: String
+    },
+    contentHeight: {
+      type: String
+    },
     padding: {
       type: String,
       default: '5px 10px'

+ 54 - 14
magic-editor/src/console/src/components/layout/magic-header.vue

@@ -61,8 +61,19 @@
         <button class="ma-button" @click="() => doUpload('full')">全量上传</button>
       </template>
     </magic-dialog>
-    <magic-dialog title="远程推送" :value="showPushDialog" align="right" @onClose="showPushDialog = false" class="ma-remote-push-container" width="400px">
+    <magic-dialog v-if="exportVisible" v-model="exportVisible" title="导出"  align="right" :moveable="false" width="340px" height="490px" className="ma-tree-wrapper">
+      <template #content>
+        <magic-resource-choose ref="resourceExport" height="400px" max-height="400px"/>
+      </template>
+      <template #buttons>
+        <button class="ma-button" @click="$refs.resourceExport.doSelectAll(true)">全选</button>
+        <button class="ma-button" @click="$refs.resourceExport.doSelectAll(false)">取消全选</button>
+        <button class="ma-button active" @click="doExport">导出</button>
+      </template>
+    </magic-dialog>
+    <magic-dialog title="远程推送" :value="showPushDialog" align="right" @onClose="showPushDialog = false" class="ma-remote-push-container ma-tree-wrapper" width="400px" height="540px">
         <template #content>
+            <magic-resource-choose ref="resourcePush" height="400px" max-height="400px"/>
             <div>
                 <label>远程地址:</label>
                 <magic-input placeholder="请输入远程地址" v-model="target" width="300px"/>
@@ -73,6 +84,8 @@
             </div>
         </template>
         <template #buttons>
+            <button class="ma-button" @click="$refs.resourcePush.doSelectAll(true)">全选</button>
+            <button class="ma-button" @click="$refs.resourcePush.doSelectAll(false)">取消全选</button>
             <button class="ma-button active" @click="() => doPush('increment')">增量推送</button>
             <button class="ma-button" @click="() => doPush('full')">全量推送</button>
         </template>
@@ -90,6 +103,7 @@ import store from '@/scripts/store.js'
 import request from '@/api/request.js'
 import MagicDialog from '@/components/common/modal/magic-dialog.vue'
 import MagicInput from '@/components/common/magic-input.vue'
+import MagicResourceChoose from '@/components/resources/magic-resource-choose.vue'
 import MagicSearch from './magic-search.vue'
 
 export default {
@@ -97,7 +111,8 @@ export default {
   components: {
     MagicDialog,
     MagicInput,
-    MagicSearch
+    MagicSearch,
+    MagicResourceChoose
   },
   props: {
     config: Object,
@@ -111,6 +126,7 @@ export default {
       skinVisible: false,
       showUploadDialog: false,
       showPushDialog: false,
+      exportVisible: false,
       filename: null,
       target: 'http://host:port/_magic-api-sync',
       secretKey: '123456789',
@@ -131,11 +147,27 @@ export default {
       window.open(url)
     },
     download() {
-      request.send('/download', null, {
-        responseType: 'blob'
-      }).success(blob => {
-        downloadFile(blob, 'magic-api-all.zip')
-      });
+      this.exportVisible = true
+      // request.send('/download', null, {
+      //   responseType: 'blob'
+      // }).success(blob => {
+      //   downloadFile(blob, 'magic-api-all.zip')
+      // });
+    },
+    doExport() {
+      let selected = this.$refs.resourceExport.getSelected()
+      if(selected.length > 0){
+        request.send('/download', JSON.stringify(selected), {
+          method: 'post',
+          headers: {
+            'Content-Type': 'application/json'
+          },
+          transformRequest: [],
+          responseType: 'blob'
+        }).success(blob => {
+           downloadFile(blob, 'magic-api.zip')
+        });
+      }
     },
     onFileSelected() {
       if (this.$refs.file.files[0]) {
@@ -149,16 +181,24 @@ export default {
       this.showUploadDialog = true;
     },
     doPush(mode){
-        request.send('/push',{
-            target: this.target,
-            secretKey: this.secretKey,
-            mode: mode
-        }).success(() => {
+        let selected = this.$refs.resourcePush.getSelected()
+        if(selected.length > 0) {
+          request.send('/push', JSON.stringify(selected),{
+            method: 'post',
+            headers: {
+              'magic-push-target':  this.target,
+              'magic-push-secret-key': this.secretKey,
+              'magic-push-mode': mode,
+              'Content-Type': 'application/json'
+            },
+            transformRequest: []
+          }).success(() => {
             this.$magicAlert({
-                content: '推送成功!'
+              content: '推送成功!'
             })
             this.showPushDialog = false;
-        })
+          })
+        }
     },
     doUpload(mode) {
       let file = this.$refs.file.files[0];

+ 1 - 1
magic-editor/src/console/src/components/magic-editor.vue

@@ -256,7 +256,7 @@ export default {
       })
     },
     async checkUpdate() {
-      fetch('https://img.shields.io/maven-central/v/org.ssssssss/magic-api.json')
+      fetch('https://img.shields.io/maven-metadata/v.json?label=maven-central&metadataUrl=https%3A%2F%2Frepo1.maven.org%2Fmaven2%2Forg%2Fssssssss%2Fmagic-api%2Fmaven-metadata.xml')
         .then(response => {
           if (response.status === 200) {
             response.json().then(json => {

+ 5 - 4
magic-editor/src/console/src/components/resources/magic-api-list.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="ma-api-tree">
+  <div class="ma-tree-wrapper">
     <div class="ma-tree-toolbar">
       <div class="ma-tree-toolbar-search"><i class="ma-icon ma-icon-search"></i><input placeholder="输入关键字搜索"
                                                                                        @input="e => doSearch(e.target.value)"/>
@@ -370,9 +370,10 @@ export default {
             label: '导出',
             icon: 'ma-icon-download',
             onClick: () => {
-              request.send('download',{
-                groupId: item.id
-              },{
+              request.send(`/download?groupId=${item.id}`,null,{
+                headers: {
+                  'Content-Type': 'application/json'
+                },
                 responseType: 'blob'
               }).success(blob => downloadFile(blob,`${item.name}.zip`))
             }

+ 2 - 5
magic-editor/src/console/src/components/resources/magic-datasource-list.vue

@@ -1,6 +1,6 @@
 <template>
   <div style="width: 100%;height: 100%;background: var(--toolbox-background)">
-    <div class="ma-api-tree">
+    <div class="ma-tree-wrapper">
       <div class="ma-tree-toolbar">
         <div class="ma-tree-toolbar-search">
           <i class="ma-icon ma-icon-search"></i>
@@ -294,9 +294,6 @@ ul li {
 ul li:hover{
   background: var(--toolbox-list-hover-background);
 }
-.ma-icon-datasource{
-  color: #089910;
-}
 .ds-form{
   margin-bottom: 5px;
 }
@@ -312,7 +309,7 @@ ul li:hover{
 .ma-editor span{
   color: unset;
 }
-.ma-api-tree{
+.ma-tree-wrapper{
   width: 100%;
   height: 100%;
 }

+ 1 - 1
magic-editor/src/console/src/components/resources/magic-function-list.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="ma-api-tree">
+  <div class="ma-tree-wrapper">
     <div class="ma-tree-toolbar">
       <div class="ma-tree-toolbar-search"><i class="ma-icon ma-icon-search"></i><input placeholder="输入关键字搜索"
                                                                                        @input="e => doSearch(e.target.value)"/>

+ 361 - 0
magic-editor/src/console/src/components/resources/magic-resource-choose.vue

@@ -0,0 +1,361 @@
+<template>
+    <magic-tree :data="tree" :forceUpdate="forceUpdate" :style="{ height, maxHeight}" style="overflow: auto">
+      <template #folder="{ item }">
+        <div
+            :style="{ 'padding-left': 17 * item.level + 'px' }"
+            :title="`${item.name||''}${item.parentId !== 'root' ? '(' + (item.path || '') + ')' : ''}`"
+            class="ma-tree-item-header ma-tree-hover"
+            @click.stop="$set(item,'opened',!item.opened)"
+        >
+          <magic-checkbox v-model="item.selected" :checked-half="item.checkedHalf" @change="e => doSelected(item,e)"/>
+          <i :class="item.opened ? 'ma-icon-arrow-bottom' : 'ma-icon-arrow-right'" class="ma-icon" />
+          <i class="ma-icon ma-icon-list"></i>
+          <label>{{ item.name }}</label>
+          <span v-if="item.parentId !== 'root'">({{ item.path }})</span>
+        </div>
+      </template>
+      <template #file="{ item }">
+        <div
+            :style="{ 'padding-left': 17 * item.level + 'px' }"
+            class="ma-tree-hover"
+            :title="(item.name || '') + '(' + (item.path || '') + ')'"
+            @click.stop="doSelected(item,item.selected = !item.selected)"
+        >
+          <magic-checkbox v-model="item.selected" @change="e => doSelected(item,e)"/>
+          <i v-if="item._type === 'api'" class="ma-svg-icon" :class="['request-method-' + item.method]" />
+          <i v-if="item._type === 'function'" class="ma-svg-icon icon-function" />
+          <i v-if="item._type === 'datasource'" class="ma-icon ma-icon-datasource" />
+          <label>{{ item.name }}</label>
+          <span>({{ item.path }})</span>
+        </div>
+      </template>
+    </magic-tree>
+</template>
+
+<script>
+import bus from '../../scripts/bus.js'
+import MagicTree from '../common/magic-tree.vue'
+import request from '@/api/request.js'
+import contants from '@/scripts/contants.js'
+import MagicCheckbox from "@/components/common/magic-checkbox";
+
+export default {
+  name: 'MagicResourceChoose',
+  props: {
+    height: {
+      type: String,
+      required: true
+    },
+    maxHeight: {
+      type: String,
+      required: true
+    }
+  },
+  components: {
+    MagicCheckbox,
+    MagicTree
+  },
+  data() {
+    return {
+      vieible: true,
+      bus: bus,
+      // 分组list数据
+      listGroupData: [],
+      // 接口list数据
+      listChildrenData: [],
+      // 分组+接口tree数据
+      tree: [],
+      // 数据排序规则,true:升序,false:降序
+      treeSort: true,
+      // 换成一个临时对象,修改使用
+      tempGroupObj: {},
+      // 当前打开的文件
+      currentFileItem: {},
+      // 绑定给magic-tree组件,用来触发子组件强制更新
+      forceUpdate: true,
+    }
+  },
+  created() {
+    this.initData();
+  },
+  methods: {
+    // 初始化数据
+    initData() {
+      this.tree = []
+      this.listGroupData = [
+          { id: 'api',_type: 'root', name: '1.接口列表', parentId: 'root', path:'', selected: false, checkedHalf: false},
+          { id: 'function',_type: 'root', name: '2.函数列表', parentId: 'root', path:'', selected: false, checkedHalf: false},
+          { id: 'datasource',_type: 'root', name: '3.数据源', parentId: 'root', path:'', selected: false, checkedHalf: false}
+      ]
+      request.send('group/list?type=1').success(data => {
+        this.listGroupData.push(...data.map(it => {
+          it.parentId = it.parentId == '0' ? 'api' : it.parentId;
+          it.selected = false;
+          it.checkedHalf = false;
+          it._type = 'group';
+          return it;
+        }))
+        request.send('list').success(data => {
+          this.listChildrenData.push(...data.map(it => {
+            it._type = 'api';
+            it.selected = false;
+            return it;
+          }))
+          this.initTreeData()
+        })
+      })
+      request.send('group/list?type=2').success(data => {
+        this.listGroupData.push(...data.map(it => {
+          it.parentId = it.parentId == '0' ? 'function' : it.parentId;
+          it.selected = false;
+          it.checkedHalf = false;
+          it._type = 'group'
+          return it;
+        }))
+        request.send('function/list').success(data => {
+          this.listChildrenData.push(...data.map(it => {
+            it._type = 'function';
+            it.selected = false;
+            return it;
+          }))
+          this.initTreeData()
+        })
+      })
+      request.send('datasource/list').success(data => {
+        this.listChildrenData.push(...data.filter(it => it.id).map(it => {
+          it._type = 'datasource';
+          it.selected = false;
+          it.path = it.key;
+          it.groupId = 'datasource'
+          return it;
+        }))
+        this.initTreeData()
+      })
+    },
+    // 初始化tree结构数据
+    initTreeData() {
+      // 1.把所有的分组id存map,方便接口列表放入,root为没有分组的接口
+      let groupItem = {root: []}
+      this.listGroupData.forEach(element => {
+        groupItem[element.id] = []
+        element.folder = true
+        this.$set(element, 'opened', contants.DEFAULT_EXPAND)
+        // 缓存一个name和path给后面使用
+        element.tmpName = element.name.indexOf('/') === 0 ? element.name : '/' + element.name
+        element.tmpPath = element.path.indexOf('/') === 0 ? element.path : '/' + element.path
+      })
+      // 2.把所有的接口列表放入分组的children
+      this.listChildrenData.forEach((element, index) => {
+        element.tmp_id = element.id
+        if (groupItem[element.groupId]) {
+          groupItem[element.groupId].push(element)
+        } else {
+          element.groupName = ''
+          element.groupPath = ''
+          groupItem['root'].push(element)
+        }
+      })
+      // 3.将分组列表变成tree,并放入接口列表,分组在前,接口在后
+      let arrayToTree = (arr, parentItem, groupName, groupPath, level) => {
+        //  arr 是返回的数据  parendId 父id
+        let temp = []
+        let treeArr = arr
+        treeArr.forEach((item, index) => {
+          if (item.parentId == parentItem.id) {
+            item.level = level
+            item.tmpName = groupName + item.tmpName
+            item.tmpPath = groupPath + item.tmpPath
+            // 递归调用此函数
+            item.children = arrayToTree(treeArr, item, item.tmpName, item.tmpPath, level + 1)
+            if (groupItem[item.id]) {
+              groupItem[item.id].forEach(element => {
+                element.level = item.level + 1
+                element.groupName = item.tmpName
+                element.groupPath = item.tmpPath
+                element.groupId = item.id
+                this.$set(item.children, item.children.length, element)
+              })
+            }
+            this.$set(temp, temp.length, treeArr[index])
+          }
+        })
+        return temp
+      }
+      this.tree = [...arrayToTree(this.listGroupData, {id: 'root'}, '', '', 0), ...groupItem['root']]
+      this.sortTree()
+    },
+    getSelected() {
+      let array = []
+      let process = (node) => {
+        array.push({
+          type: node._type || 'group',
+          id: node.id
+        })
+        node.children && node.children.filter(it => it.selected).forEach(it => process(it))
+      }
+      this.tree.filter(it => it.selected).forEach(it => process(it))
+      return array
+    },
+    doSelectAll(flag) {
+      let process = (node) => {
+        node.selected = flag
+        if(node.folder){
+          node.checkedHalf = false
+        }
+        node.children && node.children.forEach(it => process(it))
+      }
+      this.tree.forEach(it => process(it))
+    },
+    doSelected(item,selected) {
+      let process = node => {
+        node.selected = selected
+        node.checkedHalf = !selected
+        node.children&&node.children.forEach(it => process(it))
+      }
+      item.selected = selected;
+      item.children&&item.children.forEach(it => process(it))
+      if(item.folder){
+        item.checkedHalf = false
+      }
+      this.getParents(item.folder ? item.parentId : item.groupId).forEach(node => {
+        node.selected = node.children.some(it => it.selected)
+        node.checkedHalf = node.children.some(it => !it.selected || it.checkedHalf)
+        console.log(node.name,node)
+      })
+    },
+    // 重新构建tree的path和name,第一个参数表示是否全部折叠
+    rebuildTree(folding) {
+      let buildHandle = (arr, parentItem, level) => {
+        arr.forEach(element => {
+          element.level = level
+          // 处理分组
+          if (element.folder === true) {
+            element.tmpName = (parentItem.tmpName + '/' + element.name).replace(new RegExp('(/)+', 'gm'), '/')
+            element.tmpPath = (parentItem.tmpPath + '/' + element.path).replace(new RegExp('(/)+', 'gm'), '/')
+            if (folding === true) {
+              this.$set(element, 'opened', false)
+            }
+            if (element.children && element.children.length > 0) {
+              buildHandle(element.children, element, level + 1)
+            }
+          } else {
+            // 处理接口
+            element.groupName = parentItem.tmpName
+            element.groupPath = parentItem.tmpPath
+            element.groupId = parentItem.id
+          }
+        })
+      }
+      buildHandle(this.tree, {tmpName: '', tmpPath: ''}, 0)
+      if (this.currentFileItem.tmp_id) {
+        this.open(this.currentFileItem)
+      }
+      this.sortTree()
+    },
+    treeSortHandle(flag) {
+      this.treeSort = !this.treeSort
+      this.sortTree()
+    },
+    // 排序tree,分组在前,接口在后
+    sortTree() {
+      if (this.treeSort === null) {
+        return
+      }
+      let sortItem = function (item1, item2) {
+        return item1.name.localeCompare(item2.name, 'zh-CN')
+      }
+      let sortHandle = arr => {
+        // 分组
+        let folderArr = []
+        // 接口
+        let fileArr = []
+        arr.forEach(element => {
+          if (element.folder === true) {
+            if (element.children && element.children.length > 0) {
+              element.children = sortHandle(element.children)
+            }
+            folderArr.push(element)
+          } else {
+            fileArr.push(element)
+          }
+        })
+        folderArr.sort(sortItem)
+        fileArr.sort(sortItem)
+        if (this.treeSort === false) {
+          folderArr.reverse()
+          fileArr.reverse()
+        }
+        return folderArr.concat(fileArr)
+      }
+      this.tree = sortHandle(this.tree)
+      this.changeForceUpdate()
+    },
+    // 将文件类型的对象,放入到点击的同级
+    pushFileItemToGroup(tree, newItem) {
+      // 标记是否找到对应的item,找到了就退出递归
+      let find = false
+      for (const index in tree) {
+        const element = tree[index]
+        // 排除分组类型
+        if (element.folder === true && element.id === newItem.groupId) {
+          this.$set(element.children, element.children.length, newItem)
+          this.changeForceUpdate()
+          return true
+        } else if (element.children && element.children.length > 0) {
+          find = this.pushFileItemToGroup(element.children, newItem)
+        }
+        if (find === true) {
+          return true
+        }
+      }
+    },
+    // 强制触发子组件更新
+    changeForceUpdate() {
+      this.forceUpdate = !this.forceUpdate
+    },
+    getParents(id){
+      let findId = id;
+      let result = [];
+      let handle= (items) => {
+        items.forEach(item => {
+          if (item.id === findId) {
+            result.push(item)
+            findId = item.parentId;
+            if(item.id !== 'root'){
+              handle(this.tree)
+            }
+          } else if (item.children && item.children.length > 0) {
+            handle(item.children)
+          }
+        })
+      }
+      handle(this.tree)
+      return result
+    }
+  },
+  mounted() {
+    console.log('mounted')
+  }
+}
+</script>
+
+<style>
+@import './magic-resource.css';
+.ma-tree-wrapper .ma-checkbox input + label::after{
+  width: 12px !important;
+  height: 12px !important;
+  line-height: 12px !important;
+  top: 0 !important;
+  left: -5px !important;
+}
+</style>
+<style scoped>
+.ma-checkbox{
+  display: inline-block;
+  width: 20px;
+  height: 12px;
+}
+.ma-tree-wrapper .ma-tree-container{
+  height: 100%;
+}
+</style>

+ 23 - 19
magic-editor/src/console/src/components/resources/magic-resource.css

@@ -1,4 +1,4 @@
-.ma-api-tree {
+.ma-tree-wrapper:not(.ma-dialog-wrapper) {
     text-align: left;
     float: left;
     border-right: 1px solid var(--toolbox-border-right-color);
@@ -9,60 +9,60 @@
     position: relative;
 }
 
-.ma-api-tree .ma-tree-item .ma-tree-item-header {
+.ma-tree-wrapper .ma-tree-item .ma-tree-item-header {
     font-weight: bold;
     height: 20px;
     line-height: 20px;
 }
 
-.ma-api-tree .ma-tree-item .ma-tree-sub-items > * {
+.ma-tree-wrapper .ma-tree-item .ma-tree-sub-items > * {
     line-height: 20px;
     /* padding-left: 17px; */
     white-space: nowrap;
 }
 
-.ma-api-tree .ma-tree-hover:hover,
-.ma-api-tree .ma-tree-select {
+.ma-tree-wrapper .ma-tree-hover:hover,
+.ma-tree-wrapper .ma-tree-select {
     background: var(--toolbox-list-hover-background);
 }
 
-.ma-api-tree .ma-tree-item .ma-tree-sub-items > *.selected {
+.ma-tree-wrapper .ma-tree-item .ma-tree-sub-items > *.selected {
     background: var(--toolbox-list-selected-background);
 }
 
-.ma-api-tree .ma-icon {
+.ma-tree-wrapper .ma-icon {
     color: var(--toolbox-list-icon-color);
     padding-right: 2px;
     font-size: 14px;
 }
-.ma-api-tree .ma-icon-arrow-bottom {
+.ma-tree-wrapper .ma-icon-arrow-bottom {
     color: var(--toolbox-list-arrow-color);
 }
 
-.ma-api-tree span {
+.ma-tree-wrapper span {
     color: var(--toolbox-list-span-color);
 }
 
-.ma-api-tree label {
+.ma-tree-wrapper label {
     color: var(--toolbox-list-label-color);
 }
 
-.ma-api-tree .ma-tree-toolbar-search {
+.ma-tree-wrapper .ma-tree-toolbar-search {
     flex: 1;
 }
 
-.ma-api-tree .ma-tree-toolbar-search input {
+.ma-tree-wrapper .ma-tree-toolbar-search input {
     border: none;
     background: none;
     height: 100%;
     color: var(--input-color);
 }
 
-.ma-api-tree .ma-tree-toolbar-search input:focus {
+.ma-tree-wrapper .ma-tree-toolbar-search input:focus {
     outline: none;
 }
 
-.ma-api-tree .ma-tree-toolbar {
+.ma-tree-wrapper .ma-tree-toolbar {
     background: var(--background);
     color: var(--toolbox-list-label-color);
     border-bottom: 1px solid var(--border-color);
@@ -72,22 +72,26 @@
     padding: 1px;
 }
 
-.ma-api-tree .ma-tree-toolbar-btn {
+.ma-tree-wrapper .ma-tree-toolbar-btn {
     padding: 2px;
     align-self: flex-end;
     display: inline-block;
 }
 
-.ma-api-tree .ma-tree-toolbar-btn:hover,
-.ma-api-tree .ma-tree-toolbar-btn.hover {
+.ma-tree-wrapper .ma-tree-toolbar-btn:hover,
+.ma-tree-wrapper .ma-tree-toolbar-btn.hover {
     background: var(--toolbox-list-hover-background);
 }
 
-.ma-api-tree .ma-tree-toolbar i {
+.ma-tree-wrapper .ma-tree-toolbar i {
     color: var(--toolbox-list-header-icon-color);
 }
 
-.ma-api-tree .ma-tree-container {
+.ma-tree-wrapper .ma-tree-container {
     height: calc(100% - 25px);
     overflow: auto;
 }
+
+.ma-tree-wrapper .ma-icon-datasource{
+    color: #089910;
+}