'use client'; import { useRef, useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Link2, Loader2, UploadCloud } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Progress } from '@/components/ui/progress'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { PagesService } from '@/lib/services/openflare'; import { cn } from '@/lib/utils'; import { deploymentsQueryKey, formatBytes, projectQueryKey, projectsQueryKey, } from './pages-utils'; const PAGES_PACKAGE_ACCEPT = '.zip,.tar.gz,.tgz,.tar.xz,.txz,.tar.bz2,.tbz2,.tbz,.tar,.7z'; const PAGES_PACKAGE_EXTENSIONS = [ '.zip', '.tar.gz', '.tgz', '.tar.xz', '.txz', '.tar.bz2', '.tbz2', '.tbz', '.tar', '.7z', ] as const; function isSupportedPagesPackage(fileName: string) { const lower = fileName.toLowerCase(); return PAGES_PACKAGE_EXTENSIONS.some((ext) => lower.endsWith(ext)); } interface DeploymentUploadDialogProps { open: boolean; onOpenChange: (open: boolean) => void; projectId: number; } export function DeploymentUploadDialog({ open, onOpenChange, projectId, }: DeploymentUploadDialogProps) { const queryClient = useQueryClient(); const fileInputRef = useRef(null); const [mode, setMode] = useState<'file' | 'url'>('file'); const [file, setFile] = useState(null); const [packageURL, setPackageURL] = useState(''); const [isDragActive, setIsDragActive] = useState(false); const [uploadProgress, setUploadProgress] = useState(null); const resetForm = () => { setFile(null); setPackageURL(''); setIsDragActive(false); setUploadProgress(null); if (fileInputRef.current) fileInputRef.current.value = ''; }; const handleClose = (nextOpen: boolean) => { if (!nextOpen) resetForm(); onOpenChange(nextOpen); }; const invalidate = async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: deploymentsQueryKey(projectId), }), queryClient.invalidateQueries({ queryKey: projectQueryKey(projectId) }), queryClient.invalidateQueries({ queryKey: projectsQueryKey }), ]); }; const uploadFileMutation = useMutation({ mutationFn: () => { if (!file) throw new Error('请选择部署包'); return PagesService.uploadDeployment(projectId, { file, onProgress: setUploadProgress, }); }, onSuccess: async () => { toast.success('部署包上传成功'); await invalidate(); handleClose(false); }, onError: (error) => { toast.error(error instanceof Error ? error.message : '上传失败'); setUploadProgress(null); }, }); const uploadURLMutation = useMutation({ mutationFn: () => { const url = packageURL.trim(); if (!url) throw new Error('请填写部署包下载链接'); if (!/^https?:\/\//i.test(url)) { throw new Error('链接必须以 http:// 或 https:// 开头'); } return PagesService.uploadDeploymentFromURL(projectId, { url }); }, onSuccess: async () => { toast.success('已从链接下载并创建部署'); await invalidate(); handleClose(false); }, onError: (error) => { toast.error(error instanceof Error ? error.message : '从链接导入失败'); }, }); const isPending = uploadFileMutation.isPending || uploadURLMutation.isPending; const handleFileSelect = (selected: File | null) => { if (!selected) return; if (!isSupportedPagesPackage(selected.name)) { toast.error('仅支持 zip、tar.gz、tar.xz、tar.bz2、tar、7z 格式的部署包'); return; } setFile(selected); }; const handleSubmit = () => { if (mode === 'file') { uploadFileMutation.mutate(); return; } uploadURLMutation.mutate(); }; const canSubmit = mode === 'file' ? Boolean(file) : packageURL.trim().length > 0; return ( 上传部署包 支持本地上传或从 URL 下载静态资源压缩包(zip / tar.gz / tar.xz / tar.bz2 / tar / 7z),创建部署后可在列表中激活。 setMode(value as 'file' | 'url')} className='w-full' > 本地上传 从 URL 下载
{ e.preventDefault(); setIsDragActive(true); }} onDragOver={(e) => e.preventDefault()} onDragLeave={(e) => { e.preventDefault(); setIsDragActive(false); }} onDrop={(e) => { e.preventDefault(); setIsDragActive(false); handleFileSelect(e.dataTransfer.files[0] ?? null); }} >

拖拽部署包到此处,或点击选择文件

支持 zip、tar.gz、tar.xz、tar.bz2、tar、7z

handleFileSelect(e.target.files?.[0] ?? null)} />
{file ? (

{file.name}

{formatBytes(file.size)}

) : null} {uploadProgress !== null ? (
{uploadProgress >= 100 ? '服务端处理中' : '上传进度'} {uploadProgress >= 100 ? '请稍候' : `${uploadProgress}%`}
= 100 ? 100 : uploadProgress} />
) : null}
setPackageURL(e.target.value)} disabled={isPending} />

服务端将使用浏览器环境请求头从该链接下载压缩包,支持内网地址与自签证书 HTTPS。

); }