126 lines
4.2 KiB
JavaScript
126 lines
4.2 KiB
JavaScript
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import { EChart } from "../charts/EChart.jsx";
|
|
import { createGeoOption } from "../charts/options/createGeoOption.js";
|
|
import { formatMoney, moneyMajor } from "../utils/format.js";
|
|
import { GlobeMap } from "./GlobeMap.jsx";
|
|
import { Panel } from "./Panel.jsx";
|
|
import { RankList } from "./RankList.jsx";
|
|
|
|
const MODAL_TRANSITION_MS = 180;
|
|
|
|
export function GlobalOverviewPanel({ countryBreakdown, loading = false }) {
|
|
const [globeOpen, setGlobeOpen] = useState(false);
|
|
const rechargeCountries = useMemo(() => countryBreakdown.filter(hasRecharge), [countryBreakdown]);
|
|
const rankedCountries = useMemo(() => rechargeCountries.slice(0, 4), [rechargeCountries]);
|
|
|
|
return (
|
|
<>
|
|
<Panel className="panel--map" title="全球概览">
|
|
{loading ? <GlobalOverviewSkeleton /> : (
|
|
<div className="geo-layout">
|
|
<div className="geo-map">
|
|
{rankedCountries.length ? <EChart className="geo-chart" option={createGeoOption(rankedCountries)} /> : <div className="panel-empty">暂无国家数据</div>}
|
|
<div className="geo-scale">
|
|
<span>高</span>
|
|
<i />
|
|
<span>低</span>
|
|
</div>
|
|
</div>
|
|
<div className="rank-block">
|
|
<RankList items={rankedCountries} title="充值国家排行" valueFormatter={formatRankMoney} />
|
|
<button className="all-countries-button" disabled={!rechargeCountries.length} type="button" onClick={() => setGlobeOpen(true)}>
|
|
查看全部国家
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Panel>
|
|
{globeOpen ? <CountryGlobeDialog items={rechargeCountries} onClose={() => setGlobeOpen(false)} /> : null}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function GlobalOverviewSkeleton() {
|
|
return (
|
|
<div className="geo-layout skeleton-panel" aria-busy="true">
|
|
<div className="geo-map skeleton-map">
|
|
<span className="skeleton-block skeleton-map-glow" />
|
|
</div>
|
|
<div className="rank-block">
|
|
<span className="skeleton-block skeleton-rank-title" />
|
|
{Array.from({ length: 4 }).map((_, index) => (
|
|
<span className="skeleton-block skeleton-rank-row" key={index} />
|
|
))}
|
|
<span className="skeleton-block skeleton-button" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CountryGlobeDialog({ items, onClose }) {
|
|
const [closing, setClosing] = useState(false);
|
|
const closeTimerRef = useRef(null);
|
|
const requestClose = useCallback(() => {
|
|
if (closing) {
|
|
return;
|
|
}
|
|
setClosing(true);
|
|
closeTimerRef.current = window.setTimeout(onClose, MODAL_TRANSITION_MS);
|
|
}, [closing, onClose]);
|
|
|
|
useEffect(() => () => {
|
|
if (closeTimerRef.current) {
|
|
window.clearTimeout(closeTimerRef.current);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const closeOnEscape = (event) => {
|
|
if (event.key === "Escape") {
|
|
requestClose();
|
|
}
|
|
};
|
|
window.addEventListener("keydown", closeOnEscape);
|
|
return () => window.removeEventListener("keydown", closeOnEscape);
|
|
}, [requestClose]);
|
|
|
|
return (
|
|
<div
|
|
className={["country-globe-modal", closing ? "is-closing" : ""].filter(Boolean).join(" ")}
|
|
role="presentation"
|
|
onMouseDown={(event) => {
|
|
if (event.target === event.currentTarget) {
|
|
requestClose();
|
|
}
|
|
}}
|
|
>
|
|
<section aria-label="全部国家地球仪" aria-modal="true" className="country-globe-dialog" role="dialog">
|
|
<div className="country-globe-head">
|
|
<div>
|
|
<h2>全部国家</h2>
|
|
<span>{items.length} 个国家 / 按充值规模展示</span>
|
|
</div>
|
|
<button aria-label="关闭" type="button" onClick={requestClose}>
|
|
<span aria-hidden="true" className="modal-close-mark" />
|
|
</button>
|
|
</div>
|
|
<div className="country-globe-body">
|
|
<GlobeMap items={items} />
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function formatRankMoney(value) {
|
|
const amount = moneyMajor(value);
|
|
if (Math.abs(amount) >= 100_000) {
|
|
return `$${(amount / 1_000_000).toFixed(2)}M`;
|
|
}
|
|
return formatMoney(value);
|
|
}
|
|
|
|
function hasRecharge(item) {
|
|
return Number(item?.recharge_usd_minor) > 0;
|
|
}
|