195 lines
5.9 KiB
TypeScript
195 lines
5.9 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
|
import { Table, Button, Tag, Space, message, Modal, Form, Input, Select } from 'antd'
|
|
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
|
|
import axios from 'axios'
|
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
|
|
|
interface User {
|
|
id: number
|
|
username: string
|
|
email: string
|
|
role: string
|
|
is_active: boolean
|
|
created_at: string
|
|
}
|
|
|
|
function Users() {
|
|
const [users, setUsers] = useState<User[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [modalVisible, setModalVisible] = useState(false)
|
|
const [editingUser, setEditingUser] = useState<User | null>(null)
|
|
const tableRegionRef = useRef<HTMLDivElement | null>(null)
|
|
const [tableHeight, setTableHeight] = useState(360)
|
|
const [form] = Form.useForm()
|
|
|
|
const fetchUsers = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const res = await axios.get('/api/v1/users')
|
|
setUsers(res.data.data || [])
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchUsers()
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const updateTableHeight = () => {
|
|
const regionHeight = tableRegionRef.current?.offsetHeight || 0
|
|
setTableHeight(Math.max(220, regionHeight - 56))
|
|
}
|
|
|
|
updateTableHeight()
|
|
|
|
if (typeof ResizeObserver === 'undefined') {
|
|
return undefined
|
|
}
|
|
|
|
const observer = new ResizeObserver(updateTableHeight)
|
|
if (tableRegionRef.current) observer.observe(tableRegionRef.current)
|
|
|
|
return () => observer.disconnect()
|
|
}, [users.length])
|
|
|
|
const handleAdd = () => {
|
|
setEditingUser(null)
|
|
form.resetFields()
|
|
setModalVisible(true)
|
|
}
|
|
|
|
const handleEdit = (user: User) => {
|
|
setEditingUser(user)
|
|
form.setFieldsValue(user)
|
|
setModalVisible(true)
|
|
}
|
|
|
|
const handleDelete = async (id: number) => {
|
|
Modal.confirm({
|
|
title: '确认删除',
|
|
content: '确定要删除此用户吗?',
|
|
onOk: async () => {
|
|
await axios.delete(`/api/v1/users/${id}`)
|
|
message.success('删除成功')
|
|
fetchUsers()
|
|
},
|
|
})
|
|
}
|
|
|
|
const handleSubmit = async (values: Record<string, unknown>) => {
|
|
try {
|
|
if (editingUser) {
|
|
await axios.put(`/api/v1/users/${editingUser.id}`, values)
|
|
message.success('更新成功')
|
|
} else {
|
|
await axios.post('/api/v1/users', values)
|
|
message.success('创建成功')
|
|
}
|
|
setModalVisible(false)
|
|
fetchUsers()
|
|
} catch (error: unknown) {
|
|
const err = error as { response?: { data?: { detail?: string } } }
|
|
message.error(err.response?.data?.detail || '操作失败')
|
|
}
|
|
}
|
|
|
|
const columns = [
|
|
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
|
{ title: '用户名', dataIndex: 'username', key: 'username', width: 180 },
|
|
{ title: '邮箱', dataIndex: 'email', key: 'email', width: 260, ellipsis: true },
|
|
{
|
|
title: '角色',
|
|
dataIndex: 'role',
|
|
key: 'role',
|
|
width: 140,
|
|
render: (role: string) => {
|
|
const colors: Record<string, string> = {
|
|
super_admin: 'red',
|
|
admin: 'orange',
|
|
operator: 'blue',
|
|
viewer: 'green',
|
|
}
|
|
return <Tag color={colors[role] || 'default'}>{role}</Tag>
|
|
},
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'is_active',
|
|
key: 'is_active',
|
|
width: 120,
|
|
render: (active: boolean) => (
|
|
<Tag color={active ? 'green' : 'red'}>{active ? '活跃' : '禁用'}</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'action',
|
|
width: 180,
|
|
render: (_: unknown, record: User) => (
|
|
<Space>
|
|
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
|
|
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)}>删除</Button>
|
|
</Space>
|
|
),
|
|
},
|
|
]
|
|
|
|
return (
|
|
<AppLayout>
|
|
<div className="page-shell">
|
|
<div className="page-shell__header">
|
|
<h2 style={{ margin: 0 }}>用户管理</h2>
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加用户</Button>
|
|
</div>
|
|
<div className="page-shell__body">
|
|
<div ref={tableRegionRef} className="table-scroll-region data-source-table-region users-table-region" style={{ height: '100%' }}>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={users}
|
|
rowKey="id"
|
|
loading={loading}
|
|
scroll={{ x: 'max-content', y: tableHeight }}
|
|
tableLayout="fixed"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Modal
|
|
title={editingUser ? '编辑用户' : '添加用户'}
|
|
open={modalVisible}
|
|
onCancel={() => setModalVisible(false)}
|
|
footer={null}
|
|
>
|
|
<Form form={form} onFinish={handleSubmit} layout="vertical">
|
|
<Form.Item name="username" label="用户名" rules={[{ required: true }]}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item name="email" label="邮箱" rules={[{ required: true, type: 'email' }]}>
|
|
<Input />
|
|
</Form.Item>
|
|
{!editingUser && (
|
|
<Form.Item name="password" label="密码" rules={[{ required: true, min: 8 }]}>
|
|
<Input.Password />
|
|
</Form.Item>
|
|
)}
|
|
<Form.Item name="role" label="角色" rules={[{ required: true }]}>
|
|
<Select>
|
|
<Select.Option value="super_admin">超级管理员</Select.Option>
|
|
<Select.Option value="admin">管理员</Select.Option>
|
|
<Select.Option value="operator">操作员</Select.Option>
|
|
<Select.Option value="viewer">只读用户</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
<Form.Item>
|
|
<Button type="primary" htmlType="submit" block>提交</Button>
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</AppLayout>
|
|
)
|
|
}
|
|
|
|
export default Users
|