Files
ViGent2/frontend/src/components/AccountSettingsDropdown.tsx
Kevin Wong e226224119 更新
2026-02-08 19:54:11 +08:00

214 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect, useRef } from "react";
import { useAuth } from "@/shared/contexts/AuthContext";
import api from "@/shared/api/axios";
import { ApiResponse } from "@/shared/api/types";
// 账户设置下拉菜单组件
export default function AccountSettingsDropdown() {
const { user } = useAuth();
const [isOpen, setIsOpen] = useState(false);
const [showPasswordModal, setShowPasswordModal] = useState(false);
const [oldPassword, setOldPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [loading, setLoading] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// 点击外部关闭菜单
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
// 格式化有效期
const formatExpiry = (expiresAt: string | null) => {
if (!expiresAt) return '永久有效';
const date = new Date(expiresAt);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
};
const handleLogout = async () => {
if (confirm('确定要退出登录吗?')) {
try {
await api.post('/api/auth/logout');
} catch { }
window.location.href = '/login';
}
};
const handleChangePassword = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setSuccess('');
if (newPassword !== confirmPassword) {
setError('两次输入的新密码不一致');
return;
}
if (newPassword.length < 6) {
setError('新密码长度至少6位');
return;
}
setLoading(true);
try {
const { data: res } = await api.post<ApiResponse<null>>('/api/auth/change-password', {
old_password: oldPassword,
new_password: newPassword
});
if (res.success) {
setSuccess(res.message || '密码修改成功,正在跳转登录页...');
// 清除登录状态并跳转
setTimeout(async () => {
try {
await api.post('/api/auth/logout');
} catch { }
window.location.href = '/login';
}, 1500);
} else {
setError(res.message || '修改失败');
}
} catch (err: unknown) {
const axiosErr = err as { response?: { data?: { message?: string } } };
setError(axiosErr.response?.data?.message || '修改失败,请重试');
} finally {
setLoading(false);
}
};
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="px-2 sm:px-4 py-1 sm:py-2 text-sm sm:text-base bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors flex items-center gap-1"
>
<span></span>
<span className="hidden sm:inline"></span>
<svg className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* 下拉菜单 */}
{isOpen && (
<div className="absolute right-0 mt-2 bg-gray-800 border border-white/10 rounded-lg shadow-xl z-[160] overflow-hidden whitespace-nowrap">
{/* 有效期显示 */}
<div className="px-3 py-2 border-b border-white/10 text-center">
<div className="text-xs text-gray-400"></div>
<div className="text-sm text-white font-medium">
{user?.expires_at ? formatExpiry(user.expires_at) : '永久有效'}
</div>
</div>
<button
onClick={() => {
setIsOpen(false);
setShowPasswordModal(true);
}}
className="w-full px-3 py-2 text-left text-sm text-white hover:bg-white/10 flex items-center gap-2"
>
🔐
</button>
<button
onClick={handleLogout}
className="w-full px-3 py-2 text-left text-sm text-red-300 hover:bg-red-500/20 flex items-center gap-2"
>
🚪 退
</button>
</div>
)}
{/* 修改密码弹窗 */}
{showPasswordModal && (
<div className="fixed inset-0 z-[200] flex items-start justify-center pt-20 bg-black/60 backdrop-blur-sm p-4">
<div className="w-full max-w-md p-6 bg-gray-900 border border-white/10 rounded-2xl shadow-2xl mx-4">
<h3 className="text-xl font-bold text-white mb-4"></h3>
<form onSubmit={handleChangePassword} className="space-y-4">
<div>
<label className="block text-sm text-gray-300 mb-1"></label>
<input
type="password"
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
required
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
placeholder="输入当前密码"
/>
</div>
<div>
<label className="block text-sm text-gray-300 mb-1"></label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
placeholder="至少6位"
/>
</div>
<div>
<label className="block text-sm text-gray-300 mb-1"></label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
placeholder="再次输入新密码"
/>
</div>
{error && (
<div className="p-2 bg-red-500/20 border border-red-500/50 rounded text-red-200 text-sm">
{error}
</div>
)}
{success && (
<div className="p-2 bg-green-500/20 border border-green-500/50 rounded text-green-200 text-sm">
{success}
</div>
)}
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={() => {
setShowPasswordModal(false);
setError('');
setOldPassword('');
setNewPassword('');
setConfirmPassword('');
}}
className="flex-1 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors"
>
</button>
<button
type="submit"
disabled={loading}
className="flex-1 py-2 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white rounded-lg transition-colors disabled:opacity-50"
>
{loading ? '修改中...' : '确认修改'}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}