chore: sync with gin-template

This commit is contained in:
JustSong
2022-11-22 11:04:06 +08:00
parent 8247192977
commit 5913266969
41 changed files with 813 additions and 490 deletions
+7 -7
View File
@@ -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>
)}
+22 -19
View File
@@ -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>
+2 -2
View File
@@ -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}
+10 -11
View File
@@ -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();
+2 -2
View File
@@ -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}
+35 -4
View File
@@ -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}
+61 -8
View File
@@ -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>
+48 -5
View File
@@ -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>
+154 -45
View File
@@ -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>
+75 -31
View File
@@ -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>