From 05ee4d597765554f918c9dc192bbee65fcae70d8 Mon Sep 17 00:00:00 2001 From: wood Date: Thu, 16 Nov 2023 03:21:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=80=E4=BA=9B=E6=8C=89=E9=92=AE=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/relay-text.go | 2 +- web/src/components/ChannelsTable.js | 26 ++--- web/src/components/Header.js | 6 +- web/src/components/LogsTable.js | 2 + web/src/components/PersonalSetting.js | 149 ++++++++++++++++++++++--- web/src/components/RedemptionsTable.js | 127 +++++++++++---------- web/src/components/TokensTable.js | 63 +++++++---- web/src/components/UsersTable.js | 74 ++++++++---- web/src/index.css | 2 + web/src/pages/TopUp/index.js | 26 +++-- 10 files changed, 328 insertions(+), 149 deletions(-) diff --git a/controller/relay-text.go b/controller/relay-text.go index 62c0c41a..e5c67126 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -442,7 +442,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { logContent = fmt.Sprintf("单价: $%.6g/1k tokens", inputPrice) } else { outputPrice := inputPrice * completionRatio - logContent = fmt.Sprintf("输入: $%.6g/1k tokens, 输出: $%.6g/1k tokens", inputPrice, outputPrice) + logContent = fmt.Sprintf("输入:$%.6g/1k tokens, 输出:$%.6g/1k tokens", inputPrice, outputPrice) } model.RecordConsumeLog(ctx, userId, channelId, promptTokens, completionTokens, textRequest.Model, tokenName, quota, logContent) model.UpdateUserUsedQuotaAndRequestCount(userId, quota) diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index b07edd90..dc146345 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -442,20 +442,18 @@ const ChannelsTable = () => {
+ /> + - 删除 - + + + /> + + />
+ ); })} diff --git a/web/src/components/Header.js b/web/src/components/Header.js index 053cf5a8..d2607583 100644 --- a/web/src/components/Header.js +++ b/web/src/components/Header.js @@ -43,7 +43,7 @@ let headerButtons = [ { name: '用户', to: '/user', - icon: 'user', + icon: 'users', color: 'var(--czl-primary-color)', admin: true }, @@ -54,9 +54,9 @@ let headerButtons = [ color: 'var(--czl-primary-color)' }, { - name: '设置', + name: '个人', to: '/setting', - icon: 'setting', + icon: 'user', color: 'var(--czl-primary-color)' }, { diff --git a/web/src/components/LogsTable.js b/web/src/components/LogsTable.js index a79e13e5..9ce8ca09 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/LogsTable.js @@ -476,6 +476,8 @@ const LogsTable = () => { Math.ceil(logs.length / ITEMS_PER_PAGE) + (logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0) } + firstItem={null} // 不显示第一页按钮 + lastItem={null} // 不显示最后一页按钮 /> diff --git a/web/src/components/PersonalSetting.js b/web/src/components/PersonalSetting.js index 0ade235d..cfbf0eae 100644 --- a/web/src/components/PersonalSetting.js +++ b/web/src/components/PersonalSetting.js @@ -32,6 +32,12 @@ const PersonalSetting = () => { const [usedQuota, setUsedQuota] = useState(null); const [requestCount, setRequestCount] = useState(null); const [githubID, setGithubID] = useState(null); + const [username, setUsername] = useState(null); + const [display_name, setDisplay_name] = useState(null); + const [email, setEmail] = useState(null); + + const [modelsByOwner, setModelsByOwner] = useState({}); + const [key, setKey] = useState(""); @@ -52,7 +58,10 @@ const PersonalSetting = () => { (async () => { const res = await API.get(`/api/user/self`); if (res.data.success) { + setDisplay_name(res.data.data.display_name); + setUsername(res.data.data.username); setUserGroup(res.data.data.group); + setEmail(res.data.data.email); setQuota(res.data.data.quota); setUsedQuota(res.data.data.used_quota); setRequestCount(res.data.data.request_count); @@ -64,6 +73,66 @@ const PersonalSetting = () => { }, []); const quotaPerUnit = parseInt(localStorage.getItem("quota_per_unit")); + // useEffect(() => { + // // 获取用户的第一个key + // const fetchFirstKey = async () => { + // try { + // const tokenRes = await API.get('/api/token/?p=0'); + // if (tokenRes.data.success && tokenRes.data.data.length > 0) { + // const firstKey = tokenRes.data.data[0].key; + // setKey(firstKey); + // fetchModels(firstKey); + // } else { + // // 如果没有获取到key,显示提示消息 + // showError('请先创建一个key'); + // } + // } catch (error) { + // showError('获取key失败'); + // } + // }; + + // // 获取模型信息 + // const fetchModels = async (key) => { + // try { + // const modelsRes = await API.get('/v1/models', { + // headers: { Authorization: `Bearer sk-${key}` }, + // }); + // if (modelsRes.data && modelsRes.data.data) { + // const models = modelsRes.data.data; + // const groupedByOwner = models.reduce((acc, model) => { + // const owner = model.owned_by.toUpperCase(); + // if (!acc[owner]) { + // acc[owner] = []; + // } + // acc[owner].push(model.id); + // return acc; + // }, {}); + + // // 对owners进行排序 + // const sortedOwners = Object.keys(groupedByOwner).sort(); + // const sortedGroupedByOwner = {}; + // sortedOwners.forEach(owner => { + // // 对每个owner的models进行排序 + // sortedGroupedByOwner[owner] = groupedByOwner[owner].sort(); + // }); + // setModelsByOwner(sortedGroupedByOwner); + // } + // } catch (error) { + // showError('获取模型失败'); + // } + // }; + + // fetchFirstKey(); + // }, []); + + // // 定义一个固定宽度的label样式 + // const fixedWidthLabelStyle = { + // display: 'inline-block', + // minWidth: '150px', // 根据需要调整宽度 + // textAlign: 'center', + // margin: '5px', + // }; + const transformUserGroup = (group) => { switch (group) { @@ -207,25 +276,69 @@ const PersonalSetting = () => { return (
使用信息
- {userGroup && ( - +
+ {display_name && ( + + )} + {username && ( + + )} + + +

+ {userGroup && ( + + )} + {quota !== null && quotaPerUnit && ( + + )} + {usedQuota !== null && quotaPerUnit && ( + + )} + {requestCount !== null && ( + + )} +
+ + + + {/*
模型支持度
+ {Object.keys(modelsByOwner).length > 0 ? ( + Object.entries(modelsByOwner).map(([owner, models], index) => ( +
+
{owner}
+
+ {models.map((modelId, index) => ( + + ))} +
+
+ )) + ) : ( + + 尚未绑定模型 +

请先创建一个key

+
)} - {quota !== null && quotaPerUnit && ( - - )} - {usedQuota !== null && quotaPerUnit && ( - - )} - {requestCount !== null && } - - + */}
通用设置
{/* 注意,此处生成的Key用于系统管理,而非用于请求 OpenAI 相关的服务,请知悉。 diff --git a/web/src/components/RedemptionsTable.js b/web/src/components/RedemptionsTable.js index ee6be19c..1cce8a1c 100644 --- a/web/src/components/RedemptionsTable.js +++ b/web/src/components/RedemptionsTable.js @@ -17,13 +17,13 @@ function renderTimestamp(timestamp) { function renderStatus(status) { switch (status) { case 1: - return ; + return ; case 2: - return ; + return ; case 3: - return ; + return ; default: - return ; + return ; } } @@ -232,66 +232,67 @@ const RedemptionsTable = () => { {redemption.redeemed_time ? renderTimestamp(redemption.redeemed_time) : "尚未兑换"}
- - - 删除 - - } - on='click' - flowing - hoverable -> - - - - - + + +
+ ); })} @@ -313,6 +314,8 @@ const RedemptionsTable = () => { Math.ceil(redemptions.length / ITEMS_PER_PAGE) + (redemptions.length % ITEMS_PER_PAGE === 0 ? 1 : 0) } + firstItem={null} // 不显示第一页按钮 + lastItem={null} // 不显示最后一页按钮 /> diff --git a/web/src/components/TokensTable.js b/web/src/components/TokensTable.js index 9afd6c2e..0ec94484 100644 --- a/web/src/components/TokensTable.js +++ b/web/src/components/TokensTable.js @@ -1,5 +1,7 @@ import React, { useEffect, useState } from 'react'; import { Button, Dropdown, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react'; +// import { Icon } from 'semantic-ui-react'; + import { Link } from 'react-router-dom'; import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers'; @@ -7,6 +9,7 @@ import { ITEMS_PER_PAGE } from '../constants'; import { renderQuota } from '../helpers/render'; + function renderTimestamp(timestamp) { return ( <> @@ -234,6 +237,14 @@ const TokensTable = () => { setLoading(false); }; + // 对key脱敏 + function renderKey(key) { + // 使用固定数量的星号(例如8个) + const fixedNumberOfAsterisks = '********'; + return `sk-${key.substring(0, 4)}${fixedNumberOfAsterisks}${key.substring(key.length - 4)}`; + } + + return ( <>
@@ -259,6 +270,14 @@ const TokensTable = () => { > 名称 + { + sortToken('key'); + }} + > + Key + { @@ -314,6 +333,7 @@ const TokensTable = () => { return ( {token.name ? token.name : '无'} + {renderKey(token.key)} {renderStatus(token.status)} {renderQuota(token.used_quota)} {token.unlimited_quota ? '无限制' : renderQuota(token.remain_quota, 2)} @@ -323,20 +343,16 @@ const TokensTable = () => {
- {' '} + /> - 删除 - + + /> + /> + + />
+
); })} @@ -382,7 +401,7 @@ const TokensTable = () => { - + @@ -399,12 +418,16 @@ const TokensTable = () => { activePage={activePage} onPageChange={onPaginationChange} size='small' - siblingRange={1} + siblingRange={0} // 不显示邻近页码 totalPages={ Math.ceil(tokens.length / ITEMS_PER_PAGE) + (tokens.length % ITEMS_PER_PAGE === 0 ? 1 : 0) } + ellipsisItem={null} // 不显示省略号 + firstItem={null} // 不显示第一页按钮 + lastItem={null} // 不显示最后一页按钮 /> + diff --git a/web/src/components/UsersTable.js b/web/src/components/UsersTable.js index c4007fb7..60fca53a 100644 --- a/web/src/components/UsersTable.js +++ b/web/src/components/UsersTable.js @@ -61,20 +61,41 @@ const UsersTable = () => { }); }, []); - const manageUser = (username, idx, newGroup) => { + const manageUser = (username, action, idx, value = null) => { (async () => { - const res = await API.post('/api/user/manage', { + let dataToSend = { username, - newGroup - }); + action + }; + + // 如果是修改用户组,需要在请求体中包含新的组别信息 + if (action === 'changeGroup' && value !== null) { + dataToSend.newGroup = value; + } + + const res = await API.post('/api/user/manage', dataToSend); + const { success, message, data } = res.data; - const { success, message } = res.data; if (success) { showSuccess('操作成功完成!'); - let user = res.data.data; let newUsers = [...users]; let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; - newUsers[realIdx].group = user.group; // 用新的 user.group 更新用户分组 + + switch (action) { + case 'delete': + newUsers[realIdx].deleted = true; + break; + case 'disable': + case 'enable': + newUsers[realIdx].status = data.status; // 假设API返回了新的状态 + break; + case 'changeGroup': + newUsers[realIdx].group = data.group; // 假设API返回了新的用户组信息 + break; + default: + console.error('未知的操作类型'); + } + setUsers(newUsers); } else { showError(message); @@ -83,6 +104,7 @@ const UsersTable = () => { }; + const groupOptions = [ { key: 'default', value: 'default', text: '默认', color: 'var(--czl-grayA)' }, { key: 'vip', value: 'vip', text: 'VIP', color: 'var(--czl-success-color)' }, @@ -257,13 +279,13 @@ const UsersTable = () => { {renderStatus(user.status)}
- + + + /> + /> + /> +
+ ); })} @@ -343,6 +369,8 @@ const UsersTable = () => { Math.ceil(users.length / ITEMS_PER_PAGE) + (users.length % ITEMS_PER_PAGE === 0 ? 1 : 0) } + firstItem={null} // 不显示第一页按钮 + lastItem={null} // 不显示最后一页按钮 />
diff --git a/web/src/index.css b/web/src/index.css index c7020d59..d299cc08 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -217,4 +217,6 @@ code { /* padding: 0 !important; */ } } + + diff --git a/web/src/pages/TopUp/index.js b/web/src/pages/TopUp/index.js index 53c6f3cd..2b5f5684 100644 --- a/web/src/pages/TopUp/index.js +++ b/web/src/pages/TopUp/index.js @@ -19,7 +19,7 @@ const TopUp = () => { const res = await API.post('/api/user/topup', { key: redemptionCode }); - const { success, message, data ,upgradedToVIP } = res.data; + const { success, message, data, upgradedToVIP } = res.data; if (success) { if (upgradedToVIP) { // 如果用户成功升级为 VIP showSuccess('充值成功,升级为 VIP 会员'); @@ -85,13 +85,23 @@ const TopUp = () => { setRedemptionCode(e.target.value); }} /> - - - +