🐛 fix: fixed the hydrated false problem (#10308)

* fix: fixed the hydrated error problem

* fix: use next/dynamic to replace react-router-dom lazy import

* fix: add registor NavigatorRegistrar back

* fix: add dynamic loading components

* fix: change the dynamic config

* fix: add losting loading layout

* fix: delete useless memo

* fix: add  ErrorBoundary in some layout
This commit is contained in:
Shinji-Li
2025-11-20 00:04:04 +08:00
committed by GitHub
parent a7d1878630
commit 340aa2a9e9
9 changed files with 627 additions and 373 deletions

View File

@@ -0,0 +1,20 @@
'use client';
import { RouterProvider } from 'react-router-dom';
import type { Locales } from '@/types/locale';
import { createDesktopRouter } from './desktopRouter.config';
interface ClientRouterProps {
locale: Locales;
}
const ClientRouter = ({ locale }: ClientRouterProps) => {
const router = createDesktopRouter(locale);
return <RouterProvider router={router} />;
};
ClientRouter.displayName = 'ClientRouter';
export default ClientRouter;

View File

@@ -1,31 +1,11 @@
'use client';
import dynamic from 'next/dynamic';
import { memo, useMemo } from 'react';
import { RouterProvider } from 'react-router-dom';
import BootErrorBoundary from '@/components/BootErrorBoundary';
import Loading from '@/components/Loading/BrandTextLoading';
import type { Locales } from '@/types/locale';
import { createDesktopRouter } from './desktopRouter.config';
interface ClientRouterProps {
locale: Locales;
}
const ClientRouter = memo<ClientRouterProps>(({ locale }) => {
const router = useMemo(() => createDesktopRouter(locale), [locale]);
return (
<BootErrorBoundary fallback={<Loading />}>
<RouterProvider router={router} />
</BootErrorBoundary>
);
});
ClientRouter.displayName = 'ClientRouter';
const DesktopRouterClient = dynamic(() => Promise.resolve(ClientRouter), {
const DesktopRouterClient = dynamic(() => import('./DesktopClientRouter'), {
loading: () => <Loading />,
ssr: false,
});

View File

@@ -0,0 +1,20 @@
'use client';
import { RouterProvider } from 'react-router-dom';
import type { Locales } from '@/types/locale';
import { createMobileRouter } from './mobileRouter.config';
interface ClientRouterProps {
locale: Locales;
}
const ClientRouter = ({ locale }: ClientRouterProps) => {
const router = createMobileRouter(locale);
return <RouterProvider router={router} />;
};
ClientRouter.displayName = 'ClientRouter';
export default ClientRouter;

View File

@@ -1,32 +1,11 @@
'use client';
import dynamic from 'next/dynamic';
import { memo, useMemo } from 'react';
import { RouterProvider } from 'react-router-dom';
import BootErrorBoundary from '@/components/BootErrorBoundary';
import Loading from '@/components/Loading/BrandTextLoading';
import type { Locales } from '@/types/locale';
import { createMobileRouter } from './mobileRouter.config';
interface ClientRouterProps {
locale: Locales;
}
const ClientRouter = memo<ClientRouterProps>(({ locale }) => {
const router = useMemo(() => createMobileRouter(locale), [locale]);
return (
<BootErrorBoundary fallback={<Loading />}>
<RouterProvider router={router} />
</BootErrorBoundary>
);
});
ClientRouter.displayName = 'ClientRouter';
const MobileRouterClient = dynamic(() => Promise.resolve(ClientRouter), {
const MobileRouterClient = dynamic(() => import('./MobileClientRouter'), {
loading: () => <Loading />,
ssr: false,
});

View File

@@ -1,8 +1,10 @@
'use client';
import dynamic from 'next/dynamic';
import { useEffect } from 'react';
import { type LoaderFunction, createBrowserRouter, redirect, useNavigate } from 'react-router-dom';
import { createBrowserRouter, redirect, useNavigate, useRouteError } from 'react-router-dom';
import ErrorCapture from '@/components/Error';
import Loading from '@/components/Loading/BrandTextLoading';
import { useGlobalStore } from '@/store/global';
import type { Locales } from '@/types/locale';
@@ -10,6 +12,213 @@ import type { Locales } from '@/types/locale';
import DesktopMainLayout from './(main)/layouts/desktop';
import { idLoader, slugLoader } from './loaders/routeParams';
/**
* Desktop Router Configuration - Pure CSR Mode
*
* IMPORTANT: This router runs ONLY in the browser (client-side).
*
* Key characteristics:
* - createBrowserRouter uses window.history API (client-only)
* - All loaders execute in the browser during navigation
* - No server-side rendering or hydration involved
* - Route data fetching happens on-demand during client navigation
*
* The entire router tree is wrapped with Next.js dynamic import (ssr: false),
* ensuring this code never executes on the server.
*/
// Chat components
const DesktopChatPage = dynamic(
() => import('./(main)/chat/index').then((m) => m.DesktopChatPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const ChatLayout = dynamic(() => import('./(main)/chat/_layout/Desktop'), {
loading: () => <Loading />,
ssr: false,
});
// Discover List components
const DesktopHomePage = dynamic(
() => import('./(main)/discover/(list)/(home)/index').then((m) => m.DesktopHomePage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DesktopAssistantPage = dynamic(
() => import('./(main)/discover/(list)/assistant/index').then((m) => m.DesktopAssistantPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverAssistantLayout = dynamic(
() => import('./(main)/discover/(list)/assistant/_layout/Desktop'),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverListMcpPage = dynamic(
() => import('./(main)/discover/(list)/mcp/index').then((m) => m.DesktopMcpPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverMcpLayout = dynamic(() => import('./(main)/discover/(list)/mcp/_layout/Desktop'), {
loading: () => <Loading />,
ssr: false,
});
const DiscoverListModelPage = dynamic(
() => import('./(main)/discover/(list)/model/index').then((m) => m.DesktopModelPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverModelLayout = dynamic(
() => import('./(main)/discover/(list)/model/_layout/Desktop'),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverListProviderPage = dynamic(
() => import('./(main)/discover/(list)/provider/index').then((m) => m.DesktopProviderPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverListLayout = dynamic(() => import('./(main)/discover/(list)/_layout/Desktop/index'), {
loading: () => <Loading />,
ssr: false,
});
// Discover Detail components
const DesktopDiscoverAssistantDetailPage = dynamic(
() =>
import('./(main)/discover/(detail)/assistant/index').then(
(m) => m.DesktopDiscoverAssistantDetailPage,
),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverDetailMcpPage = dynamic(
() => import('./(main)/discover/(detail)/mcp/index').then((m) => m.DesktopMcpPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverDetailModelPage = dynamic(
() => import('./(main)/discover/(detail)/model/index').then((m) => m.DesktopModelPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverDetailProviderPage = dynamic(
() => import('./(main)/discover/(detail)/provider/index').then((m) => m.DesktopProviderPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverDetailLayout = dynamic(() => import('./(main)/discover/(detail)/_layout/Desktop'), {
loading: () => <Loading />,
ssr: false,
});
const DiscoverLayout = dynamic(() => import('./(main)/discover/_layout/Desktop/index'), {
loading: () => <Loading />,
ssr: false,
});
// Knowledge components
const KnowledgeHome = dynamic(() => import('./(main)/knowledge/routes/KnowledgeHome'), {
loading: () => <Loading />,
ssr: false,
});
const KnowledgeBasesList = dynamic(() => import('./(main)/knowledge/routes/KnowledgeBasesList'), {
loading: () => <Loading />,
ssr: false,
});
const KnowledgeBaseDetail = dynamic(() => import('./(main)/knowledge/routes/KnowledgeBaseDetail'), {
loading: () => <Loading />,
ssr: false,
});
const KnowledgeLayout = dynamic(() => import('./(main)/knowledge/_layout/Desktop'), {
loading: () => <Loading />,
ssr: false,
});
// Settings components
const SettingsLayout = dynamic(() => import('./(main)/settings/_layout/Desktop'), {
loading: () => <Loading />,
ssr: false,
});
const SettingsLayoutWrapper = dynamic(() => import('./(main)/settings/_layout/DesktopWrapper'), {
loading: () => <Loading />,
ssr: false,
});
// Image components
const ImagePage = dynamic(() => import('./(main)/image'), {
loading: () => <Loading />,
ssr: false,
});
const ImageLayoutWrapper = dynamic(() => import('./(main)/image/_layout/DesktopWrapper'), {
loading: () => <Loading />,
ssr: false,
});
// Labs components
const LabsPage = dynamic(() => import('./(main)/labs'), {
loading: () => <Loading />,
ssr: false,
});
// Profile components
const ProfileHomePage = dynamic(() => import('./(main)/profile/(home)/desktop'), {
loading: () => <Loading />,
ssr: false,
});
const ProfileApikeyPage = dynamic(() => import('./(main)/profile/apikey/index'), {
loading: () => <Loading />,
ssr: false,
});
const DesktopProfileSecurityPage = dynamic(
() => import('./(main)/profile/security/index').then((m) => m.DesktopProfileSecurityPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DesktopProfileStatsPage = dynamic(
() => import('./(main)/profile/stats/index').then((m) => m.DesktopProfileStatsPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DesktopProfileUsagePage = dynamic(
() => import('./(main)/profile/usage/index').then((m) => m.DesktopProfileUsagePage),
{
loading: () => <Loading />,
ssr: false,
},
);
const ProfileLayoutWrapper = dynamic(() => import('./(main)/profile/_layout/DesktopWrapper'), {
loading: () => <Loading />,
ssr: false,
});
// Component to register navigate function in global store
const NavigatorRegistrar = () => {
const navigate = useNavigate();
@@ -25,50 +234,57 @@ const NavigatorRegistrar = () => {
return null;
};
// Root layout wrapper component - just registers navigator and renders outlet
// Note: Desktop layout is provided by individual route components
const RootLayout = (props: { locale: Locales }) => {
return (
<>
<NavigatorRegistrar />
<DesktopMainLayout locale={props.locale} />
</>
);
// Error boundary factory for React Router errorElement
const createErrorBoundary = (resetPath: string) => {
const ErrorBoundary = () => {
const error = useRouteError() as Error;
const navigate = useNavigate();
const reset = () => {
navigate(resetPath);
};
return <ErrorCapture error={error} reset={reset} />;
};
return ErrorBoundary;
};
// Hydration gate loader -always return true to bypass hydration gate
const hydrationGateLoader: LoaderFunction = () => {
return true
};
// Create error boundaries for each route
const ChatErrorBoundary = createErrorBoundary('/chat');
const DiscoverErrorBoundary = createErrorBoundary('/discover');
const KnowledgeErrorBoundary = createErrorBoundary('/knowledge');
const SettingsErrorBoundary = createErrorBoundary('/settings');
const ImageErrorBoundary = createErrorBoundary('/image');
const ProfileErrorBoundary = createErrorBoundary('/profile');
const RootErrorBoundary = createErrorBoundary('/chat'); // Root level falls back to chat
// Root layout wrapper component
const RootLayout = (props: { locale: Locales }) => (
<>
<NavigatorRegistrar />
<DesktopMainLayout locale={props.locale} />
</>
);
// Create desktop router configuration
export const createDesktopRouter = (locale: Locales) =>
createBrowserRouter([
{
HydrateFallback: () => <Loading />,
children: [
// Chat routes
{
children: [
{
element: <DesktopChatPage />,
index: true,
lazy: () =>
import('./(main)/chat/index').then((m) => ({
Component: m.DesktopChatPage,
})),
},
{
lazy: () =>
import('./(main)/chat/index').then((m) => ({
Component: m.DesktopChatPage,
})),
element: <DesktopChatPage />,
path: '*',
},
],
lazy: () =>
import('./(main)/chat/_layout/Desktop').then((m) => ({
Component: m.default,
})),
element: <ChatLayout />,
errorElement: <ChatErrorBoundary />,
path: 'chat',
},
@@ -81,117 +297,73 @@ export const createDesktopRouter = (locale: Locales) =>
{
children: [
{
element: <DesktopAssistantPage />,
index: true,
lazy: () =>
import('./(main)/discover/(list)/assistant/index').then((m) => ({
Component: m.DesktopAssistantPage,
})),
},
],
lazy: () =>
import('./(main)/discover/(list)/assistant/_layout/Desktop').then((m) => ({
Component: m.default,
})),
element: <DiscoverAssistantLayout />,
path: 'assistant',
},
{
children: [
{
element: <DiscoverListModelPage />,
index: true,
lazy: () =>
import('./(main)/discover/(list)/model/index').then((m) => ({
Component: m.DesktopModelPage,
})),
},
],
lazy: () =>
import('./(main)/discover/(list)/model/_layout/Desktop').then((m) => ({
Component: m.default,
})),
element: <DiscoverModelLayout />,
path: 'model',
},
{
lazy: () =>
import('./(main)/discover/(list)/provider/index').then((m) => ({
Component: m.DesktopProviderPage,
})),
element: <DiscoverListProviderPage />,
path: 'provider',
},
{
children: [
{
element: <DiscoverListMcpPage />,
index: true,
lazy: () =>
import('./(main)/discover/(list)/mcp/index').then((m) => ({
Component: m.DesktopMcpPage,
})),
},
],
lazy: () =>
import('./(main)/discover/(list)/mcp/_layout/Desktop').then((m) => ({
Component: m.default,
})),
element: <DiscoverMcpLayout />,
path: 'mcp',
},
{
element: <DesktopHomePage />,
index: true,
lazy: () =>
import('./(main)/discover/(list)/(home)/index').then((m) => ({
Component: m.DesktopHomePage,
})),
},
],
lazy: () =>
import('./(main)/discover/(list)/_layout/Desktop/index').then((m) => ({
Component: m.default,
})),
element: <DiscoverListLayout />,
},
// Detail routes (with DetailLayout)
{
children: [
{
lazy: () =>
import('./(main)/discover/(detail)/assistant/index').then((m) => ({
Component: m.DesktopDiscoverAssistantDetailPage,
})),
element: <DesktopDiscoverAssistantDetailPage />,
loader: slugLoader,
path: 'assistant/:slug',
},
{
lazy: () =>
import('./(main)/discover/(detail)/model/index').then((m) => ({
Component: m.DesktopModelPage,
})),
element: <DiscoverDetailModelPage />,
loader: slugLoader,
path: 'model/:slug',
},
{
lazy: () =>
import('./(main)/discover/(detail)/provider/index').then((m) => ({
Component: m.DesktopProviderPage,
})),
element: <DiscoverDetailProviderPage />,
loader: slugLoader,
path: 'provider/:slug',
},
{
lazy: () =>
import('./(main)/discover/(detail)/mcp/index').then((m) => ({
Component: m.DesktopMcpPage,
})),
element: <DiscoverDetailMcpPage />,
loader: slugLoader,
path: 'mcp/:slug',
},
],
lazy: () =>
import('./(main)/discover/(detail)/_layout/Desktop').then((m) => ({
Component: m.default,
})),
element: <DiscoverDetailLayout />,
},
],
lazy: () =>
import('./(main)/discover/_layout/Desktop/index').then((m) => ({
Component: m.default,
})),
element: <DiscoverLayout />,
errorElement: <DiscoverErrorBoundary />,
path: 'discover',
},
@@ -199,40 +371,26 @@ export const createDesktopRouter = (locale: Locales) =>
{
children: [
{
element: <KnowledgeHome />,
index: true,
lazy: () =>
import('./(main)/knowledge/routes/KnowledgeHome').then((m) => ({
Component: m.default,
})),
},
{
lazy: () =>
import('./(main)/knowledge/routes/KnowledgeBasesList').then((m) => ({
Component: m.default,
})),
element: <KnowledgeBasesList />,
path: 'bases',
},
{
lazy: () =>
import('./(main)/knowledge/routes/KnowledgeBaseDetail').then((m) => ({
Component: m.default,
})),
element: <KnowledgeBaseDetail />,
loader: idLoader,
path: 'bases/:id',
},
{
lazy: () =>
import('./(main)/knowledge/routes/KnowledgeBaseDetail').then((m) => ({
Component: m.default,
})),
element: <KnowledgeBaseDetail />,
loader: idLoader,
path: '*',
},
],
lazy: () =>
import('./(main)/knowledge/_layout/Desktop').then((m) => ({
Component: m.default,
})),
element: <KnowledgeLayout />,
errorElement: <KnowledgeErrorBoundary />,
path: 'knowledge',
},
@@ -240,17 +398,12 @@ export const createDesktopRouter = (locale: Locales) =>
{
children: [
{
element: <SettingsLayout />,
index: true,
lazy: () =>
import('./(main)/settings/_layout/Desktop').then((m) => ({
Component: m.default,
})),
},
],
lazy: () =>
import('./(main)/settings/_layout/DesktopWrapper').then((m) => ({
Component: m.default,
})),
element: <SettingsLayoutWrapper />,
errorElement: <SettingsErrorBoundary />,
path: 'settings',
},
@@ -258,26 +411,18 @@ export const createDesktopRouter = (locale: Locales) =>
{
children: [
{
element: <ImagePage />,
index: true,
lazy: () =>
import('./(main)/image').then((m) => ({
Component: m.default,
})),
},
],
lazy: () =>
import('./(main)/image/_layout/DesktopWrapper').then((m) => ({
Component: m.default,
})),
element: <ImageLayoutWrapper />,
errorElement: <ImageErrorBoundary />,
path: 'image',
},
// Labs routes
{
lazy: () =>
import('./(main)/labs').then((m) => ({
Component: m.default,
})),
element: <LabsPage />,
path: 'labs',
},
@@ -285,45 +430,28 @@ export const createDesktopRouter = (locale: Locales) =>
{
children: [
{
element: <ProfileHomePage />,
index: true,
lazy: () =>
import('./(main)/profile/(home)/desktop').then((m) => ({
Component: m.default,
})),
},
{
lazy: () =>
import('./(main)/profile/apikey/index').then((m) => ({
Component: m.default,
})),
element: <ProfileApikeyPage />,
path: 'apikey',
},
{
lazy: () =>
import('./(main)/profile/security/index').then((m) => ({
Component: m.DesktopProfileSecurityPage,
})),
element: <DesktopProfileSecurityPage />,
path: 'security',
},
{
lazy: () =>
import('./(main)/profile/stats/index').then((m) => ({
Component: m.DesktopProfileStatsPage,
})),
element: <DesktopProfileStatsPage />,
path: 'stats',
},
{
lazy: () =>
import('./(main)/profile/usage/index').then((m) => ({
Component: m.DesktopProfileUsagePage,
})),
element: <DesktopProfileUsagePage />,
path: 'usage',
},
],
lazy: () =>
import('./(main)/profile/_layout/DesktopWrapper').then((m) => ({
Component: m.default,
})),
element: <ProfileLayoutWrapper />,
errorElement: <ProfileErrorBoundary />,
path: 'profile',
},
@@ -340,7 +468,7 @@ export const createDesktopRouter = (locale: Locales) =>
},
],
element: <RootLayout locale={locale} />,
loader: hydrationGateLoader,
errorElement: <RootErrorBoundary />,
path: '/',
},
]);

View File

@@ -1,8 +1,10 @@
'use client';
import dynamic from 'next/dynamic';
import { useEffect } from 'react';
import { type LoaderFunction, createBrowserRouter, redirect, useNavigate } from 'react-router-dom';
import { createBrowserRouter, redirect, useNavigate, useRouteError } from 'react-router-dom';
import ErrorCapture from '@/components/Error';
import Loading from '@/components/Loading/BrandTextLoading';
import { useGlobalStore } from '@/store/global';
import type { Locales } from '@/types/locale';
@@ -10,6 +12,233 @@ import type { Locales } from '@/types/locale';
import { MobileMainLayout } from './(main)/layouts/mobile';
import { idLoader, slugLoader } from './loaders/routeParams';
/**
* Mobile Router Configuration - Pure CSR Mode
*
* IMPORTANT: This router runs ONLY in the browser (client-side).
*
* Key characteristics:
* - createBrowserRouter uses window.history API (client-only)
* - All loaders execute in the browser during navigation
* - No server-side rendering or hydration involved
* - Route data fetching happens on-demand during client navigation
*
* The entire router tree is wrapped with Next.js dynamic import (ssr: false),
* ensuring this code never executes on the server.
*/
// Chat components
const MobileChatPage = dynamic(() => import('./(main)/chat/index').then((m) => m.MobileChatPage), {
loading: () => <Loading />,
ssr: false,
});
const ChatSettings = dynamic(() => import('./(main)/chat/settings'), {
loading: () => <Loading />,
ssr: false,
});
const ChatLayout = dynamic(() => import('./(main)/chat/_layout/Mobile'), {
loading: () => <Loading />,
ssr: false,
});
// Discover List components
const MobileHomePage = dynamic(
() => import('./(main)/discover/(list)/(home)/index').then((m) => m.MobileHomePage),
{
loading: () => <Loading />,
ssr: false,
},
);
const MobileAssistantPage = dynamic(
() => import('./(main)/discover/(list)/assistant/index').then((m) => m.MobileAssistantPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverAssistantLayout = dynamic(
() => import('./(main)/discover/(list)/assistant/_layout/Mobile'),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverListMobileModelPage = dynamic(
() => import('./(main)/discover/(list)/model/index').then((m) => m.MobileModelPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverModelLayout = dynamic(() => import('./(main)/discover/(list)/model/_layout/Mobile'), {
loading: () => <Loading />,
ssr: false,
});
const DiscoverListMobileProviderPage = dynamic(
() => import('./(main)/discover/(list)/provider/index').then((m) => m.MobileProviderPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverListMobileMcpPage = dynamic(
() => import('./(main)/discover/(list)/mcp/index').then((m) => m.MobileMcpPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverMcpLayout = dynamic(() => import('./(main)/discover/(list)/mcp/_layout/Mobile'), {
loading: () => <Loading />,
ssr: false,
});
const DiscoverListLayout = dynamic(() => import('./(main)/discover/(list)/_layout/Mobile/index'), {
loading: () => <Loading />,
ssr: false,
});
// Discover Detail components
const MobileDiscoverAssistantDetailPage = dynamic(
() =>
import('./(main)/discover/(detail)/assistant/index').then(
(m) => m.MobileDiscoverAssistantDetailPage,
),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverDetailMobileModelPage = dynamic(
() => import('./(main)/discover/(detail)/model/index').then((m) => m.MobileModelPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverDetailMobileProviderPage = dynamic(
() => import('./(main)/discover/(detail)/provider/index').then((m) => m.MobileProviderPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverDetailMobileMcpPage = dynamic(
() => import('./(main)/discover/(detail)/mcp/index').then((m) => m.MobileMcpPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverDetailLayout = dynamic(
() => import('./(main)/discover/(detail)/_layout/Mobile/index'),
{
loading: () => <Loading />,
ssr: false,
},
);
const DiscoverLayout = dynamic(() => import('./(main)/discover/_layout/Mobile/index'), {
loading: () => <Loading />,
ssr: false,
});
// Knowledge components
const KnowledgeHome = dynamic(() => import('./(main)/knowledge/routes/KnowledgeHome'), {
loading: () => <Loading />,
ssr: false,
});
const KnowledgeBasesList = dynamic(() => import('./(main)/knowledge/routes/KnowledgeBasesList'), {
loading: () => <Loading />,
ssr: false,
});
const KnowledgeBaseDetail = dynamic(() => import('./(main)/knowledge/routes/KnowledgeBaseDetail'), {
loading: () => <Loading />,
ssr: false,
});
const KnowledgeLayout = dynamic(() => import('./(main)/knowledge/_layout/Mobile'), {
loading: () => <Loading />,
ssr: false,
});
// Settings components
const SettingsLayout = dynamic(() => import('./(main)/settings/_layout/Mobile'), {
loading: () => <Loading />,
ssr: false,
});
const SettingsLayoutWrapper = dynamic(() => import('./(main)/settings/_layout/MobileWrapper'), {
loading: () => <Loading />,
ssr: false,
});
// Image components
const ImageComingSoon = dynamic(() => import('./(main)/image/ComingSoon'), {
loading: () => <Loading />,
ssr: false,
});
const ImageLayoutMobile = dynamic(() => import('./(main)/image/_layout/Mobile'), {
loading: () => <Loading />,
ssr: false,
});
// Labs components
const LabsPage = dynamic(() => import('./(main)/labs'), {
loading: () => <Loading />,
ssr: false,
});
// Profile components
const ProfileHomePage = dynamic(() => import('./(main)/profile/(home)'), {
loading: () => <Loading />,
ssr: false,
});
const ProfileApikeyPage = dynamic(() => import('./(main)/profile/apikey/index'), {
loading: () => <Loading />,
ssr: false,
});
const MobileProfileSecurityPage = dynamic(
() => import('./(main)/profile/security').then((m) => m.MobileProfileSecurityPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const MobileProfileStatsPage = dynamic(
() => import('./(main)/profile/stats').then((m) => m.MobileProfileStatsPage),
{
loading: () => <Loading />,
ssr: false,
},
);
const ProfileLayoutMobile = dynamic(() => import('./(main)/profile/_layout/Mobile'), {
loading: () => <Loading />,
ssr: false,
});
// Me (mobile personal center) components
const MeHomePage = dynamic(() => import('./(main)/(mobile)/me/(home)'), {
loading: () => <Loading />,
ssr: false,
});
const MeHomeLayout = dynamic(() => import('./(main)/(mobile)/me/(home)/layout'), {
loading: () => <Loading />,
ssr: false,
});
const MeProfilePage = dynamic(() => import('./(main)/(mobile)/me/profile'), {
loading: () => <Loading />,
ssr: false,
});
const MeProfileLayout = dynamic(() => import('./(main)/(mobile)/me/profile/layout'), {
loading: () => <Loading />,
ssr: false,
});
const MeSettingsPage = dynamic(() => import('./(main)/(mobile)/me/settings'), {
loading: () => <Loading />,
ssr: false,
});
const MeSettingsLayout = dynamic(() => import('./(main)/(mobile)/me/settings/layout'), {
loading: () => <Loading />,
ssr: false,
});
// Component to register navigate function in global store
const NavigatorRegistrar = () => {
const navigate = useNavigate();
@@ -25,8 +254,32 @@ const NavigatorRegistrar = () => {
return null;
};
// Root layout wrapper component - just registers navigator and renders outlet
// Note: Mobile layout is provided by individual route components
// Error boundary factory for React Router errorElement
const createErrorBoundary = (resetPath: string) => {
const ErrorBoundary = () => {
const error = useRouteError() as Error;
const navigate = useNavigate();
const reset = () => {
navigate(resetPath);
};
return <ErrorCapture error={error} reset={reset} />;
};
return ErrorBoundary;
};
// Create error boundaries for each route
const ChatErrorBoundary = createErrorBoundary('/chat');
const DiscoverErrorBoundary = createErrorBoundary('/discover');
const KnowledgeErrorBoundary = createErrorBoundary('/knowledge');
const SettingsErrorBoundary = createErrorBoundary('/settings');
const ImageErrorBoundary = createErrorBoundary('/image');
const ProfileErrorBoundary = createErrorBoundary('/profile');
const MeErrorBoundary = createErrorBoundary('/me'); // Mobile only
const RootErrorBoundary = createErrorBoundary('/chat'); // Root level falls back to chat
// Root layout wrapper component
const RootLayout = (props: { locale: Locales }) => (
<>
<NavigatorRegistrar />
@@ -34,39 +287,25 @@ const RootLayout = (props: { locale: Locales }) => (
</>
);
// Hydration gate loader -always return true to bypass hydration gate
const hydrationGateLoader: LoaderFunction = () => {
return true
};
// Create mobile router configuration
export const createMobileRouter = (locale: Locales) =>
createBrowserRouter([
{
HydrateFallback: () => <Loading />,
children: [
// Chat routes
{
children: [
{
element: <MobileChatPage />,
index: true,
lazy: () =>
import('./(main)/chat/index').then((m) => ({
Component: m.MobileChatPage,
})),
},
{
lazy: () =>
import('./(main)/chat/settings').then((m) => ({
Component: m.default,
})),
element: <ChatSettings />,
path: 'settings',
},
],
lazy: () =>
import('./(main)/chat/_layout/Mobile').then((m) => ({
Component: m.default,
})),
element: <ChatLayout />,
errorElement: <ChatErrorBoundary />,
path: 'chat',
},
@@ -77,116 +316,72 @@ export const createMobileRouter = (locale: Locales) =>
{
children: [
{
element: <MobileHomePage />,
index: true,
lazy: () =>
import('./(main)/discover/(list)/(home)/index').then((m) => ({
Component: m.MobileHomePage,
})),
},
{
children: [
{
lazy: () =>
import('./(main)/discover/(list)/assistant/index').then((m) => ({
Component: m.MobileAssistantPage,
})),
element: <MobileAssistantPage />,
path: 'assistant',
},
],
lazy: () =>
import('./(main)/discover/(list)/assistant/_layout/Mobile').then((m) => ({
Component: m.default,
})),
element: <DiscoverAssistantLayout />,
},
{
children: [
{
lazy: () =>
import('./(main)/discover/(list)/model/index').then((m) => ({
Component: m.MobileModelPage,
})),
element: <DiscoverListMobileModelPage />,
path: 'model',
},
],
lazy: () =>
import('./(main)/discover/(list)/model/_layout/Mobile').then((m) => ({
Component: m.default,
})),
element: <DiscoverModelLayout />,
},
{
lazy: () =>
import('./(main)/discover/(list)/provider/index').then((m) => ({
Component: m.MobileProviderPage,
})),
element: <DiscoverListMobileProviderPage />,
path: 'provider',
},
{
children: [
{
lazy: () =>
import('./(main)/discover/(list)/mcp/index').then((m) => ({
Component: m.MobileMcpPage,
})),
element: <DiscoverListMobileMcpPage />,
path: 'mcp',
},
],
lazy: () =>
import('./(main)/discover/(list)/mcp/_layout/Mobile').then((m) => ({
Component: m.default,
})),
element: <DiscoverMcpLayout />,
},
],
lazy: () =>
import('./(main)/discover/(list)/_layout/Mobile/index').then((m) => ({
Component: m.default,
})),
element: <DiscoverListLayout />,
},
// Detail routes (with DetailLayout)
{
children: [
{
lazy: () =>
import('./(main)/discover/(detail)/assistant/index').then((m) => ({
Component: m.MobileDiscoverAssistantDetailPage,
})),
element: <MobileDiscoverAssistantDetailPage />,
loader: slugLoader,
path: 'assistant/:slug',
},
{
lazy: () =>
import('./(main)/discover/(detail)/model/index').then((m) => ({
Component: m.MobileModelPage,
})),
element: <DiscoverDetailMobileModelPage />,
loader: slugLoader,
path: 'model/:slug',
},
{
lazy: () =>
import('./(main)/discover/(detail)/provider/index').then((m) => ({
Component: m.MobileProviderPage,
})),
element: <DiscoverDetailMobileProviderPage />,
loader: slugLoader,
path: 'provider/:slug',
},
{
lazy: () =>
import('./(main)/discover/(detail)/mcp/index').then((m) => ({
Component: m.MobileMcpPage,
})),
element: <DiscoverDetailMobileMcpPage />,
loader: slugLoader,
path: 'mcp/:slug',
},
],
lazy: () =>
import('./(main)/discover/(detail)/_layout/Mobile/index').then((m) => ({
Component: m.default,
})),
element: <DiscoverDetailLayout />,
},
],
lazy: () =>
import('./(main)/discover/_layout/Mobile/index').then((m) => ({
Component: m.default,
})),
element: <DiscoverLayout />,
errorElement: <DiscoverErrorBoundary />,
path: 'discover',
},
@@ -194,32 +389,21 @@ export const createMobileRouter = (locale: Locales) =>
{
children: [
{
element: <KnowledgeHome />,
index: true,
lazy: () =>
import('./(main)/knowledge/routes/KnowledgeHome').then((m) => ({
Component: m.default,
})),
},
{
lazy: () =>
import('./(main)/knowledge/routes/KnowledgeBasesList').then((m) => ({
Component: m.default,
})),
element: <KnowledgeBasesList />,
path: 'bases',
},
{
lazy: () =>
import('./(main)/knowledge/routes/KnowledgeBaseDetail').then((m) => ({
Component: m.default,
})),
element: <KnowledgeBaseDetail />,
loader: idLoader,
path: 'bases/:id',
},
],
lazy: () =>
import('./(main)/knowledge/_layout/Mobile').then((m) => ({
Component: m.default,
})),
element: <KnowledgeLayout />,
errorElement: <KnowledgeErrorBoundary />,
path: 'knowledge',
},
@@ -227,17 +411,12 @@ export const createMobileRouter = (locale: Locales) =>
{
children: [
{
element: <SettingsLayout />,
index: true,
lazy: () =>
import('./(main)/settings/_layout/Mobile').then((m) => ({
Component: m.default,
})),
},
],
lazy: () =>
import('./(main)/settings/_layout/MobileWrapper').then((m) => ({
Component: m.default,
})),
element: <SettingsLayoutWrapper />,
errorElement: <SettingsErrorBoundary />,
path: 'settings',
},
@@ -245,26 +424,18 @@ export const createMobileRouter = (locale: Locales) =>
{
children: [
{
element: <ImageComingSoon />,
index: true,
lazy: () =>
import('./(main)/image/ComingSoon').then((m) => ({
Component: m.default,
})),
},
],
lazy: () =>
import('./(main)/image/_layout/Mobile').then((m) => ({
Component: m.default,
})),
element: <ImageLayoutMobile />,
errorElement: <ImageErrorBoundary />,
path: 'image',
},
// Labs routes
{
lazy: () =>
import('./(main)/labs').then((m) => ({
Component: m.default,
})),
element: <LabsPage />,
path: 'labs',
},
@@ -272,38 +443,24 @@ export const createMobileRouter = (locale: Locales) =>
{
children: [
{
element: <ProfileHomePage />,
index: true,
lazy: () =>
import('./(main)/profile/(home)').then((m) => ({
Component: m.default,
})),
},
{
lazy: () =>
import('./(main)/profile/apikey/index').then((m) => ({
Component: m.default,
})),
element: <ProfileApikeyPage />,
path: 'apikey',
},
{
lazy: () =>
import('./(main)/profile/security').then((m) => ({
Component: m.MobileProfileSecurityPage,
})),
element: <MobileProfileSecurityPage />,
path: 'security',
},
{
lazy: () =>
import('./(main)/profile/stats').then((m) => ({
Component: m.MobileProfileStatsPage,
})),
element: <MobileProfileStatsPage />,
path: 'stats',
},
],
lazy: () =>
import('./(main)/profile/_layout/Mobile').then((m) => ({
Component: m.default,
})),
element: <ProfileLayoutMobile />,
errorElement: <ProfileErrorBoundary />,
path: 'profile',
},
@@ -313,49 +470,32 @@ export const createMobileRouter = (locale: Locales) =>
{
children: [
{
element: <MeHomePage />,
index: true,
lazy: () =>
import('./(main)/(mobile)/me/(home)').then((m) => ({
Component: m.default,
})),
},
],
lazy: () =>
import('./(main)/(mobile)/me/(home)/layout').then((m) => ({
Component: m.default,
})),
element: <MeHomeLayout />,
},
{
children: [
{
lazy: () =>
import('./(main)/(mobile)/me/profile').then((m) => ({
Component: m.default,
})),
element: <MeProfilePage />,
path: 'profile',
},
],
lazy: () =>
import('./(main)/(mobile)/me/profile/layout').then((m) => ({
Component: m.default,
})),
element: <MeProfileLayout />,
},
{
children: [
{
lazy: () =>
import('./(main)/(mobile)/me/settings').then((m) => ({
Component: m.default,
})),
element: <MeSettingsPage />,
path: 'settings',
},
],
lazy: () =>
import('./(main)/(mobile)/me/settings/layout').then((m) => ({
Component: m.default,
})),
element: <MeSettingsLayout />,
},
],
errorElement: <MeErrorBoundary />,
path: 'me',
},
@@ -372,7 +512,7 @@ export const createMobileRouter = (locale: Locales) =>
},
],
element: <RootLayout locale={locale} />,
loader: hydrationGateLoader,
errorElement: <RootErrorBoundary />,
path: '/',
},
]);

View File

@@ -12,6 +12,9 @@ export default async (props: DynamicLayoutProps) => {
// Conditionally load and render based on device type
// Using native dynamic import ensures complete code splitting
// Mobile and Desktop bundles will be completely separate
console.log('isMobile', isMobile);
console.log('locale', locale);
if (isMobile) {
return <MobileRouter locale={locale} />;
}

View File

@@ -63,16 +63,7 @@ const StoreInitialization = memo(() => {
// init user state
useInitUserState(isLoginOnInit, serverConfig, {
onError: () => {
// 即使失败也要设置标志,避免应用卡住
useGlobalStore.setState({ isAppHydrated: true });
console.warn('[Hydration] Client state initialization failed.');
},
onSuccess: (state) => {
// 设置水合完成标志
useGlobalStore.setState({ isAppHydrated: true });
console.log('[Hydration] Client state initialized successfully.');
if (state.isOnboard === false) {
router.push('/onboard');
}

View File

@@ -122,12 +122,6 @@ export interface GlobalState {
* 启动时为 Idle完成为 Ready报错为 Error
*/
initClientDBStage: DatabaseLoadingState;
/**
* 应用水合状态标志
* 用于指示客户端状态是否已从 StoreInitialization 完成加载
* 默认为 falseStoreInitialization 完成后设置为 true
*/
isAppHydrated: boolean;
isMobile?: boolean;
isStatusInit?: boolean;
latestVersion?: string;
@@ -170,7 +164,6 @@ export const INITIAL_STATUS = {
export const initialState: GlobalState = {
initClientDBStage: DatabaseLoadingState.Idle,
isAppHydrated: false,
isMobile: false,
isStatusInit: false,
sidebarKey: SidebarTabKey.Chat,