- backend: FastAPI 应用工厂、统一错误信封、JWT、短信登录、工作区/成员/邀请、幂等中间件 - alembic 0001: workspace/member/invitation/audit_log/app_user + RLS 策略 + 双角色 GRANT - tests: 认证/工作区/租户隔离/幂等 四组用例(尚未在本机执行) - web: React18+Vite 骨架(登录、工作区、邀请落地页) - android: Compose 骨架(登录、工作区列表) - 2.Design: PRODUCT-PLAN / ROADMAP-M0-M1 / NAMING / DEVELOPMENT
54 lines
1.9 KiB
TypeScript
54 lines
1.9 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import { useAuth } from "../App";
|
|
import { Layout } from "../components/Layout";
|
|
import { ApiError, api, session } from "../lib/client";
|
|
import type { Workspace } from "../lib/types";
|
|
|
|
/** 邀请链接落地页:/join/:wsId/:code。未登录先去登录,登录后再回来。 */
|
|
export function JoinPage() {
|
|
const { wsId, code } = useParams<{ wsId: string; code: string }>();
|
|
const { user, loading, reload } = useAuth();
|
|
const navigate = useNavigate();
|
|
const [message, setMessage] = useState("正在加入工作区…");
|
|
const [failed, setFailed] = useState(false);
|
|
const started = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (loading) return;
|
|
if (!session.access) {
|
|
navigate(`/login?next=${encodeURIComponent(`/join/${wsId}/${code}`)}`, { replace: true });
|
|
return;
|
|
}
|
|
if (!user || started.current || !wsId || !code) return;
|
|
started.current = true;
|
|
|
|
void (async () => {
|
|
try {
|
|
const ws = await api.post<Workspace>(`/workspaces/${wsId}/join`, { code });
|
|
await reload();
|
|
setMessage(`已加入「${ws.name}」,正在跳转…`);
|
|
setTimeout(() => navigate(`/w/${wsId}`, { replace: true }), 600);
|
|
} catch (err) {
|
|
setFailed(true);
|
|
setMessage(err instanceof ApiError ? err.message : "加入失败");
|
|
}
|
|
})();
|
|
}, [loading, user, wsId, code, navigate, reload]);
|
|
|
|
return (
|
|
<Layout>
|
|
<div className="mx-auto mt-10 max-w-md">
|
|
<div className="card text-center">
|
|
<p className={failed ? "text-sm text-red-600" : "text-sm text-ink-700"}>{message}</p>
|
|
{failed && (
|
|
<button className="btn-ghost mt-4" onClick={() => navigate("/")}>
|
|
返回工作区列表
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Layout>
|
|
);
|
|
}
|