diff --git a/apps/admin/src/sections/lottery/prize-form.tsx b/apps/admin/src/sections/lottery/prize-form.tsx index 1a1a1e9..0eb487d 100644 --- a/apps/admin/src/sections/lottery/prize-form.tsx +++ b/apps/admin/src/sections/lottery/prize-form.tsx @@ -43,6 +43,8 @@ import { toast } from "sonner"; import { z } from "zod"; const CENTS_PER_YUAN = 100; +// 概率单位约定:weight 1000 = 1%(支持 3 位小数概率,总权重 100000 = 100%)。 +const WEIGHT_PER_PERCENT = 1000; type TranslateFn = (key: string, defaultValue: string) => string; @@ -102,7 +104,6 @@ const getPrizeSchema = (t: TranslateFn) => .optional(), weight: z .number() - .int(t("prize.weightInteger", "Probability must be an integer")) .min(0, t("prize.weightMin", "Probability must be at least 0")) .max(100, t("prize.weightMax", "Probability cannot exceed 100")), unlimited_stock: z.boolean(), @@ -221,7 +222,7 @@ function toFormValues(prize?: API.LotteryPrize): PrizeFormValues { type: (prize?.type as PrizeFormValues["type"]) ?? "vpn_duration", name: prize?.name ?? "", icon_url: prize?.icon_url ?? "", - weight: prize?.weight ?? 1, + weight: (prize?.weight ?? 0) / WEIGHT_PER_PERCENT, unlimited_stock: prize ? prize.total_stock === null : true, total_stock: prize?.total_stock ?? undefined, is_fallback: prize?.is_fallback ?? false, @@ -325,7 +326,9 @@ export default function PrizeForm({ name: values.name.trim(), icon_url: values.icon_url?.trim() ?? "", config: toConfig(values), - weight: values.is_fallback ? 0 : values.weight, + weight: values.is_fallback + ? 0 + : Math.round(values.weight * WEIGHT_PER_PERCENT), total_stock: values.unlimited_stock ? null : (values.total_stock ?? null), @@ -643,7 +646,7 @@ export default function PrizeForm({ type: "number", min: 0, max: 100, - step: 1, + step: 0.001, suffix: "%", disabled: isFallback, }, diff --git a/apps/admin/src/sections/lottery/prize-grid.tsx b/apps/admin/src/sections/lottery/prize-grid.tsx index a5565a7..4bc9d01 100644 --- a/apps/admin/src/sections/lottery/prize-grid.tsx +++ b/apps/admin/src/sections/lottery/prize-grid.tsx @@ -14,9 +14,16 @@ import PrizeForm from "./prize-form"; const DEFAULT_GRID_SIZE = 9; const CENTER_SLOT = 4; -const PERCENT_DECIMALS = 1; -const REQUIRED_TOTAL_PERCENT = 100; const CENTS_PER_YUAN = 100; +// 概率单位约定:weight 1000 = 1%,总权重 100000 = 100.000%(支持 3 位小数概率)。 +const WEIGHT_PER_PERCENT = 1000; +const FULL_TOTAL_WEIGHT = 100 * WEIGHT_PER_PERCENT; + +// weightToPercent 把原始 weight 转成百分比字符串(最多 3 位小数,去尾零)。 +function weightToPercent(weight: number): string { + const pct = weight / WEIGHT_PER_PERCENT; + return `${Number.parseFloat(pct.toFixed(3))}`; +} interface PrizeGridProps { activity: API.LotteryActivity; @@ -110,7 +117,7 @@ export default function PrizeGrid({ activity }: PrizeGridProps) { [randomPrizes] ); const hasFallback = prizes.some((prize) => prize.is_fallback); - const totalIsValid = totalWeight === REQUIRED_TOTAL_PERCENT; + const totalIsValid = totalWeight === FULL_TOTAL_WEIGHT; const gridSize = activity.grid_size || DEFAULT_GRID_SIZE; const slots = Array.from({ length: gridSize }, (_, index) => index); @@ -137,7 +144,8 @@ export default function PrizeGrid({ activity }: PrizeGridProps) {

{randomPrizes.length > 0 && ( - {t("prize.probabilityTotal", "Total")}: {totalWeight}% + {t("prize.probabilityTotal", "Total")}:{" "} + {weightToPercent(totalWeight)}% )} @@ -145,9 +153,7 @@ export default function PrizeGrid({ activity }: PrizeGridProps) {
{randomPrizes.map((prize) => ( - {prize.name}:{" "} - {((prize.weight / totalWeight) * 100).toFixed(PERCENT_DECIMALS)} - % + {prize.name}: {weightToPercent(prize.weight)}% ))}
@@ -170,7 +176,7 @@ export default function PrizeGrid({ activity }: PrizeGridProps) { {t( "prize.totalNot100Hint", "The drawable prizes currently total {{total}}%. Adjust each prize's win probability so they sum to exactly 100%.", - { total: totalWeight } + { total: weightToPercent(totalWeight) } )} @@ -244,8 +250,10 @@ export default function PrizeGrid({ activity }: PrizeGridProps) { })()} {t("prize.weightShort", "Prob")}:{" "} - {prize.is_fallback ? "—" : `${prize.weight}%`} ·{" "} - {t("prize.stockShort", "Stock")}:{" "} + {prize.is_fallback + ? "—" + : `${weightToPercent(prize.weight)}%`}{" "} + · {t("prize.stockShort", "Stock")}:{" "} {prize.total_stock === null ? "∞" : prize.total_stock}