feat: 增加管理端多语言与多模块界面国际化支持
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
@@ -37,6 +38,7 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
|
||||
}
|
||||
|
||||
export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
||||
const { t } = useTranslation(["draws", "common"]);
|
||||
const idNum = Number(drawId);
|
||||
const profile = useAdminProfile();
|
||||
const canManageDraw = adminHasAnyPermission(profile?.permissions, [PRD_DRAW_RESULT_MANAGE]);
|
||||
@@ -49,7 +51,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!Number.isFinite(idNum)) {
|
||||
setError("无效的期号 ID");
|
||||
setError(t("invalidDrawId"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -59,21 +61,21 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
||||
setData(await getAdminDraw(idNum));
|
||||
} catch (e) {
|
||||
setData(null);
|
||||
setError(e instanceof LotteryApiBizError ? e.message : "加载失败");
|
||||
setError(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idNum]);
|
||||
}, [idNum, t]);
|
||||
|
||||
async function runAction(name: string, action: () => Promise<unknown>): Promise<void> {
|
||||
if (!Number.isFinite(idNum)) return;
|
||||
setActing(name);
|
||||
try {
|
||||
await action();
|
||||
toast.success(`${name}成功`);
|
||||
toast.success(t("actionSuccess", { name }));
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : `${name}失败`);
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("actionFailed", { name }));
|
||||
} finally {
|
||||
setActing(null);
|
||||
}
|
||||
@@ -87,11 +89,11 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
||||
}, [load]);
|
||||
|
||||
if (loading && !data) {
|
||||
return <p className="text-sm text-muted-foreground">加载中…</p>;
|
||||
return <p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>;
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return <p className="text-sm text-destructive">{error ?? "无数据"}</p>;
|
||||
return <p className="text-sm text-destructive">{error ?? t("states.noData", { ns: "common" })}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -101,46 +103,49 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-xl">{data.draw_no}</CardTitle>
|
||||
<p className="mt-1 text-sm text-muted-foreground">开奖详情</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{t("drawDetail")}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DrawStatusBadge status={data.status} label={data.status} />
|
||||
<DrawStatusBadge status={data.hall_preview_status} label={`大厅预览 ${data.hall_preview_status}`} />
|
||||
<DrawStatusBadge
|
||||
status={data.hall_preview_status}
|
||||
label={t("hallPreviewStatus", { status: data.hall_preview_status })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6 p-6 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Field label="业务日">{data.business_date}</Field>
|
||||
<Field label="流水序号">{data.sequence_no}</Field>
|
||||
<Field label="开始时间">{formatDt(data.start_time)}</Field>
|
||||
<Field label="封盘时间">{formatDt(data.close_time)}</Field>
|
||||
<Field label="计划开奖">{formatDt(data.draw_time)}</Field>
|
||||
<Field label="冷静期结束">{formatDt(data.cooling_end_time)}</Field>
|
||||
<Field label={t("businessDate")}>{data.business_date}</Field>
|
||||
<Field label={t("sequenceNo")}>{data.sequence_no}</Field>
|
||||
<Field label={t("startTime")}>{formatDt(data.start_time)}</Field>
|
||||
<Field label={t("closeTime")}>{formatDt(data.close_time)}</Field>
|
||||
<Field label={t("plannedDraw")}>{formatDt(data.draw_time)}</Field>
|
||||
<Field label={t("coolingEndTime")}>{formatDt(data.cooling_end_time)}</Field>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Field label="结果来源">{data.result_source ?? "—"}</Field>
|
||||
<Field label="当前结果版本">{data.current_result_version}</Field>
|
||||
<Field label="结算版本">{data.settle_version}</Field>
|
||||
<Field label="是否重开">{data.is_reopened ? "是" : "否"}</Field>
|
||||
<Field label={t("resultSource")}>{data.result_source ?? "—"}</Field>
|
||||
<Field label={t("currentResultVersion")}>{data.current_result_version}</Field>
|
||||
<Field label={t("settleVersion")}>{data.settle_version}</Field>
|
||||
<Field label={t("isReopened")}>{data.is_reopened ? t("yes") : t("no")}</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-muted/20 p-4">
|
||||
<p className="text-sm font-medium text-muted-foreground">批次统计</p>
|
||||
<p className="text-sm font-medium text-muted-foreground">{t("batchStats")}</p>
|
||||
<div className="mt-3 grid gap-3 text-sm">
|
||||
<div className="flex items-center justify-between rounded-lg bg-background px-3 py-2">
|
||||
<span>总批次</span>
|
||||
<span>{t("batchTotal")}</span>
|
||||
<span className="font-semibold">{data.result_batch_counts.total}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-lg bg-background px-3 py-2 text-amber-600 dark:text-amber-400">
|
||||
<span>待审核</span>
|
||||
<span>{t("pendingReview")}</span>
|
||||
<span className="font-semibold">{data.result_batch_counts.pending_review}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-lg bg-background px-3 py-2 text-emerald-600 dark:text-emerald-400">
|
||||
<span>已发布</span>
|
||||
<span>{t("published")}</span>
|
||||
<span className="font-semibold">{data.result_batch_counts.published}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,7 +153,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
||||
href={`/admin/draws/${drawId}/finance`}
|
||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "mt-4 w-full")}
|
||||
>
|
||||
查看期号收支
|
||||
{t("viewFinance")}
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -156,9 +161,9 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">期号操作</CardTitle>
|
||||
<CardTitle className="text-base">{t("drawActions")}</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
手动封盘 / 取消 / RNG / 重开 / 触发结算均直接调用后台接口。
|
||||
{t("drawActionsDesc")}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap gap-2">
|
||||
@@ -166,42 +171,42 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={!canManageDraw || acting !== null || !["pending", "open"].includes(data.status)}
|
||||
onClick={() => void runAction("手动封盘", () => postAdminManualCloseDraw(idNum))}
|
||||
onClick={() => void runAction(t("manualClose"), () => postAdminManualCloseDraw(idNum))}
|
||||
>
|
||||
{acting === "手动封盘" ? "处理中…" : "手动封盘"}
|
||||
{acting === t("manualClose") ? t("processing") : t("manualClose")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={!canManageDraw || acting !== null || !["pending", "open", "closing", "closed"].includes(data.status)}
|
||||
onClick={() => void runAction("取消期号", () => postAdminCancelDraw(idNum))}
|
||||
onClick={() => void runAction(t("cancelDraw"), () => postAdminCancelDraw(idNum))}
|
||||
>
|
||||
{acting === "取消期号" ? "处理中…" : "未开奖前取消"}
|
||||
{acting === t("cancelDraw") ? t("processing") : t("cancelBeforeDraw")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!canManageDraw || acting !== null || data.status !== "closed"}
|
||||
onClick={() => void runAction("RNG开奖", () => postAdminRunDrawRng(idNum))}
|
||||
onClick={() => void runAction(t("rngDraw"), () => postAdminRunDrawRng(idNum))}
|
||||
>
|
||||
{acting === "RNG开奖" ? "生成中…" : "RNG 自动生成"}
|
||||
{acting === t("rngDraw") ? t("generating") : t("rngAutoGenerate")}
|
||||
</Button>
|
||||
{isSuperAdmin ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
disabled={acting !== null || data.status !== "cooldown"}
|
||||
onClick={() => void runAction("重开", () => postAdminReopenDraw(idNum))}
|
||||
onClick={() => void runAction(t("reopen"), () => postAdminReopenDraw(idNum))}
|
||||
>
|
||||
{acting === "重开" ? "处理中…" : "冷静期重开"}
|
||||
{acting === t("reopen") ? t("processing") : t("cooldownReopen")}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={acting !== null || !["settling", "cooldown"].includes(data.status)}
|
||||
onClick={() => void runAction("触发结算", () => postAdminRunDrawSettlement(idNum))}
|
||||
onClick={() => void runAction(t("runSettlement"), () => postAdminRunDrawSettlement(idNum))}
|
||||
>
|
||||
{acting === "触发结算" ? "处理中…" : "触发结算"}
|
||||
{acting === t("runSettlement") ? t("processing") : t("runSettlement")}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getAdminDrawFinanceSummary } from "@/api/admin-draws";
|
||||
import { postAdminRunDrawSettlement } from "@/api/admin-settlement";
|
||||
@@ -21,6 +22,7 @@ import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactElement {
|
||||
const { t } = useTranslation(["draws", "common"]);
|
||||
const idNum = Number(drawId);
|
||||
const [data, setData] = useState<AdminDrawFinanceSummaryData | null>(null);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
@@ -29,7 +31,7 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!Number.isFinite(idNum) || idNum < 1) {
|
||||
setErr("无效的期号 ID");
|
||||
setErr(t("invalidDrawId"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -38,22 +40,22 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
||||
try {
|
||||
setData(await getAdminDrawFinanceSummary(idNum));
|
||||
} catch (e) {
|
||||
setErr(e instanceof LotteryApiBizError ? e.message : "加载失败");
|
||||
setErr(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
|
||||
setData(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idNum]);
|
||||
}, [idNum, t]);
|
||||
|
||||
async function runSettlement(): Promise<void> {
|
||||
if (!Number.isFinite(idNum) || idNum < 1) return;
|
||||
setSettling(true);
|
||||
try {
|
||||
const res = await postAdminRunDrawSettlement(idNum);
|
||||
toast.success(res.ran ? "已触发结算" : "当前状态不可结算或已处理");
|
||||
toast.success(res.ran ? t("runSettlement") : t("status"));
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "触发结算失败");
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("actionFailed", { name: t("runSettlement") }));
|
||||
} finally {
|
||||
setSettling(false);
|
||||
}
|
||||
@@ -66,44 +68,44 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
||||
}, [load]);
|
||||
|
||||
if (loading && !data) {
|
||||
return <p className="text-muted-foreground text-sm">加载中…</p>;
|
||||
return <p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p>;
|
||||
}
|
||||
|
||||
if (err || !data) {
|
||||
return <p className="text-destructive text-sm">{err ?? "无数据"}</p>;
|
||||
return <p className="text-destructive text-sm">{err ?? t("states.noData", { ns: "common" })}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">期号收支概览</CardTitle>
|
||||
<CardTitle className="text-lg">{t("financeOverview")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 text-sm sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<span className="text-muted-foreground">期号</span>
|
||||
<span className="text-muted-foreground">{t("drawNo")}</span>
|
||||
<p className="font-mono font-semibold">{data.draw_no}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">状态</span>
|
||||
<span className="text-muted-foreground">{t("status")}</span>
|
||||
<p>{data.draw_status}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">订单数 / 注项数</span>
|
||||
<span className="text-muted-foreground">{t("orderAndItemCount")}</span>
|
||||
<p className="tabular-nums">
|
||||
{data.order_count} / {data.ticket_item_count}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">当期实扣投注</span>
|
||||
<span className="text-muted-foreground">{t("actualBet")}</span>
|
||||
<p className="tabular-nums font-medium">{data.total_bet_minor}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">当期派彩合计</span>
|
||||
<span className="text-muted-foreground">{t("currentPayout")}</span>
|
||||
<p className="tabular-nums font-medium">{data.total_payout_minor}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">近似毛损益</span>
|
||||
<span className="text-muted-foreground">{t("grossProfit")}</span>
|
||||
<p
|
||||
className={cn(
|
||||
"tabular-nums font-semibold",
|
||||
@@ -118,38 +120,38 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button type="button" variant="secondary" size="sm" onClick={() => void load()}>
|
||||
刷新
|
||||
{t("actions.refresh", { ns: "common" })}
|
||||
</Button>
|
||||
<Button type="button" size="sm" disabled={settling} onClick={() => void runSettlement()}>
|
||||
{settling ? "处理中…" : "触发结算"}
|
||||
{settling ? t("processing") : t("runSettlement")}
|
||||
</Button>
|
||||
<Link
|
||||
href="/admin/settlement-batches"
|
||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
|
||||
>
|
||||
结算批次列表(按期号筛选)
|
||||
{t("settlementBatchList")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">本关联期结算批次</CardTitle>
|
||||
<CardTitle className="text-base">{t("relatedSettlementBatches")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{data.settlement_batches.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">暂无结算批次记录。</p>
|
||||
<p className="text-muted-foreground text-sm">{t("noSettlementBatches")}</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-20">ID</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead className="text-right">票数</TableHead>
|
||||
<TableHead className="text-right">中奖数</TableHead>
|
||||
<TableHead className="text-right">派彩</TableHead>
|
||||
<TableHead className="text-right">Jackpot</TableHead>
|
||||
<TableHead>完成时间</TableHead>
|
||||
<TableHead>{t("status")}</TableHead>
|
||||
<TableHead className="text-right">{t("ticketCount")}</TableHead>
|
||||
<TableHead className="text-right">{t("winCount")}</TableHead>
|
||||
<TableHead className="text-right">{t("payoutTotal")}</TableHead>
|
||||
<TableHead className="text-right">{t("jackpot")}</TableHead>
|
||||
<TableHead>{t("finishedAt")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { getAdminDrawResultBatches, postAdminPublishResultBatch } from "@/api/admin-draws";
|
||||
@@ -25,6 +26,7 @@ import type { AdminDrawBatchRow, AdminDrawBatchesData } from "@/types/api/admin-
|
||||
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
|
||||
|
||||
export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchId: string }) {
|
||||
const { t } = useTranslation(["draws", "common"]);
|
||||
const profile = useAdminProfile();
|
||||
const canManageDraw = adminHasAnyPermission(profile?.permissions, [
|
||||
PRD_DRAW_RESULT_MANAGE,
|
||||
@@ -38,7 +40,7 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!Number.isFinite(idNum)) {
|
||||
setError("无效的期号 ID");
|
||||
setError(t("invalidDrawId"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -48,11 +50,11 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
|
||||
setData(await getAdminDrawResultBatches(idNum));
|
||||
} catch (e) {
|
||||
setData(null);
|
||||
setError(e instanceof LotteryApiBizError ? e.message : "加载失败");
|
||||
setError(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idNum]);
|
||||
}, [idNum, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
@@ -71,10 +73,10 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
|
||||
setPublishing(true);
|
||||
try {
|
||||
const res = await postAdminPublishResultBatch(idNum, batchNum);
|
||||
toast.success(`已发布 · ${res.draw_no} · 状态 ${res.status}`);
|
||||
toast.success(t("publishSuccess", { drawNo: res.draw_no, status: res.status }));
|
||||
await load();
|
||||
} catch (e) {
|
||||
const msg = e instanceof LotteryApiBizError ? e.message : "发布失败";
|
||||
const msg = e instanceof LotteryApiBizError ? e.message : t("publishFailed");
|
||||
toast.error(msg);
|
||||
} finally {
|
||||
setPublishing(false);
|
||||
@@ -82,18 +84,18 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
|
||||
}
|
||||
|
||||
if (loading && !data) {
|
||||
return <p className="text-sm text-muted-foreground">加载中…</p>;
|
||||
return <p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>;
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return <p className="text-sm text-destructive">{error ?? "无数据"}</p>;
|
||||
return <p className="text-sm text-destructive">{error ?? t("states.noData", { ns: "common" })}</p>;
|
||||
}
|
||||
|
||||
if (!batch) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>未找到批次</AlertTitle>
|
||||
<AlertDescription>请返回审核列表确认 batch id。</AlertDescription>
|
||||
<AlertTitle>{t("batchNotFound")}</AlertTitle>
|
||||
<AlertDescription>{t("batchNotFoundDesc")}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -105,31 +107,31 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<Link href={`/admin/draws/${drawId}/review`} className={buttonVariants({ variant: "ghost", size: "sm" })}>
|
||||
← 审核队列
|
||||
← {t("backToReviewQueue")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">发布</CardTitle>
|
||||
<CardTitle className="text-lg">{t("publishTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{!canManageDraw ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>无发布权限</AlertTitle>
|
||||
<AlertDescription>当前账号不可执行发布。</AlertDescription>
|
||||
<AlertTitle>{t("noPublishPermission")}</AlertTitle>
|
||||
<AlertDescription>{t("noPublishPermission")}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
{!canPublish && canManageDraw ? (
|
||||
<Alert>
|
||||
<AlertTitle>不可发布</AlertTitle>
|
||||
<AlertDescription>当前批次状态为「{batch.status}」。</AlertDescription>
|
||||
<AlertTitle>{t("cannotPublish")}</AlertTitle>
|
||||
<AlertDescription>{t("cannotPublishDesc", { status: batch.status })}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
{canPublish ? (
|
||||
<Alert>
|
||||
<AlertTitle>请核对以下号码后再发布</AlertTitle>
|
||||
<AlertDescription>确认无误后点击发布。</AlertDescription>
|
||||
<AlertTitle>{t("checkBeforePublish")}</AlertTitle>
|
||||
<AlertDescription>{t("checkBeforePublishDesc")}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
@@ -137,7 +139,7 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>奖项</TableHead>
|
||||
<TableHead>{t("prize")}</TableHead>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead className="font-mono">4D</TableHead>
|
||||
</TableRow>
|
||||
@@ -155,8 +157,11 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
|
||||
</div>
|
||||
|
||||
<p className="font-mono text-xs text-muted-foreground">
|
||||
生成方式:{batch.source_type === "manual" ? "人工录入" : "RNG 自动生成"} · 号码条数:
|
||||
{batch.items.length}/23 · RNG 摘要:{batch.rng_seed_hash ?? "—"}
|
||||
{t("sourceTypeFull", {
|
||||
source: batch.source_type === "manual" ? t("manualEntry") : t("rngAutoGenerate"),
|
||||
count: batch.items.length,
|
||||
hash: batch.rng_seed_hash ?? "—",
|
||||
})}
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end gap-2">
|
||||
@@ -164,14 +169,14 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
|
||||
href={`/admin/draws/${drawId}/results`}
|
||||
className={cn(buttonVariants({ variant: "outline", size: "default" }))}
|
||||
>
|
||||
查看已发布展示
|
||||
{t("publishedView")}
|
||||
</Link>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!canPublish || publishing}
|
||||
onClick={() => void publish()}
|
||||
>
|
||||
{publishing ? "提交中…" : "确认发布"}
|
||||
{publishing ? t("submitting") : t("confirmPublish")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getAdminDrawResultBatches } from "@/api/admin-draws";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
@@ -24,6 +25,7 @@ import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
|
||||
import { DrawStatusBadge } from "./draw-status-badge";
|
||||
|
||||
export function DrawResultsConsole({ drawId }: { drawId: string }) {
|
||||
const { t } = useTranslation(["draws", "common"]);
|
||||
const profile = useAdminProfile();
|
||||
const canManageDraw = adminHasAnyPermission(profile?.permissions, [
|
||||
PRD_DRAW_RESULT_MANAGE,
|
||||
@@ -35,7 +37,7 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!Number.isFinite(idNum)) {
|
||||
setError("无效的期号 ID");
|
||||
setError(t("invalidDrawId"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -45,11 +47,11 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
|
||||
setData(await getAdminDrawResultBatches(idNum));
|
||||
} catch (e) {
|
||||
setData(null);
|
||||
setError(e instanceof LotteryApiBizError ? e.message : "加载失败");
|
||||
setError(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idNum]);
|
||||
}, [idNum, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
@@ -59,11 +61,11 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
|
||||
}, [load]);
|
||||
|
||||
if (loading && !data) {
|
||||
return <p className="text-sm text-muted-foreground">加载中…</p>;
|
||||
return <p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>;
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return <p className="text-sm text-destructive">{error ?? "无数据"}</p>;
|
||||
return <p className="text-sm text-destructive">{error ?? t("states.noData", { ns: "common" })}</p>;
|
||||
}
|
||||
|
||||
const published = data.batches.filter((b) => b.status === "published");
|
||||
@@ -72,23 +74,23 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">开奖结果</h2>
|
||||
<h2 className="text-lg font-semibold">{t("resultsTitle")}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
期号 {data.draw_no} · <DrawStatusBadge status={data.draw_status} />
|
||||
{t("drawNo")} {data.draw_no} · <DrawStatusBadge status={data.draw_status} />
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href={`/admin/draws/${drawId}/review`}
|
||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
|
||||
>
|
||||
{canManageDraw ? "去审核 / 发布" : "查看审核队列"}
|
||||
{canManageDraw ? t("reviewAndPublish") : t("viewReviewQueue")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{published.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
||||
暂无已发布批次。
|
||||
{t("noPublishedBatch")}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -99,24 +101,29 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
|
||||
}
|
||||
|
||||
function BatchTable({ batch }: { batch: AdminDrawBatchRow }) {
|
||||
const { t } = useTranslation("draws");
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">版本 v{batch.result_version}</CardTitle>
|
||||
<CardTitle className="text-base">{t("version", { version: batch.result_version })}</CardTitle>
|
||||
<p className="font-mono text-xs text-muted-foreground">
|
||||
生成方式 {batch.source_type === "manual" ? "人工录入" : "RNG"} · RNG 摘要 {batch.rng_seed_hash ?? "—"} · 确认时间 {batch.confirmed_at ?? "—"}
|
||||
{t("sourceType", {
|
||||
source: batch.source_type === "manual" ? t("manualEntry") : t("rng"),
|
||||
})}{" "}
|
||||
· {t("rngSummary", { hash: batch.rng_seed_hash ?? "—" })} ·{" "}
|
||||
{t("confirmedAt", { time: batch.confirmed_at ?? "—" })}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="overflow-x-auto pt-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>奖项</TableHead>
|
||||
<TableHead>{t("prize")}</TableHead>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead className="font-mono">4D</TableHead>
|
||||
<TableHead className="hidden sm:table-cell">尾3</TableHead>
|
||||
<TableHead className="hidden sm:table-cell">尾2</TableHead>
|
||||
<TableHead className="hidden md:table-cell">头/尾</TableHead>
|
||||
<TableHead className="hidden sm:table-cell">{t("tail3")}</TableHead>
|
||||
<TableHead className="hidden sm:table-cell">{t("tail2")}</TableHead>
|
||||
<TableHead className="hidden md:table-cell">{t("headTail")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { getAdminDrawResultBatches, postAdminCreateManualResultBatch } from "@/api/admin-draws";
|
||||
@@ -26,22 +27,25 @@ import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
|
||||
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: "三奖" },
|
||||
{ prize_type: "first", prize_index: 0, label: "resultSlots.first" },
|
||||
{ prize_type: "second", prize_index: 0, label: "resultSlots.second" },
|
||||
{ prize_type: "third", prize_index: 0, label: "resultSlots.third" },
|
||||
...Array.from({ length: 10 }, (_, i) => ({
|
||||
prize_type: "starter",
|
||||
prize_index: i,
|
||||
label: `特别奖 ${i + 1}`,
|
||||
label: `resultSlots.starter`,
|
||||
labelIndex: i + 1,
|
||||
})),
|
||||
...Array.from({ length: 10 }, (_, i) => ({
|
||||
prize_type: "consolation",
|
||||
prize_index: i,
|
||||
label: `安慰奖 ${i + 1}`,
|
||||
label: `resultSlots.consolation`,
|
||||
labelIndex: i + 1,
|
||||
})),
|
||||
] as const;
|
||||
|
||||
export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
||||
const { t } = useTranslation(["draws", "common"]);
|
||||
const profile = useAdminProfile();
|
||||
const canManageDraw = adminHasAnyPermission(profile?.permissions, [
|
||||
PRD_DRAW_RESULT_MANAGE,
|
||||
@@ -57,7 +61,7 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!Number.isFinite(idNum)) {
|
||||
setError("无效的期号 ID");
|
||||
setError(t("invalidDrawId"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -67,11 +71,11 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
||||
setData(await getAdminDrawResultBatches(idNum));
|
||||
} catch (e) {
|
||||
setData(null);
|
||||
setError(e instanceof LotteryApiBizError ? e.message : "加载失败");
|
||||
setError(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [idNum]);
|
||||
}, [idNum, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
@@ -88,7 +92,7 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
||||
if (!Number.isFinite(idNum)) return;
|
||||
const invalid = manualNumbers.some((n) => !/^[0-9]{4}$/.test(n));
|
||||
if (invalid) {
|
||||
toast.error("请完整输入 23 组 4 位数字");
|
||||
toast.error(t("enter23Numbers"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -101,38 +105,46 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
||||
number_4d: manualNumbers[i],
|
||||
})),
|
||||
});
|
||||
toast.success(`已保存草稿 v${res.batch.result_version},等待确认发布`);
|
||||
toast.success(t("draftSaved", { version: res.batch.result_version }));
|
||||
setManualNumbers(RESULT_SLOTS.map(() => ""));
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "保存失败");
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("saveFailed"));
|
||||
} finally {
|
||||
setSavingManual(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading && !data) {
|
||||
return <p className="text-sm text-muted-foreground">加载中…</p>;
|
||||
return <p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>;
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return <p className="text-sm text-destructive">{error ?? "无数据"}</p>;
|
||||
return <p className="text-sm text-destructive">{error ?? t("states.noData", { ns: "common" })}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">人工录入开奖结果</CardTitle>
|
||||
<CardTitle className="text-lg">{t("manualResultEntry")}</CardTitle>
|
||||
<p className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||
当前状态 <DrawStatusBadge status={data.draw_status} /> · 保存后生成待确认批次,不会直接发布
|
||||
{t("currentStatusAndDraft", {
|
||||
status: data.draw_status,
|
||||
}).split(data.draw_status)[0]}
|
||||
<DrawStatusBadge status={data.draw_status} />
|
||||
{t("currentStatusAndDraft", {
|
||||
status: data.draw_status,
|
||||
}).split(data.draw_status)[1] ?? ""}
|
||||
</p>
|
||||
</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>
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{t(slot.label, { index: "labelIndex" in slot ? slot.labelIndex : undefined })}
|
||||
</span>
|
||||
<Input
|
||||
inputMode="numeric"
|
||||
maxLength={4}
|
||||
@@ -155,14 +167,14 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
||||
disabled={savingManual}
|
||||
onClick={() => setManualNumbers(RESULT_SLOTS.map(() => ""))}
|
||||
>
|
||||
清空
|
||||
{t("clear")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!canManageDraw || savingManual || !["closed", "review"].includes(data.draw_status)}
|
||||
onClick={() => void saveManualDraft()}
|
||||
>
|
||||
{savingManual ? "保存中…" : "保存草稿"}
|
||||
{savingManual ? t("saving") : t("saveDraft")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -170,21 +182,21 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">待确认批次</CardTitle>
|
||||
<CardTitle className="text-lg">{t("pendingBatches")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{pending.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-6 text-center">
|
||||
当前没有待审核(pending_review)批次。
|
||||
{t("noPendingBatches")}
|
||||
</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>批次 ID</TableHead>
|
||||
<TableHead>版本</TableHead>
|
||||
<TableHead>号码条数</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
<TableHead>{t("batchId")}</TableHead>
|
||||
<TableHead>{t("version", { version: "" }).replace(" v", "").trim()}</TableHead>
|
||||
<TableHead>{t("numberCount")}</TableHead>
|
||||
<TableHead className="text-right">{t("actions")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -199,10 +211,10 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
|
||||
href={`/admin/draws/${drawId}/publish/${b.id}`}
|
||||
className={cn(buttonVariants({ size: "sm" }))}
|
||||
>
|
||||
核对并发布
|
||||
{t("reviewAndPublishAction")}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">无发布权限</span>
|
||||
<span className="text-xs text-muted-foreground">{t("noPublishPermission")}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const segments = [
|
||||
{ suffix: "", key: "status", label: "期号状态" },
|
||||
{ suffix: "/results", key: "results", label: "开奖结果" },
|
||||
{ suffix: "/finance", key: "finance", label: "期号收支" },
|
||||
{ suffix: "/review", key: "review", label: "审核与发布" },
|
||||
{ suffix: "", key: "status", label: "subnav.status" },
|
||||
{ suffix: "/results", key: "results", label: "subnav.results" },
|
||||
{ suffix: "/finance", key: "finance", label: "subnav.finance" },
|
||||
{ suffix: "/review", key: "review", label: "subnav.review" },
|
||||
] as const;
|
||||
|
||||
function isReviewTabActive(pathname: string, base: string): boolean {
|
||||
@@ -24,6 +25,7 @@ function isReviewTabActive(pathname: string, base: string): boolean {
|
||||
}
|
||||
|
||||
export function DrawSubnav({ drawId }: { drawId: string }) {
|
||||
const { t } = useTranslation("draws");
|
||||
const pathname = usePathname();
|
||||
const base = `/admin/draws/${drawId}`;
|
||||
|
||||
@@ -46,7 +48,7 @@ export function DrawSubnav({ drawId }: { drawId: string }) {
|
||||
buttonVariants({ variant: active ? "default" : "outline", size: "sm" }),
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{t(label)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { getAdminDraws, postAdminGenerateDrawPlan } from "@/api/admin-draws";
|
||||
@@ -37,27 +38,29 @@ const DRAW_FILTER_ALL = "__all__";
|
||||
|
||||
/** 与 {@see App\Lottery\DrawStatus} 一致 */
|
||||
const DRAW_STATUS_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: "pending", label: "未开始" },
|
||||
{ value: "open", label: "可下注" },
|
||||
{ value: "closing", label: "封盘中" },
|
||||
{ value: "closed", label: "已封盘待开奖" },
|
||||
{ value: "drawing", label: "开奖处理中" },
|
||||
{ value: "review", label: "待人工审核" },
|
||||
{ value: "cooldown", label: "冷静期" },
|
||||
{ value: "settling", label: "结算处理中" },
|
||||
{ value: "settled", label: "已结算" },
|
||||
{ value: "cancelled", label: "已取消" },
|
||||
{ value: "pending", label: "statusOptions.pending" },
|
||||
{ value: "open", label: "statusOptions.open" },
|
||||
{ value: "closing", label: "statusOptions.closing" },
|
||||
{ value: "closed", label: "statusOptions.closed" },
|
||||
{ value: "drawing", label: "statusOptions.drawing" },
|
||||
{ value: "review", label: "statusOptions.review" },
|
||||
{ value: "cooldown", label: "statusOptions.cooldown" },
|
||||
{ value: "settling", label: "statusOptions.settling" },
|
||||
{ value: "settled", label: "statusOptions.settled" },
|
||||
{ value: "cancelled", label: "statusOptions.cancelled" },
|
||||
];
|
||||
|
||||
function drawAdminStatusSelectLabel(raw: unknown): string {
|
||||
function drawAdminStatusSelectLabel(raw: unknown, t: (key: string) => string): string {
|
||||
const v = raw == null ? "" : String(raw);
|
||||
if (v === "" || v === DRAW_FILTER_ALL) {
|
||||
return "不限";
|
||||
return t("statusOptions.all");
|
||||
}
|
||||
return DRAW_STATUS_OPTIONS.find((o) => o.value === v)?.label ?? v;
|
||||
const key = DRAW_STATUS_OPTIONS.find((o) => o.value === v)?.label;
|
||||
return key ? t(key) : v;
|
||||
}
|
||||
|
||||
export function DrawsIndexConsole() {
|
||||
const { t } = useTranslation(["draws", "common"]);
|
||||
const formatDt = useAdminDateTimeFormatter();
|
||||
const [data, setData] = useState<AdminDrawListData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -76,8 +79,9 @@ export function DrawsIndexConsole() {
|
||||
draftStatus === "" || !DRAW_STATUS_OPTIONS.some((o) => o.value === draftStatus)
|
||||
? DRAW_FILTER_ALL
|
||||
: draftStatus,
|
||||
t,
|
||||
),
|
||||
[draftStatus],
|
||||
[draftStatus, t],
|
||||
);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
@@ -96,22 +100,28 @@ export function DrawsIndexConsole() {
|
||||
setData(d);
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e instanceof LotteryApiBizError ? e.message : "加载失败,请检查登录与 API 配置";
|
||||
e instanceof LotteryApiBizError ? e.message : t("loadFailed");
|
||||
setError(msg);
|
||||
setData(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page, perPage, appliedDrawNo, appliedStatus]);
|
||||
}, [page, perPage, appliedDrawNo, appliedStatus, t]);
|
||||
|
||||
async function generatePlan(): Promise<void> {
|
||||
setGenerating(true);
|
||||
try {
|
||||
const res = await postAdminGenerateDrawPlan();
|
||||
toast.success(`已生成 ${res.created} 期,当前缓冲 ${res.upcoming}/${res.buffer_target}`);
|
||||
toast.success(
|
||||
t("generateSuccess", {
|
||||
created: res.created,
|
||||
upcoming: res.upcoming,
|
||||
target: res.buffer_target,
|
||||
}),
|
||||
);
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "生成失败");
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("generateFailed"));
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
@@ -127,9 +137,9 @@ export function DrawsIndexConsole() {
|
||||
return (
|
||||
<Card>
|
||||
<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">{t("statusListTitle")}</CardTitle>
|
||||
<Button type="button" onClick={() => void generatePlan()} disabled={generating}>
|
||||
{generating ? "生成中…" : "批量生成期开奖计划"}
|
||||
{generating ? t("generating") : t("generatePlan")}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -138,17 +148,17 @@ export function DrawsIndexConsole() {
|
||||
className="grid max-w-full gap-x-6 gap-y-3 sm:grid-cols-[minmax(0,12rem)_minmax(0,11rem)_auto] sm:gap-y-1.5"
|
||||
>
|
||||
<Label htmlFor="draw-filter-no">
|
||||
期号
|
||||
{t("drawNo")}
|
||||
</Label>
|
||||
<Input
|
||||
id="draw-filter-no"
|
||||
placeholder="模糊匹配期号"
|
||||
placeholder={t("fuzzyDrawNo")}
|
||||
value={draftDrawNo}
|
||||
className="w-full min-w-0 sm:w-full"
|
||||
onChange={(e) => setDraftDrawNo(e.target.value)}
|
||||
/>
|
||||
<Label htmlFor="draw-filter-status">
|
||||
状态
|
||||
{t("status")}
|
||||
</Label>
|
||||
<div className="min-w-0">
|
||||
<Select
|
||||
@@ -167,10 +177,10 @@ export function DrawsIndexConsole() {
|
||||
<SelectValue>{drawStatusTriggerLabel}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent align="start" sideOffset={6}>
|
||||
<SelectItem value={DRAW_FILTER_ALL}>不限</SelectItem>
|
||||
<SelectItem value={DRAW_FILTER_ALL}>{t("statusOptions.all")}</SelectItem>
|
||||
{DRAW_STATUS_OPTIONS.map((o) => (
|
||||
<SelectItem key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
{t(o.label)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -185,7 +195,7 @@ export function DrawsIndexConsole() {
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
查询期号
|
||||
{t("queryDraw")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -198,7 +208,7 @@ export function DrawsIndexConsole() {
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
重置
|
||||
{t("reset")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -211,28 +221,28 @@ export function DrawsIndexConsole() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<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>{t("drawNo")}</TableHead>
|
||||
<TableHead>{t("startTime")}</TableHead>
|
||||
<TableHead>{t("closeTime")}</TableHead>
|
||||
<TableHead>{t("drawTime")}</TableHead>
|
||||
<TableHead>{t("status")}</TableHead>
|
||||
<TableHead className="text-right">{t("betTotal")}</TableHead>
|
||||
<TableHead className="text-right">{t("payoutTotal")}</TableHead>
|
||||
<TableHead className="text-right">{t("profitLoss")}</TableHead>
|
||||
<TableHead className="text-right">{t("actions")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-muted-foreground">
|
||||
加载中…
|
||||
{t("states.loading", { ns: "common" })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : data === null || data.items.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-muted-foreground">
|
||||
暂无数据
|
||||
{t("states.noData", { ns: "common" })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
@@ -264,7 +274,7 @@ export function DrawsIndexConsole() {
|
||||
href={`/admin/draws/${row.id}`}
|
||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
|
||||
>
|
||||
查看详情
|
||||
{t("viewDetails")}
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const drawsModuleMeta = {
|
||||
segment: "draws",
|
||||
title: "期号列表",
|
||||
title: "Draws",
|
||||
description: "",
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user