import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { fetchFilterOptions, fetchSelfGameStatisticsOverview, fetchStatisticsOverview, getCurrentAppCode, setCurrentAppCode } from "./api.js"; import { CountryPerformanceTable } from "./components/CountryPerformanceTable.jsx"; import { DatabiHeader } from "./components/DatabiHeader.jsx"; import { FunnelPanel } from "./components/FunnelPanel.jsx"; import { GlobalOverviewPanel } from "./components/GlobalOverviewPanel.jsx"; import { LuckyGiftPoolModal } from "./components/LuckyGiftPoolModal.jsx"; import { MetricCard } from "./components/MetricCard.jsx"; import { MetricTrendModal } from "./components/MetricTrendModal.jsx"; import { RevenueTrendPanel } from "./components/RevenueTrendPanel.jsx"; import { SelfGameScreen } from "./components/SelfGameScreen.jsx"; import { createDashboardModel } from "./data/createDashboardModel.js"; import { lastDaysRange, rangeEndMs, rangeStartMs, readStoredTimeZone, sameRange, thisMonthRange, TIME_ZONE_OPTIONS, todayRange, writeStoredTimeZone } from "./utils/time.js"; const initialFilterOptions = { appOptions: [], countryOptions: [{ countryCode: "", id: 0, label: "全部国家", regionIds: [], searchText: "全部国家 all", value: 0 }], regionOptions: [{ countryCodes: [], id: "all", label: "全部区域", value: "all" }] }; export function DatabiApp() { const initialTimeZone = useMemo(() => readStoredTimeZone(), []); const initialRange = useMemo(() => todayRange(initialTimeZone), [initialTimeZone]); const [timeZone, setTimeZone] = useState(initialTimeZone); const [range, setRange] = useState(initialRange); const [countryId, setCountryId] = useState(0); const [regionId, setRegionId] = useState("all"); const [appCode, setAppCode] = useState(() => getCurrentAppCode()); const [activeScreen, setActiveScreen] = useState("overview"); const [gameId, setGameId] = useState("all"); const [filterOptions, setFilterOptions] = useState(initialFilterOptions); const [filtersLoading, setFiltersLoading] = useState(false); const [overview, setOverview] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [selfGameOverview, setSelfGameOverview] = useState(null); const [selfGameLoading, setSelfGameLoading] = useState(false); const [selfGameError, setSelfGameError] = useState(""); const [poolModalOpen, setPoolModalOpen] = useState(false); const [trendModalItem, setTrendModalItem] = useState(null); const overviewRequestIdRef = useRef(0); const selfGameRequestIdRef = useRef(0); const overviewRef = useRef(null); const selfGameOverviewRef = useRef(null); const scopedCountries = useMemo(() => { return filterOptions.countryOptions.filter((country) => Number(country.id) > 0 && countryBelongsToRegion(country, regionId)); }, [filterOptions.countryOptions, regionId]); const handleAppChange = useCallback((value) => { setAppCode(setCurrentAppCode(value)); }, []); const handleRegionChange = useCallback((value) => { setRegionId(String(value || "all")); setCountryId(0); }, []); const handleTimeZoneChange = useCallback((value) => { const nextTimeZone = writeStoredTimeZone(value); setRange((currentRange) => { const currentPreset = resolveRangePresetValue(currentRange, timeZone); return currentPreset ? rangeForPreset(currentPreset, nextTimeZone) : currentRange; }); setTimeZone(nextTimeZone); }, [timeZone]); const loadFilterOptions = useCallback(async () => { setFiltersLoading(true); try { const options = await fetchFilterOptions({ appCode }); setFilterOptions(options); const nextAppCode = resolveSelectedAppCode(options.appOptions, appCode); if (nextAppCode !== appCode) { setAppCode(setCurrentAppCode(nextAppCode)); } setRegionId((current) => (options.regionOptions.some((item) => String(item.id) === String(current)) ? current : "all")); setCountryId((current) => (options.countryOptions.some((item) => Number(item.id) === Number(current)) ? current : 0)); } catch (err) { setError(err.message || "筛选项加载失败"); } finally { setFiltersLoading(false); } }, [appCode]); useEffect(() => { overviewRef.current = overview; }, [overview]); useEffect(() => { selfGameOverviewRef.current = selfGameOverview; }, [selfGameOverview]); const loadOverview = useCallback(async ({ preserveData = false } = {}) => { const requestId = overviewRequestIdRef.current + 1; overviewRequestIdRef.current = requestId; const keepCurrentOverview = preserveData && overviewRef.current; setLoading(true); setError(""); if (!keepCurrentOverview) { setOverview(null); } try { const startMs = rangeStartMs(range, timeZone); const endMs = rangeEndMs(range, timeZone); const seriesRange = sameRange(range, todayRange(timeZone)) ? lastDaysRange(7, timeZone) : range; const data = await fetchStatisticsOverview({ appCode, countryId, endMs, regionId, seriesEndMs: rangeEndMs(seriesRange, timeZone), seriesStartMs: rangeStartMs(seriesRange, timeZone), startMs }); const nextOverview = enrichCountryBreakdown(data || {}, scopedCountries); if (requestId === overviewRequestIdRef.current) { setOverview(nextOverview); } } catch (err) { if (requestId === overviewRequestIdRef.current) { if (!keepCurrentOverview) { setOverview(null); } setError(err.message || "请求失败"); } } finally { if (requestId === overviewRequestIdRef.current) { setLoading(false); } } }, [appCode, countryId, range, regionId, scopedCountries, timeZone]); const loadSelfGameOverview = useCallback(async ({ preserveData = false } = {}) => { const requestId = selfGameRequestIdRef.current + 1; selfGameRequestIdRef.current = requestId; const keepCurrentOverview = preserveData && selfGameOverviewRef.current; setSelfGameLoading(true); setSelfGameError(""); if (!keepCurrentOverview) { setSelfGameOverview(null); } try { const data = await fetchSelfGameStatisticsOverview({ appCode, countryId, endMs: rangeEndMs(range, timeZone), gameId, regionId, startMs: rangeStartMs(range, timeZone) }); if (requestId === selfGameRequestIdRef.current) { setSelfGameOverview(data || {}); } } catch (err) { if (requestId === selfGameRequestIdRef.current) { if (!keepCurrentOverview) { setSelfGameOverview(null); } setSelfGameError(err.message || "请求失败"); } } finally { if (requestId === selfGameRequestIdRef.current) { setSelfGameLoading(false); } } }, [appCode, countryId, gameId, range, regionId, timeZone]); useEffect(() => { void loadFilterOptions(); }, [loadFilterOptions]); useEffect(() => { if (activeScreen === "selfGames") { void loadSelfGameOverview(); const timer = window.setInterval(() => void loadSelfGameOverview({ preserveData: true }), 60_000); return () => window.clearInterval(timer); } void loadOverview(); const timer = window.setInterval(() => void loadOverview({ preserveData: true }), 60_000); return () => window.clearInterval(timer); }, [activeScreen, loadOverview, loadSelfGameOverview]); const handleRefresh = useCallback(() => { if (activeScreen === "selfGames") { void loadSelfGameOverview({ preserveData: true }); return; } void loadOverview({ preserveData: true }); }, [activeScreen, loadOverview, loadSelfGameOverview]); const model = useMemo(() => createDashboardModel(overview, { appCode, countryId, range, timeZone }), [appCode, countryId, overview, range, timeZone]); const showSkeleton = loading && !overview; const openLuckyGiftPools = useCallback(() => { setPoolModalOpen(true); }, []); return (
{activeScreen === "selfGames" ? ( ) : ( <>
{model.kpis.map((item) => ( setTrendModalItem(item)} /> ))}
{model.businessKpis.map((item) => ( ))}
{model.robotGiftKpis.map((item) => ( setTrendModalItem(item)} /> ))}
)}
{activeScreen === "overview" ? ( <> {poolModalOpen ? setPoolModalOpen(false)} /> : null} {trendModalItem ? setTrendModalItem(null)} /> : null} ) : null}
); } function resolveSelectedAppCode(options, current) { const normalized = String(current || "").trim().toLowerCase(); if (options.some((item) => String(item.code || item.value || "").trim().toLowerCase() === normalized)) { return normalized; } return String(options[0]?.code || options[0]?.value || normalized || "lalu").trim().toLowerCase(); } function resolveRangePresetValue(range, timeZone) { const presets = [ { range: todayRange(timeZone), value: "today" }, { range: lastDaysRange(7, timeZone), value: "7d" }, { range: lastDaysRange(14, timeZone), value: "14d" }, { range: lastDaysRange(30, timeZone), value: "30d" }, { range: thisMonthRange(timeZone), value: "month" } ]; return presets.find((preset) => sameRange(preset.range, range))?.value || ""; } function rangeForPreset(value, timeZone) { switch (value) { case "today": return todayRange(timeZone); case "7d": return lastDaysRange(7, timeZone); case "14d": return lastDaysRange(14, timeZone); case "30d": return lastDaysRange(30, timeZone); case "month": return thisMonthRange(timeZone); default: return null; } } function countryBelongsToRegion(country, regionId) { if (!regionId || regionId === "all") { return true; } if (Array.isArray(country.regionIds) && country.regionIds.length) { return country.regionIds.includes(String(regionId)); } return String(country.regionId || "") === String(regionId); } function enrichCountryBreakdown(data, countries) { if (!Array.isArray(data?.country_breakdown) || !data.country_breakdown.length) { return data; } const countryById = new Map(countries.map((country) => [Number(country.id), country])); return { ...data, country_breakdown: data.country_breakdown.map((row) => { const country = countryById.get(Number(row.country_id ?? row.countryId)); if (!country) { return row; } return { ...row, country: row.country || country.name || country.label, country_code: row.country_code || country.countryCode, flag: row.flag || country.flag }; }) }; }