feat: 扩展开奖与结算管理,支持手动操作、导出和版本展示
This commit is contained in:
@@ -5,8 +5,12 @@ import { API_V1_PREFIX } from "./paths";
|
|||||||
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
|
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
|
||||||
import type {
|
import type {
|
||||||
AdminDrawBatchesData,
|
AdminDrawBatchesData,
|
||||||
|
AdminDrawActionResponse,
|
||||||
AdminDrawListData,
|
AdminDrawListData,
|
||||||
|
AdminDrawBatchCreateResponse,
|
||||||
|
AdminDrawManualBatchPayload,
|
||||||
AdminDrawPublishResponse,
|
AdminDrawPublishResponse,
|
||||||
|
AdminDrawPlanGenerateResponse,
|
||||||
AdminDrawShowData,
|
AdminDrawShowData,
|
||||||
} from "@/types/api/admin-draws";
|
} from "@/types/api/admin-draws";
|
||||||
|
|
||||||
@@ -47,3 +51,33 @@ export async function postAdminPublishResultBatch(
|
|||||||
`${A}/draws/${drawId}/result-batches/${batchId}/publish`,
|
`${A}/draws/${drawId}/result-batches/${batchId}/publish`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function postAdminGenerateDrawPlan(): Promise<AdminDrawPlanGenerateResponse> {
|
||||||
|
return adminRequest.post<AdminDrawPlanGenerateResponse>(`${A}/draws/generate-plan`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postAdminManualCloseDraw(drawId: number): Promise<AdminDrawActionResponse> {
|
||||||
|
return adminRequest.post<AdminDrawActionResponse>(`${A}/draws/${drawId}/manual-close`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postAdminCancelDraw(drawId: number): Promise<AdminDrawActionResponse> {
|
||||||
|
return adminRequest.post<AdminDrawActionResponse>(`${A}/draws/${drawId}/cancel`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postAdminRunDrawRng(drawId: number): Promise<AdminDrawBatchCreateResponse> {
|
||||||
|
return adminRequest.post<AdminDrawBatchCreateResponse>(`${A}/draws/${drawId}/rng`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postAdminCreateManualResultBatch(
|
||||||
|
drawId: number,
|
||||||
|
payload: AdminDrawManualBatchPayload,
|
||||||
|
): Promise<AdminDrawBatchCreateResponse> {
|
||||||
|
return adminRequest.post<AdminDrawBatchCreateResponse>(
|
||||||
|
`${A}/draws/${drawId}/result-batches`,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postAdminReopenDraw(drawId: number): Promise<AdminDrawActionResponse> {
|
||||||
|
return adminRequest.post<AdminDrawActionResponse>(`${A}/draws/${drawId}/reopen`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { adminRequest } from "@/lib/admin-http";
|
import { adminHttp, adminRequest } from "@/lib/admin-http";
|
||||||
|
import { withAdminAuthHeader } from "@/lib/admin-auth";
|
||||||
|
import { withAdminLocaleHeaders } from "@/lib/admin-locale";
|
||||||
|
|
||||||
import { API_V1_PREFIX } from "./paths";
|
import { API_V1_PREFIX } from "./paths";
|
||||||
|
|
||||||
@@ -6,6 +8,8 @@ import type {
|
|||||||
AdminSettlementBatchDetailsData,
|
AdminSettlementBatchDetailsData,
|
||||||
AdminSettlementBatchListData,
|
AdminSettlementBatchListData,
|
||||||
AdminSettlementBatchShowData,
|
AdminSettlementBatchShowData,
|
||||||
|
AdminSettlementRunResponse,
|
||||||
|
AdminSettlementWorkflowResponse,
|
||||||
} from "@/types/api/admin-settlement";
|
} from "@/types/api/admin-settlement";
|
||||||
|
|
||||||
const A = `${API_V1_PREFIX}/admin`;
|
const A = `${API_V1_PREFIX}/admin`;
|
||||||
@@ -41,3 +45,42 @@ export async function getAdminSettlementBatchDetails(
|
|||||||
{ params: q },
|
{ params: q },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function postAdminRunDrawSettlement(drawId: number): Promise<AdminSettlementRunResponse> {
|
||||||
|
return adminRequest.post<AdminSettlementRunResponse>(`${A}/draws/${drawId}/settlement/run`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postAdminApproveSettlementBatch(
|
||||||
|
batchId: number,
|
||||||
|
remark?: string,
|
||||||
|
): Promise<AdminSettlementWorkflowResponse> {
|
||||||
|
return adminRequest.post<AdminSettlementWorkflowResponse>(
|
||||||
|
`${A}/settlement-batches/${batchId}/approve`,
|
||||||
|
{ remark },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postAdminRejectSettlementBatch(
|
||||||
|
batchId: number,
|
||||||
|
remark?: string,
|
||||||
|
): Promise<AdminSettlementWorkflowResponse> {
|
||||||
|
return adminRequest.post<AdminSettlementWorkflowResponse>(
|
||||||
|
`${A}/settlement-batches/${batchId}/reject`,
|
||||||
|
{ remark },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postAdminPayoutSettlementBatch(batchId: number): Promise<AdminSettlementWorkflowResponse> {
|
||||||
|
return adminRequest.post<AdminSettlementWorkflowResponse>(`${A}/settlement-batches/${batchId}/payout`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadAdminSettlementBatchExport(batchId: number): Promise<Blob> {
|
||||||
|
const res = await adminHttp.request<Blob>(
|
||||||
|
withAdminAuthHeader(withAdminLocaleHeaders({
|
||||||
|
url: `${A}/settlement-batches/${batchId}/export`,
|
||||||
|
method: "GET",
|
||||||
|
responseType: "blob",
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|||||||
<th
|
<th
|
||||||
data-slot="table-head"
|
data-slot="table-head"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
|
"h-10 px-2 text-center align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -83,7 +83,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|||||||
<td
|
<td
|
||||||
data-slot="table-cell"
|
data-slot="table-cell"
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
"p-2 text-center align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
27
src/modules/config/config-readonly-value.tsx
Normal file
27
src/modules/config/config-readonly-value.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type ConfigReadonlyValueProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
mono?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ConfigReadonlyValue({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
mono = false,
|
||||||
|
}: ConfigReadonlyValueProps) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-flex min-h-8 w-full items-center rounded-md border border-transparent bg-slate-50 px-2.5 text-sm text-slate-800",
|
||||||
|
mono && "font-mono tabular-nums",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children ?? "—"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
81
src/modules/config/config-version-actions.tsx
Normal file
81
src/modules/config/config-version-actions.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Plus, RefreshCw, Rocket, Save } from "lucide-react";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type ConfigVersionActionsProps = {
|
||||||
|
isDraft: boolean;
|
||||||
|
loadingList?: boolean;
|
||||||
|
loadingDetail?: boolean;
|
||||||
|
saving?: boolean;
|
||||||
|
publishLabel?: string;
|
||||||
|
onRefresh: () => void;
|
||||||
|
onNewDraft: () => void;
|
||||||
|
onSaveDraft: () => void;
|
||||||
|
onPublish: () => void;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ConfigVersionActions({
|
||||||
|
isDraft,
|
||||||
|
loadingList = false,
|
||||||
|
loadingDetail = false,
|
||||||
|
saving = false,
|
||||||
|
publishLabel = "启用为当前版本",
|
||||||
|
onRefresh,
|
||||||
|
onNewDraft,
|
||||||
|
onSaveDraft,
|
||||||
|
onPublish,
|
||||||
|
className,
|
||||||
|
}: ConfigVersionActionsProps) {
|
||||||
|
const draftActionBusy = saving || loadingDetail;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-wrap items-center gap-2 lg:justify-end", className)}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="h-9 border-slate-300 bg-white px-3 text-slate-700 hover:bg-slate-50 hover:text-slate-950"
|
||||||
|
disabled={loadingList}
|
||||||
|
onClick={onRefresh}
|
||||||
|
>
|
||||||
|
<RefreshCw className={loadingList ? "size-4 animate-spin" : "size-4"} aria-hidden />
|
||||||
|
{loadingList ? "刷新中" : "刷新版本"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="h-9 bg-slate-950 px-3 text-white hover:bg-slate-800"
|
||||||
|
disabled={saving}
|
||||||
|
onClick={onNewDraft}
|
||||||
|
>
|
||||||
|
<Plus className="size-4" aria-hidden />
|
||||||
|
新建草稿
|
||||||
|
</Button>
|
||||||
|
{isDraft ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="h-9 border-amber-300 bg-amber-50 px-3 text-amber-900 hover:bg-amber-100 hover:text-amber-950"
|
||||||
|
disabled={draftActionBusy}
|
||||||
|
onClick={onSaveDraft}
|
||||||
|
>
|
||||||
|
<Save className="size-4" aria-hidden />
|
||||||
|
保存草稿
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="h-9 bg-emerald-600 px-3 text-white hover:bg-emerald-700"
|
||||||
|
disabled={draftActionBusy}
|
||||||
|
onClick={onPublish}
|
||||||
|
>
|
||||||
|
<Rocket className="size-4" aria-hidden />
|
||||||
|
{publishLabel}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { Layers } from "lucide-react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
@@ -12,7 +13,6 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
@@ -59,7 +59,6 @@ export function ConfigVersionSwitcher({
|
|||||||
selectedId,
|
selectedId,
|
||||||
onSelectedIdChange,
|
onSelectedIdChange,
|
||||||
loading = false,
|
loading = false,
|
||||||
label = "配置版本",
|
|
||||||
sheetTitle = "切换配置版本",
|
sheetTitle = "切换配置版本",
|
||||||
sheetDescription = "选择一条版本在本页查看;草稿可编辑,生效中与已归档为只读。",
|
sheetDescription = "选择一条版本在本页查看;草稿可编辑,生效中与已归档为只读。",
|
||||||
className,
|
className,
|
||||||
@@ -128,9 +127,7 @@ export function ConfigVersionSwitcher({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={cn("flex w-full flex-col gap-2 sm:w-auto sm:min-w-[280px]", className)}>
|
<div className={cn("flex min-w-0 items-center gap-2", className)}>
|
||||||
<Label className="text-xs text-muted-foreground">{label}</Label>
|
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
||||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
||||||
{selectedVersion ? (
|
{selectedVersion ? (
|
||||||
<>
|
<>
|
||||||
@@ -149,21 +146,20 @@ export function ConfigVersionSwitcher({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={loading || sortedVersions.length === 0}
|
disabled={loading || sortedVersions.length === 0}
|
||||||
onClick={() => setSheetOpen(true)}
|
onClick={() => setSheetOpen(true)}
|
||||||
|
className="h-9 shrink-0 border-slate-300 bg-white px-3 text-slate-800 hover:bg-slate-50 hover:text-slate-950"
|
||||||
>
|
>
|
||||||
|
<Layers className="size-4" aria-hidden />
|
||||||
切换版本
|
切换版本
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||||
<SheetContent
|
<SheetContent
|
||||||
side="right"
|
side="right"
|
||||||
className="flex flex-col overflow-hidden border-l-0 bg-[#f6f7fb] p-0 shadow-[-24px_0_80px_rgba(15,23,42,0.22)] sm:max-w-[430px]"
|
className="flex flex-col overflow-hidden border-l bg-slate-50 p-0 shadow-xl sm:max-w-[430px]"
|
||||||
>
|
>
|
||||||
<div className="relative overflow-hidden border-b border-slate-200/80 bg-white px-5 pb-4 pt-5">
|
<div className="border-b bg-white px-5 pb-4 pt-5">
|
||||||
<div className="pointer-events-none absolute -right-10 -top-12 size-32 rounded-full bg-amber-200/45 blur-2xl" />
|
<SheetHeader className="space-y-2 text-left">
|
||||||
<div className="pointer-events-none absolute right-14 top-7 size-16 rounded-full bg-emerald-200/50 blur-xl" />
|
|
||||||
<SheetHeader className="relative space-y-2 text-left">
|
|
||||||
<SheetTitle className="text-[17px] font-semibold tracking-tight text-slate-950">
|
<SheetTitle className="text-[17px] font-semibold tracking-tight text-slate-950">
|
||||||
{sheetTitle}
|
{sheetTitle}
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
@@ -172,13 +168,10 @@ export function ConfigVersionSwitcher({
|
|||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b border-slate-200/80 bg-white/80 px-4 py-3 backdrop-blur">
|
<div className="border-b bg-white px-4 py-3">
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
{statusCounts.map((s) => (
|
{statusCounts.map((s) => (
|
||||||
<div
|
<div key={s.status} className="rounded-xl border bg-white px-3 py-2">
|
||||||
key={s.status}
|
|
||||||
className="rounded-2xl border border-slate-200 bg-white px-3 py-2 shadow-[0_6px_18px_rgba(15,23,42,0.04)]"
|
|
||||||
>
|
|
||||||
<p className="text-[11px] font-medium text-slate-500">{s.label}</p>
|
<p className="text-[11px] font-medium text-slate-500">{s.label}</p>
|
||||||
<p className="mt-0.5 text-lg font-semibold tabular-nums text-slate-950">
|
<p className="mt-0.5 text-lg font-semibold tabular-nums text-slate-950">
|
||||||
{s.count}
|
{s.count}
|
||||||
@@ -187,7 +180,7 @@ export function ConfigVersionSwitcher({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto px-4 py-4 space-y-5">
|
<div className="flex-1 overflow-auto space-y-5 px-4 py-4">
|
||||||
{sortedVersions.length === 0 ? (
|
{sortedVersions.length === 0 ? (
|
||||||
<Card className="border-dashed border-slate-200 bg-white/80 p-5 text-center text-sm text-slate-500 shadow-none">
|
<Card className="border-dashed border-slate-200 bg-white/80 p-5 text-center text-sm text-slate-500 shadow-none">
|
||||||
暂无版本记录。
|
暂无版本记录。
|
||||||
@@ -225,15 +218,14 @@ export function ConfigVersionSwitcher({
|
|||||||
<Card
|
<Card
|
||||||
key={v.id}
|
key={v.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group overflow-hidden rounded-3xl border border-slate-200/90 bg-white p-0 shadow-[0_12px_34px_rgba(15,23,42,0.06)] transition-all duration-200 hover:-translate-y-0.5 hover:border-slate-300 hover:shadow-[0_18px_42px_rgba(15,23,42,0.1)]",
|
"group overflow-hidden rounded-2xl border bg-white p-0 shadow-none transition-colors hover:border-slate-300",
|
||||||
isCurrent &&
|
isCurrent && "border-slate-900 ring-1 ring-slate-900/10",
|
||||||
"border-amber-300 bg-gradient-to-br from-amber-50 via-white to-white shadow-[0_18px_48px_rgba(245,158,11,0.16)] ring-1 ring-amber-200/70",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex gap-3 p-3.5">
|
<div className="flex gap-3 p-3.5">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-1 h-auto w-1.5 shrink-0 rounded-full bg-slate-200",
|
"mt-1 h-auto w-1 shrink-0 rounded-full bg-slate-200",
|
||||||
v.status === "draft" && "bg-amber-300",
|
v.status === "draft" && "bg-amber-300",
|
||||||
v.status === "active" && "bg-emerald-400",
|
v.status === "active" && "bg-emerald-400",
|
||||||
v.status === "archived" && "bg-slate-300",
|
v.status === "archived" && "bg-slate-300",
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ export function ConfigWorkspaceShell({ children }: { children: ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6">
|
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6">
|
||||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-start">
|
<div className="flex flex-col gap-6 lg:flex-row lg:items-start">
|
||||||
<aside className="shrink-0 lg:sticky lg:top-4 lg:w-56 lg:self-start">
|
<aside className="shrink-0 lg:sticky lg:top-20 lg:h-[calc(100vh-6rem)] lg:w-56 lg:self-start">
|
||||||
<div className="rounded-2xl border border-border/70 bg-card/80 p-3 shadow-sm backdrop-blur lg:max-h-[calc(100vh-2rem)] lg:overflow-auto">
|
<div className="h-full rounded-2xl border border-border/70 bg-card/80 p-3 shadow-sm backdrop-blur lg:overflow-auto">
|
||||||
<div className="mb-3 px-1">
|
<div className="mb-3 px-1">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
||||||
运营配置导航
|
运营配置导航
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
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 { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
|
||||||
|
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
|
||||||
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
|
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||||
@@ -174,7 +176,13 @@ export function OddsConfigDocScreen() {
|
|||||||
return filteredTypes[0].play_code;
|
return filteredTypes[0].play_code;
|
||||||
}, [filteredTypes, playCode]);
|
}, [filteredTypes, playCode]);
|
||||||
|
|
||||||
const isDraft = detail?.status === "draft";
|
const selectedVersionSummary = useMemo(
|
||||||
|
() => list.find((x) => String(x.id) === selectedId) ?? null,
|
||||||
|
[list, selectedId],
|
||||||
|
);
|
||||||
|
const isSelectedDetail = detail !== null && String(detail.id) === selectedId;
|
||||||
|
const selectedStatus = isSelectedDetail ? detail.status : selectedVersionSummary?.status;
|
||||||
|
const isDraft = selectedStatus === "draft";
|
||||||
|
|
||||||
const scopeRows = useMemo(() => {
|
const scopeRows = useMemo(() => {
|
||||||
const rows: Partial<Record<PrizeScopeCode, OddsItemRow>> = {};
|
const rows: Partial<Record<PrizeScopeCode, OddsItemRow>> = {};
|
||||||
@@ -375,6 +383,12 @@ export function OddsConfigDocScreen() {
|
|||||||
key={t.play_code}
|
key={t.play_code}
|
||||||
type="button"
|
type="button"
|
||||||
variant={resolvedPlayCode === t.play_code ? "secondary" : "outline"}
|
variant={resolvedPlayCode === t.play_code ? "secondary" : "outline"}
|
||||||
|
className={cn(
|
||||||
|
"h-9 border-slate-300 px-5 text-[18px] font-medium",
|
||||||
|
resolvedPlayCode === t.play_code
|
||||||
|
? "border-slate-950 bg-slate-950 text-white shadow-sm hover:bg-slate-900"
|
||||||
|
: "bg-white text-slate-900 hover:border-slate-400 hover:bg-slate-50",
|
||||||
|
)}
|
||||||
onClick={() => setPlayCode(t.play_code)}
|
onClick={() => setPlayCode(t.play_code)}
|
||||||
>
|
>
|
||||||
{t.display_name_zh ?? t.play_code}
|
{t.display_name_zh ?? t.play_code}
|
||||||
@@ -384,6 +398,8 @@ export function OddsConfigDocScreen() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl border bg-muted/20 p-3">
|
||||||
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
|
||||||
<ConfigVersionSwitcher
|
<ConfigVersionSwitcher
|
||||||
versions={list}
|
versions={list}
|
||||||
selectedId={selectedId}
|
selectedId={selectedId}
|
||||||
@@ -394,30 +410,25 @@ export function OddsConfigDocScreen() {
|
|||||||
onDeleteVersion={handleDeleteVersion}
|
onDeleteVersion={handleDeleteVersion}
|
||||||
onRollbackVersion={requestRollback}
|
onRollbackVersion={requestRollback}
|
||||||
rollbackBusy={saving}
|
rollbackBusy={saving}
|
||||||
|
className="lg:flex-1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<ConfigVersionActions
|
||||||
<Button
|
isDraft={isDraft}
|
||||||
type="button"
|
loadingList={loadingList}
|
||||||
variant="secondary"
|
loadingDetail={loadingDetail}
|
||||||
disabled={loadingList}
|
saving={saving}
|
||||||
onClick={() => void refreshList()}
|
onRefresh={() => void refreshList()}
|
||||||
>
|
onNewDraft={() => void handleNewDraft()}
|
||||||
刷新
|
onSaveDraft={() => void handleSave()}
|
||||||
</Button>
|
onPublish={() => void handlePublish()}
|
||||||
<Button type="button" onClick={() => void handleNewDraft()} disabled={saving}>
|
/>
|
||||||
新建草稿
|
</div>
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{detail ? (
|
{detail ? (
|
||||||
<div className="rounded-lg border bg-muted/30 px-4 py-3 text-sm space-y-1">
|
<p className="text-sm text-muted-foreground">
|
||||||
<p>
|
当前生效版本:
|
||||||
<span className="text-muted-foreground">当前编辑版本:</span>v{detail.version_no} ·{" "}
|
|
||||||
{detail.status === "active" ? "生效中" : detail.status === "draft" ? "草稿" : "已归档"}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span className="text-muted-foreground">当前生效版本:</span>
|
|
||||||
{activeHead ? (
|
{activeHead ? (
|
||||||
<>
|
<>
|
||||||
v{activeHead.version_no}
|
v{activeHead.version_no}
|
||||||
@@ -426,11 +437,10 @@ export function OddsConfigDocScreen() {
|
|||||||
) : (
|
) : (
|
||||||
"—"
|
"—"
|
||||||
)}
|
)}
|
||||||
</p>
|
|
||||||
{!isDraft ? (
|
{!isDraft ? (
|
||||||
<p className="text-amber-600 dark:text-amber-400">当前为只读版本,请新建草稿后再改赔率。</p>
|
<span className="text-amber-600 dark:text-amber-400"> — 当前为只读版本,请新建草稿后再改赔率。</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
||||||
@@ -453,11 +463,12 @@ export function OddsConfigDocScreen() {
|
|||||||
</Label>
|
</Label>
|
||||||
{row && idx >= 0 ? (
|
{row && idx >= 0 ? (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{isDraft ? (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="text"
|
||||||
min={0}
|
inputMode="numeric"
|
||||||
className="h-9 font-mono tabular-nums max-w-[200px]"
|
className="h-9 max-w-[200px] font-mono tabular-nums"
|
||||||
disabled={!isDraft || saving}
|
disabled={saving}
|
||||||
value={row.odds_value}
|
value={row.odds_value}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateOddsForScope(scope, {
|
updateOddsForScope(scope, {
|
||||||
@@ -465,6 +476,11 @@ export function OddsConfigDocScreen() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfigReadonlyValue mono className="max-w-[200px]">
|
||||||
|
{row.odds_value}
|
||||||
|
</ConfigReadonlyValue>
|
||||||
|
)}
|
||||||
<span className="text-sm text-muted-foreground tabular-nums">
|
<span className="text-sm text-muted-foreground tabular-nums">
|
||||||
乘数 ×{oddsMultiplierLabel(row.odds_value)} · {row.currency_code}
|
乘数 ×{oddsMultiplierLabel(row.odds_value)} · {row.currency_code}
|
||||||
</span>
|
</span>
|
||||||
@@ -477,33 +493,25 @@ export function OddsConfigDocScreen() {
|
|||||||
})}
|
})}
|
||||||
<div className="grid gap-1 pt-2 border-t">
|
<div className="grid gap-1 pt-2 border-t">
|
||||||
<Label>回水率(%)</Label>
|
<Label>回水率(%)</Label>
|
||||||
|
{isDraft ? (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="text"
|
||||||
step="0.01"
|
inputMode="decimal"
|
||||||
min={0}
|
className="h-9 max-w-[200px] font-mono tabular-nums"
|
||||||
className="h-9 font-mono tabular-nums max-w-[200px]"
|
disabled={saving}
|
||||||
disabled={!isDraft || saving}
|
|
||||||
value={rebatePercentUi}
|
value={rebatePercentUi}
|
||||||
onChange={(e) => setRebateForPlayPercent(e.target.value)}
|
onChange={(e) => setRebateForPlayPercent(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfigReadonlyValue mono className="max-w-[200px]">
|
||||||
|
{rebatePercentUi}
|
||||||
|
</ConfigReadonlyValue>
|
||||||
|
)}
|
||||||
<p className="text-sm text-muted-foreground">写入该玩法下全部奖项档位的 rebate_rate。</p>
|
<p className="text-sm text-muted-foreground">写入该玩法下全部奖项档位的 rebate_rate。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2 pt-2">
|
|
||||||
<Button type="button" onClick={() => void handleSave()} disabled={!isDraft || saving || loadingDetail}>
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="bg-emerald-600 text-white hover:bg-emerald-600/90"
|
|
||||||
onClick={() => void handlePublish()}
|
|
||||||
disabled={!isDraft || saving || loadingDetail}
|
|
||||||
>
|
|
||||||
启用为当前版本
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<Dialog open={rollbackOpen} onOpenChange={setRollbackOpen}>
|
<Dialog open={rollbackOpen} onOpenChange={setRollbackOpen}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +33,8 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
|
||||||
|
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
|
||||||
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
|
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
import type {
|
import type {
|
||||||
@@ -96,7 +98,9 @@ export function PlayConfigDocScreen() {
|
|||||||
const [loadingList, setLoadingList] = useState(true);
|
const [loadingList, setLoadingList] = useState(true);
|
||||||
const [loadingDetail, setLoadingDetail] = useState(false);
|
const [loadingDetail, setLoadingDetail] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [creatingDraftId, setCreatingDraftId] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const detailRequestSeq = useRef(0);
|
||||||
|
|
||||||
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
||||||
const [rulePlayCode, setRulePlayCode] = useState<string | null>(null);
|
const [rulePlayCode, setRulePlayCode] = useState<string | null>(null);
|
||||||
@@ -108,6 +112,9 @@ export function PlayConfigDocScreen() {
|
|||||||
try {
|
try {
|
||||||
const d = await getAllConfigVersions(getPlayConfigVersions);
|
const d = await getAllConfigVersions(getPlayConfigVersions);
|
||||||
setList(d.items);
|
setList(d.items);
|
||||||
|
setCreatingDraftId((draftId) =>
|
||||||
|
draftId !== null && d.items.some((x) => String(x.id) === draftId) ? null : draftId,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof LotteryApiBizError ? e.message : "加载版本列表失败";
|
const msg = e instanceof LotteryApiBizError ? e.message : "加载版本列表失败";
|
||||||
setError(msg);
|
setError(msg);
|
||||||
@@ -124,33 +131,58 @@ export function PlayConfigDocScreen() {
|
|||||||
}, [refreshList]);
|
}, [refreshList]);
|
||||||
|
|
||||||
const loadDetail = useCallback(async (id: number) => {
|
const loadDetail = useCallback(async (id: number) => {
|
||||||
|
const requestSeq = detailRequestSeq.current + 1;
|
||||||
|
detailRequestSeq.current = requestSeq;
|
||||||
setLoadingDetail(true);
|
setLoadingDetail(true);
|
||||||
|
setDetail(null);
|
||||||
|
setDraftRows([]);
|
||||||
try {
|
try {
|
||||||
const d = await getPlayConfigVersion(id);
|
const d = await getPlayConfigVersion(id);
|
||||||
|
if (detailRequestSeq.current !== requestSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setDetail(d);
|
setDetail(d);
|
||||||
setDraftRows(d.items.map((it) => ({ ...it })));
|
setDraftRows(d.items.map((it) => ({ ...it })));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (detailRequestSeq.current !== requestSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
toast.error(e instanceof LotteryApiBizError ? e.message : "加载版本明细失败");
|
toast.error(e instanceof LotteryApiBizError ? e.message : "加载版本明细失败");
|
||||||
setDetail(null);
|
setDetail(null);
|
||||||
setDraftRows([]);
|
setDraftRows([]);
|
||||||
} finally {
|
} finally {
|
||||||
|
if (detailRequestSeq.current === requestSeq) {
|
||||||
setLoadingDetail(false);
|
setLoadingDetail(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (list.length === 0 || selectedId !== "") {
|
if (list.length === 0) {
|
||||||
|
if (selectedId !== "") {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
setSelectedId("");
|
||||||
|
setDetail(null);
|
||||||
|
setDraftRows([]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedId !== "" && list.some((x) => String(x.id) === selectedId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (creatingDraftId !== null && selectedId === creatingDraftId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
const drafts = list.filter((x) => x.status === "draft").sort((a, b) => b.id - a.id);
|
|
||||||
const active = list.find((x) => x.status === "active");
|
const active = list.find((x) => x.status === "active");
|
||||||
const pick = drafts[0] ?? active ?? list.sort((a, b) => b.id - a.id)[0];
|
const drafts = list.filter((x) => x.status === "draft").sort((a, b) => b.id - a.id);
|
||||||
|
const pick = active ?? drafts[0] ?? [...list].sort((a, b) => b.id - a.id)[0];
|
||||||
if (pick) {
|
if (pick) {
|
||||||
setSelectedId(String(pick.id));
|
setSelectedId(String(pick.id));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [list, selectedId]);
|
}, [list, selectedId, creatingDraftId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedId === "") {
|
if (selectedId === "") {
|
||||||
@@ -165,7 +197,13 @@ export function PlayConfigDocScreen() {
|
|||||||
});
|
});
|
||||||
}, [selectedId, loadDetail]);
|
}, [selectedId, loadDetail]);
|
||||||
|
|
||||||
const isDraft = detail?.status === "draft";
|
const selectedVersionSummary = useMemo(
|
||||||
|
() => list.find((x) => String(x.id) === selectedId) ?? null,
|
||||||
|
[list, selectedId],
|
||||||
|
);
|
||||||
|
const isSelectedDetail = detail !== null && String(detail.id) === selectedId;
|
||||||
|
const selectedStatus = isSelectedDetail ? detail.status : selectedVersionSummary?.status;
|
||||||
|
const isDraft = selectedStatus === "draft";
|
||||||
|
|
||||||
const orderedRows = useMemo(
|
const orderedRows = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -232,10 +270,11 @@ export function PlayConfigDocScreen() {
|
|||||||
clone_from_version_id: active?.id ?? null,
|
clone_from_version_id: active?.id ?? null,
|
||||||
});
|
});
|
||||||
toast.success(`已创建草稿 v${d.version_no}`);
|
toast.success(`已创建草稿 v${d.version_no}`);
|
||||||
await refreshList();
|
setCreatingDraftId(String(d.id));
|
||||||
setSelectedId(String(d.id));
|
setSelectedId(String(d.id));
|
||||||
setDetail(d);
|
setDetail(d);
|
||||||
setDraftRows(d.items.map((it) => ({ ...it })));
|
setDraftRows(d.items.map((it) => ({ ...it })));
|
||||||
|
void refreshList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof LotteryApiBizError ? e.message : "创建草稿失败");
|
toast.error(e instanceof LotteryApiBizError ? e.message : "创建草稿失败");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -291,25 +330,16 @@ export function PlayConfigDocScreen() {
|
|||||||
className="lg:flex-1"
|
className="lg:flex-1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
|
<ConfigVersionActions
|
||||||
<Button type="button" variant="secondary" onClick={() => void refreshList()} disabled={loadingList}>
|
isDraft={isDraft}
|
||||||
刷新版本
|
loadingList={loadingList}
|
||||||
</Button>
|
loadingDetail={loadingDetail}
|
||||||
<Button type="button" onClick={() => void handleNewDraft()} disabled={saving}>
|
saving={saving}
|
||||||
新建草稿
|
onRefresh={() => void refreshList()}
|
||||||
</Button>
|
onNewDraft={() => void handleNewDraft()}
|
||||||
<Button type="button" onClick={() => void handleSaveDraft()} disabled={!isDraft || saving || loadingDetail}>
|
onSaveDraft={() => void handleSaveDraft()}
|
||||||
保存草稿
|
onPublish={() => void handlePublish()}
|
||||||
</Button>
|
/>
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="bg-emerald-600 text-white hover:bg-emerald-600/90"
|
|
||||||
onClick={() => void handlePublish()}
|
|
||||||
disabled={!isDraft || saving || loadingDetail}
|
|
||||||
>
|
|
||||||
启用为当前版本
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -339,22 +369,23 @@ export function PlayConfigDocScreen() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>玩法名称</TableHead>
|
<TableHead className="text-center">玩法名称</TableHead>
|
||||||
<TableHead className="w-[100px]">分类</TableHead>
|
<TableHead className="w-[100px] text-center">分类</TableHead>
|
||||||
<TableHead className="w-[88px] text-center">状态</TableHead>
|
<TableHead className="w-[88px] text-center">状态</TableHead>
|
||||||
<TableHead className="min-w-[120px]">显示名称</TableHead>
|
<TableHead className="min-w-[120px] text-center">显示名称</TableHead>
|
||||||
<TableHead className="w-[120px]">排序</TableHead>
|
<TableHead className="w-[120px] text-center">排序</TableHead>
|
||||||
<TableHead className="w-[110px]">最小下注</TableHead>
|
<TableHead className="w-[110px] text-center">最小下注</TableHead>
|
||||||
<TableHead className="w-[110px]">最大下注</TableHead>
|
<TableHead className="w-[110px] text-center">最大下注</TableHead>
|
||||||
<TableHead className="w-[140px]">操作</TableHead>
|
<TableHead className="w-[140px] text-center">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{orderedRows.map((row) => (
|
{orderedRows.map((row) => (
|
||||||
<TableRow key={row.play_code}>
|
<TableRow key={row.play_code}>
|
||||||
<TableCell className="font-mono text-sm">{row.play_code}</TableCell>
|
<TableCell className="text-center font-mono text-sm">{row.play_code}</TableCell>
|
||||||
<TableCell className="text-muted-foreground text-sm">{row.category ?? "—"}</TableCell>
|
<TableCell className="text-center text-muted-foreground text-sm">{row.category ?? "—"}</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
|
{isDraft ? (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={row.is_enabled}
|
checked={row.is_enabled}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
@@ -363,10 +394,16 @@ export function PlayConfigDocScreen() {
|
|||||||
}}
|
}}
|
||||||
aria-label={`启用 ${row.play_code}`}
|
aria-label={`启用 ${row.play_code}`}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfigReadonlyValue className="justify-center">
|
||||||
|
{row.is_enabled ? "启用" : "停用"}
|
||||||
|
</ConfigReadonlyValue>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
{isDraft ? (
|
||||||
<Input
|
<Input
|
||||||
className="h-8 text-sm"
|
className="h-8 text-center text-sm"
|
||||||
value={row.display_name_zh ?? ""}
|
value={row.display_name_zh ?? ""}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -374,8 +411,14 @@ export function PlayConfigDocScreen() {
|
|||||||
updateConfigRow(row.play_code, { display_name_zh: next });
|
updateConfigRow(row.play_code, { display_name_zh: next });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfigReadonlyValue className="justify-center">
|
||||||
|
{row.display_name_zh ?? "—"}
|
||||||
|
</ConfigReadonlyValue>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="w-[96px]">
|
<TableCell className="w-[96px] text-center">
|
||||||
|
{isDraft ? (
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
@@ -389,13 +432,19 @@ export function PlayConfigDocScreen() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfigReadonlyValue mono className="justify-center">
|
||||||
|
{row.display_order}
|
||||||
|
</ConfigReadonlyValue>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="text-center">
|
||||||
|
{isDraft ? (
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
className="h-8 font-mono tabular-nums"
|
className="h-8 text-center font-mono tabular-nums"
|
||||||
disabled={!isDraft || saving}
|
disabled={saving}
|
||||||
value={row.min_bet_amount}
|
value={row.min_bet_amount}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfigRow(row.play_code, {
|
updateConfigRow(row.play_code, {
|
||||||
@@ -403,13 +452,19 @@ export function PlayConfigDocScreen() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfigReadonlyValue mono className="justify-center">
|
||||||
|
{row.min_bet_amount}
|
||||||
|
</ConfigReadonlyValue>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="text-center">
|
||||||
|
{isDraft ? (
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
className="h-8 font-mono tabular-nums"
|
className="h-8 text-center font-mono tabular-nums"
|
||||||
disabled={!isDraft || saving}
|
disabled={saving}
|
||||||
value={row.max_bet_amount}
|
value={row.max_bet_amount}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfigRow(row.play_code, {
|
updateConfigRow(row.play_code, {
|
||||||
@@ -417,16 +472,25 @@ export function PlayConfigDocScreen() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfigReadonlyValue mono className="justify-center">
|
||||||
|
{row.max_bet_amount}
|
||||||
|
</ConfigReadonlyValue>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="text-center">
|
||||||
|
{isDraft ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!isDraft || saving}
|
disabled={saving}
|
||||||
onClick={() => openRuleEditor(row.play_code)}
|
onClick={() => openRuleEditor(row.play_code)}
|
||||||
>
|
>
|
||||||
规则说明
|
规则说明
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-muted-foreground">只读</span>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ import {
|
|||||||
publishOddsVersion,
|
publishOddsVersion,
|
||||||
putOddsItems,
|
putOddsItems,
|
||||||
} from "@/api/admin-config";
|
} from "@/api/admin-config";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, 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";
|
||||||
|
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
|
||||||
|
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
|
||||||
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
|
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
|
||||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
@@ -147,7 +148,13 @@ export function RebateConfigDocScreen() {
|
|||||||
return m;
|
return m;
|
||||||
}, [types]);
|
}, [types]);
|
||||||
|
|
||||||
const isDraft = detail?.status === "draft";
|
const selectedVersionSummary = useMemo(
|
||||||
|
() => listRows.find((x) => String(x.id) === selectedId) ?? null,
|
||||||
|
[listRows, selectedId],
|
||||||
|
);
|
||||||
|
const isSelectedDetail = detail !== null && String(detail.id) === selectedId;
|
||||||
|
const selectedStatus = isSelectedDetail ? detail.status : selectedVersionSummary?.status;
|
||||||
|
const isDraft = selectedStatus === "draft";
|
||||||
|
|
||||||
function applyDimensionPercentsToRows(rows: OddsItemRow[]): OddsItemRow[] {
|
function applyDimensionPercentsToRows(rows: OddsItemRow[]): OddsItemRow[] {
|
||||||
const r2 = Number.parseFloat(p2);
|
const r2 = Number.parseFloat(p2);
|
||||||
@@ -261,7 +268,8 @@ export function RebateConfigDocScreen() {
|
|||||||
<CardHeader className="space-y-1">
|
<CardHeader className="space-y-1">
|
||||||
<CardTitle className="text-lg">佣金 / 回水配置</CardTitle>
|
<CardTitle className="text-lg">佣金 / 回水配置</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6 max-w-xl">
|
<CardContent className="space-y-6">
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<ConfigVersionSwitcher
|
<ConfigVersionSwitcher
|
||||||
versions={listRows}
|
versions={listRows}
|
||||||
selectedId={selectedId}
|
selectedId={selectedId}
|
||||||
@@ -270,27 +278,20 @@ export function RebateConfigDocScreen() {
|
|||||||
sheetTitle="回水配置版本"
|
sheetTitle="回水配置版本"
|
||||||
sheetDescription="回水写入赔率版本草稿;选择与赔率配置共用同一套版本。"
|
sheetDescription="回水写入赔率版本草稿;选择与赔率配置共用同一套版本。"
|
||||||
onDeleteVersion={handleDeleteVersion}
|
onDeleteVersion={handleDeleteVersion}
|
||||||
|
className="w-auto min-w-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<ConfigVersionActions
|
||||||
<Button type="button" variant="secondary" onClick={() => void refreshList()} disabled={loading}>
|
isDraft={isDraft}
|
||||||
刷新
|
loadingList={loading}
|
||||||
</Button>
|
loadingDetail={loadingDetail}
|
||||||
<Button type="button" onClick={() => void handleNewDraft()} disabled={saving}>
|
saving={saving}
|
||||||
新建草稿
|
publishLabel="发布生效"
|
||||||
</Button>
|
onRefresh={() => void refreshList()}
|
||||||
<Button type="button" onClick={() => void handleSave()} disabled={!isDraft || saving || loadingDetail}>
|
onNewDraft={() => void handleNewDraft()}
|
||||||
保存草稿
|
onSaveDraft={() => void handleSave()}
|
||||||
</Button>
|
onPublish={() => void handlePublish()}
|
||||||
<Button
|
/>
|
||||||
type="button"
|
|
||||||
className="bg-emerald-600 text-white hover:bg-emerald-600/90"
|
|
||||||
onClick={() => void handlePublish()}
|
|
||||||
disabled={!isDraft || saving || loadingDetail}
|
|
||||||
>
|
|
||||||
发布生效
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{detail ? (
|
{detail ? (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
@@ -300,43 +301,56 @@ export function RebateConfigDocScreen() {
|
|||||||
) : null}
|
) : null}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>2D 回水率(%)</Label>
|
<Label>2D 回水率(%)</Label>
|
||||||
|
{isDraft ? (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
min={0}
|
min={0}
|
||||||
className="font-mono tabular-nums"
|
className="font-mono tabular-nums"
|
||||||
disabled={!isDraft || saving}
|
disabled={saving}
|
||||||
value={p2}
|
value={p2}
|
||||||
onChange={(e) => setP2(e.target.value)}
|
onChange={(e) => setP2(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfigReadonlyValue mono>{p2}</ConfigReadonlyValue>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>3D 回水率(%)</Label>
|
<Label>3D 回水率(%)</Label>
|
||||||
|
{isDraft ? (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
min={0}
|
min={0}
|
||||||
className="font-mono tabular-nums"
|
className="font-mono tabular-nums"
|
||||||
disabled={!isDraft || saving}
|
disabled={saving}
|
||||||
value={p3}
|
value={p3}
|
||||||
onChange={(e) => setP3(e.target.value)}
|
onChange={(e) => setP3(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfigReadonlyValue mono>{p3}</ConfigReadonlyValue>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>4D 回水率(%)</Label>
|
<Label>4D 回水率(%)</Label>
|
||||||
|
{isDraft ? (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
min={0}
|
min={0}
|
||||||
className="font-mono tabular-nums"
|
className="font-mono tabular-nums"
|
||||||
disabled={!isDraft || saving}
|
disabled={saving}
|
||||||
value={p4}
|
value={p4}
|
||||||
onChange={(e) => setP4(e.target.value)}
|
onChange={(e) => setP4(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfigReadonlyValue mono>{p4}</ConfigReadonlyValue>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
|
||||||
|
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
|
||||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
import type {
|
import type {
|
||||||
@@ -150,7 +152,13 @@ export function RiskCapDocScreen() {
|
|||||||
});
|
});
|
||||||
}, [selectedId, loadDetail]);
|
}, [selectedId, loadDetail]);
|
||||||
|
|
||||||
const isDraft = detail?.status === "draft";
|
const selectedVersionSummary = useMemo(
|
||||||
|
() => list.find((x) => String(x.id) === selectedId) ?? null,
|
||||||
|
[list, selectedId],
|
||||||
|
);
|
||||||
|
const isSelectedDetail = detail !== null && String(detail.id) === selectedId;
|
||||||
|
const selectedStatus = isSelectedDetail ? detail.status : selectedVersionSummary?.status;
|
||||||
|
const isDraft = selectedStatus === "draft";
|
||||||
|
|
||||||
const updateRow = (idx: number, patch: Partial<DraftRiskRow>) => {
|
const updateRow = (idx: number, patch: Partial<DraftRiskRow>) => {
|
||||||
setDraftRows((prev) => prev.map((r, i) => (i === idx ? { ...r, ...patch } : r)));
|
setDraftRows((prev) => prev.map((r, i) => (i === idx ? { ...r, ...patch } : r)));
|
||||||
@@ -301,6 +309,7 @@ export function RiskCapDocScreen() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-8">
|
<CardContent className="space-y-8">
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<ConfigVersionSwitcher
|
<ConfigVersionSwitcher
|
||||||
versions={list}
|
versions={list}
|
||||||
selectedId={selectedId}
|
selectedId={selectedId}
|
||||||
@@ -308,29 +317,19 @@ export function RiskCapDocScreen() {
|
|||||||
loading={loadingList}
|
loading={loadingList}
|
||||||
sheetTitle="风控封顶版本"
|
sheetTitle="风控封顶版本"
|
||||||
onDeleteVersion={handleDeleteVersion}
|
onDeleteVersion={handleDeleteVersion}
|
||||||
|
className="w-auto min-w-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-end gap-4">
|
<ConfigVersionActions
|
||||||
<Button type="button" variant="secondary" onClick={() => void refreshList()}>
|
isDraft={isDraft}
|
||||||
刷新
|
loadingList={loadingList}
|
||||||
</Button>
|
loadingDetail={loadingDetail}
|
||||||
<Button type="button" onClick={() => void handleNewDraft()} disabled={saving}>
|
saving={saving}
|
||||||
新建草稿
|
onRefresh={() => void refreshList()}
|
||||||
</Button>
|
onNewDraft={() => void handleNewDraft()}
|
||||||
<Button type="button" onClick={() => void handleSave()} disabled={!isDraft || saving || loadingDetail}>
|
onSaveDraft={() => void handleSave()}
|
||||||
保存草稿
|
onPublish={() => void handlePublish()}
|
||||||
</Button>
|
/>
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="bg-emerald-600 text-white hover:bg-emerald-600/90"
|
|
||||||
onClick={() => void handlePublish()}
|
|
||||||
disabled={!isDraft || saving || loadingDetail}
|
|
||||||
>
|
|
||||||
启用为当前版本
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
|
||||||
|
|
||||||
{detail ? (
|
{detail ? (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
@@ -340,6 +339,9 @@ export function RiskCapDocScreen() {
|
|||||||
) : null}
|
) : null}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
||||||
|
|
||||||
<section className="space-y-3 rounded-lg border bg-muted/20 p-4">
|
<section className="space-y-3 rounded-lg border bg-muted/20 p-4">
|
||||||
<h3 className="text-sm font-medium">默认封顶</h3>
|
<h3 className="text-sm font-medium">默认封顶</h3>
|
||||||
@@ -349,33 +351,43 @@ export function RiskCapDocScreen() {
|
|||||||
<div className="flex flex-wrap items-end gap-2">
|
<div className="flex flex-wrap items-end gap-2">
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
<Label htmlFor="default-cap">封顶金额(最小货币单位)</Label>
|
<Label htmlFor="default-cap">封顶金额(最小货币单位)</Label>
|
||||||
|
{isDraft ? (
|
||||||
<Input
|
<Input
|
||||||
id="default-cap"
|
id="default-cap"
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
className="w-[220px] font-mono tabular-nums"
|
className="w-[220px] font-mono tabular-nums"
|
||||||
disabled={!isDraft || saving}
|
disabled={saving}
|
||||||
value={defaultCapStr}
|
value={defaultCapStr}
|
||||||
onChange={(e) => setDefaultCapStr(e.target.value)}
|
onChange={(e) => setDefaultCapStr(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfigReadonlyValue mono className="w-[220px]">
|
||||||
|
{defaultCapStr || "—"}
|
||||||
|
</ConfigReadonlyValue>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" variant="secondary" disabled={!isDraft || saving} onClick={() => setSyncOpen(true)}>
|
{isDraft ? (
|
||||||
|
<Button type="button" variant="secondary" disabled={saving} onClick={() => setSyncOpen(true)}>
|
||||||
更新
|
更新
|
||||||
</Button>
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="space-y-3">
|
<section className="space-y-3">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<h3 className="text-sm font-medium">特殊封顶</h3>
|
<h3 className="text-sm font-medium">特殊封顶</h3>
|
||||||
|
{isDraft ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!isDraft || saving}
|
disabled={saving}
|
||||||
onClick={() => setDraftRows((prev) => [...prev, newRow()])}
|
onClick={() => setDraftRows((prev) => [...prev, newRow()])}
|
||||||
>
|
>
|
||||||
+ 添加特殊封顶
|
+ 添加特殊封顶
|
||||||
</Button>
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{loadingDetail ? (
|
{loadingDetail ? (
|
||||||
<p className="text-sm text-muted-foreground">加载明细…</p>
|
<p className="text-sm text-muted-foreground">加载明细…</p>
|
||||||
@@ -398,10 +410,11 @@ export function RiskCapDocScreen() {
|
|||||||
{draftRows.map((r, idx) => (
|
{draftRows.map((r, idx) => (
|
||||||
<TableRow key={r.clientKey}>
|
<TableRow key={r.clientKey}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
{isDraft ? (
|
||||||
<Input
|
<Input
|
||||||
className="h-8 font-mono tabular-nums"
|
className="h-8 font-mono tabular-nums"
|
||||||
maxLength={4}
|
maxLength={4}
|
||||||
disabled={!isDraft || saving}
|
disabled={saving}
|
||||||
value={r.normalized_number}
|
value={r.normalized_number}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateRow(idx, {
|
updateRow(idx, {
|
||||||
@@ -409,13 +422,17 @@ export function RiskCapDocScreen() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfigReadonlyValue mono>{r.normalized_number}</ConfigReadonlyValue>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
{isDraft ? (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
className="h-8 font-mono tabular-nums"
|
className="h-8 font-mono tabular-nums"
|
||||||
disabled={!isDraft || saving}
|
disabled={saving}
|
||||||
value={r.cap_amount}
|
value={r.cap_amount}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateRow(idx, {
|
updateRow(idx, {
|
||||||
@@ -423,20 +440,27 @@ export function RiskCapDocScreen() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfigReadonlyValue mono>{r.cap_amount}</ConfigReadonlyValue>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-muted-foreground tabular-nums text-sm">—</TableCell>
|
<TableCell className="text-right text-muted-foreground tabular-nums text-sm">—</TableCell>
|
||||||
<TableCell className="text-right text-muted-foreground tabular-nums text-sm">—</TableCell>
|
<TableCell className="text-right text-muted-foreground tabular-nums text-sm">—</TableCell>
|
||||||
<TableCell className="text-center text-muted-foreground text-sm">—</TableCell>
|
<TableCell className="text-center text-muted-foreground text-sm">—</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
{isDraft ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="text-destructive"
|
className="text-destructive"
|
||||||
disabled={!isDraft || saving || draftRows.length <= 1}
|
disabled={saving || draftRows.length <= 1}
|
||||||
onClick={() => removeRow(idx)}
|
onClick={() => removeRow(idx)}
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-muted-foreground">只读</span>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -2,19 +2,30 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { getAdminDraw } from "@/api/admin-draws";
|
import {
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
getAdminDraw,
|
||||||
|
postAdminCancelDraw,
|
||||||
|
postAdminManualCloseDraw,
|
||||||
|
postAdminReopenDraw,
|
||||||
|
postAdminRunDrawRng,
|
||||||
|
} from "@/api/admin-draws";
|
||||||
|
import { postAdminRunDrawSettlement } from "@/api/admin-settlement";
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
import { Card, CardContent, 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";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
import type { AdminDrawShowData } from "@/types/api/admin-draws";
|
import type { AdminDrawShowData } from "@/types/api/admin-draws";
|
||||||
|
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||||
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { DrawStatusBadge } from "./draw-status-badge";
|
import { DrawStatusBadge } from "./draw-status-badge";
|
||||||
|
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
|
||||||
|
|
||||||
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@@ -27,10 +38,14 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
|
|||||||
|
|
||||||
export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
||||||
const idNum = Number(drawId);
|
const idNum = Number(drawId);
|
||||||
|
const profile = useAdminProfile();
|
||||||
|
const canManageDraw = adminHasAnyPermission(profile?.permissions, [PRD_DRAW_RESULT_MANAGE]);
|
||||||
|
const isSuperAdmin = profile?.permissions?.includes("prd.admin_user.manage") ?? false;
|
||||||
const formatDt = useAdminDateTimeFormatter();
|
const formatDt = useAdminDateTimeFormatter();
|
||||||
const [data, setData] = useState<AdminDrawShowData | null>(null);
|
const [data, setData] = useState<AdminDrawShowData | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [acting, setActing] = useState<string | null>(null);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
if (!Number.isFinite(idNum)) {
|
if (!Number.isFinite(idNum)) {
|
||||||
@@ -50,6 +65,20 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
|||||||
}
|
}
|
||||||
}, [idNum]);
|
}, [idNum]);
|
||||||
|
|
||||||
|
async function runAction(name: string, action: () => Promise<unknown>): Promise<void> {
|
||||||
|
if (!Number.isFinite(idNum)) return;
|
||||||
|
setActing(name);
|
||||||
|
try {
|
||||||
|
await action();
|
||||||
|
toast.success(`${name}成功`);
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e instanceof LotteryApiBizError ? e.message : `${name}失败`);
|
||||||
|
} finally {
|
||||||
|
setActing(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = window.setTimeout(() => {
|
const timer = window.setTimeout(() => {
|
||||||
void load();
|
void load();
|
||||||
@@ -124,6 +153,58 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">期号操作</CardTitle>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
手动封盘 / 取消 / RNG / 重开 / 触发结算均直接调用后台接口。
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
disabled={!canManageDraw || acting !== null || !["pending", "open"].includes(data.status)}
|
||||||
|
onClick={() => void runAction("手动封盘", () => postAdminManualCloseDraw(idNum))}
|
||||||
|
>
|
||||||
|
{acting === "手动封盘" ? "处理中…" : "手动封盘"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={!canManageDraw || acting !== null || !["pending", "open", "closing", "closed"].includes(data.status)}
|
||||||
|
onClick={() => void runAction("取消期号", () => postAdminCancelDraw(idNum))}
|
||||||
|
>
|
||||||
|
{acting === "取消期号" ? "处理中…" : "未开奖前取消"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={!canManageDraw || acting !== null || data.status !== "closed"}
|
||||||
|
onClick={() => void runAction("RNG开奖", () => postAdminRunDrawRng(idNum))}
|
||||||
|
>
|
||||||
|
{acting === "RNG开奖" ? "生成中…" : "RNG 自动生成"}
|
||||||
|
</Button>
|
||||||
|
{isSuperAdmin ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
disabled={acting !== null || data.status !== "cooldown"}
|
||||||
|
onClick={() => void runAction("重开", () => postAdminReopenDraw(idNum))}
|
||||||
|
>
|
||||||
|
{acting === "重开" ? "处理中…" : "冷静期重开"}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={acting !== null || !["settling", "cooldown"].includes(data.status)}
|
||||||
|
onClick={() => void runAction("触发结算", () => postAdminRunDrawSettlement(idNum))}
|
||||||
|
>
|
||||||
|
{acting === "触发结算" ? "处理中…" : "触发结算"}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Link from "next/link";
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { getAdminDrawFinanceSummary } from "@/api/admin-draws";
|
import { getAdminDrawFinanceSummary } from "@/api/admin-draws";
|
||||||
|
import { postAdminRunDrawSettlement } from "@/api/admin-settlement";
|
||||||
import { Button, buttonVariants } from "@/components/ui/button";
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
@@ -17,12 +18,14 @@ import {
|
|||||||
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 { toast } from "sonner";
|
||||||
|
|
||||||
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);
|
||||||
const [err, setErr] = useState<string | null>(null);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [settling, setSettling] = useState(false);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
if (!Number.isFinite(idNum) || idNum < 1) {
|
if (!Number.isFinite(idNum) || idNum < 1) {
|
||||||
@@ -42,6 +45,20 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
|||||||
}
|
}
|
||||||
}, [idNum]);
|
}, [idNum]);
|
||||||
|
|
||||||
|
async function runSettlement(): Promise<void> {
|
||||||
|
if (!Number.isFinite(idNum) || idNum < 1) return;
|
||||||
|
setSettling(true);
|
||||||
|
try {
|
||||||
|
const res = await postAdminRunDrawSettlement(idNum);
|
||||||
|
toast.success(res.ran ? "已触发结算" : "当前状态不可结算或已处理");
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e instanceof LotteryApiBizError ? e.message : "触发结算失败");
|
||||||
|
} finally {
|
||||||
|
setSettling(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
void load();
|
void load();
|
||||||
@@ -103,6 +120,9 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
|||||||
<Button type="button" variant="secondary" size="sm" onClick={() => void load()}>
|
<Button type="button" variant="secondary" size="sm" onClick={() => void load()}>
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="button" size="sm" disabled={settling} onClick={() => void runSettlement()}>
|
||||||
|
{settling ? "处理中…" : "触发结算"}
|
||||||
|
</Button>
|
||||||
<Link
|
<Link
|
||||||
href="/admin/settlement-batches"
|
href="/admin/settlement-batches"
|
||||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
|
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
|
||||||
|
|||||||
@@ -155,7 +155,8 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="font-mono text-xs text-muted-foreground">
|
<p className="font-mono text-xs text-muted-foreground">
|
||||||
RNG 摘要:{batch.rng_seed_hash ?? "—"}
|
生成方式:{batch.source_type === "manual" ? "人工录入" : "RNG 自动生成"} · 号码条数:
|
||||||
|
{batch.items.length}/23 · RNG 摘要:{batch.rng_seed_hash ?? "—"}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="justify-end gap-2">
|
<CardFooter className="justify-end gap-2">
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ function BatchTable({ batch }: { batch: AdminDrawBatchRow }) {
|
|||||||
<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>
|
||||||
<p className="font-mono text-xs text-muted-foreground">
|
<p className="font-mono text-xs text-muted-foreground">
|
||||||
RNG 摘要 {batch.rng_seed_hash ?? "—"} · 确认时间 {batch.confirmed_at ?? "—"}
|
生成方式 {batch.source_type === "manual" ? "人工录入" : "RNG"} · RNG 摘要 {batch.rng_seed_hash ?? "—"} · 确认时间 {batch.confirmed_at ?? "—"}
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="overflow-x-auto pt-0">
|
<CardContent className="overflow-x-auto pt-0">
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { getAdminDrawResultBatches } from "@/api/admin-draws";
|
import { getAdminDrawResultBatches, postAdminCreateManualResultBatch } from "@/api/admin-draws";
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -23,6 +25,22 @@ import type { AdminDrawBatchesData } from "@/types/api/admin-draws";
|
|||||||
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
|
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
|
||||||
import { DrawStatusBadge } from "./draw-status-badge";
|
import { DrawStatusBadge } from "./draw-status-badge";
|
||||||
|
|
||||||
|
const RESULT_SLOTS = [
|
||||||
|
{ prize_type: "first", prize_index: 0, label: "头奖" },
|
||||||
|
{ prize_type: "second", prize_index: 0, label: "二奖" },
|
||||||
|
{ prize_type: "third", prize_index: 0, label: "三奖" },
|
||||||
|
...Array.from({ length: 10 }, (_, i) => ({
|
||||||
|
prize_type: "starter",
|
||||||
|
prize_index: i,
|
||||||
|
label: `特别奖 ${i + 1}`,
|
||||||
|
})),
|
||||||
|
...Array.from({ length: 10 }, (_, i) => ({
|
||||||
|
prize_type: "consolation",
|
||||||
|
prize_index: i,
|
||||||
|
label: `安慰奖 ${i + 1}`,
|
||||||
|
})),
|
||||||
|
] as const;
|
||||||
|
|
||||||
export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
||||||
const profile = useAdminProfile();
|
const profile = useAdminProfile();
|
||||||
const canManageDraw = adminHasAnyPermission(profile?.permissions, [
|
const canManageDraw = adminHasAnyPermission(profile?.permissions, [
|
||||||
@@ -32,6 +50,10 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
|||||||
const [data, setData] = useState<AdminDrawBatchesData | null>(null);
|
const [data, setData] = useState<AdminDrawBatchesData | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [savingManual, setSavingManual] = useState(false);
|
||||||
|
const [manualNumbers, setManualNumbers] = useState<string[]>(
|
||||||
|
() => RESULT_SLOTS.map(() => ""),
|
||||||
|
);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
if (!Number.isFinite(idNum)) {
|
if (!Number.isFinite(idNum)) {
|
||||||
@@ -62,6 +84,33 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
|||||||
data,
|
data,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
async function saveManualDraft(): Promise<void> {
|
||||||
|
if (!Number.isFinite(idNum)) return;
|
||||||
|
const invalid = manualNumbers.some((n) => !/^[0-9]{4}$/.test(n));
|
||||||
|
if (invalid) {
|
||||||
|
toast.error("请完整输入 23 组 4 位数字");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSavingManual(true);
|
||||||
|
try {
|
||||||
|
const res = await postAdminCreateManualResultBatch(idNum, {
|
||||||
|
items: RESULT_SLOTS.map((slot, i) => ({
|
||||||
|
prize_type: slot.prize_type,
|
||||||
|
prize_index: slot.prize_index,
|
||||||
|
number_4d: manualNumbers[i],
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
toast.success(`已保存草稿 v${res.batch.result_version},等待确认发布`);
|
||||||
|
setManualNumbers(RESULT_SLOTS.map(() => ""));
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e instanceof LotteryApiBizError ? e.message : "保存失败");
|
||||||
|
} finally {
|
||||||
|
setSavingManual(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (loading && !data) {
|
if (loading && !data) {
|
||||||
return <p className="text-sm text-muted-foreground">加载中…</p>;
|
return <p className="text-sm text-muted-foreground">加载中…</p>;
|
||||||
}
|
}
|
||||||
@@ -71,13 +120,58 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">审核</CardTitle>
|
<CardTitle className="text-lg">人工录入开奖结果</CardTitle>
|
||||||
<p className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
<p className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||||
当前状态 <DrawStatusBadge status={data.draw_status} />
|
当前状态 <DrawStatusBadge status={data.draw_status} /> · 保存后生成待确认批次,不会直接发布
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{RESULT_SLOTS.map((slot, i) => (
|
||||||
|
<label key={`${slot.prize_type}-${slot.prize_index}`} className="space-y-1.5">
|
||||||
|
<span className="text-xs font-medium text-muted-foreground">{slot.label}</span>
|
||||||
|
<Input
|
||||||
|
inputMode="numeric"
|
||||||
|
maxLength={4}
|
||||||
|
value={manualNumbers[i]}
|
||||||
|
disabled={!canManageDraw || savingManual}
|
||||||
|
placeholder="0000"
|
||||||
|
className="font-mono"
|
||||||
|
onChange={(e) => {
|
||||||
|
const next = e.target.value.replace(/\D/g, "").slice(0, 4);
|
||||||
|
setManualNumbers((old) => old.map((v, idx) => (idx === i ? next : v)));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={savingManual}
|
||||||
|
onClick={() => setManualNumbers(RESULT_SLOTS.map(() => ""))}
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={!canManageDraw || savingManual || !["closed", "review"].includes(data.draw_status)}
|
||||||
|
onClick={() => void saveManualDraft()}
|
||||||
|
>
|
||||||
|
{savingManual ? "保存中…" : "保存草稿"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">待确认批次</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{pending.length === 0 ? (
|
{pending.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground py-6 text-center">
|
<p className="text-sm text-muted-foreground py-6 text-center">
|
||||||
@@ -118,5 +212,6 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { getAdminDraws } from "@/api/admin-draws";
|
import { getAdminDraws, postAdminGenerateDrawPlan } from "@/api/admin-draws";
|
||||||
import { Button, buttonVariants } from "@/components/ui/button";
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
import { Card, CardContent, 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";
|
||||||
@@ -67,6 +68,7 @@ export function DrawsIndexConsole() {
|
|||||||
const [appliedStatus, setAppliedStatus] = useState("");
|
const [appliedStatus, setAppliedStatus] = useState("");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [perPage, setPerPage] = useState<number>(20);
|
const [perPage, setPerPage] = useState<number>(20);
|
||||||
|
const [generating, setGenerating] = useState(false);
|
||||||
|
|
||||||
const drawStatusTriggerLabel = useMemo(
|
const drawStatusTriggerLabel = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -102,6 +104,19 @@ export function DrawsIndexConsole() {
|
|||||||
}
|
}
|
||||||
}, [page, perPage, appliedDrawNo, appliedStatus]);
|
}, [page, perPage, appliedDrawNo, appliedStatus]);
|
||||||
|
|
||||||
|
async function generatePlan(): Promise<void> {
|
||||||
|
setGenerating(true);
|
||||||
|
try {
|
||||||
|
const res = await postAdminGenerateDrawPlan();
|
||||||
|
toast.success(`已生成 ${res.created} 期,当前缓冲 ${res.upcoming}/${res.buffer_target}`);
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e instanceof LotteryApiBizError ? e.message : "生成失败");
|
||||||
|
} finally {
|
||||||
|
setGenerating(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = window.setTimeout(() => {
|
const timer = window.setTimeout(() => {
|
||||||
void load();
|
void load();
|
||||||
@@ -111,8 +126,11 @@ export function DrawsIndexConsole() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="space-y-1">
|
<CardHeader className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<CardTitle className="text-lg">期号列表</CardTitle>
|
<CardTitle className="text-lg">期号列表</CardTitle>
|
||||||
|
<Button type="button" onClick={() => void generatePlan()} disabled={generating}>
|
||||||
|
{generating ? "生成中…" : "批量生成期开奖计划"}
|
||||||
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{/* Grid:桌面端标签一行 / 控件一行,避免 flex+items-end 与各列实际高度不一致;移动端单列自上而下 */}
|
{/* Grid:桌面端标签一行 / 控件一行,避免 flex+items-end 与各列实际高度不一致;移动端单列自上而下 */}
|
||||||
@@ -194,22 +212,26 @@ export function DrawsIndexConsole() {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>期号</TableHead>
|
<TableHead>期号</TableHead>
|
||||||
<TableHead>状态</TableHead>
|
<TableHead>开始时间</TableHead>
|
||||||
<TableHead>开奖时间</TableHead>
|
|
||||||
<TableHead>封盘时间</TableHead>
|
<TableHead>封盘时间</TableHead>
|
||||||
|
<TableHead>开奖时间</TableHead>
|
||||||
|
<TableHead>状态</TableHead>
|
||||||
|
<TableHead className="text-right">下注总额</TableHead>
|
||||||
|
<TableHead className="text-right">派彩总额</TableHead>
|
||||||
|
<TableHead className="text-right">盈亏</TableHead>
|
||||||
<TableHead className="text-right">操作</TableHead>
|
<TableHead className="text-right">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className="text-muted-foreground">
|
<TableCell colSpan={9} className="text-muted-foreground">
|
||||||
加载中…
|
加载中…
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : data === null || data.items.length === 0 ? (
|
) : data === null || data.items.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className="text-muted-foreground">
|
<TableCell colSpan={9} className="text-muted-foreground">
|
||||||
暂无数据
|
暂无数据
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -217,11 +239,26 @@ export function DrawsIndexConsole() {
|
|||||||
data.items.map((row: AdminDrawListItem) => (
|
data.items.map((row: AdminDrawListItem) => (
|
||||||
<TableRow key={row.id}>
|
<TableRow key={row.id}>
|
||||||
<TableCell className="font-mono text-xs">{row.draw_no}</TableCell>
|
<TableCell className="font-mono text-xs">{row.draw_no}</TableCell>
|
||||||
|
<TableCell className="text-sm">{formatDt(row.start_time)}</TableCell>
|
||||||
|
<TableCell className="text-sm">{formatDt(row.close_time)}</TableCell>
|
||||||
|
<TableCell className="text-sm">{formatDt(row.draw_time)}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<DrawStatusBadge status={row.status} />
|
<DrawStatusBadge status={row.status} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm">{formatDt(row.draw_time)}</TableCell>
|
<TableCell className="text-right font-mono text-xs tabular-nums">
|
||||||
<TableCell className="text-sm">{formatDt(row.close_time)}</TableCell>
|
{row.total_bet_minor ?? "—"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-mono text-xs tabular-nums">
|
||||||
|
{row.total_payout_minor ?? "—"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
className={cn(
|
||||||
|
"text-right font-mono text-xs tabular-nums",
|
||||||
|
(row.profit_loss_minor ?? 0) < 0 ? "text-destructive" : "text-emerald-600",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{row.profit_loss_minor ?? "—"}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/draws/${row.id}`}
|
href={`/admin/draws/${row.id}`}
|
||||||
|
|||||||
@@ -2,8 +2,16 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { getAdminSettlementBatch, getAdminSettlementBatchDetails } from "@/api/admin-settlement";
|
import {
|
||||||
|
downloadAdminSettlementBatchExport,
|
||||||
|
getAdminSettlementBatch,
|
||||||
|
getAdminSettlementBatchDetails,
|
||||||
|
postAdminApproveSettlementBatch,
|
||||||
|
postAdminPayoutSettlementBatch,
|
||||||
|
postAdminRejectSettlementBatch,
|
||||||
|
} 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";
|
||||||
@@ -37,6 +45,7 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
|||||||
const [err, setErr] = useState<string | null>(null);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [perPage, setPerPage] = useState(25);
|
const [perPage, setPerPage] = useState(25);
|
||||||
|
const [acting, setActing] = useState<string | null>(null);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -57,6 +66,38 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
|||||||
}
|
}
|
||||||
}, [batchId, page, perPage]);
|
}, [batchId, page, perPage]);
|
||||||
|
|
||||||
|
async function runAction(label: string, action: () => Promise<unknown>): Promise<void> {
|
||||||
|
setActing(label);
|
||||||
|
try {
|
||||||
|
await action();
|
||||||
|
toast.success(`${label}成功`);
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e instanceof LotteryApiBizError ? e.message : `${label}失败`);
|
||||||
|
} finally {
|
||||||
|
setActing(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportCsv(): Promise<void> {
|
||||||
|
setActing("导出");
|
||||||
|
try {
|
||||||
|
const blob = await downloadAdminSettlementBatchExport(batchId);
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `settlement-${batchId}.csv`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e instanceof LotteryApiBizError ? e.message : "导出失败");
|
||||||
|
} finally {
|
||||||
|
setActing(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const t = window.setTimeout(() => void load(), 0);
|
const t = window.setTimeout(() => void load(), 0);
|
||||||
return () => window.clearTimeout(t);
|
return () => window.clearTimeout(t);
|
||||||
@@ -98,6 +139,10 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
|||||||
<span className="text-muted-foreground">结算状态</span>{" "}
|
<span className="text-muted-foreground">结算状态</span>{" "}
|
||||||
<span className="font-mono">{summary.status}</span>
|
<span className="font-mono">{summary.status}</span>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="text-muted-foreground">审核状态</span>{" "}
|
||||||
|
<span className="font-mono">{summary.review_status ?? "—"}</span>
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-muted-foreground">注单数</span>{" "}
|
<span className="text-muted-foreground">注单数</span>{" "}
|
||||||
<span className="tabular-nums">{summary.total_ticket_count}</span>
|
<span className="tabular-nums">{summary.total_ticket_count}</span>
|
||||||
@@ -122,6 +167,37 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
|
|||||||
<p>
|
<p>
|
||||||
<span className="text-muted-foreground">结束</span> {formatDt(summary.finished_at)}
|
<span className="text-muted-foreground">结束</span> {formatDt(summary.finished_at)}
|
||||||
</p>
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2 sm:col-span-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={acting !== null || summary.status !== "pending_review"}
|
||||||
|
onClick={() => void runAction("审核通过", () => postAdminApproveSettlementBatch(batchId))}
|
||||||
|
>
|
||||||
|
审核通过
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={acting !== null || summary.status !== "pending_review"}
|
||||||
|
onClick={() => void runAction("驳回", () => postAdminRejectSettlementBatch(batchId))}
|
||||||
|
>
|
||||||
|
驳回
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
disabled={acting !== null || summary.status !== "approved"}
|
||||||
|
onClick={() => void runAction("执行派彩", () => postAdminPayoutSettlementBatch(batchId))}
|
||||||
|
>
|
||||||
|
执行派彩
|
||||||
|
</Button>
|
||||||
|
<Button type="button" size="sm" variant="secondary" disabled={acting !== null} onClick={() => void exportCsv()}>
|
||||||
|
导出结算报表
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : loading ? (
|
) : loading ? (
|
||||||
|
|||||||
@@ -2,8 +2,15 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { getAdminSettlementBatches } from "@/api/admin-settlement";
|
import {
|
||||||
|
downloadAdminSettlementBatchExport,
|
||||||
|
getAdminSettlementBatches,
|
||||||
|
postAdminApproveSettlementBatch,
|
||||||
|
postAdminPayoutSettlementBatch,
|
||||||
|
postAdminRejectSettlementBatch,
|
||||||
|
} 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";
|
||||||
@@ -52,6 +59,7 @@ export function SettlementBatchesConsole() {
|
|||||||
const [appliedStatus, setAppliedStatus] = useState(STATUS_ALL);
|
const [appliedStatus, setAppliedStatus] = useState(STATUS_ALL);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [perPage, setPerPage] = useState(20);
|
const [perPage, setPerPage] = useState(20);
|
||||||
|
const [actingId, setActingId] = useState<number | null>(null);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -86,6 +94,38 @@ export function SettlementBatchesConsole() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function runBatchAction(batchId: number, label: string, action: () => Promise<unknown>): Promise<void> {
|
||||||
|
setActingId(batchId);
|
||||||
|
try {
|
||||||
|
await action();
|
||||||
|
toast.success(`${label}成功`);
|
||||||
|
await load();
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e instanceof LotteryApiBizError ? e.message : `${label}失败`);
|
||||||
|
} finally {
|
||||||
|
setActingId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportBatch(batchId: number): Promise<void> {
|
||||||
|
setActingId(batchId);
|
||||||
|
try {
|
||||||
|
const blob = await downloadAdminSettlementBatchExport(batchId);
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `settlement-${batchId}.csv`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e instanceof LotteryApiBizError ? e.message : "导出失败");
|
||||||
|
} finally {
|
||||||
|
setActingId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModuleScaffold>
|
<ModuleScaffold>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@@ -142,6 +182,7 @@ export function SettlementBatchesConsole() {
|
|||||||
<TableHead>ID</TableHead>
|
<TableHead>ID</TableHead>
|
||||||
<TableHead>期号</TableHead>
|
<TableHead>期号</TableHead>
|
||||||
<TableHead>版本</TableHead>
|
<TableHead>版本</TableHead>
|
||||||
|
<TableHead>审核状态</TableHead>
|
||||||
<TableHead>状态</TableHead>
|
<TableHead>状态</TableHead>
|
||||||
<TableHead className="text-right">注单数</TableHead>
|
<TableHead className="text-right">注单数</TableHead>
|
||||||
<TableHead className="text-right">中奖笔数</TableHead>
|
<TableHead className="text-right">中奖笔数</TableHead>
|
||||||
@@ -157,6 +198,9 @@ export function SettlementBatchesConsole() {
|
|||||||
<TableCell className="font-mono text-xs">{row.id}</TableCell>
|
<TableCell className="font-mono text-xs">{row.id}</TableCell>
|
||||||
<TableCell className="font-mono text-sm">{row.draw_no ?? "—"}</TableCell>
|
<TableCell className="font-mono text-sm">{row.draw_no ?? "—"}</TableCell>
|
||||||
<TableCell className="font-mono text-xs">v{row.settle_version}</TableCell>
|
<TableCell className="font-mono text-xs">v{row.settle_version}</TableCell>
|
||||||
|
<TableCell className="text-xs text-muted-foreground">
|
||||||
|
{row.review_status ?? "—"}
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -181,12 +225,49 @@ export function SettlementBatchesConsole() {
|
|||||||
{formatDt(row.finished_at ?? row.started_at)}
|
{formatDt(row.finished_at ?? row.started_at)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
<div className="flex flex-wrap justify-end gap-1.5">
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/settlement-batches/${row.id}/details`}
|
href={`/admin/settlement-batches/${row.id}/details`}
|
||||||
className={cn(buttonVariants({ variant: "link", size: "sm" }), "px-0")}
|
className={cn(buttonVariants({ variant: "link", size: "sm" }), "px-0")}
|
||||||
>
|
>
|
||||||
明细
|
明细
|
||||||
</Link>
|
</Link>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={actingId !== null || row.status !== "pending_review"}
|
||||||
|
onClick={() => void runBatchAction(row.id, "审核通过", () => postAdminApproveSettlementBatch(row.id))}
|
||||||
|
>
|
||||||
|
通过
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={actingId !== null || row.status !== "pending_review"}
|
||||||
|
onClick={() => void runBatchAction(row.id, "驳回", () => postAdminRejectSettlementBatch(row.id))}
|
||||||
|
>
|
||||||
|
驳回
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
disabled={actingId !== null || row.status !== "approved"}
|
||||||
|
onClick={() => void runBatchAction(row.id, "执行派彩", () => postAdminPayoutSettlementBatch(row.id))}
|
||||||
|
>
|
||||||
|
派彩
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
disabled={actingId !== null}
|
||||||
|
onClick={() => void exportBatch(row.id)}
|
||||||
|
>
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export type AdminDrawListItem = {
|
|||||||
current_result_version: number;
|
current_result_version: number;
|
||||||
settle_version: number;
|
settle_version: number;
|
||||||
is_reopened: boolean;
|
is_reopened: boolean;
|
||||||
|
total_bet_minor?: number;
|
||||||
|
total_payout_minor?: number;
|
||||||
|
profit_loss_minor?: number;
|
||||||
updated_at: string | null;
|
updated_at: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,3 +90,38 @@ export type AdminDrawPublishResponse = {
|
|||||||
status: string;
|
status: string;
|
||||||
result_version: number;
|
result_version: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AdminDrawActionResponse = {
|
||||||
|
draw_no: string;
|
||||||
|
status: string;
|
||||||
|
close_time?: string | null;
|
||||||
|
is_reopened?: boolean;
|
||||||
|
current_result_version?: number;
|
||||||
|
cooling_end_time?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdminDrawPlanGenerateResponse = {
|
||||||
|
created: number;
|
||||||
|
buffer_target: number;
|
||||||
|
upcoming: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdminDrawManualBatchPayload = {
|
||||||
|
items: Array<{
|
||||||
|
prize_type: string;
|
||||||
|
prize_index: number;
|
||||||
|
number_4d: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdminDrawBatchCreateResponse = {
|
||||||
|
draw_no: string;
|
||||||
|
status: string;
|
||||||
|
batch: {
|
||||||
|
id: number;
|
||||||
|
result_version: number;
|
||||||
|
source_type: string;
|
||||||
|
status: string;
|
||||||
|
items_count: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export type AdminSettlementBatchRow = {
|
|||||||
result_batch_id: number;
|
result_batch_id: number;
|
||||||
settle_version: number;
|
settle_version: number;
|
||||||
status: string;
|
status: string;
|
||||||
|
review_status: string | null;
|
||||||
|
reviewed_at: string | null;
|
||||||
|
paid_at: string | null;
|
||||||
total_ticket_count: number;
|
total_ticket_count: number;
|
||||||
total_win_count: number;
|
total_win_count: number;
|
||||||
total_payout_amount: number;
|
total_payout_amount: number;
|
||||||
@@ -34,6 +37,11 @@ export type AdminSettlementBatchShowData = {
|
|||||||
result_batch_status: string | null;
|
result_batch_status: string | null;
|
||||||
settle_version: number;
|
settle_version: number;
|
||||||
status: string;
|
status: string;
|
||||||
|
review_status: string | null;
|
||||||
|
reviewed_by: number | null;
|
||||||
|
reviewed_at: string | null;
|
||||||
|
review_remark: string | null;
|
||||||
|
paid_at: string | null;
|
||||||
total_ticket_count: number;
|
total_ticket_count: number;
|
||||||
total_win_count: number;
|
total_win_count: number;
|
||||||
total_payout_amount: number;
|
total_payout_amount: number;
|
||||||
@@ -68,3 +76,17 @@ export type AdminSettlementBatchDetailsData = {
|
|||||||
last_page: number;
|
last_page: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AdminSettlementRunResponse = {
|
||||||
|
ran: boolean;
|
||||||
|
draw_no: string;
|
||||||
|
status: string;
|
||||||
|
settle_version: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdminSettlementWorkflowResponse = {
|
||||||
|
id: number;
|
||||||
|
status: string;
|
||||||
|
review_status?: string | null;
|
||||||
|
paid_at?: string | null;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user