Files
makemd/dashboard/src/layouts/index.tsx
wurenzhi e47beffaf9 feat: 重构前端代码结构并添加Java后端支持
- 重构前端导入和组件结构,优化代码组织
- 添加Java后端基础框架和API实现
- 修复类型定义和接口兼容性问题
- 新增测试页面和工具函数
- 优化国际化支持和错误处理
- 更新依赖配置和构建脚本

新增Java后端模块:
- 实现基础认证、订单、支付等服务
- 添加Swagger API文档支持
- 配置数据库连接和缓存
- 实现国际化消息处理
- 添加安全过滤器和限流控制
2026-03-30 16:51:18 +08:00

592 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useMemo, FC } from 'react';
import {
Layout,
Menu,
Typography,
Avatar,
Dropdown,
Badge,
Space,
Tag,
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,
} from '@ant-design/icons';
import { Link, useLocation, useNavigate, Outlet } from 'react-router-dom';
const { Header, Sider, Content } = Layout;
const { Title, Text } = Typography;
import type { MenuProps } from 'antd';
import { useUser, MOCK_USERS, ROLE_CONFIG, FEATURES, PERMISSIONS } from '@/contexts/UserContext';
import { useLocale } from '@/contexts/LocaleContext';
import ThemeSwitch from '@/components/ThemeSwitch';
import Breadcrumb from '@/components/Breadcrumb';
interface CustomMenuItem {
key: string;
icon?: React.ReactNode;
label?: React.ReactNode;
requiredPermission?: string | null;
requiredFeature?: string | null;
children?: CustomMenuItem[];
type?: 'divider' | 'group';
}
type MenuItem = Required<MenuProps>['items'][number];
const MainLayoutContent: FC = () => {
const [collapsed, setCollapsed] = useState(false);
const location = useLocation();
const navigate = useNavigate();
const { currentUser, setCurrentUser, hasPermission, hasFeature, getPlanLabel, isPaidUser } = useUser();
const { locale, setLocale, t } = useLocale();
const ALL_MENU_ITEMS = useMemo(() => [
{
key: '/dashboard',
icon: <DashboardOutlined />,
label: <Link to="/dashboard">{t('menu.dashboard')}</Link>,
requiredPermission: null,
},
{
key: 'ai-operations',
icon: <RobotOutlined />,
label: t('menu.aiOperations'),
requiredFeature: FEATURES.AI_OPERATIONS,
children: [
{ key: '/dashboard/operation-agent-enhanced', label: <Link to="/dashboard/operation-agent-enhanced">{t('menu.operationAgent')}</Link> },
{ key: '/dashboard/auto-pilot', label: <Link to="/dashboard/auto-pilot">{t('menu.autoPilot')}</Link> },
{ key: '/dashboard/task-center', label: <Link to="/dashboard/task-center">{t('menu.taskCenter')}</Link> },
{ key: '/dashboard/ai-action-task-manager', label: <Link to="/dashboard/ai-action-task-manager">{t('menu.taskManager')}</Link> },
{ key: '/dashboard/ai-suggestion', label: <Link to="/dashboard/ai-suggestion">{t('menu.aiSuggestion')}</Link> },
{ key: '/dashboard/human-approval', label: <Link to="/dashboard/human-approval">{t('menu.humanApproval')}</Link> },
{ key: '/dashboard/client-management', label: <Link to="/dashboard/client-management">{t('menu.clientManagement')}</Link> },
{ key: '/dashboard/execution-results', label: <Link to="/dashboard/execution-results">{t('menu.executionResults')}</Link> },
{ key: '/dashboard/ai-decision-log', label: <Link to="/dashboard/ai-decision-log">{t('menu.aiDecisionLog')}</Link> },
{ key: '/dashboard/strategy-marketplace', label: <Link to="/dashboard/strategy-marketplace">{t('menu.strategyMarketplace')}</Link> },
{ key: '/dashboard/auto-product-selection', label: <Link to="/dashboard/auto-product-selection">{t('menu.autoProductSelection')}</Link> },
{ key: '/dashboard/auto-execution', label: <Link to="/dashboard/auto-execution">{t('menu.autoExecution')}</Link> },
{ key: '/dashboard/workflow', label: <Link to="/dashboard/workflow">{t('menu.workflowManagement')}</Link> },
],
},
{
key: 'product-cycle',
icon: <ShoppingOutlined />,
label: t('menu.productCycle'),
children: [
{ key: '/dashboard/product', label: <Link to="/dashboard/product">{t('menu.productManagement')}</Link>, requiredPermission: PERMISSIONS.PRODUCT_READ },
{ key: '/dashboard/product/publish', label: <Link to="/dashboard/product/publish">{t('menu.productPublish')}</Link>, requiredPermission: PERMISSIONS.PRODUCT_WRITE },
{ key: '/dashboard/inventory', label: <Link to="/dashboard/inventory">{t('menu.inventoryManagement')}</Link>, requiredPermission: PERMISSIONS.INVENTORY_READ },
{ key: '/dashboard/product/ai-pricing', label: <Link to="/dashboard/product/ai-pricing">{t('menu.aiPricing')}</Link>, requiredFeature: FEATURES.AUTO_PRICING },
{ key: '/dashboard/product/profit-monitor', label: <Link to="/dashboard/product/profit-monitor">{t('menu.profitMonitor')}</Link> },
],
},
{
key: 'order-cycle',
icon: <FileTextOutlined />,
label: t('menu.orderCycle'),
children: [
{ key: '/dashboard/orders', label: <Link to="/dashboard/orders">{t('menu.orderManagement')}</Link>, requiredPermission: PERMISSIONS.ORDER_READ },
{ key: '/dashboard/orders/exception', label: <Link to="/dashboard/orders/exception">{t('menu.exceptionOrders')}</Link> },
{ key: '/dashboard/after-sales', label: <Link to="/dashboard/after-sales">{t('menu.afterSales')}</Link> },
{ key: '/dashboard/after-sales/refund', label: <Link to="/dashboard/after-sales/refund">{t('menu.refundProcessing')}</Link> },
],
},
{
key: 'logistics-cycle',
icon: <TruckOutlined />,
label: t('menu.logisticsCycle'),
requiredPermission: PERMISSIONS.INVENTORY_READ,
children: [
{ key: '/dashboard/logistics', label: <Link to="/dashboard/logistics">{t('menu.logisticsQuery')}</Link> },
{ key: '/dashboard/logistics/freight-calc', label: <Link to="/dashboard/logistics/freight-calc">{t('menu.freightCalculation')}</Link> },
],
},
{
key: 'finance-cycle',
icon: <WalletOutlined />,
label: t('menu.financeCycle'),
requiredPermission: PERMISSIONS.FINANCE_READ,
children: [
{ key: '/dashboard/finance', label: <Link to="/dashboard/finance">{t('menu.financeOverview')}</Link> },
{ key: '/dashboard/finance/transactions', label: <Link to="/dashboard/finance/transactions">{t('menu.transactionRecords')}</Link> },
{ key: '/dashboard/finance/reconciliation', label: <Link to="/dashboard/finance/reconciliation">{t('menu.financeReconciliation')}</Link> },
{ key: '/dashboard/user-asset', label: <Link to="/dashboard/user-asset">{t('menu.userAssets')}</Link> },
],
},
{
key: 'marketing-cycle',
icon: <LineChartOutlined />,
label: t('menu.marketingCycle'),
children: [
{ key: '/dashboard/ad', label: <Link to="/dashboard/ad">{t('menu.adManagement')}</Link> },
{ key: '/dashboard/marketing/competitors', label: <Link to="/dashboard/marketing/competitors">{t('menu.competitorAnalysis')}</Link> },
{ key: '/dashboard/dynamic-pricing', label: <Link to="/dashboard/dynamic-pricing">{t('menu.dynamicPricing')}</Link>, requiredFeature: FEATURES.AUTO_PRICING },
{ key: '/dashboard/ab-test', label: <Link to="/dashboard/ab-test">{t('menu.abTest')}</Link> },
],
},
{
key: 'analytics',
icon: <AppstoreOutlined />,
label: t('menu.analytics'),
children: [
{ key: '/dashboard/analytics', label: <Link to="/dashboard/analytics">{t('menu.dataAnalysis')}</Link> },
{ key: '/dashboard/analytics/dashboard', label: <Link to="/dashboard/analytics/dashboard">{t('menu.analyticsDashboard')}</Link> },
{ key: '/dashboard/reports', label: <Link to="/dashboard/reports">{t('menu.reportCenter')}</Link> },
{ key: '/dashboard/multi-shop-report', label: <Link to="/dashboard/multi-shop-report">{t('menu.multiShopReport')}</Link>, requiredFeature: FEATURES.MULTI_SHOP },
{ key: '/dashboard/leaderboard', label: <Link to="/dashboard/leaderboard">{t('menu.leaderboard')}</Link> },
],
},
{
key: 'b2b-trade',
icon: <ShopOutlined />,
label: t('menu.b2bTrade'),
requiredFeature: FEATURES.B2B_TRADE,
children: [
{ key: '/dashboard/merchant', label: <Link to="/dashboard/merchant">{t('menu.merchantManagement')}</Link> },
{ key: '/dashboard/b2b', label: <Link to="/dashboard/b2b">{t('menu.b2bTradeManagement')}</Link> },
{ key: '/dashboard/b2b/batch-order', label: <Link to="/dashboard/b2b/batch-order">{t('menu.batchOrder')}</Link> },
{ key: '/dashboard/b2b/enterprise-quote', label: <Link to="/dashboard/b2b/enterprise-quote">{t('menu.enterpriseQuote')}</Link> },
{ key: '/dashboard/b2b/contract-manage', label: <Link to="/dashboard/b2b/contract-manage">{t('menu.contractManagement')}</Link> },
{ key: '/dashboard/procurement', label: <Link to="/dashboard/procurement">{t('menu.procurementManagement')}</Link> },
{ key: '/dashboard/suppliers', label: <Link to="/dashboard/suppliers">{t('menu.suppliers')}</Link> },
{ key: '/dashboard/warehouse', label: <Link to="/dashboard/warehouse">{t('menu.warehouseInventory')}</Link> },
],
},
{
key: 'independent-site',
icon: <GlobalOutlined />,
label: t('menu.independentSite'),
requiredFeature: FEATURES.INDEPENDENT_SITE,
children: [
{ key: '/dashboard/independent-site', label: <Link to="/dashboard/independent-site">{t('menu.siteManagement')}</Link> },
{ key: '/dashboard/independent-site/create', label: <Link to="/dashboard/independent-site/create">{t('menu.createSite')}</Link> },
{ key: '/dashboard/independent-site/templates', label: <Link to="/dashboard/independent-site/templates">{t('menu.siteTemplates')}</Link> },
{ key: '/dashboard/independent-site/analytics', label: <Link to="/dashboard/independent-site/analytics">{t('menu.siteAnalytics')}</Link> },
{ key: '/dashboard/independent-site/orders', label: <Link to="/dashboard/independent-site/orders">{t('menu.independentSiteOrders')}</Link> },
{ key: '/dashboard/independent-site/products', label: <Link to="/dashboard/independent-site/products">{t('menu.independentSiteProducts')}</Link> },
{ key: '/dashboard/independent-site/domains', label: <Link to="/dashboard/independent-site/domains">{t('menu.domainManagement')}</Link> },
],
},
{
key: 'risk-compliance',
icon: <SafetyOutlined />,
label: t('menu.riskCompliance'),
children: [
{ key: '/dashboard/blacklist', label: <Link to="/dashboard/blacklist">{t('menu.riskMonitoring')}</Link> },
{ key: '/dashboard/compliance', label: <Link to="/dashboard/compliance">{t('menu.complianceCheck')}</Link> },
{ key: '/dashboard/audit', label: <Link to="/dashboard/audit">{t('menu.auditLogs')}</Link> },
{ key: '/dashboard/operation-logs', label: <Link to="/dashboard/operation-logs">{t('menu.operationLogs')}</Link> },
{ key: '/dashboard/governance', label: <Link to="/dashboard/governance">{t('menu.governanceCenter')}</Link> },
{ key: '/dashboard/sovereignty', label: <Link to="/dashboard/sovereignty">{t('menu.sovereigntyManagement')}</Link> },
{ key: '/dashboard/compliance/certificates', label: <Link to="/dashboard/compliance/certificates">{t('menu.certificateManagement')}</Link> },
],
},
{
key: 'settings',
icon: <SettingOutlined />,
label: t('menu.settings'),
children: [
{ key: '/dashboard/settings', label: <Link to="/dashboard/settings">{t('menu.settingsOverview')}</Link> },
{ key: '/dashboard/settings/platform-auth', label: <Link to="/dashboard/settings/platform-auth">{t('menu.platformAuth')}</Link> },
{ key: '/dashboard/settings/service-manager', label: <Link to="/dashboard/settings/service-manager">{t('menu.serviceManager')}</Link> },
{ key: '/dashboard/settings/subscription', label: <Link to="/dashboard/settings/subscription">{t('menu.subscription')}</Link> },
{ key: '/dashboard/settings/my-shops', label: <Link to="/dashboard/settings/my-shops"></Link> },
{ key: '/dashboard/settings/shop-members', label: <Link to="/dashboard/settings/shop-members"></Link> },
{ key: '/dashboard/user', label: <Link to="/dashboard/user">{t('menu.userManagement')}</Link> },
{ key: '/dashboard/role', label: <Link to="/dashboard/role">{t('menu.rolePermissions')}</Link> },
],
},
], [t]);
const menuItems = useMemo(() => {
const stripCustomProps = (item: CustomMenuItem): MenuItem => {
const { requiredPermission, requiredFeature, ...rest } = item;
return rest as MenuItem;
};
const filterMenuItems = (items: CustomMenuItem[]): MenuItem[] => {
const result: MenuItem[] = [];
items.forEach((item) => {
if (item.requiredFeature && !hasFeature(item.requiredFeature)) {
return;
}
if (item.requiredPermission && !hasPermission(item.requiredPermission)) {
return;
}
if (item.children && Array.isArray(item.children)) {
const filteredChildren = filterMenuItems(item.children);
if (filteredChildren.length > 0) {
result.push(stripCustomProps({ ...item, children: filteredChildren as CustomMenuItem[] }));
}
} else {
result.push(stripCustomProps(item));
}
});
return result;
};
return filterMenuItems(ALL_MENU_ITEMS);
}, [hasPermission, hasFeature, t]);
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 Array.from(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',
};
// 如果当前路径不是以 /dashboard 开头,则只渲染 Outlet避免布局嵌套
if (!location.pathname.startsWith('/dashboard')) {
return <Outlet context={{ currentUser, hasPermission, hasFeature, isPaidUser }} />;
}
return (
<Layout style={{ minHeight: '100vh', display: 'flex' }}>
<Sider
trigger={null}
collapsible
collapsed={collapsed}
width={collapsed ? 80 : 200}
style={{
background: '#001529',
zIndex: 99,
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>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<Header
style={{
background: '#fff',
padding: '0 24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
boxShadow: '0 1px 4px rgba(0, 21, 41, 0.08)',
zIndex: 100,
height: '64px',
flexShrink: 0,
}}
>
<div style={{ fontSize: '16px', fontWeight: 500, color: '#333' }}>
{t('app.name')} {t('app.title')}
</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>
<ThemeSwitch />
<Dropdown
menu={{
items: [
{ key: 'zh-CN', label: t('settings.language') === 'Language' ? '简体中文' : 'Chinese' },
{ key: 'en-US', label: t('settings.language') === 'Language' ? 'English' : '英文' },
],
onClick: ({ key }) => setLocale(key),
selectedKeys: [locale],
}}
placement="bottomRight"
>
<Button type="text" icon={<GlobalOutlined />} style={{ height: '32px' }}>
{locale === 'zh-CN' ? t('settings.language') === 'Language' ? '中文' : 'Chinese' : t('settings.language') === 'Language' ? 'EN' : '英文'}
</Button>
</Dropdown>
<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,
flex: 1,
}}
>
<div
style={{
background: '#fff',
padding: '24px',
borderRadius: '8px',
minHeight: 'calc(100vh - 184px)',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.06)',
}}
>
<Breadcrumb />
<Outlet context={{ currentUser, hasPermission, hasFeature, isPaidUser }} />
</div>
</Content>
</div>
</Layout>
);
};
const MainLayout: FC = () => {
return <MainLayoutContent />;
};
export default MainLayout;