forked from newthuhole/hole_thu_frontend
add life info box
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
.balance-popover {
|
||||
position: absolute;
|
||||
top: 2em;
|
||||
margin: auto;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
opacity: 0;
|
||||
animation: balance-disappear 2s ease-in;
|
||||
}
|
||||
|
||||
@keyframes balance-disappear {
|
||||
from {opacity: 1;}
|
||||
75% {opacity: 1;}
|
||||
to {opacity: 0;}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import React, {Component, PureComponent} from 'react';
|
||||
import {PKUHELPER_ROOT} from './infrastructure/const';
|
||||
import {API_VERSION_PARAM, get_json} from './flows_api';
|
||||
import {TokenCtx} from './UserAction';
|
||||
|
||||
import './BalanceShower.css';
|
||||
|
||||
export class BalanceShower extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state={
|
||||
loading_status: 'idle',
|
||||
error: null,
|
||||
balance: null,
|
||||
};
|
||||
}
|
||||
|
||||
do_load(e,token) {
|
||||
if(this.state.loading_status==='loading')
|
||||
return;
|
||||
if(e.target.closest('a')) // clicking at a link
|
||||
return;
|
||||
if(!token || !window.config.easter_egg) {
|
||||
this.setState({
|
||||
loading_status: 'idle',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading_status: 'loading',
|
||||
},()=>{
|
||||
fetch(
|
||||
PKUHELPER_ROOT+'api_xmcp/isop/card_balance'
|
||||
+'?user_token='+encodeURIComponent(token)
|
||||
+API_VERSION_PARAM()
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json)=>{
|
||||
console.log(json);
|
||||
if(!json.success)
|
||||
throw new Error(JSON.stringify(json));
|
||||
|
||||
this.setState({
|
||||
loading_status: 'done',
|
||||
error: null,
|
||||
balance: json.balance,
|
||||
});
|
||||
})
|
||||
.catch((e)=>{
|
||||
console.error(e);
|
||||
this.setState({
|
||||
loading_status: 'error',
|
||||
error: ''+e,
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
render_popover() {
|
||||
if(this.state.loading_status==='idle') // no token or disabled
|
||||
return null;
|
||||
else if(this.state.loading_status==='loading')
|
||||
return (<div className="box box-tip">……</div>);
|
||||
else if(this.state.loading_status==='error')
|
||||
return (<div className="box box-tip balance-value"><a onClick={()=>{alert(this.state.error)}}>无法查询余额</a></div>);
|
||||
else if(this.state.loading_status==='done')
|
||||
return (<div className="box box-tip balance-value">校园卡 ¥{this.state.balance.toFixed(2)}</div>);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TokenCtx.Consumer>{(token)=>(
|
||||
<div onClick={(e)=>this.do_load(e,token.value)}>
|
||||
<div className="balance-popover">{this.render_popover()}</div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)}</TokenCtx.Consumer>
|
||||
)
|
||||
}
|
||||
}
|
||||
92
src/Title.js
92
src/Title.js
@@ -1,65 +1,12 @@
|
||||
import React, {Component, PureComponent} from 'react';
|
||||
import {AppSwitcher} from './infrastructure/widgets';
|
||||
import {LoginForm, PostForm} from './UserAction';
|
||||
import {InfoSidebar, PostForm} from './UserAction';
|
||||
import {TokenCtx} from './UserAction';
|
||||
import {PromotionBar} from './Common';
|
||||
import {ConfigUI} from './Config';
|
||||
|
||||
import './Title.css';
|
||||
import {BalanceShower} from './BalanceShower';
|
||||
import {cache} from './cache';
|
||||
|
||||
const flag_re=/^\/\/setflag ([a-zA-Z0-9_]+)=(.*)$/;
|
||||
|
||||
const HELP_TEXT=(
|
||||
<div className="box help-desc-box">
|
||||
<p>
|
||||
PKUHelper 网页版树洞 by @xmcp,
|
||||
基于
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.zh-cn.html" target="_blank">GPLv3</a>
|
||||
协议在 <a href="https://github.com/pkuhelper-web/webhole" target="_blank">GitHub</a> 开源
|
||||
</p>
|
||||
<p>
|
||||
PKUHelper 网页版的诞生离不开
|
||||
<a href="https://reactjs.org/" target="_blank" rel="noopener">React</a>
|
||||
、
|
||||
<a href="https://icomoon.io/#icons" target="_blank" rel="noopener">IcoMoon</a>
|
||||
等开源项目
|
||||
</p>
|
||||
<p>
|
||||
<a onClick={()=>{
|
||||
if('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations()
|
||||
.then((registrations)=>{
|
||||
for(let registration of registrations) {
|
||||
console.log('unregister',registration);
|
||||
registration.unregister();
|
||||
}
|
||||
});
|
||||
}
|
||||
cache().clear();
|
||||
setTimeout(()=>{
|
||||
window.location.reload(true);
|
||||
},200);
|
||||
}}>强制检查更新</a>
|
||||
({process.env.REACT_APP_BUILD_INFO||'---'} {process.env.NODE_ENV} 会自动在后台检查更新并在下次访问时更新)
|
||||
</p>
|
||||
<p>
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
</p>
|
||||
<p>
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.zh-cn.html" target="_blank">GNU General Public License</a>
|
||||
for more details.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
class ControlBar extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -148,27 +95,7 @@ class ControlBar extends PureComponent {
|
||||
<a className="no-underline control-btn" onClick={()=>{
|
||||
this.props.show_sidebar(
|
||||
'P大树洞',
|
||||
<div>
|
||||
<PromotionBar />
|
||||
<LoginForm show_sidebar={this.props.show_sidebar} />
|
||||
<div className="box list-menu">
|
||||
<a onClick={()=>{this.props.show_sidebar(
|
||||
'设置',
|
||||
<ConfigUI />
|
||||
)}}>
|
||||
<span className="icon icon-settings" /><label>网页版树洞设置</label>
|
||||
</a>
|
||||
|
||||
<a href="http://pkuhelper.pku.edu.cn/treehole_rules.html" target="_blank">
|
||||
<span className="icon icon-textfile" /><label>树洞规范</label>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/pkuhelper-web/webhole/issues" target="_blank">
|
||||
<span className="icon icon-github" /><label>意见反馈</label>
|
||||
</a>
|
||||
</div>
|
||||
{HELP_TEXT}
|
||||
</div>
|
||||
<InfoSidebar show_sidebar={this.props.show_sidebar} />
|
||||
)
|
||||
}}>
|
||||
<span className={'icon icon-'+(token ? 'about' : 'login')} />
|
||||
@@ -199,13 +126,16 @@ export function Title(props) {
|
||||
<div className="title-bar">
|
||||
<AppSwitcher appid="hole" />
|
||||
<div className="aux-margin">
|
||||
<BalanceShower>
|
||||
<div className="title">
|
||||
<p className="centered-line">
|
||||
<div className="title">
|
||||
<p className="centered-line">
|
||||
<span onClick={()=>props.show_sidebar(
|
||||
'P大树洞',
|
||||
<InfoSidebar show_sidebar={props.show_sidebar} />
|
||||
)}>
|
||||
P大树洞
|
||||
</p>
|
||||
</div>
|
||||
</BalanceShower>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<ControlBar show_sidebar={props.show_sidebar} set_mode={props.set_mode} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,3 +57,24 @@
|
||||
min-height: 5em;
|
||||
height: 20em;
|
||||
}
|
||||
|
||||
.life-info-table {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
@media screen and (min-width: 375px) {
|
||||
.life-info-table {
|
||||
width: 315px;
|
||||
}
|
||||
}
|
||||
.life-info-table td {
|
||||
padding: .25em;
|
||||
}
|
||||
.life-info-table td:nth-child(1) {
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.life-info-error a {
|
||||
--var-link-color: hsl(25,100%,45%);
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import React, {Component, PureComponent} from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import {API_BASE,SafeTextarea} from './Common';
|
||||
import {API_BASE, SafeTextarea, PromotionBar} from './Common';
|
||||
import {MessageViewer} from './Message';
|
||||
import {API_VERSION_PARAM, PKUHELPER_ROOT, API, get_json, token_param} from './flows_api';
|
||||
|
||||
import './UserAction.css';
|
||||
import {LoginPopup} from './infrastructure/widgets';
|
||||
import {ConfigUI} from './Config';
|
||||
import {cache} from './cache';
|
||||
|
||||
const BASE64_RATE=4/3;
|
||||
const MAX_IMG_DIAM=8000;
|
||||
@@ -17,6 +19,252 @@ export const TokenCtx=React.createContext({
|
||||
set_value: ()=>{},
|
||||
});
|
||||
|
||||
class LifeInfoBox extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
if(!window._life_info_cache)
|
||||
window._life_info_cache={};
|
||||
this.CACHE_TIMEOUT_S=15;
|
||||
this.state={
|
||||
today_info: this.cache_get('today_info'),
|
||||
card_balance: this.cache_get('card_balance'),
|
||||
net_balance: this.cache_get('net_balance'),
|
||||
mail_count: this.cache_get('mail_count'),
|
||||
};
|
||||
this.INTERNAL_NETWORK_FAILURE='_network_failure';
|
||||
this.API_NAME={
|
||||
today_info: 'hole/today_info',
|
||||
card_balance: 'isop/card_balance',
|
||||
net_balance: 'isop/net_balance',
|
||||
mail_count: 'isop/mail_count',
|
||||
};
|
||||
}
|
||||
|
||||
cache_get(key) {
|
||||
let cache_item=window._life_info_cache[key];
|
||||
if(!cache_item || (+new Date())-cache_item[0]>1000*this.CACHE_TIMEOUT_S)
|
||||
return null;
|
||||
else
|
||||
return cache_item[1];
|
||||
}
|
||||
cache_set(key,value) {
|
||||
if(!window._life_info_cache[key] || window._life_info_cache[key][1]!==value)
|
||||
window._life_info_cache[key]=[+new Date(),value];
|
||||
}
|
||||
|
||||
load(state_key) {
|
||||
this.setState({
|
||||
[state_key]: null,
|
||||
},()=>{
|
||||
fetch(
|
||||
PKUHELPER_ROOT+'api_xmcp/'+this.API_NAME[state_key]
|
||||
+'?user_token='+encodeURIComponent(this.props.token)
|
||||
+API_VERSION_PARAM()
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json)=>{
|
||||
console.log(json);
|
||||
|
||||
this.setState({
|
||||
[state_key]: json,
|
||||
});
|
||||
})
|
||||
.catch((e)=>{
|
||||
this.setState({
|
||||
[state_key]: {
|
||||
errMsg: '网络错误 '+e,
|
||||
errCode: this.INTERNAL_NETWORK_FAILURE,
|
||||
success: false,
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
['today_info','card_balance','net_balance','mail_count'].forEach((k)=>{
|
||||
if(!this.state[k])
|
||||
this.load(k);
|
||||
});
|
||||
}
|
||||
|
||||
reload_all() {
|
||||
['today_info','card_balance','net_balance','mail_count'].forEach((k)=>{
|
||||
this.load(k);
|
||||
});
|
||||
}
|
||||
|
||||
render_line(state_key,title,value_fn,action,url_fn,do_login) {
|
||||
let s=this.state[state_key];
|
||||
if(!s)
|
||||
return (
|
||||
<tr>
|
||||
<td>{title}</td>
|
||||
<td>加载中……</td>
|
||||
<td />
|
||||
</tr>
|
||||
);
|
||||
else if(!s.success) {
|
||||
let type='加载失败';
|
||||
if(s.errCode===this.INTERNAL_NETWORK_FAILURE)
|
||||
type='网络错误';
|
||||
else if(['E01','E02','E03'].indexOf(s.errCode)!==-1)
|
||||
type='授权失效';
|
||||
|
||||
let details=JSON.stringify(s);
|
||||
if(s.errMsg)
|
||||
details=s.errMsg;
|
||||
else if(s.error)
|
||||
details=s.error;
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{title}</td>
|
||||
<td className="life-info-error">
|
||||
<a onClick={()=>alert(details)}>{type}</a>
|
||||
</td>
|
||||
<td>
|
||||
{type==='授权失效' ?
|
||||
<a onClick={do_login}>
|
||||
<span className="icon icon-forward" /> 重新登录
|
||||
</a> :
|
||||
<a onClick={()=>this.load(state_key)}>
|
||||
<span className="icon icon-forward" /> 重试
|
||||
</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
else {
|
||||
this.cache_set(state_key,s);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{title}</td>
|
||||
<td>{value_fn(s)}</td>
|
||||
<td>
|
||||
<a href={url_fn(s)} target="_blank">
|
||||
<span className="icon icon-forward" /> {action}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<LoginPopup token_callback={(t)=>{
|
||||
this.props.set_token(t);
|
||||
this.reload_all();
|
||||
}}>{(do_login)=>(
|
||||
<div className="box">
|
||||
<table className="life-info-table">
|
||||
<tbody>
|
||||
{this.render_line(
|
||||
'today_info',
|
||||
'今日',(s)=>s.info,
|
||||
'校历',(s)=>s.schedule_url,
|
||||
do_login,
|
||||
)}
|
||||
{this.render_line(
|
||||
'card_balance',
|
||||
'校园卡',(s)=>`余额¥${s.balance.toFixed(2)}`,
|
||||
'充值',()=>'https://virtualprod.alipay.com/educate/educatePcRecharge.htm?schoolCode=PKU&schoolName=',
|
||||
do_login,
|
||||
)}
|
||||
{this.render_line(
|
||||
'net_balance',
|
||||
'网费',(s)=>`余额¥${s.balance.toFixed(2)}`,
|
||||
'充值',()=>'https://its.pku.edu.cn/epay.jsp',
|
||||
do_login,
|
||||
)}
|
||||
{this.render_line(
|
||||
'mail_count',
|
||||
'邮件',(s)=>`未读 ${s.count} 封`,
|
||||
'查看',()=>'https://mail.pku.edu.cn/',
|
||||
do_login,
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}</LoginPopup>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function InfoSidebar(props) {
|
||||
return (
|
||||
<div>
|
||||
<PromotionBar />
|
||||
<LoginForm show_sidebar={props.show_sidebar} />
|
||||
<div className="box list-menu">
|
||||
<a onClick={()=>{props.show_sidebar(
|
||||
'设置',
|
||||
<ConfigUI />
|
||||
)}}>
|
||||
<span className="icon icon-settings" /><label>网页版树洞设置</label>
|
||||
</a>
|
||||
|
||||
<a href="http://pkuhelper.pku.edu.cn/treehole_rules.html" target="_blank">
|
||||
<span className="icon icon-textfile" /><label>树洞规范</label>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/pkuhelper-web/webhole/issues" target="_blank">
|
||||
<span className="icon icon-github" /><label>意见反馈</label>
|
||||
</a>
|
||||
</div>
|
||||
<div className="box help-desc-box">
|
||||
<p>
|
||||
PKUHelper 网页版树洞 by @xmcp,
|
||||
基于
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.zh-cn.html" target="_blank">GPLv3</a>
|
||||
协议在 <a href="https://github.com/pkuhelper-web/webhole" target="_blank">GitHub</a> 开源
|
||||
</p>
|
||||
<p>
|
||||
PKUHelper 网页版的诞生离不开
|
||||
<a href="https://reactjs.org/" target="_blank" rel="noopener">React</a>
|
||||
、
|
||||
<a href="https://icomoon.io/#icons" target="_blank" rel="noopener">IcoMoon</a>
|
||||
等开源项目
|
||||
</p>
|
||||
<p>
|
||||
<a onClick={()=>{
|
||||
if('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations()
|
||||
.then((registrations)=>{
|
||||
for(let registration of registrations) {
|
||||
console.log('unregister',registration);
|
||||
registration.unregister();
|
||||
}
|
||||
});
|
||||
}
|
||||
cache().clear();
|
||||
setTimeout(()=>{
|
||||
window.location.reload(true);
|
||||
},200);
|
||||
}}>强制检查更新</a>
|
||||
({process.env.REACT_APP_BUILD_INFO||'---'} {process.env.NODE_ENV} 会自动在后台检查更新并在下次访问时更新)
|
||||
</p>
|
||||
<p>
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
</p>
|
||||
<p>
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.zh-cn.html" target="_blank">GNU General Public License</a>
|
||||
for more details.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
class ResetUsertokenWidget extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -80,45 +328,50 @@ export class LoginForm extends Component {
|
||||
render() {
|
||||
return (
|
||||
<TokenCtx.Consumer>{(token)=>
|
||||
<div className="login-form box">
|
||||
{token.value ?
|
||||
<div>
|
||||
<p>
|
||||
<b>您已登录。</b>
|
||||
<button type="button" onClick={()=>{token.set_value(null);}}>
|
||||
<span className="icon icon-logout" /> 注销
|
||||
</button>
|
||||
<br />
|
||||
</p>
|
||||
<p>
|
||||
根据计算中心要求,访问授权三个月内有效。<br />若提示“授权过期”,请注销后重新登录。
|
||||
</p>
|
||||
<p>
|
||||
<a onClick={()=>{this.props.show_sidebar(
|
||||
'系统消息',
|
||||
<MessageViewer token={token.value} />
|
||||
)}}>查看系统消息</a><br />
|
||||
当您发送的内容违规时,我们将用系统消息提示您
|
||||
</p>
|
||||
<p>
|
||||
<a onClick={this.copy_token.bind(this,token.value)}>复制 User Token</a><br />
|
||||
User Token 用于迁移登录状态,切勿告知他人,若怀疑被盗号请尽快 <ResetUsertokenWidget token={token.value} />
|
||||
</p>
|
||||
</div> :
|
||||
<LoginPopup token_callback={token.set_value}>{(do_popup)=>(
|
||||
<div>
|
||||
{!!token.value &&
|
||||
<LifeInfoBox token={token.value} set_token={token.set_value} />
|
||||
}
|
||||
<div className="login-form box">
|
||||
{token.value ?
|
||||
<div>
|
||||
<p>
|
||||
<button type="button" onClick={do_popup}>
|
||||
<span className="icon icon-login" />
|
||||
登录
|
||||
<b>您已登录。</b>
|
||||
<button type="button" onClick={()=>{token.set_value(null);}}>
|
||||
<span className="icon icon-logout" /> 注销
|
||||
</button>
|
||||
<br />
|
||||
</p>
|
||||
<p><small>
|
||||
PKU Helper 面向北京大学学生,通过 ISOP(北京大学数据共享开放服务平台)验证您的身份并提供服务。
|
||||
</small></p>
|
||||
</div>
|
||||
)}</LoginPopup>
|
||||
}
|
||||
<p>
|
||||
根据计算中心要求,访问授权三个月内有效,过期需重新登录。
|
||||
</p>
|
||||
<p>
|
||||
<a onClick={()=>{this.props.show_sidebar(
|
||||
'系统消息',
|
||||
<MessageViewer token={token.value} />
|
||||
)}}>查看系统消息</a><br />
|
||||
当您发送的内容违规时,我们将用系统消息提示您
|
||||
</p>
|
||||
<p>
|
||||
<a onClick={this.copy_token.bind(this,token.value)}>复制 User Token</a><br />
|
||||
User Token 用于迁移登录状态,切勿告知他人,若怀疑被盗号请尽快 <ResetUsertokenWidget token={token.value} />
|
||||
</p>
|
||||
</div> :
|
||||
<LoginPopup token_callback={token.set_value}>{(do_popup)=>(
|
||||
<div>
|
||||
<p>
|
||||
<button type="button" onClick={do_popup}>
|
||||
<span className="icon icon-login" />
|
||||
登录
|
||||
</button>
|
||||
</p>
|
||||
<p><small>
|
||||
PKU Helper 面向北京大学学生,通过 ISOP(北京大学数据共享开放服务平台)验证您的身份并提供服务。
|
||||
</small></p>
|
||||
</div>
|
||||
)}</LoginPopup>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}</TokenCtx.Consumer>
|
||||
)
|
||||
|
||||
Submodule src/infrastructure updated: 8004bac71e...110f225b4e
Reference in New Issue
Block a user