Initialization

This commit is contained in:
He
2026-06-15 19:01:02 +08:00
commit 325c267f0c
28 changed files with 12087 additions and 0 deletions
+305
View File
@@ -0,0 +1,305 @@
"use client"
import { useState, useCallback } from "react"
export type Mode = "single" | "multi"
function getRandomDistinct(total: number, count: number, forbidSet: Set<number> = new Set()): number[] | null {
if (count > total - forbidSet.size) return null
const result: number[] = []
const selected = new Set<number>()
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<number>()
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<number>
usedAll: Set<number>
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
}
export const COLORS = [
"#0f141a", // 黑色
"#d92d20", // 红色
"#0070f3", // 蓝色
"#6d6df6", // 紫色
"#00b8d9", // 青色
"#36b37e", // 绿色
"#ff991f", // 橙色
"#8754ad", // 深紫
]
export function useRollCallLogic(): [RollCallState, RollCallActions] {
const [mode, setMode] = useState<Mode>("single")
const [totalTasks, setTotalTasks] = useState(20)
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<Set<number>>(new Set())
const [usedAll, setUsedAll] = useState<Set<number>>(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<number[]>([])
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<string | undefined>()
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<number>() : 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") {
const isInRecords = selectedRecords.some(r => r.number === n)
if (isInRecords) return "highlighted"
return "idle"
}
if (highlighted.has(n)) return "highlighted"
if (hasResult && usedAll.size > 0 && !usedAll.has(n)) return "used"
return "idle"
}, [mode, selectedRecords, 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 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,
}
return [state, actions]
}