import { useStore } from '@nanostores/react'; import { useFetcher, useRouteLoaderData } from '@remix-run/react'; import classNames from 'classnames'; import React, { useEffect, useMemo, useState } from 'react'; import { toast } from 'sonner'; import { logStore } from '~/lib/stores/logs'; import { fetchVercelStats, isFetchingStats, updateVercelConnection, vercelConnection } from '~/lib/stores/vercel'; import type { ConnectionSettings } from '~/root'; import { logger } from '~/utils/logger'; import ConnectionBorder from './components/ConnectionBorder'; interface ApiResponse { success: boolean; message?: string; data?: any; } export default function VercelConnection() { const rootData = useRouteLoaderData<{ connectionSettings?: ConnectionSettings }>('root'); const settingsFetcher = useFetcher(); const connectFetcher = useFetcher(); const connection = useStore(vercelConnection); const fetchingStats = useStore(isFetchingStats); const [isProjectsExpanded, setIsProjectsExpanded] = useState(false); const [tokenInput, setTokenInput] = useState(''); useEffect(() => { updateVercelConnection({ isConnect: rootData?.connectionSettings?.vercelConnection, }); }, [rootData]); useEffect(() => { if (connection.isConnect) { fetchVercelStats().catch((err) => { logger.error('获取 Vercel 统计信息失败:', err); }); if (!connection.user) { handleConnect(); } } }, [connection.isConnect]); useEffect(() => { if (settingsFetcher.state === 'idle' && settingsFetcher.data) { if (settingsFetcher.data.success) { updateVercelConnection({ isConnect: false, user: null }); toast.success('断开 Vercel 连接'); } } }, [settingsFetcher.state, settingsFetcher.data]); useEffect(() => { if (connectFetcher.state === 'idle' && connectFetcher.data) { if (connectFetcher.data.success) { updateVercelConnection({ isConnect: connectFetcher.data.data.isConnect, user: connectFetcher.data.data.user, }); toast.success('连接 Vercel 成功'); setTokenInput(''); } else if (connectFetcher.data.message) { toast.error(connectFetcher.data.message || '连接失败'); updateVercelConnection({ isConnect: false, user: null }); } } }, [connectFetcher.state, connectFetcher.data]); const isConnecting = useMemo(() => { return connectFetcher.state !== 'idle'; }, [connectFetcher.state]); const handleConnect = async () => { try { connectFetcher.submit( { token: tokenInput }, { method: 'POST', action: '/api/vercel/auth', encType: 'application/json', }, ); } catch (error) { toast.error('连接 Vercel 失败'); logger.error('连接 Vercel 失败:', error); logStore.logError('Failed to authenticate with Vercel', { error }); } }; const handleDisconnect = async () => { try { settingsFetcher.submit( { category: 'connectivity', key: 'vercel_token', }, { method: 'DELETE', action: '/api/user/settings', encType: 'application/json', }, ); } catch (error) { toast.error('断开 Vercel 连接失败'); logger.error('断开 Vercel 连接失败:', error); } }; return ( {!connection.isConnect ? (
setTokenInput(e.target.value)} disabled={isConnecting} placeholder="输入您的 Vercel 个人访问令牌" className={classNames( 'w-full px-3 py-2 rounded-lg text-sm', 'bg-[#F8F8F8] dark:bg-[#1A1A1A]', 'border border-[#E5E5E5] dark:border-[#333333]', 'text-upage-elements-textPrimary placeholder-upage-elements-textTertiary', 'focus:outline-none focus:ring-1 focus:ring-upage-elements-borderColorActive', 'disabled:opacity-50', )} />
) : (
已连接到 Vercel
{JSON.stringify(connection.user, null, 2)}
User Avatar

{connection.user?.username || connection.user?.user?.username || 'Vercel User'}

{connection.user?.email || connection.user?.user?.email || 'No email available'}

{fetchingStats ? (
正在获取 Vercel 项目...
) : (
{isProjectsExpanded && connection.stats?.projects?.length ? (
{connection.stats.projects.map((project) => (
{project.name}
) : isProjectsExpanded ? (
未找到您的 Vercel 账户中的项目
) : null}
)}
)} ); }