Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
51e02785
authored
Sep 29, 2025
by
comeback01
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'main' into traduction
# Conflicts: # web/src/i18n/locales/zh.json
parents
cbbe4cb4
78b9b21a
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
413 additions
and
64 deletions
+413
-64
controller/channel.go
+3
-0
controller/topup_stripe.go
+2
-1
dto/gemini.go
+1
-0
main.go
+18
-0
model/option.go
+3
-0
relay/channel/gemini/relay-gemini.go
+10
-0
service/http_client.go
+39
-5
setting/payment_stripe.go
+1
-0
web/index.html
+1
-0
web/src/components/settings/PaymentSetting.jsx
+1
-0
web/src/components/settings/PersonalSetting.jsx
+39
-10
web/src/components/settings/personal/cards/AccountManagement.jsx
+53
-20
web/src/components/table/channels/modals/EditChannelModal.jsx
+105
-23
web/src/components/table/task-logs/modals/ContentModal.jsx
+110
-4
web/src/i18n/locales/en.json
+1
-0
web/src/i18n/locales/zh.json
+2
-1
web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx
+24
-0
No files found.
controller/channel.go
View file @
51e02785
...
@@ -8,6 +8,7 @@ import (
...
@@ -8,6 +8,7 @@ import (
"one-api/constant"
"one-api/constant"
"one-api/dto"
"one-api/dto"
"one-api/model"
"one-api/model"
"one-api/service"
"strconv"
"strconv"
"strings"
"strings"
...
@@ -633,6 +634,7 @@ func AddChannel(c *gin.Context) {
...
@@ -633,6 +634,7 @@ func AddChannel(c *gin.Context) {
common
.
ApiError
(
c
,
err
)
common
.
ApiError
(
c
,
err
)
return
return
}
}
service
.
ResetProxyClientCache
()
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"success"
:
true
,
"message"
:
""
,
"message"
:
""
,
...
@@ -894,6 +896,7 @@ func UpdateChannel(c *gin.Context) {
...
@@ -894,6 +896,7 @@ func UpdateChannel(c *gin.Context) {
return
return
}
}
model
.
InitChannelCache
()
model
.
InitChannelCache
()
service
.
ResetProxyClientCache
()
channel
.
Key
=
""
channel
.
Key
=
""
clearChannelInfo
(
&
channel
.
Channel
)
clearChannelInfo
(
&
channel
.
Channel
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
...
...
controller/topup_stripe.go
View file @
51e02785
...
@@ -225,7 +225,8 @@ func genStripeLink(referenceId string, customerId string, email string, amount i
...
@@ -225,7 +225,8 @@ func genStripeLink(referenceId string, customerId string, email string, amount i
Quantity
:
stripe
.
Int64
(
amount
),
Quantity
:
stripe
.
Int64
(
amount
),
},
},
},
},
Mode
:
stripe
.
String
(
string
(
stripe
.
CheckoutSessionModePayment
)),
Mode
:
stripe
.
String
(
string
(
stripe
.
CheckoutSessionModePayment
)),
AllowPromotionCodes
:
stripe
.
Bool
(
setting
.
StripePromotionCodesEnabled
),
}
}
if
""
==
customerId
{
if
""
==
customerId
{
...
...
dto/gemini.go
View file @
51e02785
...
@@ -251,6 +251,7 @@ type GeminiChatTool struct {
...
@@ -251,6 +251,7 @@ type GeminiChatTool struct {
GoogleSearchRetrieval
any
`json:"googleSearchRetrieval,omitempty"`
GoogleSearchRetrieval
any
`json:"googleSearchRetrieval,omitempty"`
CodeExecution
any
`json:"codeExecution,omitempty"`
CodeExecution
any
`json:"codeExecution,omitempty"`
FunctionDeclarations
any
`json:"functionDeclarations,omitempty"`
FunctionDeclarations
any
`json:"functionDeclarations,omitempty"`
URLContext
any
`json:"urlContext,omitempty"`
}
}
type
GeminiChatGenerationConfig
struct
{
type
GeminiChatGenerationConfig
struct
{
...
...
main.go
View file @
51e02785
package
main
package
main
import
(
import
(
"bytes"
"embed"
"embed"
"fmt"
"fmt"
"log"
"log"
...
@@ -16,6 +17,7 @@ import (
...
@@ -16,6 +17,7 @@ import (
"one-api/setting/ratio_setting"
"one-api/setting/ratio_setting"
"os"
"os"
"strconv"
"strconv"
"strings"
"time"
"time"
"github.com/bytedance/gopkg/util/gopool"
"github.com/bytedance/gopkg/util/gopool"
...
@@ -147,6 +149,22 @@ func main() {
...
@@ -147,6 +149,22 @@ func main() {
})
})
server
.
Use
(
sessions
.
Sessions
(
"session"
,
store
))
server
.
Use
(
sessions
.
Sessions
(
"session"
,
store
))
analyticsInjectBuilder
:=
&
strings
.
Builder
{}
if
os
.
Getenv
(
"UMAMI_WEBSITE_ID"
)
!=
""
{
umamiSiteID
:=
os
.
Getenv
(
"UMAMI_WEBSITE_ID"
)
umamiScriptURL
:=
os
.
Getenv
(
"UMAMI_SCRIPT_URL"
)
if
umamiScriptURL
==
""
{
umamiScriptURL
=
"https://analytics.umami.is/script.js"
}
analyticsInjectBuilder
.
WriteString
(
"<script defer src=
\"
"
)
analyticsInjectBuilder
.
WriteString
(
umamiScriptURL
)
analyticsInjectBuilder
.
WriteString
(
"
\"
data-website-id=
\"
"
)
analyticsInjectBuilder
.
WriteString
(
umamiSiteID
)
analyticsInjectBuilder
.
WriteString
(
"
\"
></script>"
)
}
analyticsInject
:=
analyticsInjectBuilder
.
String
()
indexPage
=
bytes
.
ReplaceAll
(
indexPage
,
[]
byte
(
"<analytics></analytics>
\n
"
),
[]
byte
(
analyticsInject
))
router
.
SetRouter
(
server
,
buildFS
,
indexPage
)
router
.
SetRouter
(
server
,
buildFS
,
indexPage
)
var
port
=
os
.
Getenv
(
"PORT"
)
var
port
=
os
.
Getenv
(
"PORT"
)
if
port
==
""
{
if
port
==
""
{
...
...
model/option.go
View file @
51e02785
...
@@ -82,6 +82,7 @@ func InitOptionMap() {
...
@@ -82,6 +82,7 @@ func InitOptionMap() {
common
.
OptionMap
[
"StripeWebhookSecret"
]
=
setting
.
StripeWebhookSecret
common
.
OptionMap
[
"StripeWebhookSecret"
]
=
setting
.
StripeWebhookSecret
common
.
OptionMap
[
"StripePriceId"
]
=
setting
.
StripePriceId
common
.
OptionMap
[
"StripePriceId"
]
=
setting
.
StripePriceId
common
.
OptionMap
[
"StripeUnitPrice"
]
=
strconv
.
FormatFloat
(
setting
.
StripeUnitPrice
,
'f'
,
-
1
,
64
)
common
.
OptionMap
[
"StripeUnitPrice"
]
=
strconv
.
FormatFloat
(
setting
.
StripeUnitPrice
,
'f'
,
-
1
,
64
)
common
.
OptionMap
[
"StripePromotionCodesEnabled"
]
=
strconv
.
FormatBool
(
setting
.
StripePromotionCodesEnabled
)
common
.
OptionMap
[
"TopupGroupRatio"
]
=
common
.
TopupGroupRatio2JSONString
()
common
.
OptionMap
[
"TopupGroupRatio"
]
=
common
.
TopupGroupRatio2JSONString
()
common
.
OptionMap
[
"Chats"
]
=
setting
.
Chats2JsonString
()
common
.
OptionMap
[
"Chats"
]
=
setting
.
Chats2JsonString
()
common
.
OptionMap
[
"AutoGroups"
]
=
setting
.
AutoGroups2JsonString
()
common
.
OptionMap
[
"AutoGroups"
]
=
setting
.
AutoGroups2JsonString
()
...
@@ -330,6 +331,8 @@ func updateOptionMap(key string, value string) (err error) {
...
@@ -330,6 +331,8 @@ func updateOptionMap(key string, value string) (err error) {
setting
.
StripeUnitPrice
,
_
=
strconv
.
ParseFloat
(
value
,
64
)
setting
.
StripeUnitPrice
,
_
=
strconv
.
ParseFloat
(
value
,
64
)
case
"StripeMinTopUp"
:
case
"StripeMinTopUp"
:
setting
.
StripeMinTopUp
,
_
=
strconv
.
Atoi
(
value
)
setting
.
StripeMinTopUp
,
_
=
strconv
.
Atoi
(
value
)
case
"StripePromotionCodesEnabled"
:
setting
.
StripePromotionCodesEnabled
=
value
==
"true"
case
"TopupGroupRatio"
:
case
"TopupGroupRatio"
:
err
=
common
.
UpdateTopupGroupRatioByJSONString
(
value
)
err
=
common
.
UpdateTopupGroupRatioByJSONString
(
value
)
case
"GitHubClientId"
:
case
"GitHubClientId"
:
...
...
relay/channel/gemini/relay-gemini.go
View file @
51e02785
...
@@ -245,6 +245,7 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
...
@@ -245,6 +245,7 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
functions
:=
make
([]
dto
.
FunctionRequest
,
0
,
len
(
textRequest
.
Tools
))
functions
:=
make
([]
dto
.
FunctionRequest
,
0
,
len
(
textRequest
.
Tools
))
googleSearch
:=
false
googleSearch
:=
false
codeExecution
:=
false
codeExecution
:=
false
urlContext
:=
false
for
_
,
tool
:=
range
textRequest
.
Tools
{
for
_
,
tool
:=
range
textRequest
.
Tools
{
if
tool
.
Function
.
Name
==
"googleSearch"
{
if
tool
.
Function
.
Name
==
"googleSearch"
{
googleSearch
=
true
googleSearch
=
true
...
@@ -254,6 +255,10 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
...
@@ -254,6 +255,10 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
codeExecution
=
true
codeExecution
=
true
continue
continue
}
}
if
tool
.
Function
.
Name
==
"urlContext"
{
urlContext
=
true
continue
}
if
tool
.
Function
.
Parameters
!=
nil
{
if
tool
.
Function
.
Parameters
!=
nil
{
params
,
ok
:=
tool
.
Function
.
Parameters
.
(
map
[
string
]
interface
{})
params
,
ok
:=
tool
.
Function
.
Parameters
.
(
map
[
string
]
interface
{})
...
@@ -281,6 +286,11 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
...
@@ -281,6 +286,11 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
GoogleSearch
:
make
(
map
[
string
]
string
),
GoogleSearch
:
make
(
map
[
string
]
string
),
})
})
}
}
if
urlContext
{
geminiTools
=
append
(
geminiTools
,
dto
.
GeminiChatTool
{
URLContext
:
make
(
map
[
string
]
string
),
})
}
if
len
(
functions
)
>
0
{
if
len
(
functions
)
>
0
{
geminiTools
=
append
(
geminiTools
,
dto
.
GeminiChatTool
{
geminiTools
=
append
(
geminiTools
,
dto
.
GeminiChatTool
{
FunctionDeclarations
:
functions
,
FunctionDeclarations
:
functions
,
...
...
service/http_client.go
View file @
51e02785
...
@@ -7,12 +7,17 @@ import (
...
@@ -7,12 +7,17 @@ import (
"net/http"
"net/http"
"net/url"
"net/url"
"one-api/common"
"one-api/common"
"sync"
"time"
"time"
"golang.org/x/net/proxy"
"golang.org/x/net/proxy"
)
)
var
httpClient
*
http
.
Client
var
(
httpClient
*
http
.
Client
proxyClientLock
sync
.
Mutex
proxyClients
=
make
(
map
[
string
]
*
http
.
Client
)
)
func
InitHttpClient
()
{
func
InitHttpClient
()
{
if
common
.
RelayTimeout
==
0
{
if
common
.
RelayTimeout
==
0
{
...
@@ -28,12 +33,31 @@ func GetHttpClient() *http.Client {
...
@@ -28,12 +33,31 @@ func GetHttpClient() *http.Client {
return
httpClient
return
httpClient
}
}
// ResetProxyClientCache 清空代理客户端缓存,确保下次使用时重新初始化
func
ResetProxyClientCache
()
{
proxyClientLock
.
Lock
()
defer
proxyClientLock
.
Unlock
()
for
_
,
client
:=
range
proxyClients
{
if
transport
,
ok
:=
client
.
Transport
.
(
*
http
.
Transport
);
ok
&&
transport
!=
nil
{
transport
.
CloseIdleConnections
()
}
}
proxyClients
=
make
(
map
[
string
]
*
http
.
Client
)
}
// NewProxyHttpClient 创建支持代理的 HTTP 客户端
// NewProxyHttpClient 创建支持代理的 HTTP 客户端
func
NewProxyHttpClient
(
proxyURL
string
)
(
*
http
.
Client
,
error
)
{
func
NewProxyHttpClient
(
proxyURL
string
)
(
*
http
.
Client
,
error
)
{
if
proxyURL
==
""
{
if
proxyURL
==
""
{
return
http
.
DefaultClient
,
nil
return
http
.
DefaultClient
,
nil
}
}
proxyClientLock
.
Lock
()
if
client
,
ok
:=
proxyClients
[
proxyURL
];
ok
{
proxyClientLock
.
Unlock
()
return
client
,
nil
}
proxyClientLock
.
Unlock
()
parsedURL
,
err
:=
url
.
Parse
(
proxyURL
)
parsedURL
,
err
:=
url
.
Parse
(
proxyURL
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
...
@@ -41,11 +65,16 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
...
@@ -41,11 +65,16 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
switch
parsedURL
.
Scheme
{
switch
parsedURL
.
Scheme
{
case
"http"
,
"https"
:
case
"http"
,
"https"
:
return
&
http
.
Client
{
client
:=
&
http
.
Client
{
Transport
:
&
http
.
Transport
{
Transport
:
&
http
.
Transport
{
Proxy
:
http
.
ProxyURL
(
parsedURL
),
Proxy
:
http
.
ProxyURL
(
parsedURL
),
},
},
},
nil
}
client
.
Timeout
=
time
.
Duration
(
common
.
RelayTimeout
)
*
time
.
Second
proxyClientLock
.
Lock
()
proxyClients
[
proxyURL
]
=
client
proxyClientLock
.
Unlock
()
return
client
,
nil
case
"socks5"
,
"socks5h"
:
case
"socks5"
,
"socks5h"
:
// 获取认证信息
// 获取认证信息
...
@@ -67,13 +96,18 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
...
@@ -67,13 +96,18 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
return
nil
,
err
return
nil
,
err
}
}
return
&
http
.
Client
{
client
:=
&
http
.
Client
{
Transport
:
&
http
.
Transport
{
Transport
:
&
http
.
Transport
{
DialContext
:
func
(
ctx
context
.
Context
,
network
,
addr
string
)
(
net
.
Conn
,
error
)
{
DialContext
:
func
(
ctx
context
.
Context
,
network
,
addr
string
)
(
net
.
Conn
,
error
)
{
return
dialer
.
Dial
(
network
,
addr
)
return
dialer
.
Dial
(
network
,
addr
)
},
},
},
},
},
nil
}
client
.
Timeout
=
time
.
Duration
(
common
.
RelayTimeout
)
*
time
.
Second
proxyClientLock
.
Lock
()
proxyClients
[
proxyURL
]
=
client
proxyClientLock
.
Unlock
()
return
client
,
nil
default
:
default
:
return
nil
,
fmt
.
Errorf
(
"unsupported proxy scheme: %s"
,
parsedURL
.
Scheme
)
return
nil
,
fmt
.
Errorf
(
"unsupported proxy scheme: %s"
,
parsedURL
.
Scheme
)
...
...
setting/payment_stripe.go
View file @
51e02785
...
@@ -5,3 +5,4 @@ var StripeWebhookSecret = ""
...
@@ -5,3 +5,4 @@ var StripeWebhookSecret = ""
var
StripePriceId
=
""
var
StripePriceId
=
""
var
StripeUnitPrice
=
8.0
var
StripeUnitPrice
=
8.0
var
StripeMinTopUp
=
1
var
StripeMinTopUp
=
1
var
StripePromotionCodesEnabled
=
false
web/index.html
View file @
51e02785
...
@@ -10,6 +10,7 @@
...
@@ -10,6 +10,7 @@
content=
"OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
content=
"OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
/>
/>
<title>
New API
</title>
<title>
New API
</title>
<analytics></analytics>
</head>
</head>
<body>
<body>
...
...
web/src/components/settings/PaymentSetting.jsx
View file @
51e02785
...
@@ -45,6 +45,7 @@ const PaymentSetting = () => {
...
@@ -45,6 +45,7 @@ const PaymentSetting = () => {
StripePriceId
:
''
,
StripePriceId
:
''
,
StripeUnitPrice
:
8.0
,
StripeUnitPrice
:
8.0
,
StripeMinTopUp
:
1
,
StripeMinTopUp
:
1
,
StripePromotionCodesEnabled
:
false
,
});
});
let
[
loading
,
setLoading
]
=
useState
(
false
);
let
[
loading
,
setLoading
]
=
useState
(
false
);
...
...
web/src/components/settings/PersonalSetting.jsx
View file @
51e02785
...
@@ -19,7 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
...
@@ -19,7 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
import
React
,
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
React
,
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
useNavigate
}
from
'react-router-dom'
;
import
{
useNavigate
}
from
'react-router-dom'
;
import
{
API
,
copy
,
showError
,
showInfo
,
showSuccess
}
from
'../../helpers'
;
import
{
API
,
copy
,
showError
,
showInfo
,
showSuccess
,
setStatusData
,
}
from
'../../helpers'
;
import
{
UserContext
}
from
'../../context/User'
;
import
{
UserContext
}
from
'../../context/User'
;
import
{
Modal
}
from
'@douyinfe/semi-ui'
;
import
{
Modal
}
from
'@douyinfe/semi-ui'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
useTranslation
}
from
'react-i18next'
;
...
@@ -71,18 +78,40 @@ const PersonalSetting = () => {
...
@@ -71,18 +78,40 @@ const PersonalSetting = () => {
});
});
useEffect
(()
=>
{
useEffect
(()
=>
{
let
s
tatus
=
localStorage
.
getItem
(
'status'
);
let
s
aved
=
localStorage
.
getItem
(
'status'
);
if
(
s
tatus
)
{
if
(
s
aved
)
{
status
=
JSON
.
parse
(
status
);
const
parsed
=
JSON
.
parse
(
saved
);
setStatus
(
status
);
setStatus
(
parsed
);
if
(
status
.
turnstile_check
)
{
if
(
parsed
.
turnstile_check
)
{
setTurnstileEnabled
(
true
);
setTurnstileEnabled
(
true
);
setTurnstileSiteKey
(
status
.
turnstile_site_key
);
setTurnstileSiteKey
(
parsed
.
turnstile_site_key
);
}
else
{
setTurnstileEnabled
(
false
);
setTurnstileSiteKey
(
''
);
}
}
}
}
getUserData
().
then
((
res
)
=>
{
// Always refresh status from server to avoid stale flags (e.g., admin just enabled OAuth)
console
.
log
(
userState
);
(
async
()
=>
{
});
try
{
const
res
=
await
API
.
get
(
'/api/status'
);
const
{
success
,
data
}
=
res
.
data
;
if
(
success
&&
data
)
{
setStatus
(
data
);
setStatusData
(
data
);
if
(
data
.
turnstile_check
)
{
setTurnstileEnabled
(
true
);
setTurnstileSiteKey
(
data
.
turnstile_site_key
);
}
else
{
setTurnstileEnabled
(
false
);
setTurnstileSiteKey
(
''
);
}
}
}
catch
(
e
)
{
// ignore and keep local status
}
})();
getUserData
();
},
[]);
},
[]);
useEffect
(()
=>
{
useEffect
(()
=>
{
...
...
web/src/components/settings/personal/cards/AccountManagement.jsx
View file @
51e02785
...
@@ -28,6 +28,7 @@ import {
...
@@ -28,6 +28,7 @@ import {
Tabs
,
Tabs
,
TabPane
,
TabPane
,
Popover
,
Popover
,
Modal
,
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
import
{
import
{
IconMail
,
IconMail
,
...
@@ -83,6 +84,9 @@ const AccountManagement = ({
...
@@ -83,6 +84,9 @@ const AccountManagement = ({
</
Popover
>
</
Popover
>
);
);
};
};
const
isBound
=
(
accountId
)
=>
Boolean
(
accountId
);
const
[
showTelegramBindModal
,
setShowTelegramBindModal
]
=
React
.
useState
(
false
);
return
(
return
(
<
Card
className=
'!rounded-2xl'
>
<
Card
className=
'!rounded-2xl'
>
{
/* 卡片头部 */
}
{
/* 卡片头部 */
}
...
@@ -142,7 +146,7 @@ const AccountManagement = ({
...
@@ -142,7 +146,7 @@ const AccountManagement = ({
size=
'small'
size=
'small'
onClick=
{
()
=>
setShowEmailBindModal
(
true
)
}
onClick=
{
()
=>
setShowEmailBindModal
(
true
)
}
>
>
{
userState
.
user
&&
userState
.
user
.
email
!==
''
{
isBound
(
userState
.
user
?.
email
)
?
t
(
'修改绑定'
)
?
t
(
'修改绑定'
)
:
t
(
'绑定'
)
}
:
t
(
'绑定'
)
}
</
Button
>
</
Button
>
...
@@ -165,9 +169,11 @@ const AccountManagement = ({
...
@@ -165,9 +169,11 @@ const AccountManagement = ({
{
t
(
'微信'
)
}
{
t
(
'微信'
)
}
</
div
>
</
div
>
<
div
className=
'text-sm text-gray-500 truncate'
>
<
div
className=
'text-sm text-gray-500 truncate'
>
{
userState
.
user
&&
userState
.
user
.
wechat_id
!==
''
{
!
status
.
wechat_login
?
t
(
'已绑定'
)
?
t
(
'未启用'
)
:
t
(
'未绑定'
)
}
:
isBound
(
userState
.
user
?.
wechat_id
)
?
t
(
'已绑定'
)
:
t
(
'未绑定'
)
}
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
...
@@ -179,7 +185,7 @@ const AccountManagement = ({
...
@@ -179,7 +185,7 @@ const AccountManagement = ({
disabled=
{
!
status
.
wechat_login
}
disabled=
{
!
status
.
wechat_login
}
onClick=
{
()
=>
setShowWeChatBindModal
(
true
)
}
onClick=
{
()
=>
setShowWeChatBindModal
(
true
)
}
>
>
{
userState
.
user
&&
userState
.
user
.
wechat_id
!==
''
{
isBound
(
userState
.
user
?.
wechat_id
)
?
t
(
'修改绑定'
)
?
t
(
'修改绑定'
)
:
status
.
wechat_login
:
status
.
wechat_login
?
t
(
'绑定'
)
?
t
(
'绑定'
)
...
@@ -220,8 +226,7 @@ const AccountManagement = ({
...
@@ -220,8 +226,7 @@ const AccountManagement = ({
onGitHubOAuthClicked
(
status
.
github_client_id
)
onGitHubOAuthClicked
(
status
.
github_client_id
)
}
}
disabled=
{
disabled=
{
(
userState
.
user
&&
userState
.
user
.
github_id
!==
''
)
||
isBound
(
userState
.
user
?.
github_id
)
||
!
status
.
github_oauth
!
status
.
github_oauth
}
}
>
>
{
status
.
github_oauth
?
t
(
'绑定'
)
:
t
(
'未启用'
)
}
{
status
.
github_oauth
?
t
(
'绑定'
)
:
t
(
'未启用'
)
}
...
@@ -264,8 +269,7 @@ const AccountManagement = ({
...
@@ -264,8 +269,7 @@ const AccountManagement = ({
)
)
}
}
disabled=
{
disabled=
{
(
userState
.
user
&&
userState
.
user
.
oidc_id
!==
''
)
||
isBound
(
userState
.
user
?.
oidc_id
)
||
!
status
.
oidc_enabled
!
status
.
oidc_enabled
}
}
>
>
{
status
.
oidc_enabled
?
t
(
'绑定'
)
:
t
(
'未启用'
)
}
{
status
.
oidc_enabled
?
t
(
'绑定'
)
:
t
(
'未启用'
)
}
...
@@ -298,26 +302,56 @@ const AccountManagement = ({
...
@@ -298,26 +302,56 @@ const AccountManagement = ({
</
div
>
</
div
>
<
div
className=
'flex-shrink-0'
>
<
div
className=
'flex-shrink-0'
>
{
status
.
telegram_oauth
?
(
{
status
.
telegram_oauth
?
(
userState
.
user
.
telegram_id
!==
''
?
(
isBound
(
userState
.
user
?.
telegram_id
)
?
(
<
Button
disabled=
{
true
}
size=
'small'
>
<
Button
disabled
size=
'small'
type=
'primary'
theme=
'outline'
>
{
t
(
'已绑定'
)
}
{
t
(
'已绑定'
)
}
</
Button
>
</
Button
>
)
:
(
)
:
(
<
div
className=
'scale-75'
>
<
Button
<
TelegramLoginButton
type=
'primary'
dataAuthUrl=
'/api/oauth/telegram/bind'
theme=
'outline'
botName=
{
status
.
telegram_bot_name
}
size=
'small'
/>
onClick=
{
()
=>
setShowTelegramBindModal
(
true
)
}
</
div
>
>
{
t
(
'绑定'
)
}
</
Button
>
)
)
)
:
(
)
:
(
<
Button
disabled=
{
true
}
size=
'small'
>
<
Button
disabled
size=
'small'
type=
'primary'
theme=
'outline'
>
{
t
(
'未启用'
)
}
{
t
(
'未启用'
)
}
</
Button
>
</
Button
>
)
}
)
}
</
div
>
</
div
>
</
div
>
</
div
>
</
Card
>
</
Card
>
<
Modal
title=
{
t
(
'绑定 Telegram'
)
}
visible=
{
showTelegramBindModal
}
onCancel=
{
()
=>
setShowTelegramBindModal
(
false
)
}
footer=
{
null
}
>
<
div
className=
'my-3 text-sm text-gray-600'
>
{
t
(
'点击下方按钮通过 Telegram 完成绑定'
)
}
</
div
>
<
div
className=
'flex justify-center'
>
<
div
className=
'scale-90'
>
<
TelegramLoginButton
dataAuthUrl=
'/api/oauth/telegram/bind'
botName=
{
status
.
telegram_bot_name
}
/>
</
div
>
</
div
>
</
Modal
>
{
/* LinuxDO绑定 */
}
{
/* LinuxDO绑定 */
}
<
Card
className=
'!rounded-xl'
>
<
Card
className=
'!rounded-xl'
>
...
@@ -350,8 +384,7 @@ const AccountManagement = ({
...
@@ -350,8 +384,7 @@ const AccountManagement = ({
onLinuxDOOAuthClicked
(
status
.
linuxdo_client_id
)
onLinuxDOOAuthClicked
(
status
.
linuxdo_client_id
)
}
}
disabled=
{
disabled=
{
(
userState
.
user
&&
userState
.
user
.
linux_do_id
!==
''
)
||
isBound
(
userState
.
user
?.
linux_do_id
)
||
!
status
.
linuxdo_oauth
!
status
.
linuxdo_oauth
}
}
>
>
{
status
.
linuxdo_oauth
?
t
(
'绑定'
)
:
t
(
'未启用'
)
}
{
status
.
linuxdo_oauth
?
t
(
'绑定'
)
:
t
(
'未启用'
)
}
...
...
web/src/components/table/channels/modals/EditChannelModal.jsx
View file @
51e02785
...
@@ -455,6 +455,14 @@ const EditChannelModal = (props) => {
...
@@ -455,6 +455,14 @@ const EditChannelModal = (props) => {
data
.
is_enterprise_account
=
false
;
data
.
is_enterprise_account
=
false
;
}
}
if
(
data
.
type
===
45
&&
(
!
data
.
base_url
||
(
typeof
data
.
base_url
===
'string'
&&
data
.
base_url
.
trim
()
===
''
))
)
{
data
.
base_url
=
'https://ark.cn-beijing.volces.com'
;
}
setInputs
(
data
);
setInputs
(
data
);
if
(
formApiRef
.
current
)
{
if
(
formApiRef
.
current
)
{
formApiRef
.
current
.
setValues
(
data
);
formApiRef
.
current
.
setValues
(
data
);
...
@@ -837,7 +845,9 @@ const EditChannelModal = (props) => {
...
@@ -837,7 +845,9 @@ const EditChannelModal = (props) => {
delete
localInputs
.
key
;
delete
localInputs
.
key
;
}
}
}
else
{
}
else
{
localInputs
.
key
=
batch
?
JSON
.
stringify
(
keys
)
:
JSON
.
stringify
(
keys
[
0
]);
localInputs
.
key
=
batch
?
JSON
.
stringify
(
keys
)
:
JSON
.
stringify
(
keys
[
0
]);
}
}
}
}
}
}
...
@@ -954,6 +964,56 @@ const EditChannelModal = (props) => {
...
@@ -954,6 +964,56 @@ const EditChannelModal = (props) => {
}
}
};
};
// 密钥去重函数
const
deduplicateKeys
=
()
=>
{
const
currentKey
=
formApiRef
.
current
?.
getValue
(
'key'
)
||
inputs
.
key
||
''
;
if
(
!
currentKey
.
trim
())
{
showInfo
(
t
(
'请先输入密钥'
));
return
;
}
// 按行分割密钥
const
keyLines
=
currentKey
.
split
(
'\n'
);
const
beforeCount
=
keyLines
.
length
;
// 使用哈希表去重,保持原有顺序
const
keySet
=
new
Set
();
const
deduplicatedKeys
=
[];
keyLines
.
forEach
((
line
)
=>
{
const
trimmedLine
=
line
.
trim
();
if
(
trimmedLine
&&
!
keySet
.
has
(
trimmedLine
))
{
keySet
.
add
(
trimmedLine
);
deduplicatedKeys
.
push
(
trimmedLine
);
}
});
const
afterCount
=
deduplicatedKeys
.
length
;
const
deduplicatedKeyText
=
deduplicatedKeys
.
join
(
'\n'
);
// 更新表单和状态
if
(
formApiRef
.
current
)
{
formApiRef
.
current
.
setValue
(
'key'
,
deduplicatedKeyText
);
}
handleInputChange
(
'key'
,
deduplicatedKeyText
);
// 显示去重结果
const
message
=
t
(
'去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥'
,
{
before
:
beforeCount
,
after
:
afterCount
,
},
);
if
(
beforeCount
===
afterCount
)
{
showInfo
(
t
(
'未发现重复密钥'
));
}
else
{
showSuccess
(
message
);
}
};
const
addCustomModels
=
()
=>
{
const
addCustomModels
=
()
=>
{
if
(
customModel
.
trim
()
===
''
)
return
;
if
(
customModel
.
trim
()
===
''
)
return
;
const
modelArray
=
customModel
.
split
(
','
).
map
((
model
)
=>
model
.
trim
());
const
modelArray
=
customModel
.
split
(
','
).
map
((
model
)
=>
model
.
trim
());
...
@@ -1049,24 +1109,41 @@ const EditChannelModal = (props) => {
...
@@ -1049,24 +1109,41 @@ const EditChannelModal = (props) => {
</
Checkbox
>
</
Checkbox
>
)
}
)
}
{
batch
&&
(
{
batch
&&
(
<
Checkbox
<>
disabled=
{
isEdit
}
<
Checkbox
checked=
{
multiToSingle
}
disabled=
{
isEdit
}
onChange=
{
()
=>
{
checked=
{
multiToSingle
}
setMultiToSingle
((
prev
)
=>
!
prev
);
onChange=
{
()
=>
{
setInputs
((
prev
)
=>
{
setMultiToSingle
((
prev
)
=>
{
const
newInputs
=
{
...
prev
};
const
nextValue
=
!
prev
;
if
(
!
multiToSingle
)
{
setInputs
((
prevInputs
)
=>
{
newInputs
.
multi_key_mode
=
multiKeyMode
;
const
newInputs
=
{
...
prevInputs
};
}
else
{
if
(
nextValue
)
{
delete
newInputs
.
multi_key_mode
;
newInputs
.
multi_key_mode
=
multiKeyMode
;
}
}
else
{
return
newInputs
;
delete
newInputs
.
multi_key_mode
;
});
}
}
}
return
newInputs
;
>
});
{
t
(
'密钥聚合模式'
)
}
return
nextValue
;
</
Checkbox
>
});
}
}
>
{
t
(
'密钥聚合模式'
)
}
</
Checkbox
>
{
inputs
.
type
!==
41
&&
(
<
Button
size=
'small'
type=
'tertiary'
theme=
'outline'
onClick=
{
deduplicateKeys
}
style=
{
{
textDecoration
:
'underline'
}
}
>
{
t
(
'密钥去重'
)
}
</
Button
>
)
}
</>
)
}
)
}
</
Space
>
</
Space
>
)
:
null
;
)
:
null
;
...
@@ -1268,7 +1345,10 @@ const EditChannelModal = (props) => {
...
@@ -1268,7 +1345,10 @@ const EditChannelModal = (props) => {
value=
{
inputs
.
vertex_key_type
||
'json'
}
value=
{
inputs
.
vertex_key_type
||
'json'
}
onChange=
{
(
value
)
=>
{
onChange=
{
(
value
)
=>
{
// 更新设置中的 vertex_key_type
// 更新设置中的 vertex_key_type
handleChannelOtherSettingsChange
(
'vertex_key_type'
,
value
);
handleChannelOtherSettingsChange
(
'vertex_key_type'
,
value
,
);
// 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件
// 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件
if
(
value
===
'api_key'
)
{
if
(
value
===
'api_key'
)
{
setBatch
(
false
);
setBatch
(
false
);
...
@@ -1288,7 +1368,8 @@ const EditChannelModal = (props) => {
...
@@ -1288,7 +1368,8 @@ const EditChannelModal = (props) => {
/>
/>
)
}
)
}
{
batch
?
(
{
batch
?
(
inputs
.
type
===
41
&&
(
inputs
.
vertex_key_type
||
'json'
)
===
'json'
?
(
inputs
.
type
===
41
&&
(
inputs
.
vertex_key_type
||
'json'
)
===
'json'
?
(
<
Form
.
Upload
<
Form
.
Upload
field=
'vertex_files'
field=
'vertex_files'
label=
{
t
(
'密钥文件 (.json)'
)
}
label=
{
t
(
'密钥文件 (.json)'
)
}
...
@@ -1324,7 +1405,7 @@ const EditChannelModal = (props) => {
...
@@ -1324,7 +1405,7 @@ const EditChannelModal = (props) => {
autoComplete=
'new-password'
autoComplete=
'new-password'
onChange=
{
(
value
)
=>
handleInputChange
(
'key'
,
value
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'key'
,
value
)
}
extraText=
{
extraText=
{
<
div
className=
'flex items-center gap-2'
>
<
div
className=
'flex items-center gap-2
flex-wrap
'
>
{
isEdit
&&
{
isEdit
&&
isMultiKeyChannel
&&
isMultiKeyChannel
&&
keyMode
===
'append'
&&
(
keyMode
===
'append'
&&
(
...
@@ -1352,7 +1433,8 @@ const EditChannelModal = (props) => {
...
@@ -1352,7 +1433,8 @@ const EditChannelModal = (props) => {
)
)
)
:
(
)
:
(
<>
<>
{
inputs
.
type
===
41
&&
(
inputs
.
vertex_key_type
||
'json'
)
===
'json'
?
(
{
inputs
.
type
===
41
&&
(
inputs
.
vertex_key_type
||
'json'
)
===
'json'
?
(
<>
<>
{
!
batch
&&
(
{
!
batch
&&
(
<
div
className=
'flex items-center justify-between mb-3'
>
<
div
className=
'flex items-center justify-between mb-3'
>
...
...
web/src/components/table/task-logs/modals/ContentModal.jsx
View file @
51e02785
...
@@ -17,8 +17,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -17,8 +17,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
React
from
'react'
;
import
React
,
{
useState
,
useEffect
}
from
'react'
;
import
{
Modal
}
from
'@douyinfe/semi-ui'
;
import
{
Modal
,
Button
,
Typography
,
Spin
}
from
'@douyinfe/semi-ui'
;
import
{
IconExternalOpen
,
IconCopy
}
from
'@douyinfe/semi-icons'
;
const
{
Text
}
=
Typography
;
const
ContentModal
=
({
const
ContentModal
=
({
isModalOpen
,
isModalOpen
,
...
@@ -26,17 +29,120 @@ const ContentModal = ({
...
@@ -26,17 +29,120 @@ const ContentModal = ({
modalContent
,
modalContent
,
isVideo
,
isVideo
,
})
=>
{
})
=>
{
const
[
videoError
,
setVideoError
]
=
useState
(
false
);
const
[
isLoading
,
setIsLoading
]
=
useState
(
false
);
useEffect
(()
=>
{
if
(
isModalOpen
&&
isVideo
)
{
setVideoError
(
false
);
setIsLoading
(
true
);
}
},
[
isModalOpen
,
isVideo
]);
const
handleVideoError
=
()
=>
{
setVideoError
(
true
);
setIsLoading
(
false
);
};
const
handleVideoLoaded
=
()
=>
{
setIsLoading
(
false
);
};
const
handleCopyUrl
=
()
=>
{
navigator
.
clipboard
.
writeText
(
modalContent
);
};
const
handleOpenInNewTab
=
()
=>
{
window
.
open
(
modalContent
,
'_blank'
);
};
const
renderVideoContent
=
()
=>
{
if
(
videoError
)
{
return
(
<
div
style=
{
{
textAlign
:
'center'
,
padding
:
'40px'
}
}
>
<
Text
type=
"tertiary"
style=
{
{
display
:
'block'
,
marginBottom
:
'16px'
}
}
>
视频无法在当前浏览器中播放,这可能是由于:
</
Text
>
<
Text
type=
"tertiary"
style=
{
{
display
:
'block'
,
marginBottom
:
'8px'
,
fontSize
:
'12px'
}
}
>
• 视频服务商的跨域限制
</
Text
>
<
Text
type=
"tertiary"
style=
{
{
display
:
'block'
,
marginBottom
:
'8px'
,
fontSize
:
'12px'
}
}
>
• 需要特定的请求头或认证
</
Text
>
<
Text
type=
"tertiary"
style=
{
{
display
:
'block'
,
marginBottom
:
'16px'
,
fontSize
:
'12px'
}
}
>
• 防盗链保护机制
</
Text
>
<
div
style=
{
{
marginTop
:
'20px'
}
}
>
<
Button
icon=
{
<
IconExternalOpen
/>
}
onClick=
{
handleOpenInNewTab
}
style=
{
{
marginRight
:
'8px'
}
}
>
在新标签页中打开
</
Button
>
<
Button
icon=
{
<
IconCopy
/>
}
onClick=
{
handleCopyUrl
}
>
复制链接
</
Button
>
</
div
>
<
div
style=
{
{
marginTop
:
'16px'
,
padding
:
'8px'
,
backgroundColor
:
'#f8f9fa'
,
borderRadius
:
'4px'
}
}
>
<
Text
type=
"tertiary"
style=
{
{
fontSize
:
'10px'
,
wordBreak
:
'break-all'
}
}
>
{
modalContent
}
</
Text
>
</
div
>
</
div
>
);
}
return
(
<
div
style=
{
{
position
:
'relative'
}
}
>
{
isLoading
&&
(
<
div
style=
{
{
position
:
'absolute'
,
top
:
'50%'
,
left
:
'50%'
,
transform
:
'translate(-50%, -50%)'
,
zIndex
:
10
}
}
>
<
Spin
size=
"large"
/>
</
div
>
)
}
<
video
src=
{
modalContent
}
controls
style=
{
{
width
:
'100%'
}
}
autoPlay
crossOrigin=
"anonymous"
onError=
{
handleVideoError
}
onLoadedData=
{
handleVideoLoaded
}
onLoadStart=
{
()
=>
setIsLoading
(
true
)
}
/>
</
div
>
);
};
return
(
return
(
<
Modal
<
Modal
visible=
{
isModalOpen
}
visible=
{
isModalOpen
}
onOk=
{
()
=>
setIsModalOpen
(
false
)
}
onOk=
{
()
=>
setIsModalOpen
(
false
)
}
onCancel=
{
()
=>
setIsModalOpen
(
false
)
}
onCancel=
{
()
=>
setIsModalOpen
(
false
)
}
closable=
{
null
}
closable=
{
null
}
bodyStyle=
{
{
height
:
'400px'
,
overflow
:
'auto'
}
}
bodyStyle=
{
{
height
:
isVideo
?
'450px'
:
'400px'
,
overflow
:
'auto'
,
padding
:
isVideo
&&
videoError
?
'0'
:
'24px'
}
}
width=
{
800
}
width=
{
800
}
>
>
{
isVideo
?
(
{
isVideo
?
(
<
video
src=
{
modalContent
}
controls
style=
{
{
width
:
'100%'
}
}
autoPlay
/>
renderVideoContent
()
)
:
(
)
:
(
<
p
style=
{
{
whiteSpace
:
'pre-line'
}
}
>
{
modalContent
}
</
p
>
<
p
style=
{
{
whiteSpace
:
'pre-line'
}
}
>
{
modalContent
}
</
p
>
)
}
)
}
...
...
web/src/i18n/locales/en.json
View file @
51e02785
...
@@ -837,6 +837,7 @@
...
@@ -837,6 +837,7 @@
"确定要充值 $"
:
"Confirm to top up $"
,
"确定要充值 $"
:
"Confirm to top up $"
,
"微信/支付宝 实付金额:"
:
"WeChat/Alipay actual payment amount:"
,
"微信/支付宝 实付金额:"
:
"WeChat/Alipay actual payment amount:"
,
"Stripe 实付金额:"
:
"Stripe actual payment amount:"
,
"Stripe 实付金额:"
:
"Stripe actual payment amount:"
,
"允许在 Stripe 支付中输入促销码"
:
"Allow entering promotion codes during Stripe checkout"
,
"支付中..."
:
"Paying"
,
"支付中..."
:
"Paying"
,
"支付宝"
:
"Alipay"
,
"支付宝"
:
"Alipay"
,
"收益统计"
:
"Income statistics"
,
"收益统计"
:
"Income statistics"
,
...
...
web/src/i18n/locales/zh.json
View file @
51e02785
...
@@ -35,5 +35,6 @@
...
@@ -35,5 +35,6 @@
"域名IP过滤详细说明"
:
"⚠️此功能为实验性选项,域名可能解析到多个 IPv4/IPv6 地址,若开启,请确保 IP 过滤列表覆盖这些地址,否则可能导致访问失败。"
,
"域名IP过滤详细说明"
:
"⚠️此功能为实验性选项,域名可能解析到多个 IPv4/IPv6 地址,若开启,请确保 IP 过滤列表覆盖这些地址,否则可能导致访问失败。"
,
"common"
:
{
"common"
:
{
"changeLanguage"
:
"切换语言"
"changeLanguage"
:
"切换语言"
}
},
"允许在 Stripe 支付中输入促销码"
:
"允许在 Stripe 支付中输入促销码"
}
}
web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx
View file @
51e02785
...
@@ -45,6 +45,7 @@ export default function SettingsPaymentGateway(props) {
...
@@ -45,6 +45,7 @@ export default function SettingsPaymentGateway(props) {
StripePriceId
:
''
,
StripePriceId
:
''
,
StripeUnitPrice
:
8.0
,
StripeUnitPrice
:
8.0
,
StripeMinTopUp
:
1
,
StripeMinTopUp
:
1
,
StripePromotionCodesEnabled
:
false
,
});
});
const
[
originInputs
,
setOriginInputs
]
=
useState
({});
const
[
originInputs
,
setOriginInputs
]
=
useState
({});
const
formApiRef
=
useRef
(
null
);
const
formApiRef
=
useRef
(
null
);
...
@@ -63,6 +64,10 @@ export default function SettingsPaymentGateway(props) {
...
@@ -63,6 +64,10 @@ export default function SettingsPaymentGateway(props) {
props
.
options
.
StripeMinTopUp
!==
undefined
props
.
options
.
StripeMinTopUp
!==
undefined
?
parseFloat
(
props
.
options
.
StripeMinTopUp
)
?
parseFloat
(
props
.
options
.
StripeMinTopUp
)
:
1
,
:
1
,
StripePromotionCodesEnabled
:
props
.
options
.
StripePromotionCodesEnabled
!==
undefined
?
props
.
options
.
StripePromotionCodesEnabled
:
false
,
};
};
setInputs
(
currentInputs
);
setInputs
(
currentInputs
);
setOriginInputs
({
...
currentInputs
});
setOriginInputs
({
...
currentInputs
});
...
@@ -114,6 +119,16 @@ export default function SettingsPaymentGateway(props) {
...
@@ -114,6 +119,16 @@ export default function SettingsPaymentGateway(props) {
value
:
inputs
.
StripeMinTopUp
.
toString
(),
value
:
inputs
.
StripeMinTopUp
.
toString
(),
});
});
}
}
if
(
originInputs
[
'StripePromotionCodesEnabled'
]
!==
inputs
.
StripePromotionCodesEnabled
&&
inputs
.
StripePromotionCodesEnabled
!==
undefined
)
{
options
.
push
({
key
:
'StripePromotionCodesEnabled'
,
value
:
inputs
.
StripePromotionCodesEnabled
?
'true'
:
'false'
,
});
}
// 发送请求
// 发送请求
const
requestQueue
=
options
.
map
((
opt
)
=>
const
requestQueue
=
options
.
map
((
opt
)
=>
...
@@ -225,6 +240,15 @@ export default function SettingsPaymentGateway(props) {
...
@@ -225,6 +240,15 @@ export default function SettingsPaymentGateway(props) {
placeholder=
{
t
(
'例如:2,就是最低充值2$'
)
}
placeholder=
{
t
(
'例如:2,就是最低充值2$'
)
}
/>
/>
</
Col
>
</
Col
>
<
Col
xs=
{
24
}
sm=
{
24
}
md=
{
8
}
lg=
{
8
}
xl=
{
8
}
>
<
Form
.
Switch
field=
'StripePromotionCodesEnabled'
size=
'default'
checkedText=
'|'
uncheckedText=
'〇'
label=
{
t
(
'允许在 Stripe 支付中输入促销码'
)
}
/>
</
Col
>
</
Row
>
</
Row
>
<
Button
onClick=
{
submitStripeSetting
}
>
{
t
(
'更新 Stripe 设置'
)
}
</
Button
>
<
Button
onClick=
{
submitStripeSetting
}
>
{
t
(
'更新 Stripe 设置'
)
}
</
Button
>
</
Form
.
Section
>
</
Form
.
Section
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment