优化api文档ui

This commit is contained in:
yml2213
2026-08-04 14:12:43 +08:00
parent ca58c2bcaa
commit 6404b10626
5 changed files with 185 additions and 50 deletions
+1 -8
View File
@@ -56,14 +56,7 @@ function AppRoutes() {
</AdminRoute>
}
/>
<Route
path="open-api"
element={
<AdminRoute>
<OpenApiDocs />
</AdminRoute>
}
/>
<Route path="open-api" element={<OpenApiDocs />} />
<Route
path="api-debug"
element={
+5 -4
View File
@@ -862,10 +862,11 @@ a:hover {
}
.api-docs__inner {
max-width: 1160px;
width: 100%;
max-width: 1600px;
min-height: 100%;
margin: 0 auto 0 0;
padding: 32px 40px 64px;
margin: 0;
padding: 32px 48px 64px;
}
.api-docs__h2 {
@@ -877,7 +878,7 @@ a:hover {
}
.api-docs__summary {
max-width: 1080px;
width: 100%;
margin: 0 0 16px !important;
color: #475569 !important;
line-height: 1.7;
+1
View File
@@ -143,6 +143,7 @@ const merchantSections: SidebarSection[] = [
children: [
{ key: 'api-keys', label: 'API 密钥', path: '/merchant-api-keys' },
{ key: 'api-callbacks', label: '回调订阅', path: '/merchant-callbacks' },
{ key: 'api-docs', label: '对接文档', path: '/open-api' },
],
},
]
+173 -32
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { useLocation, useNavigate } from 'react-router-dom'
import {
Button,
Card,
@@ -20,16 +20,21 @@ import {
message,
} from 'antd'
import {
ApiOutlined,
ClockCircleOutlined,
CodeOutlined,
CopyOutlined,
DollarOutlined,
FileTextOutlined,
KeyOutlined,
LinkOutlined,
PlusOutlined,
ReloadOutlined,
SafetyCertificateOutlined,
SendOutlined,
WalletOutlined,
} from '@ant-design/icons'
import type { ColumnsType } from 'antd/es/table'
import { useAuth } from '../store/auth'
import { merchantApi } from '../api'
import { formatDateTime } from '../utils/time'
import { PageHeader } from '../components/PageHeader'
@@ -93,7 +98,9 @@ interface MerchantCenterProps {
}
export default function MerchantCenter({ fixedTab, title = '商户中心' }: MerchantCenterProps = {}) {
const { isAdmin } = useAuth()
const location = useLocation()
const navigate = useNavigate()
const routeTab = useMemo(() => fixedTab ?? tabFromSearch(location.search), [fixedTab, location.search])
const [merchant, setMerchant] = useState<Merchant | null>(null)
const [merchantRole, setMerchantRole] = useState<MerchantMember['role']>()
@@ -790,25 +797,118 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
)
const apiKeyContent = (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"> API Secret {apiClientMax} </Typography.Text>
{canManage && <Button type="primary" icon={<ApiOutlined />} disabled={apiClients.length >= apiClientMax} onClick={() => {
apiClientForm.resetFields()
apiClientForm.setFieldsValue({ signature_version: 'v1', scopes: ['products:read', 'orders:read', 'orders:write'] })
setApiClientOpen(true)
}}>{apiClients.length}/{apiClientMax}</Button>}
</Space>
<Table rowKey="id" loading={loading} columns={apiClientColumns} dataSource={apiClients} tableLayout="fixed" scroll={{ x: 1050 }} />
<Space direction="vertical" style={{ width: '100%' }} size="large">
<Card
size="small"
style={{ background: '#ffffff', border: '1px solid #e2e8f0', borderRadius: 10 }}
title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12 }}>
<Space size={8}>
<KeyOutlined style={{ color: '#2563eb', fontSize: 16 }} />
<span style={{ fontWeight: 700, color: '#0f172a' }}>API </span>
<Tag color="blue" style={{ borderRadius: 10, padding: '0 8px', fontWeight: 600 }}>
{apiClients.length} / {apiClientMax}
</Tag>
</Space>
<Space size={8}>
<Button icon={<FileTextOutlined />} onClick={() => navigate('/open-api')}>
</Button>
{isAdmin && (
<Button icon={<CodeOutlined />} onClick={() => navigate('/api-debug')}>
</Button>
)}
{canManage && (
<Button
type="primary"
icon={<PlusOutlined />}
disabled={apiClients.length >= apiClientMax}
onClick={() => {
apiClientForm.resetFields()
apiClientForm.setFieldsValue({
signature_version: 'v1',
scopes: ['products:read', 'orders:read', 'orders:write'],
})
setApiClientOpen(true)
}}
>
API
</Button>
)}
</Space>
</div>
}
>
<div style={{ background: '#f8fafc', padding: '10px 14px', borderRadius: 8, marginBottom: 16, border: '1px solid #e2e8f0' }}>
<Typography.Text type="secondary" style={{ fontSize: 12.5 }}>
API () HMAC-SHA256 App Secret
</Typography.Text>
</div>
<Table
rowKey="id"
loading={loading}
columns={apiClientColumns}
dataSource={apiClients}
scroll={{ x: 1150 }}
pagination={false}
/>
</Card>
</Space>
)
const callbackContent = (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Typography.Text type="secondary">
outbox 退 16
</Typography.Text>
<Card size="small" loading={loading}>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<Card
size="small"
style={{ background: '#ffffff', border: '1px solid #e2e8f0', borderRadius: 10 }}
title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12 }}>
<Space size={8}>
<SendOutlined style={{ color: '#2563eb', fontSize: 16 }} />
<span style={{ fontWeight: 700, color: '#0f172a' }}>Webhook </span>
{callback?.status === 'active' ? (
<span className="status-tag status-tag--green">
<span className="status-dot"></span>
</span>
) : (
<span className="status-tag status-tag--gray">
<span className="status-dot"></span>
</span>
)}
</Space>
<Tag color="cyan" style={{ borderRadius: 6, fontWeight: 500 }}>
<SafetyCertificateOutlined style={{ marginRight: 4 }} /> HMAC-SHA256
</Tag>
</div>
}
>
<div style={{ background: '#f8fafc', padding: '12px 16px', borderRadius: 8, marginBottom: 20, border: '1px solid #e2e8f0' }}>
<Row gutter={[16, 8]}>
<Col xs={24} sm={8}>
<div style={{ fontSize: 12, color: '#64748b' }}></div>
<div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', marginTop: 2 }}>
Outbox + 退 ( 16 )
</div>
</Col>
<Col xs={24} sm={8}>
<div style={{ fontSize: 12, color: '#64748b' }}> Header</div>
<div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', marginTop: 2 }}>
X-Signature (HMAC-SHA256)
</div>
</Col>
<Col xs={24} sm={8}>
<div style={{ fontSize: 12, color: '#64748b' }}></div>
<div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', marginTop: 2 }}>
5000 ms
</div>
</Col>
</Row>
</div>
<Form
form={callbackForm}
layout="vertical"
@@ -816,34 +916,75 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
onFinish={submitCallback}
initialValues={defaultCallbackFormValues()}
>
<Form.Item name="url" label="回调 URL" rules={[{ required: true, message: '请填写回调 URL' }]}>
<Input placeholder="https://example.com/callback" />
<Form.Item
name="url"
label="回调接收端 URL"
rules={[
{ required: true, message: '请填写回调 URL' },
{ type: 'url', message: '请填写正确的 HTTP(S) 地址' },
]}
>
<Input
prefix={<LinkOutlined style={{ color: '#94a3b8' }} />}
placeholder="https://your-domain.com/api/v1/webhook"
allowClear
/>
</Form.Item>
<Form.Item name="events" label="事件" rules={[{ required: true, message: '请选择回调事件' }]}>
<Select mode="multiple" options={eventOptions} />
<Form.Item name="events" label="订阅事件类型" rules={[{ required: true, message: '请选择至少一个回调事件' }]}>
<Select
mode="multiple"
placeholder="请选择订阅的事件列表"
options={eventOptions}
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item name="status" label="状态" rules={[{ required: true }]}>
<Select options={[{ value: 'active', label: '启用' }, { value: 'disabled', label: '禁用' }]} />
<Form.Item name="status" label="回调状态" rules={[{ required: true }]}>
<Select
options={[
{ value: 'active', label: '启用回调推送' },
{ value: 'disabled', label: '禁用回调推送' },
]}
style={{ width: 200 }}
/>
</Form.Item>
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary">
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: 16,
borderTop: '1px solid #f1f5f9',
marginTop: 20,
flexWrap: 'wrap',
gap: 12,
}}
>
<Typography.Text type="secondary" style={{ fontSize: 12.5 }}>
{callback?.updated_at ? `上次保存:${formatDateTime(callback.updated_at)}` : '尚未保存回调配置'}
</Typography.Text>
<Space>
<Space size={12}>
{canManage && (
<Popconfirm
title="重置回调密钥"
description="重置后旧密钥立即失效,平台将用新密钥签名推送,新密钥仅展示一次。"
okText="重置"
title="确认重置回调 Secret"
description="重置后旧 Secret 立即失效,平台将使用新 Secret 进行 HMAC 签名推送,新 Secret 仅展示一次。"
okText="确认重置"
okButtonProps={{ danger: true }}
onConfirm={rotateCallbackSecret}
>
<Button></Button>
<Button danger> Secret</Button>
</Popconfirm>
)}
{canManage && <Button type="primary" htmlType="submit"></Button>}
{canManage && (
<Button type="primary" htmlType="submit">
</Button>
)}
</Space>
</Space>
</div>
</Form>
</Card>
</Space>
+5 -6
View File
@@ -335,36 +335,35 @@ function OverviewSection() {
pagination={false}
rowKey="mode"
dataSource={deliveryModes}
scroll={{ x: 1080 }}
columns={[
{
title: '方式',
dataIndex: 'mode',
width: 140,
width: '14%',
render: (v: string) => <span style={{ fontWeight: 700, whiteSpace: 'nowrap', color: '#0f172a' }}>{v}</span>,
},
{
title: '适用场景',
dataIndex: 'scene',
width: 280,
width: '28%',
render: (v: string) => <span style={{ fontSize: 13, lineHeight: 1.6, display: 'block', color: '#334155' }}>{v}</span>,
},
{
title: '接口顺序',
dataIndex: 'steps',
width: 240,
width: '22%',
render: (v: string) => <pre className="api-docs__pre" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{v}</pre>,
},
{
title: '相关接口',
dataIndex: 'interfaces',
width: 260,
width: '22%',
render: (v: string) => <pre className="api-docs__pre" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{v}</pre>,
},
{
title: '建议',
dataIndex: 'recommend',
width: 180,
width: '14%',
render: (v: string) => <span style={{ fontSize: 13, lineHeight: 1.5, display: 'block', color: '#475569' }}>{v}</span>,
},
]}