Compare commits

...

2 Commits

59 changed files with 270 additions and 521 deletions

View File

@@ -0,0 +1,12 @@
import { adminRequest } from "@/lib/admin-http";
import { API_V1_PREFIX } from "./paths";
import type { AdminDashboardData } from "@/types/api/admin-dashboard";
const A = `${API_V1_PREFIX}/admin`;
/** 首页仪表盘聚合(大厅 + 当期财务/风控/异常转账等,按账号权限填充各块) */
export async function getAdminDashboard(): Promise<AdminDashboardData> {
return adminRequest.get<AdminDashboardData>(`${A}/dashboard`);
}

View File

@@ -31,7 +31,6 @@ export async function getAdminDrawResultBatches(drawId: number): Promise<AdminDr
return adminRequest.get<AdminDrawBatchesData>(`${A}/draws/${drawId}/result-batches`); return adminRequest.get<AdminDrawBatchesData>(`${A}/draws/${drawId}/result-batches`);
} }
/** PRD §15.4:单期投注/派彩与结算批次摘要 */
export async function getAdminDrawFinanceSummary( export async function getAdminDrawFinanceSummary(
drawId: number, drawId: number,
): Promise<AdminDrawFinanceSummaryData> { ): Promise<AdminDrawFinanceSummaryData> {

View File

@@ -1,10 +1,10 @@
import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { ModuleScaffold } from "@/components/admin/module-scaffold";
import { auditModuleMeta } from "@/modules/audit/meta"; import { auditLogsModuleMeta } from "@/modules/audit/meta";
import { AuditLogsConsole } from "@/modules/audit/audit-logs-console"; import { AuditLogsConsole } from "@/modules/audit/audit-logs-console";
import type { Metadata } from "next"; import type { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: auditModuleMeta.title, title: auditLogsModuleMeta.title,
}; };
export default function AdminAuditLogsPage() { export default function AdminAuditLogsPage() {

View File

@@ -1,12 +1,7 @@
import Link from "next/link"; import Link from "next/link";
import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { ModuleScaffold } from "@/components/admin/module-scaffold";
import { import { Card, CardHeader, CardTitle } from "@/components/ui/card";
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { configHubMeta } from "@/modules/config/meta"; import { configHubMeta } from "@/modules/config/meta";
import type { Metadata } from "next"; import type { Metadata } from "next";
@@ -15,47 +10,27 @@ export const metadata: Metadata = {
}; };
const SECTIONS = [ const SECTIONS = [
{ { href: "/admin/config/plays", title: "玩法配置" },
href: "/admin/config/plays", { href: "/admin/config/odds", title: "赔率配置" },
title: "玩法配置", { href: "/admin/config/rebate", title: "佣金 / 回水" },
description: "§5.4:目录开关、显示名与排序、限额、规则说明(玩法配置版本)。", { href: "/admin/config/risk-cap", title: "风控封顶" },
}, { href: "/admin/config/versions", title: "配置版本历史" },
{
href: "/admin/config/odds",
title: "赔率配置",
description: "§5.5:按维度 / 玩法编辑五档赔率、回水率、历史版本与回滚。",
},
{
href: "/admin/config/rebate",
title: "佣金 / 回水",
description: "§5.6:按 2D / 3D / 4D 批量写入 rebate_rate共用赔率版本。",
},
{
href: "/admin/config/risk-cap",
title: "风控封顶",
description: "§5.7:默认封顶、特殊号码封顶;占用列为占位,待注单汇总接入。",
},
{
href: "/admin/config/versions",
title: "配置版本历史",
description: "三套流水线的版本列表(玩法配置 / 赔率 / 风控封顶)。",
},
] as const; ] as const;
export default function AdminConfigHubPage() { export default function AdminConfigHubPage() {
return ( return (
<ModuleScaffold className="max-w-4xl"> <ModuleScaffold className="max-w-4xl">
<div className="mb-6 space-y-1"> <h1 className="mb-6 text-xl font-semibold tracking-tight">{configHubMeta.title}</h1>
<h1 className="text-xl font-semibold tracking-tight">{configHubMeta.title}</h1>
<p className="text-sm text-muted-foreground">{configHubMeta.description}</p>
</div>
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
{SECTIONS.map((s) => ( {SECTIONS.map((s) => (
<Link key={s.href} href={s.href} className="block rounded-lg outline-none ring-offset-background focus-visible:ring-2 focus-visible:ring-ring"> <Link
key={s.href}
href={s.href}
className="block rounded-lg outline-none ring-offset-background focus-visible:ring-2 focus-visible:ring-ring"
>
<Card className="h-full transition-colors hover:bg-muted/40"> <Card className="h-full transition-colors hover:bg-muted/40">
<CardHeader> <CardHeader>
<CardTitle className="text-base">{s.title}</CardTitle> <CardTitle className="text-base">{s.title}</CardTitle>
<CardDescription>{s.description}</CardDescription>
</CardHeader> </CardHeader>
</Card> </Card>
</Link> </Link>

View File

@@ -5,7 +5,6 @@ export const metadata: Metadata = {
title: "开奖结果发布", title: "开奖结果发布",
}; };
/** PRD §11.4:与 `POST /api/v1/admin/draws/{draw}/result-batches/{batch}/publish` 联调页 */
export default async function AdminDrawPublishBatchPage(props: { export default async function AdminDrawPublishBatchPage(props: {
params: Promise<{ drawId: string; batchId: string }>; params: Promise<{ drawId: string; batchId: string }>;
}) { }) {

View File

@@ -11,7 +11,6 @@ export default function AdminJackpotPoolsPage() {
<> <>
<div className="mx-auto mb-6 max-w-5xl"> <div className="mx-auto mb-6 max-w-5xl">
<h1 className="text-lg font-semibold tracking-tight">{jackpotModuleMeta.title}</h1> <h1 className="text-lg font-semibold tracking-tight">{jackpotModuleMeta.title}</h1>
<p className="text-muted-foreground mt-1 text-sm">{jackpotModuleMeta.description}</p>
</div> </div>
<JackpotPoolsConsole /> <JackpotPoolsConsole />
</> </>

View File

@@ -1,5 +1,4 @@
import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { ModuleScaffold } from "@/components/admin/module-scaffold";
import { getAdminPing } from "@/api";
import { DashboardConsole } from "@/modules/dashboard/dashboard-console"; import { DashboardConsole } from "@/modules/dashboard/dashboard-console";
import type { Metadata } from "next"; import type { Metadata } from "next";
@@ -7,33 +6,10 @@ export const metadata: Metadata = {
title: "仪表盘", title: "仪表盘",
}; };
export default async function AdminDashboardPage() { export default function AdminDashboardPage() {
const ping = await getAdminPing();
const apiReady = process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL?.trim() !== "";
return ( return (
<ModuleScaffold className="max-w-7xl"> <ModuleScaffold className="max-w-7xl">
<DashboardConsole /> <DashboardConsole />
<details className="mt-8 rounded-lg border border-border/80 bg-muted/30 px-4 py-3 text-sm">
<summary className="cursor-pointer font-medium text-muted-foreground">API </summary>
<div className="mt-3 grid gap-3 sm:grid-cols-2">
<p className="text-muted-foreground">
{apiReady ? (
<span className="ml-1 font-medium text-emerald-600 dark:text-emerald-400"></span>
) : (
<span className="ml-1 font-medium text-amber-600 dark:text-amber-400">
NEXT_PUBLIC_LOTTERY_API_BASE_URL
</span>
)}
</p>
<p className="font-mono text-xs text-foreground">
Admin ping Token
{!apiReady ? "—" : ping ? JSON.stringify(ping) : "未返回"}
</p>
</div>
</details>
</ModuleScaffold> </ModuleScaffold>
); );
} }

View File

@@ -10,7 +10,6 @@ export default async function AdminRiskHotPage(props: {
<RiskPoolsConsole <RiskPoolsConsole
drawId={id} drawId={id}
title="热门号码监控" title="热门号码监控"
description="按「已占用 / 封顶」比例与已占用额排序,便于发现集中下注号码(产品文档 §13.4)。"
soldOutOnly={false} soldOutOnly={false}
defaultSort="usage_desc" defaultSort="usage_desc"
/> />

View File

@@ -10,7 +10,6 @@ export default async function AdminRiskPoolsPage(props: {
<RiskPoolsConsole <RiskPoolsConsole
drawId={id} drawId={id}
title="全部风险池" title="全部风险池"
description="本期已出现的全部号码赔付池事实表;可排序,点击「查看」进入单号码详情页。"
soldOutOnly={false} soldOutOnly={false}
defaultSort="number_asc" defaultSort="number_asc"
allowSortChange allowSortChange

View File

@@ -10,7 +10,6 @@ export default async function AdminRiskSoldOutPage(props: {
<RiskPoolsConsole <RiskPoolsConsole
drawId={id} drawId={id}
title="售罄号码列表" title="售罄号码列表"
description="剩余赔付额度为 0 的 4D 号码;玩家侧将收到售罄拒单(产品文档 §6.4、§13.4)。"
soldOutOnly soldOutOnly
defaultSort="number_asc" defaultSort="number_asc"
/> />

View File

@@ -10,11 +10,8 @@ export const metadata: Metadata = {
export default function AdminServiceDeskPage() { export default function AdminServiceDeskPage() {
return ( return (
<ModuleScaffold className="w-full max-w-none"> <ModuleScaffold className="w-full max-w-none">
<div className="mb-8 space-y-2"> <div className="mb-8">
<h1 className="text-2xl font-semibold tracking-tight">{serviceDeskModuleMeta.title}</h1> <h1 className="text-2xl font-semibold tracking-tight">{serviceDeskModuleMeta.title}</h1>
<p className="text-muted-foreground max-w-3xl text-sm leading-relaxed">
{serviceDeskModuleMeta.description}
</p>
</div> </div>
<ServiceDeskConsole /> <ServiceDeskConsole />
</ModuleScaffold> </ModuleScaffold>

View File

@@ -8,14 +8,7 @@ import { isAxiosError } from "axios";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { getAdminCaptcha, postAdminLogin } from "@/api"; import { getAdminCaptcha, postAdminLogin } from "@/api";
@@ -151,9 +144,6 @@ export function LoginForm() {
<CardTitle className="text-balance text-2xl font-semibold tracking-tight"> <CardTitle className="text-balance text-2xl font-semibold tracking-tight">
{authModuleMeta.title} {authModuleMeta.title}
</CardTitle> </CardTitle>
<CardDescription className="text-pretty text-sm leading-relaxed">
使
</CardDescription>
</div> </div>
</CardHeader> </CardHeader>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>

View File

@@ -5,12 +5,7 @@ function pad2(n: number): string {
} }
/** /**
* 将接口返回的 ISO 时间串格式化为 **浏览器本地时区** 下的 * 将接口返回的 ISO 时间串格式化为浏览器本地时区下的 `YYYY-MM-DD HH:mm:ss`。
* `YYYY-MM-DD HH:mm:ss`与《01-界面文档》时间展示习惯一致,且列宽稳定)。
*
* 不使用 `Intl` 拼接 `GMT+8` 等时区后缀,避免在表格里挤出过长、重叠的字符串。
*
* `@param options.locale` 与调用方保持一致,便于日后按界面语言微调格式。
*/ */
export function formatAdminInstant( export function formatAdminInstant(
iso: string | null | undefined, iso: string | null | undefined,

View File

@@ -5,7 +5,7 @@ import { useCallback, useEffect, useState } from "react";
import { getAdminAuditLogs } from "@/api/admin-audit"; import { getAdminAuditLogs } from "@/api/admin-audit";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@@ -67,10 +67,6 @@ export function AuditLogsConsole(): React.ReactElement {
<CardHeader className="flex flex-row flex-wrap items-end justify-between gap-4"> <CardHeader className="flex flex-row flex-wrap items-end justify-between gap-4">
<div> <div>
<CardTitle></CardTitle> <CardTitle></CardTitle>
<CardDescription>
<code className="rounded bg-muted px-1">GET /api/v1/admin/audit-logs</code>{" "}
RBAC
</CardDescription>
</div> </div>
<Button type="button" variant="secondary" size="sm" onClick={() => void load()}> <Button type="button" variant="secondary" size="sm" onClick={() => void load()}>

View File

@@ -1,5 +1,5 @@
export const auditModuleMeta = { export const auditLogsModuleMeta = {
segment: "audit", segment: "audit-logs",
title: "审计日志", title: "审计日志",
description: "运营留痕查询(权限范围由后端按角色过滤)。", description: "",
} as const; } as const;

View File

@@ -1,5 +1,5 @@
export const authModuleMeta = { export const authModuleMeta = {
segment: "auth", segment: "login",
title: "登录", title: "登录",
description: "账号、密码与图形验证码登录;对接 Laravel Sanctum。", description: "",
} as const; } as const;

View File

@@ -7,7 +7,7 @@ import {
getPlayConfigVersions, getPlayConfigVersions,
getRiskCapVersions, getRiskCapVersions,
} from "@/api/admin-config"; } from "@/api/admin-config";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Table, Table,
TableBody, TableBody,
@@ -111,9 +111,6 @@ export function ConfigVersionsConsole() {
<Card> <Card>
<CardHeader className="space-y-1"> <CardHeader className="space-y-1">
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
<CardDescription>
线
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{error ? <p className="text-sm text-destructive">{error}</p> : null} {error ? <p className="text-sm text-destructive">{error}</p> : null}

View File

@@ -12,7 +12,7 @@ import {
putOddsItems, putOddsItems,
} from "@/api/admin-config"; } from "@/api/admin-config";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -346,9 +346,6 @@ export function OddsConfigDocScreen() {
<Card> <Card>
<CardHeader className="space-y-1"> <CardHeader className="space-y-1">
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
<CardDescription>
§5.5odds_value = × 10000NPR 100
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">

View File

@@ -13,7 +13,7 @@ import {
putPlayConfigItems, putPlayConfigItems,
} from "@/api/admin-config"; } from "@/api/admin-config";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { import {
Dialog, Dialog,
@@ -332,9 +332,6 @@ export function PlayConfigDocScreen() {
<Card> <Card>
<CardHeader className="space-y-1"> <CardHeader className="space-y-1">
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
<CardDescription>
§5.4稿
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">

View File

@@ -1,4 +1,4 @@
/** 对齐界面文档 §5.5:头 / 二 / 三 / 特别 / 安慰(含 starter / consolation 映射)。 */ /** 奖项档位顺序(含 starter / consolation。 */
export const PRIZE_SCOPE_ORDER = [ export const PRIZE_SCOPE_ORDER = [
"first", "first",

View File

@@ -12,7 +12,7 @@ import {
putOddsItems, putOddsItems,
} from "@/api/admin-config"; } from "@/api/admin-config";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@@ -246,9 +246,6 @@ export function RebateConfigDocScreen() {
<Card> <Card>
<CardHeader className="space-y-1"> <CardHeader className="space-y-1">
<CardTitle className="text-lg"> / </CardTitle> <CardTitle className="text-lg"> / </CardTitle>
<CardDescription>
§5.6 2D / 3D / 4D rebate_rate odds_versions
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6 max-w-xl"> <CardContent className="space-y-6 max-w-xl">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">

View File

@@ -11,7 +11,7 @@ import {
putRiskCapItems, putRiskCapItems,
} from "@/api/admin-config"; } from "@/api/admin-config";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -294,9 +294,6 @@ export function RiskCapDocScreen() {
</span> </span>
) : null} ) : null}
</CardTitle> </CardTitle>
<CardDescription>
§5.7
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-8"> <CardContent className="space-y-8">
<div className="flex flex-wrap items-end gap-4"> <div className="flex flex-wrap items-end gap-4">

View File

@@ -1,14 +1,29 @@
export const configHubMeta = { export const configHubMeta = {
title: "运营配置", title: "配置中心",
description: "玩法、赔率、回水、风控封顶(界面文档 §5.4§5.7", description: "",
}; } as const;
/** @deprecated 路由重定向至 `/admin/config/plays` */ export const configPlayConfigMeta = {
export const configPlaySwitchesMeta = { title: "玩法开关 · 运营配置" }; title: "玩法配置",
/** @deprecated 路由重定向至 `/admin/config/plays` */ description: "",
export const configPlayLimitsMeta = { title: "最小/最大下注 · 运营配置" }; } as const;
export const configPlayConfigMeta = { title: "玩法配置 · 运营配置" };
export const configOddsMeta = { title: "赔率配置 · 运营配置" }; export const configOddsMeta = {
export const configRebateMeta = { title: "佣金 / 回水 · 运营配置" }; title: "赔率配置",
export const configRiskCapMeta = { title: "风控封顶 · 运营配置" }; description: "",
export const configVersionsMeta = { title: "配置版本历史 · 运营配置" }; } as const;
export const configRebateMeta = {
title: "佣金 / 回水",
description: "",
} as const;
export const configRiskCapMeta = {
title: "风控封顶",
description: "",
} as const;
export const configVersionsMeta = {
title: "配置版本历史",
description: "",
} as const;

View File

@@ -14,28 +14,23 @@ import {
RefreshCw, RefreshCw,
ScrollText, ScrollText,
Shield, Shield,
ShieldCheck,
Ticket, Ticket,
TrendingUp, TrendingUp,
Wallet, Wallet,
} from "lucide-react"; } from "lucide-react";
import { getAdminDraw, getAdminDrawFinanceSummary, getAdminDraws } from "@/api/admin-draws"; import { getAdminDashboard } from "@/api/admin-dashboard";
import { getAdminRiskPools } from "@/api/admin-risk";
import { getAdminTransferOrders } from "@/api/admin-wallet";
import { getDrawCurrent } from "@/api/public-draw";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { LotteryApiBizError } from "@/types/api/errors"; import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance"; import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
import type { AdminDrawShowData } from "@/types/api/admin-draws";
import type { AdminRiskPoolRow } from "@/types/api/admin-risk"; import type { AdminRiskPoolRow } from "@/types/api/admin-risk";
import type { DrawCurrentSnapshot } from "@/types/api/public-draw"; import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
type HotPlayTab = "4D" | "3D" | "2D"; type HotPlayTab = "4D" | "3D" | "2D" | "特别";
type SoldOutBuckets = { type SoldOutBuckets = {
d4: number; d4: number;
@@ -71,22 +66,28 @@ function formatSignedMoneyMinor(minor: number, currencyCode: string | null): str
return `${s}${formatMoneyMinor(Math.abs(minor), currencyCode)}`; return `${s}${formatMoneyMinor(Math.abs(minor), currencyCode)}`;
} }
/** 与后端 {@see AdminDashboardSnapshotBuilder::soldOutBucketKey} 维度对齐 */
function poolPlayCategory(normalizedNumber: string): HotPlayTab | "other" { function poolPlayCategory(normalizedNumber: string): HotPlayTab | "other" {
const raw = normalizedNumber.trim(); const raw = normalizedNumber.trim();
if (/[A-Za-z]/.test(raw)) {
return "other";
}
const digits = raw.replace(/\D/g, ""); const digits = raw.replace(/\D/g, "");
const len = digits.length > 0 ? digits.length : raw.length; const digitLen = digits.length;
if (len >= 4) { const hasLetter = /[A-Za-z]/.test(raw);
if (hasLetter && digitLen < 3) {
return "特别";
}
if (digitLen >= 4) {
return "4D"; return "4D";
} }
if (len === 3) { if (digitLen === 3) {
return "3D"; return "3D";
} }
if (len === 2) { if (digitLen === 2) {
return "2D"; return "2D";
} }
if (hasLetter) {
return "特别";
}
return "other"; return "other";
} }
@@ -97,84 +98,6 @@ function topPoolsForTab(pools: AdminRiskPoolRow[], tab: HotPlayTab): AdminRiskPo
.slice(0, 10); .slice(0, 10);
} }
function soldOutBucketKey(normalizedNumber: string): keyof SoldOutBuckets {
const raw = normalizedNumber.trim();
if (/[A-Za-z]/.test(raw)) {
return "special";
}
const digits = raw.replace(/\D/g, "");
const len = digits.length > 0 ? digits.length : raw.length;
if (len >= 4) {
return "d4";
}
if (len === 3) {
return "d3";
}
if (len === 2) {
return "d2";
}
return "other";
}
async function aggregateRiskPools(
drawId: number,
): Promise<{ locked: number; cap: number; soldOutCount: number; top10: AdminRiskPoolRow[] }> {
let locked = 0;
let cap = 0;
let soldOutCount = 0;
let top10: AdminRiskPoolRow[] = [];
let page = 1;
const safetyMaxPages = 200;
while (page <= safetyMaxPages) {
const d = await getAdminRiskPools(drawId, {
page,
per_page: 100,
sort: "usage_desc",
});
if (page === 1) {
top10 = d.items.slice(0, 10);
}
for (const it of d.items) {
locked += it.locked_amount;
cap += it.total_cap_amount;
if (it.is_sold_out) {
soldOutCount += 1;
}
}
if (page >= d.meta.last_page) {
break;
}
page += 1;
}
return { locked, cap, soldOutCount, top10 };
}
async function aggregateSoldOutBuckets(drawId: number): Promise<SoldOutBuckets> {
const buckets: SoldOutBuckets = { d4: 0, d3: 0, d2: 0, special: 0, other: 0 };
let page = 1;
const safetyMaxPages = 80;
while (page <= safetyMaxPages) {
const d = await getAdminRiskPools(drawId, {
page,
per_page: 100,
sold_out_only: true,
sort: "usage_desc",
});
for (const it of d.items) {
buckets[soldOutBucketKey(it.normalized_number)] += 1;
}
if (page >= d.meta.last_page) {
break;
}
page += 1;
}
return buckets;
}
function RiskSemiGauge({ pct }: { pct: number }): ReactElement { function RiskSemiGauge({ pct }: { pct: number }): ReactElement {
const v = Math.min(100, Math.max(0, pct)); const v = Math.min(100, Math.max(0, pct));
const r = 76; const r = 76;
@@ -214,7 +137,7 @@ function HotBarChart({ rows }: { rows: AdminRiskPoolRow[] }): ReactElement {
return ( return (
<div className="flex h-52 flex-col"> <div className="flex h-52 flex-col">
<div className="relative flex flex-1 items-end gap-1.5 border-b border-slate-200/90 pb-0.5 pl-7"> <div className="relative flex h-[168px] min-h-[168px] w-full items-stretch gap-1.5 border-b border-slate-200/90 pb-0.5 pl-7">
<span <span
className="pointer-events-none absolute bottom-6 left-0 top-2 w-6 rotate-180 text-[10px] leading-tight text-muted-foreground [writing-mode:vertical-rl]" className="pointer-events-none absolute bottom-6 left-0 top-2 w-6 rotate-180 text-[10px] leading-tight text-muted-foreground [writing-mode:vertical-rl]"
aria-hidden aria-hidden
@@ -226,15 +149,22 @@ function HotBarChart({ rows }: { rows: AdminRiskPoolRow[] }): ReactElement {
) : ( ) : (
rows.map((row) => { rows.map((row) => {
const u = row.usage_ratio ?? 0; const u = row.usage_ratio ?? 0;
const h = Math.max(6, (u / maxU) * 100); const h = Math.max(8, (u / maxU) * 100);
return ( return (
<div key={row.normalized_number} className="flex min-w-0 flex-1 flex-col items-center gap-1">
<div <div
className="w-full max-w-[2.25rem] rounded-t-sm bg-[#c41e3a]/90 shadow-sm transition-all" key={row.normalized_number}
style={{ height: `${h}%` }} className="flex min-h-0 min-w-0 flex-1 flex-col items-stretch gap-1"
>
<div className="flex min-h-0 flex-1 flex-col justify-end">
<div
className="mx-auto w-full max-w-[2.25rem] rounded-t-sm bg-[#c41e3a]/90 shadow-sm transition-all"
style={{ height: `${h}%`, minHeight: 6 }}
title={`${row.normalized_number}: ${(u * 100).toFixed(1)}%`} title={`${row.normalized_number}: ${(u * 100).toFixed(1)}%`}
/> />
<span className="truncate font-mono text-[10px] text-[#1a365d]">{row.normalized_number}</span> </div>
<span className="truncate text-center font-mono text-[10px] text-[#1a365d]">
{row.normalized_number.trim()}
</span>
</div> </div>
); );
}) })
@@ -320,11 +250,12 @@ export function DashboardConsole(): ReactElement {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [hall, setHall] = useState<DrawCurrentSnapshot | null>(null); const [hall, setHall] = useState<DrawCurrentSnapshot | null>(null);
const [drawId, setDrawId] = useState<number | null>(null); const [drawId, setDrawId] = useState<number | null>(null);
const [finance, setFinance] = useState<AdminDrawFinanceSummaryData | null>(null); const [finance, setFinance] = useState<AdminDrawFinanceSummaryData | null>(null);
const [drawDetail, setDrawDetail] = useState<AdminDrawShowData | null>(null); const [pendingReview, setPendingReview] = useState<number | null>(null);
const [riskLocked, setRiskLocked] = useState(0); const [riskLocked, setRiskLocked] = useState(0);
const [riskCap, setRiskCap] = useState(0); const [riskCap, setRiskCap] = useState(0);
const [hotPoolSample, setHotPoolSample] = useState<AdminRiskPoolRow[]>([]); const [hotPoolSample, setHotPoolSample] = useState<AdminRiskPoolRow[]>([]);
@@ -339,8 +270,9 @@ export function DashboardConsole(): ReactElement {
setLoading(true); setLoading(true);
} }
setError(null); setError(null);
setNotice(null);
setFinance(null); setFinance(null);
setDrawDetail(null); setPendingReview(null);
setDrawId(null); setDrawId(null);
setRiskLocked(0); setRiskLocked(0);
setRiskCap(0); setRiskCap(0);
@@ -349,67 +281,35 @@ export function DashboardConsole(): ReactElement {
setAbnormalTransferTotal(null); setAbnormalTransferTotal(null);
try { try {
const current = await getDrawCurrent(); const d = await getAdminDashboard();
setHall(current); setHall(d.hall);
if (!current) { if (d.resolved_draw != null) {
return; setDrawId(d.resolved_draw.id);
} }
let resolvedId: number | null = null; if (d.finance != null) {
try { setFinance(d.finance);
const list = await getAdminDraws({ draw_no: current.draw_no, per_page: 1, page: 1 });
resolvedId = list.items[0]?.id ?? null;
} catch {
setError("已连接大厅期号,但需登录后台后才能拉取汇总与风控数据。");
return;
} }
if (d.draw != null) {
setPendingReview(d.draw.result_batch_counts.pending_review);
}
if (d.risk != null) {
setRiskLocked(d.risk.locked_amount);
setRiskCap(d.risk.cap_amount);
setHotPoolSample(d.risk.hot_pool_rows);
setSoldOutBuckets(d.risk.sold_out_buckets);
}
setAbnormalTransferTotal(d.abnormal_transfer_total);
if (resolvedId == null) { const noticeParts: string[] = d.warnings.map((w) => w.message);
setError("大厅期号在后台期号列表中未匹配到记录(或无权查看)。"); if (d.resolved_draw != null && !d.capabilities.draw_finance_risk) {
return; noticeParts.push("当前账号无开奖查看/管理权限,财务与风控数据未返回。");
} }
if (d.hall != null && !d.capabilities.wallet_transfer_view) {
setDrawId(resolvedId); noticeParts.push("当前账号无钱包对账查看权限,异常转账计数未返回。");
const results = await Promise.allSettled([
getAdminDrawFinanceSummary(resolvedId),
getAdminDraw(resolvedId),
aggregateRiskPools(resolvedId),
getAdminTransferOrders({ abnormal: true, per_page: 1, page: 1 }),
getAdminRiskPools(resolvedId, { page: 1, per_page: 100, sort: "usage_desc" }),
aggregateSoldOutBuckets(resolvedId),
]);
if (results[0].status === "fulfilled") {
setFinance(results[0].value);
}
if (results[1].status === "fulfilled") {
setDrawDetail(results[1].value);
}
if (results[2].status === "fulfilled") {
const r = results[2].value;
setRiskLocked(r.locked);
setRiskCap(r.cap);
}
if (results[3].status === "fulfilled") {
setAbnormalTransferTotal(results[3].value.total);
}
if (results[4].status === "fulfilled") {
setHotPoolSample(results[4].value.items);
}
if (results[5].status === "fulfilled") {
setSoldOutBuckets(results[5].value);
}
const failed = results.find((r) => r.status === "rejected");
if (failed && failed.status === "rejected") {
const msg =
failed.reason instanceof LotteryApiBizError
? failed.reason.message
: "部分数据加载失败(权限或网络)。";
setError(msg);
} }
setNotice(noticeParts.length > 0 ? noticeParts.join(" ") : null);
} catch (e) { } catch (e) {
const msg = const msg =
e instanceof LotteryApiBizError ? e.message : "加载失败,请检查 API 与登录状态。"; e instanceof LotteryApiBizError ? e.message : "加载失败,请检查 API 与登录状态。";
@@ -429,7 +329,6 @@ export function DashboardConsole(): ReactElement {
const currency = finance?.currency_code ?? null; const currency = finance?.currency_code ?? null;
const usagePct = riskCap > 0 ? (riskLocked / riskCap) * 100 : 0; const usagePct = riskCap > 0 ? (riskLocked / riskCap) * 100 : 0;
const pendingReview = drawDetail?.result_batch_counts?.pending_review ?? null;
const hotRows = useMemo(() => topPoolsForTab(hotPoolSample, hotTab), [hotPoolSample, hotTab]); const hotRows = useMemo(() => topPoolsForTab(hotPoolSample, hotTab), [hotPoolSample, hotTab]);
@@ -458,7 +357,6 @@ export function DashboardConsole(): ReactElement {
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold tracking-tight text-[#1a365d]"></h1> <h1 className="text-2xl font-bold tracking-tight text-[#1a365d]"></h1>
<p className="text-sm text-muted-foreground"> · </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">{todayLabel}</span> <span className="text-sm text-muted-foreground">{todayLabel}</span>
@@ -483,6 +381,13 @@ export function DashboardConsole(): ReactElement {
</Alert> </Alert>
) : null} ) : null}
{notice && !error ? (
<Alert className="border-sky-200 bg-sky-50 dark:border-sky-900/50 dark:bg-sky-950/30">
<AlertTitle></AlertTitle>
<AlertDescription>{notice}</AlertDescription>
</Alert>
) : null}
{/* Row 1 — 核心财务 KPI */} {/* Row 1 — 核心财务 KPI */}
<div className="grid gap-4 md:grid-cols-3"> <div className="grid gap-4 md:grid-cols-3">
{loading ? ( {loading ? (
@@ -647,11 +552,10 @@ export function DashboardConsole(): ReactElement {
<CardHeader className="flex flex-row flex-wrap items-end justify-between gap-2 space-y-0 pb-2"> <CardHeader className="flex flex-row flex-wrap items-end justify-between gap-2 space-y-0 pb-2">
<div> <div>
<CardTitle className="text-base font-semibold text-[#1a365d]"> Top 10</CardTitle> <CardTitle className="text-base font-semibold text-[#1a365d]"> Top 10</CardTitle>
<CardDescription> 100 </CardDescription>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div role="tablist" aria-label="玩法维度" className="flex gap-1 border-b border-transparent"> <div role="tablist" aria-label="玩法维度" className="flex gap-1 border-b border-transparent">
{(["4D", "3D", "2D"] as const).map((tab) => ( {(["4D", "3D", "2D", "特别"] as const).map((tab) => (
<button <button
key={tab} key={tab}
type="button" type="button"
@@ -692,7 +596,6 @@ export function DashboardConsole(): ReactElement {
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div> <div>
<CardTitle className="text-base font-semibold text-[#1a365d]"></CardTitle> <CardTitle className="text-base font-semibold text-[#1a365d]"></CardTitle>
<CardDescription></CardDescription>
</div> </div>
{drawId != null ? ( {drawId != null ? (
<Link <Link
@@ -727,7 +630,6 @@ export function DashboardConsole(): ReactElement {
<p className="mt-1 text-4xl font-bold tabular-nums text-[#c41e3a]"> <p className="mt-1 text-4xl font-bold tabular-nums text-[#c41e3a]">
{pendingReview ?? "—"} {pendingReview ?? "—"}
</p> </p>
<p className="text-xs text-muted-foreground"></p>
</div> </div>
</div> </div>
{drawId != null ? ( {drawId != null ? (
@@ -752,7 +654,6 @@ export function DashboardConsole(): ReactElement {
<p className="mt-1 text-4xl font-bold tabular-nums text-[#c41e3a]"> <p className="mt-1 text-4xl font-bold tabular-nums text-[#c41e3a]">
{abnormalTransferTotal ?? "—"} {abnormalTransferTotal ?? "—"}
</p> </p>
<p className="text-xs text-muted-foreground"></p>
</div> </div>
</div> </div>
<Link <Link
@@ -785,10 +686,6 @@ export function DashboardConsole(): ReactElement {
</CardContent> </CardContent>
</Card> </Card>
<footer className="flex items-center justify-center gap-2 text-xs text-muted-foreground">
<ShieldCheck className="size-4 text-[#c41e3a]" aria-hidden />
<span> · 访</span>
</footer>
</div> </div>
); );
} }

View File

@@ -1,5 +1,5 @@
export const dashboardModuleMeta = { export const dashboardModuleMeta = {
segment: "dashboard", segment: "dashboard",
title: "仪表盘", title: "仪表盘",
description: "仪表盘:大厅当前期、投注/派彩/风控占用与快捷入口。", description: "",
} as const; } as const;

View File

@@ -5,7 +5,7 @@ import { useCallback, useEffect, useState } from "react";
import { getAdminDraw } from "@/api/admin-draws"; import { getAdminDraw } from "@/api/admin-draws";
import { buttonVariants } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter"; import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
@@ -70,10 +70,6 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
<CardDescription>
<code className="rounded bg-muted px-1 py-0.5 text-xs">status</code>{" "}
tick DB
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
@@ -103,7 +99,6 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base"></CardTitle> <CardTitle className="text-base"></CardTitle>
<CardDescription> / </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
<div className="flex flex-wrap gap-6 text-sm"> <div className="flex flex-wrap gap-6 text-sm">

View File

@@ -5,7 +5,7 @@ import { useCallback, useEffect, useState } from "react";
import { getAdminDrawFinanceSummary } from "@/api/admin-draws"; import { getAdminDrawFinanceSummary } from "@/api/admin-draws";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Table, Table,
TableBody, TableBody,
@@ -18,7 +18,6 @@ import { cn } from "@/lib/utils";
import { LotteryApiBizError } from "@/types/api/errors"; import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance"; import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
/** PRD §15.4:单期投注/派彩与结算批次(`GET …/draws/{id}/finance-summary` */
export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactElement { export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactElement {
const idNum = Number(drawId); const idNum = Number(drawId);
const [data, setData] = useState<AdminDrawFinanceSummaryData | null>(null); const [data, setData] = useState<AdminDrawFinanceSummaryData | null>(null);
@@ -57,16 +56,11 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
return <p className="text-destructive text-sm">{err ?? "无数据"}</p>; return <p className="text-destructive text-sm">{err ?? "无数据"}</p>;
} }
const cur = data.currency_code ?? "—";
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
<CardDescription>
{cur} = + Jackpot
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="grid gap-3 text-sm sm:grid-cols-2 lg:grid-cols-3"> <CardContent className="grid gap-3 text-sm sm:grid-cols-2 lg:grid-cols-3">
<div> <div>
@@ -120,7 +114,6 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base"></CardTitle> <CardTitle className="text-base"></CardTitle>
<CardDescription> `settlement_batches` </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{data.settlement_batches.length === 0 ? ( {data.settlement_batches.length === 0 ? (

View File

@@ -1,17 +1,2 @@
/** /** 开奖结果发布权限 slug */
* 后台开奖域与 PRD 阶段 3 · §11.4 / §11.7 验收对照(路径以 `API_V1_PREFIX=/api/v1` 为前缀)。
*
* - 期号列表:`GET …/admin/draws`
* - 期号详情 / 状态:`GET …/admin/draws/{draw}`
* - 开奖结果批次(含待审核、已发布):`GET …/admin/draws/{draw}/result-batches`
* - 发布:`POST …/admin/draws/{draw}/result-batches/{batch}/publish`
*/
export const DRAW_ADMIN_API_PRD_LINES = [
"GET /api/v1/admin/draws",
"GET /api/v1/admin/draws/{draw}",
"GET /api/v1/admin/draws/{draw}/result-batches",
"POST /api/v1/admin/draws/{draw}/result-batches/{batch}/publish",
] as const;
/** 具备其一即可执行发布、进入发布页(与 Laravel `prd.draw_result.manage` 一致) */
export const PRD_DRAW_RESULT_MANAGE = "prd.draw_result.manage" as const; export const PRD_DRAW_RESULT_MANAGE = "prd.draw_result.manage" as const;

View File

@@ -7,7 +7,7 @@ import { toast } from "sonner";
import { getAdminDrawResultBatches, postAdminPublishResultBatch } from "@/api/admin-draws"; import { getAdminDrawResultBatches, postAdminPublishResultBatch } from "@/api/admin-draws";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Table, Table,
TableBody, TableBody,
@@ -112,38 +112,24 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
<CardDescription>
<span className="font-mono font-medium">{data.draw_no}</span> · v
{batch.result_version}{" "}
<span className="rounded bg-muted px-1 py-0.5 font-mono text-xs">{batch.status}</span>
· {" "}
<code className="rounded bg-muted px-1 text-xs">
POST /result-batches/{batch.id}/publish
</code>
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{!canManageDraw ? ( {!canManageDraw ? (
<Alert variant="destructive"> <Alert variant="destructive">
<AlertTitle></AlertTitle> <AlertTitle></AlertTitle>
<AlertDescription> <AlertDescription></AlertDescription>
<code className="rounded bg-background/80 px-1">{PRD_DRAW_RESULT_MANAGE}</code>{" "}
</AlertDescription>
</Alert> </Alert>
) : null} ) : null}
{!canPublish && canManageDraw ? ( {!canPublish && canManageDraw ? (
<Alert> <Alert>
<AlertTitle></AlertTitle> <AlertTitle></AlertTitle>
<AlertDescription> <AlertDescription>{batch.status}</AlertDescription>
pending_review {batch.status}
</AlertDescription>
</Alert> </Alert>
) : null} ) : null}
{canPublish ? ( {canPublish ? (
<Alert> <Alert>
<AlertTitle></AlertTitle> <AlertTitle></AlertTitle>
<AlertDescription></AlertDescription> <AlertDescription></AlertDescription>
</Alert> </Alert>
) : null} ) : null}

View File

@@ -5,7 +5,7 @@ import { useCallback, useEffect, useState } from "react";
import { getAdminDrawResultBatches } from "@/api/admin-draws"; import { getAdminDrawResultBatches } from "@/api/admin-draws";
import { buttonVariants } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Table, Table,
TableBody, TableBody,
@@ -74,8 +74,7 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
<div> <div>
<h2 className="text-lg font-semibold"></h2> <h2 className="text-lg font-semibold"></h2>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{data.draw_no} · <DrawStatusBadge status={data.draw_status} /> · {data.draw_no} · <DrawStatusBadge status={data.draw_status} />
</p> </p>
</div> </div>
<Link <Link
@@ -89,7 +88,7 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
{published.length === 0 ? ( {published.length === 0 ? (
<Card> <Card>
<CardContent className="py-8 text-center text-sm text-muted-foreground"> <CardContent className="py-8 text-center text-sm text-muted-foreground">
RNG
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
@@ -104,9 +103,9 @@ function BatchTable({ batch }: { batch: AdminDrawBatchRow }) {
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base"> v{batch.result_version}</CardTitle> <CardTitle className="text-base"> v{batch.result_version}</CardTitle>
<CardDescription className="font-mono text-xs"> <p className="font-mono text-xs text-muted-foreground">
RNG {batch.rng_seed_hash ?? "—"} · {batch.confirmed_at ?? "—"} RNG {batch.rng_seed_hash ?? "—"} · {batch.confirmed_at ?? "—"}
</CardDescription> </p>
</CardHeader> </CardHeader>
<CardContent className="overflow-x-auto pt-0"> <CardContent className="overflow-x-auto pt-0">
<Table> <Table>

View File

@@ -5,7 +5,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { getAdminDrawResultBatches } from "@/api/admin-draws"; import { getAdminDrawResultBatches } from "@/api/admin-draws";
import { buttonVariants } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Table, Table,
TableBody, TableBody,
@@ -74,13 +74,9 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
<CardDescription> <p className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
RNG {" "} <DrawStatusBadge status={data.draw_status} />
<code className="rounded bg-muted px-1 text-xs">{PRD_DRAW_RESULT_MANAGE}</code>{" "} </p>
{" "}
<code className="rounded bg-muted px-1 text-xs">POST /result-batches//publish</code>
DB <DrawStatusBadge status={data.draw_status} />
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{pending.length === 0 ? ( {pending.length === 0 ? (

View File

@@ -5,7 +5,7 @@ import { useCallback, useEffect, useState } from "react";
import { getAdminDraws } from "@/api/admin-draws"; import { getAdminDraws } from "@/api/admin-draws";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@@ -29,7 +29,6 @@ import { cn } from "@/lib/utils";
import { LotteryApiBizError } from "@/types/api/errors"; import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminDrawListData, AdminDrawListItem } from "@/types/api/admin-draws"; import type { AdminDrawListData, AdminDrawListItem } from "@/types/api/admin-draws";
import { DRAW_ADMIN_API_PRD_LINES } from "./draw-prd";
import { DrawStatusBadge } from "./draw-status-badge"; import { DrawStatusBadge } from "./draw-status-badge";
/** 下拉「不限」;请求时不传 status */ /** 下拉「不限」;请求时不传 status */
@@ -104,14 +103,6 @@ export function DrawsIndexConsole() {
<Card> <Card>
<CardHeader className="space-y-1"> <CardHeader className="space-y-1">
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
<CardDescription className="space-y-2">
<span>
3 · §11.4
</span>
<span className="block font-mono text-[11px] leading-relaxed text-muted-foreground">
{DRAW_ADMIN_API_PRD_LINES.join(" · ")}
</span>
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{/* Grid桌面端标签一行 / 控件一行,避免 flex+items-end 与各列实际高度不一致;移动端单列自上而下 */} {/* Grid桌面端标签一行 / 控件一行,避免 flex+items-end 与各列实际高度不一致;移动端单列自上而下 */}

View File

@@ -1,6 +1,5 @@
export const drawsModuleMeta = { export const drawsModuleMeta = {
segment: "draws", segment: "draws",
title: "开奖", title: "开奖",
description: description: "",
"PRD 阶段3 §11.4:期号列表 / 状态 / 开奖结果 / 审核队列 / 发布POST …/result-batches/…/publish路由 `/admin/draws/[id]/publish/[batchId]` 与接口动词对齐。",
} as const; } as const;

View File

@@ -5,7 +5,7 @@ import { useCallback, useEffect, useState } from "react";
import { getAdminJackpotPools, putAdminJackpotPool } from "@/api/admin-jackpot"; import { getAdminJackpotPools, putAdminJackpotPool } from "@/api/admin-jackpot";
import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { ModuleScaffold } from "@/components/admin/module-scaffold";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@@ -106,7 +106,6 @@ export function JackpotPoolsConsole() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base">Jackpot </CardTitle> <CardTitle className="text-base">Jackpot </CardTitle>
<CardDescription></CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-8"> <CardContent className="space-y-8">
{loading ? <p className="text-muted-foreground text-sm"></p> : null} {loading ? <p className="text-muted-foreground text-sm"></p> : null}

View File

@@ -6,7 +6,7 @@ import { getAdminJackpotContributions, getAdminJackpotPayoutLogs } from "@/api/a
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { ModuleScaffold } from "@/components/admin/module-scaffold";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@@ -97,7 +97,6 @@ export function JackpotRecordsConsole() {
<Card className="mb-6"> <Card className="mb-6">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle> <CardTitle className="text-base"></CardTitle>
<CardDescription></CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-3 sm:flex-row sm:items-end"> <CardContent className="flex flex-col gap-3 sm:flex-row sm:items-end">
<div className="flex max-w-xs flex-1 flex-col gap-1.5"> <div className="flex max-w-xs flex-1 flex-col gap-1.5">
@@ -121,7 +120,6 @@ export function JackpotRecordsConsole() {
<Card className="mb-8"> <Card className="mb-8">
<CardHeader> <CardHeader>
<CardTitle className="text-base">Jackpot </CardTitle> <CardTitle className="text-base">Jackpot </CardTitle>
<CardDescription></CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{loadingP && !payouts ? ( {loadingP && !payouts ? (
@@ -177,7 +175,6 @@ export function JackpotRecordsConsole() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base">Jackpot </CardTitle> <CardTitle className="text-base">Jackpot </CardTitle>
<CardDescription></CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{loadingC && !contribs ? ( {loadingC && !contribs ? (

View File

@@ -1,4 +1,4 @@
export const jackpotModuleMeta = { export const jackpotModuleMeta = {
title: "Jackpot", title: "奖池",
description: "奖池配置与蓄水 / 派彩记录", description: "",
} as const; } as const;

View File

@@ -1,5 +1,5 @@
export const playersModuleMeta = { export const playersModuleMeta = {
segment: "players", segment: "players",
title: "玩家查询", title: "玩家",
description: "按玩家 ID 查钱包等PRD 15.3);列表/冻结待管理端用户 API。", description: "",
} as const; } as const;

View File

@@ -1,23 +1,15 @@
"use client"; "use client";
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardHeader, CardTitle } from "@/components/ui/card";
import { PlayerWalletPanel } from "@/modules/wallet/wallet-console"; import { PlayerWalletPanel } from "@/modules/wallet/wallet-console";
/** PRD 15.3:玩家查询(当前对接 `GET .../players/{id}/wallets`)。 */
export function PlayersConsole(): React.ReactElement { export function PlayersConsole(): React.ReactElement {
return ( return (
<div className="flex w-full max-w-none flex-col gap-6"> <div className="flex w-full max-w-none flex-col gap-6">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
<CardDescription>
{" "}
<code className="rounded bg-muted px-1">
prd.users.manage | view_finance | view_cs
</code>{" "}
/ API
</CardDescription>
</CardHeader> </CardHeader>
</Card> </Card>
<PlayerWalletPanel /> <PlayerWalletPanel />

View File

@@ -1,5 +1,5 @@
export const reconcileModuleMeta = { export const reconcileModuleMeta = {
segment: "reconcile", segment: "reconcile",
title: "对账", title: "对账",
description: "钱包对账任务列表、创建与明细PRD §8 钱包对账)。", description: "",
} as const; } as const;

View File

@@ -11,7 +11,7 @@ import {
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@@ -143,10 +143,6 @@ export function ReconcileConsole(): React.ReactElement {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
<CardDescription>
<code className="rounded bg-muted px-1">prd.wallet_reconcile.manage</code>
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="grid max-w-3xl gap-4"> <CardContent className="grid max-w-3xl gap-4">
<div className="grid gap-1.5"> <div className="grid gap-1.5">
@@ -193,16 +189,13 @@ export function ReconcileConsole(): React.ReactElement {
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm"></p>
·
</p>
)} )}
<Card> <Card>
<CardHeader className="flex flex-row flex-wrap items-end justify-between gap-4"> <CardHeader className="flex flex-row flex-wrap items-end justify-between gap-4">
<div> <div>
<CardTitle></CardTitle> <CardTitle></CardTitle>
<CardDescription></CardDescription>
</div> </div>
<Button type="button" variant="secondary" size="sm" onClick={() => void loadJobs()}> <Button type="button" variant="secondary" size="sm" onClick={() => void loadJobs()}>
@@ -290,13 +283,16 @@ export function ReconcileConsole(): React.ReactElement {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle> #{selectedId} </CardTitle> <CardTitle> #{selectedId} </CardTitle>
<CardDescription>
{itemsLoading ? "加载中…" : items?.job_no ?? ""}
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{itemsLoading && !items ? (
<p className="text-sm text-muted-foreground"></p>
) : null}
{items ? ( {items ? (
<> <>
{items.job_no ? (
<p className="font-mono text-sm text-muted-foreground">{items.job_no}</p>
) : null}
<div className="rounded-md border"> <div className="rounded-md border">
<Table> <Table>
<TableHeader> <TableHeader>

View File

@@ -1,5 +1,5 @@
export const reportsModuleMeta = { export const reportsModuleMeta = {
segment: "reports", segment: "reports",
title: "报表导出", title: "报表",
description: "创建异步导出任务并查看历史记录PRD §8 报表权限)。", description: "",
} as const; } as const;

View File

@@ -7,7 +7,7 @@ import { getAdminReportJobs, postAdminReportJob } from "@/api/admin-reports";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
Select, Select,
@@ -105,11 +105,6 @@ export function ReportsConsole(): React.ReactElement {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
<CardDescription>
<code className="rounded bg-muted px-1">POST /api/v1/admin/report-jobs</code>
<code className="rounded bg-muted px-1">output_path</code>{" "}
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <CardContent className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div className="grid gap-1.5"> <div className="grid gap-1.5">
@@ -177,7 +172,6 @@ export function ReportsConsole(): React.ReactElement {
<CardHeader className="flex flex-row flex-wrap items-end justify-between gap-4"> <CardHeader className="flex flex-row flex-wrap items-end justify-between gap-4">
<div> <div>
<CardTitle></CardTitle> <CardTitle></CardTitle>
<CardDescription></CardDescription>
</div> </div>
<Button type="button" variant="secondary" size="sm" onClick={() => void load()}> <Button type="button" variant="secondary" size="sm" onClick={() => void load()}>

View File

@@ -1,5 +1,5 @@
export const riskModuleMeta = { export const riskModuleMeta = {
segment: "risk", segment: "risk",
title: "风控", title: "风控",
description: "规则、告警与黑名单(占位)。", description: "",
} as const; } as const;

View File

@@ -48,10 +48,6 @@ export function RiskDrawHeader({ drawId }: { drawId: number }) {
<DrawStatusBadge status={draw.status} /> <DrawStatusBadge status={draw.status} />
<span className="text-xs opacity-80">{draw.hall_preview_status}</span> <span className="text-xs opacity-80">{draw.hall_preview_status}</span>
</p> </p>
<p className="text-xs text-muted-foreground">
`risk_pools` /
`risk_pool_lock_logs`
</p>
</div> </div>
); );
} }

View File

@@ -5,7 +5,7 @@ import { useCallback, useEffect, useState } from "react";
import { getAdminDraws } from "@/api/admin-draws"; import { getAdminDraws } from "@/api/admin-draws";
import { buttonVariants } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Table, Table,
TableBody, TableBody,
@@ -52,9 +52,6 @@ export function RiskIndexConsole() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
<CardDescription>
§13.4
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{error ? <p className="text-sm text-destructive">{error}</p> : null} {error ? <p className="text-sm text-destructive">{error}</p> : null}

View File

@@ -5,7 +5,7 @@ import { useCallback, useEffect, useState } from "react";
import { getAdminRiskPoolLockLogs } from "@/api/admin-risk"; import { getAdminRiskPoolLockLogs } from "@/api/admin-risk";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@@ -77,9 +77,6 @@ export function RiskLockLogsConsole({ drawId }: { drawId: number }) {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
<CardDescription>
/ `risk_pool_lock_logs`
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid max-w-full gap-3 sm:grid-cols-[minmax(0,8rem)_minmax(0,10rem)_auto] sm:items-end"> <div className="grid max-w-full gap-3 sm:grid-cols-[minmax(0,8rem)_minmax(0,10rem)_auto] sm:items-end">

View File

@@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from "react";
import { getAdminRiskPoolDetail } from "@/api/admin-risk"; import { getAdminRiskPoolDetail } from "@/api/admin-risk";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { buttonVariants } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Table, Table,
TableBody, TableBody,
@@ -62,9 +62,9 @@ export function RiskPoolDetailConsole({
<Card className="border-destructive/40"> <Card className="border-destructive/40">
<CardHeader> <CardHeader>
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
<CardDescription>{error}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="space-y-2">
<p className="text-sm text-destructive">{error}</p>
<Link <Link
href={`/admin/risk/draws/${drawId}/pools`} href={`/admin/risk/draws/${drawId}/pools`}
className={cn(buttonVariants({ variant: "outline", size: "sm" }))} className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
@@ -102,9 +102,7 @@ export function RiskPoolDetailConsole({
<CardTitle className="text-lg"> <CardTitle className="text-lg">
<span className="font-mono">{pool.normalized_number}</span> <span className="font-mono">{pool.normalized_number}</span>
</CardTitle> </CardTitle>
<CardDescription> <p className="text-sm text-muted-foreground"> {data.draw_no}</p>
{data.draw_no} · `risk_pools` 0 §6.4
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"> <CardContent className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<div className="rounded-lg border bg-muted/40 p-3"> <div className="rounded-lg border bg-muted/40 p-3">
@@ -139,7 +137,6 @@ export function RiskPoolDetailConsole({
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base"> / </CardTitle> <CardTitle className="text-base"> / </CardTitle>
<CardDescription></CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="overflow-x-auto rounded-md border"> <div className="overflow-x-auto rounded-md border">

View File

@@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from "react";
import { getAdminRiskPools } from "@/api/admin-risk"; import { getAdminRiskPools } from "@/api/admin-risk";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { buttonVariants } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
Select, Select,
@@ -39,7 +39,6 @@ const SORT_OPTIONS: { value: "usage_desc" | "locked_desc" | "remaining_asc" | "n
type RiskPoolsConsoleProps = { type RiskPoolsConsoleProps = {
drawId: number; drawId: number;
title: string; title: string;
description: string;
soldOutOnly: boolean; soldOutOnly: boolean;
defaultSort: "usage_desc" | "locked_desc" | "remaining_asc" | "number_asc"; defaultSort: "usage_desc" | "locked_desc" | "remaining_asc" | "number_asc";
allowSortChange?: boolean; allowSortChange?: boolean;
@@ -48,7 +47,6 @@ type RiskPoolsConsoleProps = {
export function RiskPoolsConsole({ export function RiskPoolsConsole({
drawId, drawId,
title, title,
description,
soldOutOnly, soldOutOnly,
defaultSort, defaultSort,
allowSortChange = false, allowSortChange = false,
@@ -91,7 +89,6 @@ export function RiskPoolsConsole({
<Card> <Card>
<CardHeader className="space-y-2"> <CardHeader className="space-y-2">
<CardTitle className="text-lg">{title}</CardTitle> <CardTitle className="text-lg">{title}</CardTitle>
<CardDescription>{description}</CardDescription>
{allowSortChange ? ( {allowSortChange ? (
<div className="flex max-w-xs flex-col gap-2 sm:flex-row sm:items-end"> <div className="flex max-w-xs flex-col gap-2 sm:flex-row sm:items-end">
<div className="space-y-1.5"> <div className="space-y-1.5">

View File

@@ -1,5 +1,5 @@
export const serviceDeskModuleMeta = { export const serviceDeskModuleMeta = {
segment: "service_desk", segment: "service-desk",
title: "客服 / 财务", title: "服务台",
description: "PRD §15.4 能力入口:注单、流水、转账失败、期号收支、报表。", description: "",
} as const; } as const;

View File

@@ -1,58 +1,29 @@
"use client"; "use client";
import type { ReactElement } from "react";
import Link from "next/link"; import Link from "next/link";
import { buttonVariants } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const items: { title: string; desc: string; href: string; hint: string }[] = [ const items: { title: string; href: string }[] = [
{ { title: "玩家注单", href: "/admin/tickets" },
title: "玩家注单", { title: "钱包流水", href: "/admin/wallet/transactions" },
desc: "按玩家 ID 查注单、失败原因、中奖金额。", { title: "转账单", href: "/admin/wallet/transfer-orders" },
href: "/admin/tickets", { title: "期号列表", href: "/admin/draws" },
hint: "GET …/players/{id}/ticket-items", { title: "报表导出", href: "/admin/reports" },
},
{
title: "钱包流水",
desc: "按玩家、单号、日期筛选流水。",
href: "/admin/wallet/transactions",
hint: "GET …/wallet/transactions",
},
{
title: "转账单与失败原因",
desc: "筛选 status=failed 或异常单,查看 fail_reason 列。",
href: "/admin/wallet/transfer-orders",
hint: "GET …/wallet/transfer-orders",
},
{
title: "期号列表 → 期号收支",
desc: "开奖 → 点期号 →「期号收支」看当期投注/派彩与结算批次。",
href: "/admin/draws",
hint: "GET …/draws/{id}/finance-summary",
},
{
title: "报表导出",
desc: "异步导出任务(如钱包流水日报)。",
href: "/admin/reports",
hint: "POST …/report-jobs",
},
]; ];
/** PRD §15.4 客服/财务能力入口聚合 */ export function ServiceDeskConsole(): ReactElement {
export function ServiceDeskConsole(): React.ReactElement {
return ( return (
<div className="grid gap-6 md:grid-cols-2"> <div className="grid gap-6 md:grid-cols-2">
{items.map((it) => ( {items.map((it) => (
<Card key={it.href}> <Card key={it.href}>
<CardHeader> <CardHeader>
<CardTitle className="text-base">{it.title}</CardTitle> <CardTitle className="text-base">{it.title}</CardTitle>
<CardDescription>{it.desc}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-3"> <CardContent>
<code className="block rounded-md bg-muted px-2 py-1.5 font-mono text-[11px] text-muted-foreground">
{it.hint}
</code>
<Link href={it.href} className={cn(buttonVariants({ size: "sm" }), "w-fit")}> <Link href={it.href} className={cn(buttonVariants({ size: "sm" }), "w-fit")}>
</Link> </Link>

View File

@@ -1,5 +1,5 @@
export const settingsModuleMeta = { export const settingsModuleMeta = {
segment: "settings", segment: "settings",
title: "系统设置", title: "系统设置",
description: "运营参数与权限(占位)。", description: "",
} as const; } as const;

View File

@@ -1,4 +1,4 @@
export const settlementModuleMeta = { export const settlementModuleMeta = {
title: "结算批次", title: "结算",
description: "按期查看结算批次与注单结算明细", description: "",
} as const; } as const;

View File

@@ -7,7 +7,7 @@ import { getAdminSettlementBatch, getAdminSettlementBatchDetails } from "@/api/a
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { ModuleScaffold } from "@/components/admin/module-scaffold";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Table, Table,
TableBody, TableBody,
@@ -74,9 +74,9 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
<Card className="border-destructive/40"> <Card className="border-destructive/40">
<CardHeader> <CardHeader>
<CardTitle className="text-base"></CardTitle> <CardTitle className="text-base"></CardTitle>
<CardDescription>{err}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="space-y-2">
<p className="text-sm text-destructive">{err}</p>
<Button type="button" size="sm" variant="secondary" onClick={() => void load()}> <Button type="button" size="sm" variant="secondary" onClick={() => void load()}>
</Button> </Button>
@@ -88,10 +88,10 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
<Card className="mb-6"> <Card className="mb-6">
<CardHeader> <CardHeader>
<CardTitle className="font-mono text-base"> #{summary.id}</CardTitle> <CardTitle className="font-mono text-base"> #{summary.id}</CardTitle>
<CardDescription> <p className="text-sm text-muted-foreground">
{summary.draw_no ?? "—"} · {summary.draw_status ?? "—"} · v {summary.draw_no ?? "—"} · {summary.draw_status ?? "—"} · v
{summary.result_batch_version ?? "—"} {summary.result_batch_version ?? "—"}
</CardDescription> </p>
</CardHeader> </CardHeader>
<CardContent className="grid gap-2 text-sm sm:grid-cols-2"> <CardContent className="grid gap-2 text-sm sm:grid-cols-2">
<p> <p>
@@ -131,7 +131,6 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base"></CardTitle> <CardTitle className="text-base"></CardTitle>
<CardDescription></CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{details ? ( {details ? (

View File

@@ -7,7 +7,7 @@ import { getAdminSettlementBatches } from "@/api/admin-settlement";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { ModuleScaffold } from "@/components/admin/module-scaffold";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@@ -90,12 +90,10 @@ export function SettlementBatchesConsole() {
<ModuleScaffold> <ModuleScaffold>
<div className="mb-6"> <div className="mb-6">
<h1 className="text-lg font-semibold tracking-tight">{settlementModuleMeta.title}</h1> <h1 className="text-lg font-semibold tracking-tight">{settlementModuleMeta.title}</h1>
<p className="text-muted-foreground mt-1 text-sm">{settlementModuleMeta.description}</p>
</div> </div>
<Card className="mb-6"> <Card className="mb-6">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle> <CardTitle className="text-base"></CardTitle>
<CardDescription></CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-end"> <CardContent className="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-end">
<div className="flex min-w-[12rem] flex-1 flex-col gap-1.5"> <div className="flex min-w-[12rem] flex-1 flex-col gap-1.5">
@@ -132,7 +130,6 @@ export function SettlementBatchesConsole() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base"></CardTitle> <CardTitle className="text-base"></CardTitle>
<CardDescription></CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{error ? <p className="text-destructive text-sm">{error}</p> : null} {error ? <p className="text-destructive text-sm">{error}</p> : null}

View File

@@ -1,5 +1,5 @@
export const ticketsModuleMeta = { export const ticketsModuleMeta = {
segment: "tickets", segment: "tickets",
title: "玩家注单", title: "注单",
description: "PRD §15.4:按玩家查注单(管理端 ticket-items API", description: "",
} as const; } as const;

View File

@@ -5,7 +5,7 @@ import { useCallback, useEffect, useState } from "react";
import { getAdminPlayerTicketItems } from "@/api/admin-player-tickets"; import { getAdminPlayerTicketItems } from "@/api/admin-player-tickets";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@@ -19,7 +19,6 @@ import {
import { LotteryApiBizError } from "@/types/api/errors"; import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminPlayerTicketItemsData } from "@/types/api/admin-player-tickets"; import type { AdminPlayerTicketItemsData } from "@/types/api/admin-player-tickets";
/** PRD §15.4:按玩家主键查注单(`GET …/admin/players/{id}/ticket-items` */
export function PlayerTicketsConsole(): React.ReactElement { export function PlayerTicketsConsole(): React.ReactElement {
const [playerIdDraft, setPlayerIdDraft] = useState(""); const [playerIdDraft, setPlayerIdDraft] = useState("");
const [drawNoDraft, setDrawNoDraft] = useState(""); const [drawNoDraft, setDrawNoDraft] = useState("");
@@ -77,10 +76,6 @@ export function PlayerTicketsConsole(): React.ReactElement {
<Card className="w-full max-w-none"> <Card className="w-full max-w-none">
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
<CardDescription>
<code className="rounded bg-muted px-1 text-xs">prd.users.view_cs | view_finance | manage</code>
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex flex-wrap items-end gap-3"> <div className="flex flex-wrap items-end gap-3">

View File

@@ -1,5 +1,5 @@
export const walletModuleMeta = { export const walletModuleMeta = {
segment: "wallet", segment: "wallet",
title: "钱包流水与对账", title: "钱包流水与对账",
description:"" description: "",
} as const; } as const;

View File

@@ -13,7 +13,7 @@ import { AdminDateRangeField } from "@/components/admin/admin-date-range-field";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@@ -243,11 +243,6 @@ export function TransferOrdersPanel(): React.ReactElement {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
<CardDescription>
§5.11 {" "}
<code className="rounded bg-muted px-1">player_id</code> ID PRD §15.4{" "}
<code className="rounded bg-muted px-1">status=failed</code>
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
@@ -510,10 +505,6 @@ export function WalletTxnsPanel(): React.ReactElement {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
<CardDescription>
§5.11 {" "}
<code className="rounded bg-muted px-1">pending_reconcile</code>
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
@@ -771,10 +762,6 @@ export function PlayerWalletPanel(): React.ReactElement {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
<CardDescription>
<code className="rounded bg-muted px-1">players.id</code>{" "}
§5.12
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex flex-wrap items-end gap-2"> <div className="flex flex-wrap items-end gap-2">

View File

@@ -0,0 +1,62 @@
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
import type { AdminRiskPoolRow } from "@/types/api/admin-risk";
import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
export type AdminDashboardWarning = {
code: string;
message: string;
};
export type AdminDashboardResolvedDraw = {
id: number;
draw_no: string;
};
export type AdminDashboardDrawPanel = {
id: number;
draw_no: string;
business_date: string;
sequence_no: number;
status: string;
hall_preview_status: string;
result_batch_counts: {
total: number;
pending_review: number;
published: number;
};
};
export type AdminDashboardSoldOutBuckets = {
d4: number;
d3: number;
d2: number;
special: number;
other: number;
};
export type AdminDashboardRiskSnapshot = {
draw_id: number;
draw_no: string;
locked_amount: number;
cap_amount: number;
usage_percent: number;
hot_pool_rows: AdminRiskPoolRow[];
sold_out_buckets: AdminDashboardSoldOutBuckets;
};
export type AdminDashboardCapabilities = {
draw_finance_risk: boolean;
wallet_transfer_view: boolean;
};
/** `GET /api/v1/admin/dashboard` */
export type AdminDashboardData = {
hall: DrawCurrentSnapshot | null;
resolved_draw: AdminDashboardResolvedDraw | null;
finance: AdminDrawFinanceSummaryData | null;
draw: AdminDashboardDrawPanel | null;
risk: AdminDashboardRiskSnapshot | null;
abnormal_transfer_total: number | null;
warnings: AdminDashboardWarning[];
capabilities: AdminDashboardCapabilities;
};