223 lines
9.6 KiB
TypeScript
223 lines
9.6 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
|
import {
|
|
Typography, Card, List, Button, Checkbox, Select, Space, message,
|
|
Modal, Form, Input, Switch, Divider, Empty, Tag
|
|
} from 'antd'
|
|
import { PlusOutlined, SaveOutlined, CrownOutlined, ToolOutlined } from '@ant-design/icons'
|
|
import { roleApi, RoleItem, PermissionItem } from '../../api/system'
|
|
|
|
export default function RolesPage() {
|
|
const [roles, setRoles] = useState<RoleItem[]>([])
|
|
const [permissions, setPermissions] = useState<PermissionItem[]>([])
|
|
const [selected, setSelected] = useState<RoleItem | null>(null)
|
|
const [checked, setChecked] = useState<number[]>([])
|
|
const [dataScope, setDataScope] = useState('all')
|
|
const [agentAutoExecute, setAgentAutoExecute] = useState(false)
|
|
const [saving, setSaving] = useState(false)
|
|
const [createOpen, setCreateOpen] = useState(false)
|
|
const [createForm] = Form.useForm()
|
|
|
|
const menuPerms = useMemo(() => permissions.filter((p) => p.resource === 'menu'), [permissions])
|
|
const toolPerms = useMemo(() => permissions.filter((p) => p.resource === 'agent_tool'), [permissions])
|
|
|
|
const loadRoles = async (keepSelected = true) => {
|
|
const res: any = await roleApi.list()
|
|
const data: RoleItem[] = res.data || []
|
|
setRoles(data)
|
|
if (!keepSelected) return
|
|
if (selected) {
|
|
const updated = data.find((r) => r.id === selected.id)
|
|
if (updated) selectRole(updated)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadRoles(false)
|
|
roleApi.permissions().then((res: any) => setPermissions(res.data || []))
|
|
}, [])
|
|
|
|
const selectRole = (r: RoleItem) => {
|
|
setSelected(r)
|
|
setChecked(r.permissionIds || [])
|
|
setDataScope(r.dataScope || 'all')
|
|
setAgentAutoExecute(!!r.agentAutoExecute)
|
|
}
|
|
|
|
const save = async () => {
|
|
if (!selected) return
|
|
setSaving(true)
|
|
try {
|
|
await roleApi.update(selected.id, {
|
|
name: selected.name,
|
|
description: selected.description,
|
|
dataScope,
|
|
agentAutoExecute,
|
|
permissionIds: checked,
|
|
})
|
|
message.success('配置已保存')
|
|
await loadRoles()
|
|
} catch { /* ignore */ }
|
|
setSaving(false)
|
|
}
|
|
|
|
const createRole = async () => {
|
|
const values = await createForm.validateFields()
|
|
const res: any = await roleApi.create({ ...values, dataScope: 'all', agentAutoExecute: false, permissionIds: [] })
|
|
message.success('角色已创建')
|
|
setCreateOpen(false)
|
|
createForm.resetFields()
|
|
await loadRoles(false)
|
|
selectRole(res.data)
|
|
}
|
|
|
|
const menuChecked = menuPerms.every((p) => checked.includes(p.id))
|
|
const toolChecked = toolPerms.every((p) => checked.includes(p.id))
|
|
|
|
const toggleAll = (list: PermissionItem[], allChecked: boolean) => {
|
|
const ids = list.map((p) => p.id)
|
|
setChecked((prev) => allChecked
|
|
? prev.filter((id) => !ids.includes(id))
|
|
: Array.from(new Set([...prev, ...ids])))
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
|
<Typography.Title level={4} style={{ margin: 0 }}>角色权限配置</Typography.Title>
|
|
<Button type="primary" icon={<SaveOutlined />} onClick={save} loading={saving} disabled={!selected}>保存配置</Button>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 16 }}>
|
|
<Card size="small" title="角色列表" style={{ width: 260, flexShrink: 0 }}>
|
|
<List
|
|
dataSource={roles}
|
|
renderItem={(r) => (
|
|
<List.Item
|
|
onClick={() => selectRole(r)}
|
|
style={{
|
|
cursor: 'pointer', padding: '8px 12px', borderRadius: 6,
|
|
background: selected?.id === r.id ? '#e6f4ff' : undefined,
|
|
}}
|
|
>
|
|
<List.Item.Meta
|
|
avatar={<CrownOutlined style={{ color: r.id === 1 ? '#faad14' : '#999' }} />}
|
|
title={r.name}
|
|
description={r.description}
|
|
/>
|
|
</List.Item>
|
|
)}
|
|
/>
|
|
<Button block type="dashed" icon={<PlusOutlined />} style={{ marginTop: 12 }} onClick={() => setCreateOpen(true)}>
|
|
新增角色
|
|
</Button>
|
|
</Card>
|
|
<Card size="small" title={selected ? `权限配置 — ${selected.name}` : '权限配置'} style={{ flex: 1 }}>
|
|
{!selected ? (
|
|
<Empty description="请选择左侧角色" style={{ padding: 48 }} />
|
|
) : (
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
<div>
|
|
<Typography.Text strong>数据权限范围</Typography.Text>
|
|
<Select
|
|
style={{ width: 220, marginLeft: 12 }} value={dataScope}
|
|
onChange={(v) => setDataScope(v)}
|
|
options={[
|
|
{ value: 'all', label: '全系统数据' },
|
|
{ value: 'dept', label: '所属部门数据' },
|
|
{ value: 'own', label: '仅本人相关数据' },
|
|
]}
|
|
/>
|
|
<Typography.Text strong style={{ marginLeft: 32 }}>全局 Agent 自动执行</Typography.Text>
|
|
<Switch style={{ marginLeft: 12 }} checked={agentAutoExecute} onChange={setAgentAutoExecute} />
|
|
</div>
|
|
<Divider style={{ margin: '4px 0' }} />
|
|
<div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<Typography.Text strong>页面与功能权限</Typography.Text>
|
|
<Checkbox
|
|
checked={menuPerms.length > 0 && menuChecked}
|
|
indeterminate={menuPerms.length > 0 && !menuChecked && menuPerms.some((p) => checked.includes(p.id))}
|
|
onChange={(e) => toggleAll(menuPerms, e.target.checked)}
|
|
>
|
|
全选
|
|
</Checkbox>
|
|
</div>
|
|
<Checkbox.Group
|
|
style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 8 }}
|
|
value={checked}
|
|
onChange={(v) => setChecked(v as number[])}
|
|
>
|
|
{menuPerms.map((p) => (
|
|
<Checkbox key={p.id} value={p.id}>{p.name} <Typography.Text type="secondary">({p.code})</Typography.Text></Checkbox>
|
|
))}
|
|
</Checkbox.Group>
|
|
</div>
|
|
<Divider style={{ margin: '4px 0' }} />
|
|
<div style={{
|
|
background: 'linear-gradient(135deg, #1a73e8, #4a90f2)',
|
|
borderRadius: 16, padding: 20, color: '#fff',
|
|
boxShadow: '0 8px 24px rgba(26,115,232,.18)',
|
|
position: 'relative', overflow: 'hidden',
|
|
}}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<ToolOutlined style={{ fontSize: 18 }} />
|
|
<Typography.Text strong style={{ color: '#fff', fontSize: 15 }}>
|
|
Agent 工具调用权限 <Tag color="gold" style={{ marginLeft: 6 }}>核心</Tag>
|
|
</Typography.Text>
|
|
</div>
|
|
<Checkbox
|
|
checked={toolPerms.length > 0 && toolChecked}
|
|
indeterminate={toolPerms.length > 0 && !toolChecked && toolPerms.some((p) => checked.includes(p.id))}
|
|
onChange={(e) => toggleAll(toolPerms, e.target.checked)}
|
|
style={{ color: '#fff' }}
|
|
>
|
|
全选
|
|
</Checkbox>
|
|
</div>
|
|
<p style={{ color: 'rgba(255,255,255,.78)', fontSize: 12, marginBottom: 14 }}>
|
|
配置此角色在与 AI Agent 交互时,允许 Agent 自动执行的原子工具。敏感操作建议开启"人机协同审批"。
|
|
</p>
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 8 }}>
|
|
{toolPerms.map((p) => (
|
|
<label
|
|
key={p.id}
|
|
style={{
|
|
display: 'flex', alignItems: 'center', gap: 8, padding: '10px 12px',
|
|
background: 'rgba(255,255,255,.12)', border: '1px solid rgba(255,255,255,.22)',
|
|
borderRadius: 10, cursor: 'pointer',
|
|
}}
|
|
>
|
|
<Checkbox
|
|
checked={checked.includes(p.id)}
|
|
onChange={(e) => {
|
|
setChecked((prev) => e.target.checked
|
|
? Array.from(new Set([...prev, p.id]))
|
|
: prev.filter((i) => i !== p.id))
|
|
}}
|
|
/>
|
|
<code style={{ color: '#fff', fontFamily: "'JetBrains Mono', Consolas, monospace", fontSize: 12, fontWeight: 700 }}>
|
|
{p.code}
|
|
</code>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Space>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
|
|
<Modal title="新增角色" open={createOpen} onCancel={() => setCreateOpen(false)} onOk={createRole}>
|
|
<Form form={createForm} layout="vertical">
|
|
<Form.Item name="name" label="角色名称" rules={[{ required: true, message: '请输入角色名称' }]}>
|
|
<Input placeholder="请输入角色名称" />
|
|
</Form.Item>
|
|
<Form.Item name="description" label="描述">
|
|
<Input placeholder="角色描述" />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|