This commit is contained in:
ryan
2026-07-13 15:08:40 +08:00
parent 08e8eea932
commit 1eff7878a1
441 changed files with 26491 additions and 18405 deletions
+241 -100
View File
@@ -1,7 +1,7 @@
"use client"
'use client';
import {ComponentType, useMemo} from "react"
import {useMutation, useQueryClient} from "@tanstack/react-query"
import { ComponentType, useMemo } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import {
Bell,
Database,
@@ -21,179 +21,320 @@ import {
Globe,
GitBranch,
ScrollText,
Gauge
} from "lucide-react"
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 {
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';
interface MenuItem {
path: string
label: string
description: string
icon: ComponentType<{ className?: string }>
readOnly?: boolean
path: string;
label: string;
description: string;
icon: ComponentType<{ className?: string }>;
readOnly?: boolean;
}
interface MenuGroup {
name: string
items: MenuItem[]
name: string;
items: MenuItem[];
}
const MENU_GROUPS: MenuGroup[] = [
{
name: "业务菜单",
name: '业务菜单',
items: [
{ path: "/", label: "数据看板", description: "查看边缘节点请求、访客与流量等多维度图表看板", icon: LayoutDashboard, readOnly: true },
{ path: "/nodes", label: "节点管理", description: "注册与管理反向代理边缘节点服务状态", icon: Server },
{ path: "/proxy-routes", label: "规则管理", description: "配置和发布反代路由规则、负载均衡与源站绑定", icon: Route },
{ path: "/websites", label: "域名列表", description: "配置站点根域名并查看关联网站详情", icon: Globe },
{ path: "/certificates", label: "TLS证书", description: "管理和下发网站 SSL/TLS 证书", icon: ShieldCheck },
{ path: "/dns-accounts", label: "DNS账号", description: "配置 Cloudflare/Alidns 等 DNS 服务商 API 凭证", icon: Settings },
{ path: "/origins", label: "源站地址", description: "集中维护和管理后端业务源站服务器组", icon: Home },
{ path: "/waf", label: "WAF 管理", description: "配置 Web 应用防火墙自定义规则与频率限制", icon: ShieldCheck },
{ path: "/ip-groups", label: "IP 组", description: "管理黑白名单 IP 集合,用于 WAF 规则过滤", icon: Layers },
{ path: "/pages", label: "Pages 静态站", description: "托管和发布静态网页项目,支持自动关联域名", icon: FileText },
{ path: "/config-versions", label: "版本发布", description: "查看规则快照版本并进行配置热更新分发与回滚", icon: GitBranch },
{ path: "/access-logs", label: "访问日志", description: "多维度实时浏览边缘节点请求访问明细", icon: ScrollText },
{ path: "/performance", label: "性能调优", description: "配置缓存、图片压缩等页面性能加速策略", icon: Gauge },
]
{
path: '/',
label: '数据看板',
description: '查看边缘节点请求、访客与流量等多维度图表看板',
icon: LayoutDashboard,
readOnly: true,
},
{
path: '/nodes',
label: '节点管理',
description: '注册与管理反向代理边缘节点服务状态',
icon: Server,
},
{
path: '/proxy-routes',
label: '规则管理',
description: '配置和发布反代路由规则、负载均衡与源站绑定',
icon: Route,
},
{
path: '/websites',
label: '域名列表',
description: '配置站点根域名并查看关联网站详情',
icon: Globe,
},
{
path: '/certificates',
label: 'TLS证书',
description: '管理和下发网站 SSL/TLS 证书',
icon: ShieldCheck,
},
{
path: '/dns-accounts',
label: 'DNS账号',
description: '配置 Cloudflare/Alidns 等 DNS 服务商 API 凭证',
icon: Settings,
},
{
path: '/origins',
label: '源站地址',
description: '集中维护和管理后端业务源站服务器组',
icon: Home,
},
{
path: '/waf',
label: 'WAF 管理',
description: '配置 Web 应用防火墙自定义规则与频率限制',
icon: ShieldCheck,
},
{
path: '/ip-groups',
label: 'IP 组',
description: '管理黑白名单 IP 集合,用于 WAF 规则过滤',
icon: Layers,
},
{
path: '/pages',
label: 'Pages 静态站',
description: '托管和发布静态网页项目,支持自动关联域名',
icon: FileText,
},
{
path: '/config-versions',
label: '版本发布',
description: '查看规则快照版本并进行配置热更新分发与回滚',
icon: GitBranch,
},
{
path: '/access-logs',
label: '访问日志',
description: '多维度实时浏览边缘节点请求访问明细',
icon: ScrollText,
},
{
path: '/performance',
label: '性能调优',
description: '配置缓存、图片压缩等页面性能加速策略',
icon: Gauge,
},
],
},
{
name: "管理菜单",
name: '管理菜单',
items: [
{ path: "/admin/users", label: "用户管理", description: "查看和管理系统用户列表及其状态", icon: UserRound },
{ path: "/admin/tasks", label: "任务管理", description: "查看和调度系统异步及定时任务", icon: Layers },
{ path: "/admin/files", label: "存储管理", description: "管理系统文件存储和清理无用文件", icon: FolderOpen },
{ path: "/admin/database", label: "数据管理", description: "监控物理数据库状态、分页浏览表数据及交互式 SQL 查询", icon: Database },
{ path: "/admin/push", label: "通知推送", description: "配置和发送系统通知及推送消息", icon: Bell },
{ path: "/admin/logs", label: "系统日志", description: "查看异步任务执行日志和系统运行情况", icon: Terminal },
{ path: "/admin/system", label: "系统配置", description: "管理和维护系统基础键值对配置", icon: ShieldCheck },
{ path: "/admin/settings", label: "系统设置", description: "配置安全验证、邮箱服务及目录显示", icon: Settings, readOnly: true },
]
{
path: '/admin/users',
label: '用户管理',
description: '查看和管理系统用户列表及其状态',
icon: UserRound,
},
{
path: '/admin/tasks',
label: '任务管理',
description: '查看和调度系统异步及定时任务',
icon: Layers,
},
{
path: '/admin/files',
label: '存储管理',
description: '管理系统文件存储和清理无用文件',
icon: FolderOpen,
},
{
path: '/admin/database',
label: '数据管理',
description: '监控物理数据库状态、分页浏览表数据及交互式 SQL 查询',
icon: Database,
},
{
path: '/admin/push',
label: '通知推送',
description: '配置和发送系统通知及推送消息',
icon: Bell,
},
{
path: '/admin/logs',
label: '系统日志',
description: '查看异步任务执行日志和系统运行情况',
icon: Terminal,
},
{
path: '/admin/system',
label: '系统配置',
description: '管理和维护系统基础键值对配置',
icon: ShieldCheck,
},
{
path: '/admin/settings',
label: '系统设置',
description: '配置安全验证、邮箱服务及目录显示',
icon: Settings,
readOnly: true,
},
],
},
{
name: "文档菜单",
name: '文档菜单',
items: [
{ path: "https://open-flare.pages.dev/", label: "使用文档", description: "面向开发与运营的部署使用指南", icon: FileText },
]
}
]
{
path: 'https://open-flare.pages.dev/',
label: '使用文档',
description: '面向开发与运营的部署使用指南',
icon: FileText,
},
],
},
];
interface OtherTabProps {
configs: Record<string, SystemConfig>
configs: Record<string, SystemConfig>;
}
export function OtherTab({ configs }: OtherTabProps) {
const queryClient = useQueryClient()
const queryClient = useQueryClient();
const menuDisplayConfig = useMemo(() => {
const raw = configs["menu_display_config"]?.value
if (!raw) return {} as Record<string, boolean>
const raw = configs['menu_display_config']?.value;
if (!raw) return {} as Record<string, boolean>;
try {
return JSON.parse(raw) as Record<string, boolean>
return JSON.parse(raw) as Record<string, boolean>;
} catch {
return {} as Record<string, boolean>
return {} as Record<string, boolean>;
}
}, [configs])
}, [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})",
})
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("目录显示配置已更新")
await queryClient.invalidateQueries({
queryKey: ['admin', 'system-configs'],
});
await queryClient.invalidateQueries({ queryKey: ['public-config'] });
toast.success('目录显示配置已更新');
},
onError: (error: Error) => {
toast.error(error.message || "更新配置失败")
toast.error(error.message || '更新配置失败');
},
})
});
const handleMenuToggle = (path: string, checked: boolean) => {
updateMenuConfigMutation.mutate({ path, enabled: checked })
}
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" />
<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">目录显示管理</CardTitle>
<CardDescription className="text-xs">
<CardTitle className='text-base font-semibold'>
目录显示管理
</CardTitle>
<CardDescription className='text-xs'>
配置系统左侧菜单的显示与隐藏状态,适用于所有登录用户。
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="pt-6 space-y-6">
<CardContent className='pt-6 space-y-6'>
{MENU_GROUPS.map((group) => (
<div key={group.name} className="space-y-3">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-muted-foreground tracking-wider uppercase">
<div key={group.name} className='space-y-3'>
<div className='flex items-center gap-2'>
<span className='text-xs font-semibold text-muted-foreground tracking-wider uppercase'>
{group.name}
</span>
<div className="h-px bg-border/40 flex-1" />
<div className='h-px bg-border/40 flex-1' />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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
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"
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">{item.label}</span>
<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'>
{item.label}
</span>
{isReadOnly && (
<span className="text-[9px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground border shrink-0">
<span className='text-[9px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground border shrink-0'>
不可隐藏
</span>
)}
</div>
<p className="text-xs text-muted-foreground leading-normal line-clamp-2">
<p className='text-xs text-muted-foreground leading-normal line-clamp-2'>
{item.description}
</p>
</div>
<div className="flex items-center">
<div className='flex items-center'>
<Switch
checked={checked}
disabled={isReadOnly || updateMenuConfigMutation.isPending}
onCheckedChange={(val) => handleMenuToggle(item.path, val)}
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">安全提示:</span>
<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'>安全提示:</span>
为了防止管理员在关闭“系统设置”后导致无法重新访问此配置页,系统限制了“系统设置”的关闭权限。其它所有菜单均可自由开关,隐藏后对应的分组标题在为空时也会自动隐藏。
</div>
</div>
</CardContent>
</Card>
)
);
}