- Introduced Vite configuration files (vite.config.js, vite.config.ts, vite.config.d.ts) for the React frontend. - Configured server proxy settings for API endpoints. - Added Vitest configuration files (vitest.config.js, vitest.config.ts, vitest.config.d.ts) for testing. - Updated architecture overview to reflect the separation of the platform admin frontend into its own React project. - Modified quickstart documentation to guide users on starting the platform admin frontend.
96 lines
4.5 KiB
TypeScript
96 lines
4.5 KiB
TypeScript
import {
|
|
AlertOutlined,
|
|
ApartmentOutlined,
|
|
AuditOutlined,
|
|
BankOutlined,
|
|
BookOutlined,
|
|
ContactsOutlined,
|
|
CreditCardOutlined,
|
|
DashboardOutlined,
|
|
FileTextOutlined,
|
|
LogoutOutlined,
|
|
MenuFoldOutlined,
|
|
MenuUnfoldOutlined,
|
|
MessageOutlined,
|
|
PieChartOutlined,
|
|
ShopOutlined,
|
|
UserSwitchOutlined,
|
|
} from '@ant-design/icons';
|
|
import { Avatar, Button, Dropdown, Flex, Layout, Menu, Space, Spin, Typography } from 'antd';
|
|
import { useState } from 'react';
|
|
import { Navigate, Outlet, useLocation, useNavigate } from 'react-router';
|
|
import { useAuth } from '../auth/AuthProvider';
|
|
|
|
const { Header, Sider, Content } = Layout;
|
|
const items = [
|
|
{ key: '/', icon: <DashboardOutlined />, label: '经营概览' },
|
|
{ type: 'group' as const, label: '业务运营', children: [
|
|
{ key: '/tenants', icon: <ApartmentOutlined />, label: '租户管理' },
|
|
{ key: '/subscriptions', icon: <ShopOutlined />, label: '套餐与订阅' },
|
|
{ key: '/billing', icon: <BankOutlined />, label: '订单与发票' },
|
|
{ key: '/usage', icon: <PieChartOutlined />, label: '用量计费' },
|
|
{ key: '/dunning', icon: <FileTextOutlined />, label: '收款与催缴' },
|
|
] },
|
|
{ type: 'group' as const, label: '内容资产', children: [
|
|
{ key: '/question-banks', icon: <BookOutlined />, label: '公共题库' },
|
|
] },
|
|
{ type: 'group' as const, label: '平台服务', children: [
|
|
{ key: '/crm', icon: <ContactsOutlined />, label: 'CRM 服务' },
|
|
{ key: '/sms', icon: <MessageOutlined />, label: '短信服务' },
|
|
{ key: '/payments', icon: <CreditCardOutlined />, label: '支付服务' },
|
|
] },
|
|
{ type: 'group' as const, label: '安全与系统', children: [
|
|
{ key: '/staff', icon: <UserSwitchOutlined />, label: '员工与角色' },
|
|
{ key: '/audit', icon: <AuditOutlined />, label: '审计日志' },
|
|
{ key: '/alerts', icon: <AlertOutlined />, label: '审计告警' },
|
|
] },
|
|
];
|
|
|
|
const pageTitles: Record<string, string> = {
|
|
'/': '经营概览', '/tenants': '租户管理', '/subscriptions': '套餐与订阅', '/billing': '订单与发票',
|
|
'/usage': '用量计费', '/dunning': '收款与催缴', '/question-banks': '公共题库', '/crm': 'CRM 服务',
|
|
'/sms': '短信服务', '/payments': '支付服务', '/staff': '员工与角色', '/audit': '审计日志', '/alerts': '审计告警',
|
|
};
|
|
|
|
export function AppLayout() {
|
|
const auth = useAuth();
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
if (auth.loading) return <div className="full-screen-loading"><Spin size="large" /></div>;
|
|
if (!auth.user) return <Navigate to="/login" replace state={{ from: location.pathname }} />;
|
|
const displayName = auth.user.name || auth.user.phone || auth.user.email || '平台管理员';
|
|
return (
|
|
<Layout className="app-shell">
|
|
<Sider theme="light" collapsible collapsed={collapsed} collapsedWidth={76} trigger={null} width={248} className="app-sider">
|
|
<div className="sider-brand">
|
|
<div className="brand-mark"><img src="/logo.png" alt="Logo" /></div>
|
|
{!collapsed && <div className="brand-copy"><strong>恭学题库</strong><span>平台管理中心</span></div>}
|
|
</div>
|
|
<div className="sider-navigation"><Menu theme="light" mode="inline" selectedKeys={[location.pathname]} items={items} onClick={({ key }) => navigate(key)} /></div>
|
|
<div className="sider-footer">
|
|
<Button type="text" block icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />} onClick={() => setCollapsed((value) => !value)}>{collapsed ? null : '收起导航'}</Button>
|
|
</div>
|
|
</Sider>
|
|
<Layout>
|
|
<Header className="app-header">
|
|
<Typography.Text className="header-page-title">{pageTitles[location.pathname] || '平台管理'}</Typography.Text>
|
|
<Dropdown
|
|
menu={{ items: [{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', onClick: () => void auth.logout() }] }}
|
|
trigger={['click']}
|
|
>
|
|
<Flex align="center" gap={10} className="user-menu">
|
|
<Avatar>{String(displayName).slice(0, 1)}</Avatar>
|
|
<Space direction="vertical" size={0}>
|
|
<Typography.Text strong>{displayName}</Typography.Text>
|
|
<Typography.Text type="secondary" className="user-role">平台管理</Typography.Text>
|
|
</Space>
|
|
</Flex>
|
|
</Dropdown>
|
|
</Header>
|
|
<Content className="app-content"><Outlet /></Content>
|
|
</Layout>
|
|
</Layout>
|
|
);
|
|
}
|