合并submodule
4
.gitmodules
vendored
@@ -1,7 +1,3 @@
|
|||||||
[submodule "src/react-lazyload"]
|
[submodule "src/react-lazyload"]
|
||||||
path = src/react-lazyload
|
path = src/react-lazyload
|
||||||
url = https://github.com/xmcp/react-lazyload
|
url = https://github.com/xmcp/react-lazyload
|
||||||
|
|
||||||
[submodule "src/infrastructure"]
|
|
||||||
path = src/infrastructure
|
|
||||||
url = https://github.com/thuhole/infrastructure
|
|
||||||
|
|||||||
BIN
src/infrastructure/appicon/course_survey.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/infrastructure/appicon/dropdown.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/infrastructure/appicon/dropdown_rev.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/infrastructure/appicon/hole.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/infrastructure/appicon/homepage.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/infrastructure/appicon/imasugu.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/infrastructure/appicon/imasugu_rev.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/infrastructure/appicon/score.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/infrastructure/appicon/syllabus.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
2
src/infrastructure/const.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// export const THUHOLE_API_ROOT='//localhost:5001/';
|
||||||
|
export const THUHOLE_API_ROOT = 'https://thuhole.com/'
|
||||||
47
src/infrastructure/elevator.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
const DUMP_VER='dump_v1';
|
||||||
|
|
||||||
|
function dump() {
|
||||||
|
return JSON.stringify({
|
||||||
|
_dump_ver: DUMP_VER,
|
||||||
|
token: localStorage['TOKEN']||null,
|
||||||
|
hole_config: localStorage['hole_config']||null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function load(s) {
|
||||||
|
console.log('elevator: loading',s);
|
||||||
|
let obj=JSON.parse(s);
|
||||||
|
if(obj._dump_ver!==DUMP_VER) {
|
||||||
|
console.error('elevator: loading version mismatch, current',DUMP_VER,'param',obj._dump_ver);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(localStorage['TOKEN']===undefined && obj.token) {
|
||||||
|
console.log('replace token');
|
||||||
|
localStorage['TOKEN']=obj.token;
|
||||||
|
}
|
||||||
|
if(localStorage['hole_config']===undefined && obj.hole_config) {
|
||||||
|
console.log('replace hole config');
|
||||||
|
localStorage['hole_config']=obj.hole_config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function elevate() {
|
||||||
|
// load
|
||||||
|
// '?foo=fo&bar=ba' -> [["foo","fo"],["bar","ba"]]
|
||||||
|
let params=window.location.search.substr(1).split('&').map((kv)=>kv.split('='));
|
||||||
|
params.forEach((kv)=>{
|
||||||
|
if(kv.length===2 && kv[0]==='_elevator_data') {
|
||||||
|
load(decodeURIComponent(kv[1]));
|
||||||
|
let url=new URL(window.location.href);
|
||||||
|
url.search='';
|
||||||
|
window.history.replaceState('','',url.href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// dump
|
||||||
|
if(window.location.protocol==='http:' && window.location.hostname==='pkuhelper.pku.edu.cn') {
|
||||||
|
let url=new URL(window.location.href);
|
||||||
|
url.protocol='https:';
|
||||||
|
url.search='?_elevator_data='+encodeURIComponent(dump());
|
||||||
|
window.location.replace(url.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/infrastructure/functions.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export function get_json(res) {
|
||||||
|
if(!res.ok) throw Error(`网络错误 ${res.status} ${res.statusText}`);
|
||||||
|
return (
|
||||||
|
res
|
||||||
|
.text()
|
||||||
|
.then((t)=>{
|
||||||
|
try {
|
||||||
|
return JSON.parse(t);
|
||||||
|
} catch(e) {
|
||||||
|
console.error('json parse error');
|
||||||
|
console.trace(e);
|
||||||
|
console.log(t);
|
||||||
|
throw new SyntaxError('JSON Parse Error '+t.substr(0,50));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listen_darkmode(override) { // override: true/false/undefined
|
||||||
|
function update_color_scheme() {
|
||||||
|
if(override===undefined ? window.matchMedia('(prefers-color-scheme: dark)').matches : override)
|
||||||
|
document.body.classList.add('root-dark-mode');
|
||||||
|
else
|
||||||
|
document.body.classList.remove('root-dark-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
update_color_scheme();
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addListener(()=>{
|
||||||
|
update_color_scheme();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function API_VERSION_PARAM() {
|
||||||
|
return '&PKUHelperAPI=3.0&jsapiver='+encodeURIComponent((process.env.REACT_APP_BUILD_INFO||'null')+'-'+(Math.floor(+new Date()/7200000)*2));
|
||||||
|
}
|
||||||
37
src/infrastructure/global.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
:root {
|
||||||
|
--foreground-dark: hsl(0,0%,93%);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, textarea, pre {
|
||||||
|
font-family: 'Segoe UI', '微软雅黑', 'Microsoft YaHei', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
word-wrap: break-word;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
p, pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: Consolas, Courier, monospace;
|
||||||
|
}
|
||||||
301
src/infrastructure/widgets.css
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
.centered-line {
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.centered-line::before,
|
||||||
|
.centered-line::after {
|
||||||
|
background-color: #000;
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
height: 1px;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root-dark-mode .centered-line {
|
||||||
|
color: var(--foreground-dark);
|
||||||
|
}
|
||||||
|
.root-dark-mode .centered-line::before, .root-dark-mode .centered-line::after {
|
||||||
|
background-color: var(--foreground-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.centered-line::before {
|
||||||
|
right: 1em;
|
||||||
|
margin-left: -50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.centered-line::after {
|
||||||
|
left: 1em;
|
||||||
|
margin-right: -50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-line {
|
||||||
|
color: #fff;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
.title-line::before,
|
||||||
|
.title-line::after {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 1px 1px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root-dark-mode .title-line {
|
||||||
|
color: var(--foreground-dark);
|
||||||
|
}
|
||||||
|
.root-dark-mode .title-line::before, .root-dark-mode .title-line::after {
|
||||||
|
background-color: var(--foreground-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-switcher {
|
||||||
|
display: flex;
|
||||||
|
height: 2em;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 .1em;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.app-switcher-desc {
|
||||||
|
margin: 0 .5em;
|
||||||
|
flex: 1 1 0;
|
||||||
|
opacity: .5;
|
||||||
|
height: 2em;
|
||||||
|
line-height: 2rem;
|
||||||
|
font-size: .8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root-dark-mode .app-switcher-desc {
|
||||||
|
color: var(--foreground-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 570px) {
|
||||||
|
.app-switcher-desc {
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.app-switcher-item {
|
||||||
|
flex: 1 1 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.app-switcher-dropdown-title {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
.app-switcher-dropdown-item {
|
||||||
|
margin-left: -2em !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-switcher a:hover { /* reset underline from /hole style */
|
||||||
|
border-bottom: unset;
|
||||||
|
margin-bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-switcher-desc a {
|
||||||
|
color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-switcher-left {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.app-switcher-right {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.app-switcher-item {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-radius: 3px;
|
||||||
|
height: 1.6em;
|
||||||
|
line-height: 1.6em;
|
||||||
|
margin: .2em .1em;
|
||||||
|
padding: 0 .45em;
|
||||||
|
}
|
||||||
|
a.app-switcher-item, .app-switcher-item a {
|
||||||
|
transition: unset; /* override ant design */
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.app-switcher-item img {
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
position: relative;
|
||||||
|
top: .2rem;
|
||||||
|
vertical-align: unset; /* override ant design */
|
||||||
|
}
|
||||||
|
.app-switcher-item span:not(:empty) {
|
||||||
|
margin-left: .2rem;
|
||||||
|
}
|
||||||
|
.app-switcher-logo-hover {
|
||||||
|
margin-left: -1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-switcher-item:hover {
|
||||||
|
background-color: black;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
.app-switcher-item:hover a {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
.app-switcher-item-current {
|
||||||
|
background-color: rgba(0,0,0,.4);
|
||||||
|
text-shadow: 0 0 5px rgba(0,0,0,.5);
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
.app-switcher-item-current a {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root-dark-mode .app-switcher-item, .root-dark-mode .app-switcher-dropdown-title a {
|
||||||
|
color: var(--foreground-dark);
|
||||||
|
}
|
||||||
|
.root-dark-mode .app-switcher-item:hover, .root-dark-mode .app-switcher-item-current, .root-dark-mode .app-switcher-dropdown-title:hover a {
|
||||||
|
background-color: #555;
|
||||||
|
color: var(--foreground-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-switcher-item:hover .app-switcher-logo-normal, .app-switcher-item-current .app-switcher-logo-normal {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.app-switcher-item:not(.app-switcher-item-current):not(:hover) .app-switcher-logo-hover {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root-dark-mode .app-switcher-logo-normal {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
.root-dark-mode .app-switcher-logo-hover {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-switcher-dropdown {
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-switcher-dropdown:not(:hover) {
|
||||||
|
max-height: 1.6rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-switcher-dropdown-item {
|
||||||
|
background-color: hsla(0,0%,35%,.9);
|
||||||
|
padding: .125em .25em;
|
||||||
|
margin-left: -.75em;
|
||||||
|
margin-right: -.75em;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.app-switcher-dropdown-item:hover {
|
||||||
|
background-color: rgba(0,0,0,.9);
|
||||||
|
}
|
||||||
|
.app-switcher-dropdown-item:nth-child(2) {
|
||||||
|
border-top-left-radius: 3px;
|
||||||
|
border-top-right-radius: 3px;
|
||||||
|
}
|
||||||
|
.app-switcher-dropdown-item:last-child {
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
border-bottom-right-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-switcher-dropdown-title {
|
||||||
|
padding-bottom: .2em;
|
||||||
|
padding-left: .5em;
|
||||||
|
padding-right: .25em;
|
||||||
|
}
|
||||||
|
.app-switcher-dropdown-title a {
|
||||||
|
cursor: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thuhole-login-popup {
|
||||||
|
font-size: 1rem;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
color: black;
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 320px;
|
||||||
|
z-index: 114515;
|
||||||
|
transform: translateX(-50%) translateY(-50%);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.thuhole-login-popup a {
|
||||||
|
color: #00c;
|
||||||
|
}
|
||||||
|
.thuhole-login-popup p {
|
||||||
|
margin: .75em 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
/* override ant design */
|
||||||
|
.thuhole-login-popup input, .thuhole-login-popup button {
|
||||||
|
font-size: .85em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.thuhole-login-popup input:not([type="checkbox"]) {
|
||||||
|
width: 8rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid black;
|
||||||
|
outline: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 .5em;
|
||||||
|
line-height: 2em;
|
||||||
|
}
|
||||||
|
.thuhole-login-popup button {
|
||||||
|
width: 6rem;
|
||||||
|
color: black;
|
||||||
|
background-color: rgba(235,235,235,.5);
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid black;
|
||||||
|
line-height: 2em;
|
||||||
|
margin: 0 .5rem;
|
||||||
|
}
|
||||||
|
.thuhole-login-popup button:hover {
|
||||||
|
background-color: rgba(255,255,255,.7);
|
||||||
|
}
|
||||||
|
.thuhole-login-popup button:disabled {
|
||||||
|
background-color: rgba(128,128,128,.5);
|
||||||
|
}
|
||||||
|
.thuhole-login-type {
|
||||||
|
display: inline-block;
|
||||||
|
width: 6rem;
|
||||||
|
margin: 0 .5rem;
|
||||||
|
}
|
||||||
|
.thuhole-login-popup-shadow {
|
||||||
|
opacity: .5;
|
||||||
|
background-color: black;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 114514;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thuhole-login-popup label.perm-item {
|
||||||
|
font-size: .8em;
|
||||||
|
vertical-align: .1rem;
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aux-margin {
|
||||||
|
width: calc(100% - 2 * 50px);
|
||||||
|
margin: 0 50px;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 1300px) {
|
||||||
|
.aux-margin {
|
||||||
|
width: calc(100% - 2 * 10px);
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
height: 4rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-str {
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
472
src/infrastructure/widgets.js
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
import React, {Component, PureComponent} from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
import TimeAgo from 'react-timeago';
|
||||||
|
import chineseStrings from 'react-timeago/lib/language-strings/zh-CN';
|
||||||
|
import buildFormatter from 'react-timeago/lib/formatters/buildFormatter';
|
||||||
|
|
||||||
|
import './global.css';
|
||||||
|
import './widgets.css';
|
||||||
|
|
||||||
|
import appicon_hole from './appicon/hole.png';
|
||||||
|
import appicon_imasugu from './appicon/imasugu.png';
|
||||||
|
import appicon_imasugu_rev from './appicon/imasugu_rev.png';
|
||||||
|
import appicon_syllabus from './appicon/syllabus.png';
|
||||||
|
import appicon_score from './appicon/score.png';
|
||||||
|
import appicon_course_survey from './appicon/course_survey.png';
|
||||||
|
import appicon_dropdown from './appicon/dropdown.png';
|
||||||
|
import appicon_dropdown_rev from './appicon/dropdown_rev.png';
|
||||||
|
import appicon_homepage from './appicon/homepage.png';
|
||||||
|
import {THUHOLE_API_ROOT} from './const';
|
||||||
|
import {get_json, API_VERSION_PARAM} from './functions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
GoogleReCaptchaProvider,
|
||||||
|
GoogleReCaptcha
|
||||||
|
} from 'react-google-recaptcha-v3';
|
||||||
|
|
||||||
|
const LOGIN_POPUP_ANCHOR_ID='pkuhelper_login_popup_anchor';
|
||||||
|
|
||||||
|
function pad2(x) {
|
||||||
|
return x<10 ? '0'+x : ''+x;
|
||||||
|
}
|
||||||
|
export function format_time(time) {
|
||||||
|
return `${time.getMonth()+1}-${pad2(time.getDate())} ${time.getHours()}:${pad2(time.getMinutes())}:${pad2(time.getSeconds())}`;
|
||||||
|
}
|
||||||
|
const chinese_format=buildFormatter(chineseStrings);
|
||||||
|
export function Time(props) {
|
||||||
|
const time=new Date(props.stamp*1000);
|
||||||
|
return (
|
||||||
|
<span className={"time-str"}>
|
||||||
|
<TimeAgo date={time} formatter={chinese_format} title={time.toLocaleString('zh-CN', {
|
||||||
|
timeZone: 'Asia/Shanghai',
|
||||||
|
hour12: false,
|
||||||
|
})} />
|
||||||
|
|
||||||
|
{!props.short ? format_time(time) : null}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TitleLine(props) {
|
||||||
|
return (
|
||||||
|
<p className="centered-line title-line aux-margin">
|
||||||
|
<span className="black-outline">{props.text}</span>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GlobalTitle(props) {
|
||||||
|
return (
|
||||||
|
<div className="aux-margin">
|
||||||
|
<div className="title">
|
||||||
|
<p className="centered-line">{props.text}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FALLBACK_APPS={
|
||||||
|
// id, text, url, icon_normal, icon_hover, new_tab
|
||||||
|
bar: [
|
||||||
|
['hole', '树洞', '/hole', appicon_hole, null, false],
|
||||||
|
['imasugu', '教室', '/spare_classroom', appicon_imasugu, appicon_imasugu_rev, false],
|
||||||
|
['syllabus', '课表', '/syllabus', appicon_syllabus, null, false],
|
||||||
|
['score', '成绩', '/my_score', appicon_score, null, false],
|
||||||
|
],
|
||||||
|
dropdown: [
|
||||||
|
['course_survey', '课程测评', 'https://courses.pinzhixiaoyuan.com/', appicon_course_survey, null, true],
|
||||||
|
['homepage', '客户端', '/', appicon_homepage, null, true],
|
||||||
|
],
|
||||||
|
fix: {},
|
||||||
|
};
|
||||||
|
// const SWITCHER_DATA_VER='switcher_2';
|
||||||
|
// const SWITCHER_DATA_URL=THUHOLE_API_ROOT+'web_static/appswitcher_items.json';
|
||||||
|
|
||||||
|
// export class AppSwitcher extends Component {
|
||||||
|
// constructor(props) {
|
||||||
|
// super(props);
|
||||||
|
// this.state={
|
||||||
|
// apps: this.get_apps_from_localstorage(),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// get_apps_from_localstorage() {
|
||||||
|
// let ret=FALLBACK_APPS;
|
||||||
|
// if(localStorage['APPSWITCHER_ITEMS'])
|
||||||
|
// try {
|
||||||
|
// let content=JSON.parse(localStorage['APPSWITCHER_ITEMS'])[SWITCHER_DATA_VER];
|
||||||
|
// if(!content || !content.bar)
|
||||||
|
// throw new Error('content is empty');
|
||||||
|
//
|
||||||
|
// ret=content;
|
||||||
|
// } catch(e) {
|
||||||
|
// console.error('load appswitcher items from localstorage failed');
|
||||||
|
// console.trace(e);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return ret;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// check_fix() {
|
||||||
|
// if(this.state.apps && this.state.apps.fix && this.state.apps.fix[this.props.appid])
|
||||||
|
// setTimeout(()=>{
|
||||||
|
// window.HOTFIX_CONTEXT={
|
||||||
|
// build_info: process.env.REACT_APP_BUILD_INFO || '---',
|
||||||
|
// build_env: process.env.NODE_ENV,
|
||||||
|
// };
|
||||||
|
// eval(this.state.apps.fix[this.props.appid]);
|
||||||
|
// },1); // make it async so failures won't be critical
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// componentDidMount() {
|
||||||
|
// this.check_fix();
|
||||||
|
// setTimeout(()=>{
|
||||||
|
// fetch(SWITCHER_DATA_URL)
|
||||||
|
// .then((res)=>{
|
||||||
|
// if(!res.ok) throw Error(`网络错误 ${res.status} ${res.statusText}`);
|
||||||
|
// return res.text();
|
||||||
|
// })
|
||||||
|
// .then((txt)=>{
|
||||||
|
// if(txt!==localStorage['APPSWITCHER_ITEMS']) {
|
||||||
|
// console.log('loaded new appswitcher items',txt);
|
||||||
|
// localStorage['APPSWITCHER_ITEMS']=txt;
|
||||||
|
//
|
||||||
|
// this.setState({
|
||||||
|
// apps: this.get_apps_from_localstorage(),
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// console.log('appswitcher items unchanged');
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .catch((e)=>{
|
||||||
|
// console.error('loading appswitcher items failed');
|
||||||
|
// console.trace(e);
|
||||||
|
// });
|
||||||
|
// },500);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// componentDidUpdate(prevProps, prevState) {
|
||||||
|
// if(this.state.apps!==prevState.apps)
|
||||||
|
// this.check_fix();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// render() {
|
||||||
|
// let cur_id=this.props.appid;
|
||||||
|
//
|
||||||
|
// function app_elem([id,title,url,icon_normal,icon_hover,new_tab],no_class=false,ref=null) {
|
||||||
|
// return (
|
||||||
|
// <a ref={ref} key={id} className={no_class ? null : ('app-switcher-item'+(id===cur_id ? ' app-switcher-item-current' : ''))}
|
||||||
|
// href={url} target={new_tab ? '_blank' : '_self'}>
|
||||||
|
// {!!icon_normal && [
|
||||||
|
// <img key="normal" src={icon_normal} className="app-switcher-logo-normal" />,
|
||||||
|
// <img key="hover" src={icon_hover||icon_normal} className="app-switcher-logo-hover" />
|
||||||
|
// ]}
|
||||||
|
// <span>{title}</span>
|
||||||
|
// </a>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let dropdown_cur_app=null;
|
||||||
|
// this.state.apps.dropdown.forEach((app)=>{
|
||||||
|
// if(app[0]===cur_id)
|
||||||
|
// dropdown_cur_app=app;
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// //console.log(JSON.stringify(this.state.apps));
|
||||||
|
//
|
||||||
|
// return (
|
||||||
|
// <div className="app-switcher">
|
||||||
|
// <span className="app-switcher-desc app-switcher-left">PKUHelper</span>
|
||||||
|
// {this.state.apps.bar.map((app)=>
|
||||||
|
// app_elem(app)
|
||||||
|
// )}
|
||||||
|
// {!!this.state.apps.dropdown.length &&
|
||||||
|
// <div className={
|
||||||
|
// 'app-switcher-item app-switcher-dropdown '
|
||||||
|
// +(dropdown_cur_app ? ' app-switcher-item-current' : '')
|
||||||
|
// }>
|
||||||
|
// <p className="app-switcher-dropdown-title">
|
||||||
|
// {!!dropdown_cur_app ?
|
||||||
|
// app_elem((()=>{
|
||||||
|
// let [id,title,_url,icon_normal,icon_hover,_new_tab]=dropdown_cur_app;
|
||||||
|
// return [id,title+'▾',null,icon_normal,icon_hover,false];
|
||||||
|
// })(),true) :
|
||||||
|
// app_elem(['-placeholder-elem','更多▾',null,appicon_dropdown,appicon_dropdown_rev,false],true)
|
||||||
|
// }
|
||||||
|
// </p>
|
||||||
|
// {this.state.apps.dropdown.map((app)=>{
|
||||||
|
// let ref=React.createRef();
|
||||||
|
// return (
|
||||||
|
// <p key={app[0]} className="app-switcher-dropdown-item" onClick={(e)=>{
|
||||||
|
// if(!e.target.closest('a') && ref.current)
|
||||||
|
// ref.current.click();
|
||||||
|
// }}>
|
||||||
|
// {app_elem(app,true,ref)}
|
||||||
|
// </p>
|
||||||
|
// );
|
||||||
|
// })}
|
||||||
|
// </div>
|
||||||
|
// }
|
||||||
|
// <span className="app-switcher-desc app-switcher-right">网页版</span>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
class LoginPopupSelf extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state={
|
||||||
|
loading_status: 'idle',
|
||||||
|
recaptcha_verified: false
|
||||||
|
// excluded_scopes: [],
|
||||||
|
};
|
||||||
|
this.username_ref=React.createRef();
|
||||||
|
this.password_ref=React.createRef();
|
||||||
|
this.input_token_ref=React.createRef();
|
||||||
|
|
||||||
|
this.popup_anchor=document.getElementById(LOGIN_POPUP_ANCHOR_ID);
|
||||||
|
if(!this.popup_anchor) {
|
||||||
|
this.popup_anchor=document.createElement('div');
|
||||||
|
this.popup_anchor.id=LOGIN_POPUP_ANCHOR_ID;
|
||||||
|
document.body.appendChild(this.popup_anchor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do_sendcode(type) {
|
||||||
|
if(!this.state.recaptcha_verified) {
|
||||||
|
alert("reCAPTCHA风控系统正在评估您的浏览器安全状态,请稍后重试。")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(this.state.loading_status==='loading')
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loading_status: 'loading',
|
||||||
|
},()=>{
|
||||||
|
fetch(
|
||||||
|
THUHOLE_API_ROOT+'api_xmcp/login/send_code'
|
||||||
|
+'?user='+encodeURIComponent(this.username_ref.current.value)
|
||||||
|
+'&code_type='+encodeURIComponent(type)
|
||||||
|
+"&recaptcha_token="+localStorage["recaptcha"]
|
||||||
|
+API_VERSION_PARAM(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
excluded_scopes: [],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(get_json)
|
||||||
|
.then((json)=>{
|
||||||
|
console.log(json);
|
||||||
|
if(!json.success)
|
||||||
|
throw new Error(JSON.stringify(json));
|
||||||
|
|
||||||
|
alert(json.msg);
|
||||||
|
this.setState({
|
||||||
|
loading_status: 'done',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e)=>{
|
||||||
|
console.error(e);
|
||||||
|
alert('发送失败\n'+e);
|
||||||
|
this.setState({
|
||||||
|
loading_status: 'done',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
do_login(set_token) {
|
||||||
|
if(this.state.loading_status==='loading')
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loading_status: 'loading',
|
||||||
|
},()=>{
|
||||||
|
fetch(
|
||||||
|
THUHOLE_API_ROOT+'api_xmcp/login/login'
|
||||||
|
+'?user='+encodeURIComponent(this.username_ref.current.value)
|
||||||
|
+'&valid_code='+encodeURIComponent(this.password_ref.current.value)
|
||||||
|
+API_VERSION_PARAM(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
excluded_scopes: [],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(get_json)
|
||||||
|
.then((json)=>{
|
||||||
|
if(json.code!==0) {
|
||||||
|
if(json.msg) throw new Error(json.msg);
|
||||||
|
throw new Error(JSON.stringify(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
set_token(json.user_token);
|
||||||
|
alert(`登录成功`);
|
||||||
|
this.setState({
|
||||||
|
loading_status: 'done',
|
||||||
|
});
|
||||||
|
this.props.on_close();
|
||||||
|
})
|
||||||
|
.catch((e)=>{
|
||||||
|
console.error(e);
|
||||||
|
alert('登录失败\n'+e);
|
||||||
|
this.setState({
|
||||||
|
loading_status: 'done',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
do_input_token(set_token) {
|
||||||
|
if(this.state.loading_status==='loading')
|
||||||
|
return;
|
||||||
|
|
||||||
|
let token=this.input_token_ref.current.value;
|
||||||
|
this.setState({
|
||||||
|
loading_status: 'loading',
|
||||||
|
},()=>{
|
||||||
|
fetch(THUHOLE_API_ROOT+'api_xmcp/hole/system_msg?user_token='+encodeURIComponent(token)+API_VERSION_PARAM())
|
||||||
|
.then((res)=>res.json())
|
||||||
|
.then((json)=>{
|
||||||
|
if(json.error)
|
||||||
|
throw new Error(json.error);
|
||||||
|
if(json.result.length===0)
|
||||||
|
throw new Error('result check failed');
|
||||||
|
this.setState({
|
||||||
|
loading_status: 'done',
|
||||||
|
});
|
||||||
|
set_token(token);
|
||||||
|
this.props.on_close();
|
||||||
|
})
|
||||||
|
.catch((e)=>{
|
||||||
|
alert('Token检验失败\n'+e);
|
||||||
|
this.setState({
|
||||||
|
loading_status: 'done',
|
||||||
|
});
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// perm_alert() {
|
||||||
|
// alert('如果你不需要 PKU Helper 的某项功能,可以取消相应权限。\n其中【状态信息】包括你的网费、校园卡余额等。\n该设置应用到你的【所有】设备,取消后如需再次启用相应功能需要重新登录。');
|
||||||
|
// }
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// let PERM_SCOPES=[
|
||||||
|
// ['score','成绩查询'],
|
||||||
|
// ['syllabus','课表查询'],
|
||||||
|
// ['my_info','状态信息'],
|
||||||
|
// ];
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<GoogleReCaptchaProvider reCaptchaKey={"6Leq0a0ZAAAAAHEStocsqtJfKEs9APB0LdgzTNfZ"} useRecaptchaNet={true}>
|
||||||
|
<GoogleReCaptcha onVerify={(token) => {
|
||||||
|
this.setState({
|
||||||
|
recaptcha_verified: true,
|
||||||
|
});
|
||||||
|
localStorage["recaptcha"] = token
|
||||||
|
}} />
|
||||||
|
<div>
|
||||||
|
<div className="thuhole-login-popup-shadow" />
|
||||||
|
<div className="thuhole-login-popup">
|
||||||
|
<p>
|
||||||
|
<b>接收验证码来登录 T大树洞</b>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
邮箱
|
||||||
|
<input ref={this.username_ref} type="email" autoFocus={true} defaultValue="@mails.tsinghua.edu.cn" />
|
||||||
|
</label>
|
||||||
|
<span className="thuhole-login-type">
|
||||||
|
{/*<a onClick={(e)=>this.do_sendcode('sms')}>*/}
|
||||||
|
{/* 短信 */}
|
||||||
|
{/*</a>*/}
|
||||||
|
{/*/*/}
|
||||||
|
<a onClick={(e)=>this.do_sendcode('mail')}>
|
||||||
|
发送邮件
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
验证码
|
||||||
|
<input ref={this.password_ref} type="tel" />
|
||||||
|
</label>
|
||||||
|
<button type="button" disabled={this.state.loading_status==='loading'}
|
||||||
|
onClick={(e)=>this.do_login(this.props.token_callback)}>
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
<b>从其他设备导入登录状态</b>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<input ref={this.input_token_ref} placeholder="User Token" />
|
||||||
|
<button type="button" disabled={this.state.loading_status==='loading'}
|
||||||
|
onClick={(e)=>this.do_input_token(this.props.token_callback)}>
|
||||||
|
导入
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<hr />
|
||||||
|
<p style={{fontSize:11}}>
|
||||||
|
This site is protected by reCAPTCHA and the Google <a
|
||||||
|
href="https://policies.google.com/privacy">Privacy Policy</a> and <a
|
||||||
|
href="https://policies.google.com/terms">Terms of Service</a> apply.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button onClick={this.props.on_close}>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GoogleReCaptchaProvider>,
|
||||||
|
this.popup_anchor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoginPopup extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state={
|
||||||
|
popup_show: false,
|
||||||
|
};
|
||||||
|
this.on_popup_bound=this.on_popup.bind(this);
|
||||||
|
this.on_close_bound=this.on_close.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
on_popup() {
|
||||||
|
this.setState({
|
||||||
|
popup_show: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
on_close() {
|
||||||
|
this.setState({
|
||||||
|
popup_show: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.props.children(this.on_popup_bound)}
|
||||||
|
{this.state.popup_show &&
|
||||||
|
<LoginPopupSelf token_callback={this.props.token_callback} on_close={this.on_close_bound} />
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||