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
This commit is contained in:
MHSanaei
2026-06-22 22:20:26 +02:00
parent 20094c8d35
commit 718b7e16e1
18 changed files with 39 additions and 7 deletions
+2
View File
@@ -10,6 +10,8 @@ const TITLE_KEYS: Record<string, string> = {
'/nodes': 'menu.nodes', '/nodes': 'menu.nodes',
'/settings': 'menu.settings', '/settings': 'menu.settings',
'/xray': 'menu.xray', '/xray': 'menu.xray',
'/outbound': 'menu.outbounds',
'/routing': 'menu.routing',
'/api-docs': 'menu.apiDocs', '/api-docs': 'menu.apiDocs',
}; };
+5 -6
View File
@@ -42,7 +42,7 @@ const DONATE_URL = 'https://donate.sanaei.dev/';
const REPO_URL = 'https://github.com/MHSanaei/3x-ui'; const REPO_URL = 'https://github.com/MHSanaei/3x-ui';
const LOGOUT_KEY = '__logout__'; 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> = { const iconByName: Record<IconName, ComponentType> = {
dashboard: DashboardOutlined, dashboard: DashboardOutlined,
@@ -56,6 +56,7 @@ const iconByName: Record<IconName, ComponentType> = {
logout: LogoutOutlined, logout: LogoutOutlined,
apidocs: ApiOutlined, apidocs: ApiOutlined,
outbound: ExportOutlined, outbound: ExportOutlined,
routing: SwapOutlined,
}; };
function readCollapsed(): boolean { function readCollapsed(): boolean {
@@ -142,7 +143,8 @@ export default function AppSidebar() {
{ key: '/groups', icon: 'groups', title: t('menu.groups') }, { key: '/groups', icon: 'groups', title: t('menu.groups') },
{ key: '/nodes', icon: 'cluster', title: t('menu.nodes') }, { key: '/nodes', icon: 'cluster', title: t('menu.nodes') },
{ key: '/hosts', icon: 'hosts', title: t('menu.hosts') }, { 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: '/settings', icon: 'setting', title: t('menu.settings') },
{ key: '/xray', icon: 'tool', title: t('menu.xray') }, { key: '/xray', icon: 'tool', title: t('menu.xray') },
{ key: '/api-docs', icon: 'apidocs', title: t('menu.apiDocs') }, { key: '/api-docs', icon: 'apidocs', title: t('menu.apiDocs') },
@@ -168,7 +170,6 @@ export default function AppSidebar() {
const xrayChildren = useMemo<NonNullable<MenuProps['items']>>(() => [ const xrayChildren = useMemo<NonNullable<MenuProps['items']>>(() => [
{ key: '/xray#basic', icon: <SettingOutlined />, label: t('pages.xray.basicTemplate') }, { 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#balancer', icon: <ClusterOutlined />, label: t('pages.xray.Balancers') },
{ key: '/xray#dns', icon: <DatabaseOutlined />, label: 'DNS' }, { key: '/xray#dns', icon: <DatabaseOutlined />, label: 'DNS' },
{ key: '/xray#advanced', icon: <CodeOutlined />, label: t('pages.xray.advancedTemplate') }, { key: '/xray#advanced', icon: <CodeOutlined />, label: t('pages.xray.advancedTemplate') },
@@ -182,9 +183,7 @@ export default function AppSidebar() {
? `/xray${hash || '#basic'}` ? `/xray${hash || '#basic'}`
: (pathname === '' ? '/' : pathname); : (pathname === '' ? '/' : pathname);
// The Outbounds top-level item lives on /xray#outbound, so don't auto-open the const openSubmenu = settingsActive ? '/settings' : xrayActive ? '/xray' : null;
// Xray Configs submenu for it.
const openSubmenu = settingsActive ? '/settings' : xrayActive && hash !== '#outbound' ? '/xray' : null;
const [openKeys, setOpenKeys] = useState<string[]>(() => (openSubmenu ? [openSubmenu] : [])); const [openKeys, setOpenKeys] = useState<string[]>(() => (openSubmenu ? [openSubmenu] : []));
useEffect(() => { useEffect(() => {
if (openSubmenu) { if (openSubmenu) {
+2 -1
View File
@@ -78,7 +78,8 @@ export default function XrayPage() {
const [advSettings, setAdvSettings] = useState<AdvKey>('xraySetting'); const [advSettings, setAdvSettings] = useState<AdvKey>('xraySetting');
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); 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 activeSection = SECTION_SLUGS.includes(sectionSlug) ? sectionSlug : 'basic';
const mutate = useCallback( const mutate = useCallback(
+2
View File
@@ -30,6 +30,8 @@ const routes: RouteObject[] = [
{ path: 'hosts', element: withSuspense(<HostsPage />) }, { path: 'hosts', element: withSuspense(<HostsPage />) },
{ path: 'settings', element: withSuspense(<SettingsPage />) }, { path: 'settings', element: withSuspense(<SettingsPage />) },
{ path: 'xray', element: withSuspense(<XrayPage />) }, { path: 'xray', element: withSuspense(<XrayPage />) },
{ path: 'outbound', element: withSuspense(<XrayPage />) },
{ path: 'routing', element: withSuspense(<XrayPage />) },
{ path: 'api-docs', element: withSuspense(<ApiDocsPage />) }, { path: 'api-docs', element: withSuspense(<ApiDocsPage />) },
], ],
}, },
+2
View File
@@ -40,6 +40,8 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
g.GET("/nodes", a.panelSPA) g.GET("/nodes", a.panelSPA)
g.GET("/settings", a.panelSPA) g.GET("/settings", a.panelSPA)
g.GET("/xray", a.panelSPA) g.GET("/xray", a.panelSPA)
g.GET("/outbound", a.panelSPA)
g.GET("/routing", a.panelSPA)
g.GET("/api-docs", a.panelSPA) g.GET("/api-docs", a.panelSPA)
// SPA pages built by Vite don't have a server-rendered <meta name="csrf-token">, // SPA pages built by Vite don't have a server-rendered <meta name="csrf-token">,
+2
View File
@@ -111,6 +111,8 @@
"nodes": "النودز", "nodes": "النودز",
"settings": "إعدادات اللوحة", "settings": "إعدادات اللوحة",
"xray": "إعدادات Xray", "xray": "إعدادات Xray",
"routing": "التوجيه",
"outbounds": "الصادرات",
"apiDocs": "توثيق API", "apiDocs": "توثيق API",
"logout": "تسجيل خروج", "logout": "تسجيل خروج",
"link": "إدارة", "link": "إدارة",
+2
View File
@@ -112,6 +112,8 @@
"hosts": "Hosts", "hosts": "Hosts",
"settings": "Panel Settings", "settings": "Panel Settings",
"xray": "Xray Configs", "xray": "Xray Configs",
"routing": "Routing",
"outbounds": "Outbounds",
"apiDocs": "API Docs", "apiDocs": "API Docs",
"logout": "Log Out", "logout": "Log Out",
"link": "Manage", "link": "Manage",
+2
View File
@@ -111,6 +111,8 @@
"nodes": "Nodos", "nodes": "Nodos",
"settings": "Ajustes del panel", "settings": "Ajustes del panel",
"xray": "Configuración Xray", "xray": "Configuración Xray",
"routing": "Enrutamiento",
"outbounds": "Salidas",
"apiDocs": "Documentación de la API", "apiDocs": "Documentación de la API",
"logout": "Cerrar Sesión", "logout": "Cerrar Sesión",
"link": "Gestionar", "link": "Gestionar",
+2
View File
@@ -111,6 +111,8 @@
"nodes": "نودها", "nodes": "نودها",
"settings": "تنظیمات پنل", "settings": "تنظیمات پنل",
"xray": "پیکربندی Xray", "xray": "پیکربندی Xray",
"routing": "مسیریابی",
"outbounds": "خروجی‌ها",
"apiDocs": "مستندات API", "apiDocs": "مستندات API",
"logout": "خروج", "logout": "خروج",
"link": "مدیریت", "link": "مدیریت",
+2
View File
@@ -111,6 +111,8 @@
"nodes": "Node", "nodes": "Node",
"settings": "Pengaturan Panel", "settings": "Pengaturan Panel",
"xray": "Konfigurasi Xray", "xray": "Konfigurasi Xray",
"routing": "Pengalihan",
"outbounds": "Outbound",
"apiDocs": "Dokumentasi API", "apiDocs": "Dokumentasi API",
"logout": "Keluar", "logout": "Keluar",
"link": "Kelola", "link": "Kelola",
+2
View File
@@ -111,6 +111,8 @@
"nodes": "ノード", "nodes": "ノード",
"settings": "パネル設定", "settings": "パネル設定",
"xray": "Xray 設定", "xray": "Xray 設定",
"routing": "ルーティング",
"outbounds": "アウトバウンド",
"apiDocs": "API ドキュメント", "apiDocs": "API ドキュメント",
"logout": "ログアウト", "logout": "ログアウト",
"link": "リンク管理", "link": "リンク管理",
+2
View File
@@ -111,6 +111,8 @@
"nodes": "Nós", "nodes": "Nós",
"settings": "Configurações do Painel", "settings": "Configurações do Painel",
"xray": "Configurações Xray", "xray": "Configurações Xray",
"routing": "Roteamento",
"outbounds": "Saídas",
"apiDocs": "Documentação da API", "apiDocs": "Documentação da API",
"logout": "Sair", "logout": "Sair",
"link": "Gerenciar", "link": "Gerenciar",
+2
View File
@@ -111,6 +111,8 @@
"nodes": "Узлы", "nodes": "Узлы",
"settings": "Настройки панели", "settings": "Настройки панели",
"xray": "Конфигурации Xray", "xray": "Конфигурации Xray",
"routing": "Маршрутизация",
"outbounds": "Исходящие",
"apiDocs": "Документация API", "apiDocs": "Документация API",
"logout": "Выход", "logout": "Выход",
"link": "Управление", "link": "Управление",
+2
View File
@@ -111,6 +111,8 @@
"nodes": "Düğümler", "nodes": "Düğümler",
"settings": "Panel Ayarları", "settings": "Panel Ayarları",
"xray": "Xray Yapılandırmaları", "xray": "Xray Yapılandırmaları",
"routing": "Yönlendirme",
"outbounds": "Giden Bağlantılar",
"apiDocs": "API Belgeleri", "apiDocs": "API Belgeleri",
"logout": "Çıkış Yap", "logout": "Çıkış Yap",
"link": "Yönet", "link": "Yönet",
+2
View File
@@ -111,6 +111,8 @@
"nodes": "Вузли", "nodes": "Вузли",
"settings": "Налаштування панелі", "settings": "Налаштування панелі",
"xray": "Конфігурації Xray", "xray": "Конфігурації Xray",
"routing": "Маршрутизація",
"outbounds": "Вихідні",
"apiDocs": "Документація API", "apiDocs": "Документація API",
"logout": "Вийти", "logout": "Вийти",
"link": "Керувати", "link": "Керувати",
+2
View File
@@ -111,6 +111,8 @@
"nodes": "Nút", "nodes": "Nút",
"settings": "Cài đặt bảng điều khiển", "settings": "Cài đặt bảng điều khiển",
"xray": "Cấu hình Xray", "xray": "Cấu hình Xray",
"routing": "Định tuyến",
"outbounds": "Outbound",
"apiDocs": "Tài liệu API", "apiDocs": "Tài liệu API",
"logout": "Đăng xuất", "logout": "Đăng xuất",
"link": "Quản lý", "link": "Quản lý",
+2
View File
@@ -111,6 +111,8 @@
"nodes": "节点", "nodes": "节点",
"settings": "面板设置", "settings": "面板设置",
"xray": "Xray 配置", "xray": "Xray 配置",
"routing": "路由",
"outbounds": "出站",
"apiDocs": "API 文档", "apiDocs": "API 文档",
"logout": "退出登录", "logout": "退出登录",
"link": "管理", "link": "管理",
+2
View File
@@ -111,6 +111,8 @@
"nodes": "節點", "nodes": "節點",
"settings": "面板設定", "settings": "面板設定",
"xray": "Xray 設定", "xray": "Xray 設定",
"routing": "路由",
"outbounds": "出站",
"apiDocs": "API 文件", "apiDocs": "API 文件",
"logout": "退出登入", "logout": "退出登入",
"link": "管理", "link": "管理",