fix(admin): 超管补「升为主管理员」入口 — 不然主管撤了加不回去

之前只能撤主管不能升,撤完只能新建账号或重建团队,不合理。
后端 admin_team_member_role_view 早就支持 is_team_owner=true(L1254-1260,
设 owner=True 时自动同时 admin=True);前端 setMemberRole API 只传
is_team_admin,从没用过 is_team_owner 参数。

修法:
- lib/api.ts 加 adminApi.setMemberAsOwner(teamId, memberId)
- TeamsPage 副管/成员行 role badge 旁加小灰字 "→主管" 按钮
- 点击 confirm 提示"不会自动降级现有主管,需自己先撤旧主管再升新主管"
  (后端没强制约束一团队一主管,降级逻辑交给操作员判断)

UX 流程:
- 主管 → 单击 badge = 撤销 (现有)
- 副管 → 单击 badge = 取消副管(变成员)(现有) + [→主管] 升主管
- 成员 → 单击文字 = 升副管 (现有)              + [→主管] 升主管

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-05-12 21:08:30 +08:00
parent 2289ce7d30
commit 2f6d3a60cc
2 changed files with 50 additions and 14 deletions

View File

@ -210,6 +210,10 @@ export const adminApi = {
setMemberRole: (teamId: number, memberId: number, isTeamAdmin: boolean) =>
api.patch(`/admin/teams/${teamId}/members/${memberId}/role`, { is_team_admin: isTeamAdmin }),
// 升某成员为主管(后端会自动同时设 is_team_admin=true)
setMemberAsOwner: (teamId: number, memberId: number) =>
api.patch(`/admin/teams/${teamId}/members/${memberId}/role`, { is_team_owner: true }),
// User management
createUser: (data: {
username: string;

View File

@ -833,21 +833,53 @@ export function TeamsPage() {
} catch { showToast('操作失败'); }
}}></span>
) : m.is_team_admin ? (
<span className={styles.adminBadge} style={{ cursor: 'pointer' }} title="点击取消副管理员" onClick={async () => {
try {
await adminApi.setMemberRole(detailTeam!.id, m.id, false);
showToast('已取消副管理员');
const { data: refreshed } = await adminApi.getTeamDetail(detailTeam!.id); setDetailTeam(refreshed);
} catch { showToast('操作失败'); }
}}></span>
<>
<span className={styles.adminBadge} style={{ cursor: 'pointer' }} title="点击取消副管理员(变回普通成员)" onClick={async () => {
try {
await adminApi.setMemberRole(detailTeam!.id, m.id, false);
showToast('已取消副管理员');
const { data: refreshed } = await adminApi.getTeamDetail(detailTeam!.id); setDetailTeam(refreshed);
} catch { showToast('操作失败'); }
}}></span>
<button
type="button"
style={{ marginLeft: 6, fontSize: 11, color: 'var(--color-text-tertiary)',
background: 'none', border: 'none', cursor: 'pointer', padding: 0, textDecoration: 'underline' }}
title="升为主管理员(原主管不会自动降级,如需保持唯一主管请先撤销原主管)"
onClick={async () => {
if (!window.confirm(`${m.username} 设为主管理员?\n\n注意:不会自动降级现有主管。如果想换主管,请先撤销原主管再升新主管。`)) return;
try {
await adminApi.setMemberAsOwner(detailTeam!.id, m.id);
showToast('已升为主管理员');
const { data: refreshed } = await adminApi.getTeamDetail(detailTeam!.id); setDetailTeam(refreshed);
} catch { showToast('操作失败'); }
}}
></button>
</>
) : (
<span style={{ cursor: 'pointer', color: 'var(--color-text-secondary)' }} title="点击设为副管理员" onClick={async () => {
try {
await adminApi.setMemberRole(detailTeam!.id, m.id, true);
showToast('已设为副管理员');
const { data: refreshed } = await adminApi.getTeamDetail(detailTeam!.id); setDetailTeam(refreshed);
} catch { showToast('操作失败'); }
}}></span>
<>
<span style={{ cursor: 'pointer', color: 'var(--color-text-secondary)' }} title="点击设为副管理员" onClick={async () => {
try {
await adminApi.setMemberRole(detailTeam!.id, m.id, true);
showToast('已设为副管理员');
const { data: refreshed } = await adminApi.getTeamDetail(detailTeam!.id); setDetailTeam(refreshed);
} catch { showToast('操作失败'); }
}}></span>
<button
type="button"
style={{ marginLeft: 6, fontSize: 11, color: 'var(--color-text-tertiary)',
background: 'none', border: 'none', cursor: 'pointer', padding: 0, textDecoration: 'underline' }}
title="直接升为主管理员(原主管不会自动降级)"
onClick={async () => {
if (!window.confirm(`${m.username} 设为主管理员?\n\n注意:不会自动降级现有主管。如果想换主管,请先撤销原主管再升新主管。`)) return;
try {
await adminApi.setMemberAsOwner(detailTeam!.id, m.id);
showToast('已升为主管理员');
const { data: refreshed } = await adminApi.getTeamDetail(detailTeam!.id); setDetailTeam(refreshed);
} catch { showToast('操作失败'); }
}}
></button>
</>
)}
</td>
<td>