diff --git a/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/SwaggerDocumentController.java b/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/SwaggerDocumentController.java index 9d547948..2ac9d7f2 100644 --- a/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/SwaggerDocumentController.java +++ b/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/SwaggerDocumentController.java @@ -75,7 +75,8 @@ public class SwaggerDocumentController { // UI地址替换为文档json地址 String docUrl = SwaggerDocUtil.replaceSwaggerResources(swaggerDoc.getDocUrl()); if (SwaggerDocUtil.isSwaggerResources(docUrl)) { - String resourcesStr = swaggerHttpRequestService.requestSwaggerUrl(request, docUrl); + String swaggerDomain = SwaggerDocUtil.getSwaggerResourceDomain(docUrl); + String resourcesStr = swaggerHttpRequestService.requestSwaggerUrl(request, docUrl, swaggerDomain); List resourceList = JSON.parseArray(resourcesStr, SwaggerResource.class); if (resourceList == null || resourceList.isEmpty()) { return DocResponseJson.warn("该地址未找到文档"); @@ -85,7 +86,6 @@ public class SwaggerDocumentController { swaggerDocService.removeById(swaggerDoc.getId()); } // 存明细地址 - String swaggerDomain = SwaggerDocUtil.getSwaggerResourceDomain(docUrl); for (SwaggerResource resource : resourceList) { swaggerDoc.setId(null); swaggerDoc.setDocUrl(swaggerDomain + resource.getUrl()); diff --git a/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/SwaggerProxyController.java b/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/SwaggerProxyController.java index 446917f4..c2848f21 100644 --- a/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/SwaggerProxyController.java +++ b/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/SwaggerProxyController.java @@ -5,6 +5,7 @@ import com.zyplayer.doc.core.exception.ConfirmException; import com.zyplayer.doc.data.repository.manage.entity.SwaggerDoc; import com.zyplayer.doc.data.service.manage.SwaggerDocService; import com.zyplayer.doc.swaggerplus.controller.vo.SwaggerResourceVo; +import com.zyplayer.doc.swaggerplus.framework.utils.SwaggerDocUtil; import com.zyplayer.doc.swaggerplus.service.SwaggerHttpRequestService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -63,7 +64,8 @@ public class SwaggerProxyController { throw new ConfirmException("文档不存在"); } if (Objects.equals(swaggerDoc.getDocType(), 1)) { - String contentStr = swaggerHttpRequestService.requestSwaggerUrl(request, swaggerDoc.getDocUrl()); + String docsDomain = SwaggerDocUtil.getV2ApiDocsDomain(swaggerDoc.getDocUrl()); + String contentStr = swaggerHttpRequestService.requestSwaggerUrl(request, swaggerDoc.getDocUrl(), docsDomain); return new ResponseEntity<>(new Json(contentStr), HttpStatus.OK); } return new ResponseEntity<>(new Json(swaggerDoc.getJsonContent()), HttpStatus.OK); diff --git a/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/param/ProxyRequestParam.java b/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/param/ProxyRequestParam.java index 68edf118..130a7d87 100644 --- a/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/param/ProxyRequestParam.java +++ b/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/param/ProxyRequestParam.java @@ -12,6 +12,7 @@ import java.util.List; */ public class ProxyRequestParam { private String url; + private String host; private String method; private String contentType; private String headerParam; @@ -99,4 +100,12 @@ public class ProxyRequestParam { public void setContentType(String contentType) { this.contentType = contentType; } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } } diff --git a/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/vo/ProxyRequestResultVo.java b/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/vo/ProxyRequestResultVo.java index 10a2bab0..d48a6a44 100644 --- a/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/vo/ProxyRequestResultVo.java +++ b/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/controller/vo/ProxyRequestResultVo.java @@ -8,6 +8,7 @@ public class ProxyRequestResultVo { private List headers; private Integer status; private Long useTime; + private Integer bodyLength; private String data; private String errorMsg; @@ -58,4 +59,12 @@ public class ProxyRequestResultVo { public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; } + + public Integer getBodyLength() { + return bodyLength; + } + + public void setBodyLength(Integer bodyLength) { + this.bodyLength = bodyLength; + } } diff --git a/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/framework/utils/SwaggerDocUtil.java b/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/framework/utils/SwaggerDocUtil.java index b21b4214..f50aae1f 100644 --- a/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/framework/utils/SwaggerDocUtil.java +++ b/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/framework/utils/SwaggerDocUtil.java @@ -22,6 +22,24 @@ public class SwaggerDocUtil { return ""; } + public static String getV2ApiDocsDomain(String docUrl) { + int index = docUrl.indexOf("/v2/api-docs"); + if (index >= 0) { + return docUrl.substring(0, index); + } + return ""; + } + + public static String getDomainHost(String domain) { + domain = domain.replace("http://", ""); + domain = domain.replace("https://", ""); + int index = domain.indexOf("/"); + if (index >= 0) { + return domain.substring(0, index); + } + return domain; + } + public static boolean isSwaggerLocation(String docUrl) { return docUrl.contains("/v2/api-docs"); } diff --git a/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/service/SwaggerHttpRequestService.java b/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/service/SwaggerHttpRequestService.java index 6c7ed964..a5067f5d 100644 --- a/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/service/SwaggerHttpRequestService.java +++ b/zyplayer-doc-swagger-plus/src/main/java/com/zyplayer/doc/swaggerplus/service/SwaggerHttpRequestService.java @@ -2,6 +2,7 @@ package com.zyplayer.doc.swaggerplus.service; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; import cn.hutool.http.Method; import com.zyplayer.doc.core.exception.ConfirmException; import com.zyplayer.doc.data.repository.manage.entity.SwaggerGlobalParam; @@ -10,9 +11,9 @@ import com.zyplayer.doc.swaggerplus.controller.param.ProxyRequestParam; import com.zyplayer.doc.swaggerplus.controller.vo.HttpCookieVo; import com.zyplayer.doc.swaggerplus.controller.vo.HttpHeaderVo; import com.zyplayer.doc.swaggerplus.controller.vo.ProxyRequestResultVo; +import com.zyplayer.doc.swaggerplus.framework.utils.SwaggerDocUtil; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; -import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Service; @@ -32,13 +33,16 @@ public class SwaggerHttpRequestService { private static final Map requestMethodMap = Stream.of(Method.values()).collect(Collectors.toMap(val -> val.name().toLowerCase(), val -> val)); + List domainHeaderKeys = Arrays.asList("referer", "origin"); + List needRequestHeaderKeys = Arrays.asList("user-agent"); + /** * 请求真实的swagger文档内容 * * @author 暮光:城中城 * @since 2021-11-04 */ - public String requestSwaggerUrl(HttpServletRequest request, String docUrl) { + public String requestSwaggerUrl(HttpServletRequest request, String docUrl, String docDomain) { List globalParamList = swaggerGlobalParamService.getGlobalParamList(); Map globalFormParamMap = globalParamList.stream().filter(item -> Objects.equals(item.getParamType(), 1)) .collect(Collectors.toMap(SwaggerGlobalParam::getParamKey, SwaggerGlobalParam::getParamValue)); @@ -46,10 +50,15 @@ public class SwaggerHttpRequestService { .collect(Collectors.toMap(SwaggerGlobalParam::getParamKey, SwaggerGlobalParam::getParamValue)); Map globalCookieParamMap = globalParamList.stream().filter(item -> Objects.equals(item.getParamType(), 3)) .collect(Collectors.toMap(SwaggerGlobalParam::getParamKey, SwaggerGlobalParam::getParamValue)); + Map requestHeaders = this.getHttpHeader(request, globalHeaderParamMap); + if (StringUtils.isNotBlank(docDomain)) { + domainHeaderKeys.forEach(key -> requestHeaders.put(key, docDomain)); + requestHeaders.put("host", SwaggerDocUtil.getDomainHost(docDomain)); + } // 执行请求 String resultStr = HttpRequest.get(docUrl) .form(globalFormParamMap) - .addHeaders(this.getHttpHeader(request, globalHeaderParamMap)) + .addHeaders(requestHeaders) .header("Accept", "application/json, text/javascript, */*; q=0.01") .cookie(this.getHttpCookie(request, globalCookieParamMap)) .timeout(10000).execute().body(); @@ -71,9 +80,15 @@ public class SwaggerHttpRequestService { if (method == null) { throw new ConfirmException("不支持的请求方式:" + requestParam.getMethod()); } - HttpRequest httpRequest = new HttpRequest(requestParam.getUrl()).setMethod(method); + HttpRequest httpRequest = HttpUtil.createRequest(method, requestParam.getUrl()); + // header获取 + Map requestHeaders = this.getHttpHeader(request, Collections.emptyMap()); + if (StringUtils.isNotBlank(requestParam.getHost())) { + domainHeaderKeys.forEach(key -> requestHeaders.put(key, requestParam.getHost())); + requestHeaders.put("host", SwaggerDocUtil.getDomainHost(requestParam.getHost())); + } // http自带参数 - httpRequest.addHeaders(this.getHttpHeader(request, Collections.emptyMap())); + httpRequest.addHeaders(requestHeaders); httpRequest.cookie(this.getHttpCookie(request, Collections.emptyMap())); // 用户输入的参数 requestParam.getFormParamData().forEach(data -> httpRequest.form(data.getCode(), data.getValue())); @@ -90,6 +105,7 @@ public class SwaggerHttpRequestService { HttpResponse httpResponse = httpRequest.timeout(10000).execute(); resultVo.setData(httpResponse.body()); resultVo.setStatus(httpResponse.getStatus()); + resultVo.setBodyLength(httpResponse.bodyBytes().length); // 设置返回的cookies List responseCookies = httpResponse.getCookies(); if (CollectionUtils.isNotEmpty(responseCookies)) { @@ -141,13 +157,11 @@ public class SwaggerHttpRequestService { private Map getHttpHeader(HttpServletRequest request, Map globalHeaderParamMap) { Map headerParamMap = new HashMap<>(); Enumeration headerNames = request.getHeaderNames(); - String[] notNeedHeaders = new String[]{"referer", "origin", "host"}; while (headerNames.hasMoreElements()) { - String headerName = headerNames.nextElement(); - if (ArrayUtils.contains(notNeedHeaders, headerName)) { - continue; + String headerName = StringUtils.lowerCase(headerNames.nextElement()); + if (needRequestHeaderKeys.contains(headerName)) { + headerParamMap.put(headerName, request.getHeader(headerName)); } -// headerParamMap.put(headerName, request.getHeader(headerName)); } if (MapUtils.isNotEmpty(globalHeaderParamMap)) { headerParamMap.putAll(globalHeaderParamMap); diff --git a/zyplayer-doc-ui/swagger-ui/src/assets/ace-editor/DatabaseCompleter.js b/zyplayer-doc-ui/swagger-ui/src/assets/ace-editor/DatabaseCompleter.js new file mode 100644 index 00000000..b226a36d --- /dev/null +++ b/zyplayer-doc-ui/swagger-ui/src/assets/ace-editor/DatabaseCompleter.js @@ -0,0 +1,209 @@ + +import datasourceApi from '../../api/datasource' +/** + * 编辑框自动提示数据库、表和字段等 + */ +export default { + isInit: false, + source: {}, + databaseInfo: {}, + tableInfo: {}, + columnInfo: {}, + lastCallbackArr: [], + isAutocomplete: false, + change(source) { + this.source = source; + this.lastCallbackArr = []; + console.log("change(sourceId):" + JSON.stringify(this.source)); + if (!this.isInit) { + console.log("change(sourceId),isInit:" + this.isInit); + this.isInit = true; + let languageTools = ace.acequire("ace/ext/language_tools"); + languageTools.addCompleter(this); + } + // 初始加载 + if (!!this.source.sourceId) { + // 加载所有库 + let databaseList = this.databaseInfo[this.source.sourceId] || []; + if (databaseList.length <= 0) { + datasourceApi.databaseList({sourceId: this.source.sourceId}).then(json => { + this.databaseInfo[this.source.sourceId] = json.data || []; + }); + } + // 加载库下所有表 + if (!!this.source.dbName) { + let tableKey = this.source.sourceId + '_' + this.source.dbName; + let tableList = this.tableInfo[tableKey] || []; + if (tableList.length <= 0) { + datasourceApi.tableList({sourceId: this.source.sourceId, dbName: this.source.dbName}).then(json => { + this.tableInfo[tableKey] = json.data || []; + }); + } + } + // 加载表下所有字段 + if (!!this.source.tableName) { + let columnKey = this.source.sourceId + '_' + this.source.dbName + '_' + this.source.tableName; + let columnList = this.columnInfo[columnKey] || []; + if (columnList.length <= 0) { + datasourceApi.tableColumnList({sourceId: this.source.sourceId, dbName: this.source.dbName, tableName: this.source.tableName}).then(json => { + this.columnInfo[columnKey] = json.data.columnList || []; + }); + } + } + } + }, + startAutocomplete(editor) { + this.isAutocomplete = true; + editor.execCommand("startAutocomplete"); + }, + async getCompletions(editor, session, pos, prefix, callback) { + let callbackArr = []; + let endPos = this.isAutocomplete ? pos.column : pos.column - 1; + let lineStr = session.getLine(pos.row).substring(0, endPos); + this.isAutocomplete = false; + console.log("Executor.vue getCompletions,sourceId:" + JSON.stringify(this.source) + ', lineStr:' + lineStr, pos); + if (!!this.source.tableName) { + // 如果指定了表名,则只提示字段,其他都不用管,用在表数据查看页面 + callbackArr = await this.getAssignTableColumns(this.source.dbName, this.source.tableName); + callback(null, callbackArr); + } else if (lineStr.endsWith("from ") || lineStr.endsWith("join ") || lineStr.endsWith("into ") + || lineStr.endsWith("update ") || lineStr.endsWith("table ")) { + // 获取库和表 + callbackArr = this.getDatabasesAndTables(); + this.lastCallbackArr = callbackArr; + callback(null, callbackArr); + } else if (lineStr.endsWith(".")) { + // 获取表和字段 + callbackArr = await this.getTablesAndColumns(lineStr); + this.lastCallbackArr = callbackArr; + callback(null, callbackArr); + } else if (lineStr.endsWith("select ") || lineStr.endsWith("where ") || lineStr.endsWith("and ") + || lineStr.endsWith("or ") || lineStr.endsWith("set ")) { + // 获取字段 + callbackArr = await this.getTableColumns(session, pos); + this.lastCallbackArr = callbackArr; + callback(null, callbackArr); + } else { + callback(null, this.lastCallbackArr); + } + }, + getDatabasesAndTables() { + let callbackArr = []; + // 所有表 + let tableList = this.tableInfo[this.source.sourceId + '_' + this.source.dbName] || []; + tableList.forEach(item => callbackArr.push({ + caption: (!!item.tableComment) ? item.tableName + '-' + item.tableComment : item.tableName, + snippet: item.tableName, + meta: "表", + type: "snippet", + score: 1000 + })); + // 所有库 + let databaseList = this.databaseInfo[this.source.sourceId] || []; + databaseList.forEach(item => callbackArr.push({ + caption: item.dbName, + snippet: item.dbName, + meta: "库", + type: "snippet", + score: 1000 + })); + return callbackArr; + }, + async getTablesAndColumns(lineStr) { + let isFound = false; + let callbackArr = []; + // 匹配 库名. 搜索表名 + let databaseList = this.databaseInfo[this.source.sourceId] || []; + for (let i = 0; i < databaseList.length; i++) { + let item = databaseList[i]; + if (lineStr.endsWith(item.dbName + ".")) { + let tableList = this.tableInfo[this.source.sourceId + '_' + item.dbName] || []; + if (tableList.length <= 0) { + let res = await datasourceApi.tableList({sourceId: this.source.sourceId, dbName: item.dbName}); + tableList = res.data || []; + this.tableInfo[this.source.sourceId + '_' + item.dbName] = tableList; + } + tableList.forEach(item => callbackArr.push({ + caption: (!!item.tableComment) ? item.tableName + '-' + item.tableComment : item.tableName, + snippet: item.tableName, + meta: "表", + type: "snippet", + score: 1000 + })); + isFound = true; + } + } + // 未找到,匹配 表名. 搜索字段名 + if (!isFound) { + let tableList = this.tableInfo[this.source.sourceId + '_' + this.source.dbName] || []; + for (let i = 0; i < tableList.length; i++) { + let tableName = tableList[i].tableName; + if (lineStr.endsWith(tableName + ".")) { + callbackArr = await this.getAssignTableColumns(this.source.dbName, tableName); + } + } + } + return callbackArr; + }, + async getTableColumns(session, pos) { + let queryText = ""; + // 往前加 + for (let i = pos.row; i >= 0; i--) { + let tempLine = session.getLine(i); + queryText = tempLine + " " + queryText; + if (tempLine.indexOf(";") >= 0) { + break; + } + } + // 往后加 + for (let i = pos.row + 1; i < session.getLength(); i++) { + let tempLine = session.getLine(i); + queryText = queryText + " " + tempLine; + if (tempLine.indexOf(";") >= 0) { + break; + } + } + // 所有表,找下面的字段列表 + let callbackArr = []; + let tableList = this.tableInfo[this.source.sourceId + '_' + this.source.dbName] || []; + for (let i = 0; i < tableList.length; i++) { + let tableName = tableList[i].tableName; + if (queryText.indexOf(tableName) >= 0) { + let tempArr = await this.getAssignTableColumns(this.source.dbName, tableName); + callbackArr = callbackArr.concat(tempArr); + } + } + return callbackArr; + }, + + /** + * 获取指定数据表的字段 + * @param dbName + * @param tableName + */ + async getAssignTableColumns(dbName, tableName) { + let columnKey = this.source.sourceId + '_' + dbName + '_' + tableName; + let columnList = this.columnInfo[columnKey] || []; + if (columnList.length <= 0) { + let res = await datasourceApi.tableColumnList({ + sourceId: this.source.sourceId, + dbName: dbName, + tableName: tableName + }); + columnList = res.data.columnList || []; + this.columnInfo[columnKey] = columnList; + } + let callbackArr = []; + columnList.forEach(item => { + let caption = (!!item.description) ? item.name + "-" + item.description : item.name; + callbackArr.push({ + caption: caption, + snippet: item.name, + meta: "字段", + type: "snippet", + score: 1000 + }); + }); + return callbackArr; + } +} diff --git a/zyplayer-doc-ui/swagger-ui/src/assets/ace-editor/index.css b/zyplayer-doc-ui/swagger-ui/src/assets/ace-editor/index.css new file mode 100644 index 00000000..1f5e870d --- /dev/null +++ b/zyplayer-doc-ui/swagger-ui/src/assets/ace-editor/index.css @@ -0,0 +1,3 @@ +.ace_editor.ace_autocomplete{ + width: 400px; +} diff --git a/zyplayer-doc-ui/swagger-ui/src/assets/ace-editor/index.js b/zyplayer-doc-ui/swagger-ui/src/assets/ace-editor/index.js new file mode 100644 index 00000000..e5c17a15 --- /dev/null +++ b/zyplayer-doc-ui/swagger-ui/src/assets/ace-editor/index.js @@ -0,0 +1,108 @@ +import ace from 'brace'; +import 'brace/ext/language_tools'; +import 'brace/mode/sql'; +import 'brace/snippets/sql'; +import 'brace/mode/json'; +import 'brace/snippets/json'; +import 'brace/mode/xml'; +import 'brace/snippets/xml'; +import 'brace/mode/html'; +import 'brace/snippets/html'; +import 'brace/mode/text'; +import 'brace/snippets/text'; +import 'brace/theme/monokai'; +import 'brace/theme/chrome'; +import './index.css'; +import { h, reactive } from 'vue' + +export default { + render() { + let height = this.height ? this.px(this.height) : '100%'; + let width = this.width ? this.px(this.width) : '100%'; + return h('div', { + attrs: { + style: "height: " + height + '; width: ' + width, + } + }); + }, + props: { + value: String, + lang: String, + theme: String, + height: String, + width: String, + options: Object + }, + data() { + return { + editor: null, + contentBackup: "" + } + }, + watch: { + value: function (val) { + if (this.contentBackup !== val) { + this.editor.session.setValue(val, 1); + this.contentBackup = val; + } + }, + theme: function (newTheme) { + this.editor.setTheme('ace/theme/' + newTheme); + }, + lang: function (newLang) { + this.editor.getSession().setMode(typeof newLang === 'string' ? ('ace/mode/' + newLang) : newLang); + }, + options: function (newOption) { + this.editor.setOptions(newOption); + }, + height: function () { + this.$nextTick(function () { + this.editor.resize() + }) + }, + width: function () { + this.$nextTick(function () { + this.editor.resize() + }) + }, + }, + beforeDestroy() { + this.editor.destroy(); + this.editor.container.remove(); + }, + mounted() { + let vm = this; + let lang = this.lang || 'text'; + let theme = this.theme || 'chrome'; + let editor = vm.editor = ace.edit(this.$el); + editor.$blockScrolling = Infinity; + this.$emit('init', editor); + //editor.setOption("enableEmmet", true); + editor.getSession().setMode(typeof lang === 'string' ? ('ace/mode/' + lang) : lang); + editor.setTheme('ace/theme/' + theme); + if (this.value) { + editor.setValue(this.value, 1); + } + this.contentBackup = this.value; + editor.on('change', function () { + let content = editor.getValue(); + vm.$emit('update:value', content); + vm.contentBackup = content; + // 内容改变就执行输入提示功能,和自动的冲突了,感觉自动的就符合了,但是按空格他不出现提示框 + // console.log('change content:' + content); + // editor.execCommand("startAutocomplete"); + }); + if (vm.options) { + editor.setOptions(vm.options); + } + }, + methods: { + px: function (n) { + if (/^\d*$/.test(n)) { + return n + "px"; + } + return n; + }, + }, +} +; diff --git a/zyplayer-doc-ui/swagger-ui/src/components/params/ParamBody.vue b/zyplayer-doc-ui/swagger-ui/src/components/params/ParamBody.vue index 6f8ae9bf..a1ea3c28 100644 --- a/zyplayer-doc-ui/swagger-ui/src/components/params/ParamBody.vue +++ b/zyplayer-doc-ui/swagger-ui/src/components/params/ParamBody.vue @@ -1,5 +1,6 @@ +