import { ChevronLeft, ChevronRight } from 'lucide-react' import { useMemo } from 'react' import { Segmented } from '@shared/ui' const MIN = 60_000 const DAY = 24 * 60 * MIN /** 线路详情页的时间范围:只提供这条线路“有数据”的档位。 */ export const PRESETS = [ { value: '5m', label: '5 分钟', ms: 5 * MIN }, { value: '15m', label: '15 分钟', ms: 15 * MIN }, { value: '30m', label: '30 分钟', ms: 30 * MIN }, { value: '1d', label: '1 天', ms: DAY }, { value: '3d', label: '3 天', ms: 3 * DAY }, { value: '7d', label: '7 天', ms: 7 * DAY }, { value: '14d', label: '14 天', ms: 14 * DAY }, { value: '30d', label: '一个月', ms: 30 * DAY }, ] as const export type PresetValue = (typeof PRESETS)[number]['value'] /** 进入某个月之后的切片:整月,或者从月初起按固定长度翻页。 */ export const SLICES = [ { value: 'all', label: '整月', ms: 0 }, { value: '14d', label: '14 天', ms: 14 * DAY }, { value: '7d', label: '7 天', ms: 7 * DAY }, { value: '3d', label: '3 天', ms: 3 * DAY }, { value: '1d', label: '1 天', ms: DAY }, ] as const export type SliceValue = (typeof SLICES)[number]['value'] /** 数据不足整档时允许 10% 的误差:29 分钟的数据算得上“30 分钟”。至少保留最小的一档。 */ const TOLERANCE = 0.9 export function availablePresets(spanMs: number | null) { if (spanMs === null) return [PRESETS[0]] const list = PRESETS.filter((p) => spanMs >= p.ms * TOLERANCE) return list.length > 0 ? list : [PRESETS[0]] } /** 默认选“有数据的最大档”,但最长到 1 天(再长的范围图会很挤,用户可以自己点)。 */ export function defaultPreset(spanMs: number | null): PresetValue { const list = availablePresets(spanMs).filter((p) => p.ms <= DAY) return list[list.length - 1]?.value ?? '5m' } export interface MonthInfo { key: string // YYYY-MM label: string start: number end: number // 不含 } const pad = (n: number) => String(n).padStart(2, '0') export function monthOf(ms: number): MonthInfo { const d = new Date(ms) const start = new Date(d.getFullYear(), d.getMonth(), 1).getTime() const end = new Date(d.getFullYear(), d.getMonth() + 1, 1).getTime() return { key: `${d.getFullYear()}-${pad(d.getMonth() + 1)}`, label: `${d.getFullYear()} 年 ${d.getMonth() + 1} 月`, start, end } } /** 从有数据的第一个月到当前月,新的在前。 */ export function monthsWithData(sinceMs: number | null, nowMs: number): MonthInfo[] { if (sinceMs === null) return [] const months: MonthInfo[] = [] let cursor = monthOf(sinceMs) const last = monthOf(nowMs) while (cursor.start <= last.start) { months.push(cursor) cursor = monthOf(cursor.end) } return months.reverse() } export interface RangeState { /** 没有 month 就是“最近一段”。 */ preset: PresetValue month: string | null slice: SliceValue page: number } export interface ResolvedRange { query: { range?: string; from?: string; to?: string; anchor?: string } /** 主机历史只支持 ≥ 1 小时的 range,这里给一个对应的值。 */ hostRange: string label: string } const fmtDay = (ms: number) => { const d = new Date(ms) return `${d.getMonth() + 1}/${d.getDate()}` } export function resolveRange(state: RangeState, months: MonthInfo[], nowMs: number): ResolvedRange { if (!state.month) { const hostRange = state.preset === '5m' || state.preset === '15m' || state.preset === '30m' ? '1h' : state.preset return { query: { range: state.preset, anchor: 'latest' }, hostRange, label: PRESETS.find((p) => p.value === state.preset)?.label ?? state.preset } } const month = months.find((m) => m.key === state.month) ?? monthOf(nowMs) const slice = SLICES.find((s) => s.value === state.slice) ?? SLICES[0] const end = Math.min(month.end, nowMs) let from = month.start let to = end if (slice.ms > 0) { from = month.start + state.page * slice.ms to = Math.min(from + slice.ms, end) } const spanDays = Math.max(1, Math.round((to - from) / DAY)) return { query: { from: new Date(from).toISOString(), to: new Date(to).toISOString() }, hostRange: spanDays >= 30 ? '30d' : `${Math.max(1, spanDays)}d`, label: slice.ms > 0 ? `${month.label} ${fmtDay(from)} – ${fmtDay(to - 1)}` : month.label, } } /** 一个月里按固定长度能翻几页。 */ function pagesIn(month: MonthInfo, sliceMs: number, nowMs: number, sinceMs: number | null): { first: number; last: number } { const start = Math.max(month.start, sinceMs === null ? month.start : sinceMs - (sinceMs % DAY)) const end = Math.min(month.end, nowMs) return { first: Math.max(0, Math.floor((start - month.start) / sliceMs)), last: Math.max(0, Math.ceil((end - month.start) / sliceMs) - 1) } } interface Props { state: RangeState onChange: (next: RangeState) => void sinceMs: number | null nowMs: number } export function RangePicker({ state, onChange, sinceMs, nowMs }: Props) { const spanMs = sinceMs === null ? null : nowMs - sinceMs const presets = useMemo(() => availablePresets(spanMs), [spanMs]) const months = useMemo(() => monthsWithData(sinceMs, nowMs), [sinceMs, nowMs]) const month = state.month ? months.find((m) => m.key === state.month) : undefined const monthLength = month ? Math.min(month.end, nowMs) - Math.max(month.start, sinceMs ?? month.start) : 0 const slices = SLICES.filter((s) => s.ms === 0 || monthLength >= s.ms * TOLERANCE) const slice = SLICES.find((s) => s.value === state.slice) ?? SLICES[0] const pages = month && slice.ms > 0 ? pagesIn(month, slice.ms, nowMs, sinceMs) : undefined return (