AdminLayout.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import {
  2. ApiOutlined,
  3. AppstoreOutlined,
  4. CloudServerOutlined,
  5. DashboardOutlined,
  6. GlobalOutlined,
  7. InboxOutlined,
  8. KeyOutlined,
  9. MailOutlined,
  10. ReloadOutlined,
  11. SafetyCertificateOutlined,
  12. SendOutlined,
  13. SettingOutlined,
  14. UserOutlined
  15. } from '@ant-design/icons';
  16. import { Avatar, Breadcrumb, Button, Dropdown, Layout, Menu, Select, Space, Typography } from 'antd';
  17. import type { ReactNode } from 'react';
  18. import { useI18n } from '../frontend/i18n/react';
  19. import type { User, ViewKey } from '../frontend/types';
  20. const { Header, Sider, Content } = Layout;
  21. const navGroups: Array<{
  22. key: string;
  23. labelKey: string;
  24. items: Array<{ key: ViewKey; labelKey: string; icon: ReactNode; adminOnly?: boolean }>;
  25. }> = [
  26. {
  27. key: 'overview',
  28. labelKey: 'nav.group.overview',
  29. items: [
  30. { key: 'dashboard', labelKey: 'nav.dashboard', icon: <DashboardOutlined /> },
  31. { key: 'domains', labelKey: 'nav.domains', icon: <GlobalOutlined /> },
  32. { key: 'dns-api', labelKey: 'nav.dnsApi', icon: <CloudServerOutlined /> }
  33. ]
  34. },
  35. {
  36. key: 'delivery',
  37. labelKey: 'nav.group.delivery',
  38. items: [
  39. { key: 'smtp', labelKey: 'nav.smtp', icon: <MailOutlined /> },
  40. { key: 'inbox', labelKey: 'nav.inbox', icon: <InboxOutlined /> },
  41. { key: 'tokens', labelKey: 'nav.tokens', icon: <KeyOutlined /> },
  42. { key: 'logs', labelKey: 'nav.logs', icon: <SendOutlined /> },
  43. { key: 'webhooks', labelKey: 'nav.webhooks', icon: <ApiOutlined /> }
  44. ]
  45. },
  46. {
  47. key: 'system',
  48. labelKey: 'nav.group.system',
  49. items: [
  50. { key: 'admin', labelKey: 'nav.admin', icon: <SafetyCertificateOutlined />, adminOnly: true },
  51. { key: 'settings', labelKey: 'nav.settings', icon: <SettingOutlined /> }
  52. ]
  53. }
  54. ];
  55. interface AdminLayoutProps {
  56. activeView: ViewKey;
  57. breadcrumb: string[];
  58. user: User | null;
  59. runtimeLine: string;
  60. loading: boolean;
  61. children: ReactNode;
  62. onViewChange: (view: ViewKey) => void;
  63. onRefresh: () => void;
  64. onAddDomain: () => void;
  65. onLogout: () => void;
  66. }
  67. export function AdminLayout({
  68. activeView,
  69. breadcrumb,
  70. user,
  71. runtimeLine,
  72. loading,
  73. children,
  74. onViewChange,
  75. onRefresh,
  76. onAddDomain,
  77. onLogout
  78. }: AdminLayoutProps) {
  79. const { locale, locales, setLocale, t } = useI18n();
  80. const isAdmin = user?.role === 'admin';
  81. const menuItems = navGroups.map((group) => ({
  82. type: 'group' as const,
  83. key: group.key,
  84. label: t(group.labelKey),
  85. children: group.items
  86. .filter((item) => !item.adminOnly || isAdmin)
  87. .map((item) => ({
  88. key: item.key,
  89. icon: item.icon,
  90. label: t(item.labelKey)
  91. }))
  92. }));
  93. return (
  94. <Layout className="admin-layout">
  95. <Sider breakpoint="lg" collapsedWidth={0} width={248} className="admin-sider">
  96. <div className="admin-sider-inner">
  97. <div className="brand">
  98. <div className="brand-logo">MH</div>
  99. <div>
  100. <div className="brand-title">MailHub</div>
  101. <div className="brand-subtitle">Email Delivery</div>
  102. </div>
  103. </div>
  104. <Menu
  105. theme="dark"
  106. mode="inline"
  107. selectedKeys={[activeView]}
  108. items={menuItems}
  109. onClick={({ key }) => onViewChange(key as ViewKey)}
  110. className="admin-menu"
  111. />
  112. {user ? (
  113. <div className="admin-sider-footer">
  114. <Avatar size={28} icon={<UserOutlined />} className="sider-user-avatar" />
  115. <div className="sider-user-meta">
  116. <div className="sider-user-name">{user.username || t('common.user')}</div>
  117. <div className="sider-user-role">{user.role || '—'}</div>
  118. </div>
  119. </div>
  120. ) : null}
  121. </div>
  122. </Sider>
  123. <Layout>
  124. <Header className="admin-header">
  125. <div className="header-title">
  126. <Breadcrumb items={breadcrumb.map((title) => ({ title }))} className="header-breadcrumb" />
  127. <Typography.Text type="secondary" className="runtime-line">
  128. {runtimeLine}
  129. </Typography.Text>
  130. </div>
  131. <Space wrap className="header-actions">
  132. <Select
  133. aria-label="Language"
  134. value={locale}
  135. options={locales}
  136. onChange={setLocale}
  137. className="language-select"
  138. />
  139. <Button icon={<ReloadOutlined />} loading={loading} onClick={onRefresh}>
  140. {t('common.refresh')}
  141. </Button>
  142. <Button type="primary" icon={<AppstoreOutlined />} onClick={onAddDomain}>
  143. {t('common.addDomain')}
  144. </Button>
  145. <Dropdown
  146. menu={{
  147. items: [
  148. { key: 'profile', label: user?.email || user?.username || t('common.account'), disabled: true },
  149. { key: 'logout', label: t('common.logout'), onClick: onLogout }
  150. ]
  151. }}
  152. >
  153. <Button className="user-button">
  154. <Space>
  155. <Avatar size={24} icon={<UserOutlined />} />
  156. <span>{user?.username || t('common.user')}</span>
  157. </Space>
  158. </Button>
  159. </Dropdown>
  160. </Space>
  161. </Header>
  162. <Content className="admin-content">{children}</Content>
  163. </Layout>
  164. </Layout>
  165. );
  166. }