| | 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', |
| | }, |
| | { |
| | title: t('用户名'), |
| | dataIndex: 'username', |
| | }, |
| | { |
| | title: t('分组'), |
| | dataIndex: 'group', |
| | render: (text, record, index) => { |
| | return <div>{renderGroup(text)}</div>; |
| | }, |
| | }, |
| | { |
| | title: t('统计信息'), |
| | dataIndex: 'info', |
| | 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', |
| | 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', |
| | render: (text, record, index) => { |
| | return <div>{renderRole(text)}</div>; |
| | }, |
| | }, |
| | { |
| | title: t('状态'), |
| | dataIndex: 'status', |
| | render: (text, record, index) => { |
| | return ( |
| | <div> |
| | {record.DeletedAt !== null ? ( |
| | <Tag color='red' shape='circle'>{t('已注销')}</Tag> |
| | ) : ( |
| | renderStatus(text) |
| | )} |
| | </div> |
| | ); |
| | }, |
| | }, |
| | { |
| | title: '', |
| | dataIndex: 'operate', |
| | fixed: 'right', |
| | 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) { |
| | |
| | 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 === '') { |
| | |
| | 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/`); |
| | |
| | |
| | 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" |
| | title={renderHeader()} |
| | shadows='always' |
| | bordered={false} |
| | > |
| | <Table |
| | columns={columns} |
| | dataSource={users} |
| | scroll={{ x: 'max-content' }} |
| | 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; |
| |
|