Files
tape-springboot-docs/src/App.vue
iqudoo 827a0283ba fix
2026-05-24 22:58:52 +08:00

3493 lines
96 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<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"
>
<div class="sidebar-inner">
<div class="sidebar-header">
<div class="sidebar-brand">
<el-icon class="brand-icon"><Document /></el-icon>
<div class="brand-text">
<span class="brand-title">API 文档中心</span>
<span v-if="!loading && !loadError" class="brand-subtitle">
{{ apis.length }} 个接口
</span>
</div>
</div>
<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"
/>
</div>
<div class="sidebar-content">
<div v-if="loadError" class="sidebar-error">
<el-empty description="加载失败" :image-size="80">
<el-button type="primary" size="small" @click="location.reload()">
重新加载
</el-button>
</el-empty>
</div>
<template v-else>
<div v-if="currentPath.length > 0" class="breadcrumb-nav">
<div class="breadcrumb-toolbar">
<el-button link :icon="ArrowLeft" @click="goBack" class="breadcrumb-btn">
返回
</el-button>
<el-button link :icon="HomeFilled" @click="goHome" class="breadcrumb-btn">
根目录
</el-button>
</div>
<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="{
'breadcrumb-clickable': index < currentPath.length - 1,
'breadcrumb-current': index === currentPath.length - 1,
}"
@click="index < currentPath.length - 1 && navigateToPath(index)"
>
{{ path }}
</span>
</template>
</div>
</div>
<el-empty
v-if="
!loading &&
!loadError &&
currentLevelNodes.length === 0 &&
currentLevelApis.length === 0
"
description="没有找到相关接口"
:image-size="64"
class="sidebar-empty"
/>
<el-menu
v-else
class="menu-vertical"
:default-active="activeMenuIndex"
>
<template v-for="node in currentLevelNodes" :key="node.key">
<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"
>
<div class="menu-item-inner">
<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>
</div>
</el-menu-item>
<el-menu-item
v-else-if="node.isUngrouped"
:index="node.key"
@click="
goToApi(node.apis[0]);
isMobile && toggleMenu();
"
class="menu-api-item"
:disabled="loading"
>
<div class="menu-item-inner">
<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"
>
<span class="menu-section-label">接口</span>
</el-menu-item>
<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"
>
<div class="menu-item-inner">
<el-icon class="menu-item-icon api"><Document /></el-icon>
<span class="menu-item-label">{{
getCurrentNodeApiDisplayName(api)
}}</span>
</div>
</el-menu-item>
</template>
</el-menu>
</template>
</div>
</div>
</el-aside>
<el-main class="main-content">
<!-- 接口列表加载失败时全屏显示错误 -->
<div v-if="loadError" class="error-container fullscreen-error">
<el-result icon="error" title="加载失败" :sub-title="loadError">
<template #extra>
<el-button type="primary" @click="location.reload()">重新加载</el-button>
</template>
</el-result>
</div>
<template v-else>
<div class="content-frame">
<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>
<button
type="button"
class="content-nav-tab"
:class="{ active: activeTab === 'common' }"
@click="activeTab = 'common'"
>
通用信息
</button>
</div>
<el-button
type="primary"
plain
size="small"
:icon="Download"
@click="showExportDrawer = true"
>
导出文档
</el-button>
</div>
</div>
<div class="content-frame-body">
<div v-show="activeTab === 'docs'" class="content-tab-panel">
<!-- README引导页 -->
<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>
<div>
<h1 class="page-hero-title">接口文档中心</h1>
<p class="page-hero-desc">
欢迎使用 API 文档中心,这里提供了完整的接口文档和使用说明
</p>
</div>
</div>
</div>
<div class="readme-grid">
<div class="readme-card">
<div class="readme-card-num">01</div>
<h3>浏览接口</h3>
<p>在左侧菜单中浏览所有可用的接口,接口按模块分组显示。</p>
<ul>
<li>点击文件夹进入下一级目录</li>
<li>点击接口名称查看详细文档</li>
<li>使用搜索框快速查找接口</li>
</ul>
</div>
<div class="readme-card">
<div class="readme-card-num">02</div>
<h3>查看接口详情</h3>
<p>点击任意接口后,右侧会显示该接口的详细信息,包括:</p>
<ul>
<li>请求参数说明</li>
<li>响应数据结构</li>
<li>调用示例cURL、JavaScript</li>
</ul>
</div>
<div class="readme-card">
<div class="readme-card-num">03</div>
<h3>导出文档</h3>
<p>点击上方「导出文档」按钮,可以:</p>
<ul>
<li>一键导出所有接口文档</li>
<li>按需选择接口进行导出</li>
<li>导出为 Markdown 格式</li>
</ul>
</div>
<div class="readme-card">
<div class="readme-card-num">04</div>
<h3>通用信息</h3>
<p>切换到「通用信息」标签页,可以查看:</p>
<ul>
<li>通用请求参数说明</li>
<li>通用响应格式说明</li>
</ul>
</div>
</div>
<div class="content-section tips-card">
<div class="tips-card-title">使用提示</div>
<ul>
<li>所有接口都需要携带鉴权 token</li>
<li>请求参数需要放在 params 对象中</li>
<li>响应格式统一code 为 0 表示成功</li>
<li>可以在「通用信息」标签页查看详细的请求和响应说明</li>
</ul>
</div>
</div>
<div
v-if="currentApi"
class="content-page api-detail"
v-loading="apiDetailLoading"
element-loading-text="正在加载接口详情..."
>
<div class="content-section page-header-card">
<div class="page-header-main">
<el-icon class="api-detail-icon"><Document /></el-icon>
<div class="page-header-text">
<div class="page-header-label">接口</div>
<h1 class="api-action-name">{{ currentApi.action }}</h1>
</div>
</div>
<el-button type="primary" plain size="small" :icon="DocumentCopy" @click="copyApiMarkdown">
复制文档
</el-button>
</div>
<div v-if="apiDetailError" class="api-detail-alert">
<el-alert
:title="apiDetailError"
type="warning"
:closable="false"
show-icon
/>
</div>
<div v-if="apiDetail" class="api-detail-body">
<section class="content-section">
<h3 class="section-title">请求参数</h3>
<div v-if="apiDetail?.paramModel">
<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>
<!-- 桌面端表格布局 -->
<el-table v-if="!isMobile" :data="model.fields" class="data-table">
<el-table-column prop="name" label="参数名" width="auto" />
<el-table-column label="类型" width="auto">
<template #default="scope">
<div class="type-display">
<template v-if="isGenericType(scope.row)">
<template v-if="isObjectType(scope.row)">
<a :href="'#model-' + getTypeName(scope.row)">{{
getTypeName(scope.row)
}}</a>
</template>
<template v-else>
{{ getTypeName(scope.row) }}
</template>
&lt;
<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>
&gt;
</template>
<template v-else>
<template v-if="isObjectType(scope.row)">
<a :href="'#model-' + getTypeName(scope.row)">{{
getTypeName(scope.row)
}}</a>
</template>
<template v-else>
{{ getTypeName(scope.row) }}
</template>
</template>
</div>
</template>
</el-table-column>
<el-table-column prop="required" label="必填" width="auto">
<template #default="scope">
{{ scope.row.required ? "是" : "否" }}
</template>
</el-table-column>
<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) => {
return `${item.desc}(${item.value})`;
})
"
style="color: #606266"
></div>
</div>
<div
v-if="
!scope.row.desc &&
!(scope.row.enumInfo && scope.row.enumInfo.properties)
"
>
-
</div>
</div>
</template>
</el-table-column>
</el-table>
<!-- 移动端卡片布局 -->
<div v-else class="mobile-field-list">
<el-collapse accordion>
<el-collapse-item v-for="field in model.fields" :key="field.name">
<template #title>
<div class="mobile-field-title">
<span class="field-name">{{ field.name }}</span>
<span v-if="field.required" class="field-required">必填</span>
</div>
</template>
<div class="mobile-field-content">
<div class="field-item">
<div class="field-label">类型:</div>
<div class="field-value type-display">
<template v-if="isGenericType(field)">
<template v-if="isObjectType(field)">
<a :href="'#model-' + getTypeName(field)">{{
getTypeName(field)
}}</a>
</template>
<template v-else>
{{ getTypeName(field) }}
</template>
&lt;
<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>
&gt;
</template>
<template v-else>
<template v-if="isObjectType(field)">
<a :href="'#model-' + getTypeName(field)">{{
getTypeName(field)
}}</a>
</template>
<template v-else>
{{ getTypeName(field) }}
</template>
</template>
</div>
</div>
<div class="field-item" v-if="field.desc">
<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-label">枚举值:</div>
<div class="field-value">
<div
v-html="
field.enumInfo.properties.map((item) => {
return `${item.desc}(${item.value})`;
})
"
></div>
</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</div>
</section>
<section class="content-section">
<h3 class="section-title">请求响应</h3>
<div v-if="apiDetail?.responseModel">
<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>
<!-- 桌面端表格布局 -->
<el-table v-if="!isMobile" :data="model.fields" class="data-table">
<el-table-column prop="name" label="参数名" width="auto" />
<el-table-column label="类型" width="auto">
<template #default="scope">
<div class="type-display">
<template v-if="isGenericType(scope.row)">
<template v-if="isObjectType(scope.row)">
<a :href="'#model-' + getTypeName(scope.row)">{{
getTypeName(scope.row)
}}</a>
</template>
<template v-else>
{{ getTypeName(scope.row) }}
</template>
&lt;
<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>
&gt;
</template>
<template v-else>
<template v-if="isObjectType(scope.row)">
<a :href="'#model-' + getTypeName(scope.row)">{{
getTypeName(scope.row)
}}</a>
</template>
<template v-else>
{{ getTypeName(scope.row) }}
</template>
</template>
</div>
</template>
</el-table-column>
<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) => {
return `${item.desc}(${item.value})`;
})
"
style="color: #606266"
></div>
</div>
<div
v-if="
!scope.row.desc &&
!(scope.row.enumInfo && scope.row.enumInfo.properties)
"
>
-
</div>
</div>
</template>
</el-table-column>
</el-table>
<!-- 移动端卡片布局 -->
<div v-else class="mobile-field-list">
<el-collapse accordion>
<el-collapse-item v-for="field in model.fields" :key="field.name">
<template #title>
<div class="mobile-field-title">
<span class="field-name">{{ field.name }}</span>
</div>
</template>
<div class="mobile-field-content">
<div class="field-item">
<div class="field-label">类型:</div>
<div class="field-value type-display">
<template v-if="isGenericType(field)">
<template v-if="isObjectType(field)">
<a :href="'#model-' + getTypeName(field)">{{
getTypeName(field)
}}</a>
</template>
<template v-else>
{{ getTypeName(field) }}
</template>
&lt;
<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>
&gt;
</template>
<template v-else>
<template v-if="isObjectType(field)">
<a :href="'#model-' + getTypeName(field)">{{
getTypeName(field)
}}</a>
</template>
<template v-else>
{{ getTypeName(field) }}
</template>
</template>
</div>
</div>
<div class="field-item" v-if="field.desc">
<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-label">枚举值:</div>
<div class="field-value">
<div
v-html="
field.enumInfo.properties.map((item) => {
return `${item.desc}(${item.value})`;
})
"
></div>
</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</div>
</section>
<section class="content-section">
<h3 class="section-title">调用示例</h3>
<el-tabs class="code-tabs">
<el-tab-pane label="cURL">
<pre class="code-block">{{ generateCurlExample(currentApi) }}</pre>
</el-tab-pane>
<el-tab-pane label="JavaScript">
<pre class="code-block">{{ generateJsExample(currentApi) }}</pre>
</el-tab-pane>
</el-tabs>
</section>
</div>
</div>
</div>
<div v-show="activeTab === 'common'" class="content-tab-panel">
<div class="content-page common-info">
<div class="content-section page-header-card">
<div class="page-header-main">
<div class="page-header-text">
<div class="page-header-label">说明</div>
<h1 class="page-hero-title">调用说明</h1>
<p class="page-hero-desc">所有接口通用的请求参数与响应格式</p>
</div>
</div>
</div>
<section class="content-section">
<h3 class="section-title">通用请求参数</h3>
<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="说明" />
</el-table>
</section>
<section class="content-section">
<h3 class="section-title">通用响应格式</h3>
<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>
</section>
</div>
</div>
</div>
</div>
</template>
</el-main>
</el-container>
<el-drawer
v-model="showExportDrawer"
title="导出文档"
direction="rtl"
size="480px"
class="export-drawer"
>
<div class="export-module">
<p class="export-description">
导出接口文档为 Markdown 格式,支持导出全部或按需选择
</p>
<el-card shadow="never" class="quick-export-card">
<template #header>
<span class="card-header">快速导出</span>
</template>
<div class="quick-export-content">
<p class="quick-export-desc">一键导出所有接口文档</p>
<el-button
type="primary"
@click="downloadApiDocumentation"
:loading="downloading"
style="width: 100%"
>
{{ downloading ? "下载中..." : "下载全部接口文档" }}
</el-button>
</div>
</el-card>
<div class="export-content">
<div class="export-section-title">
<span class="card-header">按需导出</span>
<p class="section-desc">选择需要导出的接口,生成自定义文档</p>
</div>
<div class="export-select-header">
<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>
<span class="selected-count">
已选择: {{ selectedApiIds.length }} / {{ totalExportApiCount }}
</span>
</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"
>
<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>
<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%"
>
{{ downloading ? "导出中..." : "导出所选文档" }}
</el-button>
</div>
</div>
</div>
</el-drawer>
</template>
<script setup>
import { ref, onMounted, computed, watch, defineComponent, h } from "vue";
import axios from "axios";
import { ElMessage } from "element-plus";
import { DocumentCopy, Close, Document, Folder, ArrowRight, ArrowLeft, HomeFilled, Search, Download, Menu } from "@element-plus/icons-vue";
const activeMenuIndex = computed(() => currentApi.value?.uniqueId ?? "");
// 统计节点下接口总数(含子目录)
const getNodeApiCount = (node) => {
let count = node.apis?.length || 0;
(node.children || []).forEach((child) => {
count += getNodeApiCount(child);
});
return count;
};
// 递归菜单节点组件(用于处理无限级嵌套)
const MenuTreeNode = defineComponent({
name: "MenuTreeNode",
props: {
node: {
type: Object,
required: true,
},
},
emits: ["go-to-api"],
setup(props, { emit, slots }) {
const handleGoToApi = (api) => {
emit("go-to-api", api);
};
return () => {
const { node } = props;
// 未分组的API直接显示为菜单项
if (node.isUngrouped) {
return h(
"el-menu-item",
{
index: node.key,
onClick: () => {
handleGoToApi(node.apis[0]);
},
},
() => node.displayName
);
}
// 分组的API显示为子菜单
return h(
"el-sub-menu",
{
index: node.key,
},
{
title: () => h("span", node.displayName),
default: () => [
// 显示该节点下的API
...(node.apis || []).map((api) =>
h(
"el-menu-item",
{
key: api.uniqueId,
index: api.uniqueId,
onClick: () => {
handleGoToApi(api);
},
},
() => getActionDisplayName(api.action, node.fullPath)
)
),
// 递归渲染子节点
...(node.children || []).map((child) =>
h(MenuTreeNode, {
key: child.key,
node: child,
"onGo-to-api": handleGoToApi,
})
),
],
}
);
};
},
});
const apis = ref([]);
const currentApi = ref(null);
const apiDetail = ref(null);
const searchQuery = ref("");
const isMobile = ref(false);
const showMenu = ref(false);
const currentPath = ref([]); // 当前路径,用于记录层级
const currentPathKeys = ref([]); // 当前路径的key数组
const downloading = ref(false); // 下载状态
const apiDetailsCache = ref({}); // API详情缓存
const activeTab = ref("docs"); // 当前激活的标签页
const selectedApiIds = ref([]); // 选中的API ID列表
const exportSearchQuery = ref(""); // 导出搜索查询
const exportTreeRef = ref(null); // 导出树引用
const loading = ref(true); // 接口列表加载状态初始为true减少闪烁
const loadError = ref(null); // 加载错误信息(接口列表加载失败)
const apiDetailLoading = ref(false); // 接口详情加载状态
const apiDetailError = ref(null); // 接口详情加载错误
const exportTreeProps = {
children: "children",
label: "label",
};
const hostUrl = ref(import.meta.env.VITE_API_URL || "/api");
const showExportDrawer = ref(false);
// 监听搜索,如果有搜索则自动返回根级别显示所有匹配结果
watch(searchQuery, (newVal) => {
if (newVal && newVal.trim()) {
// 搜索时自动返回根级别,显示所有匹配结果
currentPath.value = [];
currentPathKeys.value = [];
}
});
// IndexedDB 缓存工具
const DB_NAME = "api-docs-cache";
const DB_VERSION = 4;
const STORE_NAME = "api-list";
// 初始化 IndexedDB支持版本兼容
const initDB = () => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = (event) => {
const error = event.target.error;
// 如果是版本错误,尝试使用当前版本打开
if (error && error.name === "VersionError") {
// 先打开数据库获取当前版本
const checkRequest = indexedDB.open(DB_NAME);
checkRequest.onsuccess = () => {
const currentVersion = checkRequest.result.version;
checkRequest.result.close();
// 使用当前版本打开(不触发升级)
const currentRequest = indexedDB.open(DB_NAME, currentVersion);
currentRequest.onsuccess = () => resolve(currentRequest.result);
currentRequest.onerror = () => reject(currentRequest.error);
};
checkRequest.onerror = () => reject(checkRequest.error);
} else {
reject(error);
}
};
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME);
}
};
request.onblocked = () => {
console.warn("Database upgrade blocked");
};
});
};
// 从 IndexedDB 获取缓存的接口列表
const getCachedApiList = async () => {
try {
const db = await initDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], "readonly");
const store = transaction.objectStore(STORE_NAME);
const request = store.get("api-list");
request.onsuccess = () => resolve(request.result || null);
request.onerror = () => reject(request.error);
});
} catch (error) {
console.error("Failed to get cached API list:", error);
return null;
}
};
// 将接口列表保存到 IndexedDB
const saveApiListToCache = async (apiList) => {
try {
const db = await initDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], "readwrite");
const store = transaction.objectStore(STORE_NAME);
const request = store.put(apiList, "api-list");
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
} catch (error) {
console.error("Failed to save API list to cache:", error);
}
};
const commonParams = [
{ name: "action", desc: "请求的服务接口名称", required: "是" },
{ name: "token", desc: "携带的鉴权token", required: "是" },
{ name: "params", desc: "业务参数对象", required: "是" },
];
const commonResponse = [
{ name: "code", desc: "状态码, 0表示成功103表示鉴权失败" },
{ name: "msg", desc: "错误信息" },
{ name: "data", desc: "数据内容,对象或列表" },
];
// 处理接口列表数据
const processApiList = (apiData) => {
// 展开API列表如果一个API包含多个action则分开显示
const expandedApis = [];
apiData.forEach((api) => {
// 如果action是数组展开成多个API项
if (Array.isArray(api.action)) {
api.action.forEach((action, index) => {
expandedApis.push({
...api,
action: action,
uniqueId: `${api.id}_${index}`, // 生成唯一ID
originalId: api.id, // 保留原始ID用于获取详情
});
});
} else {
// 单个action直接添加
expandedApis.push({
...api,
uniqueId: `${api.id}_0`, // 生成唯一ID
originalId: api.id, // 保留原始ID用于获取详情
});
}
});
// 按action排序
return expandedApis.sort((a, b) => a.action.localeCompare(b.action));
};
onMounted(async () => {
checkDeviceType();
window.addEventListener("resize", checkDeviceType);
loading.value = true; // 先显示加载状态,避免显示空白页面
loadError.value = null;
// 先从缓存获取接口列表
let cachedApiList = null;
try {
cachedApiList = await getCachedApiList();
if (cachedApiList) {
// 使用缓存数据先渲染
apis.value = processApiList(cachedApiList);
// 有缓存数据后,立即隐藏加载状态,让用户看到内容
loading.value = false;
// 检查URL是否包含API id参数
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get("id");
if (id) {
const apiToShow = findApiById(id);
if (apiToShow) {
showApiDetail(apiToShow);
}
}
}
// 如果没有缓存数据,继续显示加载状态,等待请求完成
} catch (error) {
console.error("Failed to load cached API list:", error);
// 缓存加载失败,继续显示加载状态,等待请求完成
}
// 然后请求最新的接口列表(后台无感刷新)
try {
const response = await axios.get(hostUrl.value, {
params: {
action: "getActionList",
},
});
// 处理并保存最新的接口列表
const processedApis = processApiList(response.data.data);
apis.value = processedApis;
// 保存到缓存
await saveApiListToCache(response.data.data);
// 检查URL是否包含API id参数
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get("id");
if (id) {
const apiToShow = findApiById(id);
if (apiToShow) {
showApiDetail(apiToShow);
}
}
} catch (error) {
console.error("Failed to fetch API list:", error);
// 如果请求失败且没有缓存数据,显示错误
if (!cachedApiList) {
loadError.value =
error.response?.data?.msg || error.message || "获取接口列表失败,请稍后重试";
} else {
// 有缓存数据,只提示用户使用的是缓存数据(静默提示,不干扰用户)
ElMessage.warning("获取最新接口列表失败,已使用缓存数据");
}
} finally {
// 确保最终隐藏加载状态(如果没有缓存数据,这里会隐藏;如果有缓存数据,已经在上面隐藏了)
loading.value = false;
}
});
// 根据id查找API
const findApiById = (id) => {
return apis.value.find((a) => a.uniqueId === id || a.originalId === id) || null;
};
// 更新URL参数
const updateUrlParameter = (id) => {
const url = new URL(window.location);
if (id) {
url.searchParams.set("id", id);
} else {
url.searchParams.delete("id");
}
window.history.pushState({}, "", url);
};
// 路由到首页
const goHome = () => {
currentApi.value = null;
currentPath.value = [];
currentPathKeys.value = [];
updateUrlParameter(null);
};
// 路由到指定API
const goToApi = (api) => {
showApiDetail(api);
updateUrlParameter(api.uniqueId);
};
const checkDeviceType = () => {
isMobile.value = window.innerWidth <= 768;
showMenu.value = !isMobile.value;
};
const toggleMenu = () => {
showMenu.value = !showMenu.value;
};
const showApiDetail = async (api) => {
currentApi.value = api;
apiDetail.value = null; // 清空之前的详情
apiDetailError.value = null; // 清空错误信息
apiDetailLoading.value = true;
try {
const response = await axios.get(hostUrl.value, {
params: {
action: "getActionDetail",
id: api.originalId || api.id,
},
});
apiDetail.value = response.data.data;
} catch (error) {
console.error("Failed to fetch API detail:", error);
// 如果接口详情加载失败,尝试从接口列表中获取基本信息
if (api) {
// 使用接口列表中的基本信息作为详情
apiDetail.value = {
action: api.action,
paramModel: null,
responseModel: null,
// 可以添加其他从接口列表中获取的字段
};
apiDetailError.value = "获取接口详情失败,已使用接口列表中的基本信息";
ElMessage.warning("获取接口详情失败,已使用接口列表中的基本信息");
} else {
apiDetailError.value =
error.response?.data?.msg || error.message || "获取接口详情失败";
}
} finally {
apiDetailLoading.value = false;
}
};
const generateCurlExample = (api) => {
// 获取必传参数
const requiredParams =
apiDetail.value?.paramModel?.fields
?.filter((field) => field.required)
?.reduce((acc, field) => {
acc[field.name] = getExampleValue(field);
return acc;
}, {}) || {};
return `curl -X POST 'API_URL' \\
-H 'Content-Type: application/json' \\
-d '{
"action": "${api.action}",
"token": "YOUR_TOKEN",
"params": {${
Object.entries(requiredParams)
.map(([key, value]) => '\n "' + key + '": ' + JSON.stringify(value))
.join(",") || ""
}
}
}'`;
};
const generateJsExample = (api) => {
// 获取必传参数
const requiredParams =
apiDetail.value?.paramModel?.fields
?.filter((field) => field.required)
?.reduce((acc, field) => {
acc[field.name] = getExampleValue(field);
return acc;
}, {}) || {};
return `// 使用 axios
axios.post('API_URL', {
action: '${api.action}',
token: 'YOUR_TOKEN',
params: {${
Object.entries(requiredParams)
.map(([key, value]) => "\n " + key + ": " + JSON.stringify(value))
.join(",") || ""
}
}
})
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error(error);
});`;
};
// 根据类型生成示例值
const getExampleValue = (field) => {
if (!field) return null;
if (field.typeName === "String") return "example";
if (field.typeName === "Integer" || field.typeName === "Long") return 0;
if (field.typeName === "Double" || field.typeName === "Float") return 0.0;
if (field.typeName === "Boolean") return false;
if (field.typeName === "Date") return "2023-01-01";
if (field.typeName === "DateTime") return "2023-01-01 00:00:00";
if (field.typeName === "List" || field.typeName === "Set" || field.typeName === "Array")
return [];
if (field.typeName === "Map" || !field.basicProperty) return {};
return null;
};
const isModelGroup = (model) => {
return (
model &&
!model.basicProperty &&
Array.isArray(model.fields) &&
model.fields.length > 0
);
};
const getModelDisplayName = (model) => {
if (!model) return "";
const containerTypes = ["List", "Set", "Map", "Array"];
if (model.simpleName && !containerTypes.includes(model.simpleName)) {
return model.simpleName;
}
if (model.className) {
const shortName = model.className.split(".").pop();
if (shortName) return shortName;
}
return model.simpleName || "";
};
const hasNestedModelContent = (model) => {
if (!model) return false;
if (isModelGroup(model)) return true;
return (model.parametricModels || []).some((item) => hasNestedModelContent(item));
};
const getAllModels = (model, parentInfo) => {
if (!model) return [];
const models = [];
if (isModelGroup(model)) {
models.push(Object.assign({ parentInfo }, model));
}
const collectFromFields = (fields) => {
(fields || []).forEach((field) => {
if (field.parametricModels?.length) {
field.parametricModels.forEach((parametricModel) => {
models.push(...getAllModels(parametricModel));
});
} else if (field.model?.parametricModels?.length) {
field.model.parametricModels.forEach((parametricModel) => {
models.push(...getAllModels(parametricModel));
});
} else if (field.model && !field.model.basicProperty) {
models.push(...getAllModels(field.model));
}
});
};
if (isModelGroup(model)) {
collectFromFields(model.fields);
}
// List / Map 等 basicProperty 泛型容器,继续展开泛型参数中的模型
if (model.parametricModels?.length) {
model.parametricModels.forEach((parametricModel, index) => {
models.push(
...getAllModels(parametricModel, {
index,
length: model.parametricModels.length,
simpleName: model.simpleName,
})
);
});
}
return models;
};
// 是否应显示模型锚点链接basicProperty 基础类型不跳转,泛型参数中的对象类型可跳转)
const isObjectType = (field) => {
if (!field) return false;
// 泛型容器List/Map 等)本身不跳转,由 getGenericParams 处理泛型参数链接
if (field.basicProperty && isGenericType(field)) return false;
if (field.basicProperty) return false;
if (field.model?.basicProperty && !field.model?.parametricModels?.length) return false;
if (field.model) return hasNestedModelContent(field.model);
return true;
};
const isGenericType = (field) => {
return field && field.parametricModels && field.parametricModels.length > 0;
};
const getTypeName = (field) => {
if (!field) {
return "";
}
return field.typeName || field.simpleName || "";
};
const getGenericParams = (field) => {
if (!field || !field.parametricModels) {
return [];
}
return field.parametricModels
.filter((item) => !!item)
.map((item) => ({
name: getModelDisplayName(item),
isObject: hasNestedModelContent(item),
}));
};
// 获取完整的类型名称(包括泛型参数)
const getFullTypeName = (field) => {
if (!field) {
return "";
}
const baseType = getTypeName(field);
// 如果是泛型类型,添加泛型参数
if (isGenericType(field)) {
const genericParams = getGenericParams(field)
.map((param) => param.name)
.join(", ");
return `${baseType}<${genericParams}>`;
}
return baseType;
};
// 获取action的显示名称去掉上级路径
const getActionDisplayName = (action, parentPath) => {
if (!action || typeof action !== "string") return action;
if (!parentPath) return action;
const prefix = parentPath + ".";
if (action.startsWith(prefix)) {
return action.substring(prefix.length);
}
return action;
};
// 解析点分路径,构建目录/文件树(最后一段为接口,其余为目录)
const buildTree = (paths) => {
const root = {};
paths.forEach((path) => {
const parts = path.split(".");
let current = root;
parts.forEach((part, index) => {
const isFile = index === parts.length - 1;
if (!current[part]) {
current[part] = isFile
? { type: "file" }
: { type: "dir", children: {} };
}
if (!isFile && current[part].type !== "dir") {
throw new Error(`冲突: "${part}" 不能同时是文件和目录`);
}
if (!isFile) {
current = current[part].children;
}
});
});
return root;
};
// 将 buildTree 结果转为侧边栏节点,并挂载 API 对象
const convertTreeToUiNodes = (treeObj, parentPath, apiMap) => {
return Object.entries(treeObj)
.filter(([, node]) => node.type === "dir")
.map(([name, node]) => {
const fullPath = parentPath ? `${parentPath}.${name}` : name;
const children = [];
const apis = [];
Object.entries(node.children).forEach(([part, child]) => {
if (child.type === "file") {
const api = apiMap.get(`${fullPath}.${part}`);
if (api) apis.push(api);
} else if (child.type === "dir") {
children.push(...convertTreeToUiNodes({ [part]: child }, fullPath, apiMap));
}
});
return {
key: fullPath,
displayName: name,
fullPath,
children,
apis,
};
})
.sort((a, b) => {
if (a.children.length > 0 && b.children.length === 0) return -1;
if (a.children.length === 0 && b.children.length > 0) return 1;
return a.displayName.localeCompare(b.displayName);
});
};
// 按点分路径分类接口(有层级 / 未分组)
const classifyApisByPath = (apiList) => {
const ungrouped = [];
const groupedApis = [];
apiList.forEach((api) => {
if (!api.action || typeof api.action !== "string") {
ungrouped.push(api);
return;
}
const parts = api.action.split(".");
if (parts.length <= 1 || parts[0] === "") {
ungrouped.push(api);
} else {
groupedApis.push(api);
}
});
return { groupedApis, ungrouped };
};
// 导出树节点排序:目录在前,接口在后,同类型按名称排序
const sortExportNodes = (nodes) => {
return nodes.sort((a, b) => {
if (!a.isApi && b.isApi) return -1;
if (a.isApi && !b.isApi) return 1;
return a.label.localeCompare(b.label);
});
};
// 将 buildTree 结果转为导出树节点el-tree 格式)
const convertTreeToExportNodes = (treeObj, parentPath, apiMap) => {
const result = [];
Object.entries(treeObj).forEach(([name, node]) => {
if (node.type === "dir") {
const fullPath = parentPath ? `${parentPath}.${name}` : name;
const children = [];
Object.entries(node.children).forEach(([part, child]) => {
if (child.type === "file") {
const api = apiMap.get(`${fullPath}.${part}`);
if (api) {
children.push({
id: api.uniqueId,
label: getActionDisplayName(api.action, fullPath),
isApi: true,
api,
});
}
} else if (child.type === "dir") {
const subNodes = convertTreeToExportNodes({ [part]: child }, fullPath, apiMap);
children.push(...subNodes);
}
});
result.push({
id: fullPath,
label: name,
isApi: false,
children: sortExportNodes(children),
});
} else if (node.type === "file") {
const actionPath = parentPath ? `${parentPath}.${name}` : name;
const api = apiMap.get(actionPath);
if (api) {
result.push({
id: api.uniqueId,
label: api.action,
isApi: true,
api,
});
}
}
});
return sortExportNodes(result);
};
// 构建导出树(支持无限级)
const buildExportTree = (apiList) => {
const { groupedApis, ungrouped } = classifyApisByPath(apiList);
const result = [];
if (groupedApis.length > 0) {
try {
const tree = buildTree(groupedApis.map((api) => api.action));
const apiMap = new Map(groupedApis.map((api) => [api.action, api]));
result.push(...convertTreeToExportNodes(tree, "", apiMap));
} catch (error) {
console.error("导出树路径解析冲突:", error);
groupedApis.forEach((api) => ungrouped.push(api));
}
}
ungrouped.forEach((api) => {
result.push({
id: api.uniqueId,
label: api.action,
isApi: true,
api,
});
});
return sortExportNodes(result);
};
// 构建树形结构(支持无限级)
const buildApiTree = (apiList) => {
const { groupedApis, ungrouped } = classifyApisByPath(apiList);
let treeArray = [];
if (groupedApis.length > 0) {
try {
const tree = buildTree(groupedApis.map((api) => api.action));
const apiMap = new Map(groupedApis.map((api) => [api.action, api]));
treeArray = convertTreeToUiNodes(tree, "", apiMap);
} catch (error) {
console.error("接口路径解析冲突:", error);
groupedApis.forEach((api) => ungrouped.push(api));
}
}
// 未分组的API也添加到结果中
ungrouped.forEach((api) => {
treeArray.push({
key: api.uniqueId,
displayName: api.action,
fullPath: "",
children: [],
apis: [api], // 将api放入apis数组中保持结构一致
isUngrouped: true, // 标记为未分组
});
});
return treeArray;
};
// 搜索匹配函数(大小写不敏感)
const matchesSearch = (text, query) => {
if (!text || !query) return false;
return text.toLowerCase().includes(query.toLowerCase());
};
// 过滤后的API列表搜索时使用
const filteredApis = computed(() => {
if (!searchQuery.value) {
return apis.value;
}
const query = searchQuery.value.toLowerCase();
return apis.value.filter((api) => {
if (!api.action) return false;
return matchesSearch(api.action, query);
});
});
// API树形结构根据是否有搜索决定使用原始数据还是过滤后的数据
const apiTree = computed(() => {
// 如果有搜索使用过滤后的API列表构建树否则使用原始API列表
const apiList = searchQuery.value ? filteredApis.value : apis.value;
return buildApiTree(apiList);
});
// 过滤API树保持兼容性实际已通过 apiTree 实现)
const filteredApiTree = computed(() => {
return apiTree.value;
});
// 根据当前路径获取当前层级要显示的节点和API
const currentLevelNodes = computed(() => {
// 根据路径找到当前节点
let currentNode = apiTree.value;
let currentParentNode = null;
for (const key of currentPathKeys.value) {
const found = currentNode.find((node) => node.key === key);
if (found) {
currentParentNode = found;
currentNode = found.children || [];
} else {
return [];
}
}
// 如果当前父节点只有API没有子节点返回空数组API由 currentLevelApis 处理)
if (
currentParentNode &&
currentParentNode.apis.length > 0 &&
currentNode.length === 0
) {
return [];
}
// 如果有搜索,由于已经通过 filteredApis 过滤了数据并重新构建树,所以直接返回当前节点即可
// 搜索只匹配 action不匹配节点名称
if (searchQuery.value) {
// 树中已经只包含匹配的 API直接返回
return currentNode;
}
return currentNode;
});
// 当前层级的API用于显示当前节点下的API列表即使该节点有子节点也会显示
const currentLevelApis = computed(() => {
if (currentPathKeys.value.length === 0) return [];
let currentNode = apiTree.value;
let currentParentNode = null;
for (const key of currentPathKeys.value) {
const found = currentNode.find((node) => node.key === key);
if (found) {
currentParentNode = found;
currentNode = found.children || [];
} else {
return [];
}
}
// 如果当前父节点有API返回这些API即使该节点有子节点也会显示
if (currentParentNode && currentParentNode.apis.length > 0) {
// 如果有搜索,由于已经通过 filteredApis 过滤了数据,所以 apis 中已经只包含匹配的 API
// 搜索只匹配 action不匹配节点名称
if (searchQuery.value) {
const queryLower = searchQuery.value.toLowerCase();
return currentParentNode.apis.sort((a, b) => {
// 排序:精确匹配优先
const aMatch = a.action.toLowerCase() === queryLower;
const bMatch = b.action.toLowerCase() === queryLower;
if (aMatch && !bMatch) return -1;
if (!aMatch && bMatch) return 1;
return a.action.localeCompare(b.action);
});
}
return currentParentNode.apis;
}
return [];
});
// 进入分组
const enterGroup = (node) => {
currentPath.value.push(node.displayName);
currentPathKeys.value.push(node.key);
// 自动遍历:如果当前目录没有接口且只有一个子目录,自动进入下一个目录
autoNavigateToNextGroup();
};
// 自动遍历到下一个目录(如果当前目录没有接口且只有一个子目录)
const autoNavigateToNextGroup = () => {
// 根据路径找到当前节点
let currentNode = apiTree.value;
let currentParentNode = null;
for (const key of currentPathKeys.value) {
const found = currentNode.find((node) => node.key === key);
if (found) {
currentParentNode = found;
currentNode = found.children || [];
} else {
return;
}
}
// 如果当前目录没有API且只有一个子目录自动进入该子目录
// 循环直到找到有API的目录或者有多个子目录的目录
while (
currentParentNode &&
currentParentNode.apis.length === 0 &&
currentNode.length === 1
) {
const nextNode = currentNode[0];
currentPath.value.push(nextNode.displayName);
currentPathKeys.value.push(nextNode.key);
// 更新当前节点
currentParentNode = nextNode;
currentNode = nextNode.children || [];
}
};
// 返回上一级
const goBack = () => {
if (currentPath.value.length > 0) {
currentPath.value.pop();
currentPathKeys.value.pop();
}
};
// 导航到指定路径
const navigateToPath = (index) => {
// 截取到指定索引的路径
currentPath.value = currentPath.value.slice(0, index + 1);
currentPathKeys.value = currentPathKeys.value.slice(0, index + 1);
};
// 获取当前节点API的显示名称
const getCurrentNodeApiDisplayName = (api) => {
if (!api || !api.action) return api.action || "";
// 找到当前节点的 fullPath
let currentNode = apiTree.value;
let currentParentNode = null;
for (const key of currentPathKeys.value) {
const found = currentNode.find((node) => node.key === key);
if (found) {
currentParentNode = found;
currentNode = found.children || [];
} else {
break;
}
}
if (currentParentNode && currentParentNode.fullPath) {
return getActionDisplayName(api.action, currentParentNode.fullPath);
}
return api.action;
};
// 下载接口文档
const downloadApiDocumentation = async () => {
downloading.value = true;
try {
// 直接使用接口列表数据,不请求详情
const allApiDetails = apis.value.map((api) => ({
api,
detail: api, // 使用接口列表数据本身作为详情
}));
// 生成Markdown文档
const markdown = generateMarkdownDocumentation(allApiDetails);
// 下载文件
const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `API文档_${new Date().toISOString().split("T")[0]}.md`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error("Failed to download documentation:", error);
alert("下载失败,请稍后重试");
} finally {
downloading.value = false;
}
};
// 生成Markdown文档内容
const generateMarkdownDocumentation = (allApiDetails) => {
let markdown = "# API 接口文档\n\n";
markdown += `生成时间: ${new Date().toLocaleString("zh-CN")}\n\n`;
markdown += `总计接口数: ${allApiDetails.length}\n\n`;
markdown += "---\n\n";
// 通用请求参数
markdown += "## 通用请求参数\n\n";
markdown += "| 参数名| 必填 | 说明 |\n";
markdown += "|--------|------|----------|\n";
commonParams.forEach((param) => {
markdown += `| ${param.name} | ${param.required} | ${param.desc} |\n`;
});
markdown += "\n";
// 通用响应格式
markdown += "## 通用响应格式\n\n";
markdown += "| 字段名 | 说明 |\n";
markdown += "|--------|------|\n";
commonResponse.forEach((field) => {
markdown += `| ${field.name} | ${field.desc} |\n`;
});
markdown += "\n---\n\n";
// 接口详情
markdown += "## 接口详情\n\n";
allApiDetails.forEach(({ api, detail }, index) => {
markdown += `### ${index + 1}. ${api.action}\n\n`;
if (detail) {
// 请求参数
if (detail.paramModel) {
markdown += "#### 请求参数\n\n";
if (detail.paramModel.basicProperty) {
markdown += `类型:${getFullTypeName(detail.paramModel)}\n\n`;
}
const paramModels = getAllModels(detail.paramModel);
if (paramModels.length > 0) {
paramModels.forEach((model) => {
markdown += `**${getModelDisplayName(model)}**\n\n`;
markdown += "| 参数名 | 类型 | 必填 | 说明 |\n";
markdown += "|--------|------|------|----------|\n";
(model.fields || []).forEach((field) => {
const typeName = getFullTypeName(field);
const required = field.required ? "是" : "否";
let desc = field.desc || "-";
if (
field.enumInfo &&
field.enumInfo.properties &&
field.enumInfo.properties.length > 0
) {
const enumValues = field.enumInfo.properties
.map((item) => `${item.desc}(${item.value})`)
.join("、");
desc =
desc !== "-"
? `${desc};枚举值:${enumValues}`
: `枚举值:${enumValues}`;
}
markdown += `| ${field.name} | ${typeName} | ${required} | ${desc} |\n`;
});
markdown += "\n";
});
} else if (!detail.paramModel.basicProperty) {
markdown += "无参数\n\n";
}
}
// 请求响应
if (detail.responseModel) {
markdown += "#### 请求响应\n\n";
if (detail.responseModel.basicProperty) {
markdown += `类型:${getFullTypeName(detail.responseModel)}\n\n`;
}
const responseModels = getAllModels(detail.responseModel);
if (responseModels.length > 0) {
responseModels.forEach((model) => {
markdown += `**${getModelDisplayName(model)}**\n\n`;
markdown += "| 字段名 | 类型 | 说明 |\n";
markdown += "|--------|------|------|\n";
(model.fields || []).forEach((field) => {
const typeName = getFullTypeName(field);
let desc = field.desc || "-";
if (
field.enumInfo &&
field.enumInfo.properties &&
field.enumInfo.properties.length > 0
) {
const enumValues = field.enumInfo.properties
.map((item) => `${item.desc}(${item.value})`)
.join("、");
desc =
desc !== "-"
? `${desc};枚举值:${enumValues}`
: `枚举值:${enumValues}`;
}
markdown += `| ${field.name} | ${typeName} | ${desc} |\n`;
});
markdown += "\n";
});
} else if (!detail.responseModel.basicProperty) {
markdown += "无响应数据\n\n";
}
} else {
markdown += "#### 请求响应\n\n";
markdown += "无响应数据\n\n";
}
} else {
markdown += "> 无法获取接口详情\n\n";
}
markdown += "---\n\n";
});
return markdown;
};
// 生成单个接口的Markdown文档
const generateSingleApiMarkdown = (api, detail) => {
let markdown = `# ${api.action}\n\n`;
markdown += `生成时间: ${new Date().toLocaleString("zh-CN")}\n\n`;
markdown += "---\n\n";
// 通用请求参数
markdown += "## 通用请求参数\n\n";
markdown += "| 参数名| 必填 | 说明 |\n";
markdown += "|--------|------|----------|\n";
commonParams.forEach((param) => {
markdown += `| ${param.name} | ${param.required} | ${param.desc} |\n`;
});
markdown += "\n";
// 通用响应格式
markdown += "## 通用响应格式\n\n";
markdown += "| 字段名 | 说明 |\n";
markdown += "|--------|------|\n";
commonResponse.forEach((field) => {
markdown += `| ${field.name} | ${field.desc} |\n`;
});
markdown += "\n---\n\n";
// 接口详情
markdown += `## ${api.action}\n\n`;
if (detail) {
// 请求参数
if (detail.paramModel) {
markdown += "### 请求参数\n\n";
if (detail.paramModel.basicProperty) {
markdown += `类型:${getFullTypeName(detail.paramModel)}\n\n`;
}
const paramModels = getAllModels(detail.paramModel);
if (paramModels.length > 0) {
paramModels.forEach((model) => {
markdown += `**${getModelDisplayName(model)}**\n\n`;
markdown += "| 参数名 | 类型 | 必填 | 说明 |\n";
markdown += "|--------|------|------|----------|\n";
(model.fields || []).forEach((field) => {
const typeName = getFullTypeName(field);
const required = field.required ? "是" : "否";
let desc = field.desc || "-";
if (
field.enumInfo &&
field.enumInfo.properties &&
field.enumInfo.properties.length > 0
) {
const enumValues = field.enumInfo.properties
.map((item) => `${item.desc}(${item.value})`)
.join("、");
desc =
desc !== "-" ? `${desc};枚举值:${enumValues}` : `枚举值:${enumValues}`;
}
markdown += `| ${field.name} | ${typeName} | ${required} | ${desc} |\n`;
});
markdown += "\n";
});
} else if (!detail.paramModel.basicProperty) {
markdown += "无参数\n\n";
}
}
// 请求响应
if (detail.responseModel) {
markdown += "### 请求响应\n\n";
if (detail.responseModel.basicProperty) {
markdown += `类型:${getFullTypeName(detail.responseModel)}\n\n`;
}
const responseModels = getAllModels(detail.responseModel);
if (responseModels.length > 0) {
responseModels.forEach((model) => {
markdown += `**${getModelDisplayName(model)}**\n\n`;
markdown += "| 字段名 | 类型 | 说明 |\n";
markdown += "|--------|------|------|\n";
(model.fields || []).forEach((field) => {
const typeName = getFullTypeName(field);
let desc = field.desc || "-";
if (
field.enumInfo &&
field.enumInfo.properties &&
field.enumInfo.properties.length > 0
) {
const enumValues = field.enumInfo.properties
.map((item) => `${item.desc}(${item.value})`)
.join("、");
desc =
desc !== "-" ? `${desc};枚举值:${enumValues}` : `枚举值:${enumValues}`;
}
markdown += `| ${field.name} | ${typeName} | ${desc} |\n`;
});
markdown += "\n";
});
} else if (!detail.responseModel.basicProperty) {
markdown += "无响应数据\n\n";
}
} else {
markdown += "### 请求响应\n\n";
markdown += "无响应数据\n\n";
}
} else {
markdown += "> 无法获取接口详情\n\n";
}
return markdown;
};
// 复制接口MD文档到剪贴板
const copyApiMarkdown = async () => {
if (!currentApi.value) {
return;
}
try {
// 使用apiDetail如果有或api本身作为detail
const detail = apiDetail.value || currentApi.value;
const markdown = generateSingleApiMarkdown(currentApi.value, detail);
// 使用Clipboard API复制到剪贴板
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(markdown);
ElMessage.success("接口文档已复制到剪贴板");
} else {
// 降级方案:使用传统方法
const textArea = document.createElement("textarea");
textArea.value = markdown;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
textArea.style.top = "-999999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand("copy");
ElMessage.success("接口文档已复制到剪贴板");
} catch (err) {
ElMessage.error("复制失败,请手动复制");
}
document.body.removeChild(textArea);
}
} catch (error) {
console.error("Failed to copy markdown:", error);
ElMessage.error("复制失败,请稍后重试");
}
};
// 导出树数据
const exportTreeData = computed(() => {
const apiList = exportSearchQuery.value
? apis.value.filter((api) => {
if (!api.action) return false;
return api.action.toLowerCase().includes(exportSearchQuery.value.toLowerCase());
})
: apis.value;
return buildExportTree(apiList);
});
// 计算总API数量
const totalExportApiCount = computed(() => {
return apis.value.length;
});
// 过滤树节点
const filterExportNode = (value, data) => {
if (!value) return true;
const query = value.toLowerCase();
if (data.isApi) {
return data.label.toLowerCase().includes(query);
}
// 对于文件夹节点,检查其子节点是否匹配
return true;
};
// 监听搜索查询,过滤树
watch(exportSearchQuery, (val) => {
if (exportTreeRef.value) {
exportTreeRef.value.filter(val);
}
});
// 处理树节点选择
const handleExportTreeCheck = (data, checked) => {
const checkedKeys = exportTreeRef.value.getCheckedKeys();
const halfCheckedKeys = exportTreeRef.value.getHalfCheckedKeys();
// 收集所有选中的API ID
const apiIds = new Set();
const collectApiIds = (nodes) => {
nodes.forEach((node) => {
if (node.isApi) {
if (checkedKeys.includes(node.id) || halfCheckedKeys.includes(node.id)) {
apiIds.add(node.id);
}
} else if (node.children) {
collectApiIds(node.children);
}
});
};
// 从树数据中收集所有API ID
const collectAllApiIds = (nodes) => {
nodes.forEach((node) => {
if (node.isApi && checkedKeys.includes(node.id)) {
apiIds.add(node.id);
} else if (node.children) {
collectAllApiIds(node.children);
}
});
};
collectAllApiIds(exportTreeData.value);
selectedApiIds.value = Array.from(apiIds);
};
// 全选所有API
const selectAllExportApis = () => {
if (exportTreeRef.value) {
// 获取所有API节点
const getAllApiNodes = (nodes) => {
const apiNodes = [];
nodes.forEach((node) => {
if (node.isApi) {
apiNodes.push(node.id);
} else if (node.children) {
apiNodes.push(...getAllApiNodes(node.children));
}
});
return apiNodes;
};
const allApiIds = getAllApiNodes(exportTreeData.value);
exportTreeRef.value.setCheckedKeys(allApiIds);
selectedApiIds.value = allApiIds;
}
};
// 取消全选
const deselectAllExportApis = () => {
if (exportTreeRef.value) {
exportTreeRef.value.setCheckedKeys([]);
selectedApiIds.value = [];
}
};
// 下载文件辅助函数
const downloadFile = (content, filename, mimeType) => {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
// 处理按需导出
const handleExportSelected = async () => {
if (selectedApiIds.value.length === 0) {
return;
}
downloading.value = true;
try {
// 过滤选中的API直接使用接口列表数据
const selectedApis = apis.value.filter((api) =>
selectedApiIds.value.includes(api.uniqueId)
);
// 直接使用接口列表数据,不请求详情
const allApiDetails = selectedApis.map((api) => ({
api,
detail: api, // 使用接口列表数据本身作为详情
}));
// 导出文档
const markdown = generateMarkdownDocumentation(allApiDetails);
const filename = `API文档_${new Date().toISOString().split("T")[0]}.md`;
downloadFile(markdown, filename, "text/markdown;charset=utf-8");
ElMessage.success("导出成功!");
} catch (error) {
console.error("Failed to export:", error);
ElMessage.error("导出失败,请稍后重试");
} finally {
downloading.value = false;
}
};
</script>
<style>
* {
margin: 0;
padding: 0;
}
/* 移除 scoped使样式全局生效 */
.type-display a.model-link {
color: var(--el-color-primary);
text-decoration: none;
cursor: pointer;
}
.type-display a.model-link:hover {
text-decoration: underline;
}
/* 响应式表格样式 */
@media (max-width: 768px) {
.responsive-table {
width: 100%;
overflow-x: auto;
}
.responsive-table .el-table__body,
.responsive-table .el-table__header {
min-width: 100%;
}
}
</style>
<style scoped>
.el-container {
height: 100vh;
}
.mobile-mode {
flex-direction: column;
}
.mobile-header {
display: grid;
grid-template-columns: 40px 1fr 40px;
align-items: center;
background-color: #fff;
border-bottom: 1px solid #eef0f3;
height: 52px;
width: 100%;
padding: 0 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
}
.mobile-header h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1f2329;
text-align: center;
}
.menu-toggle-btn {
font-size: 20px;
color: #303133;
}
.mobile-header-spacer {
width: 40px;
}
.mobile-menu-close {
text-align: right;
padding: 10px;
cursor: pointer;
color: var(--el-color-primary);
}
/* 手机端侧边栏顶部头部(已合并至 sidebar-header */
.el-aside.sidebar {
background: #f7f8fa;
border-right: 1px solid #e4e7ed;
z-index: 1000;
overflow: hidden;
}
.sidebar-inner {
display: flex;
flex-direction: column;
height: 100%;
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 16px 12px;
flex-shrink: 0;
border-bottom: 1px solid #eef0f3;
background: #fff;
}
.sidebar-brand {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.brand-icon {
font-size: 22px;
color: var(--el-color-primary);
flex-shrink: 0;
}
.brand-text {
display: flex;
flex-direction: column;
min-width: 0;
}
.brand-title {
font-size: 15px;
font-weight: 600;
color: #1f2329;
line-height: 1.3;
}
.brand-subtitle {
font-size: 12px;
color: #8f959e;
margin-top: 2px;
}
.sidebar-close-btn {
flex-shrink: 0;
margin-left: 8px;
}
.sidebar-search {
padding: 10px 12px;
flex-shrink: 0;
background: #fff;
border-bottom: 1px solid #eef0f3;
}
.sidebar-search :deep(.el-input__wrapper) {
border-radius: 8px;
box-shadow: 0 0 0 1px #dcdfe6 inset;
transition: box-shadow 0.2s ease;
}
.sidebar-search :deep(.el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
}
.sidebar-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 16px;
scrollbar-width: thin;
scrollbar-color: #dcdfe6 transparent;
}
.sidebar-content::-webkit-scrollbar {
width: 4px;
}
.sidebar-content::-webkit-scrollbar-thumb {
background: #dcdfe6;
border-radius: 4px;
}
.sidebar-content::-webkit-scrollbar-thumb:hover {
background: #c0c4cc;
}
.sidebar-error {
padding: 32px 16px;
}
.sidebar-empty {
padding: 24px 0;
}
@media (max-width: 768px) {
.sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 100%;
z-index: 2000;
}
}
.menu-vertical {
background: transparent;
border: none;
padding: 4px 0;
}
.menu-vertical :deep(.el-menu-item) {
height: 40px;
line-height: 40px;
border-radius: 8px;
margin: 2px 8px;
padding: 0 10px !important;
transition: background-color 0.2s ease, color 0.2s ease;
position: relative;
}
.menu-item-inner {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
}
.menu-item-icon {
font-size: 16px;
flex-shrink: 0;
}
.menu-item-icon.folder {
color: #e6a23c;
}
.menu-item-icon.api {
color: #409eff;
}
.menu-item-label {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
color: #303133;
}
.menu-item-count {
flex-shrink: 0;
font-size: 11px;
color: #909399;
background: #eef0f3;
padding: 1px 6px;
border-radius: 10px;
line-height: 16px;
}
.menu-item-arrow {
flex-shrink: 0;
font-size: 12px;
color: #c0c4cc;
transition: transform 0.2s ease, color 0.2s ease;
}
.menu-folder-item:hover .menu-item-arrow {
color: var(--el-color-primary);
transform: translateX(2px);
}
.menu-folder-item :deep(.menu-item-label) {
font-weight: 500;
}
.menu-vertical :deep(.menu-section-item) {
height: 32px !important;
line-height: 32px !important;
margin-top: 4px !important;
cursor: default !important;
opacity: 1 !important;
}
.menu-vertical :deep(.menu-section-item:hover) {
background: transparent !important;
}
.menu-vertical :deep(.menu-section-item::before) {
display: none !important;
}
.menu-section-label {
font-size: 11px;
font-weight: 600;
color: #909399;
letter-spacing: 0.5px;
}
.menu-vertical :deep(.el-menu-item:hover) {
background-color: #eef2ff;
}
.menu-vertical :deep(.el-menu-item.is-active) {
background-color: #e8f0fe;
color: var(--el-color-primary);
}
.menu-vertical :deep(.el-menu-item.is-active)::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 18px;
background: var(--el-color-primary);
border-radius: 0 2px 2px 0;
}
.menu-vertical :deep(.menu-api-item.is-active .menu-item-label) {
color: var(--el-color-primary);
font-weight: 500;
}
.menu-vertical :deep(.menu-api-item.is-active .menu-item-icon) {
color: var(--el-color-primary);
}
.main-content {
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0 !important;
background: #f7f8fa;
}
.content-frame {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: #f7f8fa;
}
.content-nav-wrap,
.content-page {
width: 100%;
padding-left: 24px;
padding-right: 24px;
box-sizing: border-box;
}
.content-nav-wrap {
flex-shrink: 0;
padding-top: 16px;
padding-bottom: 0;
}
.content-nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 20px;
margin-bottom: 0;
width: 100%;
box-sizing: border-box;
}
.content-frame-body {
flex: 1;
overflow-y: auto;
}
.content-tab-panel {
min-height: 100%;
}
.content-nav-tabs {
display: flex;
align-items: center;
gap: 4px;
}
.content-nav-tab {
border: none;
background: transparent;
padding: 6px 14px;
font-size: 14px;
font-weight: 500;
color: #606266;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
line-height: 1.4;
}
.content-nav-tab:hover {
color: var(--el-color-primary);
background: #f0f4ff;
}
.content-nav-tab.active {
color: var(--el-color-primary);
background: #ecf5ff;
}
.page-header-card {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.page-header-card-center {
justify-content: center;
}
.page-header-card-center .page-header-main {
flex-direction: column;
align-items: center;
text-align: center;
}
.page-header-main {
display: flex;
align-items: flex-start;
gap: 12px;
min-width: 0;
flex: 1;
}
.page-header-text {
min-width: 0;
}
.page-header-label {
font-size: 12px;
font-weight: 500;
color: #909399;
margin-bottom: 4px;
letter-spacing: 0.02em;
}
.page-header-card .page-hero-title {
font-size: 18px;
margin-bottom: 4px;
}
.page-header-card .page-hero-desc {
font-size: 13px;
}
.page-header-card-center .page-hero-icon {
margin-bottom: 8px;
}
.page-header-card-center .page-hero-title {
font-size: 22px;
}
/* 内容页通用布局 */
.content-page {
padding-bottom: 24px;
animation: fadeIn 0.35s ease;
}
.page-hero-icon {
font-size: 32px;
color: var(--el-color-primary);
flex-shrink: 0;
}
.page-hero-title {
font-size: 18px;
font-weight: 600;
color: #1f2329;
margin: 0 0 4px;
line-height: 1.4;
}
.page-hero-desc {
font-size: 13px;
color: #8f959e;
margin: 0;
line-height: 1.6;
}
.content-section {
background: #fff;
border: 1px solid #eef0f3;
border-radius: 10px;
padding: 20px 24px;
margin-bottom: 16px;
}
.section-title {
margin: 0 0 16px;
padding: 0 0 12px;
border-bottom: 1px solid #f0f2f5;
border-left: none;
background: none;
border-radius: 0;
font-size: 15px;
font-weight: 600;
color: #1f2329;
scroll-margin-top: 20px;
word-break: break-all;
transition: none;
transform: none;
}
.section-title:hover {
background: none;
transform: none;
}
.model-card {
margin-bottom: 16px;
}
.model-card:last-child {
margin-bottom: 0;
}
.model-title {
margin: 0 0 12px;
padding: 8px 12px;
background: #f7f8fa;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
color: #409eff;
font-family: "SF Mono", "Menlo", "Monaco", "Consolas", monospace;
scroll-margin-top: 20px;
word-break: break-all;
}
.basic-type-display {
display: inline-block;
color: #606266;
margin-bottom: 12px;
padding: 6px 12px;
font-size: 13px;
background: #f7f8fa;
border-radius: 6px;
font-family: "SF Mono", "Menlo", "Monaco", "Consolas", monospace;
}
/* API 详情页 */
.api-detail-icon {
font-size: 20px;
color: var(--el-color-primary);
flex-shrink: 0;
margin-top: 2px;
}
.api-action-name {
margin: 0;
font-size: 15px;
font-weight: 600;
color: #1f2329;
line-height: 1.5;
word-break: break-all;
font-family: "SF Mono", "Menlo", "Monaco", "Consolas", monospace;
}
.api-detail-alert {
margin-bottom: 16px;
}
.api-detail-body {
display: flex;
flex-direction: column;
gap: 0;
}
/* README 引导页 */
.readme-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.readme-card {
background: #fff;
border: 1px solid #eef0f3;
border-radius: 10px;
padding: 20px;
transition: box-shadow 0.2s ease, border-color 0.2s ease;
}
.readme-card:hover {
border-color: #d0d7de;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
}
.readme-card-num {
display: inline-block;
font-size: 12px;
font-weight: 700;
color: var(--el-color-primary);
background: #ecf5ff;
padding: 2px 8px;
border-radius: 4px;
margin-bottom: 10px;
}
.readme-card h3 {
margin: 0 0 8px;
padding: 0;
border: none;
background: none;
font-size: 15px;
font-weight: 600;
color: #1f2329;
}
.readme-card p {
margin: 0 0 10px;
font-size: 13px;
color: #8f959e;
line-height: 1.6;
}
.readme-card ul {
margin: 0;
padding-left: 18px;
font-size: 13px;
color: #606266;
line-height: 1.8;
}
.readme-card li {
margin: 4px 0;
}
.tips-card {
border-left: 3px solid var(--el-color-primary);
}
.tips-card-title {
font-size: 14px;
font-weight: 600;
color: #1f2329;
margin-bottom: 12px;
}
.tips-card ul {
margin: 0;
padding-left: 20px;
font-size: 13px;
color: #606266;
line-height: 1.8;
}
.tips-card li {
margin: 6px 0;
}
/* 数据表格 */
.data-table {
width: 100%;
border-radius: 8px;
overflow: hidden;
}
.data-table :deep(.el-table__header th) {
background: #f7f8fa !important;
color: #606266;
font-weight: 600;
font-size: 13px;
}
.data-table :deep(.el-table__body td) {
font-size: 13px;
}
.data-table :deep(.el-table__row:hover > td) {
background: #fafbfc !important;
}
/* 代码示例 */
.code-tabs :deep(.el-tabs__header) {
margin-bottom: 1px;
border-radius: 8px 8px 0 0;
background: #f7f8fa;
padding: 0 20px;
}
.code-tabs :deep(.el-tabs__item) {
padding: 0px 10px;
}
.code-tabs :deep(.el-tabs__content) {
padding: 0;
}
.code-block {
margin: 0;
padding: 16px 20px;
background: #1e1e1e;
color: #d4d4d4;
border-radius: 0 0 8px 8px;
overflow-x: auto;
font-size: 13px;
line-height: 1.6;
font-family: "SF Mono", "Menlo", "Monaco", "Consolas", monospace;
border: none;
box-shadow: none;
}
.code-block:hover {
border: none;
box-shadow: none;
}
.common-info .content-section:last-child {
margin-bottom: 0;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.el-card {
margin: 20px 0;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border: 1px solid #ebeef5;
}
.el-card:hover {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
h2 {
margin-bottom: 20px;
}
h3 {
margin: 20px 0;
padding: 12px 16px;
border-left: 4px solid var(--el-color-primary);
scroll-margin-top: 20px;
word-break: break-all;
background: linear-gradient(90deg, #f0f9ff 0%, transparent 100%);
border-radius: 4px;
font-weight: 600;
color: #303133;
}
pre {
background: linear-gradient(135deg, #f5f7fa 0%, #fafbfc 100%);
padding: 16px;
border-radius: 8px;
overflow-x: auto;
border: 1px solid #e4e7ed;
font-size: 13px;
line-height: 1.6;
}
h4 {
margin: 15px 0;
padding: 10px;
background-color: #f5f7fa;
scroll-margin-top: 20px;
word-break: break-all;
}
/* 对象类型的属性名称样式 */
.el-table .object-field {
color: var(--el-color-primary);
cursor: pointer;
text-decoration: underline;
}
/* 内嵌表格样式 */
.el-table__expanded-cell {
padding: 20px !important;
}
.el-table__expanded-cell .el-table {
margin-bottom: 0;
}
@media (max-width: 768px) {
.content-nav-wrap,
.content-page {
padding-left: 16px;
padding-right: 16px;
}
.content-page {
padding-bottom: 16px;
}
.content-nav-wrap {
padding-top: 12px;
}
.content-nav-bar {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.content-nav-tabs {
width: 100%;
}
.content-nav-tab {
flex: 1;
text-align: center;
}
.page-header-card {
flex-direction: column;
align-items: stretch;
}
.readme-grid {
grid-template-columns: 1fr;
}
.page-header-card-center .page-hero-title {
font-size: 18px;
}
.api-action-name {
font-size: 13px;
}
.content-section {
padding: 16px;
}
}
/* 隐藏滚动条但保持可滚动 */
.hide-scrollbar {
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
overflow-y: auto;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
/* Chrome, Safari and Opera */
}
.type-display {
white-space: nowrap;
}
.type-display a {
color: var(--el-color-primary);
text-decoration: none;
cursor: pointer;
}
.type-display a:hover {
text-decoration: underline;
}
/* 移动端适配样式 */
@media (max-width: 768px) {
.el-container {
height: auto;
min-height: 100vh;
}
.el-main.main-content {
padding: 0 !important;
background: #f7f8fa;
}
.api-detail {
max-width: 100%;
}
pre {
font-size: 12px;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.2rem;
}
h4 {
font-size: 1rem;
}
}
/* 移动端字段列表样式 */
.mobile-field-list {
margin-bottom: 0;
}
.mobile-field-list :deep(.el-collapse) {
border: 1px solid #eef0f3;
border-radius: 8px;
overflow: hidden;
}
.mobile-field-list :deep(.el-collapse-item__header) {
padding: 0 12px;
font-size: 13px;
background: #fafbfc;
}
.mobile-field-list :deep(.el-collapse-item__wrap) {
border-bottom: none;
}
.mobile-field-title {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding-right: 8px;
}
.field-name {
font-weight: 600;
color: #1f2329;
}
.field-required {
color: #f56c6c;
background: #fef0f0;
padding: 1px 6px;
border-radius: 4px;
font-size: 11px;
}
.mobile-field-content {
padding: 12px;
background: #fff;
}
.field-item {
margin-bottom: 10px;
}
.field-item:last-child {
margin-bottom: 0;
}
.field-label {
font-weight: 600;
margin-bottom: 4px;
color: #909399;
font-size: 12px;
}
.field-value {
padding: 2px 0;
color: #303133;
font-size: 13px;
}
/* 目录式导航样式 */
.breadcrumb-nav {
margin: 8px 8px 4px;
padding: 10px 12px;
background: #fff;
border-radius: 8px;
border: 1px solid #eef0f3;
}
.breadcrumb-toolbar {
display: flex;
gap: 4px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f2f5;
}
.breadcrumb-btn {
font-size: 13px;
padding: 4px 8px;
height: auto;
}
.breadcrumb-path {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 2px;
font-size: 12px;
line-height: 1.6;
}
.breadcrumb-tag {
color: #606266;
padding: 1px 4px;
border-radius: 4px;
}
.breadcrumb-clickable {
cursor: pointer;
color: var(--el-color-primary);
transition: background-color 0.2s ease;
}
.breadcrumb-clickable:hover {
background-color: #ecf5ff;
}
.breadcrumb-current {
color: #303133;
font-weight: 500;
}
.breadcrumb-chevron {
font-size: 10px;
color: #c0c4cc;
flex-shrink: 0;
}
/* 导出模块样式 */
.export-drawer :deep(.el-drawer__body) {
padding: 0;
overflow-y: auto;
}
.export-module {
padding: 0 20px 24px;
}
.export-description {
color: #909399;
font-size: 13px;
margin: 0 0 16px;
line-height: 1.5;
}
.quick-export-card {
border-radius: 8px;
margin-bottom: 20px;
border: 1px solid #eef0f3;
}
.quick-export-card :deep(.el-card__header) {
padding: 12px 16px;
background: #fafbfc;
border-bottom: 1px solid #eef0f3;
}
.quick-export-card :deep(.el-card__body) {
padding: 16px;
}
.card-header {
display: flex;
align-items: center;
font-weight: 600;
color: #303133;
}
.quick-export-content {
text-align: center;
padding: 10px 0;
}
.quick-export-desc {
margin-bottom: 20px;
color: #606266;
font-size: 14px;
}
/* 按需导出部分 */
.export-section-title {
margin-bottom: 20px;
padding-bottom: 15px;
}
.export-section-title h3 {
margin-bottom: 8px;
color: #303133;
font-size: 18px;
}
.section-desc {
color: #909399;
font-size: 13px;
margin: 0;
}
.export-content {
background: #fafbfc;
border-radius: 8px;
padding: 16px;
border: 1px solid #eef0f3;
}
/* 导出对话框样式 */
.export-dialog-content {
max-height: 60vh;
overflow-y: auto;
}
.export-options {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e4e7ed;
}
.export-select-header {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e4e7ed;
}
.select-actions {
display: flex;
align-items: center;
gap: 15px;
margin-top: 10px;
}
.selected-count {
margin-left: auto;
color: var(--el-color-primary);
font-weight: 600;
}
.export-api-list {
max-height: calc(100vh - 420px);
min-height: 200px;
overflow-y: auto;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
background: #fff;
}
.export-api-item {
padding: 8px 0;
border-bottom: 1px solid #f5f7fa;
}
.export-api-item:last-child {
border-bottom: none;
}
.api-action {
font-family: "Courier New", monospace;
color: #606266;
font-size: 13px;
}
.export-api-item:hover {
background-color: #f5f7fa;
border-radius: 4px;
padding-left: 8px;
padding-right: 8px;
}
.export-actions {
text-align: center;
padding-top: 20px;
border-top: 1px solid #e4e7ed;
}
/* 导出树样式 */
.export-tree-node {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.api-icon,
.folder-icon {
font-size: 16px;
}
.node-label {
flex: 1;
}
.export-api-list :deep(.el-tree-node__content) {
height: 32px;
line-height: 32px;
}
.export-api-list :deep(.el-tree-node__label) {
font-size: 13px;
}
.export-api-list :deep(.el-checkbox) {
margin-right: 8px;
}
/* 全屏错误状态样式 */
.fullscreen-error {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
width: 100%;
}
/* 禁用 loading 的淡入淡出动画,让初始显示时立即显示 */
:deep(.el-loading-mask) {
transition: none !important;
}
:deep(.el-loading-mask.is-fullscreen) {
transition: none !important;
}
:deep(.el-table__inner-wrapper:before) {
background-color: transparent;
}
:deep(.el-collapse-item__wrap) {
border-bottom: none;
}
</style>