mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-08 05:29:51 +00:00
Merge branch 'l123wx-i18n'
This commit is contained in:
commit
6b85b4a0c9
313
ui/dist/assets/index-CQVPrK_Y.js
vendored
313
ui/dist/assets/index-CQVPrK_Y.js
vendored
File diff suppressed because one or more lines are too long
1
ui/dist/assets/index-Djc_JtNf.css
vendored
1
ui/dist/assets/index-Djc_JtNf.css
vendored
File diff suppressed because one or more lines are too long
1
ui/dist/assets/index-I--T0qY3.css
vendored
Normal file
1
ui/dist/assets/index-I--T0qY3.css
vendored
Normal file
File diff suppressed because one or more lines are too long
322
ui/dist/assets/index-TzNEc_kS.js
vendored
Normal file
322
ui/dist/assets/index-TzNEc_kS.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
ui/dist/index.html
vendored
4
ui/dist/index.html
vendored
@ -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
ui/package-lock.json
generated
141
ui/package-lock.json
generated
@ -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",
|
||||
|
@ -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
ui/src/components/LocaleToggle.tsx
Normal file
33
ui/src/components/LocaleToggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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 +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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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 = ({
|
||||
|
@ -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";
|
||||
|
@ -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
ui/src/i18n/index.ts
Normal file
22
ui/src/i18n/index.ts
Normal 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
ui/src/i18n/locales/en.json
Normal file
210
ui/src/i18n/locales/en.json
Normal 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
ui/src/i18n/locales/index.ts
Normal file
17
ui/src/i18n/locales/index.ts
Normal 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
ui/src/i18n/locales/zh.json
Normal file
210
ui/src/i18n/locales/zh.json
Normal 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": "加签的签名"
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
|
@ -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]">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
@ -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]">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,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>
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user