重命名前端目录
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import {
|
||||
AuditOutlined,
|
||||
DashboardOutlined,
|
||||
FileSearchOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
OrderedListOutlined,
|
||||
SettingOutlined,
|
||||
ShopOutlined,
|
||||
TeamOutlined,
|
||||
UnorderedListOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { App, Button, Dropdown, Layout, Menu, Space, Typography } from 'antd'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router'
|
||||
|
||||
import { logoutAdmin } from '@/services/admin'
|
||||
import {
|
||||
clearAdminSession,
|
||||
getAdminRole,
|
||||
getAdminTokenExpiresAt,
|
||||
getAdminUsername,
|
||||
} from '@/utils/admin-auth'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
const SIDEBAR_COLLAPSED_KEY = 'react-admin-sidebar-collapsed'
|
||||
|
||||
export default function AdminLayout() {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { message } = App.useApp()
|
||||
const [collapsed, setCollapsed] = useState(
|
||||
() => window.localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === '1',
|
||||
)
|
||||
const role = getAdminRole()
|
||||
const username = getAdminUsername() || 'admin'
|
||||
const expiresAt = getAdminTokenExpiresAt()
|
||||
|
||||
const menuItems = useMemo<MenuProps['items']>(() => {
|
||||
const operationItems: MenuProps['items'] = [
|
||||
{ key: '/admin/dashboard', icon: <DashboardOutlined />, label: '概览' },
|
||||
{ key: '/admin/orders', icon: <OrderedListOutlined />, label: '订单' },
|
||||
{ key: '/admin/tasks', icon: <UnorderedListOutlined />, label: '任务' },
|
||||
{ key: '/admin/cloudtentacles-records', icon: <FileSearchOutlined />, label: '发货记录' },
|
||||
]
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: 'group-operation',
|
||||
type: 'group',
|
||||
label: '运营',
|
||||
children: operationItems,
|
||||
},
|
||||
]
|
||||
|
||||
if (role === 'admin') {
|
||||
items.push(
|
||||
{
|
||||
key: 'group-config',
|
||||
type: 'group',
|
||||
label: '配置',
|
||||
children: [
|
||||
{ key: '/admin/users', icon: <TeamOutlined />, label: '后台用户' },
|
||||
{ key: '/admin/platform-shops', icon: <SettingOutlined />, label: '平台配置' },
|
||||
{ key: '/admin/platform-fulfillment', icon: <ShopOutlined />, label: '履约配置' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'group-system',
|
||||
type: 'group',
|
||||
label: '系统',
|
||||
children: [{ key: '/admin/audit-logs', icon: <AuditOutlined />, label: '审计日志' }],
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return items
|
||||
}, [role])
|
||||
|
||||
async function submitLogout() {
|
||||
try {
|
||||
await logoutAdmin()
|
||||
} catch {
|
||||
// 后端退出是无状态的,本地清理优先。
|
||||
} finally {
|
||||
clearAdminSession()
|
||||
message.success('已退出后台')
|
||||
navigate('/admin/login', { replace: true })
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCollapsed() {
|
||||
const next = !collapsed
|
||||
setCollapsed(next)
|
||||
window.localStorage.setItem(SIDEBAR_COLLAPSED_KEY, next ? '1' : '0')
|
||||
}
|
||||
|
||||
const selectedKey = resolveSelectedKey(location.pathname)
|
||||
|
||||
return (
|
||||
<Layout className="admin-shell">
|
||||
<Layout.Sider
|
||||
width={232}
|
||||
collapsedWidth={72}
|
||||
collapsed={collapsed}
|
||||
className="admin-sider"
|
||||
trigger={null}
|
||||
>
|
||||
<div className="admin-sider-inner">
|
||||
<div className="admin-brand">
|
||||
<div className="admin-brand-copy">
|
||||
<strong>{collapsed ? 'OS' : '运营后台'}</strong>
|
||||
{!collapsed ? <span>订单与交付管理</span> : null}
|
||||
</div>
|
||||
<Button
|
||||
className="admin-collapse-button"
|
||||
aria-label={collapsed ? '展开导航' : '折叠导航'}
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={toggleCollapsed}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!collapsed ? (
|
||||
<div className="admin-sidebar-profile">
|
||||
<Typography.Text strong>{username}</Typography.Text>
|
||||
<Typography.Text type="secondary">{roleLabel(role)}</Typography.Text>
|
||||
{expiresAt ? (
|
||||
<Typography.Text type="secondary">
|
||||
{formatAdminDateTime(expiresAt)} 到期
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Menu
|
||||
mode="inline"
|
||||
inlineIndent={16}
|
||||
selectedKeys={[selectedKey]}
|
||||
items={menuItems}
|
||||
className="admin-menu"
|
||||
onClick={({ key }) => navigate(String(key))}
|
||||
/>
|
||||
|
||||
<div className="admin-sider-footer">
|
||||
<Button
|
||||
block={!collapsed}
|
||||
icon={<LogoutOutlined />}
|
||||
onClick={submitLogout}
|
||||
aria-label="退出登录"
|
||||
>
|
||||
{collapsed ? null : '退出登录'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Sider>
|
||||
<Layout className="admin-main-shell">
|
||||
<Layout.Header className="admin-topbar">
|
||||
<Space className="admin-user" size={12}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [{ key: 'logout', label: '退出登录' }],
|
||||
onClick: submitLogout,
|
||||
}}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button icon={<UserOutlined />}>账号</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Layout.Header>
|
||||
<Layout.Content className="admin-content">
|
||||
<Outlet />
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
function resolveSelectedKey(pathname: string) {
|
||||
if (pathname.startsWith('/admin/orders')) return '/admin/orders'
|
||||
if (pathname.startsWith('/admin/tasks')) return '/admin/tasks'
|
||||
if (pathname === '/admin/platform-shops') return '/admin/platform-shops'
|
||||
return pathname
|
||||
}
|
||||
|
||||
function roleLabel(role: string) {
|
||||
if (role === 'admin') return '管理员'
|
||||
if (role === 'operator') return '普通运营'
|
||||
return '客服'
|
||||
}
|
||||
Reference in New Issue
Block a user