Browse Source

feat(sidebar): move Routing/Outbounds to top-level items with clean URLs

- Move Routing out of the Xray Configs submenu; add Routing and Outbounds
  as top-level sidebar items below Hosts
- Give them their own clean routes (/routing, /outbound) instead of
  /xray#routing and /xray#outbound, registered in the React router and the
  Go SPA shell so direct links and refresh work
- XrayPage derives the active section from the pathname for those routes
- Add menu.routing and menu.outbounds translation keys across all locales
MHSanaei 17 hours ago
parent
commit
718b7e16e1

+ 2 - 0
frontend/src/hooks/usePageTitle.ts

@@ -10,6 +10,8 @@ const TITLE_KEYS: Record<string, string> = {
   '/nodes': 'menu.nodes',
   '/settings': 'menu.settings',
   '/xray': 'menu.xray',
+  '/outbound': 'menu.outbounds',
+  '/routing': 'menu.routing',
   '/api-docs': 'menu.apiDocs',
 };
 

+ 5 - 6
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<IconName, ComponentType> = {
   dashboard: DashboardOutlined,
@@ -56,6 +56,7 @@ const iconByName: Record<IconName, ComponentType> = {
   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<NonNullable<MenuProps['items']>>(() => [
     { key: '/xray#basic', icon: <SettingOutlined />, label: t('pages.xray.basicTemplate') },
-    { key: '/xray#routing', icon: <SwapOutlined />, label: t('pages.xray.Routings') },
     { key: '/xray#balancer', icon: <ClusterOutlined />, label: t('pages.xray.Balancers') },
     { key: '/xray#dns', icon: <DatabaseOutlined />, label: 'DNS' },
     { key: '/xray#advanced', icon: <CodeOutlined />, 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<string[]>(() => (openSubmenu ? [openSubmenu] : []));
   useEffect(() => {
     if (openSubmenu) {

+ 2 - 1
frontend/src/pages/xray/XrayPage.tsx

@@ -78,7 +78,8 @@ export default function XrayPage() {
   const [advSettings, setAdvSettings] = useState<AdvKey>('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(

+ 2 - 0
frontend/src/routes.tsx

@@ -30,6 +30,8 @@ const routes: RouteObject[] = [
       { path: 'hosts', element: withSuspense(<HostsPage />) },
       { path: 'settings', element: withSuspense(<SettingsPage />) },
       { path: 'xray', element: withSuspense(<XrayPage />) },
+      { path: 'outbound', element: withSuspense(<XrayPage />) },
+      { path: 'routing', element: withSuspense(<XrayPage />) },
       { path: 'api-docs', element: withSuspense(<ApiDocsPage />) },
     ],
   },

+ 2 - 0
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 <meta name="csrf-token">,

+ 2 - 0
internal/web/translation/ar-EG.json

@@ -111,6 +111,8 @@
     "nodes": "النودز",
     "settings": "إعدادات اللوحة",
     "xray": "إعدادات Xray",
+    "routing": "التوجيه",
+    "outbounds": "الصادرات",
     "apiDocs": "توثيق API",
     "logout": "تسجيل خروج",
     "link": "إدارة",

+ 2 - 0
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",

+ 2 - 0
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",

+ 2 - 0
internal/web/translation/fa-IR.json

@@ -111,6 +111,8 @@
     "nodes": "نودها",
     "settings": "تنظیمات پنل",
     "xray": "پیکربندی Xray",
+    "routing": "مسیریابی",
+    "outbounds": "خروجی‌ها",
     "apiDocs": "مستندات API",
     "logout": "خروج",
     "link": "مدیریت",

+ 2 - 0
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",

+ 2 - 0
internal/web/translation/ja-JP.json

@@ -111,6 +111,8 @@
     "nodes": "ノード",
     "settings": "パネル設定",
     "xray": "Xray 設定",
+    "routing": "ルーティング",
+    "outbounds": "アウトバウンド",
     "apiDocs": "API ドキュメント",
     "logout": "ログアウト",
     "link": "リンク管理",

+ 2 - 0
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",

+ 2 - 0
internal/web/translation/ru-RU.json

@@ -111,6 +111,8 @@
     "nodes": "Узлы",
     "settings": "Настройки панели",
     "xray": "Конфигурации Xray",
+    "routing": "Маршрутизация",
+    "outbounds": "Исходящие",
     "apiDocs": "Документация API",
     "logout": "Выход",
     "link": "Управление",

+ 2 - 0
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",

+ 2 - 0
internal/web/translation/uk-UA.json

@@ -111,6 +111,8 @@
     "nodes": "Вузли",
     "settings": "Налаштування панелі",
     "xray": "Конфігурації Xray",
+    "routing": "Маршрутизація",
+    "outbounds": "Вихідні",
     "apiDocs": "Документація API",
     "logout": "Вийти",
     "link": "Керувати",

+ 2 - 0
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ý",

+ 2 - 0
internal/web/translation/zh-CN.json

@@ -111,6 +111,8 @@
     "nodes": "节点",
     "settings": "面板设置",
     "xray": "Xray 配置",
+    "routing": "路由",
+    "outbounds": "出站",
     "apiDocs": "API 文档",
     "logout": "退出登录",
     "link": "管理",

+ 2 - 0
internal/web/translation/zh-TW.json

@@ -111,6 +111,8 @@
     "nodes": "節點",
     "settings": "面板設定",
     "xray": "Xray 設定",
+    "routing": "路由",
+    "outbounds": "出站",
     "apiDocs": "API 文件",
     "logout": "退出登入",
     "link": "管理",