| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586 |
- import React, { useEffect, useState } from 'react';
- import { API, showError, showSuccess, renderGroup, renderNumber, renderQuota } from '../helpers';
- import {
- Button,
- Card,
- Divider,
- Dropdown,
- Input,
- Modal,
- Select,
- Space,
- Table,
- Tag,
- Typography,
- } from '@douyinfe/semi-ui';
- import {
- IconPlus,
- IconSearch,
- IconEdit,
- IconDelete,
- IconStop,
- IconPlay,
- IconMore,
- IconUserAdd,
- IconArrowUp,
- IconArrowDown,
- } from '@douyinfe/semi-icons';
- import { ITEMS_PER_PAGE } from '../constants';
- import AddUser from '../pages/User/AddUser';
- import EditUser from '../pages/User/EditUser';
- import { useTranslation } from 'react-i18next';
- const { Text } = Typography;
- const UsersTable = () => {
- const { t } = useTranslation();
- function renderRole(role) {
- switch (role) {
- case 1:
- return (
- <Tag size='large' color='blue' shape='circle'>
- {t('普通用户')}
- </Tag>
- );
- case 10:
- return (
- <Tag color='yellow' size='large' shape='circle'>
- {t('管理员')}
- </Tag>
- );
- case 100:
- return (
- <Tag color='orange' size='large' shape='circle'>
- {t('超级管理员')}
- </Tag>
- );
- default:
- return (
- <Tag color='red' size='large' shape='circle'>
- {t('未知身份')}
- </Tag>
- );
- }
- }
- const renderStatus = (status) => {
- switch (status) {
- case 1:
- return <Tag size='large' color='green' shape='circle'>{t('已激活')}</Tag>;
- case 2:
- return (
- <Tag size='large' color='red' shape='circle'>
- {t('已封禁')}
- </Tag>
- );
- default:
- return (
- <Tag size='large' color='grey' shape='circle'>
- {t('未知状态')}
- </Tag>
- );
- }
- };
- const columns = [
- {
- title: 'ID',
- dataIndex: 'id',
- width: 50,
- },
- {
- title: t('用户名'),
- dataIndex: 'username',
- width: 100,
- },
- {
- title: t('分组'),
- dataIndex: 'group',
- width: 100,
- render: (text, record, index) => {
- return <div>{renderGroup(text)}</div>;
- },
- },
- {
- title: t('统计信息'),
- dataIndex: 'info',
- width: 280,
- render: (text, record, index) => {
- return (
- <div>
- <Space spacing={1}>
- <Tag color='white' size='large' shape='circle' className="!text-xs">
- {t('剩余')}: {renderQuota(record.quota)}
- </Tag>
- <Tag color='white' size='large' shape='circle' className="!text-xs">
- {t('已用')}: {renderQuota(record.used_quota)}
- </Tag>
- <Tag color='white' size='large' shape='circle' className="!text-xs">
- {t('调用')}: {renderNumber(record.request_count)}
- </Tag>
- </Space>
- </div>
- );
- },
- },
- {
- title: t('邀请信息'),
- dataIndex: 'invite',
- width: 250,
- render: (text, record, index) => {
- return (
- <div>
- <Space spacing={1}>
- <Tag color='white' size='large' shape='circle' className="!text-xs">
- {t('邀请')}: {renderNumber(record.aff_count)}
- </Tag>
- <Tag color='white' size='large' shape='circle' className="!text-xs">
- {t('收益')}: {renderQuota(record.aff_history_quota)}
- </Tag>
- <Tag color='white' size='large' shape='circle' className="!text-xs">
- {record.inviter_id === 0 ? t('无邀请人') : `邀请人: ${record.inviter_id}`}
- </Tag>
- </Space>
- </div>
- );
- },
- },
- {
- title: t('角色'),
- dataIndex: 'role',
- width: 120,
- render: (text, record, index) => {
- return <div>{renderRole(text)}</div>;
- },
- },
- {
- title: t('状态'),
- dataIndex: 'status',
- width: 100,
- render: (text, record, index) => {
- return (
- <div>
- {record.DeletedAt !== null ? (
- <Tag color='red' shape='circle'>{t('已注销')}</Tag>
- ) : (
- renderStatus(text)
- )}
- </div>
- );
- },
- },
- {
- title: '',
- dataIndex: 'operate',
- width: 150,
- render: (text, record, index) => {
- if (record.DeletedAt !== null) {
- return <></>;
- }
- // 创建更多操作的下拉菜单项
- const moreMenuItems = [
- {
- node: 'item',
- name: t('提升'),
- icon: <IconArrowUp />,
- type: 'warning',
- onClick: () => {
- Modal.confirm({
- title: t('确定要提升此用户吗?'),
- content: t('此操作将提升用户的权限级别'),
- onOk: () => {
- manageUser(record.id, 'promote', record);
- },
- });
- },
- },
- {
- node: 'item',
- name: t('降级'),
- icon: <IconArrowDown />,
- type: 'secondary',
- onClick: () => {
- Modal.confirm({
- title: t('确定要降级此用户吗?'),
- content: t('此操作将降低用户的权限级别'),
- onOk: () => {
- manageUser(record.id, 'demote', record);
- },
- });
- },
- },
- {
- node: 'item',
- name: t('注销'),
- icon: <IconDelete />,
- type: 'danger',
- onClick: () => {
- Modal.confirm({
- title: t('确定是否要注销此用户?'),
- content: t('相当于删除用户,此修改将不可逆'),
- onOk: () => {
- manageUser(record.id, 'delete', record).then(() => {
- removeRecord(record.id);
- });
- },
- });
- },
- }
- ];
- // 动态添加启用/禁用按钮
- if (record.status === 1) {
- moreMenuItems.splice(-1, 0, {
- node: 'item',
- name: t('禁用'),
- icon: <IconStop />,
- type: 'warning',
- onClick: () => {
- manageUser(record.id, 'disable', record);
- },
- });
- } else {
- moreMenuItems.splice(-1, 0, {
- node: 'item',
- name: t('启用'),
- icon: <IconPlay />,
- type: 'secondary',
- onClick: () => {
- manageUser(record.id, 'enable', record);
- },
- disabled: record.status === 3,
- });
- }
- return (
- <Space>
- <Button
- icon={<IconEdit />}
- theme='light'
- type='tertiary'
- size="small"
- className="!rounded-full"
- onClick={() => {
- setEditingUser(record);
- setShowEditUser(true);
- }}
- >
- {t('编辑')}
- </Button>
- <Dropdown
- trigger='click'
- position='bottomRight'
- menu={moreMenuItems}
- >
- <Button
- icon={<IconMore />}
- theme='light'
- type='tertiary'
- size="small"
- className="!rounded-full"
- />
- </Dropdown>
- </Space>
- );
- },
- },
- ];
- const [users, setUsers] = useState([]);
- const [loading, setLoading] = useState(true);
- const [activePage, setActivePage] = useState(1);
- const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
- const [searchKeyword, setSearchKeyword] = useState('');
- const [searching, setSearching] = useState(false);
- const [searchGroup, setSearchGroup] = useState('');
- const [groupOptions, setGroupOptions] = useState([]);
- const [userCount, setUserCount] = useState(ITEMS_PER_PAGE);
- const [showAddUser, setShowAddUser] = useState(false);
- const [showEditUser, setShowEditUser] = useState(false);
- const [editingUser, setEditingUser] = useState({
- id: undefined,
- });
- const removeRecord = (key) => {
- let newDataSource = [...users];
- if (key != null) {
- let idx = newDataSource.findIndex((data) => data.id === key);
- if (idx > -1) {
- // update deletedAt
- newDataSource[idx].DeletedAt = new Date();
- setUsers(newDataSource);
- }
- }
- };
- const setUserFormat = (users) => {
- for (let i = 0; i < users.length; i++) {
- users[i].key = users[i].id;
- }
- setUsers(users);
- };
- const loadUsers = async (startIdx, pageSize) => {
- const res = await API.get(`/api/user/?p=${startIdx}&page_size=${pageSize}`);
- const { success, message, data } = res.data;
- if (success) {
- const newPageData = data.items;
- setActivePage(data.page);
- setUserCount(data.total);
- setUserFormat(newPageData);
- } else {
- showError(message);
- }
- setLoading(false);
- };
- useEffect(() => {
- loadUsers(0, pageSize)
- .then()
- .catch((reason) => {
- showError(reason);
- });
- fetchGroups().then();
- }, []);
- const manageUser = async (userId, action, record) => {
- const res = await API.post('/api/user/manage', {
- id: userId,
- action,
- });
- const { success, message } = res.data;
- if (success) {
- showSuccess('操作成功完成!');
- let user = res.data.data;
- let newUsers = [...users];
- if (action === 'delete') {
- } else {
- record.status = user.status;
- record.role = user.role;
- }
- setUsers(newUsers);
- } else {
- showError(message);
- }
- };
- const searchUsers = async (
- startIdx,
- pageSize,
- searchKeyword,
- searchGroup,
- ) => {
- if (searchKeyword === '' && searchGroup === '') {
- // if keyword is blank, load files instead.
- await loadUsers(startIdx, pageSize);
- return;
- }
- setSearching(true);
- const res = await API.get(
- `/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`,
- );
- const { success, message, data } = res.data;
- if (success) {
- const newPageData = data.items;
- setActivePage(data.page);
- setUserCount(data.total);
- setUserFormat(newPageData);
- } else {
- showError(message);
- }
- setSearching(false);
- };
- const handleKeywordChange = async (value) => {
- setSearchKeyword(value.trim());
- };
- const handlePageChange = (page) => {
- setActivePage(page);
- if (searchKeyword === '' && searchGroup === '') {
- loadUsers(page, pageSize).then();
- } else {
- searchUsers(page, pageSize, searchKeyword, searchGroup).then();
- }
- };
- const closeAddUser = () => {
- setShowAddUser(false);
- };
- const closeEditUser = () => {
- setShowEditUser(false);
- setEditingUser({
- id: undefined,
- });
- };
- const refresh = async () => {
- setActivePage(1);
- if (searchKeyword === '') {
- await loadUsers(activePage, pageSize);
- } else {
- await searchUsers(activePage, pageSize, searchKeyword, searchGroup);
- }
- };
- const fetchGroups = async () => {
- try {
- let res = await API.get(`/api/group/`);
- // add 'all' option
- // res.data.data.unshift('all');
- if (res === undefined) {
- return;
- }
- setGroupOptions(
- res.data.data.map((group) => ({
- label: group,
- value: group,
- })),
- );
- } catch (error) {
- showError(error.message);
- }
- };
- const handlePageSizeChange = async (size) => {
- localStorage.setItem('page-size', size + '');
- setPageSize(size);
- setActivePage(1);
- loadUsers(activePage, size)
- .then()
- .catch((reason) => {
- showError(reason);
- });
- };
- const handleRow = (record, index) => {
- if (record.DeletedAt !== null || record.status !== 1) {
- return {
- style: {
- background: 'var(--semi-color-disabled-border)',
- },
- };
- } else {
- return {};
- }
- };
- const renderHeader = () => (
- <div className="flex flex-col w-full">
- <div className="mb-2">
- <div className="flex items-center text-blue-500">
- <IconUserAdd className="mr-2" />
- <Text>{t('用户管理页面,可以查看和管理所有注册用户的信息、权限和状态。')}</Text>
- </div>
- </div>
- <Divider margin="12px" />
- <div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
- <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
- <Button
- theme='light'
- type='primary'
- icon={<IconPlus />}
- className="!rounded-full w-full md:w-auto"
- onClick={() => {
- setShowAddUser(true);
- }}
- >
- {t('添加用户')}
- </Button>
- </div>
- <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2">
- <div className="relative w-full md:w-64">
- <Input
- prefix={<IconSearch />}
- placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
- value={searchKeyword}
- onChange={handleKeywordChange}
- className="!rounded-full"
- showClear
- />
- </div>
- <div className="w-full md:w-48">
- <Select
- placeholder={t('选择分组')}
- optionList={groupOptions}
- value={searchGroup}
- onChange={(value) => {
- setSearchGroup(value);
- searchUsers(activePage, pageSize, searchKeyword, value);
- }}
- className="!rounded-full w-full"
- showClear
- />
- </div>
- <Button
- type="primary"
- onClick={() => {
- searchUsers(activePage, pageSize, searchKeyword, searchGroup);
- }}
- loading={searching}
- className="!rounded-full w-full md:w-auto"
- >
- {t('查询')}
- </Button>
- </div>
- </div>
- </div>
- );
- return (
- <>
- <AddUser
- refresh={refresh}
- visible={showAddUser}
- handleClose={closeAddUser}
- ></AddUser>
- <EditUser
- refresh={refresh}
- visible={showEditUser}
- handleClose={closeEditUser}
- editingUser={editingUser}
- ></EditUser>
- <Card
- className="!rounded-2xl overflow-hidden"
- title={renderHeader()}
- shadows='hover'
- >
- <Table
- columns={columns}
- dataSource={users}
- pagination={{
- formatPageText: (page) =>
- t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
- start: page.currentStart,
- end: page.currentEnd,
- total: userCount,
- }),
- currentPage: activePage,
- pageSize: pageSize,
- total: userCount,
- pageSizeOpts: [10, 20, 50, 100],
- showSizeChanger: true,
- onPageSizeChange: (size) => {
- handlePageSizeChange(size);
- },
- onPageChange: handlePageChange,
- }}
- loading={loading}
- onRow={handleRow}
- className="rounded-xl overflow-hidden"
- size="middle"
- />
- </Card>
- </>
- );
- };
- export default UsersTable;
|