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" />
-
-
-
-
-
-
-
-
-
- } onClick={() => update('attrs', [...form.attrs, ['', '']])} />
-
-
- {form.attrs.map((attr, idx) => (
-
- {`${idx + 1}`}
- {
- const next = form.attrs.map((a, i) => (i === idx ? ([e.target.value, a[1]] as [string, string]) : a));
- update('attrs', next);
- }}
+
+
+
+
+ }>
+ update('sourcePort', v)}
+ placeholder={"53\n443\n1000-2000"}
/>
- }
- onClick={() => update('attrs', form.attrs.filter((_, i) => i !== idx))}
+
+
+
+
+ }>
+ update('vlessRoute', v)}
+ placeholder={"53\n443\n1000-2000"}
/>
-
- ))}
-
-
-
- IP
-
- }
- >
- update('ip', e.target.value)} placeholder="0.0.0.0/8, fc00::/7, geoip:ir" />
-
-
-
- {t('domainName')}
-
- }
- >
- update('domain', e.target.value)} placeholder="google.com, geosite:cn" />
-
-
-
- {t('pages.xray.ruleForm.user')}
-
- }
- >
- update('user', e.target.value)} placeholder="email address" />
-
-
-
- {t('pages.inbounds.port')}
-
- }
- >
- update('port', e.target.value)} placeholder="53,443,1000-2000" />
-
-
-
-
-
-
-
-
-
- {t('pages.xray.ruleForm.balancerTag')}
-
- }
- >
-
+
+
+
+
+
+ }>
+ update('user', v)}
+ placeholder="email address"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+ }>
+ update('ip', v)}
+ placeholder={`0.0.0.0/8\nfc00::/7\ngeoip:ir`}
+ />
+
+
+
+
+ }>
+ update('port', v)}
+ placeholder={`53\n443\n1000-2000`}
+ />
+
+
+
+
+ }>
+ update('domain', v)}
+ placeholder={`google.com\ngeosite:cn`}
+ />
+
+
+
+
+
+
+
+ {t('pages.xray.ruleForm.attributes')}
+
+
+
+
+ {form.attrs.length > 0 && (
+
+ {form.attrs.map((attr, idx) => (
+
+ {`${idx + 1}`}
+ {
+ const next = form.attrs.map((a, i) => (i === idx ? ([e.target.value, a[1]] as [string, string]) : a));
+ update('attrs', next);
+ }}
+ />
+ {
+ const next = form.attrs.map((a, i) => (i === idx ? ([a[0], e.target.value] as [string, string]) : a));
+ update('attrs', next);
+ }}
+ />
+
+ ))}
+
+ )}
);
-}
+}
\ No newline at end of file
diff --git a/web/translation/ar-EG.json b/web/translation/ar-EG.json
index 6e10e486..e9787b3b 100644
--- a/web/translation/ar-EG.json
+++ b/web/translation/ar-EG.json
@@ -1171,7 +1171,8 @@
"info": "معلومات",
"add": "أضف قاعدة",
"edit": "عدل القاعدة",
- "useComma": "عناصر مفصولة بفواصل"
+ "useComma": "عناصر مفصولة بفواصل",
+ "onePerLine": "عنصر واحد لكل سطر"
},
"routing": {
"dragToReorder": "اسحب لإعادة الترتيب"
diff --git a/web/translation/en-US.json b/web/translation/en-US.json
index b7f82163..02eef1b0 100644
--- a/web/translation/en-US.json
+++ b/web/translation/en-US.json
@@ -1171,7 +1171,8 @@
"info": "Info",
"add": "Add Rule",
"edit": "Edit Rule",
- "useComma": "Comma-separated list"
+ "useComma": "Comma-separated list",
+ "onePerLine": "One item per line"
},
"routing": {
"dragToReorder": "Drag to reorder"
diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json
index 3493c659..3144a769 100644
--- a/web/translation/es-ES.json
+++ b/web/translation/es-ES.json
@@ -1171,7 +1171,8 @@
"info": "Info",
"add": "Agregar Regla",
"edit": "Editar Regla",
- "useComma": "Elementos separados por comas"
+ "useComma": "Elementos separados por comas",
+ "onePerLine": "Un elemento por línea"
},
"routing": {
"dragToReorder": "Arrastra para reordenar"
diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json
index 94ffef41..6a7b8a24 100644
--- a/web/translation/fa-IR.json
+++ b/web/translation/fa-IR.json
@@ -1171,7 +1171,8 @@
"info": "اطلاعات",
"add": "افزودن قانون",
"edit": "ویرایش قانون",
- "useComma": "موارد جدا شده با کاما"
+ "useComma": "موارد جدا شده با کاما",
+ "onePerLine": "یک مورد در هر خط"
},
"routing": {
"dragToReorder": "برای تغییر ترتیب بکشید"
diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json
index 56c18e3c..9cb41655 100644
--- a/web/translation/id-ID.json
+++ b/web/translation/id-ID.json
@@ -1171,7 +1171,8 @@
"info": "Info",
"add": "Tambahkan Aturan",
"edit": "Edit Aturan",
- "useComma": "Item yang dipisahkan koma"
+ "useComma": "Item yang dipisahkan koma",
+ "onePerLine": "Satu item per baris"
},
"routing": {
"dragToReorder": "Seret untuk mengurutkan ulang"
diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json
index 5875133f..ae017d4e 100644
--- a/web/translation/ja-JP.json
+++ b/web/translation/ja-JP.json
@@ -1171,7 +1171,8 @@
"info": "情報",
"add": "ルール追加",
"edit": "ルール編集",
- "useComma": "カンマ区切りの項目"
+ "useComma": "カンマ区切りの項目",
+ "onePerLine": "1行につき1項目"
},
"routing": {
"dragToReorder": "ドラッグして並べ替え"
diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json
index 2fa9013c..8eaddcfa 100644
--- a/web/translation/pt-BR.json
+++ b/web/translation/pt-BR.json
@@ -1171,7 +1171,8 @@
"info": "Info",
"add": "Adicionar Regra",
"edit": "Editar Regra",
- "useComma": "Itens separados por vírgula"
+ "useComma": "Itens separados por vírgula",
+ "onePerLine": "Um item por linha"
},
"routing": {
"dragToReorder": "Arraste para reordenar"
diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json
index 15d968c3..7faf15fe 100644
--- a/web/translation/ru-RU.json
+++ b/web/translation/ru-RU.json
@@ -1171,7 +1171,8 @@
"info": "Инфо",
"add": "Создать правило",
"edit": "Редактировать правило",
- "useComma": "Элементы, разделённые запятыми"
+ "useComma": "Элементы, разделённые запятыми",
+ "onePerLine": "Один элемент на строку"
},
"routing": {
"dragToReorder": "Перетащите для изменения порядка"
diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json
index 0ed4a47f..e3dffbe3 100644
--- a/web/translation/tr-TR.json
+++ b/web/translation/tr-TR.json
@@ -1171,7 +1171,8 @@
"info": "Bilgi",
"add": "Kural Ekle",
"edit": "Kuralı Düzenle",
- "useComma": "Virgülle ayrılmış öğeler"
+ "useComma": "Virgülle ayrılmış öğeler",
+ "onePerLine": "Her satırda bir öğe"
},
"routing": {
"dragToReorder": "Yeniden sıralamak için sürükleyin"
diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json
index fac25d90..02e1e053 100644
--- a/web/translation/uk-UA.json
+++ b/web/translation/uk-UA.json
@@ -1171,7 +1171,8 @@
"info": "Інфо",
"add": "Додати правило",
"edit": "Редагувати правило",
- "useComma": "Елементи, розділені комами"
+ "useComma": "Елементи, розділені комами",
+ "onePerLine": "Один елемент на рядок"
},
"routing": {
"dragToReorder": "Перетягніть для зміни порядку"
diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json
index fb238514..c1f59f98 100644
--- a/web/translation/vi-VN.json
+++ b/web/translation/vi-VN.json
@@ -1171,7 +1171,8 @@
"info": "Thông tin",
"add": "Thêm quy tắc",
"edit": "Chỉnh sửa quy tắc",
- "useComma": "Các mục được phân tách bằng dấu phẩy"
+ "useComma": "Các mục được phân tách bằng dấu phẩy",
+ "onePerLine": "Một mục trên mỗi dòng"
},
"routing": {
"dragToReorder": "Kéo để sắp xếp lại"
diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json
index 762fb505..afa7fdc1 100644
--- a/web/translation/zh-CN.json
+++ b/web/translation/zh-CN.json
@@ -1171,7 +1171,8 @@
"info": "信息",
"add": "添加规则",
"edit": "编辑规则",
- "useComma": "逗号分隔的项目"
+ "useComma": "逗号分隔的项目",
+ "onePerLine": "每行一件"
},
"routing": {
"dragToReorder": "拖动以重新排序"
diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json
index b15ffae4..a4e028d1 100644
--- a/web/translation/zh-TW.json
+++ b/web/translation/zh-TW.json
@@ -1171,7 +1171,8 @@
"info": "資訊",
"add": "新增規則",
"edit": "編輯規則",
- "useComma": "逗號分隔的項目"
+ "useComma": "逗號分隔的項目",
+ "onePerLine": "每行一件"
},
"routing": {
"dragToReorder": "拖曳以重新排序"