Commit 3eb7843b by CaIon

完善个人中心

parent 89c04a2c
...@@ -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)
......
...@@ -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`"
......
...@@ -220,28 +220,30 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo ...@@ -220,28 +220,30 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
} }
if sendEmail { if sendEmail {
quotaTooLow := userQuota >= common.QuotaRemindThreshold && userQuota-(quota+preConsumedQuota) < common.QuotaRemindThreshold if (quota + preConsumedQuota) != 0 {
noMoreQuota := userQuota-(quota+preConsumedQuota) <= 0 quotaTooLow := userQuota >= common.QuotaRemindThreshold && userQuota-(quota+preConsumedQuota) < common.QuotaRemindThreshold
if quotaTooLow || noMoreQuota { noMoreQuota := userQuota-(quota+preConsumedQuota) <= 0
go func() { if quotaTooLow || noMoreQuota {
email, err := GetUserEmail(token.UserId) go func() {
if err != nil { email, err := GetUserEmail(token.UserId)
common.SysError("failed to fetch user email: " + 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, topUpLink, topUpLink))
if err != nil { if err != nil {
common.SysError("failed to send email" + err.Error()) common.SysError("failed to fetch user email: " + err.Error())
} }
common.SysLog("user quota is low, consumed quota: " + strconv.Itoa(quota) + ", user quota: " + strconv.Itoa(userQuota)) 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, topUpLink, topUpLink))
if err != nil {
common.SysError("failed to send email" + err.Error())
}
common.SysLog("user quota is low, consumed quota: " + strconv.Itoa(quota) + ", user quota: " + strconv.Itoa(userQuota))
}
}()
}
} }
} }
......
...@@ -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
......
...@@ -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("/")
......
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);
let navigate = useNavigate(); let navigate = useNavigate();
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
wechat_verification_code: '', wechat_verification_code: '',
email_verification_code: '', email_verification_code: '',
email: '', email: '',
self_account_deletion_confirmation: '' self_account_deletion_confirmation: ''
}); });
const [status, setStatus] = useState({}); const [status, setStatus] = useState({});
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false); const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
const [showEmailBindModal, setShowEmailBindModal] = useState(false); const [showEmailBindModal, setShowEmailBindModal] = useState(false);
const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false); const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
const [turnstileEnabled, setTurnstileEnabled] = useState(false); const [turnstileEnabled, setTurnstileEnabled] = useState(false);
const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
const [turnstileToken, setTurnstileToken] = useState(''); const [turnstileToken, setTurnstileToken] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [disableButton, setDisableButton] = useState(false); const [disableButton, setDisableButton] = useState(false);
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 status = localStorage.getItem('status'); // let user = localStorage.getItem('user');
if (status) { // if (user) {
status = JSON.parse(status); // userDispatch({ type: 'login', payload: user });
setStatus(status); // }
if (status.turnstile_check) { // console.log(localStorage.getItem('user'))
setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key);
}
}
}, []);
useEffect(() => { let status = localStorage.getItem('status');
let countdownInterval = null; if (status) {
if (disableButton && countdown > 0) { status = JSON.parse(status);
countdownInterval = setInterval(() => { setStatus(status);
setCountdown(countdown - 1); if (status.turnstile_check) {
}, 1000); setTurnstileEnabled(true);
} else if (countdown === 0) { setTurnstileSiteKey(status.turnstile_site_key);
setDisableButton(false); }
setCountdown(30); }
} getUserData().then(
return () => clearInterval(countdownInterval); // Clean up on unmount (res) => {
}, [disableButton, countdown]); console.log(userState)
}
);
loadModels().then();
getAffLink().then();
setTransferAmount(getQuotaPerUnit())
}, []);
useEffect(() => {
let countdownInterval = null;
if (disableButton && countdown > 0) {
countdownInterval = setInterval(() => {
setCountdown(countdown - 1);
}, 1000);
} else if (countdown === 0) {
setDisableButton(false);
setCountdown(30);
}
return () => clearInterval(countdownInterval); // Clean up on unmount
}, [disableButton, countdown]);
const handleInputChange = (name, value) => {
setInputs((inputs) => ({...inputs, [name]: value}));
};
const generateAccessToken = async () => {
const res = await API.get('/api/user/token');
const {success, message, data} = res.data;
if (success) {
setSystemToken(data);
await copy(data);
showSuccess(`令牌已重置并已复制到剪贴板`);
} else {
showError(message);
}
};
const handleInputChange = (e, { name, value }) => { const getAffLink = async () => {
setInputs((inputs) => ({ ...inputs, [name]: value })); const res = await API.get('/api/user/aff');
}; const {success, message, data} = res.data;
if (success) {
let link = `${window.location.origin}/register?aff=${data}`;
setAffLink(link);
} else {
showError(message);
}
};
const generateAccessToken = async () => { const getUserData = async () => {
const res = await API.get('/api/user/token'); let res = await API.get(`/api/user/self`);
const { success, message, data } = res.data; const {success, message, data} = res.data;
if (success) { if (success) {
setSystemToken(data); userDispatch({type: 'login', payload: data});
setAffLink(""); } else {
await copy(data); showError(message);
showSuccess(`令牌已重置并已复制到剪贴板`); }
} else {
showError(message);
} }
};
const getAffLink = async () => { const loadModels = async () => {
const res = await API.get('/api/user/aff'); let res = await API.get(`/api/user/models`);
const { success, message, data } = res.data; const {success, message, data} = res.data;
if (success) { if (success) {
let link = `${window.location.origin}/register?aff=${data}`; setModels(data);
setAffLink(link); console.log(data)
setSystemToken(""); } else {
await copy(link); showError(message);
showSuccess(`邀请链接已复制到剪切板`); }
} 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);
showSuccess(`邀请链接已复制到剪切板`); showSuccess(`邀请链接已复制到剪切板`);
}; };
const handleSystemTokenClick = async (e) => { const handleSystemTokenClick = async (e) => {
e.target.select(); e.target.select();
await copy(e.target.value); await copy(e.target.value);
showSuccess(`系统令牌已复制到剪切板`); showSuccess(`系统令牌已复制到剪切板`);
}; };
const deleteAccount = async () => { const deleteAccount = async () => {
if (inputs.self_account_deletion_confirmation !== userState.user.username) { if (inputs.self_account_deletion_confirmation !== userState.user.username) {
showError('请输入你的账户名以确认删除!'); showError('请输入你的账户名以确认删除!');
return; return;
} }
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 {
showError(message); showError(message);
} }
}; };
const bindWeChat = async () => { const bindWeChat = async () => {
if (inputs.wechat_verification_code === '') return; if (inputs.wechat_verification_code === '') return;
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);
} else { } else {
showError(message); showError(message);
} }
}; };
const sendVerificationCode = async () => { const transfer = async () => {
setDisableButton(true); if (transferAmount < getQuotaPerUnit()) {
if (inputs.email === '') return; showError('划转金额最低为' + renderQuota(getQuotaPerUnit()));
if (turnstileEnabled && turnstileToken === '') { return;
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); }
return; const res = await API.post(
} `/api/user/aff_transfer`,
setLoading(true); {
const res = await API.get( quota: transferAmount
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}` }
); );
const { success, message } = res.data; const {success, message} = res.data;
if (success) { if (success) {
showSuccess('验证码发送成功,请检查邮箱!'); showSuccess(message);
} else { setOpenTransfer(false);
showError(message); getUserData().then();
} else {
showError(message);
}
};
const sendVerificationCode = async () => {
if (inputs.email === '') {
showError('请输入邮箱!');
return;
}
setDisableButton(true);
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
return;
}
setLoading(true);
const res = await API.get(
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
);
const {success, message} = res.data;
if (success) {
showSuccess('验证码发送成功,请检查邮箱!');
} else {
showError(message);
}
setLoading(false);
};
const bindEmail = async () => {
if (inputs.email_verification_code === '') {
showError('请输入邮箱验证码!');
return;
}
setLoading(true);
const res = await API.get(
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
);
const {success, message} = res.data;
if (success) {
showSuccess('邮箱账户绑定成功!');
setShowEmailBindModal(false);
userState.user.email = inputs.email;
} else {
showError(message);
}
setLoading(false);
};
const getUsername = () => {
if (userState.user) {
return userState.user.username;
} else {
return 'null';
}
} }
setLoading(false);
};
const bindEmail = async () => { const handleCancel = () => {
if (inputs.email_verification_code === '') return; setOpenTransfer(false);
setLoading(true);
const res = await API.get(
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
);
const { success, message } = res.data;
if (success) {
showSuccess('邮箱账户绑定成功!');
setShowEmailBindModal(false);
} else {
showError(message);
} }
setLoading(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}
</Button> onCancel={handleCancel}
<Button onClick={generateAccessToken}>生成系统访问令牌</Button> maskClosable={false}
<Button onClick={getAffLink}>复制邀请链接</Button> size={'small'}
<Button onClick={() => { centered={true}
setShowAccountDeleteModal(true); >
}}>删除个人账户</Button> <div style={{marginTop: 20}}>
<Typography.Text>{`可用额度${renderQuotaWithPrompt(userState?.user?.aff_quota)}`}</Typography.Text>
{systemToken && ( <Input style={{marginTop: 5}} value={userState?.user?.aff_quota} disabled={true}></Input>
<Form.Input </div>
fluid <div style={{marginTop: 20}}>
readOnly <Typography.Text>{`划转额度${renderQuotaWithPrompt(transferAmount)} 最低` + renderQuota(getQuotaPerUnit())}</Typography.Text>
value={systemToken} <div>
onClick={handleSystemTokenClick} <InputNumber min={0} style={{marginTop: 5}} value={transferAmount} onChange={(value)=>setTransferAmount(value)} disabled={false}></InputNumber>
style={{ marginTop: '10px' }} </div>
/> </div>
)} </Modal>
{affLink && ( <div style={{marginTop: 20}}>
<Form.Input <Card
fluid title={
readOnly <Card.Meta
value={affLink} avatar={<Avatar size="default" color={stringToColor(getUsername())}
onClick={handleAffLinkClick} style={{marginRight: 4}}>
style={{ marginTop: '10px' }} {typeof getUsername() === 'string' && getUsername().slice(0, 1)}
/> </Avatar>}
)} title={<Typography.Text>{getUsername()}</Typography.Text>}
<Divider /> description={isRoot()?<Tag color="red">管理员</Tag>:<Tag color="blue">普通用户</Tag>}
<Header as='h3'>账号绑定</Header> ></Card.Meta>
{ }
status.wechat_login && ( headerExtraContent={
<Button <>
onClick={() => { <Space vertical align="start">
setShowWeChatBindModal(true); <Tag color="green">{'ID: ' + userState?.user?.id}</Tag>
}} <Tag color="blue">{userState?.user?.group}</Tag>
> </Space>
绑定微信账号 </>
</Button> }
) footer={
} <Descriptions row>
<Modal <Descriptions.Item itemKey="当前余额">{renderQuota(userState?.user?.quota)}</Descriptions.Item>
onClose={() => setShowWeChatBindModal(false)} <Descriptions.Item itemKey="历史消耗">{renderQuota(userState?.user?.used_quota)}</Descriptions.Item>
onOpen={() => setShowWeChatBindModal(true)} <Descriptions.Item itemKey="请求次数">{userState.user?.request_count}</Descriptions.Item>
open={showWeChatBindModal} </Descriptions>
size={'mini'} }
> >
<Modal.Content> <Typography.Title heading={6}>可用模型</Typography.Title>
<Modal.Description> <div style={{marginTop: 10}}>
<Image src={status.wechat_qrcode} fluid /> <Space wrap>
<div style={{ textAlign: 'center' }}> {models.map((model) => (
<p> <Tag key={model} color="cyan">
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) {model}
</p> </Tag>
</div> ))}
<Form size='large'> </Space>
<Form.Input </div>
fluid
placeholder='验证码' </Card>
name='wechat_verification_code' <Card
value={inputs.wechat_verification_code} footer={
onChange={handleInputChange} <div>
/> <Typography.Text>邀请链接</Typography.Text>
<Button color='' fluid size='large' onClick={bindWeChat}> <Input
绑定 style={{marginTop: 10}}
</Button> value={affLink}
</Form> onClick={handleAffLinkClick}
</Modal.Description> readOnly
</Modal.Content> />
</Modal> </div>
{ }
status.github_oauth && ( >
<Button onClick={()=>{onGitHubOAuthClicked(status.github_client_id)}}>绑定 GitHub 账号</Button> <Typography.Title heading={6}>邀请信息</Typography.Title>
) <div style={{marginTop: 10}}>
} <Descriptions row>
<Button <Descriptions.Item itemKey="待使用收益">
onClick={() => { <span style={{color: 'rgba(var(--semi-red-5), 1)'}}>
setShowEmailBindModal(true); {
}} renderQuota(userState?.user?.aff_quota)
> }
绑定邮箱地址 </span>
</Button> <Button type={'secondary'} onClick={()=>setOpenTransfer(true)} size={'small'} style={{marginLeft: 10}}>划转</Button>
<Modal </Descriptions.Item>
onClose={() => setShowEmailBindModal(false)} <Descriptions.Item itemKey="总收益">{renderQuota(userState?.user?.aff_history_quota)}</Descriptions.Item>
onOpen={() => setShowEmailBindModal(true)} <Descriptions.Item itemKey="邀请人数">{userState?.user?.aff_count}</Descriptions.Item>
open={showEmailBindModal} </Descriptions>
size={'tiny'} </div>
style={{ maxWidth: '450px' }} </Card>
> <Card>
<Modal.Header>绑定邮箱地址</Modal.Header> <Typography.Title heading={6}>个人信息</Typography.Title>
<Modal.Content> <div style={{marginTop: 20}}>
<Modal.Description> <Typography.Text strong>邮箱</Typography.Text>
<Form size='large'> <div style={{display: 'flex', justifyContent: 'space-between'}}>
<Form.Input <div>
fluid <Input
placeholder='输入邮箱地址' value={userState.user && userState.user.email !== ''?userState.user.email:'未绑定'}
onChange={handleInputChange} readonly={true}
name='email' ></Input>
type='email' </div>
action={ <div>
<Button onClick={sendVerificationCode} disabled={disableButton || loading}> <Button onClick={()=>{setShowEmailBindModal(true)}} disabled={userState.user && userState.user.email !== ''}>绑定邮箱</Button>
{disableButton ? `重新发送(${countdown})` : '获取验证码'} </div>
</Button> </div>
} </div>
/> <div style={{marginTop: 10}}>
<Form.Input <Typography.Text strong>微信</Typography.Text>
fluid <div style={{display: 'flex', justifyContent: 'space-between'}}>
placeholder='验证码' <div>
name='email_verification_code' <Input
value={inputs.email_verification_code} value={userState.user && userState.user.wechat_id !== ''?'已绑定':'未绑定'}
onChange={handleInputChange} readonly={true}
/> ></Input>
{turnstileEnabled ? ( </div>
<Turnstile <div>
sitekey={turnstileSiteKey} <Button disabled={(userState.user && userState.user.wechat_id !== '') || !status.wechat_login}>
onVerify={(token) => { {
setTurnstileToken(token); status.wechat_login?'绑定':'未启用'
}} }
/> </Button>
) : ( </div>
<></> </div>
)} </div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}> <div style={{marginTop: 10}}>
<Button <Typography.Text strong>GitHub</Typography.Text>
color='' <div style={{display: 'flex', justifyContent: 'space-between'}}>
fluid <div>
size='large' <Input
onClick={bindEmail} value={userState.user && userState.user.github_id !== ''?userState.user.github_id:'未绑定'}
loading={loading} readonly={true}
> ></Input>
确认绑定 </div>
</Button> <div>
<div style={{ width: '1rem' }}></div> <Button
<Button onClick={() => {onGitHubOAuthClicked(status.github_client_id)}}
fluid disabled={(userState.user && userState.user.github_id !== '') || !status.github_oauth}
size='large' >
onClick={() => setShowEmailBindModal(false)} {
> status.github_oauth?'绑定':'未启用'
取消 }
</Button> </Button>
</div> </div>
</Form> </div>
</Modal.Description> </div>
</Modal.Content>
</Modal> <div style={{marginTop: 10}}>
<Modal <Space>
onClose={() => setShowAccountDeleteModal(false)} <Button onClick={generateAccessToken}>生成系统访问令牌</Button>
onOpen={() => setShowAccountDeleteModal(true)} <Button onClick={() => {
open={showAccountDeleteModal} setShowAccountDeleteModal(true);
size={'tiny'} }}>删除个人账户</Button>
style={{ maxWidth: '450px' }} </Space>
>
<Modal.Header>危险操作</Modal.Header> {systemToken && (
<Modal.Content> <Form.Input
<Message>您正在删除自己的帐户,将清空所有数据且不可恢复</Message> fluid
<Modal.Description> readOnly
<Form size='large'> value={systemToken}
<Form.Input onClick={handleSystemTokenClick}
fluid style={{marginTop: '10px'}}
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`} />
name='self_account_deletion_confirmation' )}
value={inputs.self_account_deletion_confirmation} {
onChange={handleInputChange} status.wechat_login && (
/> <Button
{turnstileEnabled ? ( onClick={() => {
<Turnstile setShowWeChatBindModal(true);
sitekey={turnstileSiteKey} }}
onVerify={(token) => { >
setTurnstileToken(token); 绑定微信账号
}} </Button>
/> )
) : ( }
<></> <Modal
)} onCancel={() => setShowWeChatBindModal(false)}
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}> // onOpen={() => setShowWeChatBindModal(true)}
<Button visible={showWeChatBindModal}
color='red' size={'mini'}
fluid >
size='large' <Image src={status.wechat_qrcode} fluid/>
onClick={deleteAccount} <div style={{textAlign: 'center'}}>
loading={loading} <p>
> 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
确认删除 </p>
</Button> </div>
<div style={{ width: '1rem' }}></div> <Form size='large'>
<Button <Form.Input
fluid fluid
size='large' placeholder='验证码'
onClick={() => setShowAccountDeleteModal(false)} name='wechat_verification_code'
> value={inputs.wechat_verification_code}
取消 onChange={handleInputChange}
</Button> />
</div> <Button color='' fluid size='large' onClick={bindWeChat}>
</Form> 绑定
</Modal.Description> </Button>
</Modal.Content> </Form>
</Modal> </Modal>
</div> </div>
); </Card>
<Modal
onCancel={() => setShowEmailBindModal(false)}
// onOpen={() => setShowEmailBindModal(true)}
onOk={bindEmail}
visible={showEmailBindModal}
size={'small'}
centered={true}
maskClosable={false}
>
<Typography.Title heading={6}>绑定邮箱地址</Typography.Title>
<div style={{marginTop: 20, display: 'flex', justifyContent: 'space-between'}}>
<Input
fluid
placeholder='输入邮箱地址'
onChange={(value)=>handleInputChange('email', value)}
name='email'
type='email'
/>
<Button onClick={sendVerificationCode}
disabled={disableButton || loading}>
{disableButton ? `重新发送(${countdown})` : '获取验证码'}
</Button>
</div>
<div style={{marginTop: 10}}>
<Input
fluid
placeholder='验证码'
name='email_verification_code'
value={inputs.email_verification_code}
onChange={(value)=>handleInputChange('email_verification_code', value)}
/>
</div>
{turnstileEnabled ? (
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
) : (
<></>
)}
</Modal>
<Modal
onCancel={() => setShowAccountDeleteModal(false)}
visible={showAccountDeleteModal}
size={'small'}
centered={true}
onOk={deleteAccount}
>
<div style={{marginTop: 20}}>
<Banner
type="danger"
description="您正在删除自己的帐户,将清空所有数据且不可恢复"
closeIcon={null}
/>
</div>
<div style={{marginTop: 20}}>
<Input
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
name='self_account_deletion_confirmation'
value={inputs.self_account_deletion_confirmation}
onChange={(value)=>handleInputChange('self_account_deletion_confirmation', value)}
/>
{turnstileEnabled ? (
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
) : (
<></>
)}
</div>
</Modal>
</div>
</Layout.Content>
</Layout>
</div>
);
}; };
export default PersonalSetting; export default PersonalSetting;
...@@ -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');
......
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({
) tab: '系统设置',
}); content: <SystemSetting/>,
panes.push({ itemKey: '3'
menuItem: '系统设置', });
render: () => ( panes.push({
<Tab.Pane attached={false}> tab: '其他设置',
<SystemSetting /> content: <OtherSetting/>,
</Tab.Pane> itemKey: '4'
) });
}); }
panes.push({
menuItem: '其他设置',
render: () => (
<Tab.Pane attached={false}>
<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>
);
}; };
export default Setting; export default Setting;
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,105 +86,122 @@ const EditUser = () => { ...@@ -84,105 +86,122 @@ 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
label='用户名' label='用户名'
name='username' name='username'
placeholder={'请输入新的用户名'} placeholder={'请输入新的用户名'}
onChange={handleInputChange} onChange={handleInputChange}
value={username} value={username}
autoComplete='new-password' autoComplete='new-password'
/> />
</Form.Field> </Form.Field>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='密码' label='密码'
name='password' name='password'
type={'password'} type={'password'}
placeholder={'请输入新的密码,最短 8 位'} placeholder={'请输入新的密码,最短 8 位'}
onChange={handleInputChange} onChange={handleInputChange}
value={password} value={password}
autoComplete='new-password' autoComplete='new-password'
/> />
</Form.Field> </Form.Field>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='显示名称' label='显示名称'
name='display_name' name='display_name'
placeholder={'请输入新的显示名称'} placeholder={'请输入新的显示名称'}
onChange={handleInputChange} onChange={handleInputChange}
value={display_name} value={display_name}
autoComplete='new-password' autoComplete='new-password'
/> />
</Form.Field> </Form.Field>
{ {
userId && <> userId && <>
<Form.Field> <Form.Field>
<Form.Dropdown <Form.Dropdown
label='分组' label='分组'
placeholder={'请选择分组'} placeholder={'请选择分组'}
name='group' name='group'
fluid fluid
search search
selection selection
allowAdditions allowAdditions
additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'} additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
onChange={handleInputChange} onChange={handleInputChange}
value={inputs.group} value={inputs.group}
autoComplete='new-password' autoComplete='new-password'
options={groupOptions} options={groupOptions}
/> />
</Form.Field> </Form.Field>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label={`剩余额度${renderQuotaWithPrompt(quota)}`} label={`剩余额度${renderQuotaWithPrompt(quota)}`}
name='quota' name='quota'
placeholder={'请输入新的剩余额度'} placeholder={'请输入新的剩余额度'}
onChange={handleInputChange} onChange={handleInputChange}
value={quota} value={quota}
type={'number'} type={'number'}
autoComplete='new-password' autoComplete='new-password'
/> />
</Form.Field> </Form.Field>
</> </>
} }
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='已绑定的 GitHub 账户' label='已绑定的 GitHub 账户'
name='github_id' name='github_id'
value={github_id} value={github_id}
autoComplete='new-password' autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改' placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readOnly readOnly
/> />
</Form.Field> </Form.Field>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='已绑定的微信账户' label='已绑定的微信账户'
name='wechat_id' name='wechat_id'
value={wechat_id} value={wechat_id}
autoComplete='new-password' autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改' placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readOnly readOnly
/> />
</Form.Field> </Form.Field>
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='已绑定的邮箱账户' label='已绑定的邮箱账户'
name='email' name='email'
value={email} value={email}
autoComplete='new-password' autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改' placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readOnly readOnly
/> />
</Form.Field> </Form.Field>
<Button onClick={handleCancel}>取消</Button> <Button onClick={handleCancel}>取消</Button>
<Button positive onClick={submit}>提交</Button> <Button positive onClick={submit}>提交</Button>
</Form> </Form>
</Segment>
</SideSheet>
</> </>
); );
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment