394 lines
12 KiB
TypeScript
394 lines
12 KiB
TypeScript
"use client"
|
||
|
||
import { useState } from "react"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Button } from "@/components/ui/button"
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select"
|
||
import { CreateRedPacketPayload } from "@/lib/types/red-packet"
|
||
import { createRedPacket } from "../server/actions/create-red-packet"
|
||
import { toast } from "sonner"
|
||
import { useRouter } from "next/navigation"
|
||
|
||
type RuleType = "fixed" | "random" | "luck"
|
||
|
||
type CurrencyUnit = {
|
||
name: string
|
||
precision: number
|
||
}
|
||
|
||
type CurrencyUnitPreset = {
|
||
key: string
|
||
name: string
|
||
precision: number
|
||
custom?: boolean
|
||
}
|
||
|
||
const CURRENCY_PRESETS: CurrencyUnitPreset[] = [
|
||
{ key: "rmb", name: "人民币", precision: 2 },
|
||
{ key: "custom", name: "自定义", precision: 0, custom: true },
|
||
]
|
||
|
||
/* ---------- utils ---------- */
|
||
function formatByPrecision(value: string, precision: number) {
|
||
if (!value) return ""
|
||
const num = Number(value)
|
||
if (Number.isNaN(num)) return ""
|
||
return num.toFixed(precision)
|
||
}
|
||
|
||
function isValidPrecision(p: string) {
|
||
const n = Number(p)
|
||
if (isNaN(n)) return false
|
||
if (n < 0 || n > 8) return false
|
||
return true
|
||
}
|
||
|
||
function buildCreatePayload(params: {
|
||
count: number
|
||
currency: { name: string; precision: number }
|
||
ruleType: RuleType
|
||
singleAmount: string
|
||
minAmount: string
|
||
maxAmount: string
|
||
totalAmount: string
|
||
maxDrawTimes?: string
|
||
}): CreateRedPacketPayload {
|
||
const {
|
||
count,
|
||
currency,
|
||
ruleType,
|
||
singleAmount,
|
||
minAmount,
|
||
maxAmount,
|
||
totalAmount,
|
||
maxDrawTimes,
|
||
} = params
|
||
|
||
const rule =
|
||
ruleType === "fixed"
|
||
? {
|
||
type: "fixed",
|
||
singleAmount,
|
||
} as const
|
||
: ruleType === "random"
|
||
? {
|
||
type: "random",
|
||
min: minAmount,
|
||
max: maxAmount,
|
||
} as const
|
||
: {
|
||
type: "luck",
|
||
totalAmount,
|
||
} as const;
|
||
|
||
return {
|
||
count,
|
||
currency,
|
||
rule,
|
||
maxDrawTimes: maxDrawTimes
|
||
? Number(maxDrawTimes)
|
||
: null,
|
||
}
|
||
}
|
||
|
||
/* ---------- page ---------- */
|
||
|
||
export function CreateRedPacketPage() {
|
||
const router = useRouter();
|
||
/* 红包数量(允许为空) */
|
||
const [count, setCount] = useState("1")
|
||
|
||
const parsedCount = Math.max(1, Number(count) || 1)
|
||
|
||
/* 金额单位 */
|
||
const [unitKey, setUnitKey] = useState("rmb")
|
||
const preset = CURRENCY_PRESETS.find((u) => u.key === unitKey)!
|
||
|
||
const [customUnitName, setCustomUnitName] = useState("")
|
||
const [customPrecision, setCustomPrecision] = useState("0")
|
||
|
||
const currency: CurrencyUnit = preset.custom
|
||
? {
|
||
name: customUnitName,
|
||
precision: Number(customPrecision) || preset.precision,
|
||
}
|
||
: {
|
||
name: preset.name,
|
||
precision: preset.precision,
|
||
}
|
||
|
||
const precision = currency.precision
|
||
|
||
/* 规则 */
|
||
const [ruleType, setRuleType] = useState<RuleType>("fixed")
|
||
|
||
/* 金额 */
|
||
const [singleAmount, setSingleAmount] = useState("")
|
||
const [minAmount, setMinAmount] = useState("")
|
||
const [maxAmount, setMaxAmount] = useState("")
|
||
const [totalAmount, setTotalAmount] = useState("")
|
||
|
||
/* 抽取次数 */
|
||
const [maxDrawTimes, setMaxDrawTimes] = useState("")
|
||
|
||
/* ---------- total estimate ---------- */
|
||
|
||
function renderTotalEstimate() {
|
||
if (ruleType === "fixed" && singleAmount) {
|
||
return `总金额:${formatByPrecision(
|
||
(Number(singleAmount) * parsedCount).toString(),
|
||
precision
|
||
)} ${currency.name}`
|
||
}
|
||
|
||
if (ruleType === "luck" && totalAmount) {
|
||
return `总金额:${formatByPrecision(
|
||
totalAmount,
|
||
precision
|
||
)} ${currency.name}`
|
||
}
|
||
|
||
if (ruleType === "random" && minAmount && maxAmount) {
|
||
return `总金额范围:${formatByPrecision(
|
||
(Number(minAmount) * parsedCount).toString(),
|
||
precision
|
||
)} ~ ${formatByPrecision(
|
||
(Number(maxAmount) * parsedCount).toString(),
|
||
precision
|
||
)} ${currency.name}`
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-100 px-4 py-6">
|
||
<div className="mx-auto max-w-md space-y-5">
|
||
<h1 className="text-center text-2xl font-semibold">创建红包</h1>
|
||
|
||
<Card>
|
||
<Field label="红包数量">
|
||
<Input
|
||
type="number"
|
||
value={count}
|
||
onChange={(e) => setCount(e.target.value)}
|
||
onBlur={() => {
|
||
if (!count || Number(count) < 1) setCount("1")
|
||
}}
|
||
/>
|
||
</Field>
|
||
|
||
<Field label="金额单位">
|
||
<Select value={unitKey} onValueChange={setUnitKey}>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{CURRENCY_PRESETS.map((u) => (
|
||
<SelectItem key={u.key} value={u.key}>
|
||
{u.name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</Field>
|
||
|
||
{preset.custom && (
|
||
<>
|
||
<Field label="单位名称">
|
||
<Input
|
||
value={customUnitName}
|
||
onChange={(e) => setCustomUnitName(e.target.value)}
|
||
/>
|
||
</Field>
|
||
|
||
<Field label="金额精度(支持0~8)">
|
||
<Input
|
||
type="number"
|
||
value={customPrecision}
|
||
onChange={(e) => {
|
||
let n = Number(e.target.value);
|
||
if (isNaN(n)) n = 0;
|
||
setCustomPrecision(`${n}`)
|
||
}}
|
||
/>
|
||
{!isValidPrecision(customPrecision) && (
|
||
<p className="text-xs text-red-500">精度格式不合法</p>
|
||
)}
|
||
</Field>
|
||
</>
|
||
)}
|
||
</Card>
|
||
|
||
<Card>
|
||
<RuleSelector value={ruleType} onChange={setRuleType} />
|
||
|
||
{ruleType === "fixed" && (
|
||
<MoneyInput
|
||
label="单个红包金额"
|
||
value={singleAmount}
|
||
precision={precision}
|
||
onChange={setSingleAmount}
|
||
/>
|
||
)}
|
||
|
||
{ruleType === "random" && (
|
||
<>
|
||
<MoneyInput
|
||
label="最小金额"
|
||
value={minAmount}
|
||
precision={precision}
|
||
onChange={setMinAmount}
|
||
/>
|
||
<MoneyInput
|
||
label="最大金额"
|
||
value={maxAmount}
|
||
precision={precision}
|
||
onChange={setMaxAmount}
|
||
/>
|
||
</>
|
||
)}
|
||
|
||
{ruleType === "luck" && (
|
||
<MoneyInput
|
||
label="总金额"
|
||
value={totalAmount}
|
||
precision={precision}
|
||
onChange={setTotalAmount}
|
||
/>
|
||
)}
|
||
</Card>
|
||
|
||
{parsedCount >= 2 && (
|
||
<Card>
|
||
<Field label="单个用户最大抽取次数(可选)">
|
||
<Input
|
||
inputMode="numeric"
|
||
placeholder="不填默认为1"
|
||
value={maxDrawTimes}
|
||
onChange={(e) => setMaxDrawTimes(e.target.value)}
|
||
/>
|
||
</Field>
|
||
</Card>
|
||
)}
|
||
|
||
{renderTotalEstimate() && (
|
||
<div className="rounded-xl bg-blue-50 px-4 py-3 text-sm text-blue-700">
|
||
{renderTotalEstimate()}
|
||
</div>
|
||
)}
|
||
|
||
<Button
|
||
className="h-12 w-full text-base"
|
||
onClick={async () => {
|
||
const payload = buildCreatePayload({
|
||
count: parsedCount,
|
||
currency,
|
||
ruleType,
|
||
singleAmount,
|
||
minAmount,
|
||
maxAmount,
|
||
totalAmount,
|
||
maxDrawTimes,
|
||
})
|
||
|
||
const result = await createRedPacket(payload).catch((e) => {
|
||
toast.error(`${e.message || '请求失败'}`)
|
||
return null;
|
||
});
|
||
|
||
if (result && result.adminId) {
|
||
router.push(`/admin/${result.adminId}`)
|
||
}
|
||
}}
|
||
>
|
||
创建红包
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/* ---------- UI helpers ---------- */
|
||
|
||
function Card({ children }: { children: React.ReactNode }) {
|
||
return <div className="space-y-4 rounded-2xl bg-white p-4">{children}</div>
|
||
}
|
||
|
||
function Field({
|
||
label,
|
||
children,
|
||
}: {
|
||
label: string
|
||
children: React.ReactNode
|
||
}) {
|
||
return (
|
||
<div className="space-y-1.5">
|
||
<label className="text-sm font-medium">{label}</label>
|
||
{children}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function MoneyInput({
|
||
label,
|
||
value,
|
||
precision,
|
||
onChange,
|
||
}: {
|
||
label: string
|
||
value: string
|
||
precision: number
|
||
onChange: (v: string) => void
|
||
}) {
|
||
return (
|
||
<Field label={label}>
|
||
<Input
|
||
inputMode="decimal"
|
||
value={value}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
onBlur={() => onChange(formatByPrecision(value, precision))}
|
||
placeholder={`精度 ${precision}`}
|
||
/>
|
||
</Field>
|
||
)
|
||
}
|
||
|
||
function RuleSelector({
|
||
value,
|
||
onChange,
|
||
}: {
|
||
value: RuleType
|
||
onChange: (v: RuleType) => void
|
||
}) {
|
||
const rules = [
|
||
{ type: "fixed", label: "固定" },
|
||
{ type: "random", label: "随机" },
|
||
{ type: "luck", label: "拼手气" },
|
||
] as const
|
||
|
||
return (
|
||
<div className="flex rounded-xl bg-muted p-1">
|
||
{rules.map((r) => (
|
||
<button
|
||
key={r.type}
|
||
onClick={() => onChange(r.type)}
|
||
className={`flex-1 rounded-lg py-2 text-sm font-medium transition
|
||
${value === r.type
|
||
? "bg-background shadow"
|
||
: "text-muted-foreground"
|
||
}`}
|
||
>
|
||
{r.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|