mirror of
https://github.com/linux-do/new-api.git
synced 2025-09-17 16:06:38 +08:00
完善令牌界面
This commit is contained in:
parent
f98dd8d693
commit
0a945c160d
11
README.md
11
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
|
@ -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接口应用,释放商业价值"/>
|
content="NekoAPI,企业级AI接口调用平台,专为企业级需求打造,提供高性能、高并发、高可用的服务,一站式处理大规模数据和复杂任务。我们的稳定高并发处理能力和高可用性保证您的业务流畅运行,结合OpenAI、Claude、Midjourney等AI接口和专业的技术支持,为您的企业快速部署和实现AI接口应用,释放商业价值"/>
|
||||||
<title>Neko API</title>
|
|
||||||
<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>
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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: '日志',
|
||||||
|
@ -1,464 +1,460 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
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 { Link } from 'react-router-dom';
|
import {API, copy, isAdmin, showError, showSuccess, showWarning, timestamp2string} from '../helpers';
|
||||||
import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers';
|
|
||||||
|
|
||||||
import { ITEMS_PER_PAGE } from '../constants';
|
import {ITEMS_PER_PAGE} from '../constants';
|
||||||
import { renderQuota } from '../helpers/render';
|
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: 'next', text: 'ChatGPT Next Web', value: 'next'},
|
||||||
{ key: 'ama', text: 'AMA 问天', value: 'ama' },
|
{key: 'ama', text: 'AMA 问天', value: 'ama'},
|
||||||
{ key: 'opencat', text: 'OpenCat', value: 'opencat' },
|
{key: 'opencat', text: 'OpenCat', value: 'opencat'},
|
||||||
];
|
];
|
||||||
|
|
||||||
const OPEN_LINK_OPTIONS = [
|
const OPEN_LINK_OPTIONS = [
|
||||||
{ key: 'ama', text: 'AMA 问天', value: 'ama' },
|
{key: 'ama', text: 'AMA 问天', value: 'ama'},
|
||||||
{ key: 'opencat', text: 'OpenCat', value: 'opencat' },
|
{key: 'opencat', text: 'OpenCat', value: 'opencat'},
|
||||||
];
|
];
|
||||||
|
|
||||||
function renderTimestamp(timestamp) {
|
function renderTimestamp(timestamp) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{timestamp2string(timestamp)}
|
{timestamp2string(timestamp)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStatus(status) {
|
function renderStatus(status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 1:
|
case 1:
|
||||||
return <Label basic color='green'>已启用</Label>;
|
return <Tag color='green' size='large'>已启用</Tag>;
|
||||||
case 2:
|
case 2:
|
||||||
return <Label basic color='red'> 已禁用 </Label>;
|
return <Tag color='red' size='large'> 已禁用 </Tag>;
|
||||||
case 3:
|
case 3:
|
||||||
return <Label basic color='yellow'> 已过期 </Label>;
|
return <Tag color='yellow' size='large'> 已过期 </Tag>;
|
||||||
case 4:
|
case 4:
|
||||||
return <Label basic color='grey'> 已耗尽 </Label>;
|
return <Tag color='grey' size='large'> 已耗尽 </Tag>;
|
||||||
default:
|
default:
|
||||||
return <Label basic color='black'> 未知状态 </Label>;
|
return <Tag color='black' size='large'> 未知状态 </Tag>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TokensTable = () => {
|
const TokensTable = () => {
|
||||||
const [tokens, setTokens] = useState([]);
|
const columns = [
|
||||||
const [loading, setLoading] = useState(true);
|
{
|
||||||
const [activePage, setActivePage] = useState(1);
|
title: '名称',
|
||||||
const [searchKeyword, setSearchKeyword] = useState('');
|
dataIndex: 'name',
|
||||||
const [searching, setSearching] = useState(false);
|
},
|
||||||
const [showTopUpModal, setShowTopUpModal] = useState(false);
|
{
|
||||||
const [targetTokenIdx, setTargetTokenIdx] = useState(0);
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
const loadTokens = async (startIdx) => {
|
key: 'status',
|
||||||
const res = await API.get(`/api/token/?p=${startIdx}`);
|
render: (text, record, index) => {
|
||||||
const { success, message, data } = res.data;
|
return (
|
||||||
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);
|
|
||||||
}
|
|
||||||
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 () => {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
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>
|
<div>
|
||||||
<Button.Group color='green' size={'small'}>
|
{renderStatus(text)}
|
||||||
<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>
|
</div>
|
||||||
</Table.Cell>
|
);
|
||||||
</Table.Row>
|
},
|
||||||
);
|
},
|
||||||
})}
|
{
|
||||||
</Table.Body>
|
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>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
<Table.Footer>
|
const [tokens, setTokens] = useState([]);
|
||||||
<Table.Row>
|
const [selectedKeys, setSelectedKeys] = useState([]);
|
||||||
<Table.HeaderCell colSpan='7'>
|
const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE);
|
||||||
<Button size='small' as={Link} to='/token/add' loading={loading}>
|
const [loading, setLoading] = useState(true);
|
||||||
添加新的令牌
|
const [activePage, setActivePage] = useState(1);
|
||||||
</Button>
|
const [searchKeyword, setSearchKeyword] = useState('');
|
||||||
<Button size='small' onClick={refresh} loading={loading}>刷新</Button>
|
const [searching, setSearching] = useState(false);
|
||||||
<Pagination
|
const [showTopUpModal, setShowTopUpModal] = useState(false);
|
||||||
floated='right'
|
const [targetTokenIdx, setTargetTokenIdx] = useState(0);
|
||||||
activePage={activePage}
|
|
||||||
onPageChange={onPaginationChange}
|
const setTokensFormat = (tokens) => {
|
||||||
size='small'
|
setTokens(tokens);
|
||||||
siblingRange={1}
|
if (tokens.length === ITEMS_PER_PAGE) {
|
||||||
totalPages={
|
setTokenCount(tokens.length + ITEMS_PER_PAGE);
|
||||||
Math.ceil(tokens.length / ITEMS_PER_PAGE) +
|
} else {
|
||||||
(tokens.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
|
setTokenCount(tokens.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 copyText = async (text) => {
|
||||||
|
if (await copy(text)) {
|
||||||
|
showSuccess('已复制到剪贴板!');
|
||||||
|
} else {
|
||||||
|
// setSearchKeyword(text);
|
||||||
|
Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
/>
|
}>复制所选令牌到剪贴板</Button>
|
||||||
</Table.HeaderCell>
|
</>
|
||||||
</Table.Row>
|
);
|
||||||
</Table.Footer>
|
|
||||||
</Table>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TokensTable;
|
export default TokensTable;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
<Layout>
|
||||||
<Header as='h3'>我的令牌</Header>
|
<Header>
|
||||||
<TokensTable/>
|
<h3>我的令牌</h3>
|
||||||
</Segment>
|
</Header>
|
||||||
|
<Content>
|
||||||
|
<TokensTable/>
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user