This commit is contained in:
iqudoo
2026-05-25 11:42:34 +08:00
parent a74a20049a
commit 4ebe673885

View File

@@ -1,21 +1,14 @@
<template>
<el-container
:class="{ 'mobile-mode': isMobile }"
v-loading="loading"
element-loading-text="正在加载接口列表..."
element-loading-background="rgba(255, 255, 255, 1)"
>
<el-container :class="{ 'mobile-mode': isMobile }" v-loading="loading" element-loading-text="正在加载接口列表..."
element-loading-background="rgba(255, 255, 255, 1)">
<el-header v-if="isMobile" class="mobile-header">
<el-button :icon="Menu" circle text class="menu-toggle-btn" @click="toggleMenu" />
<h2>API 文档中心</h2>
<span class="mobile-header-spacer" />
</el-header>
<el-aside
:width="isMobile ? '100%' : '280px'"
class="sidebar"
v-show="(!isMobile || (isMobile && showMenu)) && !loadError"
>
<el-aside :width="isMobile ? '100%' : '280px'" class="sidebar"
v-show="(!isMobile || (isMobile && showMenu)) && !loadError">
<div class="sidebar-inner">
<div class="sidebar-header">
<div class="sidebar-brand">
@@ -26,24 +19,12 @@
</span>
</div>
</div>
<el-button
v-if="isMobile"
:icon="Close"
circle
text
class="sidebar-close-btn"
@click="toggleMenu"
/>
<el-button v-if="isMobile" :icon="Close" circle text class="sidebar-close-btn" @click="toggleMenu" />
</div>
<div class="sidebar-search">
<el-input
placeholder="搜索接口..."
v-model="searchQuery"
clearable
:prefix-icon="Search"
:disabled="loading || !!loadError"
/>
<el-input placeholder="搜索接口..." v-model="searchQuery" clearable :prefix-icon="Search"
:disabled="loading || !!loadError" />
</div>
<div class="sidebar-content">
@@ -68,95 +49,69 @@
<div class="breadcrumb-path">
<span class="breadcrumb-tag breadcrumb-clickable" @click="goHome">根目录</span>
<template v-for="(path, index) in currentPath" :key="index">
<el-icon class="breadcrumb-chevron"><ArrowRight /></el-icon>
<span
class="breadcrumb-tag"
:class="{
<el-icon class="breadcrumb-chevron">
<ArrowRight />
</el-icon>
<span class="breadcrumb-tag" :class="{
'breadcrumb-clickable': index < currentPath.length - 1,
'breadcrumb-current': index === currentPath.length - 1,
}"
@click="index < currentPath.length - 1 && navigateToPath(index)"
>
}" @click="index < currentPath.length - 1 && navigateToPath(index)">
{{ path }}
</span>
</template>
</div>
</div>
<el-empty
v-if="
<el-empty v-if="
!loading &&
!loadError &&
currentLevelNodes.length === 0 &&
currentLevelApis.length === 0
"
description="没有找到相关接口"
:image-size="64"
class="sidebar-empty"
/>
" description="没有找到相关接口" :image-size="64" class="sidebar-empty" />
<el-menu
v-else
class="menu-vertical"
:default-active="activeMenuIndex"
>
<el-menu v-else class="menu-vertical" :default-active="activeMenuIndex">
<template v-for="node in currentLevelNodes" :key="node.key">
<el-menu-item
v-if="
<el-menu-item v-if="
!node.isUngrouped && (node.children.length > 0 || node.apis.length > 0)
"
:index="node.key"
@click="enterGroup(node)"
class="menu-folder-item"
:disabled="loading"
>
" :index="node.key" @click="enterGroup(node)" class="menu-folder-item" :disabled="loading">
<div class="menu-item-inner">
<el-icon class="menu-item-icon folder"><Folder /></el-icon>
<el-icon class="menu-item-icon folder">
<Folder />
</el-icon>
<span class="menu-item-label">{{ node.displayName }}</span>
<span class="menu-item-count">{{ getNodeApiCount(node) }}</span>
<el-icon class="menu-item-arrow"><ArrowRight /></el-icon>
<el-icon class="menu-item-arrow">
<ArrowRight />
</el-icon>
</div>
</el-menu-item>
<el-menu-item
v-else-if="node.isUngrouped"
:index="node.key"
@click="
<el-menu-item v-else-if="node.isUngrouped" :index="node.key" @click="
goToApi(node.apis[0]);
isMobile && toggleMenu();
"
class="menu-api-item"
:disabled="loading"
>
" class="menu-api-item" :disabled="loading">
<div class="menu-item-inner">
<el-icon class="menu-item-icon api"><Document /></el-icon>
<el-icon class="menu-item-icon api">
<Document />
</el-icon>
<span class="menu-item-label">{{ node.displayName }}</span>
</div>
</el-menu-item>
</template>
<template v-if="currentLevelApis.length > 0">
<el-menu-item
v-if="currentLevelNodes.length > 0"
index="__section_apis__"
disabled
class="menu-section-item"
>
<el-menu-item v-if="currentLevelNodes.length > 0" index="__section_apis__" disabled
class="menu-section-item">
<span class="menu-section-label">接口</span>
</el-menu-item>
<el-menu-item
v-for="api in currentLevelApis"
:key="api.uniqueId"
:index="api.uniqueId"
@click="
<el-menu-item v-for="api in currentLevelApis" :key="api.uniqueId" :index="api.uniqueId" @click="
goToApi(api);
isMobile && toggleMenu();
"
class="menu-api-item"
:disabled="loading"
>
" class="menu-api-item" :disabled="loading">
<div class="menu-item-inner">
<el-icon class="menu-item-icon api"><Document /></el-icon>
<el-icon class="menu-item-icon api">
<Document />
</el-icon>
<span class="menu-item-label">{{
getCurrentNodeApiDisplayName(api)
}}</span>
@@ -184,28 +139,16 @@
<div class="content-nav-wrap">
<div class="content-section content-nav-bar">
<div class="content-nav-tabs">
<button
type="button"
class="content-nav-tab"
:class="{ active: activeTab === 'docs' }"
@click="activeTab = 'docs'"
>
<button type="button" class="content-nav-tab" :class="{ active: activeTab === 'docs' }"
@click="activeTab = 'docs'">
接口文档
</button>
<button
type="button"
class="content-nav-tab"
:class="{ active: activeTab === 'common' }"
@click="activeTab = 'common'"
>
<button type="button" class="content-nav-tab" :class="{ active: activeTab === 'common' }"
@click="activeTab = 'common'">
通用信息
</button>
</div>
<el-button
type="primary"
:icon="Download"
@click="showExportDrawer = true"
>
<el-button type="primary" :icon="Download" @click="showExportDrawer = true">
导出文档
</el-button>
</div>
@@ -217,7 +160,9 @@
<div v-if="!currentApi" class="content-page readme-guide">
<div class="content-section page-header-card page-header-card-center">
<div class="page-header-main">
<el-icon class="page-hero-icon"><Document /></el-icon>
<el-icon class="page-hero-icon">
<Document />
</el-icon>
<div>
<h1 class="page-hero-title">接口文档中心</h1>
<p class="page-hero-desc">
@@ -283,10 +228,7 @@
</div>
</div>
<div
v-if="currentApi"
class="content-page api-detail"
>
<div v-if="currentApi" class="content-page api-detail">
<div class="content-section page-header-card">
<div class="page-header-main">
<div class="page-header-text">
@@ -294,25 +236,14 @@
<h1 class="api-action-name">{{ currentApi.action }}</h1>
</div>
</div>
<el-button
type="primary"
plain
size="small"
:icon="DocumentCopy"
:disabled="apiDetailLoading || !apiDetail"
@click="copyApiMarkdown"
>
<el-button type="primary" plain size="small" :icon="DocumentCopy"
:disabled="apiDetailLoading || !apiDetail" @click="copyApiMarkdown">
复制文档
</el-button>
</div>
<div v-if="apiDetailError" class="api-detail-alert">
<el-alert
:title="apiDetailError"
type="warning"
:closable="false"
show-icon
/>
<el-alert :title="apiDetailError" type="warning" :closable="false" show-icon />
</div>
<div class="api-detail-body">
@@ -380,12 +311,10 @@
<p v-if="apiDetail.paramModel.basicProperty" class="basic-type-display">
类型:{{ getFullTypeName(apiDetail.paramModel) }}
</p>
<div
v-for="model in getAllModels(apiDetail.paramModel)"
:key="model.simpleName"
class="model-card"
>
<h4 :id="'model-' + model.simpleName" class="model-title">{{ getModelDisplayName(model) }}</h4>
<div v-for="model in getAllModels(apiDetail.paramModel)" :key="model.simpleName"
class="model-card">
<h4 :id="'model-' + model.simpleName" class="model-title">{{ getModelDisplayName(model) }}
</h4>
<!-- 桌面端表格布局 -->
<el-table v-if="!isMobile" :data="model.fields" class="data-table">
@@ -403,20 +332,14 @@
{{ getTypeName(scope.row) }}
</template>
&lt;
<template
v-for="(param, index) in getGenericParams(scope.row)"
:key="index"
>
<template v-for="(param, index) in getGenericParams(scope.row)" :key="index">
<template v-if="param.isObject">
<a :href="'#model-' + param.name">{{ param.name }}</a>
</template>
<template v-else>
{{ param.name }}
</template>
<template
v-if="index < getGenericParams(scope.row).length - 1"
>,</template
>
<template v-if="index < getGenericParams(scope.row).length - 1">,</template>
</template>
&gt;
</template>
@@ -441,35 +364,19 @@
<el-table-column prop="desc" label="说明">
<template #default="scope">
<div>
<div v-if="scope.row.desc">{{ scope.row.desc }}</div>
<div
v-if="scope.row.enumInfo && scope.row.enumInfo.properties"
style="margin-top: 8px"
>
<div
style="
font-weight: bolder;
color: #409eff;
margin-bottom: 4px;
"
>
枚举值:
</div>
<div
v-html="
scope.row.enumInfo.properties.map((item) => {
<span v-if="scope.row.desc" style="margin-right: 8px">{{ scope.row.desc }}</span>
<span v-if="scope.row.enumInfo && scope.row.enumInfo.properties">
<span style="font-weight: bolder;color: #409eff;margin-right: 4px;">
枚举值
</span>
<span v-html="scope.row.enumInfo.properties.map((item) => {
return `${item.desc}(${item.value})`;
})
"
style="color: #606266"
></div>
</div>
<div
v-if="
}).join(', ')" style="color: #606266"></span>
</span>
<div v-if="
!scope.row.desc &&
!(scope.row.enumInfo && scope.row.enumInfo.properties)
"
>
">
-
</div>
</div>
@@ -501,20 +408,14 @@
{{ getTypeName(field) }}
</template>
&lt;
<template
v-for="(param, index) in getGenericParams(field)"
:key="index"
>
<template v-for="(param, index) in getGenericParams(field)" :key="index">
<template v-if="param.isObject">
<a :href="'#model-' + param.name">{{ param.name }}</a>
</template>
<template v-else>
{{ param.name }}
</template>
<template
v-if="index < getGenericParams(field).length - 1"
>,</template
>
<template v-if="index < getGenericParams(field).length - 1">,</template>
</template>
&gt;
</template>
@@ -534,19 +435,12 @@
<div class="field-label">说明:</div>
<div class="field-value">{{ field.desc }}</div>
</div>
<div
class="field-item"
v-if="field.enumInfo && field.enumInfo.properties"
>
<div class="field-item" v-if="field.enumInfo && field.enumInfo.properties">
<div class="field-label">枚举值:</div>
<div class="field-value">
<div
v-html="
field.enumInfo.properties.map((item) => {
<div v-html="field.enumInfo.properties.map((item) => {
return `${item.desc}(${item.value})`;
})
"
></div>
}).join(', ')" style="color: #606266"></div>
</div>
</div>
</div>
@@ -563,12 +457,10 @@
<p v-if="apiDetail.responseModel.basicProperty" class="basic-type-display">
类型:{{ getFullTypeName(apiDetail.responseModel) }}
</p>
<div
v-for="model in getAllModels(apiDetail.responseModel)"
:key="model.simpleName"
class="model-card"
>
<h4 :id="'model-' + model.simpleName" class="model-title">{{ getModelDisplayName(model) }}</h4>
<div v-for="model in getAllModels(apiDetail.responseModel)" :key="model.simpleName"
class="model-card">
<h4 :id="'model-' + model.simpleName" class="model-title">{{ getModelDisplayName(model) }}
</h4>
<!-- 桌面端表格布局 -->
<el-table v-if="!isMobile" :data="model.fields" class="data-table">
<el-table-column prop="name" label="参数名" width="auto" />
@@ -585,20 +477,14 @@
{{ getTypeName(scope.row) }}
</template>
&lt;
<template
v-for="(param, index) in getGenericParams(scope.row)"
:key="index"
>
<template v-for="(param, index) in getGenericParams(scope.row)" :key="index">
<template v-if="param.isObject">
<a :href="'#model-' + param.name">{{ param.name }}</a>
</template>
<template v-else>
{{ param.name }}
</template>
<template
v-if="index < getGenericParams(scope.row).length - 1"
>,</template
>
<template v-if="index < getGenericParams(scope.row).length - 1">,</template>
</template>
&gt;
</template>
@@ -618,35 +504,19 @@
<el-table-column prop="desc" label="说明">
<template #default="scope">
<div>
<div v-if="scope.row.desc">{{ scope.row.desc }}</div>
<div
v-if="scope.row.enumInfo && scope.row.enumInfo.properties"
style="margin-top: 8px"
>
<div
style="
font-weight: bolder;
color: #409eff;
margin-bottom: 4px;
"
>
枚举值:
</div>
<div
v-html="
scope.row.enumInfo.properties.map((item) => {
<span v-if="scope.row.desc" style="margin-right: 8px">{{ scope.row.desc }}</span>
<span v-if="scope.row.enumInfo && scope.row.enumInfo.properties">
<span style="font-weight: bolder;color: #409eff; margin-right: 4px;">
枚举值
</span>
<span v-html="scope.row.enumInfo.properties.map((item) => {
return `${item.desc}(${item.value})`;
})
"
style="color: #606266"
></div>
</div>
<div
v-if="
}).join(', ')" style="color: #606266"></span>
</span>
<div v-if="
!scope.row.desc &&
!(scope.row.enumInfo && scope.row.enumInfo.properties)
"
>
">
-
</div>
</div>
@@ -677,20 +547,14 @@
{{ getTypeName(field) }}
</template>
&lt;
<template
v-for="(param, index) in getGenericParams(field)"
:key="index"
>
<template v-for="(param, index) in getGenericParams(field)" :key="index">
<template v-if="param.isObject">
<a :href="'#model-' + param.name">{{ param.name }}</a>
</template>
<template v-else>
{{ param.name }}
</template>
<template
v-if="index < getGenericParams(field).length - 1"
>,</template
>
<template v-if="index < getGenericParams(field).length - 1">,</template>
</template>
&gt;
</template>
@@ -710,19 +574,13 @@
<div class="field-label">说明:</div>
<div class="field-value">{{ field.desc }}</div>
</div>
<div
class="field-item"
v-if="field.enumInfo && field.enumInfo.properties"
>
<div class="field-item" v-if="field.enumInfo && field.enumInfo.properties">
<div class="field-label">枚举值:</div>
<div class="field-value">
<div
v-html="
field.enumInfo.properties.map((item) => {
<div v-html="field.enumInfo.properties.map((item) => {
return `${item.desc}(${item.value})`;
})
"
></div>
"></div>
</div>
</div>
</div>
@@ -763,11 +621,7 @@
<section class="content-section">
<h3 class="section-title">通用请求参数</h3>
<el-table
:data="commonParams"
class="data-table"
:class="{ 'responsive-table': isMobile }"
>
<el-table :data="commonParams" class="data-table" :class="{ 'responsive-table': isMobile }">
<el-table-column prop="name" label="参数名" width="auto" />
<el-table-column prop="required" label="必填" width="auto" />
<el-table-column prop="desc" label="说明" />
@@ -776,11 +630,7 @@
<section class="content-section">
<h3 class="section-title">通用响应格式</h3>
<el-table
:data="commonResponse"
class="data-table"
:class="{ 'responsive-table': isMobile }"
>
<el-table :data="commonResponse" class="data-table" :class="{ 'responsive-table': isMobile }">
<el-table-column prop="name" label="字段名" width="auto" />
<el-table-column prop="desc" label="说明" />
</el-table>
@@ -793,13 +643,7 @@
</el-main>
</el-container>
<el-drawer
v-model="showExportDrawer"
title="导出文档"
direction="rtl"
:size="exportDrawerSize"
class="export-drawer"
>
<el-drawer v-model="showExportDrawer" title="导出文档" direction="rtl" :size="exportDrawerSize" class="export-drawer">
<div class="export-module">
<p class="export-description">
导出接口文档为 Markdown 格式,支持导出全部或按需选择
@@ -811,12 +655,7 @@
</template>
<div class="quick-export-content">
<p class="quick-export-desc">一键导出所有接口文档</p>
<el-button
type="primary"
@click="downloadApiDocumentation"
:loading="downloading"
style="width: 100%"
>
<el-button type="primary" @click="downloadApiDocumentation" :loading="downloading" style="width: 100%">
{{ downloading ? "下载中..." : "下载全部接口文档" }}
</el-button>
</div>
@@ -828,12 +667,7 @@
<p class="section-desc">选择需要导出的接口,生成自定义文档</p>
</div>
<div class="export-select-header">
<el-input
v-model="exportSearchQuery"
placeholder="搜索接口"
clearable
:prefix-icon="Search"
/>
<el-input v-model="exportSearchQuery" placeholder="搜索接口" clearable :prefix-icon="Search" />
<div class="select-actions">
<el-button type="primary" @click="selectAllExportApis">全选</el-button>
<el-button @click="deselectAllExportApis">取消全选</el-button>
@@ -843,33 +677,24 @@
</div>
</div>
<div class="export-api-list">
<el-tree
ref="exportTreeRef"
:data="exportTreeData"
:props="exportTreeProps"
show-checkbox
node-key="id"
:default-expand-all="false"
:filter-node-method="filterExportNode"
@check="handleExportTreeCheck"
>
<el-tree ref="exportTreeRef" :data="exportTreeData" :props="exportTreeProps" show-checkbox node-key="id"
:default-expand-all="false" :filter-node-method="filterExportNode" @check="handleExportTreeCheck">
<template #default="{ node, data }">
<span class="export-tree-node">
<el-icon v-if="data.isApi" class="api-icon"><Document /></el-icon>
<el-icon v-else class="folder-icon"><Folder /></el-icon>
<el-icon v-if="data.isApi" class="api-icon">
<Document />
</el-icon>
<el-icon v-else class="folder-icon">
<Folder />
</el-icon>
<span class="node-label">{{ node.label }}</span>
</span>
</template>
</el-tree>
</div>
<div class="export-actions">
<el-button
type="primary"
@click="handleExportSelected"
:loading="downloading"
:disabled="selectedApiIds.length === 0"
style="width: 100%"
>
<el-button type="primary" @click="handleExportSelected" :loading="downloading"
:disabled="selectedApiIds.length === 0" style="width: 100%">
{{ downloading ? "导出中..." : "导出所选文档" }}
</el-button>
</div>
@@ -1151,7 +976,7 @@ onMounted(async () => {
if (id) {
const apiToShow = findApiById(id);
if (apiToShow) {
showApiDetail(apiToShow);
goToApi(apiToShow);
}
}
}
@@ -1182,7 +1007,7 @@ onMounted(async () => {
if (id) {
const apiToShow = findApiById(id);
if (apiToShow) {
showApiDetail(apiToShow);
goToApi(apiToShow);
}
}
} catch (error) {
@@ -1226,8 +1051,46 @@ const goHome = () => {
updateUrlParameter(null);
};
// 在侧边栏树中查找 API 所在的目录路径
const findApiMenuPath = (api) => {
if (!api) return { pathKeys: [], pathNames: [] };
const search = (nodes, pathKeys, pathNames) => {
for (const node of nodes) {
if (node.apis?.some((item) => item.uniqueId === api.uniqueId)) {
if (node.isUngrouped) {
return { pathKeys: [], pathNames: [] };
}
return {
pathKeys: [...pathKeys, node.key],
pathNames: [...pathNames, node.displayName],
};
}
if (node.children?.length) {
const result = search(
node.children,
[...pathKeys, node.key],
[...pathNames, node.displayName]
);
if (result) return result;
}
}
return null;
};
return search(apiTree.value, [], []) || { pathKeys: [], pathNames: [] };
};
// 将左侧菜单导航到 API 所在目录
const navigateToApiInMenu = (api) => {
const { pathKeys, pathNames } = findApiMenuPath(api);
currentPathKeys.value = pathKeys;
currentPath.value = pathNames;
};
// 路由到指定API
const goToApi = (api) => {
navigateToApiInMenu(api);
showApiDetail(api);
updateUrlParameter(api.uniqueId);
};
@@ -1293,8 +1156,7 @@ const generateCurlExample = (api) => {
-d '{
"action": "${api.action}",
"token": "YOUR_TOKEN",
"params": {${
Object.entries(requiredParams)
"params": {${Object.entries(requiredParams)
.map(([key, value]) => '\n "' + key + '": ' + JSON.stringify(value))
.join(",") || ""
}
@@ -1316,8 +1178,7 @@ const generateJsExample = (api) => {
axios.post('API_URL', {
action: '${api.action}',
token: 'YOUR_TOKEN',
params: {${
Object.entries(requiredParams)
params: {${Object.entries(requiredParams)
.map(([key, value]) => "\n " + key + ": " + JSON.stringify(value))
.join(",") || ""
}
@@ -3089,6 +2950,7 @@ const handleExportSelected = async () => {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
@@ -3159,6 +3021,7 @@ h4 {
}
@media (max-width: 768px) {
.content-nav-wrap,
.content-page {
padding-left: 16px;
@@ -3596,6 +3459,7 @@ h4 {
:deep(.el-table__inner-wrapper:before) {
background-color: transparent;
}
:deep(.el-collapse-item__wrap) {
border-bottom: none;
}