Files
makemd/dashboard/src/layouts/index.tsx

568 lines
22 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect, useMemo } from 'react';
import { Layout, Menu, Typography, Avatar, Dropdown, Badge, Space, Tag, Divider, Switch, message, Button } from 'antd';
import {
DashboardOutlined,
ShoppingOutlined,
FileTextOutlined,
UserOutlined,
TruckOutlined,
AlertOutlined,
SettingOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
BellOutlined,
DownOutlined,
ShopOutlined,
WalletOutlined,
GlobalOutlined,
RobotOutlined,
AppstoreOutlined,
LineChartOutlined,
SafetyOutlined,
CrownOutlined,
SwapOutlined,
ClusterOutlined,
} from '@ant-design/icons';
import { Link, useLocation, useNavigate, Outlet } from 'react-router-dom';
import type { MenuProps } from 'antd';
import { UserProvider, useUser, MOCK_USERS, ROLE_CONFIG, FEATURES, PERMISSIONS, UserRole } from '@/contexts/UserContext';
const { Header, Sider, Content } = Layout;
const { Title, Text } = Typography;
type MenuItem = Required<MenuProps>['items'][number];
const ALL_MENU_ITEMS: MenuItem[] = [
{
key: '/dashboard',
icon: <DashboardOutlined />,
label: <Link to="/dashboard"></Link>,
requiredPermission: null,
},
{
key: 'aggregation',
icon: <ClusterOutlined />,
label: '聚合管理',
children: [
{ key: '/dashboard/aggregation/products', label: <Link to="/dashboard/aggregation/products"></Link> },
{ key: '/dashboard/aggregation/orders', label: <Link to="/dashboard/aggregation/orders"></Link> },
{ key: '/dashboard/aggregation/inventory', label: <Link to="/dashboard/aggregation/inventory"></Link> },
{ key: '/dashboard/aggregation/customers', label: <Link to="/dashboard/aggregation/customers"></Link> },
{ key: '/dashboard/aggregation/authorization', label: <Link to="/dashboard/aggregation/authorization"></Link> },
],
},
{
key: 'ai-operations',
icon: <RobotOutlined />,
label: 'AI运营中心',
requiredFeature: FEATURES.AI_OPERATIONS,
children: [
{ key: '/dashboard/operation-agent', label: <Link to="/dashboard/operation-agent">Agent</Link> },
{ key: '/dashboard/auto-pilot', label: <Link to="/dashboard/auto-pilot"></Link> },
{ key: '/dashboard/task-center', label: <Link to="/dashboard/task-center"></Link> },
{ key: '/dashboard/ai-decision-log', label: <Link to="/dashboard/ai-decision-log">AI决策日志</Link> },
{ key: '/dashboard/strategy-marketplace', label: <Link to="/dashboard/strategy-marketplace"></Link> },
{ key: '/dashboard/auto-product-selection', label: <Link to="/dashboard/auto-product-selection"></Link> },
{ key: '/dashboard/auto-execution', label: <Link to="/dashboard/auto-execution"></Link> },
],
},
{
key: 'product-cycle',
icon: <ShoppingOutlined />,
label: '商品闭环',
children: [
{ key: '/dashboard/product', label: <Link to="/dashboard/product"></Link>, requiredPermission: PERMISSIONS.PRODUCT_READ },
{ key: '/dashboard/product/publish', label: <Link to="/dashboard/product/publish"></Link>, requiredPermission: PERMISSIONS.PRODUCT_WRITE },
{ key: '/dashboard/product/cross-platform', label: <Link to="/dashboard/product/cross-platform"></Link>, requiredFeature: FEATURES.MULTI_SHOP },
{ key: '/dashboard/product/ai-pricing', label: <Link to="/dashboard/product/ai-pricing">AI定价</Link>, requiredFeature: FEATURES.AUTO_PRICING },
{ key: '/dashboard/product/profit-monitor', label: <Link to="/dashboard/product/profit-monitor"></Link> },
{ key: '/dashboard/inventory', label: <Link to="/dashboard/inventory"></Link>, requiredPermission: PERMISSIONS.INVENTORY_READ },
{ key: '/dashboard/inventory/forecast', label: <Link to="/dashboard/inventory/forecast"></Link> },
{ key: '/dashboard/inventory/warehouses', label: <Link to="/dashboard/inventory/warehouses"></Link> },
],
},
{
key: 'order-cycle',
icon: <FileTextOutlined />,
label: '订单闭环',
children: [
{ key: '/dashboard/orders', label: <Link to="/dashboard/orders"></Link>, requiredPermission: PERMISSIONS.ORDER_READ },
{ key: '/dashboard/orders/exception', label: <Link to="/dashboard/orders/exception"></Link> },
{ key: '/dashboard/orders/aggregation', label: <Link to="/dashboard/orders/aggregation"></Link> },
{ key: '/dashboard/after-sales', label: <Link to="/dashboard/after-sales"></Link> },
{ key: '/dashboard/after-sales/refund', label: <Link to="/dashboard/after-sales/refund">退</Link> },
{ key: '/dashboard/after-sales/customer-service', label: <Link to="/dashboard/after-sales/customer-service"></Link> },
],
},
{
key: 'logistics-cycle',
icon: <TruckOutlined />,
label: '物流闭环',
requiredPermission: PERMISSIONS.INVENTORY_READ,
children: [
{ key: '/dashboard/logistics', label: <Link to="/dashboard/logistics"></Link> },
{ key: '/dashboard/logistics/track', label: <Link to="/dashboard/logistics/track"></Link> },
{ key: '/dashboard/logistics/freight-calc', label: <Link to="/dashboard/logistics/freight-calc"></Link> },
],
},
{
key: 'finance-cycle',
icon: <WalletOutlined />,
label: '财务闭环',
requiredPermission: PERMISSIONS.FINANCE_READ,
children: [
{ key: '/dashboard/finance', label: <Link to="/dashboard/finance"></Link> },
{ key: '/dashboard/finance/transactions', label: <Link to="/dashboard/finance/transactions"></Link> },
{ key: '/dashboard/finance/reconciliation', label: <Link to="/dashboard/finance/reconciliation"></Link> },
{ key: '/dashboard/user-asset', label: <Link to="/dashboard/user-asset"></Link> },
{ key: '/dashboard/user-asset/member-level', label: <Link to="/dashboard/user-asset/member-level"></Link> },
{ key: '/dashboard/user-asset/points', label: <Link to="/dashboard/user-asset/points"></Link> },
],
},
{
key: 'marketing-cycle',
icon: <LineChartOutlined />,
label: '营销闭环',
children: [
{ key: '/dashboard/ad', label: <Link to="/dashboard/ad">广</Link> },
{ key: '/dashboard/ad/auto-adjustment', label: <Link to="/dashboard/ad/auto-adjustment"></Link>, requiredFeature: FEATURES.AUTO_PRICING },
{ key: '/dashboard/ad/ai-optimization', label: <Link to="/dashboard/ad/ai-optimization">AI优化</Link>, requiredFeature: FEATURES.AI_OPERATIONS },
{ key: '/dashboard/marketing/competitors', label: <Link to="/dashboard/marketing/competitors"></Link> },
{ key: '/dashboard/dynamic-pricing', label: <Link to="/dashboard/dynamic-pricing"></Link>, requiredFeature: FEATURES.AUTO_PRICING },
{ key: '/dashboard/ab-test', label: <Link to="/dashboard/ab-test">AB测试</Link> },
{ key: '/dashboard/ab-test/results', label: <Link to="/dashboard/ab-test/results"></Link> },
],
},
{
key: 'analytics',
icon: <AppstoreOutlined />,
label: '数据分析',
children: [
{ key: '/dashboard/analytics', label: <Link to="/dashboard/analytics"></Link> },
{ key: '/dashboard/reports', label: <Link to="/dashboard/reports"></Link> },
{ key: '/dashboard/multi-shop-report', label: <Link to="/dashboard/multi-shop-report"></Link>, requiredFeature: FEATURES.MULTI_SHOP },
{ key: '/dashboard/leaderboard', label: <Link to="/dashboard/leaderboard"></Link> },
],
},
{
key: 'b2b-trade',
icon: <ShopOutlined />,
label: 'B2B贸易',
requiredFeature: FEATURES.B2B_TRADE,
children: [
{ key: '/dashboard/merchant', label: <Link to="/dashboard/merchant"></Link> },
{ key: '/dashboard/merchant/orders', label: <Link to="/dashboard/merchant/orders"></Link> },
{ key: '/dashboard/b2b', label: <Link to="/dashboard/b2b">B2B贸易</Link> },
{ key: '/dashboard/b2b-trade', label: <Link to="/dashboard/b2b-trade">B2B交易</Link> },
{ key: '/dashboard/suppliers', label: <Link to="/dashboard/suppliers"></Link> },
],
},
{
key: 'independent-site',
icon: <GlobalOutlined />,
label: '独立站',
requiredFeature: FEATURES.INDEPENDENT_SITE,
children: [
{ key: '/dashboard/independent-site', label: <Link to="/dashboard/independent-site"></Link> },
{ key: '/dashboard/independent-site/create', label: <Link to="/dashboard/independent-site/create"></Link> },
{ key: '/dashboard/independent-site/builder', label: <Link to="/dashboard/independent-site/builder"></Link> },
{ key: '/dashboard/independent-site/templates', label: <Link to="/dashboard/independent-site/templates"></Link> },
{ key: '/dashboard/independent-site/domains', label: <Link to="/dashboard/independent-site/domains"></Link> },
],
},
{
key: 'risk-compliance',
icon: <SafetyOutlined />,
label: '风控合规',
children: [
{ key: '/dashboard/compliance', label: <Link to="/dashboard/compliance"></Link> },
{ key: '/dashboard/compliance/certificates', label: <Link to="/dashboard/compliance/certificates"></Link> },
{ key: '/dashboard/compliance/check', label: <Link to="/dashboard/compliance/check"></Link> },
{ key: '/dashboard/blacklist', label: <Link to="/dashboard/blacklist"></Link> },
{ key: '/dashboard/arbitrage-monitor', label: <Link to="/dashboard/arbitrage-monitor"></Link> },
],
},
{
key: 'settings',
icon: <SettingOutlined />,
label: '系统设置',
children: [
{ key: '/dashboard/settings', label: <Link to="/dashboard/settings"></Link> },
{ key: '/dashboard/settings/profile', label: <Link to="/dashboard/settings/profile"></Link> },
{ key: '/dashboard/settings/users', label: <Link to="/dashboard/settings/users"></Link>, requiredPermission: PERMISSIONS.USER_MANAGE },
{ key: '/dashboard/settings/system', label: <Link to="/dashboard/settings/system"></Link>, requiredPermission: PERMISSIONS.SYSTEM_CONFIG },
{ key: '/dashboard/role', label: <Link to="/dashboard/role"></Link>, requiredPermission: PERMISSIONS.USER_MANAGE },
{ key: '/dashboard/settings/subscription', label: <Link to="/dashboard/settings/subscription"></Link> },
],
},
];
const MainLayoutContent: React.FC = () => {
const [collapsed, setCollapsed] = useState(false);
const location = useLocation();
const navigate = useNavigate();
const { currentUser, setCurrentUser, hasPermission, hasFeature, getPlanLabel, isPaidUser } = useUser();
const menuItems = useMemo(() => {
const stripCustomProps = (item: any): MenuItem => {
const { requiredPermission, requiredFeature, ...rest } = item;
return rest;
};
const filterMenuItems = (items: MenuItem[]): MenuItem[] => {
return items
.filter((item): item is NonNullable<MenuItem> & { key: string } => item != null && 'key' in item)
.filter((item) => {
const itemWithMeta = item as any;
if (itemWithMeta.requiredFeature && !hasFeature(itemWithMeta.requiredFeature)) {
return false;
}
if (itemWithMeta.requiredPermission && !hasPermission(itemWithMeta.requiredPermission)) {
return false;
}
return true;
})
.map((item) => {
if ('children' in item && Array.isArray(item.children)) {
const filteredChildren = filterMenuItems(item.children);
if (filteredChildren.length === 0) {
return null;
}
return stripCustomProps({ ...item, children: filteredChildren });
}
return stripCustomProps(item);
})
.filter((item): item is NonNullable<MenuItem> => item !== null);
};
return filterMenuItems(ALL_MENU_ITEMS);
}, [hasPermission, hasFeature]);
const getSelectedKeys = (): string[] => {
const pathname = location.pathname;
let bestMatch: string | null = null;
let bestMatchLength = 0;
const findMatch = (items: MenuItem[]) => {
items.forEach((item) => {
if (item && 'key' in item && typeof item.key === 'string') {
const key = item.key;
if (pathname === key || pathname.startsWith(key + '/')) {
if (key.length > bestMatchLength) {
bestMatch = key;
bestMatchLength = key.length;
}
}
}
if (item && 'children' in item && Array.isArray(item.children)) {
findMatch(item.children);
}
});
};
findMatch(menuItems);
return bestMatch ? [bestMatch] : [];
};
const getOpenKeys = (): string[] => {
const pathname = location.pathname;
const openKeys: string[] = [];
const findParent = (items: MenuItem[], parentKey?: string) => {
items.forEach((item) => {
if (item && 'key' in item && typeof item.key === 'string') {
const key = item.key;
if (pathname === key || pathname.startsWith(key + '/')) {
if (parentKey) {
openKeys.push(parentKey);
}
}
}
if (item && 'children' in item && Array.isArray(item.children)) {
const itemKey = 'key' in item ? item.key as string : undefined;
findParent(item.children, itemKey);
}
});
};
findParent(menuItems);
return [...new Set(openKeys)];
};
const selectedKeys = getSelectedKeys();
const openKeys = getOpenKeys();
useEffect(() => {
const savedCollapsed = localStorage.getItem('sidebar_collapsed');
if (savedCollapsed !== null) {
setCollapsed(savedCollapsed === 'true');
}
}, []);
const handleCollapse = (value: boolean) => {
setCollapsed(value);
localStorage.setItem('sidebar_collapsed', String(value));
};
const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') {
localStorage.removeItem('token');
navigate('/auth/login');
} else if (key === 'profile') {
navigate('/dashboard/settings/profile');
} else if (key === 'settings') {
navigate('/dashboard/settings');
} else if (key === 'subscription') {
navigate('/dashboard/settings/subscription');
}
};
const handleSwitchUser = (userId: string) => {
const user = MOCK_USERS.find(u => u.id === userId);
if (user) {
setCurrentUser(user);
message.success(`已切换到 ${user.name} (${ROLE_CONFIG[user.role].label})`);
}
};
const userMenuItems: MenuItem[] = [
{
key: 'profile',
icon: <UserOutlined />,
label: '个人中心',
},
{
key: 'subscription',
icon: <CrownOutlined />,
label: '订阅管理',
},
{
key: 'settings',
icon: <SettingOutlined />,
label: '账号设置',
},
{ type: 'divider' },
{
key: 'logout',
icon: <AlertOutlined />,
label: '退出登录',
},
];
const planColors: Record<string, string> = {
free: 'default',
basic: 'blue',
pro: 'gold',
enterprise: 'purple',
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider
trigger={null}
collapsible
collapsed={collapsed}
width={collapsed ? 80 : 200}
style={{
background: '#001529',
position: 'fixed',
left: 0,
top: 0,
height: '100vh',
zIndex: 100,
padding: 0,
}}
>
<div style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}>
<div
style={{
height: '64px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#002140',
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
flexShrink: 0,
}}
>
{collapsed ? (
<div style={{ color: '#fff', fontSize: '20px', fontWeight: 'bold' }}>C</div>
) : (
<Title level={4} style={{ color: '#fff', margin: 0, whiteSpace: 'nowrap' }}>
Crawlful Hub
</Title>
)}
</div>
<div
style={{
flex: '1 1 auto',
minHeight: 0,
overflow: 'auto',
overflowX: 'hidden',
}}
className="custom-scrollbar"
>
<style>{`
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 2px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 2px;
transition: background 0.3s;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
`}</style>
<Menu
theme="dark"
mode="inline"
selectedKeys={selectedKeys}
defaultOpenKeys={openKeys}
items={menuItems}
style={{
borderRight: 0,
paddingTop: '8px',
height: 'auto',
}}
/>
</div>
<div
style={{
height: '48px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderTop: '1px solid rgba(255, 255, 255, 0.1)',
flexShrink: 0,
cursor: 'pointer',
background: 'rgba(255, 255, 255, 0.05)',
}}
onClick={() => handleCollapse(!collapsed)}
>
{collapsed ? (
<MenuUnfoldOutlined style={{ fontSize: '16px', color: '#fff' }} />
) : (
<MenuFoldOutlined style={{ fontSize: '16px', color: '#fff' }} />
)}
</div>
</div>
</Sider>
<Layout
style={{
marginLeft: collapsed ? 80 : 200,
transition: 'all 0.2s',
}}
>
<Header
style={{
background: '#fff',
padding: '0 24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
boxShadow: '0 1px 4px rgba(0, 21, 41, 0.08)',
position: 'sticky',
top: 0,
zIndex: 99,
}}
>
<div style={{ fontSize: '16px', fontWeight: 500, color: '#333' }}>
Crawlful Hub
</div>
<Space size={16}>
<Dropdown
menu={{
items: MOCK_USERS.map(user => ({
key: user.id,
label: (
<Space>
<span>{user.name}</span>
<Tag color={ROLE_CONFIG[user.role].color}>{ROLE_CONFIG[user.role].label}</Tag>
<Tag color={planColors[user.subscription?.plan || 'free']}>{getPlanLabel()}</Tag>
</Space>
),
})),
onClick: ({ key }) => handleSwitchUser(key),
}}
placement="bottomRight"
>
<Button size="small" icon={<SwapOutlined />}>
</Button>
</Dropdown>
<Badge count={5} size="small">
<BellOutlined style={{ fontSize: '18px', cursor: 'pointer', color: '#666' }} />
</Badge>
<Dropdown
menu={{ items: userMenuItems, onClick: handleUserMenuClick }}
placement="bottomRight"
>
<Space style={{ cursor: 'pointer' }}>
<Avatar style={{ backgroundColor: '#1890ff' }} icon={<UserOutlined />} />
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<Text strong style={{ fontSize: '14px' }}>{currentUser.name}</Text>
<Space size={4}>
<Tag color={ROLE_CONFIG[currentUser.role].color} style={{ margin: 0, fontSize: '10px', padding: '0 4px' }}>
{ROLE_CONFIG[currentUser.role].label}
</Tag>
<Tag color={planColors[currentUser.subscription?.plan || 'free']} style={{ margin: 0, fontSize: '10px', padding: '0 4px' }}>
{getPlanLabel()}
</Tag>
</Space>
</div>
<DownOutlined style={{ fontSize: '12px', color: '#999' }} />
</Space>
</Dropdown>
</Space>
</Header>
<Content
style={{
margin: '24px',
padding: '24px',
background: '#f0f2f5',
minHeight: 280,
}}
>
<div
style={{
background: '#fff',
padding: '24px',
borderRadius: '8px',
minHeight: 'calc(100vh - 184px)',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.06)',
}}
>
<Outlet context={{ currentUser, hasPermission, hasFeature, isPaidUser }} />
</div>
</Content>
</Layout>
</Layout>
);
};
const MainLayout: React.FC = () => {
return (
<UserProvider>
<MainLayoutContent />
</UserProvider>
);
};
export default MainLayout;