Users.tsx 8.5 KB

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