"use client"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@workspace/ui/components/dialog"; import { Input } from "@workspace/ui/components/input"; import { Label } from "@workspace/ui/components/label"; import { Switch } from "@workspace/ui/components/switch"; import { Textarea } from "@workspace/ui/components/textarea"; import { AlertCircle, Loader2 } from "lucide-react"; import { type RefObject, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; interface NodeGroupFormProps { initialValues?: Partial; allNodeGroups?: API.NodeGroup[]; currentGroupId?: number; loading?: boolean; onSubmit: (values: Record) => Promise; title: string; trigger: React.ReactNode; } const NodeGroupForm = ({ initialValues, allNodeGroups = [], currentGroupId, loading, onSubmit, title, trigger, ref, }: NodeGroupFormProps & { ref?: RefObject }) => { const { t } = useTranslation("group"); const [open, setOpen] = useState(false); const [submitting, setSubmitting] = useState(false); const [conflictError, setConflictError] = useState(""); const [values, setValues] = useState({ name: "", description: "", sort: 0, for_calculation: true, is_expired_group: false, expired_days_limit: 7, max_traffic_gb_expired: 0, speed_limit: 0, min_traffic_gb: 0, max_traffic_gb: 0, }); useEffect(() => { if (open) { setConflictError(""); // 重置冲突错误 if (initialValues) { setValues({ name: initialValues.name || "", description: initialValues.description || "", sort: initialValues.sort ?? 0, for_calculation: initialValues.for_calculation ?? true, is_expired_group: initialValues.is_expired_group ?? false, expired_days_limit: initialValues.expired_days_limit ?? 7, max_traffic_gb_expired: initialValues.max_traffic_gb_expired ?? 0, speed_limit: initialValues.speed_limit ?? 0, min_traffic_gb: initialValues.min_traffic_gb ?? 0, max_traffic_gb: initialValues.max_traffic_gb ?? 0, }); } else { setValues({ name: "", description: "", sort: 0, for_calculation: true, is_expired_group: false, expired_days_limit: 7, max_traffic_gb_expired: 0, speed_limit: 0, min_traffic_gb: 0, max_traffic_gb: 0, }); } } }, [initialValues, open]); // 检测流量区间冲突 const checkTrafficRangeConflict = ( minTraffic: number, maxTraffic: number ): string => { // 如果 min=0 且 max=0,表示不参与流量分组,跳过所有验证 if (minTraffic === 0 && maxTraffic === 0) { return ""; } // 验证区间有效性:min 必须 < max(除非 max=0 表示无上限) if (minTraffic > 0 && maxTraffic > 0 && minTraffic >= maxTraffic) { return t("invalidRange", "Min traffic must be less than max traffic"); } // 处理 max=0 的情况,表示无上限,使用一个很大的数代替 const actualMax = maxTraffic === 0 ? Number.MAX_VALUE : maxTraffic; // 检查与其他节点组的冲突 for (const group of allNodeGroups) { // 跳过当前编辑的节点组 if (currentGroupId && group.id === currentGroupId) { continue; } // 跳过没有设置流量区间的节点组(min=0 且 max=0 表示未配置) const existingMin = group.min_traffic_gb ?? 0; const existingMax = group.max_traffic_gb ?? 0; if (existingMin === 0 && existingMax === 0) { continue; } // 处理现有节点组 max=0 的情况 const actualExistingMax = existingMax === 0 ? Number.MAX_VALUE : existingMax; // 检测区间重叠 // 两个区间 [min1, max1] 和 [min2, max2] 重叠的条件: // max1 > min2 && max2 > min1 const hasOverlap = actualMax > existingMin && actualExistingMax > minTraffic; if (hasOverlap) { return t("rangeConflict", { name: group.name, min: existingMin.toString(), max: existingMax === 0 ? "∞" : existingMax.toString(), }); } } return ""; }; // 检测过期节点组冲突 const checkExpiredGroupConflict = async ( isExpiredGroup: boolean ): Promise => { if (!isExpiredGroup) { return ""; } // 检查是否已存在其他过期节点组 const existingExpiredGroup = allNodeGroups.find( (group) => group.is_expired_group && group.id !== currentGroupId ); if (existingExpiredGroup) { return t( "expiredGroupExists", `System already has an expired node group: ${existingExpiredGroup.name}` ); } // 检查当前节点组是否被订阅商品使用 if (currentGroupId) { try { const { getSubscribeList } = await import( "@workspace/ui/services/admin/subscribe" ); const { data } = await getSubscribeList({ page: 1, size: 1, node_group_id: currentGroupId, }); if (data.data && data.data.total > 0) { return t( "nodeGroupUsedBySubscribe", "This node group is used as default node group in subscription products, cannot set as expired group" ); } } catch (error) { console.error("Failed to check subscribe usage:", error); } } return ""; }; // 检查是否存在其他过期节点组(用于隐藏开关) const hasOtherExpiredGroup = allNodeGroups.some( (group) => group.is_expired_group && group.id !== currentGroupId ); // 当前是否是过期节点组(编辑模式下) const isCurrentExpiredGroup = initialValues?.is_expired_group ?? false; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); // 检测过期节点组冲突 const expiredGroupConflict = await checkExpiredGroupConflict( values.is_expired_group ); if (expiredGroupConflict) { setConflictError(expiredGroupConflict); return; } // 仅在非过期节点组时检测流量区间冲突 if (!values.is_expired_group) { const conflict = checkTrafficRangeConflict( values.min_traffic_gb, values.max_traffic_gb ); if (conflict) { setConflictError(conflict); return; } } setSubmitting(true); const success = await onSubmit(values); setSubmitting(false); if (success) { setOpen(false); setConflictError(""); setValues({ name: "", description: "", sort: 0, for_calculation: true, is_expired_group: false, expired_days_limit: 7, max_traffic_gb_expired: 0, speed_limit: 0, min_traffic_gb: 0, max_traffic_gb: 0, }); } }; return ( {trigger} {title} {t("nodeGroupFormDescription", "Configure node group settings")}
setValues({ ...values, name: e.target.value })} placeholder={t("namePlaceholder", "Enter name")} required value={values.name} />