Merge branch 'songquanpeng:main' into refactor-main

This commit is contained in:
ckt 2023-08-07 09:26:22 +08:00 committed by GitHub
commit 5826ffa66c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 275 additions and 64 deletions

View File

@ -173,6 +173,10 @@ If you encounter a blank page after deployment, refer to [#97](https://github.co
<summary><strong>Deploy on Sealos</strong></summary> <summary><strong>Deploy on Sealos</strong></summary>
<div> <div>
> Sealos supports high concurrency, dynamic scaling, and stable operations for millions of users.
> Click the button below to deploy with one click.👇
[![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api) [![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api)

View File

@ -104,7 +104,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
如果上面的镜像无法拉取,可以尝试使用 GitHub 的 Docker 镜像,将上面的 `justsong/one-api` 替换为 `ghcr.io/songquanpeng/one-api` 即可。 如果上面的镜像无法拉取,可以尝试使用 GitHub 的 Docker 镜像,将上面的 `justsong/one-api` 替换为 `ghcr.io/songquanpeng/one-api` 即可。
如果你的并发量较大,推荐设置 `SQL_DSN`,详见下面[环境变量](#环境变量)一节。 如果你的并发量较大,**务必**设置 `SQL_DSN`,详见下面[环境变量](#环境变量)一节。
更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR` 更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR`
@ -211,9 +211,11 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
<summary><strong>部署到 Sealos </strong></summary> <summary><strong>部署到 Sealos </strong></summary>
<div> <div>
> Sealos 可视化一键部署 > Sealos 的服务器在国外,不需要额外处理网络问题,支持高并发 & 动态伸缩
[![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api) 点击以下按钮一键部署(部署后访问出现 404 请等待 3~5 分钟):
[![Deploy-on-Sealos.svg](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api)
</div> </div>
</details> </details>

View File

@ -111,7 +111,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O
return nil, responseText return nil, responseText
} }
func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*OpenAIErrorWithStatusCode, *Usage) { func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool, promptTokens int, model string) (*OpenAIErrorWithStatusCode, *Usage) {
var textResponse TextResponse var textResponse TextResponse
if consumeQuota { if consumeQuota {
responseBody, err := io.ReadAll(resp.Body) responseBody, err := io.ReadAll(resp.Body)
@ -151,5 +151,17 @@ func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*Ope
if err != nil { if err != nil {
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
} }
if textResponse.Usage.TotalTokens == 0 {
completionTokens := 0
for _, choice := range textResponse.Choices {
completionTokens += countTokenText(choice.Message.Content, model)
}
textResponse.Usage = Usage{
PromptTokens: promptTokens,
CompletionTokens: completionTokens,
TotalTokens: promptTokens + completionTokens,
}
}
return nil, &textResponse.Usage return nil, &textResponse.Usage
} }

View File

@ -302,7 +302,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
if err != nil { if err != nil {
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
} }
isStream = strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") isStream = isStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return errorWrapper( return errorWrapper(
@ -366,7 +366,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model) textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
return nil return nil
} else { } else {
err, usage := openaiHandler(c, resp, consumeQuota) err, usage := openaiHandler(c, resp, consumeQuota, promptTokens, textRequest.Model)
if err != nil { if err != nil {
return err return err
} }

View File

@ -63,7 +63,6 @@ type XunfeiChatResponse struct {
Seq int `json:"seq"` Seq int `json:"seq"`
Text []XunfeiChatResponseTextItem `json:"text"` Text []XunfeiChatResponseTextItem `json:"text"`
} `json:"choices"` } `json:"choices"`
} `json:"payload"`
Usage struct { Usage struct {
//Text struct { //Text struct {
// QuestionTokens string `json:"question_tokens"` // QuestionTokens string `json:"question_tokens"`
@ -73,6 +72,7 @@ type XunfeiChatResponse struct {
//} `json:"text"` //} `json:"text"`
Text Usage `json:"text"` Text Usage `json:"text"`
} `json:"usage"` } `json:"usage"`
} `json:"payload"`
} }
func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string) *XunfeiChatRequest { func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string) *XunfeiChatRequest {
@ -123,7 +123,7 @@ func responseXunfei2OpenAI(response *XunfeiChatResponse) *OpenAITextResponse {
Object: "chat.completion", Object: "chat.completion",
Created: common.GetTimestamp(), Created: common.GetTimestamp(),
Choices: []OpenAITextResponseChoice{choice}, Choices: []OpenAITextResponseChoice{choice},
Usage: response.Usage.Text, Usage: response.Payload.Usage.Text,
} }
return &fullTextResponse return &fullTextResponse
} }
@ -222,9 +222,9 @@ func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId
c.Stream(func(w io.Writer) bool { c.Stream(func(w io.Writer) bool {
select { select {
case xunfeiResponse := <-dataChan: case xunfeiResponse := <-dataChan:
usage.PromptTokens += xunfeiResponse.Usage.Text.PromptTokens usage.PromptTokens += xunfeiResponse.Payload.Usage.Text.PromptTokens
usage.CompletionTokens += xunfeiResponse.Usage.Text.CompletionTokens usage.CompletionTokens += xunfeiResponse.Payload.Usage.Text.CompletionTokens
usage.TotalTokens += xunfeiResponse.Usage.Text.TotalTokens usage.TotalTokens += xunfeiResponse.Payload.Usage.Text.TotalTokens
response := streamResponseXunfei2OpenAI(&xunfeiResponse) response := streamResponseXunfei2OpenAI(&xunfeiResponse)
jsonResponse, err := json.Marshal(response) jsonResponse, err := json.Marshal(response)
if err != nil { if err != nil {

View File

@ -82,6 +82,7 @@ type OpenAIErrorWithStatusCode struct {
} }
type TextResponse struct { type TextResponse struct {
Choices []OpenAITextResponseChoice `json:"choices"`
Usage `json:"usage"` Usage `json:"usage"`
Error OpenAIError `json:"error"` Error OpenAIError `json:"error"`
} }

View File

@ -3,6 +3,11 @@
"%d 点额度": "%d point quota", "%d 点额度": "%d point quota",
"尚未实现": "Not yet implemented", "尚未实现": "Not yet implemented",
"余额不足": "Insufficient balance", "余额不足": "Insufficient balance",
"危险操作": "Hazardous operations",
"输入你的账户名": "Enter your account name",
"确认删除": "Confirm Delete",
"确认绑定": "Confirm Binding",
"您正在删除自己的帐户,将清空所有数据且不可恢复": "You are deleting your account, all data will be cleared and unrecoverable.",
"\"通道「%s」#%d已被禁用\"": "\"Channel %s (#%d) has been disabled\"", "\"通道「%s」#%d已被禁用\"": "\"Channel %s (#%d) has been disabled\"",
"通道「%s」#%d已被禁用原因%s": "Channel %s (#%d) has been disabled, reason: %s", "通道「%s」#%d已被禁用原因%s": "Channel %s (#%d) has been disabled, reason: %s",
"测试已在运行中": "Test is already running", "测试已在运行中": "Test is already running",
@ -427,7 +432,7 @@
"一分钟后过期": "Expires after one minute", "一分钟后过期": "Expires after one minute",
"创建新的令牌": "Create New Token", "创建新的令牌": "Create New Token",
"注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。": "Note that the quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account.", "注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。": "Note that the quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account.",
"设为无限额度": "Set to unlimited quota", "设为无限额度": "Set to unlimited quota",
"更新令牌信息": "Update Token Information", "更新令牌信息": "Update Token Information",
"请输入充值码!": "Please enter the recharge code!", "请输入充值码!": "Please enter the recharge code!",
"请输入名称": "Please enter a name", "请输入名称": "Please enter a name",
@ -493,6 +498,7 @@
"参数替换为你的部署名称(模型名称中的点会被剔除)": "Replace the parameter with your deployment name (dots in the model name will be removed)", "参数替换为你的部署名称(模型名称中的点会被剔除)": "Replace the parameter with your deployment name (dots in the model name will be removed)",
"模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!", "模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
"取消无限额度": "Cancel unlimited quota", "取消无限额度": "Cancel unlimited quota",
"取消": "Cancel",
"请输入新的剩余额度": "Please enter the new remaining quota", "请输入新的剩余额度": "Please enter the new remaining quota",
"请输入单个兑换码中包含的额度": "Please enter the quota included in a single redemption code", "请输入单个兑换码中包含的额度": "Please enter the quota included in a single redemption code",
"请输入用户名": "Please enter username", "请输入用户名": "Please enter username",
@ -591,5 +597,7 @@
"请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel", "请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel",
"注意,": "Note that, ", "注意,": "Note that, ",
",图片演示。": "related image demo.", ",图片演示。": "related image demo.",
"令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!" "令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!",
"代理": "Proxy",
"此项可选,用于通过代理站来进行 API 调用请输入代理站地址格式为https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com"
} }

View File

@ -38,7 +38,7 @@ func SetApiRouter(router *gin.Engine) {
{ {
selfRoute.GET("/self", controller.GetSelf) selfRoute.GET("/self", controller.GetSelf)
selfRoute.PUT("/self", controller.UpdateSelf) selfRoute.PUT("/self", controller.UpdateSelf)
selfRoute.DELETE("/self", middleware.TurnstileCheck(), controller.DeleteSelf) selfRoute.DELETE("/self", controller.DeleteSelf)
selfRoute.GET("/token", controller.GenerateAccessToken) selfRoute.GET("/token", controller.GenerateAccessToken)
selfRoute.GET("/aff", controller.GetAffCode) selfRoute.GET("/aff", controller.GetAffCode)
selfRoute.POST("/topup", controller.TopUp) selfRoute.POST("/topup", controller.TopUp)

View File

@ -25,6 +25,8 @@ const PersonalSetting = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [disableButton, setDisableButton] = useState(false); const [disableButton, setDisableButton] = useState(false);
const [countdown, setCountdown] = useState(30); const [countdown, setCountdown] = useState(30);
const [affLink, setAffLink] = useState("");
const [systemToken, setSystemToken] = useState("");
useEffect(() => { useEffect(() => {
let status = localStorage.getItem('status'); let status = localStorage.getItem('status');
@ -59,8 +61,10 @@ const PersonalSetting = () => {
const res = await API.get('/api/user/token'); const res = await API.get('/api/user/token');
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
setSystemToken(data);
setAffLink("");
await copy(data); await copy(data);
showSuccess(`令牌已重置并已复制到剪贴板:${data}`); showSuccess(`令牌已重置并已复制到剪贴板`);
} else { } else {
showError(message); showError(message);
} }
@ -71,13 +75,27 @@ const PersonalSetting = () => {
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
let link = `${window.location.origin}/register?aff=${data}`; let link = `${window.location.origin}/register?aff=${data}`;
setAffLink(link);
setSystemToken("");
await copy(link); await copy(link);
showNotice(`邀请链接已复制到剪切板:${link}`); showSuccess(`邀请链接已复制到剪切板`);
} else { } else {
showError(message); showError(message);
} }
}; };
const handleAffLinkClick = async (e) => {
e.target.select();
await copy(e.target.value);
showSuccess(`邀请链接已复制到剪切板`);
};
const handleSystemTokenClick = async (e) => {
e.target.select();
await copy(e.target.value);
showSuccess(`系统令牌已复制到剪切板`);
};
const deleteAccount = async () => { const deleteAccount = async () => {
if (inputs.self_account_deletion_confirmation !== userState.user.username) { if (inputs.self_account_deletion_confirmation !== userState.user.username) {
showError('请输入你的账户名以确认删除!'); showError('请输入你的账户名以确认删除!');
@ -180,6 +198,25 @@ const PersonalSetting = () => {
<Button onClick={() => { <Button onClick={() => {
setShowAccountDeleteModal(true); setShowAccountDeleteModal(true);
}}>删除个人账户</Button> }}>删除个人账户</Button>
{systemToken && (
<Form.Input
fluid
readOnly
value={systemToken}
onClick={handleSystemTokenClick}
style={{ marginTop: '10px' }}
/>
)}
{affLink && (
<Form.Input
fluid
readOnly
value={affLink}
onClick={handleAffLinkClick}
style={{ marginTop: '10px' }}
/>
)}
<Divider /> <Divider />
<Header as='h3'>账号绑定</Header> <Header as='h3'>账号绑定</Header>
{ {
@ -285,6 +322,7 @@ const PersonalSetting = () => {
) : ( ) : (
<></> <></>
)} )}
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
<Button <Button
color='' color=''
fluid fluid
@ -292,8 +330,17 @@ const PersonalSetting = () => {
onClick={bindEmail} onClick={bindEmail}
loading={loading} loading={loading}
> >
绑定 确认绑定
</Button> </Button>
<div style={{ width: '1rem' }}></div>
<Button
fluid
size='large'
onClick={() => setShowEmailBindModal(false)}
>
取消
</Button>
</div>
</Form> </Form>
</Modal.Description> </Modal.Description>
</Modal.Content> </Modal.Content>
@ -305,8 +352,9 @@ const PersonalSetting = () => {
size={'tiny'} size={'tiny'}
style={{ maxWidth: '450px' }} style={{ maxWidth: '450px' }}
> >
<Modal.Header>确认删除自己的帐户</Modal.Header> <Modal.Header>危险操作</Modal.Header>
<Modal.Content> <Modal.Content>
<Message>您正在删除自己的帐户将清空所有数据且不可恢复</Message>
<Modal.Description> <Modal.Description>
<Form size='large'> <Form size='large'>
<Form.Input <Form.Input
@ -326,6 +374,7 @@ const PersonalSetting = () => {
) : ( ) : (
<></> <></>
)} )}
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
<Button <Button
color='red' color='red'
fluid fluid
@ -333,8 +382,17 @@ const PersonalSetting = () => {
onClick={deleteAccount} onClick={deleteAccount}
loading={loading} loading={loading}
> >
删除 确认删除
</Button> </Button>
<div style={{ width: '1rem' }}></div>
<Button
fluid
size='large'
onClick={() => setShowAccountDeleteModal(false)}
>
取消
</Button>
</div>
</Form> </Form>
</Modal.Description> </Modal.Description>
</Modal.Content> </Modal.Content>

View File

@ -1,11 +1,22 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Label, Modal, Pagination, Popup, Table } from 'semantic-ui-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, 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 } from '../helpers/render';
const COPY_OPTIONS = [
{ key: 'next', text: 'ChatGPT Next Web', value: 'next' },
{ key: 'ama', text: 'AMA 问天', value: 'ama' },
{ key: 'opencat', text: 'OpenCat', value: 'opencat' },
];
const OPEN_LINK_OPTIONS = [
{ key: 'ama', text: 'AMA 问天', value: 'ama' },
{ key: 'opencat', text: 'OpenCat', value: 'opencat' },
];
function renderTimestamp(timestamp) { function renderTimestamp(timestamp) {
return ( return (
<> <>
@ -68,6 +79,84 @@ const TokensTable = () => {
const refresh = async () => { const refresh = async () => {
setLoading(true); setLoading(true);
await loadTokens(activePage - 1); 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}"}`;
} 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}"}`;
} 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(() => { useEffect(() => {
@ -235,21 +324,51 @@ const TokensTable = () => {
<Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell> <Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
<Table.Cell> <Table.Cell>
<div> <div>
<Button.Group color='green' size={'small'}>
<Button <Button
size={'small'} size={'small'}
positive positive
onClick={async () => { onClick={async () => {
let key = "sk-" + token.key; await onCopy('', token.key);
if (await copy(key)) {
showSuccess('已复制到剪贴板!');
} else {
showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
setSearchKeyword(key);
}
}} }}
> >
复制 复制
</Button> </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 <Popup
trigger={ trigger={
<Button size='small' negative> <Button size='small' negative>

View File

@ -401,9 +401,9 @@ const EditChannel = () => {
inputs.type !== 3 && inputs.type !== 8 && ( inputs.type !== 3 && inputs.type !== 8 && (
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='镜像' label='代理'
name='base_url' name='base_url'
placeholder={'此项可选,用于通过镜像站来进行 API 调用,请输入镜像站地址格式为https://domain.com'} placeholder={'此项可选,用于通过代理站来进行 API 调用,请输入代理站地址格式为https://domain.com'}
onChange={handleInputChange} onChange={handleInputChange}
value={inputs.base_url} value={inputs.base_url}
autoComplete='new-password' autoComplete='new-password'

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react'; import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
import { useParams } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { API, showError, showSuccess, timestamp2string } from '../../helpers'; import { API, showError, showSuccess, timestamp2string } from '../../helpers';
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
@ -17,11 +17,13 @@ const EditToken = () => {
}; };
const [inputs, setInputs] = useState(originInputs); const [inputs, setInputs] = useState(originInputs);
const { name, remain_quota, expired_time, unlimited_quota } = inputs; const { name, remain_quota, expired_time, unlimited_quota } = inputs;
const navigate = useNavigate();
const handleInputChange = (e, { name, value }) => { const handleInputChange = (e, { name, value }) => {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
}; };
const handleCancel = () => {
navigate("/token");
}
const setExpiredTime = (month, day, hour, minute) => { const setExpiredTime = (month, day, hour, minute) => {
let now = new Date(); let now = new Date();
let timestamp = now.getTime() / 1000; let timestamp = now.getTime() / 1000;
@ -150,8 +152,9 @@ const EditToken = () => {
</Form.Field> </Form.Field>
<Button type={'button'} onClick={() => { <Button type={'button'} onClick={() => {
setUnlimitedQuota(); setUnlimitedQuota();
}}>{unlimited_quota ? '取消无限额度' : '设置为无限额度'}</Button> }}>{unlimited_quota ? '取消无限额度' : '设为无限额度'}</Button>
<Button positive onClick={submit}>提交</Button> <Button floated='right' positive onClick={submit}>提交</Button>
<Button floated='right' onClick={handleCancel}>取消</Button>
</Form> </Form>
</Segment> </Segment>
</> </>

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Header, Segment } from 'semantic-ui-react'; import { Button, Form, Header, Segment } from 'semantic-ui-react';
import { useParams } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { API, showError, showSuccess } from '../../helpers'; import { API, showError, showSuccess } from '../../helpers';
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
@ -38,7 +38,10 @@ const EditUser = () => {
showError(error.message); showError(error.message);
} }
}; };
const navigate = useNavigate();
const handleCancel = () => {
navigate("/setting");
}
const loadUser = async () => { const loadUser = async () => {
let res = undefined; let res = undefined;
if (userId) { if (userId) {
@ -198,6 +201,7 @@ const EditUser = () => {
readOnly readOnly
/> />
</Form.Field> </Form.Field>
<Button onClick={handleCancel}>取消</Button>
<Button positive onClick={submit}>提交</Button> <Button positive onClick={submit}>提交</Button>
</Form> </Form>
</Segment> </Segment>