137 lines
7.4 KiB
TypeScript
137 lines
7.4 KiB
TypeScript
// 系统管理(重设计方案 阶段②):全屏独立视图(带返回,非弹窗)。
|
|
// 用户决策:系统管理 = 全屏独立视图;普通用户界面不暴露任何系统能力。
|
|
// 分区注册表:新增管理页只改 SECTIONS,不改 AdminZone 本体(声明式注册)。
|
|
// WP3-I3/KTD4:编译插件可经 Registry.AddAdminZone 声明展示分区(服务端
|
|
// /api/admin-zone/sections 下发);声明分区对全部认证主体开放,区内数据
|
|
// 能力由分区组件自身的命令面 fail-closed——前端只消费、不合并角色。
|
|
import { useState } from 'react'
|
|
import { store, useStore } from '../store'
|
|
import { EmptyState, Icon, type IconName } from '@ui'
|
|
import { UsersPanel, ProfilesSection, PluginsSection, PluginWorkspacesPanel, SystemSettingsPanel } from './adminSections'
|
|
import { ExecEnvPanel } from './ExecEnvPanel'
|
|
import { RdmAuditWorkspace } from './RdmAuditWorkspace'
|
|
import { AccountUsersPanel } from './AccountUsersPanel'
|
|
import { RolePermissionsPanel, roleDraftLeaveGuard, hasSystemAction } from './RolePermissionsPanel'
|
|
import type { AdminZoneSection } from '../api'
|
|
|
|
interface AdminSectionDef {
|
|
key: string
|
|
label: string
|
|
icon: IconName
|
|
desc: string
|
|
requiredAction?: string
|
|
rootOnly?: boolean
|
|
/** 懒渲染:进入分区才挂载(每个面板自带数据加载)。 */
|
|
render: () => React.ReactNode
|
|
}
|
|
|
|
function ProfilesPanelWrapper() {
|
|
const profiles = useStore(s => s.profiles)
|
|
return <ProfilesSection profiles={profiles} />
|
|
}
|
|
|
|
/** 账号目录(按系统权限开放)+ 登录凭据与恢复提案(根管理员专属,旧 /api/admin/users 列表与密码端点)。 */
|
|
function UsersSection() {
|
|
const isAdmin = useStore(s => !!s.me?.isAdmin)
|
|
return <>
|
|
<AccountUsersPanel />
|
|
{isAdmin && <>
|
|
<div className="az-section-head" style={{ marginTop: 18 }}>
|
|
<h3>登录凭据与恢复</h3>
|
|
<p>建号并发放初始密码、重置临时密码、发起恢复提案(仅根管理员)</p>
|
|
</div>
|
|
<UsersPanel />
|
|
</>}
|
|
</>
|
|
}
|
|
|
|
const SECTIONS: AdminSectionDef[] = [
|
|
{ key: 'users', label: '用户管理', icon: 'users', desc: '可选资料、账号状态、唯一生效角色与明确系统权限;根管理员另可发放与恢复登录凭据', requiredAction: 'system.users.view', render: () => <UsersSection /> },
|
|
{ key: 'roles', label: '角色与权限', icon: 'shield', desc: '职位角色、默认权限与上限;支持单角色和角色矩阵配置', requiredAction: 'system.roles.view', render: () => <RolePermissionsPanel /> },
|
|
{ key: 'profiles', label: '智能体画像', icon: 'bot', desc: '系统级画像:系统提示、工具允许/确认/拒绝清单', rootOnly: true, render: () => <ProfilesPanelWrapper /> },
|
|
// P2 受管:模型/密钥/Tavily/遥测/运行基础设施的唯一写入面(Backend PR 188 系统设置 API)。
|
|
{ key: 'system-settings', label: '模型与服务配置', icon: 'settings', desc: '系统受管项:模型、密钥、Tavily、遥测与运行基础设施(仅管理员)', rootOnly: true, render: () => <SystemSettingsPanel /> },
|
|
{ key: 'plugins', label: '插件管理', icon: 'puzzle', desc: '已装载插件清单与热重载(装卸不影响核心)', rootOnly: true, render: () => <PluginsSection /> },
|
|
{ key: 'python-env', label: 'Python 环境', icon: 'package', desc: '基础层、扩展申请、回收和 wheel 上传(仅管理员)', rootOnly: true, render: () => <ExecEnvPanel /> },
|
|
{ key: 'workspaces', label: '插件工作区', icon: 'grid', desc: '插件声明面板与服务端声明工作区', rootOnly: true, render: () => <PluginWorkspacesPanel /> },
|
|
]
|
|
|
|
/** 声明分区 → 内置分区组件(按声明 ID 映射;AdminZone 不硬编码领域逻辑)。 */
|
|
const DECLARED_COMPONENTS: Record<string, () => React.ReactNode> = {
|
|
'rdm.audit': () => <RdmAuditWorkspace />,
|
|
}
|
|
|
|
/** icon 走基座 IconName 词表;声明携带未知图标时回退通用图标(不渲染空白)。 */
|
|
const ICONS: IconName[] = ['plus', 'search', 'panel-left', 'inbox', 'settings', 'sun', 'moon', 'pin', 'archive', 'undo',
|
|
'chevron-right', 'chevron-down', 'folder', 'message', 'package', 'bot', 'puzzle', 'grid', 'arrow-up', 'paperclip',
|
|
'more', 'logout', 'key', 'user', 'users', 'shield', 'x', 'check', 'alert', 'send', 'sparkles', 'clock', 'list',
|
|
'file', 'image', 'download', 'trash', 'eye', 'maximize', 'minimize']
|
|
function declaredIcon(name?: string): IconName {
|
|
return (ICONS as string[]).includes(name ?? '') ? (name as IconName) : 'grid'
|
|
}
|
|
|
|
function DeclaredSection({ section }: { section: AdminZoneSection }) {
|
|
const render = DECLARED_COMPONENTS[section.id]
|
|
if (!render) {
|
|
return <EmptyState icon="puzzle">分区「{section.title}」由内置插件声明,但当前前端版本未携带其组件<br />(部署前端与二进制版本不匹配时会出现;升级前端即可)</EmptyState>
|
|
}
|
|
return <>{render()}</>
|
|
}
|
|
|
|
export function AdminZone() {
|
|
const me = useStore(s => s.me)
|
|
const declared = useStore(s => s.adminSections)
|
|
const [sectionKey, setSectionKey] = useState('users')
|
|
const isAdmin = !!me?.isAdmin
|
|
const systemActions = me?.systemActions ?? []
|
|
const canOpenSystem = isAdmin || systemActions.length > 0
|
|
// 门控:基座系统分区仅 isAdmin(纵深防御与菜单层一致);声明分区对全部
|
|
// 认证主体开放(区内能力由组件命令面判定)。两者皆无时不渲染。
|
|
if (!canOpenSystem && declared.length === 0) return null
|
|
const sections: AdminSectionDef[] = [
|
|
...SECTIONS.filter(section => section.rootOnly ? isAdmin : isAdmin || hasSystemAction(systemActions, section.requiredAction ?? '')),
|
|
...declared.map(s => ({
|
|
key: `decl:${s.id}`, label: s.title, icon: declaredIcon(s.icon),
|
|
desc: s.desc || '插件声明的管理分区(区内数据能力由插件命令面判定)',
|
|
render: () => <DeclaredSection section={s} />,
|
|
})),
|
|
]
|
|
const fallback = sections[0]?.key ?? ''
|
|
const section = sections.find(s => s.key === sectionKey) ?? sections[0]
|
|
if (!section) return null
|
|
return (
|
|
<div className="az" role="dialog" aria-label="系统管理">
|
|
<div className="az-head">
|
|
<button className="btn ghost" onClick={() => {
|
|
// DR9:带未保存权限草稿离开须显式决定;守卫返回 false 表示用户取消。
|
|
if (roleDraftLeaveGuard.current && !roleDraftLeaveGuard.current()) return
|
|
store.setUI({ adminZoneOpen: false })
|
|
}}>
|
|
<Icon name="chevron-down" size={14} /> 返回工作台
|
|
</button>
|
|
<span className="az-title">系统管理</span>
|
|
<span className="az-sub">{me?.isSuperAdmin ? '超级管理员' : isAdmin ? '根管理员' : '按系统权限开放'} · 角色与权限由服务端判定</span>
|
|
<span style={{ flex: 1 }} />
|
|
<span className="az-tenant">{me?.owner ?? ''}</span>
|
|
</div>
|
|
<div className="az-body">
|
|
<nav className="az-side">
|
|
{sections.map(s => (
|
|
<button key={s.key} className={`az-nav-item ${s.key === (section?.key ?? fallback) ? 'on' : ''}`} onClick={() => setSectionKey(s.key)}>
|
|
<Icon name={s.icon} size={15} />
|
|
<span>{s.label}</span>
|
|
</button>
|
|
))}
|
|
</nav>
|
|
<main className="az-main">
|
|
<div className="az-section-head">
|
|
<h3>{section.label}</h3>
|
|
<p>{section.desc}</p>
|
|
</div>
|
|
{section.render()}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|