Sidebar.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import * as React from 'react';
  2. import { NavItem, NavItemKey } from '../types';
  3. import { Icon } from './ui/Icon';
  4. import { useTranslation, Language } from '../hooks/useI18n';
  5. interface SidebarProps {
  6. activePageName: string;
  7. activePageThemeColor: string;
  8. onOpenPageManager: () => void;
  9. activeNavKey: NavItemKey;
  10. setActiveNavKey: (page: NavItemKey) => void;
  11. theme: 'light' | 'dark';
  12. setTheme: (theme: 'light' | 'dark') => void;
  13. currentUser: string;
  14. onLogout: () => void;
  15. }
  16. const NavMenu: React.FC<{
  17. activeKey: NavItemKey;
  18. onSelect: (key: NavItemKey) => void;
  19. }> = ({ activeKey, onSelect }) => {
  20. const { t } = useTranslation();
  21. const navItems: NavItem[] = [
  22. {
  23. name: t('nav.page'),
  24. key: 'Page',
  25. icon: <path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />,
  26. children: [
  27. { name: t('nav.page.link'), key: 'Page.Link' },
  28. { name: t('nav.page.media'), key: 'Page.Media' },
  29. { name: t('nav.page.design'), key: 'Page.Design' },
  30. ],
  31. },
  32. {
  33. name: t('nav.ai_assistant'),
  34. key: 'AI Assistant',
  35. icon: <path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />,
  36. children: [
  37. { name: t('nav.ai_assistant.persona'), key: 'AI Assistant.Persona' },
  38. { name: t('nav.ai_assistant.knowledge'), key: 'AI Assistant.Knowledge' },
  39. { name: t('nav.ai_assistant.sensitivity'), key: 'AI Assistant.Sensitivity' },
  40. ],
  41. },
  42. {
  43. name: t('nav.analytics'),
  44. key: 'Analytics',
  45. icon: <path strokeLinecap="round" strokeLinejoin="round" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4m-4-11v11" />,
  46. children: [
  47. { name: t('nav.analytics.page'), key: 'Analytics.Page' },
  48. { name: t('nav.analytics.interactions'), key: 'Analytics.Interactions' },
  49. { name: t('nav.analytics.crm'), key: 'Analytics.CRM' },
  50. ],
  51. },
  52. {
  53. name: t('nav.seo'),
  54. key: 'SEO',
  55. icon: <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />,
  56. children: [
  57. { name: t('nav.seo.short_links'), key: 'SEO.ShortLinks' },
  58. { name: t('nav.seo.hosting'), key: 'SEO.Hosting' },
  59. { name: t('nav.seo.services'), key: 'SEO.Services' },
  60. ],
  61. },
  62. {
  63. name: t('nav.aigc'),
  64. key: 'AIGC',
  65. icon: <path strokeLinecap="round" strokeLinejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c.251.023.501.05.75.082a9.75 9.75 0 016 6.062c.313.958.5 1.965.5 3.004v.75a2.25 2.25 0 01-2.25 2.25H3.75a2.25 2.25 0 01-2.25-2.25v-.75c0-1.04.187-2.046.5-3.004a9.75 9.75 0 016-6.062 12.312 12.312 0 01.75-.082zM9.75 18.75c-2.482 0-4.72-1.22-6.16-3.223" />,
  66. children: [
  67. { name: t('nav.aigc.creator'), key: 'AIGC.Creator' },
  68. { name: t('nav.aigc.news'), key: 'AIGC.News' },
  69. { name: t('nav.aigc.scheduler'), key: 'AIGC.Scheduler' },
  70. ]
  71. },
  72. {
  73. name: t('nav.showcase'),
  74. key: 'Showcase',
  75. icon: <path strokeLinecap="round" strokeLinejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />,
  76. children: [
  77. { name: t('nav.showcase.personal'), key: 'Showcase.Personal' },
  78. { name: t('nav.showcase.enterprise'), key: 'Showcase.Enterprise' },
  79. ],
  80. },
  81. ];
  82. return (
  83. <nav>
  84. {navItems.map(item => {
  85. const isActive = activeKey.startsWith(item.key);
  86. return (
  87. <div key={item.key} className="mb-2">
  88. <button
  89. onClick={() => onSelect(item.children ? item.children[0].key : item.key)}
  90. className={`w-full flex items-center gap-3 p-3 rounded-lg text-left transition-colors ${isActive ? 'bg-brand-primary/10 text-brand-primary' : 'hover:bg-gray-200 dark:hover:bg-gray-700'}`}
  91. >
  92. <Icon className="h-6 w-6">{item.icon}</Icon>
  93. <span className="font-semibold">{item.name}</span>
  94. </button>
  95. {item.children && isActive && (
  96. <div className="mt-2 pl-9 space-y-1">
  97. {item.children.map(child => (
  98. <button
  99. key={child.key}
  100. onClick={() => onSelect(child.key)}
  101. className={`block w-full text-left text-sm p-2 rounded-md transition-colors ${activeKey === child.key ? 'text-brand-primary font-semibold' : 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'}`}
  102. >
  103. {child.name}
  104. </button>
  105. ))}
  106. </div>
  107. )}
  108. </div>
  109. );
  110. })}
  111. </nav>
  112. );
  113. };
  114. export const Sidebar: React.FC<SidebarProps> = ({ activePageName, activePageThemeColor, onOpenPageManager, activeNavKey, setActiveNavKey, theme, setTheme, currentUser, onLogout }) => {
  115. const { t, language, setLanguage } = useTranslation();
  116. return (
  117. <aside className="w-64 bg-white dark:bg-gray-800 p-4 flex flex-col border-r border-gray-200 dark:border-gray-700">
  118. <div className="flex-shrink-0 mb-6">
  119. <button onClick={onOpenPageManager} className={`w-full bg-gradient-to-br ${activePageThemeColor} p-3 rounded-lg text-left shadow-lg`}>
  120. <div className="font-bold text-white truncate">{activePageName}</div>
  121. <div className="text-xs text-white/80">{t('sidebar.select_page')}</div>
  122. </button>
  123. </div>
  124. <div className="flex-1 overflow-y-auto pr-2 -mr-4">
  125. <NavMenu activeKey={activeNavKey} onSelect={setActiveNavKey} />
  126. </div>
  127. <div className="flex-shrink-0 space-y-4 pt-4">
  128. <div className="p-3 bg-gray-100 dark:bg-gray-900 rounded-lg flex items-center justify-between">
  129. <div className="min-w-0">
  130. <p className="text-sm font-semibold truncate text-gray-800 dark:text-gray-200" title={currentUser}>{currentUser}</p>
  131. </div>
  132. <button onClick={onLogout} title={t('sidebar.logout')} className="text-gray-400 dark:text-gray-500 hover:text-red-500">
  133. <Icon className="h-5 w-5"><path strokeLinecap="round" strokeLinejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></Icon>
  134. </button>
  135. </div>
  136. <div className="flex items-center justify-center gap-2 p-2 bg-gray-100 dark:bg-gray-900 rounded-lg">
  137. <button onClick={() => setTheme('light')} className={`flex-1 p-2 text-sm rounded-md ${theme === 'light' ? 'bg-brand-primary text-white' : ''}`}>{t('sidebar.light_theme')}</button>
  138. <button onClick={() => setTheme('dark')} className={`flex-1 p-2 text-sm rounded-md ${theme === 'dark' ? 'bg-brand-primary text-white' : ''}`}>{t('sidebar.dark_theme')}</button>
  139. </div>
  140. <div>
  141. <select
  142. value={language}
  143. onChange={(e) => setLanguage(e.target.value as Language)}
  144. aria-label={t('sidebar.language')}
  145. className="w-full bg-gray-100 dark:bg-gray-900 p-2 rounded-lg text-sm text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-700 focus:ring-brand-primary focus:border-brand-primary"
  146. >
  147. <option value="en">English</option>
  148. <option value="ja">日本語</option>
  149. <option value="zh">中文</option>
  150. <option value="ko">한국어</option>
  151. </select>
  152. </div>
  153. </div>
  154. </aside>
  155. );
  156. };