refactor(layout, i18n, admin): 优化布局结构与多语言支持
调整 AdminShell 组件的子组件顺序,提升代码可读性。更新 admin-breadcrumb 组件,简化导航标签翻译逻辑,确保多语言支持的一致性。重构 admin-language-switcher 组件,优化语言切换的用户体验,增强界面交互性。更新多语言配置,新增登录界面的副标题,提升用户体验。
This commit is contained in:
193
src/modules/dashboard/use-dashboard-analytics.ts
Normal file
193
src/modules/dashboard/use-dashboard-analytics.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { format, subDays } from "date-fns";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getAdminDashboardAnalytics } from "@/api/admin-dashboard";
|
||||
import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
|
||||
import { getAdminRequestLocale } from "@/lib/admin-locale";
|
||||
import { formatAdminMinorUnits, getAdminCurrencyDecimalPlaces } from "@/lib/money";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type {
|
||||
AdminDashboardAnalyticsData,
|
||||
DashboardAnalyticsMetric,
|
||||
DashboardAnalyticsPeriod,
|
||||
} from "@/types/api/admin-dashboard-analytics";
|
||||
|
||||
export const DASHBOARD_ANALYTICS_PERIODS: DashboardAnalyticsPeriod[] = [
|
||||
"today",
|
||||
"last_7_days",
|
||||
"last_30_days",
|
||||
"this_month",
|
||||
"lifetime",
|
||||
"custom",
|
||||
];
|
||||
|
||||
export const DASHBOARD_RANKING_METRICS: DashboardAnalyticsMetric[] = ["bet", "payout", "profit"];
|
||||
|
||||
export function formatDashboardMoneyMinor(minor: number, currencyCode: string | null): string {
|
||||
const code = (currencyCode ?? "NPR").toUpperCase();
|
||||
const decimals = getAdminCurrencyDecimalPlaces(code);
|
||||
const major = minor / 10 ** decimals;
|
||||
try {
|
||||
return new Intl.NumberFormat(getAdminRequestLocale(), {
|
||||
style: "currency",
|
||||
currency: code,
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
}).format(major);
|
||||
} catch {
|
||||
return formatAdminMinorUnits(minor, code, decimals);
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDashboardSignedMoneyMinor(minor: number, currencyCode: string | null): string {
|
||||
if (minor === 0) {
|
||||
return formatDashboardMoneyMinor(0, currencyCode);
|
||||
}
|
||||
const s = minor > 0 ? "+" : "−";
|
||||
return `${s}${formatDashboardMoneyMinor(Math.abs(minor), currencyCode)}`;
|
||||
}
|
||||
|
||||
export function useDashboardAnalytics({
|
||||
enabled,
|
||||
playOptions,
|
||||
}: {
|
||||
enabled: boolean;
|
||||
playOptions: { code: string; label: string }[];
|
||||
}) {
|
||||
const { t } = useTranslation(["dashboard", "common"]);
|
||||
const playLabel = useAdminPlayCodeLabel();
|
||||
|
||||
const [period, setPeriod] = useState<DashboardAnalyticsPeriod>("last_7_days");
|
||||
const [rankingMetric, setRankingMetric] = useState<DashboardAnalyticsMetric>("bet");
|
||||
const [playCode, setPlayCode] = useState<string>("");
|
||||
const [customFrom, setCustomFrom] = useState(() => format(subDays(new Date(), 6), "yyyy-MM-dd"));
|
||||
const [customTo, setCustomTo] = useState(() => format(new Date(), "yyyy-MM-dd"));
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [data, setData] = useState<AdminDashboardAnalyticsData | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!enabled) {
|
||||
setLoading(false);
|
||||
setData(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const payload = await getAdminDashboardAnalytics({
|
||||
period,
|
||||
metric: "overview",
|
||||
play_code: playCode !== "" ? playCode : undefined,
|
||||
...(period === "custom" ? { date_from: customFrom, date_to: customTo } : {}),
|
||||
});
|
||||
setData(payload);
|
||||
} catch (e) {
|
||||
setData(null);
|
||||
const raw = e instanceof LotteryApiBizError ? e.message : "";
|
||||
const needsAuthSync =
|
||||
raw.includes("admin.dashboard.analytics") || raw.includes("资源未配置");
|
||||
setError(
|
||||
needsAuthSync ? t("warnings.apiResourceMissing") : raw || t("warnings.loadFailed"),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [enabled, period, playCode, customFrom, customTo, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
void load();
|
||||
}, 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [load]);
|
||||
|
||||
const currency = data?.currency_code ?? null;
|
||||
const summary = data?.summary;
|
||||
|
||||
const periodRangeLabel = useMemo(() => {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
return data.date_from === data.date_to
|
||||
? data.date_from
|
||||
: `${data.date_from} — ${data.date_to}`;
|
||||
}, [data]);
|
||||
|
||||
const playFilterLabel = useMemo(() => {
|
||||
if (playCode === "") {
|
||||
return t("analytics.allPlays");
|
||||
}
|
||||
return playOptions.find((p) => p.code === playCode)?.label ?? playCode;
|
||||
}, [playCode, playOptions, t]);
|
||||
|
||||
const resolvePlayLabel = useCallback(
|
||||
(code: string, dimension: number) => {
|
||||
const base = playLabel(code);
|
||||
return dimension > 0 ? `${base} · ${dimension}D` : base;
|
||||
},
|
||||
[playLabel],
|
||||
);
|
||||
|
||||
const topPlayRows = useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
const rows = [...data.play_breakdown];
|
||||
rows.sort((a, b) => {
|
||||
if (rankingMetric === "payout") {
|
||||
return b.total_payout_minor - a.total_payout_minor;
|
||||
}
|
||||
if (rankingMetric === "profit") {
|
||||
return b.approx_house_gross_minor - a.approx_house_gross_minor;
|
||||
}
|
||||
return b.total_bet_minor - a.total_bet_minor;
|
||||
});
|
||||
return rows.slice(0, 5);
|
||||
}, [data, rankingMetric]);
|
||||
|
||||
const sparklines = useMemo(() => {
|
||||
const series = data?.daily_series ?? [];
|
||||
return {
|
||||
bet: series.map((d) => d.total_bet_minor),
|
||||
payout: series.map((d) => d.total_payout_minor),
|
||||
profit: series.map((d) => d.approx_house_gross_minor),
|
||||
};
|
||||
}, [data?.daily_series]);
|
||||
|
||||
return {
|
||||
enabled,
|
||||
period,
|
||||
setPeriod,
|
||||
rankingMetric,
|
||||
setRankingMetric,
|
||||
playCode,
|
||||
setPlayCode,
|
||||
customFrom,
|
||||
setCustomFrom,
|
||||
customTo,
|
||||
setCustomTo,
|
||||
loading,
|
||||
error,
|
||||
data,
|
||||
currency,
|
||||
summary,
|
||||
periodRangeLabel,
|
||||
playFilterLabel,
|
||||
playOptions,
|
||||
resolvePlayLabel,
|
||||
topPlayRows,
|
||||
sparklines,
|
||||
formatMoney: formatDashboardMoneyMinor,
|
||||
formatSignedMoney: formatDashboardSignedMoneyMinor,
|
||||
t,
|
||||
};
|
||||
}
|
||||
|
||||
export type DashboardAnalyticsState = ReturnType<typeof useDashboardAnalytics>;
|
||||
Reference in New Issue
Block a user