diff --git a/api-test/backend/src/main/java/io/metersphere/controller/ShareController.java b/api-test/backend/src/main/java/io/metersphere/controller/ShareController.java index 9a460f3df2..d3fad603ae 100644 --- a/api-test/backend/src/main/java/io/metersphere/controller/ShareController.java +++ b/api-test/backend/src/main/java/io/metersphere/controller/ShareController.java @@ -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); diff --git a/api-test/backend/src/main/java/io/metersphere/service/ShareInfoService.java b/api-test/backend/src/main/java/io/metersphere/service/ShareInfoService.java index 82158ee00d..f9c04b1161 100644 --- a/api-test/backend/src/main/java/io/metersphere/service/ShareInfoService.java +++ b/api-test/backend/src/main/java/io/metersphere/service/ShareInfoService.java @@ -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 shareIdList = this.selectShareIdByShareInfoId(request.getShareId()); - request.setApiIdList(shareIdList); + if (StringUtils.isNotBlank(request.getShareId())) { + List shareIdList = this.selectShareIdByShareInfoId(request.getShareId()); + request.setApiIdList(shareIdList); + } } public List selectByRequest(ApiDocumentRequest request) { @@ -641,4 +650,48 @@ public class ShareInfoService extends BaseShareInfoService { MSException.throwException("ShareInfo not exist!"); } } + + + public void render(Pager> 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> listPager = this.selectApiInfoByParam(apiDocumentRequest, goPage, pageSize); + try { + this.render(listPager, response); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } } diff --git a/api-test/frontend/package.json b/api-test/frontend/package.json index 1c714393ff..813259ac89 100644 --- a/api-test/frontend/package.json +++ b/api-test/frontend/package.json @@ -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" ] -} \ No newline at end of file +} diff --git a/api-test/frontend/src/business/definition/components/document/ApiDocumentAnchor.vue b/api-test/frontend/src/business/definition/components/document/ApiDocumentAnchor.vue index b7c668567b..088d9624c2 100644 --- a/api-test/frontend/src/business/definition/components/document/ApiDocumentAnchor.vue +++ b/api-test/frontend/src/business/definition/components/document/ApiDocumentAnchor.vue @@ -80,14 +80,15 @@ + " + v-show="!isTemplate"> + + + + @@ -168,6 +176,7 @@ :key="apiInfo.id" :api-info="apiInfo" :project-id="projectId" + @handleExportHtml="handleExportHtml" ref="apiDocInfoDivItem" /> @@ -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"> @@ -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 = ''; diff --git a/api-test/frontend/src/business/definition/components/document/components/ApiInformation.vue b/api-test/frontend/src/business/definition/components/document/components/ApiInformation.vue index 6c3426ea42..dc28e04858 100644 --- a/api-test/frontend/src/business/definition/components/document/components/ApiInformation.vue +++ b/api-test/frontend/src/business/definition/components/document/components/ApiInformation.vue @@ -8,6 +8,13 @@ + + + + {{ apiInfo.name }} @@ -171,6 +178,9 @@ export default { computed: {}, watch: {}, methods: { + handleExportHtml() { + this.$emit('handleExportHtml', this.apiInfo.id); + }, isArrayHasData(arrayData) { if (!arrayData) { return false; diff --git a/api-test/frontend/src/template/document/ApiDocumentTemplate.vue b/api-test/frontend/src/template/document/ApiDocumentTemplate.vue new file mode 100644 index 0000000000..1761cf09a1 --- /dev/null +++ b/api-test/frontend/src/template/document/ApiDocumentTemplate.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/api-test/frontend/src/template/document/api-document.html b/api-test/frontend/src/template/document/api-document.html new file mode 100644 index 0000000000..806faa39bb --- /dev/null +++ b/api-test/frontend/src/template/document/api-document.html @@ -0,0 +1,15 @@ + + + + + + + + api doc + + + + +
+ + diff --git a/api-test/frontend/src/template/document/api-document.js b/api-test/frontend/src/template/document/api-document.js new file mode 100644 index 0000000000..79b1cc5102 --- /dev/null +++ b/api-test/frontend/src/template/document/api-document.js @@ -0,0 +1,4 @@ +import ApiDocumentTemplate from '@/template/document/ApiDocumentTemplate'; +import documentUse from '@/template/document/document-use'; + +documentUse('#apiDocument', ApiDocumentTemplate); diff --git a/api-test/frontend/src/template/document/document-use.js b/api-test/frontend/src/template/document/document-use.js index 64af5829a8..5a5b9fe5ff 100644 --- a/api-test/frontend/src/template/document/document-use.js +++ b/api-test/frontend/src/template/document/document-use.js @@ -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; diff --git a/api-test/frontend/vue.config.js b/api-test/frontend/vue.config.js index 61fd300a9d..eb4f2ee856 100644 --- a/api-test/frontend/vue.config.js +++ b/api-test/frontend/vue.config.js @@ -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, [ {