2026-03-19 01:39:34 +08:00
|
|
|
|
import {
|
2026-03-27 16:56:06 +08:00
|
|
|
|
useState,
|
|
|
|
|
|
useEffect,
|
|
|
|
|
|
useMemo,
|
|
|
|
|
|
Layout,
|
|
|
|
|
|
Menu,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
Avatar,
|
|
|
|
|
|
Dropdown,
|
|
|
|
|
|
Badge,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
message,
|
|
|
|
|
|
Button,
|
2026-03-19 01:39:34 +08:00
|
|
|
|
DashboardOutlined,
|
|
|
|
|
|
ShoppingOutlined,
|
|
|
|
|
|
FileTextOutlined,
|
|
|
|
|
|
UserOutlined,
|
|
|
|
|
|
TruckOutlined,
|
|
|
|
|
|
AlertOutlined,
|
|
|
|
|
|
SettingOutlined,
|
|
|
|
|
|
MenuFoldOutlined,
|
|
|
|
|
|
MenuUnfoldOutlined,
|
|
|
|
|
|
BellOutlined,
|
|
|
|
|
|
DownOutlined,
|
|
|
|
|
|
ShopOutlined,
|
|
|
|
|
|
WalletOutlined,
|
|
|
|
|
|
GlobalOutlined,
|
2026-03-19 14:19:01 +08:00
|
|
|
|
RobotOutlined,
|
2026-03-22 11:25:28 +08:00
|
|
|
|
AppstoreOutlined,
|
|
|
|
|
|
LineChartOutlined,
|
|
|
|
|
|
SafetyOutlined,
|
|
|
|
|
|
CrownOutlined,
|
2026-03-19 14:19:01 +08:00
|
|
|
|
SwapOutlined,
|
2026-03-27 16:56:06 +08:00
|
|
|
|
Header,
|
|
|
|
|
|
Sider,
|
|
|
|
|
|
Content,
|
|
|
|
|
|
Title,
|
|
|
|
|
|
Text,
|
|
|
|
|
|
Link,
|
|
|
|
|
|
useLocation,
|
|
|
|
|
|
useNavigate,
|
|
|
|
|
|
Outlet,
|
|
|
|
|
|
FC,
|
|
|
|
|
|
} from '@/imports';
|
2026-03-21 15:04:06 +08:00
|
|
|
|
import type { MenuProps } from 'antd';
|
2026-03-27 16:56:06 +08:00
|
|
|
|
import { UserProvider, useUser, MOCK_USERS, ROLE_CONFIG, FEATURES, PERMISSIONS } from '@/contexts/UserContext';
|
|
|
|
|
|
import { LocaleProvider, useLocale } from '@/contexts/LocaleContext';
|
2026-03-28 22:52:12 +08:00
|
|
|
|
import { ThemeProvider } from '@/contexts/ThemeContext';
|
|
|
|
|
|
import ThemeSwitch from '@/components/ThemeSwitch';
|
|
|
|
|
|
import Breadcrumb from '@/components/Breadcrumb';
|
2026-03-19 01:39:34 +08:00
|
|
|
|
|
2026-03-23 12:41:35 +08:00
|
|
|
|
interface CustomMenuItem {
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
icon?: React.ReactNode;
|
|
|
|
|
|
label?: React.ReactNode;
|
|
|
|
|
|
requiredPermission?: string | null;
|
|
|
|
|
|
requiredFeature?: string | null;
|
|
|
|
|
|
children?: CustomMenuItem[];
|
|
|
|
|
|
type?: 'divider' | 'group';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 11:25:28 +08:00
|
|
|
|
type MenuItem = Required<MenuProps>['items'][number];
|
2026-03-20 17:53:46 +08:00
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
const MainLayoutContent: FC = () => {
|
2026-03-22 11:25:28 +08:00
|
|
|
|
const [collapsed, setCollapsed] = useState(false);
|
2026-03-19 01:39:34 +08:00
|
|
|
|
const location = useLocation();
|
2026-03-20 17:53:46 +08:00
|
|
|
|
const navigate = useNavigate();
|
2026-03-22 11:25:28 +08:00
|
|
|
|
const { currentUser, setCurrentUser, hasPermission, hasFeature, getPlanLabel, isPaidUser } = useUser();
|
2026-03-27 16:56:06 +08:00
|
|
|
|
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> },
|
2026-03-28 22:52:12 +08:00
|
|
|
|
{ key: '/dashboard/workflow', label: <Link to="/dashboard/workflow">{t('menu.workflowManagement')}</Link> },
|
2026-03-27 16:56:06 +08:00
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-03-28 22:52:12 +08:00
|
|
|
|
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> },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
2026-03-27 16:56:06 +08:00
|
|
|
|
{
|
|
|
|
|
|
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> },
|
2026-03-28 22:52:12 +08:00
|
|
|
|
{ key: '/dashboard/analytics/dashboard', label: <Link to="/dashboard/analytics/dashboard">{t('menu.analyticsDashboard')}</Link> },
|
2026-03-27 16:56:06 +08:00
|
|
|
|
{ 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> },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-03-28 22:52:12 +08:00
|
|
|
|
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> },
|
2026-03-30 01:20:57 +08:00
|
|
|
|
{ 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> },
|
2026-03-28 22:52:12 +08:00
|
|
|
|
{ key: '/dashboard/user', label: <Link to="/dashboard/user">{t('menu.userManagement')}</Link> },
|
|
|
|
|
|
{ key: '/dashboard/role', label: <Link to="/dashboard/role">{t('menu.rolePermissions')}</Link> },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
2026-03-27 16:56:06 +08:00
|
|
|
|
], [t]);
|
2026-03-19 01:39:34 +08:00
|
|
|
|
|
2026-03-22 11:25:28 +08:00
|
|
|
|
const menuItems = useMemo(() => {
|
2026-03-23 12:41:35 +08:00
|
|
|
|
const stripCustomProps = (item: CustomMenuItem): MenuItem => {
|
2026-03-22 11:25:28 +08:00
|
|
|
|
const { requiredPermission, requiredFeature, ...rest } = item;
|
2026-03-23 12:41:35 +08:00
|
|
|
|
return rest as MenuItem;
|
2026-03-22 11:25:28 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-23 12:41:35 +08:00
|
|
|
|
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[] }));
|
2026-03-22 11:25:28 +08:00
|
|
|
|
}
|
2026-03-23 12:41:35 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
result.push(stripCustomProps(item));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return result;
|
2026-03-22 11:25:28 +08:00
|
|
|
|
};
|
|
|
|
|
|
return filterMenuItems(ALL_MENU_ITEMS);
|
2026-03-27 16:56:06 +08:00
|
|
|
|
}, [hasPermission, hasFeature, t]);
|
2026-03-22 11:25:28 +08:00
|
|
|
|
|
|
|
|
|
|
const getSelectedKeys = (): string[] => {
|
2026-03-19 01:39:34 +08:00
|
|
|
|
const pathname = location.pathname;
|
2026-03-22 11:25:28 +08:00
|
|
|
|
let bestMatch: string | null = null;
|
|
|
|
|
|
let bestMatchLength = 0;
|
|
|
|
|
|
|
|
|
|
|
|
const findMatch = (items: MenuItem[]) => {
|
|
|
|
|
|
items.forEach((item) => {
|
2026-03-21 15:04:06 +08:00
|
|
|
|
if (item && 'key' in item && typeof item.key === 'string') {
|
2026-03-22 11:25:28 +08:00
|
|
|
|
const key = item.key;
|
|
|
|
|
|
if (pathname === key || pathname.startsWith(key + '/')) {
|
|
|
|
|
|
if (key.length > bestMatchLength) {
|
|
|
|
|
|
bestMatch = key;
|
|
|
|
|
|
bestMatchLength = key.length;
|
|
|
|
|
|
}
|
2026-03-19 14:19:01 +08:00
|
|
|
|
}
|
2026-03-22 11:25:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-03-19 14:19:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-22 11:25:28 +08:00
|
|
|
|
if (item && 'children' in item && Array.isArray(item.children)) {
|
|
|
|
|
|
const itemKey = 'key' in item ? item.key as string : undefined;
|
|
|
|
|
|
findParent(item.children, itemKey);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-03-19 14:19:01 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-22 11:25:28 +08:00
|
|
|
|
findParent(menuItems);
|
2026-03-23 12:41:35 +08:00
|
|
|
|
return Array.from(new Set(openKeys));
|
2026-03-22 11:25:28 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const selectedKeys = getSelectedKeys();
|
|
|
|
|
|
const openKeys = getOpenKeys();
|
2026-03-19 01:39:34 +08:00
|
|
|
|
|
|
|
|
|
|
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');
|
2026-03-22 11:25:28 +08:00
|
|
|
|
navigate('/auth/login');
|
2026-03-19 01:39:34 +08:00
|
|
|
|
} else if (key === 'profile') {
|
2026-03-22 11:25:28 +08:00
|
|
|
|
navigate('/dashboard/settings/profile');
|
2026-03-19 01:39:34 +08:00
|
|
|
|
} else if (key === 'settings') {
|
2026-03-22 11:25:28 +08:00
|
|
|
|
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})`);
|
2026-03-19 01:39:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-22 11:25:28 +08:00
|
|
|
|
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',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
// 如果当前路径不是以 /dashboard 开头,则只渲染 Outlet,避免布局嵌套
|
|
|
|
|
|
if (!location.pathname.startsWith('/dashboard')) {
|
|
|
|
|
|
return <Outlet context={{ currentUser, hasPermission, hasFeature, isPaidUser }} />;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 01:39:34 +08:00
|
|
|
|
return (
|
2026-03-27 16:56:06 +08:00
|
|
|
|
<Layout style={{ minHeight: '100vh', display: 'flex' }}>
|
2026-03-19 01:39:34 +08:00
|
|
|
|
<Sider
|
|
|
|
|
|
trigger={null}
|
|
|
|
|
|
collapsible
|
|
|
|
|
|
collapsed={collapsed}
|
2026-03-22 11:25:28 +08:00
|
|
|
|
width={collapsed ? 80 : 200}
|
2026-03-19 01:39:34 +08:00
|
|
|
|
style={{
|
|
|
|
|
|
background: '#001529',
|
2026-03-27 16:56:06 +08:00
|
|
|
|
zIndex: 99,
|
2026-03-22 11:25:28 +08:00
|
|
|
|
padding: 0,
|
2026-03-19 01:39:34 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-22 11:25:28 +08:00
|
|
|
|
<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>
|
2026-03-19 01:39:34 +08:00
|
|
|
|
|
2026-03-22 11:25:28 +08:00
|
|
|
|
<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>
|
2026-03-19 01:39:34 +08:00
|
|
|
|
</Sider>
|
|
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
2026-03-19 01:39:34 +08:00
|
|
|
|
<Header
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
padding: '0 24px',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
boxShadow: '0 1px 4px rgba(0, 21, 41, 0.08)',
|
2026-03-27 16:56:06 +08:00
|
|
|
|
zIndex: 100,
|
|
|
|
|
|
height: '64px',
|
|
|
|
|
|
flexShrink: 0,
|
2026-03-19 01:39:34 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-22 11:25:28 +08:00
|
|
|
|
<div style={{ fontSize: '16px', fontWeight: 500, color: '#333' }}>
|
2026-03-27 16:56:06 +08:00
|
|
|
|
{t('app.name')} {t('app.title')}
|
2026-03-19 01:39:34 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-22 11:25:28 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-03-19 01:39:34 +08:00
|
|
|
|
<Badge count={5} size="small">
|
|
|
|
|
|
<BellOutlined style={{ fontSize: '18px', cursor: 'pointer', color: '#666' }} />
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
|
2026-03-28 22:52:12 +08:00
|
|
|
|
<ThemeSwitch />
|
|
|
|
|
|
|
2026-03-23 12:41:35 +08:00
|
|
|
|
<Dropdown
|
|
|
|
|
|
menu={{
|
|
|
|
|
|
items: [
|
2026-03-27 16:56:06 +08:00
|
|
|
|
{ key: 'zh-CN', label: t('settings.language') === 'Language' ? '简体中文' : 'Chinese' },
|
|
|
|
|
|
{ key: 'en-US', label: t('settings.language') === 'Language' ? 'English' : '英文' },
|
2026-03-23 12:41:35 +08:00
|
|
|
|
],
|
|
|
|
|
|
onClick: ({ key }) => setLocale(key),
|
|
|
|
|
|
selectedKeys: [locale],
|
|
|
|
|
|
}}
|
|
|
|
|
|
placement="bottomRight"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button type="text" icon={<GlobalOutlined />} style={{ height: '32px' }}>
|
2026-03-27 16:56:06 +08:00
|
|
|
|
{locale === 'zh-CN' ? t('settings.language') === 'Language' ? '中文' : 'Chinese' : t('settings.language') === 'Language' ? 'EN' : '英文'}
|
2026-03-23 12:41:35 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
</Dropdown>
|
|
|
|
|
|
|
2026-03-19 01:39:34 +08:00
|
|
|
|
<Dropdown
|
|
|
|
|
|
menu={{ items: userMenuItems, onClick: handleUserMenuClick }}
|
|
|
|
|
|
placement="bottomRight"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Space style={{ cursor: 'pointer' }}>
|
|
|
|
|
|
<Avatar style={{ backgroundColor: '#1890ff' }} icon={<UserOutlined />} />
|
2026-03-22 11:25:28 +08:00
|
|
|
|
<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' }} />
|
2026-03-19 01:39:34 +08:00
|
|
|
|
</Space>
|
|
|
|
|
|
</Dropdown>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Header>
|
|
|
|
|
|
|
|
|
|
|
|
<Content
|
|
|
|
|
|
style={{
|
|
|
|
|
|
margin: '24px',
|
|
|
|
|
|
padding: '24px',
|
|
|
|
|
|
background: '#f0f2f5',
|
|
|
|
|
|
minHeight: 280,
|
2026-03-27 16:56:06 +08:00
|
|
|
|
flex: 1,
|
2026-03-19 01:39:34 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
padding: '24px',
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
minHeight: 'calc(100vh - 184px)',
|
|
|
|
|
|
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.06)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-28 22:52:12 +08:00
|
|
|
|
<Breadcrumb />
|
2026-03-22 11:25:28 +08:00
|
|
|
|
<Outlet context={{ currentUser, hasPermission, hasFeature, isPaidUser }} />
|
2026-03-19 01:39:34 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</Content>
|
2026-03-27 16:56:06 +08:00
|
|
|
|
</div>
|
2026-03-19 01:39:34 +08:00
|
|
|
|
</Layout>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 16:56:06 +08:00
|
|
|
|
const MainLayout: FC = () => {
|
2026-03-22 11:25:28 +08:00
|
|
|
|
return (
|
2026-03-28 22:52:12 +08:00
|
|
|
|
<ThemeProvider>
|
|
|
|
|
|
<LocaleProvider>
|
|
|
|
|
|
<UserProvider>
|
|
|
|
|
|
<MainLayoutContent />
|
|
|
|
|
|
</UserProvider>
|
|
|
|
|
|
</LocaleProvider>
|
|
|
|
|
|
</ThemeProvider>
|
2026-03-22 11:25:28 +08:00
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-19 01:39:34 +08:00
|
|
|
|
export default MainLayout;
|