translate new element to English

This commit is contained in:
yoan
2024-09-28 07:46:55 +08:00
48 changed files with 1532 additions and 806 deletions
-313
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+322
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Certimate - Your Trusted SSL Automation Partner</title>
<script type="module" crossorigin src="/assets/index-CQVPrK_Y.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Djc_JtNf.css">
<script type="module" crossorigin src="/assets/index-TzNEc_kS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-I--T0qY3.css">
</head>
<body class="bg-background">
<div id="root"></div>
+141
View File
@@ -27,6 +27,9 @@
"@radix-ui/react-tooltip": "^1.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.6.1",
"jszip": "^3.10.1",
"lucide-react": "^0.417.0",
"moment": "^2.30.1",
@@ -34,6 +37,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.1",
"react-i18next": "^15.0.2",
"react-router-dom": "^6.25.1",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
@@ -382,6 +386,17 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.25.6",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.25.6.tgz",
"integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.24.7",
"resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.24.7.tgz",
@@ -2959,6 +2974,14 @@
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/cross-fetch": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/cross-fetch/-/cross-fetch-4.0.0.tgz",
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
"dependencies": {
"node-fetch": "^2.6.12"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -3697,6 +3720,52 @@
"node": ">= 0.4"
}
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/i18next": {
"version": "23.15.1",
"resolved": "https://registry.npmmirror.com/i18next/-/i18next-23.15.1.tgz",
"integrity": "sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz",
"integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-http-backend": {
"version": "2.6.1",
"resolved": "https://registry.npmmirror.com/i18next-http-backend/-/i18next-http-backend-2.6.1.tgz",
"integrity": "sha512-rCilMAnlEQNeKOZY1+x8wLM5IpYOj10guGvEpeC59tNjj6MMreLIjIW8D1RclhD3ifLwn6d/Y9HEM1RUE6DSog==",
"dependencies": {
"cross-fetch": "4.0.0"
}
},
"node_modules/ignore": {
"version": "5.3.1",
"resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.1.tgz",
@@ -4112,6 +4181,25 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.18.tgz",
@@ -4553,6 +4641,27 @@
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-i18next": {
"version": "15.0.2",
"resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-15.0.2.tgz",
"integrity": "sha512-z0W3/RES9Idv3MmJUcf0mDNeeMOUXe+xoL0kPfQPbDoZHmni/XsIoq5zgT2MCFUiau283GuBUK578uD/mkAbLQ==",
"dependencies": {
"@babel/runtime": "^7.25.0",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 23.2.3",
"react": ">= 16.8.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -4692,6 +4801,11 @@
"node": ">=8.10.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz",
@@ -5140,6 +5254,11 @@
"node": ">=8.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/ts-api-utils": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
@@ -5361,6 +5480,28 @@
}
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
+5 -1
View File
@@ -40,7 +40,11 @@
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.1",
"zod": "^3.23.8"
"zod": "^3.23.8",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.6.1",
"react-i18next": "^15.0.2"
},
"devDependencies": {
"@types/node": "^22.0.0",
+33
View File
@@ -0,0 +1,33 @@
import { Languages } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export default function LocaleToggle() {
const { i18n } = useTranslation()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Languages className="h-[1.2rem] w-[1.2rem] dark:text-white" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{Object.keys(i18n.store.data).map(key => (
<DropdownMenuItem onClick={() => i18n.changeLanguage(key)}>
{i18n.store.data[key].name as string}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
+6 -3
View File
@@ -1,4 +1,5 @@
import { Moon, Sun } from "lucide-react";
import { useTranslation } from 'react-i18next'
import { Button } from "@/components/ui/button";
import {
@@ -11,6 +12,8 @@ import { useTheme } from "./ThemeProvider";
export function ThemeToggle() {
const { setTheme } = useTheme();
const { t } = useTranslation();
return (
<DropdownMenu>
@@ -23,13 +26,13 @@ export function ThemeToggle() {
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
浅色
{t('theme.light')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
暗黑
{t('theme.dark')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
系统
{t('theme.system')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -29,12 +30,13 @@ const AccessAliyunForm = ({
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
configType: accessFormType,
accessKeyId: z.string().min(1).max(64),
accessSecretId: z.string().min(1).max(64),
accessKeyId: z.string().min(1, 'access.form.access.key.id.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
accessSecretId: z.string().min(1, 'access.form.access.key.secret.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
});
let config: AliyunConfig = {
@@ -47,7 +49,7 @@ const AccessAliyunForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || '',
configType: "aliyun",
accessKeyId: config.accessKeyId,
accessSecretId: config.accessKeySecret,
@@ -111,9 +113,9 @@ const AccessAliyunForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>名称</FormLabel>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input placeholder={t('access.form.name.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -126,7 +128,7 @@ const AccessAliyunForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -141,7 +143,7 @@ const AccessAliyunForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -156,9 +158,9 @@ const AccessAliyunForm = ({
name="accessKeyId"
render={({ field }) => (
<FormItem>
<FormLabel>AccessKeyId</FormLabel>
<FormLabel>{t('access.form.access.key.id')}</FormLabel>
<FormControl>
<Input placeholder="请输入AccessKeyId" {...field} />
<Input placeholder={t('access.form.access.key.id.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -171,9 +173,9 @@ const AccessAliyunForm = ({
name="accessSecretId"
render={({ field }) => (
<FormItem>
<FormLabel>AccessKeySecret</FormLabel>
<FormLabel>{t('access.form.access.key.secret')}</FormLabel>
<FormControl>
<Input placeholder="请输入AccessKeySecret" {...field} />
<Input placeholder={t('access.form.access.key.secret.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -184,7 +186,7 @@ const AccessAliyunForm = ({
<FormMessage />
<div className="flex justify-end">
<Button type="submit">保存</Button>
<Button type="submit">{t('save')}</Button>
</div>
</form>
</Form>
@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -28,11 +29,12 @@ const AccessCloudflareForm = ({
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
configType: accessFormType,
dnsApiToken: z.string().min(1).max(64),
dnsApiToken: z.string().min(1, 'access.form.cloud.dns.api.token.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
});
let config: CloudflareConfig = {
@@ -44,7 +46,7 @@ const AccessCloudflareForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || '',
configType: "cloudflare",
dnsApiToken: config.dnsApiToken,
},
@@ -106,9 +108,9 @@ const AccessCloudflareForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>名称</FormLabel>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input placeholder={t('access.form.name.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -121,7 +123,7 @@ const AccessCloudflareForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -136,7 +138,7 @@ const AccessCloudflareForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -151,9 +153,9 @@ const AccessCloudflareForm = ({
name="dnsApiToken"
render={({ field }) => (
<FormItem>
<FormLabel>CLOUD_DNS_API_TOKEN</FormLabel>
<FormLabel>{t('access.form.cloud.dns.api.token')}</FormLabel>
<FormControl>
<Input placeholder="请输入CLOUD_DNS_API_TOKEN" {...field} />
<Input placeholder={t('access.form.cloud.dns.api.token.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -162,7 +164,7 @@ const AccessCloudflareForm = ({
/>
<div className="flex justify-end">
<Button type="submit">保存</Button>
<Button type="submit">{t('save')}</Button>
</div>
</form>
</Form>
+7 -6
View File
@@ -8,6 +8,7 @@ import {
import { ScrollArea } from "@/components/ui/scroll-area";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import AccessTencentForm from "./AccessTencentForm";
@@ -47,6 +48,7 @@ export function AccessEdit({
className,
}: TargetConfigEditProps) {
const [open, setOpen] = useState(false);
const { t } = useTranslation();
const typeKeys = Array.from(accessTypeMap.keys());
@@ -157,25 +159,24 @@ export function AccessEdit({
</DialogTrigger>
<DialogContent className="sm:max-w-[600px] w-full dark:text-stone-200">
<DialogHeader>
<DialogTitle>{op == "add" ? "添加" : "编辑"}授权</DialogTitle>
<DialogTitle>{op == "add" ? t('access.add') : t('access.edit')}</DialogTitle>
</DialogHeader>
<ScrollArea className="max-h-[80vh]">
<div className="container py-3">
<Label>服务商</Label>
<Label>{t('access.type')}</Label>
<Select
onValueChange={(val) => {
console.log(val);
setConfigType(val);
}}
defaultValue={configType}
>
<SelectTrigger className="mt-3">
<SelectValue placeholder="请选择服务商" />
<SelectValue placeholder={t('access.type.not.empty')} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>服务商</SelectLabel>
<SelectLabel>{t('access.type')}</SelectLabel>
{typeKeys.map((key) => (
<SelectItem value={key} key={key}>
<div
@@ -188,7 +189,7 @@ export function AccessEdit({
src={accessTypeMap.get(key)?.[1]}
className="h-6 w-6"
/>
<div>{accessTypeMap.get(key)?.[0]}</div>
<div>{t(accessTypeMap.get(key)?.[0] || '')}</div>
</div>
</SelectItem>
))}
@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -33,12 +34,13 @@ const AccessGodaddyFrom = ({
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
configType: accessFormType,
apiKey: z.string().min(1).max(64),
apiSecret: z.string().min(1).max(64),
apiKey: z.string().min(1, 'access.form.go.daddy.api.key.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
apiSecret: z.string().min(1, 'access.form.go.daddy.api.secret.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
});
let config: GodaddyConfig = {
@@ -51,7 +53,7 @@ const AccessGodaddyFrom = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || '',
configType: "godaddy",
apiKey: config.apiKey,
apiSecret: config.apiSecret,
@@ -115,9 +117,9 @@ const AccessGodaddyFrom = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>名称</FormLabel>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input placeholder={t('access.form.name.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -130,7 +132,7 @@ const AccessGodaddyFrom = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -145,7 +147,7 @@ const AccessGodaddyFrom = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -160,9 +162,9 @@ const AccessGodaddyFrom = ({
name="apiKey"
render={({ field }) => (
<FormItem>
<FormLabel>GODADDY_API_KEY</FormLabel>
<FormLabel>{t('access.form.go.daddy.api.key')}</FormLabel>
<FormControl>
<Input placeholder="请输入GODADDY_API_KEY" {...field} />
<Input placeholder={t('access.form.go.daddy.api.key.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -175,9 +177,9 @@ const AccessGodaddyFrom = ({
name="apiSecret"
render={({ field }) => (
<FormItem>
<FormLabel>GODADDY_API_SECRET</FormLabel>
<FormLabel>{t('access.form.go.daddy.api.secret')}</FormLabel>
<FormControl>
<Input placeholder="请输入GODADDY_API_SECRET" {...field} />
<Input placeholder={t('access.form.go.daddy.api.secret.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -186,7 +188,7 @@ const AccessGodaddyFrom = ({
/>
<div className="flex justify-end">
<Button type="submit">保存</Button>
<Button type="submit">{t('save')}</Button>
</div>
</form>
</Form>
@@ -25,6 +25,7 @@ import { update } from "@/repository/access_group";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
import { useState } from "react";
import { useTranslation } from "react-i18next";
type AccessGroupEditProps = {
className?: string;
@@ -35,9 +36,10 @@ const AccessGroupEdit = ({ className, trigger }: AccessGroupEditProps) => {
const { reloadAccessGroups } = useConfig();
const [open, setOpen] = useState(false);
const { t } = useTranslation();
const formSchema = z.object({
name: z.string().min(1).max(64),
name: z.string().min(1, 'access.group.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
});
const form = useForm<z.infer<typeof formSchema>>({
@@ -78,14 +80,13 @@ const AccessGroupEdit = ({ className, trigger }: AccessGroupEditProps) => {
</DialogTrigger>
<DialogContent className="sm:max-w-[600px] w-full dark:text-stone-200">
<DialogHeader>
<DialogTitle>添加分组</DialogTitle>
<DialogTitle>{t('access.group.add')}</DialogTitle>
</DialogHeader>
<div className="container py-3">
<Form {...form}>
<form
onSubmit={(e) => {
console.log(e);
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
@@ -96,9 +97,9 @@ const AccessGroupEdit = ({ className, trigger }: AccessGroupEditProps) => {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>组名</FormLabel>
<FormLabel>{t('access.group.name')}</FormLabel>
<FormControl>
<Input placeholder="请输入组名" {...field} type="text" />
<Input placeholder={t('access.group.name.not.empty')} {...field} type="text" />
</FormControl>
<FormMessage />
@@ -107,7 +108,7 @@ const AccessGroupEdit = ({ className, trigger }: AccessGroupEditProps) => {
/>
<div className="flex justify-end">
<Button type="submit">保存</Button>
<Button type="submit">{t('save')}</Button>
</div>
</form>
</Form>
+15 -17
View File
@@ -29,6 +29,7 @@ import { Group } from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const AccessGroupList = () => {
const {
@@ -39,8 +40,7 @@ const AccessGroupList = () => {
const { toast } = useToast();
const navigate = useNavigate();
const { t } = useTranslation();
const handleRemoveClick = async (id: string) => {
try {
@@ -48,7 +48,7 @@ const AccessGroupList = () => {
reloadAccessGroups();
} catch (e) {
toast({
title: "删除失败",
title: t('delete.failed'),
description: getErrMessage(e),
variant: "destructive",
});
@@ -69,10 +69,10 @@ const AccessGroupList = () => {
</span>
<div className="text-center text-sm text-muted-foreground mt-3">
请添加域名开始部署证书吧。
{t('access.group.domain.empty')}
</div>
<AccessGroupEdit
trigger={<Button>新增授权组</Button>}
trigger={<Button>{t('access.group.add')}</Button>}
className="mt-3"
/>
</div>
@@ -86,9 +86,7 @@ const AccessGroupList = () => {
<CardHeader>
<CardTitle>{accessGroup.name}</CardTitle>
<CardDescription>
共有
{accessGroup.expand ? accessGroup.expand.access.length : 0}
个部署授权配置
{t('access.group.total', { total: accessGroup.expand ? accessGroup.expand.access.length : 0 })}
</CardDescription>
</CardHeader>
<CardContent className="min-h-[180px]">
@@ -123,7 +121,7 @@ const AccessGroupList = () => {
<Group size={40} />
</div>
<div className="ml-2">
暂无部署授权配置,请添加后开始使用吧
{t('access.group.empty')}
</div>
</div>
</>
@@ -151,7 +149,7 @@ const AccessGroupList = () => {
);
}}
>
所有授权
{t('access.all')}
</Button>
</div>
</Show>
@@ -159,14 +157,14 @@ const AccessGroupList = () => {
<Show
when={
!accessGroup.expand ||
accessGroup.expand.access.length == 0
accessGroup.expand.access.length == 0
? true
: false
}
>
<div>
<Button size="sm" onClick={handleAddAccess}>
新增授权
{t('access.add')}
</Button>
</div>
</Show>
@@ -175,21 +173,21 @@ const AccessGroupList = () => {
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"destructive"} size={"sm"}>
删除
{t('delete')}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="dark:text-gray-200">
删除组
{t('access.group.delete')}
</AlertDialogTitle>
<AlertDialogDescription>
确定要删除部署授权组吗?
{t('access.group.delete.confirm')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="dark:text-gray-200">
取消
{t('cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
@@ -198,7 +196,7 @@ const AccessGroupList = () => {
);
}}
>
确认
{t('confirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -28,11 +29,12 @@ const AccessNamesiloForm = ({
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
configType: accessFormType,
apiKey: z.string().min(1).max(64),
apiKey: z.string().min(1, 'access.form.namesilo.api.key.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
});
let config: NamesiloConfig = {
@@ -44,14 +46,13 @@ const AccessNamesiloForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || '',
configType: "namesilo",
apiKey: config.apiKey,
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
console.log(data);
const req: Access = {
id: data.id as string,
name: data.name,
@@ -106,9 +107,9 @@ const AccessNamesiloForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>名称</FormLabel>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input placeholder={t('access.form.name.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -121,7 +122,7 @@ const AccessNamesiloForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -136,7 +137,7 @@ const AccessNamesiloForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -151,9 +152,9 @@ const AccessNamesiloForm = ({
name="apiKey"
render={({ field }) => (
<FormItem>
<FormLabel>NAMESILO_API_KEY</FormLabel>
<FormLabel>{t('access.form.namesilo.api.key')}</FormLabel>
<FormControl>
<Input placeholder="请输入NAMESILO_API_KEY" {...field} />
<Input placeholder={t('access.form.namesilo.api.key.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -162,7 +163,7 @@ const AccessNamesiloForm = ({
/>
<div className="flex justify-end">
<Button type="submit">保存</Button>
<Button type="submit">{t('save')}</Button>
</div>
</form>
</Form>
+15 -13
View File
@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -29,12 +30,13 @@ const AccessQiniuForm = ({
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
configType: accessFormType,
accessKey: z.string().min(1).max(64),
secretKey: z.string().min(1).max(64),
accessKey: z.string().min(1, 'access.form.access.key.not.empty').max(64),
secretKey: z.string().min(1, 'access.form.secret.key.not.empty').max(64),
});
let config: QiniuConfig = {
@@ -47,7 +49,7 @@ const AccessQiniuForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || '',
configType: "qiniu",
accessKey: config.accessKey,
secretKey: config.secretKey,
@@ -111,9 +113,9 @@ const AccessQiniuForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>名称</FormLabel>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input placeholder={t('access.form.name.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -126,7 +128,7 @@ const AccessQiniuForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -141,7 +143,7 @@ const AccessQiniuForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -156,9 +158,9 @@ const AccessQiniuForm = ({
name="accessKey"
render={({ field }) => (
<FormItem>
<FormLabel>AccessKey</FormLabel>
<FormLabel>{t('access.form.access.key')}</FormLabel>
<FormControl>
<Input placeholder="请输入AccessKey" {...field} />
<Input placeholder={t('access.form.access.key.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -171,9 +173,9 @@ const AccessQiniuForm = ({
name="secretKey"
render={({ field }) => (
<FormItem>
<FormLabel>SecretKey</FormLabel>
<FormLabel>{t('access.form.secret.key')}</FormLabel>
<FormControl>
<Input placeholder="请输入SecretKey" {...field} />
<Input placeholder={t('access.form.secret.key.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -184,7 +186,7 @@ const AccessQiniuForm = ({
<FormMessage />
<div className="flex justify-end">
<Button type="submit">保存</Button>
<Button type="submit">{t('save')}</Button>
</div>
</form>
</Form>
+41 -38
View File
@@ -24,6 +24,7 @@ import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
import { readFileContent } from "@/lib/file";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Select,
SelectContent,
@@ -53,6 +54,7 @@ const AccessSSHForm = ({
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [fileName, setFileName] = useState("");
const { t } = useTranslation();
const originGroup = data ? (data.group ? data.group : "") : "";
@@ -62,26 +64,27 @@ const AccessSSHForm = ({
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
configType: accessFormType,
host: z.string().refine(
(str) => {
return ipReg.test(str) || domainReg.test(str);
},
{
message: "请输入正确的域名或IP",
message: "zod.rule.ssh.host",
}
),
group: z.string().optional(),
port: z.string().min(1).max(5),
username: z.string().min(1).max(64),
password: z.string().min(0).max(64),
key: z.string().min(0).max(20480),
port: z.string().min(1, 'access.form.ssh.port.not.empty').max(5, t('zod.rule.string.max', { max: 5 })),
username: z.string().min(1, 'username.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
password: z.string().min(0, 'password.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
key: z.string().min(0, 'access.form.ssh.key.not.empty').max(20480, t('zod.rule.string.max', { max: 20480 })),
keyFile: z.any().optional(),
command: z.string().min(1).max(2048),
preCommand: z.string().min(0).max(2048).optional(),
certPath: z.string().min(0).max(2048),
keyPath: z.string().min(0).max(2048),
preCommand: z.string().min(0).max(2048).optional(),
command: z.string().min(1, 'access.form.ssh.command.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
certPath: z.string().min(0, 'access.form.ssh.cert.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
keyPath: z.string().min(0, 'access.form.ssh.key.path.not.empty').max(2048, t('zod.rule.string.max', { max: 2048 })),
});
let config: SSHConfig = {
@@ -102,7 +105,7 @@ const AccessSSHForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || '',
configType: "ssh",
group: data?.group,
host: config.host,
@@ -223,9 +226,9 @@ const AccessSSHForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>名称</FormLabel>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input placeholder={t('access.form.name.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -239,12 +242,12 @@ const AccessSSHForm = ({
render={({ field }) => (
<FormItem>
<FormLabel className="w-full flex justify-between">
<div>授权配置组(用于将一个域名证书部署到多个 ssh 主机)</div>
<div>{t('access.form.ssh.group.label')}</div>
<AccessGroupEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
新增
{t('add')}
</div>
}
/>
@@ -259,7 +262,7 @@ const AccessSSHForm = ({
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择分组" />
<SelectValue placeholder={t('access.group.not.empty')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="emptyId">
@@ -299,7 +302,7 @@ const AccessSSHForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -314,7 +317,7 @@ const AccessSSHForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -329,9 +332,9 @@ const AccessSSHForm = ({
name="host"
render={({ field }) => (
<FormItem className="grow">
<FormLabel>服务器HOST</FormLabel>
<FormLabel>{t('access.form.ssh.host')}</FormLabel>
<FormControl>
<Input placeholder="请输入Host" {...field} />
<Input placeholder={t('access.form.ssh.host.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -344,10 +347,10 @@ const AccessSSHForm = ({
name="port"
render={({ field }) => (
<FormItem>
<FormLabel>SSH端口</FormLabel>
<FormLabel>{t('access.form.ssh.port')}</FormLabel>
<FormControl>
<Input
placeholder="请输入Port"
placeholder={t('access.form.ssh.port.not.empty')}
{...field}
type="number"
/>
@@ -364,9 +367,9 @@ const AccessSSHForm = ({
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>用户名</FormLabel>
<FormLabel>{t('username')}</FormLabel>
<FormControl>
<Input placeholder="请输入用户名" {...field} />
<Input placeholder={t('username.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -379,10 +382,10 @@ const AccessSSHForm = ({
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>密码</FormLabel>
<FormLabel>{t('password')}</FormLabel>
<FormControl>
<Input
placeholder="请输入密码"
placeholder={t('password.not.empty')}
{...field}
type="password"
/>
@@ -398,9 +401,9 @@ const AccessSSHForm = ({
name="key"
render={({ field }) => (
<FormItem hidden>
<FormLabel>Key(使用证书登录)</FormLabel>
<FormLabel>{t('access.form.ssh.key')}</FormLabel>
<FormControl>
<Input placeholder="请输入Key" {...field} />
<Input placeholder={t('access.form.ssh.key.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -413,7 +416,7 @@ const AccessSSHForm = ({
name="keyFile"
render={({ field }) => (
<FormItem>
<FormLabel>Key(使用证书登录)</FormLabel>
<FormLabel>{t('access.form.ssh.key')}</FormLabel>
<FormControl>
<div>
<Button
@@ -423,10 +426,10 @@ const AccessSSHForm = ({
className="w-48"
onClick={handleSelectFileClick}
>
{fileName ? fileName : "请选择文件"}
{fileName ? fileName : t('access.form.ssh.key.file.not.empty')}
</Button>
<Input
placeholder="请输入Key"
placeholder={t('access.form.ssh.key.not.empty')}
{...field}
ref={fileInputRef}
className="hidden"
@@ -447,9 +450,9 @@ const AccessSSHForm = ({
name="certPath"
render={({ field }) => (
<FormItem>
<FormLabel>证书上传路径</FormLabel>
<FormLabel>{t('access.form.ssh.cert.path')}</FormLabel>
<FormControl>
<Input placeholder="请输入证书上传路径" {...field} />
<Input placeholder={t('access.form.ssh.cert.path.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -462,9 +465,9 @@ const AccessSSHForm = ({
name="keyPath"
render={({ field }) => (
<FormItem>
<FormLabel>私钥上传路径</FormLabel>
<FormLabel>{t('access.form.ssh.key.path')}</FormLabel>
<FormControl>
<Input placeholder="请输入私钥上传路径" {...field} />
<Input placeholder={t('access.form.ssh.key.path.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -492,9 +495,9 @@ const AccessSSHForm = ({
name="command"
render={({ field }) => (
<FormItem>
<FormLabel>Command</FormLabel>
<FormLabel>{t('access.form.ssh.command')}</FormLabel>
<FormControl>
<Textarea placeholder="请输入要执行的命令" {...field} />
<Textarea placeholder={t('access.form.ssh.command.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -505,7 +508,7 @@ const AccessSSHForm = ({
<FormMessage />
<div className="flex justify-end">
<Button type="submit">保存</Button>
<Button type="submit">{t('save')}</Button>
</div>
</form>
</Form>
@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -28,12 +29,13 @@ const AccessTencentForm = ({
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
configType: accessFormType,
secretId: z.string().min(1).max(64),
secretKey: z.string().min(1).max(64),
secretId: z.string().min(1, 'access.form.secret.id.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
secretKey: z.string().min(1, 'access.form.secret.key.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
});
let config: TencentConfig = {
@@ -46,7 +48,7 @@ const AccessTencentForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || '',
configType: "tencent",
secretId: config.secretId,
secretKey: config.secretKey,
@@ -108,9 +110,9 @@ const AccessTencentForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>名称</FormLabel>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input placeholder={t('access.form.name.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -123,7 +125,7 @@ const AccessTencentForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -138,7 +140,7 @@ const AccessTencentForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -153,9 +155,9 @@ const AccessTencentForm = ({
name="secretId"
render={({ field }) => (
<FormItem>
<FormLabel>SecretId</FormLabel>
<FormLabel>{t('access.form.secret.id')}</FormLabel>
<FormControl>
<Input placeholder="请输入SecretId" {...field} />
<Input placeholder={t('access.form.secret.id.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -168,9 +170,9 @@ const AccessTencentForm = ({
name="secretKey"
render={({ field }) => (
<FormItem>
<FormLabel>SecretKey</FormLabel>
<FormLabel>{t('access.form.secret.key')}</FormLabel>
<FormControl>
<Input placeholder="请输入SecretKey" {...field} />
<Input placeholder={t('access.form.secret.key.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -179,7 +181,7 @@ const AccessTencentForm = ({
/>
<div className="flex justify-end">
<Button type="submit">保存</Button>
<Button type="submit">{t('save')}</Button>
</div>
</form>
</Form>
@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -28,11 +29,12 @@ const WebhookForm = ({
onAfterReq: () => void;
}) => {
const { addAccess, updateAccess } = useConfig();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(64),
name: z.string().min(1, 'access.form.name.not.empty').max(64, t('zod.rule.string.max', { max: 64 })),
configType: accessFormType,
url: z.string().url(),
url: z.string().url('zod.rule.url'),
});
let config: WebhookConfig = {
@@ -44,14 +46,13 @@ const WebhookForm = ({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name,
name: data?.name || '',
configType: "webhook",
url: config.url,
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
console.log(data);
const req: Access = {
id: data.id as string,
name: data.name,
@@ -106,9 +107,9 @@ const WebhookForm = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>名称</FormLabel>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input placeholder="请输入授权名称" {...field} />
<Input placeholder={t('access.form.name.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -121,7 +122,7 @@ const WebhookForm = ({
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -136,7 +137,7 @@ const WebhookForm = ({
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>配置类型</FormLabel>
<FormLabel>{t('access.form.config.field')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -151,9 +152,9 @@ const WebhookForm = ({
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook Url</FormLabel>
<FormLabel>{t('access.form.webhook.url')}</FormLabel>
<FormControl>
<Input placeholder="请输入Webhook Url" {...field} />
<Input placeholder={t('access.form.webhook.url.not.empty')} {...field} />
</FormControl>
<FormMessage />
@@ -162,7 +163,7 @@ const WebhookForm = ({
/>
<div className="flex justify-end">
<Button type="submit">保存</Button>
<Button type="submit">{t('save')}</Button>
</div>
</form>
</Form>
+59 -71
View File
@@ -1,3 +1,8 @@
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils"
import { Separator } from "../ui/separator";
type DeployProgressProps = {
@@ -6,80 +11,63 @@ type DeployProgressProps = {
};
const DeployProgress = ({ phase, phaseSuccess }: DeployProgressProps) => {
let rs = <> </>;
const { t } = useTranslation();
let step = 0;
if (phase === "check") {
if (phaseSuccess) {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600">检查 </div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground">获取</div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground">部署</div>
</div>
);
} else {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-red-600">检查 </div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground">获取</div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground">部署</div>
</div>
);
}
step = 1
} else if (phase === "apply") {
step = 2
} else if (phase === "deploy") {
step = 3
}
if (phase === "apply") {
if (phaseSuccess) {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600">检查 </div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600">获取</div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground">部署</div>
</div>
);
} else {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600">检查 </div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-red-600">获取</div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground">部署</div>
</div>
);
}
}
if (phase === "deploy") {
if (phaseSuccess) {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600">检查 </div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600">获取</div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600">部署</div>
</div>
);
} else {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600">检查 </div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600">获取</div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-red-600">部署</div>
</div>
);
}
}
return rs;
return (
<div className="flex items-center">
<div className={
cn(
"text-xs text-nowrap",
step === 1 ? phaseSuccess ? "text-green-600" : "text-red-600" : "",
step > 1 ? "text-green-600" : "",
)
}>
{t('deploy.progress.check')}
</div>
<Separator className={
cn(
"h-1 grow max-w-[60px]",
step > 1 ? "bg-green-600" : "",
)
} />
<div className={
cn(
"text-xs text-nowrap",
step < 2 ? "text-muted-foreground" : "",
step === 2 ? phaseSuccess ? "text-green-600" : "text-red-600" : "",
step > 2 ? "text-green-600" : "",
)
}>
{t('deploy.progress.apply')}
</div>
<Separator className={
cn(
"h-1 grow max-w-[60px]",
step > 2 ? "bg-green-600" : "",
)
} />
<div className={
cn(
"text-xs text-nowrap",
step < 3 ? "text-muted-foreground" : "",
step === 3 ? phaseSuccess ? "text-green-600" : "text-red-600" : "",
step > 3 ? "text-green-600" : "",
)
}>
{t('deploy.progress.deploy')}
</div>
</div>
)
};
export default DeployProgress;
+8 -6
View File
@@ -10,6 +10,7 @@ import {
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
Form,
FormControl,
@@ -39,9 +40,10 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
} = useConfig();
const [open, setOpen] = useState(false);
const { t } = useTranslation();
const formSchema = z.object({
email: z.string().email(),
email: z.string().email("email.valid.message"),
});
const form = useForm<z.infer<typeof formSchema>>({
@@ -54,7 +56,7 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
const onSubmit = async (data: z.infer<typeof formSchema>) => {
if ((emails.content as EmailsSetting).emails.includes(data.email)) {
form.setError("email", {
message: "邮箱已存在",
message: "email.already.exist",
});
return;
}
@@ -100,7 +102,7 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
</DialogTrigger>
<DialogContent className="sm:max-w-[600px] w-full dark:text-stone-200">
<DialogHeader>
<DialogTitle>添加邮箱</DialogTitle>
<DialogTitle>{t('email.add')}</DialogTitle>
</DialogHeader>
<div className="container py-3">
@@ -118,9 +120,9 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>邮箱</FormLabel>
<FormLabel>{t('email')}</FormLabel>
<FormControl>
<Input placeholder="请输入邮箱" {...field} type="email" />
<Input placeholder={t('email.not.empty.message')} {...field} type="email" />
</FormControl>
<FormMessage />
@@ -129,7 +131,7 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
/>
<div className="flex justify-end">
<Button type="submit">保存</Button>
<Button type="submit">{t('save')}</Button>
</div>
</form>
</Form>
+4 -1
View File
@@ -1,9 +1,12 @@
import { BookOpen } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Separator } from "../ui/separator";
import { version } from "@/domain/version";
const Version = () => {
const { t } = useTranslation()
return (
<div className="fixed right-0 bottom-0 w-full flex justify-between p-5">
<div className=""></div>
@@ -14,7 +17,7 @@ const Version = () => {
className="flex items-center"
>
<BookOpen size={16} />
<div className="ml-1">文档</div>
<div className="ml-1">{t('document')}</div>
</a>
<Separator orientation="vertical" className="mx-2" />
<a
+9 -7
View File
@@ -8,6 +8,7 @@ import { useEffect, useState } from "react";
import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast";
import { useTranslation } from 'react-i18next'
type DingTalkSetting = {
id: string;
@@ -17,6 +18,7 @@ type DingTalkSetting = {
const DingTalk = () => {
const { config, setChannels } = useNotify();
const { t } = useTranslation();
const [dingtalk, setDingtalk] = useState<DingTalkSetting>({
id: config.id ?? "",
@@ -70,15 +72,15 @@ const DingTalk = () => {
setChannels(resp);
toast({
title: "保存成功",
description: "配置保存成功",
title: t('save.succeed'),
description: t('setting.notify.config.save.succeed'),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: "保存失败",
description: "配置保存失败:" + msg,
title: t('save.failed'),
description: `${t('setting.notify.config.save.failed')}: ${msg}`,
variant: "destructive",
});
}
@@ -100,7 +102,7 @@ const DingTalk = () => {
}}
/>
<Input
placeholder="加签的签名"
placeholder={t('access.form.ding.access.token.placeholder')}
className="mt-2"
value={dingtalk.data.secret}
onChange={(e) => {
@@ -127,7 +129,7 @@ const DingTalk = () => {
});
}}
/>
<Label htmlFor="airplane-mode">是否启用</Label>
<Label htmlFor="airplane-mode">{t('setting.notify.config.enable')}</Label>
</div>
<div className="flex justify-end mt-2">
@@ -136,7 +138,7 @@ const DingTalk = () => {
handleSaveClick();
}}
>
保存
{t('save')}
</Button>
</div>
</div>
+7 -5
View File
@@ -9,6 +9,7 @@ import {
} from "@/domain/settings";
import { getSetting, update } from "@/repository/settings";
import { useToast } from "../ui/use-toast";
import { useTranslation } from 'react-i18next'
const NotifyTemplate = () => {
const [id, setId] = useState("");
@@ -17,6 +18,7 @@ const NotifyTemplate = () => {
]);
const { toast } = useToast();
const { t } = useTranslation();
useEffect(() => {
const featchData = async () => {
@@ -66,8 +68,8 @@ const NotifyTemplate = () => {
}
toast({
title: "保存成功",
description: "通知模板保存成功",
title: t('save.succeed'),
description: t('setting.notify.template.save.succeed'),
});
};
@@ -81,7 +83,7 @@ const NotifyTemplate = () => {
/>
<div className="text-muted-foreground text-sm mt-1">
可选的变量, COUNT:即将过期张数
{t('setting.notify.template.variables.tips.title')}
</div>
<Textarea
@@ -92,10 +94,10 @@ const NotifyTemplate = () => {
}}
></Textarea>
<div className="text-muted-foreground text-sm mt-1">
可选的变量, COUNT:即将过期张数,DOMAINS:域名列表
{t('setting.notify.template.variables.tips.content')}
</div>
<div className="flex justify-end mt-2">
<Button onClick={handleSaveClick}>保存</Button>
<Button onClick={handleSaveClick}>{t('save')}</Button>
</div>
</div>
);
+8 -6
View File
@@ -8,6 +8,7 @@ import { useEffect, useState } from "react";
import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast";
import { useTranslation } from "react-i18next";
type TelegramSetting = {
id: string;
@@ -17,6 +18,7 @@ type TelegramSetting = {
const Telegram = () => {
const { config, setChannels } = useNotify();
const { t } = useTranslation();
const [telegram, setTelegram] = useState<TelegramSetting>({
id: config.id ?? "",
@@ -70,15 +72,15 @@ const Telegram = () => {
setChannels(resp);
toast({
title: "保存成功",
description: "配置保存成功",
title: t('save.succeed'),
description: t('setting.notify.config.save.succeed'),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: "保存失败",
description: "配置保存失败:" + msg,
title: t('save.failed'),
description: `${t('setting.notify.config.save.failed')}: ${msg}`,
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 className="flex justify-end mt-2">
@@ -137,7 +139,7 @@ const Telegram = () => {
handleSaveClick();
}}
>
保存
{t('save')}
</Button>
</div>
</div>
+10 -8
View File
@@ -9,6 +9,7 @@ import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast";
import { isValidURL } from "@/lib/url";
import { useTranslation } from 'react-i18next'
type WebhookSetting = {
id: string;
@@ -18,6 +19,7 @@ type WebhookSetting = {
const Webhook = () => {
const { config, setChannels } = useNotify();
const { t } = useTranslation();
const [webhook, setWebhook] = useState<WebhookSetting>({
id: config.id ?? "",
@@ -59,8 +61,8 @@ const Webhook = () => {
webhook.data.url = webhook.data.url.trim();
if (!isValidURL(webhook.data.url)) {
toast({
title: "保存失败",
description: "Url格式不正确",
title: t('save.failed'),
description: t('setting.notify.config.save.failed.url.not.valid'),
variant: "destructive",
});
return;
@@ -79,15 +81,15 @@ const Webhook = () => {
setChannels(resp);
toast({
title: "保存成功",
description: "配置保存成功",
title: t('save.succeed'),
description: t('setting.notify.config.save.succeed'),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: "保存失败",
description: "配置保存失败:" + msg,
title: t('save.failed'),
description: `${t('setting.notify.config.save.failed')}: ${msg}`,
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 className="flex justify-end mt-2">
@@ -132,7 +134,7 @@ const Webhook = () => {
handleSaveClick();
}}
>
保存
{t('save')}
</Button>
</div>
</div>
+4 -1
View File
@@ -12,6 +12,7 @@ import {
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { useTranslation } from "react-i18next"
const Form = FormProvider
@@ -145,7 +146,9 @@ const FormMessage = React.forwardRef<
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
const { t } = useTranslation()
const body = error ? t(String(error?.message)) : children
if (!body) {
return null
+31 -22
View File
@@ -3,6 +3,7 @@ import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
import { ButtonProps, buttonVariants } from "@/components/ui/button";
import { useTranslation } from "react-i18next";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
@@ -62,33 +63,41 @@ PaginationLink.displayName = "PaginationLink";
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>上一页</span>
</PaginationLink>
);
}: React.ComponentProps<typeof PaginationLink>) => {
const { t } = useTranslation()
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>{t('pagination.prev')}</span>
</PaginationLink>
)
};
PaginationPrevious.displayName = "PaginationPrevious";
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>下一页</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
}: React.ComponentProps<typeof PaginationLink>) => {
const { t } = useTranslation()
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>{t('pagination.next')}</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
};
PaginationNext.displayName = "PaginationNext";
const PaginationEllipsis = ({
+10 -10
View File
@@ -1,15 +1,15 @@
import { z } from "zod";
export const accessTypeMap: Map<string, [string, string]> = new Map([
["tencent", ["腾讯云", "/imgs/providers/tencent.svg"]],
["aliyun", ["阿里云", "/imgs/providers/aliyun.svg"]],
["cloudflare", ["Cloudflare", "/imgs/providers/cloudflare.svg"]],
["namesilo", ["Namesilo", "/imgs/providers/namesilo.svg"]],
["godaddy", ["GoDaddy", "/imgs/providers/godaddy.svg"]],
["qiniu", ["七牛云", "/imgs/providers/qiniu.svg"]],
["ssh", ["SSH部署", "/imgs/providers/ssh.svg"]],
["webhook", ["Webhook", "/imgs/providers/webhook.svg"]],
["local", ["本地部署", "/imgs/providers/local.svg"]],
["tencent", ["tencent", "/imgs/providers/tencent.svg"]],
["aliyun", ["aliyun", "/imgs/providers/aliyun.svg"]],
["cloudflare", ["cloudflare", "/imgs/providers/cloudflare.svg"]],
["namesilo", ["namesilo", "/imgs/providers/namesilo.svg"]],
["godaddy", ["go.daddy", "/imgs/providers/godaddy.svg"]],
["qiniu", ["qiniu", "/imgs/providers/qiniu.svg"]],
["ssh", ["ssh", "/imgs/providers/ssh.svg"]],
["webhook", ["webhook", "/imgs/providers/webhook.svg"]],
["local", ["local", "/imgs/providers/local.svg"]],
]);
export const getProviderInfo = (t: string) => {
@@ -28,7 +28,7 @@ export const accessFormType = z.union(
z.literal("godaddy"),
z.literal("local"),
],
{ message: "请选择云服务商" }
{ message: "access.not.empty" }
);
type AccessUsage = "apply" | "deploy" | "all";
+8 -8
View File
@@ -40,14 +40,14 @@ export const getLastDeployment = (domain: Domain): Deployment | undefined => {
};
export const targetTypeMap: Map<string, [string, string]> = new Map([
["aliyun-cdn", ["阿里云-CDN", "/imgs/providers/aliyun.svg"]],
["aliyun-oss", ["阿里云-OSS", "/imgs/providers/aliyun.svg"]],
["aliyun-dcdn", ["阿里云-DCDN", "/imgs/providers/aliyun.svg"]],
["tencent-cdn", ["腾讯云-CDN", "/imgs/providers/tencent.svg"]],
["ssh", ["SSH部署", "/imgs/providers/ssh.svg"]],
["qiniu-cdn", ["七牛云-CDN", "/imgs/providers/qiniu.svg"]],
["webhook", ["Webhook", "/imgs/providers/webhook.svg"]],
["local", ["本地部署", "/imgs/providers/local.svg"]],
["aliyun-cdn", ["aliyun.cdn", "/imgs/providers/aliyun.svg"]],
["aliyun-oss", ["aliyun.oss", "/imgs/providers/aliyun.svg"]],
["aliyun-dcdn", ["aliyun.dcdn", "/imgs/providers/aliyun.svg"]],
["tencent-cdn", ["tencent.cdn", "/imgs/providers/tencent.svg"]],
["ssh", ["ssh", "/imgs/providers/ssh.svg"]],
["qiniu-cdn", ["qiniu.cdn", "/imgs/providers/qiniu.svg"]],
["webhook", ["webhook", "/imgs/providers/webhook.svg"]],
["local", ["local", "/imgs/providers/local.svg"]],
]);
export const targetTypeKeys = Array.from(targetTypeMap.keys());
+22
View File
@@ -0,0 +1,22 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import resources from './locales'
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: 'zh',
debug: true,
interpolation: {
escapeValue: false,
},
backend: {
loadPath: '/locales/{{lng}}.json',
}
});
export default i18n;
+210
View File
@@ -0,0 +1,210 @@
{
"ca": "Certificate Authority",
"username": "Username",
"username.not.empty": "Please enter username",
"password": "Password",
"password.not.empty": "Please enter password",
"email": "Email",
"logout": "Logout",
"setting": "Settings",
"account": "Account",
"template": "Template",
"save": "Save",
"no.data": "No data available",
"status": "Status",
"operation": "Operation",
"enable": "Enable",
"disable": "Disable",
"deploy": "Deploy",
"download": "Download",
"delete": "Delete",
"cancel": "Cancel",
"confirm": "Confirm",
"edit": "Edit",
"succeed": "Successful",
"add": "Add",
"document": "Document",
"variables": "Variables",
"dns": "Domain Name Server",
"name": "Name",
"create.time": "CreateTime",
"update.time": "UpdateTime",
"created.in": "Created in",
"updated.in": "Updated in",
"basic.setting": "Basic Settings",
"advanced.setting": "Advanced Settings",
"operation.succeed": "Operation Successful",
"save.succeed": "Save Successful",
"save.failed": "Save Failed",
"update.succeed": "Update Successful",
"update.failed": "Update Failed",
"delete.failed": "Delete Failed",
"ding.talk": "Ding Talk",
"telegram": "Telegram",
"webhook": "Webhook",
"local": "Local Deployment",
"tencent": "Tencent",
"tencent.cdn": "Tencent-CDN",
"aliyun": "Alibaba Cloud",
"aliyun.cdn": "Alibaba Cloud-CDN",
"aliyun.oss": "Alibaba Cloud-OSS",
"aliyun.dcdn": "Alibaba Cloud-DCDN",
"qiniu": "Qiniu",
"qiniu.cdn": "Qiniu-CDN",
"cloudflare": "Cloudflare",
"namesilo": "Namesilo",
"go.daddy": "GoDaddy",
"ssh": "SSH Deployment",
"zod.rule.string.max": "Please enter no more than {{max}} characters",
"zod.rule.url": "Please enter a valid URL",
"zod.rule.ssh.host": "Please enter the correct domain name or IP",
"login.submit": "Log In",
"login.username.no.empty.message": "Please enter a valid email address",
"login.password.length.message": "Password should be at least 10 characters",
"menu.auth.management": "Authorization Management",
"theme.light": "Light",
"theme.dark": "Dark",
"theme.system": "System",
"dashboard": "Dashboard",
"dashboard.all": "All",
"dashboard.near.expired": "About to Expire",
"dashboard.enabled": "Enabled",
"dashboard.not.enabled": "Not Enabled",
"dashboard.unit": "Unit",
"deployment.log.name": "Deployment History",
"deployment.log.empty": "You have not created any deployments yet, please add a domain to start deployment!",
"deployment.log.status": "Status",
"deployment.log.stage": "Stage",
"deployment.log.last.execution.time": "Last Execution Time",
"deployment.log.detail.button.text": "Log",
"deployment.log.detail": "Deployment Details",
"pagination.next": "Next",
"pagination.prev": "Previous",
"domain": "Domain",
"domain.add": "Add Domain",
"domain.delete": "Delete Domain",
"domain.not.empty.verify.message": "Please enter domain",
"domain.management.name": "Domain List",
"domain.management.start.deploy.succeed.tips": "Deployment initiated, please check the deployment log later.",
"domain.management.execution.failed": "Execution Failed",
"domain.management.execution.failed.tips": "Execution failed, please check the details in <1>Deployment History</1>.",
"domain.management.empty": "Please add a domain to start deploying the certificate.",
"domain.management.expiry.date": "Validity Period",
"domain.management.expiry.date1": "Valid for {{date}} days",
"domain.management.expiry.date2": "Expiry on {{date}}",
"domain.management.last.execution.time": "Last Execution Time",
"domain.management.last.execution.status": "Last Execution Status",
"domain.management.last.execution.stage": "Last Execution Stage",
"domain.management.enable": "Enable",
"domain.management.start.deploying": "Deploy Now",
"domain.management.forced.deployment": "Force Deployment",
"domain.management.delete.confirm": "Are you sure you want to delete this domain?",
"domain.management.edit.title": "Edit Domain",
"domain.management.edit.dns.access.label": "DNS Provider Authorization Configuration",
"domain.management.edit.dns.access.not.empty.message": "Please select DNS provider authorization configuration",
"domain.management.edit.access.label": "Provider Authorization Configuration",
"domain.management.edit.access.not.empty.message": "Please select authorization configuration",
"domain.management.edit.target.type": "Deployment Service Type",
"domain.management.edit.target.type.not.empty.message": "Please select deployment service type",
"domain.management.edit.succeed.tips": "Successful domain editing",
"domain.management.edit.target.access": "Deployment Service Provider Authorization Configuration",
"domain.management.edit.target.access.content.label": "Provider Authorization Configuration",
"domain.management.edit.target.access.not.empty.message": "Please select authorization configuration",
"domain.management.edit.target.access.verify.msg": "At least one of the deployment authorization and deployment authorization group must be selected",
"domain.management.edit.group.label": "Deployment Configuration Group (used to deploy a domain certificate to multiple ssh hosts)",
"domain.management.edit.group.not.empty.message": "Please select group",
"domain.management.edit.email.not.empty.message": "Please select email",
"domain.management.edit.email.description": "(A email is required to apply for a certificate)",
"domain.management.edit.variables.placeholder": "It can be used in SSH deployment, like:\nkey=val;\nkey2=val2;",
"domain.management.edit.dns.placeholder": "Custom domain name server, separates multiple entries with semicolon, like:\n8.8.8.8;\n8.8.4.4;",
"domain.management.add.succeed.tips": "Domain added successfully",
"email.add": "Add Email",
"email.list": "Email List",
"email.valid.message": "Please enter a valid email address",
"email.already.exist": "Email already exists",
"email.not.empty.message": "Please enter email",
"setting.notify.menu": "Notification Push",
"setting.submit": "Confirm Changes",
"setting.account.email.valid.message": "Please enter a valid email address",
"setting.account.email.placeholder": "Please enter email",
"setting.account.email.change.succeed": "Account email altered successfully",
"setting.account.email.change.failed": "Account email alteration failed",
"setting.account.log.back.in": "Please login again",
"setting.password.length.message": "Password should be at least 10 characters",
"setting.password.not.match": "Passwords do not match",
"setting.password.change.succeed": "Password changed successfully",
"setting.password.change.failed": "Password change failed",
"setting.password.current.password": "Current Password",
"setting.password.new.password": "New Password",
"setting.password.confirm.password": "Confirm Password",
"setting.notify.template.save.succeed": "Notification template saved successfully",
"setting.notify.template.variables.tips.title": "Optional variables, COUNT: number of expiring soon",
"setting.notify.template.variables.tips.content": "Optional variables, COUNT: number of expiring soon, DOMAINS: Domain list",
"setting.notify.config.enable": "Enable",
"setting.notify.config.save.succeed": "Configuration saved successfully",
"setting.notify.config.save.failed": "Configuration save failed",
"setting.notify.config.save.failed.url.not.valid": "Invalid Url format",
"setting.ca.not.empty": "Please select a Certificate Authority",
"setting.ca.eab_kid.not.empty": "Please enter EAB_KID",
"setting.ca.eab_hmac_key.not.empty": "Please enter EAB_HMAC_KEY.",
"setting.ca.eab_kid_hmac_key.not.empty": "Please enter EAB_KID and EAB_HMAC_KEY",
"deploy.progress.check": "Check",
"deploy.progress.apply": "Apply",
"deploy.progress.deploy": "Deploy",
"access.management": "Authorization Management",
"access.add": "Add Authorization",
"access.edit": "Edit Authorization",
"access.all": "All Authorizations",
"access.list": "Authorization List",
"access.type": "Provider",
"access.type.not.empty": "Please select a provider",
"access.not.empty": "Please select a cloud provider",
"access.empty": "Please add authorization to start deploying certificate.",
"access.group.management": "Authorization Group Management",
"access.group.add": "Add Authorization Group",
"access.group.not.empty": "Please select a group",
"access.group.name": "Group Name",
"access.group.name.not.empty": "Please enter group name",
"access.group.delete": "Delete Group",
"access.group.delete.confirm": "Are you sure you want to delete the deployment authorization group?",
"access.group.domain.empty": "Please add a domain to start deploying the certificate.",
"access.group.empty": "No deployment authorization configuration yet, please add after starting use.",
"access.group.total": "Totally {{total}} deployment authorization configuration",
"access.form.name.not.empty": "Please enter authorization name",
"access.form.config.field": "Configuration Type",
"access.form.access.key.id": "AccessKeyId",
"access.form.access.key.id.not.empty": "Please enter AccessKeyId",
"access.form.access.key.secret": "AccessKeySecret",
"access.form.access.key.secret.not.empty": "Please enter AccessKeySecret",
"access.form.cloud.dns.api.token": "CLOUD_DNS_API_TOKEN",
"access.form.cloud.dns.api.token.not.empty": "Please enter CLOUD_DNS_API_TOKEN",
"access.form.go.daddy.api.key": "GO_DADDY_API_KEY",
"access.form.go.daddy.api.key.not.empty": "Please enter GO_DADDY_API_KEY",
"access.form.go.daddy.api.secret": "GO_DADDY_API_SECRET",
"access.form.go.daddy.api.secret.not.empty": "Please enter GO_DADDY_API_SECRET",
"access.form.namesilo.api.key": "NAMESILO_API_KEY",
"access.form.namesilo.api.key.not.empty": "Please enter NAMESILO_API_KEY",
"access.form.secret.id": "SecretId",
"access.form.secret.id.not.empty": "Please enter SecretId",
"access.form.secret.key": "SecretKey",
"access.form.secret.key.not.empty": "Please enter SecretKey",
"access.form.access.key": "AccessKey",
"access.form.access.key.not.empty": "Please enter AccessKey",
"access.form.webhook.url": "Webhook URL",
"access.form.webhook.url.not.empty": "Please enter Webhook URL",
"access.form.ssh.group.label": "Authorization Configuration Group (used to deploy a single domain certificate to multiple SSH hosts)",
"access.form.ssh.host": "Server Host",
"access.form.ssh.host.not.empty": "Please enter Host",
"access.form.ssh.port": "SSH Port",
"access.form.ssh.port.not.empty": "Please enter Port",
"access.form.ssh.key": "Key (Log in using certificate)",
"access.form.ssh.key.not.empty": "Please enter Key",
"access.form.ssh.key.file.not.empty": "Please select file",
"access.form.ssh.cert.path": "Certificate Upload Path",
"access.form.ssh.cert.path.not.empty": "Please enter certificate upload path",
"access.form.ssh.key.path": "Private Key Upload Path",
"access.form.ssh.key.path.not.empty": "Please enter private key upload path",
"access.form.ssh.command": "Command",
"access.form.ssh.command.not.empty": "Please enter command",
"access.form.ding.access.token.placeholder": "Signature for signed addition"
}
+17
View File
@@ -0,0 +1,17 @@
import { Resource } from 'i18next'
import zh from './zh.json'
import en from './en.json'
const resources: Resource = {
zh: {
name: '简体中文',
translation: zh
},
en: {
name: 'English',
translation: en
}
}
export default resources;
+210
View File
@@ -0,0 +1,210 @@
{
"ca": "证书颁发机构",
"username": "用户名",
"username.not.empty": "请输入用户名",
"password": "密码",
"password.not.empty": "请输入密码",
"email": "邮箱",
"logout": "退出登录",
"setting": "设置",
"account": "账户",
"template": "模版",
"save": "保存",
"no.data": "暂无数据",
"status": "状态",
"operation": "操作",
"enable": "启用",
"disable": "禁用",
"deploy": "部署",
"download": "下载",
"delete": "删除",
"cancel": "取消",
"confirm": "确认",
"edit": "编辑",
"succeed": "成功",
"add": "新增",
"document": "文档",
"variables": "变量",
"dns": "域名服务器",
"name": "名称",
"create.time": "创建时间",
"update.time": "更新时间",
"created.in": "创建于",
"updated.in": "更新于",
"basic.setting": "基础设置",
"advanced.setting": "高级设置",
"operation.succeed": "操作成功",
"save.succeed": "保存成功",
"save.failed": "保存失败",
"update.succeed": "修改成功",
"update.failed": "修改失败",
"delete.failed": "删除失败",
"ding.talk": "钉钉",
"telegram": "Telegram",
"webhook": "Webhook",
"local": "本地部署",
"tencent": "腾讯云",
"tencent.cdn": "腾讯云-CDN",
"aliyun": "阿里云",
"aliyun.cdn": "阿里云-CDN",
"aliyun.oss": "阿里云-OSS",
"aliyun.dcdn": "阿里云-DCDN",
"qiniu": "七牛云",
"qiniu.cdn": "七牛云-CDN",
"cloudflare": "Cloudflare",
"namesilo": "Namesilo",
"go.daddy": "GoDaddy",
"ssh": "SSH 部署",
"zod.rule.string.max": "请输入不超过 {{max}} 个字符",
"zod.rule.url": "请输入有效的 url 地址",
"zod.rule.ssh.host": "请输入正确的域名或IP",
"login.submit": "登录",
"login.username.no.empty.message": "请输入正确的邮箱地址",
"login.password.length.message": "密码至少10个字符",
"menu.auth.management": "授权管理",
"theme.light": "浅色",
"theme.dark": "暗黑",
"theme.system": "系统",
"dashboard": "控制面板",
"dashboard.all": "所有",
"dashboard.near.expired": "即将过期",
"dashboard.enabled": "启用中",
"dashboard.not.enabled": "未启用",
"dashboard.unit": "个",
"deployment.log.name": "部署历史",
"deployment.log.empty": "你暂未创建任何部署,请先添加域名进行部署吧!",
"deployment.log.status": "状态",
"deployment.log.stage": "阶段",
"deployment.log.last.execution.time": "最近执行时间",
"deployment.log.detail.button.text": "日志",
"deployment.log.detail": "部署详情",
"pagination.next": "下一页",
"pagination.prev": "上一页",
"domain": "域名",
"domain.add": "新增域名",
"domain.delete": "删除域名",
"domain.not.empty.verify.message": "请输入域名",
"domain.management.name": "域名列表",
"domain.management.start.deploy.succeed.tips": "已发起部署,请稍后查看部署日志。",
"domain.management.execution.failed": "执行失败",
"domain.management.execution.failed.tips": "执行失败,请在 <1>部署历史</1> 查看详情。",
"domain.management.empty": "请添加域名开始部署证书吧。",
"domain.management.expiry.date": "有效期限",
"domain.management.expiry.date1": "有效期 {{date}} 天",
"domain.management.expiry.date2": "{{date}} 到期",
"domain.management.last.execution.time": "最近执行时间",
"domain.management.last.execution.status": "最近执行状态",
"domain.management.last.execution.stage": "最近执行阶段",
"domain.management.enable": "是否启用",
"domain.management.start.deploying": "立即部署",
"domain.management.forced.deployment": "强行部署",
"domain.management.delete.confirm": "确定要删除域名吗?",
"domain.management.edit.title": "编辑域名",
"domain.management.edit.dns.access.label": "DNS 服务商授权配置",
"domain.management.edit.dns.access.not.empty.message": "请选择DNS服务商授权配置",
"domain.management.edit.access.label": "服务商授权配置",
"domain.management.edit.access.not.empty.message": "请选择授权配置",
"domain.management.edit.target.type": "部署服务类型",
"domain.management.edit.target.type.not.empty.message": "请选择部署服务类型",
"domain.management.edit.succeed.tips": "域名编辑成功",
"domain.management.edit.target.access": "部署服务商授权配置",
"domain.management.edit.target.access.content.label": "服务商授权配置",
"domain.management.edit.target.access.not.empty.message": "请选择授权配置",
"domain.management.edit.target.access.verify.msg": "部署授权和部署授权组至少选一个",
"domain.management.edit.group.label": "部署配置组(用于将一个域名证书部署到多个 ssh 主机)",
"domain.management.edit.group.not.empty.message": "请选择分组",
"domain.management.edit.email.not.empty.message": "请选择邮箱",
"domain.management.edit.email.description": "(申请证书需要提供邮箱)",
"domain.management.edit.variables.placeholder": "可在SSH部署中使用,形如:\nkey=val;\nkey2=val2;",
"domain.management.edit.dns.placeholder": "自定义域名服务器,多个用分号隔开,如:\n8.8.8.8;\n8.8.4.4;",
"domain.management.add.succeed.tips": "域名添加成功",
"email.add": "添加邮箱",
"email.list": "邮箱列表",
"email.valid.message": "请输入正确的邮箱地址",
"email.already.exist": "邮箱已存在",
"email.not.empty.message": "请输入邮箱",
"setting.notify.menu": "消息推送",
"setting.submit": "确认修改",
"setting.account.email.valid.message": "请输入正确的邮箱地址",
"setting.account.email.placeholder": "请输入邮箱",
"setting.account.email.change.succeed": "修改账户邮箱成功",
"setting.account.email.change.failed": "修改账户邮箱失败",
"setting.account.log.back.in": "请重新登录",
"setting.password.length.message": "密码至少10个字符",
"setting.password.not.match": "两次密码不一致",
"setting.password.change.succeed": "修改密码成功",
"setting.password.change.failed": "修改密码失败",
"setting.password.current.password": "当前密码",
"setting.password.new.password": "新密码",
"setting.password.confirm.password": "确认密码",
"setting.notify.template.save.succeed": "通知模板保存成功",
"setting.notify.template.variables.tips.title": "可选的变量, COUNT:即将过期张数",
"setting.notify.template.variables.tips.content": "可选的变量, COUNT:即将过期张数,DOMAINS:域名列表",
"setting.notify.config.enable": "是否启用",
"setting.notify.config.save.succeed": "配置保存成功",
"setting.notify.config.save.failed": "配置保存失败",
"setting.notify.config.save.failed.url.not.valid": "Url格式不正确",
"setting.ca.not.empty": "请选择证书分发机构",
"setting.ca.eab_kid.not.empty": "请输入EAB_KID",
"setting.ca.eab_hmac_key.not.empty": "请输入EAB_HMAC_KEY",
"setting.ca.eab_kid_hmac_key.not.empty": "请输入EAB_KID和EAB_HMAC_KEY",
"deploy.progress.check": "检查",
"deploy.progress.apply": "获取",
"deploy.progress.deploy": "部署",
"access.management": "授权管理",
"access.add": "添加授权",
"access.edit": "编辑授权",
"access.all": "所有授权",
"access.list": "授权列表",
"access.type": "服务商",
"access.type.not.empty": "请选择服务商",
"access.not.empty": "请选择云服务商",
"access.empty": "请添加授权开始部署证书吧。",
"access.group.management": "授权组管理",
"access.group.add": "添加授权组",
"access.group.not.empty": "请选择分组",
"access.group.name": "组名",
"access.group.name.not.empty": "请输入组名",
"access.group.delete": "删除组",
"access.group.delete.confirm": "确定要删除部署授权组吗?",
"access.group.domain.empty": "请添加域名开始部署证书吧。",
"access.group.empty": "暂无部署授权配置,请添加后开始使用吧",
"access.group.total": "共有 {{total}} 个部署授权配置",
"access.form.name.not.empty": "请输入授权名称",
"access.form.config.field": "配置类型",
"access.form.access.key.id": "AccessKeyId",
"access.form.access.key.id.not.empty": "请输入 AccessKeyId",
"access.form.access.key.secret": "AccessKeySecret",
"access.form.access.key.secret.not.empty": "请输入 AccessKeySecret",
"access.form.cloud.dns.api.token": "CLOUD_DNS_API_TOKEN",
"access.form.cloud.dns.api.token.not.empty": "请输入 CLOUD_DNS_API_TOKEN",
"access.form.go.daddy.api.key": "GO_DADDY_API_KEY",
"access.form.go.daddy.api.key.not.empty": "请输入 GO_DADDY_API_KEY",
"access.form.go.daddy.api.secret": "GO_DADDY_API_SECRET",
"access.form.go.daddy.api.secret.not.empty": "请输入 GO_DADDY_API_SECRET",
"access.form.namesilo.api.key": "NAMESILO_API_KEY",
"access.form.namesilo.api.key.not.empty": "请输入 NAMESILO_API_KEY",
"access.form.secret.id": "SecretId",
"access.form.secret.id.not.empty": "请输入 SecretId",
"access.form.secret.key": "SecretKey",
"access.form.secret.key.not.empty": "请输入 SecretKey",
"access.form.access.key": "AccessKey",
"access.form.access.key.not.empty": "请输入 AccessKey",
"access.form.webhook.url": "Webhook URL",
"access.form.webhook.url.not.empty": "请输入 Webhook URL",
"access.form.ssh.group.label": "授权配置组(用于将一个域名证书部署到多个 ssh 主机)",
"access.form.ssh.host": "服务器 Host",
"access.form.ssh.host.not.empty": "请输入 Host",
"access.form.ssh.port": "SSH 端口",
"access.form.ssh.port.not.empty": "请输入 Port",
"access.form.ssh.key": "Key(使用证书登录)",
"access.form.ssh.key.not.empty": "请输入 Key",
"access.form.ssh.key.file.not.empty": "请选择文件",
"access.form.ssh.cert.path": "证书上传路径",
"access.form.ssh.cert.path.not.empty": "请输入证书上传路径",
"access.form.ssh.key.path": "私钥上传路径",
"access.form.ssh.key.path.not.empty": "请输入私钥上传路径",
"access.form.ssh.command": "Command",
"access.form.ssh.command.not.empty": "请输入要执行的命令",
"access.form.ding.access.token.placeholder": "加签的签名"
}
+1
View File
@@ -4,6 +4,7 @@ import "./global.css";
import { RouterProvider } from "react-router-dom";
import { router } from "./router.tsx";
import { ThemeProvider } from "./components/ThemeProvider.tsx";
import "@/i18n";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
+15 -11
View File
@@ -6,6 +6,7 @@ import {
useNavigate,
} from "react-router-dom";
import { CircleUser, Earth, History, Home, Menu, Server } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
@@ -22,12 +23,14 @@ import { cn } from "@/lib/utils";
import { ConfigProvider } from "@/providers/config";
import { getPb } from "@/repository/api";
import { ThemeToggle } from "@/components/ThemeToggle";
import LocaleToggle from "@/components/LocaleToggle";
import Version from "@/components/certimate/Version";
export default function Dashboard() {
const navigate = useNavigate();
const location = useLocation();
const { t } = useTranslation()
if (!getPb().authStore.isValid || !getPb().authStore.isAdmin) {
return <Navigate to="/login" />;
@@ -70,7 +73,7 @@ export default function Dashboard() {
)}
>
<Home className="h-4 w-4" />
控制面板
{t('dashboard')}
</Link>
<Link
to="/domains"
@@ -80,7 +83,7 @@ export default function Dashboard() {
)}
>
<Earth className="h-4 w-4" />
域名列表
{t('domain.management.name')}
</Link>
<Link
to="/access"
@@ -90,7 +93,7 @@ export default function Dashboard() {
)}
>
<Server className="h-4 w-4" />
授权管理
{t('menu.auth.management')}
</Link>
<Link
@@ -101,7 +104,7 @@ export default function Dashboard() {
)}
>
<History className="h-4 w-4" />
部署历史
{t('deployment.log.name')}
</Link>
</nav>
</div>
@@ -138,7 +141,7 @@ export default function Dashboard() {
)}
>
<Home className="h-5 w-5" />
控制面板
{t('dashboard')}
</Link>
<Link
to="/domains"
@@ -148,7 +151,7 @@ export default function Dashboard() {
)}
>
<Earth className="h-5 w-5" />
域名列表
{t('domain.management.name')}
</Link>
<Link
to="/access"
@@ -158,7 +161,7 @@ export default function Dashboard() {
)}
>
<Server className="h-5 w-5" />
授权管理
{t('menu.auth.management')}
</Link>
<Link
@@ -169,13 +172,14 @@ export default function Dashboard() {
)}
>
<History className="h-5 w-5" />
部署历史
{t('deployment.log.name')}
</Link>
</nav>
</SheetContent>
</Sheet>
<div className="w-full flex-1"></div>
<ThemeToggle />
<LocaleToggle />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@@ -188,15 +192,15 @@ export default function Dashboard() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>账户</DropdownMenuLabel>
<DropdownMenuLabel>{t('account')}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleSettingClick}>
偏好设置
{t('setting')}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleLogoutClick}>
退出
{t('logout')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
+7 -5
View File
@@ -4,11 +4,13 @@ import { KeyRound, Megaphone, ShieldCheck, UserRound } from "lucide-react";
import { useEffect, useState } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const SettingLayout = () => {
const location = useLocation();
const [tabValue, setTabValue] = useState("account");
const navigate = useNavigate();
const { t } = useTranslation();
useEffect(() => {
const pathname = location.pathname;
@@ -20,7 +22,7 @@ const SettingLayout = () => {
<div>
<Toaster />
<div className="text-muted-foreground border-b dark:border-stone-500 py-5">
偏好设置
{t("setting")}
</div>
<div className="w-full mt-5 p-0 md:p-3 flex justify-center">
<Tabs defaultValue="account" className="w-full" value={tabValue}>
@@ -33,7 +35,7 @@ const SettingLayout = () => {
className="px-5"
>
<UserRound size={14} />
<div className="ml-1">账户</div>
<div className="ml-1">{t("account")}</div>
</TabsTrigger>
<TabsTrigger
value="password"
@@ -43,7 +45,7 @@ const SettingLayout = () => {
className="px-5"
>
<KeyRound size={14} />
<div className="ml-1">密码</div>
<div className="ml-1">{t("password")}</div>
</TabsTrigger>
<TabsTrigger
@@ -54,7 +56,7 @@ const SettingLayout = () => {
className="px-5"
>
<Megaphone size={14} />
<div className="ml-1">消息推送</div>
<div className="ml-1">{t("setting.notify.menu")}</div>
</TabsTrigger>
<TabsTrigger
value="ssl-provider"
@@ -64,7 +66,7 @@ const SettingLayout = () => {
className="px-5"
>
<ShieldCheck size={14} />
<div className="ml-1">证书厂商</div>
<div className="ml-1">{t("ca")}</div>
</TabsTrigger>
</TabsList>
<TabsContent value={tabValue}>
+23 -20
View File
@@ -9,6 +9,7 @@ import { Access as AccessType, accessTypeMap } from "@/domain/access";
import { convertZulu2Beijing } from "@/lib/time";
import { useConfig } from "@/providers/config";
import { remove } from "@/repository/access";
import { t } from "i18next";
import { Key } from "lucide-react";
import { useLocation, useNavigate } from "react-router-dom";
@@ -46,11 +47,11 @@ const Access = () => {
return (
<div className="">
<div className="flex justify-between items-center">
<div className="text-muted-foreground">授权管理</div>
<div className="text-muted-foreground">{t("access.management")}</div>
{tab != "access_group" ? (
<AccessEdit trigger={<Button>添加授权</Button>} op="add" />
<AccessEdit trigger={<Button>{t("access.add")}</Button>} op="add" />
) : (
<AccessGroupEdit trigger={<Button>添加授权组</Button>} />
<AccessGroupEdit trigger={<Button>{t("access.group.add")}</Button>} />
)}
</div>
@@ -66,7 +67,7 @@ const Access = () => {
handleTabItemClick("access");
}}
>
授权管理
{t("access.management")}
</TabsTrigger>
<TabsTrigger
value="access_group"
@@ -74,7 +75,7 @@ const Access = () => {
handleTabItemClick("access_group");
}}
>
授权组管理
{t("access.group.management")}
</TabsTrigger>
</TabsList>
<TabsContent value="access">
@@ -85,10 +86,10 @@ const Access = () => {
</span>
<div className="text-center text-sm text-muted-foreground mt-3">
请添加授权开始部署证书吧。
{t("access.empty")}
</div>
<AccessEdit
trigger={<Button>添加授权</Button>}
trigger={<Button>{t("access.add")}</Button>}
op="add"
className="mt-3"
/>
@@ -96,15 +97,15 @@ const Access = () => {
) : (
<>
<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">服务商</div>
<div className="w-48">{t("name")}</div>
<div className="w-48">{t("access.type")}</div>
<div className="w-52">创建时间</div>
<div className="w-52">更新时间</div>
<div className="grow">操作</div>
<div className="w-60">{t("create.time")}</div>
<div className="w-60">{t("update.time")}</div>
<div className="grow">{t("operation")}</div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
授权列表
{t("access.list")}
</div>
{accesses
.filter((item) => {
@@ -124,22 +125,24 @@ const Access = () => {
src={accessTypeMap.get(access.configType)?.[1]}
className="w-6"
/>
<div>{accessTypeMap.get(access.configType)?.[0]}</div>
<div>
{t(accessTypeMap.get(access.configType)?.[0] || "")}
</div>
</div>
<div className="sm:w-52 w-full pt-1 sm:pt-0 flex items-center">
创建于{" "}
<div className="sm:w-60 w-full pt-1 sm:pt-0 flex items-center">
{t("created.in")}{" "}
{access.created && convertZulu2Beijing(access.created)}
</div>
<div className="sm:w-52 w-full pt-1 sm:pt-0 flex items-center">
更新于{" "}
<div className="sm:w-60 w-full pt-1 sm:pt-0 flex items-center">
{t("updated.in")}{" "}
{access.updated && convertZulu2Beijing(access.updated)}
</div>
<div className="flex items-center grow justify-start pt-1 sm:pt-0">
<AccessEdit
trigger={
<Button variant={"link"} className="p-0">
编辑
{t("edit")}
</Button>
}
op="edit"
@@ -153,7 +156,7 @@ const Access = () => {
handleDelete(access);
}}
>
删除
{t("delete")}
</Button>
</div>
</div>
+39 -21
View File
@@ -24,12 +24,14 @@ import {
} from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const Dashboard = () => {
const [statistic, setStatistic] = useState<Statistic>();
const [deployments, setDeployments] = useState<Deployment[]>();
const navigate = useNavigate();
const { t } = useTranslation();
useEffect(() => {
const fetchStatistic = async () => {
@@ -55,7 +57,7 @@ const Dashboard = () => {
return (
<div className="flex flex-col">
<div className="flex justify-between items-center">
<div className="text-muted-foreground">控制面板</div>
<div className="text-muted-foreground">{t('dashboard')}</div>
</div>
<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">
@@ -63,7 +65,9 @@ const Dashboard = () => {
<SquareSigma size={48} strokeWidth={1} className="text-blue-400" />
</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="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.total ? (
@@ -74,7 +78,9 @@ const Dashboard = () => {
0
)}
</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>
@@ -84,7 +90,9 @@ const Dashboard = () => {
<CalendarX2 size={48} strokeWidth={1} className="text-red-400" />
</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="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.expired ? (
@@ -95,7 +103,9 @@ const Dashboard = () => {
0
)}
</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>
@@ -109,7 +119,9 @@ const Dashboard = () => {
/>
</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="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.enabled ? (
@@ -120,7 +132,9 @@ const Dashboard = () => {
0
)}
</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>
@@ -130,7 +144,7 @@ const Dashboard = () => {
<Ban size={48} strokeWidth={1} className="text-gray-400" />
</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="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.disabled ? (
@@ -144,19 +158,23 @@ const Dashboard = () => {
0
)}
</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 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 ? (
<>
<Alert className="max-w-[40em] mt-10">
<AlertTitle>暂无数据</AlertTitle>
<AlertTitle>{t('no.data')}</AlertTitle>
<AlertDescription>
<div className="flex items-center mt-5">
<div>
@@ -164,7 +182,7 @@ const Dashboard = () => {
</div>
<div className="ml-2">
{" "}
你暂未创建任何部署,请先添加域名进行部署吧!
{t('deployment.log.empty')}
</div>
</div>
<div className="mt-2 flex justify-end">
@@ -173,7 +191,7 @@ const Dashboard = () => {
navigate("/edit");
}}
>
添加域名
{t('domain.add')}
</Button>
</div>
</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="w-48">域名</div>
<div className="w-48">{t('domain')}</div>
<div className="w-24">状态</div>
<div className="w-56">阶段</div>
<div className="w-56 sm:ml-2 text-center">最近执行时间</div>
<div className="w-24">{t('deployment.log.status')}</div>
<div className="w-56">{t('deployment.log.stage')}</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 className="sm:hidden flex text-sm text-muted-foreground">
部署历史
{t('deployment.log.name')}
</div>
{deployments?.map((deployment) => (
@@ -218,14 +236,14 @@ const Dashboard = () => {
<Sheet>
<SheetTrigger asChild>
<Button variant={"link"} className="p-0">
日志
{t('deployment.log.detail.button.text')}
</Button>
</SheetTrigger>
<SheetContent className="sm:max-w-5xl">
<SheetHeader>
<SheetTitle>
{deployment.expand.domain?.domain}-{deployment.id}
部署详情
{t('deployment.log.detail')}
</SheetTitle>
</SheetHeader>
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">
+39 -37
View File
@@ -38,6 +38,7 @@ import EmailsEdit from "@/components/certimate/EmailsEdit";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { EmailsSetting } from "@/domain/settings";
import { useTranslation } from "react-i18next";
const Edit = () => {
const {
@@ -47,6 +48,7 @@ const Edit = () => {
const [domain, setDomain] = useState<Domain>();
const location = useLocation();
const { t } = useTranslation();
const [tab, setTab] = useState<"base" | "advance">("base");
@@ -69,15 +71,15 @@ const Edit = () => {
const formSchema = z.object({
id: z.string().optional(),
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: "请输入正确的域名",
message: 'domain.not.empty.verify.message',
}),
email: z.string().email().optional(),
email: z.string().email('email.valid.message').optional(),
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
message: "请选择DNS服务商授权配置",
message: 'domain.management.edit.dns.access.not.empty.message',
}),
targetAccess: z.string().optional(),
targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, {
message: "请选择部署服务类型",
message: 'domain.management.edit.target.type.not.empty.message',
}),
variables: z.string().optional(),
group: z.string().optional(),
@@ -138,11 +140,11 @@ const Edit = () => {
if (group == "" && targetAccess == "") {
form.setError("group", {
type: "manual",
message: "部署授权和部署授权组至少选一个",
message: 'domain.management.edit.target.access.verify.msg',
});
form.setError("targetAccess", {
type: "manual",
message: "部署授权和部署授权组至少选一个",
message: 'domain.management.edit.target.access.verify.msg',
});
return;
}
@@ -162,13 +164,13 @@ const Edit = () => {
try {
await save(req);
let description = "域名编辑成功";
let description = t('domain.management.edit.succeed.tips');
if (req.id == "") {
description = "域名添加成功";
description = t('domain.management.add.succeed.tips');
}
toast({
title: "成功",
title: t('succeed'),
description,
});
navigate("/domains");
@@ -193,7 +195,7 @@ const Edit = () => {
<div className="">
<Toaster />
<div className=" h-5 text-muted-foreground">
{domain?.id ? "编辑" : "新增"}域名
{domain?.id ? t('domain.edit') : t('domain.add')}
</div>
<div className="mt-5 flex w-full justify-center md:space-x-10 flex-col md:flex-row">
<div className="w-full md:w-[200px] text-muted-foreground space-x-3 md:space-y-3 flex-row md:flex-col flex">
@@ -206,7 +208,7 @@ const Edit = () => {
setTab("base");
}}
>
基础设置
{t('basic.setting')}
</div>
<div
className={cn(
@@ -217,7 +219,7 @@ const Edit = () => {
setTab("advance");
}}
>
高级设置
{t('advanced.setting')}
</div>
</div>
@@ -232,9 +234,9 @@ const Edit = () => {
name="domain"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel>域名</FormLabel>
<FormLabel>{t('domain')}</FormLabel>
<FormControl>
<Input placeholder="请输入域名" {...field} />
<Input placeholder={t('domain.not.empty.verify.message')} {...field} />
</FormControl>
<FormMessage />
@@ -247,12 +249,12 @@ const Edit = () => {
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel className="flex w-full justify-between">
<div>Email(申请证书需要提供邮箱)</div>
<div>{t('email') + t('domain.management.edit.email.description')}</div>
<EmailsEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
新增
{t('add')}
</div>
}
/>
@@ -266,11 +268,11 @@ const Edit = () => {
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择邮箱" />
<SelectValue placeholder={t('domain.management.edit.email.not.empty.message')} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>邮箱列表</SelectLabel>
<SelectLabel>{t('email.list')}</SelectLabel>
{(emails.content as EmailsSetting).emails.map(
(item) => (
<SelectItem key={item} value={item}>
@@ -293,12 +295,12 @@ const Edit = () => {
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel className="flex w-full justify-between">
<div>DNS 服务商授权配置</div>
<div>{t('domain.management.edit.dns.access.label')}</div>
<AccessEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
新增
{t('add')}
</div>
}
op="add"
@@ -313,11 +315,11 @@ const Edit = () => {
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择授权配置" />
<SelectValue placeholder={t('domain.management.edit.access.not.empty.message')} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>服务商授权配置</SelectLabel>
<SelectLabel>{t('domain.management.edit.access.label')}</SelectLabel>
{accesses
.filter((item) => item.usage != "deploy")
.map((item) => (
@@ -349,7 +351,7 @@ const Edit = () => {
name="targetType"
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel>部署服务类型</FormLabel>
<FormLabel>{t('domain.management.edit.target.type')}</FormLabel>
<FormControl>
<Select
{...field}
@@ -359,11 +361,11 @@ const Edit = () => {
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择部署服务类型" />
<SelectValue placeholder={t('domain.management.edit.target.type.not.empty.message')} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>部署服务类型</SelectLabel>
<SelectLabel>{t('domain.management.edit.target.type')}</SelectLabel>
{targetTypeKeys.map((key) => (
<SelectItem key={key} value={key}>
<div className="flex items-center space-x-2">
@@ -371,7 +373,7 @@ const Edit = () => {
className="w-6"
src={targetTypeMap.get(key)?.[1]}
/>
<div>{targetTypeMap.get(key)?.[0]}</div>
<div>{t(targetTypeMap.get(key)?.[0] || '')}</div>
</div>
</SelectItem>
))}
@@ -390,12 +392,12 @@ const Edit = () => {
render={({ field }) => (
<FormItem hidden={tab != "base"}>
<FormLabel className="w-full flex justify-between">
<div>部署服务商授权配置</div>
<div>{t('domain.management.edit.target.access')}</div>
<AccessEdit
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
新增
{t('add')}
</div>
}
op="add"
@@ -409,12 +411,12 @@ const Edit = () => {
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择授权配置" />
<SelectValue placeholder={t('domain.management.edit.target.access.not.empty.message')} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>
服务商授权配置{form.getValues().targetAccess}
{t('domain.management.edit.target.access.content.label')} {form.getValues().targetAccess}
</SelectLabel>
<SelectItem value="emptyId">
<div className="flex items-center space-x-2">
@@ -451,7 +453,7 @@ const Edit = () => {
<FormItem hidden={tab != "advance" || targetType != "ssh"}>
<FormLabel className="w-full flex justify-between">
<div>
部署配置组(用于将一个域名证书部署到多个 ssh 主机)
{t('domain.management.edit.group.label')}
</div>
</FormLabel>
<FormControl>
@@ -464,7 +466,7 @@ const Edit = () => {
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择分组" />
<SelectValue placeholder={t('domain.management.edit.group.not.empty.message')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="emptyId">
@@ -509,10 +511,10 @@ const Edit = () => {
name="variables"
render={({ field }) => (
<FormItem hidden={tab != "advance"}>
<FormLabel>变量</FormLabel>
<FormLabel>{t('variables')}</FormLabel>
<FormControl>
<Textarea
placeholder={`可在SSH部署中使用,形如:\nkey=val;\nkey2=val2;`}
placeholder={t('domain.management.edit.variables.placeholder')}
{...field}
className="placeholder:whitespace-pre-wrap"
/>
@@ -528,10 +530,10 @@ const Edit = () => {
name="nameservers"
render={({ field }) => (
<FormItem hidden={tab != "advance"}>
<FormLabel>域名服务器</FormLabel>
<FormLabel>{t('dns')}</FormLabel>
<FormControl>
<Textarea
placeholder={`自定义域名服务器,多个用分号隔开,如:\n8.8.8.8;\n8.8.4.4;`}
placeholder={t('domain.management.edit.dns.placeholder')}
{...field}
className="placeholder:whitespace-pre-wrap"
/>
@@ -543,7 +545,7 @@ const Edit = () => {
/>
<div className="flex justify-end">
<Button type="submit">保存</Button>
<Button type="submit">{t('save')}</Button>
</div>
</form>
</Form>
+38 -35
View File
@@ -35,11 +35,13 @@ import { TooltipContent, TooltipProvider } from "@radix-ui/react-tooltip";
import { Earth } from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useTranslation, Trans } from "react-i18next";
const Home = () => {
const toast = useToast();
const navigate = useNavigate();
const { t } = useTranslation()
const location = useLocation();
const query = new URLSearchParams(location.search);
@@ -127,23 +129,22 @@ const Home = () => {
await save(domain);
toast.toast({
title: "操作成功",
description: "已发起部署,请稍后查看部署日志。",
title: t('operation.succeed'),
description: t('domain.management.start.deploy.succeed.tips'),
});
} catch (e) {
toast.toast({
title: "执行失败",
title: t('domain.management.execution.failed'),
description: (
<>
执行失败,请查看
// 这里的 text 只是占位作用,实际文案在 src/i18n/locales/[lang].json
<Trans i18nKey="domain.management.execution.failed.tips">
text1
<Link
to={`/history?domain=${domain.id}`}
className="underline text-blue-500"
>
部署日志
</Link>
查看详情。
</>
>text2</Link>
text3
</Trans>
),
variant: "destructive",
});
@@ -175,8 +176,10 @@ const Home = () => {
<div className="">
<Toaster />
<div className="flex justify-between items-center">
<div className="text-muted-foreground">域名列表</div>
<Button onClick={handleCreateClick}>新增域名</Button>
<div className="text-muted-foreground">{t('domain.management.name')}</div>
<Button onClick={handleCreateClick}>
{t('domain.add')}
</Button>
</div>
{!domains.length ? (
@@ -187,26 +190,26 @@ const Home = () => {
</span>
<div className="text-center text-sm text-muted-foreground mt-3">
请添加域名开始部署证书吧。
{t('domain.management.empty')}
</div>
<Button onClick={handleCreateClick} className="mt-3">
添加域名
{t('domain.add')}
</Button>
</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="w-36">域名</div>
<div className="w-40">有效期限</div>
<div className="w-32">最近执行状态</div>
<div className="w-64">最近执行阶段</div>
<div className="w-40 sm:ml-2">最近执行时间</div>
<div className="w-24">是否启用</div>
<div className="grow">操作</div>
<div className="w-36">{t('domain')}</div>
<div className="w-40">{t('domain.management.expiry.date')}</div>
<div className="w-32">{t('domain.management.last.execution.status')}</div>
<div className="w-64">{t('domain.management.last.execution.stage')}</div>
<div className="w-40 sm:ml-2">{t('domain.management.last.execution.time')}</div>
<div className="w-24">{t('domain.management.enable')}</div>
<div className="grow">{t('operation')}</div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
域名
{t('domain')}
</div>
{domains.map((domain) => (
@@ -221,8 +224,8 @@ const Home = () => {
<div>
{domain.expiredAt ? (
<>
<div>有效期90天</div>
<div>{getDate(domain.expiredAt)}到期</div>
<div>{t('domain.management.expiry.date1', { date: 90 })}</div>
<div>{t('domain.management.expiry.date2', { date: getDate(domain.expiredAt) })}</div>
</>
) : (
"---"
@@ -266,7 +269,7 @@ const Home = () => {
</TooltipTrigger>
<TooltipContent>
<div className="border rounded-sm px-3 bg-background text-muted-foreground text-xs">
{domain.enabled ? "禁用" : "启用"}
{domain.enabled ? t('disable') : t('enable')}
</div>
</TooltipContent>
</Tooltip>
@@ -278,7 +281,7 @@ const Home = () => {
className="p-0"
onClick={() => handleHistoryClick(domain.id)}
>
部署历史
{t('deployment.log.name')}
</Button>
<Show when={domain.enabled ? true : false}>
<Separator orientation="vertical" className="h-4 mx-2" />
@@ -287,7 +290,7 @@ const Home = () => {
className="p-0"
onClick={() => handleRightNowClick(domain)}
>
立即部署
{t('domain.management.start.deploying')}
</Button>
</Show>
@@ -304,7 +307,7 @@ const Home = () => {
className="p-0"
onClick={() => handleForceClick(domain)}
>
强行部署
{t('domain.management.forced.deployment')}
</Button>
</Show>
@@ -315,7 +318,7 @@ const Home = () => {
className="p-0"
onClick={() => handleDownloadClick(domain)}
>
下载
{t('download')}
</Button>
</Show>
@@ -325,24 +328,24 @@ const Home = () => {
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"link"} className="p-0">
删除
{t('delete')}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>删除域名</AlertDialogTitle>
<AlertDialogTitle>{t('domain.delete')}</AlertDialogTitle>
<AlertDialogDescription>
确定要删除域名吗?
{t('domain.management.delete.confirm')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleDeleteClick(domain.id);
}}
>
确认
{t('confirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -354,7 +357,7 @@ const Home = () => {
className="p-0"
onClick={() => handleEditClick(domain.id)}
>
编辑
{t('edit')}
</Button>
</>
)}
+14 -12
View File
@@ -17,11 +17,13 @@ import { list } from "@/repository/deployment";
import { Smile } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
const History = () => {
const navigate = useNavigate();
const [deployments, setDeployments] = useState<Deployment[]>();
const [searchParams] = useSearchParams();
const { t } = useTranslation();
const domain = searchParams.get("domain");
useEffect(() => {
@@ -38,11 +40,11 @@ const History = () => {
return (
<ScrollArea className="h-[80vh] overflow-hidden">
<div className="text-muted-foreground">部署历史</div>
<div className="text-muted-foreground">{t('deployment.log.name')}</div>
{!deployments?.length ? (
<>
<Alert className="max-w-[40em] mx-auto mt-20">
<AlertTitle>暂无数据</AlertTitle>
<AlertTitle>{t('no.data')}</AlertTitle>
<AlertDescription>
<div className="flex items-center mt-5">
<div>
@@ -50,7 +52,7 @@ const History = () => {
</div>
<div className="ml-2">
{" "}
你暂未创建任何部署,请先添加域名进行部署吧!
{t('deployment.log.empty')}
</div>
</div>
<div className="mt-2 flex justify-end">
@@ -59,7 +61,7 @@ const History = () => {
navigate("/");
}}
>
添加域名
{t('domain.add')}
</Button>
</div>
</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="w-48">域名</div>
<div className="w-48">{t('domain')}</div>
<div className="w-24">状态</div>
<div className="w-56">阶段</div>
<div className="w-56 sm:ml-2 text-center">最近执行时间</div>
<div className="w-24">{t('deployment.log.status')}</div>
<div className="w-56">{t('deployment.log.stage')}</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 className="sm:hidden flex text-sm text-muted-foreground">
部署历史
{t('deployment.log.name')}
</div>
{deployments?.map((deployment) => (
@@ -104,14 +106,14 @@ const History = () => {
<Sheet>
<SheetTrigger asChild>
<Button variant={"link"} className="p-0">
日志
{t('deployment.log.detail.button.text')}
</Button>
</SheetTrigger>
<SheetContent className="sm:max-w-5xl">
<SheetHeader>
<SheetTitle>
{deployment.expand.domain?.domain}-{deployment.id}
部署详情
{t('deployment.log.detail')}
</SheetTitle>
</SheetHeader>
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">
+12 -8
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 {
Form,
@@ -11,20 +16,19 @@ import { Input } from "@/components/ui/input";
import { getErrMessage } from "@/lib/error";
import { getPb } from "@/repository/api";
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({
username: z.string().email({
message: "请输入正确的邮箱地址",
message: "login.username.no.empty.message",
}),
password: z.string().min(10, {
message: "密码至少10个字符",
message: "login.password.length.message",
}),
});
const Login = () => {
const { t } = useTranslation()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
@@ -61,7 +65,7 @@ const Login = () => {
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>用户名</FormLabel>
<FormLabel>{t('username')}</FormLabel>
<FormControl>
<Input placeholder="email" {...field} />
</FormControl>
@@ -76,7 +80,7 @@ const Login = () => {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>密码</FormLabel>
<FormLabel>{t('password')}</FormLabel>
<FormControl>
<Input placeholder="password" {...field} type="password" />
</FormControl>
@@ -86,7 +90,7 @@ const Login = () => {
)}
/>
<div className="flex justify-end">
<Button type="submit">登录</Button>
<Button type="submit">{t('login.submit')}</Button>
</div>
</form>
</Form>
+10 -8
View File
@@ -15,16 +15,18 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { z } from "zod";
const formSchema = z.object({
email: z.string().email("请输入正确的邮箱"),
email: z.string().email("setting.account.email.valid.message"),
});
const Account = () => {
const { toast } = useToast();
const navigate = useNavigate();
const { t } = useTranslation();
const [changed, setChanged] = useState(false);
@@ -43,8 +45,8 @@ const Account = () => {
getPb().authStore.clear();
toast({
title: "修改账户邮箱功",
description: "请重新登录",
title: t("setting.account.email.change.succeed"),
description: t("setting.account.log.back.in"),
});
setTimeout(() => {
navigate("/login");
@@ -52,7 +54,7 @@ const Account = () => {
} catch (e) {
const message = getErrMessage(e);
toast({
title: "修改账户邮箱失败",
title: t("setting.account.email.change.failed"),
description: message,
variant: "destructive",
});
@@ -72,10 +74,10 @@ const Account = () => {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>邮箱</FormLabel>
<FormLabel>{t('email')}</FormLabel>
<FormControl>
<Input
placeholder="请输入邮箱"
placeholder={t('setting.email.placeholder')}
{...field}
type="email"
onChange={(e) => {
@@ -92,10 +94,10 @@ const Account = () => {
<div className="flex justify-end">
{changed ? (
<Button type="submit">确认修改</Button>
<Button type="submit">{t('setting.submit')}</Button>
) : (
<Button type="submit" disabled variant={"secondary"}>
确认修改
{t('setting.submit')}
</Button>
)}
</div>
+7 -4
View File
@@ -9,15 +9,18 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { NotifyProvider } from "@/providers/notify";
import { useTranslation } from "react-i18next";
const Notify = () => {
const { t } = useTranslation();
return (
<>
<NotifyProvider>
<div className="border rounded-sm p-5 shadow-lg">
<Accordion type={"multiple"} className="dark:text-stone-200">
<AccordionItem value="item-1" className="dark:border-stone-200">
<AccordionTrigger>模板</AccordionTrigger>
<AccordionTrigger>{t('template')}</AccordionTrigger>
<AccordionContent>
<NotifyTemplate />
</AccordionContent>
@@ -27,21 +30,21 @@ const Notify = () => {
<div className="border rounded-md p-5 mt-7 shadow-lg">
<Accordion type={"single"} className="dark:text-stone-200">
<AccordionItem value="item-2" className="dark:border-stone-200">
<AccordionTrigger>钉钉</AccordionTrigger>
<AccordionTrigger>{t('ding.talk')}</AccordionTrigger>
<AccordionContent>
<DingTalk />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-4" className="dark:border-stone-200">
<AccordionTrigger>Telegram</AccordionTrigger>
<AccordionTrigger>{t('telegram')}</AccordionTrigger>
<AccordionContent>
<Telegram />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-5" className="dark:border-stone-200">
<AccordionTrigger>Webhook</AccordionTrigger>
<AccordionTrigger>{t('webhook')}</AccordionTrigger>
<AccordionContent>
<Webhook />
</AccordionContent>
+14 -12
View File
@@ -14,29 +14,31 @@ import { getPb } from "@/repository/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { z } from "zod";
const formSchema = z
.object({
oldPassword: z.string().min(10, {
message: "密码至少10个字符",
message: "setting.password.length.message",
}),
newPassword: z.string().min(10, {
message: "密码至少10个字符",
message: "setting.password.length.message",
}),
confirmPassword: z.string().min(10, {
message: "密码至少10个字符",
message: "setting.password.length.message",
}),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: "两次密码不一致",
message: "setting.password.not.match",
path: ["confirmPassword"],
});
const Password = () => {
const { toast } = useToast();
const navigate = useNavigate();
const { t } = useTranslation();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -66,8 +68,8 @@ const Password = () => {
getPb().authStore.clear();
toast({
title: "修改密码成功",
description: "请重新登录",
title: t('setting.password.change.succeed'),
description: t("setting.account.log.back.in"),
});
setTimeout(() => {
navigate("/login");
@@ -75,7 +77,7 @@ const Password = () => {
} catch (e) {
const message = getErrMessage(e);
toast({
title: "修改密码失败",
title: t('setting.password.change.failed'),
description: message,
variant: "destructive",
});
@@ -95,9 +97,9 @@ const Password = () => {
name="oldPassword"
render={({ field }) => (
<FormItem>
<FormLabel>当前密码</FormLabel>
<FormLabel>{t('setting.password.current.password')}</FormLabel>
<FormControl>
<Input placeholder="当前密码" {...field} type="password" />
<Input placeholder={t('setting.password.current.password')} {...field} type="password" />
</FormControl>
<FormMessage />
@@ -110,7 +112,7 @@ const Password = () => {
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>新密码</FormLabel>
<FormLabel>{t('setting.password.new.password')}</FormLabel>
<FormControl>
<Input
placeholder="newPassword"
@@ -129,7 +131,7 @@ const Password = () => {
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>确认密码</FormLabel>
<FormLabel>{t('setting.password.confirm.password')}</FormLabel>
<FormControl>
<Input
placeholder="confirmPassword"
@@ -143,7 +145,7 @@ const Password = () => {
)}
/>
<div className="flex justify-end">
<Button type="submit">确认修改</Button>
<Button type="submit">{t('setting.submit')}</Button>
</div>
</form>
</Form>
+20 -17
View File
@@ -26,18 +26,21 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
const formSchema = z.object({
provider: z.enum(["letsencrypt", "zerossl"], {
message: "请选择SSL提供商",
}),
eabKid: z.string().optional(),
eabHmacKey: z.string().optional(),
});
const SSLProvider = () => {
const { t } = useTranslation();
const formSchema = z.object({
provider: z.enum(["letsencrypt", "zerossl"], {
message: t("setting.ca.not.empty"),
}),
eabKid: z.string().optional(),
eabHmacKey: z.string().optional(),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
@@ -86,12 +89,12 @@ const SSLProvider = () => {
if (values.provider === "zerossl") {
if (!values.eabKid) {
form.setError("eabKid", {
message: "请输入EAB_KID和EAB_HMAC_KEY",
message: t("setting.ca.eab_kid_hmac_key.not.empty"),
});
}
if (!values.eabHmacKey) {
form.setError("eabHmacKey", {
message: "请输入EAB_KID和EAB_HMAC_KEY",
message: t("setting.ca.eab_kid_hmac_key.not.empty"),
});
}
if (!values.eabKid || !values.eabHmacKey) {
@@ -117,13 +120,13 @@ const SSLProvider = () => {
try {
await update(setting);
toast({
title: "修改成功",
description: "修改成功",
title: t("update.succeed"),
description: t("update.succeed"),
});
} catch (e) {
const message = getErrMessage(e);
toast({
title: "修改失败",
title: t("update.failed"),
description: message,
variant: "destructive",
});
@@ -143,7 +146,7 @@ const SSLProvider = () => {
name="provider"
render={({ field }) => (
<FormItem>
<FormLabel>证书厂商</FormLabel>
<FormLabel>{t("ca")}</FormLabel>
<FormControl>
<RadioGroup
{...field}
@@ -199,7 +202,7 @@ const SSLProvider = () => {
<FormLabel>EAB_KID</FormLabel>
<FormControl>
<Input
placeholder="请输入EAB_KID"
placeholder={t("setting.ca.eab_kid.not.empty")}
{...field}
type="text"
/>
@@ -218,7 +221,7 @@ const SSLProvider = () => {
<FormLabel>EAB_HMAC_KEY</FormLabel>
<FormControl>
<Input
placeholder="请输入EAB_HMAC_KEY"
placeholder={t("setting.ca.eab_hmac_key.not.empty")}
{...field}
type="text"
/>
@@ -235,7 +238,7 @@ const SSLProvider = () => {
/>
<div className="flex justify-end">
<Button type="submit">确认修改</Button>
<Button type="submit">{t("setting.submit")}</Button>
</div>
</form>
</Form>