diff --git a/src/api/admin-draws.ts b/src/api/admin-draws.ts index 5c4fdd9..d00aaab 100644 --- a/src/api/admin-draws.ts +++ b/src/api/admin-draws.ts @@ -5,8 +5,12 @@ import { API_V1_PREFIX } from "./paths"; import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance"; import type { AdminDrawBatchesData, + AdminDrawActionResponse, AdminDrawListData, + AdminDrawBatchCreateResponse, + AdminDrawManualBatchPayload, AdminDrawPublishResponse, + AdminDrawPlanGenerateResponse, AdminDrawShowData, } from "@/types/api/admin-draws"; @@ -47,3 +51,33 @@ export async function postAdminPublishResultBatch( `${A}/draws/${drawId}/result-batches/${batchId}/publish`, ); } + +export async function postAdminGenerateDrawPlan(): Promise { + return adminRequest.post(`${A}/draws/generate-plan`); +} + +export async function postAdminManualCloseDraw(drawId: number): Promise { + return adminRequest.post(`${A}/draws/${drawId}/manual-close`); +} + +export async function postAdminCancelDraw(drawId: number): Promise { + return adminRequest.post(`${A}/draws/${drawId}/cancel`); +} + +export async function postAdminRunDrawRng(drawId: number): Promise { + return adminRequest.post(`${A}/draws/${drawId}/rng`); +} + +export async function postAdminCreateManualResultBatch( + drawId: number, + payload: AdminDrawManualBatchPayload, +): Promise { + return adminRequest.post( + `${A}/draws/${drawId}/result-batches`, + payload, + ); +} + +export async function postAdminReopenDraw(drawId: number): Promise { + return adminRequest.post(`${A}/draws/${drawId}/reopen`); +} diff --git a/src/api/admin-settlement.ts b/src/api/admin-settlement.ts index af9652d..a6fde4a 100644 --- a/src/api/admin-settlement.ts +++ b/src/api/admin-settlement.ts @@ -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"; @@ -6,6 +8,8 @@ import type { AdminSettlementBatchDetailsData, AdminSettlementBatchListData, AdminSettlementBatchShowData, + AdminSettlementRunResponse, + AdminSettlementWorkflowResponse, } from "@/types/api/admin-settlement"; const A = `${API_V1_PREFIX}/admin`; @@ -41,3 +45,42 @@ export async function getAdminSettlementBatchDetails( { params: q }, ); } + +export async function postAdminRunDrawSettlement(drawId: number): Promise { + return adminRequest.post(`${A}/draws/${drawId}/settlement/run`); +} + +export async function postAdminApproveSettlementBatch( + batchId: number, + remark?: string, +): Promise { + return adminRequest.post( + `${A}/settlement-batches/${batchId}/approve`, + { remark }, + ); +} + +export async function postAdminRejectSettlementBatch( + batchId: number, + remark?: string, +): Promise { + return adminRequest.post( + `${A}/settlement-batches/${batchId}/reject`, + { remark }, + ); +} + +export async function postAdminPayoutSettlementBatch(batchId: number): Promise { + return adminRequest.post(`${A}/settlement-batches/${batchId}/payout`); +} + +export async function downloadAdminSettlementBatchExport(batchId: number): Promise { + const res = await adminHttp.request( + withAdminAuthHeader(withAdminLocaleHeaders({ + url: `${A}/settlement-batches/${batchId}/export`, + method: "GET", + responseType: "blob", + })), + ); + return res.data; +} diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx index abeaced..b7ce854 100644 --- a/src/components/ui/table.tsx +++ b/src/components/ui/table.tsx @@ -70,7 +70,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) { ) { + {children ?? "—"} + + ); +} diff --git a/src/modules/config/config-version-actions.tsx b/src/modules/config/config-version-actions.tsx new file mode 100644 index 0000000..7d8f4c3 --- /dev/null +++ b/src/modules/config/config-version-actions.tsx @@ -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 ( +
+ + + {isDraft ? ( + <> + + + + ) : null} +
+ ); +} diff --git a/src/modules/config/config-version-switcher.tsx b/src/modules/config/config-version-switcher.tsx index 1b2edf8..49ffae3 100644 --- a/src/modules/config/config-version-switcher.tsx +++ b/src/modules/config/config-version-switcher.tsx @@ -1,6 +1,7 @@ "use client"; import { useMemo, useState } from "react"; +import { Layers } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; @@ -12,7 +13,6 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { Label } from "@/components/ui/label"; import { Sheet, SheetContent, @@ -59,7 +59,6 @@ export function ConfigVersionSwitcher({ selectedId, onSelectedIdChange, loading = false, - label = "配置版本", sheetTitle = "切换配置版本", sheetDescription = "选择一条版本在本页查看;草稿可编辑,生效中与已归档为只读。", className, @@ -128,42 +127,39 @@ export function ConfigVersionSwitcher({ return ( <> -
- -
-
- {selectedVersion ? ( - <> - - v{selectedVersion.version_no} - - - #{selectedVersion.id} - - ) : ( - {loading ? "加载中…" : "未选择版本"} - )} -
- +
+
+ {selectedVersion ? ( + <> + + v{selectedVersion.version_no} + + + #{selectedVersion.id} + + ) : ( + {loading ? "加载中…" : "未选择版本"} + )}
+
-
-
-
- +
+ {sheetTitle} @@ -172,13 +168,10 @@ export function ConfigVersionSwitcher({
-
+
{statusCounts.map((s) => ( -
+

{s.label}

{s.count} @@ -187,7 +180,7 @@ export function ConfigVersionSwitcher({ ))}

-
+
{sortedVersions.length === 0 ? ( 暂无版本记录。 @@ -225,15 +218,14 @@ export function ConfigVersionSwitcher({
-
); } diff --git a/src/modules/draws/draws-index-console.tsx b/src/modules/draws/draws-index-console.tsx index d7b8875..557f953 100644 --- a/src/modules/draws/draws-index-console.tsx +++ b/src/modules/draws/draws-index-console.tsx @@ -2,8 +2,9 @@ import Link from "next/link"; 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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; @@ -67,6 +68,7 @@ export function DrawsIndexConsole() { const [appliedStatus, setAppliedStatus] = useState(""); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(20); + const [generating, setGenerating] = useState(false); const drawStatusTriggerLabel = useMemo( () => @@ -102,6 +104,19 @@ export function DrawsIndexConsole() { } }, [page, perPage, appliedDrawNo, appliedStatus]); + async function generatePlan(): Promise { + 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(() => { const timer = window.setTimeout(() => { void load(); @@ -111,8 +126,11 @@ export function DrawsIndexConsole() { return ( - + 期号列表 + {/* Grid:桌面端标签一行 / 控件一行,避免 flex+items-end 与各列实际高度不一致;移动端单列自上而下 */} @@ -194,22 +212,26 @@ export function DrawsIndexConsole() { 期号 - 状态 - 开奖时间 + 开始时间 封盘时间 + 开奖时间 + 状态 + 下注总额 + 派彩总额 + 盈亏 操作 {loading ? ( - + 加载中… ) : data === null || data.items.length === 0 ? ( - + 暂无数据 @@ -217,11 +239,26 @@ export function DrawsIndexConsole() { data.items.map((row: AdminDrawListItem) => ( {row.draw_no} + {formatDt(row.start_time)} + {formatDt(row.close_time)} + {formatDt(row.draw_time)} - {formatDt(row.draw_time)} - {formatDt(row.close_time)} + + {row.total_bet_minor ?? "—"} + + + {row.total_payout_minor ?? "—"} + + + {row.profit_loss_minor ?? "—"} + (null); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(25); + const [acting, setActing] = useState(null); const load = useCallback(async () => { setLoading(true); @@ -57,6 +66,38 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) { } }, [batchId, page, perPage]); + async function runAction(label: string, action: () => Promise): Promise { + 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 { + 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(() => { const t = window.setTimeout(() => void load(), 0); return () => window.clearTimeout(t); @@ -98,6 +139,10 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) { 结算状态{" "} {summary.status}

+

+ 审核状态{" "} + {summary.review_status ?? "—"} +

注单数{" "} {summary.total_ticket_count} @@ -122,6 +167,37 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {

结束 {formatDt(summary.finished_at)}

+
+ + + + +
) : loading ? ( diff --git a/src/modules/settlement/settlement-batches-console.tsx b/src/modules/settlement/settlement-batches-console.tsx index 146176b..01123cd 100644 --- a/src/modules/settlement/settlement-batches-console.tsx +++ b/src/modules/settlement/settlement-batches-console.tsx @@ -2,8 +2,15 @@ import Link from "next/link"; 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 { ModuleScaffold } from "@/components/admin/module-scaffold"; import { Button, buttonVariants } from "@/components/ui/button"; @@ -52,6 +59,7 @@ export function SettlementBatchesConsole() { const [appliedStatus, setAppliedStatus] = useState(STATUS_ALL); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(20); + const [actingId, setActingId] = useState(null); const load = useCallback(async () => { setLoading(true); @@ -86,6 +94,38 @@ export function SettlementBatchesConsole() { setPage(1); }; + async function runBatchAction(batchId: number, label: string, action: () => Promise): Promise { + 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 { + 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 (
@@ -142,6 +182,7 @@ export function SettlementBatchesConsole() { ID 期号 版本 + 审核状态 状态 注单数 中奖笔数 @@ -157,6 +198,9 @@ export function SettlementBatchesConsole() { {row.id} {row.draw_no ?? "—"} v{row.settle_version} + + {row.review_status ?? "—"} + - - 明细 - +
+ + 明细 + + + + + +
))} diff --git a/src/types/api/admin-draws.ts b/src/types/api/admin-draws.ts index 5354ee4..2d186c7 100644 --- a/src/types/api/admin-draws.ts +++ b/src/types/api/admin-draws.ts @@ -12,6 +12,9 @@ export type AdminDrawListItem = { current_result_version: number; settle_version: number; is_reopened: boolean; + total_bet_minor?: number; + total_payout_minor?: number; + profit_loss_minor?: number; updated_at: string | null; }; @@ -87,3 +90,38 @@ export type AdminDrawPublishResponse = { status: string; 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; + }; +}; diff --git a/src/types/api/admin-settlement.ts b/src/types/api/admin-settlement.ts index 2062ded..f1cfffd 100644 --- a/src/types/api/admin-settlement.ts +++ b/src/types/api/admin-settlement.ts @@ -5,6 +5,9 @@ export type AdminSettlementBatchRow = { result_batch_id: number; settle_version: number; status: string; + review_status: string | null; + reviewed_at: string | null; + paid_at: string | null; total_ticket_count: number; total_win_count: number; total_payout_amount: number; @@ -34,6 +37,11 @@ export type AdminSettlementBatchShowData = { result_batch_status: string | null; settle_version: number; 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_win_count: number; total_payout_amount: number; @@ -68,3 +76,17 @@ export type AdminSettlementBatchDetailsData = { 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; +};