diff --git a/apps/admin/dist.zip b/apps/admin/dist.zip index 4d195bb..2075c25 100644 Binary files a/apps/admin/dist.zip and b/apps/admin/dist.zip differ diff --git a/apps/admin/public/assets/locales/en-US/lottery.json b/apps/admin/public/assets/locales/en-US/lottery.json index 878c459..2b24e71 100644 --- a/apps/admin/public/assets/locales/en-US/lottery.json +++ b/apps/admin/public/assets/locales/en-US/lottery.json @@ -92,7 +92,7 @@ "iconUrlInvalid": "Must be a valid URL", "isFallback": "Fallback Prize", "isFallbackHint": "Awarded when limited prizes are sold out", - "manualPrizeHint": "Manual prize: the winner submits claim info, then an operator approves and marks it paid/shipped under the Claims tab.", + "manualPrizeHint": "Manual prize: no extra config. When won, the user contacts customer service with a screenshot to redeem — no in-app claim form.", "name": "Prize Name", "namePlaceholder": "e.g. 1-Day Pass", "nameRequired": "Name is required", diff --git a/apps/admin/public/assets/locales/zh-CN/lottery.json b/apps/admin/public/assets/locales/zh-CN/lottery.json index c092b97..e6de359 100644 --- a/apps/admin/public/assets/locales/zh-CN/lottery.json +++ b/apps/admin/public/assets/locales/zh-CN/lottery.json @@ -92,7 +92,7 @@ "iconUrlInvalid": "请输入有效的 URL", "isFallback": "保底奖品", "isFallbackHint": "限量奖品抽完后发放该奖品", - "manualPrizeHint": "人工奖:用户先提交领奖信息,运营在「领奖工单」页审核并标记发放/发货。", + "manualPrizeHint": "人工发放奖品:无需额外配置。中奖后用户凭截图联系人工客服兑换,无需在 app 内填领奖表单。", "name": "奖品名称", "namePlaceholder": "例如:1 天时长卡", "nameRequired": "请输入奖品名称", diff --git a/apps/admin/src/sections/lottery/prize-form.tsx b/apps/admin/src/sections/lottery/prize-form.tsx index 3866492..26dd1de 100644 --- a/apps/admin/src/sections/lottery/prize-form.tsx +++ b/apps/admin/src/sections/lottery/prize-form.tsx @@ -42,7 +42,6 @@ import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { z } from "zod"; -const CENTS_PER_YUAN = 100; // 概率单位约定:weight 1000 = 1%(支持 3 位小数概率,总权重 100000 = 100%)。 const WEIGHT_PER_PERCENT = 1000; @@ -101,13 +100,6 @@ const getPrizeSchema = (t: TranslateFn) => is_fallback: z.boolean(), duration_days: z.number().optional(), subscribe_id: z.number().optional(), - amount_yuan: z.number().optional(), - // 人工奖(crypto / physical / manual_other)通用/专属字段 - crypto_amount: z.string().optional(), - crypto_currency: z.string().optional(), - crypto_networks: z.string().optional(), // 逗号分隔,提交时 split 成 string[] - sku_name: z.string().optional(), - claim_ttl_hours: z.number().optional(), }) .superRefine((data, ctx) => { const issue = (path: string, message: string) => { @@ -124,45 +116,6 @@ const getPrizeSchema = (t: TranslateFn) => ) ); } - if (data.type === "crypto") { - if (!data.crypto_amount?.trim()) { - issue( - "crypto_amount", - t("prize.cryptoAmountRequired", "Amount is required") - ); - } - if (!data.crypto_currency?.trim()) { - issue( - "crypto_currency", - t("prize.cryptoCurrencyRequired", "Currency is required") - ); - } - const networks = (data.crypto_networks ?? "") - .split(",") - .map((n) => n.trim()) - .filter(Boolean); - if (networks.length === 0) { - issue( - "crypto_networks", - t( - "prize.cryptoNetworksRequired", - "At least one network is required (comma-separated)" - ) - ); - } - } - if ( - data.claim_ttl_hours !== undefined && - (!Number.isInteger(data.claim_ttl_hours) || data.claim_ttl_hours <= 0) - ) { - issue( - "claim_ttl_hours", - t( - "prize.claimTtlInvalid", - "Claim window (hours) must be a positive integer" - ) - ); - } if (!(data.unlimited_stock || isPositiveInt(data.total_stock))) { issue( "total_stock", @@ -187,9 +140,6 @@ interface PrizeFormProps { function toFormValues(prize?: API.LotteryPrize): PrizeFormValues { const config = prize?.config ?? {}; - const networks = Array.isArray(config.networks) - ? (config.networks as string[]).join(", ") - : undefined; return { type: (prize?.type as PrizeFormValues["type"]) ?? "vpn_duration", name: prize?.name ?? "", @@ -200,26 +150,10 @@ function toFormValues(prize?: API.LotteryPrize): PrizeFormValues { is_fallback: prize?.is_fallback ?? false, duration_days: config.duration_days as number | undefined, subscribe_id: config.subscribe_id as number | undefined, - amount_yuan: - typeof config.amount_cents === "number" - ? config.amount_cents / CENTS_PER_YUAN - : undefined, - crypto_amount: config.amount as string | undefined, - crypto_currency: config.currency as string | undefined, - crypto_networks: networks, - sku_name: config.sku_name as string | undefined, - claim_ttl_hours: config.claim_ttl_hours as number | undefined, }; } function toConfig(values: PrizeFormValues): Record { - // claim_ttl_hours 对所有人工奖通用:控制领奖窗口(缺省后端默认 7 天)。 - const withTtl = (base: Record): Record => { - if (values.claim_ttl_hours && values.claim_ttl_hours > 0) { - base.claim_ttl_hours = values.claim_ttl_hours; - } - return base; - }; if (values.type === "vpn_duration") { const config: Record = { duration_days: values.duration_days }; // 可选:无活跃订阅时按该套餐新建订阅发放时长。0/未选则沿用旧行为(跳过发放)。 @@ -228,16 +162,7 @@ function toConfig(values: PrizeFormValues): Record { } return config; } - if (values.type === "crypto") { - return withTtl({ - amount: values.crypto_amount?.trim() ?? "", - currency: values.crypto_currency?.trim() ?? "", - networks: (values.crypto_networks ?? "") - .split(",") - .map((n) => n.trim()) - .filter(Boolean), - }); - } + // crypto(人工发放) / none 无需额外配置:中奖后凭截图联系客服线下发放。 return {}; } @@ -326,18 +251,7 @@ export default function PrizeForm({ } const renderInput = ( - name: - | "name" - | "icon_url" - | "weight" - | "total_stock" - | "duration_days" - | "amount_yuan" - | "crypto_amount" - | "crypto_currency" - | "crypto_networks" - | "sku_name" - | "claim_ttl_hours", + name: "name" | "icon_url" | "weight" | "total_stock" | "duration_days", label: string, inputProps: Partial>, description?: string @@ -517,57 +431,12 @@ export default function PrizeForm({

)} {type === "crypto" && ( - <> - {renderInput( - "crypto_amount", - t("prize.cryptoAmount", "Amount"), - { - placeholder: t( - "prize.cryptoAmountPlaceholder", - "e.g. 0.01" - ), - } +

+ {t( + "prize.manualPrizeHint", + "Manual prize: no extra config. When won, the user contacts customer service with a screenshot to redeem — no in-app claim form." )} - {renderInput( - "crypto_currency", - t("prize.cryptoCurrency", "Currency"), - { placeholder: "USDT / BTC / ETH" } - )} - {renderInput( - "crypto_networks", - t("prize.cryptoNetworks", "Networks"), - { placeholder: "TRC20, ERC20, BTC" }, - t( - "prize.cryptoNetworksHint", - "Comma-separated. The user picks one of these when claiming." - ) - )} - - )} - {type === "crypto" && ( - <> - {renderInput( - "claim_ttl_hours", - t("prize.claimTtlHours", "Claim window (hours)"), - { - type: "text", - inputMode: "numeric", - formatOutput: numberOutput, - placeholder: "168", - suffix: t("prize.hoursSuffix", "hours"), - }, - t( - "prize.claimTtlHint", - "How long the user has to submit claim info. Default 168h (7 days)." - ) - )} -

- {t( - "prize.manualPrizeHint", - "Manual prize: the winner submits claim info, then an operator approves and marks it paid/shipped under the Claims tab." - )} -

- +

)} {renderInput( "weight",