chore: sync with gin-template
This commit is contained in:
+33
-15
@@ -1,4 +1,4 @@
|
||||
import React, { lazy, Suspense, useEffect } from 'react';
|
||||
import React, { lazy, Suspense, useContext, useEffect } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import Loading from './components/Loading';
|
||||
import User from './pages/User';
|
||||
@@ -9,34 +9,52 @@ import NotFound from './pages/NotFound';
|
||||
import Setting from './pages/Setting';
|
||||
import EditUser from './pages/User/EditUser';
|
||||
import AddUser from './pages/User/AddUser';
|
||||
import { API, showError } from './helpers';
|
||||
import { API, showError, showNotice } from './helpers';
|
||||
import PasswordResetForm from './components/PasswordResetForm';
|
||||
import GitHubOAuth from './components/GitHubOAuth';
|
||||
import PasswordResetConfirm from './components/PasswordResetConfirm';
|
||||
import { UserContext } from './context/User';
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
const About = lazy(() => import('./pages/About'));
|
||||
|
||||
function App() {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
|
||||
const loadUser = () => {
|
||||
let user = localStorage.getItem('user');
|
||||
if (user) {
|
||||
let data = JSON.parse(user);
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
}
|
||||
};
|
||||
const loadStatus = async () => {
|
||||
const res = await API.get('/api/status');
|
||||
const { success, data } = res.data;
|
||||
if (success) {
|
||||
localStorage.setItem('status', JSON.stringify(data));
|
||||
localStorage.setItem('footer_html', data.footer_html);
|
||||
let currentVersion = localStorage.getItem('version');
|
||||
if (currentVersion && currentVersion !== data.version) {
|
||||
localStorage.setItem('version', data.version);
|
||||
showNotice(
|
||||
`新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showError('无法正常连接至服务器!');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUser();
|
||||
loadStatus().then();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
path='/'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Home />
|
||||
@@ -44,7 +62,7 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/user"
|
||||
path='/user'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<User />
|
||||
@@ -52,7 +70,7 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/user/edit/:id"
|
||||
path='/user/edit/:id'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditUser />
|
||||
@@ -60,7 +78,7 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/user/edit"
|
||||
path='/user/edit'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditUser />
|
||||
@@ -68,7 +86,7 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/user/add"
|
||||
path='/user/add'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<AddUser />
|
||||
@@ -76,7 +94,7 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/user/reset"
|
||||
path='/user/reset'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<PasswordResetConfirm />
|
||||
@@ -84,7 +102,7 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/login"
|
||||
path='/login'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<LoginForm />
|
||||
@@ -92,7 +110,7 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/register"
|
||||
path='/register'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<RegisterForm />
|
||||
@@ -100,7 +118,7 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/reset"
|
||||
path='/reset'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<PasswordResetForm />
|
||||
@@ -108,7 +126,7 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/oauth/github"
|
||||
path='/oauth/github'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<GitHubOAuth />
|
||||
@@ -116,7 +134,7 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/setting"
|
||||
path='/setting'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
@@ -126,14 +144,14 @@ function App() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/about"
|
||||
path='/about'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<About />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={NotFound} />
|
||||
<Route path='*' element={NotFound} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,27 +12,27 @@ const Footer = () => {
|
||||
|
||||
return (
|
||||
<Segment vertical>
|
||||
<Container textAlign="center">
|
||||
<Container textAlign='center'>
|
||||
{Footer === '' ? (
|
||||
<div className="custom-footer">
|
||||
<div className='custom-footer'>
|
||||
<a
|
||||
href="https://github.com/songquanpeng/message-pusher"
|
||||
target="_blank"
|
||||
href='https://github.com/songquanpeng/message-pusher'
|
||||
target='_blank'
|
||||
>
|
||||
消息推送服务 {process.env.REACT_APP_VERSION}{' '}
|
||||
</a>
|
||||
由{' '}
|
||||
<a href="https://github.com/songquanpeng" target="_blank">
|
||||
<a href='https://github.com/songquanpeng' target='_blank'>
|
||||
JustSong
|
||||
</a>{' '}
|
||||
构建,源代码遵循{' '}
|
||||
<a href="https://opensource.org/licenses/mit-license.php">
|
||||
<a href='https://opensource.org/licenses/mit-license.php'>
|
||||
MIT 协议
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="custom-footer"
|
||||
className='custom-footer'
|
||||
dangerouslySetInnerHTML={{ __html: Footer }}
|
||||
></div>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useContext, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
|
||||
import { Button, Container, Icon, Menu, Segment } from 'semantic-ui-react';
|
||||
import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react';
|
||||
import { API, isAdmin, isMobile, showSuccess } from '../helpers';
|
||||
import '../index.css';
|
||||
|
||||
@@ -34,7 +34,6 @@ const headerButtons = [
|
||||
const Header = () => {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
let navigate = useNavigate();
|
||||
let size = isMobile() ? 'large' : '';
|
||||
|
||||
const [showSidebar, setShowSidebar] = useState(false);
|
||||
|
||||
@@ -80,7 +79,7 @@ const Header = () => {
|
||||
<>
|
||||
<Menu
|
||||
borderless
|
||||
size={size}
|
||||
size='large'
|
||||
style={
|
||||
showSidebar
|
||||
? {
|
||||
@@ -93,17 +92,17 @@ const Header = () => {
|
||||
}
|
||||
>
|
||||
<Container>
|
||||
<Menu.Item as={Link} to="/">
|
||||
<Menu.Item as={Link} to='/'>
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="logo"
|
||||
src='/logo.png'
|
||||
alt='logo'
|
||||
style={{ marginRight: '0.75em' }}
|
||||
/>
|
||||
<div style={{ fontSize: '20px' }}>
|
||||
<b>消息推送服务</b>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Menu position="right">
|
||||
<Menu.Menu position='right'>
|
||||
<Menu.Item onClick={toggleSidebar}>
|
||||
<Icon name={showSidebar ? 'close' : 'sidebar'} />
|
||||
</Menu.Item>
|
||||
@@ -149,28 +148,32 @@ const Header = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu borderless size={size} style={{ borderTop: 'none' }}>
|
||||
<Menu borderless style={{ borderTop: 'none' }}>
|
||||
<Container>
|
||||
<Menu.Item as={Link} to="/" className={'hide-on-mobile'}>
|
||||
<img src="/logo.png" alt="logo" style={{ marginRight: '0.75em' }} />
|
||||
<Menu.Item as={Link} to='/' className={'hide-on-mobile'}>
|
||||
<img src='/logo.png' alt='logo' style={{ marginRight: '0.75em' }} />
|
||||
<div style={{ fontSize: '20px' }}>
|
||||
<b>消息推送服务</b>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
{renderButtons(false)}
|
||||
<Menu.Menu position="right">
|
||||
<Menu.Menu position='right'>
|
||||
{userState.user ? (
|
||||
<Menu.Item
|
||||
name="注销"
|
||||
onClick={logout}
|
||||
className="btn btn-link"
|
||||
/>
|
||||
<Dropdown
|
||||
text={userState.user.username}
|
||||
pointing
|
||||
className='link item'
|
||||
>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item onClick={logout}>注销</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Menu.Item
|
||||
name="登录"
|
||||
name='登录'
|
||||
as={Link}
|
||||
to="/login"
|
||||
className="btn btn-link"
|
||||
to='/login'
|
||||
className='btn btn-link'
|
||||
/>
|
||||
)}
|
||||
</Menu.Menu>
|
||||
|
||||
@@ -114,7 +114,7 @@ const LoginForm = () => {
|
||||
value={password}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Button color='teal' fluid size='large' onClick={handleSubmit}>
|
||||
<Button color='telegram' fluid size='large' onClick={handleSubmit}>
|
||||
登录
|
||||
</Button>
|
||||
</Segment>
|
||||
@@ -179,7 +179,7 @@ const LoginForm = () => {
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Button
|
||||
color='teal'
|
||||
color='telegram'
|
||||
fluid
|
||||
size='large'
|
||||
onClick={onSubmitWeChatVerificationCode}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Form, Grid, Modal } from 'semantic-ui-react';
|
||||
import { Button, Divider, Form, Grid, Header, Modal } from 'semantic-ui-react';
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import { marked } from 'marked';
|
||||
|
||||
@@ -7,7 +7,7 @@ const OtherSetting = () => {
|
||||
let [inputs, setInputs] = useState({
|
||||
Footer: '',
|
||||
Notice: '',
|
||||
About: ''
|
||||
About: '',
|
||||
});
|
||||
let originInputs = {};
|
||||
let [loading, setLoading] = useState(false);
|
||||
@@ -42,7 +42,7 @@ const OtherSetting = () => {
|
||||
setLoading(true);
|
||||
const res = await API.put('/api/option', {
|
||||
key,
|
||||
value
|
||||
value,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
@@ -76,7 +76,7 @@ const OtherSetting = () => {
|
||||
|
||||
const checkUpdate = async () => {
|
||||
const res = await API.get(
|
||||
'https://api.github.com/repos/songquanpeng/message-pusher/releases/latest'
|
||||
'https://api.github.com/repos/songquanpeng/gin-template/releases/latest'
|
||||
);
|
||||
const { tag_name, body } = res.data;
|
||||
if (tag_name === process.env.REACT_APP_VERSION) {
|
||||
@@ -94,6 +94,7 @@ const OtherSetting = () => {
|
||||
<Grid columns={1}>
|
||||
<Grid.Column>
|
||||
<Form loading={loading}>
|
||||
<Header as='h3'>通用设置</Header>
|
||||
<Form.Button onClick={checkUpdate}>检查更新</Form.Button>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
@@ -106,6 +107,8 @@ const OtherSetting = () => {
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitNotice}>保存公告</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>个性化设置</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='关于'
|
||||
@@ -126,9 +129,7 @@ const OtherSetting = () => {
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitFooter}>
|
||||
设置页脚
|
||||
</Form.Button>
|
||||
<Form.Button onClick={submitFooter}>设置页脚</Form.Button>
|
||||
</Form>
|
||||
</Grid.Column>
|
||||
<Modal
|
||||
@@ -139,15 +140,13 @@ const OtherSetting = () => {
|
||||
<Modal.Header>新版本:{updateData.tag_name}</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Modal.Description>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: updateData.content }}
|
||||
></div>
|
||||
<div dangerouslySetInnerHTML={{ __html: updateData.content }}></div>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={() => setShowUpdateModal(false)}>关闭</Button>
|
||||
<Button
|
||||
content="详情"
|
||||
content='详情'
|
||||
onClick={() => {
|
||||
setShowUpdateModal(false);
|
||||
openGitHubRelease();
|
||||
|
||||
@@ -33,7 +33,7 @@ const PasswordResetConfirm = () => {
|
||||
if (success) {
|
||||
let password = res.data.data;
|
||||
await copy(password);
|
||||
showSuccess(`密码已重置并已复制到剪切板:${password}`);
|
||||
showSuccess(`密码已重置并已复制到剪贴板:${password}`);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@@ -58,7 +58,7 @@ const PasswordResetConfirm = () => {
|
||||
readOnly
|
||||
/>
|
||||
<Button
|
||||
color='teal'
|
||||
color='telegram'
|
||||
fluid
|
||||
size='large'
|
||||
onClick={handleSubmit}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import { API, showError, showInfo, showSuccess } from '../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
|
||||
const PasswordResetForm = () => {
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -9,6 +10,20 @@ const PasswordResetForm = () => {
|
||||
const { email } = inputs;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||
const [turnstileToken, setTurnstileToken] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let status = localStorage.getItem('status');
|
||||
if (status) {
|
||||
status = JSON.parse(status);
|
||||
if (status.turnstile_check) {
|
||||
setTurnstileEnabled(true);
|
||||
setTurnstileSiteKey(status.turnstile_site_key);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
function handleChange(e) {
|
||||
const { name, value } = e.target;
|
||||
@@ -17,8 +32,14 @@ const PasswordResetForm = () => {
|
||||
|
||||
async function handleSubmit(e) {
|
||||
if (!email) return;
|
||||
if (turnstileEnabled && turnstileToken === '') {
|
||||
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await API.get(`/api/reset_password?email=${email}`);
|
||||
const res = await API.get(
|
||||
`/api/reset_password?email=${email}&turnstile=${turnstileToken}`
|
||||
);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('重置邮件发送成功,请检查邮箱!');
|
||||
@@ -46,8 +67,18 @@ const PasswordResetForm = () => {
|
||||
value={email}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{turnstileEnabled ? (
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Button
|
||||
color='teal'
|
||||
color='telegram'
|
||||
fluid
|
||||
size='large'
|
||||
onClick={handleSubmit}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Form, Image, Modal } from 'semantic-ui-react';
|
||||
import { Button, Divider, Form, Header, Image, Modal } from 'semantic-ui-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import { API, copy, showError, showInfo, showSuccess } from '../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
|
||||
const PersonalSetting = () => {
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -12,12 +13,20 @@ const PersonalSetting = () => {
|
||||
const [status, setStatus] = useState({});
|
||||
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
|
||||
const [showEmailBindModal, setShowEmailBindModal] = useState(false);
|
||||
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||
const [turnstileToken, setTurnstileToken] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let status = localStorage.getItem('status');
|
||||
if (status) {
|
||||
status = JSON.parse(status);
|
||||
setStatus(status);
|
||||
if (status.turnstile_check) {
|
||||
setTurnstileEnabled(true);
|
||||
setTurnstileSiteKey(status.turnstile_site_key);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -25,6 +34,17 @@ const PersonalSetting = () => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
|
||||
const generateToken = async () => {
|
||||
const res = await API.get('/api/user/token');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
await copy(data);
|
||||
showSuccess(`令牌已重置并已复制到剪贴板:${data}`);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const bindWeChat = async () => {
|
||||
if (inputs.wechat_verification_code === '') return;
|
||||
const res = await API.get(
|
||||
@@ -47,17 +67,26 @@ const PersonalSetting = () => {
|
||||
|
||||
const sendVerificationCode = async () => {
|
||||
if (inputs.email === '') return;
|
||||
const res = await API.get(`/api/verification?email=${inputs.email}`);
|
||||
if (turnstileEnabled && turnstileToken === '') {
|
||||
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await API.get(
|
||||
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
|
||||
);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('验证码发送成功,请检查你的邮箱!');
|
||||
showSuccess('验证码发送成功,请检查邮箱!');
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const bindEmail = async () => {
|
||||
if (inputs.email_verification_code === '') return;
|
||||
setLoading(true);
|
||||
const res = await API.get(
|
||||
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
|
||||
);
|
||||
@@ -68,13 +97,18 @@ const PersonalSetting = () => {
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: '40px' }}>
|
||||
<Header as='h3'>通用设置</Header>
|
||||
<Button as={Link} to={`/user/edit/`}>
|
||||
更新个人信息
|
||||
</Button>
|
||||
<Button onClick={generateToken}>生成访问令牌</Button>
|
||||
<Divider />
|
||||
<Header as='h3'>账号绑定</Header>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowWeChatBindModal(true);
|
||||
@@ -104,7 +138,7 @@ const PersonalSetting = () => {
|
||||
value={inputs.wechat_verification_code}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Button color='teal' fluid size='large' onClick={bindWeChat}>
|
||||
<Button color='telegram' fluid size='large' onClick={bindWeChat}>
|
||||
绑定
|
||||
</Button>
|
||||
</Form>
|
||||
@@ -123,7 +157,8 @@ const PersonalSetting = () => {
|
||||
onClose={() => setShowEmailBindModal(false)}
|
||||
onOpen={() => setShowEmailBindModal(true)}
|
||||
open={showEmailBindModal}
|
||||
size={'mini'}
|
||||
size={'tiny'}
|
||||
style={{ maxWidth: '450px' }}
|
||||
>
|
||||
<Modal.Header>绑定邮箱地址</Modal.Header>
|
||||
<Modal.Content>
|
||||
@@ -136,7 +171,9 @@ const PersonalSetting = () => {
|
||||
name='email'
|
||||
type='email'
|
||||
action={
|
||||
<Button onClick={sendVerificationCode}>获取验证码</Button>
|
||||
<Button onClick={sendVerificationCode} disabled={loading}>
|
||||
获取验证码
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
@@ -146,7 +183,23 @@ const PersonalSetting = () => {
|
||||
value={inputs.email_verification_code}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Button color='teal' fluid size='large' onClick={bindEmail}>
|
||||
{turnstileEnabled ? (
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Button
|
||||
color='telegram'
|
||||
fluid
|
||||
size='large'
|
||||
onClick={bindEmail}
|
||||
loading={loading}
|
||||
>
|
||||
绑定
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from 'semantic-ui-react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { API, showError, showInfo, showSuccess } from '../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
|
||||
const RegisterForm = () => {
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -20,14 +21,21 @@ const RegisterForm = () => {
|
||||
verification_code: '',
|
||||
});
|
||||
const { username, password, password2 } = inputs;
|
||||
|
||||
const [showEmailVerification, setShowEmailVerification] = useState(false);
|
||||
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||
const [turnstileToken, setTurnstileToken] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let status = localStorage.getItem('status');
|
||||
if (status) {
|
||||
status = JSON.parse(status);
|
||||
setShowEmailVerification(status.email_verification);
|
||||
if (status.turnstile_check) {
|
||||
setTurnstileEnabled(true);
|
||||
setTurnstileSiteKey(status.turnstile_site_key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -49,7 +57,15 @@ const RegisterForm = () => {
|
||||
return;
|
||||
}
|
||||
if (username && password) {
|
||||
const res = await API.post('/api/user/register', inputs);
|
||||
if (turnstileEnabled && turnstileToken === '') {
|
||||
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await API.post(
|
||||
`/api/user/register?turnstile=${turnstileToken}`,
|
||||
inputs
|
||||
);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
navigate('/login');
|
||||
@@ -57,18 +73,27 @@ const RegisterForm = () => {
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const sendVerificationCode = async () => {
|
||||
if (inputs.email === '') return;
|
||||
const res = await API.get(`/api/verification?email=${inputs.email}`);
|
||||
if (turnstileEnabled && turnstileToken === '') {
|
||||
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await API.get(
|
||||
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
|
||||
);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('验证码发送成功,请检查你的邮箱!');
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -116,7 +141,9 @@ const RegisterForm = () => {
|
||||
name='email'
|
||||
type='email'
|
||||
action={
|
||||
<Button onClick={sendVerificationCode}>获取验证码</Button>
|
||||
<Button onClick={sendVerificationCode} disabled={loading}>
|
||||
获取验证码
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
@@ -131,7 +158,23 @@ const RegisterForm = () => {
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Button color='teal' fluid size='large' onClick={handleSubmit}>
|
||||
{turnstileEnabled ? (
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Button
|
||||
color='telegram'
|
||||
fluid
|
||||
size='large'
|
||||
onClick={handleSubmit}
|
||||
loading={loading}
|
||||
>
|
||||
注册
|
||||
</Button>
|
||||
</Segment>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Form, Grid } from 'semantic-ui-react';
|
||||
import { Divider, Form, Grid, Header } from 'semantic-ui-react';
|
||||
import { API, showError } from '../helpers';
|
||||
|
||||
const SystemSetting = () => {
|
||||
@@ -20,6 +20,10 @@ const SystemSetting = () => {
|
||||
WeChatServerAddress: '',
|
||||
WeChatServerToken: '',
|
||||
WeChatAccountQRCodeImageURL: '',
|
||||
TurnstileCheckEnabled: '',
|
||||
TurnstileSiteKey: '',
|
||||
TurnstileSecretKey: '',
|
||||
RegisterEnabled: '',
|
||||
});
|
||||
let originInputs = {};
|
||||
let [loading, setLoading] = useState(false);
|
||||
@@ -51,6 +55,8 @@ const SystemSetting = () => {
|
||||
case 'EmailVerificationEnabled':
|
||||
case 'GitHubOAuthEnabled':
|
||||
case 'WeChatAuthEnabled':
|
||||
case 'TurnstileCheckEnabled':
|
||||
case 'RegisterEnabled':
|
||||
value = inputs[key] === 'true' ? 'false' : 'true';
|
||||
break;
|
||||
default:
|
||||
@@ -78,7 +84,9 @@ const SystemSetting = () => {
|
||||
name === 'GitHubClientSecret' ||
|
||||
name === 'WeChatServerAddress' ||
|
||||
name === 'WeChatServerToken' ||
|
||||
name === 'WeChatAccountQRCodeImageURL'
|
||||
name === 'WeChatAccountQRCodeImageURL' ||
|
||||
name === 'TurnstileSiteKey' ||
|
||||
name === 'TurnstileSecretKey'
|
||||
) {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
} else {
|
||||
@@ -142,125 +150,226 @@ const SystemSetting = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const submitTurnstile = async () => {
|
||||
if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
|
||||
await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey);
|
||||
}
|
||||
if (
|
||||
originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey &&
|
||||
inputs.TurnstileSecretKey !== ''
|
||||
) {
|
||||
await updateOption('TurnstileSecretKey', inputs.TurnstileSecretKey);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid columns={1}>
|
||||
<Grid.Column>
|
||||
<Form loading={loading}>
|
||||
<Form.Group widths="equal">
|
||||
<Header as='h3'>通用设置</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label="服务器地址"
|
||||
placeholder="例如:https://yourdomain.com(注意没有最后的斜杠)"
|
||||
label='服务器地址'
|
||||
placeholder='例如:https://yourdomain.com(注意没有最后的斜杠)'
|
||||
value={inputs.ServerAddress}
|
||||
name="ServerAddress"
|
||||
name='ServerAddress'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitServerAddress}>
|
||||
更新服务器地址
|
||||
</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>配置登录注册</Header>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.PasswordLoginEnabled === 'true'}
|
||||
label="允许密码登录"
|
||||
name="PasswordLoginEnabled"
|
||||
label='允许通过密码进行登录'
|
||||
name='PasswordLoginEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.PasswordRegisterEnabled === 'true'}
|
||||
label="允许通过密码进行注册"
|
||||
name="PasswordRegisterEnabled"
|
||||
label='允许通过密码进行注册'
|
||||
name='PasswordRegisterEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.EmailVerificationEnabled === 'true'}
|
||||
label="强制邮箱验证"
|
||||
name="EmailVerificationEnabled"
|
||||
label='通过密码注册时需要进行邮箱验证'
|
||||
name='EmailVerificationEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.GitHubOAuthEnabled === 'true'}
|
||||
label="允许通过 GitHub 账户登录 & 注册"
|
||||
name="GitHubOAuthEnabled"
|
||||
label='允许通过 GitHub 账户登录 & 注册'
|
||||
name='GitHubOAuthEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.WeChatAuthEnabled === 'true'}
|
||||
label="允许通过微信登录 & 注册"
|
||||
name="WeChatAuthEnabled"
|
||||
label='允许通过微信登录 & 注册'
|
||||
name='WeChatAuthEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.RegisterEnabled === 'true'}
|
||||
label='允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)'
|
||||
name='RegisterEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.TurnstileCheckEnabled === 'true'}
|
||||
label='启用 Turnstile 用户校验'
|
||||
name='TurnstileCheckEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
配置 SMTP
|
||||
<Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label="SMTP 服务器地址"
|
||||
name="SMTPServer"
|
||||
label='SMTP 服务器地址'
|
||||
name='SMTPServer'
|
||||
onChange={handleInputChange}
|
||||
autoComplete="off"
|
||||
autoComplete='off'
|
||||
value={inputs.SMTPServer}
|
||||
placeholder='例如:smtp.qq.com'
|
||||
/>
|
||||
<Form.Input
|
||||
label="SMTP 账户"
|
||||
name="SMTPAccount"
|
||||
label='SMTP 账户'
|
||||
name='SMTPAccount'
|
||||
onChange={handleInputChange}
|
||||
autoComplete="off"
|
||||
autoComplete='off'
|
||||
value={inputs.SMTPAccount}
|
||||
placeholder='通常是邮箱地址'
|
||||
/>
|
||||
<Form.Input
|
||||
label="SMTP 访问凭证"
|
||||
name="SMTPToken"
|
||||
label='SMTP 访问凭证'
|
||||
name='SMTPToken'
|
||||
onChange={handleInputChange}
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
type='password'
|
||||
autoComplete='off'
|
||||
value={inputs.SMTPToken}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitSMTP}>保存 SMTP 设置</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
配置 GitHub OAuth App
|
||||
<Header.Subheader>
|
||||
用以支持通过 GitHub 进行登录注册,
|
||||
<a href='https://github.com/settings/developers' target='_blank'>
|
||||
点击此处
|
||||
</a>
|
||||
管理你的 GitHub OAuth App
|
||||
</Header.Subheader>
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label="GitHub Client ID"
|
||||
name="GitHubClientId"
|
||||
label='GitHub Client ID'
|
||||
name='GitHubClientId'
|
||||
onChange={handleInputChange}
|
||||
autoComplete="off"
|
||||
autoComplete='off'
|
||||
value={inputs.GitHubClientId}
|
||||
placeholder='输入你注册的 GitHub OAuth APP 的 ID'
|
||||
/>
|
||||
<Form.Input
|
||||
label="GitHub Client Secret"
|
||||
name="GitHubClientSecret"
|
||||
label='GitHub Client Secret'
|
||||
name='GitHubClientSecret'
|
||||
onChange={handleInputChange}
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
type='password'
|
||||
autoComplete='off'
|
||||
value={inputs.GitHubClientSecret}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitGitHubOAuth}>
|
||||
保存 GitHub OAuth 设置
|
||||
</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
配置 WeChat Server
|
||||
<Header.Subheader>
|
||||
用以支持通过微信进行登录注册,
|
||||
<a
|
||||
href='https://github.com/songquanpeng/wechat-server'
|
||||
target='_blank'
|
||||
>
|
||||
点击此处
|
||||
</a>
|
||||
了解 WeChat Server
|
||||
</Header.Subheader>
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label="WeChat Server 服务器地址"
|
||||
name="WeChatServerAddress"
|
||||
placeholder="例如:https://yourdomain.com(注意没有最后的斜杠)"
|
||||
label='WeChat Server 服务器地址'
|
||||
name='WeChatServerAddress'
|
||||
placeholder='例如:https://yourdomain.com(注意没有最后的斜杠)'
|
||||
onChange={handleInputChange}
|
||||
autoComplete="off"
|
||||
autoComplete='off'
|
||||
value={inputs.WeChatServerAddress}
|
||||
/>
|
||||
<Form.Input
|
||||
label="WeChat Server 访问凭证"
|
||||
name="WeChatServerToken"
|
||||
type="password"
|
||||
label='WeChat Server 访问凭证'
|
||||
name='WeChatServerToken'
|
||||
type='password'
|
||||
onChange={handleInputChange}
|
||||
autoComplete="off"
|
||||
autoComplete='off'
|
||||
value={inputs.WeChatServerToken}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
/>
|
||||
<Form.Input
|
||||
label="微信公众号二维码图片链接"
|
||||
name="WeChatAccountQRCodeImageURL"
|
||||
label='微信公众号二维码图片链接'
|
||||
name='WeChatAccountQRCodeImageURL'
|
||||
onChange={handleInputChange}
|
||||
autoComplete="off"
|
||||
autoComplete='off'
|
||||
value={inputs.WeChatAccountQRCodeImageURL}
|
||||
placeholder='输入一个图片链接'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitWeChat}>保存微信登录设置</Form.Button>
|
||||
<Form.Button onClick={submitWeChat}>
|
||||
保存 WeChat Server 设置
|
||||
</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
配置 Turnstile
|
||||
<Header.Subheader>
|
||||
用以支持用户校验,
|
||||
<a href='https://dash.cloudflare.com/' target='_blank'>
|
||||
点击此处
|
||||
</a>
|
||||
管理你的 Turnstile Sites,推荐选择 Invisible Widget Type
|
||||
</Header.Subheader>
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='Turnstile Site Key'
|
||||
name='TurnstileSiteKey'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='off'
|
||||
value={inputs.TurnstileSiteKey}
|
||||
placeholder='输入你注册的 Turnstile Site Key'
|
||||
/>
|
||||
<Form.Input
|
||||
label='Turnstile Secret Key'
|
||||
name='TurnstileSecretKey'
|
||||
onChange={handleInputChange}
|
||||
type='password'
|
||||
autoComplete='off'
|
||||
value={inputs.TurnstileSecretKey}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitTurnstile}>
|
||||
保存 Turnstile 设置
|
||||
</Form.Button>
|
||||
</Form>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
|
||||
@@ -3,18 +3,18 @@ import { Button, Form, Label, Pagination, Table } from 'semantic-ui-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
|
||||
const itemsPerPage = 10;
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
|
||||
function renderRole(role) {
|
||||
switch (role) {
|
||||
case 1:
|
||||
return <Label>普通用户</Label>;
|
||||
case 10:
|
||||
return <Label color="yellow">管理员</Label>;
|
||||
return <Label color='yellow'>管理员</Label>;
|
||||
case 100:
|
||||
return <Label color="orange">超级管理员</Label>;
|
||||
return <Label color='orange'>超级管理员</Label>;
|
||||
default:
|
||||
return <Label color="red">未知身份</Label>;
|
||||
return <Label color='red'>未知身份</Label>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,11 +25,17 @@ const UsersTable = () => {
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [searching, setSearching] = useState(false);
|
||||
|
||||
const loadUsers = async () => {
|
||||
const res = await API.get('/api/user');
|
||||
const loadUsers = async (startIdx) => {
|
||||
const res = await API.get(`/api/user/?p=${startIdx}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setUsers(data);
|
||||
if (startIdx === 0) {
|
||||
setUsers(data);
|
||||
} else {
|
||||
let newUsers = users;
|
||||
newUsers.push(...data);
|
||||
setUsers(newUsers);
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@@ -37,18 +43,24 @@ const UsersTable = () => {
|
||||
};
|
||||
|
||||
const onPaginationChange = (e, { activePage }) => {
|
||||
setActivePage(activePage);
|
||||
(async () => {
|
||||
if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
|
||||
// In this case we have to load more data and then append them.
|
||||
await loadUsers(activePage - 1);
|
||||
}
|
||||
setActivePage(activePage);
|
||||
})();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers()
|
||||
loadUsers(0)
|
||||
.then()
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const manageUser = (username, action) => {
|
||||
const manageUser = (username, action, idx) => {
|
||||
(async () => {
|
||||
const res = await API.post('/api/user/manage', {
|
||||
username,
|
||||
@@ -57,38 +69,62 @@ const UsersTable = () => {
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('操作成功完成!');
|
||||
await loadUsers();
|
||||
let user = res.data.data;
|
||||
let newUsers = [...users];
|
||||
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
|
||||
if (action === 'delete') {
|
||||
newUsers[realIdx].deleted = true;
|
||||
} else {
|
||||
newUsers[realIdx].status = user.status;
|
||||
newUsers[realIdx].role = user.role;
|
||||
}
|
||||
setUsers(newUsers);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const renderStatus = (status, id) => {
|
||||
const renderStatus = (status) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '已激活';
|
||||
return <Label basic>已激活</Label>;
|
||||
case 2:
|
||||
return '已封禁';
|
||||
return (
|
||||
<Label basic color='red'>
|
||||
已封禁
|
||||
</Label>
|
||||
);
|
||||
default:
|
||||
return '未知状态';
|
||||
return (
|
||||
<Label basic color='grey'>
|
||||
未知状态
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const searchUsers = async () => {
|
||||
if (searchKeyword === '') {
|
||||
// if keyword is blank, load files instead.
|
||||
await loadUsers(0);
|
||||
setActivePage(1);
|
||||
return;
|
||||
}
|
||||
setSearching(true);
|
||||
const res = await API.get(`/api/user/search?keyword=${searchKeyword}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setUsers(data);
|
||||
setActivePage(1);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const handleKeywordChange = async (e, { name, value }) => {
|
||||
setSearchKeyword(value);
|
||||
const handleKeywordChange = async (e, { value }) => {
|
||||
setSearchKeyword(value.trim());
|
||||
};
|
||||
|
||||
const sortUser = (key) => {
|
||||
@@ -109,10 +145,10 @@ const UsersTable = () => {
|
||||
<>
|
||||
<Form onSubmit={searchUsers}>
|
||||
<Form.Input
|
||||
icon="search"
|
||||
icon='search'
|
||||
fluid
|
||||
iconPosition="left"
|
||||
placeholder="搜索用户的 ID,用户名,显示名称,以及邮箱地址 ..."
|
||||
iconPosition='left'
|
||||
placeholder='搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...'
|
||||
value={searchKeyword}
|
||||
loading={searching}
|
||||
onChange={handleKeywordChange}
|
||||
@@ -168,22 +204,26 @@ const UsersTable = () => {
|
||||
|
||||
<Table.Body>
|
||||
{users
|
||||
.slice((activePage - 1) * itemsPerPage, activePage * itemsPerPage)
|
||||
.slice(
|
||||
(activePage - 1) * ITEMS_PER_PAGE,
|
||||
activePage * ITEMS_PER_PAGE
|
||||
)
|
||||
.map((user, idx) => {
|
||||
if (user.deleted) return <></>;
|
||||
return (
|
||||
<Table.Row key={user.id}>
|
||||
<Table.Cell>{user.username}</Table.Cell>
|
||||
<Table.Cell>{user.display_name}</Table.Cell>
|
||||
<Table.Cell>{user.email ? user.email : '无'}</Table.Cell>
|
||||
<Table.Cell>{renderRole(user.role)}</Table.Cell>
|
||||
<Table.Cell>{renderStatus(user.status, user.id)}</Table.Cell>
|
||||
<Table.Cell>{renderStatus(user.status)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div>
|
||||
<Button
|
||||
size={'small'}
|
||||
positive
|
||||
onClick={() => {
|
||||
manageUser(user.username, 'promote');
|
||||
manageUser(user.username, 'promote', idx);
|
||||
}}
|
||||
>
|
||||
提升
|
||||
@@ -192,7 +232,7 @@ const UsersTable = () => {
|
||||
size={'small'}
|
||||
color={'yellow'}
|
||||
onClick={() => {
|
||||
manageUser(user.username, 'demote');
|
||||
manageUser(user.username, 'demote', idx);
|
||||
}}
|
||||
>
|
||||
降级
|
||||
@@ -201,7 +241,7 @@ const UsersTable = () => {
|
||||
size={'small'}
|
||||
negative
|
||||
onClick={() => {
|
||||
manageUser(user.username, 'delete');
|
||||
manageUser(user.username, 'delete', idx);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
@@ -211,7 +251,8 @@ const UsersTable = () => {
|
||||
onClick={() => {
|
||||
manageUser(
|
||||
user.username,
|
||||
user.status === 1 ? 'disable' : 'enable'
|
||||
user.status === 1 ? 'disable' : 'enable',
|
||||
idx
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -233,17 +274,20 @@ const UsersTable = () => {
|
||||
|
||||
<Table.Footer>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell colSpan="6">
|
||||
<Button size="small" as={Link} to="/user/add" loading={loading}>
|
||||
<Table.HeaderCell colSpan='6'>
|
||||
<Button size='small' as={Link} to='/user/add' loading={loading}>
|
||||
添加新的用户
|
||||
</Button>
|
||||
<Pagination
|
||||
floated="right"
|
||||
floated='right'
|
||||
activePage={activePage}
|
||||
onPageChange={onPaginationChange}
|
||||
size="small"
|
||||
size='small'
|
||||
siblingRange={1}
|
||||
totalPages={Math.ceil(users.length / itemsPerPage)}
|
||||
totalPages={
|
||||
Math.ceil(users.length / ITEMS_PER_PAGE) +
|
||||
(users.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
|
||||
}
|
||||
/>
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend!
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './toast.constants';
|
||||
export * from './user.constants';
|
||||
export * from './user.constants';
|
||||
export * from './common.constant';
|
||||
@@ -85,3 +85,7 @@ export function showInfo(message) {
|
||||
export function showNotice(message) {
|
||||
toast.info(message, showNoticeOptions);
|
||||
}
|
||||
|
||||
export function openPage(url) {
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
@@ -26,18 +26,20 @@ const About = () => {
|
||||
return (
|
||||
<>
|
||||
<Segment>
|
||||
{
|
||||
about === '' ? <>
|
||||
{about === '' ? (
|
||||
<>
|
||||
<Header as='h3'>关于</Header>
|
||||
<p>可在设置页面设置关于内容,支持 HTML & Markdown</p>
|
||||
项目仓库地址:
|
||||
<a href="https://github.com/songquanpeng/message-pusher">
|
||||
<a href='https://github.com/songquanpeng/message-pusher'>
|
||||
https://github.com/songquanpeng/message-pusher
|
||||
</a>
|
||||
</> : <>
|
||||
<div dangerouslySetInnerHTML={{ __html: about}}></div>
|
||||
</>
|
||||
}
|
||||
) : (
|
||||
<>
|
||||
<div dangerouslySetInnerHTML={{ __html: about }}></div>
|
||||
</>
|
||||
)}
|
||||
</Segment>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -13,18 +13,9 @@ const EditUser = () => {
|
||||
password: '',
|
||||
github_id: '',
|
||||
wechat_id: '',
|
||||
email: '',
|
||||
token: '',
|
||||
email:''
|
||||
});
|
||||
const {
|
||||
username,
|
||||
display_name,
|
||||
password,
|
||||
github_id,
|
||||
wechat_id,
|
||||
email,
|
||||
token,
|
||||
} = inputs;
|
||||
const { username, display_name, password, github_id, wechat_id, email } = inputs;
|
||||
const handleInputChange = (e, { name, value }) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
@@ -39,9 +30,6 @@ const EditUser = () => {
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
data.password = '';
|
||||
if (data.token === ' ') {
|
||||
data.token = '';
|
||||
}
|
||||
setInputs(data);
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -70,77 +58,64 @@ const EditUser = () => {
|
||||
return (
|
||||
<>
|
||||
<Segment loading={loading}>
|
||||
<Header as='h3'>更新用户信息</Header>
|
||||
<Form autoComplete='off'>
|
||||
<Header as="h3">更新用户信息</Header>
|
||||
<Form autoComplete="off">
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='用户名'
|
||||
name='username'
|
||||
label="用户名"
|
||||
name="username"
|
||||
placeholder={'请输入新的用户名'}
|
||||
onChange={handleInputChange}
|
||||
value={username}
|
||||
autoComplete='off'
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='密码'
|
||||
name='password'
|
||||
label="密码"
|
||||
name="password"
|
||||
type={'password'}
|
||||
placeholder={'请输入新的密码'}
|
||||
onChange={handleInputChange}
|
||||
value={password}
|
||||
autoComplete='off'
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='显示名称'
|
||||
name='display_name'
|
||||
label="显示名称"
|
||||
name="display_name"
|
||||
placeholder={'请输入新的显示名称'}
|
||||
onChange={handleInputChange}
|
||||
value={display_name}
|
||||
autoComplete='off'
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='推送鉴权 Token'
|
||||
name='token'
|
||||
placeholder={'请输入新的 Token,留空则将 Token 置空'}
|
||||
onChange={handleInputChange}
|
||||
value={token}
|
||||
autoComplete='off'
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='已绑定的 GitHub 账户'
|
||||
name='github_id'
|
||||
label="已绑定的 GitHub 账户"
|
||||
name="github_id"
|
||||
value={github_id}
|
||||
autoComplete='off'
|
||||
autoComplete="off"
|
||||
readOnly
|
||||
placeholder={'如需绑定请到个人设置页面进行绑定'}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='已绑定的微信账户'
|
||||
name='wechat_id'
|
||||
label="已绑定的微信账户"
|
||||
name="wechat_id"
|
||||
value={wechat_id}
|
||||
autoComplete='off'
|
||||
autoComplete="off"
|
||||
readOnly
|
||||
placeholder={'如需绑定请到个人设置页面进行绑定'}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
label='已绑定的邮箱账户'
|
||||
name='email'
|
||||
label="已绑定的邮箱账户"
|
||||
name="email"
|
||||
value={email}
|
||||
autoComplete='off'
|
||||
autoComplete="off"
|
||||
readOnly
|
||||
placeholder={'如需绑定请到个人设置页面进行绑定'}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Button onClick={submit}>提交</Button>
|
||||
|
||||
Reference in New Issue
Block a user