From 4ff4e8de98d8c77f2047c73612fa268f1d376f2e Mon Sep 17 00:00:00 2001 From: rinisnotarobot Date: Thu, 28 May 2026 11:48:33 +0800 Subject: [PATCH 1/5] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=96=87=E6=A1=A3=E5=92=8C=E6=9B=B4=E6=96=B0=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README.md: 更新为完整的项目介绍,添加徽章和功能说明 - CHANGELOG.md: 记录版本变更历史 - CONTRIBUTING.md: 贡献指南和行为准则 - LICENSE: MIT 许可证 Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 45 +++++ CONTRIBUTING.md | 409 +++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 21 +++ README.md | 433 +++++++++++++++++++++++++----------------------- 4 files changed, 702 insertions(+), 206 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6ed71ba --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,45 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Article search functionality +- Virtual article list with infinite scroll + +### Changed +- Refactored article routing structure + +### Fixed +- Real-time like count updates after liking articles + +## [0.1.0] - 2026-05-20 + +### Added +- Initial release +- User authentication (email/password + OTP) +- Article writing with Tiptap editor +- Article listing and detail pages +- User profiles and settings +- Avatar upload via Cloudflare R2 +- Email notifications via Resend +- Dark/light theme toggle +- Responsive layout with sidebar + +### Technical +- TanStack Start (React SSR) +- TanStack Router (file-based routing) +- TanStack Query (data fetching) +- Prisma + PostgreSQL +- Better Auth +- Tailwind CSS v4 + Shadcn/UI +- Vitest testing setup + +--- + +[Unreleased]: https://github.com/Rinisnotarobot/cedium/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/Rinisnotarobot/cedium/releases/tag/v0.1.0 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c3f0085 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,409 @@ +# Contributing to Cedium + +首先,感谢你考虑为 Cedium 做贡献!正是像你这样的人让 Cedium 成为一个更好的创作平台。 + +## 目录 + +- [如何贡献](#如何贡献) +- [开发环境设置](#开发环境设置) +- [项目结构](#项目结构) +- [代码风格指南](#代码风格指南) +- [提交规范](#提交规范) +- [Pull Request 流程](#pull-request-流程) +- [报告 Bug](#报告-bug) +- [提出新功能](#提出新功能) +- [行为准则](#行为准则) +- [获得帮助](#获得帮助) + +--- + +## 如何贡献 + +### 贡献方式 + +我们欢迎多种形式的贡献: + +- 🐛 **报告 Bug** — 提交 Issue 描述问题 +- 💡 **提出功能** — 分享你的想法和需求 +- 📝 **改进文档** — 修正错漏、补充说明 +- 🔧 **修复 Bug** — 提交 PR 解决已知问题 +- ✨ **添加功能** — 实现新特性 +- 🌐 **翻译** — 帮助项目支持更多语言 + +### 开始之前 + +在开始贡献之前,请确保: + +1. 你已经阅读了 [README.md](README.md) +2. 你已经熟悉项目的技术栈 +3. 你已经设置好开发环境(见下一节) + +--- + +## 开发环境设置 + +### 前置要求 + +| 工具 | 版本 | 说明 | +|------|------|------| +| Node.js | 18+ | JavaScript 运行时 | +| pnpm | 8+ | 包管理器 | +| PostgreSQL | 15+ | 数据库 | + +### 安装步骤 + +1. **克隆仓库** + + ```bash + git clone https://github.com/Rinisnotarobot/cedium.git + cd cedium + ``` + +2. **安装依赖** + + ```bash + pnpm install + ``` + +3. **配置环境变量** + + ```bash + cp .env.example .env.local + ``` + + 编辑 `.env.local`,填写必需的环境变量(参见 [README.md](README.md#环境变量))。 + +4. **初始化数据库** + + ```bash + pnpm db:generate # 生成 Prisma Client + pnpm db:push # 推送数据库结构 + pnpm db:seed # 填充种子数据(可选) + ``` + +5. **启动开发服务器** + + ```bash + pnpm dev + ``` + + 访问 http://localhost:3000 验证一切正常。 + +### 运行测试 + +```bash +pnpm test # 运行所有测试 +pnpm test -- watch # 监听模式 +``` + +--- + +## 项目结构 + +熟悉项目结构有助于你快速定位代码: + +``` +src/ +├── components/ # UI 组件 +├── data/ # Server Functions (TanStack Start) +├── hooks/ # TanStack Query hooks +├── lib/ # 核心配置(auth、validators) +├── routes/ # 文件路由 +├── types/ # TypeScript 类型定义 +└── generated/ # Prisma 生成的类型 +``` + +详细结构请参考 [README.md](README.md#项目结构)。 + +--- + +## 代码风格指南 + +### TypeScript + +- **避免 `any` 类型** — 使用具体类型或 `unknown` +- **使用严格模式** — 项目已启用 TypeScript strict +- **命名约定**: + - 变量/函数:`camelCase` + - 组件/类型/接口:`PascalCase` + - 常量:`UPPER_SNAKE_CASE` + - 文件名:与导出内容一致 + +### React + +- **使用函数组件** — 项目使用 React 19 + Compiler +- **保持组件简洁** — 单一职责,< 50 行 +- **避免手动 memo** — React Compiler 自动处理 + +### CSS + +- **使用 Tailwind CSS** — 遵循项目现有的样式约定 +- **自定义样式** — 放在 `styles.css` 或组件内 + +### 文件组织 + +- **一个文件一个主要导出** +- **文件大小** — 控制在 800 行以内 +- **目录结构** — 按功能模块组织 + +--- + +## 提交规范 + +我们使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范: + +### 格式 + +``` +<类型>: <描述> + +[可选的详细说明] + +[可选的脚注] +``` + +### 类型 + +| 类型 | 说明 | 示例 | +|------|------|------| +| `feat` | 新功能 | `feat: 添加文章搜索功能` | +| `fix` | Bug 修复 | `fix: 修复登录重定向问题` | +| `docs` | 文档更新 | `docs: 更新安装说明` | +| `style` | 样式调整(不影响逻辑) | `style: 调整按钮间距` | +| `refactor` | 重构(不改变功能) | `refactor: 重构认证逻辑` | +| `perf` | 性能优化 | `perf: 优化文章列表渲染` | +| `test` | 测试相关 | `test: 添加用户注册测试` | +| `chore` | 构建/工具变更 | `chore: 更新依赖版本` | + +### 规则 + +- **使用中文描述** — 项目面向中文用户 +- **首字母小写** — 描述不以大写开头 +- **不以句号结尾** — 描述末尾不加句号 +- **原子提交** — 一个提交只做一件事 + +--- + +## Pull Request 流程 + +### 创建 PR 之前的检查清单 + +- [ ] 代码通过 `pnpm test` +- [ ] 代码符合风格指南 +- [ ] 提交消息符合规范 +- [ ] 更新了相关文档 +- [ ] 添加了必要的测试 + +### 步骤 + +1. **创建分支** + + ```bash + git checkout -b feat/your-feature-name + # 或 + git checkout -b fix/your-bug-fix + ``` + + 分支命名建议: + - `feat/xxx` — 新功能 + - `fix/xxx` — Bug 修复 + - `refactor/xxx` — 重构 + - `docs/xxx` — 文档 + +2. **编写代码** + + 确保代码符合风格指南,并添加必要的测试。 + +3. **提交变更** + + ```bash + git add . + git commit -m "feat: 添加新功能描述" + ``` + +4. **推送分支** + + ```bash + git push origin feat/your-feature-name + ``` + +5. **创建 Pull Request** + + - 在 GitHub 上打开 Pull Request + - 清晰描述 PR 的目的和变更内容 + - 关联相关 Issue(如 `Closes #123`) + - 等待代码审查 + +### PR 标题格式 + +使用与提交消息相同的格式: + +``` +feat: 添加文章搜索功能 +fix: 修复登录重定向问题 +``` + +### PR 模板 + +创建 PR 时,请包含以下信息: + +```markdown +## 变更说明 + +简要描述此 PR 解决的问题或添加的功能。 + +## 变更类型 + +- [ ] Bug 修复 +- [ ] 新功能 +- [ ] 重构 +- [ ] 文档更新 +- [ ] 其他 + +## 测试说明 + +描述如何测试这些变更。 + +## 相关 Issue + +Closes #xxx +``` + +--- + +## 报告 Bug + +### 提交 Bug 之前的检查 + +- [ ] 搜索现有 Issues,确认问题未被报告 +- [ ] 使用最新版本测试,确认问题仍存在 +- [ ] 收集必要的调试信息 + +### 如何提交 Bug + +打开一个新的 Issue,包含以下信息: + +**标题**:简洁描述问题 + +**描述**: + +```markdown +## 问题描述 + +清晰描述遇到的问题。 + +## 复现步骤 + +1. 打开 '...' +2. 点击 '...' +3. 看到错误 '...' + +## 预期行为 + +描述你期望发生什么。 + +## 实际行为 + +描述实际发生了什么。 + +## 环境信息 + +- 浏览器: [例如 Chrome 120] +- 操作系统: [例如 Windows 11] +- Node 版本: [例如 18.17.0] + +## 截图/日志 + +如果适用,添加截图或错误日志。 +``` + +--- + +## 提出新功能 + +我们欢迎新功能建议! + +### 提交之前的考虑 + +- 功能是否符合项目定位? +- 是否有现有 Issue 已提出类似想法? +- 功能是否足够通用,对大多数用户有帮助? + +### 如何提交功能建议 + +打开一个新的 Issue,使用 `enhancement` 标签: + +```markdown +## 功能描述 + +清晰描述你希望添加的功能。 + +## 使用场景 + +描述什么情况下需要此功能。 + +## 可能的实现方案 + +如果有想法,简要描述可能的实现方式。 + +## 替代方案 + +描述你考虑过的其他解决方案。 + +## 额外信息 + +任何其他相关信息或截图。 +``` + +--- + +## 行为准则 + +### 我们的承诺 + +为了营造开放和友好的环境,我们承诺: + +- 尊重不同观点和经验 +- 优雅地接受建设性批评 +- 关注对社区最有利的事情 +- 对其他社区成员表示同理心 + +### 不当行为 + +以下行为将被视为不当: + +- 使用性化语言或图像 +-侮辱性/贬损性评论 +- 公开或私下骚扰 +- 未经许可发布他人私人信息 +- 其他不道德或不专业的行为 + +### 执行 + +不当行为可能导致: + +- 警告 +- 临时禁止互动 +- 永久禁止参与项目 + +--- + +## 获得帮助 + +如果你在贡献过程中遇到问题,可以通过以下方式获得帮助: + +- **GitHub Issues** — 在 Issues 中提问 +- **GitHub Discussions** — 参与讨论(如果已启用) +- **邮件** — 聪明地描述你的问题,我们会尽快回复 + +--- + +## 致谢 + +感谢所有为 Cedium 做出贡献的人!你的每一份贡献都让这个项目变得更好。 + +贡献者将会在项目首页或 Release 说明中被提及。 + +--- + +再次感谢你的贡献!🎉 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0044b4d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Rinisnotarobot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index f17792c..880af9f 100644 --- a/README.md +++ b/README.md @@ -1,260 +1,281 @@ # Cedium -基于 TanStack Start 构建的全栈 Web 应用模板。 +> 一个现代化的创作平台,为写作者而生。 -## 技术栈 +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-green.svg)](CONTRIBUTING.md) [![Made with TanStack](https://img.shields.io/badge/Made%20with-TanStack%20Start-ff6b6b.svg)](https://tanstack.com/start) [![React 19](https://img.shields.io/badge/React-19-61dafb.svg)](https://react.dev) [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-3178c6.svg)](https://typescriptlang.org) [![Tailwind CSS](https://img.shields.io/badge/Tailwind-4-38bdf8.svg)](https://tailwindcss.com) [![Prisma](https://img.shields.io/badge/Prisma-7-2d3748.svg)](https://prisma.io) -| 类别 | 技术 | -|------|------| -| 框架 | TanStack Start (React) | -| 路由 | TanStack Router (文件路由) | -| 数据获取 | TanStack Query | -| 表格 | TanStack Table | -| 表单 | TanStack Form + Zod | -| 数据库 | Prisma + PostgreSQL | -| 认证 | Better Auth | -| 样式 | Tailwind CSS v4 | -| UI 组件 | Shadcn/UI | -| 测试 | Vitest | -| 包管理 | pnpm | +

+ 🌐 在线演示 • + 核心功能 • + 技术栈 • + 快速开始 • + 项目结构 • + 贡献 +

-## 快速开始 +--- -### 安装依赖 +## 核心功能 -```bash -pnpm install -``` +### 📝 内容创作 -### 配置环境变量 +| 功能 | 说明 | +|------|------| +| **富文本编辑** | Tiptap 编辑器,支持 Markdown、代码高亮、表格、图片 | +| **草稿管理** | DRAFT / PUBLISHED / ARCHIVED 三种状态 | +| **标签系统** | 文章分类与标签关联 | +| **封面设置** | 自定义文章封面图片 | -创建 `.env.local` 文件,参考 `.env.example`: +### 👤 用户系统 - -| 变量 | 必需 | 说明 | 示例 | -|------|------|------|------| -| `DATABASE_URL` | 是 | PostgreSQL 连接字符串 | `postgresql://user:password@localhost:5432/cedium` | -| `BETTER_AUTH_URL` | 是 | Auth 服务 URL | `http://localhost:3000` | -| `BETTER_AUTH_SECRET` | 是 | Auth 密钥(运行 `npx @better-auth/cli secret` 生成) | 32位随机字符串 | -| `R2_ENDPOINT` | 是 | Cloudflare R2 存储端点 | `https://.r2.cloudflarestorage.com` | -| `R2_ACCESS_KEY_ID` | 是 | R2 访问密钥 ID | - | -| `R2_SECRET_ACCESS_KEY` | 是 | R2 访问密钥 | - | -| `R2_BUCKET_NAME` | 是 | R2 存储桶名称 | `cedium-avatars` | -| `R2_PUBLIC_URL` | 是 | R2 公开访问 URL | `https://` | -| `RESEND_API_KEY` | 是 | Resend 邮件服务 API 密钥 | `re_xxx` | - +| 功能 | 说明 | +|------|------| +| **双重认证** | Email + OTP 验证登录 | +| **个人主页** | 用户资料、bio、pronouns 展示 | +| **头像托管** | Cloudflare R2 云存储 | +| **邮箱验证** | 发布文章前需验证邮箱 | -### 初始化数据库 +### 🌐 社交互动 -```bash -pnpm db:generate # 生成 Prisma Client -pnpm db:push # 推送数据库结构(开发) -pnpm db:seed # 填充种子数据(可选) -``` +| 功能 | 说明 | +|------|------| +| **关注系统** | 关注/取消关注作者 | +| **文章点赞** | 点赞计数实时更新 | +| **收藏文章** | 个人收藏列表管理 | +| **嵌套评论** | 支持评论回复,评论点赞 | -### 启动开发服务器 +### 🎨 界面体验 -```bash -pnpm dev -``` +| 功能 | 说明 | +|------|------| +| **深色模式** | 自动跟随系统 + 手动切换 | +| **响应式布局** | 移动端适配,侧边栏收缩 | +| **无限滚动** | TanStack Virtual 虚拟列表 | +| **流畅动画** | Framer Motion 页面过渡 | -访问 http://localhost:3000 +--- -## 项目结构 +## 技术栈 - -``` -src/ -├── components/ # UI 组件 -│ ├── articles/ # 文章相关组件 -│ ├── auth/ # 认证相关组件 -│ ├── comments/ # 评论组件 -│ ├── editor/ # 编辑器组件 -│ ├── favorites/ # 收藏组件 -│ ├── home/ # 首页组件 -│ ├── layout/ # 布局组件 -│ ├── settings/ # 设置页面组件 -│ ├── theme/ # 主题切换组件 -│ ├── users/ # 用户相关组件 -│ └── ui/ # Shadcn/UI 基础组件 + minimal-tiptap -├── data/ # Server Functions (TanStack Start) -├── hooks/ # 自定义 Hooks -│ ├── keys/ # Query keys -│ ├── mutations/ # TanStack Query mutations -│ ├── queries/ # TanStack Query queries -│ └── utils/ # 工具 Hooks -├── lib/ # 工具函数和配置 -│ ├── auth.ts # Better Auth 配置 -│ ├── auth-client.ts # 认证客户端 -│ ├── email.ts # Resend 邮件服务 -│ ├── r2.ts # Cloudflare R2 存储 -│ └── validators/ # Zod 验证器 -├── middlewares/ # 中间件(认证等) -├── routes/ # 路由文件(文件路由) -│ ├── _app/ # 需认证的路由组 -│ ├── _auth/ # 认证页面路由组 -│ └── api/ # API 路由 -├── generated/ # Prisma 生成的类型 -├── types/ # TypeScript 类型定义 -├── router.tsx # 路由配置 -├── server.ts # 服务器入口 -├── db.ts # 数据库连接 -└── styles.css # 全局样式 -prisma/ -├── schema.prisma # 数据库模型 -└── seed.ts # 种子数据 -``` - +| 类别 | 技术 | 版本 | +|:-----|:-----|:----:| +| **框架** | [TanStack Start](https://tanstack.com/start) | latest | +| **路由** | [TanStack Router](https://tanstack.com/router) | latest | +| **数据** | [TanStack Query](https://tanstack.com/query) | latest | +| **表单** | [TanStack Form](https://tanstack.com/form) + Zod | latest | +| **虚拟列表** | [TanStack Virtual](https://tanstack.com/virtual) | v3 | +| **React** | React + React Compiler | v19 | +| **状态** | Jotai | v2 | +| **动画** | Framer Motion | v12 | +| **数据库** | Prisma + PostgreSQL | v7 | +| **认证** | [Better Auth](https://better-auth.com) | v1.5 | +| **样式** | Tailwind CSS v4 + Shadcn/UI | v4 | +| **编辑器** | Tiptap (minimal-tiptap) | v3 | +| **邮件** | Resend | v6 | +| **存储** | Cloudflare R2 | - | +| **测试** | Vitest | v4 | -## 常用命令 +--- - -| 命令 | 说明 | -|------|------| -| `pnpm dev` | 启动开发服务器 (端口 3000) | -| `pnpm build` | 构建生产版本 | -| `pnpm start` | 启动生产服务器 (需要先 build) | -| `pnpm preview` | 预览生产构建 | -| `pnpm test` | 运行 Vitest 测试 | -| `pnpm db:generate` | 生成 Prisma Client | -| `pnpm db:push` | 推送数据库结构(开发环境) | -| `pnpm db:migrate` | 创建迁移(生产环境) | -| `pnpm db:studio` | 打开 Prisma Studio | -| `pnpm db:seed` | 填充种子数据 | - +## 快速开始 -## 路由系统 +### 前置要求 -采用文件路由,在 `src/routes/` 目录下创建文件自动生成路由: +- Node.js 18+ +- pnpm 8+ +- PostgreSQL 15+ - -``` -src/routes/ -├── __root.tsx # 根布局 -├── index.tsx # / 首页 -├── about.tsx # /about -├── articles.$slug.tsx # /articles/:slug 文章详情(公开) -├── _auth/ # 未认证路由组(登录页自动重定向) -│ ├── route.tsx # 认证守卫 -│ ├── login.tsx # /login -│ ├── sign-up.tsx # /sign-up -│ ├── forgot-password.tsx # /forgot-password -│ └── reset-password.tsx # /reset-password -├── _app/ # 需认证路由组 -│ ├── route.tsx # 认证守卫 + AppLayout -│ ├── search.tsx # /search -│ ├── write.tsx # /write -│ ├── articles.tsx # /articles 文章列表 -│ ├── users.$username.tsx # /users/:username 用户主页 -│ └── me/ # 个人中心 -│ ├── route.tsx # /me 路由组布局 -│ ├── index.tsx # /me -│ ├── articles.tsx # /me/articles 我的文章 -│ ├── favorites.tsx # /me/favorites -│ └── settings/ # /me/settings -│ ├── route.tsx # 设置布局 -│ ├── index.tsx # 设置首页 -│ ├── security.tsx # 安全设置 -│ ├── notifications.tsx -│ └── publishing.tsx -└── api/ # API 路由 - └── auth/$ # Better Auth 处理 /api/auth/* -``` - +### 安装 -使用 `Link` 组件导航: +```bash +# 克隆仓库 +git clone https://github.com/Rinisnotarobot/cedium.git -```tsx -import { Link } from "@tanstack/react-router"; +# 安装依赖 +pnpm install -文章详情 +# 配置环境变量 +cp .env.example .env.local ``` -## 数据获取 +### 环境变量 -### 使用 Loader(推荐) +| 变量 | 必需 | 说明 | +|------|:----:|------| +| `DATABASE_URL` | ✓ | PostgreSQL 连接字符串 | +| `BETTER_AUTH_SECRET` | ✓ | Auth 密钥(`npx @better-auth/cli secret` 生成) | +| `BETTER_AUTH_URL` | ✓ | 服务 URL(开发: `http://localhost:3000`) | +| `R2_ENDPOINT` | ✓ | Cloudflare R2 端点 | +| `R2_ACCESS_KEY_ID` | ✓ | R2 访问密钥 ID | +| `R2_SECRET_ACCESS_KEY` | ✓ | R2 访问密钥 | +| `R2_BUCKET_NAME` | ✓ | 存储桶名称 | +| `R2_PUBLIC_URL` | ✓ | 公开访问 URL | +| `RESEND_API_KEY` | ✓ | Resend 邮件 API | -在路由文件中定义 loader: +### 启动 -```tsx -import { createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/posts')({ - loader: async () => { - const posts = await fetchPosts() - return posts - }, - component: PostsPage, -}) +```bash +# 初始化数据库 +pnpm db:generate && pnpm db:push -function PostsPage() { - const posts = Route.useLoaderData() - return -} +# 开发模式 (端口 3000) +pnpm dev ``` -### 使用 TanStack Query - -```tsx -import { useQuery } from '@tanstack/react-query' - -const { data, isLoading } = useQuery({ - queryKey: ['posts'], - queryFn: fetchPosts, -}) -``` +访问 http://localhost:3000 开始探索。 -## 认证系统 +--- -Better Auth 提供完整的认证解决方案: +## 项目结构 -1. 配置认证服务 `src/lib/auth.ts` -2. 创建认证客户端 -3. 使用 React Hooks 管理登录状态 +``` +src/ +├── components/ # UI 组件 +│ ├── articles/ # 文章列表、详情、卡片 +│ ├── auth/ # 登录表单、OTP 验证 +│ ├── comments/ # 嵌套评论系统 +│ ├── editor/ # 编辑器相关 +│ ├── favorites/ # 收藏列表 +│ ├── home/ # 首页组件 +│ ├── layout/ # AppSidebar、导航栏 +│ ├── settings/ # 设置页面 +│ ├── theme/ # 深色模式切换 +│ ├── users/ # 用户主页 +│ └── ui/ # Shadcn/UI + minimal-tiptap +├── data/ # Server Functions +│ ├── articles.ts # 文章 CRUD +│ ├── bookmark.ts # 收藏操作 +│ ├── comment.ts # 评论系统 +│ ├── follow.ts # 关注关系 +│ ├── like.ts # 点赞功能 +│ └── tags.ts # 标签管理 +├── hooks/ # 自定义 Hooks +│ ├── keys/ # Query Keys +│ ├── mutations/ # TanStack Query mutations +│ ├── queries/ # TanStack Query queries +│ └── utils/ # useDebounce, useMobile 等 +├── lib/ # 核心配置 +│ ├── auth.ts # Better Auth 服务端 +│ ├── auth-client.ts # Better Auth 客户端 +│ ├── auth-guards.ts # 认证守卫 +│ ├── email.ts # Resend 邮件服务 +│ ├── r2.ts # R2 存储操作 +│ └── validators/ # Zod schemas +├── routes/ # 文件路由 +│ ├── _app/ # 认证路由组 +│ │ ├── me/ # 个人中心 +│ │ │ ├── settings/ # 设置页 +│ │ │ ├── articles/ # 我的文章 +│ │ │ └── favorites/ # 我的收藏 +│ │ ├── articles/ # 文章列表/详情 +│ │ ├── users/ # 用户主页 +│ │ ├── search/ # 文章搜索 +│ │ └── write/ # 写作页面 +│ ├── _auth/ # 未认证路由 +│ │ ├── login/ # 登录 +│ │ ├── sign-up/ # 注册 +│ │ └── forgot-password/ # 密码重置 +│ └── api/auth/ # Better Auth API +├── types/ # TypeScript 类型 +├── generated/prisma/ # Prisma 生成的类型 +└── styles.css # 全局样式 +``` -详细配置请参考 [Better Auth 文档](https://www.better-auth.com)。 +--- -## UI 组件 +## 数据模型 + +```mermaid +erDiagram + User ||--o{ Article : writes + User ||--o{ Follow : follows + User ||--o{ Bookmark : bookmarks + User ||--o{ Like : likes + User ||--o{ Comment : comments + + Article ||--o{ ArticleTag : has + Article ||--o{ Bookmark : bookmarked_by + Article ||--o{ Like : liked_by + Article ||--o{ Comment : has + + Tag ||--o{ ArticleTag : used_in + Comment ||--o{ Comment : replies_to + Comment ||--o{ CommentLike : liked_by + + Article { + string id + string title + string slug + string content + enum status "DRAFT|PUBLISHED|ARCHIVED" + int likeCount + int bookmarkCount + } + + User { + string id + string name + string email + string bio + string pronouns + int draftCount + } +``` -使用 Shadcn/UI 添加组件: +--- -```bash -pnpm dlx shadcn@latest add button -pnpm dlx shadcn@latest add card -pnpm dlx shadcn@latest add dialog -``` +## 常用命令 -组件自动安装到 `src/components/ui/`。 +| 命令 | 说明 | +|------|------| +| `pnpm dev` | 开发服务器 (端口 3000) | +| `pnpm build` | 生产构建 | +| `pnpm start` | 启动生产服务器 | +| `pnpm test` | 运行 Vitest 测试 | +| `pnpm db:generate` | 生成 Prisma Client | +| `pnpm db:push` | 推送数据库结构(开发) | +| `pnpm db:migrate` | 创建迁移(生产) | +| `pnpm db:studio` | 打开 Prisma Studio | +| `pnpm db:seed` | 填充种子数据 | -## 测试 +--- -运行测试: +## 路由一览 + +| 路径 | 页面 | 认证 | +|------|------|:----:| +| `/` | 首页 | ✗ | +| `/about` | 关于页面 | ✗ | +| `/login` | 登录 | ✗ | +| `/sign-up` | 注册 | ✗ | +| `/search` | 文章搜索 | ✓ | +| `/write` | 写作编辑器 | ✓ | +| `/articles` | 文章列表 | ✓ | +| `/articles/:slug` | 文章详情 | ✓ | +| `/users/:username` | 用户主页 | ✓ | +| `/me` | 个人中心 | ✓ | +| `/me/articles` | 我的文章 | ✓ | +| `/me/favorites` | 我的收藏 | ✓ | +| `/me/settings` | 设置中心 | ✓ | -```bash -pnpm test -``` +--- -测试文件位于 `__tests__/` 目录或在源文件旁创建 `.test.ts` 文件。 +## 贡献 -## 部署 +我们欢迎所有形式的贡献! -构建生产版本: +📖 查看 [CONTRIBUTING.md](CONTRIBUTING.md) 了解开发规范。 -```bash -pnpm build -``` +📋 查看 [CHANGELOG.md](CHANGELOG.md) 了解版本历史。 -输出目录为 `.output/`。 +--- -## 学习资源 +## License -- [TanStack Start 文档](https://tanstack.com/start) -- [TanStack Router 文档](https://tanstack.com/router) -- [TanStack Query 文档](https://tanstack.com/query) -- [Prisma 文档](https://www.prisma.io/docs) -- [Better Auth 文档](https://www.better-auth.com) -- [Shadcn/UI 文档](https://ui.shadcn.com) +[MIT](LICENSE) © 2026 Rinisnotarobot --- -由 TanStack Start 模板创建。 \ No newline at end of file +

+ 如果这个项目对你有帮助,欢迎给一个 ⭐️ +

\ No newline at end of file From 10f156ae368327717b704efb6f81bca2e376a9bc Mon Sep 17 00:00:00 2001 From: rinisnotarobot Date: Thu, 28 May 2026 11:48:41 +0800 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .gitignore: 添加 .reports/ 和 tanstack-start/ 目录 - vite.config.ts: 配置 importProtection 保护服务端模块 - dev.sh: 开发服务器启动脚本,自动清除端口占用 Co-Authored-By: Claude Opus 4.7 --- .gitignore | 2 ++ dev.sh | 14 ++++++++++++++ vite.config.ts | 18 +++++++++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100755 dev.sh diff --git a/.gitignore b/.gitignore index e074f5d..d2b9c5b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ todos.json /.claude/reviews/ docs/MODULE_COMPLETION_REPORT.md src/routeTree.gen.ts +.reports/ +tanstack-start/ diff --git a/dev.sh b/dev.sh new file mode 100755 index 0000000..5a09c10 --- /dev/null +++ b/dev.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# 查找并清除占用 3000 端口的进程 +PORT=3000 +PROCESS=$(lsof -t -i:$PORT 2>/dev/null) + +if [ -n "$PROCESS" ]; then + echo "发现进程占用端口 $PORT (PID: $PROCESS)" + kill -9 $PROCESS + echo "已清除进程" +fi + +# 启动开发服务器 +pnpm dev \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 83f8e7f..d644953 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,7 +13,23 @@ const config = defineConfig({ plugins: [ devtools(), tailwindcss(), - tanstackStart(), + tanstackStart({ + importProtection: { + client: { + specifiers: [ + '@prisma/client', + '@prisma/adapter-pg', + 'better-auth', + ], + files: [ + '**/db.ts', + '**/lib/auth.ts', + '**/lib/r2.ts', + '**/lib/email.ts', + ], + }, + }, + }), nitro(), viteReact(), babel({ presets: [reactCompilerPreset()] }), From 3410037510c0cacf0b12bd995e488c87c376f316 Mon Sep 17 00:00:00 2001 From: rinisnotarobot Date: Thu, 28 May 2026 11:48:54 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=96=87?= =?UTF-8?q?=E7=AB=A0=E6=90=9C=E7=B4=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加搜索页面 /search?q=xxx,支持 URL 参数 - navbar-search.tsx: 实现搜索表单,导航到搜索页 - virtual-article-list.tsx: 支持搜索结果渲染 - articles.ts: 新增 searchArticlesFn 服务端函数 - article-keys.ts: 添加搜索相关的 query keys - use-article-infinite-queries.ts: 新增 useSearchArticlesInfinite - article.ts (validator): 添加 searchArticlesSchema Co-Authored-By: Claude Opus 4.7 --- src/components/articles/index.ts | 4 +- .../articles/virtual-article-list.tsx | 21 ++++++-- src/components/layout/navbar-search.tsx | 28 +++++++++-- src/data/articles.ts | 48 ++++++++++++++++++- src/hooks/index.ts | 1 + src/hooks/keys/article-keys.ts | 2 + src/hooks/queries/index.ts | 2 +- .../queries/use-article-infinite-queries.ts | 19 +++++++- src/lib/validators/article.ts | 6 +++ src/routes/_app/search.tsx | 45 ++++++++++++++--- 10 files changed, 157 insertions(+), 19 deletions(-) diff --git a/src/components/articles/index.ts b/src/components/articles/index.ts index 3ff9b12..99d22ae 100644 --- a/src/components/articles/index.ts +++ b/src/components/articles/index.ts @@ -1,2 +1,4 @@ export { ArticleDetailPage } from "./article-detail-page" -export { ArticleContent } from "./article-content" \ No newline at end of file +export { ArticleContent } from "./article-content" +export { VirtualArticleList } from "./virtual-article-list" +export { MediumArticleCard } from "./medium-article-card" \ No newline at end of file diff --git a/src/components/articles/virtual-article-list.tsx b/src/components/articles/virtual-article-list.tsx index c540e17..c7061f4 100644 --- a/src/components/articles/virtual-article-list.tsx +++ b/src/components/articles/virtual-article-list.tsx @@ -2,22 +2,31 @@ import { useRef, useEffect } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; import { usePublishedArticlesInfinite, + useSearchArticlesInfinite, useMultipleBookmarkStatus, useMultipleLikeStatus, } from "#/hooks/queries"; import { MediumArticleCard } from "./medium-article-card"; interface VirtualArticleListProps { + query?: string; containerHeight?: number | string; estimatedItemHeight?: number; + emptyMessage?: string; } export function VirtualArticleList({ + query, containerHeight = "calc(100vh - 200px)", estimatedItemHeight = 120, + emptyMessage = "暂无文章", }: VirtualArticleListProps) { const parentRef = useRef(null); + // 根据 query 选择数据源 + const publishedQuery = usePublishedArticlesInfinite(10); + const searchQuery = useSearchArticlesInfinite(query || "", 10); + const { data, fetchNextPage, @@ -25,7 +34,7 @@ export function VirtualArticleList({ isFetchingNextPage, isLoading, isError, - } = usePublishedArticlesInfinite(10); + } = query ? searchQuery : publishedQuery; // 展平所有页面的文章 const allArticles = data?.pages.flatMap((page) => page.articles) ?? []; @@ -92,10 +101,12 @@ export function VirtualArticleList({ if (allArticles.length === 0) { return (
-

暂无文章

-

- 等待第一篇文章的发布... -

+

{emptyMessage}

+ {!query && ( +

+ 等待第一篇文章的发布... +

+ )}
); } diff --git a/src/components/layout/navbar-search.tsx b/src/components/layout/navbar-search.tsx index 8373022..c1a45fa 100644 --- a/src/components/layout/navbar-search.tsx +++ b/src/components/layout/navbar-search.tsx @@ -1,3 +1,5 @@ +import { useState } from 'react' +import { useNavigate } from '@tanstack/react-router' import { Search } from 'lucide-react' import { Input } from '#/components/ui/input' @@ -8,10 +10,30 @@ interface NavbarSearchProps { export function NavbarSearch({ visible = true }: NavbarSearchProps) { if (!visible) return null + const navigate = useNavigate() + const [inputValue, setInputValue] = useState('') + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault() + const searchTerm = inputValue.trim() + if (searchTerm) { + navigate({ + to: '/search', + search: { q: searchTerm } + }) + } + } + return ( -
+
- -
+ setInputValue(e.target.value)} + className="pl-9 w-full rounded-full" + /> + ) } \ No newline at end of file diff --git a/src/data/articles.ts b/src/data/articles.ts index eb4ba39..1355e4a 100644 --- a/src/data/articles.ts +++ b/src/data/articles.ts @@ -17,6 +17,7 @@ import { getMyArticlesSchema, getPublishedArticlesSchema, getPublishedArticlesInfiniteSchema, + searchArticlesSchema, getArticlesByAuthorSchema, deleteArticleSchema, } from '#/lib/validators/article' @@ -572,7 +573,7 @@ export const getPublishedArticlesInfiniteFn = createServerFn({ method: 'GET' }) }) const hasMore = articles.length > data.limit - const nextCursor = hasMore ? articles[articles.length - 1].id : undefined + const nextCursor = hasMore ? articles[data.limit - 1].id : undefined return { articles: articles.slice(0, data.limit).map(article => ({ @@ -584,6 +585,51 @@ export const getPublishedArticlesInfiniteFn = createServerFn({ method: 'GET' }) } }) +/** + * 搜索文章 (Cursor 分页) + * - 公开接口,无需认证 + * - 搜索标题、摘要、内容、标签(不区分大小写) + */ +export const searchArticlesFn = createServerFn({ method: 'GET' }) + .inputValidator(searchArticlesSchema) + .handler(async ({ data }) => { + const take = data.limit + 1 + const searchQuery = data.query.toLowerCase() + + const articles = await prisma.article.findMany({ + where: { + status: ArticleStatus.PUBLISHED, + OR: [ + { title: { contains: searchQuery, mode: 'insensitive' } }, + { excerpt: { contains: searchQuery, mode: 'insensitive' } }, + { content: { contains: searchQuery, mode: 'insensitive' } }, + { tags: { some: { tag: { name: { contains: searchQuery, mode: 'insensitive' } } } } }, + ], + }, + orderBy: { publishedAt: 'desc' }, + cursor: data.cursor ? { id: data.cursor } : undefined, + skip: data.cursor ? 1 : 0, + take, + include: { + author: { select: { id: true, name: true, image: true } }, + tags: { include: { tag: true } } + }, + }) + + const hasMore = articles.length > data.limit + const nextCursor = hasMore ? articles[data.limit - 1].id : undefined + + return { + articles: articles.slice(0, data.limit).map(article => ({ + ...article, + tags: article.tags?.map(at => at.tag) as Tag[] + })), + nextCursor, + hasMore, + query: data.query, + } + }) + /** * 获取作者文章列表 * - 公开接口,无需认证 diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 006eddf..702d5da 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -4,6 +4,7 @@ export { useAllTags, useSearchTags } from './use-tag-queries' export { useMyArticlesStats } from './use-article-stats' export { searchTags } from '#/lib/utils/tag' export { useComposedRef } from './use-composed-ref' +export { useDebounce } from './use-debounce' export { useElementRect } from './use-element-rect' export { useIsBreakpoint } from './use-is-breakpoint' export { useIsMobile } from './use-mobile' diff --git a/src/hooks/keys/article-keys.ts b/src/hooks/keys/article-keys.ts index e03fbcd..168e383 100644 --- a/src/hooks/keys/article-keys.ts +++ b/src/hooks/keys/article-keys.ts @@ -10,4 +10,6 @@ export const articleKeys = { byAuthor: (username: string) => [...articleKeys.all, 'by-author', username] as const, byTag: (slug: string) => [...articleKeys.lists(), { tag: slug }] as const, stats: () => [...articleKeys.all, 'my', 'stats'] as const, + search: (query: string) => [...articleKeys.all, 'search', query] as const, + searchInfinite: (query: string) => [...articleKeys.search(query), 'infinite'] as const, } \ No newline at end of file diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts index 6ef880b..0a42e5d 100644 --- a/src/hooks/queries/index.ts +++ b/src/hooks/queries/index.ts @@ -1,6 +1,6 @@ // Article queries removed - routes now use loaders directly export {} -export { usePublishedArticlesInfinite } from './use-article-infinite-queries' +export { usePublishedArticlesInfinite, useSearchArticlesInfinite } from './use-article-infinite-queries' export { useFollowStats, useFollowers, useFollowing, useFollowStatus } from './use-follow-queries' export { useMyBookmarks, useBookmarkStatus, useMultipleBookmarkStatus } from './use-bookmark-queries' export { useMyLikes, useLikeStatus, useMultipleLikeStatus } from './use-like-queries' diff --git a/src/hooks/queries/use-article-infinite-queries.ts b/src/hooks/queries/use-article-infinite-queries.ts index bf2df63..d8ec184 100644 --- a/src/hooks/queries/use-article-infinite-queries.ts +++ b/src/hooks/queries/use-article-infinite-queries.ts @@ -1,6 +1,6 @@ import { useInfiniteQuery } from '@tanstack/react-query' import { articleKeys } from '#/hooks/keys/article-keys' -import { getPublishedArticlesInfiniteFn } from '#/data/articles' +import { getPublishedArticlesInfiniteFn, searchArticlesFn } from '#/data/articles' const STALE_TIME_MS = 30_000 @@ -37,4 +37,21 @@ export function usePublishedArticlesInfinite(limit = 10) { lastPage.hasMore ? lastPage.nextCursor : undefined, staleTime: STALE_TIME_MS, }) +} + +interface SearchArticlesResult extends InfiniteArticlesResult { + query: string +} + +export function useSearchArticlesInfinite(query: string, limit = 10) { + return useInfiniteQuery({ + queryKey: articleKeys.searchInfinite(query), + queryFn: ({ pageParam }) => + searchArticlesFn({ data: { query, cursor: pageParam as string | undefined, limit } }), + initialPageParam: undefined as string | undefined, + getNextPageParam: (lastPage: SearchArticlesResult) => + lastPage.hasMore ? lastPage.nextCursor : undefined, + staleTime: STALE_TIME_MS, + enabled: query.length > 0, + }) } \ No newline at end of file diff --git a/src/lib/validators/article.ts b/src/lib/validators/article.ts index 647ccf5..4a602d0 100644 --- a/src/lib/validators/article.ts +++ b/src/lib/validators/article.ts @@ -90,6 +90,12 @@ export type GetPublishedArticlesInput = z.infer +// 搜索文章参数 (Cursor 分页) +export const searchArticlesSchema = cursorPaginationSchema.extend({ + query: z.string().min(1, '搜索关键词不能为空').max(100, '搜索关键词不能超过100个字符'), +}) +export type SearchArticlesInput = z.infer + // 获取作者文章列表 export const getArticlesByAuthorSchema = paginationSchema.extend({ username: z.string().optional(), diff --git a/src/routes/_app/search.tsx b/src/routes/_app/search.tsx index 0ffdb31..8cb71ce 100644 --- a/src/routes/_app/search.tsx +++ b/src/routes/_app/search.tsx @@ -1,9 +1,40 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute } from '@tanstack/react-router' +import { z } from 'zod' +import { PageContainer, PageHeader } from '#/components/layout' +import { VirtualArticleList } from '#/components/articles' -export const Route = createFileRoute("/_app/search")({ - component: RouteComponent, -}); +// 搜索参数 schema +const searchSchema = z.object({ + q: z.string().optional().default(''), +}) -function RouteComponent() { - return
Hello "/_app/search"!
; -} +export const Route = createFileRoute('/_app/search')({ + validateSearch: (search: Record) => { + return searchSchema.parse(search) + }, + component: SearchPage, +}) + +function SearchPage() { + const { q } = Route.useSearch() + + return ( + + + {q ? ( + + ) : ( +
+ 请输入搜索关键词 +
+ )} +
+ ) +} \ No newline at end of file From 7841fbf9a6beb258950349b3ec00255fe8976faf Mon Sep 17 00:00:00 2001 From: rinisnotarobot Date: Thu, 28 May 2026 11:49:02 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=E6=9C=8D=E5=8A=A1=E7=AB=AF=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=AF=BC=E5=85=A5=E4=BF=9D=E6=8A=A4=E5=92=8C=E5=85=B6?= =?UTF-8?q?=E4=BB=96=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth.ts, email.ts, r2.ts, db.ts: 添加 server-only 导入防止客户端泄露 - router.tsx: 将 defaultPreload 从 intent 改为 render - article-detail-page.tsx, navbar-actions.tsx: 小幅调整 Co-Authored-By: Claude Opus 4.7 --- src/components/articles/article-detail-page.tsx | 4 ++-- src/components/layout/navbar-actions.tsx | 2 +- src/db.ts | 2 ++ src/lib/auth.ts | 2 ++ src/lib/email.ts | 2 ++ src/lib/r2.ts | 2 ++ src/router.tsx | 2 +- 7 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/articles/article-detail-page.tsx b/src/components/articles/article-detail-page.tsx index 201183d..768a770 100644 --- a/src/components/articles/article-detail-page.tsx +++ b/src/components/articles/article-detail-page.tsx @@ -152,8 +152,8 @@ export function ArticleDetailPage() { {article.tags.map((tag) => ( {/* 移动端搜索图标 */} diff --git a/src/db.ts b/src/db.ts index 8a90bd1..349d507 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1,3 +1,5 @@ +import '@tanstack/react-start/server-only' + import { PrismaClient } from './generated/prisma/client.js' import { PrismaPg } from '@prisma/adapter-pg' diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 12b51ad..c349891 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,3 +1,5 @@ +import '@tanstack/react-start/server-only' + import { betterAuth } from 'better-auth' import { prismaAdapter } from 'better-auth/adapters/prisma' import { tanstackStartCookies } from 'better-auth/tanstack-start' diff --git a/src/lib/email.ts b/src/lib/email.ts index c527f36..5ad35cd 100644 --- a/src/lib/email.ts +++ b/src/lib/email.ts @@ -1,3 +1,5 @@ +import '@tanstack/react-start/server-only' + import { Resend } from 'resend' let resend: Resend | null = null diff --git a/src/lib/r2.ts b/src/lib/r2.ts index 3aad03a..e0004a2 100644 --- a/src/lib/r2.ts +++ b/src/lib/r2.ts @@ -1,3 +1,5 @@ +import '@tanstack/react-start/server-only' + import { S3Client, PutObjectCommand, DeleteObjectCommand, DeleteObjectsCommand } from '@aws-sdk/client-s3' const r2Client = new S3Client({ diff --git a/src/router.tsx b/src/router.tsx index 482a6e1..5a0e3e9 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -11,7 +11,7 @@ export function getRouter() { routeTree, context: { queryClient }, scrollRestoration: true, - defaultPreload: 'intent', + defaultPreload: 'render', defaultPreloadStaleTime: 0, }) From c09eadf9df13f9ffde7cbb8405f92ef9029ce2b7 Mon Sep 17 00:00:00 2001 From: rinisnotarobot Date: Thu, 28 May 2026 11:49:08 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20useDebounce=20?= =?UTF-8?q?hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 通用防抖 hook,用于延迟更新值(如搜索输入) Co-Authored-By: Claude Opus 4.7 --- src/hooks/use-debounce.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/hooks/use-debounce.ts diff --git a/src/hooks/use-debounce.ts b/src/hooks/use-debounce.ts new file mode 100644 index 0000000..0b2b850 --- /dev/null +++ b/src/hooks/use-debounce.ts @@ -0,0 +1,12 @@ +import { useState, useEffect } from 'react' + +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const handler = setTimeout(() => setDebouncedValue(value), delay) + return () => clearTimeout(handler) + }, [value, delay]) + + return debouncedValue +} \ No newline at end of file