Files
DadaoTask/web/src/pages/Join.tsx
T
dadao 5be55fdccb feat(web): 重做设计系统与全部页面,前端观感对齐当下审美
用户反馈"UI 太丑、太简陋"。不是换配色,而是先立 token 再让页面去用:

- tailwind.config.js:纸白 paper / 墨绿 primary / 陶土橙 clay + 语义色
  sage/wheat/rust/sky,字号阶、圆角阶、投影、动效
- index.css:卡片、按钮五种变体、输入、pill/chip、导航、骨架屏
- 新增 icons.tsx(线性图标集)与 ui.tsx(Button/Sheet/Toast/空态/骨架/PageHeader)
- 新增 AppShell 取代 Layout + WorkspaceNav:桌面侧栏 + 移动顶部横条,工单未读角标
- 六个页面全部重写:登录、首页、任务、看板、工单、知识、成员、邀请落地
- 详情抽屉改用 Sheet,工单态补齐需求人/办理人/退回次数/动作按钮

e2e 因 DOM 升级同步调整(抽屉按 role=dialog 定位、下拉改用 aria-label),
三套用例(7 + 8 + 7 项)全部重跑通过;tsc 0 错,构建 45 modules / gzip 76.70 kB。
2026-10-28 18:37:35 +08:00

77 lines
2.9 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useAuth } from "../App";
import { Check, Logo, Spinner } from "../components/icons";
import { Button, InlineAlert } from "../components/ui";
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 [done, setDone] = useState(false);
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();
setDone(true);
setMessage(`已加入「${ws.name}」,正在跳转…`);
window.setTimeout(() => navigate(`/w/${wsId}`, { replace: true }), 700);
} catch (err) {
setFailed(true);
setMessage(err instanceof ApiError ? err.message : "加入失败");
}
})();
}, [loading, user, wsId, code, navigate, reload]);
return (
<main className="grid min-h-full place-items-center bg-paper-100 px-6">
<div className="w-full max-w-sm animate-slide-up">
<div className="mb-6 flex justify-center">
<Logo size={40} />
</div>
<div className="card p-7 text-center">
<div className="mx-auto mb-4 grid h-12 w-12 place-items-center rounded-2xl bg-primary-50 text-primary-700">
{failed ? (
<span className="text-xl">!</span>
) : done ? (
<Check size={22} />
) : (
<Spinner size={20} />
)}
</div>
<p className="text-sm leading-relaxed text-ink-700">{message}</p>
<p className="mt-1 text-2xs text-ink-400">
{failed ? "链接可能已失效,或你已经加入过了。" : "邀请码一次有效"}
</p>
{failed && (
<>
<div className="mt-4">
<InlineAlert>可以把邀请码发给同事,让他重新生成一条链接。</InlineAlert>
</div>
<Button variant="ghost" className="mt-4 w-full" onClick={() => navigate("/")}>
返回工作区列表
</Button>
</>
)}
</div>
</div>
</main>
);
}