mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-14 00:26:06 +00:00
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:
@@ -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',
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 />) },
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user