import { useStore } from '@nanostores/react'; import { useFetcher } from '@remix-run/react'; import classNames from 'classnames'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { toast } from 'sonner'; import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client'; import useViewport from '~/lib/hooks'; import { setLocalStorage } from '~/lib/persistence'; import { aiState, setShowChat } from '~/lib/stores/ai-state'; import { webBuilderStore } from '~/lib/stores/web-builder'; import type { _1PanelDeployResponse } from '~/types/1panel'; import { DeploymentPlatformEnum } from '~/types/deployment'; import type { ApiResponse } from '~/types/global'; import { _1PanelDeploymentLink } from '../chat/_1PanelDeploymentLink.client'; import { VercelDeploymentLink } from '../chat/VercelDeploymentLink.client'; import { UPageIndex } from '../upage/Index'; import { DeployTo1PanelDialog } from './DeployTo1PanelDialog'; import { DeployToNetlifyDialog } from './DeployToNetlifyDialog'; import { DeployToVercelDialog } from './DeployToVercelDialog'; interface HeaderActionButtonsProps {} export function HeaderActionButtons({}: HeaderActionButtonsProps) { const showWorkbench = useStore(webBuilderStore.showWorkbench); const { showChat, chatId, isStreaming } = useStore(aiState); const [deployingTo, setDeployingTo] = useState<'netlify' | 'vercel' | '1panel' | null>(null); const isSmallViewport = useViewport(1024); const canHideChat = showWorkbench || !showChat; const [isDropdownOpen, setIsDropdownOpen] = useState(false); const dropdownRef = useRef(null); const netlifyFetcher = useFetcher(); const vercelFetcher = useFetcher(); const panelFetcher = useFetcher<_1PanelDeployResponse>(); const isDeploying = useMemo(() => { return netlifyFetcher.state !== 'idle' || vercelFetcher.state !== 'idle' || panelFetcher.state !== 'idle'; }, [netlifyFetcher.state, vercelFetcher.state, panelFetcher.state]); const [showNetlifyDialog, setShowNetlifyDialog] = useState(false); const [showVercelDialog, setShowVercelDialog] = useState(false); const [show1PanelDialog, setShow1PanelDialog] = useState(false); useEffect(() => { const url = new URL(window.location.href); const deploy = url.searchParams.get('deploy'); switch (deploy) { case DeploymentPlatformEnum.NETLIFY: setShowNetlifyDialog(true); break; case DeploymentPlatformEnum.VERCEL: setShowVercelDialog(true); break; case DeploymentPlatformEnum._1PANEL: setShow1PanelDialog(true); break; } const recommend = url.searchParams.get('recommend'); if (recommend) { setLocalStorage('recommend', recommend || ''); } if (deploy || recommend) { url.searchParams.delete('deploy'); url.searchParams.delete('recommend'); window.history.replaceState({}, '', url); } }, []); useEffect(() => { function handleClickOutside(event: MouseEvent) { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsDropdownOpen(false); } } document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); useEffect(() => { if (netlifyFetcher.state === 'idle' && netlifyFetcher.data) { const { data, success, message } = netlifyFetcher.data; if (success && data?.deploy && data?.site) { if (data.site) { localStorage.setItem(`netlify-site-${chatId!}`, data.site?.id); } toast.success(
部署成功!{' '} 查看站点
, ); setShowNetlifyDialog(false); } else { console.error('Invalid deploy response:', data); toast.error(message || 'Invalid deployment response'); } setDeployingTo(null); } }, [netlifyFetcher.state, netlifyFetcher.data, chatId]); useEffect(() => { if (vercelFetcher.state === 'idle' && vercelFetcher.data) { const { data, success, message } = vercelFetcher.data; if (success && data?.deploy && data?.project) { if (data.project) { localStorage.setItem(`vercel-project-${chatId!}`, data.project.id); } toast.success(
部署到 Vercel 成功!{' '} 查看站点
, ); setShowVercelDialog(false); } else { console.error('Invalid deploy response:', data); toast.error(message || 'Invalid deployment response'); } setDeployingTo(null); } }, [vercelFetcher.state, vercelFetcher.data, chatId]); useEffect(() => { if (panelFetcher.state === 'idle' && panelFetcher.data) { const data = panelFetcher.data as _1PanelDeployResponse; const { deploy } = data.data || {}; if (data.success && deploy) { localStorage.setItem(`1panel-project-${chatId!}`, deploy.id.toString()); toast.success(
部署到 1Panel 成功!{' '} 查看站点
, ); setShow1PanelDialog(false); } else { console.error('Invalid deploy response:', data); toast.error(data.message || 'Invalid deployment response'); } setDeployingTo(null); } }, [panelFetcher.state, panelFetcher.data, chatId]); async function getAllFiles(): Promise> { const files = await webBuilderStore.getProjectFiles({ inline: false }).then((files) => { return files.reduce( (acc, file) => { acc[file.filename] = file.content; return acc; }, {} as Record, ); }); const newFiles: Record = {}; for (const [key, value] of Object.entries(files)) { if (key.endsWith('.html')) { const html = new DOMParser().parseFromString(value, 'text/html'); const originalContent = html.body.innerHTML; // 添加 UPageHtml 到 body 中 const uPageHtml = renderToStaticMarkup(); html.body.innerHTML = originalContent + uPageHtml; newFiles[key] = '\n' + html.documentElement.outerHTML; } else { newFiles[key] = value; } } return newFiles; } const handleNetlifyDeploy = useCallback(async () => { if (!chatId) { toast.error('没有找到活动聊天'); return; } try { setDeployingTo('netlify'); const fileContents = await getAllFiles(); const existingSiteId = localStorage.getItem(`netlify-site-${chatId}`); netlifyFetcher.submit( { siteId: existingSiteId || '', files: fileContents, chatId: chatId!, } as any, { method: 'POST', action: '/api/netlify/deploy', encType: 'application/json', }, ); } catch (error) { console.error('Deploy error:', error); toast.error(error instanceof Error ? error.message : '部署失败'); setDeployingTo(null); } }, [chatId, netlifyFetcher]); const handleVercelDeploy = useCallback(async () => { if (!chatId) { toast.error('没有找到活动聊天'); return; } try { setDeployingTo('vercel'); const fileContents = await getAllFiles(); const existingProjectId = localStorage.getItem(`vercel-project-${chatId}`); vercelFetcher.submit( { projectId: existingProjectId || '', files: fileContents, chatId: chatId!, } as any, { method: 'POST', action: '/api/vercel/deploy', encType: 'application/json', }, ); } catch (error) { console.error('Vercel deploy error:', error); toast.error(error instanceof Error ? error.message : 'Vercel 部署失败'); setDeployingTo(null); } }, [chatId, vercelFetcher]); const handle1PanelDeploy = useCallback( async (options?: { customDomain?: string; siteId?: number; protocol?: string }) => { if (!chatId) { toast.error('没有找到活动聊天'); return; } try { setDeployingTo('1panel'); const fileContents = await getAllFiles(); const existingWebsiteId = localStorage.getItem(`1panel-project-${chatId}`); panelFetcher.submit( { websiteId: options?.siteId || existingWebsiteId || '', websiteDomain: options?.customDomain || '', protocol: options?.protocol || 'http', files: fileContents, chatId: chatId!, } as any, { method: 'POST', action: '/api/1panel/deploy', encType: 'application/json', }, ); } catch (error) { console.error('1Panel deploy error:', error); toast.error(error instanceof Error ? error.message : '1Panel 部署失败'); setDeployingTo(null); } }, [chatId, panelFetcher], ); return (
{isDropdownOpen && (
)}
setShowNetlifyDialog(false)} onDeploy={handleNetlifyDeploy} /> setShowVercelDialog(false)} onDeploy={handleVercelDeploy} /> setShow1PanelDialog(false)} onDeploy={handle1PanelDeploy} />
); } interface ButtonProps { active?: boolean; disabled?: boolean; children?: any; onClick?: VoidFunction; className?: string; } function Button({ active = false, disabled = false, children, onClick, className }: ButtonProps) { return ( ); }