unis_crm/frontend/src/pages/Profile.tsx

703 lines
28 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 {
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 = {
2026-03-26 09:29:55 +00:00
key: "personal" | "security" | "help";
2026-03-20 08:39:07 +00:00
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: "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 }}
2026-03-26 09:29:55 +00:00
className="fixed inset-0 z-50 px-0 pb-0 pt-[env(safe-area-inset-top)] sm:p-6"
2026-03-20 08:39:07 +00:00
>
2026-03-26 09:29:55 +00:00
<div className="mx-auto h-[calc(100dvh-env(safe-area-inset-top))] w-full sm:h-full sm:max-w-3xl">
2026-03-20 08:39:07 +00:00
<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>
2026-03-26 09:29:55 +00:00
<h2 className="text-base font-semibold text-slate-900 dark:text-white sm:text-lg">{title}</h2>
{subtitle ? <p className="mt-1 text-xs leading-5 text-slate-500 dark:text-slate-400">{subtitle}</p> : null}
2026-03-20 08:39:07 +00:00
</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>
2026-03-26 09:29:55 +00:00
{footer ? <div className="border-t border-slate-100 px-5 pb-[calc(1rem+env(safe-area-inset-bottom))] pt-4 dark:border-slate-800 sm:px-6 sm:pb-4">{footer}</div> : null}
2026-03-20 08:39:07 +00:00
</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 });
};
2026-03-23 01:03:27 +00:00
const handleNavigateToMonthlyOpportunity = () => {
navigate("/opportunities");
};
const handleNavigateToMonthlyExpansion = () => {
navigate("/expansion");
};
2026-03-20 08:39:07 +00:00
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("");
};
2026-03-26 09:29:55 +00:00
const handleOpenHelp = () => {
window.alert("请联系系统管理员!");
};
2026-03-20 08:39:07 +00:00
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-26 09:29:55 +00:00
<div className="crm-page-stack">
2026-03-27 09:05:41 +00:00
<header className="crm-page-header">
<div className="crm-page-heading">
<h1 className="crm-page-title"></h1>
<p className="crm-page-subtitle"></p>
</div>
2026-03-19 06:23:03 +00:00
</header>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
2026-03-26 09:29:55 +00:00
className="crm-card crm-card-pad-lg relative rounded-2xl transition-all"
2026-03-19 06:23:03 +00:00
>
2026-03-26 09:29:55 +00:00
<button
onClick={() => void handleOpenProfile()}
className="absolute right-4 top-4 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:right-6 sm:top-6"
>
2026-03-27 09:05:41 +00:00
<Settings className="crm-icon-lg" />
2026-03-26 09:29:55 +00:00
</button>
<div className="flex flex-col gap-5 pr-12 sm:flex-row sm:items-start">
2026-03-20 08:39:07 +00:00
<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">
2026-03-26 09:29:55 +00:00
<h2 className="truncate text-lg font-bold text-slate-900 dark:text-white sm:text-xl">{displayName}</h2>
<div className="mt-1 text-xs leading-5 text-slate-500 dark:text-slate-400 sm:text-sm">
2026-03-20 08:39:07 +00:00
<p>{orgNames}</p>
<p>{roleNames}</p>
</div>
</div>
2026-03-19 06:23:03 +00:00
</div>
</div>
2026-03-26 09:29:55 +00:00
<div className="mt-6 grid grid-cols-3 gap-2.5 border-t border-slate-50 pt-6 dark:border-slate-800/50 sm:gap-0 sm:divide-x sm:divide-slate-100 dark:sm:divide-slate-800">
2026-03-23 01:03:27 +00:00
<button
type="button"
onClick={handleNavigateToMonthlyOpportunity}
2026-03-26 09:29:55 +00:00
className="rounded-2xl bg-slate-50 px-2 py-4 text-center transition-colors hover:bg-slate-100 active:scale-[0.99] dark:bg-slate-800/40 dark:hover:bg-slate-800 sm:rounded-none sm:bg-transparent sm:px-2"
2026-03-23 01:03:27 +00:00
>
2026-03-26 09:29:55 +00:00
<p className="text-lg font-bold text-slate-900 dark:text-white sm:text-2xl">{numericValue(overview?.monthlyOpportunityCount)}</p>
<p className="mt-1 text-[11px] text-slate-500 dark:text-slate-400"></p>
2026-03-23 01:03:27 +00:00
</button>
<button
type="button"
onClick={handleNavigateToMonthlyExpansion}
2026-03-26 09:29:55 +00:00
className="rounded-2xl bg-slate-50 px-2 py-4 text-center transition-colors hover:bg-slate-100 active:scale-[0.99] dark:bg-slate-800/40 dark:hover:bg-slate-800 sm:rounded-none sm:bg-transparent sm:px-2"
2026-03-23 01:03:27 +00:00
>
2026-03-26 09:29:55 +00:00
<p className="text-lg font-bold text-slate-900 dark:text-white sm:text-2xl">{numericValue(overview?.monthlyExpansionCount)}</p>
<p className="mt-1 text-[11px] text-slate-500 dark:text-slate-400"></p>
2026-03-23 01:03:27 +00:00
</button>
2026-03-26 09:29:55 +00:00
<div className="rounded-2xl bg-slate-50 px-2 py-4 text-center dark:bg-slate-800/40 sm:rounded-none sm:bg-transparent sm:px-2">
<p className="text-lg font-bold text-slate-900 dark:text-white sm:text-2xl">{numericValue(overview?.averageScore)}</p>
<p className="mt-1 text-[11px] 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-26 09:29:55 +00:00
className="crm-card overflow-hidden rounded-2xl transition-all"
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">
2026-03-27 09:05:41 +00:00
{theme === "dark" ? <Sun className="crm-icon-lg text-amber-500" /> : <Moon className="crm-icon-lg text-indigo-500" />}
2026-03-19 06:23:03 +00:00
</div>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">
{theme === "dark" ? "切换亮色模式" : "切换暗色模式"}
</span>
</div>
2026-03-27 09:05:41 +00:00
<ChevronRight className="crm-icon-lg text-slate-300 dark:text-slate-600" />
2026-03-19 06:23:03 +00:00
</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
2026-03-26 09:29:55 +00:00
: item.key === "help"
? handleOpenHelp
2026-03-20 08:39:07 +00:00
: 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}`}>
2026-03-27 09:05:41 +00:00
<item.icon className={`crm-icon-lg ${item.color}`} />
2026-03-19 06:23:03 +00:00
</div>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">{item.label}</span>
</div>
2026-03-27 09:05:41 +00:00
<ChevronRight className="crm-icon-lg text-slate-300 dark:text-slate-600" />
2026-03-19 06:23:03 +00:00
</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}
2026-03-27 09:05:41 +00:00
className="crm-btn crm-btn-danger flex w-full items-center justify-center gap-2 active:scale-[0.98]"
2026-03-19 06:23:03 +00:00
>
2026-03-27 09:05:41 +00:00
<LogOut className="crm-icon-lg" />
2026-03-19 06:23:03 +00:00
退
</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}
2026-03-27 09:05:41 +00:00
className="crm-btn crm-btn-secondary"
2026-03-20 08:39:07 +00:00
>
</button>
<button
onClick={() => void handleSave()}
disabled={saving || detailLoading}
2026-03-27 09:05:41 +00:00
className="crm-btn crm-btn-primary disabled:cursor-not-allowed disabled:opacity-60"
2026-03-20 08:39:07 +00:00
>
{saving ? "保存中..." : "保存资料"}
</button>
</div>
)}
>
{error && !currentUser ? (
2026-03-27 09:05:41 +00:00
<div className="crm-alert crm-alert-error">
2026-03-20 08:39:07 +00:00
{error}
</div>
) : (
2026-03-26 09:29:55 +00:00
<div className="crm-modal-stack">
<div className="crm-form-section sm:flex-row sm:items-center">
2026-03-20 08:39:07 +00:00
<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>
2026-03-26 09:29:55 +00:00
<div className="crm-form-grid">
2026-03-20 08:39:07 +00:00
<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 }))}
2026-03-26 09:29:55 +00:00
className="crm-input-box crm-input-text w-full border border-slate-200 bg-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50"
2026-03-20 08:39:07 +00:00
/>
</label>
<label className="space-y-2">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300"></span>
2026-03-26 09:29:55 +00:00
<div className="crm-input-box-readonly crm-input-text w-full border border-slate-200 bg-slate-50 text-slate-500 dark:border-slate-800 dark:bg-slate-800/40 dark:text-slate-400">
2026-03-20 08:39:07 +00:00
{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">
2026-03-27 09:05:41 +00:00
<Phone className="crm-icon-md text-slate-400" />
2026-03-20 08:39:07 +00:00
</span>
<input
value={form.phone || ""}
onChange={(event) => setForm((current) => ({ ...current, phone: event.target.value }))}
2026-03-26 09:29:55 +00:00
className="crm-input-box crm-input-text w-full border border-slate-200 bg-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50"
2026-03-20 08:39:07 +00:00
/>
</label>
<label className="space-y-2">
<span className="flex items-center gap-2 text-sm font-medium text-slate-700 dark:text-slate-300">
2026-03-27 09:05:41 +00:00
<Mail className="crm-icon-md text-slate-400" />
2026-03-20 08:39:07 +00:00
</span>
<input
value={form.email || ""}
onChange={(event) => setForm((current) => ({ ...current, email: event.target.value }))}
2026-03-26 09:29:55 +00:00
className="crm-input-box crm-input-text w-full border border-slate-200 bg-white outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50"
2026-03-20 08:39:07 +00:00
/>
</label>
</div>
2026-03-26 09:29:55 +00:00
<div className="crm-form-grid">
<div className="crm-form-section">
2026-03-20 08:39:07 +00:00
<div className="flex items-center gap-2 text-sm font-semibold text-slate-900 dark:text-white">
2026-03-27 09:05:41 +00:00
<BriefcaseBusiness className="crm-icon-md text-violet-500" />
2026-03-20 08:39:07 +00:00
</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>
2026-03-26 09:29:55 +00:00
<div className="crm-form-section">
2026-03-20 08:39:07 +00:00
<div className="flex items-center gap-2 text-sm font-semibold text-slate-900 dark:text-white">
2026-03-27 09:05:41 +00:00
<MapPinned className="crm-icon-md text-emerald-500" />
2026-03-20 08:39:07 +00:00
</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">
2026-03-27 09:05:41 +00:00
<CircleUserRound className="crm-icon-md text-slate-400" />
2026-03-20 08:39:07 +00:00
{numericValue(overview?.onboardingDays)}
</p>
</div>
</div>
</div>
2026-03-26 09:29:55 +00:00
<div className="crm-form-grid">
2026-03-20 08:39:07 +00:00
<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 ? (
2026-03-27 09:05:41 +00:00
<div className="crm-alert crm-alert-info">
2026-03-20 08:39:07 +00:00
...
</div>
) : null}
{error ? (
2026-03-27 09:05:41 +00:00
<div className="crm-alert crm-alert-error">
2026-03-20 08:39:07 +00:00
{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}
2026-03-27 09:05:41 +00:00
className="crm-btn crm-btn-secondary"
2026-03-20 08:39:07 +00:00
>
</button>
<button
onClick={() => void handleUpdatePassword()}
disabled={securitySaving}
2026-03-27 09:05:41 +00:00
className="crm-btn crm-btn-success disabled:cursor-not-allowed disabled:opacity-60"
2026-03-20 08:39:07 +00:00
>
{securitySaving ? "提交中..." : "更新密码"}
</button>
</div>
)}
>
2026-03-26 09:29:55 +00:00
<div className="crm-modal-stack">
<div className="crm-form-section">
2026-03-20 08:39:07 +00:00
<p className="text-sm text-slate-600 dark:text-slate-300">
</p>
</div>
2026-03-26 09:29:55 +00:00
<div className="crm-section-stack">
2026-03-20 08:39:07 +00:00
<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 }))}
2026-03-26 09:29:55 +00:00
className="crm-input-box crm-input-text w-full border border-slate-200 bg-white pr-12 outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 dark:border-slate-800 dark:bg-slate-900/50"
2026-03-20 08:39:07 +00:00
/>
<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 ? "隐藏当前密码" : "显示当前密码"}
>
2026-03-27 09:05:41 +00:00
{showOldPassword ? <EyeOff className="crm-icon-md" /> : <Eye className="crm-icon-md" />}
2026-03-20 08:39:07 +00:00
</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 }))}
2026-03-26 09:29:55 +00:00
className="crm-input-box crm-input-text w-full border border-slate-200 bg-white pr-12 outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 dark:border-slate-800 dark:bg-slate-900/50"
2026-03-20 08:39:07 +00:00
/>
<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 ? "隐藏新密码" : "显示新密码"}
>
2026-03-27 09:05:41 +00:00
{showNewPassword ? <EyeOff className="crm-icon-md" /> : <Eye className="crm-icon-md" />}
2026-03-20 08:39:07 +00:00
</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 }))}
2026-03-26 09:29:55 +00:00
className="crm-input-box crm-input-text w-full border border-slate-200 bg-white pr-12 outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 dark:border-slate-800 dark:bg-slate-900/50"
2026-03-20 08:39:07 +00:00
/>
<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 ? "隐藏确认密码" : "显示确认密码"}
>
2026-03-27 09:05:41 +00:00
{showConfirmPassword ? <EyeOff className="crm-icon-md" /> : <Eye className="crm-icon-md" />}
2026-03-20 08:39:07 +00:00
</button>
</div>
</label>
</div>
{securityError ? (
2026-03-27 09:05:41 +00:00
<div className="crm-alert crm-alert-error">
2026-03-20 08:39:07 +00:00
{securityError}
</div>
) : null}
{securitySuccess ? (
2026-03-27 09:05:41 +00:00
<div className="crm-alert crm-alert-success">
2026-03-20 08:39:07 +00:00
{securitySuccess}
</div>
) : null}
</div>
</PageModal>
) : null}
</AnimatePresence>
2026-03-19 06:23:03 +00:00
</div>
);
}