mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-02 06:56:36 +08:00
5a8722ff07
接入无 URL 前缀的 zh-CN/en,顶栏与外观设置可切换语言;选择写入 cookie 后刷新生效。
182 lines
5.6 KiB
TypeScript
182 lines
5.6 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { Suspense } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
import { ArrowLeft, FileText } from 'lucide-react';
|
|
import { useTranslations } from 'next-intl';
|
|
|
|
import { EmptyStateWithBorder } from '@/components/layout/empty';
|
|
import { ErrorInline } from '@/components/layout/error';
|
|
import { LoadingStateWithBorder } from '@/components/layout/loading';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { PagesService } from '@/lib/services/openflare';
|
|
|
|
import { projectQueryKey } from '../components/pages-utils';
|
|
import { DeploymentsTab } from './tabs/deployments-tab';
|
|
import { SettingsTab } from './tabs/settings-tab';
|
|
|
|
type PagesDetailTab = 'deployments' | 'settings';
|
|
|
|
function resolveTab(value: string | null): PagesDetailTab {
|
|
if (value === 'settings') return 'settings';
|
|
return 'deployments';
|
|
}
|
|
|
|
function PagesDetailPageFallback() {
|
|
return (
|
|
<div className='flex w-full flex-col gap-6 px-1 py-6'>
|
|
<Skeleton className='h-8 w-32' />
|
|
<Skeleton className='h-12 w-full max-w-xl' />
|
|
<Skeleton className='h-10 w-64' />
|
|
<Skeleton className='h-64 w-full' />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PagesDetailRoute() {
|
|
const t = useTranslations('pages');
|
|
const searchParams = useSearchParams();
|
|
const router = useRouter();
|
|
|
|
const rawProjectId = searchParams.get('id')?.trim() ?? '';
|
|
const projectId = Number(rawProjectId);
|
|
const validProjectId =
|
|
rawProjectId !== '' && Number.isInteger(projectId) && projectId > 0;
|
|
const tab = resolveTab(searchParams.get('tab'));
|
|
|
|
const projectQuery = useQuery({
|
|
queryKey: projectQueryKey(projectId),
|
|
queryFn: () => PagesService.getProject(projectId),
|
|
enabled: validProjectId,
|
|
});
|
|
|
|
const handleTabChange = (value: string) => {
|
|
const next = resolveTab(value);
|
|
const params = new URLSearchParams();
|
|
params.set('id', String(projectId));
|
|
if (next === 'settings') {
|
|
params.set('tab', 'settings');
|
|
}
|
|
router.replace(`/pages/detail?${params.toString()}`);
|
|
};
|
|
|
|
if (!validProjectId) {
|
|
return (
|
|
<div className='w-full px-1 py-6'>
|
|
<EmptyStateWithBorder description={t('detail.invalidId')} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (projectQuery.isLoading) {
|
|
return (
|
|
<div className='w-full px-1 py-6'>
|
|
<LoadingStateWithBorder
|
|
icon={FileText}
|
|
description={t('detail.loading')}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (projectQuery.isError) {
|
|
return (
|
|
<div className='w-full px-1 py-6'>
|
|
<div className='rounded-lg border p-4'>
|
|
<ErrorInline
|
|
message={
|
|
projectQuery.error instanceof Error
|
|
? projectQuery.error.message
|
|
: t('detail.loadFailed')
|
|
}
|
|
onRetry={() => void projectQuery.refetch()}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const project = projectQuery.data;
|
|
if (!project) {
|
|
return (
|
|
<div className='flex w-full flex-col gap-4 px-1 py-6'>
|
|
<Button variant='ghost' size='sm' asChild>
|
|
<Link href='/pages'>
|
|
<ArrowLeft data-icon='inline-start' />
|
|
{t('detail.back')}
|
|
</Link>
|
|
</Button>
|
|
<EmptyStateWithBorder description={t('detail.notFound')} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className='flex w-full flex-col gap-6 px-1 py-6'>
|
|
<div className='flex flex-col gap-4'>
|
|
<Button variant='ghost' size='sm' className='self-start' asChild>
|
|
<Link href='/pages'>
|
|
<ArrowLeft data-icon='inline-start' />
|
|
{t('detail.back')}
|
|
</Link>
|
|
</Button>
|
|
<div className='flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between'>
|
|
<div className='flex flex-col gap-2'>
|
|
<div className='flex items-center gap-2'>
|
|
<FileText className='size-5 text-primary' />
|
|
<h1 className='text-2xl font-semibold tracking-tight'>
|
|
{project.name}
|
|
</h1>
|
|
<Badge variant={project.enabled ? 'secondary' : 'outline'}>
|
|
{project.enabled ? t('enabled') : t('disabled')}
|
|
</Badge>
|
|
</div>
|
|
<p className='text-sm text-muted-foreground'>
|
|
{project.slug} ·{' '}
|
|
{t('detail.deploymentCount', { count: project.deployment_count })}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Tabs value={tab} onValueChange={handleTabChange} className='w-full'>
|
|
<TabsList variant='line' className='mb-6 inline-flex w-fit gap-8'>
|
|
<TabsTrigger
|
|
value='deployments'
|
|
className='px-0 pb-2 text-xs font-semibold'
|
|
>
|
|
{t('detail.tabDeployments')}
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value='settings'
|
|
className='px-0 pb-2 text-xs font-semibold'
|
|
>
|
|
{t('detail.tabSettings')}
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value='deployments' className='focus-visible:outline-none'>
|
|
<DeploymentsTab project={project} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value='settings' className='focus-visible:outline-none'>
|
|
<SettingsTab project={project} />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function PagesDetailPage() {
|
|
return (
|
|
<Suspense fallback={<PagesDetailPageFallback />}>
|
|
<PagesDetailRoute />
|
|
</Suspense>
|
|
);
|
|
}
|