296 lines
10 KiB
TypeScript
296 lines
10 KiB
TypeScript
|
|
"use client"
|
|||
|
|
|
|||
|
|
import { cn } from "@/lib/utils"
|
|||
|
|
import type { Mode } from "@/components/logic"
|
|||
|
|
|
|||
|
|
interface SettingsPanelProps {
|
|||
|
|
mode: Mode
|
|||
|
|
totalTasks: number
|
|||
|
|
pickCount: number
|
|||
|
|
rounds: number
|
|||
|
|
allowRepeat: boolean
|
|||
|
|
singleAllowRepeat: boolean
|
|||
|
|
selectedRecords: { number: number; colorIndex: number }[]
|
|||
|
|
onModeChange: (v: Mode) => void
|
|||
|
|
onTotalTasksChange: (v: number) => void
|
|||
|
|
onPickCountChange: (v: number) => void
|
|||
|
|
onRoundsChange: (v: number) => void
|
|||
|
|
onAllowRepeatChange: (v: boolean) => void
|
|||
|
|
onSingleAllowRepeatChange: (v: boolean) => void
|
|||
|
|
onRoll: () => void
|
|||
|
|
onReset: () => void
|
|||
|
|
isRolling: boolean
|
|||
|
|
errorMsg: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function FieldRow({
|
|||
|
|
label,
|
|||
|
|
hint,
|
|||
|
|
value,
|
|||
|
|
onChange,
|
|||
|
|
min = 1,
|
|||
|
|
}: {
|
|||
|
|
label: string
|
|||
|
|
hint: string
|
|||
|
|
value: number
|
|||
|
|
onChange: (v: number) => void
|
|||
|
|
min?: number
|
|||
|
|
}) {
|
|||
|
|
return (
|
|||
|
|
<div className="flex items-center justify-between gap-4">
|
|||
|
|
<div className="min-w-0">
|
|||
|
|
<div className="text-xs text-[#16191f] leading-none mb-0.5">{label}</div>
|
|||
|
|
<div className="text-[10px] text-[#8a95a1] leading-none">{hint}</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center gap-0 border border-[#e2e6ea] flex-shrink-0">
|
|||
|
|
<button
|
|||
|
|
onClick={() => onChange(Math.max(min, value - 1))}
|
|||
|
|
className="w-7 h-7 flex items-center justify-center text-[#8a95a1] hover:text-[#16191f] hover:bg-[#f1f3f5] transition-colors text-base leading-none"
|
|||
|
|
aria-label={`减少${label}`}
|
|||
|
|
>
|
|||
|
|
−
|
|||
|
|
</button>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
value={value}
|
|||
|
|
min={min}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
const v = parseInt(e.target.value, 10)
|
|||
|
|
if (!isNaN(v) && v >= min) onChange(v)
|
|||
|
|
}}
|
|||
|
|
className={cn(
|
|||
|
|
"w-12 h-7 text-center text-sm font-mono text-[#16191f] bg-[#f7f8fa]",
|
|||
|
|
"border-x border-[#e2e6ea]",
|
|||
|
|
"focus:outline-none focus:bg-[#ffffff]",
|
|||
|
|
"[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none"
|
|||
|
|
)}
|
|||
|
|
/>
|
|||
|
|
<button
|
|||
|
|
onClick={() => onChange(value + 1)}
|
|||
|
|
className="w-7 h-7 flex items-center justify-center text-[#8a95a1] hover:text-[#16191f] hover:bg-[#f1f3f5] transition-colors text-base leading-none"
|
|||
|
|
aria-label={`增加${label}`}
|
|||
|
|
>
|
|||
|
|
+
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function SettingsPanel({
|
|||
|
|
mode,
|
|||
|
|
totalTasks,
|
|||
|
|
pickCount,
|
|||
|
|
rounds,
|
|||
|
|
allowRepeat,
|
|||
|
|
singleAllowRepeat,
|
|||
|
|
selectedRecords,
|
|||
|
|
onModeChange,
|
|||
|
|
onTotalTasksChange,
|
|||
|
|
onPickCountChange,
|
|||
|
|
onRoundsChange,
|
|||
|
|
onAllowRepeatChange,
|
|||
|
|
onSingleAllowRepeatChange,
|
|||
|
|
onRoll,
|
|||
|
|
onReset,
|
|||
|
|
isRolling,
|
|||
|
|
errorMsg,
|
|||
|
|
}: SettingsPanelProps) {
|
|||
|
|
const repeatDisabled = rounds < 2
|
|||
|
|
const isSingleMode = mode === "single"
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="flex flex-col gap-0">
|
|||
|
|
{/* Mode selector */}
|
|||
|
|
<div className="flex gap-2 mb-6">
|
|||
|
|
<button
|
|||
|
|
onClick={() => onModeChange("single")}
|
|||
|
|
className={cn(
|
|||
|
|
"flex-1 h-9 border text-xs tracking-wider uppercase transition-colors",
|
|||
|
|
isSingleMode
|
|||
|
|
? "border-[#0f141a] bg-[#0f141a] text-[#ffffff]"
|
|||
|
|
: "border-[#e2e6ea] text-[#8a95a1] hover:border-[#cdd2d8] hover:text-[#16191f]"
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
单次模式
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => onModeChange("multi")}
|
|||
|
|
className={cn(
|
|||
|
|
"flex-1 h-9 border text-xs tracking-wider uppercase transition-colors",
|
|||
|
|
!isSingleMode
|
|||
|
|
? "border-[#0f141a] bg-[#0f141a] text-[#ffffff]"
|
|||
|
|
: "border-[#e2e6ea] text-[#8a95a1] hover:border-[#cdd2d8] hover:text-[#16191f]"
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
分组模式
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Section label */}
|
|||
|
|
<div className="flex items-center gap-2 mb-4">
|
|||
|
|
<span className="text-[10px] tracking-[0.2em] uppercase text-[#8a95a1] font-mono">CONFIG</span>
|
|||
|
|
<span className="flex-1 h-px bg-[#e2e6ea]" />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Fields */}
|
|||
|
|
<div className="flex flex-col gap-4 mb-6">
|
|||
|
|
<FieldRow label="总人数" hint="参与点名的序号上限" value={totalTasks} onChange={onTotalTasksChange} />
|
|||
|
|
<div className="h-px bg-[#e2e6ea]" />
|
|||
|
|
<FieldRow
|
|||
|
|
label="每次人数"
|
|||
|
|
hint={isSingleMode ? "每次抽取的人数" : "每组抽取的人数"}
|
|||
|
|
value={pickCount}
|
|||
|
|
onChange={onPickCountChange}
|
|||
|
|
/>
|
|||
|
|
{!isSingleMode && (
|
|||
|
|
<>
|
|||
|
|
<div className="h-px bg-[#e2e6ea]" />
|
|||
|
|
<FieldRow label="抽取组数" hint="生成分组的数量" value={rounds} onChange={onRoundsChange} min={1} />
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Single mode: Allow repeat toggle */}
|
|||
|
|
{isSingleMode && (
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => onSingleAllowRepeatChange(!singleAllowRepeat)}
|
|||
|
|
className={cn(
|
|||
|
|
"flex items-center justify-between w-full px-3 py-2.5 border mb-6 transition-colors",
|
|||
|
|
singleAllowRepeat && "border-[#0f141a] bg-[#f1f3f5]",
|
|||
|
|
!singleAllowRepeat && "border-[#e2e6ea] hover:border-[#cdd2d8]"
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
<div className="text-left">
|
|||
|
|
<div className={cn("text-xs", singleAllowRepeat ? "text-[#0f141a]" : "text-[#4a5563]")}>
|
|||
|
|
允许重复选人
|
|||
|
|
</div>
|
|||
|
|
<div className="text-[10px] text-[#8a95a1] mt-0.5">已选过的人可再次被抽中</div>
|
|||
|
|
</div>
|
|||
|
|
{/* Toggle pill */}
|
|||
|
|
<div className={cn(
|
|||
|
|
"w-9 h-5 border relative flex-shrink-0 transition-colors",
|
|||
|
|
singleAllowRepeat ? "border-[#0f141a] bg-[#0f141a]/10" : "border-[#cdd2d8]"
|
|||
|
|
)}>
|
|||
|
|
<span className={cn(
|
|||
|
|
"absolute top-0.5 w-3 h-3 transition-all duration-150 rounded-full",
|
|||
|
|
singleAllowRepeat
|
|||
|
|
? "left-[calc(100%-15px)] bg-[#0f141a]"
|
|||
|
|
: "left-0.5 bg-[#cdd2d8]"
|
|||
|
|
)} />
|
|||
|
|
</div>
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Group mode only: Toggle */}
|
|||
|
|
{!isSingleMode && (
|
|||
|
|
<>
|
|||
|
|
{/* Toggle */}
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
disabled={repeatDisabled}
|
|||
|
|
onClick={() => !repeatDisabled && onAllowRepeatChange(!allowRepeat)}
|
|||
|
|
className={cn(
|
|||
|
|
"flex items-center justify-between w-full px-3 py-2.5 border mb-6 transition-colors",
|
|||
|
|
repeatDisabled && "opacity-40 cursor-not-allowed border-[#e2e6ea]",
|
|||
|
|
!repeatDisabled && allowRepeat && "border-[#0f141a] bg-[#f1f3f5]",
|
|||
|
|
!repeatDisabled && !allowRepeat && "border-[#e2e6ea] hover:border-[#cdd2d8]"
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
<div className="text-left">
|
|||
|
|
<div className={cn("text-xs", allowRepeat && !repeatDisabled ? "text-[#0f141a]" : "text-[#4a5563]")}>
|
|||
|
|
组间允许重复
|
|||
|
|
</div>
|
|||
|
|
<div className="text-[10px] text-[#8a95a1] mt-0.5">不同组可出现相同序号</div>
|
|||
|
|
</div>
|
|||
|
|
{/* Toggle pill */}
|
|||
|
|
<div className={cn(
|
|||
|
|
"w-9 h-5 border relative flex-shrink-0 transition-colors",
|
|||
|
|
allowRepeat && !repeatDisabled ? "border-[#0f141a] bg-[#0f141a]/10" : "border-[#cdd2d8]"
|
|||
|
|
)}>
|
|||
|
|
<span className={cn(
|
|||
|
|
"absolute top-0.5 w-3 h-3 transition-all duration-150 rounded-full",
|
|||
|
|
allowRepeat && !repeatDisabled
|
|||
|
|
? "left-[calc(100%-15px)] bg-[#0f141a]"
|
|||
|
|
: "left-0.5 bg-[#cdd2d8]"
|
|||
|
|
)} />
|
|||
|
|
</div>
|
|||
|
|
</button>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Error */}
|
|||
|
|
{errorMsg && (
|
|||
|
|
<div className="border border-[#d92d20]/40 bg-[#d92d20]/5 px-3 py-2 mb-4 text-xs text-[#d92d20] leading-relaxed fui-fadein">
|
|||
|
|
ERR: {errorMsg}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Action buttons */}
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
{/* Single mode: Roll button */}
|
|||
|
|
{isSingleMode && (
|
|||
|
|
<button
|
|||
|
|
onClick={onRoll}
|
|||
|
|
disabled={isRolling}
|
|||
|
|
className={cn(
|
|||
|
|
"flex-1 h-10 border text-xs tracking-[0.15em] uppercase font-mono transition-all",
|
|||
|
|
"disabled:opacity-40 disabled:cursor-not-allowed",
|
|||
|
|
isRolling
|
|||
|
|
? "border-[#e2e6ea] text-[#8a95a1]"
|
|||
|
|
: "border-[#0f141a] bg-[#0f141a] text-[#ffffff] hover:bg-[#ffffff] hover:text-[#0f141a]"
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
{isRolling ? (
|
|||
|
|
<span className="flex items-center justify-center gap-2">
|
|||
|
|
<span className="inline-block w-3 h-3 border border-[#cdd2d8] border-t-[#16191f] rounded-full animate-spin" />
|
|||
|
|
PROCESSING
|
|||
|
|
</span>
|
|||
|
|
) : (
|
|||
|
|
"SELECT"
|
|||
|
|
)}
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Single mode: Reset button */}
|
|||
|
|
{isSingleMode && selectedRecords.length > 0 && (
|
|||
|
|
<button
|
|||
|
|
onClick={onReset}
|
|||
|
|
className={cn(
|
|||
|
|
"h-10 px-4 border border-[#e2e6ea] text-xs tracking-wider uppercase font-mono transition-colors",
|
|||
|
|
"text-[#8a95a1] hover:border-[#d92d20] hover:text-[#d92d20]"
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
RESET
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Multi mode: Execute button */}
|
|||
|
|
{!isSingleMode && (
|
|||
|
|
<button
|
|||
|
|
onClick={onRoll}
|
|||
|
|
disabled={isRolling}
|
|||
|
|
className={cn(
|
|||
|
|
"w-full h-10 border text-xs tracking-[0.15em] uppercase font-mono transition-all",
|
|||
|
|
"disabled:opacity-40 disabled:cursor-not-allowed",
|
|||
|
|
isRolling
|
|||
|
|
? "border-[#e2e6ea] text-[#8a95a1]"
|
|||
|
|
: "border-[#0f141a] bg-[#0f141a] text-[#ffffff] hover:bg-[#ffffff] hover:text-[#0f141a]"
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
{isRolling ? (
|
|||
|
|
<span className="flex items-center justify-center gap-2">
|
|||
|
|
<span className="inline-block w-3 h-3 border border-[#cdd2d8] border-t-[#16191f] rounded-full animate-spin" />
|
|||
|
|
PROCESSING
|
|||
|
|
</span>
|
|||
|
|
) : (
|
|||
|
|
"EXECUTE"
|
|||
|
|
)}
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|