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:
parent
cec1e5d770
commit
c1dbc7ac86
@ -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={{
|
||||||
<span style={{ display: 'inline-flex', gap: 6, alignItems: 'center', flexWrap: 'nowrap' }}>
|
display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
|
||||||
<input
|
background: m.is_online ? 'var(--color-success)' : 'var(--color-text-quaternary)', marginRight: 6,
|
||||||
type="text"
|
verticalAlign: 'middle',
|
||||||
value={editingUsernameValue}
|
}} />
|
||||||
onChange={(e) => setEditingUsernameValue(e.target.value)}
|
{m.username}
|
||||||
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={{
|
|
||||||
display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
|
|
||||||
background: m.is_online ? 'var(--color-success)' : 'var(--color-text-quaternary)',
|
|
||||||
verticalAlign: 'middle',
|
|
||||||
}} />
|
|
||||||
<span>{m.username}</span>
|
|
||||||
{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>
|
||||||
|
|||||||
@ -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,51 +255,21 @@ export function UsersPage() {
|
|||||||
users.map((u) => (
|
users.map((u) => (
|
||||||
<tr key={u.id}>
|
<tr key={u.id}>
|
||||||
<td>
|
<td>
|
||||||
{editingUsernameId === u.id ? (
|
<button className={styles.usernameLink} onClick={() => openDrawer(u.id)}>
|
||||||
<span style={{ display: 'inline-flex', gap: 6, alignItems: 'center', flexWrap: 'nowrap' }}>
|
<span style={{
|
||||||
<input
|
display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
|
||||||
type="text"
|
background: u.is_online ? '#00b894' : '#555', marginRight: 6,
|
||||||
value={editingUsernameValue}
|
verticalAlign: 'middle',
|
||||||
onChange={(e) => setEditingUsernameValue(e.target.value)}
|
}} />
|
||||||
onKeyDown={(e) => {
|
{u.username}
|
||||||
if (e.key === 'Enter') handleSaveUsername(u);
|
</button>
|
||||||
else if (e.key === 'Escape') cancelEditUsername();
|
{u.is_observer && (
|
||||||
}}
|
<span
|
||||||
autoFocus
|
className={styles.statusBadge}
|
||||||
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 }}
|
style={{ background: 'var(--color-info-bg)', color: 'var(--color-info)', marginLeft: 6 }}
|
||||||
/>
|
title="该团管被标记为观察者,可查看全局内容资产"
|
||||||
<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)}>
|
|
||||||
<span style={{
|
|
||||||
display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
|
|
||||||
background: u.is_online ? '#00b894' : '#555', marginRight: 6,
|
|
||||||
verticalAlign: 'middle',
|
|
||||||
}} />
|
|
||||||
{u.username}
|
|
||||||
</button>
|
|
||||||
{u.username !== 'admin' && (
|
|
||||||
<button
|
|
||||||
className={styles.editBtn}
|
|
||||||
onClick={() => startEditUsername(u)}
|
|
||||||
title="修改用户名"
|
|
||||||
style={{ fontSize: 12, padding: '2px 8px', whiteSpace: 'nowrap' }}
|
|
||||||
>
|
|
||||||
改名
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{u.is_observer && (
|
|
||||||
<span
|
|
||||||
className={styles.statusBadge}
|
|
||||||
style={{ background: 'var(--color-info-bg)', color: 'var(--color-info)' }}
|
|
||||||
title="该团管被标记为观察者,可查看全局内容资产"
|
|
||||||
>
|
|
||||||
观察者
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user