From c1dbc7ac86a5ce738c1404b55d014c79ef2b5f71 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Mon, 18 May 2026 18:11:14 +0800 Subject: [PATCH] =?UTF-8?q?refactor(users):=20=E6=94=B9=E5=90=8D/=E8=A7=82?= =?UTF-8?q?=E5=AF=9F=E8=80=85/=E8=A7=92=E8=89=B2=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=BD=92=E5=B9=B6=E5=88=B0=E3=80=8C=E7=BC=96=E8=BE=91=E3=80=8D?= =?UTF-8?q?modal=20=E2=80=94=20=E6=93=8D=E4=BD=9C=E5=88=97=E5=8F=AA?= =?UTF-8?q?=E5=89=A9=203=20=E4=B8=AA=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 行内/独立按钮模式之前太散,actions 列挤满。本质都是「编辑用户属性」,合并到 modal: UsersPage(超管): - 删 cell 内联「改名」按钮 + 内联编辑 state(editingUsernameId/Value + startEditUsername/cancelEditUsername/handleSaveUsername) - 删 actions「设为观察者/取消观察者」按钮 + handleToggleObserver - 「编辑」modal 标题改「编辑用户」,加 [用户名] (admin 行 disabled) + [观察者复选框] (仅 team_admin 显示) - handleSaveQuota → handleSaveUser:串调 username → observer → quota,任一失败 toast + 停留 modal - cell 保留 observer badge 只读显示 - actions 列剩 [编辑] [重置密码] [禁用/启用] TeamMembersPage(团管): - 删 cell 内联「改名」按钮 + 内联编辑 state - 删 actions「设为副管理员/取消副管理员」按钮 - 「编辑配额」改「编辑」,modal 标题「编辑成员」,加 [用户名] (按 canEditUsernameFor) + [角色 select] (canEditRoleFor 决定 select 还是 readonly 文本) - 新 helper canEditRoleFor:仅主管可改非主管成员的角色 - handleSaveQuota → handleSaveMember:串调 username → role → quota - actions 列剩 [编辑] [重置密码(权限矩阵)] [禁用/启用] 后端零改动,纯前端串调现有 PATCH endpoints。 Co-Authored-By: Claude Opus 4.7 (1M context) --- web/src/pages/TeamMembersPage.tsx | 170 +++++++++++++-------------- web/src/pages/UsersPage.tsx | 184 +++++++++++++----------------- 2 files changed, 165 insertions(+), 189 deletions(-) diff --git a/web/src/pages/TeamMembersPage.tsx b/web/src/pages/TeamMembersPage.tsx index 585b305..48eb457 100644 --- a/web/src/pages/TeamMembersPage.tsx +++ b/web/src/pages/TeamMembersPage.tsx @@ -22,19 +22,20 @@ export function TeamMembersPage() { // Confirm toggle const [confirmMember, setConfirmMember] = useState(null); - // Edit quota modal + // Edit member modal (username + role + quota) const [editMember, setEditMember] = useState(null); + const [editUsername, setEditUsername] = useState(''); + // role 取值:'admin'(副管) | 'member'(成员);主管不可在此切换 + const [editRole, setEditRole] = useState<'admin' | 'member'>('member'); const [editDaily, setEditDaily] = useState(''); const [editMonthly, setEditMonthly] = useState(''); const [editSpendingLimit, setEditSpendingLimit] = useState(''); + const [editSaving, setEditSaving] = useState(false); + const [editError, setEditError] = useState(''); // Reset password result modal — 显示新生成的随机密码 + 复制按钮 const [resetResult, setResetResult] = useState<{ username: string; newPassword: string } | null>(null); - // Inline username edit - const [editingUsernameId, setEditingUsernameId] = useState(null); - const [editingUsernameValue, setEditingUsernameValue] = useState(''); - // 权限矩阵: // 主管(is_team_owner) → 可重置「副管 + 成员」(不可重置主管/自己) // 副管(is_team_admin) → 只能重置「成员」(不可重置副管/主管/自己) @@ -57,28 +58,12 @@ export function TeamMembersPage() { return true; }; - const startEditUsername = (m: TeamMember) => { - setEditingUsernameId(m.id); - setEditingUsernameValue(m.username); - }; - - const cancelEditUsername = () => { - setEditingUsernameId(null); - setEditingUsernameValue(''); - }; - - const handleSaveUsername = async (m: TeamMember) => { - const newName = editingUsernameValue.trim(); - if (!newName) { showToast('请输入用户名'); return; } - if (newName === m.username) { cancelEditUsername(); return; } - try { - await teamApi.updateMemberUsername(m.id, newName); - showToast(`已更新用户名为「${newName}」`); - cancelEditUsername(); - fetchMembers(); - } catch (e: any) { - showToast(e?.response?.data?.error || '操作失败'); - } + // 权限矩阵(角色切换): 仅主管能切非主管成员的副管/成员角色 + const canEditRoleFor = (m: TeamMember): boolean => { + if (!currentUser?.is_team_owner) return false; + if (m.is_team_owner) return false; + if (m.id === currentUser.id) return false; + return true; }; const handleResetPassword = async (m: TeamMember) => { @@ -125,20 +110,42 @@ export function TeamMembersPage() { const openEditModal = (member: TeamMember) => { setEditMember(member); + setEditUsername(member.username); + setEditRole(member.is_team_admin ? 'admin' : 'member'); setEditDaily(String(member.daily_generation_limit ?? 50)); setEditMonthly(String(member.monthly_generation_limit ?? 500)); setEditSpendingLimit(String(member.spending_limit ?? -1)); + setEditError(''); }; - const handleSaveQuota = async () => { + // 串调:username → role → quota。任一失败 toast 并停留。 + const handleSaveMember = async () => { if (!editMember) return; + setEditError(''); + setEditSaving(true); try { + // 1) 用户名:只在 canEditUsernameFor 且有变化时调 + const newUsername = editUsername.trim(); + if (canEditUsernameFor(editMember) && newUsername && newUsername !== editMember.username) { + await teamApi.updateMemberUsername(editMember.id, newUsername); + } + // 2) 角色:只在 canEditRoleFor 且有变化时调 + const currentRoleIsAdmin = !!editMember.is_team_admin; + const targetRoleIsAdmin = editRole === 'admin'; + if (canEditRoleFor(editMember) && currentRoleIsAdmin !== targetRoleIsAdmin) { + await teamApi.setMemberRole(editMember.id, targetRoleIsAdmin); + } + // 3) 配额:始终保存 await teamApi.updateMemberQuota(editMember.id, Number(editDaily), Number(editMonthly), Number(editSpendingLimit)); - showToast('配额已更新'); + showToast('已保存'); setEditMember(null); fetchMembers(); - } catch { - showToast('更新失败'); + } catch (e: any) { + const msg = e?.response?.data?.error || e?.response?.data?.detail || '保存失败'; + setEditError(msg); + showToast(msg); + } finally { + setEditSaving(false); } }; @@ -212,42 +219,12 @@ export function TeamMembersPage() { members.map((m) => ( - {editingUsernameId === m.id ? ( - - setEditingUsernameValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') handleSaveUsername(m); - else if (e.key === 'Escape') cancelEditUsername(); - }} - autoFocus - style={{ width: 140, padding: '3px 6px', borderRadius: 4, border: '1px solid var(--color-border-card)', background: 'var(--color-bg-page)', color: 'var(--color-text-primary)', fontSize: 13 }} - /> - - - - ) : ( - - - {m.username} - {canEditUsernameFor(m) && ( - - )} - - )} + + {m.username} {m.is_team_owner ? ( @@ -270,18 +247,7 @@ export function TeamMembersPage() { {(m.generations_this_month || 0) + '次 / ' + fmtMoney(m.spent_this_month)}
- - {currentUser?.is_team_owner && !m.is_team_owner && ( - m.is_team_admin ? ( - - ) : ( - - ) - )} + {canResetPasswordFor(m) && ( - + +
diff --git a/web/src/pages/UsersPage.tsx b/web/src/pages/UsersPage.tsx index 6fc400d..11b3404 100644 --- a/web/src/pages/UsersPage.tsx +++ b/web/src/pages/UsersPage.tsx @@ -17,11 +17,15 @@ export function UsersPage() { const [loading, setLoading] = useState(true); const pageSize = 20; - // Quota edit modal + // User edit modal (username + observer + quota) const [editUser, setEditUser] = useState(null); + const [editUsername, setEditUsername] = useState(''); + const [editIsObserver, setEditIsObserver] = useState(false); const [editDaily, setEditDaily] = useState(''); const [editMonthly, setEditMonthly] = useState(''); const [editSpendingLimit, setEditSpendingLimit] = useState(''); + const [editSaving, setEditSaving] = useState(false); + const [editError, setEditError] = useState(''); // User detail drawer const [detailUser, setDetailUser] = useState(null); @@ -35,10 +39,6 @@ export function UsersPage() { const [resetPwValue, setResetPwValue] = useState(''); const [resetPwError, setResetPwError] = useState(''); - // Inline username edit - const [editingUsernameId, setEditingUsernameId] = useState(null); - const [editingUsernameValue, setEditingUsernameValue] = useState(''); - // Create user modal const [createOpen, setCreateOpen] = useState(false); const [newUsername, setNewUsername] = useState(''); @@ -89,20 +89,43 @@ export function UsersPage() { const openEditModal = (user: AdminUser) => { setEditUser(user); + setEditUsername(user.username); + setEditIsObserver(!!user.is_observer); setEditDaily(String(user.daily_generation_limit ?? 50)); setEditMonthly(String(user.monthly_generation_limit ?? 500)); setEditSpendingLimit(String(user.spending_limit ?? -1)); + setEditError(''); }; - const handleSaveQuota = async () => { + // 串行调多个 PATCH:username → observer → quota。任一失败 toast 并停留在 modal,已成功的改动保留。 + const handleSaveUser = async () => { if (!editUser) return; + setEditError(''); + setEditSaving(true); + let observerJustEnabled = false; try { + // 1) 用户名:admin 行只读不发请求;有改动才调 + const newUsername = editUsername.trim(); + if (editUser.username !== 'admin' && newUsername && newUsername !== editUser.username) { + await adminApi.updateUserUsername(editUser.id, newUsername); + } + // 2) 观察者标记:仅团管能切,有变化才调 + if (editUser.is_team_admin && editUser.team_id && (!!editUser.is_observer) !== editIsObserver) { + await adminApi.toggleUserObserver(editUser.id, editIsObserver); + observerJustEnabled = editIsObserver; + } + // 3) 配额:始终保存 await adminApi.updateUserQuota(editUser.id, Number(editDaily), Number(editMonthly), Number(editSpendingLimit)); - showToast('配额已更新'); + + showToast(observerJustEnabled ? '已保存(观察者标记需该用户重新登录后生效)' : '已保存'); setEditUser(null); fetchUsers(); - } catch { - showToast('更新失败'); + } catch (e: any) { + const msg = e.response?.data?.error || e.response?.data?.detail || '保存失败'; + setEditError(msg); + showToast(msg); + } finally { + setEditSaving(false); } }; @@ -146,45 +169,6 @@ export function UsersPage() { } }; - const startEditUsername = (u: AdminUser) => { - setEditingUsernameId(u.id); - setEditingUsernameValue(u.username); - }; - - const cancelEditUsername = () => { - setEditingUsernameId(null); - setEditingUsernameValue(''); - }; - - const handleSaveUsername = async (u: AdminUser) => { - const newName = editingUsernameValue.trim(); - if (!newName) { showToast('请输入用户名'); return; } - if (newName === u.username) { cancelEditUsername(); return; } - try { - await adminApi.updateUserUsername(u.id, newName); - showToast(`已更新用户名为「${newName}」`); - cancelEditUsername(); - fetchUsers(); - } catch (e: any) { - showToast(e.response?.data?.error || '操作失败'); - } - }; - - const handleToggleObserver = async (u: AdminUser) => { - try { - const next = !u.is_observer; - await adminApi.toggleUserObserver(u.id, next); - showToast( - next - ? `已将「${u.username}」设为观察者(需该用户重新登录后生效)` - : `已取消「${u.username}」的观察者标记` - ); - fetchUsers(); - } catch (e: any) { - showToast(e.response?.data?.error || '操作失败'); - } - }; - const handleResetPassword = async () => { if (!resetPwUser) return; setResetPwError(''); @@ -271,51 +255,21 @@ export function UsersPage() { users.map((u) => ( - {editingUsernameId === u.id ? ( - - setEditingUsernameValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') handleSaveUsername(u); - else if (e.key === 'Escape') cancelEditUsername(); - }} - autoFocus - style={{ width: 140, padding: '3px 6px', borderRadius: 4, border: '1px solid var(--color-border-card)', background: 'var(--color-bg-page)', color: 'var(--color-text-primary)', fontSize: 13 }} - /> - - - - ) : ( - - - {u.username !== 'admin' && ( - - )} - {u.is_observer && ( - - 观察者 - - )} + + {u.is_observer && ( + + 观察者 )} @@ -341,15 +295,6 @@ export function UsersPage() {
- {u.is_team_admin && u.team_id && ( - - )} - + +