diff --git a/app/components/ui/ProjectSelector.tsx b/app/components/ui/ProjectSelector.tsx index 9ca1d57..9898248 100644 --- a/app/components/ui/ProjectSelector.tsx +++ b/app/components/ui/ProjectSelector.tsx @@ -99,32 +99,42 @@ export function ProjectSelector({ setLoading(true); setError(null); + console.log(`开始获取项目数据,用户ID: ${userId}, 团队ID过滤: ${effectiveTeamIds?.join(', ') || '无'}`); + try { const supabase = getSupabaseClient(); + console.log('Supabase客户端已创建,准备获取项目数据'); if (effectiveTeamIds && effectiveTeamIds.length > 0) { // If team IDs are provided, get projects for those teams + console.log(`通过团队ID获取项目: ${effectiveTeamIds.join(', ')}`); + const { data: projectsData, error: projectsError } = await supabase .from('team_projects') .select('project_id, projects:project_id(*), teams:team_id(name)') .in('team_id', effectiveTeamIds) .is('projects.deleted_at', null); + console.log(`团队项目查询结果:`, projectsData ? `找到${projectsData.length}个` : '无数据', + projectsError ? `错误: ${projectsError.message}` : '无错误'); + if (projectsError) throw projectsError; if (!projectsData || projectsData.length === 0) { + console.log('未找到团队项目,返回空列表'); if (isMounted) setProjects([]); return; } // Extract projects from response with team info if (isMounted) { + console.log('处理团队项目数据'); const projectList: Project[] = []; - for (const item of projectsData as ProjectWithTeam[]) { + for (const item of projectsData as unknown as ProjectWithTeam[]) { if (item.projects && typeof item.projects === 'object' && 'id' in item.projects && 'name' in item.projects) { const project = item.projects as Project; - if (item.teams && 'name' in item.teams) { + if (item.teams && typeof item.teams === 'object' && 'name' in item.teams) { project.team_name = item.teams.name; } // Avoid duplicate projects from different teams @@ -134,32 +144,42 @@ export function ProjectSelector({ } } + console.log(`处理后的项目数据: ${projectList.length}个`); setProjects(projectList); } } else { // If no team IDs, get all user's projects + console.log(`获取用户所有项目,用户ID: ${userId}`); + const { data: projectsData, error: projectsError } = await supabase .from('user_projects') .select('project_id, projects:project_id(*)') .eq('user_id', userId) .is('projects.deleted_at', null); + console.log(`用户项目查询结果:`, projectsData ? `找到${projectsData.length}个` : '无数据', + projectsError ? `错误: ${projectsError.message}` : '无错误'); + if (projectsError) throw projectsError; if (!projectsData || projectsData.length === 0) { + console.log('未找到用户项目,返回空列表'); if (isMounted) setProjects([]); return; } // Fetch team info for these projects const projectIds = projectsData.map(item => item.project_id); + console.log(`获取项目的团队信息,项目IDs: ${projectIds.join(', ')}`); // Get team info for each project const { data: teamProjectsData, error: teamProjectsError } = await supabase .from('team_projects') .select('project_id, teams:team_id(name)') .in('project_id', projectIds); - + + console.log(`项目团队关系查询结果:`, teamProjectsData ? `找到${teamProjectsData.length}个` : '无数据'); + if (teamProjectsError) throw teamProjectsError; // Create project ID to team name mapping @@ -174,6 +194,7 @@ export function ProjectSelector({ // Extract projects with team names if (isMounted && projectsData) { + console.log('处理用户项目数据'); const projectList: Project[] = []; for (const item of projectsData) { @@ -184,12 +205,14 @@ export function ProjectSelector({ } } + console.log(`处理后的项目数据: ${projectList.length}个`); setProjects(projectList); } } } catch (err) { + console.error('获取项目数据出错:', err); if (isMounted) { - setError(err instanceof Error ? err.message : 'Failed to load projects'); + setError(err instanceof Error ? err.message : '获取项目数据失败'); } } finally { if (isMounted) { @@ -198,27 +221,51 @@ export function ProjectSelector({ } }; - const supabase = getSupabaseClient(); - - const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => { - if (event === 'SIGNED_IN' && session?.user?.id) { - fetchProjects(session.user.id); - } else if (event === 'SIGNED_OUT') { - setProjects([]); - setError(null); - } - }); + // 获取Supabase客户端实例并订阅认证状态变化 + try { + const supabase = getSupabaseClient(); + console.log('注册项目选择器认证状态变化监听器'); + + const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => { + console.log(`项目选择器认证状态变化: ${event}`, session ? `用户ID: ${session.user.id}` : '无会话'); + if (event === 'SIGNED_IN' && session?.user?.id) { + fetchProjects(session.user.id); + } else if (event === 'SIGNED_OUT') { + setProjects([]); + setError(null); + } + }); - supabase.auth.getSession().then(({ data: { session } }) => { - if (session?.user?.id) { - fetchProjects(session.user.id); - } - }); + // 初始化时获取当前会话 + console.log('项目选择器获取当前会话状态'); + supabase.auth.getSession().then(({ data: { session } }) => { + console.log('项目选择器当前会话状态:', session ? `用户已登录,ID: ${session.user.id}` : '用户未登录'); + if (session?.user?.id) { + fetchProjects(session.user.id); + } else { + // 如果没有会话但组件需要初始化,可以设置加载完成 + setLoading(false); + } + }).catch(err => { + console.error('项目选择器获取会话状态失败:', err); + // 确保即使获取会话失败也停止加载状态 + setLoading(false); + }); - return () => { - isMounted = false; - subscription.unsubscribe(); - }; + return () => { + console.log('ProjectSelector组件卸载,清理订阅'); + isMounted = false; + subscription.unsubscribe(); + }; + } catch (initError) { + console.error('初始化ProjectSelector出错:', initError); + // 确保在初始化出错时也停止加载状态 + setLoading(false); + setError('初始化失败,请刷新页面重试'); + return () => { + isMounted = false; + }; + } }, [effectiveTeamIds]); const handleToggle = () => { diff --git a/app/components/ui/TagSelector.tsx b/app/components/ui/TagSelector.tsx index f687678..e194de7 100644 --- a/app/components/ui/TagSelector.tsx +++ b/app/components/ui/TagSelector.tsx @@ -123,31 +123,48 @@ export function TagSelector({ setLoading(true); setError(null); + console.log(`开始获取标签数据, 团队ID过滤: ${effectiveTeamIds?.join(', ') || '无'}`); + try { const supabase = getSupabaseClient(); + console.log('Supabase客户端已创建,准备获取标签数据'); let query = supabase.from('tags').select('*').is('deleted_at', null); // Filter by team if teamId is provided if (effectiveTeamIds) { + console.log(`通过团队ID过滤标签: ${effectiveTeamIds.join(', ')}`); query = query.in('team_id', effectiveTeamIds); } const { data: tagsData, error: tagsError } = await query; + console.log(`标签查询结果:`, tagsData ? `找到${tagsData.length}个` : '无数据', + tagsError ? `错误: ${tagsError.message}` : '无错误'); + if (tagsError) throw tagsError; if (!tagsData || tagsData.length === 0) { + console.log('未找到标签,返回空列表'); if (isMounted) setTags([]); return; } if (isMounted) { + console.log(`设置${tagsData.length}个标签数据`); setTags(tagsData as Tag[]); + + // 如果已有value但tags刚加载好,重新设置selectedIds + if (value && tagsData.length > 0) { + const ids = nameToId(value); + console.log(`根据名称设置选中的标签ID: ${ids.join(', ')}`); + setSelectedIds(ids); + } } } catch (err) { + console.error('获取标签数据出错:', err); if (isMounted) { - setError(err instanceof Error ? err.message : 'Failed to load tags'); + setError(err instanceof Error ? err.message : '获取标签数据失败'); } } finally { if (isMounted) { @@ -156,26 +173,40 @@ export function TagSelector({ } }; - const supabase = getSupabaseClient(); - - const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent) => { - if (event === 'SIGNED_IN') { - fetchTags(); - } else if (event === 'SIGNED_OUT') { - setTags([]); - setError(null); - } - }); + // 获取Supabase客户端实例并订阅认证状态变化 + try { + const supabase = getSupabaseClient(); + console.log('注册标签选择器认证状态变化监听器'); + + const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent) => { + console.log(`标签选择器认证状态变化: ${event}`); + if (event === 'SIGNED_IN') { + fetchTags(); + } else if (event === 'SIGNED_OUT') { + setTags([]); + setError(null); + } + }); - supabase.auth.getSession().then(() => { + // 初始化时获取标签数据 + console.log('标签选择器初始化,获取标签数据'); fetchTags(); - }); - return () => { - isMounted = false; - subscription.unsubscribe(); - }; - }, [effectiveTeamIds]); + return () => { + console.log('TagSelector组件卸载,清理订阅'); + isMounted = false; + subscription.unsubscribe(); + }; + } catch (initError) { + console.error('初始化TagSelector出错:', initError); + // 确保在初始化出错时也停止加载状态 + setLoading(false); + setError('初始化失败,请刷新页面重试'); + return () => { + isMounted = false; + }; + } + }, [effectiveTeamIds, nameToId, value]); const handleToggle = () => { if (!loading && !error && tags.length > 0) { diff --git a/app/components/ui/TeamSelector.tsx b/app/components/ui/TeamSelector.tsx index 79cd3a6..9aa890a 100644 --- a/app/components/ui/TeamSelector.tsx +++ b/app/components/ui/TeamSelector.tsx @@ -68,44 +68,98 @@ export function TeamSelector({ setLoading(true); setError(null); + console.log(`开始获取团队数据,用户ID: ${userId}`); + try { const supabase = getSupabaseClient(); + console.log('Supabase客户端已创建,准备获取团队数据'); - // 尝试创建默认团队和项目(如果用户还没有) + // 先尝试直接获取团队数据,不等待create-default try { - const response = await limqRequest('team/create-default', 'POST'); - console.log('Default team creation response:', response); - } catch (teamError) { - console.error('Error creating default team:', teamError); - } - - const { data: memberships, error: membershipError } = await supabase - .from('team_membership') - .select('team_id') - .eq('user_id', userId); + const { data: memberships, error: membershipError } = await supabase + .from('team_membership') + .select('team_id') + .eq('user_id', userId); - if (membershipError) throw membershipError; + console.log(`团队成员关系查询结果:`, memberships ? `找到${memberships.length}个` : '无数据', membershipError ? `错误: ${membershipError.message}` : '无错误'); - if (!memberships || memberships.length === 0) { - if (isMounted) setTeams([]); - return; - } + if (membershipError) throw membershipError; - const teamIds = memberships.map(m => m.team_id); - const { data: teamsData, error: teamsError } = await supabase - .from('teams') - .select('*') - .in('id', teamIds) - .is('deleted_at', null); + if (!memberships || memberships.length === 0) { + console.log('未找到团队成员关系,尝试创建默认团队'); + + // 尝试创建默认团队和项目(如果用户还没有) + try { + const response = await limqRequest('team/create-default', 'POST'); + console.log('默认团队创建成功:', response); + + // 创建默认团队后重新获取团队列表 + const { data: refreshedMemberships, error: refreshError } = await supabase + .from('team_membership') + .select('team_id') + .eq('user_id', userId); + + console.log(`刷新后的团队成员关系:`, refreshedMemberships ? `找到${refreshedMemberships.length}个` : '无数据'); + + if (refreshError) throw refreshError; + + if (!refreshedMemberships || refreshedMemberships.length === 0) { + if (isMounted) { + console.log('创建默认团队后仍未找到团队,设置空团队列表'); + setTeams([]); + } + return; + } + + const teamIds = refreshedMemberships.map(m => m.team_id); + console.log('获取到团队IDs:', teamIds); + + const { data: teamsData, error: teamsError } = await supabase + .from('teams') + .select('*') + .in('id', teamIds) + .is('deleted_at', null); + + console.log(`团队数据查询结果:`, teamsData ? `找到${teamsData.length}个` : '无数据'); - if (teamsError) throw teamsError; + if (teamsError) throw teamsError; - if (isMounted && teamsData) { - setTeams(teamsData); + if (isMounted && teamsData) { + setTeams(teamsData); + } + return; + } catch (teamError) { + console.error('创建默认团队失败:', teamError); + // 创建失败也继续,返回空列表 + if (isMounted) setTeams([]); + return; + } + } + + const teamIds = memberships.map(m => m.team_id); + console.log('获取到团队IDs:', teamIds); + + const { data: teamsData, error: teamsError } = await supabase + .from('teams') + .select('*') + .in('id', teamIds) + .is('deleted_at', null); + + console.log(`团队数据查询结果:`, teamsData ? `找到${teamsData.length}个` : '无数据'); + + if (teamsError) throw teamsError; + + if (isMounted && teamsData) { + setTeams(teamsData); + } + } catch (dataError) { + console.error('获取团队数据失败:', dataError); + throw dataError; } } catch (err) { + console.error('获取团队数据出错:', err); if (isMounted) { - setError(err instanceof Error ? err.message : 'Failed to load teams'); + setError(err instanceof Error ? err.message : '获取团队数据失败'); } } finally { if (isMounted) { @@ -114,27 +168,51 @@ export function TeamSelector({ } }; - const supabase = getSupabaseClient(); - - const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => { - if (event === 'SIGNED_IN' && session?.user?.id) { - fetchTeams(session.user.id); - } else if (event === 'SIGNED_OUT') { - setTeams([]); - setError(null); - } - }); + // 获取Supabase客户端实例并订阅认证状态变化 + try { + const supabase = getSupabaseClient(); + console.log('注册认证状态变化监听器'); + + const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => { + console.log(`认证状态变化: ${event}`, session ? `用户ID: ${session.user.id}` : '无会话'); + if (event === 'SIGNED_IN' && session?.user?.id) { + fetchTeams(session.user.id); + } else if (event === 'SIGNED_OUT') { + setTeams([]); + setError(null); + } + }); - supabase.auth.getSession().then(({ data: { session } }) => { - if (session?.user?.id) { - fetchTeams(session.user.id); - } - }); + // 初始化时获取当前会话 + console.log('获取当前会话状态'); + supabase.auth.getSession().then(({ data: { session } }) => { + console.log('当前会话状态:', session ? `用户已登录,ID: ${session.user.id}` : '用户未登录'); + if (session?.user?.id) { + fetchTeams(session.user.id); + } else { + // 如果没有会话但组件需要初始化,可以设置加载完成 + setLoading(false); + } + }).catch(err => { + console.error('获取会话状态失败:', err); + // 确保即使获取会话失败也停止加载状态 + setLoading(false); + }); - return () => { - isMounted = false; - subscription.unsubscribe(); - }; + return () => { + console.log('TeamSelector组件卸载,清理订阅'); + isMounted = false; + subscription.unsubscribe(); + }; + } catch (initError) { + console.error('初始化TeamSelector出错:', initError); + // 确保在初始化出错时也停止加载状态 + setLoading(false); + setError('初始化失败,请刷新页面重试'); + return () => { + isMounted = false; + }; + } }, []); const handleToggle = () => { diff --git a/app/utils/supabase.ts b/app/utils/supabase.ts index ab645b7..5165c78 100644 --- a/app/utils/supabase.ts +++ b/app/utils/supabase.ts @@ -3,11 +3,12 @@ import type { Database } from "@/types/supabase"; let supabase: SupabaseClient | null = null; -// 简单的存储适配器,使用localStorage +// 增强的存储适配器,使用localStorage并添加更多错误处理 const storageAdapter = { getItem: async (key: string) => { try { const item = localStorage.getItem(key); + console.log(`Storage get for key [${key}]: ${item ? "found" : "not found"}`); return item; } catch (error) { console.error("Storage get error:", error); @@ -18,6 +19,7 @@ const storageAdapter = { setItem: async (key: string, value: string) => { try { localStorage.setItem(key, value); + console.log(`Storage set for key [${key}] successful`); } catch (error) { console.error("Storage set error:", error); } @@ -26,18 +28,42 @@ const storageAdapter = { removeItem: async (key: string) => { try { localStorage.removeItem(key); + console.log(`Storage remove for key [${key}] successful`); } catch (error) { console.error("Storage remove error:", error); } }, }; +// 添加一个函数来检查Supabase连接状态 +export const checkSupabaseConnection = async (): Promise => { + try { + const client = getSupabaseClient(); + const { error } = await client.from('_health').select('*').limit(1); + + if (error) { + console.error('Supabase connection check failed:', error); + return false; + } + + console.log('Supabase connection check successful'); + return true; + } catch (error) { + console.error('Supabase connection check exception:', error); + return false; + } +}; + export const getSupabaseClient = (): SupabaseClient => { if (!supabase) { if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) { + console.error('Missing Supabase environment variables'); throw new Error('Missing Supabase environment variables'); } + console.log('Creating new Supabase client with URL:', process.env.NEXT_PUBLIC_SUPABASE_URL); + + // 使用as断言来避免类型错误 supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, @@ -47,13 +73,27 @@ export const getSupabaseClient = (): SupabaseClient => { storage: storageAdapter, persistSession: true, autoRefreshToken: true, + detectSessionInUrl: true, }, } - ); + ) as SupabaseClient; + + // 立即检查客户端创建后的会话状态 + void supabase.auth.getSession().then(({ data: { session } }) => { + console.log('Initial session check:', session ? 'Session exists' : 'No session'); + }).catch(err => { + console.error('Error checking initial session:', err); + }); } + + if (!supabase) { + throw new Error('Failed to create Supabase client'); + } + return supabase; }; export const clearSupabaseInstance = () => { + console.log('Clearing Supabase instance'); supabase = null; }; \ No newline at end of file diff --git a/lib/api.ts b/lib/api.ts index 59e3fc1..fc5eb9f 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -8,43 +8,132 @@ export interface ApiResponse { message?: string; } -// Common function for authenticated API requests to LIMQ +/** + * 通用的LIMQ API请求函数,包含重试机制和错误处理 + */ export async function limqRequest( endpoint: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', - data?: Record + data?: Record, + options?: { + retryCount?: number; + retryDelay?: number; + timeout?: number; + } ): Promise> { - // Get current session - const { data: { session } } = await supabase.auth.getSession(); + // 默认配置 + const retryCount = options?.retryCount ?? 2; // 默认重试2次 + const retryDelay = options?.retryDelay ?? 1000; // 默认延迟1秒 + const timeout = options?.timeout ?? 10000; // 默认超时10秒 - if (!session) { - throw new Error('No active session. User must be authenticated.'); + let lastError: Error | null = null; + let currentRetry = 0; + + // 创建延迟函数 + const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + + // 重试循环 + while (currentRetry <= retryCount) { + try { + console.log(`[API] ${method} ${endpoint} 尝试 ${currentRetry + 1}/${retryCount + 1}`); + + // 获取会话 + const { data: { session } } = await supabase.auth.getSession(); + + // 检查会话是否存在 + if (!session) { + console.error(`[API] 未找到活跃会话,用户需要登录`); + + if (currentRetry < retryCount) { + currentRetry++; + console.log(`[API] 等待 ${retryDelay}ms 后重试获取会话...`); + await delay(retryDelay); + continue; + } + + return { + success: false, + error: '需要登录才能访问API' + }; + } + + // 获取API基础URL + const baseUrl = process.env.NEXT_PUBLIC_LIMQ_API; + if (!baseUrl) { + throw new Error('API URL未配置'); + } + + const url = `${baseUrl}${endpoint.startsWith('/') ? endpoint : '/' + endpoint}`; + console.log(`[API] 请求URL: ${url}`); + + // 构建请求选项 + const fetchOptions: RequestInit = { + method, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${session.access_token}` + }, + mode: 'cors' + }; + + if (data && (method === 'POST' || method === 'PUT')) { + fetchOptions.body = JSON.stringify(data); + console.log(`[API] 请求数据:`, data); + } + + // 添加超时控制 + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + fetchOptions.signal = controller.signal; + + // 发送请求 + const response = await fetch(url, fetchOptions); + clearTimeout(timeoutId); // 清除超时控制 + + // 处理响应 + if (!response.ok) { + const errorData = await response.json().catch(() => null); + console.error(`[API] 请求失败: ${response.status} ${response.statusText}`, errorData); + + // 对于认证错误,尝试重试 + if ((response.status === 401 || response.status === 403) && currentRetry < retryCount) { + currentRetry++; + console.log(`[API] 认证错误,等待 ${retryDelay}ms 后重试...`); + await delay(retryDelay); + continue; + } + + throw new Error(errorData?.error || `请求失败: ${response.status}`); + } + + // 成功响应 + const responseData = await response.json(); + console.log(`[API] ${method} ${endpoint} 成功`); + return responseData; + + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + console.error(`[API] 请求出错:`, lastError); + + // 对于超时和网络错误,尝试重试 + if (currentRetry < retryCount && + (error instanceof DOMException && error.name === 'AbortError' || + error instanceof TypeError && error.message.includes('network'))) { + currentRetry++; + console.log(`[API] 网络错误,等待 ${retryDelay}ms 后重试...`); + await delay(retryDelay); + continue; + } + + // 已达到最大重试次数或不是网络错误 + break; + } } - const baseUrl = process.env.NEXT_PUBLIC_LIMQ_API; - const url = `${baseUrl}${endpoint.startsWith('/') ? endpoint : '/' + endpoint}`; - - const options: RequestInit = { - method, - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${session.access_token}` - }, - mode: 'cors' + // 所有重试均失败 + console.error(`[API] ${method} ${endpoint} 失败,已重试 ${currentRetry} 次`); + return { + success: false, + error: lastError?.message || '请求失败,请稍后重试' }; - - if (data && (method === 'POST' || method === 'PUT')) { - options.body = JSON.stringify(data); - } - - const response = await fetch(url, options); - - if (!response.ok) { - const errorData = await response.json().catch(() => null); - throw new Error( - errorData?.error || `Request failed with status ${response.status}` - ); - } - - return response.json(); } \ No newline at end of file