feat: 搜索框(部分内容)

This commit is contained in:
chenjinsong
2022-01-25 17:16:56 +08:00
parent d2d38c2d8c
commit f8d780cba6
13 changed files with 410 additions and 123 deletions

View File

@@ -4,7 +4,6 @@
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'App', name: 'App',
setup () { setup () {

View File

@@ -12,7 +12,7 @@
.search__suffix { .search__suffix {
margin-left: 8px; margin-left: 8px;
.cn-icon-search-advance { .cn-icon-search-advance, .cn-icon-search-normal {
color: #A6AAAE; color: #A6AAAE;
font-size: 18px; font-size: 18px;
} }

View File

@@ -9,6 +9,10 @@
border: 1px solid #CECECE; border: 1px solid #CECECE;
border-radius: 2px; border-radius: 2px;
padding-right: 80px; padding-right: 80px;
pre.CodeMirror-placeholder.CodeMirror-line-like {
color: #ccc;
}
} }
/* PADDING */ /* PADDING */

View File

@@ -1,90 +0,0 @@
<template>
<el-input
class="advanced-search"
:class="{'advanced-search--show-list': showList}"
v-model="searchContentTemp"
@keyup.enter="enter"
>
<template #suffix>
<div class="search__suffixes" v-if="!showList">
<div class="search__suffix">
<i class="cn-icon cn-icon-search-advance"></i>
</div>
<div class="search__suffix" @click="enter">
<i class="el-icon-search"></i>
</div>
</div>
<div class="search__suffixes--show-list" v-else>
<div class="search__suffix">
<i class="cn-icon cn-icon-search-advance"></i>
</div>
<div class="search__suffix" @click="enter">
<i class="el-icon-search"></i>
</div>
</div>
</template>
</el-input>
<!-- <div class="advanced-search"></div>-->
</template>
<script>
import { objToStr, strToObj } from '@/utils/tools'
export default {
name: 'AdvancedSearch',
props: {
showList: {
type: Boolean,
default: true
}
},
data () {
return {
searchContentTemp: '', // 搜索框内的文本内容按回车键后为searchContent赋值
searchContent: '', // 查询语句
searchParams: null // 搜索参数,格式为[{ name: xxx, value: xxx }, ...]
}
},
methods: {
search () {
this.$emit('search', this.searchParams)
},
reset () {
this.searchParams = null
this.searchParams = []
},
addParams (params) {
if (params) {
const newParams = { ...this.searchParams, ...params }
this.searchContentTemp = objToStr(newParams)
this.enter()
}
},
enter () {
if (!this.searchContentTemp) {
this.reset()
} else {
this.searchContent = this.searchContentTemp
}
}
},
watch: {
searchContent (n) {
if (n) {
this.searchParams = strToObj(n)
} else {
this.reset()
}
this.searchContentTemp !== n && (this.searchContentTemp = n)
},
searchParams: {
deep: true,
handler (n) {
if (n) {
// 请求接口获取左侧过滤条件和右侧entity列表
this.search()
}
}
}
}
}
</script>

View File

@@ -4,14 +4,19 @@
> >
<text-mode <text-mode
v-if="searchMode === 'text'" v-if="searchMode === 'text'"
ref="textMode"
:column-list="columnList"
:sql="sql"
@changeMode="changeMode" @changeMode="changeMode"
@search="search" @search="search"
></text-mode> ></text-mode>
<tag-mode <tag-mode
v-if="searchMode === 'tag'" v-if="searchMode === 'tag'"
ref="tagMode"
:column-list="columnList" :column-list="columnList"
:operator-list="showOperatorList" :operator-list="showOperatorList"
:connection-list="showConnectionList" :connection-list="showConnectionList"
:convert-meta-list="metaList"
@changeMode="changeMode" @changeMode="changeMode"
@search="search" @search="search"
></tag-mode> ></tag-mode>
@@ -30,6 +35,12 @@ export default {
TagMode, TagMode,
TextMode TextMode
}, },
data () {
return {
sql: null,
metaList: null
}
},
props: { props: {
// 默认模式tag | text // 默认模式tag | text
defaultMode: String, defaultMode: String,
@@ -49,11 +60,20 @@ export default {
connectionList: Array connectionList: Array
}, },
methods: { methods: {
search () { search (metaList, formatSql) {
this.$emit('search', formatSql)
}, },
changeMode (mode) { changeMode (mode, data) {
this.searchMode = mode this.searchMode = mode
if (mode === 'text') {
this.sql = data
} else if (mode === 'tag') {
this.metaList = data
}
},
addParams (params) {
this.$refs.tagMode && this.$refs.tagMode.addParams(params)
this.$refs.textMode && this.$refs.textMode.addParams(params)
} }
}, },
setup (props) { setup (props) {

View File

@@ -75,7 +75,7 @@
<div class="tag-search__add" @click="addCondition">{{$t('entities.advancedSearch.add')}}</div> <div class="tag-search__add" @click="addCondition">{{$t('entities.advancedSearch.add')}}</div>
<div class="search__suffixes search__suffixes--tag-mode"> <div class="search__suffixes search__suffixes--tag-mode">
<div class="search__suffix" @click="changeMode"> <div class="search__suffix" @click="changeMode">
<i class="cn-icon cn-icon-search-advance"></i> <i class="cn-icon cn-icon-search-normal"></i>
</div> </div>
<div class="search__suffix" @click="search"> <div class="search__suffix" @click="search">
<i class="el-icon-search"></i> <i class="el-icon-search"></i>
@@ -87,12 +87,14 @@
<script> <script>
import Meta, { connection, condition, columnType } from './meta/meta' import Meta, { connection, condition, columnType } from './meta/meta'
import _ from 'lodash' import _ from 'lodash'
import SqlParser, { stringInQuot } from '@/utils/sql-parser'
export default { export default {
name: 'TagMode', name: 'TagMode',
props: { props: {
columnList: Array, columnList: Array,
operatorList: Array, operatorList: Array,
connectionList: Array connectionList: Array,
convertMetaList: Array
}, },
data () { data () {
return { return {
@@ -103,9 +105,9 @@ export default {
}, },
methods: { methods: {
// 新增条件 // 新增条件
addCondition () { addCondition (meta) {
this.metaList.forEach(meta => { this.metaList.forEach(m => {
meta.cancelEditing() m.cancelEditing()
}) })
// 先判断上一个condition是否已填写完整没有则删除之后将当前所有meta的内容的isEditing置为false // 先判断上一个condition是否已填写完整没有则删除之后将当前所有meta的内容的isEditing置为false
if (this.metaList.length > 0) { if (this.metaList.length > 0) {
@@ -118,6 +120,9 @@ export default {
if (!_.isEmpty(this.metaList)) { if (!_.isEmpty(this.metaList)) {
this.addConnection() this.addConnection()
} }
if (meta instanceof Meta) {
this.metaList.push(meta)
} else {
const newCondition = new Meta(condition) const newCondition = new Meta(condition)
newCondition.isEditing = true newCondition.isEditing = true
newCondition.column.isEditing = true newCondition.column.isEditing = true
@@ -126,6 +131,8 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.columnSelect.focus() this.$refs.columnSelect.focus()
}) })
}
console.info(this.metaList)
}, },
addConnection () { addConnection () {
this.metaList.push(new Meta(connection)) this.metaList.push(new Meta(connection))
@@ -216,10 +223,44 @@ export default {
}) })
}, },
search () { search () {
this.$emit('search', this.metaList) const parser = new SqlParser(this.metaList, this.columnList)
const { metaList, formatSql } = parser.formatMetaList()
this.metaList = metaList
this.$emit('search', this.metaList, formatSql)
}, },
changeMode () { changeMode () {
this.$emit('changeMode', 'text') const parser = new SqlParser(this.metaList, this.columnList)
const { metaList, formatSql } = parser.formatMetaList()
this.metaList = metaList
this.$emit('changeMode', 'text', formatSql)
},
addParams (params) {
Object.keys(params).forEach(key => {
const column = this.columnList.find(column => {
return column.name === key
})
const meta = new Meta()
meta.column.name = key
meta.column.type = column ? column.type : columnType.string
meta.column.label = column ? column.label : key
meta.operator.value = '='
meta.operator.show = true
meta.value.value = meta.column.type === columnType.string ? stringInQuot(params[key]) : params[key]
meta.value.show = true
meta.value.label = meta.value.value
this.addCondition(meta)
})
}
},
watch: {
convertMetaList: {
immediate: true,
deep: true,
handler (n) {
if (!_.isEmpty(n)) {
this.metaList = n
}
}
} }
} }
} }

View File

@@ -6,7 +6,7 @@
<div class="search__suffix" @click="changeMode"> <div class="search__suffix" @click="changeMode">
<i class="cn-icon cn-icon-search-advance"></i> <i class="cn-icon cn-icon-search-advance"></i>
</div> </div>
<div class="search__suffix"> <div class="search__suffix" @click="search">
<i class="el-icon-search"></i> <i class="el-icon-search"></i>
</div> </div>
</div> </div>
@@ -16,24 +16,77 @@
import 'codemirror/theme/ambiance.css' import 'codemirror/theme/ambiance.css'
import 'codemirror/addon/hint/show-hint' import 'codemirror/addon/hint/show-hint'
import 'codemirror/addon/hint/show-hint.css' import 'codemirror/addon/hint/show-hint.css'
import 'codemirror/addon/display/placeholder'
import 'codemirror/mode/sql/sql' import 'codemirror/mode/sql/sql'
const CodeMirror = require('codemirror') import SqlParser, { stringInQuot } from '@/utils/sql-parser'
import CodeMirror from 'codemirror'
import { toRaw } from 'vue'
import { columnType } from '@/components/advancedSearch/meta/meta'
export default { export default {
name: 'TextMode', name: 'TextMode',
props: {
columnList: Array,
sql: String
},
data () {
return {
codeMirror: null
}
},
methods: { methods: {
initCodeMirror () { initCodeMirror () {
CodeMirror.fromTextArea(this.$refs.textSearch, { this.codeMirror = CodeMirror.fromTextArea(this.$refs.textSearch, {
mode: { mode: {
name: 'sql' name: 'sql'
}, },
placeholder: 'Enter...',
lineNumbers: false lineNumbers: false
}) })
}, },
search () { search () {
this.$emit('search', this.metaList) const originalSql = this.codeMirror.getValue()
const parser = new SqlParser(originalSql, this.columnList)
const errorList = parser.validate()
if (this.$_.isEmpty(errorList)) {
const { metaList, formatSql } = parser.formatSql()
toRaw(this.codeMirror).setValue(formatSql)
this.$emit('search', metaList, formatSql)
}
}, },
changeMode () { changeMode () {
this.$emit('changeMode', 'tag') const originalSql = this.codeMirror.getValue()
const parser = new SqlParser(originalSql, this.columnList)
const errorList = parser.validate()
if (this.$_.isEmpty(errorList)) {
const { metaList, formatSql } = parser.formatSql()
toRaw(this.codeMirror).setValue(formatSql)
this.$emit('changeMode', 'tag', metaList)
} else {
this.$emit('changeMode', 'tag', [])
}
},
addParams (params) {
Object.keys(params).forEach(key => {
const column = this.columnList.find(column => {
return column.name === key
})
let current = this.codeMirror.getValue()
current = `${key}=${(column.type === columnType.string ? stringInQuot(params[key]) : params[key])} ${current ? 'AND' : ''}`
toRaw(this.codeMirror).setValue(current)
})
}
},
watch: {
sql: {
immediate: true,
handler (n) {
if (n) {
setTimeout(() => {
toRaw(this.codeMirror).setValue(n)
})
}
}
} }
}, },
mounted () { mounted () {

View File

@@ -4,7 +4,9 @@ export const condition = 'condition'
export const columnType = { export const columnType = {
fullText: 'fullText', fullText: 'fullText',
string: 'string', string: 'string',
long: 'long' long: 'long',
number: 'number',
array: 'array'
} }
export const defaultOperatorList = ['=', '!=', '>', '<', '>=', '<=', 'IN', 'NOT IN', 'LIKE', 'NOT LIKE'] export const defaultOperatorList = ['=', '!=', '>', '<', '>=', '<=', 'IN', 'NOT IN', 'LIKE', 'NOT LIKE']
export const defaultConnectionList = [ export const defaultConnectionList = [

View File

@@ -71,6 +71,7 @@ export default {
this.$emit('close') this.$emit('close')
}, },
filter (row) { filter (row) {
console.info(row.name, this.item)
this.$emit('filter', row.name, this.item) this.$emit('filter', row.name, this.item)
}, },
initEntityTop (leftVal, itemVal, data, totalCount) { initEntityTop (leftVal, itemVal, data, totalCount) {

257
src/utils/sql-parser.js Normal file
View File

@@ -0,0 +1,257 @@
import { GenericSQL, SqlParserVisitor } from 'dt-sql-parser'
import Meta, { connection, condition, columnType } from '@/components/advancedSearch/meta/meta'
import _ from 'lodash'
// 补全语句,用于解析
const sqlPrev = 'select a from b where '
export default class SqlParser extends SqlParserVisitor {
constructor (init, columnList) {
super()
this.tempMeta = null
this.originalSql = ''
this.metaList = []
this.columnList = columnList
// 原始数据
if (_.isArray(init)) {
this.metaList = init
} else {
this.originalSql = init
}
// 工具类实例
this.dtSqlParser = new GenericSQL()
}
// 返回的数组是错误信息,为空即校验通过
validate () {
return this.dtSqlParser.validate(sqlPrev + this.originalSql)
}
// 规范化原始语句
formatSql () {
// 先使用originalSql走一遍parse获取metaList再将metaList转为sql
const tree = this.dtSqlParser.parse(sqlPrev + this.originalSql)
this.visit(tree)
if (this.metaList.length === 0 && this.tempMeta.column.name) {
this.tempMeta.column.type = columnType.fullText
this.tempMeta.column.name = stringInQuot(this.tempMeta.column.name)
this.tempMeta.column.label = this.tempMeta.column.name
this.metaList.push(_.cloneDeep(this.tempMeta))
}
this.tempMeta = null
return {
metaList: this.metaList,
formatSql: this.parseMetaToSql(this.metaList)
}
}
formatMetaList () {
const tempMetaList = _.cloneDeep(this.metaList)
this.metaList = []
tempMetaList.forEach(meta => {
if (meta.meta === condition) {
if (meta.column.type === columnType.fullText) {
this.metaList.push({ ...meta, name: stringInQuot(meta.name), label: stringInQuot(meta.name) })
} else if (meta.column.type === columnType.string) {
if (_.isArray(meta.value.value)) {
meta.value.value = meta.value.value.map(v => {
return stringInQuot(v)
})
meta.value.label = `(${meta.value.value.join(',')})`
} else {
meta.value.value = stringInQuot(meta.value.value)
meta.value.label = stringInQuot(meta.value.value)
}
this.metaList.push(meta)
} else {
if (_.isArray(meta.value.value)) {
meta.value.label = `(${meta.value.value.join(',')})`
} else {
meta.value.label = meta.value.value
}
this.metaList.push(meta)
}
}
})
return {
metaList: this.metaList,
formatSql: this.parseMetaToSql(this.metaList)
}
}
parseMetaToSql (metaList) {
let sql = ''
metaList.forEach(meta => {
if (meta.meta === condition) {
sql += meta.column.name
if (meta.column.type !== columnType.fullText) {
sql += `${meta.operator.value}${meta.value.value} `
}
}
if (meta.meta === connection) {
sql += `${meta.value} `
}
})
return sql
}
buildMeta (type, value) {
switch (type) {
case 'expression': {
// 如果tempMeta是空或已是完整的condition新建meta
if (!this.tempMeta || this.tempMeta.isCompleteCondition()) {
this.tempMeta = new Meta(condition)
}
// 如果column.name为空则参数值是column否则是value
if (!this.tempMeta.column.name) {
// 在columnList中的按columnList不在则先设为空串由之后value值来自动判断
const column = this.columnList.find(column => {
return column.name === value
})
this.tempMeta.column.name = value
this.tempMeta.column.label = column ? column.label : value
this.tempMeta.column.type = column ? column.type : ''
} else {
// 若column的type为空则根据value自动判断赋值
if (!this.tempMeta.column.type) {
this.tempMeta.column.type = handleType(value)
}
if (this.tempMeta.column.type === columnType.string) {
if (_.isArray(value)) {
this.tempMeta.value.value = value.map(v => {
return stringInQuot(v)
})
this.tempMeta.value.label = `(${this.tempMeta.value.value.join(',')})`
} else {
this.tempMeta.value.value = stringInQuot(value)
this.tempMeta.value.label = stringInQuot(value)
}
} else {
this.tempMeta.value.value = value
if (_.isArray(value)) {
this.tempMeta.value.label = `(${this.tempMeta.value.value.join(',')})`
} else {
this.tempMeta.value.label = value
}
}
this.tempMeta.value.show = true
this.metaList.push(_.cloneDeep(this.tempMeta))
}
break
}
case 'operator': {
this.tempMeta.operator.value = value
this.tempMeta.operator.label = value
this.tempMeta.operator.show = true
break
}
// (not)in和(not)like特殊处理
case 'like':
case 'in': {
const { column, operator, handleValue } = handleInOrLike(value)
this.buildMeta('expression', column)
this.buildMeta('operator', operator)
this.buildMeta('expression', handleValue)
break
}
case 'connection': {
// tempMeta的value为空则上个条件是全文搜索
if (!this.tempMeta.column.type && !this.tempMeta.value.value) {
this.tempMeta.column.type = columnType.fullText
this.tempMeta.column.name = stringInQuot(this.tempMeta.column.name)
this.tempMeta.column.label = this.tempMeta.column.name
this.metaList.push(_.cloneDeep(this.tempMeta))
this.tempMeta = null
}
const meta = new Meta(connection)
meta.value = value
this.metaList.push(meta)
break
}
}
}
// 字段或值
visitExpressionAtomPredicate (ctx) {
const constant = ctx.getText().toLowerCase()
this.buildMeta('expression', constant)
}
// 操作符
visitComparisonOperator (ctx) {
const comparisonOperator = ctx.getText().toLowerCase()
this.buildMeta('operator', comparisonOperator)
}
// 连接符
visitLogicalOperator (ctx) {
const logicalOperator = ctx.getText().toUpperCase()
this.buildMeta('connection', logicalOperator)
}
// in语句
visitInPredicate (ctx) {
const inPredicate = ctx.getText().toLowerCase()
this.buildMeta('in', inPredicate)
}
// like语句
visitLikePredicate (ctx) {
const likePredicate = ctx.getText().toLowerCase()
this.buildMeta('like', likePredicate)
}
}
function handleType (value) {
if (_.isInteger(value)) {
return columnType.long
} else if (_.isNumber(value)) {
return columnType.number
} else if (_.isString(value)) {
return columnType.string
} else if (_.isArray(value)) {
const arr = value.split(',')
const hasString = arr.some(v => {
return handleType(v) === columnType.string
})
return hasString ? columnType.string : columnType.number
}/* else if (isStringInQuot(value)) {
return columnType.string
} */
return null
}
// 使用单引号包裹
export function stringInQuot (value) {
const match = value.match(/^'.+?'$/)
return match ? value : `'${value}'`
}
function handleInOrLike (value, type) {
let sep
if (type === 'in') {
sep = `${type}(`
} else if (type === 'like') {
sep = `${type}'`
}
const notSep = `not${sep}`
let arr
let operator
if (value.split(notSep).length === 3) {
arr = value.split(notSep)
operator = `NOT ${type.toUpperCase()}`
} else {
arr = value.split(sep)
operator = type.toUpperCase()
}
const columnName = arr[0]
let v
if (type === 'in') {
v = arr[2].substring(0, arr[2].length - 2).split(',')
} else if (type === 'like') {
v = `'${arr[2]}'`
}
return {
column: columnName,
operator,
handleValue: v
}
}

View File

@@ -444,11 +444,11 @@ export default {
} }
// 带参数时只查询对应类型的entity不带参数时3种entity都查 // 带参数时只查询对应类型的entity不带参数时3种entity都查
if (this.searchParams && Object.keys(this.searchParams).length > 0) { if (this.searchParams && Object.keys(this.searchParams).length > 0) {
this.queryFilter({ entityType: 'ip', q: this.handleQ(this.searchParams), ...this.timeFilter }) this.queryFilter({ entityType: 'ip', q: this.searchParams.q, ...this.timeFilter })
this.queryFilter({ entityType: 'app', q: this.handleQ(this.searchParams), ...this.timeFilter }) this.queryFilter({ entityType: 'app', q: this.searchParams.q, ...this.timeFilter })
this.queryFilter({ entityType: 'domain', q: this.handleQ(this.searchParams), ...this.timeFilter }) this.queryFilter({ entityType: 'domain', q: this.searchParams.q, ...this.timeFilter })
this.queryList({ q: this.handleQ(this.searchParams), ...this.timeFilter, ...this.pageObj }) this.queryList({ q: this.searchParams.q, ...this.timeFilter, ...this.pageObj })
this.queryListTotal({ q: this.handleQ(this.searchParams), ...this.timeFilter }) this.queryListTotal({ q: this.searchParams.q, ...this.timeFilter })
} else { } else {
this.queryFilter({ entityType: 'ip', ...this.timeFilter }) this.queryFilter({ entityType: 'ip', ...this.timeFilter })
this.queryFilter({ entityType: 'app', ...this.timeFilter }) this.queryFilter({ entityType: 'app', ...this.timeFilter })

View File

@@ -63,7 +63,7 @@ export default {
const offsetLeft = this.$refs['entityTopTen' + i].offsetLeft const offsetLeft = this.$refs['entityTopTen' + i].offsetLeft
const leftVal = offsetLeft + width const leftVal = offsetLeft + width
const queryParams = { const queryParams = {
q: objToStr(this.searchParams), q: this.searchParams.q,
entityType: type, entityType: type,
column: item.topColumn, column: item.topColumn,
top: 10, top: 10,

View File

@@ -177,8 +177,8 @@ export default {
} }
}, },
methods: { methods: {
search (params = {}) { search (q) {
this.$emit('search', params) this.$emit('search', { q: q })
}, },
addParams (params) { addParams (params) {
this.$refs.search.addParams(params) this.$refs.search.addParams(params)