UsersTable.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. import React, { useEffect, useState } from 'react';
  2. import { API, showError, showSuccess, renderGroup, renderNumber, renderQuota } from '../helpers';
  3. import {
  4. Button,
  5. Card,
  6. Divider,
  7. Dropdown,
  8. Input,
  9. Modal,
  10. Select,
  11. Space,
  12. Table,
  13. Tag,
  14. Typography,
  15. } from '@douyinfe/semi-ui';
  16. import {
  17. IconPlus,
  18. IconSearch,
  19. IconEdit,
  20. IconDelete,
  21. IconStop,
  22. IconPlay,
  23. IconMore,
  24. IconUserAdd,
  25. IconArrowUp,
  26. IconArrowDown,
  27. } from '@douyinfe/semi-icons';
  28. import { ITEMS_PER_PAGE } from '../constants';
  29. import AddUser from '../pages/User/AddUser';
  30. import EditUser from '../pages/User/EditUser';
  31. import { useTranslation } from 'react-i18next';
  32. const { Text } = Typography;
  33. const UsersTable = () => {
  34. const { t } = useTranslation();
  35. function renderRole(role) {
  36. switch (role) {
  37. case 1:
  38. return (
  39. <Tag size='large' color='blue' shape='circle'>
  40. {t('普通用户')}
  41. </Tag>
  42. );
  43. case 10:
  44. return (
  45. <Tag color='yellow' size='large' shape='circle'>
  46. {t('管理员')}
  47. </Tag>
  48. );
  49. case 100:
  50. return (
  51. <Tag color='orange' size='large' shape='circle'>
  52. {t('超级管理员')}
  53. </Tag>
  54. );
  55. default:
  56. return (
  57. <Tag color='red' size='large' shape='circle'>
  58. {t('未知身份')}
  59. </Tag>
  60. );
  61. }
  62. }
  63. const renderStatus = (status) => {
  64. switch (status) {
  65. case 1:
  66. return <Tag size='large' color='green' shape='circle'>{t('已激活')}</Tag>;
  67. case 2:
  68. return (
  69. <Tag size='large' color='red' shape='circle'>
  70. {t('已封禁')}
  71. </Tag>
  72. );
  73. default:
  74. return (
  75. <Tag size='large' color='grey' shape='circle'>
  76. {t('未知状态')}
  77. </Tag>
  78. );
  79. }
  80. };
  81. const columns = [
  82. {
  83. title: 'ID',
  84. dataIndex: 'id',
  85. width: 50,
  86. },
  87. {
  88. title: t('用户名'),
  89. dataIndex: 'username',
  90. width: 100,
  91. },
  92. {
  93. title: t('分组'),
  94. dataIndex: 'group',
  95. width: 100,
  96. render: (text, record, index) => {
  97. return <div>{renderGroup(text)}</div>;
  98. },
  99. },
  100. {
  101. title: t('统计信息'),
  102. dataIndex: 'info',
  103. width: 280,
  104. render: (text, record, index) => {
  105. return (
  106. <div>
  107. <Space spacing={1}>
  108. <Tag color='white' size='large' shape='circle' className="!text-xs">
  109. {t('剩余')}: {renderQuota(record.quota)}
  110. </Tag>
  111. <Tag color='white' size='large' shape='circle' className="!text-xs">
  112. {t('已用')}: {renderQuota(record.used_quota)}
  113. </Tag>
  114. <Tag color='white' size='large' shape='circle' className="!text-xs">
  115. {t('调用')}: {renderNumber(record.request_count)}
  116. </Tag>
  117. </Space>
  118. </div>
  119. );
  120. },
  121. },
  122. {
  123. title: t('邀请信息'),
  124. dataIndex: 'invite',
  125. width: 250,
  126. render: (text, record, index) => {
  127. return (
  128. <div>
  129. <Space spacing={1}>
  130. <Tag color='white' size='large' shape='circle' className="!text-xs">
  131. {t('邀请')}: {renderNumber(record.aff_count)}
  132. </Tag>
  133. <Tag color='white' size='large' shape='circle' className="!text-xs">
  134. {t('收益')}: {renderQuota(record.aff_history_quota)}
  135. </Tag>
  136. <Tag color='white' size='large' shape='circle' className="!text-xs">
  137. {record.inviter_id === 0 ? t('无邀请人') : `邀请人: ${record.inviter_id}`}
  138. </Tag>
  139. </Space>
  140. </div>
  141. );
  142. },
  143. },
  144. {
  145. title: t('角色'),
  146. dataIndex: 'role',
  147. width: 120,
  148. render: (text, record, index) => {
  149. return <div>{renderRole(text)}</div>;
  150. },
  151. },
  152. {
  153. title: t('状态'),
  154. dataIndex: 'status',
  155. width: 100,
  156. render: (text, record, index) => {
  157. return (
  158. <div>
  159. {record.DeletedAt !== null ? (
  160. <Tag color='red' shape='circle'>{t('已注销')}</Tag>
  161. ) : (
  162. renderStatus(text)
  163. )}
  164. </div>
  165. );
  166. },
  167. },
  168. {
  169. title: '',
  170. dataIndex: 'operate',
  171. width: 150,
  172. render: (text, record, index) => {
  173. if (record.DeletedAt !== null) {
  174. return <></>;
  175. }
  176. // 创建更多操作的下拉菜单项
  177. const moreMenuItems = [
  178. {
  179. node: 'item',
  180. name: t('提升'),
  181. icon: <IconArrowUp />,
  182. type: 'warning',
  183. onClick: () => {
  184. Modal.confirm({
  185. title: t('确定要提升此用户吗?'),
  186. content: t('此操作将提升用户的权限级别'),
  187. onOk: () => {
  188. manageUser(record.id, 'promote', record);
  189. },
  190. });
  191. },
  192. },
  193. {
  194. node: 'item',
  195. name: t('降级'),
  196. icon: <IconArrowDown />,
  197. type: 'secondary',
  198. onClick: () => {
  199. Modal.confirm({
  200. title: t('确定要降级此用户吗?'),
  201. content: t('此操作将降低用户的权限级别'),
  202. onOk: () => {
  203. manageUser(record.id, 'demote', record);
  204. },
  205. });
  206. },
  207. },
  208. {
  209. node: 'item',
  210. name: t('注销'),
  211. icon: <IconDelete />,
  212. type: 'danger',
  213. onClick: () => {
  214. Modal.confirm({
  215. title: t('确定是否要注销此用户?'),
  216. content: t('相当于删除用户,此修改将不可逆'),
  217. onOk: () => {
  218. manageUser(record.id, 'delete', record).then(() => {
  219. removeRecord(record.id);
  220. });
  221. },
  222. });
  223. },
  224. }
  225. ];
  226. // 动态添加启用/禁用按钮
  227. if (record.status === 1) {
  228. moreMenuItems.splice(-1, 0, {
  229. node: 'item',
  230. name: t('禁用'),
  231. icon: <IconStop />,
  232. type: 'warning',
  233. onClick: () => {
  234. manageUser(record.id, 'disable', record);
  235. },
  236. });
  237. } else {
  238. moreMenuItems.splice(-1, 0, {
  239. node: 'item',
  240. name: t('启用'),
  241. icon: <IconPlay />,
  242. type: 'secondary',
  243. onClick: () => {
  244. manageUser(record.id, 'enable', record);
  245. },
  246. disabled: record.status === 3,
  247. });
  248. }
  249. return (
  250. <Space>
  251. <Button
  252. icon={<IconEdit />}
  253. theme='light'
  254. type='tertiary'
  255. size="small"
  256. className="!rounded-full"
  257. onClick={() => {
  258. setEditingUser(record);
  259. setShowEditUser(true);
  260. }}
  261. >
  262. {t('编辑')}
  263. </Button>
  264. <Dropdown
  265. trigger='click'
  266. position='bottomRight'
  267. menu={moreMenuItems}
  268. >
  269. <Button
  270. icon={<IconMore />}
  271. theme='light'
  272. type='tertiary'
  273. size="small"
  274. className="!rounded-full"
  275. />
  276. </Dropdown>
  277. </Space>
  278. );
  279. },
  280. },
  281. ];
  282. const [users, setUsers] = useState([]);
  283. const [loading, setLoading] = useState(true);
  284. const [activePage, setActivePage] = useState(1);
  285. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  286. const [searchKeyword, setSearchKeyword] = useState('');
  287. const [searching, setSearching] = useState(false);
  288. const [searchGroup, setSearchGroup] = useState('');
  289. const [groupOptions, setGroupOptions] = useState([]);
  290. const [userCount, setUserCount] = useState(ITEMS_PER_PAGE);
  291. const [showAddUser, setShowAddUser] = useState(false);
  292. const [showEditUser, setShowEditUser] = useState(false);
  293. const [editingUser, setEditingUser] = useState({
  294. id: undefined,
  295. });
  296. const removeRecord = (key) => {
  297. let newDataSource = [...users];
  298. if (key != null) {
  299. let idx = newDataSource.findIndex((data) => data.id === key);
  300. if (idx > -1) {
  301. // update deletedAt
  302. newDataSource[idx].DeletedAt = new Date();
  303. setUsers(newDataSource);
  304. }
  305. }
  306. };
  307. const setUserFormat = (users) => {
  308. for (let i = 0; i < users.length; i++) {
  309. users[i].key = users[i].id;
  310. }
  311. setUsers(users);
  312. };
  313. const loadUsers = async (startIdx, pageSize) => {
  314. const res = await API.get(`/api/user/?p=${startIdx}&page_size=${pageSize}`);
  315. const { success, message, data } = res.data;
  316. if (success) {
  317. const newPageData = data.items;
  318. setActivePage(data.page);
  319. setUserCount(data.total);
  320. setUserFormat(newPageData);
  321. } else {
  322. showError(message);
  323. }
  324. setLoading(false);
  325. };
  326. useEffect(() => {
  327. loadUsers(0, pageSize)
  328. .then()
  329. .catch((reason) => {
  330. showError(reason);
  331. });
  332. fetchGroups().then();
  333. }, []);
  334. const manageUser = async (userId, action, record) => {
  335. const res = await API.post('/api/user/manage', {
  336. id: userId,
  337. action,
  338. });
  339. const { success, message } = res.data;
  340. if (success) {
  341. showSuccess('操作成功完成!');
  342. let user = res.data.data;
  343. let newUsers = [...users];
  344. if (action === 'delete') {
  345. } else {
  346. record.status = user.status;
  347. record.role = user.role;
  348. }
  349. setUsers(newUsers);
  350. } else {
  351. showError(message);
  352. }
  353. };
  354. const searchUsers = async (
  355. startIdx,
  356. pageSize,
  357. searchKeyword,
  358. searchGroup,
  359. ) => {
  360. if (searchKeyword === '' && searchGroup === '') {
  361. // if keyword is blank, load files instead.
  362. await loadUsers(startIdx, pageSize);
  363. return;
  364. }
  365. setSearching(true);
  366. const res = await API.get(
  367. `/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`,
  368. );
  369. const { success, message, data } = res.data;
  370. if (success) {
  371. const newPageData = data.items;
  372. setActivePage(data.page);
  373. setUserCount(data.total);
  374. setUserFormat(newPageData);
  375. } else {
  376. showError(message);
  377. }
  378. setSearching(false);
  379. };
  380. const handleKeywordChange = async (value) => {
  381. setSearchKeyword(value.trim());
  382. };
  383. const handlePageChange = (page) => {
  384. setActivePage(page);
  385. if (searchKeyword === '' && searchGroup === '') {
  386. loadUsers(page, pageSize).then();
  387. } else {
  388. searchUsers(page, pageSize, searchKeyword, searchGroup).then();
  389. }
  390. };
  391. const closeAddUser = () => {
  392. setShowAddUser(false);
  393. };
  394. const closeEditUser = () => {
  395. setShowEditUser(false);
  396. setEditingUser({
  397. id: undefined,
  398. });
  399. };
  400. const refresh = async () => {
  401. setActivePage(1);
  402. if (searchKeyword === '') {
  403. await loadUsers(activePage, pageSize);
  404. } else {
  405. await searchUsers(activePage, pageSize, searchKeyword, searchGroup);
  406. }
  407. };
  408. const fetchGroups = async () => {
  409. try {
  410. let res = await API.get(`/api/group/`);
  411. // add 'all' option
  412. // res.data.data.unshift('all');
  413. if (res === undefined) {
  414. return;
  415. }
  416. setGroupOptions(
  417. res.data.data.map((group) => ({
  418. label: group,
  419. value: group,
  420. })),
  421. );
  422. } catch (error) {
  423. showError(error.message);
  424. }
  425. };
  426. const handlePageSizeChange = async (size) => {
  427. localStorage.setItem('page-size', size + '');
  428. setPageSize(size);
  429. setActivePage(1);
  430. loadUsers(activePage, size)
  431. .then()
  432. .catch((reason) => {
  433. showError(reason);
  434. });
  435. };
  436. const handleRow = (record, index) => {
  437. if (record.DeletedAt !== null || record.status !== 1) {
  438. return {
  439. style: {
  440. background: 'var(--semi-color-disabled-border)',
  441. },
  442. };
  443. } else {
  444. return {};
  445. }
  446. };
  447. const renderHeader = () => (
  448. <div className="flex flex-col w-full">
  449. <div className="mb-2">
  450. <div className="flex items-center text-blue-500">
  451. <IconUserAdd className="mr-2" />
  452. <Text>{t('用户管理页面,可以查看和管理所有注册用户的信息、权限和状态。')}</Text>
  453. </div>
  454. </div>
  455. <Divider margin="12px" />
  456. <div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
  457. <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
  458. <Button
  459. theme='light'
  460. type='primary'
  461. icon={<IconPlus />}
  462. className="!rounded-full w-full md:w-auto"
  463. onClick={() => {
  464. setShowAddUser(true);
  465. }}
  466. >
  467. {t('添加用户')}
  468. </Button>
  469. </div>
  470. <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2">
  471. <div className="relative w-full md:w-64">
  472. <Input
  473. prefix={<IconSearch />}
  474. placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
  475. value={searchKeyword}
  476. onChange={handleKeywordChange}
  477. className="!rounded-full"
  478. showClear
  479. />
  480. </div>
  481. <div className="w-full md:w-48">
  482. <Select
  483. placeholder={t('选择分组')}
  484. optionList={groupOptions}
  485. value={searchGroup}
  486. onChange={(value) => {
  487. setSearchGroup(value);
  488. searchUsers(activePage, pageSize, searchKeyword, value);
  489. }}
  490. className="!rounded-full w-full"
  491. showClear
  492. />
  493. </div>
  494. <Button
  495. type="primary"
  496. onClick={() => {
  497. searchUsers(activePage, pageSize, searchKeyword, searchGroup);
  498. }}
  499. loading={searching}
  500. className="!rounded-full w-full md:w-auto"
  501. >
  502. {t('查询')}
  503. </Button>
  504. </div>
  505. </div>
  506. </div>
  507. );
  508. return (
  509. <>
  510. <AddUser
  511. refresh={refresh}
  512. visible={showAddUser}
  513. handleClose={closeAddUser}
  514. ></AddUser>
  515. <EditUser
  516. refresh={refresh}
  517. visible={showEditUser}
  518. handleClose={closeEditUser}
  519. editingUser={editingUser}
  520. ></EditUser>
  521. <Card
  522. className="!rounded-2xl overflow-hidden"
  523. title={renderHeader()}
  524. shadows='hover'
  525. >
  526. <Table
  527. columns={columns}
  528. dataSource={users}
  529. pagination={{
  530. formatPageText: (page) =>
  531. t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  532. start: page.currentStart,
  533. end: page.currentEnd,
  534. total: userCount,
  535. }),
  536. currentPage: activePage,
  537. pageSize: pageSize,
  538. total: userCount,
  539. pageSizeOpts: [10, 20, 50, 100],
  540. showSizeChanger: true,
  541. onPageSizeChange: (size) => {
  542. handlePageSizeChange(size);
  543. },
  544. onPageChange: handlePageChange,
  545. }}
  546. loading={loading}
  547. onRow={handleRow}
  548. className="rounded-xl overflow-hidden"
  549. size="middle"
  550. />
  551. </Card>
  552. </>
  553. );
  554. };
  555. export default UsersTable;