686 lines
27 KiB
TypeScript
686 lines
27 KiB
TypeScript
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";
|
||
import { useTheme } from "@/components/ThemeProvider";
|
||
import { useIsMobileViewport } from "@/hooks/useIsMobileViewport";
|
||
import { useIsWecomBrowser } from "@/hooks/useIsWecomBrowser";
|
||
import {
|
||
clearAuth,
|
||
getCurrentUser,
|
||
getProfileOverview,
|
||
updateCurrentUserProfile,
|
||
updateCurrentUserPassword,
|
||
type ProfileOverview,
|
||
type UpdateCurrentUserProfilePayload,
|
||
type UpdateCurrentUserPasswordPayload,
|
||
type UserProfile,
|
||
} from "@/lib/auth";
|
||
|
||
type MenuItem = {
|
||
key: "personal" | "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: "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 px-0 pb-0 pt-[env(safe-area-inset-top)] sm:p-6"
|
||
>
|
||
<div className="mx-auto h-[calc(100dvh-env(safe-area-inset-top))] w-full sm:h-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-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}
|
||
</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 pb-[calc(1rem+env(safe-area-inset-bottom))] pt-4 dark:border-slate-800 sm:px-6 sm:pb-4">{footer}</div> : null}
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
export default function Profile() {
|
||
const { theme, setTheme } = useTheme();
|
||
const navigate = useNavigate();
|
||
const isMobileViewport = useIsMobileViewport();
|
||
const isWecomBrowser = useIsWecomBrowser();
|
||
const disableMobileMotion = isMobileViewport || isWecomBrowser;
|
||
|
||
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 handleNavigateToMonthlyOpportunity = () => {
|
||
navigate("/opportunities");
|
||
};
|
||
|
||
const handleNavigateToMonthlyExpansion = () => {
|
||
navigate("/expansion");
|
||
};
|
||
|
||
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 handleOpenHelp = () => {
|
||
window.alert("请联系系统管理员!");
|
||
};
|
||
|
||
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);
|
||
|
||
return (
|
||
<div className="crm-page-stack">
|
||
<header className="crm-page-header">
|
||
<div className="crm-page-heading">
|
||
<h1 className="crm-page-title">我的</h1>
|
||
<p className="crm-page-subtitle">查看个人信息、账号安全设置和本月工作概览。</p>
|
||
</div>
|
||
</header>
|
||
|
||
<motion.div
|
||
initial={disableMobileMotion ? false : { opacity: 0, y: 10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={disableMobileMotion ? { duration: 0 } : undefined}
|
||
className="crm-card crm-card-pad-lg relative rounded-2xl transition-shadow transition-colors"
|
||
>
|
||
<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"
|
||
>
|
||
<Settings className="crm-icon-lg" />
|
||
</button>
|
||
|
||
<div className="flex flex-col gap-5 pr-12 sm:flex-row sm:items-start">
|
||
<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-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">
|
||
<p>部门:{orgNames}</p>
|
||
<p>岗位:{roleNames}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
|
||
<motion.div
|
||
initial={disableMobileMotion ? false : { opacity: 0, y: 10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={disableMobileMotion ? { duration: 0 } : { delay: 0.1 }}
|
||
className="crm-card overflow-hidden rounded-2xl transition-shadow transition-colors"
|
||
>
|
||
<ul className="divide-y divide-slate-50 dark:divide-slate-800/50">
|
||
<li>
|
||
<button
|
||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||
className="flex w-full items-center justify-between p-4 transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/50 md:hidden"
|
||
>
|
||
<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="crm-icon-lg text-amber-500" /> : <Moon className="crm-icon-lg text-indigo-500" />}
|
||
</div>
|
||
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
||
{theme === "dark" ? "切换亮色模式" : "切换暗色模式"}
|
||
</span>
|
||
</div>
|
||
<ChevronRight className="crm-icon-lg text-slate-300 dark:text-slate-600" />
|
||
</button>
|
||
</li>
|
||
{MENU_ITEMS.map((item) => (
|
||
<li key={item.key}>
|
||
<button
|
||
onClick={
|
||
item.key === "personal"
|
||
? () => void handleOpenProfile()
|
||
: item.key === "security"
|
||
? handleOpenSecurity
|
||
: item.key === "help"
|
||
? handleOpenHelp
|
||
: 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"
|
||
>
|
||
<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={`crm-icon-lg ${item.color}`} />
|
||
</div>
|
||
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">{item.label}</span>
|
||
</div>
|
||
<ChevronRight className="crm-icon-lg text-slate-300 dark:text-slate-600" />
|
||
</button>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</motion.div>
|
||
|
||
<motion.button
|
||
initial={disableMobileMotion ? false : { opacity: 0, y: 10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={disableMobileMotion ? { duration: 0 } : { delay: 0.2 }}
|
||
onClick={handleLogout}
|
||
className="crm-btn crm-btn-danger flex w-full items-center justify-center gap-2 active:scale-100 sm:active:scale-[0.98]"
|
||
>
|
||
<LogOut className="crm-icon-lg" />
|
||
退出登录
|
||
</motion.button>
|
||
|
||
<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="crm-btn crm-btn-secondary"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
onClick={() => void handleSave()}
|
||
disabled={saving || detailLoading}
|
||
className="crm-btn crm-btn-primary disabled:cursor-not-allowed disabled:opacity-60"
|
||
>
|
||
{saving ? "保存中..." : "保存资料"}
|
||
</button>
|
||
</div>
|
||
)}
|
||
>
|
||
{error && !currentUser ? (
|
||
<div className="crm-alert crm-alert-error">
|
||
{error}
|
||
</div>
|
||
) : (
|
||
<div className="crm-modal-stack">
|
||
<div className="crm-form-section 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="crm-form-grid">
|
||
<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="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"
|
||
/>
|
||
</label>
|
||
<label className="space-y-2">
|
||
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">用户名</span>
|
||
<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">
|
||
{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="crm-icon-md text-slate-400" />
|
||
手机号
|
||
</span>
|
||
<input
|
||
value={form.phone || ""}
|
||
onChange={(event) => setForm((current) => ({ ...current, phone: event.target.value }))}
|
||
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"
|
||
/>
|
||
</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="crm-icon-md text-slate-400" />
|
||
邮箱
|
||
</span>
|
||
<input
|
||
value={form.email || ""}
|
||
onChange={(event) => setForm((current) => ({ ...current, email: event.target.value }))}
|
||
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"
|
||
/>
|
||
</label>
|
||
</div>
|
||
|
||
<div className="crm-form-grid">
|
||
<div className="crm-form-section">
|
||
<div className="flex items-center gap-2 text-sm font-semibold text-slate-900 dark:text-white">
|
||
<BriefcaseBusiness className="crm-icon-md 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="crm-form-section">
|
||
<div className="flex items-center gap-2 text-sm font-semibold text-slate-900 dark:text-white">
|
||
<MapPinned className="crm-icon-md 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="crm-icon-md text-slate-400" />
|
||
入职天数:{numericValue(overview?.onboardingDays)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="crm-form-grid">
|
||
<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="crm-alert crm-alert-info">
|
||
正在加载个人资料...
|
||
</div>
|
||
) : null}
|
||
|
||
{error ? (
|
||
<div className="crm-alert crm-alert-error">
|
||
{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="crm-btn crm-btn-secondary"
|
||
>
|
||
关闭
|
||
</button>
|
||
<button
|
||
onClick={() => void handleUpdatePassword()}
|
||
disabled={securitySaving}
|
||
className="crm-btn crm-btn-success disabled:cursor-not-allowed disabled:opacity-60"
|
||
>
|
||
{securitySaving ? "提交中..." : "更新密码"}
|
||
</button>
|
||
</div>
|
||
)}
|
||
>
|
||
<div className="crm-modal-stack">
|
||
<div className="crm-form-section">
|
||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||
为了账号安全,请先输入当前密码,再设置新密码。
|
||
</p>
|
||
</div>
|
||
|
||
<div className="crm-section-stack">
|
||
<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="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"
|
||
/>
|
||
<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="crm-icon-md" /> : <Eye className="crm-icon-md" />}
|
||
</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="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"
|
||
/>
|
||
<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="crm-icon-md" /> : <Eye className="crm-icon-md" />}
|
||
</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="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"
|
||
/>
|
||
<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="crm-icon-md" /> : <Eye className="crm-icon-md" />}
|
||
</button>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
|
||
{securityError ? (
|
||
<div className="crm-alert crm-alert-error">
|
||
{securityError}
|
||
</div>
|
||
) : null}
|
||
|
||
{securitySuccess ? (
|
||
<div className="crm-alert crm-alert-success">
|
||
{securitySuccess}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
</PageModal>
|
||
) : null}
|
||
</AnimatePresence>
|
||
</div>
|
||
);
|
||
}
|