1.后台新增移动端顶级接口文档,md格式显示
This commit is contained in:
@@ -85,6 +85,12 @@ export default {
|
||||
'Newly added tasks will never start because they are blocked by failed tasks!(Web terminal)',
|
||||
'Failed to modify the source command, Please try again manually': 'Failed to modify the source command. Please try again manually.',
|
||||
},
|
||||
docs36ziMobileApi: {
|
||||
download: 'Download Markdown',
|
||||
load_fail: 'Failed to load document',
|
||||
doc_title: '36 Zihua Mobile API Design Draft',
|
||||
nav_outline: 'Outline',
|
||||
},
|
||||
vite: {
|
||||
Later: '稍后',
|
||||
'Restart hot update': '重启热更新',
|
||||
|
||||
@@ -85,6 +85,12 @@ export default {
|
||||
'Newly added tasks will never start because they are blocked by failed tasks': '新添加的任务永远不会开始,因为被失败的任务阻塞!(WEB终端)',
|
||||
'Failed to modify the source command, Please try again manually': '修改源的命令执行失败,请手动重试。',
|
||||
},
|
||||
docs36ziMobileApi: {
|
||||
download: '下载 Markdown',
|
||||
load_fail: '文档加载失败',
|
||||
doc_title: '36字花移动端接口设计草案',
|
||||
nav_outline: '目录',
|
||||
},
|
||||
vite: {
|
||||
Later: '稍后',
|
||||
'Restart hot update': '重启热更新',
|
||||
|
||||
279
web/src/views/backend/docs/doc36ZiHuaMobileApi/index.vue
Normal file
279
web/src/views/backend/docs/doc36ZiHuaMobileApi/index.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<div class="default-main doc-md-page" v-loading="loading">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="doc-md-header">
|
||||
<span class="doc-md-title">{{ pageTitle }}</span>
|
||||
<el-button type="primary" :loading="downloading" @click="onDownload">{{ t('docs36ziMobileApi.download') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="loadError" class="color-danger">{{ loadError }}</div>
|
||||
<el-row v-else :gutter="16" class="doc-md-row">
|
||||
<el-col v-if="toc.length" :xs="24" :sm="8" :md="7" :lg="6" class="doc-md-nav-col">
|
||||
<el-affix :offset="88">
|
||||
<div class="doc-nav-panel">
|
||||
<div class="doc-nav-title">{{ t('docs36ziMobileApi.nav_outline') }}</div>
|
||||
<el-scrollbar max-height="calc(100vh - 200px)">
|
||||
<ul class="doc-nav-list">
|
||||
<li
|
||||
v-for="item in toc"
|
||||
:key="item.id"
|
||||
class="doc-nav-item"
|
||||
:class="'doc-nav-level-' + item.level"
|
||||
>
|
||||
<a href="#" class="doc-nav-link" @click.prevent="scrollToHeading(item.id)">{{
|
||||
item.text
|
||||
}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-affix>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="toc.length ? 16 : 24" :md="toc.length ? 17 : 24" :lg="toc.length ? 18 : 24">
|
||||
<div ref="bodyRef" class="ba-markdown doc-md-body" v-html="html"></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { marked } from 'marked'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getUrl } from '/@/utils/axios'
|
||||
import createAxios from '/@/utils/axios'
|
||||
import { useAdminInfo } from '/@/stores/adminInfo'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
|
||||
const { t } = useI18n()
|
||||
const adminInfo = useAdminInfo()
|
||||
const config = useConfig()
|
||||
|
||||
const bodyRef = ref<HTMLElement | null>(null)
|
||||
const loading = ref(true)
|
||||
const downloading = ref(false)
|
||||
const markdown = ref('')
|
||||
const loadError = ref('')
|
||||
const downloadFilename = ref('36字花-移动端接口设计草案.md')
|
||||
|
||||
marked.use({ breaks: true })
|
||||
|
||||
interface TocItem {
|
||||
level: number
|
||||
text: string
|
||||
id: string
|
||||
}
|
||||
|
||||
const toc = computed((): TocItem[] => {
|
||||
const md = markdown.value
|
||||
if (!md) {
|
||||
return []
|
||||
}
|
||||
const lines = md.split(/\r?\n/)
|
||||
const out: TocItem[] = []
|
||||
let idx = 0
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
const m = /^(#{1,4})\s+(.+)$/.exec(trimmed)
|
||||
if (!m) {
|
||||
continue
|
||||
}
|
||||
const level = m[1].length
|
||||
let text = m[2].trim()
|
||||
text = text.replace(/\s+#+\s*$/, '').trim()
|
||||
out.push({ level, text, id: 'md-nav-' + String(idx) })
|
||||
idx += 1
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
const html = computed(() => {
|
||||
if (!markdown.value) {
|
||||
return ''
|
||||
}
|
||||
return String(marked.parse(markdown.value))
|
||||
})
|
||||
|
||||
const pageTitle = computed(() => t('docs36ziMobileApi.doc_title'))
|
||||
|
||||
function applyHeadingIds() {
|
||||
const root = bodyRef.value
|
||||
const items = toc.value
|
||||
if (!root || !items.length) {
|
||||
return
|
||||
}
|
||||
const hs = root.querySelectorAll('h1, h2, h3, h4')
|
||||
const n = Math.min(items.length, hs.length)
|
||||
for (let i = 0; i < n; i++) {
|
||||
const el = hs.item(i)
|
||||
el.id = items[i].id
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [html.value, markdown.value],
|
||||
() => {
|
||||
void nextTick(() => {
|
||||
applyHeadingIds()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
async function loadContent() {
|
||||
loading.value = true
|
||||
loadError.value = ''
|
||||
try {
|
||||
const res = await createAxios(
|
||||
{
|
||||
url: '/admin/docs.Doc36ZiHuaMobileApi/content',
|
||||
method: 'get',
|
||||
},
|
||||
{ loading: false }
|
||||
)
|
||||
if (res.code === 1 && res.data && typeof res.data.markdown === 'string') {
|
||||
markdown.value = res.data.markdown
|
||||
if (typeof res.data.filename === 'string' && res.data.filename) {
|
||||
downloadFilename.value = res.data.filename
|
||||
}
|
||||
} else {
|
||||
loadError.value = res.msg || t('docs36ziMobileApi.load_fail')
|
||||
}
|
||||
} catch {
|
||||
loadError.value = t('docs36ziMobileApi.load_fail')
|
||||
} finally {
|
||||
loading.value = false
|
||||
void nextTick(() => {
|
||||
applyHeadingIds()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToHeading(id: string) {
|
||||
const el = document.getElementById(id)
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
}
|
||||
|
||||
function triggerBlobDownload(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
a.style.display = 'none'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
async function onDownload() {
|
||||
downloading.value = true
|
||||
try {
|
||||
const token = adminInfo.getToken()
|
||||
const fullUrl = getUrl() + '/admin/docs.Doc36ZiHuaMobileApi/download'
|
||||
const response = await fetch(fullUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
batoken: token,
|
||||
server: 'true',
|
||||
'think-lang': config.lang.defaultLang,
|
||||
},
|
||||
})
|
||||
if (!response.ok) {
|
||||
loadError.value = t('docs36ziMobileApi.load_fail')
|
||||
return
|
||||
}
|
||||
const ctype = response.headers.get('content-type') || ''
|
||||
if (ctype.includes('application/json')) {
|
||||
const j = await response.json()
|
||||
loadError.value = typeof j.msg === 'string' && j.msg ? j.msg : t('docs36ziMobileApi.load_fail')
|
||||
return
|
||||
}
|
||||
const blob = await response.blob()
|
||||
triggerBlobDownload(blob, downloadFilename.value)
|
||||
} catch {
|
||||
loadError.value = t('docs36ziMobileApi.load_fail')
|
||||
} finally {
|
||||
downloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadContent()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.doc-md-page {
|
||||
.doc-md-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.doc-md-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
.doc-md-row {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.doc-md-nav-col {
|
||||
margin-bottom: 16px;
|
||||
@media (min-width: 768px) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.doc-nav-panel {
|
||||
padding: 12px 12px 8px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
background: var(--ba-bg-color-overlay);
|
||||
}
|
||||
.doc-nav-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.doc-nav-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 0 8px;
|
||||
}
|
||||
.doc-nav-item {
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.doc-nav-level-1 {
|
||||
padding-left: 0;
|
||||
}
|
||||
.doc-nav-level-2 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
.doc-nav-level-3 {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.doc-nav-level-4 {
|
||||
padding-left: 30px;
|
||||
}
|
||||
.doc-nav-link {
|
||||
color: var(--el-text-color-regular);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
word-break: break-word;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.doc-md-body {
|
||||
max-width: 1100px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user