优化开放 API 文档界面

This commit is contained in:
yml2213
2026-07-31 14:16:29 +08:00
parent 949a0eb8f7
commit 371481126e
6 changed files with 1516 additions and 292 deletions
+228 -97
View File
@@ -1,7 +1,8 @@
import type { CSSProperties } from 'react'
import { Alert, Card, Descriptions, Space, Table, Tag, Typography } from 'antd'
import type { ReactNode } from 'react'
import { Space, Table, Tag, Typography } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import type { EndpointSpec, ParamSpec } from './types'
import { errorCodes } from './endpoints'
const { Text, Paragraph } = Typography
@@ -13,119 +14,235 @@ const methodColor: Record<string, string> = {
DELETE: 'red',
}
const preStyle: CSSProperties = {
margin: 0,
padding: 12,
background: '#f6f8fa',
borderRadius: 6,
fontSize: 12.5,
lineHeight: 1.6,
overflow: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
const methodText: Record<string, string> = {
GET: '#1677ff',
POST: '#389e0d',
PUT: '#d46b08',
PATCH: '#d48806',
DELETE: '#cf1322',
}
function FieldName({ children }: { children: string }) {
return <span className="api-docs__field">{children}</span>
}
function InlineExample({ children }: { children: string }) {
return <span className="api-docs__inline-example">{children}</span>
}
function RequiredMark({ required }: { required?: boolean }) {
return required ? <span className="api-docs__required">是</span> : <Text type="secondary">否</Text>
}
// 轻量 JSON 语法高亮:把 JSON 字符串渲染成带颜色 span 的 HTML。
function highlightJson(json: string): string {
const esc = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
return esc.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+\.?\d*([eE][+-]?\d+)?)/g,
(match) => {
let cls = 'json-number'
if (/^"/.test(match)) {
cls = /:$/.test(match) ? 'json-key' : 'json-string'
} else if (/true|false/.test(match)) {
cls = 'json-boolean'
} else if (/null/.test(match)) {
cls = 'json-null'
}
return `<span class="${cls}">${match}</span>`
},
)
}
function JsonBlock({ code }: { code: string }) {
return <pre className="api-docs__code" dangerouslySetInnerHTML={{ __html: highlightJson(code) }} />
}
function MethodPathBlock({ method, path }: { method: string; path: string }) {
return (
<pre className="api-docs__endpoint-line">
<span className="api-docs__method" style={{ color: methodText[method] }}>{method}</span>
{' '}
<span className="api-docs__path">{path}</span>
</pre>
)
}
// 请求参数表格:参数名 | 类型 | 必填 | 说明(示例并入说明列)
const paramColumns: ColumnsType<ParamSpec> = [
{
title: '字段',
title: '参数名',
dataIndex: 'name',
width: 200,
render: (v: string) => <Text code style={{ whiteSpace: 'nowrap' }}>{v}</Text>,
},
{
title: '必填',
dataIndex: 'required',
width: 70,
render: (v?: boolean) =>
v ? <Tag color="red">是</Tag> : <Tag>否</Tag>,
width: 260,
render: (v: string) => <FieldName>{v}</FieldName>,
},
{
title: '类型',
dataIndex: 'type',
width: 180,
render: (v: string) => <Text type="secondary" style={{ fontSize: 12 }}>{v}</Text>,
width: 140,
render: (v: string) => <Text className="api-docs__type">{v}</Text>,
},
{ title: '说明', dataIndex: 'desc' },
{
title: '示例',
dataIndex: 'example',
width: 160,
render: (v?: string) =>
v ? <Text code style={{ fontSize: 12 }}>{v}</Text> : null,
title: '必填',
dataIndex: 'required',
width: 72,
render: (v?: boolean) => <RequiredMark required={v} />,
},
{
title: '说明',
render: (_: unknown, record: ParamSpec) => (
<span>
{record.desc}
{record.example ? (
<>
{' '}
<Text type="secondary" style={{ fontSize: 12 }}>示例</Text>{' '}
<InlineExample>{record.example}</InlineExample>
</>
) : null}
</span>
),
},
]
// 响应字段表格:字段 | 类型 | 说明(无必填列)
const responseColumns: ColumnsType<ParamSpec> = [
{
title: '字段',
dataIndex: 'name',
width: 280,
render: (v: string) => <FieldName>{v}</FieldName>,
},
{
title: '类型',
dataIndex: 'type',
width: 140,
render: (v: string) => <Text className="api-docs__type">{v}</Text>,
},
{ title: '说明', dataIndex: 'desc' },
]
function SectionTitle({ id, children }: { id: string; children: ReactNode }) {
return (
<h4 id={id} className="doc-section-title">
{children}
</h4>
)
}
function SubTitle({ children }: { children: ReactNode }) {
return <h5 className="doc-subtitle">{children}</h5>
}
function ParamTable({ title, params }: { title: string; params: ParamSpec[] }) {
return (
<Card size="small" title={title} style={{ width: '100%' }}>
<div className="api-docs__param-group">
<SubTitle>{title}</SubTitle>
<Table
className="api-docs__table"
size="small"
pagination={false}
rowKey="name"
dataSource={params}
columns={paramColumns}
/>
</Card>
</div>
)
}
function CodeBlock({ title, code }: { title: string; code: string }) {
return (
<Card size="small" title={title} style={{ width: '100%' }}>
<pre style={preStyle}>{code}</pre>
</Card>
)
}
/** 通用接口渲染:方法+路径+权限 → 参数表 → 请求/响应示例 → 字段说明 → 注意事项。 */
/** 通用接口渲染:摘要 → 接口地址 → 请求方式 → 请求参数 → 响应参数 → 状态码 → 响应示例 → 注意事项。 */
export default function EndpointDoc({ spec }: { spec: EndpointSpec }) {
const prefix = `ep-${spec.key}`
const hasParams =
!!spec.pathParams?.length || !!spec.queryParams?.length || !!spec.bodyParams?.length
return (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card size="small">
<Space wrap size="middle" align="center">
<Tag color={methodColor[spec.method]} style={{ margin: 0, fontSize: 13, fontWeight: 700 }}>
{spec.method}
</Tag>
<Text code copyable style={{ fontSize: 14 }}>
{spec.path}
</Text>
<Tag color="purple">{spec.scope}</Tag>
</Space>
<Paragraph type="secondary" style={{ margin: '8px 0 0' }}>
{spec.summary}
</Paragraph>
</Card>
{spec.pathParams?.length ? <ParamTable title="路径参数" params={spec.pathParams} /> : null}
{spec.queryParams?.length ? <ParamTable title="查询参数" params={spec.queryParams} /> : null}
{spec.bodyParams?.length ? <ParamTable title="请求体参数" params={spec.bodyParams} /> : null}
{spec.requestExample ? <CodeBlock title="请求示例" code={spec.requestExample} /> : null}
<CodeBlock title="响应示例" code={spec.responseExample} />
{spec.responseFields?.length ? (
<ParamTable title="响应字段说明" params={spec.responseFields} />
) : null}
<Space className="api-docs__endpoint" direction="vertical" size={20} style={{ width: '100%' }}>
<Paragraph className="api-docs__summary" type="secondary">
{spec.summary}
</Paragraph>
{spec.notes?.length ? (
<Alert
type="warning"
showIcon
message="注意事项"
description={
<ul style={{ margin: 0, paddingLeft: 20 }}>
{spec.notes.map((n, i) => (
<li key={i}>{n}</li>
))}
</ul>
}
/>
<div className="api-docs__notice">
<span className="api-docs__notice-label">限制:</span>
{spec.notes.join(';')}
</div>
) : null}
<section>
<SectionTitle id={`${prefix}-address`}>接口地址</SectionTitle>
<MethodPathBlock method={spec.method} path={spec.path} />
</section>
<section>
<SectionTitle id={`${prefix}-method`}>请求方式</SectionTitle>
<Space size="middle" align="center">
<Text code style={{ color: methodText[spec.method], fontWeight: 700 }}>
{spec.method}
</Text>
<Tag color={methodColor[spec.method]} style={{ margin: 0 }}>
{spec.scope}
</Tag>
</Space>
</section>
<section>
<SectionTitle id={`${prefix}-params`}>请求参数</SectionTitle>
{spec.pathParams?.length ? <ParamTable title="路径参数" params={spec.pathParams} /> : null}
{spec.queryParams?.length ? <ParamTable title="查询参数" params={spec.queryParams} /> : null}
{spec.bodyParams?.length ? <ParamTable title="请求体参数" params={spec.bodyParams} /> : null}
{!hasParams ? <Text type="secondary">该接口无请求参数。</Text> : null}
{spec.requestExample ? (
<>
<SubTitle>请求示例</SubTitle>
<JsonBlock code={spec.requestExample} />
</>
) : null}
</section>
<section>
<SectionTitle id={`${prefix}-response`}>响应参数</SectionTitle>
{spec.responseFields?.length ? (
<Table
className="api-docs__table"
size="small"
pagination={false}
rowKey="name"
dataSource={spec.responseFields}
columns={responseColumns}
/>
) : (
<Text type="secondary">详见下方响应示例。</Text>
)}
</section>
<section>
<SectionTitle id={`${prefix}-status`}>状态码</SectionTitle>
<Table
className="api-docs__table"
size="small"
pagination={false}
rowKey="name"
dataSource={errorCodes}
columns={[
{
title: 'HTTP 状态码',
dataIndex: 'name',
width: 120,
render: (v) => <Text code>{v}</Text>,
},
{ title: '含义', dataIndex: 'desc' },
]}
/>
</section>
<section>
<SectionTitle id={`${prefix}-example`}>响应示例</SectionTitle>
<JsonBlock code={spec.responseExample} />
</section>
</Space>
)
}
/** 通用响应说明卡片,供页面顶部展示统一响应格式与错误码。 */
/** 通用响应说明,供「概述」展示统一响应格式与错误码。 */
export function CommonResponseDoc({
fields,
errors,
@@ -134,31 +251,45 @@ export function CommonResponseDoc({
errors: ParamSpec[]
}) {
return (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Card size="small" title="统一响应格式">
<Descriptions size="small" column={1} bordered>
<Descriptions.Item label="结构">
<Text code>{`{ "code": 0, "message": "ok", "data": {} }`}</Text>
</Descriptions.Item>
{fields.map((f) => (
<Descriptions.Item key={f.name} label={<Text code>{f.name}</Text>}>
{f.desc}
</Descriptions.Item>
))}
</Descriptions>
</Card>
<Card size="small" title="错误码">
<Space direction="vertical" size={24} style={{ width: '100%' }}>
<section>
<SectionTitle id="common-response">统一响应格式</SectionTitle>
<JsonBlock
code={`{
"code": 0,
"message": "ok",
"data": {}
}`}
/>
<Table
className="api-docs__table"
size="small"
pagination={false}
rowKey="name"
dataSource={fields}
columns={responseColumns}
style={{ marginTop: 14 }}
/>
</section>
<section>
<SectionTitle id="error-codes">错误码</SectionTitle>
<Table
className="api-docs__table"
size="small"
pagination={false}
rowKey="name"
dataSource={errors}
columns={[
{ title: 'HTTP/code', dataIndex: 'name', width: 120, render: (v) => <Text code>{v}</Text> },
{
title: 'HTTP/code',
dataIndex: 'name',
width: 120,
render: (v) => <Text code>{v}</Text>,
},
{ title: '含义', dataIndex: 'desc' },
]}
/>
</Card>
</section>
</Space>
)
}
+9
View File
@@ -152,6 +152,7 @@ export const endpoints: EndpointSpec[] = [
key: 'list-products',
method: 'GET',
path: '/api/client/v1/products',
title: '商品列表',
summary: '查询商户已授权且上架的商品列表',
scope: 'products:read',
queryParams: [
@@ -196,6 +197,7 @@ export const endpoints: EndpointSpec[] = [
key: 'create-order',
method: 'POST',
path: '/api/client/v1/orders',
title: '创建订单',
summary: '幂等创建订单并扣款(成功返回 201,重复请求返回 200 且 idempotent=true)',
scope: 'orders:write',
bodyParams: [
@@ -240,6 +242,7 @@ export const endpoints: EndpointSpec[] = [
key: 'query-order',
method: 'GET',
path: '/api/client/v1/orders/{order_no}',
title: '查询订单',
summary: '查询订单状态与详情',
scope: 'orders:read',
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
@@ -250,6 +253,7 @@ export const endpoints: EndpointSpec[] = [
key: 'cancel-order',
method: 'POST',
path: '/api/client/v1/orders/{order_no}/cancel',
title: '取消订单',
summary: '取消未履约订单并退款(仅 pending/failed 可取消)',
scope: 'orders:write',
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
@@ -266,6 +270,7 @@ export const endpoints: EndpointSpec[] = [
key: 'delivery-link',
method: 'GET',
path: '/api/client/v1/orders/{order_no}/delivery-link',
title: '获取发货链接',
summary: '获取签名发货链接,适合直接跳转平台 Web 发货页',
scope: 'orders:read + fulfillment:read',
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
@@ -286,6 +291,7 @@ export const endpoints: EndpointSpec[] = [
key: 'delivery-order',
method: 'GET',
path: '/api/client/v1/orders/{order_no}/delivery',
title: '查询发货数据',
summary: '查询发货页结构化数据,适合商户自建发货界面',
scope: 'orders:read + fulfillment:read',
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
@@ -316,6 +322,7 @@ export const endpoints: EndpointSpec[] = [
key: 'delivery-bind',
method: 'POST',
path: '/api/client/v1/orders/{order_no}/delivery/bind',
title: '发起绑定',
summary: '发起绑定,返回绑定跳转链接与二维码地址',
scope: 'orders:write',
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
@@ -340,6 +347,7 @@ export const endpoints: EndpointSpec[] = [
key: 'delivery-submit',
method: 'POST',
path: '/api/client/v1/orders/{order_no}/delivery/submit',
title: '提交发货',
summary: '提交发货到上游并进入履约中状态',
scope: 'orders:write',
pathParams: [{ name: 'order_no', type: 'string', required: true, desc: '平台订单号', example: 'FO20260730000123' }],
@@ -369,6 +377,7 @@ export const endpoints: EndpointSpec[] = [
key: 'get-wallet',
method: 'GET',
path: '/api/client/v1/wallet',
title: '余额查询',
summary: '查询商户钱包余额',
scope: 'wallet:read',
responseExample: `{
+12
View File
@@ -2,6 +2,16 @@
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
/** 每个接口下固定的子小节,供目录(TOC)构建与锚点跳转使用。 */
export const endpointSections = [
{ id: 'address', label: '接口地址' },
{ id: 'method', label: '请求方式' },
{ id: 'params', label: '请求参数' },
{ id: 'response', label: '响应参数' },
{ id: 'status', label: '状态码' },
{ id: 'example', label: '响应示例' },
] as const
/** 单个接口的完整描述。 */
export interface EndpointSpec {
/** 唯一 key,用于折叠面板与锚点 */
@@ -10,6 +20,8 @@ export interface EndpointSpec {
method: HttpMethod
/** 接口路径,路径参数用 {name} 表示 */
path: string
/** 中文接口名,用于目录与标题展示 */
title: string
/** 一句话说明 */
summary: string
/** 调用所需权限 scope */