mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-13 07:59:52 +00:00
support email update
This commit is contained in:
parent
7f6549bdf3
commit
b912c5e688
49
ui/src/components/certimate/DeployState.tsx
Normal file
49
ui/src/components/certimate/DeployState.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { Deployment } from "@/domain/deployment";
|
||||||
|
import { CircleCheck, CircleX } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "../ui/tooltip";
|
||||||
|
|
||||||
|
type DeployStateProps = {
|
||||||
|
deployment: Deployment;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeployState = ({ deployment }: DeployStateProps) => {
|
||||||
|
// 获取指定阶段的错误信息
|
||||||
|
const error = (state: "check" | "apply" | "deploy") => {
|
||||||
|
if (!deployment.log[state]) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return deployment.log[state][deployment.log[state].length - 1].error;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{deployment.phase === "deploy" && deployment.phaseSuccess ? (
|
||||||
|
<CircleCheck size={16} className="text-green-700" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{error(deployment.phase).length ? (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild className="cursor-pointer">
|
||||||
|
<CircleX size={16} className="text-red-700" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-[35em]">
|
||||||
|
{error(deployment.phase)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : (
|
||||||
|
<CircleX size={16} className="text-red-700" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeployState;
|
@ -46,12 +46,12 @@ export default function Dashboard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSettingClick = () => {
|
const handleSettingClick = () => {
|
||||||
navigate("/setting/password");
|
navigate("/setting/account");
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<div className="grid min-h-screen w-full md:grid-cols-[220px_1fr] lg:grid-cols-[280px_1fr]">
|
<div className="grid min-h-screen w-full md:grid-cols-[180px_1fr] lg:grid-cols-[200px_1fr] 2xl:md:grid-cols-[280px_1fr] ">
|
||||||
<div className="hidden border-r dark:border-stone-500 bg-muted/40 md:block">
|
<div className="hidden border-r dark:border-stone-500 bg-muted/40 md:block">
|
||||||
<div className="flex h-full max-h-screen flex-col gap-2">
|
<div className="flex h-full max-h-screen flex-col gap-2">
|
||||||
<div className="flex h-14 items-center border-b dark:border-stone-500 px-4 lg:h-[60px] lg:px-6">
|
<div className="flex h-14 items-center border-b dark:border-stone-500 px-4 lg:h-[60px] lg:px-6">
|
||||||
|
@ -1,20 +1,57 @@
|
|||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { Outlet } from "react-router-dom";
|
import { KeyRound, UserRound } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
const SettingLayout = () => {
|
const SettingLayout = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const [tabValue, setTabValue] = useState("account");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const pathname = location.pathname;
|
||||||
|
const tabValue = pathname.split("/")[2];
|
||||||
|
setTabValue(tabValue);
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<div className="text-muted-foreground border-b dark:border-stone-500 py-5">
|
<div className="text-muted-foreground border-b dark:border-stone-500 py-5">
|
||||||
设置密码
|
偏好设置
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full sm:w-[35em] mt-10 flex flex-col p-3 mx-auto">
|
<div className="w-full mt-5 p-3 flex justify-center">
|
||||||
{/* <div className="text-muted-foreground">
|
<Tabs defaultValue="account" className="" value={tabValue}>
|
||||||
<span className="transition-all text-sm bg-gray-400 px-3 py-1 rounded-sm text-white cursor-pointer">
|
<TabsList>
|
||||||
密码
|
<TabsTrigger
|
||||||
</span>
|
value="account"
|
||||||
</div> */}
|
onClick={() => {
|
||||||
<Outlet />
|
navigate("/setting/account");
|
||||||
|
}}
|
||||||
|
className="px-5"
|
||||||
|
>
|
||||||
|
<UserRound size={14} />
|
||||||
|
<div className="ml-1">账户</div>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="password"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("/setting/password");
|
||||||
|
}}
|
||||||
|
className="px-5"
|
||||||
|
>
|
||||||
|
<KeyRound size={14} />
|
||||||
|
<div className="ml-1">密码</div>
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value={tabValue}>
|
||||||
|
<div className="mt-5 w-full md:w-[45em]">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import DeployProgress from "@/components/certimate/DeployProgress";
|
import DeployProgress from "@/components/certimate/DeployProgress";
|
||||||
|
import DeployState from "@/components/certimate/DeployState";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@ -204,11 +205,7 @@ const Dashboard = () => {
|
|||||||
{deployment.expand.domain?.domain}
|
{deployment.expand.domain?.domain}
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
|
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
|
||||||
{deployment.phase === "deploy" && deployment.phaseSuccess ? (
|
<DeployState deployment={deployment} />
|
||||||
<CircleCheck size={16} className="text-green-700" />
|
|
||||||
) : (
|
|
||||||
<CircleX size={16} className="text-red-700" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center">
|
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center">
|
||||||
<DeployProgress
|
<DeployProgress
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import DeployProgress from "@/components/certimate/DeployProgress";
|
import DeployProgress from "@/components/certimate/DeployProgress";
|
||||||
|
import DeployState from "@/components/certimate/DeployState";
|
||||||
import XPagination from "@/components/certimate/XPagination";
|
import XPagination from "@/components/certimate/XPagination";
|
||||||
import Show from "@/components/Show";
|
import Show from "@/components/Show";
|
||||||
import {
|
import {
|
||||||
@ -196,12 +197,12 @@ const Home = () => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
|
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
|
||||||
<div className="w-40">域名</div>
|
<div className="w-36">域名</div>
|
||||||
<div className="w-48">有效期限</div>
|
<div className="w-40">有效期限</div>
|
||||||
<div className="w-32">最近执行状态</div>
|
<div className="w-32">最近执行状态</div>
|
||||||
<div className="w-64">最近执行阶段</div>
|
<div className="w-64">最近执行阶段</div>
|
||||||
<div className="w-40 sm:ml-2">最近执行时间</div>
|
<div className="w-40 sm:ml-2">最近执行时间</div>
|
||||||
<div className="w-32">是否启用</div>
|
<div className="w-24">是否启用</div>
|
||||||
<div className="grow">操作</div>
|
<div className="grow">操作</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:hidden flex text-sm text-muted-foreground">
|
<div className="sm:hidden flex text-sm text-muted-foreground">
|
||||||
@ -213,10 +214,10 @@ const Home = () => {
|
|||||||
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
|
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
|
||||||
key={domain.id}
|
key={domain.id}
|
||||||
>
|
>
|
||||||
<div className="sm:w-40 w-full pt-1 sm:pt-0 flex items-center">
|
<div className="sm:w-36 w-full pt-1 sm:pt-0 flex items-center">
|
||||||
{domain.domain}
|
{domain.domain}
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex items-center">
|
<div className="sm:w-40 w-full pt-1 sm:pt-0 flex items-center">
|
||||||
<div>
|
<div>
|
||||||
{domain.expiredAt ? (
|
{domain.expiredAt ? (
|
||||||
<>
|
<>
|
||||||
@ -231,12 +232,7 @@ const Home = () => {
|
|||||||
<div className="sm:w-32 w-full pt-1 sm:pt-0 flex items-center">
|
<div className="sm:w-32 w-full pt-1 sm:pt-0 flex items-center">
|
||||||
{domain.lastDeployedAt && domain.expand?.lastDeployment ? (
|
{domain.lastDeployedAt && domain.expand?.lastDeployment ? (
|
||||||
<>
|
<>
|
||||||
{domain.expand.lastDeployment?.phase === "deploy" &&
|
<DeployState deployment={domain.expand.lastDeployment} />
|
||||||
domain.expand.lastDeployment?.phaseSuccess ? (
|
|
||||||
<CircleCheck size={16} className="text-green-700" />
|
|
||||||
) : (
|
|
||||||
<CircleX size={16} className="text-red-700" />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"---"
|
"---"
|
||||||
@ -257,7 +253,7 @@ const Home = () => {
|
|||||||
? convertZulu2Beijing(domain.lastDeployedAt)
|
? convertZulu2Beijing(domain.lastDeployedAt)
|
||||||
: "---"}
|
: "---"}
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:w-32 flex items-center">
|
<div className="sm:w-24 flex items-center">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import DeployProgress from "@/components/certimate/DeployProgress";
|
import DeployProgress from "@/components/certimate/DeployProgress";
|
||||||
|
import DeployState from "@/components/certimate/DeployState";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
@ -88,11 +89,7 @@ const History = () => {
|
|||||||
{deployment.expand.domain?.domain}
|
{deployment.expand.domain?.domain}
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
|
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
|
||||||
{deployment.phase === "deploy" && deployment.phaseSuccess ? (
|
<DeployState deployment={deployment} />
|
||||||
<CircleCheck size={16} className="text-green-700" />
|
|
||||||
) : (
|
|
||||||
<CircleX size={16} className="text-red-700" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center">
|
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center">
|
||||||
<DeployProgress
|
<DeployProgress
|
||||||
|
109
ui/src/pages/setting/Account.tsx
Normal file
109
ui/src/pages/setting/Account.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { getErrMessage } from "@/lib/error";
|
||||||
|
import { getPb } from "@/repository/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
email: z.string().email("请输入正确的邮箱"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Account = () => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [changed, setChanged] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: getPb().authStore.model?.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
|
try {
|
||||||
|
await getPb().admins.update(getPb().authStore.model?.id, {
|
||||||
|
email: values.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
getPb().authStore.clear();
|
||||||
|
toast({
|
||||||
|
title: "修改账户邮箱功",
|
||||||
|
description: "请重新登录",
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate("/login");
|
||||||
|
}, 500);
|
||||||
|
} catch (e) {
|
||||||
|
const message = getErrMessage(e);
|
||||||
|
toast({
|
||||||
|
title: "修改账户邮箱失败",
|
||||||
|
description: message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full md:max-w-[35em]">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-8 dark:text-stone-200"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>邮箱</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
{...field}
|
||||||
|
type="email"
|
||||||
|
onChange={(e) => {
|
||||||
|
setChanged(true);
|
||||||
|
form.setValue("email", e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
{changed ? (
|
||||||
|
<Button type="submit">确认修改</Button>
|
||||||
|
) : (
|
||||||
|
<Button type="submit" disabled variant={"secondary"}>
|
||||||
|
确认修改
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Account;
|
@ -84,64 +84,70 @@ const Password = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form {...form}>
|
<div className="w-full md:max-w-[35em]">
|
||||||
<form
|
<Form {...form}>
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
<form
|
||||||
className="space-y-8 dark:text-stone-200"
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
>
|
className="space-y-8 dark:text-stone-200"
|
||||||
<FormField
|
>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="oldPassword"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="oldPassword"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>当前密码</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>当前密码</FormLabel>
|
||||||
<Input placeholder="当前密码" {...field} type="password" />
|
<FormControl>
|
||||||
</FormControl>
|
<Input placeholder="当前密码" {...field} type="password" />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="newPassword"
|
name="newPassword"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>新密码</FormLabel>
|
<FormLabel>新密码</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="newPassword" {...field} type="password" />
|
<Input
|
||||||
</FormControl>
|
placeholder="newPassword"
|
||||||
|
{...field}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>确认密码</FormLabel>
|
<FormLabel>确认密码</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="confirmPassword"
|
placeholder="confirmPassword"
|
||||||
{...field}
|
{...field}
|
||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit">确认修改</Button>
|
<Button type="submit">确认修改</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,7 @@ import LoginLayout from "./pages/LoginLayout";
|
|||||||
import Password from "./pages/setting/Password";
|
import Password from "./pages/setting/Password";
|
||||||
import SettingLayout from "./pages/SettingLayout";
|
import SettingLayout from "./pages/SettingLayout";
|
||||||
import Dashboard from "./pages/dashboard/Dashboard";
|
import Dashboard from "./pages/dashboard/Dashboard";
|
||||||
|
import Account from "./pages/setting/Account";
|
||||||
|
|
||||||
export const router = createHashRouter([
|
export const router = createHashRouter([
|
||||||
{
|
{
|
||||||
@ -44,6 +45,10 @@ export const router = createHashRouter([
|
|||||||
path: "/setting/password",
|
path: "/setting/password",
|
||||||
element: <Password />,
|
element: <Password />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/setting/account",
|
||||||
|
element: <Account />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user