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:
fit2-zhao 2023-09-07 14:41:26 +08:00 committed by fit2-zhao
parent bab97d7794
commit 28d5780ee6
10 changed files with 265 additions and 31 deletions

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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"
]
}
}

View File

@ -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 = '';

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,4 @@
import ApiDocumentTemplate from '@/template/document/ApiDocumentTemplate';
import documentUse from '@/template/document/document-use';
documentUse('#apiDocument', ApiDocumentTemplate);

View File

@ -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;

View File

@ -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, [
{