From 0000732e3bdf3f571c0c42e0bfd8fe4ae4db96ed Mon Sep 17 00:00:00 2001 From: root Date: Sat, 12 Jul 2025 17:31:46 +0800 Subject: [PATCH] fix --- package.json | 1 + python_core/database/types.py | 17 +- python_core/services/template_manager.py | 34 +- src/App.tsx | 56 +-- src/components/ProtectedRoute.tsx | 48 +++ src/components/TitleBar.tsx | 65 ++-- src/components/UserMenu.tsx | 181 ++++++++++ src/index.css | 53 +++ src/pages/LoginPage.tsx | 420 +++++++++++++++++++++++ src/services/nakamaAuth.ts | 329 ++++++++++++++++++ src/stores/useAuthStore.ts | 197 +++++++++++ 11 files changed, 1318 insertions(+), 83 deletions(-) create mode 100644 src/components/ProtectedRoute.tsx create mode 100644 src/components/UserMenu.tsx create mode 100644 src/pages/LoginPage.tsx create mode 100644 src/services/nakamaAuth.ts create mode 100644 src/stores/useAuthStore.ts diff --git a/package.json b/package.json index c109f17..e6a3ab2 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@ag-ui/core": "^0.0.33", + "@heroiclabs/nakama-js": "^2.7.0", "@tauri-apps/api": "^2.0.0", "@tauri-apps/plugin-dialog": "^2.0.0", "@tauri-apps/plugin-fs": "^2.0.0", diff --git a/python_core/database/types.py b/python_core/database/types.py index d270cb2..f4101de 100644 --- a/python_core/database/types.py +++ b/python_core/database/types.py @@ -1,5 +1,18 @@ from dataclasses import dataclass -from typing import Dict, List, Any +from typing import Dict, List, Any, Optional + + +@dataclass +class MaterialInfo: + """Material information structure""" + id: str + material_id: str + name: str + type: str # video, audio, image, text, sticker, etc. + original_path: str + relative_path: str + duration: Optional[int] = None + size: Optional[int] = None @dataclass class TemplateInfo: @@ -16,7 +29,7 @@ class TemplateInfo: material_count: int track_count: int tags: List[str] - # local/cloud + # local/cloud 如果类型是 local 那么只加载自己的 如果是 cloud 就是公用的 type: str # 创建用户 user_id: str \ No newline at end of file diff --git a/python_core/services/template_manager.py b/python_core/services/template_manager.py index e3a61b7..b1d36de 100644 --- a/python_core/services/template_manager.py +++ b/python_core/services/template_manager.py @@ -14,41 +14,9 @@ from datetime import datetime from ..utils.logger import setup_logger from ..config import settings - +from python_core.database.types import TemplateInfo, MaterialInfo logger = setup_logger(__name__) - -@dataclass -class TemplateInfo: - """Template information structure""" - id: str - name: str - description: str - thumbnail_path: str - draft_content_path: str - resources_path: str - created_at: str - updated_at: str - canvas_config: Dict[str, Any] - duration: int - material_count: int - track_count: int - tags: List[str] - - -@dataclass -class MaterialInfo: - """Material information structure""" - id: str - material_id: str - name: str - type: str # video, audio, image, text, sticker, etc. - original_path: str - relative_path: str - duration: Optional[int] = None - size: Optional[int] = None - - class TemplateManager: """Template management service""" diff --git a/src/App.tsx b/src/App.tsx index d6749b8..e6c9a52 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,8 @@ import React, { useEffect } from 'react' import { Routes, Route } from 'react-router-dom' import Layout from './components/Layout' +import ProtectedRoute from './components/ProtectedRoute' +import LoginPage from './pages/LoginPage' import HomePage from './pages/HomePage' import EditorPage from './pages/EditorPage' import AIVideoPage from './pages/AIVideoPage' @@ -17,6 +19,7 @@ import KVTestPage from './pages/KVTestPage' import TextVideoGeneratorPage from './pages/TextVideoGeneratorPage' import PythonCoreTestPage from './pages/PythonCoreTestPage' import PythonEnvManagerPage from './pages/PythonEnvManagerPage' +import { initializeAuth } from './stores/useAuthStore' import * as path from '@tauri-apps/api/path'; import { convertFileSrc } from '@tauri-apps/api/core' async function testPath() { @@ -32,33 +35,46 @@ function App() { useEffect(() => { testPath() + // 初始化认证状态 + initializeAuth() }, []) + return ( - {/* 项目详情页 - 无侧边栏布局 */} - } /> + {/* 登录页面 - 公开访问 */} + } /> - {/* 其他页面 - 带侧边栏布局 */} + {/* 受保护的路由 */} + - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + {/* 项目详情页 - 无侧边栏布局 */} + } /> + + {/* 其他页面 - 带侧边栏布局 */} + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + } /> - + } /> ) diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..c98532d --- /dev/null +++ b/src/components/ProtectedRoute.tsx @@ -0,0 +1,48 @@ +/** + * 受保护的路由组件 + * 检查用户认证状态,未登录时重定向到登录页面 + */ + +import React, { useEffect } from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuthStore, initializeAuth } from '../stores/useAuthStore'; +import { Loader2 } from 'lucide-react'; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +const ProtectedRoute: React.FC = ({ children }) => { + const { isAuthenticated, loading, getCurrentUser } = useAuthStore(); + const location = useLocation(); + + useEffect(() => { + // 初始化认证状态 + initializeAuth(); + }, []); + + // 如果正在加载,显示加载界面 + if (loading) { + return ( +
+
+
+ +
+

正在验证身份

+

请稍候...

+
+
+ ); + } + + // 如果未认证,重定向到登录页面 + if (!isAuthenticated) { + return ; + } + + // 如果已认证,渲染子组件 + return <>{children}; +}; + +export default ProtectedRoute; diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index abffe10..7790c7e 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react' import { Minimize2, Maximize2, Square, X } from 'lucide-react' import { getCurrentWindow } from '@tauri-apps/api/window' +import UserMenu from './UserMenu' const TitleBar: React.FC = () => { const [isMaximized, setIsMaximized] = useState(false) @@ -128,34 +129,42 @@ const TitleBar: React.FC = () => { MixVideo V2 -
- - - +
+ {/* 用户菜单 */} +
e.stopPropagation()}> + +
+ + {/* 窗口控制按钮 */} +
+ + + +
) diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx new file mode 100644 index 0000000..92ec25e --- /dev/null +++ b/src/components/UserMenu.tsx @@ -0,0 +1,181 @@ +/** + * 用户菜单组件 + * 显示用户头像、信息和操作菜单 + */ + +import React, { useState, useRef, useEffect } from 'react'; +import { + User, + Settings, + LogOut, + ChevronDown, + Shield, + Edit3 +} from 'lucide-react'; +import { useAuthStore } from '../stores/useAuthStore'; +import { useNavigate } from 'react-router-dom'; + +const UserMenu: React.FC = () => { + const { user, logout } = useAuthStore(); + const navigate = useNavigate(); + const [isOpen, setIsOpen] = useState(false); + const menuRef = useRef(null); + + // 点击外部关闭菜单 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const handleLogout = async () => { + try { + await logout(); + navigate('/login'); + } catch (error) { + console.error('Logout failed:', error); + } + }; + + const getAvatarUrl = () => { + if (user?.avatarUrl) { + return user.avatarUrl; + } + // 生成基于用户名的头像 + const name = user?.displayName || user?.username || 'User'; + return `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=2563eb&color=ffffff&size=40`; + }; + + const getUserInitials = () => { + const name = user?.displayName || user?.username || 'User'; + return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2); + }; + + if (!user) { + return null; + } + + return ( +
+ {/* 用户头像按钮 */} + + + {/* 下拉菜单 */} + {isOpen && ( +
+ {/* 用户信息 */} +
+
+ {user.displayName +
+

+ {user.displayName || user.username} +

+

{user.email}

+
+ + 已验证 +
+
+
+
+ + {/* 菜单项 */} +
+ + + + + +
+ + {/* 分割线 */} +
+ + {/* 登出 */} + +
+ )} +
+ ); +}; + +export default UserMenu; diff --git a/src/index.css b/src/index.css index dd29536..ca150ef 100644 --- a/src/index.css +++ b/src/index.css @@ -48,6 +48,59 @@ .timeline-track { @apply relative h-16 bg-secondary-200 border-b border-secondary-300; } + + /* 登录页面特殊样式 */ + .login-bg-pattern { + background-image: + radial-gradient(circle at 25% 25%, rgba(59, 130, 246, 0.1) 0%, transparent 50%), + radial-gradient(circle at 75% 75%, rgba(147, 51, 234, 0.1) 0%, transparent 50%); + } + + .login-glass { + backdrop-filter: blur(20px); + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + } + + .login-input { + backdrop-filter: blur(10px); + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + transition: all 0.3s ease; + } + + .login-input:focus { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(59, 130, 246, 0.5); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); + } + + .login-button { + background: linear-gradient(135deg, #2563eb 0%, #3b82f6 50%, #1d4ed8 100%); + transition: all 0.3s ease; + position: relative; + overflow: hidden; + } + + .login-button:hover { + transform: translateY(-1px); + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3); + } + + .login-button:before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.5s; + } + + .login-button:hover:before { + left: 100%; + } .timeline-clip { @apply absolute top-0 h-full bg-primary-500 rounded border border-primary-600 cursor-pointer hover:bg-primary-600 transition-colors; diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx new file mode 100644 index 0000000..de03a77 --- /dev/null +++ b/src/pages/LoginPage.tsx @@ -0,0 +1,420 @@ +/** + * 登录页面 + * 美观大气的科技风登录界面,集成Nakama用户系统 + */ + +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + User, + Lock, + Mail, + Eye, + EyeOff, + Zap, + Shield, + Cpu, + Play, + Sparkles, + ArrowRight +} from 'lucide-react'; +import { useAuthStore } from '../stores/useAuthStore'; + +interface LoginFormData { + email: string; + password: string; + rememberMe: boolean; +} + +interface RegisterFormData { + email: string; + username: string; + password: string; + confirmPassword: string; + displayName: string; +} + +const LoginPage: React.FC = () => { + const navigate = useNavigate(); + const { login, register, loading, error, clearError, isAuthenticated } = useAuthStore(); + + const [isRegisterMode, setIsRegisterMode] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + + const [loginForm, setLoginForm] = useState({ + email: '', + password: '', + rememberMe: false + }); + + const [registerForm, setRegisterForm] = useState({ + email: '', + username: '', + password: '', + confirmPassword: '', + displayName: '' + }); + + // 如果已登录,重定向到首页 + useEffect(() => { + if (isAuthenticated) { + navigate('/'); + } + }, [isAuthenticated, navigate]); + + // 清除错误信息 + useEffect(() => { + clearError(); + }, [isRegisterMode, clearError]); + + const handleLoginSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + await login({ + email: loginForm.email, + password: loginForm.password + }); + navigate('/'); + } catch (error) { + // 错误已在store中处理 + } + }; + + const handleRegisterSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (registerForm.password !== registerForm.confirmPassword) { + return; + } + + try { + await register({ + email: registerForm.email, + username: registerForm.username, + password: registerForm.password, + displayName: registerForm.displayName || registerForm.username + }); + navigate('/'); + } catch (error) { + // 错误已在store中处理 + } + }; + + const toggleMode = () => { + setIsRegisterMode(!isRegisterMode); + clearError(); + }; + + + return ( +
+ {/* 背景动画元素 */} +
+
+
+
+
+ + {/* 网格背景 */} +
+ +
+ {/* 左侧 - 品牌展示 */} +
+
+
+
+ +
+

MixVideo

+
+ +

+ 智能视频混剪 +
+ + 创作平台 + +

+ +

+ 基于AI技术的专业视频混剪工具,让创作更简单,让想象成为现实 +

+
+ + {/* 特性展示 */} +
+
+
+ +
+
+

智能场景检测

+

AI驱动的视频场景自动识别与分割

+
+
+ +
+
+ +
+
+

高性能处理

+

多核并行处理,快速完成视频编辑任务

+
+
+ +
+
+ +
+
+

模板系统

+

丰富的视频模板库,快速创建专业作品

+
+
+
+
+ + {/* 右侧 - 登录表单 */} +
+
+ {/* 表单头部 */} +
+
+ +
+

+ {isRegisterMode ? '创建账户' : '欢迎回来'} +

+

+ {isRegisterMode ? '加入MixVideo,开始您的创作之旅' : '登录您的账户继续使用'} +

+
+ + {/* 错误提示 */} + {error && ( +
+

{error}

+
+ )} + + {/* 登录表单 */} + {!isRegisterMode ? ( +
+
+ +
+ + setLoginForm({ ...loginForm, email: e.target.value })} + className="w-full pl-10 pr-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-secondary-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all" + placeholder="请输入邮箱地址" + /> +
+
+ +
+ +
+ + setLoginForm({ ...loginForm, password: e.target.value })} + className="w-full pl-10 pr-12 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-secondary-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all" + placeholder="请输入密码" + /> + +
+
+ +
+ + +
+ + +
+ ) : ( + /* 注册表单 */ +
+
+
+ +
+ + setRegisterForm({ ...registerForm, username: e.target.value })} + className="w-full pl-10 pr-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-secondary-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all" + placeholder="用户名" + /> +
+
+ +
+ + setRegisterForm({ ...registerForm, displayName: e.target.value })} + className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-secondary-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all" + placeholder="显示名称" + /> +
+
+ +
+ +
+ + setRegisterForm({ ...registerForm, email: e.target.value })} + className="w-full pl-10 pr-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-secondary-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all" + placeholder="请输入邮箱地址" + /> +
+
+ +
+ +
+ + setRegisterForm({ ...registerForm, password: e.target.value })} + className="w-full pl-10 pr-12 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-secondary-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all" + placeholder="请输入密码" + /> + +
+
+ +
+ +
+ + setRegisterForm({ ...registerForm, confirmPassword: e.target.value })} + className="w-full pl-10 pr-12 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-secondary-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all" + placeholder="请再次输入密码" + /> + +
+ {registerForm.password && registerForm.confirmPassword && registerForm.password !== registerForm.confirmPassword && ( +

密码不匹配

+ )} +
+ + +
+ )} + + {/* 切换模式 */} +
+

+ {isRegisterMode ? '已有账户?' : '还没有账户?'} + +

+
+
+
+
+
+ ); +}; + +export default LoginPage; diff --git a/src/services/nakamaAuth.ts b/src/services/nakamaAuth.ts new file mode 100644 index 0000000..e09336e --- /dev/null +++ b/src/services/nakamaAuth.ts @@ -0,0 +1,329 @@ +/** + * Nakama认证服务 + * 处理用户登录、注册、会话管理等功能 + */ + +import { Client, Session } from '@heroiclabs/nakama-js'; + +export interface LoginCredentials { + email?: string; + username?: string; + password: string; +} + +export interface RegisterCredentials { + email: string; + username: string; + password: string; + displayName?: string; +} + +export interface UserProfile { + id: string; + username: string; + displayName: string; + email?: string; + avatarUrl?: string; + createTime: string; + updateTime: string; +} + +export interface AuthState { + isAuthenticated: boolean; + user: UserProfile | null; + session: Session | null; + loading: boolean; + error: string | null; +} + +class NakamaAuthService { + private client: Client; + private session: Session | null = null; + private readonly SERVER_KEY = 'defaultkey'; // 替换为实际的服务器密钥 + private readonly HOST = 'https://nakama.bowong.cc'; // 替换为实际的Nakama服务器地址 + private readonly PORT = '443'; // 替换为实际的端口 + private readonly USE_SSL = true; // 根据实际情况设置 + + constructor() { + this.client = new Client(this.SERVER_KEY, this.HOST, this.PORT, this.USE_SSL); + this.loadStoredSession(); + } + + /** + * 从本地存储加载会话 + */ + private loadStoredSession(): void { + try { + const storedSession = localStorage.getItem('nakama_session'); + if (storedSession) { + const sessionData = JSON.parse(storedSession); + this.session = Session.restore( + sessionData.token, + sessionData.refresh_token, + sessionData.username, + sessionData.user_id, + sessionData.created, + sessionData.expires_at, + sessionData.vars + ); + + // 检查会话是否过期 + if (this.session.isexpired(Date.now() / 1000)) { + this.refreshSession(); + } + } + } catch (error) { + console.error('Failed to load stored session:', error); + this.clearStoredSession(); + } + } + + /** + * 保存会话到本地存储 + */ + private saveSession(session: Session): void { + try { + const sessionData = { + token: session.token, + refresh_token: session.refresh_token, + username: session.username, + user_id: session.user_id, + created: session.created, + expires_at: session.expires_at, + vars: session.vars + }; + localStorage.setItem('nakama_session', JSON.stringify(sessionData)); + this.session = session; + } catch (error) { + console.error('Failed to save session:', error); + } + } + + /** + * 清除本地存储的会话 + */ + private clearStoredSession(): void { + localStorage.removeItem('nakama_session'); + this.session = null; + } + + /** + * 刷新会话 + */ + private async refreshSession(): Promise { + if (!this.session?.refresh_token) { + throw new Error('No refresh token available'); + } + + try { + const newSession = await this.client.sessionRefresh(this.session); + this.saveSession(newSession); + } catch (error) { + console.error('Failed to refresh session:', error); + this.clearStoredSession(); + throw error; + } + } + + /** + * 用户登录 + */ + async login(credentials: LoginCredentials): Promise { + try { + let session: Session; + + if (credentials.email) { + // 邮箱登录 + session = await this.client.authenticateEmail(credentials.email, credentials.password); + } else if (credentials.username) { + // 用户名登录 + session = await this.client.authenticateUsername(credentials.username, credentials.password); + } else { + throw new Error('Email or username is required'); + } + + this.saveSession(session); + + // 获取用户信息 + const account = await this.client.getAccount(session); + + return { + id: account.user?.id || '', + username: account.user?.username || '', + displayName: account.user?.display_name || account.user?.username || '', + email: account.email, + avatarUrl: account.user?.avatar_url, + createTime: account.user?.create_time || '', + updateTime: account.user?.update_time || '' + }; + } catch (error) { + console.error('Login failed:', error); + throw new Error(this.getErrorMessage(error)); + } + } + + /** + * 用户注册 + */ + async register(credentials: RegisterCredentials): Promise { + try { + const session = await this.client.authenticateEmail( + credentials.email, + credentials.password, + true, // create account if not exists + credentials.username, + credentials.displayName + ); + + this.saveSession(session); + + // 获取用户信息 + const account = await this.client.getAccount(session); + + return { + id: account.user?.id || '', + username: account.user?.username || '', + displayName: account.user?.display_name || credentials.displayName || credentials.username, + email: account.email, + avatarUrl: account.user?.avatar_url, + createTime: account.user?.create_time || '', + updateTime: account.user?.update_time || '' + }; + } catch (error) { + console.error('Registration failed:', error); + throw new Error(this.getErrorMessage(error)); + } + } + + /** + * 用户登出 + */ + async logout(): Promise { + try { + if (this.session) { + await this.client.sessionLogout(this.session); + } + } catch (error) { + console.error('Logout error:', error); + } finally { + this.clearStoredSession(); + } + } + + /** + * 获取当前用户信息 + */ + async getCurrentUser(): Promise { + if (!this.session) { + return null; + } + + try { + // 检查会话是否过期 + if (this.session.isexpired(Date.now() / 1000)) { + await this.refreshSession(); + } + + const account = await this.client.getAccount(this.session); + + return { + id: account.user?.id || '', + username: account.user?.username || '', + displayName: account.user?.display_name || account.user?.username || '', + email: account.email, + avatarUrl: account.user?.avatar_url, + createTime: account.user?.create_time || '', + updateTime: account.user?.update_time || '' + }; + } catch (error) { + console.error('Failed to get current user:', error); + this.clearStoredSession(); + return null; + } + } + + /** + * 检查是否已登录 + */ + isAuthenticated(): boolean { + return this.session !== null && !this.session.isexpired(Date.now() / 1000); + } + + /** + * 获取当前会话 + */ + getSession(): Session | null { + return this.session; + } + + /** + * 更新用户资料 + */ + async updateProfile(updates: Partial>): Promise { + if (!this.session) { + throw new Error('Not authenticated'); + } + + try { + await this.client.updateAccount(this.session, { + display_name: updates.displayName, + avatar_url: updates.avatarUrl + }); + } catch (error) { + console.error('Failed to update profile:', error); + throw new Error(this.getErrorMessage(error)); + } + } + + /** + * 修改密码 + */ + async changePassword(currentPassword: string, newPassword: string): Promise { + if (!this.session) { + throw new Error('Not authenticated'); + } + + try { + // 首先验证当前密码 + const account = await this.client.getAccount(this.session); + if (account.email) { + await this.client.authenticateEmail(account.email, currentPassword); + } + + // 更新密码(这里需要根据Nakama的实际API调整) + // Nakama可能需要通过其他方式更新密码 + console.warn('Password change not implemented - requires server-side function'); + throw new Error('Password change requires server-side implementation'); + } catch (error) { + console.error('Failed to change password:', error); + throw new Error(this.getErrorMessage(error)); + } + } + + /** + * 获取错误消息 + */ + private getErrorMessage(error: any): string { + if (error?.message) { + return error.message; + } + + // 根据Nakama错误码返回友好的错误消息 + switch (error?.code) { + case 3: // INVALID_ARGUMENT + return '输入参数无效'; + case 5: // NOT_FOUND + return '用户不存在'; + case 6: // ALREADY_EXISTS + return '用户已存在'; + case 16: // UNAUTHENTICATED + return '用户名或密码错误'; + default: + return '操作失败,请稍后重试'; + } + } +} + +// 导出单例实例 +export const nakamaAuth = new NakamaAuthService(); +export default nakamaAuth; diff --git a/src/stores/useAuthStore.ts b/src/stores/useAuthStore.ts new file mode 100644 index 0000000..5864c7c --- /dev/null +++ b/src/stores/useAuthStore.ts @@ -0,0 +1,197 @@ +/** + * 认证状态管理Store + * 使用Zustand管理用户认证状态 + */ + +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import { nakamaAuth, UserProfile, LoginCredentials, RegisterCredentials } from '../services/nakamaAuth'; + +interface AuthState { + // 状态 + isAuthenticated: boolean; + user: UserProfile | null; + loading: boolean; + error: string | null; + + // 操作 + login: (credentials: LoginCredentials) => Promise; + register: (credentials: RegisterCredentials) => Promise; + logout: () => Promise; + getCurrentUser: () => Promise; + updateProfile: (updates: Partial>) => Promise; + clearError: () => void; + setLoading: (loading: boolean) => void; +} + +export const useAuthStore = create()( + persist( + (set, get) => ({ + // 初始状态 + isAuthenticated: false, + user: null, + loading: false, + error: null, + + // 登录 + login: async (credentials: LoginCredentials) => { + set({ loading: true, error: null }); + + try { + const user = await nakamaAuth.login(credentials); + set({ + isAuthenticated: true, + user, + loading: false, + error: null + }); + } catch (error) { + set({ + loading: false, + error: error instanceof Error ? error.message : '登录失败', + isAuthenticated: false, + user: null + }); + throw error; + } + }, + + // 注册 + register: async (credentials: RegisterCredentials) => { + set({ loading: true, error: null }); + + try { + const user = await nakamaAuth.register(credentials); + set({ + isAuthenticated: true, + user, + loading: false, + error: null + }); + } catch (error) { + set({ + loading: false, + error: error instanceof Error ? error.message : '注册失败', + isAuthenticated: false, + user: null + }); + throw error; + } + }, + + // 登出 + logout: async () => { + set({ loading: true }); + + try { + await nakamaAuth.logout(); + set({ + isAuthenticated: false, + user: null, + loading: false, + error: null + }); + } catch (error) { + console.error('Logout error:', error); + // 即使登出失败,也要清除本地状态 + set({ + isAuthenticated: false, + user: null, + loading: false, + error: null + }); + } + }, + + // 获取当前用户信息 + getCurrentUser: async () => { + set({ loading: true, error: null }); + + try { + const user = await nakamaAuth.getCurrentUser(); + if (user) { + set({ + isAuthenticated: true, + user, + loading: false + }); + } else { + set({ + isAuthenticated: false, + user: null, + loading: false + }); + } + } catch (error) { + set({ + loading: false, + error: error instanceof Error ? error.message : '获取用户信息失败', + isAuthenticated: false, + user: null + }); + } + }, + + // 更新用户资料 + updateProfile: async (updates: Partial>) => { + set({ loading: true, error: null }); + + try { + await nakamaAuth.updateProfile(updates); + + // 更新本地用户信息 + const currentUser = get().user; + if (currentUser) { + set({ + user: { ...currentUser, ...updates }, + loading: false + }); + } + } catch (error) { + set({ + loading: false, + error: error instanceof Error ? error.message : '更新资料失败' + }); + throw error; + } + }, + + // 清除错误 + clearError: () => { + set({ error: null }); + }, + + // 设置加载状态 + setLoading: (loading: boolean) => { + set({ loading }); + } + }), + { + name: 'auth-storage', + partialize: (state) => ({ + isAuthenticated: state.isAuthenticated, + user: state.user + }) + } + ) +); + +// 初始化时检查认证状态 +export const initializeAuth = async () => { + const { getCurrentUser } = useAuthStore.getState(); + + // 检查是否有有效的会话 + if (nakamaAuth.isAuthenticated()) { + await getCurrentUser(); + } else { + // 清除过期的状态 + useAuthStore.setState({ + isAuthenticated: false, + user: null, + loading: false, + error: null + }); + } +}; + +export default useAuthStore;