import { useCallback, useEffect, useState } from 'react'; import { useAuthStore } from '../stores/authStore'; import * as api from '../api/client'; import { LockIcon, LogoutIcon } from './icons'; import KeyPanel from './KeyPanel'; import ThemePicker from './ThemePicker'; import BackgroundPicker from './BackgroundPicker'; /** 当前用户个人中心:查看资料、修改密码、管理客户端连接密钥 */ export default function AccountPage() { const user = useAuthStore(s => s.user); const logout = useAuthStore(s => s.logout); const [oldPw, setOldPw] = useState(''); const [newPw, setNewPw] = useState(''); const [confirmPw, setConfirmPw] = useState(''); const [busy, setBusy] = useState(false); const [msg, setMsg] = useState(null); const [error, setError] = useState(null); // 密钥面板状态 const [keys, setKeys] = useState([]); const [keyBusy, setKeyBusy] = useState(false); const [keyError, setKeyError] = useState(null); const [newToken, setNewToken] = useState(null); const loadKeys = useCallback(async () => { try { const r = await api.listMyKeys(); setKeys(r.keys); setKeyError(null); } catch (err) { setKeyError(err instanceof Error ? err.message : String(err)); } }, []); useEffect(() => { loadKeys(); }, [loadKeys]); const createKey = async (payload: api.CreateKeyPayload) => { setKeyBusy(true); setKeyError(null); try { const r = await api.createMyKey(payload); // 全文只在创建响应里出现一次,必须当场展示 setNewToken(r.key.key_token ?? null); await loadKeys(); } catch (err) { setKeyError(err instanceof Error ? err.message : String(err)); } finally { setKeyBusy(false); } }; const deleteKey = async (id: string) => { setKeyError(null); try { await api.deleteMyKey(id); await loadKeys(); } catch (err) { setKeyError(err instanceof Error ? err.message : String(err)); } }; const mismatch = confirmPw !== '' && newPw !== confirmPw; const canSubmit = oldPw.length > 0 && newPw.length >= 8 && !mismatch && !busy; const changePw = async (e: React.FormEvent) => { e.preventDefault(); if (!canSubmit) return; setBusy(true); setError(null); setMsg(null); try { await api.changePassword(oldPw, newPw); setMsg('密码已修改,请重新登录'); setOldPw(''); setNewPw(''); setConfirmPw(''); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } }; if (!user) return null; return (

账号信息

{/* 滚动容器。 缺了它的后果:这个页的内容(资料 + 权限 + 改密码 + 密钥 + 退出) 比视口高,而父级是 overflow-hidden 的 flex 列 —— 超出那段直接被裁掉, 没有任何办法滚到。实测 390px 下内容需 860px、容器只有 795px; 1280x800 的桌面上同样看不到最后的「退出登录」。 */}
{/* 基本信息 */}

基本资料

{/* 权限边界 */} {user.role !== 'admin' && (

权限范围

)} {/* 修改密码 */}

修改密码

setOldPw(e.target.value)} autoComplete="current-password" className="w-full text-sm border border-gray-300 rounded-md px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400" />
setNewPw(e.target.value)} autoComplete="new-password" className="w-full text-sm border border-gray-300 rounded-md px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400" />
setConfirmPw(e.target.value)} autoComplete="new-password" className={`w-full text-sm border rounded-md px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100 ${ mismatch ? 'border-red-300' : 'border-gray-300 focus:border-blue-400' }`} /> {mismatch &&

两次密码不一致

}
{error && (

{error}

)} {msg && (

{msg}

)}
{/* 客户端连接密钥 */}
setNewToken(null)} />
{/* 外观。放在密钥之后、退出之前:它是一个高频且完全可逆的偏好, 与「账号自身」的密码/密钥属于不同性质,但同样是「我的设置」。 */}
{/* 退出登录。 放在这里而不是导航里:它是一个低频且不可逆的动作, 与密码、密钥同属「账号自身」。窄屏下这也是唯一的退出口: 抽屉式侧栏已删(它的其余入口与底部导航完全重复)。 */}

登录状态

); } function Row({ label, value, mono }: { label: string; value: string; mono?: boolean }) { return (
{label}
{value}
); }