优化api文档ui
This commit is contained in:
@@ -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={
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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 }} />
|
||||||
</Space>
|
<span style={{ fontWeight: 700, color: '#0f172a' }}>API 密钥与开放接口鉴权</span>
|
||||||
<Table rowKey="id" loading={loading} columns={apiClientColumns} dataSource={apiClients} tableLayout="fixed" scroll={{ x: 1050 }} />
|
<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>
|
</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 && (
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
保存回调配置
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -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>,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
Reference in New Issue
Block a user