feat(admin): 仪表盘重构、代理线路修复与后台体验优化
Some checks failed
lotteryadmin CI / build (push) Has been cancelled

- 各角色仪表盘:KPI 可点击跳转、快捷入口、去重冗余区块,代理/站点降级 analytics
- 代理线路:创建玩家权限、侧栏搜索、深链 URL、档案保存与删除预检等修复
- 统一密码最短 6 位;代理模块表格/侧栏背景色一致(admin-table-inset)
- 报表、钱包、对账、开奖、结算、配置等模块结构与 i18n 同步更新
This commit is contained in:
2026-06-26 14:39:22 +08:00
parent 7d075ceb62
commit 0d984b00f0
126 changed files with 7423 additions and 7084 deletions

View File

@@ -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" />
) : (