日志是「来一条 push 一条」,store 里的数组每来一条就是新的,于是整个列表重渲染; 而列表最多留 500 条,不做 memo 就是每来一行重渲染 500 行,每行还要跑 5 次 cn() (tailwind-merge 不便宜)。爬虫一跑日志是成片来的,主线程直接被占满 —— 表现就是 点什么都没反应,点折叠最容易注意到,因为那一下还要把整份列表挂载/卸载一遍。 log 对象在数组里是稳定引用,memo 之后只有新来的那一行会渲染。
84 lines
2.4 KiB
TypeScript
84 lines
2.4 KiB
TypeScript
import { memo } from 'react'
|
||
|
||
import { cn } from '@/lib/utils'
|
||
import type { LogEntry } from '@/types/crawler'
|
||
|
||
interface TerminalLineProps {
|
||
log: LogEntry
|
||
}
|
||
|
||
const levelConfig: Record<string, { text: string; bg: string; glow: string }> = {
|
||
info: {
|
||
text: 'text-cyber-neon-cyan',
|
||
bg: 'bg-cyber-neon-cyan/10',
|
||
glow: 'shadow-[0_0_3px_rgba(0,255,255,0.3)]'
|
||
},
|
||
success: {
|
||
text: 'text-cyber-neon-green',
|
||
bg: 'bg-cyber-neon-green/10',
|
||
glow: 'shadow-[0_0_3px_rgba(0,255,65,0.3)]'
|
||
},
|
||
warning: {
|
||
text: 'text-cyber-neon-orange',
|
||
bg: 'bg-cyber-neon-orange/10',
|
||
glow: 'shadow-[0_0_3px_rgba(255,152,0,0.3)]'
|
||
},
|
||
error: {
|
||
text: 'text-cyber-neon-pink',
|
||
bg: 'bg-cyber-neon-pink/10',
|
||
glow: 'shadow-[0_0_3px_rgba(255,0,128,0.3)]'
|
||
},
|
||
debug: {
|
||
text: 'text-[#8b949e]',
|
||
bg: 'bg-[#21262d]',
|
||
glow: ''
|
||
},
|
||
}
|
||
|
||
const levelIcons: Record<string, string> = {
|
||
info: 'DATA',
|
||
success: 'OK',
|
||
warning: 'WARN',
|
||
error: 'ERR',
|
||
debug: 'DBG',
|
||
}
|
||
|
||
/**
|
||
* 一行日志。
|
||
*
|
||
* **必须 memo。** 日志是「来一条就 push 一条」,每来一条 store 里的数组就是新的,
|
||
* 于是整个列表重渲染;而这个列表最多留 500 条 —— 不做 memo 就是每来一行重渲染 500 行,
|
||
* 每行还要跑 5 次 `cn()`(tailwind-merge 不便宜)。爬虫在跑的时候日志是成片来的,
|
||
* 主线程直接被占满,表现就是**点什么都没反应**(点折叠最容易注意到,因为那一下还要
|
||
* 把整份列表挂载/卸载一遍)。
|
||
*
|
||
* `log` 对象在数组里是稳定引用,所以 memo 之后只有新来的那一行会渲染。
|
||
*/
|
||
export const TerminalLine = memo(function TerminalLine({ log }: TerminalLineProps) {
|
||
const config = levelConfig[log.level] || levelConfig.info
|
||
|
||
return (
|
||
<div className="flex gap-2 text-xs leading-relaxed font-mono group hover:bg-[#21262d]/50 px-1 -mx-1 rounded transition-colors">
|
||
{/* Timestamp */}
|
||
<span className="text-[#8b949e] flex-shrink-0 opacity-60 group-hover:opacity-100 transition-opacity">
|
||
[{log.timestamp}]
|
||
</span>
|
||
|
||
{/* Level badge */}
|
||
<span className={cn(
|
||
'flex-shrink-0 w-14 px-1 rounded text-center',
|
||
config.bg,
|
||
config.text,
|
||
config.glow
|
||
)}>
|
||
[{levelIcons[log.level]}]
|
||
</span>
|
||
|
||
{/* Message */}
|
||
<span className={cn('break-all', config.text)}>
|
||
{log.message}
|
||
</span>
|
||
</div>
|
||
)
|
||
})
|