fix: 【Api配置】修复api响应参数类型展示错误,接口文档添加注释

This commit is contained in:
XieYongHong 2024-10-17 15:22:34 +08:00
parent 08c8eda133
commit bda3e6a4c7
7 changed files with 456 additions and 228 deletions

View File

@ -24,7 +24,12 @@
</div>
<div class="api-card" v-if="requestCard.codeText !== undefined">
<h5>请求示例</h5>
<JsonViewer :value="requestCard.codeText" copyable />
<Monaco
:tips="requestCard.tips"
:codeText="requestCard.codeText"
:loading="loading"
/>
</div>
<div class="api-card" v-if="requestCard.tableData.length">
<h5>请求参数</h5>
@ -37,10 +42,7 @@
size="small"
>
<template #required="slotProps">
<span>{{ Boolean(slotProps.required) + '' }}</span>
</template>
<template #type="slotProps">
<span>{{ slotProps?.schema.type }}</span>
<span :style="{ color: Boolean(slotProps.required) ? '#f81d22' : ''}">{{ Boolean(slotProps.required) + '' }}</span>
</template>
</j-pro-table>
</div>
@ -80,18 +82,23 @@
</j-pro-table>
</div>
<JsonViewer :value="respParamsCard.codeText" copyable />
<!-- <JsonViewer :value="respParamsCard.codeText" copyable />-->
<Monaco
:tips="respParamsCard.tips"
:codeText="respParamsCard.codeText"
:loading="loading"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
<script setup lang="ts" name="APIDoes">
import type { apiDetailsType } from '../typing';
import InputCard from './InputCard.vue';
import { PropType } from 'vue';
import type { PropType } from 'vue';
import { findData, getCodeText, dealNoRef } from '../utils';
import {randomString} from "@/utils/utils";
import Monaco from './monaco.vue'
type cardType = {
columns: object[];
@ -112,38 +119,49 @@ const props = defineProps({
},
});
const { selectApi } = toRefs(props);
const loading = ref(false)
const requestCard = reactive<cardType>({
columns: [
{
title: '参数名',
dataIndex: 'name',
key: 'name',
dataIndex: 'paramsName',
key: 'paramsName',
width: 320
},
{
title: '参数说明',
dataIndex: 'description',
key: 'description',
dataIndex: 'desc',
key: 'desc',
},
{
title: '请求类型',
dataIndex: 'in',
key: 'in',
width: 80
},
{
title: '是否必须',
dataIndex: 'required',
key: 'required',
scopedSlots: true,
width: 80
},
{
title: '参数类型',
dataIndex: 'paramsType',
key: 'paramsType',
width: 200
},
{
title: 'schema',
dataIndex: 'type',
key: 'type',
scopedSlots: true,
width: 200
},
],
tableData: [],
tips: [],
codeText: undefined,
getData: () => {
if (!props.selectApi.requestBody)
@ -159,27 +177,30 @@ const requestCard = reactive<cardType>({
const schemaName = _ref?.split('/').pop();
const type = schema.type || '';
const tableData = findData(props.schemas, schemaName);
requestCard.codeText =
type === 'array'
? [getCodeText(props.schemas, tableData, 3)]
: getCodeText(props.schemas, tableData, 3);
// requestCard.codeText =
// type === 'array'
// ? [getCodeText(props.schemas, tableData, 3)]
// : getCodeText(props.schemas, tableData, 3);
const { codeText, codeTips } = getCodeText(props.schemas, tableData, 3)
requestCard.codeText = JSON.stringify(codeText)
requestCard.tips = codeTips
requestCard.tableData = [
{
name: schemaName[0].toLowerCase() + schemaName.substring(1),
description: schemaName,
paramsName: schemaName[0].toLowerCase() + schemaName.substring(1),
desc: schemaName,
in: 'body',
id: randomString(),
required: true,
schema: { type: type || schemaName },
children: tableData.map((item) => ({
name: item.paramsName,
description: item.desc,
required: false,
schema: { type: item.paramsType },
})),
paramsType: type || schemaName,
type: type || schemaName,
children: tableData,
},
];
// console.log(requestCard,'requestCard')
}
setTimeout(() => {
loading.value = true
}, 1000)
},
});
const responseStatusCard = reactive<cardType>({
@ -189,6 +210,7 @@ const responseStatusCard = reactive<cardType>({
title: '状态码',
dataIndex: 'code',
key: 'code',
width: 200
},
{
title: '说明',
@ -199,6 +221,7 @@ const responseStatusCard = reactive<cardType>({
title: 'schema',
dataIndex: 'schema',
key: 'schema',
width: 200
},
],
tableData: [],
@ -231,6 +254,7 @@ const respParamsCard = reactive<cardType>({
{
title: '参数名称',
dataIndex: 'paramsName',
width: 320
},
{
title: '参数说明',
@ -239,9 +263,16 @@ const respParamsCard = reactive<cardType>({
{
title: '类型',
dataIndex: 'paramsType',
width: 200
},
{
title: 'schema',
dataIndex: 'schema',
width: 200
},
],
tableData: [],
tips: [],
codeText: '',
getData: (code: string) => {
const schemaName = responseStatusCard.tableData.find(
@ -249,11 +280,22 @@ const respParamsCard = reactive<cardType>({
)?.schema;
const tableData = findData(props.schemas, schemaName);
respParamsCard.codeText = getCodeText(props.schemas, tableData, 3);
const { codeText, codeTips } = getCodeText(props.schemas, tableData, 3)
respParamsCard.codeText = JSON.stringify(codeText)
respParamsCard.tips = codeTips
respParamsCard.tableData = tableData;
setTimeout(() => {
loading.value = true
}, 1000)
},
});
const options = {
minimap: {
enabled: false
}
}
const getContent = (data: any) => {
if (data && data.content) {
return Object.keys(data.content || {})[0];
@ -263,17 +305,18 @@ const getContent = (data: any) => {
onMounted(() => {
requestCard.getData();
responseStatusCard.getData();
watch(
() => props.selectApi,
() => {
requestCard.getData();
responseStatusCard.getData();
},
);
});
watch([() => responseStatusCard.activeKey, () => props.selectApi], (n) => {
n[0] && respParamsCard.getData(n[0]);
});
watch(
() => props.selectApi,
() => {
requestCard.getData();
responseStatusCard.getData();
},
);
watch([() => responseStatusCard.activeKey, () => props.selectApi], (n) => {
n[0] && respParamsCard.getData(n[0]);
});
</script>

View File

@ -5,7 +5,7 @@
<div class="input">
<InputCard :value="props.selectApi.method" />
<j-input :value="props.selectApi?.url" disabled />
<span class="send" @click="send">发送</span>
<j-button type="primary" @click="send" :loading="loading">发送</j-button>
</div>
</div>
@ -107,14 +107,31 @@
<AIcon type="PlusOutlined" />新增
</j-button>
</div>
<j-monaco-editor
<a-select v-model:value="bodyType" @change="handleChangeBodyType">
<a-select-option value="json">Json</a-select-option>
<a-select-option value="text">Text</a-select-option>
</a-select>
<template
v-if="showRequestBody"
ref="editorRef"
language="json"
style="height: 100% ; min-height: 200px;"
theme="vs"
v-model:modelValue="requestBody.code"
/>
>
<j-monaco-editor
v-if="bodyType === 'json'"
ref="editorRef"
language="json"
style="height: 100% ; min-height: 200px;"
theme="vs"
v-model:modelValue="requestBody.code"
/>
<j-monaco-editor
v-else
ref="editorRef"
language="text"
style="height: 100% ; min-height: 200px;"
theme="vs"
v-model:modelValue="requestBody.code"
/>
</template>
</div>
</div>
<div class="api-card">
@ -145,6 +162,8 @@ const editorRef = ref();
const formRef = ref<FormInstance>();
const method = ref()
const showRequestBody = ref(!!props.selectApi?.requestBody)
const bodyType = ref('text');
const loading = ref(false);
const requestBody = reactive({
tableColumns: [
{
@ -208,6 +227,11 @@ const init = () => {
};
init();
const handleChangeBodyType = () => {
console.log(editorRef.value)
editorRef.value?.setModelLanguage(editorRef.value.getModel(), bodyType.value);
}
const send = () => {
if (paramsTable.value.length)
formRef.value &&
@ -240,16 +264,22 @@ const _send = () => {
...urlParams,
};
}else{
params = JSON.parse(requestBody.code || '{}')
if(bodyType.value == 'text') {
params = requestBody.code
} else {
params = JSON.parse(requestBody.code || '{}')
}
}
server[methodObj[methodName]](url, params).then((resp: any) => {
loading.value = true;
server[methodObj[methodName]](url, params, {}, bodyType.value === 'text' ? {headers: {'Content-Type': 'text/plain'}} : {}).then((resp: any) => {
// body
if (Object.keys(params).length === 0 && refStr.value) {
requestBody.code = JSON.stringify(getDefaultParams());
editorRef.value?.editorFormat();
}
responsesContent.value = resp;
}).finally(() => {
loading.value = false;
});
};

View File

@ -5,6 +5,7 @@
:columns="columns"
:dataSource="_tableData"
:rowSelection="props.mode !== 'home' ? rowSelection : undefined"
:style="{padding: 0}"
noPagination
model="TABLE"
>

View File

@ -0,0 +1,63 @@
<template>
<div class="api-example">
<j-monaco-editor
v-if="loading"
language="json"
:model-value="codeText"
:init="init"
/>
<div class="api-example-tips" :style="{ transform: `translateY(${scrollTop}px)`}" v-if="loading">
<div class="tips-line" v-for="tip in tips">
<span v-if="tip">//{{tip}}</span>
</div>
</div>
</div>
</template>
<script setup name="ApiMonaco">
defineProps({
tips: {
type: Array,
default: () => []
},
codeText: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: false
}
})
const scrollTop = ref(0)
const init = (editor) => {
editor.onDidScrollChange(e => {
scrollTop.value = 0 - e.scrollTop
})
}
</script>
<style scoped lang="less">
.api-example {
height: 350px;
position: relative;
overflow: hidden;
.api-example-tips {
position: absolute;
left: 50%;
top: 19px;
bottom: 0;
right: 0;
pointer-events: none;
color: #608b4e;
.tips-line {
height: 19px;
}
}
}
</style>

View File

@ -1,76 +1,77 @@
<template>
<div class="api-page-container">
<div class="top">
<slot name="top" />
</div>
<j-row class="content" :style="{padding:'24px'}" >
<j-col
:span="24"
v-if="props.showTitle"
style="font-size: 16px;margin-bottom: 48px;"
>
API文档
</j-col>
<j-col :span="5" class="tree-content">
<LeftTree
@select="treeSelect"
:mode="props.mode"
:has-home="props.hasHome"
:code="props.code"
/>
</j-col>
<j-col :span="19">
<HomePage v-show="showHome" />
<div class="url-page" v-show="!showHome">
<ChooseApi
v-show="!selectedApi.url"
v-model:click-api="selectedApi"
v-model:selectedRowKeys="selectedKeys"
v-model:changedApis="changedApis"
:table-data="tableData"
:source-keys="selectSourceKeys"
:mode="props.mode"
@refresh="getSelectKeys"
/>
<div
class="api-details"
v-if="selectedApi.url && tableData.length > 0"
>
<j-button
@click="selectedApi = initSelectedApi"
style="margin-bottom: 24px"
>返回</j-button
>
<j-tabs v-model:activeKey="activeKey" type="card">
<j-tab-pane key="does" tab="文档">
<ApiDoes
:select-api="selectedApi"
:schemas="schemas"
/>
</j-tab-pane>
<j-tab-pane key="test" tab="调试">
<ApiTest
:select-api="selectedApi"
:schemas="schemas"
/>
</j-tab-pane>
</j-tabs>
</div>
</div>
</j-col>
</j-row>
<div class="api-page-container">
<div class="top">
<slot name="top" />
</div>
<div class="api-page-content" :style="styles">
<div
v-if="props.showTitle"
style="font-size: 16px;margin-bottom: 48px;"
>
API文档
</div>
<div class="api-page-body">
<div class="tree-content">
<LeftTree
@select="treeSelect"
:mode="props.mode"
:has-home="props.hasHome"
:code="props.code"
/>
</div>
<div class="api-page-detail">
<HomePage v-show="showHome" />
<div class="url-page" v-show="!showHome">
<ChooseApi
v-show="!selectedApi.url"
v-model:click-api="selectedApi"
v-model:selectedRowKeys="selectedKeys"
v-model:changedApis="changedApis"
:table-data="tableData"
:source-keys="selectSourceKeys"
:mode="props.mode"
@refresh="getSelectKeys"
/>
<div
class="api-details"
v-if="selectedApi.url && tableData.length > 0"
>
<j-button
@click="selectedApi = initSelectedApi"
style="margin-bottom: 24px; width: 80px">返回</j-button>
<div class="api-details-tabs">
<j-tabs v-model:activeKey="activeKey" type="card">
<j-tab-pane key="does" tab="文档">
<ApiDoes
:select-api="selectedApi"
:schemas="schemas"
/>
</j-tab-pane>
<j-tab-pane key="test" tab="调试">
<ApiTest
:select-api="selectedApi"
:schemas="schemas"
/>
</j-tab-pane>
</j-tabs>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="apiPage">
import HomePage from './components/HomePage.vue';
import { getApiGranted_api, apiOperations_api } from '@/api/system/apiPage';
import type {
treeNodeTpye,
apiObjType,
apiDetailsType,
modeType,
treeNodeTpye,
apiObjType,
apiDetailsType,
modeType,
} from './typing';
import LeftTree from './components/LeftTree.vue';
import ChooseApi from './components/ChooseApi.vue';
@ -81,46 +82,52 @@ import { useDepartmentStore } from '@/store/department';
const department = useDepartmentStore();
const props = defineProps<{
mode: modeType;
showTitle?: boolean;
hasHome?: boolean;
code?: string;
mode: modeType;
showTitle?: boolean;
hasHome?: boolean;
code?: string;
}>();
const showHome = ref<boolean>(Boolean(props.hasHome)); // home
const tableData = ref([]);
const styles = computed(() => {
return {
padding: props.mode === 'api' ? 0 : '24px'
}
})
const treeSelect = (node: treeNodeTpye, nodeSchemas: object = {}) => {
if (node.key === 'home') return (showHome.value = true);
schemas.value = nodeSchemas;
if (!node.apiList) return;
showHome.value = false;
const apiList: apiObjType[] = node.apiList as apiObjType[];
const table: any = [];
//
apiList?.forEach((apiItem) => {
const { method, url } = apiItem as any;
for (const key in method) {
if (Object.prototype.hasOwnProperty.call(method, key)) {
table.push({
...method[key],
url,
method: key,
id: method[key].operationId,
});
}
}
});
tableData.value = table;
if (node.key === 'home') return (showHome.value = true);
schemas.value = nodeSchemas;
if (!node.apiList) return;
showHome.value = false;
const apiList: apiObjType[] = node.apiList as apiObjType[];
const table: any = [];
//
apiList?.forEach((apiItem) => {
const { method, url } = apiItem as any;
for (const key in method) {
if (Object.prototype.hasOwnProperty.call(method, key)) {
table.push({
...method[key],
url,
method: key,
id: method[key].operationId,
});
}
}
});
tableData.value = table;
};
const activeKey = ref<'does' | 'test'>('does');
const schemas = ref({}); // api
const initSelectedApi: apiDetailsType = {
url: '',
method: '',
summary: '',
parameters: [],
responses: {},
requestBody: {},
url: '',
method: '',
summary: '',
parameters: [],
responses: {},
requestBody: {},
};
const selectedApi = ref<apiDetailsType>(initSelectedApi);
@ -131,62 +138,95 @@ const changedApis = ref({}); // 勾选发生变化的项以对应的id作为k
init();
function init() {
getSelectKeys();
watch(tableData, () => {
activeKey.value = 'does';
selectedApi.value = initSelectedApi;
});
watch(
() => selectedApi.value.url,
() => (activeKey.value = 'does'),
);
getSelectKeys();
watch(tableData, () => {
activeKey.value = 'does';
selectedApi.value = initSelectedApi;
});
watch(
() => selectedApi.value.url,
() => (activeKey.value = 'does'),
);
}
/**
* 右侧api选中项
*/
function getSelectKeys() {
if (props.mode === 'appManger') {
getApiGranted_api(props.code as string).then((resp) => {
selectedKeys.value = resp.result as string[];
selectSourceKeys.value = [...(resp.result as string[])];
changedApis.value = {};
});
} else if (props.mode === 'api') {
apiOperations_api().then((resp) => {
selectedKeys.value = resp.result as string[];
selectSourceKeys.value = [...(resp.result as string[])];
});
}
if (props.mode === 'appManger') {
getApiGranted_api(props.code as string).then((resp) => {
selectedKeys.value = resp.result as string[];
selectSourceKeys.value = [...(resp.result as string[])];
changedApis.value = {};
});
} else if (props.mode === 'api') {
apiOperations_api().then((resp) => {
selectedKeys.value = resp.result as string[];
selectSourceKeys.value = [...(resp.result as string[])];
});
}
}
watch(
() => selectedKeys.value,
(val: any) => {
// console.log('selectedKeys: ', val);
department.setSelectedKeys(val);
},
() => selectedKeys.value,
(val: any) => {
// console.log('selectedKeys: ', val);
department.setSelectedKeys(val);
},
);
watch(
() => changedApis.value,
(val: any) => {
// console.log('changedApis: ', val);
department.setChangedApis(val);
},
() => changedApis.value,
(val: any) => {
// console.log('changedApis: ', val);
department.setChangedApis(val);
},
);
</script>
<style lang="less" scoped>
.api-page-container {
.content {
background-color: #fff;
margin: 0 !important;
.tree-content {
padding-bottom: 30px;
height: calc(100vh - 230px);
overflow-y: auto;
border-right: 1px solid #e9e9e9;
height: 100%;
.api-page-content {
background-color: #fff;
margin: 0 !important;
.api-page-body {
position: relative;
display: flex;
gap: 16px;
.tree-content {
height: calc(100vh - 230px);
width: 280px;
overflow-y: auto;
border-right: 1px solid #e9e9e9;
}
.api-page-detail {
position: absolute;
left: 296px;
top: 0;
bottom: 0;
right: 0;
.url-page {
height: 100%;
}
.api-details {
max-height: 100%;
display: flex;
flex-direction: column;
.api-details-tabs {
overflow-y: auto;
}
}
}
}
}
}
</style>

View File

@ -34,6 +34,11 @@ export type modeType = 'api'| 'appManger' | 'home'
export type schemaObjType = {
paramsName: string;
paramsType: string;
id: string;
type?: string;
schema?: string;
required?: boolean
desc?: string;
children?: schemaObjType[];
};

View File

@ -1,4 +1,5 @@
import { schemaObjType } from "./typing";
import {randomString} from "@/utils/utils";
/**
@ -14,22 +15,32 @@ export function findData(schemas: object, schemaName: string , paths:string[]=[]
}
const result: schemaObjType[] = [];
const schema = schemas[schemaName];
const required = schema.required || []
Object.entries(schema.properties).forEach((item: [string, any]) => {
const [paramsName, extra] = item
const paramsType =
(item[1].$ref && item[1].$ref.split('/').pop()) ||
(item[1].items?.$ref && item[1].items.$ref.split('/').pop()) ||
item[1].item?.type ||
item[1].type ||
(extra.$ref && extra.$ref.split('/').pop()) ||
(extra.items?.$ref && extra.items.$ref.split('/').pop()) ||
extra.item?.type ||
extra.type ||
'';
const schema = extra.format ? `${paramsType}(${extra.format})` : ''
const schemaObj: schemaObjType = {
paramsName: item[0],
paramsName: paramsName,
paramsType,
desc: item[1].description || '',
required: required.includes(paramsName),
desc: extra.description || '',
schema: schema,
type: !basicType.includes(paramsType) ? paramsType : '',
id: randomString()
};
if (!basicType.includes(paramsType) && paths.filter(path=>path === schemaName).length >=2 ){
if (!basicType.includes(paramsType) && (paths.filter(path=>path === schemaName).length <=2) ){
paths.push(schemaName)
schemaObj.children = findData(schemas, paramsType);
schemaObj.children = findData(schemas, paramsType, paths);
}
result.push(schemaObj);
});
@ -47,54 +58,89 @@ export function getCodeText(
schemas: object,
arr: schemaObjType[],
level: number,
): object {
const result = {};
paths: string[] = []
): any {
const tips: Array<string | undefined> = []
let result = {}
arr.forEach((item) => {
let value: any = ""
tips.push(item.desc)
switch (item.paramsType) {
case 'string':
result[item.paramsName] = '';
value = ''
break;
case 'integer':
result[item.paramsName] = 0;
case 'number':
value = 0
break;
case 'boolean':
result[item.paramsName] = true;
value = true
break;
case 'array':
result[item.paramsName] = [];
value = []
break;
case 'object':
result[item.paramsName] = '';
break;
case 'number':
result[item.paramsName] = 0;
value = {}
break;
default: {
const properties = schemas[item.paramsType]?.properties as object || {};
const newArr = Object.entries(properties).map(
(item: [string, any]) => {
return{
paramsName: item[0],
paramsType: level
? (item[1].$ref && item[1].$ref.split('/').pop()) ||
(item[1].items?.$ref &&
item[1].items.$ref.split('/').pop()) ||
item[1].item?.type ||
item[1].type ||
''
: item[1].type,
}},
);
result[item.paramsName] = getCodeText(
schemas,
newArr,
level - 1,
);
if (item.children) {
if (paths.filter(path=> path === item.paramsName).length >=2) {
break
}
paths.push(item.paramsName)
const _result = getCodeText(
schemas,
item.children,
level - 1,
paths
)
value = _result.codeText
tips.push(..._result.codeTips)
tips.push(undefined)
} else {
if (paths.filter(path=> path === item.paramsName).length >=2) {
break
}
paths.push(item.paramsName)
const properties = schemas[item.paramsType]?.properties as object || {};
const newArr = Object.entries(properties).map(
(item: [string, any]) => {
const [paramsName, extra] = item
const paramsType =
(extra.$ref && extra.$ref.split('/').pop()) ||
(extra.items?.$ref && extra.items.$ref.split('/').pop()) ||
extra.item?.type ||
extra.type ||
'';
return{
paramsName: paramsName,
desc: extra.description || '',
paramsType: paramsType,
schema: extra.format ? `${paramsType}(${extra.format})` : ''
}},
);
const _result = getCodeText(
schemas,
newArr,
level - 1,
paths
)
value = _result.codeText
tips.push(..._result.codeTips)
tips.push(undefined)
}
}
}
result[item.paramsName] = value
});
return {
codeText: result,
codeTips: tips
return result;
};
}
/**