首先在包含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);
},