UsersTable.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. import React, { useEffect, useState } from 'react';
  2. import { API, showError, showSuccess } from '../helpers';
  3. import {
  4. Button,
  5. Form,
  6. Popconfirm,
  7. Space,
  8. Table,
  9. Tag,
  10. Tooltip,
  11. } from '@douyinfe/semi-ui';
  12. import { ITEMS_PER_PAGE } from '../constants';
  13. import { renderGroup, renderNumber, renderQuota } from '../helpers/render';
  14. import AddUser from '../pages/User/AddUser';
  15. import EditUser from '../pages/User/EditUser';
  16. import { useTranslation } from 'react-i18next';
  17. const UsersTable = () => {
  18. const { t } = useTranslation();
  19. function renderRole(role) {
  20. switch (role) {
  21. case 1:
  22. return <Tag size='large'>{t('普通用户')}</Tag>;
  23. case 10:
  24. return (
  25. <Tag color='yellow' size='large'>
  26. {t('管理员')}
  27. </Tag>
  28. );
  29. case 100:
  30. return (
  31. <Tag color='orange' size='large'>
  32. {t('超级管理员')}
  33. </Tag>
  34. );
  35. default:
  36. return (
  37. <Tag color='red' size='large'>
  38. {t('未知身份')}
  39. </Tag>
  40. );
  41. }
  42. }
  43. const columns = [
  44. {
  45. title: 'ID',
  46. dataIndex: 'id',
  47. },
  48. {
  49. title: t('用户名'),
  50. dataIndex: 'username',
  51. },
  52. {
  53. title: t('分组'),
  54. dataIndex: 'group',
  55. render: (text, record, index) => {
  56. return <div>{renderGroup(text)}</div>;
  57. },
  58. },
  59. {
  60. title: t('统计信息'),
  61. dataIndex: 'info',
  62. render: (text, record, index) => {
  63. return (
  64. <div>
  65. <Space spacing={1}>
  66. <Tooltip content={t('剩余额度')}>
  67. <Tag color='white' size='large'>
  68. {renderQuota(record.quota)}
  69. </Tag>
  70. </Tooltip>
  71. <Tooltip content={t('已用额度')}>
  72. <Tag color='white' size='large'>
  73. {renderQuota(record.used_quota)}
  74. </Tag>
  75. </Tooltip>
  76. <Tooltip content={t('调用次数')}>
  77. <Tag color='white' size='large'>
  78. {renderNumber(record.request_count)}
  79. </Tag>
  80. </Tooltip>
  81. </Space>
  82. </div>
  83. );
  84. },
  85. },
  86. {
  87. title: t('邀请信息'),
  88. dataIndex: 'invite',
  89. render: (text, record, index) => {
  90. return (
  91. <div>
  92. <Space spacing={1}>
  93. <Tooltip content={t('邀请人数')}>
  94. <Tag color='white' size='large'>
  95. {renderNumber(record.aff_count)}
  96. </Tag>
  97. </Tooltip>
  98. <Tooltip content={t('邀请总收益')}>
  99. <Tag color='white' size='large'>
  100. {renderQuota(record.aff_history_quota)}
  101. </Tag>
  102. </Tooltip>
  103. <Tooltip content={t('邀请人ID')}>
  104. {record.inviter_id === 0 ? (
  105. <Tag color='white' size='large'>
  106. {t('无')}
  107. </Tag>
  108. ) : (
  109. <Tag color='white' size='large'>
  110. {record.inviter_id}
  111. </Tag>
  112. )}
  113. </Tooltip>
  114. </Space>
  115. </div>
  116. );
  117. },
  118. },
  119. {
  120. title: t('角色'),
  121. dataIndex: 'role',
  122. render: (text, record, index) => {
  123. return <div>{renderRole(text)}</div>;
  124. },
  125. },
  126. {
  127. title: t('状态'),
  128. dataIndex: 'status',
  129. render: (text, record, index) => {
  130. return (
  131. <div>
  132. {record.DeletedAt !== null ? (
  133. <Tag color='red'>{t('已注销')}</Tag>
  134. ) : (
  135. renderStatus(text)
  136. )}
  137. </div>
  138. );
  139. },
  140. },
  141. {
  142. title: '',
  143. dataIndex: 'operate',
  144. render: (text, record, index) => (
  145. <div>
  146. {record.DeletedAt !== null ? (
  147. <></>
  148. ) : (
  149. <>
  150. <Popconfirm
  151. title={t('确定?')}
  152. okType={'warning'}
  153. onConfirm={() => {
  154. manageUser(record.id, 'promote', record);
  155. }}
  156. >
  157. <Button theme='light' type='warning' style={{ marginRight: 1 }}>
  158. {t('提升')}
  159. </Button>
  160. </Popconfirm>
  161. <Popconfirm
  162. title={t('确定?')}
  163. okType={'warning'}
  164. onConfirm={() => {
  165. manageUser(record.id, 'demote', record);
  166. }}
  167. >
  168. <Button theme='light' type='secondary' style={{ marginRight: 1 }}>
  169. {t('降级')}
  170. </Button>
  171. </Popconfirm>
  172. {record.status === 1 ? (
  173. <Button
  174. theme='light'
  175. type='warning'
  176. style={{ marginRight: 1 }}
  177. onClick={async () => {
  178. manageUser(record.id, 'disable', record);
  179. }}
  180. >
  181. {t('禁用')}
  182. </Button>
  183. ) : (
  184. <Button
  185. theme='light'
  186. type='secondary'
  187. style={{ marginRight: 1 }}
  188. onClick={async () => {
  189. manageUser(record.id, 'enable', record);
  190. }}
  191. disabled={record.status === 3}
  192. >
  193. {t('启用')}
  194. </Button>
  195. )}
  196. <Button
  197. theme='light'
  198. type='tertiary'
  199. style={{ marginRight: 1 }}
  200. onClick={() => {
  201. setEditingUser(record);
  202. setShowEditUser(true);
  203. }}
  204. >
  205. {t('编辑')}
  206. </Button>
  207. <Popconfirm
  208. title={t('确定是否要注销此用户?')}
  209. content={t('相当于删除用户,此修改将不可逆')}
  210. okType={'danger'}
  211. position={'left'}
  212. onConfirm={() => {
  213. manageUser(record.id, 'delete', record).then(() => {
  214. removeRecord(record.id);
  215. });
  216. }}
  217. >
  218. <Button theme='light' type='danger' style={{ marginRight: 1 }}>
  219. {t('注销')}
  220. </Button>
  221. </Popconfirm>
  222. </>
  223. )}
  224. </div>
  225. ),
  226. },
  227. ];
  228. const [users, setUsers] = useState([]);
  229. const [loading, setLoading] = useState(true);
  230. const [activePage, setActivePage] = useState(1);
  231. const [searchKeyword, setSearchKeyword] = useState('');
  232. const [searching, setSearching] = useState(false);
  233. const [searchGroup, setSearchGroup] = useState('');
  234. const [groupOptions, setGroupOptions] = useState([]);
  235. const [userCount, setUserCount] = useState(ITEMS_PER_PAGE);
  236. const [showAddUser, setShowAddUser] = useState(false);
  237. const [showEditUser, setShowEditUser] = useState(false);
  238. const [editingUser, setEditingUser] = useState({
  239. id: undefined,
  240. });
  241. const setCount = (data) => {
  242. if (data.length >= activePage * ITEMS_PER_PAGE) {
  243. setUserCount(data.length + 1);
  244. } else {
  245. setUserCount(data.length);
  246. }
  247. };
  248. const removeRecord = (key) => {
  249. let newDataSource = [...users];
  250. if (key != null) {
  251. let idx = newDataSource.findIndex((data) => data.id === key);
  252. if (idx > -1) {
  253. // update deletedAt
  254. newDataSource[idx].DeletedAt = new Date();
  255. setUsers(newDataSource);
  256. }
  257. }
  258. };
  259. const loadUsers = async (startIdx) => {
  260. const res = await API.get(`/api/user/?p=${startIdx}`);
  261. const { success, message, data } = res.data;
  262. if (success) {
  263. if (startIdx === 0) {
  264. setUsers(data);
  265. setCount(data);
  266. } else {
  267. let newUsers = users;
  268. newUsers.push(...data);
  269. setUsers(newUsers);
  270. setCount(newUsers);
  271. }
  272. } else {
  273. showError(message);
  274. }
  275. setLoading(false);
  276. };
  277. const onPaginationChange = (e, { activePage }) => {
  278. (async () => {
  279. if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
  280. // In this case we have to load more data and then append them.
  281. await loadUsers(activePage - 1);
  282. }
  283. setActivePage(activePage);
  284. })();
  285. };
  286. useEffect(() => {
  287. loadUsers(0)
  288. .then()
  289. .catch((reason) => {
  290. showError(reason);
  291. });
  292. fetchGroups().then();
  293. }, []);
  294. const manageUser = async (userId, action, record) => {
  295. const res = await API.post('/api/user/manage', {
  296. id: userId,
  297. action,
  298. });
  299. const { success, message } = res.data;
  300. if (success) {
  301. showSuccess('操作成功完成!');
  302. let user = res.data.data;
  303. let newUsers = [...users];
  304. if (action === 'delete') {
  305. } else {
  306. record.status = user.status;
  307. record.role = user.role;
  308. }
  309. setUsers(newUsers);
  310. } else {
  311. showError(message);
  312. }
  313. };
  314. const renderStatus = (status) => {
  315. switch (status) {
  316. case 1:
  317. return <Tag size='large'>{t('已激活')}</Tag>;
  318. case 2:
  319. return (
  320. <Tag size='large' color='red'>
  321. {t('已封禁')}
  322. </Tag>
  323. );
  324. default:
  325. return (
  326. <Tag size='large' color='grey'>
  327. {t('未知状态')}
  328. </Tag>
  329. );
  330. }
  331. };
  332. const searchUsers = async (searchKeyword, searchGroup) => {
  333. if (searchKeyword === '' && searchGroup === '') {
  334. // if keyword is blank, load files instead.
  335. await loadUsers(0);
  336. setActivePage(1);
  337. return;
  338. }
  339. setSearching(true);
  340. const res = await API.get(`/api/user/search?keyword=${searchKeyword}&group=${searchGroup}`);
  341. const { success, message, data } = res.data;
  342. if (success) {
  343. setUsers(data);
  344. setActivePage(1);
  345. } else {
  346. showError(message);
  347. }
  348. setSearching(false);
  349. };
  350. const handleKeywordChange = async (value) => {
  351. setSearchKeyword(value.trim());
  352. };
  353. const sortUser = (key) => {
  354. if (users.length === 0) return;
  355. setLoading(true);
  356. let sortedUsers = [...users];
  357. sortedUsers.sort((a, b) => {
  358. return ('' + a[key]).localeCompare(b[key]);
  359. });
  360. if (sortedUsers[0].id === users[0].id) {
  361. sortedUsers.reverse();
  362. }
  363. setUsers(sortedUsers);
  364. setLoading(false);
  365. };
  366. const handlePageChange = (page) => {
  367. setActivePage(page);
  368. if (page === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
  369. // In this case we have to load more data and then append them.
  370. loadUsers(page - 1).then((r) => {});
  371. }
  372. };
  373. const pageData = users.slice(
  374. (activePage - 1) * ITEMS_PER_PAGE,
  375. activePage * ITEMS_PER_PAGE,
  376. );
  377. const closeAddUser = () => {
  378. setShowAddUser(false);
  379. };
  380. const closeEditUser = () => {
  381. setShowEditUser(false);
  382. setEditingUser({
  383. id: undefined,
  384. });
  385. };
  386. const refresh = async () => {
  387. if (searchKeyword === '') {
  388. await loadUsers(activePage - 1);
  389. } else {
  390. await searchUsers(searchKeyword, searchGroup);
  391. }
  392. };
  393. const fetchGroups = async () => {
  394. try {
  395. let res = await API.get(`/api/group/`);
  396. // add 'all' option
  397. // res.data.data.unshift('all');
  398. if (res === undefined) {
  399. return;
  400. }
  401. setGroupOptions(
  402. res.data.data.map((group) => ({
  403. label: group,
  404. value: group,
  405. })),
  406. );
  407. } catch (error) {
  408. showError(error.message);
  409. }
  410. };
  411. return (
  412. <>
  413. <AddUser
  414. refresh={refresh}
  415. visible={showAddUser}
  416. handleClose={closeAddUser}
  417. ></AddUser>
  418. <EditUser
  419. refresh={refresh}
  420. visible={showEditUser}
  421. handleClose={closeEditUser}
  422. editingUser={editingUser}
  423. ></EditUser>
  424. <Form
  425. onSubmit={() => {
  426. searchUsers(searchKeyword, searchGroup);
  427. }}
  428. labelPosition='left'
  429. >
  430. <div style={{ display: 'flex' }}>
  431. <Space>
  432. <Form.Input
  433. label={t('搜索关键字')}
  434. icon='search'
  435. field='keyword'
  436. iconPosition='left'
  437. placeholder={t('搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...')}
  438. value={searchKeyword}
  439. loading={searching}
  440. onChange={(value) => handleKeywordChange(value)}
  441. />
  442. <Form.Select
  443. field='group'
  444. label={t('分组')}
  445. optionList={groupOptions}
  446. onChange={(value) => {
  447. setSearchGroup(value);
  448. searchUsers(searchKeyword, value);
  449. }}
  450. />
  451. <Button
  452. label={t('查询')}
  453. type='primary'
  454. htmlType='submit'
  455. className='btn-margin-right'
  456. >
  457. {t('查询')}
  458. </Button>
  459. <Button
  460. theme='light'
  461. type='primary'
  462. onClick={() => {
  463. setShowAddUser(true);
  464. }}
  465. >
  466. {t('添加用户')}
  467. </Button>
  468. </Space>
  469. </div>
  470. </Form>
  471. <Table
  472. columns={columns}
  473. dataSource={pageData}
  474. pagination={{
  475. formatPageText: (page) =>
  476. t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  477. start: page.currentStart,
  478. end: page.currentEnd,
  479. total: users.length
  480. }),
  481. currentPage: activePage,
  482. pageSize: ITEMS_PER_PAGE,
  483. total: userCount,
  484. pageSizeOpts: [10, 20, 50, 100],
  485. onPageChange: handlePageChange,
  486. }}
  487. loading={loading}
  488. />
  489. </>
  490. );
  491. };
  492. export default UsersTable;