mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-12 11:23:42 +08:00
feat: add new theme berry (#860)
* feat: add theme berry * docs: add development notes * fix: fix blank page * chore: update implementation * fix: fix package.json * chore: update ui copy --------- Co-authored-by: JustSong <songquanpeng@foxmail.com>
This commit is contained in:
294
web/berry/src/views/Channel/index.js
Normal file
294
web/berry/src/views/Channel/index.js
Normal file
@@ -0,0 +1,294 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { showError, showSuccess, showInfo } from 'utils/common';
|
||||
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import TablePagination from '@mui/material/TablePagination';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
|
||||
import { Button, IconButton, Card, Box, Stack, Container, Typography, Divider } from '@mui/material';
|
||||
import ChannelTableRow from './component/TableRow';
|
||||
import ChannelTableHead from './component/TableHead';
|
||||
import TableToolBar from 'ui-component/TableToolBar';
|
||||
import { API } from 'utils/api';
|
||||
import { ITEMS_PER_PAGE } from 'constants';
|
||||
import { IconRefresh, IconHttpDelete, IconPlus, IconBrandSpeedtest, IconCoinYuan } from '@tabler/icons-react';
|
||||
import EditeModal from './component/EditModal';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// CHANNEL_OPTIONS,
|
||||
export default function ChannelPage() {
|
||||
const [channels, setChannels] = useState([]);
|
||||
const [activePage, setActivePage] = useState(0);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const theme = useTheme();
|
||||
const matchUpMd = useMediaQuery(theme.breakpoints.up('sm'));
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [editChannelId, setEditChannelId] = useState(0);
|
||||
|
||||
const loadChannels = async (startIdx) => {
|
||||
setSearching(true);
|
||||
const res = await API.get(`/api/channel/?p=${startIdx}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (startIdx === 0) {
|
||||
setChannels(data);
|
||||
} else {
|
||||
let newChannels = [...channels];
|
||||
newChannels.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
|
||||
setChannels(newChannels);
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const onPaginationChange = (event, activePage) => {
|
||||
(async () => {
|
||||
if (activePage === Math.ceil(channels.length / ITEMS_PER_PAGE)) {
|
||||
// In this case we have to load more data and then append them.
|
||||
await loadChannels(activePage);
|
||||
}
|
||||
setActivePage(activePage);
|
||||
})();
|
||||
};
|
||||
|
||||
const searchChannels = async (event) => {
|
||||
event.preventDefault();
|
||||
if (searchKeyword === '') {
|
||||
await loadChannels(0);
|
||||
setActivePage(0);
|
||||
return;
|
||||
}
|
||||
setSearching(true);
|
||||
const res = await API.get(`/api/channel/search?keyword=${searchKeyword}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setChannels(data);
|
||||
setActivePage(0);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const handleSearchKeyword = (event) => {
|
||||
setSearchKeyword(event.target.value);
|
||||
};
|
||||
|
||||
const manageChannel = async (id, action, value) => {
|
||||
const url = '/api/channel/';
|
||||
let data = { id };
|
||||
let res;
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
res = await API.delete(url + id);
|
||||
break;
|
||||
case 'status':
|
||||
res = await API.put(url, {
|
||||
...data,
|
||||
status: value
|
||||
});
|
||||
break;
|
||||
case 'priority':
|
||||
if (value === '') {
|
||||
return;
|
||||
}
|
||||
res = await API.put(url, {
|
||||
...data,
|
||||
priority: parseInt(value)
|
||||
});
|
||||
break;
|
||||
case 'test':
|
||||
res = await API.get(url + `test/${id}`);
|
||||
break;
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('操作成功完成!');
|
||||
if (action === 'delete') {
|
||||
await handleRefresh();
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
};
|
||||
|
||||
// 处理刷新
|
||||
const handleRefresh = async () => {
|
||||
await loadChannels(activePage);
|
||||
};
|
||||
|
||||
// 处理测试所有启用渠道
|
||||
const testAllChannels = async () => {
|
||||
const res = await API.get(`/api/channel/test`);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showInfo('已成功开始测试所有通道,请刷新页面查看结果。');
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理删除所有禁用渠道
|
||||
const deleteAllDisabledChannels = async () => {
|
||||
const res = await API.delete(`/api/channel/disabled`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
showSuccess(`已删除所有禁用渠道,共计 ${data} 个`);
|
||||
await handleRefresh();
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理更新所有启用渠道余额
|
||||
const updateAllChannelsBalance = async () => {
|
||||
setSearching(true);
|
||||
const res = await API.get(`/api/channel/update_balance`);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showInfo('已更新完毕所有已启用通道余额!');
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const handleOpenModal = (channelId) => {
|
||||
setEditChannelId(channelId);
|
||||
setOpenModal(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setOpenModal(false);
|
||||
setEditChannelId(0);
|
||||
};
|
||||
|
||||
const handleOkModal = (status) => {
|
||||
if (status === true) {
|
||||
handleCloseModal();
|
||||
handleRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadChannels(0)
|
||||
.then()
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
|
||||
<Typography variant="h4">渠道</Typography>
|
||||
|
||||
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
|
||||
新建渠道
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack mb={5}>
|
||||
<Alert severity="info">
|
||||
当前版本测试是通过按照 OpenAI API 格式使用 gpt-3.5-turbo
|
||||
模型进行非流式请求实现的,因此测试报错并不一定代表通道不可用,该功能后续会修复。 另外,OpenAI 渠道已经不再支持通过 key
|
||||
获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。
|
||||
</Alert>
|
||||
</Stack>
|
||||
<Card>
|
||||
<Box component="form" onSubmit={searchChannels} noValidate>
|
||||
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索渠道的 ID,名称和密钥 ...'} />
|
||||
</Box>
|
||||
<Toolbar
|
||||
sx={{
|
||||
textAlign: 'right',
|
||||
height: 50,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
p: (theme) => theme.spacing(0, 1, 0, 3)
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
{matchUpMd ? (
|
||||
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
|
||||
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button onClick={testAllChannels} startIcon={<IconBrandSpeedtest width={'18px'} />}>
|
||||
测试启用渠道
|
||||
</Button>
|
||||
<Button onClick={updateAllChannelsBalance} startIcon={<IconCoinYuan width={'18px'} />}>
|
||||
更新启用余额
|
||||
</Button>
|
||||
<Button onClick={deleteAllDisabledChannels} startIcon={<IconHttpDelete width={'18px'} />}>
|
||||
删除禁用渠道
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
) : (
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
divider={<Divider orientation="vertical" flexItem />}
|
||||
justifyContent="space-around"
|
||||
alignItems="center"
|
||||
>
|
||||
<IconButton onClick={handleRefresh} size="large">
|
||||
<IconRefresh />
|
||||
</IconButton>
|
||||
<IconButton onClick={testAllChannels} size="large">
|
||||
<IconBrandSpeedtest />
|
||||
</IconButton>
|
||||
<IconButton onClick={updateAllChannelsBalance} size="large">
|
||||
<IconCoinYuan />
|
||||
</IconButton>
|
||||
<IconButton onClick={deleteAllDisabledChannels} size="large">
|
||||
<IconHttpDelete />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
)}
|
||||
</Container>
|
||||
</Toolbar>
|
||||
{searching && <LinearProgress />}
|
||||
<PerfectScrollbar component="div">
|
||||
<TableContainer sx={{ overflow: 'unset' }}>
|
||||
<Table sx={{ minWidth: 800 }}>
|
||||
<ChannelTableHead />
|
||||
<TableBody>
|
||||
{channels.slice(activePage * ITEMS_PER_PAGE, (activePage + 1) * ITEMS_PER_PAGE).map((row) => (
|
||||
<ChannelTableRow
|
||||
item={row}
|
||||
manageChannel={manageChannel}
|
||||
key={row.id}
|
||||
handleOpenModal={handleOpenModal}
|
||||
setModalChannelId={setEditChannelId}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</PerfectScrollbar>
|
||||
<TablePagination
|
||||
page={activePage}
|
||||
component="div"
|
||||
count={channels.length + (channels.length % ITEMS_PER_PAGE === 0 ? 1 : 0)}
|
||||
rowsPerPage={ITEMS_PER_PAGE}
|
||||
onPageChange={onPaginationChange}
|
||||
rowsPerPageOptions={[ITEMS_PER_PAGE]}
|
||||
/>
|
||||
</Card>
|
||||
<EditeModal open={openModal} onCancel={handleCloseModal} onOk={handleOkModal} channelId={editChannelId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user