313 lines
11 KiB
React
313 lines
11 KiB
React
import React, { useState, useRef, useEffect } from 'react';
|
||
import { useStoreData } from '../hooks/useStoreData';
|
||
import { Store } from '../utils/store';
|
||
import { userApi } from '../api/user';
|
||
import { User, Settings, LogOut, X, Edit2 } from 'lucide-react';
|
||
import { GlassCard } from './ui/GlassCard';
|
||
import { Button } from './ui/Button';
|
||
import clsx from 'clsx';
|
||
|
||
/**
|
||
* 用户资料菜单组件
|
||
* 显示用户信息、编辑资料和退出登录选项
|
||
*/
|
||
export function UserMenu({ isOpen, onClose, onLogout }) {
|
||
const data = useStoreData();
|
||
const [showEditModal, setShowEditModal] = useState(false);
|
||
const menuRef = useRef(null);
|
||
|
||
// 每次打开菜单时,重置编辑模态框状态(模仿 PncyssD 的逻辑)
|
||
// 确保每次打开菜单都显示主菜单界面,而不是编辑界面
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
// 菜单打开时,确保编辑模态框是关闭的
|
||
setShowEditModal(false);
|
||
}
|
||
}, [isOpen]);
|
||
|
||
// 点击外部关闭菜单
|
||
useEffect(() => {
|
||
if (!isOpen) return;
|
||
|
||
const handleClickOutside = (event) => {
|
||
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||
onClose();
|
||
}
|
||
};
|
||
|
||
document.addEventListener('mousedown', handleClickOutside);
|
||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||
}, [isOpen, onClose]);
|
||
|
||
// 如果菜单未打开,不渲染任何内容(除非正在显示编辑模态框)
|
||
if (!isOpen && !showEditModal) return null;
|
||
|
||
// 如果正在显示编辑模态框,只渲染编辑模态框
|
||
if (showEditModal) {
|
||
return (
|
||
<EditProfileModal
|
||
onClose={() => {
|
||
setShowEditModal(false);
|
||
// 编辑模态框关闭后,如果菜单是打开的,会显示主菜单
|
||
}}
|
||
userProfile={data.userProfile}
|
||
/>
|
||
);
|
||
}
|
||
|
||
// 显示主菜单(isOpen 为 true 且 showEditModal 为 false)
|
||
|
||
// 显示主菜单
|
||
return (
|
||
<>
|
||
{/* 菜单遮罩层 */}
|
||
<div
|
||
className="fixed inset-0 z-40"
|
||
onClick={onClose}
|
||
/>
|
||
|
||
{/* 用户菜单 */}
|
||
<div
|
||
ref={menuRef}
|
||
className="fixed top-24 md:top-4 left-4 md:left-[300px] z-50 w-[calc(100%-2rem)] md:w-80 animate-fade-in"
|
||
>
|
||
<div className="bg-[#1a1c2c]/95 border border-white/20 shadow-2xl rounded-2xl p-6 space-y-6 backdrop-blur-sm">
|
||
{/* 用户信息头部 */}
|
||
<div className="flex items-center gap-4 pb-4 border-b border-white/10">
|
||
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-primary/30 to-blue-600/30 flex items-center justify-center text-2xl font-bold text-white border border-white/20 shadow-lg">
|
||
{data.userProfile.nickname?.[0] || 'U'}
|
||
</div>
|
||
<div className="flex-1 overflow-hidden">
|
||
<div className="font-bold text-gray-100 text-lg truncate">
|
||
{data.userProfile.nickname || '旅人'}
|
||
</div>
|
||
<div className="text-xs text-primary flex items-center gap-1.5 mt-1">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-primary shadow-[0_0_5px_rgba(205,133,63,0.5)] animate-pulse"></span>
|
||
{data.userProfile.mbti || '未知'} · {data.userProfile.zodiac || '未知'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 统计数据 */}
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div className="p-4 bg-white/5 rounded-xl border border-white/5 text-center">
|
||
<div className="text-2xl font-bold text-primary">
|
||
{data.lifeTimeline?.length || 0}
|
||
</div>
|
||
<div className="text-[10px] text-white/40 uppercase tracking-widest mt-1">
|
||
生命足迹
|
||
</div>
|
||
</div>
|
||
<div className="p-4 bg-white/5 rounded-xl border border-white/5 text-center">
|
||
<div className="text-2xl font-bold text-blue-400">
|
||
{data.generatedScripts?.length || 0}
|
||
</div>
|
||
<div className="text-[10px] text-white/40 uppercase tracking-widest mt-1">
|
||
剧本生成
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="space-y-2 pt-2 border-t border-white/5">
|
||
<Button
|
||
variant="secondary"
|
||
size="md"
|
||
className="w-full justify-start"
|
||
onClick={() => {
|
||
setShowEditModal(true);
|
||
// 不关闭主菜单,让编辑模态框显示在主菜单之上
|
||
// 这样关闭编辑模态框后,主菜单仍然可见
|
||
}}
|
||
>
|
||
<Settings className="w-4 h-4 mr-2" />
|
||
编辑资料
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="md"
|
||
className="w-full justify-start text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||
onClick={() => {
|
||
if (window.confirm('确定要退出登录并清除所有数据吗?此操作不可逆。')) {
|
||
Store.reset();
|
||
if (onLogout) onLogout();
|
||
}
|
||
}}
|
||
>
|
||
<LogOut className="w-4 h-4 mr-2" />
|
||
退出登录
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 编辑资料模态框组件
|
||
*/
|
||
function EditProfileModal({ onClose, userProfile }) {
|
||
const [formData, setFormData] = useState({
|
||
nickname: userProfile.nickname || '',
|
||
mbti: userProfile.mbti || '',
|
||
zodiac: userProfile.zodiac || '',
|
||
hobbies: userProfile.hobbies?.join(', ') || '',
|
||
gender: userProfile.gender || 'secret'
|
||
});
|
||
const [isSaving, setIsSaving] = useState(false);
|
||
const [error, setError] = useState('');
|
||
|
||
const handleSave = async () => {
|
||
setIsSaving(true);
|
||
setError('');
|
||
|
||
const updatedProfile = {
|
||
nickname: formData.nickname,
|
||
mbti: formData.mbti,
|
||
zodiac: formData.zodiac,
|
||
hobbies: formData.hobbies.split(',').map(s => s.trim()).filter(s => s),
|
||
gender: formData.gender
|
||
};
|
||
|
||
try {
|
||
// 1. 更新本地 Store
|
||
Store.updateProfile(updatedProfile);
|
||
|
||
// 2. 同步到后端
|
||
const currentProfile = await userApi.getCurrentUser();
|
||
if (currentProfile.data && currentProfile.data.id) {
|
||
await userApi.updateUserProfile({
|
||
id: currentProfile.data.id,
|
||
nickname: updatedProfile.nickname,
|
||
mbti: updatedProfile.mbti,
|
||
zodiac: updatedProfile.zodiac,
|
||
hobbies: JSON.stringify(updatedProfile.hobbies),
|
||
gender: updatedProfile.gender
|
||
});
|
||
}
|
||
|
||
onClose();
|
||
} catch (e) {
|
||
console.error('保存资料失败:', e);
|
||
setError(e.response?.data?.message || '保存失败,请重试');
|
||
} finally {
|
||
setIsSaving(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-xl">
|
||
<GlassCard className="w-full max-w-lg p-8 relative max-h-[90vh] overflow-y-auto">
|
||
{/* 关闭按钮 */}
|
||
<button
|
||
onClick={onClose}
|
||
className="absolute top-6 right-6 text-white/40 hover:text-white transition-colors z-10"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
|
||
{/* 标题 */}
|
||
<div className="flex items-center gap-4 mb-8">
|
||
<div className="w-12 h-12 rounded-xl bg-primary/20 flex items-center justify-center">
|
||
<Edit2 className="text-primary w-6 h-6" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-2xl font-bold text-gray-100">编辑资料</h3>
|
||
<p className="text-xs text-gray-400 mt-1">调整你的人生航向基础信息</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 表单 */}
|
||
<div className="space-y-6">
|
||
<div>
|
||
<label className="text-sm font-medium text-gray-300 mb-2 block">昵称</label>
|
||
<input
|
||
type="text"
|
||
value={formData.nickname}
|
||
onChange={(e) => setFormData({ ...formData, nickname: e.target.value })}
|
||
placeholder="你想被如何称呼?"
|
||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition-all"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="text-sm font-medium text-gray-300 mb-2 block">MBTI</label>
|
||
<input
|
||
type="text"
|
||
value={formData.mbti}
|
||
onChange={(e) => setFormData({ ...formData, mbti: e.target.value })}
|
||
placeholder="性格色彩"
|
||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition-all"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="text-sm font-medium text-gray-300 mb-2 block">星座</label>
|
||
<input
|
||
type="text"
|
||
value={formData.zodiac}
|
||
onChange={(e) => setFormData({ ...formData, zodiac: e.target.value })}
|
||
placeholder="星辰指引"
|
||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition-all"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="text-sm font-medium text-gray-300 mb-2 block">兴趣爱好</label>
|
||
<input
|
||
type="text"
|
||
value={formData.hobbies}
|
||
onChange={(e) => setFormData({ ...formData, hobbies: e.target.value })}
|
||
placeholder="让灵魂起舞的事物(用逗号分隔)"
|
||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition-all"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="text-sm font-medium text-gray-300 mb-2 block">性别</label>
|
||
<select
|
||
value={formData.gender}
|
||
onChange={(e) => setFormData({ ...formData, gender: e.target.value })}
|
||
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition-all"
|
||
>
|
||
<option value="secret">保密</option>
|
||
<option value="male">男</option>
|
||
<option value="female">女</option>
|
||
</select>
|
||
</div>
|
||
|
||
{/* 错误提示 */}
|
||
{error && (
|
||
<div className="p-3 rounded-xl bg-red-500/10 border border-red-500/20 text-red-200 text-sm">
|
||
{error}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="flex gap-4 mt-8 pt-6 border-t border-white/5">
|
||
<Button
|
||
variant="primary"
|
||
size="md"
|
||
className="flex-1"
|
||
onClick={handleSave}
|
||
isLoading={isSaving}
|
||
>
|
||
保存修改
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="md"
|
||
onClick={onClose}
|
||
>
|
||
取消
|
||
</Button>
|
||
</div>
|
||
</GlassCard>
|
||
</div>
|
||
);
|
||
}
|
||
|