magic-search.vue 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. <template>
  2. <magic-dialog :title="$i('message.search')" v-model:value="show" :shade="false" padding="0" width="700px" top="60px">
  3. <magic-input v-model:value="keyword" :placeholder="$i('message.searchText')" />
  4. <template v-if="results.length >0">
  5. <div class="magic-search-result">
  6. <div v-for="(item, key) in results" :key="key" class="magic-search-result-item" :class="{ selected: selectedItem === item}" @click="onClick(item)" @dblclick="onClick(item, true)">
  7. <div class="label" v-html="item.text"></div>
  8. <div class="name"><magic-text-icon :icon="item.icon" />{{ item.name }}</div>
  9. <div class="line" v-text="item.line"></div>
  10. </div>
  11. </div>
  12. <div class="display-text"><magic-text-icon :icon="selectedItem.icon" />{{displayText}}</div>
  13. <magic-monaco-editor readonly :value="selectedItem.script" :language="selectedItem.language" style="width: 100%; height:300px" :matches="keyword"></magic-monaco-editor>
  14. </template>
  15. </magic-dialog>
  16. </template>
  17. <script setup>
  18. import { computed, inject, ref, watch } from 'vue'
  19. import bus from '../../../scripts/bus.js'
  20. import $i from '../../../scripts/i18n.js'
  21. import Message from '../../../scripts/constants/message.js'
  22. import request from '../../../scripts/request.js'
  23. import { TokenizationRegistry } from 'monaco-editor/esm/vs/editor/common/modes.js'
  24. import { tokenizeToString } from 'monaco-editor/esm/vs/editor/common/modes/textToHtmlTokenizer.js'
  25. const keyword = ref('')
  26. const show = ref(false)
  27. const findResource = inject('findResource')
  28. const services = inject('service')
  29. const results = ref([])
  30. const selectedItem = ref({})
  31. const displayText = computed(() => selectedItem.value.name + (selectedItem.value.path ? `(${selectedItem.value.path})` : ''))
  32. const fetchScript = item => {
  33. if(!item.script){
  34. request.sendGet(`/resource/file/${item.id}`).success(data => {
  35. item.script = data.script
  36. })
  37. }
  38. }
  39. const onClick = (item, open) => {
  40. selectedItem.value = item
  41. fetchScript(item)
  42. if(open) {
  43. bus.$emit(Message.OPEN_WITH_ID, item.id)
  44. show.value = false
  45. results.value = []
  46. keyword.value = ''
  47. }
  48. }
  49. bus.$on(Message.DO_SEARCH, () => {
  50. results.value = []
  51. keyword.value = ''
  52. show.value = !show.value
  53. })
  54. let timer = null
  55. const getTextNodeList = (dom) => {
  56. const nodeList = [...dom.childNodes]
  57. const textNodes = []
  58. while (nodeList.length) {
  59. const node = nodeList.shift()
  60. if (node.nodeType === node.TEXT_NODE) {
  61. textNodes.push(node)
  62. } else {
  63. nodeList.unshift(...node.childNodes)
  64. }
  65. }
  66. return textNodes
  67. }
  68. const getTextInfoList = (textNodes) => {
  69. let length = 0
  70. return textNodes.map(node => {
  71. let startIdx = length, endIdx = length + node.wholeText.length
  72. length = endIdx
  73. return {
  74. text: node.wholeText,
  75. startIdx,
  76. endIdx
  77. }
  78. })
  79. }
  80. const getMatchList = (content, keyword) => {
  81. const characters = [...'[]()?.+*^${}:'].reduce((r, c) => (r[c] = true, r), {})
  82. keyword = keyword.split('').map(s => characters[s] ? `\\${s}` : s).join('[\\s\\n]*')
  83. const reg = new RegExp(keyword, 'gmi')
  84. return [...content.matchAll(reg)] // matchAll结果是个迭代器,用扩展符展开得到数组
  85. }
  86. const replaceMatchResult = (textNodes, textList, matchList) => {
  87. // 对于每一个匹配结果,可能分散在多个标签中,找出这些标签,截取匹配片段并用font标签替换出
  88. for (let i = matchList.length - 1; i >= 0; i--) {
  89. const match = matchList[i]
  90. const matchStart = match.index, matchEnd = matchStart + match[0].length // 匹配结果在拼接字符串中的起止索引
  91. // 遍历文本信息列表,查找匹配的文本节点
  92. for (let textIdx = 0; textIdx < textList.length; textIdx++) {
  93. const { text, startIdx, endIdx } = textList[textIdx] // 文本内容、文本在拼接串中开始、结束索引
  94. if (endIdx < matchStart) continue // 匹配的文本节点还在后面
  95. if (startIdx >= matchEnd) break // 匹配文本节点已经处理完了
  96. let textNode = textNodes[textIdx] // 这个节点中的部分或全部内容匹配到了关键词,将匹配部分截取出来进行替换
  97. const nodeMatchStartIdx = Math.max(0, matchStart - startIdx) // 匹配内容在文本节点内容中的开始索引
  98. const nodeMatchLength = Math.min(endIdx, matchEnd) - startIdx - nodeMatchStartIdx // 文本节点内容匹配关键词的长度
  99. if (nodeMatchStartIdx > 0) textNode = textNode.splitText(nodeMatchStartIdx) // textNode取后半部分
  100. if (nodeMatchLength < textNode.wholeText.length) textNode.splitText(nodeMatchLength)
  101. const span = document.createElement('span')
  102. span.innerText = text.substr(nodeMatchStartIdx, nodeMatchLength)
  103. span.className = 'keyword'
  104. textNode.parentNode.replaceChild(span, textNode)
  105. }
  106. }
  107. }
  108. const replaceKeywords = (htmlString, keyword) => {
  109. if (!keyword) return htmlString
  110. const div = document.createElement('div')
  111. div.innerHTML = htmlString
  112. const textNodes = getTextNodeList(div)
  113. const textList = getTextInfoList(textNodes)
  114. const content = textList.map(({ text }) => text).join('')
  115. const matchList = getMatchList(content, keyword)
  116. replaceMatchResult(textNodes, textList, matchList)
  117. return div.innerHTML
  118. }
  119. watch(keyword, (val) => {
  120. const text = val.trim()
  121. clearTimeout(timer)
  122. if(text){
  123. timer = setTimeout(()=>{
  124. request.send('/search', { keyword: text} ,{ method: 'POST'}).success(async data => {
  125. const list = []
  126. for (let index = 0; index < data.length; index++) {
  127. const item = data[index];
  128. const find = findResource(item.id)
  129. const config = services[find.type]
  130. const language = config.language || 'magicscript'
  131. const tokenizer = await TokenizationRegistry.getPromise(language)
  132. list.push({
  133. ...item,
  134. icon: config.getIcon(find.item),
  135. text: replaceKeywords(await tokenizeToString(item.text, tokenizer), text),
  136. name: find && find.name || 'unknown',
  137. script: '',
  138. language
  139. })
  140. }
  141. if(list.length > 0){
  142. selectedItem.value = list[0]
  143. fetchScript(selectedItem.value)
  144. }
  145. results.value = list
  146. })
  147. }, 600)
  148. }
  149. })
  150. </script>
  151. <style scoped>
  152. .magic-search-result {
  153. overflow: auto;
  154. max-height: 200px;
  155. background-color: var(--navbar-body-background-color);
  156. }
  157. .magic-search-result .magic-search-result-item {
  158. display: flex;
  159. padding: 0 5px;
  160. line-height: 20px;
  161. }
  162. .magic-search-result .magic-search-result-item:hover,
  163. .magic-search-result .magic-search-result-item.selected {
  164. background-color: var(--tree-hover-background-color);
  165. }
  166. .magic-search-result .magic-search-result-item .label {
  167. flex: 1;
  168. overflow: hidden;
  169. text-overflow: ellipsis;
  170. white-space: nowrap;
  171. }
  172. .magic-search-result .magic-search-result-item .label :deep(.keyword) {
  173. background: #FFDE7B;
  174. color: #000000;
  175. }
  176. .magic-search-result-item .name, .magic-search-result-item .line{
  177. color: var(--resource-span-color)
  178. }
  179. .magic-search-result .magic-search-result-item .line {
  180. padding-left: 5px;
  181. }
  182. .display-text {
  183. height: 30px;
  184. line-height: 30px;
  185. border-top: 1px solid var(--main-border-color);
  186. border-bottom: 1px solid var(--main-border-color);
  187. }
  188. </style>