优化开放 API 文档界面
This commit is contained in:
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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: `{
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user