优化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> </AdminRoute>
} }
/> />
<Route <Route path="open-api" element={<OpenApiDocs />} />
path="open-api"
element={
<AdminRoute>
<OpenApiDocs />
</AdminRoute>
}
/>
<Route <Route
path="api-debug" path="api-debug"
element={ element={
+5 -4
View File
@@ -862,10 +862,11 @@ a:hover {
} }
.api-docs__inner { .api-docs__inner {
max-width: 1160px; width: 100%;
max-width: 1600px;
min-height: 100%; min-height: 100%;
margin: 0 auto 0 0; margin: 0;
padding: 32px 40px 64px; padding: 32px 48px 64px;
} }
.api-docs__h2 { .api-docs__h2 {
@@ -877,7 +878,7 @@ a:hover {
} }
.api-docs__summary { .api-docs__summary {
max-width: 1080px; width: 100%;
margin: 0 0 16px !important; margin: 0 0 16px !important;
color: #475569 !important; color: #475569 !important;
line-height: 1.7; line-height: 1.7;
+1
View File
@@ -143,6 +143,7 @@ const merchantSections: SidebarSection[] = [
children: [ children: [
{ key: 'api-keys', label: 'API 密钥', path: '/merchant-api-keys' }, { key: 'api-keys', label: 'API 密钥', path: '/merchant-api-keys' },
{ key: 'api-callbacks', label: '回调订阅', path: '/merchant-callbacks' }, { key: 'api-callbacks', label: '回调订阅', path: '/merchant-callbacks' },
{ key: 'api-docs', label: '对接文档', path: '/open-api' },
], ],
}, },
] ]
+172 -31
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { import {
Button, Button,
Card, Card,
@@ -20,16 +20,21 @@ import {
message, message,
} from 'antd' } from 'antd'
import { import {
ApiOutlined,
ClockCircleOutlined, ClockCircleOutlined,
CodeOutlined,
CopyOutlined, CopyOutlined,
DollarOutlined, DollarOutlined,
FileTextOutlined,
KeyOutlined,
LinkOutlined, LinkOutlined,
PlusOutlined, PlusOutlined,
ReloadOutlined, ReloadOutlined,
SafetyCertificateOutlined,
SendOutlined,
WalletOutlined, WalletOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import type { ColumnsType } from 'antd/es/table' import type { ColumnsType } from 'antd/es/table'
import { useAuth } from '../store/auth'
import { merchantApi } from '../api' import { merchantApi } from '../api'
import { formatDateTime } from '../utils/time' import { formatDateTime } from '../utils/time'
import { PageHeader } from '../components/PageHeader' import { PageHeader } from '../components/PageHeader'
@@ -93,7 +98,9 @@ interface MerchantCenterProps {
} }
export default function MerchantCenter({ fixedTab, title = '商户中心' }: MerchantCenterProps = {}) { export default function MerchantCenter({ fixedTab, title = '商户中心' }: MerchantCenterProps = {}) {
const { isAdmin } = useAuth()
const location = useLocation() const location = useLocation()
const navigate = useNavigate()
const routeTab = useMemo(() => fixedTab ?? tabFromSearch(location.search), [fixedTab, location.search]) const routeTab = useMemo(() => fixedTab ?? tabFromSearch(location.search), [fixedTab, location.search])
const [merchant, setMerchant] = useState<Merchant | null>(null) const [merchant, setMerchant] = useState<Merchant | null>(null)
const [merchantRole, setMerchantRole] = useState<MerchantMember['role']>() const [merchantRole, setMerchantRole] = useState<MerchantMember['role']>()
@@ -790,25 +797,118 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
) )
const apiKeyContent = ( const apiKeyContent = (
<Space direction="vertical" style={{ width: '100%' }} size="middle"> <Space direction="vertical" style={{ width: '100%' }} size="large">
<Space style={{ width: '100%', justifyContent: 'space-between' }}> <Card
<Typography.Text type="secondary"> API Secret {apiClientMax} </Typography.Text> size="small"
{canManage && <Button type="primary" icon={<ApiOutlined />} disabled={apiClients.length >= apiClientMax} onClick={() => { style={{ background: '#ffffff', border: '1px solid #e2e8f0', borderRadius: 10 }}
apiClientForm.resetFields() title={
apiClientForm.setFieldsValue({ signature_version: 'v1', scopes: ['products:read', 'orders:read', 'orders:write'] }) <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12 }}>
setApiClientOpen(true) <Space size={8}>
}}>{apiClients.length}/{apiClientMax}</Button>} <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>
<Table rowKey="id" loading={loading} columns={apiClientColumns} dataSource={apiClients} tableLayout="fixed" scroll={{ x: 1050 }} /> <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> </Space>
) )
const callbackContent = ( const callbackContent = (
<Space direction="vertical" style={{ width: '100%' }} size="middle"> <Space direction="vertical" style={{ width: '100%' }} size="large">
<Typography.Text type="secondary"> <Card
outbox 退 16 size="small"
</Typography.Text> style={{ background: '#ffffff', border: '1px solid #e2e8f0', borderRadius: 10 }}
<Card size="small" loading={loading}> 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
form={callbackForm} form={callbackForm}
layout="vertical" layout="vertical"
@@ -816,34 +916,75 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
onFinish={submitCallback} onFinish={submitCallback}
initialValues={defaultCallbackFormValues()} initialValues={defaultCallbackFormValues()}
> >
<Form.Item name="url" label="回调 URL" rules={[{ required: true, message: '请填写回调 URL' }]}> <Form.Item
<Input placeholder="https://example.com/callback" /> 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>
<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>
<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> </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)}` : '尚未保存回调配置'} {callback?.updated_at ? `上次保存:${formatDateTime(callback.updated_at)}` : '尚未保存回调配置'}
</Typography.Text> </Typography.Text>
<Space>
<Space size={12}>
{canManage && ( {canManage && (
<Popconfirm <Popconfirm
title="重置回调密钥" title="确认重置回调 Secret"
description="重置后旧密钥立即失效,平台将用新密钥签名推送,新密钥仅展示一次。" description="重置后旧 Secret 立即失效,平台将使用新 Secret 进行 HMAC 签名推送,新 Secret 仅展示一次。"
okText="重置" okText="确认重置"
okButtonProps={{ danger: true }} okButtonProps={{ danger: true }}
onConfirm={rotateCallbackSecret} onConfirm={rotateCallbackSecret}
> >
<Button></Button> <Button danger> Secret</Button>
</Popconfirm> </Popconfirm>
)} )}
{canManage && <Button type="primary" htmlType="submit"></Button>} {canManage && (
</Space> <Button type="primary" htmlType="submit">
</Button>
)}
</Space> </Space>
</div>
</Form> </Form>
</Card> </Card>
</Space> </Space>
+5 -6
View File
@@ -335,36 +335,35 @@ function OverviewSection() {
pagination={false} pagination={false}
rowKey="mode" rowKey="mode"
dataSource={deliveryModes} dataSource={deliveryModes}
scroll={{ x: 1080 }}
columns={[ columns={[
{ {
title: '方式', title: '方式',
dataIndex: 'mode', dataIndex: 'mode',
width: 140, width: '14%',
render: (v: string) => <span style={{ fontWeight: 700, whiteSpace: 'nowrap', color: '#0f172a' }}>{v}</span>, render: (v: string) => <span style={{ fontWeight: 700, whiteSpace: 'nowrap', color: '#0f172a' }}>{v}</span>,
}, },
{ {
title: '适用场景', title: '适用场景',
dataIndex: 'scene', dataIndex: 'scene',
width: 280, width: '28%',
render: (v: string) => <span style={{ fontSize: 13, lineHeight: 1.6, display: 'block', color: '#334155' }}>{v}</span>, render: (v: string) => <span style={{ fontSize: 13, lineHeight: 1.6, display: 'block', color: '#334155' }}>{v}</span>,
}, },
{ {
title: '接口顺序', title: '接口顺序',
dataIndex: 'steps', dataIndex: 'steps',
width: 240, width: '22%',
render: (v: string) => <pre className="api-docs__pre" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{v}</pre>, render: (v: string) => <pre className="api-docs__pre" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{v}</pre>,
}, },
{ {
title: '相关接口', title: '相关接口',
dataIndex: 'interfaces', dataIndex: 'interfaces',
width: 260, width: '22%',
render: (v: string) => <pre className="api-docs__pre" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{v}</pre>, render: (v: string) => <pre className="api-docs__pre" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{v}</pre>,
}, },
{ {
title: '建议', title: '建议',
dataIndex: 'recommend', dataIndex: 'recommend',
width: 180, width: '14%',
render: (v: string) => <span style={{ fontSize: 13, lineHeight: 1.5, display: 'block', color: '#475569' }}>{v}</span>, render: (v: string) => <span style={{ fontSize: 13, lineHeight: 1.5, display: 'block', color: '#475569' }}>{v}</span>,
}, },
]} ]}