mirror of
https://github.com/linux-do/new-api.git
synced 2025-11-09 15:43:41 +08:00
更换前端组件库
This commit is contained in:
405
web/src/App.js
405
web/src/App.js
@@ -24,6 +24,7 @@ import EditRedemption from './pages/Redemption/EditRedemption';
|
||||
import TopUp from './pages/TopUp';
|
||||
import Log from './pages/Log';
|
||||
import Chat from './pages/Chat';
|
||||
import {Layout} from "@douyinfe/semi-ui";
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
const About = lazy(() => import('./pages/About'));
|
||||
@@ -86,207 +87,209 @@ function App() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<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='/token/edit/:id'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditToken />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/token/add'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditToken />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/redemption'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Redemption />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/redemption/edit/:id'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditRedemption />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/redemption/add'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditRedemption />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<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/add'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<AddUser />
|
||||
</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='/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='/about'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<About />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/chat'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Chat />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route path='*' element={
|
||||
<NotFound />
|
||||
} />
|
||||
</Routes>
|
||||
<Layout>
|
||||
<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='/token/edit/:id'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditToken />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/token/add'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditToken />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/redemption'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Redemption />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/redemption/edit/:id'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditRedemption />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/redemption/add'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditRedemption />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<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/add'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<AddUser />
|
||||
</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='/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='/about'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<About />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/chat'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Chat />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route path='*' element={
|
||||
<NotFound />
|
||||
} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ const Footer = () => {
|
||||
{systemName} {process.env.REACT_APP_VERSION}{' '}
|
||||
</a>
|
||||
由{' '}
|
||||
<a href='https://github.com/songquanpeng' target='_blank'>
|
||||
JustSong
|
||||
<a href='https://github.com/Calcium-Ion' target='_blank'>
|
||||
Calcium-Ion
|
||||
</a>{' '}
|
||||
构建,源代码遵循{' '}
|
||||
<a href='https://opensource.org/licenses/mit-license.php'>
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
|
||||
import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react';
|
||||
import { API, getLogo, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers';
|
||||
import '../index.css';
|
||||
|
||||
// Header Buttons
|
||||
let headerButtons = [
|
||||
{
|
||||
name: '首页',
|
||||
to: '/',
|
||||
icon: 'home'
|
||||
},
|
||||
{
|
||||
name: '渠道',
|
||||
to: '/channel',
|
||||
icon: 'sitemap',
|
||||
admin: true
|
||||
},
|
||||
{
|
||||
name: '令牌',
|
||||
to: '/token',
|
||||
icon: 'key'
|
||||
},
|
||||
{
|
||||
name: '兑换',
|
||||
to: '/redemption',
|
||||
icon: 'dollar sign',
|
||||
admin: true
|
||||
},
|
||||
{
|
||||
name: '充值',
|
||||
to: '/topup',
|
||||
icon: 'cart'
|
||||
},
|
||||
{
|
||||
name: '用户',
|
||||
to: '/user',
|
||||
icon: 'user',
|
||||
admin: true
|
||||
},
|
||||
{
|
||||
name: '日志',
|
||||
to: '/log',
|
||||
icon: 'book'
|
||||
},
|
||||
{
|
||||
name: 'Midjourney',
|
||||
to: '/midjourney',
|
||||
icon: 'images outline'
|
||||
},
|
||||
{
|
||||
name: '设置',
|
||||
to: '/setting',
|
||||
icon: 'setting'
|
||||
},
|
||||
{
|
||||
name: '关于',
|
||||
to: '/about',
|
||||
icon: 'info circle'
|
||||
}
|
||||
];
|
||||
|
||||
if (localStorage.getItem('chat_link')) {
|
||||
headerButtons.splice(1, 0, {
|
||||
name: '聊天',
|
||||
to: '/chat',
|
||||
icon: 'comments'
|
||||
});
|
||||
}
|
||||
|
||||
const Header = () => {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
let navigate = useNavigate();
|
||||
|
||||
const [showSidebar, setShowSidebar] = useState(false);
|
||||
const systemName = getSystemName();
|
||||
const logo = getLogo();
|
||||
|
||||
async function logout() {
|
||||
setShowSidebar(false);
|
||||
await API.get('/api/user/logout');
|
||||
showSuccess('注销成功!');
|
||||
userDispatch({ type: 'logout' });
|
||||
localStorage.removeItem('user');
|
||||
navigate('/login');
|
||||
}
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setShowSidebar(!showSidebar);
|
||||
};
|
||||
|
||||
const renderButtons = (isMobile) => {
|
||||
return headerButtons.map((button) => {
|
||||
if (button.admin && !isAdmin()) return <></>;
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
navigate(button.to);
|
||||
setShowSidebar(false);
|
||||
}}
|
||||
>
|
||||
{button.name}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Menu.Item key={button.name} as={Link} to={button.to}>
|
||||
<Icon name={button.icon} />
|
||||
{button.name}
|
||||
</Menu.Item>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (isMobile()) {
|
||||
return (
|
||||
<>
|
||||
<Menu
|
||||
borderless
|
||||
size='large'
|
||||
style={
|
||||
showSidebar
|
||||
? {
|
||||
borderBottom: 'none',
|
||||
marginBottom: '0',
|
||||
borderTop: 'none',
|
||||
height: '51px'
|
||||
}
|
||||
: { borderTop: 'none', height: '52px' }
|
||||
}
|
||||
>
|
||||
<Container>
|
||||
<Menu.Item as={Link} to='/'>
|
||||
<img
|
||||
src={logo}
|
||||
alt='logo'
|
||||
style={{ marginRight: '0.75em' }}
|
||||
/>
|
||||
<div style={{ fontSize: '20px' }}>
|
||||
<b>{systemName}</b>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Menu position='right'>
|
||||
<Menu.Item onClick={toggleSidebar}>
|
||||
<Icon name={showSidebar ? 'close' : 'sidebar'} />
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
</Container>
|
||||
</Menu>
|
||||
{showSidebar ? (
|
||||
<Segment style={{ marginTop: 0, borderTop: '0' }}>
|
||||
<Menu secondary vertical style={{ width: '100%', margin: 0 }}>
|
||||
{renderButtons(true)}
|
||||
<Menu.Item>
|
||||
{userState.user ? (
|
||||
<Button onClick={logout}>注销</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowSidebar(false);
|
||||
navigate('/login');
|
||||
}}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowSidebar(false);
|
||||
navigate('/register');
|
||||
}}
|
||||
>
|
||||
注册
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Segment>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu borderless style={{ borderTop: 'none' }}>
|
||||
<Container>
|
||||
<Menu.Item as={Link} to='/' className={'hide-on-mobile'}>
|
||||
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
|
||||
<div style={{ fontSize: '20px' }}>
|
||||
<b>{systemName}</b>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
{renderButtons(false)}
|
||||
<Menu.Menu position='right'>
|
||||
{userState.user ? (
|
||||
<Dropdown
|
||||
text={userState.user.username}
|
||||
pointing
|
||||
className='link item'
|
||||
>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item onClick={logout}>注销</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Menu.Item
|
||||
name='登录'
|
||||
as={Link}
|
||||
to='/login'
|
||||
className='btn btn-link'
|
||||
/>
|
||||
)}
|
||||
</Menu.Menu>
|
||||
</Container>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
230
web/src/components/HeaderBar.js
Normal file
230
web/src/components/HeaderBar.js
Normal file
@@ -0,0 +1,230 @@
|
||||
import React, {useContext, useState} from 'react';
|
||||
import {Link, useNavigate} from 'react-router-dom';
|
||||
import {UserContext} from '../context/User';
|
||||
|
||||
import {Button, Container, Icon, Menu, Segment} from 'semantic-ui-react';
|
||||
import {API, getLogo, getSystemName, isAdmin, isMobile, showSuccess} from '../helpers';
|
||||
import '../index.css';
|
||||
|
||||
import {
|
||||
IconAt,
|
||||
IconHistogram,
|
||||
IconGift,
|
||||
IconKey,
|
||||
IconUser,
|
||||
IconLayers,
|
||||
IconHelpCircle,
|
||||
IconCreditCard,
|
||||
IconSemiLogo,
|
||||
IconHome,
|
||||
IconImage
|
||||
} from '@douyinfe/semi-icons';
|
||||
import {Nav, Avatar, Dropdown, Layout, Switch} from '@douyinfe/semi-ui';
|
||||
import {stringToColor} from "../helpers/render";
|
||||
|
||||
// HeaderBar Buttons
|
||||
let headerButtons = [
|
||||
{
|
||||
text: '关于',
|
||||
itemKey: 'about',
|
||||
to: '/about',
|
||||
icon: <IconHelpCircle/>
|
||||
},
|
||||
];
|
||||
|
||||
if (localStorage.getItem('chat_link')) {
|
||||
headerButtons.splice(1, 0, {
|
||||
name: '聊天',
|
||||
to: '/chat',
|
||||
icon: 'comments'
|
||||
});
|
||||
}
|
||||
|
||||
const HeaderBar = () => {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
let navigate = useNavigate();
|
||||
|
||||
const [showSidebar, setShowSidebar] = useState(false);
|
||||
const systemName = getSystemName();
|
||||
const logo = getLogo();
|
||||
|
||||
async function logout() {
|
||||
setShowSidebar(false);
|
||||
await API.get('/api/user/logout');
|
||||
showSuccess('注销成功!');
|
||||
userDispatch({type: 'logout'});
|
||||
localStorage.removeItem('user');
|
||||
navigate('/login');
|
||||
}
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setShowSidebar(!showSidebar);
|
||||
};
|
||||
|
||||
const renderButtons = (isMobile) => {
|
||||
return headerButtons.map((button) => {
|
||||
if (button.admin && !isAdmin()) return <></>;
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
navigate(button.to);
|
||||
setShowSidebar(false);
|
||||
}}
|
||||
>
|
||||
{button.name}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Menu.Item key={button.name} as={Link} to={button.to}>
|
||||
<Icon name={button.icon}/>
|
||||
{button.name}
|
||||
</Menu.Item>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (isMobile()) {
|
||||
return (
|
||||
<>
|
||||
<Menu
|
||||
borderless
|
||||
size='large'
|
||||
style={
|
||||
showSidebar
|
||||
? {
|
||||
borderBottom: 'none',
|
||||
marginBottom: '0',
|
||||
borderTop: 'none',
|
||||
height: '51px'
|
||||
}
|
||||
: {borderTop: 'none', height: '52px'}
|
||||
}
|
||||
>
|
||||
<Container>
|
||||
<Menu.Item as={Link} to='/'>
|
||||
<img
|
||||
src={logo}
|
||||
alt='logo'
|
||||
style={{marginRight: '0.75em'}}
|
||||
/>
|
||||
<div style={{fontSize: '20px'}}>
|
||||
<b>{systemName}</b>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Menu position='right'>
|
||||
<Menu.Item onClick={toggleSidebar}>
|
||||
<Icon name={showSidebar ? 'close' : 'sidebar'}/>
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
</Container>
|
||||
</Menu>
|
||||
|
||||
{showSidebar ? (
|
||||
<Segment style={{marginTop: 0, borderTop: '0'}}>
|
||||
<Menu secondary vertical style={{ width: '100%', margin: 0 }}>
|
||||
{renderButtons(true)}
|
||||
<Menu.Item>
|
||||
{userState.user ? (
|
||||
<Button onClick={logout}>注销</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowSidebar(false);
|
||||
navigate('/login');
|
||||
}}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowSidebar(false);
|
||||
navigate('/register');
|
||||
}}
|
||||
>
|
||||
注册
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
|
||||
</Segment>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
const switchMode = (model) => {
|
||||
const body = document.body;
|
||||
if (!model) {
|
||||
body.removeAttribute('theme-mode');
|
||||
} else {
|
||||
body.setAttribute('theme-mode', 'dark');
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<div style={{width: '100%'}}>
|
||||
<Nav
|
||||
mode={'horizontal'}
|
||||
// bodyStyle={{ height: 100 }}
|
||||
renderWrapper={({itemElement, isSubNav, isInSubNav, props}) => {
|
||||
const routerMap = {
|
||||
about: "/about",
|
||||
};
|
||||
return (
|
||||
<Link
|
||||
style={{textDecoration: "none"}}
|
||||
to={routerMap[props.itemKey]}
|
||||
>
|
||||
{itemElement}
|
||||
</Link>
|
||||
);
|
||||
}}
|
||||
selectedKeys={[]}
|
||||
// items={headerButtons}
|
||||
onSelect={key => console.log(key)}
|
||||
footer={
|
||||
<>
|
||||
<Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
|
||||
<Switch checkedText="🌞" size={'large'} uncheckedText="🌙" onChange={switchMode} />
|
||||
{userState.user ?
|
||||
<>
|
||||
<Dropdown
|
||||
position="bottomRight"
|
||||
render={
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item>退出</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
<Avatar size="small" color={stringToColor(userState.user.username)} style={{ margin: 4 }}>
|
||||
{userState.user.username[0]}
|
||||
</Avatar>
|
||||
<span>{userState.user.username}</span>
|
||||
</Dropdown>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<Nav.Item itemKey={'login'} text={'登录'} icon={<IconKey />} />
|
||||
<Nav.Item itemKey={'register'} text={'注册'} icon={<IconUser />} />
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
>
|
||||
</Nav>
|
||||
</div>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderBar;
|
||||
@@ -1,403 +1,434 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Form, Header, Label, Pagination, Segment, Select, Table } from 'semantic-ui-react';
|
||||
import { API, isAdmin, showError, timestamp2string } from '../helpers';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Label} from 'semantic-ui-react';
|
||||
import {API, isAdmin, showError, timestamp2string} from '../helpers';
|
||||
|
||||
import {Table, Avatar, Tag, Form, Button, Layout, Select} from '@douyinfe/semi-ui';
|
||||
import {ITEMS_PER_PAGE} from '../constants';
|
||||
import {renderQuota, stringToColor} from '../helpers/render';
|
||||
import {
|
||||
IconAt,
|
||||
IconHistogram,
|
||||
IconGift,
|
||||
IconKey,
|
||||
IconUser,
|
||||
IconLayers,
|
||||
IconSetting,
|
||||
IconCreditCard,
|
||||
IconSemiLogo,
|
||||
IconHome,
|
||||
IconMore
|
||||
} from '@douyinfe/semi-icons';
|
||||
|
||||
const {Sider, Content, Header} = Layout;
|
||||
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderQuota } from '../helpers/render';
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return (
|
||||
<>
|
||||
{timestamp2string(timestamp)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{timestamp2string(timestamp)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const MODE_OPTIONS = [
|
||||
{ key: 'all', text: '全部用户', value: 'all' },
|
||||
{ key: 'self', text: '当前用户', value: 'self' }
|
||||
{key: 'all', text: '全部用户', value: 'all'},
|
||||
{key: 'self', text: '当前用户', value: 'self'}
|
||||
];
|
||||
|
||||
const LOG_OPTIONS = [
|
||||
{ key: '0', text: '全部', value: 0 },
|
||||
{ key: '1', text: '充值', value: 1 },
|
||||
{ key: '2', text: '消费', value: 2 },
|
||||
{ key: '3', text: '管理', value: 3 },
|
||||
{ key: '4', text: '系统', value: 4 }
|
||||
];
|
||||
const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo',
|
||||
'light-blue', 'lime', 'orange', 'pink',
|
||||
'purple', 'red', 'teal', 'violet', 'yellow'
|
||||
]
|
||||
|
||||
function renderType(type) {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return <Label basic color='green'> 充值 </Label>;
|
||||
case 2:
|
||||
return <Label basic color='olive'> 消费 </Label>;
|
||||
case 3:
|
||||
return <Label basic color='orange'> 管理 </Label>;
|
||||
case 4:
|
||||
return <Label basic color='purple'> 系统 </Label>;
|
||||
default:
|
||||
return <Label basic color='black'> 未知 </Label>;
|
||||
}
|
||||
switch (type) {
|
||||
case 1:
|
||||
return <Tag color='cyan' size='large'> 充值 </Tag>;
|
||||
case 2:
|
||||
return <Tag color='yellow' size='large'> 消费 </Tag>;
|
||||
case 3:
|
||||
return <Tag color='orange' size='large'> 管理 </Tag>;
|
||||
case 4:
|
||||
return <Tag color='purple' size='large'> 系统 </Tag>;
|
||||
default:
|
||||
return <Tag color='black' size='large'> 未知 </Tag>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const LogsTable = () => {
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [showStat, setShowStat] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [logType, setLogType] = useState(0);
|
||||
const isAdminUser = isAdmin();
|
||||
let now = new Date();
|
||||
const [inputs, setInputs] = useState({
|
||||
username: '',
|
||||
token_name: '',
|
||||
model_name: '',
|
||||
start_timestamp: timestamp2string(0),
|
||||
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
|
||||
channel: ''
|
||||
});
|
||||
const { username, token_name, model_name, start_timestamp, end_timestamp, channel } = inputs;
|
||||
|
||||
const [stat, setStat] = useState({
|
||||
quota: 0,
|
||||
token: 0
|
||||
});
|
||||
|
||||
const handleInputChange = (e, { name, value }) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
|
||||
const getLogSelfStat = async () => {
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setStat(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const getLogStat = async () => {
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
let res = await API.get(`/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setStat(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEyeClick = async () => {
|
||||
if (!showStat) {
|
||||
if (isAdminUser) {
|
||||
await getLogStat();
|
||||
} else {
|
||||
await getLogSelfStat();
|
||||
}
|
||||
}
|
||||
setShowStat(!showStat);
|
||||
};
|
||||
|
||||
const loadLogs = async (startIdx) => {
|
||||
let url = '';
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
if (isAdminUser) {
|
||||
url = `/api/log/?p=${startIdx}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`;
|
||||
} else {
|
||||
url = `/api/log/self/?p=${startIdx}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
|
||||
}
|
||||
const res = await API.get(url);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (startIdx === 0) {
|
||||
setLogs(data);
|
||||
} else {
|
||||
let newLogs = [...logs];
|
||||
newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
|
||||
setLogs(newLogs);
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const onPaginationChange = (e, { activePage }) => {
|
||||
(async () => {
|
||||
if (activePage === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
|
||||
// In this case we have to load more data and then append them.
|
||||
await loadLogs(activePage - 1);
|
||||
}
|
||||
setActivePage(activePage);
|
||||
})();
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
setLoading(true);
|
||||
setActivePage(1);
|
||||
await loadLogs(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refresh().then();
|
||||
}, [logType]);
|
||||
|
||||
const searchLogs = async () => {
|
||||
if (searchKeyword === '') {
|
||||
// if keyword is blank, load files instead.
|
||||
await loadLogs(0);
|
||||
setActivePage(1);
|
||||
return;
|
||||
}
|
||||
setSearching(true);
|
||||
const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setLogs(data);
|
||||
setActivePage(1);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const handleKeywordChange = async (e, { value }) => {
|
||||
setSearchKeyword(value.trim());
|
||||
};
|
||||
|
||||
const sortLog = (key) => {
|
||||
if (logs.length === 0) return;
|
||||
setLoading(true);
|
||||
let sortedLogs = [...logs];
|
||||
if (typeof sortedLogs[0][key] === 'string') {
|
||||
sortedLogs.sort((a, b) => {
|
||||
return ('' + a[key]).localeCompare(b[key]);
|
||||
});
|
||||
} else {
|
||||
sortedLogs.sort((a, b) => {
|
||||
if (a[key] === b[key]) return 0;
|
||||
if (a[key] > b[key]) return -1;
|
||||
if (a[key] < b[key]) return 1;
|
||||
});
|
||||
}
|
||||
if (sortedLogs[0].id === logs[0].id) {
|
||||
sortedLogs.reverse();
|
||||
}
|
||||
setLogs(sortedLogs);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Segment>
|
||||
<Header as='h3'>
|
||||
使用明细(总消耗额度:
|
||||
{showStat && renderQuota(stat.quota)}
|
||||
{!showStat && <span onClick={handleEyeClick} style={{ cursor: 'pointer', color: 'gray' }}>点击查看</span>}
|
||||
)
|
||||
</Header>
|
||||
<Form>
|
||||
<Form.Group>
|
||||
<Form.Input fluid label={'令牌名称'} width={3} value={token_name}
|
||||
placeholder={'可选值'} name='token_name' onChange={handleInputChange} />
|
||||
<Form.Input fluid label='模型名称' width={3} value={model_name} placeholder='可选值'
|
||||
name='model_name'
|
||||
onChange={handleInputChange} />
|
||||
<Form.Input fluid label='起始时间' width={4} value={start_timestamp} type='datetime-local'
|
||||
name='start_timestamp'
|
||||
onChange={handleInputChange} />
|
||||
<Form.Input fluid label='结束时间' width={4} value={end_timestamp} type='datetime-local'
|
||||
name='end_timestamp'
|
||||
onChange={handleInputChange} />
|
||||
<Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button>
|
||||
</Form.Group>
|
||||
{
|
||||
isAdminUser && <>
|
||||
<Form.Group>
|
||||
<Form.Input fluid label={'渠道 ID'} width={3} value={channel}
|
||||
placeholder='可选值' name='channel'
|
||||
onChange={handleInputChange} />
|
||||
<Form.Input fluid label={'用户名称'} width={3} value={username}
|
||||
placeholder={'可选值'} name='username'
|
||||
onChange={handleInputChange} />
|
||||
|
||||
</Form.Group>
|
||||
</>
|
||||
}
|
||||
</Form>
|
||||
<Table basic compact size='small'>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('created_time');
|
||||
}}
|
||||
width={3}
|
||||
>
|
||||
时间
|
||||
</Table.HeaderCell>
|
||||
{
|
||||
isAdminUser && <Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('channel');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
渠道
|
||||
</Table.HeaderCell>
|
||||
}
|
||||
{
|
||||
isAdminUser && <Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('username');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
用户
|
||||
</Table.HeaderCell>
|
||||
}
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('token_name');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
令牌
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('type');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
类型
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('model_name');
|
||||
}}
|
||||
width={2}
|
||||
>
|
||||
模型
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('prompt_tokens');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
提示
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('completion_tokens');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
补全
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('quota');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
额度
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('content');
|
||||
}}
|
||||
width={isAdminUser ? 4 : 6}
|
||||
>
|
||||
详情
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{logs
|
||||
.slice(
|
||||
(activePage - 1) * ITEMS_PER_PAGE,
|
||||
activePage * ITEMS_PER_PAGE
|
||||
)
|
||||
.map((log, idx) => {
|
||||
if (log.deleted) return <></>;
|
||||
const columns = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp2string',
|
||||
},
|
||||
{
|
||||
title: '渠道',
|
||||
dataIndex: 'channel',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Table.Row key={log.id}>
|
||||
<Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
|
||||
{
|
||||
isAdminUser && (
|
||||
<Table.Cell>{log.channel ? <Label basic>{log.channel}</Label> : ''}</Table.Cell>
|
||||
)
|
||||
}
|
||||
{
|
||||
isAdminUser && (
|
||||
<Table.Cell>{log.username ? <Label>{log.username}</Label> : ''}</Table.Cell>
|
||||
)
|
||||
}
|
||||
<Table.Cell>{log.token_name ? <Label basic>{log.token_name}</Label> : ''}</Table.Cell>
|
||||
<Table.Cell>{renderType(log.type)}</Table.Cell>
|
||||
<Table.Cell>{log.model_name ? <Label basic>{log.model_name}</Label> : ''}</Table.Cell>
|
||||
<Table.Cell>{log.prompt_tokens ? log.prompt_tokens : ''}</Table.Cell>
|
||||
<Table.Cell>{log.completion_tokens ? log.completion_tokens : ''}</Table.Cell>
|
||||
<Table.Cell>{log.quota ? renderQuota(log.quota, 6) : ''}</Table.Cell>
|
||||
<Table.Cell>{log.content}</Table.Cell>
|
||||
</Table.Row>
|
||||
isAdminUser ?
|
||||
logType === 0 || logType === 2 ?
|
||||
<div>
|
||||
{<Tag color={colors[parseInt(text) % 10]} size='large'> {text} </Tag>}
|
||||
</div>
|
||||
:
|
||||
<></>
|
||||
:
|
||||
<></>
|
||||
);
|
||||
})}
|
||||
</Table.Body>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'username',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
isAdminUser ?
|
||||
<div>
|
||||
<Avatar size="small" color={stringToColor(text)} style={{marginRight: 4}}>
|
||||
{typeof text === 'string' && text.slice(0, 1)}
|
||||
</Avatar>
|
||||
{text}
|
||||
</div>
|
||||
:
|
||||
<></>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '令牌',
|
||||
dataIndex: 'token_name',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
logType === 0 || logType === 2 ?
|
||||
<div>
|
||||
{<Tag color='grey' size='large'> {text} </Tag>}
|
||||
</div>
|
||||
:
|
||||
<></>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
{renderType(text)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '模型',
|
||||
dataIndex: 'model_name',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
logType === 0 || logType === 2 ?
|
||||
<div>
|
||||
{<Tag color={stringToColor(text)} size='large'> {text} </Tag>}
|
||||
</div>
|
||||
:
|
||||
<></>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '提示',
|
||||
dataIndex: 'prompt_tokens',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
logType === 0 || logType === 2 ?
|
||||
<div>
|
||||
{<span> {text} </span>}
|
||||
</div>
|
||||
:
|
||||
<></>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '补全',
|
||||
dataIndex: 'completion_tokens',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
logType === 0 || logType === 2 ?
|
||||
<div>
|
||||
{<span> {text} </span>}
|
||||
</div>
|
||||
:
|
||||
<></>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '花费',
|
||||
dataIndex: 'quota',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
logType === 0 || logType === 2 ?
|
||||
<div>
|
||||
{
|
||||
renderQuota(text, 6)
|
||||
}
|
||||
</div>
|
||||
:
|
||||
<></>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '详情',
|
||||
dataIndex: 'content',
|
||||
}
|
||||
];
|
||||
|
||||
<Table.Footer>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell colSpan={'10'}>
|
||||
<Select
|
||||
placeholder='选择明细分类'
|
||||
options={LOG_OPTIONS}
|
||||
style={{ marginRight: '8px' }}
|
||||
name='logType'
|
||||
value={logType}
|
||||
onChange={(e, { name, value }) => {
|
||||
setLogType(value);
|
||||
}}
|
||||
/>
|
||||
<Button size='small' onClick={refresh} loading={loading}>刷新</Button>
|
||||
<Pagination
|
||||
floated='right'
|
||||
activePage={activePage}
|
||||
onPageChange={onPaginationChange}
|
||||
size='small'
|
||||
siblingRange={1}
|
||||
totalPages={
|
||||
Math.ceil(logs.length / ITEMS_PER_PAGE) +
|
||||
(logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
|
||||
}
|
||||
/>
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Footer>
|
||||
</Table>
|
||||
</Segment>
|
||||
</>
|
||||
);
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [showStat, setShowStat] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [logType, setLogType] = useState(0);
|
||||
const isAdminUser = isAdmin();
|
||||
let now = new Date();
|
||||
const [inputs, setInputs] = useState({
|
||||
username: '',
|
||||
token_name: '',
|
||||
model_name: '',
|
||||
start_timestamp: timestamp2string(0),
|
||||
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
|
||||
channel: ''
|
||||
});
|
||||
const {username, token_name, model_name, start_timestamp, end_timestamp, channel} = inputs;
|
||||
|
||||
const [stat, setStat] = useState({
|
||||
quota: 0,
|
||||
token: 0
|
||||
});
|
||||
|
||||
const handleInputChange = (value, name) => {
|
||||
setInputs((inputs) => ({...inputs, [name]: value}));
|
||||
};
|
||||
|
||||
const getLogSelfStat = async () => {
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
setStat(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const getLogStat = async () => {
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
let res = await API.get(`/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`);
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
setStat(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEyeClick = async () => {
|
||||
if (!showStat) {
|
||||
if (isAdminUser) {
|
||||
await getLogStat();
|
||||
} else {
|
||||
await getLogSelfStat();
|
||||
}
|
||||
}
|
||||
setShowStat(!showStat);
|
||||
};
|
||||
|
||||
const setLogsFormat = (logs) => {
|
||||
for (let i = 0; i < logs.length; i++) {
|
||||
logs[i].timestamp2string = timestamp2string(logs[i].created_at);
|
||||
logs[i].key = '' + logs[i].id;
|
||||
}
|
||||
// data.key = '' + data.id
|
||||
setLogs(logs);
|
||||
setLogCount(logs.length + ITEMS_PER_PAGE);
|
||||
console.log(logCount);
|
||||
}
|
||||
|
||||
const loadLogs = async (startIdx) => {
|
||||
setLoading(true);
|
||||
|
||||
let url = '';
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
if (isAdminUser) {
|
||||
url = `/api/log/?p=${startIdx}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`;
|
||||
} else {
|
||||
url = `/api/log/self/?p=${startIdx}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
|
||||
}
|
||||
const res = await API.get(url);
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
if (startIdx === 0) {
|
||||
setLogsFormat(data);
|
||||
} else {
|
||||
let newLogs = [...logs];
|
||||
newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
|
||||
setLogsFormat(newLogs);
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const pageData = logs.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
|
||||
|
||||
// const onPaginationChange = (e, { activePage }) => {
|
||||
// (async () => {
|
||||
// if (activePage === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
|
||||
// // In this case we have to load more data and then append them.
|
||||
// await loadLogs(activePage - 1);
|
||||
// }
|
||||
// setActivePage(activePage);
|
||||
// })();
|
||||
// };
|
||||
const handlePageChange = page => {
|
||||
setActivePage(page);
|
||||
if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
|
||||
// In this case we have to load more data and then append them.
|
||||
loadLogs(page - 1).then(r => {
|
||||
});
|
||||
}
|
||||
// setLoading(false);
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
// setLoading(true);
|
||||
setActivePage(1);
|
||||
await loadLogs(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refresh().then();
|
||||
}, [logType]);
|
||||
|
||||
const searchLogs = async () => {
|
||||
if (searchKeyword === '') {
|
||||
// if keyword is blank, load files instead.
|
||||
await loadLogs(0);
|
||||
setActivePage(1);
|
||||
return;
|
||||
}
|
||||
setSearching(true);
|
||||
const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`);
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
setLogs(data);
|
||||
setActivePage(1);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const handleKeywordChange = async (e, {value}) => {
|
||||
setSearchKeyword(value.trim());
|
||||
};
|
||||
|
||||
const sortLog = (key) => {
|
||||
if (logs.length === 0) return;
|
||||
setLoading(true);
|
||||
let sortedLogs = [...logs];
|
||||
if (typeof sortedLogs[0][key] === 'string') {
|
||||
sortedLogs.sort((a, b) => {
|
||||
return ('' + a[key]).localeCompare(b[key]);
|
||||
});
|
||||
} else {
|
||||
sortedLogs.sort((a, b) => {
|
||||
if (a[key] === b[key]) return 0;
|
||||
if (a[key] > b[key]) return -1;
|
||||
if (a[key] < b[key]) return 1;
|
||||
});
|
||||
}
|
||||
if (sortedLogs[0].id === logs[0].id) {
|
||||
sortedLogs.reverse();
|
||||
}
|
||||
setLogs(sortedLogs);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<Header>
|
||||
<h3>使用明细(总消耗额度:
|
||||
{showStat && renderQuota(stat.quota)}
|
||||
{!showStat &&
|
||||
<span onClick={handleEyeClick} style={{cursor: 'pointer', color: 'gray'}}>点击查看</span>}
|
||||
)
|
||||
</h3>
|
||||
</Header>
|
||||
<Form layout='horizontal' style={{marginTop: 10}}>
|
||||
<>
|
||||
<Form.Input field="token_name" label='令牌名称' style={{width: 176}} value={token_name}
|
||||
placeholder={'可选值'} name='token_name'
|
||||
onChange={value => handleInputChange(value, 'token_name')}/>
|
||||
<Form.Input field="model_name" label='模型名称' style={{width: 176}} value={model_name}
|
||||
placeholder='可选值'
|
||||
name='model_name'
|
||||
onChange={value => handlePageChange(value, 'model_name')}/>
|
||||
<Form.DatePicker field="start_timestamp" label='起始时间' style={{width: 272}}
|
||||
value={start_timestamp} type='dateTime'
|
||||
name='start_timestamp'
|
||||
onChange={value => handleInputChange(value, 'start_timestamp')}/>
|
||||
<Form.DatePicker field="end_timestamp" fluid label='结束时间' style={{width: 272}}
|
||||
value={end_timestamp} type='dateTime'
|
||||
name='end_timestamp'
|
||||
onChange={value => handleInputChange(value, 'end_timestamp')}/>
|
||||
{/*<Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button>*/}
|
||||
{
|
||||
isAdminUser && <>
|
||||
<Form.Input field="channel" label='渠道 ID' style={{width: 176}} value={channel}
|
||||
placeholder='可选值' name='channel'
|
||||
onChange={value => handleInputChange(value, 'channel')}/>
|
||||
<Form.Input field="username" label='用户名称' style={{width: 176}} value={username}
|
||||
placeholder={'可选值'} name='username'
|
||||
onChange={value => handleInputChange(value, 'username')}/>
|
||||
</>
|
||||
}
|
||||
<Form.Section>
|
||||
<Button label='操作' type="primary" htmlType="submit" className="btn-margin-right"
|
||||
onClick={refresh}>查询</Button>
|
||||
</Form.Section>
|
||||
</>
|
||||
</Form>
|
||||
<Table columns={columns} dataSource={pageData} pagination={{
|
||||
currentPage: activePage,
|
||||
pageSize: ITEMS_PER_PAGE,
|
||||
total: logCount,
|
||||
pageSizeOpts: [10, 20, 50, 100],
|
||||
onPageChange: handlePageChange,
|
||||
}} loading={loading}/>
|
||||
<Select defaultValue="0" style={{width: 120}} onChange={
|
||||
(value) => {
|
||||
setLogType(parseInt(value));
|
||||
}
|
||||
}>
|
||||
<Select.Option value="0">全部</Select.Option>
|
||||
<Select.Option value="1">充值</Select.Option>
|
||||
<Select.Option value="2">消费</Select.Option>
|
||||
<Select.Option value="3">管理</Select.Option>
|
||||
<Select.Option value="4">系统</Select.Option>
|
||||
</Select>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogsTable;
|
||||
|
||||
274
web/src/components/SiderBar.js
Normal file
274
web/src/components/SiderBar.js
Normal file
@@ -0,0 +1,274 @@
|
||||
import React, {useContext, useState} from 'react';
|
||||
import {Link, useNavigate} from 'react-router-dom';
|
||||
import {UserContext} from '../context/User';
|
||||
|
||||
import {Button, Container, Icon, Menu, Segment} from 'semantic-ui-react';
|
||||
import {API, getLogo, getSystemName, isAdmin, isMobile, showSuccess} from '../helpers';
|
||||
import '../index.css';
|
||||
|
||||
import {
|
||||
IconAt,
|
||||
IconHistogram,
|
||||
IconGift,
|
||||
IconKey,
|
||||
IconUser,
|
||||
IconLayers,
|
||||
IconSetting,
|
||||
IconCreditCard,
|
||||
IconSemiLogo,
|
||||
IconHome,
|
||||
IconImage
|
||||
} from '@douyinfe/semi-icons';
|
||||
import {Nav, Avatar, Dropdown, Layout} from '@douyinfe/semi-ui';
|
||||
|
||||
// HeaderBar Buttons
|
||||
let headerButtons = [
|
||||
{
|
||||
text: '首页',
|
||||
itemKey: 'home',
|
||||
to: '/',
|
||||
icon: <IconHome/>
|
||||
},
|
||||
{
|
||||
text: '渠道',
|
||||
itemKey: 'channel',
|
||||
to: '/channel',
|
||||
icon: <IconLayers/>,
|
||||
admin: true
|
||||
},
|
||||
|
||||
{
|
||||
text: '令牌',
|
||||
itemKey: 'token',
|
||||
to: '/token',
|
||||
icon: <IconKey/>
|
||||
},
|
||||
{
|
||||
text: '兑换码',
|
||||
itemKey: 'redemption',
|
||||
to: '/redemption',
|
||||
icon: <IconGift/>,
|
||||
admin: true
|
||||
},
|
||||
{
|
||||
text: '钱包',
|
||||
itemKey: 'topup',
|
||||
to: '/topup',
|
||||
icon: <IconCreditCard/>
|
||||
},
|
||||
{
|
||||
text: '用户管理',
|
||||
itemKey: 'user',
|
||||
to: '/user',
|
||||
icon: <IconUser/>,
|
||||
admin: true
|
||||
},
|
||||
{
|
||||
text: '日志',
|
||||
itemKey: 'log',
|
||||
to: '/log',
|
||||
icon: <IconHistogram/>
|
||||
},
|
||||
{
|
||||
text: '绘图',
|
||||
itemKey: 'midjourney',
|
||||
to: '/midjourney',
|
||||
icon: <IconImage/>
|
||||
},
|
||||
{
|
||||
text: '设置',
|
||||
itemKey: 'setting',
|
||||
to: '/setting',
|
||||
icon: <IconSetting/>
|
||||
},
|
||||
// {
|
||||
// text: '关于',
|
||||
// itemKey: 'about',
|
||||
// to: '/about',
|
||||
// icon: <IconAt/>
|
||||
// }
|
||||
];
|
||||
|
||||
if (localStorage.getItem('chat_link')) {
|
||||
headerButtons.splice(1, 0, {
|
||||
name: '聊天',
|
||||
to: '/chat',
|
||||
icon: 'comments'
|
||||
});
|
||||
}
|
||||
|
||||
const HeaderBar = () => {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
let navigate = useNavigate();
|
||||
const [selectedKeys, setSelectedKeys] = useState(['home']);
|
||||
const [showSidebar, setShowSidebar] = useState(false);
|
||||
const systemName = getSystemName();
|
||||
const logo = getLogo();
|
||||
|
||||
async function logout() {
|
||||
setShowSidebar(false);
|
||||
await API.get('/api/user/logout');
|
||||
showSuccess('注销成功!');
|
||||
userDispatch({type: 'logout'});
|
||||
localStorage.removeItem('user');
|
||||
navigate('/login');
|
||||
}
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setShowSidebar(!showSidebar);
|
||||
};
|
||||
|
||||
const renderButtons = (isMobile) => {
|
||||
return headerButtons.map((button) => {
|
||||
if (button.admin && !isAdmin()) return <></>;
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
navigate(button.to);
|
||||
setShowSidebar(false);
|
||||
}}
|
||||
>
|
||||
{button.name}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Menu.Item key={button.name} as={Link} to={button.to}>
|
||||
<Icon name={button.icon}/>
|
||||
{button.name}
|
||||
</Menu.Item>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (isMobile()) {
|
||||
return (
|
||||
<>
|
||||
<Menu
|
||||
borderless
|
||||
size='large'
|
||||
style={
|
||||
showSidebar
|
||||
? {
|
||||
borderBottom: 'none',
|
||||
marginBottom: '0',
|
||||
borderTop: 'none',
|
||||
height: '51px'
|
||||
}
|
||||
: {borderTop: 'none', height: '52px'}
|
||||
}
|
||||
>
|
||||
<Container>
|
||||
<Menu.Item as={Link} to='/'>
|
||||
<img
|
||||
src={logo}
|
||||
alt='logo'
|
||||
style={{marginRight: '0.75em'}}
|
||||
/>
|
||||
<div style={{fontSize: '20px'}}>
|
||||
<b>{systemName}</b>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Menu position='right'>
|
||||
<Menu.Item onClick={toggleSidebar}>
|
||||
<Icon name={showSidebar ? 'close' : 'sidebar'}/>
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
</Container>
|
||||
</Menu>
|
||||
|
||||
{showSidebar ? (
|
||||
<Segment style={{marginTop: 0, borderTop: '0'}}>
|
||||
{/*<Menu secondary vertical style={{ width: '100%', margin: 0 }}>*/}
|
||||
{/* {renderButtons(true)}*/}
|
||||
{/* <Menu.Item>*/}
|
||||
{/* {userState.user ? (*/}
|
||||
{/* <Button onClick={logout}>注销</Button>*/}
|
||||
{/* ) : (*/}
|
||||
{/* <>*/}
|
||||
{/* <Button*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* setShowSidebar(false);*/}
|
||||
{/* navigate('/login');*/}
|
||||
{/* }}*/}
|
||||
{/* >*/}
|
||||
{/* 登录*/}
|
||||
{/* </Button>*/}
|
||||
{/* <Button*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* setShowSidebar(false);*/}
|
||||
{/* navigate('/register');*/}
|
||||
{/* }}*/}
|
||||
{/* >*/}
|
||||
{/* 注册*/}
|
||||
{/* </Button>*/}
|
||||
{/* </>*/}
|
||||
{/* )}*/}
|
||||
{/* </Menu.Item>*/}
|
||||
{/*</Menu>*/}
|
||||
|
||||
</Segment>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<div style={{height: '100%'}}>
|
||||
<Nav
|
||||
// mode={'horizontal'}
|
||||
// bodyStyle={{ height: 100 }}
|
||||
selectedKeys={selectedKeys}
|
||||
renderWrapper={({itemElement, isSubNav, isInSubNav, props}) => {
|
||||
const routerMap = {
|
||||
home: "/",
|
||||
channel: "/channel",
|
||||
token: "/token",
|
||||
redemption: "/redemption",
|
||||
topup: "/topup",
|
||||
user: "/user",
|
||||
log: "/log",
|
||||
midjourney: "/midjourney",
|
||||
setting: "/setting",
|
||||
about: "/about",
|
||||
};
|
||||
return (
|
||||
<Link
|
||||
style={{textDecoration: "none"}}
|
||||
to={routerMap[props.itemKey]}
|
||||
>
|
||||
{itemElement}
|
||||
</Link>
|
||||
);
|
||||
}}
|
||||
items={headerButtons}
|
||||
onSelect={key => {
|
||||
console.log(key);
|
||||
setSelectedKeys([key.itemKey]);
|
||||
}}
|
||||
header={{
|
||||
logo: <IconSemiLogo style={{height: '36px', fontSize: 36}}/>,
|
||||
text: 'NekoAPI'
|
||||
}}
|
||||
// footer={{
|
||||
// text: '© 2021 NekoAPI',
|
||||
// }}
|
||||
>
|
||||
|
||||
<Nav.Footer collapseButton={true}>
|
||||
</Nav.Footer>
|
||||
</Nav>
|
||||
</div>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderBar;
|
||||
@@ -55,4 +55,21 @@ export function renderQuotaWithPrompt(quota, digits) {
|
||||
return `(等价金额:${renderQuota(quota, digits)})`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo',
|
||||
'light-blue', 'lime', 'orange', 'pink',
|
||||
'purple', 'red', 'teal', 'violet', 'yellow'
|
||||
]
|
||||
|
||||
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];
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export function isRoot() {
|
||||
|
||||
export function getSystemName() {
|
||||
let system_name = localStorage.getItem('system_name');
|
||||
if (!system_name) return 'One API Midjourney';
|
||||
if (!system_name) return 'Neko API';
|
||||
return system_name;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ body {
|
||||
-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;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar {
|
||||
@@ -16,8 +18,27 @@ code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.semi-navigation-vertical {
|
||||
/*display: flex;*/
|
||||
/*flex-direction: column;*/
|
||||
}
|
||||
|
||||
.semi-navigation-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.semi-navigation-vertical {
|
||||
/*flex: 0 0 auto;*/
|
||||
/*display: flex;*/
|
||||
/*flex-direction: column;*/
|
||||
/*width: 100%;*/
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.small-icon .icon {
|
||||
|
||||
@@ -1,31 +1,49 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Container } from 'semantic-ui-react';
|
||||
import {BrowserRouter} from 'react-router-dom';
|
||||
import {Container} from 'semantic-ui-react';
|
||||
import App from './App';
|
||||
import Header from './components/Header';
|
||||
import HeaderBar from './components/HeaderBar';
|
||||
import Footer from './components/Footer';
|
||||
import 'semantic-ui-css/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 {StatusProvider} from './context/Status';
|
||||
import {Layout} from "@douyinfe/semi-ui";
|
||||
import SiderBar from "./components/SiderBar";
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
const {Sider, Content, Header} = Layout;
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<Header />
|
||||
<Container className={'main-content'}>
|
||||
<App />
|
||||
</Container>
|
||||
<ToastContainer />
|
||||
<Footer />
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</React.StrictMode>
|
||||
<React.StrictMode>
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<Layout>
|
||||
|
||||
<Sider>
|
||||
<SiderBar/>
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header>
|
||||
<HeaderBar/>
|
||||
</Header>
|
||||
<Content
|
||||
style={{
|
||||
padding: '24px',
|
||||
}}
|
||||
>
|
||||
<App/>
|
||||
</Content>
|
||||
<Footer></Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<ToastContainer/>
|
||||
{/*<Footer />*/}
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user