[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">
<strong>中文</strong> | <a href="./README.en.md">English</a> | <a href="./README.ja.md">日本語</a>
</p>

View File

@ -7,7 +7,6 @@ import (
"one-api/common"
"one-api/model"
"strconv"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
@ -554,10 +553,128 @@ func CreateUser(c *gin.Context) {
type ManageRequest struct {
Username string `json:"username"`
Action string `json:"action"`
NewGroup string `json:"newGroup"`
}
// 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) {
var req ManageRequest
err := json.NewDecoder(c.Request.Body).Decode(&req)
@ -569,10 +686,11 @@ func ManageUser(c *gin.Context) {
})
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{
@ -581,6 +699,7 @@ func ManageUser(c *gin.Context) {
})
return
}
myRole := c.GetInt("role")
if myRole <= user.Role && myRole != common.RoleRootUser {
c.JSON(http.StatusOK, gin.H{
@ -589,66 +708,9 @@ func ManageUser(c *gin.Context) {
})
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
}
// 更新用户分组
user.Group = req.NewGroup
if err := user.Update(false); err != nil {
c.JSON(http.StatusOK, gin.H{
@ -657,10 +719,13 @@ func ManageUser(c *gin.Context) {
})
return
}
clearUser := model.User{
Group: user.Group,
Role: user.Role,
Status: user.Status,
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@ -669,6 +734,7 @@ func ManageUser(c *gin.Context) {
return
}
func EmailBind(c *gin.Context) {
email := c.Query("email")
code := c.Query("code")

View File

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

View File

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

View File

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

View File

@ -29,15 +29,15 @@ const LOG_OPTIONS = [
function renderType(type) {
switch (type) {
case 1:
return <Label basic color='green'> 充值 </Label>;
return <Label basic style={{ color: 'var(--czl-success-color)' }}> 充值 </Label>;
case 2:
return <Label basic color='olive'> 消费 </Label>;
return <Label basic style={{ color: 'var(--czl-primary-color)' }}> 消费 </Label>;
case 3:
return <Label basic color='orange'> 管理 </Label>;
return <Label basic style={{ color: 'var(--czl-warning-color)' }}> 管理 </Label>;
case 4:
return <Label basic color='purple'> 系统 </Label>;
return <Label basic style={{ color: 'var(--czl-primary-color-suppl-dark)' }}> 系统 </Label>;
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 }));
};
// 快速选时间
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 () => {
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
@ -146,7 +219,7 @@ const LogsTable = () => {
useEffect(() => {
refresh().then();
}, [logType,username, token_name, model_name, channel]);
}, [logType, username, token_name, model_name, channel, start_timestamp, end_timestamp]);
const searchLogs = async () => {
if (searchKeyword === '') {
@ -198,7 +271,7 @@ const LogsTable = () => {
<Segment>
<Header as='h3'>
使用明细消耗额度
{renderQuota(stat.quota)}
<span style={{ color: 'var(--czl-primary-color)' }}>{renderQuota(stat.quota)}</span>
</Header>
<Form>
@ -215,20 +288,36 @@ const LogsTable = () => {
name='end_timestamp'
onChange={handleInputChange} />
<Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button>
</Form.Group>
{
isAdminUser && <>
<Form.Group>
<Form.Group>
{isAdminUser && (
<>
<Form.Input fluid label={'渠道 ID'} width={3} value={channel}
placeholder='可选值' name='channel'
onChange={handleInputChange} />
<Form.Input fluid label={'用户名称'} width={3} value={username}
placeholder={'可选值'} name='username'
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>
<Table basic compact size='small'>
<Table.Header>

View File

@ -13,11 +13,6 @@ const OtherSetting = () => {
HomePageContent: ''
});
let [loading, setLoading] = useState(false);
const [showUpdateModal, setShowUpdateModal] = useState(false);
const [updateData, setUpdateData] = useState({
tag_name: '',
content: ''
});
const getOptions = async () => {
const res = await API.get('/api/option/');
@ -82,33 +77,12 @@ const OtherSetting = () => {
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 (
<Grid columns={1}>
<Grid.Column>
<Form loading={loading}>
<Header as='h3'>通用设置</Header>
<Form.Button onClick={checkUpdate}>检查更新</Form.Button>
<Form.Group widths='equal'>
<Form.TextArea
label='公告'
@ -178,28 +152,6 @@ const OtherSetting = () => {
<Form.Button onClick={submitFooter}>设置页脚</Form.Button>
</Form>
</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>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@ -18,15 +18,16 @@ function renderTimestamp(timestamp) {
function renderStatus(status) {
switch (status) {
case 1:
return <Label basic color='green'>已启用</Label>;
return <Label basic style={{ color: 'var(--czl-success-color)' }}>已启用</Label>;
case 2:
return <Label basic color='red'> 已禁用 </Label>;
return <Label basic style={{ color: 'var(--czl-error-color)' }}> 已禁用 </Label>;
case 3:
return <Label basic color='yellow'> 已过期 </Label>;
return <Label basic style={{ color: 'var(--czl-warning-color)' }}> 已过期 </Label>;
case 4:
return <Label basic color='grey'> 已耗尽 </Label>;
return <Label basic style={{ color: 'var(--czl-grayB)' }}> 已耗尽 </Label>;
default:
return <Label basic color='black'> 未知状态 </Label>;
return <Label basic style={{ color: 'var(--czl-grayD)' }}> 未知状态 </Label>;
}
}
@ -314,19 +315,20 @@ const TokensTable = () => {
<Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
<Table.Cell>
<div>
<Button
size={'small'}
positive
onClick={async () => {
await onCopy('', token.key);
}}
>
复制
</Button>
<Button
size={'small'}
positive
onClick={async () => {
await onCopy('', token.key);
}}
style={{ backgroundColor: 'var(--czl-success-color)', borderColor: 'var(--czl-success-color)' }}
>
复制
</Button>
{' '}
<Popup
trigger={
<Button size='small' negative>
<Button size='small' negative style={{ backgroundColor: 'var(--czl-error-color)', borderColor: 'var(--czl-error-color)' }}>
删除
</Button>
}
@ -339,6 +341,7 @@ const TokensTable = () => {
onClick={() => {
manageToken(token.id, 'delete', idx);
}}
style={{ backgroundColor: 'var(--czl-error-color)', borderColor: 'var(--czl-error-color)' }}
>
删除令牌 {token.name}
</Button>
@ -356,9 +359,11 @@ const TokensTable = () => {
{token.status === 1 ? '禁用' : '启用'}
</Button>
<Button
negative
size={'small'}
as={Link}
to={'/token/edit/' + token.id}
style={{ backgroundColor: 'var(--czl-primary-color)', borderColor: 'var(--czl-primary-color)' }}
>
编辑
</Button>
@ -372,10 +377,17 @@ const TokensTable = () => {
<Table.Footer>
<Table.Row>
<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 size='small' onClick={refresh} loading={loading}>刷新</Button>
<Pagination
floated='right'
activePage={activePage}

View File

@ -1,5 +1,5 @@
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 { API, showError, showSuccess } from '../helpers';
@ -11,11 +11,11 @@ function renderRole(role) {
case 1:
return <Label>普通用户</Label>;
case 10:
return <Label color='yellow'>管理员</Label>;
return <Label color='var(--czl-warning-color)'>管理员</Label>;
case 100:
return <Label color='orange'>超级管理员</Label>;
return <Label color='var(--czl-error-color)'>超级管理员</Label>;
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 () => {
const res = await API.post('/api/user/manage', {
username,
action
newGroup
});
const { success, message } = res.data;
if (success) {
showSuccess('操作成功完成!');
let user = res.data.data;
let newUsers = [...users];
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
if (action === 'delete') {
newUsers[realIdx].deleted = true;
} else {
newUsers[realIdx].status = user.status;
newUsers[realIdx].role = user.role;
}
newUsers[realIdx].group = user.group; // 用新的 user.group 更新用户分组
setUsers(newUsers);
} else {
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) => {
switch (status) {
case 1:
@ -175,14 +185,14 @@ const UsersTable = () => {
>
用户名
</Table.HeaderCell>
<Table.HeaderCell
{/* <Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortUser('group');
}}
>
分组
</Table.HeaderCell>
</Table.HeaderCell> */}
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
@ -231,10 +241,7 @@ const UsersTable = () => {
hoverable
/>
</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>{renderGroup(user.group)}</Table.Cell> */}
<Table.Cell>
<Popup content='剩余额度' trigger={<Label basic>{renderQuota(user.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>
<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
trigger={
<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-family: 'CZL';
src: url('https://cdn-r2.czl.net/fonts/CZL/CZL_Sans_SC_Thin.woff2') format('woff2');
@ -8,7 +70,7 @@
@font-face {
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-style: normal;
font-display: swap;

View File

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

View File

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