[x] 当用户充值达到5刀时,自动提升为vip分组,具体金额可在web\src\pages\TopUp\index.js调整;

[x] 修改颜色
[x] 日志页面新增快速筛选日期、修复渠道ID查询、自动更新消耗额度、删除日志详情(因为没有太大意义);
[x] 用户管理界面,支持快速设置用户组,在`web\src\components\UsersTable.js`处修改相关值;
[x] 令牌界面,删除无用的多种复制、聊天等按钮;
[x] 删除 系统访问令牌;
This commit is contained in:
wood 2023-11-07 01:43:09 +08:00
parent eb82fb79a2
commit 825374c334
16 changed files with 559 additions and 271 deletions

View File

@ -1,3 +1,17 @@
## 自定义部分
[x] 当用户充值达到5刀时自动提升为vip分组具体金额可在`web\src\pages\TopUp\index.js`调整;
[x] 修改颜色
[x] 日志页面新增快速筛选日期、修复渠道ID查询、自动更新消耗额度、删除日志详情因为没有太大意义
[x] 用户管理界面,支持快速设置用户组,在`web\src\components\UsersTable.js`处修改相关值;
[x] 令牌界面,删除无用的多种复制、聊天等按钮;
[x] 删除 系统访问令牌;
[ ] 在个人设置页面显示分组;
[ ] 渠道管理处,已启用渠道和禁用渠道, 启用/禁用 按钮改为不同颜色;
[x] 等
---
<p align="right"> <p align="right">
<strong>中文</strong> | <a href="./README.en.md">English</a> | <a href="./README.ja.md">日本語</a> <strong>中文</strong> | <a href="./README.en.md">English</a> | <a href="./README.ja.md">日本語</a>
</p> </p>

View File

@ -7,7 +7,6 @@ import (
"one-api/common" "one-api/common"
"one-api/model" "one-api/model"
"strconv" "strconv"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -554,10 +553,128 @@ func CreateUser(c *gin.Context) {
type ManageRequest struct { type ManageRequest struct {
Username string `json:"username"` Username string `json:"username"`
Action string `json:"action"` NewGroup string `json:"newGroup"`
} }
// ManageUser Only admin user can do this // ManageUser Only admin user can do this
// func ManageUser(c *gin.Context) {
// var req ManageRequest
// err := json.NewDecoder(c.Request.Body).Decode(&req)
// if err != nil {
// c.JSON(http.StatusOK, gin.H{
// "success": false,
// "message": "无效的参数",
// })
// return
// }
// user := model.User{
// Username: req.Username,
// }
// // Fill attributes
// model.DB.Where(&user).First(&user)
// if user.Id == 0 {
// c.JSON(http.StatusOK, gin.H{
// "success": false,
// "message": "用户不存在",
// })
// return
// }
// myRole := c.GetInt("role")
// if myRole <= user.Role && myRole != common.RoleRootUser {
// c.JSON(http.StatusOK, gin.H{
// "success": false,
// "message": "无权更新同权限等级或更高权限等级的用户信息",
// })
// return
// }
// switch req.Action {
// case "disable":
// user.Status = common.UserStatusDisabled
// if user.Role == common.RoleRootUser {
// c.JSON(http.StatusOK, gin.H{
// "success": false,
// "message": "无法禁用超级管理员用户",
// })
// return
// }
// case "enable":
// user.Status = common.UserStatusEnabled
// case "delete":
// if user.Role == common.RoleRootUser {
// c.JSON(http.StatusOK, gin.H{
// "success": false,
// "message": "无法删除超级管理员用户",
// })
// return
// }
// if err := user.Delete(); err != nil {
// c.JSON(http.StatusOK, gin.H{
// "success": false,
// "message": err.Error(),
// })
// return
// }
// case "promote":
// if myRole != common.RoleRootUser {
// c.JSON(http.StatusOK, gin.H{
// "success": false,
// "message": "普通管理员用户无法提升其他用户为管理员",
// })
// return
// }
// if user.Role >= common.RoleAdminUser {
// c.JSON(http.StatusOK, gin.H{
// "success": false,
// "message": "该用户已经是管理员",
// })
// return
// }
// user.Role = common.RoleAdminUser
// case "demote":
// if user.Role == common.RoleRootUser {
// c.JSON(http.StatusOK, gin.H{
// "success": false,
// "message": "无法降级超级管理员用户",
// })
// return
// }
// if user.Role == common.RoleCommonUser {
// c.JSON(http.StatusOK, gin.H{
// "success": false,
// "message": "该用户已经是普通用户",
// })
// return
// }
// user.Role = common.RoleCommonUser
// }
// if err := user.Update(false); err != nil {
// c.JSON(http.StatusOK, gin.H{
// "success": false,
// "message": err.Error(),
// })
// return
// }
// user.Group = req.NewGroup
// clearUser := model.User{
// Group: user.Group,
// Role: user.Role,
// Status: user.Status,
// }
// c.JSON(http.StatusOK, gin.H{
// "success": true,
// "message": "",
// "data": clearUser,
// })
// return
// }
func ManageUser(c *gin.Context) { func ManageUser(c *gin.Context) {
var req ManageRequest var req ManageRequest
err := json.NewDecoder(c.Request.Body).Decode(&req) err := json.NewDecoder(c.Request.Body).Decode(&req)
@ -569,10 +686,11 @@ func ManageUser(c *gin.Context) {
}) })
return return
} }
user := model.User{ user := model.User{
Username: req.Username, Username: req.Username,
} }
// Fill attributes
model.DB.Where(&user).First(&user) model.DB.Where(&user).First(&user)
if user.Id == 0 { if user.Id == 0 {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@ -581,6 +699,7 @@ func ManageUser(c *gin.Context) {
}) })
return return
} }
myRole := c.GetInt("role") myRole := c.GetInt("role")
if myRole <= user.Role && myRole != common.RoleRootUser { if myRole <= user.Role && myRole != common.RoleRootUser {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@ -589,66 +708,9 @@ func ManageUser(c *gin.Context) {
}) })
return return
} }
switch req.Action {
case "disable": // 更新用户分组
user.Status = common.UserStatusDisabled user.Group = req.NewGroup
if user.Role == common.RoleRootUser {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无法禁用超级管理员用户",
})
return
}
case "enable":
user.Status = common.UserStatusEnabled
case "delete":
if user.Role == common.RoleRootUser {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无法删除超级管理员用户",
})
return
}
if err := user.Delete(); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
case "promote":
if myRole != common.RoleRootUser {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "普通管理员用户无法提升其他用户为管理员",
})
return
}
if user.Role >= common.RoleAdminUser {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该用户已经是管理员",
})
return
}
user.Role = common.RoleAdminUser
case "demote":
if user.Role == common.RoleRootUser {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无法降级超级管理员用户",
})
return
}
if user.Role == common.RoleCommonUser {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该用户已经是普通用户",
})
return
}
user.Role = common.RoleCommonUser
}
if err := user.Update(false); err != nil { if err := user.Update(false); err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@ -657,10 +719,13 @@ func ManageUser(c *gin.Context) {
}) })
return return
} }
clearUser := model.User{ clearUser := model.User{
Group: user.Group,
Role: user.Role, Role: user.Role,
Status: user.Status, Status: user.Status,
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
@ -669,6 +734,7 @@ func ManageUser(c *gin.Context) {
return return
} }
func EmailBind(c *gin.Context) { func EmailBind(c *gin.Context) {
email := c.Query("email") email := c.Query("email")
code := c.Query("code") code := c.Query("code")

View File

@ -30,21 +30,22 @@ function renderType(type) {
function renderBalance(type, balance) { function renderBalance(type, balance) {
switch (type) { switch (type) {
case 1: // OpenAI case 1: // OpenAI
return <span>${balance.toFixed(2)}</span>; return <span style={{ color: 'var(--czl-primary-color)' }}>${balance.toFixed(2)}</span>;
case 4: // CloseAI case 4: // CloseAI
return <span>¥{balance.toFixed(2)}</span>; return <span style={{ color: 'var(--czl-primary-color-hover)' }}>¥{balance.toFixed(2)}</span>;
case 8: // 自定义 case 8: // 自定义
return <span>${balance.toFixed(2)}</span>; return <span style={{ color: 'var(--czl-primary-color-pressed)' }}>${balance.toFixed(2)}</span>;
case 5: // OpenAI-SB case 5: // OpenAI-SB
return <span>¥{(balance / 10000).toFixed(2)}</span>; return <span style={{ color: 'var(--czl-primary-color-suppl)' }}>¥{(balance / 10000).toFixed(2)}</span>;
case 10: // AI Proxy case 10: // AI Proxy
return <span>{renderNumber(balance)}</span>; return <span style={{ color: 'var(--czl-success-color)' }}>{renderNumber(balance)}</span>;
case 12: // API2GPT case 12: // API2GPT
return <span>¥{balance.toFixed(2)}</span>; return <span style={{ color: 'var(--czl-error-color)' }}>¥{balance.toFixed(2)}</span>;
case 13: // AIGC2D case 13: // AIGC2D
return <span>{renderNumber(balance)}</span>; return <span style={{ color: 'var(--czl-warning-color)' }}>{renderNumber(balance)}</span>;
default: default:
return <span>不支持</span>; return <span style={{ color: 'var(--czl-info-color)' }}>不支持</span>;
} }
} }
@ -150,11 +151,11 @@ const ChannelsTable = () => {
const renderStatus = (status) => { const renderStatus = (status) => {
switch (status) { switch (status) {
case 1: case 1:
return <Label basic color='green'>已启用</Label>; return <Label basic style={{ color: 'var(--czl-success-color)' }}>已启用</Label>;
case 2: case 2:
return ( return (
<Popup <Popup
trigger={<Label basic color='red'> trigger={<Label basic style={{ color: 'var(--czl-error-color)' }}>
已禁用 已禁用
</Label>} </Label>}
content='本渠道被手动禁用' content='本渠道被手动禁用'
@ -164,7 +165,7 @@ const ChannelsTable = () => {
case 3: case 3:
return ( return (
<Popup <Popup
trigger={<Label basic color='yellow'> trigger={<Label basic style={{ color: 'var(--czl-warning-color)' }}>
已禁用 已禁用
</Label>} </Label>}
content='本渠道被程序自动禁用' content='本渠道被程序自动禁用'
@ -173,10 +174,11 @@ const ChannelsTable = () => {
); );
default: default:
return ( return (
<Label basic color='grey'> <Label basic style={{ color: 'var(--czl-grayC)' }}>
未知状态 未知状态
</Label> </Label>
); );
} }
}; };
@ -184,15 +186,15 @@ const ChannelsTable = () => {
let time = responseTime / 1000; let time = responseTime / 1000;
time = time.toFixed(2) + ' 秒'; time = time.toFixed(2) + ' 秒';
if (responseTime === 0) { if (responseTime === 0) {
return <Label basic color='grey'>未测试</Label>; return <Label basic style={{ color: 'var(--czl-grayA)' }}>未测试</Label>;
} else if (responseTime <= 1000) { } else if (responseTime <= 1000) {
return <Label basic color='green'>{time}</Label>; return <Label basic style={{ color: 'var(--czl-success-color)' }}>{time}</Label>;
} else if (responseTime <= 3000) { } else if (responseTime <= 3000) {
return <Label basic color='olive'>{time}</Label>; return <Label basic style={{ color: 'var(--czl-primary-color)' }}>{time}</Label>;
} else if (responseTime <= 5000) { } else if (responseTime <= 5000) {
return <Label basic color='yellow'>{time}</Label>; return <Label basic style={{ color: 'var(--czl-warning-color)' }}>{time}</Label>;
} else { } else {
return <Label basic color='red'>{time}</Label>; return <Label basic style={{ color: 'var(--czl-error-color)' }}>{time}</Label>;
} }
}; };
@ -416,8 +418,8 @@ const ChannelsTable = () => {
trigger={<span onClick={() => { trigger={<span onClick={() => {
updateChannelBalance(channel.id, channel.name, idx); updateChannelBalance(channel.id, channel.name, idx);
}} style={{ cursor: 'pointer' }}> }} style={{ cursor: 'pointer' }}>
{renderBalance(channel.type, channel.balance)} {renderBalance(channel.type, channel.balance)}
</span>} </span>}
content='点击更新' content='点击更新'
basic basic
/> />
@ -443,6 +445,7 @@ const ChannelsTable = () => {
<Button <Button
size={'small'} size={'small'}
positive positive
style={{ backgroundColor: 'var(--czl-success-color)' }}
onClick={() => { onClick={() => {
testChannel(channel.id, channel.name, idx); testChannel(channel.id, channel.name, idx);
}} }}
@ -451,7 +454,7 @@ const ChannelsTable = () => {
</Button> </Button>
<Popup <Popup
trigger={ trigger={
<Button size='small' negative> <Button size='small' negative style={{ backgroundColor: 'var(--czl-error-color)' }}>
删除 删除
</Button> </Button>
} }
@ -461,6 +464,7 @@ const ChannelsTable = () => {
> >
<Button <Button
negative negative
style={{ backgroundColor: 'var(--czl-error-color)' }}
onClick={() => { onClick={() => {
manageChannel(channel.id, 'delete', idx); manageChannel(channel.id, 'delete', idx);
}} }}
@ -469,7 +473,9 @@ const ChannelsTable = () => {
</Button> </Button>
</Popup> </Popup>
<Button <Button
negative
size={'small'} size={'small'}
style={{ backgroundColor: 'var(--czl-warning-color)' }}
onClick={() => { onClick={() => {
manageChannel( manageChannel(
channel.id, channel.id,
@ -481,9 +487,11 @@ const ChannelsTable = () => {
{channel.status === 1 ? '禁用' : '启用'} {channel.status === 1 ? '禁用' : '启用'}
</Button> </Button>
<Button <Button
negative
size={'small'} size={'small'}
as={Link} as={Link}
to={'/channel/edit/' + channel.id} to={'/channel/edit/' + channel.id}
style={{ backgroundColor: 'var(--czl-primary-color)' }}
> >
编辑 编辑
</Button> </Button>
@ -504,7 +512,7 @@ const ChannelsTable = () => {
测试所有已启用通道 测试所有已启用通道
</Button> </Button>
<Button size='small' onClick={updateAllChannelsBalance} <Button size='small' onClick={updateAllChannelsBalance}
loading={loading || updatingBalance}>更新所有已启用通道余额</Button> loading={loading || updatingBalance}>更新所有已启用通道余额</Button>
<Popup <Popup
trigger={ trigger={
<Button size='small' loading={loading}> <Button size='small' loading={loading}>

View File

@ -11,53 +11,63 @@ let headerButtons = [
{ {
name: '首页', name: '首页',
to: '/', to: '/',
icon: 'home' icon: 'home',
color: 'var(--czl-primary-color)'
}, },
{ {
name: '渠道', name: '渠道',
to: '/channel', to: '/channel',
icon: 'sitemap', icon: 'sitemap',
color: 'var(--czl-primary-color)',
admin: true admin: true
}, },
{ {
name: '令牌', name: '令牌',
to: '/token', to: '/token',
icon: 'key' icon: 'key',
color: 'var(--czl-primary-color)'
}, },
{ {
name: '兑换', name: '兑换',
to: '/redemption', to: '/redemption',
icon: 'dollar sign', icon: 'dollar sign',
color: 'var(--czl-primary-color)',
admin: true admin: true
}, },
{ {
name: '充值', name: '充值',
to: '/topup', to: '/topup',
icon: 'cart' icon: 'cart',
color: 'var(--czl-primary-color)'
}, },
{ {
name: '用户', name: '用户',
to: '/user', to: '/user',
icon: 'user', icon: 'user',
color: 'var(--czl-primary-color)',
admin: true admin: true
}, },
{ {
name: '日志', name: '日志',
to: '/log', to: '/log',
icon: 'book' icon: 'book',
color: 'var(--czl-primary-color)'
}, },
{ {
name: '设置', name: '设置',
to: '/setting', to: '/setting',
icon: 'setting' icon: 'setting',
color: 'var(--czl-primary-color)'
}, },
{ {
name: '关于', name: '关于',
to: '/about', to: '/about',
icon: 'info circle' icon: 'info circle',
color: 'var(--czl-primary-color)'
} }
]; ];
if (localStorage.getItem('chat_link')) { if (localStorage.getItem('chat_link')) {
headerButtons.splice(1, 0, { headerButtons.splice(1, 0, {
name: '聊天', name: '聊天',
@ -148,11 +158,11 @@ const Header = () => {
</Menu> </Menu>
{showSidebar ? ( {showSidebar ? (
<Segment style={{ marginTop: 0, borderTop: '0' }}> <Segment style={{ marginTop: 0, borderTop: '0' }}>
<Menu secondary vertical style={{ width: '100%', margin: 0 }}> <Menu secondary vertical style={{ width: '100%', margin: 0, backgroundColor: 'var(--czl-main)' }}>
{renderButtons(true)} {renderButtons(true)}
<Menu.Item> <Menu.Item>
{userState.user ? ( {userState.user ? (
<Button onClick={logout}>注销</Button> <Button onClick={logout} style={{ backgroundColor: 'var(--czl-primary-color)' }}>注销</Button>
) : ( ) : (
<> <>
<Button <Button
@ -160,6 +170,7 @@ const Header = () => {
setShowSidebar(false); setShowSidebar(false);
navigate('/login'); navigate('/login');
}} }}
style={{ backgroundColor: 'var(--czl-primary-color)' }}
> >
登录 登录
</Button> </Button>
@ -168,6 +179,7 @@ const Header = () => {
setShowSidebar(false); setShowSidebar(false);
navigate('/register'); navigate('/register');
}} }}
style={{ backgroundColor: 'var(--czl-primary-color)' }}
> >
注册 注册
</Button> </Button>
@ -175,6 +187,7 @@ const Header = () => {
)} )}
</Menu.Item> </Menu.Item>
</Menu> </Menu>
</Segment> </Segment>
) : ( ) : (
<></> <></>

View File

@ -98,6 +98,7 @@ const LoginForm = () => {
name='username' name='username'
value={username} value={username}
onChange={handleChange} onChange={handleChange}
style={{ backgroundColor: 'var(--czl-main)', color: 'var(--czl-grayA)' }}
/> />
<Form.Input <Form.Input
fluid fluid
@ -108,22 +109,29 @@ const LoginForm = () => {
type='password' type='password'
value={password} value={password}
onChange={handleChange} onChange={handleChange}
style={{ backgroundColor: 'var(--czl-main)', color: 'var(--czl-grayA)' }}
/> />
<Button color='green' fluid size='large' onClick={handleSubmit}> <Button
fluid
size='large'
onClick={handleSubmit}
style={{ backgroundColor: 'var(--czl-success-color)', color: 'var(--czl-main)' }}
>
登录 登录
</Button> </Button>
</Segment> </Segment>
</Form> </Form>
<Message> <Message>
忘记密码 忘记密码
<Link to='/reset' className='btn btn-link'> <Link to='/reset' className='btn btn-link' style={{ color: 'var(--czl-link-color)' }}>
点击重置 点击重置
</Link> </Link>
没有账户 没有账户
<Link to='/register' className='btn btn-link'> <Link to='/register' className='btn btn-link' style={{ color: 'var(--czl-link-color)' }}>
点击注册 点击注册
</Link> </Link>
</Message> </Message>
{status.github_oauth || status.wechat_login ? ( {status.github_oauth || status.wechat_login ? (
<> <>
<Divider horizontal>Or</Divider> <Divider horizontal>Or</Divider>

View File

@ -29,15 +29,15 @@ const LOG_OPTIONS = [
function renderType(type) { function renderType(type) {
switch (type) { switch (type) {
case 1: case 1:
return <Label basic color='green'> 充值 </Label>; return <Label basic style={{ color: 'var(--czl-success-color)' }}> 充值 </Label>;
case 2: case 2:
return <Label basic color='olive'> 消费 </Label>; return <Label basic style={{ color: 'var(--czl-primary-color)' }}> 消费 </Label>;
case 3: case 3:
return <Label basic color='orange'> 管理 </Label>; return <Label basic style={{ color: 'var(--czl-warning-color)' }}> 管理 </Label>;
case 4: case 4:
return <Label basic color='purple'> 系统 </Label>; return <Label basic style={{ color: 'var(--czl-primary-color-suppl-dark)' }}> 系统 </Label>;
default: default:
return <Label basic color='black'> 未知 </Label>; return <Label basic style={{ color: 'var(--czl-primary-color-dark)' }}> 未知 </Label>;
} }
} }
@ -70,6 +70,79 @@ const LogsTable = () => {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
}; };
// 快速选时间
const handleTimePresetClick = (preset) => {
let start, end;
switch (preset) {
case 'today':
start = new Date();
start.setHours(0, 0, 0, 0);
end = new Date();
end.setHours(23, 59, 59, 999);
break;
case 'yesterday':
start = new Date(Date.now() - 24 * 60 * 60 * 1000);
start.setHours(0, 0, 0, 0);
end = new Date(Date.now() - 24 * 60 * 60 * 1000);
end.setHours(23, 59, 59, 999);
break;
case 'week':
start = new Date();
start.setDate(start.getDate() - start.getDay() + (start.getDay() === 0 ? -6 : 1));
start.setHours(0, 0, 0, 0);
end = new Date();
end.setHours(23, 59, 59, 999);
break;
case 'lastWeek':
start = new Date();
start.setDate(start.getDate() - start.getDay() + (start.getDay() === 0 ? -13 : -6));
start.setHours(0, 0, 0, 0);
end = new Date(start);
end.setDate(end.getDate() + 6);
end.setHours(23, 59, 59, 999);
break;
case '30days':
start = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
start.setHours(0, 0, 0, 0);
end = new Date();
end.setHours(23, 59, 59, 999);
break;
case 'month':
start = new Date();
start.setDate(1);
start.setHours(0, 0, 0, 0);
end = new Date();
end.setHours(23, 59, 59, 999);
break;
case 'lastMonth':
start = new Date();
start.setDate(1);
start.setMonth(start.getMonth() - 1);
start.setHours(0, 0, 0, 0);
end = new Date(start);
end.setMonth(end.getMonth() + 1);
end.setDate(end.getDate() - 1);
end.setHours(23, 59, 59, 999);
break;
case 'reset':
start = new Date(0);
end = new Date(now.getTime() + 3600 * 1000);
break;
default:
break;
}
setInputs((inputs) => ({
...inputs,
start_timestamp: timestamp2string(start.getTime() / 1000),
end_timestamp: timestamp2string(end.getTime() / 1000),
}));
};
const getLogSelfStat = async () => { const getLogSelfStat = async () => {
let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000; let localEndTimestamp = Date.parse(end_timestamp) / 1000;
@ -146,7 +219,7 @@ const LogsTable = () => {
useEffect(() => { useEffect(() => {
refresh().then(); refresh().then();
}, [logType,username, token_name, model_name, channel]); }, [logType, username, token_name, model_name, channel, start_timestamp, end_timestamp]);
const searchLogs = async () => { const searchLogs = async () => {
if (searchKeyword === '') { if (searchKeyword === '') {
@ -198,7 +271,7 @@ const LogsTable = () => {
<Segment> <Segment>
<Header as='h3'> <Header as='h3'>
使用明细消耗额度 使用明细消耗额度
{renderQuota(stat.quota)} <span style={{ color: 'var(--czl-primary-color)' }}>{renderQuota(stat.quota)}</span>
</Header> </Header>
<Form> <Form>
@ -215,20 +288,36 @@ const LogsTable = () => {
name='end_timestamp' name='end_timestamp'
onChange={handleInputChange} /> onChange={handleInputChange} />
<Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button> <Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button>
</Form.Group> </Form.Group>
{ <Form.Group>
isAdminUser && <> {isAdminUser && (
<Form.Group> <>
<Form.Input fluid label={'渠道 ID'} width={3} value={channel} <Form.Input fluid label={'渠道 ID'} width={3} value={channel}
placeholder='可选值' name='channel' placeholder='可选值' name='channel'
onChange={handleInputChange} /> onChange={handleInputChange} />
<Form.Input fluid label={'用户名称'} width={3} value={username} <Form.Input fluid label={'用户名称'} width={3} value={username}
placeholder={'可选值'} name='username' placeholder={'可选值'} name='username'
onChange={handleInputChange} /> onChange={handleInputChange} />
</>
)}
{/* 将按钮组移动到这里 */}
<Form.Field>
<label>筛选时间</label>
<Button.Group>
<Button onClick={() => handleTimePresetClick('today')}>今天</Button>
<Button onClick={() => handleTimePresetClick('yesterday')}>昨天</Button>
<Button onClick={() => handleTimePresetClick('week')}>本周</Button>
<Button onClick={() => handleTimePresetClick('lastWeek')}>上周</Button>
<Button onClick={() => handleTimePresetClick('month')}>本月</Button>
<Button onClick={() => handleTimePresetClick('lastMonth')}>上月</Button>
<Button onClick={() => handleTimePresetClick('30days')}>30天内</Button>
<Button onClick={() => handleTimePresetClick('reset')}>重置</Button>
</Form.Group> </Button.Group>
</> </Form.Field>
} </Form.Group>
</Form> </Form>
<Table basic compact size='small'> <Table basic compact size='small'>
<Table.Header> <Table.Header>

View File

@ -13,11 +13,6 @@ const OtherSetting = () => {
HomePageContent: '' HomePageContent: ''
}); });
let [loading, setLoading] = useState(false); let [loading, setLoading] = useState(false);
const [showUpdateModal, setShowUpdateModal] = useState(false);
const [updateData, setUpdateData] = useState({
tag_name: '',
content: ''
});
const getOptions = async () => { const getOptions = async () => {
const res = await API.get('/api/option/'); const res = await API.get('/api/option/');
@ -82,33 +77,12 @@ const OtherSetting = () => {
await updateOption(key, inputs[key]); await updateOption(key, inputs[key]);
}; };
const openGitHubRelease = () => {
window.location =
'https://github.com/songquanpeng/one-api/releases/latest';
};
const checkUpdate = async () => {
const res = await API.get(
'https://api.github.com/repos/songquanpeng/one-api/releases/latest'
);
const { tag_name, body } = res.data;
if (tag_name === process.env.REACT_APP_VERSION) {
showSuccess(`已是最新版本:${tag_name}`);
} else {
setUpdateData({
tag_name: tag_name,
content: marked.parse(body)
});
setShowUpdateModal(true);
}
};
return ( return (
<Grid columns={1}> <Grid columns={1}>
<Grid.Column> <Grid.Column>
<Form loading={loading}> <Form loading={loading}>
<Header as='h3'>通用设置</Header> <Header as='h3'>通用设置</Header>
<Form.Button onClick={checkUpdate}>检查更新</Form.Button>
<Form.Group widths='equal'> <Form.Group widths='equal'>
<Form.TextArea <Form.TextArea
label='公告' label='公告'
@ -178,28 +152,6 @@ const OtherSetting = () => {
<Form.Button onClick={submitFooter}>设置页脚</Form.Button> <Form.Button onClick={submitFooter}>设置页脚</Form.Button>
</Form> </Form>
</Grid.Column> </Grid.Column>
<Modal
onClose={() => setShowUpdateModal(false)}
onOpen={() => setShowUpdateModal(true)}
open={showUpdateModal}
>
<Modal.Header>新版本{updateData.tag_name}</Modal.Header>
<Modal.Content>
<Modal.Description>
<div dangerouslySetInnerHTML={{ __html: updateData.content }}></div>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => setShowUpdateModal(false)}>关闭</Button>
<Button
content='详情'
onClick={() => {
setShowUpdateModal(false);
openGitHubRelease();
}}
/>
</Modal.Actions>
</Modal>
</Grid> </Grid>
); );
}; };

View File

@ -94,7 +94,7 @@ const PasswordResetConfirm = () => {
/> />
)} )}
<Button <Button
color='green' color='var(--czl-success-color)'
fluid fluid
size='large' size='large'
onClick={handleSubmit} onClick={handleSubmit}

View File

@ -83,7 +83,7 @@ const PasswordResetForm = () => {
<></> <></>
)} )}
<Button <Button
color='green' style={{backgroundColor: 'var(--czl-success-color)', color: 'white'}}
fluid fluid
size='large' size='large'
onClick={handleSubmit} onClick={handleSubmit}

View File

@ -17,13 +17,13 @@ function renderTimestamp(timestamp) {
function renderStatus(status) { function renderStatus(status) {
switch (status) { switch (status) {
case 1: case 1:
return <Label basic color='green'>未使用</Label>; return <Label basic style={{color: 'var(--czl-primary-color)'}}>未使用</Label>;
case 2: case 2:
return <Label basic color='red'> 已禁用 </Label>; return <Label basic style={{color: 'var(--czl-error-color)'}}> 已禁用 </Label>;
case 3: case 3:
return <Label basic color='grey'> 已使用 </Label>; return <Label basic style={{color: 'var(--czl-success-color)'}}> 已使用 </Label>;
default: default:
return <Label basic color='black'> 未知状态 </Label>; return <Label basic style={{color: 'var(--czl-grayD)'}}> 未知状态 </Label>;
} }
} }
@ -226,59 +226,64 @@ const RedemptionsTable = () => {
<Table.Cell>{redemption.redeemed_time ? renderTimestamp(redemption.redeemed_time) : "尚未兑换"} </Table.Cell> <Table.Cell>{redemption.redeemed_time ? renderTimestamp(redemption.redeemed_time) : "尚未兑换"} </Table.Cell>
<Table.Cell> <Table.Cell>
<div> <div>
<Button <Button
size={'small'} size={'small'}
positive positive
onClick={async () => { onClick={async () => {
if (await copy(redemption.key)) { if (await copy(redemption.key)) {
showSuccess('已复制到剪贴板!'); showSuccess('已复制到剪贴板!');
} else { } else {
showWarning('无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。') showWarning('无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。')
setSearchKeyword(redemption.key); setSearchKeyword(redemption.key);
} }
}} }}
> style={{ backgroundColor: "var(--czl-success-color)", color: "white"}}
复制 >
</Button> 复制
<Popup </Button>
trigger={ <Popup
<Button size='small' negative> trigger={
删除 <Button size='small' negative style={{ backgroundColor: "var(--czl-error-color)", color: "white"}}>
</Button> 删除
} </Button>
on='click' }
flowing on='click'
hoverable flowing
> hoverable
<Button >
negative <Button
onClick={() => { negative
manageRedemption(redemption.id, 'delete', idx); onClick={() => {
}} manageRedemption(redemption.id, 'delete', idx);
> }}
确认删除 style={{ backgroundColor: "var(--czl-error-color)", color: "white"}}
</Button> >
</Popup> 确认删除
<Button </Button>
size={'small'} </Popup>
disabled={redemption.status === 3} // used <Button
onClick={() => { size={'small'}
manageRedemption( disabled={redemption.status === 3} // used
redemption.id, onClick={() => {
redemption.status === 1 ? 'disable' : 'enable', manageRedemption(
idx redemption.id,
); redemption.status === 1 ? 'disable' : 'enable',
}} idx
> );
{redemption.status === 1 ? '禁用' : '启用'} }}
</Button> style={{ backgroundColor: "var(--czl-link-color)", color: "white"}}
<Button >
size={'small'} {redemption.status === 1 ? '禁用' : '启用'}
as={Link} </Button>
to={'/redemption/edit/' + redemption.id} <Button
> size={'small'}
编辑 as={Link}
</Button> to={'/redemption/edit/' + redemption.id}
style={{ backgroundColor: "var(--czl-primary-color)", color: "white"}}
>
编辑
</Button>
</div> </div>
</Table.Cell> </Table.Cell>
</Table.Row> </Table.Row>

View File

@ -142,7 +142,7 @@ const RegisterForm = () => {
name='email' name='email'
type='email' type='email'
action={ action={
<Button onClick={sendVerificationCode} disabled={loading}> <Button onClick={sendVerificationCode} disabled={loading} style={{ backgroundColor: 'var(--czl-primary-color)', color: '#fff' }}>
获取验证码 获取验证码
</Button> </Button>
} }
@ -170,7 +170,7 @@ const RegisterForm = () => {
<></> <></>
)} )}
<Button <Button
color='green' style={{ backgroundColor: 'var(--czl-success-color)', color: '#fff' }}
fluid fluid
size='large' size='large'
onClick={handleSubmit} onClick={handleSubmit}
@ -182,7 +182,7 @@ const RegisterForm = () => {
</Form> </Form>
<Message> <Message>
已有账户 已有账户
<Link to='/login' className='btn btn-link'> <Link to='/login' className='btn btn-link' style={{ color: 'var(--czl-link-color)' }}>
点击登录 点击登录
</Link> </Link>
</Message> </Message>

View File

@ -18,15 +18,16 @@ function renderTimestamp(timestamp) {
function renderStatus(status) { function renderStatus(status) {
switch (status) { switch (status) {
case 1: case 1:
return <Label basic color='green'>已启用</Label>; return <Label basic style={{ color: 'var(--czl-success-color)' }}>已启用</Label>;
case 2: case 2:
return <Label basic color='red'> 已禁用 </Label>; return <Label basic style={{ color: 'var(--czl-error-color)' }}> 已禁用 </Label>;
case 3: case 3:
return <Label basic color='yellow'> 已过期 </Label>; return <Label basic style={{ color: 'var(--czl-warning-color)' }}> 已过期 </Label>;
case 4: case 4:
return <Label basic color='grey'> 已耗尽 </Label>; return <Label basic style={{ color: 'var(--czl-grayB)' }}> 已耗尽 </Label>;
default: default:
return <Label basic color='black'> 未知状态 </Label>; return <Label basic style={{ color: 'var(--czl-grayD)' }}> 未知状态 </Label>;
} }
} }
@ -84,7 +85,7 @@ const TokensTable = () => {
let encodedServerAddress = encodeURIComponent(serverAddress); let encodedServerAddress = encodeURIComponent(serverAddress);
const nextLink = localStorage.getItem('chat_link'); const nextLink = localStorage.getItem('chat_link');
let nextUrl; let nextUrl;
if (nextLink) { if (nextLink) {
nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
} else { } else {
@ -118,7 +119,7 @@ const TokensTable = () => {
let serverAddress = ''; let serverAddress = '';
if (status) { if (status) {
status = JSON.parse(status); status = JSON.parse(status);
serverAddress = status.server_address; serverAddress = status.server_address;
} }
if (serverAddress === '') { if (serverAddress === '') {
serverAddress = window.location.origin; serverAddress = window.location.origin;
@ -126,7 +127,7 @@ const TokensTable = () => {
let encodedServerAddress = encodeURIComponent(serverAddress); let encodedServerAddress = encodeURIComponent(serverAddress);
const chatLink = localStorage.getItem('chat_link'); const chatLink = localStorage.getItem('chat_link');
let defaultUrl; let defaultUrl;
if (chatLink) { if (chatLink) {
defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
} else { } else {
@ -137,15 +138,15 @@ const TokensTable = () => {
case 'ama': case 'ama':
url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`; url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
break; break;
case 'opencat': case 'opencat':
url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
break; break;
default: default:
url = defaultUrl; url = defaultUrl;
} }
window.open(url, '_blank'); window.open(url, '_blank');
} }
@ -314,19 +315,20 @@ const TokensTable = () => {
<Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell> <Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
<Table.Cell> <Table.Cell>
<div> <div>
<Button <Button
size={'small'} size={'small'}
positive positive
onClick={async () => { onClick={async () => {
await onCopy('', token.key); await onCopy('', token.key);
}} }}
> style={{ backgroundColor: 'var(--czl-success-color)', borderColor: 'var(--czl-success-color)' }}
复制 >
</Button> 复制
</Button>
{' '} {' '}
<Popup <Popup
trigger={ trigger={
<Button size='small' negative> <Button size='small' negative style={{ backgroundColor: 'var(--czl-error-color)', borderColor: 'var(--czl-error-color)' }}>
删除 删除
</Button> </Button>
} }
@ -339,6 +341,7 @@ const TokensTable = () => {
onClick={() => { onClick={() => {
manageToken(token.id, 'delete', idx); manageToken(token.id, 'delete', idx);
}} }}
style={{ backgroundColor: 'var(--czl-error-color)', borderColor: 'var(--czl-error-color)' }}
> >
删除令牌 {token.name} 删除令牌 {token.name}
</Button> </Button>
@ -356,9 +359,11 @@ const TokensTable = () => {
{token.status === 1 ? '禁用' : '启用'} {token.status === 1 ? '禁用' : '启用'}
</Button> </Button>
<Button <Button
negative
size={'small'} size={'small'}
as={Link} as={Link}
to={'/token/edit/' + token.id} to={'/token/edit/' + token.id}
style={{ backgroundColor: 'var(--czl-primary-color)', borderColor: 'var(--czl-primary-color)' }}
> >
编辑 编辑
</Button> </Button>
@ -372,10 +377,17 @@ const TokensTable = () => {
<Table.Footer> <Table.Footer>
<Table.Row> <Table.Row>
<Table.HeaderCell colSpan='7'> <Table.HeaderCell colSpan='7'>
<Button size='small' as={Link} to='/token/add' loading={loading}> <Button
size='small'
as={Link}
to='/token/add'
loading={loading}
style={{ color: "var(--czl-main)", backgroundColor: "var(--czl-link-color)" }}
>
添加新的令牌 添加新的令牌
</Button> </Button>
<Button size='small' onClick={refresh} loading={loading}>刷新</Button> <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
<Pagination <Pagination
floated='right' floated='right'
activePage={activePage} activePage={activePage}

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react'; import { Button, Form, Label, Pagination, Popup, Table, Dropdown } from 'semantic-ui-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { API, showError, showSuccess } from '../helpers'; import { API, showError, showSuccess } from '../helpers';
@ -11,11 +11,11 @@ function renderRole(role) {
case 1: case 1:
return <Label>普通用户</Label>; return <Label>普通用户</Label>;
case 10: case 10:
return <Label color='yellow'>管理员</Label>; return <Label color='var(--czl-warning-color)'>管理员</Label>;
case 100: case 100:
return <Label color='orange'>超级管理员</Label>; return <Label color='var(--czl-error-color)'>超级管理员</Label>;
default: default:
return <Label color='red'>未知身份</Label>; return <Label color='var(--czl-error-color)'>未知身份</Label>;
} }
} }
@ -61,24 +61,20 @@ const UsersTable = () => {
}); });
}, []); }, []);
const manageUser = (username, action, idx) => { const manageUser = (username, idx, newGroup) => {
(async () => { (async () => {
const res = await API.post('/api/user/manage', { const res = await API.post('/api/user/manage', {
username, username,
action newGroup
}); });
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess('操作成功完成!'); showSuccess('操作成功完成!');
let user = res.data.data; let user = res.data.data;
let newUsers = [...users]; let newUsers = [...users];
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
if (action === 'delete') { newUsers[realIdx].group = user.group; // 用新的 user.group 更新用户分组
newUsers[realIdx].deleted = true;
} else {
newUsers[realIdx].status = user.status;
newUsers[realIdx].role = user.role;
}
setUsers(newUsers); setUsers(newUsers);
} else { } else {
showError(message); showError(message);
@ -86,6 +82,20 @@ const UsersTable = () => {
})(); })();
}; };
const groupOptions = [
{ key: 'default', value: 'default', text: '默认', color: 'var(--czl-grayA)' },
{ key: 'vip', value: 'vip', text: 'VIP', color: 'var(--czl-success-color)' },
{ key: 'svip', value: 'svip', text: '超级VIP', color: 'var(--czl-error-color)' },
];
const groupColor = (userGroup) => {
const group = groupOptions.find((option) => option.value === userGroup);
return group ? group.color : 'inherit'; // 如果未找到分组,则返回默认颜色
};
const renderStatus = (status) => { const renderStatus = (status) => {
switch (status) { switch (status) {
case 1: case 1:
@ -175,14 +185,14 @@ const UsersTable = () => {
> >
用户名 用户名
</Table.HeaderCell> </Table.HeaderCell>
<Table.HeaderCell {/* <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
sortUser('group'); sortUser('group');
}} }}
> >
分组 分组
</Table.HeaderCell> </Table.HeaderCell> */}
<Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
@ -231,10 +241,7 @@ const UsersTable = () => {
hoverable hoverable
/> />
</Table.Cell> </Table.Cell>
<Table.Cell>{renderGroup(user.group)}</Table.Cell> {/* <Table.Cell>{renderGroup(user.group)}</Table.Cell> */}
{/*<Table.Cell>*/}
{/* {user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}*/}
{/*</Table.Cell>*/}
<Table.Cell> <Table.Cell>
<Popup content='剩余额度' trigger={<Label basic>{renderQuota(user.quota)}</Label>} /> <Popup content='剩余额度' trigger={<Label basic>{renderQuota(user.quota)}</Label>} />
<Popup content='已用额度' trigger={<Label basic>{renderQuota(user.used_quota)}</Label>} /> <Popup content='已用额度' trigger={<Label basic>{renderQuota(user.used_quota)}</Label>} />
@ -244,6 +251,30 @@ const UsersTable = () => {
<Table.Cell>{renderStatus(user.status)}</Table.Cell> <Table.Cell>{renderStatus(user.status)}</Table.Cell>
<Table.Cell> <Table.Cell>
<div> <div>
<Button.Group size={'small'} style={{marginRight: '10px'}}>
<Button
positive
size={'small'}
className={`group-button ${user.group}`}
style={{
backgroundColor: groupColor(user.group), // 设置透明背景颜色
width: '100px',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{user.group}
</Button>
<Dropdown
className="button icon"
style={{
backgroundColor: groupColor(user.group),}}
floating
options={groupOptions}
trigger={<></>}
onChange={(e, { value }) => manageUser(user.username, idx, value)}
/>
</Button.Group>
<Popup <Popup
trigger={ trigger={
<Button size='small' negative disabled={user.role === 100}> <Button size='small' negative disabled={user.role === 100}>

View File

@ -1,3 +1,65 @@
:root {
--toastify-color-light: var(--czl-main) !important;
--toastify-color-dark: var(--czl-main-dark) !important;
--toastify-color-info: var(--czl-info-color) !important;
--toastify-color-success: var(--czl-success-color) !important;
--toastify-color-warning: var(--czl-warning-color) !important;
--toastify-color-error: var(--czl-error-color) !important;
--toastify-icon-color-info: var(--czl-info-color) !important;
--toastify-icon-color-success: var(--czl-success-color) !important;
--toastify-icon-color-warning: var(--czl-warning-color) !important;
--toastify-icon-color-error: var(--czl-error-color) !important;
--toastify-toast-background: var(--czl-main) !important;
--toastify-text-color-light: var(--czl-seat) !important;
--toastify-text-color-dark: var(--czl-grayD) !important;
--toastify-text-color-info: var(--czl-info-color) !important;
--toastify-text-color-success: var(--czl-success-color) !important;
--toastify-text-color-warning: var(--czl-warning-color) !important;
--toastify-text-color-error: var(--czl-error-color) !important;
--toastify-spinner-color: var(--czl-seat) !important;
--toastify-spinner-color-empty-area: var(--czl-minor) !important;
--toastify-color-progress-light: linear-gradient(to right, var(--czl-success-color), var(--czl-primary-color-hover), var(--czl-primary-color), var(--czl-primary-color-suppl), var(--czl-chat-bg-color), var(--czl-error-color)) !important;
--toastify-color-progress-dark: var(--czl-primary-color-dark) !important;
--toastify-color-progress-info: var(--czl-info-color) !important;
--toastify-color-progress-success: var(--czl-success-color) !important;
--toastify-color-progress-warning: var(--czl-warning-color) !important;
--toastify-color-progress-error: var(--czl-error-color) !important;
/* Light Mode Colors */
--czl-primary-color: #2EA7E0;
--czl-primary-color-hover: #58c3ed;
--czl-primary-color-pressed: #1e83ba;
--czl-primary-color-suppl: #58c3ed;
--czl-chat-bg-color: #84ddfa;
--czl-success-color: #22BB33;
--czl-error-color: #FF3333;
--czl-warning-color: #FFAA00;
--czl-info-color: #3366FF;
--czl-link-color: #0645AD;
--czl-main: #f5f5f7;
--czl-routine: #DCDFE6;
--czl-minor: #C0C4CC;
--czl-seat: #666;
--czl-grayA: #515253;
--czl-grayB: #454545;
--czl-grayC: #414243;
--czl-grayD: #303030;
/* Dark Mode Colors */
--czl-primary-color-dark: #84ddfa;
--czl-primary-color-hover-dark: #b0eeff;
--czl-primary-color-pressed-dark: #58c3ed;
--czl-primary-color-suppl-dark: #b0eeff;
--czl-chat-bg-color-dark: #d9f8ff;
--czl-success-color-dark: #22BB33;
--czl-error-color-dark: #FF3333;
--czl-warning-color-dark: #FFAA00;
--czl-info-color-dark: #3366FF;
--czl-link-color-dark: #0645AD;
--czl-main-dark: #181818;
}
@font-face { @font-face {
font-family: 'CZL'; font-family: 'CZL';
src: url('https://cdn-r2.czl.net/fonts/CZL/CZL_Sans_SC_Thin.woff2') format('woff2'); src: url('https://cdn-r2.czl.net/fonts/CZL/CZL_Sans_SC_Thin.woff2') format('woff2');
@ -8,7 +70,7 @@
@font-face { @font-face {
font-family: 'CZL'; font-family: 'CZL';
src: url('https://cdn-r2.czl.net/fonts/CZL/CZL_Sans_SC_Black.woff2') format('woff2'); src: url('https://cdn-r2.czl.net/fonts/CZL/CZL_Sans_SC_gray.woff2') format('woff2');
font-weight: 900; font-weight: 900;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;

View File

@ -117,7 +117,7 @@ const EditRedemption = () => {
</Form.Field> </Form.Field>
</> </>
} }
<Button positive onClick={submit}>提交</Button> <Button positive onClick={submit} style={{ background: 'var(--czl-success-color)' }}>提交</Button>
<Button onClick={handleCancel}>取消</Button> <Button onClick={handleCancel}>取消</Button>
</Form> </Form>
</Segment> </Segment>

View File

@ -7,8 +7,31 @@ const TopUp = () => {
const [redemptionCode, setRedemptionCode] = useState(''); const [redemptionCode, setRedemptionCode] = useState('');
const [topUpLink, setTopUpLink] = useState(''); const [topUpLink, setTopUpLink] = useState('');
const [userQuota, setUserQuota] = useState(0); const [userQuota, setUserQuota] = useState(0);
const [userGroup, setUserGroup] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
// 升级用户组
const updateUserGroupIfNecessary = async (quota) => {
if (userGroup === 'vip') return; // 添加这一行
if (quota >= 5*500000) {
try {
const res = await API.post('/api/user/manage', {
username: localStorage.getItem('username'),
newGroup: 'vip'
});
const { success, message } = res.data;
if (success) {
showSuccess('已成功升级为 VIP 会员!');
} else {
showError('请右下角联系客服');
}
} catch (err) {
showError('请右下角联系客服');
}
}
};
const topUp = async () => { const topUp = async () => {
if (redemptionCode === '') { if (redemptionCode === '') {
showInfo('请输入充值码!') showInfo('请输入充值码!')
@ -23,16 +46,18 @@ const TopUp = () => {
if (success) { if (success) {
showSuccess('充值成功!'); showSuccess('充值成功!');
setUserQuota((quota) => { setUserQuota((quota) => {
return quota + data; const newQuota = quota + data;
updateUserGroupIfNecessary(newQuota);
return newQuota;
}); });
setRedemptionCode(''); setRedemptionCode('');
} else { } else {
showError(message); showError(message);
} }
} catch (err) { } catch (err) {
showError('请求失败'); showError('请右下角联系客服');
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}; };
@ -44,11 +69,12 @@ const TopUp = () => {
window.open(topUpLink, '_blank'); window.open(topUpLink, '_blank');
}; };
const getUserQuota = async ()=>{ const getUserQuota = async () => {
let res = await API.get(`/api/user/self`); let res = await API.get(`/api/user/self`);
const {success, message, data} = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
setUserQuota(data.quota); setUserQuota(data.quota);
setUserGroup(data.group); // 添加这一行
} else { } else {
showError(message); showError(message);
} }
@ -79,19 +105,21 @@ const TopUp = () => {
setRedemptionCode(e.target.value); setRedemptionCode(e.target.value);
}} }}
/> />
<Button color='green' onClick={openTopUpLink}> <Button negative style={{ backgroundColor: 'var(--czl-primary-color)' }} onClick={openTopUpLink}>
获取兑换码 获取兑换码
</Button> </Button>
<Button color='yellow' onClick={topUp} disabled={isSubmitting}> <Button negative style={{ backgroundColor: 'var(--czl-success-color)' }} onClick={topUp} disabled={isSubmitting}>
{isSubmitting ? '兑换中...' : '兑换'} {isSubmitting ? '兑换中...' : '兑换'}
</Button> </Button>
</Form> </Form>
</Grid.Column> </Grid.Column>
<Grid.Column> <Grid.Column>
<Statistic.Group widths='one'> <Statistic.Group widths='one'>
<Statistic> <Statistic>
<Statistic.Value>{renderQuota(userQuota)}</Statistic.Value> <Statistic.Value style={{ color: 'var(--czl-error-color)' }}>{renderQuota(userQuota)}</Statistic.Value>
<Statistic.Label>剩余额度</Statistic.Label> <Statistic.Label style={{ color: 'var(--czl-error-color)' }}>剩余额度</Statistic.Label>
</Statistic> </Statistic>
</Statistic.Group> </Statistic.Group>
</Grid.Column> </Grid.Column>