优化交互,开放文档支持目录导航

This commit is contained in:
暮光:城中城
2021-12-06 23:03:56 +08:00
parent 189e96ff42
commit 1c747054bb
39 changed files with 7870 additions and 870 deletions

View File

@@ -1,6 +1,6 @@
<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;">
<a-layout-sider theme="light" :trigger="null" collapsible v-model:collapsed="appMenuCollapsed" :width="leftAsideWidth" style="height: 100vh;overflow: auto;">
<div class="logo">
<img src="../../assets/api-logo.png">
<h1>API开放文档</h1>
@@ -14,8 +14,8 @@
<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"/>
<MenuUnfoldOutlined class="trigger" v-if="appMenuCollapsed" @click="turnLeftCollapse"/>
<MenuFoldOutlined class="trigger" v-else @click="turnLeftCollapse"/>
</a-col>
<a-col flex="400px" style="text-align: right;padding-right: 20px;">
</a-col>
@@ -39,15 +39,28 @@
return {
minHeight: minHeight + 'px',
appMenuCollapsed: false,
rightAsideWidth: 300
leftAsideWidth: 300
}
},
computed: {},
mounted() {
this.dragChangeRightAsideWidth();
this.dragChangeLeftAsideWidth();
},
methods: {
dragChangeRightAsideWidth: function() {
turnLeftCollapse() {
this.appMenuCollapsed = !this.appMenuCollapsed;
setTimeout(() => {
if (this.appMenuCollapsed) {
this.leftAsideWidthChange(this.leftAsideWidth + 1);
} else {
this.leftAsideWidthChange(1);
}
}, 100);
},
leftAsideWidthChange(width) {
this.$store.commit('setLeftAsideWidth', width);
},
dragChangeLeftAsideWidth: function() {
// 保留this引用
let resize = this.$refs.rightResize;
let resizeBar = this.$refs.rightResizeBar;
@@ -61,12 +74,13 @@
// 计算并应用位移量
let endX = e2.clientX;
let moveLen = startX - endX;
if ((moveLen < 0 && this.rightAsideWidth < 600) || (moveLen > 0 && this.rightAsideWidth > 280)) {
if ((moveLen < 0 && this.leftAsideWidth < 600) || (moveLen > 0 && this.leftAsideWidth > 280)) {
startX = endX;
this.rightAsideWidth -= moveLen;
if (this.rightAsideWidth < 280) {
this.rightAsideWidth = 280;
this.leftAsideWidth -= moveLen;
if (this.leftAsideWidth < 280) {
this.leftAsideWidth = 280;
}
this.leftAsideWidthChange(this.leftAsideWidth);
}
};
document.onmouseup = () => {

View File

@@ -13,6 +13,8 @@ export default createStore({
apiDoc: {},
// 全局参数
globalParam: [],
// 左侧菜单栏宽度
leftAsideWidth: 0,
// swagger原始文档
swaggerDoc: {},
@@ -36,7 +38,13 @@ export default createStore({
getters: {
getDocChangedNum: (state) => () => {
return state.docChangedNum;
}
},
getLeftAsideWidth: (state) => () => {
return state.leftAsideWidth;
},
getApiDoc: (state) => () => {
return state.apiDoc;
},
},
mutations: {
setUserInfo(state, userInfo) {
@@ -51,6 +59,9 @@ export default createStore({
addDocChangedNum(state) {
state.docChangedNum++;
},
setLeftAsideWidth(state, leftAsideWidth) {
state.leftAsideWidth = leftAsideWidth;
},
// swagger
setSwaggerDoc(state, swaggerDoc) {
state.swaggerDoc = swaggerDoc;

View File

@@ -90,9 +90,9 @@
</template>
</a-form-item>
<a-form-item label="文档内容" required name="jsonContent" v-else-if="docEdit.docType === 2">
<!-- 尝试使用ace编辑器但感觉除了卡顿意义不大不会在这里编辑json内容暂时用textarea代替-->
<!-- <ace-editor v-model:value="docEdit.jsonContent" lang="json" theme="monokai" width="100%" height="100" :options="aceEditorConfig"></ace-editor>-->
<a-textarea placeholder="请输入JSON格式的Swagger文档内容" v-model:value="docEdit.jsonContent" :auto-size="{ minRows: 5, maxRows: 10 }"></a-textarea>
<!-- textarea在内容很多的时候>300KB会卡顿ace不会-->
<ace-editor v-model:value="docEdit.jsonContent" lang="json" theme="monokai" width="100%" height="100" :options="aceEditorConfig"></ace-editor>
<!-- <a-textarea placeholder="请输入JSON格式的Swagger文档内容" v-model:value="docEdit.jsonContent" :auto-size="{ minRows: 5, maxRows: 10 }"></a-textarea>-->
<template #extra>
查看文档内容
<a-popover title="文档内容说明">
@@ -119,8 +119,8 @@
</template>
</a-form-item>
<a-form-item label="文档内容" required name="jsonContent" v-else-if="docEdit.docType === 4">
<!-- <ace-editor v-model:value="docEdit.jsonContent" lang="json" theme="monokai" width="100%" height="100" :options="aceEditorConfig"></ace-editor>-->
<a-textarea placeholder="请输入JSON格式的OpenApi文档内容" v-model:value="docEdit.jsonContent" :auto-size="{ minRows: 5, maxRows: 10 }"></a-textarea>
<ace-editor v-model:value="docEdit.jsonContent" lang="json" theme="monokai" width="100%" height="100" :options="aceEditorConfig"></ace-editor>
<!-- <a-textarea placeholder="请输入JSON格式的OpenApi文档内容" v-model:value="docEdit.jsonContent" :auto-size="{ minRows: 5, maxRows: 10 }"></a-textarea>-->
<template #extra>
查看文档内容
<a-popover title="文档内容说明">
@@ -150,10 +150,10 @@
<a-form-item label="开放访问" required name="openVisit">
<a-radio-group v-model:value="docEdit.openVisit">
<a-radio :value="0"></a-radio>
<a-radio :value="1">开放</a-radio>
<a-radio :value="1">开放访问</a-radio>
</a-radio-group>
<template #extra>
开放访问后无需登录即可通过<a @click="openShareViewWindow(docEdit)">开放文档URL</a>访问文档信息
开放访问后无需登录即可通过<a @click="openShareViewWindow(docEdit)">开放文档URL</a>访问文档信息
</template>
</a-form-item>
<a-form-item label="状态" required name="docStatus">
@@ -240,17 +240,16 @@
});
};
const deleteDoc = async (row) => updateDoc(row.id, null, 0);
// 打开开放文档新窗口
const openShareViewWindow = record => {
if (!record.shareUuid) {
message.warning('请先保存文档后再试');
} else if (record.openVisit === 1) {
window.open(getZyplayerApiBaseUrl() + '/doc-api#/share/home?uuid=' + record.shareUuid);
} else if (record.openVisit !== 1) {
message.warning('该文档尚未开启开放访问功能,请在编辑页选择开放后再试');
} else {
message.warning('改文档尚未开启开放访问功能,请在编辑页选择开放后再试');
window.open(getZyplayerApiBaseUrl() + '/doc-api#/share/home?uuid=' + record.shareUuid);
}
};
const handleActionMenuClick = (item, record) => {
if (item.key === 'shareView') {
openShareViewWindow(record);

View File

@@ -9,9 +9,9 @@
</a-tooltip>
</template>
<mavon-editor ref="mavonEditor" v-model="shareInstruction" :toolbars="toolbars"
:externalLink="false"
:externalLink="false" @imgAdd="addMarkdownImage" :imageFilter="imageFilter"
style="height: 100%;"
placeholder="请录入文档内容"/>
placeholder="请录入开放文档说明"/>
</a-modal>
</template>
@@ -39,6 +39,7 @@
}
zyplayerApi.apiDocUpdate({id: docEdit.value.id, shareInstruction: shareInstruction.value}).then(res => {
editShareInstructionVisible.value = false;
message.success('保存成功!');
});
};
const editDoc = async (id) => {
@@ -48,6 +49,12 @@
shareInstruction.value = res.data.shareInstruction;
});
};
const addMarkdownImage = (pos, file) => {
};
const imageFilter = (pos, file) => {
message.error('暂不支持图片上传');
return false;
};
onMounted(() => {
});
return {
@@ -56,6 +63,8 @@
shareInstruction,
handleNewDocOk,
editDoc,
imageFilter,
addMarkdownImage,
toolbars: {
bold: true, // 粗体
italic: true, // 斜体
@@ -69,7 +78,7 @@
ol: true, // 有序列表
ul: true, // 无序列表
link: true, // 链接
imagelink: true, // 图片链接
imagelink: false, // 图片链接
code: true, // code
table: true, // 表格
fullscreen: true, // 全屏编辑

View File

@@ -1,34 +1,70 @@
<template>
<div v-if="apiDoc.shareInstruction">
<div class="markdown-body" v-html="markdownToHtml(apiDoc.shareInstruction)" style="margin: 0 auto;max-width: 1000px;"></div>
<a-row>
<a-col :xs="0" :sm="4" :md="4" :lg="6" :xl="6" v-if="navigationList.length > 0">
<Navigation ref="navigationRef" :heading="navigationList"></Navigation>
</a-col>
<a-col :xs="24" :sm="navigationList.length > 0?20:24" :md="navigationList.length > 0?20:24" :lg="navigationList.length > 0?18:24" :xl="navigationList.length > 0?18:24">
<div class="markdown-body share-instruction" v-html="markdownToHtml(apiDoc.shareInstruction)" style="margin: 0 auto;max-width: 1000px;"></div>
</a-col>
</a-row>
</div>
<div v-else style="text-align: center;">欢迎访问开放API文档</div>
</template>
<script>
import {toRefs, ref, reactive, onMounted, watch, computed} from 'vue';
import { useRouter, useRoute } from "vue-router";
import {computed, onMounted, ref, watch} from 'vue';
import {useStore} from 'vuex';
import { message } from 'ant-design-vue';
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import {markdownIt} from 'mavon-editor'
import 'mavon-editor/dist/markdown/github-markdown.min.css'
import 'mavon-editor/dist/css/index.css'
import swaggerAnalysis from "../../assets/core/SwaggerAnalysis";
import Navigation from './components/Navigation.vue'
export default {
components: {Navigation},
setup() {
const store = useStore();
const apiDoc = computed(() => store.state.apiDoc);
onMounted(() => {
let navigationRef = ref();
watch(store.getters.getApiDoc, () => {
setTimeout(() => {
createNavigationHeading('.share-instruction');
}, 100);
});
const markdownToHtml = desc => {
return markdownIt.render(desc || '');
}
let navigationList = ref([]);
const createNavigationHeading = (domClass) => {
if (!document.querySelector(domClass)) {
return [];
}
let headNodeArr = document.querySelector(domClass).querySelectorAll('h1,h2,h3,h4,h5,h6');
if (headNodeArr.length <= 0) {
return [];
}
let headArr = [];
headNodeArr.forEach(node => {
let text = node.innerHTML.replace(/^\s+/g, '').replace(/\s+$/g, '').replace(/<\/?[^>]+(>|$)/g, '');
headArr.push({
node: node,
level: parseInt(node.tagName.replace(/[h]/i, ''), 10),
text: text
});
});
navigationList.value = headArr;
};
onMounted(() => {
});
return {
apiDoc,
navigationRef,
navigationList,
markdownToHtml,
};
},
};
</script>
<style>
.share-instruction{padding: 0 10px;}
</style>

View File

@@ -0,0 +1,106 @@
<template>
<div class="navigation">
<div ref="navigationRef" style="display: inline-block;width: 100%;height: 1px;"></div>
<div class="navigation-heading" :style="{width: navigationWidth}">
<div v-for="item in heading" :class="'heading-item heading-'+item.level" @click="headingItemClick(item)">
{{item.text}}
</div>
</div>
</div>
</template>
<script>
import {computed, onMounted, ref, watch} from 'vue';
import {useStore} from 'vuex';
export default {
props: {
heading: {
type: Array,
default: []
},
},
setup(props) {
const store = useStore();
let navigationWidth = ref('100px');
onMounted(() => {
window.onresize = () => {
computeNavigationWidth();
}
setTimeout(() => {
computeNavigationWidth();
}, 100);
});
watch(store.getters.getLeftAsideWidth, () => {
computeNavigationWidth();
});
let navigationRef = ref();
const computeNavigationWidth = () => {
navigationWidth.value = window.getComputedStyle(navigationRef.value, null).width;
};
// 滚动到指定节点
const headingItemClick = (item) => {
item.node.scrollIntoView({behavior: "smooth", block: "start", inline: "nearest"});
};
return {
navigationRef,
navigationWidth,
headingItemClick,
};
},
};
</script>
<style>
.navigation {
width: 100%;
}
.navigation-heading {
position: fixed;
z-index: 4;
top: 150px;
height: calc(100vh - 250px);
width: 100%;
overflow-y: auto;
padding-left: 16px;
box-sizing: border-box;
}
.navigation-heading .heading-item {
padding: 5px 0;
cursor: pointer;
color: #646a73;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.navigation-heading .heading-item:hover {
color: #3370ff;
}
.navigation-heading .heading-1 {
padding-left: 0;
}
.navigation-heading .heading-2 {
padding-left: 16px;
}
.navigation-heading .heading-3 {
padding-left: 32px;
}
.navigation-heading .heading-4 {
padding-left: 48px;
}
.navigation-heading .heading-5 {
padding-left: 64px;
}
.navigation-heading .heading-6 {
padding-left: 80px;
}
</style>