Files
tonesc-red-packet/tonesc-red-packet/app/create/create-page.tsx

394 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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="金额精度支持08">
<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>
)
}