优化项目 (#75)
* 💄 improve: channel search * 💄 improve: channel Table * 💄 improve: add channel batch
This commit is contained in:
parent
ec64bf1ad4
commit
6b8ba36213
@ -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(¶ms); 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(¶ms)
|
||||
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(¶ms)
|
||||
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(¶ms)
|
||||
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(¶ms)
|
||||
if err != nil {
|
||||
common.APIRespondWithError(c, http.StatusOK, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": count,
|
||||
"success": true,
|
||||
"message": "更新成功",
|
||||
})
|
||||
}
|
||||
|
100
model/channel.go
100
model/channel.go
@ -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, ¶ms.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
|
||||
|
@ -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
|
||||
}
|
||||
|
12
model/log.go
12
model/log.go
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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;
|
||||
|
125
web/src/views/Channel/component/BatchAzureAPI.js
Normal file
125
web/src/views/Channel/component/BatchAzureAPI.js
Normal 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;
|
125
web/src/views/Channel/component/BatchDelModel.js
Normal file
125
web/src/views/Channel/component/BatchDelModel.js
Normal 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;
|
67
web/src/views/Channel/component/BatchModal.js
Normal file
67
web/src/views/Channel/component/BatchModal.js
Normal 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
|
||||
};
|
@ -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
|
||||
};
|
||||
|
@ -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>
|
||||
|
216
web/src/views/Channel/component/TableToolBar.js
Normal file
216
web/src/views/Channel/component/TableToolBar.js
Normal 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
|
||||
};
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user