forked from newthuhole/hole_thu_frontend
add login and attention
This commit is contained in:
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