THU Hole react frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

371 lines
12 KiB

import React, {Component, PureComponent} from 'react';
import {SafeTextarea} from './Common';
import md5 from 'md5';
import './UserAction.css';
import {API_BASE} from './Common';
const LOGIN_BASE=window.location.protocol==='https:' ? '/login_proxy' : 'http://www.pkuhelper.com/services/login';
const MAX_IMG_PX=2000;
const MAX_IMG_FILESIZE=256000;
const ISOP_APPKEY='0feb3a8a831e11e8933a0050568508a5';
const ISOP_APPCODE='0fec960a831e11e8933a0050568508a5';
const ISOP_SVCID='PERSON_BASE_INFO,STUDENT_SCORE,STUDENT_COURSE_TABLE,STUDENT_COURSE_TABLE_ROOM,CARD_BALANCE';
export const TokenCtx=React.createContext({
value: null,
set_value: ()=>{},
});
export class LoginForm extends Component {
constructor(props) {
super(props);
this.state={
loading_status: 'done',
};
this.username_ref=React.createRef();
this.password_ref=React.createRef();
}
do_sendcode() {
if(this.state.loading_status==='loading')
return;
let param=
'user='+this.username_ref.current.value+
'&svcId='+ISOP_SVCID+
'&appKey='+ISOP_APPKEY+
'&timestamp='+(+new Date());
fetch(
'https://isop.pku.edu.cn/svcpub/svc/oauth/validcode?'+param+
'&msg='+md5(param+ISOP_APPCODE),
{mode: 'no-cors'}
);
alert('短信验证码应该会发到您的手机上,请注意查收!');
}
do_login(set_token) {
if(this.state.loading_status==='loading')
return;
this.setState({
loading_status: 'loading',
});
let data=new URLSearchParams();
data.append('username', this.username_ref.current.value);
data.append('valid_code', this.password_ref.current.value);
data.append('isnewloginflow', 'true');
fetch(LOGIN_BASE+'/login.php?platform=hole_xmcp_ml', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data,
})
.then((res)=>res.json())
.then((json)=>{
if(json.code!==0) {
if(json.msg) alert(json.msg);
throw new Error(json);
}
set_token(json.token);
alert(`成功以 ${json.name} 的身份登录`);
this.setState({
loading_status: 'done',
});
})
.catch((e)=>{
alert('登录失败');
this.setState({
loading_status: 'done',
});
console.error(e);
});
}
render() {
return (
<TokenCtx.Consumer>{(token)=>
<div className="login-form box">
{token.value ?
<div>
<p>
<b>您已登录</b>
<button type="button" onClick={()=>{token.set_value(null);}}>注销</button>
</p>
<p>
Token: <code>{token.value||'(null)'}</code> <br />
请勿泄露 Token它代表您的登录状态与您的账户唯一对应且泄露后无法重置
</p>
</div> :
<div>
<p>登录后可以使用关注回复等功能</p>
<p>
<label>
 学号&nbsp;
<input ref={this.username_ref} type="tel" />
</label>
<button type="button" disabled={this.state.loading_status==='loading'}
onClick={(e)=>this.do_sendcode()}>
发送验证码
</button>
</p>
<p>
<label>
验证码&nbsp;
<input ref={this.password_ref} type="tel" />
</label>
<button type="button" disabled={this.state.loading_status==='loading'}
onClick={(e)=>this.do_login(token.set_value)}>
登录
</button>
</p>
<p>
登录请求会被发送到北大统一验证接口和 PKU Helper 服务器 <br />
我们不会记录或使用您的登录信息
</p>
</div>
}
</div>
}</TokenCtx.Consumer>
)
}
}
export class ReplyForm extends Component {
constructor(props) {
super(props);
this.state={
text: '',
loading_status: 'done',
};
this.on_change_bound=this.on_change.bind(this);
this.area_ref=React.createRef();
}
on_change(value) {
this.setState({
text: value,
});
}
on_submit(event) {
event.preventDefault();
if(this.state.loading_status==='loading')
return;
this.setState({
loading_status: 'loading',
});
let data=new URLSearchParams();
data.append('action','docomment');
data.append('pid',this.props.pid);
data.append('text',this.state.text);
data.append('token',this.props.token);
fetch(API_BASE+'/api.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data,
})
.then((res)=>res.json())
.then((json)=>{
if(json.code!==0) {
if(json.msg) alert(json.msg);
throw new Error(json);
}
this.setState({
loading_status: 'done',
text: '',
});
this.area_ref.current.clear();
this.props.on_complete();
})
.catch((e)=>{
console.error(e);
alert('回复失败\n(树洞服务器经常抽风,其实有可能已经回复上了,不妨点“刷新回复”看一看)');
this.setState({
loading_status: 'done',
});
});
}
render() {
return (
<form onSubmit={this.on_submit.bind(this)} className="reply-form box">
<SafeTextarea ref={this.area_ref} id={this.props.pid} on_change={this.on_change_bound} />
{this.state.loading_status==='loading' ?
<button disabled="disabled">
<span className="icon icon-loading" />
</button> :
<button type="submit">
<span className="icon icon-send" />
</button>
}
</form>
)
}
}
export class PostForm extends Component {
constructor(props) {
super(props);
this.state={
text: '',
loading_status: 'done',
};
this.img_ref=React.createRef();
this.area_ref=React.createRef();
this.on_change_bound=this.on_change.bind(this);
}
on_change(value) {
this.setState({
text: value,
});
}
do_post(text,img) {
let data=new URLSearchParams();
data.append('action','dopost');
data.append('text',this.state.text);
data.append('type',img ? 'image' : 'text');
data.append('token',this.props.token);
if(img)
data.append('data',img);
fetch(API_BASE+'/api.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data,
})
.then((res)=>res.json())
.then((json)=>{
if(json.code!==0) {
if(json.msg) alert(json.msg);
throw new Error(json);
}
this.setState({
loading_status: 'done',
text: '',
});
this.area_ref.current.clear();
this.props.on_complete();
})
.catch((e)=>{
console.error(e);
alert('发表失败');
this.setState({
loading_status: 'done',
});
});
}
proc_img(file) { // http://pkuhole.chenpong.com/
return new Promise((resolve,reject)=>{
function return_url(url) {
const idx=url.indexOf(';base64,');
if(idx===-1)
throw new Error('img not base64 encoded');
resolve(url.substr(idx+8));
}
let reader=new FileReader();
reader.onload=((event)=>{ // check size
const url=event.target.result;
const image = new Image();
image.src=url;
image.onload=(()=>{
let width=image.width;
let height=image.height;
if(width>MAX_IMG_PX) {
height=height*MAX_IMG_PX/width;
width=MAX_IMG_PX;
}
if(height>MAX_IMG_PX) {
width=width*MAX_IMG_PX/height;
height=MAX_IMG_PX;
}
let canvas=document.createElement('canvas');
let ctx=canvas.getContext('2d');
canvas.width=width;
canvas.height=height;
ctx.drawImage(image,0,0,width,height);
for(let quality=.9;quality>0;quality-=0.1) {
const url=canvas.toDataURL('image/jpeg',quality);
console.log('quality',quality,'size',url.length);
if(url.length<=MAX_IMG_FILESIZE) {
console.log('chosen img quality',quality);
return return_url(url);
}
}
// else
alert('图片过大,无法上传');
reject('img too large');
});
});
reader.readAsDataURL(file);
});
}
on_submit(event) {
event.preventDefault();
if(this.state.loading_status==='loading')
return;
if(this.img_ref.current.files.length) {
this.setState({
loading_status: 'processing',
});
this.proc_img(this.img_ref.current.files[0])
.then((img)=>{
this.setState({
loading_status: 'loading',
});
this.do_post(this.state.text,img);
})
} else {
this.setState({
loading_status: 'loading',
});
this.do_post(this.state.text,null);
}
}
render() {
return (
<form onSubmit={this.on_submit.bind(this)} className="post-form box">
<div className="post-form-bar">
<label>
图片
<input ref={this.img_ref} type="file" accept="image/*"
{...this.state.loading_status!=='done' ? {disabled: true} : {}} />
</label>
{this.state.loading_status!=='done' ?
<button disabled="disabled">
<span className="icon icon-loading" />
&nbsp;正在{this.state.loading_status==='processing' ? '处理' : '上传'}
</button> :
<button type="submit">
<span className="icon icon-send" />
&nbsp;发表
</button>
}
</div>
<SafeTextarea ref={this.area_ref} id="new_post" on_change={this.on_change_bound} />
</form>
)
}
}