优化api文档ui
This commit is contained in:
@@ -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={
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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 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>
|
||||
<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>
|
||||
)
|
||||
|
||||
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>}
|
||||
</Space>
|
||||
{canManage && (
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存回调配置
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</Space>
|
||||
|
||||
@@ -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>,
|
||||
},
|
||||
]}
|
||||
|
||||
Reference in New Issue
Block a user