Files
tiku-backend.net/Tiku.PlatformAdmin.Web/src/layout/AppLayout.tsx
xiong 30fd159041 feat: add Vite configuration for Tiku.PlatformAdmin.Web and update documentation
- 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.
2026-07-30 14:54:53 +08:00

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>
);
}