From a026c043104049db673ce73fb295cc75238490b4 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Mon, 23 Mar 2026 20:16:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v0.12.5=20admin=20=E4=BF=9D=E6=8A=A4=20?= =?UTF-8?q?+=20=E7=AE=A1=E7=90=86=E5=91=98=E8=A7=92=E8=89=B2=E5=88=87?= =?UTF-8?q?=E6=8D=A2=20+=20=E5=9B=A2=E9=98=9F=E8=AF=A6=E6=83=85=E5=8A=A0?= =?UTF-8?q?=E5=AE=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ①admin 账号不可被禁用(包括自己,防误操作) ②admin 密码不可被其他超管重置(admin 自己可以) ③超管可在团队详情点击角色切换成员/管理员 ④团队详情弹窗宽度 1080→1280px Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/apps/generation/urls.py | 1 + backend/apps/generation/views.py | 38 ++++++++++++++++++++++++++++++ web/src/lib/api.ts | 3 +++ web/src/pages/TeamsPage.module.css | 2 +- web/src/pages/TeamsPage.tsx | 18 ++++++++++++-- 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/backend/apps/generation/urls.py b/backend/apps/generation/urls.py index 18993f1..43da766 100644 --- a/backend/apps/generation/urls.py +++ b/backend/apps/generation/urls.py @@ -22,6 +22,7 @@ urlpatterns = [ path('admin/teams//topup', views.admin_team_topup_view, name='admin_team_topup'), path('admin/teams//set-pool', views.admin_team_set_pool_view, name='admin_team_set_pool'), path('admin/teams//admin', views.admin_team_create_admin_view, name='admin_team_create_admin'), + path('admin/teams//members//role', views.admin_team_member_role_view, name='admin_team_member_role'), # ── Super Admin: User management ── path('admin/users', views.admin_users_list_view, name='admin_users_list'), diff --git a/backend/apps/generation/views.py b/backend/apps/generation/views.py index 91c9de5..8d22f2e 100644 --- a/backend/apps/generation/views.py +++ b/backend/apps/generation/views.py @@ -1019,6 +1019,36 @@ def admin_team_detail_view(request, team_id): }) +@api_view(['PATCH']) +@permission_classes([IsSuperAdmin]) +def admin_team_member_role_view(request, team_id, member_id): + """PATCH /api/v1/admin/teams//members//role — Toggle team admin role.""" + try: + team = Team.objects.get(id=team_id) + except Team.DoesNotExist: + return Response({'error': '团队不存在'}, status=status.HTTP_404_NOT_FOUND) + try: + member = team.members.get(id=member_id) + except User.DoesNotExist: + return Response({'error': '成员不存在'}, status=status.HTTP_404_NOT_FOUND) + + is_admin = request.data.get('is_team_admin') + if is_admin is None: + return Response({'error': '请提供 is_team_admin 参数'}, status=status.HTTP_400_BAD_REQUEST) + + before = {'is_team_admin': member.is_team_admin} + member.is_team_admin = bool(is_admin) + member.save(update_fields=['is_team_admin']) + log_admin_action(request, 'team_update', 'user', target_id=member.id, target_name=member.username, + before=before, after={'is_team_admin': member.is_team_admin}) + + return Response({ + 'user_id': member.id, + 'username': member.username, + 'is_team_admin': member.is_team_admin, + }) + + @api_view(['POST']) @permission_classes([IsSuperAdmin]) def admin_team_topup_view(request, team_id): @@ -1419,6 +1449,10 @@ def admin_user_status_view(request, user_id): except User.DoesNotExist: return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND) + # 保护 admin 账号不被任何人禁用(包括自己,防误操作) + if user.username == 'admin': + return Response({'error': '不能禁用超级管理员账号'}, status=status.HTTP_403_FORBIDDEN) + serializer = UserStatusSerializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -1451,6 +1485,10 @@ def admin_reset_password_view(request, user_id): except User.DoesNotExist: return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND) + # 保护 admin 账号密码不被其他超管重置 + if user.username == 'admin' and request.user.username != 'admin': + return Response({'error': '不能重置超级管理员的密码'}, status=status.HTTP_403_FORBIDDEN) + new_password = request.data.get('new_password', '') if len(new_password) < 8: return Response({'error': '密码至少8位'}, status=status.HTTP_400_BAD_REQUEST) diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index bf4d1bd..014df4f 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -190,6 +190,9 @@ export const adminApi = { createTeamAdmin: (teamId: number, data: { username: string; email: string; password: string }) => api.post(`/admin/teams/${teamId}/admin`, data), + setMemberRole: (teamId: number, memberId: number, isTeamAdmin: boolean) => + api.patch(`/admin/teams/${teamId}/members/${memberId}/role`, { is_team_admin: isTeamAdmin }), + // User management createUser: (data: { username: string; diff --git a/web/src/pages/TeamsPage.module.css b/web/src/pages/TeamsPage.module.css index 8c1e9a1..2155776 100644 --- a/web/src/pages/TeamsPage.module.css +++ b/web/src/pages/TeamsPage.module.css @@ -90,7 +90,7 @@ -webkit-backdrop-filter: blur(24px) saturate(180%); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 16px; - width: 1080px; + width: 1280px; max-width: 96vw; min-height: 70vh; max-height: 90vh; diff --git a/web/src/pages/TeamsPage.tsx b/web/src/pages/TeamsPage.tsx index 2b0eb24..7ed5803 100644 --- a/web/src/pages/TeamsPage.tsx +++ b/web/src/pages/TeamsPage.tsx @@ -823,8 +823,22 @@ export function TeamsPage() { {m.email} {m.is_team_admin ? ( - 管理员 - ) : '成员'} + { + try { + await adminApi.setMemberRole(detailTeam!.id, m.id, false); + showToast('已取消管理员'); + const { data: refreshed } = await adminApi.getTeamDetail(detailTeam!.id); setDetailTeam(refreshed); + } catch { showToast('操作失败'); } + }}>管理员 + ) : ( + { + try { + await adminApi.setMemberRole(detailTeam!.id, m.id, true); + showToast('已设为管理员'); + const { data: refreshed } = await adminApi.getTeamDetail(detailTeam!.id); setDetailTeam(refreshed); + } catch { showToast('操作失败'); } + }}>成员 + )}