"use client" import { useState, useCallback } from "react" export type Mode = "single" | "multi" function getRandomDistinct(total: number, count: number, forbidSet: Set = new Set()): number[] | null { if (count > total - forbidSet.size) return null const result: number[] = [] const selected = new Set() let attempts = 0 while (selected.size < count && attempts < 100000) { const rand = Math.floor(Math.random() * total) + 1 if (!forbidSet.has(rand) && !selected.has(rand)) { selected.add(rand) result.push(rand) } attempts++ } return selected.size === count ? result : null } function generateGroups( total: number, perPick: number, rounds: number, allowGroupRepeat: boolean ): { success: boolean; groups: { numbers: number[]; colorIndex: number }[]; errorMsg: string } { if (total <= 0) return { success: false, groups: [], errorMsg: "总人数必须大于 0" } if (perPick <= 0) return { success: false, groups: [], errorMsg: "每次人数必须大于 0" } if (rounds <= 0) return { success: false, groups: [], errorMsg: "抽取组数必须为正整数" } if (perPick > total) return { success: false, groups: [], errorMsg: `每次人数 (${perPick}) 不能大于总人数 (${total})` } if (!allowGroupRepeat && rounds * perPick > total) { return { success: false, groups: [], errorMsg: `组间不可重复时,总抽取数 (${rounds}×${perPick}=${rounds * perPick}) 超出总人数 (${total})`, } } const groups: { numbers: number[]; colorIndex: number }[] = [] const globalUsed = new Set() for (let i = 0; i < rounds; i++) { const nums = allowGroupRepeat ? getRandomDistinct(total, perPick) : getRandomDistinct(total, perPick, globalUsed) if (!nums) return { success: false, groups: [], errorMsg: `第 ${i + 1} 组抽取失败,剩余人数不足` } if (!allowGroupRepeat) nums.forEach((n) => globalUsed.add(n)) groups.push({ numbers: nums, colorIndex: i % COLORS.length }) } return { success: true, groups, errorMsg: "" } } export interface RollCallState { mode: Mode totalTasks: number pickCount: number rounds: number allowRepeat: boolean // Multi mode groups: { numbers: number[]; colorIndex: number }[] highlighted: Set usedAll: Set hasResult: boolean // Single mode selectedRecords: { number: number; colorIndex: number }[] singleBatches: { numbers: number[]; colorIndex: number; batchNumber: number }[] singleHighlighted: number[] isSingleComplete: boolean singleAllowRepeat: boolean // Common isRolling: boolean errorMsg: string placeholder: string | undefined } export interface RollCallActions { setMode: (v: Mode) => void setTotalTasks: (v: number) => void setPickCount: (v: number) => void setRounds: (v: number) => void setAllowRepeat: (v: boolean) => void setSingleAllowRepeat: (v: boolean) => void handleRoll: () => void handleSingleRoll: () => void resetSingle: () => void clearResult: () => void getBallState: (n: number) => "idle" | "highlighted" | "used" getBallColorIndex: (n: number) => number | undefined getBallDimmed: (n: number) => boolean } export const COLORS = [ "#d92d20", "#0070f3", "#6d6df6", "#00b8d9", "#36b37e", "#ff991f", "#8754ad", "#e63946", "#457b9d", "#2a9d8f", "#f4a261", "#264653", "#9d4edd", "#fb5607", "#06d6a0", "#118ab2", "#ffd166", ] export function useRollCallLogic(): [RollCallState, RollCallActions] { const [mode, setMode] = useState("single") const [totalTasks, setTotalTasks] = useState(35) const [pickCount, setPickCount] = useState(1) const [rounds, setRounds] = useState(1) const [allowRepeat, setAllowRepeat] = useState(false) // Multi mode state const [groups, setGroups] = useState<{ numbers: number[]; colorIndex: number }[]>([]) const [highlighted, setHighlighted] = useState>(new Set()) const [usedAll, setUsedAll] = useState>(new Set()) const [hasResult, setHasResult] = useState(false) // Single mode state const [selectedRecords, setSelectedRecords] = useState<{ number: number; colorIndex: number }[]>([]) const [singleBatches, setSingleBatches] = useState<{ numbers: number[]; colorIndex: number; batchNumber: number }[]>([]) const [singleHighlighted, setSingleHighlighted] = useState([]) const [isSingleComplete, setIsSingleComplete] = useState(false) const [singleAllowRepeat, setSingleAllowRepeat] = useState(false) // Common state const [isRolling, setIsRolling] = useState(false) const [errorMsg, setErrorMsg] = useState("") const [placeholder, setPlaceholder] = useState() const handleModeChange = useCallback((newMode: Mode) => { setMode(newMode) setHighlighted(new Set()) setUsedAll(new Set()) setGroups([]) setHasResult(false) setSelectedRecords([]) setSingleBatches([]) setSingleHighlighted([]) setIsSingleComplete(false) setErrorMsg("") setPlaceholder(undefined) }, []) const handleTotalChange = useCallback((v: number) => { setTotalTasks(v) setHighlighted(new Set()) setUsedAll(new Set()) setGroups([]) setHasResult(false) setSelectedRecords([]) setSingleBatches([]) setSingleHighlighted([]) setIsSingleComplete(false) setErrorMsg("") setPlaceholder(undefined) }, []) const handleRoundsChange = useCallback((v: number) => { setRounds(v) if (v < 2) setAllowRepeat(false) setHighlighted(new Set()) setGroups([]) setHasResult(false) setErrorMsg("") setPlaceholder("参数已修改,请重新执行") }, []) const handleRoll = useCallback(async () => { setErrorMsg("") setIsRolling(true) await new Promise((r) => setTimeout(r, 240)) const effectiveRepeat = rounds < 2 ? false : allowRepeat const result = generateGroups(totalTasks, pickCount, rounds, effectiveRepeat) if (!result.success) { setErrorMsg(result.errorMsg) setIsRolling(false) return } const allPicked = new Set(result.groups.flatMap(g => g.numbers)) setGroups(result.groups) setHighlighted(allPicked) if (!effectiveRepeat) { setUsedAll(allPicked) } else { setUsedAll(new Set()) } setHasResult(true) setIsRolling(false) setPlaceholder(undefined) }, [totalTasks, pickCount, rounds, allowRepeat]) const handleSingleRoll = useCallback(async () => { setErrorMsg("") setIsRolling(true) setIsSingleComplete(false) await new Promise((r) => setTimeout(r, 240)) const usedNumbers = new Set(selectedRecords.map(r => r.number)) // 如果不允许重复,检查剩余人数 if (!singleAllowRepeat && pickCount > totalTasks - usedNumbers.size) { setErrorMsg(`剩余可选人数不足 ${pickCount} 人`) setIsRolling(false) return } // 允许重复时,不需要检查人数 const forbidSet = singleAllowRepeat ? new Set() : usedNumbers const nums = getRandomDistinct(totalTasks, pickCount, forbidSet) if (!nums) { setErrorMsg("抽取失败,人数不足") setIsRolling(false) return } const newColorIndex = singleBatches.length % COLORS.length const newBatchNumber = singleBatches.length + 1 const newRecords = [...selectedRecords, ...nums.map(n => ({ number: n, colorIndex: newColorIndex }))] const newBatch = { numbers: nums, colorIndex: newColorIndex, batchNumber: newBatchNumber } setSelectedRecords(newRecords) setSingleBatches([...singleBatches, newBatch]) setSingleHighlighted(nums) setIsSingleComplete(true) setIsRolling(false) }, [totalTasks, pickCount, selectedRecords, singleBatches, singleAllowRepeat]) const resetSingle = useCallback(() => { setSelectedRecords([]) setSingleBatches([]) setSingleHighlighted([]) setIsSingleComplete(false) setErrorMsg("") }, []) const clearResult = useCallback(() => { setGroups([]) setHighlighted(new Set()) setUsedAll(new Set()) setHasResult(false) setPlaceholder(undefined) }, []) const getBallState = useCallback((n: number): "idle" | "highlighted" | "used" => { if (mode === "single") { // Only highlight current round's selections const isCurrentRound = singleHighlighted.includes(n) if (isCurrentRound) return "highlighted" return "idle" } if (highlighted.has(n)) return "highlighted" if (hasResult && usedAll.size > 0 && !usedAll.has(n)) return "used" return "idle" }, [mode, singleHighlighted, highlighted, hasResult, usedAll]) const getBallColorIndex = useCallback((n: number): number | undefined => { if (mode === "single") { // Find the LAST occurrence (latest color when repeats are allowed) const record = [...selectedRecords].reverse().find(r => r.number === n) return record?.colorIndex } // Group mode: find which group this number belongs to (use last matching group) for (let i = groups.length - 1; i >= 0; i--) { if (groups[i].numbers.includes(n)) { return groups[i].colorIndex } } return undefined }, [mode, selectedRecords, groups]) const getBallDimmed = useCallback((n: number): boolean => { // Only dim in single mode when there are highlighted numbers if (mode === "single" && singleHighlighted.length > 0) { return !singleHighlighted.includes(n) } return false }, [mode, singleHighlighted]) const state: RollCallState = { mode, totalTasks, pickCount, rounds, allowRepeat, groups, highlighted, usedAll, hasResult, selectedRecords, singleBatches, singleHighlighted, isSingleComplete, singleAllowRepeat, isRolling, errorMsg, placeholder, } const actions: RollCallActions = { setMode: handleModeChange, setTotalTasks: handleTotalChange, setPickCount: (v: number) => { setPickCount(v); setErrorMsg("") }, setRounds: handleRoundsChange, setAllowRepeat, setSingleAllowRepeat, handleRoll, handleSingleRoll, resetSingle, clearResult, getBallState, getBallColorIndex, getBallDimmed, } return [state, actions] }