374 lines
13 KiB
TypeScript
374 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
import { usePathname, useRouter } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import { t } from '@/i18n/locales';
|
|
import { useAuth } from '@/lib/auth-context';
|
|
|
|
/**
|
|
* D. Web管理前端 - 主布局
|
|
*
|
|
* 左侧Sidebar导航 + 顶部Header + 主内容区
|
|
* 覆盖 D1-D8 所有管理模块的路由结构
|
|
*/
|
|
|
|
interface NavItem {
|
|
key: string;
|
|
icon: string;
|
|
label: string;
|
|
children?: NavItem[];
|
|
badge?: number;
|
|
}
|
|
|
|
const navItems: NavItem[] = [
|
|
{ key: 'dashboard', icon: '📊', label: t('nav_dashboard') },
|
|
{
|
|
key: 'issuers', icon: '🏢', label: t('nav_issuers'),
|
|
children: [
|
|
{ key: 'issuers/review', icon: '', label: t('nav_issuers_review') },
|
|
{ key: 'issuers/list', icon: '', label: t('nav_issuers_list') },
|
|
{ key: 'issuers/coupons', icon: '', label: t('nav_issuers_coupons') },
|
|
],
|
|
},
|
|
{
|
|
key: 'users', icon: '👤', label: t('nav_users'),
|
|
children: [
|
|
{ key: 'users/list', icon: '', label: t('nav_users_list') },
|
|
{ key: 'users/kyc', icon: '', label: t('nav_users_kyc'), badge: 5 },
|
|
{ key: 'users/sms-logs', icon: '', label: t('nav_users_sms_logs') },
|
|
],
|
|
},
|
|
{
|
|
key: 'trading', icon: '💱', label: t('nav_trading'),
|
|
children: [
|
|
{ key: 'trading/realtime', icon: '', label: t('nav_trading_realtime') },
|
|
{ key: 'trading/stats', icon: '', label: t('nav_trading_stats') },
|
|
{ key: 'trading/orders', icon: '', label: t('nav_trading_orders') },
|
|
],
|
|
},
|
|
{
|
|
key: 'risk', icon: '🛡️', label: t('nav_risk'),
|
|
children: [
|
|
{ key: 'risk/dashboard', icon: '', label: t('nav_risk_dashboard'), badge: 3 },
|
|
{ key: 'risk/suspicious', icon: '', label: t('nav_risk_suspicious') },
|
|
{ key: 'risk/blacklist', icon: '', label: t('nav_risk_blacklist') },
|
|
{ key: 'risk/ofac', icon: '', label: t('nav_risk_ofac') },
|
|
],
|
|
},
|
|
{
|
|
key: 'compliance', icon: '📋', label: t('nav_compliance'),
|
|
children: [
|
|
{ key: 'compliance/sar', icon: '', label: t('nav_compliance_sar') },
|
|
{ key: 'compliance/ctr', icon: '', label: t('nav_compliance_ctr') },
|
|
{ key: 'compliance/audit', icon: '', label: t('nav_compliance_audit') },
|
|
{ key: 'compliance/reports', icon: '', label: t('nav_compliance_reports') },
|
|
],
|
|
},
|
|
{ key: 'app-versions', icon: '📱', label: t('nav_app_versions') },
|
|
{
|
|
key: 'system', icon: '⚙️', label: t('nav_system'),
|
|
children: [
|
|
{ key: 'system/admins', icon: '', label: t('nav_system_admins') },
|
|
{ key: 'system/config', icon: '', label: t('nav_system_config') },
|
|
{ key: 'system/contracts', icon: '', label: t('nav_system_contracts') },
|
|
{ key: 'system/monitor', icon: '', label: t('nav_system_monitor') },
|
|
],
|
|
},
|
|
{ key: 'disputes', icon: '⚖️', label: t('nav_disputes'), badge: 8 },
|
|
{ key: 'coupons', icon: '🎫', label: t('nav_coupons_mgmt') },
|
|
{ key: 'finance', icon: '💰', label: t('nav_finance') },
|
|
{ key: 'chain', icon: '⛓️', label: t('nav_chain') },
|
|
{ key: 'reports', icon: '📈', label: t('nav_reports') },
|
|
{ key: 'merchant', icon: '🏪', label: t('nav_merchant') },
|
|
{ key: 'agent', icon: '🤖', label: t('nav_agent') },
|
|
{ key: 'insurance', icon: '🛡️', label: t('nav_insurance') },
|
|
{
|
|
key: 'analytics', icon: '📊', label: t('nav_analytics'),
|
|
children: [
|
|
{ key: 'analytics/users', icon: '', label: t('nav_analytics_users') },
|
|
{ key: 'analytics/coupons', icon: '', label: t('nav_analytics_coupons') },
|
|
{ key: 'analytics/market-maker', icon: '', label: t('nav_analytics_market_maker') },
|
|
{ key: 'analytics/consumer-protection', icon: '', label: t('nav_analytics_consumer_protection') },
|
|
],
|
|
},
|
|
];
|
|
|
|
export const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
const pathname = usePathname();
|
|
const router = useRouter();
|
|
const { isAuthenticated, isLoading, user, logout } = useAuth();
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
|
|
// 未登录 → /login
|
|
if (!isLoading && !isAuthenticated) {
|
|
router.replace('/login');
|
|
return null;
|
|
}
|
|
// 加载中显示空白
|
|
if (isLoading) return null;
|
|
|
|
// Derive activeKey from current pathname
|
|
const activeKey = pathname.replace(/^\//, '') || 'dashboard';
|
|
|
|
// Auto-expand parent nav items based on current path
|
|
const getInitialExpanded = () => {
|
|
const expanded: string[] = [];
|
|
navItems.forEach(item => {
|
|
if (item.children && item.children.some(c => activeKey.startsWith(c.key) || activeKey === item.key)) {
|
|
expanded.push(item.key);
|
|
}
|
|
});
|
|
return expanded;
|
|
};
|
|
const [expandedKeys, setExpandedKeys] = useState<string[]>(getInitialExpanded);
|
|
|
|
const toggleExpand = (key: string) => {
|
|
setExpandedKeys(prev =>
|
|
prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key]
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div style={{ display: 'flex', height: '100vh', background: 'var(--color-bg)' }}>
|
|
{/* Sidebar */}
|
|
<aside
|
|
style={{
|
|
width: collapsed ? 'var(--sidebar-collapsed-width)' : 'var(--sidebar-width)',
|
|
background: 'var(--color-surface)',
|
|
borderRight: '1px solid var(--color-border-light)',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
transition: 'width 0.2s ease',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
{/* Logo */}
|
|
<div style={{
|
|
height: 'var(--header-height)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
padding: '0 20px',
|
|
borderBottom: '1px solid var(--color-border-light)',
|
|
}}>
|
|
<img
|
|
src="/logo_icon.png"
|
|
alt="Genex"
|
|
style={{ width: 32, height: 32, borderRadius: 'var(--radius-sm)' }}
|
|
/>
|
|
{!collapsed && (
|
|
<span style={{
|
|
marginLeft: 12,
|
|
font: 'var(--text-h3)',
|
|
color: 'var(--color-primary)',
|
|
}}>
|
|
Genex Admin
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Nav */}
|
|
<nav style={{ flex: 1, overflow: 'auto', padding: '12px 8px' }}>
|
|
{navItems.map(item => (
|
|
<div key={item.key}>
|
|
<button
|
|
onClick={() => {
|
|
if (item.children) {
|
|
toggleExpand(item.key);
|
|
} else {
|
|
router.push('/' + item.key);
|
|
}
|
|
}}
|
|
style={{
|
|
width: '100%',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
padding: '10px 12px',
|
|
border: 'none',
|
|
borderRadius: 'var(--radius-sm)',
|
|
background: activeKey === item.key || activeKey.startsWith(item.key + '/') ? 'var(--color-primary-surface)' : 'transparent',
|
|
color: activeKey === item.key || activeKey.startsWith(item.key + '/') ? 'var(--color-primary)' : 'var(--color-text-secondary)',
|
|
cursor: 'pointer',
|
|
font: 'var(--text-label)',
|
|
marginBottom: 2,
|
|
textAlign: 'left',
|
|
}}
|
|
>
|
|
<span style={{ fontSize: 18, width: 24 }}>{item.icon}</span>
|
|
{!collapsed && (
|
|
<>
|
|
<span style={{ flex: 1, marginLeft: 8 }}>{item.label}</span>
|
|
{item.badge && (
|
|
<span style={{
|
|
background: 'var(--color-error)',
|
|
color: 'white',
|
|
borderRadius: 'var(--radius-full)',
|
|
padding: '1px 6px',
|
|
fontSize: 10,
|
|
fontWeight: 600,
|
|
}}>
|
|
{item.badge}
|
|
</span>
|
|
)}
|
|
{item.children && (
|
|
<span style={{ fontSize: 12, transform: expandedKeys.includes(item.key) ? 'rotate(90deg)' : 'none', transition: 'transform 0.2s' }}>
|
|
▸
|
|
</span>
|
|
)}
|
|
</>
|
|
)}
|
|
</button>
|
|
{/* Sub items */}
|
|
{item.children && expandedKeys.includes(item.key) && !collapsed && (
|
|
<div style={{ marginLeft: 36, marginBottom: 4 }}>
|
|
{item.children.map(sub => (
|
|
<button
|
|
key={sub.key}
|
|
onClick={() => router.push('/' + sub.key)}
|
|
style={{
|
|
width: '100%',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
padding: '8px 12px',
|
|
border: 'none',
|
|
borderRadius: 'var(--radius-sm)',
|
|
background: activeKey === sub.key ? 'var(--color-primary-surface)' : 'transparent',
|
|
color: activeKey === sub.key ? 'var(--color-primary)' : 'var(--color-text-tertiary)',
|
|
cursor: 'pointer',
|
|
font: 'var(--text-body-sm)',
|
|
textAlign: 'left',
|
|
}}
|
|
>
|
|
<span style={{ flex: 1 }}>{sub.label}</span>
|
|
{sub.badge && (
|
|
<span style={{
|
|
background: 'var(--color-error)',
|
|
color: 'white',
|
|
borderRadius: 'var(--radius-full)',
|
|
padding: '1px 6px',
|
|
fontSize: 10,
|
|
}}>
|
|
{sub.badge}
|
|
</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</nav>
|
|
|
|
{/* Collapse toggle */}
|
|
<button
|
|
onClick={() => setCollapsed(!collapsed)}
|
|
style={{
|
|
padding: 12,
|
|
border: 'none',
|
|
borderTop: '1px solid var(--color-border-light)',
|
|
background: 'none',
|
|
cursor: 'pointer',
|
|
color: 'var(--color-text-tertiary)',
|
|
}}
|
|
>
|
|
{collapsed ? '→' : `← ${t('collapse')}`}
|
|
</button>
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
{/* Header */}
|
|
<header style={{
|
|
height: 'var(--header-height)',
|
|
background: 'var(--color-surface)',
|
|
borderBottom: '1px solid var(--color-border-light)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
padding: '0 24px',
|
|
justifyContent: 'space-between',
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<input
|
|
placeholder={t('header_search_placeholder')}
|
|
style={{
|
|
width: 320,
|
|
height: 36,
|
|
border: '1px solid var(--color-border)',
|
|
borderRadius: 'var(--radius-full)',
|
|
padding: '0 16px',
|
|
font: 'var(--text-body)',
|
|
background: 'var(--color-gray-50)',
|
|
outline: 'none',
|
|
}}
|
|
/>
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
|
{/* AI Agent Button */}
|
|
<button style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
padding: '6px 14px',
|
|
border: '1px solid var(--color-primary)',
|
|
borderRadius: 'var(--radius-full)',
|
|
background: 'var(--color-primary-surface)',
|
|
color: 'var(--color-primary)',
|
|
cursor: 'pointer',
|
|
font: 'var(--text-label-sm)',
|
|
}}>
|
|
✨ {t('header_ai_assistant')}
|
|
</button>
|
|
{/* Notifications */}
|
|
<button style={{
|
|
position: 'relative',
|
|
border: 'none',
|
|
background: 'none',
|
|
cursor: 'pointer',
|
|
fontSize: 20,
|
|
}}>
|
|
🔔
|
|
<span style={{
|
|
position: 'absolute',
|
|
top: -2, right: -2,
|
|
width: 8, height: 8,
|
|
background: 'var(--color-error)',
|
|
borderRadius: '50%',
|
|
}} />
|
|
</button>
|
|
{/* Admin avatar + logout */}
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
cursor: 'pointer',
|
|
}}
|
|
onClick={logout}
|
|
title={t('header_logout')}
|
|
>
|
|
<div style={{
|
|
width: 32, height: 32,
|
|
borderRadius: '50%',
|
|
background: 'var(--color-primary)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: 'white',
|
|
fontSize: 14,
|
|
fontWeight: 600,
|
|
}}>
|
|
{user?.name?.charAt(0)?.toUpperCase() || 'A'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Page Content */}
|
|
<main style={{ flex: 1, overflow: 'auto', padding: 24 }}>
|
|
{children}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|