搜索建议组件。
安装组件:
pnpm install highlight-search
import { createMEditor, parseFetchRes2FieldMapping, MappingItem, Suggest } from "highlight-search";
// for vue2
this.suggest=new Suggest({
mapping: mappings,
wordElement: () => {
return document.querySelectorAll('.word')
},
async fetchQuery(params, options) {
const { field, operator } = params
const queryParams = {
field: field.value,
operator: operator,
start_time: new Date("2022-12-01").getTime(),
module: MODULE_NAME,
end_time: new Date("2023-12-02").getTime()
}
try {
// use fetch or axios to got mappingField List
const res = await fetch(`/api/v1/sop/utils/xqlite/suggest/`, {
signal: options?.canalSignal,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(queryParams)
})
const info = await res.json() as {
data: string[]
}
return [{
title: "搜索建议",
mappings: info.data.map((item) => {
return {
name: item,
value: `${item}`,
match: `${item}`
}
}),
render(item: MappingItem) {
return item.name
},
}]
} catch (e) {
console.warn("[meditor],fetchQuery error", e)
return []
}
},
})
// for vue3
const suggest = ref(new Suggest({
// .. same as vue2 config
}))
// 然后创建editor实例
const editor = createMeditor({
// 要挂载到的dom元素,可以直接设置dom元素,或者通过函数返回
target:HTMLElement | ()=>HTMLElement
//将上面的suggest传递过来,保存响应式
// vue2
suggest:this.suggest;
//vue3
suggest:suggest.value,
onSubmit:(value:string)=>{
// 编辑器触发搜索时的回调,value是纯文本字符串,等同于editor.getValue()
// 检索完毕之后,调用suggest.removeCache() 清空检索缓存
}
})
createMeditor
会返回editor实例,在原有TextBus的editor实例的基础上,还挂载了辅助函数。
suggest:搜索建议相关的所有东西都在这里处理。
Suggest对象上可能会用到的一些属性和函数:
名称 | 类型 | 说明 |
---|---|---|
loading | boolean(只读) | 是否正在loading,目前仅在查询接口时会调用。 |
showList | FieldMapping[] (只读) | 应该展示的候选词,已经过滤并且按原分组分割 |
queryTypeByText | (text:string)=>MappingType | 根据传入的单词判断该字符串的类型 |
queryNextType | (text:string)=>MappingType | 根据传入的单词推断下一个字符串的类型 |
parseToFormatter | (text:string)=>Formatter[] | 根据传入的字符串返回解析格式之后的类型 |
removeCache | ()=>void | 清除缓存数据,建议每次提交检索之后都调用一次 |
editor实例上挂载的其他函数
名称 | 参数类型 | 说明 |
---|---|---|
choose | (item:MappingItem) | 用于鼠标手动选中候选词的插入 |
setValue | (str:string) | 手动设置编辑器的内容,会自动解析格式并格式化渲染到编辑器中 |
getValue | - | 获取当前编辑器的内容,返回纯文本字符串 |
Vue2
<template>
<div class="editor-box">
<h3>Highlight-search In Vue2.x</h3>
<div class="tool-line">
<button @click="handleSetValue()">设置默认值</button>
<button @click="handleSearch()">获取输入的语句 </button>
<span>{{ userInputValue }}</span>
<span>loading:{{ suggest?.loading }}</span>
</div>
<div ref="editorRef" id="editor">
</div>
<!-- 以下内容可以任意写,搜索建议的数组是meditor?.suggest.showList,同时,loading和一些辅助函数也已经暴露出来了 -->
<TransitionGroup tag="div" name="fade" class="suggest-list">
<div v-if="suggest?.loading" :key="123">
loading...
</div>
<template v-else-if="suggest?.showList?.length">
<div class="word-box" v-for="(item, index) in suggest?.showList" :key="index">
<div class="word-title">{{ item.title }} </div>
<TransitionGroup tag="div" name="fade" class="words">
<div class="word" v-for="(word, index) in item.mappings" :key="index" :class="{
active: suggest.activeItem?.value === word.value
}" @click="handelChoose(word)">
{{ item?.render?.(word) ?? word.name }}
</div>
</TransitionGroup>
</div>
</template>
<div :key="222" v-else>
暂无搜索建议
</div>
</TransitionGroup>
</div>
</template>
<script>
import { createMEditor, parseFetchRes2FieldMapping, Suggest } from 'highlight-search'
import "highlight-search/index.css"
const MODULE_NAME = "alarm"
export default {
name: 'App',
data() {
return {
userInputValue: '',
meditor: {},
suggest: {}
}
},
mounted() {
this.initData()
},
methods: {
async initData() {
const res = await fetch("/api/v1/sop/utils/xqlite/mappings/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
})
const info = await res.json()
const mappings = parseFetchRes2FieldMapping(info.data, MODULE_NAME)
this.suggest = new Suggest({
mapping: mappings,
wordElement: () => {
return document.querySelectorAll('.word')
},
async fetchQuery(params, options) {
const { field, operator } = params
const queryParams = {
field: field.value,
operator: operator,
start_time: new Date("2022-12-01").getTime(),
module: MODULE_NAME,
end_time: new Date("2023-12-02").getTime()
}
try {
const res = await fetch(`/api/v1/sop/utils/xqlite/suggest/`, {
signal: options?.canalSignal,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(queryParams)
})
const info = await res.json()
return [{
title: "搜索建议",
mappings: info.data.map((item) => {
return {
name: item,
value: `${item}`,
match: `${item}`
}
}),
render(item) {
return item.name
},
}]
} catch (e) {
console.warn("[meditor],fetchQuery error", e)
return []
}
}
})
this.meditor = await createMEditor(
{
target: this.$refs['editorRef'],
suggest: this.suggest,
onSubmit: (value) => {
console.log('[MEditor]:onSubmit,str is"', value + '"')
alert(value)
}
})
},
handelChoose(word) {
this.meditor?.choose(word)
},
handleSetValue() {
const info = "source_ip = 123 and destination_ip_province = 123.2.2.2 or log_name exist"
this.meditor.setValue(info)
},
handleSearch() {
const str = this.meditor?.getValue()
console.log('meditor.value.suggest.showList', this.meditor.suggest.showList)
this.userInputValue = str ?? ''
this.meditor.suggest.removeCache()
}
}
}
</script>
<style lang="scss">
.editor-box {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
overflow: hidden;
}
#editor {
flex-shrink: 0;
text-align: left;
width: 100%;
}
.tool-line {
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: 20px;
button {
margin: 0 20px;
}
}
.suggest-list {
flex: 1 0 0;
overflow-y: auto;
margin-top: 10px;
width: 100%;
display: flex;
flex-direction: column;
text-align: left;
position: relative;
align-items: flex-start;
.word-box {
width: calc(100% - 40px);
padding: 20px;
background-color: #eeeeee;
.word-title {
text-align: left;
font-size: 20px;
font-weight: bold;
flex-shrink: 0;
margin-left: 10px;
}
.words {
display: flex;
flex-wrap: wrap;
.word {
margin: 10px;
padding: 6px 10px;
background-color: #fff;
border-radius: 2px;
cursor: pointer;
&:hover {
background-color: #cccccc;
color: #ffffff;
}
&.active {
background-color: #000000;
color: #ffffff;
}
}
}
}
}
/* 1. 声明过渡效果 */
.fade-move,
.fade-enter-active,
.fade-leave-active {
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
}
/* 2. 声明进入和离开的状态 */
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: scaleY(0.01) translate(30px, 0);
}
/* 3. 确保离开的项目被移除出了布局流
以便正确地计算移动时的动画效果。 */
.fade-leave-active {
position: absolute;
}
</style>
Vue3
<template>
<div class="editor-box">
<h3>highlight-search in Vue3 </h3>
<div class="tool-line">
<button @click="handleSetValue()">设置默认值</button>
<button @click="handleSearch()">获取输入的语句 </button>
<span>{{ userInputValue }}</span>
<span>loading:{{ meditor?.suggest?.loading }}</span>
</div>
<div ref="editorRef" id="editor">
</div>
<!-- 以下内容可以任意写,搜索建议的数组是meditor?.suggest.showList,同时,loading和一些辅助函数也已经暴露出来了 -->
<TransitionGroup tag="div" name="fade" class="suggest-list">
<div v-if="suggest?.loading" :key="123">
loading...
</div>
<div class="word-box" v-else-if="suggest?.showList?.length" v-for="(item, index) in suggest?.showList" :key="index">
<div class="word-title">{{ item.title }} </div>
<TransitionGroup tag="div" name="fade" class="words">
<div class="word" v-for="(word, index) in item.mappings" :key="index" :class="{
active: suggest?.activeItem?.value === word.value
}" @click="handelChoose(word)">
{{ item?.render?.(word) ?? word.name }}
</div>
</TransitionGroup>
</div>
<div :key="222" v-else>
暂无搜索建议
</div>
</TransitionGroup>
</div>
</template>
<script setup lang="ts" name="App">
import { onMounted, ref, watch } from "vue";
import { createMEditor, parseFetchRes2FieldMapping, MappingItem, Suggest } from "highlight-search";
import "highlight-search/index.css";
const editorRef = ref<HTMLElement>()
const meditor = ref()
const suggest = ref()
const userInputValue = ref('')
const MODULE_NAME = "alarm"
const initEditor = async () => {
const res = await fetch("/api/v1/sop/utils/xqlite/mappings/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
})
const info = await res.json()
const mappings = parseFetchRes2FieldMapping(info.data, MODULE_NAME)
suggest.value = new Suggest({
mapping: mappings,
wordElement: () => {
return document.querySelectorAll('.word')
},
async fetchQuery(params, options) {
const { field, operator } = params
const queryParams = {
field: field.value,
operator: operator,
start_time: new Date("2022-12-01").getTime(),
module: MODULE_NAME,
end_time: new Date("2023-12-02").getTime()
}
try {
const res = await fetch(`/api/v1/sop/utils/xqlite/suggest/`, {
signal: options?.canalSignal,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(queryParams)
})
const info = await res.json() as {
data: string[]
}
return [{
title: "搜索建议",
mappings: info.data.map((item) => {
return {
name: item,
value: `${item}`,
match: `${item}`
}
}),
render(item: MappingItem) {
return item.name
},
}]
} catch (e) {
console.warn("[meditor],fetchQuery error", e)
return []
}
},
})
const editor = await createMEditor(
{
target: editorRef.value!,
suggest: suggest.value,
onSubmit: (value: any) => {
alert(value)
}
})
meditor.value = editor
}
watch(() => suggest, (val) => {
console.log('val', val)
}, {
deep: true
})
onMounted(() => {
initEditor()
})
const handelChoose = (word: MappingItem) => {
meditor.value?.choose(word)
}
const handleSetValue = () => {
const info = "source_ip = 123 and destination_ip_province = 123.2.2.2 or log_name exist"
meditor.value?.setValue(info)
}
const handleSearch = () => {
const str = meditor.value?.getValue()
console.log('meditor.value.suggest.showList', meditor.value?.suggest?.showList)
userInputValue.value = str ?? ''
suggest.value.removeCache()
}
</script>
<style scoped lang="scss">
.editor-box {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
overflow: hidden;
}
#editor {
flex-shrink: 0;
text-align: left;
width: 100%;
}
.tool-line {
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: 20px;
button {
margin: 0 20px;
}
}
.suggest-list {
flex: 1 0 0;
overflow-y: auto;
margin-top: 10px;
width: 100%;
display: flex;
flex-direction: column;
text-align: left;
position: relative;
align-items: flex-start;
.word-box {
width: calc(100% - 40px);
padding: 20px;
background-color: #eeeeee;
.word-title {
text-align: left;
font-size: 20px;
font-weight: bold;
flex-shrink: 0;
margin-left: 10px;
}
.words {
display: flex;
flex-wrap: wrap;
.word {
margin: 10px;
padding: 6px 10px;
background-color: #fff;
border-radius: 2px;
cursor: pointer;
&:hover {
background-color: #cccccc;
color: #ffffff;
}
&.active {
background-color: #000000;
color: #ffffff;
}
}
}
}
}
/* 1. 声明过渡效果 */
.fade-move,
.fade-enter-active,
.fade-leave-active {
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
}
/* 2. 声明进入和离开的状态 */
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: scaleY(0.01) translate(30px, 0);
}
/* 3. 确保离开的项目被移除出了布局流
以便正确地计算移动时的动画效果。 */
.fade-leave-active {
position: absolute;
}
</style>
插件并不会自带任何样式,但是预设了一套基本的样式。
你可以引用查看简单的示例
import "highlight-search/index.css"
实际渲染的dom结构是这样的(最理想的情况下,实际上会存在各种随机的嵌套)
<div data-component="root" class="meditor-root">
<span class="meditor-comp-mappingField">source_ip</span>
<span class="meditor-comp-comparator"> =</span>
<span class="meditor-comp-inputValue"> 123</span>
<span class="meditor-comp-logical"> and</span>
<span class="meditor-comp-mappingField"> dest_port</span>
<span class="meditor-comp-comparator"> =</span>
<span class="meditor-comp-inputValue"> 80</span>
</div>
涉及到的class类名
- meditor-root : root元素,所有的内容都在改dom元素下。
- meditor-comp-mappingField :MappingField 对应的class类名。( 其余的同理)