forked from newthuhole/hole_thu_frontend
add login and attention
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
/api_proxy/* http://www.pkuhelper.com:10301/services/pkuhole/:splat 200
|
||||
/audio_proxy/* http://www.pkuhelper.com:10301/services/pkuhole/audios/:splat 200
|
||||
/api_proxy/* http://www.pkuhelper.com/services/pkuhole/:splat 200
|
||||
/audio_proxy/* http://www.pkuhelper.com/services/pkuhole/audios/:splat 200
|
||||
/login_proxy/* http://www.pkuhelper.com/services/login/:splat 200
|
||||
@@ -6,6 +6,8 @@
|
||||
<link rel="icon" href="%PUBLIC_URL%/static/favicon/256.png">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
|
||||
<link rel="stylesheet" href="%PUBLIC_URL%/static/fonts_1/icomoon.css" />
|
||||
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/static/favicon/256.png">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/static/manifest.json">
|
||||
|
||||
49
public/static/fonts_1/icomoon.css
Normal file
49
public/static/fonts_1/icomoon.css
Normal file
@@ -0,0 +1,49 @@
|
||||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src:
|
||||
url('icomoon.ttf?4yzqd4') format('truetype'),
|
||||
url('icomoon.woff?4yzqd4') format('woff'),
|
||||
url('icomoon.svg?4yzqd4#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.icon {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
/*noinspection CssNoGenericFontName*/
|
||||
font-family: 'icomoon' !important;
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-reply:before {
|
||||
content: "\e96b";
|
||||
}
|
||||
.icon-login-ok:before {
|
||||
content: "\e975";
|
||||
}
|
||||
.icon-login:before {
|
||||
content: "\e98d";
|
||||
}
|
||||
.icon-attention:before {
|
||||
content: "\e9d3";
|
||||
}
|
||||
.icon-star:before {
|
||||
content: "\e9d7";
|
||||
}
|
||||
.icon-star-ok:before {
|
||||
content: "\e9d9";
|
||||
}
|
||||
.icon-help:before {
|
||||
content: "\ea09";
|
||||
}
|
||||
.icon-refresh:before {
|
||||
content: "\ea2e";
|
||||
}
|
||||
18
public/static/fonts_1/icomoon.svg
Normal file
18
public/static/fonts_1/icomoon.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>Generated by IcoMoon</metadata>
|
||||
<defs>
|
||||
<font id="icomoon" horiz-adv-x="1024">
|
||||
<font-face units-per-em="1024" ascent="960" descent="-64" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||
<glyph unicode="" glyph-name="reply" d="M512 896c282.77 0 512-186.25 512-416 0-229.752-229.23-416-512-416-27.156 0-53.81 1.734-79.824 5.044-109.978-109.978-241.25-129.7-368.176-132.596v26.916c68.536 33.578 128 94.74 128 164.636 0 9.754-0.758 19.33-2.164 28.696-115.796 76.264-189.836 192.754-189.836 323.304 0 229.75 229.23 416 512 416z" />
|
||||
<glyph unicode="" glyph-name="login-ok" d="M960 352l-288-288-96 96-64-64 160-160 352 352zM448 192h320v115.128c-67.22 39.2-156.308 66.11-256 74.26v52.78c70.498 39.728 128 138.772 128 237.832 0 159.058 0 288-192 288s-192-128.942-192-288c0-99.060 57.502-198.104 128-237.832v-52.78c-217.102-17.748-384-124.42-384-253.388h448v64z" />
|
||||
<glyph unicode="" glyph-name="login" d="M704 960c-176.73 0-320-143.268-320-320 0-20.026 1.858-39.616 5.376-58.624l-389.376-389.376v-192c0-35.346 28.654-64 64-64h64v64h128v128h128v128h128l83.042 83.042c34.010-12.316 70.696-19.042 108.958-19.042 176.73 0 320 143.268 320 320s-143.27 320-320 320zM799.874 639.874c-53.020 0-96 42.98-96 96s42.98 96 96 96 96-42.98 96-96-42.98-96-96-96z" />
|
||||
<glyph unicode="" glyph-name="attention" d="M256 832v-896l320 320 320-320v896zM768 960h-640v-896l64 64v768h576z" />
|
||||
<glyph unicode="" glyph-name="star" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538zM512 206.502l-223.462-117.48 42.676 248.83-180.786 176.222 249.84 36.304 111.732 226.396 111.736-226.396 249.836-36.304-180.788-176.222 42.678-248.83-223.462 117.48z" />
|
||||
<glyph unicode="" glyph-name="star-ok" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538z" />
|
||||
<glyph unicode="" glyph-name="help" d="M448 256h128v-128h-128zM704 704c35.346 0 64-28.654 64-64v-192l-192-128h-128v64l192 128v64h-320v128h384zM512 864c-111.118 0-215.584-43.272-294.156-121.844s-121.844-183.038-121.844-294.156c0-111.118 43.272-215.584 121.844-294.156s183.038-121.844 294.156-121.844c111.118 0 215.584 43.272 294.156 121.844s121.844 183.038 121.844 294.156c0 111.118-43.272 215.584-121.844 294.156s-183.038 121.844-294.156 121.844zM512 960v0c282.77 0 512-229.23 512-512s-229.23-512-512-512c-282.77 0-512 229.23-512 512s229.23 512 512 512z" />
|
||||
<glyph unicode="" glyph-name="refresh" d="M889.68 793.68c-93.608 102.216-228.154 166.32-377.68 166.32-282.77 0-512-229.23-512-512h96c0 229.75 186.25 416 416 416 123.020 0 233.542-53.418 309.696-138.306l-149.696-149.694h352v352l-134.32-134.32zM928 448c0-229.75-186.25-416-416-416-123.020 0-233.542 53.418-309.694 138.306l149.694 149.694h-352v-352l134.32 134.32c93.608-102.216 228.154-166.32 377.68-166.32 282.77 0 512 229.23 512 512h-96z" />
|
||||
</font></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/static/fonts_1/icomoon.ttf
Normal file
BIN
public/static/fonts_1/icomoon.ttf
Normal file
Binary file not shown.
BIN
public/static/fonts_1/icomoon.woff
Normal file
BIN
public/static/fonts_1/icomoon.woff
Normal file
Binary file not shown.
46
src/App.js
46
src/App.js
@@ -2,6 +2,7 @@ import React, {Component} from 'react';
|
||||
import {Flow} from './Flows';
|
||||
import {Title} from './Title';
|
||||
import {Sidebar} from './Sidebar';
|
||||
import {TokenCtx} from './UserAction';
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
@@ -9,9 +10,10 @@ class App extends Component {
|
||||
this.state={
|
||||
sidebar_title: null,
|
||||
sidebar_content: null,
|
||||
mode: 'list', // list, single, search
|
||||
mode: 'list', // list, single, search, attention
|
||||
search_text: null,
|
||||
flow_render_key: +new Date(),
|
||||
token: localStorage['TOKEN']||null,
|
||||
};
|
||||
this.show_sidebar_bound=this.show_sidebar.bind(this);
|
||||
this.set_mode_bound=this.set_mode.bind(this);
|
||||
@@ -34,23 +36,35 @@ class App extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-img" style={{
|
||||
backgroundImage: 'url('+(localStorage['REPLACE_ERIRI_WITH_URL'] || 'static/eriri_bg.jpg')+')'
|
||||
}} />
|
||||
<Title show_sidebar={this.show_sidebar_bound} set_mode={this.set_mode_bound} />
|
||||
<div className="left-container">
|
||||
<Flow key={this.state.flow_render_key} show_sidebar={this.show_sidebar_bound}
|
||||
mode={this.state.mode} search_text={this.state.search_text}
|
||||
/>
|
||||
<br />
|
||||
</div>
|
||||
<Sidebar do_close={()=>{
|
||||
<TokenCtx.Provider value={{
|
||||
value: this.state.token,
|
||||
set_value: (x)=>{
|
||||
localStorage['TOKEN']=x||'';
|
||||
this.setState({
|
||||
sidebar_content: null,
|
||||
token: x,
|
||||
});
|
||||
}} content={this.state.sidebar_content} title={this.state.sidebar_title} />
|
||||
</div>
|
||||
},
|
||||
}}>
|
||||
<div>
|
||||
<div className="bg-img" style={{
|
||||
backgroundImage: 'url('+(localStorage['REPLACE_ERIRI_WITH_URL'] || 'static/eriri_bg.jpg')+')'
|
||||
}} />
|
||||
<Title show_sidebar={this.show_sidebar_bound} set_mode={this.set_mode_bound} />
|
||||
<div className="left-container">
|
||||
<TokenCtx.Consumer>{(token)=>(
|
||||
<Flow key={this.state.flow_render_key} show_sidebar={this.show_sidebar_bound}
|
||||
mode={this.state.mode} search_text={this.state.search_text} token={token.value}
|
||||
/>
|
||||
)}</TokenCtx.Consumer>
|
||||
<br />
|
||||
</div>
|
||||
<Sidebar do_close={()=>{
|
||||
this.setState({
|
||||
sidebar_content: null,
|
||||
});
|
||||
}} content={this.state.sidebar_content} title={this.state.sidebar_title} />
|
||||
</div>
|
||||
</TokenCtx.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
}
|
||||
|
||||
.centered-line::before {
|
||||
right: 0.5em;
|
||||
right: 1em;
|
||||
margin-left: -50%;
|
||||
}
|
||||
|
||||
.centered-line::after {
|
||||
left: 0.5em;
|
||||
left: 1em;
|
||||
margin-right: -50%;
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,6 @@ p.img img {
|
||||
}
|
||||
|
||||
.box-id {
|
||||
font-family: Consolas, Courier, monospace;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
|
||||
127
src/Flows.js
127
src/Flows.js
@@ -4,10 +4,11 @@ import {Time, TitleLine, HighlightedText} from './Common.js';
|
||||
import './Flows.css';
|
||||
import LazyLoad from 'react-lazyload';
|
||||
import {AudioWidget} from './AudioWidget.js';
|
||||
import {TokenCtx} from './UserAction';
|
||||
|
||||
const IMAGE_BASE='http://www.pkuhelper.com/services/pkuhole/images/';
|
||||
const AUDIO_BASE='/audio_proxy/';
|
||||
const API_BASE=window.location.protocol==='https:' ? '/api_proxy' : 'http://www.pkuhelper.com:10301/services/pkuhole';
|
||||
const API_BASE=window.location.protocol==='https:' ? '/api_proxy' : 'http://www.pkuhelper.com/services/pkuhole';
|
||||
|
||||
const SEARCH_PAGESIZE=50;
|
||||
const CLICKABLE_TAGS={a: true, audio: true};
|
||||
@@ -21,7 +22,7 @@ function Reply(props) {
|
||||
backgroundColor: props.info._display_color,
|
||||
} : null}>
|
||||
<div className="box-header">
|
||||
<span className="box-id">#{props.info.cid}</span>
|
||||
<code className="box-id">#{props.info.cid}</code>
|
||||
<Time stamp={props.info.timestamp} />
|
||||
</div>
|
||||
<HighlightedText text={props.info.text} color_picker={props.color_picker} />
|
||||
@@ -34,9 +35,19 @@ function FlowItem(props) {
|
||||
<div className="flow-item box">
|
||||
{parseInt(props.info.pid,10)>window.LATEST_POST_ID && <div className="flow-item-dot" /> }
|
||||
<div className="box-header">
|
||||
{!!parseInt(props.info.likenum,10) && <span className="box-header-badge">{props.info.likenum}★</span>}
|
||||
{!!parseInt(props.info.reply,10) && <span className="box-header-badge">{props.info.reply}回复</span>}
|
||||
<span className="box-id">#{props.info.pid}</span>
|
||||
{!!parseInt(props.info.likenum,10) &&
|
||||
<span className="box-header-badge">
|
||||
{props.info.likenum}
|
||||
<span className={'icon icon-'+(props.attention ? 'star-ok' : 'star')} />
|
||||
</span>
|
||||
}
|
||||
{!!parseInt(props.info.reply,10) &&
|
||||
<span className="box-header-badge">
|
||||
{props.info.reply}
|
||||
<span className="icon icon-reply" />
|
||||
</span>
|
||||
}
|
||||
<code className="box-id">#{props.info.pid}</code>
|
||||
<Time stamp={props.info.timestamp} />
|
||||
</div>
|
||||
<HighlightedText text={props.info.text} color_picker={props.color_picker} />
|
||||
@@ -53,6 +64,7 @@ class FlowItemRow extends PureComponent {
|
||||
replies: [],
|
||||
reply_status: 'done',
|
||||
info: props.info,
|
||||
attention: false,
|
||||
};
|
||||
this.color_picker=new ColorPicker();
|
||||
}
|
||||
@@ -68,11 +80,16 @@ class FlowItemRow extends PureComponent {
|
||||
this.setState({
|
||||
reply_status: 'loading',
|
||||
});
|
||||
fetch(API_BASE+'/api.php?action=getcomment&pid='+this.state.info.pid)
|
||||
const token_param=this.props.token ? '&token='+this.props.token : '';
|
||||
fetch(
|
||||
API_BASE+'/api.php?action=getcomment'+
|
||||
'&pid='+this.state.info.pid+
|
||||
token_param
|
||||
)
|
||||
.then((res)=>res.json())
|
||||
.then((json)=>{
|
||||
if(json.code!==0)
|
||||
throw new Error(json.code);
|
||||
throw new Error(json);
|
||||
const replies=json.data
|
||||
.sort((a,b)=>{
|
||||
return parseInt(a.timestamp,10)-parseInt(b.timestamp,10);
|
||||
@@ -86,6 +103,7 @@ class FlowItemRow extends PureComponent {
|
||||
info: Object.assign({}, prev.info, {
|
||||
reply: ''+replies.length,
|
||||
}),
|
||||
attention: !!json.attention,
|
||||
reply_status: 'done',
|
||||
}),callback);
|
||||
})
|
||||
@@ -106,9 +124,9 @@ class FlowItemRow extends PureComponent {
|
||||
<a onClick={()=>{
|
||||
this.props.show_sidebar('帖子详情',<p className="box box-tip">加载中……</p>);
|
||||
this.load_replies(this.show_sidebar);
|
||||
}}>更新回复</a>
|
||||
}}>刷新回复</a>
|
||||
</div>
|
||||
<FlowItem info={this.state.info} color_picker={this.color_picker} />
|
||||
<FlowItem info={this.state.info} color_picker={this.color_picker} attention={this.state.attention} />
|
||||
{this.state.replies.map((reply)=>(
|
||||
<LazyLoad offset={500} height="5em" overflow={true} once={true}>
|
||||
<Reply key={reply.cid} info={reply} color_picker={this.color_picker} />
|
||||
@@ -125,7 +143,7 @@ class FlowItemRow extends PureComponent {
|
||||
if(!CLICKABLE_TAGS[event.target.tagName.toLowerCase()])
|
||||
this.show_sidebar();
|
||||
}}>
|
||||
<FlowItem info={this.state.info} color_picker={this.color_picker} />
|
||||
<FlowItem info={this.state.info} color_picker={this.color_picker} attention={this.state.attention} />
|
||||
<div className="flow-reply-row">
|
||||
{this.state.reply_status==='loading' && <div className="box box-tip">加载中</div>}
|
||||
{this.state.reply_status==='failed' &&
|
||||
@@ -145,14 +163,16 @@ class FlowItemRow extends PureComponent {
|
||||
|
||||
function FlowChunk(props) {
|
||||
return (
|
||||
<div className="flow-chunk">
|
||||
<TitleLine text={props.title} />
|
||||
{props.list.map((info)=>(
|
||||
<LazyLoad key={info.pid} offset={500} height="15em" once={true} >
|
||||
<FlowItemRow info={info} show_sidebar={props.show_sidebar} />
|
||||
</LazyLoad>
|
||||
))}
|
||||
</div>
|
||||
<TokenCtx.Consumer>{({value: token})=>(
|
||||
<div className="flow-chunk">
|
||||
<TitleLine text={props.title} />
|
||||
{props.list.map((info)=>(
|
||||
<LazyLoad key={info.pid} offset={500} height="15em" once={true} >
|
||||
<FlowItemRow info={info} show_sidebar={props.show_sidebar} token={token} />
|
||||
</LazyLoad>
|
||||
))}
|
||||
</div>
|
||||
)}</TokenCtx.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -171,16 +191,30 @@ export class Flow extends PureComponent {
|
||||
}
|
||||
|
||||
load_page(page) {
|
||||
const failed=(err)=>{
|
||||
console.trace(err);
|
||||
this.setState((prev,props)=>({
|
||||
loaded_pages: prev.loaded_pages-1,
|
||||
loading_status: 'failed',
|
||||
}));
|
||||
};
|
||||
|
||||
const token_param=this.props.token ? '&token='+this.props.token : '';
|
||||
|
||||
if(page>this.state.loaded_pages+1)
|
||||
throw new Error('bad page');
|
||||
if(page===this.state.loaded_pages+1) {
|
||||
console.log('fetching page',page);
|
||||
if(this.state.mode==='list') {
|
||||
fetch(API_BASE+'/api.php?action=getlist&p='+page)
|
||||
fetch(
|
||||
API_BASE+'/api.php?action=getlist'+
|
||||
'&p='+page+
|
||||
token_param
|
||||
)
|
||||
.then((res)=>res.json())
|
||||
.then((json)=>{
|
||||
if(json.code!==0)
|
||||
throw new Error(json.code);
|
||||
throw new Error(json);
|
||||
json.data.forEach((x)=>{
|
||||
if(parseInt(x.pid,10)>(parseInt(localStorage['_LATEST_POST_ID'],10)||0))
|
||||
localStorage['_LATEST_POST_ID']=x.pid;
|
||||
@@ -196,23 +230,18 @@ export class Flow extends PureComponent {
|
||||
loading_status: 'done',
|
||||
}));
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.trace(err);
|
||||
this.setState((prev,props)=>({
|
||||
loaded_pages: prev.loaded_pages-1,
|
||||
loading_status: 'failed',
|
||||
}));
|
||||
});
|
||||
.catch(failed);
|
||||
} else if(this.state.mode==='search') {
|
||||
fetch(
|
||||
API_BASE+'/api.php?action=search'+
|
||||
'&pagesize='+SEARCH_PAGESIZE*page+
|
||||
'&keywords='+encodeURIComponent(this.state.search_param)
|
||||
'&keywords='+encodeURIComponent(this.state.search_param)+
|
||||
token_param
|
||||
)
|
||||
.then((res)=>res.json())
|
||||
.then((json)=>{
|
||||
if(json.code!==0)
|
||||
throw new Error(json.code);
|
||||
throw new Error(json);
|
||||
const finished=json.data.length<SEARCH_PAGESIZE;
|
||||
this.setState({
|
||||
chunks: [{
|
||||
@@ -223,23 +252,18 @@ export class Flow extends PureComponent {
|
||||
loading_status: 'done',
|
||||
});
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.trace(err);
|
||||
this.setState((prev,props)=>({
|
||||
loaded_pages: prev.loaded_pages-1,
|
||||
loading_status: 'failed',
|
||||
}));
|
||||
});
|
||||
.catch(failed);
|
||||
} else if(this.state.mode==='single') {
|
||||
const pid=parseInt(this.state.search_param.substr(1),10);
|
||||
fetch(
|
||||
API_BASE+'/api.php?action=getone'+
|
||||
'&pid='+pid
|
||||
'&pid='+pid+
|
||||
token_param
|
||||
)
|
||||
.then((res)=>res.json())
|
||||
.then((json)=>{
|
||||
if(json.code!==0)
|
||||
throw new Error(json.code);
|
||||
throw new Error(json);
|
||||
this.setState({
|
||||
chunks: [{
|
||||
title: 'PID = '+pid,
|
||||
@@ -249,13 +273,26 @@ export class Flow extends PureComponent {
|
||||
loading_status: 'done',
|
||||
});
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.trace(err);
|
||||
this.setState((prev,props)=>({
|
||||
loaded_pages: prev.loaded_pages-1,
|
||||
loading_status: 'failed',
|
||||
}));
|
||||
});
|
||||
.catch(failed);
|
||||
} else if(this.state.mode==='attention') {
|
||||
fetch(
|
||||
API_BASE+'/api.php?action=getattention'+
|
||||
token_param
|
||||
)
|
||||
.then((res)=>res.json())
|
||||
.then((json)=>{
|
||||
if(json.code!==0)
|
||||
throw new Error(json);
|
||||
this.setState({
|
||||
chunks: [{
|
||||
title: 'Attention List',
|
||||
data: json.data,
|
||||
}],
|
||||
mode: 'attention_finished',
|
||||
loading_status: 'done',
|
||||
});
|
||||
})
|
||||
.catch(failed);
|
||||
} else {
|
||||
console.log('nothing to load');
|
||||
return;
|
||||
|
||||
@@ -41,10 +41,11 @@
|
||||
}
|
||||
@media screen and (max-width: 600px) {
|
||||
.sidebar {
|
||||
width: calc(100% - 50px);
|
||||
width: calc(100% - 25px);
|
||||
padding: 1em .5em;
|
||||
}
|
||||
.sidebar-on .sidebar {
|
||||
left: 50px;
|
||||
left: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,9 @@
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.title-bar a {
|
||||
padding: 0 .5em;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2em;
|
||||
line-height: 3em;
|
||||
font-size: 1.5em;
|
||||
line-height: 4em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -26,13 +22,10 @@
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
.control-bar .refresh-btn {
|
||||
flex: 0 0 100px;
|
||||
color: black;
|
||||
background-color: rgba(255,255,255,.5);
|
||||
border-radius: 5px;
|
||||
.control-btn {
|
||||
flex: 0 0 2em;
|
||||
text-align: center;
|
||||
border: 1px solid black;
|
||||
color: black;
|
||||
}
|
||||
.control-bar input {
|
||||
flex: auto;
|
||||
|
||||
56
src/Title.js
56
src/Title.js
@@ -1,4 +1,7 @@
|
||||
import React, {Component, PureComponent} from 'react';
|
||||
import {LoginForm} from './UserAction';
|
||||
import {TokenCtx} from './UserAction';
|
||||
|
||||
import './Title.css';
|
||||
|
||||
const HELP_TEXT=(
|
||||
@@ -10,7 +13,7 @@ const HELP_TEXT=(
|
||||
<li>在搜索框输入 #472865 等可以查看指定 ID 的树洞</li>
|
||||
<li>新的帖子会在左上角显示一个圆点</li>
|
||||
<li>请注意:使用 HTTPS 访问本站可能会<b>大幅减慢</b>加载速度</li>
|
||||
<li>自定义背景图片请修改 localStorage['REPLACE_ERIRI_WITH_URL']</li>
|
||||
<li>自定义背景图片请修改 <code>localStorage['REPLACE_ERIRI_WITH_URL']</code></li>
|
||||
</ul>
|
||||
<p>使用本网站时,您需要了解并同意:</p>
|
||||
<ul>
|
||||
@@ -50,6 +53,7 @@ class ControlBar extends PureComponent {
|
||||
this.on_change_bound=this.on_change.bind(this);
|
||||
this.on_keypress_bound=this.on_keypress.bind(this);
|
||||
this.do_refresh_bound=this.do_refresh.bind(this);
|
||||
this.do_attention_bound=this.do_attention.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -69,8 +73,10 @@ class ControlBar extends PureComponent {
|
||||
}
|
||||
|
||||
on_keypress(event) {
|
||||
if(event.key==='Enter')
|
||||
this.set_mode('search',this.state.search_text||null);
|
||||
if(event.key==='Enter') {
|
||||
const mode=this.state.search_text.startsWith('#') ? 'single' : 'search';
|
||||
this.set_mode(mode,this.state.search_text||null);
|
||||
}
|
||||
}
|
||||
|
||||
do_refresh() {
|
||||
@@ -81,20 +87,40 @@ class ControlBar extends PureComponent {
|
||||
this.set_mode('list',null);
|
||||
}
|
||||
|
||||
do_attention() {
|
||||
window.scrollTo(0,0);
|
||||
this.setState({
|
||||
search_text: '',
|
||||
});
|
||||
this.set_mode('attention',null);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="control-bar">
|
||||
<a className="refresh-btn" onClick={this.do_refresh_bound}>最新树洞</a>
|
||||
|
||||
<input value={this.state.search_text} placeholder="搜索 或 #PID"
|
||||
onChange={this.on_change_bound} onKeyPress={this.on_keypress_bound}
|
||||
/>
|
||||
|
||||
<a onClick={()=>{this.props.show_sidebar(
|
||||
'关于 P大树洞(非官方) 网页版',
|
||||
HELP_TEXT
|
||||
)}}>Help</a>
|
||||
</div>
|
||||
<TokenCtx.Consumer>{({value: token})=>(
|
||||
<div className="control-bar">
|
||||
<a className="control-btn" onClick={this.do_refresh_bound}>
|
||||
<span className="icon icon-refresh" />
|
||||
</a>
|
||||
{!!token &&
|
||||
<a className="control-btn" onClick={this.do_attention_bound}>
|
||||
<span className="icon icon-attention" />
|
||||
</a>
|
||||
}
|
||||
<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>
|
||||
</div>
|
||||
)}</TokenCtx.Consumer>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
8
src/UserAction.css
Normal file
8
src/UserAction.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.login-form form p {
|
||||
margin: 1em 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-form button {
|
||||
min-width: 100px;
|
||||
}
|
||||
92
src/UserAction.js
Normal file
92
src/UserAction.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import React, {Component, PureComponent} from 'react';
|
||||
|
||||
import './UserAction.css';
|
||||
|
||||
const LOGIN_BASE=window.location.protocol==='https:' ? '/login_proxy' : 'http://www.pkuhelper.com/services/login';
|
||||
|
||||
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_login(event,set_token) {
|
||||
event.preventDefault();
|
||||
this.setState({
|
||||
loading_status: 'loading',
|
||||
});
|
||||
let data=new URLSearchParams();
|
||||
data.append('uid', this.username_ref.current.value);
|
||||
data.append('password', this.password_ref.current.value);
|
||||
fetch(LOGIN_BASE+'/login.php?platform=hole_xmcp_ml', {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
.then((res)=>res.json())
|
||||
.then((json)=>{
|
||||
if(json.code!==0)
|
||||
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.trace(e);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TokenCtx.Consumer>{(token)=>
|
||||
<div className="login-form">
|
||||
<form onSubmit={(e)=>this.do_login(e,token.set_value)} className="box">
|
||||
<p>Token: <code>{token.value||'(null)'}</code></p>
|
||||
<p>
|
||||
<label>
|
||||
学号:
|
||||
<input ref={this.username_ref} type="tel" />
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
密码:
|
||||
<input ref={this.password_ref} type="password" />
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
{this.state.loading_status==='loading' ?
|
||||
<button disabled="disbled">正在登录……</button> :
|
||||
<button type="submit">登录</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>
|
||||
</div>
|
||||
}</TokenCtx.Consumer>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ input {
|
||||
border-radius: 5px;
|
||||
border: 1px solid black;
|
||||
outline: none;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
audio {
|
||||
@@ -40,4 +41,22 @@ audio {
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
font-family: '微软雅黑', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
button, .button {
|
||||
color: black;
|
||||
background-color: rgba(255,255,255,.5);
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
border: 1px solid black;
|
||||
line-height: 2em;
|
||||
margin: 0 .5em;
|
||||
}
|
||||
|
||||
button:disabled, .button:disabled {
|
||||
background-color: rgba(128,128,128,.5);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: Consolas, Courier, monospace;
|
||||
}
|
||||
Reference in New Issue
Block a user