1212 <div class =" flex items-center space-x-3" >
1313 <span v-if =" isCopied" class =" text-xs text-gray-400" >{{ t('console.copied') }}</span >
1414
15+ <!-- 过滤按钮 -->
16+ <button v-if =" output"
17+ @click =" showFilter = !showFilter"
18+ class =" text-gray-400 hover:text-white transition-colors duration-200 p-1 rounded hover:bg-gray-700 cursor-pointer"
19+ :class =" { 'text-white bg-gray-700': showFilter }"
20+ :title =" t('console.filterTitle')" >
21+ <Search class="w-3 h-3"/>
22+ </button >
23+
1524 <!-- 复制按钮 -->
1625 <button v-if =" output && !isRunning"
1726 @click =" copyOutput"
3544 </div >
3645 </div >
3746
47+ <!-- 过滤栏:仅显示包含关键字的行 -->
48+ <div v-if =" showFilter && output" class =" px-2 py-1 border-b border-gray-700 flex items-center gap-2" >
49+ <Search class="w-3 h-3 text-gray-500 flex-shrink-0"/>
50+ <input v-model =" filterQuery"
51+ class =" flex-1 bg-transparent text-xs text-gray-200 placeholder-gray-600 focus:outline-none"
52+ :placeholder =" t('console.filterPlaceholder')" />
53+ <span v-if =" filterQuery.trim()" class =" text-[10px] text-gray-500 flex-shrink-0" >{{ t('console.filterCount', { n: matchCount }) }}</span >
54+ </div >
55+
3856 <div class =" flex-1 overflow-auto" ref =" outputContainer" >
3957 <div v-if =" isRunning && !output" class =" p-4 flex items-center space-x-2 text-yellow-400" >
4058 <Loader class="w-4 h-4 animate-spin"/>
6381<script setup lang="ts">
6482import { computed , nextTick , ref , watch } from ' vue'
6583import { useI18n } from ' vue-i18n'
66- import { Check , Clock , Copy , Loader , Terminal , Trash2 } from ' lucide-vue-next'
84+ import { Check , Clock , Copy , Loader , Search , Terminal , Trash2 } from ' lucide-vue-next'
6785import { ansiToHtml } from ' ../utils/ansi'
6886
6987const props = defineProps <{
@@ -81,12 +99,31 @@ const {t} = useI18n()
8199const isCopied = ref (false )
82100const outputContainer = ref <HTMLElement >()
83101
102+ // 输出过滤:仅显示包含关键字的行
103+ const showFilter = ref (false )
104+ const filterQuery = ref (' ' )
105+ const filteredText = computed (() => {
106+ const q = filterQuery .value .trim ().toLowerCase ()
107+ const text = props .output || ' '
108+ if (! showFilter .value || ! q ) {
109+ return text
110+ }
111+ return text .split (' \n ' ).filter (l => l .toLowerCase ().includes (q )).join (' \n ' )
112+ })
113+ const matchCount = computed (() => {
114+ const q = filterQuery .value .trim ()
115+ if (! q ) {
116+ return 0
117+ }
118+ return filteredText .value ? filteredText .value .split (' \n ' ).length : 0
119+ })
120+
84121// 动态切换图标
85122const copyIcon = computed (() => isCopied .value ? Check : Copy )
86123
87124// 根据执行状态和成功状态确定输出样式
88- // 渲染 ANSI 颜色(自带 HTML 转义)
89- const renderedOutput = computed (() => ansiToHtml (props . output || ' ' ))
125+ // 渲染 ANSI 颜色(自带 HTML 转义);过滤开启时仅渲染匹配行
126+ const renderedOutput = computed (() => ansiToHtml (filteredText . value ))
90127
91128const getOutputClass = () => {
92129 if (props .isRunning ) {
0 commit comments