270 lines
8.1 KiB
TypeScript
270 lines
8.1 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { toast } from "sonner";
|
|
|
|
import { useAuth } from "~/context/AuthContext";
|
|
import { Button } from "~/components/ui/button";
|
|
import { Input } from "~/components/ui/input";
|
|
import {
|
|
formFieldClass,
|
|
formHintClass,
|
|
formInputClass,
|
|
modalFormClass,
|
|
} from "~/features/platform/modalUi";
|
|
import { cn } from "~/lib/utils";
|
|
|
|
import {
|
|
AppFormDialog,
|
|
dialogPrimaryButtonClass,
|
|
dialogSecondaryButtonClass,
|
|
} from "./AppFormDialog";
|
|
|
|
type AccountSettingsDialogProps = {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
};
|
|
|
|
type SettingsTab = "profile" | "password";
|
|
|
|
const TAB_ITEMS: Array<{ id: SettingsTab; label: string }> = [
|
|
{ id: "profile", label: "基本信息" },
|
|
{ id: "password", label: "修改密码" },
|
|
];
|
|
|
|
export function AccountSettingsDialog({
|
|
open,
|
|
onOpenChange,
|
|
}: AccountSettingsDialogProps) {
|
|
const { user, updateProfile, changePassword, logout } = useAuth();
|
|
const [tab, setTab] = useState<SettingsTab>("profile");
|
|
const [displayName, setDisplayName] = useState("");
|
|
const [email, setEmail] = useState("");
|
|
const [savingProfile, setSavingProfile] = useState(false);
|
|
const [currentPassword, setCurrentPassword] = useState("");
|
|
const [newPassword, setNewPassword] = useState("");
|
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
const [savingPassword, setSavingPassword] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!open || !user) return;
|
|
setTab("profile");
|
|
setDisplayName(user.display_name);
|
|
setEmail(user.email ?? "");
|
|
setCurrentPassword("");
|
|
setNewPassword("");
|
|
setConfirmPassword("");
|
|
}, [open, user]);
|
|
|
|
const handleSaveProfile = async (event: React.FormEvent) => {
|
|
event.preventDefault();
|
|
const trimmedName = displayName.trim();
|
|
if (!trimmedName) {
|
|
toast.error("显示名称不能为空");
|
|
return;
|
|
}
|
|
setSavingProfile(true);
|
|
try {
|
|
await updateProfile({
|
|
display_name: trimmedName,
|
|
email: email.trim(),
|
|
});
|
|
toast.success("资料已更新");
|
|
onOpenChange(false);
|
|
} catch (error) {
|
|
toast.error(error instanceof Error ? error.message : "资料更新失败");
|
|
} finally {
|
|
setSavingProfile(false);
|
|
}
|
|
};
|
|
|
|
const handleChangePassword = async (event: React.FormEvent) => {
|
|
event.preventDefault();
|
|
if (!currentPassword) {
|
|
toast.error("请输入当前密码");
|
|
return;
|
|
}
|
|
if (newPassword.length < 8 || newPassword.length > 72) {
|
|
toast.error("新密码长度需为 8~72 字符");
|
|
return;
|
|
}
|
|
if (newPassword !== confirmPassword) {
|
|
toast.error("两次输入的新密码不一致");
|
|
return;
|
|
}
|
|
if (newPassword === currentPassword) {
|
|
toast.error("新密码不能与当前密码相同");
|
|
return;
|
|
}
|
|
setSavingPassword(true);
|
|
try {
|
|
await changePassword({
|
|
current_password: currentPassword,
|
|
new_password: newPassword,
|
|
});
|
|
toast.success("密码已修改,请重新登录");
|
|
onOpenChange(false);
|
|
await logout();
|
|
} catch (error) {
|
|
toast.error(error instanceof Error ? error.message : "密码修改失败");
|
|
} finally {
|
|
setSavingPassword(false);
|
|
}
|
|
};
|
|
|
|
const formId =
|
|
tab === "profile" ? "account-profile-form" : "account-password-form";
|
|
const saving = tab === "profile" ? savingProfile : savingPassword;
|
|
const primaryLabel =
|
|
tab === "profile"
|
|
? savingProfile
|
|
? "保存中…"
|
|
: "保存资料"
|
|
: savingPassword
|
|
? "修改中…"
|
|
: "修改密码";
|
|
|
|
return (
|
|
<AppFormDialog
|
|
open={open}
|
|
onOpenChange={onOpenChange}
|
|
eyebrow="ACCOUNT"
|
|
title="账号设置"
|
|
size="panel"
|
|
showCloseButton
|
|
footer={
|
|
<>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className={dialogSecondaryButtonClass}
|
|
onClick={() => onOpenChange(false)}
|
|
disabled={saving}
|
|
>
|
|
取消
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
form={formId}
|
|
disabled={saving}
|
|
className={dialogPrimaryButtonClass}
|
|
>
|
|
{primaryLabel}
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
<div className="border-b border-line px-[22px]">
|
|
<div className="flex gap-1" role="tablist" aria-label="账号设置分区">
|
|
{TAB_ITEMS.map((item) => {
|
|
const active = tab === item.id;
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={active}
|
|
className={cn(
|
|
"relative -mb-px cursor-pointer border-0 border-b-2 bg-transparent px-3 py-2.5 text-[12px] font-[650] outline-none transition-colors",
|
|
active
|
|
? "border-[#2d82d4] text-[#1c5fa8]"
|
|
: "border-transparent text-[#6b7c8f] hover:text-[#3c4e62]",
|
|
)}
|
|
onClick={() => setTab(item.id)}
|
|
>
|
|
{item.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className={`${modalFormClass} pb-5`}>
|
|
{tab === "profile" ? (
|
|
<form
|
|
id={formId}
|
|
onSubmit={(event) => void handleSaveProfile(event)}
|
|
>
|
|
<label className={formFieldClass}>
|
|
<span>登录账号</span>
|
|
<Input
|
|
className={formInputClass}
|
|
value={user?.username ?? ""}
|
|
disabled
|
|
readOnly
|
|
/>
|
|
<span className={formHintClass}>登录账号创建后不可修改</span>
|
|
</label>
|
|
<label className={formFieldClass}>
|
|
<span>
|
|
显示名称<span className="text-danger">*</span>
|
|
</span>
|
|
<Input
|
|
className={formInputClass}
|
|
value={displayName}
|
|
onChange={(event) => setDisplayName(event.target.value)}
|
|
placeholder="请输入显示名称"
|
|
autoComplete="name"
|
|
autoFocus
|
|
/>
|
|
</label>
|
|
<label className={formFieldClass}>
|
|
<span>邮箱</span>
|
|
<Input
|
|
className={formInputClass}
|
|
type="email"
|
|
value={email}
|
|
onChange={(event) => setEmail(event.target.value)}
|
|
placeholder="可选"
|
|
autoComplete="email"
|
|
/>
|
|
</label>
|
|
</form>
|
|
) : (
|
|
<form
|
|
id={formId}
|
|
onSubmit={(event) => void handleChangePassword(event)}
|
|
>
|
|
<label className={formFieldClass}>
|
|
<span>
|
|
当前密码<span className="text-danger">*</span>
|
|
</span>
|
|
<Input
|
|
className={formInputClass}
|
|
type="password"
|
|
value={currentPassword}
|
|
onChange={(event) => setCurrentPassword(event.target.value)}
|
|
autoComplete="current-password"
|
|
autoFocus
|
|
/>
|
|
</label>
|
|
<label className={formFieldClass}>
|
|
<span>
|
|
新密码<span className="text-danger">*</span>
|
|
</span>
|
|
<Input
|
|
className={formInputClass}
|
|
type="password"
|
|
value={newPassword}
|
|
onChange={(event) => setNewPassword(event.target.value)}
|
|
autoComplete="new-password"
|
|
placeholder="8~72 字符"
|
|
/>
|
|
</label>
|
|
<label className={formFieldClass}>
|
|
<span>
|
|
确认新密码<span className="text-danger">*</span>
|
|
</span>
|
|
<Input
|
|
className={formInputClass}
|
|
type="password"
|
|
value={confirmPassword}
|
|
onChange={(event) => setConfirmPassword(event.target.value)}
|
|
autoComplete="new-password"
|
|
/>
|
|
</label>
|
|
</form>
|
|
)}
|
|
</div>
|
|
</AppFormDialog>
|
|
);
|
|
}
|