研发协作
扩展 Git City 3D GitHub 可视化:把…
在维护或扩展 Git City 项目时,用 Next.js、React Three Fiber、Three.js 和 Supabase 把 GitHub 用户数据渲染成 3D 像素建筑城市。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
扩展 Git City 3D GitHu特定
解决思路
- 克隆项目:`git clone https://github.com/srizzon/git-city.git`
- 进入目录:`cd git-city`
- 安装依赖:`npm install`
- 复制环境变量模板:Linux/macOS 用 `cp .env.example .env.local`,Windows CMD 用 `copy .env.example .env.local`,PowerShell 用 `Copy-Item .env.example .env.local`
- 启动开发服务:`npm run dev`,访问 `http://localhost:3001`
- 在 `.env.local` 填入 Supabase 配置:`NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co`、`NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key`、`SUPABASE_SERVICE_ROLE_KEY=your-service-role-key`
- 在 `.env.local` 填入 GitHub token:`GITHUB_TOKEN=github_pat_your_token_here`
- 可选:为 `/admin/ads` 访问权限填入 `ADMIN_GITHUB_LOGINS=your_github_login`,多个 GitHub 登录名用逗号分隔
- 查找 Supabase 值:进入 Supabase Dashboard → Project Settings → API
- 查找 GitHub token:进入 github.com → Settings → Developer settings → Personal access tokens,推荐使用 fine-grained token
- 按 App Router 组织页面:`app/page.tsx` 主城市视图、`app/[username]/` 用户资料页、`app/compare/` 并排比较模式、`app/admin/` 管理面板
- 按 3D 城市组件组织代码:`components/city/Building.tsx` 单个建筑 mesh、`components/city/CityScene.tsx` 主 R3F canvas/scene、`components/city/LODManager.tsx` LOD 系统
- 放置 2D 覆盖 UI 组件到 `components/ui/`
- 放置资料页组件到 `components/profile/`
- 放置 GitHub API 辅助函数到 `lib/github.ts`
- 放置 Supabase client 与 server 工具到 `lib/supabase/`
- 放置建筑指标计算到 `lib/buildings.ts`
- 放置成就逻辑到 `lib/achievements.ts`
- 放置自定义 React hooks 到 `hooks/`
- 放置 TypeScript 类型定义到 `types/`
- 放置静态资源到 `public/`
- 定义 `BuildingMetrics`:`height` 来自总贡献数、`width` 来自公开仓库数、`windowBrightness` 来自获得的总 stars、`windowPattern` 来自近期活动模式
- 实现 `calculateBuildingMetrics(profile)`:`height = Math.log10(profile.totalContributions + 1) * 10`;`width = Math.min(Math.ceil(profile.publicRepos / 10), 8)`;`windowBrightness = Math.min(profile.totalStars / 1000, 1)`;返回 `{ height, width, windowBrightness, windowPattern: [] }`
- 实现 `Building` 组件:使用 `useRef`、`useFrame`、`THREE`,接收 `position`、`metrics`、`username`、`isSelected`、`onClick`
- 在 `Building` 的 `useFrame` 中,当 `isSelected` 时执行 `meshRef.current.rotation.y = Math.sin(state.clock.elapsedTime) * 0.05` 来让选中建筑动画旋转
- 在 `Building` 中返回 `<group position={position} onClick={onClick}>`,内部放主建筑 `mesh`,使用 `boxGeometry args={[metrics.width, metrics.height, metrics.width]}` 和 `meshStandardMaterial color='#1a1a2e'`,并挂载 `WindowInstances metrics={metrics}`
- 使用 instanced mesh 渲染窗户以提升性能:在 `WindowInstances` 中使用 `useRef<InstancedMesh>`、`useEffect`、`Matrix4`、`Color`
- 遍历楼层和列:`for (let floor = 0; floor < metrics.height; floor++)` 与 `for (let col = 0; col < metrics.width; col++)`,根据 `metrics.windowPattern[index] > 0.5` 判断窗户是否点亮
- 设置每个窗户矩阵:`matrix.setPosition(col * 1.1 - metrics.width / 2, floor * 1.2, 0.51)`,然后调用 `meshRef.current.setMatrixAt(index, matrix)`
- 设置每个窗户颜色:点亮的窗户用 `#FFD700`,未点亮用 `#1a1a2e`,调用 `meshRef.current.setColorAt(index, color.set(...))`,并递增 `index`
- 更新 instanced mesh:设置 `meshRef.current.instanceMatrix.needsUpdate = true`,如果存在 `instanceColor` 也设置 `needsUpdate = true`
- 计算窗户数量:`const windowCount = Math.floor(metrics.height) * metrics.width`
- 返回 `<instancedMesh ref={meshRef} args={[undefined, undefined, windowCount]}>`,内部使用 `planeGeometry args={[0.4, 0.5]}` 和 `meshBasicMaterial`
- 初始化 GitHub API:使用 `new Octokit({ auth: process.env.GITHUB_TOKEN })`
- 实现 `fetchGitHubProfile(username)`:用 `Promise.all` 并发调用 `octokit.users.getByUsername({ username })` 和 `octokit.repos.listForUser({ username, per_page: 100, sort: 'updated' })`
- 汇总仓库 stars:对 `reposResponse.data` 执行 `reduce`,累加 `repo.stargazers_count ?? 0` 得到 `totalStars`
- 返回 GitHub 资料对象:包含 `username: userResponse.data.login`、`avatarUrl: userResponse.data.avatar_url`、`publicRepos: userResponse.data.public_repos`、`followers: userResponse.data.followers`、`totalStars`
- 实现 `fetchContributionData(username)`:使用 GitHub GraphQL 查询 `user(login: $username) { contributionsCollection { contributionCalendar { totalContributions weeks { contributionDays { contributionCount date } } } } }`
- 发送 GraphQL 请求:`fetch('https://api.github.com/graphql', { method: 'POST', headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables: { username } }) })`
- 解析贡献总数:读取 `data.data.user.contributionsCollection.contributionCalendar.totalContributions` 并返回
- 实现 Supabase server client:在 `lib/supabase/server.ts` 中使用 `createServerClient`、`cookies`,配置 `getAll: () => cookieStore.getAll()` 与 `setAll`,传入 `NEXT_PUBLIC_SUPABASE_URL` 和 `NEXT_PUBLIC_SUPABASE_ANON_KEY`
- 实现 Supabase browser client:在 `lib/supabase/client.ts` 中使用 `createBrowserClient`,传入 `NEXT_PUBLIC_SUPABASE_URL` 和 `NEXT_PUBLIC_SUPABASE_ANON_KEY`
- 定义 `Achievement` 接口:包含 `id`、`name`、`description`、`icon`、`condition: (stats: UserStats) => boolean`
- 定义 `ACHIEVEMENTS`:`first-commit` 条件 `stats.totalContributions >= 1`;`thousand-commits` 条件 `stats.totalContributions >= 1000`;`star-collector` 条件 `stats.totalStars >= 100`;`open-sourcer` 条件 `stats.publicRepos >= 20`
- 实现 `calculateAchievements(stats)`:返回 `ACHIEVEMENTS.filter((achievement) => achievement.condition(stats))`
- 定义 `DecorationSlot` 类型:`'crown' | 'aura' | 'roof' | 'face'`
- 定义 `Decoration` 接口:包含 `id`、`slot`、`name`、`price`、`component: React.ComponentType<DecorationProps>`
- 创建装饰组件示例 `CrownDecoration({ position, buildingWidth })`:返回 `<group position={[position[0], position[1], position[2]]}>`,内部 `mesh` 使用 `coneGeometry args={[buildingWidth / 3, 2, 4]}` 和 `meshStandardMaterial color='#FFD700' metalness={0.8} roughness={0.2}`
- 注册装饰到 `DECORATIONS` 数组:例如 `{ id: 'golden-crown', slot: 'crown', name: 'Golden Crown', price: 500, component: CrownDecoration }`
- 实现相机/飞行控制:在 `CameraController` 中使用 `useThree`、`useFrame`、`targetRef`、`velocityRef`
- 在 `useFrame` 中平滑移动相机:`camera.position.lerp(targetRef.current, delta * 2)`
- 暴露 `flyTo(position)`:执行 `targetRef.current.copy(position).add(new THREE.Vector3(0, 10, 20))`
- 实现 Server Action `app/actions/kudos.ts`:文件顶部写 `'use server'`,创建 Supabase client,调用 `supabase.auth.getUser()`
- 如果 Server Action 中未登录则抛出 `new Error('Must be logged in to send kudos')`
- 插入 kudos 数据:`supabase.from('kudos').insert({ from_user_id: user.id, to_username: toUsername, created_at: new Date().toISOString() })`,若有 `error` 则抛出
- 插入成功后调用 `revalidatePath(`/${toUsername}`)` 刷新用户页面
- 实现 `app/[username]/page.tsx`:接收 `params.username`,用 `Promise.all` 并发执行 `fetchGitHubProfile(username)` 和 `createClient()`
- 查询城市资料:`supabase.from('profiles').select('*, decorations(*)').eq('username', username).single()`
- 计算成就:调用 `calculateAchievements({ totalContributions: githubProfile.totalContributions, totalStars: githubProfile.totalStars, publicRepos: githubProfile.publicRepos })`
- 渲染资料页:输出 `<BuildingPreview profile={githubProfile} cityProfile={cityProfile} />` 和 `<AchievementGrid achievements={achievements} />`
- 实现 `generateMetadata`:返回 `title: `${params.username} — Git City``、描述 `View ${params.username}'s building in Git City`、以及 `openGraph.images: [`/api/og/${params.username}`]`
- 实现 LOD 系统:在 `useLOD(buildingPosition)` 中用 `useThree` 获取相机,计算 `camera.position.distanceTo(buildingPosition)`
- 按距离返回细节级别:距离 `< 50` 返回 `'high'`,距离 `< 150` 返回 `'medium'`,否则返回 `'low'`
- 在城市视图中使用 SWR 拉取建筑:定义 `fetcher = (url) => fetch(url).then(r => r.json())`
- 实现 `useCityBuildings`:调用 `useSWR('/api/buildings', fetcher, { refreshInterval: 30000 })`,用于每 30 秒刷新实时活动
- 提供 API `GET /api/buildings`:获取所有城市建筑及位置
- 提供 API `GET /api/profile/[username]`:获取 GitHub 与城市资料数据
- 提供 API `POST /api/kudos`:向用户发送 kudos
- 提供 API `GET /api/og/[username]`:生成 OG 分享卡片图片
- 提供 API `POST /api/webhook/stripe`:处理 Stripe 支付 webhook
- 提供 `GET /admin/ads` 管理面板,需要 `ADMIN_GITHUB_LOGINS` 才可访问
- 排查 3D 场景不渲染:检查 `@react-three/fiber` 和 `three` 版本是否兼容,并确保 canvas 容器 div 设置了高度
- 排查 GitHub API 速率限制:使用带合适 scope 的 fine-grained token,并将 GitHub 响应缓存到 Supabase 以减少重复 API 调用
- 排查本地 Supabase 认证不工作:在 Supabase 项目中配置 GitHub OAuth provider,并把本地回调 URL `http://localhost:3001/auth/callback` 加入允许列表
- 排查建筑不出现:检查 Supabase Row Level Security 策略是否允许匿名用户读取 `profiles` 和 `buildings` 表
- 排查窗户闪烁:通常是 Z-fighting,沿法线轴给窗户 mesh 位置增加极小偏移 `0.001`
- 排查大量建筑性能问题:确保窗户使用 instanced meshes 且 LOD 系统启用,避免在 render loop 中新建 `THREE.Material`,应在组件外定义或使用 `useMemo`
- 技术栈约束:Framework 使用 Next.js 16 App Router 与 Turbopack
- 技术栈约束:3D 渲染使用 Three.js + `@react-three/fiber` + drei
- 技术栈约束:数据库使用 Supabase PostgreSQL + RLS
- 技术栈约束:认证使用 Supabase GitHub OAuth
- 技术栈约束:支付使用 Stripe
- 技术栈约束:样式使用 Tailwind CSS v4 + Silkscreen 字体
- 技术栈约束:部署使用 Vercel
- 许可约束:项目使用 AGPL-3.0 许可证
适用边界 · 注意事项
- 不适用于不想使用 Next.js、React Three Fiber、Supabase 或 Stripe 的技术栈项目
- 不适用于只需要普通 GitHub 数据展示、不需要 3D 建筑或城市交互的场景
- 没有 GitHub token、Supabase 项目或 Stripe 账号时,无法完整搭建和运行涉及对应服务的功能