From 508cfa91bab436b9c68acbdef12b89b561324ef6 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Thu, 16 Jul 2026 23:41:47 -0700 Subject: [PATCH] =?UTF-8?q?feat(=E6=8A=BD=E5=A5=96):=20=E5=90=8E=E5=8F=B0"?= =?UTF-8?q?=E6=8A=BD=E5=A5=96=E8=AE=B0=E5=BD=95"=E9=A1=B5=20=E2=80=94=20?= =?UTF-8?q?=E4=B8=AD=E5=A5=96/=E5=8F=91=E6=94=BE=E6=B5=81=E6=B0=B4?= =?UTF-8?q?=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - draws-panel(新): ProTable 列出中奖人/奖品/发放状态/发放结果,按 是否中奖/发放状态/用户ID 过滤 - activity-detail: 新增"抽奖记录"Tab - service+types: getLotteryDrawList + AdminLotteryDraw - i18n: draw.* zh/en Co-Authored-By: Claude Opus 4.8 (1M context) --- .../public/assets/locales/en-US/lottery.json | 22 +- .../public/assets/locales/zh-CN/lottery.json | 22 +- .../src/sections/lottery/activity-detail.tsx | 7 + .../src/sections/lottery/draws-panel.tsx | 193 ++++++++++++++++++ packages/ui/src/services/admin/lottery.ts | 18 ++ packages/ui/src/services/admin/typings.d.ts | 46 +++++ 6 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 apps/admin/src/sections/lottery/draws-panel.tsx diff --git a/apps/admin/public/assets/locales/en-US/lottery.json b/apps/admin/public/assets/locales/en-US/lottery.json index f6a6a33..26a80ee 100644 --- a/apps/admin/public/assets/locales/en-US/lottery.json +++ b/apps/admin/public/assets/locales/en-US/lottery.json @@ -10,7 +10,8 @@ "detail": { "prizesTab": "Prizes", "rulesTab": "Rules", - "claimsTab": "Claims" + "claimsTab": "Claims", + "drawsTab": "Draw records" }, "edit": "Edit", "editActivity": "Edit Activity", @@ -245,5 +246,24 @@ "txHash": "Tx hash", "txHashRequired": "Tx hash is required for crypto", "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" } } diff --git a/apps/admin/public/assets/locales/zh-CN/lottery.json b/apps/admin/public/assets/locales/zh-CN/lottery.json index 4eff1c0..1a226a9 100644 --- a/apps/admin/public/assets/locales/zh-CN/lottery.json +++ b/apps/admin/public/assets/locales/zh-CN/lottery.json @@ -10,7 +10,8 @@ "detail": { "prizesTab": "奖品", "rulesTab": "规则", - "claimsTab": "领奖工单" + "claimsTab": "领奖工单", + "drawsTab": "抽奖记录" }, "edit": "编辑", "editActivity": "编辑活动", @@ -245,5 +246,24 @@ "txHash": "交易哈希", "txHashRequired": "加密货币奖品必须填写交易哈希", "user": "用户" + }, + "draw": { + "dispatchAutoClaimed": "已自动发放", + "dispatchExpired": "已过期", + "dispatchFailed": "发放失败", + "dispatchNone": "待发放", + "dispatchPaid": "已发放", + "dispatchPendingClaim": "待领取", + "dispatchState": "发放状态", + "drawnAt": "抽奖时间", + "filterDispatch": "发放状态", + "filterUserId": "用户 ID", + "filterWin": "是否中奖", + "grant": "发放结果", + "loseOnly": "未中奖", + "prize": "奖品", + "tableTitle": "抽奖记录", + "user": "用户", + "winOnly": "已中奖" } } diff --git a/apps/admin/src/sections/lottery/activity-detail.tsx b/apps/admin/src/sections/lottery/activity-detail.tsx index 86514f0..94dc96f 100644 --- a/apps/admin/src/sections/lottery/activity-detail.tsx +++ b/apps/admin/src/sections/lottery/activity-detail.tsx @@ -15,6 +15,7 @@ import { getLotteryActivityDetail } from "@workspace/ui/services/admin/lottery"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import ClaimsPanel from "./claims-panel"; +import DrawsPanel from "./draws-panel"; import PrizeGrid from "./prize-grid"; import RuleEditor from "./rule-editor"; @@ -106,6 +107,9 @@ export default function ActivityDetail({ {t("detail.claimsTab", "Claims")} + + {t("detail.drawsTab", "Draw records")} + @@ -116,6 +120,9 @@ export default function ActivityDetail({ + + + diff --git a/apps/admin/src/sections/lottery/draws-panel.tsx b/apps/admin/src/sections/lottery/draws-panel.tsx new file mode 100644 index 0000000..18b2a93 --- /dev/null +++ b/apps/admin/src/sections/lottery/draws-panel.tsx @@ -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 = { + 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(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 ( + + action={ref} + columns={[ + { accessorKey: "draw_id", header: "ID" }, + { + accessorKey: "user", + header: t("draw.user", "User"), + cell: ({ row }) => ( +
+
#{row.original.user.id}
+ {row.original.user.email && ( +
+ {row.original.user.email} +
+ )} +
+ ), + }, + { + accessorKey: "prize", + header: t("draw.prize", "Prize"), + cell: ({ row }) => { + const prize = row.original.prize; + if (!(row.original.is_win && prize)) { + return ( + + {t("prize.typeNone", "No Prize (Thanks)")} + + ); + } + return ( +
+
{prize.name}
+ + {prizeTypeLabel(prize.type, t)} + +
+ ); + }, + }, + { + accessorKey: "dispatch_state", + header: t("draw.dispatchState", "Dispatch"), + cell: ({ row }) => { + const meta = DISPATCH_META[row.original.dispatch_state]; + return ( + + {meta + ? t(meta.labelKey, meta.labelDefault) + : row.original.dispatch_state} + + ); + }, + }, + { + accessorKey: "grant_message", + header: t("draw.grant", "Grant result"), + cell: ({ row }) => ( +
+ {row.original.grant_message || "--"} + {row.original.dispatch_error && ( +
+ {row.original.dispatch_error} +
+ )} +
+ ), + }, + { + 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, + }; + }} + /> + ); +} diff --git a/packages/ui/src/services/admin/lottery.ts b/packages/ui/src/services/admin/lottery.ts index 436f301..215ec5b 100644 --- a/packages/ui/src/services/admin/lottery.ts +++ b/packages/ui/src/services/admin/lottery.ts @@ -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( + `${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/lottery/draws`, + { + method: "GET", + params: { ...params }, + ...(options || {}), + } + ); +} diff --git a/packages/ui/src/services/admin/typings.d.ts b/packages/ui/src/services/admin/typings.d.ts index 7f0c7d4..26d631a 100644 --- a/packages/ui/src/services/admin/typings.d.ts +++ b/packages/ui/src/services/admin/typings.d.ts @@ -3503,4 +3503,50 @@ declare namespace API { delivery_ref?: string; 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; + }; + + 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[]; + }; }