feat: 用户管理页加角色列和角色编辑弹窗

- 用户表新增「角色」列,显示超级管理员/管理员/用户徽章
- 编辑用户弹窗增加角色选择(USER/TENANT_ADMIN/SUPER_ADMIN)
- 角色选择时同步显示该角色的权限预览
- 保存时自动调用 PATCH /tenants/:id/members/:userId 更新角色
- 新增 test-permission-flow.mjs 三层用户权限测试脚本

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Developer
2026-06-08 23:37:13 +08:00
parent ba33d517c1
commit 9b4412792b
2 changed files with 237 additions and 7 deletions
+105 -7
View File
@@ -419,7 +419,15 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
const [passwordChangeUserData, setPasswordChangeUserData] = useState<{ userId: string, newPassword: string } | null>(null);
// --- Edit User State ---
const [editUserData, setEditUserData] = useState<{ userId: string, username: string, displayName: string } | null>(null);
const [editUserData, setEditUserData] = useState<{
userId: string;
username: string;
displayName: string;
memberId?: string;
tenantId?: string;
role?: string;
isAdmin?: boolean;
} | null>(null);
const handleToggleUserAdmin = async (userId: string, newAdminStatus: boolean) => {
try {
@@ -582,10 +590,26 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
const handleUpdateUser = async () => {
if (!editUserData) return;
try {
// 更新基本信息
await userService.updateUserInfo(editUserData.userId, {
username: editUserData.username,
displayName: editUserData.displayName
displayName: editUserData.displayName,
isAdmin: editUserData.role === 'SUPER_ADMIN',
});
// 更新角色(通过 tenant member API
if (editUserData.memberId && editUserData.tenantId) {
const res = await fetch(`/api/v1/tenants/${editUserData.tenantId}/members/${editUserData.userId}`, {
method: 'PATCH',
headers: {
'x-api-key': authToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ role: editUserData.role }),
});
if (!res.ok) console.warn('Role update returned', res.status);
}
showSuccess(t('featureUpdated'));
setEditUserData(null);
fetchUsers();
@@ -1028,11 +1052,63 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
required
/>
</div>
<div className="py-2.5 px-4 bg-indigo-50 rounded-2xl border border-indigo-100/50">
<p className="text-[10px] font-black text-indigo-500 uppercase tracking-widest mb-1">{t('globalUserNote') || "Note"}</p>
<p className="text-[11px] text-indigo-700/70 leading-relaxed font-medium">
{t('roleManagedInOrg') || "Roles are managed within organizations."}
{/* 角色选择 */}
<div>
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 px-1">
</label>
<div className="flex gap-2">
{['USER', 'TENANT_ADMIN', 'SUPER_ADMIN'].map(r => (
<button
key={r}
type="button"
onClick={() => setEditUserData({ ...editUserData, role: r })}
disabled={r === 'SUPER_ADMIN' && currentUser?.role !== 'SUPER_ADMIN'}
className={`px-4 py-2.5 rounded-xl text-xs font-black uppercase tracking-wider transition-all border-2 ${
editUserData.role === r
? r === 'SUPER_ADMIN'
? 'border-red-500 bg-red-50 text-red-700'
: r === 'TENANT_ADMIN'
? 'border-indigo-500 bg-indigo-50 text-indigo-700'
: 'border-slate-300 bg-slate-50 text-slate-600'
: 'border-transparent text-slate-400 hover:bg-slate-50'
} ${
r === 'SUPER_ADMIN' && currentUser?.role !== 'SUPER_ADMIN'
? 'opacity-30 cursor-not-allowed'
: 'cursor-pointer'
}`}
>
{r === 'SUPER_ADMIN' ? '超级管理员' : r === 'TENANT_ADMIN' ? '管理员' : '用户'}
</button>
))}
</div>
{editUserData.role === 'SUPER_ADMIN' && currentUser?.role !== 'SUPER_ADMIN' && (
<p className="text-[10px] text-amber-600 mt-1"></p>
)}
</div>
{/* 权限预览 */}
<div className="py-3 px-4 bg-slate-50 rounded-2xl border border-slate-100">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2">
({editUserData.role === 'SUPER_ADMIN' ? '26项' : editUserData.role === 'TENANT_ADMIN' ? '21项' : '5项'})
</p>
<div className="flex flex-wrap gap-1">
{editUserData.role === 'SUPER_ADMIN' && (
['全部权限:用户管理、租户管理、知识库、考核评估、模型配置、插件管理、系统设置'].map(p => (
<span key={p} className="px-2 py-0.5 bg-indigo-50 text-indigo-600 text-[9px] font-bold rounded-md">{p}</span>
))
)}
{editUserData.role === 'TENANT_ADMIN' && (
['查看用户','创建用户','编辑用户','重置密码','管理知识库','管理考核','管理模型','管理插件'].map(p => (
<span key={p} className="px-2 py-0.5 bg-indigo-50 text-indigo-600 text-[9px] font-bold rounded-md">{p}</span>
))
)}
{editUserData.role === 'USER' && (
['使用知识库','参与考核'].map(p => (
<span key={p} className="px-2 py-0.5 bg-slate-100 text-slate-500 text-[9px] font-bold rounded-md">{p}</span>
))
)}
</div>
</div>
<div className="flex gap-4 pt-4">
@@ -1054,6 +1130,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('username')}</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('displayName') || t('name')}</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('organizations')}</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest"></th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">{t('createdAt')}</th>
<th className="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest text-right">{t('actions')}</th>
</tr>
@@ -1108,6 +1185,22 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
<span className="text-[10px] text-slate-400 italic">{t('noOrganization')}</span>
)}
</td>
<td className="px-6 py-4">
{(() => {
const tm = user.tenantMembers?.filter((m: any) => m.tenant?.name !== 'Default')?.[0];
const role = user.isAdmin ? 'SUPER_ADMIN' : tm?.role || 'USER';
const colors: Record<string, string> = {
SUPER_ADMIN: 'bg-red-50 text-red-700 border-red-100',
TENANT_ADMIN: 'bg-indigo-50 text-indigo-700 border-indigo-100',
USER: 'bg-slate-50 text-slate-500 border-slate-100',
};
return (
<span className={`inline-flex items-center gap-1 px-2 py-0.5 text-[9px] font-black rounded-md uppercase tracking-wider border ${colors[role] || colors.USER}`}>
{role === 'SUPER_ADMIN' ? '超级管理员' : role === 'TENANT_ADMIN' ? '管理员' : '用户'}
</span>
);
})()}
</td>
<td className="px-6 py-4">
<p className="text-[11px] font-medium text-slate-600">
{new Date(user.createdAt).toLocaleDateString()}
@@ -1121,10 +1214,15 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
const tm = user.tenantMembers?.filter((m: any) => m.tenant?.name !== 'Default')?.[0];
setEditUserData({
userId: user.id,
username: user.username,
displayName: user.displayName || ''
displayName: user.displayName || '',
memberId: tm?.id,
tenantId: tm?.tenantId,
role: user.isAdmin ? 'SUPER_ADMIN' : tm?.role || 'USER',
isAdmin: !!user.isAdmin,
});
}}
className="p-2 rounded-lg text-slate-400 hover:text-indigo-600 hover:bg-white shadow-sm transition-all"