mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-13 02:26:36 +08:00
feat(frontend): 控制台 next-intl 中英双语
接入无 URL 前缀的 zh-CN/en,顶栏与外观设置可切换语言;选择写入 cookie 后刷新生效。
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -43,6 +44,8 @@ export function GroupDialog({
|
||||
pending: boolean;
|
||||
onSubmit: (payload: CloudflareGroupPayload) => void;
|
||||
}) {
|
||||
const t = useTranslations('cloudflare.groupDialog');
|
||||
const tCommon = useTranslations('common');
|
||||
const edgeNodes = useMemo(
|
||||
() => nodes.filter((node) => node.node_type === 'edge_node'),
|
||||
[nodes],
|
||||
@@ -66,14 +69,14 @@ export function GroupDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{group ? '编辑指向分组' : '新增指向分组'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
一期使用主节点作为生效节点;备用节点仅保存,不会自动切换。
|
||||
</DialogDescription>
|
||||
<DialogTitle>
|
||||
{group ? t('editTitle') : t('createTitle')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{t('description')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-group-name'>分组名称</FieldLabel>
|
||||
<FieldLabel htmlFor='cf-group-name'>{t('name')}</FieldLabel>
|
||||
<Input
|
||||
id='cf-group-name'
|
||||
value={name}
|
||||
@@ -81,10 +84,10 @@ export function GroupDialog({
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-primary-node'>主节点</FieldLabel>
|
||||
<FieldLabel htmlFor='cf-primary-node'>{t('primary')}</FieldLabel>
|
||||
<Select value={primaryNodeID} onValueChange={setPrimaryNodeID}>
|
||||
<SelectTrigger id='cf-primary-node' className='w-full'>
|
||||
<SelectValue placeholder='选择带 IPv4 的边缘节点' />
|
||||
<SelectValue placeholder={t('primaryPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -94,7 +97,7 @@ export function GroupDialog({
|
||||
value={String(node.id)}
|
||||
disabled={!node.ip}
|
||||
>
|
||||
{node.name} · {node.ip || '未配置 IP'}
|
||||
{node.name} · {node.ip || t('noIp')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@@ -102,19 +105,19 @@ export function GroupDialog({
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-backup-node'>备用节点</FieldLabel>
|
||||
<FieldLabel htmlFor='cf-backup-node'>{t('backup')}</FieldLabel>
|
||||
<Select value={backupNodeID} onValueChange={setBackupNodeID}>
|
||||
<SelectTrigger id='cf-backup-node' className='w-full'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value='none'>不设置</SelectItem>
|
||||
<SelectItem value='none'>{t('none')}</SelectItem>
|
||||
{edgeNodes
|
||||
.filter((node) => String(node.id) !== primaryNodeID)
|
||||
.map((node) => (
|
||||
<SelectItem key={node.id} value={String(node.id)}>
|
||||
{node.name} · {node.ip || '未配置 IP'}
|
||||
{node.name} · {node.ip || t('noIp')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@@ -123,7 +126,7 @@ export function GroupDialog({
|
||||
</Field>
|
||||
<Field orientation='horizontal'>
|
||||
<FieldLabel htmlFor='cf-default-proxied'>
|
||||
新成员默认开启橙云
|
||||
{t('defaultProxied')}
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id='cf-default-proxied'
|
||||
@@ -132,7 +135,7 @@ export function GroupDialog({
|
||||
/>
|
||||
</Field>
|
||||
<Field orientation='horizontal'>
|
||||
<FieldLabel htmlFor='cf-enabled'>启用同步</FieldLabel>
|
||||
<FieldLabel htmlFor='cf-enabled'>{t('enableSync')}</FieldLabel>
|
||||
<Switch
|
||||
id='cf-enabled'
|
||||
checked={enabled}
|
||||
@@ -142,7 +145,7 @@ export function GroupDialog({
|
||||
</FieldGroup>
|
||||
<DialogFooter>
|
||||
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
{tCommon('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={pending || !name.trim() || !primaryNodeID}
|
||||
@@ -157,7 +160,7 @@ export function GroupDialog({
|
||||
})
|
||||
}
|
||||
>
|
||||
保存
|
||||
{t('save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Check, ChevronDown, Search } from 'lucide-react';
|
||||
|
||||
@@ -72,6 +73,8 @@ export function MemberAddDialog({
|
||||
pending: boolean;
|
||||
onSubmit: (zoneDomainIDs: number[], proxied: boolean) => void;
|
||||
}) {
|
||||
const t = useTranslations('cloudflare.memberDialog');
|
||||
const tCommon = useTranslations('common');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [selectedIDs, setSelectedIDs] = useState<number[]>([]);
|
||||
const [proxied, setProxied] = useState(defaultProxied);
|
||||
@@ -157,15 +160,14 @@ export function MemberAddDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className='sm:max-w-lg'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加域名成员</DialogTitle>
|
||||
<DialogDescription>
|
||||
支持搜索筛选与批量勾选;加入后会创建或接管唯一同名 A 记录,多条同名
|
||||
A 会拒绝同步。
|
||||
</DialogDescription>
|
||||
<DialogTitle>{t('title')}</DialogTitle>
|
||||
<DialogDescription>{t('description')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-domain-search'>Zone 域名</FieldLabel>
|
||||
<FieldLabel htmlFor='cf-domain-search'>
|
||||
{t('zoneDomain')}
|
||||
</FieldLabel>
|
||||
<div className='space-y-2'>
|
||||
<div className='relative'>
|
||||
<Search className='pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground' />
|
||||
@@ -173,7 +175,7 @@ export function MemberAddDialog({
|
||||
id='cf-domain-search'
|
||||
value={keyword}
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
placeholder='搜索域名或顶级域…'
|
||||
placeholder={t('searchPlaceholder')}
|
||||
className='pl-8'
|
||||
disabled={pending}
|
||||
/>
|
||||
@@ -182,9 +184,9 @@ export function MemberAddDialog({
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
<div className='flex items-center gap-2 text-xs text-muted-foreground'>
|
||||
<Badge variant='secondary' className='font-normal'>
|
||||
已选 {selectedIDs.length}
|
||||
{t('selected', { count: selectedIDs.length })}
|
||||
</Badge>
|
||||
<span>可见 {visibleIDs.length}</span>
|
||||
<span>{t('visible', { count: visibleIDs.length })}</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Button
|
||||
@@ -195,7 +197,7 @@ export function MemberAddDialog({
|
||||
disabled={pending || visibleIDs.length === 0}
|
||||
onClick={toggleAllVisible}
|
||||
>
|
||||
{allVisibleSelected ? '取消全选' : '全选可见'}
|
||||
{allVisibleSelected ? t('unselectAll') : t('selectVisible')}
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
@@ -205,18 +207,18 @@ export function MemberAddDialog({
|
||||
disabled={pending || selectedIDs.length === 0}
|
||||
onClick={() => setSelectedIDs([])}
|
||||
>
|
||||
清空
|
||||
{t('clear')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{domains.length === 0 ? (
|
||||
<div className='rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground'>
|
||||
暂无可用域名(均已加入分组,或尚未在网站管理中添加)。
|
||||
{t('noAvailable')}
|
||||
</div>
|
||||
) : filteredGroups.length === 0 ? (
|
||||
<div className='rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground'>
|
||||
没有匹配的域名
|
||||
{t('noMatch')}
|
||||
</div>
|
||||
) : (
|
||||
<div className='max-h-72 space-y-1 overflow-y-auto rounded-lg border p-2'>
|
||||
@@ -233,7 +235,7 @@ export function MemberAddDialog({
|
||||
disabled={pending}
|
||||
onCheckedChange={toggleAllVisible}
|
||||
/>
|
||||
<span>按顶级域分组</span>
|
||||
<span>{t('groupByZone')}</span>
|
||||
</label>
|
||||
|
||||
{filteredGroups.map((group) => {
|
||||
@@ -270,7 +272,9 @@ export function MemberAddDialog({
|
||||
}
|
||||
disabled={pending}
|
||||
onCheckedChange={() => toggleGroup(group)}
|
||||
aria-label={`选择顶级域 ${group.zoneDomain}`}
|
||||
aria-label={t('selectZone', {
|
||||
name: group.zoneDomain,
|
||||
})}
|
||||
className='ml-1'
|
||||
/>
|
||||
<CollapsibleTrigger asChild>
|
||||
@@ -330,7 +334,7 @@ export function MemberAddDialog({
|
||||
variant='secondary'
|
||||
className='shrink-0 text-[10px] font-normal'
|
||||
>
|
||||
顶级域
|
||||
{t('zone')}
|
||||
</Badge>
|
||||
) : null}
|
||||
{checked ? (
|
||||
@@ -350,7 +354,7 @@ export function MemberAddDialog({
|
||||
</div>
|
||||
</Field>
|
||||
<Field orientation='horizontal'>
|
||||
<FieldLabel htmlFor='cf-member-proxied'>开启橙云代理</FieldLabel>
|
||||
<FieldLabel htmlFor='cf-member-proxied'>{t('proxied')}</FieldLabel>
|
||||
<Switch
|
||||
id='cf-member-proxied'
|
||||
checked={proxied}
|
||||
@@ -365,7 +369,7 @@ export function MemberAddDialog({
|
||||
disabled={pending}
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
取消
|
||||
{tCommon('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={pending || selectedIDs.length === 0}
|
||||
@@ -377,8 +381,8 @@ export function MemberAddDialog({
|
||||
}
|
||||
>
|
||||
{selectedIDs.length > 1
|
||||
? `添加并同步(${selectedIDs.length})`
|
||||
: '添加并同步'}
|
||||
? t('submitCount', { count: selectedIDs.length })
|
||||
: t('submit')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
RefreshCw,
|
||||
RotateCcw,
|
||||
} from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -109,20 +110,6 @@ async function listCloudflareSyncExecutions(options: {
|
||||
};
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<TaskExecutionStatus, string> = {
|
||||
pending: '等待中',
|
||||
running: '执行中',
|
||||
succeeded: '成功',
|
||||
failed: '失败',
|
||||
};
|
||||
|
||||
const TRIGGER_LABELS: Record<string, string> = {
|
||||
system: '系统',
|
||||
manual: '手动',
|
||||
retry: '重试',
|
||||
schedule: '定时',
|
||||
};
|
||||
|
||||
function formatDateTime(value?: string | null) {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
@@ -143,6 +130,7 @@ function statusVariant(status: TaskExecutionStatus) {
|
||||
}
|
||||
|
||||
export function SyncTasksPanel() {
|
||||
const t = useTranslations('cloudflare.tasks');
|
||||
const queryClient = useQueryClient();
|
||||
const [page, setPage] = useState(1);
|
||||
const [status, setStatus] = useState<TaskExecutionStatus | 'all'>('all');
|
||||
@@ -175,16 +163,16 @@ export function SyncTasksPanel() {
|
||||
const retryMutation = useMutation({
|
||||
mutationFn: (id: string) => AdminTaskService.retryTaskExecution(id),
|
||||
onSuccess: (taskID) => {
|
||||
toast.success('同步任务已重新下发', {
|
||||
description: `新任务 ID:${taskID}`,
|
||||
toast.success(t('retried'), {
|
||||
description: t('newTaskId', { id: taskID }),
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [...cloudflareQueryKey, 'sync-executions'],
|
||||
});
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
toast.error('任务重试失败', {
|
||||
description: err.message || '未知错误',
|
||||
toast.error(t('retryFailed'), {
|
||||
description: err.message || t('unknownError'),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -197,6 +185,18 @@ export function SyncTasksPanel() {
|
||||
() => Math.max(1, Math.ceil(total / PAGE_SIZE)),
|
||||
[total],
|
||||
);
|
||||
const statusLabels: Record<TaskExecutionStatus, string> = {
|
||||
pending: t('pending'),
|
||||
running: t('running'),
|
||||
succeeded: t('succeeded'),
|
||||
failed: t('failed'),
|
||||
};
|
||||
const triggerLabels: Record<string, string> = {
|
||||
system: t('system'),
|
||||
manual: t('manual'),
|
||||
retry: t('retry'),
|
||||
schedule: t('schedule'),
|
||||
};
|
||||
|
||||
const openDetail = (execution: TaskExecution) => {
|
||||
setPreview(execution);
|
||||
@@ -210,12 +210,9 @@ export function SyncTasksPanel() {
|
||||
<div className='space-y-1'>
|
||||
<CardTitle className='flex items-center gap-2 text-base'>
|
||||
<Activity className='size-4 text-primary' />
|
||||
同步任务
|
||||
{t('title')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
仅展示 Cloudflare
|
||||
域名同步(sync_member)与分组同步(sync_group)的执行记录。
|
||||
</CardDescription>
|
||||
<CardDescription>{t('description')}</CardDescription>
|
||||
</div>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<Select
|
||||
@@ -226,14 +223,14 @@ export function SyncTasksPanel() {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger size='sm' className='w-[120px]'>
|
||||
<SelectValue placeholder='状态' />
|
||||
<SelectValue placeholder={t('statusPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='all'>全部状态</SelectItem>
|
||||
<SelectItem value='pending'>等待中</SelectItem>
|
||||
<SelectItem value='running'>执行中</SelectItem>
|
||||
<SelectItem value='succeeded'>成功</SelectItem>
|
||||
<SelectItem value='failed'>失败</SelectItem>
|
||||
<SelectItem value='all'>{t('allStatus')}</SelectItem>
|
||||
<SelectItem value='pending'>{t('pending')}</SelectItem>
|
||||
<SelectItem value='running'>{t('running')}</SelectItem>
|
||||
<SelectItem value='succeeded'>{t('succeeded')}</SelectItem>
|
||||
<SelectItem value='failed'>{t('failed')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
@@ -247,7 +244,7 @@ export function SyncTasksPanel() {
|
||||
) : (
|
||||
<RefreshCw data-icon='inline-start' />
|
||||
)}
|
||||
刷新
|
||||
{t('refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -257,32 +254,44 @@ export function SyncTasksPanel() {
|
||||
message={
|
||||
executionsQuery.error instanceof Error
|
||||
? executionsQuery.error.message
|
||||
: '加载同步任务失败'
|
||||
: t('loadFailed')
|
||||
}
|
||||
onRetry={() => void executionsQuery.refetch()}
|
||||
/>
|
||||
) : loading && executions.length === 0 ? (
|
||||
<LoadingStateWithBorder
|
||||
icon={Activity}
|
||||
description='加载同步任务中...'
|
||||
description={t('loading')}
|
||||
/>
|
||||
) : executions.length === 0 ? (
|
||||
<EmptyStateWithBorder
|
||||
icon={Activity}
|
||||
title='暂无同步任务'
|
||||
description='添加域名或同步分组后,这里会显示 Cloudflare 域名 / 分组同步执行记录。'
|
||||
title={t('emptyTitle')}
|
||||
description={t('emptyDesc')}
|
||||
/>
|
||||
) : (
|
||||
<div className='rounded-lg border'>
|
||||
<Table className='min-w-[720px]'>
|
||||
<TableHeader>
|
||||
<TableRow className='hover:bg-transparent'>
|
||||
<TableHead className='w-[200px]'>任务</TableHead>
|
||||
<TableHead className='w-[100px]'>状态</TableHead>
|
||||
<TableHead className='w-[100px]'>触发</TableHead>
|
||||
<TableHead className='w-[100px]'>耗时</TableHead>
|
||||
<TableHead className='min-w-[180px]'>结果/错误</TableHead>
|
||||
<TableHead className='w-[160px]'>创建时间</TableHead>
|
||||
<TableHead className='w-[200px]'>
|
||||
{t('columns.task')}
|
||||
</TableHead>
|
||||
<TableHead className='w-[100px]'>
|
||||
{t('columns.status')}
|
||||
</TableHead>
|
||||
<TableHead className='w-[100px]'>
|
||||
{t('columns.trigger')}
|
||||
</TableHead>
|
||||
<TableHead className='w-[100px]'>
|
||||
{t('columns.duration')}
|
||||
</TableHead>
|
||||
<TableHead className='min-w-[180px]'>
|
||||
{t('columns.result')}
|
||||
</TableHead>
|
||||
<TableHead className='w-[160px]'>
|
||||
{t('columns.createdAt')}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -304,12 +313,12 @@ export function SyncTasksPanel() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={statusVariant(execution.status)}>
|
||||
{STATUS_LABELS[execution.status] || execution.status}
|
||||
{statusLabels[execution.status] || execution.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant='outline'>
|
||||
{TRIGGER_LABELS[execution.triggered_by] ||
|
||||
{triggerLabels[execution.triggered_by] ||
|
||||
execution.triggered_by}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
@@ -331,7 +340,7 @@ export function SyncTasksPanel() {
|
||||
|
||||
<div className='flex items-center justify-between gap-2'>
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
共 {total} 条,第 {page}/{totalPages} 页
|
||||
{t('pageSummary', { total, page, pages: totalPages })}
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
@@ -341,7 +350,7 @@ export function SyncTasksPanel() {
|
||||
disabled={page <= 1 || loading}
|
||||
>
|
||||
<ChevronLeft className='size-4' />
|
||||
上一页
|
||||
{t('prev')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
@@ -349,7 +358,7 @@ export function SyncTasksPanel() {
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page >= totalPages || loading}
|
||||
>
|
||||
下一页
|
||||
{t('next')}
|
||||
<ChevronRight className='size-4' />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -359,43 +368,51 @@ export function SyncTasksPanel() {
|
||||
<Sheet open={detailOpen} onOpenChange={setDetailOpen}>
|
||||
<SheetContent className='w-full p-0 sm:max-w-[560px]'>
|
||||
<SheetHeader className='border-b'>
|
||||
<SheetTitle>同步任务详情</SheetTitle>
|
||||
<SheetTitle>{t('detailTitle')}</SheetTitle>
|
||||
<SheetDescription>
|
||||
{selected?.task_name || selected?.task_type || 'Cloudflare 同步'}
|
||||
{selected?.task_name || selected?.task_type || t('defaultName')}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className='flex-1 space-y-4 overflow-y-auto px-4 py-4'>
|
||||
{detailQuery.isFetching && !selected ? (
|
||||
<LoadingStateWithBorder
|
||||
icon={Activity}
|
||||
description='加载任务详情中...'
|
||||
description={t('loadingDetail')}
|
||||
/>
|
||||
) : selected ? (
|
||||
<>
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<div className='rounded-lg border p-3'>
|
||||
<div className='text-xs text-muted-foreground'>状态</div>
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
{t('status')}
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
<Badge variant={statusVariant(selected.status)}>
|
||||
{STATUS_LABELS[selected.status] || selected.status}
|
||||
{statusLabels[selected.status] || selected.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-lg border p-3'>
|
||||
<div className='text-xs text-muted-foreground'>触发</div>
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
{t('trigger')}
|
||||
</div>
|
||||
<div className='mt-2 text-sm font-medium'>
|
||||
{TRIGGER_LABELS[selected.triggered_by] ||
|
||||
{triggerLabels[selected.triggered_by] ||
|
||||
selected.triggered_by}
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-lg border p-3'>
|
||||
<div className='text-xs text-muted-foreground'>重试</div>
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
{t('retries')}
|
||||
</div>
|
||||
<div className='mt-2 font-mono text-sm'>
|
||||
{selected.retry_count}/{selected.max_retry}
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-lg border p-3'>
|
||||
<div className='text-xs text-muted-foreground'>耗时</div>
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
{t('duration')}
|
||||
</div>
|
||||
<div className='mt-2 font-mono text-sm'>
|
||||
{formatDuration(selected.duration)}
|
||||
</div>
|
||||
@@ -403,7 +420,9 @@ export function SyncTasksPanel() {
|
||||
</div>
|
||||
|
||||
<div className='space-y-1'>
|
||||
<div className='text-xs text-muted-foreground'>任务 ID</div>
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
{t('taskId')}
|
||||
</div>
|
||||
<div className='rounded-md border bg-muted/40 px-3 py-2 font-mono text-xs break-all'>
|
||||
{selected.task_id}
|
||||
</div>
|
||||
@@ -412,7 +431,7 @@ export function SyncTasksPanel() {
|
||||
<div className='grid gap-3 sm:grid-cols-2'>
|
||||
<div className='space-y-1'>
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
创建时间
|
||||
{t('createdAt')}
|
||||
</div>
|
||||
<div className='font-mono text-xs'>
|
||||
{formatDateTime(selected.created_at)}
|
||||
@@ -420,7 +439,7 @@ export function SyncTasksPanel() {
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
结束时间
|
||||
{t('finishedAt')}
|
||||
</div>
|
||||
<div className='font-mono text-xs'>
|
||||
{formatDateTime(selected.finished_at)}
|
||||
@@ -429,7 +448,9 @@ export function SyncTasksPanel() {
|
||||
</div>
|
||||
|
||||
<div className='space-y-1'>
|
||||
<div className='text-xs text-muted-foreground'>执行结果</div>
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
{t('result')}
|
||||
</div>
|
||||
<div className='min-h-10 rounded-md border bg-muted/30 px-3 py-2 text-sm whitespace-pre-wrap break-all'>
|
||||
{selected.result || '-'}
|
||||
</div>
|
||||
@@ -438,7 +459,7 @@ export function SyncTasksPanel() {
|
||||
{selected.error_message ? (
|
||||
<div className='space-y-1'>
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
错误信息
|
||||
{t('error')}
|
||||
</div>
|
||||
<div className='rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive whitespace-pre-wrap break-all'>
|
||||
{selected.error_message}
|
||||
@@ -456,7 +477,7 @@ export function SyncTasksPanel() {
|
||||
{selected.log ? (
|
||||
<div className='space-y-1'>
|
||||
<div className='text-xs text-muted-foreground'>
|
||||
执行日志
|
||||
{t('logs')}
|
||||
</div>
|
||||
<pre className='max-h-48 overflow-auto rounded-md border bg-muted/40 p-3 text-xs leading-relaxed'>
|
||||
{selected.log}
|
||||
@@ -478,11 +499,11 @@ export function SyncTasksPanel() {
|
||||
) : (
|
||||
<RotateCcw data-icon='inline-start' />
|
||||
)}
|
||||
重试
|
||||
{t('retry')}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button variant='outline' onClick={() => setDetailOpen(false)}>
|
||||
关闭
|
||||
{t('close')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
@@ -46,6 +47,7 @@ import { GroupDialog } from '../../components/group-dialog';
|
||||
import { MemberAddDialog } from '../../components/member-add-dialog';
|
||||
|
||||
export function CloudflareGroupDetailPageClient() {
|
||||
const t = useTranslations('cloudflare');
|
||||
const params = useParams<{ id: string }>();
|
||||
const groupID = Number(params.id);
|
||||
const queryClient = useQueryClient();
|
||||
@@ -72,7 +74,7 @@ export function CloudflareGroupDetailPageClient() {
|
||||
mutationFn: (payload: CloudflareGroupPayload) =>
|
||||
CloudflareService.updateGroup(groupID, payload),
|
||||
onSuccess: async () => {
|
||||
toast.success('分组配置已更新');
|
||||
toast.success(t('groupUpdated'));
|
||||
setEditOpen(false);
|
||||
await invalidate();
|
||||
},
|
||||
@@ -102,13 +104,13 @@ export function CloudflareGroupDetailPageClient() {
|
||||
if (failed === 0) {
|
||||
toast.success(
|
||||
total === 1
|
||||
? '域名已加入并排队同步'
|
||||
: `已添加 ${succeeded} 个域名并排队同步`,
|
||||
? t('memberAddedOne')
|
||||
: t('memberAddedMany', { count: succeeded }),
|
||||
);
|
||||
} else if (succeeded === 0) {
|
||||
toast.error(`添加失败:${failed} 个域名未能加入`);
|
||||
toast.error(t('memberAddFailed', { count: failed }));
|
||||
} else {
|
||||
toast.warning(`部分成功:${succeeded} 个已加入,${failed} 个失败`);
|
||||
toast.warning(t('memberPartial', { succeeded, failed }));
|
||||
}
|
||||
setAddOpen(false);
|
||||
await invalidate();
|
||||
@@ -124,7 +126,7 @@ export function CloudflareGroupDetailPageClient() {
|
||||
proxied: boolean;
|
||||
}) => CloudflareService.updateMember(groupID, memberID, proxied),
|
||||
onSuccess: async () => {
|
||||
toast.success('橙云设置已更新并排队同步');
|
||||
toast.success(t('proxyUpdated'));
|
||||
await invalidate();
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
@@ -132,14 +134,14 @@ export function CloudflareGroupDetailPageClient() {
|
||||
const syncMutation = useMutation({
|
||||
mutationFn: (memberID: number) =>
|
||||
CloudflareService.syncMember(groupID, memberID),
|
||||
onSuccess: () => toast.success('成员同步任务已入队'),
|
||||
onSuccess: () => toast.success(t('memberSyncQueued')),
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
});
|
||||
const removeMutation = useMutation({
|
||||
mutationFn: (memberID: number) =>
|
||||
CloudflareService.removeMember(groupID, memberID),
|
||||
onSuccess: async () => {
|
||||
toast.success('成员及远端 A 记录已删除');
|
||||
toast.success(t('memberDeleted'));
|
||||
await invalidate();
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
@@ -148,7 +150,10 @@ export function CloudflareGroupDetailPageClient() {
|
||||
if (detailQuery.isLoading)
|
||||
return (
|
||||
<div className='w-full py-6 px-1'>
|
||||
<LoadingStateWithBorder icon={Cloud} description='加载分组详情中...' />
|
||||
<LoadingStateWithBorder
|
||||
icon={Cloud}
|
||||
description={t('loadingDetail')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
if (detailQuery.isError || !detailQuery.data)
|
||||
@@ -168,7 +173,7 @@ export function CloudflareGroupDetailPageClient() {
|
||||
<Button variant='ghost' size='sm' className='self-start' asChild>
|
||||
<Link href='/cloudflare'>
|
||||
<ArrowLeft data-icon='inline-start' />
|
||||
返回列表
|
||||
{t('back')}
|
||||
</Link>
|
||||
</Button>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
@@ -190,7 +195,7 @@ export function CloudflareGroupDetailPageClient() {
|
||||
) : (
|
||||
<RefreshCw data-icon='inline-start' />
|
||||
)}
|
||||
刷新
|
||||
{t('refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
@@ -198,11 +203,11 @@ export function CloudflareGroupDetailPageClient() {
|
||||
onClick={() => setEditOpen(true)}
|
||||
>
|
||||
<Settings data-icon='inline-start' />
|
||||
编辑
|
||||
{t('edit')}
|
||||
</Button>
|
||||
<Button size='sm' onClick={() => setAddOpen(true)}>
|
||||
<Plus data-icon='inline-start' />
|
||||
添加域名
|
||||
{t('addDomain')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,41 +215,50 @@ export function CloudflareGroupDetailPageClient() {
|
||||
|
||||
<Card className='border-dashed shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-base'>当前指向</CardTitle>
|
||||
<CardTitle className='text-base'>{t('currentPoint')}</CardTitle>
|
||||
<CardDescription>
|
||||
生效节点 {group.active_node.name} · {group.active_node.ip}
|
||||
{t('activeNode', {
|
||||
name: group.active_node.name,
|
||||
ip: group.active_node.ip,
|
||||
})}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='flex flex-wrap gap-2'>
|
||||
<Badge variant={group.enabled ? 'default' : 'secondary'}>
|
||||
{group.enabled ? '同步已启用' : '同步已停用'}
|
||||
{group.enabled ? t('syncEnabled') : t('syncDisabled')}
|
||||
</Badge>
|
||||
<Badge variant='outline'>主节点 {group.primary_node.name}</Badge>
|
||||
<Badge variant='outline'>
|
||||
备用节点 {group.backup_node?.name ?? '未设置'}
|
||||
{t('primaryNode', { name: group.primary_node.name })}
|
||||
</Badge>
|
||||
<Badge variant='outline'>
|
||||
{t('backupNode', {
|
||||
name: group.backup_node?.name ?? t('backupUnset'),
|
||||
})}
|
||||
</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className='border-dashed shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-base'>域名成员</CardTitle>
|
||||
<CardDescription>成员级橙云是同步时的唯一依据。</CardDescription>
|
||||
<CardTitle className='text-base'>{t('members')}</CardTitle>
|
||||
<CardDescription>{t('membersDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{members.length === 0 ? (
|
||||
<p className='py-8 text-center text-sm text-muted-foreground'>
|
||||
暂无域名成员。
|
||||
{t('noMembers')}
|
||||
</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>域名</TableHead>
|
||||
<TableHead>期望 IP</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>橙云</TableHead>
|
||||
<TableHead className='text-right'>操作</TableHead>
|
||||
<TableHead>{t('columns.domain')}</TableHead>
|
||||
<TableHead>{t('columns.desiredIp')}</TableHead>
|
||||
<TableHead>{t('columns.status')}</TableHead>
|
||||
<TableHead>{t('columns.proxied')}</TableHead>
|
||||
<TableHead className='text-right'>
|
||||
{t('columns.actions')}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -258,7 +272,9 @@ export function CloudflareGroupDetailPageClient() {
|
||||
</p>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell>{member.desired_ip || '待同步'}</TableCell>
|
||||
<TableCell>
|
||||
{member.desired_ip || t('pendingSync')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
@@ -291,7 +307,7 @@ export function CloudflareGroupDetailPageClient() {
|
||||
onClick={() => syncMutation.mutate(member.id)}
|
||||
>
|
||||
<RefreshCw data-icon='inline-start' />
|
||||
同步
|
||||
{t('sync')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='destructive'
|
||||
@@ -299,7 +315,7 @@ export function CloudflareGroupDetailPageClient() {
|
||||
onClick={() => removeMutation.mutate(member.id)}
|
||||
>
|
||||
<Trash2 data-icon='inline-start' />
|
||||
移出
|
||||
{t('remove')}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Suspense } from 'react';
|
||||
import { Cloud } from 'lucide-react';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { LoadingStateWithBorder } from '@/components/layout/loading';
|
||||
|
||||
@@ -11,13 +12,14 @@ export async function generateStaticParams() {
|
||||
}
|
||||
|
||||
export default async function CloudflareGroupDetailPage() {
|
||||
const t = await getTranslations('cloudflare');
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className='w-full py-6 px-1'>
|
||||
<LoadingStateWithBorder
|
||||
icon={Cloud}
|
||||
description='加载分组详情中...'
|
||||
description={t('loadingDetail')}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -48,6 +49,8 @@ import {
|
||||
import { getErrorMessage } from '../websites/components/website-utils';
|
||||
|
||||
export default function CloudflarePage() {
|
||||
const t = useTranslations('cloudflare');
|
||||
const tCommon = useTranslations('common');
|
||||
const queryClient = useQueryClient();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState<CloudflareGroup | null>(
|
||||
@@ -72,7 +75,7 @@ export default function CloudflarePage() {
|
||||
mutationFn: (payload: CloudflareGroupPayload) =>
|
||||
CloudflareService.createGroup(payload),
|
||||
onSuccess: async () => {
|
||||
toast.success('指向分组已创建');
|
||||
toast.success(t('created'));
|
||||
setCreateOpen(false);
|
||||
await invalidate();
|
||||
},
|
||||
@@ -81,7 +84,7 @@ export default function CloudflarePage() {
|
||||
const syncMutation = useMutation({
|
||||
mutationFn: (id: number) => CloudflareService.syncGroup(id),
|
||||
onSuccess: async () => {
|
||||
toast.success('分组同步任务已入队');
|
||||
toast.success(t('syncQueued'));
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [...cloudflareQueryKey, 'sync-executions'],
|
||||
});
|
||||
@@ -91,7 +94,7 @@ export default function CloudflarePage() {
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => CloudflareService.deleteGroup(id),
|
||||
onSuccess: async () => {
|
||||
toast.success('指向分组及远端记录已删除');
|
||||
toast.success(t('deleted'));
|
||||
setDeleteTarget(null);
|
||||
await invalidate();
|
||||
},
|
||||
@@ -105,18 +108,20 @@ export default function CloudflarePage() {
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Cloud className='size-5 text-primary' />
|
||||
<h1 className='text-2xl font-semibold tracking-tight'>Cloudflare</h1>
|
||||
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||
{t('title')}
|
||||
</h1>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button asChild variant='outline' size='sm'>
|
||||
<Link href='/cloudflare/settings'>
|
||||
<Settings data-icon='inline-start' />
|
||||
连接设置
|
||||
{t('connectionSettings')}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size='sm' onClick={() => setCreateOpen(true)}>
|
||||
<Plus data-icon='inline-start' />
|
||||
新增分组
|
||||
{t('addGroup')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,7 +129,7 @@ export default function CloudflarePage() {
|
||||
{overviewQuery.isLoading ? (
|
||||
<LoadingStateWithBorder
|
||||
icon={Cloud}
|
||||
description='加载 Cloudflare 总览中...'
|
||||
description={t('loadingOverview')}
|
||||
/>
|
||||
) : overviewQuery.isError ? (
|
||||
<ErrorInline
|
||||
@@ -134,14 +139,11 @@ export default function CloudflarePage() {
|
||||
) : !overview?.connection.ready ? (
|
||||
<Alert>
|
||||
<AlertTriangle />
|
||||
<AlertTitle>Cloudflare 连接尚未就绪</AlertTitle>
|
||||
<AlertTitle>{t('notReadyTitle')}</AlertTitle>
|
||||
<AlertDescription className='flex flex-col items-start gap-3'>
|
||||
<span>
|
||||
请先导入现有 Cloudflare DNS 账号,或录入独立 API Token
|
||||
并完成连接测试。
|
||||
</span>
|
||||
<span>{t('notReadyDesc')}</span>
|
||||
<Button asChild size='sm'>
|
||||
<Link href='/cloudflare/settings'>配置连接</Link>
|
||||
<Link href='/cloudflare/settings'>{t('configure')}</Link>
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -149,16 +151,14 @@ export default function CloudflarePage() {
|
||||
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div>
|
||||
<h2 className='text-lg font-semibold'>指向分组</h2>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
管理 Cloudflare A 记录对应的节点与域名成员。
|
||||
</p>
|
||||
<h2 className='text-lg font-semibold'>{t('groupsTitle')}</h2>
|
||||
<p className='text-sm text-muted-foreground'>{t('groupsDesc')}</p>
|
||||
</div>
|
||||
|
||||
{groupsQuery.isLoading ? (
|
||||
<LoadingStateWithBorder
|
||||
icon={Cloud}
|
||||
description='加载指向分组中...'
|
||||
description={t('loadingGroups')}
|
||||
/>
|
||||
) : groupsQuery.isError ? (
|
||||
<ErrorInline
|
||||
@@ -168,9 +168,9 @@ export default function CloudflarePage() {
|
||||
) : (groupsQuery.data ?? []).length === 0 ? (
|
||||
<EmptyStateWithBorder
|
||||
icon={Cloud}
|
||||
title='暂无指向分组'
|
||||
description='创建分组后,可将域名成员同步到指定的边缘节点。'
|
||||
actionText='新增分组'
|
||||
title={t('emptyTitle')}
|
||||
description={t('emptyDesc')}
|
||||
actionText={t('addGroup')}
|
||||
onAction={() => setCreateOpen(true)}
|
||||
/>
|
||||
) : (
|
||||
@@ -186,20 +186,22 @@ export default function CloudflarePage() {
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant={group.enabled ? 'default' : 'secondary'}>
|
||||
{group.enabled ? '已启用' : '已停用'}
|
||||
{group.enabled ? t('enabled') : t('disabled')}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className='flex flex-col gap-4'>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
成员 {group.member_count} 个 · 新成员默认
|
||||
{group.default_proxied ? '开启' : '关闭'}橙云
|
||||
{t('memberSummary', {
|
||||
count: group.member_count,
|
||||
proxy: group.default_proxied ? t('proxyOn') : t('proxyOff'),
|
||||
})}
|
||||
</p>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button asChild size='sm'>
|
||||
<Link href={`/cloudflare/groups/${group.id}`}>
|
||||
<Settings data-icon='inline-start' />
|
||||
管理
|
||||
{t('manage')}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
@@ -208,7 +210,7 @@ export default function CloudflarePage() {
|
||||
onClick={() => syncMutation.mutate(group.id)}
|
||||
>
|
||||
<RefreshCw data-icon='inline-start' />
|
||||
同步
|
||||
{t('sync')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='destructive'
|
||||
@@ -216,7 +218,7 @@ export default function CloudflarePage() {
|
||||
onClick={() => setDeleteTarget(group)}
|
||||
>
|
||||
<Trash2 data-icon='inline-start' />
|
||||
删除
|
||||
{t('delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -242,20 +244,19 @@ export default function CloudflarePage() {
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>删除指向分组</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('deleteTitle')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
将删除 {deleteTarget?.name} 的全部成员及本模块管理的远端 A
|
||||
记录。此操作不可撤销。
|
||||
{t('deleteDesc', { name: deleteTarget?.name ?? '' })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogCancel>{tCommon('cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() =>
|
||||
deleteTarget && deleteMutation.mutate(deleteTarget.id)
|
||||
}
|
||||
>
|
||||
确认删除
|
||||
{t('confirmDelete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ArrowLeft, Cloud, Save, ShieldCheck, Trash2 } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
@@ -42,6 +43,7 @@ import { getErrorMessage } from '../../websites/components/website-utils';
|
||||
const dnsAccountsQueryKey = ['openflare', 'dns-accounts'] as const;
|
||||
|
||||
export default function CloudflareSettingsPage() {
|
||||
const t = useTranslations('cloudflare.settings');
|
||||
const queryClient = useQueryClient();
|
||||
const [source, setSource] =
|
||||
useState<CloudflareConnectionSource>('dns_account');
|
||||
@@ -85,7 +87,7 @@ export default function CloudflareSettingsPage() {
|
||||
api_token: source === 'standalone' ? apiToken : '',
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
toast.success('Cloudflare 连接配置已保存');
|
||||
toast.success(t('saved'));
|
||||
setAPIToken('');
|
||||
await refresh();
|
||||
},
|
||||
@@ -95,7 +97,7 @@ export default function CloudflareSettingsPage() {
|
||||
const verifyMutation = useMutation({
|
||||
mutationFn: () => CloudflareService.verifyConnection(),
|
||||
onSuccess: async () => {
|
||||
toast.success('Cloudflare 连接验证成功');
|
||||
toast.success(t('verified'));
|
||||
await refresh();
|
||||
},
|
||||
onError: (error) => toast.error(getErrorMessage(error)),
|
||||
@@ -104,7 +106,7 @@ export default function CloudflareSettingsPage() {
|
||||
const clearMutation = useMutation({
|
||||
mutationFn: () => CloudflareService.clearConnection(),
|
||||
onSuccess: async () => {
|
||||
toast.success('Cloudflare 连接已清除');
|
||||
toast.success(t('cleared'));
|
||||
setDNSAccountID('');
|
||||
setAPIToken('');
|
||||
await refresh();
|
||||
@@ -118,13 +120,13 @@ export default function CloudflareSettingsPage() {
|
||||
<Button variant='ghost' size='sm' className='self-start' asChild>
|
||||
<Link href='/cloudflare'>
|
||||
<ArrowLeft data-icon='inline-start' />
|
||||
返回列表
|
||||
{t('back')}
|
||||
</Link>
|
||||
</Button>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Cloud className='size-5 text-primary' />
|
||||
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||
Cloudflare 连接设置
|
||||
{t('title')}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,16 +140,15 @@ export default function CloudflareSettingsPage() {
|
||||
|
||||
<Card className='border-dashed shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-base'>凭据来源</CardTitle>
|
||||
<CardTitle className='text-base'>{t('sourceTitle')}</CardTitle>
|
||||
<CardDescription>
|
||||
Token 建议授予 Zone:Read 与 DNS:Edit 权限;Token 不会在 API
|
||||
或页面中回显。
|
||||
{t('sourceDesc')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-source'>连接来源</FieldLabel>
|
||||
<FieldLabel htmlFor='cf-source'>{t('source')}</FieldLabel>
|
||||
<Select
|
||||
value={source}
|
||||
onValueChange={(value) =>
|
||||
@@ -160,9 +161,11 @@ export default function CloudflareSettingsPage() {
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value='dns_account'>
|
||||
导入现有 DNS 账号
|
||||
{t('importDns')}
|
||||
</SelectItem>
|
||||
<SelectItem value='standalone'>
|
||||
{t('standalone')}
|
||||
</SelectItem>
|
||||
<SelectItem value='standalone'>独立 API Token</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -171,11 +174,11 @@ export default function CloudflareSettingsPage() {
|
||||
{source === 'dns_account' ? (
|
||||
<Field>
|
||||
<FieldLabel htmlFor='cf-dns-account'>
|
||||
Cloudflare DNS 账号
|
||||
{t('dnsAccount')}
|
||||
</FieldLabel>
|
||||
<Select value={dnsAccountID} onValueChange={setDNSAccountID}>
|
||||
<SelectTrigger id='cf-dns-account' className='w-full'>
|
||||
<SelectValue placeholder='选择 DNS 账号' />
|
||||
<SelectValue placeholder={t('selectAccount')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -188,8 +191,9 @@ export default function CloudflareSettingsPage() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>
|
||||
仅显示类型为 cloudflare 的 DNS 账号。可前往{' '}
|
||||
<Link href='/dns-accounts'>DNS 账号</Link> 新增。
|
||||
{t('dnsHint')}{' '}
|
||||
<Link href='/dns-accounts'>{t('dnsLink')}</Link>
|
||||
{t('dnsHintAfter')}
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
) : (
|
||||
@@ -203,8 +207,8 @@ export default function CloudflareSettingsPage() {
|
||||
onChange={(event) => setAPIToken(event.target.value)}
|
||||
placeholder={
|
||||
connectionQuery.data?.configured
|
||||
? '留空不会回显现有 Token;保存将替换'
|
||||
: '输入 Cloudflare API Token'
|
||||
? t('tokenKeep')
|
||||
: t('tokenEnter')
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
@@ -219,7 +223,7 @@ export default function CloudflareSettingsPage() {
|
||||
}
|
||||
>
|
||||
<Save data-icon='inline-start' />
|
||||
保存配置
|
||||
{t('save')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
@@ -229,7 +233,7 @@ export default function CloudflareSettingsPage() {
|
||||
}
|
||||
>
|
||||
<ShieldCheck data-icon='inline-start' />
|
||||
测试连接
|
||||
{t('test')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='destructive'
|
||||
@@ -239,7 +243,7 @@ export default function CloudflareSettingsPage() {
|
||||
}
|
||||
>
|
||||
<Trash2 data-icon='inline-start' />
|
||||
清除连接
|
||||
{t('clear')}
|
||||
</Button>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
@@ -248,11 +252,11 @@ export default function CloudflareSettingsPage() {
|
||||
|
||||
<Alert>
|
||||
<ShieldCheck />
|
||||
<AlertTitle>当前状态</AlertTitle>
|
||||
<AlertTitle>{t('statusTitle')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{connectionQuery.data?.ready
|
||||
? 'Token 已验证,Cloudflare 指向同步可以执行。'
|
||||
: '保存配置后仍需测试连接;未验证状态下同步任务会被拒绝。'}
|
||||
? t('ready')
|
||||
: t('notReady')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user