Day3
Step1:初始化环境与依赖(已完成)
代码/命令
- 安装依赖:
pnpm i
- 安装前端依赖:
pnpm --filter @aitodos/web add react-router-dom zustand
- 安装样式依赖:
pnpm --filter @aitodos/web add -D tailwindcss postcss autoprefixer
配置说明
apps/web/package.json 已包含:react-router-dom、zustand、tailwindcss@4
- 当前样式方案使用 Tailwind v4 + Vite 插件(非 v3 的
tailwindcss init -p 流程)
关键代码(示例)
pnpm --filter @aitodos/web add react-router-dom zustand
pnpm --filter @aitodos/web add -D tailwindcss postcss autoprefixer
Step2:接入 API-SDK(已完成)
代码
packages/api-sdk/src/index.ts
apps/web/src/lib/api.ts
代码说明
createApiClient(baseUrl) 统一封装 request(),处理 JSON、状态码、错误抛出。
- 已接入接口:
register / login / me
getProfile
listTodos / createTodo
listAiNews / addAiNewsToTodo
配置说明
apps/web/.env.local:VITE_API_BASE_URL=http://localhost:3000/api
VITE_ 前缀表示该变量可在浏览器端访问。
关键代码(示例)
import {
createApiClient } from "@aitodos/api-sdk";
const baseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:3000/api";
export const api = createApiClient(baseUrl);
Step3:Zustand 业务状态(已完成)
代码
apps/web/src/store/auth.store.ts
apps/web/src/store/todo.store.ts
apps/web/src/store/news.store.ts
代码说明
auth.store:登录、注册、恢复登录态、拉取 me/profile。
todo.store:待办拉取、新增。
news.store:资讯拉取、一键加入待办。
- 统一保留
loading、error 字段,页面直接订阅并展示状态。
配置说明
- 登录态以
localStorage(aitodos_token) 保存。
- 当前后端鉴权依赖
x-user-id,前端通过 dev-token-<userId> 解析用户 ID。
关键代码(示例)
const TOKEN_KEY = "aitodos_token";
export const extractUserIdFromToken = (token: string | null): string | null => {
if (!token) return null;
const prefix = "dev-token-";
if (!token.startsWith(prefix)) return null;
return token.slice(prefix.length) || null;
};
restoreSession: () => {
const token = localStorage.getItem(TOKEN_KEY);
set({
token, hydrated: true });
}
Step4:注册/登录页(已完成)
代码
apps/web/src/pages/LoginPage.tsx
apps/web/src/pages/RegisterPage.tsx
代码说明
- 注册后跳转登录页;登录成功跳转工作台。
- 表单提交已做类型安全处理,不再使用 deprecated 的
FormEvent 别名。
- 页面已展示错误反馈信息(如
Failed to fetch / 接口异常)。
- 已优化登录跳转逻辑:支持登录后回跳来源页面(如
/todos / /ai-news)。
- 已登录用户访问
/login 与 /register 时自动跳转到 /web。
配置说明
- 注册与登录都走
api-sdk,避免页面直接写 fetch.
关键代码(示例)
// apps/web/src/pages/LoginPage.tsx
const redirectTo = (location.state as { from?: string } | null)?.from ?? "/web";
const onSubmit = async (e: FormSubmitEvent) => {
e.preventDefault();
await login({ email, password });
navigate(redirectTo, { replace: true });
};
Step5:基础登录态与路由守卫(已完成)
代码
apps/web/src/App.tsx
apps/web/src/main.tsx
代码说明
main.tsx 使用 BrowserRouter 包裹 <App />,启用前端路由。
App.tsx 使用 ProtectedRoute:无 token 跳登录,有 token 访问业务页。
- 应用启动时执行
restoreSession(),并在有 token 时执行 fetchMe()。
- 路由结构调整为:
/ 门户页(公开) -> /web 工作台(受保护) -> /todos / /ai-news(受保护)。
配置说明
- 这是 SPA 路由守卫方案;后续迁移 Next.js SSR 时再升级为服务端鉴权策略.
关键代码(示例)
// apps/web/src/main.tsx
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
// apps/web/src/App.tsx
if (!hydrated) return <p className="p-6">恢复登录态中...</p>;
if (!token) return <Navigate to="/login" replace state={
{ from: location.pathname }} />;
Step6:首页工作台(已完成)
代码
apps/web/src/pages/DashboardPage.tsx
apps/web/src/pages/PortalPage.tsx
代码说明
- 展示个人信息:邮箱、昵称、角色。
- 展示业务入口:
/todos、/ai-news。
- 门户页作为软件介绍首页,提供“进入web端”按钮。
- 工作台模块化展示为:个人信息、TodoList、AI资讯。
- 已补齐三态反馈:
loading:加载个人信息中
error:加载失败提示
empty:无 user / 无 profile 提示
配置说明
- 工作台数据来源于
auth.store 的 user/profile,非页面内直接请求.
关键代码(示例)
// apps/web/src/pages/DashboardPage.tsx
{loading ? <p className="mb-3 text-sm text-slate-600">加载个人信息中...</p> : null}
{error ? <p className="mb-3 text-sm text-red-600">加载失败:{error}</p> : null}
{!loading && !error && !user ? (
<p className="mb-3 rounded bg-amber-50 px-3 py-2 text-sm text-amber-700">暂无用户信息,请重新登录后再试。</p>
) : null}
Step7:待办页(已完成)
代码
apps/web/src/pages/TodoPage.tsx
apps/web/src/store/todo.store.ts
apps/web/src/pages/components/TodoCreateForm.tsx
apps/web/src/pages/components/TodoQueueSection.tsx
apps/web/src/pages/components/DoneQueueSection.tsx
代码说明
- 已实现完整 CRUD:新增、编辑、完成/改回未完成、删除。
- 已实现未完成队列与已完成队列分区展示。
- 已有基础三态展示:
loading / empty / error。
- 页面结构已完成组件拆分:创建表单、未完成队列、已完成队列。
配置说明
- 待办列表数据来源于
todo.store 的 items,非页面内直接请求.
关键代码(示例)
// apps/web/src/pages/TodoPage.tsx
<TodoCreateForm
title={title}
description={description}
onTitleChange={setTitle}
onDescriptionChange={setDescription}
onSubmit={onCreate}
/>
<TodoQueueSection ... />
<DoneQueueSection ... />
Step8:AI 资讯页(已完成)
代码
apps/web/src/pages/AiNewsPage.tsx
apps/web/src/store/news.store.ts
代码说明
- 拉取并展示 AI 资讯列表(后端当前返回最新 5 条)。
- 每条资讯提供“一键加入待办”按钮。
- 点击后调用
addToTodo,并在按钮上展示“加入中...”防重复点击。
- 失败时给出错误消息,成功后给出“已加入待办,正在跳转...”提示并跳转待办页。
配置说明
- 资讯读取走后端
ai-news 缓存逻辑(Redis 日缓存),前端无需处理缓存细节.
关键代码(示例)
// apps/web/src/pages/AiNewsPage.tsx
const onAdd = async (newsId: string) => {
if (!userId) return;
setSubmittingId(newsId);
await addToTodo(newsId, userId);
navigate("/todos", { state: { message: "已从 AI 资讯加入待办" } });
};
Step9:一键加入待办全链路(已完成)
代码
apps/web/src/pages/AiNewsPage.tsx
apps/web/src/store/news.store.ts
apps/web/src/store/todo.store.ts
apps/web/src/pages/TodoPage.tsx
代码说明
- 已实现一键加入待办功能,并跳转到待办页。
- 已实现跨页成功提示:
AiNewsPage 通过路由 state 传递消息给 TodoPage。
TodoPage 显示消息后自动 2 秒消失,避免常驻。
配置说明
- 一键加入待办功能依赖
news.store 的 addToTodo 和 todo.store 的 createTodo.
Step10:联调验收(已完成)
验收清单
- [x] 注册登录完整可用
- [x] 工作台稳定显示个人信息
- [x] AI 资讯可见并支持一键加入待办
- [x] 待办页可见新增项
- [x] 待办支持编辑 / 完成 / 删除
关键配置补充(当前已生效)
CORS(后端)
- 文件:
apps/server/src/main.ts
- 说明:已加
app.enableCors(...),放行 http://localhost:5173,解决前后端联调跨域.
app.enableCors({
origin: ["http://localhost:5173"],
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "x-user-id"],
credentials: false
});
类型检查
- 命令:
pnpm --filter @aitodos/web type-check
- 当前状态:已通过(
tsc --noEmit 无报错).
当前结论
- Day3 主链路已打通并完成优化:门户 -> 登录 -> 工作台 -> AI 资讯 -> 加入待办 -> 待办 CRUD。
- 登录体验已优化:支持来源页回跳、已登录自动跳转、门户按登录态展示入口。
- Todo 页面已完成组件化拆分,便于后续迁移 Next.js / SSR 时复用.