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
  169. theme='light'
  170. type='secondary'
  171. style={{ marginRight: 1 }}
  172. >
  173. {t('降级')}
  174. </Button>
  175. </Popconfirm>
  176. {record.status === 1 ? (
  177. <Button
  178. theme='light'
  179. type='warning'
  180. style={{ marginRight: 1 }}
  181. onClick={async () => {
  182. manageUser(record.id, 'disable', record);
  183. }}
  184. >
  185. {t('禁用')}
  186. </Button>
  187. ) : (
  188. <Button
  189. theme='light'
  190. type='secondary'
  191. style={{ marginRight: 1 }}
  192. onClick={async () => {
  193. manageUser(record.id, 'enable', record);
  194. }}
  195. disabled={record.status === 3}
  196. >
  197. {t('启用')}
  198. </Button>
  199. )}
  200. <Button
  201. theme='light'
  202. type='tertiary'
  203. style={{ marginRight: 1 }}
  204. onClick={() => {
  205. setEditingUser(record);
  206. setShowEditUser(true);
  207. }}
  208. >
  209. {t('编辑')}
  210. </Button>
  211. <Popconfirm
  212. title={t('确定是否要注销此用户?')}
  213. content={t('相当于删除用户,此修改将不可逆')}
  214. okType={'danger'}
  215. position={'left'}
  216. onConfirm={() => {
  217. manageUser(record.id, 'delete', record).then(() => {
  218. removeRecord(record.id);
  219. });
  220. }}
  221. >
  222. <Button theme='light' type='danger' style={{ marginRight: 1 }}>
  223. {t('注销')}
  224. </Button>
  225. </Popconfirm>
  226. </>
  227. )}
  228. </div>
  229. ),
  230. },
  231. ];
  232. const [users, setUsers] = useState([]);
  233. const [loading, setLoading] = useState(true);
  234. const [activePage, setActivePage] = useState(1);
  235. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  236. const [searchKeyword, setSearchKeyword] = useState('');
  237. const [searching, setSearching] = useState(false);
  238. const [searchGroup, setSearchGroup] = useState('');
  239. const [groupOptions, setGroupOptions] = useState([]);
  240. const [userCount, setUserCount] = useState(ITEMS_PER_PAGE);
  241. const [showAddUser, setShowAddUser] = useState(false);
  242. const [showEditUser, setShowEditUser] = useState(false);
  243. const [editingUser, setEditingUser] = useState({
  244. id: undefined,
  245. });
  246. const removeRecord = (key) => {
  247. let newDataSource = [...users];
  248. if (key != null) {
  249. let idx = newDataSource.findIndex((data) => data.id === key);
  250. if (idx > -1) {
  251. // update deletedAt
  252. newDataSource[idx].DeletedAt = new Date();
  253. setUsers(newDataSource);
  254. }
  255. }
  256. };
  257. const setUserFormat = (users) => {
  258. for (let i = 0; i < users.length; i++) {
  259. users[i].key = users[i].id;
  260. }
  261. setUsers(users);
  262. };
  263. const loadUsers = async (startIdx, pageSize) => {
  264. const res = await API.get(`/api/user/?p=${startIdx}&page_size=${pageSize}`);
  265. const { success, message, data } = res.data;
  266. if (success) {
  267. const newPageData = data.items;
  268. setActivePage(data.page);
  269. setUserCount(data.total);
  270. setUserFormat(newPageData);
  271. } else {
  272. showError(message);
  273. }
  274. setLoading(false);
  275. };
  276. useEffect(() => {
  277. loadUsers(0, pageSize)
  278. .then()
  279. .catch((reason) => {
  280. showError(reason);
  281. });
  282. fetchGroups().then();
  283. }, []);
  284. const manageUser = async (userId, action, record) => {
  285. const res = await API.post('/api/user/manage', {
  286. id: userId,
  287. action,
  288. });
  289. const { success, message } = res.data;
  290. if (success) {
  291. showSuccess('操作成功完成!');
  292. let user = res.data.data;
  293. let newUsers = [...users];
  294. if (action === 'delete') {
  295. } else {
  296. record.status = user.status;
  297. record.role = user.role;
  298. }
  299. setUsers(newUsers);
  300. } else {
  301. showError(message);
  302. }
  303. };
  304. const renderStatus = (status) => {
  305. switch (status) {
  306. case 1:
  307. return <Tag size='large'>{t('已激活')}</Tag>;
  308. case 2:
  309. return (
  310. <Tag size='large' color='red'>
  311. {t('已封禁')}
  312. </Tag>
  313. );
  314. default:
  315. return (
  316. <Tag size='large' color='grey'>
  317. {t('未知状态')}
  318. </Tag>
  319. );
  320. }
  321. };
  322. const searchUsers = async (
  323. startIdx,
  324. pageSize,
  325. searchKeyword,
  326. searchGroup,
  327. ) => {
  328. if (searchKeyword === '' && searchGroup === '') {
  329. // if keyword is blank, load files instead.
  330. await loadUsers(startIdx, pageSize);
  331. return;
  332. }
  333. setSearching(true);
  334. const res = await API.get(
  335. `/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`,
  336. );
  337. const { success, message, data } = res.data;
  338. if (success) {
  339. const newPageData = data.items;
  340. setActivePage(data.page);
  341. setUserCount(data.total);
  342. setUserFormat(newPageData);
  343. } else {
  344. showError(message);
  345. }
  346. setSearching(false);
  347. };
  348. const handleKeywordChange = async (value) => {
  349. setSearchKeyword(value.trim());
  350. };
  351. const handlePageChange = (page) => {
  352. setActivePage(page);
  353. if (searchKeyword === '' && searchGroup === '') {
  354. loadUsers(page, pageSize).then();
  355. } else {
  356. searchUsers(page, pageSize, searchKeyword, searchGroup).then();
  357. }
  358. };
  359. const closeAddUser = () => {
  360. setShowAddUser(false);
  361. };
  362. const closeEditUser = () => {
  363. setShowEditUser(false);
  364. setEditingUser({
  365. id: undefined,
  366. });
  367. };
  368. const refresh = async () => {
  369. setActivePage(1);
  370. if (searchKeyword === '') {
  371. await loadUsers(activePage, pageSize);
  372. } else {
  373. await searchUsers(activePage, pageSize, searchKeyword, searchGroup);
  374. }
  375. };
  376. const fetchGroups = async () => {
  377. try {
  378. let res = await API.get(`/api/group/`);
  379. // add 'all' option
  380. // res.data.data.unshift('all');
  381. if (res === undefined) {
  382. return;
  383. }
  384. setGroupOptions(
  385. res.data.data.map((group) => ({
  386. label: group,
  387. value: group,
  388. })),
  389. );
  390. } catch (error) {
  391. showError(error.message);
  392. }
  393. };
  394. const handlePageSizeChange = async (size) => {
  395. localStorage.setItem('page-size', size + '');
  396. setPageSize(size);
  397. setActivePage(1);
  398. loadUsers(activePage, size)
  399. .then()
  400. .catch((reason) => {
  401. showError(reason);
  402. });
  403. };
  404. return (
  405. <>
  406. <AddUser
  407. refresh={refresh}
  408. visible={showAddUser}
  409. handleClose={closeAddUser}
  410. ></AddUser>
  411. <EditUser
  412. refresh={refresh}
  413. visible={showEditUser}
  414. handleClose={closeEditUser}
  415. editingUser={editingUser}
  416. ></EditUser>
  417. <Form
  418. onSubmit={() => {
  419. searchUsers(activePage, pageSize, searchKeyword, searchGroup);
  420. }}
  421. labelPosition='left'
  422. >
  423. <div style={{ display: 'flex' }}>
  424. <Space>
  425. <Tooltip
  426. content={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
  427. >
  428. <Form.Input
  429. label={t('搜索关键字')}
  430. icon='search'
  431. field='keyword'
  432. iconPosition='left'
  433. placeholder={t('搜索关键字')}
  434. value={searchKeyword}
  435. loading={searching}
  436. onChange={(value) => handleKeywordChange(value)}
  437. />
  438. </Tooltip>
  439. <Form.Select
  440. field='group'
  441. label={t('分组')}
  442. optionList={groupOptions}
  443. onChange={(value) => {
  444. setSearchGroup(value);
  445. searchUsers(activePage, pageSize, searchKeyword, value);
  446. }}
  447. />
  448. <Button
  449. label={t('查询')}
  450. type='primary'
  451. htmlType='submit'
  452. className='btn-margin-right'
  453. >
  454. {t('查询')}
  455. </Button>
  456. <Button
  457. theme='light'
  458. type='primary'
  459. onClick={() => {
  460. setShowAddUser(true);
  461. }}
  462. >
  463. {t('添加用户')}
  464. </Button>
  465. </Space>
  466. </div>
  467. </Form>
  468. <Table
  469. columns={columns}
  470. dataSource={users}
  471. pagination={{
  472. formatPageText: (page) =>
  473. t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  474. start: page.currentStart,
  475. end: page.currentEnd,
  476. total: users.length,
  477. }),
  478. currentPage: activePage,
  479. pageSize: pageSize,
  480. total: userCount,
  481. pageSizeOpts: [10, 20, 50, 100],
  482. showSizeChanger: true,
  483. onPageSizeChange: (size) => {
  484. handlePageSizeChange(size);
  485. },
  486. onPageChange: handlePageChange,
  487. }}
  488. loading={loading}
  489. />
  490. </>
  491. );
  492. };
  493. export default UsersTable;