1.后台新增移动端顶级接口文档,md格式显示
This commit is contained in:
77
app/admin/controller/docs/Doc36ZiHuaMobileApi.php
Normal file
77
app/admin/controller/docs/Doc36ZiHuaMobileApi.php
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\admin\controller\docs;
|
||||||
|
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use support\Response;
|
||||||
|
use Webman\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台只读展示《36字花-移动端接口设计草案》Markdown,并提供下载。
|
||||||
|
*/
|
||||||
|
class Doc36ZiHuaMobileApi extends Backend
|
||||||
|
{
|
||||||
|
private const DOC_RELATIVE = 'docs' . DIRECTORY_SEPARATOR . '36字花-移动端接口设计草案.md';
|
||||||
|
|
||||||
|
public function content(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $this->docAbsolutePath();
|
||||||
|
if (!is_file($path)) {
|
||||||
|
return $this->error(__('Document file not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = file_get_contents($path);
|
||||||
|
if ($raw === false) {
|
||||||
|
return $this->error(__('Failed to read document'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success('', [
|
||||||
|
'markdown' => $raw,
|
||||||
|
'filename' => '36字花-移动端接口设计草案.md',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function download(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeBackend($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $this->docAbsolutePath();
|
||||||
|
if (!is_file($path)) {
|
||||||
|
return $this->error(__('Document file not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = file_get_contents($path);
|
||||||
|
if ($body === false) {
|
||||||
|
return $this->error(__('Failed to read document'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$utf8Name = '36字花-移动端接口设计草案.md';
|
||||||
|
$asciiFallback = '36zihua-mobile-api-design-draft.md';
|
||||||
|
$disposition = sprintf(
|
||||||
|
'attachment; filename="%s"; filename*=UTF-8\'\'%s',
|
||||||
|
$asciiFallback,
|
||||||
|
rawurlencode($utf8Name)
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Response(200, [
|
||||||
|
'Content-Type' => 'text/markdown; charset=UTF-8',
|
||||||
|
'Content-Disposition' => $disposition,
|
||||||
|
'Cache-Control' => 'private, max-age=0, must-revalidate',
|
||||||
|
], $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function docAbsolutePath(): string
|
||||||
|
{
|
||||||
|
return rtrim(base_path(), DIRECTORY_SEPARATOR . '/') . DIRECTORY_SEPARATOR . self::DOC_RELATIVE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,4 +96,6 @@ return [
|
|||||||
'Please input correct username' => 'Please enter the correct username',
|
'Please input correct username' => 'Please enter the correct username',
|
||||||
'Please enter a valid commission rate for non-top role group' => 'Non-top role groups require a commission rate between 0 and 100 (%)',
|
'Please enter a valid commission rate for non-top role group' => 'Non-top role groups require a commission rate between 0 and 100 (%)',
|
||||||
'Group Name Arr' => 'Group Name Arr',
|
'Group Name Arr' => 'Group Name Arr',
|
||||||
|
'Document file not found' => 'Document file not found',
|
||||||
|
'Failed to read document' => 'Failed to read document',
|
||||||
];
|
];
|
||||||
@@ -115,4 +115,6 @@ return [
|
|||||||
'Please input correct username' => '请输入正确的用户名',
|
'Please input correct username' => '请输入正确的用户名',
|
||||||
'Please enter a valid commission rate for non-top role group' => '非顶级角色组须填写 0~100 的分红比例(%)',
|
'Please enter a valid commission rate for non-top role group' => '非顶级角色组须填写 0~100 的分红比例(%)',
|
||||||
'Group Name Arr' => '分组名称数组',
|
'Group Name Arr' => '分组名称数组',
|
||||||
|
'Document file not found' => '文档文件不存在',
|
||||||
|
'Failed to read document' => '读取文档失败',
|
||||||
];
|
];
|
||||||
@@ -124,4 +124,12 @@ return [
|
|||||||
// 其它中文按钮文案
|
// 其它中文按钮文案
|
||||||
'期号开关' => 'Period toggle',
|
'期号开关' => 'Period toggle',
|
||||||
'手动创建下一期' => 'Create next period manually',
|
'手动创建下一期' => 'Create next period manually',
|
||||||
|
|
||||||
|
// Markdown 文档目录
|
||||||
|
'Markdown文档' => 'Markdown documents',
|
||||||
|
|
||||||
|
// 文档:36字花移动端接口草案
|
||||||
|
'36字花移动端接口文档' => '36 Zihua mobile API design draft',
|
||||||
|
'拉取正文' => 'Load document body',
|
||||||
|
'下载 Markdown' => 'Download Markdown',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"element-plus": "2.9.1",
|
"element-plus": "2.9.1",
|
||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
"lodash-es": "4.17.21",
|
"lodash-es": "4.17.21",
|
||||||
|
"marked": "9.1.6",
|
||||||
"mitt": "3.0.1",
|
"mitt": "3.0.1",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"pinia": "2.3.0",
|
"pinia": "2.3.0",
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ export default {
|
|||||||
'Newly added tasks will never start because they are blocked by failed tasks!(Web terminal)',
|
'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.',
|
'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: {
|
vite: {
|
||||||
Later: '稍后',
|
Later: '稍后',
|
||||||
'Restart hot update': '重启热更新',
|
'Restart hot update': '重启热更新',
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ export default {
|
|||||||
'Newly added tasks will never start because they are blocked by failed tasks': '新添加的任务永远不会开始,因为被失败的任务阻塞!(WEB终端)',
|
'Newly added tasks will never start because they are blocked by failed tasks': '新添加的任务永远不会开始,因为被失败的任务阻塞!(WEB终端)',
|
||||||
'Failed to modify the source command, Please try again manually': '修改源的命令执行失败,请手动重试。',
|
'Failed to modify the source command, Please try again manually': '修改源的命令执行失败,请手动重试。',
|
||||||
},
|
},
|
||||||
|
docs36ziMobileApi: {
|
||||||
|
download: '下载 Markdown',
|
||||||
|
load_fail: '文档加载失败',
|
||||||
|
doc_title: '36字花移动端接口设计草案',
|
||||||
|
nav_outline: '目录',
|
||||||
|
},
|
||||||
vite: {
|
vite: {
|
||||||
Later: '稍后',
|
Later: '稍后',
|
||||||
'Restart hot update': '重启热更新',
|
'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