Files

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>
);
}