1.新增显示分红说明文档菜单

2.文档新增英文版
This commit is contained in:
2026-05-29 11:18:10 +08:00
parent f3677eb0e3
commit 9be9e2666b
13 changed files with 1669 additions and 17 deletions

View File

@@ -91,6 +91,12 @@ export default {
doc_title: '36 Zihua Mobile API Design Draft',
nav_outline: 'Outline',
},
docsCommissionShare: {
download: 'Download Markdown',
load_fail: 'Failed to load document',
doc_title: 'Commission Share Guide',
nav_outline: 'Outline',
},
vite: {
Later: '稍后',
'Restart hot update': '重启热更新',

View File

@@ -91,6 +91,12 @@ export default {
doc_title: '36字花移动端接口设计草案',
nav_outline: '目录',
},
docsCommissionShare: {
download: '下载 Markdown',
load_fail: '文档加载失败',
doc_title: '分红说明文档',
nav_outline: '目录',
},
vite: {
Later: '稍后',
'Restart hot update': '重启热更新',

View File

@@ -204,6 +204,13 @@ async function onDownload() {
onMounted(() => {
void loadContent()
})
watch(
() => config.lang.defaultLang,
() => {
void loadContent()
}
)
</script>
<style scoped lang="scss">

View File

@@ -0,0 +1,290 @@
<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('docsCommissionShare.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('docsCommissionShare.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'
defineOptions({
name: 'docs/docCommissionShare',
})
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('分红说明文档.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('docsCommissionShare.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.DocCommissionShare/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('docsCommissionShare.load_fail')
}
} catch {
loadError.value = t('docsCommissionShare.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.DocCommissionShare/download'
const response = await fetch(fullUrl, {
method: 'GET',
headers: {
batoken: token,
server: 'true',
'think-lang': config.lang.defaultLang,
},
})
if (!response.ok) {
loadError.value = t('docsCommissionShare.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('docsCommissionShare.load_fail')
return
}
const blob = await response.blob()
triggerBlobDownload(blob, downloadFilename.value)
} catch {
loadError.value = t('docsCommissionShare.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>