@@ -6,6 +6,8 @@ import { Badge } from '@/components/ui/badge';
66import { Search , Play , Pause , Trash2 , AlertCircle } from 'lucide-react' ;
77import { useLogsStream , LogContext } from '@/api/v1' ;
88import { useInstance } from '@/contexts/instance' ;
9+ import { useVirtualizer } from '@tanstack/react-virtual' ;
10+ import debounce from 'lodash/debounce' ;
911import clsx from 'clsx' ;
1012
1113// 日志级别颜色映射(全部转为小写处理)
@@ -57,6 +59,11 @@ export const LogsPage = () => {
5759 const scrollAreaRef = useRef < HTMLDivElement > ( null ) ;
5860 const [ shouldAutoScroll , setShouldAutoScroll ] = useState ( true ) ;
5961
62+ // 使用防抖来处理搜索,避免频繁过滤
63+ const debouncedSetSearchQuery = debounce ( ( value : string ) => {
64+ setSearchQuery ( value ) ;
65+ } , 300 ) ;
66+
6067 // 监听滚动事件
6168 const handleScroll = ( ) => {
6269 const scrollArea = scrollAreaRef . current ;
@@ -74,23 +81,45 @@ export const LogsPage = () => {
7481 const scrollArea = scrollAreaRef . current ;
7582 scrollArea . scrollTop = scrollArea . scrollHeight ;
7683 }
77- } , [ logs , shouldAutoScroll , isPaused ] ) ;
84+ } , [ logs . length , shouldAutoScroll , isPaused ] ) ;
7885
7986 const filteredLogs = useMemo ( ( ) => {
8087 if ( ! searchQuery ) return logs ;
8188
8289 const searchLower = searchQuery . toLowerCase ( ) ;
8390 return logs . filter ( log => {
84- const fieldsStr = log . fields ?. ctx ? JSON . stringify ( log . fields ) : '' ;
85- return (
86- log . message ?. toLowerCase ( ) . includes ( searchLower ) ||
87- log . level ?. toLowerCase ( ) . includes ( searchLower ) ||
88- log . target ?. toLowerCase ( ) . includes ( searchLower ) ||
89- fieldsStr . toLowerCase ( ) . includes ( searchLower )
90- ) ;
91+ // 避免不必要的 JSON.stringify
92+ const basicMatch =
93+ ( log . message ?. toLowerCase ( ) . includes ( searchLower ) ||
94+ log . level ?. toLowerCase ( ) . includes ( searchLower ) ||
95+ log . target ?. toLowerCase ( ) . includes ( searchLower ) ) ;
96+
97+ if ( basicMatch ) return true ;
98+
99+ // 只在必要时检查 fields
100+ if ( log . fields ) {
101+ const fieldsMatch = Object . entries ( log . fields ) . some ( ( [ , value ] ) => {
102+ if ( typeof value === 'string' ) {
103+ return value . toLowerCase ( ) . includes ( searchLower ) ;
104+ }
105+ return false ;
106+ } ) ;
107+ return fieldsMatch ;
108+ }
109+
110+ return false ;
91111 } ) ;
92112 } , [ logs , searchQuery ] ) ;
93113
114+ const parentRef = useRef < HTMLDivElement > ( null ) ;
115+
116+ const rowVirtualizer = useVirtualizer ( {
117+ count : filteredLogs . length ,
118+ getScrollElement : ( ) => parentRef . current ,
119+ estimateSize : ( ) => 100 , // 预估每行高度
120+ overscan : 5 , // 预加载的行数
121+ } ) ;
122+
94123 const formatTime = ( timestamp : string ) => {
95124 try {
96125 const date = new Date ( timestamp ) ;
@@ -151,65 +180,82 @@ export const LogsPage = () => {
151180 < Input
152181 placeholder = "搜索日志..."
153182 className = "pl-8 bg-white border-gray-300"
154- value = { searchQuery }
155- onChange = { ( e ) => setSearchQuery ( e . target . value ) }
183+ onChange = { ( e ) => debouncedSetSearchQuery ( e . target . value ) }
156184 />
157185 </ div >
158186 </ CardHeader >
159187 < CardContent className = "p-0 flex-1 overflow-hidden" >
160188 < div
161- ref = { scrollAreaRef }
189+ ref = { parentRef }
162190 className = "h-full overflow-auto"
163191 onScroll = { handleScroll }
164192 >
165- < div className = "p-4 space-y-3" >
166- { filteredLogs . map ( ( log , index ) => (
167- < div
168- key = { index }
169- className = { clsx (
170- 'text-sm rounded-lg p-2 transition-colors' ,
171- log . level ?. toLowerCase ( ) === 'error' && 'bg-red-50' ,
172- ( log . level ?. toLowerCase ( ) === 'warn' || log . level ?. toLowerCase ( ) === 'warning' ) && 'bg-amber-50' ,
173- ) }
174- >
175- < div className = "flex items-start gap-2" >
176- < span className = "text-gray-500 shrink-0 font-mono" >
177- { formatTime ( log . timestamp ) }
178- </ span >
179- < Badge
180- variant = "outline"
181- className = { LOG_LEVEL_COLORS [ log . level ?. toLowerCase ( ) ] || LOG_LEVEL_COLORS . trace }
182- >
183- { log . level ?. toLowerCase ( ) === 'error' && < AlertCircle className = "h-3 w-3 mr-1" /> }
184- { log . level }
185- </ Badge >
186- { log . target && (
187- < Badge variant = "outline" className = "bg-gray-50 text-gray-700 border-gray-200" >
188- { log . target }
189- </ Badge >
193+ < div
194+ style = { {
195+ height : ` ${ rowVirtualizer . getTotalSize ( ) } px` ,
196+ width : '100%' ,
197+ position : 'relative' ,
198+ } }
199+ >
200+ { rowVirtualizer . getVirtualItems ( ) . map ( ( virtualRow ) => {
201+ const log = filteredLogs [ virtualRow . index ] ;
202+ return (
203+ < div
204+ key = { virtualRow . index }
205+ data-index = { virtualRow . index }
206+ ref = { rowVirtualizer . measureElement }
207+ style = { {
208+ position : 'absolute' ,
209+ top : 0 ,
210+ left : 0 ,
211+ width : '100%' ,
212+ transform : `translateY( ${ virtualRow . start } px)` ,
213+ } }
214+ className = { clsx (
215+ 'p-4' ,
216+ log . level ?. toLowerCase ( ) === 'error' && 'bg-red-50' ,
217+ ( log . level ?. toLowerCase ( ) === 'warn' || log . level ?. toLowerCase ( ) === 'warning' ) && 'bg-amber-50' ,
190218 ) }
191- </ div >
192-
193- < div className = "mt-1 text-gray-700 font-medium pl-[84px]" >
194- { log . fields ?. message || log . message }
195- </ div >
219+ >
220+ < div className = "flex items-start gap-2" >
221+ < span className = "text-gray-500 shrink-0 font-mono" >
222+ { formatTime ( log . timestamp ) }
223+ </ span >
224+ < Badge
225+ variant = "outline"
226+ className = { LOG_LEVEL_COLORS [ log . level ?. toLowerCase ( ) ] || LOG_LEVEL_COLORS . trace }
227+ >
228+ { log . level ?. toLowerCase ( ) === 'error' && < AlertCircle className = "h-3 w-3 mr-1" /> }
229+ { log . level }
230+ </ Badge >
231+ { log . target && (
232+ < Badge variant = "outline" className = "bg-gray-50 text-gray-700 border-gray-200" >
233+ { log . target }
234+ </ Badge >
235+ ) }
236+ </ div >
196237
197- { log . fields ?. parsedCtx && (
198- < div className = "pl-[84px]" >
199- < ConnectionDetails ctx = { log . fields . parsedCtx } />
238+ < div className = "mt-1 text-gray-700 font-medium pl-[84px]" >
239+ { log . fields ?. message || log . message }
200240 </ div >
201- ) }
202-
203- { /* 显示其他字段信息 */ }
204- { log . fields && Object . entries ( log . fields )
205- . filter ( ( [ key ] ) => ! [ 'message' , 'ctx' , 'parsedCtx' ] . includes ( key ) )
206- . map ( ( [ key , value ] ) => (
207- < div key = { key } className = "pl-[84px] mt-1 text-xs text-gray-500" >
208- < span className = "font-medium" > { key } :</ span > { JSON . stringify ( value ) }
241+
242+ { log . fields ?. parsedCtx && (
243+ < div className = "pl-[84px]" >
244+ < ConnectionDetails ctx = { log . fields . parsedCtx } />
209245 </ div >
210- ) ) }
211- </ div >
212- ) ) }
246+ ) }
247+
248+ { /* 显示其他字段信息 */ }
249+ { log . fields && Object . entries ( log . fields )
250+ . filter ( ( [ key ] ) => ! [ 'message' , 'ctx' , 'parsedCtx' ] . includes ( key ) )
251+ . map ( ( [ key , value ] ) => (
252+ < div key = { key } className = "pl-[84px] mt-1 text-xs text-gray-500" >
253+ < span className = "font-medium" > { key } :</ span > { JSON . stringify ( value ) }
254+ </ div >
255+ ) ) }
256+ </ div >
257+ ) ;
258+ } ) }
213259 </ div >
214260 </ div >
215261 </ CardContent >
0 commit comments