import { useEffect, useMemo, useRef, useState } from "react"; import CalendarMonthOutlined from "@mui/icons-material/CalendarMonthOutlined"; import KeyboardArrowDownOutlined from "@mui/icons-material/KeyboardArrowDownOutlined"; import PublicOutlined from "@mui/icons-material/PublicOutlined"; import { getTimeZoneLabel, lastDaysRange, rangeEndMs, rangeStartMs, sameRange, thisMonthRange, TIME_ZONE_OPTIONS, todayRange } from "../utils/time.js"; const defaultCountryOptions = [{ countryCode: "", id: 0, label: "全部国家", regionIds: [], searchText: "全部国家 all", value: 0 }]; const defaultRegionOptions = [{ countryCodes: [], id: "all", label: "全部区域", value: "all" }]; const rangeOptions = [ { label: "今日", value: "today", getRange: (timeZone) => todayRange(timeZone) }, { label: "近 7 日", value: "7d", getRange: (timeZone) => lastDaysRange(7, timeZone) }, { label: "近 14 日", value: "14d", getRange: (timeZone) => lastDaysRange(14, timeZone) }, { label: "近 30 日", value: "30d", getRange: (timeZone) => lastDaysRange(30, timeZone) }, { label: "本月", value: "month", getRange: (timeZone) => thisMonthRange(timeZone) } ]; const weekLabels = ["一", "二", "三", "四", "五", "六", "日"]; const hours = Array.from({ length: 24 }, (_, index) => pad2(index)); const minutes = Array.from({ length: 60 }, (_, index) => pad2(index)); const seconds = minutes; export function DatabiHeader({ activeScreen = "overview", appCode, appOptions = [], countryId, countryOptions = defaultCountryOptions, error = "", filtersLoading = false, loading = false, onScreenChange, onAppChange, onCountryChange, onRangeChange, onRegionChange, onTimeZoneChange, onViewModeChange, range, regionId, regionOptions = defaultRegionOptions, timeZone, timeZoneOptions = TIME_ZONE_OPTIONS, viewMode = "report" }) { const [openControl, setOpenControl] = useState(""); const headerRef = useRef(null); const appLabel = appOptions.find((item) => item.code === appCode || item.value === appCode)?.label || appCode; const regionLabel = regionOptions.find((item) => String(item.id) === String(regionId) || String(item.value) === String(regionId))?.label || "全部区域"; const timeZoneLabel = getTimeZoneLabel(timeZone); const filteredCountries = useMemo(() => { if (regionId === "all" || !regionOptions.some((item) => item.countryCodes?.length)) { return countryOptions; } return countryOptions.filter((item) => Number(item.id) === 0 || item.regionIds?.includes(String(regionId)) || item.regionId === regionId); }, [countryOptions, regionId, regionOptions]); const countryLabel = filteredCountries.find((item) => Number(item.id) === Number(countryId))?.label || "全部国家"; const activeRange = rangeOptions.find((item) => { const next = item.getRange(timeZone); return sameRange(next, range); }); useEffect(() => { const closeOnOutsideClick = (event) => { if (!headerRef.current?.contains(event.target)) { setOpenControl(""); } }; document.addEventListener("mousedown", closeOnOutsideClick); return () => document.removeEventListener("mousedown", closeOnOutsideClick); }, []); const toggleControl = (name) => { setOpenControl((current) => (current === name ? "" : name)); }; const selectItem = (callback, value) => { callback(value); setOpenControl(""); }; return (

数据中心

TZ} isOpen={openControl === "timezone"} label="时区" onSelect={(value) => selectItem(onTimeZoneChange, value)} onToggle={() => toggleControl("timezone")} options={timeZoneOptions} value={timeZone} valueLabel={timeZoneLabel} /> APP} isOpen={openControl === "app"} label="App" loading={filtersLoading} onSelect={(value) => selectItem(onAppChange, value)} onToggle={() => toggleControl("app")} options={appOptions.map((item) => ({ label: item.label, value: item.code }))} value={appCode} valueLabel={appLabel} /> selectItem(onRangeChange, nextRange)} onToggle={() => toggleControl("range")} range={range} timeZone={timeZone} /> } isOpen={openControl === "region"} label="区域" loading={filtersLoading} onSelect={(value) => selectItem(onRegionChange, value)} onToggle={() => toggleControl("region")} options={regionOptions} value={regionId} valueLabel={regionLabel} /> CN} isOpen={openControl === "country"} label="国家" loading={filtersLoading} onSelect={(value) => selectItem(onCountryChange, Number(value))} onToggle={() => toggleControl("country")} options={filteredCountries} value={countryId} valueLabel={countryLabel} /> {error ? "异常" : loading ? "刷新中" : "实时"} {error ? {error} : null}
); } function FilterMenu({ className = "", icon, isOpen, label, loading = false, onSelect, onToggle, options, value, valueLabel }) { return (
{isOpen ? (
{loading ? 加载中... : null} {!loading && !options.length ? 暂无选项 : null} {!loading ? options.map((item) => ( )) : null}
) : null}
); } function SearchableFilterMenu({ icon, isOpen, label, loading = false, onSelect, onToggle, options, value, valueLabel }) { const [query, setQuery] = useState(""); const filteredOptions = useMemo(() => { const keyword = query.trim().toLowerCase(); if (!keyword) { return options; } return options.filter((item) => `${item.label || ""} ${item.countryCode || ""} ${item.searchText || ""}`.toLowerCase().includes(keyword)); }, [options, query]); useEffect(() => { if (isOpen) { setQuery(""); } }, [isOpen]); return (
{isOpen ? (
{loading ? 加载中... : null} {!loading && !filteredOptions.length ? 没有匹配国家 : null} {!loading && filteredOptions.length ? (
{filteredOptions.map((item) => ( ))}
) : null}
) : null}
); } function RangeMenu({ activeValue, isOpen, onSelect, onToggle, range, timeZone }) { const options = rangeOptions.map((item) => ({ ...item, range: item.getRange(timeZone) })); const activeOption = options.find((item) => item.value === activeValue); const [draft, setDraft] = useState(() => normalizeRange(range)); const [activeField, setActiveField] = useState("start"); const [viewMonth, setViewMonth] = useState(() => monthDate(range.start)); const [error, setError] = useState(""); const calendarDays = useMemo(() => createCalendarDays(viewMonth), [viewMonth]); const activeTime = splitTime(draft[`${activeField}Time`]); useEffect(() => { if (isOpen) { const normalized = normalizeRange(range); setDraft(normalized); setActiveField("start"); setViewMonth(monthDate(normalized.start)); setError(""); } }, [isOpen, range]); const changeMonth = (offset) => { setViewMonth((current) => new Date(current.getFullYear(), current.getMonth() + offset, 1)); }; const changeYear = (offset) => { setViewMonth((current) => new Date(current.getFullYear() + offset, current.getMonth(), 1)); }; const selectQuickRange = (nextRange) => { const normalized = normalizeRange(nextRange); setDraft(normalized); setActiveField("start"); setViewMonth(monthDate(normalized.start)); setError(""); }; const activateField = (field) => { setActiveField(field); setViewMonth(monthDate(draft[field])); setError(""); }; const selectDate = (date) => { const selectedValue = dateValue(date); setDraft((current) => ({ ...current, [activeField]: selectedValue })); if (activeField === "start") { setActiveField("end"); setViewMonth(monthDate(selectedValue)); } setError(""); }; const selectTime = (part, value) => { setDraft((current) => ({ ...current, [`${activeField}Time`]: updateTimePart(current[`${activeField}Time`], part, value) })); setError(""); }; const applyDraft = () => { const normalized = normalizeRange(draft); if (rangeStartMs(normalized, timeZone) > rangeEndMs(normalized, timeZone)) { setError("结束时间必须晚于开始时间"); return; } onSelect(normalized); }; return (
{isOpen ? (
{options.map((item) => ( ))}
{viewMonth.getFullYear()}年 {viewMonth.getMonth() + 1}月
{weekLabels.map((label) => ( {label} ))} {calendarDays.map((date) => { const value = dateValue(date); const className = [ "calendar-day", date.getMonth() !== viewMonth.getMonth() ? "is-muted" : "", value === draft.start ? "is-start" : "", value === draft.end ? "is-end" : "", isBetween(value, draft.start, draft.end) ? "is-between" : "", value === draft[activeField] ? "is-active" : "" ].filter(Boolean).join(" "); return ( ); })}
selectTime("hour", value)} options={hours} value={activeTime.hour} /> selectTime("minute", value)} options={minutes} value={activeTime.minute} /> selectTime("second", value)} options={seconds} value={activeTime.second} />
{error ? {error} : {formatRange(draft)}}
) : null}
); } function TimeColumn({ label, onSelect, options, value }) { return (
{label}
{options.map((item) => ( ))}
); } function dateValue(date) { return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`; } function formatRange(range) { return `${formatDateTimePart(range.start, range.startTime)} ~ ${formatDateTimePart(range.end, range.endTime)}`; } function formatDateTimePart(date, time) { return `${date} ${normalizeTime(time)}`; } function normalizeRange(range) { return { end: range?.end || dateValue(new Date()), endTime: normalizeTime(range?.endTime || "23:59:59"), start: range?.start || dateValue(new Date()), startTime: normalizeTime(range?.startTime || "00:00:00") }; } function monthDate(value) { const [year, month] = String(value || dateValue(new Date())).split("-").map(Number); return new Date(year || new Date().getFullYear(), (month || 1) - 1, 1); } function createCalendarDays(viewMonth) { const firstDay = new Date(viewMonth.getFullYear(), viewMonth.getMonth(), 1); const mondayIndex = (firstDay.getDay() + 6) % 7; const start = new Date(firstDay); start.setDate(firstDay.getDate() - mondayIndex); return Array.from({ length: 42 }, (_, index) => { const date = new Date(start); date.setDate(start.getDate() + index); return date; }); } function isBetween(value, start, end) { return value > start && value < end; } function splitTime(value) { const [hour, minute, second] = normalizeTime(value).split(":"); return { hour, minute, second }; } function updateTimePart(value, part, nextValue) { const parts = splitTime(value); return [part === "hour" ? nextValue : parts.hour, part === "minute" ? nextValue : parts.minute, part === "second" ? nextValue : parts.second].join(":"); } function normalizeTime(value) { const [hour = "00", minute = "00", second = "00"] = String(value || "00:00:00").split(":"); return `${pad2(hour)}:${pad2(minute)}:${pad2(second)}`; } function pad2(value) { return String(value).padStart(2, "0"); }