比較提交
11 次程式碼提交
45df1f7409
...
master
作者 | SHA1 | 日期 | |
---|---|---|---|
9b3c1a7b55 | |||
c80ce3d31b | |||
bbcfe3e324 | |||
a5358fef87 | |||
b0f33d0bf4 | |||
5910b2f560 | |||
cfa6734461 | |||
e2fb2ccc2b | |||
26b3a50f20 | |||
2967009dfb | |||
1b0e9d3042 |
@ -6,4 +6,4 @@
|
|||||||
请根据`env.example.json`的格式按照自己的需求编写`env.json`,或者直接运行`cp env.example.json env.json`,**否则无法正确处理网络请求!**
|
请根据`env.example.json`的格式按照自己的需求编写`env.json`,或者直接运行`cp env.example.json env.json`,**否则无法正确处理网络请求!**
|
||||||
|
|
||||||
### 网络请求的用法
|
### 网络请求的用法
|
||||||
引入./src/helper/axios.js(请以相对路径引入),尽情使用其中的post、get方法,它们会返回一个Promise(或可被视作Promise的AsyncFunction),且不会以抛出异常的方式通知网络错误,它会在服务器返回内容中插入属性`networkStatus`,**当值为200时才可以视作正确的服务器响应**,当值为-1时说明这是用户的网络错误,**你不需要处理200以外的情况,代码能够发出弹窗并建议用户重试**
|
引入./src/helper/axios.js(请以相对路径引入),尽情使用其中的post、get方法,它们会返回一个Promise(或可被视作Promise的AsyncFunction),且为方便不会以抛出异常的方式通知网络错误。它会在服务器返回内容中插入属性`networkStatus`,**当值为200时才可以视作正确的服务器响应**,当值为-1时说明这是用户的网络错误。**你不需要处理200以外的情况,代码能够发出弹窗并建议用户重试**
|
15
src/App.css
15
src/App.css
@ -101,6 +101,21 @@ a.btn {
|
|||||||
height: 30px;
|
height: 30px;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
.nav-item {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
.nav-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.nav-link:active {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
b {
|
b {
|
||||||
color: #9E0004;
|
color: #9E0004;
|
||||||
|
13
src/App.js
13
src/App.js
@ -1,5 +1,5 @@
|
|||||||
import './App.css';
|
import './App.css';
|
||||||
import { Redirect, Route, SingleRouter } from './components/SingleRouter/SingleRouter';
|
import { Route, SingleRouter } from './components/SingleRouter/SingleRouter';
|
||||||
import { AppContainer } from './index/index';
|
import { AppContainer } from './index/index';
|
||||||
import { UploadContainer } from './upload/upload';
|
import { UploadContainer } from './upload/upload';
|
||||||
import { LogInContainer } from './login/login';
|
import { LogInContainer } from './login/login';
|
||||||
@ -12,13 +12,13 @@ class App extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
name: '',
|
name: '',
|
||||||
role: -1
|
role: 0
|
||||||
};
|
};
|
||||||
this.setUserData = this.setUserData.bind(this);
|
this.setUserData = this.setUserData.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserData({ name, role }) {
|
setUserData({ name, role, avatar }) {
|
||||||
this.setState({ name, role });
|
this.setState({ name, role, avatar });
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -29,15 +29,10 @@ class App extends Component {
|
|||||||
<Route path="/upload" component={UploadContainer} />
|
<Route path="/upload" component={UploadContainer} />
|
||||||
<Route path="/login" component={LogInContainer} />
|
<Route path="/login" component={LogInContainer} />
|
||||||
<Route path="/admin/review" component={ReviewContainer} />
|
<Route path="/admin/review" component={ReviewContainer} />
|
||||||
<Route component={CheckLogIn} />
|
|
||||||
</SingleRouter>
|
</SingleRouter>
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function CheckLogIn() {
|
|
||||||
return localStorage.getItem('jwt') ? null : <Redirect from={/^\/.*/} to="/login" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
@ -7,6 +7,7 @@ import { apis } from '../../helper/apis';
|
|||||||
import { alert, confirmWithClose } from '../../helper/alert';
|
import { alert, confirmWithClose } from '../../helper/alert';
|
||||||
import { UserContext } from '../../helper/Context';
|
import { UserContext } from '../../helper/Context';
|
||||||
import Spinner from '../../components/Spinner/Spinner';
|
import Spinner from '../../components/Spinner/Spinner';
|
||||||
|
import { Link } from '../../components/SingleRouter/SingleRouter';
|
||||||
|
|
||||||
export class ReviewContainer extends Component {
|
export class ReviewContainer extends Component {
|
||||||
static contextType = UserContext;
|
static contextType = UserContext;
|
||||||
@ -66,8 +67,13 @@ export class ReviewContainer extends Component {
|
|||||||
<div className="sdu">
|
<div className="sdu">
|
||||||
<img src={images.icon} className="sdu-logo" alt="logo" />
|
<img src={images.icon} className="sdu-logo" alt="logo" />
|
||||||
<img src={images.name} className="sdu-name" alt="" />
|
<img src={images.name} className="sdu-name" alt="" />
|
||||||
|
<div className="nav">
|
||||||
|
<div className="nav-item">
|
||||||
|
<Link to="/" className="nav-link">首页</Link>
|
||||||
</div>
|
</div>
|
||||||
<UserControl />
|
</div>
|
||||||
|
</div>
|
||||||
|
<UserControl pageAuthLevel={2} />
|
||||||
</div>
|
</div>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
{
|
{
|
||||||
|
151
src/components/AvatarUnit/AvatarUnit.css
Normal file
151
src/components/AvatarUnit/AvatarUnit.css
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
.user-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar > img {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-edit {
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
width: 160px;
|
||||||
|
height: 26px;
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 10px;
|
||||||
|
left: -60px;
|
||||||
|
filter: opacity(0);
|
||||||
|
transition-property: width, height, filter;
|
||||||
|
transition-duration: .3s;
|
||||||
|
transition-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-edit > .pointer {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 10px;
|
||||||
|
top: -10px;
|
||||||
|
left: 70px;
|
||||||
|
}
|
||||||
|
.user-avatar-edit > .pointer > svg {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-edit-tip {
|
||||||
|
width: 160px;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
padding-left: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-edit.expand {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-edit-tip,
|
||||||
|
.avatar-upload > * {
|
||||||
|
position: absolute;
|
||||||
|
filter: opacity(0);
|
||||||
|
transition: filter .3s ease-out;
|
||||||
|
}
|
||||||
|
.user-avatar-edit-tip.show,
|
||||||
|
.avatar-upload > .show,
|
||||||
|
.user-avatar-edit.show {
|
||||||
|
filter: opacity(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-edit-content {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.avatar-upload {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
.avatar-upload .upload-unit-progress.show {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.avatar-upload .upload-unit-progress > .progress-detail {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.upload-unit-progress > .icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
.avatar-preview.show {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.avatar-preview > .upload-btns {
|
||||||
|
margin: 0;
|
||||||
|
bottom: 10px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
position: absolute;
|
||||||
|
right: 3px;
|
||||||
|
top: 3px;
|
||||||
|
z-index: 2;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #e5e5e5bb;
|
||||||
|
border-radius: 4px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
.btn-close > svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-edit {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-left: -1px;
|
||||||
|
width: 360px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.nickname-edit > label {
|
||||||
|
line-height: 1.7em;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.nickname-edit > input {
|
||||||
|
flex-grow: 1;
|
||||||
|
border: none;
|
||||||
|
background-color: #eee;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding: 0 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.nickname-edit > .btn {
|
||||||
|
padding: .1em 0;
|
||||||
|
width: 60px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
169
src/components/AvatarUnit/AvatarUnit.js
Normal file
169
src/components/AvatarUnit/AvatarUnit.js
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import { useState, useCallback, useRef, useEffect } from "react";
|
||||||
|
import Spinner from '../Spinner/Spinner';
|
||||||
|
import './AvatarUnit.css';
|
||||||
|
import { multiFormPost, post } from '../../helper/axios';
|
||||||
|
import { alert } from '../../helper/alert';
|
||||||
|
import { apis } from "../../helper/apis";
|
||||||
|
|
||||||
|
export default function AvatarUnit({ avatar, nickname, onChangeAvatar, showTip }) {
|
||||||
|
const [showBubble, setShowBubble] = useState(showTip);
|
||||||
|
const [showUpload, setShowUpload] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [uploaded, setUploaded] = useState(false);
|
||||||
|
const [dataURL, setDataURL] = useState(null);
|
||||||
|
const [file, setFile] = useState(null);
|
||||||
|
const [nicknameNow, setNicknameNow] = useState('');
|
||||||
|
const [nicknameUploading, setNicknameUploading] = useState(false);
|
||||||
|
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNicknameNow(nickname || '');
|
||||||
|
}, [nickname]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// setShowBubble(!localStorage.getItem("avatarTipShown"));
|
||||||
|
// localStorage.setItem("avatarTipShown", true);
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setShowBubble(false);
|
||||||
|
setShowUpload(false);
|
||||||
|
setLoading(false);
|
||||||
|
setLoaded(false);
|
||||||
|
setUploading(false);
|
||||||
|
setUploaded(false);
|
||||||
|
setFile(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = useCallback(file => {
|
||||||
|
if (file) {
|
||||||
|
if (!["image/jpeg", "image/png", "image/gif"].includes(file.type)) {
|
||||||
|
alert('请不要上传jpg、png、gif格式以外的文件!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
setFile(file);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
setDataURL(reader.result);
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.value = "";
|
||||||
|
}
|
||||||
|
setLoaded(true);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
setLoaded(false);
|
||||||
|
setFile(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUpload = useCallback(() => {
|
||||||
|
if (file) {
|
||||||
|
setUploading(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("image", file);
|
||||||
|
multiFormPost(apis.uploadAvatar, formData).then(data => {
|
||||||
|
setUploading(false);
|
||||||
|
if (data.networkStatus !== 200) return;
|
||||||
|
if (data.status) {
|
||||||
|
setUploaded(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
handleClose();
|
||||||
|
onChangeAvatar(data.data);
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alert('头像上传失败:' + data.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [file, onChangeAvatar]);
|
||||||
|
|
||||||
|
const handleChangeNickname = useCallback(() => {
|
||||||
|
setNicknameUploading(true);
|
||||||
|
post(apis.updateNickname, { nickname: nicknameNow }).then(data => {
|
||||||
|
setNicknameUploading(false);
|
||||||
|
if (data.networkStatus !== 200) return;
|
||||||
|
if (data.status) {
|
||||||
|
alert('昵称已修改为:' + nicknameNow);
|
||||||
|
setNicknameNow(nicknameNow);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alert('修改失败:' + data.data.map(item => item.msg).join(','));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [nicknameNow]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="user-avatar">
|
||||||
|
<img src={avatar} alt="user avatar" onClick={() => {
|
||||||
|
setShowBubble(true);
|
||||||
|
setShowUpload(true);
|
||||||
|
}} />
|
||||||
|
<div className={"user-avatar-edit" + (showUpload ? ' expand' : '') + (showBubble ? ' show' : '')}>
|
||||||
|
<div className="pointer">
|
||||||
|
<svg viewBox="0 0 20 10">
|
||||||
|
<path stroke="#e5e5e5" d="M0.112042+10.4465C1.43209+10.4306+4.24584+9.35904+5.0218+8.01502L6.77358+4.98086L8.52535+1.94671C9.30131+0.602693+10.5594+0.602693+11.3354+1.94671L13.0871+4.98086L14.8389+8.01502C15.6149+9.35904+18.4986+10.4864+20.0163+10.4757" fill="#f7f7f7"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-close" onClick={handleClose}>
|
||||||
|
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M597.333333 512l284.444445 284.444444c2.929778 3.328 2.929778 22.357333 0 28.444445l-56.888889 56.888889c-6.087111 2.929778-25.116444 2.929778-28.444445 0L512 597.333333 227.555556 881.777778c-3.328 2.929778-22.328889 2.929778-28.444445 0l-56.888889-56.888889c-2.929778-6.087111-2.929778-25.116444 0-28.444445l284.444445-284.444444L142.222222 227.555556c-2.929778-3.328-2.929778-22.328889 0-28.444445l56.888889-56.888889c6.115556-2.929778 25.116444-2.929778 28.444445 0l284.444444 284.444445L796.444444 142.222222c3.328-2.929778 22.357333-2.929778 28.444445 0l56.888889 56.888889c2.929778 6.115556 2.929778 25.116444 0 28.444445L597.333333 512z" fill="#4a4a4a"></path></svg>
|
||||||
|
</button>
|
||||||
|
<div className="user-avatar-edit-content">
|
||||||
|
<span className={'user-avatar-edit-tip' + (showBubble && !showUpload ? ' show' : '')}>点击图标修改头像和昵称</span>
|
||||||
|
<div className="avatar-upload">
|
||||||
|
<div className={"upload-unit-progress" + (uploaded ? ' show' : '')}>
|
||||||
|
<div className="icon">
|
||||||
|
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M939.717 106.665l-304.049 324.529-78.769 85.071-225.28 240.246-123.668-115.003-129.182-121.305-78.769 84.283 129.969 121.305 33.083 31.508 129.969 121.305 44.111 40.96 304.049-324.529 78.769-85.071 304.049-324.529-84.283-78.769z" fill="#47c74c"></path></svg>
|
||||||
|
</div>
|
||||||
|
<div className="progress-detail">
|
||||||
|
上传成功
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"upload-unit-progress" + (loading || uploading ? ' show' : '')}>
|
||||||
|
<Spinner isGray />
|
||||||
|
<div className="progress-detail">
|
||||||
|
{loading ? '加载中' : uploading ? '上传中' : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"avatar-preview" + (loaded && !uploading && !uploaded ? ' show' : '')} style={{ backgroundImage: `url(${dataURL})` }}>
|
||||||
|
<div className="upload-btns">
|
||||||
|
<button className="btn btn-circle btn-ok" onClick={handleUpload}>
|
||||||
|
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M939.717 106.665l-304.049 324.529-78.769 85.071-225.28 240.246-123.668-115.003-129.182-121.305-78.769 84.283 129.969 121.305 33.083 31.508 129.969 121.305 44.111 40.96 304.049-324.529 78.769-85.071 304.049-324.529-84.283-78.769z" fill="#ffffff"></path></svg>
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-circle btn-gray" onClick={handleCancel}>
|
||||||
|
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M597.333333 512l284.444445 284.444444c2.929778 3.328 2.929778 22.357333 0 28.444445l-56.888889 56.888889c-6.087111 2.929778-25.116444 2.929778-28.444445 0L512 597.333333 227.555556 881.777778c-3.328 2.929778-22.328889 2.929778-28.444445 0l-56.888889-56.888889c-2.929778-6.087111-2.929778-25.116444 0-28.444445l284.444445-284.444444L142.222222 227.555556c-2.929778-3.328-2.929778-22.328889 0-28.444445l56.888889-56.888889c6.115556-2.929778 25.116444-2.929778 28.444445 0l284.444444 284.444445L796.444444 142.222222c3.328-2.929778 22.357333-2.929778 28.444445 0l56.888889 56.888889c2.929778 6.115556 2.929778 25.116444 0 28.444445L597.333333 512z" fill="#4a4a4a"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"upload-image-btn" + (showUpload && !file ? ' show' : '')}>
|
||||||
|
<input type="file" accept="image/*" multiple={false} onChange={e => handleFileChange(e.target.files[0])} ref={inputRef} />
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 54"><path d="M60.342,102.091l5.331-8a2.249,2.249,0,0,1,1.872-1h18a2.246,2.246,0,0,1,1.872,1l5.332,8h7.094a6.72,6.72,0,0,1,6.7,6.7v31.6a6.722,6.722,0,0,1-6.7,6.7h-46.6a6.722,6.722,0,0,1-6.7-6.7v-31.6a6.72,6.72,0,0,1,6.7-6.7h7.094Zm5.407,0H87.341l-3-4.5H68.751l-3,4.5Zm10.8,35.25a12.75,12.75,0,1,0-12.75-12.75A12.764,12.764,0,0,0,76.545,137.341Zm0-21a8.25,8.25,0,1,1-8.25,8.25A8.261,8.261,0,0,1,76.545,116.341Z" transform="translate(-46.546 -93.091)" fill="#8A8A8A"/></svg>
|
||||||
|
<div className="upload-image-btn-text">添加图片</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="nickname-edit">
|
||||||
|
<label>昵称:</label>
|
||||||
|
<input type="text" value={nicknameNow} onChange={e => setNicknameNow(e.target.value)} />
|
||||||
|
<button className="btn btn-primary" onClick={handleChangeNickname} disabled={nicknameUploading}>
|
||||||
|
{
|
||||||
|
nicknameUploading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
'保存'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
8
src/components/ImagePreview/ImagePreview.css
Normal file
8
src/components/ImagePreview/ImagePreview.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.preview {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
.preview > img {
|
||||||
|
height: 200px;
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
69
src/components/ImagePreview/ImagePreview.js
Normal file
69
src/components/ImagePreview/ImagePreview.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import './ImagePreview.css';
|
||||||
|
|
||||||
|
import { useCallback, useRef, useState, useEffect, Fragment } from 'react';
|
||||||
|
import Spinner from '../Spinner/Spinner';
|
||||||
|
|
||||||
|
export default function ImagePreview({ image }) {
|
||||||
|
const [isScaled, setIsScaled] = useState(false);
|
||||||
|
const [transform, setTransform] = useState('');
|
||||||
|
const imgRef = useRef(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const imgWidth = useRef(0), imgHeight = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
let img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
setLoading(false);
|
||||||
|
imgWidth.current = img.width;
|
||||||
|
imgHeight.current = img.height;
|
||||||
|
};
|
||||||
|
img.src = image;
|
||||||
|
}, [image]);
|
||||||
|
|
||||||
|
const scale = useCallback(() => {
|
||||||
|
if (isScaled) return;
|
||||||
|
let el = imgRef.current;
|
||||||
|
let { x, y } = el.getBoundingClientRect();
|
||||||
|
let { innerWidth, innerHeight } = window;
|
||||||
|
let initialScale = 200 / imgHeight.current,
|
||||||
|
terminalScale = Math.min(1, Math.min((innerWidth - 10) / imgWidth.current, (innerHeight - 10) / imgHeight.current));
|
||||||
|
setTransform(`translate(${x + (imgWidth.current * initialScale - innerWidth) / 2}px, ${y + (200 - innerHeight) / 2}px) scale(${initialScale})`);
|
||||||
|
setIsScaled(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setTransform(`translate(0, 0) scale(${terminalScale})`);
|
||||||
|
}, 0);
|
||||||
|
}, [isScaled]);
|
||||||
|
|
||||||
|
const unscale = useCallback(() => {
|
||||||
|
if (!isScaled) return;
|
||||||
|
let el = imgRef.current;
|
||||||
|
let { x, y } = el.getBoundingClientRect();
|
||||||
|
let { innerWidth, innerHeight } = window;
|
||||||
|
let initialScale = 200 / imgHeight.current;
|
||||||
|
setTransform(`translate(${x + (imgWidth.current * initialScale - innerWidth) / 2}px, ${y + (200 - innerHeight) / 2}px) scale(${initialScale})`);
|
||||||
|
setIsScaled(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsScaled(false);
|
||||||
|
}, 300);
|
||||||
|
}, [isScaled]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="preview">
|
||||||
|
{
|
||||||
|
loading ? (
|
||||||
|
<div className="spinner-wrap">
|
||||||
|
<Spinner isGray />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Fragment>
|
||||||
|
<img src={image} alt="图片" onClick={scale} ref={imgRef} />
|
||||||
|
<div className={isScaled ? 'scaled-wrap open' : 'scaled-wrap'} onClick={unscale}>
|
||||||
|
<img className="scaled-image" src={image} alt="图片" style={{ transform }} />
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -9,6 +9,14 @@
|
|||||||
animation: spin 1s steps(8, start) infinite;
|
animation: spin 1s steps(8, start) infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spinner-wrap {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
|
@ -56,6 +56,8 @@
|
|||||||
color: white;
|
color: white;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
width: 100px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
.progress-circle {
|
.progress-circle {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { useEffect, useContext, Fragment } from 'react';
|
||||||
|
|
||||||
import './userControl.css';
|
import './userControl.css';
|
||||||
import { images } from '../../resources.json';
|
import { images } from '../../resources.json';
|
||||||
import { UserContext } from '../../helper/Context';
|
import { UserContext } from '../../helper/Context';
|
||||||
@ -6,56 +8,91 @@ import { get } from '../../helper/axios';
|
|||||||
import { apis } from '../../helper/apis';
|
import { apis } from '../../helper/apis';
|
||||||
import { alert } from '../../helper/alert';
|
import { alert } from '../../helper/alert';
|
||||||
import History from '../../helper/history';
|
import History from '../../helper/history';
|
||||||
|
import AvatarUnit from '../AvatarUnit/AvatarUnit';
|
||||||
|
|
||||||
export default function UserControl(props) {
|
// page level: 0: everyone, 1: login needed, 2: admin only
|
||||||
return (
|
export default function UserControl({ pageAuthLevel, buttonOnly }) {
|
||||||
<UserContext.Consumer>
|
const { userData, setUserData } = useContext(UserContext);
|
||||||
{({ userData, setUserData }) => (
|
useEffect(() => {
|
||||||
userData.role === 2
|
if (userData.role === -1) return;
|
||||||
? (
|
if (userData.role === 0 && localStorage.getItem('jwt')) {
|
||||||
<div className="user">
|
// 没有用户信息,但是有登录信息
|
||||||
<button
|
setUserData({ role: -1, name: '加载中' });
|
||||||
className="btn btn-hollow btn-straight"
|
|
||||||
onClick={() => {
|
|
||||||
localStorage.setItem('jwt', '');
|
|
||||||
setUserData({ role: -1, name: '' });
|
|
||||||
History.force('/login');
|
|
||||||
}}
|
|
||||||
>退出审核</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<div className="user">
|
|
||||||
{
|
|
||||||
userData.role === -1
|
|
||||||
? (
|
|
||||||
<Spinner />
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<div className="user-avatar">
|
|
||||||
<img src={images.avatar} alt="user avatar" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<div className="user-name">{userData.name || "加载中"}</div>
|
|
||||||
{
|
|
||||||
(() => {
|
|
||||||
if (userData.role === 1 && History.getHref().match(/^\/admin.*/))
|
|
||||||
History.force('/');
|
|
||||||
if (userData.role !== -1 || !localStorage.getItem('jwt')) return null;
|
|
||||||
get(apis.getProfile).then(({ data, status, networkStatus }) => {
|
get(apis.getProfile).then(({ data, status, networkStatus }) => {
|
||||||
if (networkStatus !== 200) return;
|
if (networkStatus !== 200) return;
|
||||||
if (!status) return alert('获取用户信息失败:' + data + ',请稍候刷新再试');
|
if (!status) return alert('获取用户信息失败:' + data + ',请稍候刷新再试');
|
||||||
setUserData({
|
setUserData({
|
||||||
name: data.realName,
|
name: data.realName,
|
||||||
role: data.role
|
role: data.role,
|
||||||
|
avatar: data.avatar,
|
||||||
|
nickname: data.nickname
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return null;
|
return;
|
||||||
})()
|
}
|
||||||
|
|
||||||
|
if (userData.role < pageAuthLevel) {
|
||||||
|
// 用户权限不足,依照页面权限跳转
|
||||||
|
if (userData.role === 1) {
|
||||||
|
History.force('/');
|
||||||
|
alert('您没有权限访问该页面,退出登录后可以使用有权限的账号登录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
History.force('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [pageAuthLevel, setUserData, userData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserContext.Consumer>
|
||||||
|
{({ userData, setUserData }) => (
|
||||||
|
<div className="user">
|
||||||
|
{
|
||||||
|
userData.role === -1
|
||||||
|
? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<Fragment>
|
||||||
|
{buttonOnly || (
|
||||||
|
<Fragment>
|
||||||
|
{
|
||||||
|
userData.role > 0 && (
|
||||||
|
<AvatarUnit
|
||||||
|
avatar={userData.avatar || images.avatar}
|
||||||
|
onChangeAvatar={url => setUserData({ ...userData, avatar: url })}
|
||||||
|
showTip={!userData.avatar}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
userData.role !== 0 && (
|
||||||
|
<div className="user-name">{userData.name}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="btn btn-hollow btn-straight"
|
||||||
|
onClick={() => {
|
||||||
|
localStorage.setItem('jwt', '');
|
||||||
|
setUserData({ role: 0, name: '' });
|
||||||
|
History.force('/login');
|
||||||
|
}}
|
||||||
|
>{userData.role > 0 ? '退出账号' : '登录'}</button>
|
||||||
|
{
|
||||||
|
userData.role === 2 && pageAuthLevel !== 2 && (
|
||||||
|
<button
|
||||||
|
className="btn btn-light btn-straight"
|
||||||
|
onClick={() => {
|
||||||
|
History.push('/admin/review');
|
||||||
|
}}
|
||||||
|
>进入审核</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</UserContext.Consumer>
|
</UserContext.Consumer>
|
||||||
);
|
);
|
||||||
|
@ -4,16 +4,8 @@
|
|||||||
height: 50px;
|
height: 50px;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
color: white;
|
color: white;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
.user > :not(:first-child) {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-avatar {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-avatar > img {
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
@ -4,6 +4,8 @@ export const apis = {
|
|||||||
|
|
||||||
login: backEndBaseURL + "/user/login",
|
login: backEndBaseURL + "/user/login",
|
||||||
getProfile: backEndBaseURL + "/user/me",
|
getProfile: backEndBaseURL + "/user/me",
|
||||||
|
uploadAvatar: backEndBaseURL + "/user/uploadAvatar",
|
||||||
|
updateNickname: backEndBaseURL + "/user/updateNickname",
|
||||||
|
|
||||||
submitMessage: backEndBaseURL + "/post/submit",
|
submitMessage: backEndBaseURL + "/post/submit",
|
||||||
listEssence: backEndBaseURL + "/post/listPublished?page=1",
|
listEssence: backEndBaseURL + "/post/listPublished?page=1",
|
||||||
|
@ -3,7 +3,7 @@ import { failed } from './alert';
|
|||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import History from './history';
|
import History from './history';
|
||||||
|
|
||||||
export function get(url) {
|
export function get(url, evoker) {
|
||||||
return send(
|
return send(
|
||||||
axios.get(url, {
|
axios.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
@ -11,10 +11,13 @@ export function get(url) {
|
|||||||
"Allow-Control-Allow-Origin": "*"
|
"Allow-Control-Allow-Origin": "*"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{ fn: () => get(url), identifier: 'get:' + url }
|
{
|
||||||
|
fetcher: () => get(url, evoker),
|
||||||
|
identifier: 'get:' + url + (evoker ? `@${evoker}` : '')
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export function post(url, data) {
|
export function post(url, data, evoker) {
|
||||||
return send(
|
return send(
|
||||||
axios.post(url, qs.stringify(data), {
|
axios.post(url, qs.stringify(data), {
|
||||||
headers: {
|
headers: {
|
||||||
@ -22,7 +25,24 @@ export function post(url, data) {
|
|||||||
"Allow-Control-Allow-Origin": "*"
|
"Allow-Control-Allow-Origin": "*"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{ fn: () => post(url, data), identifier: 'post:' + url + ' ' + JSON.stringify(data) }
|
{
|
||||||
|
fetcher: () => post(url, data, evoker),
|
||||||
|
identifier: 'post:' + url + (evoker ? `@${evoker}` : '')
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function multiFormPost(url, data, evoker) {
|
||||||
|
return send(
|
||||||
|
axios.post(url, data, {
|
||||||
|
headers: {
|
||||||
|
Authorization: localStorage.getItem('jwt'),
|
||||||
|
"Allow-Control-Allow-Origin": "*"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
fetcher: () => multiFormPost(url, data, evoker),
|
||||||
|
identifier: 'mfPost:' + url + (evoker ? `@${evoker}` : '')
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,9 +50,19 @@ const waitToSend = [];
|
|||||||
|
|
||||||
async function send(xhr, retryConf) {
|
async function send(xhr, retryConf) {
|
||||||
if (waitToSend.length) {
|
if (waitToSend.length) {
|
||||||
|
// 等待列表不为空,说明需要等待用户决定是否重试,这也提高了目前请求的成功率
|
||||||
|
return await new Promise(res => {
|
||||||
if (waitToSend.every(retryConfItem => retryConfItem.identifier !== retryConf.identifier))
|
if (waitToSend.every(retryConfItem => retryConfItem.identifier !== retryConf.identifier))
|
||||||
waitToSend.push(retryConf);
|
waitToSend.push({ ...retryConf, resolver: res });
|
||||||
return;
|
else
|
||||||
|
waitToSend
|
||||||
|
.filter(retryConfItem => retryConfItem.identifier === retryConf.identifier)
|
||||||
|
.forEach(retryConfItem => {
|
||||||
|
retryConfItem.resolver({ networkStatus: -2, status: false });
|
||||||
|
retryConfItem.resolver = res;
|
||||||
|
});
|
||||||
|
// 虽然说理论上filter的结果只会有一个,但是还是要forEach一下,因为写起来方便hhhh
|
||||||
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { data } = await xhr;
|
const { data } = await xhr;
|
||||||
@ -49,21 +79,50 @@ async function send(xhr, retryConf) {
|
|||||||
};
|
};
|
||||||
if (err?.response?.status === 401) {
|
if (err?.response?.status === 401) {
|
||||||
History.force('/login');
|
History.force('/login');
|
||||||
|
feedWaitList(failData);
|
||||||
|
// 若是认证问题导致的错误,则没有必要继续后面等待的请求,且强制跳转后重试可能会导致已卸载的组件被更新
|
||||||
return failData;
|
return failData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (waitToSend.length) return failData;
|
||||||
waitToSend.push(retryConf);
|
waitToSend.push(retryConf);
|
||||||
|
console.log(err);
|
||||||
|
// 等待列表不为空时弹框要么出现了要么就是在消失的路上,没有办法给予用户点击重试的机会,所以交由外部逻辑处理
|
||||||
|
|
||||||
|
const failPostProceess = () => {
|
||||||
|
waitToSend.splice(waitToSend.indexOf(retryConf), 1);
|
||||||
|
return failData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注意,理论上带有时间戳的请求是不可以重试的,但是这里不做那方面考虑,如果未来有需要,可以自己实现一个刷新时间戳重试的逻辑
|
||||||
if (err.message === 'Network Error')
|
if (err.message === 'Network Error')
|
||||||
return await failed('您的设备似乎断网了,请检查网络后重试或刷新', flushWaitList) || failData;
|
return (await failed('您的设备似乎断网了,或者服务器发生了问题,请检查网络后重试或刷新', flushWaitList(retryConf))) || failPostProceess();
|
||||||
if (!err?.response?.status)
|
if (!err?.response?.status)
|
||||||
return await failed('请求发生问题:' + err.message, flushWaitList) || failData;
|
return (await failed('请求发生问题:' + err.message, flushWaitList(retryConf))) || failPostProceess();
|
||||||
if (err.response.status === 504)
|
if (err.response.status === 504)
|
||||||
return await failed('请求超时,请耐心等待几秒后重试或刷新', flushWaitList) || failData;
|
return (await failed('请求超时,请耐心等待几秒后重试或刷新', flushWaitList(retryConf))) || failPostProceess();
|
||||||
return await failed('服务器出现问题,请稍后重试或刷新,错误代码' + err.response.status, flushWaitList) || failData;
|
return (await failed('服务器出现问题,请稍后重试或刷新,错误代码' + err.response.status, flushWaitList(retryConf))) || failPostProceess();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function flushWaitList() {
|
function flushWaitList(confToRun) {
|
||||||
let fns = waitToSend.map(item => item.fn);
|
return async () => {
|
||||||
|
waitToSend.splice(waitToSend.indexOf(confToRun), 1);
|
||||||
|
let fns = waitToSend.map(
|
||||||
|
conf =>
|
||||||
|
async () => conf.resolver ? conf.resolver(await conf.fetcher()) : await conf.fetcher()
|
||||||
|
);
|
||||||
|
waitToSend.splice(0, waitToSend.length);
|
||||||
|
fns.forEach(fn => fn());
|
||||||
|
return confToRun.resolver ? confToRun.resolver(await confToRun.fetcher()) : await confToRun.fetcher()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function feedWaitList(data) {
|
||||||
|
let fns = waitToSend.map(
|
||||||
|
conf =>
|
||||||
|
() => conf.resolver(data)
|
||||||
|
);
|
||||||
waitToSend.splice(0, waitToSend.length);
|
waitToSend.splice(0, waitToSend.length);
|
||||||
fns.forEach(fn => fn());
|
fns.forEach(fn => fn());
|
||||||
}
|
}
|
@ -1,21 +1,30 @@
|
|||||||
|
.index-container {
|
||||||
|
background-color: #F4F4F4;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
.poster {
|
.poster {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 400px;
|
height: 400px;
|
||||||
background-color: #80010a;
|
background-color: #84000B;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.img-poster {
|
.index-btns {
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.btn-partIn {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 40px;
|
right: 40px;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.index-btns > .user {
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
/* 这里的卡片和分栏结构就比较接近bootstrap了,可惜视觉做的太古板,如果愿意的话改成Material Design就更好了 */
|
|
||||||
.split-lg > .card {
|
.split-lg > .card {
|
||||||
width: 440px;
|
width: 440px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@ -24,7 +33,7 @@
|
|||||||
.card-header {
|
.card-header {
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
border-bottom: 1px solid #e6e6e6;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.card-body {
|
.card-body {
|
||||||
padding: 10px 5px;
|
padding: 10px 5px;
|
||||||
@ -36,42 +45,47 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.img-list {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin: -3px -5px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.img-list > img {
|
|
||||||
width: 200px;
|
|
||||||
margin: 3px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-list {
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
.message-list > li {
|
|
||||||
line-height: 20px;
|
|
||||||
padding: 8px 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
/* list disk */
|
|
||||||
.message-list > li::before {
|
|
||||||
content: "";
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
background-color: #DADADA;
|
|
||||||
margin-right: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.split-lg > .card {
|
.split-lg > .card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.posts {
|
||||||
|
|
||||||
|
}
|
||||||
|
.post {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: white;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 2px 2px 4px #0001;
|
||||||
|
}
|
||||||
|
.post > p {
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
.post-header {
|
||||||
|
padding: 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.post-body {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
.post-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.post-username {
|
||||||
|
}
|
||||||
|
.post-time {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.post-image {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
import ImagePreview from '../components/ImagePreview/ImagePreview';
|
||||||
import { Link } from '../components/SingleRouter/SingleRouter';
|
import { Link } from '../components/SingleRouter/SingleRouter';
|
||||||
import Spinner from '../components/Spinner/Spinner';
|
import Spinner from '../components/Spinner/Spinner';
|
||||||
|
import UserControl from '../components/UserControl/UserControl';
|
||||||
import { alert } from '../helper/alert';
|
import { alert } from '../helper/alert';
|
||||||
import { apis } from '../helper/apis';
|
import { apis } from '../helper/apis';
|
||||||
import { get } from '../helper/axios';
|
import { get } from '../helper/axios';
|
||||||
@ -11,8 +13,7 @@ export class AppContainer extends Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
essentialMessages: [],
|
posts: [],
|
||||||
essentialImages: [],
|
|
||||||
fetchingEssential: false,
|
fetchingEssential: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -24,23 +25,20 @@ export class AppContainer extends Component {
|
|||||||
this.setState({ fetchingEssential: false });
|
this.setState({ fetchingEssential: false });
|
||||||
if (networkStatus !== 200) return;
|
if (networkStatus !== 200) return;
|
||||||
if (!status) return alert('获取精选列表失败:' + data + ',请刷新重试');
|
if (!status) return alert('获取精选列表失败:' + data + ',请刷新重试');
|
||||||
let messages = [], images = [];
|
this.setState({ posts: data });
|
||||||
data.forEach(post => {
|
|
||||||
if (post.content) messages.push(post.content);
|
|
||||||
if (post.image) images.push(post.image);
|
|
||||||
});
|
|
||||||
this.setState({ essentialMessages: messages, essentialImages: images });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="index-container">
|
<div className="index-container">
|
||||||
<div className="poster">
|
<div className="poster" style={{ backgroundImage: `url(${images.poster})` }}>
|
||||||
<img src={images.poster} className="img-poster" alt="海报" />
|
<div className='index-btns'>
|
||||||
|
<UserControl pageAuthLevel={1} buttonOnly />
|
||||||
<Link to="/upload" className="btn btn-light btn-straight btn-partIn">点击参加</Link>
|
<Link to="/upload" className="btn btn-light btn-straight btn-partIn">点击参加</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="content split-lg">
|
</div>
|
||||||
|
<div className="content">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
精选留言
|
精选留言
|
||||||
@ -52,43 +50,33 @@ export class AppContainer extends Component {
|
|||||||
<div className="col-center"><Spinner isGray /></div>
|
<div className="col-center"><Spinner isGray /></div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
this.state.essentialMessages.length === 0
|
this.state.posts.length === 0
|
||||||
? (
|
? (
|
||||||
<div className="col-center">暂时没有精选留言</div>
|
<div className="col-center">暂时没有精选留言</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<ul className="message-list">
|
<div className="posts">
|
||||||
{
|
{
|
||||||
this.state.essentialMessages.map((msg, i) => (
|
this.state.posts.map((post, i) => (
|
||||||
<li dangerouslySetInnerHTML={{ __html: msg }} key={i}></li>
|
<div className="post" key={i}>
|
||||||
))
|
<div className="post-header">
|
||||||
}
|
<img class="post-avatar" src={post.avatar} alt="头像" />
|
||||||
</ul>
|
<div>
|
||||||
)
|
<div class="post-username">{post.nickname}</div>
|
||||||
|
<div class="post-time">于{new Date(post.time * 1000).toLocaleString()}发布</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="post-body">
|
||||||
|
<p dangerouslySetInnerHTML={{ __html: post.content }}></p>
|
||||||
|
{
|
||||||
|
post.image && (
|
||||||
|
<div className="post-image">
|
||||||
|
<ImagePreview image={post.image} />
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card">
|
|
||||||
<div className="card-header">
|
|
||||||
精选图片
|
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
|
||||||
{
|
|
||||||
this.state.fetchingEssential
|
|
||||||
? (
|
|
||||||
<div className="col-center"><Spinner isGray /></div>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
this.state.essentialImages.length === 0
|
|
||||||
? (
|
|
||||||
<div className="col-center">暂时没有精选图片</div>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<div className="img-list">
|
|
||||||
{
|
|
||||||
this.state.essentialImages.map((src, i) => (
|
|
||||||
<img src={src} key={i} alt="精选图片" />
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
.form-group > input:focus {
|
.form-group > input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #FDFDFD;
|
border-color: transparent;
|
||||||
background-color: #FDFDFD;
|
background-color: #FDFDFD;
|
||||||
box-shadow: 1px 3px 6px #0001;
|
box-shadow: 1px 3px 6px #0001;
|
||||||
transform: translate(-2px, -2px);
|
transform: translate(-2px, -2px);
|
||||||
|
@ -8,6 +8,7 @@ import { images } from '../resources.json';
|
|||||||
|
|
||||||
import './upload.css';
|
import './upload.css';
|
||||||
import UserControl from '../components/UserControl/UserControl';
|
import UserControl from '../components/UserControl/UserControl';
|
||||||
|
import { Link } from '../components/SingleRouter/SingleRouter';
|
||||||
|
|
||||||
export class UploadContainer extends Component {
|
export class UploadContainer extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -34,27 +35,32 @@ export class UploadContainer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit() {
|
handleSubmit() {
|
||||||
if ((this.state.msg === "" && this.file === null) || this.state.submitting) return;
|
if (this.file === null || this.state.submitting) return;
|
||||||
this.setState({ submitting: true });
|
this.setState({ submitting: true });
|
||||||
|
localStorage.removeItem('msgTemp');
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
if (this.state.submitting && (!this.state.file || this.state.url !== "")) {
|
if (this.state.submitting && (!this.state.file || this.state.url !== "")) {
|
||||||
post(apis.submitMessage, { content: this.state.msg, image: this.state.url })
|
post(apis.submitMessage, { content: this.state.msg, image: this.state.url })
|
||||||
.then(({ data, status, networkStatus }) => {
|
.then(({ data, status, networkStatus }) => {
|
||||||
|
this.setState({ submitting: false });
|
||||||
if (networkStatus !== 200) return;
|
if (networkStatus !== 200) return;
|
||||||
if (!status) {
|
if (!status) {
|
||||||
this.setState({ submitting: false });
|
|
||||||
return alert('提交内容失败:' + data);
|
return alert('提交内容失败:' + data);
|
||||||
}
|
}
|
||||||
this.setState({ submitting: false, msg: "", url: "", file: null });
|
this.setState({ msg: "", url: "", file: null });
|
||||||
alert('内容提交成功啦').then(({ isConfirmed }) => {
|
alert('内容提交成功啦');
|
||||||
if (isConfirmed) window.close();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (localStorage.getItem('msgTemp')) {
|
||||||
|
this.setState({ msg: localStorage.getItem('msgTemp') });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="upload-container">
|
<div className="upload-container">
|
||||||
@ -62,8 +68,13 @@ export class UploadContainer extends Component {
|
|||||||
<div className="sdu">
|
<div className="sdu">
|
||||||
<img src={images.icon} className="sdu-logo" alt="logo" />
|
<img src={images.icon} className="sdu-logo" alt="logo" />
|
||||||
<img src={images.name} className="sdu-name" alt="" />
|
<img src={images.name} className="sdu-name" alt="" />
|
||||||
|
<div className="nav">
|
||||||
|
<div className="nav-item">
|
||||||
|
<Link to="/" className="nav-link">首页</Link>
|
||||||
</div>
|
</div>
|
||||||
<UserControl />
|
</div>
|
||||||
|
</div>
|
||||||
|
<UserControl pageAuthLevel={1} />
|
||||||
</div>
|
</div>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<div className="message-box">
|
<div className="message-box">
|
||||||
@ -71,7 +82,10 @@ export class UploadContainer extends Component {
|
|||||||
className="message-box-textarea"
|
className="message-box-textarea"
|
||||||
placeholder="你的留言…"
|
placeholder="你的留言…"
|
||||||
value={this.state.msg}
|
value={this.state.msg}
|
||||||
onChange={e => this.setState({ msg: e.target.value })}
|
onChange={e => {
|
||||||
|
this.setState({ msg: e.target.value });
|
||||||
|
localStorage.setItem('msgTemp', e.target.value);
|
||||||
|
}}
|
||||||
disabled={this.state.submitting}
|
disabled={this.state.submitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -99,7 +113,7 @@ export class UploadContainer extends Component {
|
|||||||
<button
|
<button
|
||||||
className="btn btn-sdu btn-straight btn-upload"
|
className="btn btn-sdu btn-straight btn-upload"
|
||||||
onClick={this.handleSubmit}
|
onClick={this.handleSubmit}
|
||||||
disabled={(this.state.msg === "" && this.state.file === null) || this.state.submitting}
|
disabled={this.state.file === null || this.state.submitting}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
this.state.submitting
|
this.state.submitting
|
||||||
|
載入中…
x
新增問題並參考
Block a user