214 lines
10 KiB
TypeScript
214 lines
10 KiB
TypeScript
"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>
|
||
);
|
||
}
|