Commit 4a1af1c8 by Calcium-Ion Committed by GitHub

Merge pull request #141 from Calcium-Ion/vite-support

feat: vite
parents 7dc71738 eb955ae4
...@@ -5,7 +5,7 @@ COPY web/package.json . ...@@ -5,7 +5,7 @@ COPY web/package.json .
RUN npm install RUN npm install
COPY ./web . COPY ./web .
COPY ./VERSION . COPY ./VERSION .
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) npm run build
FROM golang AS builder2 FROM golang AS builder2
......
...@@ -20,10 +20,10 @@ import ( ...@@ -20,10 +20,10 @@ import (
_ "net/http/pprof" _ "net/http/pprof"
) )
//go:embed web/build //go:embed web/dist
var buildFS embed.FS var buildFS embed.FS
//go:embed web/build/index.html //go:embed web/dist/index.html
var indexPage []byte var indexPage []byte
func main() { func main() {
......
...@@ -17,7 +17,7 @@ func SetApiRouter(router *gin.Engine) { ...@@ -17,7 +17,7 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus) apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
apiRouter.GET("/notice", controller.GetNotice) apiRouter.GET("/notice", controller.GetNotice)
apiRouter.GET("/about", controller.GetAbout) apiRouter.GET("/about", controller.GetAbout)
apiRouter.GET("/midjourney", controller.GetMidjourney) //apiRouter.GET("/midjourney", controller.GetMidjourney)
apiRouter.GET("/home_page_content", controller.GetHomePageContent) apiRouter.GET("/home_page_content", controller.GetHomePageContent)
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification) apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
......
...@@ -16,9 +16,9 @@ func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { ...@@ -16,9 +16,9 @@ func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
router.Use(gzip.Gzip(gzip.DefaultCompression)) router.Use(gzip.Gzip(gzip.DefaultCompression))
router.Use(middleware.GlobalWebRateLimit()) router.Use(middleware.GlobalWebRateLimit())
router.Use(middleware.Cache()) router.Use(middleware.Cache())
router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build"))) router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/dist")))
router.NoRoute(func(c *gin.Context) { router.NoRoute(func(c *gin.Context) {
if strings.HasPrefix(c.Request.RequestURI, "/v1") || strings.HasPrefix(c.Request.RequestURI, "/api") { if strings.HasPrefix(c.Request.RequestURI, "/v1") || strings.HasPrefix(c.Request.RequestURI, "/api") || strings.HasPrefix(c.Request.RequestURI, "/assets") {
controller.RelayNotFound(c) controller.RelayNotFound(c)
return return
} }
......
module.exports = require("@so1ve/prettier-config");
\ No newline at end of file
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="logo.png" /> <link rel="icon" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
<meta <meta
...@@ -10,9 +10,10 @@ ...@@ -10,9 +10,10 @@
content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用" content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
/> />
<title>New API</title> <title>New API</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
</body> <script type="module" src="/src/index.js"></script>
</body>
</html> </html>
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
"name": "react-template", "name": "react-template",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module",
"dependencies": { "dependencies": {
"@douyinfe/semi-icons": "^2.46.1", "@douyinfe/semi-icons": "^2.46.1",
"@douyinfe/semi-ui": "^2.46.1", "@douyinfe/semi-ui": "^2.46.1",
...@@ -16,19 +17,18 @@ ...@@ -16,19 +17,18 @@
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-fireworks": "^1.0.4", "react-fireworks": "^1.0.4",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"react-telegram-login": "^1.1.2", "react-telegram-login": "^1.1.2",
"react-toastify": "^9.0.8", "react-toastify": "^9.0.8",
"react-turnstile": "^1.0.5", "react-turnstile": "^1.0.5",
"semantic-ui-css": "^2.5.0", "semantic-ui-offline": "^2.5.0",
"semantic-ui-react": "^2.1.3", "semantic-ui-react": "^2.1.3"
"usehooks-ts": "^2.9.1"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "dev": "vite",
"build": "react-scripts build", "build": "vite build",
"test": "react-scripts test", "lint": "prettier . --check",
"eject": "react-scripts eject" "lint:fix": "prettier . --write",
"preview": "vite preview"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
...@@ -49,8 +49,11 @@ ...@@ -49,8 +49,11 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"prettier": "2.8.8", "@so1ve/prettier-config": "^2.0.0",
"typescript": "4.4.2" "@vitejs/plugin-react": "^4.2.1",
"prettier": "^3.0.0",
"typescript": "4.4.2",
"vite": "^5.2.0"
}, },
"prettier": { "prettier": {
"singleQuote": true, "singleQuote": true,
......
...@@ -22,9 +22,10 @@ import Log from './pages/Log'; ...@@ -22,9 +22,10 @@ import Log from './pages/Log';
import Chat from './pages/Chat'; import Chat from './pages/Chat';
import { Layout } from '@douyinfe/semi-ui'; import { Layout } from '@douyinfe/semi-ui';
import Midjourney from './pages/Midjourney'; import Midjourney from './pages/Midjourney';
import Detail from './pages/Detail'; // import Detail from './pages/Detail';
const Home = lazy(() => import('./pages/Home')); const Home = lazy(() => import('./pages/Home'));
const Detail = lazy(() => import('./pages/Detail'));
const About = lazy(() => import('./pages/About')); const About = lazy(() => import('./pages/About'));
function App() { function App() {
...@@ -47,7 +48,7 @@ function App() { ...@@ -47,7 +48,7 @@ function App() {
} }
let logo = getLogo(); let logo = getLogo();
if (logo) { if (logo) {
let linkElement = document.querySelector('link[rel~=\'icon\']'); let linkElement = document.querySelector("link[rel~='icon']");
if (linkElement) { if (linkElement) {
linkElement.href = logo; linkElement.href = logo;
} }
...@@ -59,7 +60,7 @@ function App() { ...@@ -59,7 +60,7 @@ function App() {
<Layout.Content> <Layout.Content>
<Routes> <Routes>
<Route <Route
path="/" path='/'
element={ element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<Home /> <Home />
...@@ -67,7 +68,7 @@ function App() { ...@@ -67,7 +68,7 @@ function App() {
} }
/> />
<Route <Route
path="/channel" path='/channel'
element={ element={
<PrivateRoute> <PrivateRoute>
<Channel /> <Channel />
...@@ -75,7 +76,7 @@ function App() { ...@@ -75,7 +76,7 @@ function App() {
} }
/> />
<Route <Route
path="/channel/edit/:id" path='/channel/edit/:id'
element={ element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<EditChannel /> <EditChannel />
...@@ -83,7 +84,7 @@ function App() { ...@@ -83,7 +84,7 @@ function App() {
} }
/> />
<Route <Route
path="/channel/add" path='/channel/add'
element={ element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<EditChannel /> <EditChannel />
...@@ -91,7 +92,7 @@ function App() { ...@@ -91,7 +92,7 @@ function App() {
} }
/> />
<Route <Route
path="/token" path='/token'
element={ element={
<PrivateRoute> <PrivateRoute>
<Token /> <Token />
...@@ -99,7 +100,7 @@ function App() { ...@@ -99,7 +100,7 @@ function App() {
} }
/> />
<Route <Route
path="/redemption" path='/redemption'
element={ element={
<PrivateRoute> <PrivateRoute>
<Redemption /> <Redemption />
...@@ -107,7 +108,7 @@ function App() { ...@@ -107,7 +108,7 @@ function App() {
} }
/> />
<Route <Route
path="/user" path='/user'
element={ element={
<PrivateRoute> <PrivateRoute>
<User /> <User />
...@@ -115,7 +116,7 @@ function App() { ...@@ -115,7 +116,7 @@ function App() {
} }
/> />
<Route <Route
path="/user/edit/:id" path='/user/edit/:id'
element={ element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<EditUser /> <EditUser />
...@@ -123,7 +124,7 @@ function App() { ...@@ -123,7 +124,7 @@ function App() {
} }
/> />
<Route <Route
path="/user/edit" path='/user/edit'
element={ element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<EditUser /> <EditUser />
...@@ -131,7 +132,7 @@ function App() { ...@@ -131,7 +132,7 @@ function App() {
} }
/> />
<Route <Route
path="/user/reset" path='/user/reset'
element={ element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<PasswordResetConfirm /> <PasswordResetConfirm />
...@@ -139,7 +140,7 @@ function App() { ...@@ -139,7 +140,7 @@ function App() {
} }
/> />
<Route <Route
path="/login" path='/login'
element={ element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<LoginForm /> <LoginForm />
...@@ -147,7 +148,7 @@ function App() { ...@@ -147,7 +148,7 @@ function App() {
} }
/> />
<Route <Route
path="/register" path='/register'
element={ element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<RegisterForm /> <RegisterForm />
...@@ -155,7 +156,7 @@ function App() { ...@@ -155,7 +156,7 @@ function App() {
} }
/> />
<Route <Route
path="/reset" path='/reset'
element={ element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<PasswordResetForm /> <PasswordResetForm />
...@@ -163,7 +164,7 @@ function App() { ...@@ -163,7 +164,7 @@ function App() {
} }
/> />
<Route <Route
path="/oauth/github" path='/oauth/github'
element={ element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<GitHubOAuth /> <GitHubOAuth />
...@@ -171,7 +172,7 @@ function App() { ...@@ -171,7 +172,7 @@ function App() {
} }
/> />
<Route <Route
path="/setting" path='/setting'
element={ element={
<PrivateRoute> <PrivateRoute>
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
...@@ -181,7 +182,7 @@ function App() { ...@@ -181,7 +182,7 @@ function App() {
} }
/> />
<Route <Route
path="/topup" path='/topup'
element={ element={
<PrivateRoute> <PrivateRoute>
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
...@@ -191,7 +192,7 @@ function App() { ...@@ -191,7 +192,7 @@ function App() {
} }
/> />
<Route <Route
path="/log" path='/log'
element={ element={
<PrivateRoute> <PrivateRoute>
<Log /> <Log />
...@@ -199,23 +200,27 @@ function App() { ...@@ -199,23 +200,27 @@ function App() {
} }
/> />
<Route <Route
path="/detail" path='/detail'
element={ element={
<PrivateRoute> <PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Detail /> <Detail />
</Suspense>
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route <Route
path="/midjourney" path='/midjourney'
element={ element={
<PrivateRoute> <PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Midjourney /> <Midjourney />
</Suspense>
</PrivateRoute> </PrivateRoute>
} }
/> />
<Route <Route
path="/about" path='/about'
element={ element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<About /> <About />
...@@ -223,16 +228,14 @@ function App() { ...@@ -223,16 +228,14 @@ function App() {
} }
/> />
<Route <Route
path="/chat" path='/chat'
element={ element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<Chat /> <Chat />
</Suspense> </Suspense>
} }
/> />
<Route path="*" element={ <Route path='*' element={<NotFound />} />
<NotFound />
} />
</Routes> </Routes>
</Layout.Content> </Layout.Content>
</Layout> </Layout>
......
...@@ -32,27 +32,36 @@ const Footer = () => { ...@@ -32,27 +32,36 @@ const Footer = () => {
<Layout.Content style={{ textAlign: 'center' }}> <Layout.Content style={{ textAlign: 'center' }}>
{footer ? ( {footer ? (
<div <div
className="custom-footer" className='custom-footer'
dangerouslySetInnerHTML={{ __html: footer }} dangerouslySetInnerHTML={{ __html: footer }}
></div> ></div>
) : ( ) : (
<div className="custom-footer"> <div className='custom-footer'>
<a <a
href="https://github.com/Calcium-Ion/new-api" href='https://github.com/Calcium-Ion/new-api'
target="_blank" rel="noreferrer" target='_blank'
rel='noreferrer'
> >
New API {process.env.REACT_APP_VERSION}{' '} New API {import.meta.env.VITE_REACT_APP_VERSION}{' '}
</a> </a>
{' '} {' '}
<a href="https://github.com/Calcium-Ion" target="_blank" rel="noreferrer"> <a
href='https://github.com/Calcium-Ion'
target='_blank'
rel='noreferrer'
>
Calcium-Ion Calcium-Ion
</a>{' '} </a>{' '}
开发,基于{' '} 开发,基于{' '}
<a href="https://github.com/songquanpeng/one-api" target="_blank" rel="noreferrer"> <a
href='https://github.com/songquanpeng/one-api'
target='_blank'
rel='noreferrer'
>
One API v0.5.4 One API v0.5.4
</a>{' '} </a>{' '}
,本项目根据{' '} ,本项目根据{' '}
<a href="https://opensource.org/licenses/mit-license.php"> <a href='https://opensource.org/licenses/mit-license.php'>
MIT 许可证 MIT 许可证
</a>{' '} </a>{' '}
授权 授权
......
...@@ -49,7 +49,7 @@ const GitHubOAuth = () => { ...@@ -49,7 +49,7 @@ const GitHubOAuth = () => {
return ( return (
<Segment style={{ minHeight: '300px' }}> <Segment style={{ minHeight: '300px' }}>
<Dimmer active inverted> <Dimmer active inverted>
<Loader size="large">{prompt}</Loader> <Loader size='large'>{prompt}</Loader>
</Dimmer> </Dimmer>
</Segment> </Segment>
); );
......
...@@ -17,15 +17,15 @@ let headerButtons = [ ...@@ -17,15 +17,15 @@ let headerButtons = [
text: '关于', text: '关于',
itemKey: 'about', itemKey: 'about',
to: '/about', to: '/about',
icon: <IconHelpCircle /> icon: <IconHelpCircle />,
} },
]; ];
if (localStorage.getItem('chat_link')) { if (localStorage.getItem('chat_link')) {
headerButtons.splice(1, 0, { headerButtons.splice(1, 0, {
name: '聊天', name: '聊天',
to: '/chat', to: '/chat',
icon: 'comments' icon: 'comments',
}); });
} }
...@@ -40,7 +40,11 @@ const HeaderBar = () => { ...@@ -40,7 +40,11 @@ const HeaderBar = () => {
var themeMode = localStorage.getItem('theme-mode'); var themeMode = localStorage.getItem('theme-mode');
const currentDate = new Date(); const currentDate = new Date();
// enable fireworks on new year(1.1 and 2.9-2.24) // enable fireworks on new year(1.1 and 2.9-2.24)
const isNewYear = (currentDate.getMonth() === 0 && currentDate.getDate() === 1) || (currentDate.getMonth() === 1 && currentDate.getDate() >= 9 && currentDate.getDate() <= 24); const isNewYear =
(currentDate.getMonth() === 0 && currentDate.getDate() === 1) ||
(currentDate.getMonth() === 1 &&
currentDate.getDate() >= 9 &&
currentDate.getDate() <= 24);
async function logout() { async function logout() {
setShowSidebar(false); setShowSidebar(false);
...@@ -93,7 +97,7 @@ const HeaderBar = () => { ...@@ -93,7 +97,7 @@ const HeaderBar = () => {
const routerMap = { const routerMap = {
about: '/about', about: '/about',
login: '/login', login: '/login',
register: '/register' register: '/register',
}; };
return ( return (
<Link <Link
...@@ -106,52 +110,69 @@ const HeaderBar = () => { ...@@ -106,52 +110,69 @@ const HeaderBar = () => {
}} }}
selectedKeys={[]} selectedKeys={[]}
// items={headerButtons} // items={headerButtons}
onSelect={key => { onSelect={(key) => {}}
}}
footer={ footer={
<> <>
{isNewYear && {isNewYear && (
// happy new year // happy new year
<Dropdown <Dropdown
position="bottomRight" position='bottomRight'
render={ render={
<Dropdown.Menu> <Dropdown.Menu>
<Dropdown.Item onClick={handleNewYearClick}>Happy New Year!!!</Dropdown.Item> <Dropdown.Item onClick={handleNewYearClick}>
Happy New Year!!!
</Dropdown.Item>
</Dropdown.Menu> </Dropdown.Menu>
} }
> >
<Nav.Item itemKey={'new-year'} text={'🏮'} /> <Nav.Item itemKey={'new-year'} text={'🏮'} />
</Dropdown> </Dropdown>
} )}
<Nav.Item itemKey={'about'} icon={<IconHelpCircle />} /> <Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
<Switch checkedText="🌞" size={'large'} checked={dark} uncheckedText="🌙" onChange={switchMode} /> <Switch
{userState.user ? checkedText='🌞'
size={'large'}
checked={dark}
uncheckedText='🌙'
onChange={switchMode}
/>
{userState.user ? (
<> <>
<Dropdown <Dropdown
position="bottomRight" position='bottomRight'
render={ render={
<Dropdown.Menu> <Dropdown.Menu>
<Dropdown.Item onClick={logout}>退出</Dropdown.Item> <Dropdown.Item onClick={logout}>退出</Dropdown.Item>
</Dropdown.Menu> </Dropdown.Menu>
} }
> >
<Avatar size="small" color={stringToColor(userState.user.username)} style={{ margin: 4 }}> <Avatar
size='small'
color={stringToColor(userState.user.username)}
style={{ margin: 4 }}
>
{userState.user.username[0]} {userState.user.username[0]}
</Avatar> </Avatar>
<span>{userState.user.username}</span> <span>{userState.user.username}</span>
</Dropdown> </Dropdown>
</> </>
: ) : (
<> <>
<Nav.Item itemKey={'login'} text={'登录'} icon={<IconKey />} /> <Nav.Item
<Nav.Item itemKey={'register'} text={'注册'} icon={<IconUser />} /> itemKey={'login'}
text={'登录'}
icon={<IconKey />}
/>
<Nav.Item
itemKey={'register'}
text={'注册'}
icon={<IconUser />}
/>
</> </>
} )}
</> </>
} }
> ></Nav>
</Nav>
</div> </div>
</Layout> </Layout>
</> </>
......
import React from 'react'; import React from 'react';
import { Dimmer, Loader, Segment } from 'semantic-ui-react'; import { Spin } from '@douyinfe/semi-ui';
const Loading = ({ prompt: name = 'page' }) => { const Loading = ({ prompt: name = 'page' }) => {
return ( return (
<Segment style={{ height: 100 }}> <Spin style={{ height: 100 }} spinning={true}>
<Dimmer active inverted> 加载{name}...
<Loader indeterminate>加载{name}...</Loader> </Spin>
</Dimmer>
</Segment>
); );
}; };
......
...@@ -10,21 +10,20 @@ const OtherSetting = () => { ...@@ -10,21 +10,20 @@ const OtherSetting = () => {
Logo: '', Logo: '',
Footer: '', Footer: '',
About: '', About: '',
HomePageContent: '' HomePageContent: '',
}); });
let [loading, setLoading] = useState(false); let [loading, setLoading] = useState(false);
const [showUpdateModal, setShowUpdateModal] = useState(false); const [showUpdateModal, setShowUpdateModal] = useState(false);
const [updateData, setUpdateData] = useState({ const [updateData, setUpdateData] = useState({
tag_name: '', tag_name: '',
content: '' content: '',
}); });
const updateOption = async (key, value) => { const updateOption = async (key, value) => {
setLoading(true); setLoading(true);
const res = await API.put('/api/option/', { const res = await API.put('/api/option/', {
key, key,
value value,
}); });
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
...@@ -41,7 +40,7 @@ const OtherSetting = () => { ...@@ -41,7 +40,7 @@ const OtherSetting = () => {
Logo: false, Logo: false,
HomePageContent: false, HomePageContent: false,
About: false, About: false,
Footer: false Footer: false,
}); });
const handleInputChange = async (value, e) => { const handleInputChange = async (value, e) => {
const name = e.target.id; const name = e.target.id;
...@@ -68,14 +67,20 @@ const OtherSetting = () => { ...@@ -68,14 +67,20 @@ const OtherSetting = () => {
// 个性化设置 - SystemName // 个性化设置 - SystemName
const submitSystemName = async () => { const submitSystemName = async () => {
try { try {
setLoadingInput((loadingInput) => ({ ...loadingInput, SystemName: true })); setLoadingInput((loadingInput) => ({
...loadingInput,
SystemName: true,
}));
await updateOption('SystemName', inputs.SystemName); await updateOption('SystemName', inputs.SystemName);
showSuccess('系统名称已更新'); showSuccess('系统名称已更新');
} catch (error) { } catch (error) {
console.error('系统名称更新失败', error); console.error('系统名称更新失败', error);
showError('系统名称更新失败'); showError('系统名称更新失败');
} finally { } finally {
setLoadingInput((loadingInput) => ({ ...loadingInput, SystemName: false })); setLoadingInput((loadingInput) => ({
...loadingInput,
SystemName: false,
}));
} }
}; };
...@@ -95,14 +100,20 @@ const OtherSetting = () => { ...@@ -95,14 +100,20 @@ const OtherSetting = () => {
// 个性化设置 - 首页内容 // 个性化设置 - 首页内容
const submitOption = async (key) => { const submitOption = async (key) => {
try { try {
setLoadingInput((loadingInput) => ({ ...loadingInput, HomePageContent: true })); setLoadingInput((loadingInput) => ({
...loadingInput,
HomePageContent: true,
}));
await updateOption(key, inputs[key]); await updateOption(key, inputs[key]);
showSuccess('首页内容已更新'); showSuccess('首页内容已更新');
} catch (error) { } catch (error) {
console.error('首页内容更新失败', error); console.error('首页内容更新失败', error);
showError('首页内容更新失败'); showError('首页内容更新失败');
} finally { } finally {
setLoadingInput((loadingInput) => ({ ...loadingInput, HomePageContent: false })); setLoadingInput((loadingInput) => ({
...loadingInput,
HomePageContent: false,
}));
} }
}; };
// 个性化设置 - 关于 // 个性化设置 - 关于
...@@ -132,15 +143,13 @@ const OtherSetting = () => { ...@@ -132,15 +143,13 @@ const OtherSetting = () => {
} }
}; };
const openGitHubRelease = () => { const openGitHubRelease = () => {
window.location = window.location = 'https://github.com/songquanpeng/one-api/releases/latest';
'https://github.com/songquanpeng/one-api/releases/latest';
}; };
const checkUpdate = async () => { const checkUpdate = async () => {
const res = await API.get( const res = await API.get(
'https://api.github.com/repos/songquanpeng/one-api/releases/latest' 'https://api.github.com/repos/songquanpeng/one-api/releases/latest',
); );
const { tag_name, body } = res.data; const { tag_name, body } = res.data;
if (tag_name === process.env.REACT_APP_VERSION) { if (tag_name === process.env.REACT_APP_VERSION) {
...@@ -148,7 +157,7 @@ const OtherSetting = () => { ...@@ -148,7 +157,7 @@ const OtherSetting = () => {
} else { } else {
setUpdateData({ setUpdateData({
tag_name: tag_name, tag_name: tag_name,
content: marked.parse(body) content: marked.parse(body),
}); });
setShowUpdateModal(true); setShowUpdateModal(true);
} }
...@@ -175,13 +184,15 @@ const OtherSetting = () => { ...@@ -175,13 +184,15 @@ const OtherSetting = () => {
getOptions(); getOptions();
}, []); }, []);
return ( return (
<Row> <Row>
<Col span={24}> <Col span={24}>
{/* 通用设置 */} {/* 通用设置 */}
<Form values={inputs} getFormApi={formAPI => formAPISettingGeneral.current = formAPI} <Form
style={{ marginBottom: 15 }}> values={inputs}
getFormApi={(formAPI) => (formAPISettingGeneral.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'通用设置'}> <Form.Section text={'通用设置'}>
<Form.TextArea <Form.TextArea
label={'公告'} label={'公告'}
...@@ -191,12 +202,17 @@ const OtherSetting = () => { ...@@ -191,12 +202,17 @@ const OtherSetting = () => {
style={{ fontFamily: 'JetBrains Mono, Consolas' }} style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
/> />
<Button onClick={submitNotice} loading={loadingInput['Notice']}>设置公告</Button> <Button onClick={submitNotice} loading={loadingInput['Notice']}>
设置公告
</Button>
</Form.Section> </Form.Section>
</Form> </Form>
{/* 个性化设置 */} {/* 个性化设置 */}
<Form values={inputs} getFormApi={formAPI => formAPIPersonalization.current = formAPI} <Form
style={{ marginBottom: 15 }}> values={inputs}
getFormApi={(formAPI) => (formAPIPersonalization.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={'个性化设置'}> <Form.Section text={'个性化设置'}>
<Form.Input <Form.Input
label={'系统名称'} label={'系统名称'}
...@@ -204,48 +220,69 @@ const OtherSetting = () => { ...@@ -204,48 +220,69 @@ const OtherSetting = () => {
field={'SystemName'} field={'SystemName'}
onChange={handleInputChange} onChange={handleInputChange}
/> />
<Button onClick={submitSystemName} loading={loadingInput['SystemName']}>设置系统名称</Button> <Button
onClick={submitSystemName}
loading={loadingInput['SystemName']}
>
设置系统名称
</Button>
<Form.Input <Form.Input
label={'Logo 图片地址'} label={'Logo 图片地址'}
placeholder={'在此输入 Logo 图片地址'} placeholder={'在此输入 Logo 图片地址'}
field={'Logo'} field={'Logo'}
onChange={handleInputChange} onChange={handleInputChange}
/> />
<Button onClick={submitLogo} loading={loadingInput['Logo']}>设置 Logo</Button> <Button onClick={submitLogo} loading={loadingInput['Logo']}>
设置 Logo
</Button>
<Form.TextArea <Form.TextArea
label={'首页内容'} label={'首页内容'}
placeholder={'在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'} placeholder={
'在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'
}
field={'HomePageContent'} field={'HomePageContent'}
onChange={handleInputChange} onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }} style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
/> />
<Button onClick={() => submitOption('HomePageContent')} <Button
loading={loadingInput['HomePageContent']}>设置首页内容</Button> onClick={() => submitOption('HomePageContent')}
loading={loadingInput['HomePageContent']}
>
设置首页内容
</Button>
<Form.TextArea <Form.TextArea
label={'关于'} label={'关于'}
placeholder={'在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'} placeholder={
'在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'
}
field={'About'} field={'About'}
onChange={handleInputChange} onChange={handleInputChange}
style={{ fontFamily: 'JetBrains Mono, Consolas' }} style={{ fontFamily: 'JetBrains Mono, Consolas' }}
autosize={{ minRows: 6, maxRows: 12 }} autosize={{ minRows: 6, maxRows: 12 }}
/> />
<Button onClick={submitAbout} loading={loadingInput['About']}>设置关于</Button> <Button onClick={submitAbout} loading={loadingInput['About']}>
设置关于
</Button>
{/* */} {/* */}
<Banner <Banner
fullMode={false} fullMode={false}
type="info" type='info'
description="移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。" description='移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。'
closeIcon={null} closeIcon={null}
style={{ marginTop: 15 }} style={{ marginTop: 15 }}
/> />
<Form.Input <Form.Input
label={'页脚'} label={'页脚'}
placeholder={'在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码'} placeholder={
'在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码'
}
field={'Footer'} field={'Footer'}
onChange={handleInputChange} onChange={handleInputChange}
/> />
<Button onClick={submitFooter} loading={loadingInput['Footer']}>设置页脚</Button> <Button onClick={submitFooter} loading={loadingInput['Footer']}>
设置页脚
</Button>
</Form.Section> </Form.Section>
</Form> </Form>
</Col> </Col>
......
...@@ -6,7 +6,7 @@ import { useSearchParams } from 'react-router-dom'; ...@@ -6,7 +6,7 @@ import { useSearchParams } from 'react-router-dom';
const PasswordResetConfirm = () => { const PasswordResetConfirm = () => {
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
email: '', email: '',
token: '' token: '',
}); });
const { email, token } = inputs; const { email, token } = inputs;
...@@ -23,7 +23,7 @@ const PasswordResetConfirm = () => { ...@@ -23,7 +23,7 @@ const PasswordResetConfirm = () => {
let email = searchParams.get('email'); let email = searchParams.get('email');
setInputs({ setInputs({
token, token,
email email,
}); });
}, []); }, []);
...@@ -46,7 +46,7 @@ const PasswordResetConfirm = () => { ...@@ -46,7 +46,7 @@ const PasswordResetConfirm = () => {
setLoading(true); setLoading(true);
const res = await API.post(`/api/user/reset`, { const res = await API.post(`/api/user/reset`, {
email, email,
token token,
}); });
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
...@@ -61,29 +61,29 @@ const PasswordResetConfirm = () => { ...@@ -61,29 +61,29 @@ const PasswordResetConfirm = () => {
} }
return ( return (
<Grid textAlign="center" style={{ marginTop: '48px' }}> <Grid textAlign='center' style={{ marginTop: '48px' }}>
<Grid.Column style={{ maxWidth: 450 }}> <Grid.Column style={{ maxWidth: 450 }}>
<Header as="h2" color="" textAlign="center"> <Header as='h2' color='' textAlign='center'>
<Image src="/logo.png" /> 密码重置确认 <Image src='/logo.png' /> 密码重置确认
</Header> </Header>
<Form size="large"> <Form size='large'>
<Segment> <Segment>
<Form.Input <Form.Input
fluid fluid
icon="mail" icon='mail'
iconPosition="left" iconPosition='left'
placeholder="邮箱地址" placeholder='邮箱地址'
name="email" name='email'
value={email} value={email}
readOnly readOnly
/> />
{newPassword && ( {newPassword && (
<Form.Input <Form.Input
fluid fluid
icon="lock" icon='lock'
iconPosition="left" iconPosition='left'
placeholder="新密码" placeholder='新密码'
name="newPassword" name='newPassword'
value={newPassword} value={newPassword}
readOnly readOnly
onClick={(e) => { onClick={(e) => {
...@@ -94,9 +94,9 @@ const PasswordResetConfirm = () => { ...@@ -94,9 +94,9 @@ const PasswordResetConfirm = () => {
/> />
)} )}
<Button <Button
color="green" color='green'
fluid fluid
size="large" size='large'
onClick={handleSubmit} onClick={handleSubmit}
loading={loading} loading={loading}
disabled={disableButton} disabled={disableButton}
......
...@@ -5,7 +5,7 @@ import Turnstile from 'react-turnstile'; ...@@ -5,7 +5,7 @@ import Turnstile from 'react-turnstile';
const PasswordResetForm = () => { const PasswordResetForm = () => {
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
email: '' email: '',
}); });
const { email } = inputs; const { email } = inputs;
...@@ -31,7 +31,7 @@ const PasswordResetForm = () => { ...@@ -31,7 +31,7 @@ const PasswordResetForm = () => {
function handleChange(e) { function handleChange(e) {
const { name, value } = e.target; const { name, value } = e.target;
setInputs(inputs => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
} }
async function handleSubmit(e) { async function handleSubmit(e) {
...@@ -43,7 +43,7 @@ const PasswordResetForm = () => { ...@@ -43,7 +43,7 @@ const PasswordResetForm = () => {
} }
setLoading(true); setLoading(true);
const res = await API.get( const res = await API.get(
`/api/reset_password?email=${email}&turnstile=${turnstileToken}` `/api/reset_password?email=${email}&turnstile=${turnstileToken}`,
); );
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
...@@ -56,19 +56,19 @@ const PasswordResetForm = () => { ...@@ -56,19 +56,19 @@ const PasswordResetForm = () => {
} }
return ( return (
<Grid textAlign="center" style={{ marginTop: '48px' }}> <Grid textAlign='center' style={{ marginTop: '48px' }}>
<Grid.Column style={{ maxWidth: 450 }}> <Grid.Column style={{ maxWidth: 450 }}>
<Header as="h2" color="" textAlign="center"> <Header as='h2' color='' textAlign='center'>
<Image src="/logo.png" /> 密码重置 <Image src='/logo.png' /> 密码重置
</Header> </Header>
<Form size="large"> <Form size='large'>
<Segment> <Segment>
<Form.Input <Form.Input
fluid fluid
icon="mail" icon='mail'
iconPosition="left" iconPosition='left'
placeholder="邮箱地址" placeholder='邮箱地址'
name="email" name='email'
value={email} value={email}
onChange={handleChange} onChange={handleChange}
/> />
...@@ -83,9 +83,9 @@ const PasswordResetForm = () => { ...@@ -83,9 +83,9 @@ const PasswordResetForm = () => {
<></> <></>
)} )}
<Button <Button
color="green" color='green'
fluid fluid
size="large" size='large'
onClick={handleSubmit} onClick={handleSubmit}
loading={loading} loading={loading}
disabled={disableButton} disabled={disableButton}
......
...@@ -2,10 +2,9 @@ import { Navigate } from 'react-router-dom'; ...@@ -2,10 +2,9 @@ import { Navigate } from 'react-router-dom';
import { history } from '../helpers'; import { history } from '../helpers';
function PrivateRoute({ children }) { function PrivateRoute({ children }) {
if (!localStorage.getItem('user')) { if (!localStorage.getItem('user')) {
return <Navigate to="/login" state={{ from: history.location }} />; return <Navigate to='/login' state={{ from: history.location }} />;
} }
return children; return children;
} }
......
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Grid, Header, Image, Message, Segment } from 'semantic-ui-react'; import {
Button,
Form,
Grid,
Header,
Image,
Message,
Segment,
} from 'semantic-ui-react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers'; import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
import Turnstile from 'react-turnstile'; import Turnstile from 'react-turnstile';
...@@ -10,7 +18,7 @@ const RegisterForm = () => { ...@@ -10,7 +18,7 @@ const RegisterForm = () => {
password: '', password: '',
password2: '', password2: '',
email: '', email: '',
verification_code: '' verification_code: '',
}); });
const { username, password, password2 } = inputs; const { username, password, password2 } = inputs;
const [showEmailVerification, setShowEmailVerification] = useState(false); const [showEmailVerification, setShowEmailVerification] = useState(false);
...@@ -65,7 +73,7 @@ const RegisterForm = () => { ...@@ -65,7 +73,7 @@ const RegisterForm = () => {
inputs.aff_code = affCode; inputs.aff_code = affCode;
const res = await API.post( const res = await API.post(
`/api/user/register?turnstile=${turnstileToken}`, `/api/user/register?turnstile=${turnstileToken}`,
inputs inputs,
); );
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
...@@ -86,7 +94,7 @@ const RegisterForm = () => { ...@@ -86,7 +94,7 @@ const RegisterForm = () => {
} }
setLoading(true); setLoading(true);
const res = await API.get( const res = await API.get(
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}` `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
); );
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
...@@ -98,49 +106,49 @@ const RegisterForm = () => { ...@@ -98,49 +106,49 @@ const RegisterForm = () => {
}; };
return ( return (
<Grid textAlign="center" style={{ marginTop: '48px' }}> <Grid textAlign='center' style={{ marginTop: '48px' }}>
<Grid.Column style={{ maxWidth: 450 }}> <Grid.Column style={{ maxWidth: 450 }}>
<Header as="h2" color="" textAlign="center"> <Header as='h2' color='' textAlign='center'>
<Image src={logo} /> 新用户注 <Image src={logo} /> 新用户注
</Header> </Header>
<Form size="large"> <Form size='large'>
<Segment> <Segment>
<Form.Input <Form.Input
fluid fluid
icon="user" icon='user'
iconPosition="left" iconPosition='left'
placeholder="输入用户名,最长 12 位" placeholder='输入用户名,最长 12 位'
onChange={handleChange} onChange={handleChange}
name="username" name='username'
/> />
<Form.Input <Form.Input
fluid fluid
icon="lock" icon='lock'
iconPosition="left" iconPosition='left'
placeholder="输入密码,最短 8 位,最长 20 位" placeholder='输入密码,最短 8 位,最长 20 位'
onChange={handleChange} onChange={handleChange}
name="password" name='password'
type="password" type='password'
/> />
<Form.Input <Form.Input
fluid fluid
icon="lock" icon='lock'
iconPosition="left" iconPosition='left'
placeholder="输入密码,最短 8 位,最长 20 位" placeholder='输入密码,最短 8 位,最长 20 位'
onChange={handleChange} onChange={handleChange}
name="password2" name='password2'
type="password" type='password'
/> />
{showEmailVerification ? ( {showEmailVerification ? (
<> <>
<Form.Input <Form.Input
fluid fluid
icon="mail" icon='mail'
iconPosition="left" iconPosition='left'
placeholder="输入邮箱地址" placeholder='输入邮箱地址'
onChange={handleChange} onChange={handleChange}
name="email" name='email'
type="email" type='email'
action={ action={
<Button onClick={sendVerificationCode} disabled={loading}> <Button onClick={sendVerificationCode} disabled={loading}>
获取验证码 获取验证码
...@@ -149,11 +157,11 @@ const RegisterForm = () => { ...@@ -149,11 +157,11 @@ const RegisterForm = () => {
/> />
<Form.Input <Form.Input
fluid fluid
icon="lock" icon='lock'
iconPosition="left" iconPosition='left'
placeholder="输入验证码" placeholder='输入验证码'
onChange={handleChange} onChange={handleChange}
name="verification_code" name='verification_code'
/> />
</> </>
) : ( ) : (
...@@ -170,9 +178,9 @@ const RegisterForm = () => { ...@@ -170,9 +178,9 @@ const RegisterForm = () => {
<></> <></>
)} )}
<Button <Button
color="green" color='green'
fluid fluid
size="large" size='large'
onClick={handleSubmit} onClick={handleSubmit}
loading={loading} loading={loading}
> >
...@@ -182,7 +190,7 @@ const RegisterForm = () => { ...@@ -182,7 +190,7 @@ const RegisterForm = () => {
</Form> </Form>
<Message> <Message>
已有账户? 已有账户?
<Link to="/login" className="btn btn-link"> <Link to='/login' className='btn btn-link'>
点击登录 点击登录
</Link> </Link>
</Message> </Message>
......
...@@ -3,7 +3,14 @@ import { Link, useNavigate } from 'react-router-dom'; ...@@ -3,7 +3,14 @@ import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User'; import { UserContext } from '../context/User';
import { StatusContext } from '../context/Status'; import { StatusContext } from '../context/Status';
import { API, getLogo, getSystemName, isAdmin, isMobile, showError } from '../helpers'; import {
API,
getLogo,
getSystemName,
isAdmin,
isMobile,
showError,
} from '../helpers';
import '../index.css'; import '../index.css';
import { import {
...@@ -17,7 +24,7 @@ import { ...@@ -17,7 +24,7 @@ import {
IconKey, IconKey,
IconLayers, IconLayers,
IconSetting, IconSetting,
IconUser IconUser,
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
import { Layout, Nav } from '@douyinfe/semi-ui'; import { Layout, Nav } from '@douyinfe/semi-ui';
...@@ -26,7 +33,8 @@ import { Layout, Nav } from '@douyinfe/semi-ui'; ...@@ -26,7 +33,8 @@ import { Layout, Nav } from '@douyinfe/semi-ui';
const SiderBar = () => { const SiderBar = () => {
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
const [statusState, statusDispatch] = useContext(StatusContext); const [statusState, statusDispatch] = useContext(StatusContext);
const defaultIsCollapsed = isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true'; const defaultIsCollapsed =
isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true';
let navigate = useNavigate(); let navigate = useNavigate();
const [selectedKeys, setSelectedKeys] = useState(['home']); const [selectedKeys, setSelectedKeys] = useState(['home']);
...@@ -46,89 +54,105 @@ const SiderBar = () => { ...@@ -46,89 +54,105 @@ const SiderBar = () => {
setting: '/setting', setting: '/setting',
about: '/about', about: '/about',
chat: '/chat', chat: '/chat',
detail: '/detail' detail: '/detail',
}; };
const headerButtons = useMemo(() => [ const headerButtons = useMemo(
() => [
{ {
text: '首页', text: '首页',
itemKey: 'home', itemKey: 'home',
to: '/', to: '/',
icon: <IconHome /> icon: <IconHome />,
}, },
{ {
text: '渠道', text: '渠道',
itemKey: 'channel', itemKey: 'channel',
to: '/channel', to: '/channel',
icon: <IconLayers />, icon: <IconLayers />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle' className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
}, },
{ {
text: '聊天', text: '聊天',
itemKey: 'chat', itemKey: 'chat',
to: '/chat', to: '/chat',
icon: <IconComment />, icon: <IconComment />,
className: localStorage.getItem('chat_link') ? 'semi-navigation-item-normal' : 'tableHiddle' className: localStorage.getItem('chat_link')
? 'semi-navigation-item-normal'
: 'tableHiddle',
}, },
{ {
text: '令牌', text: '令牌',
itemKey: 'token', itemKey: 'token',
to: '/token', to: '/token',
icon: <IconKey /> icon: <IconKey />,
}, },
{ {
text: '兑换码', text: '兑换码',
itemKey: 'redemption', itemKey: 'redemption',
to: '/redemption', to: '/redemption',
icon: <IconGift />, icon: <IconGift />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle' className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
}, },
{ {
text: '钱包', text: '钱包',
itemKey: 'topup', itemKey: 'topup',
to: '/topup', to: '/topup',
icon: <IconCreditCard /> icon: <IconCreditCard />,
}, },
{ {
text: '用户管理', text: '用户管理',
itemKey: 'user', itemKey: 'user',
to: '/user', to: '/user',
icon: <IconUser />, icon: <IconUser />,
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle' className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
}, },
{ {
text: '日志', text: '日志',
itemKey: 'log', itemKey: 'log',
to: '/log', to: '/log',
icon: <IconHistogram /> icon: <IconHistogram />,
}, },
{ {
text: '数据看板', text: '数据看板',
itemKey: 'detail', itemKey: 'detail',
to: '/detail', to: '/detail',
icon: <IconCalendarClock />, icon: <IconCalendarClock />,
className: localStorage.getItem('enable_data_export') === 'true' ? 'semi-navigation-item-normal' : 'tableHiddle' className:
localStorage.getItem('enable_data_export') === 'true'
? 'semi-navigation-item-normal'
: 'tableHiddle',
}, },
{ {
text: '绘图', text: '绘图',
itemKey: 'midjourney', itemKey: 'midjourney',
to: '/midjourney', to: '/midjourney',
icon: <IconImage />, icon: <IconImage />,
className: localStorage.getItem('enable_drawing') === 'true' ? 'semi-navigation-item-normal' : 'tableHiddle' className:
localStorage.getItem('enable_drawing') === 'true'
? 'semi-navigation-item-normal'
: 'tableHiddle',
}, },
{ {
text: '设置', text: '设置',
itemKey: 'setting', itemKey: 'setting',
to: '/setting', to: '/setting',
icon: <IconSetting /> icon: <IconSetting />,
} },
// { // {
// text: '关于', // text: '关于',
// itemKey: 'about', // itemKey: 'about',
// to: '/about', // to: '/about',
// icon: <IconAt/> // icon: <IconAt/>
// } // }
], [localStorage.getItem('enable_data_export'), localStorage.getItem('enable_drawing'), localStorage.getItem('chat_link'), isAdmin()]); ],
[
localStorage.getItem('enable_data_export'),
localStorage.getItem('enable_drawing'),
localStorage.getItem('chat_link'),
isAdmin(),
],
);
const loadStatus = async () => { const loadStatus = async () => {
const res = await API.get('/api/status'); const res = await API.get('/api/status');
...@@ -143,8 +167,14 @@ const SiderBar = () => { ...@@ -143,8 +167,14 @@ const SiderBar = () => {
localStorage.setItem('display_in_currency', data.display_in_currency); localStorage.setItem('display_in_currency', data.display_in_currency);
localStorage.setItem('enable_drawing', data.enable_drawing); localStorage.setItem('enable_drawing', data.enable_drawing);
localStorage.setItem('enable_data_export', data.enable_data_export); localStorage.setItem('enable_data_export', data.enable_data_export);
localStorage.setItem('data_export_default_time', data.data_export_default_time); localStorage.setItem(
localStorage.setItem('default_collapse_sidebar', data.default_collapse_sidebar); 'data_export_default_time',
data.data_export_default_time,
);
localStorage.setItem(
'default_collapse_sidebar',
data.default_collapse_sidebar,
);
localStorage.setItem('mj_notify_enabled', data.mj_notify_enabled); localStorage.setItem('mj_notify_enabled', data.mj_notify_enabled);
if (data.chat_link) { if (data.chat_link) {
localStorage.setItem('chat_link', data.chat_link); localStorage.setItem('chat_link', data.chat_link);
...@@ -163,11 +193,14 @@ const SiderBar = () => { ...@@ -163,11 +193,14 @@ const SiderBar = () => {
useEffect(() => { useEffect(() => {
loadStatus().then(() => { loadStatus().then(() => {
setIsCollapsed(isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true'); setIsCollapsed(
isMobile() ||
localStorage.getItem('default_collapse_sidebar') === 'true',
);
}); });
let localKey = window.location.pathname.split('/')[1] let localKey = window.location.pathname.split('/')[1];
if (localKey === '') { if (localKey === '') {
localKey = 'home' localKey = 'home';
} }
setSelectedKeys([localKey]); setSelectedKeys([localKey]);
}, []); }, []);
...@@ -179,9 +212,12 @@ const SiderBar = () => { ...@@ -179,9 +212,12 @@ const SiderBar = () => {
<Nav <Nav
// bodyStyle={{ maxWidth: 200 }} // bodyStyle={{ maxWidth: 200 }}
style={{ maxWidth: 200 }} style={{ maxWidth: 200 }}
defaultIsCollapsed={isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true'} defaultIsCollapsed={
isMobile() ||
localStorage.getItem('default_collapse_sidebar') === 'true'
}
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
onCollapseChange={collapsed => { onCollapseChange={(collapsed) => {
setIsCollapsed(collapsed); setIsCollapsed(collapsed);
}} }}
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
...@@ -196,20 +232,20 @@ const SiderBar = () => { ...@@ -196,20 +232,20 @@ const SiderBar = () => {
); );
}} }}
items={headerButtons} items={headerButtons}
onSelect={key => { onSelect={(key) => {
setSelectedKeys([key.itemKey]); setSelectedKeys([key.itemKey]);
}} }}
header={{ header={{
logo: <img src={logo} alt="logo" style={{ marginRight: '0.75em' }} />, logo: (
text: systemName <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
),
text: systemName,
}} }}
// footer={{ // footer={{
// text: '© 2021 NekoAPI', // text: '© 2021 NekoAPI',
// }} // }}
> >
<Nav.Footer collapseButton={true}></Nav.Footer>
<Nav.Footer collapseButton={true}>
</Nav.Footer>
</Nav> </Nav>
</div> </div>
</Layout> </Layout>
......
...@@ -3,15 +3,27 @@ import { Icon } from '@douyinfe/semi-ui'; ...@@ -3,15 +3,27 @@ import { Icon } from '@douyinfe/semi-ui';
const WeChatIcon = () => { const WeChatIcon = () => {
function CustomIcon() { function CustomIcon() {
return <svg t="1709714447384" className="icon" viewBox="0 0 1024 1024" version="1.1" return (
xmlns="http://www.w3.org/2000/svg" p-id="5091" width="16" height="16"> <svg
t='1709714447384'
className='icon'
viewBox='0 0 1024 1024'
version='1.1'
xmlns='http://www.w3.org/2000/svg'
p-id='5091'
width='16'
height='16'
>
<path <path
d="M690.1 377.4c5.9 0 11.8 0.2 17.6 0.5-24.4-128.7-158.3-227.1-319.9-227.1C209 150.8 64 271.4 64 420.2c0 81.1 43.6 154.2 111.9 203.6 5.5 3.9 9.1 10.3 9.1 17.6 0 2.4-0.5 4.6-1.1 6.9-5.5 20.3-14.2 52.8-14.6 54.3-0.7 2.6-1.7 5.2-1.7 7.9 0 5.9 4.8 10.8 10.8 10.8 2.3 0 4.2-0.9 6.2-2l70.9-40.9c5.3-3.1 11-5 17.2-5 3.2 0 6.4 0.5 9.5 1.4 33.1 9.5 68.8 14.8 105.7 14.8 6 0 11.9-0.1 17.8-0.4-7.1-21-10.9-43.1-10.9-66 0-135.8 132.2-245.8 295.3-245.8z m-194.3-86.5c23.8 0 43.2 19.3 43.2 43.1s-19.3 43.1-43.2 43.1c-23.8 0-43.2-19.3-43.2-43.1s19.4-43.1 43.2-43.1z m-215.9 86.2c-23.8 0-43.2-19.3-43.2-43.1s19.3-43.1 43.2-43.1 43.2 19.3 43.2 43.1-19.4 43.1-43.2 43.1z" d='M690.1 377.4c5.9 0 11.8 0.2 17.6 0.5-24.4-128.7-158.3-227.1-319.9-227.1C209 150.8 64 271.4 64 420.2c0 81.1 43.6 154.2 111.9 203.6 5.5 3.9 9.1 10.3 9.1 17.6 0 2.4-0.5 4.6-1.1 6.9-5.5 20.3-14.2 52.8-14.6 54.3-0.7 2.6-1.7 5.2-1.7 7.9 0 5.9 4.8 10.8 10.8 10.8 2.3 0 4.2-0.9 6.2-2l70.9-40.9c5.3-3.1 11-5 17.2-5 3.2 0 6.4 0.5 9.5 1.4 33.1 9.5 68.8 14.8 105.7 14.8 6 0 11.9-0.1 17.8-0.4-7.1-21-10.9-43.1-10.9-66 0-135.8 132.2-245.8 295.3-245.8z m-194.3-86.5c23.8 0 43.2 19.3 43.2 43.1s-19.3 43.1-43.2 43.1c-23.8 0-43.2-19.3-43.2-43.1s19.4-43.1 43.2-43.1z m-215.9 86.2c-23.8 0-43.2-19.3-43.2-43.1s19.3-43.1 43.2-43.1 43.2 19.3 43.2 43.1-19.4 43.1-43.2 43.1z'
p-id="5092"></path> p-id='5092'
></path>
<path <path
d="M866.7 792.7c56.9-41.2 93.2-102 93.2-169.7 0-124-120.8-224.5-269.9-224.5-149 0-269.9 100.5-269.9 224.5S540.9 847.5 690 847.5c30.8 0 60.6-4.4 88.1-12.3 2.6-0.8 5.2-1.2 7.9-1.2 5.2 0 9.9 1.6 14.3 4.1l59.1 34c1.7 1 3.3 1.7 5.2 1.7 2.4 0 4.7-0.9 6.4-2.6 1.7-1.7 2.6-4 2.6-6.4 0-2.2-0.9-4.4-1.4-6.6-0.3-1.2-7.6-28.3-12.2-45.3-0.5-1.9-0.9-3.8-0.9-5.7 0.1-5.9 3.1-11.2 7.6-14.5zM600.2 587.2c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c0 19.8-16.2 35.9-36 35.9z m179.9 0c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c-0.1 19.8-16.2 35.9-36 35.9z" d='M866.7 792.7c56.9-41.2 93.2-102 93.2-169.7 0-124-120.8-224.5-269.9-224.5-149 0-269.9 100.5-269.9 224.5S540.9 847.5 690 847.5c30.8 0 60.6-4.4 88.1-12.3 2.6-0.8 5.2-1.2 7.9-1.2 5.2 0 9.9 1.6 14.3 4.1l59.1 34c1.7 1 3.3 1.7 5.2 1.7 2.4 0 4.7-0.9 6.4-2.6 1.7-1.7 2.6-4 2.6-6.4 0-2.2-0.9-4.4-1.4-6.6-0.3-1.2-7.6-28.3-12.2-45.3-0.5-1.9-0.9-3.8-0.9-5.7 0.1-5.9 3.1-11.2 7.6-14.5zM600.2 587.2c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c0 19.8-16.2 35.9-36 35.9z m179.9 0c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c-0.1 19.8-16.2 35.9-36 35.9z'
p-id="5093"></path> p-id='5093'
</svg>; ></path>
</svg>
);
} }
return ( return (
......
...@@ -15,6 +15,6 @@ export async function onGitHubOAuthClicked(github_client_id) { ...@@ -15,6 +15,6 @@ export async function onGitHubOAuthClicked(github_client_id) {
const state = await getOAuthState(); const state = await getOAuthState();
if (!state) return; if (!state) return;
window.open( 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`,
); );
} }
export const CHANNEL_OPTIONS = [ export const CHANNEL_OPTIONS = [
{key: 1, text: 'OpenAI', value: 1, color: 'green', label: 'OpenAI'}, { 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: 2,
{key: 4, text: 'Ollama', value: 4, color: 'grey', label: 'Ollama'}, text: 'Midjourney Proxy',
{key: 14, text: 'Anthropic Claude', value: 14, color: 'indigo', label: 'Anthropic Claude'}, value: 2,
{key: 3, text: 'Azure OpenAI', value: 3, color: 'teal', label: 'Azure OpenAI'}, color: 'light-blue',
{key: 11, text: 'Google PaLM2', value: 11, color: 'orange', label: 'Google PaLM2'}, label: 'Midjourney Proxy',
{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: 5,
{key: 18, text: '讯飞星火认知', value: 18, color: 'blue', label: '讯飞星火认知'}, text: 'Midjourney Proxy Plus',
{key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet', label: '智谱 ChatGLM'}, value: 5,
{key: 16, text: '智谱 GLM-4V', value: 26, color: 'purple', label: '智谱 GLM-4V'}, color: 'blue',
{key: 16, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot'}, label: 'Midjourney Proxy Plus',
{key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑'}, },
{key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元'}, { key: 4, text: 'Ollama', value: 4, color: 'grey', label: 'Ollama' },
{key: 31, text: '零一万物', value: 31, color: 'green', label: '零一万物'}, {
{key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道'}, key: 14,
{key: 22, text: '知识库:FastGPT', value: 22, color: 'blue', label: '知识库:FastGPT'}, text: 'Anthropic Claude',
{key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple', label: '知识库:AI Proxy'}, 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',
},
]; ];
...@@ -3,5 +3,5 @@ export const toastConstants = { ...@@ -3,5 +3,5 @@ export const toastConstants = {
INFO_TIMEOUT: 3000, INFO_TIMEOUT: 3000,
ERROR_TIMEOUT: 5000, ERROR_TIMEOUT: 5000,
WARNING_TIMEOUT: 10000, WARNING_TIMEOUT: 10000,
NOTICE_TIMEOUT: 20000 NOTICE_TIMEOUT: 20000,
}; };
...@@ -15,5 +15,5 @@ export const userConstants = { ...@@ -15,5 +15,5 @@ export const userConstants = {
DELETE_REQUEST: 'USERS_DELETE_REQUEST', DELETE_REQUEST: 'USERS_DELETE_REQUEST',
DELETE_SUCCESS: 'USERS_DELETE_SUCCESS', DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
DELETE_FAILURE: 'USERS_DELETE_FAILURE' DELETE_FAILURE: 'USERS_DELETE_FAILURE',
}; };
// contexts/User/index.jsx // contexts/User/index.jsx
import React from "react" import React from 'react';
import { reducer, initialState } from "./reducer" import { reducer, initialState } from './reducer';
export const UserContext = React.createContext({ export const UserContext = React.createContext({
state: initialState, state: initialState,
dispatch: () => null dispatch: () => null,
}) });
export const UserProvider = ({ children }) => { export const UserProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialState) const [state, dispatch] = React.useReducer(reducer, initialState);
return ( return (
<UserContext.Provider value={[ state, dispatch ]}> <UserContext.Provider value={[state, dispatch]}>
{ children } {children}
</UserContext.Provider> </UserContext.Provider>
) );
} };
\ No newline at end of file
...@@ -3,12 +3,12 @@ export const reducer = (state, action) => { ...@@ -3,12 +3,12 @@ export const reducer = (state, action) => {
case 'login': case 'login':
return { return {
...state, ...state,
user: action.payload user: action.payload,
}; };
case 'logout': case 'logout':
return { return {
...state, ...state,
user: undefined user: undefined,
}; };
default: default:
...@@ -17,5 +17,5 @@ export const reducer = (state, action) => { ...@@ -17,5 +17,5 @@ export const reducer = (state, action) => {
}; };
export const initialState = { export const initialState = {
user: undefined user: undefined,
}; };
...@@ -2,12 +2,14 @@ import { showError } from './utils'; ...@@ -2,12 +2,14 @@ import { showError } from './utils';
import axios from 'axios'; import axios from 'axios';
export const API = axios.create({ 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( API.interceptors.response.use(
(response) => response, (response) => response,
(error) => { (error) => {
showError(error); showError(error);
} },
); );
...@@ -3,7 +3,7 @@ export function authHeader() { ...@@ -3,7 +3,7 @@ export function authHeader() {
let user = JSON.parse(localStorage.getItem('user')); let user = JSON.parse(localStorage.getItem('user'));
if (user && user.token) { if (user && user.token) {
return { 'Authorization': 'Bearer ' + user.token }; return { Authorization: 'Bearer ' + user.token };
} else { } else {
return {}; return {};
} }
......
import {Label} from 'semantic-ui-react'; import { Label } from 'semantic-ui-react';
import {Tag} from "@douyinfe/semi-ui"; import { Tag } from '@douyinfe/semi-ui';
export function renderText(text, limit) { export function renderText(text, limit) {
if (text.length > limit) { if (text.length > limit) {
...@@ -14,20 +14,34 @@ export function renderGroup(group) { ...@@ -14,20 +14,34 @@ export function renderGroup(group) {
} }
let groups = group.split(','); let groups = group.split(',');
groups.sort(); groups.sort();
return <> return (
<>
{groups.map((group) => { {groups.map((group) => {
if (group === 'vip' || group === 'pro') { if (group === 'vip' || group === 'pro') {
return <Tag size='large' color='yellow'>{group}</Tag>; return (
<Tag size='large' color='yellow'>
{group}
</Tag>
);
} else if (group === 'svip' || group === 'premium') { } else if (group === 'svip' || group === 'premium') {
return <Tag size='large' color='red'>{group}</Tag>; return (
<Tag size='large' color='red'>
{group}
</Tag>
);
} }
if (group === 'default') { if (group === 'default') {
return <Tag size='large'>{group}</Tag>; return <Tag size='large'>{group}</Tag>;
} else { } else {
return <Tag size='large' color={stringToColor(group)}>{group}</Tag>; return (
<Tag size='large' color={stringToColor(group)}>
{group}
</Tag>
);
} }
})} })}
</>; </>
);
} }
export function renderNumber(num) { export function renderNumber(num) {
...@@ -111,16 +125,29 @@ export function renderQuotaWithPrompt(quota, digits) { ...@@ -111,16 +125,29 @@ export function renderQuotaWithPrompt(quota, digits) {
return ''; return '';
} }
const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo', const colors = [
'light-blue', 'lime', 'orange', 'pink', 'amber',
'purple', 'red', 'teal', 'violet', 'yellow' 'blue',
] 'cyan',
'green',
'grey',
'indigo',
'light-blue',
'lime',
'orange',
'pink',
'purple',
'red',
'teal',
'violet',
'yellow',
];
export const modelColorMap = { export const modelColorMap = {
'dall-e': 'rgb(147,112,219)', // 深紫色 'dall-e': 'rgb(147,112,219)', // 深紫色
'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调 'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
'midjourney': 'rgb(136,43,180)', // 介于紫罗兰和洋红之间的色调 midjourney: 'rgb(136,43,180)', // 介于紫罗兰和洋红之间的色调
'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色 'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
...@@ -154,8 +181,8 @@ export const modelColorMap = { ...@@ -154,8 +181,8 @@ export const modelColorMap = {
'tts-1-1106': 'rgb(255,165,0)', // 橙色 'tts-1-1106': 'rgb(255,165,0)', // 橙色
'tts-1-hd': 'rgb(255,215,0)', // 金色 'tts-1-hd': 'rgb(255,215,0)', // 金色
'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别) 'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别)
'whisper-1': 'rgb(245,245,220)' // 米色 'whisper-1': 'rgb(245,245,220)', // 米色
} };
export function stringToColor(str) { export function stringToColor(str) {
let sum = 0; let sum = 0;
......
import { Toast } from '@douyinfe/semi-ui'; import { Toast } from '@douyinfe/semi-ui';
import { toastConstants } from '../constants'; import { toastConstants } from '../constants';
import React from 'react'; import React from 'react';
import {toast} from "react-toastify"; import { toast } from 'react-toastify';
const HTMLToastContent = ({ htmlContent }) => { const HTMLToastContent = ({ htmlContent }) => {
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />; return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
...@@ -30,7 +30,7 @@ export function getSystemName() { ...@@ -30,7 +30,7 @@ export function getSystemName() {
export function getLogo() { export function getLogo() {
let logo = localStorage.getItem('logo'); let logo = localStorage.getItem('logo');
if (!logo) return '/logo.png'; if (!logo) return '/logo.png';
return logo return logo;
} }
export function getFooterHTML() { export function getFooterHTML() {
...@@ -157,17 +157,7 @@ export function timestamp2string(timestamp) { ...@@ -157,17 +157,7 @@ export function timestamp2string(timestamp) {
second = '0' + second; second = '0' + second;
} }
return ( return (
year + year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second
'-' +
month +
'-' +
day +
' ' +
hour +
':' +
minute +
':' +
second
); );
} }
...@@ -186,9 +176,9 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') { ...@@ -186,9 +176,9 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') {
if (hour.length === 1) { if (hour.length === 1) {
hour = '0' + hour; hour = '0' + hour;
} }
let str = month + '-' + day let str = month + '-' + day;
if (dataExportDefaultTime === 'hour') { if (dataExportDefaultTime === 'hour') {
str += ' ' + hour + ":00" str += ' ' + hour + ':00';
} else if (dataExportDefaultTime === 'week') { } else if (dataExportDefaultTime === 'week') {
let nextWeek = new Date(timestamp * 1000 + 6 * 24 * 60 * 60 * 1000); let nextWeek = new Date(timestamp * 1000 + 6 * 24 * 60 * 60 * 1000);
let nextMonth = (nextWeek.getMonth() + 1).toString(); let nextMonth = (nextWeek.getMonth() + 1).toString();
...@@ -199,7 +189,7 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') { ...@@ -199,7 +189,7 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') {
if (nextDay.length === 1) { if (nextDay.length === 1) {
nextDay = '0' + nextDay; nextDay = '0' + nextDay;
} }
str += ' - ' + nextMonth + '-' + nextDay str += ' - ' + nextMonth + '-' + nextDay;
} }
return str; return str;
} }
...@@ -225,7 +215,6 @@ export const verifyJSON = (str) => { ...@@ -225,7 +215,6 @@ export const verifyJSON = (str) => {
export function shouldShowPrompt(id) { export function shouldShowPrompt(id) {
let prompt = localStorage.getItem(`prompt-${id}`); let prompt = localStorage.getItem(`prompt-${id}`);
return !prompt; return !prompt;
} }
export function setPromptShown(id) { export function setPromptShown(id) {
......
...@@ -2,12 +2,13 @@ body { ...@@ -2,12 +2,13 @@ body {
margin: 0; margin: 0;
padding-top: 55px; padding-top: 55px;
overflow-y: scroll; overflow-y: scroll;
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif; font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei',
sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
scrollbar-width: none; scrollbar-width: none;
color: var(--semi-color-text-0) !important; color: var(--semi-color-text-0) !important;
background-color: var( --semi-color-bg-0) !important; background-color: var(--semi-color-bg-0) !important;
height: 100%; height: 100%;
} }
...@@ -16,16 +17,18 @@ body { ...@@ -16,16 +17,18 @@ body {
} }
@media only screen and (max-width: 767px) { @media only screen and (max-width: 767px) {
.semi-table-tbody, .semi-table-row, .semi-table-row-cell { .semi-table-tbody,
display: block!important; .semi-table-row,
width: auto!important; .semi-table-row-cell {
padding: 2px!important; display: block !important;
width: auto !important;
padding: 2px !important;
} }
.semi-table-row-cell { .semi-table-row-cell {
border-bottom: 0!important; border-bottom: 0 !important;
} }
.semi-table-tbody>.semi-table-row { .semi-table-tbody > .semi-table-row {
border-bottom: 1px solid rgba(0,0,0,.1); border-bottom: 1px solid rgba(0, 0, 0, 0.1);
} }
.semi-space { .semi-space {
/*display: block!important;*/ /*display: block!important;*/
...@@ -64,7 +67,8 @@ body::-webkit-scrollbar { ...@@ -64,7 +67,8 @@ body::-webkit-scrollbar {
} }
code { 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 { .semi-navigation-vertical {
......
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import {BrowserRouter} from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import App from './App'; import App from './App';
import HeaderBar from './components/HeaderBar'; import HeaderBar from './components/HeaderBar';
import Footer from './components/Footer'; import Footer from './components/Footer';
import 'semantic-ui-css/semantic.min.css'; import 'semantic-ui-offline/semantic.min.css';
import './index.css'; import './index.css';
import {UserProvider} from './context/User'; import { UserProvider } from './context/User';
import {ToastContainer} from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import {StatusProvider} from './context/Status'; import { StatusProvider } from './context/Status';
import {Layout} from "@douyinfe/semi-ui"; import { Layout } from '@douyinfe/semi-ui';
import SiderBar from "./components/SiderBar"; import SiderBar from './components/SiderBar';
// initialization // initialization
initVChartSemiTheme({
isWatchingThemeSwitch: true,
});
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
const {Sider, Content, Header} = Layout; const { Sider, Content, Header } = Layout;
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<StatusProvider> <StatusProvider>
...@@ -28,27 +24,27 @@ root.render( ...@@ -28,27 +24,27 @@ root.render(
<BrowserRouter> <BrowserRouter>
<Layout> <Layout>
<Sider> <Sider>
<SiderBar/> <SiderBar />
</Sider> </Sider>
<Layout> <Layout>
<Header> <Header>
<HeaderBar/> <HeaderBar />
</Header> </Header>
<Content <Content
style={{ style={{
padding: '24px', padding: '24px',
}} }}
> >
<App/> <App />
</Content> </Content>
<Layout.Footer> <Layout.Footer>
<Footer></Footer> <Footer></Footer>
</Layout.Footer> </Layout.Footer>
</Layout> </Layout>
<ToastContainer/> <ToastContainer />
</Layout> </Layout>
</BrowserRouter> </BrowserRouter>
</UserProvider> </UserProvider>
</StatusProvider> </StatusProvider>
</React.StrictMode> </React.StrictMode>,
); );
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { API, showError } from '../../helpers'; import { API, showError } from '../../helpers';
import { marked } from 'marked'; import { marked } from 'marked';
import {Layout} from "@douyinfe/semi-ui"; import { Layout } from '@douyinfe/semi-ui';
const About = () => { const About = () => {
const [about, setAbout] = useState(''); const [about, setAbout] = useState('');
...@@ -31,37 +31,42 @@ const About = () => { ...@@ -31,37 +31,42 @@ const About = () => {
return ( return (
<> <>
{ {aboutLoaded && about === '' ? (
aboutLoaded && about === '' ? <> <>
<Layout> <Layout>
<Layout.Header> <Layout.Header>
<h3>关于</h3> <h3>关于</h3>
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<p> <p>可在设置页面设置关于内容,支持 HTML & Markdown</p>
可在设置页面设置关于内容,支持 HTML & Markdown
</p>
new-api项目仓库地址: new-api项目仓库地址:
<a href='https://github.com/Calcium-Ion/new-api'> <a href='https://github.com/Calcium-Ion/new-api'>
https://github.com/Calcium-Ion/new-api https://github.com/Calcium-Ion/new-api
</a> </a>
<p> <p>
NewAPI © 2023 CalciumIon | 基于 One API v0.5.4 © 2023 JustSong。本项目根据MIT许可证授权。 NewAPI © 2023 CalciumIon | 基于 One API v0.5.4 © 2023
JustSong。本项目根据MIT许可证授权。
</p> </p>
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</> : <> </>
{ ) : (
about.startsWith('https://') ? <iframe <>
{about.startsWith('https://') ? (
<iframe
src={about} src={about}
style={{ width: '100%', height: '100vh', border: 'none' }} style={{ width: '100%', height: '100vh', border: 'none' }}
/> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div> />
} ) : (
<div
style={{ fontSize: 'larger' }}
dangerouslySetInnerHTML={{ __html: about }}
></div>
)}
</> </>
} )}
</> </>
); );
}; };
export default About; export default About;
import React from 'react'; import React from 'react';
import ChannelsTable from '../../components/ChannelsTable'; import ChannelsTable from '../../components/ChannelsTable';
import {Layout} from "@douyinfe/semi-ui"; import { Layout } from '@douyinfe/semi-ui';
const File = () => ( const File = () => (
<> <>
...@@ -9,7 +9,7 @@ const File = () => ( ...@@ -9,7 +9,7 @@ const File = () => (
<h3>管理渠道</h3> <h3>管理渠道</h3>
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<ChannelsTable/> <ChannelsTable />
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</> </>
......
...@@ -11,5 +11,4 @@ const Chat = () => { ...@@ -11,5 +11,4 @@ const Chat = () => {
); );
}; };
export default Chat; export default Chat;
...@@ -53,8 +53,7 @@ const Home = () => { ...@@ -53,8 +53,7 @@ const Home = () => {
}, []); }, []);
return ( return (
<> <>
{ {homePageContentLoaded && homePageContent === '' ? (
homePageContentLoaded && homePageContent === '' ?
<> <>
<Card <Card
bordered={false} bordered={false}
...@@ -66,15 +65,30 @@ const Home = () => { ...@@ -66,15 +65,30 @@ const Home = () => {
<Col span={12}> <Col span={12}>
<Card <Card
title='系统信息' title='系统信息'
headerExtraContent={<span headerExtraContent={
style={{ fontSize: '12px', color: 'var(--semi-color-text-1)' }}>系统信息总览</span>}> <span
style={{
fontSize: '12px',
color: 'var(--semi-color-text-1)',
}}
>
系统信息总览
</span>
}
>
<p>名称:{statusState?.status?.system_name}</p> <p>名称:{statusState?.status?.system_name}</p>
<p>版本:{statusState?.status?.version ? statusState?.status?.version : 'unknown'}</p> <p>
版本:
{statusState?.status?.version
? statusState?.status?.version
: 'unknown'}
</p>
<p> <p>
源码: 源码:
<a <a
href='https://github.com/songquanpeng/one-api' href='https://github.com/songquanpeng/one-api'
target='_blank' rel='noreferrer' target='_blank'
rel='noreferrer'
> >
https://github.com/songquanpeng/one-api https://github.com/songquanpeng/one-api
</a> </a>
...@@ -85,44 +99,67 @@ const Home = () => { ...@@ -85,44 +99,67 @@ const Home = () => {
<Col span={12}> <Col span={12}>
<Card <Card
title='系统配置' title='系统配置'
headerExtraContent={<span headerExtraContent={
style={{ fontSize: '12px', color: 'var(--semi-color-text-1)' }}>系统配置总览</span>}> <span
style={{
fontSize: '12px',
color: 'var(--semi-color-text-1)',
}}
>
系统配置总览
</span>
}
>
<p> <p>
邮箱验证: 邮箱验证:
{statusState?.status?.email_verification === true ? '已启用' : '未启用'} {statusState?.status?.email_verification === true
? '已启用'
: '未启用'}
</p> </p>
<p> <p>
GitHub 身份验证: GitHub 身份验证:
{statusState?.status?.github_oauth === true ? '已启用' : '未启用'} {statusState?.status?.github_oauth === true
? '已启用'
: '未启用'}
</p> </p>
<p> <p>
微信身份验证: 微信身份验证:
{statusState?.status?.wechat_login === true ? '已启用' : '未启用'} {statusState?.status?.wechat_login === true
? '已启用'
: '未启用'}
</p> </p>
<p> <p>
Turnstile 用户校验: Turnstile 用户校验:
{statusState?.status?.turnstile_check === true ? '已启用' : '未启用'} {statusState?.status?.turnstile_check === true
? '已启用'
: '未启用'}
</p> </p>
<p> <p>
Telegram 身份验证: Telegram 身份验证:
{statusState?.status?.telegram_oauth === true {statusState?.status?.telegram_oauth === true
? '已启用' : '未启用'} ? '已启用'
: '未启用'}
</p> </p>
</Card> </Card>
</Col> </Col>
</Row> </Row>
</Card> </Card>
</> </>
: <> ) : (
{ <>
homePageContent.startsWith('https://') ? {homePageContent.startsWith('https://') ? (
<iframe src={homePageContent} style={{ width: '100%', height: '100vh', border: 'none' }} /> : <iframe
<div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: homePageContent }}></div> src={homePageContent}
} style={{ width: '100%', height: '100vh', border: 'none' }}
/>
) : (
<div
style={{ fontSize: 'larger' }}
dangerouslySetInnerHTML={{ __html: homePageContent }}
></div>
)}
</> </>
} )}
</> </>
); );
}; };
......
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; 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 { 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 Title from '@douyinfe/semi-ui/lib/es/typography/title';
import { Divider } from 'semantic-ui-react'; import { Divider } from 'semantic-ui-react';
...@@ -15,7 +30,7 @@ const EditRedemption = (props) => { ...@@ -15,7 +30,7 @@ const EditRedemption = (props) => {
const originInputs = { const originInputs = {
name: '', name: '',
quota: 100000, quota: 100000,
count: 1 count: 1,
}; };
const [inputs, setInputs] = useState(originInputs); const [inputs, setInputs] = useState(originInputs);
const { name, quota, count } = inputs; const { name, quota, count } = inputs;
...@@ -42,11 +57,9 @@ const EditRedemption = (props) => { ...@@ -42,11 +57,9 @@ const EditRedemption = (props) => {
useEffect(() => { useEffect(() => {
if (isEdit) { if (isEdit) {
loadRedemption().then( loadRedemption().then(() => {
() => {
// console.log(inputs); // console.log(inputs);
} });
);
} else { } else {
setInputs(originInputs); setInputs(originInputs);
} }
...@@ -60,10 +73,13 @@ const EditRedemption = (props) => { ...@@ -60,10 +73,13 @@ const EditRedemption = (props) => {
localInputs.quota = parseInt(localInputs.quota); localInputs.quota = parseInt(localInputs.quota);
let res; let res;
if (isEdit) { 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 { } else {
res = await API.post(`/api/redemption/`, { res = await API.post(`/api/redemption/`, {
...localInputs ...localInputs,
}); });
} }
const { success, message, data } = res.data; const { success, message, data } = res.data;
...@@ -97,7 +113,7 @@ const EditRedemption = (props) => { ...@@ -97,7 +113,7 @@ const EditRedemption = (props) => {
), ),
onOk: () => { onOk: () => {
downloadTextAsFile(text, `${inputs.name}.txt`); downloadTextAsFile(text, `${inputs.name}.txt`);
} },
}); });
} }
setLoading(false); setLoading(false);
...@@ -107,15 +123,28 @@ const EditRedemption = (props) => { ...@@ -107,15 +123,28 @@ const EditRedemption = (props) => {
<> <>
<SideSheet <SideSheet
placement={isEdit ? 'right' : 'left'} placement={isEdit ? 'right' : 'left'}
title={<Title level={3}>{isEdit ? '更新兑换码信息' : '创建新的兑换码'}</Title>} title={
<Title level={3}>
{isEdit ? '更新兑换码信息' : '创建新的兑换码'}
</Title>
}
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
visible={props.visiable} visible={props.visiable}
footer={ footer={
<div style={{ display: 'flex', justifyContent: 'flex-end' }}> <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space> <Space>
<Button theme="solid" size={'large'} onClick={submit}>提交</Button> <Button theme='solid' size={'large'} onClick={submit}>
<Button theme="solid" size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button> 提交
</Button>
<Button
theme='solid'
size={'large'}
type={'tertiary'}
onClick={handleCancel}
>
取消
</Button>
</Space> </Space>
</div> </div>
} }
...@@ -126,12 +155,12 @@ const EditRedemption = (props) => { ...@@ -126,12 +155,12 @@ const EditRedemption = (props) => {
<Spin spinning={loading}> <Spin spinning={loading}>
<Input <Input
style={{ marginTop: 20 }} style={{ marginTop: 20 }}
label="名称" label='名称'
name="name" name='name'
placeholder={'请输入名称'} placeholder={'请输入名称'}
onChange={value => handleInputChange('name', value)} onChange={(value) => handleInputChange('name', value)}
value={name} value={name}
autoComplete="new-password" autoComplete='new-password'
required={!isEdit} required={!isEdit}
/> />
<Divider /> <Divider />
...@@ -140,12 +169,12 @@ const EditRedemption = (props) => { ...@@ -140,12 +169,12 @@ const EditRedemption = (props) => {
</div> </div>
<AutoComplete <AutoComplete
style={{ marginTop: 8 }} style={{ marginTop: 8 }}
name="quota" name='quota'
placeholder={'请输入额度'} placeholder={'请输入额度'}
onChange={(value) => handleInputChange('quota', value)} onChange={(value) => handleInputChange('quota', value)}
value={quota} value={quota}
autoComplete="new-password" autoComplete='new-password'
type="number" type='number'
position={'bottom'} position={'bottom'}
data={[ data={[
{ value: 500000, label: '1$' }, { value: 500000, label: '1$' },
...@@ -153,25 +182,25 @@ const EditRedemption = (props) => { ...@@ -153,25 +182,25 @@ const EditRedemption = (props) => {
{ value: 25000000, label: '50$' }, { value: 25000000, label: '50$' },
{ value: 50000000, label: '100$' }, { value: 50000000, label: '100$' },
{ value: 250000000, label: '500$' }, { value: 250000000, label: '500$' },
{ value: 500000000, label: '1000$' } { value: 500000000, label: '1000$' },
]} ]}
/> />
{ {!isEdit && (
!isEdit && <> <>
<Divider /> <Divider />
<Typography.Text>生成数量</Typography.Text> <Typography.Text>生成数量</Typography.Text>
<Input <Input
style={{ marginTop: 8 }} style={{ marginTop: 8 }}
label="生成数量" label='生成数量'
name="count" name='count'
placeholder={'请输入生成数量'} placeholder={'请输入生成数量'}
onChange={value => handleInputChange('count', value)} onChange={(value) => handleInputChange('count', value)}
value={count} value={count}
autoComplete="new-password" autoComplete='new-password'
type="number" type='number'
/> />
</> </>
} )}
</Spin> </Spin>
</SideSheet> </SideSheet>
</> </>
......
import React from 'react'; import React from 'react';
import RedemptionsTable from '../../components/RedemptionsTable'; import RedemptionsTable from '../../components/RedemptionsTable';
import {Layout} from "@douyinfe/semi-ui"; import { Layout } from '@douyinfe/semi-ui';
const Redemption = () => ( const Redemption = () => (
<> <>
...@@ -9,7 +9,7 @@ const Redemption = () => ( ...@@ -9,7 +9,7 @@ const Redemption = () => (
<h3>管理兑换码</h3> <h3>管理兑换码</h3>
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<RedemptionsTable/> <RedemptionsTable />
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</> </>
......
import React from 'react'; import React from 'react';
import SystemSetting from '../../components/SystemSetting'; import SystemSetting from '../../components/SystemSetting';
import {isRoot} from '../../helpers'; import { isRoot } from '../../helpers';
import OtherSetting from '../../components/OtherSetting'; import OtherSetting from '../../components/OtherSetting';
import PersonalSetting from '../../components/PersonalSetting'; import PersonalSetting from '../../components/PersonalSetting';
import OperationSetting from '../../components/OperationSetting'; import OperationSetting from '../../components/OperationSetting';
import {Layout, TabPane, Tabs} from "@douyinfe/semi-ui"; import { Layout, TabPane, Tabs } from '@douyinfe/semi-ui';
const Setting = () => { const Setting = () => {
let panes = [ let panes = [
{ {
tab: '个人设置', tab: '个人设置',
content: <PersonalSetting/>, content: <PersonalSetting />,
itemKey: '1' itemKey: '1',
} },
]; ];
if (isRoot()) { if (isRoot()) {
panes.push({ panes.push({
tab: '运营设置', tab: '运营设置',
content: <OperationSetting/>, content: <OperationSetting />,
itemKey: '2' itemKey: '2',
}); });
panes.push({ panes.push({
tab: '系统设置', tab: '系统设置',
content: <SystemSetting/>, content: <SystemSetting />,
itemKey: '3' itemKey: '3',
}); });
panes.push({ panes.push({
tab: '其他设置', tab: '其他设置',
content: <OtherSetting/>, content: <OtherSetting />,
itemKey: '4' itemKey: '4',
}); });
} }
...@@ -37,8 +37,8 @@ const Setting = () => { ...@@ -37,8 +37,8 @@ const Setting = () => {
<div> <div>
<Layout> <Layout>
<Layout.Content> <Layout.Content>
<Tabs type="line" defaultActiveKey="1"> <Tabs type='line' defaultActiveKey='1'>
{panes.map(pane => ( {panes.map((pane) => (
<TabPane itemKey={pane.itemKey} tab={pane.tab}> <TabPane itemKey={pane.itemKey} tab={pane.tab}>
{pane.content} {pane.content}
</TabPane> </TabPane>
......
import React from 'react'; import React from 'react';
import TokensTable from '../../components/TokensTable'; import TokensTable from '../../components/TokensTable';
import {Layout} from "@douyinfe/semi-ui"; import { Layout } from '@douyinfe/semi-ui';
const Token = () => ( const Token = () => (
<> <>
<Layout> <Layout>
...@@ -8,7 +8,7 @@ const Token = () => ( ...@@ -8,7 +8,7 @@ const Token = () => (
<h3>我的令牌</h3> <h3>我的令牌</h3>
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<TokensTable/> <TokensTable />
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</> </>
......
...@@ -7,7 +7,7 @@ const AddUser = (props) => { ...@@ -7,7 +7,7 @@ const AddUser = (props) => {
const originInputs = { const originInputs = {
username: '', username: '',
display_name: '', display_name: '',
password: '' password: '',
}; };
const [inputs, setInputs] = useState(originInputs); const [inputs, setInputs] = useState(originInputs);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
...@@ -48,8 +48,17 @@ const AddUser = (props) => { ...@@ -48,8 +48,17 @@ const AddUser = (props) => {
footer={ footer={
<div style={{ display: 'flex', justifyContent: 'flex-end' }}> <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space> <Space>
<Button theme="solid" size={'large'} onClick={submit}>提交</Button> <Button theme='solid' size={'large'} onClick={submit}>
<Button theme="solid" size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button> 提交
</Button>
<Button
theme='solid'
size={'large'}
type={'tertiary'}
onClick={handleCancel}
>
取消
</Button>
</Space> </Space>
</div> </div>
} }
...@@ -60,34 +69,34 @@ const AddUser = (props) => { ...@@ -60,34 +69,34 @@ const AddUser = (props) => {
<Spin spinning={loading}> <Spin spinning={loading}>
<Input <Input
style={{ marginTop: 20 }} style={{ marginTop: 20 }}
label="用户名" label='用户名'
name="username" name='username'
addonBefore={'用户名'} addonBefore={'用户名'}
placeholder={'请输入用户名'} placeholder={'请输入用户名'}
onChange={value => handleInputChange('username', value)} onChange={(value) => handleInputChange('username', value)}
value={username} value={username}
autoComplete="off" autoComplete='off'
/> />
<Input <Input
style={{ marginTop: 20 }} style={{ marginTop: 20 }}
addonBefore={'显示名'} addonBefore={'显示名'}
label="显示名称" label='显示名称'
name="display_name" name='display_name'
autoComplete="off" autoComplete='off'
placeholder={'请输入显示名称'} placeholder={'请输入显示名称'}
onChange={value => handleInputChange('display_name', value)} onChange={(value) => handleInputChange('display_name', value)}
value={display_name} value={display_name}
/> />
<Input <Input
style={{ marginTop: 20 }} style={{ marginTop: 20 }}
label="密 码" label='密 码'
name="password" name='password'
type={'password'} type={'password'}
addonBefore={'密码'} addonBefore={'密码'}
placeholder={'请输入密码'} placeholder={'请输入密码'}
onChange={value => handleInputChange('password', value)} onChange={(value) => handleInputChange('password', value)}
value={password} value={password}
autoComplete="off" autoComplete='off'
/> />
</Spin> </Spin>
</SideSheet> </SideSheet>
......
...@@ -3,7 +3,16 @@ import { useNavigate } from 'react-router-dom'; ...@@ -3,7 +3,16 @@ import { useNavigate } from 'react-router-dom';
import { API, isMobile, showError, showSuccess } from '../../helpers'; import { API, isMobile, showError, showSuccess } from '../../helpers';
import { renderQuotaWithPrompt } from '../../helpers/render'; import { renderQuotaWithPrompt } from '../../helpers/render';
import Title from '@douyinfe/semi-ui/lib/es/typography/title'; import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import { Button, Divider, Input, Select, SideSheet, Space, Spin, Typography } from '@douyinfe/semi-ui'; import {
Button,
Divider,
Input,
Select,
SideSheet,
Space,
Spin,
Typography,
} from '@douyinfe/semi-ui';
const EditUser = (props) => { const EditUser = (props) => {
const userId = props.editingUser.id; const userId = props.editingUser.id;
...@@ -16,21 +25,32 @@ const EditUser = (props) => { ...@@ -16,21 +25,32 @@ const EditUser = (props) => {
wechat_id: '', wechat_id: '',
email: '', email: '',
quota: 0, quota: 0,
group: 'default' group: 'default',
}); });
const [groupOptions, setGroupOptions] = useState([]); const [groupOptions, setGroupOptions] = useState([]);
const { username, display_name, password, github_id, wechat_id, telegram_id, email, quota, group } = const {
inputs; username,
display_name,
password,
github_id,
wechat_id,
telegram_id,
email,
quota,
group,
} = inputs;
const handleInputChange = (name, value) => { const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
}; };
const fetchGroups = async () => { const fetchGroups = async () => {
try { try {
let res = await API.get(`/api/group/`); let res = await API.get(`/api/group/`);
setGroupOptions(res.data.data.map((group) => ({ setGroupOptions(
res.data.data.map((group) => ({
label: group, label: group,
value: group value: group,
}))); })),
);
} catch (error) { } catch (error) {
showError(error.message); showError(error.message);
} }
...@@ -98,8 +118,17 @@ const EditUser = (props) => { ...@@ -98,8 +118,17 @@ const EditUser = (props) => {
footer={ footer={
<div style={{ display: 'flex', justifyContent: 'flex-end' }}> <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space> <Space>
<Button theme="solid" size={'large'} onClick={submit}>提交</Button> <Button theme='solid' size={'large'} onClick={submit}>
<Button theme="solid" size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button> 提交
</Button>
<Button
theme='solid'
size={'large'}
type={'tertiary'}
onClick={handleCancel}
>
取消
</Button>
</Space> </Space>
</div> </div>
} }
...@@ -112,103 +141,103 @@ const EditUser = (props) => { ...@@ -112,103 +141,103 @@ const EditUser = (props) => {
<Typography.Text>用户名</Typography.Text> <Typography.Text>用户名</Typography.Text>
</div> </div>
<Input <Input
label="用户名" label='用户名'
name="username" name='username'
placeholder={'请输入新的用户名'} placeholder={'请输入新的用户名'}
onChange={value => handleInputChange('username', value)} onChange={(value) => handleInputChange('username', value)}
value={username} value={username}
autoComplete="new-password" autoComplete='new-password'
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>密码</Typography.Text> <Typography.Text>密码</Typography.Text>
</div> </div>
<Input <Input
label="密码" label='密码'
name="password" name='password'
type={'password'} type={'password'}
placeholder={'请输入新的密码,最短 8 位'} placeholder={'请输入新的密码,最短 8 位'}
onChange={value => handleInputChange('password', value)} onChange={(value) => handleInputChange('password', value)}
value={password} value={password}
autoComplete="new-password" autoComplete='new-password'
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>显示名称</Typography.Text> <Typography.Text>显示名称</Typography.Text>
</div> </div>
<Input <Input
label="显示名称" label='显示名称'
name="display_name" name='display_name'
placeholder={'请输入新的显示名称'} placeholder={'请输入新的显示名称'}
onChange={value => handleInputChange('display_name', value)} onChange={(value) => handleInputChange('display_name', value)}
value={display_name} value={display_name}
autoComplete="new-password" autoComplete='new-password'
/> />
{ {userId && (
userId && <> <>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>分组</Typography.Text> <Typography.Text>分组</Typography.Text>
</div> </div>
<Select <Select
placeholder={'请选择分组'} placeholder={'请选择分组'}
name="group" name='group'
fluid fluid
search search
selection selection
allowAdditions allowAdditions
additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'} additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
onChange={value => handleInputChange('group', value)} onChange={(value) => handleInputChange('group', value)}
value={inputs.group} value={inputs.group}
autoComplete="new-password" autoComplete='new-password'
optionList={groupOptions} optionList={groupOptions}
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>{`剩余额度${renderQuotaWithPrompt(quota)}`}</Typography.Text> <Typography.Text>{`剩余额度${renderQuotaWithPrompt(quota)}`}</Typography.Text>
</div> </div>
<Input <Input
name="quota" name='quota'
placeholder={'请输入新的剩余额度'} placeholder={'请输入新的剩余额度'}
onChange={value => handleInputChange('quota', value)} onChange={(value) => handleInputChange('quota', value)}
value={quota} value={quota}
type={'number'} type={'number'}
autoComplete="new-password" autoComplete='new-password'
/> />
</> </>
} )}
<Divider style={{ marginTop: 20 }}>以下信息不可修改</Divider> <Divider style={{ marginTop: 20 }}>以下信息不可修改</Divider>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的 GitHub 账户</Typography.Text> <Typography.Text>已绑定的 GitHub 账户</Typography.Text>
</div> </div>
<Input <Input
name="github_id" name='github_id'
value={github_id} value={github_id}
autoComplete="new-password" autoComplete='new-password'
placeholder="此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改" placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly readonly
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的微信账户</Typography.Text> <Typography.Text>已绑定的微信账户</Typography.Text>
</div> </div>
<Input <Input
name="wechat_id" name='wechat_id'
value={wechat_id} value={wechat_id}
autoComplete="new-password" autoComplete='new-password'
placeholder="此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改" placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly readonly
/> />
<Input <Input
name="telegram_id" name='telegram_id'
value={telegram_id} value={telegram_id}
autoComplete="new-password" autoComplete='new-password'
placeholder="此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改" placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly readonly
/> />
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的邮箱账户</Typography.Text> <Typography.Text>已绑定的邮箱账户</Typography.Text>
</div> </div>
<Input <Input
name="email" name='email'
value={email} value={email}
autoComplete="new-password" autoComplete='new-password'
placeholder="此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改" placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly readonly
/> />
</Spin> </Spin>
......
import React from 'react'; import React from 'react';
import UsersTable from '../../components/UsersTable'; import UsersTable from '../../components/UsersTable';
import {Layout} from "@douyinfe/semi-ui"; import { Layout } from '@douyinfe/semi-ui';
const User = () => ( const User = () => (
<> <>
...@@ -9,7 +9,7 @@ const User = () => ( ...@@ -9,7 +9,7 @@ const User = () => (
<h3>管理用户</h3> <h3>管理用户</h3>
</Layout.Header> </Layout.Header>
<Layout.Content> <Layout.Content>
<UsersTable/> <UsersTable />
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</> </>
......
import react from '@vitejs/plugin-react';
import { defineConfig, transformWithEsbuild } from 'vite';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
{
name: 'treat-js-files-as-jsx',
async transform(code, id) {
if (!/src\/.*\.js$/.test(id)) {
return null;
}
// Use the exposed transform from vite, instead of directly
// transforming with esbuild
return transformWithEsbuild(code, id, {
loader: 'jsx',
jsx: 'automatic',
});
},
},
react(),
],
optimizeDeps: {
force: true,
esbuildOptions: {
loader: {
'.js': 'jsx',
},
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
'react-core': ['react', 'react-dom', 'react-router-dom'],
'semi-ui': ['@douyinfe/semi-icons', '@douyinfe/semi-ui'],
semantic: ['semantic-ui-offline', 'semantic-ui-react'],
visactor: ['@visactor/react-vchart', '@visactor/vchart'],
tools: ['axios', 'history', 'marked'],
'react-components': [
'react-dropzone',
'react-fireworks',
'react-telegram-login',
'react-toastify',
'react-turnstile',
],
},
},
},
},
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment