diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index d99f9e9..1c3e8c0 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,21 +1,33 @@
+import { Suspense, lazy } from 'react'
import { BrowserRouter, Navigate, Route, Routes, useLocation } from 'react-router-dom'
import { ConfigProvider, App as AntdApp, Spin } from 'antd'
import zhCN from 'antd/locale/zh_CN'
-import { AuthProvider, useAuth } from './store/auth'
+import { AuthProvider } from './store/auth'
+import { useAuth } from './store/authContext'
import MainLayout from './layouts/MainLayout'
import Login from './pages/Login'
-import Dashboard from './pages/Dashboard'
-import OpenApiDocs from './pages/OpenApiDocs'
-import ApiDebugger from './pages/ApiDebugger'
-import Delivery from './pages/Delivery'
-import MerchantCenter from './pages/MerchantCenter'
-import MerchantRecharge from './pages/MerchantRecharge'
-import AlertSettings from './pages/AlertSettings'
-import PlatformMerchants from './pages/PlatformMerchants'
-import PlatformProducts from './pages/PlatformProducts'
-import PlatformUsers from './pages/PlatformUsers'
-import PlatformRock from './pages/PlatformRock'
import type { ReactNode } from 'react'
+
+// 路由级懒加载:把原先 1.6MB 的首屏单包按页面拆分。
+// Login 与 MainLayout 保持同步加载——它们是未登录用户和登录后首帧的必经路径,
+// 懒加载只会凭空多一次往返。
+const Dashboard = lazy(() => import('./pages/Dashboard'))
+const OpenApiDocs = lazy(() => import('./pages/OpenApiDocs'))
+const ApiDebugger = lazy(() => import('./pages/ApiDebugger'))
+const Delivery = lazy(() => import('./pages/Delivery'))
+const MerchantCenter = lazy(() => import('./pages/MerchantCenter'))
+const MerchantRecharge = lazy(() => import('./pages/MerchantRecharge'))
+const AlertSettings = lazy(() => import('./pages/AlertSettings'))
+const PlatformMerchants = lazy(() => import('./pages/PlatformMerchants'))
+const PlatformProducts = lazy(() => import('./pages/PlatformProducts'))
+const PlatformUsers = lazy(() => import('./pages/PlatformUsers'))
+const PlatformRock = lazy(() => import('./pages/PlatformRock'))
+
+// RouteFallback 是懒加载页面与权限校验的统一占位,避免切换时白屏。
+function RouteFallback() {
+ return
+}
+
function PrivateRoute({ children }: { children: ReactNode }) {
const { token } = useAuth()
if (!token) return
@@ -41,7 +53,7 @@ function MerchantPermissionRoute({ permission, children }: { permission?: string
const { isAdmin, merchantLoading, merchantPermissions } = useAuth()
if (isAdmin || !permission) return <>{children}>
if (merchantLoading) {
- return
+ return
}
return merchantPermissions.includes(permission) ? <>{children}> :
}
@@ -158,7 +170,9 @@ export default function App() {
-
+ }>
+
+
diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx
index 3d5cfaa..263296f 100644
--- a/frontend/src/layouts/MainLayout.tsx
+++ b/frontend/src/layouts/MainLayout.tsx
@@ -31,7 +31,7 @@ import {
UserOutlined,
WalletOutlined,
} from '@ant-design/icons'
-import { useAuth } from '../store/auth'
+import { useAuth } from '../store/authContext'
import { authApi } from '../api'
import type { MenuProps } from 'antd'
@@ -215,8 +215,8 @@ function loadStoredOpenKeys(sections: SidebarSection[], selectedKey: string): st
return Array.from(new Set([...parsed, ...initial]))
}
}
- } catch (e) {
- // ignore
+ } catch {
+ // localStorage 内容损坏时回退到默认展开状态
}
return initial
}
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
index c7fa258..4168b81 100644
--- a/frontend/src/pages/Dashboard.tsx
+++ b/frontend/src/pages/Dashboard.tsx
@@ -19,7 +19,7 @@ import { useNavigate } from 'react-router-dom'
import { dashboardApi } from '../api'
import type { DashboardStats } from '../types'
import { PageHeader } from '../components/PageHeader'
-import { useAuth } from '../store/auth'
+import { useAuth } from '../store/authContext'
const numberText = (value?: number) => (value ?? 0).toLocaleString('zh-CN')
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx
index 58ab176..da2815e 100644
--- a/frontend/src/pages/Login.tsx
+++ b/frontend/src/pages/Login.tsx
@@ -2,7 +2,7 @@ import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Form, Input, Button, Typography, message, Space } from 'antd'
import { UserOutlined, LockOutlined, RightOutlined } from '@ant-design/icons'
-import { useAuth } from '../store/auth'
+import { useAuth } from '../store/authContext'
export default function Login() {
const { login } = useAuth()
diff --git a/frontend/src/pages/MerchantCenter.tsx b/frontend/src/pages/MerchantCenter.tsx
index ce00022..0b4b04e 100644
--- a/frontend/src/pages/MerchantCenter.tsx
+++ b/frontend/src/pages/MerchantCenter.tsx
@@ -13,7 +13,7 @@ import {
message,
} from 'antd'
import { ReloadOutlined } from '@ant-design/icons'
-import { useAuth } from '../store/auth'
+import { useAuth } from '../store/authContext'
import { merchantApi } from '../api'
import { PageHeader } from '../components/PageHeader'
import type {
diff --git a/frontend/src/pages/MerchantRecharge.tsx b/frontend/src/pages/MerchantRecharge.tsx
index b9681ca..8047ca5 100644
--- a/frontend/src/pages/MerchantRecharge.tsx
+++ b/frontend/src/pages/MerchantRecharge.tsx
@@ -28,7 +28,7 @@ import type { ColumnsType } from 'antd/es/table'
import { PageHeader } from '../components/PageHeader'
import ImageUploader from '../components/ImageUploader'
import { merchantApi, platformApi } from '../api'
-import { useAuth } from '../store/auth'
+import { useAuth } from '../store/authContext'
import type { PageResult, RechargeApplication } from '../types'
import { formatDateTime } from '../utils/time'
diff --git a/frontend/src/pages/PlatformUsers.tsx b/frontend/src/pages/PlatformUsers.tsx
index defd1d5..785d38e 100644
--- a/frontend/src/pages/PlatformUsers.tsx
+++ b/frontend/src/pages/PlatformUsers.tsx
@@ -6,7 +6,7 @@ import { platformApi, userApi } from '../api'
import { PageHeader } from '../components/PageHeader'
import type { MerchantMember, MerchantMemberGroup, PageResult, User } from '../types'
import { formatDateTime } from '../utils/time'
-import { useAuth } from '../store/auth'
+import { useAuth } from '../store/authContext'
import { roleText } from './merchantCenterUtils'
import { useNavigate } from 'react-router-dom'
@@ -62,11 +62,11 @@ export default function PlatformUsers() {
setModalOpen(true)
}
- const openEdit = (record: User) => {
+ const openEdit = useCallback((record: User) => {
setEditingAdmin(record)
form.setFieldsValue({ username: record.username, nickname: record.nickname, password: '', status: record.status === 1 })
setModalOpen(true)
- }
+ }, [form])
const submit = async () => {
const values = await form.validateFields()
@@ -94,7 +94,7 @@ export default function PlatformUsers() {
}
}
- const remove = async (record: User) => {
+ const remove = useCallback(async (record: User) => {
try {
await userApi.remove(record.id)
message.success('平台管理员已删除')
@@ -102,9 +102,9 @@ export default function PlatformUsers() {
} catch (e) {
message.error(e instanceof Error ? e.message : '删除失败')
}
- }
+ }, [loadAdmins])
- const removeMerchantAccount = async (record: MerchantMember) => {
+ const removeMerchantAccount = useCallback(async (record: MerchantMember) => {
try {
await platformApi.deleteMerchantAccount(record.user_id)
message.success('商户账号已删除')
@@ -112,9 +112,9 @@ export default function PlatformUsers() {
} catch (e) {
message.error(e instanceof Error ? e.message : '删除失败')
}
- }
+ }, [loadMerchantAccounts])
- const columns: ColumnsType = [
+ const columns: ColumnsType = useMemo(() => [
{ title: '用户名', dataIndex: 'username', width: 260, render: (value) => {value} },
{ title: '昵称', dataIndex: 'nickname', width: 220, render: (value) => value || '-' },
{ title: '状态', dataIndex: 'status', width: 120, render: (value) => value === 1 ? 启用 : 停用 },
@@ -131,9 +131,9 @@ export default function PlatformUsers() {
),
},
- ]
+ ], [currentUser?.id, openEdit, remove])
- const memberColumns: ColumnsType = [
+ const memberColumns: ColumnsType = useMemo(() => [
{ title: '员工账号', dataIndex: ['user', 'username'], width: 260, render: (_, record) => {record.user?.username || `#${record.user_id}`} },
{ title: '昵称', dataIndex: ['user', 'nickname'], width: 220, render: (_, record) => record.user?.nickname || '-' },
{ title: '商户角色', dataIndex: 'role', width: 160, render: (role) => {roleText(role)} },
@@ -150,9 +150,9 @@ export default function PlatformUsers() {
}>删除账号
,
},
- ]
+ ], [removeMerchantAccount])
- const merchantColumns: ColumnsType = [
+ const merchantColumns: ColumnsType = useMemo(() => [
{
title: '商户',
width: 320,
@@ -174,7 +174,7 @@ export default function PlatformUsers() {
navigate('/merchant-members')
}}>管理商户成员,
},
- ]
+ ], [navigate])
const items = useMemo(() => [
{
diff --git a/frontend/src/store/auth.tsx b/frontend/src/store/auth.tsx
index 17aa8b8..025c604 100644
--- a/frontend/src/store/auth.tsx
+++ b/frontend/src/store/auth.tsx
@@ -1,7 +1,5 @@
import {
- createContext,
useCallback,
- useContext,
useEffect,
useMemo,
useState,
@@ -9,22 +7,7 @@ import {
} from 'react'
import type { Merchant, User } from '../types'
import { authApi, merchantApi } from '../api'
-
-interface AuthState {
- token: string | null
- user: User | null
- login: (username: string, password: string) => Promise
- logout: () => void
- refreshProfile: () => Promise
- merchant: Merchant | null
- merchantRole: string | null
- merchantPermissions: string[]
- merchantLoading: boolean
- refreshMerchantContext: () => Promise
- isAdmin: boolean
-}
-
-const AuthContext = createContext(null)
+import { AuthContext } from './authContext'
function loadUser(): User | null {
try {
@@ -122,9 +105,3 @@ export function AuthProvider({ children }: { children: ReactNode }) {
return {children}
}
-
-export function useAuth() {
- const ctx = useContext(AuthContext)
- if (!ctx) throw new Error('useAuth must be used within AuthProvider')
- return ctx
-}
diff --git a/frontend/src/store/authContext.ts b/frontend/src/store/authContext.ts
new file mode 100644
index 0000000..216f197
--- /dev/null
+++ b/frontend/src/store/authContext.ts
@@ -0,0 +1,26 @@
+import { createContext, useContext } from 'react'
+import type { Merchant, User } from '../types'
+
+export interface AuthState {
+ token: string | null
+ user: User | null
+ login: (username: string, password: string) => Promise
+ logout: () => void
+ refreshProfile: () => Promise
+ merchant: Merchant | null
+ merchantRole: string | null
+ merchantPermissions: string[]
+ merchantLoading: boolean
+ refreshMerchantContext: () => Promise
+ isAdmin: boolean
+}
+
+// Context 与 useAuth 单独放在本文件:auth.tsx 因此只导出 AuthProvider 组件,
+// 否则 react(only-export-components) 会让该模块的 HMR 退化成整页刷新。
+export const AuthContext = createContext(null)
+
+export function useAuth() {
+ const ctx = useContext(AuthContext)
+ if (!ctx) throw new Error('useAuth must be used within AuthProvider')
+ return ctx
+}