api文档支持开放访问

This commit is contained in:
暮光:城中城
2021-12-05 22:58:48 +08:00
parent 928c79b747
commit 189e96ff42
70 changed files with 5578 additions and 2983 deletions

View File

@@ -2,7 +2,7 @@
<a-layout class="api-menu-trigger">
<a-layout-sider theme="light" :trigger="null" collapsible v-model:collapsed="appMenuCollapsed" :width="rightAsideWidth" style="height: 100vh;overflow: auto;">
<div class="logo">
<img src="../../assets/logo.png">
<img src="../../assets/api-logo.png">
<h1>API接口文档管理</h1>
</div>
<menu-layout :collapsed="appMenuCollapsed"></menu-layout>

View File

@@ -18,7 +18,7 @@
</template>
<script>
import {toRefs, ref, reactive, onMounted, watch} from 'vue';
import {toRefs, ref, reactive, onMounted, watch, nextTick} from 'vue';
import { useRouter, useRoute } from "vue-router";
import {useStore} from 'vuex';
import { message } from 'ant-design-vue';
@@ -53,7 +53,7 @@
let v2Doc = toJsonObj(res.data);
// osdoc.swagger 和 doc.openapi 的区别
if (typeof v2Doc !== 'object' || !v2Doc.openapi) {
message.error('获取文档数据请求失败');
message.error('获取文档数据失败请检查文档是否为标准的OpenApi文档格式');
return;
}
openApiDoc.value = v2Doc;
@@ -72,10 +72,11 @@
}, 0);
});
};
const loadTreeData = () => {
expandedKeys.value = ['main'];
const loadTreeData = async () => {
let metaInfo = {id: choiceDocId.value};
treeData.value = getTreeDataForTag(openApiDoc.value, tagPathMap.value, searchKeywords.value, metaInfo);
await nextTick();
expandedKeys.value = ['main'];
};
const toJsonObj = (value) => {
if (typeof value !== 'string') {

View File

@@ -18,7 +18,7 @@
</template>
<script>
import {toRefs, ref, reactive, onMounted, watch} from 'vue';
import {toRefs, ref, reactive, onMounted, watch, nextTick} from 'vue';
import { useRouter, useRoute } from "vue-router";
import {useStore} from 'vuex';
import { message } from 'ant-design-vue';
@@ -53,7 +53,7 @@
let v2Doc = toJsonObj(res.data);
if (typeof v2Doc !== 'object' || !v2Doc.swagger) {
callback(false);
message.error('获取文档数据请求失败');
message.error('获取文档数据失败请检查文档是否为标准的Swagger文档格式');
return;
}
swaggerDoc.value = v2Doc;
@@ -72,10 +72,11 @@
}, 0);
});
};
const loadTreeData = () => {
expandedKeys.value = ['main'];
const loadTreeData = async () => {
let metaInfo = {id: choiceDocId.value};
treeData.value = getTreeDataForTag(swaggerDoc.value, tagPathMap.value, searchKeywords.value, metaInfo);
await nextTick();
expandedKeys.value = ['main'];
};
const toJsonObj = (value) => {
if (typeof value !== 'string') {

View File

@@ -0,0 +1,154 @@
<template>
<a-layout class="api-menu-trigger">
<a-layout-sider theme="light" :trigger="null" collapsible v-model:collapsed="appMenuCollapsed" :width="rightAsideWidth" style="height: 100vh;overflow: auto;">
<div class="logo">
<img src="../../assets/api-logo.png">
<h1>API开放文档</h1>
</div>
<menu-layout :collapsed="appMenuCollapsed"></menu-layout>
</a-layout-sider>
<div ref="rightResize" class="right-resize" v-show="!appMenuCollapsed">
<i ref="rightResizeBar">...</i>
</div>
<a-layout>
<a-layout-header style="border-bottom: 2px solid #eee;background: #fff; padding: 0; box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);-webkit-box-shadow:0 1px 4px rgba(0, 21, 41, 0.08);">
<a-row type="flex">
<a-col flex="auto">
<MenuUnfoldOutlined class="trigger" v-if="appMenuCollapsed" @click="appMenuCollapsed = !appMenuCollapsed"/>
<MenuFoldOutlined class="trigger" v-else @click="appMenuCollapsed = !appMenuCollapsed"/>
</a-col>
<a-col flex="400px" style="text-align: right;padding-right: 20px;">
</a-col>
</a-row>
</a-layout-header>
<a-layout-content style="height: calc(100vh - 80px);overflow: auto;background: #fff;">
<router-view></router-view>
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script>
import MenuLayout from './MenuLayout.vue'
import {BarChartOutlined, MenuFoldOutlined, MenuUnfoldOutlined} from '@ant-design/icons-vue';
const minHeight = window.innerHeight - 64 - 122;
export default {
components: {MenuLayout, BarChartOutlined, MenuFoldOutlined, MenuUnfoldOutlined},
data() {
return {
minHeight: minHeight + 'px',
appMenuCollapsed: false,
rightAsideWidth: 300
}
},
computed: {},
mounted() {
this.dragChangeRightAsideWidth();
},
methods: {
dragChangeRightAsideWidth: function() {
// 保留this引用
let resize = this.$refs.rightResize;
let resizeBar = this.$refs.rightResizeBar;
resize.onmousedown = e => {
let startX = e.clientX;
// 颜色改变提醒
resize.style.background = "#ccc";
resizeBar.style.background = "#aaa";
resize.left = resize.offsetLeft;
document.onmousemove = e2 => {
// 计算并应用位移量
let endX = e2.clientX;
let moveLen = startX - endX;
if ((moveLen < 0 && this.rightAsideWidth < 600) || (moveLen > 0 && this.rightAsideWidth > 280)) {
startX = endX;
this.rightAsideWidth -= moveLen;
if (this.rightAsideWidth < 280) {
this.rightAsideWidth = 280;
}
}
};
document.onmouseup = () => {
// 颜色恢复
resize.style.background = "#fafafa";
resizeBar.style.background = "#ccc";
document.onmousemove = null;
document.onmouseup = null;
};
return false;
};
}
},
}
</script>
<style scoped>
.trigger {
font-size: 20px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color .3s;
}
.trigger:hover {
color: #1890ff;
}
.logo {
height: 64px;
position: relative;
line-height: 64px;
padding-left: 24px;
-webkit-transition: all .3s;
transition: all .3s;
overflow: hidden;
background: #1d4e89;
}
.logo h1 {
color: #fff;
font-size: 20px;
margin: 0 0 0 12px;
font-family: "Myriad Pro", "Helvetica Neue", Arial, Helvetica, sans-serif;
font-weight: 600;
display: inline-block;
height: 32px;
line-height: 32px;
vertical-align: middle;
}
.logo img {
width: 32px;
display: inline-block;
vertical-align: middle;
}
.api-menu-trigger {
min-height: 100%;
}
.right-resize {
width: 5px;
cursor: w-resize;
background: #fafafa;
}
.right-resize i{
margin-top: 300px;
width: 5px;
height: 35px;
display: inline-block;
word-wrap: break-word;
word-break: break-all;
line-height: 8px;
border-radius: 5px;
background: #ccc;
color: #888;
}
</style>
<style>
.ant-layout-sider {
transition: none;
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<template class="menu-layout-children" v-if="!menuItem.meta || !menuItem.meta.hidden">
<template v-if="!!menuItem.children">
<a-sub-menu :key="menuItem.path" v-if="haveShowChildren(menuItem.children)">
<template #title>
<template v-if="menuItem.meta">
<SettingOutlined v-if="menuItem.meta.icon === 'SettingOutlined'"/>
<FileTextOutlined v-if="menuItem.meta.icon === 'FileTextOutlined'"/>
</template>
<span>{{menuItem.name}}</span>
</template>
<MenuLayoutChildren :menuItem="children" v-for="children in menuItem.children"></MenuLayoutChildren>
</a-sub-menu>
</template>
<a-menu-item :key="menuItem.path" v-else>
<router-link :to="{path: menuItem.path, query: menuItem.query}">
<template v-if="menuItem.meta">
<DashboardOutlined v-if="menuItem.meta.icon === 'DashboardOutlined'"/>
<FileTextOutlined v-if="menuItem.meta.icon === 'FileTextOutlined'"/>
<InfoCircleOutlined v-if="menuItem.meta.icon === 'InfoCircleOutlined'"/>
</template>
<span>{{menuItem.name}}</span>
</router-link>
</a-menu-item>
</template>
</template>
<script>
import {
StarOutlined,
SettingOutlined,
CarryOutOutlined,
FileTextOutlined,
DashboardOutlined,
InfoCircleOutlined,
} from '@ant-design/icons-vue';
export default {
name: 'MenuLayoutChildren',
props: {
menuItem: Object,
},
data() {
return {}
},
components: {
StarOutlined, SettingOutlined, CarryOutOutlined, FileTextOutlined,
DashboardOutlined, InfoCircleOutlined
},
methods: {
haveShowChildren(children) {
return children.filter(item => (!item.meta || !item.meta.hidden)).length > 0;
},
}
}
</script>

View File

@@ -0,0 +1,139 @@
<template>
<div class="menu-layout">
<a-menu theme="light" mode="inline" :inline-collapsed="false" v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys">
<menu-children-layout :menuItem="menuItem" v-for="menuItem in menuData"></menu-children-layout>
</a-menu>
<a-divider style="margin: 6px 0;"/>
<div v-show="!collapsed" class="doc-tree">
<a-spin tip="加载中..." :spinning="treeDataLoading">
<div style="margin-bottom: 10px;">
<a-input-search v-model:value="searchKeywords" placeholder="搜索文档内容" style="width: 100%;margin-top: 10px;" @search="docSearch"/>
</div>
<template v-if="docChoice && docChoice.docType">
<DocTreeSwagger v-if="docChoice.docType === 1 || docChoice.docType === 2" ref="swaggerRef"></DocTreeSwagger>
<DocTreeOpenApi v-if="docChoice.docType === 3 || docChoice.docType === 4" ref="openApiRef"></DocTreeOpenApi>
</template>
</a-spin>
</div>
</div>
</template>
<script>
import {toRefs, ref, reactive, onMounted, watch, nextTick} from 'vue';
import { useRouter, useRoute } from "vue-router";
import {useStore} from 'vuex';
import { message } from 'ant-design-vue';
import MenuChildrenLayout from './MenuChildrenLayout.vue'
import {zyplayerApi} from '../../api'
import DocTreeSwagger from './doc-tree/Swagger.vue'
import DocTreeOpenApi from './doc-tree/OpenApi.vue'
export default {
props: {
collapsed: {
type: Boolean,
default: false
},
},
components: {MenuChildrenLayout, DocTreeSwagger, DocTreeOpenApi},
setup(props) {
const store = useStore();
const route = useRoute();
const router = useRouter();
let menuData = ref([]);
let selectedKeys = ref([]);
let openKeys = ref([]);
// 文档;
let treeDataLoading = ref(false);
let docResourceList = ref([]);
let docChoiceId = ref();
let searchKeywords = ref('');
let docChoice = ref({});
const getApiDocList = () => {
zyplayerApi.apiShareDocDetail({shareUuid: docChoiceId.value}).then(res => {
docChoice.value = res.data || {};
store.commit('setApiDoc', docChoice.value);
loadDoc();
});
};
let swaggerRef = ref();
let openApiRef = ref();
const loadDoc = async () => {
treeDataLoading.value = true;
await nextTick();
const loadDocCallback = () => {
treeDataLoading.value = false;
};
// 如果文档是swagger类型
if (docChoice.value.docType === 1 || docChoice.value.docType === 2) {
if (swaggerRef.value) {
swaggerRef.value.loadDoc(docChoiceId.value, searchKeywords.value, loadDocCallback);
}
} else if (docChoice.value.docType === 3 || docChoice.value.docType === 4) {
if (openApiRef.value) {
openApiRef.value.loadDoc(docChoiceId.value, searchKeywords.value, loadDocCallback);
}
}
};
const docSearch = () => {
loadDoc();
};
onMounted(() => {
docChoiceId.value = route.query.uuid;
if (!docChoiceId.value) {
message.error('访问的开放文档参数错误');
return;
}
// 左侧菜单处理
// menuData.value = router.options.routes.find((item) => item.path === '/share').children[0].children;
menuData.value = [
{
path: '/share/home',
name: '开放文档使用说明',
meta: {
icon: 'FileTextOutlined'
},
query: {uuid: docChoiceId.value},
}
];
let meta = route.meta || {};
let path = route.path;
if (!!meta.parentPath) {
path = meta.parentPath;
}
selectedKeys.value = [path];
let matched = route.matched;
if (matched.length >= 1) {
openKeys.value = [matched[1].path];
}
getApiDocList();
});
return {
menuData,
selectedKeys,
openKeys,
treeDataLoading,
docResourceList,
docChoiceId,
searchKeywords,
swaggerRef,
openApiRef,
docChoice,
docSearch,
};
},
};
</script>
<style>
.doc-tree{padding: 10px 4px;}
.doc-tree .ant-tree-switcher{width: 15px;}
.doc-tree .ant-tree-switcher-noop{width: 0;}
.doc-tree .ant-tag{margin-right: 0;}
.ant-badge-not-a-wrapper:not(.ant-badge-status) {
vertical-align: text-top;
}
</style>

View File

@@ -0,0 +1,97 @@
<template>
<div class="page-layout">
<a-tabs type="editable-card" hide-add v-model:activeKey="activePage" @tab-click="changePage" @edit="editPageTab" style="padding: 5px 10px 0;">
<a-tab-pane closable :tab="pageTabNameMap[item.fullPath]||item.name" :name="getRouteRealPath(item)" :fullPath="item.fullPath" :key="item.fullPath" v-for="item in pageList"/>
</a-tabs>
<div class="page-body">
<router-view v-slot="{ Component, route }">
<keep-alive>
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
</div>
</div>
</template>
<script>
export default {
name: 'PageTableView',
components: {},
data() {
return {
pageList: [],
linkList: [],
activePage: '',
multiPage: true,
ignoreParamPath: [
],
apiRequestIndex: 1,
}
},
computed: {
pageTabNameMap () {
return this.$store.state.pageTabNameMap;
}
},
created() {
let {name, path, fullPath} = this.$route;
this.pageList.push({name, path, fullPath});
let activePage = this.getRouteRealPath(this.$route);
this.linkList.push(activePage);
this.activePage = activePage;
this.$router.push(this.$route.fullPath);
},
watch: {
'$route': function (newRoute, oldRoute) {
let activePage = this.getRouteRealPath(newRoute);
this.activePage = activePage;
if (this.linkList.indexOf(activePage) < 0) {
this.linkList.push(activePage);
let {name, path, fullPath} = newRoute;
this.pageList.push({name, path, fullPath});
}
let pageRoute = this.pageList.find(item => this.getRouteRealPath(item) === activePage);
pageRoute.fullPath = newRoute.fullPath;
},
},
methods: {
isIgnoreParamPath(path) {
return this.ignoreParamPath.indexOf(path) >= 0;
},
getRouteRealPath(route) {
return this.isIgnoreParamPath(route.path) ? route.path : route.fullPath;
},
changePage(tab) {
let checkedTab = this.pageList.find(item => item.fullPath === tab);
this.activePage = this.getRouteRealPath(checkedTab);
this.$router.push(checkedTab.fullPath);
},
editPage(key, action) {
this[action](key);
},
editPageTab(key, action) {
this.removePageTab(key);
},
removePageTab(key) {
if (this.pageList.length === 1) {
this.$message.warning('这是最后一页,不能再关闭了啦');
return;
}
this.pageList = this.pageList.filter(item => this.getRouteRealPath(item) !== key);
this.linkList = this.linkList.filter(item => item !== key);
let index = this.linkList.indexOf(this.activePage);
if (index < 0) {
index = this.linkList.length - 1;
this.activePage = this.linkList[index];
this.$router.push(this.activePage);
}
},
}
}
</script>
<style>
.page-layout{background: #fff;}
.page-layout .page-body{padding: 0 10px 10px 10px;}
.ant-tabs-bar{margin-bottom: 0;}
</style>

View File

@@ -0,0 +1,106 @@
<template>
<a-directory-tree :showIcon="false" :tree-data="treeData" v-model:expandedKeys="expandedKeys" @select="docChecked">
<template #title="{ title, isLeaf, method, children, key }">
<template v-if="isLeaf">
<a-tag color="pink" v-if="method === 'get'">get</a-tag>
<a-tag color="red" v-else-if="method === 'post'">post</a-tag>
<a-tag color="orange" v-else-if="method === 'put'">put</a-tag>
<a-tag color="green" v-else-if="method === 'head'">head</a-tag>
<a-tag color="cyan" v-else-if="method === 'patch'">patch</a-tag>
<a-tag color="blue" v-else-if="method === 'delete'">delete</a-tag>
<a-tag color="purple" v-else-if="method === 'options'">options</a-tag>
<a-tag color="purple" v-else-if="method === 'trace'">trace</a-tag>
</template>
<span style="margin: 0 6px 0 3px;">{{title}}</span>
<a-badge v-if="children" :count="children.length" :number-style="{backgroundColor: '#fff', color: '#999', boxShadow: '0 0 0 1px #d9d9d9 inset'}"/>
</template>
</a-directory-tree>
</template>
<script>
import {toRefs, ref, reactive, onMounted, watch, nextTick} from 'vue';
import { useRouter, useRoute } from "vue-router";
import {useStore} from 'vuex';
import { message } from 'ant-design-vue';
import {zyplayerApi} from '../../../api'
import {analysisOpenApiData, getTreeDataForTag} from '../../../assets/core/OpenApiTreeAnalysis.js'
export default {
setup() {
const store = useStore();
const route = useRoute();
const router = useRouter();
let tagPathMap = ref({});
let openApiDoc = ref({});
let treeData = ref([]);
let expandedKeys = ref([]);
let choiceDocId = ref('');
let searchKeywords = ref('');
const docChecked = (val, node) => {
if (node.node.isLeaf) {
let dataRef = node.node.dataRef;
router.push({path: '/share/openapi/view', query: dataRef.query});
}
};
const loadDoc = (docId, keyword, callback) => {
choiceDocId.value = docId;
searchKeywords.value = keyword;
zyplayerApi.apiShareDocApisDetail({shareUuid: docId}).then(res => {
let v2Doc = toJsonObj(res.data);
// osdoc.swagger 和 doc.openapi 的区别
if (typeof v2Doc !== 'object' || !v2Doc.openapi) {
message.error('获取文档数据失败请检查文档是否为标准的OpenApi文档格式');
return;
}
openApiDoc.value = v2Doc;
store.commit('setOpenApiDoc', v2Doc);
let treeData = analysisOpenApiData(v2Doc);
store.commit('setOpenApiUrlMethodMap', treeData.urlMethodMap);
store.commit('setOpenApiMethodStatistic', treeData.methodStatistic);
tagPathMap.value = treeData.tagPathMap;
loadTreeData();
callback();
});
};
const loadTreeData = async () => {
let metaInfo = {uuid: choiceDocId.value};
treeData.value = getTreeDataForTag(openApiDoc.value, tagPathMap.value, searchKeywords.value, metaInfo);
await nextTick();
expandedKeys.value = ['main'];
};
const toJsonObj = (value) => {
if (typeof value !== 'string') {
return value;
}
try {
return JSON.parse(value);
} catch (e) {
try {
// 处理变态的单双引号共存字符串
return eval('(' + value + ')');
} catch (e) {
return value || undefined;
}
}
};
return {
expandedKeys,
docChecked,
loadDoc,
treeData,
};
},
};
</script>
<style>
.doc-tree{padding: 10px 4px;}
.doc-tree .ant-tree-switcher{width: 15px;}
.doc-tree .ant-tree-switcher-noop{width: 0;}
.doc-tree .ant-tag{margin-right: 0;}
.ant-badge-not-a-wrapper:not(.ant-badge-status) {
vertical-align: text-top;
}
</style>

View File

@@ -0,0 +1,106 @@
<template>
<a-directory-tree :showIcon="false" :tree-data="treeData" v-model:expandedKeys="expandedKeys" @select="docChecked">
<template #title="{ title, isLeaf, method, children, key }">
<template v-if="isLeaf">
<a-tag color="pink" v-if="method === 'get'">get</a-tag>
<a-tag color="red" v-else-if="method === 'post'">post</a-tag>
<a-tag color="orange" v-else-if="method === 'put'">put</a-tag>
<a-tag color="green" v-else-if="method === 'head'">head</a-tag>
<a-tag color="cyan" v-else-if="method === 'patch'">patch</a-tag>
<a-tag color="blue" v-else-if="method === 'delete'">delete</a-tag>
<a-tag color="purple" v-else-if="method === 'options'">options</a-tag>
<a-tag color="purple" v-else-if="method === 'trace'">trace</a-tag>
</template>
<span style="margin: 0 6px 0 3px;">{{title}}</span>
<a-badge v-if="children" :count="children.length" :number-style="{backgroundColor: '#fff', color: '#999', boxShadow: '0 0 0 1px #d9d9d9 inset'}"/>
</template>
</a-directory-tree>
</template>
<script>
import {toRefs, ref, reactive, onMounted, watch, nextTick} from 'vue';
import { useRouter, useRoute } from "vue-router";
import {useStore} from 'vuex';
import { message } from 'ant-design-vue';
import {zyplayerApi} from '../../../api'
import {analysisSwaggerData, getTreeDataForTag} from '../../../assets/core/SwaggerTreeAnalysis.js'
export default {
setup() {
const store = useStore();
const route = useRoute();
const router = useRouter();
let tagPathMap = ref({});
let swaggerDoc = ref({});
let treeData = ref([]);
let expandedKeys = ref([]);
let choiceDocId = ref('');
let searchKeywords = ref('');
const docChecked = (val, node) => {
if (node.node.isLeaf) {
let dataRef = node.node.dataRef;
router.push({path: '/share/swagger/view', query: dataRef.query});
}
};
const loadDoc = (docId, keyword, callback) => {
choiceDocId.value = docId;
searchKeywords.value = keyword;
zyplayerApi.apiShareDocApisDetail({shareUuid: docId}).then(res => {
let v2Doc = toJsonObj(res.data);
if (typeof v2Doc !== 'object' || !v2Doc.swagger) {
callback(false);
message.error('获取文档数据失败请检查文档是否为标准的Swagger文档格式');
return;
}
swaggerDoc.value = v2Doc;
store.commit('setSwaggerDoc', v2Doc);
let treeData = analysisSwaggerData(v2Doc);
store.commit('setSwaggerUrlMethodMap', treeData.urlMethodMap);
store.commit('setSwaggerMethodStatistic', treeData.methodStatistic);
tagPathMap.value = treeData.tagPathMap;
loadTreeData();
callback(true);
});
};
const loadTreeData = async () => {
let metaInfo = {uuid: choiceDocId.value};
treeData.value = getTreeDataForTag(swaggerDoc.value, tagPathMap.value, searchKeywords.value, metaInfo);
await nextTick();
expandedKeys.value = ['main'];
};
const toJsonObj = (value) => {
if (typeof value !== 'string') {
return value;
}
try {
return JSON.parse(value);
} catch (e) {
try {
// 处理变态的单双引号共存字符串
return eval('(' + value + ')');
} catch (e) {
return value || undefined;
}
}
};
return {
expandedKeys,
docChecked,
loadDoc,
treeData,
};
},
};
</script>
<style>
.doc-tree{padding: 10px 4px;}
.doc-tree .ant-tree-switcher{width: 15px;}
.doc-tree .ant-tree-switcher-noop{width: 0;}
.doc-tree .ant-tag{margin-right: 0;}
.ant-badge-not-a-wrapper:not(.ant-badge-status) {
vertical-align: text-top;
}
</style>