Files
OpenFlare/frontend/components/common/settings/other-tab.tsx
T
ryan 454542c1d0 docs(i18n): 恢复并补齐英文版 vitepress,README 默认改为英文
- README 默认英文:README.en.md → README.md(英文为默认),中文移至 README.zh-CN.md,语言切换链接同步
- 恢复被删除的 docs/en/ 英文文档(git 历史 cc5e53c5^),删除 4 篇已废弃文件
- 英文导航 config.ts 对齐中文结构(新增 Deployment/Changelog 侧栏,同步 Guide/Design 条目)
- 翻译 15 篇中文新增文档:guide 5 篇(certificates/pages-usage/proxy-config/uptime-kuma/zone-domain-migration)+ design 10 篇(zone-design/cloudflare-pointing/waf-orchestration/origin-error-page/edge-cache-design/pages-design/logstore/kuma-design/login-captcha/observability 三篇)
- en 首页更新(新增 Pages 特性、tagline 同步);changelog 英文入口指向中文版
- vitepress 构建验证:43 个英文页面全部渲染

注意:29 篇旧英文文档为恢复版,部分内容(如 deployment/server、reference/configuration)可能落后于中文,需后续逐篇同步
2026-08-16 23:18:29 +08:00

346 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { ComponentType, useMemo } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import {
Bell,
Database,
FileText,
FolderOpen,
Home,
Info,
Layers,
LayoutList,
Settings,
ShieldCheck,
Terminal,
UserRound,
LayoutDashboard,
Route,
Server,
Globe,
GitBranch,
ScrollText,
Gauge,
} from 'lucide-react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import services from '@/lib/services';
import type { SystemConfig } from '@/lib/services/admin';
import { toast } from 'sonner';
import { useTranslations } from 'next-intl';
interface MenuItem {
path: string;
labelKey: string;
descKey: string;
icon: ComponentType<{ className?: string }>;
readOnly?: boolean;
}
interface MenuGroup {
nameKey: string;
items: MenuItem[];
}
const MENU_GROUPS: MenuGroup[] = [
{
nameKey: 'groupBusiness',
items: [
{
path: '/',
labelKey: 'dashboard',
descKey: 'descDashboard',
icon: LayoutDashboard,
readOnly: true,
},
{
path: '/nodes',
labelKey: 'nodes',
descKey: 'descNodes',
icon: Server,
},
{
path: '/proxy-routes',
labelKey: 'proxyRoutes',
descKey: 'descProxyRoutes',
icon: Route,
},
{
path: '/websites',
labelKey: 'websites',
descKey: 'descWebsites',
icon: Globe,
},
{
path: '/certificates',
labelKey: 'certificates',
descKey: 'descCertificates',
icon: ShieldCheck,
},
{
path: '/dns-accounts',
labelKey: 'dnsAccounts',
descKey: 'descDnsAccounts',
icon: Settings,
},
{
path: '/origins',
labelKey: 'origins',
descKey: 'descOrigins',
icon: Home,
},
{
path: '/waf',
labelKey: 'waf',
descKey: 'descWaf',
icon: ShieldCheck,
},
{
path: '/ip-groups',
labelKey: 'ipGroups',
descKey: 'descIpGroups',
icon: Layers,
},
{
path: '/pages',
labelKey: 'pages',
descKey: 'descPages',
icon: FileText,
},
{
path: '/config-versions',
labelKey: 'configVersions',
descKey: 'descConfigVersions',
icon: GitBranch,
},
{
path: '/access-logs',
labelKey: 'accessLogs',
descKey: 'descAccessLogs',
icon: ScrollText,
},
{
path: '/performance',
labelKey: 'performance',
descKey: 'descPerformance',
icon: Gauge,
},
],
},
{
nameKey: 'groupAdmin',
items: [
{
path: '/admin/users',
labelKey: 'users',
descKey: 'descUsers',
icon: UserRound,
},
{
path: '/admin/tasks',
labelKey: 'tasks',
descKey: 'descTasks',
icon: Layers,
},
{
path: '/admin/files',
labelKey: 'storage',
descKey: 'descStorage',
icon: FolderOpen,
},
{
path: '/admin/database',
labelKey: 'database',
descKey: 'descDatabase',
icon: Database,
},
{
path: '/admin/push',
labelKey: 'push',
descKey: 'descPush',
icon: Bell,
},
{
path: '/admin/logs',
labelKey: 'logs',
descKey: 'descLogs',
icon: Terminal,
},
{
path: '/admin/system',
labelKey: 'system',
descKey: 'descSystem',
icon: ShieldCheck,
},
{
path: '/admin/settings',
labelKey: 'adminSettings',
descKey: 'descAdminSettings',
icon: Settings,
readOnly: true,
},
],
},
{
nameKey: 'groupDocs',
items: [
{
path: 'https://openflare.fyrn.link/',
labelKey: 'usageDocs',
descKey: 'descUsageDocs',
icon: FileText,
},
],
},
];
interface OtherTabProps {
configs: Record<string, SystemConfig>;
}
export function OtherTab({ configs }: OtherTabProps) {
const queryClient = useQueryClient();
const t = useTranslations('settings.other');
const tNav = useTranslations('layout.nav');
const menuDisplayConfig = useMemo(() => {
const raw = configs['menu_display_config']?.value;
if (!raw) return {} as Record<string, boolean>;
try {
return JSON.parse(raw) as Record<string, boolean>;
} catch {
return {} as Record<string, boolean>;
}
}, [configs]);
const updateMenuConfigMutation = useMutation({
mutationFn: async ({
path,
enabled,
}: {
path: string;
enabled: boolean;
}) => {
const newConfig = { ...menuDisplayConfig, [path]: enabled };
const currentCfg = configs['menu_display_config'];
await services.adminSystemConfig.updateSystemConfig(
'menu_display_config',
{
value: JSON.stringify(newConfig),
description:
currentCfg?.description ||
'目录显示配置(JSON 字符串,格式为 {url: enabled})',
},
);
},
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: ['admin', 'system-configs'],
});
await queryClient.invalidateQueries({ queryKey: ['public-config'] });
toast.success(t('menuDisplayUpdated'));
},
onError: (error: Error) => {
toast.error(error.message || t('updateConfigFailed'));
},
});
const handleMenuToggle = (path: string, checked: boolean) => {
updateMenuConfigMutation.mutate({ path, enabled: checked });
};
return (
<Card className='border border-dashed shadow-sm'>
<CardHeader className='border-b border-dashed pb-4'>
<div className='flex items-center gap-2'>
<div className='p-1.5 rounded-lg bg-primary/10 text-primary'>
<LayoutList className='size-4' />
</div>
<div>
<CardTitle className='text-base font-semibold'>
{t('menuDisplayManagement')}
</CardTitle>
<CardDescription className='text-xs'>
{t('menuDisplayManagementDesc')}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className='pt-6 space-y-6'>
{MENU_GROUPS.map((group) => (
<div key={group.nameKey} className='space-y-3'>
<div className='flex items-center gap-2'>
<span className='text-xs font-semibold text-muted-foreground tracking-wider uppercase'>
{t(group.nameKey)}
</span>
<div className='h-px bg-border/40 flex-1' />
</div>
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
{group.items.map((item) => {
const Icon = item.icon;
const isReadOnly = !!item.readOnly;
const checked = menuDisplayConfig[item.path] !== false;
return (
<div
key={item.path}
className='flex items-center justify-between gap-4 rounded-xl border border-dashed p-4 bg-card hover:bg-muted/10 hover:border-primary/30 transition-all duration-300 shadow-sm'
>
<div className='space-y-1.5 flex-1 min-w-0 pr-2'>
<div className='flex items-center gap-2'>
{Icon && (
<Icon className='size-4 text-primary shrink-0' />
)}
<span className='font-medium text-sm text-foreground truncate'>
{tNav(item.labelKey)}
</span>
{isReadOnly && (
<span className='text-[9px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground border shrink-0'>
{t('notHideable')}
</span>
)}
</div>
<p className='text-xs text-muted-foreground leading-normal line-clamp-2'>
{t(item.descKey)}
</p>
</div>
<div className='flex items-center'>
<Switch
checked={checked}
disabled={
isReadOnly || updateMenuConfigMutation.isPending
}
onCheckedChange={(val) =>
handleMenuToggle(item.path, val)
}
/>
</div>
</div>
);
})}
</div>
</div>
))}
<div className='p-3.5 rounded-lg border border-dashed border-primary/20 bg-primary/5 flex items-start gap-2.5'>
<Info className='size-4 text-primary shrink-0 mt-0.5' />
<div className='text-xs text-muted-foreground leading-relaxed'>
<span className='font-semibold text-foreground'>
{t('securityTipTitle')}
</span>
{t('securityTipDesc')}
</div>
</div>
</CardContent>
</Card>
);
}