Merge branch 'songquanpeng:main' into main

This commit is contained in:
Jialinvip 2023-08-12 13:41:50 +08:00 committed by GitHub
commit 45f3e35fb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 154 additions and 43 deletions

View File

@ -4,7 +4,7 @@ WORKDIR /build
COPY ./web . COPY ./web .
COPY ./VERSION . COPY ./VERSION .
RUN npm install RUN npm install
RUN REACT_APP_VERSION=$(cat VERSION) npm run build RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build
FROM golang AS builder2 FROM golang AS builder2

View File

@ -190,7 +190,7 @@ If you encounter a blank page after deployment, refer to [#97](https://github.co
> Zeabur's servers are located overseas, automatically solving network issues, and the free quota is sufficient for personal usage. > Zeabur's servers are located overseas, automatically solving network issues, and the free quota is sufficient for personal usage.
1. First, fork the code. 1. First, fork the code.
2. Go to [Zeabur](https://zeabur.com/), log in, and enter the console. 2. Go to [Zeabur](https://zeabur.com?referralCode=songquanpeng), log in, and enter the console.
3. Create a new project. In Service -> Add Service, select Marketplace, and choose MySQL. Note down the connection parameters (username, password, address, and port). 3. Create a new project. In Service -> Add Service, select Marketplace, and choose MySQL. Note down the connection parameters (username, password, address, and port).
4. Copy the connection parameters and run ```create database `one-api` ``` to create the database. 4. Copy the connection parameters and run ```create database `one-api` ``` to create the database.
5. Then, in Service -> Add Service, select Git (authorization is required for the first use) and choose your forked repository. 5. Then, in Service -> Add Service, select Git (authorization is required for the first use) and choose your forked repository.

View File

@ -102,16 +102,16 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
### 基于 Docker 进行部署 ### 基于 Docker 进行部署
部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api` 部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api`
其中,`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。
数据将会保存在宿主机的 `/home/ubuntu/data/one-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`
`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。
数据将会保存在宿主机的 `/home/ubuntu/data/one-api` 目录,请确保该目录存在且具有写入权限,或者更改为合适的目录。
Nginx 的参考配置: Nginx 的参考配置:
``` ```
server{ server{
@ -227,7 +227,7 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
> Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用。 > Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用。
1. 首先 fork 一份代码。 1. 首先 fork 一份代码。
2. 进入 [Zeabur](https://zeabur.com/),登录,进入控制台。 2. 进入 [Zeabur](https://zeabur.com?referralCode=songquanpeng),登录,进入控制台。
3. 新建一个 Project在 Service -> Add Service 选择 Marketplace选择 MySQL并记下连接参数用户名、密码、地址、端口 3. 新建一个 Project在 Service -> Add Service 选择 Marketplace选择 MySQL并记下连接参数用户名、密码、地址、端口
4. 复制链接参数,运行 ```create database `one-api` ``` 创建数据库。 4. 复制链接参数,运行 ```create database `one-api` ``` 创建数据库。
5. 然后在 Service -> Add Service选择 Git第一次使用需要先授权选择你 fork 的仓库。 5. 然后在 Service -> Add Service选择 Git第一次使用需要先授权选择你 fork 的仓库。
@ -281,6 +281,11 @@ graph LR
+ 注意需要提前建立数据库 `oneapi`,无需手动建表,程序将自动建表。 + 注意需要提前建立数据库 `oneapi`,无需手动建表,程序将自动建表。
+ 如果使用本地数据库:部署命令可添加 `--network="host"` 以使得容器内的程序可以访问到宿主机上的 MySQL。 + 如果使用本地数据库:部署命令可添加 `--network="host"` 以使得容器内的程序可以访问到宿主机上的 MySQL。
+ 如果使用云数据库:如果云服务器需要验证身份,需要在连接参数中添加 `?tls=skip-verify` + 如果使用云数据库:如果云服务器需要验证身份,需要在连接参数中添加 `?tls=skip-verify`
+ 请根据你的数据库配置修改下列参数(或者保持默认值):
+ `SQL_MAX_IDLE_CONNS`:最大空闲连接数,默认为 `10`
+ `SQL_MAX_OPEN_CONNS`:最大打开连接数,默认为 `100`
+ 如果报错 `Error 1040: Too many connections`,请适当减小该值。
+ `SQL_CONN_MAX_LIFETIME`:连接的最大生命周期,默认为 `60`,单位分钟。
4. `FRONTEND_BASE_URL`:设置之后将重定向页面请求到指定的地址,仅限从服务器设置。 4. `FRONTEND_BASE_URL`:设置之后将重定向页面请求到指定的地址,仅限从服务器设置。
+ 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn` + 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn`
5. `SYNC_FREQUENCY`:设置之后将定期与数据库同步配置,单位为秒,未设置则不进行同步。 5. `SYNC_FREQUENCY`:设置之后将定期与数据库同步配置,单位为秒,未设置则不进行同步。

View File

@ -7,6 +7,7 @@ import (
"log" "log"
"math/rand" "math/rand"
"net" "net"
"os"
"os/exec" "os/exec"
"runtime" "runtime"
"strconv" "strconv"
@ -177,3 +178,15 @@ func Max(a int, b int) int {
return b return b
} }
} }
func GetOrDefault(env string, defaultValue int) int {
if env == "" || os.Getenv(env) == "" {
return defaultValue
}
num, err := strconv.Atoi(os.Getenv(env))
if err != nil {
SysError(fmt.Sprintf("failed to parse %s: %s, using default value: %d", env, err.Error(), defaultValue))
return defaultValue
}
return num
}

View File

@ -121,7 +121,10 @@ func responseAli2OpenAI(response *AliChatResponse) *OpenAITextResponse {
func streamResponseAli2OpenAI(aliResponse *AliChatResponse) *ChatCompletionsStreamResponse { func streamResponseAli2OpenAI(aliResponse *AliChatResponse) *ChatCompletionsStreamResponse {
var choice ChatCompletionsStreamResponseChoice var choice ChatCompletionsStreamResponseChoice
choice.Delta.Content = aliResponse.Output.Text choice.Delta.Content = aliResponse.Output.Text
choice.FinishReason = aliResponse.Output.FinishReason if aliResponse.Output.FinishReason != "null" {
finishReason := aliResponse.Output.FinishReason
choice.FinishReason = &finishReason
}
response := ChatCompletionsStreamResponse{ response := ChatCompletionsStreamResponse{
Id: aliResponse.RequestId, Id: aliResponse.RequestId,
Object: "chat.completion.chunk", Object: "chat.completion.chunk",

View File

@ -120,7 +120,9 @@ func responseBaidu2OpenAI(response *BaiduChatResponse) *OpenAITextResponse {
func streamResponseBaidu2OpenAI(baiduResponse *BaiduChatStreamResponse) *ChatCompletionsStreamResponse { func streamResponseBaidu2OpenAI(baiduResponse *BaiduChatStreamResponse) *ChatCompletionsStreamResponse {
var choice ChatCompletionsStreamResponseChoice var choice ChatCompletionsStreamResponseChoice
choice.Delta.Content = baiduResponse.Result choice.Delta.Content = baiduResponse.Result
choice.FinishReason = "stop" if baiduResponse.IsEnd {
choice.FinishReason = &stopFinishReason
}
response := ChatCompletionsStreamResponse{ response := ChatCompletionsStreamResponse{
Id: baiduResponse.Id, Id: baiduResponse.Id,
Object: "chat.completion.chunk", Object: "chat.completion.chunk",

View File

@ -81,7 +81,10 @@ func requestOpenAI2Claude(textRequest GeneralOpenAIRequest) *ClaudeRequest {
func streamResponseClaude2OpenAI(claudeResponse *ClaudeResponse) *ChatCompletionsStreamResponse { func streamResponseClaude2OpenAI(claudeResponse *ClaudeResponse) *ChatCompletionsStreamResponse {
var choice ChatCompletionsStreamResponseChoice var choice ChatCompletionsStreamResponseChoice
choice.Delta.Content = claudeResponse.Completion choice.Delta.Content = claudeResponse.Completion
choice.FinishReason = stopReasonClaude2OpenAI(claudeResponse.StopReason) finishReason := stopReasonClaude2OpenAI(claudeResponse.StopReason)
if finishReason != "null" {
choice.FinishReason = &finishReason
}
var response ChatCompletionsStreamResponse var response ChatCompletionsStreamResponse
response.Object = "chat.completion.chunk" response.Object = "chat.completion.chunk"
response.Model = claudeResponse.Model response.Model = claudeResponse.Model

View File

@ -94,7 +94,7 @@ func streamResponsePaLM2OpenAI(palmResponse *PaLMChatResponse) *ChatCompletionsS
if len(palmResponse.Candidates) > 0 { if len(palmResponse.Candidates) > 0 {
choice.Delta.Content = palmResponse.Candidates[0].Content choice.Delta.Content = palmResponse.Candidates[0].Content
} }
choice.FinishReason = "stop" choice.FinishReason = &stopFinishReason
var response ChatCompletionsStreamResponse var response ChatCompletionsStreamResponse
response.Object = "chat.completion.chunk" response.Object = "chat.completion.chunk"
response.Model = "palm2" response.Model = "palm2"

View File

@ -6,6 +6,8 @@ import (
"one-api/common" "one-api/common"
) )
var stopFinishReason = "stop"
var tokenEncoderMap = map[string]*tiktoken.Tiktoken{} var tokenEncoderMap = map[string]*tiktoken.Tiktoken{}
func getTokenEncoder(model string) *tiktoken.Tiktoken { func getTokenEncoder(model string) *tiktoken.Tiktoken {

View File

@ -138,6 +138,9 @@ func streamResponseXunfei2OpenAI(xunfeiResponse *XunfeiChatResponse) *ChatComple
} }
var choice ChatCompletionsStreamResponseChoice var choice ChatCompletionsStreamResponseChoice
choice.Delta.Content = xunfeiResponse.Payload.Choices.Text[0].Content choice.Delta.Content = xunfeiResponse.Payload.Choices.Text[0].Content
if xunfeiResponse.Payload.Choices.Status == 2 {
choice.FinishReason = &stopFinishReason
}
response := ChatCompletionsStreamResponse{ response := ChatCompletionsStreamResponse{
Object: "chat.completion.chunk", Object: "chat.completion.chunk",
Created: common.GetTimestamp(), Created: common.GetTimestamp(),

View File

@ -163,7 +163,6 @@ func responseZhipu2OpenAI(response *ZhipuResponse) *OpenAITextResponse {
func streamResponseZhipu2OpenAI(zhipuResponse string) *ChatCompletionsStreamResponse { func streamResponseZhipu2OpenAI(zhipuResponse string) *ChatCompletionsStreamResponse {
var choice ChatCompletionsStreamResponseChoice var choice ChatCompletionsStreamResponseChoice
choice.Delta.Content = zhipuResponse choice.Delta.Content = zhipuResponse
choice.FinishReason = ""
response := ChatCompletionsStreamResponse{ response := ChatCompletionsStreamResponse{
Object: "chat.completion.chunk", Object: "chat.completion.chunk",
Created: common.GetTimestamp(), Created: common.GetTimestamp(),
@ -176,7 +175,7 @@ func streamResponseZhipu2OpenAI(zhipuResponse string) *ChatCompletionsStreamResp
func streamMetaResponseZhipu2OpenAI(zhipuResponse *ZhipuStreamMetaResponse) (*ChatCompletionsStreamResponse, *Usage) { func streamMetaResponseZhipu2OpenAI(zhipuResponse *ZhipuStreamMetaResponse) (*ChatCompletionsStreamResponse, *Usage) {
var choice ChatCompletionsStreamResponseChoice var choice ChatCompletionsStreamResponseChoice
choice.Delta.Content = "" choice.Delta.Content = ""
choice.FinishReason = "stop" choice.FinishReason = &stopFinishReason
response := ChatCompletionsStreamResponse{ response := ChatCompletionsStreamResponse{
Id: zhipuResponse.RequestId, Id: zhipuResponse.RequestId,
Object: "chat.completion.chunk", Object: "chat.completion.chunk",

View File

@ -124,7 +124,7 @@ type ChatCompletionsStreamResponseChoice struct {
Delta struct { Delta struct {
Content string `json:"content"` Content string `json:"content"`
} `json:"delta"` } `json:"delta"`
FinishReason string `json:"finish_reason,omitempty"` FinishReason *string `json:"finish_reason"`
} }
type ChatCompletionsStreamResponse struct { type ChatCompletionsStreamResponse struct {
@ -176,7 +176,7 @@ func Relay(c *gin.Context) {
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?retry=%d", c.Request.URL.Path, retryTimes-1)) c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?retry=%d", c.Request.URL.Path, retryTimes-1))
} else { } else {
if err.StatusCode == http.StatusTooManyRequests { if err.StatusCode == http.StatusTooManyRequests {
err.OpenAIError.Message = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。" err.OpenAIError.Message = "当前分组上游负载已饱和,请稍后再试"
} }
c.JSON(err.StatusCode, gin.H{ c.JSON(err.StatusCode, gin.H{
"error": err.OpenAIError, "error": err.OpenAIError,

View File

@ -109,10 +109,10 @@ func AddToken(c *gin.Context) {
}) })
return return
} }
if len(token.Name) == 0 || len(token.Name) > 20 { if len(token.Name) == 0 || len(token.Name) > 30 {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": "令牌名称度必须在1-20之间", "message": "令牌名称长",
}) })
return return
} }
@ -171,6 +171,13 @@ func UpdateToken(c *gin.Context) {
}) })
return return
} }
if len(token.Name) == 0 || len(token.Name) > 30 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "令牌名称过长",
})
return
}
cleanToken, err := model.GetTokenByIds(token.Id, userId) cleanToken, err := model.GetTokenByIds(token.Id, userId)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{

View File

@ -39,8 +39,8 @@
"兑换码个数必须大于0": "The number of redemption codes must be greater than 0", "兑换码个数必须大于0": "The number of redemption codes must be greater than 0",
"一次兑换码批量生成的个数不能大于 100": "The number of redemption codes generated in a batch cannot be greater than 100", "一次兑换码批量生成的个数不能大于 100": "The number of redemption codes generated in a batch cannot be greater than 100",
"通过令牌「%s」使用模型 %s 消耗 %s模型倍率 %.2f,分组倍率 %.2f": "Using model %s with token %s consumes %s (model rate %.2f, group rate %.2f)", "通过令牌「%s」使用模型 %s 消耗 %s模型倍率 %.2f,分组倍率 %.2f": "Using model %s with token %s consumes %s (model rate %.2f, group rate %.2f)",
"当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。": "The current group load is saturated, please try again later, or upgrade your account to improve service quality.", "当前分组上游负载已饱和,请稍后再试": "The current group load is saturated, please try again later",
"令牌名称长度必须在1-20之间": "The length of the token name must be between 1-20", "令牌名称过长": "Token name is too long",
"令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "The token has expired and cannot be enabled. Please modify the expiration time of the token, or set it to never expire.", "令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "The token has expired and cannot be enabled. Please modify the expiration time of the token, or set it to never expire.",
"令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "The available quota of the token has been used up and cannot be enabled. Please modify the remaining quota of the token, or set it to unlimited quota", "令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "The available quota of the token has been used up and cannot be enabled. Please modify the remaining quota of the token, or set it to unlimited quota",
"管理员关闭了密码登录": "The administrator has turned off password login", "管理员关闭了密码登录": "The administrator has turned off password login",
@ -229,7 +229,7 @@
"已是最新版本": "Is the latest version", "已是最新版本": "Is the latest version",
"检查更新": "Check for updates", "检查更新": "Check for updates",
"公告": "Announcement", "公告": "Announcement",
"在此输入新的公告内容": "Enter new announcement content here", "在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code",
"保存公告": "Save Announcement", "保存公告": "Save Announcement",
"个性化设置": "Personalization Settings", "个性化设置": "Personalization Settings",
"系统名称": "System Name", "系统名称": "System Name",
@ -518,5 +518,6 @@
",图片演示。": "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", "代理": "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" "此项可选,用于通过代理站来进行 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",
"取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?"
} }

View File

@ -6,6 +6,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"one-api/common" "one-api/common"
"os" "os"
"time"
) )
var DB *gorm.DB var DB *gorm.DB
@ -57,10 +58,18 @@ func InitDB() (err error) {
common.SysLog("database connected") common.SysLog("database connected")
if err == nil { if err == nil {
DB = db DB = db
sqlDB, err := DB.DB()
if err != nil {
return err
}
sqlDB.SetMaxIdleConns(common.GetOrDefault("SQL_MAX_IDLE_CONNS", 10))
sqlDB.SetMaxOpenConns(common.GetOrDefault("SQL_MAX_OPEN_CONNS", 100))
sqlDB.SetConnMaxLifetime(time.Second * time.Duration(common.GetOrDefault("SQL_MAX_LIFETIME", 60)))
if !common.IsMasterNode { if !common.IsMasterNode {
return nil return nil
} }
err := db.AutoMigrate(&Channel{}) err = db.AutoMigrate(&Channel{})
if err != nil { if err != nil {
return err return err
} }

View File

@ -447,8 +447,8 @@ const ChannelsTable = () => {
<Button size='small' loading={loading} onClick={testAllChannels}> <Button size='small' loading={loading} onClick={testAllChannels}>
测试所有已启用通道 测试所有已启用通道
</Button> </Button>
<Button size='small' onClick={updateAllChannelsBalance} {/* <Button size='small' onClick={updateAllChannelsBalance}
loading={loading || updatingBalance}>更新所有已启用通道余额</Button> loading={loading || updatingBalance}>更新所有已启用通道余额</Button> */}
<Pagination <Pagination
floated='right' floated='right'
activePage={activePage} activePage={activePage}

View File

@ -112,7 +112,7 @@ const OtherSetting = () => {
<Form.Group widths='equal'> <Form.Group widths='equal'>
<Form.TextArea <Form.TextArea
label='公告' label='公告'
placeholder='在此输入新的公告内容' placeholder='在此输入新的公告内容,支持 Markdown & HTML 代码'
value={inputs.Notice} value={inputs.Notice}
name='Notice' name='Notice'
onChange={handleInputChange} onChange={handleInputChange}

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Label, Message, Pagination, Table } from 'semantic-ui-react'; import { Button, Form, Label, Popup, Pagination, Table } from 'semantic-ui-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string } from '../helpers'; import { API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string } from '../helpers';
@ -240,15 +240,25 @@ const RedemptionsTable = () => {
> >
复制 复制
</Button> </Button>
<Popup
trigger={
<Button size='small' negative>
删除
</Button>
}
on='click'
flowing
hoverable
>
<Button <Button
size={'small'}
negative negative
onClick={() => { onClick={() => {
manageRedemption(redemption.id, 'delete', idx); manageRedemption(redemption.id, 'delete', idx);
}} }}
> >
删除 确认删除
</Button> </Button>
</Popup>
<Button <Button
size={'small'} size={'small'}
disabled={redemption.status === 3} // used disabled={redemption.status === 3} // used

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Divider, Form, Grid, Header, Input, Message } from 'semantic-ui-react'; import { Button, Divider, Form, Grid, Header, Modal, Message } from 'semantic-ui-react';
import { API, removeTrailingSlash, showError } from '../helpers'; import { API, removeTrailingSlash, showError } from '../helpers';
const SystemSetting = () => { const SystemSetting = () => {
@ -33,6 +33,7 @@ const SystemSetting = () => {
let [loading, setLoading] = useState(false); let [loading, setLoading] = useState(false);
const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]); const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
const [restrictedDomainInput, setRestrictedDomainInput] = useState(''); const [restrictedDomainInput, setRestrictedDomainInput] = useState('');
const [showPasswordWarningModal, setShowPasswordWarningModal] = useState(false);
const getOptions = async () => { const getOptions = async () => {
const res = await API.get('/api/option/'); const res = await API.get('/api/option/');
@ -95,6 +96,11 @@ const SystemSetting = () => {
}; };
const handleInputChange = async (e, { name, value }) => { const handleInputChange = async (e, { name, value }) => {
if (name === 'PasswordLoginEnabled' && inputs[name] === 'true') {
// block disabling password login
setShowPasswordWarningModal(true);
return;
}
if ( if (
name === 'Notice' || name === 'Notice' ||
name.startsWith('SMTP') || name.startsWith('SMTP') ||
@ -243,6 +249,32 @@ const SystemSetting = () => {
name='PasswordLoginEnabled' name='PasswordLoginEnabled'
onChange={handleInputChange} onChange={handleInputChange}
/> />
{
showPasswordWarningModal &&
<Modal
open={showPasswordWarningModal}
onClose={() => setShowPasswordWarningModal(false)}
size={'tiny'}
style={{ maxWidth: '450px' }}
>
<Modal.Header>警告</Modal.Header>
<Modal.Content>
<p>取消密码登录将导致所有未绑定其他登录方式的用户包括管理员无法通过密码登录确认取消</p>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => setShowPasswordWarningModal(false)}>取消</Button>
<Button
color='yellow'
onClick={async () => {
setShowPasswordWarningModal(false);
await updateOption('PasswordLoginEnabled', 'false');
}}
>
确定
</Button>
</Modal.Actions>
</Modal>
}
<Form.Checkbox <Form.Checkbox
checked={inputs.PasswordRegisterEnabled === 'true'} checked={inputs.PasswordRegisterEnabled === 'true'}
label='允许通过密码进行注册' label='允许通过密码进行注册'

View File

@ -1,6 +1,11 @@
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { toastConstants } from '../constants'; import { toastConstants } from '../constants';
import React from 'react';
const HTMLToastContent = ({ htmlContent }) => {
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
};
export default HTMLToastContent;
export function isAdmin() { export function isAdmin() {
let user = localStorage.getItem('user'); let user = localStorage.getItem('user');
if (!user) return false; if (!user) return false;
@ -107,9 +112,13 @@ export function showInfo(message) {
toast.info(message, showInfoOptions); toast.info(message, showInfoOptions);
} }
export function showNotice(message) { export function showNotice(message, isHTML = false) {
if (isHTML) {
toast(<HTMLToastContent htmlContent={message} />, showNoticeOptions);
} else {
toast.info(message, showNoticeOptions); toast.info(message, showNoticeOptions);
} }
}
export function openPage(url) { export function openPage(url) {
window.open(url); window.open(url);

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react'; import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react';
import { useParams } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers'; import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers';
import { CHANNEL_OPTIONS } from '../../constants'; import { CHANNEL_OPTIONS } from '../../constants';
@ -12,9 +12,14 @@ const MODEL_MAPPING_EXAMPLE = {
const EditChannel = () => { const EditChannel = () => {
const params = useParams(); const params = useParams();
const navigate = useNavigate();
const channelId = params.id; const channelId = params.id;
const isEdit = channelId !== undefined; const isEdit = channelId !== undefined;
const [loading, setLoading] = useState(isEdit); const [loading, setLoading] = useState(isEdit);
const handleCancel = () => {
navigate('/channel');
};
const originInputs = { const originInputs = {
name: '', name: '',
type: 1, type: 1,
@ -381,6 +386,7 @@ const EditChannel = () => {
</Form.Field> </Form.Field>
) )
} }
<Button onClick={handleCancel}>取消</Button>
<Button type={isEdit ? 'button' : 'submit'} positive onClick={submit}>提交</Button> <Button type={isEdit ? 'button' : 'submit'} positive onClick={submit}>提交</Button>
</Form> </Form>
</Segment> </Segment>

View File

@ -15,7 +15,8 @@ const Home = () => {
if (success) { if (success) {
let oldNotice = localStorage.getItem('notice'); let oldNotice = localStorage.getItem('notice');
if (data !== oldNotice && data !== '') { if (data !== oldNotice && data !== '') {
showNotice(data); const htmlNotice = marked(data);
showNotice(htmlNotice, true);
localStorage.setItem('notice', data); localStorage.setItem('notice', data);
} }
} else { } else {

View File

@ -1,11 +1,12 @@
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, downloadTextAsFile, showError, showSuccess } from '../../helpers'; import { API, downloadTextAsFile, showError, showSuccess } from '../../helpers';
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
const EditRedemption = () => { const EditRedemption = () => {
const params = useParams(); const params = useParams();
const navigate = useNavigate();
const redemptionId = params.id; const redemptionId = params.id;
const isEdit = redemptionId !== undefined; const isEdit = redemptionId !== undefined;
const [loading, setLoading] = useState(isEdit); const [loading, setLoading] = useState(isEdit);
@ -17,6 +18,10 @@ const EditRedemption = () => {
const [inputs, setInputs] = useState(originInputs); const [inputs, setInputs] = useState(originInputs);
const { name, quota, count } = inputs; const { name, quota, count } = inputs;
const handleCancel = () => {
navigate('/redemption');
};
const handleInputChange = (e, { name, value }) => { const handleInputChange = (e, { name, value }) => {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
}; };
@ -113,6 +118,7 @@ const EditRedemption = () => {
</> </>
} }
<Button positive onClick={submit}>提交</Button> <Button positive onClick={submit}>提交</Button>
<Button onClick={handleCancel}>取消</Button>
</Form> </Form>
</Segment> </Segment>
</> </>