diff --git a/controller/channel.go b/controller/channel.go index c05909eb..3c7402d3 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -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": "更新成功", + }) +} diff --git a/model/channel.go b/model/channel.go index 548a8c96..608e67ad 100644 --- a/model/channel.go +++ b/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 diff --git a/model/common.go b/model/common.go index 84e1e422..018a8642 100644 --- a/model/common.go +++ b/model/common.go @@ -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 +} diff --git a/model/log.go b/model/log.go index a3cc46ad..988a2d59 100644 --- a/model/log.go +++ b/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 -} diff --git a/router/api-router.go b/router/api-router.go index f1bd65f2..a5ea2e5b 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -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) } diff --git a/web/src/ui-component/AdminContainer.js b/web/src/ui-component/AdminContainer.js index eff42a22..85a131e8 100644 --- a/web/src/ui-component/AdminContainer.js +++ b/web/src/ui-component/AdminContainer.js @@ -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; diff --git a/web/src/views/Channel/component/BatchAzureAPI.js b/web/src/views/Channel/component/BatchAzureAPI.js new file mode 100644 index 00000000..d5dbed94 --- /dev/null +++ b/web/src/views/Channel/component/BatchAzureAPI.js @@ -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 ( + + + { + setValue(e.target.value); + }} + InputProps={{ + endAdornment: ( + + + + + + ) + }} + /> + + {data.length === 0 ? ( + + 暂无数据 + + ) : ( + <> + + + + + {data.map((item) => ( + handleSelect(item.id)} />} + label={item.name + '(' + item.other + ')'} + /> + ))} + + + { + setReplaceValue(e.target.value); + }} + InputProps={{ + endAdornment: ( + + + + + + ) + }} + /> + + + )} + + ); +}; + +export default BatchAzureAPI; diff --git a/web/src/views/Channel/component/BatchDelModel.js b/web/src/views/Channel/component/BatchDelModel.js new file mode 100644 index 00000000..9d57f185 --- /dev/null +++ b/web/src/views/Channel/component/BatchDelModel.js @@ -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 ( + + + 如果渠道只有一个模型的,将不会显示,请手动去列表删除渠道 + + + { + setValue(e.target.value); + }} + InputProps={{ + endAdornment: ( + + + + + + ) + }} + /> + + {data.length === 0 ? ( + + 暂无数据 + + ) : ( + <> + + + + + {data.map((item) => ( + handleSelect(item.id)} />} + label={item.name} + /> + ))} + + + + + + )} + + ); +}; + +export default BatchDelModel; diff --git a/web/src/views/Channel/component/BatchModal.js b/web/src/views/Channel/component/BatchModal.js new file mode 100644 index 00000000..43ac6a1c --- /dev/null +++ b/web/src/views/Channel/component/BatchModal.js @@ -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 ( + + ); +} + +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 ( + setOpen(!open)} fullWidth maxWidth={'md'}> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default BatchModal; + +BatchModal.propTypes = { + open: PropTypes.bool, + setOpen: PropTypes.func +}; diff --git a/web/src/views/Channel/component/EditModal.js b/web/src/views/Channel/component/EditModal.js index 379746df..a1fb429c 100644 --- a/web/src/views/Channel/component/EditModal.js +++ b/web/src/views/Channel/component/EditModal.js @@ -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 }; diff --git a/web/src/views/Channel/component/TableRow.js b/web/src/views/Channel/component/TableRow.js index 72b62c0f..b5765ea6 100644 --- a/web/src/views/Channel/component/TableRow.js +++ b/web/src/views/Channel/component/TableRow.js @@ -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 ( <> + + setOpenRow(!openRow)}> + {openRow ? : } + + + {item.id} - - - + {item.name} @@ -126,7 +141,6 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal, )} - @@ -196,6 +210,81 @@ export default function ChannelTableRow({ item, manageChannel, handleOpenModal, + + + + + + + + 可用模型: + + {modelMap.map((model) => ( + + ))} + + + {item.test_model && ( + + + + 测速模型: + + + + + )} + {item.proxy && ( + + + + 代理地址: + + {item.proxy} + + + )} + {item.other && ( + + + + 其他参数: + + + + + )} + + + + 删除通道 diff --git a/web/src/views/Channel/component/TableToolBar.js b/web/src/views/Channel/component/TableToolBar.js new file mode 100644 index 00000000..7808d784 --- /dev/null +++ b/web/src/views/Channel/component/TableToolBar.js @@ -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 ( + <> + + + 渠道名称 + + + + } + /> + + + 模型名称 + + + + } + /> + + + 测试模型 + + + + } + /> + + + key + + + + } + /> + + + 其他参数 + + + + } + /> + + + + + + 渠道类型 + + + + 状态 + + + + + 分组 + + + + + ); +} + +TableToolBar.propTypes = { + filterName: PropTypes.object, + handleFilterName: PropTypes.func, + groupOptions: PropTypes.array +}; diff --git a/web/src/views/Channel/index.js b/web/src/views/Channel/index.js index 631595fd..dedce917 100644 --- a/web/src/views/Channel/index.js +++ b/web/src/views/Channel/index.js @@ -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 ( <> 渠道 - - - - - 当前版本测试是通过按照 OpenAI API 格式使用 gpt-3.5-turbo - 模型进行非流式请求实现的,因此测试报错并不一定代表通道不可用,该功能后续会修复。 另外,OpenAI 渠道已经不再支持通过 key - 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。 - + + + + - - + + + +