Files
DadaoTask/web/src/pages/Join.tsx
T
Steven 0c53aa7062 feat(m0): 后端骨架 + Web/安卓前端骨架 + 设计文档
- 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
2026-10-28 02:38:53 +08:00

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>
);
}