This repository has been archived on 2025-09-14. You can view files and clone it, but cannot push or open issues or pull requests.
Files
cyber-narrator-cn-ui/src/components/advancedSearch/TextMode.vue
2024-02-02 18:13:11 +08:00

513 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div @click="handleClick" v-ele-click-outside="handleBlur">
<textarea
style="text-indent: 65px;"
cols="40"
ref="textSearch"
></textarea>
<div class="search__suffixes search__suffixes--text-mode" :class="showList ? '' : 'entity-explorer-home'" style="padding-left: 1px;background: #fff;">
<!--切换texttag模式图标-->
<span class="search__suffix">
<el-popover
popper-class="my-popper-class"
placement="top"
trigger="click"
:content="$t('overall.switchToTag')"
>
<template #reference>
<i test-id="text-change-mode" class="cn-icon cn-icon-filter" @click="changeMode"></i>
</template>
</el-popover>
</span>
<!--删除图标-->
<span v-show="isCloseIcon" class="search__suffix search__suffix-close" @click="cleanParams">
<el-icon><CircleCloseFilled /></el-icon>
<!--<i class="el-icon-error"></i>-->
</span>
<!--搜索图标-->
<span class="search__suffix" test-id="text-search" @click.stop="search">
<el-icon><Search /></el-icon>
<!--<i class="el-icon-search"></i>-->
</span>
</div>
<!--showHint弹窗部分-->
<el-popover
placement="bottom"
width="100%"
ref="popoverRef"
:visible="hintVisible"
popper-class="search-show-hint-popover"
trigger="click">
<template #reference>
<div>
<hint v-if="hintVisible" :hintList="hintList"
@load="handleHintLoad"
@select="handleSelect"
:hintParams="hintParams"
:hintSearch="searchStr"></hint>
</div>
</template>
</el-popover>
</div>
</template>
<script>
import Parser, { stringInQuot, handleOperatorSpace } from '@/components/advancedSearch/meta/parser'
import CodeMirror from 'codemirror'
import { toRaw } from 'vue'
import _ from 'lodash'
import { columnType } from '@/components/advancedSearch/meta/meta'
import { handleErrorTip } from '@/components/advancedSearch/meta/error'
import { overwriteUrl, urlParamsHandler } from '@/utils/tools'
import Hint from '@/components/advancedSearch/showhint/Hint/Hint'
import { getDataset } from '@/components/advancedSearch/showhint/packages/getDataset'
import codeMirrorMixins from '@/components/advancedSearch/showhint/myCodeMirror.js'
export default {
name: 'TextMode',
mixins: [codeMirrorMixins],
props: {
columnList: Array,
str: String,
showList: Boolean,
showCloseIcon: Boolean,
isShowHint: {
type: Boolean,
default: false
},
unitTestStr: String
},
data () {
return {
codeMirror: null,
isCloseIcon: this.showCloseIcon,
isEdit: false,
hintVisible: false,
dataset: null,
CodeMirror,
myUnitTestStr: this.unitTestStr
}
},
emits: ['changeMode', 'search'],
inject: ['myHighLight'],
created () {
if (this.isShowHint && !this.isUnitTesting) {
this._initComponent()
}
},
provide () {
return {
getDataset: () => {
// provide() 写成方法之后,保证this的指向
return this.dataset || null
}
}
},
components: {
Hint
},
computed: {
searchStr () {
const { wholeTokenStr } = this.getWholeToken() || ''
if (['not in', 'not like', 'order by', 'group by'].includes(wholeTokenStr?.toLowerCase())) {
return wholeTokenStr
}
if (['operator', 'keyword', 'builtin'].includes(this.hintParams?.token?.type)) {
return this.hintParams?.token?.string
}
return this.hintParams.leftpart || this.hintSearch || ''
}
},
methods: {
cleanParams () {
toRaw(this.codeMirror).setValue('')
this.isEdit = false
this.isCloseIcon = false
const routeQuery = this.$route.query
delete routeQuery.q
this.reloadUrl(routeQuery, 'cleanOldParams')
},
initCodeMirror () {
let option = {
mode: 'sql',
placeholder: '',
lineNumbers: false
}
if (this.isShowHint) {
option = {
keyMap: 'sublime',
tabSize: 2, // 缩进格式
// theme: 'eclipse', // 主题,对应主题库 JS 需要提前引入
line: true,
lineNumbers: false, // 显示行数
indentUnit: 4, // 缩进单位为4
styleActiveLine: true, // 当前行背景高亮
// mode: 'text/x-filter', // HMTL混合模式
mode: 'sql', // HMTL混合模式
foldGutter: true,
lint: true,
auto: 'auto', // 自动换行
autoCloseBrackets: true, // 自动闭合符号
matchBrackets: true, // 是否添加匹配括号高亮
spellcheck: true, // 启用拼写检查
autocorrect: true, // 启用自动更正
lineWrapping: true, // 滚动或换行以显示长行
// 提示配置
hintOptions: {
completeSingle: false, // 自动匹配唯一值
// 匹配 t_test_login.col_a 用. 来连接的
tables: {
filter_table: ['recv_time']
},
alignWithWord: false
}
}
}
this.codeMirror = CodeMirror.fromTextArea(this.$refs.textSearch, option)
if (this.codeMirror) {
this.codeMirror.setOption('extraKeys', {
Enter: (cm) => {}
})
this.setCodemirrorValue()
this.initEvent()
this.initHint()
}
},
search () {
this.handleBlur()
let str
if (!this.isUnitTesting) {
str = this.codeMirror.getValue().trim()
} else {
str = this.myUnitTestStr
}
if (str) {
const parser = new Parser(this.columnList)
const keyInfo = parser.comparedEntityKey(parser.handleEntityTypeByStr(str)) // 校验输入str字段是schema内的字段并将语句进行规范
const metaList = parser.parseStr(_.cloneDeep(str)).metaList
const keywordList = this.myHighLight ? parser.getKeywordList(metaList) : [] // 搜索高亮所需的关键字
if (keyInfo.isKey) {
const enumKey = parser.conversionEnum(keyInfo.key) // 检查是否包含枚举字段,包含的话进行替换
const errorList = parser.validateStr(enumKey) // 检查语句是否有错误
if (_.isEmpty(errorList)) {
// 补全模糊搜索
if (!this.isUnitTesting) {
toRaw(this.codeMirror).setValue(parser.handleEntityTypeByStr(str))
}
// 注参数str1.是用户搜索框的内容在补全模糊搜索后的内容2.部分参数是用户主观可见但格式不符合接口原则的如status='Active'接口需要status=0
this.$emit('search', { ...parser.parseStr(enumKey), str: parser.handleEntityTypeByStr(str), keywordList: keywordList })
} else {
this.$message.error(handleErrorTip(errorList[0]))
}
} else {
this.$message.error(this.$t('tip.invalidQueryField') + ' ' + keyInfo.key)
}
} else {
this.$emit('search', { q: '', str: '', metaList: [] })
}
},
focus (e) {
toRaw(this.codeMirror).setValue(e.str)
// this.codeMirror.focus()
},
changeMode () {
const str = this.codeMirror.getValue().trim()
if (str) {
const parser = new Parser(this.columnList)
const errorList = parser.validateStr(str)
if (_.isEmpty(errorList)) {
const metaList = parser.parseStr(str)
this.reloadUrl({ mode: 'tag' })
this.$emit('changeMode', 'tag', metaList)
} else {
this.reloadUrl({ mode: 'tag' })
this.$emit('changeMode', 'tag', { metaList: [], str: '' })
}
} else {
this.reloadUrl({ mode: 'tag' })
this.$emit('changeMode', 'tag', { str: '', metaList: [] })
}
},
// 处理value例如转换IN的值
handleValue (value, column, operator) {
const isArray = ['IN', 'NOT IN'].indexOf(operator) > -1
if (isArray) {
if (_.isArray(value)) {
value = value.map(v => column.type === columnType.string ? stringInQuot(v) : v)
return `(${value.join(',')})`
} else {
return value
}
} else {
return (column.type.items ? column.type.items : column.type) === columnType.string ? stringInQuot(value) : value
}
},
addParams (params) {
let current = ''
if (!this.isUnitTesting) {
current = this.codeMirror.getValue()
} else {
current = this.myUnitTestStr
}
params.forEach(param => {
const column = this.columnList.find(c => c.label === param.column)
if (param.operator === 'has') {
current = `${current ? current + ' AND ' : ''}${param.operator}(${param.column},${this.handleValue(param.value, column, param.operator)})`
} else {
current = `${current ? current + ' AND ' : ''}${param.column}${handleOperatorSpace(param.operator)}${this.handleValue(param.value, column, param.operator)}`
}
})
if (!this.isUnitTesting) {
toRaw(this.codeMirror).setValue(current.trim())
} else {
this.myUnitTestStr = current
}
},
removeParams (params) {
let current = this.codeMirror.getValue()
params.forEach(param => {
const column = this.columnList.find(c => c.label === param.column)
// 将对应内容替换为空串
const sqlPiece = `${param.column}${handleOperatorSpace(param.operator)}${this.handleValue(param.value, column, param.operator)}`.trim()
const sqlPieceWithConnection = [` AND ${sqlPiece}`, ` OR ${sqlPiece}`, `${sqlPiece} AND `, `${sqlPiece} OR `, sqlPiece]
sqlPieceWithConnection.forEach(piece => {
current = current.replace(piece, '')
})
})
toRaw(this.codeMirror).setValue(current.trim())
},
changeParams (params) {
let current = this.codeMirror.getValue()
params.forEach(param => {
const oldColumn = this.columnList.find(c => c.label === param.oldParam.column)
const newColumn = this.columnList.find(c => c.label === param.newParam.column)
// 将oldParam内容替换为newParam
const oldSqlPiece = `${param.oldParam.column}${handleOperatorSpace(param.oldParam.operator)}${this.handleValue(param.oldParam.value, oldColumn, param.oldParam.operator)}`.trim()
const newSqlPiece = `${param.newParam.column}${handleOperatorSpace(param.newParam.operator)}${this.handleValue(param.newParam.value, newColumn, param.newParam.operator)}`.trim()
current = current.replace(oldSqlPiece, newSqlPiece)
})
toRaw(this.codeMirror).setValue(current.trim())
},
/**
* 向地址栏添加/删除参数
*/
reloadUrl (newParam, clean) {
const { query } = this.$route
let newUrl = urlParamsHandler(window.location.href, query, newParam)
if (clean) {
newUrl = urlParamsHandler(window.location.href, query, newParam, clean)
}
overwriteUrl(newUrl)
},
initEvent () {
this.codeMirror.on('focus', (coder) => {
if (this.codeMirror.getValue().trim() !== '') {
this.isEdit = true
this.isCloseIcon = true
}
if (this.isShowHint && this.$emit) {
this.$emit('focus', coder.getValue())
}
})
this.codeMirror.on('blur', (coder) => {
const timer = setTimeout(() => {
this.isEdit = false
this.isCloseIcon = false
if (this.isShowHint && this.$emit) {
this.$emit('blur', coder.getValue())
}
clearTimeout(timer)
}, 200)
})
this.codeMirror.on('update', () => {
this.isEdit = true
this.isCloseIcon = true
})
if (this.isShowHint) {
// 支持双向绑定
this.codeMirror.on('change', (coder) => {
if (this.$emit) {
this.$emit('input', coder.getValue())
}
})
this.codeMirror.on('startCompletion', () => {
// 展开自动提示的 事件回调
this.hintVisible = true
this.hintVm?.hintDeactive()
})
this.codeMirror.on('endCompletion', () => {
// 自动提示关闭
this.hintVisible = false
this.hintParams = {}
this.hintList = []
})
this.$emit('load', this.codeMirror)
}
},
_initComponent () {
getDataset(this, this.queryParams || {}, this.columnList).then((dataset, dataDisposeFun) => {
this.dataset = Object.freeze(dataset)
}).catch(err => {
console.error(err)
})
},
initHint () {
this.codeMirror.on('inputRead', () => {
setTimeout(() => {
this.codeMirror.showHint()
})
})
},
handleBlur () {
if (this.isShowHint) {
this.hintVisible = false
this.hintParams = {}
this.hintList = []
}
},
handleClick () {
if (this.isShowHint) {
this.hintVisible = true
this.codeMirror.showHint()
}
},
getWholeToken () {
// 获取 前一个token
const editor = this.hintParams.editor
const Pos = this.CodeMirror.Pos
const cur = this.hintParams.cur
const token = this.hintParams.token
if (!editor) {
return
}
const spaceToken = editor.getTokenAt(Pos(cur.line, token.start))
let preToken = ''
if (spaceToken && spaceToken?.string === ' ') {
preToken = editor.getTokenAt(Pos(cur.line, spaceToken.start))
}
const searchKey = `${preToken?.string} ${token?.string}`
return {
wholeTokenStr: searchKey,
spaceToken,
preToken,
token
}
},
handleHintLoad ({ vm }) {
this.hintVm = vm
},
handleSelect (item, index, hintList) {
if (index === 0) {
// 不可能选中0 第0项是标题, 选中0 说明没选中
this.hintParams?.editor?.closeHint()
this.$emit('query', CodeMirror)
return
}
const data = {
from: this.hintParams.from,
to: this.hintParams.to,
list: hintList
}
const { wholeTokenStr, preToken, token } = this.getWholeToken() || ''
let cur = null
cur = this.hintParams?.cur
// 上一个字段 是存在空格的关键字,整体删除上一个关键字
if (['not in', 'not like', 'order by', 'group by'].includes(wholeTokenStr?.toLowerCase())) {
this.hintParams?.editor?.replaceRange('', { line: cur.line, ch: preToken.start }, {
line: cur.line,
ch: token.end
})
}
this.completion && this.completion.pick(data, index)
},
setCodemirrorValue () {
// 如果地址栏包含参数q则将参数q回显到搜索栏内
let { q } = this.$route.query
if (this.str) {
toRaw(this.codeMirror).setValue(this.str)
}
if (q) {
if (q.indexOf('+') > -1) {
q = q.replace('+', ' ')
}
if (q.indexOf('%') === 0 || q.indexOf('%20') > -1 || q.indexOf('%25') > -1) {
q = decodeURI(q)
} else {
const str1 = q.substring(q.indexOf('%'), q.indexOf('%') + 3)
if (q.indexOf('%') > 0 && (str1 === '%20' || str1 === '%25')) {
q = decodeURI(q)
}
}
// 为避免地址栏任意输入导致全查询的q带QUERY解析时不识别导致的语法错误
// 如地址栏输入116.178.222.171此时的q很长刷新界面时需要把q里的116.178.222.171拿出来进行搜索
if (q.indexOf('QUERY') > -1) {
const strList = q.split(' ')
if (strList.length > 0) {
// 此时strList[1]为ip_addr:116.178.222.171获取116.178.222.171
q = strList[1].slice(8)
}
}
if (this.codeMirror) {
toRaw(this.codeMirror).setValue(q)
}
} else {
this.isCloseIcon = false
}
const vm = this
this.emitter.on('advanced-search', function () {
vm.search()
})
}
},
watch: {
str: {
immediate: true,
handler (n) {
if (n) {
setTimeout(() => {
toRaw(this.codeMirror).setValue(n)
})
}
}
},
showCloseIcon (n) {
if (!this.isEdit) {
const str = this.codeMirror.getValue().trim()
if (str !== '') {
this.isCloseIcon = n
}
}
}
},
mounted () {
if (this.isShowHint) {
this.$nextTick(() => {
// dataset是避免数据未初始化完成注册失败ref是因为组件加载2次避免第二次时dom丢失导致数据挂载失败
if (this.dataset && this.$refs.textSearch) {
this.initShowHint()
this.initCodeMirror()
}
})
} else if (this.$refs.textSearch) {
this.initCodeMirror()
}
}
}
</script>
<style>
.el-popper.search-show-hint-popover {
visibility: hidden !important;
}
</style>