add post form

This commit is contained in:
xmcp
2018-08-25 23:50:38 +08:00
parent 75a35f5af6
commit 94251b394b
13 changed files with 293 additions and 63 deletions

View File

@@ -131,8 +131,13 @@ class FlowItemRow extends PureComponent {
})
.then((res)=>res.json())
.then((json)=>{
if(json.code!==0 && (!json.msg || json.msg!=='已经关注过辣'))
throw new Error(json);
if(json.code!==0) {
if(json.msg && json.msg==='已经关注过辣') {}
else {
if(json.msg) alert(json.msg);
throw new Error(json);
}
}
this.setState({
attention: next_attention,
@@ -164,8 +169,8 @@ class FlowItemRow extends PureComponent {
this.toggle_attention(this.show_sidebar.bind(this));
}}>
{this.state.attention ?
<span><span className="icon icon-star-ok" />已关注</span> :
<span><span className="icon icon-star" />未关注</span>
<span><span className="icon icon-star-ok" />&nbsp;已关注</span> :
<span><span className="icon icon-star" />&nbsp;未关注</span>
}
</a>
</span>
@@ -382,7 +387,11 @@ export class Flow extends PureComponent {
<a onClick={()=>{this.load_page(this.state.loaded_pages+1)}}>重新加载</a>
</div>
}
<TitleLine text={this.state.loading_status==='loading' ? 'Loading...' : '© xmcp'} />
<TitleLine text={
this.state.loading_status==='loading' ?
<span><span className="icon icon-loading" />&nbsp;Loading...</span> :
'© xmcp'
} />
</div>
);
}

View File

@@ -10,7 +10,6 @@ export function Sidebar(props) {
<a onClick={props.do_close}>×</a>
&nbsp;{props.title}
</p>
<br />
{props.content}
</div>
</div>

View File

@@ -1,5 +1,5 @@
import React, {Component, PureComponent} from 'react';
import {LoginForm} from './UserAction';
import {LoginForm, PostForm} from './UserAction';
import {TokenCtx} from './UserAction';
import './Title.css';
@@ -112,15 +112,30 @@ class ControlBar extends PureComponent {
<input value={this.state.search_text} placeholder="搜索 或 #PID"
onChange={this.on_change_bound} onKeyPress={this.on_keypress_bound}
/>
<a className="control-btn" onClick={()=>{this.props.show_sidebar('登录',<LoginForm />)}}>
<span className={'icon icon-'+(token ? 'login-ok' : 'login')} />
</a>
<a className="control-btn" onClick={()=>{this.props.show_sidebar(
'关于 P大树洞非官方 网页版',
HELP_TEXT
)}}>
<span className="icon icon-help" />
<a className="control-btn" onClick={()=>{
this.props.show_sidebar(
'P大树洞非官方网页版',
<div>
<LoginForm />
{HELP_TEXT}
</div>
)
}}>
<span className={'icon icon-'+(token ? 'about' : 'login')} />
</a>
{!!token &&
<a className="control-btn" onClick={()=>{
this.props.show_sidebar(
'发表树洞',
<PostForm token={token} on_complete={()=>{
this.props.show_sidebar('',null);
this.do_refresh();
}} />
)
}}>
<span className="icon icon-plus" />
</a>
}
</div>
)}</TokenCtx.Consumer>
)
@@ -131,7 +146,13 @@ export function Title(props) {
return (
<div className="title-bar">
<div className="aux-margin">
<p className="title centered-line">P大树洞</p>
<p className="title centered-line">
P大树洞
&nbsp;
<a href="https://github.com/xmcp/ashole" target="_blank">
<span className="icon icon-github" />
</a>
</p>
<ControlBar show_sidebar={props.show_sidebar} set_mode={props.set_mode} />
</div>
</div>

View File

@@ -2,11 +2,9 @@
margin: 1em 0;
text-align: center;
}
.login-form button {
min-width: 100px;
}
.reply-form {
display: flex;
}
@@ -17,5 +15,27 @@
height: 5em;
}
.reply-form button {
flex: 0 0 50px;
flex: 0 0 3em;
}
.post-form-bar {
line-height: 2em;
display: flex;
}
.post-form-bar label {
flex: 1;
}
@media screen and (max-width: 600px) {
.post-form-bar input[type=file] {
width: 150px;
}
}
.post-form-bar button {
flex: 0 0 8em;
}
.post-form textarea {
resize: vertical;
width: 100%;
min-height: 5em;
height: 20em;
}

View File

@@ -5,6 +5,8 @@ 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=1000;
const MAX_IMG_FILESIZE=100000;
export const TokenCtx=React.createContext({
value: null,
@@ -42,8 +44,10 @@ export class LoginForm extends Component {
})
.then((res)=>res.json())
.then((json)=>{
if(json.code!==0)
if(json.code!==0) {
if(json.msg) alert(json.msg);
throw new Error(json);
}
set_token(json.token);
alert(`成功以 ${json.name} 的身份登录`);
@@ -63,8 +67,8 @@ export class LoginForm extends Component {
render() {
return (
<TokenCtx.Consumer>{(token)=>
<div className="login-form">
<form onSubmit={(e)=>this.do_login(e,token.set_value)} className="box">
<div className="login-form box">
<form onSubmit={(e)=>this.do_login(e,token.set_value)}>
<p>{token.value ?
<span><b>您已登录</b>Token: <code>{token.value||'(null)'}</code></span> :
'登录后可以使用关注、回复等功能'
@@ -83,19 +87,23 @@ export class LoginForm extends Component {
</p>
<p>
{this.state.loading_status==='loading' ?
<button disabled="disbled">正在登录</button> :
<button type="submit">登录</button>
<button disabled="disbled">
<span className="icon icon-loading" />
&nbsp;正在登录
</button> :
<button type="submit">
<span className="icon icon-login" />
&nbsp;登录
</button>
}
<button type="button" onClick={()=>{token.set_value(null);}}>退出</button>
</p>
</form>
<div className="box">
<ul>
<li>我们不会记录您的密码和个人信息</li>
<li><b>请勿泄露 Token</b></li>
<li>如果您不愿输入密码可以直接修改 <code>localStorage['TOKEN']</code></li>
</ul>
</div>
</form>
</div>
}</TokenCtx.Consumer>
)
@@ -121,7 +129,6 @@ export class ReplyForm extends Component {
on_submit(event) {
event.preventDefault();
if(this.state.loading_status==='loading')
return;
this.setState({
@@ -142,8 +149,10 @@ export class ReplyForm extends Component {
})
.then((res)=>res.json())
.then((json)=>{
if(json.code!==0)
if(json.code!==0) {
if(json.msg) alert(json.msg);
throw new Error(json);
}
this.setState({
loading_status: 'done',
@@ -163,16 +172,175 @@ export class ReplyForm extends Component {
render() {
return (
<div className="box">
<form onSubmit={this.on_submit.bind(this)} className="reply-form">
<SafeTextarea ref={this.area_ref} id={this.props.pid} on_change={this.on_change_bound} />
{this.state.loading_status==='loading' ?
<button disabled="disabled">正在回复</button> :
<button type="submit">回复</button>
<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.trace(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;
}
</form>
</div>
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>
<br />
<SafeTextarea ref={this.area_ref} id="new_post" on_change={this.on_change_bound} />
</form>
)
}
}