diff --git a/frontend/src/components/ui/TooltipsHelper.tsx b/frontend/src/components/ui/TooltipsHelper.tsx new file mode 100644 index 00000000..4b443367 --- /dev/null +++ b/frontend/src/components/ui/TooltipsHelper.tsx @@ -0,0 +1,26 @@ +import {useTranslation} from 'react-i18next'; +import {Tooltip} from 'antd'; +import {QuestionCircleOutlined} from '@ant-design/icons'; + +export function LabelWithTooltip({labelKey, tooltipKey}: { + labelKey: string; + tooltipKey: string; +}) { + const {t} = useTranslation(); + + return ( + + {t(labelKey)} + + ); +} + +export function LabelWithOnePerLineTooltip({labelKey}: { + labelKey: string; +}) { + + return +} \ No newline at end of file diff --git a/frontend/src/pages/xray/routing/RuleFormModal.tsx b/frontend/src/pages/xray/routing/RuleFormModal.tsx index cf545c26..ad4a2f34 100644 --- a/frontend/src/pages/xray/routing/RuleFormModal.tsx +++ b/frontend/src/pages/xray/routing/RuleFormModal.tsx @@ -1,9 +1,10 @@ -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Button, Form, Input, Modal, Select, Space, Tooltip } from 'antd'; -import { PlusOutlined, MinusOutlined, QuestionCircleOutlined } from '@ant-design/icons'; -import { InputAddon } from '@/components/ui'; -import { RuleFormSchema, type RuleFormValues } from '@/schemas/xray'; +import {type ChangeEvent, useEffect, useState} from 'react'; +import {useTranslation} from 'react-i18next'; +import {Button, Col, Form, Input, Modal, Row, Select, Space, Typography} from 'antd'; +import {PlusOutlined, MinusOutlined} from '@ant-design/icons'; +import {InputAddon} from '@/components/ui'; +import {RuleFormSchema, type RuleFormValues} from '@/schemas/xray'; +import {LabelWithOnePerLineTooltip, LabelWithTooltip} from "@/components/ui/TooltipsHelper"; export interface RoutingRule { type?: string; @@ -20,6 +21,7 @@ export interface RoutingRule { attrs?: Record; outboundTag?: string; balancerTag?: string; + [key: string]: unknown; } @@ -59,6 +61,30 @@ function csv(value: string): string[] { return value.split(',').map((s) => s.trim()).filter(Boolean); } +const CommaSeparatedTextArea = ({value, onChange, placeholder}: { + value: string; + onChange: (v: string) => void; + placeholder?: string; +}) => { + const displayValue = value ? value.split(',').join('\n') : ''; + + const handleChange = (e: ChangeEvent) => { + const commaSeparated = e.target.value + .split(/\r?\n/) + .join(','); + onChange(commaSeparated); + }; + + return ( + + ); +}; + export default function RuleFormModal({ open, rule, @@ -68,10 +94,10 @@ export default function RuleFormModal({ onClose, onConfirm, }: RuleFormModalProps) { - const { t } = useTranslation(); + const {t} = useTranslation(); const [form, setForm] = useState(initialForm); const isEdit = rule != null; - + useEffect(() => { if (!open) return; if (rule) { @@ -94,10 +120,10 @@ export default function RuleFormModal({ setForm(initialForm()); } }, [open, rule]); - + const update = (key: K, value: FormState[K]) => - setForm((prev) => ({ ...prev, [key]: value })); - + setForm((prev) => ({...prev, [key]: value})); + function submit() { const validated = RuleFormSchema.safeParse(form); if (!validated.success) return; @@ -128,173 +154,216 @@ export default function RuleFormModal({ } onConfirm(out); } - + const title = isEdit ? `${t('edit')} ${t('pages.xray.Routings')}` : `+ ${t('pages.xray.Routings')}`; const okText = isEdit ? t('pages.clients.submitEdit') : t('create'); - + + const rowLayout = {gutter: 16}; + const colLayout = {xs: 24, md: 8}; + return ( -
- - {t('pages.xray.ruleForm.sourceIps')} - - } - > - update('sourceIP', e.target.value)} placeholder="0.0.0.0/8, fc00::/7, geoip:ir" /> - - - - {t('pages.xray.ruleForm.sourcePort')} - - } - > - update('sourcePort', e.target.value)} placeholder="53,443,1000-2000" /> - - - - {t('pages.xray.ruleForm.vlessRoute')} - - } - > - update('vlessRoute', e.target.value)} placeholder="53,443,1000-2000" /> - - - - update('protocol', v)} - options={PROTOCOLS.map((p) => ({ value: p, label: p }))} - /> - - - -