Files
aiops/frontend/src/components/monitor/log-center/LogQueryToolbar.tsx
T

175 lines
7.0 KiB
TypeScript

// LogQueryToolbar.tsx — 查询工具栏(统计条 + 分页 + 操作)// Logan.Li
import { ArrowUpDown, Bell, Download, Copy, ChevronLeft, ChevronRight } from 'lucide-react';
import { useState } from 'react';
interface Props {
total: number;
queryTime?: number;
status?: 'precise' | 'partial' | 'failed';
perPage: number;
onPerPageChange: (n: number) => void;
page: number;
totalPages: number;
onPageChange: (p: number) => void;
sortDirection?: 'asc' | 'desc';
onSortToggle?: () => void;
onExport?: () => void;
onCopy?: () => void;
onBellClick?: () => void;
hasAlertRule?: boolean;
}
const STATUS_LABEL: Record<string, { text: string; color: string }> = {
precise: { text: '结果精确', color: '#22c55e' },
partial: { text: '部分命中', color: '#f59e0b' },
failed: { text: '查询失败', color: '#ef4444' },
};
export function LogQueryToolbar({
total, queryTime, status = 'precise', perPage, onPerPageChange,
page, totalPages, onPageChange, sortDirection, onSortToggle,
onExport, onCopy, onBellClick, hasAlertRule,
}: Props) {
const [jumpPage, setJumpPage] = useState('');
const st = STATUS_LABEL[status] || STATUS_LABEL.precise;
const handleJump = () => {
const p = parseInt(jumpPage, 10);
if (p >= 1 && p <= totalPages) onPageChange(p);
setJumpPage('');
};
return (
<div className="flex flex-wrap items-center gap-3 py-2 px-3 border rounded-lg" style={{ background: 'var(--dg-bg-secondary)', borderColor: 'var(--dg-border)' }}>
{/* 左侧统计 */}
<div className="flex items-center gap-3">
<span className="text-base font-semibold" style={{ color: 'var(--dg-text-primary)' }}>
日志条数: {total.toLocaleString()}
</span>
<span
className="text-[10px] px-1.5 py-0.5 rounded font-medium"
style={{ backgroundColor: `${st.color}15`, color: st.color }}
>
{st.text}
</span>
{queryTime !== undefined && (
<span className="text-xs" style={{ color: 'var(--dg-text-muted)' }}>耗时: {queryTime}ms</span>
)}
</div>
{/* 分页 */}
<div className="flex items-center gap-1.5 ml-auto">
<select
value={perPage}
onChange={e => onPerPageChange(Number(e.target.value))}
className="h-7 px-1.5 text-[11px] rounded dg-input"
style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }}
>
{[10, 20, 50, 100, 200].map(n => <option key={n} value={n}>每页 {n}</option>)}
</select>
<span className="text-[11px] mx-1" style={{ color: 'var(--dg-text-muted)' }}>
总数: {total.toLocaleString()} 条
</span>
<button
onClick={() => onPageChange(Math.max(1, page - 1))}
disabled={page <= 1}
className="h-7 w-7 flex items-center justify-center rounded border disabled:opacity-30 transition-colors"
style={{ borderColor: 'var(--dg-border)', color: 'var(--dg-text-secondary)', background: 'var(--dg-bg-tertiary)' }}
>
<ChevronLeft className="h-3.5 w-3.5" />
</button>
{/* 页码 */}
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let p: number;
if (totalPages <= 5) p = i + 1;
else if (page <= 3) p = i + 1;
else if (page >= totalPages - 2) p = totalPages - 4 + i;
else p = page - 2 + i;
return (
<button
key={p}
onClick={() => onPageChange(p)}
className="h-7 min-w-[28px] px-1.5 text-[11px] rounded border transition-colors"
style={{
borderColor: p === page ? 'var(--dg-blue)' : 'var(--dg-border)',
background: p === page ? 'var(--dg-blue)' : 'var(--dg-bg-tertiary)',
color: p === page ? '#fff' : 'var(--dg-text-secondary)',
}}
>
{p}
</button>
);
})}
{totalPages > 5 && page < totalPages - 2 && (
<span className="text-[11px] px-1" style={{ color: 'var(--dg-text-muted)' }}>...</span>
)}
<button
onClick={() => onPageChange(Math.min(totalPages, page + 1))}
disabled={page >= totalPages}
className="h-7 w-7 flex items-center justify-center rounded border disabled:opacity-30 transition-colors"
style={{ borderColor: 'var(--dg-border)', color: 'var(--dg-text-secondary)', background: 'var(--dg-bg-tertiary)' }}
>
<ChevronRight className="h-3.5 w-3.5" />
</button>
<div className="flex items-center gap-1 ml-1">
<span className="text-[10px]" style={{ color: 'var(--dg-text-muted)' }}>到第</span>
<input
type="number"
value={jumpPage}
onChange={e => setJumpPage(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleJump()}
className="h-7 w-10 text-center text-[11px] rounded dg-input"
style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }}
/>
<span className="text-[10px]" style={{ color: 'var(--dg-text-muted)' }}>页</span>
</div>
</div>
{/* 右侧操作 */}
<div className="flex items-center gap-1">
<button
onClick={onSortToggle}
className="h-7 w-7 flex items-center justify-center rounded border transition-colors"
style={{ borderColor: 'var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }}
title={sortDirection === 'desc' ? '降序' : '升序'}
>
<ArrowUpDown className="h-3.5 w-3.5" />
</button>
<button
onClick={onExport}
className="h-7 w-7 flex items-center justify-center rounded border transition-colors"
style={{ borderColor: 'var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }}
title="导出"
>
<Download className="h-3.5 w-3.5" />
</button>
<button
onClick={onCopy}
className="h-7 w-7 flex items-center justify-center rounded border transition-colors"
style={{ borderColor: 'var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }}
title="复制"
>
<Copy className="h-3.5 w-3.5" />
</button>
<button
onClick={onBellClick}
className="relative h-7 w-7 flex items-center justify-center rounded border transition-colors"
style={{ borderColor: 'var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }}
title="关联告警"
>
<Bell className="h-3.5 w-3.5" />
{hasAlertRule && (
<span className="absolute -top-1 -right-1 h-2.5 w-2.5 rounded-full bg-red-500 ring-1" style={{ outline: '2px solid var(--dg-bg-secondary)' }} />
)}
</button>
</div>
</div>
);
}