refactor(接口测试): 文档支持导出html
--story=1012989 --user=赵勇 接口文档增加下载功能 https://www.tapd.cn/55049933/s/1413729 Signed-off-by: fit2-zhao <yong.zhao@fit2cloud.com>
This commit is contained in:
parent
bab97d7794
commit
28d5780ee6
|
@ -10,15 +10,11 @@ import io.metersphere.dto.RequestResult;
|
|||
import io.metersphere.dto.ShareInfoDTO;
|
||||
import io.metersphere.service.ShareInfoService;
|
||||
import io.metersphere.service.scenario.ApiScenarioReportService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
@ -39,6 +35,11 @@ public class ShareController {
|
|||
return shareInfoService.selectApiInfoByParam(apiDocumentRequest, goPage, pageSize);
|
||||
}
|
||||
|
||||
@PostMapping("/doc/export/{goPage}/{pageSize}/{lang}")
|
||||
public void exportPageHtml(@RequestBody ApiDocumentRequest apiDocumentRequest, @PathVariable int goPage, @PathVariable int pageSize, @PathVariable String lang, HttpServletResponse response) {
|
||||
shareInfoService.exportPageDoc(apiDocumentRequest, goPage, pageSize, response);
|
||||
}
|
||||
|
||||
@GetMapping("/get/{id}")
|
||||
public ShareInfo get(@PathVariable String id) {
|
||||
return shareInfoService.get(id);
|
||||
|
|
|
@ -23,6 +23,8 @@ import io.metersphere.i18n.Translator;
|
|||
import io.metersphere.service.definition.ApiModuleService;
|
||||
import io.metersphere.service.scenario.ApiScenarioReportService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.ServletOutputStream;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.collections.MapUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
|
@ -32,6 +34,11 @@ import org.springframework.stereotype.Service;
|
|||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -132,8 +139,10 @@ public class ShareInfoService extends BaseShareInfoService {
|
|||
}
|
||||
|
||||
private void iniApiDocumentRequest(ApiDocumentRequest request) {
|
||||
List<String> shareIdList = this.selectShareIdByShareInfoId(request.getShareId());
|
||||
request.setApiIdList(shareIdList);
|
||||
if (StringUtils.isNotBlank(request.getShareId())) {
|
||||
List<String> shareIdList = this.selectShareIdByShareInfoId(request.getShareId());
|
||||
request.setApiIdList(shareIdList);
|
||||
}
|
||||
}
|
||||
|
||||
public List<ApiDefinitionWithBLOBs> selectByRequest(ApiDocumentRequest request) {
|
||||
|
@ -641,4 +650,48 @@ public class ShareInfoService extends BaseShareInfoService {
|
|||
MSException.throwException("ShareInfo not exist!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void render(Pager<List<ApiDocumentInfoDTO>> listPager, HttpServletResponse response) throws
|
||||
UnsupportedEncodingException {
|
||||
response.reset();
|
||||
response.setContentType("application/octet-stream");
|
||||
response.addHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode("test", StandardCharsets.UTF_8));
|
||||
|
||||
try (InputStreamReader isr = new InputStreamReader(Objects.requireNonNull(getClass().getResourceAsStream("/public/api-doc.html")), StandardCharsets.UTF_8);
|
||||
ServletOutputStream outputStream = response.getOutputStream()) {
|
||||
BufferedReader bufferedReader = new BufferedReader(isr);
|
||||
String line;
|
||||
while (null != (line = bufferedReader.readLine())) {
|
||||
if (line.contains("\"#export-doc\"")) {
|
||||
String reportInfo = JSON.toJSONString(listPager);
|
||||
line = line.replace("\"#export-doc\"", reportInfo);
|
||||
}
|
||||
line += StringUtils.LF;
|
||||
byte[] lineBytes = line.getBytes(StandardCharsets.UTF_8);
|
||||
int start = 0;
|
||||
while (start < lineBytes.length) {
|
||||
if (start + 1024 < lineBytes.length) {
|
||||
outputStream.write(lineBytes, start, 1024);
|
||||
} else {
|
||||
outputStream.write(lineBytes, start, lineBytes.length - start);
|
||||
}
|
||||
outputStream.flush();
|
||||
start += 1024;
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LogUtil.error(e);
|
||||
MSException.throwException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void exportPageDoc(ApiDocumentRequest apiDocumentRequest, int goPage, int pageSize, HttpServletResponse response) {
|
||||
Pager<List<ApiDocumentInfoDTO>> listPager = this.selectApiInfoByParam(apiDocumentRequest, goPage, pageSize);
|
||||
try {
|
||||
this.render(listPager, response);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
"report": "NODE_ENV=analyze vue-cli-service build"
|
||||
},
|
||||
"dependencies": {
|
||||
"html-webpack-inline-source-plugin": "1.0.0-beta.2",
|
||||
"inline-source-webpack-plugin": "^3.0.1",
|
||||
"@ckeditor/ckeditor5-build-classic": "^18.0.0",
|
||||
"@ckeditor/ckeditor5-vue": "^1.0.1",
|
||||
"@fit2cloud-ui/vue-virtual-tree": "^1.0.0",
|
||||
|
@ -122,4 +124,4 @@
|
|||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,14 +80,15 @@
|
|||
<el-row
|
||||
v-else
|
||||
style="
|
||||
margin-top: 0px;
|
||||
margin-top: 5px;
|
||||
position: fixed;
|
||||
float: right;
|
||||
margin-right: 0px;
|
||||
margin-left: 400px;
|
||||
top: 90px;
|
||||
right: 40px;
|
||||
">
|
||||
"
|
||||
v-show="!isTemplate">
|
||||
<el-select
|
||||
size="small"
|
||||
:placeholder="$t('api_test.definition.document.order')"
|
||||
|
@ -159,6 +160,13 @@
|
|||
:project-id="projectId"
|
||||
:share-url="batchShareUrl"
|
||||
style="float: right; margin: 6px; font-size: 17px" />
|
||||
|
||||
<el-tooltip :content="$t('commons.export')" placement="top" v-xpack>
|
||||
<i
|
||||
class="el-icon-download"
|
||||
@click="handleExportHtml()"
|
||||
style="margin-top: 5px; font-size: 20px; cursor: pointer" />
|
||||
</el-tooltip>
|
||||
</el-row>
|
||||
<el-divider></el-divider>
|
||||
<!-- 展示区域 -->
|
||||
|
@ -168,6 +176,7 @@
|
|||
:key="apiInfo.id"
|
||||
:api-info="apiInfo"
|
||||
:project-id="projectId"
|
||||
@handleExportHtml="handleExportHtml"
|
||||
ref="apiDocInfoDivItem" />
|
||||
</div>
|
||||
</el-main>
|
||||
|
@ -198,7 +207,8 @@
|
|||
:page-sizes="[10, 20, 50]"
|
||||
:page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total">
|
||||
:total="total"
|
||||
v-show="!isTemplate">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -210,6 +220,8 @@ import ApiStatus from '@/business/definition/components/list/ApiStatus';
|
|||
import MsJsonCodeEdit from '@/business/commons/json-schema/JsonSchemaEditor';
|
||||
import { generateApiDocumentShareInfo, documentShareUrl, selectApiInfoByParam } from '@/api/share';
|
||||
import ApiInformation from '@/business/definition/components/document/components/ApiInformation';
|
||||
import { getCurrentUser } from 'metersphere-frontend/src/utils/token';
|
||||
import { request } from 'metersphere-frontend/src/plugins/request';
|
||||
|
||||
export default {
|
||||
name: 'ApiDocumentAnchor',
|
||||
|
@ -271,6 +283,10 @@ export default {
|
|||
moduleIds: Array,
|
||||
sharePage: Boolean,
|
||||
pageHeaderHeight: Number,
|
||||
isTemplate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
trashEnable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
@ -322,6 +338,70 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
fileDownload(url, param) {
|
||||
let config = {
|
||||
url: url,
|
||||
method: 'post',
|
||||
data: param,
|
||||
responseType: 'blob',
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
};
|
||||
request(config).then(
|
||||
(response) => {
|
||||
let link = document.createElement('a');
|
||||
link.href = window.URL.createObjectURL(
|
||||
new Blob([response.data], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8',
|
||||
})
|
||||
);
|
||||
link.download = 'api-doc.html';
|
||||
this.result = false;
|
||||
link.click();
|
||||
},
|
||||
(error) => {
|
||||
this.result = false;
|
||||
if (error.response && error.response.status === 509) {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
let content = reader.result;
|
||||
$error(content);
|
||||
};
|
||||
reader.readAsText(error.response.data);
|
||||
} else {
|
||||
$error('导出doc文件失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
handleExportHtml(id) {
|
||||
let url = '/share/doc/export/' + this.currentPage + '/' + this.pageSize;
|
||||
let lang = 'zh_CN';
|
||||
let user = getCurrentUser();
|
||||
if (user && user.language) {
|
||||
lang = user.language;
|
||||
}
|
||||
url = url + '/' + lang;
|
||||
this.loading = true;
|
||||
let simpleRequest = this.apiSearch;
|
||||
if (this.projectId != null && this.projectId !== '') {
|
||||
simpleRequest.projectId = this.projectId;
|
||||
}
|
||||
if (this.documentId != null && this.documentId !== '') {
|
||||
simpleRequest.shareId = this.documentId;
|
||||
}
|
||||
if (this.moduleIds.length > 0) {
|
||||
simpleRequest.moduleIds = this.moduleIds;
|
||||
} else {
|
||||
simpleRequest.moduleIds = [];
|
||||
}
|
||||
simpleRequest.apiIdList = [];
|
||||
if (id) {
|
||||
simpleRequest.apiIdList = [id];
|
||||
}
|
||||
simpleRequest.versionId = this.versionId;
|
||||
simpleRequest.trashEnable = this.trashEnable;
|
||||
this.fileDownload(url, simpleRequest);
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.initApiDocSimpleList();
|
||||
|
@ -333,34 +413,40 @@ export default {
|
|||
changeFixed(clientHeight) {
|
||||
if (this.$refs.apiDocInfoDiv) {
|
||||
let countPageHeight = 210;
|
||||
if (this.pageHeaderHeight != 0 && this.pageHeaderHeight != null) {
|
||||
if (this.pageHeaderHeight && this.pageHeaderHeight !== 0) {
|
||||
countPageHeight = this.pageHeaderHeight;
|
||||
}
|
||||
this.$refs.apiDocInfoDiv.style.height = clientHeight - countPageHeight + 'px';
|
||||
if (this.isTemplate) {
|
||||
this.$refs.apiDocInfoDiv.style.height = clientHeight - 50 + 'px';
|
||||
this.$refs.apiDocList.style.height = clientHeight - 50 + 'px';
|
||||
} else {
|
||||
this.$refs.apiDocInfoDiv.style.height = clientHeight - countPageHeight + 'px';
|
||||
this.$refs.apiDocList.style.height = clientHeight - countPageHeight + 'px';
|
||||
}
|
||||
this.$refs.apiDocInfoDiv.style.overflow = 'auto';
|
||||
this.$refs.apiDocList.style.height = clientHeight - countPageHeight + 'px';
|
||||
}
|
||||
},
|
||||
initApiDocSimpleList() {
|
||||
this.apiInfoArray = [];
|
||||
let simpleRequest = this.apiSearch;
|
||||
if (this.projectId != null && this.projectId != '') {
|
||||
if (this.projectId !== null && this.projectId !== '') {
|
||||
simpleRequest.projectId = this.projectId;
|
||||
}
|
||||
if (this.documentId != null && this.documentId != '') {
|
||||
if (this.documentId !== null && this.documentId !== '') {
|
||||
simpleRequest.shareId = this.documentId;
|
||||
}
|
||||
if (this.moduleIds.length > 0) {
|
||||
if (this.moduleIds && this.moduleIds.length > 0) {
|
||||
simpleRequest.moduleIds = this.moduleIds;
|
||||
} else {
|
||||
simpleRequest.moduleIds = [];
|
||||
}
|
||||
simpleRequest.versionId = this.versionId;
|
||||
simpleRequest.trashEnable = this.trashEnable;
|
||||
selectApiInfoByParam(simpleRequest, this.currentPage, this.pageSize).then((response) => {
|
||||
this.apiInfoArray = response.data.listObject;
|
||||
this.total = response.data.itemCount;
|
||||
if (response.data.length > this.maxComponentSize) {
|
||||
if (this.isTemplate) {
|
||||
let response = '#export-doc';
|
||||
this.apiInfoArray = response.listObject;
|
||||
this.total = response.itemCount;
|
||||
if (response.length > this.maxComponentSize) {
|
||||
this.needAsyncSelect = true;
|
||||
} else {
|
||||
this.needAsyncSelect = false;
|
||||
|
@ -369,7 +455,21 @@ export default {
|
|||
this.$nextTick(() => {
|
||||
this.handleScroll();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
selectApiInfoByParam(simpleRequest, this.currentPage, this.pageSize).then((response) => {
|
||||
this.apiInfoArray = response.data.listObject;
|
||||
this.total = response.data.itemCount;
|
||||
if (response.data.length > this.maxComponentSize) {
|
||||
this.needAsyncSelect = true;
|
||||
} else {
|
||||
this.needAsyncSelect = false;
|
||||
}
|
||||
//每次查询完成之后定位右侧的步骤
|
||||
this.$nextTick(() => {
|
||||
this.handleScroll();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
shareApiDocument() {
|
||||
this.shareUrl = '';
|
||||
|
|
|
@ -8,6 +8,13 @@
|
|||
</div>
|
||||
<i class="el-icon-share" slot="reference" style="margin-right: 10px; cursor: pointer"></i>
|
||||
</el-popover>
|
||||
<el-tooltip :content="$t('commons.export')" placement="top">
|
||||
<i
|
||||
class="el-icon-download"
|
||||
@click="handleExportHtml()"
|
||||
style="margin-right: 5px; cursor: pointer; font-size: 20px" />
|
||||
</el-tooltip>
|
||||
|
||||
{{ apiInfo.name }}
|
||||
<span class="apiStatusTag">
|
||||
<api-status :value="apiInfo.status" />
|
||||
|
@ -171,6 +178,9 @@ export default {
|
|||
computed: {},
|
||||
watch: {},
|
||||
methods: {
|
||||
handleExportHtml() {
|
||||
this.$emit('handleExportHtml', this.apiInfo.id);
|
||||
},
|
||||
isArrayHasData(arrayData) {
|
||||
if (!arrayData) {
|
||||
return false;
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-main>
|
||||
<api-document-anchor :is-template="true" />
|
||||
</el-main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ApiDocumentAnchor from '@/business/definition/components/document/ApiDocumentAnchor';
|
||||
export default {
|
||||
name: 'DocumentAnchorTemplate',
|
||||
components: { ApiDocumentAnchor },
|
||||
data() {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link rel="shortcut icon" href="<%= BASE_URL %>favicon.ico" />
|
||||
<title>api doc</title>
|
||||
<link inline rel="stylesheet" href="/prd/element-ui/element-font.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="apiDocument"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,4 @@
|
|||
import ApiDocumentTemplate from '@/template/document/ApiDocumentTemplate';
|
||||
import documentUse from '@/template/document/document-use';
|
||||
|
||||
documentUse('#apiDocument', ApiDocumentTemplate);
|
|
@ -32,11 +32,14 @@ function documentUse(id, template) {
|
|||
Vue.use(icons);
|
||||
Vue.use(plugins);
|
||||
|
||||
new Vue({
|
||||
el: id,
|
||||
i18n,
|
||||
render: (h) => h(template),
|
||||
});
|
||||
setTimeout(() => {
|
||||
new Vue({
|
||||
el: id,
|
||||
i18n,
|
||||
render: (h) => h(template),
|
||||
});
|
||||
// 不延迟页面渲染不出来
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
export default documentUse;
|
||||
|
|
|
@ -2,6 +2,9 @@ const path = require('path');
|
|||
const { name } = require('./package');
|
||||
const { defineConfig } = require('@vue/cli-service');
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const InlineSourceWebpackPlugin = require('inline-source-webpack-plugin');
|
||||
|
||||
function resolve(dir) {
|
||||
return path.join(__dirname, dir);
|
||||
|
@ -15,7 +18,7 @@ module.exports = defineConfig({
|
|||
client: {
|
||||
webSocketTransport: 'sockjs',
|
||||
overlay: false,
|
||||
},
|
||||
},
|
||||
allowedHosts: 'all',
|
||||
webSocketServer: 'sockjs',
|
||||
proxy: {
|
||||
|
@ -39,6 +42,7 @@ module.exports = defineConfig({
|
|||
template: 'public/index.html',
|
||||
filename: 'index.html',
|
||||
},
|
||||
|
||||
shareApiReport: {
|
||||
entry: 'src/template/report/share/share-api-report.js',
|
||||
template: 'src/template/report/share/share-api-report.html',
|
||||
|
@ -49,6 +53,12 @@ module.exports = defineConfig({
|
|||
template: 'src/template/document/share/share-document.html',
|
||||
filename: 'share-document.html',
|
||||
},
|
||||
apiDocument: {
|
||||
entry: 'src/template/document/api-document.js',
|
||||
template: 'src/template/document/api-document.html',
|
||||
filename: 'api-doc.html',
|
||||
inlineSource: '.*',
|
||||
},
|
||||
},
|
||||
configureWebpack: {
|
||||
devtool: 'cheap-module-source-map',
|
||||
|
@ -173,6 +183,24 @@ module.exports = defineConfig({
|
|||
.options({
|
||||
symbolId: 'icon-[name]',
|
||||
});
|
||||
|
||||
// 报告模板打包成一个html
|
||||
config
|
||||
.plugin('inline-source-html')
|
||||
.after('html-apiDocument')
|
||||
.use(
|
||||
new InlineSourceWebpackPlugin({
|
||||
compress: true,
|
||||
rootpath: '../../framework/sdk-parent/frontend/public/js',
|
||||
noAssetMatch: 'warn',
|
||||
}),
|
||||
[HtmlWebpackPlugin]
|
||||
);
|
||||
|
||||
config
|
||||
.plugin('inline-source-html-apiDocument')
|
||||
.after('html-apiDocument')
|
||||
.use(HtmlWebpackInlineSourcePlugin, [HtmlWebpackPlugin]);
|
||||
if (process.env.NODE_ENV === 'analyze') {
|
||||
config.plugin('webpack-report').use(BundleAnalyzerPlugin, [
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue