gongxiangp2/components/board/SystemPerformanceCard.tsx
chenpei e864fc3249 fix: 系统性能面板改回浅色,并纠正内存和队列口径
内存改读应用容器上限,不再把整机 90% 当成素材服务快满了。
转码队列改看素材状态和租约,上传停住的会话单独标出。
2026-08-17 13:35:09 +08:00

267 lines
10 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { formatHumanBytes, formatHumanSpeed, formatTransferProgressSummary } from '@/lib/human-readable-transfer'
type UploadTask = {
uploadId?: string
fileName?: string
username?: string
materialType?: string
progress?: number
currentSpeed?: number
uploadedBytes?: number
fileSize?: number
isActive?: boolean
inactiveMs?: number
}
type TranscodeTask = {
materialId?: string
fileName?: string
materialType?: string
authorName?: string
stage?: string | null
progress?: number | null
queuePosition?: number
}
export type SystemPerformanceData = {
cpu?: { usage: number; loadAverage: number[]; cores?: number }
memory?: {
used: number
total: number
percentage: number
usedBytes?: number
totalBytes?: number
source?: 'container' | 'host'
hostUsedBytes?: number
hostTotalBytes?: number
hostPercentage?: number
}
network?: { uploadSpeed: number; downloadSpeed: number }
uploadQueue?: { total: number; processing: UploadTask[]; waiting: UploadTask[] }
transcodeQueue?: { total: number; maxConcurrent: number; processing: TranscodeTask[]; waiting: TranscodeTask[] }
}
const STAGE_LABEL: Record<string, string> = {
preview: '预览视频',
clip: '片段生成',
cover: '封面提取',
}
function formatAgo(ms: number): string {
if (!Number.isFinite(ms) || ms < 0) return '--'
const sec = Math.floor(ms / 1000)
if (sec < 60) return `${sec} 秒`
const min = Math.floor(sec / 60)
if (min < 60) return `${min} 分钟`
const hr = Math.floor(min / 60)
return `${hr} 小时 ${min % 60} 分钟`
}
function Meter({ value, warn = false }: { value: number; warn?: boolean }) {
const width = Math.min(Math.max(value, 0), 100)
return (
<div className="h-1.5 w-full overflow-hidden rounded-full bg-[#e7e8ea]">
<div
className={`h-full rounded-full ${warn ? 'bg-[#8a3b32]' : 'bg-[#1a1c1d]'}`}
style={{ width: `${width}%` }}
/>
</div>
)
}
function QueueSummary({
processing,
waiting,
capacity,
}: {
processing: number
waiting: number
capacity?: number
}) {
if (processing === 0 && waiting === 0) {
return <span className="text-[#6b7075]">空闲</span>
}
const processingText =
typeof capacity === 'number' && capacity > 0 ? `进行中 ${processing} / ${capacity}` : `进行中 ${processing}`
return (
<span>
{processingText}
{waiting > 0 ? ` · 停住 ${waiting}` : ''}
</span>
)
}
export default function SystemPerformanceCard({
data,
loading,
error,
}: {
data: SystemPerformanceData | null
loading: boolean
error: string | null
}) {
const cpu = data?.cpu
const memory = data?.memory
const memoryBytes =
typeof memory?.usedBytes === 'number' && typeof memory?.totalBytes === 'number'
? { used: memory.usedBytes, total: memory.totalBytes }
: memory
? { used: memory.used * 1024 * 1024, total: memory.total * 1024 * 1024 }
: null
const memoryWarn = (memory?.percentage ?? 0) >= 85
const uploadProcessing = data?.uploadQueue?.processing || []
const uploadWaiting = data?.uploadQueue?.waiting || []
const transcodeProcessing = data?.transcodeQueue?.processing || []
const transcodeWaiting = data?.transcodeQueue?.waiting || []
return (
<div
id="system-performance-panel"
role="status"
aria-live="polite"
className="absolute right-0 z-50 mt-2 w-[22rem] rounded-xl border border-[#e7e8ea] bg-white p-4 text-[#1a1c1d]"
>
<div className="mb-3 flex items-baseline justify-between gap-3">
<span className="text-[13px] font-semibold tracking-tight">系统性能</span>
<span className="text-[11px] text-[#6b7075]">
{error ? error : !data && loading ? '正在读取' : '应用容器此刻'}
</span>
</div>
<div className="space-y-3">
<div>
<div className="mb-1 flex justify-between text-[13px]">
<span>CPU</span>
<span>{cpu ? `${cpu.usage}%` : '--'}</span>
</div>
<Meter value={cpu?.usage ?? 0} />
<div className="mt-1 flex justify-between text-[11px] text-[#6b7075]">
<span>{cpu?.cores ? `${cpu.cores} 核 · 1/5/15 分钟负载` : '1/5/15 分钟负载'}</span>
<span>{cpu?.loadAverage?.length ? cpu.loadAverage.map((value) => value.toFixed(2)).join(' / ') : '--'}</span>
</div>
</div>
<div>
<div className="mb-1 flex justify-between text-[13px]">
<span>{memory?.source === 'host' ? '内存 · 整机' : '内存'}</span>
<span>
{memoryBytes
? `${formatHumanBytes(memoryBytes.used)} / ${formatHumanBytes(memoryBytes.total)}(${memory?.percentage}%)`
: '--'}
</span>
</div>
<Meter value={memory?.percentage ?? 0} warn={memoryWarn} />
{memory?.source === 'container' && typeof memory.hostPercentage === 'number' ? (
<div className="mt-1 text-[11px] text-[#6b7075]">
整台机器大约用了 {memory.hostPercentage}%
{typeof memory.hostUsedBytes === 'number' && typeof memory.hostTotalBytes === 'number'
? `(${formatHumanBytes(memory.hostUsedBytes)} / ${formatHumanBytes(memory.hostTotalBytes)})`
: ''}
,主要是存储机自己在用
</div>
) : null}
{memoryWarn ? (
<div className="mt-1 text-[11px] text-[#8a3b32]">这个应用自己的内存已经偏高,继续堆任务容易变慢</div>
) : null}
</div>
<div>
<div className="mb-1 flex justify-between text-[13px]">
<span>进出流量</span>
<span className="text-[11px] text-[#6b7075]">本应用,不是整台机器</span>
</div>
<div className="space-y-1 text-[12px] text-[#6b7075]">
<div className="flex justify-between">
<span>进来(别人上传)</span>
<span>{data?.network ? formatHumanSpeed(data.network.uploadSpeed) : '--'}</span>
</div>
<div className="flex justify-between">
<span>出去(别人下载)</span>
<span>{data?.network ? formatHumanSpeed(data.network.downloadSpeed) : '--'}</span>
</div>
</div>
</div>
</div>
<div className="mt-3 space-y-3 border-t border-[#e7e8ea] pt-3">
<div>
<div className="mb-1 flex justify-between text-[13px]">
<span>上传</span>
<QueueSummary processing={uploadProcessing.length} waiting={uploadWaiting.length} />
</div>
<div className="max-h-32 overflow-y-auto">
{uploadProcessing.map((task, index) => {
const uploadedBytes = task.uploadedBytes || (task.fileSize || 0) * ((task.progress || 0) / 100)
const speedText = task.currentSpeed
? formatTransferProgressSummary({
speedBytesPerSecond: task.currentSpeed,
loadedBytes: uploadedBytes,
totalBytes: task.fileSize,
})
: '速度还在统计'
return (
<div key={task.uploadId || `upload-p-${index}`} className="mt-1.5 rounded-md bg-[#f7f7f8] px-2.5 py-2 text-[12px] text-[#6b7075]">
<div className="truncate font-medium text-[#1a1c1d]">{task.fileName || '未命名文件'}</div>
<div className="mt-1 flex justify-between">
<span>{task.username || '未知用户'}</span>
<span>{task.materialType || '未知类型'}</span>
</div>
<div className="mt-1">{task.progress ?? 0}% · {speedText}</div>
</div>
)
})}
{uploadWaiting.map((task, index) => (
<div key={task.uploadId || `upload-w-${index}`} className="mt-1.5 rounded-md bg-[#f7f7f8] px-2.5 py-2 text-[12px] text-[#6b7075]">
<div className="truncate font-medium text-[#1a1c1d]">{task.fileName || '未命名文件'}</div>
<div className="mt-1 flex justify-between">
<span>{task.username || '未知用户'}</span>
<span>{task.materialType || '未知类型'}</span>
</div>
<div className="mt-1">已经停住 {formatAgo(task.inactiveMs || 0)}</div>
</div>
))}
</div>
</div>
<div>
<div className="mb-1 flex justify-between text-[13px]">
<span>转码</span>
<QueueSummary
processing={transcodeProcessing.length}
waiting={transcodeWaiting.length}
capacity={data?.transcodeQueue?.maxConcurrent}
/>
</div>
<div className="max-h-32 overflow-y-auto">
{transcodeProcessing.map((task, index) => (
<div key={task.materialId || `transcode-p-${index}`} className="mt-1.5 rounded-md bg-[#f7f7f8] px-2.5 py-2 text-[12px] text-[#6b7075]">
<div className="truncate font-medium text-[#1a1c1d]">{task.fileName || '未命名文件'}</div>
<div className="mt-1 flex justify-between">
<span>
{task.materialType || '未知类型'}
{task.stage ? ` · ${STAGE_LABEL[task.stage] || task.stage}` : ''}
</span>
<span>{task.progress ?? 0}%</span>
</div>
<div className="mt-1">上传人 {task.authorName || '未知用户'}</div>
</div>
))}
{transcodeWaiting.map((task, index) => (
<div key={task.materialId || `transcode-w-${index}`} className="mt-1.5 rounded-md bg-[#f7f7f8] px-2.5 py-2 text-[12px] text-[#6b7075]">
<div className="truncate font-medium text-[#1a1c1d]">{task.fileName || '未命名文件'}</div>
<div className="mt-1 flex justify-between">
<span>{task.materialType || '未知类型'}</span>
<span>排队 #{task.queuePosition || index + 1}</span>
</div>
<div className="mt-1">上传人 {task.authorName || '未知用户'}</div>
</div>
))}
</div>
</div>
</div>
</div>
)
}