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");
}