Files
zclaw_openfang/admin-v2/src/pages/Roles.tsx
iven 8898bb399e
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
docs: audit reports + feature docs + skills + admin-v2 + config sync
Update audit tracker, roadmap, architecture docs,
add admin-v2 Roles page + Billing tests,
sync CLAUDE.md, Cargo.toml, docker-compose.yml,
add deep-research / frontend-design / chart-visualization skills

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 19:25:00 +08:00

510 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================
// 角色与权限模板管理
// ============================================================
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import {
Button,
message,
Tag,
Modal,
Form,
Input,
Select,
Space,
Popconfirm,
Tabs,
Tooltip,
} from 'antd'
import { PlusOutlined, SafetyOutlined, CheckCircleOutlined } from '@ant-design/icons'
import type { ProColumns } from '@ant-design/pro-components'
import { ProTable } from '@ant-design/pro-components'
import { roleService } from '@/services/roles'
import { PageHeader } from '@/components/PageHeader'
import type {
Role,
PermissionTemplate,
CreateRoleRequest,
UpdateRoleRequest,
CreateTemplateRequest,
} from '@/types'
// ============================================================
// 常见权限选项
// ============================================================
const permissionOptions = [
{ value: 'account:admin', label: 'account:admin' },
{ value: 'provider:manage', label: 'provider:manage' },
{ value: 'model:read', label: 'model:read' },
{ value: 'model:write', label: 'model:write' },
{ value: 'relay:use', label: 'relay:use' },
{ value: 'knowledge:read', label: 'knowledge:read' },
{ value: 'knowledge:write', label: 'knowledge:write' },
{ value: 'billing:read', label: 'billing:read' },
{ value: 'billing:write', label: 'billing:write' },
{ value: 'config:read', label: 'config:read' },
{ value: 'config:write', label: 'config:write' },
{ value: 'prompt:read', label: 'prompt:read' },
{ value: 'prompt:write', label: 'prompt:write' },
{ value: 'admin:full', label: 'admin:full' },
]
// ============================================================
// Roles Tab
// ============================================================
function RolesTab() {
const queryClient = useQueryClient()
const [form] = Form.useForm()
const [modalOpen, setModalOpen] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null)
const { data, isLoading } = useQuery({
queryKey: ['roles'],
queryFn: ({ signal }) => roleService.list(signal),
})
const createMutation = useMutation({
mutationFn: (data: CreateRoleRequest) => roleService.create(data),
onSuccess: () => {
message.success('角色已创建')
queryClient.invalidateQueries({ queryKey: ['roles'] })
setModalOpen(false)
form.resetFields()
},
onError: (err: Error) => message.error(err.message || '创建失败'),
})
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateRoleRequest }) =>
roleService.update(id, data),
onSuccess: () => {
message.success('角色已更新')
queryClient.invalidateQueries({ queryKey: ['roles'] })
setModalOpen(false)
},
onError: (err: Error) => message.error(err.message || '更新失败'),
})
const deleteMutation = useMutation({
mutationFn: (id: string) => roleService.delete(id),
onSuccess: () => {
message.success('角色已删除')
queryClient.invalidateQueries({ queryKey: ['roles'] })
},
onError: (err: Error) => message.error(err.message || '删除失败'),
})
const handleSave = async () => {
const values = await form.validateFields()
if (editingId) {
updateMutation.mutate({ id: editingId, data: values })
} else {
createMutation.mutate(values)
}
}
const openEdit = async (record: Role) => {
setEditingId(record.id)
const permissions = await roleService.getPermissions(record.id).catch(() => record.permissions)
form.setFieldsValue({ ...record, permissions })
setModalOpen(true)
}
const openCreate = () => {
setEditingId(null)
form.resetFields()
setModalOpen(true)
}
const closeModal = () => {
setModalOpen(false)
setEditingId(null)
form.resetFields()
}
const columns: ProColumns<Role>[] = [
{
title: '角色名称',
dataIndex: 'name',
width: 160,
render: (_, record) => (
<span className="font-medium text-neutral-900 dark:text-neutral-100">
{record.name}
</span>
),
},
{
title: '描述',
dataIndex: 'description',
width: 240,
ellipsis: true,
render: (_, record) => record.description || '-',
},
{
title: '权限数',
dataIndex: 'permissions',
width: 100,
render: (_, record) => (
<Tooltip title={record.permissions?.join(', ') || '无权限'}>
<Tag>{record.permissions?.length ?? 0} </Tag>
</Tooltip>
),
},
{
title: '关联账号',
dataIndex: 'account_count',
width: 100,
render: (_, record) => record.account_count ?? 0,
},
{
title: '创建时间',
dataIndex: 'created_at',
width: 180,
render: (_, record) =>
record.created_at ? new Date(record.created_at).toLocaleString('zh-CN') : '-',
},
{
title: '操作',
width: 160,
render: (_, record) => (
<Space>
<Button size="small" onClick={() => openEdit(record)}>
</Button>
<Popconfirm
title="确定删除此角色?"
description="删除后关联的账号将失去此角色权限"
onConfirm={() => deleteMutation.mutate(record.id)}
>
<Button size="small" danger>
</Button>
</Popconfirm>
</Space>
),
},
]
return (
<div>
<ProTable<Role>
columns={columns}
dataSource={data ?? []}
loading={isLoading}
rowKey="id"
search={false}
toolBarRender={() => [
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>,
]}
pagination={{ showSizeChanger: false }}
/>
<Modal
title={editingId ? '编辑角色' : '新建角色'}
open={modalOpen}
onOk={handleSave}
onCancel={closeModal}
confirmLoading={createMutation.isPending || updateMutation.isPending}
width={560}
>
<Form form={form} layout="vertical" className="mt-4">
<Form.Item
name="name"
label="角色名称"
rules={[{ required: true, message: '请输入角色名称' }]}
>
<Input placeholder="如 editor, viewer" />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={2} placeholder="角色用途说明" />
</Form.Item>
<Form.Item name="permissions" label="权限">
<Select
mode="multiple"
placeholder="选择权限"
options={permissionOptions}
maxTagCount={5}
allowClear
filterOption={(input, option) =>
(option?.label as string)?.toLowerCase().includes(input.toLowerCase())
}
/>
</Form.Item>
</Form>
</Modal>
</div>
)
}
// ============================================================
// Permission Templates Tab
// ============================================================
function TemplatesTab() {
const queryClient = useQueryClient()
const [form] = Form.useForm()
const [modalOpen, setModalOpen] = useState(false)
const [applyOpen, setApplyOpen] = useState(false)
const [applyForm] = Form.useForm()
const [selectedTemplate, setSelectedTemplate] = useState<PermissionTemplate | null>(null)
const { data, isLoading } = useQuery({
queryKey: ['permission-templates'],
queryFn: ({ signal }) => roleService.listTemplates(signal),
})
const createMutation = useMutation({
mutationFn: (data: CreateTemplateRequest) => roleService.createTemplate(data),
onSuccess: () => {
message.success('模板已创建')
queryClient.invalidateQueries({ queryKey: ['permission-templates'] })
setModalOpen(false)
form.resetFields()
},
onError: (err: Error) => message.error(err.message || '创建失败'),
})
const deleteMutation = useMutation({
mutationFn: (id: string) => roleService.deleteTemplate(id),
onSuccess: () => {
message.success('模板已删除')
queryClient.invalidateQueries({ queryKey: ['permission-templates'] })
},
onError: (err: Error) => message.error(err.message || '删除失败'),
})
const applyMutation = useMutation({
mutationFn: ({ templateId, accountIds }: { templateId: string; accountIds: string[] }) =>
roleService.applyTemplate(templateId, accountIds),
onSuccess: () => {
message.success('模板已应用到所选账号')
queryClient.invalidateQueries({ queryKey: ['permission-templates'] })
setApplyOpen(false)
applyForm.resetFields()
setSelectedTemplate(null)
},
onError: (err: Error) => message.error(err.message || '应用失败'),
})
const openApply = (record: PermissionTemplate) => {
setSelectedTemplate(record)
applyForm.resetFields()
setApplyOpen(true)
}
const handleApply = async () => {
const values = await applyForm.validateFields()
if (!selectedTemplate) return
const accountIds = values.account_ids
?.split(',')
.map((s: string) => s.trim())
.filter(Boolean)
if (!accountIds?.length) {
message.warning('请输入至少一个账号 ID')
return
}
applyMutation.mutate({ templateId: selectedTemplate.id, accountIds })
}
const columns: ProColumns<PermissionTemplate>[] = [
{
title: '模板名称',
dataIndex: 'name',
width: 180,
render: (_, record) => (
<span className="font-medium text-neutral-900 dark:text-neutral-100">
{record.name}
</span>
),
},
{
title: '描述',
dataIndex: 'description',
width: 240,
ellipsis: true,
render: (_, record) => record.description || '-',
},
{
title: '权限数',
dataIndex: 'permissions',
width: 100,
render: (_, record) => (
<Tooltip title={record.permissions?.join(', ') || '无权限'}>
<Tag>{record.permissions?.length ?? 0} </Tag>
</Tooltip>
),
},
{
title: '创建时间',
dataIndex: 'created_at',
width: 180,
render: (_, record) =>
record.created_at ? new Date(record.created_at).toLocaleString('zh-CN') : '-',
},
{
title: '操作',
width: 180,
render: (_, record) => (
<Space>
<Button
size="small"
icon={<CheckCircleOutlined />}
onClick={() => openApply(record)}
>
</Button>
<Popconfirm
title="确定删除此模板?"
description="删除后已应用的账号不受影响"
onConfirm={() => deleteMutation.mutate(record.id)}
>
<Button size="small" danger>
</Button>
</Popconfirm>
</Space>
),
},
]
return (
<div>
<ProTable<PermissionTemplate>
columns={columns}
dataSource={data ?? []}
loading={isLoading}
rowKey="id"
search={false}
toolBarRender={() => [
<Button
key="add"
type="primary"
icon={<PlusOutlined />}
onClick={() => {
form.resetFields()
setModalOpen(true)
}}
>
</Button>,
]}
pagination={{ showSizeChanger: false }}
/>
{/* Create Template Modal */}
<Modal
title="新建权限模板"
open={modalOpen}
onOk={async () => {
const values = await form.validateFields()
createMutation.mutate(values)
}}
onCancel={() => {
setModalOpen(false)
form.resetFields()
}}
confirmLoading={createMutation.isPending}
width={560}
>
<Form form={form} layout="vertical" className="mt-4">
<Form.Item
name="name"
label="模板名称"
rules={[{ required: true, message: '请输入模板名称' }]}
>
<Input placeholder="如 basic-user, power-user" />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={2} placeholder="模板用途说明" />
</Form.Item>
<Form.Item name="permissions" label="权限">
<Select
mode="multiple"
placeholder="选择权限"
options={permissionOptions}
maxTagCount={5}
allowClear
filterOption={(input, option) =>
(option?.label as string)?.toLowerCase().includes(input.toLowerCase())
}
/>
</Form.Item>
</Form>
</Modal>
{/* Apply Template Modal */}
<Modal
title={`应用模板: ${selectedTemplate?.name ?? ''}`}
open={applyOpen}
onOk={handleApply}
onCancel={() => {
setApplyOpen(false)
setSelectedTemplate(null)
applyForm.resetFields()
}}
confirmLoading={applyMutation.isPending}
width={480}
>
<Form form={applyForm} layout="vertical" className="mt-4">
<div className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
{selectedTemplate?.permissions?.length ?? 0}
ID ID
</div>
<Form.Item
name="account_ids"
label="账号 ID"
rules={[{ required: true, message: '请输入账号 ID' }]}
>
<Input.TextArea
rows={3}
placeholder="如: acc_abc123, acc_def456"
/>
</Form.Item>
</Form>
</Modal>
</div>
)
}
// ============================================================
// Main Page: Roles & Permissions
// ============================================================
export default function Roles() {
return (
<div>
<PageHeader
title="角色与权限"
description="管理角色、权限模板,并将权限批量应用到账号"
/>
<Tabs
defaultActiveKey="roles"
items={[
{
key: 'roles',
label: (
<span className="flex items-center gap-1.5">
<SafetyOutlined />
</span>
),
children: <RolesTab />,
},
{
key: 'templates',
label: (
<span className="flex items-center gap-1.5">
<CheckCircleOutlined />
</span>
),
children: <TemplatesTab />,
},
]}
/>
</div>
)
}