CaIon 2 лет назад
Родитель
Сommit
7dc8b0ea93

+ 1 - 1
controller/channel-billing.go

@@ -292,7 +292,7 @@ func UpdateChannelBalance(c *gin.Context) {
 }
 
 func updateAllChannelsBalance() error {
-	channels, err := model.GetAllChannels(0, 0, true)
+	channels, err := model.GetAllChannels(0, 0, true, false)
 	if err != nil {
 		return err
 	}

+ 1 - 1
controller/channel-test.go

@@ -163,7 +163,7 @@ func testAllChannels(notify bool) error {
 	}
 	testAllChannelsRunning = true
 	testAllChannelsLock.Unlock()
-	channels, err := model.GetAllChannels(0, 0, true)
+	channels, err := model.GetAllChannels(0, 0, true, false)
 	if err != nil {
 		return err
 	}

+ 5 - 2
controller/channel.go

@@ -18,7 +18,8 @@ func GetAllChannels(c *gin.Context) {
 	if pageSize < 0 {
 		pageSize = common.ItemsPerPage
 	}
-	channels, err := model.GetAllChannels(p*pageSize, pageSize, false)
+	idSort, _ := strconv.ParseBool(c.Query("id_sort"))
+	channels, err := model.GetAllChannels(p*pageSize, pageSize, false, idSort)
 	if err != nil {
 		c.JSON(http.StatusOK, gin.H{
 			"success": false,
@@ -36,7 +37,9 @@ func GetAllChannels(c *gin.Context) {
 
 func SearchChannels(c *gin.Context) {
 	keyword := c.Query("keyword")
-	channels, err := model.SearchChannels(keyword)
+	group := c.Query("group")
+	//idSort, _ := strconv.ParseBool(c.Query("id_sort"))
+	channels, err := model.SearchChannels(keyword, group)
 	if err != nil {
 		c.JSON(http.StatusOK, gin.H{
 			"success": false,

+ 17 - 5
model/channel.go

@@ -28,23 +28,35 @@ type Channel struct {
 	AutoBan            *int    `json:"auto_ban" gorm:"default:1"`
 }
 
-func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
+func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool) ([]*Channel, error) {
 	var channels []*Channel
 	var err error
+	order := "priority desc"
+	if idSort {
+		order = "id desc"
+	}
 	if selectAll {
-		err = DB.Order("priority desc").Find(&channels).Error
+		err = DB.Order(order).Find(&channels).Error
 	} else {
-		err = DB.Order("priority desc").Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error
+		err = DB.Order(order).Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error
 	}
 	return channels, err
 }
 
-func SearchChannels(keyword string) (channels []*Channel, err error) {
+func SearchChannels(keyword string, group string) (channels []*Channel, err error) {
 	keyCol := "`key`"
 	if common.UsingPostgreSQL {
 		keyCol = `"key"`
 	}
-	err = DB.Omit("key").Where("id = ? or name LIKE ? or "+keyCol+" = ?", common.String2Int(keyword), keyword+"%", keyword).Find(&channels).Error
+	if group != "" {
+		groupCol := "`group`"
+		if common.UsingPostgreSQL {
+			groupCol = `"group"`
+		}
+		err = DB.Omit("key").Where("(id = ? or name LIKE ? or "+keyCol+" = ?) and "+groupCol+" LIKE ?", common.String2Int(keyword), keyword+"%", keyword, "%"+group+"%").Find(&channels).Error
+	} else {
+		err = DB.Omit("key").Where("id = ? or name LIKE ? or "+keyCol+" = ?", common.String2Int(keyword), keyword+"%", keyword).Find(&channels).Error
+	}
 	return channels, err
 }
 

+ 606 - 556
web/src/components/ChannelsTable.js

@@ -1,583 +1,633 @@
-import React, { useEffect, useState } from 'react';
-import { Button, Form, Input, Label, Message, Pagination, Popup, Table } from 'semantic-ui-react';
-import { Link } from 'react-router-dom';
-import { API, setPromptShown, shouldShowPrompt, showError, showInfo, showSuccess, timestamp2string } from '../helpers';
+import React, {useEffect, useState} from 'react';
+import {Input, Label, Message, Popup} from 'semantic-ui-react';
+import {Link} from 'react-router-dom';
+import {API, setPromptShown, shouldShowPrompt, showError, showInfo, showSuccess, timestamp2string} from '../helpers';
 
-import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
-import {renderGroup, renderNumber, renderQuota} from '../helpers/render';
+import {CHANNEL_OPTIONS, ITEMS_PER_PAGE} from '../constants';
+import {renderGroup, renderNumber, renderQuota, renderQuotaWithPrompt} from '../helpers/render';
+import {
+    Avatar,
+    Tag,
+    Table,
+    Button,
+    Popover,
+    Form,
+    Modal,
+    Popconfirm,
+    Space,
+    Tooltip,
+    Switch,
+    Typography, InputNumber
+} from "@douyinfe/semi-ui";
+import EditChannel from "../pages/Channel/EditChannel";
 
 function renderTimestamp(timestamp) {
-  return (
-    <>
-      {timestamp2string(timestamp)}
-    </>
-  );
+    return (
+        <>
+            {timestamp2string(timestamp)}
+        </>
+    );
 }
 
 let type2label = undefined;
 
 function renderType(type) {
-  if (!type2label) {
-    type2label = new Map;
-    for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
-      type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
+    if (!type2label) {
+        type2label = new Map;
+        for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
+            type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
+        }
+        type2label[0] = {value: 0, text: '未知类型', color: 'grey'};
     }
-    type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
-  }
-  return <Label basic color={type2label[type]?.color}>{type2label[type]?.text}</Label>;
+    return <Tag size='large' color={type2label[type]?.color}>{type2label[type]?.text}</Tag>;
 }
 
 function renderBalance(type, balance) {
-  switch (type) {
-    case 1: // OpenAI
-      return <span>${balance.toFixed(2)}</span>;
-    case 4: // CloseAI
-      return <span>¥{balance.toFixed(2)}</span>;
-    case 8: // 自定义
-      return <span>${balance.toFixed(2)}</span>;
-    case 5: // OpenAI-SB
-      return <span>¥{(balance / 10000).toFixed(2)}</span>;
-    case 10: // AI Proxy
-      return <span>{renderNumber(balance)}</span>;
-    case 12: // API2GPT
-      return <span>¥{balance.toFixed(2)}</span>;
-    case 13: // AIGC2D
-      return <span>{renderNumber(balance)}</span>;
-    default:
-      return <span>不支持</span>;
-  }
+    switch (type) {
+        case 1: // OpenAI
+            return <span>${balance.toFixed(2)}</span>;
+        case 4: // CloseAI
+            return <span>¥{balance.toFixed(2)}</span>;
+        case 8: // 自定义
+            return <span>${balance.toFixed(2)}</span>;
+        case 5: // OpenAI-SB
+            return <span>¥{(balance / 10000).toFixed(2)}</span>;
+        case 10: // AI Proxy
+            return <span>{renderNumber(balance)}</span>;
+        case 12: // API2GPT
+            return <span>¥{balance.toFixed(2)}</span>;
+        case 13: // AIGC2D
+            return <span>{renderNumber(balance)}</span>;
+        default:
+            return <span>不支持</span>;
+    }
 }
 
 const ChannelsTable = () => {
-  const [channels, setChannels] = useState([]);
-  const [loading, setLoading] = useState(true);
-  const [activePage, setActivePage] = useState(1);
-  const [searchKeyword, setSearchKeyword] = useState('');
-  const [searching, setSearching] = useState(false);
-  const [updatingBalance, setUpdatingBalance] = useState(false);
-  const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
-  const [showPrompt, setShowPrompt] = useState(shouldShowPrompt("channel-test"));
-
-  const loadChannels = async (startIdx) => {
-    const res = await API.get(`/api/channel/?p=${startIdx}&page_size=${pageSize}`);
-    const { success, message, data } = res.data;
-    if (success) {
-      if (startIdx === 0) {
-        setChannels(data);
-      } else {
-        let newChannels = [...channels];
-        newChannels.splice(startIdx * pageSize, data.length, ...data);
-        setChannels(newChannels);
-      }
-    } else {
-      showError(message);
-    }
-    setLoading(false);
-  };
-
-  const onPaginationChange = (e, { activePage }) => {
-    (async () => {
-      if (activePage === Math.ceil(channels.length / pageSize) + 1) {
-        // In this case we have to load more data and then append them.
-        await loadChannels(activePage - 1, pageSize);
-      }
-      setActivePage(activePage);
-    })();
-  };
-
-  const setItemsPerPage = (e) => {
-    console.log(e.target.value);
-    //parseInt(e.target.value);
-    setPageSize(parseInt(e.target.value));
-    loadChannels(0);
-  }
-
-  const refresh = async () => {
-    setLoading(true);
-    await loadChannels(activePage - 1);
-  };
-
-  useEffect(() => {
-    loadChannels(0)
-      .then()
-      .catch((reason) => {
-        showError(reason);
-      });
-  }, []);
-
-  const manageChannel = async (id, action, idx, value) => {
-    let data = { id };
-    let res;
-    switch (action) {
-      case 'delete':
-        res = await API.delete(`/api/channel/${id}/`);
-        break;
-      case 'enable':
-        data.status = 1;
-        res = await API.put('/api/channel/', data);
-        break;
-      case 'disable':
-        data.status = 2;
-        res = await API.put('/api/channel/', data);
-        break;
-      case 'priority':
-        if (value === '') {
-          return;
+    const columns = [
+        {
+            title: 'ID',
+            dataIndex: 'id',
+        },
+        {
+            title: '名称',
+            dataIndex: 'name',
+        },
+        {
+            title: '分组',
+            dataIndex: 'group',
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        <Space spacing={2}>
+                            {
+                                text.split(',').map((item, index) => {
+                                    return (renderGroup(item))
+                                })
+                            }
+                        </Space>
+                    </div>
+                );
+            },
+        },
+        {
+            title: '类型',
+            dataIndex: 'type',
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        {renderType(text)}
+                    </div>
+                );
+            },
+        },
+        {
+            title: '状态',
+            dataIndex: 'status',
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        {renderStatus(text)}
+                    </div>
+                );
+            },
+        },
+        {
+            title: '响应时间',
+            dataIndex: 'response_time',
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        {renderResponseTime(text)}
+                    </div>
+                );
+            },
+        },
+        {
+            title: '已用/剩余',
+            dataIndex: 'expired_time',
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        <Space spacing={1}>
+                            <Tooltip content={'已用额度'}>
+                                <Tag color='white' type='ghost' size='large'>{renderQuota(record.used_quota)}</Tag>
+                            </Tooltip>
+                            <Tooltip content={'剩余额度,点击更新'}>
+                                <Tag color='white' type='ghost' size='large' onClick={() => {updateChannelBalance(record)}}>{renderQuota(record.balance)}</Tag>
+                            </Tooltip>
+                        </Space>
+                    </div>
+                );
+            },
+        },
+        {
+            title: '优先级',
+            dataIndex: 'priority',
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        <InputNumber
+                            style={{width: 70}}
+                            name='name'
+                            onChange={value => {
+                                manageChannel(record.id, 'priority', record, value);
+                            }}
+                            defaultValue={record.priority}
+                            min={0}
+                        />
+                    </div>
+                );
+            },
+        },
+        {
+            title: '',
+            dataIndex: 'operate',
+            render: (text, record, index) => (
+                <div>
+                    <Popconfirm
+                        title="确定是否要删除此渠道?"
+                        content="此修改将不可逆"
+                        okType={'danger'}
+                        position={'left'}
+                        onConfirm={() => {
+                            manageChannel(record.id, 'delete', record).then(
+                                () => {
+                                    removeRecord(record.id);
+                                }
+                            )
+                        }}
+                    >
+                        <Button theme='light' type='danger' style={{marginRight: 1}}>删除</Button>
+                    </Popconfirm>
+                    {
+                        record.status === 1 ?
+                            <Button theme='light' type='warning' style={{marginRight: 1}} onClick={
+                                async () => {
+                                    manageChannel(
+                                        record.id,
+                                        'disable',
+                                        record
+                                    )
+                                }
+                            }>禁用</Button> :
+                            <Button theme='light' type='secondary' style={{marginRight: 1}} onClick={
+                                async () => {
+                                    manageChannel(
+                                        record.id,
+                                        'enable',
+                                        record
+                                    );
+                                }
+                            }>启用</Button>
+                    }
+                    <Button theme='light' type='tertiary' style={{marginRight: 1}} onClick={
+                        () => {
+                            setEditingChannel(record);
+                            setShowEdit(true);
+                        }
+                    }>编辑</Button>
+                </div>
+            ),
+        },
+    ];
+
+    const [channels, setChannels] = useState([]);
+    const [loading, setLoading] = useState(true);
+    const [activePage, setActivePage] = useState(1);
+    const [idSort, setIdSort] = useState(false);
+    const [searchKeyword, setSearchKeyword] = useState('');
+    const [searchGroup, setSearchGroup] = useState('');
+    const [searching, setSearching] = useState(false);
+    const [updatingBalance, setUpdatingBalance] = useState(false);
+    const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
+    const [showPrompt, setShowPrompt] = useState(shouldShowPrompt("channel-test"));
+    const [channelCount, setChannelCount] = useState(pageSize);
+    const [groupOptions, setGroupOptions] = useState([]);
+    const [showEdit, setShowEdit] = useState(false);
+    const [editingChannel, setEditingChannel] = useState({
+        id: undefined,
+    });
+
+    const removeRecord = id => {
+        let newDataSource = [...channels];
+        if (id != null) {
+            let idx = newDataSource.findIndex(data => data.id === id);
+
+            if (idx > -1) {
+                newDataSource.splice(idx, 1);
+                setChannels(newDataSource);
+            }
         }
-        data.priority = parseInt(value);
-        res = await API.put('/api/channel/', data);
-        break;
-      case 'weight':
-        if (value === '') {
-          return;
+    };
+
+    const setChannelFormat = (channels) => {
+        for (let i = 0; i < channels.length; i++) {
+            channels[i].key = '' + channels[i].id;
         }
-        data.weight = parseInt(value);
-        if (data.weight < 0) {
-          data.weight = 0;
+        // data.key = '' + data.id
+        setChannels(channels);
+        if (channels.length >= pageSize) {
+            setChannelCount(channels.length + pageSize);
+        } else {
+            setChannelCount(channels.length);
         }
-        res = await API.put('/api/channel/', data);
-        break;
-    }
-    const { success, message } = res.data;
-    if (success) {
-      showSuccess('操作成功完成!');
-      let channel = res.data.data;
-      let newChannels = [...channels];
-      let realIdx = (activePage - 1) * pageSize + idx;
-      if (action === 'delete') {
-        newChannels[realIdx].deleted = true;
-      } else {
-        newChannels[realIdx].status = channel.status;
-      }
-      setChannels(newChannels);
-    } else {
-      showError(message);
     }
-  };
-
-  const renderStatus = (status) => {
-    switch (status) {
-      case 1:
-        return <Label basic color='green'>已启用</Label>;
-      case 2:
-        return (
-          <Popup
-            trigger={<Label basic color='red'>
-              已禁用
-            </Label>}
-            content='本渠道被手动禁用'
-            basic
-          />
-        );
-      case 3:
-        return (
-          <Popup
-            trigger={<Label basic color='yellow'>
-              已禁用
-            </Label>}
-            content='本渠道被程序自动禁用'
-            basic
-          />
-        );
-      default:
-        return (
-          <Label basic color='grey'>
-            未知状态
-          </Label>
-        );
-    }
-  };
-
-  const renderResponseTime = (responseTime) => {
-    let time = responseTime / 1000;
-    time = time.toFixed(2) + ' 秒';
-    if (responseTime === 0) {
-      return <Label basic color='grey'>未测试</Label>;
-    } else if (responseTime <= 1000) {
-      return <Label basic color='green'>{time}</Label>;
-    } else if (responseTime <= 3000) {
-      return <Label basic color='olive'>{time}</Label>;
-    } else if (responseTime <= 5000) {
-      return <Label basic color='yellow'>{time}</Label>;
-    } else {
-      return <Label basic color='red'>{time}</Label>;
-    }
-  };
-
-  const searchChannels = async () => {
-    if (searchKeyword === '') {
-      // if keyword is blank, load files instead.
-      await loadChannels(0);
-      setActivePage(1);
-      return;
-    }
-    setSearching(true);
-    const res = await API.get(`/api/channel/search?keyword=${searchKeyword}`);
-    const { success, message, data } = res.data;
-    if (success) {
-      setChannels(data);
-      setActivePage(1);
-    } else {
-      showError(message);
-    }
-    setSearching(false);
-  };
-
-  const testChannel = async (id, name, idx) => {
-    const res = await API.get(`/api/channel/test/${id}/`);
-    const { success, message, time } = res.data;
-    if (success) {
-      let newChannels = [...channels];
-      let realIdx = (activePage - 1) * pageSize + idx;
-      newChannels[realIdx].response_time = time * 1000;
-      newChannels[realIdx].test_time = Date.now() / 1000;
-      setChannels(newChannels);
-      showInfo(`通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
-    } else {
-      showError(message);
-    }
-  };
-
-  const testAllChannels = async () => {
-    const res = await API.get(`/api/channel/test`);
-    const { success, message } = res.data;
-    if (success) {
-      showInfo('已成功开始测试所有已启用通道,请刷新页面查看结果。');
-    } else {
-      showError(message);
-    }
-  };
-
-  const deleteAllDisabledChannels = async () => {
-    const res = await API.delete(`/api/channel/disabled`);
-    const { success, message, data } = res.data;
-    if (success) {
-      showSuccess(`已删除所有禁用渠道,共计 ${data} 个`);
-      await refresh();
-    } else {
-      showError(message);
-    }
-  };
-
-  const updateChannelBalance = async (id, name, idx) => {
-    const res = await API.get(`/api/channel/update_balance/${id}/`);
-    const { success, message, balance } = res.data;
-    if (success) {
-      let newChannels = [...channels];
-      let realIdx = (activePage - 1) * pageSize + idx;
-      newChannels[realIdx].balance = balance;
-      newChannels[realIdx].balance_updated_time = Date.now() / 1000;
-      setChannels(newChannels);
-      showInfo(`通道 ${name} 余额更新成功!`);
-    } else {
-      showError(message);
-    }
-  };
-
-  const updateAllChannelsBalance = async () => {
-    setUpdatingBalance(true);
-    const res = await API.get(`/api/channel/update_balance`);
-    const { success, message } = res.data;
-    if (success) {
-      showInfo('已更新完毕所有已启用通道余额!');
-    } else {
-      showError(message);
-    }
-    setUpdatingBalance(false);
-  };
-
-  const handleKeywordChange = async (e, { value }) => {
-    setSearchKeyword(value.trim());
-  };
-
-  const sortChannel = (key) => {
-    if (channels.length === 0) return;
-    setLoading(true);
-    let sortedChannels = [...channels];
-    if (typeof sortedChannels[0][key] === 'string') {
-      sortedChannels.sort((a, b) => {
-        return ('' + a[key]).localeCompare(b[key]);
-      });
-    } else {
-      sortedChannels.sort((a, b) => {
-        if (a[key] === b[key]) return 0;
-        if (a[key] > b[key]) return -1;
-        if (a[key] < b[key]) return 1;
-      });
-    }
-    if (sortedChannels[0].id === channels[0].id) {
-      sortedChannels.reverse();
-    }
-    setChannels(sortedChannels);
-    setLoading(false);
-  };
-
-  return (
-    <>
-      <Form onSubmit={searchChannels}>
-        <Form.Input
-          icon='search'
-          fluid
-          iconPosition='left'
-          placeholder='搜索渠道的 ID,名称和密钥 ...'
-          value={searchKeyword}
-          loading={searching}
-          onChange={handleKeywordChange}
-        />
-      </Form>
-      {
-        showPrompt && (
-          <Message onDismiss={() => {
-            setShowPrompt(false);
-            setPromptShown("channel-test");
-          }}>
-            当前版本测试是通过按照 OpenAI API 格式使用 gpt-3.5-turbo
-            模型进行非流式请求实现的,因此测试报错并不一定代表通道不可用,该功能后续会修复。
-
-            另外,OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。
-          </Message>
-        )
-      }
-      <Table basic compact size='small'>
-        <Table.Header>
-          <Table.Row>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortChannel('id');
-              }}
-            >
-              ID
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortChannel('name');
-              }}
-            >
-              名称
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortChannel('group');
-              }}
-              width={1}
-            >
-              分组
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortChannel('type');
-              }}
-              width={2}
-            >
-              类型
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortChannel('status');
-              }}
-              width={2}
-            >
-              状态
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortChannel('response_time');
-              }}
-            >
-              响应时间
-            </Table.HeaderCell>
-            <Table.HeaderCell
-                style={{ cursor: 'pointer' }}
-                onClick={() => {
-                  sortChannel('used_quota');
-                }}
-                width={1}
-            >
-              已使用
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortChannel('balance');
-              }}
-            >
-              余额
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortChannel('priority');
-              }}
-            >
-              优先级
-            </Table.HeaderCell>
-            <Table.HeaderCell>操作</Table.HeaderCell>
-          </Table.Row>
-        </Table.Header>
-
-        <Table.Body>
-          {channels
-            .slice(
-              (activePage - 1) * pageSize,
-              activePage * pageSize
-            )
-            .map((channel, idx) => {
-              if (channel.deleted) return <></>;
-              return (
-                <Table.Row key={channel.id}>
-                  <Table.Cell>{channel.id}</Table.Cell>
-                  <Table.Cell>{channel.name ? channel.name : '无'}</Table.Cell>
-                  <Table.Cell>{renderGroup(channel.group)}</Table.Cell>
-                  <Table.Cell>{renderType(channel.type)}</Table.Cell>
-                  <Table.Cell>{renderStatus(channel.status)}</Table.Cell>
-                  <Table.Cell>
-                    <Popup
-                      content={channel.test_time ? renderTimestamp(channel.test_time) : '未测试'}
-                      key={channel.id}
-                      trigger={renderResponseTime(channel.response_time)}
-                      basic
-                    />
-                  </Table.Cell>
-                  <Table.Cell>{renderQuota(channel.used_quota)}</Table.Cell>
-                  <Table.Cell>
+
+    const loadChannels = async (startIdx) => {
+        setLoading(true);
+        const res = await API.get(`/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}`);
+        const {success, message, data} = res.data;
+        if (success) {
+            if (startIdx === 0) {
+                setChannelFormat(data);
+            } else {
+                let newChannels = [...channels];
+                newChannels.splice(startIdx * pageSize, data.length, ...data);
+                setChannelFormat(newChannels);
+            }
+        } else {
+            showError(message);
+        }
+        setLoading(false);
+    };
+
+    useEffect(() => {
+        loadChannels(0)
+            .then()
+            .catch((reason) => {
+                showError(reason);
+            });
+    }, [pageSize]);
+
+    const refresh = async () => {
+        await loadChannels(activePage - 1);
+    };
+
+    useEffect(() => {
+        loadChannels(0)
+            .then()
+            .catch((reason) => {
+                showError(reason);
+            });
+        fetchGroups().then();
+        console.log(localStorage.getItem('id-sort'))
+        if (localStorage.getItem('id-sort') === 'true') {
+            setIdSort(true)
+        }
+    }, []);
+
+    useEffect(() => {
+        searchChannels()
+    }, [searchGroup]);
+
+    useEffect(() => {
+        refresh()
+        localStorage.setItem('id-sort', idSort + '');
+    }, [idSort]);
+
+    const manageChannel = async (id, action, record, value) => {
+        let data = {id};
+        let res;
+        switch (action) {
+            case 'delete':
+                res = await API.delete(`/api/channel/${id}/`);
+                break;
+            case 'enable':
+                data.status = 1;
+                res = await API.put('/api/channel/', data);
+                break;
+            case 'disable':
+                data.status = 2;
+                res = await API.put('/api/channel/', data);
+                break;
+            case 'priority':
+                if (value === '') {
+                    return;
+                }
+                data.priority = parseInt(value);
+                res = await API.put('/api/channel/', data);
+                break;
+            case 'weight':
+                if (value === '') {
+                    return;
+                }
+                data.weight = parseInt(value);
+                if (data.weight < 0) {
+                    data.weight = 0;
+                }
+                res = await API.put('/api/channel/', data);
+                break;
+        }
+        const {success, message} = res.data;
+        if (success) {
+            showSuccess('操作成功完成!');
+            let channel = res.data.data;
+            let newChannels = [...channels];
+            if (action === 'delete') {
+
+            } else {
+                record.status = channel.status;
+            }
+            setChannels(newChannels);
+        } else {
+            showError(message);
+        }
+    };
+
+    const renderStatus = (status) => {
+        switch (status) {
+            case 1:
+                return <Tag size='large' color='green'>已启用</Tag>;
+            case 2:
+                return (
                     <Popup
-                      trigger={<span onClick={() => {
-                        updateChannelBalance(channel.id, channel.name, idx);
-                      }} style={{ cursor: 'pointer' }}>
-                      {renderBalance(channel.type, channel.balance)}
-                    </span>}
-                      content='点击更新'
-                      basic
+                        trigger={<Tag size='large' color='red'>
+                            已禁用
+                        </Tag>}
+                        content='本渠道被手动禁用'
+                        basic
                     />
-                  </Table.Cell>
-                  <Table.Cell>
+                );
+            case 3:
+                return (
                     <Popup
-                      trigger={<Input type='number' defaultValue={channel.priority} onBlur={(event) => {
-                        manageChannel(
-                          channel.id,
-                          'priority',
-                          idx,
-                          event.target.value
-                        );
-                      }}>
-                        <input style={{ maxWidth: '60px' }} />
-                      </Input>}
-                      content='渠道选择优先级,越高越优先'
-                      basic
+                        trigger={<Tag size='large' color='yellow'>
+                            已禁用
+                        </Tag>}
+                        content='本渠道被程序自动禁用'
+                        basic
                     />
-                  </Table.Cell>
-                  <Table.Cell>
-                    <div>
-                      <Button
-                        size={'small'}
-                        positive
-                        onClick={() => {
-                          testChannel(channel.id, channel.name, idx);
-                        }}
-                      >
-                        测试
-                      </Button>
-                      <Popup
-                        trigger={
-                          <Button size='small' negative>
-                            删除
-                          </Button>
-                        }
-                        on='click'
-                        flowing
-                        hoverable
-                      >
-                        <Button
-                          negative
-                          onClick={() => {
-                            manageChannel(channel.id, 'delete', idx);
-                          }}
-                        >
-                          删除渠道 {channel.name}
-                        </Button>
-                      </Popup>
-                      <Button
-                        size={'small'}
-                        onClick={() => {
-                          manageChannel(
-                            channel.id,
-                            channel.status === 1 ? 'disable' : 'enable',
-                            idx
-                          );
-                        }}
-                      >
-                        {channel.status === 1 ? '禁用' : '启用'}
-                      </Button>
-                      <Button
-                        size={'small'}
-                        as={Link}
-                        to={'/channel/edit/' + channel.id}
-                      >
-                        编辑
-                      </Button>
-                    </div>
-                  </Table.Cell>
-                </Table.Row>
-              );
-            })}
-        </Table.Body>
-
-        <Table.Footer>
-          <Table.Row>
-            <Table.HeaderCell colSpan='10'>
-              <Button size='small' as={Link} to='/channel/add' loading={loading}>
-                添加新的渠道
-              </Button>
-              <Button size='small' loading={loading} onClick={testAllChannels}>
-                测试所有已启用通道
-              </Button>
-              <Button size='small' onClick={updateAllChannelsBalance}
-                      loading={loading || updatingBalance}>更新所有已启用通道余额</Button>
-
-              <div style={{ float: 'right' }}>
-                <div className="ui labeled input" style={{marginRight: '10px'}}>
-                  <div className="ui label">每页数量</div>
-                  <Input type="number" style={{width: '70px'}} defaultValue={ITEMS_PER_PAGE} onBlur={setItemsPerPage}></Input>
+                );
+            default:
+                return (
+                    <Tag size='large' color='grey'>
+                        未知状态
+                    </Tag>
+                );
+        }
+    };
+
+    const renderResponseTime = (responseTime) => {
+        let time = responseTime / 1000;
+        time = time.toFixed(2) + ' 秒';
+        if (responseTime === 0) {
+            return <Tag size='large' color='grey'>未测试</Tag>;
+        } else if (responseTime <= 1000) {
+            return <Tag size='large' color='green'>{time}</Tag>;
+        } else if (responseTime <= 3000) {
+            return <Tag size='large' color='lime'>{time}</Tag>;
+        } else if (responseTime <= 5000) {
+            return <Tag size='large' color='yellow'>{time}</Tag>;
+        } else {
+            return <Tag size='large' color='red'>{time}</Tag>;
+        }
+    };
+
+    const searchChannels = async () => {
+        if (searchKeyword === '' && searchGroup === '') {
+            // if keyword is blank, load files instead.
+            await loadChannels(0);
+            setActivePage(1);
+            return;
+        }
+        setSearching(true);
+        const res = await API.get(`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}`);
+        const {success, message, data} = res.data;
+        if (success) {
+            setChannels(data);
+            setActivePage(1);
+        } else {
+            showError(message);
+        }
+        setSearching(false);
+    };
+
+    const testChannel = async (id, name, idx) => {
+        const res = await API.get(`/api/channel/test/${id}/`);
+        const {success, message, time} = res.data;
+        if (success) {
+            let newChannels = [...channels];
+            let realIdx = (activePage - 1) * pageSize + idx;
+            newChannels[realIdx].response_time = time * 1000;
+            newChannels[realIdx].test_time = Date.now() / 1000;
+            setChannels(newChannels);
+            showInfo(`通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
+        } else {
+            showError(message);
+        }
+    };
+
+    const testAllChannels = async () => {
+        const res = await API.get(`/api/channel/test`);
+        const {success, message} = res.data;
+        if (success) {
+            showInfo('已成功开始测试所有已启用通道,请刷新页面查看结果。');
+        } else {
+            showError(message);
+        }
+    };
+
+    const deleteAllDisabledChannels = async () => {
+        const res = await API.delete(`/api/channel/disabled`);
+        const {success, message, data} = res.data;
+        if (success) {
+            showSuccess(`已删除所有禁用渠道,共计 ${data} 个`);
+            await refresh();
+        } else {
+            showError(message);
+        }
+    };
+
+    const updateChannelBalance = async (record) => {
+        const res = await API.get(`/api/channel/update_balance/${record.id}/`);
+        const {success, message, balance} = res.data;
+        if (success) {
+            record.balance = balance;
+            record.balance_updated_time = Date.now() / 1000;
+            showInfo(`通道 ${record.name} 余额更新成功!`);
+        } else {
+            showError(message);
+        }
+    };
+
+    const updateAllChannelsBalance = async () => {
+        setUpdatingBalance(true);
+        const res = await API.get(`/api/channel/update_balance`);
+        const {success, message} = res.data;
+        if (success) {
+            showInfo('已更新完毕所有已启用通道余额!');
+        } else {
+            showError(message);
+        }
+        setUpdatingBalance(false);
+    };
+
+    const sortChannel = (key) => {
+        if (channels.length === 0) return;
+        setLoading(true);
+        let sortedChannels = [...channels];
+        if (typeof sortedChannels[0][key] === 'string') {
+            sortedChannels.sort((a, b) => {
+                return ('' + a[key]).localeCompare(b[key]);
+            });
+        } else {
+            sortedChannels.sort((a, b) => {
+                if (a[key] === b[key]) return 0;
+                if (a[key] > b[key]) return -1;
+                if (a[key] < b[key]) return 1;
+            });
+        }
+        if (sortedChannels[0].id === channels[0].id) {
+            sortedChannels.reverse();
+        }
+        setChannels(sortedChannels);
+        setLoading(false);
+    };
+
+    let pageData = channels.slice((activePage - 1) * pageSize, activePage * pageSize);
+
+    const handlePageChange = page => {
+        setActivePage(page);
+        if (page === Math.ceil(channels.length / pageSize) + 1) {
+            // In this case we have to load more data and then append them.
+            loadChannels(page - 1).then(r => {
+            });
+        }
+    };
+
+    const fetchGroups = async () => {
+        try {
+            let res = await API.get(`/api/group/`);
+            // add 'all' option
+            // res.data.data.unshift('all');
+            setGroupOptions(res.data.data.map((group) => ({
+                label: group,
+                value: group,
+            })));
+        } catch (error) {
+            showError(error.message);
+        }
+    };
+
+    const closeEdit = () => {
+        setShowEdit(false);
+    }
+
+    const handleRow = (record, index) => {
+        if (record.status !== 1) {
+            return {
+                style: {
+                    background: 'var(--semi-color-disabled-border)',
+                },
+            };
+        } else {
+            return {};
+        }
+    };
+
+    return (
+        <>
+            <EditChannel refresh={refresh} visible={showEdit} handleClose={closeEdit} editingChannel={editingChannel}/>
+            <Form onSubmit={searchChannels} labelPosition='left'>
+
+                <div style={{display: 'flex'}}>
+                    <Space>
+                        <Form.Input
+                            field='search'
+                            label='关键词'
+                            placeholder='ID,名称和密钥 ...'
+                            value={searchKeyword}
+                            loading={searching}
+                            onChange={(v)=>{
+                                setSearchKeyword(v.trim())
+                            }}
+                        />
+                        <Form.Select field="group" label='分组' optionList={groupOptions} onChange={(v) => {
+                            setSearchGroup(v)
+                        }}/>
+                    </Space>
                 </div>
-                <Pagination
-                    activePage={activePage}
-                    onPageChange={onPaginationChange}
-                    size='small'
-                    siblingRange={1}
-                    totalPages={
-                        Math.ceil(channels.length / pageSize) +
-                        (channels.length % pageSize === 0 ? 1 : 0)
-                    }
-                />
-              </div>
-              <Popup
-                trigger={
-                  <Button size='small' loading={loading}>
-                    删除禁用渠道
-                  </Button>
-                }
-                on='click'
-                flowing
-                hoverable
-              >
-                <Button size='small' loading={loading} negative onClick={deleteAllDisabledChannels}>
-                  确认删除
-                </Button>
-              </Popup>
-              <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
-
-            </Table.HeaderCell>
-          </Table.Row>
-        </Table.Footer>
-      </Table>
-    </>
-  );
+            </Form>
+            <div style={{marginTop: 10, display: 'flex'}}>
+                <Space>
+                    <Typography.Text strong>使用ID排序</Typography.Text>
+                    <Switch checked={idSort} label='使用ID排序' uncheckedText="关" aria-label="是否用ID排序" onChange={(v) => {
+                        setIdSort(v)
+                    }}></Switch>
+                </Space>
+            </div>
+
+            <Table columns={columns} dataSource={pageData} pagination={{
+                currentPage: activePage,
+                pageSize: pageSize,
+                total: channelCount,
+                pageSizeOpts: [10, 20, 50, 100],
+                showSizeChanger: true,
+                onPageSizeChange: (size) => {
+                    setPageSize(size)
+                    setActivePage(1)
+                },
+                onPageChange: handlePageChange,
+            }} loading={loading} onRow={handleRow}/>
+            <div style={{display: 'flex'}}>
+                <Space>
+                    <Button theme='light' type='primary' style={{marginRight: 8}} onClick={
+                        () => {
+                            setEditingChannel({
+                                id: undefined,
+                            });
+                            setShowEdit(true)
+                        }
+                    }>添加渠道</Button>
+                    <Popconfirm
+                        title="确定?"
+                        okType={'warning'}
+                        onConfirm={testAllChannels}
+                    >
+                        <Button theme='light' type='warning' style={{marginRight: 8}}>测试所有已启用通道</Button>
+                    </Popconfirm>
+                    <Popconfirm
+                        title="确定?"
+                        okType={'secondary'}
+                        onConfirm={updateAllChannelsBalance}
+                    >
+                        <Button theme='light' type='secondary' style={{marginRight: 8}}>更新所有已启用通道余额</Button>
+                    </Popconfirm>
+                    <Popconfirm
+                        title="确定是否要删除禁用通道?"
+                        content="此修改将不可逆"
+                        okType={'danger'}
+                        onConfirm={deleteAllDisabledChannels}
+                    >
+                        <Button theme='light' type='danger' style={{marginRight: 8}}>删除禁用通道</Button>
+                    </Popconfirm>
+
+                    <Button theme='light' type='primary' style={{marginRight: 8}} onClick={refresh}>刷新</Button>
+                </Space>
+            </div>
+        </>
+    );
 };
 
 export default ChannelsTable;

+ 19 - 8
web/src/components/RedemptionsTable.js

@@ -164,11 +164,24 @@ const RedemptionsTable = () => {
         setShowEdit(false);
     }
 
-    const setCount = (data) => {
-        if (data.length >= (activePage) * ITEMS_PER_PAGE) {
-            setTokenCount(data.length + 1);
+    // const setCount = (data) => {
+    //     if (data.length >= (activePage) * ITEMS_PER_PAGE) {
+    //         setTokenCount(data.length + 1);
+    //     } else {
+    //         setTokenCount(data.length);
+    //     }
+    // }
+
+    const setRedemptionFormat = (redeptions) => {
+        for (let i = 0; i < redeptions.length; i++) {
+            redeptions[i].key = '' + redeptions[i].id;
+        }
+        // data.key = '' + data.id
+        setRedemptions(redeptions);
+        if (redeptions.length >= (activePage) * ITEMS_PER_PAGE) {
+            setTokenCount(redeptions.length + 1);
         } else {
-            setTokenCount(data.length);
+            setTokenCount(redeptions.length);
         }
     }
 
@@ -177,13 +190,11 @@ const RedemptionsTable = () => {
         const {success, message, data} = res.data;
         if (success) {
             if (startIdx === 0) {
-                setRedemptions(data);
-                setCount(data);
+                setRedemptionFormat(data);
             } else {
                 let newRedemptions = redemptions;
                 newRedemptions.push(...data);
-                setRedemptions(newRedemptions);
-                setCount(newRedemptions);
+                setRedemptionFormat(newRedemptions);
             }
         } else {
             showError(message);

+ 15 - 25
web/src/constants/channel.constants.js

@@ -1,26 +1,16 @@
 export const CHANNEL_OPTIONS = [
-  { key: 1, text: 'OpenAI', value: 1, color: 'green' },
-  { key: 99, text: 'Midjourney-Proxy', value: 99, color: 'green' },
-  { key: 14, text: 'Anthropic Claude', value: 14, color: 'black' },
-  { key: 3, text: 'Azure OpenAI', value: 3, color: 'olive' },
-  { key: 11, text: 'Google PaLM2', value: 11, color: 'orange' },
-  { key: 15, text: '百度文心千帆', value: 15, color: 'blue' },
-  { key: 17, text: '阿里通义千问', value: 17, color: 'orange' },
-  { key: 18, text: '讯飞星火认知', value: 18, color: 'blue' },
-  { key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' },
-  { key: 19, text: '360 智脑', value: 19, color: 'blue' },
-  { key: 23, text: '腾讯混元', value: 23, color: 'teal' },
-  { key: 8, text: '自定义渠道', value: 8, color: 'pink' },
-  { key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' },
-  { key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' },
-  { key: 20, text: '代理:OpenRouter', value: 20, color: 'black' },
-  { key: 2, text: '代理:API2D', value: 2, color: 'blue' },
-  { key: 5, text: '代理:OpenAI-SB', value: 5, color: 'brown' },
-  { key: 7, text: '代理:OhMyGPT', value: 7, color: 'purple' },
-  { key: 10, text: '代理:AI Proxy', value: 10, color: 'purple' },
-  { key: 4, text: '代理:CloseAI', value: 4, color: 'teal' },
-  { key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet' },
-  { key: 9, text: '代理:AI.LS', value: 9, color: 'yellow' },
-  { key: 12, text: '代理:API2GPT', value: 12, color: 'blue' },
-  { key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' }
-];
+  { key: 1, text: 'OpenAI', value: 1, color: 'green', label: 'OpenAI' },
+  { key: 24, text: 'Midjourney Proxy', value: 24, color: 'light-blue', label: 'Midjourney Proxy' },
+  { key: 14, text: 'Anthropic Claude', value: 14, color: 'black', label: 'Anthropic Claude' },
+  { key: 3, text: 'Azure OpenAI', value: 3, color: 'olive', label: 'Azure OpenAI' },
+  { key: 11, text: 'Google PaLM2', value: 11, color: 'orange', label: 'Google PaLM2' },
+  { key: 15, text: '百度文心千帆', value: 15, color: 'blue', label: '百度文心千帆' },
+  { key: 17, text: '阿里通义千问', value: 17, color: 'orange', label: '阿里通义千问' },
+  { key: 18, text: '讯飞星火认知', value: 18, color: 'blue', label: '讯飞星火认知' },
+  { key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet', label: '智谱 ChatGLM' },
+  { key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑' },
+  { key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元' },
+  { key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道' },
+  { key: 22, text: '知识库:FastGPT', value: 22, color: 'blue', label: '知识库:FastGPT' },
+  { key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple', label: '知识库:AI Proxy' },
+];

+ 571 - 488
web/src/pages/Channel/EditChannel.js

@@ -1,513 +1,596 @@
-import React, { useEffect, useState } from 'react';
-import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react';
-import { useNavigate, useParams } from 'react-router-dom';
-import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers';
-import { CHANNEL_OPTIONS } from '../../constants';
+import React, {useEffect, useRef, useState} from 'react';
+import {useNavigate, useParams} from 'react-router-dom';
+import {API, isMobile, showError, showInfo, showSuccess, verifyJSON} from '../../helpers';
+import {CHANNEL_OPTIONS} from '../../constants';
+import Title from "@douyinfe/semi-ui/lib/es/typography/title";
+import {SideSheet, Space, Spin, Button, Input, Typography, Select, TextArea, Checkbox, Banner} from "@douyinfe/semi-ui";
 
 const MODEL_MAPPING_EXAMPLE = {
-  'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
-  'gpt-4-0314': 'gpt-4',
-  'gpt-4-32k-0314': 'gpt-4-32k'
+    'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
+    'gpt-4-0314': 'gpt-4',
+    'gpt-4-32k-0314': 'gpt-4-32k'
 };
 
 function type2secretPrompt(type) {
-  // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
-  switch (type) {
-    case 15:
-      return '按照如下格式输入:APIKey|SecretKey';
-    case 18:
-      return '按照如下格式输入:APPID|APISecret|APIKey';
-    case 22:
-      return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
-    case 23:
-      return '按照如下格式输入:AppId|SecretId|SecretKey';
-    default:
-      return '请输入渠道对应的鉴权密钥';
-  }
-}
-
-const EditChannel = () => {
-  const params = useParams();
-  const navigate = useNavigate();
-  const channelId = params.id;
-  const isEdit = channelId !== undefined;
-  const [loading, setLoading] = useState(isEdit);
-  const handleCancel = () => {
-    navigate('/channel');
-  };
-
-  const originInputs = {
-    name: '',
-    type: 1,
-    key: '',
-    openai_organization:'',
-    base_url: '',
-    other: '',
-    model_mapping: '',
-    models: [],
-    auto_ban: 1,
-    groups: ['default']
-  };
-  const [batch, setBatch] = useState(false);
-  const [autoBan, setAutoBan] = useState(true);
-  // const [autoBan, setAutoBan] = useState(true);
-  const [inputs, setInputs] = useState(originInputs);
-  const [originModelOptions, setOriginModelOptions] = useState([]);
-  const [modelOptions, setModelOptions] = useState([]);
-  const [groupOptions, setGroupOptions] = useState([]);
-  const [basicModels, setBasicModels] = useState([]);
-  const [fullModels, setFullModels] = useState([]);
-  const [customModel, setCustomModel] = useState('');
-  const handleInputChange = (e, { name, value }) => {
-    setInputs((inputs) => ({ ...inputs, [name]: value }));
-    if (name === 'type' && inputs.models.length === 0) {
-      let localModels = [];
-      switch (value) {
-        case 14:
-          localModels = ['claude-instant-1', 'claude-2'];
-          break;
-        case 11:
-          localModels = ['PaLM-2'];
-          break;
+    // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
+    switch (type) {
         case 15:
-          localModels = ['ERNIE-Bot', 'ERNIE-Bot-turbo', 'ERNIE-Bot-4', 'Embedding-V1'];
-          break;
-        case 17:
-          localModels = ['qwen-turbo', 'qwen-plus', 'text-embedding-v1'];
-          break;
-        case 16:
-          localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite'];
-          break;
+            return '按照如下格式输入:APIKey|SecretKey';
         case 18:
-          localModels = ['SparkDesk'];
-          break;
-        case 19:
-          localModels = ['360GPT_S2_V9', 'embedding-bert-512-v1', 'embedding_s1_v1', 'semantic_similarity_s1_v1'];
-          break;
+            return '按照如下格式输入:APPID|APISecret|APIKey';
+        case 22:
+            return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
         case 23:
-          localModels = ['hunyuan'];
-          break;
-      }
-      setInputs((inputs) => ({ ...inputs, models: localModels }));
+            return '按照如下格式输入:AppId|SecretId|SecretKey';
+        default:
+            return '请输入渠道对应的鉴权密钥';
     }
-    //setAutoBan
-  };
+}
 
-  const loadChannel = async () => {
-    let res = await API.get(`/api/channel/${channelId}`);
-    const { success, message, data } = res.data;
-    if (success) {
-      if (data.models === '') {
-        data.models = [];
-      } else {
-        data.models = data.models.split(',');
-      }
-      if (data.group === '') {
-        data.groups = [];
-      } else {
-        data.groups = data.group.split(',');
-      }
-      if (data.model_mapping !== '') {
-        data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2);
-      }
-      setInputs(data);
-      if (data.auto_ban === 0) {
-        setAutoBan(false);
-      } else {
-        setAutoBan(true);
-      }
-      // console.log(data);
-    } else {
-      showError(message);
-    }
-    setLoading(false);
-  };
+const EditChannel = (props) => {
+    const navigate = useNavigate();
+    const channelId = props.editingChannel.id;
+    const isEdit = channelId !== undefined;
+    const [loading, setLoading] = useState(isEdit);
+    const handleCancel = () => {
+        props.handleClose()
+    };
+    const originInputs = {
+        name: '',
+        type: 1,
+        key: '',
+        openai_organization: '',
+        base_url: '',
+        other: '',
+        model_mapping: '',
+        models: [],
+        auto_ban: 1,
+        groups: ['default']
+    };
+    const [batch, setBatch] = useState(false);
+    const [autoBan, setAutoBan] = useState(true);
+    // const [autoBan, setAutoBan] = useState(true);
+    const [inputs, setInputs] = useState(originInputs);
+    const [originModelOptions, setOriginModelOptions] = useState([]);
+    const [modelOptions, setModelOptions] = useState([]);
+    const [groupOptions, setGroupOptions] = useState([]);
+    const [basicModels, setBasicModels] = useState([]);
+    const [fullModels, setFullModels] = useState([]);
+    const [customModel, setCustomModel] = useState('');
+    const handleInputChange = (name, value) => {
+        setInputs((inputs) => ({...inputs, [name]: value}));
+        if (name === 'type' && inputs.models.length === 0) {
+            let localModels = [];
+            switch (value) {
+                case 14:
+                    localModels = ['claude-instant-1', 'claude-2'];
+                    break;
+                case 11:
+                    localModels = ['PaLM-2'];
+                    break;
+                case 15:
+                    localModels = ['ERNIE-Bot', 'ERNIE-Bot-turbo', 'ERNIE-Bot-4', 'Embedding-V1'];
+                    break;
+                case 17:
+                    localModels = ['qwen-turbo', 'qwen-plus', 'text-embedding-v1'];
+                    break;
+                case 16:
+                    localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite'];
+                    break;
+                case 18:
+                    localModels = ['SparkDesk'];
+                    break;
+                case 19:
+                    localModels = ['360GPT_S2_V9', 'embedding-bert-512-v1', 'embedding_s1_v1', 'semantic_similarity_s1_v1'];
+                    break;
+                case 23:
+                    localModels = ['hunyuan'];
+                    break;
+            }
+            setInputs((inputs) => ({...inputs, models: localModels}));
+        }
+        //setAutoBan
+    };
 
-  const fetchModels = async () => {
-    try {
-      let res = await API.get(`/api/channel/models`);
-      let localModelOptions = res.data.data.map((model) => ({
-        key: model.id,
-        text: model.id,
-        value: model.id
-      }));
-      setOriginModelOptions(localModelOptions);
-      setFullModels(res.data.data.map((model) => model.id));
-      setBasicModels(res.data.data.filter((model) => {
-        return model.id.startsWith('gpt-3') || model.id.startsWith('text-');
-      }).map((model) => model.id));
-    } catch (error) {
-      showError(error.message);
-    }
-  };
 
-  const fetchGroups = async () => {
-    try {
-      let res = await API.get(`/api/group/`);
-      setGroupOptions(res.data.data.map((group) => ({
-        key: group,
-        text: group,
-        value: group
-      })));
-    } catch (error) {
-      showError(error.message);
-    }
-  };
+    const loadChannel = async () => {
+        setLoading(true)
+        let res = await API.get(`/api/channel/${channelId}`);
+        const {success, message, data} = res.data;
+        if (success) {
+            if (data.models === '') {
+                data.models = [];
+            } else {
+                data.models = data.models.split(',');
+            }
+            if (data.group === '') {
+                data.groups = [];
+            } else {
+                data.groups = data.group.split(',');
+            }
+            if (data.model_mapping !== '') {
+                data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2);
+            }
+            setInputs(data);
+            if (data.auto_ban === 0) {
+                setAutoBan(false);
+            } else {
+                setAutoBan(true);
+            }
+            // console.log(data);
+        } else {
+            showError(message);
+        }
+        setLoading(false);
+    };
 
-  useEffect(() => {
-    let localModelOptions = [...originModelOptions];
-    inputs.models.forEach((model) => {
-      if (!localModelOptions.find((option) => option.key === model)) {
-        localModelOptions.push({
-          key: model,
-          text: model,
-          value: model
+    const fetchModels = async () => {
+        try {
+            let res = await API.get(`/api/channel/models`);
+            let localModelOptions = res.data.data.map((model) => ({
+                label: model.id,
+                value: model.id
+            }));
+            setOriginModelOptions(localModelOptions);
+            setFullModels(res.data.data.map((model) => model.id));
+            setBasicModels(res.data.data.filter((model) => {
+                return model.id.startsWith('gpt-3') || model.id.startsWith('text-');
+            }).map((model) => model.id));
+        } catch (error) {
+            showError(error.message);
+        }
+    };
+
+    const fetchGroups = async () => {
+        try {
+            let res = await API.get(`/api/group/`);
+            setGroupOptions(res.data.data.map((group) => ({
+                label: group,
+                value: group
+            })));
+        } catch (error) {
+            showError(error.message);
+        }
+    };
+
+    useEffect(() => {
+        let localModelOptions = [...originModelOptions];
+        inputs.models.forEach((model) => {
+            if (!localModelOptions.find((option) => option.key === model)) {
+                localModelOptions.push({
+                    label: model,
+                    value: model
+                });
+            }
         });
-      }
-    });
-    setModelOptions(localModelOptions);
-  }, [originModelOptions, inputs.models]);
+        setModelOptions(localModelOptions);
+    }, [originModelOptions, inputs.models]);
 
-  useEffect(() => {
-    if (isEdit) {
-      loadChannel().then();
-    }
-    fetchModels().then();
-    fetchGroups().then();
-  }, []);
+    useEffect(() => {
+        fetchModels().then();
+        fetchGroups().then();
+        if (isEdit) {
+            loadChannel().then(
+                () => {
 
-  useEffect(() => {
-    setInputs((inputs) => ({ ...inputs, auto_ban: autoBan ? 1 : 0 }));
-    console.log(autoBan);
-  }, [autoBan]);
+                }
+            );
+        } else {
+            setInputs(originInputs)
+        }
+    }, [props.editingChannel.id]);
 
-  const submit = async () => {
-    if (!isEdit && (inputs.name === '' || inputs.key === '')) {
-      showInfo('请填写渠道名称和渠道密钥!');
-      return;
-    }
-    if (inputs.models.length === 0) {
-      showInfo('请至少选择一个模型!');
-      return;
-    }
-    if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
-      showInfo('模型映射必须是合法的 JSON 格式!');
-      return;
-    }
-    let localInputs = inputs;
-    if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
-      localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
-    }
-    if (localInputs.type === 3 && localInputs.other === '') {
-      localInputs.other = '2023-06-01-preview';
-    }
-    if (localInputs.type === 18 && localInputs.other === '') {
-      localInputs.other = 'v2.1';
-    }
-    let res;
-    if (!Array.isArray(localInputs.models)) {
-        showError('提交失败,请勿重复提交!');
-        handleCancel();
-        return;
-    }
-    localInputs.models = localInputs.models.join(',');
-    localInputs.group = localInputs.groups.join(',');
-    if (isEdit) {
-      res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId) });
-    } else {
-      res = await API.post(`/api/channel/`, localInputs);
-    }
-    const { success, message } = res.data;
-    if (success) {
-      if (isEdit) {
-        showSuccess('渠道更新成功!');
-      } else {
-        showSuccess('渠道创建成功!');
-        setInputs(originInputs);
-      }
-    } else {
-      showError(message);
-    }
-  };
 
-  const addCustomModel = () => {
-    if (customModel.trim() === '') return;
-    if (inputs.models.includes(customModel)) return;
-    let localModels = [...inputs.models];
-    localModels.push(customModel);
-    let localModelOptions = [];
-    localModelOptions.push({
-      key: customModel,
-      text: customModel,
-      value: customModel
-    });
-    setModelOptions(modelOptions => {
-      return [...modelOptions, ...localModelOptions];
-    });
-    setCustomModel('');
-    handleInputChange(null, { name: 'models', value: localModels });
-  };
+    const submit = async () => {
+        if (!isEdit && (inputs.name === '' || inputs.key === '')) {
+            showInfo('请填写渠道名称和渠道密钥!');
+            return;
+        }
+        if (inputs.models.length === 0) {
+            showInfo('请至少选择一个模型!');
+            return;
+        }
+        if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
+            showInfo('模型映射必须是合法的 JSON 格式!');
+            return;
+        }
+        let localInputs = {...inputs};
+        if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
+            localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
+        }
+        if (localInputs.type === 3 && localInputs.other === '') {
+            localInputs.other = '2023-06-01-preview';
+        }
+        if (localInputs.type === 18 && localInputs.other === '') {
+            localInputs.other = 'v2.1';
+        }
+        let res;
+        if (!Array.isArray(localInputs.models)) {
+            showError('提交失败,请勿重复提交!');
+            handleCancel();
+            return;
+        }
+        localInputs.models = localInputs.models.join(',');
+        localInputs.group = localInputs.groups.join(',');
+        if (isEdit) {
+            res = await API.put(`/api/channel/`, {...localInputs, id: parseInt(channelId)});
+        } else {
+            res = await API.post(`/api/channel/`, localInputs);
+        }
+        const {success, message} = res.data;
+        if (success) {
+            if (isEdit) {
+                showSuccess('渠道更新成功!');
+            } else {
+                showSuccess('渠道创建成功!');
+                setInputs(originInputs);
+            }
+            props.refresh();
+            props.handleClose();
+        } else {
+            showError(message);
+        }
+    };
 
-  return (
-    <>
-      <Segment loading={loading}>
-        <Header as='h3'>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Header>
-        <Form autoComplete='new-password'>
-          <Form.Field>
-            <Form.Select
-              label='类型'
-              name='type'
-              required
-              options={CHANNEL_OPTIONS}
-              value={inputs.type}
-              onChange={handleInputChange}
-            />
-          </Form.Field>
-          {
-            inputs.type === 3 && (
-              <>
-                <Message>
-                  注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的 model
-                  参数替换为你的部署名称(模型名称中的点会被剔除),<a target='_blank'
-                                                                    href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a>。
-                </Message>
-                <Form.Field>
-                  <Form.Input
-                    label='AZURE_OPENAI_ENDPOINT'
-                    name='base_url'
-                    placeholder={'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'}
-                    onChange={handleInputChange}
-                    value={inputs.base_url}
-                    autoComplete='new-password'
-                  />
-                </Form.Field>
-                <Form.Field>
-                  <Form.Input
-                    label='默认 API 版本'
-                    name='other'
-                    placeholder={'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'}
-                    onChange={handleInputChange}
-                    value={inputs.other}
-                    autoComplete='new-password'
-                  />
-                </Form.Field>
-              </>
-            )
-          }
-          {
-            inputs.type === 8 && (
-              <Form.Field>
-                <Form.Input
-                  label='Base URL'
-                  name='base_url'
-                  placeholder={'请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'}
-                  onChange={handleInputChange}
-                  value={inputs.base_url}
-                  autoComplete='new-password'
-                />
-              </Form.Field>
-            )
-          }
-          <Form.Field>
-            <Form.Input
-              label='名称'
-              required
-              name='name'
-              placeholder={'请为渠道命名'}
-              onChange={handleInputChange}
-              value={inputs.name}
-              autoComplete='new-password'
-            />
-          </Form.Field>
-          <Form.Field>
-            <Form.Dropdown
-              label='分组'
-              placeholder={'请选择可以使用该渠道的分组'}
-              name='groups'
-              required
-              fluid
-              multiple
-              selection
-              allowAdditions
-              additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
-              onChange={handleInputChange}
-              value={inputs.groups}
-              autoComplete='new-password'
-              options={groupOptions}
-            />
-          </Form.Field>
-          {
-            inputs.type === 18 && (
-              <Form.Field>
-                <Form.Input
-                  label='模型版本'
-                  name='other'
-                  placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
-                  onChange={handleInputChange}
-                  value={inputs.other}
-                  autoComplete='new-password'
-                />
-              </Form.Field>
-            )
-          }
-          {
-            inputs.type === 21 && (
-              <Form.Field>
-                <Form.Input
-                  label='知识库 ID'
-                  name='other'
-                  placeholder={'请输入知识库 ID,例如:123456'}
-                  onChange={handleInputChange}
-                  value={inputs.other}
-                  autoComplete='new-password'
-                />
-              </Form.Field>
-            )
-          }
-          <Form.Field>
-            <Form.Dropdown
-              label='模型'
-              placeholder={'请选择该渠道所支持的模型'}
-              name='models'
-              required
-              fluid
-              multiple
-              selection
-              onChange={handleInputChange}
-              value={inputs.models}
-              autoComplete='new-password'
-              options={modelOptions}
-            />
-          </Form.Field>
-          <div style={{ lineHeight: '40px', marginBottom: '12px' }}>
-            <Button type={'button'} onClick={() => {
-              handleInputChange(null, { name: 'models', value: basicModels });
-            }}>填入基础模型</Button>
-            <Button type={'button'} onClick={() => {
-              handleInputChange(null, { name: 'models', value: fullModels });
-            }}>填入所有模型</Button>
-            <Button type={'button'} onClick={() => {
-              handleInputChange(null, { name: 'models', value: [] });
-            }}>清除所有模型</Button>
-            <Input
-              action={
-                <Button type={'button'} onClick={addCustomModel}>填入</Button>
-              }
-              placeholder='输入自定义模型名称'
-              value={customModel}
-              onChange={(e, { value }) => {
-                setCustomModel(value);
-              }}
-              onKeyDown={(e) => {
-                if (e.key === 'Enter') {
-                  addCustomModel();
-                  e.preventDefault();
+    const addCustomModel = () => {
+        if (customModel.trim() === '') return;
+        if (inputs.models.includes(customModel)) return;
+        let localModels = [...inputs.models];
+        localModels.push(customModel);
+        let localModelOptions = [];
+        localModelOptions.push({
+            key: customModel,
+            text: customModel,
+            value: customModel
+        });
+        setModelOptions(modelOptions => {
+            return [...modelOptions, ...localModelOptions];
+        });
+        setCustomModel('');
+        handleInputChange('models', localModels);
+    };
+
+    return (
+        <>
+            <SideSheet
+                placement={isEdit ? 'right' : 'left'}
+                title={<Title level={3}>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Title>}
+                headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
+                bodyStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
+                visible={props.visible}
+                footer={
+                    <div style={{display: 'flex', justifyContent: 'flex-end'}}>
+                        <Space>
+                            <Button theme='solid' size={'large'} onClick={submit}>提交</Button>
+                            <Button theme='solid' size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button>
+                        </Space>
+                    </div>
                 }
-              }}
-            />
-          </div>
-          <Form.Field>
-            <Form.TextArea
-              label='模型重定向'
-              placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
-              name='model_mapping'
-              onChange={handleInputChange}
-              value={inputs.model_mapping}
-              style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
-              autoComplete='new-password'
-            />
-          </Form.Field>
-          {
-            batch ? <Form.Field>
-              <Form.TextArea
-                label='密钥'
-                name='key'
-                required
-                placeholder={'请输入密钥,一行一个'}
-                onChange={handleInputChange}
-                value={inputs.key}
-                style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
-                autoComplete='new-password'
-              />
-            </Form.Field> : <Form.Field>
-              <Form.Input
-                label='密钥'
-                name='key'
-                required
-                placeholder={type2secretPrompt(inputs.type)}
-                onChange={handleInputChange}
-                value={inputs.key}
-                autoComplete='new-password'
-              />
-            </Form.Field>
-          }
-          <Form.Field>
-            <Form.Input
-                label='组织,可选,不填则为默认组织'
-                name='openai_organization'
-                placeholder='请输入组织org-xxx'
-                onChange={handleInputChange}
-                value={inputs.openai_organization}
-            />
-          </Form.Field>
-          <Form.Field>
-            <Form.Checkbox
-                label='是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道'
-                name='auto_ban'
-                checked={autoBan}
-                onChange={
-                    () => {
-                        setAutoBan(!autoBan);
+                closeIcon={null}
+                onCancel={() => handleCancel()}
+                width={isMobile() ? '100%' : 600}
+            >
+                <Spin spinning={loading}>
+                    <div style={{marginTop: 10}}>
+                        <Typography.Text strong>类型:</Typography.Text>
+                    </div>
+                    <Select
+                        name='type'
+                        required
+                        optionList={CHANNEL_OPTIONS}
+                        value={inputs.type}
+                        onChange={value => handleInputChange('type', value)}
+                        style={{width: '50%'}}
+                    />
+                    {
+                        inputs.type === 3 && (
+                            <>
+                                <div style={{marginTop: 10}}>
+                                    <Banner type={"warning"} description={
+                                        <>
+                                            注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的
+                                            model
+                                            参数替换为你的部署名称(模型名称中的点会被剔除),<a target='_blank'
+                                                                                              href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a>。
+                                        </>
+                                    }>
+                                    </Banner>
+                                </div>
+                                <div style={{marginTop: 10}}>
+                                    <Typography.Text strong>AZURE_OPENAI_ENDPOINT:</Typography.Text>
+                                </div>
+                                <Input
+                                    label='AZURE_OPENAI_ENDPOINT'
+                                    name='azure_base_url'
+                                    placeholder={'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'}
+                                    onChange={value => {
+                                        handleInputChange('base_url', value)
+                                    }}
+                                    value={inputs.base_url}
+                                    autoComplete='new-password'
+                                />
+                                <div style={{marginTop: 10}}>
+                                    <Typography.Text strong>默认 API 版本:</Typography.Text>
+                                </div>
+                                <Input
+                                    label='默认 API 版本'
+                                    name='azure_other'
+                                    placeholder={'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'}
+                                    onChange={value => {
+                                        handleInputChange('other', value)
+                                    }}
+                                    value={inputs.other}
+                                    autoComplete='new-password'
+                                />
+                            </>
+                        )
+                    }
+                    {
+                        inputs.type === 8 && (
+                            <>
+                                <div style={{marginTop: 10}}>
+                                    <Typography.Text strong>Base URL:</Typography.Text>
+                                </div>
+                                <Input
+                                    name='base_url'
+                                    placeholder={'请输入自定义渠道的 Base URL'}
+                                    onChange={value => {
+                                        handleInputChange('base_url', value)
+                                    }}
+                                    value={inputs.base_url}
+                                    autoComplete='new-password'
+                                />
+                            </>
+                        )
+                    }
+                    <div style={{marginTop: 10}}>
+                        <Typography.Text strong>名称:</Typography.Text>
+                    </div>
+                    <Input
+                        required
+                        name='name'
+                        placeholder={'请为渠道命名'}
+                        onChange={value => {
+                            handleInputChange('name', value)
+                        }}
+                        value={inputs.name}
+                        autoComplete='new-password'
+                    />
+                    <div style={{marginTop: 10}}>
+                        <Typography.Text strong>分组:</Typography.Text>
+                    </div>
+                    <Select
+                        placeholder={'请选择可以使用该渠道的分组'}
+                        name='groups'
+                        required
+                        multiple
+                        selection
+                        allowAdditions
+                        additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
+                        onChange={value => {
+                            handleInputChange('groups', value)
+                        }}
+                        value={inputs.groups}
+                        autoComplete='new-password'
+                        optionList={groupOptions}
+                    />
+                    {
+                        inputs.type === 18 && (
+                            <>
+                                <div style={{marginTop: 10}}>
+                                    <Typography.Text strong>模型版本:</Typography.Text>
+                                </div>
+                                <Input
+                                    name='other'
+                                    placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
+                                    onChange={value => {
+                                        handleInputChange('other', value)
+                                    }}
+                                    value={inputs.other}
+                                    autoComplete='new-password'
+                                />
+                            </>
+                        )
+                    }
+                    {
+                        inputs.type === 21 && (
+                            <>
+                                <div style={{marginTop: 10}}>
+                                    <Typography.Text strong>知识库 ID:</Typography.Text>
+                                </div>
+                                <Input
+                                    label='知识库 ID'
+                                    name='other'
+                                    placeholder={'请输入知识库 ID,例如:123456'}
+                                    onChange={value => {
+                                        handleInputChange('other', value)
+                                    }}
+                                    value={inputs.other}
+                                    autoComplete='new-password'
+                                />
+                            </>
+                        )
+                    }
+                    <div style={{marginTop: 10}}>
+                        <Typography.Text strong>模型:</Typography.Text>
+                    </div>
+                    <Select
+                        placeholder={'请选择该渠道所支持的模型'}
+                        name='models'
+                        required
+                        multiple
+                        selection
+                        onChange={value => {
+                            handleInputChange('models', value)
+                        }}
+                        value={inputs.models}
+                        autoComplete='new-password'
+                        optionList={modelOptions}
+                    />
+                    <div style={{lineHeight: '40px', marginBottom: '12px'}}>
+                        <Space>
+                            <Button type='primary' onClick={() => {
+                                handleInputChange('models', basicModels);
+                            }}>填入基础模型</Button>
+                            <Button type='secondary' onClick={() => {
+                                handleInputChange('models', fullModels);
+                            }}>填入所有模型</Button>
+                            <Button type='warning' onClick={() => {
+                                handleInputChange('models', []);
+                            }}>清除所有模型</Button>
+                        </Space>
+                        <Input
+                            addonAfter={
+                                <Button type='primary' onClick={addCustomModel}>填入</Button>
+                            }
+                            placeholder='输入自定义模型名称'
+                            value={customModel}
+                            onChange={(value) => {
+                                setCustomModel(value);
+                            }}
+                        />
+                    </div>
+                    <div style={{marginTop: 10}}>
+                        <Typography.Text strong>模型重定向:</Typography.Text>
+                    </div>
+                    <TextArea
+                        placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
+                        name='model_mapping'
+                        onChange={value => {
+                            handleInputChange('model_mapping', value)
+                        }}
+                        autosize
+                        value={inputs.model_mapping}
+                        autoComplete='new-password'
+                    />
+                    <Typography.Text style={{
+                        color: 'rgba(var(--semi-blue-5), 1)',
+                        userSelect: 'none',
+                        cursor: 'pointer'
+                    }} onClick={
+                        () => {
+                            handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))
+                        }
+                    }>
+                        填入模板
+                    </Typography.Text>
+                    <div style={{marginTop: 10}}>
+                        <Typography.Text strong>密钥:</Typography.Text>
+                    </div>
+                    {
+                        batch ?
+                            <TextArea
+                                label='密钥'
+                                name='key'
+                                required
+                                placeholder={'请输入密钥,一行一个'}
+                                onChange={value => {
+                                    handleInputChange('key', value)
+                                }}
+                                value={inputs.key}
+                                style={{minHeight: 150, fontFamily: 'JetBrains Mono, Consolas'}}
+                                autoComplete='new-password'
+                            />
+                            :
+                            <Input
+                                label='密钥'
+                                name='key'
+                                required
+                                placeholder={type2secretPrompt(inputs.type)}
+                                onChange={value => {
+                                    handleInputChange('key', value)
+                                }}
+                                value={inputs.key}
+                                autoComplete='new-password'
+                            />
+                    }
+                    <div style={{marginTop: 10}}>
+                        <Typography.Text strong>组织:</Typography.Text>
+                    </div>
+                    <Input
+                        label='组织,可选,不填则为默认组织'
+                        name='openai_organization'
+                        placeholder='请输入组织org-xxx'
+                        onChange={value => {
+                            handleInputChange('openai_organization', value)
+                        }}
+                        value={inputs.openai_organization}
+                    />
+                    <div style={{marginTop: 10, display: 'flex'}}>
+                        <Space>
+                            <Checkbox
+                                name='auto_ban'
+                                checked={autoBan}
+                                onChange={
+                                    () => {
+                                        setAutoBan(!autoBan);
+
+                                    }
+                                }
+                                // onChange={handleInputChange}
+                            />
+                            <Typography.Text
+                                strong>是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:</Typography.Text>
+                        </Space>
+                    </div>
 
+                    {
+                        !isEdit && (
+                            <div style={{marginTop: 10, display: 'flex'}}>
+                                <Space>
+                                    <Checkbox
+                                        checked={batch}
+                                        label='批量创建'
+                                        name='batch'
+                                        onChange={() => setBatch(!batch)}
+                                    />
+                                    <Typography.Text strong>批量创建</Typography.Text>
+                                </Space>
+                            </div>
+                        )
                     }
-                }
-                // onChange={handleInputChange}
-            />
-          </Form.Field>
-          {
-            !isEdit && (
-              <Form.Checkbox
-                checked={batch}
-                label='批量创建'
-                name='batch'
-                onChange={() => setBatch(!batch)}
-              />
-            )
-          }
-          {
-            inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && (
-              <Form.Field>
-                <Form.Input
-                  label='代理'
-                  name='base_url'
-                  placeholder={'此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com'}
-                  onChange={handleInputChange}
-                  value={inputs.base_url}
-                  autoComplete='new-password'
-                />
-              </Form.Field>
-            )
-          }
-          {
-            inputs.type === 22 && (
-              <Form.Field>
-                <Form.Input
-                  label='私有部署地址'
-                  name='base_url'
-                  placeholder={'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'}
-                  onChange={handleInputChange}
-                  value={inputs.base_url}
-                  autoComplete='new-password'
-                />
-              </Form.Field>
-            )
-          }
-          <Button onClick={handleCancel}>取消</Button>
-          <Button type={isEdit ? 'button' : 'submit'} positive onClick={submit}>提交</Button>
-        </Form>
-      </Segment>
-    </>
-  );
+                    {
+                        inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && (
+                            <>
+                                <div style={{marginTop: 10}}>
+                                    <Typography.Text strong>代理:</Typography.Text>
+                                </div>
+                                <Input
+                                    label='代理'
+                                    name='base_url'
+                                    placeholder={'此项可选,用于通过代理站来进行 API 调用'}
+                                    onChange={value => {
+                                        handleInputChange('base_url', value)
+                                    }}
+                                    value={inputs.base_url}
+                                    autoComplete='new-password'
+                                />
+                            </>
+                        )
+                    }
+                    {
+                        inputs.type === 22 && (
+                            <>
+                                <div style={{marginTop: 10}}>
+                                    <Typography.Text strong>私有部署地址:</Typography.Text>
+                                </div>
+                                <Input
+                                    name='base_url'
+                                    placeholder={'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'}
+                                    onChange={value => {
+                                        handleInputChange('base_url', value)
+                                    }}
+                                    value={inputs.base_url}
+                                    autoComplete='new-password'
+                                />
+                            </>
+                        )
+                    }
+
+                </Spin>
+            </SideSheet>
+        </>
+    );
 };
 
 export default EditChannel;

+ 12 - 7
web/src/pages/Channel/index.js

@@ -1,14 +1,19 @@
 import React from 'react';
-import { Header, Segment } from 'semantic-ui-react';
 import ChannelsTable from '../../components/ChannelsTable';
+import {Layout} from "@douyinfe/semi-ui";
+import RedemptionsTable from "../../components/RedemptionsTable";
 
 const File = () => (
-  <>
-    <Segment>
-      <Header as='h3'>管理渠道</Header>
-      <ChannelsTable />
-    </Segment>
-  </>
+    <>
+        <Layout>
+            <Layout.Header>
+                <h3>管理渠道</h3>
+            </Layout.Header>
+            <Layout.Content>
+                <ChannelsTable/>
+            </Layout.Content>
+        </Layout>
+    </>
 );
 
 export default File;