feat: Phase 0 — monorepo 骨架 + 全表迁移 + FastAPI/Next 骨架 + CI

- uv(workspace) + pnpm monorepo;docker-compose(pg+api+web)
- SQLAlchemy 16 MVP 表 + Alembic 初版迁移(无漂移,users stub)
- FastAPI 骨架:统一错误信封(带 request_id) + structlog + /jobs/:id + OpenAPI
- Next.js 骨架:纸感主题 token + OpenAPI→TS 客户端代码生成(gen:api)
- CI(ruff/mypy/pytest + pg service + alembic 漂移校验)
- 四份设计规格(PRODUCT/UX/ARCHITECTURE/DEV_PLAN) + CLAUDE.md
This commit is contained in:
Yaojia Wang
2026-06-18 11:38:28 +02:00
commit d3dc620a71
74 changed files with 12960 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
"use client";
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
type ReactNode,
} from "react";
type ToastKind = "info" | "error" | "success";
interface ToastItem {
id: number;
message: string;
kind: ToastKind;
}
type ShowToast = (message: string, kind?: ToastKind) => void;
const ToastContext = createContext<ShowToast | null>(null);
const TOAST_TTL_MS = 4000;
export function ToastProvider({ children }: { children: ReactNode }) {
const [items, setItems] = useState<ToastItem[]>([]);
const show = useCallback<ShowToast>((message, kind = "info") => {
const id = Date.now() + Math.random();
setItems((prev) => [...prev, { id, message, kind }]);
setTimeout(() => {
setItems((prev) => prev.filter((t) => t.id !== id));
}, TOAST_TTL_MS);
}, []);
const value = useMemo(() => show, [show]);
return (
<ToastContext.Provider value={value}>
{children}
<div
className="pointer-events-none fixed bottom-6 right-6 z-50 flex flex-col gap-2"
aria-live="polite"
role="status"
>
{items.map((t) => (
<div
key={t.id}
className={`pointer-events-auto rounded border px-4 py-2 text-sm shadow-paper ${
t.kind === "error"
? "border-conflict bg-panel text-conflict"
: t.kind === "success"
? "border-pass bg-panel text-pass"
: "border-line bg-panel text-ink"
}`}
>
{t.message}
</div>
))}
</div>
</ToastContext.Provider>
);
}
export function useToast(): ShowToast {
const ctx = useContext(ToastContext);
if (!ctx) {
// 容错:未挂 Provider 时退化为 no-op不应在生产路径发生
return () => undefined;
}
return ctx;
}