mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 05:43:42 +08:00 
			
		
		
		
	Compare commits
	
		
			9 Commits
		
	
	
		
			v0.5.0-alp
			...
			v0.5.0-alp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 3da119efba | ||
|  | dccd66b852 | ||
|  | 2fcd6852e0 | ||
|  | 9b4d1964d4 | ||
|  | 806bf8241c | ||
|  | ce93c9b6b2 | ||
|  | 4ec4289565 | ||
|  | 3dc5a0f91d | ||
|  | 80a846673a | 
| @@ -10,7 +10,7 @@ | |||||||
|  |  | ||||||
| # One API | # One API | ||||||
|  |  | ||||||
| _✨ An OpenAI key management & redistribution system, easy to deploy & use ✨_ | _✨ Access all LLM through the standard OpenAI API format, easy to deploy & use ✨_ | ||||||
|  |  | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										46
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								README.md
									
									
									
									
									
								
							| @@ -11,7 +11,7 @@ | |||||||
|  |  | ||||||
| # One API | # One API | ||||||
|  |  | ||||||
| _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用✨_ | _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 ✨_ | ||||||
|  |  | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| @@ -58,13 +58,13 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 | |||||||
| > **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 | > **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 | ||||||
|  |  | ||||||
| ## 功能 | ## 功能 | ||||||
| 1. 支持多种 API 访问渠道: | 1. 支持多种大模型: | ||||||
|    + [x] OpenAI 官方通道(支持配置镜像) |    + [x] [OpenAI ChatGPT 系列模型](https://platform.openai.com/docs/guides/gpt/chat-completions-api)(支持 [Azure OpenAI API](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference)) | ||||||
|    + [x] **Azure OpenAI API** |  | ||||||
|    + [x] [Anthropic Claude 系列模型](https://anthropic.com) |    + [x] [Anthropic Claude 系列模型](https://anthropic.com) | ||||||
|    + [x] [Google PaLM2 系列模型](https://developers.generativeai.google) |    + [x] [Google PaLM2 系列模型](https://developers.generativeai.google) | ||||||
|    + [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html) |    + [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html) | ||||||
|    + [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn) |    + [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn) | ||||||
|  | 2. 支持配置镜像以及众多第三方代理服务: | ||||||
|    + [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj) |    + [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj) | ||||||
|    + [x] [OpenAI-SB](https://openai-sb.com) |    + [x] [OpenAI-SB](https://openai-sb.com) | ||||||
|    + [x] [API2D](https://api2d.com/r/197971) |    + [x] [API2D](https://api2d.com/r/197971) | ||||||
| @@ -72,27 +72,27 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 | |||||||
|    + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`) |    + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`) | ||||||
|    + [x] [CloseAI](https://console.closeai-asia.com/r/2412) |    + [x] [CloseAI](https://console.closeai-asia.com/r/2412) | ||||||
|    + [x] 自定义渠道:例如各种未收录的第三方代理服务 |    + [x] 自定义渠道:例如各种未收录的第三方代理服务 | ||||||
| 2. 支持通过**负载均衡**的方式访问多个渠道。 | 3. 支持通过**负载均衡**的方式访问多个渠道。 | ||||||
| 3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 | 4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 | ||||||
| 4. 支持**多机部署**,[详见此处](#多机部署)。 | 5. 支持**多机部署**,[详见此处](#多机部署)。 | ||||||
| 5. 支持**令牌管理**,设置令牌的过期时间和额度。 | 6. 支持**令牌管理**,设置令牌的过期时间和额度。 | ||||||
| 6. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。 | 7. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。 | ||||||
| 7. 支持**通道管理**,批量创建通道。 | 8. 支持**通道管理**,批量创建通道。 | ||||||
| 8. 支持**用户分组**以及**渠道分组**,支持为不同分组设置不同的倍率。 | 9. 支持**用户分组**以及**渠道分组**,支持为不同分组设置不同的倍率。 | ||||||
| 9. 支持渠道**设置模型列表**。 | 10. 支持渠道**设置模型列表**。 | ||||||
| 10. 支持**查看额度明细**。 | 11. 支持**查看额度明细**。 | ||||||
| 11. 支持**用户邀请奖励**。 | 12. 支持**用户邀请奖励**。 | ||||||
| 12. 支持以美元为单位显示额度。 | 13. 支持以美元为单位显示额度。 | ||||||
| 13. 支持发布公告,设置充值链接,设置新用户初始额度。 | 14. 支持发布公告,设置充值链接,设置新用户初始额度。 | ||||||
| 14. 支持模型映射,重定向用户的请求模型。 | 15. 支持模型映射,重定向用户的请求模型。 | ||||||
| 15. 支持失败自动重试。 | 16. 支持失败自动重试。 | ||||||
| 16. 支持绘图接口。 | 17. 支持绘图接口。 | ||||||
| 17. 支持丰富的**自定义**设置, | 18. 支持丰富的**自定义**设置, | ||||||
|     1. 支持自定义系统名称,logo 以及页脚。 |     1. 支持自定义系统名称,logo 以及页脚。 | ||||||
|     2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。 |     2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。 | ||||||
| 18. 支持通过系统访问令牌访问管理 API。 | 19. 支持通过系统访问令牌访问管理 API。 | ||||||
| 19. 支持 Cloudflare Turnstile 用户校验。 | 20. 支持 Cloudflare Turnstile 用户校验。 | ||||||
| 20. 支持用户管理,支持**多种用户登录注册方式**: | 21. 支持用户管理,支持**多种用户登录注册方式**: | ||||||
|     + 邮箱登录注册以及通过邮箱进行密码重置。 |     + 邮箱登录注册以及通过邮箱进行密码重置。 | ||||||
|     + [GitHub 开放授权](https://github.com/settings/applications/new)。 |     + [GitHub 开放授权](https://github.com/settings/applications/new)。 | ||||||
|     + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 |     + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 | ||||||
|   | |||||||
| @@ -11,9 +11,11 @@ func GetSubscription(c *gin.Context) { | |||||||
| 	var usedQuota int | 	var usedQuota int | ||||||
| 	var err error | 	var err error | ||||||
| 	var token *model.Token | 	var token *model.Token | ||||||
|  | 	var expiredTime int64 | ||||||
| 	if common.DisplayTokenStatEnabled { | 	if common.DisplayTokenStatEnabled { | ||||||
| 		tokenId := c.GetInt("token_id") | 		tokenId := c.GetInt("token_id") | ||||||
| 		token, err = model.GetTokenById(tokenId) | 		token, err = model.GetTokenById(tokenId) | ||||||
|  | 		expiredTime = token.ExpiredTime | ||||||
| 		remainQuota = token.RemainQuota | 		remainQuota = token.RemainQuota | ||||||
| 		usedQuota = token.UsedQuota | 		usedQuota = token.UsedQuota | ||||||
| 	} else { | 	} else { | ||||||
| @@ -21,6 +23,9 @@ func GetSubscription(c *gin.Context) { | |||||||
| 		remainQuota, err = model.GetUserQuota(userId) | 		remainQuota, err = model.GetUserQuota(userId) | ||||||
| 		usedQuota, err = model.GetUserUsedQuota(userId) | 		usedQuota, err = model.GetUserUsedQuota(userId) | ||||||
| 	} | 	} | ||||||
|  | 	if expiredTime <= 0 { | ||||||
|  | 		expiredTime = 0 | ||||||
|  | 	} | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		openAIError := OpenAIError{ | 		openAIError := OpenAIError{ | ||||||
| 			Message: err.Error(), | 			Message: err.Error(), | ||||||
| @@ -45,6 +50,7 @@ func GetSubscription(c *gin.Context) { | |||||||
| 		SoftLimitUSD:       amount, | 		SoftLimitUSD:       amount, | ||||||
| 		HardLimitUSD:       amount, | 		HardLimitUSD:       amount, | ||||||
| 		SystemHardLimitUSD: amount, | 		SystemHardLimitUSD: amount, | ||||||
|  | 		AccessUntil:        expiredTime, | ||||||
| 	} | 	} | ||||||
| 	c.JSON(200, subscription) | 	c.JSON(200, subscription) | ||||||
| 	return | 	return | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ type OpenAISubscriptionResponse struct { | |||||||
| 	SoftLimitUSD       float64 `json:"soft_limit_usd"` | 	SoftLimitUSD       float64 `json:"soft_limit_usd"` | ||||||
| 	HardLimitUSD       float64 `json:"hard_limit_usd"` | 	HardLimitUSD       float64 `json:"hard_limit_usd"` | ||||||
| 	SystemHardLimitUSD float64 `json:"system_hard_limit_usd"` | 	SystemHardLimitUSD float64 `json:"system_hard_limit_usd"` | ||||||
|  | 	AccessUntil        int64   `json:"access_until"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type OpenAIUsageDailyCost struct { | type OpenAIUsageDailyCost struct { | ||||||
| @@ -84,7 +85,6 @@ func GetAuthHeader(token string) http.Header { | |||||||
| } | } | ||||||
|  |  | ||||||
| func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) { | func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) { | ||||||
| 	client := &http.Client{} |  | ||||||
| 	req, err := http.NewRequest(method, url, nil) | 	req, err := http.NewRequest(method, url, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @@ -92,10 +92,13 @@ func GetResponseBody(method, url string, channel *model.Channel, headers http.He | |||||||
| 	for k := range headers { | 	for k := range headers { | ||||||
| 		req.Header.Add(k, headers.Get(k)) | 		req.Header.Add(k, headers.Get(k)) | ||||||
| 	} | 	} | ||||||
| 	res, err := client.Do(req) | 	res, err := httpClient.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | 	if res.StatusCode != http.StatusOK { | ||||||
|  | 		return nil, fmt.Errorf("status code: %d", res.StatusCode) | ||||||
|  | 	} | ||||||
| 	body, err := io.ReadAll(res.Body) | 	body, err := io.ReadAll(res.Body) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|   | |||||||
| @@ -45,8 +45,7 @@ func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIErr | |||||||
| 		req.Header.Set("Authorization", "Bearer "+channel.Key) | 		req.Header.Set("Authorization", "Bearer "+channel.Key) | ||||||
| 	} | 	} | ||||||
| 	req.Header.Set("Content-Type", "application/json") | 	req.Header.Set("Content-Type", "application/json") | ||||||
| 	client := &http.Client{} | 	resp, err := httpClient.Do(req) | ||||||
| 	resp, err := client.Do(req) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err, nil | 		return err, nil | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -127,8 +127,9 @@ func SendPasswordResetEmail(c *gin.Context) { | |||||||
| 	link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code) | 	link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", common.ServerAddress, email, code) | ||||||
| 	subject := fmt.Sprintf("%s密码重置", common.SystemName) | 	subject := fmt.Sprintf("%s密码重置", common.SystemName) | ||||||
| 	content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+ | 	content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+ | ||||||
| 		"<p>点击<a href='%s'>此处</a>进行密码重置。</p>"+ | 		"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+ | ||||||
| 		"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, common.VerificationValidMinutes) | 		"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+ | ||||||
|  | 		"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, link, common.VerificationValidMinutes) | ||||||
| 	err := common.SendEmail(subject, email, content) | 	err := common.SendEmail(subject, email, content) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|   | |||||||
| @@ -109,8 +109,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode | |||||||
| 	req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) | 	req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) | ||||||
| 	req.Header.Set("Accept", c.Request.Header.Get("Accept")) | 	req.Header.Set("Accept", c.Request.Header.Get("Accept")) | ||||||
|  |  | ||||||
| 	client := &http.Client{} | 	resp, err := httpClient.Do(req) | ||||||
| 	resp, err := client.Do(req) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return errorWrapper(err, "do_request_failed", http.StatusInternalServerError) | 		return errorWrapper(err, "do_request_failed", http.StatusInternalServerError) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -115,7 +115,7 @@ func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*Ope | |||||||
| 	} | 	} | ||||||
| 	// We shouldn't set the header before we parse the response body, because the parse part may fail. | 	// We shouldn't set the header before we parse the response body, because the parse part may fail. | ||||||
| 	// And then we will have to send an error response, but in this case, the header has already been set. | 	// And then we will have to send an error response, but in this case, the header has already been set. | ||||||
| 	// So the client will be confused by the response. | 	// So the httpClient will be confused by the response. | ||||||
| 	// For example, Postman will report error, and we cannot check the response at all. | 	// For example, Postman will report error, and we cannot check the response at all. | ||||||
| 	for k, v := range resp.Header { | 	for k, v := range resp.Header { | ||||||
| 		c.Writer.Header().Set(k, v[0]) | 		c.Writer.Header().Set(k, v[0]) | ||||||
|   | |||||||
| @@ -22,6 +22,12 @@ const ( | |||||||
| 	APITypeZhipu | 	APITypeZhipu | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | var httpClient *http.Client | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	httpClient = &http.Client{} | ||||||
|  | } | ||||||
|  |  | ||||||
| func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | ||||||
| 	channelType := c.GetInt("channel") | 	channelType := c.GetInt("channel") | ||||||
| 	tokenId := c.GetInt("token_id") | 	tokenId := c.GetInt("token_id") | ||||||
| @@ -244,8 +250,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 	req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) | 	req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) | ||||||
| 	req.Header.Set("Accept", c.Request.Header.Get("Accept")) | 	req.Header.Set("Accept", c.Request.Header.Get("Accept")) | ||||||
| 	//req.Header.Set("Connection", c.Request.Header.Get("Connection")) | 	//req.Header.Set("Connection", c.Request.Header.Get("Connection")) | ||||||
| 	client := &http.Client{} | 	resp, err := httpClient.Do(req) | ||||||
| 	resp, err := client.Do(req) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return errorWrapper(err, "do_request_failed", http.StatusInternalServerError) | 		return errorWrapper(err, "do_request_failed", http.StatusInternalServerError) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -3,12 +3,13 @@ package controller | |||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-contrib/sessions" |  | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-contrib/sessions" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type LoginRequest struct { | type LoginRequest struct { | ||||||
| @@ -477,6 +478,16 @@ func DeleteUser(c *gin.Context) { | |||||||
|  |  | ||||||
| func DeleteSelf(c *gin.Context) { | func DeleteSelf(c *gin.Context) { | ||||||
| 	id := c.GetInt("id") | 	id := c.GetInt("id") | ||||||
|  | 	user, _ := model.GetUserById(id, false) | ||||||
|  |  | ||||||
|  | 	if user.Role == common.RoleRootUser { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": "不能删除超级管理员账户", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	err := model.DeleteUserById(id) | 	err := model.DeleteUserById(id) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|   | |||||||
| @@ -36,7 +36,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", controller.DeleteSelf) | 				selfRoute.DELETE("/self", middleware.TurnstileCheck(), 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) | ||||||
|   | |||||||
| @@ -1,36 +1,25 @@ | |||||||
| import React, { useContext, useEffect, useState } from 'react'; | import React, { useContext, useEffect, useState } from 'react'; | ||||||
| import { | import { Button, Divider, Form, Grid, Header, Image, Message, Modal, Segment } from 'semantic-ui-react'; | ||||||
|   Button, |  | ||||||
|   Divider, |  | ||||||
|   Form, |  | ||||||
|   Grid, |  | ||||||
|   Header, |  | ||||||
|   Image, |  | ||||||
|   Message, |  | ||||||
|   Modal, |  | ||||||
|   Segment, |  | ||||||
| } from 'semantic-ui-react'; |  | ||||||
| import { Link, useNavigate, useSearchParams } from 'react-router-dom'; | import { Link, useNavigate, useSearchParams } from 'react-router-dom'; | ||||||
| import { UserContext } from '../context/User'; | import { UserContext } from '../context/User'; | ||||||
| import { API, getLogo, showError, showSuccess, showInfo } from '../helpers'; | import { API, getLogo, showError, showSuccess } from '../helpers'; | ||||||
|  |  | ||||||
| const LoginForm = () => { | const LoginForm = () => { | ||||||
|   const [inputs, setInputs] = useState({ |   const [inputs, setInputs] = useState({ | ||||||
|     username: '', |     username: '', | ||||||
|     password: '', |     password: '', | ||||||
|     wechat_verification_code: '', |     wechat_verification_code: '' | ||||||
|   }); |   }); | ||||||
|   const [searchParams, setSearchParams] = useSearchParams(); |   const [searchParams, setSearchParams] = useSearchParams(); | ||||||
|   const [submitted, setSubmitted] = useState(false); |   const [submitted, setSubmitted] = useState(false); | ||||||
|   const { username, password } = inputs; |   const { username, password } = inputs; | ||||||
|   const [userState, userDispatch] = useContext(UserContext); |   const [userState, userDispatch] = useContext(UserContext); | ||||||
|   let navigate = useNavigate(); |   let navigate = useNavigate(); | ||||||
|  |  | ||||||
|   const [status, setStatus] = useState({}); |   const [status, setStatus] = useState({}); | ||||||
|   const logo = getLogo(); |   const logo = getLogo(); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (searchParams.get("expired")) { |     if (searchParams.get('expired')) { | ||||||
|       showError('未登录或登录已过期,请重新登录!'); |       showError('未登录或登录已过期,请重新登录!'); | ||||||
|     } |     } | ||||||
|     let status = localStorage.getItem('status'); |     let status = localStorage.getItem('status'); | ||||||
| @@ -78,7 +67,7 @@ const LoginForm = () => { | |||||||
|     if (username && password) { |     if (username && password) { | ||||||
|       const res = await API.post(`/api/user/login`, { |       const res = await API.post(`/api/user/login`, { | ||||||
|         username, |         username, | ||||||
|         password, |         password | ||||||
|       }); |       }); | ||||||
|       const { success, message, data } = res.data; |       const { success, message, data } = res.data; | ||||||
|       if (success) { |       if (success) { | ||||||
| @@ -93,44 +82,44 @@ const LoginForm = () => { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Grid textAlign="center" style={{ marginTop: '48px' }}> |     <Grid textAlign='center' style={{ marginTop: '48px' }}> | ||||||
|       <Grid.Column style={{ maxWidth: 450 }}> |       <Grid.Column style={{ maxWidth: 450 }}> | ||||||
|         <Header as="h2" color="" textAlign="center"> |         <Header as='h2' color='' textAlign='center'> | ||||||
|           <Image src={logo} /> 用户登录 |           <Image src={logo} /> 用户登录 | ||||||
|         </Header> |         </Header> | ||||||
|         <Form size="large"> |         <Form size='large'> | ||||||
|           <Segment> |           <Segment> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               fluid |               fluid | ||||||
|               icon="user" |               icon='user' | ||||||
|               iconPosition="left" |               iconPosition='left' | ||||||
|               placeholder="用户名" |               placeholder='用户名' | ||||||
|               name="username" |               name='username' | ||||||
|               value={username} |               value={username} | ||||||
|               onChange={handleChange} |               onChange={handleChange} | ||||||
|             /> |             /> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               fluid |               fluid | ||||||
|               icon="lock" |               icon='lock' | ||||||
|               iconPosition="left" |               iconPosition='left' | ||||||
|               placeholder="密码" |               placeholder='密码' | ||||||
|               name="password" |               name='password' | ||||||
|               type="password" |               type='password' | ||||||
|               value={password} |               value={password} | ||||||
|               onChange={handleChange} |               onChange={handleChange} | ||||||
|             /> |             /> | ||||||
|             <Button color="" fluid size="large" onClick={handleSubmit}> |             <Button color='green' fluid size='large' onClick={handleSubmit}> | ||||||
|               登录 |               登录 | ||||||
|             </Button> |             </Button> | ||||||
|           </Segment> |           </Segment> | ||||||
|         </Form> |         </Form> | ||||||
|         <Message> |         <Message> | ||||||
|           忘记密码? |           忘记密码? | ||||||
|           <Link to="/reset" className="btn btn-link"> |           <Link to='/reset' className='btn btn-link'> | ||||||
|             点击重置 |             点击重置 | ||||||
|           </Link> |           </Link> | ||||||
|           ; 没有账户? |           ; 没有账户? | ||||||
|           <Link to="/register" className="btn btn-link"> |           <Link to='/register' className='btn btn-link'> | ||||||
|             点击注册 |             点击注册 | ||||||
|           </Link> |           </Link> | ||||||
|         </Message> |         </Message> | ||||||
| @@ -140,8 +129,8 @@ const LoginForm = () => { | |||||||
|             {status.github_oauth ? ( |             {status.github_oauth ? ( | ||||||
|               <Button |               <Button | ||||||
|                 circular |                 circular | ||||||
|                 color="black" |                 color='black' | ||||||
|                 icon="github" |                 icon='github' | ||||||
|                 onClick={onGitHubOAuthClicked} |                 onClick={onGitHubOAuthClicked} | ||||||
|               /> |               /> | ||||||
|             ) : ( |             ) : ( | ||||||
| @@ -150,8 +139,8 @@ const LoginForm = () => { | |||||||
|             {status.wechat_login ? ( |             {status.wechat_login ? ( | ||||||
|               <Button |               <Button | ||||||
|                 circular |                 circular | ||||||
|                 color="green" |                 color='green' | ||||||
|                 icon="wechat" |                 icon='wechat' | ||||||
|                 onClick={onWeChatLoginClicked} |                 onClick={onWeChatLoginClicked} | ||||||
|               /> |               /> | ||||||
|             ) : ( |             ) : ( | ||||||
| @@ -175,18 +164,18 @@ const LoginForm = () => { | |||||||
|                   微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) |                   微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) | ||||||
|                 </p> |                 </p> | ||||||
|               </div> |               </div> | ||||||
|               <Form size="large"> |               <Form size='large'> | ||||||
|                 <Form.Input |                 <Form.Input | ||||||
|                   fluid |                   fluid | ||||||
|                   placeholder="验证码" |                   placeholder='验证码' | ||||||
|                   name="wechat_verification_code" |                   name='wechat_verification_code' | ||||||
|                   value={inputs.wechat_verification_code} |                   value={inputs.wechat_verification_code} | ||||||
|                   onChange={handleChange} |                   onChange={handleChange} | ||||||
|                 /> |                 /> | ||||||
|                 <Button |                 <Button | ||||||
|                   color="" |                   color='' | ||||||
|                   fluid |                   fluid | ||||||
|                   size="large" |                   size='large' | ||||||
|                   onClick={onSubmitWeChatVerificationCode} |                   onClick={onSubmitWeChatVerificationCode} | ||||||
|                 > |                 > | ||||||
|                   登录 |                   登录 | ||||||
|   | |||||||
| @@ -12,6 +12,11 @@ const PasswordResetConfirm = () => { | |||||||
|  |  | ||||||
|   const [loading, setLoading] = useState(false); |   const [loading, setLoading] = useState(false); | ||||||
|  |  | ||||||
|  |   const [disableButton, setDisableButton] = useState(false); | ||||||
|  |   const [countdown, setCountdown] = useState(30); | ||||||
|  |  | ||||||
|  |   const [newPassword, setNewPassword] = useState(''); | ||||||
|  |  | ||||||
|   const [searchParams, setSearchParams] = useSearchParams(); |   const [searchParams, setSearchParams] = useSearchParams(); | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     let token = searchParams.get('token'); |     let token = searchParams.get('token'); | ||||||
| @@ -22,7 +27,21 @@ const PasswordResetConfirm = () => { | |||||||
|     }); |     }); | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     let countdownInterval = null; | ||||||
|  |     if (disableButton && countdown > 0) { | ||||||
|  |       countdownInterval = setInterval(() => { | ||||||
|  |         setCountdown(countdown - 1); | ||||||
|  |       }, 1000); | ||||||
|  |     } else if (countdown === 0) { | ||||||
|  |       setDisableButton(false); | ||||||
|  |       setCountdown(30); | ||||||
|  |     } | ||||||
|  |     return () => clearInterval(countdownInterval);  | ||||||
|  |   }, [disableButton, countdown]); | ||||||
|  |  | ||||||
|   async function handleSubmit(e) { |   async function handleSubmit(e) { | ||||||
|  |     setDisableButton(true); | ||||||
|     if (!email) return; |     if (!email) return; | ||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     const res = await API.post(`/api/user/reset`, { |     const res = await API.post(`/api/user/reset`, { | ||||||
| @@ -32,14 +51,15 @@ const PasswordResetConfirm = () => { | |||||||
|     const { success, message } = res.data; |     const { success, message } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
|       let password = res.data.data; |       let password = res.data.data; | ||||||
|  |       setNewPassword(password); | ||||||
|       await copy(password); |       await copy(password); | ||||||
|       showNotice(`密码已重置并已复制到剪贴板:${password}`); |       showNotice(`新密码已复制到剪贴板:${password}`); | ||||||
|     } else { |     } else { | ||||||
|       showError(message); |       showError(message); | ||||||
|     } |     } | ||||||
|     setLoading(false); |     setLoading(false); | ||||||
|   } |   } | ||||||
|  |    | ||||||
|   return ( |   return ( | ||||||
|     <Grid textAlign='center' style={{ marginTop: '48px' }}> |     <Grid textAlign='center' style={{ marginTop: '48px' }}> | ||||||
|       <Grid.Column style={{ maxWidth: 450 }}> |       <Grid.Column style={{ maxWidth: 450 }}> | ||||||
| @@ -57,20 +77,37 @@ const PasswordResetConfirm = () => { | |||||||
|               value={email} |               value={email} | ||||||
|               readOnly |               readOnly | ||||||
|             /> |             /> | ||||||
|  |             {newPassword && ( | ||||||
|  |               <Form.Input | ||||||
|  |               fluid | ||||||
|  |               icon='lock' | ||||||
|  |               iconPosition='left' | ||||||
|  |               placeholder='新密码' | ||||||
|  |               name='newPassword' | ||||||
|  |               value={newPassword} | ||||||
|  |               readOnly | ||||||
|  |               onClick={(e) => { | ||||||
|  |                 e.target.select(); | ||||||
|  |                 navigator.clipboard.writeText(newPassword); | ||||||
|  |                 showNotice(`密码已复制到剪贴板:${newPassword}`); | ||||||
|  |               }} | ||||||
|  |             />             | ||||||
|  |             )} | ||||||
|             <Button |             <Button | ||||||
|               color='' |               color='green' | ||||||
|               fluid |               fluid | ||||||
|               size='large' |               size='large' | ||||||
|               onClick={handleSubmit} |               onClick={handleSubmit} | ||||||
|               loading={loading} |               loading={loading} | ||||||
|  |               disabled={disableButton} | ||||||
|             > |             > | ||||||
|               提交 |               {disableButton ? `密码重置完成` : '提交'} | ||||||
|             </Button> |             </Button> | ||||||
|           </Segment> |           </Segment> | ||||||
|         </Form> |         </Form> | ||||||
|       </Grid.Column> |       </Grid.Column> | ||||||
|     </Grid> |     </Grid> | ||||||
|   ); |   );   | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default PasswordResetConfirm; | export default PasswordResetConfirm; | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import Turnstile from 'react-turnstile'; | |||||||
|  |  | ||||||
| const PasswordResetForm = () => { | const PasswordResetForm = () => { | ||||||
|   const [inputs, setInputs] = useState({ |   const [inputs, setInputs] = useState({ | ||||||
|     email: '', |     email: '' | ||||||
|   }); |   }); | ||||||
|   const { email } = inputs; |   const { email } = inputs; | ||||||
|  |  | ||||||
| @@ -13,24 +13,29 @@ const PasswordResetForm = () => { | |||||||
|   const [turnstileEnabled, setTurnstileEnabled] = useState(false); |   const [turnstileEnabled, setTurnstileEnabled] = useState(false); | ||||||
|   const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); |   const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); | ||||||
|   const [turnstileToken, setTurnstileToken] = useState(''); |   const [turnstileToken, setTurnstileToken] = useState(''); | ||||||
|  |   const [disableButton, setDisableButton] = useState(false); | ||||||
|  |   const [countdown, setCountdown] = useState(30); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     let status = localStorage.getItem('status'); |     let countdownInterval = null; | ||||||
|     if (status) { |     if (disableButton && countdown > 0) { | ||||||
|       status = JSON.parse(status); |       countdownInterval = setInterval(() => { | ||||||
|       if (status.turnstile_check) { |         setCountdown(countdown - 1); | ||||||
|         setTurnstileEnabled(true); |       }, 1000); | ||||||
|         setTurnstileSiteKey(status.turnstile_site_key); |     } else if (countdown === 0) { | ||||||
|       } |       setDisableButton(false); | ||||||
|  |       setCountdown(30); | ||||||
|     } |     } | ||||||
|   }, []); |     return () => clearInterval(countdownInterval); | ||||||
|  |   }, [disableButton, countdown]); | ||||||
|  |  | ||||||
|   function handleChange(e) { |   function handleChange(e) { | ||||||
|     const { name, value } = e.target; |     const { name, value } = e.target; | ||||||
|     setInputs((inputs) => ({ ...inputs, [name]: value })); |     setInputs(inputs => ({ ...inputs, [name]: value })); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async function handleSubmit(e) { |   async function handleSubmit(e) { | ||||||
|  |     setDisableButton(true); | ||||||
|     if (!email) return; |     if (!email) return; | ||||||
|     if (turnstileEnabled && turnstileToken === '') { |     if (turnstileEnabled && turnstileToken === '') { | ||||||
|       showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); |       showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); | ||||||
| @@ -78,13 +83,14 @@ const PasswordResetForm = () => { | |||||||
|               <></> |               <></> | ||||||
|             )} |             )} | ||||||
|             <Button |             <Button | ||||||
|               color='' |               color='green' | ||||||
|               fluid |               fluid | ||||||
|               size='large' |               size='large' | ||||||
|               onClick={handleSubmit} |               onClick={handleSubmit} | ||||||
|               loading={loading} |               loading={loading} | ||||||
|  |               disabled={disableButton} | ||||||
|             > |             > | ||||||
|               提交 |               {disableButton ? `重试 (${countdown})` : '提交'} | ||||||
|             </Button> |             </Button> | ||||||
|           </Segment> |           </Segment> | ||||||
|         </Form> |         </Form> | ||||||
|   | |||||||
| @@ -1,22 +1,30 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useContext, useEffect, useState } from 'react'; | ||||||
| import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react'; | import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react'; | ||||||
| import { Link } from 'react-router-dom'; | import { Link, useNavigate } from 'react-router-dom'; | ||||||
| import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; | import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; | ||||||
| import Turnstile from 'react-turnstile'; | import Turnstile from 'react-turnstile'; | ||||||
|  | import { UserContext } from '../context/User'; | ||||||
|  |  | ||||||
| const PersonalSetting = () => { | const PersonalSetting = () => { | ||||||
|  |   const [userState, userDispatch] = useContext(UserContext); | ||||||
|  |   let navigate = useNavigate(); | ||||||
|  |  | ||||||
|   const [inputs, setInputs] = useState({ |   const [inputs, setInputs] = useState({ | ||||||
|     wechat_verification_code: '', |     wechat_verification_code: '', | ||||||
|     email_verification_code: '', |     email_verification_code: '', | ||||||
|     email: '', |     email: '', | ||||||
|  |     self_account_deletion_confirmation: '' | ||||||
|   }); |   }); | ||||||
|   const [status, setStatus] = useState({}); |   const [status, setStatus] = useState({}); | ||||||
|   const [showWeChatBindModal, setShowWeChatBindModal] = useState(false); |   const [showWeChatBindModal, setShowWeChatBindModal] = useState(false); | ||||||
|   const [showEmailBindModal, setShowEmailBindModal] = useState(false); |   const [showEmailBindModal, setShowEmailBindModal] = useState(false); | ||||||
|  |   const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false); | ||||||
|   const [turnstileEnabled, setTurnstileEnabled] = useState(false); |   const [turnstileEnabled, setTurnstileEnabled] = useState(false); | ||||||
|   const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); |   const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); | ||||||
|   const [turnstileToken, setTurnstileToken] = useState(''); |   const [turnstileToken, setTurnstileToken] = useState(''); | ||||||
|   const [loading, setLoading] = useState(false); |   const [loading, setLoading] = useState(false); | ||||||
|  |   const [disableButton, setDisableButton] = useState(false); | ||||||
|  |   const [countdown, setCountdown] = useState(30); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     let status = localStorage.getItem('status'); |     let status = localStorage.getItem('status'); | ||||||
| @@ -30,6 +38,19 @@ const PersonalSetting = () => { | |||||||
|     } |     } | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     let countdownInterval = null; | ||||||
|  |     if (disableButton && countdown > 0) { | ||||||
|  |       countdownInterval = setInterval(() => { | ||||||
|  |         setCountdown(countdown - 1); | ||||||
|  |       }, 1000); | ||||||
|  |     } else if (countdown === 0) { | ||||||
|  |       setDisableButton(false); | ||||||
|  |       setCountdown(30); | ||||||
|  |     } | ||||||
|  |     return () => clearInterval(countdownInterval); // Clean up on unmount | ||||||
|  |   }, [disableButton, countdown]); | ||||||
|  |  | ||||||
|   const handleInputChange = (e, { name, value }) => { |   const handleInputChange = (e, { name, value }) => { | ||||||
|     setInputs((inputs) => ({ ...inputs, [name]: value })); |     setInputs((inputs) => ({ ...inputs, [name]: value })); | ||||||
|   }; |   }; | ||||||
| @@ -57,6 +78,26 @@ const PersonalSetting = () => { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const deleteAccount = async () => { | ||||||
|  |     if (inputs.self_account_deletion_confirmation !== userState.user.username) { | ||||||
|  |       showError('请输入你的账户名以确认删除!'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const res = await API.delete('/api/user/self'); | ||||||
|  |     const { success, message } = res.data; | ||||||
|  |  | ||||||
|  |     if (success) { | ||||||
|  |       showSuccess('账户已删除!'); | ||||||
|  |       await API.get('/api/user/logout'); | ||||||
|  |       userDispatch({ type: 'logout' }); | ||||||
|  |       localStorage.removeItem('user'); | ||||||
|  |       navigate('/login'); | ||||||
|  |     } else { | ||||||
|  |       showError(message); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const bindWeChat = async () => { |   const bindWeChat = async () => { | ||||||
|     if (inputs.wechat_verification_code === '') return; |     if (inputs.wechat_verification_code === '') return; | ||||||
|     const res = await API.get( |     const res = await API.get( | ||||||
| @@ -78,6 +119,7 @@ const PersonalSetting = () => { | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const sendVerificationCode = async () => { |   const sendVerificationCode = async () => { | ||||||
|  |     setDisableButton(true); | ||||||
|     if (inputs.email === '') return; |     if (inputs.email === '') return; | ||||||
|     if (turnstileEnabled && turnstileToken === '') { |     if (turnstileEnabled && turnstileToken === '') { | ||||||
|       showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); |       showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); | ||||||
| @@ -123,6 +165,9 @@ const PersonalSetting = () => { | |||||||
|       </Button> |       </Button> | ||||||
|       <Button onClick={generateAccessToken}>生成系统访问令牌</Button> |       <Button onClick={generateAccessToken}>生成系统访问令牌</Button> | ||||||
|       <Button onClick={getAffLink}>复制邀请链接</Button> |       <Button onClick={getAffLink}>复制邀请链接</Button> | ||||||
|  |       <Button onClick={() => { | ||||||
|  |         setShowAccountDeleteModal(true); | ||||||
|  |       }}>删除个人账户</Button> | ||||||
|       <Divider /> |       <Divider /> | ||||||
|       <Header as='h3'>账号绑定</Header> |       <Header as='h3'>账号绑定</Header> | ||||||
|       { |       { | ||||||
| @@ -195,8 +240,8 @@ const PersonalSetting = () => { | |||||||
|                 name='email' |                 name='email' | ||||||
|                 type='email' |                 type='email' | ||||||
|                 action={ |                 action={ | ||||||
|                   <Button onClick={sendVerificationCode} disabled={loading}> |                   <Button onClick={sendVerificationCode} disabled={disableButton || loading}> | ||||||
|                     获取验证码 |                     {disableButton ? `重新发送(${countdown})` : '获取验证码'} | ||||||
|                   </Button> |                   </Button> | ||||||
|                 } |                 } | ||||||
|               /> |               /> | ||||||
| @@ -230,6 +275,47 @@ const PersonalSetting = () => { | |||||||
|           </Modal.Description> |           </Modal.Description> | ||||||
|         </Modal.Content> |         </Modal.Content> | ||||||
|       </Modal> |       </Modal> | ||||||
|  |       <Modal | ||||||
|  |         onClose={() => setShowAccountDeleteModal(false)} | ||||||
|  |         onOpen={() => setShowAccountDeleteModal(true)} | ||||||
|  |         open={showAccountDeleteModal} | ||||||
|  |         size={'tiny'} | ||||||
|  |         style={{ maxWidth: '450px' }} | ||||||
|  |       > | ||||||
|  |         <Modal.Header>确认删除自己的帐户</Modal.Header> | ||||||
|  |         <Modal.Content> | ||||||
|  |           <Modal.Description> | ||||||
|  |             <Form size='large'> | ||||||
|  |               <Form.Input | ||||||
|  |                 fluid | ||||||
|  |                 placeholder={`输入你的账户名 ${userState.user.username} 以确认删除`} | ||||||
|  |                 name='self_account_deletion_confirmation' | ||||||
|  |                 value={inputs.self_account_deletion_confirmation} | ||||||
|  |                 onChange={handleInputChange} | ||||||
|  |               /> | ||||||
|  |               {turnstileEnabled ? ( | ||||||
|  |                 <Turnstile | ||||||
|  |                   sitekey={turnstileSiteKey} | ||||||
|  |                   onVerify={(token) => { | ||||||
|  |                     setTurnstileToken(token); | ||||||
|  |                   }} | ||||||
|  |                 /> | ||||||
|  |               ) : ( | ||||||
|  |                 <></> | ||||||
|  |               )} | ||||||
|  |               <Button | ||||||
|  |                 color='red' | ||||||
|  |                 fluid | ||||||
|  |                 size='large' | ||||||
|  |                 onClick={deleteAccount} | ||||||
|  |                 loading={loading} | ||||||
|  |               > | ||||||
|  |                 删除 | ||||||
|  |               </Button> | ||||||
|  |             </Form> | ||||||
|  |           </Modal.Description> | ||||||
|  |         </Modal.Content> | ||||||
|  |       </Modal> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,13 +1,5 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState } from 'react'; | ||||||
| import { | import { Button, Form, Grid, Header, Image, Message, Segment } from 'semantic-ui-react'; | ||||||
|   Button, |  | ||||||
|   Form, |  | ||||||
|   Grid, |  | ||||||
|   Header, |  | ||||||
|   Image, |  | ||||||
|   Message, |  | ||||||
|   Segment, |  | ||||||
| } from 'semantic-ui-react'; |  | ||||||
| import { Link, useNavigate } from 'react-router-dom'; | import { Link, useNavigate } from 'react-router-dom'; | ||||||
| import { API, getLogo, showError, showInfo, showSuccess } from '../helpers'; | import { API, getLogo, showError, showInfo, showSuccess } from '../helpers'; | ||||||
| import Turnstile from 'react-turnstile'; | import Turnstile from 'react-turnstile'; | ||||||
| @@ -18,7 +10,7 @@ const RegisterForm = () => { | |||||||
|     password: '', |     password: '', | ||||||
|     password2: '', |     password2: '', | ||||||
|     email: '', |     email: '', | ||||||
|     verification_code: '', |     verification_code: '' | ||||||
|   }); |   }); | ||||||
|   const { username, password, password2 } = inputs; |   const { username, password, password2 } = inputs; | ||||||
|   const [showEmailVerification, setShowEmailVerification] = useState(false); |   const [showEmailVerification, setShowEmailVerification] = useState(false); | ||||||
| @@ -178,7 +170,7 @@ const RegisterForm = () => { | |||||||
|               <></> |               <></> | ||||||
|             )} |             )} | ||||||
|             <Button |             <Button | ||||||
|               color='' |               color='green' | ||||||
|               fluid |               fluid | ||||||
|               size='large' |               size='large' | ||||||
|               onClick={handleSubmit} |               onClick={handleSubmit} | ||||||
|   | |||||||
| @@ -46,9 +46,7 @@ const About = () => { | |||||||
|             about.startsWith('https://') ? <iframe |             about.startsWith('https://') ? <iframe | ||||||
|               src={about} |               src={about} | ||||||
|               style={{ width: '100%', height: '100vh', border: 'none' }} |               style={{ width: '100%', height: '100vh', border: 'none' }} | ||||||
|             /> : <Segment> |             /> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div> | ||||||
|               <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div> |  | ||||||
|             </Segment> |  | ||||||
|           } |           } | ||||||
|         </> |         </> | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -215,26 +215,12 @@ const EditChannel = () => { | |||||||
|               </Form.Field> |               </Form.Field> | ||||||
|             ) |             ) | ||||||
|           } |           } | ||||||
|           { |  | ||||||
|             inputs.type !== 3 && inputs.type !== 8 && ( |  | ||||||
|               <Form.Field> |  | ||||||
|                 <Form.Input |  | ||||||
|                   label='镜像' |  | ||||||
|                   name='base_url' |  | ||||||
|                   placeholder={'此项可选,输入镜像站地址,格式为:https://domain.com'} |  | ||||||
|                   onChange={handleInputChange} |  | ||||||
|                   value={inputs.base_url} |  | ||||||
|                   autoComplete='new-password' |  | ||||||
|                 /> |  | ||||||
|               </Form.Field> |  | ||||||
|             ) |  | ||||||
|           } |  | ||||||
|           <Form.Field> |           <Form.Field> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='名称' |               label='名称' | ||||||
|               required |               required | ||||||
|               name='name' |               name='name' | ||||||
|               placeholder={'请输入名称'} |               placeholder={'请为渠道命名'} | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               value={inputs.name} |               value={inputs.name} | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
| @@ -243,7 +229,7 @@ const EditChannel = () => { | |||||||
|           <Form.Field> |           <Form.Field> | ||||||
|             <Form.Dropdown |             <Form.Dropdown | ||||||
|               label='分组' |               label='分组' | ||||||
|               placeholder={'请选择分组'} |               placeholder={'请选择可以使用该渠道的分组'} | ||||||
|               name='groups' |               name='groups' | ||||||
|               required |               required | ||||||
|               fluid |               fluid | ||||||
| @@ -260,7 +246,7 @@ const EditChannel = () => { | |||||||
|           <Form.Field> |           <Form.Field> | ||||||
|             <Form.Dropdown |             <Form.Dropdown | ||||||
|               label='模型' |               label='模型' | ||||||
|               placeholder={'请选择该通道所支持的模型'} |               placeholder={'请选择该渠道所支持的模型'} | ||||||
|               name='models' |               name='models' | ||||||
|               required |               required | ||||||
|               fluid |               fluid | ||||||
| @@ -312,7 +298,7 @@ const EditChannel = () => { | |||||||
|           <Form.Field> |           <Form.Field> | ||||||
|             <Form.TextArea |             <Form.TextArea | ||||||
|               label='模型映射' |               label='模型映射' | ||||||
|               placeholder={`此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`} |               placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`} | ||||||
|               name='model_mapping' |               name='model_mapping' | ||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               value={inputs.model_mapping} |               value={inputs.model_mapping} | ||||||
| @@ -337,7 +323,7 @@ const EditChannel = () => { | |||||||
|                 label='密钥' |                 label='密钥' | ||||||
|                 name='key' |                 name='key' | ||||||
|                 required |                 required | ||||||
|                 placeholder={inputs.type === 15 ? "请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次" : '请输入密钥'} |                 placeholder={inputs.type === 15 ? "请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次" : '请输入渠道对应的鉴权密钥'} | ||||||
|                 onChange={handleInputChange} |                 onChange={handleInputChange} | ||||||
|                 value={inputs.key} |                 value={inputs.key} | ||||||
|                 autoComplete='new-password' |                 autoComplete='new-password' | ||||||
| @@ -354,6 +340,20 @@ const EditChannel = () => { | |||||||
|               /> |               /> | ||||||
|             ) |             ) | ||||||
|           } |           } | ||||||
|  |           { | ||||||
|  |             inputs.type !== 3 && inputs.type !== 8 && ( | ||||||
|  |               <Form.Field> | ||||||
|  |                 <Form.Input | ||||||
|  |                   label='镜像' | ||||||
|  |                   name='base_url' | ||||||
|  |                   placeholder={'此项可选,用于通过镜像站来进行 API 调用,请输入镜像站地址,格式为:https://domain.com'} | ||||||
|  |                   onChange={handleInputChange} | ||||||
|  |                   value={inputs.base_url} | ||||||
|  |                   autoComplete='new-password' | ||||||
|  |                 /> | ||||||
|  |               </Form.Field> | ||||||
|  |             ) | ||||||
|  |           } | ||||||
|           <Button type={isEdit ? "button" : "submit"} positive onClick={submit}>提交</Button> |           <Button type={isEdit ? "button" : "submit"} positive onClick={submit}>提交</Button> | ||||||
|         </Form> |         </Form> | ||||||
|       </Segment> |       </Segment> | ||||||
|   | |||||||
| @@ -7,24 +7,32 @@ const TopUp = () => { | |||||||
|   const [redemptionCode, setRedemptionCode] = useState(''); |   const [redemptionCode, setRedemptionCode] = useState(''); | ||||||
|   const [topUpLink, setTopUpLink] = useState(''); |   const [topUpLink, setTopUpLink] = useState(''); | ||||||
|   const [userQuota, setUserQuota] = useState(0); |   const [userQuota, setUserQuota] = useState(0); | ||||||
|  |   const [isSubmitting, setIsSubmitting] = useState(false); | ||||||
|  |  | ||||||
|   const topUp = async () => { |   const topUp = async () => { | ||||||
|     if (redemptionCode === '') { |     if (redemptionCode === '') { | ||||||
|       showInfo('请输入充值码!') |       showInfo('请输入充值码!') | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     const res = await API.post('/api/user/topup', { |     setIsSubmitting(true); | ||||||
|       key: redemptionCode |     try { | ||||||
|     }); |       const res = await API.post('/api/user/topup', { | ||||||
|     const { success, message, data } = res.data; |         key: redemptionCode | ||||||
|     if (success) { |  | ||||||
|       showSuccess('充值成功!'); |  | ||||||
|       setUserQuota((quota) => { |  | ||||||
|         return quota + data; |  | ||||||
|       }); |       }); | ||||||
|       setRedemptionCode(''); |       const { success, message, data } = res.data; | ||||||
|     } else { |       if (success) { | ||||||
|       showError(message); |         showSuccess('充值成功!'); | ||||||
|  |         setUserQuota((quota) => { | ||||||
|  |           return quota + data; | ||||||
|  |         }); | ||||||
|  |         setRedemptionCode(''); | ||||||
|  |       } else { | ||||||
|  |         showError(message); | ||||||
|  |       } | ||||||
|  |     } catch (err) { | ||||||
|  |       showError('请求失败'); | ||||||
|  |     } finally { | ||||||
|  |       setIsSubmitting(false);  | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
| @@ -74,8 +82,8 @@ const TopUp = () => { | |||||||
|             <Button color='green' onClick={openTopUpLink}> |             <Button color='green' onClick={openTopUpLink}> | ||||||
|               获取兑换码 |               获取兑换码 | ||||||
|             </Button> |             </Button> | ||||||
|             <Button color='yellow' onClick={topUp}> |             <Button color='yellow' onClick={topUp} disabled={isSubmitting}> | ||||||
|               充值 |                 {isSubmitting ? '兑换中...' : '兑换'} | ||||||
|             </Button> |             </Button> | ||||||
|           </Form> |           </Form> | ||||||
|         </Grid.Column> |         </Grid.Column> | ||||||
| @@ -92,5 +100,4 @@ const TopUp = () => { | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export default TopUp; | ||||||
| export default TopUp; |  | ||||||
		Reference in New Issue
	
	Block a user