mirror of
https://github.com/linux-do/new-api.git
synced 2025-09-17 16:06:38 +08:00
完善个人中心
This commit is contained in:
parent
fa45f3ba7b
commit
fd57a1df08
@ -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) {
|
|||||||
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) {
|
|||||||
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 {
|
|||||||
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
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
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) {
|
|||||||
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 {
|
|||||||
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) {
|
|||||||
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) {
|
|||||||
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("/")
|
||||||
|
@ -1,376 +1,548 @@
|
|||||||
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())
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleInputChange = (e, { name, value }) => {
|
useEffect(() => {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
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 generateAccessToken = async () => {
|
const handleInputChange = (name, value) => {
|
||||||
const res = await API.get('/api/user/token');
|
setInputs((inputs) => ({...inputs, [name]: value}));
|
||||||
const { success, message, data } = res.data;
|
};
|
||||||
if (success) {
|
|
||||||
setSystemToken(data);
|
|
||||||
setAffLink("");
|
|
||||||
await copy(data);
|
|
||||||
showSuccess(`令牌已重置并已复制到剪贴板`);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAffLink = async () => {
|
const generateAccessToken = async () => {
|
||||||
const res = await API.get('/api/user/aff');
|
const res = await API.get('/api/user/token');
|
||||||
const { success, message, data } = res.data;
|
const {success, message, data} = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
let link = `${window.location.origin}/register?aff=${data}`;
|
setSystemToken(data);
|
||||||
setAffLink(link);
|
await copy(data);
|
||||||
setSystemToken("");
|
showSuccess(`令牌已重置并已复制到剪贴板`);
|
||||||
await copy(link);
|
} else {
|
||||||
showSuccess(`邀请链接已复制到剪切板`);
|
showError(message);
|
||||||
} else {
|
}
|
||||||
showError(message);
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAffLinkClick = async (e) => {
|
const getAffLink = async () => {
|
||||||
e.target.select();
|
const res = await API.get('/api/user/aff');
|
||||||
await copy(e.target.value);
|
const {success, message, data} = res.data;
|
||||||
showSuccess(`邀请链接已复制到剪切板`);
|
if (success) {
|
||||||
};
|
let link = `${window.location.origin}/register?aff=${data}`;
|
||||||
|
setAffLink(link);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSystemTokenClick = async (e) => {
|
const getUserData = async () => {
|
||||||
e.target.select();
|
let res = await API.get(`/api/user/self`);
|
||||||
await copy(e.target.value);
|
const {success, message, data} = res.data;
|
||||||
showSuccess(`系统令牌已复制到剪切板`);
|
if (success) {
|
||||||
};
|
userDispatch({type: 'login', payload: data});
|
||||||
|
} else {
|
||||||
const deleteAccount = async () => {
|
showError(message);
|
||||||
if (inputs.self_account_deletion_confirmation !== userState.user.username) {
|
}
|
||||||
showError('请输入你的账户名以确认删除!');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await API.delete('/api/user/self');
|
const loadModels = async () => {
|
||||||
const { success, message } = res.data;
|
let res = await API.get(`/api/user/models`);
|
||||||
|
const {success, message, data} = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
showSuccess('账户已删除!');
|
setModels(data);
|
||||||
await API.get('/api/user/logout');
|
console.log(data)
|
||||||
userDispatch({ type: 'logout' });
|
} else {
|
||||||
localStorage.removeItem('user');
|
showError(message);
|
||||||
navigate('/login');
|
}
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const bindWeChat = async () => {
|
const handleAffLinkClick = async (e) => {
|
||||||
if (inputs.wechat_verification_code === '') return;
|
e.target.select();
|
||||||
const res = await API.get(
|
await copy(e.target.value);
|
||||||
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
|
showSuccess(`邀请链接已复制到剪切板`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSystemTokenClick = async (e) => {
|
||||||
|
e.target.select();
|
||||||
|
await copy(e.target.value);
|
||||||
|
showSuccess(`系统令牌已复制到剪切板`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAccount = async () => {
|
||||||
|
if (inputs.self_account_deletion_confirmation !== userState.user.username) {
|
||||||
|
showError('请输入你的账户名以确认删除!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await API.delete('/api/user/self');
|
||||||
|
const {success, message} = res.data;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
showSuccess('账户已删除!');
|
||||||
|
await API.get('/api/user/logout');
|
||||||
|
userDispatch({type: 'logout'});
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
navigate('/login');
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bindWeChat = async () => {
|
||||||
|
if (inputs.wechat_verification_code === '') return;
|
||||||
|
const res = await API.get(
|
||||||
|
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
|
||||||
|
);
|
||||||
|
const {success, message} = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess('微信账户绑定成功!');
|
||||||
|
setShowWeChatBindModal(false);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setOpenTransfer(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{lineHeight: '40px'}}>
|
||||||
|
<Layout>
|
||||||
|
<Layout.Content>
|
||||||
|
<Modal
|
||||||
|
title="请输入要划转的数量"
|
||||||
|
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>
|
||||||
|
</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={() => {
|
||||||
|
setShowAccountDeleteModal(true);
|
||||||
|
}}>删除个人账户</Button>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
{systemToken && (
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
readOnly
|
||||||
|
value={systemToken}
|
||||||
|
onClick={handleSystemTokenClick}
|
||||||
|
style={{marginTop: '10px'}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{
|
||||||
|
status.wechat_login && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setShowWeChatBindModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
绑定微信账号
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<Modal
|
||||||
|
onCancel={() => setShowWeChatBindModal(false)}
|
||||||
|
// onOpen={() => setShowWeChatBindModal(true)}
|
||||||
|
visible={showWeChatBindModal}
|
||||||
|
size={'mini'}
|
||||||
|
>
|
||||||
|
<Image src={status.wechat_qrcode} fluid/>
|
||||||
|
<div style={{textAlign: 'center'}}>
|
||||||
|
<p>
|
||||||
|
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Form size='large'>
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
placeholder='验证码'
|
||||||
|
name='wechat_verification_code'
|
||||||
|
value={inputs.wechat_verification_code}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<Button color='' fluid size='large' onClick={bindWeChat}>
|
||||||
|
绑定
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
const { success, message } = res.data;
|
|
||||||
if (success) {
|
|
||||||
showSuccess('微信账户绑定成功!');
|
|
||||||
setShowWeChatBindModal(false);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendVerificationCode = async () => {
|
|
||||||
setDisableButton(true);
|
|
||||||
if (inputs.email === '') return;
|
|
||||||
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 === '') 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);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ lineHeight: '40px' }}>
|
|
||||||
<Header as='h3'>通用设置</Header>
|
|
||||||
<Message>
|
|
||||||
注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI 相关的服务,请知悉。
|
|
||||||
</Message>
|
|
||||||
<Button as={Link} to={`/user/edit/`}>
|
|
||||||
更新个人信息
|
|
||||||
</Button>
|
|
||||||
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
|
|
||||||
<Button onClick={getAffLink}>复制邀请链接</Button>
|
|
||||||
<Button onClick={() => {
|
|
||||||
setShowAccountDeleteModal(true);
|
|
||||||
}}>删除个人账户</Button>
|
|
||||||
|
|
||||||
{systemToken && (
|
|
||||||
<Form.Input
|
|
||||||
fluid
|
|
||||||
readOnly
|
|
||||||
value={systemToken}
|
|
||||||
onClick={handleSystemTokenClick}
|
|
||||||
style={{ marginTop: '10px' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{affLink && (
|
|
||||||
<Form.Input
|
|
||||||
fluid
|
|
||||||
readOnly
|
|
||||||
value={affLink}
|
|
||||||
onClick={handleAffLinkClick}
|
|
||||||
style={{ marginTop: '10px' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Divider />
|
|
||||||
<Header as='h3'>账号绑定</Header>
|
|
||||||
{
|
|
||||||
status.wechat_login && (
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setShowWeChatBindModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
绑定微信账号
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<Modal
|
|
||||||
onClose={() => setShowWeChatBindModal(false)}
|
|
||||||
onOpen={() => setShowWeChatBindModal(true)}
|
|
||||||
open={showWeChatBindModal}
|
|
||||||
size={'mini'}
|
|
||||||
>
|
|
||||||
<Modal.Content>
|
|
||||||
<Modal.Description>
|
|
||||||
<Image src={status.wechat_qrcode} fluid />
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
<p>
|
|
||||||
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Form size='large'>
|
|
||||||
<Form.Input
|
|
||||||
fluid
|
|
||||||
placeholder='验证码'
|
|
||||||
name='wechat_verification_code'
|
|
||||||
value={inputs.wechat_verification_code}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
<Button color='' fluid size='large' onClick={bindWeChat}>
|
|
||||||
绑定
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</Modal.Description>
|
|
||||||
</Modal.Content>
|
|
||||||
</Modal>
|
|
||||||
{
|
|
||||||
status.github_oauth && (
|
|
||||||
<Button onClick={()=>{onGitHubOAuthClicked(status.github_client_id)}}>绑定 GitHub 账号</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setShowEmailBindModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
绑定邮箱地址
|
|
||||||
</Button>
|
|
||||||
<Modal
|
|
||||||
onClose={() => setShowEmailBindModal(false)}
|
|
||||||
onOpen={() => setShowEmailBindModal(true)}
|
|
||||||
open={showEmailBindModal}
|
|
||||||
size={'tiny'}
|
|
||||||
style={{ maxWidth: '450px' }}
|
|
||||||
>
|
|
||||||
<Modal.Header>绑定邮箱地址</Modal.Header>
|
|
||||||
<Modal.Content>
|
|
||||||
<Modal.Description>
|
|
||||||
<Form size='large'>
|
|
||||||
<Form.Input
|
|
||||||
fluid
|
|
||||||
placeholder='输入邮箱地址'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
name='email'
|
|
||||||
type='email'
|
|
||||||
action={
|
|
||||||
<Button onClick={sendVerificationCode} disabled={disableButton || loading}>
|
|
||||||
{disableButton ? `重新发送(${countdown})` : '获取验证码'}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Form.Input
|
|
||||||
fluid
|
|
||||||
placeholder='验证码'
|
|
||||||
name='email_verification_code'
|
|
||||||
value={inputs.email_verification_code}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
{turnstileEnabled ? (
|
|
||||||
<Turnstile
|
|
||||||
sitekey={turnstileSiteKey}
|
|
||||||
onVerify={(token) => {
|
|
||||||
setTurnstileToken(token);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
<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
|
|
||||||
onClose={() => setShowAccountDeleteModal(false)}
|
|
||||||
onOpen={() => setShowAccountDeleteModal(true)}
|
|
||||||
open={showAccountDeleteModal}
|
|
||||||
size={'tiny'}
|
|
||||||
style={{ maxWidth: '450px' }}
|
|
||||||
>
|
|
||||||
<Modal.Header>危险操作</Modal.Header>
|
|
||||||
<Modal.Content>
|
|
||||||
<Message>您正在删除自己的帐户,将清空所有数据且不可恢复</Message>
|
|
||||||
<Modal.Description>
|
|
||||||
<Form size='large'>
|
|
||||||
<Form.Input
|
|
||||||
fluid
|
|
||||||
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
|
|
||||||
name='self_account_deletion_confirmation'
|
|
||||||
value={inputs.self_account_deletion_confirmation}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
{turnstileEnabled ? (
|
|
||||||
<Turnstile
|
|
||||||
sitekey={turnstileSiteKey}
|
|
||||||
onVerify={(token) => {
|
|
||||||
setTurnstileToken(token);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
<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>
|
|
||||||
</Form>
|
|
||||||
</Modal.Description>
|
|
||||||
</Modal.Content>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PersonalSetting;
|
export default PersonalSetting;
|
||||||
|
@ -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');
|
||||||
|
@ -1,55 +1,53 @@
|
|||||||
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()) {
|
||||||
|
panes.push({
|
||||||
|
tab: '运营设置',
|
||||||
|
content: <OperationSetting/>,
|
||||||
|
itemKey: '2'
|
||||||
|
});
|
||||||
|
panes.push({
|
||||||
|
tab: '系统设置',
|
||||||
|
content: <SystemSetting/>,
|
||||||
|
itemKey: '3'
|
||||||
|
});
|
||||||
|
panes.push({
|
||||||
|
tab: '其他设置',
|
||||||
|
content: <OtherSetting/>,
|
||||||
|
itemKey: '4'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
];
|
|
||||||
|
|
||||||
if (isRoot()) {
|
return (
|
||||||
panes.push({
|
<div>
|
||||||
menuItem: '运营设置',
|
<Layout>
|
||||||
render: () => (
|
<Layout.Content>
|
||||||
<Tab.Pane attached={false}>
|
<Tabs type="line" defaultActiveKey="1">
|
||||||
<OperationSetting />
|
{panes.map(pane => (
|
||||||
</Tab.Pane>
|
<TabPane itemKey={pane.itemKey} tab={pane.tab}>
|
||||||
)
|
{pane.content}
|
||||||
});
|
</TabPane>
|
||||||
panes.push({
|
))}
|
||||||
menuItem: '系统设置',
|
</Tabs>
|
||||||
render: () => (
|
</Layout.Content>
|
||||||
<Tab.Pane attached={false}>
|
</Layout>
|
||||||
<SystemSetting />
|
</div>
|
||||||
</Tab.Pane>
|
);
|
||||||
)
|
|
||||||
});
|
|
||||||
panes.push({
|
|
||||||
menuItem: '其他设置',
|
|
||||||
render: () => (
|
|
||||||
<Tab.Pane attached={false}>
|
|
||||||
<OtherSetting />
|
|
||||||
</Tab.Pane>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Segment>
|
|
||||||
<Tab menu={{ secondary: true, pointing: true }} panes={panes} />
|
|
||||||
</Segment>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Setting;
|
export default Setting;
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
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 = () => {
|
|||||||
|
|
||||||
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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user