【前端Vue】如何在log-viewer组件中添加搜索定位功能

发布于:2025-08-13 ⋅ 阅读:(30) ⋅ 点赞:(0)

首先在包含log-viewer组件的渲染标签中加入搜索框和一些操作按钮,支持通过搜索框输入和清空搜索内容,支持点击操作按钮上下定位搜索结果,并且可以显示搜索结果数量和当前所处结果的序号。输入框增加了clearable就能够直接在输入框中使用内置的点击叉叉图标清空内容

<!-- 搜索控件 -->
        <div class="log-search-container">
            <el-input
                ref="searchInput"
                v-model="searchText"
                placeholder="输入搜索关键词"
                size="mini"
                class="log-search-input"
                @keyup.enter.native="searchNext"
                @input="onSearchInput"
                clearable
            ></el-input>
            <div class="search-info" v-if="searchText">
              {{ searchCurrentIndex + 1 }} / {{ searchResults.length }}
            </div>
            <el-button-group>
            <el-button
                v-if="searchText && searchResults.length > 0"
                size="mini"
                icon="el-icon-arrow-up"
                @click="searchPrev"
            ></el-button>
            <el-button
                icon="el-icon-arrow-down"
                v-if="searchText && searchResults.length > 0"
                size="mini"
                @click="searchNext"
            ></el-button>
            </el-button-group>
          </div>
        </div>

<!-- 日志显示组件 -->
        <log-viewer
            v-if="isLoggingDom"
            ref="logViewerContainer"
            class="log-content"
            :key="logviewViewerKey"
            :log="searchText ? highlightedLogContent : logContent"
            :loading="isFetchingLogs"
         />

要在data()中定义几个参数用于搜索控件的使用:

searchText: '',

searchResults: [],

searchCurrentIndex: -1,

searchOccurrences: [], // 存储搜索结果位置

随后定义匹配的搜索相关方法,搜索时会将搜索结果呈现黄色,点击操作按钮进行上下定位,定位到的结果显示为绿色   

// 搜索相关方法
    onSearchInput() {
      if (this.searchText) {
        this.performSearch();
      } else {
        this.clearSearch();
      }
    },

    performSearch() {
      const content = this.logContent;
      if (!content || !this.searchText) {
        this.searchResults = [];
        this.searchCurrentIndex = -1;
        return;
      }

      // 查找所有匹配项
      const regex = new RegExp(this.escapeRegExp(this.searchText), 'gi');
      const matches = [];
      let match;

      while ((match = regex.exec(content)) !== null) {
        matches.push({
          index: match.index,
          text: match[0]
        });
      }

      this.searchResults = matches;
      if (matches.length > 0) {
        this.searchCurrentIndex = 0;
        this.scrollToSearchResult(0);
      } else {
        this.searchCurrentIndex = -1;
      }
    },

    searchNext() {
      if (this.searchResults.length === 0) return;

      this.searchCurrentIndex = (this.searchCurrentIndex + 1) % this.searchResults.length;
      this.scrollToSearchResult(this.searchCurrentIndex);
    },

    searchPrev() {
      if (this.searchResults.length === 0) return;

      this.searchCurrentIndex = (this.searchCurrentIndex - 1 + this.searchResults.length) % this.searchResults.length;
      this.scrollToSearchResult(this.searchCurrentIndex);
    },

    scrollToSearchResult(index) {
      if (this.searchResults.length === 0 || index < 0) return;
      // 更新当前索引以触发高亮更新
      this.searchCurrentIndex = index;

      // 获取当前显示的 log-viewer 组件
      const viewer = this.$refs.logViewerContainer;

      if (viewer && viewer.$refs && viewer.$refs.virturalList) {
        // 获取虚拟列表组件
        const virtualList = viewer.$refs.virturalList;

        // 获取内容并按行分割
        const content = this.logContent;
        const lines = content.split('\n');

        // 找到匹配项所在的行
        const matchPosition = this.searchResults[index].index;
        let lineNumber = 0;
        let charCount = 0;

        for (let i = 0; i < lines.length; i++) {
          const lineLength = lines[i].length + 1; // +1 for newline character
          if (charCount + lineLength > matchPosition) {
            lineNumber = i;
            break;
          }
          charCount += lineLength;
        }

        // 滚动到指定行(添加偏移量使内容居中)
        this.$nextTick(() => {
          // 使用 start 属性设置滚动位置
          // 虚拟列表通过 start 属性控制显示的起始行
          if (virtualList && typeof virtualList.scrollToIndex === 'function') {
            // 直接滚动到目标行,不减去偏移量
            virtualList.scrollToIndex(lineNumber);
          } else if (virtualList && typeof virtualList.$el.scrollTo === 'function') {
            // 备选方案:使用原生滚动方法,修正计算方式
            const lineHeight = 20; // 假设每行高度为20px
            // 确保滚动到正确位置,减去一些像素使目标行在视图中央
            const targetScrollTop = Math.max(0, lineNumber * lineHeight - (virtualList.$el.clientHeight / 2));
            virtualList.$el.scrollTo({ top: targetScrollTop, behavior: 'smooth' });
          }
        });
      } else {
        // 如果无法直接控制滚动,至少显示提示信息
        this.$message.info(`已定位到第 ${index + 1} 个匹配项,共找到 ${this.searchResults.length} 个匹配项`);
      }
    },

    clearSearch() {
      this.searchText = '';
      this.searchResults = [];
      this.searchCurrentIndex = -1;
    },

    // 使用 ANSI 转义序列添加高亮(log-viewer组件只支持ANSI 转义)
    highlightText(content, searchText, currentIndex = -1) {
      if (!searchText) return content;

      // 分割成行以便处理
      const lines = content.split('\n');
      let globalIndex = 0; // 全局匹配索引

      // 使用正则匹配搜索词
      const regex = new RegExp(`(${this.escapeRegExp(searchText)})`, 'gi');
      const highlightedLines = lines.map(line => {
        return line.replace(regex, (match, ...args) => {
          const currentGlobalIndex = globalIndex;
          globalIndex++;

          // 如果是当前选中的搜索结果,使用不同的颜色
          if (currentGlobalIndex === currentIndex) {
            return `\x1b[42m${match}\x1b[0m`; // 当前结果使用绿色背景
          } else {
            return `\x1b[103m${match}\x1b[0m`; // 其余使用亮黄色背景
          }
        });
      });

      return highlightedLines.join('\n');
    },

    escapeRegExp(string) {
      return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }
  },
};

随后在computed计算属性中增加对高亮文字的处理,将搜索框的内容状态与内容显示通过计算属性互相绑定起来

// 高亮显示的日志内容
    highlightedLogContent() {
      if (!this.searchText || this.searchResults.length === 0) {
        return this.logContent;
      }
      return this.highlightText(this.logContent, this.searchText,  this.searchCurrentIndex);
    },


网站公告

今日签到

点亮在社区的每一天
去签到