refactor(users): 改名/观察者/角色切换归并到「编辑」modal — 操作列只剩 3 个按钮

行内/独立按钮模式之前太散,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) <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-05-18 18:11:14 +08:00
parent cec1e5d770
commit c1dbc7ac86
2 changed files with 165 additions and 189 deletions

View File

@ -22,19 +22,20 @@ export function TeamMembersPage() {
// Confirm toggle // Confirm toggle
const [confirmMember, setConfirmMember] = useState<TeamMember | null>(null); const [confirmMember, setConfirmMember] = useState<TeamMember | null>(null);
// Edit quota modal // Edit member modal (username + role + quota)
const [editMember, setEditMember] = useState<TeamMember | null>(null); const [editMember, setEditMember] = useState<TeamMember | null>(null);
const [editUsername, setEditUsername] = useState('');
// role 取值:'admin'(副管) | 'member'(成员);主管不可在此切换
const [editRole, setEditRole] = useState<'admin' | 'member'>('member');
const [editDaily, setEditDaily] = useState(''); const [editDaily, setEditDaily] = useState('');
const [editMonthly, setEditMonthly] = useState(''); const [editMonthly, setEditMonthly] = useState('');
const [editSpendingLimit, setEditSpendingLimit] = useState(''); const [editSpendingLimit, setEditSpendingLimit] = useState('');
const [editSaving, setEditSaving] = useState(false);
const [editError, setEditError] = useState('');
// Reset password result modal — 显示新生成的随机密码 + 复制按钮 // Reset password result modal — 显示新生成的随机密码 + 复制按钮
const [resetResult, setResetResult] = useState<{ username: string; newPassword: string } | null>(null); const [resetResult, setResetResult] = useState<{ username: string; newPassword: string } | null>(null);
// Inline username edit
const [editingUsernameId, setEditingUsernameId] = useState<number | null>(null);
const [editingUsernameValue, setEditingUsernameValue] = useState('');
// 权限矩阵: // 权限矩阵:
// 主管(is_team_owner) → 可重置「副管 + 成员」(不可重置主管/自己) // 主管(is_team_owner) → 可重置「副管 + 成员」(不可重置主管/自己)
// 副管(is_team_admin) → 只能重置「成员」(不可重置副管/主管/自己) // 副管(is_team_admin) → 只能重置「成员」(不可重置副管/主管/自己)
@ -57,28 +58,12 @@ export function TeamMembersPage() {
return true; return true;
}; };
const startEditUsername = (m: TeamMember) => { // 权限矩阵(角色切换): 仅主管能切非主管成员的副管/成员角色
setEditingUsernameId(m.id); const canEditRoleFor = (m: TeamMember): boolean => {
setEditingUsernameValue(m.username); if (!currentUser?.is_team_owner) return false;
}; if (m.is_team_owner) return false;
if (m.id === currentUser.id) return false;
const cancelEditUsername = () => { return true;
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 handleResetPassword = async (m: TeamMember) => { const handleResetPassword = async (m: TeamMember) => {
@ -125,20 +110,42 @@ export function TeamMembersPage() {
const openEditModal = (member: TeamMember) => { const openEditModal = (member: TeamMember) => {
setEditMember(member); setEditMember(member);
setEditUsername(member.username);
setEditRole(member.is_team_admin ? 'admin' : 'member');
setEditDaily(String(member.daily_generation_limit ?? 50)); setEditDaily(String(member.daily_generation_limit ?? 50));
setEditMonthly(String(member.monthly_generation_limit ?? 500)); setEditMonthly(String(member.monthly_generation_limit ?? 500));
setEditSpendingLimit(String(member.spending_limit ?? -1)); setEditSpendingLimit(String(member.spending_limit ?? -1));
setEditError('');
}; };
const handleSaveQuota = async () => { // 串调:username → role → quota。任一失败 toast 并停留。
const handleSaveMember = async () => {
if (!editMember) return; if (!editMember) return;
setEditError('');
setEditSaving(true);
try { 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)); await teamApi.updateMemberQuota(editMember.id, Number(editDaily), Number(editMonthly), Number(editSpendingLimit));
showToast('配额已更新'); showToast('已保存');
setEditMember(null); setEditMember(null);
fetchMembers(); fetchMembers();
} catch { } catch (e: any) {
showToast('更新失败'); 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) => ( members.map((m) => (
<tr key={m.id}> <tr key={m.id}>
<td> <td>
{editingUsernameId === m.id ? (
<span style={{ display: 'inline-flex', gap: 6, alignItems: 'center', flexWrap: 'nowrap' }}>
<input
type="text"
value={editingUsernameValue}
onChange={(e) => 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 }}
/>
<button className={styles.editBtn} onClick={() => handleSaveUsername(m)} style={{ fontSize: 12, padding: '4px 10px', whiteSpace: 'nowrap' }}></button>
<button className={styles.editBtn} onClick={cancelEditUsername} style={{ fontSize: 12, padding: '4px 10px', whiteSpace: 'nowrap' }}></button>
</span>
) : (
<span style={{ display: 'inline-flex', gap: 6, alignItems: 'center', flexWrap: 'nowrap' }}>
<span style={{ <span style={{
display: 'inline-block', width: 8, height: 8, borderRadius: '50%', display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
background: m.is_online ? 'var(--color-success)' : 'var(--color-text-quaternary)', background: m.is_online ? 'var(--color-success)' : 'var(--color-text-quaternary)', marginRight: 6,
verticalAlign: 'middle', verticalAlign: 'middle',
}} /> }} />
<span>{m.username}</span> {m.username}
{canEditUsernameFor(m) && (
<button
className={styles.editBtn}
onClick={() => startEditUsername(m)}
title="修改用户名"
style={{ fontSize: 12, padding: '2px 8px', whiteSpace: 'nowrap' }}
>
</button>
)}
</span>
)}
</td> </td>
<td> <td>
{m.is_team_owner ? ( {m.is_team_owner ? (
@ -270,18 +247,7 @@ export function TeamMembersPage() {
<td>{(m.generations_this_month || 0) + '次 / ' + fmtMoney(m.spent_this_month)}</td> <td>{(m.generations_this_month || 0) + '次 / ' + fmtMoney(m.spent_this_month)}</td>
<td> <td>
<div className={styles.actions}> <div className={styles.actions}>
<button className={styles.editBtn} onClick={() => openEditModal(m)}></button> <button className={styles.editBtn} onClick={() => openEditModal(m)}></button>
{currentUser?.is_team_owner && !m.is_team_owner && (
m.is_team_admin ? (
<button className={styles.editBtn} onClick={async () => {
try { await teamApi.setMemberRole(m.id, false); showToast('已取消副管理员'); fetchMembers(); } catch { showToast('操作失败'); }
}}></button>
) : (
<button className={styles.editBtn} onClick={async () => {
try { await teamApi.setMemberRole(m.id, true); showToast('已设为副管理员'); fetchMembers(); } catch { showToast('操作失败'); }
}}></button>
)
)}
{canResetPasswordFor(m) && ( {canResetPasswordFor(m) && (
<button className={styles.editBtn} onClick={() => handleResetPassword(m)}> <button className={styles.editBtn} onClick={() => handleResetPassword(m)}>
@ -316,9 +282,42 @@ export function TeamMembersPage() {
{/* Edit Quota Modal */} {/* Edit Quota Modal */}
{editMember && ( {editMember && (
<div className={styles.modalOverlay} onMouseDown={(e) => { if (e.target === e.currentTarget) setEditMember(null); }}> <div className={styles.modalOverlay} onMouseDown={(e) => { if (e.target === e.currentTarget && !editSaving) setEditMember(null); }}>
<div className={styles.modal}> <div className={styles.modal}>
<h3 className={styles.modalTitle}> {editMember.username}</h3> <h3 className={styles.modalTitle}> {editMember.username}</h3>
<div className={styles.formGroup}>
<label>
{!canEditUsernameFor(editMember) && '(无权修改)'}
{canEditUsernameFor(editMember) && '3-20 字符,支持中文)'}
</label>
<input
type="text"
value={editUsername}
onChange={(e) => setEditUsername(e.target.value)}
disabled={!canEditUsernameFor(editMember)}
/>
</div>
{canEditRoleFor(editMember) ? (
<div className={styles.formGroup}>
<label></label>
<select
value={editRole}
onChange={(e) => setEditRole(e.target.value as 'admin' | 'member')}
style={{ width: '100%', padding: '8px 12px', borderRadius: 6, border: '1px solid var(--color-border-card)', background: 'var(--color-bg-page)', color: 'var(--color-text-primary)', fontSize: 14 }}
>
<option value="member"></option>
<option value="admin"></option>
</select>
</div>
) : (
<div className={styles.formGroup}>
<label></label>
<div style={{ color: 'var(--color-text-secondary)', fontSize: 13 }}>
{editMember.is_team_owner ? '主管理员(不可在此修改)' : editMember.is_team_admin ? '副管理员' : '成员'}
</div>
</div>
)}
<div className={styles.formGroup}> <div className={styles.formGroup}>
<label>-1 </label> <label>-1 </label>
<input type="number" value={editDaily} onChange={(e) => setEditDaily(e.target.value)} /> <input type="number" value={editDaily} onChange={(e) => setEditDaily(e.target.value)} />
@ -331,9 +330,14 @@ export function TeamMembersPage() {
<label>-1 </label> <label>-1 </label>
<input type="number" value={editSpendingLimit} onChange={(e) => setEditSpendingLimit(e.target.value)} /> <input type="number" value={editSpendingLimit} onChange={(e) => setEditSpendingLimit(e.target.value)} />
</div> </div>
{editError && (
<div style={{ color: 'var(--color-danger)', fontSize: 12, marginBottom: 8 }}>{editError}</div>
)}
<div className={styles.modalActions}> <div className={styles.modalActions}>
<button className={styles.cancelBtn} onClick={() => setEditMember(null)}></button> <button className={styles.cancelBtn} onClick={() => setEditMember(null)} disabled={editSaving}></button>
<button className={styles.saveBtn} onClick={handleSaveQuota}></button> <button className={styles.saveBtn} onClick={handleSaveMember} disabled={editSaving}>
{editSaving ? '保存中…' : '保存'}
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -17,11 +17,15 @@ export function UsersPage() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const pageSize = 20; const pageSize = 20;
// Quota edit modal // User edit modal (username + observer + quota)
const [editUser, setEditUser] = useState<AdminUser | null>(null); const [editUser, setEditUser] = useState<AdminUser | null>(null);
const [editUsername, setEditUsername] = useState('');
const [editIsObserver, setEditIsObserver] = useState(false);
const [editDaily, setEditDaily] = useState(''); const [editDaily, setEditDaily] = useState('');
const [editMonthly, setEditMonthly] = useState(''); const [editMonthly, setEditMonthly] = useState('');
const [editSpendingLimit, setEditSpendingLimit] = useState(''); const [editSpendingLimit, setEditSpendingLimit] = useState('');
const [editSaving, setEditSaving] = useState(false);
const [editError, setEditError] = useState('');
// User detail drawer // User detail drawer
const [detailUser, setDetailUser] = useState<AdminUserDetail | null>(null); const [detailUser, setDetailUser] = useState<AdminUserDetail | null>(null);
@ -35,10 +39,6 @@ export function UsersPage() {
const [resetPwValue, setResetPwValue] = useState(''); const [resetPwValue, setResetPwValue] = useState('');
const [resetPwError, setResetPwError] = useState(''); const [resetPwError, setResetPwError] = useState('');
// Inline username edit
const [editingUsernameId, setEditingUsernameId] = useState<number | null>(null);
const [editingUsernameValue, setEditingUsernameValue] = useState('');
// Create user modal // Create user modal
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [newUsername, setNewUsername] = useState(''); const [newUsername, setNewUsername] = useState('');
@ -89,20 +89,43 @@ export function UsersPage() {
const openEditModal = (user: AdminUser) => { const openEditModal = (user: AdminUser) => {
setEditUser(user); setEditUser(user);
setEditUsername(user.username);
setEditIsObserver(!!user.is_observer);
setEditDaily(String(user.daily_generation_limit ?? 50)); setEditDaily(String(user.daily_generation_limit ?? 50));
setEditMonthly(String(user.monthly_generation_limit ?? 500)); setEditMonthly(String(user.monthly_generation_limit ?? 500));
setEditSpendingLimit(String(user.spending_limit ?? -1)); setEditSpendingLimit(String(user.spending_limit ?? -1));
setEditError('');
}; };
const handleSaveQuota = async () => { // 串行调多个 PATCH:username → observer → quota。任一失败 toast 并停留在 modal,已成功的改动保留。
const handleSaveUser = async () => {
if (!editUser) return; if (!editUser) return;
setEditError('');
setEditSaving(true);
let observerJustEnabled = false;
try { 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)); await adminApi.updateUserQuota(editUser.id, Number(editDaily), Number(editMonthly), Number(editSpendingLimit));
showToast('配额已更新');
showToast(observerJustEnabled ? '已保存(观察者标记需该用户重新登录后生效)' : '已保存');
setEditUser(null); setEditUser(null);
fetchUsers(); fetchUsers();
} catch { } catch (e: any) {
showToast('更新失败'); 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 () => { const handleResetPassword = async () => {
if (!resetPwUser) return; if (!resetPwUser) return;
setResetPwError(''); setResetPwError('');
@ -271,24 +255,6 @@ export function UsersPage() {
users.map((u) => ( users.map((u) => (
<tr key={u.id}> <tr key={u.id}>
<td> <td>
{editingUsernameId === u.id ? (
<span style={{ display: 'inline-flex', gap: 6, alignItems: 'center', flexWrap: 'nowrap' }}>
<input
type="text"
value={editingUsernameValue}
onChange={(e) => 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 }}
/>
<button className={styles.editBtn} onClick={() => handleSaveUsername(u)} style={{ fontSize: 12, padding: '4px 10px', whiteSpace: 'nowrap' }}></button>
<button className={styles.editBtn} onClick={cancelEditUsername} style={{ fontSize: 12, padding: '4px 10px', whiteSpace: 'nowrap' }}></button>
</span>
) : (
<span style={{ display: 'inline-flex', gap: 6, alignItems: 'center', flexWrap: 'nowrap' }}>
<button className={styles.usernameLink} onClick={() => openDrawer(u.id)}> <button className={styles.usernameLink} onClick={() => openDrawer(u.id)}>
<span style={{ <span style={{
display: 'inline-block', width: 8, height: 8, borderRadius: '50%', display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
@ -297,27 +263,15 @@ export function UsersPage() {
}} /> }} />
{u.username} {u.username}
</button> </button>
{u.username !== 'admin' && (
<button
className={styles.editBtn}
onClick={() => startEditUsername(u)}
title="修改用户名"
style={{ fontSize: 12, padding: '2px 8px', whiteSpace: 'nowrap' }}
>
</button>
)}
{u.is_observer && ( {u.is_observer && (
<span <span
className={styles.statusBadge} className={styles.statusBadge}
style={{ background: 'var(--color-info-bg)', color: 'var(--color-info)' }} style={{ background: 'var(--color-info-bg)', color: 'var(--color-info)', marginLeft: 6 }}
title="该团管被标记为观察者,可查看全局内容资产" title="该团管被标记为观察者,可查看全局内容资产"
> >
</span> </span>
)} )}
</span>
)}
</td> </td>
<td>{u.team_name || '-'}</td> <td>{u.team_name || '-'}</td>
<td>{u.email}</td> <td>{u.email}</td>
@ -341,15 +295,6 @@ export function UsersPage() {
<div className={styles.actions}> <div className={styles.actions}>
<button className={styles.editBtn} onClick={() => openEditModal(u)}></button> <button className={styles.editBtn} onClick={() => openEditModal(u)}></button>
<button className={styles.editBtn} onClick={() => { setResetPwUser(u); setResetPwValue(''); setResetPwError(''); }}></button> <button className={styles.editBtn} onClick={() => { setResetPwUser(u); setResetPwValue(''); setResetPwError(''); }}></button>
{u.is_team_admin && u.team_id && (
<button
className={styles.editBtn}
onClick={() => handleToggleObserver(u)}
title={u.is_observer ? '取消该团管的观察者标记' : '把该团管标记为观察者(可看全局内容资产,无费用)'}
>
{u.is_observer ? '取消观察者' : '设为观察者'}
</button>
)}
<button <button
className={`${styles.toggleBtn} ${u.is_active ? styles.disableBtn : styles.enableBtn}`} className={`${styles.toggleBtn} ${u.is_active ? styles.disableBtn : styles.enableBtn}`}
onClick={() => setConfirmUser(u)} onClick={() => setConfirmUser(u)}
@ -401,9 +346,31 @@ export function UsersPage() {
{/* Quota Edit Modal */} {/* Quota Edit Modal */}
{editUser && ( {editUser && (
<div className={styles.modalOverlay} onMouseDown={(e) => { if (e.target === e.currentTarget) setEditUser(null); }}> <div className={styles.modalOverlay} onMouseDown={(e) => { if (e.target === e.currentTarget && !editSaving) setEditUser(null); }}>
<div className={styles.modal}> <div className={styles.modal}>
<h3 className={styles.modalTitle}> {editUser.username}</h3> <h3 className={styles.modalTitle}> {editUser.username}</h3>
<div className={styles.formGroup}>
<label>{editUser.username === 'admin' ? '(超级管理员不可修改)' : '3-20 字符,支持中文)'}</label>
<input
type="text"
value={editUsername}
onChange={(e) => setEditUsername(e.target.value)}
disabled={editUser.username === 'admin'}
/>
</div>
{editUser.is_team_admin && editUser.team_id && (
<div className={styles.formGroup}>
<label>
<input
type="checkbox"
checked={editIsObserver}
onChange={(e) => setEditIsObserver(e.target.checked)}
style={{ marginRight: 8, verticalAlign: 'middle' }}
/>
</label>
</div>
)}
<div className={styles.formGroup}> <div className={styles.formGroup}>
<label>-1 </label> <label>-1 </label>
<input type="number" value={editDaily} onChange={(e) => setEditDaily(e.target.value)} /> <input type="number" value={editDaily} onChange={(e) => setEditDaily(e.target.value)} />
@ -416,9 +383,14 @@ export function UsersPage() {
<label>-1 </label> <label>-1 </label>
<input type="number" value={editSpendingLimit} onChange={(e) => setEditSpendingLimit(e.target.value)} /> <input type="number" value={editSpendingLimit} onChange={(e) => setEditSpendingLimit(e.target.value)} />
</div> </div>
{editError && (
<div style={{ color: 'var(--color-danger)', fontSize: 12, marginBottom: 8 }}>{editError}</div>
)}
<div className={styles.modalActions}> <div className={styles.modalActions}>
<button className={styles.cancelBtn} onClick={() => setEditUser(null)}></button> <button className={styles.cancelBtn} onClick={() => setEditUser(null)} disabled={editSaving}></button>
<button className={styles.saveBtn} onClick={handleSaveQuota}></button> <button className={styles.saveBtn} onClick={handleSaveUser} disabled={editSaving}>
{editSaving ? '保存中…' : '保存'}
</button>
</div> </div>
</div> </div>
</div> </div>