Show search intro

This commit is contained in:
2026-06-26 02:05:47 +08:00
parent 351592d437
commit f79d53401a
5 changed files with 144 additions and 8 deletions

View File

@@ -33,11 +33,13 @@ class App extends Component {
sidebar_stack: [[null, null]], // list of [status, content] sidebar_stack: [[null, null]], // list of [status, content]
mode: 'list', // list, single, search, attention mode: 'list', // list, single, search, attention
search_text: null, search_text: null,
show_search_intro: false,
flow_render_key: +new Date(), flow_render_key: +new Date(),
token: localStorage['TOKEN'] || null, token: localStorage['TOKEN'] || null,
}; };
this.show_sidebar_bound = this.show_sidebar.bind(this); this.show_sidebar_bound = this.show_sidebar.bind(this);
this.set_mode_bound = this.set_mode.bind(this); this.set_mode_bound = this.set_mode.bind(this);
this.set_search_intro_bound = this.set_search_intro.bind(this);
this.on_pressure_bound = this.on_pressure.bind(this); this.on_pressure_bound = this.on_pressure.bind(this);
window.BACKEND = window.BACKEND =
@@ -120,10 +122,17 @@ class App extends Component {
this.setState({ this.setState({
mode: mode, mode: mode,
search_text: search_text, search_text: search_text,
show_search_intro: false,
flow_render_key: +new Date(), flow_render_key: +new Date(),
}); });
} }
set_search_intro(show_search_intro) {
this.setState({
show_search_intro: show_search_intro,
});
}
render() { render() {
return ( return (
<TokenCtx.Provider <TokenCtx.Provider
@@ -142,6 +151,7 @@ class App extends Component {
<Title <Title
show_sidebar={this.show_sidebar_bound} show_sidebar={this.show_sidebar_bound}
set_mode={this.set_mode_bound} set_mode={this.set_mode_bound}
set_search_intro={this.set_search_intro_bound}
mode={this.state.mode} mode={this.state.mode}
/> />
<TokenCtx.Consumer> <TokenCtx.Consumer>
@@ -168,6 +178,7 @@ class App extends Component {
show_sidebar={this.show_sidebar_bound} show_sidebar={this.show_sidebar_bound}
mode={this.state.mode} mode={this.state.mode}
search_text={this.state.search_text} search_text={this.state.search_text}
show_search_intro={this.state.show_search_intro}
token={token.value} token={token.value}
/> />
) : ( ) : (

View File

@@ -15,7 +15,9 @@
.root-dark-mode .box { .root-dark-mode .box {
background-color: var(--box-bgcolor-dark); background-color: var(--box-bgcolor-dark);
color: var(--foreground-dark); color: var(--foreground-dark);
box-shadow: 0 0 2px rgba(255, 255, 255, 0.25), 0 0 7px rgba(0, 0, 0, 0.15); box-shadow:
0 0 2px rgba(255, 255, 255, 0.25),
0 0 7px rgba(0, 0, 0, 0.15);
} }
.box-tip { .box-tip {
@@ -92,9 +94,12 @@
left: 10px; left: 10px;
margin-top: 1.5em; margin-top: 1.5em;
color: white; color: white;
text-shadow: /* copied from .black-outline */ -1px -1px 0 rgba(0, 0, 0, 0.6), text-shadow: /* copied from .black-outline */
0 -1px 0 rgba(0, 0, 0, 0.6), 1px -1px 0 rgba(0, 0, 0, 0.6), -1px -1px 0 rgba(0, 0, 0, 0.6),
-1px 1px 0 rgba(0, 0, 0, 0.6), 0 1px 0 rgba(0, 0, 0, 0.6), 0 -1px 0 rgba(0, 0, 0, 0.6),
1px -1px 0 rgba(0, 0, 0, 0.6),
-1px 1px 0 rgba(0, 0, 0, 0.6),
0 1px 0 rgba(0, 0, 0, 0.6),
1px 1px 0 rgba(0, 0, 0, 0.6); 1px 1px 0 rgba(0, 0, 0, 0.6);
font-family: 'Consolas', 'Courier', monospace; font-family: 'Consolas', 'Courier', monospace;
} }
@@ -206,6 +211,10 @@
overflow-y: hidden; overflow-y: hidden;
} }
.left-container .search-intro .box-content {
max-height: none;
}
.box-id { .box-id {
color: #666666; color: #666666;
} }
@@ -430,7 +439,6 @@
stroke: #777; stroke: #777;
} }
.vote-icon-active { .vote-icon-active {
fill: #333; fill: #333;
} }

View File

@@ -18,6 +18,7 @@ import { API, parse_replies } from './flows_api';
import { cache } from './cache'; import { cache } from './cache';
import { save_attentions } from './Attention'; import { save_attentions } from './Attention';
import Poll from 'react-polls'; import Poll from 'react-polls';
import { SEARCH_INTRO_MARKDOWN } from './SearchIntro';
/* /*
const IMAGE_BASE = 'https://thimg.yecdn.com/'; const IMAGE_BASE = 'https://thimg.yecdn.com/';
@@ -1425,6 +1426,26 @@ function Announcement(props) {
); );
} }
function SearchIntro() {
return (
<div className="flow-item-row search-intro">
<div className="flow-item">
<div className="box box-post">
<div className="box-post-main">
<div className="box-header">
<span className="icon icon-about" />
&nbsp;搜索指南
</div>
<div className="box-content">
<HighlightedMarkdown text={SEARCH_INTRO_MARKDOWN} />
</div>
</div>
</div>
</div>
</div>
);
}
export class Flow extends PureComponent { export class Flow extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -1473,7 +1494,8 @@ export class Flow extends PureComponent {
render() { render() {
const { submode, announcement } = this.state; const { submode, announcement } = this.state;
const { mode, show_sidebar, search_text, token } = this.props; const { mode, show_sidebar, search_text, show_search_intro, token } =
this.props;
const submode_names = this.get_submode_names(mode); const submode_names = this.get_submode_names(mode);
return ( return (
<> <>
@@ -1503,6 +1525,7 @@ export class Flow extends PureComponent {
mode={mode} mode={mode}
submode={submode} submode={submode}
search_text={search_text} search_text={search_text}
show_search_intro={show_search_intro}
token={token} token={token}
/> />
</> </>
@@ -1809,9 +1832,11 @@ class SubFlow extends PureComponent {
render() { render() {
const should_deletion_detect = localStorage['DELETION_DETECT'] === 'on'; const should_deletion_detect = localStorage['DELETION_DETECT'] === 'on';
const { mode, chunks, local_attention_text, search_param } = this.state; const { mode, chunks, local_attention_text, search_param } = this.state;
const { submode, show_sidebar } = this.props; const { submode, show_sidebar, show_search_intro } = this.props;
return ( return (
<div className="flow-container"> <div className="flow-container">
{show_search_intro && <SearchIntro />}
{mode === 'attention' && {mode === 'attention' &&
submode === 1 && submode === 1 &&
local_attention_text === null && ( local_attention_text === null && (

37
src/SearchIntro.js Normal file
View File

@@ -0,0 +1,37 @@
export const SEARCH_INTRO_MARKDOWN = `
树洞提供了较为丰富的搜索功能,可以支持复杂的搜索条件。搜索分为两个大类,线上全局搜索,和在已关注的帖子中过滤。
# 线上全局搜索
直接点击搜索框进入全局搜索。有三个子模式Tag搜索全文搜索和头衔。Tag搜索会查询所有匹配的 #hashtag 和折叠警告,包括 #举报 。搜索头衔会查询对应头衔发布的内容。全文搜索会全文查找匹配的搜索词,并支持一些高级语法。
## 全文搜索的基本语法
全文搜索支持一个或多个关键词,多个关键词用空格分隔。支持 AND OR 语法,空格默认视为 OR 。搜索 **电子 直博** 等效于 **电子 OR 直博** 。支持括号,例如 **(贵系 OR 雷系) AND 好工作** 。支持单双引号,例如 **"and me"** **'"鹅腿"'** 。
此外,可以直接使用 + - 符号标记必须包含和必须排除。例如,可以搜索 **+(贵系 雷系) +好工作** 。
## 字段过滤与帖子范围筛选
支持按照字段进行更复杂的筛选包括content comment post_id 三个字段。例如 **content:避雷 AND comment:cy** 。可以使用 IN 语法,例如可以用 **content: IN [微积分 线代]** 替代 **content:微积分 OR content:线代**。
可以使用post_id字段限定帖子范围例如 **+(毕业 出二手 二手) +post_id:[700000 TO \\*]** 。
## 搜索结果的排序
单个关键词的简单搜索会按照帖子从新到旧排序。复杂的搜索会按匹配程度排序,与出现次数、全文长度、搜索词稀有度等有关。主贴和评论分别记分,且主贴的权重更大。可以在单关键词的结尾加空格来改为匹配程度排序。
如需提高某个关键词的权重,可以使用 ^,例如 **微积分^4.0 线代** 会给微积分更高的权重。**+微积分 线代** 则只返回包含微积分的结果,且优先返回同时包含线代的结果。
# 在关注列表中搜索
展示关注列表的情况下,点击搜索框,进入关注列表搜索。
关注列表搜索在本地进行过滤,支持多关键词和正则表达式。请将正则包裹在/ / 中。例如, **/\\d{2,}(万|w)/** 。
关注列表搜索基本只考虑主贴,只有评论极少的洞会也考虑评论。
# 反馈
如果你发现搜索结果不符合预期,例如有应该出现的内容没有出现,请带上 #搜索反馈 发帖反馈。如果你希望涉及你个人信息的内容不要出现在搜索结果中,请使用举报功能。
`;

View File

@@ -12,15 +12,26 @@ class ControlBar extends PureComponent {
super(props); super(props);
this.state = { this.state = {
search_text: '', search_text: '',
search_focused: false,
search_submitted: false,
}; };
this.set_mode = props.set_mode; this.set_mode = props.set_mode;
this.on_change_bound = this.on_change.bind(this); this.on_change_bound = this.on_change.bind(this);
this.on_focus_bound = this.on_focus.bind(this);
this.on_blur_bound = this.on_blur.bind(this);
this.on_keypress_bound = this.on_keypress.bind(this); this.on_keypress_bound = this.on_keypress.bind(this);
this.do_refresh_bound = this.do_refresh.bind(this); this.do_refresh_bound = this.do_refresh.bind(this);
this.do_attention_bound = this.do_attention.bind(this); this.do_attention_bound = this.do_attention.bind(this);
} }
update_search_intro(search_text, search_focused, search_submitted) {
const has_search_text = search_text.trim().length > 0;
this.props.set_search_intro(
!search_submitted && (has_search_text || search_focused),
);
}
componentDidMount() { componentDidMount() {
if (window.location.hash) { if (window.location.hash) {
let text = decodeURIComponent(window.location.hash).substr(1); let text = decodeURIComponent(window.location.hash).substr(1);
@@ -60,13 +71,50 @@ class ControlBar extends PureComponent {
} }
on_change(event) { on_change(event) {
const search_text = event.target.value;
const search_submitted =
search_text.length === 0 ? false : this.state.search_submitted;
this.setState({ this.setState({
search_text: event.target.value, search_text: search_text,
search_submitted: search_submitted,
}); });
this.update_search_intro(
search_text,
this.state.search_focused,
search_submitted,
);
}
on_focus() {
this.setState({
search_focused: true,
});
this.update_search_intro(
this.state.search_text,
true,
this.state.search_submitted,
);
}
on_blur(event) {
if (event.relatedTarget?.closest('.search-intro')) return;
this.setState({
search_focused: false,
});
this.update_search_intro(
this.state.search_text,
false,
this.state.search_submitted,
);
} }
on_keypress(event) { on_keypress(event) {
if (event.key === 'Enter') { if (event.key === 'Enter') {
this.setState({
search_submitted: true,
});
this.props.set_search_intro(false);
let flag_res = flag_re.exec(this.state.search_text); let flag_res = flag_re.exec(this.state.search_text);
if (flag_res) { if (flag_res) {
if (flag_res[2]) { if (flag_res[2]) {
@@ -102,7 +150,9 @@ class ControlBar extends PureComponent {
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState({ this.setState({
search_text: '', search_text: '',
search_submitted: false,
}); });
this.props.set_search_intro(false);
this.set_mode('list', null); this.set_mode('list', null);
window.location.hash = ''; window.location.hash = '';
} }
@@ -111,7 +161,9 @@ class ControlBar extends PureComponent {
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState({ this.setState({
search_text: '', search_text: '',
search_submitted: false,
}); });
this.props.set_search_intro(false);
this.set_mode('attention', null); this.set_mode('attention', null);
} }
@@ -147,6 +199,8 @@ class ControlBar extends PureComponent {
: '关键词 / tag / #树洞号' : '关键词 / tag / #树洞号'
} }
onChange={this.on_change_bound} onChange={this.on_change_bound}
onFocus={this.on_focus_bound}
onBlur={this.on_blur_bound}
onKeyPress={this.on_keypress_bound} onKeyPress={this.on_keypress_bound}
/> />
<a <a
@@ -215,6 +269,7 @@ export function Title(props) {
<ControlBar <ControlBar
show_sidebar={props.show_sidebar} show_sidebar={props.show_sidebar}
set_mode={props.set_mode} set_mode={props.set_mode}
set_search_intro={props.set_search_intro}
mode={props.mode} mode={props.mode}
/> />
</div> </div>