diff --git a/README.md b/README.md index 3c1e03f..72b3d36 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,14 @@ WhaleTown V2 后端是基于 NestJS 的多人小镇服务,包含 REST API、We - 世界聊天、私聊、玩家位置与外观实时同步。 - 商城、钱包、背包、房间家具和排行榜。 - 咖啡店陪伴助手、课程资源与 Zulip 集成。 -- 管理员登录、用户管理、操作日志和数据管理。 +- AI 小镇 NPC 日程、移动、交互与持久化运行时。 +- 邀请码注册、管理员用户管理、邀请码管理和运行日志。 - 可选的服务端角色皮肤生成流程。 ## 要求 - Node.js 20+ -- pnpm 9+ +- pnpm 9.15.4 - MySQL 和 Redis(生产环境) - Python 3(启用皮肤生成时) @@ -23,10 +24,13 @@ WhaleTown V2 后端是基于 NestJS 的多人小镇服务,包含 REST API、We ```bash pnpm install --frozen-lockfile cp .env.example .env +pnpm run db:migrate pnpm run build 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` 开始配置,不要直接使用示例值。 API 默认监听 `3000` 端口,Swagger 地址为 `/api-docs`。 @@ -34,10 +38,22 @@ API 默认监听 `3000` 端口,Swagger 地址为 `/api-docs`。 ## 管理端 ```bash +cp client/.env.example client/.env.local 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 +``` ## 部署 diff --git a/client/src/app/App.tsx b/client/src/app/App.tsx index 5b07d9e..bb567cb 100644 --- a/client/src/app/App.tsx +++ b/client/src/app/App.tsx @@ -1,29 +1,41 @@ -import { ConfigProvider } from 'antd'; +import { lazy, Suspense } from 'react'; +import { ConfigProvider, Spin } from 'antd'; 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'; +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 ( +