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
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user