one-api/web/default/src/components/UsersTable.js
Benny 9821bc7281
feat: add user list sorting and pagination enhancements (#1178)
* feat: add user list sorting and pagination enhancements

* feat: add user list sorting for THEME=air

* feat: add token list sorting and pagination enhancements

* feat: add token list sorting for THEME=air
2024-03-17 19:25:36 +08:00

365 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState } from 'react';
import { Button, Form, Label, Pagination, Popup, Table, Dropdown } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { API, showError, showSuccess } from '../helpers';
import { ITEMS_PER_PAGE } from '../constants';
import { renderGroup, renderNumber, renderQuota, renderText } from '../helpers/render';
function renderRole(role) {
switch (role) {
case 1:
return <Label>普通用户</Label>;
case 10:
return <Label color='yellow'>管理员</Label>;
case 100:
return <Label color='orange'>超级管理员</Label>;
default:
return <Label color='red'>未知身份</Label>;
}
}
const UsersTable = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(1);
const [searchKeyword, setSearchKeyword] = useState('');
const [searching, setSearching] = useState(false);
const [orderBy, setOrderBy] = useState('');
const loadUsers = async (startIdx) => {
const res = await API.get(`/api/user/?p=${startIdx}&order=${orderBy}`);
const { success, message, data } = res.data;
if (success) {
if (startIdx === 0) {
setUsers(data);
} else {
let newUsers = users;
newUsers.push(...data);
setUsers(newUsers);
}
} else {
showError(message);
}
setLoading(false);
};
const onPaginationChange = (e, { activePage }) => {
(async () => {
if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
await loadUsers(activePage - 1, orderBy);
}
setActivePage(activePage);
})();
};
useEffect(() => {
loadUsers(0, orderBy)
.then()
.catch((reason) => {
showError(reason);
});
}, [orderBy]);
const manageUser = (username, action, idx) => {
(async () => {
const res = await API.post('/api/user/manage', {
username,
action
});
const { success, message } = res.data;
if (success) {
showSuccess('操作成功完成!');
let user = res.data.data;
let newUsers = [...users];
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
if (action === 'delete') {
newUsers[realIdx].deleted = true;
} else {
newUsers[realIdx].status = user.status;
newUsers[realIdx].role = user.role;
}
setUsers(newUsers);
} else {
showError(message);
}
})();
};
const renderStatus = (status) => {
switch (status) {
case 1:
return <Label basic>已激活</Label>;
case 2:
return (
<Label basic color='red'>
已封禁
</Label>
);
default:
return (
<Label basic color='grey'>
未知状态
</Label>
);
}
};
const searchUsers = async () => {
if (searchKeyword === '') {
// if keyword is blank, load files instead.
await loadUsers(0);
setActivePage(1);
setOrderBy('');
return;
}
setSearching(true);
const res = await API.get(`/api/user/search?keyword=${searchKeyword}`);
const { success, message, data } = res.data;
if (success) {
setUsers(data);
setActivePage(1);
} else {
showError(message);
}
setSearching(false);
};
const handleKeywordChange = async (e, { value }) => {
setSearchKeyword(value.trim());
};
const sortUser = (key) => {
if (users.length === 0) return;
setLoading(true);
let sortedUsers = [...users];
sortedUsers.sort((a, b) => {
if (!isNaN(a[key])) {
// If the value is numeric, subtract to sort
return a[key] - b[key];
} else {
// If the value is not numeric, sort as strings
return ('' + a[key]).localeCompare(b[key]);
}
});
if (sortedUsers[0].id === users[0].id) {
sortedUsers.reverse();
}
setUsers(sortedUsers);
setLoading(false);
};
const handleOrderByChange = (e, { value }) => {
setOrderBy(value);
setActivePage(1);
};
return (
<>
<Form onSubmit={searchUsers}>
<Form.Input
icon='search'
fluid
iconPosition='left'
placeholder='搜索用户的 ID用户名显示名称以及邮箱地址 ...'
value={searchKeyword}
loading={searching}
onChange={handleKeywordChange}
/>
</Form>
<Table basic compact size='small'>
<Table.Header>
<Table.Row>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortUser('id');
}}
>
ID
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortUser('username');
}}
>
用户名
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortUser('group');
}}
>
分组
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortUser('quota');
}}
>
统计信息
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortUser('role');
}}
>
用户角色
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortUser('status');
}}
>
状态
</Table.HeaderCell>
<Table.HeaderCell>操作</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{users
.slice(
(activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE
)
.map((user, idx) => {
if (user.deleted) return <></>;
return (
<Table.Row key={user.id}>
<Table.Cell>{user.id}</Table.Cell>
<Table.Cell>
<Popup
content={user.email ? user.email : '未绑定邮箱地址'}
key={user.username}
header={user.display_name ? user.display_name : user.username}
trigger={<span>{renderText(user.username, 15)}</span>}
hoverable
/>
</Table.Cell>
<Table.Cell>{renderGroup(user.group)}</Table.Cell>
{/*<Table.Cell>*/}
{/* {user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}*/}
{/*</Table.Cell>*/}
<Table.Cell>
<Popup content='剩余额度' trigger={<Label basic>{renderQuota(user.quota)}</Label>} />
<Popup content='已用额度' trigger={<Label basic>{renderQuota(user.used_quota)}</Label>} />
<Popup content='请求次数' trigger={<Label basic>{renderNumber(user.request_count)}</Label>} />
</Table.Cell>
<Table.Cell>{renderRole(user.role)}</Table.Cell>
<Table.Cell>{renderStatus(user.status)}</Table.Cell>
<Table.Cell>
<div>
<Button
size={'small'}
positive
onClick={() => {
manageUser(user.username, 'promote', idx);
}}
disabled={user.role === 100}
>
提升
</Button>
<Button
size={'small'}
color={'yellow'}
onClick={() => {
manageUser(user.username, 'demote', idx);
}}
disabled={user.role === 100}
>
降级
</Button>
<Popup
trigger={
<Button size='small' negative disabled={user.role === 100}>
删除
</Button>
}
on='click'
flowing
hoverable
>
<Button
negative
onClick={() => {
manageUser(user.username, 'delete', idx);
}}
>
删除用户 {user.username}
</Button>
</Popup>
<Button
size={'small'}
onClick={() => {
manageUser(
user.username,
user.status === 1 ? 'disable' : 'enable',
idx
);
}}
disabled={user.role === 100}
>
{user.status === 1 ? '禁用' : '启用'}
</Button>
<Button
size={'small'}
as={Link}
to={'/user/edit/' + user.id}
>
编辑
</Button>
</div>
</Table.Cell>
</Table.Row>
);
})}
</Table.Body>
<Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan='7'>
<Button size='small' as={Link} to='/user/add' loading={loading}>
添加新的用户
</Button>
<Dropdown
placeholder='排序方式'
selection
options={[
{ key: '', text: '默认排序', value: '' },
{ key: 'quota', text: '按剩余额度排序', value: 'quota' },
{ key: 'used_quota', text: '按已用额度排序', value: 'used_quota' },
{ key: 'request_count', text: '按请求次数排序', value: 'request_count' },
]}
value={orderBy}
onChange={handleOrderByChange}
style={{ marginLeft: '10px' }}
/>
<Pagination
floated='right'
activePage={activePage}
onPageChange={onPaginationChange}
size='small'
siblingRange={1}
totalPages={
Math.ceil(users.length / ITEMS_PER_PAGE) +
(users.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
}
/>
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
</Table>
</>
);
};
export default UsersTable;