基于 Taro 4 + Nest.js 的帝国政治模拟系统,实现议会投票、政党系统、内阁管理、军部帷幄、派阀管理、舆论监控、经济报表等完整功能。
| 文档 | 说明 | 适用场景 |
|---|---|---|
| 快速入门指南 | 5分钟快速了解项目并开始开发 | 新成员上手、快速查询 |
| 完整项目指南 | 项目的所有详细信息 | 深入了解、开发参考 |
| 设计指南 | 完整的设计规范和组件说明 | UI/UX 设计、开发参考 |
| 数据库设计文档 | 云数据库集合设计 | 数据库开发、数据管理 |
| 项目结构文档 | 项目目录结构说明 | 了解代码组织 |
| 性能优化文档 | 性能优化方案 | 性能调优、代码优化 |
| 调试期服务器部署 | Ubuntu 服务器调试部署步骤 | 解绑托管后接自己的服务器 |
| Bug 记录文档 | Bug 记录与预防清单 | 编码前必读、问题排查 |
| 清理总结文档 | 项目清理的详细内容 | 了解清理过程 |
| 文档更新日志 | 文档更新历史记录 | 了解项目变更 |
问题描述: 页面快速滑动或切换时出现短暂的白色闪烁,降低美观度且不流畅。
修复方案:
- 在全局配置中添加深色背景颜色
#0d1442 - 优化 ImperialBackground 组件,添加外层包装器确保背景立即可见
- 添加全局页面容器样式,确保页面切换时背景始终是深色
- 统一所有 19 个页面的配置,设置一致的背景颜色
预期效果:
- ✅ 页面切换时不再出现白色闪烁
- ✅ 页面背景始终是深蓝色渐变
- ✅ 页面切换更加流畅
- ✅ 用户体验得到显著提升
详见:文档更新日志
- 整体框架: Taro 4.1.9
- 语言: TypeScript 5.4.5
- 渲染: React 18.0.0
- 样式: TailwindCSS 4.1.18
- Tailwind 适配层: weapp-tailwindcss 4.9.2
- 状态管理: Zustand 5.0.9
- 图标库: lucide-react-taro latest
- 工程化: Vite 4.2.0
- 包管理: pnpm
- 运行时: Node.js >= 18
- 服务端: NestJS 10.4.15
- 数据库 ORM: Drizzle ORM 0.45.1
- 类型校验: Zod 4.3.5
├── .cozeproj/ # Coze 平台配置
│ └── scripts/ # 构建和运行脚本
├── config/ # Taro 构建配置
│ ├── index.ts # 主配置文件
│ ├── dev.ts # 开发环境配置
│ └── prod.ts # 生产环境配置
├── server/ # NestJS 后端服务
│ └── src/
│ ├── main.ts # 服务入口
│ ├── app.module.ts # 根模块
│ ├── app.controller.ts # 应用控制器
│ └── app.service.ts # 应用服务
├── src/ # 前端源码
│ ├── pages/ # 页面组件
│ ├── presets/ # 框架预置逻辑(无需读取,如无必要不改动)
│ ├── utils/ # 工具函数
│ ├── network.ts # 封装好的网络请求工具
│ ├── app.ts # 应用入口
│ ├── app.config.ts # 应用配置
│ └── app.css # 全局样式
├── types/ # TypeScript 类型定义
├── key/ # 小程序密钥(CI 上传用)
├── .env.local # 环境变量
└── project.config.json # 微信小程序项目配置
pnpm install同时启动 H5 前端和 NestJS 后端:
pnpm dev- 前端地址:http://localhost:5000
- 后端地址:http://localhost:3000
单独启动:
pnpm dev:web # 仅 H5 前端
pnpm dev:weapp # 仅微信小程序
pnpm dev:server # 仅后端服务pnpm build # 构建所有(H5 + 小程序 + 后端)
pnpm build:web # 仅构建 H5,输出到 dist-web
pnpm build:weapp # 仅构建微信小程序,输出到 dist
pnpm build:server # 仅构建后端pnpm preview:weapp # 构建并生成预览小程序二维码命令行预览前请先完成以下配置:
- 复制
.env.example为.env.local - 确认
.env.local中的TARO_APP_WEAPP_APPID与project.config.json中的appid一致 - 将微信后台导出的代码上传私钥保存到
key/private.appid.key - 在微信小程序后台的「开发与服务 -> 开发管理 -> 小程序代码上传」中关闭
IP白名单,或将当前机器出口 IP 加入白名单
预览成功后,二维码图片默认输出到 dist/preview.jpg。
如果当前是在自己的 Ubuntu 服务器上调试 Nest 后端,先把 .env.local 里的
PROJECT_DOMAIN 指到你的实际调试入口,例如:
PROJECT_DOMAIN=http://124.220.233.126:3000说明:
124.220.233.126:5000是旧网关口径,不应再继续当作本项目的 Nest API 入口。- 服务器拉起、systemd 挂载、端口放行与域名切换步骤,统一看 调试期服务器部署。
- 域名审批通过后,记得把
PROJECT_DOMAIN改回你自己的 HTTPS 域名,再重新构建或预览小程序。
- 在 `src/pages/` 下创建页面目录
- 创建 `index.tsx`(页面组件)
- 创建 `index.config.ts`(页面配置)
- 创建 `index.css`(页面样式,可选)
- 在 `src/app.config.ts` 的 `pages` 数组中注册页面路径
或使用 Taro 脚手架命令:
pnpm new # 交互式创建页面/组件引入方式
import { Text } from '@tarojs/components'- 基础组件
- Text
- Icon
- Progress
- RichText
- 表单组件
- Button
- Checkbox
- CheckboxGroup
- Editor
- Form
- Input
- Label
- Picker
- PickerView
- PickerViewColumn
- Radio
- RadioGroup
- Slider
- Switch
- Textarea
- 导航组件
- FunctionalPageNavigator
- NavigationBar
- Navigator
- TabItem
- Tabs
- 媒体组件
- Camera
- Image
- Video
- 视图容器
- ScrollView
- Swiper
- SwiperItem
- View
项目配置了 @/* 路径别名指向 src/*:
import { SomeComponent } from '@/components/SomeComponent'
import { useUserStore } from '@/stores/user'// src/pages/example/index.tsx
import { View, Text } from '@tarojs/components'
import { useLoad, useDidShow } from '@tarojs/taro'
import type { FC } from 'react'
import './index.css'
const ExamplePage: FC = () => {
useLoad(() => {
console.log('Page loaded.')
})
useDidShow(() => {
console.log('Page showed.')
})
return (
<View className="flex flex-col items-center p-4">
<Text className="text-lg font-bold">Hello Taro!</Text>
</View>
)
}
export default ExamplePage// src/pages/example/index.config.ts
import { definePageConfig } from '@tarojs/taro'
export default definePageConfig({
navigationBarTitleText: '示例页面',
enablePullDownRefresh: true,
backgroundTextStyle: 'dark',
})// src/app.config.ts
import { defineAppConfig } from '@tarojs/taro'
export default defineAppConfig({
pages: [
'pages/index/index',
'pages/example/index',
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'App',
navigationBarTextStyle: 'black',
},
// TabBar 配置 (可选)
// tabBar: {
// list: [
// { pagePath: 'pages/index/index', text: '首页' },
// ],
// },
})IMPORTANT: 禁止直接使用 Taro.request、Taro.uploadFile、Taro.downloadFile,使用 Network.request、Network.uploadFile、Network.downloadFile 替代。
Network 是对 Taro.request、Taro.uploadFile、Taro.downloadFile 的封装,自动添加项目域名前缀,参数与 Taro 一致。
✅ 正确使用方式
import { Network } from '@/network'
// GET 请求
const data = await Network.request({
url: '/api/hello'
})
// POST 请求
const result = await Network.request({
url: '/api/user/login',
method: 'POST',
data: { username, password }
})
// 文件上传
await Network.uploadFile({
url: '/api/upload',
filePath: tempFilePath,
name: 'file'
})
// 文件下载
await Network.downloadFile({
url: '/api/download/file.pdf'
})❌ 错误用法
import Taro from '@tarojs/taro'
// ❌ 会导致自动域名拼接无法生效,除非是特殊指定域名
const data = await Network.request({
url: 'http://localhost/api/hello'
})
// ❌ 不要直接使用 Taro.request
await Taro.request({ url: '/api/hello' })
// ❌ 不要直接使用 Taro.uploadFile
await Taro.uploadFile({ url: '/api/upload', filePath, name: 'file' })// src/stores/user.ts
import { create } from 'zustand'
interface UserState {
userInfo: UserInfo | null
token: string
setUserInfo: (info: UserInfo) => void
setToken: (token: string) => void
logout: () => void
}
interface UserInfo {
id: string
name: string
avatar: string
}
export const useUserStore = create<UserState>((set) => ({
userInfo: null,
token: '',
setUserInfo: (info) => set({ userInfo: info }),
setToken: (token) => set({ token }),
logout: () => set({ userInfo: null, token: '' }),
}))import {
useLoad, // 页面加载 (onLoad)
useReady, // 页面初次渲染完成 (onReady)
useDidShow, // 页面显示 (onShow)
useDidHide, // 页面隐藏 (onHide)
usePullDownRefresh, // 下拉刷新 (onPullDownRefresh)
useReachBottom, // 触底加载 (onReachBottom)
useShareAppMessage, // 分享 (onShareAppMessage)
useRouter, // 获取路由参数
} from '@tarojs/taro'import Taro from '@tarojs/taro'
// 保留当前页面,跳转到新页面
Taro.navigateTo({ url: '/pages/detail/index?id=1' })
// 关闭当前页面,跳转到新页面
Taro.redirectTo({ url: '/pages/detail/index' })
// 跳转到 tabBar 页面
Taro.switchTab({ url: '/pages/index/index' })
// 返回上一页
Taro.navigateBack({ delta: 1 })
// 获取路由参数
const router = useRouter()
const { id } = router.paramsIMPORTANT: 禁止使用 lucide-react,必须使用 lucide-react-taro 替代。
lucide-react-taro 是 Lucide 图标库的 Taro 适配版本,专为小程序环境优化,API 与 lucide-react 一致:
import { View } from '@tarojs/components'
import { House, Settings, User, Search, Camera, Zap } from 'lucide-react-taro'
const IconDemo = () => {
return (
<View className="flex gap-4">
{/* 基本用法 */}
<House />
{/* 自定义尺寸和颜色 */}
<Settings size={32} color="#1890ff" />
{/* 自定义描边宽度 */}
<User size={24} strokeWidth={1.5} />
{/* 绝对描边宽度(描边不随 size 缩放) */}
<Camera size={48} strokeWidth={2} absoluteStrokeWidth />
{/* 组合使用 */}
<Zap size={32} color="#ff6b00" strokeWidth={1.5} className="my-icon" />
</View>
)
}常用属性:
size- 图标大小(默认 24)color- 图标颜色(默认 currentColor,小程序中建议显式设置)strokeWidth- 线条粗细(默认 2)absoluteStrokeWidth- 绝对描边宽度,启用后描边不随 size 缩放className/style- 自定义样式
更多图标请访问:https://lucide.dev/icons
IMPORTANT: 微信小程序的 TabBar 不支持 base64 或 SVG 图片,必须使用本地 PNG 文件。
lucide-react-taro 提供了 CLI 工具来生成 TabBar 所需的 PNG 图标:
# 生成带选中状态的图标
npx taro-lucide-tabbar House Settings User -c "#999999" -a "#1890ff"
# 指定输出目录和尺寸
npx taro-lucide-tabbar House Settings User -c "#999999" -a "#1890ff" -o ./src/assets/tabbar -s 81CLI 参数:
--color, -c(默认 #000000): 图标颜色--active-color, -a: 选中状态颜色--size, -s(默认 81): 图标尺寸--output, -o(默认 ./tabbar-icons): 输出目录--stroke-width(默认 2): 描边宽度
在 app.config.ts 中使用生成的图标:
IMPORTANT:iconPath 和 selectedIconPath 必须以
./开头,否则图标无法渲染
export default defineAppConfig({
tabBar: {
color: '#999999',
selectedColor: '#1890ff',
backgroundColor: '#ffffff',
borderStyle: 'black',
list: [
{
pagePath: 'pages/index/index',
text: '首页',
iconPath: './assets/tabbar/house.png',
selectedIconPath: './assets/tabbar/house-active.png',
},
{
pagePath: 'pages/settings/index',
text: '设置',
iconPath: './assets/tabbar/settings.png',
selectedIconPath: './assets/tabbar/settings-active.png',
},
{
pagePath: 'pages/user/index',
text: '用户',
iconPath: './assets/tabbar/user.png',
selectedIconPath: './assets/tabbar/user-active.png',
},
],
},
})
### Tailwind CSS 样式开发
IMPORTANT:必须使用 tailwindcss 实现样式,只有在必要情况下才能 fallback 到 css / less
> 项目已集成 Tailwind CSS 4.x + weapp-tailwindcss,支持跨端原子化样式:
```tsx
<View className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
<Text className="text-2xl font-bold text-blue-600 mb-4">标题</Text>
<View className="w-full px-4">
<Button className="w-full bg-blue-500 text-white rounded-lg py-3">
按钮
</Button>
</View>
</View>import { Image } from '@tarojs/components'
<Image src={imageUrl} lazyLoad mode="aspectFill" />import { VirtualList } from '@tarojs/components'
<VirtualList
height={500}
itemData={list}
itemCount={list.length}
itemSize={100}
renderItem={({ index, style, data }) => (
<View style={style}>{data[index].name}</View>
)}
/>// src/app.config.ts
export default defineAppConfig({
pages: ['pages/index/index'],
subPackages: [
{
root: 'packageA',
pages: ['pages/detail/index'],
},
],
})| 限制项 | 说明 |
|---|---|
| 主包体积 | ≤ 2MB |
| 总包体积 | ≤ 20MB |
| 域名配置 | 生产环境需在小程序后台配置合法域名 |
| 本地开发 | 需在微信开发者工具开启「不校验合法域名」 |
// src/app.config.ts
export default defineAppConfig({
// ...其他配置
permission: {
'scope.userLocation': {
desc: '你的位置信息将用于小程序位置接口的效果展示'
}
},
requiredPrivateInfos: ['getLocation', 'chooseAddress']
})// 需先在 app.config.ts 中配置 permission
async function getLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
return await Taro.getLocation({ type: 'gcj02' })
}本项目后端基于 NestJS + TypeScript 构建,提供高效、可扩展的服务端能力。
.
├── server/ # NestJS 后端服务
│ └── src/
│ ├── main.ts # 服务入口
│ ├── app.module.ts # 根模块
│ ├── app.controller.ts # 根控制器
│ └── app.service.ts # 根服务pnpm dev:server // 启动开发服务 (热重载, 默认端口 3000)
pnpm build:server // 构建生产版本快速生成样板代码:
cd server
# 生成完整的 CRUD 资源 (包含 Module, Controller, Service, DTO, Entity)
npx nest g resource modules/product
# 仅生成特定部分
npx nest g module modules/order
npx nest g controller modules/order
npx nest g service modules/order在 server/ 根目录创建 .env 文件:
## 服务端口
PORT=3000
## 微信小程序配置
WX_APP_ID=你的AppID
WX_APP_SECRET=你的AppSecret
## JWT 密钥
JWT_SECRET=your-super-secret-key在代码中使用 @nestjs/config 读取环境变量:
import { ConfigService } from '@nestjs/config';
// 在 Service 中注入
constructor(private configService: ConfigService) {}
getWxConfig() {
return {
appId: this.configService.get<string>('WX_APP_ID'),
secret: this.configService.get<string>('WX_APP_SECRET'),
};
}建议使用拦截器 (Interceptor) 统一 API 响应格式:
// src/common/interceptors/transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
code: number;
data: T;
message: string;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(
map((data) => ({
code: 200,
data,
message: 'success',
})),
);
}
}在 main.ts 中全局注册:
app.useGlobalInterceptors(new TransformInterceptor());// src/modules/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { lastValueFrom } from 'rxjs';
@Injectable()
export class AuthService {
constructor(
private httpService: HttpService,
private configService: ConfigService,
) {}
async code2Session(code: string) {
const appId = this.configService.get('WX_APP_ID');
const secret = this.configService.get('WX_APP_SECRET');
const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${secret}&js_code=${code}&grant_type=authorization_code`;
const { data } = await lastValueFrom(this.httpService.get(url));
if (data.errcode) {
throw new UnauthorizedException(`微信登录失败: ${data.errmsg}`);
}
return data; // 包含 openid, session_key
}
}使用全局异常过滤器 (Filter) 统一错误响应:
// src/common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
response.status(status).json({
code: status,
message: typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse as any).message,
data: null,
});
}
}在 main.ts 中注册:
app.useGlobalFilters(new HttpExceptionFilter());
推荐使用 Drizzle ORM,已预安装。
项目集成了 Zod 用于运行时类型校验。
import { z } from 'zod';
// 基础类型
const userSchema = z.object({
id: z.number(),
name: z.string().min(1).max(50),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
// 从 schema 推导 TypeScript 类型
type User = z.infer<typeof userSchema>;// src/modules/user/dto/create-user.dto.ts
import { z } from 'zod';
export const createUserSchema = z.object({
nickname: z.string().min(1, '昵称不能为空').max(20, '昵称最多20个字符'),
avatar: z.string().url('头像必须是有效的URL').optional(),
phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确').optional(),
});
export type CreateUserDto = z.infer<typeof createUserSchema>;
// 在 Controller 中使用
@Post()
create(@Body() body: unknown) {
const result = createUserSchema.safeParse(body);
if (!result.success) {
throw new BadRequestException(result.error.errors);
}
return this.userService.create(result.data);
}