微信
-
GitHub
-
@@ -452,33 +522,56 @@ const PersonalSetting = () => {
Telegram
-
-
-
-
+
+
+
{systemToken && (
@@ -489,17 +582,15 @@ const PersonalSetting = () => {
style={{ marginTop: '10px' }}
/>
)}
- {
- status.wechat_login && (
-
- )
- }
+ {status.wechat_login && (
+
+ )}
setShowWeChatBindModal(false)}
// onOpen={() => setShowWeChatBindModal(true)}
@@ -513,12 +604,14 @@ const PersonalSetting = () => {
handleInputChange('wechat_verification_code', v)}
+ onChange={(v) =>
+ handleInputChange('wechat_verification_code', v)
+ }
/>
-
diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js
index 213685b..3716a00 100644
--- a/web/src/components/SystemSetting.js
+++ b/web/src/components/SystemSetting.js
@@ -1,5 +1,13 @@
import React, { useEffect, useState } from 'react';
-import { Button, Divider, Form, Grid, Header, Message, Modal } from 'semantic-ui-react';
+import {
+ Button,
+ Divider,
+ Form,
+ Grid,
+ Header,
+ Message,
+ Modal,
+} from 'semantic-ui-react';
import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers';
const SystemSetting = () => {
@@ -38,13 +46,14 @@ const SystemSetting = () => {
// telegram login
TelegramOAuthEnabled: '',
TelegramBotToken: '',
- TelegramBotName: ''
+ TelegramBotName: '',
});
const [originInputs, setOriginInputs] = useState({});
let [loading, setLoading] = useState(false);
const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
const [restrictedDomainInput, setRestrictedDomainInput] = useState('');
- const [showPasswordWarningModal, setShowPasswordWarningModal] = useState(false);
+ const [showPasswordWarningModal, setShowPasswordWarningModal] =
+ useState(false);
const getOptions = async () => {
const res = await API.get('/api/option/');
@@ -59,13 +68,15 @@ const SystemSetting = () => {
});
setInputs({
...newInputs,
- EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(',')
+ EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(','),
});
setOriginInputs(newInputs);
- setEmailDomainWhitelist(newInputs.EmailDomainWhitelist.split(',').map((item) => {
- return { key: item, text: item, value: item };
- }));
+ setEmailDomainWhitelist(
+ newInputs.EmailDomainWhitelist.split(',').map((item) => {
+ return { key: item, text: item, value: item };
+ }),
+ );
} else {
showError(message);
}
@@ -94,7 +105,7 @@ const SystemSetting = () => {
}
const res = await API.put('/api/option/', {
key,
- value
+ value,
});
const { success, message } = res.data;
if (success) {
@@ -105,7 +116,8 @@ const SystemSetting = () => {
value = parseFloat(value);
}
setInputs((inputs) => ({
- ...inputs, [key]: value
+ ...inputs,
+ [key]: value,
}));
} else {
showError(message);
@@ -197,13 +209,16 @@ const SystemSetting = () => {
}
};
-
const submitEmailDomainWhitelist = async () => {
if (
- originInputs['EmailDomainWhitelist'] !== inputs.EmailDomainWhitelist.join(',') &&
+ originInputs['EmailDomainWhitelist'] !==
+ inputs.EmailDomainWhitelist.join(',') &&
inputs.SMTPToken !== ''
) {
- await updateOption('EmailDomainWhitelist', inputs.EmailDomainWhitelist.join(','));
+ await updateOption(
+ 'EmailDomainWhitelist',
+ inputs.EmailDomainWhitelist.join(','),
+ );
}
};
@@ -211,7 +226,7 @@ const SystemSetting = () => {
if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
await updateOption(
'WeChatServerAddress',
- removeTrailingSlash(inputs.WeChatServerAddress)
+ removeTrailingSlash(inputs.WeChatServerAddress),
);
}
if (
@@ -220,7 +235,7 @@ const SystemSetting = () => {
) {
await updateOption(
'WeChatAccountQRCodeImageURL',
- inputs.WeChatAccountQRCodeImageURL
+ inputs.WeChatAccountQRCodeImageURL,
);
}
if (
@@ -263,17 +278,23 @@ const SystemSetting = () => {
const submitNewRestrictedDomain = () => {
const localDomainList = inputs.EmailDomainWhitelist;
- if (restrictedDomainInput !== '' && !localDomainList.includes(restrictedDomainInput)) {
+ if (
+ restrictedDomainInput !== '' &&
+ !localDomainList.includes(restrictedDomainInput)
+ ) {
setRestrictedDomainInput('');
setInputs({
...inputs,
- EmailDomainWhitelist: [...localDomainList, restrictedDomainInput]
+ EmailDomainWhitelist: [...localDomainList, restrictedDomainInput],
});
- setEmailDomainWhitelist([...EmailDomainWhitelist, {
- key: restrictedDomainInput,
- text: restrictedDomainInput,
- value: restrictedDomainInput
- }]);
+ setEmailDomainWhitelist([
+ ...EmailDomainWhitelist,
+ {
+ key: restrictedDomainInput,
+ text: restrictedDomainInput,
+ value: restrictedDomainInput,
+ },
+ ]);
}
};
@@ -281,13 +302,13 @@ const SystemSetting = () => {
+
+
@@ -295,81 +316,79 @@ const SystemSetting = () => {
更新服务器地址
- 支付设置(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
-
+
+ 支付设置(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
+
+
-
-
+
-
+
-
- 更新支付设置
-
+ 更新支付设置
-
+
- {
- showPasswordWarningModal &&
+ {showPasswordWarningModal && (
setShowPasswordWarningModal(false)}
@@ -378,12 +397,16 @@ const SystemSetting = () => {
>
警告
- 取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?
+
+ 取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?
+
- setShowPasswordWarningModal(false)}>取消
+ setShowPasswordWarningModal(false)}>
+ 取消
+
{
setShowPasswordWarningModal(false);
await updateOption('PasswordLoginEnabled', 'false');
@@ -393,157 +416,170 @@ const SystemSetting = () => {
- }
+ )}
-
+
配置邮箱域名白名单
- 用以防止恶意用户利用临时邮箱批量注册
+
+ 用以防止恶意用户利用临时邮箱批量注册
+
{
- submitNewRestrictedDomain();
- }}>填入
+ {
+ submitNewRestrictedDomain();
+ }}
+ >
+ 填入
+
}
onKeyDown={(e) => {
if (e.key === 'Enter') {
submitNewRestrictedDomain();
}
}}
- autoComplete="new-password"
- placeholder="输入新的允许的邮箱域名"
+ autoComplete='new-password'
+ placeholder='输入新的允许的邮箱域名'
value={restrictedDomainInput}
onChange={(e, { value }) => {
setRestrictedDomainInput(value);
}}
/>
- 保存邮箱域名白名单设置
+
+ 保存邮箱域名白名单设置
+
-
+
保存 SMTP 设置
-
+
配置 GitHub OAuth App
用以支持通过 GitHub 进行登录注册,
-
+
点击此处
管理你的 GitHub OAuth App
@@ -556,34 +592,35 @@ const SystemSetting = () => {
保存 GitHub OAuth 设置
-
+
配置 WeChat Server
用以支持通过微信进行登录注册,
点击此处
@@ -592,61 +629,65 @@ const SystemSetting = () => {
保存 WeChat Server 设置
-
+
保存 Telegram 登录设置
-
+
配置 Turnstile
用以支持用户校验,
-
+
点击此处
管理你的 Turnstile Sites,推荐选择 Invisible Widget Type
@@ -654,21 +695,21 @@ const SystemSetting = () => {
diff --git a/web/src/components/TokensTable.js b/web/src/components/TokensTable.js
index 5901bfd..4687ce9 100644
--- a/web/src/components/TokensTable.js
+++ b/web/src/components/TokensTable.js
@@ -1,9 +1,25 @@
import React, { useEffect, useState } from 'react';
-import { API, copy, showError, showSuccess, timestamp2string } from '../helpers';
+import {
+ API,
+ copy,
+ showError,
+ showSuccess,
+ timestamp2string,
+} from '../helpers';
import { ITEMS_PER_PAGE } from '../constants';
import { renderQuota } from '../helpers/render';
-import { Button, Dropdown, Form, Modal, Popconfirm, Popover, SplitButtonGroup, Table, Tag } from '@douyinfe/semi-ui';
+import {
+ Button,
+ Dropdown,
+ Form,
+ Modal,
+ Popconfirm,
+ Popover,
+ SplitButtonGroup,
+ Table,
+ Tag,
+} from '@douyinfe/semi-ui';
import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
import EditToken from '../pages/Token/EditToken';
@@ -11,85 +27,107 @@ import EditToken from '../pages/Token/EditToken';
const COPY_OPTIONS = [
{ key: 'next', text: 'ChatGPT Next Web', value: 'next' },
{ key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' },
- { key: 'opencat', text: 'OpenCat', value: 'opencat' }
+ { key: 'opencat', text: 'OpenCat', value: 'opencat' },
];
const OPEN_LINK_OPTIONS = [
{ key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' },
- { key: 'opencat', text: 'OpenCat', value: 'opencat' }
+ { key: 'opencat', text: 'OpenCat', value: 'opencat' },
];
function renderTimestamp(timestamp) {
- return (
- <>
- {timestamp2string(timestamp)}
- >
- );
+ return <>{timestamp2string(timestamp)}>;
}
function renderStatus(status, model_limits_enabled = false) {
switch (status) {
case 1:
if (model_limits_enabled) {
- return 已启用:限制模型;
+ return (
+
+ 已启用:限制模型
+
+ );
} else {
- return 已启用;
+ return (
+
+ 已启用
+
+ );
}
case 2:
- return 已禁用 ;
+ return (
+
+ {' '}
+ 已禁用{' '}
+
+ );
case 3:
- return 已过期 ;
+ return (
+
+ {' '}
+ 已过期{' '}
+
+ );
case 4:
- return 已耗尽 ;
+ return (
+
+ {' '}
+ 已耗尽{' '}
+
+ );
default:
- return 未知状态 ;
+ return (
+
+ {' '}
+ 未知状态{' '}
+
+ );
}
}
const TokensTable = () => {
-
const link_menu = [
{
- node: 'item', key: 'next', name: 'ChatGPT Next Web', onClick: () => {
+ node: 'item',
+ key: 'next',
+ name: 'ChatGPT Next Web',
+ onClick: () => {
onOpenLink('next');
- }
+ },
},
{ node: 'item', key: 'ama', name: 'AMA 问天', value: 'ama' },
{
- node: 'item', key: 'next-mj', name: 'ChatGPT Web & Midjourney', value: 'next-mj', onClick: () => {
+ node: 'item',
+ key: 'next-mj',
+ name: 'ChatGPT Web & Midjourney',
+ value: 'next-mj',
+ onClick: () => {
onOpenLink('next-mj');
- }
+ },
},
- { node: 'item', key: 'opencat', name: 'OpenCat', value: 'opencat' }
+ { node: 'item', key: 'opencat', name: 'OpenCat', value: 'opencat' },
];
const columns = [
{
title: '名称',
- dataIndex: 'name'
+ dataIndex: 'name',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (text, record, index) => {
- return (
-
- {renderStatus(text, record.model_limits_enabled)}
-
- );
- }
+ return {renderStatus(text, record.model_limits_enabled)}
;
+ },
},
{
title: '已用额度',
dataIndex: 'used_quota',
render: (text, record, index) => {
- return (
-
- {renderQuota(parseInt(text))}
-
- );
- }
+ return {renderQuota(parseInt(text))}
;
+ },
},
{
title: '剩余额度',
@@ -97,22 +135,25 @@ const TokensTable = () => {
render: (text, record, index) => {
return (
- {record.unlimited_quota ? 无限制 :
- {renderQuota(parseInt(text))}}
+ {record.unlimited_quota ? (
+
+ 无限制
+
+ ) : (
+
+ {renderQuota(parseInt(text))}
+
+ )}
);
- }
+ },
},
{
title: '创建时间',
dataIndex: 'created_time',
render: (text, record, index) => {
- return (
-
- {renderTimestamp(text)}
-
- );
- }
+ return {renderTimestamp(text)}
;
+ },
},
{
title: '过期时间',
@@ -123,7 +164,7 @@ const TokensTable = () => {
{record.expired_time === -1 ? '永不过期' : renderTimestamp(text)}
);
- }
+ },
},
{
title: '',
@@ -131,25 +172,41 @@ const TokensTable = () => {
render: (text, record, index) => (
- 查看
+
+ 查看
+
-
{
- await copyText('sk-' + record.key);
- }}
- >复制
-
- {
- onOpenLink('next', record.key);
- }}>聊天
- {
+ await copyText('sk-' + record.key);
+ }}
+ >
+ 复制
+
+
+ {
+ onOpenLink('next', record.key);
+ }}
+ >
+ 聊天
+
+ {
name: 'ChatGPT Next Web',
onClick: () => {
onOpenLink('next', record.key);
- }
+ },
},
{
node: 'item',
@@ -166,70 +223,88 @@ const TokensTable = () => {
name: 'ChatGPT Web & Midjourney',
onClick: () => {
onOpenLink('next-mj', record.key);
- }
+ },
},
{
- node: 'item', key: 'ama', name: 'AMA 问天(BotGem)', onClick: () => {
+ node: 'item',
+ key: 'ama',
+ name: 'AMA 问天(BotGem)',
+ onClick: () => {
onOpenLink('ama', record.key);
- }
+ },
},
{
- node: 'item', key: 'opencat', name: 'OpenCat', onClick: () => {
+ node: 'item',
+ key: 'opencat',
+ name: 'OpenCat',
+ onClick: () => {
onOpenLink('opencat', record.key);
- }
- }
- ]
- }
+ },
+ },
+ ]}
>
- }>
+ }
+ >
{
- manageToken(record.id, 'delete', record).then(
- () => {
- removeRecord(record.key);
- }
- );
+ manageToken(record.id, 'delete', record).then(() => {
+ removeRecord(record.key);
+ });
}}
>
- 删除
+
+ 删除
+
- {
- record.status === 1 ?
- {
- manageToken(
- record.id,
- 'disable',
- record
- );
- }
- }>禁用 :
- {
- manageToken(
- record.id,
- 'enable',
- record
- );
- }
- }>启用
- }
- {
+ {record.status === 1 ? (
+ {
+ manageToken(record.id, 'disable', record);
+ }}
+ >
+ 禁用
+
+ ) : (
+ {
+ manageToken(record.id, 'enable', record);
+ }}
+ >
+ 启用
+
+ )}
+ {
setEditingToken(record);
setShowEdit(true);
- }
- }>编辑
+ }}
+ >
+ 编辑
+
- )
- }
+ ),
+ },
];
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
@@ -245,14 +320,14 @@ const TokensTable = () => {
const [showTopUpModal, setShowTopUpModal] = useState(false);
const [targetTokenIdx, setTargetTokenIdx] = useState(0);
const [editingToken, setEditingToken] = useState({
- id: undefined
+ id: undefined,
});
const closeEdit = () => {
setShowEdit(false);
setTimeout(() => {
setEditingToken({
- id: undefined
+ id: undefined,
});
}, 500);
};
@@ -266,7 +341,10 @@ const TokensTable = () => {
}
};
- let pageData = tokens.slice((activePage - 1) * pageSize, activePage * pageSize);
+ let pageData = tokens.slice(
+ (activePage - 1) * pageSize,
+ activePage * pageSize,
+ );
const loadTokens = async (startIdx) => {
setLoading(true);
const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}`);
@@ -315,7 +393,8 @@ const TokensTable = () => {
let nextUrl;
if (nextLink) {
- nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
+ nextUrl =
+ nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
} else {
nextUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
}
@@ -323,7 +402,8 @@ const TokensTable = () => {
let url;
switch (type) {
case 'ama':
- url = mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
+ url =
+ mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
break;
case 'opencat':
url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
@@ -367,7 +447,8 @@ const TokensTable = () => {
let defaultUrl;
if (chatLink) {
- defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
+ defaultUrl =
+ chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
}
let url;
switch (type) {
@@ -378,7 +459,8 @@ const TokensTable = () => {
url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
break;
case 'next-mj':
- url = mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
+ url =
+ mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
break;
default:
if (!chatLink) {
@@ -399,10 +481,10 @@ const TokensTable = () => {
});
}, [pageSize]);
- const removeRecord = key => {
+ const removeRecord = (key) => {
let newDataSource = [...tokens];
if (key != null) {
- let idx = newDataSource.findIndex(data => data.key === key);
+ let idx = newDataSource.findIndex((data) => data.key === key);
if (idx > -1) {
newDataSource.splice(idx, 1);
@@ -435,7 +517,6 @@ const TokensTable = () => {
let newTokens = [...tokens];
// let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
if (action === 'delete') {
-
} else {
record.status = token.status;
// newTokens[realIdx].status = token.status;
@@ -455,7 +536,9 @@ const TokensTable = () => {
return;
}
setSearching(true);
- const res = await API.get(`/api/token/search?keyword=${searchKeyword}&token=${searchToken}`);
+ const res = await API.get(
+ `/api/token/search?keyword=${searchKeyword}&token=${searchToken}`,
+ );
const { success, message, data } = res.data;
if (success) {
setTokensFormat(data);
@@ -488,32 +571,28 @@ const TokensTable = () => {
setLoading(false);
};
-
- const handlePageChange = page => {
+ const handlePageChange = (page) => {
setActivePage(page);
if (page === Math.ceil(tokens.length / pageSize) + 1) {
// In this case we have to load more data and then append them.
- loadTokens(page - 1).then(r => {
- });
+ loadTokens(page - 1).then((r) => {});
}
};
const rowSelection = {
- onSelect: (record, selected) => {
- },
- onSelectAll: (selected, selectedRows) => {
- },
+ onSelect: (record, selected) => {},
+ onSelectAll: (selected, selectedRows) => {},
onChange: (selectedRowKeys, selectedRows) => {
setSelectedKeys(selectedRows);
- }
+ },
};
const handleRow = (record, index) => {
if (record.status !== 1) {
return {
style: {
- background: 'var(--semi-color-disabled-border)'
- }
+ background: 'var(--semi-color-disabled-border)',
+ },
};
} else {
return {};
@@ -522,63 +601,98 @@ const TokensTable = () => {
return (
<>
-
-
-
查询
+
+ 查询
+
-
`第 ${page.currentStart} - ${page.currentEnd} 条,共 ${tokens.length} 条`,
- onPageSizeChange: (size) => {
- setPageSize(size);
- setActivePage(1);
- },
- onPageChange: handlePageChange
- }} loading={loading} rowSelection={rowSelection} onRow={handleRow}>
-
-
{
+
+ `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${tokens.length} 条`,
+ onPageSizeChange: (size) => {
+ setPageSize(size);
+ setActivePage(1);
+ },
+ onPageChange: handlePageChange,
+ }}
+ loading={loading}
+ rowSelection={rowSelection}
+ onRow={handleRow}
+ >
+ {
setEditingToken({
- id: undefined
+ id: undefined,
});
setShowEdit(true);
- }
- }>添加令牌
- {
+ }}
+ >
+ 添加令牌
+
+ {
if (selectedKeys.length === 0) {
showError('请至少选择一个令牌!');
return;
}
let keys = '';
for (let i = 0; i < selectedKeys.length; i++) {
- keys += selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n';
+ keys +=
+ selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n';
}
await copyText(keys);
- }
- }>复制所选令牌到剪贴板
+ }}
+ >
+ 复制所选令牌到剪贴板
+
>
);
};
diff --git a/web/src/components/UsersTable.js b/web/src/components/UsersTable.js
index 7757afa..50fe7a0 100644
--- a/web/src/components/UsersTable.js
+++ b/web/src/components/UsersTable.js
@@ -1,6 +1,14 @@
import React, { useEffect, useState } from 'react';
import { API, showError, showSuccess } from '../helpers';
-import { Button, Form, Popconfirm, Space, Table, Tag, Tooltip } from '@douyinfe/semi-ui';
+import {
+ Button,
+ Form,
+ Popconfirm,
+ Space,
+ Table,
+ Tag,
+ Tooltip,
+} from '@douyinfe/semi-ui';
import { ITEMS_PER_PAGE } from '../constants';
import { renderGroup, renderNumber, renderQuota } from '../helpers/render';
import AddUser from '../pages/User/AddUser';
@@ -9,124 +17,218 @@ import EditUser from '../pages/User/EditUser';
function renderRole(role) {
switch (role) {
case 1:
- return
普通用户;
+ return
普通用户;
case 10:
- return
管理员;
+ return (
+
+ 管理员
+
+ );
case 100:
- return
超级管理员;
+ return (
+
+ 超级管理员
+
+ );
default:
- return
未知身份;
+ return (
+
+ 未知身份
+
+ );
}
}
const UsersTable = () => {
- const columns = [{
- title: 'ID', dataIndex: 'id'
- }, {
- title: '用户名', dataIndex: 'username'
- }, {
- title: '分组', dataIndex: 'group', render: (text, record, index) => {
- return (
- {renderGroup(text)}
-
);
- }
- }, {
- title: '统计信息', dataIndex: 'info', render: (text, record, index) => {
- return (
-
-
- {renderQuota(record.quota)}
-
-
- {renderQuota(record.used_quota)}
-
-
- {renderNumber(record.request_count)}
-
-
-
);
- }
- }, {
- title: '邀请信息', dataIndex: 'invite', render: (text, record, index) => {
- return (
-
-
- {renderNumber(record.aff_count)}
-
-
- {renderQuota(record.aff_history_quota)}
-
-
- {record.inviter_id === 0 ? 无 :
- {record.inviter_id}}
-
-
-
);
- }
- }, {
- title: '角色', dataIndex: 'role', render: (text, record, index) => {
- return (
- {renderRole(text)}
-
);
- }
- }, {
- title: '状态', dataIndex: 'status', render: (text, record, index) => {
- return (
- {record.DeletedAt !== null ? 已注销 : renderStatus(text)}
-
);
- }
- }, {
- title: '', dataIndex: 'operate', render: (text, record, index) => (
- {
- record.DeletedAt !== null ? <>> :
- <>
-
{
- manageUser(record.username, 'promote', record);
- }}
- >
- 提升
-
-
{
- manageUser(record.username, 'demote', record);
- }}
- >
- 降级
-
- {record.status === 1 ?
-
{
- manageUser(record.username, 'disable', record);
- }}>禁用 :
-
{
- manageUser(record.username, 'enable', record);
- }} disabled={record.status === 3}>启用}
-
{
- setEditingUser(record);
- setShowEditUser(true);
- }}>编辑
- >
-
- }
-
{
- manageUser(record.username, 'delete', record).then(() => {
- removeRecord(record.id);
- });
- }}
- >
- 删除
-
-
)
- }];
+ const columns = [
+ {
+ title: 'ID',
+ dataIndex: 'id',
+ },
+ {
+ title: '用户名',
+ dataIndex: 'username',
+ },
+ {
+ title: '分组',
+ dataIndex: 'group',
+ render: (text, record, index) => {
+ return
{renderGroup(text)}
;
+ },
+ },
+ {
+ title: '统计信息',
+ dataIndex: 'info',
+ render: (text, record, index) => {
+ return (
+
+
+
+
+ {renderQuota(record.quota)}
+
+
+
+
+ {renderQuota(record.used_quota)}
+
+
+
+
+ {renderNumber(record.request_count)}
+
+
+
+
+ );
+ },
+ },
+ {
+ title: '邀请信息',
+ dataIndex: 'invite',
+ render: (text, record, index) => {
+ return (
+
+
+
+
+ {renderNumber(record.aff_count)}
+
+
+
+
+ {renderQuota(record.aff_history_quota)}
+
+
+
+ {record.inviter_id === 0 ? (
+
+ 无
+
+ ) : (
+
+ {record.inviter_id}
+
+ )}
+
+
+
+ );
+ },
+ },
+ {
+ title: '角色',
+ dataIndex: 'role',
+ render: (text, record, index) => {
+ return
{renderRole(text)}
;
+ },
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ render: (text, record, index) => {
+ return (
+
+ {record.DeletedAt !== null ? (
+ 已注销
+ ) : (
+ renderStatus(text)
+ )}
+
+ );
+ },
+ },
+ {
+ title: '',
+ dataIndex: 'operate',
+ render: (text, record, index) => (
+
+ {record.DeletedAt !== null ? (
+ <>>
+ ) : (
+ <>
+
{
+ manageUser(record.username, 'promote', record);
+ }}
+ >
+
+ 提升
+
+
+
{
+ manageUser(record.username, 'demote', record);
+ }}
+ >
+
+ 降级
+
+
+ {record.status === 1 ? (
+
{
+ manageUser(record.username, 'disable', record);
+ }}
+ >
+ 禁用
+
+ ) : (
+
{
+ manageUser(record.username, 'enable', record);
+ }}
+ disabled={record.status === 3}
+ >
+ 启用
+
+ )}
+
{
+ setEditingUser(record);
+ setShowEditUser(true);
+ }}
+ >
+ 编辑
+
+ >
+ )}
+
{
+ manageUser(record.username, 'delete', record).then(() => {
+ removeRecord(record.id);
+ });
+ }}
+ >
+
+ 删除
+
+
+
+ ),
+ },
+ ];
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
@@ -137,22 +239,22 @@ const UsersTable = () => {
const [showAddUser, setShowAddUser] = useState(false);
const [showEditUser, setShowEditUser] = useState(false);
const [editingUser, setEditingUser] = useState({
- id: undefined
+ id: undefined,
});
const setCount = (data) => {
- if (data.length >= (activePage) * ITEMS_PER_PAGE) {
+ if (data.length >= activePage * ITEMS_PER_PAGE) {
setUserCount(data.length + 1);
} else {
setUserCount(data.length);
}
};
- const removeRecord = key => {
+ const removeRecord = (key) => {
console.log(key);
let newDataSource = [...users];
if (key != null) {
- let idx = newDataSource.findIndex(data => data.id === key);
+ let idx = newDataSource.findIndex((data) => data.id === key);
if (idx > -1) {
newDataSource.splice(idx, 1);
@@ -200,7 +302,8 @@ const UsersTable = () => {
const manageUser = async (username, action, record) => {
const res = await API.post('/api/user/manage', {
- username, action
+ username,
+ action,
});
const { success, message } = res.data;
if (success) {
@@ -208,7 +311,6 @@ const UsersTable = () => {
let user = res.data.data;
let newUsers = [...users];
if (action === 'delete') {
-
} else {
record.status = user.status;
record.role = user.role;
@@ -222,15 +324,19 @@ const UsersTable = () => {
const renderStatus = (status) => {
switch (status) {
case 1:
- return
已激活;
+ return
已激活;
case 2:
- return (
- 已封禁
- );
+ return (
+
+ 已封禁
+
+ );
default:
- return (
- 未知状态
- );
+ return (
+
+ 未知状态
+
+ );
}
};
@@ -271,16 +377,18 @@ const UsersTable = () => {
setLoading(false);
};
- const handlePageChange = page => {
+ const handlePageChange = (page) => {
setActivePage(page);
if (page === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
- loadUsers(page - 1).then(r => {
- });
+ loadUsers(page - 1).then((r) => {});
}
};
- const pageData = users.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
+ const pageData = users.slice(
+ (activePage - 1) * ITEMS_PER_PAGE,
+ activePage * ITEMS_PER_PAGE,
+ );
const closeAddUser = () => {
setShowAddUser(false);
@@ -289,7 +397,7 @@ const UsersTable = () => {
const closeEditUser = () => {
setShowEditUser(false);
setEditingUser({
- id: undefined
+ id: undefined,
});
};
@@ -303,34 +411,52 @@ const UsersTable = () => {
return (
<>
-
-
+
+
handleKeywordChange(value)}
+ onChange={(value) => handleKeywordChange(value)}
/>
-
- {
+
+ {
setShowAddUser(true);
- }
- }>添加用户
+ }}
+ >
+ 添加用户
+
>
);
};
diff --git a/web/src/components/WeChatIcon.js b/web/src/components/WeChatIcon.js
index 22210d9..d3f5742 100644
--- a/web/src/components/WeChatIcon.js
+++ b/web/src/components/WeChatIcon.js
@@ -3,15 +3,27 @@ import { Icon } from '@douyinfe/semi-ui';
const WeChatIcon = () => {
function CustomIcon() {
- return ;
+ return (
+
+ );
}
return (
diff --git a/web/src/components/utils.js b/web/src/components/utils.js
index 5363ba5..59e3a01 100644
--- a/web/src/components/utils.js
+++ b/web/src/components/utils.js
@@ -15,6 +15,6 @@ export async function onGitHubOAuthClicked(github_client_id) {
const state = await getOAuthState();
if (!state) return;
window.open(
- `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`
+ `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`,
);
-}
\ No newline at end of file
+}
diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js
index 9c4165d..8fdfd1b 100644
--- a/web/src/constants/channel.constants.js
+++ b/web/src/constants/channel.constants.js
@@ -1,22 +1,100 @@
export const CHANNEL_OPTIONS = [
- {key: 1, text: 'OpenAI', value: 1, color: 'green', label: 'OpenAI'},
- {key: 2, text: 'Midjourney Proxy', value: 2, color: 'light-blue', label: 'Midjourney Proxy'},
- {key: 5, text: 'Midjourney Proxy Plus', value: 5, color: 'blue', label: 'Midjourney Proxy Plus'},
- {key: 4, text: 'Ollama', value: 4, color: 'grey', label: 'Ollama'},
- {key: 14, text: 'Anthropic Claude', value: 14, color: 'indigo', label: 'Anthropic Claude'},
- {key: 3, text: 'Azure OpenAI', value: 3, color: 'teal', label: 'Azure OpenAI'},
- {key: 11, text: 'Google PaLM2', value: 11, color: 'orange', label: 'Google PaLM2'},
- {key: 24, text: 'Google Gemini', value: 24, color: 'orange', label: 'Google Gemini'},
- {key: 15, text: '百度文心千帆', value: 15, color: 'blue', label: '百度文心千帆'},
- {key: 17, text: '阿里通义千问', value: 17, color: 'orange', label: '阿里通义千问'},
- {key: 18, text: '讯飞星火认知', value: 18, color: 'blue', label: '讯飞星火认知'},
- {key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet', label: '智谱 ChatGLM'},
- {key: 16, text: '智谱 GLM-4V', value: 26, color: 'purple', label: '智谱 GLM-4V'},
- {key: 16, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot'},
- {key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑'},
- {key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元'},
- {key: 31, text: '零一万物', value: 31, color: 'green', label: '零一万物'},
- {key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道'},
- {key: 22, text: '知识库:FastGPT', value: 22, color: 'blue', label: '知识库:FastGPT'},
- {key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple', label: '知识库:AI Proxy'},
+ { key: 1, text: 'OpenAI', value: 1, color: 'green', label: 'OpenAI' },
+ {
+ key: 2,
+ text: 'Midjourney Proxy',
+ value: 2,
+ color: 'light-blue',
+ label: 'Midjourney Proxy',
+ },
+ {
+ key: 5,
+ text: 'Midjourney Proxy Plus',
+ value: 5,
+ color: 'blue',
+ label: 'Midjourney Proxy Plus',
+ },
+ { key: 4, text: 'Ollama', value: 4, color: 'grey', label: 'Ollama' },
+ {
+ key: 14,
+ text: 'Anthropic Claude',
+ value: 14,
+ color: 'indigo',
+ label: 'Anthropic Claude',
+ },
+ {
+ key: 3,
+ text: 'Azure OpenAI',
+ value: 3,
+ color: 'teal',
+ label: 'Azure OpenAI',
+ },
+ {
+ key: 11,
+ text: 'Google PaLM2',
+ value: 11,
+ color: 'orange',
+ label: 'Google PaLM2',
+ },
+ {
+ key: 24,
+ text: 'Google Gemini',
+ value: 24,
+ color: 'orange',
+ label: 'Google Gemini',
+ },
+ {
+ key: 15,
+ text: '百度文心千帆',
+ value: 15,
+ color: 'blue',
+ label: '百度文心千帆',
+ },
+ {
+ key: 17,
+ text: '阿里通义千问',
+ value: 17,
+ color: 'orange',
+ label: '阿里通义千问',
+ },
+ {
+ key: 18,
+ text: '讯飞星火认知',
+ value: 18,
+ color: 'blue',
+ label: '讯飞星火认知',
+ },
+ {
+ key: 16,
+ text: '智谱 ChatGLM',
+ value: 16,
+ color: 'violet',
+ label: '智谱 ChatGLM',
+ },
+ {
+ key: 16,
+ text: '智谱 GLM-4V',
+ value: 26,
+ color: 'purple',
+ label: '智谱 GLM-4V',
+ },
+ { key: 16, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot' },
+ { key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑' },
+ { key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元' },
+ { key: 31, text: '零一万物', value: 31, color: 'green', label: '零一万物' },
+ { key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道' },
+ {
+ key: 22,
+ text: '知识库:FastGPT',
+ value: 22,
+ color: 'blue',
+ label: '知识库:FastGPT',
+ },
+ {
+ key: 21,
+ text: '知识库:AI Proxy',
+ value: 21,
+ color: 'purple',
+ label: '知识库:AI Proxy',
+ },
];
diff --git a/web/src/constants/index.js b/web/src/constants/index.js
index e83152b..1321207 100644
--- a/web/src/constants/index.js
+++ b/web/src/constants/index.js
@@ -1,4 +1,4 @@
export * from './toast.constants';
export * from './user.constants';
export * from './common.constant';
-export * from './channel.constants';
\ No newline at end of file
+export * from './channel.constants';
diff --git a/web/src/constants/toast.constants.js b/web/src/constants/toast.constants.js
index 5068472..f8853df 100644
--- a/web/src/constants/toast.constants.js
+++ b/web/src/constants/toast.constants.js
@@ -3,5 +3,5 @@ export const toastConstants = {
INFO_TIMEOUT: 3000,
ERROR_TIMEOUT: 5000,
WARNING_TIMEOUT: 10000,
- NOTICE_TIMEOUT: 20000
+ NOTICE_TIMEOUT: 20000,
};
diff --git a/web/src/constants/user.constants.js b/web/src/constants/user.constants.js
index 2680d8e..cde70df 100644
--- a/web/src/constants/user.constants.js
+++ b/web/src/constants/user.constants.js
@@ -1,19 +1,19 @@
export const userConstants = {
- REGISTER_REQUEST: 'USERS_REGISTER_REQUEST',
- REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS',
- REGISTER_FAILURE: 'USERS_REGISTER_FAILURE',
+ REGISTER_REQUEST: 'USERS_REGISTER_REQUEST',
+ REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS',
+ REGISTER_FAILURE: 'USERS_REGISTER_FAILURE',
- LOGIN_REQUEST: 'USERS_LOGIN_REQUEST',
- LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS',
- LOGIN_FAILURE: 'USERS_LOGIN_FAILURE',
-
- LOGOUT: 'USERS_LOGOUT',
+ LOGIN_REQUEST: 'USERS_LOGIN_REQUEST',
+ LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS',
+ LOGIN_FAILURE: 'USERS_LOGIN_FAILURE',
- GETALL_REQUEST: 'USERS_GETALL_REQUEST',
- GETALL_SUCCESS: 'USERS_GETALL_SUCCESS',
- GETALL_FAILURE: 'USERS_GETALL_FAILURE',
+ LOGOUT: 'USERS_LOGOUT',
- DELETE_REQUEST: 'USERS_DELETE_REQUEST',
- DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
- DELETE_FAILURE: 'USERS_DELETE_FAILURE'
+ GETALL_REQUEST: 'USERS_GETALL_REQUEST',
+ GETALL_SUCCESS: 'USERS_GETALL_SUCCESS',
+ GETALL_FAILURE: 'USERS_GETALL_FAILURE',
+
+ DELETE_REQUEST: 'USERS_DELETE_REQUEST',
+ DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
+ DELETE_FAILURE: 'USERS_DELETE_FAILURE',
};
diff --git a/web/src/context/Status/index.js b/web/src/context/Status/index.js
index 71f0682..5a5319e 100644
--- a/web/src/context/Status/index.js
+++ b/web/src/context/Status/index.js
@@ -16,4 +16,4 @@ export const StatusProvider = ({ children }) => {
{children}
);
-};
\ No newline at end of file
+};
diff --git a/web/src/context/User/index.js b/web/src/context/User/index.js
index c667159..033b361 100644
--- a/web/src/context/User/index.js
+++ b/web/src/context/User/index.js
@@ -1,19 +1,19 @@
// contexts/User/index.jsx
-import React from "react"
-import { reducer, initialState } from "./reducer"
+import React from 'react';
+import { reducer, initialState } from './reducer';
export const UserContext = React.createContext({
state: initialState,
- dispatch: () => null
-})
+ dispatch: () => null,
+});
export const UserProvider = ({ children }) => {
- const [state, dispatch] = React.useReducer(reducer, initialState)
+ const [state, dispatch] = React.useReducer(reducer, initialState);
return (
-
- { children }
+
+ {children}
- )
-}
\ No newline at end of file
+ );
+};
diff --git a/web/src/context/User/reducer.js b/web/src/context/User/reducer.js
index 9ed1d80..d44cffc 100644
--- a/web/src/context/User/reducer.js
+++ b/web/src/context/User/reducer.js
@@ -3,12 +3,12 @@ export const reducer = (state, action) => {
case 'login':
return {
...state,
- user: action.payload
+ user: action.payload,
};
case 'logout':
return {
...state,
- user: undefined
+ user: undefined,
};
default:
@@ -17,5 +17,5 @@ export const reducer = (state, action) => {
};
export const initialState = {
- user: undefined
-};
\ No newline at end of file
+ user: undefined,
+};
diff --git a/web/src/helpers/api.js b/web/src/helpers/api.js
index 35fdb1e..31a8c14 100644
--- a/web/src/helpers/api.js
+++ b/web/src/helpers/api.js
@@ -2,12 +2,14 @@ import { showError } from './utils';
import axios from 'axios';
export const API = axios.create({
- baseURL: process.env.REACT_APP_SERVER ? process.env.REACT_APP_SERVER : '',
+ baseURL: import.meta.env.VITE_REACT_APP_SERVER_URL
+ ? import.meta.env.VITE_REACT_APP_SERVER_URL
+ : '',
});
API.interceptors.response.use(
(response) => response,
(error) => {
showError(error);
- }
+ },
);
diff --git a/web/src/helpers/auth-header.js b/web/src/helpers/auth-header.js
index a8fe5f5..f094dd1 100644
--- a/web/src/helpers/auth-header.js
+++ b/web/src/helpers/auth-header.js
@@ -1,10 +1,10 @@
export function authHeader() {
- // return authorization header with jwt token
- let user = JSON.parse(localStorage.getItem('user'));
+ // return authorization header with jwt token
+ let user = JSON.parse(localStorage.getItem('user'));
- if (user && user.token) {
- return { 'Authorization': 'Bearer ' + user.token };
- } else {
- return {};
- }
-}
\ No newline at end of file
+ if (user && user.token) {
+ return { Authorization: 'Bearer ' + user.token };
+ } else {
+ return {};
+ }
+}
diff --git a/web/src/helpers/history.js b/web/src/helpers/history.js
index 629039e..f529e5d 100644
--- a/web/src/helpers/history.js
+++ b/web/src/helpers/history.js
@@ -1,3 +1,3 @@
import { createBrowserHistory } from 'history';
-export const history = createBrowserHistory();
\ No newline at end of file
+export const history = createBrowserHistory();
diff --git a/web/src/helpers/index.js b/web/src/helpers/index.js
index 505a8cf..ac16496 100644
--- a/web/src/helpers/index.js
+++ b/web/src/helpers/index.js
@@ -1,4 +1,4 @@
export * from './history';
export * from './auth-header';
export * from './utils';
-export * from './api';
\ No newline at end of file
+export * from './api';
diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js
index 62fb0dc..a71215e 100644
--- a/web/src/helpers/render.js
+++ b/web/src/helpers/render.js
@@ -1,170 +1,197 @@
-import {Label} from 'semantic-ui-react';
-import {Tag} from "@douyinfe/semi-ui";
+import { Label } from 'semantic-ui-react';
+import { Tag } from '@douyinfe/semi-ui';
export function renderText(text, limit) {
- if (text.length > limit) {
- return text.slice(0, limit - 3) + '...';
- }
- return text;
+ if (text.length > limit) {
+ return text.slice(0, limit - 3) + '...';
+ }
+ return text;
}
export function renderGroup(group) {
- if (group === '') {
- return default;
- }
- let groups = group.split(',');
- groups.sort();
- return <>
- {groups.map((group) => {
- if (group === 'vip' || group === 'pro') {
- return {group};
- } else if (group === 'svip' || group === 'premium') {
- return {group};
- }
- if (group === 'default') {
- return {group};
- } else {
- return {group};
- }
- })}
- >;
+ if (group === '') {
+ return default;
+ }
+ let groups = group.split(',');
+ groups.sort();
+ return (
+ <>
+ {groups.map((group) => {
+ if (group === 'vip' || group === 'pro') {
+ return (
+
+ {group}
+
+ );
+ } else if (group === 'svip' || group === 'premium') {
+ return (
+
+ {group}
+
+ );
+ }
+ if (group === 'default') {
+ return {group};
+ } else {
+ return (
+
+ {group}
+
+ );
+ }
+ })}
+ >
+ );
}
export function renderNumber(num) {
- if (num >= 1000000000) {
- return (num / 1000000000).toFixed(1) + 'B';
- } else if (num >= 1000000) {
- return (num / 1000000).toFixed(1) + 'M';
- } else if (num >= 10000) {
- return (num / 1000).toFixed(1) + 'k';
- } else {
- return num;
- }
+ if (num >= 1000000000) {
+ return (num / 1000000000).toFixed(1) + 'B';
+ } else if (num >= 1000000) {
+ return (num / 1000000).toFixed(1) + 'M';
+ } else if (num >= 10000) {
+ return (num / 1000).toFixed(1) + 'k';
+ } else {
+ return num;
+ }
}
export function renderQuotaNumberWithDigit(num, digits = 2) {
- let displayInCurrency = localStorage.getItem('display_in_currency');
- num = num.toFixed(digits);
- if (displayInCurrency) {
- return '$' + num;
- }
- return num;
+ let displayInCurrency = localStorage.getItem('display_in_currency');
+ num = num.toFixed(digits);
+ if (displayInCurrency) {
+ return '$' + num;
+ }
+ return num;
}
export function renderNumberWithPoint(num) {
- num = num.toFixed(2);
- if (num >= 100000) {
- // Convert number to string to manipulate it
- let numStr = num.toString();
- // Find the position of the decimal point
- let decimalPointIndex = numStr.indexOf('.');
+ num = num.toFixed(2);
+ if (num >= 100000) {
+ // Convert number to string to manipulate it
+ let numStr = num.toString();
+ // Find the position of the decimal point
+ let decimalPointIndex = numStr.indexOf('.');
- let wholePart = numStr;
- let decimalPart = '';
+ let wholePart = numStr;
+ let decimalPart = '';
- // If there is a decimal point, split the number into whole and decimal parts
- if (decimalPointIndex !== -1) {
- wholePart = numStr.slice(0, decimalPointIndex);
- decimalPart = numStr.slice(decimalPointIndex);
- }
-
- // Take the first two and last two digits of the whole number part
- let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2);
-
- // Return the formatted number
- return shortenedWholePart + decimalPart;
+ // If there is a decimal point, split the number into whole and decimal parts
+ if (decimalPointIndex !== -1) {
+ wholePart = numStr.slice(0, decimalPointIndex);
+ decimalPart = numStr.slice(decimalPointIndex);
}
- // If the number is less than 100,000, return it unmodified
- return num;
+ // Take the first two and last two digits of the whole number part
+ let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2);
+
+ // Return the formatted number
+ return shortenedWholePart + decimalPart;
+ }
+
+ // If the number is less than 100,000, return it unmodified
+ return num;
}
export function getQuotaPerUnit() {
- let quotaPerUnit = localStorage.getItem('quota_per_unit');
- quotaPerUnit = parseFloat(quotaPerUnit);
- return quotaPerUnit;
+ let quotaPerUnit = localStorage.getItem('quota_per_unit');
+ quotaPerUnit = parseFloat(quotaPerUnit);
+ return quotaPerUnit;
}
export function getQuotaWithUnit(quota, digits = 6) {
- let quotaPerUnit = localStorage.getItem('quota_per_unit');
- quotaPerUnit = parseFloat(quotaPerUnit);
- return (quota / quotaPerUnit).toFixed(digits);
+ let quotaPerUnit = localStorage.getItem('quota_per_unit');
+ quotaPerUnit = parseFloat(quotaPerUnit);
+ return (quota / quotaPerUnit).toFixed(digits);
}
export function renderQuota(quota, digits = 2) {
- let quotaPerUnit = localStorage.getItem('quota_per_unit');
- let displayInCurrency = localStorage.getItem('display_in_currency');
- quotaPerUnit = parseFloat(quotaPerUnit);
- displayInCurrency = displayInCurrency === 'true';
- if (displayInCurrency) {
- return '$' + (quota / quotaPerUnit).toFixed(digits);
- }
- return renderNumber(quota);
+ let quotaPerUnit = localStorage.getItem('quota_per_unit');
+ let displayInCurrency = localStorage.getItem('display_in_currency');
+ quotaPerUnit = parseFloat(quotaPerUnit);
+ displayInCurrency = displayInCurrency === 'true';
+ if (displayInCurrency) {
+ return '$' + (quota / quotaPerUnit).toFixed(digits);
+ }
+ return renderNumber(quota);
}
export function renderQuotaWithPrompt(quota, digits) {
- let displayInCurrency = localStorage.getItem('display_in_currency');
- displayInCurrency = displayInCurrency === 'true';
- if (displayInCurrency) {
- return `(等价金额:${renderQuota(quota, digits)})`;
- }
- return '';
+ let displayInCurrency = localStorage.getItem('display_in_currency');
+ displayInCurrency = displayInCurrency === 'true';
+ if (displayInCurrency) {
+ return `(等价金额:${renderQuota(quota, digits)})`;
+ }
+ return '';
}
-const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo',
- 'light-blue', 'lime', 'orange', 'pink',
- 'purple', 'red', 'teal', 'violet', 'yellow'
-]
+const colors = [
+ 'amber',
+ 'blue',
+ 'cyan',
+ 'green',
+ 'grey',
+ 'indigo',
+ 'light-blue',
+ 'lime',
+ 'orange',
+ 'pink',
+ 'purple',
+ 'red',
+ 'teal',
+ 'violet',
+ 'yellow',
+];
export const modelColorMap = {
- 'dall-e': 'rgb(147,112,219)', // 深紫色
- 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
- 'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
- 'midjourney': 'rgb(136,43,180)', // 介于紫罗兰和洋红之间的色调
- 'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
- 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
- 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
- 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
- 'gpt-3.5-turbo-16k': 'rgb(252,200,149)', // 淡橙色
- 'gpt-3.5-turbo-16k-0613': 'rgb(255,181,119)', // 淡桃色
- 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
- 'gpt-4': 'rgb(135,206,235)', // 天蓝色
- 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
- 'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
- 'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
- 'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
- 'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
- 'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
- 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
- 'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色
- 'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝
- 'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色
- 'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝
- 'text-ada-001': 'rgb(255,192,203)', // 粉红色
- 'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色
- 'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色
- 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色
- 'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列)
- 'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色
- 'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
- 'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
- 'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
- 'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
- 'tts-1': 'rgb(255,140,0)', // 深橙色
- 'tts-1-1106': 'rgb(255,165,0)', // 橙色
- 'tts-1-hd': 'rgb(255,215,0)', // 金色
- 'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别)
- 'whisper-1': 'rgb(245,245,220)' // 米色
-}
+ 'dall-e': 'rgb(147,112,219)', // 深紫色
+ 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
+ 'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
+ midjourney: 'rgb(136,43,180)', // 介于紫罗兰和洋红之间的色调
+ 'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
+ 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
+ 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
+ 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
+ 'gpt-3.5-turbo-16k': 'rgb(252,200,149)', // 淡橙色
+ 'gpt-3.5-turbo-16k-0613': 'rgb(255,181,119)', // 淡桃色
+ 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
+ 'gpt-4': 'rgb(135,206,235)', // 天蓝色
+ 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
+ 'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
+ 'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
+ 'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
+ 'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
+ 'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
+ 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
+ 'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色
+ 'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝
+ 'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色
+ 'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝
+ 'text-ada-001': 'rgb(255,192,203)', // 粉红色
+ 'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色
+ 'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色
+ 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色
+ 'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列)
+ 'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色
+ 'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
+ 'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
+ 'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
+ 'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
+ 'tts-1': 'rgb(255,140,0)', // 深橙色
+ 'tts-1-1106': 'rgb(255,165,0)', // 橙色
+ 'tts-1-hd': 'rgb(255,215,0)', // 金色
+ 'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别)
+ 'whisper-1': 'rgb(245,245,220)', // 米色
+};
export function stringToColor(str) {
- let sum = 0;
- // 对字符串中的每个字符进行操作
- for (let i = 0; i < str.length; i++) {
- // 将字符的ASCII值加到sum中
- sum += str.charCodeAt(i);
- }
- // 使用模运算得到个位数
- let i = sum % colors.length;
- return colors[i];
-}
\ No newline at end of file
+ let sum = 0;
+ // 对字符串中的每个字符进行操作
+ for (let i = 0; i < str.length; i++) {
+ // 将字符的ASCII值加到sum中
+ sum += str.charCodeAt(i);
+ }
+ // 使用模运算得到个位数
+ let i = sum % colors.length;
+ return colors[i];
+}
diff --git a/web/src/helpers/utils.js b/web/src/helpers/utils.js
index 3e1fdb2..78fa3d6 100644
--- a/web/src/helpers/utils.js
+++ b/web/src/helpers/utils.js
@@ -1,7 +1,7 @@
import { Toast } from '@douyinfe/semi-ui';
import { toastConstants } from '../constants';
import React from 'react';
-import {toast} from "react-toastify";
+import { toast } from 'react-toastify';
const HTMLToastContent = ({ htmlContent }) => {
return ;
@@ -30,7 +30,7 @@ export function getSystemName() {
export function getLogo() {
let logo = localStorage.getItem('logo');
if (!logo) return '/logo.png';
- return logo
+ return logo;
}
export function getFooterHTML() {
@@ -157,17 +157,7 @@ export function timestamp2string(timestamp) {
second = '0' + second;
}
return (
- year +
- '-' +
- month +
- '-' +
- day +
- ' ' +
- hour +
- ':' +
- minute +
- ':' +
- second
+ year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second
);
}
@@ -186,20 +176,20 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') {
if (hour.length === 1) {
hour = '0' + hour;
}
- let str = month + '-' + day
+ let str = month + '-' + day;
if (dataExportDefaultTime === 'hour') {
- str += ' ' + hour + ":00"
+ str += ' ' + hour + ':00';
} else if (dataExportDefaultTime === 'week') {
let nextWeek = new Date(timestamp * 1000 + 6 * 24 * 60 * 60 * 1000);
let nextMonth = (nextWeek.getMonth() + 1).toString();
let nextDay = nextWeek.getDate().toString();
if (nextMonth.length === 1) {
- nextMonth = '0' + nextMonth;
+ nextMonth = '0' + nextMonth;
}
if (nextDay.length === 1) {
- nextDay = '0' + nextDay;
+ nextDay = '0' + nextDay;
}
- str += ' - ' + nextMonth + '-' + nextDay
+ str += ' - ' + nextMonth + '-' + nextDay;
}
return str;
}
@@ -225,9 +215,8 @@ export const verifyJSON = (str) => {
export function shouldShowPrompt(id) {
let prompt = localStorage.getItem(`prompt-${id}`);
return !prompt;
-
}
export function setPromptShown(id) {
localStorage.setItem(`prompt-${id}`, 'true');
-}
\ No newline at end of file
+}
diff --git a/web/src/index.css b/web/src/index.css
index 8e53624..9c77d18 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -1,105 +1,109 @@
body {
- margin: 0;
- padding-top: 55px;
- overflow-y: scroll;
- font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- scrollbar-width: none;
- color: var(--semi-color-text-0) !important;
- background-color: var( --semi-color-bg-0) !important;
- height: 100%;
+ margin: 0;
+ padding-top: 55px;
+ overflow-y: scroll;
+ font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ scrollbar-width: none;
+ color: var(--semi-color-text-0) !important;
+ background-color: var(--semi-color-bg-0) !important;
+ height: 100%;
}
#root {
- height: 100%;
+ height: 100%;
}
@media only screen and (max-width: 767px) {
- .semi-table-tbody, .semi-table-row, .semi-table-row-cell {
- display: block!important;
- width: auto!important;
- padding: 2px!important;
- }
- .semi-table-row-cell {
- border-bottom: 0!important;
- }
- .semi-table-tbody>.semi-table-row {
- border-bottom: 1px solid rgba(0,0,0,.1);
- }
- .semi-space {
- /*display: block!important;*/
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- row-gap: 3px;
- column-gap: 10px;
- }
+ .semi-table-tbody,
+ .semi-table-row,
+ .semi-table-row-cell {
+ display: block !important;
+ width: auto !important;
+ padding: 2px !important;
+ }
+ .semi-table-row-cell {
+ border-bottom: 0 !important;
+ }
+ .semi-table-tbody > .semi-table-row {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+ }
+ .semi-space {
+ /*display: block!important;*/
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ row-gap: 3px;
+ column-gap: 10px;
+ }
}
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {
- padding: 16px 14px;
+ padding: 16px 14px;
}
.channel-table {
- .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
- padding: 16px 8px;
- }
+ .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
+ padding: 16px 8px;
+ }
}
.semi-layout {
- height: 100%;
+ height: 100%;
}
.tableShow {
- display: revert;
+ display: revert;
}
.tableHiddle {
- display: none !important;
+ display: none !important;
}
body::-webkit-scrollbar {
- display: none;
+ display: none;
}
code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
}
.semi-navigation-vertical {
- /*display: flex;*/
- /*flex-direction: column;*/
+ /*display: flex;*/
+ /*flex-direction: column;*/
}
.semi-navigation-item {
- margin-bottom: 0;
+ margin-bottom: 0;
}
.semi-navigation-vertical {
- /*flex: 0 0 auto;*/
- /*display: flex;*/
- /*flex-direction: column;*/
- /*width: 100%;*/
- height: 100%;
- overflow: hidden;
+ /*flex: 0 0 auto;*/
+ /*display: flex;*/
+ /*flex-direction: column;*/
+ /*width: 100%;*/
+ height: 100%;
+ overflow: hidden;
}
.main-content {
- padding: 4px;
- height: 100%;
+ padding: 4px;
+ height: 100%;
}
.small-icon .icon {
- font-size: 1em !important;
+ font-size: 1em !important;
}
.custom-footer {
- font-size: 1.1em;
+ font-size: 1.1em;
}
@media only screen and (max-width: 600px) {
- .hide-on-mobile {
- display: none !important;
- }
+ .hide-on-mobile {
+ display: none !important;
+ }
}
diff --git a/web/src/index.js b/web/src/index.js
index 25b1d39..c73daef 100644
--- a/web/src/index.js
+++ b/web/src/index.js
@@ -1,54 +1,50 @@
-import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import React from 'react';
import ReactDOM from 'react-dom/client';
-import {BrowserRouter} from 'react-router-dom';
+import { BrowserRouter } from 'react-router-dom';
import App from './App';
import HeaderBar from './components/HeaderBar';
import Footer from './components/Footer';
-import 'semantic-ui-css/semantic.min.css';
+import 'semantic-ui-offline/semantic.min.css';
import './index.css';
-import {UserProvider} from './context/User';
-import {ToastContainer} from 'react-toastify';
+import { UserProvider } from './context/User';
+import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
-import {StatusProvider} from './context/Status';
-import {Layout} from "@douyinfe/semi-ui";
-import SiderBar from "./components/SiderBar";
+import { StatusProvider } from './context/Status';
+import { Layout } from '@douyinfe/semi-ui';
+import SiderBar from './components/SiderBar';
// initialization
-initVChartSemiTheme({
- isWatchingThemeSwitch: true,
-});
const root = ReactDOM.createRoot(document.getElementById('root'));
-const {Sider, Content, Header} = Layout;
+const { Sider, Content, Header } = Layout;
root.render(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
);
diff --git a/web/src/pages/About/index.js b/web/src/pages/About/index.js
index c9c2fbf..35cc676 100644
--- a/web/src/pages/About/index.js
+++ b/web/src/pages/About/index.js
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { API, showError } from '../../helpers';
import { marked } from 'marked';
-import {Layout} from "@douyinfe/semi-ui";
+import { Layout } from '@douyinfe/semi-ui';
const About = () => {
const [about, setAbout] = useState('');
@@ -31,37 +31,42 @@ const About = () => {
return (
<>
- {
- aboutLoaded && about === '' ? <>
+ {aboutLoaded && about === '' ? (
+ <>
关于
-
- 可在设置页面设置关于内容,支持 HTML & Markdown
-
+ 可在设置页面设置关于内容,支持 HTML & Markdown
new-api项目仓库地址:
https://github.com/Calcium-Ion/new-api
- NewAPI © 2023 CalciumIon | 基于 One API v0.5.4 © 2023 JustSong。本项目根据MIT许可证授权。
+ NewAPI © 2023 CalciumIon | 基于 One API v0.5.4 © 2023
+ JustSong。本项目根据MIT许可证授权。
- > : <>
- {
- about.startsWith('https://') ?
+ ) : (
+ <>
+ {about.startsWith('https://') ? (
+ :
- }
+ />
+ ) : (
+
+ )}
>
- }
+ )}
>
);
};
-
export default About;
diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js
index fac136d..ab0a373 100644
--- a/web/src/pages/Channel/EditChannel.js
+++ b/web/src/pages/Channel/EditChannel.js
@@ -1,631 +1,733 @@
-import React, {useEffect, useRef, useState} from 'react';
-import {useNavigate, useParams} from 'react-router-dom';
-import {API, isMobile, showError, showInfo, showSuccess, verifyJSON} from '../../helpers';
-import {CHANNEL_OPTIONS} from '../../constants';
-import Title from "@douyinfe/semi-ui/lib/es/typography/title";
-import {SideSheet, Space, Spin, Button, Input, Typography, Select, TextArea, Checkbox, Banner} from "@douyinfe/semi-ui";
+import React, { useEffect, useRef, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import {
+ API,
+ isMobile,
+ showError,
+ showInfo,
+ showSuccess,
+ verifyJSON,
+} from '../../helpers';
+import { CHANNEL_OPTIONS } from '../../constants';
+import Title from '@douyinfe/semi-ui/lib/es/typography/title';
+import {
+ SideSheet,
+ Space,
+ Spin,
+ Button,
+ Input,
+ Typography,
+ Select,
+ TextArea,
+ Checkbox,
+ Banner,
+} from '@douyinfe/semi-ui';
const MODEL_MAPPING_EXAMPLE = {
- 'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
- 'gpt-4-0314': 'gpt-4',
- 'gpt-4-32k-0314': 'gpt-4-32k'
+ 'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
+ 'gpt-4-0314': 'gpt-4',
+ 'gpt-4-32k-0314': 'gpt-4-32k',
};
function type2secretPrompt(type) {
- // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
- switch (type) {
- case 15:
- return '按照如下格式输入:APIKey|SecretKey';
- case 18:
- return '按照如下格式输入:APPID|APISecret|APIKey';
- case 22:
- return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
- case 23:
- return '按照如下格式输入:AppId|SecretId|SecretKey';
- default:
- return '请输入渠道对应的鉴权密钥';
- }
+ // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
+ switch (type) {
+ case 15:
+ return '按照如下格式输入:APIKey|SecretKey';
+ case 18:
+ return '按照如下格式输入:APPID|APISecret|APIKey';
+ case 22:
+ return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
+ case 23:
+ return '按照如下格式输入:AppId|SecretId|SecretKey';
+ default:
+ return '请输入渠道对应的鉴权密钥';
+ }
}
const EditChannel = (props) => {
- const navigate = useNavigate();
- const channelId = props.editingChannel.id;
- const isEdit = channelId !== undefined;
- const [loading, setLoading] = useState(isEdit);
- const handleCancel = () => {
- props.handleClose()
- };
- const originInputs = {
- name: '',
- type: 1,
- key: '',
- openai_organization: '',
- base_url: '',
- other: '',
- model_mapping: '',
- models: [],
- auto_ban: 1,
- groups: ['default']
- };
- const [batch, setBatch] = useState(false);
- const [autoBan, setAutoBan] = useState(true);
- // const [autoBan, setAutoBan] = useState(true);
- const [inputs, setInputs] = useState(originInputs);
- const [originModelOptions, setOriginModelOptions] = useState([]);
- const [modelOptions, setModelOptions] = useState([]);
- const [groupOptions, setGroupOptions] = useState([]);
- const [basicModels, setBasicModels] = useState([]);
- const [fullModels, setFullModels] = useState([]);
- const [customModel, setCustomModel] = useState('');
- const handleInputChange = (name, value) => {
- setInputs((inputs) => ({...inputs, [name]: value}));
- if (name === 'type' && inputs.models.length === 0) {
- let localModels = [];
- switch (value) {
- case 14:
- localModels = ["claude-instant-1.2", "claude-2", "claude-2.0", "claude-2.1", "claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307"];
- break;
- case 11:
- localModels = ['PaLM-2'];
- break;
- case 15:
- localModels = ['ERNIE-Bot', 'ERNIE-Bot-turbo', 'ERNIE-Bot-4', 'Embedding-V1'];
- break;
- case 17:
- localModels = ["qwen-turbo", "qwen-plus", "qwen-max", "qwen-max-longcontext", 'text-embedding-v1'];
- break;
- case 16:
- localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite'];
- break;
- case 18:
- localModels = ['SparkDesk', 'SparkDesk-v1.1', 'SparkDesk-v2.1', 'SparkDesk-v3.1', 'SparkDesk-v3.5'];
- break;
- case 19:
- localModels = ['360GPT_S2_V9', 'embedding-bert-512-v1', 'embedding_s1_v1', 'semantic_similarity_s1_v1'];
- break;
- case 23:
- localModels = ['hunyuan'];
- break;
- case 24:
- localModels = ['gemini-pro', 'gemini-pro-vision'];
- break;
- case 25:
- localModels = ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'];
- break;
- case 26:
- localModels = ['glm-4', 'glm-4v', 'glm-3-turbo'];
- break;
- case 31:
- localModels = ['yi-34b-chat-0205', 'yi-34b-chat-200k', 'yi-vl-plus'];
- break;
- case 2:
- localModels = ['mj_imagine', 'mj_variation', 'mj_reroll', 'mj_blend', 'mj_upscale', 'mj_describe'];
- break;
- case 5:
- localModels = [
- 'swap_face',
- 'mj_imagine',
- 'mj_variation',
- 'mj_reroll',
- 'mj_blend',
- 'mj_upscale',
- 'mj_describe',
- 'mj_zoom',
- 'mj_shorten',
- 'mj_modal',
- 'mj_inpaint',
- 'mj_custom_zoom',
- 'mj_high_variation',
- 'mj_low_variation',
- 'mj_pan',
- ];
- break;
- }
- setInputs((inputs) => ({...inputs, models: localModels}));
- }
- //setAutoBan
- };
+ const navigate = useNavigate();
+ const channelId = props.editingChannel.id;
+ const isEdit = channelId !== undefined;
+ const [loading, setLoading] = useState(isEdit);
+ const handleCancel = () => {
+ props.handleClose();
+ };
+ const originInputs = {
+ name: '',
+ type: 1,
+ key: '',
+ openai_organization: '',
+ base_url: '',
+ other: '',
+ model_mapping: '',
+ models: [],
+ auto_ban: 1,
+ groups: ['default'],
+ };
+ const [batch, setBatch] = useState(false);
+ const [autoBan, setAutoBan] = useState(true);
+ // const [autoBan, setAutoBan] = useState(true);
+ const [inputs, setInputs] = useState(originInputs);
+ const [originModelOptions, setOriginModelOptions] = useState([]);
+ const [modelOptions, setModelOptions] = useState([]);
+ const [groupOptions, setGroupOptions] = useState([]);
+ const [basicModels, setBasicModels] = useState([]);
+ const [fullModels, setFullModels] = useState([]);
+ const [customModel, setCustomModel] = useState('');
+ const handleInputChange = (name, value) => {
+ setInputs((inputs) => ({ ...inputs, [name]: value }));
+ if (name === 'type' && inputs.models.length === 0) {
+ let localModels = [];
+ switch (value) {
+ case 14:
+ localModels = [
+ 'claude-instant-1.2',
+ 'claude-2',
+ 'claude-2.0',
+ 'claude-2.1',
+ 'claude-3-opus-20240229',
+ 'claude-3-sonnet-20240229',
+ 'claude-3-haiku-20240307',
+ ];
+ break;
+ case 11:
+ localModels = ['PaLM-2'];
+ break;
+ case 15:
+ localModels = [
+ 'ERNIE-Bot',
+ 'ERNIE-Bot-turbo',
+ 'ERNIE-Bot-4',
+ 'Embedding-V1',
+ ];
+ break;
+ case 17:
+ localModels = [
+ 'qwen-turbo',
+ 'qwen-plus',
+ 'qwen-max',
+ 'qwen-max-longcontext',
+ 'text-embedding-v1',
+ ];
+ break;
+ case 16:
+ localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite'];
+ break;
+ case 18:
+ localModels = [
+ 'SparkDesk',
+ 'SparkDesk-v1.1',
+ 'SparkDesk-v2.1',
+ 'SparkDesk-v3.1',
+ 'SparkDesk-v3.5',
+ ];
+ break;
+ case 19:
+ localModels = [
+ '360GPT_S2_V9',
+ 'embedding-bert-512-v1',
+ 'embedding_s1_v1',
+ 'semantic_similarity_s1_v1',
+ ];
+ break;
+ case 23:
+ localModels = ['hunyuan'];
+ break;
+ case 24:
+ localModels = ['gemini-pro', 'gemini-pro-vision'];
+ break;
+ case 25:
+ localModels = [
+ 'moonshot-v1-8k',
+ 'moonshot-v1-32k',
+ 'moonshot-v1-128k',
+ ];
+ break;
+ case 26:
+ localModels = ['glm-4', 'glm-4v', 'glm-3-turbo'];
+ break;
+ case 31:
+ localModels = ['yi-34b-chat-0205', 'yi-34b-chat-200k', 'yi-vl-plus'];
+ break;
+ case 2:
+ localModels = [
+ 'mj_imagine',
+ 'mj_variation',
+ 'mj_reroll',
+ 'mj_blend',
+ 'mj_upscale',
+ 'mj_describe',
+ ];
+ break;
+ case 5:
+ localModels = [
+ 'swap_face',
+ 'mj_imagine',
+ 'mj_variation',
+ 'mj_reroll',
+ 'mj_blend',
+ 'mj_upscale',
+ 'mj_describe',
+ 'mj_zoom',
+ 'mj_shorten',
+ 'mj_modal',
+ 'mj_inpaint',
+ 'mj_custom_zoom',
+ 'mj_high_variation',
+ 'mj_low_variation',
+ 'mj_pan',
+ ];
+ break;
+ }
+ setInputs((inputs) => ({ ...inputs, models: localModels }));
+ }
+ //setAutoBan
+ };
+ const loadChannel = async () => {
+ setLoading(true);
+ let res = await API.get(`/api/channel/${channelId}`);
+ const { success, message, data } = res.data;
+ if (success) {
+ if (data.models === '') {
+ data.models = [];
+ } else {
+ data.models = data.models.split(',');
+ }
+ if (data.group === '') {
+ data.groups = [];
+ } else {
+ data.groups = data.group.split(',');
+ }
+ if (data.model_mapping !== '') {
+ data.model_mapping = JSON.stringify(
+ JSON.parse(data.model_mapping),
+ null,
+ 2,
+ );
+ }
+ setInputs(data);
+ if (data.auto_ban === 0) {
+ setAutoBan(false);
+ } else {
+ setAutoBan(true);
+ }
+ // console.log(data);
+ } else {
+ showError(message);
+ }
+ setLoading(false);
+ };
- const loadChannel = async () => {
- setLoading(true)
- let res = await API.get(`/api/channel/${channelId}`);
- const {success, message, data} = res.data;
- if (success) {
- if (data.models === '') {
- data.models = [];
- } else {
- data.models = data.models.split(',');
- }
- if (data.group === '') {
- data.groups = [];
- } else {
- data.groups = data.group.split(',');
- }
- if (data.model_mapping !== '') {
- data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2);
- }
- setInputs(data);
- if (data.auto_ban === 0) {
- setAutoBan(false);
- } else {
- setAutoBan(true);
- }
- // console.log(data);
- } else {
- showError(message);
- }
- setLoading(false);
- };
+ const fetchModels = async () => {
+ try {
+ let res = await API.get(`/api/channel/models`);
+ let localModelOptions = res.data.data.map((model) => ({
+ label: model.id,
+ value: model.id,
+ }));
+ setOriginModelOptions(localModelOptions);
+ setFullModels(res.data.data.map((model) => model.id));
+ setBasicModels(
+ res.data.data
+ .filter((model) => {
+ return model.id.startsWith('gpt-3') || model.id.startsWith('text-');
+ })
+ .map((model) => model.id),
+ );
+ } catch (error) {
+ showError(error.message);
+ }
+ };
- const fetchModels = async () => {
- try {
- let res = await API.get(`/api/channel/models`);
- let localModelOptions = res.data.data.map((model) => ({
- label: model.id,
- value: model.id
- }));
- setOriginModelOptions(localModelOptions);
- setFullModels(res.data.data.map((model) => model.id));
- setBasicModels(res.data.data.filter((model) => {
- return model.id.startsWith('gpt-3') || model.id.startsWith('text-');
- }).map((model) => model.id));
- } catch (error) {
- showError(error.message);
- }
- };
+ const fetchGroups = async () => {
+ try {
+ let res = await API.get(`/api/group/`);
+ setGroupOptions(
+ res.data.data.map((group) => ({
+ label: group,
+ value: group,
+ })),
+ );
+ } catch (error) {
+ showError(error.message);
+ }
+ };
- const fetchGroups = async () => {
- try {
- let res = await API.get(`/api/group/`);
- setGroupOptions(res.data.data.map((group) => ({
- label: group,
- value: group
- })));
- } catch (error) {
- showError(error.message);
- }
- };
-
- useEffect(() => {
- let localModelOptions = [...originModelOptions];
- inputs.models.forEach((model) => {
- if (!localModelOptions.find((option) => option.key === model)) {
- localModelOptions.push({
- label: model,
- value: model
- });
- }
- });
- setModelOptions(localModelOptions);
- }, [originModelOptions, inputs.models]);
-
- useEffect(() => {
- fetchModels().then();
- fetchGroups().then();
- if (isEdit) {
- loadChannel().then(
- () => {
-
- }
- );
- } else {
- setInputs(originInputs)
- }
- }, [props.editingChannel.id]);
-
-
- const submit = async () => {
- if (!isEdit && (inputs.name === '' || inputs.key === '')) {
- showInfo('请填写渠道名称和渠道密钥!');
- return;
- }
- if (inputs.models.length === 0) {
- showInfo('请至少选择一个模型!');
- return;
- }
- if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
- showInfo('模型映射必须是合法的 JSON 格式!');
- return;
- }
- let localInputs = {...inputs};
- if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
- localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
- }
- if (localInputs.type === 3 && localInputs.other === '') {
- localInputs.other = '2023-06-01-preview';
- }
- if (localInputs.type === 18 && localInputs.other === '') {
- localInputs.other = 'v2.1';
- }
- let res;
- if (!Array.isArray(localInputs.models)) {
- showError('提交失败,请勿重复提交!');
- handleCancel();
- return;
- }
- localInputs.auto_ban = autoBan ? 1 : 0;
- localInputs.models = localInputs.models.join(',');
- localInputs.group = localInputs.groups.join(',');
- if (isEdit) {
- res = await API.put(`/api/channel/`, {...localInputs, id: parseInt(channelId)});
- } else {
- res = await API.post(`/api/channel/`, localInputs);
- }
- const {success, message} = res.data;
- if (success) {
- if (isEdit) {
- showSuccess('渠道更新成功!');
- } else {
- showSuccess('渠道创建成功!');
- setInputs(originInputs);
- }
- props.refresh();
- props.handleClose();
- } else {
- showError(message);
- }
- };
-
- const addCustomModel = () => {
- if (customModel.trim() === '') return;
- if (inputs.models.includes(customModel)) return showError("该模型已存在!");
- let localModels = [...inputs.models];
- localModels.push(customModel);
- let localModelOptions = [];
+ useEffect(() => {
+ let localModelOptions = [...originModelOptions];
+ inputs.models.forEach((model) => {
+ if (!localModelOptions.find((option) => option.key === model)) {
localModelOptions.push({
- key: customModel,
- text: customModel,
- value: customModel
+ label: model,
+ value: model,
});
- setModelOptions(modelOptions => {
- return [...modelOptions, ...localModelOptions];
- });
- setCustomModel('');
- handleInputChange('models', localModels);
- };
+ }
+ });
+ setModelOptions(localModelOptions);
+ }, [originModelOptions, inputs.models]);
- return (
- <>
- {isEdit ? '更新渠道信息' : '创建新的渠道'}}
- headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
- bodyStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
- visible={props.visible}
- footer={
-
-
- 提交
- 取消
-
-
+ useEffect(() => {
+ fetchModels().then();
+ fetchGroups().then();
+ if (isEdit) {
+ loadChannel().then(() => {});
+ } else {
+ setInputs(originInputs);
+ }
+ }, [props.editingChannel.id]);
+
+ const submit = async () => {
+ if (!isEdit && (inputs.name === '' || inputs.key === '')) {
+ showInfo('请填写渠道名称和渠道密钥!');
+ return;
+ }
+ if (inputs.models.length === 0) {
+ showInfo('请至少选择一个模型!');
+ return;
+ }
+ if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
+ showInfo('模型映射必须是合法的 JSON 格式!');
+ return;
+ }
+ let localInputs = { ...inputs };
+ if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
+ localInputs.base_url = localInputs.base_url.slice(
+ 0,
+ localInputs.base_url.length - 1,
+ );
+ }
+ if (localInputs.type === 3 && localInputs.other === '') {
+ localInputs.other = '2023-06-01-preview';
+ }
+ if (localInputs.type === 18 && localInputs.other === '') {
+ localInputs.other = 'v2.1';
+ }
+ let res;
+ if (!Array.isArray(localInputs.models)) {
+ showError('提交失败,请勿重复提交!');
+ handleCancel();
+ return;
+ }
+ localInputs.auto_ban = autoBan ? 1 : 0;
+ localInputs.models = localInputs.models.join(',');
+ localInputs.group = localInputs.groups.join(',');
+ if (isEdit) {
+ res = await API.put(`/api/channel/`, {
+ ...localInputs,
+ id: parseInt(channelId),
+ });
+ } else {
+ res = await API.post(`/api/channel/`, localInputs);
+ }
+ const { success, message } = res.data;
+ if (success) {
+ if (isEdit) {
+ showSuccess('渠道更新成功!');
+ } else {
+ showSuccess('渠道创建成功!');
+ setInputs(originInputs);
+ }
+ props.refresh();
+ props.handleClose();
+ } else {
+ showError(message);
+ }
+ };
+
+ const addCustomModel = () => {
+ if (customModel.trim() === '') return;
+ if (inputs.models.includes(customModel)) return showError('该模型已存在!');
+ let localModels = [...inputs.models];
+ localModels.push(customModel);
+ let localModelOptions = [];
+ localModelOptions.push({
+ key: customModel,
+ text: customModel,
+ value: customModel,
+ });
+ setModelOptions((modelOptions) => {
+ return [...modelOptions, ...localModelOptions];
+ });
+ setCustomModel('');
+ handleInputChange('models', localModels);
+ };
+
+ return (
+ <>
+ {isEdit ? '更新渠道信息' : '创建新的渠道'}
+ }
+ headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
+ bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
+ visible={props.visible}
+ footer={
+
+
+
+ 提交
+
+
+ 取消
+
+
+
+ }
+ closeIcon={null}
+ onCancel={() => handleCancel()}
+ width={isMobile() ? '100%' : 600}
+ >
+
+
+ 类型:
+
+
- >
- );
+ {!isEdit && (
+
+
+ setBatch(!batch)}
+ />
+ 批量创建
+
+
+ )}
+ {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && (
+ <>
+
+ 代理:
+
+ {
+ handleInputChange('base_url', value);
+ }}
+ value={inputs.base_url}
+ autoComplete='new-password'
+ />
+ >
+ )}
+ {inputs.type === 22 && (
+ <>
+
+ 私有部署地址:
+
+ {
+ handleInputChange('base_url', value);
+ }}
+ value={inputs.base_url}
+ autoComplete='new-password'
+ />
+ >
+ )}
+
+
+ >
+ );
};
export default EditChannel;
diff --git a/web/src/pages/Channel/index.js b/web/src/pages/Channel/index.js
index 402a9b1..1b8b77b 100644
--- a/web/src/pages/Channel/index.js
+++ b/web/src/pages/Channel/index.js
@@ -1,18 +1,18 @@
import React from 'react';
import ChannelsTable from '../../components/ChannelsTable';
-import {Layout} from "@douyinfe/semi-ui";
+import { Layout } from '@douyinfe/semi-ui';
const File = () => (
- <>
-
-
- 管理渠道
-
-
-
-
-
- >
+ <>
+
+
+ 管理渠道
+
+
+
+
+
+ >
);
export default File;
diff --git a/web/src/pages/Chat/index.js b/web/src/pages/Chat/index.js
index dbc4920..86f2fd6 100644
--- a/web/src/pages/Chat/index.js
+++ b/web/src/pages/Chat/index.js
@@ -11,5 +11,4 @@ const Chat = () => {
);
};
-
export default Chat;
diff --git a/web/src/pages/Detail/index.js b/web/src/pages/Detail/index.js
index d2373cb..ccc07c1 100644
--- a/web/src/pages/Detail/index.js
+++ b/web/src/pages/Detail/index.js
@@ -1,359 +1,423 @@
-import React, {useEffect, useRef, useState} from 'react';
-import {Button, Col, Form, Layout, Row, Spin} from "@douyinfe/semi-ui";
+import React, { useEffect, useRef, useState } from 'react';
+import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
+
+import { Button, Col, Form, Layout, Row, Spin } from '@douyinfe/semi-ui';
import VChart from '@visactor/vchart';
-import {API, isAdmin, showError, timestamp2string, timestamp2string1} from "../../helpers";
import {
- getQuotaWithUnit, modelColorMap,
- renderNumber,
- renderQuota,
- renderQuotaNumberWithDigit,
- stringToColor
-} from "../../helpers/render";
+ API,
+ isAdmin,
+ showError,
+ timestamp2string,
+ timestamp2string1,
+} from '../../helpers';
+import {
+ getQuotaWithUnit,
+ modelColorMap,
+ renderNumber,
+ renderQuota,
+ renderQuotaNumberWithDigit,
+ stringToColor,
+} from '../../helpers/render';
const Detail = (props) => {
- const formRef = useRef();
- let now = new Date();
- const [inputs, setInputs] = useState({
- username: '',
- token_name: '',
- model_name: '',
- start_timestamp: localStorage.getItem('data_export_default_time') === 'hour' ? timestamp2string(now.getTime() / 1000 - 86400) : (localStorage.getItem('data_export_default_time') === 'week' ? timestamp2string(now.getTime() / 1000 - 86400 * 30) : timestamp2string(now.getTime() / 1000 - 86400 * 7)),
- end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
- channel: '',
- data_export_default_time: ''
- });
- const {username, model_name, start_timestamp, end_timestamp, channel} = inputs;
- const isAdminUser = isAdmin();
- const initialized = useRef(false)
- const [modelDataChart, setModelDataChart] = useState(null);
- const [modelDataPieChart, setModelDataPieChart] = useState(null);
- const [loading, setLoading] = useState(false);
- const [quotaData, setQuotaData] = useState([]);
- const [consumeQuota, setConsumeQuota] = useState(0);
- const [times, setTimes] = useState(0);
- const [dataExportDefaultTime, setDataExportDefaultTime] = useState(localStorage.getItem('data_export_default_time') || 'hour');
+ const formRef = useRef();
+ let now = new Date();
+ const [inputs, setInputs] = useState({
+ username: '',
+ token_name: '',
+ model_name: '',
+ start_timestamp:
+ localStorage.getItem('data_export_default_time') === 'hour'
+ ? timestamp2string(now.getTime() / 1000 - 86400)
+ : localStorage.getItem('data_export_default_time') === 'week'
+ ? timestamp2string(now.getTime() / 1000 - 86400 * 30)
+ : timestamp2string(now.getTime() / 1000 - 86400 * 7),
+ end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
+ channel: '',
+ data_export_default_time: '',
+ });
+ const { username, model_name, start_timestamp, end_timestamp, channel } =
+ inputs;
+ const isAdminUser = isAdmin();
+ const initialized = useRef(false);
+ const [modelDataChart, setModelDataChart] = useState(null);
+ const [modelDataPieChart, setModelDataPieChart] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [quotaData, setQuotaData] = useState([]);
+ const [consumeQuota, setConsumeQuota] = useState(0);
+ const [times, setTimes] = useState(0);
+ const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
+ localStorage.getItem('data_export_default_time') || 'hour',
+ );
- const handleInputChange = (value, name) => {
- if (name === 'data_export_default_time') {
- setDataExportDefaultTime(value);
- return
- }
- setInputs((inputs) => ({...inputs, [name]: value}));
- };
-
- const spec_line = {
- type: 'bar',
- data: [
- {
- id: 'barData',
- values: []
- }
- ],
- xField: 'Time',
- yField: 'Usage',
- seriesField: 'Model',
- stack: true,
- legends: {
- visible: true
- },
- title: {
- visible: true,
- text: '模型消耗分布',
- subtext: '0'
- },
- bar: {
- // The state style of bar
- state: {
- hover: {
- stroke: '#000',
- lineWidth: 1
- }
- }
- },
- tooltip: {
- mark: {
- content: [
- {
- key: datum => datum['Model'],
- value: datum => renderQuotaNumberWithDigit(parseFloat(datum['Usage']), 4)
- }
- ]
- },
- dimension: {
- content: [
- {
- key: datum => datum['Model'],
- value: datum => datum['Usage']
- }
- ],
- updateContent: array => {
- // sort by value
- array.sort((a, b) => b.value - a.value);
- // add $
- let sum = 0;
- for (let i = 0; i < array.length; i++) {
- sum += parseFloat(array[i].value);
- array[i].value = renderQuotaNumberWithDigit(parseFloat(array[i].value), 4);
- }
- // add to first
- array.unshift({
- key: '总计',
- value: renderQuotaNumberWithDigit(sum, 4)
- });
- return array;
- }
- }
- },
- color: {
- specified: modelColorMap
- }
- };
-
- const spec_pie = {
- type: 'pie',
- data: [
- {
- id: 'id0',
- values: [
- {type: 'null', value: '0'},
- ]
- }
- ],
- outerRadius: 0.8,
- innerRadius: 0.5,
- padAngle: 0.6,
- valueField: 'value',
- categoryField: 'type',
- pie: {
- style: {
- cornerRadius: 10
- },
- state: {
- hover: {
- outerRadius: 0.85,
- stroke: '#000',
- lineWidth: 1
- },
- selected: {
- outerRadius: 0.85,
- stroke: '#000',
- lineWidth: 1
- }
- }
- },
- title: {
- visible: true,
- text: '模型调用次数占比'
- },
- legends: {
- visible: true,
- orient: 'left'
- },
- label: {
- visible: true
- },
- tooltip: {
- mark: {
- content: [
- {
- key: datum => datum['type'],
- value: datum => renderNumber(datum['value'])
- }
- ]
- }
- },
- color: {
- specified: modelColorMap
- }
- };
-
- const loadQuotaData = async (lineChart, pieChart) => {
- setLoading(true);
-
- let url = '';
- let localStartTimestamp = Date.parse(start_timestamp) / 1000;
- let localEndTimestamp = Date.parse(end_timestamp) / 1000;
- if (isAdminUser) {
- url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
- } else {
- url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
- }
- const res = await API.get(url);
- const {success, message, data} = res.data;
- if (success) {
- setQuotaData(data);
- if (data.length === 0) {
- data.push({
- 'count': 0,
- 'model_name': '无数据',
- 'quota': 0,
- 'created_at': now.getTime() / 1000
- })
- }
- // 根据dataExportDefaultTime重制时间粒度
- let timeGranularity = 3600;
- if (dataExportDefaultTime === 'day') {
- timeGranularity = 86400;
- } else if (dataExportDefaultTime === 'week') {
- timeGranularity = 604800;
- }
- data.forEach(item => {
- item['created_at'] = Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
- });
- updateChart(lineChart, pieChart, data);
- } else {
- showError(message);
- }
- setLoading(false);
- };
-
- const refresh = async () => {
- await loadQuotaData(modelDataChart, modelDataPieChart);
- };
-
- const initChart = async () => {
- let lineChart = modelDataChart
- if (!modelDataChart) {
- lineChart = new VChart(spec_line, {dom: 'model_data'});
- setModelDataChart(lineChart);
- lineChart.renderAsync();
- }
- let pieChart = modelDataPieChart
- if (!modelDataPieChart) {
- pieChart = new VChart(spec_pie, {dom: 'model_pie'});
- setModelDataPieChart(pieChart);
- pieChart.renderAsync();
- }
- console.log('init vchart');
- await loadQuotaData(lineChart, pieChart)
+ const handleInputChange = (value, name) => {
+ if (name === 'data_export_default_time') {
+ setDataExportDefaultTime(value);
+ return;
}
+ setInputs((inputs) => ({ ...inputs, [name]: value }));
+ };
- const updateChart = (lineChart, pieChart, data) => {
- if (isAdminUser) {
- // 将所有用户合并
- }
- let pieData = [];
- let lineData = [];
- let consumeQuota = 0;
- let times = 0;
- for (let i = 0; i < data.length; i++) {
- const item = data[i];
- consumeQuota += item.quota;
- times += item.count;
- // 合并model_name
- let pieItem = pieData.find(it => it.type === item.model_name);
- if (pieItem) {
- pieItem.value += item.count;
- } else {
- pieData.push({
- "type": item.model_name,
- "value": item.count
- });
- }
- // 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
- // 转换日期格式
- let createTime = timestamp2string1(item.created_at, dataExportDefaultTime);
- let lineItem = lineData.find(it => it.Time === createTime && it.Model === item.model_name);
- if (lineItem) {
- lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
- } else {
- lineData.push({
- "Time": createTime,
- "Model": item.model_name,
- "Usage": parseFloat(getQuotaWithUnit(item.quota))
- });
- }
- }
- setConsumeQuota(consumeQuota);
- setTimes(times);
+ const spec_line = {
+ type: 'bar',
+ data: [
+ {
+ id: 'barData',
+ values: [],
+ },
+ ],
+ xField: 'Time',
+ yField: 'Usage',
+ seriesField: 'Model',
+ stack: true,
+ legends: {
+ visible: true,
+ },
+ title: {
+ visible: true,
+ text: '模型消耗分布',
+ subtext: '0',
+ },
+ bar: {
+ // The state style of bar
+ state: {
+ hover: {
+ stroke: '#000',
+ lineWidth: 1,
+ },
+ },
+ },
+ tooltip: {
+ mark: {
+ content: [
+ {
+ key: (datum) => datum['Model'],
+ value: (datum) =>
+ renderQuotaNumberWithDigit(parseFloat(datum['Usage']), 4),
+ },
+ ],
+ },
+ dimension: {
+ content: [
+ {
+ key: (datum) => datum['Model'],
+ value: (datum) => datum['Usage'],
+ },
+ ],
+ updateContent: (array) => {
+ // sort by value
+ array.sort((a, b) => b.value - a.value);
+ // add $
+ let sum = 0;
+ for (let i = 0; i < array.length; i++) {
+ sum += parseFloat(array[i].value);
+ array[i].value = renderQuotaNumberWithDigit(
+ parseFloat(array[i].value),
+ 4,
+ );
+ }
+ // add to first
+ array.unshift({
+ key: '总计',
+ value: renderQuotaNumberWithDigit(sum, 4),
+ });
+ return array;
+ },
+ },
+ },
+ color: {
+ specified: modelColorMap,
+ },
+ };
- // sort by count
- pieData.sort((a, b) => b.value - a.value);
- spec_pie.title.subtext = `总计:${renderNumber(times)}`;
- spec_pie.data[0].values = pieData;
+ const spec_pie = {
+ type: 'pie',
+ data: [
+ {
+ id: 'id0',
+ values: [{ type: 'null', value: '0' }],
+ },
+ ],
+ outerRadius: 0.8,
+ innerRadius: 0.5,
+ padAngle: 0.6,
+ valueField: 'value',
+ categoryField: 'type',
+ pie: {
+ style: {
+ cornerRadius: 10,
+ },
+ state: {
+ hover: {
+ outerRadius: 0.85,
+ stroke: '#000',
+ lineWidth: 1,
+ },
+ selected: {
+ outerRadius: 0.85,
+ stroke: '#000',
+ lineWidth: 1,
+ },
+ },
+ },
+ title: {
+ visible: true,
+ text: '模型调用次数占比',
+ },
+ legends: {
+ visible: true,
+ orient: 'left',
+ },
+ label: {
+ visible: true,
+ },
+ tooltip: {
+ mark: {
+ content: [
+ {
+ key: (datum) => datum['type'],
+ value: (datum) => renderNumber(datum['value']),
+ },
+ ],
+ },
+ },
+ color: {
+ specified: modelColorMap,
+ },
+ };
- spec_line.title.subtext = `总计:${renderQuota(consumeQuota, 2)}`;
- spec_line.data[0].values = lineData;
- pieChart.updateSpec(spec_pie);
- lineChart.updateSpec(spec_line);
+ const loadQuotaData = async (lineChart, pieChart) => {
+ setLoading(true);
- // pieChart.updateData('id0', pieData);
- // lineChart.updateData('barData', lineData);
- pieChart.reLayout();
- lineChart.reLayout();
+ let url = '';
+ let localStartTimestamp = Date.parse(start_timestamp) / 1000;
+ let localEndTimestamp = Date.parse(end_timestamp) / 1000;
+ if (isAdminUser) {
+ url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
+ } else {
+ url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
}
+ const res = await API.get(url);
+ const { success, message, data } = res.data;
+ if (success) {
+ setQuotaData(data);
+ if (data.length === 0) {
+ data.push({
+ count: 0,
+ model_name: '无数据',
+ quota: 0,
+ created_at: now.getTime() / 1000,
+ });
+ }
+ // 根据dataExportDefaultTime重制时间粒度
+ let timeGranularity = 3600;
+ if (dataExportDefaultTime === 'day') {
+ timeGranularity = 86400;
+ } else if (dataExportDefaultTime === 'week') {
+ timeGranularity = 604800;
+ }
+ data.forEach((item) => {
+ item['created_at'] =
+ Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
+ });
+ updateChart(lineChart, pieChart, data);
+ } else {
+ showError(message);
+ }
+ setLoading(false);
+ };
- useEffect(() => {
- // setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
- // if (dataExportDefaultTime === 'day') {
- // // 设置开始时间为7天前
- // let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
- // inputs.start_timestamp = st;
- // formRef.current.formApi.setValue('start_timestamp', st);
- // }
- if (!initialized.current) {
- initialized.current = true;
- initChart();
- }
- }, []);
+ const refresh = async () => {
+ await loadQuotaData(modelDataChart, modelDataPieChart);
+ };
- return (
- <>
-
-
- 数据看板
-
-
- handleInputChange(value, 'start_timestamp')}/>
- handleInputChange(value, 'end_timestamp')}/>
- handleInputChange(value, 'data_export_default_time')}>
-
- {
- isAdminUser && <>
- handleInputChange(value, 'username')}/>
- >
- }
-
- 查询
-
- >
-
-
-
-
-
-
-
- >
- );
+ const initChart = async () => {
+ let lineChart = modelDataChart;
+ if (!modelDataChart) {
+ lineChart = new VChart(spec_line, { dom: 'model_data' });
+ setModelDataChart(lineChart);
+ lineChart.renderAsync();
+ }
+ let pieChart = modelDataPieChart;
+ if (!modelDataPieChart) {
+ pieChart = new VChart(spec_pie, { dom: 'model_pie' });
+ setModelDataPieChart(pieChart);
+ pieChart.renderAsync();
+ }
+ console.log('init vchart');
+ await loadQuotaData(lineChart, pieChart);
+ };
+
+ const updateChart = (lineChart, pieChart, data) => {
+ if (isAdminUser) {
+ // 将所有用户合并
+ }
+ let pieData = [];
+ let lineData = [];
+ let consumeQuota = 0;
+ let times = 0;
+ for (let i = 0; i < data.length; i++) {
+ const item = data[i];
+ consumeQuota += item.quota;
+ times += item.count;
+ // 合并model_name
+ let pieItem = pieData.find((it) => it.type === item.model_name);
+ if (pieItem) {
+ pieItem.value += item.count;
+ } else {
+ pieData.push({
+ type: item.model_name,
+ value: item.count,
+ });
+ }
+ // 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
+ // 转换日期格式
+ let createTime = timestamp2string1(
+ item.created_at,
+ dataExportDefaultTime,
+ );
+ let lineItem = lineData.find(
+ (it) => it.Time === createTime && it.Model === item.model_name,
+ );
+ if (lineItem) {
+ lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
+ } else {
+ lineData.push({
+ Time: createTime,
+ Model: item.model_name,
+ Usage: parseFloat(getQuotaWithUnit(item.quota)),
+ });
+ }
+ }
+ setConsumeQuota(consumeQuota);
+ setTimes(times);
+
+ // sort by count
+ pieData.sort((a, b) => b.value - a.value);
+ spec_pie.title.subtext = `总计:${renderNumber(times)}`;
+ spec_pie.data[0].values = pieData;
+
+ spec_line.title.subtext = `总计:${renderQuota(consumeQuota, 2)}`;
+ spec_line.data[0].values = lineData;
+ pieChart.updateSpec(spec_pie);
+ lineChart.updateSpec(spec_line);
+
+ // pieChart.updateData('id0', pieData);
+ // lineChart.updateData('barData', lineData);
+ pieChart.reLayout();
+ lineChart.reLayout();
+ };
+
+ useEffect(() => {
+ // setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
+ // if (dataExportDefaultTime === 'day') {
+ // // 设置开始时间为7天前
+ // let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
+ // inputs.start_timestamp = st;
+ // formRef.current.formApi.setValue('start_timestamp', st);
+ // }
+ if (!initialized.current) {
+ initVChartSemiTheme({
+ isWatchingThemeSwitch: true,
+ });
+ initialized.current = true;
+ initChart();
+ }
+ }, []);
+
+ return (
+ <>
+
+
+ 数据看板
+
+
+
+ handleInputChange(value, 'start_timestamp')
+ }
+ />
+ handleInputChange(value, 'end_timestamp')}
+ />
+
+ handleInputChange(value, 'data_export_default_time')
+ }
+ >
+ {isAdminUser && (
+ <>
+ handleInputChange(value, 'username')}
+ />
+ >
+ )}
+
+
+ 查询
+
+
+ >
+
+
+
+
+
+
+
+ >
+ );
};
-
export default Detail;
diff --git a/web/src/pages/Home/index.js b/web/src/pages/Home/index.js
index f3e2980..49ffc14 100644
--- a/web/src/pages/Home/index.js
+++ b/web/src/pages/Home/index.js
@@ -53,78 +53,115 @@ const Home = () => {
}, []);
return (
<>
- {
- homePageContentLoaded && homePageContent === '' ?
- <>
-
-
-
- 系统信息总览}>
- 名称:{statusState?.status?.system_name}
- 版本:{statusState?.status?.version ? statusState?.status?.version : 'unknown'}
-
- 源码:
-
- https://github.com/songquanpeng/one-api
-
-
- 启动时间:{getStartTimeString()}
-
-
-
- 系统配置总览}>
-
- 邮箱验证:
- {statusState?.status?.email_verification === true ? '已启用' : '未启用'}
-
-
- GitHub 身份验证:
- {statusState?.status?.github_oauth === true ? '已启用' : '未启用'}
-
-
- 微信身份验证:
- {statusState?.status?.wechat_login === true ? '已启用' : '未启用'}
-
-
- Turnstile 用户校验:
- {statusState?.status?.turnstile_check === true ? '已启用' : '未启用'}
-
-
- Telegram 身份验证:
- {statusState?.status?.telegram_oauth === true
- ? '已启用' : '未启用'}
-
-
-
-
-
-
- >
- : <>
- {
- homePageContent.startsWith('https://') ?
- :
-
- }
- >
- }
-
+ {homePageContentLoaded && homePageContent === '' ? (
+ <>
+
+
+
+
+ 系统信息总览
+
+ }
+ >
+ 名称:{statusState?.status?.system_name}
+
+ 版本:
+ {statusState?.status?.version
+ ? statusState?.status?.version
+ : 'unknown'}
+
+
+ 源码:
+
+ https://github.com/songquanpeng/one-api
+
+
+ 启动时间:{getStartTimeString()}
+
+
+
+
+ 系统配置总览
+
+ }
+ >
+
+ 邮箱验证:
+ {statusState?.status?.email_verification === true
+ ? '已启用'
+ : '未启用'}
+
+
+ GitHub 身份验证:
+ {statusState?.status?.github_oauth === true
+ ? '已启用'
+ : '未启用'}
+
+
+ 微信身份验证:
+ {statusState?.status?.wechat_login === true
+ ? '已启用'
+ : '未启用'}
+
+
+ Turnstile 用户校验:
+ {statusState?.status?.turnstile_check === true
+ ? '已启用'
+ : '未启用'}
+
+
+ Telegram 身份验证:
+ {statusState?.status?.telegram_oauth === true
+ ? '已启用'
+ : '未启用'}
+
+
+
+
+
+ >
+ ) : (
+ <>
+ {homePageContent.startsWith('https://') ? (
+
+ ) : (
+
+ )}
+ >
+ )}
>
);
};
-export default Home;
\ No newline at end of file
+export default Home;
diff --git a/web/src/pages/Redemption/EditRedemption.js b/web/src/pages/Redemption/EditRedemption.js
index be62fb2..2711a17 100644
--- a/web/src/pages/Redemption/EditRedemption.js
+++ b/web/src/pages/Redemption/EditRedemption.js
@@ -1,8 +1,23 @@
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
-import { API, downloadTextAsFile, isMobile, showError, showSuccess } from '../../helpers';
+import {
+ API,
+ downloadTextAsFile,
+ isMobile,
+ showError,
+ showSuccess,
+} from '../../helpers';
import { renderQuotaWithPrompt } from '../../helpers/render';
-import { AutoComplete, Button, Input, Modal, SideSheet, Space, Spin, Typography } from '@douyinfe/semi-ui';
+import {
+ AutoComplete,
+ Button,
+ Input,
+ Modal,
+ SideSheet,
+ Space,
+ Spin,
+ Typography,
+} from '@douyinfe/semi-ui';
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import { Divider } from 'semantic-ui-react';
@@ -15,7 +30,7 @@ const EditRedemption = (props) => {
const originInputs = {
name: '',
quota: 100000,
- count: 1
+ count: 1,
};
const [inputs, setInputs] = useState(originInputs);
const { name, quota, count } = inputs;
@@ -42,11 +57,9 @@ const EditRedemption = (props) => {
useEffect(() => {
if (isEdit) {
- loadRedemption().then(
- () => {
- // console.log(inputs);
- }
- );
+ loadRedemption().then(() => {
+ // console.log(inputs);
+ });
} else {
setInputs(originInputs);
}
@@ -60,10 +73,13 @@ const EditRedemption = (props) => {
localInputs.quota = parseInt(localInputs.quota);
let res;
if (isEdit) {
- res = await API.put(`/api/redemption/`, { ...localInputs, id: parseInt(props.editingRedemption.id) });
+ res = await API.put(`/api/redemption/`, {
+ ...localInputs,
+ id: parseInt(props.editingRedemption.id),
+ });
} else {
res = await API.post(`/api/redemption/`, {
- ...localInputs
+ ...localInputs,
});
}
const { success, message, data } = res.data;
@@ -97,7 +113,7 @@ const EditRedemption = (props) => {
),
onOk: () => {
downloadTextAsFile(text, `${inputs.name}.txt`);
- }
+ },
});
}
setLoading(false);
@@ -107,15 +123,28 @@ const EditRedemption = (props) => {
<>
{isEdit ? '更新兑换码信息' : '创建新的兑换码'}}
+ title={
+
+ {isEdit ? '更新兑换码信息' : '创建新的兑换码'}
+
+ }
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
visible={props.visiable}
footer={
- 提交
- 取消
+
+ 提交
+
+
+ 取消
+
}
@@ -126,12 +155,12 @@ const EditRedemption = (props) => {
handleInputChange('name', value)}
+ onChange={(value) => handleInputChange('name', value)}
value={name}
- autoComplete="new-password"
+ autoComplete='new-password'
required={!isEdit}
/>
@@ -140,12 +169,12 @@ const EditRedemption = (props) => {