diff --git a/frontend/src/hooks/usePageTitle.ts b/frontend/src/hooks/usePageTitle.ts index af06952bd..4a0c2d5d2 100644 --- a/frontend/src/hooks/usePageTitle.ts +++ b/frontend/src/hooks/usePageTitle.ts @@ -10,6 +10,8 @@ const TITLE_KEYS: Record = { '/nodes': 'menu.nodes', '/settings': 'menu.settings', '/xray': 'menu.xray', + '/outbound': 'menu.outbounds', + '/routing': 'menu.routing', '/api-docs': 'menu.apiDocs', }; diff --git a/frontend/src/layouts/AppSidebar.tsx b/frontend/src/layouts/AppSidebar.tsx index 9b92ba7f4..df0f07c6c 100644 --- a/frontend/src/layouts/AppSidebar.tsx +++ b/frontend/src/layouts/AppSidebar.tsx @@ -42,7 +42,7 @@ const DONATE_URL = 'https://donate.sanaei.dev/'; const REPO_URL = 'https://github.com/MHSanaei/3x-ui'; const LOGOUT_KEY = '__logout__'; -type IconName = 'dashboard' | 'inbound' | 'team' | 'groups' | 'setting' | 'tool' | 'cluster' | 'hosts' | 'logout' | 'apidocs' | 'outbound'; +type IconName = 'dashboard' | 'inbound' | 'team' | 'groups' | 'setting' | 'tool' | 'cluster' | 'hosts' | 'logout' | 'apidocs' | 'outbound' | 'routing'; const iconByName: Record = { dashboard: DashboardOutlined, @@ -56,6 +56,7 @@ const iconByName: Record = { logout: LogoutOutlined, apidocs: ApiOutlined, outbound: ExportOutlined, + routing: SwapOutlined, }; function readCollapsed(): boolean { @@ -142,7 +143,8 @@ export default function AppSidebar() { { key: '/groups', icon: 'groups', title: t('menu.groups') }, { key: '/nodes', icon: 'cluster', title: t('menu.nodes') }, { key: '/hosts', icon: 'hosts', title: t('menu.hosts') }, - { key: '/xray#outbound', icon: 'outbound', title: t('pages.xray.Outbounds') }, + { key: '/outbound', icon: 'outbound', title: t('menu.outbounds') }, + { key: '/routing', icon: 'routing', title: t('menu.routing') }, { key: '/settings', icon: 'setting', title: t('menu.settings') }, { key: '/xray', icon: 'tool', title: t('menu.xray') }, { key: '/api-docs', icon: 'apidocs', title: t('menu.apiDocs') }, @@ -168,7 +170,6 @@ export default function AppSidebar() { const xrayChildren = useMemo>(() => [ { key: '/xray#basic', icon: , label: t('pages.xray.basicTemplate') }, - { key: '/xray#routing', icon: , label: t('pages.xray.Routings') }, { key: '/xray#balancer', icon: , label: t('pages.xray.Balancers') }, { key: '/xray#dns', icon: , label: 'DNS' }, { key: '/xray#advanced', icon: , label: t('pages.xray.advancedTemplate') }, @@ -182,9 +183,7 @@ export default function AppSidebar() { ? `/xray${hash || '#basic'}` : (pathname === '' ? '/' : pathname); - // The Outbounds top-level item lives on /xray#outbound, so don't auto-open the - // Xray Configs submenu for it. - const openSubmenu = settingsActive ? '/settings' : xrayActive && hash !== '#outbound' ? '/xray' : null; + const openSubmenu = settingsActive ? '/settings' : xrayActive ? '/xray' : null; const [openKeys, setOpenKeys] = useState(() => (openSubmenu ? [openSubmenu] : [])); useEffect(() => { if (openSubmenu) { diff --git a/frontend/src/pages/xray/XrayPage.tsx b/frontend/src/pages/xray/XrayPage.tsx index a0a1e42f6..5ccfa9205 100644 --- a/frontend/src/pages/xray/XrayPage.tsx +++ b/frontend/src/pages/xray/XrayPage.tsx @@ -78,7 +78,8 @@ export default function XrayPage() { const [advSettings, setAdvSettings] = useState('xraySetting'); const location = useLocation(); const navigate = useNavigate(); - const sectionSlug = location.hash.replace(/^#/, ''); + const pathSection = location.pathname === '/outbound' ? 'outbound' : location.pathname === '/routing' ? 'routing' : ''; + const sectionSlug = pathSection || location.hash.replace(/^#/, ''); const activeSection = SECTION_SLUGS.includes(sectionSlug) ? sectionSlug : 'basic'; const mutate = useCallback( diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index f7a239004..2b670b173 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -30,6 +30,8 @@ const routes: RouteObject[] = [ { path: 'hosts', element: withSuspense() }, { path: 'settings', element: withSuspense() }, { path: 'xray', element: withSuspense() }, + { path: 'outbound', element: withSuspense() }, + { path: 'routing', element: withSuspense() }, { path: 'api-docs', element: withSuspense() }, ], }, diff --git a/internal/web/controller/spa.go b/internal/web/controller/spa.go index 192f13b1b..b999bc8f7 100644 --- a/internal/web/controller/spa.go +++ b/internal/web/controller/spa.go @@ -40,6 +40,8 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) { g.GET("/nodes", a.panelSPA) g.GET("/settings", a.panelSPA) g.GET("/xray", a.panelSPA) + g.GET("/outbound", a.panelSPA) + g.GET("/routing", a.panelSPA) g.GET("/api-docs", a.panelSPA) // SPA pages built by Vite don't have a server-rendered , diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index 22720b5c7..90c77371d 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -111,6 +111,8 @@ "nodes": "النودز", "settings": "إعدادات اللوحة", "xray": "إعدادات Xray", + "routing": "التوجيه", + "outbounds": "الصادرات", "apiDocs": "توثيق API", "logout": "تسجيل خروج", "link": "إدارة", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index 66828a94c..fbd30a641 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -112,6 +112,8 @@ "hosts": "Hosts", "settings": "Panel Settings", "xray": "Xray Configs", + "routing": "Routing", + "outbounds": "Outbounds", "apiDocs": "API Docs", "logout": "Log Out", "link": "Manage", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index ecd28d350..2164769ca 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -111,6 +111,8 @@ "nodes": "Nodos", "settings": "Ajustes del panel", "xray": "Configuración Xray", + "routing": "Enrutamiento", + "outbounds": "Salidas", "apiDocs": "Documentación de la API", "logout": "Cerrar Sesión", "link": "Gestionar", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index 32e7bd86a..3243844dc 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -111,6 +111,8 @@ "nodes": "نودها", "settings": "تنظیمات پنل", "xray": "پیکربندی Xray", + "routing": "مسیریابی", + "outbounds": "خروجی‌ها", "apiDocs": "مستندات API", "logout": "خروج", "link": "مدیریت", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index 574767dd8..b85587b37 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -111,6 +111,8 @@ "nodes": "Node", "settings": "Pengaturan Panel", "xray": "Konfigurasi Xray", + "routing": "Pengalihan", + "outbounds": "Outbound", "apiDocs": "Dokumentasi API", "logout": "Keluar", "link": "Kelola", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index 759637844..fe88a20a9 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -111,6 +111,8 @@ "nodes": "ノード", "settings": "パネル設定", "xray": "Xray 設定", + "routing": "ルーティング", + "outbounds": "アウトバウンド", "apiDocs": "API ドキュメント", "logout": "ログアウト", "link": "リンク管理", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index 95faf631a..3154f57eb 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -111,6 +111,8 @@ "nodes": "Nós", "settings": "Configurações do Painel", "xray": "Configurações Xray", + "routing": "Roteamento", + "outbounds": "Saídas", "apiDocs": "Documentação da API", "logout": "Sair", "link": "Gerenciar", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index 6363fe8b2..ce0cbc9be 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -111,6 +111,8 @@ "nodes": "Узлы", "settings": "Настройки панели", "xray": "Конфигурации Xray", + "routing": "Маршрутизация", + "outbounds": "Исходящие", "apiDocs": "Документация API", "logout": "Выход", "link": "Управление", diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index 2cbb4fe31..3a6413432 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -111,6 +111,8 @@ "nodes": "Düğümler", "settings": "Panel Ayarları", "xray": "Xray Yapılandırmaları", + "routing": "Yönlendirme", + "outbounds": "Giden Bağlantılar", "apiDocs": "API Belgeleri", "logout": "Çıkış Yap", "link": "Yönet", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index 6b8c47b9a..c5e8999e1 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -111,6 +111,8 @@ "nodes": "Вузли", "settings": "Налаштування панелі", "xray": "Конфігурації Xray", + "routing": "Маршрутизація", + "outbounds": "Вихідні", "apiDocs": "Документація API", "logout": "Вийти", "link": "Керувати", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index 11e1c14fb..b070b586a 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -111,6 +111,8 @@ "nodes": "Nút", "settings": "Cài đặt bảng điều khiển", "xray": "Cấu hình Xray", + "routing": "Định tuyến", + "outbounds": "Outbound", "apiDocs": "Tài liệu API", "logout": "Đăng xuất", "link": "Quản lý", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index 3d12de3de..22119c098 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -111,6 +111,8 @@ "nodes": "节点", "settings": "面板设置", "xray": "Xray 配置", + "routing": "路由", + "outbounds": "出站", "apiDocs": "API 文档", "logout": "退出登录", "link": "管理", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index ca12cce76..e38aae9d5 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -111,6 +111,8 @@ "nodes": "節點", "settings": "面板設定", "xray": "Xray 設定", + "routing": "路由", + "outbounds": "出站", "apiDocs": "API 文件", "logout": "退出登入", "link": "管理",