Files
OpenFlare/frontend/app/(main)/pages/detail/page.tsx
T
ryan 5a8722ff07 feat(frontend): 控制台 next-intl 中英双语
接入无 URL 前缀的 zh-CN/en,顶栏与外观设置可切换语言;选择写入 cookie 后刷新生效。
2026-08-16 16:49:16 +08:00

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>
);
}