优化开放 API 文档界面

This commit is contained in:
yml2213
2026-07-31 14:16:29 +08:00
parent 949a0eb8f7
commit 371481126e
6 changed files with 1516 additions and 292 deletions
+214 -69
View File
@@ -1,52 +1,180 @@
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import type { ReactNode } from 'react'
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
import {
Layout,
Menu,
theme,
Dropdown,
Space,
Typography,
Avatar,
} from 'antd'
import {
DashboardOutlined,
UserOutlined,
LogoutOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
ApiOutlined,
AppstoreOutlined,
DashboardOutlined,
DownOutlined,
LeftOutlined,
LogoutOutlined,
OrderedListOutlined,
RightOutlined,
ShopOutlined,
BugOutlined,
TeamOutlined,
UserOutlined,
WalletOutlined,
} from '@ant-design/icons'
import { useAuth } from '../store/auth'
import type { MenuProps } from 'antd'
const { Header, Sider, Content } = Layout
interface SidebarChild {
key: string
label: string
path?: string
disabled?: boolean
}
interface SidebarSection {
key: string
label: string
icon: ReactNode
path?: string
disabled?: boolean
children?: SidebarChild[]
}
const adminSections: SidebarSection[] = [
{
key: 'dashboard',
label: '工作台',
icon: <DashboardOutlined />,
path: '/',
},
{
key: 'products',
label: '商品管理',
icon: <AppstoreOutlined />,
children: [{ key: 'platform-products', label: '平台授权商品', disabled: true }],
},
{
key: 'shops',
label: '店铺管理',
icon: <ShopOutlined />,
children: [
{ key: 'shop-list', label: '店铺列表', path: '/platform-merchants' },
{ key: 'shop-products', label: '店铺商品绑定', disabled: true },
],
},
{
key: 'orders',
label: '订单管理',
icon: <OrderedListOutlined />,
children: [
{ key: 'auto-orders', label: '自动发货订单', disabled: true },
{ key: 'manual-verify', label: '手动核销/反核销', disabled: true },
{ key: 'after-sales', label: '售后订单', disabled: true },
{ key: 'manual-orders', label: '手动处理订单', disabled: true },
],
},
{
key: 'staff',
label: '员工管理',
icon: <TeamOutlined />,
children: [
{ key: 'staff-list', label: '员工列表', disabled: true },
{ key: 'positions', label: '岗位列表', disabled: true },
{ key: 'operation-logs', label: '操作日志', disabled: true },
],
},
{
key: 'funds',
label: '资金',
icon: <WalletOutlined />,
children: [
{ key: 'point-details', label: '积分明细', disabled: true },
{ key: 'recharge-requests', label: '充值申请', disabled: true },
],
},
{
key: 'open-api',
label: '开放 API',
icon: <ApiOutlined />,
children: [
{ key: 'api-keys', label: 'API 密钥', path: '/merchant-center' },
{ key: 'api-docs', label: '对接文档', path: '/open-api' },
{ key: 'api-debug', label: '调用调试', path: '/api-debug' },
],
},
]
const merchantSections: SidebarSection[] = [
{
key: 'dashboard',
label: '工作台',
icon: <DashboardOutlined />,
path: '/',
},
{
key: 'open-api',
label: '开放 API',
icon: <ApiOutlined />,
children: [{ key: 'api-keys', label: 'API 密钥', path: '/merchant-center' }],
},
]
function getSelectedKey(pathname: string) {
if (pathname === '/') return 'dashboard'
if (pathname.startsWith('/merchant-center')) return 'api-keys'
if (pathname.startsWith('/open-api')) return 'api-docs'
if (pathname.startsWith('/api-debug')) return 'api-debug'
if (pathname.startsWith('/platform-merchants')) return 'shop-list'
return 'dashboard'
}
function getInitialOpenKeys(sections: SidebarSection[], selectedKey: string) {
return sections
.filter((section) => section.children?.some((child) => child.key === selectedKey))
.map((section) => section.key)
}
export default function MainLayout() {
const [collapsed, setCollapsed] = useState(false)
const { user, logout, isAdmin } = useAuth()
const navigate = useNavigate()
const location = useLocation()
const {
token: { colorBgContainer, borderRadiusLG },
} = theme.useToken()
const isDocsPage = location.pathname.startsWith('/open-api')
const menuItems: MenuProps['items'] = useMemo(() => {
const items: MenuProps['items'] = [
{ key: '/', icon: <DashboardOutlined />, label: '数据概览' },
{ key: '/merchant-center', icon: <ShopOutlined />, label: '商户中心' },
]
if (isAdmin) {
items.push(
{ key: '/platform-merchants', icon: <ShopOutlined />, label: '商户管理' },
{ key: '/open-api', icon: <ApiOutlined />, label: '开放接口' },
{ key: '/api-debug', icon: <BugOutlined />, label: 'API 调试' },
)
const sections = useMemo(() => (isAdmin ? adminSections : merchantSections), [isAdmin])
const selectedKey = getSelectedKey(location.pathname)
const [openKeys, setOpenKeys] = useState<string[]>(() => getInitialOpenKeys(sections, selectedKey))
useEffect(() => {
const nextOpenKeys = getInitialOpenKeys(sections, selectedKey)
setOpenKeys((current) => Array.from(new Set([...current, ...nextOpenKeys])))
}, [sections, selectedKey])
const toggleSection = (section: SidebarSection) => {
if (section.disabled) return
if (section.path) {
navigate(section.path)
return
}
return items
}, [isAdmin])
if (collapsed) {
const firstEnabledChild = section.children?.find((child) => child.path && !child.disabled)
if (firstEnabledChild?.path) navigate(firstEnabledChild.path)
return
}
setOpenKeys((current) =>
current.includes(section.key)
? current.filter((key) => key !== section.key)
: [...current, section.key],
)
}
const handleChildClick = (child: SidebarChild) => {
if (!child.path || child.disabled) return
navigate(child.path)
}
const userMenu: MenuProps['items'] = [
{
@@ -61,46 +189,70 @@ export default function MainLayout() {
]
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider trigger={null} collapsible collapsed={collapsed} theme="dark">
<div
style={{
height: 64,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontWeight: 700,
fontSize: collapsed ? 14 : 16,
letterSpacing: 1,
}}
<Layout className="app-shell">
<Sider
className={`app-sidebar${collapsed ? ' app-sidebar--collapsed' : ''}`}
trigger={null}
collapsible
collapsed={collapsed}
width={230}
collapsedWidth={74}
>
<button
className="app-sidebar__collapse"
type="button"
onClick={() => setCollapsed(!collapsed)}
aria-label={collapsed ? '展开侧边栏' : '收起侧边栏'}
>
{collapsed ? '供货' : '皮肤供货平台'}
{collapsed ? <RightOutlined /> : <LeftOutlined />}
</button>
<div className="app-sidebar__brand">
<span className="app-sidebar__brand-mark">S</span>
{!collapsed ? <span className="app-sidebar__brand-text">Skin Sales</span> : null}
</div>
<Menu
theme="dark"
mode="inline"
selectedKeys={[location.pathname === '/' ? '/' : `/${location.pathname.split('/')[1]}`]}
items={menuItems}
onClick={({ key }) => navigate(key)}
/>
<nav className="app-sidebar__nav">
{sections.map((section) => {
const isOpen = openKeys.includes(section.key)
const isSelected = selectedKey === section.key
const hasSelectedChild = section.children?.some((child) => child.key === selectedKey)
return (
<div className="app-sidebar__group" key={section.key}>
<button
className={`app-sidebar__item${isSelected || hasSelectedChild ? ' app-sidebar__item--active' : ''}${section.disabled ? ' app-sidebar__item--disabled' : ''}`}
type="button"
onClick={() => toggleSection(section)}
title={collapsed ? section.label : undefined}
>
<span className="app-sidebar__icon">{section.icon}</span>
{!collapsed ? <span className="app-sidebar__label">{section.label}</span> : null}
{!collapsed && section.children?.length ? (
<DownOutlined className={`app-sidebar__arrow${isOpen ? ' app-sidebar__arrow--open' : ''}`} />
) : null}
</button>
{!collapsed && section.children?.length && isOpen ? (
<div className="app-sidebar__children">
{section.children.map((child) => (
<button
className={`app-sidebar__child${selectedKey === child.key ? ' app-sidebar__child--active' : ''}${child.disabled ? ' app-sidebar__child--disabled' : ''}`}
key={child.key}
type="button"
onClick={() => handleChildClick(child)}
disabled={child.disabled}
>
{child.label}
</button>
))}
</div>
) : null}
</div>
)
})}
</nav>
</Sider>
<Layout>
<Layout className={`app-main${isDocsPage ? ' app-main--flush' : ''}`}>
<Header
style={{
padding: '0 24px',
background: colorBgContainer,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
className="app-header"
>
<span
style={{ fontSize: 18, cursor: 'pointer' }}
onClick={() => setCollapsed(!collapsed)}
>
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</span>
<Dropdown menu={{ items: userMenu }}>
<Space style={{ cursor: 'pointer' }}>
<Avatar size="small" icon={<UserOutlined />} />
@@ -111,15 +263,8 @@ export default function MainLayout() {
</Space>
</Dropdown>
</Header>
<Content style={{ margin: 24 }}>
<div
style={{
padding: 24,
minHeight: 360,
background: colorBgContainer,
borderRadius: borderRadiusLG,
}}
>
<Content className="app-content">
<div className="app-content__body">
<Outlet />
</div>
</Content>