优化项目 (#75)

* 💄 improve: channel search

* 💄 improve: channel Table

* 💄 improve: add channel batch
This commit is contained in:
Buer 2024-02-26 19:06:18 +08:00 committed by GitHub
parent ec64bf1ad4
commit 6b8ba36213
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 889 additions and 96 deletions

View File

@ -1,6 +1,7 @@
package controller
import (
"errors"
"net/http"
"one-api/common"
"one-api/model"
@ -11,7 +12,7 @@ import (
)
func GetChannelsList(c *gin.Context) {
var params model.GenericParams
var params model.SearchChannelsParams
if err := c.ShouldBindQuery(&params); err != nil {
common.APIRespondWithError(c, http.StatusOK, err)
return
@ -145,3 +146,54 @@ func UpdateChannel(c *gin.Context) {
"data": channel,
})
}
func BatchUpdateChannelsAzureApi(c *gin.Context) {
var params model.BatchChannelsParams
err := c.ShouldBindJSON(&params)
if err != nil {
common.APIRespondWithError(c, http.StatusOK, err)
return
}
if params.Ids == nil || len(params.Ids) == 0 {
common.APIRespondWithError(c, http.StatusOK, errors.New("ids不能为空"))
return
}
var count int64
count, err = model.BatchUpdateChannelsAzureApi(&params)
if err != nil {
common.APIRespondWithError(c, http.StatusOK, err)
return
}
c.JSON(http.StatusOK, gin.H{
"data": count,
"success": true,
"message": "更新成功",
})
}
func BatchDelModelChannels(c *gin.Context) {
var params model.BatchChannelsParams
err := c.ShouldBindJSON(&params)
if err != nil {
common.APIRespondWithError(c, http.StatusOK, err)
return
}
if params.Ids == nil || len(params.Ids) == 0 {
common.APIRespondWithError(c, http.StatusOK, errors.New("ids不能为空"))
return
}
var count int64
count, err = model.BatchDelModelChannels(&params)
if err != nil {
common.APIRespondWithError(c, http.StatusOK, err)
return
}
c.JSON(http.StatusOK, gin.H{
"data": count,
"success": true,
"message": "更新成功",
})
}

View File

@ -2,31 +2,32 @@ package model
import (
"one-api/common"
"strings"
"gorm.io/gorm"
)
type Channel struct {
Id int `json:"id"`
Type int `json:"type" gorm:"default:0"`
Key string `json:"key" gorm:"type:varchar(767);not null;index"`
Status int `json:"status" gorm:"default:1"`
Name string `json:"name" gorm:"index"`
Type int `json:"type" form:"type" gorm:"default:0"`
Key string `json:"key" form:"key" gorm:"type:varchar(767);not null;index"`
Status int `json:"status" form:"status" gorm:"default:1"`
Name string `json:"name" form:"name" gorm:"index"`
Weight *uint `json:"weight" gorm:"default:0"`
CreatedTime int64 `json:"created_time" gorm:"bigint"`
TestTime int64 `json:"test_time" gorm:"bigint"`
ResponseTime int `json:"response_time"` // in milliseconds
BaseURL *string `json:"base_url" gorm:"column:base_url;default:''"`
Other string `json:"other"`
Other string `json:"other" form:"other"`
Balance float64 `json:"balance"` // in USD
BalanceUpdatedTime int64 `json:"balance_updated_time" gorm:"bigint"`
Models string `json:"models"`
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
Models string `json:"models" form:"models"`
Group string `json:"group" form:"group" gorm:"type:varchar(32);default:'default'"`
UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"`
ModelMapping *string `json:"model_mapping" gorm:"type:varchar(1024);default:''"`
Priority *int64 `json:"priority" gorm:"bigint;default:0"`
Proxy *string `json:"proxy" gorm:"type:varchar(255);default:''"`
TestModel string `json:"test_model" gorm:"type:varchar(50);default:''"`
TestModel string `json:"test_model" form:"test_model" gorm:"type:varchar(50);default:''"`
}
var allowedChannelOrderFields = map[string]bool{
@ -40,16 +41,46 @@ var allowedChannelOrderFields = map[string]bool{
"priority": true,
}
func GetChannelsList(params *GenericParams) (*DataResult[Channel], error) {
type SearchChannelsParams struct {
Channel
PaginationParams
}
func GetChannelsList(params *SearchChannelsParams) (*DataResult[Channel], error) {
var channels []*Channel
db := DB.Omit("key")
if params.Keyword != "" {
keyCol := "`key`"
if common.UsingPostgreSQL {
keyCol = `"key"`
}
db = db.Where("id = ? or name LIKE ? or "+keyCol+" = ?", common.String2Int(params.Keyword), params.Keyword+"%", params.Keyword)
if params.Type != 0 {
db = db.Where("type = ?", params.Type)
}
if params.Status != 0 {
db = db.Where("status = ?", params.Status)
}
if params.Name != "" {
db = db.Where("name LIKE ?", params.Name+"%")
}
if params.Group != "" {
db = db.Where("id IN (SELECT channel_id FROM abilities WHERE "+quotePostgresField("group")+" = ?)", params.Group)
}
if params.Models != "" {
db = db.Where("id IN (SELECT channel_id FROM abilities WHERE model IN (?))", params.Models)
}
if params.Other != "" {
db = db.Where("other LIKE ?", params.Other+"%")
}
if params.Key != "" {
db = db.Where(quotePostgresField("key")+" = ?", params.Key)
}
if params.TestModel != "" {
db = db.Where("test_model LIKE ?", params.TestModel+"%")
}
return PaginateAndOrder[Channel](db, &params.PaginationParams, &channels, allowedChannelOrderFields)
@ -87,6 +118,45 @@ func BatchInsertChannels(channels []Channel) error {
return nil
}
type BatchChannelsParams struct {
Value string `json:"value" form:"value" binding:"required"`
Ids []int `json:"ids" form:"ids" binding:"required"`
}
func BatchUpdateChannelsAzureApi(params *BatchChannelsParams) (int64, error) {
db := DB.Model(&Channel{}).Where("id IN ?", params.Ids).Update("other", params.Value)
if db.Error != nil {
return 0, db.Error
}
return db.RowsAffected, nil
}
func BatchDelModelChannels(params *BatchChannelsParams) (int64, error) {
var count int64
var channels []*Channel
err := DB.Select("id, models, "+quotePostgresField("group")).Find(&channels, "id IN ?", params.Ids).Error
if err != nil {
return 0, err
}
for _, channel := range channels {
modelsSlice := strings.Split(channel.Models, ",")
for i, m := range modelsSlice {
if m == params.Value {
modelsSlice = append(modelsSlice[:i], modelsSlice[i+1:]...)
break
}
}
channel.Models = strings.Join(modelsSlice, ",")
channel.Update()
count++
}
return count, nil
}
func (channel *Channel) GetPriority() int64 {
if channel.Priority == nil {
return 0

View File

@ -120,3 +120,23 @@ func getTimestampGroupsSelect(fieldName, groupType, alias string) string {
return groupSelect
}
func quotePostgresField(field string) string {
if common.UsingPostgreSQL {
return fmt.Sprintf(`"%s"`, field)
}
return fmt.Sprintf("`%s`", field)
}
func assembleSumSelectStr(selectStr string) string {
sumSelectStr := "%s(sum(%s),0)"
nullfunc := "ifnull"
if common.UsingPostgreSQL {
nullfunc = "coalesce"
}
sumSelectStr = fmt.Sprintf(sumSelectStr, nullfunc, selectStr)
return sumSelectStr
}

View File

@ -269,15 +269,3 @@ func GetChannelExpensesByPeriod(startTimestamp, endTimestamp int64) (LogStatisti
return LogStatistics, err
}
func assembleSumSelectStr(selectStr string) string {
sumSelectStr := "%s(sum(%s),0)"
nullfunc := "ifnull"
if common.UsingPostgreSQL {
nullfunc = "coalesce"
}
sumSelectStr = fmt.Sprintf(sumSelectStr, nullfunc, selectStr)
return sumSelectStr
}

View File

@ -81,6 +81,8 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.GET("/update_balance/:id", controller.UpdateChannelBalance)
channelRoute.POST("/", controller.AddChannel)
channelRoute.PUT("/", controller.UpdateChannel)
channelRoute.PUT("/batch/azure_api", controller.BatchUpdateChannelsAzureApi)
channelRoute.PUT("/batch/del_model", controller.BatchDelModelChannels)
channelRoute.DELETE("/disabled", controller.DeleteDisabledChannel)
channelRoute.DELETE("/:id", controller.DeleteChannel)
}

View File

@ -1,11 +1,9 @@
import { styled } from '@mui/material/styles';
import { Container } from '@mui/material';
const AdminContainer = styled(Container)(({ theme }) => ({
[theme.breakpoints.down('md')]: {
paddingLeft: '0px',
paddingRight: '0px'
}
}));
const AdminContainer = styled(Container)({
paddingLeft: '0px !important',
paddingRight: '0px !important'
});
export default AdminContainer;

View File

@ -0,0 +1,125 @@
import { useState } from 'react';
import { Grid, TextField, InputAdornment, Checkbox, Button, FormControlLabel, IconButton } from '@mui/material';
import { gridSpacing } from 'store/constant';
import { IconSearch, IconSend } from '@tabler/icons-react';
import { fetchChannelData } from '../index';
import { API } from 'utils/api';
import { showError, showSuccess } from 'utils/common';
const BatchAzureAPI = () => {
const [value, setValue] = useState('');
const [data, setData] = useState([]);
const [selected, setSelected] = useState([]);
const [replaceValue, setReplaceValue] = useState('');
const handleSearch = async () => {
const data = await fetchChannelData(0, 100, { other: value, type: 3 }, 'desc', 'id');
if (data) {
setData(data.data);
}
};
const handleSelect = (id) => {
setSelected((prev) => {
if (prev.includes(id)) {
return prev.filter((i) => i !== id);
} else {
return [...prev, id];
}
});
};
const handleSelectAll = () => {
if (selected.length === data.length) {
setSelected([]);
} else {
setSelected(data.map((item) => item.id));
}
};
const handleSubmit = async () => {
try {
const res = await API.put(`/api/channel/batch/azure_api`, {
ids: selected,
value: replaceValue
});
const { success, message, data } = res.data;
if (success) {
showSuccess('成功更新' + data + '条数据');
return;
} else {
showError(message);
}
} catch (error) {
console.error(error);
}
};
return (
<Grid container spacing={gridSpacing}>
<Grid item xs={12}>
<TextField
sx={{ ml: 1, flex: 1 }}
placeholder="请输入api版本号"
inputProps={{ 'aria-label': '请输入api版本号' }}
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton aria-label="toggle password visibility" onClick={handleSearch} edge="end">
<IconSearch />
</IconButton>
</InputAdornment>
)
}}
/>
</Grid>
{data.length === 0 ? (
<Grid item xs={12}>
暂无数据
</Grid>
) : (
<>
<Grid item xs={12}>
<Button onClick={handleSelectAll}>{selected.length === data.length ? '反全选' : '全选'}</Button>
</Grid>
<Grid item xs={12}>
{data.map((item) => (
<FormControlLabel
key={item.id}
control={<Checkbox checked={selected.includes(item.id)} onChange={() => handleSelect(item.id)} />}
label={item.name + '(' + item.other + ')'}
/>
))}
</Grid>
<Grid item xs={12}>
<TextField
sx={{ ml: 1, flex: 1 }}
placeholder="替换值"
inputProps={{ 'aria-label': '替换值' }}
value={replaceValue}
onChange={(e) => {
setReplaceValue(e.target.value);
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton aria-label="toggle password visibility" onClick={handleSubmit} edge="end">
<IconSend />
</IconButton>
</InputAdornment>
)
}}
/>
</Grid>
</>
)}
</Grid>
);
};
export default BatchAzureAPI;

View File

@ -0,0 +1,125 @@
import { useState } from 'react';
import { Grid, TextField, InputAdornment, Checkbox, Button, FormControlLabel, IconButton, Alert } from '@mui/material';
import { gridSpacing } from 'store/constant';
import { IconSearch, IconHttpDelete } from '@tabler/icons-react';
import { fetchChannelData } from '../index';
import { API } from 'utils/api';
import { showError, showSuccess } from 'utils/common';
const BatchDelModel = () => {
const [value, setValue] = useState('');
const [data, setData] = useState([]);
const [selected, setSelected] = useState([]);
const [loadding, setLoadding] = useState(false);
const handleSearch = async () => {
const data = await fetchChannelData(0, 100, { models: value }, 'desc', 'id');
if (data) {
// 遍历data 逗号分隔models 检测是否只有一个model 如果是则排除
const newData = data.data.filter((item) => {
if (item.models.split(',').length > 1) {
return true;
}
return false;
});
setData(newData);
}
};
const handleSelect = (id) => {
setSelected((prev) => {
if (prev.includes(id)) {
return prev.filter((i) => i !== id);
} else {
return [...prev, id];
}
});
};
const handleSelectAll = () => {
if (selected.length === data.length) {
setSelected([]);
} else {
setSelected(data.map((item) => item.id));
}
};
const handleSubmit = async () => {
if (value === '' || selected.length === 0) {
return;
}
setLoadding(true);
try {
const res = await API.put(`/api/channel/batch/del_model`, {
ids: selected,
value: value
});
const { success, message, data } = res.data;
if (success) {
showSuccess('成功删除' + data + '条数据');
} else {
showError(message);
}
} catch (error) {
console.error(error);
}
setLoadding(false);
};
return (
<Grid container spacing={gridSpacing}>
<Grid item xs={12}>
<Alert severity="info">如果渠道只有一个模型的将不会显示请手动去列表删除渠道</Alert>
</Grid>
<Grid item xs={12}>
<TextField
sx={{ ml: 1, flex: 1 }}
placeholder="请输入完整模型名称"
inputProps={{ 'aria-label': '请输入完整模型名称' }}
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton aria-label="toggle password visibility" onClick={handleSearch} edge="end">
<IconSearch />
</IconButton>
</InputAdornment>
)
}}
/>
</Grid>
{data.length === 0 ? (
<Grid item xs={12}>
暂无数据
</Grid>
) : (
<>
<Grid item xs={12}>
<Button onClick={handleSelectAll}>{selected.length === data.length ? '反全选' : '全选'}</Button>
</Grid>
<Grid item xs={12}>
{data.map((item) => (
<FormControlLabel
key={item.id}
control={<Checkbox checked={selected.includes(item.id)} onChange={() => handleSelect(item.id)} />}
label={item.name}
/>
))}
</Grid>
<Grid item xs={12}>
<Button variant="contained" color="primary" startIcon={<IconHttpDelete />} onClick={handleSubmit} disabled={loadding}>
删除
</Button>
</Grid>
</>
)}
</Grid>
);
};
export default BatchDelModel;

View File

@ -0,0 +1,67 @@
import PropTypes from 'prop-types';
import { useState } from 'react';
import { Dialog, DialogTitle, DialogContent, DialogActions, Divider, Button, Tabs, Tab, Box } from '@mui/material';
import BatchAzureAPI from './BatchAzureAPI';
import BatchDelModel from './BatchDelModel';
function CustomTabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div role="tabpanel" hidden={value !== index} id={`setting-tabpanel-${index}`} aria-labelledby={`channel-tab-${index}`} {...other}>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}
CustomTabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired
};
function a11yProps(index) {
return {
id: `channel-tab-${index}`,
'aria-controls': `channel-tabpanel-${index}`
};
}
const BatchModal = ({ open, setOpen }) => {
const [value, setValue] = useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<Dialog open={open} onClose={() => setOpen(!open)} fullWidth maxWidth={'md'}>
<DialogTitle>
<Box>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs channel">
<Tab label="Azure 版本号" {...a11yProps(0)} />
<Tab label="批量删除模型" {...a11yProps(1)} />
</Tabs>
</Box>
</DialogTitle>
<Divider />
<DialogContent>
<CustomTabPanel value={value} index={0}>
<BatchAzureAPI />
</CustomTabPanel>
<CustomTabPanel value={value} index={1}>
<BatchDelModel />
</CustomTabPanel>
<DialogActions>
<Button onClick={() => setOpen(!open)}>取消</Button>
</DialogActions>
</DialogContent>
</Dialog>
);
};
export default BatchModal;
BatchModal.propTypes = {
open: PropTypes.bool,
setOpen: PropTypes.func
};

View File

@ -66,13 +66,12 @@ const validationSchema = Yup.object().shape({
})
});
const EditModal = ({ open, channelId, onCancel, onOk }) => {
const EditModal = ({ open, channelId, onCancel, onOk, groupOptions }) => {
const theme = useTheme();
// const [loading, setLoading] = useState(false);
const [initialInput, setInitialInput] = useState(defaultConfig.input);
const [inputLabel, setInputLabel] = useState(defaultConfig.inputLabel); //
const [inputPrompt, setInputPrompt] = useState(defaultConfig.prompt);
const [groupOptions, setGroupOptions] = useState([]);
const [modelOptions, setModelOptions] = useState([]);
const initChannel = (typeValue) => {
@ -123,15 +122,6 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
return modelList;
};
const fetchGroups = async () => {
try {
let res = await API.get(`/api/group/`);
setGroupOptions(res.data.data);
} catch (error) {
showError(error.message);
}
};
const fetchModels = async () => {
try {
let res = await API.get(`/api/channel/models`);
@ -252,7 +242,6 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
};
useEffect(() => {
fetchGroups().then();
fetchModels().then();
}, []);
@ -596,5 +585,6 @@ EditModal.propTypes = {
open: PropTypes.bool,
channelId: PropTypes.number,
onCancel: PropTypes.func,
onOk: PropTypes.func
onOk: PropTypes.func,
groupOptions: PropTypes.array
};

View File

@ -21,7 +21,11 @@ import {
DialogContentText,
DialogTitle,
Tooltip,
Button
Button,
Grid,
Collapse,
Typography,
Box
} from '@mui/material';
import Label from 'ui-component/Label';
@ -29,9 +33,11 @@ import TableSwitch from 'ui-component/Switch';
import ResponseTimeLabel from './ResponseTimeLabel';
import GroupLabel from './GroupLabel';
import NameLabel from './NameLabel';
import { IconDotsVertical, IconEdit, IconTrash, IconPencil } from '@tabler/icons-react';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import { copy } from 'utils/common';
export default function ChannelTableRow({ item, manageChannel, handleOpenModal, setModalChannelId }) {
const [open, setOpen] = useState(null);
@ -41,6 +47,11 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
const [responseTimeData, setResponseTimeData] = useState({ test_time: item.test_time, response_time: item.response_time });
const [itemBalance, setItemBalance] = useState(item.balance);
const [openRow, setOpenRow] = useState(false);
let modelMap = [];
modelMap = item.models.split(',');
modelMap.sort();
const handleDeleteOpen = () => {
handleCloseMenu();
setOpenDelete(true);
@ -105,11 +116,15 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
return (
<>
<TableRow tabIndex={item.id}>
<TableCell>
<IconButton aria-label="expand row" size="small" onClick={() => setOpenRow(!openRow)}>
{openRow ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
<TableCell>{item.id}</TableCell>
<TableCell>
<NameLabel name={item.name} models={item.models} />
</TableCell>
<TableCell>{item.name}</TableCell>
<TableCell>
<GroupLabel group={item.group} />
@ -126,7 +141,6 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
</Label>
)}
</TableCell>
<TableCell>
<TableSwitch id={`switch-${item.id}`} checked={statusSwitch === 1} onChange={handleStatus} />
</TableCell>
@ -196,6 +210,81 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
</MenuItem>
</Popover>
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0, textAlign: 'left' }} colSpan={10}>
<Collapse in={openRow} timeout="auto" unmountOnExit>
<Grid container spacing={1}>
<Grid item xs={12}>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '10px', margin: 1 }}>
<Typography variant="h6" gutterBottom component="div">
可用模型:
</Typography>
{modelMap.map((model) => (
<Label
variant="outlined"
color="primary"
key={model}
onClick={() => {
copy(model, '模型名称');
}}
>
{model}
</Label>
))}
</Box>
</Grid>
{item.test_model && (
<Grid item xs={12}>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '10px', margin: 1 }}>
<Typography variant="h6" gutterBottom component="div">
测速模型:
</Typography>
<Label
variant="outlined"
color="default"
key={item.test_model}
onClick={() => {
copy(item.test_model, '测速模型');
}}
>
{item.test_model}
</Label>
</Box>
</Grid>
)}
{item.proxy && (
<Grid item xs={12}>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '10px', margin: 1 }}>
<Typography variant="h6" gutterBottom component="div">
代理地址:
</Typography>
{item.proxy}
</Box>
</Grid>
)}
{item.other && (
<Grid item xs={12}>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '10px', margin: 1 }}>
<Typography variant="h6" gutterBottom component="div">
其他参数:
</Typography>
<Label
variant="outlined"
color="default"
key={item.other}
onClick={() => {
copy(item.other, '其他参数');
}}
>
{item.other}
</Label>
</Box>
</Grid>
)}
</Grid>
</Collapse>
</TableCell>
</TableRow>
<Dialog open={openDelete} onClose={handleDeleteClose}>
<DialogTitle>删除通道</DialogTitle>
<DialogContent>

View File

@ -0,0 +1,216 @@
import PropTypes from 'prop-types';
import { useTheme } from '@mui/material/styles';
import { IconKey, IconBrandGithubCopilot, IconSitemap, IconVersions } from '@tabler/icons-react';
import { InputAdornment, OutlinedInput, Stack, FormControl, InputLabel, Select, MenuItem } from '@mui/material'; //
import { CHANNEL_OPTIONS } from 'constants/ChannelConstants';
// ----------------------------------------------------------------------
export default function TableToolBar({ filterName, handleFilterName, groupOptions }) {
const theme = useTheme();
const grey500 = theme.palette.grey[500];
return (
<>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={{ xs: 3, sm: 2, md: 4 }} padding={'24px'} paddingBottom={'0px'}>
<FormControl>
<InputLabel htmlFor="channel-name-label">渠道名称</InputLabel>
<OutlinedInput
id="name"
name="name"
sx={{
minWidth: '100%'
}}
label="渠道名称"
value={filterName.name}
onChange={handleFilterName}
placeholder="渠道名称"
startAdornment={
<InputAdornment position="start">
<IconSitemap stroke={1.5} size="20px" color={grey500} />
</InputAdornment>
}
/>
</FormControl>
<FormControl>
<InputLabel htmlFor="channel-models-label">模型名称</InputLabel>
<OutlinedInput
id="models"
name="models"
sx={{
minWidth: '100%'
}}
label="模型名称"
value={filterName.models}
onChange={handleFilterName}
placeholder="模型名称"
startAdornment={
<InputAdornment position="start">
<IconBrandGithubCopilot stroke={1.5} size="20px" color={grey500} />
</InputAdornment>
}
/>
</FormControl>
<FormControl>
<InputLabel htmlFor="channel-test_model-label">测试模型</InputLabel>
<OutlinedInput
id="test_model"
name="test_model"
sx={{
minWidth: '100%'
}}
label="测试模型"
value={filterName.test_model}
onChange={handleFilterName}
placeholder="测试模型"
startAdornment={
<InputAdornment position="start">
<IconBrandGithubCopilot stroke={1.5} size="20px" color={grey500} />
</InputAdornment>
}
/>
</FormControl>
<FormControl>
<InputLabel htmlFor="channel-key-label">key</InputLabel>
<OutlinedInput
id="key"
name="key"
sx={{
minWidth: '100%'
}}
label="key"
value={filterName.key}
onChange={handleFilterName}
placeholder="key"
startAdornment={
<InputAdornment position="start">
<IconKey stroke={1.5} size="20px" color={grey500} />
</InputAdornment>
}
/>
</FormControl>
<FormControl>
<InputLabel htmlFor="channel-other-label">其他参数</InputLabel>
<OutlinedInput
id="other"
name="other"
sx={{
minWidth: '100%'
}}
label="其他参数"
value={filterName.other}
onChange={handleFilterName}
placeholder="其他参数"
startAdornment={
<InputAdornment position="start">
<IconVersions stroke={1.5} size="20px" color={grey500} />
</InputAdornment>
}
/>
</FormControl>
</Stack>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={{ xs: 3, sm: 2, md: 4 }} padding={'24px'}>
<FormControl sx={{ minWidth: '22%' }}>
<InputLabel htmlFor="channel-type-label">渠道类型</InputLabel>
<Select
id="channel-type-label"
label="渠道类型"
value={filterName.type}
name="type"
onChange={handleFilterName}
sx={{
minWidth: '100%'
}}
MenuProps={{
PaperProps: {
style: {
maxHeight: 200
}
}
}}
>
<MenuItem key={0} value={0}>
全部
</MenuItem>
{Object.values(CHANNEL_OPTIONS).map((option) => {
return (
<MenuItem key={option.value} value={option.value}>
{option.text}
</MenuItem>
);
})}
</Select>
</FormControl>
<FormControl sx={{ minWidth: '22%' }}>
<InputLabel htmlFor="channel-status-label">状态</InputLabel>
<Select
id="channel-status-label"
label="状态"
value={filterName.status}
name="status"
onChange={handleFilterName}
sx={{
minWidth: '100%'
}}
MenuProps={{
PaperProps: {
style: {
maxHeight: 200
}
}
}}
>
<MenuItem key={0} value={0}>
全部
</MenuItem>
<MenuItem key={1} value={1}>
启用
</MenuItem>
<MenuItem key={2} value={2}>
禁用
</MenuItem>
<MenuItem key={3} value={3}>
测速禁用
</MenuItem>
</Select>
</FormControl>
<FormControl sx={{ minWidth: '22%' }}>
<InputLabel htmlFor="channel-group-label">分组</InputLabel>
<Select
id="channel-group-label"
label="分组"
value={filterName.group}
name="group"
onChange={handleFilterName}
sx={{
minWidth: '100%'
}}
MenuProps={{
PaperProps: {
style: {
maxHeight: 200
}
}
}}
>
{groupOptions.map((option) => {
return (
<MenuItem key={option} value={option}>
{option}
</MenuItem>
);
})}
</Select>
</FormControl>
</Stack>
</>
);
}
TableToolBar.propTypes = {
filterName: PropTypes.object,
handleFilterName: PropTypes.func,
groupOptions: PropTypes.array
};

View File

@ -8,7 +8,6 @@ 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';
@ -16,11 +15,49 @@ import useMediaQuery from '@mui/material/useMediaQuery';
import { Button, IconButton, Card, Box, Stack, Container, Typography, Divider } from '@mui/material';
import ChannelTableRow from './component/TableRow';
import KeywordTableHead from 'ui-component/TableHead';
import TableToolBar from 'ui-component/TableToolBar';
import { API } from 'utils/api';
import { IconRefresh, IconHttpDelete, IconPlus, IconBrandSpeedtest, IconCoinYuan } from '@tabler/icons-react';
import { IconRefresh, IconHttpDelete, IconPlus, IconMenu2, IconBrandSpeedtest, IconCoinYuan, IconSearch } from '@tabler/icons-react';
import EditeModal from './component/EditModal';
import { ITEMS_PER_PAGE } from 'constants';
import TableToolBar from './component/TableToolBar';
import BatchModal from './component/BatchModal';
const originalKeyword = {
type: 0,
status: 0,
name: '',
group: '',
models: '',
key: '',
test_model: '',
other: ''
};
export async function fetchChannelData(page, rowsPerPage, keyword, order, orderBy) {
try {
if (orderBy) {
orderBy = order === 'desc' ? '-' + orderBy : orderBy;
}
const res = await API.get(`/api/channel/`, {
params: {
page: page + 1,
size: rowsPerPage,
order: orderBy,
...keyword
}
});
const { success, message, data } = res.data;
if (success) {
return data;
} else {
showError(message);
}
} catch (error) {
console.error(error);
}
return false;
}
// ----------------------------------------------------------------------
// CHANNEL_OPTIONS,
@ -30,15 +67,19 @@ export default function ChannelPage() {
const [orderBy, setOrderBy] = useState('id');
const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE);
const [listCount, setListCount] = useState(0);
const [searchKeyword, setSearchKeyword] = useState('');
const [searching, setSearching] = useState(false);
const [channels, setChannels] = useState([]);
const [refreshFlag, setRefreshFlag] = useState(false);
const [groupOptions, setGroupOptions] = useState([]);
const [toolBarValue, setToolBarValue] = useState(originalKeyword);
const [searchKeyword, setSearchKeyword] = useState(originalKeyword);
const theme = useTheme();
const matchUpMd = useMediaQuery(theme.breakpoints.up('sm'));
const [openModal, setOpenModal] = useState(false);
const [editChannelId, setEditChannelId] = useState(0);
const [openBatchModal, setOpenBatchModal] = useState(false);
const handleSort = (event, id) => {
const isAsc = orderBy === id && order === 'asc';
@ -57,11 +98,15 @@ export default function ChannelPage() {
setRowsPerPage(parseInt(event.target.value, 10));
};
const searchChannels = async (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const searchChannels = async () => {
// event.preventDefault();
// const formData = new FormData(event.target);
setPage(0);
setSearchKeyword(formData.get('keyword'));
setSearchKeyword(toolBarValue);
};
const handleToolBarValue = (event) => {
setToolBarValue({ ...toolBarValue, [event.target.name]: event.target.value });
};
const manageChannel = async (id, action, value) => {
@ -113,6 +158,8 @@ export default function ChannelPage() {
const handleRefresh = async () => {
setOrderBy('id');
setOrder('desc');
setToolBarValue(originalKeyword);
setSearchKeyword(originalKeyword);
setRefreshFlag(!refreshFlag);
};
@ -184,55 +231,51 @@ export default function ChannelPage() {
const fetchData = async (page, rowsPerPage, keyword, order, orderBy) => {
setSearching(true);
try {
if (orderBy) {
orderBy = order === 'desc' ? '-' + orderBy : orderBy;
}
const res = await API.get(`/api/channel/`, {
params: {
page: page + 1,
size: rowsPerPage,
keyword: keyword,
order: orderBy
}
});
const { success, message, data } = res.data;
if (success) {
setListCount(data.total_count);
setChannels(data.data);
} else {
showError(message);
}
} catch (error) {
console.error(error);
const data = await fetchChannelData(page, rowsPerPage, keyword, order, orderBy);
if (data) {
setListCount(data.total_count);
setChannels(data.data);
}
setSearching(false);
};
const fetchGroups = async () => {
try {
let res = await API.get(`/api/group/`);
setGroupOptions(res.data.data);
} catch (error) {
showError(error.message);
}
};
useEffect(() => {
fetchData(page, rowsPerPage, searchKeyword, order, orderBy);
}, [page, rowsPerPage, searchKeyword, order, orderBy, refreshFlag]);
useEffect(() => {
fetchGroups().then();
}, []);
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>
<ButtonGroup variant="contained" aria-label="outlined small primary button group">
<Button color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
新建渠道
</Button>
<Button color="primary" startIcon={<IconMenu2 />} onClick={() => setOpenBatchModal(true)}>
批量处理
</Button>
</ButtonGroup>
</Stack>
<Card>
<Box component="form" onSubmit={searchChannels} noValidate>
<TableToolBar placeholder={'搜索渠道的 ID名称和密钥 ...'} />
<Box component="form" noValidate>
<TableToolBar filterName={toolBarValue} handleFilterName={handleToolBarValue} groupOptions={groupOptions} />
</Box>
<Toolbar
sx={{
textAlign: 'right',
@ -246,7 +289,10 @@ export default function ChannelPage() {
{matchUpMd ? (
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
刷新
刷新/清除搜索条件
</Button>
<Button onClick={searchChannels} startIcon={<IconSearch width={'18px'} />}>
搜索
</Button>
<Button onClick={testAllChannels} startIcon={<IconBrandSpeedtest width={'18px'} />}>
测试启用渠道
@ -269,6 +315,9 @@ export default function ChannelPage() {
<IconButton onClick={handleRefresh} size="large">
<IconRefresh />
</IconButton>
<IconButton onClick={searchChannels} size="large">
<IconSearch />
</IconButton>
<IconButton onClick={testAllChannels} size="large">
<IconBrandSpeedtest />
</IconButton>
@ -291,6 +340,7 @@ export default function ChannelPage() {
orderBy={orderBy}
onRequestSort={handleSort}
headLabel={[
{ id: 'collapse', label: '', disableSort: true },
{ id: 'id', label: 'ID', disableSort: false },
{ id: 'name', label: '名称', disableSort: false },
{ id: 'group', label: '分组', disableSort: true },
@ -328,7 +378,8 @@ export default function ChannelPage() {
showLastButton
/>
</Card>
<EditeModal open={openModal} onCancel={handleCloseModal} onOk={handleOkModal} channelId={editChannelId} />
<EditeModal open={openModal} onCancel={handleCloseModal} onOk={handleOkModal} channelId={editChannelId} groupOptions={groupOptions} />
<BatchModal open={openBatchModal} setOpen={setOpenBatchModal} />
</>
);
}