unis_crm/frontend/src/pages/Profile.tsx

679 lines
29 KiB
TypeScript
Raw Normal View History

2026-03-20 08:39:07 +00:00
import { useEffect, useState, type ReactNode } from "react";
import { AnimatePresence, motion } from "motion/react";
import {
Bell,
BriefcaseBusiness,
ChevronRight,
CircleUserRound,
HelpCircle,
LogOut,
Mail,
MapPinned,
Moon,
Eye,
EyeOff,
Phone,
Settings,
Shield,
Sun,
User,
X,
} from "lucide-react";
import { useNavigate } from "react-router-dom";
2026-03-19 06:23:03 +00:00
import { useTheme } from "@/components/ThemeProvider";
2026-03-20 08:39:07 +00:00
import {
clearAuth,
getCurrentUser,
getProfileOverview,
updateCurrentUserProfile,
updateCurrentUserPassword,
type ProfileOverview,
type UpdateCurrentUserProfilePayload,
type UpdateCurrentUserPasswordPayload,
type UserProfile,
} from "@/lib/auth";
type MenuItem = {
key: "personal" | "notice" | "security" | "help";
icon: typeof User;
label: string;
color: string;
bg: string;
};
type EditableProfileForm = UpdateCurrentUserProfilePayload;
const MENU_ITEMS: MenuItem[] = [
{ key: "personal", icon: User, label: "个人资料", color: "text-blue-500 dark:text-blue-400", bg: "bg-blue-50 dark:bg-blue-500/10" },
{ key: "notice", icon: Bell, label: "消息通知", color: "text-amber-500 dark:text-amber-400", bg: "bg-amber-50 dark:bg-amber-500/10" },
{ key: "security", icon: Shield, label: "账号安全", color: "text-emerald-500 dark:text-emerald-400", bg: "bg-emerald-50 dark:bg-emerald-500/10" },
{ key: "help", icon: HelpCircle, label: "帮助中心", color: "text-violet-500 dark:text-violet-400", bg: "bg-violet-50 dark:bg-violet-500/10" },
];
const EMPTY_FORM: EditableProfileForm = {
displayName: "",
email: "",
phone: "",
};
const EMPTY_PASSWORD_FORM: SecurityForm = {
oldPassword: "",
newPassword: "",
confirmPassword: "",
};
type SecurityForm = UpdateCurrentUserPasswordPayload & {
confirmPassword: string;
};
function displayText(value?: string | null) {
return value && value.trim() ? value : "无";
}
function numericValue(value?: number | null) {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
function PageModal({
title,
subtitle,
onClose,
children,
footer,
}: {
title: string;
subtitle?: string;
onClose: () => void;
children: ReactNode;
footer?: ReactNode;
}) {
return (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 z-40 bg-slate-900/25 backdrop-blur-sm dark:bg-slate-900/70"
/>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
className="fixed inset-0 z-50 p-0 sm:p-6"
>
<div className="mx-auto h-full w-full sm:max-w-3xl">
<div className="flex h-full flex-col overflow-hidden border border-slate-200 bg-white shadow-2xl dark:border-slate-800 dark:bg-slate-900 sm:rounded-3xl">
<div className="flex items-center justify-between border-b border-slate-100 px-5 py-4 dark:border-slate-800 sm:px-6">
<div>
<h2 className="text-lg font-semibold text-slate-900 dark:text-white">{title}</h2>
{subtitle ? <p className="mt-1 text-xs text-slate-500 dark:text-slate-400">{subtitle}</p> : null}
</div>
<button
onClick={onClose}
className="rounded-full p-2 text-slate-400 transition-colors hover:bg-slate-100 dark:hover:bg-slate-800"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="flex-1 overflow-y-auto px-5 py-5 sm:px-6">{children}</div>
{footer ? <div className="border-t border-slate-100 px-5 py-4 dark:border-slate-800 sm:px-6">{footer}</div> : null}
</div>
</div>
</motion.div>
</>
);
}
2026-03-19 06:23:03 +00:00
export default function Profile() {
const { theme, setTheme } = useTheme();
2026-03-20 08:39:07 +00:00
const navigate = useNavigate();
const [overview, setOverview] = useState<ProfileOverview | null>(null);
const [currentUser, setCurrentUser] = useState<UserProfile | null>(null);
const [profileDrawerOpen, setProfileDrawerOpen] = useState(false);
const [securityDrawerOpen, setSecurityDrawerOpen] = useState(false);
const [form, setForm] = useState<EditableProfileForm>(EMPTY_FORM);
const [detailLoading, setDetailLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const [securityForm, setSecurityForm] = useState<SecurityForm>(EMPTY_PASSWORD_FORM);
const [securitySaving, setSecuritySaving] = useState(false);
const [securityError, setSecurityError] = useState("");
const [securitySuccess, setSecuritySuccess] = useState("");
const [showOldPassword, setShowOldPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
useEffect(() => {
let ignore = false;
async function loadProfileData() {
try {
const me = await getCurrentUser();
const overviewData = await getProfileOverview().catch(() => null);
if (ignore) {
return;
}
setCurrentUser(me);
setOverview(overviewData);
} catch {
if (ignore) {
return;
}
setOverview(null);
setCurrentUser(null);
}
}
void loadProfileData();
return () => {
ignore = true;
};
}, []);
const handleLogout = () => {
clearAuth();
navigate("/login", { replace: true });
};
const handleOpenProfile = async () => {
setDetailLoading(true);
setError("");
try {
const me = currentUser ?? await getCurrentUser();
const latestOverview = overview ?? await getProfileOverview().catch(() => null);
setCurrentUser(me);
setOverview(latestOverview);
setForm({
userId: me.userId,
username: me.username,
displayName: me.displayName || "",
email: me.email || "",
phone: me.phone || "",
pwdResetRequired: me.pwdResetRequired,
isPlatformAdmin: me.isPlatformAdmin,
orgId: undefined,
});
setProfileDrawerOpen(true);
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : "获取个人资料失败");
setProfileDrawerOpen(true);
} finally {
setDetailLoading(false);
}
};
const handleCloseProfile = () => {
if (saving) {
return;
}
setProfileDrawerOpen(false);
setError("");
};
const handleSave = async () => {
if (saving) {
return;
}
setSaving(true);
setError("");
try {
await updateCurrentUserProfile({
...form,
email: form.email || undefined,
phone: form.phone || undefined,
});
const me = await getCurrentUser();
const overviewData = await getProfileOverview().catch(() => overview);
setCurrentUser(me);
setOverview(overviewData);
setForm({
userId: me.userId,
username: me.username,
displayName: me.displayName || "",
email: me.email || "",
phone: me.phone || "",
pwdResetRequired: me.pwdResetRequired,
isPlatformAdmin: me.isPlatformAdmin,
orgId: undefined,
});
sessionStorage.setItem("userProfile", JSON.stringify(me));
setProfileDrawerOpen(false);
} catch (saveError) {
setError(saveError instanceof Error ? saveError.message : "保存失败");
} finally {
setSaving(false);
}
};
const handleOpenSecurity = () => {
setSecurityForm(EMPTY_PASSWORD_FORM);
setSecurityError("");
setSecuritySuccess("");
setShowOldPassword(false);
setShowNewPassword(false);
setShowConfirmPassword(false);
setSecurityDrawerOpen(true);
};
const handleCloseSecurity = () => {
if (securitySaving) {
return;
}
setSecurityDrawerOpen(false);
setSecurityError("");
setSecuritySuccess("");
};
const handleUpdatePassword = async () => {
if (securitySaving) {
return;
}
if (!securityForm.oldPassword || !securityForm.newPassword || !securityForm.confirmPassword) {
setSecurityError("请完整填写旧密码、新密码和确认密码");
setSecuritySuccess("");
return;
}
if (securityForm.newPassword !== securityForm.confirmPassword) {
setSecurityError("两次输入的新密码不一致");
setSecuritySuccess("");
return;
}
if (securityForm.newPassword.length < 6) {
setSecurityError("新密码长度不能少于6位");
setSecuritySuccess("");
return;
}
setSecuritySaving(true);
setSecurityError("");
setSecuritySuccess("");
try {
await updateCurrentUserPassword({
oldPassword: securityForm.oldPassword,
newPassword: securityForm.newPassword,
});
clearAuth();
navigate("/login", { replace: true });
} catch (saveError) {
setSecurityError(saveError instanceof Error ? saveError.message : "密码修改失败");
} finally {
setSecuritySaving(false);
}
};
const roleNames = displayText(overview?.jobTitle);
const orgNames = displayText(overview?.deptName);
const displayName = displayText(currentUser?.displayName || overview?.realName);
const avatarText = displayName === "无" ? "无" : displayName.slice(0, 1);
const currentEmail = displayText(currentUser?.email);
const currentPhone = displayText(currentUser?.phone);
2026-03-19 06:23:03 +00:00
return (
2026-03-20 08:39:07 +00:00
<div className="space-y-5 sm:space-y-6">
2026-03-19 06:23:03 +00:00
<header className="flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white"></h1>
</header>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
2026-03-20 08:39:07 +00:00
className="rounded-2xl border border-slate-100 bg-white p-4 shadow-sm backdrop-blur-sm transition-all dark:border-slate-800 dark:bg-slate-900/50 sm:p-6"
2026-03-19 06:23:03 +00:00
>
2026-03-20 08:39:07 +00:00
<div className="flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
<div className="flex items-center gap-4">
<div className="flex h-16 w-16 shrink-0 items-center justify-center rounded-full bg-violet-100 text-2xl font-bold text-violet-600 dark:bg-violet-500/20 dark:text-violet-400">
{avatarText}
</div>
<div className="min-w-0 flex-1">
<h2 className="truncate text-xl font-bold text-slate-900 dark:text-white">{displayName}</h2>
<div className="mt-1 text-sm text-slate-500 dark:text-slate-400">
<p>{orgNames}</p>
<p>{roleNames}</p>
</div>
</div>
2026-03-19 06:23:03 +00:00
</div>
2026-03-20 08:39:07 +00:00
<button
onClick={() => void handleOpenProfile()}
className="self-end rounded-full bg-slate-50 p-2 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-600 dark:bg-slate-800 dark:text-slate-500 dark:hover:bg-slate-700 dark:hover:text-slate-300 sm:self-start"
>
2026-03-19 06:23:03 +00:00
<Settings className="h-5 w-5" />
</button>
</div>
2026-03-20 08:39:07 +00:00
<div className="mt-6 grid grid-cols-1 gap-3 border-t border-slate-50 pt-6 dark:border-slate-800/50 sm:grid-cols-3 sm:gap-0 sm:divide-x sm:divide-slate-100 dark:sm:divide-slate-800">
<div className="rounded-2xl bg-slate-50 px-4 py-4 text-center dark:bg-slate-800/40 sm:rounded-none sm:bg-transparent sm:px-2">
<p className="text-2xl font-bold text-slate-900 dark:text-white">{numericValue(overview?.monthlyOpportunityCount)}</p>
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400"></p>
2026-03-19 06:23:03 +00:00
</div>
2026-03-20 08:39:07 +00:00
<div className="rounded-2xl bg-slate-50 px-4 py-4 text-center dark:bg-slate-800/40 sm:rounded-none sm:bg-transparent sm:px-2">
<p className="text-2xl font-bold text-slate-900 dark:text-white">{numericValue(overview?.monthlyExpansionCount)}</p>
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400"></p>
2026-03-19 06:23:03 +00:00
</div>
2026-03-20 08:39:07 +00:00
<div className="rounded-2xl bg-slate-50 px-4 py-4 text-center dark:bg-slate-800/40 sm:rounded-none sm:bg-transparent sm:px-2">
<p className="text-2xl font-bold text-slate-900 dark:text-white">{numericValue(overview?.averageScore)}</p>
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400"></p>
2026-03-19 06:23:03 +00:00
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
2026-03-20 08:39:07 +00:00
className="overflow-hidden rounded-2xl border border-slate-100 bg-white shadow-sm backdrop-blur-sm transition-all dark:border-slate-800 dark:bg-slate-900/50"
2026-03-19 06:23:03 +00:00
>
<ul className="divide-y divide-slate-50 dark:divide-slate-800/50">
<li>
<button
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
2026-03-20 08:39:07 +00:00
className="flex w-full items-center justify-between p-4 transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/50 md:hidden"
2026-03-19 06:23:03 +00:00
>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-slate-100 dark:bg-slate-800">
{theme === "dark" ? <Sun className="h-5 w-5 text-amber-500" /> : <Moon className="h-5 w-5 text-indigo-500" />}
</div>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">
{theme === "dark" ? "切换亮色模式" : "切换暗色模式"}
</span>
</div>
<ChevronRight className="h-5 w-5 text-slate-300 dark:text-slate-600" />
</button>
</li>
2026-03-20 08:39:07 +00:00
{MENU_ITEMS.map((item) => (
<li key={item.key}>
<button
onClick={
item.key === "personal"
? () => void handleOpenProfile()
: item.key === "security"
? handleOpenSecurity
: undefined
}
className="flex w-full items-center justify-between p-4 text-left transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/50"
>
2026-03-19 06:23:03 +00:00
<div className="flex items-center gap-3">
<div className={`flex h-10 w-10 items-center justify-center rounded-xl ${item.bg}`}>
<item.icon className={`h-5 w-5 ${item.color}`} />
</div>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">{item.label}</span>
</div>
<ChevronRight className="h-5 w-5 text-slate-300 dark:text-slate-600" />
</button>
</li>
))}
</ul>
</motion.div>
<motion.button
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
2026-03-20 08:39:07 +00:00
onClick={handleLogout}
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-rose-100 bg-rose-50 p-4 text-sm font-semibold text-rose-600 transition-all hover:bg-rose-100 active:scale-[0.98] dark:border-rose-900/50 dark:bg-rose-500/10 dark:text-rose-400 dark:hover:bg-rose-500/20"
2026-03-19 06:23:03 +00:00
>
<LogOut className="h-5 w-5" />
退
</motion.button>
2026-03-20 08:39:07 +00:00
<AnimatePresence>
{profileDrawerOpen ? (
<PageModal
title="个人资料"
onClose={handleCloseProfile}
footer={(
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
<button
onClick={handleCloseProfile}
className="rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
>
</button>
<button
onClick={() => void handleSave()}
disabled={saving || detailLoading}
className="rounded-xl bg-violet-600 px-4 py-3 text-sm font-medium text-white shadow-sm transition-colors hover:bg-violet-700 disabled:cursor-not-allowed disabled:opacity-60"
>
{saving ? "保存中..." : "保存资料"}
</button>
</div>
)}
>
{error && !currentUser ? (
<div className="rounded-xl border border-rose-100 bg-rose-50 px-4 py-3 text-sm text-rose-600 dark:border-rose-900/50 dark:bg-rose-500/10 dark:text-rose-300">
{error}
</div>
) : (
<div className="space-y-6">
<div className="flex flex-col gap-4 rounded-2xl border border-slate-100 bg-slate-50/70 p-4 dark:border-slate-800 dark:bg-slate-800/20 sm:flex-row sm:items-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-violet-100 text-2xl font-bold text-violet-600 dark:bg-violet-500/20 dark:text-violet-400">
{(form.displayName || currentUser?.displayName || "无").slice(0, 1)}
</div>
<div className="min-w-0 flex-1">
<div className="text-lg font-semibold text-slate-900 dark:text-white">{displayText(currentUser?.displayName)}</div>
<div className="mt-1 text-sm text-slate-500 dark:text-slate-400">
<p>{orgNames}</p>
<p>{roleNames}</p>
</div>
<div className="mt-2 text-xs text-slate-400 dark:text-slate-500">{displayText(currentUser?.username)}</div>
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<label className="space-y-2">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300"></span>
<input
value={form.displayName || ""}
onChange={(event) => setForm((current) => ({ ...current, displayName: event.target.value }))}
className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50"
/>
</label>
<label className="space-y-2">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300"></span>
<div className="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-500 dark:border-slate-800 dark:bg-slate-800/40 dark:text-slate-400">
{displayText(currentUser?.username)}
</div>
</label>
<label className="space-y-2">
<span className="flex items-center gap-2 text-sm font-medium text-slate-700 dark:text-slate-300">
<Phone className="h-4 w-4 text-slate-400" />
</span>
<input
value={form.phone || ""}
onChange={(event) => setForm((current) => ({ ...current, phone: event.target.value }))}
className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50"
/>
</label>
<label className="space-y-2">
<span className="flex items-center gap-2 text-sm font-medium text-slate-700 dark:text-slate-300">
<Mail className="h-4 w-4 text-slate-400" />
</span>
<input
value={form.email || ""}
onChange={(event) => setForm((current) => ({ ...current, email: event.target.value }))}
className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50"
/>
</label>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="rounded-2xl border border-slate-100 bg-slate-50/70 p-4 dark:border-slate-800 dark:bg-slate-800/20">
<div className="flex items-center gap-2 text-sm font-semibold text-slate-900 dark:text-white">
<BriefcaseBusiness className="h-4 w-4 text-violet-500" />
</div>
<div className="mt-3 space-y-2 text-sm text-slate-500 dark:text-slate-400">
<p>{roleNames}</p>
<p>{displayText(overview?.accountStatus)}</p>
</div>
</div>
<div className="rounded-2xl border border-slate-100 bg-slate-50/70 p-4 dark:border-slate-800 dark:bg-slate-800/20">
<div className="flex items-center gap-2 text-sm font-semibold text-slate-900 dark:text-white">
<MapPinned className="h-4 w-4 text-emerald-500" />
</div>
<div className="mt-3 space-y-2 text-sm text-slate-500 dark:text-slate-400">
<p>{orgNames}</p>
<p className="flex items-center gap-2">
<CircleUserRound className="h-4 w-4 text-slate-400" />
{numericValue(overview?.onboardingDays)}
</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="rounded-xl border border-slate-100 bg-slate-50/70 px-4 py-3 text-sm text-slate-500 dark:border-slate-800 dark:bg-slate-800/20 dark:text-slate-400">
{currentPhone}
</div>
<div className="rounded-xl border border-slate-100 bg-slate-50/70 px-4 py-3 text-sm text-slate-500 dark:border-slate-800 dark:bg-slate-800/20 dark:text-slate-400">
{currentEmail}
</div>
</div>
{detailLoading ? (
<div className="rounded-xl border border-slate-100 bg-slate-50 px-4 py-3 text-sm text-slate-500 dark:border-slate-800 dark:bg-slate-800/40 dark:text-slate-400">
...
</div>
) : null}
{error ? (
<div className="rounded-xl border border-rose-100 bg-rose-50 px-4 py-3 text-sm text-rose-600 dark:border-rose-900/50 dark:bg-rose-500/10 dark:text-rose-300">
{error}
</div>
) : null}
</div>
)}
</PageModal>
) : null}
{securityDrawerOpen ? (
<PageModal
title="账号安全"
onClose={handleCloseSecurity}
footer={(
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
<button
onClick={handleCloseSecurity}
className="rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
>
</button>
<button
onClick={() => void handleUpdatePassword()}
disabled={securitySaving}
className="rounded-xl bg-emerald-600 px-4 py-3 text-sm font-medium text-white shadow-sm transition-colors hover:bg-emerald-700 disabled:cursor-not-allowed disabled:opacity-60"
>
{securitySaving ? "提交中..." : "更新密码"}
</button>
</div>
)}
>
<div className="space-y-6">
<div className="rounded-2xl border border-slate-100 bg-slate-50/70 p-4 dark:border-slate-800 dark:bg-slate-800/20">
<p className="text-sm text-slate-600 dark:text-slate-300">
</p>
</div>
<div className="grid grid-cols-1 gap-4">
<label className="space-y-2">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300"></span>
<div className="relative">
<input
type={showOldPassword ? "text" : "password"}
autoComplete="current-password"
value={securityForm.oldPassword}
onChange={(event) => setSecurityForm((current) => ({ ...current, oldPassword: event.target.value }))}
className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 pr-12 text-sm outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 dark:border-slate-800 dark:bg-slate-900/50"
/>
<button
type="button"
onClick={() => setShowOldPassword((current) => !current)}
className="absolute inset-y-0 right-0 flex w-12 items-center justify-center text-slate-400 transition-colors hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-300"
aria-label={showOldPassword ? "隐藏当前密码" : "显示当前密码"}
>
{showOldPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</label>
<label className="space-y-2">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300"></span>
<div className="relative">
<input
type={showNewPassword ? "text" : "password"}
autoComplete="new-password"
value={securityForm.newPassword}
onChange={(event) => setSecurityForm((current) => ({ ...current, newPassword: event.target.value }))}
className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 pr-12 text-sm outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 dark:border-slate-800 dark:bg-slate-900/50"
/>
<button
type="button"
onClick={() => setShowNewPassword((current) => !current)}
className="absolute inset-y-0 right-0 flex w-12 items-center justify-center text-slate-400 transition-colors hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-300"
aria-label={showNewPassword ? "隐藏新密码" : "显示新密码"}
>
{showNewPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</label>
<label className="space-y-2">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300"></span>
<div className="relative">
<input
type={showConfirmPassword ? "text" : "password"}
autoComplete="new-password"
value={securityForm.confirmPassword}
onChange={(event) => setSecurityForm((current) => ({ ...current, confirmPassword: event.target.value }))}
className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 pr-12 text-sm outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 dark:border-slate-800 dark:bg-slate-900/50"
/>
<button
type="button"
onClick={() => setShowConfirmPassword((current) => !current)}
className="absolute inset-y-0 right-0 flex w-12 items-center justify-center text-slate-400 transition-colors hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-300"
aria-label={showConfirmPassword ? "隐藏确认密码" : "显示确认密码"}
>
{showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</label>
</div>
{securityError ? (
<div className="rounded-xl border border-rose-100 bg-rose-50 px-4 py-3 text-sm text-rose-600 dark:border-rose-900/50 dark:bg-rose-500/10 dark:text-rose-300">
{securityError}
</div>
) : null}
{securitySuccess ? (
<div className="rounded-xl border border-emerald-100 bg-emerald-50 px-4 py-3 text-sm text-emerald-600 dark:border-emerald-900/50 dark:bg-emerald-500/10 dark:text-emerald-300">
{securitySuccess}
</div>
) : null}
</div>
</PageModal>
) : null}
</AnimatePresence>
2026-03-19 06:23:03 +00:00
</div>
);
}