Files
aiops/frontend/src/pages/monitor/host-monitor/HostTimeRangePicker.tsx
T
attacker d21eac9117 refactor(i18n): U24 Monitor 模块 i18n hook 迁移 — 26 个 .tsx 文件从全局 i18n.t() 迁移到 useTranslation() hook + 89 个 phantom 中文文本键替换为 monitor.lbl.* 语义化键
- 新增 monitor.lbl 翻译子命名空间 89 键(HostMonitor/Middleware/Site/SNMP/Shared)
- 26 .tsx: import i18n → useTranslation() hook + i18n.t('中文') → t('monitor.lbl.semanticKey')
- 模块级代码(TABS/METRIC_CATEGORIES)保留 i18n.t() + 语义化键
- 1 .ts helpers 文件 phantom 键替换
- zh/en 6886→6975 +89 键对齐
- tsc -b=0, npm build✓ (17.16s), ESLint=0/53/0, go build=0, Docker healthy
2026-07-11 20:15:44 +08:00

128 lines
5.1 KiB
TypeScript

// 主机监控时间范围选择器 — 预设 + 自定义 // Logan.Li
import { useState, useRef, useEffect } from 'react';
import { Calendar, ChevronDown, Timer } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
TIME_RANGE_PRESETS,
type HostMonitorTimeRange,
type TimeRangePresetId,
} from './types';
import { presetToRange, toDatetimeLocalValue, formatRangeSummary } from './timeRange';
import { useTranslation } from 'react-i18next';
interface Props {
value: HostMonitorTimeRange;
onChange: (range: HostMonitorTimeRange) => void;
}
export function HostTimeRangePicker({ value, onChange }: Props) {
const { t } = useTranslation();
const [customOpen, setCustomOpen] = useState(false);
const now = new Date();
const [customStart, setCustomStart] = useState(toDatetimeLocalValue(new Date(now.getTime() - 3600_000)));
const [customEnd, setCustomEnd] = useState(toDatetimeLocalValue(now));
const popRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handler = (e: MouseEvent) => {
if (popRef.current && !popRef.current.contains(e.target as Node)) setCustomOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, []);
const selectPreset = (id: TimeRangePresetId) => {
if (id === 'custom') return;
onChange(presetToRange(id));
setCustomOpen(false);
};
const applyCustom = () => {
const start = new Date(customStart);
const end = new Date(customEnd);
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return;
const s = start.getTime() <= end.getTime() ? start : end;
const e = start.getTime() <= end.getTime() ? end : start;
onChange({
preset: 'custom',
label: t('aiops.providerCustom'),
start: s.toISOString(),
end: e.toISOString(),
buckets: Math.min(120, Math.max(36, Math.ceil((e.getTime() - s.getTime()) / 60_000 / 2))),
});
setCustomOpen(false);
};
return (
<div className="flex items-center gap-2 flex-wrap">
<span className="text-[11px] dg-text-muted hidden sm:inline-flex items-center gap-1">
<Timer className="h-3 w-3" />
时间范围
</span>
<div className="flex items-center gap-1 rounded-md border border-[var(--dg-border)] p-0.5 bg-[var(--dg-bg-secondary)] flex-wrap">
{TIME_RANGE_PRESETS.map((p) => (
<Button
key={p.id}
type="button"
variant="ghost"
size="sm"
className={`h-6 text-[11px] px-2 rounded ${value.preset === p.id ? 'dg-btn-primary shadow-sm' : 'dg-text-secondary hover:dg-text-primary'}`}
onClick={() => selectPreset(p.id)}
>
{p.label}
</Button>
))}
<div className="relative" ref={popRef}>
<Button
type="button"
variant="ghost"
size="sm"
className={`h-6 text-[11px] px-2 rounded gap-1 ${value.preset === 'custom' ? 'dg-btn-primary shadow-sm' : 'dg-text-secondary hover:dg-text-primary'}`}
onClick={() => setCustomOpen((v) => !v)}
>
<Calendar className="h-3 w-3" />
自定义
<ChevronDown className={`h-3 w-3 transition-transform ${customOpen ? 'rotate-180' : ''}`} />
</Button>
{customOpen && (
<div className="absolute right-0 top-full mt-1 z-50 w-72 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-primary)] p-3 shadow-lg">
<p className="text-xs font-medium text-[var(--dg-text-primary)] mb-2"> {t('monitor.lbl.unlimited')} </p>
<div className="space-y-2">
<div>
<label className="text-[10px] dg-text-muted"> {t('common.start')} </label>
<Input
type="datetime-local"
value={customStart}
onChange={(e) => setCustomStart(e.target.value)}
className="h-8 text-xs dg-input mt-0.5"
/>
</div>
<div>
<label className="text-[10px] dg-text-muted"> {t('monitor.lbl.unlimited')} </label>
<Input
type="datetime-local"
value={customEnd}
onChange={(e) => setCustomEnd(e.target.value)}
className="h-8 text-xs dg-input mt-0.5"
/>
</div>
</div>
<div className="flex justify-end gap-2 mt-3">
<Button type="button" variant="ghost" size="sm" className="h-7 text-xs" onClick={() => setCustomOpen(false)}>
取消
</Button>
<Button type="button" size="sm" className="h-7 text-xs dg-btn-primary" onClick={applyCustom}>
应用
</Button>
</div>
</div>
)}
</div>
</div>
<span className="text-[10px] dg-text-muted tabular-nums hidden md:inline">
{formatRangeSummary(value)}
</span>
</div>
);
}