mirror of
https://github.com/linux-do/new-api.git
synced 2025-11-12 17:13:43 +08:00
merge upstream
Signed-off-by: wozulong <>
This commit is contained in:
405
web/src/App.js
405
web/src/App.js
@@ -21,11 +21,11 @@ import Redemption from './pages/Redemption';
|
||||
import TopUp from './pages/TopUp';
|
||||
import Log from './pages/Log';
|
||||
import Chat from './pages/Chat';
|
||||
import Chat2Link from './pages/Chat2Link';
|
||||
import { Layout } from '@douyinfe/semi-ui';
|
||||
import Midjourney from './pages/Midjourney';
|
||||
import Pricing from './pages/Pricing/index.js';
|
||||
import Task from './pages/Task/index.js';
|
||||
// import Detail from './pages/Detail';
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
const Detail = lazy(() => import('./pages/Detail'));
|
||||
@@ -59,215 +59,224 @@ function App() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
<Routes>
|
||||
<Route
|
||||
path='/'
|
||||
element={
|
||||
<>
|
||||
<Routes>
|
||||
<Route
|
||||
path='/'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Home />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/channel'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Channel />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/channel/edit/:id'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditChannel />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/channel/add'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditChannel />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/token'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Token />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/redemption'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Redemption />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<User />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user/edit/:id'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditUser />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user/edit'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditUser />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user/reset'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<PasswordResetConfirm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/login'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<LoginForm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/register'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<RegisterForm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/reset'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<PasswordResetForm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/oauth/github'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<GitHubOAuth />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/oauth/linuxdo'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<LinuxDoOAuth />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/setting'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Home />
|
||||
<Setting />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/channel'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Channel />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/channel/edit/:id'
|
||||
element={
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/topup'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditChannel />
|
||||
<TopUp />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/channel/add'
|
||||
element={
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/log'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Log />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/detail'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditChannel />
|
||||
<Detail />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/token'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Token />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/redemption'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Redemption />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<User />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user/edit/:id'
|
||||
element={
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/midjourney'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditUser />
|
||||
<Midjourney />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user/edit'
|
||||
element={
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/task'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditUser />
|
||||
<Task />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user/reset'
|
||||
element={
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/pricing'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Pricing />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/about'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<About />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/chat'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Chat />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
{/* 方便使用chat2link直接跳转聊天... */}
|
||||
<Route
|
||||
path='/chat2link'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<PasswordResetConfirm />
|
||||
<Chat2Link />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/login'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<LoginForm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/register'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<RegisterForm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/reset'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<PasswordResetForm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/oauth/github'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<GitHubOAuth />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/oauth/linuxdo'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<LinuxDoOAuth />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/setting'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Setting />
|
||||
</Suspense>
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/topup'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<TopUp />
|
||||
</Suspense>
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/log'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Log />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/detail'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Detail />
|
||||
</Suspense>
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/midjourney'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Midjourney />
|
||||
</Suspense>
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/task'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Task />
|
||||
</Suspense>
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/pricing'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Pricing />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/about'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<About />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/chat'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Chat />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route path='*' element={<NotFound />} />
|
||||
</Routes>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route path='*' element={<NotFound />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { getFooterHTML, getSystemName } from '../helpers';
|
||||
import { Layout, Tooltip } from '@douyinfe/semi-ui';
|
||||
|
||||
const Footer = () => {
|
||||
const FooterBar = () => {
|
||||
const systemName = getSystemName();
|
||||
const [footer, setFooter] = useState(getFooterHTML());
|
||||
let remainCheckTimes = 5;
|
||||
@@ -52,19 +52,17 @@ const Footer = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Layout.Content style={{ textAlign: 'center' }}>
|
||||
{footer ? (
|
||||
<div
|
||||
className='custom-footer'
|
||||
dangerouslySetInnerHTML={{ __html: footer }}
|
||||
></div>
|
||||
) : (
|
||||
defaultFooter
|
||||
)}
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
{footer ? (
|
||||
<div
|
||||
className='custom-footer'
|
||||
dangerouslySetInnerHTML={{ __html: footer }}
|
||||
></div>
|
||||
) : (
|
||||
defaultFooter
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
export default FooterBar;
|
||||
|
||||
@@ -3,14 +3,23 @@ import { Link, useNavigate } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
import { useSetTheme, useTheme } from '../context/Theme';
|
||||
|
||||
import { API, getLogo, getSystemName, showSuccess } from '../helpers';
|
||||
import { API, getLogo, getSystemName, isMobile, showSuccess } from '../helpers';
|
||||
import '../index.css';
|
||||
|
||||
import fireworks from 'react-fireworks';
|
||||
|
||||
import { IconHelpCircle, IconKey, IconUser } from '@douyinfe/semi-icons';
|
||||
import {
|
||||
IconHelpCircle,
|
||||
IconHome,
|
||||
IconHomeStroked,
|
||||
IconKey,
|
||||
IconNoteMoneyStroked,
|
||||
IconPriceTag,
|
||||
IconUser,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
|
||||
import { stringToColor } from '../helpers/render';
|
||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
|
||||
// HeaderBar Buttons
|
||||
let headerButtons = [
|
||||
@@ -22,6 +31,21 @@ let headerButtons = [
|
||||
},
|
||||
];
|
||||
|
||||
let buttons = [
|
||||
{
|
||||
text: '首页',
|
||||
itemKey: 'home',
|
||||
to: '/',
|
||||
// icon: <IconHomeStroked />,
|
||||
},
|
||||
// {
|
||||
// text: '模型价格',
|
||||
// itemKey: 'pricing',
|
||||
// to: '/pricing',
|
||||
// icon: <IconNoteMoneyStroked />,
|
||||
// },
|
||||
];
|
||||
|
||||
if (localStorage.getItem('chat_link')) {
|
||||
headerButtons.splice(1, 0, {
|
||||
name: '聊天',
|
||||
@@ -90,6 +114,7 @@ const HeaderBar = () => {
|
||||
about: '/about',
|
||||
login: '/login',
|
||||
register: '/register',
|
||||
home: '/',
|
||||
};
|
||||
return (
|
||||
<Link
|
||||
@@ -103,6 +128,23 @@ const HeaderBar = () => {
|
||||
selectedKeys={[]}
|
||||
// items={headerButtons}
|
||||
onSelect={(key) => {}}
|
||||
header={
|
||||
isMobile()
|
||||
? {
|
||||
logo: (
|
||||
<img
|
||||
src={logo}
|
||||
alt='logo'
|
||||
style={{ marginRight: '0.75em' }}
|
||||
/>
|
||||
),
|
||||
}
|
||||
: {
|
||||
logo: <img src={logo} alt='logo' />,
|
||||
text: systemName,
|
||||
}
|
||||
}
|
||||
items={buttons}
|
||||
footer={
|
||||
<>
|
||||
{isNewYear && (
|
||||
@@ -121,15 +163,19 @@ const HeaderBar = () => {
|
||||
</Dropdown>
|
||||
)}
|
||||
<Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
|
||||
<Switch
|
||||
checkedText='🌞'
|
||||
size={'large'}
|
||||
checked={theme === 'dark'}
|
||||
uncheckedText='🌙'
|
||||
onChange={(checked) => {
|
||||
setTheme(checked);
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
{!isMobile() && (
|
||||
<Switch
|
||||
checkedText='🌞'
|
||||
size={'large'}
|
||||
checked={theme === 'dark'}
|
||||
uncheckedText='🌙'
|
||||
onChange={(checked) => {
|
||||
setTheme(checked);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{userState.user ? (
|
||||
<>
|
||||
<Dropdown
|
||||
@@ -155,7 +201,7 @@ const HeaderBar = () => {
|
||||
<Nav.Item
|
||||
itemKey={'login'}
|
||||
text={'登录'}
|
||||
icon={<IconKey />}
|
||||
// icon={<IconKey />}
|
||||
/>
|
||||
<Nav.Item
|
||||
itemKey={'register'}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
|
||||
import { API, copy, showError, showSuccess } from '../helpers';
|
||||
import { API, copy, showError, showInfo, showSuccess } from '../helpers';
|
||||
|
||||
import {
|
||||
Banner,
|
||||
@@ -83,6 +83,7 @@ const ModelPricing = () => {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||
const [modalImageUrl, setModalImageUrl] = useState('');
|
||||
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
||||
const [selectedGroup, setSelectedGroup] = useState('default');
|
||||
|
||||
const rowSelection = useMemo(
|
||||
() => ({
|
||||
@@ -116,7 +117,8 @@ const ModelPricing = () => {
|
||||
title: '可用性',
|
||||
dataIndex: 'available',
|
||||
render: (text, record, index) => {
|
||||
return renderAvailable(text);
|
||||
// if record.enable_groups contains selectedGroup, then available is true
|
||||
return renderAvailable(record.enable_groups.includes(selectedGroup));
|
||||
},
|
||||
sorter: (a, b) => a.available - b.available,
|
||||
},
|
||||
@@ -162,6 +164,44 @@ const ModelPricing = () => {
|
||||
},
|
||||
sorter: (a, b) => a.quota_type - b.quota_type,
|
||||
},
|
||||
{
|
||||
title: '可用分组',
|
||||
dataIndex: 'enable_groups',
|
||||
render: (text, record, index) => {
|
||||
// enable_groups is a string array
|
||||
return (
|
||||
<Space>
|
||||
{text.map((group) => {
|
||||
if (group === selectedGroup) {
|
||||
return (
|
||||
<Tag color='blue' size='large' prefixIcon={<IconVerify />}>
|
||||
{group}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag
|
||||
color='blue'
|
||||
size='large'
|
||||
onClick={() => {
|
||||
setSelectedGroup(group);
|
||||
showInfo(
|
||||
'当前查看的分组为:' +
|
||||
group +
|
||||
',倍率为:' +
|
||||
groupRatio[group],
|
||||
);
|
||||
}}
|
||||
>
|
||||
{group}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: () => (
|
||||
<span style={{ display: 'flex', alignItems: 'center' }}>
|
||||
@@ -203,6 +243,8 @@ const ModelPricing = () => {
|
||||
<Text>
|
||||
补全:{record.quota_type === 0 ? completionRatio : '无'}
|
||||
</Text>
|
||||
<br />
|
||||
<Text>分组:{groupRatio[selectedGroup]}</Text>
|
||||
</>
|
||||
);
|
||||
return <div>{content}</div>;
|
||||
@@ -215,12 +257,13 @@ const ModelPricing = () => {
|
||||
let content = text;
|
||||
if (record.quota_type === 0) {
|
||||
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
|
||||
let inputRatioPrice = record.model_ratio * 2 * record.group_ratio;
|
||||
let inputRatioPrice =
|
||||
record.model_ratio * 2 * groupRatio[selectedGroup];
|
||||
let completionRatioPrice =
|
||||
record.model_ratio *
|
||||
record.completion_ratio *
|
||||
2 *
|
||||
record.group_ratio;
|
||||
groupRatio[selectedGroup];
|
||||
content = (
|
||||
<>
|
||||
<Text>提示 ${inputRatioPrice} / 1M tokens</Text>
|
||||
@@ -229,7 +272,7 @@ const ModelPricing = () => {
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
let price = parseFloat(text) * record.group_ratio;
|
||||
let price = parseFloat(text) * groupRatio[selectedGroup];
|
||||
content = <>模型价格:${price}</>;
|
||||
}
|
||||
return <div>{content}</div>;
|
||||
@@ -240,12 +283,12 @@ const ModelPricing = () => {
|
||||
const [models, setModels] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [groupRatio, setGroupRatio] = useState(1);
|
||||
const [groupRatio, setGroupRatio] = useState({});
|
||||
|
||||
const setModelsFormat = (models, groupRatio) => {
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
models[i].key = models[i].model_name;
|
||||
models[i].group_ratio = groupRatio;
|
||||
models[i].group_ratio = groupRatio[models[i].model_name];
|
||||
}
|
||||
// sort by quota_type
|
||||
models.sort((a, b) => {
|
||||
@@ -278,6 +321,7 @@ const ModelPricing = () => {
|
||||
const { success, message, data, group_ratio } = res.data;
|
||||
if (success) {
|
||||
setGroupRatio(group_ratio);
|
||||
setSelectedGroup(userState.user ? userState.user.group : 'default');
|
||||
setModelsFormat(data, group_ratio);
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -310,14 +354,14 @@ const ModelPricing = () => {
|
||||
type='success'
|
||||
fullMode={false}
|
||||
closeIcon='null'
|
||||
description={`您的分组为:${userState.user.group},分组倍率为:${groupRatio}`}
|
||||
description={`您的默认分组为:${userState.user.group},分组倍率为:${groupRatio[userState.user.group]}`}
|
||||
/>
|
||||
) : (
|
||||
<Banner
|
||||
type='warning'
|
||||
fullMode={false}
|
||||
closeIcon='null'
|
||||
description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio}`}
|
||||
description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio['default']}`}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
|
||||
@@ -23,6 +23,7 @@ const OperationSetting = () => {
|
||||
CompletionRatio: '',
|
||||
ModelPrice: '',
|
||||
GroupRatio: '',
|
||||
UserUsableGroups: '',
|
||||
TopUpLink: '',
|
||||
ChatLink: '',
|
||||
ChatLink2: '', // 添加的新状态变量
|
||||
@@ -62,6 +63,7 @@ const OperationSetting = () => {
|
||||
if (
|
||||
item.key === 'ModelRatio' ||
|
||||
item.key === 'GroupRatio' ||
|
||||
item.key === 'UserUsableGroups' ||
|
||||
item.key === 'CompletionRatio' ||
|
||||
item.key === 'ModelPrice'
|
||||
) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
IconComment,
|
||||
IconCreditCard,
|
||||
IconGift,
|
||||
IconHelpCircle,
|
||||
IconHistogram,
|
||||
IconHome,
|
||||
IconImage,
|
||||
@@ -28,8 +29,10 @@ import {
|
||||
IconSetting,
|
||||
IconUser,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { Layout, Nav } from '@douyinfe/semi-ui';
|
||||
import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
|
||||
import { setStatusData } from '../helpers/data.js';
|
||||
import { stringToColor } from '../helpers/render.js';
|
||||
import { useSetTheme, useTheme } from '../context/Theme/index.js';
|
||||
|
||||
// HeaderBar Buttons
|
||||
|
||||
@@ -44,6 +47,8 @@ const SiderBar = () => {
|
||||
const systemName = getSystemName();
|
||||
const logo = getLogo();
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
|
||||
const theme = useTheme();
|
||||
const setTheme = useSetTheme();
|
||||
|
||||
const routerMap = {
|
||||
home: '/',
|
||||
@@ -64,11 +69,17 @@ const SiderBar = () => {
|
||||
|
||||
const headerButtons = useMemo(
|
||||
() => [
|
||||
// {
|
||||
// text: '首页',
|
||||
// itemKey: 'home',
|
||||
// to: '/',
|
||||
// icon: <IconHome />,
|
||||
// },
|
||||
{
|
||||
text: '首页',
|
||||
itemKey: 'home',
|
||||
to: '/',
|
||||
icon: <IconHome />,
|
||||
text: '模型价格',
|
||||
itemKey: 'pricing',
|
||||
to: '/pricing',
|
||||
icon: <IconPriceTag />,
|
||||
},
|
||||
{
|
||||
text: '渠道',
|
||||
@@ -105,12 +116,6 @@ const SiderBar = () => {
|
||||
to: '/topup',
|
||||
icon: <IconCreditCard />,
|
||||
},
|
||||
{
|
||||
text: '模型价格',
|
||||
itemKey: 'pricing',
|
||||
to: '/pricing',
|
||||
icon: <IconPriceTag />,
|
||||
},
|
||||
{
|
||||
text: '用户管理',
|
||||
itemKey: 'user',
|
||||
@@ -206,48 +211,58 @@ const SiderBar = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<div style={{ height: '100%' }}>
|
||||
<Nav
|
||||
// bodyStyle={{ maxWidth: 200 }}
|
||||
style={{ maxWidth: 200 }}
|
||||
defaultIsCollapsed={
|
||||
isMobile() ||
|
||||
localStorage.getItem('default_collapse_sidebar') === 'true'
|
||||
}
|
||||
isCollapsed={isCollapsed}
|
||||
onCollapseChange={(collapsed) => {
|
||||
setIsCollapsed(collapsed);
|
||||
}}
|
||||
selectedKeys={selectedKeys}
|
||||
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
|
||||
return (
|
||||
<Link
|
||||
style={{ textDecoration: 'none' }}
|
||||
to={routerMap[props.itemKey]}
|
||||
>
|
||||
{itemElement}
|
||||
</Link>
|
||||
);
|
||||
}}
|
||||
items={headerButtons}
|
||||
onSelect={(key) => {
|
||||
setSelectedKeys([key.itemKey]);
|
||||
}}
|
||||
header={{
|
||||
logo: (
|
||||
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
|
||||
),
|
||||
text: systemName,
|
||||
}}
|
||||
// footer={{
|
||||
// text: '© 2021 NekoAPI',
|
||||
// }}
|
||||
>
|
||||
<Nav.Footer collapseButton={true}></Nav.Footer>
|
||||
</Nav>
|
||||
</div>
|
||||
</Layout>
|
||||
<Nav
|
||||
style={{ maxWidth: 220, height: '100%' }}
|
||||
defaultIsCollapsed={
|
||||
isMobile() ||
|
||||
localStorage.getItem('default_collapse_sidebar') === 'true'
|
||||
}
|
||||
isCollapsed={isCollapsed}
|
||||
onCollapseChange={(collapsed) => {
|
||||
setIsCollapsed(collapsed);
|
||||
}}
|
||||
selectedKeys={selectedKeys}
|
||||
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
|
||||
return (
|
||||
<Link
|
||||
style={{ textDecoration: 'none' }}
|
||||
to={routerMap[props.itemKey]}
|
||||
>
|
||||
{itemElement}
|
||||
</Link>
|
||||
);
|
||||
}}
|
||||
items={headerButtons}
|
||||
onSelect={(key) => {
|
||||
setSelectedKeys([key.itemKey]);
|
||||
}}
|
||||
// header={{
|
||||
// logo: (
|
||||
// <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
|
||||
// ),
|
||||
// text: systemName,
|
||||
// }}
|
||||
// footer={{
|
||||
// text: '© 2021 NekoAPI',
|
||||
// }}
|
||||
footer={
|
||||
<>
|
||||
{isMobile() && (
|
||||
<Switch
|
||||
checkedText='🌞'
|
||||
size={'small'}
|
||||
checked={theme === 'dark'}
|
||||
uncheckedText='🌙'
|
||||
onChange={(checked) => {
|
||||
setTheme(checked);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Nav.Footer collapseButton={true}></Nav.Footer>
|
||||
</Nav>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '../helpers';
|
||||
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderQuota } from '../helpers/render';
|
||||
import { renderGroup, renderQuota } from '../helpers/render';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Popover,
|
||||
Space,
|
||||
SplitButtonGroup,
|
||||
Table,
|
||||
Tag,
|
||||
@@ -119,7 +120,14 @@ const TokensTable = () => {
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderStatus(text, record.model_limits_enabled)}</div>;
|
||||
return (
|
||||
<div>
|
||||
<Space>
|
||||
{renderStatus(text, record.model_limits_enabled)}
|
||||
{renderGroup(record.group)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
70
web/src/components/fetchTokenKeys.js
Normal file
70
web/src/components/fetchTokenKeys.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// src/hooks/useTokenKeys.js
|
||||
import { useEffect, useState } from 'react';
|
||||
import { API, showError } from '../helpers';
|
||||
|
||||
async function fetchTokenKeys() {
|
||||
try {
|
||||
const response = await API.get('/api/token/?p=0&size=999');
|
||||
const { success, data } = response.data;
|
||||
if (success) {
|
||||
const activeTokens = data.filter((token) => token.status === 1);
|
||||
return activeTokens.map((token) => token.key);
|
||||
} else {
|
||||
throw new Error('Failed to fetch token keys');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching token keys:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getServerAddress() {
|
||||
let status = localStorage.getItem('status');
|
||||
let serverAddress = '';
|
||||
|
||||
if (status) {
|
||||
try {
|
||||
status = JSON.parse(status);
|
||||
serverAddress = status.server_address || '';
|
||||
} catch (error) {
|
||||
console.error('Failed to parse status from localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!serverAddress) {
|
||||
serverAddress = window.location.origin;
|
||||
}
|
||||
|
||||
return serverAddress;
|
||||
}
|
||||
|
||||
export function useTokenKeys() {
|
||||
const [keys, setKeys] = useState([]);
|
||||
const [chatLink, setChatLink] = useState('');
|
||||
const [serverAddress, setServerAddress] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadAllData = async () => {
|
||||
const fetchedKeys = await fetchTokenKeys();
|
||||
if (fetchedKeys.length === 0) {
|
||||
showError('当前没有可用的启用令牌,请确认是否有令牌处于启用状态!');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/token';
|
||||
}, 1500); // 延迟 1.5 秒后跳转
|
||||
}
|
||||
setKeys(fetchedKeys);
|
||||
setIsLoading(false);
|
||||
|
||||
const link = localStorage.getItem('chat_link');
|
||||
setChatLink(link);
|
||||
|
||||
const address = getServerAddress();
|
||||
setServerAddress(address);
|
||||
};
|
||||
|
||||
loadAllData();
|
||||
}, []);
|
||||
|
||||
return { keys, chatLink, serverAddress, isLoading };
|
||||
}
|
||||
@@ -15,8 +15,8 @@ export function renderText(text, limit) {
|
||||
export function renderGroup(group) {
|
||||
if (group === '') {
|
||||
return (
|
||||
<Tag size='large' key='default'>
|
||||
unknown
|
||||
<Tag size='large' key='default' color='orange'>
|
||||
用户分组
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@ body {
|
||||
scrollbar-width: none;
|
||||
color: var(--semi-color-text-0) !important;
|
||||
background-color: var(--semi-color-bg-0) !important;
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
@@ -50,9 +51,9 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.semi-layout {
|
||||
height: 100%;
|
||||
}
|
||||
/*.semi-layout {*/
|
||||
/* height: 100%;*/
|
||||
/*}*/
|
||||
|
||||
.tableShow {
|
||||
display: revert;
|
||||
|
||||
@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import HeaderBar from './components/HeaderBar';
|
||||
import Footer from './components/Footer';
|
||||
import 'semantic-ui-offline/semantic.min.css';
|
||||
import './index.css';
|
||||
import { UserProvider } from './context/User';
|
||||
@@ -13,35 +12,40 @@ import { StatusProvider } from './context/Status';
|
||||
import { Layout } from '@douyinfe/semi-ui';
|
||||
import SiderBar from './components/SiderBar';
|
||||
import { ThemeProvider } from './context/Theme';
|
||||
import FooterBar from './components/Footer';
|
||||
|
||||
// initialization
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
const { Sider, Content, Header } = Layout;
|
||||
const { Sider, Content, Header, Footer } = Layout;
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<ThemeProvider>
|
||||
<Layout>
|
||||
<Sider>
|
||||
<SiderBar />
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header>
|
||||
<HeaderBar />
|
||||
</Header>
|
||||
<Content
|
||||
style={{
|
||||
padding: '24px',
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
</Content>
|
||||
<Layout.Footer>
|
||||
<Footer></Footer>
|
||||
</Layout.Footer>
|
||||
<Layout
|
||||
style={{
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Header>
|
||||
<HeaderBar />
|
||||
</Header>
|
||||
<Layout style={{ flex: 1, overflow: 'hidden' }}>
|
||||
<Sider>
|
||||
<SiderBar />
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Content style={{ overflowY: 'auto', padding: '24px' }}>
|
||||
<App />
|
||||
</Content>
|
||||
<Layout.Footer>
|
||||
<FooterBar></FooterBar>
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<ToastContainer />
|
||||
</Layout>
|
||||
|
||||
@@ -1,14 +1,36 @@
|
||||
import React from 'react';
|
||||
import { useTokenKeys } from '../../components/fetchTokenKeys';
|
||||
import { Layout } from '@douyinfe/semi-ui';
|
||||
|
||||
const Chat = () => {
|
||||
const chatLink = localStorage.getItem('chat_link');
|
||||
const ChatPage = () => {
|
||||
const { keys, chatLink, serverAddress, isLoading } = useTokenKeys();
|
||||
|
||||
return (
|
||||
const comLink = (key) => {
|
||||
if (!chatLink || !serverAddress || !key) return '';
|
||||
return `${chatLink}/#/?settings={"key":"sk-${key}","url":"${encodeURIComponent(serverAddress)}"}`;
|
||||
};
|
||||
|
||||
const iframeSrc = keys.length > 0 ? comLink(keys[0]) : '';
|
||||
|
||||
return !isLoading && iframeSrc ? (
|
||||
<iframe
|
||||
src={chatLink}
|
||||
src={iframeSrc}
|
||||
style={{ width: '100%', height: '85vh', border: 'none' }}
|
||||
title='Token Frame'
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<Layout>
|
||||
<Layout.Header>
|
||||
<h3 style={{ color: 'red' }}>
|
||||
当前没有可用的已启用令牌,请确认是否有令牌处于启用状态!
|
||||
<br />
|
||||
正在跳转......
|
||||
</h3>
|
||||
</Layout.Header>
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
export default ChatPage;
|
||||
|
||||
26
web/src/pages/Chat2Link/index.js
Normal file
26
web/src/pages/Chat2Link/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { useTokenKeys } from '../../components/fetchTokenKeys';
|
||||
|
||||
const chat2page = () => {
|
||||
const { keys, chatLink, serverAddress, isLoading } = useTokenKeys();
|
||||
|
||||
const comLink = (key) => {
|
||||
if (!chatLink || !serverAddress || !key) return '';
|
||||
return `${chatLink}/#/?settings={"key":"sk-${key}","url":"${encodeURIComponent(serverAddress)}"}`;
|
||||
};
|
||||
|
||||
if (keys.length > 0) {
|
||||
const redirectLink = comLink(keys[0]);
|
||||
if (redirectLink) {
|
||||
window.location.href = redirectLink;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>正在加载,请稍候...</h3>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default chat2page;
|
||||
@@ -25,6 +25,7 @@ export default function SettingsMagnification(props) {
|
||||
ModelRatio: '',
|
||||
CompletionRatio: '',
|
||||
GroupRatio: '',
|
||||
UserUsableGroups: '',
|
||||
});
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
@@ -224,6 +225,33 @@ export default function SettingsMagnification(props) {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<Form.TextArea
|
||||
label={'用户可选分组'}
|
||||
extraText={''}
|
||||
placeholder={'为一个 JSON 文本,键为分组名称,值为倍率'}
|
||||
field={'UserUsableGroups'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
return verifyJSON(value);
|
||||
},
|
||||
message: '不是合法的 JSON 字符串',
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
UserUsableGroups: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
<Space>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
SideSheet,
|
||||
Space,
|
||||
Spin,
|
||||
TextArea,
|
||||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||
@@ -34,6 +35,8 @@ const EditToken = (props) => {
|
||||
unlimited_quota: false,
|
||||
model_limits_enabled: false,
|
||||
model_limits: [],
|
||||
allow_ips: '',
|
||||
group: '',
|
||||
};
|
||||
const [inputs, setInputs] = useState(originInputs);
|
||||
const {
|
||||
@@ -43,9 +46,12 @@ const EditToken = (props) => {
|
||||
unlimited_quota,
|
||||
model_limits_enabled,
|
||||
model_limits,
|
||||
allow_ips,
|
||||
group,
|
||||
} = inputs;
|
||||
// const [visible, setVisible] = useState(false);
|
||||
const [models, setModels] = useState({});
|
||||
const [groups, setGroups] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
const handleInputChange = (name, value) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
@@ -86,6 +92,22 @@ const EditToken = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadGroups = async () => {
|
||||
let res = await API.get(`/api/user/groups`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
// return data is a map, key is group name, value is group description
|
||||
// label is group description, value is group name
|
||||
let localGroupOptions = Object.keys(data).map((group) => ({
|
||||
label: data[group],
|
||||
value: group,
|
||||
}));
|
||||
setGroups(localGroupOptions);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const loadToken = async () => {
|
||||
setLoading(true);
|
||||
let res = await API.get(`/api/token/${props.editingToken.id}`);
|
||||
@@ -118,6 +140,7 @@ const EditToken = (props) => {
|
||||
});
|
||||
}
|
||||
loadModels();
|
||||
loadGroups();
|
||||
}, [isEdit]);
|
||||
|
||||
// 新增 state 变量 tokenCount 来记录用户想要创建的令牌数量,默认为 1
|
||||
@@ -374,6 +397,19 @@ const EditToken = (props) => {
|
||||
</Button>
|
||||
</div>
|
||||
<Divider />
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text>IP白名单(请勿过度信任此功能)</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
label='IP白名单'
|
||||
name='allow_ips'
|
||||
placeholder={'允许的IP,一行一个'}
|
||||
onChange={(value) => {
|
||||
handleInputChange('allow_ips', value);
|
||||
}}
|
||||
value={inputs.allow_ips}
|
||||
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
/>
|
||||
<div style={{ marginTop: 10, display: 'flex' }}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
@@ -404,6 +440,31 @@ const EditToken = (props) => {
|
||||
optionList={models}
|
||||
disabled={!model_limits_enabled}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text>令牌分组,默认为用户的分组</Typography.Text>
|
||||
</div>
|
||||
{groups.length > 0 ? (
|
||||
<Select
|
||||
style={{ marginTop: 8 }}
|
||||
placeholder={'令牌分组,默认为用户的分组'}
|
||||
name='gruop'
|
||||
required
|
||||
selection
|
||||
onChange={(value) => {
|
||||
handleInputChange('group', value);
|
||||
}}
|
||||
value={inputs.group}
|
||||
autoComplete='new-password'
|
||||
optionList={groups}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
style={{ marginTop: 8 }}
|
||||
placeholder={'管理员未设置用户可选分组'}
|
||||
name='gruop'
|
||||
disabled={true}
|
||||
/>
|
||||
)}
|
||||
</Spin>
|
||||
</SideSheet>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user