refactor(risk, navigation): update risk management redirects and enhance loading states
Changed default redirects in risk management pages to point to the new risk pools section. Removed unused risk lock log components and streamlined the admin reports page with a loading state for better user experience. Added a new DocFigure component for improved documentation visuals and updated localization files to include new figure descriptions.
This commit is contained in:
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { AdminSubnav, AdminSubnavButton } from "@/components/admin/admin-subnav";
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminLoadingInline } from "@/components/admin/admin-loading-state";
|
||||
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
|
||||
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
|
||||
import {
|
||||
@@ -233,7 +234,7 @@ export function AgentLineDetailPanel({
|
||||
|
||||
<AdminSubnav
|
||||
aria-label={t("detailTabs", { defaultValue: "代理详情" })}
|
||||
className="overflow-x-auto border-b border-border/60 px-4 sm:px-5"
|
||||
className="min-h-11 overflow-x-auto border-b border-border/60 px-4 sm:px-5"
|
||||
>
|
||||
{tabs
|
||||
.filter((tab) => tab.visible)
|
||||
@@ -251,6 +252,10 @@ export function AgentLineDetailPanel({
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain bg-muted/15 px-5 py-5 sm:px-6 sm:py-6">
|
||||
{profileLoading && detailTab !== "overview" ? (
|
||||
<AdminLoadingInline className="py-16" />
|
||||
) : null}
|
||||
|
||||
{detailTab === "overview" ? (
|
||||
<OverviewTab
|
||||
profile={profile}
|
||||
@@ -259,7 +264,7 @@ export function AgentLineDetailPanel({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{detailTab === "profile" && canViewProfileTab && profileFields ? (
|
||||
{detailTab === "profile" && canViewProfileTab && profileFields && !profileLoading ? (
|
||||
<Card className="mx-auto max-w-3xl border-border/70 shadow-sm">
|
||||
<CardHeader className="border-b border-border/60 pb-4">
|
||||
<CardTitle className="text-base">
|
||||
@@ -303,7 +308,7 @@ export function AgentLineDetailPanel({
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{detailTab === "downline" && canViewDownlineTab ? (
|
||||
{detailTab === "downline" && canViewDownlineTab && !profileLoading ? (
|
||||
<DownlineTable
|
||||
childAgents={childAgents}
|
||||
childCountById={childCountById}
|
||||
@@ -318,7 +323,7 @@ export function AgentLineDetailPanel({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{detailTab === "players" && canViewPlayersTab ? (
|
||||
{detailTab === "players" && canViewPlayersTab && !profileLoading ? (
|
||||
<AgentsPlayersPanel
|
||||
siteCode={siteCode}
|
||||
agentNodeId={node.id}
|
||||
@@ -393,44 +398,49 @@ function OverviewTab({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!profileLoading && profile ? (
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
<MetricCard
|
||||
label={t("profile.rebateLimit", { defaultValue: "回水上限 (%)" })}
|
||||
value={`${rebateCap ?? "0"}%`}
|
||||
/>
|
||||
<MetricCard
|
||||
label={t("profile.defaultPlayerRebate", { defaultValue: "默认玩家回水 (%)" })}
|
||||
value={`${percentValueToUi(profile.default_player_rebate ?? 0)}%`}
|
||||
/>
|
||||
<MetricCard
|
||||
label={t("profile.riskTags", { defaultValue: "风控标签" })}
|
||||
value={
|
||||
(profile.risk_tags?.length ?? 0) > 0
|
||||
? profile.risk_tags!.join(", ")
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
<MetricCard
|
||||
label={t("profile.rebateLimit", { defaultValue: "回水上限 (%)" })}
|
||||
value={profileLoading ? "…" : `${rebateCap ?? "0"}%`}
|
||||
/>
|
||||
<MetricCard
|
||||
label={t("profile.defaultPlayerRebate", { defaultValue: "默认玩家回水 (%)" })}
|
||||
value={
|
||||
profileLoading ? "…" : `${percentValueToUi(profile?.default_player_rebate ?? 0)}%`
|
||||
}
|
||||
/>
|
||||
<MetricCard
|
||||
label={t("profile.riskTags", { defaultValue: "风控标签" })}
|
||||
value={
|
||||
profileLoading
|
||||
? "…"
|
||||
: (profile?.risk_tags?.length ?? 0) > 0
|
||||
? profile!.risk_tags!.join(", ")
|
||||
: t("common:states.none", { defaultValue: "无" })
|
||||
}
|
||||
/>
|
||||
<CapabilityMetric
|
||||
label={t("profile.canGrantExtraRebate", { defaultValue: "允许额外回水" })}
|
||||
enabled={profile.can_grant_extra_rebate === true}
|
||||
yesLabel={yesLabel}
|
||||
noLabel={noLabel}
|
||||
/>
|
||||
<CapabilityMetric
|
||||
label={t("profile.canCreatePlayer", { defaultValue: "允许创建玩家" })}
|
||||
enabled={profile.can_create_player !== false}
|
||||
yesLabel={yesLabel}
|
||||
noLabel={noLabel}
|
||||
/>
|
||||
<CapabilityMetric
|
||||
label={t("profile.canCreateChildAgent", { defaultValue: "允许创建下级代理" })}
|
||||
enabled={profile.can_create_child_agent === true}
|
||||
yesLabel={yesLabel}
|
||||
noLabel={noLabel}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
}
|
||||
/>
|
||||
<CapabilityMetric
|
||||
label={t("profile.canGrantExtraRebate", { defaultValue: "允许额外回水" })}
|
||||
enabled={profile?.can_grant_extra_rebate === true}
|
||||
loading={profileLoading}
|
||||
yesLabel={yesLabel}
|
||||
noLabel={noLabel}
|
||||
/>
|
||||
<CapabilityMetric
|
||||
label={t("profile.canCreatePlayer", { defaultValue: "允许创建玩家" })}
|
||||
enabled={profile?.can_create_player !== false}
|
||||
loading={profileLoading}
|
||||
yesLabel={yesLabel}
|
||||
noLabel={noLabel}
|
||||
/>
|
||||
<CapabilityMetric
|
||||
label={t("profile.canCreateChildAgent", { defaultValue: "允许创建下级代理" })}
|
||||
enabled={profile?.can_create_child_agent === true}
|
||||
loading={profileLoading}
|
||||
yesLabel={yesLabel}
|
||||
noLabel={noLabel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -438,11 +448,13 @@ function OverviewTab({
|
||||
function CapabilityMetric({
|
||||
label,
|
||||
enabled,
|
||||
loading = false,
|
||||
yesLabel,
|
||||
noLabel,
|
||||
}: {
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
loading?: boolean;
|
||||
yesLabel: string;
|
||||
noLabel: string;
|
||||
}): React.ReactElement {
|
||||
@@ -452,10 +464,10 @@ function CapabilityMetric({
|
||||
<p
|
||||
className={cn(
|
||||
"mt-1.5 text-2xl font-semibold tracking-tight",
|
||||
enabled ? "text-foreground" : "text-muted-foreground",
|
||||
loading ? "text-muted-foreground" : enabled ? "text-foreground" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{enabled ? yesLabel : noLabel}
|
||||
{loading ? "…" : enabled ? yesLabel : noLabel}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminLoadingInline } from "@/components/admin/admin-loading-state";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatAdminCreditMajorDecimal } from "@/lib/money";
|
||||
@@ -55,15 +56,6 @@ function pruneTreeForSearch(
|
||||
return out;
|
||||
}
|
||||
|
||||
function collectExpandableIds(nodes: AgentNodeRow[], into: Set<number>): void {
|
||||
for (const node of nodes) {
|
||||
if ((node.children?.length ?? 0) > 0) {
|
||||
into.add(node.id);
|
||||
collectExpandableIds(node.children ?? [], into);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type AgentLineSidebarProps = {
|
||||
siteLabel: string | null;
|
||||
/** API 返回的嵌套树(含 children) */
|
||||
@@ -72,6 +64,7 @@ export type AgentLineSidebarProps = {
|
||||
selectedId: number | null;
|
||||
keyword: string;
|
||||
agentCount: number;
|
||||
loading?: boolean;
|
||||
onKeywordChange: (value: string) => void;
|
||||
onSelect: (node: AgentNodeRow) => void;
|
||||
};
|
||||
@@ -167,6 +160,7 @@ export function AgentLineSidebar({
|
||||
selectedId,
|
||||
keyword,
|
||||
agentCount,
|
||||
loading = false,
|
||||
onKeywordChange,
|
||||
onSelect,
|
||||
}: AgentLineSidebarProps): React.ReactElement {
|
||||
@@ -180,9 +174,20 @@ export function AgentLineSidebar({
|
||||
}, [normalizedKeyword, parentNameMap, tree]);
|
||||
|
||||
useEffect(() => {
|
||||
const next = new Set<number>();
|
||||
collectExpandableIds(tree, next);
|
||||
setExpandedIds(next);
|
||||
if (tree.length === 0) {
|
||||
setExpandedIds(new Set());
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
for (const node of tree) {
|
||||
if ((node.children?.length ?? 0) > 0) {
|
||||
next.add(node.id);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [tree]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -258,7 +263,9 @@ export function AgentLineSidebar({
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-2 py-2">
|
||||
{!hasAnyAgent ? (
|
||||
{loading ? (
|
||||
<AdminLoadingInline className="py-10" />
|
||||
) : !hasAnyAgent ? (
|
||||
<AdminNoResourceState className="px-2 py-8 text-center text-sm text-muted-foreground" />
|
||||
) : (
|
||||
<ul className="space-y-0.5" role="listbox" aria-label={t("listTitle", { defaultValue: "代理列表" })}>
|
||||
|
||||
@@ -464,8 +464,8 @@ export function AgentsConsole(): React.ReactElement {
|
||||
const canShowDownlineTab = useMemo(
|
||||
() =>
|
||||
selectedNode !== null &&
|
||||
!selectedProfileLoading &&
|
||||
(isSiteAdmin ||
|
||||
(selectedProfileLoading ||
|
||||
isSiteAdmin ||
|
||||
isSuperAdmin ||
|
||||
selectedProfile?.can_create_child_agent === true),
|
||||
[isSiteAdmin, isSuperAdmin, selectedNode, selectedProfile, selectedProfileLoading],
|
||||
@@ -474,11 +474,11 @@ export function AgentsConsole(): React.ReactElement {
|
||||
const canShowPlayersTab = useMemo(
|
||||
() =>
|
||||
selectedNode !== null &&
|
||||
!selectedProfileLoading &&
|
||||
hasUsersManagePermission &&
|
||||
(isSiteAdmin ||
|
||||
isSuperAdmin ||
|
||||
selectedProfile?.can_create_player === true),
|
||||
(selectedProfileLoading ||
|
||||
(hasUsersManagePermission &&
|
||||
(isSiteAdmin ||
|
||||
isSuperAdmin ||
|
||||
selectedProfile?.can_create_player === true))),
|
||||
[hasUsersManagePermission, isSiteAdmin, isSuperAdmin, selectedNode, selectedProfile, selectedProfileLoading],
|
||||
);
|
||||
|
||||
@@ -747,7 +747,19 @@ export function AgentsConsole(): React.ReactElement {
|
||||
selectedProfileLoading,
|
||||
]);
|
||||
|
||||
const showAgentSidebar = visibleAgentRows.length > 0;
|
||||
const showAgentSidebar = loading || visibleAgentRows.length > 0;
|
||||
|
||||
const hasSiteContext =
|
||||
siteOptions.length > 0 ||
|
||||
profile?.site != null ||
|
||||
(profile?.accessible_sites?.length ?? 0) > 0;
|
||||
|
||||
const isAgentLineBootLoading =
|
||||
canViewAgents &&
|
||||
(sitesLoading ||
|
||||
profile === null ||
|
||||
(hasSiteContext && adminSiteId === null) ||
|
||||
(adminSiteId !== null && loading));
|
||||
|
||||
const openAddAgent = (): void => {
|
||||
const parent = selectedNode ?? rootNode;
|
||||
@@ -910,17 +922,12 @@ export function AgentsConsole(): React.ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
const hasSiteContext =
|
||||
siteOptions.length > 0 ||
|
||||
profile?.site != null ||
|
||||
(profile?.accessible_sites?.length ?? 0) > 0;
|
||||
|
||||
if (canViewAgents && profile?.agent == null && !sitesLoading && !hasSiteContext) {
|
||||
return <AdminNoIntegrationSiteState canCreate={isSuperAdmin} />;
|
||||
}
|
||||
|
||||
if (canViewAgents && loading && tree.length === 0 && adminSiteId !== null) {
|
||||
return <AdminLoadingState label={t("listTitle", { defaultValue: "代理列表" })} />;
|
||||
if (canViewAgents && isAgentLineBootLoading) {
|
||||
return <AdminLoadingState label={t("listTitle", { defaultValue: "代理列表" })} minHeight="28rem" />;
|
||||
}
|
||||
|
||||
const showSiteAdminAwaitingRoot =
|
||||
@@ -980,6 +987,7 @@ export function AgentsConsole(): React.ReactElement {
|
||||
selectedId={selectedNodeId}
|
||||
keyword={keyword}
|
||||
agentCount={visibleAgentRows.length}
|
||||
loading={loading && visibleAgentRows.length === 0}
|
||||
onKeywordChange={(value) => {
|
||||
setKeyword(value);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user