5 files modified
2 files added
| | |
| | | |
| | | ### 4.2 注册路由 |
| | | |
| | | 在 `src/router/routeConfig.tsx` 中增加配置,并补充组件与图标的 import: |
| | | 在 `src/router/routeConfig.tsx` 中增加配置,并补充图标 import 与懒加载页面组件: |
| | | |
| | | ```tsx |
| | | // 顶部:import { lazy } from 'react'; |
| | | const Invoices = lazy(() => import('@/pages/business/invoices')); |
| | | |
| | | // appRoutes 中 |
| | | { |
| | | path: '/business/invoices', |
| | | title: '发票管理', |
| | |
| | | }, |
| | | ``` |
| | | |
| | | 放入现有分组时写在其 `children` 中;新增一级分组时保持与现有分组相同的结构。 |
| | | 页面组件必须用 `lazy(() => import(...))` 注册,不要静态 import,否则会把页面打进首屏 chunk。放入现有分组时写在其 `children` 中;新增一级分组时保持与现有分组相同的结构。 |
| | | |
| | | ### 4.3 更新文档 |
| | | |
| | |
| | | ``` |
| | | |
| | | - **路由与菜单**:`routeConfig.tsx` 是唯一数据源,配置 `path`、`title`、`icon`、`element` 后侧边栏与面包屑自动生效。支持 2-3 级嵌套;分组路由(无 `element`)通过 `<Outlet />` 渲染并默认跳转到第一个子页面;未匹配路由显示 404。 |
| | | - **路由懒加载**:页面组件统一通过 `lazy(() => import('@/pages/...'))` 注册,`BasicLayout` 的内容区已提供 `<Suspense>` 边界;新增页面不要用静态 import,避免所有页面打进入口 chunk。 |
| | | - **登录态**:`src/store/auth.ts` 持久化 `token` 与 `user`;`src/api/auth.ts` 调用 `POST /auth/tenant-login`(`{ phone, password, tenantCode }`)。未登录访问受保护页面跳转 `/login`,登录成功后返回原地址。 |
| | | - **布局与样式**:页面只负责内容区。全局 CSS 变量 `--app-primary`、`--app-bg`、`--app-border`、`--app-text`、`--app-text-secondary`;通用类 `.page-header`、`.page-toolbar`、`.filter-card`、`.table-card`、`.text-muted`;Ant Design 主题在 `main.tsx` 的 `ConfigProvider` 统一配置。 |
| | | |
| | |
| | | /new-page 根据 <接口文档路径>,添加一个【页面名】页面,放到【分组名】目录下 |
| | | ``` |
| | | |
| | | 仅改动现有页面(如调整某一列)等不涉及脚手架的场景才直接改代码。新页面必须在 `src/router/routeConfig.tsx` 用完整路径注册(`path`、`title`、`icon`、`element`,需要分组时使用 `children`),完成后执行 `npm run typecheck`。 |
| | | 仅改动现有页面(如调整某一列)等不涉及脚手架的场景才直接改代码。新页面必须在 `src/router/routeConfig.tsx` 用完整路径注册(`path`、`title`、`icon`、`element`,需要分组时使用 `children`),页面组件使用 `lazy(() => import(...))` 懒加载,完成后执行 `npm run typecheck`。 |
| | | |
| | | ## 五、接口与状态约定 |
| | | |
| | |
| | | |
| | | - 使用 BrowserRouter,生产环境需配置 SPA fallback:所有前端路由回退到 `index.html`;构建产物在 `dist/`。 |
| | | - 开发环境通过 Vite 代理 `/proxy-api` 转发到 `VITE_API_PROXY_TARGET`(未配置时取 `VITE_API_BASE_URL`),浏览器端不会跨域;生产环境需后端开启 CORS,或将前端与接口部署在同一域名下由反向代理转发。 |
| | | - 当前主包较大(Ant Design 全量依赖),后续可通过路由懒加载 / 手动分包优化。 |
| | | - 已启用路由懒加载与依赖拆包:`vite.config.ts` 的 `manualChunks` 将 React、antd 核心、其余依赖拆成长缓存 chunk,ProComponents 及业务页面依赖由 Rollup 按需拆分为异步 chunk。新增页面时保持懒加载即可,无需再调整拆包配置。 |
| New file |
| | |
| | | .page-loading { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 12px; |
| | | min-height: 320px; |
| | | padding: 48px 0; |
| | | |
| | | &__text { |
| | | color: var(--app-text-secondary); |
| | | font-size: 13px; |
| | | } |
| | | } |
| New file |
| | |
| | | import { Spin } from 'antd'; |
| | | import './PageLoading.scss'; |
| | | |
| | | /** 路由懒加载时的占位内容,尽量保持内容区高度,避免页面跳动。 */ |
| | | const PageLoading = () => ( |
| | | <div className="page-loading" role="status" aria-live="polite"> |
| | | <Spin size="large" /> |
| | | <span className="page-loading__text">页面加载中…</span> |
| | | </div> |
| | | ); |
| | | |
| | | export default PageLoading; |
| | |
| | | import { useEffect, useMemo, useState } from 'react'; |
| | | import { Suspense, useEffect, useMemo, useState } from 'react'; |
| | | import { |
| | | BellOutlined, |
| | | DownOutlined, |
| | |
| | | Tooltip, |
| | | } from 'antd'; |
| | | import { Outlet, useLocation, useNavigate } from 'react-router-dom'; |
| | | import PageLoading from '@/components/PageLoading'; |
| | | import { buildMenuItems, getOpenKeys } from '@/router/menu'; |
| | | import { appRoutes, findBreadcrumb } from '@/router/routeConfig'; |
| | | import { useAuthStore } from '@/store/auth'; |
| | |
| | | </Header> |
| | | |
| | | <Content className="app-content"> |
| | | <Outlet /> |
| | | {/* 路由懒加载边界放在内容区内,切页时侧边栏与头部保持不动 */} |
| | | <Suspense fallback={<PageLoading />}> |
| | | <Outlet /> |
| | | </Suspense> |
| | | </Content> |
| | | |
| | | <Footer className="app-footer"> |
| | |
| | | import type { ReactNode } from 'react'; |
| | | import { lazy, type ReactNode } from 'react'; |
| | | import { |
| | | ApartmentOutlined, |
| | | AppstoreOutlined, |
| | |
| | | TeamOutlined, |
| | | UnorderedListOutlined, |
| | | } from '@ant-design/icons'; |
| | | import Orders from '@/pages/business/orders'; |
| | | import CustomerList from '@/pages/customers/list'; |
| | | import Dashboard from '@/pages/dashboard'; |
| | | import AuditLogs from '@/pages/platform/audit-logs'; |
| | | import Departments from '@/pages/system/departments'; |
| | | import Roles from '@/pages/system/roles'; |
| | | import Users from '@/pages/system/users'; |
| | | |
| | | /** |
| | | * 页面组件统一按路由懒加载:首屏只加载布局与当前页面, |
| | | * 其余页面在切换菜单时按需加载,避免所有页面打进同一个入口 chunk。 |
| | | */ |
| | | const Orders = lazy(() => import('@/pages/business/orders')); |
| | | const CustomerList = lazy(() => import('@/pages/customers/list')); |
| | | const Dashboard = lazy(() => import('@/pages/dashboard')); |
| | | const AuditLogs = lazy(() => import('@/pages/platform/audit-logs')); |
| | | const Departments = lazy(() => import('@/pages/system/departments')); |
| | | const Roles = lazy(() => import('@/pages/system/roles')); |
| | | const Users = lazy(() => import('@/pages/system/users')); |
| | | |
| | | export interface AppRouteItem { |
| | | /** 完整路径,同时作为菜单 key */ |
| | |
| | | import react from '@vitejs/plugin-react'; |
| | | import { defineConfig, loadEnv } from 'vite'; |
| | | |
| | | /** |
| | | * 布局、登录等入口页面会用到的 antd 组件。 |
| | | * 这些模块首屏必载,单独放进 antd chunk 以稳定缓存; |
| | | * 表格、日期选择等只在业务页面使用的组件则交给 Rollup 按需分包。 |
| | | * |
| | | * 注意:不要匹配 `antd/es/index.js`(`from 'antd'` 的 barrel 文件)。 |
| | | * manualChunks 会把 chunk 当成入口保留全部导出,barrel 进手动 chunk 后 |
| | | * 会把所有 antd 组件都拉进首屏(约 1.3MB),失去按需分包的意义。 |
| | | */ |
| | | const ANTD_CORE_DIRS = [ |
| | | '_util', |
| | | 'app', |
| | | 'avatar', |
| | | 'breadcrumb', |
| | | 'button', |
| | | 'config-provider', |
| | | 'dropdown', |
| | | 'form', |
| | | 'input', |
| | | 'layout', |
| | | 'locale', |
| | | 'menu', |
| | | 'message', |
| | | 'modal', |
| | | 'notification', |
| | | 'space', |
| | | 'spin', |
| | | 'style', |
| | | 'tag', |
| | | 'theme', |
| | | 'tooltip', |
| | | 'version', |
| | | 'wave', |
| | | ].join('|'); |
| | | |
| | | /** 第三方依赖拆包规则,页面本身通过路由懒加载按需加载。 */ |
| | | const resolveVendorChunk = (id: string): string | undefined => { |
| | | if (!id.includes('node_modules')) return undefined; |
| | | |
| | | const path = id.replace(/\\/g, '/'); |
| | | |
| | | // 图标与 antd 互相引用,放在同一个 chunk,避免循环依赖。 |
| | | if ( |
| | | new RegExp( |
| | | `\\/node_modules\\/(antd\\/es\\/(${ANTD_CORE_DIRS})\\/|@ant-design\\/(cssinjs|cssinjs-utils|colors|fast-color|icons|icons-svg)\\/)`, |
| | | ).test(path) |
| | | ) { |
| | | return 'antd'; |
| | | } |
| | | |
| | | if ( |
| | | /\/node_modules\/(react|react-dom|scheduler|react-router|react-router-dom|@remix-run\/[^/]+)\//.test( |
| | | path, |
| | | ) |
| | | ) { |
| | | return 'react'; |
| | | } |
| | | |
| | | if (/\/node_modules\/(axios|zustand|dayjs)\//.test(path)) { |
| | | return 'vendor'; |
| | | } |
| | | |
| | | return undefined; |
| | | }; |
| | | |
| | | /** 开发环境通过 Vite 代理访问后端,避免浏览器跨域。 */ |
| | | export default defineConfig(({ mode }) => { |
| | | const env = loadEnv(mode, process.cwd(), ''); |
| | |
| | | }, |
| | | }, |
| | | }, |
| | | build: { |
| | | // 首屏 chunk 已控制在 600KB 以内;按需加载的 ProComponents 业务包约 830KB。 |
| | | chunkSizeWarningLimit: 900, |
| | | rollupOptions: { |
| | | output: { |
| | | manualChunks: resolveVendorChunk, |
| | | }, |
| | | }, |
| | | }, |
| | | }; |
| | | }); |