CaIon 2 лет назад
Родитель
Сommit
0a945c160d

+ 11 - 0
README.md

@@ -1,3 +1,14 @@
 
 
 # Neko API
 # Neko API
 
 
+> **Note**
+> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发,感谢原作者的无私奉献。 
+> 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
+
+
+> **Warning**
+> 本项目为个人学习使用,不保证稳定性,且不提供任何技术支持,使用者必须在遵循 OpenAI 的使用条款以及法律法规的情况下使用,不得用于非法用途。  
+> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
+
+> **Note**
+> 最新版Docker镜像 calciumion/neko-api:main

+ 2 - 3
web/public/index.html

@@ -6,10 +6,9 @@
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <meta name="theme-color" content="#ffffff" />
     <meta name="theme-color" content="#ffffff" />
     <meta name="description"
     <meta name="description"
-          content="Neko API,企业级AI接口调用平台,专为企业级需求打造,提供高性能、高并发、高可用的服务,一站式处理大规模数据和复杂任务。我们的稳定高并发处理能力和高可用性保证您的业务流畅运行,结合OpenAI、Claude、Midjourney等AI接口和专业的技术支持,为您的企业快速部署和实现AI接口应用,释放商业价值"/>
-    <title>Neko API</title>
+          content="NekoAPI,企业级AI接口调用平台,专为企业级需求打造,提供高性能、高并发、高可用的服务,一站式处理大规模数据和复杂任务。我们的稳定高并发处理能力和高可用性保证您的业务流畅运行,结合OpenAI、Claude、Midjourney等AI接口和专业的技术支持,为您的企业快速部署和实现AI接口应用,释放商业价值"/>
     <meta name="keywords" content="Neko API,OpenAI,Claude,Midjourney,高并发,高可用,高性能,企业级AI接口调用平台"/>
     <meta name="keywords" content="Neko API,OpenAI,Claude,Midjourney,高并发,高可用,高性能,企业级AI接口调用平台"/>
-    <title>Neko API</title>
+    <title>NekoAPI</title>
   </head>
   </head>
   <body>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>
     <noscript>You need to enable JavaScript to run this app.</noscript>

+ 5 - 15
web/src/components/LogsTable.js

@@ -20,7 +20,7 @@ import {
 } from '@douyinfe/semi-icons';
 } from '@douyinfe/semi-icons';
 
 
 const {Sider, Content, Header} = Layout;
 const {Sider, Content, Header} = Layout;
-const { Column } = Table;
+const {Column} = Table;
 
 
 
 
 function renderTimestamp(timestamp) {
 function renderTimestamp(timestamp) {
@@ -65,13 +65,13 @@ const LogsTable = () => {
         {
         {
             title: '渠道',
             title: '渠道',
             dataIndex: 'channel',
             dataIndex: 'channel',
-            className: isAdmin()?'tableShow':'tableHiddle',
+            className: isAdmin() ? 'tableShow' : 'tableHiddle',
             render: (text, record, index) => {
             render: (text, record, index) => {
                 return (
                 return (
                     isAdminUser ?
                     isAdminUser ?
                         record.type === 0 || record.type === 2 ?
                         record.type === 0 || record.type === 2 ?
                             <div>
                             <div>
-                                {<Tag color={colors[parseInt(text) % 10]} size='large'> {text} </Tag>}
+                                {<Tag color={stringToColor(text)} size='large'> {text} </Tag>}
                             </div>
                             </div>
                             :
                             :
                             <></>
                             <></>
@@ -83,7 +83,7 @@ const LogsTable = () => {
         {
         {
             title: '用户',
             title: '用户',
             dataIndex: 'username',
             dataIndex: 'username',
-            className: isAdmin()?'tableShow':'tableHiddle',
+            className: isAdmin() ? 'tableShow' : 'tableHiddle',
             render: (text, record, index) => {
             render: (text, record, index) => {
                 return (
                 return (
                     isAdminUser ?
                     isAdminUser ?
@@ -291,15 +291,6 @@ const LogsTable = () => {
 
 
     const pageData = logs.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
     const pageData = logs.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
 
 
-    // const onPaginationChange = (e, { activePage }) => {
-    //   (async () => {
-    //     if (activePage === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
-    //       // In this case we have to load more data and then append them.
-    //       await loadLogs(activePage - 1);
-    //     }
-    //     setActivePage(activePage);
-    //   })();
-    // };
     const handlePageChange = page => {
     const handlePageChange = page => {
         setActivePage(page);
         setActivePage(page);
         if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
         if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
@@ -307,7 +298,6 @@ const LogsTable = () => {
             loadLogs(page - 1).then(r => {
             loadLogs(page - 1).then(r => {
             });
             });
         }
         }
-        // setLoading(false);
     };
     };
 
 
     const refresh = async () => {
     const refresh = async () => {
@@ -405,7 +395,7 @@ const LogsTable = () => {
                             </>
                             </>
                         }
                         }
                         <Form.Section>
                         <Form.Section>
-                            <Button label='操作' type="primary" htmlType="submit" className="btn-margin-right"
+                            <Button label='查询' type="primary" htmlType="submit" className="btn-margin-right"
                                     onClick={refresh}>查询</Button>
                                     onClick={refresh}>查询</Button>
                         </Form.Section>
                         </Form.Section>
                     </>
                     </>

+ 3 - 3
web/src/components/SiderBar.js

@@ -34,7 +34,7 @@ let headerButtons = [
         itemKey: 'channel',
         itemKey: 'channel',
         to: '/channel',
         to: '/channel',
         icon: <IconLayers/>,
         icon: <IconLayers/>,
-        admin: true
+        className: isAdmin()?'semi-navigation-item-normal':'tableHiddle',
     },
     },
 
 
     {
     {
@@ -48,7 +48,7 @@ let headerButtons = [
         itemKey: 'redemption',
         itemKey: 'redemption',
         to: '/redemption',
         to: '/redemption',
         icon: <IconGift/>,
         icon: <IconGift/>,
-        admin: true
+        className: isAdmin()?'semi-navigation-item-normal':'tableHiddle',
     },
     },
     {
     {
         text: '钱包',
         text: '钱包',
@@ -61,7 +61,7 @@ let headerButtons = [
         itemKey: 'user',
         itemKey: 'user',
         to: '/user',
         to: '/user',
         icon: <IconUser/>,
         icon: <IconUser/>,
-        admin: true
+        className: isAdmin()?'semi-navigation-item-normal':'tableHiddle',
     },
     },
     {
     {
         text: '日志',
         text: '日志',

+ 434 - 438
web/src/components/TokensTable.js

@@ -1,464 +1,460 @@
-import React, { useEffect, useState } from 'react';
-import { Button, Dropdown, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react';
-import { Link } from 'react-router-dom';
-import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers';
+import React, {useEffect, useState} from 'react';
+import {Link} from 'react-router-dom';
+import {API, copy, isAdmin, showError, showSuccess, showWarning, timestamp2string} from '../helpers';
 
 
-import { ITEMS_PER_PAGE } from '../constants';
-import { renderQuota } from '../helpers/render';
+import {ITEMS_PER_PAGE} from '../constants';
+import {renderQuota, stringToColor} from '../helpers/render';
+import {Avatar, Tag, Table, Button, Popover, Form, Modal, Popconfirm} from "@douyinfe/semi-ui";
+
+const {Column} = Table;
 
 
 const COPY_OPTIONS = [
 const COPY_OPTIONS = [
-  { key: 'next', text: 'ChatGPT Next Web', value: 'next' },
-  { key: 'ama', text: 'AMA 问天', value: 'ama' },
-  { key: 'opencat', text: 'OpenCat', value: 'opencat' },
+    {key: 'next', text: 'ChatGPT Next Web', value: 'next'},
+    {key: 'ama', text: 'AMA 问天', value: 'ama'},
+    {key: 'opencat', text: 'OpenCat', value: 'opencat'},
 ];
 ];
 
 
 const OPEN_LINK_OPTIONS = [
 const OPEN_LINK_OPTIONS = [
-  { key: 'ama', text: 'AMA 问天', value: 'ama' },
-  { key: 'opencat', text: 'OpenCat', value: 'opencat' },
+    {key: 'ama', text: 'AMA 问天', value: 'ama'},
+    {key: 'opencat', text: 'OpenCat', value: 'opencat'},
 ];
 ];
 
 
 function renderTimestamp(timestamp) {
 function renderTimestamp(timestamp) {
-  return (
-    <>
-      {timestamp2string(timestamp)}
-    </>
-  );
+    return (
+        <>
+            {timestamp2string(timestamp)}
+        </>
+    );
 }
 }
 
 
 function renderStatus(status) {
 function renderStatus(status) {
-  switch (status) {
-    case 1:
-      return <Label basic color='green'>已启用</Label>;
-    case 2:
-      return <Label basic color='red'> 已禁用 </Label>;
-    case 3:
-      return <Label basic color='yellow'> 已过期 </Label>;
-    case 4:
-      return <Label basic color='grey'> 已耗尽 </Label>;
-    default:
-      return <Label basic color='black'> 未知状态 </Label>;
-  }
+    switch (status) {
+        case 1:
+            return <Tag color='green' size='large'>已启用</Tag>;
+        case 2:
+            return <Tag color='red' size='large'> 已禁用 </Tag>;
+        case 3:
+            return <Tag color='yellow' size='large'> 已过期 </Tag>;
+        case 4:
+            return <Tag color='grey' size='large'> 已耗尽 </Tag>;
+        default:
+            return <Tag color='black' size='large'> 未知状态 </Tag>;
+    }
 }
 }
 
 
 const TokensTable = () => {
 const TokensTable = () => {
-  const [tokens, setTokens] = useState([]);
-  const [loading, setLoading] = useState(true);
-  const [activePage, setActivePage] = useState(1);
-  const [searchKeyword, setSearchKeyword] = useState('');
-  const [searching, setSearching] = useState(false);
-  const [showTopUpModal, setShowTopUpModal] = useState(false);
-  const [targetTokenIdx, setTargetTokenIdx] = useState(0);
-
-  const loadTokens = async (startIdx) => {
-    const res = await API.get(`/api/token/?p=${startIdx}`);
-    const { success, message, data } = res.data;
-    if (success) {
-      if (startIdx === 0) {
-        setTokens(data);
-      } else {
-        let newTokens = [...tokens];
-        newTokens.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
-        setTokens(newTokens);
-      }
-    } else {
-      showError(message);
+    const columns = [
+        {
+            title: '名称',
+            dataIndex: 'name',
+        },
+        {
+            title: '状态',
+            dataIndex: 'status',
+            key: 'status',
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        {renderStatus(text)}
+                    </div>
+                );
+            },
+        },
+        {
+            title: '已用额度',
+            dataIndex: 'used_quota',
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        {renderQuota(parseInt(text))}
+                    </div>
+                );
+            },
+        },
+        {
+            title: '剩余额度',
+            dataIndex: 'remain_quota',
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        {renderQuota(parseInt(text))}
+                    </div>
+                );
+            },
+        },
+        {
+            title: '创建时间',
+            dataIndex: 'created_time',
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        {renderTimestamp(text)}
+                    </div>
+                );
+            },
+        },
+        {
+            title: '过期时间',
+            dataIndex: 'accessed_time',
+            render: (text, record, index) => {
+                return (
+                    <div>
+                        {renderTimestamp(text)}
+                    </div>
+                );
+            },
+        },
+        {
+            title: '',
+            dataIndex: 'operate',
+            render: (text, record, index) => (
+                <div>
+                    <Popover
+                        content={
+                            'sk-' + record.key
+                        }
+                        style={{padding: 20}}
+                        position="top"
+                    >
+                        <Button theme='light' type='tertiary' style={{marginRight: 1}}>查看</Button>
+                    </Popover>
+                    <Button theme='light' type='secondary' style={{marginRight: 1}}
+                            onClick={async (text) => {
+                                await copyText('sk-' + record.key)
+                            }}
+                    >复制</Button>
+                    <Popconfirm
+                        title="确定是否要删除此令牌?"
+                        content="此修改将不可逆"
+                        okType={'danger'}
+                        position={'left'}
+                        onConfirm={() => {
+                            manageToken(record.id, 'delete', record).then(
+                                () => {
+                                    removeRecord(record.key);
+                                }
+                            )
+                        }}
+                    >
+                        <Button theme='light' type='danger' style={{marginRight: 1}}>删除</Button>
+                    </Popconfirm>
+                    {
+                        record.status === 1 ?
+                            <Button theme='light' type='warning' style={{marginRight: 1}} onClick={
+                                async () => {
+                                    manageToken(
+                                        record.id,
+                                        'disable',
+                                        record
+                                    )
+                                }
+                            }>禁用</Button> :
+                            <Button theme='light' type='secondary' style={{marginRight: 1}} onClick={
+                                async () => {
+                                    manageToken(
+                                        record.id,
+                                        'enable',
+                                        record
+                                    );
+                                }
+                            }>启用</Button>
+                    }
+                    <Button theme='light' type='tertiary' style={{marginRight: 1}}>编辑</Button>
+                </div>
+            ),
+        },
+    ];
+
+    const [tokens, setTokens] = useState([]);
+    const [selectedKeys, setSelectedKeys] = useState([]);
+    const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE);
+    const [loading, setLoading] = useState(true);
+    const [activePage, setActivePage] = useState(1);
+    const [searchKeyword, setSearchKeyword] = useState('');
+    const [searching, setSearching] = useState(false);
+    const [showTopUpModal, setShowTopUpModal] = useState(false);
+    const [targetTokenIdx, setTargetTokenIdx] = useState(0);
+
+    const setTokensFormat = (tokens) => {
+        setTokens(tokens);
+        if (tokens.length === ITEMS_PER_PAGE) {
+            setTokenCount(tokens.length + ITEMS_PER_PAGE);
+        } else {
+            setTokenCount(tokens.length);
+        }
     }
     }
-    setLoading(false);
-  };
 
 
-  const onPaginationChange = (e, { activePage }) => {
-    (async () => {
-      if (activePage === Math.ceil(tokens.length / ITEMS_PER_PAGE) + 1) {
-        // In this case we have to load more data and then append them.
+    // let pageData = tokens.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
+    const loadTokens = async (startIdx) => {
+        setLoading(true);
+        const res = await API.get(`/api/token/?p=${startIdx}`);
+        const {success, message, data} = res.data;
+        if (success) {
+            if (startIdx === 0) {
+                setTokensFormat(data);
+            } else {
+                let newTokens = [...tokens];
+                newTokens.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
+                setTokensFormat(newTokens);
+            }
+        } else {
+            showError(message);
+        }
+        setLoading(false);
+    };
+
+    const onPaginationChange = (e, {activePage}) => {
+        (async () => {
+            if (activePage === Math.ceil(tokens.length / ITEMS_PER_PAGE) + 1) {
+                // In this case we have to load more data and then append them.
+                await loadTokens(activePage - 1);
+            }
+            setActivePage(activePage);
+        })();
+    };
+
+    const refresh = async () => {
         await loadTokens(activePage - 1);
         await loadTokens(activePage - 1);
-      }
-      setActivePage(activePage);
-    })();
-  };
-
-  const refresh = async () => {
-    setLoading(true);
-    await loadTokens(activePage - 1);
-  };
-
-  const onCopy = async (type, key) => {
-    let status = localStorage.getItem('status');
-    let serverAddress = '';
-    if (status) {
-      status = JSON.parse(status);
-      serverAddress = status.server_address;
-    }
-    if (serverAddress === '') {
-      serverAddress = window.location.origin;
-    }
-    let encodedServerAddress = encodeURIComponent(serverAddress);
-    const nextLink = localStorage.getItem('chat_link');
-    let nextUrl;
-
-    if (nextLink) {
-      nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
-    } else {
-      nextUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
-    }
+    };
 
 
-    let url;
-    switch (type) {
-      case 'ama':
-        url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
-        break;
-      case 'opencat':
-        url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
-        break;
-      case 'next':
-        url = nextUrl;
-        break;
-      default:
-        url = `sk-${key}`;
-    }
-    if (await copy(url)) {
-      showSuccess('已复制到剪贴板!');
-    } else {
-      showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
-      setSearchKeyword(url);
-    }
-  };
-
-  const onOpenLink = async (type, key) => {
-    let status = localStorage.getItem('status');
-    let serverAddress = '';
-    if (status) {
-      status = JSON.parse(status);
-      serverAddress = status.server_address;
-    }
-    if (serverAddress === '') {
-      serverAddress = window.location.origin;
-    }
-    let encodedServerAddress = encodeURIComponent(serverAddress);
-    const chatLink = localStorage.getItem('chat_link');
-    let defaultUrl;
-
-    if (chatLink) {
-      defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
-    } else {
-      defaultUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
-    }
-    let url;
-    switch (type) {
-      case 'ama':
-        url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
-        break;
-
-      case 'opencat':
-        url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
-        break;
-
-      default:
-        url = defaultUrl;
-    }
+    const onCopy = async (type, key) => {
+        let status = localStorage.getItem('status');
+        let serverAddress = '';
+        if (status) {
+            status = JSON.parse(status);
+            serverAddress = status.server_address;
+        }
+        if (serverAddress === '') {
+            serverAddress = window.location.origin;
+        }
+        let encodedServerAddress = encodeURIComponent(serverAddress);
+        const nextLink = localStorage.getItem('chat_link');
+        let nextUrl;
 
 
-    window.open(url, '_blank');
-  }
-
-  useEffect(() => {
-    loadTokens(0)
-      .then()
-      .catch((reason) => {
-        showError(reason);
-      });
-  }, []);
-
-  const manageToken = async (id, action, idx) => {
-    let data = { id };
-    let res;
-    switch (action) {
-      case 'delete':
-        res = await API.delete(`/api/token/${id}/`);
-        break;
-      case 'enable':
-        data.status = 1;
-        res = await API.put('/api/token/?status_only=true', data);
-        break;
-      case 'disable':
-        data.status = 2;
-        res = await API.put('/api/token/?status_only=true', data);
-        break;
-    }
-    const { success, message } = res.data;
-    if (success) {
-      showSuccess('操作成功完成!');
-      let token = res.data.data;
-      let newTokens = [...tokens];
-      let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
-      if (action === 'delete') {
-        newTokens[realIdx].deleted = true;
-      } else {
-        newTokens[realIdx].status = token.status;
-      }
-      setTokens(newTokens);
-    } else {
-      showError(message);
-    }
-  };
-
-  const searchTokens = async () => {
-    if (searchKeyword === '') {
-      // if keyword is blank, load files instead.
-      await loadTokens(0);
-      setActivePage(1);
-      return;
-    }
-    setSearching(true);
-    const res = await API.get(`/api/token/search?keyword=${searchKeyword}`);
-    const { success, message, data } = res.data;
-    if (success) {
-      setTokens(data);
-      setActivePage(1);
-    } else {
-      showError(message);
+        if (nextLink) {
+            nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
+        } else {
+            nextUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
+        }
+
+        let url;
+        switch (type) {
+            case 'ama':
+                url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
+                break;
+            case 'opencat':
+                url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
+                break;
+            case 'next':
+                url = nextUrl;
+                break;
+            default:
+                url = `sk-${key}`;
+        }
+        // if (await copy(url)) {
+        //     showSuccess('已复制到剪贴板!');
+        // } else {
+        //     showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
+        //     setSearchKeyword(url);
+        // }
+    };
+
+    const copyText = async (text) => {
+        if (await copy(text)) {
+            showSuccess('已复制到剪贴板!');
+        } else {
+            // setSearchKeyword(text);
+            Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
+        }
     }
     }
-    setSearching(false);
-  };
-
-  const handleKeywordChange = async (e, { value }) => {
-    setSearchKeyword(value.trim());
-  };
-
-  const sortToken = (key) => {
-    if (tokens.length === 0) return;
-    setLoading(true);
-    let sortedTokens = [...tokens];
-    sortedTokens.sort((a, b) => {
-      return ('' + a[key]).localeCompare(b[key]);
-    });
-    if (sortedTokens[0].id === tokens[0].id) {
-      sortedTokens.reverse();
+
+    const onOpenLink = async (type, key) => {
+        let status = localStorage.getItem('status');
+        let serverAddress = '';
+        if (status) {
+            status = JSON.parse(status);
+            serverAddress = status.server_address;
+        }
+        if (serverAddress === '') {
+            serverAddress = window.location.origin;
+        }
+        let encodedServerAddress = encodeURIComponent(serverAddress);
+        const chatLink = localStorage.getItem('chat_link');
+        let defaultUrl;
+
+        if (chatLink) {
+            defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
+        } else {
+            defaultUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
+        }
+        let url;
+        switch (type) {
+            case 'ama':
+                url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
+                break;
+
+            case 'opencat':
+                url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
+                break;
+
+            default:
+                url = defaultUrl;
+        }
+
+        window.open(url, '_blank');
     }
     }
-    setTokens(sortedTokens);
-    setLoading(false);
-  };
-
-  return (
-    <>
-      <Form onSubmit={searchTokens}>
-        <Form.Input
-          icon='search'
-          fluid
-          iconPosition='left'
-          placeholder='搜索令牌的名称 ...'
-          value={searchKeyword}
-          loading={searching}
-          onChange={handleKeywordChange}
-        />
-      </Form>
-
-      <Table basic compact size='small'>
-        <Table.Header>
-          <Table.Row>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortToken('name');
-              }}
-            >
-              名称
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortToken('status');
-              }}
-            >
-              状态
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortToken('used_quota');
-              }}
-            >
-              已用额度
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortToken('remain_quota');
-              }}
-            >
-              剩余额度
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortToken('created_time');
-              }}
-            >
-              创建时间
-            </Table.HeaderCell>
-            <Table.HeaderCell
-              style={{ cursor: 'pointer' }}
-              onClick={() => {
-                sortToken('expired_time');
-              }}
-            >
-              过期时间
-            </Table.HeaderCell>
-            <Table.HeaderCell>操作</Table.HeaderCell>
-          </Table.Row>
-        </Table.Header>
-
-        <Table.Body>
-          {tokens
-            .slice(
-              (activePage - 1) * ITEMS_PER_PAGE,
-              activePage * ITEMS_PER_PAGE
-            )
-            .map((token, idx) => {
-              if (token.deleted) return <></>;
-              return (
-                <Table.Row key={token.id}>
-                  <Table.Cell>{token.name ? token.name : '无'}</Table.Cell>
-                  <Table.Cell>{renderStatus(token.status)}</Table.Cell>
-                  <Table.Cell>{renderQuota(token.used_quota)}</Table.Cell>
-                  <Table.Cell>{token.unlimited_quota ? '无限制' : renderQuota(token.remain_quota, 2)}</Table.Cell>
-                  <Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell>
-                  <Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
-                  <Table.Cell>
-                    <div>
-                    <Button.Group color='green' size={'small'}>
-                        <Button
-                          size={'small'}
-                          positive
-                          onClick={async () => {
-                            await onCopy('', token.key);
-                          }}
-                        >
-                          复制
-                        </Button>
-                        <Dropdown
-                          className='button icon'
-                          floating
-                          options={COPY_OPTIONS.map(option => ({
-                            ...option,
-                            onClick: async () => {
-                              await onCopy(option.value, token.key);
-                            }
-                          }))}
-                          trigger={<></>}
-                        />
-                      </Button.Group>
-                      {' '}
-                      <Button.Group color='blue' size={'small'}>
-                        <Button
-                            size={'small'}
-                            positive
-                            onClick={() => {
-                              onOpenLink('', token.key);
-                            }}>
-                            聊天
-                          </Button>
-                          <Dropdown
-                            className="button icon"
-                            floating
-                            options={OPEN_LINK_OPTIONS.map(option => ({
-                              ...option,
-                              onClick: async () => {
-                                await onOpenLink(option.value, token.key);
-                              }
-                            }))}
-                            trigger={<></>}
-                          />
-                      </Button.Group>
-                      {' '}
-                      <Popup
-                        trigger={
-                          <Button
-                              size={'small'}
-                              positive
-                              onClick={async () => {
-                                let key = "sk-" + token.key;
-                                if (await copy(key)) {
-                                  showSuccess('已复制到剪贴板!');
-                                } else {
-                                  showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
-                                  setSearchKeyword(key);
-                                }
-                              }}
-                          >
-                            复制
-                          </Button>
-                        }
-                        on={'hover'}
-                        content={"sk-" + token.key}
-                        />
-                      <Popup
-                        trigger={
-                          <Button size='small' negative>
-                            删除
-                          </Button>
-                        }
-                        on='click'
-                        flowing
-                        hoverable
-                      >
-                        <Button
-                          negative
-                          onClick={() => {
-                            manageToken(token.id, 'delete', idx);
-                          }}
-                        >
-                          删除令牌 {token.name}
-                        </Button>
-                      </Popup>
-                      <Button
-                        size={'small'}
-                        onClick={() => {
-                          manageToken(
-                            token.id,
-                            token.status === 1 ? 'disable' : 'enable',
-                            idx
-                          );
-                        }}
-                      >
-                        {token.status === 1 ? '禁用' : '启用'}
-                      </Button>
-                      <Button
-                        size={'small'}
-                        as={Link}
-                        to={'/token/edit/' + token.id}
-                      >
-                        编辑
-                      </Button>
-                    </div>
-                  </Table.Cell>
-                </Table.Row>
-              );
-            })}
-        </Table.Body>
-
-        <Table.Footer>
-          <Table.Row>
-            <Table.HeaderCell colSpan='7'>
-              <Button size='small' as={Link} to='/token/add' loading={loading}>
-                添加新的令牌
-              </Button>
-              <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
-              <Pagination
-                floated='right'
-                activePage={activePage}
-                onPageChange={onPaginationChange}
-                size='small'
-                siblingRange={1}
-                totalPages={
-                  Math.ceil(tokens.length / ITEMS_PER_PAGE) +
-                  (tokens.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
+
+    useEffect(() => {
+        loadTokens(0)
+            .then()
+            .catch((reason) => {
+                showError(reason);
+            });
+    }, []);
+
+    const removeRecord = key => {
+        let newDataSource = [...tokens];
+        if (key != null) {
+            let idx = newDataSource.findIndex(data => data.key === key);
+
+            if (idx > -1) {
+                newDataSource.splice(idx, 1);
+                setTokensFormat(newDataSource);
+            }
+        }
+    };
+
+    const manageToken = async (id, action, record) => {
+        setLoading(true);
+        let data = {id};
+        let res;
+        switch (action) {
+            case 'delete':
+                res = await API.delete(`/api/token/${id}/`);
+                break;
+            case 'enable':
+                data.status = 1;
+                res = await API.put('/api/token/?status_only=true', data);
+                break;
+            case 'disable':
+                data.status = 2;
+                res = await API.put('/api/token/?status_only=true', data);
+                break;
+        }
+        const {success, message} = res.data;
+        if (success) {
+            showSuccess('操作成功完成!');
+            let token = res.data.data;
+            let newTokens = [...tokens];
+            // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
+            if (action === 'delete') {
+
+            } else {
+                record.status = token.status;
+                // newTokens[realIdx].status = token.status;
+            }
+            setTokensFormat(newTokens);
+        } else {
+            showError(message);
+        }
+        setLoading(false);
+    };
+
+    const searchTokens = async () => {
+        if (searchKeyword === '') {
+            // if keyword is blank, load files instead.
+            await loadTokens(0);
+            setActivePage(1);
+            return;
+        }
+        setSearching(true);
+        const res = await API.get(`/api/token/search?keyword=${searchKeyword}`);
+        const {success, message, data} = res.data;
+        if (success) {
+            setTokensFormat(data);
+            setActivePage(1);
+        } else {
+            showError(message);
+        }
+        setSearching(false);
+    };
+
+    const handleKeywordChange = async (value) => {
+        setSearchKeyword(value.trim());
+    };
+
+    const sortToken = (key) => {
+        if (tokens.length === 0) return;
+        setLoading(true);
+        let sortedTokens = [...tokens];
+        sortedTokens.sort((a, b) => {
+            return ('' + a[key]).localeCompare(b[key]);
+        });
+        if (sortedTokens[0].id === tokens[0].id) {
+            sortedTokens.reverse();
+        }
+        setTokens(sortedTokens);
+        setLoading(false);
+    };
+
+
+    const handlePageChange = page => {
+        setActivePage(page);
+        if (page === Math.ceil(tokens.length / ITEMS_PER_PAGE) + 1) {
+            // In this case we have to load more data and then append them.
+            loadTokens(page - 1).then(r => {
+            });
+        }
+    };
+
+    const rowSelection = {
+        onSelect: (record, selected) => {
+            // console.log(`select row: ${selected}`, record);
+        },
+        onSelectAll: (selected, selectedRows) => {
+            // console.log(`select all rows: ${selected}`, selectedRows);
+        },
+        onChange: (selectedRowKeys, selectedRows) => {
+            // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
+            setSelectedKeys(selectedRows);
+        },
+    };
+
+    return (
+        <>
+            <Form layout='horizontal' style={{marginTop: 10}} labelPosition={'left'}>
+                <Form.Input
+                    field="keyword"
+                    label='搜索关键字'
+                    placeholder='令牌名称'
+                    value={searchKeyword}
+                    loading={searching}
+                    onChange={handleKeywordChange}
+                />
+                <Button label='查询' type="primary" htmlType="submit" className="btn-margin-right"
+                        onClick={searchTokens} style={{marginRight: 8}}>查询</Button>
+            </Form>
+
+            <Table style={{marginTop: 20}} columns={columns} dataSource={tokens} pagination={{
+                currentPage: activePage,
+                pageSize: ITEMS_PER_PAGE,
+                total: tokenCount,
+                pageSizeOpts: [10, 20, 50, 100],
+                onPageChange: handlePageChange,
+            }} loading={loading} rowSelection={rowSelection}>
+            </Table>
+            <Button theme='light' type='primary' style={{marginRight: 8}}>添加令牌</Button>
+            <Button label='复制所选令牌' type="warning" onClick={
+                async () => {
+                    let keys = "";
+                    for (let i = 0; i < selectedKeys.length; i++) {
+                        keys += selectedKeys[i].name + "    sk-" + selectedKeys[i].key + "\n";
+                    }
+                    await copyText(keys);
                 }
                 }
-              />
-            </Table.HeaderCell>
-          </Table.Row>
-        </Table.Footer>
-      </Table>
-    </>
-  );
+            }>复制所选令牌到剪贴板</Button>
+        </>
+    );
 };
 };
 
 
 export default TokensTable;
 export default TokensTable;

+ 12 - 11
web/src/helpers/utils.js

@@ -1,6 +1,7 @@
-import { toast } from 'react-toastify';
+import { Toast } from '@douyinfe/semi-ui';
 import { toastConstants } from '../constants';
 import { toastConstants } from '../constants';
 import React from 'react';
 import React from 'react';
+import {toast} from "react-toastify";
 
 
 const HTMLToastContent = ({ htmlContent }) => {
 const HTMLToastContent = ({ htmlContent }) => {
   return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
   return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
@@ -81,42 +82,42 @@ export function showError(error) {
           window.location.href = '/login?expired=true';
           window.location.href = '/login?expired=true';
           break;
           break;
         case 429:
         case 429:
-          toast.error('错误:请求次数过多,请稍后再试!', showErrorOptions);
+          Toast.error('错误:请求次数过多,请稍后再试!');
           break;
           break;
         case 500:
         case 500:
-          toast.error('错误:服务器内部错误,请联系管理员!', showErrorOptions);
+          Toast.error('错误:服务器内部错误,请联系管理员!');
           break;
           break;
         case 405:
         case 405:
-          toast.info('本站仅作演示之用,无服务端!');
+          Toast.info('本站仅作演示之用,无服务端!');
           break;
           break;
         default:
         default:
-          toast.error('错误:' + error.message, showErrorOptions);
+          Toast.error('错误:' + error.message);
       }
       }
       return;
       return;
     }
     }
-    toast.error('错误:' + error.message, showErrorOptions);
+    Toast.error('错误:' + error.message);
   } else {
   } else {
-    toast.error('错误:' + error, showErrorOptions);
+    Toast.error('错误:' + error);
   }
   }
 }
 }
 
 
 export function showWarning(message) {
 export function showWarning(message) {
-  toast.warn(message, showWarningOptions);
+  Toast.warning(message);
 }
 }
 
 
 export function showSuccess(message) {
 export function showSuccess(message) {
-  toast.success(message, showSuccessOptions);
+  Toast.success(message);
 }
 }
 
 
 export function showInfo(message) {
 export function showInfo(message) {
-  toast.info(message, showInfoOptions);
+  Toast.info(message);
 }
 }
 
 
 export function showNotice(message, isHTML = false) {
 export function showNotice(message, isHTML = false) {
   if (isHTML) {
   if (isHTML) {
     toast(<HTMLToastContent htmlContent={message} />, showNoticeOptions);
     toast(<HTMLToastContent htmlContent={message} />, showNoticeOptions);
   } else {
   } else {
-    toast.info(message, showNoticeOptions);
+    Toast.info(message);
   }
   }
 }
 }
 
 

+ 10 - 6
web/src/pages/Token/index.js

@@ -1,13 +1,17 @@
 import React from 'react';
 import React from 'react';
-import { Segment, Header } from 'semantic-ui-react';
 import TokensTable from '../../components/TokensTable';
 import TokensTable from '../../components/TokensTable';
-
+import {Layout} from "@douyinfe/semi-ui";
+const {Content, Header} = Layout;
 const Token = () => (
 const Token = () => (
   <>
   <>
-    <Segment>
-      <Header as='h3'>我的令牌</Header>
-      <TokensTable/>
-    </Segment>
+    <Layout>
+      <Header>
+          <h3>我的令牌</h3>
+      </Header>
+      <Content>
+          <TokensTable/>
+      </Content>
+    </Layout>
   </>
   </>
 );
 );