perf(web): 路由级懒加载,修复失效 useMemo 与 HMR 全页刷新
- 11 个页面改为 React.lazy;Login 与 MainLayout 保持同步加载(未登录用户与 登录后首帧的必经路径);新增统一占位 RouteFallback,替换权限校验处重复的 div 首屏静态资源 gzip 497.7 kB → 268.4 kB(-46.1%),>500 kB 体积警告消失 - PlatformUsers 的 columns/memberColumns/merchantColumns 每次渲染都重建, 使依赖它们的 items 的 useMemo 完全失效;三个回调 useCallback 化、 三个列定义 useMemo 化,让 memo 真正命中 - useAuth/AuthContext 拆到 store/authContext.ts,使 store/auth.tsx 只导出 AuthProvider 组件,消除 react(only-export-components) 导致的 HMR 整页刷新 (同步更新 7 处引用) - MainLayout 未使用的 catch 参数清理 oxlint:5 warnings → 0 warnings / 0 errors。
This commit is contained in:
+28
-14
@@ -1,21 +1,33 @@
|
|||||||
|
import { Suspense, lazy } from 'react'
|
||||||
import { BrowserRouter, Navigate, Route, Routes, useLocation } from 'react-router-dom'
|
import { BrowserRouter, Navigate, Route, Routes, useLocation } from 'react-router-dom'
|
||||||
import { ConfigProvider, App as AntdApp, Spin } from 'antd'
|
import { ConfigProvider, App as AntdApp, Spin } from 'antd'
|
||||||
import zhCN from 'antd/locale/zh_CN'
|
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 MainLayout from './layouts/MainLayout'
|
||||||
import Login from './pages/Login'
|
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'
|
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 <div style={{ textAlign: 'center', padding: '100px 0' }}><Spin size="large" /></div>
|
||||||
|
}
|
||||||
|
|
||||||
function PrivateRoute({ children }: { children: ReactNode }) {
|
function PrivateRoute({ children }: { children: ReactNode }) {
|
||||||
const { token } = useAuth()
|
const { token } = useAuth()
|
||||||
if (!token) return <Navigate to="/login" replace />
|
if (!token) return <Navigate to="/login" replace />
|
||||||
@@ -41,7 +53,7 @@ function MerchantPermissionRoute({ permission, children }: { permission?: string
|
|||||||
const { isAdmin, merchantLoading, merchantPermissions } = useAuth()
|
const { isAdmin, merchantLoading, merchantPermissions } = useAuth()
|
||||||
if (isAdmin || !permission) return <>{children}</>
|
if (isAdmin || !permission) return <>{children}</>
|
||||||
if (merchantLoading) {
|
if (merchantLoading) {
|
||||||
return <div style={{ textAlign: 'center', padding: '100px 0' }}><Spin size="large" /></div>
|
return <RouteFallback />
|
||||||
}
|
}
|
||||||
return merchantPermissions.includes(permission) ? <>{children}</> : <Navigate to="/" replace />
|
return merchantPermissions.includes(permission) ? <>{children}</> : <Navigate to="/" replace />
|
||||||
}
|
}
|
||||||
@@ -158,7 +170,9 @@ export default function App() {
|
|||||||
<AntdApp>
|
<AntdApp>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AppRoutes />
|
<Suspense fallback={<RouteFallback />}>
|
||||||
|
<AppRoutes />
|
||||||
|
</Suspense>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</AntdApp>
|
</AntdApp>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
UserOutlined,
|
UserOutlined,
|
||||||
WalletOutlined,
|
WalletOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { useAuth } from '../store/auth'
|
import { useAuth } from '../store/authContext'
|
||||||
import { authApi } from '../api'
|
import { authApi } from '../api'
|
||||||
import type { MenuProps } from 'antd'
|
import type { MenuProps } from 'antd'
|
||||||
|
|
||||||
@@ -215,8 +215,8 @@ function loadStoredOpenKeys(sections: SidebarSection[], selectedKey: string): st
|
|||||||
return Array.from(new Set([...parsed, ...initial]))
|
return Array.from(new Set([...parsed, ...initial]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
// ignore
|
// localStorage 内容损坏时回退到默认展开状态
|
||||||
}
|
}
|
||||||
return initial
|
return initial
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import { dashboardApi } from '../api'
|
import { dashboardApi } from '../api'
|
||||||
import type { DashboardStats } from '../types'
|
import type { DashboardStats } from '../types'
|
||||||
import { PageHeader } from '../components/PageHeader'
|
import { PageHeader } from '../components/PageHeader'
|
||||||
import { useAuth } from '../store/auth'
|
import { useAuth } from '../store/authContext'
|
||||||
|
|
||||||
const numberText = (value?: number) => (value ?? 0).toLocaleString('zh-CN')
|
const numberText = (value?: number) => (value ?? 0).toLocaleString('zh-CN')
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState } from 'react'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Form, Input, Button, Typography, message, Space } from 'antd'
|
import { Form, Input, Button, Typography, message, Space } from 'antd'
|
||||||
import { UserOutlined, LockOutlined, RightOutlined } from '@ant-design/icons'
|
import { UserOutlined, LockOutlined, RightOutlined } from '@ant-design/icons'
|
||||||
import { useAuth } from '../store/auth'
|
import { useAuth } from '../store/authContext'
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const { login } = useAuth()
|
const { login } = useAuth()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
message,
|
message,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import { ReloadOutlined } from '@ant-design/icons'
|
import { ReloadOutlined } from '@ant-design/icons'
|
||||||
import { useAuth } from '../store/auth'
|
import { useAuth } from '../store/authContext'
|
||||||
import { merchantApi } from '../api'
|
import { merchantApi } from '../api'
|
||||||
import { PageHeader } from '../components/PageHeader'
|
import { PageHeader } from '../components/PageHeader'
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import type { ColumnsType } from 'antd/es/table'
|
|||||||
import { PageHeader } from '../components/PageHeader'
|
import { PageHeader } from '../components/PageHeader'
|
||||||
import ImageUploader from '../components/ImageUploader'
|
import ImageUploader from '../components/ImageUploader'
|
||||||
import { merchantApi, platformApi } from '../api'
|
import { merchantApi, platformApi } from '../api'
|
||||||
import { useAuth } from '../store/auth'
|
import { useAuth } from '../store/authContext'
|
||||||
import type { PageResult, RechargeApplication } from '../types'
|
import type { PageResult, RechargeApplication } from '../types'
|
||||||
import { formatDateTime } from '../utils/time'
|
import { formatDateTime } from '../utils/time'
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { platformApi, userApi } from '../api'
|
|||||||
import { PageHeader } from '../components/PageHeader'
|
import { PageHeader } from '../components/PageHeader'
|
||||||
import type { MerchantMember, MerchantMemberGroup, PageResult, User } from '../types'
|
import type { MerchantMember, MerchantMemberGroup, PageResult, User } from '../types'
|
||||||
import { formatDateTime } from '../utils/time'
|
import { formatDateTime } from '../utils/time'
|
||||||
import { useAuth } from '../store/auth'
|
import { useAuth } from '../store/authContext'
|
||||||
import { roleText } from './merchantCenterUtils'
|
import { roleText } from './merchantCenterUtils'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
@@ -62,11 +62,11 @@ export default function PlatformUsers() {
|
|||||||
setModalOpen(true)
|
setModalOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openEdit = (record: User) => {
|
const openEdit = useCallback((record: User) => {
|
||||||
setEditingAdmin(record)
|
setEditingAdmin(record)
|
||||||
form.setFieldsValue({ username: record.username, nickname: record.nickname, password: '', status: record.status === 1 })
|
form.setFieldsValue({ username: record.username, nickname: record.nickname, password: '', status: record.status === 1 })
|
||||||
setModalOpen(true)
|
setModalOpen(true)
|
||||||
}
|
}, [form])
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
const values = await form.validateFields()
|
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 {
|
try {
|
||||||
await userApi.remove(record.id)
|
await userApi.remove(record.id)
|
||||||
message.success('平台管理员已删除')
|
message.success('平台管理员已删除')
|
||||||
@@ -102,9 +102,9 @@ export default function PlatformUsers() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
message.error(e instanceof Error ? e.message : '删除失败')
|
message.error(e instanceof Error ? e.message : '删除失败')
|
||||||
}
|
}
|
||||||
}
|
}, [loadAdmins])
|
||||||
|
|
||||||
const removeMerchantAccount = async (record: MerchantMember) => {
|
const removeMerchantAccount = useCallback(async (record: MerchantMember) => {
|
||||||
try {
|
try {
|
||||||
await platformApi.deleteMerchantAccount(record.user_id)
|
await platformApi.deleteMerchantAccount(record.user_id)
|
||||||
message.success('商户账号已删除')
|
message.success('商户账号已删除')
|
||||||
@@ -112,9 +112,9 @@ export default function PlatformUsers() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
message.error(e instanceof Error ? e.message : '删除失败')
|
message.error(e instanceof Error ? e.message : '删除失败')
|
||||||
}
|
}
|
||||||
}
|
}, [loadMerchantAccounts])
|
||||||
|
|
||||||
const columns: ColumnsType<User> = [
|
const columns: ColumnsType<User> = useMemo(() => [
|
||||||
{ title: '用户名', dataIndex: 'username', width: 260, render: (value) => <Typography.Text strong>{value}</Typography.Text> },
|
{ title: '用户名', dataIndex: 'username', width: 260, render: (value) => <Typography.Text strong>{value}</Typography.Text> },
|
||||||
{ title: '昵称', dataIndex: 'nickname', width: 220, render: (value) => value || '-' },
|
{ title: '昵称', dataIndex: 'nickname', width: 220, render: (value) => value || '-' },
|
||||||
{ title: '状态', dataIndex: 'status', width: 120, render: (value) => value === 1 ? <Tag color="green">启用</Tag> : <Tag>停用</Tag> },
|
{ title: '状态', dataIndex: 'status', width: 120, render: (value) => value === 1 ? <Tag color="green">启用</Tag> : <Tag>停用</Tag> },
|
||||||
@@ -131,9 +131,9 @@ export default function PlatformUsers() {
|
|||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]
|
], [currentUser?.id, openEdit, remove])
|
||||||
|
|
||||||
const memberColumns: ColumnsType<MerchantMember> = [
|
const memberColumns: ColumnsType<MerchantMember> = useMemo(() => [
|
||||||
{ title: '员工账号', dataIndex: ['user', 'username'], width: 260, render: (_, record) => <Typography.Text strong>{record.user?.username || `#${record.user_id}`}</Typography.Text> },
|
{ title: '员工账号', dataIndex: ['user', 'username'], width: 260, render: (_, record) => <Typography.Text strong>{record.user?.username || `#${record.user_id}`}</Typography.Text> },
|
||||||
{ title: '昵称', dataIndex: ['user', 'nickname'], width: 220, render: (_, record) => record.user?.nickname || '-' },
|
{ title: '昵称', dataIndex: ['user', 'nickname'], width: 220, render: (_, record) => record.user?.nickname || '-' },
|
||||||
{ title: '商户角色', dataIndex: 'role', width: 160, render: (role) => <Tag color="blue">{roleText(role)}</Tag> },
|
{ title: '商户角色', dataIndex: 'role', width: 160, render: (role) => <Tag color="blue">{roleText(role)}</Tag> },
|
||||||
@@ -150,9 +150,9 @@ export default function PlatformUsers() {
|
|||||||
<Button type="link" danger size="small" icon={<DeleteOutlined />}>删除账号</Button>
|
<Button type="link" danger size="small" icon={<DeleteOutlined />}>删除账号</Button>
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
},
|
},
|
||||||
]
|
], [removeMerchantAccount])
|
||||||
|
|
||||||
const merchantColumns: ColumnsType<MerchantMemberGroup> = [
|
const merchantColumns: ColumnsType<MerchantMemberGroup> = useMemo(() => [
|
||||||
{
|
{
|
||||||
title: '商户',
|
title: '商户',
|
||||||
width: 320,
|
width: 320,
|
||||||
@@ -174,7 +174,7 @@ export default function PlatformUsers() {
|
|||||||
navigate('/merchant-members')
|
navigate('/merchant-members')
|
||||||
}}>管理商户成员</Button>,
|
}}>管理商户成员</Button>,
|
||||||
},
|
},
|
||||||
]
|
], [navigate])
|
||||||
|
|
||||||
const items = useMemo(() => [
|
const items = useMemo(() => [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
createContext,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
@@ -9,22 +7,7 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import type { Merchant, User } from '../types'
|
import type { Merchant, User } from '../types'
|
||||||
import { authApi, merchantApi } from '../api'
|
import { authApi, merchantApi } from '../api'
|
||||||
|
import { AuthContext } from './authContext'
|
||||||
interface AuthState {
|
|
||||||
token: string | null
|
|
||||||
user: User | null
|
|
||||||
login: (username: string, password: string) => Promise<void>
|
|
||||||
logout: () => void
|
|
||||||
refreshProfile: () => Promise<void>
|
|
||||||
merchant: Merchant | null
|
|
||||||
merchantRole: string | null
|
|
||||||
merchantPermissions: string[]
|
|
||||||
merchantLoading: boolean
|
|
||||||
refreshMerchantContext: () => Promise<void>
|
|
||||||
isAdmin: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const AuthContext = createContext<AuthState | null>(null)
|
|
||||||
|
|
||||||
function loadUser(): User | null {
|
function loadUser(): User | null {
|
||||||
try {
|
try {
|
||||||
@@ -122,9 +105,3 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAuth() {
|
|
||||||
const ctx = useContext(AuthContext)
|
|
||||||
if (!ctx) throw new Error('useAuth must be used within AuthProvider')
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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<void>
|
||||||
|
logout: () => void
|
||||||
|
refreshProfile: () => Promise<void>
|
||||||
|
merchant: Merchant | null
|
||||||
|
merchantRole: string | null
|
||||||
|
merchantPermissions: string[]
|
||||||
|
merchantLoading: boolean
|
||||||
|
refreshMerchantContext: () => Promise<void>
|
||||||
|
isAdmin: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context 与 useAuth 单独放在本文件:auth.tsx 因此只导出 AuthProvider 组件,
|
||||||
|
// 否则 react(only-export-components) 会让该模块的 HMR 退化成整页刷新。
|
||||||
|
export const AuthContext = createContext<AuthState | null>(null)
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const ctx = useContext(AuthContext)
|
||||||
|
if (!ctx) throw new Error('useAuth must be used within AuthProvider')
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user