init: reinitialize from gin-template
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user