perf: lazy-load admin routes

This commit is contained in:
2026-09-08 22:59:03 +08:00
parent 513a3eba31
commit f8f6ee6a5e
2 changed files with 50 additions and 22 deletions

View File

@@ -8,13 +8,14 @@ WhaleTown V2 后端是基于 NestJS 的多人小镇服务,包含 REST API、We
- 世界聊天、私聊、玩家位置与外观实时同步。 - 世界聊天、私聊、玩家位置与外观实时同步。
- 商城、钱包、背包、房间家具和排行榜。 - 商城、钱包、背包、房间家具和排行榜。
- 咖啡店陪伴助手、课程资源与 Zulip 集成。 - 咖啡店陪伴助手、课程资源与 Zulip 集成。
- 管理员登录、用户管理、操作日志和数据管理 - AI 小镇 NPC 日程、移动、交互与持久化运行时
- 邀请码注册、管理员用户管理、邀请码管理和运行日志。
- 可选的服务端角色皮肤生成流程。 - 可选的服务端角色皮肤生成流程。
## 要求 ## 要求
- Node.js 20+ - Node.js 20+
- pnpm 9+ - pnpm 9.15.4
- MySQL 和 Redis生产环境 - MySQL 和 Redis生产环境
- Python 3启用皮肤生成时 - Python 3启用皮肤生成时
@@ -23,10 +24,13 @@ WhaleTown V2 后端是基于 NestJS 的多人小镇服务,包含 REST API、We
```bash ```bash
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
cp .env.example .env cp .env.example .env
pnpm run db:migrate
pnpm run build pnpm run build
pnpm run start:prod pnpm run start:prod
``` ```
首次启用邀请码功能时必须先执行 `pnpm run db:migrate`。该命令使用 `.env` 中的 MySQL 配置创建邀请码表。
启动前至少需要在 `.env` 中设置随机的 `JWT_SECRET``ADMIN_TOKEN_SECRET`。启用 Zulip 时还必须设置 `ZULIP_API_KEY_ENCRYPTION_KEY`;若 `ZULIP_DEGRADED_MODE_ENABLED=true`,可以不配置 Zulip 凭据和加密密钥,但 Zulip 集成及 API Key 加密存取功能将不可用。生产环境请从 `.env.production.example` 开始配置,不要直接使用示例值。 启动前至少需要在 `.env` 中设置随机的 `JWT_SECRET``ADMIN_TOKEN_SECRET`。启用 Zulip 时还必须设置 `ZULIP_API_KEY_ENCRYPTION_KEY`;若 `ZULIP_DEGRADED_MODE_ENABLED=true`,可以不配置 Zulip 凭据和加密密钥,但 Zulip 集成及 API Key 加密存取功能将不可用。生产环境请从 `.env.production.example` 开始配置,不要直接使用示例值。
API 默认监听 `3000` 端口Swagger 地址为 `/api-docs` API 默认监听 `3000` 端口Swagger 地址为 `/api-docs`
@@ -34,10 +38,22 @@ API 默认监听 `3000` 端口Swagger 地址为 `/api-docs`。
## 管理端 ## 管理端
```bash ```bash
cp client/.env.example client/.env.local
pnpm --filter whale-town-admin run build pnpm --filter whale-town-admin run build
``` ```
管理端的 API 地址通过 `client/.env.local` 中的 `VITE_API_BASE_URL` 配置。 管理端默认以 `/admin/` 为部署路径,并通过 `client/.env.local` 中的 `VITE_API_BASE_URL` 配置 API 地址。同域生产部署保持示例值 `/api`
页面按路由懒加载Vite 会在 `client/dist/assets` 中生成多个带哈希的 JS 文件。部署时应整体替换 `client/dist`,不要只上传 `index.html` 或单个 JS 文件。
## 验证
```bash
pnpm run build
pnpm --filter whale-town-admin run build
pnpm test
pnpm run test:world-npc
```
## 部署 ## 部署

View File

@@ -1,16 +1,27 @@
import { ConfigProvider } from 'antd'; import { lazy, Suspense } from 'react';
import { ConfigProvider, Spin } from 'antd';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { AdminLayout } from './AdminLayout';
import { LoginPage } from '../pages/LoginPage';
import { UsersPage } from '../pages/UsersPage';
import { LogsPage } from '../pages/LogsPage';
import { InvitationCodesPage } from '../pages/InvitationCodesPage';
import { isAuthed } from '../lib/adminAuth'; import { isAuthed } from '../lib/adminAuth';
const AdminLayout = lazy(() => import('./AdminLayout').then((module) => ({ default: module.AdminLayout })));
const LoginPage = lazy(() => import('../pages/LoginPage').then((module) => ({ default: module.LoginPage })));
const UsersPage = lazy(() => import('../pages/UsersPage').then((module) => ({ default: module.UsersPage })));
const LogsPage = lazy(() => import('../pages/LogsPage').then((module) => ({ default: module.LogsPage })));
const InvitationCodesPage = lazy(() => import('../pages/InvitationCodesPage').then((module) => ({ default: module.InvitationCodesPage })));
function RouteLoading() {
return (
<div style={{ minHeight: '100vh', display: 'grid', placeItems: 'center' }}>
<Spin size="large" />
</div>
);
}
export function App() { export function App() {
return ( return (
<ConfigProvider> <ConfigProvider>
<BrowserRouter basename={import.meta.env.BASE_URL}> <BrowserRouter basename={import.meta.env.BASE_URL}>
<Suspense fallback={<RouteLoading />}>
<Routes> <Routes>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route <Route
@@ -24,6 +35,7 @@ export function App() {
</Route> </Route>
<Route path="*" element={<Navigate to={isAuthed() ? '/users' : '/login'} replace />} /> <Route path="*" element={<Navigate to={isAuthed() ? '/users' : '/login'} replace />} />
</Routes> </Routes>
</Suspense>
</BrowserRouter> </BrowserRouter>
</ConfigProvider> </ConfigProvider>
); );