Merge remote-tracking branch 'origin/upstream/main'

This commit is contained in:
Laisky.Cai
2024-03-18 01:31:21 +00:00
40 changed files with 340 additions and 145 deletions

View File

@@ -9,7 +9,7 @@
1.`web` 文件夹下新建一个文件夹,文件夹名为主题名。
2. 把你的主题文件放到这个文件夹下。
3. 修改你的 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv -f build ../build/default"`,其中 `default` 为你的主题名。
4. 修改 `common/constants.go` 中的 `ValidThemes`,把你的主题名称注册进去。
4. 修改 `common/config/config.go` 中的 `ValidThemes`,把你的主题名称注册进去。
5. 修改 `web/THEMES` 文件,这里也需要同步修改。
## 主题列表

View File

@@ -9,7 +9,7 @@
name="description"
content="OpenAI 接口聚合管理,支持多种渠道包括 Azure可用于二次分发管理 key仅单可执行文件已打包好 Docker 镜像,一键部署,开箱即用"
/>
<title>New API</title>
<title>One API</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -247,6 +247,8 @@ const TokensTable = () => {
const [editingToken, setEditingToken] = useState({
id: undefined
});
const [orderBy, setOrderBy] = useState('');
const [dropdownVisible, setDropdownVisible] = useState(false);
const closeEdit = () => {
setShowEdit(false);
@@ -269,7 +271,7 @@ const TokensTable = () => {
let pageData = tokens.slice((activePage - 1) * pageSize, activePage * pageSize);
const loadTokens = async (startIdx) => {
setLoading(true);
const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}`);
const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}&order=${orderBy}`);
const { success, message, data } = res.data;
if (success) {
if (startIdx === 0) {
@@ -289,7 +291,7 @@ const TokensTable = () => {
(async () => {
if (activePage === Math.ceil(tokens.length / pageSize) + 1) {
// In this case we have to load more data and then append them.
await loadTokens(activePage - 1);
await loadTokens(activePage - 1, orderBy);
}
setActivePage(activePage);
})();
@@ -392,12 +394,12 @@ const TokensTable = () => {
};
useEffect(() => {
loadTokens(0)
loadTokens(0, orderBy)
.then()
.catch((reason) => {
showError(reason);
});
}, [pageSize]);
}, [pageSize, orderBy]);
const removeRecord = key => {
let newDataSource = [...tokens];
@@ -452,6 +454,7 @@ const TokensTable = () => {
// if keyword is blank, load files instead.
await loadTokens(0);
setActivePage(1);
setOrderBy('');
return;
}
setSearching(true);
@@ -520,6 +523,23 @@ const TokensTable = () => {
}
};
const handleOrderByChange = (e, { value }) => {
setOrderBy(value);
setActivePage(1);
setDropdownVisible(false);
};
const renderSelectedOption = (orderBy) => {
switch (orderBy) {
case 'remain_quota':
return '按剩余额度排序';
case 'used_quota':
return '按已用额度排序';
default:
return '默认排序';
}
};
return (
<>
<EditToken refresh={refresh} editingToken={editingToken} visiable={showEdit} handleClose={closeEdit}></EditToken>
@@ -579,6 +599,21 @@ const TokensTable = () => {
await copyText(keys);
}
}>复制所选令牌到剪贴板</Button>
<Dropdown
trigger="click"
position="bottomLeft"
visible={dropdownVisible}
onVisibleChange={(visible) => setDropdownVisible(visible)}
render={
<Dropdown.Menu>
<Dropdown.Item onClick={() => handleOrderByChange('', { value: '' })}>默认排序</Dropdown.Item>
<Dropdown.Item onClick={() => handleOrderByChange('', { value: 'remain_quota' })}>按剩余额度排序</Dropdown.Item>
<Dropdown.Item onClick={() => handleOrderByChange('', { value: 'used_quota' })}>按已用额度排序</Dropdown.Item>
</Dropdown.Menu>
}
>
<Button style={{ marginLeft: '10px' }}>{renderSelectedOption(orderBy)}</Button>
</Dropdown>
</>
);
};

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { API, showError, showSuccess } from '../helpers';
import { Button, Form, Popconfirm, Space, Table, Tag, Tooltip } from '@douyinfe/semi-ui';
import { Button, Form, Popconfirm, Space, Table, Tag, Tooltip, Dropdown } from '@douyinfe/semi-ui';
import { ITEMS_PER_PAGE } from '../constants';
import { renderGroup, renderNumber, renderQuota } from '../helpers/render';
import AddUser from '../pages/User/AddUser';
@@ -139,6 +139,8 @@ const UsersTable = () => {
const [editingUser, setEditingUser] = useState({
id: undefined
});
const [orderBy, setOrderBy] = useState('');
const [dropdownVisible, setDropdownVisible] = useState(false);
const setCount = (data) => {
if (data.length >= (activePage) * ITEMS_PER_PAGE) {
@@ -162,7 +164,7 @@ const UsersTable = () => {
};
const loadUsers = async (startIdx) => {
const res = await API.get(`/api/user/?p=${startIdx}`);
const res = await API.get(`/api/user/?p=${startIdx}&order=${orderBy}`);
const { success, message, data } = res.data;
if (success) {
if (startIdx === 0) {
@@ -184,19 +186,19 @@ const UsersTable = () => {
(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);
await loadUsers(activePage - 1, orderBy);
}
setActivePage(activePage);
})();
};
useEffect(() => {
loadUsers(0)
loadUsers(0, orderBy)
.then()
.catch((reason) => {
showError(reason);
});
}, []);
}, [orderBy]);
const manageUser = async (username, action, record) => {
const res = await API.post('/api/user/manage', {
@@ -239,6 +241,7 @@ const UsersTable = () => {
// if keyword is blank, load files instead.
await loadUsers(0);
setActivePage(1);
setOrderBy('');
return;
}
setSearching(true);
@@ -301,6 +304,25 @@ const UsersTable = () => {
}
};
const handleOrderByChange = (e, { value }) => {
setOrderBy(value);
setActivePage(1);
setDropdownVisible(false);
};
const renderSelectedOption = (orderBy) => {
switch (orderBy) {
case 'quota':
return '按剩余额度排序';
case 'used_quota':
return '按已用额度排序';
case 'request_count':
return '按请求次数排序';
default:
return '默认排序';
}
};
return (
<>
<AddUser refresh={refresh} visible={showAddUser} handleClose={closeAddUser}></AddUser>
@@ -331,6 +353,22 @@ const UsersTable = () => {
setShowAddUser(true);
}
}>添加用户</Button>
<Dropdown
trigger="click"
position="bottomLeft"
visible={dropdownVisible}
onVisibleChange={(visible) => setDropdownVisible(visible)}
render={
<Dropdown.Menu>
<Dropdown.Item onClick={() => handleOrderByChange('', { value: '' })}>默认排序</Dropdown.Item>
<Dropdown.Item onClick={() => handleOrderByChange('', { value: 'quota' })}>按剩余额度排序</Dropdown.Item>
<Dropdown.Item onClick={() => handleOrderByChange('', { value: 'used_quota' })}>按已用额度排序</Dropdown.Item>
<Dropdown.Item onClick={() => handleOrderByChange('', { value: 'request_count' })}>按请求次数排序</Dropdown.Item>
</Dropdown.Menu>
}
>
<Button style={{ marginLeft: '10px' }}>{renderSelectedOption(orderBy)}</Button>
</Dropdown>
</>
);
};

View File

@@ -23,7 +23,7 @@ export function isRoot() {
export function getSystemName() {
let system_name = localStorage.getItem('system_name');
if (!system_name) return 'New API';
if (!system_name) return 'One API';
return system_name;
}

View File

@@ -103,3 +103,14 @@ code {
display: none !important;
}
}
/* 隐藏浏览器默认的滚动条 */
body {
overflow: hidden;
}
/* 自定义滚动条样式 */
body::-webkit-scrollbar {
width: 0; /* 隐藏滚动条的宽度 */
}

View File

@@ -230,7 +230,7 @@ const EditChannel = (props) => {
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
}
if (localInputs.type === 3 && localInputs.other === '') {
localInputs.other = '2023-06-01-preview';
localInputs.other = '2024-03-01-preview';
}
if (localInputs.type === 18 && localInputs.other === '') {
localInputs.other = 'v2.1';
@@ -348,7 +348,7 @@ const EditChannel = (props) => {
<Input
label='默认 API 版本'
name='azure_other'
placeholder={'请输入默认 API 版本例如2023-06-01-preview该配置可以被实际的请求查询参数所覆盖'}
placeholder={'请输入默认 API 版本例如2024-03-01-preview该配置可以被实际的请求查询参数所覆盖'}
onChange={value => {
handleInputChange('other', value)
}}

View File

@@ -49,7 +49,7 @@ const typeConfig = {
base_url: "请填写AZURE_OPENAI_ENDPOINT",
// 注意:通过判断 `other` 是否有值来判断是否需要显示 `other` 输入框, 默认是没有值的
other: "请输入默认API版本例如2023-06-01-preview",
other: "请输入默认API版本例如2024-03-01-preview",
},
modelGroup: "openai", // 模型组名称,这个值是给 填入渠道支持模型 按钮使用的。 填入渠道支持模型 按钮会根据这个值来获取模型组,如果填写默认是 openai
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -3,186 +3,186 @@ export const CHANNEL_OPTIONS = {
key: 1,
text: 'OpenAI',
value: 1,
color: 'primary'
color: 'success'
},
14: {
key: 14,
text: 'Anthropic Claude',
value: 14,
color: 'info'
color: 'primary'
},
3: {
key: 3,
text: 'Azure OpenAI',
value: 3,
color: 'secondary'
color: 'success'
},
11: {
key: 11,
text: 'Google PaLM2',
value: 11,
color: 'orange'
color: 'warning'
},
24: {
key: 24,
text: 'Google Gemini',
value: 24,
color: 'orange'
color: 'warning'
},
28: {
key: 28,
text: 'Mistral AI',
value: 28,
color: 'orange'
color: 'warning'
},
15: {
key: 15,
text: '百度文心千帆',
value: 15,
color: 'default'
color: 'primary'
},
17: {
key: 17,
text: '阿里通义千问',
value: 17,
color: 'default'
color: 'primary'
},
18: {
key: 18,
text: '讯飞星火认知',
value: 18,
color: 'default'
color: 'primary'
},
16: {
key: 16,
text: '智谱 ChatGLM',
value: 16,
color: 'default'
color: 'primary'
},
19: {
key: 19,
text: '360 智脑',
value: 19,
color: 'default'
color: 'primary'
},
25: {
key: 25,
text: 'Moonshot AI',
value: 25,
color: 'default'
color: 'primary'
},
23: {
key: 23,
text: '腾讯混元',
value: 23,
color: 'default'
color: 'primary'
},
26: {
key: 26,
text: '百川大模型',
value: 26,
color: 'default'
color: 'primary'
},
27: {
key: 27,
text: 'MiniMax',
value: 27,
color: 'default'
color: 'primary'
},
29: {
key: 29,
text: 'Groq',
value: 29,
color: 'default'
color: 'primary'
},
30: {
key: 30,
text: 'Ollama',
value: 30,
color: 'default'
color: 'primary'
},
31: {
key: 31,
text: '零一万物',
value: 31,
color: 'default'
color: 'primary'
},
8: {
key: 8,
text: '自定义渠道',
value: 8,
color: 'primary'
color: 'error'
},
22: {
key: 22,
text: '知识库FastGPT',
value: 22,
color: 'default'
color: 'success'
},
21: {
key: 21,
text: '知识库AI Proxy',
value: 21,
color: 'purple'
color: 'success'
},
20: {
key: 20,
text: '代理OpenRouter',
value: 20,
color: 'primary'
color: 'success'
},
2: {
key: 2,
text: '代理API2D',
value: 2,
color: 'primary'
color: 'success'
},
5: {
key: 5,
text: '代理OpenAI-SB',
value: 5,
color: 'primary'
color: 'success'
},
7: {
key: 7,
text: '代理OhMyGPT',
value: 7,
color: 'primary'
color: 'success'
},
10: {
key: 10,
text: '代理AI Proxy',
value: 10,
color: 'primary'
color: 'success'
},
4: {
key: 4,
text: '代理CloseAI',
value: 4,
color: 'primary'
color: 'success'
},
6: {
key: 6,
text: '代理OpenAI Max',
value: 6,
color: 'primary'
color: 'success'
},
9: {
key: 9,
text: '代理AI.LS',
value: 9,
color: 'primary'
color: 'success'
},
12: {
key: 12,
text: '代理API2GPT',
value: 12,
color: 'primary'
color: 'success'
},
13: {
key: 13,
text: '代理AIGC2D',
value: 13,
color: 'primary'
color: 'success'
}
};

View File

@@ -180,7 +180,7 @@ const LoginForm = ({ ...others }) => {
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
<form noValidate onSubmit={handleSubmit} {...others}>
<FormControl fullWidth error={Boolean(touched.username && errors.username)} sx={{ ...theme.typography.customInput }}>
<InputLabel htmlFor="outlined-adornment-username-login">用户名</InputLabel>
<InputLabel htmlFor="outlined-adornment-username-login">用户名 / 邮箱</InputLabel>
<OutlinedInput
id="outlined-adornment-username-login"
type="text"

View File

@@ -3,6 +3,19 @@ import Label from "ui-component/Label";
import Stack from "@mui/material/Stack";
import Divider from "@mui/material/Divider";
function name2color(name) {
switch (name) {
case "default":
return "info";
case "vip":
return "warning"
case "svip":
return "error"
default:
return "info"
}
}
const GroupLabel = ({ group }) => {
let groups = [];
if (group === "") {
@@ -14,7 +27,7 @@ const GroupLabel = ({ group }) => {
return (
<Stack divider={<Divider orientation="vertical" flexItem />} spacing={0.5}>
{groups.map((group, index) => {
return <Label key={index}>{group}</Label>;
return <Label key={index} color={name2color(group)}>{group}</Label>;
})}
</Stack>
);

View File

@@ -10,6 +10,7 @@ const ChannelTableHead = () => {
<TableCell>类型</TableCell>
<TableCell>状态</TableCell>
<TableCell>响应时间</TableCell>
<TableCell>已消耗</TableCell>
<TableCell>余额</TableCell>
<TableCell>优先级</TableCell>
<TableCell>操作</TableCell>

View File

@@ -170,6 +170,9 @@ export default function ChannelTableRow({
handle_action={handleResponseTime}
/>
</TableCell>
<TableCell>
{renderNumber(item.used_quota)}
</TableCell>
<TableCell>
<Tooltip
title={"点击更新余额"}

View File

@@ -193,20 +193,14 @@ export default function ChannelPage() {
return (
<>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={2.5}>
<Typography variant="h4">渠道</Typography>
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
新建渠道
</Button>
</Stack>
<Stack mb={5}>
<Alert severity="info">
OpenAI 渠道已经不再支持通过 key 获取余额因此余额显示为 0对于支持的渠道类型请点击余额进行刷新
</Alert>
</Stack>
<Card>
<Box component="form" onSubmit={searchChannels} noValidate>
<Box component="form" onSubmit={searchChannels} noValidate sx={{marginTop: 2}}>
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索渠道的 ID名称和密钥 ...'} />
</Box>
<Toolbar
@@ -220,7 +214,7 @@ export default function ChannelPage() {
>
<Container>
{matchUpMd ? (
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
<ButtonGroup variant="outlined" aria-label="outlined small primary button group" sx={{marginBottom: 2}}>
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
刷新
</Button>

View File

@@ -41,7 +41,7 @@ const typeConfig = {
},
prompt: {
base_url: "请填写AZURE_OPENAI_ENDPOINT",
other: "请输入默认API版本例如2023-06-01-preview",
other: "请输入默认API版本例如2024-03-01-preview",
},
},
11: {

View File

@@ -65,7 +65,7 @@ const StatisticalLineChartCard = ({ isLoading, title, chartData, todayValue }) =
) : (
<CardWrapper border={false} content={false}>
<Box sx={{ p: 2.25 }}>
<Grid container direction="column">
<Grid>
<Grid item sx={{ mb: 0.75 }}>
<Grid container alignItems="center">
<Grid item xs={6}>

View File

@@ -102,11 +102,11 @@ export default function Log() {
return (
<>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={2.5}>
<Typography variant="h4">日志</Typography>
</Stack>
<Card>
<Box component="form" onSubmit={searchLogs} noValidate>
<Box component="form" onSubmit={searchLogs} noValidate sx={{marginTop: 2}}>
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} userIsAdmin={userIsAdmin} />
</Box>
<Toolbar
@@ -119,7 +119,7 @@ export default function Log() {
}}
>
<Container>
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
<ButtonGroup variant="outlined" aria-label="outlined small primary button group" sx={{marginBottom: 2}}>
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
刷新/清除搜索条件
</Button>

View File

@@ -141,7 +141,7 @@ export default function Redemption() {
return (
<>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={2.5}>
<Typography variant="h4">兑换</Typography>
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
@@ -149,7 +149,7 @@ export default function Redemption() {
</Button>
</Stack>
<Card>
<Box component="form" onSubmit={searchRedemptions} noValidate>
<Box component="form" onSubmit={searchRedemptions} noValidate sx={{marginTop: 2}}>
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索兑换码的ID和名称...'} />
</Box>
<Toolbar
@@ -162,7 +162,7 @@ export default function Redemption() {
}}
>
<Container>
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
<ButtonGroup variant="outlined" aria-label="outlined small primary button group" sx={{marginBottom: 2}}>
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
刷新
</Button>

View File

@@ -141,9 +141,8 @@ export default function Token() {
return (
<>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={2.5}>
<Typography variant="h4">令牌</Typography>
<Button
variant="contained"
color="primary"
@@ -155,13 +154,13 @@ export default function Token() {
新建令牌
</Button>
</Stack>
<Stack mb={5}>
<Stack mb={2}>
<Alert severity="info">
OpenAI API 基础地址 https://api.openai.com 替换为 <b>{siteInfo.server_address}</b>,复制下面的密钥即可使用
</Alert>
</Stack>
<Card>
<Box component="form" onSubmit={searchTokens} noValidate>
<Box component="form" onSubmit={searchTokens} noValidate sx={{marginTop: 2}}>
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索令牌的名称...'} />
</Box>
<Toolbar
@@ -174,7 +173,7 @@ export default function Token() {
}}
>
<Container>
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
<ButtonGroup variant="outlined" aria-label="outlined small primary button group" sx={{marginBottom: 2}}>
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
刷新
</Button>

View File

@@ -139,7 +139,7 @@ export default function Users() {
return (
<>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={2.5}>
<Typography variant="h4">用户</Typography>
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
@@ -147,7 +147,7 @@ export default function Users() {
</Button>
</Stack>
<Card>
<Box component="form" onSubmit={searchUsers} noValidate>
<Box component="form" onSubmit={searchUsers} noValidate sx={{marginTop: 2}}>
<TableToolBar
filterName={searchKeyword}
handleFilterName={handleSearchKeyword}
@@ -164,7 +164,7 @@ export default function Users() {
}}
>
<Container>
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
<ButtonGroup variant="outlined" aria-label="outlined small primary button group" sx={{marginBottom: 2}}>
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
刷新
</Button>

View File

@@ -333,6 +333,8 @@ const ChannelsTable = () => {
setPromptShown("channel-test");
}}>
OpenAI 渠道已经不再支持通过 key 获取余额因此余额显示为 0对于支持的渠道类型请点击余额进行刷新
<br/>
渠道测试仅支持 chat 模型优先使用 gpt-3.5-turbo如果该模型不可用则使用你所配置的模型列表中的第一个模型
</Message>
)
}

View File

@@ -94,7 +94,7 @@ const LoginForm = () => {
fluid
icon='user'
iconPosition='left'
placeholder='用户名'
placeholder='用户名 / 邮箱地址'
name='username'
value={username}
onChange={handleChange}

View File

@@ -47,9 +47,10 @@ const TokensTable = () => {
const [searching, setSearching] = useState(false);
const [showTopUpModal, setShowTopUpModal] = useState(false);
const [targetTokenIdx, setTargetTokenIdx] = useState(0);
const [orderBy, setOrderBy] = useState('');
const loadTokens = async (startIdx) => {
const res = await API.get(`/api/token/?p=${startIdx}`);
const res = await API.get(`/api/token/?p=${startIdx}&order=${orderBy}`);
const { success, message, data } = res.data;
if (success) {
if (startIdx === 0) {
@@ -69,7 +70,7 @@ const TokensTable = () => {
(async () => {
if (activePage === Math.ceil(tokens.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
await loadTokens(activePage - 1);
await loadTokens(activePage - 1, orderBy);
}
setActivePage(activePage);
})();
@@ -159,12 +160,12 @@ const TokensTable = () => {
}
useEffect(() => {
loadTokens(0)
loadTokens(0, orderBy)
.then()
.catch((reason) => {
showError(reason);
});
}, []);
}, [orderBy]);
const manageToken = async (id, action, idx) => {
let data = { id };
@@ -204,6 +205,7 @@ const TokensTable = () => {
// if keyword is blank, load files instead.
await loadTokens(0);
setActivePage(1);
setOrderBy('');
return;
}
setSearching(true);
@@ -242,6 +244,11 @@ const TokensTable = () => {
setLoading(false);
};
const handleOrderByChange = (e, { value }) => {
setOrderBy(value);
setActivePage(1);
};
return (
<>
<Form onSubmit={searchTokens}>
@@ -404,6 +411,18 @@ const TokensTable = () => {
添加新的令牌
</Button>
<Button size='small' onClick={refresh} loading={loading}>刷新</Button>
<Dropdown
placeholder='排序方式'
selection
options={[
{ key: '', text: '默认排序', value: '' },
{ key: 'remain_quota', text: '按剩余额度排序', value: 'remain_quota' },
{ key: 'used_quota', text: '按已用额度排序', value: 'used_quota' },
]}
value={orderBy}
onChange={handleOrderByChange}
style={{ marginLeft: '10px' }}
/>
<Pagination
floated='right'
activePage={activePage}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-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';
@@ -25,9 +25,10 @@ const UsersTable = () => {
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}`);
const res = await API.get(`/api/user/?p=${startIdx}&order=${orderBy}`);
const { success, message, data } = res.data;
if (success) {
if (startIdx === 0) {
@@ -47,19 +48,19 @@ const UsersTable = () => {
(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);
await loadUsers(activePage - 1, orderBy);
}
setActivePage(activePage);
})();
};
useEffect(() => {
loadUsers(0)
loadUsers(0, orderBy)
.then()
.catch((reason) => {
showError(reason);
});
}, []);
}, [orderBy]);
const manageUser = (username, action, idx) => {
(async () => {
@@ -110,6 +111,7 @@ const UsersTable = () => {
// if keyword is blank, load files instead.
await loadUsers(0);
setActivePage(1);
setOrderBy('');
return;
}
setSearching(true);
@@ -148,6 +150,11 @@ const UsersTable = () => {
setLoading(false);
};
const handleOrderByChange = (e, { value }) => {
setOrderBy(value);
setActivePage(1);
};
return (
<>
<Form onSubmit={searchUsers}>
@@ -322,6 +329,19 @@ const UsersTable = () => {
<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}

View File

@@ -160,7 +160,7 @@ const EditChannel = () => {
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
}
if (localInputs.type === 3 && localInputs.other === '') {
localInputs.other = '2023-12-01-preview';
localInputs.other = '2024-03-01-preview';
}
if (localInputs.type === 18 && localInputs.other === '') {
localInputs.other = 'v2.1';
@@ -242,7 +242,7 @@ const EditChannel = () => {
<Form.Input
label='默认 API 版本'
name='other'
placeholder={'请输入默认 API 版本例如2023-12-01-preview该配置可以被实际的请求查询参数所覆盖'}
placeholder={'请输入默认 API 版本例如2024-03-01-preview该配置可以被实际的请求查询参数所覆盖'}
onChange={handleInputChange}
value={inputs.other}
autoComplete='new-password'