wip: i18n chinese

main
elvis liao 2024-09-26 17:57:30 +08:00
parent 85df8eb09d
commit 0abb030889
18 changed files with 289 additions and 184 deletions

View File

@ -1,4 +1,5 @@
import { Moon, Sun } from "lucide-react"; import { Moon, Sun } from "lucide-react";
import { useTranslation } from 'react-i18next'
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -11,6 +12,8 @@ import { useTheme } from "./ThemeProvider";
export function ThemeToggle() { export function ThemeToggle() {
const { setTheme } = useTheme(); const { setTheme } = useTheme();
const { t } = useTranslation();
return ( return (
<DropdownMenu> <DropdownMenu>
@ -23,13 +26,13 @@ export function ThemeToggle() {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}> <DropdownMenuItem onClick={() => setTheme("light")}>
{t('theme.light')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}> <DropdownMenuItem onClick={() => setTheme("dark")}>
{t('theme.dark')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}> <DropdownMenuItem onClick={() => setTheme("system")}>
{t('theme.system')}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

View File

@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { Separator } from "../ui/separator"; import { Separator } from "../ui/separator";
type DeployProgressProps = { type DeployProgressProps = {
@ -6,26 +8,40 @@ type DeployProgressProps = {
}; };
const DeployProgress = ({ phase, phaseSuccess }: DeployProgressProps) => { const DeployProgress = ({ phase, phaseSuccess }: DeployProgressProps) => {
const { t } = useTranslation();
let rs = <> </>; let rs = <> </>;
if (phase === "check") { if (phase === "check") {
if (phaseSuccess) { if (phaseSuccess) {
rs = ( rs = (
<div className="flex items-center"> <div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div> <div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.check')}
</div>
<Separator className="h-1 grow" /> <Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div> <div className="text-xs text-nowrap text-muted-foreground">
{t('deploy.progress.apply')}
</div>
<Separator className="h-1 grow" /> <Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div> <div className="text-xs text-nowrap text-muted-foreground">
{t('deploy.progress.deploy')}
</div>
</div> </div>
); );
} else { } else {
rs = ( rs = (
<div className="flex items-center"> <div className="flex items-center">
<div className="text-xs text-nowrap text-red-600"> </div> <div className="text-xs text-nowrap text-red-600">
{t('deploy.progress.check')}
</div>
<Separator className="h-1 grow" /> <Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div> <div className="text-xs text-nowrap text-muted-foreground">
{t('deploy.progress.apply')}
</div>
<Separator className="h-1 grow" /> <Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div> <div className="text-xs text-nowrap text-muted-foreground">
{t('deploy.progress.deploy')}
</div>
</div> </div>
); );
} }
@ -35,21 +51,33 @@ const DeployProgress = ({ phase, phaseSuccess }: DeployProgressProps) => {
if (phaseSuccess) { if (phaseSuccess) {
rs = ( rs = (
<div className="flex items-center"> <div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div> <div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.check')}
</div>
<Separator className="h-1 grow bg-green-600" /> <Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600"></div> <div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.apply')}
</div>
<Separator className="h-1 grow" /> <Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div> <div className="text-xs text-nowrap text-muted-foreground">
{t('deploy.progress.deploy')}
</div>
</div> </div>
); );
} else { } else {
rs = ( rs = (
<div className="flex items-center"> <div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div> <div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.check')}
</div>
<Separator className="h-1 grow bg-green-600" /> <Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-red-600"></div> <div className="text-xs text-nowrap text-red-600">
{t('deploy.progress.apply')}
</div>
<Separator className="h-1 grow" /> <Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div> <div className="text-xs text-nowrap text-muted-foreground">
{t('deploy.progress.deploy')}
</div>
</div> </div>
); );
} }
@ -59,21 +87,33 @@ const DeployProgress = ({ phase, phaseSuccess }: DeployProgressProps) => {
if (phaseSuccess) { if (phaseSuccess) {
rs = ( rs = (
<div className="flex items-center"> <div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div> <div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.check')}
</div>
<Separator className="h-1 grow bg-green-600" /> <Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600"></div> <div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.apply')}
</div>
<Separator className="h-1 grow bg-green-600" /> <Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600"></div> <div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.deploy')}
</div>
</div> </div>
); );
} else { } else {
rs = ( rs = (
<div className="flex items-center"> <div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div> <div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.check')}
</div>
<Separator className="h-1 grow bg-green-600" /> <Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600"></div> <div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.apply')}
</div>
<Separator className="h-1 grow bg-green-600" /> <Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-red-600"></div> <div className="text-xs text-nowrap text-red-600">
{t('deploy.progress.deploy')}
</div>
</div> </div>
); );
} }

View File

@ -8,6 +8,7 @@ import { useEffect, useState } from "react";
import { update } from "@/repository/settings"; import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error"; import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast"; import { useToast } from "../ui/use-toast";
import { useTranslation } from 'react-i18next'
type DingTalkSetting = { type DingTalkSetting = {
id: string; id: string;
@ -17,6 +18,7 @@ type DingTalkSetting = {
const DingTalk = () => { const DingTalk = () => {
const { config, setChannels } = useNotify(); const { config, setChannels } = useNotify();
const { t } = useTranslation();
const [dingtalk, setDingtalk] = useState<DingTalkSetting>({ const [dingtalk, setDingtalk] = useState<DingTalkSetting>({
id: config.id ?? "", id: config.id ?? "",
@ -70,15 +72,15 @@ const DingTalk = () => {
setChannels(resp); setChannels(resp);
toast({ toast({
title: "保存成功", title: t('save.succeed'),
description: "配置保存成功", description: t('setting.notify.config.save.succeed'),
}); });
} catch (e) { } catch (e) {
const msg = getErrMessage(e); const msg = getErrMessage(e);
toast({ toast({
title: "保存失败", title: t('save.failed'),
description: "配置保存失败:" + msg, description: `${t('setting.notify.config.save.failed')}: ${msg}`,
variant: "destructive", variant: "destructive",
}); });
} }
@ -127,7 +129,7 @@ const DingTalk = () => {
}); });
}} }}
/> />
<Label htmlFor="airplane-mode"></Label> <Label htmlFor="airplane-mode">{t('setting.notify.config.enable')}</Label>
</div> </div>
<div className="flex justify-end mt-2"> <div className="flex justify-end mt-2">
@ -136,7 +138,7 @@ const DingTalk = () => {
handleSaveClick(); handleSaveClick();
}} }}
> >
{t('save')}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -9,6 +9,7 @@ import {
} from "@/domain/settings"; } from "@/domain/settings";
import { getSetting, update } from "@/repository/settings"; import { getSetting, update } from "@/repository/settings";
import { useToast } from "../ui/use-toast"; import { useToast } from "../ui/use-toast";
import { useTranslation } from 'react-i18next'
const NotifyTemplate = () => { const NotifyTemplate = () => {
const [id, setId] = useState(""); const [id, setId] = useState("");
@ -17,6 +18,7 @@ const NotifyTemplate = () => {
]); ]);
const { toast } = useToast(); const { toast } = useToast();
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
const featchData = async () => { const featchData = async () => {
@ -66,8 +68,8 @@ const NotifyTemplate = () => {
} }
toast({ toast({
title: "保存成功", title: t('save.succeed'),
description: "通知模板保存成功", description: t('setting.notify.template.save.succeed'),
}); });
}; };
@ -81,7 +83,7 @@ const NotifyTemplate = () => {
/> />
<div className="text-muted-foreground text-sm mt-1"> <div className="text-muted-foreground text-sm mt-1">
, COUNT: {t('setting.notify.template.variables.tips.title')}
</div> </div>
<Textarea <Textarea
@ -92,10 +94,10 @@ const NotifyTemplate = () => {
}} }}
></Textarea> ></Textarea>
<div className="text-muted-foreground text-sm mt-1"> <div className="text-muted-foreground text-sm mt-1">
, COUNT:DOMAINS: {t('setting.notify.template.variables.tips.content')}
</div> </div>
<div className="flex justify-end mt-2"> <div className="flex justify-end mt-2">
<Button onClick={handleSaveClick}></Button> <Button onClick={handleSaveClick}>{t('save')}</Button>
</div> </div>
</div> </div>
); );

View File

@ -8,6 +8,7 @@ import { useEffect, useState } from "react";
import { update } from "@/repository/settings"; import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error"; import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast"; import { useToast } from "../ui/use-toast";
import { useTranslation } from "react-i18next";
type TelegramSetting = { type TelegramSetting = {
id: string; id: string;
@ -17,6 +18,7 @@ type TelegramSetting = {
const Telegram = () => { const Telegram = () => {
const { config, setChannels } = useNotify(); const { config, setChannels } = useNotify();
const { t } = useTranslation();
const [telegram, setTelegram] = useState<TelegramSetting>({ const [telegram, setTelegram] = useState<TelegramSetting>({
id: config.id ?? "", id: config.id ?? "",
@ -70,15 +72,15 @@ const Telegram = () => {
setChannels(resp); setChannels(resp);
toast({ toast({
title: "保存成功", title: t('save.succeed'),
description: "配置保存成功", description: t('setting.notify.config.save.succeed'),
}); });
} catch (e) { } catch (e) {
const msg = getErrMessage(e); const msg = getErrMessage(e);
toast({ toast({
title: "保存失败", title: t('save.failed'),
description: "配置保存失败:" + msg, description: `${t('setting.notify.config.save.failed')}: ${msg}`,
variant: "destructive", variant: "destructive",
}); });
} }
@ -128,7 +130,7 @@ const Telegram = () => {
}); });
}} }}
/> />
<Label htmlFor="airplane-mode"></Label> <Label htmlFor="airplane-mode">{t('setting.notify.config.enable')}</Label>
</div> </div>
<div className="flex justify-end mt-2"> <div className="flex justify-end mt-2">
@ -137,7 +139,7 @@ const Telegram = () => {
handleSaveClick(); handleSaveClick();
}} }}
> >
{t('save')}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -9,6 +9,7 @@ import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error"; import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast"; import { useToast } from "../ui/use-toast";
import { isValidURL } from "@/lib/url"; import { isValidURL } from "@/lib/url";
import { useTranslation } from 'react-i18next'
type WebhookSetting = { type WebhookSetting = {
id: string; id: string;
@ -18,6 +19,7 @@ type WebhookSetting = {
const Webhook = () => { const Webhook = () => {
const { config, setChannels } = useNotify(); const { config, setChannels } = useNotify();
const { t } = useTranslation();
const [webhook, setWebhook] = useState<WebhookSetting>({ const [webhook, setWebhook] = useState<WebhookSetting>({
id: config.id ?? "", id: config.id ?? "",
@ -59,8 +61,8 @@ const Webhook = () => {
webhook.data.url = webhook.data.url.trim(); webhook.data.url = webhook.data.url.trim();
if (!isValidURL(webhook.data.url)) { if (!isValidURL(webhook.data.url)) {
toast({ toast({
title: "保存失败", title: t('save.failed'),
description: "Url格式不正确", description: t('setting.notify.config.save.failed.url.not.valid'),
variant: "destructive", variant: "destructive",
}); });
return; return;
@ -79,15 +81,15 @@ const Webhook = () => {
setChannels(resp); setChannels(resp);
toast({ toast({
title: "保存成功", title: t('save.succeed'),
description: "配置保存成功", description: t('setting.notify.config.save.succeed'),
}); });
} catch (e) { } catch (e) {
const msg = getErrMessage(e); const msg = getErrMessage(e);
toast({ toast({
title: "保存失败", title: t('save.failed'),
description: "配置保存失败:" + msg, description: `${t('setting.notify.config.save.failed')}: ${msg}`,
variant: "destructive", variant: "destructive",
}); });
} }
@ -123,7 +125,7 @@ const Webhook = () => {
}); });
}} }}
/> />
<Label htmlFor="airplane-mode"></Label> <Label htmlFor="airplane-mode">{t('setting.notify.config.enable')}</Label>
</div> </div>
<div className="flex justify-end mt-2"> <div className="flex justify-end mt-2">
@ -132,7 +134,7 @@ const Webhook = () => {
handleSaveClick(); handleSaveClick();
}} }}
> >
{t('save')}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -12,6 +12,7 @@ import {
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { useTranslation } from "react-i18next"
const Form = FormProvider const Form = FormProvider
@ -145,7 +146,9 @@ const FormMessage = React.forwardRef<
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => { >(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField() const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children const { t } = useTranslation()
const body = error ? t(String(error?.message)) : children
if (!body) { if (!body) {
return null return null

View File

@ -3,6 +3,7 @@ import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ButtonProps, buttonVariants } from "@/components/ui/button"; import { ButtonProps, buttonVariants } from "@/components/ui/button";
import { useTranslation } from "react-i18next";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav <nav
@ -62,33 +63,41 @@ PaginationLink.displayName = "PaginationLink";
const PaginationPrevious = ({ const PaginationPrevious = ({
className, className,
...props ...props
}: React.ComponentProps<typeof PaginationLink>) => ( }: React.ComponentProps<typeof PaginationLink>) => {
<PaginationLink const { t } = useTranslation()
aria-label="Go to previous page"
size="default" return (
className={cn("gap-1 pl-2.5", className)} <PaginationLink
{...props} aria-label="Go to previous page"
> size="default"
<ChevronLeft className="h-4 w-4" /> className={cn("gap-1 pl-2.5", className)}
<span></span> {...props}
</PaginationLink> >
); <ChevronLeft className="h-4 w-4" />
<span>{t('pagination.prev')}</span>
</PaginationLink>
)
};
PaginationPrevious.displayName = "PaginationPrevious"; PaginationPrevious.displayName = "PaginationPrevious";
const PaginationNext = ({ const PaginationNext = ({
className, className,
...props ...props
}: React.ComponentProps<typeof PaginationLink>) => ( }: React.ComponentProps<typeof PaginationLink>) => {
<PaginationLink const { t } = useTranslation()
aria-label="Go to next page"
size="default" return (
className={cn("gap-1 pr-2.5", className)} <PaginationLink
{...props} aria-label="Go to next page"
> size="default"
<span></span> className={cn("gap-1 pr-2.5", className)}
<ChevronRight className="h-4 w-4" /> {...props}
</PaginationLink> >
); <span>{t('pagination.next')}</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
};
PaginationNext.displayName = "PaginationNext"; PaginationNext.displayName = "PaginationNext";
const PaginationEllipsis = ({ const PaginationEllipsis = ({

View File

@ -6,6 +6,7 @@ import {
useNavigate, useNavigate,
} from "react-router-dom"; } from "react-router-dom";
import { CircleUser, Earth, History, Home, Menu, Server } from "lucide-react"; import { CircleUser, Earth, History, Home, Menu, Server } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -22,12 +23,14 @@ import { cn } from "@/lib/utils";
import { ConfigProvider } from "@/providers/config"; import { ConfigProvider } from "@/providers/config";
import { getPb } from "@/repository/api"; import { getPb } from "@/repository/api";
import { ThemeToggle } from "@/components/ThemeToggle"; import { ThemeToggle } from "@/components/ThemeToggle";
import LocaleToggle from "@/components/LocaleToggle";
import Version from "@/components/certimate/Version"; import Version from "@/components/certimate/Version";
export default function Dashboard() { export default function Dashboard() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { t } = useTranslation()
if (!getPb().authStore.isValid || !getPb().authStore.isAdmin) { if (!getPb().authStore.isValid || !getPb().authStore.isAdmin) {
return <Navigate to="/login" />; return <Navigate to="/login" />;
@ -70,7 +73,7 @@ export default function Dashboard() {
)} )}
> >
<Home className="h-4 w-4" /> <Home className="h-4 w-4" />
{t('dashboard')}
</Link> </Link>
<Link <Link
to="/domains" to="/domains"
@ -80,7 +83,7 @@ export default function Dashboard() {
)} )}
> >
<Earth className="h-4 w-4" /> <Earth className="h-4 w-4" />
{t('domain.management.name')}
</Link> </Link>
<Link <Link
to="/access" to="/access"
@ -90,7 +93,7 @@ export default function Dashboard() {
)} )}
> >
<Server className="h-4 w-4" /> <Server className="h-4 w-4" />
{t('menu.auth.management')}
</Link> </Link>
<Link <Link
@ -101,7 +104,7 @@ export default function Dashboard() {
)} )}
> >
<History className="h-4 w-4" /> <History className="h-4 w-4" />
{t('deployment.log.name')}
</Link> </Link>
</nav> </nav>
</div> </div>
@ -138,7 +141,7 @@ export default function Dashboard() {
)} )}
> >
<Home className="h-5 w-5" /> <Home className="h-5 w-5" />
{t('dashboard')}
</Link> </Link>
<Link <Link
to="/domains" to="/domains"
@ -148,7 +151,7 @@ export default function Dashboard() {
)} )}
> >
<Earth className="h-5 w-5" /> <Earth className="h-5 w-5" />
{t('domain.management.name')}
</Link> </Link>
<Link <Link
to="/access" to="/access"
@ -158,7 +161,7 @@ export default function Dashboard() {
)} )}
> >
<Server className="h-5 w-5" /> <Server className="h-5 w-5" />
{t('menu.auth.management')}
</Link> </Link>
<Link <Link
@ -169,13 +172,14 @@ export default function Dashboard() {
)} )}
> >
<History className="h-5 w-5" /> <History className="h-5 w-5" />
{t('deployment.log.name')}
</Link> </Link>
</nav> </nav>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
<div className="w-full flex-1"></div> <div className="w-full flex-1"></div>
<ThemeToggle /> <ThemeToggle />
<LocaleToggle />
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
@ -188,15 +192,15 @@ export default function Dashboard() {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuLabel></DropdownMenuLabel> <DropdownMenuLabel>{t('account')}</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={handleSettingClick}> <DropdownMenuItem onClick={handleSettingClick}>
{t('setting')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={handleLogoutClick}> <DropdownMenuItem onClick={handleLogoutClick}>
退 {t('logout')}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

View File

@ -4,11 +4,13 @@ import { KeyRound, Megaphone, UserRound } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const SettingLayout = () => { const SettingLayout = () => {
const location = useLocation(); const location = useLocation();
const [tabValue, setTabValue] = useState("account"); const [tabValue, setTabValue] = useState("account");
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
const pathname = location.pathname; const pathname = location.pathname;
@ -20,7 +22,7 @@ const SettingLayout = () => {
<div> <div>
<Toaster /> <Toaster />
<div className="text-muted-foreground border-b dark:border-stone-500 py-5"> <div className="text-muted-foreground border-b dark:border-stone-500 py-5">
{t('setting')}
</div> </div>
<div className="w-full mt-5 p-0 md:p-3 flex justify-center"> <div className="w-full mt-5 p-0 md:p-3 flex justify-center">
<Tabs defaultValue="account" className="w-full" value={tabValue}> <Tabs defaultValue="account" className="w-full" value={tabValue}>
@ -33,7 +35,7 @@ const SettingLayout = () => {
className="px-5" className="px-5"
> >
<UserRound size={14} /> <UserRound size={14} />
<div className="ml-1"></div> <div className="ml-1">{t('account')}</div>
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="password" value="password"
@ -43,7 +45,7 @@ const SettingLayout = () => {
className="px-5" className="px-5"
> >
<KeyRound size={14} /> <KeyRound size={14} />
<div className="ml-1"></div> <div className="ml-1">{t('password')}</div>
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
@ -54,7 +56,7 @@ const SettingLayout = () => {
className="px-5" className="px-5"
> >
<Megaphone size={14} /> <Megaphone size={14} />
<div className="ml-1"></div> <div className="ml-1">{t('setting.notify.menu')}</div>
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent value={tabValue}> <TabsContent value={tabValue}>

View File

@ -24,12 +24,14 @@ import {
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const Dashboard = () => { const Dashboard = () => {
const [statistic, setStatistic] = useState<Statistic>(); const [statistic, setStatistic] = useState<Statistic>();
const [deployments, setDeployments] = useState<Deployment[]>(); const [deployments, setDeployments] = useState<Deployment[]>();
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
const fetchStatistic = async () => { const fetchStatistic = async () => {
@ -55,7 +57,7 @@ const Dashboard = () => {
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="text-muted-foreground"></div> <div className="text-muted-foreground">{t('dashboard')}</div>
</div> </div>
<div className="flex mt-10 gap-5 flex-col flex-wrap md:flex-row"> <div className="flex mt-10 gap-5 flex-col flex-wrap md:flex-row">
<div className="w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg border"> <div className="w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg border">
@ -63,7 +65,9 @@ const Dashboard = () => {
<SquareSigma size={48} strokeWidth={1} className="text-blue-400" /> <SquareSigma size={48} strokeWidth={1} className="text-blue-400" />
</div> </div>
<div> <div>
<div className="text-muted-foreground font-semibold"></div> <div className="text-muted-foreground font-semibold">
{t('dashboard.all')}
</div>
<div className="flex items-baseline"> <div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200"> <div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.total ? ( {statistic?.total ? (
@ -74,7 +78,9 @@ const Dashboard = () => {
0 0
)} )}
</div> </div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div> <div className="ml-1 text-stone-700 dark:text-stone-200">
{t("dashboard.unit")}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -84,7 +90,9 @@ const Dashboard = () => {
<CalendarX2 size={48} strokeWidth={1} className="text-red-400" /> <CalendarX2 size={48} strokeWidth={1} className="text-red-400" />
</div> </div>
<div> <div>
<div className="text-muted-foreground font-semibold"></div> <div className="text-muted-foreground font-semibold">
{t('dashboard.near.expired')}
</div>
<div className="flex items-baseline"> <div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200"> <div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.expired ? ( {statistic?.expired ? (
@ -95,7 +103,9 @@ const Dashboard = () => {
0 0
)} )}
</div> </div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div> <div className="ml-1 text-stone-700 dark:text-stone-200">
{t("dashboard.unit")}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -109,7 +119,9 @@ const Dashboard = () => {
/> />
</div> </div>
<div> <div>
<div className="text-muted-foreground font-semibold"></div> <div className="text-muted-foreground font-semibold">
{t('dashboard.enabled')}
</div>
<div className="flex items-baseline"> <div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200"> <div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.enabled ? ( {statistic?.enabled ? (
@ -120,7 +132,9 @@ const Dashboard = () => {
0 0
)} )}
</div> </div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div> <div className="ml-1 text-stone-700 dark:text-stone-200">
{t("dashboard.unit")}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -130,7 +144,7 @@ const Dashboard = () => {
<Ban size={48} strokeWidth={1} className="text-gray-400" /> <Ban size={48} strokeWidth={1} className="text-gray-400" />
</div> </div>
<div> <div>
<div className="text-muted-foreground font-semibold"></div> <div className="text-muted-foreground font-semibold">{t('dashboard.not.enabled')}</div>
<div className="flex items-baseline"> <div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200"> <div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.disabled ? ( {statistic?.disabled ? (
@ -144,19 +158,23 @@ const Dashboard = () => {
0 0
)} )}
</div> </div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div> <div className="ml-1 text-stone-700 dark:text-stone-200">
{t("dashboard.unit")}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div> <div>
<div className="text-muted-foreground mt-5 text-sm"></div> <div className="text-muted-foreground mt-5 text-sm">
{t('deployment.log.name')}
</div>
{deployments?.length == 0 ? ( {deployments?.length == 0 ? (
<> <>
<Alert className="max-w-[40em] mt-10"> <Alert className="max-w-[40em] mt-10">
<AlertTitle></AlertTitle> <AlertTitle>{t('no.data')}</AlertTitle>
<AlertDescription> <AlertDescription>
<div className="flex items-center mt-5"> <div className="flex items-center mt-5">
<div> <div>
@ -164,7 +182,7 @@ const Dashboard = () => {
</div> </div>
<div className="ml-2"> <div className="ml-2">
{" "} {" "}
{t('deployment.log.empty')}
</div> </div>
</div> </div>
<div className="mt-2 flex justify-end"> <div className="mt-2 flex justify-end">
@ -173,7 +191,7 @@ const Dashboard = () => {
navigate("/edit"); navigate("/edit");
}} }}
> >
{t('domain.add')}
</Button> </Button>
</div> </div>
</AlertDescription> </AlertDescription>
@ -182,16 +200,16 @@ const Dashboard = () => {
) : ( ) : (
<> <>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5"> <div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48"></div> <div className="w-48">{t('domain')}</div>
<div className="w-24"></div> <div className="w-24">{t('deployment.log.status')}</div>
<div className="w-56"></div> <div className="w-56">{t('deployment.log.stage')}</div>
<div className="w-56 sm:ml-2 text-center"></div> <div className="w-56 sm:ml-2 text-center">{t('deployment.log.last.execution.time')}</div>
<div className="grow"></div> <div className="grow">{t('operation')}</div>
</div> </div>
<div className="sm:hidden flex text-sm text-muted-foreground"> <div className="sm:hidden flex text-sm text-muted-foreground">
{t('deployment.log.name')}
</div> </div>
{deployments?.map((deployment) => ( {deployments?.map((deployment) => (
@ -218,14 +236,14 @@ const Dashboard = () => {
<Sheet> <Sheet>
<SheetTrigger asChild> <SheetTrigger asChild>
<Button variant={"link"} className="p-0"> <Button variant={"link"} className="p-0">
{t('deployment.log.detail.button.text')}
</Button> </Button>
</SheetTrigger> </SheetTrigger>
<SheetContent className="sm:max-w-5xl"> <SheetContent className="sm:max-w-5xl">
<SheetHeader> <SheetHeader>
<SheetTitle> <SheetTitle>
{deployment.expand.domain?.domain}-{deployment.id} {deployment.expand.domain?.domain}-{deployment.id}
{t('deployment.log.detail')}
</SheetTitle> </SheetTitle>
</SheetHeader> </SheetHeader>
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]"> <div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">

View File

@ -38,6 +38,7 @@ import EmailsEdit from "@/components/certimate/EmailsEdit";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { EmailsSetting } from "@/domain/settings"; import { EmailsSetting } from "@/domain/settings";
import { useTranslation } from "react-i18next";
const Edit = () => { const Edit = () => {
const { const {
@ -47,6 +48,7 @@ const Edit = () => {
const [domain, setDomain] = useState<Domain>(); const [domain, setDomain] = useState<Domain>();
const location = useLocation(); const location = useLocation();
const { t } = useTranslation();
const [tab, setTab] = useState<"base" | "advance">("base"); const [tab, setTab] = useState<"base" | "advance">("base");
@ -69,15 +71,15 @@ const Edit = () => {
const formSchema = z.object({ const formSchema = z.object({
id: z.string().optional(), id: z.string().optional(),
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: "请输入正确的域名", message: t('domain.management.edit.domain.verify.tips'),
}), }),
email: z.string().email().optional(), email: z.string().email().optional(),
access: z.string().regex(/^[a-zA-Z0-9]+$/, { access: z.string().regex(/^[a-zA-Z0-9]+$/, {
message: "请选择DNS服务商授权配置", message: t('domain.management.edit.dns.verify.tips'),
}), }),
targetAccess: z.string().optional(), targetAccess: z.string().optional(),
targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, { targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, {
message: "请选择部署服务类型", message: t('domain.management.edit.target.type.verify.tips'),
}), }),
variables: z.string().optional(), variables: z.string().optional(),
group: z.string().optional(), group: z.string().optional(),

View File

@ -35,11 +35,13 @@ import { TooltipContent, TooltipProvider } from "@radix-ui/react-tooltip";
import { Earth } from "lucide-react"; import { Earth } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
import { useTranslation, Trans } from "react-i18next";
const Home = () => { const Home = () => {
const toast = useToast(); const toast = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation()
const location = useLocation(); const location = useLocation();
const query = new URLSearchParams(location.search); const query = new URLSearchParams(location.search);
@ -127,23 +129,22 @@ const Home = () => {
await save(domain); await save(domain);
toast.toast({ toast.toast({
title: "操作成功", title: t('operation.succeed'),
description: "已发起部署,请稍后查看部署日志。", description: t('domain.management.start.deploy.succeed.tips'),
}); });
} catch (e) { } catch (e) {
toast.toast({ toast.toast({
title: "执行失败", title: t('domain.management.execution.failed'),
description: ( description: (
<> // 这里的 text 只是占位作用,实际文案在 src/i18n/locales/[lang].json
<Trans i18nKey="domain.management.execution.failed.tips">
text1
<Link <Link
to={`/history?domain=${domain.id}`} to={`/history?domain=${domain.id}`}
className="underline text-blue-500" className="underline text-blue-500"
> >text2</Link>
text3
</Link> </Trans>
</>
), ),
variant: "destructive", variant: "destructive",
}); });
@ -175,8 +176,10 @@ const Home = () => {
<div className=""> <div className="">
<Toaster /> <Toaster />
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="text-muted-foreground"></div> <div className="text-muted-foreground">{t('domain.management.name')}</div>
<Button onClick={handleCreateClick}></Button> <Button onClick={handleCreateClick}>
{t('domain.add')}
</Button>
</div> </div>
{!domains.length ? ( {!domains.length ? (
@ -187,26 +190,26 @@ const Home = () => {
</span> </span>
<div className="text-center text-sm text-muted-foreground mt-3"> <div className="text-center text-sm text-muted-foreground mt-3">
{t('domain.management.empty')}
</div> </div>
<Button onClick={handleCreateClick} className="mt-3"> <Button onClick={handleCreateClick} className="mt-3">
{t('domain.add')}
</Button> </Button>
</div> </div>
</> </>
) : ( ) : (
<> <>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5"> <div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-36"></div> <div className="w-36">{t('domain')}</div>
<div className="w-40"></div> <div className="w-40">{t('domain.management.expiry.date')}</div>
<div className="w-32"></div> <div className="w-32">{t('domain.management.last.execution.status')}</div>
<div className="w-64"></div> <div className="w-64">{t('domain.management.last.execution.stage')}</div>
<div className="w-40 sm:ml-2"></div> <div className="w-40 sm:ml-2">{t('domain.management.last.execution.time')}</div>
<div className="w-24"></div> <div className="w-24">{t('domain.management.enable')}</div>
<div className="grow"></div> <div className="grow">{t('operation')}</div>
</div> </div>
<div className="sm:hidden flex text-sm text-muted-foreground"> <div className="sm:hidden flex text-sm text-muted-foreground">
{t('domain')}
</div> </div>
{domains.map((domain) => ( {domains.map((domain) => (
@ -221,8 +224,8 @@ const Home = () => {
<div> <div>
{domain.expiredAt ? ( {domain.expiredAt ? (
<> <>
<div>90</div> <div>{t('domain.management.expiry.date1', { date: 90 })}</div>
<div>{getDate(domain.expiredAt)}</div> <div>{t('domain.management.expiry.date2', { date: getDate(domain.expiredAt) })}</div>
</> </>
) : ( ) : (
"---" "---"
@ -266,7 +269,7 @@ const Home = () => {
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<div className="border rounded-sm px-3 bg-background text-muted-foreground text-xs"> <div className="border rounded-sm px-3 bg-background text-muted-foreground text-xs">
{domain.enabled ? "禁用" : "启用"} {domain.enabled ? t('disable') : t('enable')}
</div> </div>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -278,7 +281,7 @@ const Home = () => {
className="p-0" className="p-0"
onClick={() => handleHistoryClick(domain.id)} onClick={() => handleHistoryClick(domain.id)}
> >
{t('deployment.log.name')}
</Button> </Button>
<Show when={domain.enabled ? true : false}> <Show when={domain.enabled ? true : false}>
<Separator orientation="vertical" className="h-4 mx-2" /> <Separator orientation="vertical" className="h-4 mx-2" />
@ -287,7 +290,7 @@ const Home = () => {
className="p-0" className="p-0"
onClick={() => handleRightNowClick(domain)} onClick={() => handleRightNowClick(domain)}
> >
{t('domain.management.start.deploying')}
</Button> </Button>
</Show> </Show>
@ -304,7 +307,7 @@ const Home = () => {
className="p-0" className="p-0"
onClick={() => handleForceClick(domain)} onClick={() => handleForceClick(domain)}
> >
{t('domain.management.forced.deployment')}
</Button> </Button>
</Show> </Show>
@ -315,7 +318,7 @@ const Home = () => {
className="p-0" className="p-0"
onClick={() => handleDownloadClick(domain)} onClick={() => handleDownloadClick(domain)}
> >
{t('download')}
</Button> </Button>
</Show> </Show>
@ -325,24 +328,24 @@ const Home = () => {
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button variant={"link"} className="p-0"> <Button variant={"link"} className="p-0">
{t('delete')}
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle> <AlertDialogTitle>{t('domain.delete')}</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{t('domain.management.delete.confirm')}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel> <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={() => { onClick={() => {
handleDeleteClick(domain.id); handleDeleteClick(domain.id);
}} }}
> >
{t('confirm')}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@ -354,7 +357,7 @@ const Home = () => {
className="p-0" className="p-0"
onClick={() => handleEditClick(domain.id)} onClick={() => handleEditClick(domain.id)}
> >
{t('edit')}
</Button> </Button>
</> </>
)} )}

View File

@ -17,11 +17,13 @@ import { list } from "@/repository/deployment";
import { Smile } from "lucide-react"; import { Smile } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
const History = () => { const History = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [deployments, setDeployments] = useState<Deployment[]>(); const [deployments, setDeployments] = useState<Deployment[]>();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { t } = useTranslation();
const domain = searchParams.get("domain"); const domain = searchParams.get("domain");
useEffect(() => { useEffect(() => {
@ -38,11 +40,11 @@ const History = () => {
return ( return (
<ScrollArea className="h-[80vh] overflow-hidden"> <ScrollArea className="h-[80vh] overflow-hidden">
<div className="text-muted-foreground"></div> <div className="text-muted-foreground">{t('deployment.log.name')}</div>
{!deployments?.length ? ( {!deployments?.length ? (
<> <>
<Alert className="max-w-[40em] mx-auto mt-20"> <Alert className="max-w-[40em] mx-auto mt-20">
<AlertTitle></AlertTitle> <AlertTitle>{t('no.data')}</AlertTitle>
<AlertDescription> <AlertDescription>
<div className="flex items-center mt-5"> <div className="flex items-center mt-5">
<div> <div>
@ -50,7 +52,7 @@ const History = () => {
</div> </div>
<div className="ml-2"> <div className="ml-2">
{" "} {" "}
{t('deployment.log.empty')}
</div> </div>
</div> </div>
<div className="mt-2 flex justify-end"> <div className="mt-2 flex justify-end">
@ -59,7 +61,7 @@ const History = () => {
navigate("/"); navigate("/");
}} }}
> >
{t('domain.add')}
</Button> </Button>
</div> </div>
</AlertDescription> </AlertDescription>
@ -68,16 +70,16 @@ const History = () => {
) : ( ) : (
<> <>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5"> <div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48"></div> <div className="w-48">{t('domain')}</div>
<div className="w-24"></div> <div className="w-24">{t('deployment.log.status')}</div>
<div className="w-56"></div> <div className="w-56">{t('deployment.log.stage')}</div>
<div className="w-56 sm:ml-2 text-center"></div> <div className="w-56 sm:ml-2 text-center">{t('deployment.log.last.execution.time')}</div>
<div className="grow"></div> <div className="grow">{t('operation')}</div>
</div> </div>
<div className="sm:hidden flex text-sm text-muted-foreground"> <div className="sm:hidden flex text-sm text-muted-foreground">
{t('deployment.log.name')}
</div> </div>
{deployments?.map((deployment) => ( {deployments?.map((deployment) => (
@ -104,14 +106,14 @@ const History = () => {
<Sheet> <Sheet>
<SheetTrigger asChild> <SheetTrigger asChild>
<Button variant={"link"} className="p-0"> <Button variant={"link"} className="p-0">
{t('deployment.log.detail.button.text')}
</Button> </Button>
</SheetTrigger> </SheetTrigger>
<SheetContent className="sm:max-w-5xl"> <SheetContent className="sm:max-w-5xl">
<SheetHeader> <SheetHeader>
<SheetTitle> <SheetTitle>
{deployment.expand.domain?.domain}-{deployment.id} {deployment.expand.domain?.domain}-{deployment.id}
{t('deployment.log.detail')}
</SheetTitle> </SheetTitle>
</SheetHeader> </SheetHeader>
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]"> <div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">

View File

@ -1,3 +1,8 @@
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { z } from "zod";
import { useTranslation } from 'react-i18next'
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Form, Form,
@ -11,20 +16,19 @@ import { Input } from "@/components/ui/input";
import { getErrMessage } from "@/lib/error"; import { getErrMessage } from "@/lib/error";
import { getPb } from "@/repository/api"; import { getPb } from "@/repository/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { z } from "zod";
const formSchema = z.object({ const formSchema = z.object({
username: z.string().email({ username: z.string().email({
message: "请输入正确的邮箱地址", message: "login.username.no.empty.message",
}), }),
password: z.string().min(10, { password: z.string().min(10, {
message: "密码至少10个字符", message: "login.password.length.message",
}), }),
}); });
const Login = () => { const Login = () => {
const { t } = useTranslation()
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
@ -61,7 +65,7 @@ const Login = () => {
name="username" name="username"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel></FormLabel> <FormLabel>{t('username')}</FormLabel>
<FormControl> <FormControl>
<Input placeholder="email" {...field} /> <Input placeholder="email" {...field} />
</FormControl> </FormControl>
@ -76,7 +80,7 @@ const Login = () => {
name="password" name="password"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel></FormLabel> <FormLabel>{t('password')}</FormLabel>
<FormControl> <FormControl>
<Input placeholder="shadcn" {...field} type="password" /> <Input placeholder="shadcn" {...field} type="password" />
</FormControl> </FormControl>
@ -86,7 +90,7 @@ const Login = () => {
)} )}
/> />
<div className="flex justify-end"> <div className="flex justify-end">
<Button type="submit"></Button> <Button type="submit">{t('login.submit')}</Button>
</div> </div>
</form> </form>
</Form> </Form>

View File

@ -15,16 +15,18 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { z } from "zod"; import { z } from "zod";
const formSchema = z.object({ const formSchema = z.object({
email: z.string().email("请输入正确的邮箱"), email: z.string().email("setting.account.email.valid.message"),
}); });
const Account = () => { const Account = () => {
const { toast } = useToast(); const { toast } = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation();
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
@ -43,8 +45,8 @@ const Account = () => {
getPb().authStore.clear(); getPb().authStore.clear();
toast({ toast({
title: "修改账户邮箱功", title: t("setting.account.email.change.succeed"),
description: "请重新登录", description: t("setting.account.log.back.in"),
}); });
setTimeout(() => { setTimeout(() => {
navigate("/login"); navigate("/login");
@ -52,7 +54,7 @@ const Account = () => {
} catch (e) { } catch (e) {
const message = getErrMessage(e); const message = getErrMessage(e);
toast({ toast({
title: "修改账户邮箱失败", title: t("setting.account.email.change.failed"),
description: message, description: message,
variant: "destructive", variant: "destructive",
}); });
@ -72,10 +74,10 @@ const Account = () => {
name="email" name="email"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel></FormLabel> <FormLabel>{t('email')}</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="请输入邮箱" placeholder={t('setting.email.placeholder')}
{...field} {...field}
type="email" type="email"
onChange={(e) => { onChange={(e) => {
@ -92,10 +94,10 @@ const Account = () => {
<div className="flex justify-end"> <div className="flex justify-end">
{changed ? ( {changed ? (
<Button type="submit"></Button> <Button type="submit">{t('setting.submit')}</Button>
) : ( ) : (
<Button type="submit" disabled variant={"secondary"}> <Button type="submit" disabled variant={"secondary"}>
{t('setting.submit')}
</Button> </Button>
)} )}
</div> </div>

View File

@ -9,15 +9,18 @@ import {
AccordionTrigger, AccordionTrigger,
} from "@/components/ui/accordion"; } from "@/components/ui/accordion";
import { NotifyProvider } from "@/providers/notify"; import { NotifyProvider } from "@/providers/notify";
import { useTranslation } from "react-i18next";
const Notify = () => { const Notify = () => {
const { t } = useTranslation();
return ( return (
<> <>
<NotifyProvider> <NotifyProvider>
<div className="border rounded-sm p-5 shadow-lg"> <div className="border rounded-sm p-5 shadow-lg">
<Accordion type={"multiple"} className="dark:text-stone-200"> <Accordion type={"multiple"} className="dark:text-stone-200">
<AccordionItem value="item-1" className="dark:border-stone-200"> <AccordionItem value="item-1" className="dark:border-stone-200">
<AccordionTrigger></AccordionTrigger> <AccordionTrigger>{t('template')}</AccordionTrigger>
<AccordionContent> <AccordionContent>
<NotifyTemplate /> <NotifyTemplate />
</AccordionContent> </AccordionContent>

View File

@ -14,29 +14,31 @@ import { getPb } from "@/repository/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { z } from "zod"; import { z } from "zod";
const formSchema = z const formSchema = z
.object({ .object({
oldPassword: z.string().min(10, { oldPassword: z.string().min(10, {
message: "密码至少10个字符", message: "setting.password.length.message",
}), }),
newPassword: z.string().min(10, { newPassword: z.string().min(10, {
message: "密码至少10个字符", message: "setting.password.length.message",
}), }),
confirmPassword: z.string().min(10, { confirmPassword: z.string().min(10, {
message: "密码至少10个字符", message: "setting.password.length.message",
}), }),
}) })
.refine((data) => data.newPassword === data.confirmPassword, { .refine((data) => data.newPassword === data.confirmPassword, {
message: "两次密码不一致", message: "setting.password.not.match",
path: ["confirmPassword"], path: ["confirmPassword"],
}); });
const Password = () => { const Password = () => {
const { toast } = useToast(); const { toast } = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation();
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
@ -66,8 +68,8 @@ const Password = () => {
getPb().authStore.clear(); getPb().authStore.clear();
toast({ toast({
title: "修改密码成功", title: t('setting.password.change.succeed'),
description: "请重新登录", description: t("setting.account.log.back.in"),
}); });
setTimeout(() => { setTimeout(() => {
navigate("/login"); navigate("/login");
@ -75,7 +77,7 @@ const Password = () => {
} catch (e) { } catch (e) {
const message = getErrMessage(e); const message = getErrMessage(e);
toast({ toast({
title: "修改密码失败", title: t('setting.password.change.failed'),
description: message, description: message,
variant: "destructive", variant: "destructive",
}); });
@ -95,9 +97,9 @@ const Password = () => {
name="oldPassword" name="oldPassword"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel></FormLabel> <FormLabel>{t('setting.password.current.password')}</FormLabel>
<FormControl> <FormControl>
<Input placeholder="当前密码" {...field} type="password" /> <Input placeholder={t('setting.password.current.password')} {...field} type="password" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@ -110,7 +112,7 @@ const Password = () => {
name="newPassword" name="newPassword"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel></FormLabel> <FormLabel>{t('setting.password.new.password')}</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="newPassword" placeholder="newPassword"
@ -129,7 +131,7 @@ const Password = () => {
name="confirmPassword" name="confirmPassword"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel></FormLabel> <FormLabel>{t('setting.password.confirm.password')}</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="confirmPassword" placeholder="confirmPassword"
@ -143,7 +145,7 @@ const Password = () => {
)} )}
/> />
<div className="flex justify-end"> <div className="flex justify-end">
<Button type="submit"></Button> <Button type="submit">{t('setting.submit')}</Button>
</div> </div>
</form> </form>
</Form> </Form>