ProductReadyProductReady

项目结构

理解 ProductReady 的架构 - 代码在哪里,为什么这样组织

项目结构

ProductReady 遵循清晰、合理的结构。本指南帮助你理解代码的组织方式和添加新功能的位置。

刚接触 Next.js? 本项目遵循 Next.js 15+ App Router 约定,并增加了可扩展性的组织结构。


概览

apps/productready/
├── src/                    # 所有应用代码
│   ├── app/                # Next.js 页面与路由
│   ├── components/         # React 组件
│   ├── db/                 # 数据库 schema 与查询
│   ├── lib/                # 工具与配置
│   └── server/             # 服务端代码 (tRPC)
├── content/                # 文档 (MDX)
├── public/                 # 静态资源
├── spec/                   # 产品规格与设计文档
└── tests/                  # 测试文件

关键目录详解

src/app/ - Next.js 页面

这是你的页面和路由所在位置。Next.js 使用基于文件的路由。

文件夹结构 = URL 结构

  • app/(home)/page.tsx/ (首页)
  • app/dashboard/page.tsx/dashboard
  • app/dashboard/tasks/page.tsx/dashboard/tasks
  • app/api/health/route.ts/api/health (API 端点)

路由组: (home) - 括号不影响 URL,只用于组织

特殊文件

  • page.tsx - 实际页面组件
  • layout.tsx - 共享布局包装器
  • loading.tsx - 加载状态
  • error.tsx - 错误边界
  • route.ts - API 端点

新功能添加位置:

  • 新页面 → 创建文件夹并添加 page.tsx
  • API 端点 → 在 app/api/ 中创建 route.ts

Wouter 单页应用路由

ProductReady 使用 Wouter 在仪表盘页面内实现客户端 SPA(单页应用)路由。

架构模式

  • Next.js App Router 处理顶级路由并提供 SSR (/dashboard, /agent, /kadmin)
  • Wouter 处理每个页面内的子路由,实现无刷新的 SPA 导航

示例 (app/dashboard/page.tsx):

import { Route, Switch } from 'wouter';
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/app-sidebar';

export default function DashboardPage() {
  return (
    <SidebarProvider>
      <AppSidebar />
      <SidebarInset>
        {/* Wouter 路由实现 SPA 导航 */}
        <Switch>
          <Route path="/analytics">
            <AnalyticsView />
          </Route>
          <Route path="/reports">
            <ReportsView />
          </Route>
          <Route path="/">
            <DashboardOverview />
          </Route>
        </Switch>
      </SidebarInset>
    </SidebarProvider>
  );
}

优势

  • 快速导航 - 子路由之间无需整页刷新
  • 更好的用户体验 - 平滑过渡,保留侧边栏状态
  • SEO 友好 - Next.js 处理初始 SSR,Wouter 处理客户端
  • 独立模块 - 每个仪表盘页面有自己的 wouter 实例

使用场景

  • ✅ 带有多个视图的仪表盘部分 (/dashboard, /agent, /kadmin)
  • ✅ 任何有标签页/导航且子路由无需 SSR 的页面
  • ❌ 需要每个子路由都有 SEO 的公开页面(使用 Next.js App Router)

src/components/ - React 组件

按类别组织的可复用 UI 组件。

组织方式

components/
├── ui/              # 通用 UI 组件 (Button, Card, Dialog)
├── auth/            # 认证相关 (LoginForm, AuthButton)
├── dashboard/       # 仪表盘专用组件
└── shared/          # 跨所有页面共享的组件

何时创建组件

  • ✅ 在 2 个以上地方使用
  • ✅ 复杂逻辑会让页面混乱
  • ✅ 可复用的模式(表单、卡片、弹窗)

何时不创建

  • ❌ 一次性 UI(直接放在 page.tsx)
  • ❌ 过度细粒度(不要提取每个 <div>

KUI 库: 不要直接修改 packages/kui!在 src/components/ui/ 中创建包装组件。


src/db/ - 数据库层

所有数据库相关内容:schemas、migrations、查询。

关键文件

  • schema/*.ts - 表定义(Drizzle ORM)
  • index.ts - 数据库客户端导出
  • seed.ts - 开发环境示例数据

示例 schema (schema/tasks.ts):

import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';

export const tasks = pgTable('tasks', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  description: text('description'),
  status: text('status', { 
    enum: ['pending', 'in_progress', 'completed'] 
  }).default('pending'),
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow(),
});

新功能添加位置

  • 新表 → 创建 schema/your-table.ts
  • 查询 → 添加到 tRPC router(见 /server/routers/

src/server/ - 服务端代码

后端逻辑:tRPC routers、procedures、middleware。

结构

server/
├── routers/          # tRPC 路由(每个领域一个)
│   ├── tasks.ts      # tasks.list, tasks.create 等
│   ├── users.ts      # users.me, users.update 等
│   └── index.ts      # 合并所有路由
├── trpc.ts           # tRPC 配置与 procedures
└── index.ts          # 主 tRPC app 导出

示例 router (routers/tasks.ts):

import { z } from 'zod';
import { createTRPCRouter, protectedProcedure } from '../trpc';

export const tasksRouter = createTRPCRouter({
  list: protectedProcedure
    .input(z.object({ limit: z.number().default(20) }))
    .query(async ({ ctx, input }) => {
      return ctx.db.select().from(tasks).limit(input.limit);
    }),
    
  create: protectedProcedure
    .input(z.object({ title: z.string() }))
    .mutation(async ({ ctx, input }) => {
      return ctx.db.insert(tasks).values(input);
    }),
});

更多参考

完整的文件结构和最佳实践,请参考英文版文档

On this page