pnpm ipnpm --filter @aitodos/web add react-router-dom zustandpnpm --filter @aitodos/web add -D tailwindcss postcss autoprefixerapps/web/package.json 已包含:react-router-dom、zustand、tailwindcss@4tailwindcss init -p 流程)pnpm --filter @aitodos/web add react-router-dom zustand
pnpm --filter @aitodos/web add -D tailwindcss postcss autoprefixerpackages/api-sdk/src/index.tsapps/web/src/lib/api.tscreateApiClient(baseUrl) 统一封装 request(),处理 JSON、状态码、错误抛出。register / login / megetProfilelistTodos / createTodolistAiNews / addAiNewsToTodoapps/web/.env.local:VITE_API_BASE_URL=http://localhost:3000/apiVITE_ 前缀表示该变量可在浏览器端访问。// apps/web/src/lib/api.ts
import { createApiClient } from "@aitodos/api-sdk";
const baseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:3000/api";
export const api = createApiClient(baseUrl);apps/web/src/store/auth.store.tsapps/web/src/store/todo.store.tsapps/web/src/store/news.store.tsauth.store:登录、注册、恢复登录态、拉取 me/profile。todo.store:待办拉取、新增。news.store:资讯拉取、一键加入待办。loading、error 字段,页面直接订阅并展示状态。localStorage(aitodos_token) 保存。x-user-id,前端通过 dev-token-<userId> 解析用户 ID。// apps/web/src/store/auth.store.ts
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 });
}apps/web/src/pages/LoginPage.tsxapps/web/src/pages/RegisterPage.tsxFormEvent 别名。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 });
};apps/web/src/App.tsxapps/web/src/main.tsxmain.tsx 使用 BrowserRouter 包裹 <App />,启用前端路由。App.tsx 使用 ProtectedRoute:无 token 跳登录,有 token 访问业务页。restoreSession(),并在有 token 时执行 fetchMe()。/ 门户页(公开) -> /web 工作台(受保护) -> /todos / /ai-news(受保护)。// 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 }} />;apps/web/src/pages/DashboardPage.tsxapps/web/src/pages/PortalPage.tsx/todos、/ai-news。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}apps/web/src/pages/TodoPage.tsxapps/web/src/store/todo.store.tsapps/web/src/pages/components/TodoCreateForm.tsxapps/web/src/pages/components/TodoQueueSection.tsxapps/web/src/pages/components/DoneQueueSection.tsxloading / empty / error。todo.store 的 items,非页面内直接请求.// apps/web/src/pages/TodoPage.tsx
<TodoCreateForm
title={title}
description={description}
onTitleChange={setTitle}
onDescriptionChange={setDescription}
onSubmit={onCreate}
/>
<TodoQueueSection ... />
<DoneQueueSection ... />apps/web/src/pages/AiNewsPage.tsxapps/web/src/store/news.store.tsaddToTodo,并在按钮上展示“加入中...”防重复点击。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 资讯加入待办" } });
};apps/web/src/pages/AiNewsPage.tsxapps/web/src/store/news.store.tsapps/web/src/store/todo.store.tsapps/web/src/pages/TodoPage.tsxAiNewsPage 通过路由 state 传递消息给 TodoPage。TodoPage 显示消息后自动 2 秒消失,避免常驻。news.store 的 addToTodo 和 todo.store 的 createTodo.apps/server/src/main.tsapp.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-checktsc --noEmit 无报错).原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。