'use client'; import { useEffect, useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient, type UseQueryResult, } from '@tanstack/react-query'; import { Database, KeyRound, Save, ShieldAlert, X } from 'lucide-react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Spinner } from '@/components/ui/spinner'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import services from '@/lib/services'; import type { SystemConfig } from '@/lib/services/admin'; import { TemplatesManager } from './templates'; import { toast } from 'sonner'; const LOG_RETENTION_FIELDS = [ { key: 'log_retention_days_postgres', label: 'PostgreSQL', description: '访问日志与可观测指标统一保留天数', }, { key: 'log_retention_days_sqlite', label: 'SQLite', description: 'SQLite 日志保留天数', }, { key: 'log_retention_days_clickhouse', label: 'ClickHouse', description: 'ClickHouse 日志保留天数', }, ] as const; interface OperationTabProps { configs: Record; systemConfigsQuery: UseQueryResult; } export function OperationTab({ configs, systemConfigsQuery, }: OperationTabProps) { const queryClient = useQueryClient(); const uploadTypesQuery = useQuery({ queryKey: ['admin', 'upload-types'], queryFn: () => services.adminSystemConfig.listUploadTypes(), }); const businessConfigsQuery = useQuery({ queryKey: ['admin', 'system-configs', 'business'], queryFn: () => services.adminSystemConfig.listSystemConfigs('business'), }); const businessConfigs = useMemo(() => { return (businessConfigsQuery.data ?? []).reduce< Record >((acc, config) => { acc[config.key] = config; return acc; }, {}); }, [businessConfigsQuery.data]); const [retentionValues, setRetentionValues] = useState< Record >({}); useEffect(() => { if (!businessConfigsQuery.data) return; setRetentionValues((prev) => { const next: Record = {}; LOG_RETENTION_FIELDS.forEach((field) => { const config = businessConfigs[field.key]; next[field.key] = config?.value || prev[field.key] || '90'; }); return next; }); }, [businessConfigsQuery.data, businessConfigs]); const updateRetentionMutation = useMutation({ mutationFn: async (values: Record) => { for (const field of LOG_RETENTION_FIELDS) { const raw = (values[field.key] ?? '').trim(); const num = Number(raw); if (!raw || !Number.isInteger(num) || num < 1) { throw new Error(`${field.label}必须为大于等于 1 的整数`); } const config = businessConfigs[field.key]; if (!config) { throw new Error(`缺少配置项: ${field.key}`); } await services.adminSystemConfig.updateSystemConfig(field.key, { value: String(num), description: config.description, }); } }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['admin', 'system-configs'], }); toast.success('日志保留时间已更新'); }, onError: (error: Error) => { toast.error(error.message || '更新日志保留时间失败'); }, }); const updateWhitelistMutation = useMutation({ mutationFn: async (newValue: string) => { const config = configs['file_access_whitelist']; if (!config) { throw new Error('缺少配置项: file_access_whitelist'); } await services.adminSystemConfig.updateSystemConfig( 'file_access_whitelist', { value: newValue, description: config.description, }, ); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['admin', 'system-configs'], }); await queryClient.invalidateQueries({ queryKey: ['public-config'] }); toast.success('文件访问白名单已更新'); }, onError: (error: Error) => { toast.error(error.message || '更新白名单失败'); }, }); const whitelistConfig = configs['file_access_whitelist']; const currentWhitelist = useMemo(() => { if (!whitelistConfig?.value) return ['avatar']; try { const parsed = JSON.parse(whitelistConfig.value); if (Array.isArray(parsed)) return parsed; } catch { // 降级支持逗号分隔解析 return whitelistConfig.value .split(',') .map((s) => s.trim()) .filter(Boolean); } return ['avatar']; }, [whitelistConfig?.value]); const handleAddType = (type: string) => { if (!type || currentWhitelist.includes(type)) return; const newWhitelist = [...currentWhitelist, type]; updateWhitelistMutation.mutate(JSON.stringify(newWhitelist)); }; const handleRemoveType = (typeToRemove: string) => { const newWhitelist = currentWhitelist.filter((t) => t !== typeToRemove); updateWhitelistMutation.mutate(JSON.stringify(newWhitelist)); }; const availableTypes = useMemo(() => { const types = uploadTypesQuery.data ?? []; return types.map((t) => { let label = t; if (t === 'avatar') label = '头像 (avatar)'; else if (t === 'attachment') label = '附件 (attachment)'; else if (t === 'doc') label = '文档 (doc)'; else if (t === 'generic') label = '通用 (generic)'; return { value: t, label }; }); }, [uploadTypesQuery.data]); return (
{/* 文件访问白名单设置 */}
文件访问权限控制 配置免登录直接访问的文件业务类型。不在白名单内的文件将要求登录鉴权。
添加免鉴权类型:
{/* 当前白名单列表 */}
当前免鉴权列表
{currentWhitelist.length > 0 ? (
{currentWhitelist.map((type) => ( {availableTypes.find((t) => t.value === type)?.label || type} ))}
) : (

白名单已空,所有类型文件的访问都将需要登录。

)}
{/* 日志保留时间设置 */}
日志保留时间 配置各日志数据库的日志保留天数,切换日志数据库后自动按对应配置清理过期日志。
{LOG_RETENTION_FIELDS.map((field) => (
setRetentionValues((prev) => ({ ...prev, [field.key]: e.target.value, })) } /> 天

{field.description}

))}
{/* 通知模板管理 */}
); }