pages_users.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import React, { useState } from 'react';
  2. import {
  3. Button, Table, Space, Form, Input, Select,
  4. message, Modal, Card, Typography, Tag, Popconfirm
  5. } from 'antd';
  6. import { useQuery } from '@tanstack/react-query';
  7. import dayjs from 'dayjs';
  8. import { UserAPI } from './api/users.ts';
  9. import { AxiosError } from "axios";
  10. const { Title } = Typography;
  11. // 用户管理页面
  12. export const UsersPage = () => {
  13. const [searchParams, setSearchParams] = useState({
  14. page: 1,
  15. limit: 10,
  16. search: ''
  17. });
  18. const [modalVisible, setModalVisible] = useState(false);
  19. const [modalTitle, setModalTitle] = useState('');
  20. const [editingUser, setEditingUser] = useState<any>(null);
  21. const [form] = Form.useForm();
  22. const [convertingId, setConvertingId] = useState<number | null>(null);
  23. const { data: usersData, isLoading, refetch } = useQuery({
  24. queryKey: ['users', searchParams],
  25. queryFn: async () => {
  26. return await UserAPI.getUsers(searchParams);
  27. }
  28. });
  29. const users = usersData?.data || [];
  30. const pagination = {
  31. current: searchParams.page,
  32. pageSize: searchParams.limit,
  33. total: usersData?.pagination?.total || 0
  34. };
  35. // 处理搜索
  36. const handleSearch = (values: any) => {
  37. setSearchParams(prev => ({
  38. ...prev,
  39. search: values.search || '',
  40. page: 1
  41. }));
  42. };
  43. // 处理分页变化
  44. const handleTableChange = (newPagination: any) => {
  45. setSearchParams(prev => ({
  46. ...prev,
  47. page: newPagination.current,
  48. limit: newPagination.pageSize
  49. }));
  50. };
  51. // 打开创建用户模态框
  52. const showCreateModal = () => {
  53. setModalTitle('创建用户');
  54. setEditingUser(null);
  55. form.resetFields();
  56. setModalVisible(true);
  57. };
  58. // 打开编辑用户模态框
  59. const showEditModal = (user: any) => {
  60. setModalTitle('编辑用户');
  61. setEditingUser(user);
  62. form.setFieldsValue(user);
  63. setModalVisible(true);
  64. };
  65. // 处理模态框确认
  66. const handleModalOk = async () => {
  67. try {
  68. const values = await form.validateFields();
  69. if (editingUser) {
  70. // 编辑用户
  71. await UserAPI.updateUser(editingUser.id, values);
  72. message.success('用户更新成功');
  73. } else {
  74. // 创建用户
  75. await UserAPI.createUser(values);
  76. message.success('用户创建成功');
  77. }
  78. setModalVisible(false);
  79. form.resetFields();
  80. refetch(); // 刷新用户列表
  81. } catch (error) {
  82. console.error('表单提交失败:', error);
  83. message.error('操作失败,请重试');
  84. }
  85. };
  86. // 处理删除用户
  87. const handleDelete = async (id: number) => {
  88. try {
  89. await UserAPI.deleteUser(id);
  90. message.success('用户删除成功');
  91. refetch(); // 刷新用户列表
  92. } catch (error) {
  93. console.error('删除用户失败:', error);
  94. message.error('删除失败,请重试');
  95. }
  96. };
  97. // 处理转为学员
  98. const handleConvertToStudent = async (id: number) => {
  99. setConvertingId(id);
  100. try {
  101. await UserAPI.convertToStudent(id);
  102. message.success('用户已转为学员');
  103. refetch(); // 刷新用户列表
  104. } catch (error) {
  105. console.error('转为学员失败:', error);
  106. if(error instanceof AxiosError)
  107. message.error(error.response?.data.error || '操作失败,请重试');
  108. else
  109. message.error('操作失败,请重试');
  110. } finally {
  111. setConvertingId(null);
  112. }
  113. };
  114. const columns = [
  115. {
  116. title: '用户名',
  117. dataIndex: 'username',
  118. key: 'username',
  119. },
  120. {
  121. title: '昵称',
  122. dataIndex: 'nickname',
  123. key: 'nickname',
  124. },
  125. {
  126. title: '邮箱',
  127. dataIndex: 'email',
  128. key: 'email',
  129. },
  130. {
  131. title: '角色',
  132. dataIndex: 'role',
  133. key: 'role',
  134. render: (role: string) => (
  135. <Tag color={role === 'admin' ? 'red' : 'blue'}>
  136. {role }
  137. </Tag>
  138. ),
  139. },
  140. {
  141. title: '创建时间',
  142. dataIndex: 'created_at',
  143. key: 'created_at',
  144. render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
  145. },
  146. {
  147. title: '有效期至',
  148. dataIndex: 'expires_at',
  149. key: 'expires_at',
  150. render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD') : '永久',
  151. },
  152. {
  153. title: '操作',
  154. key: 'action',
  155. render: (_: any, record: any) => (
  156. <Space size="middle">
  157. <Button type="link" onClick={() => showEditModal(record)}>
  158. 编辑
  159. </Button>
  160. {record.role === 'admin' && (
  161. <Popconfirm
  162. title="确定要删除此用户吗?"
  163. onConfirm={() => handleDelete(record.id)}
  164. okText="确定"
  165. cancelText="取消"
  166. >
  167. <Button type="link" danger>
  168. 删除
  169. </Button>
  170. </Popconfirm>
  171. )}
  172. {record.role !== 'student' && (
  173. <Button
  174. type="link"
  175. onClick={() => handleConvertToStudent(record.id)}
  176. loading={convertingId === record.id}
  177. >
  178. 转为学员
  179. </Button>
  180. )}
  181. </Space>
  182. ),
  183. },
  184. ];
  185. return (
  186. <div>
  187. <Title level={2}>用户管理</Title>
  188. <Card>
  189. <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
  190. <Form.Item name="search" label="搜索">
  191. <Input placeholder="用户名/昵称/邮箱" allowClear />
  192. </Form.Item>
  193. <Form.Item>
  194. <Space>
  195. <Button type="primary" htmlType="submit">
  196. 搜索
  197. </Button>
  198. <Button type="primary" onClick={showCreateModal}>
  199. 创建用户
  200. </Button>
  201. </Space>
  202. </Form.Item>
  203. </Form>
  204. <Table
  205. columns={columns}
  206. dataSource={users}
  207. loading={isLoading}
  208. rowKey="id"
  209. pagination={{
  210. ...pagination,
  211. showSizeChanger: true,
  212. showQuickJumper: true,
  213. showTotal: (total) => `共 ${total} 条记录`
  214. }}
  215. onChange={handleTableChange}
  216. />
  217. </Card>
  218. {/* 创建/编辑用户模态框 */}
  219. <Modal
  220. title={modalTitle}
  221. open={modalVisible}
  222. onOk={handleModalOk}
  223. onCancel={() => {
  224. setModalVisible(false);
  225. form.resetFields();
  226. }}
  227. width={600}
  228. >
  229. <Form
  230. form={form}
  231. layout="vertical"
  232. >
  233. <Form.Item
  234. name="username"
  235. label="用户名"
  236. rules={[
  237. { required: true, message: '请输入用户名' },
  238. { min: 3, message: '用户名至少3个字符' }
  239. ]}
  240. >
  241. <Input placeholder="请输入用户名" />
  242. </Form.Item>
  243. <Form.Item
  244. name="nickname"
  245. label="昵称"
  246. rules={[{ required: true, message: '请输入昵称' }]}
  247. >
  248. <Input placeholder="请输入昵称" />
  249. </Form.Item>
  250. <Form.Item
  251. name="email"
  252. label="邮箱"
  253. rules={[
  254. { required: true, message: '请输入邮箱' },
  255. { type: 'email', message: '请输入有效的邮箱地址' }
  256. ]}
  257. >
  258. <Input placeholder="请输入邮箱" />
  259. </Form.Item>
  260. {!editingUser && (
  261. <Form.Item
  262. name="password"
  263. label="密码"
  264. rules={[
  265. { required: true, message: '请输入密码' },
  266. { min: 6, message: '密码至少6个字符' }
  267. ]}
  268. >
  269. <Input.Password placeholder="请输入密码" />
  270. </Form.Item>
  271. )}
  272. <Form.Item
  273. name="role"
  274. label="角色"
  275. rules={[{ required: true, message: '请选择角色' }]}
  276. >
  277. <Select placeholder="请选择角色">
  278. <Select.Option value="user">普通用户</Select.Option>
  279. <Select.Option value="admin">管理员</Select.Option>
  280. </Select>
  281. </Form.Item>
  282. </Form>
  283. </Modal>
  284. </div>
  285. );
  286. };