You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
890 lines
25 KiB
890 lines
25 KiB
import React, { Component } from 'react'; |
|
import { |
|
get_api_base, |
|
get_api_base_2, |
|
STORAGE_BASE, |
|
SafeTextarea, |
|
PromotionBar, |
|
HighlightedMarkdown, |
|
check_service_work_update, |
|
} from './Common'; |
|
import { MessageViewer } from './Message'; |
|
import { LoginPopup } from './infrastructure/widgets'; |
|
import { ColorPicker } from './color_picker'; |
|
import { ConfigUI } from './Config'; |
|
import copy from 'copy-to-clipboard'; |
|
import { get_json } from './flows_api'; |
|
import { save_attentions } from './Attention'; |
|
|
|
import './UserAction.css'; |
|
|
|
const REPOSITORY = 'https://git.thu.monster/newthuhole/'; |
|
const EMAIL = '[email protected]'; |
|
|
|
export const TokenCtx = React.createContext({ |
|
value: null, |
|
set_value: () => {}, |
|
}); |
|
|
|
export function InfoSidebar(props) { |
|
return ( |
|
<div> |
|
<PromotionBar /> |
|
<LoginForm show_sidebar={props.show_sidebar} /> |
|
<div className="box list-menu"> |
|
<a href="/about.html" target="_blank"> |
|
<span className="icon icon-about" /> |
|
<label>关于</label> |
|
</a> |
|
|
|
<a |
|
href="###" |
|
onClick={() => { |
|
props.show_sidebar('设置', <ConfigUI />); |
|
}} |
|
> |
|
<span className="icon icon-settings" /> |
|
<label>本地设置</label> |
|
</a> |
|
|
|
<a href="/policy.html" target="_blank"> |
|
<span className="icon icon-textfile" /> |
|
<label>树洞规范(试行)</label> |
|
</a> |
|
<p> |
|
<em>强烈建议开始使用前先看一遍所有设置选项</em> |
|
</p> |
|
</div> |
|
<div className="box help-desc-box"> |
|
<p> |
|
<a |
|
href="###" |
|
onClick={() => { |
|
check_service_work_update(true); |
|
}} |
|
> |
|
立即更新 |
|
</a> |
|
(当前版本:【{process.env.REACT_APP_BUILD_INFO || '---'}{' '} |
|
{process.env.NODE_ENV}】 会自动在后台检查更新并在下次访问时更新) |
|
</p> |
|
</div> |
|
<div className="box help-desc-box"> |
|
<p>意见反馈请加tag #意见反馈 或到github后端的issue区。</p> |
|
<p> |
|
新T树洞强烈期待有其他更多树洞的出现,一起分布式互联,构建清华树洞族。详情见 |
|
关于 中的描述。 |
|
</p> |
|
<p> |
|
联系我们:<a href={'mailto:' + EMAIL}>{EMAIL}</a> 。 |
|
</p> |
|
</div> |
|
<div className="box help-desc-box"> |
|
<p> |
|
新T树洞 网页版 by @hole_thu,基于 |
|
<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank"> |
|
AGPLv3 |
|
</a> |
|
协议在{' '} |
|
<a href={REPOSITORY} target="_blank"> |
|
Gitea |
|
</a>{' '} |
|
开源。 |
|
</p> |
|
<p> |
|
新T树洞 网页版基于 |
|
<a |
|
href="https://github.com/pkuhelper-web/webhole" |
|
target="_blank" |
|
rel="noopener" |
|
> |
|
P大树洞网页版 by @xmcp |
|
</a> |
|
、 |
|
<a |
|
href="https://github.com/thuhole/webhole" |
|
target="_blank" |
|
rel="noopener" |
|
> |
|
T大树洞网页版 by @thuhole |
|
</a> |
|
、{' '} |
|
<a href="https://reactjs.org/" target="_blank" rel="noopener"> |
|
React |
|
</a> |
|
、 |
|
<a href="https://icomoon.io/#icons" target="_blank" rel="noopener"> |
|
IcoMoon |
|
</a> |
|
等开源项目。 |
|
</p> |
|
<hr /> |
|
<p> |
|
新T树洞 后端 by @hole_thu,基于 |
|
<a href="http://www.wtfpl.net/about/" target="_blank"> |
|
WTFPLv2 |
|
</a> |
|
协议在{' '} |
|
<a href={REPOSITORY} target="_blank"> |
|
Gitea |
|
</a>{' '} |
|
开源。 |
|
</p> |
|
</div> |
|
</div> |
|
); |
|
} |
|
|
|
export class LoginForm extends Component { |
|
constructor(props) { |
|
super(props); |
|
this.state = { |
|
custom_title: window.TITLE || '', |
|
auto_block_rank: window.AUTO_BLCOK || 2, |
|
is_admin: window.IS_ADMIN, |
|
}; |
|
} |
|
|
|
update_title(title, token) { |
|
if (title === window.TITLE) { |
|
alert('无变化'); |
|
return; |
|
} |
|
let data = new FormData(); |
|
data.append('title', title); |
|
data.append('secret', window.TITLE_SECRET); |
|
fetch(get_api_base_2() + '/set-title', { |
|
method: 'POST', |
|
headers: { 'User-Token': token }, |
|
body: data, |
|
}) |
|
.then(get_json) |
|
.then((j) => { |
|
if (j.code !== 0) { |
|
throw new Error(j.msg); |
|
} |
|
window.TITLE = title; |
|
window.TITLE_SECRET = j.data; |
|
localStorage['TITLE_SECRET'] = j.data; |
|
alert('专属头衔设置成功'); |
|
window.IS_ADMIN = false; |
|
window.IS_CANDIDATE = false; |
|
this.setState({ is_admin: false }); |
|
}) |
|
.catch((err) => alert('设置头衔出错了:\n' + err)); |
|
} |
|
|
|
update_auto_block(rank, token) { |
|
let data = new FormData(); |
|
data.append('rank', rank); |
|
fetch(get_api_base() + '/auto_block', { |
|
method: 'POST', |
|
headers: { 'User-Token': token }, |
|
body: data, |
|
}) |
|
.then(get_json) |
|
.then((j) => { |
|
if (j.code !== 0) { |
|
throw new Error(j.msg); |
|
} |
|
window.AUTO_BLCOK = rank; |
|
alert('设置自动拉黑阈值成功'); |
|
}) |
|
.catch((err) => alert('设置自动拉黑出错了:\n' + err)); |
|
} |
|
|
|
copy_token(token) { |
|
if (copy(token)) alert('复制成功!\n请一定不要泄露哦'); |
|
} |
|
|
|
render() { |
|
return ( |
|
<TokenCtx.Consumer> |
|
{(token) => ( |
|
<div> |
|
{/*{!!token.value &&*/} |
|
{/* <LifeInfoBox token={token.value} set_token={token.set_value} />*/} |
|
{/*}*/} |
|
<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> |
|
<a |
|
href="###" |
|
onClick={() => { |
|
this.props.show_sidebar( |
|
'系统日志', |
|
<MessageViewer token={token.value} />, |
|
); |
|
}} |
|
> |
|
查看系统日志 |
|
</a> |
|
<br /> |
|
举报记录、管理日志等都是公开的。 |
|
</p> |
|
<p> |
|
<a |
|
href="###" |
|
onClick={this.copy_token.bind(this, token.value)} |
|
> |
|
复制 User Token |
|
</a> |
|
<br /> |
|
User |
|
Token仅用于开发bot,切勿告知他人。若怀疑被盗号请刷新Token(刷新功能即将上线)。 |
|
</p> |
|
<p> |
|
{this.state.is_admin && ( |
|
<span className="role-card">你是管理员</span> |
|
)} |
|
<br /> |
|
专属头衔: |
|
<input |
|
value={this.state.custom_title} |
|
onChange={(e) => { |
|
this.setState({ custom_title: e.target.value }); |
|
}} |
|
maxLength={10} |
|
/> |
|
<button |
|
className="update-title-btn" |
|
type="button" |
|
onClick={(e) => { |
|
this.update_title(this.state.custom_title, token.value); |
|
}} |
|
disabled={!this.state.custom_title} |
|
> |
|
提交 |
|
</button> |
|
<br /> |
|
设置专属头衔后,可在发言时选择使用。重置后需重新设置。临时用户如需保持头衔请使用相同后缀。 |
|
</p> |
|
<p> |
|
自动拉黑阈值: |
|
<span style={{ display: 'inline-block', width: '3rem' }}> |
|
<b>{this.state.auto_block_rank * 5}</b> |
|
</span> |
|
<input |
|
value={this.state.auto_block_rank} |
|
type="range" |
|
min="1" |
|
max="10" |
|
list="autoBlock" |
|
onChange={(e) => { |
|
this.setState({ auto_block_rank: e.target.value }); |
|
}} |
|
/> |
|
<button |
|
className="update-title-btn" |
|
type="button" |
|
onClick={(e) => { |
|
this.update_auto_block( |
|
this.state.auto_block_rank, |
|
token.value, |
|
); |
|
}} |
|
> |
|
提交 |
|
</button> |
|
<datalist id="autoBlock"> |
|
<option>1</option> |
|
<option>2</option> |
|
<option>3</option> |
|
</datalist> |
|
<br /> |
|
自动不展示被拉黑次数较多用户发布的内容,包括自己。对每个洞及其评论的可见性会有1小时缓存,频繁修改无效。 |
|
</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> |
|
新T树洞 |
|
面向T大学生,通过已验证身份的第三方服务授权登陆。 |
|
</small> |
|
</p> |
|
</div> |
|
)} |
|
</LoginPopup> |
|
)} |
|
</div> |
|
</div> |
|
)} |
|
</TokenCtx.Consumer> |
|
); |
|
} |
|
} |
|
|
|
export class ReplyForm extends Component { |
|
constructor(props) { |
|
super(props); |
|
this.state = { |
|
text: '', |
|
loading_status: 'done', |
|
preview: false, |
|
use_title: false, |
|
}; |
|
this.on_change_bound = this.on_change.bind(this); |
|
this.on_use_title_change_bound = this.on_use_title_change.bind(this); |
|
this.area_ref = this.props.area_ref || React.createRef(); |
|
this.global_keypress_handler_bound = |
|
this.global_keypress_handler.bind(this); |
|
this.color_picker = new ColorPicker(); |
|
this.forced_use_title = window.IS_ADMIN || window.IS_CANDIDATE; |
|
} |
|
|
|
global_keypress_handler(e) { |
|
if ( |
|
e.code === 'Enter' && |
|
!e.ctrlKey && |
|
!e.altKey && |
|
['input', 'textarea'].indexOf(e.target.tagName.toLowerCase()) === -1 |
|
) { |
|
if (this.area_ref.current) { |
|
e.preventDefault(); |
|
this.area_ref.current.focus(); |
|
} |
|
} |
|
} |
|
componentDidMount() { |
|
document.addEventListener('keypress', this.global_keypress_handler_bound); |
|
} |
|
componentWillUnmount() { |
|
document.removeEventListener( |
|
'keypress', |
|
this.global_keypress_handler_bound, |
|
); |
|
} |
|
|
|
on_change(value) { |
|
this.setState({ |
|
text: value, |
|
}); |
|
} |
|
|
|
on_use_title_change(event) { |
|
this.setState({ |
|
use_title: event.target.checked, |
|
}); |
|
} |
|
|
|
on_submit(event) { |
|
if (event) event.preventDefault(); |
|
if (this.state.loading_status === 'loading') return; |
|
if (!this.state.text) return; |
|
this.setState({ |
|
loading_status: 'loading', |
|
}); |
|
|
|
const { pid } = this.props; |
|
const { text, use_title } = this.state; |
|
let data = new URLSearchParams({ |
|
text: text, |
|
use_title: use_title ? '1' : '', |
|
}); |
|
fetch(`${get_api_base_2()}/post/${pid}/comment`, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/x-www-form-urlencoded', |
|
'User-Token': this.props.token, |
|
}, |
|
body: data, |
|
}) |
|
.then(get_json) |
|
.then((json) => { |
|
if (json.code !== 0) { |
|
throw new Error(json.msg); |
|
} |
|
|
|
let saved_attentions = window.saved_attentions; |
|
if (!saved_attentions.includes(pid)) { |
|
saved_attentions.unshift(pid); |
|
window.saved_attentions = saved_attentions; |
|
save_attentions(); |
|
} |
|
|
|
this.setState( |
|
{ |
|
loading_status: 'done', |
|
text: '', |
|
preview: false, |
|
}, |
|
() => { |
|
this.area_ref.current.clear_and_then(this.props.on_complete); |
|
}, |
|
); |
|
}) |
|
.catch((e) => { |
|
console.error(e); |
|
alert('回复失败\n' + e); |
|
this.setState({ |
|
loading_status: 'done', |
|
}); |
|
}); |
|
} |
|
|
|
toggle_preview() { |
|
this.setState({ |
|
preview: !this.state.preview, |
|
}); |
|
} |
|
|
|
render() { |
|
return ( |
|
<form |
|
onSubmit={this.on_submit.bind(this)} |
|
className={'reply-form box' + (this.state.text ? ' reply-sticky' : '')} |
|
> |
|
{this.state.preview ? ( |
|
<div className="reply-preview"> |
|
<HighlightedMarkdown |
|
text={this.state.text} |
|
color_picker={this.color_picker} |
|
show_pid={() => {}} |
|
/> |
|
</div> |
|
) : ( |
|
<SafeTextarea |
|
ref={this.area_ref} |
|
id={this.props.pid} |
|
on_change={this.on_change_bound} |
|
on_submit={this.on_submit.bind(this)} |
|
/> |
|
)} |
|
<div className="reply-form-opt-box"> |
|
<div className="reply-form-buttons"> |
|
<button |
|
type="button" |
|
onClick={() => { |
|
this.toggle_preview(); |
|
}} |
|
> |
|
{this.state.preview ? ( |
|
<span className="icon icon-eye-blocked" /> |
|
) : ( |
|
<span className="icon icon-eye" /> |
|
)} |
|
</button> |
|
{this.state.loading_status === 'loading' ? ( |
|
<button disabled="disabled"> |
|
<span className="icon icon-loading" /> |
|
</button> |
|
) : ( |
|
<button type="submit"> |
|
<span className="icon icon-send" /> |
|
</button> |
|
)} |
|
</div> |
|
{window.TITLE && ( |
|
<label> |
|
<input |
|
type="checkbox" |
|
onChange={this.on_use_title_change_bound} |
|
checked={this.forced_use_title || this.state.use_title} |
|
disabled={this.forced_use_title} |
|
/>{' '} |
|
使用头衔 |
|
</label> |
|
)} |
|
</div> |
|
</form> |
|
); |
|
} |
|
} |
|
|
|
export class PostForm extends Component { |
|
constructor(props) { |
|
super(props); |
|
this.state = { |
|
text: '', |
|
upload_progress_text: '', |
|
is_loading: false, |
|
file_name: '', |
|
file_type: '', |
|
cw: window.CW_BACKUP || '', |
|
allow_search: window.AS_BACKUP || false, |
|
loading_status: 'done', |
|
preview: false, |
|
has_poll: !!window.POLL_BACKUP, |
|
poll_options: JSON.parse(window.POLL_BACKUP || '[""]'), |
|
use_title: false, |
|
}; |
|
this.area_ref = React.createRef(); |
|
this.on_change_bound = this.on_change.bind(this); |
|
this.on_allow_search_change_bound = this.on_allow_search_change.bind(this); |
|
this.on_use_title_change_bound = this.on_use_title_change.bind(this); |
|
this.on_cw_change_bound = this.on_cw_change.bind(this); |
|
this.on_poll_option_change_bound = this.on_poll_option_change.bind(this); |
|
this.color_picker = new ColorPicker(); |
|
this.forced_use_title = window.IS_ADMIN || window.IS_CANDIDATE; |
|
} |
|
|
|
componentDidMount() { |
|
if (this.area_ref.current) this.area_ref.current.focus(); |
|
} |
|
|
|
componentWillUnmount() { |
|
const { cw, allow_search, has_poll, poll_options } = this.state; |
|
window.CW_BACKUP = cw; |
|
window.AS_BACKUP = allow_search; |
|
localStorage['DEFAULT_ALLOW_SEARCH'] = allow_search ? '1' : ''; |
|
window.POLL_BACKUP = has_poll ? JSON.stringify(poll_options) : null; |
|
} |
|
|
|
on_allow_search_change(event) { |
|
this.setState({ |
|
allow_search: event.target.checked, |
|
}); |
|
} |
|
|
|
on_use_title_change(event) { |
|
this.setState({ |
|
use_title: event.target.checked, |
|
}); |
|
} |
|
|
|
on_cw_change(event) { |
|
this.setState({ |
|
cw: event.target.value, |
|
}); |
|
} |
|
|
|
on_change(value) { |
|
this.setState({ |
|
text: value, |
|
}); |
|
} |
|
|
|
do_post() { |
|
const { cw, text, allow_search, use_title, has_poll, poll_options } = |
|
this.state; |
|
let data = new URLSearchParams({ |
|
cw: cw, |
|
text: text, |
|
allow_search: allow_search ? '1' : '', |
|
use_title: use_title ? '1' : '', |
|
room_id: window.config.room, |
|
}); |
|
if (has_poll) { |
|
poll_options.forEach((opt) => { |
|
if (opt) data.append('poll_options', opt); |
|
}); |
|
} |
|
|
|
fetch(get_api_base() + '/dopost', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/x-www-form-urlencoded', |
|
'User-Token': this.props.token, |
|
}, |
|
body: data, |
|
}) |
|
.then(get_json) |
|
.then((json) => { |
|
if (json.code !== 0) { |
|
throw new Error(json.msg); |
|
} |
|
|
|
this.setState( |
|
{ |
|
loading_status: 'done', |
|
text: '', |
|
preview: false, |
|
cw: '', |
|
has_poll: false, |
|
}, |
|
() => { |
|
this.area_ref.current.clear_and_then(this.props.on_complete); |
|
}, |
|
); |
|
}) |
|
.catch((e) => { |
|
console.error(e); |
|
alert('发表失败\n' + e); |
|
this.setState({ |
|
loading_status: 'done', |
|
}); |
|
}); |
|
} |
|
|
|
on_submit(event) { |
|
if (event) event.preventDefault(); |
|
if (this.state.loading_status === 'loading') return; |
|
if (!this.state.text) return; |
|
this.setState({ |
|
loading_status: 'loading', |
|
}); |
|
this.do_post(); |
|
} |
|
|
|
toggle_preview() { |
|
this.setState({ |
|
preview: !this.state.preview, |
|
}); |
|
} |
|
|
|
on_poll_option_change(event, idx) { |
|
let poll_options = this.state.poll_options; |
|
let text = event.target.value; |
|
poll_options[idx] = text; |
|
if (!text && poll_options.length > 1) { |
|
poll_options.splice(idx, 1); |
|
} |
|
if (poll_options[poll_options.length - 1] && poll_options.length < 8) { |
|
poll_options.push(''); |
|
} |
|
this.setState({ poll_options: poll_options }); |
|
} |
|
|
|
on_file_change(event) { |
|
console.log(event); |
|
let f = event.target.files[0]; |
|
if (f) { |
|
console.log(f); |
|
this.setState({ is_loading: true, file_name: f.name, file_type: f.type }); |
|
// let data = new FormData(); |
|
// data.append('file', f); |
|
|
|
var xh = new XMLHttpRequest(); |
|
xh.upload.addEventListener( |
|
'progress', |
|
this.upload_progress.bind(this), |
|
false, |
|
); |
|
xh.addEventListener('load', this.upload_complete.bind(this), false); |
|
xh.addEventListener('error', this.upload_error.bind(this), false); |
|
xh.addEventListener('abort', this.upload_abort.bind(this), false); |
|
xh.open('POST', get_api_base_2() + '/upload'); |
|
xh.setRequestHeader('User-Token', this.props.token); |
|
xh.send(f); |
|
} |
|
} |
|
|
|
update_text_after_upload(data) { |
|
const { file_name, file_type } = this.state; |
|
let url = `${STORAGE_BASE}${data.path}?filename=${encodeURIComponent( |
|
file_name, |
|
)}&filetype=${encodeURIComponent(file_type)}`; |
|
let new_text = |
|
this.state.text + |
|
'\n' + |
|
(file_type.startsWith('image/') ? `` : url); |
|
this.setState({ text: new_text }); |
|
this.area_ref.current.set(new_text); |
|
} |
|
|
|
upload_progress(event) { |
|
console.log(event.loaded, event.total); |
|
this.setState({ |
|
upload_progress_text: `${((event.loaded * 100) / event.total).toFixed( |
|
2, |
|
)}%`, |
|
}); |
|
} |
|
|
|
upload_complete(event) { |
|
try { |
|
let j = JSON.parse(event.target.responseText); |
|
if (j.code !== 0) { |
|
alert(j.msg); |
|
throw new Error(); |
|
} |
|
this.update_text_after_upload(j.data); |
|
this.setState({ is_loading: false }); |
|
} catch (e) { |
|
console.log(e); |
|
this.upload_error(event); |
|
} |
|
} |
|
|
|
upload_error(event) { |
|
alert( |
|
'上传失败\n' + |
|
(event.target.responseText.length < 100 |
|
? event.target.responseText |
|
: event.target.status), |
|
); |
|
this.setState({ is_loading: false }); |
|
} |
|
|
|
upload_abort(event) { |
|
alert('上传已中断'); |
|
this.setState({ is_loading: false }); |
|
} |
|
|
|
render() { |
|
const { |
|
has_poll, |
|
poll_options, |
|
preview, |
|
loading_status, |
|
upload_progress_text, |
|
is_loading, |
|
file_name, |
|
} = this.state; |
|
return ( |
|
<form onSubmit={this.on_submit.bind(this)} className="post-form box"> |
|
<div className="post-form-bar"> |
|
<div> |
|
{preview ? ( |
|
<button |
|
type="button" |
|
onClick={() => { |
|
this.toggle_preview(); |
|
}} |
|
> |
|
<span className="icon icon-eye-blocked" /> |
|
编辑 |
|
</button> |
|
) : ( |
|
<button |
|
type="button" |
|
onClick={() => { |
|
this.toggle_preview(); |
|
}} |
|
> |
|
<span className="icon icon-eye" /> |
|
预览 |
|
</button> |
|
)} |
|
|
|
{loading_status !== 'done' ? ( |
|
<button disabled="disabled"> |
|
<span className="icon icon-loading" /> |
|
上传 |
|
</button> |
|
) : ( |
|
<button type="submit"> |
|
<span className="icon icon-send" /> |
|
发表 |
|
</button> |
|
)} |
|
</div> |
|
<div className="checkbox-bar"> |
|
<label> |
|
<input |
|
type="checkbox" |
|
onChange={this.on_allow_search_change_bound} |
|
checked={this.state.allow_search} |
|
/>{' '} |
|
允许搜索 |
|
</label> |
|
{window.TITLE && ( |
|
<label> |
|
<input |
|
type="checkbox" |
|
onChange={this.on_use_title_change_bound} |
|
checked={this.forced_use_title || this.state.use_title} |
|
disabled={this.forced_use_title} |
|
/>{' '} |
|
使用头衔 |
|
</label> |
|
)} |
|
</div> |
|
</div> |
|
{preview ? ( |
|
<div className="post-preview"> |
|
<HighlightedMarkdown |
|
text={this.state.text} |
|
color_picker={this.color_picker} |
|
show_pid={() => {}} |
|
/> |
|
</div> |
|
) : ( |
|
<> |
|
<span>上传并插入文件: </span> |
|
<input |
|
className="file-input" |
|
type="file" |
|
name="file" |
|
onChange={this.on_file_change.bind(this)} |
|
disabled={is_loading} |
|
/> |
|
{is_loading && ( |
|
<p> |
|
<small> |
|
上传 <i>{file_name}</i> 中: {upload_progress_text} |
|
</small> |
|
</p> |
|
)} |
|
<input |
|
type="text" |
|
placeholder="折叠警告(留空表示不折叠)" |
|
value={this.state.cw} |
|
id="post_cw" |
|
className="spoiler-input" |
|
onChange={this.on_cw_change_bound} |
|
maxLength="32" |
|
/> |
|
<SafeTextarea |
|
ref={this.area_ref} |
|
id="new_post" |
|
on_change={this.on_change_bound} |
|
on_submit={this.on_submit.bind(this)} |
|
/> |
|
</> |
|
)} |
|
<button |
|
type="button" |
|
onClick={() => { |
|
this.setState({ has_poll: !has_poll }); |
|
}} |
|
> |
|
{has_poll ? '取消' : '添加'}投票 |
|
</button> |
|
|
|
{has_poll && ( |
|
<div className="post-form-poll-options"> |
|
<h6>投票选项</h6> |
|
{poll_options.map((option, idx) => ( |
|
<input |
|
key={idx} |
|
type="text" |
|
value={option} |
|
placeholder="输入以添加选项..." |
|
onChange={(e) => this.on_poll_option_change_bound(e, idx)} |
|
maxLength="32" |
|
/> |
|
))} |
|
</div> |
|
)} |
|
<br /> |
|
<br /> |
|
<br /> |
|
<p> |
|
<small> |
|
请遵守 |
|
<a href="/policy.html" target="_blank"> |
|
树洞管理规范(试行) |
|
</a> |
|
和所在分区的规定,文明发言。 |
|
</small> |
|
</p> |
|
<p> |
|
<small>上传文件限制200M,保留至少一个月。也可使用第三方图床。</small> |
|
</p> |
|
</form> |
|
); |
|
} |
|
}
|
|
|