/**
* 账号弹窗:登录 / 注册;团队管理:建团队、邀请成员、成员擅长标签。
*/
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 (
);
}
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={
团队成员共享同一份云端空间;擅长标签帮助快速找到对口的人
}
>
{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}
}
);
}