mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-14 02:46:37 +08:00
feat(frontend): 控制台 next-intl 中英双语
接入无 URL 前缀的 zh-CN/en,顶栏与外观设置可切换语言;选择写入 cookie 后刷新生效。
This commit is contained in:
@@ -35,6 +35,8 @@ import { CertificateDetailDialog } from '../websites/components/certificate-deta
|
||||
import { CertificateEditorDialog } from '../websites/components/certificate-editor-dialog';
|
||||
import { CertificateImportDialog } from '../websites/components/certificate-import-dialog';
|
||||
import { WebsiteStatusBadge } from '../websites/components/status-badge';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import {
|
||||
getCertificateStatus,
|
||||
getErrorMessage,
|
||||
@@ -45,6 +47,8 @@ const certificatesQueryKey = ['openflare', 'tls-certificates'];
|
||||
type CertificateApplyMode = 'edit-acme' | 'convert-upload';
|
||||
|
||||
export default function CertificatesPage() {
|
||||
const t = useTranslations('certificates');
|
||||
const tc = useTranslations('common');
|
||||
const queryClient = useQueryClient();
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [applyOpen, setApplyOpen] = useState(false);
|
||||
@@ -68,20 +72,20 @@ export default function CertificatesPage() {
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => TlsCertificateService.deleteById(id),
|
||||
onSuccess: async () => {
|
||||
toast.success('证书已删除');
|
||||
toast.success(t('deleted'));
|
||||
setDeleteTarget(null);
|
||||
await queryClient.invalidateQueries({ queryKey: certificatesQueryKey });
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
onError: (error) => toast.error(getErrorMessage(error, t('requestFailed'))),
|
||||
});
|
||||
|
||||
const renewMutation = useMutation({
|
||||
mutationFn: (id: number) => TlsCertificateService.renew(id),
|
||||
onSuccess: async (cert) => {
|
||||
toast.success(`证书 ${cert.name} 续期任务已提交`);
|
||||
toast.success(t('renewSubmitted', { name: cert.name }));
|
||||
await queryClient.invalidateQueries({ queryKey: certificatesQueryKey });
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
onError: (error) => toast.error(getErrorMessage(error, t('requestFailed'))),
|
||||
});
|
||||
|
||||
const certificates = useMemo(
|
||||
@@ -105,7 +109,7 @@ export default function CertificatesPage() {
|
||||
<div className='flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<FileKey className='size-5 text-primary' />
|
||||
<h1 className='text-2xl font-semibold tracking-tight'>证书</h1>
|
||||
<h1 className='text-2xl font-semibold tracking-tight'>{t('title')}</h1>
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button
|
||||
@@ -119,7 +123,7 @@ export default function CertificatesPage() {
|
||||
}
|
||||
>
|
||||
<RefreshCw className='size-3.5 mr-1' />
|
||||
刷新证书
|
||||
{t('refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='secondary'
|
||||
@@ -127,7 +131,7 @@ export default function CertificatesPage() {
|
||||
className='h-7 text-xs'
|
||||
onClick={() => setImportOpen(true)}
|
||||
>
|
||||
导入证书
|
||||
{t('importCert')}
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
@@ -135,28 +139,29 @@ export default function CertificatesPage() {
|
||||
onClick={() => setApplyOpen(true)}
|
||||
>
|
||||
<Plus className='size-3.5 mr-1' />
|
||||
申请证书
|
||||
{t('applyTitle')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className='border-dashed shadow-none'>
|
||||
<CardHeader className='pb-3'>
|
||||
<CardTitle className='text-base font-semibold'>证书列表</CardTitle>
|
||||
<CardDescription>
|
||||
展示证书有效期、备注和状态,支持查看详情、编辑内容或删除证书。
|
||||
</CardDescription>
|
||||
<CardTitle className='text-base font-semibold'>{t('listTitle')}</CardTitle>
|
||||
<CardDescription>{t('listDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{certificatesQuery.isLoading ? (
|
||||
<LoadingStateWithBorder
|
||||
icon={FileKey}
|
||||
description='加载证书列表中...'
|
||||
description={t('loadingList')}
|
||||
/>
|
||||
) : certificatesQuery.isError ? (
|
||||
<div className='p-8 border border-dashed rounded-lg'>
|
||||
<ErrorInline
|
||||
message={getErrorMessage(certificatesQuery.error)}
|
||||
message={getErrorMessage(
|
||||
certificatesQuery.error,
|
||||
t('requestFailed'),
|
||||
)}
|
||||
onRetry={() => void certificatesQuery.refetch()}
|
||||
className='justify-center'
|
||||
/>
|
||||
@@ -164,12 +169,12 @@ export default function CertificatesPage() {
|
||||
) : certificates.length === 0 ? (
|
||||
<EmptyStateWithBorder
|
||||
icon={FileKey}
|
||||
description='暂无证书,点击右上角「导入证书」或「申请证书」开始录入。'
|
||||
description={t('emptyList')}
|
||||
/>
|
||||
) : (
|
||||
<div className='space-y-3'>
|
||||
{certificates.map((certificate) => {
|
||||
const status = getCertificateStatus(certificate);
|
||||
const status = getCertificateStatus(certificate, t);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -188,32 +193,50 @@ export default function CertificatesPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className='text-xs leading-5 text-muted-foreground space-y-0.5'>
|
||||
<p>生效:{formatDateTime(certificate.not_before)}</p>
|
||||
<p>到期:{formatDateTime(certificate.not_after)}</p>
|
||||
<p>
|
||||
来源:
|
||||
{certificate.provider === 'acme'
|
||||
? 'ACME 申请'
|
||||
: '手动上传'}
|
||||
{t('notBeforeLabel', {
|
||||
value: formatDateTime(certificate.not_before),
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t('notAfterLabel', {
|
||||
value: formatDateTime(certificate.not_after),
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t('sourceLabel', {
|
||||
value:
|
||||
certificate.provider === 'acme'
|
||||
? t('sourceAcme')
|
||||
: t('sourceUpload'),
|
||||
})}
|
||||
</p>
|
||||
{certificate.apply_status === 'applying' ? (
|
||||
<p className='text-blue-600'>
|
||||
状态:
|
||||
{certificate.provider === 'upload'
|
||||
? '转换申请中...'
|
||||
: '申请中...'}
|
||||
{t('statusLabel', {
|
||||
value:
|
||||
certificate.provider === 'upload'
|
||||
? t('converting')
|
||||
: t('applying'),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
{certificate.apply_status === 'error' ? (
|
||||
<p className='text-destructive'>
|
||||
状态:
|
||||
{certificate.provider === 'upload'
|
||||
? '转换失败'
|
||||
: '申请失败'}
|
||||
({certificate.apply_message})
|
||||
{t('statusError', {
|
||||
value:
|
||||
certificate.provider === 'upload'
|
||||
? t('convertFailed')
|
||||
: t('applyFailed'),
|
||||
message: certificate.apply_message,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
<p>备注:{certificate.remark || '暂无备注'}</p>
|
||||
<p>
|
||||
{t('remarkLabel', {
|
||||
value: certificate.remark || t('noRemark'),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -227,7 +250,7 @@ export default function CertificatesPage() {
|
||||
setDetailOpen(true);
|
||||
}}
|
||||
>
|
||||
查看
|
||||
{t('view')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
@@ -235,7 +258,7 @@ export default function CertificatesPage() {
|
||||
className='h-7 text-xs'
|
||||
onClick={() => handleOpenEditor(certificate)}
|
||||
>
|
||||
编辑
|
||||
{t('edit')}
|
||||
</Button>
|
||||
{certificate.provider === 'acme' ? (
|
||||
<Button
|
||||
@@ -245,7 +268,7 @@ export default function CertificatesPage() {
|
||||
disabled={renewMutation.isPending}
|
||||
onClick={() => renewMutation.mutate(certificate.id)}
|
||||
>
|
||||
续期
|
||||
{t('renew')}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
@@ -270,7 +293,7 @@ export default function CertificatesPage() {
|
||||
open={importOpen}
|
||||
onOpenChange={setImportOpen}
|
||||
onImported={(certificate) =>
|
||||
toast.success(`证书 ${certificate.name} 已导入`)
|
||||
toast.success(t('imported', { name: certificate.name }))
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -278,7 +301,7 @@ export default function CertificatesPage() {
|
||||
open={applyOpen && !applyCertificate}
|
||||
onOpenChange={setApplyOpen}
|
||||
onApplied={(certificate) =>
|
||||
toast.success(`证书 ${certificate.name} 申请任务已提交`)
|
||||
toast.success(t('applySubmitted', { name: certificate.name }))
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -295,8 +318,8 @@ export default function CertificatesPage() {
|
||||
setApplyCertificate(null);
|
||||
toast.success(
|
||||
applyMode === 'convert-upload'
|
||||
? `证书 ${certificate.name} 转换申请已提交`
|
||||
: `证书 ${certificate.name} 配置已更新,重新申请中...`,
|
||||
? t('convertSubmitted', { name: certificate.name })
|
||||
: t('reapplySubmitted', { name: certificate.name }),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -326,7 +349,7 @@ export default function CertificatesPage() {
|
||||
open={editorOpen}
|
||||
onOpenChange={setEditorOpen}
|
||||
onSaved={(certificate) =>
|
||||
toast.success(`证书 ${certificate.name} 已更新`)
|
||||
toast.success(t('updated', { name: certificate.name }))
|
||||
}
|
||||
onConvert={(certificate) => {
|
||||
setEditorOpen(false);
|
||||
@@ -342,20 +365,20 @@ export default function CertificatesPage() {
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除证书</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('deleteTitle')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确认删除证书 {deleteTarget?.name} 吗?
|
||||
{t('deleteDesc', { name: deleteTarget?.name ?? '' })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogCancel>{tc('cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||
onClick={() =>
|
||||
deleteTarget && deleteMutation.mutate(deleteTarget.id)
|
||||
}
|
||||
>
|
||||
删除
|
||||
{tc('delete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
Reference in New Issue
Block a user