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
bcb4862a
authored
May 16, 2023
by
JustSong
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor: bind quota to account instead of token (close #64, #31)
parent
349aca58
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
239 additions
and
175 deletions
+239
-175
common/constants.go
+2
-2
controller/token.go
+8
-57
controller/user.go
+31
-0
model/redemption.go
+4
-4
model/token.go
+55
-17
model/user.go
+11
-0
router/api-router.go
+1
-1
web/src/App.js
+11
-0
web/src/components/Header.js
+6
-0
web/src/components/TokensTable.js
+0
-73
web/src/pages/Token/EditToken.js
+16
-21
web/src/pages/TopUp/index.js
+94
-0
No files found.
common/constants.go
View file @
bcb4862a
...
...
@@ -50,10 +50,10 @@ var WeChatAccountQRCodeImageURL = ""
var
TurnstileSiteKey
=
""
var
TurnstileSecretKey
=
""
var
QuotaForNewUser
=
10
0
var
QuotaForNewUser
=
0
var
ChannelDisableThreshold
=
5.0
var
AutomaticDisableChannelEnabled
=
false
var
QuotaRemindThreshold
=
1000
// TODO: QuotaRemindThreshold
var
QuotaRemindThreshold
=
1000
var
RootUserEmail
=
""
...
...
controller/token.go
View file @
bcb4862a
...
...
@@ -100,7 +100,6 @@ func GetTokenStatus(c *gin.Context) {
}
func
AddToken
(
c
*
gin
.
Context
)
{
isAdmin
:=
c
.
GetInt
(
"role"
)
>=
common
.
RoleAdminUser
token
:=
model
.
Token
{}
err
:=
c
.
ShouldBindJSON
(
&
token
)
if
err
!=
nil
{
...
...
@@ -118,27 +117,14 @@ func AddToken(c *gin.Context) {
return
}
cleanToken
:=
model
.
Token
{
UserId
:
c
.
GetInt
(
"id"
),
Name
:
token
.
Name
,
Key
:
common
.
GetUUID
(),
CreatedTime
:
common
.
GetTimestamp
(),
AccessedTime
:
common
.
GetTimestamp
(),
ExpiredTime
:
token
.
ExpiredTime
,
}
if
isAdmin
{
cleanToken
.
RemainQuota
=
token
.
RemainQuota
cleanToken
.
UnlimitedQuota
=
token
.
UnlimitedQuota
}
else
{
userId
:=
c
.
GetInt
(
"id"
)
quota
,
err
:=
model
.
GetUserQuota
(
userId
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
err
.
Error
(),
})
return
}
cleanToken
.
RemainQuota
=
quota
UserId
:
c
.
GetInt
(
"id"
),
Name
:
token
.
Name
,
Key
:
common
.
GetUUID
(),
CreatedTime
:
common
.
GetTimestamp
(),
AccessedTime
:
common
.
GetTimestamp
(),
ExpiredTime
:
token
.
ExpiredTime
,
RemainQuota
:
token
.
RemainQuota
,
UnlimitedQuota
:
token
.
UnlimitedQuota
,
}
err
=
cleanToken
.
Insert
()
if
err
!=
nil
{
...
...
@@ -148,10 +134,6 @@ func AddToken(c *gin.Context) {
})
return
}
if
!
isAdmin
{
// update user quota
err
=
model
.
DecreaseUserQuota
(
c
.
GetInt
(
"id"
),
cleanToken
.
RemainQuota
)
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
...
...
@@ -240,34 +222,3 @@ func UpdateToken(c *gin.Context) {
})
return
}
type
topUpRequest
struct
{
Id
int
`json:"id"`
Key
string
`json:"key"`
}
func
TopUp
(
c
*
gin
.
Context
)
{
req
:=
topUpRequest
{}
err
:=
c
.
ShouldBindJSON
(
&
req
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
err
.
Error
(),
})
return
}
quota
,
err
:=
model
.
Redeem
(
req
.
Key
,
req
.
Id
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
err
.
Error
(),
})
return
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
"data"
:
quota
,
})
return
}
controller/user.go
View file @
bcb4862a
...
...
@@ -654,3 +654,34 @@ func EmailBind(c *gin.Context) {
})
return
}
type
topUpRequest
struct
{
Key
string
`json:"key"`
}
func
TopUp
(
c
*
gin
.
Context
)
{
req
:=
topUpRequest
{}
err
:=
c
.
ShouldBindJSON
(
&
req
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
err
.
Error
(),
})
return
}
id
:=
c
.
GetInt
(
"id"
)
quota
,
err
:=
model
.
Redeem
(
req
.
Key
,
id
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
err
.
Error
(),
})
return
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
"data"
:
quota
,
})
return
}
model/redemption.go
View file @
bcb4862a
...
...
@@ -40,12 +40,12 @@ func GetRedemptionById(id int) (*Redemption, error) {
return
&
redemption
,
err
}
func
Redeem
(
key
string
,
token
Id
int
)
(
quota
int
,
err
error
)
{
func
Redeem
(
key
string
,
user
Id
int
)
(
quota
int
,
err
error
)
{
if
key
==
""
{
return
0
,
errors
.
New
(
"未提供兑换码"
)
}
if
token
Id
==
0
{
return
0
,
errors
.
New
(
"
未提供 token
id"
)
if
user
Id
==
0
{
return
0
,
errors
.
New
(
"
无效的 user
id"
)
}
redemption
:=
&
Redemption
{}
err
=
DB
.
Where
(
"`key` = ?"
,
key
)
.
First
(
redemption
)
.
Error
...
...
@@ -55,7 +55,7 @@ func Redeem(key string, tokenId int) (quota int, err error) {
if
redemption
.
Status
!=
common
.
RedemptionCodeStatusEnabled
{
return
0
,
errors
.
New
(
"该兑换码已被使用"
)
}
err
=
Increase
TokenQuota
(
token
Id
,
redemption
.
Quota
)
err
=
Increase
UserQuota
(
user
Id
,
redemption
.
Quota
)
if
err
!=
nil
{
return
0
,
err
}
...
...
model/token.go
View file @
bcb4862a
...
...
@@ -2,6 +2,7 @@ package model
import
(
"errors"
"fmt"
_
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"one-api/common"
...
...
@@ -82,6 +83,16 @@ func GetTokenByIds(id int, userId int) (*Token, error) {
return
&
token
,
err
}
func
GetTokenById
(
id
int
)
(
*
Token
,
error
)
{
if
id
==
0
{
return
nil
,
errors
.
New
(
"id 为空!"
)
}
token
:=
Token
{
Id
:
id
}
var
err
error
=
nil
err
=
DB
.
First
(
&
token
,
"id = ?"
,
id
)
.
Error
return
&
token
,
err
}
func
(
token
*
Token
)
Insert
()
error
{
var
err
error
err
=
DB
.
Create
(
token
)
.
Error
...
...
@@ -116,26 +127,53 @@ func DeleteTokenById(id int, userId int) (err error) {
if
err
!=
nil
{
return
err
}
quota
:=
token
.
RemainQuota
if
quota
!=
0
{
if
quota
>
0
{
err
=
IncreaseUserQuota
(
userId
,
quota
)
}
else
{
err
=
DecreaseUserQuota
(
userId
,
-
quota
)
}
return
token
.
Delete
()
}
func
DecreaseTokenQuota
(
tokenId
int
,
quota
int
)
(
err
error
)
{
if
quota
<
0
{
return
errors
.
New
(
"quota 不能为负数!"
)
}
token
,
err
:=
GetTokenById
(
tokenId
)
if
err
!=
nil
{
return
err
}
return
token
.
Delete
()
}
func
IncreaseTokenQuota
(
id
int
,
quota
int
)
(
err
error
)
{
err
=
DB
.
Model
(
&
Token
{})
.
Where
(
"id = ?"
,
id
)
.
Update
(
"remain_quota"
,
gorm
.
Expr
(
"remain_quota + ?"
,
quota
))
.
Error
return
err
}
func
DecreaseTokenQuota
(
id
int
,
quota
int
)
(
err
error
)
{
err
=
DB
.
Model
(
&
Token
{})
.
Where
(
"id = ?"
,
id
)
.
Update
(
"remain_quota"
,
gorm
.
Expr
(
"remain_quota - ?"
,
quota
))
.
Error
if
token
.
RemainQuota
<
quota
{
return
errors
.
New
(
"令牌额度不足"
)
}
userQuota
,
err
:=
GetUserQuota
(
token
.
UserId
)
if
err
!=
nil
{
return
err
}
if
userQuota
<
quota
{
return
errors
.
New
(
"用户额度不足"
)
}
quotaTooLow
:=
userQuota
>=
common
.
QuotaRemindThreshold
&&
userQuota
-
quota
<
common
.
QuotaRemindThreshold
noMoreQuota
:=
userQuota
-
quota
<=
0
if
quotaTooLow
||
noMoreQuota
{
go
func
()
{
email
,
err
:=
GetUserEmail
(
token
.
UserId
)
if
err
!=
nil
{
common
.
SysError
(
"获取用户邮箱失败:"
+
err
.
Error
())
}
prompt
:=
"您的额度即将用尽"
if
noMoreQuota
{
prompt
=
"您的额度已用尽"
}
if
email
!=
""
{
topUpLink
:=
fmt
.
Sprintf
(
"%s/topup"
,
common
.
ServerAddress
)
err
=
common
.
SendEmail
(
prompt
,
email
,
fmt
.
Sprintf
(
"%s,剩余额度为 %d,为了不影响您的使用,请及时充值。<br/>充值链接:<a href='%s'>%s</a>"
,
prompt
,
userQuota
-
quota
,
topUpLink
,
topUpLink
))
if
err
!=
nil
{
common
.
SysError
(
"发送邮件失败:"
+
err
.
Error
())
}
}
}()
}
err
=
DB
.
Model
(
&
Token
{})
.
Where
(
"id = ?"
,
tokenId
)
.
Update
(
"remain_quota"
,
gorm
.
Expr
(
"remain_quota - ?"
,
quota
))
.
Error
if
err
!=
nil
{
return
err
}
err
=
DecreaseUserQuota
(
token
.
UserId
,
quota
)
return
err
}
model/user.go
View file @
bcb4862a
...
...
@@ -225,12 +225,23 @@ func GetUserQuota(id int) (quota int, err error) {
return
quota
,
err
}
func
GetUserEmail
(
id
int
)
(
email
string
,
err
error
)
{
err
=
DB
.
Model
(
&
User
{})
.
Where
(
"id = ?"
,
id
)
.
Select
(
"email"
)
.
Find
(
&
email
)
.
Error
return
email
,
err
}
func
IncreaseUserQuota
(
id
int
,
quota
int
)
(
err
error
)
{
if
quota
<
0
{
return
errors
.
New
(
"quota 不能为负数!"
)
}
err
=
DB
.
Model
(
&
User
{})
.
Where
(
"id = ?"
,
id
)
.
Update
(
"quota"
,
gorm
.
Expr
(
"quota + ?"
,
quota
))
.
Error
return
err
}
func
DecreaseUserQuota
(
id
int
,
quota
int
)
(
err
error
)
{
if
quota
<
0
{
return
errors
.
New
(
"quota 不能为负数!"
)
}
err
=
DB
.
Model
(
&
User
{})
.
Where
(
"id = ?"
,
id
)
.
Update
(
"quota"
,
gorm
.
Expr
(
"quota - ?"
,
quota
))
.
Error
return
err
}
...
...
router/api-router.go
View file @
bcb4862a
...
...
@@ -37,6 +37,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute
.
PUT
(
"/self"
,
controller
.
UpdateSelf
)
selfRoute
.
DELETE
(
"/self"
,
controller
.
DeleteSelf
)
selfRoute
.
GET
(
"/token"
,
controller
.
GenerateAccessToken
)
selfRoute
.
POST
(
"/topup"
,
controller
.
TopUp
)
}
adminRoute
:=
userRoute
.
Group
(
"/"
)
...
...
@@ -74,7 +75,6 @@ func SetApiRouter(router *gin.Engine) {
{
tokenRoute
.
GET
(
"/"
,
controller
.
GetAllTokens
)
tokenRoute
.
GET
(
"/search"
,
controller
.
SearchTokens
)
tokenRoute
.
POST
(
"/topup"
,
controller
.
TopUp
)
tokenRoute
.
GET
(
"/:id"
,
controller
.
GetToken
)
tokenRoute
.
POST
(
"/"
,
controller
.
AddToken
)
tokenRoute
.
PUT
(
"/"
,
controller
.
UpdateToken
)
...
...
web/src/App.js
View file @
bcb4862a
...
...
@@ -21,6 +21,7 @@ import EditToken from './pages/Token/EditToken';
import
EditChannel
from
'./pages/Channel/EditChannel'
;
import
Redemption
from
'./pages/Redemption'
;
import
EditRedemption
from
'./pages/Redemption/EditRedemption'
;
import
TopUp
from
'./pages/TopUp'
;
const
Home
=
lazy
(()
=>
import
(
'./pages/Home'
));
const
About
=
lazy
(()
=>
import
(
'./pages/About'
));
...
...
@@ -240,6 +241,16 @@ function App() {
}
/
>
<
Route
path
=
'/topup'
element
=
{
<
PrivateRoute
>
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
<
TopUp
/>
<
/Suspense
>
<
/PrivateRoute
>
}
/
>
<
Route
path
=
'/about'
element
=
{
<
Suspense
fallback
=
{
<
Loading
><
/Loading>}
>
...
...
web/src/components/Header.js
View file @
bcb4862a
...
...
@@ -31,6 +31,12 @@ const headerButtons = [
admin
:
true
,
},
{
name
:
'充值'
,
to
:
'/topup'
,
icon
:
'cart'
,
admin
:
true
,
},
{
name
:
'用户'
,
to
:
'/user'
,
icon
:
'user'
,
...
...
web/src/components/TokensTable.js
View file @
bcb4862a
...
...
@@ -36,8 +36,6 @@ const TokensTable = () => {
const
[
searching
,
setSearching
]
=
useState
(
false
);
const
[
showTopUpModal
,
setShowTopUpModal
]
=
useState
(
false
);
const
[
targetTokenIdx
,
setTargetTokenIdx
]
=
useState
(
0
);
const
[
redemptionCode
,
setRedemptionCode
]
=
useState
(
''
);
const
[
topUpLink
,
setTopUpLink
]
=
useState
(
''
);
const
loadTokens
=
async
(
startIdx
)
=>
{
const
res
=
await
API
.
get
(
`/api/token/?p=
${
startIdx
}
`
);
...
...
@@ -77,13 +75,6 @@ const TokensTable = () => {
.
catch
((
reason
)
=>
{
showError
(
reason
);
});
let
status
=
localStorage
.
getItem
(
'status'
);
if
(
status
)
{
status
=
JSON
.
parse
(
status
);
if
(
status
.
top_up_link
)
{
setTopUpLink
(
status
.
top_up_link
);
}
}
},
[]);
const
manageToken
=
async
(
id
,
action
,
idx
)
=>
{
...
...
@@ -156,28 +147,6 @@ const TokensTable = () => {
setLoading
(
false
);
};
const
topUp
=
async
()
=>
{
if
(
redemptionCode
===
''
)
{
return
;
}
const
res
=
await
API
.
post
(
'/api/token/topup/'
,
{
id
:
tokens
[
targetTokenIdx
].
id
,
key
:
redemptionCode
});
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
showSuccess
(
'充值成功!'
);
let
newTokens
=
[...
tokens
];
let
realIdx
=
(
activePage
-
1
)
*
ITEMS_PER_PAGE
+
targetTokenIdx
;
newTokens
[
realIdx
].
remain_quota
+=
data
;
setTokens
(
newTokens
);
setRedemptionCode
(
''
);
setShowTopUpModal
(
false
);
}
else
{
showError
(
message
);
}
}
return
(
<>
<
Form
onSubmit
=
{
searchTokens
}
>
...
...
@@ -279,15 +248,6 @@ const TokensTable = () => {
>
复制
<
/Button
>
<
Button
size
=
{
'small'
}
color
=
{
'yellow'
}
onClick
=
{()
=>
{
setTargetTokenIdx
(
idx
);
setShowTopUpModal
(
true
);
}}
>
充值
<
/Button
>
<
Popup
trigger
=
{
<
Button
size
=
'small'
negative
>
...
...
@@ -355,39 +315,6 @@ const TokensTable = () => {
<
/Table.Row
>
<
/Table.Footer
>
<
/Table
>
<
Modal
onClose
=
{()
=>
setShowTopUpModal
(
false
)}
onOpen
=
{()
=>
setShowTopUpModal
(
true
)}
open
=
{
showTopUpModal
}
size
=
{
'mini'
}
>
<
Modal
.
Header
>
通过兑换码为令牌「
{
tokens
[
targetTokenIdx
]?.
name
}
」充值
<
/Modal.Header
>
<
Modal
.
Content
>
<
Modal
.
Description
>
{
/*<Image src={status.wechat_qrcode} fluid />*/
}
{
topUpLink
&&
<
p
>
<
a
target
=
'_blank'
href
=
{
topUpLink
}
>
点击此处获取兑换码
<
/a
>
<
/p
>
}
<
Form
size
=
'large'
>
<
Form
.
Input
fluid
placeholder
=
'兑换码'
name
=
'redemptionCode'
value
=
{
redemptionCode
}
onChange
=
{(
e
)
=>
{
setRedemptionCode
(
e
.
target
.
value
);
}}
/
>
<
Button
color
=
''
fluid
size
=
'large'
onClick
=
{
topUp
}
>
充值
<
/Button
>
<
/Form
>
<
/Modal.Description
>
<
/Modal.Content
>
<
/Modal
>
<
/
>
);
};
...
...
web/src/pages/Token/EditToken.js
View file @
bcb4862a
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
Button
,
Form
,
Header
,
Segment
}
from
'semantic-ui-react'
;
import
{
useParams
}
from
'react-router-dom'
;
import
{
API
,
isAdmin
,
showError
,
showSuccess
,
timestamp2string
}
from
'../../helpers'
;
import
{
API
,
showError
,
showSuccess
,
timestamp2string
}
from
'../../helpers'
;
const
EditToken
=
()
=>
{
const
params
=
useParams
();
...
...
@@ -14,7 +14,6 @@ const EditToken = () => {
expired_time
:
-
1
,
unlimited_quota
:
false
};
const
isAdminUser
=
isAdmin
();
const
[
inputs
,
setInputs
]
=
useState
(
originInputs
);
const
{
name
,
remain_quota
,
expired_time
,
unlimited_quota
}
=
inputs
;
...
...
@@ -107,25 +106,21 @@ const EditToken = () => {
required
=
{
!
isEdit
}
/
>
<
/Form.Field
>
{
isAdminUser
&&
<>
<
Form
.
Field
>
<
Form
.
Input
label
=
'额度'
name
=
'remain_quota'
placeholder
=
{
'请输入额度'
}
onChange
=
{
handleInputChange
}
value
=
{
remain_quota
}
autoComplete
=
'new-password'
type
=
'number'
disabled
=
{
unlimited_quota
}
/
>
<
/Form.Field
>
<
Button
type
=
{
'button'
}
style
=
{{
marginBottom
:
'14px'
}}
onClick
=
{()
=>
{
setUnlimitedQuota
();
}}
>
{
unlimited_quota
?
'取消无限额度'
:
'设置为无限额度'
}
<
/Button
>
<
/
>
}
<
Form
.
Field
>
<
Form
.
Input
label
=
'额度'
name
=
'remain_quota'
placeholder
=
{
'请输入额度'
}
onChange
=
{
handleInputChange
}
value
=
{
remain_quota
}
autoComplete
=
'new-password'
type
=
'number'
disabled
=
{
unlimited_quota
}
/
>
<
/Form.Field
>
<
Button
type
=
{
'button'
}
style
=
{{
marginBottom
:
'14px'
}}
onClick
=
{()
=>
{
setUnlimitedQuota
();
}}
>
{
unlimited_quota
?
'取消无限额度'
:
'设置为无限额度'
}
<
/Button
>
<
Form
.
Field
>
<
Form
.
Input
label
=
'过期时间'
...
...
web/src/pages/TopUp/index.js
0 → 100644
View file @
bcb4862a
import
React
,
{
useEffect
,
useState
}
from
'react'
;
import
{
Button
,
Form
,
Grid
,
Header
,
Segment
,
Statistic
}
from
'semantic-ui-react'
;
import
{
API
,
showError
,
showSuccess
}
from
'../../helpers'
;
const
TopUp
=
()
=>
{
const
[
redemptionCode
,
setRedemptionCode
]
=
useState
(
''
);
const
[
topUpLink
,
setTopUpLink
]
=
useState
(
''
);
const
[
userQuota
,
setUserQuota
]
=
useState
(
0
);
const
topUp
=
async
()
=>
{
if
(
redemptionCode
===
''
)
{
return
;
}
const
res
=
await
API
.
post
(
'/api/user/topup'
,
{
key
:
redemptionCode
});
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
showSuccess
(
'充值成功!'
);
setUserQuota
((
quota
)
=>
{
return
quota
+
data
;
});
setRedemptionCode
(
''
);
}
else
{
showError
(
message
);
}
};
const
openTopUpLink
=
()
=>
{
if
(
!
topUpLink
)
{
showError
(
'超级管理员未设置充值链接!'
);
return
;
}
window
.
open
(
topUpLink
,
'_blank'
);
};
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
);
}
}
useEffect
(()
=>
{
let
status
=
localStorage
.
getItem
(
'status'
);
if
(
status
)
{
status
=
JSON
.
parse
(
status
);
if
(
status
.
top_up_link
)
{
setTopUpLink
(
status
.
top_up_link
);
}
}
getUserQuota
().
then
();
},
[]);
return
(
<
Segment
>
<
Header
as
=
'h3'
>
充值额度
<
/Header
>
<
Grid
columns
=
{
2
}
stackable
>
<
Grid
.
Column
>
<
Form
>
<
Form
.
Input
placeholder
=
'兑换码'
name
=
'redemptionCode'
value
=
{
redemptionCode
}
onChange
=
{(
e
)
=>
{
setRedemptionCode
(
e
.
target
.
value
);
}}
/
>
<
Button
color
=
'green'
onClick
=
{
openTopUpLink
}
>
获取兑换码
<
/Button
>
<
Button
color
=
'yellow'
onClick
=
{
topUp
}
>
充值
<
/Button
>
<
/Form
>
<
/Grid.Column
>
<
Grid
.
Column
>
<
Statistic
.
Group
widths
=
'one'
>
<
Statistic
>
<
Statistic
.
Value
>
{
userQuota
}
<
/Statistic.Value
>
<
Statistic
.
Label
>
剩余额度
<
/Statistic.Label
>
<
/Statistic
>
<
/Statistic.Group
>
<
/Grid.Column
>
<
/Grid
>
<
/Segment
>
);
};
export
default
TopUp
;
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