perf: complete sidebar menu

This commit is contained in:
Junyan Qin
2025-08-12 21:02:40 +08:00
parent f7cd6b76f2
commit d6a5fdd911
8 changed files with 210 additions and 179 deletions

View File

@@ -11,16 +11,17 @@ import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConf
import langbotIcon from '@/app/assets/langbot-logo.webp';
import { systemInfo } from '@/app/infra/http/HttpClient';
import { useTranslation } from 'react-i18next';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Moon, Sun, Monitor } from 'lucide-react';
import { useTheme } from 'next-themes';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { LanguageSelector } from '@/components/ui/language-selector';
// TODO 侧边导航栏要加动画
export default function HomeSidebar({
@@ -37,8 +38,10 @@ export default function HomeSidebar({
}, [pathname]);
const [selectedChild, setSelectedChild] = useState<SidebarChildVO>();
const { theme, setTheme } = useTheme();
const { t } = useTranslation();
const [popoverOpen, setPopoverOpen] = useState(false);
const [languageSelectorOpen, setLanguageSelectorOpen] = useState(false);
useEffect(() => {
initSelect();
@@ -178,9 +181,32 @@ export default function HomeSidebar({
}
name={t('common.helpDocs')}
/>
{/* <SidebarChild
onClick={() => {
handleLogout();
}}
isSelected={false}
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M4 18H6V20H18V4H6V6H4V3C4 2.44772 4.44772 2 5 2H19C19.5523 2 20 2.44772 20 3V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V18ZM6 11H13V13H6V16L1 12L6 8V11Z"></path>
</svg>
}
name={t('common.logout')}
/> */}
<DropdownMenu>
<DropdownMenuTrigger>
<Popover
open={popoverOpen}
onOpenChange={(open) => {
// 防止语言选择器打开时关闭popover
if (!open && languageSelectorOpen) return;
setPopoverOpen(open);
}}
>
<PopoverTrigger>
<SidebarChild
onClick={() => {}}
isSelected={false}
@@ -195,26 +221,65 @@ export default function HomeSidebar({
}
name={t('common.accountOptions')}
/>
</DropdownMenuTrigger>
<DropdownMenuContent side="right" align="end">
{/* <DropdownMenuLabel>My Account</DropdownMenuLabel> */}
<DropdownMenuItem>{/* 主题颜色切换 */}</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
handleLogout();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
</PopoverTrigger>
<PopoverContent
side="right"
align="end"
className="w-auto p-4 flex flex-col gap-4"
>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm font-medium">{t('common.theme')}</span>
<ToggleGroup
type="single"
value={theme}
onValueChange={(value) => {
if (value) setTheme(value);
}}
className="justify-start"
>
<path d="M4 18H6V20H18V4H6V6H4V3C4 2.44772 4.44772 2 5 2H19C19.5523 2 20 2.44772 20 3V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V18ZM6 11H13V13H6V16L1 12L6 8V11Z"></path>
</svg>
{t('common.logout')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<ToggleGroupItem value="light" size="sm">
<Sun className="h-4 w-4" />
</ToggleGroupItem>
<ToggleGroupItem value="dark" size="sm">
<Moon className="h-4 w-4" />
</ToggleGroupItem>
<ToggleGroupItem value="system" size="sm">
<Monitor className="h-4 w-4" />
</ToggleGroupItem>
</ToggleGroup>
</div>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm font-medium">
{t('common.language')}
</span>
<LanguageSelector
triggerClassName="w-full"
onOpenChange={setLanguageSelectorOpen}
/>
</div>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm font-medium">{t('common.account')}</span>
<Button
variant="ghost"
className="w-full justify-start font-normal"
onClick={() => {
handleLogout();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M4 18H6V20H18V4H6V6H4V3C4 2.44772 4.44772 2 5 2H19C19.5523 2 20 2.44772 20 3V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V18ZM6 11H13V13H6V16L1 12L6 8V11Z"></path>
</svg>
{t('common.logout')}
</Button>
</div>
</PopoverContent>
</Popover>
</div>
</div>
);

View File

@@ -8,13 +8,7 @@ import {
CardTitle,
CardDescription,
} from '@/components/ui/card';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { LanguageSelector } from '@/components/ui/language-selector';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
@@ -26,14 +20,13 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { useRouter } from 'next/navigation';
import { Mail, Lock, Globe } from 'lucide-react';
import { Mail, Lock } from 'lucide-react';
import langbotIcon from '@/app/assets/langbot-logo.webp';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import i18n from '@/i18n';
import Link from 'next/link';
import { ThemeToggle } from '@/components/ui/theme-toggle';
@@ -46,7 +39,6 @@ const formSchema = (t: (key: string) => string) =>
export default function Login() {
const router = useRouter();
const { t } = useTranslation();
const [currentLanguage, setCurrentLanguage] = useState<string>(i18n.language);
const form = useForm<z.infer<ReturnType<typeof formSchema>>>({
resolver: zodResolver(formSchema(t)),
@@ -57,57 +49,10 @@ export default function Login() {
});
useEffect(() => {
judgeLanguage();
getIsInitialized();
checkIfAlreadyLoggedIn();
}, []);
const judgeLanguage = () => {
if (i18n.language === 'zh-CN' || i18n.language === 'zh-Hans') {
setCurrentLanguage('zh-Hans');
localStorage.setItem('langbot_language', 'zh-Hans');
} else if (i18n.language === 'zh-TW' || i18n.language === 'zh-Hant') {
setCurrentLanguage('zh-Hant');
localStorage.setItem('langbot_language', 'zh-Hant');
} else if (i18n.language === 'ja' || i18n.language === 'ja-JP') {
setCurrentLanguage('ja-JP');
localStorage.setItem('langbot_language', 'ja-JP');
} else {
setCurrentLanguage('en-US');
localStorage.setItem('langbot_language', 'en-US');
}
// check if the language is already set
const lang = localStorage.getItem('langbot_language');
if (lang) {
i18n.changeLanguage(lang);
setCurrentLanguage(lang);
return;
} else {
const language = navigator.language;
if (language) {
let lang = 'zh-Hans';
if (language === 'zh-CN') {
lang = 'zh-Hans';
} else if (language === 'zh-TW') {
lang = 'zh-Hant';
} else if (language === 'ja' || language === 'ja-JP') {
lang = 'ja-JP';
} else {
lang = 'en-US';
}
i18n.changeLanguage(lang);
setCurrentLanguage(lang);
localStorage.setItem('langbot_language', lang);
}
}
};
const handleLanguageChange = (value: string) => {
i18n.changeLanguage(value);
setCurrentLanguage(value);
localStorage.setItem('langbot_language', value);
};
function getIsInitialized() {
httpClient
.checkIfInited()
@@ -161,21 +106,7 @@ export default function Login() {
<CardHeader>
<div className="flex justify-between items-center mb-6">
<ThemeToggle />
<Select
value={currentLanguage}
onValueChange={handleLanguageChange}
>
<SelectTrigger className="w-[140px]">
<Globe className="h-4 w-4 mr-2" />
<SelectValue placeholder={t('common.language')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="zh-Hans"></SelectItem>
<SelectItem value="zh-Hant"></SelectItem>
<SelectItem value="en-US">English</SelectItem>
<SelectItem value="ja-JP"></SelectItem>
</SelectContent>
</Select>
<LanguageSelector />
</div>
<img
src={langbotIcon.src}

View File

@@ -8,13 +8,7 @@ import {
CardTitle,
CardDescription,
} from '@/components/ui/card';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { LanguageSelector } from '@/components/ui/language-selector';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
@@ -26,14 +20,13 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { useRouter } from 'next/navigation';
import { Mail, Lock, Globe } from 'lucide-react';
import { Mail, Lock } from 'lucide-react';
import langbotIcon from '@/app/assets/langbot-logo.webp';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import i18n from '@/i18n';
import { ThemeToggle } from '@/components/ui/theme-toggle';
const formSchema = (t: (key: string) => string) =>
@@ -45,7 +38,6 @@ const formSchema = (t: (key: string) => string) =>
export default function Register() {
const router = useRouter();
const { t } = useTranslation();
const [currentLanguage, setCurrentLanguage] = useState<string>(i18n.language);
const form = useForm<z.infer<ReturnType<typeof formSchema>>>({
resolver: zodResolver(formSchema(t)),
@@ -56,58 +48,9 @@ export default function Register() {
});
useEffect(() => {
judgeLanguage();
getIsInitialized();
}, []);
const judgeLanguage = () => {
if (i18n.language === 'zh-CN' || i18n.language === 'zh-Hans') {
setCurrentLanguage('zh-Hans');
localStorage.setItem('langbot_language', 'zh-Hans');
} else if (i18n.language === 'zh-TW' || i18n.language === 'zh-Hant') {
setCurrentLanguage('zh-Hant');
localStorage.setItem('langbot_language', 'zh-Hant');
} else if (i18n.language === 'ja' || i18n.language === 'ja-JP') {
setCurrentLanguage('ja-JP');
localStorage.setItem('langbot_language', 'ja-JP');
} else {
setCurrentLanguage('en-US');
localStorage.setItem('langbot_language', 'en-US');
}
// check if the language is already set
const lang = localStorage.getItem('langbot_language');
console.log('lang: ', lang);
if (lang) {
i18n.changeLanguage(lang);
setCurrentLanguage(lang);
} else {
const language = navigator.language;
if (language) {
let lang = 'zh-Hans';
if (language === 'zh-CN') {
lang = 'zh-Hans';
} else if (language === 'zh-TW') {
lang = 'zh-Hant';
} else if (language === 'ja' || language === 'ja-JP') {
lang = 'ja-JP';
} else {
lang = 'en-US';
}
console.log('language: ', lang);
i18n.changeLanguage(lang);
setCurrentLanguage(lang);
localStorage.setItem('langbot_language', lang);
}
}
};
const handleLanguageChange = (value: string) => {
console.log('handleLanguageChange: ', value);
i18n.changeLanguage(value);
setCurrentLanguage(value);
localStorage.setItem('langbot_language', value);
};
function getIsInitialized() {
httpClient
.checkIfInited()
@@ -145,21 +88,7 @@ export default function Register() {
<CardHeader>
<div className="flex justify-between items-center mb-6">
<ThemeToggle />
<Select
value={currentLanguage}
onValueChange={handleLanguageChange}
>
<SelectTrigger className="w-[140px]">
<Globe className="h-4 w-4 mr-2" />
<SelectValue placeholder={t('common.language')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="zh-Hans"></SelectItem>
<SelectItem value="zh-Hant"></SelectItem>
<SelectItem value="en-US">English</SelectItem>
<SelectItem value="ja-JP"></SelectItem>
</SelectContent>
</Select>
<LanguageSelector />
</div>
<img
src={langbotIcon.src}

View File

@@ -0,0 +1,98 @@
'use client';
import { useState, useEffect } from 'react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Globe } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import i18n from '@/i18n';
interface LanguageSelectorProps {
className?: string;
triggerClassName?: string;
onOpenChange?: (open: boolean) => void;
}
export function LanguageSelector({
triggerClassName,
onOpenChange,
}: LanguageSelectorProps) {
const { t } = useTranslation();
const [currentLanguage, setCurrentLanguage] = useState<string>(i18n.language);
useEffect(() => {
initializeLanguage();
}, []);
const initializeLanguage = () => {
if (i18n.language === 'zh-CN' || i18n.language === 'zh-Hans') {
setCurrentLanguage('zh-Hans');
localStorage.setItem('langbot_language', 'zh-Hans');
} else if (i18n.language === 'zh-TW' || i18n.language === 'zh-Hant') {
setCurrentLanguage('zh-Hant');
localStorage.setItem('langbot_language', 'zh-Hant');
} else if (i18n.language === 'ja' || i18n.language === 'ja-JP') {
setCurrentLanguage('ja-JP');
localStorage.setItem('langbot_language', 'ja-JP');
} else {
setCurrentLanguage('en-US');
localStorage.setItem('langbot_language', 'en-US');
}
const savedLanguage = localStorage.getItem('langbot_language');
if (savedLanguage) {
i18n.changeLanguage(savedLanguage);
setCurrentLanguage(savedLanguage);
} else {
const browserLanguage = navigator.language;
if (browserLanguage) {
let detectedLanguage = 'zh-Hans';
if (browserLanguage === 'zh-CN') {
detectedLanguage = 'zh-Hans';
} else if (browserLanguage === 'zh-TW') {
detectedLanguage = 'zh-Hant';
} else if (browserLanguage === 'ja' || browserLanguage === 'ja-JP') {
detectedLanguage = 'ja-JP';
} else {
detectedLanguage = 'en-US';
}
i18n.changeLanguage(detectedLanguage);
setCurrentLanguage(detectedLanguage);
localStorage.setItem('langbot_language', detectedLanguage);
}
}
};
const handleLanguageChange = (value: string) => {
i18n.changeLanguage(value);
setCurrentLanguage(value);
localStorage.setItem('langbot_language', value);
// 刷新页面以应用新的语言设置
window.location.reload();
};
return (
<Select
value={currentLanguage}
onValueChange={handleLanguageChange}
onOpenChange={onOpenChange}
>
<SelectTrigger className={triggerClassName || 'w-[140px]'}>
<Globe className="h-4 w-4 mr-2" />
<SelectValue placeholder={t('common.language')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="zh-Hans"></SelectItem>
<SelectItem value="zh-Hant"></SelectItem>
<SelectItem value="en-US">English</SelectItem>
<SelectItem value="ja-JP"></SelectItem>
</SelectContent>
</Select>
);
}

View File

@@ -2,7 +2,8 @@ const enUS = {
common: {
login: 'Login',
logout: 'Logout',
accountOptions: 'Account Options',
accountOptions: 'Account',
account: 'Account',
email: 'Email',
password: 'Password',
welcome: 'Welcome back to LangBot 👋',
@@ -42,6 +43,7 @@ const enUS = {
test: 'Test',
forgotPassword: 'Forgot Password?',
loading: 'Loading...',
theme: 'Theme',
},
notFound: {
title: 'Page not found',

View File

@@ -3,6 +3,7 @@ const jaJP = {
login: 'ログイン',
logout: 'ログアウト',
accountOptions: 'アカウントオプション',
account: 'アカウント',
email: 'メールアドレス',
password: 'パスワード',
welcome: 'LangBot へおかえりなさい 👋',
@@ -43,6 +44,7 @@ const jaJP = {
test: 'テスト',
forgotPassword: 'パスワードを忘れた?',
loading: '読み込み中...',
theme: 'テーマ',
},
notFound: {
title: 'ページが見つかりません',

View File

@@ -3,6 +3,7 @@ const zhHans = {
login: '登录',
logout: '退出登录',
accountOptions: '账户选项',
account: '账户',
email: '邮箱',
password: '密码',
welcome: '欢迎回到 LangBot 👋',
@@ -42,6 +43,7 @@ const zhHans = {
test: '测试',
forgotPassword: '忘记密码?',
loading: '加载中...',
theme: '主题',
},
notFound: {
title: '页面不存在',

View File

@@ -3,6 +3,7 @@ const zhHant = {
login: '登入',
logout: '登出',
accountOptions: '帳戶選項',
account: '帳戶',
email: '電子郵件',
password: '密碼',
welcome: '歡迎回到 LangBot 👋',
@@ -42,6 +43,7 @@ const zhHant = {
test: '測試',
forgotPassword: '忘記密碼?',
loading: '載入中...',
theme: '主題',
},
notFound: {
title: '頁面不存在',