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
eb955ae4
authored
Mar 23, 2024
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore: lint fix
parent
d572a43f
Hide whitespace changes
Inline
Side-by-side
Showing
55 changed files
with
5246 additions
and
3586 deletions
+5246
-3586
web/README.md
+1
-2
web/index.html
+1
-1
web/src/App.js
+23
-25
web/src/components/ChannelsTable.js
+336
-203
web/src/components/Footer.js
+16
-7
web/src/components/GitHubOAuth.js
+1
-1
web/src/components/HeaderBar.js
+43
-22
web/src/components/Loading.js
+3
-1
web/src/components/LoginForm.js
+103
-36
web/src/components/LogsTable.js
+435
-182
web/src/components/MjLogsTable.js
+294
-127
web/src/components/OperationSetting.js
+219
-178
web/src/components/OtherSetting.js
+68
-31
web/src/components/PasswordResetConfirm.js
+17
-17
web/src/components/PasswordResetForm.js
+13
-13
web/src/components/PersonalSetting.js
+234
-123
web/src/components/PrivateRoute.js
+2
-4
web/src/components/RedemptionsTable.js
+177
-132
web/src/components/RegisterForm.js
+41
-33
web/src/components/SiderBar.js
+133
-97
web/src/components/SystemSetting.js
+202
-161
web/src/components/TokensTable.js
+276
-162
web/src/components/UsersTable.js
+276
-150
web/src/components/WeChatIcon.js
+21
-9
web/src/components/utils.js
+2
-3
web/src/constants/channel.constants.js
+98
-20
web/src/constants/index.js
+1
-2
web/src/constants/toast.constants.js
+1
-1
web/src/constants/user.constants.js
+14
-14
web/src/context/Status/index.js
+1
-2
web/src/context/User/index.js
+9
-10
web/src/context/User/reducer.js
+4
-5
web/src/helpers/api.js
+4
-2
web/src/helpers/auth-header.js
+8
-9
web/src/helpers/history.js
+1
-2
web/src/helpers/index.js
+1
-2
web/src/helpers/render.js
+162
-136
web/src/helpers/utils.js
+9
-21
web/src/index.css
+59
-55
web/src/index.js
+36
-37
web/src/pages/About/index.js
+19
-14
web/src/pages/Channel/EditChannel.js
+708
-606
web/src/pages/Channel/index.js
+11
-11
web/src/pages/Chat/index.js
+0
-1
web/src/pages/Detail/index.js
+396
-337
web/src/pages/Home/index.js
+108
-72
web/src/pages/Redemption/EditRedemption.js
+59
-30
web/src/pages/Redemption/index.js
+9
-9
web/src/pages/Setting/index.js
+41
-41
web/src/pages/Token/EditToken.js
+130
-68
web/src/pages/Token/index.js
+3
-3
web/src/pages/TopUp/index.js
+314
-291
web/src/pages/User/AddUser.js
+24
-15
web/src/pages/User/EditUser.js
+72
-43
web/src/pages/User/index.js
+7
-7
No files found.
web/README.md
View file @
eb955ae4
...
...
@@ -18,4 +18,4 @@ Before you start editing, make sure your `Actions on Save` options have `Optimiz
## Reference
1.
https://github.com/OIerDb-ng/OIerDb
2.
https://github.com/cornflourblue/react-hooks-redux-registration-login-example
\ No newline at end of file
2.
https://github.com/cornflourblue/react-hooks-redux-registration-login-example
web/index.html
View file @
eb955ae4
<!
DOCTYPE
html>
<!
doctype
html>
<html
lang=
"en"
>
<head>
<meta
charset=
"utf-8"
/>
...
...
web/src/App.js
View file @
eb955ae4
...
...
@@ -48,7 +48,7 @@ function App() {
}
let
logo
=
getLogo
();
if
(
logo
)
{
let
linkElement
=
document
.
querySelector
(
'link[rel~=\'icon\']'
);
let
linkElement
=
document
.
querySelector
(
"link[rel~='icon']"
);
if
(
linkElement
)
{
linkElement
.
href
=
logo
;
}
...
...
@@ -60,7 +60,7 @@ function App() {
<
Layout
.
Content
>
<
Routes
>
<
Route
path
=
"/"
path
=
'/'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
<
Home
/>
...
...
@@ -68,7 +68,7 @@ function App() {
}
/
>
<
Route
path
=
"/channel"
path
=
'/channel'
element
=
{
<
PrivateRoute
>
<
Channel
/>
...
...
@@ -76,7 +76,7 @@ function App() {
}
/
>
<
Route
path
=
"/channel/edit/:id"
path
=
'/channel/edit/:id'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
<
EditChannel
/>
...
...
@@ -84,7 +84,7 @@ function App() {
}
/
>
<
Route
path
=
"/channel/add"
path
=
'/channel/add'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
<
EditChannel
/>
...
...
@@ -92,7 +92,7 @@ function App() {
}
/
>
<
Route
path
=
"/token"
path
=
'/token'
element
=
{
<
PrivateRoute
>
<
Token
/>
...
...
@@ -100,7 +100,7 @@ function App() {
}
/
>
<
Route
path
=
"/redemption"
path
=
'/redemption'
element
=
{
<
PrivateRoute
>
<
Redemption
/>
...
...
@@ -108,7 +108,7 @@ function App() {
}
/
>
<
Route
path
=
"/user"
path
=
'/user'
element
=
{
<
PrivateRoute
>
<
User
/>
...
...
@@ -116,7 +116,7 @@ function App() {
}
/
>
<
Route
path
=
"/user/edit/:id"
path
=
'/user/edit/:id'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
<
EditUser
/>
...
...
@@ -124,7 +124,7 @@ function App() {
}
/
>
<
Route
path
=
"/user/edit"
path
=
'/user/edit'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
<
EditUser
/>
...
...
@@ -132,7 +132,7 @@ function App() {
}
/
>
<
Route
path
=
"/user/reset"
path
=
'/user/reset'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
<
PasswordResetConfirm
/>
...
...
@@ -140,7 +140,7 @@ function App() {
}
/
>
<
Route
path
=
"/login"
path
=
'/login'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
<
LoginForm
/>
...
...
@@ -148,7 +148,7 @@ function App() {
}
/
>
<
Route
path
=
"/register"
path
=
'/register'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
<
RegisterForm
/>
...
...
@@ -156,7 +156,7 @@ function App() {
}
/
>
<
Route
path
=
"/reset"
path
=
'/reset'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
<
PasswordResetForm
/>
...
...
@@ -164,7 +164,7 @@ function App() {
}
/
>
<
Route
path
=
"/oauth/github"
path
=
'/oauth/github'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
<
GitHubOAuth
/>
...
...
@@ -172,7 +172,7 @@ function App() {
}
/
>
<
Route
path
=
"/setting"
path
=
'/setting'
element
=
{
<
PrivateRoute
>
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
...
...
@@ -182,7 +182,7 @@ function App() {
}
/
>
<
Route
path
=
"/topup"
path
=
'/topup'
element
=
{
<
PrivateRoute
>
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
...
...
@@ -192,7 +192,7 @@ function App() {
}
/
>
<
Route
path
=
"/log"
path
=
'/log'
element
=
{
<
PrivateRoute
>
<
Log
/>
...
...
@@ -200,7 +200,7 @@ function App() {
}
/
>
<
Route
path
=
"/detail"
path
=
'/detail'
element
=
{
<
PrivateRoute
>
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
...
...
@@ -210,7 +210,7 @@ function App() {
}
/
>
<
Route
path
=
"/midjourney"
path
=
'/midjourney'
element
=
{
<
PrivateRoute
>
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
...
...
@@ -220,7 +220,7 @@ function App() {
}
/
>
<
Route
path
=
"/about"
path
=
'/about'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
<
About
/>
...
...
@@ -228,16 +228,14 @@ function App() {
}
/
>
<
Route
path
=
"/chat"
path
=
'/chat'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
<
Chat
/>
<
/Suspense
>
}
/
>
<
Route
path
=
"*"
element
=
{
<
NotFound
/>
}
/
>
<
Route
path
=
'*'
element
=
{
<
NotFound
/>
}
/
>
<
/Routes
>
<
/Layout.Content
>
<
/Layout
>
...
...
web/src/components/ChannelsTable.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
API
,
isMobile
,
shouldShowPrompt
,
showError
,
showInfo
,
showSuccess
,
timestamp2string
}
from
'../helpers'
;
import
{
API
,
isMobile
,
shouldShowPrompt
,
showError
,
showInfo
,
showSuccess
,
timestamp2string
,
}
from
'../helpers'
;
import
{
CHANNEL_OPTIONS
,
ITEMS_PER_PAGE
}
from
'../constants'
;
import
{
renderGroup
,
renderNumberWithPoint
,
renderQuota
}
from
'../helpers/render'
;
import
{
Button
,
Dropdown
,
Form
,
InputNumber
,
Popconfirm
,
Space
,
SplitButtonGroup
,
Switch
,
Table
,
Tag
,
Tooltip
,
Typography
renderGroup
,
renderNumberWithPoint
,
renderQuota
,
}
from
'../helpers/render'
;
import
{
Button
,
Dropdown
,
Form
,
InputNumber
,
Popconfirm
,
Space
,
SplitButtonGroup
,
Switch
,
Table
,
Tag
,
Tooltip
,
Typography
,
}
from
'@douyinfe/semi-ui'
;
import
EditChannel
from
'../pages/Channel/EditChannel'
;
import
{
IconTreeTriangleDown
}
from
'@douyinfe/semi-icons'
;
function
renderTimestamp
(
timestamp
)
{
return
(
<>
{
timestamp2string
(
timestamp
)}
<
/
>
);
return
<>
{
timestamp2string
(
timestamp
)}
<
/>
;
}
let
type2label
=
undefined
;
...
...
@@ -38,7 +46,11 @@ function renderType(type) {
}
type2label
[
0
]
=
{
value
:
0
,
text
:
'未知类型'
,
color
:
'grey'
};
}
return
<
Tag
size
=
"large"
color
=
{
type2label
[
type
]?.
color
}
>
{
type2label
[
type
]?.
text
}
<
/Tag>
;
return
(
<
Tag
size
=
'large'
color
=
{
type2label
[
type
]?.
color
}
>
{
type2label
[
type
]?.
text
}
<
/Tag
>
);
}
const
ChannelsTable
=
()
=>
{
...
...
@@ -50,11 +62,11 @@ const ChannelsTable = () => {
// },
{
title
:
'ID'
,
dataIndex
:
'id'
dataIndex
:
'id'
,
},
{
title
:
'名称'
,
dataIndex
:
'name'
dataIndex
:
'name'
,
},
{
title
:
'分组'
,
...
...
@@ -63,48 +75,34 @@ const ChannelsTable = () => {
return
(
<
div
>
<
Space
spacing
=
{
2
}
>
{
text
.
split
(
','
).
map
((
item
,
index
)
=>
{
return
(
renderGroup
(
item
));
})
}
{
text
.
split
(
','
).
map
((
item
,
index
)
=>
{
return
renderGroup
(
item
);
})}
<
/Space
>
<
/div
>
);
}
}
,
},
{
title
:
'类型'
,
dataIndex
:
'type'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderType
(
text
)}
<
/div
>
);
}
return
<
div
>
{
renderType
(
text
)}
<
/div>
;
},
},
{
title
:
'状态'
,
dataIndex
:
'status'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderStatus
(
text
)}
<
/div
>
);
}
return
<
div
>
{
renderStatus
(
text
)}
<
/div>
;
},
},
{
title
:
'响应时间'
,
dataIndex
:
'response_time'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderResponseTime
(
text
)}
<
/div
>
);
}
return
<
div
>
{
renderResponseTime
(
text
)}
<
/div>
;
},
},
{
title
:
'已用/剩余'
,
...
...
@@ -114,17 +112,26 @@ const ChannelsTable = () => {
<
div
>
<
Space
spacing
=
{
1
}
>
<
Tooltip
content
=
{
'已用额度'
}
>
<
Tag
color
=
"white"
type
=
"ghost"
size
=
"large"
>
{
renderQuota
(
record
.
used_quota
)}
<
/Tag
>
<
Tag
color
=
'white'
type
=
'ghost'
size
=
'large'
>
{
renderQuota
(
record
.
used_quota
)}
<
/Tag
>
<
/Tooltip
>
<
Tooltip
content
=
{
'剩余额度'
+
record
.
balance
+
',点击更新'
}
>
<
Tag
color
=
"white"
type
=
"ghost"
size
=
"large"
onClick
=
{()
=>
{
updateChannelBalance
(
record
);
}}
>
$
{
renderNumberWithPoint
(
record
.
balance
)}
<
/Tag
>
<
Tag
color
=
'white'
type
=
'ghost'
size
=
'large'
onClick
=
{()
=>
{
updateChannelBalance
(
record
);
}}
>
$
{
renderNumberWithPoint
(
record
.
balance
)}
<
/Tag
>
<
/Tooltip
>
<
/Space
>
<
/div
>
);
}
}
,
},
{
title
:
'优先级'
,
...
...
@@ -134,8 +141,8 @@ const ChannelsTable = () => {
<
div
>
<
InputNumber
style
=
{{
width
:
70
}}
name
=
"priority"
onBlur
=
{
e
=>
{
name
=
'priority'
onBlur
=
{
(
e
)
=>
{
manageChannel
(
record
.
id
,
'priority'
,
record
,
e
.
target
.
value
);
}}
keepFocus
=
{
true
}
...
...
@@ -145,7 +152,7 @@ const ChannelsTable = () => {
/
>
<
/div
>
);
}
}
,
},
{
title
:
'权重'
,
...
...
@@ -155,8 +162,8 @@ const ChannelsTable = () => {
<
div
>
<
InputNumber
style
=
{{
width
:
70
}}
name
=
"weight"
onBlur
=
{
e
=>
{
name
=
'weight'
onBlur
=
{
(
e
)
=>
{
manageChannel
(
record
.
id
,
'weight'
,
record
,
e
.
target
.
value
);
}}
keepFocus
=
{
true
}
...
...
@@ -166,68 +173,90 @@ const ChannelsTable = () => {
/
>
<
/div
>
);
}
}
,
},
{
title
:
''
,
dataIndex
:
'operate'
,
render
:
(
text
,
record
,
index
)
=>
(
<
div
>
<
SplitButtonGroup
style
=
{{
marginRight
:
1
}}
aria
-
label
=
"测试操作项目组"
>
<
Button
theme
=
"light"
onClick
=
{()
=>
{
testChannel
(
record
,
''
);
}}
>
测试
<
/Button
>
<
Dropdown
trigger
=
"click"
position
=
"bottomRight"
menu
=
{
record
.
test_models
}
<
SplitButtonGroup
style
=
{{
marginRight
:
1
}}
aria
-
label
=
'测试操作项目组'
>
<
Button
theme
=
'light'
onClick
=
{()
=>
{
testChannel
(
record
,
''
);
}}
>
<
Button
style
=
{{
padding
:
'8px 4px'
}}
type
=
"primary"
icon
=
{
<
IconTreeTriangleDown
/>
}
><
/Button
>
测试
<
/Button
>
<
Dropdown
trigger
=
'click'
position
=
'bottomRight'
menu
=
{
record
.
test_models
}
>
<
Button
style
=
{{
padding
:
'8px 4px'
}}
type
=
'primary'
icon
=
{
<
IconTreeTriangleDown
/>
}
><
/Button
>
<
/Dropdown
>
<
/SplitButtonGroup
>
{
/*<Button theme='light' type='primary' style={{marginRight: 1}} onClick={()=>testChannel(record)}>测试</Button>*/
}
<
Popconfirm
title
=
"确定是否要删除此渠道?"
content
=
"此修改将不可逆"
title
=
'确定是否要删除此渠道?'
content
=
'此修改将不可逆'
okType
=
{
'danger'
}
position
=
{
'left'
}
onConfirm
=
{()
=>
{
manageChannel
(
record
.
id
,
'delete'
,
record
).
then
(
()
=>
{
removeRecord
(
record
.
id
);
}
);
manageChannel
(
record
.
id
,
'delete'
,
record
).
then
(()
=>
{
removeRecord
(
record
.
id
);
});
}}
>
<
Button
theme
=
"light"
type
=
"danger"
style
=
{{
marginRight
:
1
}}
>
删除
<
/Button
>
<
Button
theme
=
'light'
type
=
'danger'
style
=
{{
marginRight
:
1
}}
>
删除
<
/Button
>
<
/Popconfirm
>
{
record
.
status
===
1
?
<
Button
theme
=
"light"
type
=
"warning"
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageChannel
(
record
.
id
,
'disable'
,
record
);
}
}
>
禁用
<
/Button>
:
<
Button
theme
=
"light"
type
=
"secondary"
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageChannel
(
record
.
id
,
'enable'
,
record
);
}
}
>
启用
<
/Button
>
}
<
Button
theme
=
"light"
type
=
"tertiary"
style
=
{{
marginRight
:
1
}}
onClick
=
{
()
=>
{
{
record
.
status
===
1
?
(
<
Button
theme
=
'light'
type
=
'warning'
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageChannel
(
record
.
id
,
'disable'
,
record
);
}}
>
禁用
<
/Button
>
)
:
(
<
Button
theme
=
'light'
type
=
'secondary'
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageChannel
(
record
.
id
,
'enable'
,
record
);
}}
>
启用
<
/Button
>
)}
<
Button
theme
=
'light'
type
=
'tertiary'
style
=
{{
marginRight
:
1
}}
onClick
=
{()
=>
{
setEditingChannel
(
record
);
setShowEdit
(
true
);
}
}
>
编辑
<
/Button
>
}}
>
编辑
<
/Button
>
<
/div
>
)
}
)
,
}
,
];
const
[
channels
,
setChannels
]
=
useState
([]);
...
...
@@ -240,20 +269,22 @@ const ChannelsTable = () => {
const
[
searching
,
setSearching
]
=
useState
(
false
);
const
[
updatingBalance
,
setUpdatingBalance
]
=
useState
(
false
);
const
[
pageSize
,
setPageSize
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
showPrompt
,
setShowPrompt
]
=
useState
(
shouldShowPrompt
(
'channel-test'
));
const
[
showPrompt
,
setShowPrompt
]
=
useState
(
shouldShowPrompt
(
'channel-test'
),
);
const
[
channelCount
,
setChannelCount
]
=
useState
(
pageSize
);
const
[
groupOptions
,
setGroupOptions
]
=
useState
([]);
const
[
showEdit
,
setShowEdit
]
=
useState
(
false
);
const
[
enableBatchDelete
,
setEnableBatchDelete
]
=
useState
(
false
);
const
[
editingChannel
,
setEditingChannel
]
=
useState
({
id
:
undefined
id
:
undefined
,
});
const
[
selectedChannels
,
setSelectedChannels
]
=
useState
([]);
const
removeRecord
=
id
=>
{
const
removeRecord
=
(
id
)
=>
{
let
newDataSource
=
[...
channels
];
if
(
id
!=
null
)
{
let
idx
=
newDataSource
.
findIndex
(
data
=>
data
.
id
===
id
);
let
idx
=
newDataSource
.
findIndex
(
(
data
)
=>
data
.
id
===
id
);
if
(
idx
>
-
1
)
{
newDataSource
.
splice
(
idx
,
1
);
...
...
@@ -272,7 +303,7 @@ const ChannelsTable = () => {
name
:
item
,
onClick
:
()
=>
{
testChannel
(
channels
[
i
],
item
);
}
}
,
});
});
channels
[
i
].
test_models
=
test_models
;
...
...
@@ -288,7 +319,9 @@ const ChannelsTable = () => {
const
loadChannels
=
async
(
startIdx
,
pageSize
,
idSort
)
=>
{
setLoading
(
true
);
const
res
=
await
API
.
get
(
`/api/channel/?p=
${
startIdx
}
&page_size=
${
pageSize
}
&id_sort=
${
idSort
}
`
);
const
res
=
await
API
.
get
(
`/api/channel/?p=
${
startIdx
}
&page_size=
${
pageSize
}
&id_sort=
${
idSort
}
`
,
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
if
(
startIdx
===
0
)
{
...
...
@@ -311,7 +344,8 @@ const ChannelsTable = () => {
useEffect
(()
=>
{
// console.log('default effect')
const
localIdSort
=
localStorage
.
getItem
(
'id-sort'
)
===
'true'
;
const
localPageSize
=
parseInt
(
localStorage
.
getItem
(
'page-size'
))
||
ITEMS_PER_PAGE
;
const
localPageSize
=
parseInt
(
localStorage
.
getItem
(
'page-size'
))
||
ITEMS_PER_PAGE
;
setIdSort
(
localIdSort
);
setPageSize
(
localPageSize
);
loadChannels
(
0
,
localPageSize
,
localIdSort
)
...
...
@@ -361,7 +395,6 @@ const ChannelsTable = () => {
let
channel
=
res
.
data
.
data
;
let
newChannels
=
[...
channels
];
if
(
action
===
'delete'
)
{
}
else
{
record
.
status
=
channel
.
status
;
}
...
...
@@ -374,22 +407,26 @@ const ChannelsTable = () => {
const
renderStatus
=
(
status
)
=>
{
switch
(
status
)
{
case
1
:
return
<
Tag
size
=
"large"
color
=
"green"
>
已启用
<
/Tag>
;
return
(
<
Tag
size
=
'large'
color
=
'green'
>
已启用
<
/Tag
>
);
case
2
:
return
(
<
Tag
size
=
"large"
color
=
"yellow"
>
<
Tag
size
=
'large'
color
=
'yellow'
>
已禁用
<
/Tag
>
);
case
3
:
return
(
<
Tag
size
=
"large"
color
=
"yellow"
>
<
Tag
size
=
'large'
color
=
'yellow'
>
自动禁用
<
/Tag
>
);
default
:
return
(
<
Tag
size
=
"large"
color
=
"grey"
>
<
Tag
size
=
'large'
color
=
'grey'
>
未知状态
<
/Tag
>
);
...
...
@@ -400,15 +437,35 @@ const ChannelsTable = () => {
let
time
=
responseTime
/
1000
;
time
=
time
.
toFixed
(
2
)
+
' 秒'
;
if
(
responseTime
===
0
)
{
return
<
Tag
size
=
"large"
color
=
"grey"
>
未测试
<
/Tag>
;
return
(
<
Tag
size
=
'large'
color
=
'grey'
>
未测试
<
/Tag
>
);
}
else
if
(
responseTime
<=
1000
)
{
return
<
Tag
size
=
"large"
color
=
"green"
>
{
time
}
<
/Tag>
;
return
(
<
Tag
size
=
'large'
color
=
'green'
>
{
time
}
<
/Tag
>
);
}
else
if
(
responseTime
<=
3000
)
{
return
<
Tag
size
=
"large"
color
=
"lime"
>
{
time
}
<
/Tag>
;
return
(
<
Tag
size
=
'large'
color
=
'lime'
>
{
time
}
<
/Tag
>
);
}
else
if
(
responseTime
<=
5000
)
{
return
<
Tag
size
=
"large"
color
=
"yellow"
>
{
time
}
<
/Tag>
;
return
(
<
Tag
size
=
'large'
color
=
'yellow'
>
{
time
}
<
/Tag
>
);
}
else
{
return
<
Tag
size
=
"large"
color
=
"red"
>
{
time
}
<
/Tag>
;
return
(
<
Tag
size
=
'large'
color
=
'red'
>
{
time
}
<
/Tag
>
);
}
};
...
...
@@ -420,7 +477,9 @@ const ChannelsTable = () => {
return
;
}
setSearching
(
true
);
const
res
=
await
API
.
get
(
`/api/channel/search?keyword=
${
searchKeyword
}
&group=
${
searchGroup
}
&model=
${
searchModel
}
`
);
const
res
=
await
API
.
get
(
`/api/channel/search?keyword=
${
searchKeyword
}
&group=
${
searchGroup
}
&model=
${
searchModel
}
`
,
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
setChannels
(
data
);
...
...
@@ -520,14 +579,16 @@ const ChannelsTable = () => {
}
};
let
pageData
=
channels
.
slice
((
activePage
-
1
)
*
pageSize
,
activePage
*
pageSize
);
let
pageData
=
channels
.
slice
(
(
activePage
-
1
)
*
pageSize
,
activePage
*
pageSize
,
);
const
handlePageChange
=
page
=>
{
const
handlePageChange
=
(
page
)
=>
{
setActivePage
(
page
);
if
(
page
===
Math
.
ceil
(
channels
.
length
/
pageSize
)
+
1
)
{
// In this case we have to load more data and then append them.
loadChannels
(
page
-
1
,
pageSize
,
idSort
).
then
(
r
=>
{
});
loadChannels
(
page
-
1
,
pageSize
,
idSort
).
then
((
r
)
=>
{});
}
};
...
...
@@ -547,10 +608,12 @@ const ChannelsTable = () => {
let
res
=
await
API
.
get
(
`/api/group/`
);
// add 'all' option
// res.data.data.unshift('all');
setGroupOptions
(
res
.
data
.
data
.
map
((
group
)
=>
({
label
:
group
,
value
:
group
})));
setGroupOptions
(
res
.
data
.
data
.
map
((
group
)
=>
({
label
:
group
,
value
:
group
,
})),
);
}
catch
(
error
)
{
showError
(
error
.
message
);
}
...
...
@@ -564,27 +627,34 @@ const ChannelsTable = () => {
if
(
record
.
status
!==
1
)
{
return
{
style
:
{
background
:
'var(--semi-color-disabled-border)'
}
background
:
'var(--semi-color-disabled-border)'
,
}
,
};
}
else
{
return
{};
}
};
return
(
<>
<
EditChannel
refresh
=
{
refresh
}
visible
=
{
showEdit
}
handleClose
=
{
closeEdit
}
editingChannel
=
{
editingChannel
}
/
>
<
Form
onSubmit
=
{()
=>
{
searchChannels
(
searchKeyword
,
searchGroup
,
searchModel
);
}}
labelPosition
=
"left"
>
<
EditChannel
refresh
=
{
refresh
}
visible
=
{
showEdit
}
handleClose
=
{
closeEdit
}
editingChannel
=
{
editingChannel
}
/
>
<
Form
onSubmit
=
{()
=>
{
searchChannels
(
searchKeyword
,
searchGroup
,
searchModel
);
}}
labelPosition
=
'left'
>
<
div
style
=
{{
display
:
'flex'
}}
>
<
Space
>
<
Form
.
Input
field
=
"search_keyword"
label
=
"搜索渠道关键词"
placeholder
=
"ID,名称和密钥 ..."
field
=
'search_keyword'
label
=
'搜索渠道关键词'
placeholder
=
'ID,名称和密钥 ...'
value
=
{
searchKeyword
}
loading
=
{
searching
}
onChange
=
{(
v
)
=>
{
...
...
@@ -592,21 +662,33 @@ const ChannelsTable = () => {
}}
/
>
<
Form
.
Input
field
=
"search_model"
label
=
"模型"
placeholder
=
"模型关键字"
field
=
'search_model'
label
=
'模型'
placeholder
=
'模型关键字'
value
=
{
searchModel
}
loading
=
{
searching
}
onChange
=
{(
v
)
=>
{
setSearchModel
(
v
.
trim
());
}}
/
>
<
Form
.
Select
field
=
"group"
label
=
"分组"
optionList
=
{
groupOptions
}
onChange
=
{(
v
)
=>
{
setSearchGroup
(
v
);
searchChannels
(
searchKeyword
,
v
,
searchModel
);
}}
/
>
<
Button
label
=
"查询"
type
=
"primary"
htmlType
=
"submit"
className
=
"btn-margin-right"
style
=
{{
marginRight
:
8
}}
>
查询
<
/Button
>
<
Form
.
Select
field
=
'group'
label
=
'分组'
optionList
=
{
groupOptions
}
onChange
=
{(
v
)
=>
{
setSearchGroup
(
v
);
searchChannels
(
searchKeyword
,
v
,
searchModel
);
}}
/
>
<
Button
label
=
'查询'
type
=
'primary'
htmlType
=
'submit'
className
=
'btn-margin-right'
style
=
{{
marginRight
:
8
}}
>
查询
<
/Button
>
<
/Space
>
<
/div
>
<
/Form
>
...
...
@@ -614,80 +696,118 @@ const ChannelsTable = () => {
<
Space
>
<
Space
>
<
Typography
.
Text
strong
>
使用
ID
排序
<
/Typography.Text
>
<
Switch
checked
=
{
idSort
}
label
=
"使用ID排序"
uncheckedText
=
"关"
aria
-
label
=
"是否用ID排序"
onChange
=
{(
v
)
=>
{
localStorage
.
setItem
(
'id-sort'
,
v
+
''
);
setIdSort
(
v
);
loadChannels
(
0
,
pageSize
,
v
)
.
then
()
.
catch
((
reason
)
=>
{
showError
(
reason
);
});
}}
><
/Switch
>
<
Switch
checked
=
{
idSort
}
label
=
'使用ID排序'
uncheckedText
=
'关'
aria
-
label
=
'是否用ID排序'
onChange
=
{(
v
)
=>
{
localStorage
.
setItem
(
'id-sort'
,
v
+
''
);
setIdSort
(
v
);
loadChannels
(
0
,
pageSize
,
v
)
.
then
()
.
catch
((
reason
)
=>
{
showError
(
reason
);
});
}}
><
/Switch
>
<
/Space
>
<
/Space
>
<
/div
>
<
Table
className
=
{
'channel-table'
}
style
=
{{
marginTop
:
15
}}
columns
=
{
columns
}
dataSource
=
{
pageData
}
pagination
=
{{
currentPage
:
activePage
,
pageSize
:
pageSize
,
total
:
channelCount
,
pageSizeOpts
:
[
10
,
20
,
50
,
100
],
showSizeChanger
:
true
,
formatPageText
:
(
page
)
=>
''
,
onPageSizeChange
:
(
size
)
=>
{
handlePageSizeChange
(
size
).
then
();
},
onPageChange
:
handlePageChange
}}
loading
=
{
loading
}
onRow
=
{
handleRow
}
rowSelection
=
{
enableBatchDelete
?
{
onChange
:
(
selectedRowKeys
,
selectedRows
)
=>
{
// console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
setSelectedChannels
(
selectedRows
);
}
}
:
null
}
/
>
<
div
style
=
{{
display
:
isMobile
()
?
''
:
'flex'
,
marginTop
:
isMobile
()
?
0
:
-
45
,
zIndex
:
999
,
position
:
'relative'
,
pointerEvents
:
'none'
}}
>
<
Space
style
=
{{
pointerEvents
:
'auto'
,
marginTop
:
isMobile
()
?
0
:
45
}}
>
<
Button
theme
=
"light"
type
=
"primary"
style
=
{{
marginRight
:
8
}}
onClick
=
{
()
=>
{
<
Table
className
=
{
'channel-table'
}
style
=
{{
marginTop
:
15
}}
columns
=
{
columns
}
dataSource
=
{
pageData
}
pagination
=
{{
currentPage
:
activePage
,
pageSize
:
pageSize
,
total
:
channelCount
,
pageSizeOpts
:
[
10
,
20
,
50
,
100
],
showSizeChanger
:
true
,
formatPageText
:
(
page
)
=>
''
,
onPageSizeChange
:
(
size
)
=>
{
handlePageSizeChange
(
size
).
then
();
},
onPageChange
:
handlePageChange
,
}}
loading
=
{
loading
}
onRow
=
{
handleRow
}
rowSelection
=
{
enableBatchDelete
?
{
onChange
:
(
selectedRowKeys
,
selectedRows
)
=>
{
// console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
setSelectedChannels
(
selectedRows
);
},
}
:
null
}
/
>
<
div
style
=
{{
display
:
isMobile
()
?
''
:
'flex'
,
marginTop
:
isMobile
()
?
0
:
-
45
,
zIndex
:
999
,
position
:
'relative'
,
pointerEvents
:
'none'
,
}}
>
<
Space
style
=
{{
pointerEvents
:
'auto'
,
marginTop
:
isMobile
()
?
0
:
45
}}
>
<
Button
theme
=
'light'
type
=
'primary'
style
=
{{
marginRight
:
8
}}
onClick
=
{()
=>
{
setEditingChannel
({
id
:
undefined
id
:
undefined
,
});
setShowEdit
(
true
);
}
}
>
添加渠道
<
/Button
>
}}
>
添加渠道
<
/Button
>
<
Popconfirm
title
=
"确定?"
title
=
'确定?'
okType
=
{
'warning'
}
onConfirm
=
{
testAllChannels
}
position
=
{
isMobile
()
?
'top'
:
'top'
}
>
<
Button
theme
=
"light"
type
=
"warning"
style
=
{{
marginRight
:
8
}}
>
测试所有通道
<
/Button
>
<
Button
theme
=
'light'
type
=
'warning'
style
=
{{
marginRight
:
8
}}
>
测试所有通道
<
/Button
>
<
/Popconfirm
>
<
Popconfirm
title
=
"确定?"
title
=
'确定?'
okType
=
{
'secondary'
}
onConfirm
=
{
updateAllChannelsBalance
}
>
<
Button
theme
=
"light"
type
=
"secondary"
style
=
{{
marginRight
:
8
}}
>
更新所有已启用通道余额
<
/Button
>
<
Button
theme
=
'light'
type
=
'secondary'
style
=
{{
marginRight
:
8
}}
>
更新所有已启用通道余额
<
/Button
>
<
/Popconfirm
>
<
Popconfirm
title
=
"确定是否要删除禁用通道?"
content
=
"此修改将不可逆"
title
=
'确定是否要删除禁用通道?'
content
=
'此修改将不可逆'
okType
=
{
'danger'
}
onConfirm
=
{
deleteAllDisabledChannels
}
>
<
Button
theme
=
"light"
type
=
"danger"
style
=
{{
marginRight
:
8
}}
>
删除禁用通道
<
/Button
>
<
Button
theme
=
'light'
type
=
'danger'
style
=
{{
marginRight
:
8
}}
>
删除禁用通道
<
/Button
>
<
/Popconfirm
>
<
Button
theme
=
"light"
type
=
"primary"
style
=
{{
marginRight
:
8
}}
onClick
=
{
refresh
}
>
刷新
<
/Button
>
<
Button
theme
=
'light'
type
=
'primary'
style
=
{{
marginRight
:
8
}}
onClick
=
{
refresh
}
>
刷新
<
/Button
>
<
/Space
>
{
/*<div style={{width: '100%', pointerEvents: 'none', position: 'absolute'}}>*/
}
...
...
@@ -696,28 +816,41 @@ const ChannelsTable = () => {
<
div
style
=
{{
marginTop
:
20
}}
>
<
Space
>
<
Typography
.
Text
strong
>
开启批量删除
<
/Typography.Text
>
<
Switch
label
=
"开启批量删除"
uncheckedText
=
"关"
aria
-
label
=
"是否开启批量删除"
onChange
=
{(
v
)
=>
{
setEnableBatchDelete
(
v
);
}}
><
/Switch
>
<
Switch
label
=
'开启批量删除'
uncheckedText
=
'关'
aria
-
label
=
'是否开启批量删除'
onChange
=
{(
v
)
=>
{
setEnableBatchDelete
(
v
);
}}
><
/Switch
>
<
Popconfirm
title
=
"确定是否要删除所选通道?"
content
=
"此修改将不可逆"
title
=
'确定是否要删除所选通道?'
content
=
'此修改将不可逆'
okType
=
{
'danger'
}
onConfirm
=
{
batchDeleteChannels
}
disabled
=
{
!
enableBatchDelete
}
position
=
{
'top'
}
>
<
Button
disabled
=
{
!
enableBatchDelete
}
theme
=
"light"
type
=
"danger"
style
=
{{
marginRight
:
8
}}
>
删除所选通道
<
/Button
>
<
Button
disabled
=
{
!
enableBatchDelete
}
theme
=
'light'
type
=
'danger'
style
=
{{
marginRight
:
8
}}
>
删除所选通道
<
/Button
>
<
/Popconfirm
>
<
Popconfirm
title
=
"确定是否要修复数据库一致性?"
content
=
"进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用"
title
=
'确定是否要修复数据库一致性?'
content
=
'进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用'
okType
=
{
'warning'
}
onConfirm
=
{
fixChannelsAbilities
}
position
=
{
'top'
}
>
<
Button
theme
=
"light"
type
=
"secondary"
style
=
{{
marginRight
:
8
}}
>
修复数据库一致性
<
/Button
>
<
Button
theme
=
'light'
type
=
'secondary'
style
=
{{
marginRight
:
8
}}
>
修复数据库一致性
<
/Button
>
<
/Popconfirm
>
<
/Space
>
<
/div
>
...
...
web/src/components/Footer.js
View file @
eb955ae4
...
...
@@ -32,27 +32,36 @@ const Footer = () => {
<
Layout
.
Content
style
=
{{
textAlign
:
'center'
}}
>
{
footer
?
(
<
div
className
=
"custom-footer"
className
=
'custom-footer'
dangerouslySetInnerHTML
=
{{
__html
:
footer
}}
><
/div
>
)
:
(
<
div
className
=
"custom-footer"
>
<
div
className
=
'custom-footer'
>
<
a
href
=
"https://github.com/Calcium-Ion/new-api"
target
=
"_blank"
rel
=
"noreferrer"
href
=
'https://github.com/Calcium-Ion/new-api'
target
=
'_blank'
rel
=
'noreferrer'
>
New
API
{
import
.
meta
.
env
.
VITE_REACT_APP_VERSION
}{
' '
}
<
/a
>
由
{
' '
}
<
a
href
=
"https://github.com/Calcium-Ion"
target
=
"_blank"
rel
=
"noreferrer"
>
<
a
href
=
'https://github.com/Calcium-Ion'
target
=
'_blank'
rel
=
'noreferrer'
>
Calcium
-
Ion
<
/a>{' '
}
开发,基于
{
' '
}
<
a
href
=
"https://github.com/songquanpeng/one-api"
target
=
"_blank"
rel
=
"noreferrer"
>
<
a
href
=
'https://github.com/songquanpeng/one-api'
target
=
'_blank'
rel
=
'noreferrer'
>
One
API
v0
.
5.4
<
/a>{' '
}
,本项目根据
{
' '
}
<
a
href
=
"https://opensource.org/licenses/mit-license.php"
>
<
a
href
=
'https://opensource.org/licenses/mit-license.php'
>
MIT
许可证
<
/a>{' '
}
授权
...
...
web/src/components/GitHubOAuth.js
View file @
eb955ae4
...
...
@@ -49,7 +49,7 @@ const GitHubOAuth = () => {
return
(
<
Segment
style
=
{{
minHeight
:
'300px'
}}
>
<
Dimmer
active
inverted
>
<
Loader
size
=
"large"
>
{
prompt
}
<
/Loader
>
<
Loader
size
=
'large'
>
{
prompt
}
<
/Loader
>
<
/Dimmer
>
<
/Segment
>
);
...
...
web/src/components/HeaderBar.js
View file @
eb955ae4
...
...
@@ -17,15 +17,15 @@ let headerButtons = [
text
:
'关于'
,
itemKey
:
'about'
,
to
:
'/about'
,
icon
:
<
IconHelpCircle
/>
}
icon
:
<
IconHelpCircle
/>
,
}
,
];
if
(
localStorage
.
getItem
(
'chat_link'
))
{
headerButtons
.
splice
(
1
,
0
,
{
name
:
'聊天'
,
to
:
'/chat'
,
icon
:
'comments'
icon
:
'comments'
,
});
}
...
...
@@ -40,7 +40,11 @@ const HeaderBar = () => {
var
themeMode
=
localStorage
.
getItem
(
'theme-mode'
);
const
currentDate
=
new
Date
();
// enable fireworks on new year(1.1 and 2.9-2.24)
const
isNewYear
=
(
currentDate
.
getMonth
()
===
0
&&
currentDate
.
getDate
()
===
1
)
||
(
currentDate
.
getMonth
()
===
1
&&
currentDate
.
getDate
()
>=
9
&&
currentDate
.
getDate
()
<=
24
);
const
isNewYear
=
(
currentDate
.
getMonth
()
===
0
&&
currentDate
.
getDate
()
===
1
)
||
(
currentDate
.
getMonth
()
===
1
&&
currentDate
.
getDate
()
>=
9
&&
currentDate
.
getDate
()
<=
24
);
async
function
logout
()
{
setShowSidebar
(
false
);
...
...
@@ -93,7 +97,7 @@ const HeaderBar = () => {
const
routerMap
=
{
about
:
'/about'
,
login
:
'/login'
,
register
:
'/register'
register
:
'/register'
,
};
return
(
<
Link
...
...
@@ -106,52 +110,69 @@ const HeaderBar = () => {
}}
selectedKeys
=
{[]}
// items={headerButtons}
onSelect
=
{
key
=>
{
}}
onSelect
=
{(
key
)
=>
{}}
footer
=
{
<>
{
isNewYear
&&
{
isNewYear
&&
(
// happy new year
<
Dropdown
position
=
"bottomRight"
position
=
'bottomRight'
render
=
{
<
Dropdown
.
Menu
>
<
Dropdown
.
Item
onClick
=
{
handleNewYearClick
}
>
Happy
New
Year
!!!<
/Dropdown.Item
>
<
Dropdown
.
Item
onClick
=
{
handleNewYearClick
}
>
Happy
New
Year
!!!
<
/Dropdown.Item
>
<
/Dropdown.Menu
>
}
>
<
Nav
.
Item
itemKey
=
{
'new-year'
}
text
=
{
'🏮'
}
/
>
<
/Dropdown
>
}
)
}
<
Nav
.
Item
itemKey
=
{
'about'
}
icon
=
{
<
IconHelpCircle
/>
}
/
>
<
Switch
checkedText
=
"🌞"
size
=
{
'large'
}
checked
=
{
dark
}
uncheckedText
=
"🌙"
onChange
=
{
switchMode
}
/
>
{
userState
.
user
?
<
Switch
checkedText
=
'🌞'
size
=
{
'large'
}
checked
=
{
dark
}
uncheckedText
=
'🌙'
onChange
=
{
switchMode
}
/
>
{
userState
.
user
?
(
<>
<
Dropdown
position
=
"bottomRight"
position
=
'bottomRight'
render
=
{
<
Dropdown
.
Menu
>
<
Dropdown
.
Item
onClick
=
{
logout
}
>
退出
<
/Dropdown.Item
>
<
/Dropdown.Menu
>
}
>
<
Avatar
size
=
"small"
color
=
{
stringToColor
(
userState
.
user
.
username
)}
style
=
{{
margin
:
4
}}
>
<
Avatar
size
=
'small'
color
=
{
stringToColor
(
userState
.
user
.
username
)}
style
=
{{
margin
:
4
}}
>
{
userState
.
user
.
username
[
0
]}
<
/Avatar
>
<
span
>
{
userState
.
user
.
username
}
<
/span
>
<
/Dropdown
>
<
/
>
:
)
:
(
<>
<
Nav
.
Item
itemKey
=
{
'login'
}
text
=
{
'登录'
}
icon
=
{
<
IconKey
/>
}
/
>
<
Nav
.
Item
itemKey
=
{
'register'
}
text
=
{
'注册'
}
icon
=
{
<
IconUser
/>
}
/
>
<
Nav
.
Item
itemKey
=
{
'login'
}
text
=
{
'登录'
}
icon
=
{
<
IconKey
/>
}
/
>
<
Nav
.
Item
itemKey
=
{
'register'
}
text
=
{
'注册'
}
icon
=
{
<
IconUser
/>
}
/
>
<
/
>
}
)
}
<
/
>
}
>
<
/Nav
>
><
/Nav
>
<
/div
>
<
/Layout
>
<
/
>
...
...
web/src/components/Loading.js
View file @
eb955ae4
...
...
@@ -3,7 +3,9 @@ import { Spin } from '@douyinfe/semi-ui';
const
Loading
=
({
prompt
:
name
=
'page'
})
=>
{
return
(
<
Spin
style
=
{{
height
:
100
}}
spinning
=
{
true
}
>
加载
{
name
}
中
...
<
/Spin
>
<
Spin
style
=
{{
height
:
100
}}
spinning
=
{
true
}
>
加载
{
name
}
中
...
<
/Spin
>
);
};
...
...
web/src/components/LoginForm.js
View file @
eb955ae4
...
...
@@ -4,7 +4,15 @@ import { UserContext } from '../context/User';
import
{
API
,
getLogo
,
showError
,
showInfo
,
showSuccess
}
from
'../helpers'
;
import
{
onGitHubOAuthClicked
}
from
'./utils'
;
import
Turnstile
from
'react-turnstile'
;
import
{
Button
,
Card
,
Divider
,
Form
,
Icon
,
Layout
,
Modal
}
from
'@douyinfe/semi-ui'
;
import
{
Button
,
Card
,
Divider
,
Form
,
Icon
,
Layout
,
Modal
,
}
from
'@douyinfe/semi-ui'
;
import
Title
from
'@douyinfe/semi-ui/lib/es/typography/title'
;
import
Text
from
'@douyinfe/semi-ui/lib/es/typography/text'
;
import
TelegramLoginButton
from
'react-telegram-login'
;
...
...
@@ -16,7 +24,7 @@ const LoginForm = () => {
const
[
inputs
,
setInputs
]
=
useState
({
username
:
''
,
password
:
''
,
wechat_verification_code
:
''
wechat_verification_code
:
''
,
});
const
[
searchParams
,
setSearchParams
]
=
useSearchParams
();
const
[
submitted
,
setSubmitted
]
=
useState
(
false
);
...
...
@@ -56,7 +64,7 @@ const LoginForm = () => {
return
;
}
const
res
=
await
API
.
get
(
`/api/oauth/wechat?code=
${
inputs
.
wechat_verification_code
}
`
`/api/oauth/wechat?code=
${
inputs
.
wechat_verification_code
}
`
,
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
...
...
@@ -81,17 +89,24 @@ const LoginForm = () => {
}
setSubmitted
(
true
);
if
(
username
&&
password
)
{
const
res
=
await
API
.
post
(
`/api/user/login?turnstile=
${
turnstileToken
}
`
,
{
username
,
password
});
const
res
=
await
API
.
post
(
`/api/user/login?turnstile=
${
turnstileToken
}
`
,
{
username
,
password
,
},
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
userDispatch
({
type
:
'login'
,
payload
:
data
});
localStorage
.
setItem
(
'user'
,
JSON
.
stringify
(
data
));
showSuccess
(
'登录成功!'
);
if
(
username
===
'root'
&&
password
===
'123456'
)
{
Modal
.
error
({
title
:
'您正在使用默认密码!'
,
content
:
'请立刻修改默认密码!'
,
centered
:
true
});
Modal
.
error
({
title
:
'您正在使用默认密码!'
,
content
:
'请立刻修改默认密码!'
,
centered
:
true
,
});
}
navigate
(
'/token'
);
}
else
{
...
...
@@ -104,7 +119,16 @@ const LoginForm = () => {
// 添加Telegram登录处理函数
const
onTelegramLoginClicked
=
async
(
response
)
=>
{
const
fields
=
[
'id'
,
'first_name'
,
'last_name'
,
'username'
,
'photo_url'
,
'auth_date'
,
'hash'
,
'lang'
];
const
fields
=
[
'id'
,
'first_name'
,
'last_name'
,
'username'
,
'photo_url'
,
'auth_date'
,
'hash'
,
'lang'
,
];
const
params
=
{};
fields
.
forEach
((
field
)
=>
{
if
(
response
[
field
])
{
...
...
@@ -126,10 +150,15 @@ const LoginForm = () => {
return
(
<
div
>
<
Layout
>
<
Layout
.
Header
>
<
/Layout.Header
>
<
Layout
.
Header
><
/Layout.Header
>
<
Layout
.
Content
>
<
div
style
=
{{
justifyContent
:
'center'
,
display
:
'flex'
,
marginTop
:
120
}}
>
<
div
style
=
{{
justifyContent
:
'center'
,
display
:
'flex'
,
marginTop
:
120
,
}}
>
<
div
style
=
{{
width
:
500
}}
>
<
Card
>
<
Title
heading
=
{
2
}
style
=
{{
textAlign
:
'center'
}}
>
...
...
@@ -139,50 +168,72 @@ const LoginForm = () => {
<
Form
.
Input
field
=
{
'username'
}
label
=
{
'用户名'
}
placeholder
=
"用户名"
name
=
"username"
placeholder
=
'用户名'
name
=
'username'
onChange
=
{(
value
)
=>
handleChange
(
'username'
,
value
)}
/
>
<
Form
.
Input
field
=
{
'password'
}
label
=
{
'密码'
}
placeholder
=
"密码"
name
=
"password"
type
=
"password"
placeholder
=
'密码'
name
=
'password'
type
=
'password'
onChange
=
{(
value
)
=>
handleChange
(
'password'
,
value
)}
/
>
<
Button
theme
=
"solid"
style
=
{{
width
:
'100%'
}}
type
=
{
'primary'
}
size
=
"large"
htmlType
=
{
'submit'
}
onClick
=
{
handleSubmit
}
>
<
Button
theme
=
'solid'
style
=
{{
width
:
'100%'
}}
type
=
{
'primary'
}
size
=
'large'
htmlType
=
{
'submit'
}
onClick
=
{
handleSubmit
}
>
登录
<
/Button
>
<
/Form
>
<
div
style
=
{{
display
:
'flex'
,
justifyContent
:
'space-between'
,
marginTop
:
20
}}
>
<
div
style
=
{{
display
:
'flex'
,
justifyContent
:
'space-between'
,
marginTop
:
20
,
}}
>
<
Text
>
没有账号请先
<
Link
to
=
"/register"
>
注册账号
<
/Link
>
没有账号请先
<
Link
to
=
'/register'
>
注册账号
<
/Link
>
<
/Text
>
<
Text
>
忘记密码
<
Link
to
=
"/reset"
>
点击重置
<
/Link
>
忘记密码
<
Link
to
=
'/reset'
>
点击重置
<
/Link
>
<
/Text
>
<
/div
>
{
status
.
github_oauth
||
status
.
wechat_login
||
status
.
telegram_oauth
?
(
{
status
.
github_oauth
||
status
.
wechat_login
||
status
.
telegram_oauth
?
(
<>
<
Divider
margin
=
"12px"
align
=
"center"
>
<
Divider
margin
=
'12px'
align
=
'center'
>
第三方登录
<
/Divider
>
<
div
style
=
{{
display
:
'flex'
,
justifyContent
:
'center'
,
marginTop
:
20
}}
>
<
div
style
=
{{
display
:
'flex'
,
justifyContent
:
'center'
,
marginTop
:
20
,
}}
>
{
status
.
github_oauth
?
(
<
Button
type
=
"primary"
type
=
'primary'
icon
=
{
<
IconGithubLogo
/>
}
onClick
=
{()
=>
onGitHubOAuthClicked
(
status
.
github_client_id
)}
onClick
=
{()
=>
onGitHubOAuthClicked
(
status
.
github_client_id
)
}
/
>
)
:
(
<><
/
>
)}
{
status
.
wechat_login
?
(
<
Button
type
=
"primary"
type
=
'primary'
style
=
{{
color
:
'rgba(var(--semi-green-5), 1)'
}}
icon
=
{
<
Icon
svg
=
{
<
WeChatIcon
/>
}
/>
}
onClick
=
{
onWeChatLoginClicked
}
...
...
@@ -192,7 +243,10 @@ const LoginForm = () => {
)}
{
status
.
telegram_oauth
?
(
<
TelegramLoginButton
dataOnauth
=
{
onTelegramLoginClicked
}
botName
=
{
status
.
telegram_bot_name
}
/
>
<
TelegramLoginButton
dataOnauth
=
{
onTelegramLoginClicked
}
botName
=
{
status
.
telegram_bot_name
}
/
>
)
:
(
<><
/
>
)}
...
...
@@ -202,7 +256,7 @@ const LoginForm = () => {
<><
/
>
)}
<
Modal
title
=
"微信扫码登录"
title
=
'微信扫码登录'
visible
=
{
showWeChatLoginModal
}
maskClosable
=
{
true
}
onOk
=
{
onSubmitWeChatVerificationCode
}
...
...
@@ -211,7 +265,13 @@ const LoginForm = () => {
size
=
{
'small'
}
centered
=
{
true
}
>
<
div
style
=
{{
display
:
'flex'
,
alignItem
:
'center'
,
flexDirection
:
'column'
}}
>
<
div
style
=
{{
display
:
'flex'
,
alignItem
:
'center'
,
flexDirection
:
'column'
,
}}
>
<
img
src
=
{
status
.
wechat_qrcode
}
/
>
<
/div
>
<
div
style
=
{{
textAlign
:
'center'
}}
>
...
...
@@ -219,19 +279,27 @@ const LoginForm = () => {
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
<
/p
>
<
/div
>
<
Form
size
=
"large"
>
<
Form
size
=
'large'
>
<
Form
.
Input
field
=
{
'wechat_verification_code'
}
placeholder
=
"验证码"
placeholder
=
'验证码'
label
=
{
'验证码'
}
value
=
{
inputs
.
wechat_verification_code
}
onChange
=
{(
value
)
=>
handleChange
(
'wechat_verification_code'
,
value
)}
onChange
=
{(
value
)
=>
handleChange
(
'wechat_verification_code'
,
value
)
}
/
>
<
/Form
>
<
/Modal
>
<
/Card
>
{
turnstileEnabled
?
(
<
div
style
=
{{
display
:
'flex'
,
justifyContent
:
'center'
,
marginTop
:
20
}}
>
<
div
style
=
{{
display
:
'flex'
,
justifyContent
:
'center'
,
marginTop
:
20
,
}}
>
<
Turnstile
sitekey
=
{
turnstileSiteKey
}
onVerify
=
{(
token
)
=>
{
...
...
@@ -244,7 +312,6 @@ const LoginForm = () => {
)}
<
/div
>
<
/div
>
<
/Layout.Content
>
<
/Layout
>
<
/div
>
...
...
web/src/components/LogsTable.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
API
,
copy
,
isAdmin
,
showError
,
showSuccess
,
timestamp2string
}
from
'../helpers'
;
import
{
API
,
copy
,
isAdmin
,
showError
,
showSuccess
,
timestamp2string
,
}
from
'../helpers'
;
import
{
Avatar
,
Button
,
Form
,
Layout
,
Modal
,
Select
,
Space
,
Spin
,
Table
,
Tag
}
from
'@douyinfe/semi-ui'
;
import
{
Avatar
,
Button
,
Form
,
Layout
,
Modal
,
Select
,
Space
,
Spin
,
Table
,
Tag
,
}
from
'@douyinfe/semi-ui'
;
import
{
ITEMS_PER_PAGE
}
from
'../constants'
;
import
{
renderNumber
,
renderQuota
,
stringToColor
}
from
'../helpers/render'
;
import
Paragraph
from
'@douyinfe/semi-ui/lib/es/typography/paragraph'
;
...
...
@@ -9,131 +27,285 @@ import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
const
{
Header
}
=
Layout
;
function
renderTimestamp
(
timestamp
)
{
return
(
<>
{
timestamp2string
(
timestamp
)}
<
/>
)
;
return
<>
{
timestamp2string
(
timestamp
)}
<
/>
;
}
const
MODE_OPTIONS
=
[{
key
:
'all'
,
text
:
'全部用户'
,
value
:
'all'
},
{
key
:
'self'
,
text
:
'当前用户'
,
value
:
'self'
}];
const
MODE_OPTIONS
=
[
{
key
:
'all'
,
text
:
'全部用户'
,
value
:
'all'
},
{
key
:
'self'
,
text
:
'当前用户'
,
value
:
'self'
},
];
const
colors
=
[
'amber'
,
'blue'
,
'cyan'
,
'green'
,
'grey'
,
'indigo'
,
'light-blue'
,
'lime'
,
'orange'
,
'pink'
,
'purple'
,
'red'
,
'teal'
,
'violet'
,
'yellow'
];
const
colors
=
[
'amber'
,
'blue'
,
'cyan'
,
'green'
,
'grey'
,
'indigo'
,
'light-blue'
,
'lime'
,
'orange'
,
'pink'
,
'purple'
,
'red'
,
'teal'
,
'violet'
,
'yellow'
,
];
function
renderType
(
type
)
{
switch
(
type
)
{
case
1
:
return
<
Tag
color
=
"cyan"
size
=
"large"
>
充值
<
/Tag>
;
return
(
<
Tag
color
=
'cyan'
size
=
'large'
>
{
' '
}
充值
{
' '
}
<
/Tag
>
);
case
2
:
return
<
Tag
color
=
"lime"
size
=
"large"
>
消费
<
/Tag>
;
return
(
<
Tag
color
=
'lime'
size
=
'large'
>
{
' '
}
消费
{
' '
}
<
/Tag
>
);
case
3
:
return
<
Tag
color
=
"orange"
size
=
"large"
>
管理
<
/Tag>
;
return
(
<
Tag
color
=
'orange'
size
=
'large'
>
{
' '
}
管理
{
' '
}
<
/Tag
>
);
case
4
:
return
<
Tag
color
=
"purple"
size
=
"large"
>
系统
<
/Tag>
;
return
(
<
Tag
color
=
'purple'
size
=
'large'
>
{
' '
}
系统
{
' '
}
<
/Tag
>
);
default
:
return
<
Tag
color
=
"black"
size
=
"large"
>
未知
<
/Tag>
;
return
(
<
Tag
color
=
'black'
size
=
'large'
>
{
' '
}
未知
{
' '
}
<
/Tag
>
);
}
}
function
renderIsStream
(
bool
)
{
if
(
bool
)
{
return
<
Tag
color
=
"blue"
size
=
"large"
>
流
<
/Tag>
;
return
(
<
Tag
color
=
'blue'
size
=
'large'
>
流
<
/Tag
>
);
}
else
{
return
<
Tag
color
=
"purple"
size
=
"large"
>
非流
<
/Tag>
;
return
(
<
Tag
color
=
'purple'
size
=
'large'
>
非流
<
/Tag
>
);
}
}
function
renderUseTime
(
type
)
{
const
time
=
parseInt
(
type
);
if
(
time
<
101
)
{
return
<
Tag
color
=
"green"
size
=
"large"
>
{
time
}
s
<
/Tag>
;
return
(
<
Tag
color
=
'green'
size
=
'large'
>
{
' '
}
{
time
}
s
{
' '
}
<
/Tag
>
);
}
else
if
(
time
<
300
)
{
return
<
Tag
color
=
"orange"
size
=
"large"
>
{
time
}
s
<
/Tag>
;
return
(
<
Tag
color
=
'orange'
size
=
'large'
>
{
' '
}
{
time
}
s
{
' '
}
<
/Tag
>
);
}
else
{
return
<
Tag
color
=
"red"
size
=
"large"
>
{
time
}
s
<
/Tag>
;
return
(
<
Tag
color
=
'red'
size
=
'large'
>
{
' '
}
{
time
}
s
{
' '
}
<
/Tag
>
);
}
}
const
LogsTable
=
()
=>
{
const
columns
=
[{
title
:
'时间'
,
dataIndex
:
'timestamp2string'
},
{
title
:
'渠道'
,
dataIndex
:
'channel'
,
className
:
isAdmin
()
?
'tableShow'
:
'tableHiddle'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
isAdminUser
?
record
.
type
===
0
||
record
.
type
===
2
?
<
div
>
{
<
Tag
color
=
{
colors
[
parseInt
(
text
)
%
colors
.
length
]}
size
=
"large"
>
{
text
}
<
/Tag>
}
<
/div> : <></
>
:
<><
/>
)
;
}
},
{
title
:
'用户'
,
dataIndex
:
'username'
,
className
:
isAdmin
()
?
'tableShow'
:
'tableHiddle'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
isAdminUser
?
<
div
>
<
Avatar
size
=
"small"
color
=
{
stringToColor
(
text
)}
style
=
{{
marginRight
:
4
}}
onClick
=
{()
=>
showUserInfo
(
record
.
user_id
)}
>
{
typeof
text
===
'string'
&&
text
.
slice
(
0
,
1
)}
<
/Avatar
>
{
text
}
<
/div> : <></
>
);
}
},
{
title
:
'令牌'
,
dataIndex
:
'token_name'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
record
.
type
===
0
||
record
.
type
===
2
?
<
div
>
<
Tag
color
=
"grey"
size
=
"large"
onClick
=
{()
=>
{
copyText
(
text
);
}}
>
{
text
}
<
/Tag
>
<
/div> : <></
>
);
}
},
{
title
:
'类型'
,
dataIndex
:
'type'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderType
(
text
)}
<
/div>
)
;
}
},
{
title
:
'模型'
,
dataIndex
:
'model_name'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
record
.
type
===
0
||
record
.
type
===
2
?
<
div
>
<
Tag
color
=
{
stringToColor
(
text
)}
size
=
"large"
onClick
=
{()
=>
{
copyText
(
text
);
}}
>
{
text
}
<
/Tag
>
<
/div> : <></
>
);
}
},
{
title
:
'用时'
,
dataIndex
:
'use_time'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
<
Space
>
{
renderUseTime
(
text
)}
{
renderIsStream
(
record
.
is_stream
)}
<
/Space
>
<
/div>
)
;
}
},
{
title
:
'提示'
,
dataIndex
:
'prompt_tokens'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
record
.
type
===
0
||
record
.
type
===
2
?
<
div
>
{
<
span
>
{
text
}
<
/span>
}
<
/div> : <></
>
);
}
},
{
title
:
'补全'
,
dataIndex
:
'completion_tokens'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
parseInt
(
text
)
>
0
&&
(
record
.
type
===
0
||
record
.
type
===
2
)
?
<
div
>
{
<
span
>
{
text
}
<
/span>
}
<
/div> : <></
>
);
}
},
{
title
:
'花费'
,
dataIndex
:
'quota'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
record
.
type
===
0
||
record
.
type
===
2
?
<
div
>
{
renderQuota
(
text
,
6
)}
<
/div> : <></
>
);
}
},
{
title
:
'详情'
,
dataIndex
:
'content'
,
render
:
(
text
,
record
,
index
)
=>
{
return
<
Paragraph
ellipsis
=
{{
rows
:
2
,
showTooltip
:
{
type
:
'popover'
,
opts
:
{
style
:
{
width
:
240
}
}
}
}}
style
=
{{
maxWidth
:
240
}}
>
{
text
}
<
/Paragraph>
;
}
}];
const
columns
=
[
{
title
:
'时间'
,
dataIndex
:
'timestamp2string'
,
},
{
title
:
'渠道'
,
dataIndex
:
'channel'
,
className
:
isAdmin
()
?
'tableShow'
:
'tableHiddle'
,
render
:
(
text
,
record
,
index
)
=>
{
return
isAdminUser
?
(
record
.
type
===
0
||
record
.
type
===
2
?
(
<
div
>
{
<
Tag
color
=
{
colors
[
parseInt
(
text
)
%
colors
.
length
]}
size
=
'large'
>
{
' '
}
{
text
}{
' '
}
<
/Tag
>
}
<
/div
>
)
:
(
<><
/
>
)
)
:
(
<><
/
>
);
},
},
{
title
:
'用户'
,
dataIndex
:
'username'
,
className
:
isAdmin
()
?
'tableShow'
:
'tableHiddle'
,
render
:
(
text
,
record
,
index
)
=>
{
return
isAdminUser
?
(
<
div
>
<
Avatar
size
=
'small'
color
=
{
stringToColor
(
text
)}
style
=
{{
marginRight
:
4
}}
onClick
=
{()
=>
showUserInfo
(
record
.
user_id
)}
>
{
typeof
text
===
'string'
&&
text
.
slice
(
0
,
1
)}
<
/Avatar
>
{
text
}
<
/div
>
)
:
(
<><
/
>
);
},
},
{
title
:
'令牌'
,
dataIndex
:
'token_name'
,
render
:
(
text
,
record
,
index
)
=>
{
return
record
.
type
===
0
||
record
.
type
===
2
?
(
<
div
>
<
Tag
color
=
'grey'
size
=
'large'
onClick
=
{()
=>
{
copyText
(
text
);
}}
>
{
' '
}
{
text
}{
' '
}
<
/Tag
>
<
/div
>
)
:
(
<><
/
>
);
},
},
{
title
:
'类型'
,
dataIndex
:
'type'
,
render
:
(
text
,
record
,
index
)
=>
{
return
<
div
>
{
renderType
(
text
)}
<
/div>
;
},
},
{
title
:
'模型'
,
dataIndex
:
'model_name'
,
render
:
(
text
,
record
,
index
)
=>
{
return
record
.
type
===
0
||
record
.
type
===
2
?
(
<
div
>
<
Tag
color
=
{
stringToColor
(
text
)}
size
=
'large'
onClick
=
{()
=>
{
copyText
(
text
);
}}
>
{
' '
}
{
text
}{
' '
}
<
/Tag
>
<
/div
>
)
:
(
<><
/
>
);
},
},
{
title
:
'用时'
,
dataIndex
:
'use_time'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
<
Space
>
{
renderUseTime
(
text
)}
{
renderIsStream
(
record
.
is_stream
)}
<
/Space
>
<
/div
>
);
},
},
{
title
:
'提示'
,
dataIndex
:
'prompt_tokens'
,
render
:
(
text
,
record
,
index
)
=>
{
return
record
.
type
===
0
||
record
.
type
===
2
?
(
<
div
>
{
<
span
>
{
text
}
<
/span>}</
div
>
)
:
(
<><
/
>
);
},
},
{
title
:
'补全'
,
dataIndex
:
'completion_tokens'
,
render
:
(
text
,
record
,
index
)
=>
{
return
parseInt
(
text
)
>
0
&&
(
record
.
type
===
0
||
record
.
type
===
2
)
?
(
<
div
>
{
<
span
>
{
text
}
<
/span>}</
div
>
)
:
(
<><
/
>
);
},
},
{
title
:
'花费'
,
dataIndex
:
'quota'
,
render
:
(
text
,
record
,
index
)
=>
{
return
record
.
type
===
0
||
record
.
type
===
2
?
(
<
div
>
{
renderQuota
(
text
,
6
)}
<
/div
>
)
:
(
<><
/
>
);
},
},
{
title
:
'详情'
,
dataIndex
:
'content'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
Paragraph
ellipsis
=
{{
rows
:
2
,
showTooltip
:
{
type
:
'popover'
,
opts
:
{
style
:
{
width
:
240
}
}
},
}}
style
=
{{
maxWidth
:
240
}}
>
{
text
}
<
/Paragraph
>
);
},
},
];
const
[
logs
,
setLogs
]
=
useState
([]);
const
[
showStat
,
setShowStat
]
=
useState
(
false
);
...
...
@@ -154,12 +326,20 @@ const LogsTable = () => {
model_name
:
''
,
start_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
-
86400
),
end_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
),
channel
:
''
channel
:
''
,
});
const
{
username
,
token_name
,
model_name
,
start_timestamp
,
end_timestamp
,
channel
}
=
inputs
;
const
{
username
,
token_name
,
model_name
,
start_timestamp
,
end_timestamp
,
channel
,
}
=
inputs
;
const
[
stat
,
setStat
]
=
useState
({
quota
:
0
,
token
:
0
quota
:
0
,
token
:
0
,
});
const
handleInputChange
=
(
value
,
name
)
=>
{
...
...
@@ -169,7 +349,9 @@ const LogsTable = () => {
const
getLogSelfStat
=
async
()
=>
{
let
localStartTimestamp
=
Date
.
parse
(
start_timestamp
)
/
1000
;
let
localEndTimestamp
=
Date
.
parse
(
end_timestamp
)
/
1000
;
let
res
=
await
API
.
get
(
`/api/log/self/stat?type=
${
logType
}
&token_name=
${
token_name
}
&model_name=
${
model_name
}
&start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
`
);
let
res
=
await
API
.
get
(
`/api/log/self/stat?type=
${
logType
}
&token_name=
${
token_name
}
&model_name=
${
model_name
}
&start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
`
,
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
setStat
(
data
);
...
...
@@ -181,7 +363,9 @@ const LogsTable = () => {
const
getLogStat
=
async
()
=>
{
let
localStartTimestamp
=
Date
.
parse
(
start_timestamp
)
/
1000
;
let
localEndTimestamp
=
Date
.
parse
(
end_timestamp
)
/
1000
;
let
res
=
await
API
.
get
(
`/api/log/stat?type=
${
logType
}
&username=
${
username
}
&token_name=
${
token_name
}
&model_name=
${
model_name
}
&start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
&channel=
${
channel
}
`
);
let
res
=
await
API
.
get
(
`/api/log/stat?type=
${
logType
}
&username=
${
username
}
&token_name=
${
token_name
}
&model_name=
${
model_name
}
&start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
&channel=
${
channel
}
`
,
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
setStat
(
data
);
...
...
@@ -209,12 +393,16 @@ const LogsTable = () => {
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
Modal
.
info
({
title
:
'用户信息'
,
content
:
<
div
style
=
{{
padding
:
12
}}
>
<
p
>
用户名
:
{
data
.
username
}
<
/p
>
<
p
>
余额
:
{
renderQuota
(
data
.
quota
)}
<
/p
>
<
p
>
已用额度:
{
renderQuota
(
data
.
used_quota
)}
<
/p
>
<
p
>
请求次数:
{
renderNumber
(
data
.
request_count
)}
<
/p
>
<
/div>, centered: tru
e
title
:
'用户信息'
,
content
:
(
<
div
style
=
{{
padding
:
12
}}
>
<
p
>
用户名
:
{
data
.
username
}
<
/p
>
<
p
>
余额
:
{
renderQuota
(
data
.
quota
)}
<
/p
>
<
p
>
已用额度:
{
renderQuota
(
data
.
used_quota
)}
<
/p
>
<
p
>
请求次数:
{
renderNumber
(
data
.
request_count
)}
<
/p
>
<
/div
>
),
centered
:
true
,
});
}
else
{
showError
(
message
);
...
...
@@ -259,14 +447,16 @@ const LogsTable = () => {
setLoading
(
false
);
};
const
pageData
=
logs
.
slice
((
activePage
-
1
)
*
pageSize
,
activePage
*
pageSize
);
const
pageData
=
logs
.
slice
(
(
activePage
-
1
)
*
pageSize
,
activePage
*
pageSize
,
);
const
handlePageChange
=
page
=>
{
const
handlePageChange
=
(
page
)
=>
{
setActivePage
(
page
);
if
(
page
===
Math
.
ceil
(
logs
.
length
/
pageSize
)
+
1
)
{
// In this case we have to load more data and then append them.
loadLogs
(
page
-
1
,
pageSize
,
logType
).
then
(
r
=>
{
});
loadLogs
(
page
-
1
,
pageSize
,
logType
).
then
((
r
)
=>
{});
}
};
...
...
@@ -298,7 +488,8 @@ const LogsTable = () => {
useEffect
(()
=>
{
// console.log('default effect')
const
localPageSize
=
parseInt
(
localStorage
.
getItem
(
'page-size'
))
||
ITEMS_PER_PAGE
;
const
localPageSize
=
parseInt
(
localStorage
.
getItem
(
'page-size'
))
||
ITEMS_PER_PAGE
;
setPageSize
(
localPageSize
);
loadLogs
(
0
,
localPageSize
)
.
then
()
...
...
@@ -326,74 +517,136 @@ const LogsTable = () => {
setSearching
(
false
);
};
return
(
<>
<
Layout
>
<
Header
>
<
Spin
spinning
=
{
loadingStat
}
>
<
h3
>
使用明细(总消耗额度:
<
span
onClick
=
{
handleEyeClick
}
style
=
{{
cursor
:
'pointer'
,
color
:
'gray'
}}
>
{
showStat
?
renderQuota
(
stat
.
quota
)
:
'点击查看'
}
<
/span
>
)
<
/h3
>
<
/Spin
>
<
/Header
>
<
Form
layout
=
"horizontal"
style
=
{{
marginTop
:
10
}}
>
<>
<
Form
.
Input
field
=
"token_name"
label
=
"令牌名称"
style
=
{{
width
:
176
}}
value
=
{
token_name
}
placeholder
=
{
'可选值'
}
name
=
"token_name"
onChange
=
{
value
=>
handleInputChange
(
value
,
'token_name'
)}
/
>
<
Form
.
Input
field
=
"model_name"
label
=
"模型名称"
style
=
{{
width
:
176
}}
value
=
{
model_name
}
placeholder
=
"可选值"
name
=
"model_name"
onChange
=
{
value
=>
handleInputChange
(
value
,
'model_name'
)}
/
>
<
Form
.
DatePicker
field
=
"start_timestamp"
label
=
"起始时间"
style
=
{{
width
:
272
}}
initValue
=
{
start_timestamp
}
value
=
{
start_timestamp
}
type
=
"dateTime"
name
=
"start_timestamp"
onChange
=
{
value
=>
handleInputChange
(
value
,
'start_timestamp'
)}
/
>
<
Form
.
DatePicker
field
=
"end_timestamp"
fluid
label
=
"结束时间"
style
=
{{
width
:
272
}}
initValue
=
{
end_timestamp
}
value
=
{
end_timestamp
}
type
=
"dateTime"
name
=
"end_timestamp"
onChange
=
{
value
=>
handleInputChange
(
value
,
'end_timestamp'
)}
/
>
{
isAdminUser
&&
<>
<
Form
.
Input
field
=
"channel"
label
=
"渠道 ID"
style
=
{{
width
:
176
}}
value
=
{
channel
}
placeholder
=
"可选值"
name
=
"channel"
onChange
=
{
value
=>
handleInputChange
(
value
,
'channel'
)}
/
>
<
Form
.
Input
field
=
"username"
label
=
"用户名称"
style
=
{{
width
:
176
}}
value
=
{
username
}
placeholder
=
{
'可选值'
}
name
=
"username"
onChange
=
{
value
=>
handleInputChange
(
value
,
'username'
)}
/
>
<
/>
}
<
Form
.
Section
>
<
Button
label
=
"查询"
type
=
"primary"
htmlType
=
"submit"
className
=
"btn-margin-right"
onClick
=
{
refresh
}
loading
=
{
loading
}
>
查询
<
/Button
>
<
/Form.Section
>
<
/
>
<
/Form
>
<
Table
style
=
{{
marginTop
:
5
}}
columns
=
{
columns
}
dataSource
=
{
pageData
}
pagination
=
{{
currentPage
:
activePage
,
pageSize
:
pageSize
,
total
:
logCount
,
pageSizeOpts
:
[
10
,
20
,
50
,
100
],
showSizeChanger
:
true
,
onPageSizeChange
:
(
size
)
=>
{
handlePageSizeChange
(
size
).
then
();
},
onPageChange
:
handlePageChange
}}
/
>
<
Select
defaultValue
=
"0"
style
=
{{
width
:
120
}}
onChange
=
{(
value
)
=>
{
setLogType
(
parseInt
(
value
));
refresh
(
parseInt
(
value
)).
then
();
}}
>
<
Select
.
Option
value
=
"0"
>
全部
<
/Select.Option
>
<
Select
.
Option
value
=
"1"
>
充值
<
/Select.Option
>
<
Select
.
Option
value
=
"2"
>
消费
<
/Select.Option
>
<
Select
.
Option
value
=
"3"
>
管理
<
/Select.Option
>
<
Select
.
Option
value
=
"4"
>
系统
<
/Select.Option
>
<
/Select
>
<
/Layout
>
<
/>
)
;
return
(
<>
<
Layout
>
<
Header
>
<
Spin
spinning
=
{
loadingStat
}
>
<
h3
>
使用明细(总消耗额度:
<
span
onClick
=
{
handleEyeClick
}
style
=
{{
cursor
:
'pointer'
,
color
:
'gray'
,
}}
>
{
showStat
?
renderQuota
(
stat
.
quota
)
:
'点击查看'
}
<
/span
>
)
<
/h3
>
<
/Spin
>
<
/Header
>
<
Form
layout
=
'horizontal'
style
=
{{
marginTop
:
10
}}
>
<>
<
Form
.
Input
field
=
'token_name'
label
=
'令牌名称'
style
=
{{
width
:
176
}}
value
=
{
token_name
}
placeholder
=
{
'可选值'
}
name
=
'token_name'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'token_name'
)}
/
>
<
Form
.
Input
field
=
'model_name'
label
=
'模型名称'
style
=
{{
width
:
176
}}
value
=
{
model_name
}
placeholder
=
'可选值'
name
=
'model_name'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'model_name'
)}
/
>
<
Form
.
DatePicker
field
=
'start_timestamp'
label
=
'起始时间'
style
=
{{
width
:
272
}}
initValue
=
{
start_timestamp
}
value
=
{
start_timestamp
}
type
=
'dateTime'
name
=
'start_timestamp'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'start_timestamp'
)}
/
>
<
Form
.
DatePicker
field
=
'end_timestamp'
fluid
label
=
'结束时间'
style
=
{{
width
:
272
}}
initValue
=
{
end_timestamp
}
value
=
{
end_timestamp
}
type
=
'dateTime'
name
=
'end_timestamp'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'end_timestamp'
)}
/
>
{
isAdminUser
&&
(
<>
<
Form
.
Input
field
=
'channel'
label
=
'渠道 ID'
style
=
{{
width
:
176
}}
value
=
{
channel
}
placeholder
=
'可选值'
name
=
'channel'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'channel'
)}
/
>
<
Form
.
Input
field
=
'username'
label
=
'用户名称'
style
=
{{
width
:
176
}}
value
=
{
username
}
placeholder
=
{
'可选值'
}
name
=
'username'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'username'
)}
/
>
<
/
>
)}
<
Form
.
Section
>
<
Button
label
=
'查询'
type
=
'primary'
htmlType
=
'submit'
className
=
'btn-margin-right'
onClick
=
{
refresh
}
loading
=
{
loading
}
>
查询
<
/Button
>
<
/Form.Section
>
<
/
>
<
/Form
>
<
Table
style
=
{{
marginTop
:
5
}}
columns
=
{
columns
}
dataSource
=
{
pageData
}
pagination
=
{{
currentPage
:
activePage
,
pageSize
:
pageSize
,
total
:
logCount
,
pageSizeOpts
:
[
10
,
20
,
50
,
100
],
showSizeChanger
:
true
,
onPageSizeChange
:
(
size
)
=>
{
handlePageSizeChange
(
size
).
then
();
},
onPageChange
:
handlePageChange
,
}}
/
>
<
Select
defaultValue
=
'0'
style
=
{{
width
:
120
}}
onChange
=
{(
value
)
=>
{
setLogType
(
parseInt
(
value
));
refresh
(
parseInt
(
value
)).
then
();
}}
>
<
Select
.
Option
value
=
'0'
>
全部
<
/Select.Option
>
<
Select
.
Option
value
=
'1'
>
充值
<
/Select.Option
>
<
Select
.
Option
value
=
'2'
>
消费
<
/Select.Option
>
<
Select
.
Option
value
=
'3'
>
管理
<
/Select.Option
>
<
Select
.
Option
value
=
'4'
>
系统
<
/Select.Option
>
<
/Select
>
<
/Layout
>
<
/
>
);
};
export
default
LogsTable
;
web/src/components/MjLogsTable.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
API
,
copy
,
isAdmin
,
showError
,
showSuccess
,
timestamp2string
}
from
'../helpers'
;
import
{
Banner
,
Button
,
Form
,
ImagePreview
,
Layout
,
Modal
,
Progress
,
Table
,
Tag
,
Typography
}
from
'@douyinfe/semi-ui'
;
import
{
API
,
copy
,
isAdmin
,
showError
,
showSuccess
,
timestamp2string
,
}
from
'../helpers'
;
import
{
Banner
,
Button
,
Form
,
ImagePreview
,
Layout
,
Modal
,
Progress
,
Table
,
Tag
,
Typography
,
}
from
'@douyinfe/semi-ui'
;
import
{
ITEMS_PER_PAGE
}
from
'../constants'
;
const
colors
=
[
'amber'
,
'blue'
,
'cyan'
,
'green'
,
'grey'
,
'indigo'
,
'light-blue'
,
'lime'
,
'orange'
,
'pink'
,
'purple'
,
'red'
,
'teal'
,
'violet'
,
'yellow'
const
colors
=
[
'amber'
,
'blue'
,
'cyan'
,
'green'
,
'grey'
,
'indigo'
,
'light-blue'
,
'lime'
,
'orange'
,
'pink'
,
'purple'
,
'red'
,
'teal'
,
'violet'
,
'yellow'
,
];
function
renderType
(
type
)
{
switch
(
type
)
{
case
'IMAGINE'
:
return
<
Tag
color
=
"blue"
size
=
"large"
>
绘图
<
/Tag>
;
return
(
<
Tag
color
=
'blue'
size
=
'large'
>
绘图
<
/Tag
>
);
case
'UPSCALE'
:
return
<
Tag
color
=
"orange"
size
=
"large"
>
放大
<
/Tag>
;
return
(
<
Tag
color
=
'orange'
size
=
'large'
>
放大
<
/Tag
>
);
case
'VARIATION'
:
return
<
Tag
color
=
"purple"
size
=
"large"
>
变换
<
/Tag>
;
return
(
<
Tag
color
=
'purple'
size
=
'large'
>
变换
<
/Tag
>
);
case
'HIGH_VARIATION'
:
return
<
Tag
color
=
"purple"
size
=
"large"
>
强变换
<
/Tag>
;
return
(
<
Tag
color
=
'purple'
size
=
'large'
>
强变换
<
/Tag
>
);
case
'LOW_VARIATION'
:
return
<
Tag
color
=
"purple"
size
=
"large"
>
弱变换
<
/Tag>
;
return
(
<
Tag
color
=
'purple'
size
=
'large'
>
弱变换
<
/Tag
>
);
case
'PAN'
:
return
<
Tag
color
=
"cyan"
size
=
"large"
>
平移
<
/Tag>
;
return
(
<
Tag
color
=
'cyan'
size
=
'large'
>
平移
<
/Tag
>
);
case
'DESCRIBE'
:
return
<
Tag
color
=
"yellow"
size
=
"large"
>
图生文
<
/Tag>
;
return
(
<
Tag
color
=
'yellow'
size
=
'large'
>
图生文
<
/Tag
>
);
case
'BLEND'
:
return
<
Tag
color
=
"lime"
size
=
"large"
>
图混合
<
/Tag>
;
return
(
<
Tag
color
=
'lime'
size
=
'large'
>
图混合
<
/Tag
>
);
case
'SHORTEN'
:
return
<
Tag
color
=
"pink"
size
=
"large"
>
缩词
<
/Tag>
;
return
(
<
Tag
color
=
'pink'
size
=
'large'
>
缩词
<
/Tag
>
);
case
'REROLL'
:
return
<
Tag
color
=
"indigo"
size
=
"large"
>
重绘
<
/Tag>
;
return
(
<
Tag
color
=
'indigo'
size
=
'large'
>
重绘
<
/Tag
>
);
case
'INPAINT'
:
return
<
Tag
color
=
"violet"
size
=
"large"
>
局部重绘
-
提交
<
/Tag>
;
return
(
<
Tag
color
=
'violet'
size
=
'large'
>
局部重绘
-
提交
<
/Tag
>
);
case
'ZOOM'
:
return
<
Tag
color
=
"teal"
size
=
"large"
>
变焦
<
/Tag>
;
return
(
<
Tag
color
=
'teal'
size
=
'large'
>
变焦
<
/Tag
>
);
case
'CUSTOM_ZOOM'
:
return
<
Tag
color
=
"teal"
size
=
"large"
>
自定义变焦
-
提交
<
/Tag>
;
return
(
<
Tag
color
=
'teal'
size
=
'large'
>
自定义变焦
-
提交
<
/Tag
>
);
case
'MODAL'
:
return
<
Tag
color
=
"green"
size
=
"large"
>
窗口处理
<
/Tag>
;
return
(
<
Tag
color
=
'green'
size
=
'large'
>
窗口处理
<
/Tag
>
);
case
'SWAP_FACE'
:
return
<
Tag
color
=
"light-green"
size
=
"large"
>
换脸
<
/Tag>
;
return
(
<
Tag
color
=
'light-green'
size
=
'large'
>
换脸
<
/Tag
>
);
default
:
return
<
Tag
color
=
"white"
size
=
"large"
>
未知
<
/Tag>
;
return
(
<
Tag
color
=
'white'
size
=
'large'
>
未知
<
/Tag
>
);
}
}
function
renderCode
(
code
)
{
switch
(
code
)
{
case
1
:
return
<
Tag
color
=
"green"
size
=
"large"
>
已提交
<
/Tag>
;
return
(
<
Tag
color
=
'green'
size
=
'large'
>
已提交
<
/Tag
>
);
case
21
:
return
<
Tag
color
=
"lime"
size
=
"large"
>
等待中
<
/Tag>
;
return
(
<
Tag
color
=
'lime'
size
=
'large'
>
等待中
<
/Tag
>
);
case
22
:
return
<
Tag
color
=
"orange"
size
=
"large"
>
重复提交
<
/Tag>
;
return
(
<
Tag
color
=
'orange'
size
=
'large'
>
重复提交
<
/Tag
>
);
case
0
:
return
<
Tag
color
=
"yellow"
size
=
"large"
>
未提交
<
/Tag>
;
return
(
<
Tag
color
=
'yellow'
size
=
'large'
>
未提交
<
/Tag
>
);
default
:
return
<
Tag
color
=
"white"
size
=
"large"
>
未知
<
/Tag>
;
return
(
<
Tag
color
=
'white'
size
=
'large'
>
未知
<
/Tag
>
);
}
}
function
renderStatus
(
type
)
{
// Ensure all cases are string literals by adding quotes.
switch
(
type
)
{
case
'SUCCESS'
:
return
<
Tag
color
=
"green"
size
=
"large"
>
成功
<
/Tag>
;
return
(
<
Tag
color
=
'green'
size
=
'large'
>
成功
<
/Tag
>
);
case
'NOT_START'
:
return
<
Tag
color
=
"grey"
size
=
"large"
>
未启动
<
/Tag>
;
return
(
<
Tag
color
=
'grey'
size
=
'large'
>
未启动
<
/Tag
>
);
case
'SUBMITTED'
:
return
<
Tag
color
=
"yellow"
size
=
"large"
>
队列中
<
/Tag>
;
return
(
<
Tag
color
=
'yellow'
size
=
'large'
>
队列中
<
/Tag
>
);
case
'IN_PROGRESS'
:
return
<
Tag
color
=
"blue"
size
=
"large"
>
执行中
<
/Tag>
;
return
(
<
Tag
color
=
'blue'
size
=
'large'
>
执行中
<
/Tag
>
);
case
'FAILURE'
:
return
<
Tag
color
=
"red"
size
=
"large"
>
失败
<
/Tag>
;
return
(
<
Tag
color
=
'red'
size
=
'large'
>
失败
<
/Tag
>
);
case
'MODAL'
:
return
<
Tag
color
=
"yellow"
size
=
"large"
>
窗口等待
<
/Tag>
;
return
(
<
Tag
color
=
'yellow'
size
=
'large'
>
窗口等待
<
/Tag
>
);
default
:
return
<
Tag
color
=
"white"
size
=
"large"
>
未知
<
/Tag>
;
return
(
<
Tag
color
=
'white'
size
=
'large'
>
未知
<
/Tag
>
);
}
}
...
...
@@ -97,7 +237,6 @@ const renderTimestamp = (timestampInSeconds) => {
return
`
${
year
}
-
${
month
}
-
${
day
}
${
hours
}
:
${
minutes
}
:
${
seconds
}
`
;
// 格式化输出
};
const
LogsTable
=
()
=>
{
const
[
isModalOpen
,
setIsModalOpen
]
=
useState
(
false
);
const
[
modalContent
,
setModalContent
]
=
useState
(
''
);
...
...
@@ -106,12 +245,8 @@ const LogsTable = () => {
title
:
'提交时间'
,
dataIndex
:
'submit_time'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderTimestamp
(
text
/
1000
)}
<
/div
>
);
}
return
<
div
>
{
renderTimestamp
(
text
/
1000
)}
<
/div>
;
},
},
{
title
:
'渠道'
,
...
...
@@ -119,61 +254,50 @@ const LogsTable = () => {
className
:
isAdmin
()
?
'tableShow'
:
'tableHiddle'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
<
Tag
color
=
{
colors
[
parseInt
(
text
)
%
colors
.
length
]}
size
=
"large"
onClick
=
{()
=>
{
copyText
(
text
);
// 假设copyText是用于文本复制的函数
}}
>
{
text
}
<
/Tag
>
<
Tag
color
=
{
colors
[
parseInt
(
text
)
%
colors
.
length
]}
size
=
'large'
onClick
=
{()
=>
{
copyText
(
text
);
// 假设copyText是用于文本复制的函数
}}
>
{
' '
}
{
text
}{
' '
}
<
/Tag
>
<
/div
>
);
}
}
,
},
{
title
:
'类型'
,
dataIndex
:
'action'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderType
(
text
)}
<
/div
>
);
}
return
<
div
>
{
renderType
(
text
)}
<
/div>
;
},
},
{
title
:
'任务ID'
,
dataIndex
:
'mj_id'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
text
}
<
/div
>
);
}
return
<
div
>
{
text
}
<
/div>
;
},
},
{
title
:
'提交结果'
,
dataIndex
:
'code'
,
className
:
isAdmin
()
?
'tableShow'
:
'tableHiddle'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderCode
(
text
)}
<
/div
>
);
}
return
<
div
>
{
renderCode
(
text
)}
<
/div>
;
},
},
{
title
:
'任务状态'
,
dataIndex
:
'status'
,
className
:
isAdmin
()
?
'tableShow'
:
'tableHiddle'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderStatus
(
text
)}
<
/div
>
);
}
return
<
div
>
{
renderStatus
(
text
)}
<
/div>
;
},
},
{
title
:
'进度'
,
...
...
@@ -183,13 +307,20 @@ const LogsTable = () => {
<
div
>
{
// 转换例如100%为数字100,如果text未定义,返回0
<
Progress
stroke
=
{
record
.
status
===
'FAILURE'
?
'var(--semi-color-warning)'
:
null
}
percent
=
{
text
?
parseInt
(
text
.
replace
(
'%'
,
''
))
:
0
}
showInfo
=
{
true
}
aria
-
label
=
"drawing progress"
/>
<
Progress
stroke
=
{
record
.
status
===
'FAILURE'
?
'var(--semi-color-warning)'
:
null
}
percent
=
{
text
?
parseInt
(
text
.
replace
(
'%'
,
''
))
:
0
}
showInfo
=
{
true
}
aria
-
label
=
'drawing progress'
/>
}
<
/div
>
);
}
}
,
},
{
title
:
'结果图片'
,
...
...
@@ -201,14 +332,14 @@ const LogsTable = () => {
return
(
<
Button
onClick
=
{()
=>
{
setModalImageUrl
(
text
);
// 更新图片URL状态
setIsModalOpenurl
(
true
);
// 打开模态框
setModalImageUrl
(
text
);
// 更新图片URL状态
setIsModalOpenurl
(
true
);
// 打开模态框
}}
>
查看图片
<
/Button
>
);
}
}
,
},
{
title
:
'Prompt'
,
...
...
@@ -231,7 +362,7 @@ const LogsTable = () => {
{
text
}
<
/Typography.Text
>
);
}
}
,
},
{
title
:
'PromptEn'
,
...
...
@@ -254,7 +385,7 @@ const LogsTable = () => {
{
text
}
<
/Typography.Text
>
);
}
}
,
},
{
title
:
'失败原因'
,
...
...
@@ -277,9 +408,8 @@ const LogsTable = () => {
{
text
}
<
/Typography.Text
>
);
}
}
},
},
];
const
[
logs
,
setLogs
]
=
useState
([]);
...
...
@@ -299,20 +429,19 @@ const LogsTable = () => {
channel_id
:
''
,
mj_id
:
''
,
start_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
-
2592000
),
end_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
)
end_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
)
,
});
const
{
channel_id
,
mj_id
,
start_timestamp
,
end_timestamp
}
=
inputs
;
const
[
stat
,
setStat
]
=
useState
({
quota
:
0
,
token
:
0
token
:
0
,
});
const
handleInputChange
=
(
value
,
name
)
=>
{
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
};
const
setLogsFormat
=
(
logs
)
=>
{
for
(
let
i
=
0
;
i
<
logs
.
length
;
i
++
)
{
logs
[
i
].
timestamp2string
=
timestamp2string
(
logs
[
i
].
created_at
);
...
...
@@ -351,14 +480,16 @@ const LogsTable = () => {
setLoading
(
false
);
};
const
pageData
=
logs
.
slice
((
activePage
-
1
)
*
ITEMS_PER_PAGE
,
activePage
*
ITEMS_PER_PAGE
);
const
pageData
=
logs
.
slice
(
(
activePage
-
1
)
*
ITEMS_PER_PAGE
,
activePage
*
ITEMS_PER_PAGE
,
);
const
handlePageChange
=
page
=>
{
const
handlePageChange
=
(
page
)
=>
{
setActivePage
(
page
);
if
(
page
===
Math
.
ceil
(
logs
.
length
/
ITEMS_PER_PAGE
)
+
1
)
{
// In this case we have to load more data and then append them.
loadLogs
(
page
-
1
).
then
(
r
=>
{
});
loadLogs
(
page
-
1
).
then
((
r
)
=>
{});
}
};
...
...
@@ -390,46 +521,83 @@ const LogsTable = () => {
return
(
<>
<
Layout
>
{
isAdminUser
&&
showBanner
?
<
Banner
type
=
"info"
description
=
"当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。"
/>
:
<><
/
>
}
<
Form
layout
=
"horizontal"
style
=
{{
marginTop
:
10
}}
>
{
isAdminUser
&&
showBanner
?
(
<
Banner
type
=
'info'
description
=
'当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。'
/>
)
:
(
<><
/
>
)}
<
Form
layout
=
'horizontal'
style
=
{{
marginTop
:
10
}}
>
<>
<
Form
.
Input
field
=
"channel_id"
label
=
"渠道 ID"
style
=
{{
width
:
176
}}
value
=
{
channel_id
}
placeholder
=
{
'可选值'
}
name
=
"channel_id"
onChange
=
{
value
=>
handleInputChange
(
value
,
'channel_id'
)}
/
>
<
Form
.
Input
field
=
"mj_id"
label
=
"任务 ID"
style
=
{{
width
:
176
}}
value
=
{
mj_id
}
placeholder
=
"可选值"
name
=
"mj_id"
onChange
=
{
value
=>
handleInputChange
(
value
,
'mj_id'
)}
/
>
<
Form
.
DatePicker
field
=
"start_timestamp"
label
=
"起始时间"
style
=
{{
width
:
272
}}
initValue
=
{
start_timestamp
}
value
=
{
start_timestamp
}
type
=
"dateTime"
name
=
"start_timestamp"
onChange
=
{
value
=>
handleInputChange
(
value
,
'start_timestamp'
)}
/
>
<
Form
.
DatePicker
field
=
"end_timestamp"
fluid
label
=
"结束时间"
style
=
{{
width
:
272
}}
initValue
=
{
end_timestamp
}
value
=
{
end_timestamp
}
type
=
"dateTime"
name
=
"end_timestamp"
onChange
=
{
value
=>
handleInputChange
(
value
,
'end_timestamp'
)}
/
>
<
Form
.
Input
field
=
'channel_id'
label
=
'渠道 ID'
style
=
{{
width
:
176
}}
value
=
{
channel_id
}
placeholder
=
{
'可选值'
}
name
=
'channel_id'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'channel_id'
)}
/
>
<
Form
.
Input
field
=
'mj_id'
label
=
'任务 ID'
style
=
{{
width
:
176
}}
value
=
{
mj_id
}
placeholder
=
'可选值'
name
=
'mj_id'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'mj_id'
)}
/
>
<
Form
.
DatePicker
field
=
'start_timestamp'
label
=
'起始时间'
style
=
{{
width
:
272
}}
initValue
=
{
start_timestamp
}
value
=
{
start_timestamp
}
type
=
'dateTime'
name
=
'start_timestamp'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'start_timestamp'
)}
/
>
<
Form
.
DatePicker
field
=
'end_timestamp'
fluid
label
=
'结束时间'
style
=
{{
width
:
272
}}
initValue
=
{
end_timestamp
}
value
=
{
end_timestamp
}
type
=
'dateTime'
name
=
'end_timestamp'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'end_timestamp'
)}
/
>
<
Form
.
Section
>
<
Button
label
=
"查询"
type
=
"primary"
htmlType
=
"submit"
className
=
"btn-margin-right"
onClick
=
{
refresh
}
>
查询
<
/Button
>
<
Button
label
=
'查询'
type
=
'primary'
htmlType
=
'submit'
className
=
'btn-margin-right'
onClick
=
{
refresh
}
>
查询
<
/Button
>
<
/Form.Section
>
<
/
>
<
/Form
>
<
Table
style
=
{{
marginTop
:
5
}}
columns
=
{
columns
}
dataSource
=
{
pageData
}
pagination
=
{{
currentPage
:
activePage
,
pageSize
:
ITEMS_PER_PAGE
,
total
:
logCount
,
pageSizeOpts
:
[
10
,
20
,
50
,
100
],
onPageChange
:
handlePageChange
}}
loading
=
{
loading
}
/
>
<
Table
style
=
{{
marginTop
:
5
}}
columns
=
{
columns
}
dataSource
=
{
pageData
}
pagination
=
{{
currentPage
:
activePage
,
pageSize
:
ITEMS_PER_PAGE
,
total
:
logCount
,
pageSizeOpts
:
[
10
,
20
,
50
,
100
],
onPageChange
:
handlePageChange
,
}}
loading
=
{
loading
}
/
>
<
Modal
visible
=
{
isModalOpen
}
onOk
=
{()
=>
setIsModalOpen
(
false
)}
...
...
@@ -445,7 +613,6 @@ const LogsTable = () => {
visible
=
{
isModalOpenurl
}
onVisibleChange
=
{(
visible
)
=>
setIsModalOpenurl
(
visible
)}
/
>
<
/Layout
>
<
/
>
);
...
...
web/src/components/OperationSetting.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
Divider
,
Form
,
Grid
,
Header
}
from
'semantic-ui-react'
;
import
{
API
,
showError
,
showSuccess
,
timestamp2string
,
verifyJSON
}
from
'../helpers'
;
import
{
API
,
showError
,
showSuccess
,
timestamp2string
,
verifyJSON
,
}
from
'../helpers'
;
const
OperationSetting
=
()
=>
{
let
now
=
new
Date
();
...
...
@@ -35,16 +41,18 @@ const OperationSetting = () => {
DataExportDefaultTime
:
'hour'
,
DataExportInterval
:
5
,
DefaultCollapseSidebar
:
''
,
// 默认折叠侧边栏
RetryTimes
:
0
RetryTimes
:
0
,
});
const
[
originInputs
,
setOriginInputs
]
=
useState
({});
let
[
loading
,
setLoading
]
=
useState
(
false
);
let
[
historyTimestamp
,
setHistoryTimestamp
]
=
useState
(
timestamp2string
(
now
.
getTime
()
/
1000
-
30
*
24
*
3600
));
// a month ago
let
[
historyTimestamp
,
setHistoryTimestamp
]
=
useState
(
timestamp2string
(
now
.
getTime
()
/
1000
-
30
*
24
*
3600
),
);
// a month ago
// 精确时间选项(小时,天,周)
const
timeOptions
=
[
{
key
:
'hour'
,
text
:
'小时'
,
value
:
'hour'
},
{
key
:
'day'
,
text
:
'天'
,
value
:
'day'
},
{
key
:
'week'
,
text
:
'周'
,
value
:
'week'
}
{
key
:
'week'
,
text
:
'周'
,
value
:
'week'
}
,
];
const
getOptions
=
async
()
=>
{
const
res
=
await
API
.
get
(
'/api/option/'
);
...
...
@@ -52,7 +60,11 @@ const OperationSetting = () => {
if
(
success
)
{
let
newInputs
=
{};
data
.
forEach
((
item
)
=>
{
if
(
item
.
key
===
'ModelRatio'
||
item
.
key
===
'GroupRatio'
||
item
.
key
===
'ModelPrice'
)
{
if
(
item
.
key
===
'ModelRatio'
||
item
.
key
===
'GroupRatio'
||
item
.
key
===
'ModelPrice'
)
{
item
.
value
=
JSON
.
stringify
(
JSON
.
parse
(
item
.
value
),
null
,
2
);
}
newInputs
[
item
.
key
]
=
item
.
value
;
...
...
@@ -79,7 +91,7 @@ const OperationSetting = () => {
console
.
log
(
key
,
value
);
const
res
=
await
API
.
put
(
'/api/option/'
,
{
key
,
value
value
,
});
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
...
...
@@ -91,7 +103,12 @@ const OperationSetting = () => {
};
const
handleInputChange
=
async
(
e
,
{
name
,
value
})
=>
{
if
(
name
.
endsWith
(
'Enabled'
)
||
name
===
'DataExportInterval'
||
name
===
'DataExportDefaultTime'
||
name
===
'DefaultCollapseSidebar'
)
{
if
(
name
.
endsWith
(
'Enabled'
)
||
name
===
'DataExportInterval'
||
name
===
'DataExportDefaultTime'
||
name
===
'DefaultCollapseSidebar'
)
{
if
(
name
===
'DataExportDefaultTime'
)
{
localStorage
.
setItem
(
'data_export_default_time'
,
value
);
}
else
if
(
name
===
'MjNotifyEnabled'
)
{
...
...
@@ -106,11 +123,22 @@ const OperationSetting = () => {
const
submitConfig
=
async
(
group
)
=>
{
switch
(
group
)
{
case
'monitor'
:
if
(
originInputs
[
'ChannelDisableThreshold'
]
!==
inputs
.
ChannelDisableThreshold
)
{
await
updateOption
(
'ChannelDisableThreshold'
,
inputs
.
ChannelDisableThreshold
);
if
(
originInputs
[
'ChannelDisableThreshold'
]
!==
inputs
.
ChannelDisableThreshold
)
{
await
updateOption
(
'ChannelDisableThreshold'
,
inputs
.
ChannelDisableThreshold
,
);
}
if
(
originInputs
[
'QuotaRemindThreshold'
]
!==
inputs
.
QuotaRemindThreshold
)
{
await
updateOption
(
'QuotaRemindThreshold'
,
inputs
.
QuotaRemindThreshold
);
if
(
originInputs
[
'QuotaRemindThreshold'
]
!==
inputs
.
QuotaRemindThreshold
)
{
await
updateOption
(
'QuotaRemindThreshold'
,
inputs
.
QuotaRemindThreshold
,
);
}
break
;
case
'ratio'
:
...
...
@@ -177,7 +205,9 @@ const OperationSetting = () => {
const
deleteHistoryLogs
=
async
()
=>
{
console
.
log
(
inputs
);
const
res
=
await
API
.
delete
(
`/api/log/?target_timestamp=
${
Date
.
parse
(
historyTimestamp
)
/
1000
}
`
);
const
res
=
await
API
.
delete
(
`/api/log/?target_timestamp=
${
Date
.
parse
(
historyTimestamp
)
/
1000
}
`
,
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
showSuccess
(
`
${
data
}
条日志已清理!`
);
...
...
@@ -189,131 +219,129 @@ const OperationSetting = () => {
<
Grid
columns
=
{
1
}
>
<
Grid
.
Column
>
<
Form
loading
=
{
loading
}
>
<
Header
as
=
"h3"
>
通用设置
<
/Header
>
<
Header
as
=
'h3'
>
通用设置
<
/Header
>
<
Form
.
Group
widths
=
{
4
}
>
<
Form
.
Input
label
=
"充值链接"
name
=
"TopUpLink"
label
=
'充值链接'
name
=
'TopUpLink'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
TopUpLink
}
type
=
"link"
placeholder
=
"例如发卡网站的购买链接"
type
=
'link'
placeholder
=
'例如发卡网站的购买链接'
/>
<
Form
.
Input
label
=
"默认聊天页面链接"
name
=
"ChatLink"
label
=
'默认聊天页面链接'
name
=
'ChatLink'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
ChatLink
}
type
=
"link"
placeholder
=
"例如 ChatGPT Next Web 的部署地址"
type
=
'link'
placeholder
=
'例如 ChatGPT Next Web 的部署地址'
/>
<
Form
.
Input
label
=
"聊天页面2链接"
name
=
"ChatLink2"
label
=
'聊天页面2链接'
name
=
'ChatLink2'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
ChatLink2
}
type
=
"link"
placeholder
=
"例如 ChatGPT Web & Midjourney 的部署地址"
type
=
'link'
placeholder
=
'例如 ChatGPT Web & Midjourney 的部署地址'
/>
<
Form
.
Input
label
=
"单位美元额度"
name
=
"QuotaPerUnit"
label
=
'单位美元额度'
name
=
'QuotaPerUnit'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
QuotaPerUnit
}
type
=
"number"
step
=
"0.01"
placeholder
=
"一单位货币能兑换的额度"
type
=
'number'
step
=
'0.01'
placeholder
=
'一单位货币能兑换的额度'
/>
<
Form
.
Input
label
=
"失败重试次数"
name
=
"RetryTimes"
label
=
'失败重试次数'
name
=
'RetryTimes'
type
=
{
'number'
}
step
=
"1"
min
=
"0"
step
=
'1'
min
=
'0'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
RetryTimes
}
placeholder
=
"失败重试次数"
placeholder
=
'失败重试次数'
/>
<
/Form.Group
>
<
Form
.
Group
inline
>
<
Form
.
Checkbox
checked
=
{
inputs
.
DisplayInCurrencyEnabled
===
'true'
}
label
=
"以货币形式显示额度"
name
=
"DisplayInCurrencyEnabled"
label
=
'以货币形式显示额度'
name
=
'DisplayInCurrencyEnabled'
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Checkbox
checked
=
{
inputs
.
DisplayTokenStatEnabled
===
'true'
}
label
=
"Billing 相关 API 显示令牌额度而非用户额度"
name
=
"DisplayTokenStatEnabled"
label
=
'Billing 相关 API 显示令牌额度而非用户额度'
name
=
'DisplayTokenStatEnabled'
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Checkbox
checked
=
{
inputs
.
DefaultCollapseSidebar
===
'true'
}
label
=
"默认折叠侧边栏"
name
=
"DefaultCollapseSidebar"
label
=
'默认折叠侧边栏'
name
=
'DefaultCollapseSidebar'
onChange
=
{
handleInputChange
}
/
>
<
/Form.Group
>
<
Form
.
Button
onClick
=
{()
=>
{
submitConfig
(
'general'
).
then
();
}}
>
保存通用设置
<
/Form.Button
>
<
Form
.
Button
onClick
=
{()
=>
{
submitConfig
(
'general'
).
then
();
}}
>
保存通用设置
<
/Form.Button
>
<
Divider
/>
<
Header
as
=
"h3"
>
绘图设置
<
/Header
>
<
Header
as
=
'h3'
>
绘图设置
<
/Header
>
<
Form
.
Group
inline
>
<
Form
.
Checkbox
checked
=
{
inputs
.
DrawingEnabled
===
'true'
}
label
=
"启用绘图功能"
name
=
"DrawingEnabled"
label
=
'启用绘图功能'
name
=
'DrawingEnabled'
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Checkbox
checked
=
{
inputs
.
MjNotifyEnabled
===
'true'
}
label
=
"允许回调(会泄露服务器ip地址)"
name
=
"MjNotifyEnabled"
label
=
'允许回调(会泄露服务器ip地址)'
name
=
'MjNotifyEnabled'
onChange
=
{
handleInputChange
}
/
>
<
/Form.Group
>
<
Divider
/>
<
Header
as
=
"h3"
>
屏蔽词过滤设置
<
/Header
>
<
Header
as
=
'h3'
>
屏蔽词过滤设置
<
/Header
>
<
Form
.
Group
inline
>
<
Form
.
Checkbox
checked
=
{
inputs
.
CheckSensitiveEnabled
===
'true'
}
label
=
"启用屏蔽词过滤功能"
name
=
"CheckSensitiveEnabled"
label
=
'启用屏蔽词过滤功能'
name
=
'CheckSensitiveEnabled'
onChange
=
{
handleInputChange
}
/
>
<
/Form.Group
>
<
Form
.
Group
inline
>
<
Form
.
Checkbox
checked
=
{
inputs
.
CheckSensitiveOnPromptEnabled
===
'true'
}
label
=
"启用prompt检查"
name
=
"CheckSensitiveOnPromptEnabled"
label
=
'启用prompt检查'
name
=
'CheckSensitiveOnPromptEnabled'
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Checkbox
checked
=
{
inputs
.
CheckSensitiveOnCompletionEnabled
===
'true'
}
label
=
"启用生成内容检查"
name
=
"CheckSensitiveOnCompletionEnabled"
label
=
'启用生成内容检查'
name
=
'CheckSensitiveOnCompletionEnabled'
onChange
=
{
handleInputChange
}
/
>
<
/Form.Group
>
<
Form
.
Group
inline
>
<
Form
.
Checkbox
checked
=
{
inputs
.
StopOnSensitiveEnabled
===
'true'
}
label
=
"在检测到屏蔽词时,立刻停止生成,否则替换屏蔽词"
name
=
"StopOnSensitiveEnabled"
label
=
'在检测到屏蔽词时,立刻停止生成,否则替换屏蔽词'
name
=
'StopOnSensitiveEnabled'
onChange
=
{
handleInputChange
}
/
>
<
/Form.Group
>
...
...
@@ -328,210 +356,223 @@ const OperationSetting = () => {
{
/* placeholder="例如:10"*/
}
{
/* />*/
}
{
/*</Form.Group>*/
}
<
Form
.
Group
widths
=
"equal"
>
<
Form
.
Group
widths
=
'equal'
>
<
Form
.
TextArea
label
=
"屏蔽词列表,一行一个屏蔽词,不需要符号分割"
name
=
"SensitiveWords"
label
=
'屏蔽词列表,一行一个屏蔽词,不需要符号分割'
name
=
'SensitiveWords'
onChange
=
{
handleInputChange
}
style
=
{{
minHeight
:
250
,
fontFamily
:
'JetBrains Mono, Consolas'
}}
value
=
{
inputs
.
SensitiveWords
}
placeholder
=
"一行一个屏蔽词"
placeholder
=
'一行一个屏蔽词'
/>
<
/Form.Group
>
<
Form
.
Button
onClick
=
{()
=>
{
submitConfig
(
'words'
).
then
();
}}
>
保存屏蔽词设置
<
/Form.Button
>
<
Form
.
Button
onClick
=
{()
=>
{
submitConfig
(
'words'
).
then
();
}}
>
保存屏蔽词设置
<
/Form.Button
>
<
Divider
/>
<
Header
as
=
"h3"
>
日志设置
<
/Header
>
<
Header
as
=
'h3'
>
日志设置
<
/Header
>
<
Form
.
Group
inline
>
<
Form
.
Checkbox
checked
=
{
inputs
.
LogConsumeEnabled
===
'true'
}
label
=
"启用额度消费日志记录"
name
=
"LogConsumeEnabled"
label
=
'启用额度消费日志记录'
name
=
'LogConsumeEnabled'
onChange
=
{
handleInputChange
}
/
>
<
/Form.Group
>
<
Form
.
Group
widths
=
{
4
}
>
<
Form
.
Input
label
=
"目标时间"
value
=
{
historyTimestamp
}
type
=
"datetime-local"
name
=
"history_timestamp"
onChange
=
{(
e
,
{
name
,
value
})
=>
{
setHistoryTimestamp
(
value
);
}}
/
>
<
Form
.
Input
label
=
'目标时间'
value
=
{
historyTimestamp
}
type
=
'datetime-local'
name
=
'history_timestamp'
onChange
=
{(
e
,
{
name
,
value
})
=>
{
setHistoryTimestamp
(
value
);
}}
/
>
<
/Form.Group
>
<
Form
.
Button
onClick
=
{()
=>
{
deleteHistoryLogs
().
then
();
}}
>
清理历史日志
<
/Form.Button
>
<
Form
.
Button
onClick
=
{()
=>
{
deleteHistoryLogs
().
then
();
}}
>
清理历史日志
<
/Form.Button
>
<
Divider
/>
<
Header
as
=
"h3"
>
数据看板
<
/Header
>
<
Header
as
=
'h3'
>
数据看板
<
/Header
>
<
Form
.
Checkbox
checked
=
{
inputs
.
DataExportEnabled
===
'true'
}
label
=
"启用数据看板(实验性)"
name
=
"DataExportEnabled"
label
=
'启用数据看板(实验性)'
name
=
'DataExportEnabled'
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Group
>
<
Form
.
Input
label
=
"数据看板更新间隔(分钟,设置过短会影响数据库性能)"
name
=
"DataExportInterval"
label
=
'数据看板更新间隔(分钟,设置过短会影响数据库性能)'
name
=
'DataExportInterval'
type
=
{
'number'
}
step
=
"1"
min
=
"1"
step
=
'1'
min
=
'1'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
DataExportInterval
}
placeholder
=
"数据看板更新间隔(分钟,设置过短会影响数据库性能)"
placeholder
=
'数据看板更新间隔(分钟,设置过短会影响数据库性能)'
/>
<
Form
.
Select
label
=
"数据看板默认时间粒度(仅修改展示粒度,统计精确到小时)"
label
=
'数据看板默认时间粒度(仅修改展示粒度,统计精确到小时)'
options
=
{
timeOptions
}
name
=
"DataExportDefaultTime"
name
=
'DataExportDefaultTime'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
DataExportDefaultTime
}
placeholder
=
"数据看板默认时间粒度"
placeholder
=
'数据看板默认时间粒度'
/>
<
/Form.Group
>
<
Divider
/>
<
Header
as
=
"h3"
>
监控设置
<
/Header
>
<
Header
as
=
'h3'
>
监控设置
<
/Header
>
<
Form
.
Group
widths
=
{
3
}
>
<
Form
.
Input
label
=
"最长响应时间"
name
=
"ChannelDisableThreshold"
label
=
'最长响应时间'
name
=
'ChannelDisableThreshold'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
ChannelDisableThreshold
}
type
=
"number"
min
=
"0"
placeholder
=
"单位秒,当运行通道全部测试时,超过此时间将自动禁用通道"
type
=
'number'
min
=
'0'
placeholder
=
'单位秒,当运行通道全部测试时,超过此时间将自动禁用通道'
/>
<
Form
.
Input
label
=
"额度提醒阈值"
name
=
"QuotaRemindThreshold"
label
=
'额度提醒阈值'
name
=
'QuotaRemindThreshold'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
QuotaRemindThreshold
}
type
=
"number"
min
=
"0"
placeholder
=
"低于此额度时将发送邮件提醒用户"
type
=
'number'
min
=
'0'
placeholder
=
'低于此额度时将发送邮件提醒用户'
/>
<
/Form.Group
>
<
Form
.
Group
inline
>
<
Form
.
Checkbox
checked
=
{
inputs
.
AutomaticDisableChannelEnabled
===
'true'
}
label
=
"失败时自动禁用通道"
name
=
"AutomaticDisableChannelEnabled"
label
=
'失败时自动禁用通道'
name
=
'AutomaticDisableChannelEnabled'
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Checkbox
checked
=
{
inputs
.
AutomaticEnableChannelEnabled
===
'true'
}
label
=
"成功时自动启用通道"
name
=
"AutomaticEnableChannelEnabled"
label
=
'成功时自动启用通道'
name
=
'AutomaticEnableChannelEnabled'
onChange
=
{
handleInputChange
}
/
>
<
/Form.Group
>
<
Form
.
Button
onClick
=
{()
=>
{
submitConfig
(
'monitor'
).
then
();
}}
>
保存监控设置
<
/Form.Button
>
<
Form
.
Button
onClick
=
{()
=>
{
submitConfig
(
'monitor'
).
then
();
}}
>
保存监控设置
<
/Form.Button
>
<
Divider
/>
<
Header
as
=
"h3"
>
额度设置
<
/Header
>
<
Header
as
=
'h3'
>
额度设置
<
/Header
>
<
Form
.
Group
widths
=
{
4
}
>
<
Form
.
Input
label
=
"新用户初始额度"
name
=
"QuotaForNewUser"
label
=
'新用户初始额度'
name
=
'QuotaForNewUser'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
QuotaForNewUser
}
type
=
"number"
min
=
"0"
placeholder
=
"例如:100"
type
=
'number'
min
=
'0'
placeholder
=
'例如:100'
/>
<
Form
.
Input
label
=
"请求预扣费额度"
name
=
"PreConsumedQuota"
label
=
'请求预扣费额度'
name
=
'PreConsumedQuota'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
PreConsumedQuota
}
type
=
"number"
min
=
"0"
placeholder
=
"请求结束后多退少补"
type
=
'number'
min
=
'0'
placeholder
=
'请求结束后多退少补'
/>
<
Form
.
Input
label
=
"邀请新用户奖励额度"
name
=
"QuotaForInviter"
label
=
'邀请新用户奖励额度'
name
=
'QuotaForInviter'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
QuotaForInviter
}
type
=
"number"
min
=
"0"
placeholder
=
"例如:2000"
type
=
'number'
min
=
'0'
placeholder
=
'例如:2000'
/>
<
Form
.
Input
label
=
"新用户使用邀请码奖励额度"
name
=
"QuotaForInvitee"
label
=
'新用户使用邀请码奖励额度'
name
=
'QuotaForInvitee'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
QuotaForInvitee
}
type
=
"number"
min
=
"0"
placeholder
=
"例如:1000"
type
=
'number'
min
=
'0'
placeholder
=
'例如:1000'
/>
<
/Form.Group
>
<
Form
.
Button
onClick
=
{()
=>
{
submitConfig
(
'quota'
).
then
();
}}
>
保存额度设置
<
/Form.Button
>
<
Form
.
Button
onClick
=
{()
=>
{
submitConfig
(
'quota'
).
then
();
}}
>
保存额度设置
<
/Form.Button
>
<
Divider
/>
<
Header
as
=
"h3"
>
倍率设置
<
/Header
>
<
Form
.
Group
widths
=
"equal"
>
<
Header
as
=
'h3'
>
倍率设置
<
/Header
>
<
Form
.
Group
widths
=
'equal'
>
<
Form
.
TextArea
label
=
"模型固定价格(一次调用消耗多少刀,优先级大于模型倍率)"
name
=
"ModelPrice"
label
=
'模型固定价格(一次调用消耗多少刀,优先级大于模型倍率)'
name
=
'ModelPrice'
onChange
=
{
handleInputChange
}
style
=
{{
minHeight
:
250
,
fontFamily
:
'JetBrains Mono, Consolas'
}}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
ModelPrice
}
placeholder
=
'为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀'
/>
<
/Form.Group
>
<
Form
.
Group
widths
=
"equal"
>
<
Form
.
Group
widths
=
'equal'
>
<
Form
.
TextArea
label
=
"模型倍率"
name
=
"ModelRatio"
label
=
'模型倍率'
name
=
'ModelRatio'
onChange
=
{
handleInputChange
}
style
=
{{
minHeight
:
250
,
fontFamily
:
'JetBrains Mono, Consolas'
}}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
ModelRatio
}
placeholder
=
"为一个 JSON 文本,键为模型名称,值为倍率"
placeholder
=
'为一个 JSON 文本,键为模型名称,值为倍率'
/>
<
/Form.Group
>
<
Form
.
Group
widths
=
"equal"
>
<
Form
.
Group
widths
=
'equal'
>
<
Form
.
TextArea
label
=
"分组倍率"
name
=
"GroupRatio"
label
=
'分组倍率'
name
=
'GroupRatio'
onChange
=
{
handleInputChange
}
style
=
{{
minHeight
:
250
,
fontFamily
:
'JetBrains Mono, Consolas'
}}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
GroupRatio
}
placeholder
=
"为一个 JSON 文本,键为分组名称,值为倍率"
placeholder
=
'为一个 JSON 文本,键为分组名称,值为倍率'
/>
<
/Form.Group
>
<
Form
.
Button
onClick
=
{()
=>
{
submitConfig
(
'ratio'
).
then
();
}}
>
保存倍率设置
<
/Form.Button
>
<
Form
.
Button
onClick
=
{()
=>
{
submitConfig
(
'ratio'
).
then
();
}}
>
保存倍率设置
<
/Form.Button
>
<
/Form
>
<
/Grid.Column
>
<
/Grid
>
)
;
);
};
export
default
OperationSetting
;
web/src/components/OtherSetting.js
View file @
eb955ae4
...
...
@@ -10,21 +10,20 @@ const OtherSetting = () => {
Logo
:
''
,
Footer
:
''
,
About
:
''
,
HomePageContent
:
''
HomePageContent
:
''
,
});
let
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
showUpdateModal
,
setShowUpdateModal
]
=
useState
(
false
);
const
[
updateData
,
setUpdateData
]
=
useState
({
tag_name
:
''
,
content
:
''
content
:
''
,
});
const
updateOption
=
async
(
key
,
value
)
=>
{
setLoading
(
true
);
const
res
=
await
API
.
put
(
'/api/option/'
,
{
key
,
value
value
,
});
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
...
...
@@ -41,7 +40,7 @@ const OtherSetting = () => {
Logo
:
false
,
HomePageContent
:
false
,
About
:
false
,
Footer
:
false
Footer
:
false
,
});
const
handleInputChange
=
async
(
value
,
e
)
=>
{
const
name
=
e
.
target
.
id
;
...
...
@@ -68,14 +67,20 @@ const OtherSetting = () => {
// 个性化设置 - SystemName
const
submitSystemName
=
async
()
=>
{
try
{
setLoadingInput
((
loadingInput
)
=>
({
...
loadingInput
,
SystemName
:
true
}));
setLoadingInput
((
loadingInput
)
=>
({
...
loadingInput
,
SystemName
:
true
,
}));
await
updateOption
(
'SystemName'
,
inputs
.
SystemName
);
showSuccess
(
'系统名称已更新'
);
}
catch
(
error
)
{
console
.
error
(
'系统名称更新失败'
,
error
);
showError
(
'系统名称更新失败'
);
}
finally
{
setLoadingInput
((
loadingInput
)
=>
({
...
loadingInput
,
SystemName
:
false
}));
setLoadingInput
((
loadingInput
)
=>
({
...
loadingInput
,
SystemName
:
false
,
}));
}
};
...
...
@@ -95,14 +100,20 @@ const OtherSetting = () => {
// 个性化设置 - 首页内容
const
submitOption
=
async
(
key
)
=>
{
try
{
setLoadingInput
((
loadingInput
)
=>
({
...
loadingInput
,
HomePageContent
:
true
}));
setLoadingInput
((
loadingInput
)
=>
({
...
loadingInput
,
HomePageContent
:
true
,
}));
await
updateOption
(
key
,
inputs
[
key
]);
showSuccess
(
'首页内容已更新'
);
}
catch
(
error
)
{
console
.
error
(
'首页内容更新失败'
,
error
);
showError
(
'首页内容更新失败'
);
}
finally
{
setLoadingInput
((
loadingInput
)
=>
({
...
loadingInput
,
HomePageContent
:
false
}));
setLoadingInput
((
loadingInput
)
=>
({
...
loadingInput
,
HomePageContent
:
false
,
}));
}
};
// 个性化设置 - 关于
...
...
@@ -132,15 +143,13 @@ const OtherSetting = () => {
}
};
const
openGitHubRelease
=
()
=>
{
window
.
location
=
'https://github.com/songquanpeng/one-api/releases/latest'
;
window
.
location
=
'https://github.com/songquanpeng/one-api/releases/latest'
;
};
const
checkUpdate
=
async
()
=>
{
const
res
=
await
API
.
get
(
'https://api.github.com/repos/songquanpeng/one-api/releases/latest'
'https://api.github.com/repos/songquanpeng/one-api/releases/latest'
,
);
const
{
tag_name
,
body
}
=
res
.
data
;
if
(
tag_name
===
process
.
env
.
REACT_APP_VERSION
)
{
...
...
@@ -148,7 +157,7 @@ const OtherSetting = () => {
}
else
{
setUpdateData
({
tag_name
:
tag_name
,
content
:
marked
.
parse
(
body
)
content
:
marked
.
parse
(
body
)
,
});
setShowUpdateModal
(
true
);
}
...
...
@@ -175,13 +184,15 @@ const OtherSetting = () => {
getOptions
();
},
[]);
return
(
<
Row
>
<
Col
span
=
{
24
}
>
{
/* 通用设置 */
}
<
Form
values
=
{
inputs
}
getFormApi
=
{
formAPI
=>
formAPISettingGeneral
.
current
=
formAPI
}
style
=
{{
marginBottom
:
15
}}
>
<
Form
values
=
{
inputs
}
getFormApi
=
{(
formAPI
)
=>
(
formAPISettingGeneral
.
current
=
formAPI
)}
style
=
{{
marginBottom
:
15
}}
>
<
Form
.
Section
text
=
{
'通用设置'
}
>
<
Form
.
TextArea
label
=
{
'公告'
}
...
...
@@ -191,12 +202,17 @@ const OtherSetting = () => {
style
=
{{
fontFamily
:
'JetBrains Mono, Consolas'
}}
autosize
=
{{
minRows
:
6
,
maxRows
:
12
}}
/
>
<
Button
onClick
=
{
submitNotice
}
loading
=
{
loadingInput
[
'Notice'
]}
>
设置公告
<
/Button
>
<
Button
onClick
=
{
submitNotice
}
loading
=
{
loadingInput
[
'Notice'
]}
>
设置公告
<
/Button
>
<
/Form.Section
>
<
/Form
>
{
/* 个性化设置 */
}
<
Form
values
=
{
inputs
}
getFormApi
=
{
formAPI
=>
formAPIPersonalization
.
current
=
formAPI
}
style
=
{{
marginBottom
:
15
}}
>
<
Form
values
=
{
inputs
}
getFormApi
=
{(
formAPI
)
=>
(
formAPIPersonalization
.
current
=
formAPI
)}
style
=
{{
marginBottom
:
15
}}
>
<
Form
.
Section
text
=
{
'个性化设置'
}
>
<
Form
.
Input
label
=
{
'系统名称'
}
...
...
@@ -204,48 +220,69 @@ const OtherSetting = () => {
field
=
{
'SystemName'
}
onChange
=
{
handleInputChange
}
/
>
<
Button
onClick
=
{
submitSystemName
}
loading
=
{
loadingInput
[
'SystemName'
]}
>
设置系统名称
<
/Button
>
<
Button
onClick
=
{
submitSystemName
}
loading
=
{
loadingInput
[
'SystemName'
]}
>
设置系统名称
<
/Button
>
<
Form
.
Input
label
=
{
'Logo 图片地址'
}
placeholder
=
{
'在此输入 Logo 图片地址'
}
field
=
{
'Logo'
}
onChange
=
{
handleInputChange
}
/
>
<
Button
onClick
=
{
submitLogo
}
loading
=
{
loadingInput
[
'Logo'
]}
>
设置
Logo
<
/Button
>
<
Button
onClick
=
{
submitLogo
}
loading
=
{
loadingInput
[
'Logo'
]}
>
设置
Logo
<
/Button
>
<
Form
.
TextArea
label
=
{
'首页内容'
}
placeholder
=
{
'在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'
}
placeholder
=
{
'在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'
}
field
=
{
'HomePageContent'
}
onChange
=
{
handleInputChange
}
style
=
{{
fontFamily
:
'JetBrains Mono, Consolas'
}}
autosize
=
{{
minRows
:
6
,
maxRows
:
12
}}
/
>
<
Button
onClick
=
{()
=>
submitOption
(
'HomePageContent'
)}
loading
=
{
loadingInput
[
'HomePageContent'
]}
>
设置首页内容
<
/Button
>
<
Button
onClick
=
{()
=>
submitOption
(
'HomePageContent'
)}
loading
=
{
loadingInput
[
'HomePageContent'
]}
>
设置首页内容
<
/Button
>
<
Form
.
TextArea
label
=
{
'关于'
}
placeholder
=
{
'在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'
}
placeholder
=
{
'在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'
}
field
=
{
'About'
}
onChange
=
{
handleInputChange
}
style
=
{{
fontFamily
:
'JetBrains Mono, Consolas'
}}
autosize
=
{{
minRows
:
6
,
maxRows
:
12
}}
/
>
<
Button
onClick
=
{
submitAbout
}
loading
=
{
loadingInput
[
'About'
]}
>
设置关于
<
/Button
>
<
Button
onClick
=
{
submitAbout
}
loading
=
{
loadingInput
[
'About'
]}
>
设置关于
<
/Button
>
{
/* */
}
<
Banner
fullMode
=
{
false
}
type
=
"info"
description
=
"移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。"
type
=
'info'
description
=
'移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。'
closeIcon
=
{
null
}
style
=
{{
marginTop
:
15
}}
/
>
<
Form
.
Input
label
=
{
'页脚'
}
placeholder
=
{
'在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码'
}
placeholder
=
{
'在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码'
}
field
=
{
'Footer'
}
onChange
=
{
handleInputChange
}
/
>
<
Button
onClick
=
{
submitFooter
}
loading
=
{
loadingInput
[
'Footer'
]}
>
设置页脚
<
/Button
>
<
Button
onClick
=
{
submitFooter
}
loading
=
{
loadingInput
[
'Footer'
]}
>
设置页脚
<
/Button
>
<
/Form.Section
>
<
/Form
>
<
/Col
>
...
...
web/src/components/PasswordResetConfirm.js
View file @
eb955ae4
...
...
@@ -6,7 +6,7 @@ import { useSearchParams } from 'react-router-dom';
const
PasswordResetConfirm
=
()
=>
{
const
[
inputs
,
setInputs
]
=
useState
({
email
:
''
,
token
:
''
token
:
''
,
});
const
{
email
,
token
}
=
inputs
;
...
...
@@ -23,7 +23,7 @@ const PasswordResetConfirm = () => {
let
email
=
searchParams
.
get
(
'email'
);
setInputs
({
token
,
email
email
,
});
},
[]);
...
...
@@ -46,7 +46,7 @@ const PasswordResetConfirm = () => {
setLoading
(
true
);
const
res
=
await
API
.
post
(
`/api/user/reset`
,
{
email
,
token
token
,
});
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
...
...
@@ -61,29 +61,29 @@ const PasswordResetConfirm = () => {
}
return
(
<
Grid
textAlign
=
"center"
style
=
{{
marginTop
:
'48px'
}}
>
<
Grid
textAlign
=
'center'
style
=
{{
marginTop
:
'48px'
}}
>
<
Grid
.
Column
style
=
{{
maxWidth
:
450
}}
>
<
Header
as
=
"h2"
color
=
""
textAlign
=
"center"
>
<
Image
src
=
"/logo.png"
/>
密码重置确认
<
Header
as
=
'h2'
color
=
''
textAlign
=
'center'
>
<
Image
src
=
'/logo.png'
/>
密码重置确认
<
/Header
>
<
Form
size
=
"large"
>
<
Form
size
=
'large'
>
<
Segment
>
<
Form
.
Input
fluid
icon
=
"mail"
iconPosition
=
"left"
placeholder
=
"邮箱地址"
name
=
"email"
icon
=
'mail'
iconPosition
=
'left'
placeholder
=
'邮箱地址'
name
=
'email'
value
=
{
email
}
readOnly
/>
{
newPassword
&&
(
<
Form
.
Input
fluid
icon
=
"lock"
iconPosition
=
"left"
placeholder
=
"新密码"
name
=
"newPassword"
icon
=
'lock'
iconPosition
=
'left'
placeholder
=
'新密码'
name
=
'newPassword'
value
=
{
newPassword
}
readOnly
onClick
=
{(
e
)
=>
{
...
...
@@ -94,9 +94,9 @@ const PasswordResetConfirm = () => {
/
>
)}
<
Button
color
=
"green"
color
=
'green'
fluid
size
=
"large"
size
=
'large'
onClick
=
{
handleSubmit
}
loading
=
{
loading
}
disabled
=
{
disableButton
}
...
...
web/src/components/PasswordResetForm.js
View file @
eb955ae4
...
...
@@ -5,7 +5,7 @@ import Turnstile from 'react-turnstile';
const
PasswordResetForm
=
()
=>
{
const
[
inputs
,
setInputs
]
=
useState
({
email
:
''
email
:
''
,
});
const
{
email
}
=
inputs
;
...
...
@@ -31,7 +31,7 @@ const PasswordResetForm = () => {
function
handleChange
(
e
)
{
const
{
name
,
value
}
=
e
.
target
;
setInputs
(
inputs
=>
({
...
inputs
,
[
name
]:
value
}));
setInputs
(
(
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
}
async
function
handleSubmit
(
e
)
{
...
...
@@ -43,7 +43,7 @@ const PasswordResetForm = () => {
}
setLoading
(
true
);
const
res
=
await
API
.
get
(
`/api/reset_password?email=
${
email
}
&turnstile=
${
turnstileToken
}
`
`/api/reset_password?email=
${
email
}
&turnstile=
${
turnstileToken
}
`
,
);
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
...
...
@@ -56,19 +56,19 @@ const PasswordResetForm = () => {
}
return
(
<
Grid
textAlign
=
"center"
style
=
{{
marginTop
:
'48px'
}}
>
<
Grid
textAlign
=
'center'
style
=
{{
marginTop
:
'48px'
}}
>
<
Grid
.
Column
style
=
{{
maxWidth
:
450
}}
>
<
Header
as
=
"h2"
color
=
""
textAlign
=
"center"
>
<
Image
src
=
"/logo.png"
/>
密码重置
<
Header
as
=
'h2'
color
=
''
textAlign
=
'center'
>
<
Image
src
=
'/logo.png'
/>
密码重置
<
/Header
>
<
Form
size
=
"large"
>
<
Form
size
=
'large'
>
<
Segment
>
<
Form
.
Input
fluid
icon
=
"mail"
iconPosition
=
"left"
placeholder
=
"邮箱地址"
name
=
"email"
icon
=
'mail'
iconPosition
=
'left'
placeholder
=
'邮箱地址'
name
=
'email'
value
=
{
email
}
onChange
=
{
handleChange
}
/
>
...
...
@@ -83,9 +83,9 @@ const PasswordResetForm = () => {
<><
/
>
)}
<
Button
color
=
"green"
color
=
'green'
fluid
size
=
"large"
size
=
'large'
onClick
=
{
handleSubmit
}
loading
=
{
loading
}
disabled
=
{
disableButton
}
...
...
web/src/components/PersonalSetting.js
View file @
eb955ae4
import
React
,
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
useNavigate
}
from
'react-router-dom'
;
import
{
API
,
copy
,
isRoot
,
showError
,
showInfo
,
showSuccess
}
from
'../helpers'
;
import
{
API
,
copy
,
isRoot
,
showError
,
showInfo
,
showSuccess
,
}
from
'../helpers'
;
import
Turnstile
from
'react-turnstile'
;
import
{
UserContext
}
from
'../context/User'
;
import
{
onGitHubOAuthClicked
}
from
'./utils'
;
...
...
@@ -17,9 +24,14 @@ import {
Modal
,
Space
,
Tag
,
Typography
Typography
,
}
from
'@douyinfe/semi-ui'
;
import
{
getQuotaPerUnit
,
renderQuota
,
renderQuotaWithPrompt
,
stringToColor
}
from
'../helpers/render'
;
import
{
getQuotaPerUnit
,
renderQuota
,
renderQuotaWithPrompt
,
stringToColor
,
}
from
'../helpers/render'
;
import
TelegramLoginButton
from
'react-telegram-login'
;
const
PersonalSetting
=
()
=>
{
...
...
@@ -32,7 +44,7 @@ const PersonalSetting = () => {
email
:
''
,
self_account_deletion_confirmation
:
''
,
set_new_password
:
''
,
set_new_password_confirmation
:
''
set_new_password_confirmation
:
''
,
});
const
[
status
,
setStatus
]
=
useState
({});
const
[
showChangePasswordModal
,
setShowChangePasswordModal
]
=
useState
(
false
);
...
...
@@ -67,11 +79,9 @@ const PersonalSetting = () => {
setTurnstileSiteKey
(
status
.
turnstile_site_key
);
}
}
getUserData
().
then
(
(
res
)
=>
{
console
.
log
(
userState
);
}
);
getUserData
().
then
((
res
)
=>
{
console
.
log
(
userState
);
});
loadModels
().
then
();
getAffLink
().
then
();
setTransferAmount
(
getQuotaPerUnit
());
...
...
@@ -173,7 +183,7 @@ const PersonalSetting = () => {
const
bindWeChat
=
async
()
=>
{
if
(
inputs
.
wechat_verification_code
===
''
)
return
;
const
res
=
await
API
.
get
(
`/api/oauth/wechat/bind?code=
${
inputs
.
wechat_verification_code
}
`
`/api/oauth/wechat/bind?code=
${
inputs
.
wechat_verification_code
}
`
,
);
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
...
...
@@ -189,12 +199,9 @@ const PersonalSetting = () => {
showError
(
'两次输入的密码不一致!'
);
return
;
}
const
res
=
await
API
.
put
(
`/api/user/self`
,
{
password
:
inputs
.
set_new_password
}
);
const
res
=
await
API
.
put
(
`/api/user/self`
,
{
password
:
inputs
.
set_new_password
,
});
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
showSuccess
(
'密码修改成功!'
);
...
...
@@ -210,12 +217,9 @@ const PersonalSetting = () => {
showError
(
'划转金额最低为'
+
renderQuota
(
getQuotaPerUnit
()));
return
;
}
const
res
=
await
API
.
post
(
`/api/user/aff_transfer`
,
{
quota
:
transferAmount
}
);
const
res
=
await
API
.
post
(
`/api/user/aff_transfer`
,
{
quota
:
transferAmount
,
});
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
showSuccess
(
message
);
...
...
@@ -238,7 +242,7 @@ const PersonalSetting = () => {
}
setLoading
(
true
);
const
res
=
await
API
.
get
(
`/api/verification?email=
${
inputs
.
email
}
&turnstile=
${
turnstileToken
}
`
`/api/verification?email=
${
inputs
.
email
}
&turnstile=
${
turnstileToken
}
`
,
);
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
...
...
@@ -256,7 +260,7 @@ const PersonalSetting = () => {
}
setLoading
(
true
);
const
res
=
await
API
.
get
(
`/api/oauth/email/bind?email=
${
inputs
.
email
}
&code=
${
inputs
.
email_verification_code
}
`
`/api/oauth/email/bind?email=
${
inputs
.
email
}
&code=
${
inputs
.
email_verification_code
}
`
,
);
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
...
...
@@ -295,7 +299,7 @@ const PersonalSetting = () => {
<
Layout
>
<
Layout
.
Content
>
<
Modal
title
=
"请输入要划转的数量"
title
=
'请输入要划转的数量'
visible
=
{
openTransfer
}
onOk
=
{
transfer
}
onCancel
=
{
handleCancel
}
...
...
@@ -305,13 +309,25 @@ const PersonalSetting = () => {
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Typography
.
Text
>
{
`可用额度
${
renderQuotaWithPrompt
(
userState
?.
user
?.
aff_quota
)}
`}</Typography.Text>
<Input style={{ marginTop: 5 }} value={userState?.user?.aff_quota} disabled={true}></Input>
<Input
style={{ marginTop: 5 }}
value={userState?.user?.aff_quota}
disabled={true}
></Input>
</div>
<div style={{ marginTop: 20 }}>
<Typography.Text>{`
划转额度
$
{
renderQuotaWithPrompt
(
transferAmount
)}
最低
` + renderQuota(getQuotaPerUnit())}</Typography.Text>
<Typography.Text>
{`
划转额度
$
{
renderQuotaWithPrompt
(
transferAmount
)}
最低
` +
renderQuota(getQuotaPerUnit())}
</Typography.Text>
<div>
<InputNumber min={0} style={{ marginTop: 5 }} value={transferAmount}
onChange={(value) => setTransferAmount(value)} disabled={false}></InputNumber>
<InputNumber
min={0}
style={{ marginTop: 5 }}
value={transferAmount}
onChange={(value) => setTransferAmount(value)}
disabled={false}
></InputNumber>
</div>
</div>
</Modal>
...
...
@@ -319,27 +335,45 @@ const PersonalSetting = () => {
<Card
title={
<Card.Meta
avatar={<Avatar size="default" color={stringToColor(getUsername())}
style={{ marginRight: 4 }}>
{typeof getUsername() === 'string' && getUsername().slice(0, 1)}
</Avatar>}
avatar={
<Avatar
size='default'
color={stringToColor(getUsername())}
style={{ marginRight: 4 }}
>
{typeof getUsername() === 'string' &&
getUsername().slice(0, 1)}
</Avatar>
}
title={<Typography.Text>{getUsername()}</Typography.Text>}
description={isRoot() ? <Tag color="red">管理员</Tag> : <Tag color="blue">普通用户</Tag>}
description={
isRoot() ? (
<Tag color='red'>管理员</Tag>
) : (
<Tag color='blue'>普通用户</Tag>
)
}
></Card.Meta>
}
headerExtraContent={
<>
<Space vertical align=
"start"
>
<Tag color=
"green"
>{'ID: ' + userState?.user?.id}</Tag>
<Tag color=
"blue"
>{userState?.user?.group}</Tag>
<Space vertical align=
'start'
>
<Tag color=
'green'
>{'ID: ' + userState?.user?.id}</Tag>
<Tag color=
'blue'
>{userState?.user?.group}</Tag>
</Space>
</>
}
footer={
<Descriptions row>
<Descriptions.Item itemKey="当前余额">{renderQuota(userState?.user?.quota)}</Descriptions.Item>
<Descriptions.Item itemKey="历史消耗">{renderQuota(userState?.user?.used_quota)}</Descriptions.Item>
<Descriptions.Item itemKey="请求次数">{userState.user?.request_count}</Descriptions.Item>
<Descriptions.Item itemKey='当前余额'>
{renderQuota(userState?.user?.quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='历史消耗'>
{renderQuota(userState?.user?.used_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='请求次数'>
{userState.user?.request_count}
</Descriptions.Item>
</Descriptions>
}
>
...
...
@@ -347,15 +381,18 @@ const PersonalSetting = () => {
<div style={{ marginTop: 10 }}>
<Space wrap>
{models.map((model) => (
<Tag key={model} color="cyan" onClick={() => {
copyText(model);
}}>
<Tag
key={model}
color='cyan'
onClick={() => {
copyText(model);
}}
>
{model}
</Tag>
))}
</Space>
</div>
</Card>
<Card
footer={
...
...
@@ -373,18 +410,25 @@ const PersonalSetting = () => {
<Typography.Title heading={6}>邀请信息</Typography.Title>
<div style={{ marginTop: 10 }}>
<Descriptions row>
<Descriptions.Item itemKey="待使用收益">
<span style={{ color: 'rgba(var(--semi-red-5), 1)' }}>
{
renderQuota(userState?.user?.aff_quota)
}
</span>
<Button type={'secondary'} onClick={() => setOpenTransfer(true)} size={'small'}
style={{ marginLeft: 10 }}>划转</Button>
<Descriptions.Item itemKey='待使用收益'>
<span style={{ color: 'rgba(var(--semi-red-5), 1)' }}>
{renderQuota(userState?.user?.aff_quota)}
</span>
<Button
type={'secondary'}
onClick={() => setOpenTransfer(true)}
size={'small'}
style={{ marginLeft: 10 }}
>
划转
</Button>
</Descriptions.Item>
<Descriptions.Item itemKey='总收益'>
{renderQuota(userState?.user?.aff_history_quota)}
</Descriptions.Item>
<Descriptions.Item itemKey='邀请人数'>
{userState?.user?.aff_count}
</Descriptions.Item>
<Descriptions.Item
itemKey="总收益">{renderQuota(userState?.user?.aff_history_quota)}</Descriptions.Item>
<Descriptions.Item itemKey="邀请人数">{userState?.user?.aff_count}</Descriptions.Item>
</Descriptions>
</div>
</Card>
...
...
@@ -392,46 +436,71 @@ const PersonalSetting = () => {
<Typography.Title heading={6}>个人信息</Typography.Title>
<div style={{ marginTop: 20 }}>
<Typography.Text strong>邮箱</Typography.Text>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between' }}
>
<div>
<Input
value={userState.user && userState.user.email !== '' ? userState.user.email : '未绑定'}
value={
userState.user && userState.user.email !== ''
? userState.user.email
: '未绑定'
}
readonly={true}
></Input>
</div>
<div>
<Button onClick={() => {
setShowEmailBindModal(true);
}}>{
userState.user && userState.user.email !== '' ? '修改绑定' : '绑定邮箱'
}</Button>
<Button
onClick={() => {
setShowEmailBindModal(true);
}}
>
{userState.user && userState.user.email !== ''
? '修改绑定'
: '绑定邮箱'}
</Button>
</div>
</div>
</div>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>微信</Typography.Text>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between' }}
>
<div>
<Input
value={userState.user && userState.user.wechat_id !== '' ? '已绑定' : '未绑定'}
value={
userState.user && userState.user.wechat_id !== ''
? '已绑定'
: '未绑定'
}
readonly={true}
></Input>
</div>
<div>
<Button disabled={(userState.user && userState.user.wechat_id !== '') || !status.wechat_login}>
{
status.wechat_login ? '绑定' : '未启用'
<Button
disabled={
(userState.user && userState.user.wechat_id !== '') ||
!status.wechat_login
}
>
{status.wechat_login ? '绑定' : '未启用'}
</Button>
</div>
</div>
</div>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>GitHub</Typography.Text>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between' }}
>
<div>
<Input
value={userState.user && userState.user.github_id !== '' ? userState.user.github_id : '未绑定'}
value={
userState.user && userState.user.github_id !== ''
? userState.user.github_id
: '未绑定'
}
readonly={true}
></Input>
</div>
...
...
@@ -440,11 +509,12 @@ const PersonalSetting = () => {
onClick={() => {
onGitHubOAuthClicked(status.github_client_id);
}}
disabled={(userState.user && userState.user.github_id !== '') || !status.github_oauth}
>
{
status.github_oauth ? '绑定' : '未启用'
disabled={
(userState.user && userState.user.github_id !== '') ||
!status.github_oauth
}
>
{status.github_oauth ? '绑定' : '未启用'}
</Button>
</div>
</div>
...
...
@@ -452,33 +522,56 @@ const PersonalSetting = () => {
<div style={{ marginTop: 10 }}>
<Typography.Text strong>Telegram</Typography.Text>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between' }}
>
<div>
<Input
value={userState.user && userState.user.telegram_id !== '' ? userState.user.telegram_id : '未绑定'}
value={
userState.user && userState.user.telegram_id !== ''
? userState.user.telegram_id
: '未绑定'
}
readonly={true}
></Input>
</div>
<div>
{status.telegram_oauth ?
userState.user.telegram_id !== '' ? <Button disabled={true}>已绑定</Button>
: <TelegramLoginButton dataAuthUrl="/api/oauth/telegram/bind"
botName={status.telegram_bot_name} />
: <Button disabled={true}>未启用</Button>
}
{status.telegram_oauth ? (
userState.user.telegram_id !== '' ? (
<Button disabled={true}>已绑定</Button>
) : (
<TelegramLoginButton
dataAuthUrl='/api/oauth/telegram/bind'
botName={status.telegram_bot_name}
/>
)
) : (
<Button disabled={true}>未启用</Button>
)}
</div>
</div>
</div>
<div style={{ marginTop: 10 }}>
<Space>
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
<Button onClick={() => {
setShowChangePasswordModal(true);
}}>修改密码</Button>
<Button type={'danger'} onClick={() => {
setShowAccountDeleteModal(true);
}}>删除个人账户</Button>
<Button onClick={generateAccessToken}>
生成系统访问令牌
</Button>
<Button
onClick={() => {
setShowChangePasswordModal(true);
}}
>
修改密码
</Button>
<Button
type={'danger'}
onClick={() => {
setShowAccountDeleteModal(true);
}}
>
删除个人账户
</Button>
</Space>
{systemToken && (
...
...
@@ -489,17 +582,15 @@ const PersonalSetting = () => {
style={{ marginTop: '10px' }}
/>
)}
{
status.wechat_login && (
<Button
onClick={() => {
setShowWeChatBindModal(true);
}}
>
绑定微信账号
</Button>
)
}
{status.wechat_login && (
<Button
onClick={() => {
setShowWeChatBindModal(true);
}}
>
绑定微信账号
</Button>
)}
<Modal
onCancel={() => setShowWeChatBindModal(false)}
// onOpen={() => setShowWeChatBindModal(true)}
...
...
@@ -513,12 +604,14 @@ const PersonalSetting = () => {
</p>
</div>
<Input
placeholder=
"验证码"
name=
"wechat_verification_code"
placeholder=
'验证码'
name=
'wechat_verification_code'
value={inputs.wechat_verification_code}
onChange={(v) => handleInputChange('wechat_verification_code', v)}
onChange={(v) =>
handleInputChange('wechat_verification_code', v)
}
/>
<Button color=
"" fluid size="large"
onClick={bindWeChat}>
<Button color=
'' fluid size='large'
onClick={bindWeChat}>
绑定
</Button>
</Modal>
...
...
@@ -534,26 +627,36 @@ const PersonalSetting = () => {
maskClosable={false}
>
<Typography.Title heading={6}>绑定邮箱地址</Typography.Title>
<div style={{ marginTop: 20, display: 'flex', justifyContent: 'space-between' }}>
<div
style={{
marginTop: 20,
display: 'flex',
justifyContent: 'space-between',
}}
>
<Input
fluid
placeholder=
"输入邮箱地址"
placeholder=
'输入邮箱地址'
onChange={(value) => handleInputChange('email', value)}
name=
"email"
type=
"email"
name=
'email'
type=
'email'
/>
<Button onClick={sendVerificationCode}
disabled={disableButton || loading}>
<Button
onClick={sendVerificationCode}
disabled={disableButton || loading}
>
{disableButton ? `
重新发送
(
$
{
countdown
})
` : '获取验证码'}
</Button>
</div>
<div style={{ marginTop: 10 }}>
<Input
fluid
placeholder=
"验证码"
name=
"email_verification_code"
placeholder=
'验证码'
name=
'email_verification_code'
value={inputs.email_verification_code}
onChange={(value) => handleInputChange('email_verification_code', value)}
onChange={(value) =>
handleInputChange('email_verification_code', value)
}
/>
</div>
{turnstileEnabled ? (
...
...
@@ -576,17 +679,22 @@ const PersonalSetting = () => {
>
<div style={{ marginTop: 20 }}>
<Banner
type=
"danger"
description=
"您正在删除自己的帐户,将清空所有数据且不可恢复"
type=
'danger'
description=
'您正在删除自己的帐户,将清空所有数据且不可恢复'
closeIcon={null}
/>
</div>
<div style={{ marginTop: 20 }}>
<Input
placeholder={`
输入你的账户名
$
{
userState
?.
user
?.
username
}
以确认删除
`}
name=
"self_account_deletion_confirmation"
name=
'self_account_deletion_confirmation'
value={inputs.self_account_deletion_confirmation}
onChange={(value) => handleInputChange('self_account_deletion_confirmation', value)}
onChange={(value) =>
handleInputChange(
'self_account_deletion_confirmation',
value,
)
}
/>
{turnstileEnabled ? (
<Turnstile
...
...
@@ -609,17 +717,21 @@ const PersonalSetting = () => {
>
<div style={{ marginTop: 20 }}>
<Input
name=
"set_new_password"
placeholder=
"新密码"
name=
'set_new_password'
placeholder=
'新密码'
value={inputs.set_new_password}
onChange={(value) => handleInputChange('set_new_password', value)}
onChange={(value) =>
handleInputChange('set_new_password', value)
}
/>
<Input
style={{ marginTop: 20 }}
name=
"set_new_password_confirmation"
placeholder=
"确认新密码"
name=
'set_new_password_confirmation'
placeholder=
'确认新密码'
value={inputs.set_new_password_confirmation}
onChange={(value) => handleInputChange('set_new_password_confirmation', value)}
onChange={(value) =>
handleInputChange('set_new_password_confirmation', value)
}
/>
{turnstileEnabled ? (
<Turnstile
...
...
@@ -634,7 +746,6 @@ const PersonalSetting = () => {
</div>
</Modal>
</div>
</Layout.Content>
</Layout>
</div>
...
...
web/src/components/PrivateRoute.js
View file @
eb955ae4
...
...
@@ -2,12 +2,11 @@ import { Navigate } from 'react-router-dom';
import
{
history
}
from
'../helpers'
;
function
PrivateRoute
({
children
})
{
if
(
!
localStorage
.
getItem
(
'user'
))
{
return
<
Navigate
to
=
"/login"
state
=
{{
from
:
history
.
location
}}
/>
;
return
<
Navigate
to
=
'/login'
state
=
{{
from
:
history
.
location
}}
/>
;
}
return
children
;
}
export
{
PrivateRoute
};
\ No newline at end of file
export
{
PrivateRoute
};
web/src/components/RedemptionsTable.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
API
,
copy
,
showError
,
showSuccess
,
timestamp2string
}
from
'../helpers'
;
import
{
API
,
copy
,
showError
,
showSuccess
,
timestamp2string
,
}
from
'../helpers'
;
import
{
ITEMS_PER_PAGE
}
from
'../constants'
;
import
{
renderQuota
}
from
'../helpers/render'
;
import
{
Button
,
Form
,
Modal
,
Popconfirm
,
Popover
,
Table
,
Tag
}
from
'@douyinfe/semi-ui'
;
import
{
Button
,
Form
,
Modal
,
Popconfirm
,
Popover
,
Table
,
Tag
,
}
from
'@douyinfe/semi-ui'
;
import
EditRedemption
from
'../pages/Redemption/EditRedemption'
;
function
renderTimestamp
(
timestamp
)
{
return
(
<>
{
timestamp2string
(
timestamp
)}
<
/
>
);
return
<>
{
timestamp2string
(
timestamp
)}
<
/>
;
}
function
renderStatus
(
status
)
{
switch
(
status
)
{
case
1
:
return
<
Tag
color
=
"green"
size
=
"large"
>
未使用
<
/Tag>
;
return
(
<
Tag
color
=
'green'
size
=
'large'
>
未使用
<
/Tag
>
);
case
2
:
return
<
Tag
color
=
"red"
size
=
"large"
>
已禁用
<
/Tag>
;
return
(
<
Tag
color
=
'red'
size
=
'large'
>
{
' '
}
已禁用
{
' '
}
<
/Tag
>
);
case
3
:
return
<
Tag
color
=
"grey"
size
=
"large"
>
已使用
<
/Tag>
;
return
(
<
Tag
color
=
'grey'
size
=
'large'
>
{
' '
}
已使用
{
' '
}
<
/Tag
>
);
default
:
return
<
Tag
color
=
"black"
size
=
"large"
>
未知状态
<
/Tag>
;
return
(
<
Tag
color
=
'black'
size
=
'large'
>
{
' '
}
未知状态
{
' '
}
<
/Tag
>
);
}
}
...
...
@@ -31,121 +60,115 @@ const RedemptionsTable = () => {
const
columns
=
[
{
title
:
'ID'
,
dataIndex
:
'id'
dataIndex
:
'id'
,
},
{
title
:
'名称'
,
dataIndex
:
'name'
dataIndex
:
'name'
,
},
{
title
:
'状态'
,
dataIndex
:
'status'
,
key
:
'status'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderStatus
(
text
)}
<
/div
>
);
}
return
<
div
>
{
renderStatus
(
text
)}
<
/div>
;
},
},
{
title
:
'额度'
,
dataIndex
:
'quota'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderQuota
(
parseInt
(
text
))}
<
/div
>
);
}
return
<
div
>
{
renderQuota
(
parseInt
(
text
))}
<
/div>
;
},
},
{
title
:
'创建时间'
,
dataIndex
:
'created_time'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderTimestamp
(
text
)}
<
/div
>
);
}
return
<
div
>
{
renderTimestamp
(
text
)}
<
/div>
;
},
},
{
title
:
'兑换人ID'
,
dataIndex
:
'used_user_id'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
text
===
0
?
'无'
:
text
}
<
/div
>
);
}
return
<
div
>
{
text
===
0
?
'无'
:
text
}
<
/div>
;
},
},
{
title
:
''
,
dataIndex
:
'operate'
,
render
:
(
text
,
record
,
index
)
=>
(
<
div
>
<
Popover
content
=
{
record
.
key
}
style
=
{{
padding
:
20
}}
position
=
"top"
>
<
Button
theme
=
"light"
type
=
"tertiary"
style
=
{{
marginRight
:
1
}}
>
查看
<
/Button
>
<
Popover
content
=
{
record
.
key
}
style
=
{{
padding
:
20
}}
position
=
'top'
>
<
Button
theme
=
'light'
type
=
'tertiary'
style
=
{{
marginRight
:
1
}}
>
查看
<
/Button
>
<
/Popover
>
<
Button
theme
=
"light"
type
=
"secondary"
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
(
text
)
=>
{
await
copyText
(
record
.
key
);
}}
>
复制
<
/Button
>
<
Button
theme
=
'light'
type
=
'secondary'
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
(
text
)
=>
{
await
copyText
(
record
.
key
);
}}
>
复制
<
/Button
>
<
Popconfirm
title
=
"确定是否要删除此兑换码?"
content
=
"此修改将不可逆"
title
=
'确定是否要删除此兑换码?'
content
=
'此修改将不可逆'
okType
=
{
'danger'
}
position
=
{
'left'
}
onConfirm
=
{()
=>
{
manageRedemption
(
record
.
id
,
'delete'
,
record
).
then
(
()
=>
{
removeRecord
(
record
.
key
);
}
);
manageRedemption
(
record
.
id
,
'delete'
,
record
).
then
(()
=>
{
removeRecord
(
record
.
key
);
});
}}
>
<
Button
theme
=
"light"
type
=
"danger"
style
=
{{
marginRight
:
1
}}
>
删除
<
/Button
>
<
Button
theme
=
'light'
type
=
'danger'
style
=
{{
marginRight
:
1
}}
>
删除
<
/Button
>
<
/Popconfirm
>
{
record
.
status
===
1
?
<
Button
theme
=
"light"
type
=
"warning"
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageRedemption
(
record
.
id
,
'disable'
,
record
);
}
}
>
禁用
<
/Button>
:
<
Button
theme
=
"light"
type
=
"secondary"
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageRedemption
(
record
.
id
,
'enable'
,
record
);
}
}
disabled
=
{
record
.
status
===
3
}
>
启用
<
/Button
>
}
<
Button
theme
=
"light"
type
=
"tertiary"
style
=
{{
marginRight
:
1
}}
onClick
=
{
()
=>
{
{
record
.
status
===
1
?
(
<
Button
theme
=
'light'
type
=
'warning'
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageRedemption
(
record
.
id
,
'disable'
,
record
);
}}
>
禁用
<
/Button
>
)
:
(
<
Button
theme
=
'light'
type
=
'secondary'
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageRedemption
(
record
.
id
,
'enable'
,
record
);
}}
disabled
=
{
record
.
status
===
3
}
>
启用
<
/Button
>
)}
<
Button
theme
=
'light'
type
=
'tertiary'
style
=
{{
marginRight
:
1
}}
onClick
=
{()
=>
{
setEditingRedemption
(
record
);
setShowEdit
(
true
);
}
}
disabled
=
{
record
.
status
!==
1
}
>
编辑
<
/Button
>
}}
disabled
=
{
record
.
status
!==
1
}
>
编辑
<
/Button
>
<
/div
>
)
}
)
,
}
,
];
const
[
redemptions
,
setRedemptions
]
=
useState
([]);
...
...
@@ -156,7 +179,7 @@ const RedemptionsTable = () => {
const
[
tokenCount
,
setTokenCount
]
=
useState
(
ITEMS_PER_PAGE
);
const
[
selectedKeys
,
setSelectedKeys
]
=
useState
([]);
const
[
editingRedemption
,
setEditingRedemption
]
=
useState
({
id
:
undefined
id
:
undefined
,
});
const
[
showEdit
,
setShowEdit
]
=
useState
(
false
);
...
...
@@ -178,7 +201,7 @@ const RedemptionsTable = () => {
// }
// data.key = '' + data.id
setRedemptions
(
redeptions
);
if
(
redeptions
.
length
>=
(
activePage
)
*
ITEMS_PER_PAGE
)
{
if
(
redeptions
.
length
>=
activePage
*
ITEMS_PER_PAGE
)
{
setTokenCount
(
redeptions
.
length
+
1
);
}
else
{
setTokenCount
(
redeptions
.
length
);
...
...
@@ -202,10 +225,10 @@ const RedemptionsTable = () => {
setLoading
(
false
);
};
const
removeRecord
=
key
=>
{
const
removeRecord
=
(
key
)
=>
{
let
newDataSource
=
[...
redemptions
];
if
(
key
!=
null
)
{
let
idx
=
newDataSource
.
findIndex
(
data
=>
data
.
key
===
key
);
let
idx
=
newDataSource
.
findIndex
(
(
data
)
=>
data
.
key
===
key
);
if
(
idx
>
-
1
)
{
newDataSource
.
splice
(
idx
,
1
);
...
...
@@ -268,7 +291,6 @@ const RedemptionsTable = () => {
let
newRedemptions
=
[...
redemptions
];
// let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
if
(
action
===
'delete'
)
{
}
else
{
record
.
status
=
redemption
.
status
;
}
...
...
@@ -286,7 +308,9 @@ const RedemptionsTable = () => {
return
;
}
setSearching
(
true
);
const
res
=
await
API
.
get
(
`/api/redemption/search?keyword=
${
searchKeyword
}
`
);
const
res
=
await
API
.
get
(
`/api/redemption/search?keyword=
${
searchKeyword
}
`
,
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
setRedemptions
(
data
);
...
...
@@ -315,32 +339,32 @@ const RedemptionsTable = () => {
setLoading
(
false
);
};
const
handlePageChange
=
page
=>
{
const
handlePageChange
=
(
page
)
=>
{
setActivePage
(
page
);
if
(
page
===
Math
.
ceil
(
redemptions
.
length
/
ITEMS_PER_PAGE
)
+
1
)
{
// In this case we have to load more data and then append them.
loadRedemptions
(
page
-
1
).
then
(
r
=>
{
});
loadRedemptions
(
page
-
1
).
then
((
r
)
=>
{});
}
};
let
pageData
=
redemptions
.
slice
((
activePage
-
1
)
*
ITEMS_PER_PAGE
,
activePage
*
ITEMS_PER_PAGE
);
let
pageData
=
redemptions
.
slice
(
(
activePage
-
1
)
*
ITEMS_PER_PAGE
,
activePage
*
ITEMS_PER_PAGE
,
);
const
rowSelection
=
{
onSelect
:
(
record
,
selected
)
=>
{
},
onSelectAll
:
(
selected
,
selectedRows
)
=>
{
},
onSelect
:
(
record
,
selected
)
=>
{},
onSelectAll
:
(
selected
,
selectedRows
)
=>
{},
onChange
:
(
selectedRowKeys
,
selectedRows
)
=>
{
setSelectedKeys
(
selectedRows
);
}
}
,
};
const
handleRow
=
(
record
,
index
)
=>
{
if
(
record
.
status
!==
1
)
{
return
{
style
:
{
background
:
'var(--semi-color-disabled-border)'
}
background
:
'var(--semi-color-disabled-border)'
,
}
,
};
}
else
{
return
{};
...
...
@@ -349,45 +373,64 @@ const RedemptionsTable = () => {
return
(
<>
<
EditRedemption
refresh
=
{
refresh
}
editingRedemption
=
{
editingRedemption
}
visiable
=
{
showEdit
}
handleClose
=
{
closeEdit
}
><
/EditRedemption
>
<
EditRedemption
refresh
=
{
refresh
}
editingRedemption
=
{
editingRedemption
}
visiable
=
{
showEdit
}
handleClose
=
{
closeEdit
}
><
/EditRedemption
>
<
Form
onSubmit
=
{
searchRedemptions
}
>
<
Form
.
Input
label
=
"搜索关键字"
field
=
"keyword"
icon
=
"search"
iconPosition
=
"left"
placeholder
=
"关键字(id或者名称)"
label
=
'搜索关键字'
field
=
'keyword'
icon
=
'search'
iconPosition
=
'left'
placeholder
=
'关键字(id或者名称)'
value
=
{
searchKeyword
}
loading
=
{
searching
}
onChange
=
{
handleKeywordChange
}
/
>
<
/Form
>
<
Table
style
=
{{
marginTop
:
20
}}
columns
=
{
columns
}
dataSource
=
{
pageData
}
pagination
=
{{
currentPage
:
activePage
,
pageSize
:
ITEMS_PER_PAGE
,
total
:
tokenCount
,
// showSizeChanger: true,
// pageSizeOptions: [10, 20, 50, 100],
formatPageText
:
(
page
)
=>
`第
${
page
.
currentStart
}
-
${
page
.
currentEnd
}
条,共
${
redemptions
.
length
}
条`
,
// onPageSizeChange: (size) => {
// setPageSize(size);
// setActivePage(1);
// },
onPageChange
:
handlePageChange
}}
loading
=
{
loading
}
rowSelection
=
{
rowSelection
}
onRow
=
{
handleRow
}
>
<
/Table
>
<
Button
theme
=
"light"
type
=
"primary"
style
=
{{
marginRight
:
8
}}
onClick
=
{
()
=>
{
<
Table
style
=
{{
marginTop
:
20
}}
columns
=
{
columns
}
dataSource
=
{
pageData
}
pagination
=
{{
currentPage
:
activePage
,
pageSize
:
ITEMS_PER_PAGE
,
total
:
tokenCount
,
// showSizeChanger: true,
// pageSizeOptions: [10, 20, 50, 100],
formatPageText
:
(
page
)
=>
`第
${
page
.
currentStart
}
-
${
page
.
currentEnd
}
条,共
${
redemptions
.
length
}
条`
,
// onPageSizeChange: (size) => {
// setPageSize(size);
// setActivePage(1);
// },
onPageChange
:
handlePageChange
,
}}
loading
=
{
loading
}
rowSelection
=
{
rowSelection
}
onRow
=
{
handleRow
}
><
/Table
>
<
Button
theme
=
'light'
type
=
'primary'
style
=
{{
marginRight
:
8
}}
onClick
=
{()
=>
{
setEditingRedemption
({
id
:
undefined
id
:
undefined
,
});
setShowEdit
(
true
);
}
}
>
添加兑换码
<
/Button
>
<
Button
label
=
"复制所选兑换码"
type
=
"warning"
onClick
=
{
async
()
=>
{
}}
>
添加兑换码
<
/Button
>
<
Button
label
=
'复制所选兑换码'
type
=
'warning'
onClick
=
{
async
()
=>
{
if
(
selectedKeys
.
length
===
0
)
{
showError
(
'请至少选择一个兑换码!'
);
return
;
...
...
@@ -397,8 +440,10 @@ const RedemptionsTable = () => {
keys
+=
selectedKeys
[
i
].
name
+
' '
+
selectedKeys
[
i
].
key
+
'\n'
;
}
await
copyText
(
keys
);
}
}
>
复制所选兑换码到剪贴板
<
/Button
>
}}
>
复制所选兑换码到剪贴板
<
/Button
>
<
/
>
);
};
...
...
web/src/components/RegisterForm.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
Button
,
Form
,
Grid
,
Header
,
Image
,
Message
,
Segment
}
from
'semantic-ui-react'
;
import
{
Button
,
Form
,
Grid
,
Header
,
Image
,
Message
,
Segment
,
}
from
'semantic-ui-react'
;
import
{
Link
,
useNavigate
}
from
'react-router-dom'
;
import
{
API
,
getLogo
,
showError
,
showInfo
,
showSuccess
}
from
'../helpers'
;
import
Turnstile
from
'react-turnstile'
;
...
...
@@ -10,7 +18,7 @@ const RegisterForm = () => {
password
:
''
,
password2
:
''
,
email
:
''
,
verification_code
:
''
verification_code
:
''
,
});
const
{
username
,
password
,
password2
}
=
inputs
;
const
[
showEmailVerification
,
setShowEmailVerification
]
=
useState
(
false
);
...
...
@@ -65,7 +73,7 @@ const RegisterForm = () => {
inputs
.
aff_code
=
affCode
;
const
res
=
await
API
.
post
(
`/api/user/register?turnstile=
${
turnstileToken
}
`
,
inputs
inputs
,
);
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
...
...
@@ -86,7 +94,7 @@ const RegisterForm = () => {
}
setLoading
(
true
);
const
res
=
await
API
.
get
(
`/api/verification?email=
${
inputs
.
email
}
&turnstile=
${
turnstileToken
}
`
`/api/verification?email=
${
inputs
.
email
}
&turnstile=
${
turnstileToken
}
`
,
);
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
...
...
@@ -98,49 +106,49 @@ const RegisterForm = () => {
};
return
(
<
Grid
textAlign
=
"center"
style
=
{{
marginTop
:
'48px'
}}
>
<
Grid
textAlign
=
'center'
style
=
{{
marginTop
:
'48px'
}}
>
<
Grid
.
Column
style
=
{{
maxWidth
:
450
}}
>
<
Header
as
=
"h2"
color
=
""
textAlign
=
"center"
>
<
Header
as
=
'h2'
color
=
''
textAlign
=
'center'
>
<
Image
src
=
{
logo
}
/> 新用户注
册
<
/Header
>
<
Form
size
=
"large"
>
<
Form
size
=
'large'
>
<
Segment
>
<
Form
.
Input
fluid
icon
=
"user"
iconPosition
=
"left"
placeholder
=
"输入用户名,最长 12 位"
icon
=
'user'
iconPosition
=
'left'
placeholder
=
'输入用户名,最长 12 位'
onChange
=
{
handleChange
}
name
=
"username"
name
=
'username'
/>
<
Form
.
Input
fluid
icon
=
"lock"
iconPosition
=
"left"
placeholder
=
"输入密码,最短 8 位,最长 20 位"
icon
=
'lock'
iconPosition
=
'left'
placeholder
=
'输入密码,最短 8 位,最长 20 位'
onChange
=
{
handleChange
}
name
=
"password"
type
=
"password"
name
=
'password'
type
=
'password'
/>
<
Form
.
Input
fluid
icon
=
"lock"
iconPosition
=
"left"
placeholder
=
"输入密码,最短 8 位,最长 20 位"
icon
=
'lock'
iconPosition
=
'left'
placeholder
=
'输入密码,最短 8 位,最长 20 位'
onChange
=
{
handleChange
}
name
=
"password2"
type
=
"password"
name
=
'password2'
type
=
'password'
/>
{
showEmailVerification
?
(
<>
<
Form
.
Input
fluid
icon
=
"mail"
iconPosition
=
"left"
placeholder
=
"输入邮箱地址"
icon
=
'mail'
iconPosition
=
'left'
placeholder
=
'输入邮箱地址'
onChange
=
{
handleChange
}
name
=
"email"
type
=
"email"
name
=
'email'
type
=
'email'
action
=
{
<
Button
onClick
=
{
sendVerificationCode
}
disabled
=
{
loading
}
>
获取验证码
...
...
@@ -149,11 +157,11 @@ const RegisterForm = () => {
/
>
<
Form
.
Input
fluid
icon
=
"lock"
iconPosition
=
"left"
placeholder
=
"输入验证码"
icon
=
'lock'
iconPosition
=
'left'
placeholder
=
'输入验证码'
onChange
=
{
handleChange
}
name
=
"verification_code"
name
=
'verification_code'
/>
<
/
>
)
:
(
...
...
@@ -170,9 +178,9 @@ const RegisterForm = () => {
<><
/
>
)}
<
Button
color
=
"green"
color
=
'green'
fluid
size
=
"large"
size
=
'large'
onClick
=
{
handleSubmit
}
loading
=
{
loading
}
>
...
...
@@ -182,7 +190,7 @@ const RegisterForm = () => {
<
/Form
>
<
Message
>
已有账户?
<
Link
to
=
"/login"
className
=
"btn btn-link"
>
<
Link
to
=
'/login'
className
=
'btn btn-link'
>
点击登录
<
/Link
>
<
/Message
>
...
...
web/src/components/SiderBar.js
View file @
eb955ae4
...
...
@@ -3,7 +3,14 @@ import { Link, useNavigate } from 'react-router-dom';
import
{
UserContext
}
from
'../context/User'
;
import
{
StatusContext
}
from
'../context/Status'
;
import
{
API
,
getLogo
,
getSystemName
,
isAdmin
,
isMobile
,
showError
}
from
'../helpers'
;
import
{
API
,
getLogo
,
getSystemName
,
isAdmin
,
isMobile
,
showError
,
}
from
'../helpers'
;
import
'../index.css'
;
import
{
...
...
@@ -17,7 +24,7 @@ import {
IconKey
,
IconLayers
,
IconSetting
,
IconUser
IconUser
,
}
from
'@douyinfe/semi-icons'
;
import
{
Layout
,
Nav
}
from
'@douyinfe/semi-ui'
;
...
...
@@ -26,7 +33,8 @@ import { Layout, Nav } from '@douyinfe/semi-ui';
const
SiderBar
=
()
=>
{
const
[
userState
,
userDispatch
]
=
useContext
(
UserContext
);
const
[
statusState
,
statusDispatch
]
=
useContext
(
StatusContext
);
const
defaultIsCollapsed
=
isMobile
()
||
localStorage
.
getItem
(
'default_collapse_sidebar'
)
===
'true'
;
const
defaultIsCollapsed
=
isMobile
()
||
localStorage
.
getItem
(
'default_collapse_sidebar'
)
===
'true'
;
let
navigate
=
useNavigate
();
const
[
selectedKeys
,
setSelectedKeys
]
=
useState
([
'home'
]);
...
...
@@ -46,89 +54,105 @@ const SiderBar = () => {
setting
:
'/setting'
,
about
:
'/about'
,
chat
:
'/chat'
,
detail
:
'/detail'
detail
:
'/detail'
,
};
const
headerButtons
=
useMemo
(()
=>
[
{
text
:
'首页'
,
itemKey
:
'home'
,
to
:
'/'
,
icon
:
<
IconHome
/>
},
{
text
:
'渠道'
,
itemKey
:
'channel'
,
to
:
'/channel'
,
icon
:
<
IconLayers
/>
,
className
:
isAdmin
()
?
'semi-navigation-item-normal'
:
'tableHiddle'
},
{
text
:
'聊天'
,
itemKey
:
'chat'
,
to
:
'/chat'
,
icon
:
<
IconComment
/>
,
className
:
localStorage
.
getItem
(
'chat_link'
)
?
'semi-navigation-item-normal'
:
'tableHiddle'
},
{
text
:
'令牌'
,
itemKey
:
'token'
,
to
:
'/token'
,
icon
:
<
IconKey
/>
},
{
text
:
'兑换码'
,
itemKey
:
'redemption'
,
to
:
'/redemption'
,
icon
:
<
IconGift
/>
,
className
:
isAdmin
()
?
'semi-navigation-item-normal'
:
'tableHiddle'
},
{
text
:
'钱包'
,
itemKey
:
'topup'
,
to
:
'/topup'
,
icon
:
<
IconCreditCard
/>
},
{
text
:
'用户管理'
,
itemKey
:
'user'
,
to
:
'/user'
,
icon
:
<
IconUser
/>
,
className
:
isAdmin
()
?
'semi-navigation-item-normal'
:
'tableHiddle'
},
{
text
:
'日志'
,
itemKey
:
'log'
,
to
:
'/log'
,
icon
:
<
IconHistogram
/>
},
{
text
:
'数据看板'
,
itemKey
:
'detail'
,
to
:
'/detail'
,
icon
:
<
IconCalendarClock
/>
,
className
:
localStorage
.
getItem
(
'enable_data_export'
)
===
'true'
?
'semi-navigation-item-normal'
:
'tableHiddle'
},
{
text
:
'绘图'
,
itemKey
:
'midjourney'
,
to
:
'/midjourney'
,
icon
:
<
IconImage
/>
,
className
:
localStorage
.
getItem
(
'enable_drawing'
)
===
'true'
?
'semi-navigation-item-normal'
:
'tableHiddle'
},
{
text
:
'设置'
,
itemKey
:
'setting'
,
to
:
'/setting'
,
icon
:
<
IconSetting
/>
}
// {
// text: '关于',
// itemKey: 'about',
// to: '/about',
// icon: <IconAt/>
// }
],
[
localStorage
.
getItem
(
'enable_data_export'
),
localStorage
.
getItem
(
'enable_drawing'
),
localStorage
.
getItem
(
'chat_link'
),
isAdmin
()]);
const
headerButtons
=
useMemo
(
()
=>
[
{
text
:
'首页'
,
itemKey
:
'home'
,
to
:
'/'
,
icon
:
<
IconHome
/>
,
},
{
text
:
'渠道'
,
itemKey
:
'channel'
,
to
:
'/channel'
,
icon
:
<
IconLayers
/>
,
className
:
isAdmin
()
?
'semi-navigation-item-normal'
:
'tableHiddle'
,
},
{
text
:
'聊天'
,
itemKey
:
'chat'
,
to
:
'/chat'
,
icon
:
<
IconComment
/>
,
className
:
localStorage
.
getItem
(
'chat_link'
)
?
'semi-navigation-item-normal'
:
'tableHiddle'
,
},
{
text
:
'令牌'
,
itemKey
:
'token'
,
to
:
'/token'
,
icon
:
<
IconKey
/>
,
},
{
text
:
'兑换码'
,
itemKey
:
'redemption'
,
to
:
'/redemption'
,
icon
:
<
IconGift
/>
,
className
:
isAdmin
()
?
'semi-navigation-item-normal'
:
'tableHiddle'
,
},
{
text
:
'钱包'
,
itemKey
:
'topup'
,
to
:
'/topup'
,
icon
:
<
IconCreditCard
/>
,
},
{
text
:
'用户管理'
,
itemKey
:
'user'
,
to
:
'/user'
,
icon
:
<
IconUser
/>
,
className
:
isAdmin
()
?
'semi-navigation-item-normal'
:
'tableHiddle'
,
},
{
text
:
'日志'
,
itemKey
:
'log'
,
to
:
'/log'
,
icon
:
<
IconHistogram
/>
,
},
{
text
:
'数据看板'
,
itemKey
:
'detail'
,
to
:
'/detail'
,
icon
:
<
IconCalendarClock
/>
,
className
:
localStorage
.
getItem
(
'enable_data_export'
)
===
'true'
?
'semi-navigation-item-normal'
:
'tableHiddle'
,
},
{
text
:
'绘图'
,
itemKey
:
'midjourney'
,
to
:
'/midjourney'
,
icon
:
<
IconImage
/>
,
className
:
localStorage
.
getItem
(
'enable_drawing'
)
===
'true'
?
'semi-navigation-item-normal'
:
'tableHiddle'
,
},
{
text
:
'设置'
,
itemKey
:
'setting'
,
to
:
'/setting'
,
icon
:
<
IconSetting
/>
,
},
// {
// text: '关于',
// itemKey: 'about',
// to: '/about',
// icon: <IconAt/>
// }
],
[
localStorage
.
getItem
(
'enable_data_export'
),
localStorage
.
getItem
(
'enable_drawing'
),
localStorage
.
getItem
(
'chat_link'
),
isAdmin
(),
],
);
const
loadStatus
=
async
()
=>
{
const
res
=
await
API
.
get
(
'/api/status'
);
...
...
@@ -143,8 +167,14 @@ const SiderBar = () => {
localStorage
.
setItem
(
'display_in_currency'
,
data
.
display_in_currency
);
localStorage
.
setItem
(
'enable_drawing'
,
data
.
enable_drawing
);
localStorage
.
setItem
(
'enable_data_export'
,
data
.
enable_data_export
);
localStorage
.
setItem
(
'data_export_default_time'
,
data
.
data_export_default_time
);
localStorage
.
setItem
(
'default_collapse_sidebar'
,
data
.
default_collapse_sidebar
);
localStorage
.
setItem
(
'data_export_default_time'
,
data
.
data_export_default_time
,
);
localStorage
.
setItem
(
'default_collapse_sidebar'
,
data
.
default_collapse_sidebar
,
);
localStorage
.
setItem
(
'mj_notify_enabled'
,
data
.
mj_notify_enabled
);
if
(
data
.
chat_link
)
{
localStorage
.
setItem
(
'chat_link'
,
data
.
chat_link
);
...
...
@@ -163,11 +193,14 @@ const SiderBar = () => {
useEffect
(()
=>
{
loadStatus
().
then
(()
=>
{
setIsCollapsed
(
isMobile
()
||
localStorage
.
getItem
(
'default_collapse_sidebar'
)
===
'true'
);
setIsCollapsed
(
isMobile
()
||
localStorage
.
getItem
(
'default_collapse_sidebar'
)
===
'true'
,
);
});
let
localKey
=
window
.
location
.
pathname
.
split
(
'/'
)[
1
]
let
localKey
=
window
.
location
.
pathname
.
split
(
'/'
)[
1
]
;
if
(
localKey
===
''
)
{
localKey
=
'home'
localKey
=
'home'
;
}
setSelectedKeys
([
localKey
]);
},
[]);
...
...
@@ -179,9 +212,12 @@ const SiderBar = () => {
<
Nav
// bodyStyle={{ maxWidth: 200 }}
style
=
{{
maxWidth
:
200
}}
defaultIsCollapsed
=
{
isMobile
()
||
localStorage
.
getItem
(
'default_collapse_sidebar'
)
===
'true'
}
defaultIsCollapsed
=
{
isMobile
()
||
localStorage
.
getItem
(
'default_collapse_sidebar'
)
===
'true'
}
isCollapsed
=
{
isCollapsed
}
onCollapseChange
=
{
collapsed
=>
{
onCollapseChange
=
{
(
collapsed
)
=>
{
setIsCollapsed
(
collapsed
);
}}
selectedKeys
=
{
selectedKeys
}
...
...
@@ -196,20 +232,20 @@ const SiderBar = () => {
);
}}
items
=
{
headerButtons
}
onSelect
=
{
key
=>
{
onSelect
=
{
(
key
)
=>
{
setSelectedKeys
([
key
.
itemKey
]);
}}
header
=
{{
logo
:
<
img
src
=
{
logo
}
alt
=
"logo"
style
=
{{
marginRight
:
'0.75em'
}}
/>
,
text
:
systemName
logo
:
(
<
img
src
=
{
logo
}
alt
=
'logo'
style
=
{{
marginRight
:
'0.75em'
}}
/
>
),
text
:
systemName
,
}}
// footer={{
// text: '© 2021 NekoAPI',
// }}
>
<
Nav
.
Footer
collapseButton
=
{
true
}
>
<
/Nav.Footer
>
<
Nav
.
Footer
collapseButton
=
{
true
}
><
/Nav.Footer
>
<
/Nav
>
<
/div
>
<
/Layout
>
...
...
web/src/components/SystemSetting.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
Button
,
Divider
,
Form
,
Grid
,
Header
,
Message
,
Modal
}
from
'semantic-ui-react'
;
import
{
Button
,
Divider
,
Form
,
Grid
,
Header
,
Message
,
Modal
,
}
from
'semantic-ui-react'
;
import
{
API
,
removeTrailingSlash
,
showError
,
verifyJSON
}
from
'../helpers'
;
const
SystemSetting
=
()
=>
{
...
...
@@ -38,13 +46,14 @@ const SystemSetting = () => {
// telegram login
TelegramOAuthEnabled
:
''
,
TelegramBotToken
:
''
,
TelegramBotName
:
''
TelegramBotName
:
''
,
});
const
[
originInputs
,
setOriginInputs
]
=
useState
({});
let
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
EmailDomainWhitelist
,
setEmailDomainWhitelist
]
=
useState
([]);
const
[
restrictedDomainInput
,
setRestrictedDomainInput
]
=
useState
(
''
);
const
[
showPasswordWarningModal
,
setShowPasswordWarningModal
]
=
useState
(
false
);
const
[
showPasswordWarningModal
,
setShowPasswordWarningModal
]
=
useState
(
false
);
const
getOptions
=
async
()
=>
{
const
res
=
await
API
.
get
(
'/api/option/'
);
...
...
@@ -59,13 +68,15 @@ const SystemSetting = () => {
});
setInputs
({
...
newInputs
,
EmailDomainWhitelist
:
newInputs
.
EmailDomainWhitelist
.
split
(
','
)
EmailDomainWhitelist
:
newInputs
.
EmailDomainWhitelist
.
split
(
','
)
,
});
setOriginInputs
(
newInputs
);
setEmailDomainWhitelist
(
newInputs
.
EmailDomainWhitelist
.
split
(
','
).
map
((
item
)
=>
{
return
{
key
:
item
,
text
:
item
,
value
:
item
};
}));
setEmailDomainWhitelist
(
newInputs
.
EmailDomainWhitelist
.
split
(
','
).
map
((
item
)
=>
{
return
{
key
:
item
,
text
:
item
,
value
:
item
};
}),
);
}
else
{
showError
(
message
);
}
...
...
@@ -94,7 +105,7 @@ const SystemSetting = () => {
}
const
res
=
await
API
.
put
(
'/api/option/'
,
{
key
,
value
value
,
});
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
...
...
@@ -105,7 +116,8 @@ const SystemSetting = () => {
value
=
parseFloat
(
value
);
}
setInputs
((
inputs
)
=>
({
...
inputs
,
[
key
]:
value
...
inputs
,
[
key
]:
value
,
}));
}
else
{
showError
(
message
);
...
...
@@ -197,13 +209,16 @@ const SystemSetting = () => {
}
};
const
submitEmailDomainWhitelist
=
async
()
=>
{
if
(
originInputs
[
'EmailDomainWhitelist'
]
!==
inputs
.
EmailDomainWhitelist
.
join
(
','
)
&&
originInputs
[
'EmailDomainWhitelist'
]
!==
inputs
.
EmailDomainWhitelist
.
join
(
','
)
&&
inputs
.
SMTPToken
!==
''
)
{
await
updateOption
(
'EmailDomainWhitelist'
,
inputs
.
EmailDomainWhitelist
.
join
(
','
));
await
updateOption
(
'EmailDomainWhitelist'
,
inputs
.
EmailDomainWhitelist
.
join
(
','
),
);
}
};
...
...
@@ -211,7 +226,7 @@ const SystemSetting = () => {
if
(
originInputs
[
'WeChatServerAddress'
]
!==
inputs
.
WeChatServerAddress
)
{
await
updateOption
(
'WeChatServerAddress'
,
removeTrailingSlash
(
inputs
.
WeChatServerAddress
)
removeTrailingSlash
(
inputs
.
WeChatServerAddress
)
,
);
}
if
(
...
...
@@ -220,7 +235,7 @@ const SystemSetting = () => {
)
{
await
updateOption
(
'WeChatAccountQRCodeImageURL'
,
inputs
.
WeChatAccountQRCodeImageURL
inputs
.
WeChatAccountQRCodeImageURL
,
);
}
if
(
...
...
@@ -263,17 +278,23 @@ const SystemSetting = () => {
const
submitNewRestrictedDomain
=
()
=>
{
const
localDomainList
=
inputs
.
EmailDomainWhitelist
;
if
(
restrictedDomainInput
!==
''
&&
!
localDomainList
.
includes
(
restrictedDomainInput
))
{
if
(
restrictedDomainInput
!==
''
&&
!
localDomainList
.
includes
(
restrictedDomainInput
)
)
{
setRestrictedDomainInput
(
''
);
setInputs
({
...
inputs
,
EmailDomainWhitelist
:
[...
localDomainList
,
restrictedDomainInput
]
EmailDomainWhitelist
:
[...
localDomainList
,
restrictedDomainInput
]
,
});
setEmailDomainWhitelist
([...
EmailDomainWhitelist
,
{
key
:
restrictedDomainInput
,
text
:
restrictedDomainInput
,
value
:
restrictedDomainInput
}]);
setEmailDomainWhitelist
([
...
EmailDomainWhitelist
,
{
key
:
restrictedDomainInput
,
text
:
restrictedDomainInput
,
value
:
restrictedDomainInput
,
},
]);
}
};
...
...
@@ -281,13 +302,13 @@ const SystemSetting = () => {
<
Grid
columns
=
{
1
}
>
<
Grid
.
Column
>
<
Form
loading
=
{
loading
}
>
<
Header
as
=
"h3"
>
通用设置
<
/Header
>
<
Form
.
Group
widths
=
"equal"
>
<
Header
as
=
'h3'
>
通用设置
<
/Header
>
<
Form
.
Group
widths
=
'equal'
>
<
Form
.
Input
label
=
"服务器地址"
placeholder
=
"例如:https://yourdomain.com"
label
=
'服务器地址'
placeholder
=
'例如:https://yourdomain.com'
value
=
{
inputs
.
ServerAddress
}
name
=
"ServerAddress"
name
=
'ServerAddress'
onChange
=
{
handleInputChange
}
/
>
<
/Form.Group
>
...
...
@@ -295,81 +316,79 @@ const SystemSetting = () => {
更新服务器地址
<
/Form.Button
>
<
Divider
/>
<
Header
as
=
"h3"
>
支付设置(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
<
/Header
>
<
Form
.
Group
widths
=
"equal"
>
<
Header
as
=
'h3'
>
支付设置(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
<
/Header
>
<
Form
.
Group
widths
=
'equal'
>
<
Form
.
Input
label
=
"支付地址,不填写则不启用在线支付"
placeholder
=
"例如:https://yourdomain.com"
label
=
'支付地址,不填写则不启用在线支付'
placeholder
=
'例如:https://yourdomain.com'
value
=
{
inputs
.
PayAddress
}
name
=
"PayAddress"
name
=
'PayAddress'
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Input
label
=
"易支付商户ID"
placeholder
=
"例如:0001"
label
=
'易支付商户ID'
placeholder
=
'例如:0001'
value
=
{
inputs
.
EpayId
}
name
=
"EpayId"
name
=
'EpayId'
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Input
label
=
"易支付商户密钥"
placeholder
=
"例如:dejhfueqhujasjmndbjkqaw"
label
=
'易支付商户密钥'
placeholder
=
'例如:dejhfueqhujasjmndbjkqaw'
value
=
{
inputs
.
EpayKey
}
name
=
"EpayKey"
name
=
'EpayKey'
onChange
=
{
handleInputChange
}
/
>
<
/Form.Group
>
<
Form
.
Group
widths
=
"equal"
>
<
Form
.
Group
widths
=
'equal'
>
<
Form
.
Input
label
=
"回调地址,不填写则使用上方服务器地址作为回调地址"
placeholder
=
"例如:https://yourdomain.com"
label
=
'回调地址,不填写则使用上方服务器地址作为回调地址'
placeholder
=
'例如:https://yourdomain.com'
value
=
{
inputs
.
CustomCallbackAddress
}
name
=
"CustomCallbackAddress"
name
=
'CustomCallbackAddress'
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Input
label
=
"充值价格(x元/美金)"
placeholder
=
"例如:7,就是7元/美金"
label
=
'充值价格(x元/美金)'
placeholder
=
'例如:7,就是7元/美金'
value
=
{
inputs
.
Price
}
name
=
"Price"
name
=
'Price'
min
=
{
0
}
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Input
label
=
"最低充值数量"
placeholder
=
"例如:2,就是最低充值2$"
label
=
'最低充值数量'
placeholder
=
'例如:2,就是最低充值2$'
value
=
{
inputs
.
MinTopUp
}
name
=
"MinTopUp"
name
=
'MinTopUp'
min
=
{
1
}
onChange
=
{
handleInputChange
}
/
>
<
/Form.Group
>
<
Form
.
Group
widths
=
"equal"
>
<
Form
.
Group
widths
=
'equal'
>
<
Form
.
TextArea
label
=
"充值分组倍率"
name
=
"TopupGroupRatio"
label
=
'充值分组倍率'
name
=
'TopupGroupRatio'
onChange
=
{
handleInputChange
}
style
=
{{
minHeight
:
250
,
fontFamily
:
'JetBrains Mono, Consolas'
}}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
TopupGroupRatio
}
placeholder
=
"为一个 JSON 文本,键为组名称,值为倍率"
placeholder
=
'为一个 JSON 文本,键为组名称,值为倍率'
/>
<
/Form.Group
>
<
Form
.
Button
onClick
=
{
submitPayAddress
}
>
更新支付设置
<
/Form.Button
>
<
Form
.
Button
onClick
=
{
submitPayAddress
}
>
更新支付设置
<
/Form.Button
>
<
Divider
/>
<
Header
as
=
"h3"
>
配置登录注册
<
/Header
>
<
Header
as
=
'h3'
>
配置登录注册
<
/Header
>
<
Form
.
Group
inline
>
<
Form
.
Checkbox
checked
=
{
inputs
.
PasswordLoginEnabled
===
'true'
}
label
=
"允许通过密码进行登录"
name
=
"PasswordLoginEnabled"
label
=
'允许通过密码进行登录'
name
=
'PasswordLoginEnabled'
onChange
=
{
handleInputChange
}
/
>
{
showPasswordWarningModal
&&
{
showPasswordWarningModal
&&
(
<
Modal
open
=
{
showPasswordWarningModal
}
onClose
=
{()
=>
setShowPasswordWarningModal
(
false
)}
...
...
@@ -378,12 +397,16 @@ const SystemSetting = () => {
>
<
Modal
.
Header
>
警告
<
/Modal.Header
>
<
Modal
.
Content
>
<
p
>
取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?
<
/p
>
<
p
>
取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?
<
/p
>
<
/Modal.Content
>
<
Modal
.
Actions
>
<
Button
onClick
=
{()
=>
setShowPasswordWarningModal
(
false
)}
>
取消
<
/Button
>
<
Button
onClick
=
{()
=>
setShowPasswordWarningModal
(
false
)}
>
取消
<
/Button
>
<
Button
color
=
"yellow"
color
=
'yellow'
onClick
=
{
async
()
=>
{
setShowPasswordWarningModal
(
false
);
await
updateOption
(
'PasswordLoginEnabled'
,
'false'
);
...
...
@@ -393,157 +416,170 @@ const SystemSetting = () => {
<
/Button
>
<
/Modal.Actions
>
<
/Modal
>
}
)
}
<
Form
.
Checkbox
checked
=
{
inputs
.
PasswordRegisterEnabled
===
'true'
}
label
=
"允许通过密码进行注册"
name
=
"PasswordRegisterEnabled"
label
=
'允许通过密码进行注册'
name
=
'PasswordRegisterEnabled'
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Checkbox
checked
=
{
inputs
.
EmailVerificationEnabled
===
'true'
}
label
=
"通过密码注册时需要进行邮箱验证"
name
=
"EmailVerificationEnabled"
label
=
'通过密码注册时需要进行邮箱验证'
name
=
'EmailVerificationEnabled'
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Checkbox
checked
=
{
inputs
.
GitHubOAuthEnabled
===
'true'
}
label
=
"允许通过 GitHub 账户登录 & 注册"
name
=
"GitHubOAuthEnabled"
label
=
'允许通过 GitHub 账户登录 & 注册'
name
=
'GitHubOAuthEnabled'
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Checkbox
checked
=
{
inputs
.
WeChatAuthEnabled
===
'true'
}
label
=
"允许通过微信登录 & 注册"
name
=
"WeChatAuthEnabled"
label
=
'允许通过微信登录 & 注册'
name
=
'WeChatAuthEnabled'
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Checkbox
checked
=
{
inputs
.
TelegramOAuthEnabled
===
'true'
}
label
=
"允许通过 Telegram 进行登录"
name
=
"TelegramOAuthEnabled"
label
=
'允许通过 Telegram 进行登录'
name
=
'TelegramOAuthEnabled'
onChange
=
{
handleInputChange
}
/
>
<
/Form.Group
>
<
Form
.
Group
inline
>
<
Form
.
Checkbox
checked
=
{
inputs
.
RegisterEnabled
===
'true'
}
label
=
"允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)"
name
=
"RegisterEnabled"
label
=
'允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)'
name
=
'RegisterEnabled'
onChange
=
{
handleInputChange
}
/
>
<
Form
.
Checkbox
checked
=
{
inputs
.
TurnstileCheckEnabled
===
'true'
}
label
=
"启用 Turnstile 用户校验"
name
=
"TurnstileCheckEnabled"
label
=
'启用 Turnstile 用户校验'
name
=
'TurnstileCheckEnabled'
onChange
=
{
handleInputChange
}
/
>
<
/Form.Group
>
<
Divider
/>
<
Header
as
=
"h3"
>
<
Header
as
=
'h3'
>
配置邮箱域名白名单
<
Header
.
Subheader
>
用以防止恶意用户利用临时邮箱批量注册
<
/Header.Subheader
>
<
Header
.
Subheader
>
用以防止恶意用户利用临时邮箱批量注册
<
/Header.Subheader
>
<
/Header
>
<
Form
.
Group
widths
=
{
3
}
>
<
Form
.
Checkbox
label
=
"启用邮箱域名白名单"
name
=
"EmailDomainRestrictionEnabled"
label
=
'启用邮箱域名白名单'
name
=
'EmailDomainRestrictionEnabled'
onChange
=
{
handleInputChange
}
checked
=
{
inputs
.
EmailDomainRestrictionEnabled
===
'true'
}
/
>
<
/Form.Group
>
<
Form
.
Group
widths
=
{
2
}
>
<
Form
.
Dropdown
label
=
"允许的邮箱域名"
placeholder
=
"允许的邮箱域名"
name
=
"EmailDomainWhitelist"
label
=
'允许的邮箱域名'
placeholder
=
'允许的邮箱域名'
name
=
'EmailDomainWhitelist'
required
fluid
multiple
selection
onChange
=
{
handleInputChange
}
value
=
{
inputs
.
EmailDomainWhitelist
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
options
=
{
EmailDomainWhitelist
}
/
>
<
Form
.
Input
label
=
"添加新的允许的邮箱域名"
label
=
'添加新的允许的邮箱域名'
action
=
{
<
Button
type
=
"button"
onClick
=
{()
=>
{
submitNewRestrictedDomain
();
}}
>
填入
<
/Button
>
<
Button
type
=
'button'
onClick
=
{()
=>
{
submitNewRestrictedDomain
();
}}
>
填入
<
/Button
>
}
onKeyDown
=
{(
e
)
=>
{
if
(
e
.
key
===
'Enter'
)
{
submitNewRestrictedDomain
();
}
}}
autoComplete
=
"new-password"
placeholder
=
"输入新的允许的邮箱域名"
autoComplete
=
'new-password'
placeholder
=
'输入新的允许的邮箱域名'
value
=
{
restrictedDomainInput
}
onChange
=
{(
e
,
{
value
})
=>
{
setRestrictedDomainInput
(
value
);
}}
/
>
<
/Form.Group
>
<
Form
.
Button
onClick
=
{
submitEmailDomainWhitelist
}
>
保存邮箱域名白名单设置
<
/Form.Button
>
<
Form
.
Button
onClick
=
{
submitEmailDomainWhitelist
}
>
保存邮箱域名白名单设置
<
/Form.Button
>
<
Divider
/>
<
Header
as
=
"h3"
>
<
Header
as
=
'h3'
>
配置
SMTP
<
Header
.
Subheader
>
用以支持系统的邮件发送
<
/Header.Subheader
>
<
/Header
>
<
Form
.
Group
widths
=
{
3
}
>
<
Form
.
Input
label
=
"SMTP 服务器地址"
name
=
"SMTPServer"
label
=
'SMTP 服务器地址'
name
=
'SMTPServer'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
SMTPServer
}
placeholder
=
"例如:smtp.qq.com"
placeholder
=
'例如:smtp.qq.com'
/>
<
Form
.
Input
label
=
"SMTP 端口"
name
=
"SMTPPort"
label
=
'SMTP 端口'
name
=
'SMTPPort'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
SMTPPort
}
placeholder
=
"默认: 587"
placeholder
=
'默认: 587'
/>
<
Form
.
Input
label
=
"SMTP 账户"
name
=
"SMTPAccount"
label
=
'SMTP 账户'
name
=
'SMTPAccount'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
SMTPAccount
}
placeholder
=
"通常是邮箱地址"
placeholder
=
'通常是邮箱地址'
/>
<
/Form.Group
>
<
Form
.
Group
widths
=
{
3
}
>
<
Form
.
Input
label
=
"SMTP 发送者邮箱"
name
=
"SMTPFrom"
label
=
'SMTP 发送者邮箱'
name
=
'SMTPFrom'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
SMTPFrom
}
placeholder
=
"通常和邮箱地址保持一致"
placeholder
=
'通常和邮箱地址保持一致'
/>
<
Form
.
Input
label
=
"SMTP 访问凭证"
name
=
"SMTPToken"
label
=
'SMTP 访问凭证'
name
=
'SMTPToken'
onChange
=
{
handleInputChange
}
type
=
"password"
autoComplete
=
"new-password"
type
=
'password'
autoComplete
=
'new-password'
checked
=
{
inputs
.
RegisterEnabled
===
'true'
}
placeholder
=
"敏感信息不会发送到前端显示"
placeholder
=
'敏感信息不会发送到前端显示'
/>
<
/Form.Group
>
<
Form
.
Button
onClick
=
{
submitSMTP
}
>
保存
SMTP
设置
<
/Form.Button
>
<
Divider
/>
<
Header
as
=
"h3"
>
<
Header
as
=
'h3'
>
配置
GitHub
OAuth
App
<
Header
.
Subheader
>
用以支持通过
GitHub
进行登录注册,
<
a
href
=
"https://github.com/settings/developers"
target
=
"_blank"
rel
=
"noreferrer"
>
<
a
href
=
'https://github.com/settings/developers'
target
=
'_blank'
rel
=
'noreferrer'
>
点击此处
<
/a
>
管理你的
GitHub
OAuth
App
...
...
@@ -556,34 +592,35 @@ const SystemSetting = () => {
<
/Message
>
<
Form
.
Group
widths
=
{
3
}
>
<
Form
.
Input
label
=
"GitHub Client ID"
name
=
"GitHubClientId"
label
=
'GitHub Client ID'
name
=
'GitHubClientId'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
GitHubClientId
}
placeholder
=
"输入你注册的 GitHub OAuth APP 的 ID"
placeholder
=
'输入你注册的 GitHub OAuth APP 的 ID'
/>
<
Form
.
Input
label
=
"GitHub Client Secret"
name
=
"GitHubClientSecret"
label
=
'GitHub Client Secret'
name
=
'GitHubClientSecret'
onChange
=
{
handleInputChange
}
type
=
"password"
autoComplete
=
"new-password"
type
=
'password'
autoComplete
=
'new-password'
value
=
{
inputs
.
GitHubClientSecret
}
placeholder
=
"敏感信息不会发送到前端显示"
placeholder
=
'敏感信息不会发送到前端显示'
/>
<
/Form.Group
>
<
Form
.
Button
onClick
=
{
submitGitHubOAuth
}
>
保存
GitHub
OAuth
设置
<
/Form.Button
>
<
Divider
/>
<
Header
as
=
"h3"
>
<
Header
as
=
'h3'
>
配置
WeChat
Server
<
Header
.
Subheader
>
用以支持通过微信进行登录注册,
<
a
href
=
"https://github.com/songquanpeng/wechat-server"
target
=
"_blank"
rel
=
"noreferrer"
href
=
'https://github.com/songquanpeng/wechat-server'
target
=
'_blank'
rel
=
'noreferrer'
>
点击此处
<
/a
>
...
...
@@ -592,61 +629,65 @@ const SystemSetting = () => {
<
/Header
>
<
Form
.
Group
widths
=
{
3
}
>
<
Form
.
Input
label
=
"WeChat Server 服务器地址"
name
=
"WeChatServerAddress"
placeholder
=
"例如:https://yourdomain.com"
label
=
'WeChat Server 服务器地址'
name
=
'WeChatServerAddress'
placeholder
=
'例如:https://yourdomain.com'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
WeChatServerAddress
}
/
>
<
Form
.
Input
label
=
"WeChat Server 访问凭证"
name
=
"WeChatServerToken"
type
=
"password"
label
=
'WeChat Server 访问凭证'
name
=
'WeChatServerToken'
type
=
'password'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
WeChatServerToken
}
placeholder
=
"敏感信息不会发送到前端显示"
placeholder
=
'敏感信息不会发送到前端显示'
/>
<
Form
.
Input
label
=
"微信公众号二维码图片链接"
name
=
"WeChatAccountQRCodeImageURL"
label
=
'微信公众号二维码图片链接'
name
=
'WeChatAccountQRCodeImageURL'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
WeChatAccountQRCodeImageURL
}
placeholder
=
"输入一个图片链接"
placeholder
=
'输入一个图片链接'
/>
<
/Form.Group
>
<
Form
.
Button
onClick
=
{
submitWeChat
}
>
保存
WeChat
Server
设置
<
/Form.Button
>
<
Divider
/>
<
Header
as
=
"h3"
>
配置
Telegram
登录
<
/Header
>
<
Header
as
=
'h3'
>
配置
Telegram
登录
<
/Header
>
<
Form
.
Group
inline
>
<
Form
.
Input
label
=
"Telegram Bot Token"
name
=
"TelegramBotToken"
label
=
'Telegram Bot Token'
name
=
'TelegramBotToken'
onChange
=
{
handleInputChange
}
value
=
{
inputs
.
TelegramBotToken
}
placeholder
=
"输入你的 Telegram Bot Token"
placeholder
=
'输入你的 Telegram Bot Token'
/>
<
Form
.
Input
label
=
"Telegram Bot 名称"
name
=
"TelegramBotName"
label
=
'Telegram Bot 名称'
name
=
'TelegramBotName'
onChange
=
{
handleInputChange
}
value
=
{
inputs
.
TelegramBotName
}
placeholder
=
"输入你的 Telegram Bot 名称"
placeholder
=
'输入你的 Telegram Bot 名称'
/>
<
/Form.Group
>
<
Form
.
Button
onClick
=
{
submitTelegramSettings
}
>
保存
Telegram
登录设置
<
/Form.Button
>
<
Divider
/>
<
Header
as
=
"h3"
>
<
Header
as
=
'h3'
>
配置
Turnstile
<
Header
.
Subheader
>
用以支持用户校验,
<
a
href
=
"https://dash.cloudflare.com/"
target
=
"_blank"
rel
=
"noreferrer"
>
<
a
href
=
'https://dash.cloudflare.com/'
target
=
'_blank'
rel
=
'noreferrer'
>
点击此处
<
/a
>
管理你的
Turnstile
Sites
,推荐选择
Invisible
Widget
Type
...
...
@@ -654,21 +695,21 @@ const SystemSetting = () => {
<
/Header
>
<
Form
.
Group
widths
=
{
3
}
>
<
Form
.
Input
label
=
"Turnstile Site Key"
name
=
"TurnstileSiteKey"
label
=
'Turnstile Site Key'
name
=
'TurnstileSiteKey'
onChange
=
{
handleInputChange
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
value
=
{
inputs
.
TurnstileSiteKey
}
placeholder
=
"输入你注册的 Turnstile Site Key"
placeholder
=
'输入你注册的 Turnstile Site Key'
/>
<
Form
.
Input
label
=
"Turnstile Secret Key"
name
=
"TurnstileSecretKey"
label
=
'Turnstile Secret Key'
name
=
'TurnstileSecretKey'
onChange
=
{
handleInputChange
}
type
=
"password"
autoComplete
=
"new-password"
type
=
'password'
autoComplete
=
'new-password'
value
=
{
inputs
.
TurnstileSecretKey
}
placeholder
=
"敏感信息不会发送到前端显示"
placeholder
=
'敏感信息不会发送到前端显示'
/>
<
/Form.Group
>
<
Form
.
Button
onClick
=
{
submitTurnstile
}
>
...
...
web/src/components/TokensTable.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
API
,
copy
,
showError
,
showSuccess
,
timestamp2string
}
from
'../helpers'
;
import
{
API
,
copy
,
showError
,
showSuccess
,
timestamp2string
,
}
from
'../helpers'
;
import
{
ITEMS_PER_PAGE
}
from
'../constants'
;
import
{
renderQuota
}
from
'../helpers/render'
;
import
{
Button
,
Dropdown
,
Form
,
Modal
,
Popconfirm
,
Popover
,
SplitButtonGroup
,
Table
,
Tag
}
from
'@douyinfe/semi-ui'
;
import
{
Button
,
Dropdown
,
Form
,
Modal
,
Popconfirm
,
Popover
,
SplitButtonGroup
,
Table
,
Tag
,
}
from
'@douyinfe/semi-ui'
;
import
{
IconTreeTriangleDown
}
from
'@douyinfe/semi-icons'
;
import
EditToken
from
'../pages/Token/EditToken'
;
...
...
@@ -11,85 +27,107 @@ import EditToken from '../pages/Token/EditToken';
const
COPY_OPTIONS
=
[
{
key
:
'next'
,
text
:
'ChatGPT Next Web'
,
value
:
'next'
},
{
key
:
'ama'
,
text
:
'ChatGPT Web & Midjourney'
,
value
:
'ama'
},
{
key
:
'opencat'
,
text
:
'OpenCat'
,
value
:
'opencat'
}
{
key
:
'opencat'
,
text
:
'OpenCat'
,
value
:
'opencat'
}
,
];
const
OPEN_LINK_OPTIONS
=
[
{
key
:
'ama'
,
text
:
'ChatGPT Web & Midjourney'
,
value
:
'ama'
},
{
key
:
'opencat'
,
text
:
'OpenCat'
,
value
:
'opencat'
}
{
key
:
'opencat'
,
text
:
'OpenCat'
,
value
:
'opencat'
}
,
];
function
renderTimestamp
(
timestamp
)
{
return
(
<>
{
timestamp2string
(
timestamp
)}
<
/
>
);
return
<>
{
timestamp2string
(
timestamp
)}
<
/>
;
}
function
renderStatus
(
status
,
model_limits_enabled
=
false
)
{
switch
(
status
)
{
case
1
:
if
(
model_limits_enabled
)
{
return
<
Tag
color
=
"green"
size
=
"large"
>
已启用:限制模型
<
/Tag>
;
return
(
<
Tag
color
=
'green'
size
=
'large'
>
已启用:限制模型
<
/Tag
>
);
}
else
{
return
<
Tag
color
=
"green"
size
=
"large"
>
已启用
<
/Tag>
;
return
(
<
Tag
color
=
'green'
size
=
'large'
>
已启用
<
/Tag
>
);
}
case
2
:
return
<
Tag
color
=
"red"
size
=
"large"
>
已禁用
<
/Tag>
;
return
(
<
Tag
color
=
'red'
size
=
'large'
>
{
' '
}
已禁用
{
' '
}
<
/Tag
>
);
case
3
:
return
<
Tag
color
=
"yellow"
size
=
"large"
>
已过期
<
/Tag>
;
return
(
<
Tag
color
=
'yellow'
size
=
'large'
>
{
' '
}
已过期
{
' '
}
<
/Tag
>
);
case
4
:
return
<
Tag
color
=
"grey"
size
=
"large"
>
已耗尽
<
/Tag>
;
return
(
<
Tag
color
=
'grey'
size
=
'large'
>
{
' '
}
已耗尽
{
' '
}
<
/Tag
>
);
default
:
return
<
Tag
color
=
"black"
size
=
"large"
>
未知状态
<
/Tag>
;
return
(
<
Tag
color
=
'black'
size
=
'large'
>
{
' '
}
未知状态
{
' '
}
<
/Tag
>
);
}
}
const
TokensTable
=
()
=>
{
const
link_menu
=
[
{
node
:
'item'
,
key
:
'next'
,
name
:
'ChatGPT Next Web'
,
onClick
:
()
=>
{
node
:
'item'
,
key
:
'next'
,
name
:
'ChatGPT Next Web'
,
onClick
:
()
=>
{
onOpenLink
(
'next'
);
}
}
,
},
{
node
:
'item'
,
key
:
'ama'
,
name
:
'AMA 问天'
,
value
:
'ama'
},
{
node
:
'item'
,
key
:
'next-mj'
,
name
:
'ChatGPT Web & Midjourney'
,
value
:
'next-mj'
,
onClick
:
()
=>
{
node
:
'item'
,
key
:
'next-mj'
,
name
:
'ChatGPT Web & Midjourney'
,
value
:
'next-mj'
,
onClick
:
()
=>
{
onOpenLink
(
'next-mj'
);
}
}
,
},
{
node
:
'item'
,
key
:
'opencat'
,
name
:
'OpenCat'
,
value
:
'opencat'
}
{
node
:
'item'
,
key
:
'opencat'
,
name
:
'OpenCat'
,
value
:
'opencat'
}
,
];
const
columns
=
[
{
title
:
'名称'
,
dataIndex
:
'name'
dataIndex
:
'name'
,
},
{
title
:
'状态'
,
dataIndex
:
'status'
,
key
:
'status'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderStatus
(
text
,
record
.
model_limits_enabled
)}
<
/div
>
);
}
return
<
div
>
{
renderStatus
(
text
,
record
.
model_limits_enabled
)}
<
/div>
;
},
},
{
title
:
'已用额度'
,
dataIndex
:
'used_quota'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderQuota
(
parseInt
(
text
))}
<
/div
>
);
}
return
<
div
>
{
renderQuota
(
parseInt
(
text
))}
<
/div>
;
},
},
{
title
:
'剩余额度'
,
...
...
@@ -97,22 +135,25 @@ const TokensTable = () => {
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
record
.
unlimited_quota
?
<
Tag
size
=
{
'large'
}
color
=
{
'white'
}
>
无限制
<
/Tag>
:
<
Tag
size
=
{
'large'
}
color
=
{
'light-blue'
}
>
{
renderQuota
(
parseInt
(
text
))}
<
/Tag>
}
{
record
.
unlimited_quota
?
(
<
Tag
size
=
{
'large'
}
color
=
{
'white'
}
>
无限制
<
/Tag
>
)
:
(
<
Tag
size
=
{
'large'
}
color
=
{
'light-blue'
}
>
{
renderQuota
(
parseInt
(
text
))}
<
/Tag
>
)}
<
/div
>
);
}
}
,
},
{
title
:
'创建时间'
,
dataIndex
:
'created_time'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderTimestamp
(
text
)}
<
/div
>
);
}
return
<
div
>
{
renderTimestamp
(
text
)}
<
/div>
;
},
},
{
title
:
'过期时间'
,
...
...
@@ -123,7 +164,7 @@ const TokensTable = () => {
{
record
.
expired_time
===
-
1
?
'永不过期'
:
renderTimestamp
(
text
)}
<
/div
>
);
}
}
,
},
{
title
:
''
,
...
...
@@ -131,25 +172,41 @@ const TokensTable = () => {
render
:
(
text
,
record
,
index
)
=>
(
<
div
>
<
Popover
content
=
{
'sk-'
+
record
.
key
}
content
=
{
'sk-'
+
record
.
key
}
style
=
{{
padding
:
20
}}
position
=
"top"
position
=
'top'
>
<
Button
theme
=
"light"
type
=
"tertiary"
style
=
{{
marginRight
:
1
}}
>
查看
<
/Button
>
<
Button
theme
=
'light'
type
=
'tertiary'
style
=
{{
marginRight
:
1
}}
>
查看
<
/Button
>
<
/Popover
>
<
Button
theme
=
"light"
type
=
"secondary"
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
(
text
)
=>
{
await
copyText
(
'sk-'
+
record
.
key
);
}}
>
复制
<
/Button
>
<
SplitButtonGroup
style
=
{{
marginRight
:
1
}}
aria
-
label
=
"项目操作按钮组"
>
<
Button
theme
=
"light"
style
=
{{
color
:
'rgba(var(--semi-teal-7), 1)'
}}
onClick
=
{()
=>
{
onOpenLink
(
'next'
,
record
.
key
);
}}
>
聊天
<
/Button
>
<
Dropdown
trigger
=
"click"
position
=
"bottomRight"
menu
=
{
[
<
Button
theme
=
'light'
type
=
'secondary'
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
(
text
)
=>
{
await
copyText
(
'sk-'
+
record
.
key
);
}}
>
复制
<
/Button
>
<
SplitButtonGroup
style
=
{{
marginRight
:
1
}}
aria
-
label
=
'项目操作按钮组'
>
<
Button
theme
=
'light'
style
=
{{
color
:
'rgba(var(--semi-teal-7), 1)'
}}
onClick
=
{()
=>
{
onOpenLink
(
'next'
,
record
.
key
);
}}
>
聊天
<
/Button
>
<
Dropdown
trigger
=
'click'
position
=
'bottomRight'
menu
=
{[
{
node
:
'item'
,
key
:
'next'
,
...
...
@@ -157,7 +214,7 @@ const TokensTable = () => {
name
:
'ChatGPT Next Web'
,
onClick
:
()
=>
{
onOpenLink
(
'next'
,
record
.
key
);
}
}
,
},
{
node
:
'item'
,
...
...
@@ -166,70 +223,88 @@ const TokensTable = () => {
name
:
'ChatGPT Web & Midjourney'
,
onClick
:
()
=>
{
onOpenLink
(
'next-mj'
,
record
.
key
);
}
}
,
},
{
node
:
'item'
,
key
:
'ama'
,
name
:
'AMA 问天(BotGem)'
,
onClick
:
()
=>
{
node
:
'item'
,
key
:
'ama'
,
name
:
'AMA 问天(BotGem)'
,
onClick
:
()
=>
{
onOpenLink
(
'ama'
,
record
.
key
);
}
}
,
},
{
node
:
'item'
,
key
:
'opencat'
,
name
:
'OpenCat'
,
onClick
:
()
=>
{
node
:
'item'
,
key
:
'opencat'
,
name
:
'OpenCat'
,
onClick
:
()
=>
{
onOpenLink
(
'opencat'
,
record
.
key
);
}
}
]
}
},
},
]}
>
<
Button
style
=
{{
padding
:
'8px 4px'
,
color
:
'rgba(var(--semi-teal-7), 1)'
}}
type
=
"primary"
icon
=
{
<
IconTreeTriangleDown
/>
}
><
/Button
>
<
Button
style
=
{{
padding
:
'8px 4px'
,
color
:
'rgba(var(--semi-teal-7), 1)'
,
}}
type
=
'primary'
icon
=
{
<
IconTreeTriangleDown
/>
}
><
/Button
>
<
/Dropdown
>
<
/SplitButtonGroup
>
<
Popconfirm
title
=
"确定是否要删除此令牌?"
content
=
"此修改将不可逆"
title
=
'确定是否要删除此令牌?'
content
=
'此修改将不可逆'
okType
=
{
'danger'
}
position
=
{
'left'
}
onConfirm
=
{()
=>
{
manageToken
(
record
.
id
,
'delete'
,
record
).
then
(
()
=>
{
removeRecord
(
record
.
key
);
}
);
manageToken
(
record
.
id
,
'delete'
,
record
).
then
(()
=>
{
removeRecord
(
record
.
key
);
});
}}
>
<
Button
theme
=
"light"
type
=
"danger"
style
=
{{
marginRight
:
1
}}
>
删除
<
/Button
>
<
Button
theme
=
'light'
type
=
'danger'
style
=
{{
marginRight
:
1
}}
>
删除
<
/Button
>
<
/Popconfirm
>
{
record
.
status
===
1
?
<
Button
theme
=
"light"
type
=
"warning"
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageToken
(
record
.
id
,
'disable'
,
record
);
}
}
>
禁用
<
/Button>
:
<
Button
theme
=
"light"
type
=
"secondary"
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageToken
(
record
.
id
,
'enable'
,
record
);
}
}
>
启用
<
/Button
>
}
<
Button
theme
=
"light"
type
=
"tertiary"
style
=
{{
marginRight
:
1
}}
onClick
=
{
()
=>
{
{
record
.
status
===
1
?
(
<
Button
theme
=
'light'
type
=
'warning'
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageToken
(
record
.
id
,
'disable'
,
record
);
}}
>
禁用
<
/Button
>
)
:
(
<
Button
theme
=
'light'
type
=
'secondary'
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageToken
(
record
.
id
,
'enable'
,
record
);
}}
>
启用
<
/Button
>
)}
<
Button
theme
=
'light'
type
=
'tertiary'
style
=
{{
marginRight
:
1
}}
onClick
=
{()
=>
{
setEditingToken
(
record
);
setShowEdit
(
true
);
}
}
>
编辑
<
/Button
>
}}
>
编辑
<
/Button
>
<
/div
>
)
}
)
,
}
,
];
const
[
pageSize
,
setPageSize
]
=
useState
(
ITEMS_PER_PAGE
);
...
...
@@ -245,14 +320,14 @@ const TokensTable = () => {
const
[
showTopUpModal
,
setShowTopUpModal
]
=
useState
(
false
);
const
[
targetTokenIdx
,
setTargetTokenIdx
]
=
useState
(
0
);
const
[
editingToken
,
setEditingToken
]
=
useState
({
id
:
undefined
id
:
undefined
,
});
const
closeEdit
=
()
=>
{
setShowEdit
(
false
);
setTimeout
(()
=>
{
setEditingToken
({
id
:
undefined
id
:
undefined
,
});
},
500
);
};
...
...
@@ -266,7 +341,10 @@ const TokensTable = () => {
}
};
let
pageData
=
tokens
.
slice
((
activePage
-
1
)
*
pageSize
,
activePage
*
pageSize
);
let
pageData
=
tokens
.
slice
(
(
activePage
-
1
)
*
pageSize
,
activePage
*
pageSize
,
);
const
loadTokens
=
async
(
startIdx
)
=>
{
setLoading
(
true
);
const
res
=
await
API
.
get
(
`/api/token/?p=
${
startIdx
}
&size=
${
pageSize
}
`
);
...
...
@@ -315,7 +393,8 @@ const TokensTable = () => {
let
nextUrl
;
if
(
nextLink
)
{
nextUrl
=
nextLink
+
`/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
nextUrl
=
nextLink
+
`/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
}
else
{
nextUrl
=
`https://chat.oneapi.pro/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
}
...
...
@@ -323,7 +402,8 @@ const TokensTable = () => {
let
url
;
switch
(
type
)
{
case
'ama'
:
url
=
mjLink
+
`/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
url
=
mjLink
+
`/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
break
;
case
'opencat'
:
url
=
`opencat://team/join?domain=
${
encodedServerAddress
}
&token=sk-
${
key
}
`
;
...
...
@@ -367,7 +447,8 @@ const TokensTable = () => {
let
defaultUrl
;
if
(
chatLink
)
{
defaultUrl
=
chatLink
+
`/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
defaultUrl
=
chatLink
+
`/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
}
let
url
;
switch
(
type
)
{
...
...
@@ -378,7 +459,8 @@ const TokensTable = () => {
url
=
`opencat://team/join?domain=
${
encodedServerAddress
}
&token=sk-
${
key
}
`
;
break
;
case
'next-mj'
:
url
=
mjLink
+
`/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
url
=
mjLink
+
`/#/?settings={"key":"sk-
${
key
}
","url":"
${
serverAddress
}
"}`
;
break
;
default
:
if
(
!
chatLink
)
{
...
...
@@ -399,10 +481,10 @@ const TokensTable = () => {
});
},
[
pageSize
]);
const
removeRecord
=
key
=>
{
const
removeRecord
=
(
key
)
=>
{
let
newDataSource
=
[...
tokens
];
if
(
key
!=
null
)
{
let
idx
=
newDataSource
.
findIndex
(
data
=>
data
.
key
===
key
);
let
idx
=
newDataSource
.
findIndex
(
(
data
)
=>
data
.
key
===
key
);
if
(
idx
>
-
1
)
{
newDataSource
.
splice
(
idx
,
1
);
...
...
@@ -435,7 +517,6 @@ const TokensTable = () => {
let
newTokens
=
[...
tokens
];
// let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
if
(
action
===
'delete'
)
{
}
else
{
record
.
status
=
token
.
status
;
// newTokens[realIdx].status = token.status;
...
...
@@ -455,7 +536,9 @@ const TokensTable = () => {
return
;
}
setSearching
(
true
);
const
res
=
await
API
.
get
(
`/api/token/search?keyword=
${
searchKeyword
}
&token=
${
searchToken
}
`
);
const
res
=
await
API
.
get
(
`/api/token/search?keyword=
${
searchKeyword
}
&token=
${
searchToken
}
`
,
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
setTokensFormat
(
data
);
...
...
@@ -488,32 +571,28 @@ const TokensTable = () => {
setLoading
(
false
);
};
const
handlePageChange
=
page
=>
{
const
handlePageChange
=
(
page
)
=>
{
setActivePage
(
page
);
if
(
page
===
Math
.
ceil
(
tokens
.
length
/
pageSize
)
+
1
)
{
// In this case we have to load more data and then append them.
loadTokens
(
page
-
1
).
then
(
r
=>
{
});
loadTokens
(
page
-
1
).
then
((
r
)
=>
{});
}
};
const
rowSelection
=
{
onSelect
:
(
record
,
selected
)
=>
{
},
onSelectAll
:
(
selected
,
selectedRows
)
=>
{
},
onSelect
:
(
record
,
selected
)
=>
{},
onSelectAll
:
(
selected
,
selectedRows
)
=>
{},
onChange
:
(
selectedRowKeys
,
selectedRows
)
=>
{
setSelectedKeys
(
selectedRows
);
}
}
,
};
const
handleRow
=
(
record
,
index
)
=>
{
if
(
record
.
status
!==
1
)
{
return
{
style
:
{
background
:
'var(--semi-color-disabled-border)'
}
background
:
'var(--semi-color-disabled-border)'
,
}
,
};
}
else
{
return
{};
...
...
@@ -522,63 +601,98 @@ const TokensTable = () => {
return
(
<>
<
EditToken
refresh
=
{
refresh
}
editingToken
=
{
editingToken
}
visiable
=
{
showEdit
}
handleClose
=
{
closeEdit
}
><
/EditToken
>
<
Form
layout
=
"horizontal"
style
=
{{
marginTop
:
10
}}
labelPosition
=
{
'left'
}
>
<
EditToken
refresh
=
{
refresh
}
editingToken
=
{
editingToken
}
visiable
=
{
showEdit
}
handleClose
=
{
closeEdit
}
><
/EditToken
>
<
Form
layout
=
'horizontal'
style
=
{{
marginTop
:
10
}}
labelPosition
=
{
'left'
}
>
<
Form
.
Input
field
=
"keyword"
label
=
"搜索关键字"
placeholder
=
"令牌名称"
field
=
'keyword'
label
=
'搜索关键字'
placeholder
=
'令牌名称'
value
=
{
searchKeyword
}
loading
=
{
searching
}
onChange
=
{
handleKeywordChange
}
/
>
<
Form
.
Input
field
=
"token"
label
=
"Key"
placeholder
=
"密钥"
field
=
'token'
label
=
'Key'
placeholder
=
'密钥'
value
=
{
searchToken
}
loading
=
{
searching
}
onChange
=
{
handleSearchTokenChange
}
/
>
<
Button
label
=
"查询"
type
=
"primary"
htmlType
=
"submit"
className
=
"btn-margin-right"
onClick
=
{
searchTokens
}
style
=
{{
marginRight
:
8
}}
>
查询
<
/Button
>
<
Button
label
=
'查询'
type
=
'primary'
htmlType
=
'submit'
className
=
'btn-margin-right'
onClick
=
{
searchTokens
}
style
=
{{
marginRight
:
8
}}
>
查询
<
/Button
>
<
/Form
>
<
Table
style
=
{{
marginTop
:
20
}}
columns
=
{
columns
}
dataSource
=
{
pageData
}
pagination
=
{{
currentPage
:
activePage
,
pageSize
:
pageSize
,
total
:
tokenCount
,
showSizeChanger
:
true
,
pageSizeOptions
:
[
10
,
20
,
50
,
100
],
formatPageText
:
(
page
)
=>
`第
${
page
.
currentStart
}
-
${
page
.
currentEnd
}
条,共
${
tokens
.
length
}
条`
,
onPageSizeChange
:
(
size
)
=>
{
setPageSize
(
size
);
setActivePage
(
1
);
},
onPageChange
:
handlePageChange
}}
loading
=
{
loading
}
rowSelection
=
{
rowSelection
}
onRow
=
{
handleRow
}
>
<
/Table
>
<
Button
theme
=
"light"
type
=
"primary"
style
=
{{
marginRight
:
8
}}
onClick
=
{
()
=>
{
<
Table
style
=
{{
marginTop
:
20
}}
columns
=
{
columns
}
dataSource
=
{
pageData
}
pagination
=
{{
currentPage
:
activePage
,
pageSize
:
pageSize
,
total
:
tokenCount
,
showSizeChanger
:
true
,
pageSizeOptions
:
[
10
,
20
,
50
,
100
],
formatPageText
:
(
page
)
=>
`第
${
page
.
currentStart
}
-
${
page
.
currentEnd
}
条,共
${
tokens
.
length
}
条`
,
onPageSizeChange
:
(
size
)
=>
{
setPageSize
(
size
);
setActivePage
(
1
);
},
onPageChange
:
handlePageChange
,
}}
loading
=
{
loading
}
rowSelection
=
{
rowSelection
}
onRow
=
{
handleRow
}
><
/Table
>
<
Button
theme
=
'light'
type
=
'primary'
style
=
{{
marginRight
:
8
}}
onClick
=
{()
=>
{
setEditingToken
({
id
:
undefined
id
:
undefined
,
});
setShowEdit
(
true
);
}
}
>
添加令牌
<
/Button
>
<
Button
label
=
"复制所选令牌"
type
=
"warning"
onClick
=
{
async
()
=>
{
}}
>
添加令牌
<
/Button
>
<
Button
label
=
'复制所选令牌'
type
=
'warning'
onClick
=
{
async
()
=>
{
if
(
selectedKeys
.
length
===
0
)
{
showError
(
'请至少选择一个令牌!'
);
return
;
}
let
keys
=
''
;
for
(
let
i
=
0
;
i
<
selectedKeys
.
length
;
i
++
)
{
keys
+=
selectedKeys
[
i
].
name
+
' sk-'
+
selectedKeys
[
i
].
key
+
'\n'
;
keys
+=
selectedKeys
[
i
].
name
+
' sk-'
+
selectedKeys
[
i
].
key
+
'\n'
;
}
await
copyText
(
keys
);
}
}
>
复制所选令牌到剪贴板
<
/Button
>
}}
>
复制所选令牌到剪贴板
<
/Button
>
<
/
>
);
};
...
...
web/src/components/UsersTable.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
API
,
showError
,
showSuccess
}
from
'../helpers'
;
import
{
Button
,
Form
,
Popconfirm
,
Space
,
Table
,
Tag
,
Tooltip
}
from
'@douyinfe/semi-ui'
;
import
{
Button
,
Form
,
Popconfirm
,
Space
,
Table
,
Tag
,
Tooltip
,
}
from
'@douyinfe/semi-ui'
;
import
{
ITEMS_PER_PAGE
}
from
'../constants'
;
import
{
renderGroup
,
renderNumber
,
renderQuota
}
from
'../helpers/render'
;
import
AddUser
from
'../pages/User/AddUser'
;
...
...
@@ -9,124 +17,218 @@ import EditUser from '../pages/User/EditUser';
function
renderRole
(
role
)
{
switch
(
role
)
{
case
1
:
return
<
Tag
size
=
"large"
>
普通用户
<
/Tag>
;
return
<
Tag
size
=
'large'
>
普通用户
<
/Tag>
;
case
10
:
return
<
Tag
color
=
"yellow"
size
=
"large"
>
管理员
<
/Tag>
;
return
(
<
Tag
color
=
'yellow'
size
=
'large'
>
管理员
<
/Tag
>
);
case
100
:
return
<
Tag
color
=
"orange"
size
=
"large"
>
超级管理员
<
/Tag>
;
return
(
<
Tag
color
=
'orange'
size
=
'large'
>
超级管理员
<
/Tag
>
);
default
:
return
<
Tag
color
=
"red"
size
=
"large"
>
未知身份
<
/Tag>
;
return
(
<
Tag
color
=
'red'
size
=
'large'
>
未知身份
<
/Tag
>
);
}
}
const
UsersTable
=
()
=>
{
const
columns
=
[{
title
:
'ID'
,
dataIndex
:
'id'
},
{
title
:
'用户名'
,
dataIndex
:
'username'
},
{
title
:
'分组'
,
dataIndex
:
'group'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderGroup
(
text
)}
<
/div>
)
;
}
},
{
title
:
'统计信息'
,
dataIndex
:
'info'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
<
Space
spacing
=
{
1
}
>
<
Tooltip
content
=
{
'剩余额度'
}
>
<
Tag
color
=
"white"
size
=
"large"
>
{
renderQuota
(
record
.
quota
)}
<
/Tag
>
<
/Tooltip
>
<
Tooltip
content
=
{
'已用额度'
}
>
<
Tag
color
=
"white"
size
=
"large"
>
{
renderQuota
(
record
.
used_quota
)}
<
/Tag
>
<
/Tooltip
>
<
Tooltip
content
=
{
'调用次数'
}
>
<
Tag
color
=
"white"
size
=
"large"
>
{
renderNumber
(
record
.
request_count
)}
<
/Tag
>
<
/Tooltip
>
<
/Space
>
<
/div>
)
;
}
},
{
title
:
'邀请信息'
,
dataIndex
:
'invite'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
<
Space
spacing
=
{
1
}
>
<
Tooltip
content
=
{
'邀请人数'
}
>
<
Tag
color
=
"white"
size
=
"large"
>
{
renderNumber
(
record
.
aff_count
)}
<
/Tag
>
<
/Tooltip
>
<
Tooltip
content
=
{
'邀请总收益'
}
>
<
Tag
color
=
"white"
size
=
"large"
>
{
renderQuota
(
record
.
aff_history_quota
)}
<
/Tag
>
<
/Tooltip
>
<
Tooltip
content
=
{
'邀请人ID'
}
>
{
record
.
inviter_id
===
0
?
<
Tag
color
=
"white"
size
=
"large"
>
无
<
/Tag>
:
<
Tag
color
=
"white"
size
=
"large"
>
{
record
.
inviter_id
}
<
/Tag>
}
<
/Tooltip
>
<
/Space
>
<
/div>
)
;
}
},
{
title
:
'角色'
,
dataIndex
:
'role'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
renderRole
(
text
)}
<
/div>
)
;
}
},
{
title
:
'状态'
,
dataIndex
:
'status'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
record
.
DeletedAt
!==
null
?
<
Tag
color
=
"red"
>
已注销
<
/Tag> : renderStatus
(
text
)
}
<
/div>
)
;
}
},
{
title
:
''
,
dataIndex
:
'operate'
,
render
:
(
text
,
record
,
index
)
=>
(
<
div
>
{
record
.
DeletedAt
!==
null
?
<><
/>
:
<>
<
Popconfirm
title
=
"确定?"
okType
=
{
'warning'
}
onConfirm
=
{()
=>
{
manageUser
(
record
.
username
,
'promote'
,
record
);
}}
>
<
Button
theme
=
"light"
type
=
"warning"
style
=
{{
marginRight
:
1
}}
>
提升
<
/Button
>
<
/Popconfirm
>
<
Popconfirm
title
=
"确定?"
okType
=
{
'warning'
}
onConfirm
=
{()
=>
{
manageUser
(
record
.
username
,
'demote'
,
record
);
}}
>
<
Button
theme
=
"light"
type
=
"secondary"
style
=
{{
marginRight
:
1
}}
>
降级
<
/Button
>
<
/Popconfirm
>
{
record
.
status
===
1
?
<
Button
theme
=
"light"
type
=
"warning"
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageUser
(
record
.
username
,
'disable'
,
record
);
}}
>
禁用
<
/Button>
:
<
Button
theme
=
"light"
type
=
"secondary"
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageUser
(
record
.
username
,
'enable'
,
record
);
}}
disabled
=
{
record
.
status
===
3
}
>
启用
<
/Button>
}
<
Button
theme
=
"light"
type
=
"tertiary"
style
=
{{
marginRight
:
1
}}
onClick
=
{()
=>
{
setEditingUser
(
record
);
setShowEditUser
(
true
);
}}
>
编辑
<
/Button
>
<
/
>
}
<
Popconfirm
title
=
"确定是否要删除此用户?"
content
=
"硬删除,此修改将不可逆"
okType
=
{
'danger'
}
position
=
{
'left'
}
onConfirm
=
{()
=>
{
manageUser
(
record
.
username
,
'delete'
,
record
).
then
(()
=>
{
removeRecord
(
record
.
id
);
});
}}
>
<
Button
theme
=
"light"
type
=
"danger"
style
=
{{
marginRight
:
1
}}
>
删除
<
/Button
>
<
/Popconfirm
>
<
/div>
)
}];
const
columns
=
[
{
title
:
'ID'
,
dataIndex
:
'id'
,
},
{
title
:
'用户名'
,
dataIndex
:
'username'
,
},
{
title
:
'分组'
,
dataIndex
:
'group'
,
render
:
(
text
,
record
,
index
)
=>
{
return
<
div
>
{
renderGroup
(
text
)}
<
/div>
;
},
},
{
title
:
'统计信息'
,
dataIndex
:
'info'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
<
Space
spacing
=
{
1
}
>
<
Tooltip
content
=
{
'剩余额度'
}
>
<
Tag
color
=
'white'
size
=
'large'
>
{
renderQuota
(
record
.
quota
)}
<
/Tag
>
<
/Tooltip
>
<
Tooltip
content
=
{
'已用额度'
}
>
<
Tag
color
=
'white'
size
=
'large'
>
{
renderQuota
(
record
.
used_quota
)}
<
/Tag
>
<
/Tooltip
>
<
Tooltip
content
=
{
'调用次数'
}
>
<
Tag
color
=
'white'
size
=
'large'
>
{
renderNumber
(
record
.
request_count
)}
<
/Tag
>
<
/Tooltip
>
<
/Space
>
<
/div
>
);
},
},
{
title
:
'邀请信息'
,
dataIndex
:
'invite'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
<
Space
spacing
=
{
1
}
>
<
Tooltip
content
=
{
'邀请人数'
}
>
<
Tag
color
=
'white'
size
=
'large'
>
{
renderNumber
(
record
.
aff_count
)}
<
/Tag
>
<
/Tooltip
>
<
Tooltip
content
=
{
'邀请总收益'
}
>
<
Tag
color
=
'white'
size
=
'large'
>
{
renderQuota
(
record
.
aff_history_quota
)}
<
/Tag
>
<
/Tooltip
>
<
Tooltip
content
=
{
'邀请人ID'
}
>
{
record
.
inviter_id
===
0
?
(
<
Tag
color
=
'white'
size
=
'large'
>
无
<
/Tag
>
)
:
(
<
Tag
color
=
'white'
size
=
'large'
>
{
record
.
inviter_id
}
<
/Tag
>
)}
<
/Tooltip
>
<
/Space
>
<
/div
>
);
},
},
{
title
:
'角色'
,
dataIndex
:
'role'
,
render
:
(
text
,
record
,
index
)
=>
{
return
<
div
>
{
renderRole
(
text
)}
<
/div>
;
},
},
{
title
:
'状态'
,
dataIndex
:
'status'
,
render
:
(
text
,
record
,
index
)
=>
{
return
(
<
div
>
{
record
.
DeletedAt
!==
null
?
(
<
Tag
color
=
'red'
>
已注销
<
/Tag
>
)
:
(
renderStatus
(
text
)
)}
<
/div
>
);
},
},
{
title
:
''
,
dataIndex
:
'operate'
,
render
:
(
text
,
record
,
index
)
=>
(
<
div
>
{
record
.
DeletedAt
!==
null
?
(
<><
/
>
)
:
(
<>
<
Popconfirm
title
=
'确定?'
okType
=
{
'warning'
}
onConfirm
=
{()
=>
{
manageUser
(
record
.
username
,
'promote'
,
record
);
}}
>
<
Button
theme
=
'light'
type
=
'warning'
style
=
{{
marginRight
:
1
}}
>
提升
<
/Button
>
<
/Popconfirm
>
<
Popconfirm
title
=
'确定?'
okType
=
{
'warning'
}
onConfirm
=
{()
=>
{
manageUser
(
record
.
username
,
'demote'
,
record
);
}}
>
<
Button
theme
=
'light'
type
=
'secondary'
style
=
{{
marginRight
:
1
}}
>
降级
<
/Button
>
<
/Popconfirm
>
{
record
.
status
===
1
?
(
<
Button
theme
=
'light'
type
=
'warning'
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageUser
(
record
.
username
,
'disable'
,
record
);
}}
>
禁用
<
/Button
>
)
:
(
<
Button
theme
=
'light'
type
=
'secondary'
style
=
{{
marginRight
:
1
}}
onClick
=
{
async
()
=>
{
manageUser
(
record
.
username
,
'enable'
,
record
);
}}
disabled
=
{
record
.
status
===
3
}
>
启用
<
/Button
>
)}
<
Button
theme
=
'light'
type
=
'tertiary'
style
=
{{
marginRight
:
1
}}
onClick
=
{()
=>
{
setEditingUser
(
record
);
setShowEditUser
(
true
);
}}
>
编辑
<
/Button
>
<
/
>
)}
<
Popconfirm
title
=
'确定是否要删除此用户?'
content
=
'硬删除,此修改将不可逆'
okType
=
{
'danger'
}
position
=
{
'left'
}
onConfirm
=
{()
=>
{
manageUser
(
record
.
username
,
'delete'
,
record
).
then
(()
=>
{
removeRecord
(
record
.
id
);
});
}}
>
<
Button
theme
=
'light'
type
=
'danger'
style
=
{{
marginRight
:
1
}}
>
删除
<
/Button
>
<
/Popconfirm
>
<
/div
>
),
},
];
const
[
users
,
setUsers
]
=
useState
([]);
const
[
loading
,
setLoading
]
=
useState
(
true
);
...
...
@@ -137,22 +239,22 @@ const UsersTable = () => {
const
[
showAddUser
,
setShowAddUser
]
=
useState
(
false
);
const
[
showEditUser
,
setShowEditUser
]
=
useState
(
false
);
const
[
editingUser
,
setEditingUser
]
=
useState
({
id
:
undefined
id
:
undefined
,
});
const
setCount
=
(
data
)
=>
{
if
(
data
.
length
>=
(
activePage
)
*
ITEMS_PER_PAGE
)
{
if
(
data
.
length
>=
activePage
*
ITEMS_PER_PAGE
)
{
setUserCount
(
data
.
length
+
1
);
}
else
{
setUserCount
(
data
.
length
);
}
};
const
removeRecord
=
key
=>
{
const
removeRecord
=
(
key
)
=>
{
console
.
log
(
key
);
let
newDataSource
=
[...
users
];
if
(
key
!=
null
)
{
let
idx
=
newDataSource
.
findIndex
(
data
=>
data
.
id
===
key
);
let
idx
=
newDataSource
.
findIndex
(
(
data
)
=>
data
.
id
===
key
);
if
(
idx
>
-
1
)
{
newDataSource
.
splice
(
idx
,
1
);
...
...
@@ -200,7 +302,8 @@ const UsersTable = () => {
const
manageUser
=
async
(
username
,
action
,
record
)
=>
{
const
res
=
await
API
.
post
(
'/api/user/manage'
,
{
username
,
action
username
,
action
,
});
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
...
...
@@ -208,7 +311,6 @@ const UsersTable = () => {
let
user
=
res
.
data
.
data
;
let
newUsers
=
[...
users
];
if
(
action
===
'delete'
)
{
}
else
{
record
.
status
=
user
.
status
;
record
.
role
=
user
.
role
;
...
...
@@ -222,15 +324,19 @@ const UsersTable = () => {
const
renderStatus
=
(
status
)
=>
{
switch
(
status
)
{
case
1
:
return
<
Tag
size
=
"large"
>
已激活
<
/Tag>
;
return
<
Tag
size
=
'large'
>
已激活
<
/Tag>
;
case
2
:
return
(
<
Tag
size
=
"large"
color
=
"red"
>
已封禁
<
/Tag>
)
;
return
(
<
Tag
size
=
'large'
color
=
'red'
>
已封禁
<
/Tag
>
);
default
:
return
(
<
Tag
size
=
"large"
color
=
"grey"
>
未知状态
<
/Tag>
)
;
return
(
<
Tag
size
=
'large'
color
=
'grey'
>
未知状态
<
/Tag
>
);
}
};
...
...
@@ -271,16 +377,18 @@ const UsersTable = () => {
setLoading
(
false
);
};
const
handlePageChange
=
page
=>
{
const
handlePageChange
=
(
page
)
=>
{
setActivePage
(
page
);
if
(
page
===
Math
.
ceil
(
users
.
length
/
ITEMS_PER_PAGE
)
+
1
)
{
// In this case we have to load more data and then append them.
loadUsers
(
page
-
1
).
then
(
r
=>
{
});
loadUsers
(
page
-
1
).
then
((
r
)
=>
{});
}
};
const
pageData
=
users
.
slice
((
activePage
-
1
)
*
ITEMS_PER_PAGE
,
activePage
*
ITEMS_PER_PAGE
);
const
pageData
=
users
.
slice
(
(
activePage
-
1
)
*
ITEMS_PER_PAGE
,
activePage
*
ITEMS_PER_PAGE
,
);
const
closeAddUser
=
()
=>
{
setShowAddUser
(
false
);
...
...
@@ -289,7 +397,7 @@ const UsersTable = () => {
const
closeEditUser
=
()
=>
{
setShowEditUser
(
false
);
setEditingUser
({
id
:
undefined
id
:
undefined
,
});
};
...
...
@@ -303,34 +411,52 @@ const UsersTable = () => {
return
(
<>
<
AddUser
refresh
=
{
refresh
}
visible
=
{
showAddUser
}
handleClose
=
{
closeAddUser
}
><
/AddUser
>
<
EditUser
refresh
=
{
refresh
}
visible
=
{
showEditUser
}
handleClose
=
{
closeEditUser
}
editingUser
=
{
editingUser
}
><
/EditUser
>
<
AddUser
refresh
=
{
refresh
}
visible
=
{
showAddUser
}
handleClose
=
{
closeAddUser
}
><
/AddUser
>
<
EditUser
refresh
=
{
refresh
}
visible
=
{
showEditUser
}
handleClose
=
{
closeEditUser
}
editingUser
=
{
editingUser
}
><
/EditUser
>
<
Form
onSubmit
=
{
searchUsers
}
>
<
Form
.
Input
label
=
"搜索关键字"
icon
=
"search"
field
=
"keyword"
iconPosition
=
"left"
placeholder
=
"搜索用户的 ID,用户名,显示名称,以及邮箱地址 ..."
label
=
'搜索关键字'
icon
=
'search'
field
=
'keyword'
iconPosition
=
'left'
placeholder
=
'搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...'
value
=
{
searchKeyword
}
loading
=
{
searching
}
onChange
=
{
value
=>
handleKeywordChange
(
value
)}
onChange
=
{
(
value
)
=>
handleKeywordChange
(
value
)}
/
>
<
/Form
>
<
Table
columns
=
{
columns
}
dataSource
=
{
pageData
}
pagination
=
{{
currentPage
:
activePage
,
pageSize
:
ITEMS_PER_PAGE
,
total
:
userCount
,
pageSizeOpts
:
[
10
,
20
,
50
,
100
],
onPageChange
:
handlePageChange
}}
loading
=
{
loading
}
/
>
<
Button
theme
=
"light"
type
=
"primary"
style
=
{{
marginRight
:
8
}}
onClick
=
{
()
=>
{
<
Table
columns
=
{
columns
}
dataSource
=
{
pageData
}
pagination
=
{{
currentPage
:
activePage
,
pageSize
:
ITEMS_PER_PAGE
,
total
:
userCount
,
pageSizeOpts
:
[
10
,
20
,
50
,
100
],
onPageChange
:
handlePageChange
,
}}
loading
=
{
loading
}
/
>
<
Button
theme
=
'light'
type
=
'primary'
style
=
{{
marginRight
:
8
}}
onClick
=
{()
=>
{
setShowAddUser
(
true
);
}
}
>
添加用户
<
/Button
>
}}
>
添加用户
<
/Button
>
<
/
>
);
};
...
...
web/src/components/WeChatIcon.js
View file @
eb955ae4
...
...
@@ -3,15 +3,27 @@ import { Icon } from '@douyinfe/semi-ui';
const
WeChatIcon
=
()
=>
{
function
CustomIcon
()
{
return
<
svg
t
=
"1709714447384"
className
=
"icon"
viewBox
=
"0 0 1024 1024"
version
=
"1.1"
xmlns
=
"http://www.w3.org/2000/svg"
p
-
id
=
"5091"
width
=
"16"
height
=
"16"
>
<
path
d
=
"M690.1 377.4c5.9 0 11.8 0.2 17.6 0.5-24.4-128.7-158.3-227.1-319.9-227.1C209 150.8 64 271.4 64 420.2c0 81.1 43.6 154.2 111.9 203.6 5.5 3.9 9.1 10.3 9.1 17.6 0 2.4-0.5 4.6-1.1 6.9-5.5 20.3-14.2 52.8-14.6 54.3-0.7 2.6-1.7 5.2-1.7 7.9 0 5.9 4.8 10.8 10.8 10.8 2.3 0 4.2-0.9 6.2-2l70.9-40.9c5.3-3.1 11-5 17.2-5 3.2 0 6.4 0.5 9.5 1.4 33.1 9.5 68.8 14.8 105.7 14.8 6 0 11.9-0.1 17.8-0.4-7.1-21-10.9-43.1-10.9-66 0-135.8 132.2-245.8 295.3-245.8z m-194.3-86.5c23.8 0 43.2 19.3 43.2 43.1s-19.3 43.1-43.2 43.1c-23.8 0-43.2-19.3-43.2-43.1s19.4-43.1 43.2-43.1z m-215.9 86.2c-23.8 0-43.2-19.3-43.2-43.1s19.3-43.1 43.2-43.1 43.2 19.3 43.2 43.1-19.4 43.1-43.2 43.1z"
p
-
id
=
"5092"
><
/path
>
<
path
d
=
"M866.7 792.7c56.9-41.2 93.2-102 93.2-169.7 0-124-120.8-224.5-269.9-224.5-149 0-269.9 100.5-269.9 224.5S540.9 847.5 690 847.5c30.8 0 60.6-4.4 88.1-12.3 2.6-0.8 5.2-1.2 7.9-1.2 5.2 0 9.9 1.6 14.3 4.1l59.1 34c1.7 1 3.3 1.7 5.2 1.7 2.4 0 4.7-0.9 6.4-2.6 1.7-1.7 2.6-4 2.6-6.4 0-2.2-0.9-4.4-1.4-6.6-0.3-1.2-7.6-28.3-12.2-45.3-0.5-1.9-0.9-3.8-0.9-5.7 0.1-5.9 3.1-11.2 7.6-14.5zM600.2 587.2c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c0 19.8-16.2 35.9-36 35.9z m179.9 0c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c-0.1 19.8-16.2 35.9-36 35.9z"
p
-
id
=
"5093"
><
/path
>
<
/svg>
;
return
(
<
svg
t
=
'1709714447384'
className
=
'icon'
viewBox
=
'0 0 1024 1024'
version
=
'1.1'
xmlns
=
'http://www.w3.org/2000/svg'
p
-
id
=
'5091'
width
=
'16'
height
=
'16'
>
<
path
d
=
'M690.1 377.4c5.9 0 11.8 0.2 17.6 0.5-24.4-128.7-158.3-227.1-319.9-227.1C209 150.8 64 271.4 64 420.2c0 81.1 43.6 154.2 111.9 203.6 5.5 3.9 9.1 10.3 9.1 17.6 0 2.4-0.5 4.6-1.1 6.9-5.5 20.3-14.2 52.8-14.6 54.3-0.7 2.6-1.7 5.2-1.7 7.9 0 5.9 4.8 10.8 10.8 10.8 2.3 0 4.2-0.9 6.2-2l70.9-40.9c5.3-3.1 11-5 17.2-5 3.2 0 6.4 0.5 9.5 1.4 33.1 9.5 68.8 14.8 105.7 14.8 6 0 11.9-0.1 17.8-0.4-7.1-21-10.9-43.1-10.9-66 0-135.8 132.2-245.8 295.3-245.8z m-194.3-86.5c23.8 0 43.2 19.3 43.2 43.1s-19.3 43.1-43.2 43.1c-23.8 0-43.2-19.3-43.2-43.1s19.4-43.1 43.2-43.1z m-215.9 86.2c-23.8 0-43.2-19.3-43.2-43.1s19.3-43.1 43.2-43.1 43.2 19.3 43.2 43.1-19.4 43.1-43.2 43.1z'
p
-
id
=
'5092'
><
/path
>
<
path
d
=
'M866.7 792.7c56.9-41.2 93.2-102 93.2-169.7 0-124-120.8-224.5-269.9-224.5-149 0-269.9 100.5-269.9 224.5S540.9 847.5 690 847.5c30.8 0 60.6-4.4 88.1-12.3 2.6-0.8 5.2-1.2 7.9-1.2 5.2 0 9.9 1.6 14.3 4.1l59.1 34c1.7 1 3.3 1.7 5.2 1.7 2.4 0 4.7-0.9 6.4-2.6 1.7-1.7 2.6-4 2.6-6.4 0-2.2-0.9-4.4-1.4-6.6-0.3-1.2-7.6-28.3-12.2-45.3-0.5-1.9-0.9-3.8-0.9-5.7 0.1-5.9 3.1-11.2 7.6-14.5zM600.2 587.2c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c0 19.8-16.2 35.9-36 35.9z m179.9 0c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c-0.1 19.8-16.2 35.9-36 35.9z'
p
-
id
=
'5093'
><
/path
>
<
/svg
>
);
}
return
(
...
...
web/src/components/utils.js
View file @
eb955ae4
...
...
@@ -15,6 +15,6 @@ export async function onGitHubOAuthClicked(github_client_id) {
const
state
=
await
getOAuthState
();
if
(
!
state
)
return
;
window
.
open
(
`https://github.com/login/oauth/authorize?client_id=
${
github_client_id
}
&state=
${
state
}
&scope=user:email`
`https://github.com/login/oauth/authorize?client_id=
${
github_client_id
}
&state=
${
state
}
&scope=user:email`
,
);
}
\ No newline at end of file
}
web/src/constants/channel.constants.js
View file @
eb955ae4
export
const
CHANNEL_OPTIONS
=
[
{
key
:
1
,
text
:
'OpenAI'
,
value
:
1
,
color
:
'green'
,
label
:
'OpenAI'
},
{
key
:
2
,
text
:
'Midjourney Proxy'
,
value
:
2
,
color
:
'light-blue'
,
label
:
'Midjourney Proxy'
},
{
key
:
5
,
text
:
'Midjourney Proxy Plus'
,
value
:
5
,
color
:
'blue'
,
label
:
'Midjourney Proxy Plus'
},
{
key
:
4
,
text
:
'Ollama'
,
value
:
4
,
color
:
'grey'
,
label
:
'Ollama'
},
{
key
:
14
,
text
:
'Anthropic Claude'
,
value
:
14
,
color
:
'indigo'
,
label
:
'Anthropic Claude'
},
{
key
:
3
,
text
:
'Azure OpenAI'
,
value
:
3
,
color
:
'teal'
,
label
:
'Azure OpenAI'
},
{
key
:
11
,
text
:
'Google PaLM2'
,
value
:
11
,
color
:
'orange'
,
label
:
'Google PaLM2'
},
{
key
:
24
,
text
:
'Google Gemini'
,
value
:
24
,
color
:
'orange'
,
label
:
'Google Gemini'
},
{
key
:
15
,
text
:
'百度文心千帆'
,
value
:
15
,
color
:
'blue'
,
label
:
'百度文心千帆'
},
{
key
:
17
,
text
:
'阿里通义千问'
,
value
:
17
,
color
:
'orange'
,
label
:
'阿里通义千问'
},
{
key
:
18
,
text
:
'讯飞星火认知'
,
value
:
18
,
color
:
'blue'
,
label
:
'讯飞星火认知'
},
{
key
:
16
,
text
:
'智谱 ChatGLM'
,
value
:
16
,
color
:
'violet'
,
label
:
'智谱 ChatGLM'
},
{
key
:
16
,
text
:
'智谱 GLM-4V'
,
value
:
26
,
color
:
'purple'
,
label
:
'智谱 GLM-4V'
},
{
key
:
16
,
text
:
'Moonshot'
,
value
:
25
,
color
:
'green'
,
label
:
'Moonshot'
},
{
key
:
19
,
text
:
'360 智脑'
,
value
:
19
,
color
:
'blue'
,
label
:
'360 智脑'
},
{
key
:
23
,
text
:
'腾讯混元'
,
value
:
23
,
color
:
'teal'
,
label
:
'腾讯混元'
},
{
key
:
31
,
text
:
'零一万物'
,
value
:
31
,
color
:
'green'
,
label
:
'零一万物'
},
{
key
:
8
,
text
:
'自定义渠道'
,
value
:
8
,
color
:
'pink'
,
label
:
'自定义渠道'
},
{
key
:
22
,
text
:
'知识库:FastGPT'
,
value
:
22
,
color
:
'blue'
,
label
:
'知识库:FastGPT'
},
{
key
:
21
,
text
:
'知识库:AI Proxy'
,
value
:
21
,
color
:
'purple'
,
label
:
'知识库:AI Proxy'
},
{
key
:
1
,
text
:
'OpenAI'
,
value
:
1
,
color
:
'green'
,
label
:
'OpenAI'
},
{
key
:
2
,
text
:
'Midjourney Proxy'
,
value
:
2
,
color
:
'light-blue'
,
label
:
'Midjourney Proxy'
,
},
{
key
:
5
,
text
:
'Midjourney Proxy Plus'
,
value
:
5
,
color
:
'blue'
,
label
:
'Midjourney Proxy Plus'
,
},
{
key
:
4
,
text
:
'Ollama'
,
value
:
4
,
color
:
'grey'
,
label
:
'Ollama'
},
{
key
:
14
,
text
:
'Anthropic Claude'
,
value
:
14
,
color
:
'indigo'
,
label
:
'Anthropic Claude'
,
},
{
key
:
3
,
text
:
'Azure OpenAI'
,
value
:
3
,
color
:
'teal'
,
label
:
'Azure OpenAI'
,
},
{
key
:
11
,
text
:
'Google PaLM2'
,
value
:
11
,
color
:
'orange'
,
label
:
'Google PaLM2'
,
},
{
key
:
24
,
text
:
'Google Gemini'
,
value
:
24
,
color
:
'orange'
,
label
:
'Google Gemini'
,
},
{
key
:
15
,
text
:
'百度文心千帆'
,
value
:
15
,
color
:
'blue'
,
label
:
'百度文心千帆'
,
},
{
key
:
17
,
text
:
'阿里通义千问'
,
value
:
17
,
color
:
'orange'
,
label
:
'阿里通义千问'
,
},
{
key
:
18
,
text
:
'讯飞星火认知'
,
value
:
18
,
color
:
'blue'
,
label
:
'讯飞星火认知'
,
},
{
key
:
16
,
text
:
'智谱 ChatGLM'
,
value
:
16
,
color
:
'violet'
,
label
:
'智谱 ChatGLM'
,
},
{
key
:
16
,
text
:
'智谱 GLM-4V'
,
value
:
26
,
color
:
'purple'
,
label
:
'智谱 GLM-4V'
,
},
{
key
:
16
,
text
:
'Moonshot'
,
value
:
25
,
color
:
'green'
,
label
:
'Moonshot'
},
{
key
:
19
,
text
:
'360 智脑'
,
value
:
19
,
color
:
'blue'
,
label
:
'360 智脑'
},
{
key
:
23
,
text
:
'腾讯混元'
,
value
:
23
,
color
:
'teal'
,
label
:
'腾讯混元'
},
{
key
:
31
,
text
:
'零一万物'
,
value
:
31
,
color
:
'green'
,
label
:
'零一万物'
},
{
key
:
8
,
text
:
'自定义渠道'
,
value
:
8
,
color
:
'pink'
,
label
:
'自定义渠道'
},
{
key
:
22
,
text
:
'知识库:FastGPT'
,
value
:
22
,
color
:
'blue'
,
label
:
'知识库:FastGPT'
,
},
{
key
:
21
,
text
:
'知识库:AI Proxy'
,
value
:
21
,
color
:
'purple'
,
label
:
'知识库:AI Proxy'
,
},
];
web/src/constants/index.js
View file @
eb955ae4
export
*
from
'./toast.constants'
;
export
*
from
'./user.constants'
;
export
*
from
'./common.constant'
;
export
*
from
'./channel.constants'
;
\ No newline at end of file
export
*
from
'./channel.constants'
;
web/src/constants/toast.constants.js
View file @
eb955ae4
...
...
@@ -3,5 +3,5 @@ export const toastConstants = {
INFO_TIMEOUT
:
3000
,
ERROR_TIMEOUT
:
5000
,
WARNING_TIMEOUT
:
10000
,
NOTICE_TIMEOUT
:
20000
NOTICE_TIMEOUT
:
20000
,
};
web/src/constants/user.constants.js
View file @
eb955ae4
export
const
userConstants
=
{
REGISTER_REQUEST
:
'USERS_REGISTER_REQUEST'
,
REGISTER_SUCCESS
:
'USERS_REGISTER_SUCCESS'
,
REGISTER_FAILURE
:
'USERS_REGISTER_FAILURE'
,
REGISTER_REQUEST
:
'USERS_REGISTER_REQUEST'
,
REGISTER_SUCCESS
:
'USERS_REGISTER_SUCCESS'
,
REGISTER_FAILURE
:
'USERS_REGISTER_FAILURE'
,
LOGIN_REQUEST
:
'USERS_LOGIN_REQUEST'
,
LOGIN_SUCCESS
:
'USERS_LOGIN_SUCCESS'
,
LOGIN_FAILURE
:
'USERS_LOGIN_FAILURE'
,
LOGOUT
:
'USERS_LOGOUT'
,
LOGIN_REQUEST
:
'USERS_LOGIN_REQUEST'
,
LOGIN_SUCCESS
:
'USERS_LOGIN_SUCCESS'
,
LOGIN_FAILURE
:
'USERS_LOGIN_FAILURE'
,
GETALL_REQUEST
:
'USERS_GETALL_REQUEST'
,
GETALL_SUCCESS
:
'USERS_GETALL_SUCCESS'
,
GETALL_FAILURE
:
'USERS_GETALL_FAILURE'
,
LOGOUT
:
'USERS_LOGOUT'
,
DELETE_REQUEST
:
'USERS_DELETE_REQUEST'
,
DELETE_SUCCESS
:
'USERS_DELETE_SUCCESS'
,
DELETE_FAILURE
:
'USERS_DELETE_FAILURE'
GETALL_REQUEST
:
'USERS_GETALL_REQUEST'
,
GETALL_SUCCESS
:
'USERS_GETALL_SUCCESS'
,
GETALL_FAILURE
:
'USERS_GETALL_FAILURE'
,
DELETE_REQUEST
:
'USERS_DELETE_REQUEST'
,
DELETE_SUCCESS
:
'USERS_DELETE_SUCCESS'
,
DELETE_FAILURE
:
'USERS_DELETE_FAILURE'
,
};
web/src/context/Status/index.js
View file @
eb955ae4
...
...
@@ -16,4 +16,4 @@ export const StatusProvider = ({ children }) => {
{
children
}
<
/StatusContext.Provider
>
);
};
\ No newline at end of file
};
web/src/context/User/index.js
View file @
eb955ae4
// contexts/User/index.jsx
import
React
from
"react"
import
{
reducer
,
initialState
}
from
"./reducer"
import
React
from
'react'
;
import
{
reducer
,
initialState
}
from
'./reducer'
;
export
const
UserContext
=
React
.
createContext
({
state
:
initialState
,
dispatch
:
()
=>
null
})
dispatch
:
()
=>
null
,
})
;
export
const
UserProvider
=
({
children
})
=>
{
const
[
state
,
dispatch
]
=
React
.
useReducer
(
reducer
,
initialState
)
const
[
state
,
dispatch
]
=
React
.
useReducer
(
reducer
,
initialState
)
;
return
(
<
UserContext
.
Provider
value
=
{[
state
,
dispatch
]}
>
{
children
}
<
UserContext
.
Provider
value
=
{[
state
,
dispatch
]}
>
{
children
}
<
/UserContext.Provider
>
)
}
\ No newline at end of file
);
};
web/src/context/User/reducer.js
View file @
eb955ae4
...
...
@@ -3,12 +3,12 @@ export const reducer = (state, action) => {
case
'login'
:
return
{
...
state
,
user
:
action
.
payload
user
:
action
.
payload
,
};
case
'logout'
:
return
{
...
state
,
user
:
undefined
user
:
undefined
,
};
default
:
...
...
@@ -17,5 +17,5 @@ export const reducer = (state, action) => {
};
export
const
initialState
=
{
user
:
undefined
};
\ No newline at end of file
user
:
undefined
,
};
web/src/helpers/api.js
View file @
eb955ae4
...
...
@@ -2,12 +2,14 @@ import { showError } from './utils';
import
axios
from
'axios'
;
export
const
API
=
axios
.
create
({
baseURL
:
import
.
meta
.
env
.
VITE_REACT_APP_SERVER_URL
?
import
.
meta
.
env
.
VITE_REACT_APP_SERVER_URL
:
''
,
baseURL
:
import
.
meta
.
env
.
VITE_REACT_APP_SERVER_URL
?
import
.
meta
.
env
.
VITE_REACT_APP_SERVER_URL
:
''
,
});
API
.
interceptors
.
response
.
use
(
(
response
)
=>
response
,
(
error
)
=>
{
showError
(
error
);
}
}
,
);
web/src/helpers/auth-header.js
View file @
eb955ae4
export
function
authHeader
()
{
// return authorization header with jwt token
let
user
=
JSON
.
parse
(
localStorage
.
getItem
(
'user'
));
// return authorization header with jwt token
let
user
=
JSON
.
parse
(
localStorage
.
getItem
(
'user'
));
if
(
user
&&
user
.
token
)
{
return
{
'Authorization'
:
'Bearer '
+
user
.
token
};
}
else
{
return
{};
}
}
\ No newline at end of file
if
(
user
&&
user
.
token
)
{
return
{
Authorization
:
'Bearer '
+
user
.
token
};
}
else
{
return
{};
}
}
web/src/helpers/history.js
View file @
eb955ae4
import
{
createBrowserHistory
}
from
'history'
;
export
const
history
=
createBrowserHistory
();
\ No newline at end of file
export
const
history
=
createBrowserHistory
();
web/src/helpers/index.js
View file @
eb955ae4
export
*
from
'./history'
;
export
*
from
'./auth-header'
;
export
*
from
'./utils'
;
export
*
from
'./api'
;
\ No newline at end of file
export
*
from
'./api'
;
web/src/helpers/render.js
View file @
eb955ae4
import
{
Label
}
from
'semantic-ui-react'
;
import
{
Tag
}
from
"@douyinfe/semi-ui"
;
import
{
Label
}
from
'semantic-ui-react'
;
import
{
Tag
}
from
'@douyinfe/semi-ui'
;
export
function
renderText
(
text
,
limit
)
{
if
(
text
.
length
>
limit
)
{
return
text
.
slice
(
0
,
limit
-
3
)
+
'...'
;
}
return
text
;
if
(
text
.
length
>
limit
)
{
return
text
.
slice
(
0
,
limit
-
3
)
+
'...'
;
}
return
text
;
}
export
function
renderGroup
(
group
)
{
if
(
group
===
''
)
{
return
<
Tag
size
=
'large'
>
default
<
/Tag>
;
}
let
groups
=
group
.
split
(
','
);
groups
.
sort
();
return
<>
{
groups
.
map
((
group
)
=>
{
if
(
group
===
'vip'
||
group
===
'pro'
)
{
return
<
Tag
size
=
'large'
color
=
'yellow'
>
{
group
}
<
/Tag>
;
}
else
if
(
group
===
'svip'
||
group
===
'premium'
)
{
return
<
Tag
size
=
'large'
color
=
'red'
>
{
group
}
<
/Tag>
;
}
if
(
group
===
'default'
)
{
return
<
Tag
size
=
'large'
>
{
group
}
<
/Tag>
;
}
else
{
return
<
Tag
size
=
'large'
color
=
{
stringToColor
(
group
)}
>
{
group
}
<
/Tag>
;
}
})}
<
/>
;
if
(
group
===
''
)
{
return
<
Tag
size
=
'large'
>
default
<
/Tag>
;
}
let
groups
=
group
.
split
(
','
);
groups
.
sort
();
return
(
<>
{
groups
.
map
((
group
)
=>
{
if
(
group
===
'vip'
||
group
===
'pro'
)
{
return
(
<
Tag
size
=
'large'
color
=
'yellow'
>
{
group
}
<
/Tag
>
);
}
else
if
(
group
===
'svip'
||
group
===
'premium'
)
{
return
(
<
Tag
size
=
'large'
color
=
'red'
>
{
group
}
<
/Tag
>
);
}
if
(
group
===
'default'
)
{
return
<
Tag
size
=
'large'
>
{
group
}
<
/Tag>
;
}
else
{
return
(
<
Tag
size
=
'large'
color
=
{
stringToColor
(
group
)}
>
{
group
}
<
/Tag
>
);
}
})}
<
/
>
);
}
export
function
renderNumber
(
num
)
{
if
(
num
>=
1000000000
)
{
return
(
num
/
1000000000
).
toFixed
(
1
)
+
'B'
;
}
else
if
(
num
>=
1000000
)
{
return
(
num
/
1000000
).
toFixed
(
1
)
+
'M'
;
}
else
if
(
num
>=
10000
)
{
return
(
num
/
1000
).
toFixed
(
1
)
+
'k'
;
}
else
{
return
num
;
}
if
(
num
>=
1000000000
)
{
return
(
num
/
1000000000
).
toFixed
(
1
)
+
'B'
;
}
else
if
(
num
>=
1000000
)
{
return
(
num
/
1000000
).
toFixed
(
1
)
+
'M'
;
}
else
if
(
num
>=
10000
)
{
return
(
num
/
1000
).
toFixed
(
1
)
+
'k'
;
}
else
{
return
num
;
}
}
export
function
renderQuotaNumberWithDigit
(
num
,
digits
=
2
)
{
let
displayInCurrency
=
localStorage
.
getItem
(
'display_in_currency'
);
num
=
num
.
toFixed
(
digits
);
if
(
displayInCurrency
)
{
return
'$'
+
num
;
}
return
num
;
let
displayInCurrency
=
localStorage
.
getItem
(
'display_in_currency'
);
num
=
num
.
toFixed
(
digits
);
if
(
displayInCurrency
)
{
return
'$'
+
num
;
}
return
num
;
}
export
function
renderNumberWithPoint
(
num
)
{
num
=
num
.
toFixed
(
2
);
if
(
num
>=
100000
)
{
// Convert number to string to manipulate it
let
numStr
=
num
.
toString
();
// Find the position of the decimal point
let
decimalPointIndex
=
numStr
.
indexOf
(
'.'
);
let
wholePart
=
numStr
;
let
decimalPart
=
''
;
// If there is a decimal point, split the number into whole and decimal parts
if
(
decimalPointIndex
!==
-
1
)
{
wholePart
=
numStr
.
slice
(
0
,
decimalPointIndex
);
decimalPart
=
numStr
.
slice
(
decimalPointIndex
);
}
num
=
num
.
toFixed
(
2
);
if
(
num
>=
100000
)
{
// Convert number to string to manipulate it
let
numStr
=
num
.
toString
();
// Find the position of the decimal point
let
decimalPointIndex
=
numStr
.
indexOf
(
'.'
);
let
wholePart
=
numStr
;
let
decimalPart
=
''
;
// If there is a decimal point, split the number into whole and decimal parts
if
(
decimalPointIndex
!==
-
1
)
{
wholePart
=
numStr
.
slice
(
0
,
decimalPointIndex
);
decimalPart
=
numStr
.
slice
(
decimalPointIndex
);
}
// Take the first two and last two digits of the whole number part
let
shortenedWholePart
=
wholePart
.
slice
(
0
,
2
)
+
'..'
+
wholePart
.
slice
(
-
2
);
// Take the first two and last two digits of the whole number part
let
shortenedWholePart
=
wholePart
.
slice
(
0
,
2
)
+
'..'
+
wholePart
.
slice
(
-
2
);
// Return the formatted number
return
shortenedWholePart
+
decimalPart
;
}
// Return the formatted number
return
shortenedWholePart
+
decimalPart
;
}
// If the number is less than 100,000, return it unmodified
return
num
;
// If the number is less than 100,000, return it unmodified
return
num
;
}
export
function
getQuotaPerUnit
()
{
let
quotaPerUnit
=
localStorage
.
getItem
(
'quota_per_unit'
);
quotaPerUnit
=
parseFloat
(
quotaPerUnit
);
return
quotaPerUnit
;
let
quotaPerUnit
=
localStorage
.
getItem
(
'quota_per_unit'
);
quotaPerUnit
=
parseFloat
(
quotaPerUnit
);
return
quotaPerUnit
;
}
export
function
getQuotaWithUnit
(
quota
,
digits
=
6
)
{
let
quotaPerUnit
=
localStorage
.
getItem
(
'quota_per_unit'
);
quotaPerUnit
=
parseFloat
(
quotaPerUnit
);
return
(
quota
/
quotaPerUnit
).
toFixed
(
digits
);
let
quotaPerUnit
=
localStorage
.
getItem
(
'quota_per_unit'
);
quotaPerUnit
=
parseFloat
(
quotaPerUnit
);
return
(
quota
/
quotaPerUnit
).
toFixed
(
digits
);
}
export
function
renderQuota
(
quota
,
digits
=
2
)
{
let
quotaPerUnit
=
localStorage
.
getItem
(
'quota_per_unit'
);
let
displayInCurrency
=
localStorage
.
getItem
(
'display_in_currency'
);
quotaPerUnit
=
parseFloat
(
quotaPerUnit
);
displayInCurrency
=
displayInCurrency
===
'true'
;
if
(
displayInCurrency
)
{
return
'$'
+
(
quota
/
quotaPerUnit
).
toFixed
(
digits
);
}
return
renderNumber
(
quota
);
let
quotaPerUnit
=
localStorage
.
getItem
(
'quota_per_unit'
);
let
displayInCurrency
=
localStorage
.
getItem
(
'display_in_currency'
);
quotaPerUnit
=
parseFloat
(
quotaPerUnit
);
displayInCurrency
=
displayInCurrency
===
'true'
;
if
(
displayInCurrency
)
{
return
'$'
+
(
quota
/
quotaPerUnit
).
toFixed
(
digits
);
}
return
renderNumber
(
quota
);
}
export
function
renderQuotaWithPrompt
(
quota
,
digits
)
{
let
displayInCurrency
=
localStorage
.
getItem
(
'display_in_currency'
);
displayInCurrency
=
displayInCurrency
===
'true'
;
if
(
displayInCurrency
)
{
return
`(等价金额:
${
renderQuota
(
quota
,
digits
)}
)`
;
}
return
''
;
let
displayInCurrency
=
localStorage
.
getItem
(
'display_in_currency'
);
displayInCurrency
=
displayInCurrency
===
'true'
;
if
(
displayInCurrency
)
{
return
`(等价金额:
${
renderQuota
(
quota
,
digits
)}
)`
;
}
return
''
;
}
const
colors
=
[
'amber'
,
'blue'
,
'cyan'
,
'green'
,
'grey'
,
'indigo'
,
'light-blue'
,
'lime'
,
'orange'
,
'pink'
,
'purple'
,
'red'
,
'teal'
,
'violet'
,
'yellow'
]
const
colors
=
[
'amber'
,
'blue'
,
'cyan'
,
'green'
,
'grey'
,
'indigo'
,
'light-blue'
,
'lime'
,
'orange'
,
'pink'
,
'purple'
,
'red'
,
'teal'
,
'violet'
,
'yellow'
,
];
export
const
modelColorMap
=
{
'dall-e'
:
'rgb(147,112,219)'
,
// 深紫色
'dall-e-2'
:
'rgb(147,112,219)'
,
// 介于紫色和蓝色之间的色调
'dall-e-3'
:
'rgb(153,50,204)'
,
// 介于紫罗兰和洋红之间的色调
'midjourney'
:
'rgb(136,43,180)'
,
// 介于紫罗兰和洋红之间的色调
'gpt-3.5-turbo'
:
'rgb(184,227,167)'
,
// 浅绿色
'gpt-3.5-turbo-0301'
:
'rgb(131,220,131)'
,
// 亮绿色
'gpt-3.5-turbo-0613'
:
'rgb(60,179,113)'
,
// 海洋绿
'gpt-3.5-turbo-1106'
:
'rgb(32,178,170)'
,
// 浅海洋绿
'gpt-3.5-turbo-16k'
:
'rgb(252,200,149)'
,
// 淡橙色
'gpt-3.5-turbo-16k-0613'
:
'rgb(255,181,119)'
,
// 淡桃色
'gpt-3.5-turbo-instruct'
:
'rgb(175,238,238)'
,
// 粉蓝色
'gpt-4'
:
'rgb(135,206,235)'
,
// 天蓝色
'gpt-4-0314'
:
'rgb(70,130,180)'
,
// 钢蓝色
'gpt-4-0613'
:
'rgb(100,149,237)'
,
// 矢车菊蓝
'gpt-4-1106-preview'
:
'rgb(30,144,255)'
,
// 道奇蓝
'gpt-4-0125-preview'
:
'rgb(2,177,236)'
,
// 深天蓝
'gpt-4-turbo-preview'
:
'rgb(2,177,255)'
,
// 深天蓝
'gpt-4-32k'
:
'rgb(104,111,238)'
,
// 中紫色
'gpt-4-32k-0314'
:
'rgb(90,105,205)'
,
// 暗灰蓝色
'gpt-4-32k-0613'
:
'rgb(61,71,139)'
,
// 暗蓝灰色
'gpt-4-all'
:
'rgb(65,105,225)'
,
// 皇家蓝
'gpt-4-gizmo-*'
:
'rgb(0,0,255)'
,
// 纯蓝色
'gpt-4-vision-preview'
:
'rgb(25,25,112)'
,
// 午夜蓝
'text-ada-001'
:
'rgb(255,192,203)'
,
// 粉红色
'text-babbage-001'
:
'rgb(255,160,122)'
,
// 浅珊瑚色
'text-curie-001'
:
'rgb(219,112,147)'
,
// 苍紫罗兰色
'text-davinci-002'
:
'rgb(199,21,133)'
,
// 中紫罗兰红色
'text-davinci-003'
:
'rgb(219,112,147)'
,
// 苍紫罗兰色(与Curie相同,表示同一个系列)
'text-davinci-edit-001'
:
'rgb(255,105,180)'
,
// 热粉色
'text-embedding-ada-002'
:
'rgb(255,182,193)'
,
// 浅粉红
'text-embedding-v1'
:
'rgb(255,174,185)'
,
// 浅粉红色(略有区别)
'text-moderation-latest'
:
'rgb(255,130,171)'
,
// 强粉色
'text-moderation-stable'
:
'rgb(255,160,122)'
,
// 浅珊瑚色(与Babbage相同,表示同一类功能)
'tts-1'
:
'rgb(255,140,0)'
,
// 深橙色
'tts-1-1106'
:
'rgb(255,165,0)'
,
// 橙色
'tts-1-hd'
:
'rgb(255,215,0)'
,
// 金色
'tts-1-hd-1106'
:
'rgb(255,223,0)'
,
// 金黄色(略有区别)
'whisper-1'
:
'rgb(245,245,220)'
// 米色
}
'dall-e'
:
'rgb(147,112,219)'
,
// 深紫色
'dall-e-2'
:
'rgb(147,112,219)'
,
// 介于紫色和蓝色之间的色调
'dall-e-3'
:
'rgb(153,50,204)'
,
// 介于紫罗兰和洋红之间的色调
midjourney
:
'rgb(136,43,180)'
,
// 介于紫罗兰和洋红之间的色调
'gpt-3.5-turbo'
:
'rgb(184,227,167)'
,
// 浅绿色
'gpt-3.5-turbo-0301'
:
'rgb(131,220,131)'
,
// 亮绿色
'gpt-3.5-turbo-0613'
:
'rgb(60,179,113)'
,
// 海洋绿
'gpt-3.5-turbo-1106'
:
'rgb(32,178,170)'
,
// 浅海洋绿
'gpt-3.5-turbo-16k'
:
'rgb(252,200,149)'
,
// 淡橙色
'gpt-3.5-turbo-16k-0613'
:
'rgb(255,181,119)'
,
// 淡桃色
'gpt-3.5-turbo-instruct'
:
'rgb(175,238,238)'
,
// 粉蓝色
'gpt-4'
:
'rgb(135,206,235)'
,
// 天蓝色
'gpt-4-0314'
:
'rgb(70,130,180)'
,
// 钢蓝色
'gpt-4-0613'
:
'rgb(100,149,237)'
,
// 矢车菊蓝
'gpt-4-1106-preview'
:
'rgb(30,144,255)'
,
// 道奇蓝
'gpt-4-0125-preview'
:
'rgb(2,177,236)'
,
// 深天蓝
'gpt-4-turbo-preview'
:
'rgb(2,177,255)'
,
// 深天蓝
'gpt-4-32k'
:
'rgb(104,111,238)'
,
// 中紫色
'gpt-4-32k-0314'
:
'rgb(90,105,205)'
,
// 暗灰蓝色
'gpt-4-32k-0613'
:
'rgb(61,71,139)'
,
// 暗蓝灰色
'gpt-4-all'
:
'rgb(65,105,225)'
,
// 皇家蓝
'gpt-4-gizmo-*'
:
'rgb(0,0,255)'
,
// 纯蓝色
'gpt-4-vision-preview'
:
'rgb(25,25,112)'
,
// 午夜蓝
'text-ada-001'
:
'rgb(255,192,203)'
,
// 粉红色
'text-babbage-001'
:
'rgb(255,160,122)'
,
// 浅珊瑚色
'text-curie-001'
:
'rgb(219,112,147)'
,
// 苍紫罗兰色
'text-davinci-002'
:
'rgb(199,21,133)'
,
// 中紫罗兰红色
'text-davinci-003'
:
'rgb(219,112,147)'
,
// 苍紫罗兰色(与Curie相同,表示同一个系列)
'text-davinci-edit-001'
:
'rgb(255,105,180)'
,
// 热粉色
'text-embedding-ada-002'
:
'rgb(255,182,193)'
,
// 浅粉红
'text-embedding-v1'
:
'rgb(255,174,185)'
,
// 浅粉红色(略有区别)
'text-moderation-latest'
:
'rgb(255,130,171)'
,
// 强粉色
'text-moderation-stable'
:
'rgb(255,160,122)'
,
// 浅珊瑚色(与Babbage相同,表示同一类功能)
'tts-1'
:
'rgb(255,140,0)'
,
// 深橙色
'tts-1-1106'
:
'rgb(255,165,0)'
,
// 橙色
'tts-1-hd'
:
'rgb(255,215,0)'
,
// 金色
'tts-1-hd-1106'
:
'rgb(255,223,0)'
,
// 金黄色(略有区别)
'whisper-1'
:
'rgb(245,245,220)'
,
// 米色
}
;
export
function
stringToColor
(
str
)
{
let
sum
=
0
;
// 对字符串中的每个字符进行操作
for
(
let
i
=
0
;
i
<
str
.
length
;
i
++
)
{
// 将字符的ASCII值加到sum中
sum
+=
str
.
charCodeAt
(
i
);
}
// 使用模运算得到个位数
let
i
=
sum
%
colors
.
length
;
return
colors
[
i
];
}
\ No newline at end of file
let
sum
=
0
;
// 对字符串中的每个字符进行操作
for
(
let
i
=
0
;
i
<
str
.
length
;
i
++
)
{
// 将字符的ASCII值加到sum中
sum
+=
str
.
charCodeAt
(
i
);
}
// 使用模运算得到个位数
let
i
=
sum
%
colors
.
length
;
return
colors
[
i
];
}
web/src/helpers/utils.js
View file @
eb955ae4
import
{
Toast
}
from
'@douyinfe/semi-ui'
;
import
{
toastConstants
}
from
'../constants'
;
import
React
from
'react'
;
import
{
toast
}
from
"react-toastify"
;
import
{
toast
}
from
'react-toastify'
;
const
HTMLToastContent
=
({
htmlContent
})
=>
{
return
<
div
dangerouslySetInnerHTML
=
{{
__html
:
htmlContent
}}
/>
;
...
...
@@ -30,7 +30,7 @@ export function getSystemName() {
export
function
getLogo
()
{
let
logo
=
localStorage
.
getItem
(
'logo'
);
if
(
!
logo
)
return
'/logo.png'
;
return
logo
return
logo
;
}
export
function
getFooterHTML
()
{
...
...
@@ -157,17 +157,7 @@ export function timestamp2string(timestamp) {
second
=
'0'
+
second
;
}
return
(
year
+
'-'
+
month
+
'-'
+
day
+
' '
+
hour
+
':'
+
minute
+
':'
+
second
year
+
'-'
+
month
+
'-'
+
day
+
' '
+
hour
+
':'
+
minute
+
':'
+
second
);
}
...
...
@@ -186,20 +176,20 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') {
if
(
hour
.
length
===
1
)
{
hour
=
'0'
+
hour
;
}
let
str
=
month
+
'-'
+
day
let
str
=
month
+
'-'
+
day
;
if
(
dataExportDefaultTime
===
'hour'
)
{
str
+=
' '
+
hour
+
":00"
str
+=
' '
+
hour
+
':00'
;
}
else
if
(
dataExportDefaultTime
===
'week'
)
{
let
nextWeek
=
new
Date
(
timestamp
*
1000
+
6
*
24
*
60
*
60
*
1000
);
let
nextMonth
=
(
nextWeek
.
getMonth
()
+
1
).
toString
();
let
nextDay
=
nextWeek
.
getDate
().
toString
();
if
(
nextMonth
.
length
===
1
)
{
nextMonth
=
'0'
+
nextMonth
;
nextMonth
=
'0'
+
nextMonth
;
}
if
(
nextDay
.
length
===
1
)
{
nextDay
=
'0'
+
nextDay
;
nextDay
=
'0'
+
nextDay
;
}
str
+=
' - '
+
nextMonth
+
'-'
+
nextDay
str
+=
' - '
+
nextMonth
+
'-'
+
nextDay
;
}
return
str
;
}
...
...
@@ -225,9 +215,8 @@ export const verifyJSON = (str) => {
export
function
shouldShowPrompt
(
id
)
{
let
prompt
=
localStorage
.
getItem
(
`prompt-
${
id
}
`
);
return
!
prompt
;
}
export
function
setPromptShown
(
id
)
{
localStorage
.
setItem
(
`prompt-
${
id
}
`
,
'true'
);
}
\ No newline at end of file
}
web/src/index.css
View file @
eb955ae4
body
{
margin
:
0
;
padding-top
:
55px
;
overflow-y
:
scroll
;
font-family
:
Lato
,
'Helvetica Neue'
,
Arial
,
Helvetica
,
"Microsoft YaHei"
,
sans-serif
;
-webkit-font-smoothing
:
antialiased
;
-moz-osx-font-smoothing
:
grayscale
;
scrollbar-width
:
none
;
color
:
var
(
--semi-color-text-0
)
!important
;
background-color
:
var
(
--semi-color-bg-0
)
!important
;
height
:
100%
;
margin
:
0
;
padding-top
:
55px
;
overflow-y
:
scroll
;
font-family
:
Lato
,
'Helvetica Neue'
,
Arial
,
Helvetica
,
'Microsoft YaHei'
,
sans-serif
;
-webkit-font-smoothing
:
antialiased
;
-moz-osx-font-smoothing
:
grayscale
;
scrollbar-width
:
none
;
color
:
var
(
--semi-color-text-0
)
!important
;
background-color
:
var
(
--semi-color-bg-0
)
!important
;
height
:
100%
;
}
#root
{
height
:
100%
;
height
:
100%
;
}
@media
only
screen
and
(
max-width
:
767px
)
{
.semi-table-tbody
,
.semi-table-row
,
.semi-table-row-cell
{
display
:
block
!important
;
width
:
auto
!important
;
padding
:
2px
!important
;
}
.semi-table-row-cell
{
border-bottom
:
0
!important
;
}
.semi-table-tbody
>
.semi-table-row
{
border-bottom
:
1px
solid
rgba
(
0
,
0
,
0
,
.1
);
}
.semi-space
{
/*display: block!important;*/
display
:
flex
;
flex-direction
:
row
;
flex-wrap
:
wrap
;
row-gap
:
3px
;
column-gap
:
10px
;
}
.semi-table-tbody
,
.semi-table-row
,
.semi-table-row-cell
{
display
:
block
!important
;
width
:
auto
!important
;
padding
:
2px
!important
;
}
.semi-table-row-cell
{
border-bottom
:
0
!important
;
}
.semi-table-tbody
>
.semi-table-row
{
border-bottom
:
1px
solid
rgba
(
0
,
0
,
0
,
0.1
);
}
.semi-space
{
/*display: block!important;*/
display
:
flex
;
flex-direction
:
row
;
flex-wrap
:
wrap
;
row-gap
:
3px
;
column-gap
:
10px
;
}
}
.semi-table-tbody
>
.semi-table-row
>
.semi-table-row-cell
{
padding
:
16px
14px
;
padding
:
16px
14px
;
}
.channel-table
{
.semi-table-tbody
>
.semi-table-row
>
.semi-table-row-cell
{
padding
:
16px
8px
;
}
.semi-table-tbody
>
.semi-table-row
>
.semi-table-row-cell
{
padding
:
16px
8px
;
}
}
.semi-layout
{
height
:
100%
;
height
:
100%
;
}
.tableShow
{
display
:
revert
;
display
:
revert
;
}
.tableHiddle
{
display
:
none
!important
;
display
:
none
!important
;
}
body
::-webkit-scrollbar
{
display
:
none
;
display
:
none
;
}
code
{
font-family
:
source-code-pro
,
Menlo
,
Monaco
,
Consolas
,
'Courier New'
,
monospace
;
font-family
:
source-code-pro
,
Menlo
,
Monaco
,
Consolas
,
'Courier New'
,
monospace
;
}
.semi-navigation-vertical
{
/*display: flex;*/
/*flex-direction: column;*/
/*display: flex;*/
/*flex-direction: column;*/
}
.semi-navigation-item
{
margin-bottom
:
0
;
margin-bottom
:
0
;
}
.semi-navigation-vertical
{
/*flex: 0 0 auto;*/
/*display: flex;*/
/*flex-direction: column;*/
/*width: 100%;*/
height
:
100%
;
overflow
:
hidden
;
/*flex: 0 0 auto;*/
/*display: flex;*/
/*flex-direction: column;*/
/*width: 100%;*/
height
:
100%
;
overflow
:
hidden
;
}
.main-content
{
padding
:
4px
;
height
:
100%
;
padding
:
4px
;
height
:
100%
;
}
.small-icon
.icon
{
font-size
:
1em
!important
;
font-size
:
1em
!important
;
}
.custom-footer
{
font-size
:
1.1em
;
font-size
:
1.1em
;
}
@media
only
screen
and
(
max-width
:
600px
)
{
.hide-on-mobile
{
display
:
none
!important
;
}
.hide-on-mobile
{
display
:
none
!important
;
}
}
web/src/index.js
View file @
eb955ae4
import
React
from
'react'
;
import
ReactDOM
from
'react-dom/client'
;
import
{
BrowserRouter
}
from
'react-router-dom'
;
import
{
BrowserRouter
}
from
'react-router-dom'
;
import
App
from
'./App'
;
import
HeaderBar
from
'./components/HeaderBar'
;
import
Footer
from
'./components/Footer'
;
import
'semantic-ui-offline/semantic.min.css'
;
import
'./index.css'
;
import
{
UserProvider
}
from
'./context/User'
;
import
{
ToastContainer
}
from
'react-toastify'
;
import
{
UserProvider
}
from
'./context/User'
;
import
{
ToastContainer
}
from
'react-toastify'
;
import
'react-toastify/dist/ReactToastify.css'
;
import
{
StatusProvider
}
from
'./context/Status'
;
import
{
Layout
}
from
"@douyinfe/semi-ui"
;
import
SiderBar
from
"./components/SiderBar"
;
import
{
StatusProvider
}
from
'./context/Status'
;
import
{
Layout
}
from
'@douyinfe/semi-ui'
;
import
SiderBar
from
'./components/SiderBar'
;
// initialization
const
root
=
ReactDOM
.
createRoot
(
document
.
getElementById
(
'root'
));
const
{
Sider
,
Content
,
Header
}
=
Layout
;
const
{
Sider
,
Content
,
Header
}
=
Layout
;
root
.
render
(
<
React
.
StrictMode
>
<
StatusProvider
>
<
UserProvider
>
<
BrowserRouter
>
<
Layout
>
<
Sider
>
<
SiderBar
/>
<
/Sider
>
<
Layout
>
<
Header
>
<
HeaderBar
/>
<
/Header
>
<
Content
style
=
{{
padding
:
'24px'
,
}}
>
<
App
/>
<
/Content
>
<
Layout
.
Footer
>
<
Footer
><
/Footer
>
<
/Layout.Footer
>
<
/Layout
>
<
ToastContainer
/>
<
/Layout
>
<
/BrowserRouter
>
<
/UserProvider
>
<
/StatusProvider
>
<
/React.StrictMode
>
<
React
.
StrictMode
>
<
StatusProvider
>
<
UserProvider
>
<
BrowserRouter
>
<
Layout
>
<
Sider
>
<
SiderBar
/>
<
/Sider
>
<
Layout
>
<
Header
>
<
HeaderBar
/>
<
/Header
>
<
Content
style
=
{{
padding
:
'24px'
,
}}
>
<
App
/>
<
/Content
>
<
Layout
.
Footer
>
<
Footer
><
/Footer
>
<
/Layout.Footer
>
<
/Layout
>
<
ToastContainer
/>
<
/Layout
>
<
/BrowserRouter
>
<
/UserProvider
>
<
/StatusProvider
>
<
/React.StrictMode>
,
);
web/src/pages/About/index.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
API
,
showError
}
from
'../../helpers'
;
import
{
marked
}
from
'marked'
;
import
{
Layout
}
from
"@douyinfe/semi-ui"
;
import
{
Layout
}
from
'@douyinfe/semi-ui'
;
const
About
=
()
=>
{
const
[
about
,
setAbout
]
=
useState
(
''
);
...
...
@@ -31,37 +31,42 @@ const About = () => {
return
(
<>
{
aboutLoaded
&&
about
===
''
?
<>
{
aboutLoaded
&&
about
===
''
?
(
<>
<
Layout
>
<
Layout
.
Header
>
<
h3
>
关于
<
/h3
>
<
/Layout.Header
>
<
Layout
.
Content
>
<
p
>
可在设置页面设置关于内容,支持
HTML
&
Markdown
<
/p
>
<
p
>
可在设置页面设置关于内容,支持
HTML
&
Markdown
<
/p
>
new
-
api
项目仓库地址:
<
a
href
=
'https://github.com/Calcium-Ion/new-api'
>
https
:
//github.com/Calcium-Ion/new-api
<
/a
>
<
p
>
NewAPI
©
2023
CalciumIon
|
基于
One
API
v0
.
5.4
©
2023
JustSong
。本项目根据
MIT
许可证授权。
NewAPI
©
2023
CalciumIon
|
基于
One
API
v0
.
5.4
©
2023
JustSong
。本项目根据
MIT
许可证授权。
<
/p
>
<
/Layout.Content
>
<
/Layout
>
<
/> : <
>
{
about
.
startsWith
(
'https://'
)
?
<
iframe
<
/
>
)
:
(
<>
{
about
.
startsWith
(
'https://'
)
?
(
<
iframe
src
=
{
about
}
style
=
{{
width
:
'100%'
,
height
:
'100vh'
,
border
:
'none'
}}
/> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></
div
>
}
/
>
)
:
(
<
div
style
=
{{
fontSize
:
'larger'
}}
dangerouslySetInnerHTML
=
{{
__html
:
about
}}
><
/div
>
)}
<
/
>
}
)
}
<
/
>
);
};
export
default
About
;
web/src/pages/Channel/EditChannel.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useRef
,
useState
}
from
'react'
;
import
{
useNavigate
,
useParams
}
from
'react-router-dom'
;
import
{
API
,
isMobile
,
showError
,
showInfo
,
showSuccess
,
verifyJSON
}
from
'../../helpers'
;
import
{
CHANNEL_OPTIONS
}
from
'../../constants'
;
import
Title
from
"@douyinfe/semi-ui/lib/es/typography/title"
;
import
{
SideSheet
,
Space
,
Spin
,
Button
,
Input
,
Typography
,
Select
,
TextArea
,
Checkbox
,
Banner
}
from
"@douyinfe/semi-ui"
;
import
React
,
{
useEffect
,
useRef
,
useState
}
from
'react'
;
import
{
useNavigate
,
useParams
}
from
'react-router-dom'
;
import
{
API
,
isMobile
,
showError
,
showInfo
,
showSuccess
,
verifyJSON
,
}
from
'../../helpers'
;
import
{
CHANNEL_OPTIONS
}
from
'../../constants'
;
import
Title
from
'@douyinfe/semi-ui/lib/es/typography/title'
;
import
{
SideSheet
,
Space
,
Spin
,
Button
,
Input
,
Typography
,
Select
,
TextArea
,
Checkbox
,
Banner
,
}
from
'@douyinfe/semi-ui'
;
const
MODEL_MAPPING_EXAMPLE
=
{
'gpt-3.5-turbo-0301'
:
'gpt-3.5-turbo'
,
'gpt-4-0314'
:
'gpt-4'
,
'gpt-4-32k-0314'
:
'gpt-4-32k'
'gpt-3.5-turbo-0301'
:
'gpt-3.5-turbo'
,
'gpt-4-0314'
:
'gpt-4'
,
'gpt-4-32k-0314'
:
'gpt-4-32k'
,
};
function
type2secretPrompt
(
type
)
{
// inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
switch
(
type
)
{
// inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
switch
(
type
)
{
case
15
:
return
'按照如下格式输入:APIKey|SecretKey'
;
case
18
:
return
'按照如下格式输入:APPID|APISecret|APIKey'
;
case
22
:
return
'按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041'
;
case
23
:
return
'按照如下格式输入:AppId|SecretId|SecretKey'
;
default
:
return
'请输入渠道对应的鉴权密钥'
;
}
}
const
EditChannel
=
(
props
)
=>
{
const
navigate
=
useNavigate
();
const
channelId
=
props
.
editingChannel
.
id
;
const
isEdit
=
channelId
!==
undefined
;
const
[
loading
,
setLoading
]
=
useState
(
isEdit
);
const
handleCancel
=
()
=>
{
props
.
handleClose
();
};
const
originInputs
=
{
name
:
''
,
type
:
1
,
key
:
''
,
openai_organization
:
''
,
base_url
:
''
,
other
:
''
,
model_mapping
:
''
,
models
:
[],
auto_ban
:
1
,
groups
:
[
'default'
],
};
const
[
batch
,
setBatch
]
=
useState
(
false
);
const
[
autoBan
,
setAutoBan
]
=
useState
(
true
);
// const [autoBan, setAutoBan] = useState(true);
const
[
inputs
,
setInputs
]
=
useState
(
originInputs
);
const
[
originModelOptions
,
setOriginModelOptions
]
=
useState
([]);
const
[
modelOptions
,
setModelOptions
]
=
useState
([]);
const
[
groupOptions
,
setGroupOptions
]
=
useState
([]);
const
[
basicModels
,
setBasicModels
]
=
useState
([]);
const
[
fullModels
,
setFullModels
]
=
useState
([]);
const
[
customModel
,
setCustomModel
]
=
useState
(
''
);
const
handleInputChange
=
(
name
,
value
)
=>
{
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
if
(
name
===
'type'
&&
inputs
.
models
.
length
===
0
)
{
let
localModels
=
[];
switch
(
value
)
{
case
14
:
localModels
=
[
'claude-instant-1.2'
,
'claude-2'
,
'claude-2.0'
,
'claude-2.1'
,
'claude-3-opus-20240229'
,
'claude-3-sonnet-20240229'
,
'claude-3-haiku-20240307'
,
];
break
;
case
11
:
localModels
=
[
'PaLM-2'
];
break
;
case
15
:
return
'按照如下格式输入:APIKey|SecretKey'
;
localModels
=
[
'ERNIE-Bot'
,
'ERNIE-Bot-turbo'
,
'ERNIE-Bot-4'
,
'Embedding-V1'
,
];
break
;
case
17
:
localModels
=
[
'qwen-turbo'
,
'qwen-plus'
,
'qwen-max'
,
'qwen-max-longcontext'
,
'text-embedding-v1'
,
];
break
;
case
16
:
localModels
=
[
'chatglm_pro'
,
'chatglm_std'
,
'chatglm_lite'
];
break
;
case
18
:
return
'按照如下格式输入:APPID|APISecret|APIKey'
;
case
22
:
return
'按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041'
;
localModels
=
[
'SparkDesk'
,
'SparkDesk-v1.1'
,
'SparkDesk-v2.1'
,
'SparkDesk-v3.1'
,
'SparkDesk-v3.5'
,
];
break
;
case
19
:
localModels
=
[
'360GPT_S2_V9'
,
'embedding-bert-512-v1'
,
'embedding_s1_v1'
,
'semantic_similarity_s1_v1'
,
];
break
;
case
23
:
return
'按照如下格式输入:AppId|SecretId|SecretKey'
;
default
:
return
'请输入渠道对应的鉴权密钥'
;
localModels
=
[
'hunyuan'
];
break
;
case
24
:
localModels
=
[
'gemini-pro'
,
'gemini-pro-vision'
];
break
;
case
25
:
localModels
=
[
'moonshot-v1-8k'
,
'moonshot-v1-32k'
,
'moonshot-v1-128k'
,
];
break
;
case
26
:
localModels
=
[
'glm-4'
,
'glm-4v'
,
'glm-3-turbo'
];
break
;
case
31
:
localModels
=
[
'yi-34b-chat-0205'
,
'yi-34b-chat-200k'
,
'yi-vl-plus'
];
break
;
case
2
:
localModels
=
[
'mj_imagine'
,
'mj_variation'
,
'mj_reroll'
,
'mj_blend'
,
'mj_upscale'
,
'mj_describe'
,
];
break
;
case
5
:
localModels
=
[
'swap_face'
,
'mj_imagine'
,
'mj_variation'
,
'mj_reroll'
,
'mj_blend'
,
'mj_upscale'
,
'mj_describe'
,
'mj_zoom'
,
'mj_shorten'
,
'mj_modal'
,
'mj_inpaint'
,
'mj_custom_zoom'
,
'mj_high_variation'
,
'mj_low_variation'
,
'mj_pan'
,
];
break
;
}
setInputs
((
inputs
)
=>
({
...
inputs
,
models
:
localModels
}));
}
}
//setAutoBan
};
const
EditChannel
=
(
props
)
=>
{
const
navigate
=
useNavigate
();
const
channelId
=
props
.
editingChannel
.
id
;
const
isEdit
=
channelId
!==
undefined
;
const
[
loading
,
setLoading
]
=
useState
(
isEdit
);
const
handleCancel
=
()
=>
{
props
.
handleClose
()
};
const
originInputs
=
{
name
:
''
,
type
:
1
,
key
:
''
,
openai_organization
:
''
,
base_url
:
''
,
other
:
''
,
model_mapping
:
''
,
models
:
[],
auto_ban
:
1
,
groups
:
[
'default'
]
};
const
[
batch
,
setBatch
]
=
useState
(
false
);
const
[
autoBan
,
setAutoBan
]
=
useState
(
true
);
// const [autoBan, setAutoBan] = useState(true);
const
[
inputs
,
setInputs
]
=
useState
(
originInputs
);
const
[
originModelOptions
,
setOriginModelOptions
]
=
useState
([]);
const
[
modelOptions
,
setModelOptions
]
=
useState
([]);
const
[
groupOptions
,
setGroupOptions
]
=
useState
([]);
const
[
basicModels
,
setBasicModels
]
=
useState
([]);
const
[
fullModels
,
setFullModels
]
=
useState
([]);
const
[
customModel
,
setCustomModel
]
=
useState
(
''
);
const
handleInputChange
=
(
name
,
value
)
=>
{
setInputs
((
inputs
)
=>
({...
inputs
,
[
name
]:
value
}));
if
(
name
===
'type'
&&
inputs
.
models
.
length
===
0
)
{
let
localModels
=
[];
switch
(
value
)
{
case
14
:
localModels
=
[
"claude-instant-1.2"
,
"claude-2"
,
"claude-2.0"
,
"claude-2.1"
,
"claude-3-opus-20240229"
,
"claude-3-sonnet-20240229"
,
"claude-3-haiku-20240307"
];
break
;
case
11
:
localModels
=
[
'PaLM-2'
];
break
;
case
15
:
localModels
=
[
'ERNIE-Bot'
,
'ERNIE-Bot-turbo'
,
'ERNIE-Bot-4'
,
'Embedding-V1'
];
break
;
case
17
:
localModels
=
[
"qwen-turbo"
,
"qwen-plus"
,
"qwen-max"
,
"qwen-max-longcontext"
,
'text-embedding-v1'
];
break
;
case
16
:
localModels
=
[
'chatglm_pro'
,
'chatglm_std'
,
'chatglm_lite'
];
break
;
case
18
:
localModels
=
[
'SparkDesk'
,
'SparkDesk-v1.1'
,
'SparkDesk-v2.1'
,
'SparkDesk-v3.1'
,
'SparkDesk-v3.5'
];
break
;
case
19
:
localModels
=
[
'360GPT_S2_V9'
,
'embedding-bert-512-v1'
,
'embedding_s1_v1'
,
'semantic_similarity_s1_v1'
];
break
;
case
23
:
localModels
=
[
'hunyuan'
];
break
;
case
24
:
localModels
=
[
'gemini-pro'
,
'gemini-pro-vision'
];
break
;
case
25
:
localModels
=
[
'moonshot-v1-8k'
,
'moonshot-v1-32k'
,
'moonshot-v1-128k'
];
break
;
case
26
:
localModels
=
[
'glm-4'
,
'glm-4v'
,
'glm-3-turbo'
];
break
;
case
31
:
localModels
=
[
'yi-34b-chat-0205'
,
'yi-34b-chat-200k'
,
'yi-vl-plus'
];
break
;
case
2
:
localModels
=
[
'mj_imagine'
,
'mj_variation'
,
'mj_reroll'
,
'mj_blend'
,
'mj_upscale'
,
'mj_describe'
];
break
;
case
5
:
localModels
=
[
'swap_face'
,
'mj_imagine'
,
'mj_variation'
,
'mj_reroll'
,
'mj_blend'
,
'mj_upscale'
,
'mj_describe'
,
'mj_zoom'
,
'mj_shorten'
,
'mj_modal'
,
'mj_inpaint'
,
'mj_custom_zoom'
,
'mj_high_variation'
,
'mj_low_variation'
,
'mj_pan'
,
];
break
;
}
setInputs
((
inputs
)
=>
({...
inputs
,
models
:
localModels
}));
}
//setAutoBan
};
const
loadChannel
=
async
()
=>
{
setLoading
(
true
)
let
res
=
await
API
.
get
(
`/api/channel/
${
channelId
}
`
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
if
(
data
.
models
===
''
)
{
data
.
models
=
[];
}
else
{
data
.
models
=
data
.
models
.
split
(
','
);
}
if
(
data
.
group
===
''
)
{
data
.
groups
=
[];
}
else
{
data
.
groups
=
data
.
group
.
split
(
','
);
}
if
(
data
.
model_mapping
!==
''
)
{
data
.
model_mapping
=
JSON
.
stringify
(
JSON
.
parse
(
data
.
model_mapping
),
null
,
2
);
}
setInputs
(
data
);
if
(
data
.
auto_ban
===
0
)
{
setAutoBan
(
false
);
}
else
{
setAutoBan
(
true
);
}
// console.log(data);
}
else
{
showError
(
message
);
}
setLoading
(
false
);
};
const
loadChannel
=
async
()
=>
{
setLoading
(
true
);
let
res
=
await
API
.
get
(
`/api/channel/
${
channelId
}
`
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
if
(
data
.
models
===
''
)
{
data
.
models
=
[];
}
else
{
data
.
models
=
data
.
models
.
split
(
','
);
}
if
(
data
.
group
===
''
)
{
data
.
groups
=
[];
}
else
{
data
.
groups
=
data
.
group
.
split
(
','
);
}
if
(
data
.
model_mapping
!==
''
)
{
data
.
model_mapping
=
JSON
.
stringify
(
JSON
.
parse
(
data
.
model_mapping
),
null
,
2
,
);
}
setInputs
(
data
);
if
(
data
.
auto_ban
===
0
)
{
setAutoBan
(
false
);
}
else
{
setAutoBan
(
true
);
}
// console.log(data);
}
else
{
showError
(
message
);
}
setLoading
(
false
);
};
const
fetchModels
=
async
()
=>
{
try
{
let
res
=
await
API
.
get
(
`/api/channel/models`
);
let
localModelOptions
=
res
.
data
.
data
.
map
((
model
)
=>
({
label
:
model
.
id
,
value
:
model
.
id
}));
setOriginModelOptions
(
localModelOptions
);
setFullModels
(
res
.
data
.
data
.
map
((
model
)
=>
model
.
id
));
setBasicModels
(
res
.
data
.
data
.
filter
((
model
)
=>
{
return
model
.
id
.
startsWith
(
'gpt-3'
)
||
model
.
id
.
startsWith
(
'text-'
);
}).
map
((
model
)
=>
model
.
id
));
}
catch
(
error
)
{
showError
(
error
.
message
);
}
};
const
fetchModels
=
async
()
=>
{
try
{
let
res
=
await
API
.
get
(
`/api/channel/models`
);
let
localModelOptions
=
res
.
data
.
data
.
map
((
model
)
=>
({
label
:
model
.
id
,
value
:
model
.
id
,
}));
setOriginModelOptions
(
localModelOptions
);
setFullModels
(
res
.
data
.
data
.
map
((
model
)
=>
model
.
id
));
setBasicModels
(
res
.
data
.
data
.
filter
((
model
)
=>
{
return
model
.
id
.
startsWith
(
'gpt-3'
)
||
model
.
id
.
startsWith
(
'text-'
);
})
.
map
((
model
)
=>
model
.
id
),
);
}
catch
(
error
)
{
showError
(
error
.
message
);
}
};
const
fetchGroups
=
async
()
=>
{
try
{
let
res
=
await
API
.
get
(
`/api/group/`
);
setGroupOptions
(
res
.
data
.
data
.
map
((
group
)
=>
({
label
:
group
,
value
:
group
})));
}
catch
(
error
)
{
showError
(
error
.
message
);
}
};
const
fetchGroups
=
async
()
=>
{
try
{
let
res
=
await
API
.
get
(
`/api/group/`
);
setGroupOptions
(
res
.
data
.
data
.
map
((
group
)
=>
({
label
:
group
,
value
:
group
,
})),
);
}
catch
(
error
)
{
showError
(
error
.
message
);
}
};
useEffect
(()
=>
{
let
localModelOptions
=
[...
originModelOptions
];
inputs
.
models
.
forEach
((
model
)
=>
{
if
(
!
localModelOptions
.
find
((
option
)
=>
option
.
key
===
model
))
{
localModelOptions
.
push
({
label
:
model
,
value
:
model
});
}
useEffect
(()
=>
{
let
localModelOptions
=
[...
originModelOptions
];
inputs
.
models
.
forEach
((
model
)
=>
{
if
(
!
localModelOptions
.
find
((
option
)
=>
option
.
key
===
model
))
{
localModelOptions
.
push
({
label
:
model
,
value
:
model
,
});
setModelOptions
(
localModelOptions
);
},
[
originModelOptions
,
inputs
.
models
]);
}
});
setModelOptions
(
localModelOptions
);
},
[
originModelOptions
,
inputs
.
models
]);
useEffect
(()
=>
{
fetchModels
().
then
();
fetchGroups
().
then
();
if
(
isEdit
)
{
loadChannel
().
then
(
()
=>
{
useEffect
(()
=>
{
fetchModels
().
then
();
fetchGroups
().
then
();
if
(
isEdit
)
{
loadChannel
().
then
(()
=>
{});
}
else
{
setInputs
(
originInputs
);
}
},
[
props
.
editingChannel
.
id
]);
}
);
}
else
{
setInputs
(
originInputs
)
}
},
[
props
.
editingChannel
.
id
]);
const
submit
=
async
()
=>
{
if
(
!
isEdit
&&
(
inputs
.
name
===
''
||
inputs
.
key
===
''
))
{
showInfo
(
'请填写渠道名称和渠道密钥!'
);
return
;
}
if
(
inputs
.
models
.
length
===
0
)
{
showInfo
(
'请至少选择一个模型!'
);
return
;
}
if
(
inputs
.
model_mapping
!==
''
&&
!
verifyJSON
(
inputs
.
model_mapping
))
{
showInfo
(
'模型映射必须是合法的 JSON 格式!'
);
return
;
}
let
localInputs
=
{
...
inputs
};
if
(
localInputs
.
base_url
&&
localInputs
.
base_url
.
endsWith
(
'/'
))
{
localInputs
.
base_url
=
localInputs
.
base_url
.
slice
(
0
,
localInputs
.
base_url
.
length
-
1
,
);
}
if
(
localInputs
.
type
===
3
&&
localInputs
.
other
===
''
)
{
localInputs
.
other
=
'2023-06-01-preview'
;
}
if
(
localInputs
.
type
===
18
&&
localInputs
.
other
===
''
)
{
localInputs
.
other
=
'v2.1'
;
}
let
res
;
if
(
!
Array
.
isArray
(
localInputs
.
models
))
{
showError
(
'提交失败,请勿重复提交!'
);
handleCancel
();
return
;
}
localInputs
.
auto_ban
=
autoBan
?
1
:
0
;
localInputs
.
models
=
localInputs
.
models
.
join
(
','
);
localInputs
.
group
=
localInputs
.
groups
.
join
(
','
);
if
(
isEdit
)
{
res
=
await
API
.
put
(
`/api/channel/`
,
{
...
localInputs
,
id
:
parseInt
(
channelId
),
});
}
else
{
res
=
await
API
.
post
(
`/api/channel/`
,
localInputs
);
}
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
if
(
isEdit
)
{
showSuccess
(
'渠道更新成功!'
);
}
else
{
showSuccess
(
'渠道创建成功!'
);
setInputs
(
originInputs
);
}
props
.
refresh
();
props
.
handleClose
();
}
else
{
showError
(
message
);
}
};
const
addCustomModel
=
()
=>
{
if
(
customModel
.
trim
()
===
''
)
return
;
if
(
inputs
.
models
.
includes
(
customModel
))
return
showError
(
'该模型已存在!'
);
let
localModels
=
[...
inputs
.
models
];
localModels
.
push
(
customModel
);
let
localModelOptions
=
[];
localModelOptions
.
push
({
key
:
customModel
,
text
:
customModel
,
value
:
customModel
,
});
setModelOptions
((
modelOptions
)
=>
{
return
[...
modelOptions
,
...
localModelOptions
];
});
setCustomModel
(
''
);
handleInputChange
(
'models'
,
localModels
);
};
const
submit
=
async
()
=>
{
if
(
!
isEdit
&&
(
inputs
.
name
===
''
||
inputs
.
key
===
''
))
{
showInfo
(
'请填写渠道名称和渠道密钥!'
);
return
;
}
if
(
inputs
.
models
.
length
===
0
)
{
showInfo
(
'请至少选择一个模型!'
);
return
;
}
if
(
inputs
.
model_mapping
!==
''
&&
!
verifyJSON
(
inputs
.
model_mapping
))
{
showInfo
(
'模型映射必须是合法的 JSON 格式!'
);
return
;
}
let
localInputs
=
{...
inputs
};
if
(
localInputs
.
base_url
&&
localInputs
.
base_url
.
endsWith
(
'/'
))
{
localInputs
.
base_url
=
localInputs
.
base_url
.
slice
(
0
,
localInputs
.
base_url
.
length
-
1
);
}
if
(
localInputs
.
type
===
3
&&
localInputs
.
other
===
''
)
{
localInputs
.
other
=
'2023-06-01-preview'
;
}
if
(
localInputs
.
type
===
18
&&
localInputs
.
other
===
''
)
{
localInputs
.
other
=
'v2.1'
;
}
let
res
;
if
(
!
Array
.
isArray
(
localInputs
.
models
))
{
showError
(
'提交失败,请勿重复提交!'
);
handleCancel
();
return
;
return
(
<>
<
SideSheet
maskClosable
=
{
false
}
placement
=
{
isEdit
?
'right'
:
'left'
}
title
=
{
<
Title
level
=
{
3
}
>
{
isEdit
?
'更新渠道信息'
:
'创建新的渠道'
}
<
/Title
>
}
localInputs
.
auto_ban
=
autoBan
?
1
:
0
;
localInputs
.
models
=
localInputs
.
models
.
join
(
','
);
localInputs
.
group
=
localInputs
.
groups
.
join
(
','
);
if
(
isEdit
)
{
res
=
await
API
.
put
(
`/api/channel/`
,
{...
localInputs
,
id
:
parseInt
(
channelId
)});
}
else
{
res
=
await
API
.
post
(
`/api/channel/`
,
localInputs
);
headerStyle
=
{{
borderBottom
:
'1px solid var(--semi-color-border)'
}}
bodyStyle
=
{{
borderBottom
:
'1px solid var(--semi-color-border)'
}}
visible
=
{
props
.
visible
}
footer
=
{
<
div
style
=
{{
display
:
'flex'
,
justifyContent
:
'flex-end'
}}
>
<
Space
>
<
Button
theme
=
'solid'
size
=
{
'large'
}
onClick
=
{
submit
}
>
提交
<
/Button
>
<
Button
theme
=
'solid'
size
=
{
'large'
}
type
=
{
'tertiary'
}
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
<
/Space
>
<
/div
>
}
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
if
(
isEdit
)
{
showSuccess
(
'渠道更新成功!'
);
}
else
{
showSuccess
(
'渠道创建成功!'
);
setInputs
(
originInputs
);
}
props
.
refresh
();
props
.
handleClose
();
}
else
{
showError
(
message
);
}
};
const
addCustomModel
=
()
=>
{
if
(
customModel
.
trim
()
===
''
)
return
;
if
(
inputs
.
models
.
includes
(
customModel
))
return
showError
(
"该模型已存在!"
);
let
localModels
=
[...
inputs
.
models
];
localModels
.
push
(
customModel
);
let
localModelOptions
=
[];
localModelOptions
.
push
({
key
:
customModel
,
text
:
customModel
,
value
:
customModel
});
setModelOptions
(
modelOptions
=>
{
return
[...
modelOptions
,
...
localModelOptions
];
});
setCustomModel
(
''
);
handleInputChange
(
'models'
,
localModels
);
};
return
(
<>
<
SideSheet
maskClosable
=
{
false
}
placement
=
{
isEdit
?
'right'
:
'left'
}
title
=
{
<
Title
level
=
{
3
}
>
{
isEdit
?
'更新渠道信息'
:
'创建新的渠道'
}
<
/Title>
}
headerStyle
=
{{
borderBottom
:
'1px solid var(--semi-color-border)'
}}
bodyStyle
=
{{
borderBottom
:
'1px solid var(--semi-color-border)'
}}
visible
=
{
props
.
visible
}
footer
=
{
<
div
style
=
{{
display
:
'flex'
,
justifyContent
:
'flex-end'
}}
>
<
Space
>
<
Button
theme
=
'solid'
size
=
{
'large'
}
onClick
=
{
submit
}
>
提交
<
/Button
>
<
Button
theme
=
'solid'
size
=
{
'large'
}
type
=
{
'tertiary'
}
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
<
/Space
>
<
/div
>
closeIcon
=
{
null
}
onCancel
=
{()
=>
handleCancel
()}
width
=
{
isMobile
()
?
'100%'
:
600
}
>
<
Spin
spinning
=
{
loading
}
>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
类型:
<
/Typography.Text
>
<
/div
>
<
Select
name
=
'type'
required
optionList
=
{
CHANNEL_OPTIONS
}
value
=
{
inputs
.
type
}
onChange
=
{(
value
)
=>
handleInputChange
(
'type'
,
value
)}
style
=
{{
width
:
'50%'
}}
/
>
{
inputs
.
type
===
3
&&
(
<>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Banner
type
=
{
'warning'
}
description
=
{
<>
注意,
<
strong
>
模型部署名称必须和模型名称保持一致
<
/strong
>
,因为
One
API
会把请求体中的
model
参数替换为你的部署名称(模型名称中的点会被剔除),
<
a
target
=
'_blank'
href
=
'https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'
>
图片演示
<
/a
>
。
<
/
>
}
><
/Banner
>
<
/div
>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
AZURE_OPENAI_ENDPOINT
:
<
/Typography.Text
>
<
/div
>
<
Input
label
=
'AZURE_OPENAI_ENDPOINT'
name
=
'azure_base_url'
placeholder
=
{
'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
}
closeIcon
=
{
null
}
onCancel
=
{()
=>
handleCancel
()}
width
=
{
isMobile
()
?
'100%'
:
600
}
>
<
Spin
spinning
=
{
loading
}
>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
类型:
<
/Typography.Text
>
<
/div
>
<
Select
name
=
'type'
required
optionList
=
{
CHANNEL_OPTIONS
}
value
=
{
inputs
.
type
}
onChange
=
{
value
=>
handleInputChange
(
'type'
,
value
)}
style
=
{{
width
:
'50%'
}}
/
>
{
inputs
.
type
===
3
&&
(
<>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Banner
type
=
{
"warning"
}
description
=
{
<>
注意,
<
strong
>
模型部署名称必须和模型名称保持一致
<
/strong>,因为 One API 会把请求体中
的
model
参数替换为你的部署名称(模型名称中的点会被剔除),
<
a
target
=
'_blank'
href
=
'https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'
>
图片演示
<
/a>
。
<
/
>
}
>
<
/Banner
>
<
/div
>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
AZURE_OPENAI_ENDPOINT
:
<
/Typography.Text
>
<
/div
>
<
Input
label
=
'AZURE_OPENAI_ENDPOINT'
name
=
'azure_base_url'
placeholder
=
{
'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
}
onChange
=
{
value
=>
{
handleInputChange
(
'base_url'
,
value
)
}}
value
=
{
inputs
.
base_url
}
autoComplete
=
'new-password'
/>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
默认
API
版本:
<
/Typography.Text
>
<
/div
>
<
Input
label
=
'默认 API 版本'
name
=
'azure_other'
placeholder
=
{
'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'
}
onChange
=
{
value
=>
{
handleInputChange
(
'other'
,
value
)
}}
value
=
{
inputs
.
other
}
autoComplete
=
'new-password'
/>
<
/
>
)
}
{
inputs
.
type
===
8
&&
(
<>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
Base
URL
:
<
/Typography.Text
>
<
/div
>
<
Input
name
=
'base_url'
placeholder
=
{
'请输入自定义渠道的 Base URL'
}
onChange
=
{
value
=>
{
handleInputChange
(
'base_url'
,
value
)
}}
value
=
{
inputs
.
base_url
}
autoComplete
=
'new-password'
/>
<
/
>
)
}
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
名称:
<
/Typography.Text
>
<
/div
>
<
Input
required
name
=
'name'
placeholder
=
{
'请为渠道命名'
}
onChange
=
{
value
=>
{
handleInputChange
(
'name'
,
value
)
}}
value
=
{
inputs
.
name
}
autoComplete
=
'new-password'
/>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
分组:
<
/Typography.Text
>
<
/div
>
<
Select
placeholder
=
{
'请选择可以使用该渠道的分组'
}
name
=
'groups'
required
multiple
selection
allowAdditions
additionLabel
=
{
'请在系统设置页面编辑分组倍率以添加新的分组:'
}
onChange
=
{
value
=>
{
handleInputChange
(
'groups'
,
value
)
}}
value
=
{
inputs
.
groups
}
autoComplete
=
'new-password'
optionList
=
{
groupOptions
}
/
>
{
inputs
.
type
===
18
&&
(
<>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
模型版本:
<
/Typography.Text
>
<
/div
>
<
Input
name
=
'other'
placeholder
=
{
'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
}
onChange
=
{
value
=>
{
handleInputChange
(
'other'
,
value
)
}}
value
=
{
inputs
.
other
}
autoComplete
=
'new-password'
/>
<
/
>
)
}
{
inputs
.
type
===
21
&&
(
<>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
知识库
ID
:
<
/Typography.Text
>
<
/div
>
<
Input
label
=
'知识库 ID'
name
=
'other'
placeholder
=
{
'请输入知识库 ID,例如:123456'
}
onChange
=
{
value
=>
{
handleInputChange
(
'other'
,
value
)
}}
value
=
{
inputs
.
other
}
autoComplete
=
'new-password'
/>
<
/
>
)
}
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
模型:
<
/Typography.Text
>
<
/div
>
<
Select
placeholder
=
{
'请选择该渠道所支持的模型'
}
name
=
'models'
required
multiple
selection
onChange
=
{
value
=>
{
handleInputChange
(
'models'
,
value
)
}}
value
=
{
inputs
.
models
}
autoComplete
=
'new-password'
optionList
=
{
modelOptions
}
/
>
<
div
style
=
{{
lineHeight
:
'40px'
,
marginBottom
:
'12px'
}}
>
<
Space
>
<
Button
type
=
'primary'
onClick
=
{()
=>
{
handleInputChange
(
'models'
,
basicModels
);
}}
>
填入基础模型
<
/Button
>
<
Button
type
=
'secondary'
onClick
=
{()
=>
{
handleInputChange
(
'models'
,
fullModels
);
}}
>
填入所有模型
<
/Button
>
<
Button
type
=
'warning'
onClick
=
{()
=>
{
handleInputChange
(
'models'
,
[]);
}}
>
清除所有模型
<
/Button
>
<
/Space
>
<
Input
addonAfter
=
{
<
Button
type
=
'primary'
onClick
=
{
addCustomModel
}
>
填入
<
/Button
>
}
placeholder
=
'输入自定义模型名称'
value
=
{
customModel
}
onChange
=
{(
value
)
=>
{
setCustomModel
(
value
.
trim
());
}}
/
>
<
/div
>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
模型重定向:
<
/Typography.Text
>
<
/div
>
<
TextArea
placeholder
=
{
`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n
${
JSON
.
stringify
(
MODEL_MAPPING_EXAMPLE
,
null
,
2
)}
`
}
name
=
'model_mapping'
onChange
=
{
value
=>
{
handleInputChange
(
'model_mapping'
,
value
)
}}
autosize
value
=
{
inputs
.
model_mapping
}
autoComplete
=
'new-password'
/>
<
Typography
.
Text
style
=
{{
color
:
'rgba(var(--semi-blue-5), 1)'
,
userSelect
:
'none'
,
cursor
:
'pointer'
}}
onClick
=
{
()
=>
{
handleInputChange
(
'model_mapping'
,
JSON
.
stringify
(
MODEL_MAPPING_EXAMPLE
,
null
,
2
))
}
}
>
填入模板
<
/Typography.Text
>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
密钥:
<
/Typography.Text
>
<
/div
>
{
batch
?
<
TextArea
label
=
'密钥'
name
=
'key'
required
placeholder
=
{
'请输入密钥,一行一个'
}
onChange
=
{
value
=>
{
handleInputChange
(
'key'
,
value
)
}}
value
=
{
inputs
.
key
}
style
=
{{
minHeight
:
150
,
fontFamily
:
'JetBrains Mono, Consolas'
}}
autoComplete
=
'new-password'
/>
:
<
Input
label
=
'密钥'
name
=
'key'
required
placeholder
=
{
type2secretPrompt
(
inputs
.
type
)}
onChange
=
{
value
=>
{
handleInputChange
(
'key'
,
value
)
}}
value
=
{
inputs
.
key
}
autoComplete
=
'new-password'
/>
}
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
组织:
<
/Typography.Text
>
<
/div
>
<
Input
label
=
'组织,可选,不填则为默认组织'
name
=
'openai_organization'
placeholder
=
'请输入组织org-xxx'
onChange
=
{
value
=>
{
handleInputChange
(
'openai_organization'
,
value
)
}}
value
=
{
inputs
.
openai_organization
}
/
>
<
div
style
=
{{
marginTop
:
10
,
display
:
'flex'
}}
>
<
Space
>
<
Checkbox
name
=
'auto_ban'
checked
=
{
autoBan
}
onChange
=
{
()
=>
{
setAutoBan
(
!
autoBan
);
}
}
// onChange={handleInputChange}
/
>
<
Typography
.
Text
strong
>
是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:
<
/Typography.Text
>
<
/Space
>
<
/div
>
{
!
isEdit
&&
(
<
div
style
=
{{
marginTop
:
10
,
display
:
'flex'
}}
>
<
Space
>
<
Checkbox
checked
=
{
batch
}
label
=
'批量创建'
name
=
'batch'
onChange
=
{()
=>
setBatch
(
!
batch
)}
/
>
<
Typography
.
Text
strong
>
批量创建
<
/Typography.Text
>
<
/Space
>
<
/div
>
)
}
{
inputs
.
type
!==
3
&&
inputs
.
type
!==
8
&&
inputs
.
type
!==
22
&&
(
<>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
代理:
<
/Typography.Text
>
<
/div
>
<
Input
label
=
'代理'
name
=
'base_url'
placeholder
=
{
'此项可选,用于通过代理站来进行 API 调用'
}
onChange
=
{
value
=>
{
handleInputChange
(
'base_url'
,
value
)
}}
value
=
{
inputs
.
base_url
}
autoComplete
=
'new-password'
/>
<
/
>
)
}
{
inputs
.
type
===
22
&&
(
<>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
私有部署地址:
<
/Typography.Text
>
<
/div
>
<
Input
name
=
'base_url'
placeholder
=
{
'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'
}
onChange
=
{
value
=>
{
handleInputChange
(
'base_url'
,
value
)
}}
value
=
{
inputs
.
base_url
}
autoComplete
=
'new-password'
/>
<
/
>
)
}
onChange
=
{(
value
)
=>
{
handleInputChange
(
'base_url'
,
value
);
}}
value
=
{
inputs
.
base_url
}
autoComplete
=
'new-password'
/>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
默认
API
版本:
<
/Typography.Text
>
<
/div
>
<
Input
label
=
'默认 API 版本'
name
=
'azure_other'
placeholder
=
{
'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'
}
onChange
=
{(
value
)
=>
{
handleInputChange
(
'other'
,
value
);
}}
value
=
{
inputs
.
other
}
autoComplete
=
'new-password'
/>
<
/
>
)}
{
inputs
.
type
===
8
&&
(
<>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
Base
URL
:
<
/Typography.Text
>
<
/div
>
<
Input
name
=
'base_url'
placeholder
=
{
'请输入自定义渠道的 Base URL'
}
onChange
=
{(
value
)
=>
{
handleInputChange
(
'base_url'
,
value
);
}}
value
=
{
inputs
.
base_url
}
autoComplete
=
'new-password'
/>
<
/
>
)}
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
名称:
<
/Typography.Text
>
<
/div
>
<
Input
required
name
=
'name'
placeholder
=
{
'请为渠道命名'
}
onChange
=
{(
value
)
=>
{
handleInputChange
(
'name'
,
value
);
}}
value
=
{
inputs
.
name
}
autoComplete
=
'new-password'
/>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
分组:
<
/Typography.Text
>
<
/div
>
<
Select
placeholder
=
{
'请选择可以使用该渠道的分组'
}
name
=
'groups'
required
multiple
selection
allowAdditions
additionLabel
=
{
'请在系统设置页面编辑分组倍率以添加新的分组:'
}
onChange
=
{(
value
)
=>
{
handleInputChange
(
'groups'
,
value
);
}}
value
=
{
inputs
.
groups
}
autoComplete
=
'new-password'
optionList
=
{
groupOptions
}
/
>
{
inputs
.
type
===
18
&&
(
<>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
模型版本:
<
/Typography.Text
>
<
/div
>
<
Input
name
=
'other'
placeholder
=
{
'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
}
onChange
=
{(
value
)
=>
{
handleInputChange
(
'other'
,
value
);
}}
value
=
{
inputs
.
other
}
autoComplete
=
'new-password'
/>
<
/
>
)}
{
inputs
.
type
===
21
&&
(
<>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
知识库
ID
:
<
/Typography.Text
>
<
/div
>
<
Input
label
=
'知识库 ID'
name
=
'other'
placeholder
=
{
'请输入知识库 ID,例如:123456'
}
onChange
=
{(
value
)
=>
{
handleInputChange
(
'other'
,
value
);
}}
value
=
{
inputs
.
other
}
autoComplete
=
'new-password'
/>
<
/
>
)}
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
模型:
<
/Typography.Text
>
<
/div
>
<
Select
placeholder
=
{
'请选择该渠道所支持的模型'
}
name
=
'models'
required
multiple
selection
onChange
=
{(
value
)
=>
{
handleInputChange
(
'models'
,
value
);
}}
value
=
{
inputs
.
models
}
autoComplete
=
'new-password'
optionList
=
{
modelOptions
}
/
>
<
div
style
=
{{
lineHeight
:
'40px'
,
marginBottom
:
'12px'
}}
>
<
Space
>
<
Button
type
=
'primary'
onClick
=
{()
=>
{
handleInputChange
(
'models'
,
basicModels
);
}}
>
填入基础模型
<
/Button
>
<
Button
type
=
'secondary'
onClick
=
{()
=>
{
handleInputChange
(
'models'
,
fullModels
);
}}
>
填入所有模型
<
/Button
>
<
Button
type
=
'warning'
onClick
=
{()
=>
{
handleInputChange
(
'models'
,
[]);
}}
>
清除所有模型
<
/Button
>
<
/Space
>
<
Input
addonAfter
=
{
<
Button
type
=
'primary'
onClick
=
{
addCustomModel
}
>
填入
<
/Button
>
}
placeholder
=
'输入自定义模型名称'
value
=
{
customModel
}
onChange
=
{(
value
)
=>
{
setCustomModel
(
value
.
trim
());
}}
/
>
<
/div
>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
模型重定向:
<
/Typography.Text
>
<
/div
>
<
TextArea
placeholder
=
{
`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n
${
JSON
.
stringify
(
MODEL_MAPPING_EXAMPLE
,
null
,
2
)}
`
}
name
=
'model_mapping'
onChange
=
{(
value
)
=>
{
handleInputChange
(
'model_mapping'
,
value
);
}}
autosize
value
=
{
inputs
.
model_mapping
}
autoComplete
=
'new-password'
/>
<
Typography
.
Text
style
=
{{
color
:
'rgba(var(--semi-blue-5), 1)'
,
userSelect
:
'none'
,
cursor
:
'pointer'
,
}}
onClick
=
{()
=>
{
handleInputChange
(
'model_mapping'
,
JSON
.
stringify
(
MODEL_MAPPING_EXAMPLE
,
null
,
2
),
);
}}
>
填入模板
<
/Typography.Text
>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
密钥:
<
/Typography.Text
>
<
/div
>
{
batch
?
(
<
TextArea
label
=
'密钥'
name
=
'key'
required
placeholder
=
{
'请输入密钥,一行一个'
}
onChange
=
{(
value
)
=>
{
handleInputChange
(
'key'
,
value
);
}}
value
=
{
inputs
.
key
}
style
=
{{
minHeight
:
150
,
fontFamily
:
'JetBrains Mono, Consolas'
}}
autoComplete
=
'new-password'
/>
)
:
(
<
Input
label
=
'密钥'
name
=
'key'
required
placeholder
=
{
type2secretPrompt
(
inputs
.
type
)}
onChange
=
{(
value
)
=>
{
handleInputChange
(
'key'
,
value
);
}}
value
=
{
inputs
.
key
}
autoComplete
=
'new-password'
/>
)}
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
组织:
<
/Typography.Text
>
<
/div
>
<
Input
label
=
'组织,可选,不填则为默认组织'
name
=
'openai_organization'
placeholder
=
'请输入组织org-xxx'
onChange
=
{(
value
)
=>
{
handleInputChange
(
'openai_organization'
,
value
);
}}
value
=
{
inputs
.
openai_organization
}
/
>
<
div
style
=
{{
marginTop
:
10
,
display
:
'flex'
}}
>
<
Space
>
<
Checkbox
name
=
'auto_ban'
checked
=
{
autoBan
}
onChange
=
{()
=>
{
setAutoBan
(
!
autoBan
);
}}
// onChange={handleInputChange}
/
>
<
Typography
.
Text
strong
>
是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:
<
/Typography.Text
>
<
/Space
>
<
/div
>
<
/Spin
>
<
/SideSheet
>
<
/
>
);
{
!
isEdit
&&
(
<
div
style
=
{{
marginTop
:
10
,
display
:
'flex'
}}
>
<
Space
>
<
Checkbox
checked
=
{
batch
}
label
=
'批量创建'
name
=
'batch'
onChange
=
{()
=>
setBatch
(
!
batch
)}
/
>
<
Typography
.
Text
strong
>
批量创建
<
/Typography.Text
>
<
/Space
>
<
/div
>
)}
{
inputs
.
type
!==
3
&&
inputs
.
type
!==
8
&&
inputs
.
type
!==
22
&&
(
<>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
代理:
<
/Typography.Text
>
<
/div
>
<
Input
label
=
'代理'
name
=
'base_url'
placeholder
=
{
'此项可选,用于通过代理站来进行 API 调用'
}
onChange
=
{(
value
)
=>
{
handleInputChange
(
'base_url'
,
value
);
}}
value
=
{
inputs
.
base_url
}
autoComplete
=
'new-password'
/>
<
/
>
)}
{
inputs
.
type
===
22
&&
(
<>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Typography
.
Text
strong
>
私有部署地址:
<
/Typography.Text
>
<
/div
>
<
Input
name
=
'base_url'
placeholder
=
{
'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'
}
onChange
=
{(
value
)
=>
{
handleInputChange
(
'base_url'
,
value
);
}}
value
=
{
inputs
.
base_url
}
autoComplete
=
'new-password'
/>
<
/
>
)}
<
/Spin
>
<
/SideSheet
>
<
/
>
);
};
export
default
EditChannel
;
web/src/pages/Channel/index.js
View file @
eb955ae4
import
React
from
'react'
;
import
ChannelsTable
from
'../../components/ChannelsTable'
;
import
{
Layout
}
from
"@douyinfe/semi-ui"
;
import
{
Layout
}
from
'@douyinfe/semi-ui'
;
const
File
=
()
=>
(
<>
<
Layout
>
<
Layout
.
Header
>
<
h3
>
管理渠道
<
/h3
>
<
/Layout.Header
>
<
Layout
.
Content
>
<
ChannelsTable
/>
<
/Layout.Content
>
<
/Layout
>
<
/
>
<>
<
Layout
>
<
Layout
.
Header
>
<
h3
>
管理渠道
<
/h3
>
<
/Layout.Header
>
<
Layout
.
Content
>
<
ChannelsTable
/>
<
/Layout.Content
>
<
/Layout
>
<
/
>
);
export
default
File
;
web/src/pages/Chat/index.js
View file @
eb955ae4
...
...
@@ -11,5 +11,4 @@ const Chat = () => {
);
};
export
default
Chat
;
web/src/pages/Detail/index.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useRef
,
useState
}
from
'react'
;
import
React
,
{
useEffect
,
useRef
,
useState
}
from
'react'
;
import
{
initVChartSemiTheme
}
from
'@visactor/vchart-semi-theme'
;
import
{
Button
,
Col
,
Form
,
Layout
,
Row
,
Spin
}
from
"@douyinfe/semi-ui"
;
import
{
Button
,
Col
,
Form
,
Layout
,
Row
,
Spin
}
from
'@douyinfe/semi-ui'
;
import
VChart
from
'@visactor/vchart'
;
import
{
API
,
isAdmin
,
showError
,
timestamp2string
,
timestamp2string1
}
from
"../../helpers"
;
import
{
getQuotaWithUnit
,
modelColorMap
,
renderNumber
,
renderQuota
,
renderQuotaNumberWithDigit
,
stringToColor
}
from
"../../helpers/render"
;
API
,
isAdmin
,
showError
,
timestamp2string
,
timestamp2string1
,
}
from
'../../helpers'
;
import
{
getQuotaWithUnit
,
modelColorMap
,
renderNumber
,
renderQuota
,
renderQuotaNumberWithDigit
,
stringToColor
,
}
from
'../../helpers/render'
;
const
Detail
=
(
props
)
=>
{
const
formRef
=
useRef
();
let
now
=
new
Date
();
const
[
inputs
,
setInputs
]
=
useState
({
username
:
''
,
token_name
:
''
,
model_name
:
''
,
start_timestamp
:
localStorage
.
getItem
(
'data_export_default_time'
)
===
'hour'
?
timestamp2string
(
now
.
getTime
()
/
1000
-
86400
)
:
(
localStorage
.
getItem
(
'data_export_default_time'
)
===
'week'
?
timestamp2string
(
now
.
getTime
()
/
1000
-
86400
*
30
)
:
timestamp2string
(
now
.
getTime
()
/
1000
-
86400
*
7
)),
end_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
),
channel
:
''
,
data_export_default_time
:
''
});
const
{
username
,
model_name
,
start_timestamp
,
end_timestamp
,
channel
}
=
inputs
;
const
isAdminUser
=
isAdmin
();
const
initialized
=
useRef
(
false
)
const
[
modelDataChart
,
setModelDataChart
]
=
useState
(
null
);
const
[
modelDataPieChart
,
setModelDataPieChart
]
=
useState
(
null
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
quotaData
,
setQuotaData
]
=
useState
([]);
const
[
consumeQuota
,
setConsumeQuota
]
=
useState
(
0
);
const
[
times
,
setTimes
]
=
useState
(
0
);
const
[
dataExportDefaultTime
,
setDataExportDefaultTime
]
=
useState
(
localStorage
.
getItem
(
'data_export_default_time'
)
||
'hour'
);
const
formRef
=
useRef
();
let
now
=
new
Date
();
const
[
inputs
,
setInputs
]
=
useState
({
username
:
''
,
token_name
:
''
,
model_name
:
''
,
start_timestamp
:
localStorage
.
getItem
(
'data_export_default_time'
)
===
'hour'
?
timestamp2string
(
now
.
getTime
()
/
1000
-
86400
)
:
localStorage
.
getItem
(
'data_export_default_time'
)
===
'week'
?
timestamp2string
(
now
.
getTime
()
/
1000
-
86400
*
30
)
:
timestamp2string
(
now
.
getTime
()
/
1000
-
86400
*
7
),
end_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
),
channel
:
''
,
data_export_default_time
:
''
,
});
const
{
username
,
model_name
,
start_timestamp
,
end_timestamp
,
channel
}
=
inputs
;
const
isAdminUser
=
isAdmin
();
const
initialized
=
useRef
(
false
);
const
[
modelDataChart
,
setModelDataChart
]
=
useState
(
null
);
const
[
modelDataPieChart
,
setModelDataPieChart
]
=
useState
(
null
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
quotaData
,
setQuotaData
]
=
useState
([]);
const
[
consumeQuota
,
setConsumeQuota
]
=
useState
(
0
);
const
[
times
,
setTimes
]
=
useState
(
0
);
const
[
dataExportDefaultTime
,
setDataExportDefaultTime
]
=
useState
(
localStorage
.
getItem
(
'data_export_default_time'
)
||
'hour'
,
);
const
handleInputChange
=
(
value
,
name
)
=>
{
if
(
name
===
'data_export_default_time'
)
{
setDataExportDefaultTime
(
value
);
return
}
setInputs
((
inputs
)
=>
({...
inputs
,
[
name
]:
value
}));
};
const
handleInputChange
=
(
value
,
name
)
=>
{
if
(
name
===
'data_export_default_time'
)
{
setDataExportDefaultTime
(
value
);
return
;
}
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
};
const
spec_line
=
{
type
:
'bar'
,
data
:
[
{
id
:
'barData'
,
values
:
[]
}
],
xField
:
'Time'
,
yField
:
'Usage'
,
seriesField
:
'Model'
,
stack
:
true
,
legends
:
{
visible
:
true
},
title
:
{
visible
:
true
,
text
:
'模型消耗分布'
,
subtext
:
'0'
const
spec_line
=
{
type
:
'bar'
,
data
:
[
{
id
:
'barData'
,
values
:
[],
},
],
xField
:
'Time'
,
yField
:
'Usage'
,
seriesField
:
'Model'
,
stack
:
true
,
legends
:
{
visible
:
true
,
},
title
:
{
visible
:
true
,
text
:
'模型消耗分布'
,
subtext
:
'0'
,
},
bar
:
{
// The state style of bar
state
:
{
hover
:
{
stroke
:
'#000'
,
lineWidth
:
1
,
},
bar
:
{
// The state style of bar
state
:
{
hover
:
{
stroke
:
'#000'
,
lineWidth
:
1
}
}
},
tooltip
:
{
mark
:
{
content
:
[
{
key
:
datum
=>
datum
[
'Model'
],
value
:
datum
=>
renderQuotaNumberWithDigit
(
parseFloat
(
datum
[
'Usage'
]),
4
)
}
]
},
dimension
:
{
content
:
[
{
key
:
datum
=>
datum
[
'Model'
],
value
:
datum
=>
datum
[
'Usage'
]
}
],
updateContent
:
array
=>
{
// sort by value
array
.
sort
((
a
,
b
)
=>
b
.
value
-
a
.
value
);
// add $
let
sum
=
0
;
for
(
let
i
=
0
;
i
<
array
.
length
;
i
++
)
{
sum
+=
parseFloat
(
array
[
i
].
value
);
array
[
i
].
value
=
renderQuotaNumberWithDigit
(
parseFloat
(
array
[
i
].
value
),
4
);
}
// add to first
array
.
unshift
({
key
:
'总计'
,
value
:
renderQuotaNumberWithDigit
(
sum
,
4
)
});
return
array
;
}
}
},
color
:
{
specified
:
modelColorMap
}
};
const
spec_pie
=
{
type
:
'pie'
,
data
:
[
{
id
:
'id0'
,
values
:
[
{
type
:
'null'
,
value
:
'0'
},
]
}
},
},
tooltip
:
{
mark
:
{
content
:
[
{
key
:
(
datum
)
=>
datum
[
'Model'
],
value
:
(
datum
)
=>
renderQuotaNumberWithDigit
(
parseFloat
(
datum
[
'Usage'
]),
4
),
},
],
outerRadius
:
0.8
,
innerRadius
:
0.5
,
padAngle
:
0.6
,
valueField
:
'value'
,
categoryField
:
'type'
,
pie
:
{
style
:
{
cornerRadius
:
10
},
state
:
{
hover
:
{
outerRadius
:
0.85
,
stroke
:
'#000'
,
lineWidth
:
1
},
selected
:
{
outerRadius
:
0.85
,
stroke
:
'#000'
,
lineWidth
:
1
}
}
},
title
:
{
visible
:
true
,
text
:
'模型调用次数占比'
},
legends
:
{
visible
:
true
,
orient
:
'left'
},
dimension
:
{
content
:
[
{
key
:
(
datum
)
=>
datum
[
'Model'
],
value
:
(
datum
)
=>
datum
[
'Usage'
],
},
],
updateContent
:
(
array
)
=>
{
// sort by value
array
.
sort
((
a
,
b
)
=>
b
.
value
-
a
.
value
);
// add $
let
sum
=
0
;
for
(
let
i
=
0
;
i
<
array
.
length
;
i
++
)
{
sum
+=
parseFloat
(
array
[
i
].
value
);
array
[
i
].
value
=
renderQuotaNumberWithDigit
(
parseFloat
(
array
[
i
].
value
),
4
,
);
}
// add to first
array
.
unshift
({
key
:
'总计'
,
value
:
renderQuotaNumberWithDigit
(
sum
,
4
),
});
return
array
;
},
label
:
{
visible
:
true
},
},
color
:
{
specified
:
modelColorMap
,
},
};
const
spec_pie
=
{
type
:
'pie'
,
data
:
[
{
id
:
'id0'
,
values
:
[{
type
:
'null'
,
value
:
'0'
}],
},
],
outerRadius
:
0.8
,
innerRadius
:
0.5
,
padAngle
:
0.6
,
valueField
:
'value'
,
categoryField
:
'type'
,
pie
:
{
style
:
{
cornerRadius
:
10
,
},
state
:
{
hover
:
{
outerRadius
:
0.85
,
stroke
:
'#000'
,
lineWidth
:
1
,
},
tooltip
:
{
mark
:
{
content
:
[
{
key
:
datum
=>
datum
[
'type'
],
value
:
datum
=>
renderNumber
(
datum
[
'value'
])
}
]
}
selected
:
{
outerRadius
:
0.85
,
stroke
:
'#000'
,
lineWidth
:
1
,
},
color
:
{
specified
:
modelColorMap
}
};
},
},
title
:
{
visible
:
true
,
text
:
'模型调用次数占比'
,
},
legends
:
{
visible
:
true
,
orient
:
'left'
,
},
label
:
{
visible
:
true
,
},
tooltip
:
{
mark
:
{
content
:
[
{
key
:
(
datum
)
=>
datum
[
'type'
],
value
:
(
datum
)
=>
renderNumber
(
datum
[
'value'
]),
},
],
},
},
color
:
{
specified
:
modelColorMap
,
},
};
const
loadQuotaData
=
async
(
lineChart
,
pieChart
)
=>
{
setLoading
(
true
);
const
loadQuotaData
=
async
(
lineChart
,
pieChart
)
=>
{
setLoading
(
true
);
let
url
=
''
;
let
localStartTimestamp
=
Date
.
parse
(
start_timestamp
)
/
1000
;
let
localEndTimestamp
=
Date
.
parse
(
end_timestamp
)
/
1000
;
if
(
isAdminUser
)
{
url
=
`/api/data/?username=
${
username
}
&start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
&default_time=
${
dataExportDefaultTime
}
`
;
}
else
{
url
=
`/api/data/self/?start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
&default_time=
${
dataExportDefaultTime
}
`
;
}
const
res
=
await
API
.
get
(
url
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
setQuotaData
(
data
);
if
(
data
.
length
===
0
)
{
data
.
push
({
count
:
0
,
model_name
:
'无数据'
,
quota
:
0
,
created_at
:
now
.
getTime
()
/
1000
,
});
}
// 根据dataExportDefaultTime重制时间粒度
let
timeGranularity
=
3600
;
if
(
dataExportDefaultTime
===
'day'
)
{
timeGranularity
=
86400
;
}
else
if
(
dataExportDefaultTime
===
'week'
)
{
timeGranularity
=
604800
;
}
data
.
forEach
((
item
)
=>
{
item
[
'created_at'
]
=
Math
.
floor
(
item
[
'created_at'
]
/
timeGranularity
)
*
timeGranularity
;
});
updateChart
(
lineChart
,
pieChart
,
data
);
}
else
{
showError
(
message
);
}
setLoading
(
false
);
};
let
url
=
''
;
let
localStartTimestamp
=
Date
.
parse
(
start_timestamp
)
/
1000
;
let
localEndTimestamp
=
Date
.
parse
(
end_timestamp
)
/
1000
;
if
(
isAdminUser
)
{
url
=
`/api/data/?username=
${
username
}
&start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
&default_time=
${
dataExportDefaultTime
}
`
;
}
else
{
url
=
`/api/data/self/?start_timestamp=
${
localStartTimestamp
}
&end_timestamp=
${
localEndTimestamp
}
&default_time=
${
dataExportDefaultTime
}
`
;
}
const
res
=
await
API
.
get
(
url
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
setQuotaData
(
data
);
if
(
data
.
length
===
0
)
{
data
.
push
({
'count'
:
0
,
'model_name'
:
'无数据'
,
'quota'
:
0
,
'created_at'
:
now
.
getTime
()
/
1000
})
}
// 根据dataExportDefaultTime重制时间粒度
let
timeGranularity
=
3600
;
if
(
dataExportDefaultTime
===
'day'
)
{
timeGranularity
=
86400
;
}
else
if
(
dataExportDefaultTime
===
'week'
)
{
timeGranularity
=
604800
;
}
data
.
forEach
(
item
=>
{
item
[
'created_at'
]
=
Math
.
floor
(
item
[
'created_at'
]
/
timeGranularity
)
*
timeGranularity
;
});
updateChart
(
lineChart
,
pieChart
,
data
);
}
else
{
showError
(
message
);
}
setLoading
(
false
);
};
const
refresh
=
async
()
=>
{
await
loadQuotaData
(
modelDataChart
,
modelDataPieChart
);
};
const
refresh
=
async
()
=>
{
await
loadQuotaData
(
modelDataChart
,
modelDataPieChart
);
};
const
initChart
=
async
()
=>
{
let
lineChart
=
modelDataChart
;
if
(
!
modelDataChart
)
{
lineChart
=
new
VChart
(
spec_line
,
{
dom
:
'model_data'
});
setModelDataChart
(
lineChart
);
lineChart
.
renderAsync
();
}
let
pieChart
=
modelDataPieChart
;
if
(
!
modelDataPieChart
)
{
pieChart
=
new
VChart
(
spec_pie
,
{
dom
:
'model_pie'
});
setModelDataPieChart
(
pieChart
);
pieChart
.
renderAsync
();
}
console
.
log
(
'init vchart'
);
await
loadQuotaData
(
lineChart
,
pieChart
);
};
const
initChart
=
async
()
=>
{
let
lineChart
=
modelDataChart
if
(
!
modelDataChart
)
{
lineChart
=
new
VChart
(
spec_line
,
{
dom
:
'model_data'
});
setModelDataChart
(
lineChart
);
lineChart
.
renderAsync
();
}
let
pieChart
=
modelDataPieChart
if
(
!
modelDataPieChart
)
{
pieChart
=
new
VChart
(
spec_pie
,
{
dom
:
'model_pie'
});
setModelDataPieChart
(
pieChart
);
pieChart
.
renderAsync
();
}
console
.
log
(
'init vchart'
);
await
loadQuotaData
(
lineChart
,
pieChart
)
const
updateChart
=
(
lineChart
,
pieChart
,
data
)
=>
{
if
(
isAdminUser
)
{
// 将所有用户合并
}
let
pieData
=
[];
let
lineData
=
[];
let
consumeQuota
=
0
;
let
times
=
0
;
for
(
let
i
=
0
;
i
<
data
.
length
;
i
++
)
{
const
item
=
data
[
i
];
consumeQuota
+=
item
.
quota
;
times
+=
item
.
count
;
// 合并model_name
let
pieItem
=
pieData
.
find
((
it
)
=>
it
.
type
===
item
.
model_name
);
if
(
pieItem
)
{
pieItem
.
value
+=
item
.
count
;
}
else
{
pieData
.
push
({
type
:
item
.
model_name
,
value
:
item
.
count
,
});
}
// 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
// 转换日期格式
let
createTime
=
timestamp2string1
(
item
.
created_at
,
dataExportDefaultTime
,
);
let
lineItem
=
lineData
.
find
(
(
it
)
=>
it
.
Time
===
createTime
&&
it
.
Model
===
item
.
model_name
,
);
if
(
lineItem
)
{
lineItem
.
Usage
+=
parseFloat
(
getQuotaWithUnit
(
item
.
quota
));
}
else
{
lineData
.
push
({
Time
:
createTime
,
Model
:
item
.
model_name
,
Usage
:
parseFloat
(
getQuotaWithUnit
(
item
.
quota
)),
});
}
}
setConsumeQuota
(
consumeQuota
);
setTimes
(
times
);
const
updateChart
=
(
lineChart
,
pieChart
,
data
)
=>
{
if
(
isAdminUser
)
{
// 将所有用户合并
}
let
pieData
=
[];
let
lineData
=
[];
let
consumeQuota
=
0
;
let
times
=
0
;
for
(
let
i
=
0
;
i
<
data
.
length
;
i
++
)
{
const
item
=
data
[
i
];
consumeQuota
+=
item
.
quota
;
times
+=
item
.
count
;
// 合并model_name
let
pieItem
=
pieData
.
find
(
it
=>
it
.
type
===
item
.
model_name
);
if
(
pieItem
)
{
pieItem
.
value
+=
item
.
count
;
}
else
{
pieData
.
push
({
"type"
:
item
.
model_name
,
"value"
:
item
.
count
});
}
// 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
// 转换日期格式
let
createTime
=
timestamp2string1
(
item
.
created_at
,
dataExportDefaultTime
);
let
lineItem
=
lineData
.
find
(
it
=>
it
.
Time
===
createTime
&&
it
.
Model
===
item
.
model_name
);
if
(
lineItem
)
{
lineItem
.
Usage
+=
parseFloat
(
getQuotaWithUnit
(
item
.
quota
));
}
else
{
lineData
.
push
({
"Time"
:
createTime
,
"Model"
:
item
.
model_name
,
"Usage"
:
parseFloat
(
getQuotaWithUnit
(
item
.
quota
))
});
}
}
setConsumeQuota
(
consumeQuota
);
setTimes
(
times
);
// sort by count
pieData
.
sort
((
a
,
b
)
=>
b
.
value
-
a
.
value
);
spec_pie
.
title
.
subtext
=
`总计:
${
renderNumber
(
times
)}
`
;
spec_pie
.
data
[
0
].
values
=
pieData
;
// sort by count
pieData
.
sort
((
a
,
b
)
=>
b
.
value
-
a
.
value
)
;
spec_pie
.
title
.
subtext
=
`总计:
${
renderNumber
(
times
)}
`
;
spec_pie
.
data
[
0
].
values
=
pieData
;
spec_line
.
title
.
subtext
=
`总计:
${
renderQuota
(
consumeQuota
,
2
)}
`
;
spec_line
.
data
[
0
].
values
=
lineData
;
pieChart
.
updateSpec
(
spec_pie
)
;
lineChart
.
updateSpec
(
spec_line
)
;
spec_line
.
title
.
subtext
=
`总计:
${
renderQuota
(
consumeQuota
,
2
)}
`
;
spec_line
.
data
[
0
].
values
=
lineData
;
pieChart
.
updateSpec
(
spec_pie
);
lineChart
.
updateSpec
(
spec_line
);
// pieChart.updateData('id0', pieData);
// lineChart.updateData('barData', lineData);
pieChart
.
reLayout
();
lineChart
.
reLayout
();
};
// pieChart.updateData('id0', pieData);
// lineChart.updateData('barData', lineData);
pieChart
.
reLayout
();
lineChart
.
reLayout
();
useEffect
(()
=>
{
// setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
// if (dataExportDefaultTime === 'day') {
// // 设置开始时间为7天前
// let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
// inputs.start_timestamp = st;
// formRef.current.formApi.setValue('start_timestamp', st);
// }
if
(
!
initialized
.
current
)
{
initVChartSemiTheme
({
isWatchingThemeSwitch
:
true
,
});
initialized
.
current
=
true
;
initChart
();
}
},
[]);
useEffect
(()
=>
{
// setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
// if (dataExportDefaultTime === 'day') {
// // 设置开始时间为7天前
// let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
// inputs.start_timestamp = st;
// formRef.current.formApi.setValue('start_timestamp', st);
// }
if
(
!
initialized
.
current
)
{
initVChartSemiTheme
({
isWatchingThemeSwitch
:
true
,
});
initialized
.
current
=
true
;
initChart
();
}
},
[]);
return
(
<>
<
Layout
>
<
Layout
.
Header
>
<
h3
>
数据看板
<
/h3
>
<
/Layout.Header
>
<
Layout
.
Content
>
<
Form
ref
=
{
formRef
}
layout
=
'horizontal'
style
=
{{
marginTop
:
10
}}
>
<>
<
Form
.
DatePicker
field
=
"start_timestamp"
label
=
'起始时间'
style
=
{{
width
:
272
}}
initValue
=
{
start_timestamp
}
value
=
{
start_timestamp
}
type
=
'dateTime'
name
=
'start_timestamp'
onChange
=
{
value
=>
handleInputChange
(
value
,
'start_timestamp'
)}
/
>
<
Form
.
DatePicker
field
=
"end_timestamp"
fluid
label
=
'结束时间'
style
=
{{
width
:
272
}}
initValue
=
{
end_timestamp
}
value
=
{
end_timestamp
}
type
=
'dateTime'
name
=
'end_timestamp'
onChange
=
{
value
=>
handleInputChange
(
value
,
'end_timestamp'
)}
/
>
<
Form
.
Select
field
=
"data_export_default_time"
label
=
'时间粒度'
style
=
{{
width
:
176
}}
initValue
=
{
dataExportDefaultTime
}
placeholder
=
{
'时间粒度'
}
name
=
'data_export_default_time'
optionList
=
{
[
{
label
:
'小时'
,
value
:
'hour'
},
{
label
:
'天'
,
value
:
'day'
},
{
label
:
'周'
,
value
:
'week'
}
]
}
onChange
=
{
value
=>
handleInputChange
(
value
,
'data_export_default_time'
)}
>
<
/Form.Select
>
{
isAdminUser
&&
<>
<
Form
.
Input
field
=
"username"
label
=
'用户名称'
style
=
{{
width
:
176
}}
value
=
{
username
}
placeholder
=
{
'可选值'
}
name
=
'username'
onChange
=
{
value
=>
handleInputChange
(
value
,
'username'
)}
/
>
<
/
>
}
<
Form
.
Section
>
<
Button
label
=
'查询'
type
=
"primary"
htmlType
=
"submit"
className
=
"btn-margin-right"
onClick
=
{
refresh
}
loading
=
{
loading
}
>
查询
<
/Button
>
<
/Form.Section
>
<
/
>
<
/Form
>
<
Spin
spinning
=
{
loading
}
>
<
div
style
=
{{
height
:
500
}}
>
<
div
id
=
"model_pie"
style
=
{{
width
:
'100%'
,
minWidth
:
100
}}
><
/div
>
<
/div
>
<
div
style
=
{{
height
:
500
}}
>
<
div
id
=
"model_data"
style
=
{{
width
:
'100%'
,
minWidth
:
100
}}
><
/div
>
<
/div
>
<
/Spin
>
<
/Layout.Content
>
<
/Layout
>
<
/
>
);
return
(
<>
<
Layout
>
<
Layout
.
Header
>
<
h3
>
数据看板
<
/h3
>
<
/Layout.Header
>
<
Layout
.
Content
>
<
Form
ref
=
{
formRef
}
layout
=
'horizontal'
style
=
{{
marginTop
:
10
}}
>
<>
<
Form
.
DatePicker
field
=
'start_timestamp'
label
=
'起始时间'
style
=
{{
width
:
272
}}
initValue
=
{
start_timestamp
}
value
=
{
start_timestamp
}
type
=
'dateTime'
name
=
'start_timestamp'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'start_timestamp'
)
}
/
>
<
Form
.
DatePicker
field
=
'end_timestamp'
fluid
label
=
'结束时间'
style
=
{{
width
:
272
}}
initValue
=
{
end_timestamp
}
value
=
{
end_timestamp
}
type
=
'dateTime'
name
=
'end_timestamp'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'end_timestamp'
)}
/
>
<
Form
.
Select
field
=
'data_export_default_time'
label
=
'时间粒度'
style
=
{{
width
:
176
}}
initValue
=
{
dataExportDefaultTime
}
placeholder
=
{
'时间粒度'
}
name
=
'data_export_default_time'
optionList
=
{[
{
label
:
'小时'
,
value
:
'hour'
},
{
label
:
'天'
,
value
:
'day'
},
{
label
:
'周'
,
value
:
'week'
},
]}
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'data_export_default_time'
)
}
><
/Form.Select
>
{
isAdminUser
&&
(
<>
<
Form
.
Input
field
=
'username'
label
=
'用户名称'
style
=
{{
width
:
176
}}
value
=
{
username
}
placeholder
=
{
'可选值'
}
name
=
'username'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'username'
)}
/
>
<
/
>
)}
<
Form
.
Section
>
<
Button
label
=
'查询'
type
=
'primary'
htmlType
=
'submit'
className
=
'btn-margin-right'
onClick
=
{
refresh
}
loading
=
{
loading
}
>
查询
<
/Button
>
<
/Form.Section
>
<
/
>
<
/Form
>
<
Spin
spinning
=
{
loading
}
>
<
div
style
=
{{
height
:
500
}}
>
<
div
id
=
'model_pie'
style
=
{{
width
:
'100%'
,
minWidth
:
100
}}
><
/div
>
<
/div
>
<
div
style
=
{{
height
:
500
}}
>
<
div
id
=
'model_data'
style
=
{{
width
:
'100%'
,
minWidth
:
100
}}
><
/div
>
<
/div
>
<
/Spin
>
<
/Layout.Content
>
<
/Layout
>
<
/
>
);
};
export
default
Detail
;
web/src/pages/Home/index.js
View file @
eb955ae4
...
...
@@ -53,78 +53,115 @@ const Home = () => {
},
[]);
return
(
<>
{
homePageContentLoaded
&&
homePageContent
===
''
?
<>
<
Card
bordered
=
{
false
}
headerLine
=
{
false
}
title
=
'系统状况'
bodyStyle
=
{{
padding
:
'10px 20px'
}}
>
<
Row
gutter
=
{
16
}
>
<
Col
span
=
{
12
}
>
<
Card
title
=
'系统信息'
headerExtraContent
=
{
<
span
style
=
{{
fontSize
:
'12px'
,
color
:
'var(--semi-color-text-1)'
}}
>
系统信息总览
<
/span>}
>
<
p
>
名称:
{
statusState
?.
status
?.
system_name
}
<
/p
>
<
p
>
版本:
{
statusState
?.
status
?.
version
?
statusState
?.
status
?.
version
:
'unknown'
}
<
/p
>
<
p
>
源码:
<
a
href
=
'https://github.com/songquanpeng/one-api'
target
=
'_blank'
rel
=
'noreferrer'
>
https
:
//github.com/songquanpeng/one-api
<
/a
>
<
/p
>
<
p
>
启动时间:
{
getStartTimeString
()}
<
/p
>
<
/Card
>
<
/Col
>
<
Col
span
=
{
12
}
>
<
Card
title
=
'系统配置'
headerExtraContent
=
{
<
span
style
=
{{
fontSize
:
'12px'
,
color
:
'var(--semi-color-text-1)'
}}
>
系统配置总览
<
/span>}
>
<
p
>
邮箱验证:
{
statusState
?.
status
?.
email_verification
===
true
?
'已启用'
:
'未启用'
}
<
/p
>
<
p
>
GitHub
身份验证:
{
statusState
?.
status
?.
github_oauth
===
true
?
'已启用'
:
'未启用'
}
<
/p
>
<
p
>
微信身份验证:
{
statusState
?.
status
?.
wechat_login
===
true
?
'已启用'
:
'未启用'
}
<
/p
>
<
p
>
Turnstile
用户校验:
{
statusState
?.
status
?.
turnstile_check
===
true
?
'已启用'
:
'未启用'
}
<
/p
>
<
p
>
Telegram
身份验证:
{
statusState
?.
status
?.
telegram_oauth
===
true
?
'已启用'
:
'未启用'
}
<
/p
>
<
/Card
>
<
/Col
>
<
/Row
>
<
/Card
>
<
/
>
:
<>
{
homePageContent
.
startsWith
(
'https://'
)
?
<
iframe
src
=
{
homePageContent
}
style
=
{{
width
:
'100%'
,
height
:
'100vh'
,
border
:
'none'
}}
/>
:
<
div
style
=
{{
fontSize
:
'larger'
}}
dangerouslySetInnerHTML
=
{{
__html
:
homePageContent
}}
><
/div
>
}
<
/
>
}
{
homePageContentLoaded
&&
homePageContent
===
''
?
(
<>
<
Card
bordered
=
{
false
}
headerLine
=
{
false
}
title
=
'系统状况'
bodyStyle
=
{{
padding
:
'10px 20px'
}}
>
<
Row
gutter
=
{
16
}
>
<
Col
span
=
{
12
}
>
<
Card
title
=
'系统信息'
headerExtraContent
=
{
<
span
style
=
{{
fontSize
:
'12px'
,
color
:
'var(--semi-color-text-1)'
,
}}
>
系统信息总览
<
/span
>
}
>
<
p
>
名称:
{
statusState
?.
status
?.
system_name
}
<
/p
>
<
p
>
版本:
{
statusState
?.
status
?.
version
?
statusState
?.
status
?.
version
:
'unknown'
}
<
/p
>
<
p
>
源码:
<
a
href
=
'https://github.com/songquanpeng/one-api'
target
=
'_blank'
rel
=
'noreferrer'
>
https
:
//github.com/songquanpeng/one-api
<
/a
>
<
/p
>
<
p
>
启动时间:
{
getStartTimeString
()}
<
/p
>
<
/Card
>
<
/Col
>
<
Col
span
=
{
12
}
>
<
Card
title
=
'系统配置'
headerExtraContent
=
{
<
span
style
=
{{
fontSize
:
'12px'
,
color
:
'var(--semi-color-text-1)'
,
}}
>
系统配置总览
<
/span
>
}
>
<
p
>
邮箱验证:
{
statusState
?.
status
?.
email_verification
===
true
?
'已启用'
:
'未启用'
}
<
/p
>
<
p
>
GitHub
身份验证:
{
statusState
?.
status
?.
github_oauth
===
true
?
'已启用'
:
'未启用'
}
<
/p
>
<
p
>
微信身份验证:
{
statusState
?.
status
?.
wechat_login
===
true
?
'已启用'
:
'未启用'
}
<
/p
>
<
p
>
Turnstile
用户校验:
{
statusState
?.
status
?.
turnstile_check
===
true
?
'已启用'
:
'未启用'
}
<
/p
>
<
p
>
Telegram
身份验证:
{
statusState
?.
status
?.
telegram_oauth
===
true
?
'已启用'
:
'未启用'
}
<
/p
>
<
/Card
>
<
/Col
>
<
/Row
>
<
/Card
>
<
/
>
)
:
(
<>
{
homePageContent
.
startsWith
(
'https://'
)
?
(
<
iframe
src
=
{
homePageContent
}
style
=
{{
width
:
'100%'
,
height
:
'100vh'
,
border
:
'none'
}}
/
>
)
:
(
<
div
style
=
{{
fontSize
:
'larger'
}}
dangerouslySetInnerHTML
=
{{
__html
:
homePageContent
}}
><
/div
>
)}
<
/
>
)}
<
/
>
);
};
export
default
Home
;
\ No newline at end of file
export
default
Home
;
web/src/pages/Redemption/EditRedemption.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
useNavigate
,
useParams
}
from
'react-router-dom'
;
import
{
API
,
downloadTextAsFile
,
isMobile
,
showError
,
showSuccess
}
from
'../../helpers'
;
import
{
API
,
downloadTextAsFile
,
isMobile
,
showError
,
showSuccess
,
}
from
'../../helpers'
;
import
{
renderQuotaWithPrompt
}
from
'../../helpers/render'
;
import
{
AutoComplete
,
Button
,
Input
,
Modal
,
SideSheet
,
Space
,
Spin
,
Typography
}
from
'@douyinfe/semi-ui'
;
import
{
AutoComplete
,
Button
,
Input
,
Modal
,
SideSheet
,
Space
,
Spin
,
Typography
,
}
from
'@douyinfe/semi-ui'
;
import
Title
from
'@douyinfe/semi-ui/lib/es/typography/title'
;
import
{
Divider
}
from
'semantic-ui-react'
;
...
...
@@ -15,7 +30,7 @@ const EditRedemption = (props) => {
const
originInputs
=
{
name
:
''
,
quota
:
100000
,
count
:
1
count
:
1
,
};
const
[
inputs
,
setInputs
]
=
useState
(
originInputs
);
const
{
name
,
quota
,
count
}
=
inputs
;
...
...
@@ -42,11 +57,9 @@ const EditRedemption = (props) => {
useEffect
(()
=>
{
if
(
isEdit
)
{
loadRedemption
().
then
(
()
=>
{
// console.log(inputs);
}
);
loadRedemption
().
then
(()
=>
{
// console.log(inputs);
});
}
else
{
setInputs
(
originInputs
);
}
...
...
@@ -60,10 +73,13 @@ const EditRedemption = (props) => {
localInputs
.
quota
=
parseInt
(
localInputs
.
quota
);
let
res
;
if
(
isEdit
)
{
res
=
await
API
.
put
(
`/api/redemption/`
,
{
...
localInputs
,
id
:
parseInt
(
props
.
editingRedemption
.
id
)
});
res
=
await
API
.
put
(
`/api/redemption/`
,
{
...
localInputs
,
id
:
parseInt
(
props
.
editingRedemption
.
id
),
});
}
else
{
res
=
await
API
.
post
(
`/api/redemption/`
,
{
...
localInputs
...
localInputs
,
});
}
const
{
success
,
message
,
data
}
=
res
.
data
;
...
...
@@ -97,7 +113,7 @@ const EditRedemption = (props) => {
),
onOk
:
()
=>
{
downloadTextAsFile
(
text
,
`
${
inputs
.
name
}
.txt`
);
}
}
,
});
}
setLoading
(
false
);
...
...
@@ -107,15 +123,28 @@ const EditRedemption = (props) => {
<>
<
SideSheet
placement
=
{
isEdit
?
'right'
:
'left'
}
title
=
{
<
Title
level
=
{
3
}
>
{
isEdit
?
'更新兑换码信息'
:
'创建新的兑换码'
}
<
/Title>
}
title
=
{
<
Title
level
=
{
3
}
>
{
isEdit
?
'更新兑换码信息'
:
'创建新的兑换码'
}
<
/Title
>
}
headerStyle
=
{{
borderBottom
:
'1px solid var(--semi-color-border)'
}}
bodyStyle
=
{{
borderBottom
:
'1px solid var(--semi-color-border)'
}}
visible
=
{
props
.
visiable
}
footer
=
{
<
div
style
=
{{
display
:
'flex'
,
justifyContent
:
'flex-end'
}}
>
<
Space
>
<
Button
theme
=
"solid"
size
=
{
'large'
}
onClick
=
{
submit
}
>
提交
<
/Button
>
<
Button
theme
=
"solid"
size
=
{
'large'
}
type
=
{
'tertiary'
}
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
<
Button
theme
=
'solid'
size
=
{
'large'
}
onClick
=
{
submit
}
>
提交
<
/Button
>
<
Button
theme
=
'solid'
size
=
{
'large'
}
type
=
{
'tertiary'
}
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
<
/Space
>
<
/div
>
}
...
...
@@ -126,12 +155,12 @@ const EditRedemption = (props) => {
<
Spin
spinning
=
{
loading
}
>
<
Input
style
=
{{
marginTop
:
20
}}
label
=
"名称"
name
=
"name"
label
=
'名称'
name
=
'name'
placeholder
=
{
'请输入名称'
}
onChange
=
{
value
=>
handleInputChange
(
'name'
,
value
)}
onChange
=
{
(
value
)
=>
handleInputChange
(
'name'
,
value
)}
value
=
{
name
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
required
=
{
!
isEdit
}
/
>
<
Divider
/>
...
...
@@ -140,12 +169,12 @@ const EditRedemption = (props) => {
<
/div
>
<
AutoComplete
style
=
{{
marginTop
:
8
}}
name
=
"quota"
name
=
'quota'
placeholder
=
{
'请输入额度'
}
onChange
=
{(
value
)
=>
handleInputChange
(
'quota'
,
value
)}
value
=
{
quota
}
autoComplete
=
"new-password"
type
=
"number"
autoComplete
=
'new-password'
type
=
'number'
position
=
{
'bottom'
}
data
=
{[
{
value
:
500000
,
label
:
'1$'
},
...
...
@@ -153,25 +182,25 @@ const EditRedemption = (props) => {
{
value
:
25000000
,
label
:
'50$'
},
{
value
:
50000000
,
label
:
'100$'
},
{
value
:
250000000
,
label
:
'500$'
},
{
value
:
500000000
,
label
:
'1000$'
}
{
value
:
500000000
,
label
:
'1000$'
}
,
]}
/
>
{
!
isEdit
&&
<>
{
!
isEdit
&&
(
<>
<
Divider
/>
<
Typography
.
Text
>
生成数量
<
/Typography.Text
>
<
Input
style
=
{{
marginTop
:
8
}}
label
=
"生成数量"
name
=
"count"
label
=
'生成数量'
name
=
'count'
placeholder
=
{
'请输入生成数量'
}
onChange
=
{
value
=>
handleInputChange
(
'count'
,
value
)}
onChange
=
{
(
value
)
=>
handleInputChange
(
'count'
,
value
)}
value
=
{
count
}
autoComplete
=
"new-password"
type
=
"number"
autoComplete
=
'new-password'
type
=
'number'
/>
<
/
>
}
)
}
<
/Spin
>
<
/SideSheet
>
<
/
>
...
...
web/src/pages/Redemption/index.js
View file @
eb955ae4
import
React
from
'react'
;
import
RedemptionsTable
from
'../../components/RedemptionsTable'
;
import
{
Layout
}
from
"@douyinfe/semi-ui"
;
import
{
Layout
}
from
'@douyinfe/semi-ui'
;
const
Redemption
=
()
=>
(
<>
<
Layout
>
<
Layout
.
Header
>
<
h3
>
管理兑换码
<
/h3
>
<
/Layout.Header
>
<
Layout
.
Content
>
<
RedemptionsTable
/>
<
/Layout.Content
>
<
/Layout
>
<
Layout
>
<
Layout
.
Header
>
<
h3
>
管理兑换码
<
/h3
>
<
/Layout.Header
>
<
Layout
.
Content
>
<
RedemptionsTable
/>
<
/Layout.Content
>
<
/Layout
>
<
/
>
);
...
...
web/src/pages/Setting/index.js
View file @
eb955ae4
import
React
from
'react'
;
import
SystemSetting
from
'../../components/SystemSetting'
;
import
{
isRoot
}
from
'../../helpers'
;
import
{
isRoot
}
from
'../../helpers'
;
import
OtherSetting
from
'../../components/OtherSetting'
;
import
PersonalSetting
from
'../../components/PersonalSetting'
;
import
OperationSetting
from
'../../components/OperationSetting'
;
import
{
Layout
,
TabPane
,
Tabs
}
from
"@douyinfe/semi-ui"
;
import
{
Layout
,
TabPane
,
Tabs
}
from
'@douyinfe/semi-ui'
;
const
Setting
=
()
=>
{
let
panes
=
[
{
tab
:
'个人设置'
,
content
:
<
PersonalSetting
/>
,
itemKey
:
'1'
}
];
let
panes
=
[
{
tab
:
'个人设置'
,
content
:
<
PersonalSetting
/>
,
itemKey
:
'1'
,
},
];
if
(
isRoot
())
{
panes
.
push
({
tab
:
'运营设置'
,
content
:
<
OperationSetting
/>
,
itemKey
:
'2'
});
panes
.
push
({
tab
:
'系统设置'
,
content
:
<
SystemSetting
/>
,
itemKey
:
'3'
});
panes
.
push
({
tab
:
'其他设置'
,
content
:
<
OtherSetting
/>
,
itemKey
:
'4'
});
}
if
(
isRoot
())
{
panes
.
push
({
tab
:
'运营设置'
,
content
:
<
OperationSetting
/>
,
itemKey
:
'2'
,
});
panes
.
push
({
tab
:
'系统设置'
,
content
:
<
SystemSetting
/>
,
itemKey
:
'3'
,
});
panes
.
push
({
tab
:
'其他设置'
,
content
:
<
OtherSetting
/>
,
itemKey
:
'4'
,
});
}
return
(
<
div
>
<
Layout
>
<
Layout
.
Content
>
<
Tabs
type
=
"line"
defaultActiveKey
=
"1"
>
{
panes
.
map
(
pane
=>
(
<
TabPane
itemKey
=
{
pane
.
itemKey
}
tab
=
{
pane
.
tab
}
>
{
pane
.
content
}
<
/TabPane
>
))}
<
/Tabs
>
<
/Layout.Content
>
<
/Layout
>
<
/div
>
);
return
(
<
div
>
<
Layout
>
<
Layout
.
Content
>
<
Tabs
type
=
'line'
defaultActiveKey
=
'1'
>
{
panes
.
map
((
pane
)
=>
(
<
TabPane
itemKey
=
{
pane
.
itemKey
}
tab
=
{
pane
.
tab
}
>
{
pane
.
content
}
<
/TabPane
>
))}
<
/Tabs
>
<
/Layout.Content
>
<
/Layout
>
<
/div
>
);
};
export
default
Setting
;
web/src/pages/Token/EditToken.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
useNavigate
}
from
'react-router-dom'
;
import
{
API
,
isMobile
,
showError
,
showSuccess
,
timestamp2string
}
from
'../../helpers'
;
import
{
API
,
isMobile
,
showError
,
showSuccess
,
timestamp2string
,
}
from
'../../helpers'
;
import
{
renderQuotaWithPrompt
}
from
'../../helpers/render'
;
import
{
AutoComplete
,
Banner
,
Button
,
Checkbox
,
DatePicker
,
Input
,
Select
,
SideSheet
,
Space
,
Spin
,
Typography
AutoComplete
,
Banner
,
Button
,
Checkbox
,
DatePicker
,
Input
,
Select
,
SideSheet
,
Space
,
Spin
,
Typography
,
}
from
'@douyinfe/semi-ui'
;
import
Title
from
'@douyinfe/semi-ui/lib/es/typography/title'
;
import
{
Divider
}
from
'semantic-ui-react'
;
...
...
@@ -27,10 +33,17 @@ const EditToken = (props) => {
expired_time
:
-
1
,
unlimited_quota
:
false
,
model_limits_enabled
:
false
,
model_limits
:
[]
model_limits
:
[]
,
};
const
[
inputs
,
setInputs
]
=
useState
(
originInputs
);
const
{
name
,
remain_quota
,
expired_time
,
unlimited_quota
,
model_limits_enabled
,
model_limits
}
=
inputs
;
const
{
name
,
remain_quota
,
expired_time
,
unlimited_quota
,
model_limits_enabled
,
model_limits
,
}
=
inputs
;
// const [visible, setVisible] = useState(false);
const
[
models
,
setModels
]
=
useState
({});
const
navigate
=
useNavigate
();
...
...
@@ -65,7 +78,7 @@ const EditToken = (props) => {
if
(
success
)
{
let
localModelOptions
=
data
.
map
((
model
)
=>
({
label
:
model
,
value
:
model
value
:
model
,
}));
setModels
(
localModelOptions
);
}
else
{
...
...
@@ -100,11 +113,9 @@ const EditToken = (props) => {
if
(
!
isEdit
)
{
setInputs
(
originInputs
);
}
else
{
loadToken
().
then
(
()
=>
{
// console.log(inputs);
}
);
loadToken
().
then
(()
=>
{
// console.log(inputs);
});
}
loadModels
();
},
[
isEdit
]);
...
...
@@ -123,10 +134,13 @@ const EditToken = (props) => {
// 生成一个随机的四位字母数字字符串
const
generateRandomSuffix
=
()
=>
{
const
characters
=
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
;
const
characters
=
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
;
let
result
=
''
;
for
(
let
i
=
0
;
i
<
6
;
i
++
)
{
result
+=
characters
.
charAt
(
Math
.
floor
(
Math
.
random
()
*
characters
.
length
));
result
+=
characters
.
charAt
(
Math
.
floor
(
Math
.
random
()
*
characters
.
length
),
);
}
return
result
;
};
...
...
@@ -147,7 +161,10 @@ const EditToken = (props) => {
localInputs
.
expired_time
=
Math
.
ceil
(
time
/
1000
);
}
localInputs
.
model_limits
=
localInputs
.
model_limits
.
join
(
','
);
let
res
=
await
API
.
put
(
`/api/token/`
,
{
...
localInputs
,
id
:
parseInt
(
props
.
editingToken
.
id
)
});
let
res
=
await
API
.
put
(
`/api/token/`
,
{
...
localInputs
,
id
:
parseInt
(
props
.
editingToken
.
id
),
});
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
showSuccess
(
'令牌更新成功!'
);
...
...
@@ -189,7 +206,9 @@ const EditToken = (props) => {
}
if
(
successCount
>
0
)
{
showSuccess
(
`
${
successCount
}
个令牌创建成功,请在列表页面点击复制获取令牌!`
);
showSuccess
(
`
${
successCount
}
个令牌创建成功,请在列表页面点击复制获取令牌!`
,
);
props
.
refresh
();
props
.
handleClose
();
}
...
...
@@ -199,20 +218,30 @@ const EditToken = (props) => {
setTokenCount
(
1
);
// 重置数量为默认值
};
return
(
<>
<
SideSheet
placement
=
{
isEdit
?
'right'
:
'left'
}
title
=
{
<
Title
level
=
{
3
}
>
{
isEdit
?
'更新令牌信息'
:
'创建新的令牌'
}
<
/Title>
}
title
=
{
<
Title
level
=
{
3
}
>
{
isEdit
?
'更新令牌信息'
:
'创建新的令牌'
}
<
/Title
>
}
headerStyle
=
{{
borderBottom
:
'1px solid var(--semi-color-border)'
}}
bodyStyle
=
{{
borderBottom
:
'1px solid var(--semi-color-border)'
}}
visible
=
{
props
.
visiable
}
footer
=
{
<
div
style
=
{{
display
:
'flex'
,
justifyContent
:
'flex-end'
}}
>
<
Space
>
<
Button
theme
=
"solid"
size
=
{
'large'
}
onClick
=
{
submit
}
>
提交
<
/Button
>
<
Button
theme
=
"solid"
size
=
{
'large'
}
type
=
{
'tertiary'
}
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
<
Button
theme
=
'solid'
size
=
{
'large'
}
onClick
=
{
submit
}
>
提交
<
/Button
>
<
Button
theme
=
'solid'
size
=
{
'large'
}
type
=
{
'tertiary'
}
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
<
/Space
>
<
/div
>
}
...
...
@@ -223,55 +252,79 @@ const EditToken = (props) => {
<
Spin
spinning
=
{
loading
}
>
<
Input
style
=
{{
marginTop
:
20
}}
label
=
"名称"
name
=
"name"
label
=
'名称'
name
=
'name'
placeholder
=
{
'请输入名称'
}
onChange
=
{(
value
)
=>
handleInputChange
(
'name'
,
value
)}
value
=
{
name
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
required
=
{
!
isEdit
}
/
>
<
Divider
/>
<
DatePicker
label
=
"过期时间"
name
=
"expired_time"
label
=
'过期时间'
name
=
'expired_time'
placeholder
=
{
'请选择过期时间'
}
onChange
=
{(
value
)
=>
handleInputChange
(
'expired_time'
,
value
)}
value
=
{
expired_time
}
autoComplete
=
"new-password"
type
=
"dateTime"
autoComplete
=
'new-password'
type
=
'dateTime'
/>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Space
>
<
Button
type
=
{
'tertiary'
}
onClick
=
{()
=>
{
setExpiredTime
(
0
,
0
,
0
,
0
);
}}
>
永不过期
<
/Button
>
<
Button
type
=
{
'tertiary'
}
onClick
=
{()
=>
{
setExpiredTime
(
0
,
0
,
1
,
0
);
}}
>
一小时
<
/Button
>
<
Button
type
=
{
'tertiary'
}
onClick
=
{()
=>
{
setExpiredTime
(
1
,
0
,
0
,
0
);
}}
>
一个月
<
/Button
>
<
Button
type
=
{
'tertiary'
}
onClick
=
{()
=>
{
setExpiredTime
(
0
,
1
,
0
,
0
);
}}
>
一天
<
/Button
>
<
Button
type
=
{
'tertiary'
}
onClick
=
{()
=>
{
setExpiredTime
(
0
,
0
,
0
,
0
);
}}
>
永不过期
<
/Button
>
<
Button
type
=
{
'tertiary'
}
onClick
=
{()
=>
{
setExpiredTime
(
0
,
0
,
1
,
0
);
}}
>
一小时
<
/Button
>
<
Button
type
=
{
'tertiary'
}
onClick
=
{()
=>
{
setExpiredTime
(
1
,
0
,
0
,
0
);
}}
>
一个月
<
/Button
>
<
Button
type
=
{
'tertiary'
}
onClick
=
{()
=>
{
setExpiredTime
(
0
,
1
,
0
,
0
);
}}
>
一天
<
/Button
>
<
/Space
>
<
/div
>
<
Divider
/>
<
Banner
type
=
{
'warning'
}
description
=
{
'注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。'
}
><
/Banner
>
<
Banner
type
=
{
'warning'
}
description
=
{
'注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。'
}
><
/Banner
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Typography
.
Text
>
{
`额度
${
renderQuotaWithPrompt
(
remain_quota
)}
`
}
<
/Typography.Text
>
<
/div
>
<
AutoComplete
style
=
{{
marginTop
:
8
}}
name
=
"remain_quota"
name
=
'remain_quota'
placeholder
=
{
'请输入额度'
}
onChange
=
{(
value
)
=>
handleInputChange
(
'remain_quota'
,
value
)}
value
=
{
remain_quota
}
autoComplete
=
"new-password"
type
=
"number"
autoComplete
=
'new-password'
type
=
'number'
// position={'top'}
data
=
{[
{
value
:
500000
,
label
:
'1$'
},
...
...
@@ -279,7 +332,7 @@ const EditToken = (props) => {
{
value
:
25000000
,
label
:
'50$'
},
{
value
:
50000000
,
label
:
'100$'
},
{
value
:
250000000
,
label
:
'500$'
},
{
value
:
500000000
,
label
:
'1000$'
}
{
value
:
500000000
,
label
:
'1000$'
}
,
]}
disabled
=
{
unlimited_quota
}
/
>
...
...
@@ -291,18 +344,18 @@ const EditToken = (props) => {
<
/div
>
<
AutoComplete
style
=
{{
marginTop
:
8
}}
label
=
"数量"
label
=
'数量'
placeholder
=
{
'请选择或输入创建令牌的数量'
}
onChange
=
{(
value
)
=>
handleTokenCountChange
(
value
)}
onSelect
=
{(
value
)
=>
handleTokenCountChange
(
value
)}
value
=
{
tokenCount
.
toString
()}
autoComplete
=
"off"
type
=
"number"
autoComplete
=
'off'
type
=
'number'
data
=
{[
{
value
:
10
,
label
:
'10个'
},
{
value
:
20
,
label
:
'20个'
},
{
value
:
30
,
label
:
'30个'
},
{
value
:
100
,
label
:
'100个'
}
{
value
:
100
,
label
:
'100个'
}
,
]}
disabled
=
{
unlimited_quota
}
/
>
...
...
@@ -310,35 +363,44 @@ const EditToken = (props) => {
)}
<
div
>
<
Button
style
=
{{
marginTop
:
8
}}
type
=
{
'warning'
}
onClick
=
{()
=>
{
setUnlimitedQuota
();
}}
>
{
unlimited_quota
?
'取消无限额度'
:
'设为无限额度'
}
<
/Button
>
<
Button
style
=
{{
marginTop
:
8
}}
type
=
{
'warning'
}
onClick
=
{()
=>
{
setUnlimitedQuota
();
}}
>
{
unlimited_quota
?
'取消无限额度'
:
'设为无限额度'
}
<
/Button
>
<
/div
>
<
Divider
/>
<
div
style
=
{{
marginTop
:
10
,
display
:
'flex'
}}
>
<
Space
>
<
Checkbox
name
=
"model_limits_enabled"
name
=
'model_limits_enabled'
checked
=
{
model_limits_enabled
}
onChange
=
{(
e
)
=>
handleInputChange
(
'model_limits_enabled'
,
e
.
target
.
checked
)}
>
<
/Checkbox
>
<
Typography
.
Text
>
启用模型限制(非必要,不建议启用)
<
/Typography.Text
>
onChange
=
{(
e
)
=>
handleInputChange
(
'model_limits_enabled'
,
e
.
target
.
checked
)
}
><
/Checkbox
>
<
Typography
.
Text
>
启用模型限制(非必要,不建议启用)
<
/Typography.Text
>
<
/Space
>
<
/div
>
<
Select
style
=
{{
marginTop
:
8
}}
placeholder
=
{
'请选择该渠道所支持的模型'
}
name
=
"models"
name
=
'models'
required
multiple
selection
onChange
=
{
value
=>
{
onChange
=
{
(
value
)
=>
{
handleInputChange
(
'model_limits'
,
value
);
}}
value
=
{
inputs
.
model_limits
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
optionList
=
{
models
}
disabled
=
{
!
model_limits_enabled
}
/
>
...
...
web/src/pages/Token/index.js
View file @
eb955ae4
import
React
from
'react'
;
import
TokensTable
from
'../../components/TokensTable'
;
import
{
Layout
}
from
"@douyinfe/semi-ui"
;
import
{
Layout
}
from
'@douyinfe/semi-ui'
;
const
Token
=
()
=>
(
<>
<
Layout
>
<
Layout
.
Header
>
<
h3
>
我的令牌
<
/h3
>
<
h3
>
我的令牌
<
/h3
>
<
/Layout.Header
>
<
Layout
.
Content
>
<
TokensTable
/>
<
TokensTable
/>
<
/Layout.Content
>
<
/Layout
>
<
/
>
...
...
web/src/pages/TopUp/index.js
View file @
eb955ae4
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
API
,
isMobile
,
showError
,
showInfo
,
showSuccess
}
from
'../../helpers'
;
import
{
renderNumber
,
renderQuota
}
from
'../../helpers/render'
;
import
{
Col
,
Layout
,
Row
,
Typography
,
Card
,
Button
,
Form
,
Divider
,
Space
,
Modal
}
from
"@douyinfe/semi-ui"
;
import
Title
from
"@douyinfe/semi-ui/lib/es/typography/title"
;
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
API
,
isMobile
,
showError
,
showInfo
,
showSuccess
}
from
'../../helpers'
;
import
{
renderNumber
,
renderQuota
}
from
'../../helpers/render'
;
import
{
Col
,
Layout
,
Row
,
Typography
,
Card
,
Button
,
Form
,
Divider
,
Space
,
Modal
,
}
from
'@douyinfe/semi-ui'
;
import
Title
from
'@douyinfe/semi-ui/lib/es/typography/title'
;
import
Text
from
'@douyinfe/semi-ui/lib/es/typography/text'
;
import
{
Link
}
from
'react-router-dom'
;
const
TopUp
=
()
=>
{
const
[
redemptionCode
,
setRedemptionCode
]
=
useState
(
''
);
const
[
topUpCode
,
setTopUpCode
]
=
useState
(
''
);
const
[
topUpCount
,
setTopUpCount
]
=
useState
(
10
);
const
[
minTopupCount
,
setMinTopUpCount
]
=
useState
(
1
);
const
[
amount
,
setAmount
]
=
useState
(
0.0
);
const
[
minTopUp
,
setMinTopUp
]
=
useState
(
1
);
const
[
topUpLink
,
setTopUpLink
]
=
useState
(
''
);
const
[
enableOnlineTopUp
,
setEnableOnlineTopUp
]
=
useState
(
false
);
const
[
userQuota
,
setUserQuota
]
=
useState
(
0
);
const
[
isSubmitting
,
setIsSubmitting
]
=
useState
(
false
);
const
[
open
,
setOpen
]
=
useState
(
false
);
const
[
payWay
,
setPayWay
]
=
useState
(
''
);
const
[
redemptionCode
,
setRedemptionCode
]
=
useState
(
''
);
const
[
topUpCode
,
setTopUpCode
]
=
useState
(
''
);
const
[
topUpCount
,
setTopUpCount
]
=
useState
(
10
);
const
[
minTopupCount
,
setMinTopUpCount
]
=
useState
(
1
);
const
[
amount
,
setAmount
]
=
useState
(
0.0
);
const
[
minTopUp
,
setMinTopUp
]
=
useState
(
1
);
const
[
topUpLink
,
setTopUpLink
]
=
useState
(
''
);
const
[
enableOnlineTopUp
,
setEnableOnlineTopUp
]
=
useState
(
false
);
const
[
userQuota
,
setUserQuota
]
=
useState
(
0
);
const
[
isSubmitting
,
setIsSubmitting
]
=
useState
(
false
);
const
[
open
,
setOpen
]
=
useState
(
false
);
const
[
payWay
,
setPayWay
]
=
useState
(
''
);
const
topUp
=
async
()
=>
{
if
(
redemptionCode
===
''
)
{
showInfo
(
'请输入兑换码!'
)
return
;
}
setIsSubmitting
(
true
);
try
{
const
res
=
await
API
.
post
(
'/api/user/topup'
,
{
key
:
redemptionCode
});
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
showSuccess
(
'兑换成功!'
);
Modal
.
success
({
title
:
'兑换成功!'
,
content
:
'成功兑换额度:'
+
renderQuota
(
data
),
centered
:
true
});
setUserQuota
((
quota
)
=>
{
return
quota
+
data
;
});
setRedemptionCode
(
''
);
}
else
{
showError
(
message
);
}
}
catch
(
err
)
{
showError
(
'请求失败'
);
}
finally
{
setIsSubmitting
(
false
);
}
};
const
openTopUpLink
=
()
=>
{
if
(
!
topUpLink
)
{
showError
(
'超级管理员未设置充值链接!'
);
return
;
}
window
.
open
(
topUpLink
,
'_blank'
);
};
const
preTopUp
=
async
(
payment
)
=>
{
if
(
!
enableOnlineTopUp
)
{
showError
(
'管理员未开启在线充值!'
);
return
;
}
if
(
amount
===
0
)
{
await
getAmount
();
}
if
(
topUpCount
<
minTopUp
)
{
showInfo
(
'充值数量不能小于'
+
minTopUp
);
return
;
}
setPayWay
(
payment
)
setOpen
(
true
);
const
topUp
=
async
()
=>
{
if
(
redemptionCode
===
''
)
{
showInfo
(
'请输入兑换码!'
);
return
;
}
setIsSubmitting
(
true
);
try
{
const
res
=
await
API
.
post
(
'/api/user/topup'
,
{
key
:
redemptionCode
,
});
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
showSuccess
(
'兑换成功!'
);
Modal
.
success
({
title
:
'兑换成功!'
,
content
:
'成功兑换额度:'
+
renderQuota
(
data
),
centered
:
true
,
});
setUserQuota
((
quota
)
=>
{
return
quota
+
data
;
});
setRedemptionCode
(
''
);
}
else
{
showError
(
message
);
}
}
catch
(
err
)
{
showError
(
'请求失败'
);
}
finally
{
setIsSubmitting
(
false
);
}
};
const
onlineTopUp
=
async
()
=>
{
if
(
amount
===
0
)
{
await
getAmount
();
}
if
(
topUpCount
<
minTopUp
)
{
showInfo
(
'充值数量不能小于'
+
minTopUp
);
return
;
}
setOpen
(
false
);
try
{
const
res
=
await
API
.
post
(
'/api/user/pay'
,
{
amount
:
parseInt
(
topUpCount
),
top_up_code
:
topUpCode
,
payment_method
:
payWay
});
if
(
res
!==
undefined
)
{
const
{
message
,
data
}
=
res
.
data
;
// showInfo(message);
if
(
message
===
'success'
)
{
const
openTopUpLink
=
()
=>
{
if
(
!
topUpLink
)
{
showError
(
'超级管理员未设置充值链接!'
);
return
;
}
window
.
open
(
topUpLink
,
'_blank'
);
};
let
params
=
data
let
url
=
res
.
data
.
url
let
form
=
document
.
createElement
(
'form'
)
form
.
action
=
url
form
.
method
=
'POST'
// 判断是否为safari浏览器
let
isSafari
=
navigator
.
userAgent
.
indexOf
(
"Safari"
)
>
-
1
&&
navigator
.
userAgent
.
indexOf
(
"Chrome"
)
<
1
;
if
(
!
isSafari
)
{
form
.
target
=
'_blank'
}
for
(
let
key
in
params
)
{
let
input
=
document
.
createElement
(
'input'
)
input
.
type
=
'hidden'
input
.
name
=
key
input
.
value
=
params
[
key
]
form
.
appendChild
(
input
)
}
document
.
body
.
appendChild
(
form
)
form
.
submit
()
document
.
body
.
removeChild
(
form
)
}
else
{
showError
(
data
);
// setTopUpCount(parseInt(res.data.count));
// setAmount(parseInt(data));
}
}
else
{
showError
(
res
);
}
}
catch
(
err
)
{
console
.
log
(
err
);
}
finally
{
}
const
preTopUp
=
async
(
payment
)
=>
{
if
(
!
enableOnlineTopUp
)
{
showError
(
'管理员未开启在线充值!'
);
return
;
}
if
(
amount
===
0
)
{
await
getAmount
();
}
if
(
topUpCount
<
minTopUp
)
{
showInfo
(
'充值数量不能小于'
+
minTopUp
);
return
;
}
setPayWay
(
payment
);
setOpen
(
true
);
};
const
getUserQuota
=
async
()
=>
{
let
res
=
await
API
.
get
(
`/api/user/self`
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
setUserQuota
(
data
.
quota
);
const
onlineTopUp
=
async
()
=>
{
if
(
amount
===
0
)
{
await
getAmount
();
}
if
(
topUpCount
<
minTopUp
)
{
showInfo
(
'充值数量不能小于'
+
minTopUp
);
return
;
}
setOpen
(
false
);
try
{
const
res
=
await
API
.
post
(
'/api/user/pay'
,
{
amount
:
parseInt
(
topUpCount
),
top_up_code
:
topUpCode
,
payment_method
:
payWay
,
});
if
(
res
!==
undefined
)
{
const
{
message
,
data
}
=
res
.
data
;
// showInfo(message);
if
(
message
===
'success'
)
{
let
params
=
data
;
let
url
=
res
.
data
.
url
;
let
form
=
document
.
createElement
(
'form'
);
form
.
action
=
url
;
form
.
method
=
'POST'
;
// 判断是否为safari浏览器
let
isSafari
=
navigator
.
userAgent
.
indexOf
(
'Safari'
)
>
-
1
&&
navigator
.
userAgent
.
indexOf
(
'Chrome'
)
<
1
;
if
(
!
isSafari
)
{
form
.
target
=
'_blank'
;
}
for
(
let
key
in
params
)
{
let
input
=
document
.
createElement
(
'input'
);
input
.
type
=
'hidden'
;
input
.
name
=
key
;
input
.
value
=
params
[
key
];
form
.
appendChild
(
input
);
}
document
.
body
.
appendChild
(
form
);
form
.
submit
();
document
.
body
.
removeChild
(
form
);
}
else
{
showError
(
message
);
showError
(
data
);
// setTopUpCount(parseInt(res.data.count));
// setAmount(parseInt(data));
}
}
else
{
showError
(
res
);
}
}
catch
(
err
)
{
console
.
log
(
err
);
}
finally
{
}
};
useEffect
(()
=>
{
let
status
=
localStorage
.
getItem
(
'status'
);
if
(
status
)
{
status
=
JSON
.
parse
(
status
);
if
(
status
.
top_up_link
)
{
setTopUpLink
(
status
.
top_up_link
);
}
if
(
status
.
min_topup
)
{
setMinTopUp
(
status
.
min_topup
);
}
if
(
status
.
enable_online_topup
)
{
setEnableOnlineTopUp
(
status
.
enable_online_topup
);
}
}
getUserQuota
().
then
();
},
[]);
const
getUserQuota
=
async
()
=>
{
let
res
=
await
API
.
get
(
`/api/user/self`
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
setUserQuota
(
data
.
quota
);
}
else
{
showError
(
message
);
}
};
const
renderAmount
=
()
=>
{
// console.log(amount);
return
amount
+
'元'
;
useEffect
(()
=>
{
let
status
=
localStorage
.
getItem
(
'status'
);
if
(
status
)
{
status
=
JSON
.
parse
(
status
);
if
(
status
.
top_up_link
)
{
setTopUpLink
(
status
.
top_up_link
);
}
if
(
status
.
min_topup
)
{
setMinTopUp
(
status
.
min_topup
);
}
if
(
status
.
enable_online_topup
)
{
setEnableOnlineTopUp
(
status
.
enable_online_topup
);
}
}
getUserQuota
().
then
();
},
[]);
const
getAmount
=
async
(
value
)
=>
{
if
(
value
===
undefined
)
{
value
=
topUpCount
;
}
try
{
const
res
=
await
API
.
post
(
'/api/user/amount'
,
{
amount
:
parseFloat
(
value
),
top_up_code
:
topUpCode
});
if
(
res
!==
undefined
)
{
const
{
message
,
data
}
=
res
.
data
;
// showInfo(message);
if
(
message
===
'success'
)
{
setAmount
(
parseFloat
(
data
));
}
else
{
showError
(
data
);
// setTopUpCount(parseInt(res.data.count));
// setAmount(parseInt(data));
}
}
else
{
showError
(
res
);
}
}
catch
(
err
)
{
console
.
log
(
err
);
}
finally
{
const
renderAmount
=
()
=>
{
// console.log(amount);
return
amount
+
'元'
;
};
const
getAmount
=
async
(
value
)
=>
{
if
(
value
===
undefined
)
{
value
=
topUpCount
;
}
try
{
const
res
=
await
API
.
post
(
'/api/user/amount'
,
{
amount
:
parseFloat
(
value
),
top_up_code
:
topUpCode
,
});
if
(
res
!==
undefined
)
{
const
{
message
,
data
}
=
res
.
data
;
// showInfo(message);
if
(
message
===
'success'
)
{
setAmount
(
parseFloat
(
data
));
}
else
{
showError
(
data
);
// setTopUpCount(parseInt(res.data.count));
// setAmount(parseInt(data));
}
}
else
{
showError
(
res
);
}
}
catch
(
err
)
{
console
.
log
(
err
);
}
finally
{
}
};
const
handleCancel
=
()
=>
{
setOpen
(
false
);
}
const
handleCancel
=
()
=>
{
setOpen
(
false
);
};
return
(
<
div
>
<
Layout
>
<
Layout
.
Header
>
<
h3
>
我的钱包
<
/h3
>
<
/Layout.Header
>
<
Layout
.
Content
>
<
Modal
title
=
"确定要充值吗"
visible
=
{
open
}
onOk
=
{
onlineTopUp
}
onCancel
=
{
handleCancel
}
maskClosable
=
{
false
}
size
=
{
'small'
}
centered
=
{
true
}
return
(
<
div
>
<
Layout
>
<
Layout
.
Header
>
<
h3
>
我的钱包
<
/h3
>
<
/Layout.Header
>
<
Layout
.
Content
>
<
Modal
title
=
'确定要充值吗'
visible
=
{
open
}
onOk
=
{
onlineTopUp
}
onCancel
=
{
handleCancel
}
maskClosable
=
{
false
}
size
=
{
'small'
}
centered
=
{
true
}
>
<
p
>
充值数量:
{
topUpCount
}
$
<
/p
>
<
p
>
实付金额:
{
renderAmount
()}
<
/p
>
<
p
>
是否确认充值?
<
/p
>
<
/Modal
>
<
div
style
=
{{
marginTop
:
20
,
display
:
'flex'
,
justifyContent
:
'center'
}}
>
<
Card
style
=
{{
width
:
'500px'
,
padding
:
'20px'
}}
>
<
Title
level
=
{
3
}
style
=
{{
textAlign
:
'center'
}}
>
余额
{
renderQuota
(
userQuota
)}
<
/Title
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Divider
>
兑换余额
<
/Divider
>
<
Form
>
<
Form
.
Input
field
=
{
'redemptionCode'
}
label
=
{
'兑换码'
}
placeholder
=
'兑换码'
name
=
'redemptionCode'
value
=
{
redemptionCode
}
onChange
=
{(
value
)
=>
{
setRedemptionCode
(
value
);
}}
/
>
<
Space
>
{
topUpLink
?
(
<
Button
type
=
{
'primary'
}
theme
=
{
'solid'
}
onClick
=
{
openTopUpLink
}
>
获取兑换码
<
/Button
>
)
:
null
}
<
Button
type
=
{
'warning'
}
theme
=
{
'solid'
}
onClick
=
{
topUp
}
disabled
=
{
isSubmitting
}
>
<
p
>
充值数量:
{
topUpCount
}
$
<
/p
>
<
p
>
实付金额:
{
renderAmount
()}
<
/p
>
<
p
>
是否确认充值?
<
/p
>
<
/Modal
>
<
div
style
=
{{
marginTop
:
20
,
display
:
'flex'
,
justifyContent
:
'center'
}}
>
<
Card
style
=
{{
width
:
'500px'
,
padding
:
'20px'
}}
>
<
Title
level
=
{
3
}
style
=
{{
textAlign
:
'center'
}}
>
余额
{
renderQuota
(
userQuota
)}
<
/Title
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Divider
>
兑换余额
<
/Divider
>
<
Form
>
<
Form
.
Input
field
=
{
'redemptionCode'
}
label
=
{
'兑换码'
}
placeholder
=
'兑换码'
name
=
'redemptionCode'
value
=
{
redemptionCode
}
onChange
=
{(
value
)
=>
{
setRedemptionCode
(
value
);
}}
/
>
<
Space
>
{
topUpLink
?
<
Button
type
=
{
'primary'
}
theme
=
{
'solid'
}
onClick
=
{
openTopUpLink
}
>
获取兑换码
<
/Button> : nul
l
}
<
Button
type
=
{
"warning"
}
theme
=
{
'solid'
}
onClick
=
{
topUp
}
disabled
=
{
isSubmitting
}
>
{
isSubmitting
?
'兑换中...'
:
'兑换'
}
<
/Button
>
<
/Space
>
<
/Form
>
<
/div
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Divider
>
在线充值
<
/Divider
>
<
Form
>
<
Form
.
Input
disabled
=
{
!
enableOnlineTopUp
}
field
=
{
'redemptionCount'
}
label
=
{
'实付金额:'
+
renderAmount
()}
placeholder
=
{
'充值数量,最低'
+
minTopUp
+
'$'
}
name
=
'redemptionCount'
type
=
{
'number'
}
value
=
{
topUpCount
}
suffix
=
{
'$'
}
min
=
{
minTopUp
}
defaultValue
=
{
minTopUp
}
max
=
{
100000
}
onChange
=
{
async
(
value
)
=>
{
if
(
value
<
1
)
{
value
=
1
;
}
if
(
value
>
100000
)
{
value
=
100000
;
}
setTopUpCount
(
value
);
await
getAmount
(
value
);
}}
/
>
<
Space
>
<
Button
type
=
{
'primary'
}
theme
=
{
'solid'
}
onClick
=
{
async
()
=>
{
preTopUp
(
'zfb'
)
}
}
>
支付宝
<
/Button
>
<
Button
style
=
{{
backgroundColor
:
'rgba(var(--semi-green-5), 1)'
}}
type
=
{
'primary'
}
theme
=
{
'solid'
}
onClick
=
{
async
()
=>
{
preTopUp
(
'wx'
)
}
}
>
微信
<
/Button
>
<
/Space
>
<
/Form
>
<
/div
>
{
/*<div style={{ display: 'flex', justifyContent: 'right' }}>*/
}
{
/* <Text>*/
}
{
/* <Link onClick={*/
}
{
/* async () => {*/
}
{
/* window.location.href = '/topup/history'*/
}
{
/* }*/
}
{
/* }>充值记录</Link>*/
}
{
/* </Text>*/
}
{
/*</div>*/
}
<
/Card
>
<
/div
>
<
/Layout.Content
>
<
/Layout
>
<
/div
>
);
{
isSubmitting
?
'兑换中...'
:
'兑换'
}
<
/Button
>
<
/Space
>
<
/Form
>
<
/div
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Divider
>
在线充值
<
/Divider
>
<
Form
>
<
Form
.
Input
disabled
=
{
!
enableOnlineTopUp
}
field
=
{
'redemptionCount'
}
label
=
{
'实付金额:'
+
renderAmount
()}
placeholder
=
{
'充值数量,最低'
+
minTopUp
+
'$'
}
name
=
'redemptionCount'
type
=
{
'number'
}
value
=
{
topUpCount
}
suffix
=
{
'$'
}
min
=
{
minTopUp
}
defaultValue
=
{
minTopUp
}
max
=
{
100000
}
onChange
=
{
async
(
value
)
=>
{
if
(
value
<
1
)
{
value
=
1
;
}
if
(
value
>
100000
)
{
value
=
100000
;
}
setTopUpCount
(
value
);
await
getAmount
(
value
);
}}
/
>
<
Space
>
<
Button
type
=
{
'primary'
}
theme
=
{
'solid'
}
onClick
=
{
async
()
=>
{
preTopUp
(
'zfb'
);
}}
>
支付宝
<
/Button
>
<
Button
style
=
{{
backgroundColor
:
'rgba(var(--semi-green-5), 1)'
,
}}
type
=
{
'primary'
}
theme
=
{
'solid'
}
onClick
=
{
async
()
=>
{
preTopUp
(
'wx'
);
}}
>
微信
<
/Button
>
<
/Space
>
<
/Form
>
<
/div
>
{
/*<div style={{ display: 'flex', justifyContent: 'right' }}>*/
}
{
/* <Text>*/
}
{
/* <Link onClick={*/
}
{
/* async () => {*/
}
{
/* window.location.href = '/topup/history'*/
}
{
/* }*/
}
{
/* }>充值记录</Link>*/
}
{
/* </Text>*/
}
{
/*</div>*/
}
<
/Card
>
<
/div
>
<
/Layout.Content
>
<
/Layout
>
<
/div
>
);
};
export
default
TopUp
;
\ No newline at end of file
export
default
TopUp
;
web/src/pages/User/AddUser.js
View file @
eb955ae4
...
...
@@ -7,7 +7,7 @@ const AddUser = (props) => {
const
originInputs
=
{
username
:
''
,
display_name
:
''
,
password
:
''
password
:
''
,
};
const
[
inputs
,
setInputs
]
=
useState
(
originInputs
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
...
...
@@ -48,8 +48,17 @@ const AddUser = (props) => {
footer
=
{
<
div
style
=
{{
display
:
'flex'
,
justifyContent
:
'flex-end'
}}
>
<
Space
>
<
Button
theme
=
"solid"
size
=
{
'large'
}
onClick
=
{
submit
}
>
提交
<
/Button
>
<
Button
theme
=
"solid"
size
=
{
'large'
}
type
=
{
'tertiary'
}
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
<
Button
theme
=
'solid'
size
=
{
'large'
}
onClick
=
{
submit
}
>
提交
<
/Button
>
<
Button
theme
=
'solid'
size
=
{
'large'
}
type
=
{
'tertiary'
}
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
<
/Space
>
<
/div
>
}
...
...
@@ -60,34 +69,34 @@ const AddUser = (props) => {
<
Spin
spinning
=
{
loading
}
>
<
Input
style
=
{{
marginTop
:
20
}}
label
=
"用户名"
name
=
"username"
label
=
'用户名'
name
=
'username'
addonBefore
=
{
'用户名'
}
placeholder
=
{
'请输入用户名'
}
onChange
=
{
value
=>
handleInputChange
(
'username'
,
value
)}
onChange
=
{
(
value
)
=>
handleInputChange
(
'username'
,
value
)}
value
=
{
username
}
autoComplete
=
"off"
autoComplete
=
'off'
/>
<
Input
style
=
{{
marginTop
:
20
}}
addonBefore
=
{
'显示名'
}
label
=
"显示名称"
name
=
"display_name"
autoComplete
=
"off"
label
=
'显示名称'
name
=
'display_name'
autoComplete
=
'off'
placeholder
=
{
'请输入显示名称'
}
onChange
=
{
value
=>
handleInputChange
(
'display_name'
,
value
)}
onChange
=
{
(
value
)
=>
handleInputChange
(
'display_name'
,
value
)}
value
=
{
display_name
}
/
>
<
Input
style
=
{{
marginTop
:
20
}}
label
=
"密 码"
name
=
"password"
label
=
'密 码'
name
=
'password'
type
=
{
'password'
}
addonBefore
=
{
'密码'
}
placeholder
=
{
'请输入密码'
}
onChange
=
{
value
=>
handleInputChange
(
'password'
,
value
)}
onChange
=
{
(
value
)
=>
handleInputChange
(
'password'
,
value
)}
value
=
{
password
}
autoComplete
=
"off"
autoComplete
=
'off'
/>
<
/Spin
>
<
/SideSheet
>
...
...
web/src/pages/User/EditUser.js
View file @
eb955ae4
...
...
@@ -3,7 +3,16 @@ import { useNavigate } from 'react-router-dom';
import
{
API
,
isMobile
,
showError
,
showSuccess
}
from
'../../helpers'
;
import
{
renderQuotaWithPrompt
}
from
'../../helpers/render'
;
import
Title
from
'@douyinfe/semi-ui/lib/es/typography/title'
;
import
{
Button
,
Divider
,
Input
,
Select
,
SideSheet
,
Space
,
Spin
,
Typography
}
from
'@douyinfe/semi-ui'
;
import
{
Button
,
Divider
,
Input
,
Select
,
SideSheet
,
Space
,
Spin
,
Typography
,
}
from
'@douyinfe/semi-ui'
;
const
EditUser
=
(
props
)
=>
{
const
userId
=
props
.
editingUser
.
id
;
...
...
@@ -16,21 +25,32 @@ const EditUser = (props) => {
wechat_id
:
''
,
email
:
''
,
quota
:
0
,
group
:
'default'
group
:
'default'
,
});
const
[
groupOptions
,
setGroupOptions
]
=
useState
([]);
const
{
username
,
display_name
,
password
,
github_id
,
wechat_id
,
telegram_id
,
email
,
quota
,
group
}
=
inputs
;
const
{
username
,
display_name
,
password
,
github_id
,
wechat_id
,
telegram_id
,
email
,
quota
,
group
,
}
=
inputs
;
const
handleInputChange
=
(
name
,
value
)
=>
{
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
};
const
fetchGroups
=
async
()
=>
{
try
{
let
res
=
await
API
.
get
(
`/api/group/`
);
setGroupOptions
(
res
.
data
.
data
.
map
((
group
)
=>
({
label
:
group
,
value
:
group
})));
setGroupOptions
(
res
.
data
.
data
.
map
((
group
)
=>
({
label
:
group
,
value
:
group
,
})),
);
}
catch
(
error
)
{
showError
(
error
.
message
);
}
...
...
@@ -98,8 +118,17 @@ const EditUser = (props) => {
footer
=
{
<
div
style
=
{{
display
:
'flex'
,
justifyContent
:
'flex-end'
}}
>
<
Space
>
<
Button
theme
=
"solid"
size
=
{
'large'
}
onClick
=
{
submit
}
>
提交
<
/Button
>
<
Button
theme
=
"solid"
size
=
{
'large'
}
type
=
{
'tertiary'
}
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
<
Button
theme
=
'solid'
size
=
{
'large'
}
onClick
=
{
submit
}
>
提交
<
/Button
>
<
Button
theme
=
'solid'
size
=
{
'large'
}
type
=
{
'tertiary'
}
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
<
/Space
>
<
/div
>
}
...
...
@@ -112,103 +141,103 @@ const EditUser = (props) => {
<
Typography
.
Text
>
用户名
<
/Typography.Text
>
<
/div
>
<
Input
label
=
"用户名"
name
=
"username"
label
=
'用户名'
name
=
'username'
placeholder
=
{
'请输入新的用户名'
}
onChange
=
{
value
=>
handleInputChange
(
'username'
,
value
)}
onChange
=
{
(
value
)
=>
handleInputChange
(
'username'
,
value
)}
value
=
{
username
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
/>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Typography
.
Text
>
密码
<
/Typography.Text
>
<
/div
>
<
Input
label
=
"密码"
name
=
"password"
label
=
'密码'
name
=
'password'
type
=
{
'password'
}
placeholder
=
{
'请输入新的密码,最短 8 位'
}
onChange
=
{
value
=>
handleInputChange
(
'password'
,
value
)}
onChange
=
{
(
value
)
=>
handleInputChange
(
'password'
,
value
)}
value
=
{
password
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
/>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Typography
.
Text
>
显示名称
<
/Typography.Text
>
<
/div
>
<
Input
label
=
"显示名称"
name
=
"display_name"
label
=
'显示名称'
name
=
'display_name'
placeholder
=
{
'请输入新的显示名称'
}
onChange
=
{
value
=>
handleInputChange
(
'display_name'
,
value
)}
onChange
=
{
(
value
)
=>
handleInputChange
(
'display_name'
,
value
)}
value
=
{
display_name
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
/>
{
userId
&&
<>
{
userId
&&
(
<>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Typography
.
Text
>
分组
<
/Typography.Text
>
<
/div
>
<
Select
placeholder
=
{
'请选择分组'
}
name
=
"group"
name
=
'group'
fluid
search
selection
allowAdditions
additionLabel
=
{
'请在系统设置页面编辑分组倍率以添加新的分组:'
}
onChange
=
{
value
=>
handleInputChange
(
'group'
,
value
)}
onChange
=
{
(
value
)
=>
handleInputChange
(
'group'
,
value
)}
value
=
{
inputs
.
group
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
optionList
=
{
groupOptions
}
/
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Typography
.
Text
>
{
`剩余额度
${
renderQuotaWithPrompt
(
quota
)}
`
}
<
/Typography.Text
>
<
/div
>
<
Input
name
=
"quota"
name
=
'quota'
placeholder
=
{
'请输入新的剩余额度'
}
onChange
=
{
value
=>
handleInputChange
(
'quota'
,
value
)}
onChange
=
{
(
value
)
=>
handleInputChange
(
'quota'
,
value
)}
value
=
{
quota
}
type
=
{
'number'
}
autoComplete
=
"new-password"
autoComplete
=
'new-password'
/>
<
/
>
}
)
}
<
Divider
style
=
{{
marginTop
:
20
}}
>
以下信息不可修改
<
/Divider
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Typography
.
Text
>
已绑定的
GitHub
账户
<
/Typography.Text
>
<
/div
>
<
Input
name
=
"github_id"
name
=
'github_id'
value
=
{
github_id
}
autoComplete
=
"new-password"
placeholder
=
"此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改"
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly
/>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Typography
.
Text
>
已绑定的微信账户
<
/Typography.Text
>
<
/div
>
<
Input
name
=
"wechat_id"
name
=
'wechat_id'
value
=
{
wechat_id
}
autoComplete
=
"new-password"
placeholder
=
"此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改"
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly
/>
<
Input
name
=
"telegram_id"
name
=
'telegram_id'
value
=
{
telegram_id
}
autoComplete
=
"new-password"
placeholder
=
"此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改"
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly
/>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Typography
.
Text
>
已绑定的邮箱账户
<
/Typography.Text
>
<
/div
>
<
Input
name
=
"email"
name
=
'email'
value
=
{
email
}
autoComplete
=
"new-password"
placeholder
=
"此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改"
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly
/>
<
/Spin
>
...
...
web/src/pages/User/index.js
View file @
eb955ae4
import
React
from
'react'
;
import
UsersTable
from
'../../components/UsersTable'
;
import
{
Layout
}
from
"@douyinfe/semi-ui"
;
import
{
Layout
}
from
'@douyinfe/semi-ui'
;
const
User
=
()
=>
(
<>
<
Layout
>
<
Layout
.
Header
>
<
h3
>
管理用户
<
/h3
>
<
/Layout.Header
>
<
Layout
.
Content
>
<
UsersTable
/>
<
/Layout.Content
>
<
Layout
.
Header
>
<
h3
>
管理用户
<
/h3
>
<
/Layout.Header
>
<
Layout
.
Content
>
<
UsersTable
/>
<
/Layout.Content
>
<
/Layout
>
<
/
>
);
...
...
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