287 lines
8.1 KiB
Vue
287 lines
8.1 KiB
Vue
<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()
|
|
})
|
|
|
|
watch(
|
|
() => config.lang.defaultLang,
|
|
() => {
|
|
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>
|