mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 02:52:07 +08:00
perf(frontend): eliminate navigation loading states
This commit is contained in:
@@ -29,10 +29,9 @@ interface Props {
|
|||||||
children: string
|
children: string
|
||||||
icon: ReactNode[]
|
icon: ReactNode[]
|
||||||
sortable?: SortableProps
|
sortable?: SortableProps
|
||||||
onPreload?: () => Promise<unknown>
|
|
||||||
}
|
}
|
||||||
export const LayoutItem = (props: Props) => {
|
export const LayoutItem = (props: Props) => {
|
||||||
const { to, children, icon, sortable, onPreload } = props
|
const { to, children, icon, sortable } = props
|
||||||
const { verge } = useVerge()
|
const { verge } = useVerge()
|
||||||
const { menu_icon } = verge ?? {}
|
const { menu_icon } = verge ?? {}
|
||||||
const navCollapsed = verge?.collapse_navbar ?? false
|
const navCollapsed = verge?.collapse_navbar ?? false
|
||||||
@@ -51,16 +50,11 @@ export const LayoutItem = (props: Props) => {
|
|||||||
? (listeners ?? {})
|
? (listeners ?? {})
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
const handlePreload = useCallback(() => {
|
|
||||||
void onPreload?.().catch(() => {})
|
|
||||||
}, [onPreload])
|
|
||||||
|
|
||||||
const handlePointerDown = useCallback(
|
const handlePointerDown = useCallback(
|
||||||
(event: PointerEvent<HTMLDivElement>) => {
|
(event: PointerEvent<HTMLDivElement>) => {
|
||||||
handlePreload()
|
|
||||||
onPointerDown?.(event)
|
onPointerDown?.(event)
|
||||||
},
|
},
|
||||||
[handlePreload, onPointerDown],
|
[onPointerDown],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -105,8 +99,6 @@ export const LayoutItem = (props: Props) => {
|
|||||||
]}
|
]}
|
||||||
title={navCollapsed ? children : undefined}
|
title={navCollapsed ? children : undefined}
|
||||||
aria-label={navCollapsed ? children : undefined}
|
aria-label={navCollapsed ? children : undefined}
|
||||||
onFocus={handlePreload}
|
|
||||||
onMouseEnter={handlePreload}
|
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
onClick={() => navigate(to)}
|
onClick={() => navigate(to)}
|
||||||
>
|
>
|
||||||
|
|||||||
+5
-1
@@ -10,6 +10,7 @@ import { MihomoWebSocket } from 'tauri-plugin-mihomo-api'
|
|||||||
|
|
||||||
import { BaseErrorBoundary } from './components/base'
|
import { BaseErrorBoundary } from './components/base'
|
||||||
import { router } from './pages/_routers'
|
import { router } from './pages/_routers'
|
||||||
|
import { preloadHomePageCards } from './pages/home'
|
||||||
import { AppDataProvider } from './providers/app-data-provider'
|
import { AppDataProvider } from './providers/app-data-provider'
|
||||||
import { WindowProvider } from './providers/window'
|
import { WindowProvider } from './providers/window'
|
||||||
import { FALLBACK_LANGUAGE, initializeLanguage } from './services/i18n'
|
import { FALLBACK_LANGUAGE, initializeLanguage } from './services/i18n'
|
||||||
@@ -65,7 +66,10 @@ const initializeApp = (initialThemeMode: 'light' | 'dark') => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bootstrap = async () => {
|
const bootstrap = async () => {
|
||||||
const { initialThemeMode } = await preloadAppData()
|
const appDataPromise = preloadAppData()
|
||||||
|
void preloadHomePageCards()
|
||||||
|
|
||||||
|
const { initialThemeMode } = await appDataPromise
|
||||||
initializeApp(initialThemeMode)
|
initializeApp(initialThemeMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-70
@@ -24,22 +24,14 @@ import {
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import type { CSSProperties } from 'react'
|
import type { CSSProperties } from 'react'
|
||||||
import {
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
lazy,
|
|
||||||
Suspense,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Outlet, useLocation, useNavigate } from 'react-router'
|
import { Outlet, useNavigate } from 'react-router'
|
||||||
|
|
||||||
import iconDark from '@/assets/image/icon_dark.svg?react'
|
import iconDark from '@/assets/image/icon_dark.svg?react'
|
||||||
import iconLight from '@/assets/image/icon_light.svg?react'
|
import iconLight from '@/assets/image/icon_light.svg?react'
|
||||||
import LogoSvg from '@/assets/image/logo.svg?react'
|
import LogoSvg from '@/assets/image/logo.svg?react'
|
||||||
import { BaseErrorBoundary, BaseLoading } from '@/components/base'
|
import { BaseErrorBoundary } from '@/components/base'
|
||||||
import { LayoutItem } from '@/components/layout/layout-item'
|
import { LayoutItem } from '@/components/layout/layout-item'
|
||||||
import { LayoutTraffic } from '@/components/layout/layout-traffic'
|
import { LayoutTraffic } from '@/components/layout/layout-traffic'
|
||||||
import { NoticeManager } from '@/components/layout/notice-manager'
|
import { NoticeManager } from '@/components/layout/notice-manager'
|
||||||
@@ -50,7 +42,6 @@ import {
|
|||||||
} from '@/components/layout/window-controller'
|
} from '@/components/layout/window-controller'
|
||||||
import { useI18n } from '@/hooks/use-i18n'
|
import { useI18n } from '@/hooks/use-i18n'
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { useVisibility } from '@/hooks/use-visibility'
|
|
||||||
import { useWindowDecorations } from '@/hooks/use-window'
|
import { useWindowDecorations } from '@/hooks/use-window'
|
||||||
import { useThemeMode } from '@/services/states'
|
import { useThemeMode } from '@/services/states'
|
||||||
import getSystem from '@/utils/get-system'
|
import getSystem from '@/utils/get-system'
|
||||||
@@ -62,17 +53,11 @@ import {
|
|||||||
useNavMenuOrder,
|
useNavMenuOrder,
|
||||||
} from './_layout/hooks'
|
} from './_layout/hooks'
|
||||||
import { handleNoticeMessage } from './_layout/utils'
|
import { handleNoticeMessage } from './_layout/utils'
|
||||||
import {
|
import { navItems } from './_navigation'
|
||||||
navItems,
|
|
||||||
preloadLogsPage,
|
|
||||||
preloadNavigationRoutes,
|
|
||||||
} from './_navigation'
|
|
||||||
|
|
||||||
import 'dayjs/locale/ru'
|
import 'dayjs/locale/ru'
|
||||||
import 'dayjs/locale/zh-cn'
|
import 'dayjs/locale/zh-cn'
|
||||||
|
|
||||||
const LogsPage = lazy(() => preloadLogsPage())
|
|
||||||
|
|
||||||
type NavItem = (typeof navItems)[number]
|
type NavItem = (typeof navItems)[number]
|
||||||
|
|
||||||
type MenuContextPosition = { top: number; left: number }
|
type MenuContextPosition = { top: number; left: number }
|
||||||
@@ -107,7 +92,6 @@ const SortableNavMenuItem = ({ item, label }: SortableNavMenuItemProps) => {
|
|||||||
<LayoutItem
|
<LayoutItem
|
||||||
to={item.path}
|
to={item.path}
|
||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
onPreload={item.preload}
|
|
||||||
sortable={{
|
sortable={{
|
||||||
setNodeRef,
|
setNodeRef,
|
||||||
attributes,
|
attributes,
|
||||||
@@ -135,9 +119,6 @@ const Layout = () => {
|
|||||||
const navCollapsed = verge?.collapse_navbar ?? false
|
const navCollapsed = verge?.collapse_navbar ?? false
|
||||||
const { switchLanguage } = useI18n()
|
const { switchLanguage } = useI18n()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { pathname } = useLocation()
|
|
||||||
const isLogsPage = pathname === '/logs'
|
|
||||||
const pageVisible = useVisibility()
|
|
||||||
const themeReady = useMemo(() => Boolean(theme), [theme])
|
const themeReady = useMemo(() => Boolean(theme), [theme])
|
||||||
|
|
||||||
const [menuUnlocked, setMenuUnlocked] = useState(false)
|
const [menuUnlocked, setMenuUnlocked] = useState(false)
|
||||||
@@ -236,19 +217,6 @@ const Layout = () => {
|
|||||||
|
|
||||||
useLoadingOverlay(themeReady)
|
useLoadingOverlay(themeReady)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!themeReady || !pageVisible) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const controller = new AbortController()
|
|
||||||
void preloadNavigationRoutes(controller.signal)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
controller.abort()
|
|
||||||
}
|
|
||||||
}, [themeReady, pageVisible])
|
|
||||||
|
|
||||||
const handleNotice = useCallback(
|
const handleNotice = useCallback(
|
||||||
(payload: [string, string]) => {
|
(payload: [string, string]) => {
|
||||||
const [status, msg] = payload
|
const [status, msg] = payload
|
||||||
@@ -425,12 +393,7 @@ const Layout = () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<LayoutItem
|
<LayoutItem key={item.path} to={item.path} icon={item.icon}>
|
||||||
key={item.path}
|
|
||||||
to={item.path}
|
|
||||||
icon={item.icon}
|
|
||||||
onPreload={item.preload}
|
|
||||||
>
|
|
||||||
{t(item.label)}
|
{t(item.label)}
|
||||||
</LayoutItem>
|
</LayoutItem>
|
||||||
)
|
)
|
||||||
@@ -490,34 +453,6 @@ const Layout = () => {
|
|||||||
<BaseErrorBoundary>
|
<BaseErrorBoundary>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</BaseErrorBoundary>
|
</BaseErrorBoundary>
|
||||||
{isLogsPage && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
height: '100%',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BaseLoading />
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<LogsPage />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+15
-122
@@ -6,8 +6,7 @@ import LockOpenOutlinedIcon from '@mui/icons-material/LockOpenOutlined'
|
|||||||
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'
|
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'
|
||||||
import SubjectOutlinedIcon from '@mui/icons-material/SubjectOutlined'
|
import SubjectOutlinedIcon from '@mui/icons-material/SubjectOutlined'
|
||||||
import WifiOutlinedIcon from '@mui/icons-material/WifiOutlined'
|
import WifiOutlinedIcon from '@mui/icons-material/WifiOutlined'
|
||||||
import { Box } from '@mui/material'
|
import { type ComponentType, type ReactNode } from 'react'
|
||||||
import { lazy, Suspense, type ComponentType, type ReactNode } from 'react'
|
|
||||||
|
|
||||||
import ConnectionsSvg from '@/assets/image/itemicon/connections.svg?react'
|
import ConnectionsSvg from '@/assets/image/itemicon/connections.svg?react'
|
||||||
import HomeSvg from '@/assets/image/itemicon/home.svg?react'
|
import HomeSvg from '@/assets/image/itemicon/home.svg?react'
|
||||||
@@ -17,115 +16,24 @@ import ProxiesSvg from '@/assets/image/itemicon/proxies.svg?react'
|
|||||||
import RulesSvg from '@/assets/image/itemicon/rules.svg?react'
|
import RulesSvg from '@/assets/image/itemicon/rules.svg?react'
|
||||||
import SettingsSvg from '@/assets/image/itemicon/settings.svg?react'
|
import SettingsSvg from '@/assets/image/itemicon/settings.svg?react'
|
||||||
import UnlockSvg from '@/assets/image/itemicon/unlock.svg?react'
|
import UnlockSvg from '@/assets/image/itemicon/unlock.svg?react'
|
||||||
import { BaseLoading } from '@/components/base'
|
|
||||||
import { ensureLanguageSections } from '@/services/i18n'
|
|
||||||
|
|
||||||
import { navigationItems } from './_navigation-meta'
|
import { navigationItems } from './_navigation-meta'
|
||||||
|
import ConnectionsPage from './connections'
|
||||||
import HomePage from './home'
|
import HomePage from './home'
|
||||||
|
import LogsPage from './logs'
|
||||||
|
import ProfilePage from './profiles'
|
||||||
|
import ProxyPage from './proxies'
|
||||||
|
import RulesPage from './rules'
|
||||||
|
import SettingPage from './settings'
|
||||||
|
import UnlockPage from './unlock'
|
||||||
|
|
||||||
type NavigationItem = {
|
type NavigationItem = {
|
||||||
label: (typeof navigationItems)[keyof typeof navigationItems]['label']
|
label: (typeof navigationItems)[keyof typeof navigationItems]['label']
|
||||||
path: string
|
path: string
|
||||||
icon: ReactNode[]
|
icon: ReactNode[]
|
||||||
Component: ComponentType
|
Component: ComponentType
|
||||||
preload?: () => Promise<{ default: ComponentType }>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const waitForWarmupIdle = (signal: AbortSignal) =>
|
|
||||||
new Promise<void>((resolve) => {
|
|
||||||
let idleId: number | undefined
|
|
||||||
let timeoutId: number | undefined
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
signal.removeEventListener('abort', finish)
|
|
||||||
if (idleId !== undefined) {
|
|
||||||
window.cancelIdleCallback(idleId)
|
|
||||||
}
|
|
||||||
if (timeoutId !== undefined) {
|
|
||||||
window.clearTimeout(timeoutId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const finish = () => {
|
|
||||||
cleanup()
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signal.aborted) {
|
|
||||||
resolve()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
signal.addEventListener('abort', finish, { once: true })
|
|
||||||
|
|
||||||
if (window.requestIdleCallback) {
|
|
||||||
idleId = window.requestIdleCallback(finish, { timeout: 500 })
|
|
||||||
} else {
|
|
||||||
timeoutId = window.setTimeout(finish, 120)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const createRoutePreload = (
|
|
||||||
load: () => Promise<{ default: ComponentType }>,
|
|
||||||
sections?: string | readonly string[],
|
|
||||||
) => {
|
|
||||||
let componentPromise: Promise<{ default: ComponentType }> | undefined
|
|
||||||
|
|
||||||
const loadComponent = () => {
|
|
||||||
componentPromise ??= load().catch((error) => {
|
|
||||||
componentPromise = undefined
|
|
||||||
throw error
|
|
||||||
})
|
|
||||||
|
|
||||||
return componentPromise
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sections) {
|
|
||||||
return loadComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
const [component] = await Promise.all([
|
|
||||||
loadComponent(),
|
|
||||||
ensureLanguageSections(sections),
|
|
||||||
])
|
|
||||||
return component
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createLazyRoute = (
|
|
||||||
load: () => Promise<{ default: ComponentType }>,
|
|
||||||
sections?: string | readonly string[],
|
|
||||||
) => {
|
|
||||||
const preload = createRoutePreload(load, sections)
|
|
||||||
const Component = lazy(preload)
|
|
||||||
const LazyRoute = () => (
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
height: '100%',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BaseLoading />
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Component />
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
|
|
||||||
return { Component: LazyRoute, preload }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const preloadLogsPage = createRoutePreload(
|
|
||||||
() => import('./logs'),
|
|
||||||
'logs',
|
|
||||||
)
|
|
||||||
|
|
||||||
export const navItems: NavigationItem[] = [
|
export const navItems: NavigationItem[] = [
|
||||||
{
|
{
|
||||||
...navigationItems.home,
|
...navigationItems.home,
|
||||||
@@ -135,51 +43,36 @@ export const navItems: NavigationItem[] = [
|
|||||||
{
|
{
|
||||||
...navigationItems.proxies,
|
...navigationItems.proxies,
|
||||||
icon: [<WifiOutlinedIcon key="mui" />, <ProxiesSvg key="svg" />],
|
icon: [<WifiOutlinedIcon key="mui" />, <ProxiesSvg key="svg" />],
|
||||||
...createLazyRoute(() => import('./proxies')),
|
Component: ProxyPage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...navigationItems.profiles,
|
...navigationItems.profiles,
|
||||||
icon: [<DnsOutlinedIcon key="mui" />, <ProfilesSvg key="svg" />],
|
icon: [<DnsOutlinedIcon key="mui" />, <ProfilesSvg key="svg" />],
|
||||||
...createLazyRoute(() => import('./profiles'), 'rules'),
|
Component: ProfilePage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...navigationItems.connections,
|
...navigationItems.connections,
|
||||||
icon: [<LanguageOutlinedIcon key="mui" />, <ConnectionsSvg key="svg" />],
|
icon: [<LanguageOutlinedIcon key="mui" />, <ConnectionsSvg key="svg" />],
|
||||||
...createLazyRoute(() => import('./connections'), 'connections'),
|
Component: ConnectionsPage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...navigationItems.rules,
|
...navigationItems.rules,
|
||||||
icon: [<ForkRightOutlinedIcon key="mui" />, <RulesSvg key="svg" />],
|
icon: [<ForkRightOutlinedIcon key="mui" />, <RulesSvg key="svg" />],
|
||||||
...createLazyRoute(() => import('./rules'), 'rules'),
|
Component: RulesPage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...navigationItems.logs,
|
...navigationItems.logs,
|
||||||
icon: [<SubjectOutlinedIcon key="mui" />, <LogsSvg key="svg" />],
|
icon: [<SubjectOutlinedIcon key="mui" />, <LogsSvg key="svg" />],
|
||||||
Component: () => null /* LogsPage rendered in Layout only on /logs route */,
|
Component: LogsPage,
|
||||||
preload: preloadLogsPage,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...navigationItems.unlock,
|
...navigationItems.unlock,
|
||||||
icon: [<LockOpenOutlinedIcon key="mui" />, <UnlockSvg key="svg" />],
|
icon: [<LockOpenOutlinedIcon key="mui" />, <UnlockSvg key="svg" />],
|
||||||
...createLazyRoute(() => import('./unlock')),
|
Component: UnlockPage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...navigationItems.settings,
|
...navigationItems.settings,
|
||||||
icon: [<SettingsOutlinedIcon key="mui" />, <SettingsSvg key="svg" />],
|
icon: [<SettingsOutlinedIcon key="mui" />, <SettingsSvg key="svg" />],
|
||||||
...createLazyRoute(() => import('./settings')),
|
Component: SettingPage,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const preloadNavigationRoutes = async (signal: AbortSignal) => {
|
|
||||||
await waitForWarmupIdle(signal)
|
|
||||||
if (signal.aborted) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
navItems.map((item) => {
|
|
||||||
const preload = 'preload' in item ? item.preload : undefined
|
|
||||||
return preload?.().catch(() => {})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
+23
-12
@@ -36,26 +36,37 @@ import { useProfiles } from '@/hooks/use-profiles'
|
|||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { entry_lightweight_mode, openWebUrl } from '@/services/cmds'
|
import { entry_lightweight_mode, openWebUrl } from '@/services/cmds'
|
||||||
|
|
||||||
const LazyTestCard = lazy(() =>
|
const preloadTestCard = () =>
|
||||||
import('@/components/home/test-card').then((module) => ({
|
import('@/components/home/test-card').then((module) => ({
|
||||||
default: module.TestCard,
|
default: module.TestCard,
|
||||||
})),
|
}))
|
||||||
)
|
const preloadIpInfoCard = () =>
|
||||||
const LazyIpInfoCard = lazy(() =>
|
|
||||||
import('@/components/home/ip-info-card').then((module) => ({
|
import('@/components/home/ip-info-card').then((module) => ({
|
||||||
default: module.IpInfoCard,
|
default: module.IpInfoCard,
|
||||||
})),
|
}))
|
||||||
)
|
const preloadClashInfoCard = () =>
|
||||||
const LazyClashInfoCard = lazy(() =>
|
|
||||||
import('@/components/home/clash-info-card').then((module) => ({
|
import('@/components/home/clash-info-card').then((module) => ({
|
||||||
default: module.ClashInfoCard,
|
default: module.ClashInfoCard,
|
||||||
})),
|
}))
|
||||||
)
|
const preloadSystemInfoCard = () =>
|
||||||
const LazySystemInfoCard = lazy(() =>
|
|
||||||
import('@/components/home/system-info-card').then((module) => ({
|
import('@/components/home/system-info-card').then((module) => ({
|
||||||
default: module.SystemInfoCard,
|
default: module.SystemInfoCard,
|
||||||
})),
|
}))
|
||||||
)
|
|
||||||
|
const LazyTestCard = lazy(preloadTestCard)
|
||||||
|
const LazyIpInfoCard = lazy(preloadIpInfoCard)
|
||||||
|
const LazyClashInfoCard = lazy(preloadClashInfoCard)
|
||||||
|
const LazySystemInfoCard = lazy(preloadSystemInfoCard)
|
||||||
|
|
||||||
|
// Used by bootstrap to initiate optional card imports without blocking render.
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
export const preloadHomePageCards = () =>
|
||||||
|
Promise.all([
|
||||||
|
preloadTestCard().catch(() => {}),
|
||||||
|
preloadIpInfoCard().catch(() => {}),
|
||||||
|
preloadClashInfoCard().catch(() => {}),
|
||||||
|
preloadSystemInfoCard().catch(() => {}),
|
||||||
|
])
|
||||||
|
|
||||||
// 定义首页卡片设置接口
|
// 定义首页卡片设置接口
|
||||||
interface HomeCardsSettings {
|
interface HomeCardsSettings {
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ const STARTUP_LANGUAGE_SECTIONS = [
|
|||||||
'profiles',
|
'profiles',
|
||||||
'proxies',
|
'proxies',
|
||||||
'tests',
|
'tests',
|
||||||
|
'rules',
|
||||||
|
'connections',
|
||||||
|
'logs',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const localeModules = import.meta.glob<LocaleModule>('@/locales/*/*.json')
|
const localeModules = import.meta.glob<LocaleModule>('@/locales/*/*.json')
|
||||||
@@ -174,7 +177,7 @@ export const changeLanguage = async (language: string) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
await ensureLanguageSections(
|
await ensureLanguageSections(
|
||||||
loadedSections.length ? loadedSections : STARTUP_LANGUAGE_SECTIONS,
|
[...new Set([...loadedSections, ...STARTUP_LANGUAGE_SECTIONS])],
|
||||||
targetLanguage,
|
targetLanguage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user