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
3eb7843b
authored
Nov 21, 2023
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
完善个人中心
parent
89c04a2c
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
490 additions
and
170 deletions
+490
-170
controller/user.go
+59
-0
model/ability.go
+10
-0
model/token.go
+2
-0
model/user.go
+53
-1
router/api-router.go
+2
-0
web/src/components/PersonalSetting.js
+307
-135
web/src/helpers/render.js
+6
-0
web/src/pages/Setting/index.js
+27
-29
web/src/pages/User/EditUser.js
+24
-5
No files found.
controller/user.go
View file @
3eb7843b
...
@@ -79,6 +79,7 @@ func setupLogin(user *model.User, c *gin.Context) {
...
@@ -79,6 +79,7 @@ func setupLogin(user *model.User, c *gin.Context) {
DisplayName
:
user
.
DisplayName
,
DisplayName
:
user
.
DisplayName
,
Role
:
user
.
Role
,
Role
:
user
.
Role
,
Status
:
user
.
Status
,
Status
:
user
.
Status
,
Group
:
user
.
Group
,
}
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
""
,
"message"
:
""
,
...
@@ -284,6 +285,42 @@ func GenerateAccessToken(c *gin.Context) {
...
@@ -284,6 +285,42 @@ func GenerateAccessToken(c *gin.Context) {
return
return
}
}
type
TransferAffQuotaRequest
struct
{
Quota
int
`json:"quota" binding:"required"`
}
func
TransferAffQuota
(
c
*
gin
.
Context
)
{
id
:=
c
.
GetInt
(
"id"
)
user
,
err
:=
model
.
GetUserById
(
id
,
true
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
err
.
Error
(),
})
return
}
tran
:=
TransferAffQuotaRequest
{}
if
err
:=
c
.
ShouldBindJSON
(
&
tran
);
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
err
.
Error
(),
})
return
}
err
=
user
.
TransferAffQuotaToQuota
(
tran
.
Quota
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"划转失败 "
+
err
.
Error
(),
})
return
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
"划转成功"
,
})
}
func
GetAffCode
(
c
*
gin
.
Context
)
{
func
GetAffCode
(
c
*
gin
.
Context
)
{
id
:=
c
.
GetInt
(
"id"
)
id
:=
c
.
GetInt
(
"id"
)
user
,
err
:=
model
.
GetUserById
(
id
,
true
)
user
,
err
:=
model
.
GetUserById
(
id
,
true
)
...
@@ -330,6 +367,28 @@ func GetSelf(c *gin.Context) {
...
@@ -330,6 +367,28 @@ func GetSelf(c *gin.Context) {
return
return
}
}
func
GetUserModels
(
c
*
gin
.
Context
)
{
id
,
err
:=
strconv
.
Atoi
(
c
.
Param
(
"id"
))
if
err
!=
nil
{
id
=
c
.
GetInt
(
"id"
)
}
user
,
err
:=
model
.
GetUserById
(
id
,
true
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
err
.
Error
(),
})
return
}
models
:=
model
.
GetGroupModels
(
user
.
Group
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
"data"
:
models
,
})
return
}
func
UpdateUser
(
c
*
gin
.
Context
)
{
func
UpdateUser
(
c
*
gin
.
Context
)
{
var
updatedUser
model
.
User
var
updatedUser
model
.
User
err
:=
json
.
NewDecoder
(
c
.
Request
.
Body
)
.
Decode
(
&
updatedUser
)
err
:=
json
.
NewDecoder
(
c
.
Request
.
Body
)
.
Decode
(
&
updatedUser
)
...
...
model/ability.go
View file @
3eb7843b
...
@@ -13,6 +13,16 @@ type Ability struct {
...
@@ -13,6 +13,16 @@ type Ability struct {
Priority
*
int64
`json:"priority" gorm:"bigint;default:0;index"`
Priority
*
int64
`json:"priority" gorm:"bigint;default:0;index"`
}
}
func
GetGroupModels
(
group
string
)
[]
string
{
var
abilities
[]
Ability
DB
.
Where
(
"`group` = ?"
,
group
)
.
Find
(
&
abilities
)
models
:=
make
([]
string
,
0
,
len
(
abilities
))
for
_
,
ability
:=
range
abilities
{
models
=
append
(
models
,
ability
.
Model
)
}
return
models
}
func
GetRandomSatisfiedChannel
(
group
string
,
model
string
)
(
*
Channel
,
error
)
{
func
GetRandomSatisfiedChannel
(
group
string
,
model
string
)
(
*
Channel
,
error
)
{
ability
:=
Ability
{}
ability
:=
Ability
{}
groupCol
:=
"`group`"
groupCol
:=
"`group`"
...
...
model/token.go
View file @
3eb7843b
...
@@ -220,6 +220,7 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
...
@@ -220,6 +220,7 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
}
}
if
sendEmail
{
if
sendEmail
{
if
(
quota
+
preConsumedQuota
)
!=
0
{
quotaTooLow
:=
userQuota
>=
common
.
QuotaRemindThreshold
&&
userQuota
-
(
quota
+
preConsumedQuota
)
<
common
.
QuotaRemindThreshold
quotaTooLow
:=
userQuota
>=
common
.
QuotaRemindThreshold
&&
userQuota
-
(
quota
+
preConsumedQuota
)
<
common
.
QuotaRemindThreshold
noMoreQuota
:=
userQuota
-
(
quota
+
preConsumedQuota
)
<=
0
noMoreQuota
:=
userQuota
-
(
quota
+
preConsumedQuota
)
<=
0
if
quotaTooLow
||
noMoreQuota
{
if
quotaTooLow
||
noMoreQuota
{
...
@@ -244,6 +245,7 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
...
@@ -244,6 +245,7 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
}()
}()
}
}
}
}
}
if
!
token
.
UnlimitedQuota
{
if
!
token
.
UnlimitedQuota
{
if
quota
>
0
{
if
quota
>
0
{
...
...
model/user.go
View file @
3eb7843b
...
@@ -27,6 +27,9 @@ type User struct {
...
@@ -27,6 +27,9 @@ type User struct {
RequestCount
int
`json:"request_count" gorm:"type:int;default:0;"`
// request number
RequestCount
int
`json:"request_count" gorm:"type:int;default:0;"`
// request number
Group
string
`json:"group" gorm:"type:varchar(32);default:'default'"`
Group
string
`json:"group" gorm:"type:varchar(32);default:'default'"`
AffCode
string
`json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
AffCode
string
`json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
AffCount
int
`json:"aff_count" gorm:"type:int;default:0;column:aff_count"`
AffQuota
int
`json:"aff_quota" gorm:"type:int;default:0;column:aff_quota"`
// 邀请剩余额度
AffHistoryQuota
int
`json:"aff_history_quota" gorm:"type:int;default:0;column:aff_history"`
// 邀请历史额度
InviterId
int
`json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
InviterId
int
`json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
}
}
...
@@ -77,6 +80,54 @@ func DeleteUserById(id int) (err error) {
...
@@ -77,6 +80,54 @@ func DeleteUserById(id int) (err error) {
return
user
.
Delete
()
return
user
.
Delete
()
}
}
func
inviteUser
(
inviterId
int
)
(
err
error
)
{
user
,
err
:=
GetUserById
(
inviterId
,
true
)
if
err
!=
nil
{
return
err
}
user
.
AffCount
++
user
.
AffQuota
+=
common
.
QuotaForInviter
user
.
AffHistoryQuota
+=
common
.
QuotaForInviter
return
DB
.
Save
(
user
)
.
Error
}
func
(
user
*
User
)
TransferAffQuotaToQuota
(
quota
int
)
error
{
// 检查quota是否小于最小额度
if
float64
(
quota
)
<
common
.
QuotaPerUnit
{
return
fmt
.
Errorf
(
"转移额度最小为%s!"
,
common
.
LogQuota
(
int
(
common
.
QuotaPerUnit
)))
}
// 开始数据库事务
tx
:=
DB
.
Begin
()
if
tx
.
Error
!=
nil
{
return
tx
.
Error
}
defer
tx
.
Rollback
()
// 确保在函数退出时事务能回滚
// 加锁查询用户以确保数据一致性
err
:=
tx
.
Set
(
"gorm:query_option"
,
"FOR UPDATE"
)
.
First
(
&
user
,
user
.
Id
)
.
Error
if
err
!=
nil
{
return
err
}
// 再次检查用户的AffQuota是否足够
if
user
.
AffQuota
<
quota
{
return
errors
.
New
(
"邀请额度不足!"
)
}
// 更新用户额度
user
.
AffQuota
-=
quota
user
.
Quota
+=
quota
// 保存用户状态
if
err
:=
tx
.
Save
(
user
)
.
Error
;
err
!=
nil
{
return
err
}
// 提交事务
return
tx
.
Commit
()
.
Error
}
func
(
user
*
User
)
Insert
(
inviterId
int
)
error
{
func
(
user
*
User
)
Insert
(
inviterId
int
)
error
{
var
err
error
var
err
error
if
user
.
Password
!=
""
{
if
user
.
Password
!=
""
{
...
@@ -101,8 +152,9 @@ func (user *User) Insert(inviterId int) error {
...
@@ -101,8 +152,9 @@ func (user *User) Insert(inviterId int) error {
RecordLog
(
user
.
Id
,
LogTypeSystem
,
fmt
.
Sprintf
(
"使用邀请码赠送 %s"
,
common
.
LogQuota
(
common
.
QuotaForInvitee
)))
RecordLog
(
user
.
Id
,
LogTypeSystem
,
fmt
.
Sprintf
(
"使用邀请码赠送 %s"
,
common
.
LogQuota
(
common
.
QuotaForInvitee
)))
}
}
if
common
.
QuotaForInviter
>
0
{
if
common
.
QuotaForInviter
>
0
{
_
=
IncreaseUserQuota
(
inviterId
,
common
.
QuotaForInviter
)
//
_ = IncreaseUserQuota(inviterId, common.QuotaForInviter)
RecordLog
(
inviterId
,
LogTypeSystem
,
fmt
.
Sprintf
(
"邀请用户赠送 %s"
,
common
.
LogQuota
(
common
.
QuotaForInviter
)))
RecordLog
(
inviterId
,
LogTypeSystem
,
fmt
.
Sprintf
(
"邀请用户赠送 %s"
,
common
.
LogQuota
(
common
.
QuotaForInviter
)))
_
=
inviteUser
(
inviterId
)
}
}
}
}
return
nil
return
nil
...
...
router/api-router.go
View file @
3eb7843b
...
@@ -39,6 +39,7 @@ func SetApiRouter(router *gin.Engine) {
...
@@ -39,6 +39,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute
.
Use
(
middleware
.
UserAuth
())
selfRoute
.
Use
(
middleware
.
UserAuth
())
{
{
selfRoute
.
GET
(
"/self"
,
controller
.
GetSelf
)
selfRoute
.
GET
(
"/self"
,
controller
.
GetSelf
)
selfRoute
.
GET
(
"/models"
,
controller
.
GetUserModels
)
selfRoute
.
PUT
(
"/self"
,
controller
.
UpdateSelf
)
selfRoute
.
PUT
(
"/self"
,
controller
.
UpdateSelf
)
selfRoute
.
DELETE
(
"/self"
,
controller
.
DeleteSelf
)
selfRoute
.
DELETE
(
"/self"
,
controller
.
DeleteSelf
)
selfRoute
.
GET
(
"/token"
,
controller
.
GenerateAccessToken
)
selfRoute
.
GET
(
"/token"
,
controller
.
GenerateAccessToken
)
...
@@ -46,6 +47,7 @@ func SetApiRouter(router *gin.Engine) {
...
@@ -46,6 +47,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute
.
POST
(
"/topup"
,
controller
.
TopUp
)
selfRoute
.
POST
(
"/topup"
,
controller
.
TopUp
)
selfRoute
.
POST
(
"/pay"
,
controller
.
RequestEpay
)
selfRoute
.
POST
(
"/pay"
,
controller
.
RequestEpay
)
selfRoute
.
POST
(
"/amount"
,
controller
.
RequestAmount
)
selfRoute
.
POST
(
"/amount"
,
controller
.
RequestAmount
)
selfRoute
.
POST
(
"/aff_transfer"
,
controller
.
TransferAffQuota
)
}
}
adminRoute
:=
userRoute
.
Group
(
"/"
)
adminRoute
:=
userRoute
.
Group
(
"/"
)
...
...
web/src/components/PersonalSetting.js
View file @
3eb7843b
import
React
,
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
React
,
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
Button
,
Divider
,
Form
,
Header
,
Image
,
Message
,
Modal
}
from
'semantic-ui-react'
;
import
{
Form
,
Image
,
Message
}
from
'semantic-ui-react'
;
import
{
Link
,
useNavigate
}
from
'react-router-dom'
;
import
{
Link
,
useNavigate
}
from
'react-router-dom'
;
import
{
API
,
copy
,
showError
,
showInfo
,
showNotice
,
showSuccess
}
from
'../helpers'
;
import
{
API
,
copy
,
isRoot
,
showError
,
showInfo
,
showNotice
,
showSuccess
}
from
'../helpers'
;
import
Turnstile
from
'react-turnstile'
;
import
Turnstile
from
'react-turnstile'
;
import
{
UserContext
}
from
'../context/User'
;
import
{
UserContext
}
from
'../context/User'
;
import
{
onGitHubOAuthClicked
}
from
'./utils'
;
import
{
onGitHubOAuthClicked
}
from
'./utils'
;
import
{
Avatar
,
Banner
,
Button
,
Card
,
Descriptions
,
Divider
,
Input
,
InputNumber
,
Layout
,
Modal
,
Space
,
Tag
,
Typography
}
from
"@douyinfe/semi-ui"
;
import
{
getQuotaPerUnit
,
renderQuota
,
renderQuotaWithPrompt
,
stringToColor
}
from
"../helpers/render"
;
import
EditToken
from
"../pages/Token/EditToken"
;
import
EditUser
from
"../pages/User/EditUser"
;
const
PersonalSetting
=
()
=>
{
const
PersonalSetting
=
()
=>
{
const
[
userState
,
userDispatch
]
=
useContext
(
UserContext
);
const
[
userState
,
userDispatch
]
=
useContext
(
UserContext
);
...
@@ -28,8 +44,17 @@ const PersonalSetting = () => {
...
@@ -28,8 +44,17 @@ const PersonalSetting = () => {
const
[
countdown
,
setCountdown
]
=
useState
(
30
);
const
[
countdown
,
setCountdown
]
=
useState
(
30
);
const
[
affLink
,
setAffLink
]
=
useState
(
""
);
const
[
affLink
,
setAffLink
]
=
useState
(
""
);
const
[
systemToken
,
setSystemToken
]
=
useState
(
""
);
const
[
systemToken
,
setSystemToken
]
=
useState
(
""
);
const
[
models
,
setModels
]
=
useState
([]);
const
[
openTransfer
,
setOpenTransfer
]
=
useState
(
false
);
const
[
transferAmount
,
setTransferAmount
]
=
useState
(
0
);
useEffect
(()
=>
{
useEffect
(()
=>
{
// let user = localStorage.getItem('user');
// if (user) {
// userDispatch({ type: 'login', payload: user });
// }
// console.log(localStorage.getItem('user'))
let
status
=
localStorage
.
getItem
(
'status'
);
let
status
=
localStorage
.
getItem
(
'status'
);
if
(
status
)
{
if
(
status
)
{
status
=
JSON
.
parse
(
status
);
status
=
JSON
.
parse
(
status
);
...
@@ -39,6 +64,14 @@ const PersonalSetting = () => {
...
@@ -39,6 +64,14 @@ const PersonalSetting = () => {
setTurnstileSiteKey
(
status
.
turnstile_site_key
);
setTurnstileSiteKey
(
status
.
turnstile_site_key
);
}
}
}
}
getUserData
().
then
(
(
res
)
=>
{
console
.
log
(
userState
)
}
);
loadModels
().
then
();
getAffLink
().
then
();
setTransferAmount
(
getQuotaPerUnit
())
},
[]);
},
[]);
useEffect
(()
=>
{
useEffect
(()
=>
{
...
@@ -54,16 +87,15 @@ const PersonalSetting = () => {
...
@@ -54,16 +87,15 @@ const PersonalSetting = () => {
return
()
=>
clearInterval
(
countdownInterval
);
// Clean up on unmount
return
()
=>
clearInterval
(
countdownInterval
);
// Clean up on unmount
},
[
disableButton
,
countdown
]);
},
[
disableButton
,
countdown
]);
const
handleInputChange
=
(
e
,
{
name
,
value
}
)
=>
{
const
handleInputChange
=
(
name
,
value
)
=>
{
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
setInputs
((
inputs
)
=>
({...
inputs
,
[
name
]:
value
}));
};
};
const
generateAccessToken
=
async
()
=>
{
const
generateAccessToken
=
async
()
=>
{
const
res
=
await
API
.
get
(
'/api/user/token'
);
const
res
=
await
API
.
get
(
'/api/user/token'
);
const
{
success
,
message
,
data
}
=
res
.
data
;
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
if
(
success
)
{
setSystemToken
(
data
);
setSystemToken
(
data
);
setAffLink
(
""
);
await
copy
(
data
);
await
copy
(
data
);
showSuccess
(
`令牌已重置并已复制到剪贴板`
);
showSuccess
(
`令牌已重置并已复制到剪贴板`
);
}
else
{
}
else
{
...
@@ -73,18 +105,36 @@ const PersonalSetting = () => {
...
@@ -73,18 +105,36 @@ const PersonalSetting = () => {
const
getAffLink
=
async
()
=>
{
const
getAffLink
=
async
()
=>
{
const
res
=
await
API
.
get
(
'/api/user/aff'
);
const
res
=
await
API
.
get
(
'/api/user/aff'
);
const
{
success
,
message
,
data
}
=
res
.
data
;
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
if
(
success
)
{
let
link
=
`
${
window
.
location
.
origin
}
/register?aff=
${
data
}
`
;
let
link
=
`
${
window
.
location
.
origin
}
/register?aff=
${
data
}
`
;
setAffLink
(
link
);
setAffLink
(
link
);
setSystemToken
(
""
);
await
copy
(
link
);
showSuccess
(
`邀请链接已复制到剪切板`
);
}
else
{
}
else
{
showError
(
message
);
showError
(
message
);
}
}
};
};
const
getUserData
=
async
()
=>
{
let
res
=
await
API
.
get
(
`/api/user/self`
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
userDispatch
({
type
:
'login'
,
payload
:
data
});
}
else
{
showError
(
message
);
}
}
const
loadModels
=
async
()
=>
{
let
res
=
await
API
.
get
(
`/api/user/models`
);
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
setModels
(
data
);
console
.
log
(
data
)
}
else
{
showError
(
message
);
}
}
const
handleAffLinkClick
=
async
(
e
)
=>
{
const
handleAffLinkClick
=
async
(
e
)
=>
{
e
.
target
.
select
();
e
.
target
.
select
();
await
copy
(
e
.
target
.
value
);
await
copy
(
e
.
target
.
value
);
...
@@ -104,12 +154,12 @@ const PersonalSetting = () => {
...
@@ -104,12 +154,12 @@ const PersonalSetting = () => {
}
}
const
res
=
await
API
.
delete
(
'/api/user/self'
);
const
res
=
await
API
.
delete
(
'/api/user/self'
);
const
{
success
,
message
}
=
res
.
data
;
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
if
(
success
)
{
showSuccess
(
'账户已删除!'
);
showSuccess
(
'账户已删除!'
);
await
API
.
get
(
'/api/user/logout'
);
await
API
.
get
(
'/api/user/logout'
);
userDispatch
({
type
:
'logout'
});
userDispatch
({
type
:
'logout'
});
localStorage
.
removeItem
(
'user'
);
localStorage
.
removeItem
(
'user'
);
navigate
(
'/login'
);
navigate
(
'/login'
);
}
else
{
}
else
{
...
@@ -122,7 +172,7 @@ const PersonalSetting = () => {
...
@@ -122,7 +172,7 @@ const PersonalSetting = () => {
const
res
=
await
API
.
get
(
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
;
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
if
(
success
)
{
showSuccess
(
'微信账户绑定成功!'
);
showSuccess
(
'微信账户绑定成功!'
);
setShowWeChatBindModal
(
false
);
setShowWeChatBindModal
(
false
);
...
@@ -131,9 +181,33 @@ const PersonalSetting = () => {
...
@@ -131,9 +181,33 @@ const PersonalSetting = () => {
}
}
};
};
const
transfer
=
async
()
=>
{
if
(
transferAmount
<
getQuotaPerUnit
())
{
showError
(
'划转金额最低为'
+
renderQuota
(
getQuotaPerUnit
()));
return
;
}
const
res
=
await
API
.
post
(
`/api/user/aff_transfer`
,
{
quota
:
transferAmount
}
);
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
showSuccess
(
message
);
setOpenTransfer
(
false
);
getUserData
().
then
();
}
else
{
showError
(
message
);
}
};
const
sendVerificationCode
=
async
()
=>
{
const
sendVerificationCode
=
async
()
=>
{
if
(
inputs
.
email
===
''
)
{
showError
(
'请输入邮箱!'
);
return
;
}
setDisableButton
(
true
);
setDisableButton
(
true
);
if
(
inputs
.
email
===
''
)
return
;
if
(
turnstileEnabled
&&
turnstileToken
===
''
)
{
if
(
turnstileEnabled
&&
turnstileToken
===
''
)
{
showInfo
(
'请稍后几秒重试,Turnstile 正在检查用户环境!'
);
showInfo
(
'请稍后几秒重试,Turnstile 正在检查用户环境!'
);
return
;
return
;
...
@@ -142,7 +216,7 @@ const PersonalSetting = () => {
...
@@ -142,7 +216,7 @@ const PersonalSetting = () => {
const
res
=
await
API
.
get
(
const
res
=
await
API
.
get
(
`/api/verification?email=
${
inputs
.
email
}
&turnstile=
${
turnstileToken
}
`
`/api/verification?email=
${
inputs
.
email
}
&turnstile=
${
turnstileToken
}
`
);
);
const
{
success
,
message
}
=
res
.
data
;
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
if
(
success
)
{
showSuccess
(
'验证码发送成功,请检查邮箱!'
);
showSuccess
(
'验证码发送成功,请检查邮箱!'
);
}
else
{
}
else
{
...
@@ -152,35 +226,193 @@ const PersonalSetting = () => {
...
@@ -152,35 +226,193 @@ const PersonalSetting = () => {
};
};
const
bindEmail
=
async
()
=>
{
const
bindEmail
=
async
()
=>
{
if
(
inputs
.
email_verification_code
===
''
)
return
;
if
(
inputs
.
email_verification_code
===
''
)
{
showError
(
'请输入邮箱验证码!'
);
return
;
}
setLoading
(
true
);
setLoading
(
true
);
const
res
=
await
API
.
get
(
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
;
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
if
(
success
)
{
showSuccess
(
'邮箱账户绑定成功!'
);
showSuccess
(
'邮箱账户绑定成功!'
);
setShowEmailBindModal
(
false
);
setShowEmailBindModal
(
false
);
userState
.
user
.
email
=
inputs
.
email
;
}
else
{
}
else
{
showError
(
message
);
showError
(
message
);
}
}
setLoading
(
false
);
setLoading
(
false
);
};
};
const
getUsername
=
()
=>
{
if
(
userState
.
user
)
{
return
userState
.
user
.
username
;
}
else
{
return
'null'
;
}
}
const
handleCancel
=
()
=>
{
setOpenTransfer
(
false
);
}
return
(
return
(
<
div
style
=
{{
lineHeight
:
'40px'
}}
>
<
div
style
=
{{
lineHeight
:
'40px'
}}
>
<
Header
as
=
'h3'
>
通用设置
<
/Header
>
<
Layout
>
<
Message
>
<
Layout
.
Content
>
注意,此处生成的令牌用于系统管理,而非用于请求
OpenAI
相关的服务,请知悉。
<
Modal
<
/Message
>
title
=
"请输入要划转的数量"
<
Button
as
=
{
Link
}
to
=
{
`/user/edit/`
}
>
visible
=
{
openTransfer
}
更新个人信息
onOk
=
{
transfer
}
onCancel
=
{
handleCancel
}
maskClosable
=
{
false
}
size
=
{
'small'
}
centered
=
{
true
}
>
<
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>
</div>
<div style={{marginTop: 20}}>
<Typography.Text>{`
划转额度
$
{
renderQuotaWithPrompt
(
transferAmount
)}
最低
` + renderQuota(getQuotaPerUnit())}</Typography.Text>
<div>
<InputNumber min={0} style={{marginTop: 5}} value={transferAmount} onChange={(value)=>setTransferAmount(value)} disabled={false}></InputNumber>
</div>
</div>
</Modal>
<div style={{marginTop: 20}}>
<Card
title={
<Card.Meta
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>}
></Card.Meta>
}
headerExtraContent={
<>
<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>
}
>
<Typography.Title heading={6}>可用模型</Typography.Title>
<div style={{marginTop: 10}}>
<Space wrap>
{models.map((model) => (
<Tag key={model} color="cyan">
{model}
</Tag>
))}
</Space>
</div>
</Card>
<Card
footer={
<div>
<Typography.Text>邀请链接</Typography.Text>
<Input
style={{marginTop: 10}}
value={affLink}
onClick={handleAffLinkClick}
readOnly
/>
</div>
}
>
<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>
<Descriptions.Item itemKey="总收益">{renderQuota(userState?.user?.aff_history_quota)}</Descriptions.Item>
<Descriptions.Item itemKey="邀请人数">{userState?.user?.aff_count}</Descriptions.Item>
</Descriptions>
</div>
</Card>
<Card>
<Typography.Title heading={6}>个人信息</Typography.Title>
<div style={{marginTop: 20}}>
<Typography.Text strong>邮箱</Typography.Text>
<div style={{display: 'flex', justifyContent: 'space-between'}}>
<div>
<Input
value={userState.user && userState.user.email !== ''?userState.user.email:'未绑定'}
readonly={true}
></Input>
</div>
<div>
<Button onClick={()=>{setShowEmailBindModal(true)}} disabled={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>
<Input
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>
</Button>
</div>
</div>
</div>
<div style={{marginTop: 10}}>
<Typography.Text strong>GitHub</Typography.Text>
<div style={{display: 'flex', justifyContent: 'space-between'}}>
<div>
<Input
value={userState.user && userState.user.github_id !== ''?userState.user.github_id:'未绑定'}
readonly={true}
></Input>
</div>
<div>
<Button
onClick={() => {onGitHubOAuthClicked(status.github_client_id)}}
disabled={(userState.user && userState.user.github_id !== '') || !status.github_oauth}
>
{
status.github_oauth?'绑定':'未启用'
}
</Button>
</div>
</div>
</div>
<div style={{marginTop: 10}}>
<Space>
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
<
Button
onClick
=
{
getAffLink
}
>
复制邀请链接
<
/Button
>
<Button onClick={() => {
<Button onClick={() => {
setShowAccountDeleteModal(true);
setShowAccountDeleteModal(true);
}}>删除个人账户</Button>
}}>删除个人账户</Button>
</Space>
{systemToken && (
{systemToken && (
<Form.Input
<Form.Input
...
@@ -188,20 +420,9 @@ const PersonalSetting = () => {
...
@@ -188,20 +420,9 @@ const PersonalSetting = () => {
readOnly
readOnly
value={systemToken}
value={systemToken}
onClick={handleSystemTokenClick}
onClick={handleSystemTokenClick}
style
=
{{
marginTop
:
'10px'
}}
style={{marginTop: '10px'
}}
/>
/>
)}
)}
{
affLink
&&
(
<
Form
.
Input
fluid
readOnly
value
=
{
affLink
}
onClick
=
{
handleAffLinkClick
}
style
=
{{
marginTop
:
'10px'
}}
/
>
)}
<
Divider
/>
<
Header
as
=
'h3'
>
账号绑定
<
/Header
>
{
{
status.wechat_login && (
status.wechat_login && (
<Button
<Button
...
@@ -214,15 +435,13 @@ const PersonalSetting = () => {
...
@@ -214,15 +435,13 @@ const PersonalSetting = () => {
)
)
}
}
<Modal
<Modal
onClose
=
{()
=>
setShowWeChatBindModal
(
false
)}
onCancel
={() => setShowWeChatBindModal(false)}
onOpen
=
{()
=>
setShowWeChatBindModal
(
true
)}
//
onOpen={() => setShowWeChatBindModal(true)}
open
=
{
showWeChatBindModal
}
visible
={showWeChatBindModal}
size={'mini'}
size={'mini'}
>
>
<
Modal
.
Content
>
<Image src={status.wechat_qrcode} fluid/>
<
Modal
.
Description
>
<div style={{textAlign: 'center'}}>
<
Image
src
=
{
status
.
wechat_qrcode
}
fluid
/>
<
div
style
=
{{
textAlign
:
'center'
}}
>
<p>
<p>
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
</p>
</p>
...
@@ -239,51 +458,41 @@ const PersonalSetting = () => {
...
@@ -239,51 +458,41 @@ const PersonalSetting = () => {
绑定
绑定
</Button>
</Button>
</Form>
</Form>
<
/Modal.Description
>
<
/Modal.Content
>
</Modal>
</Modal>
{
</div>
status
.
github_oauth
&&
(
</Card>
<
Button
onClick
=
{()
=>
{
onGitHubOAuthClicked
(
status
.
github_client_id
)}}
>
绑定
GitHub
账号
<
/Button
>
)
}
<
Button
onClick
=
{()
=>
{
setShowEmailBindModal
(
true
);
}}
>
绑定邮箱地址
<
/Button
>
<Modal
<Modal
onClose
=
{()
=>
setShowEmailBindModal
(
false
)}
onCancel={() => setShowEmailBindModal(false)}
onOpen
=
{()
=>
setShowEmailBindModal
(
true
)}
// onOpen={() => setShowEmailBindModal(true)}
open
=
{
showEmailBindModal
}
onOk={bindEmail}
size
=
{
'tiny'
}
visible={showEmailBindModal}
style
=
{{
maxWidth
:
'450px'
}}
size={'small'}
centered={true}
maskClosable={false}
>
>
<
Modal
.
Header
>
绑定邮箱地址
<
/Modal.Header
>
<Typography.Title heading={6}>绑定邮箱地址</Typography.Title>
<
Modal
.
Content
>
<div style={{marginTop: 20, display: 'flex', justifyContent: 'space-between'}}>
<
Modal
.
Description
>
<Input
<
Form
size
=
'large'
>
<
Form
.
Input
fluid
fluid
placeholder='输入邮箱地址'
placeholder='输入邮箱地址'
onChange
=
{
handleInputChange
}
onChange={(value)=>handleInputChange('email', value)
}
name='email'
name='email'
type='email'
type='email'
action
=
{
/>
<
Button
onClick
=
{
sendVerificationCode
}
disabled
=
{
disableButton
||
loading
}
>
<Button onClick={sendVerificationCode}
disabled={disableButton || loading}>
{disableButton ? `
重新发送
(
$
{
countdown
})
` : '获取验证码'}
{disableButton ? `
重新发送
(
$
{
countdown
})
` : '获取验证码'}
</Button>
</Button>
}
</div>
/
>
<div style={{marginTop: 10}}
>
<
Form
.
Input
<
Input
fluid
fluid
placeholder='验证码'
placeholder='验证码'
name='email_verification_code'
name='email_verification_code'
value={inputs.email_verification_code}
value={inputs.email_verification_code}
onChange
=
{
handleInputChange
}
onChange={(value)=>handleInputChange('email_verification_code', value)
}
/>
/>
</div>
{turnstileEnabled ? (
{turnstileEnabled ? (
<Turnstile
<Turnstile
sitekey={turnstileSiteKey}
sitekey={turnstileSiteKey}
...
@@ -294,47 +503,27 @@ const PersonalSetting = () => {
...
@@ -294,47 +503,27 @@ const PersonalSetting = () => {
) : (
) : (
<></>
<></>
)}
)}
<
div
style
=
{{
display
:
'flex'
,
justifyContent
:
'space-between'
,
marginTop
:
'1rem'
}}
>
<
Button
color
=
''
fluid
size
=
'large'
onClick
=
{
bindEmail
}
loading
=
{
loading
}
>
确认绑定
<
/Button
>
<
div
style
=
{{
width
:
'1rem'
}}
><
/div>
<
Button
fluid
size
=
'large'
onClick
=
{()
=>
setShowEmailBindModal
(
false
)}
>
取消
<
/Button
>
<
/div
>
<
/Form
>
<
/Modal.Description
>
<
/Modal.Content
>
</Modal>
</Modal>
<Modal
<Modal
onClose
=
{()
=>
setShowAccountDeleteModal
(
false
)}
onCancel
={() => setShowAccountDeleteModal(false)}
onOpen
=
{()
=>
setShowAccountDeleteModal
(
true
)
}
visible={showAccountDeleteModal
}
open
=
{
showAccountDeleteModal
}
size={'small'
}
size
=
{
'tiny'
}
centered={true
}
style
=
{{
maxWidth
:
'450px'
}
}
onOk={deleteAccount
}
>
>
<
Modal
.
Header
>
危险操作
<
/Modal.Header
>
<div style={{marginTop: 20}}>
<
Modal
.
Content
>
<Banner
<
Message
>
您正在删除自己的帐户,将清空所有数据且不可恢复
<
/Message
>
type="danger"
<
Modal
.
Description
>
description="您正在删除自己的帐户,将清空所有数据且不可恢复"
<
Form
size
=
'large'
>
closeIcon={null}
<
Form
.
Input
/>
fluid
</div>
<div style={{marginTop: 20}}>
<Input
placeholder={`
输入你的账户名
$
{
userState
?.
user
?.
username
}
以确认删除
`}
placeholder={`
输入你的账户名
$
{
userState
?.
user
?.
username
}
以确认删除
`}
name='self_account_deletion_confirmation'
name='self_account_deletion_confirmation'
value={inputs.self_account_deletion_confirmation}
value={inputs.self_account_deletion_confirmation}
onChange={handleInputChange
}
onChange={(value)=>handleInputChange('self_account_deletion_confirmation', value)
}
/>
/>
{turnstileEnabled ? (
{turnstileEnabled ? (
<Turnstile
<Turnstile
...
@@ -346,30 +535,13 @@ const PersonalSetting = () => {
...
@@ -346,30 +535,13 @@ const PersonalSetting = () => {
) : (
) : (
<></>
<></>
)}
)}
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
<Button
color='red'
fluid
size='large'
onClick={deleteAccount}
loading={loading}
>
确认删除
</Button>
<div style={{ width: '1rem' }}></div>
<Button
fluid
size='large'
onClick={() => setShowAccountDeleteModal(false)}
>
取消
</Button>
</div>
</div>
</Form>
</Modal.Description>
</Modal.Content>
</Modal>
</Modal>
</div>
</div>
</Layout.Content>
</Layout>
</div>
);
);
};
};
...
...
web/src/helpers/render.js
View file @
3eb7843b
...
@@ -37,6 +37,12 @@ export function renderNumber(num) {
...
@@ -37,6 +37,12 @@ export function renderNumber(num) {
}
}
}
}
export
function
getQuotaPerUnit
()
{
let
quotaPerUnit
=
localStorage
.
getItem
(
'quota_per_unit'
);
quotaPerUnit
=
parseFloat
(
quotaPerUnit
);
return
quotaPerUnit
;
}
export
function
renderQuota
(
quota
,
digits
=
2
)
{
export
function
renderQuota
(
quota
,
digits
=
2
)
{
let
quotaPerUnit
=
localStorage
.
getItem
(
'quota_per_unit'
);
let
quotaPerUnit
=
localStorage
.
getItem
(
'quota_per_unit'
);
let
displayInCurrency
=
localStorage
.
getItem
(
'display_in_currency'
);
let
displayInCurrency
=
localStorage
.
getItem
(
'display_in_currency'
);
...
...
web/src/pages/Setting/index.js
View file @
3eb7843b
import
React
from
'react'
;
import
React
from
'react'
;
import
{
Segment
,
Tab
}
from
'semantic-ui-react'
;
import
SystemSetting
from
'../../components/SystemSetting'
;
import
SystemSetting
from
'../../components/SystemSetting'
;
import
{
isRoot
}
from
'../../helpers'
;
import
{
isRoot
}
from
'../../helpers'
;
import
OtherSetting
from
'../../components/OtherSetting'
;
import
OtherSetting
from
'../../components/OtherSetting'
;
import
PersonalSetting
from
'../../components/PersonalSetting'
;
import
PersonalSetting
from
'../../components/PersonalSetting'
;
import
OperationSetting
from
'../../components/OperationSetting'
;
import
OperationSetting
from
'../../components/OperationSetting'
;
import
{
Layout
,
TabPane
,
Tabs
}
from
"@douyinfe/semi-ui"
;
const
Setting
=
()
=>
{
const
Setting
=
()
=>
{
let
panes
=
[
let
panes
=
[
{
{
menuItem
:
'个人设置'
,
tab
:
'个人设置'
,
render
:
()
=>
(
content
:
<
PersonalSetting
/>
,
<
Tab
.
Pane
attached
=
{
false
}
>
itemKey
:
'1'
<
PersonalSetting
/>
<
/Tab.Pane
>
)
}
}
];
];
if
(
isRoot
())
{
if
(
isRoot
())
{
panes
.
push
({
panes
.
push
({
menuItem
:
'运营设置'
,
tab
:
'运营设置'
,
render
:
()
=>
(
content
:
<
OperationSetting
/>
,
<
Tab
.
Pane
attached
=
{
false
}
>
itemKey
:
'2'
<
OperationSetting
/>
<
/Tab.Pane
>
)
});
});
panes
.
push
({
panes
.
push
({
menuItem
:
'系统设置'
,
tab
:
'系统设置'
,
render
:
()
=>
(
content
:
<
SystemSetting
/>
,
<
Tab
.
Pane
attached
=
{
false
}
>
itemKey
:
'3'
<
SystemSetting
/>
<
/Tab.Pane
>
)
});
});
panes
.
push
({
panes
.
push
({
menuItem
:
'其他设置'
,
tab
:
'其他设置'
,
render
:
()
=>
(
content
:
<
OtherSetting
/>
,
<
Tab
.
Pane
attached
=
{
false
}
>
itemKey
:
'4'
<
OtherSetting
/>
<
/Tab.Pane
>
)
});
});
}
}
return
(
return
(
<
Segment
>
<
div
>
<
Tab
menu
=
{{
secondary
:
true
,
pointing
:
true
}}
panes
=
{
panes
}
/
>
<
Layout
>
<
/Segment
>
<
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
>
);
);
};
};
...
...
web/src/pages/User/EditUser.js
View file @
3eb7843b
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
Button
,
Form
,
Header
,
Segment
}
from
'semantic-ui-react'
;
import
{
Button
,
Form
,
Header
,
Segment
}
from
'semantic-ui-react'
;
import
{
useParams
,
useNavigate
}
from
'react-router-dom'
;
import
{
useParams
,
useNavigate
}
from
'react-router-dom'
;
import
{
API
,
showError
,
showSuccess
}
from
'../../helpers'
;
import
{
API
,
isMobile
,
showError
,
showSuccess
}
from
'../../helpers'
;
import
{
renderQuota
,
renderQuotaWithPrompt
}
from
'../../helpers/render'
;
import
{
renderQuota
,
renderQuotaWithPrompt
}
from
'../../helpers/render'
;
import
Title
from
"@douyinfe/semi-ui/lib/es/typography/title"
;
import
{
SideSheet
,
Space
}
from
"@douyinfe/semi-ui"
;
const
EditUser
=
()
=>
{
const
EditUser
=
(
props
)
=>
{
const
params
=
useParams
();
const
params
=
useParams
();
const
userId
=
params
.
id
;
const
userId
=
params
.
id
;
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
...
@@ -84,8 +86,24 @@ const EditUser = () => {
...
@@ -84,8 +86,24 @@ const EditUser = () => {
return
(
return
(
<>
<>
<
Segment
loading
=
{
loading
}
>
<
SideSheet
<
Header
as
=
'h3'
>
更新用户信息
<
/Header
>
placement
=
{
'left'
}
title
=
{
<
Title
level
=
{
3
}
>
更新用户信息
<
/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
>
<
/Space
>
<
/div
>
}
closeIcon
=
{
null
}
onCancel
=
{()
=>
handleCancel
()}
width
=
{
isMobile
()
?
'100%'
:
600
}
>
<
Form
autoComplete
=
'new-password'
>
<
Form
autoComplete
=
'new-password'
>
<
Form
.
Field
>
<
Form
.
Field
>
<
Form
.
Input
<
Form
.
Input
...
@@ -182,7 +200,8 @@ const EditUser = () => {
...
@@ -182,7 +200,8 @@ const EditUser = () => {
<
Button
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
<
Button
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
<
Button
positive
onClick
=
{
submit
}
>
提交
<
/Button
>
<
Button
positive
onClick
=
{
submit
}
>
提交
<
/Button
>
<
/Form
>
<
/Form
>
<
/Segment
>
<
/SideSheet
>
<
/
>
<
/
>
);
);
};
};
...
...
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