forked from hangshuo652/aurak
feat: 分层 RBAC 权限管理系统
后端: - 新增 Role / RolePermission 实体(自动 seed 系统角色) - PermissionService——通过 isAdmin / TenantMember 链路解析用户权限 - @Permission() 装饰器 + PermissionsGuard 守卫 - /api/permissions 和 /api/roles REST API - UserController 内联 role 检查迁移到 @Permission() - PermissionModule 全局注册 前端: - usePermissions hook——获取当前用户权限集 - PermissionGate 组件级门控 - PermissionSettingsView——角色列表+权限矩阵编辑页面 - SettingsView 新增「权限管理」Tab(仅 admin 可见) - 权限预览(26 项,7 分类) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { usePermissions } from '../src/hooks/usePermissions';
|
||||
|
||||
interface PermissionGateProps {
|
||||
/** 需要的权限(OR 关系:有任一即可) */
|
||||
permission?: string;
|
||||
/** 多个权限 OR 关系 */
|
||||
any?: string[];
|
||||
/** 多个权限 AND 关系(必须全部拥有) */
|
||||
all?: string[];
|
||||
/** 加载中时显示的内容(默认不显示) */
|
||||
fallback?: React.ReactNode;
|
||||
/** 无权限时显示的内容(默认不显示) */
|
||||
denied?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件级权限门控
|
||||
* 根据用户权限集有条件的渲染子组件
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <PermissionGate permission="user:create">
|
||||
* <Button>创建用户</Button>
|
||||
* </PermissionGate>
|
||||
*
|
||||
* <PermissionGate any={['user:edit', 'user:delete']}>
|
||||
* <AdminPanel />
|
||||
* </PermissionGate>
|
||||
* ```
|
||||
*/
|
||||
export const PermissionGate: React.FC<PermissionGateProps> = ({
|
||||
permission,
|
||||
any,
|
||||
all,
|
||||
fallback = null,
|
||||
denied = null,
|
||||
children,
|
||||
}) => {
|
||||
const { hasPermission, hasAnyPermission, hasAllPermissions, isLoading } = usePermissions();
|
||||
|
||||
if (isLoading) {
|
||||
return <>{fallback}</>;
|
||||
}
|
||||
|
||||
let granted = false;
|
||||
|
||||
if (permission) {
|
||||
granted = hasPermission(permission);
|
||||
} else if (any && any.length > 0) {
|
||||
granted = hasAnyPermission(...any);
|
||||
} else if (all && all.length > 0) {
|
||||
granted = hasAllPermissions(...all);
|
||||
} else {
|
||||
// 没有指定权限要求 → 放行
|
||||
granted = true;
|
||||
}
|
||||
|
||||
return <>{granted ? children : denied}</>;
|
||||
};
|
||||
@@ -0,0 +1,440 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Shield,
|
||||
Plus,
|
||||
Save,
|
||||
X,
|
||||
Edit2,
|
||||
Trash2,
|
||||
Loader2,
|
||||
Check,
|
||||
Users,
|
||||
Key,
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '../../src/contexts/AuthContext';
|
||||
import { useConfirm } from '../../contexts/ConfirmContext';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { cn } from '../../src/utils/cn';
|
||||
|
||||
interface PermissionMeta {
|
||||
key: string;
|
||||
category: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
isSystem: boolean;
|
||||
baseRole?: string;
|
||||
tenantId?: string;
|
||||
}
|
||||
|
||||
/** 按分类分组的权限 */
|
||||
interface PermissionsByCategory {
|
||||
[category: string]: PermissionMeta[];
|
||||
}
|
||||
|
||||
export const PermissionSettingsView: React.FC = () => {
|
||||
const { apiKey, activeTenant } = useAuth();
|
||||
const { confirm } = useConfirm();
|
||||
const { showError, showSuccess } = useToast();
|
||||
const { t } = useLanguage();
|
||||
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [allPermissions, setAllPermissions] = useState<PermissionsByCategory>({});
|
||||
const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);
|
||||
const [rolePermissions, setRolePermissions] = useState<Set<string>>(new Set());
|
||||
const [isLoadingRoles, setIsLoadingRoles] = useState(true);
|
||||
const [isLoadingPerms, setIsLoadingPerms] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [showCreateRole, setShowCreateRole] = useState(false);
|
||||
const [newRoleName, setNewRoleName] = useState('');
|
||||
const [newRoleDesc, setNewRoleDesc] = useState('');
|
||||
const [editingPermissions, setEditingPermissions] = useState<Set<string>>(new Set());
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
const headers = {
|
||||
'x-api-key': apiKey,
|
||||
'x-tenant-id': activeTenant?.tenantId || '',
|
||||
};
|
||||
|
||||
// 加载角色列表
|
||||
const fetchRoles = async () => {
|
||||
try {
|
||||
setIsLoadingRoles(true);
|
||||
const res = await fetch('/api/roles', { headers });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setRoles(data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Failed to fetch roles:', err);
|
||||
} finally {
|
||||
setIsLoadingRoles(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载权限元数据
|
||||
const fetchPermissions = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/permissions', { headers });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setAllPermissions(data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Failed to fetch permissions:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载选中角色的权限
|
||||
const fetchRolePermissions = async (roleId: string) => {
|
||||
try {
|
||||
setIsLoadingPerms(true);
|
||||
const res = await fetch(`/api/roles/${roleId}/permissions`, { headers });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const permSet = new Set<string>(data.permissions || []);
|
||||
setRolePermissions(permSet);
|
||||
setEditingPermissions(new Set(permSet));
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Failed to fetch role permissions:', err);
|
||||
} finally {
|
||||
setIsLoadingPerms(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRoles();
|
||||
fetchPermissions();
|
||||
}, [apiKey, activeTenant]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRoleId) {
|
||||
fetchRolePermissions(selectedRoleId);
|
||||
}
|
||||
}, [selectedRoleId]);
|
||||
|
||||
// 切换权限
|
||||
const togglePermission = (key: string) => {
|
||||
setEditingPermissions(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
}
|
||||
setHasChanges(true);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// 全选/取消分类
|
||||
const toggleCategory = (category: string, perms: PermissionMeta[]) => {
|
||||
const keys = perms.map(p => p.key);
|
||||
const allChecked = keys.every(k => editingPermissions.has(k));
|
||||
setEditingPermissions(prev => {
|
||||
const next = new Set(prev);
|
||||
keys.forEach(k => {
|
||||
if (allChecked) next.delete(k);
|
||||
else next.add(k);
|
||||
});
|
||||
setHasChanges(true);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// 保存权限
|
||||
const savePermissions = async () => {
|
||||
if (!selectedRoleId) return;
|
||||
try {
|
||||
setIsSaving(true);
|
||||
const res = await fetch(`/api/roles/${selectedRoleId}/permissions`, {
|
||||
method: 'PUT',
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ permissions: [...editingPermissions] }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setRolePermissions(new Set(editingPermissions));
|
||||
setHasChanges(false);
|
||||
showSuccess?.('权限已保存');
|
||||
} else {
|
||||
const err = await res.json();
|
||||
throw new Error(err.message || '保存失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError?.(err.message);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 创建角色
|
||||
const createRole = async () => {
|
||||
if (!newRoleName.trim()) return;
|
||||
try {
|
||||
const res = await fetch('/api/roles', {
|
||||
method: 'POST',
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: newRoleName.trim(), description: newRoleDesc.trim() }),
|
||||
});
|
||||
if (res.ok) {
|
||||
await fetchRoles();
|
||||
setShowCreateRole(false);
|
||||
setNewRoleName('');
|
||||
setNewRoleDesc('');
|
||||
showSuccess?.('角色已创建');
|
||||
} else {
|
||||
const err = await res.json();
|
||||
throw new Error(err.message || '创建失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError?.(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除角色
|
||||
const deleteRole = async (role: Role) => {
|
||||
const confirmed = await confirm?.(`确定删除角色"${role.name}"?`);
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
const res = await fetch(`/api/roles/${role.id}`, { method: 'DELETE', headers });
|
||||
if (res.ok) {
|
||||
if (selectedRoleId === role.id) setSelectedRoleId(null);
|
||||
await fetchRoles();
|
||||
showSuccess?.('角色已删除');
|
||||
} else {
|
||||
const err = await res.json();
|
||||
throw new Error(err.message || '删除失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError?.(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedRole = roles.find(r => r.id === selectedRoleId);
|
||||
|
||||
return (
|
||||
<div className="flex h-full gap-6">
|
||||
{/* 左:角色列表 */}
|
||||
<div className="w-72 flex-none space-y-3">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-black text-slate-900 flex items-center gap-2 uppercase tracking-widest">
|
||||
<Shield size={16} className="text-indigo-600" />
|
||||
角色
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowCreateRole(true)}
|
||||
className="p-1.5 rounded-lg bg-indigo-50 text-indigo-600 hover:bg-indigo-100 transition-colors"
|
||||
title="新建角色"
|
||||
>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isLoadingRoles ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<Loader2 size={20} className="animate-spin text-slate-400" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{roles.map(role => (
|
||||
<button
|
||||
key={role.id}
|
||||
onClick={() => setSelectedRoleId(role.id)}
|
||||
className={cn(
|
||||
'w-full text-left px-4 py-3 rounded-xl text-sm font-medium transition-all flex items-center justify-between',
|
||||
selectedRoleId === role.id
|
||||
? 'bg-indigo-50 text-indigo-700 border border-indigo-200 shadow-sm'
|
||||
: 'text-slate-600 hover:bg-slate-50 border border-transparent',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="truncate">{role.name}</span>
|
||||
{role.isSystem && (
|
||||
<span className="text-[9px] font-black text-indigo-400 bg-indigo-100/50 px-1.5 py-0.5 rounded uppercase tracking-wider shrink-0">
|
||||
系统
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!role.isSystem && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); deleteRole(role); }}
|
||||
className="p-1 text-slate-300 hover:text-rose-500 transition-colors shrink-0"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 创建角色弹窗 */}
|
||||
{showCreateRole && (
|
||||
<div className="bg-slate-50 rounded-xl p-4 border border-slate-200 space-y-3">
|
||||
<input
|
||||
value={newRoleName}
|
||||
onChange={e => setNewRoleName(e.target.value)}
|
||||
placeholder="角色名称"
|
||||
className="w-full px-3 py-2 text-sm bg-white border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 outline-none"
|
||||
/>
|
||||
<input
|
||||
value={newRoleDesc}
|
||||
onChange={e => setNewRoleDesc(e.target.value)}
|
||||
placeholder="角色描述(可选)"
|
||||
className="w-full px-3 py-2 text-sm bg-white border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 outline-none"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={createRole}
|
||||
disabled={!newRoleName.trim()}
|
||||
className="flex-1 py-2 bg-indigo-600 text-white text-xs font-bold rounded-lg hover:bg-indigo-700 disabled:bg-slate-300 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
创建
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCreateRole(false)}
|
||||
className="px-3 py-2 text-xs font-bold text-slate-500 hover:text-slate-700 bg-white border border-slate-200 rounded-lg transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右:权限矩阵 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{!selectedRole ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-slate-400 space-y-3">
|
||||
<Shield size={40} className="opacity-30" />
|
||||
<p className="text-sm font-bold">选择一个角色查看权限</p>
|
||||
</div>
|
||||
) : isLoadingPerms ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<Loader2 size={24} className="animate-spin text-slate-400" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* 角色标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-black text-slate-900 flex items-center gap-2">
|
||||
<Key size={18} className="text-indigo-600" />
|
||||
{selectedRole.name}
|
||||
{selectedRole.isSystem && (
|
||||
<span className="text-[10px] font-black text-slate-400 bg-slate-100 px-2 py-0.5 rounded-full uppercase tracking-wider">
|
||||
系统角色
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
{selectedRole.description && (
|
||||
<p className="text-xs text-slate-500 mt-1">{selectedRole.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingPermissions(new Set(rolePermissions));
|
||||
setHasChanges(false);
|
||||
}}
|
||||
disabled={!hasChanges}
|
||||
className="px-3 py-2 text-xs font-bold text-slate-500 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
<button
|
||||
onClick={savePermissions}
|
||||
disabled={!hasChanges || isSaving || selectedRole.isSystem}
|
||||
className={cn(
|
||||
'px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all',
|
||||
hasChanges && !selectedRole.isSystem
|
||||
? 'bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm'
|
||||
: 'bg-slate-100 text-slate-400 cursor-not-allowed',
|
||||
)}
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : (
|
||||
<Save size={14} />
|
||||
)}
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedRole.isSystem && (
|
||||
<div className="px-4 py-3 bg-amber-50 border border-amber-200 rounded-xl text-xs text-amber-700 font-medium">
|
||||
系统角色的权限不可修改。可以创建自定义角色,然后分配所需权限。
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 权限矩阵 */}
|
||||
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-2 custom-scrollbar">
|
||||
{Object.entries(allPermissions).map(([category, perms]) => {
|
||||
const allChecked = perms.every(p => editingPermissions.has(p.key));
|
||||
const someChecked = perms.some(p => editingPermissions.has(p.key));
|
||||
|
||||
return (
|
||||
<div key={category} className="bg-white border border-slate-100 rounded-2xl overflow-hidden">
|
||||
{/* 分类标题 */}
|
||||
<button
|
||||
onClick={() => toggleCategory(category, perms)}
|
||||
className="w-full flex items-center justify-between px-5 py-3 bg-slate-50/80 hover:bg-slate-100 transition-colors"
|
||||
>
|
||||
<span className="text-xs font-black text-slate-700 uppercase tracking-wider">
|
||||
{category}
|
||||
</span>
|
||||
<span className={cn(
|
||||
'text-[10px] font-bold px-2 py-0.5 rounded-full',
|
||||
allChecked
|
||||
? 'bg-indigo-100 text-indigo-600'
|
||||
: someChecked
|
||||
? 'bg-amber-100 text-amber-600'
|
||||
: 'bg-slate-100 text-slate-400',
|
||||
)}>
|
||||
{perms.filter(p => editingPermissions.has(p.key)).length}/{perms.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* 权限列表 */}
|
||||
<div className="divide-y divide-slate-50">
|
||||
{perms.map(perm => (
|
||||
<label
|
||||
key={perm.key}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-5 py-2.5 cursor-pointer transition-colors hover:bg-slate-50',
|
||||
!selectedRole.isSystem ? 'cursor-pointer' : 'cursor-not-allowed opacity-60',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editingPermissions.has(perm.key)}
|
||||
onChange={() => togglePermission(perm.key)}
|
||||
disabled={selectedRole.isSystem}
|
||||
className="w-4 h-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500/20 cursor-pointer disabled:cursor-not-allowed"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-bold text-slate-800">{perm.label}</div>
|
||||
<div className="text-xs text-slate-400">{perm.description}</div>
|
||||
</div>
|
||||
<code className="text-[10px] text-slate-300 font-mono shrink-0">{perm.key}</code>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -52,6 +52,7 @@ import { userSettingService } from '../../services/userSettingService';
|
||||
import { knowledgeGroupService } from '../../services/knowledgeGroupService';
|
||||
import { apiClient } from '../../services/apiClient';
|
||||
import { AssessmentTemplateManager } from './AssessmentTemplateManager';
|
||||
import { PermissionSettingsView } from './PermissionSettingsView';
|
||||
|
||||
import { useConfirm } from '../../contexts/ConfirmContext';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
@@ -66,7 +67,7 @@ interface SettingsViewProps {
|
||||
initialTab?: TabType;
|
||||
}
|
||||
|
||||
type TabType = 'general' | 'user' | 'model' | 'tenants' | 'knowledge_base' | 'import_tasks' | 'assessment_templates';
|
||||
type TabType = 'general' | 'user' | 'model' | 'tenants' | 'knowledge_base' | 'import_tasks' | 'assessment_templates' | 'permissions';
|
||||
|
||||
const buildTenantTree = (tenants: Tenant[]): Tenant[] => {
|
||||
const map = new Map<string, Tenant>();
|
||||
@@ -2131,6 +2132,16 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
||||
{t('assessmentTemplates')}
|
||||
</button>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={() => setActiveTab('permissions')}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all ${activeTab === 'permissions' ? 'bg-white text-indigo-600 shadow-sm border border-slate-200/60' : 'text-slate-600 hover:bg-slate-100'
|
||||
}`}
|
||||
>
|
||||
<Shield size={18} />
|
||||
权限管理
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2139,10 +2150,10 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
||||
<div className="px-8 pt-8 pb-6 flex items-start justify-between shrink-0">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 leading-tight">
|
||||
{activeTab === 'general' ? t('generalSettings') : activeTab === 'user' ? t('userManagement') : activeTab === 'model' ? t('modelManagement') : activeTab === 'knowledge_base' ? t('sidebarTitle') : activeTab === 'tenants' ? t('navTenants') : t('assessmentTemplates')}
|
||||
{activeTab === 'general' ? t('generalSettings') : activeTab === 'user' ? t('userManagement') : activeTab === 'model' ? t('modelManagement') : activeTab === 'knowledge_base' ? t('sidebarTitle') : activeTab === 'tenants' ? t('navTenants') : activeTab === 'permissions' ? '权限管理' : t('assessmentTemplates')}
|
||||
</h1>
|
||||
<p className="text-[15px] text-slate-500 mt-1">
|
||||
{activeTab === 'general' ? t('generalSettingsSubtitle') : activeTab === 'user' ? t('userManagementSubtitle') : activeTab === 'model' ? t('modelManagementSubtitle') : activeTab === 'knowledge_base' ? t('kbSettingsSubtitle') : activeTab === 'tenants' ? t('tenantsSubtitle') : t('assessmentTemplatesSubtitle')}
|
||||
{activeTab === 'general' ? t('generalSettingsSubtitle') : activeTab === 'user' ? t('userManagementSubtitle') : activeTab === 'model' ? t('modelManagementSubtitle') : activeTab === 'knowledge_base' ? t('kbSettingsSubtitle') : activeTab === 'tenants' ? t('tenantsSubtitle') : activeTab === 'permissions' ? '管理角色和细粒度权限' : t('assessmentTemplatesSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2183,6 +2194,11 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
|
||||
<AssessmentTemplateManager />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'permissions' && isAdmin && (
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar" style={{ height: 'calc(100vh - 220px)' }}>
|
||||
<PermissionSettingsView />
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user