diff --git a/openflare_server/web/components/feedback/empty-state.tsx b/openflare_server/web/components/feedback/empty-state.tsx index 05fe98d6..77e6ec05 100644 --- a/openflare_server/web/components/feedback/empty-state.tsx +++ b/openflare_server/web/components/feedback/empty-state.tsx @@ -1,9 +1,12 @@ +import type { ReactNode } from 'react'; + interface EmptyStateProps { title: string; description?: string; + children?: ReactNode; } -export function EmptyState({ title, description }: EmptyStateProps) { +export function EmptyState({ title, description, children }: EmptyStateProps) { return (

@@ -14,6 +17,8 @@ export function EmptyState({ title, description }: EmptyStateProps) { {description}

) : null} + {children ?
{children}
: null}
); } + diff --git a/openflare_server/web/features/pages/components/pages-page.tsx b/openflare_server/web/features/pages/components/pages-page.tsx index e0fd4a5c..cb6255f9 100644 --- a/openflare_server/web/features/pages/components/pages-page.tsx +++ b/openflare_server/web/features/pages/components/pages-page.tsx @@ -134,10 +134,8 @@ export function PagesPage() { export function PagesProjectDetailPage({ projectId }: { projectId: string }) { const router = useRouter(); const queryClient = useQueryClient(); - const [file, setFile] = useState(null); - const [entryFile, setEntryFile] = useState('index.html'); - const [uploadProgress, setUploadProgress] = useState(null); const [isEditModalOpen, setEditModalOpen] = useState(false); + const [isUploadModalOpen, setUploadModalOpen] = useState(false); const parsedProjectId = Number(projectId); const projectQuery = useQuery({ @@ -151,29 +149,6 @@ export function PagesProjectDetailPage({ projectId }: { projectId: string }) { enabled: projectId !== '' && Number.isFinite(parsedProjectId), }); - const uploadMutation = useMutation({ - mutationFn: () => { - if (!file) { - throw new Error('请选择 zip 文件'); - } - setUploadProgress(0); - return uploadPagesDeployment(parsedProjectId, file, entryFile, (percent) => { - setUploadProgress(percent); - }); - }, - onSuccess: () => { - setFile(null); - setUploadProgress(null); - queryClient.invalidateQueries({ - queryKey: deploymentsQueryKey(parsedProjectId), - }); - queryClient.invalidateQueries({ queryKey: projectQueryKey(projectId) }); - queryClient.invalidateQueries({ queryKey: projectsQueryKey }); - }, - onError: () => { - setUploadProgress(null); - }, - }); const activateMutation = useMutation({ mutationFn: (deploymentId: number) => activatePagesDeployment(parsedProjectId, deploymentId), @@ -262,6 +237,12 @@ export function PagesProjectDetailPage({ projectId }: { projectId: string }) { > 编辑项目 + setUploadModalOpen(true)} + > + 上传部署包 + - -
- -
- - setFile(event.target.files?.[0] ?? null)} - /> - - - setEntryFile(event.target.value)} - /> - - {uploadProgress !== null && ( -
-
- 上传进度 - {uploadProgress}% -
-
-
-
-
- )} + 0 ? ( uploadMutation.mutate()} + onClick={() => setUploadModalOpen(true)} > - {uploadMutation.isPending - ? `上传中 (${uploadProgress ?? 0}%)...` - : '上传部署'} + 上传部署包 - {uploadMutation.error ? ( -

- {uploadMutation.error.message} -

- ) : null} -
- - - - {deploymentsQuery.isLoading ? ( -

- 加载中... -

- ) : deploymentsQuery.isError ? ( -

- {deploymentsQuery.error.message} -

- ) : (deploymentsQuery.data ?? []).length === 0 ? ( - - ) : ( -
- {(deploymentsQuery.data ?? []).map((deployment) => ( -
-
-

- #{deployment.deployment_number}{' '} - {deployment.status === 'active' ? '· 已激活' : ''} -

-

- {deployment.checksum.slice(0, 16)} ·{' '} - {deployment.file_count} files ·{' '} - {formatBytes(deployment.total_size)} -

-
-
- { - if ( - window.confirm( - `确认激活部署 #${deployment.deployment_number} 吗?`, - ) - ) { - activateMutation.mutate(deployment.id); - } - }} - > - 激活 - - { - if ( - window.confirm( - `确认删除部署 #${deployment.deployment_number} 吗?`, - ) - ) { - deleteDeploymentMutation.mutate(deployment.id); - } - }} - > - 删除 - -
+ ) : null + } + > + {deploymentsQuery.isLoading ? ( +

+ 加载中... +

+ ) : deploymentsQuery.isError ? ( +

+ {deploymentsQuery.error.message} +

+ ) : (deploymentsQuery.data ?? []).length === 0 ? ( + + setUploadModalOpen(true)} + > + 上传部署包 + + + ) : ( +
+ {(deploymentsQuery.data ?? []).map((deployment) => ( +
+
+

+ #{deployment.deployment_number}{' '} + {deployment.status === 'active' ? '· 已激活' : ''} +

+

+ {deployment.checksum.slice(0, 16)} ·{' '} + {deployment.file_count} files ·{' '} + {formatBytes(deployment.total_size)} +

- ))} -
- )} - -
+
+ { + if ( + window.confirm( + `确认激活部署 #${deployment.deployment_number} 吗?`, + ) + ) { + activateMutation.mutate(deployment.id); + } + }} + > + 激活 + + { + if ( + window.confirm( + `确认删除部署 #${deployment.deployment_number} 吗?`, + ) + ) { + deleteDeploymentMutation.mutate(deployment.id); + } + }} + > + 删除 + +
+
+ ))} +
+ )} +
setEditModalOpen(false)} project={project} /> + + setUploadModalOpen(false)} + projectId={parsedProjectId} + />
); } +function PagesDeploymentUploadModal({ + isOpen, + onClose, + projectId, +}: { + isOpen: boolean; + onClose: () => void; + projectId: number; +}) { + const queryClient = useQueryClient(); + const [file, setFile] = useState(null); + const [entryFile, setEntryFile] = useState('index.html'); + const [uploadProgress, setUploadProgress] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + + const resetForm = () => { + setFile(null); + setEntryFile('index.html'); + setUploadProgress(null); + setErrorMessage(null); + }; + + const handleClose = () => { + resetForm(); + onClose(); + }; + + const uploadMutation = useMutation({ + mutationFn: async ({ shouldActivate }: { shouldActivate: boolean }) => { + if (!file) { + throw new Error('请选择 zip 文件'); + } + setUploadProgress(0); + setErrorMessage(null); + + const deployment = await uploadPagesDeployment( + projectId, + file, + entryFile, + (percent) => { + setUploadProgress(percent); + } + ); + + if (shouldActivate) { + await activatePagesDeployment(projectId, deployment.id); + } + return deployment; + }, + onSuccess: () => { + resetForm(); + queryClient.invalidateQueries({ + queryKey: deploymentsQueryKey(projectId), + }); + queryClient.invalidateQueries({ + queryKey: projectQueryKey(projectId), + }); + queryClient.invalidateQueries({ + queryKey: projectsQueryKey, + }); + onClose(); + }, + onError: (error) => { + setUploadProgress(null); + setErrorMessage(error instanceof Error ? error.message : '上传失败'); + }, + }); + + const handleUploadOnly = () => { + uploadMutation.mutate({ shouldActivate: false }); + }; + + const handleUploadAndDeploy = () => { + uploadMutation.mutate({ shouldActivate: true }); + }; + + return ( + + + 取消 + + + {uploadMutation.isPending && !uploadMutation.variables?.shouldActivate + ? `上传中 (${uploadProgress ?? 0}%)...` + : '上传'} + + + {uploadMutation.isPending && uploadMutation.variables?.shouldActivate + ? `上传并部署中 (${uploadProgress ?? 0}%)...` + : '上传并部署'} + +
+ } + > +
+ + setFile(event.target.files?.[0] ?? null)} + /> + + + setEntryFile(event.target.value)} + /> + + {uploadProgress !== null && ( +
+
+ 上传进度 + {uploadProgress}% +
+
+
+
+
+ )} + {errorMessage ? ( +

+ {errorMessage} +

+ ) : null} +
+ + ); +} + + function PagesProjectEditModal({ isOpen, onClose,