feat(api, ui, i18n): 增强奖池管理与钱包功能
新增奖池余额调整与调整记录查询相关 API,提升后台对奖池的管理与控制能力。 更新奖池与钱包相关多语言文案,新增余额调整与转账完成提示信息,提升用户理解与反馈体验。 优化奖池管理相关 UI 组件,新增余额调整功能并改进页面布局,提升操作易用性。 重构相关组件以整合新功能,并进一步优化后台管理界面的整体用户体验。
This commit is contained in:
@@ -4,8 +4,10 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
getAdminJackpotPoolAdjustments,
|
||||
getAdminJackpotPools,
|
||||
postAdminJackpotManualBurst,
|
||||
postAdminJackpotPoolAdjustment,
|
||||
putAdminJackpotPool,
|
||||
} from "@/api/admin-jackpot";
|
||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||
@@ -29,10 +31,17 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type { AdminJackpotPoolRow } from "@/types/api/admin-jackpot";
|
||||
import type { AdminJackpotPoolAdjustmentRow, AdminJackpotPoolRow } from "@/types/api/admin-jackpot";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
type Draft = {
|
||||
current_amount: string;
|
||||
contribution_rate: string;
|
||||
trigger_threshold: string;
|
||||
payout_rate: string;
|
||||
@@ -43,9 +52,14 @@ type Draft = {
|
||||
manual_burst_draw_id: string;
|
||||
};
|
||||
|
||||
type AdjustmentDraft = {
|
||||
direction: "increase" | "decrease";
|
||||
amount: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
function toDraft(p: AdminJackpotPoolRow): Draft {
|
||||
return {
|
||||
current_amount: formatAdminMinorDecimal(p.current_amount, p.currency_code),
|
||||
contribution_rate: String(p.contribution_rate),
|
||||
trigger_threshold: formatAdminMinorDecimal(p.trigger_threshold, p.currency_code),
|
||||
payout_rate: String(p.payout_rate),
|
||||
@@ -74,6 +88,9 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
||||
const [savingId, setSavingId] = useState<number | null>(null);
|
||||
const [burstingId, setBurstingId] = useState<number | null>(null);
|
||||
const [confirmBurstPoolId, setConfirmBurstPoolId] = useState<number | null>(null);
|
||||
const [adjustmentDrafts, setAdjustmentDrafts] = useState<Record<number, AdjustmentDraft>>({});
|
||||
const [adjustmentRows, setAdjustmentRows] = useState<Record<number, AdminJackpotPoolAdjustmentRow[]>>({});
|
||||
const [adjustingId, setAdjustingId] = useState<number | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -81,10 +98,21 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
||||
const res = await getAdminJackpotPools();
|
||||
setItems(res.items);
|
||||
const d: Record<number, Draft> = {};
|
||||
const adjDrafts: Record<number, AdjustmentDraft> = {};
|
||||
const adjRows: Record<number, AdminJackpotPoolAdjustmentRow[]> = {};
|
||||
for (const p of res.items) {
|
||||
d[p.id] = toDraft(p);
|
||||
adjDrafts[p.id] = { direction: "increase", amount: "", reason: "" };
|
||||
try {
|
||||
const ledger = await getAdminJackpotPoolAdjustments(p.id, { per_page: 5 });
|
||||
adjRows[p.id] = ledger.items;
|
||||
} catch {
|
||||
adjRows[p.id] = [];
|
||||
}
|
||||
}
|
||||
setDrafts(d);
|
||||
setAdjustmentDrafts(adjDrafts);
|
||||
setAdjustmentRows(adjRows);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("loadFailed"));
|
||||
} finally {
|
||||
@@ -105,13 +133,19 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
||||
}));
|
||||
};
|
||||
|
||||
const updateAdjustmentDraft = (id: number, patch: Partial<AdjustmentDraft>) => {
|
||||
setAdjustmentDrafts((prev) => ({
|
||||
...prev,
|
||||
[id]: { ...(prev[id] ?? { direction: "increase", amount: "", reason: "" }), ...patch },
|
||||
}));
|
||||
};
|
||||
|
||||
const save = async (p: AdminJackpotPoolRow) => {
|
||||
const d = drafts[p.id];
|
||||
if (!d) return;
|
||||
setSavingId(p.id);
|
||||
try {
|
||||
await putAdminJackpotPool(p.id, {
|
||||
current_amount: parseAdminMajorToMinor(d.current_amount, p.currency_code) ?? 0,
|
||||
contribution_rate: Number(d.contribution_rate),
|
||||
trigger_threshold: parseAdminMajorToMinor(d.trigger_threshold, p.currency_code) ?? 0,
|
||||
payout_rate: Number(d.payout_rate),
|
||||
@@ -132,6 +166,43 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
||||
}
|
||||
};
|
||||
|
||||
const submitAdjustment = async (p: AdminJackpotPoolRow) => {
|
||||
const adj = adjustmentDrafts[p.id];
|
||||
if (!adj) return;
|
||||
const minor = parseAdminMajorToMinor(adj.amount, p.currency_code);
|
||||
if (minor === null || minor <= 0) {
|
||||
toast.error(t("adjustmentAmountInvalid"));
|
||||
return;
|
||||
}
|
||||
const trimmedReason = adj.reason.trim();
|
||||
if (trimmedReason.length < 3) {
|
||||
toast.error(t("adjustmentReasonRequired"));
|
||||
return;
|
||||
}
|
||||
const amountDelta = adj.direction === "decrease" ? -minor : minor;
|
||||
|
||||
setAdjustingId(p.id);
|
||||
try {
|
||||
const res = await postAdminJackpotPoolAdjustment(p.id, {
|
||||
amount_delta: amountDelta,
|
||||
reason: trimmedReason,
|
||||
});
|
||||
toast.success(t("adjustmentSuccess"));
|
||||
setItems((prev) =>
|
||||
prev.map((row) =>
|
||||
row.id === p.id ? { ...row, current_amount: res.pool.current_amount } : row,
|
||||
),
|
||||
);
|
||||
updateAdjustmentDraft(p.id, { amount: "", reason: "" });
|
||||
const ledger = await getAdminJackpotPoolAdjustments(p.id, { per_page: 5 });
|
||||
setAdjustmentRows((prev) => ({ ...prev, [p.id]: ledger.items }));
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("adjustmentFailed"));
|
||||
} finally {
|
||||
setAdjustingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const manualBurst = async (p: AdminJackpotPoolRow) => {
|
||||
const d = drafts[p.id];
|
||||
if (!d) return;
|
||||
@@ -164,22 +235,96 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
||||
) : null}
|
||||
{items.map((p) => {
|
||||
const d = drafts[p.id] ?? toDraft(p);
|
||||
const adj = adjustmentDrafts[p.id] ?? { direction: "increase", amount: "", reason: "" };
|
||||
const ledger = adjustmentRows[p.id] ?? [];
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className="space-y-4 rounded-xl border border-border/60 bg-muted/10 p-4"
|
||||
>
|
||||
<h3 className="font-mono text-sm font-semibold">{p.currency_code}</h3>
|
||||
<fieldset disabled={!canManageJackpot} className="grid gap-4 border-0 p-0 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={`amt-${p.id}`}>{t("currentAmount")}</Label>
|
||||
<Input
|
||||
id={`amt-${p.id}`}
|
||||
className="font-mono"
|
||||
value={d.current_amount}
|
||||
onChange={(e) => updateDraft(p.id, { current_amount: e.target.value })}
|
||||
/>
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<h3 className="font-mono text-sm font-semibold">{p.currency_code}</h3>
|
||||
<p className="text-muted-foreground font-mono text-xs">
|
||||
{t("displayBalance", {
|
||||
amount: formatAdminMinorDecimal(p.current_amount, p.currency_code),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
{canManageJackpot ? (
|
||||
<div className="space-y-3 rounded-lg border border-border/60 bg-background/60 p-4">
|
||||
<p className="text-sm font-medium">{t("balanceAdjustmentTitle")}</p>
|
||||
<p className="text-muted-foreground text-xs">{t("balanceAdjustmentHint")}</p>
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label>{t("adjustmentDirection")}</Label>
|
||||
<Select
|
||||
value={adj.direction}
|
||||
onValueChange={(value: "increase" | "decrease") =>
|
||||
updateAdjustmentDraft(p.id, { direction: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="font-mono">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="increase">{t("adjustmentIncrease")}</SelectItem>
|
||||
<SelectItem value="decrease">{t("adjustmentDecrease")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={`adj-amt-${p.id}`}>{t("adjustmentAmount")}</Label>
|
||||
<Input
|
||||
id={`adj-amt-${p.id}`}
|
||||
className="font-mono"
|
||||
value={adj.amount}
|
||||
onChange={(e) => updateAdjustmentDraft(p.id, { amount: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5 sm:col-span-2 xl:col-span-2">
|
||||
<Label htmlFor={`adj-reason-${p.id}`}>{t("adjustmentReason")}</Label>
|
||||
<Textarea
|
||||
id={`adj-reason-${p.id}`}
|
||||
rows={2}
|
||||
value={adj.reason}
|
||||
onChange={(e) => updateAdjustmentDraft(p.id, { reason: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={adjustingId === p.id}
|
||||
onClick={() =>
|
||||
requestConfirm({
|
||||
title: t("confirmAdjustmentTitle"),
|
||||
description: t("confirmAdjustmentDescription"),
|
||||
confirmLabel: t("confirm.confirmSave", { ns: "common" }),
|
||||
onConfirm: () => submitAdjustment(p),
|
||||
})
|
||||
}
|
||||
>
|
||||
{adjustingId === p.id ? t("processing") : t("submitAdjustment")}
|
||||
</Button>
|
||||
</div>
|
||||
{ledger.length > 0 ? (
|
||||
<div className="space-y-2 border-t border-border/60 pt-3">
|
||||
<p className="text-xs font-medium">{t("recentAdjustments")}</p>
|
||||
<ul className="text-muted-foreground space-y-1 font-mono text-xs">
|
||||
{ledger.map((row) => (
|
||||
<li key={row.id}>
|
||||
{row.adjustment_no} · {row.amount_delta > 0 ? "+" : ""}
|
||||
{formatAdminMinorDecimal(row.amount_delta, p.currency_code)} ·{" "}
|
||||
{row.reason}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<fieldset disabled={!canManageJackpot} className="grid gap-4 border-0 p-0 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={`cr-${p.id}`}>{t("contributionRate")}</Label>
|
||||
<Input
|
||||
|
||||
Reference in New Issue
Block a user