init: reinitialize from gin-template

This commit is contained in:
JustSong
2022-11-11 15:35:02 +08:00
parent 4063fe8c31
commit f1d5d9c8d1
129 changed files with 5629 additions and 2629 deletions
+44
View File
@@ -0,0 +1,44 @@
import React, { useEffect, useState } from 'react';
import { Container, Segment } from 'semantic-ui-react';
const Footer = () => {
const [Footer, setFooter] = useState('');
useEffect(() => {
let savedFooter = localStorage.getItem('footer_html');
if (!savedFooter) savedFooter = '';
setFooter(savedFooter);
});
return (
<Segment vertical>
<Container textAlign="center">
{Footer === '' ? (
<div className="custom-footer">
<a
href="https://github.com/songquanpeng/message-pusher"
target="_blank"
>
消息推送服务 {process.env.REACT_APP_VERSION}{' '}
</a>
{' '}
<a href="https://github.com/songquanpeng" target="_blank">
JustSong
</a>{' '}
构建源代码遵循{' '}
<a href="https://opensource.org/licenses/mit-license.php">
MIT 协议
</a>
</div>
) : (
<div
className="custom-footer"
dangerouslySetInnerHTML={{ __html: Footer }}
></div>
)}
</Container>
</Segment>
);
};
export default Footer;
+57
View File
@@ -0,0 +1,57 @@
import React, { useContext, useEffect, useState } from 'react';
import { Dimmer, Loader, Segment } from 'semantic-ui-react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { API, showError, showSuccess } from '../helpers';
import { UserContext } from '../context/User';
const GitHubOAuth = () => {
const [searchParams, setSearchParams] = useSearchParams();
const [userState, userDispatch] = useContext(UserContext);
const [prompt, setPrompt] = useState('处理中...');
const [processing, setProcessing] = useState(true);
let navigate = useNavigate();
const sendCode = async (code, count) => {
const res = await API.get(`/api/oauth/github?code=${code}`);
const { success, message, data } = res.data;
if (success) {
if (message === 'bind') {
showSuccess('绑定成功!');
navigate('/setting');
} else {
userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data));
showSuccess('登录成功!');
navigate('/');
}
} else {
showError(message);
if (count === 0) {
setPrompt(`操作失败,重定向至登录界面中...`);
navigate('/setting'); // in case this is failed to bind GitHub
return;
}
count++;
setPrompt(`出现错误,第 ${count} 次重试中...`);
await new Promise((resolve) => setTimeout(resolve, count * 2000));
await sendCode(code, count);
}
};
useEffect(() => {
let code = searchParams.get('code');
sendCode(code, 0).then();
}, []);
return (
<Segment style={{ minHeight: '300px' }}>
<Dimmer active inverted>
<Loader size='large'>{prompt}</Loader>
</Dimmer>
</Segment>
);
};
export default GitHubOAuth;
+183
View File
@@ -0,0 +1,183 @@
import React, { useContext, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User';
import { Button, Container, Icon, Menu, Segment } from 'semantic-ui-react';
import { API, isAdmin, isMobile, showSuccess } from '../helpers';
import '../index.css';
// Header Buttons
const headerButtons = [
{
name: '首页',
to: '/',
icon: 'home',
},
{
name: '用户',
to: '/user',
icon: 'user',
admin: true,
},
{
name: '设置',
to: '/setting',
icon: 'setting',
},
{
name: '关于',
to: '/about',
icon: 'info circle',
},
];
const Header = () => {
const [userState, userDispatch] = useContext(UserContext);
let navigate = useNavigate();
let size = isMobile() ? 'large' : '';
const [showSidebar, setShowSidebar] = useState(false);
async function logout() {
setShowSidebar(false);
await API.get('/api/user/logout');
showSuccess('注销成功!');
userDispatch({ type: 'logout' });
localStorage.removeItem('user');
navigate('/login');
}
const toggleSidebar = () => {
setShowSidebar(!showSidebar);
};
const renderButtons = (isMobile) => {
return headerButtons.map((button) => {
if (button.admin && !isAdmin()) return <></>;
if (isMobile) {
return (
<Menu.Item
onClick={() => {
navigate(button.to);
setShowSidebar(false);
}}
>
{button.name}
</Menu.Item>
);
}
return (
<Menu.Item key={button.name} as={Link} to={button.to}>
<Icon name={button.icon} />
{button.name}
</Menu.Item>
);
});
};
if (isMobile()) {
return (
<>
<Menu
borderless
size={size}
style={
showSidebar
? {
borderBottom: 'none',
marginBottom: '0',
borderTop: 'none',
height: '51px',
}
: { borderTop: 'none', height: '52px' }
}
>
<Container>
<Menu.Item as={Link} to="/">
<img
src="/logo.png"
alt="logo"
style={{ marginRight: '0.75em' }}
/>
<div style={{ fontSize: '20px' }}>
<b>消息推送服务</b>
</div>
</Menu.Item>
<Menu.Menu position="right">
<Menu.Item onClick={toggleSidebar}>
<Icon name={showSidebar ? 'close' : 'sidebar'} />
</Menu.Item>
</Menu.Menu>
</Container>
</Menu>
{showSidebar ? (
<Segment style={{ marginTop: 0, borderTop: '0' }}>
<Menu secondary vertical style={{ width: '100%', margin: 0 }}>
{renderButtons(true)}
<Menu.Item>
{userState.user ? (
<Button onClick={logout}>注销</Button>
) : (
<>
<Button
onClick={() => {
setShowSidebar(false);
navigate('/login');
}}
>
登录
</Button>
<Button
onClick={() => {
setShowSidebar(false);
navigate('/register');
}}
>
注册
</Button>
</>
)}
</Menu.Item>
</Menu>
</Segment>
) : (
<></>
)}
</>
);
}
return (
<>
<Menu borderless size={size} style={{ borderTop: 'none' }}>
<Container>
<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">
{userState.user ? (
<Menu.Item
name="注销"
onClick={logout}
className="btn btn-link"
/>
) : (
<Menu.Item
name="登录"
as={Link}
to="/login"
className="btn btn-link"
/>
)}
</Menu.Menu>
</Container>
</Menu>
</>
);
};
export default Header;
+14
View File
@@ -0,0 +1,14 @@
import React from 'react';
import { Segment, Dimmer, Loader } from 'semantic-ui-react';
const Loading = ({ prompt: name = 'page' }) => {
return (
<Segment style={{ height: 100 }}>
<Dimmer active inverted>
<Loader indeterminate>加载{name}...</Loader>
</Dimmer>
</Segment>
);
};
export default Loading;
+198
View File
@@ -0,0 +1,198 @@
import React, { useContext, useEffect, useState } from 'react';
import {
Button,
Divider,
Form,
Grid,
Header,
Image,
Message,
Modal,
Segment,
} from 'semantic-ui-react';
import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User';
import { API, showError, showSuccess } from '../helpers';
const LoginForm = () => {
const [inputs, setInputs] = useState({
username: '',
password: '',
wechat_verification_code: '',
});
const [submitted, setSubmitted] = useState(false);
const { username, password } = inputs;
const [userState, userDispatch] = useContext(UserContext);
let navigate = useNavigate();
const [status, setStatus] = useState({});
useEffect(() => {
let status = localStorage.getItem('status');
if (status) {
status = JSON.parse(status);
setStatus(status);
}
}, []);
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
const onGitHubOAuthClicked = () => {
window.open(
`https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
);
};
const onWeChatLoginClicked = () => {
setShowWeChatLoginModal(true);
};
const onSubmitWeChatVerificationCode = async () => {
const res = await API.get(
`/api/oauth/wechat?code=${inputs.wechat_verification_code}`
);
const { success, message, data } = res.data;
if (success) {
userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data));
navigate('/');
showSuccess('登录成功!');
setShowWeChatLoginModal(false);
} else {
showError(message);
}
};
function handleChange(e) {
const { name, value } = e.target;
setInputs((inputs) => ({ ...inputs, [name]: value }));
}
async function handleSubmit(e) {
setSubmitted(true);
if (username && password) {
const res = await API.post('/api/user/login', {
username,
password,
});
const { success, message, data } = res.data;
if (success) {
userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data));
navigate('/');
showSuccess('登录成功!');
} else {
showError(message);
}
}
}
return (
<Grid textAlign="center" style={{ marginTop: '48px' }}>
<Grid.Column style={{ maxWidth: 450 }}>
<Header as="h2" color="teal" textAlign="center">
<Image src="/logo.png" /> 用户登录
</Header>
<Form size="large">
<Segment>
<Form.Input
fluid
icon="user"
iconPosition="left"
placeholder="用户名"
name="username"
value={username}
onChange={handleChange}
/>
<Form.Input
fluid
icon="lock"
iconPosition="left"
placeholder="密码"
name="password"
type="password"
value={password}
onChange={handleChange}
/>
<Button color="teal" fluid size="large" onClick={handleSubmit}>
登录
</Button>
</Segment>
</Form>
<Message>
忘记密码
<Link to="/reset" className="btn btn-link">
点击重置
</Link>
没有账户
<Link to="/register" className="btn btn-link">
点击注册
</Link>
</Message>
{status.github_oauth || status.wechat_login ? (
<>
<Divider horizontal>Or</Divider>
{status.github_oauth ? (
<Button
circular
color="black"
icon="github"
onClick={onGitHubOAuthClicked}
/>
) : (
<></>
)}
{status.wechat_login ? (
<Button
circular
color="green"
icon="wechat"
onClick={onWeChatLoginClicked}
/>
) : (
<></>
)}
</>
) : (
<></>
)}
<Modal
onClose={() => setShowWeChatLoginModal(false)}
onOpen={() => setShowWeChatLoginModal(true)}
open={showWeChatLoginModal}
size={'mini'}
>
<Modal.Content>
<Modal.Description>
<Image src={status.wechat_qrcode} fluid />
<div style={{ textAlign: 'center' }}>
<p>
微信扫码关注公众号输入验证码获取验证码三分钟内有效
</p>
</div>
<Form size="large">
<Form.Input
fluid
placeholder="验证码"
name="wechat_verification_code"
value={inputs.wechat_verification_code}
onChange={handleChange}
/>
<Button
color="teal"
fluid
size="large"
onClick={onSubmitWeChatVerificationCode}
>
登录
</Button>
</Form>
</Modal.Description>
</Modal.Content>
</Modal>
</Grid.Column>
</Grid>
);
};
export default LoginForm;
+162
View File
@@ -0,0 +1,162 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Grid, Modal } from 'semantic-ui-react';
import { API, showError, showSuccess } from '../helpers';
import { marked } from 'marked';
const OtherSetting = () => {
let [inputs, setInputs] = useState({
Footer: '',
Notice: '',
About: ''
});
let originInputs = {};
let [loading, setLoading] = useState(false);
const [showUpdateModal, setShowUpdateModal] = useState(false);
const [updateData, setUpdateData] = useState({
tag_name: '',
content: '',
});
const getOptions = async () => {
const res = await API.get('/api/option');
const { success, message, data } = res.data;
if (success) {
let newInputs = {};
data.forEach((item) => {
if (item.key in inputs) {
newInputs[item.key] = item.value;
}
});
setInputs(newInputs);
originInputs = newInputs;
} else {
showError(message);
}
};
useEffect(() => {
getOptions().then();
}, []);
const updateOption = async (key, value) => {
setLoading(true);
const res = await API.put('/api/option', {
key,
value
});
const { success, message } = res.data;
if (success) {
setInputs((inputs) => ({ ...inputs, [key]: value }));
} else {
showError(message);
}
setLoading(false);
};
const handleInputChange = async (e, { name, value }) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
const submitNotice = async () => {
await updateOption('Notice', inputs.Notice);
};
const submitFooter = async () => {
await updateOption('Footer', inputs.Footer);
};
const submitAbout = async () => {
await updateOption('About', inputs.About);
};
const openGitHubRelease = () => {
window.location =
'https://github.com/songquanpeng/message-pusher/releases/latest';
};
const checkUpdate = async () => {
const res = await API.get(
'https://api.github.com/repos/songquanpeng/message-pusher/releases/latest'
);
const { tag_name, body } = res.data;
if (tag_name === process.env.REACT_APP_VERSION) {
showSuccess(`已是最新版本:${tag_name}`);
} else {
setUpdateData({
tag_name: tag_name,
content: marked.parse(body),
});
setShowUpdateModal(true);
}
};
return (
<Grid columns={1}>
<Grid.Column>
<Form loading={loading}>
<Form.Button onClick={checkUpdate}>检查更新</Form.Button>
<Form.Group widths='equal'>
<Form.TextArea
label='公告'
placeholder='在此输入新的公告内容'
value={inputs.Notice}
name='Notice'
onChange={handleInputChange}
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
/>
</Form.Group>
<Form.Button onClick={submitNotice}>保存公告</Form.Button>
<Form.Group widths='equal'>
<Form.TextArea
label='关于'
placeholder='在此输入新的关于内容,支持 Markdown & HTML 代码'
value={inputs.About}
name='About'
onChange={handleInputChange}
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
/>
</Form.Group>
<Form.Button onClick={submitAbout}>保存关于</Form.Button>
<Form.Group widths='equal'>
<Form.Input
label='页脚'
placeholder='在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码'
value={inputs.Footer}
name='Footer'
onChange={handleInputChange}
/>
</Form.Group>
<Form.Button onClick={submitFooter}>
设置页脚
</Form.Button>
</Form>
</Grid.Column>
<Modal
onClose={() => setShowUpdateModal(false)}
onOpen={() => setShowUpdateModal(true)}
open={showUpdateModal}
>
<Modal.Header>新版本{updateData.tag_name}</Modal.Header>
<Modal.Content>
<Modal.Description>
<div
dangerouslySetInnerHTML={{ __html: updateData.content }}
></div>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => setShowUpdateModal(false)}>关闭</Button>
<Button
content="详情"
onClick={() => {
setShowUpdateModal(false);
openGitHubRelease();
}}
/>
</Modal.Actions>
</Modal>
</Grid>
);
};
export default OtherSetting;
@@ -0,0 +1,76 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
import { API, copy, showError, showSuccess } from '../helpers';
import { useSearchParams } from 'react-router-dom';
const PasswordResetConfirm = () => {
const [inputs, setInputs] = useState({
email: '',
token: ''
});
const { email, token } = inputs;
const [loading, setLoading] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
useEffect(() => {
let token = searchParams.get('token');
let email = searchParams.get('email')
setInputs({
token,
email
})
}, []);
async function handleSubmit(e) {
if (!email) return;
setLoading(true);
const res = await API.post(`/api/user/reset`, {
email,
token
});
const { success, message } = res.data;
if (success) {
let password = res.data.data;
await copy(password);
showSuccess(`密码已重置并已复制到剪切板:${password}`);
} else {
showError(message);
}
setLoading(false);
}
return (
<Grid textAlign='center' style={{ marginTop: '48px' }}>
<Grid.Column style={{ maxWidth: 450 }}>
<Header as='h2' color='teal' textAlign='center'>
<Image src='/logo.png' /> 密码重置确认
</Header>
<Form size='large'>
<Segment>
<Form.Input
fluid
icon="mail"
iconPosition="left"
placeholder="邮箱地址"
name="email"
value={email}
readOnly
/>
<Button
color='teal'
fluid
size='large'
onClick={handleSubmit}
loading={loading}
>
提交
</Button>
</Segment>
</Form>
</Grid.Column>
</Grid>
);
};
export default PasswordResetConfirm;
+65
View File
@@ -0,0 +1,65 @@
import React, { useState } from 'react';
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
import { API, showError, showSuccess } from '../helpers';
const PasswordResetForm = () => {
const [inputs, setInputs] = useState({
email: '',
});
const { email } = inputs;
const [loading, setLoading] = useState(false);
function handleChange(e) {
const { name, value } = e.target;
setInputs((inputs) => ({ ...inputs, [name]: value }));
}
async function handleSubmit(e) {
if (!email) return;
setLoading(true);
const res = await API.get(`/api/reset_password?email=${email}`);
const { success, message } = res.data;
if (success) {
showSuccess('重置邮件发送成功,请检查邮箱!');
setInputs({ ...inputs, email: '' });
} else {
showError(message);
}
setLoading(false);
}
return (
<Grid textAlign="center" style={{ marginTop: '48px' }}>
<Grid.Column style={{ maxWidth: 450 }}>
<Header as="h2" color="teal" textAlign="center">
<Image src="/logo.png" /> 密码重置
</Header>
<Form size="large">
<Segment>
<Form.Input
fluid
icon="mail"
iconPosition="left"
placeholder="邮箱地址"
name="email"
value={email}
onChange={handleChange}
/>
<Button
color="teal"
fluid
size="large"
onClick={handleSubmit}
loading={loading}
>
提交
</Button>
</Segment>
</Form>
</Grid.Column>
</Grid>
);
};
export default PasswordResetForm;
+172
View File
@@ -0,0 +1,172 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Image, Modal } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { API, copy, showError, showSuccess } from '../helpers';
const PersonalSetting = () => {
const [inputs, setInputs] = useState({
wechat_verification_code: '',
email_verification_code: '',
email: '',
});
const [status, setStatus] = useState({});
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
const [showEmailBindModal, setShowEmailBindModal] = useState(false);
useEffect(() => {
let status = localStorage.getItem('status');
if (status) {
status = JSON.parse(status);
setStatus(status);
}
}, []);
const handleInputChange = (e, { name, value }) => {
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(
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
);
const { success, message } = res.data;
if (success) {
showSuccess('微信账户绑定成功!');
setShowWeChatBindModal(false);
} else {
showError(message);
}
};
const openGitHubOAuth = () => {
window.open(
`https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
);
};
const sendVerificationCode = async () => {
if (inputs.email === '') return;
const res = await API.get(`/api/verification?email=${inputs.email}`);
const { success, message } = res.data;
if (success) {
showSuccess('验证码发送成功,请检查你的邮箱!');
} else {
showError(message);
}
};
const bindEmail = async () => {
if (inputs.email_verification_code === '') return;
const res = await API.get(
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
);
const { success, message } = res.data;
if (success) {
showSuccess('邮箱账户绑定成功!');
setShowEmailBindModal(false);
} else {
showError(message);
}
};
return (
<div style={{ lineHeight: '40px' }}>
<Button as={Link} to={`/user/edit/`}>
更新个人信息
</Button>
<Button onClick={generateToken}>生成访问令牌</Button>
<Button
onClick={() => {
setShowWeChatBindModal(true);
}}
>
绑定微信账号
</Button>
<Modal
onClose={() => setShowWeChatBindModal(false)}
onOpen={() => setShowWeChatBindModal(true)}
open={showWeChatBindModal}
size={'mini'}
>
<Modal.Content>
<Modal.Description>
<Image src={status.wechat_qrcode} fluid />
<div style={{ textAlign: 'center' }}>
<p>
微信扫码关注公众号输入验证码获取验证码三分钟内有效
</p>
</div>
<Form size='large'>
<Form.Input
fluid
placeholder='验证码'
name='wechat_verification_code'
value={inputs.wechat_verification_code}
onChange={handleInputChange}
/>
<Button color='teal' fluid size='large' onClick={bindWeChat}>
绑定
</Button>
</Form>
</Modal.Description>
</Modal.Content>
</Modal>
<Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
<Button
onClick={() => {
setShowEmailBindModal(true);
}}
>
绑定邮箱地址
</Button>
<Modal
onClose={() => setShowEmailBindModal(false)}
onOpen={() => setShowEmailBindModal(true)}
open={showEmailBindModal}
size={'mini'}
>
<Modal.Header>绑定邮箱地址</Modal.Header>
<Modal.Content>
<Modal.Description>
<Form size='large'>
<Form.Input
fluid
placeholder='输入邮箱地址'
onChange={handleInputChange}
name='email'
type='email'
action={
<Button onClick={sendVerificationCode}>获取验证码</Button>
}
/>
<Form.Input
fluid
placeholder='验证码'
name='email_verification_code'
value={inputs.email_verification_code}
onChange={handleInputChange}
/>
<Button color='teal' fluid size='large' onClick={bindEmail}>
绑定
</Button>
</Form>
</Modal.Description>
</Modal.Content>
</Modal>
</div>
);
};
export default PersonalSetting;
+13
View File
@@ -0,0 +1,13 @@
import { Navigate } from 'react-router-dom';
import { history } from '../helpers';
function PrivateRoute({ children }) {
if (!localStorage.getItem('user')) {
return <Navigate to='/login' state={{ from: history.location }} />;
}
return children;
}
export { PrivateRoute };
+150
View File
@@ -0,0 +1,150 @@
import React, { useEffect, useState } from 'react';
import {
Button,
Form,
Grid,
Header,
Image,
Message,
Segment,
} from 'semantic-ui-react';
import { Link, useNavigate } from 'react-router-dom';
import { API, showError, showInfo, showSuccess } from '../helpers';
const RegisterForm = () => {
const [inputs, setInputs] = useState({
username: '',
password: '',
password2: '',
email: '',
verification_code: '',
});
const { username, password, password2 } = inputs;
const [showEmailVerification, setShowEmailVerification] = useState(false);
useEffect(() => {
let status = localStorage.getItem('status');
if (status) {
status = JSON.parse(status);
setShowEmailVerification(status.email_verification);
}
});
let navigate = useNavigate();
function handleChange(e) {
const { name, value } = e.target;
console.log(name, value);
setInputs((inputs) => ({ ...inputs, [name]: value }));
}
async function handleSubmit(e) {
if (password.length < 8) {
showInfo('密码长度不得小于 8 位!');
return;
}
if (password !== password2) {
showInfo('两次输入的密码不一致');
return;
}
if (username && password) {
const res = await API.post('/api/user/register', inputs);
const { success, message } = res.data;
if (success) {
navigate('/login');
showSuccess('注册成功!');
} else {
showError(message);
}
}
}
const sendVerificationCode = async () => {
if (inputs.email === '') return;
const res = await API.get(`/api/verification?email=${inputs.email}`);
const { success, message } = res.data;
if (success) {
showSuccess('验证码发送成功,请检查你的邮箱!');
} else {
showError(message);
}
};
return (
<Grid textAlign='center' style={{ marginTop: '48px' }}>
<Grid.Column style={{ maxWidth: 450 }}>
<Header as='h2' color='teal' textAlign='center'>
<Image src='/logo.png' /> 新用户注册
</Header>
<Form size='large'>
<Segment>
<Form.Input
fluid
icon='user'
iconPosition='left'
placeholder='输入用户名,最长 12 位'
onChange={handleChange}
name='username'
/>
<Form.Input
fluid
icon='lock'
iconPosition='left'
placeholder='输入密码,最短 8 位,最长 20 位'
onChange={handleChange}
name='password'
type='password'
/>
<Form.Input
fluid
icon='lock'
iconPosition='left'
placeholder='输入密码,最短 8 位,最长 20 位'
onChange={handleChange}
name='password2'
type='password'
/>
{showEmailVerification ? (
<>
<Form.Input
fluid
icon='mail'
iconPosition='left'
placeholder='输入邮箱地址'
onChange={handleChange}
name='email'
type='email'
action={
<Button onClick={sendVerificationCode}>获取验证码</Button>
}
/>
<Form.Input
fluid
icon='lock'
iconPosition='left'
placeholder='输入验证码'
onChange={handleChange}
name='verification_code'
/>
</>
) : (
<></>
)}
<Button color='teal' fluid size='large' onClick={handleSubmit}>
注册
</Button>
</Segment>
</Form>
<Message>
已有账户
<Link to='/login' className='btn btn-link'>
点击登录
</Link>
</Message>
</Grid.Column>
</Grid>
);
};
export default RegisterForm;
+270
View File
@@ -0,0 +1,270 @@
import React, { useEffect, useState } from 'react';
import { Form, Grid } from 'semantic-ui-react';
import { API, showError } from '../helpers';
const SystemSetting = () => {
let [inputs, setInputs] = useState({
PasswordLoginEnabled: '',
PasswordRegisterEnabled: '',
EmailVerificationEnabled: '',
GitHubOAuthEnabled: '',
GitHubClientId: '',
GitHubClientSecret: '',
Notice: '',
SMTPServer: '',
SMTPAccount: '',
SMTPToken: '',
ServerAddress: '',
Footer: '',
WeChatAuthEnabled: '',
WeChatServerAddress: '',
WeChatServerToken: '',
WeChatAccountQRCodeImageURL: '',
});
let originInputs = {};
let [loading, setLoading] = useState(false);
const getOptions = async () => {
const res = await API.get('/api/option');
const { success, message, data } = res.data;
if (success) {
let newInputs = {};
data.forEach((item) => {
newInputs[item.key] = item.value;
});
setInputs(newInputs);
originInputs = newInputs;
} else {
showError(message);
}
};
useEffect(() => {
getOptions().then();
}, []);
const updateOption = async (key, value) => {
setLoading(true);
switch (key) {
case 'PasswordLoginEnabled':
case 'PasswordRegisterEnabled':
case 'EmailVerificationEnabled':
case 'GitHubOAuthEnabled':
case 'WeChatAuthEnabled':
value = inputs[key] === 'true' ? 'false' : 'true';
break;
default:
break;
}
const res = await API.put('/api/option', {
key,
value,
});
const { success, message } = res.data;
if (success) {
setInputs((inputs) => ({ ...inputs, [key]: value }));
} else {
showError(message);
}
setLoading(false);
};
const handleInputChange = async (e, { name, value }) => {
if (
name === 'Notice' ||
name.startsWith('SMTP') ||
name === 'ServerAddress' ||
name === 'GitHubClientId' ||
name === 'GitHubClientSecret' ||
name === 'WeChatServerAddress' ||
name === 'WeChatServerToken' ||
name === 'WeChatAccountQRCodeImageURL'
) {
setInputs((inputs) => ({ ...inputs, [name]: value }));
} else {
await updateOption(name, value);
}
};
const submitServerAddress = async () => {
let ServerAddress = inputs.ServerAddress;
if (ServerAddress.endsWith('/')) {
ServerAddress = ServerAddress.slice(0, ServerAddress.length - 1);
}
await updateOption('ServerAddress', ServerAddress);
};
const submitSMTP = async () => {
if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
await updateOption('SMTPServer', inputs.SMTPServer);
}
if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) {
await updateOption('SMTPAccount', inputs.SMTPAccount);
}
if (
originInputs['SMTPToken'] !== inputs.SMTPToken &&
inputs.SMTPToken !== ''
) {
await updateOption('SMTPToken', inputs.SMTPToken);
}
};
const submitWeChat = async () => {
if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
await updateOption('WeChatServerAddress', inputs.WeChatServerAddress);
}
if (
originInputs['WeChatAccountQRCodeImageURL'] !==
inputs.WeChatAccountQRCodeImageURL
) {
await updateOption(
'WeChatAccountQRCodeImageURL',
inputs.WeChatAccountQRCodeImageURL
);
}
if (
originInputs['WeChatServerToken'] !== inputs.WeChatServerToken &&
inputs.WeChatServerToken !== ''
) {
await updateOption('WeChatServerToken', inputs.WeChatServerToken);
}
};
const submitGitHubOAuth = async () => {
if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
await updateOption('GitHubClientId', inputs.GitHubClientId);
}
if (
originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret &&
inputs.GitHubClientSecret !== ''
) {
await updateOption('GitHubClientSecret', inputs.GitHubClientSecret);
}
};
return (
<Grid columns={1}>
<Grid.Column>
<Form loading={loading}>
<Form.Group widths="equal">
<Form.Input
label="服务器地址"
placeholder="例如:https://yourdomain.com(注意没有最后的斜杠)"
value={inputs.ServerAddress}
name="ServerAddress"
onChange={handleInputChange}
/>
</Form.Group>
<Form.Button onClick={submitServerAddress}>
更新服务器地址
</Form.Button>
<Form.Group inline>
<Form.Checkbox
checked={inputs.PasswordLoginEnabled === 'true'}
label="允许密码登录"
name="PasswordLoginEnabled"
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.PasswordRegisterEnabled === 'true'}
label="允许通过密码进行注册"
name="PasswordRegisterEnabled"
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.EmailVerificationEnabled === 'true'}
label="强制邮箱验证"
name="EmailVerificationEnabled"
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.GitHubOAuthEnabled === 'true'}
label="允许通过 GitHub 账户登录 & 注册"
name="GitHubOAuthEnabled"
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.WeChatAuthEnabled === 'true'}
label="允许通过微信登录 & 注册"
name="WeChatAuthEnabled"
onChange={handleInputChange}
/>
</Form.Group>
<Form.Group widths={3}>
<Form.Input
label="SMTP 服务器地址"
name="SMTPServer"
onChange={handleInputChange}
autoComplete="off"
value={inputs.SMTPServer}
/>
<Form.Input
label="SMTP 账户"
name="SMTPAccount"
onChange={handleInputChange}
autoComplete="off"
value={inputs.SMTPAccount}
/>
<Form.Input
label="SMTP 访问凭证"
name="SMTPToken"
onChange={handleInputChange}
type="password"
autoComplete="off"
value={inputs.SMTPToken}
/>
</Form.Group>
<Form.Button onClick={submitSMTP}>保存 SMTP 设置</Form.Button>
<Form.Group widths={3}>
<Form.Input
label="GitHub Client ID"
name="GitHubClientId"
onChange={handleInputChange}
autoComplete="off"
value={inputs.GitHubClientId}
/>
<Form.Input
label="GitHub Client Secret"
name="GitHubClientSecret"
onChange={handleInputChange}
type="password"
autoComplete="off"
value={inputs.GitHubClientSecret}
/>
</Form.Group>
<Form.Button onClick={submitGitHubOAuth}>
保存 GitHub OAuth 设置
</Form.Button>
<Form.Group widths={3}>
<Form.Input
label="WeChat Server 服务器地址"
name="WeChatServerAddress"
placeholder="例如:https://yourdomain.com(注意没有最后的斜杠)"
onChange={handleInputChange}
autoComplete="off"
value={inputs.WeChatServerAddress}
/>
<Form.Input
label="WeChat Server 访问凭证"
name="WeChatServerToken"
type="password"
onChange={handleInputChange}
autoComplete="off"
value={inputs.WeChatServerToken}
/>
<Form.Input
label="微信公众号二维码图片链接"
name="WeChatAccountQRCodeImageURL"
onChange={handleInputChange}
autoComplete="off"
value={inputs.WeChatAccountQRCodeImageURL}
/>
</Form.Group>
<Form.Button onClick={submitWeChat}>保存微信登录设置</Form.Button>
</Form>
</Grid.Column>
</Grid>
);
};
export default SystemSetting;
+256
View File
@@ -0,0 +1,256 @@
import React, { useEffect, useState } from 'react';
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;
function renderRole(role) {
switch (role) {
case 1:
return <Label>普通用户</Label>;
case 10:
return <Label color="yellow">管理员</Label>;
case 100:
return <Label color="orange">超级管理员</Label>;
default:
return <Label color="red">未知身份</Label>;
}
}
const UsersTable = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(1);
const [searchKeyword, setSearchKeyword] = useState('');
const [searching, setSearching] = useState(false);
const loadUsers = async () => {
const res = await API.get('/api/user');
const { success, message, data } = res.data;
if (success) {
setUsers(data);
} else {
showError(message);
}
setLoading(false);
};
const onPaginationChange = (e, { activePage }) => {
setActivePage(activePage);
};
useEffect(() => {
loadUsers()
.then()
.catch((reason) => {
showError(reason);
});
}, []);
const manageUser = (username, action) => {
(async () => {
const res = await API.post('/api/user/manage', {
username,
action,
});
const { success, message } = res.data;
if (success) {
showSuccess('操作成功完成!');
await loadUsers();
} else {
showError(message);
}
})();
};
const renderStatus = (status, id) => {
switch (status) {
case 1:
return '已激活';
case 2:
return '已封禁';
default:
return '未知状态';
}
};
const searchUsers = async () => {
setSearching(true);
const res = await API.get(`/api/user/search?keyword=${searchKeyword}`);
const { success, message, data } = res.data;
if (success) {
setUsers(data);
} else {
showError(message);
}
setSearching(false);
};
const handleKeywordChange = async (e, { name, value }) => {
setSearchKeyword(value);
};
const sortUser = (key) => {
if (users.length === 0) return;
setLoading(true);
let sortedUsers = [...users];
sortedUsers.sort((a, b) => {
return ('' + a[key]).localeCompare(b[key]);
});
if (sortedUsers[0].id === users[0].id) {
sortedUsers.reverse();
}
setUsers(sortedUsers);
setLoading(false);
};
return (
<>
<Form onSubmit={searchUsers}>
<Form.Input
icon="search"
fluid
iconPosition="left"
placeholder="搜索用户的 ID,用户名,显示名称,以及邮箱地址 ..."
value={searchKeyword}
loading={searching}
onChange={handleKeywordChange}
/>
</Form>
<Table basic>
<Table.Header>
<Table.Row>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortUser('username');
}}
>
用户名
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortUser('display_name');
}}
>
显示名称
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortUser('email');
}}
>
邮箱地址
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortUser('role');
}}
>
用户角色
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortUser('status');
}}
>
状态
</Table.HeaderCell>
<Table.HeaderCell>操作</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{users
.slice((activePage - 1) * itemsPerPage, activePage * itemsPerPage)
.map((user, idx) => {
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>
<div>
<Button
size={'small'}
positive
onClick={() => {
manageUser(user.username, 'promote');
}}
>
提升
</Button>
<Button
size={'small'}
color={'yellow'}
onClick={() => {
manageUser(user.username, 'demote');
}}
>
降级
</Button>
<Button
size={'small'}
negative
onClick={() => {
manageUser(user.username, 'delete');
}}
>
删除
</Button>
<Button
size={'small'}
onClick={() => {
manageUser(
user.username,
user.status === 1 ? 'disable' : 'enable'
);
}}
>
{user.status === 1 ? '禁用' : '启用'}
</Button>
<Button
size={'small'}
as={Link}
to={'/user/edit/' + user.id}
>
编辑
</Button>
</div>
</Table.Cell>
</Table.Row>
);
})}
</Table.Body>
<Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan="6">
<Button size="small" as={Link} to="/user/add" loading={loading}>
添加新的用户
</Button>
<Pagination
floated="right"
activePage={activePage}
onPageChange={onPaginationChange}
size="small"
siblingRange={1}
totalPages={Math.ceil(users.length / itemsPerPage)}
/>
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
</Table>
</>
);
};
export default UsersTable;