优化项目 (#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
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
@ -11,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func GetChannelsList(c *gin.Context) {
|
func GetChannelsList(c *gin.Context) {
|
||||||
var params model.GenericParams
|
var params model.SearchChannelsParams
|
||||||
if err := c.ShouldBindQuery(¶ms); err != nil {
|
if err := c.ShouldBindQuery(¶ms); err != nil {
|
||||||
common.APIRespondWithError(c, http.StatusOK, err)
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
return
|
return
|
||||||
@ -145,3 +146,54 @@ func UpdateChannel(c *gin.Context) {
|
|||||||
"data": channel,
|
"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": "更新成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -2,31 +2,32 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Type int `json:"type" gorm:"default:0"`
|
Type int `json:"type" form:"type" gorm:"default:0"`
|
||||||
Key string `json:"key" gorm:"type:varchar(767);not null;index"`
|
Key string `json:"key" form:"key" gorm:"type:varchar(767);not null;index"`
|
||||||
Status int `json:"status" gorm:"default:1"`
|
Status int `json:"status" form:"status" gorm:"default:1"`
|
||||||
Name string `json:"name" gorm:"index"`
|
Name string `json:"name" form:"name" gorm:"index"`
|
||||||
Weight *uint `json:"weight" gorm:"default:0"`
|
Weight *uint `json:"weight" gorm:"default:0"`
|
||||||
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||||
TestTime int64 `json:"test_time" gorm:"bigint"`
|
TestTime int64 `json:"test_time" gorm:"bigint"`
|
||||||
ResponseTime int `json:"response_time"` // in milliseconds
|
ResponseTime int `json:"response_time"` // in milliseconds
|
||||||
BaseURL *string `json:"base_url" gorm:"column:base_url;default:''"`
|
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
|
Balance float64 `json:"balance"` // in USD
|
||||||
BalanceUpdatedTime int64 `json:"balance_updated_time" gorm:"bigint"`
|
BalanceUpdatedTime int64 `json:"balance_updated_time" gorm:"bigint"`
|
||||||
Models string `json:"models"`
|
Models string `json:"models" form:"models"`
|
||||||
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
|
Group string `json:"group" form:"group" gorm:"type:varchar(32);default:'default'"`
|
||||||
UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"`
|
UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"`
|
||||||
ModelMapping *string `json:"model_mapping" gorm:"type:varchar(1024);default:''"`
|
ModelMapping *string `json:"model_mapping" gorm:"type:varchar(1024);default:''"`
|
||||||
Priority *int64 `json:"priority" gorm:"bigint;default:0"`
|
Priority *int64 `json:"priority" gorm:"bigint;default:0"`
|
||||||
Proxy *string `json:"proxy" gorm:"type:varchar(255);default:''"`
|
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{
|
var allowedChannelOrderFields = map[string]bool{
|
||||||
@ -40,16 +41,46 @@ var allowedChannelOrderFields = map[string]bool{
|
|||||||
"priority": true,
|
"priority": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetChannelsList(params *GenericParams) (*DataResult[Channel], error) {
|
type SearchChannelsParams struct {
|
||||||
|
Channel
|
||||||
|
PaginationParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetChannelsList(params *SearchChannelsParams) (*DataResult[Channel], error) {
|
||||||
var channels []*Channel
|
var channels []*Channel
|
||||||
|
|
||||||
db := DB.Omit("key")
|
db := DB.Omit("key")
|
||||||
if params.Keyword != "" {
|
|
||||||
keyCol := "`key`"
|
if params.Type != 0 {
|
||||||
if common.UsingPostgreSQL {
|
db = db.Where("type = ?", params.Type)
|
||||||
keyCol = `"key"`
|
|
||||||
}
|
}
|
||||||
db = db.Where("id = ? or name LIKE ? or "+keyCol+" = ?", common.String2Int(params.Keyword), params.Keyword+"%", params.Keyword)
|
|
||||||
|
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)
|
return PaginateAndOrder[Channel](db, ¶ms.PaginationParams, &channels, allowedChannelOrderFields)
|
||||||
@ -87,6 +118,45 @@ func BatchInsertChannels(channels []Channel) error {
|
|||||||
return nil
|
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 {
|
func (channel *Channel) GetPriority() int64 {
|
||||||
if channel.Priority == nil {
|
if channel.Priority == nil {
|
||||||
return 0
|
return 0
|
||||||
|
@ -120,3 +120,23 @@ func getTimestampGroupsSelect(fieldName, groupType, alias string) string {
|
|||||||
|
|
||||||
return groupSelect
|
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
|
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.GET("/update_balance/:id", controller.UpdateChannelBalance)
|
||||||
channelRoute.POST("/", controller.AddChannel)
|
channelRoute.POST("/", controller.AddChannel)
|
||||||
channelRoute.PUT("/", controller.UpdateChannel)
|
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("/disabled", controller.DeleteDisabledChannel)
|
||||||
channelRoute.DELETE("/:id", controller.DeleteChannel)
|
channelRoute.DELETE("/:id", controller.DeleteChannel)
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import { Container } from '@mui/material';
|
import { Container } from '@mui/material';
|
||||||
|
|
||||||
const AdminContainer = styled(Container)(({ theme }) => ({
|
const AdminContainer = styled(Container)({
|
||||||
[theme.breakpoints.down('md')]: {
|
paddingLeft: '0px !important',
|
||||||
paddingLeft: '0px',
|
paddingRight: '0px !important'
|
||||||
paddingRight: '0px'
|
});
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default AdminContainer;
|
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 theme = useTheme();
|
||||||
// const [loading, setLoading] = useState(false);
|
// const [loading, setLoading] = useState(false);
|
||||||
const [initialInput, setInitialInput] = useState(defaultConfig.input);
|
const [initialInput, setInitialInput] = useState(defaultConfig.input);
|
||||||
const [inputLabel, setInputLabel] = useState(defaultConfig.inputLabel); //
|
const [inputLabel, setInputLabel] = useState(defaultConfig.inputLabel); //
|
||||||
const [inputPrompt, setInputPrompt] = useState(defaultConfig.prompt);
|
const [inputPrompt, setInputPrompt] = useState(defaultConfig.prompt);
|
||||||
const [groupOptions, setGroupOptions] = useState([]);
|
|
||||||
const [modelOptions, setModelOptions] = useState([]);
|
const [modelOptions, setModelOptions] = useState([]);
|
||||||
|
|
||||||
const initChannel = (typeValue) => {
|
const initChannel = (typeValue) => {
|
||||||
@ -123,15 +122,6 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
|
|||||||
return modelList;
|
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 () => {
|
const fetchModels = async () => {
|
||||||
try {
|
try {
|
||||||
let res = await API.get(`/api/channel/models`);
|
let res = await API.get(`/api/channel/models`);
|
||||||
@ -252,7 +242,6 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchGroups().then();
|
|
||||||
fetchModels().then();
|
fetchModels().then();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -596,5 +585,6 @@ EditModal.propTypes = {
|
|||||||
open: PropTypes.bool,
|
open: PropTypes.bool,
|
||||||
channelId: PropTypes.number,
|
channelId: PropTypes.number,
|
||||||
onCancel: PropTypes.func,
|
onCancel: PropTypes.func,
|
||||||
onOk: PropTypes.func
|
onOk: PropTypes.func,
|
||||||
|
groupOptions: PropTypes.array
|
||||||
};
|
};
|
||||||
|
@ -21,7 +21,11 @@ import {
|
|||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Button
|
Button,
|
||||||
|
Grid,
|
||||||
|
Collapse,
|
||||||
|
Typography,
|
||||||
|
Box
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
import Label from 'ui-component/Label';
|
import Label from 'ui-component/Label';
|
||||||
@ -29,9 +33,11 @@ import TableSwitch from 'ui-component/Switch';
|
|||||||
|
|
||||||
import ResponseTimeLabel from './ResponseTimeLabel';
|
import ResponseTimeLabel from './ResponseTimeLabel';
|
||||||
import GroupLabel from './GroupLabel';
|
import GroupLabel from './GroupLabel';
|
||||||
import NameLabel from './NameLabel';
|
|
||||||
|
|
||||||
import { IconDotsVertical, IconEdit, IconTrash, IconPencil } from '@tabler/icons-react';
|
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 }) {
|
export default function ChannelTableRow({ item, manageChannel, handleOpenModal, setModalChannelId }) {
|
||||||
const [open, setOpen] = useState(null);
|
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 [responseTimeData, setResponseTimeData] = useState({ test_time: item.test_time, response_time: item.response_time });
|
||||||
const [itemBalance, setItemBalance] = useState(item.balance);
|
const [itemBalance, setItemBalance] = useState(item.balance);
|
||||||
|
|
||||||
|
const [openRow, setOpenRow] = useState(false);
|
||||||
|
let modelMap = [];
|
||||||
|
modelMap = item.models.split(',');
|
||||||
|
modelMap.sort();
|
||||||
|
|
||||||
const handleDeleteOpen = () => {
|
const handleDeleteOpen = () => {
|
||||||
handleCloseMenu();
|
handleCloseMenu();
|
||||||
setOpenDelete(true);
|
setOpenDelete(true);
|
||||||
@ -105,11 +116,15 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableRow tabIndex={item.id}>
|
<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>{item.id}</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>{item.name}</TableCell>
|
||||||
<NameLabel name={item.name} models={item.models} />
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<GroupLabel group={item.group} />
|
<GroupLabel group={item.group} />
|
||||||
@ -126,7 +141,6 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
|
|||||||
</Label>
|
</Label>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<TableSwitch id={`switch-${item.id}`} checked={statusSwitch === 1} onChange={handleStatus} />
|
<TableSwitch id={`switch-${item.id}`} checked={statusSwitch === 1} onChange={handleStatus} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -196,6 +210,81 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal,
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Popover>
|
</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}>
|
<Dialog open={openDelete} onClose={handleDeleteClose}>
|
||||||
<DialogTitle>删除通道</DialogTitle>
|
<DialogTitle>删除通道</DialogTitle>
|
||||||
<DialogContent>
|
<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 PerfectScrollbar from 'react-perfect-scrollbar';
|
||||||
import TablePagination from '@mui/material/TablePagination';
|
import TablePagination from '@mui/material/TablePagination';
|
||||||
import LinearProgress from '@mui/material/LinearProgress';
|
import LinearProgress from '@mui/material/LinearProgress';
|
||||||
import Alert from '@mui/material/Alert';
|
|
||||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
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 { Button, IconButton, Card, Box, Stack, Container, Typography, Divider } from '@mui/material';
|
||||||
import ChannelTableRow from './component/TableRow';
|
import ChannelTableRow from './component/TableRow';
|
||||||
import KeywordTableHead from 'ui-component/TableHead';
|
import KeywordTableHead from 'ui-component/TableHead';
|
||||||
import TableToolBar from 'ui-component/TableToolBar';
|
|
||||||
import { API } from 'utils/api';
|
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 EditeModal from './component/EditModal';
|
||||||
import { ITEMS_PER_PAGE } from 'constants';
|
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,
|
// CHANNEL_OPTIONS,
|
||||||
@ -30,15 +67,19 @@ export default function ChannelPage() {
|
|||||||
const [orderBy, setOrderBy] = useState('id');
|
const [orderBy, setOrderBy] = useState('id');
|
||||||
const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE);
|
const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE);
|
||||||
const [listCount, setListCount] = useState(0);
|
const [listCount, setListCount] = useState(0);
|
||||||
const [searchKeyword, setSearchKeyword] = useState('');
|
|
||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const [channels, setChannels] = useState([]);
|
const [channels, setChannels] = useState([]);
|
||||||
const [refreshFlag, setRefreshFlag] = useState(false);
|
const [refreshFlag, setRefreshFlag] = useState(false);
|
||||||
|
|
||||||
|
const [groupOptions, setGroupOptions] = useState([]);
|
||||||
|
const [toolBarValue, setToolBarValue] = useState(originalKeyword);
|
||||||
|
const [searchKeyword, setSearchKeyword] = useState(originalKeyword);
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchUpMd = useMediaQuery(theme.breakpoints.up('sm'));
|
const matchUpMd = useMediaQuery(theme.breakpoints.up('sm'));
|
||||||
const [openModal, setOpenModal] = useState(false);
|
const [openModal, setOpenModal] = useState(false);
|
||||||
const [editChannelId, setEditChannelId] = useState(0);
|
const [editChannelId, setEditChannelId] = useState(0);
|
||||||
|
const [openBatchModal, setOpenBatchModal] = useState(false);
|
||||||
|
|
||||||
const handleSort = (event, id) => {
|
const handleSort = (event, id) => {
|
||||||
const isAsc = orderBy === id && order === 'asc';
|
const isAsc = orderBy === id && order === 'asc';
|
||||||
@ -57,11 +98,15 @@ export default function ChannelPage() {
|
|||||||
setRowsPerPage(parseInt(event.target.value, 10));
|
setRowsPerPage(parseInt(event.target.value, 10));
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchChannels = async (event) => {
|
const searchChannels = async () => {
|
||||||
event.preventDefault();
|
// event.preventDefault();
|
||||||
const formData = new FormData(event.target);
|
// const formData = new FormData(event.target);
|
||||||
setPage(0);
|
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) => {
|
const manageChannel = async (id, action, value) => {
|
||||||
@ -113,6 +158,8 @@ export default function ChannelPage() {
|
|||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setOrderBy('id');
|
setOrderBy('id');
|
||||||
setOrder('desc');
|
setOrder('desc');
|
||||||
|
setToolBarValue(originalKeyword);
|
||||||
|
setSearchKeyword(originalKeyword);
|
||||||
setRefreshFlag(!refreshFlag);
|
setRefreshFlag(!refreshFlag);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -184,55 +231,51 @@ export default function ChannelPage() {
|
|||||||
|
|
||||||
const fetchData = async (page, rowsPerPage, keyword, order, orderBy) => {
|
const fetchData = async (page, rowsPerPage, keyword, order, orderBy) => {
|
||||||
setSearching(true);
|
setSearching(true);
|
||||||
try {
|
const data = await fetchChannelData(page, rowsPerPage, keyword, order, orderBy);
|
||||||
if (orderBy) {
|
|
||||||
orderBy = order === 'desc' ? '-' + orderBy : orderBy;
|
if (data) {
|
||||||
}
|
|
||||||
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);
|
setListCount(data.total_count);
|
||||||
setChannels(data.data);
|
setChannels(data.data);
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
}
|
||||||
setSearching(false);
|
setSearching(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchGroups = async () => {
|
||||||
|
try {
|
||||||
|
let res = await API.get(`/api/group/`);
|
||||||
|
setGroupOptions(res.data.data);
|
||||||
|
} catch (error) {
|
||||||
|
showError(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData(page, rowsPerPage, searchKeyword, order, orderBy);
|
fetchData(page, rowsPerPage, searchKeyword, order, orderBy);
|
||||||
}, [page, rowsPerPage, searchKeyword, order, orderBy, refreshFlag]);
|
}, [page, rowsPerPage, searchKeyword, order, orderBy, refreshFlag]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchGroups().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
|
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
|
||||||
<Typography variant="h4">渠道</Typography>
|
<Typography variant="h4">渠道</Typography>
|
||||||
|
|
||||||
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
|
<ButtonGroup variant="contained" aria-label="outlined small primary button group">
|
||||||
|
<Button color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
|
||||||
新建渠道
|
新建渠道
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
<Button color="primary" startIcon={<IconMenu2 />} onClick={() => setOpenBatchModal(true)}>
|
||||||
<Stack mb={5}>
|
批量处理
|
||||||
<Alert severity="info">
|
</Button>
|
||||||
当前版本测试是通过按照 OpenAI API 格式使用 gpt-3.5-turbo
|
</ButtonGroup>
|
||||||
模型进行非流式请求实现的,因此测试报错并不一定代表通道不可用,该功能后续会修复。 另外,OpenAI 渠道已经不再支持通过 key
|
|
||||||
获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。
|
|
||||||
</Alert>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<Card>
|
<Card>
|
||||||
<Box component="form" onSubmit={searchChannels} noValidate>
|
<Box component="form" noValidate>
|
||||||
<TableToolBar placeholder={'搜索渠道的 ID,名称和密钥 ...'} />
|
<TableToolBar filterName={toolBarValue} handleFilterName={handleToolBarValue} groupOptions={groupOptions} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Toolbar
|
<Toolbar
|
||||||
sx={{
|
sx={{
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
@ -246,7 +289,10 @@ export default function ChannelPage() {
|
|||||||
{matchUpMd ? (
|
{matchUpMd ? (
|
||||||
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
|
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
|
||||||
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
|
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
|
||||||
刷新
|
刷新/清除搜索条件
|
||||||
|
</Button>
|
||||||
|
<Button onClick={searchChannels} startIcon={<IconSearch width={'18px'} />}>
|
||||||
|
搜索
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={testAllChannels} startIcon={<IconBrandSpeedtest width={'18px'} />}>
|
<Button onClick={testAllChannels} startIcon={<IconBrandSpeedtest width={'18px'} />}>
|
||||||
测试启用渠道
|
测试启用渠道
|
||||||
@ -269,6 +315,9 @@ export default function ChannelPage() {
|
|||||||
<IconButton onClick={handleRefresh} size="large">
|
<IconButton onClick={handleRefresh} size="large">
|
||||||
<IconRefresh />
|
<IconRefresh />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<IconButton onClick={searchChannels} size="large">
|
||||||
|
<IconSearch />
|
||||||
|
</IconButton>
|
||||||
<IconButton onClick={testAllChannels} size="large">
|
<IconButton onClick={testAllChannels} size="large">
|
||||||
<IconBrandSpeedtest />
|
<IconBrandSpeedtest />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -291,6 +340,7 @@ export default function ChannelPage() {
|
|||||||
orderBy={orderBy}
|
orderBy={orderBy}
|
||||||
onRequestSort={handleSort}
|
onRequestSort={handleSort}
|
||||||
headLabel={[
|
headLabel={[
|
||||||
|
{ id: 'collapse', label: '', disableSort: true },
|
||||||
{ id: 'id', label: 'ID', disableSort: false },
|
{ id: 'id', label: 'ID', disableSort: false },
|
||||||
{ id: 'name', label: '名称', disableSort: false },
|
{ id: 'name', label: '名称', disableSort: false },
|
||||||
{ id: 'group', label: '分组', disableSort: true },
|
{ id: 'group', label: '分组', disableSort: true },
|
||||||
@ -328,7 +378,8 @@ export default function ChannelPage() {
|
|||||||
showLastButton
|
showLastButton
|
||||||
/>
|
/>
|
||||||
</Card>
|
</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