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 +}