From c6868b0dddb13ba20a66838680694c7921ecc220 Mon Sep 17 00:00:00 2001 From: logan Date: Fri, 18 Sep 2026 13:36:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=A2=E9=98=9F=E7=AE=A1=E7=90=86=EF=BC=9A?= =?UTF-8?q?=E6=88=90=E5=91=98=E6=93=85=E9=95=BF=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - team_members 新增 specialty 列(老库自动 ALTER 迁移,幂等) - 新接口:GET /api/teams/:id/members 成员列表(仅成员可看)、PUT /api/teams/:id/members/:uid 更新擅长(创建者可改他人,成员仅改自己);邀请接口支持带擅长 - TeamDialog 重构:团队卡片内成员列表(头像/名称/邮箱/角色/彩色擅长 chip,超 3 个折叠 +N)、行内擅长编辑器(10 个预设点选 + 自定义输入)、邀请时顺手点选擅长预设 - 文档 20-account 同步更新(含活动栏新导航说明) - curl E2E:邀请带擅长/创建者改他人/自改/越权与非成员 403 全部通过;53 前端测试 + Go 测试全绿 --- apps/server/data/opsxapi.db-shm | Bin 32768 -> 32768 bytes apps/server/data/opsxapi.db-wal | Bin 543872 -> 749872 bytes apps/server/internal/api/api.go | 72 ++++- .../internal/docs/content/20-account.md | 13 +- apps/server/internal/store/store.go | 63 +++- apps/web/src/styles.css | 21 ++ packages/ui/src/account.ts | 24 +- .../src/components/dialogs/AccountDialogs.tsx | 300 ++++++++++++++++-- packages/ui/src/workspaceStore.ts | 6 +- 9 files changed, 451 insertions(+), 48 deletions(-) diff --git a/apps/server/data/opsxapi.db-shm b/apps/server/data/opsxapi.db-shm index 93de15d5ee8f2d6c4321b749f7d4fa84617ba092..50a4c8d846a14b70574e186cd885fec98ef265f7 100644 GIT binary patch delta 465 zcmb76-7AA(96isIcdx~UZN-LVErpVj8#hXXT&RhZqTDFul3aRsG0Zm)^Eu4Ne1FVq z{sA}KNcamzxfr5w%kSNG!KG8D-#O=ZI*P&-h4*JgilMjGML0p}AzD zo|u}@5?viJMf1YNvNN>SpHvO-z6$levW~Gi(qX3FR+lk$K>CWQH?^b>A#aYbi^(e#uCa_HQlZ&?&ona7@@M9C( z*vC1=Q7&Fvb@C9Vu!1cL-nn>gv&$oxK>#OeMthV?i`YY0WgRbE_F@{V2*!$L3v6(r f1m$SPC}y#aP<-;S%Oa1#hYg%k@W9nbx83{$hn|BU delta 295 zcmZo@U}|V!s+V}A%K!qLK+MR%AkYG&C4uUT3zJA>z zQq=>E0t1k_|B(Pxn28~4W1~3J<`m`vyU7L2XEtAP^kCea#Jr7h^CcG(5bG4<<_qpH z);TXXkk})p$>02hfQ&axlfMK)7|fgunwt}ue=|+~5Y7ga`oT2$T@;k@CRPq8@_=db ws|1K*Rv`u*1|y*IpG@2$3{nir4EhX~452`Q&rFk_WQqV)zhc__C`V2h0KQvgegFUf diff --git a/apps/server/data/opsxapi.db-wal b/apps/server/data/opsxapi.db-wal index 5b96ffa0c0c4572fa731b9c5437f89934fb9badf..e0c877d45d47b15d7d83702c0f06cecf766d8ee1 100644 GIT binary patch delta 5375 zcmd6rdr(x@9mnt8``TT&`$K&2FhqO+BI3Sx?@ppdTbqe7R+6GLjmEpX_Y#XdwMMC^JA+jjNv#j4s3b@wsEL3i+L}qmG&LD}&b?&AqDCYB(LZ*W z0nRzU^F5#Q{hr?zy*a%4qvWDuvRG4071PB`F-(-ut|O%Mc)K1BYHRuuhgRejtc zy>Zuo*{94roF!f0YY-7i%VlUcMLE-#?=l0}!MY_DQ|#dDzq%dREZz3@Kl;8M9U z>{?X$Vm5vht3?WtJe{nHq)}9IIt9UQb=llrw;DnZX53kC?MFPHt&)_C}3KyZnU(rQ30m=HlnZ-PwXh8oF(Bkb`=`UuFzZk6!E23a0 zT!kZ01kF(9f2@<54}%9$$z?_W_ldfTat9s>EvC z+}1W#uOd>fKn3+kwEZv=D29k2>3k_K-;DK2xd&&Fx#{{fE4FE7;z?+z?coe+(KI_@ zR2Z$0Mg>Sqxd(fZe;|sY{wnf?z$EHY5J%^O5s$aU8VvhP4$sP#yK)tNt6sa2!LCB&C9z z6a=ogc{11DeIQYtBBM9B83^6SJE>F{gVF6(X3KW~b9Gh&I{m5IrW7S9E(F4ncOBC47e(bNp{~ zbJ?2McUS)XW=#PUV}*768Ul=l*-*f*F?7R0{){2QU(`b9@xGr%u*{_XuB-F6e#K4H zj1d1l<&|wE@6T4&#BuO7@~|8530#5ma26V%9c_1S4;(rL4y|u=A}d7Rtp4QqTJ7*l z_q#Im_qtCazA{1Xld>t*aN@?fH&x@P>Tw^aLUokGgeaOJl1RF_t{FcF(v-|cFDpdBXSQwW@ZEDaiw>(6^)JIks&*X{18EsgD1zXqw*4~^o?IHdvy z_7>f6D`U&T-{P|LyzVa1!Rt_Y*Fy`Z>Xq;iMXxNH9d@Tv7EDfy#bmb#ve}8!L$cak zUawU)Sv?k6w3@v(m&fFkoubET@meIo>J?3PlLjC7Lj+OOeUfNI=^TDb?3I%f@ZsqB zDSeWNpQ2__lA1+@{oRIs3XP)lp%YEp6Qw92ZN!^FqGelidxGTE@e0(%D8+Yebv9`N^Nakey*66F6BOj zpPJ*m(n2$OnmDX(x7%ztiDt=VLZCV9X74DM#U)zoW{1t?63ocPT@G1v%N~=*ZV@G$ z)#GqG-I8RJEdm13<@QRF#cXm&R*T8vmF*rYMsqRkKg2MflhG4mrpr48qtZY!P(gqn zF2KK_0#2bSH^7j=l##rnG{%=5!OR#Qxb?NaxG}DqyjS z7vxE~d5mw^Kux4_%t0+FGk$#g=O3S0`iK`YYOPQr1WZ@Q4fN$_`eHj zC;XVE2vYY50(ohCs6fRn2;0cwy@iAxx~rv!9zt9BJu>wW#njzym(ybMxCOH;S!K~B zSX^eS)9vsIqTOP#Ic*k~&Ek+G(e9R5(K9QH3YJxNcs=~Aw9~3xKBWg z`ml9V%TtEw*YHt8Rq?FS5;F+1e&sF>x)dDNA$kf8=n2Gl|&;J}#$g|U>< yrzG|IzOHlq_KpqPuI@g3b$3PVq-Xk>K 60 { + return string(r[:60]) + } + return s +} + /* ---------------- Mock 发布 ---------------- */ func (a *API) handlePublishMock(c *gin.Context, user *store.User) { diff --git a/apps/server/internal/docs/content/20-account.md b/apps/server/internal/docs/content/20-account.md index 5a19db7..33936e7 100644 --- a/apps/server/internal/docs/content/20-account.md +++ b/apps/server/internal/docs/content/20-account.md @@ -4,14 +4,14 @@ ## 注册 / 登录 -顶栏右上 **登录** 按钮打开弹窗: +最左**活动栏**底部 **登录** 按钮打开弹窗: - 注册只需 昵称 + 邮箱 + 密码(≥ 6 位); - 登录态保存在本机,令牌失效会自动清除。 ## 空间切换 -登录后顶栏出现 **用户胶囊**(头像首字母 + 当前空间名 + 同步状态),点击弹出空间列表: +活动栏**顶部空间徽标**(未登录时为产品 Logo,云端空间显示彩色徽标)点击弹出空间列表: | 空间 | 说明 | | --- | --- | @@ -20,20 +20,21 @@ | 团队 · ×× | 团队共享空间,成员读写同一份数据 | - 首次切入空的云端空间时,会自动把**当前本地数据播种上去**; -- 在云端空间内的所有改动(接口、环境、测试用例、设置)**自动同步**(改动后约 1.5 秒),胶囊显示「已同步 / 同步中 / 同步失败」; +- 在云端空间内的所有改动(接口、环境、测试用例、设置)**自动同步**(改动后约 1.5 秒),活动栏底部头像上的状态点显示 已同步 / 同步中(闪烁)/ 同步失败(红); - 切回本地空间时恢复离开前的本地快照,两端互不覆盖; - 同步策略为**后写覆盖**:多人同时编辑以最后一次保存为准。 ## 团队管理 -用户胶囊旁 **⋯** 菜单 → 团队管理: +活动栏账号菜单或空间菜单 → **团队管理**: - **新建团队**:创建后自动获得同名共享空间; -- **添加成员**:输入已注册成员邮箱(创建者可操作),成员登录后即可看到并切换到该团队空间。 +- **添加成员**:输入已注册成员邮箱(创建者可操作),邀请时可顺手点选**擅长**预设;成员登录后即可看到并切换到该团队空间; +- **成员与擅长**:每个团队卡片展示成员列表(头像 / 名称 / 角色 / 擅长标签)。擅长支持预设点选(前端、后端、全栈、测试、运维、移动端、数据库、AI、接口设计、安全)与自定义输入,空格或顿号分隔;创建者可修改任何成员的擅长,成员只能修改自己的(行尾「擅长」按钮)。 ## 数据与安全 - 密码加盐哈希后存储;令牌为随机 Bearer Token,可随时退出登录作废; - 云端空间单份数据上限 20MB; -- 团队空间权限:全体成员可读写,仅创建者可管理成员; +- 团队空间权限:全体成员可读写,仅创建者可管理成员与修改他人擅长; - 不登录不影响任何本地功能。 diff --git a/apps/server/internal/store/store.go b/apps/server/internal/store/store.go index 9821517..4ecdeca 100644 --- a/apps/server/internal/store/store.go +++ b/apps/server/internal/store/store.go @@ -71,6 +71,7 @@ CREATE TABLE IF NOT EXISTS team_members ( team_id INTEGER NOT NULL REFERENCES teams(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, role TEXT NOT NULL DEFAULT 'member', + specialty TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL, PRIMARY KEY (team_id, user_id) ); @@ -107,6 +108,8 @@ CREATE INDEX IF NOT EXISTS idx_workspaces_owner ON workspaces(owner_id); if _, err := s.db.Exec(`ALTER TABLE workspaces ADD COLUMN mock_token TEXT`); err == nil { _, _ = s.db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_workspaces_mock_token ON workspaces(mock_token)`) } + // 老库补擅长列(幂等) + _, _ = s.db.Exec(`ALTER TABLE team_members ADD COLUMN specialty TEXT NOT NULL DEFAULT ''`) rows, err := s.db.Query(`SELECT id FROM workspaces WHERE mock_token IS NULL OR mock_token = ''`) if err != nil { return err @@ -289,14 +292,68 @@ WHERE m.user_id = ? ORDER BY t.id`, userID) return out, rows.Err() } -func (s *Store) AddTeamMember(teamID int64, email string) (*User, error) { +// TeamMember 团队成员(含擅长标签,多个以空白或顿号分隔) +type TeamMember struct { + UserID int64 `json:"userId"` + Name string `json:"name"` + Email string `json:"email"` + Role string `json:"role"` + Specialty string `json:"specialty"` +} + +// TeamMembers 团队成员列表:创建者在最前,其余按加入时间 +func (s *Store) TeamMembers(teamID int64) ([]TeamMember, error) { + rows, err := s.db.Query(` +SELECT m.user_id, IFNULL(u.name, ''), IFNULL(u.email, ''), m.role, m.specialty +FROM team_members m LEFT JOIN users u ON u.id = m.user_id +WHERE m.team_id = ? +ORDER BY CASE m.role WHEN 'owner' THEN 0 ELSE 1 END, m.created_at`, teamID) + if err != nil { + return nil, err + } + defer rows.Close() + var out []TeamMember + for rows.Next() { + var m TeamMember + if err := rows.Scan(&m.UserID, &m.Name, &m.Email, &m.Role, &m.Specialty); err != nil { + return nil, err + } + out = append(out, m) + } + return out, rows.Err() +} + +// IsTeamMember 是否为团队成员(创建者亦为成员) +func (s *Store) IsTeamMember(teamID, userID int64) (bool, error) { + var one int + err := s.db.QueryRow(`SELECT 1 FROM team_members WHERE team_id = ? AND user_id = ?`, teamID, userID).Scan(&one) + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return err == nil, err +} + +// UpdateMemberSpecialty 更新成员擅长(须先确认调用者是创建者或本人) +func (s *Store) UpdateMemberSpecialty(teamID, userID int64, specialty string) error { + res, err := s.db.Exec(`UPDATE team_members SET specialty = ? WHERE team_id = ? AND user_id = ?`, specialty, teamID, userID) + if err != nil { + return err + } + n, _ := res.RowsAffected() + if n == 0 { + return ErrNotFound + } + return nil +} + +func (s *Store) AddTeamMember(teamID int64, email string, specialty string) (*User, error) { u, err := s.UserByEmail(email) if err != nil { return nil, err } _, err = s.db.Exec( - `INSERT OR IGNORE INTO team_members(team_id, user_id, role, created_at) VALUES(?,?,'member',?)`, - teamID, u.ID, time.Now().Unix(), + `INSERT OR IGNORE INTO team_members(team_id, user_id, role, specialty, created_at) VALUES(?,?,'member',?,?)`, + teamID, u.ID, specialty, time.Now().Unix(), ) return u, err } diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 1701fcc..432e434 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -596,6 +596,27 @@ background: var(--border); } + /* ---------- 团队成员行与擅长标签 ---------- */ + .member-row { + @apply flex items-center gap-2.5 rounded-lg px-1.5 py-2; + } + .member-row:hover { background: var(--panel-2); } + .spec-chip { + @apply inline-flex shrink-0 items-center rounded-full border px-2 py-px text-[10px] font-medium; + } + .spec-preset { + @apply rounded-full border px-2 py-0.5 text-[11px] transition; + color: var(--text-dim); + border-color: var(--border); + background: transparent; + } + .spec-preset:hover { color: var(--text); border-color: var(--accent); } + .spec-preset.active { + color: var(--accent-strong); + border-color: var(--accent); + background: color-mix(in srgb, var(--accent) 10%, transparent); + } + /* ---------- 方法 chip 着色 ---------- */ .ops-chip.ops-method-GET { background: color-mix(in srgb, var(--m-GET) 14%, transparent); color: var(--m-GET); } .ops-chip.ops-method-POST { background: color-mix(in srgb, var(--m-POST) 16%, transparent); color: var(--m-POST); } diff --git a/packages/ui/src/account.ts b/packages/ui/src/account.ts index 069d39d..5721420 100644 --- a/packages/ui/src/account.ts +++ b/packages/ui/src/account.ts @@ -60,10 +60,30 @@ export function createTeam(token: string, name: string) { return call("/teams", token, { method: "POST", body: JSON.stringify({ name }) }); } -export function addTeamMember(token: string, teamId: number, email: string) { +export function addTeamMember(token: string, teamId: number, email: string, specialty?: string) { return call<{ ok: boolean; member: AccountUser }>(`/teams/${teamId}/members`, token, { method: "POST", - body: JSON.stringify({ email }), + body: JSON.stringify({ email, specialty: specialty ?? "" }), + }); +} + +/** 团队成员(specialty:擅长标签,空格或顿号分隔) */ +export type AccountTeamMember = { + userId: number; + name: string; + email: string; + role: string; + specialty: string; +}; + +export function fetchTeamMembers(token: string, teamId: number) { + return call<{ members: AccountTeamMember[] }>(`/teams/${teamId}/members`, token); +} + +export function updateMemberSpecialty(token: string, teamId: number, userId: number, specialty: string) { + return call<{ ok: boolean }>(`/teams/${teamId}/members/${userId}`, token, { + method: "PUT", + body: JSON.stringify({ specialty }), }); } diff --git a/packages/ui/src/components/dialogs/AccountDialogs.tsx b/packages/ui/src/components/dialogs/AccountDialogs.tsx index ea0e7d7..0177c70 100644 --- a/packages/ui/src/components/dialogs/AccountDialogs.tsx +++ b/packages/ui/src/components/dialogs/AccountDialogs.tsx @@ -1,7 +1,8 @@ /** - * 账号弹窗:登录 / 注册;团队管理:建团队、邀请成员。 + * 账号弹窗:登录 / 注册;团队管理:建团队、邀请成员、成员擅长标签。 */ -import { useState } from "react"; +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"; @@ -103,6 +104,92 @@ export function AuthDialog() { /* ---------------- 团队管理 ---------------- */ +/** 擅长预设(点击即选/取消) */ +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); @@ -111,10 +198,42 @@ export function TeamDialog() { 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 [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); @@ -136,8 +255,10 @@ export function TeamDialog() { setBusy(true); setError(""); try { - await addTeamMember(teamId, email); + 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)); @@ -146,14 +267,32 @@ export function TeamDialog() { } }; + 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="560px" + width="600px" footer={
- 团队成员共享同一份云端空间,切换即协同 + 团队成员共享同一份云端空间;擅长标签帮助快速找到对口的人 @@ -181,34 +320,131 @@ export function TeamDialog() {
还没有团队,创建一个即可获得团队共享空间
) : (
- {account.teams.map((t) => ( -
-
- - - - {t.name} - - {t.role === "owner" ? "创建者" : "成员"} - - 创建者 {t.owner} -
- {t.role === "owner" && ( -
- setInviteEmail((m) => ({ ...m, [t.id]: e.target.value }))} - onKeyDown={(e) => e.key === "Enter" && doInvite(t.id)} - /> - + {account.teams.map((t) => { + const list = members[t.id]; + return ( +
+
+ + + + {t.name} + + {t.role === "owner" ? "创建者" : "成员"} + + + {list ? `${list.length} 名成员 · 创建者 ${t.owner}` : `创建者 ${t.owner}`} +
- )} -
- ))} + + {/* 成员与擅长 */} +
+ {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 ( + + ); + })} +
+ )} +
+ )} +
+ ); + })}
)} diff --git a/packages/ui/src/workspaceStore.ts b/packages/ui/src/workspaceStore.ts index c4cfb32..d7455e6 100644 --- a/packages/ui/src/workspaceStore.ts +++ b/packages/ui/src/workspaceStore.ts @@ -150,7 +150,7 @@ type WorkspaceStore = PersistedShape & { authLogout(): Promise; refreshAccount(): Promise; createTeam(name: string): Promise; - addTeamMember(teamId: number, email: string): Promise; + addTeamMember(teamId: number, email: string, specialty?: string): Promise; /** 切换空间:null=本地;服务端空间为空时用当前数据播种 */ switchSpace(spaceId: number | null): Promise; syncNow(): Promise; @@ -516,10 +516,10 @@ export const useWorkspace = create((set, get) => { await get().refreshAccount(); }, - async addTeamMember(teamId, email) { + async addTeamMember(teamId, email, specialty) { const { token } = get().account; if (!token) throw new Error("未登录"); - await addTeamMemberApi(token, teamId, email.trim()); + await addTeamMemberApi(token, teamId, email.trim(), specialty?.trim() || undefined); await get().refreshAccount(); },