优化开放 API 文档界面
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user