feat(抽奖): 后台"抽奖记录"页 — 中奖/发放流水列表
- draws-panel(新): ProTable 列出中奖人/奖品/发放状态/发放结果,按 是否中奖/发放状态/用户ID 过滤 - activity-detail: 新增"抽奖记录"Tab - service+types: getLotteryDrawList + AdminLotteryDraw - i18n: draw.* zh/en Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,8 @@
|
|||||||
"detail": {
|
"detail": {
|
||||||
"prizesTab": "Prizes",
|
"prizesTab": "Prizes",
|
||||||
"rulesTab": "Rules",
|
"rulesTab": "Rules",
|
||||||
"claimsTab": "Claims"
|
"claimsTab": "Claims",
|
||||||
|
"drawsTab": "Draw records"
|
||||||
},
|
},
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"editActivity": "Edit Activity",
|
"editActivity": "Edit Activity",
|
||||||
@@ -245,5 +246,24 @@
|
|||||||
"txHash": "Tx hash",
|
"txHash": "Tx hash",
|
||||||
"txHashRequired": "Tx hash is required for crypto",
|
"txHashRequired": "Tx hash is required for crypto",
|
||||||
"user": "User"
|
"user": "User"
|
||||||
|
},
|
||||||
|
"draw": {
|
||||||
|
"dispatchAutoClaimed": "Auto-granted",
|
||||||
|
"dispatchExpired": "Expired",
|
||||||
|
"dispatchFailed": "Failed",
|
||||||
|
"dispatchNone": "Pending",
|
||||||
|
"dispatchPaid": "Fulfilled",
|
||||||
|
"dispatchPendingClaim": "Awaiting claim",
|
||||||
|
"dispatchState": "Dispatch",
|
||||||
|
"drawnAt": "Drawn at",
|
||||||
|
"filterDispatch": "Dispatch",
|
||||||
|
"filterUserId": "User ID",
|
||||||
|
"filterWin": "Won?",
|
||||||
|
"grant": "Grant result",
|
||||||
|
"loseOnly": "Not won",
|
||||||
|
"prize": "Prize",
|
||||||
|
"tableTitle": "Draw records",
|
||||||
|
"user": "User",
|
||||||
|
"winOnly": "Won"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"detail": {
|
"detail": {
|
||||||
"prizesTab": "奖品",
|
"prizesTab": "奖品",
|
||||||
"rulesTab": "规则",
|
"rulesTab": "规则",
|
||||||
"claimsTab": "领奖工单"
|
"claimsTab": "领奖工单",
|
||||||
|
"drawsTab": "抽奖记录"
|
||||||
},
|
},
|
||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
"editActivity": "编辑活动",
|
"editActivity": "编辑活动",
|
||||||
@@ -245,5 +246,24 @@
|
|||||||
"txHash": "交易哈希",
|
"txHash": "交易哈希",
|
||||||
"txHashRequired": "加密货币奖品必须填写交易哈希",
|
"txHashRequired": "加密货币奖品必须填写交易哈希",
|
||||||
"user": "用户"
|
"user": "用户"
|
||||||
|
},
|
||||||
|
"draw": {
|
||||||
|
"dispatchAutoClaimed": "已自动发放",
|
||||||
|
"dispatchExpired": "已过期",
|
||||||
|
"dispatchFailed": "发放失败",
|
||||||
|
"dispatchNone": "待发放",
|
||||||
|
"dispatchPaid": "已发放",
|
||||||
|
"dispatchPendingClaim": "待领取",
|
||||||
|
"dispatchState": "发放状态",
|
||||||
|
"drawnAt": "抽奖时间",
|
||||||
|
"filterDispatch": "发放状态",
|
||||||
|
"filterUserId": "用户 ID",
|
||||||
|
"filterWin": "是否中奖",
|
||||||
|
"grant": "发放结果",
|
||||||
|
"loseOnly": "未中奖",
|
||||||
|
"prize": "奖品",
|
||||||
|
"tableTitle": "抽奖记录",
|
||||||
|
"user": "用户",
|
||||||
|
"winOnly": "已中奖"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { getLotteryActivityDetail } from "@workspace/ui/services/admin/lottery";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import ClaimsPanel from "./claims-panel";
|
import ClaimsPanel from "./claims-panel";
|
||||||
|
import DrawsPanel from "./draws-panel";
|
||||||
import PrizeGrid from "./prize-grid";
|
import PrizeGrid from "./prize-grid";
|
||||||
import RuleEditor from "./rule-editor";
|
import RuleEditor from "./rule-editor";
|
||||||
|
|
||||||
@@ -106,6 +107,9 @@ export default function ActivityDetail({
|
|||||||
<TabsTrigger value="claims">
|
<TabsTrigger value="claims">
|
||||||
{t("detail.claimsTab", "Claims")}
|
{t("detail.claimsTab", "Claims")}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="draws">
|
||||||
|
{t("detail.drawsTab", "Draw records")}
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="prizes">
|
<TabsContent value="prizes">
|
||||||
<PrizeGrid activity={current} />
|
<PrizeGrid activity={current} />
|
||||||
@@ -116,6 +120,9 @@ export default function ActivityDetail({
|
|||||||
<TabsContent value="claims">
|
<TabsContent value="claims">
|
||||||
<ClaimsPanel activity={current} />
|
<ClaimsPanel activity={current} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="draws">
|
||||||
|
<DrawsPanel activity={current} />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import { Badge } from "@workspace/ui/components/badge";
|
||||||
|
import {
|
||||||
|
ProTable,
|
||||||
|
type ProTableActions,
|
||||||
|
} from "@workspace/ui/composed/pro-table/pro-table";
|
||||||
|
import { getLotteryDrawList } from "@workspace/ui/services/admin/lottery";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { formatDate } from "@/utils/common";
|
||||||
|
|
||||||
|
interface DrawsPanelProps {
|
||||||
|
activity: API.LotteryActivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TranslateFn = (key: string, defaultValue: string) => string;
|
||||||
|
|
||||||
|
const DISPATCH_META: Record<
|
||||||
|
string,
|
||||||
|
{ labelKey: string; labelDefault: string; className: string }
|
||||||
|
> = {
|
||||||
|
none: {
|
||||||
|
labelKey: "draw.dispatchNone",
|
||||||
|
labelDefault: "Pending",
|
||||||
|
className: "bg-gray-100 text-gray-500 border-gray-200",
|
||||||
|
},
|
||||||
|
auto_claimed: {
|
||||||
|
labelKey: "draw.dispatchAutoClaimed",
|
||||||
|
labelDefault: "Auto-granted",
|
||||||
|
className: "bg-emerald-100 text-emerald-700 border-emerald-200",
|
||||||
|
},
|
||||||
|
pending_claim: {
|
||||||
|
labelKey: "draw.dispatchPendingClaim",
|
||||||
|
labelDefault: "Awaiting claim",
|
||||||
|
className: "bg-amber-100 text-amber-700 border-amber-200",
|
||||||
|
},
|
||||||
|
paid: {
|
||||||
|
labelKey: "draw.dispatchPaid",
|
||||||
|
labelDefault: "Fulfilled",
|
||||||
|
className: "bg-emerald-100 text-emerald-700 border-emerald-200",
|
||||||
|
},
|
||||||
|
expired: {
|
||||||
|
labelKey: "draw.dispatchExpired",
|
||||||
|
labelDefault: "Expired",
|
||||||
|
className: "bg-gray-100 text-gray-500 border-gray-200",
|
||||||
|
},
|
||||||
|
failed: {
|
||||||
|
labelKey: "draw.dispatchFailed",
|
||||||
|
labelDefault: "Failed",
|
||||||
|
className: "bg-rose-100 text-rose-700 border-rose-200",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function prizeTypeLabel(type: API.LotteryPrizeType, t: TranslateFn): string {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
vpn_duration: t("prize.typeVpnDuration", "VPN Duration"),
|
||||||
|
commission: t("prize.typeCommission", "Commission"),
|
||||||
|
none: t("prize.typeNone", "No Prize (Thanks)"),
|
||||||
|
crypto: t("prize.typeCrypto", "Crypto"),
|
||||||
|
physical: t("prize.typePhysical", "Physical"),
|
||||||
|
manual_other: t("prize.typeManualOther", "Manual (Other)"),
|
||||||
|
};
|
||||||
|
return labels[type] ?? type;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DrawsPanel({ activity }: DrawsPanelProps) {
|
||||||
|
const { t } = useTranslation("lottery");
|
||||||
|
const ref = useRef<ProTableActions>(null);
|
||||||
|
|
||||||
|
const dispatchOptions = Object.entries(DISPATCH_META).map(
|
||||||
|
([value, meta]) => ({
|
||||||
|
value,
|
||||||
|
label: t(meta.labelKey, meta.labelDefault),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const winOptions = [
|
||||||
|
{ value: "1", label: t("draw.winOnly", "Won") },
|
||||||
|
{ value: "0", label: t("draw.loseOnly", "Not won") },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProTable<
|
||||||
|
API.AdminLotteryDraw,
|
||||||
|
{ win?: string; dispatch_state?: string; user_id?: string }
|
||||||
|
>
|
||||||
|
action={ref}
|
||||||
|
columns={[
|
||||||
|
{ accessorKey: "draw_id", header: "ID" },
|
||||||
|
{
|
||||||
|
accessorKey: "user",
|
||||||
|
header: t("draw.user", "User"),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-sm">
|
||||||
|
<div>#{row.original.user.id}</div>
|
||||||
|
{row.original.user.email && (
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
{row.original.user.email}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "prize",
|
||||||
|
header: t("draw.prize", "Prize"),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const prize = row.original.prize;
|
||||||
|
if (!(row.original.is_win && prize)) {
|
||||||
|
return (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("prize.typeNone", "No Prize (Thanks)")}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="font-medium">{prize.name}</div>
|
||||||
|
<Badge className="mt-0.5" variant="secondary">
|
||||||
|
{prizeTypeLabel(prize.type, t)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "dispatch_state",
|
||||||
|
header: t("draw.dispatchState", "Dispatch"),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const meta = DISPATCH_META[row.original.dispatch_state];
|
||||||
|
return (
|
||||||
|
<Badge className={meta?.className} variant="outline">
|
||||||
|
{meta
|
||||||
|
? t(meta.labelKey, meta.labelDefault)
|
||||||
|
: row.original.dispatch_state}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "grant_message",
|
||||||
|
header: t("draw.grant", "Grant result"),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="max-w-[220px] break-all text-xs">
|
||||||
|
{row.original.grant_message || "--"}
|
||||||
|
{row.original.dispatch_error && (
|
||||||
|
<div className="text-rose-600">
|
||||||
|
{row.original.dispatch_error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "drawn_at",
|
||||||
|
header: t("draw.drawnAt", "Drawn at"),
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.original.drawn_at ? formatDate(row.original.drawn_at) : "--",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
header={{ title: t("draw.tableTitle", "Draw records") }}
|
||||||
|
params={[
|
||||||
|
{
|
||||||
|
key: "win",
|
||||||
|
options: winOptions,
|
||||||
|
placeholder: t("draw.filterWin", "Won?"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "dispatch_state",
|
||||||
|
options: dispatchOptions,
|
||||||
|
placeholder: t("draw.filterDispatch", "Dispatch"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "user_id",
|
||||||
|
placeholder: t("draw.filterUserId", "User ID"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
request={async (pagination, filter) => {
|
||||||
|
const { data } = await getLotteryDrawList({
|
||||||
|
page: pagination.page,
|
||||||
|
size: pagination.size,
|
||||||
|
activity_id: activity.id,
|
||||||
|
win: (filter.win as "1" | "0") || undefined,
|
||||||
|
dispatch_state: filter.dispatch_state || undefined,
|
||||||
|
user_id: filter.user_id ? Number(filter.user_id) : undefined,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
list: data.data?.list || [],
|
||||||
|
total: data.data?.total || 0,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -373,3 +373,21 @@ export async function markPaidLotteryClaim(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽奖记录(发放流水)
|
||||||
|
* GET /v1/admin/lottery/draws
|
||||||
|
*/
|
||||||
|
export async function getLotteryDrawList(
|
||||||
|
params: API.GetLotteryDrawListParams,
|
||||||
|
options?: { [key: string]: any }
|
||||||
|
) {
|
||||||
|
return request<API.Response & { data?: API.ListLotteryDrawsResponse }>(
|
||||||
|
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/lottery/draws`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
params: { ...params },
|
||||||
|
...(options || {}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
+46
@@ -3496,4 +3496,50 @@ declare namespace API {
|
|||||||
delivery_ref?: string;
|
delivery_ref?: string;
|
||||||
paid_at?: number;
|
paid_at?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- Stage 3 抽奖记录(发放流水)-----------------------------------------
|
||||||
|
|
||||||
|
type GetLotteryDrawListParams = {
|
||||||
|
activity_id?: number;
|
||||||
|
user_id?: number;
|
||||||
|
prize_type?: LotteryPrizeType;
|
||||||
|
dispatch_state?: string;
|
||||||
|
win?: "1" | "0";
|
||||||
|
from?: number;
|
||||||
|
to?: number;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AdminLotteryDrawUser = {
|
||||||
|
id: number;
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AdminLotteryDrawPrize = {
|
||||||
|
slot: number;
|
||||||
|
type: LotteryPrizeType;
|
||||||
|
name: string;
|
||||||
|
config: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AdminLotteryDraw = {
|
||||||
|
draw_id: number;
|
||||||
|
activity_id: number;
|
||||||
|
user: AdminLotteryDrawUser;
|
||||||
|
is_win: boolean;
|
||||||
|
prize?: AdminLotteryDrawPrize;
|
||||||
|
dispatch_state: string;
|
||||||
|
dispatch_error?: string;
|
||||||
|
grant_amount?: number;
|
||||||
|
grant_message?: string;
|
||||||
|
drawn_at: number;
|
||||||
|
dispatched_at?: number;
|
||||||
|
created_at: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ListLotteryDrawsResponse = {
|
||||||
|
total: number;
|
||||||
|
list: AdminLotteryDraw[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user