feat(admin): 仪表盘重构、代理线路修复与后台体验优化
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
- 各角色仪表盘:KPI 可点击跳转、快捷入口、去重冗余区块,代理/站点降级 analytics - 代理线路:创建玩家权限、侧栏搜索、深链 URL、档案保存与删除预检等修复 - 统一密码最短 6 位;代理模块表格/侧栏背景色一致(admin-table-inset) - 报表、钱包、对账、开奖、结算、配置等模块结构与 i18n 同步更新
This commit is contained in:
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminLoadingInline } from "@/components/admin/admin-loading-state";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatAdminCreditMajorDecimal } from "@/lib/money";
|
||||
@@ -56,6 +57,36 @@ function pruneTreeForSearch(
|
||||
return out;
|
||||
}
|
||||
|
||||
function collectSearchExpandIds(
|
||||
nodes: AgentNodeRow[],
|
||||
normalized: string,
|
||||
parentNameMap: Map<number, string>,
|
||||
): Set<number> {
|
||||
const ids = new Set<number>();
|
||||
|
||||
const walk = (list: AgentNodeRow[], ancestors: number[]): void => {
|
||||
for (const node of list) {
|
||||
const children = node.children ?? [];
|
||||
const prunedChildren = pruneTreeForSearch(children, normalized, parentNameMap);
|
||||
const selfMatch = nodeMatchesKeyword(node, normalized, parentNameMap);
|
||||
|
||||
if (selfMatch || prunedChildren.length > 0) {
|
||||
for (const id of ancestors) {
|
||||
ids.add(id);
|
||||
}
|
||||
if (prunedChildren.length > 0) {
|
||||
ids.add(node.id);
|
||||
}
|
||||
walk(prunedChildren, [...ancestors, node.id]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(nodes, []);
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
export type AgentLineSidebarProps = {
|
||||
siteLabel: string | null;
|
||||
/** API 返回的嵌套树(含 children) */
|
||||
@@ -67,6 +98,8 @@ export type AgentLineSidebarProps = {
|
||||
loading?: boolean;
|
||||
onKeywordChange: (value: string) => void;
|
||||
onSelect: (node: AgentNodeRow) => void;
|
||||
errorMessage?: string | null;
|
||||
onRetry?: () => void;
|
||||
};
|
||||
|
||||
type TreeRowProps = {
|
||||
@@ -98,7 +131,7 @@ function TreeRow({
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-start gap-0.5 rounded-md py-1 pr-2 transition-colors",
|
||||
active ? "bg-primary/10 ring-1 ring-primary/25" : "hover:bg-background/80",
|
||||
active ? "bg-primary/10 ring-1 ring-primary/25" : "hover:bg-muted/15",
|
||||
)}
|
||||
style={{ paddingLeft: `${6 + indent}px` }}
|
||||
>
|
||||
@@ -163,6 +196,8 @@ export function AgentLineSidebar({
|
||||
loading = false,
|
||||
onKeywordChange,
|
||||
onSelect,
|
||||
errorMessage = null,
|
||||
onRetry,
|
||||
}: AgentLineSidebarProps): React.ReactElement {
|
||||
const { t } = useTranslation(["agents", "common"]);
|
||||
const [expandedIds, setExpandedIds] = useState<Set<number>>(() => new Set());
|
||||
@@ -220,6 +255,19 @@ export function AgentLineSidebar({
|
||||
});
|
||||
}, [selectedId, tree]);
|
||||
|
||||
useEffect(() => {
|
||||
if (normalizedKeyword === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const expandIds = collectSearchExpandIds(tree, normalizedKeyword, parentNameMap);
|
||||
if (expandIds.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedIds((prev) => new Set([...prev, ...expandIds]));
|
||||
}, [normalizedKeyword, parentNameMap, tree]);
|
||||
|
||||
const toggleExpand = useCallback((id: number) => {
|
||||
setExpandedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -236,19 +284,8 @@ export function AgentLineSidebar({
|
||||
const hasAnyAgent = displayForest.length > 0;
|
||||
|
||||
return (
|
||||
<aside className="flex min-h-0 h-full w-full flex-col bg-muted/10 lg:w-[18rem] lg:shrink-0 lg:border-r lg:border-border/70">
|
||||
<div className="space-y-3 border-b border-border/60 bg-card px-4 py-4">
|
||||
{siteLabel ? (
|
||||
<p className="truncate text-xs font-medium text-foreground/80" title={siteLabel}>
|
||||
{siteLabel}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("lineUi.agentCount", {
|
||||
defaultValue: "本组 {{count}} 个代理",
|
||||
count: agentCount,
|
||||
})}
|
||||
</p>
|
||||
<aside className="flex min-h-0 h-full w-full flex-col lg:w-[18rem] lg:shrink-0 lg:border-r lg:border-border/70">
|
||||
<div className="space-y-3 border-b border-border/60 px-4 py-4">
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
@@ -265,6 +302,15 @@ export function AgentLineSidebar({
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-2 py-2">
|
||||
{loading ? (
|
||||
<AdminLoadingInline className="py-10" />
|
||||
) : !hasAnyAgent && errorMessage ? (
|
||||
<div className="space-y-3 px-2 py-8 text-center">
|
||||
<p className="text-sm text-destructive">{errorMessage}</p>
|
||||
{onRetry ? (
|
||||
<Button type="button" size="sm" variant="outline" onClick={onRetry}>
|
||||
{t("common:actions.retry", { defaultValue: "重试" })}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : !hasAnyAgent ? (
|
||||
<AdminNoResourceState className="px-2 py-8 text-center text-sm text-muted-foreground" />
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user