refactor: react router (#5073)

* refactor: react router

* chore: update

* fix: router

* refactor: generate router children by navItems

* chore: set start page when create window

* docs: update UPDATELOG.md
This commit is contained in:
oomeow
2025-10-18 20:25:31 +08:00
committed by GitHub
parent 8e20b1b0a0
commit 96ce529b16
16 changed files with 74 additions and 115 deletions

View File

@@ -35,6 +35,7 @@
- 配置重载失败时自动重启核心
- 启用 TUN 前等待服务就绪
- 卸载 TUN 时会先关闭
- 优化应用启动页
### 🐞 修复问题

View File

@@ -71,7 +71,7 @@
"react-i18next": "16.1.0",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.59.0",
"react-router-dom": "7.9.4",
"react-router": "^7.9.4",
"react-virtuoso": "^4.14.1",
"swr": "^2.3.6",
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo",

17
pnpm-lock.yaml generated
View File

@@ -119,8 +119,8 @@ importers:
react-monaco-editor:
specifier: 0.59.0
version: 0.59.0(monaco-editor@0.54.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react-router-dom:
specifier: 7.9.4
react-router:
specifier: ^7.9.4
version: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react-virtuoso:
specifier: ^4.14.1
@@ -3638,13 +3638,6 @@ packages:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
react-router-dom@7.9.4:
resolution: {integrity: sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
react-router@7.9.4:
resolution: {integrity: sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==}
engines: {node: '>=20.0.0'}
@@ -8241,12 +8234,6 @@ snapshots:
react-refresh@0.17.0: {}
react-router-dom@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
react-router: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react-router@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
cookie: 1.0.2

View File

@@ -338,7 +338,15 @@ impl IVerge {
pub async fn new() -> Self {
match dirs::verge_path() {
Ok(path) => match help::read_yaml::<IVerge>(&path).await {
Ok(config) => config,
Ok(mut config) => {
// compatibility
if let Some(start_page) = config.start_page.clone()
&& start_page == "/home"
{
config.start_page = Some(String::from("/"));
}
config
}
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
@@ -362,7 +370,7 @@ impl IVerge {
env_type: Some("bash".into()),
#[cfg(target_os = "windows")]
env_type: Some("powershell".into()),
start_page: Some("/home".into()),
start_page: Some("/".into()),
traffic_graph: Some(true),
enable_memory_usage: Some(true),
enable_group_icon: Some(true),

View File

@@ -1,6 +1,7 @@
use tauri::WebviewWindow;
use crate::{
config::Config,
core::handle,
logging_error,
utils::{
@@ -17,13 +18,19 @@ const MINIMAL_WIDTH: f64 = 520.0;
const MINIMAL_HEIGHT: f64 = 520.0;
/// 构建新的 WebView 窗口
pub fn build_new_window() -> Result<WebviewWindow, String> {
pub async fn build_new_window() -> Result<WebviewWindow, String> {
let app_handle = handle::Handle::app_handle();
let start_page = Config::verge()
.await
.latest_ref()
.start_page
.clone()
.unwrap_or("/".to_string());
match tauri::WebviewWindowBuilder::new(
app_handle,
"main", /* the unique window label */
tauri::WebviewUrl::App("index.html".into()),
tauri::WebviewUrl::App(start_page.into()),
)
.title("Clash Verge")
.center()

View File

@@ -328,7 +328,7 @@ impl WindowManager {
return false;
}
match build_new_window() {
match build_new_window().await {
Ok(_) => {
logging!(info, Type::Window, "新窗口创建成功");
}

View File

@@ -30,7 +30,7 @@ import { useLockFn } from "ahooks";
import React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
import { EnhancedCard } from "@/components/home/enhanced-card";

View File

@@ -22,7 +22,7 @@ import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { useAppData } from "@/providers/app-data-context";
import { openWebUrl, updateProfile } from "@/services/cmds";

View File

@@ -17,7 +17,7 @@ import {
import { useLockFn } from "ahooks";
import { useCallback, useEffect, useMemo, useReducer } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import useSWR from "swr";
import { useSystemState } from "@/hooks/use-system-state";

View File

@@ -5,7 +5,7 @@ import {
ListItemText,
ListItemIcon,
} from "@mui/material";
import { useMatch, useResolvedPath, useNavigate } from "react-router-dom";
import { useMatch, useResolvedPath, useNavigate } from "react-router";
import { useVerge } from "@/hooks/use-verge";
interface Props {

View File

@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
import { DialogRef } from "@/components/base";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { useVerge } from "@/hooks/use-verge";
import { routers } from "@/pages/_routers";
import { navItems } from "@/pages/_routers";
import { copyClashEnv } from "@/services/cmds";
import { supportedLanguages } from "@/services/i18n";
import { showNotice } from "@/services/noticeService";
@@ -170,7 +170,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
onGuard={(e) => patchVerge({ start_page: e })}
>
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
{routers.map((page: { label: string; path: string }) => {
{navItems.map((page: { label: string; path: string }) => {
return (
<MenuItem key={page.path} value={page.path}>
{t(page.label)}

View File

@@ -6,11 +6,11 @@ import { ResizeObserver } from "@juggle/resize-observer";
import { ComposeContextProvider } from "foxact/compose-context-provider";
import React from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { RouterProvider } from "react-router";
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
import { BaseErrorBoundary } from "./components/base";
import Layout from "./pages/_layout";
import { router } from "./pages/_routers";
import { AppDataProvider } from "./providers/app-data-provider";
import { WindowProvider } from "./providers/window";
import { initializeLanguage } from "./services/i18n";
@@ -64,9 +64,7 @@ const initializeApp = async () => {
<BaseErrorBoundary>
<WindowProvider>
<AppDataProvider>
<BrowserRouter>
<Layout />
</BrowserRouter>
<RouterProvider router={router} />
</AppDataProvider>
</WindowProvider>
</BaseErrorBoundary>

View File

@@ -4,14 +4,15 @@ import { listen } from "@tauri-apps/api/event";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import { useCallback, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate, useRoutes } from "react-router-dom";
import { Outlet, useNavigate } from "react-router";
import { SWRConfig, mutate } from "swr";
import iconDark from "@/assets/image/icon_dark.svg?react";
import iconLight from "@/assets/image/icon_light.svg?react";
import LogoSvg from "@/assets/image/logo.svg?react";
import { BaseErrorBoundary } from "@/components/base";
import { NoticeManager } from "@/components/base/NoticeManager";
import { WindowControls } from "@/components/controller/window-controller";
import { LayoutItem } from "@/components/layout/layout-item";
@@ -31,7 +32,7 @@ import { showNotice } from "@/services/noticeService";
import { useThemeMode } from "@/services/states";
import getSystem from "@/utils/get-system";
import { routers } from "./_routers";
import { navItems } from "./_routers";
import "dayjs/locale/ru";
import "dayjs/locale/zh-cn";
@@ -161,24 +162,12 @@ const Layout = () => {
const { t } = useTranslation();
const { theme } = useCustomTheme();
const { verge } = useVerge();
const { language, start_page } = verge ?? {};
const { language } = verge ?? {};
const { switchLanguage } = useI18n();
const navigate = useNavigate();
const location = useLocation();
const matchedElement = useRoutes(routers);
const routersEles = useMemo(() => {
if (!matchedElement) {
return null;
}
return (
<React.Fragment key={location.pathname}>{matchedElement}</React.Fragment>
);
}, [matchedElement, location.pathname]);
const { addListener } = useListen();
const initRef = useRef(false);
const overlayRemovedRef = useRef(false);
const lastStartPageRef = useRef<string | null>(null);
const startPageAppliedRef = useRef(false);
const themeReady = useMemo(() => Boolean(theme), [theme]);
const windowControls = useRef<any>(null);
@@ -538,35 +527,6 @@ const Layout = () => {
}
}, [language, switchLanguage]);
useEffect(() => {
if (!start_page) {
lastStartPageRef.current = null;
startPageAppliedRef.current = false;
return;
}
const normalizedStartPage = start_page.startsWith("/")
? start_page
: `/${start_page}`;
if (lastStartPageRef.current !== normalizedStartPage) {
lastStartPageRef.current = normalizedStartPage;
startPageAppliedRef.current = false;
}
if (startPageAppliedRef.current) {
return;
}
startPageAppliedRef.current = true;
if (location.pathname === normalizedStartPage) {
return;
}
navigate(normalizedStartPage, { replace: true });
}, [start_page, navigate, location.pathname]);
if (!themeReady) {
return (
<div
@@ -584,22 +544,6 @@ const Layout = () => {
);
}
if (!routersEles) {
return (
<div
style={{
width: "100vw",
height: "100vh",
background: mode === "light" ? "#fff" : "#181a1b",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: mode === "light" ? "#333" : "#fff",
}}
></div>
);
}
return (
<SWRConfig
value={{
@@ -692,7 +636,7 @@ const Layout = () => {
</div>
<List className="the-menu">
{routers.map((router) => (
{navItems.map((router) => (
<LayoutItem
key={router.label}
to={router.path}
@@ -710,7 +654,11 @@ const Layout = () => {
<div className="layout-content__right">
<div className="the-bar"></div>
<div className="the-content">{routersEles}</div>
<div className="the-content">
<BaseErrorBoundary>
<Outlet />
</BaseErrorBoundary>
</div>
</div>
</div>
</Paper>

View File

@@ -6,6 +6,7 @@ import LockOpenRoundedIcon from "@mui/icons-material/LockOpenRounded";
import SettingsRoundedIcon from "@mui/icons-material/SettingsRounded";
import SubjectRoundedIcon from "@mui/icons-material/SubjectRounded";
import WifiRoundedIcon from "@mui/icons-material/WifiRounded";
import { createBrowserRouter, RouteObject } from "react-router";
import ConnectionsSvg from "@/assets/image/itemicon/connections.svg?react";
import HomeSvg from "@/assets/image/itemicon/home.svg?react";
@@ -15,8 +16,8 @@ import ProxiesSvg from "@/assets/image/itemicon/proxies.svg?react";
import RulesSvg from "@/assets/image/itemicon/rules.svg?react";
import SettingsSvg from "@/assets/image/itemicon/settings.svg?react";
import UnlockSvg from "@/assets/image/itemicon/unlock.svg?react";
import { BaseErrorBoundary } from "@/components/base";
import Layout from "./_layout";
import ConnectionsPage from "./connections";
import HomePage from "./home";
import LogsPage from "./logs";
@@ -26,58 +27,67 @@ import RulesPage from "./rules";
import SettingsPage from "./settings";
import UnlockPage from "./unlock";
export const routers = [
export const navItems = [
{
label: "Label-Home",
path: "/home",
path: "/",
icon: [<HomeRoundedIcon key="mui" />, <HomeSvg key="svg" />],
element: <HomePage />,
Component: HomePage,
},
{
label: "Label-Proxies",
path: "/",
path: "/proxies",
icon: [<WifiRoundedIcon key="mui" />, <ProxiesSvg key="svg" />],
element: <ProxiesPage />,
Component: ProxiesPage,
},
{
label: "Label-Profiles",
path: "/profile",
icon: [<DnsRoundedIcon key="mui" />, <ProfilesSvg key="svg" />],
element: <ProfilesPage />,
Component: ProfilesPage,
},
{
label: "Label-Connections",
path: "/connections",
icon: [<LanguageRoundedIcon key="mui" />, <ConnectionsSvg key="svg" />],
element: <ConnectionsPage />,
Component: ConnectionsPage,
},
{
label: "Label-Rules",
path: "/rules",
icon: [<ForkRightRoundedIcon key="mui" />, <RulesSvg key="svg" />],
element: <RulesPage />,
Component: RulesPage,
},
{
label: "Label-Logs",
path: "/logs",
icon: [<SubjectRoundedIcon key="mui" />, <LogsSvg key="svg" />],
element: <LogsPage />,
Component: LogsPage,
},
{
label: "Label-Unlock",
path: "/unlock",
icon: [<LockOpenRoundedIcon key="mui" />, <UnlockSvg key="svg" />],
element: <UnlockPage />,
Component: UnlockPage,
},
{
label: "Label-Settings",
path: "/settings",
icon: [<SettingsRoundedIcon key="mui" />, <SettingsSvg key="svg" />],
element: <SettingsPage />,
Component: SettingsPage,
},
].map((router) => ({
...router,
element: (
<BaseErrorBoundary key={router.label}>{router.element}</BaseErrorBoundary>
),
}));
];
export const router = createBrowserRouter([
{
path: "/",
Component: Layout,
children: navItems.map(
(item) =>
({
path: item.path,
Component: item.Component,
}) as RouteObject,
),
},
]);

View File

@@ -32,7 +32,7 @@ import { useLockFn } from "ahooks";
import { throttle } from "lodash-es";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { useLocation } from "react-router";
import useSWR, { mutate } from "swr";
import { closeAllConnections } from "tauri-plugin-mihomo-api";

View File

@@ -80,7 +80,7 @@ export default defineConfig({
if (
id.includes("react") ||
id.includes("react-dom") ||
id.includes("react-router-dom")
id.includes("react-router")
) {
return "react-core";
}