/** * 账号弹窗:登录 / 注册;团队管理:建团队、邀请成员、成员擅长标签。 */ import { fetchTeamMembers, updateMemberSpecialty, type AccountTeamMember } from "../../account"; import { useEffect, useState } from "react"; import { useWorkspace } from "../../workspaceStore"; import { Modal, cx } from "../common"; import { Icon } from "../icons"; /* ---------------- 登录 / 注册 ---------------- */ export function AuthDialog() { const authLogin = useWorkspace((s) => s.authLogin); const authRegister = useWorkspace((s) => s.authRegister); const setDialog = useWorkspace((s) => s.setDialog); const [mode, setMode] = useState<"login" | "register">("login"); const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const submit = async () => { setBusy(true); setError(""); try { if (mode === "login") await authLogin(email, password); else await authRegister(name, email, password); setDialog(null); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); } }; return ( setDialog(null)} width="400px" footer={
} >
数据保存在本机 SQLite,仅用于云端空间同步
{mode === "register" && (
setName(e.target.value)} />
)}
setEmail(e.target.value)} onKeyDown={(e) => e.key === "Enter" && submit()} autoFocus />
setPassword(e.target.value)} onKeyDown={(e) => e.key === "Enter" && submit()} />
{error &&
{error}
}
); } /* ---------------- 团队管理 ---------------- */ /** 擅长预设(点击即选/取消) */ const SPECIALTY_PRESETS = ["前端", "后端", "全栈", "测试", "运维", "移动端", "数据库", "AI", "接口设计", "安全"]; const SPEC_PALETTE = ["#3b82f6", "#8b5cf6", "#10b981", "#f59e0b", "#f43f5e", "#06b6d4", "#ec4899", "#84cc16"]; function specColor(label: string): string { let h = 0; for (let i = 0; i < label.length; i++) h = (h * 31 + label.charCodeAt(i)) >>> 0; return SPEC_PALETTE[h % SPEC_PALETTE.length]; } function splitSpecialty(s: string): string[] { return s .split(/[\s,,、/]+/) .map((x) => x.trim()) .filter(Boolean); } function SpecChip({ label }: { label: string }) { const color = specColor(label); return ( {label} ); } /** 擅长编辑器:预设点选 + 自定义输入 */ function SpecialtyEditor({ value, onChange, onSave, onCancel, busy, }: { value: string; onChange: (v: string) => void; onSave: () => void; onCancel: () => void; busy: boolean; }) { const picked = splitSpecialty(value); const toggle = (p: string) => { const next = picked.includes(p) ? picked.filter((x) => x !== p) : [...picked, p]; onChange(next.join(" ")); }; return (
onChange(e.target.value)} onKeyDown={(e) => e.key === "Enter" && onSave()} autoFocus />
{SPECIALTY_PRESETS.map((p) => ( ))}
); } export function TeamDialog() { const account = useWorkspace((s) => s.account); const createTeam = useWorkspace((s) => s.createTeam); const renameTeam = useWorkspace((s) => s.renameTeam); const deleteTeam = useWorkspace((s) => s.deleteTeam); const addTeamMember = useWorkspace((s) => s.addTeamMember); const setDialog = useWorkspace((s) => s.setDialog); const [teamName, setTeamName] = useState(""); const [inviteEmail, setInviteEmail] = useState>({}); const [inviteSpecialty, setInviteSpecialty] = useState>({}); const [members, setMembers] = useState>({}); const [membersLoading, setMembersLoading] = useState(false); const [editing, setEditing] = useState<{ teamId: number; userId: number } | null>(null); const [editValue, setEditValue] = useState(""); const [renaming, setRenaming] = useState(null); const [renameValue, setRenameValue] = useState(""); const [confirmDelete, setConfirmDelete] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [okMsg, setOkMsg] = useState(""); const token = account.token; const teamCount = account.teams.length; const loadMembers = async () => { if (!token || useWorkspace.getState().account.teams.length === 0) return; setMembersLoading(true); try { const teams = useWorkspace.getState().account.teams; const next: Record = {}; await Promise.all( teams.map(async (t) => { next[t.id] = (await fetchTeamMembers(token, t.id)).members ?? []; }), ); setMembers(next); } catch { /* 拉取失败时仍展示团队概要 */ } finally { setMembersLoading(false); } }; useEffect(() => { void loadMembers(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [token, teamCount]); const doCreate = async () => { if (!teamName.trim()) return; setBusy(true); setError(""); try { await createTeam(teamName); setTeamName(""); setOkMsg("团队已创建,左侧空间列表可切换到团队空间"); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); } }; const doRename = async (teamId: number) => { if (!renameValue.trim()) return; setBusy(true); setError(""); try { await renameTeam(teamId, renameValue); setRenaming(null); setOkMsg("团队已重命名"); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); } }; const doDelete = async (teamId: number) => { setBusy(true); setError(""); try { const name = account.teams.find((t) => t.id === teamId)?.name ?? ""; await deleteTeam(teamId); setConfirmDelete(null); setMembers((m) => { const next = { ...m }; delete next[teamId]; return next; }); setOkMsg(`团队「${name}」已删除(含共享空间与 Mock 数据)`); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); } }; const doInvite = async (teamId: number) => { const email = (inviteEmail[teamId] ?? "").trim(); if (!email) return; setBusy(true); setError(""); try { await addTeamMember(teamId, email, inviteSpecialty[teamId] ?? ""); setInviteEmail((m) => ({ ...m, [teamId]: "" })); setInviteSpecialty((m) => ({ ...m, [teamId]: "" })); await loadMembers(); setOkMsg(`已添加成员 ${email}`); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); } }; const saveSpecialty = async (teamId: number, userId: number) => { if (!token) return; setBusy(true); setError(""); try { await updateMemberSpecialty(token, teamId, userId, editValue); setEditing(null); await loadMembers(); setOkMsg("擅长已更新"); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); } }; const me = account.user; return ( setDialog(null)} width="600px" footer={
团队成员共享同一份云端空间;擅长标签帮助快速找到对口的人
} >
setTeamName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && doCreate()} />
{account.teams.length === 0 ? (
还没有团队,创建一个即可获得团队共享空间
) : (
{account.teams.map((t) => { const list = members[t.id]; return (
{renaming === t.id ? (
setRenameValue(e.target.value)} onKeyDown={(e) => e.key === "Enter" && doRename(t.id)} autoFocus />
) : ( <> {t.name} {t.role === "owner" ? "创建者" : "成员"} {list ? `${list.length} 名成员 · 创建者 ${t.owner}` : `创建者 ${t.owner}`} {t.role === "owner" && ( <> )} )}
{confirmDelete === t.id && (
将永久删除团队「{t.name}」及其共享空间、成员关系、已发布 Mock 接口,不可恢复。确定删除?
)} {/* 成员与擅长 */}
{list === undefined && (membersLoading ? (
成员加载中…
) : null)} {list?.map((m) => { const canEdit = t.role === "owner" || m.userId === me?.id; const isEditing = editing?.teamId === t.id && editing.userId === m.userId; return (
{isEditing ? (
void saveSpecialty(t.id, m.userId)} onCancel={() => setEditing(null)} busy={busy} />
) : ( <> {m.name.slice(0, 1).toUpperCase()} {m.name} {m.userId === me?.id && (我)} {m.email} {m.role === "owner" ? "创建者" : "成员"} {splitSpecialty(m.specialty).slice(0, 3).map((s) => ( ))} {splitSpecialty(m.specialty).length > 3 && ( +{splitSpecialty(m.specialty).length - 3} )} {splitSpecialty(m.specialty).length === 0 && ( 未设置擅长 )} {canEdit && ( )} )}
); })}
{t.role === "owner" && (
setInviteEmail((m) => ({ ...m, [t.id]: e.target.value }))} onKeyDown={(e) => e.key === "Enter" && doInvite(t.id)} />
{(inviteEmail[t.id] ?? "").trim() !== "" && (
擅长: {SPECIALTY_PRESETS.map((p) => { const picked = splitSpecialty(inviteSpecialty[t.id] ?? "").includes(p); return ( ); })}
)}
)}
); })}
)} {error &&
{error}
} {okMsg &&
{okMsg}
}
); }