react:16.13.1 antd:3.26.17
效果图
源码
// SearchBox/index.jsx
/**
* @desc 自定义带历史记录的搜索框
* @author darcrand
* @date 2020-09-07
*/
import React, { Component } from "react"
import { Input, Button } from "antd"
import styles from "./styles.module.less"
const ADD_DELAY = 500
export default class SearchBox extends Component {
/**
* @prop {string?} value 绑定的值,如果传入值,则视为完成受控
* @prop {string[]?} defaultOptions 默认历史搜索记录
* @prop {function?} onChange 输入内容|选择历史记录项时触发
* @prop {function?} onSearch 按下回车|点击搜索按钮|选择历史记录项时触发
* @prop {function?} onOptionsUpdate 历史记录列表更新时触发
*
* @prop {number?} maxLength 文本框最大内容长度
* @prop {string?} placeholder 提示文本
* @prop {boolean?} allowClear 是否添加清空按钮
*
* @prop {string?} width 容器宽度
* @prop {number?} zIndex 历史记录下拉组件的层级
* @prop {number?} maxOptionsLength 最大历史记录数量
* @prop {boolean?} loading 是否在加载中(防止高频操作)
*/
static defaultProps = {
value: undefined,
defaultOptions: [],
onChange: (value = "") => {},
onSearch: (value = "") => {},
onOptionsUpdate: (options = []) => {},
maxLength: 30,
placeholder: "输入关键字",
allowClear: false,
width: "300px",
zIndex: 10,
maxOptionsLength: 10,
loading: false,
}
state = {
controlled: this.props.value !== undefined && typeof this.props.value === "string",
value: "",
options: this.props.defaultOptions.slice(),
visibleOptions: false,
}
refInput = React.createRef()
onInputChange = (event = {}) => {
const value = event.target.value
if (this.state.controlled) {
this.props.onChange && this.props.onChange(value)
} else {
this.setState({ value })
}
}
onHitEnter = (event = {}) => {
const { keyCode, which } = event
if (keyCode === 13 || which === 13) {
this.addOptions(this.useValue)
this.props.onSearch && this.props.onSearch(this.useValue)
this.refInput.current.blur()
}
}
onSearchButtonClick = () => {
this.addOptions(this.useValue)
if (this.props.onSearch) {
this.props.onSearch(this.useValue)
}
}
addOptions = (value = "") => {
if (!value.trim()) {
return
}
let { options = [] } = this.state
const included = options.some((v) => v === value)
if (!included) {
const t = setTimeout(() => {
clearTimeout(t)
options.unshift(value)
options = options.slice(0, Math.max(3, this.props.maxOptionsLength))
this.setState({ options }, () => {
if (this.props.onOptionsUpdate) {
this.props.onOptionsUpdate(options)
}
})
}, ADD_DELAY)
}
}
onOptionClick = (value = "") => {
if (this.state.controlled) {
this.props.onChange && this.props.onChange(value)
} else {
this.setState({ value })
}
this.props.onSearch && this.props.onSearch(value)
}
get useValue() {
return this.state.controlled ? this.props.value : this.state.value
}
render() {
const { maxLength, placeholder, allowClear, width, zIndex, loading } = this.props
const { options = [], visibleOptions } = this.state
return (
<div className={styles.container} style={{ zIndex, width }}>
<Input
ref={this.refInput}
className={styles.input}
type="text"
maxLength={maxLength}
placeholder={placeholder}
allowClear={allowClear}
value={this.useValue}
onChange={this.onInputChange}
onKeyUp={this.onHitEnter}
onFocus={() => this.setState({ visibleOptions: true })}
onBlur={() => this.setState({ visibleOptions: false })}
/>
<Button
icon={loading ? "loading" : "search"}
type="primary"
className={styles.search_btn}
onClick={this.onSearchButtonClick}
/>
<div className={loading ? styles.loading_active : styles.loading}></div>
<ul className={options.length > 0 && visibleOptions ? styles.options_visible : styles.options}>
{options.map((opt) => (
<li key={opt} className={styles.option_item} onClick={() => this.onOptionClick(opt)} title={opt}>
{opt}
</li>
))}
</ul>
</div>
)
}
}
// SearchBox/styles.module.less
@item-height: 40px;
.container {
position: relative;
}
.input {
width: 200px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.search_btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.options {
position: absolute;
z-index: inherit;
top: 100%;
left: 0;
list-style: none;
padding-left: 0;
padding-top: 10px;
margin: 0;
max-height: 5 * @item-height + 10px;
background-color: #fff;
box-shadow: 0 4px 4px #dfdfdf;
overflow-y: auto;
transition: all 0.25s ease-in-out 0.2s;
visibility: hidden;
opacity: 0;
}
.options_visible {
&:extend(.options);
visibility: visible;
opacity: 1;
}
.option_item {
padding: 10px;
height: @item-height;
box-sizing: border-box;
cursor: pointer;
color: #333;
transition: all 0.2s;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&:hover {
background-color: #eee;
}
}