diff --git a/frontend/src/pages/xray/XrayPage.tsx b/frontend/src/pages/xray/XrayPage.tsx index 08d4ed14..53b2909e 100644 --- a/frontend/src/pages/xray/XrayPage.tsx +++ b/frontend/src/pages/xray/XrayPage.tsx @@ -94,9 +94,6 @@ export default function XrayPage() { [setTemplateSettings], ); - const warpExist = !!templateSettings?.outbounds?.find((o) => o?.tag === 'warp'); - const nordExist = !!templateSettings?.outbounds?.find((o) => o?.tag?.startsWith?.('nord-')); - async function onTestOutbound(idx: number, mode: string) { const outbound = templateSettings?.outbounds?.[idx]; if (outbound) await testOutbound(idx, outbound, mode); @@ -287,10 +284,6 @@ export default function XrayPage() { setTemplateSettings={setTemplateSettings} outboundTestUrl={outboundTestUrl} onChangeOutboundTestUrl={setOutboundTestUrl} - warpExist={warpExist} - nordExist={nordExist} - onShowWarp={() => setWarpOpen(true)} - onShowNord={() => setNordOpen(true)} onResetDefault={resetToDefault} /> ); diff --git a/frontend/src/pages/xray/basics/BasicsTab.tsx b/frontend/src/pages/xray/basics/BasicsTab.tsx index 1e872d01..67fad5b5 100644 --- a/frontend/src/pages/xray/basics/BasicsTab.tsx +++ b/frontend/src/pages/xray/basics/BasicsTab.tsx @@ -2,13 +2,10 @@ import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Button, Input, Modal, Select, Space, Switch, Tabs } from 'antd'; import { - ApiOutlined, BarChartOutlined, - CloudOutlined, FileTextOutlined, ReloadOutlined, SettingOutlined, - SwapOutlined, } from '@ant-design/icons'; import { OutboundDomainStrategies } from '@/schemas/primitives'; @@ -20,29 +17,17 @@ import './BasicsTab.css'; import { ACCESS_LOG, - BITTORRENT_PROTOCOLS, - BLOCK_DOMAINS_OPTIONS, - DOMAINS_OPTIONS, ERROR_LOG, - IPS_OPTIONS, LOG_LEVELS, MASK_ADDRESS, ROUTING_DOMAIN_STRATEGIES, - SERVICES_OPTIONS, - directSettings, - ipv4Settings, } from './constants'; -import { ruleGetter, ruleSetter, syncOutbound } from './helpers'; interface BasicsTabProps { templateSettings: XraySettingsValue | null; setTemplateSettings: SetTemplate; outboundTestUrl: string; onChangeOutboundTestUrl: (v: string) => void; - warpExist: boolean; - nordExist: boolean; - onShowWarp: () => void; - onShowNord: () => void; onResetDefault: () => void; } @@ -51,10 +36,6 @@ export default function BasicsTab({ setTemplateSettings, outboundTestUrl, onChangeOutboundTestUrl, - warpExist, - nordExist, - onShowWarp, - onShowNord, onResetDefault, }: BasicsTabProps) { const { t } = useTranslation(); @@ -92,19 +73,6 @@ export default function BasicsTab({ const log = (templateSettings?.log || {}) as Record; const policy = (templateSettings?.policy?.system || {}) as Record; - const blockedIPs = ruleGetter(templateSettings, 'blocked', 'ip'); - const blockedDomains = ruleGetter(templateSettings, 'blocked', 'domain'); - const blockedProtocols = ruleGetter(templateSettings, 'blocked', 'protocol'); - const directIPs = ruleGetter(templateSettings, 'direct', 'ip'); - const directDomains = ruleGetter(templateSettings, 'direct', 'domain'); - const ipv4Domains = ruleGetter(templateSettings, 'IPv4', 'domain'); - const warpDomains = ruleGetter(templateSettings, 'warp', 'domain'); - const nordTag = - templateSettings?.outbounds?.find((o) => o?.tag?.startsWith?.('nord-'))?.tag || 'nord'; - const nordDomains = ruleGetter(templateSettings, nordTag, 'domain'); - - const torrentActive = BITTORRENT_PROTOCOLS.every((p) => blockedProtocols.includes(p)); - const items = [ { key: '1', @@ -277,165 +245,6 @@ export default function BasicsTab({ ), }, - { - key: '4', - label: catTabLabel(, t('pages.xray.basicRouting'), isMobile), - children: ( - <> - - - mutate((tt) => { - const next = checked - ? [...blockedProtocols, ...BITTORRENT_PROTOCOLS] - : blockedProtocols.filter((d) => !BITTORRENT_PROTOCOLS.includes(d)); - ruleSetter(tt, 'blocked', 'protocol', next); - })} - /> - } - /> - - mutate((tt) => ruleSetter(tt, 'blocked', 'ip', v))} - /> - } - /> - - mutate((tt) => ruleSetter(tt, 'blocked', 'domain', v))} - /> - } - /> - - - - mutate((tt) => { - ruleSetter(tt, 'direct', 'ip', v); - syncOutbound(tt, 'direct', directSettings); - })} - /> - } - /> - - mutate((tt) => { - ruleSetter(tt, 'direct', 'domain', v); - syncOutbound(tt, 'direct', directSettings); - })} - /> - } - /> - - mutate((tt) => { - ruleSetter(tt, 'IPv4', 'domain', v); - syncOutbound(tt, 'IPv4', ipv4Settings); - })} - /> - } - /> - - mutate((tt) => ruleSetter(tt, 'warp', 'domain', v))} - /> - ) : ( - - ) - } - /> - - mutate((tt) => ruleSetter(tt, nordTag, 'domain', v))} - /> - ) : ( - - ) - } - /> - - ), - }, { key: 'reset', label: catTabLabel(, t('pages.settings.resetDefaultConfig'), isMobile), diff --git a/frontend/src/pages/xray/routing/RoutingBasic.tsx b/frontend/src/pages/xray/routing/RoutingBasic.tsx new file mode 100644 index 00000000..a6e60179 --- /dev/null +++ b/frontend/src/pages/xray/routing/RoutingBasic.tsx @@ -0,0 +1,160 @@ +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Alert, Select, Switch } from 'antd'; + +import { SettingListItem } from '@/components/ui'; +import type { XraySettingsValue, SetTemplate } from '@/hooks/useXraySetting'; +import { + BITTORRENT_PROTOCOLS, + BLOCK_DOMAINS_OPTIONS, + DOMAINS_OPTIONS, + IPS_OPTIONS, + SERVICES_OPTIONS, + directSettings, + ipv4Settings, +} from '../basics/constants'; +import { ruleGetter, ruleSetter, syncOutbound } from '../basics/helpers'; + +interface RoutingBasicProps { + templateSettings: XraySettingsValue | null; + setTemplateSettings: SetTemplate; +} + +export default function RoutingBasic({ templateSettings, setTemplateSettings }: RoutingBasicProps) { + const { t } = useTranslation(); + + const mutate = useCallback( + (mutator: (next: XraySettingsValue) => void) => { + setTemplateSettings((prev) => { + if (!prev) return prev; + const clone = JSON.parse(JSON.stringify(prev)) as XraySettingsValue; + mutator(clone); + return clone; + }); + }, + [setTemplateSettings], + ); + + const blockedIPs = ruleGetter(templateSettings, 'blocked', 'ip'); + const blockedDomains = ruleGetter(templateSettings, 'blocked', 'domain'); + const blockedProtocols = ruleGetter(templateSettings, 'blocked', 'protocol'); + const directIPs = ruleGetter(templateSettings, 'direct', 'ip'); + const directDomains = ruleGetter(templateSettings, 'direct', 'domain'); + const ipv4Domains = ruleGetter(templateSettings, 'IPv4', 'domain'); + + const torrentActive = BITTORRENT_PROTOCOLS.every((p) => blockedProtocols.includes(p)); + + return ( + <> + + + mutate((tt) => { + const next = checked + ? [...blockedProtocols, ...BITTORRENT_PROTOCOLS] + : blockedProtocols.filter((d) => !BITTORRENT_PROTOCOLS.includes(d)); + ruleSetter(tt, 'blocked', 'protocol', next); + })} + /> + } + /> + + mutate((tt) => ruleSetter(tt, 'blocked', 'ip', v))} + /> + } + /> + + mutate((tt) => ruleSetter(tt, 'blocked', 'domain', v))} + /> + } + /> + + + + mutate((tt) => { + ruleSetter(tt, 'direct', 'ip', v); + syncOutbound(tt, 'direct', directSettings); + })} + /> + } + /> + + mutate((tt) => { + ruleSetter(tt, 'direct', 'domain', v); + syncOutbound(tt, 'direct', directSettings); + })} + /> + } + /> + + mutate((tt) => { + ruleSetter(tt, 'IPv4', 'domain', v); + syncOutbound(tt, 'IPv4', ipv4Settings); + })} + /> + } + /> + + ); +} diff --git a/frontend/src/pages/xray/routing/RoutingTab.css b/frontend/src/pages/xray/routing/RoutingTab.css index d79b2f17..1d3590f4 100644 --- a/frontend/src/pages/xray/routing/RoutingTab.css +++ b/frontend/src/pages/xray/routing/RoutingTab.css @@ -231,3 +231,7 @@ opacity: 0.4; } +.hint-alert { + text-align: center; +} + diff --git a/frontend/src/pages/xray/routing/RoutingTab.tsx b/frontend/src/pages/xray/routing/RoutingTab.tsx index feeffece..15aad661 100644 --- a/frontend/src/pages/xray/routing/RoutingTab.tsx +++ b/frontend/src/pages/xray/routing/RoutingTab.tsx @@ -1,8 +1,10 @@ import { useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Modal, Space, Table } from 'antd'; -import { PlusOutlined } from '@ant-design/icons'; +import { Button, Modal, Space, Table, Tabs } from 'antd'; +import { ControlOutlined, PlusOutlined, UnorderedListOutlined } from '@ant-design/icons'; +import { catTabLabel } from '@/pages/settings/catTabLabel'; +import RoutingBasic from './RoutingBasic'; import RuleFormModal from './RuleFormModal'; import type { RoutingRule } from './RuleFormModal'; import RuleCardList from './RuleCardList'; @@ -226,9 +228,14 @@ export default function RoutingTab({ document.addEventListener('pointercancel', onUp); } + const hasSource = rows.some((r) => r.sourceIP || r.sourcePort || r.vlessRoute); + const hasBalancer = rows.some((r) => r.balancerTag); + const desktopColumns = useRoutingColumns({ isMobile, rowsLength: rows.length, + showSource: hasSource, + showBalancer: hasBalancer, onHandlePointerDown, openEdit, moveUp, @@ -236,56 +243,81 @@ export default function RoutingTab({ confirmDelete, }); + const tableScrollX = desktopColumns.reduce((sum, c) => { + const col = c as { width?: number; hidden?: boolean }; + return col.hidden ? sum : sum + (typeof col.width === 'number' ? col.width : 0); + }, 0); + return ( <> {modalContextHolder} - - + , t('pages.xray.basicRouting'), isMobile), + children: ( + + ), + }, + { + key: 'rules', + label: catTabLabel(, t('pages.xray.Routings'), isMobile), + children: ( + + - {isMobile ? ( - - ) : ( - r.key} - pagination={false} - scroll={{ x: 1150 }} - size="small" - className="routing-table" - onRow={(_record, index) => { - const classes: string[] = []; - const i = index ?? -1; - if (draggedIndex === i) classes.push('row-dragging'); - if (dropTargetIndex === i && draggedIndex !== i && draggedIndex != null) { - classes.push(i > draggedIndex ? 'drop-after' : 'drop-before'); - } - return { className: classes.join(' '), 'data-row-key': i } as React.HTMLAttributes; - }} - /> - )} - - setRuleModalOpen(false)} - onConfirm={onRuleConfirm} - /> - + {isMobile ? ( + + ) : ( +
r.key} + pagination={false} + scroll={{ x: tableScrollX }} + size="small" + className="routing-table" + onRow={(_record, index) => { + const classes: string[] = []; + const i = index ?? -1; + if (draggedIndex === i) classes.push('row-dragging'); + if (dropTargetIndex === i && draggedIndex !== i && draggedIndex != null) { + classes.push(i > draggedIndex ? 'drop-after' : 'drop-before'); + } + return { className: classes.join(' '), 'data-row-key': i } as React.HTMLAttributes; + }} + /> + )} + + ), + }, + ]} + /> + setRuleModalOpen(false)} + onConfirm={onRuleConfirm} + /> ); } diff --git a/frontend/src/pages/xray/routing/useRoutingColumns.tsx b/frontend/src/pages/xray/routing/useRoutingColumns.tsx index ec0b8e27..ea9ef984 100644 --- a/frontend/src/pages/xray/routing/useRoutingColumns.tsx +++ b/frontend/src/pages/xray/routing/useRoutingColumns.tsx @@ -19,6 +19,8 @@ import type { RuleRow } from './types'; interface RoutingColumnsParams { isMobile: boolean; rowsLength: number; + showSource: boolean; + showBalancer: boolean; onHandlePointerDown: (idx: number, ev: React.PointerEvent) => void; openEdit: (idx: number) => void; moveUp: (idx: number) => void; @@ -29,6 +31,8 @@ interface RoutingColumnsParams { export function useRoutingColumns({ isMobile, rowsLength, + showSource, + showBalancer, onHandlePointerDown, openEdit, moveUp, @@ -84,6 +88,7 @@ export function useRoutingColumns({ align: 'left', width: 180, key: 'source', + hidden: !showSource, render: (_v, record) => (
{record.sourceIP && } @@ -110,6 +115,7 @@ export function useRoutingColumns({ { title: t('pages.xray.rules.dest'), align: 'left', + width: 200, key: 'destination', render: (_v, record) => (
@@ -153,6 +159,7 @@ export function useRoutingColumns({ align: 'left', width: 150, key: 'balancer', + hidden: !showBalancer, render: (_v, record) => record.balancerTag ? (
@@ -165,6 +172,6 @@ export function useRoutingColumns({ }, ], // eslint-disable-next-line react-hooks/exhaustive-deps - [t, isMobile, rowsLength], + [t, isMobile, rowsLength, showSource, showBalancer], ); }