实现swagger的代理请求和数据的格式化展示开发

This commit is contained in:
暮光:城中城
2021-11-06 22:55:10 +08:00
parent 339a29e739
commit 2f1770dcbc
12 changed files with 546 additions and 41 deletions

View File

@@ -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 getCompletionssourceId" + 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;
}
}

View File

@@ -0,0 +1,3 @@
.ace_editor.ace_autocomplete{
width: 400px;
}

View File

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

View File

@@ -1,5 +1,6 @@
<template>
<a-textarea placeholder="" v-model:value="bodyRowParam" :auto-size="{ minRows: 15, maxRows: 15 }"></a-textarea>
<!-- <a-textarea placeholder="" v-model:value="bodyRowParam" :auto-size="{ minRows: 15, maxRows: 15 }"></a-textarea>-->
<ace-editor v-model:value="bodyRowParam" @init="rowParamInit" lang="json" theme="monokai" width="100%" height="100" :options="rowParamConfig"></ace-editor>
</template>
<script>
@@ -11,6 +12,7 @@
import {CloseOutlined, UploadOutlined} from '@ant-design/icons-vue';
import 'mavon-editor/dist/markdown/github-markdown.min.css'
import 'mavon-editor/dist/css/index.css'
import aceEditor from "../../assets/ace-editor";
export default {
props: {
@@ -20,7 +22,7 @@
},
},
components: {
CloseOutlined, UploadOutlined
CloseOutlined, UploadOutlined, aceEditor
},
emits: [],
setup(props, { attrs, slots, emit, expose}) {
@@ -50,9 +52,26 @@
const getParam = () => {
return bodyRowParam.value;
}
// 编辑器
let rowParamEditor = ref();
const rowParamInit = editor => {
rowParamEditor.value = editor;
rowParamEditor.value.setFontSize(16);
}
return {
getParam,
// 编辑器
rowParamInit,
bodyRowParam,
rowParamConfig: {
wrap: true,
autoScrollEditorIntoView: true,
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true,
minLines: 18,
maxLines: 18,
},
};
},
};

View File

@@ -5,34 +5,46 @@
<a-button type="primary" :loading="requestLoading">发送请求</a-button>
</template>
</a-input-search>
<a-tabs v-model:activeKey="activePage" closable @tab-click="" style="padding: 5px 10px 0;">
<a-tabs v-model:activeKey="activePage" closable @tab-click="activePageChange" style="padding: 5px 10px 0;">
<a-tab-pane tab="URL参数" key="urlParam" forceRender>
<ParamTable ref="urlParamRef" :paramList="urlParamList"></ParamTable>
<div v-show="queryParamVisible">
<ParamTable ref="urlParamRef" :paramList="urlParamList"></ParamTable>
</div>
</a-tab-pane>
<a-tab-pane tab="请求参数" key="bodyParam" v-if="docInfoShow.method !== 'get'" forceRender>
<a-radio-group v-model:value="bodyParamType" style="margin-bottom: 5px;">
<a-radio value="none">none</a-radio>
<a-radio value="form">form-data</a-radio>
<a-radio value="formUrlEncode">x-www-form-urlencoded</a-radio>
<a-radio value="row">row</a-radio>
<a-radio value="binary">binary</a-radio>
</a-radio-group>
<div v-show="bodyParamType === 'form'">
<ParamTable ref="formParamRef" :paramList="formParamList" showType></ParamTable>
</div>
<div v-show="bodyParamType === 'formUrlEncode'">
<ParamTable ref="formEncodeParamRef" :paramList="formEncodeParamList"></ParamTable>
</div>
<div v-show="bodyParamType === 'row'">
<ParamBody ref="bodyParamRef" :paramList="bodyRowParamList"></ParamBody>
<div v-show="queryParamVisible">
<a-radio-group v-model:value="bodyParamType" style="margin-bottom: 5px;">
<a-radio value="none">none</a-radio>
<a-radio value="form">form-data</a-radio>
<a-radio value="formUrlEncode">x-www-form-urlencoded</a-radio>
<a-radio value="row">row</a-radio>
<a-radio value="binary">binary</a-radio>
</a-radio-group>
<div v-show="bodyParamType === 'form'">
<ParamTable ref="formParamRef" :paramList="formParamList" showType></ParamTable>
</div>
<div v-show="bodyParamType === 'formUrlEncode'">
<ParamTable ref="formEncodeParamRef" :paramList="formEncodeParamList"></ParamTable>
</div>
<div v-show="bodyParamType === 'row'">
<ParamBody ref="bodyParamRef" :paramList="bodyRowParamList"></ParamBody>
</div>
</div>
</a-tab-pane>
<a-tab-pane tab="Header参数" key="headerParam" forceRender>
<ParamTable ref="headerParamRef" :paramList="headerParamList"></ParamTable>
<div v-show="queryParamVisible">
<ParamTable ref="headerParamRef" :paramList="headerParamList"></ParamTable>
</div>
</a-tab-pane>
<a-tab-pane tab="Cookie参数" key="cookieParam" forceRender>
<ParamTable ref="cookieParamRef" :paramList="cookieParamList"></ParamTable>
<div v-show="queryParamVisible">
<ParamTable ref="cookieParamRef" :paramList="cookieParamList"></ParamTable>
</div>
</a-tab-pane>
<template #rightExtra>
<a-button v-if="queryParamVisible" @click="hideQueryParam" type="link">收起参数</a-button>
<a-button v-else @click="showQueryParam" type="link">展开参数</a-button>
</template>
</a-tabs>
<DocDebuggerResult :result="requestResult"></DocDebuggerResult>
</div>
@@ -47,7 +59,7 @@
import DocDebuggerResult from './DocDebuggerResult.vue'
import ParamTable from '../../../components/params/ParamTable.vue'
import ParamBody from '../../../components/params/ParamBody.vue'
import {CloseOutlined} from '@ant-design/icons-vue';
import {CloseOutlined, VerticalAlignTopOutlined, VerticalAlignBottomOutlined} from '@ant-design/icons-vue';
import 'mavon-editor/dist/markdown/github-markdown.min.css'
import 'mavon-editor/dist/css/index.css'
import {zyplayerApi} from "../../../api";
@@ -68,7 +80,7 @@
},
},
components: {
CloseOutlined, ParamTable, ParamBody, DocDebuggerResult,
VerticalAlignTopOutlined, VerticalAlignBottomOutlined, CloseOutlined, ParamTable, ParamBody, DocDebuggerResult,
},
setup(props) {
const store = useStore();
@@ -173,6 +185,7 @@
// });
let url = urlParamStr ? (docUrl.value + '?' + urlParamStr) : docUrl.value;
formData.append('url', url);
formData.append('host', urlDomain);
formData.append('method', props.docInfoShow.method);
formData.append('contentType', props.docInfoShow.consumes);
formData.append('headerParam', JSON.stringify(headerParamArr));
@@ -181,6 +194,7 @@
formData.append('formEncodeParam', JSON.stringify(formEncodeParamArr));
formData.append('bodyParam', bodyParamStr);
requestLoading.value = true;
requestResult.value = {};
zyplayerApi.requestUrl(formData).then(res => {
requestResult.value = res;
requestLoading.value = false;
@@ -188,9 +202,20 @@
requestLoading.value = false;
});
};
let queryParamVisible = ref(true);
const hideQueryParam = () => {
queryParamVisible.value = false;
}
const showQueryParam = () => {
queryParamVisible.value = true;
}
const activePageChange = () => {
queryParamVisible.value = true;
}
return {
docUrl,
activePage,
activePageChange,
requestLoading,
sendRequest,
requestResult,
@@ -223,6 +248,10 @@
{title: '类型', dataIndex: 'type', width: 250},
{title: '说明', dataIndex: 'description'},
],
// 界面控制
queryParamVisible,
hideQueryParam,
showQueryParam,
};
},
};

View File

@@ -1,10 +1,28 @@
<template>
<div>
<div style="margin-bottom: 30px;">
<a-tabs v-model:activeKey="activePage" closable @tab-click="" style="padding: 5px 10px 0;">
<a-tab-pane tab="Body" key="body" forceRender>
<template v-if="result.data && result.data.data">{{result.data.data}}</template>
<template v-else-if="result.data">{{result.data}}</template>
<template v-else>{{result}}</template>
<div style="margin-bottom: 10px;">
<a-radio-group v-model:value="bodyShowType" @change="bodyShowTypeChange" size="small">
<a-radio-button value="format">格式化</a-radio-button>
<a-radio-button value="row">原始值</a-radio-button>
<a-radio-button value="preview">预览</a-radio-button>
</a-radio-group>
<a-select v-if="bodyShowType === 'format'" placeholder="格式化" v-model:value="bodyShowFormatType" size="small" style="margin-left: 10px;">
<a-select-option value="json">JSON</a-select-option>
<a-select-option value="html">HTML</a-select-option>
<a-select-option value="xml">XML</a-select-option>
<a-select-option value="text">TEXT</a-select-option>
</a-select>
</div>
<ace-editor v-if="bodyShowType === 'format'" v-model:value="resultDataContent" @init="resultDataInit" :lang="bodyShowFormatType" theme="monokai" width="100%" height="100" :options="resultDataConfig"></ace-editor>
<ace-editor v-else-if="bodyShowType === 'row'" v-model:value="resultDataContent" @init="resultDataInit" lang="text" theme="chrome" width="100%" height="100" :options="resultDataConfig"></ace-editor>
<div v-else-if="bodyShowType === 'preview'">
<template v-if="bodyShowFormatPreview === 'html'">
<iframe ref="previewHtmlRef" width="100%" height="570px" style="border: 0;"></iframe>
</template>
<template v-else>{{resultDataContent}}</template>
</div>
</a-tab-pane>
<a-tab-pane tab="Headers" key="headers" forceRender>
<a-table :dataSource="resultHeaders"
@@ -20,6 +38,9 @@
:scroll="{ y: '300px' }">
</a-table>
</a-tab-pane>
<template #rightExtra>
<span class="status-info-box">状态码<span>{{resultData.status||'200'}}</span>耗时<span>{{resultData.useTime||0}} ms</span>大小<span>{{resultData.bodyLength||0}} B</span></span>
</template>
</a-tabs>
</div>
</template>
@@ -36,6 +57,7 @@
import 'mavon-editor/dist/markdown/github-markdown.min.css'
import 'mavon-editor/dist/css/index.css'
import {zyplayerApi} from "../../../api";
import aceEditor from "../../../assets/ace-editor";
export default {
props: {
@@ -45,17 +67,45 @@
},
},
components: {
CloseOutlined, ParamTable, ParamBody
CloseOutlined, ParamTable, ParamBody, aceEditor
},
setup(props) {
const { result } = toRefs(props);
let activePage = ref('body');
let bodyShowType = ref('format');
let bodyShowFormatType = ref('json');
let bodyShowFormatPreview = ref('');
let resultHeaders = ref([]);
let resultCookies = ref([]);
let resultDataContent = ref('');
let resultData = ref({});
let previewHtmlRef = ref();
const initData = () => {
if (props.result.data) {
resultData.value = props.result.data;
if (props.result.data.data) {
try {
let realData = JSON.parse(props.result.data.data);
resultDataContent.value = JSON.stringify(realData, null, 4);
} catch (e) {
resultDataContent.value = props.result.data.data;
}
} else {
resultDataContent.value = JSON.stringify(props.result.data, null, 4);
}
if (props.result.data.headers) {
resultHeaders.value = props.result.data.headers;
// 依据返回值header判断类型
let contentType = resultHeaders.value.find(item => item.name === 'Content-Type');
if (contentType && contentType.value) {
if (contentType.value.indexOf('text/html') >= 0) {
bodyShowFormatType.value = 'html';
bodyShowFormatPreview.value = 'html';
} else if (contentType.value.indexOf('json') >= 0) {
bodyShowFormatType.value = 'json';
bodyShowFormatPreview.value = 'json';
}
}
}
if (props.result.data.cookies) {
resultCookies.value = props.result.data.cookies;
@@ -64,8 +114,25 @@
};
initData();
watch(result, () => initData());
// 编辑器
const resultDataInit = editor => {
editor.setFontSize(16);
}
const bodyShowTypeChange = () => {
if (bodyShowType.value === 'preview') {
setTimeout(() => {
previewHtmlRef.value.contentDocument.write(resultDataContent.value);
}, 0);
}
}
return {
activePage,
bodyShowType,
bodyShowTypeChange,
bodyShowFormatType,
bodyShowFormatPreview,
previewHtmlRef,
resultData,
resultHeaders,
resultCookies,
resultHeadersColumns: [
@@ -76,7 +143,25 @@
{title: 'KEY', dataIndex: 'name'},
{title: 'VALUE', dataIndex: 'value'},
],
// 编辑器
resultDataInit,
resultDataContent,
resultDataConfig: {
wrap: true,
readOnly: true,
autoScrollEditorIntoView: true,
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true,
minLines: 30,
maxLines: 30,
},
};
},
};
</script>
<style>
.status-info-box{color: #888;}
.status-info-box span{color: #00aa00; margin-right: 15px;}
.status-info-box span:last-child{margin-right: 0;}
</style>