From 89acd1f261721bac0b70072957920202f7785780 Mon Sep 17 00:00:00 2001 From: Kegongteng Date: Tue, 16 Jun 2026 19:14:39 +0800 Subject: [PATCH] [perf] Refactor component --- app/page.tsx | 43 +++--- components/{result.tsx => group-result.tsx} | 28 ++-- components/logic.tsx | 138 ++++++++------------ components/matrix.tsx | 34 +++++ components/settings.tsx | 51 ++------ package-lock.json | 4 +- package.json | 2 +- src-tauri/tauri.conf.json | 2 +- 8 files changed, 137 insertions(+), 165 deletions(-) rename components/{result.tsx => group-result.tsx} (73%) create mode 100644 components/matrix.tsx diff --git a/app/page.tsx b/app/page.tsx index c99346b..5c3f78a 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,8 +1,8 @@ "use client" -import { NumberBall } from "@/components/number-ball" +import { Matrix } from "@/components/matrix" import { SettingsPanel } from "@/components/settings" -import { ResultPanel } from "@/components/result" +import { ResultPanel } from "@/components/group-result" import { SingleModePanel } from "@/components/single-panel" import { TitleBar } from "@/components/title-bar" import { useRollCallLogic } from "@/components/logic" @@ -74,18 +74,13 @@ export default function Page() { )}
-
- {Array.from({ length: typeof state.totalTasks === 'number' ? state.totalTasks : 0 }, (_, i) => i + 1).map((n) => ( - - ))} -
+
@@ -107,16 +102,14 @@ export default function Page() { )} -
- {Array.from({ length: typeof state.totalTasks === 'number' ? state.totalTasks : 0 }, (_, i) => i + 1).map((n) => ( - - ))} +
+
@@ -135,7 +128,7 @@ export default function Page() { )}
- +
diff --git a/components/result.tsx b/components/group-result.tsx similarity index 73% rename from components/result.tsx rename to components/group-result.tsx index a64061e..2d74bc6 100644 --- a/components/result.tsx +++ b/components/group-result.tsx @@ -1,23 +1,19 @@ "use client" -import { COLORS } from "./logic" +import { COLORS } from "@/components/logic" interface ResultPanelProps { groups: { numbers: number[]; colorIndex: number }[] hasResult: boolean - placeholder?: string } -export function ResultPanel({ groups, hasResult, placeholder }: ResultPanelProps) { +export function ResultPanel({ groups, hasResult }: ResultPanelProps) { if (!hasResult || groups.length === 0) { return (
— AWAITING INPUT —
- {placeholder && ( -
{placeholder}
- )}
) } @@ -49,6 +45,9 @@ export function ResultPanel({ groups, hasResult, placeholder }: ResultPanelProps rows.push(currentRow) } + // 预先建立索引映射 + const groupIndexMap = new Map(groups.map((g, i) => [g, i])) + return (
{rows.map((row, rowIdx) => ( @@ -57,34 +56,31 @@ export function ResultPanel({ groups, hasResult, placeholder }: ResultPanelProps className="flex gap-6 fui-slide-up" style={{ animationDelay: `${rowIdx * 40}ms` }} > - {row.map((group, idx) => { + {row.map((group) => { const sorted = [...group.numbers].sort((a, b) => a - b) const color = COLORS[group.colorIndex] - const globalIdx = groups.indexOf(group) + const globalIdx = groupIndexMap.get(group)! return (
{/* Group label with color */} -
+
- + G{String(globalIdx + 1).padStart(2, "0")} - - {String(group.numbers.length).padStart(2, "0")} P -
{/* Numbers with color */} -
+
{sorted.map((num) => ( = 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++ + // 构建可用数字数组 + const available: number[] = [] + for (let i = 1; i <= total; i++) { + if (!forbidSet.has(i)) available.push(i) } - return selected.size === count ? result : null + + if (count > available.length) return null + + // Fisher-Yates 洗牌算法 + for (let i = available.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[available[i], available[j]] = [available[j], available[i]] + } + + return available.slice(0, count) } function generateGroups( @@ -54,9 +56,9 @@ function generateGroups( export interface RollCallState { mode: Mode - totalTasks: number | string - pickCount: number | string - rounds: number | string + totalTasks: number + pickCount: number + rounds: number allowRepeat: boolean // Multi mode groups: { numbers: number[]; colorIndex: number }[] @@ -72,14 +74,13 @@ export interface RollCallState { // Common isRolling: boolean errorMsg: string - placeholder: string | undefined } export interface RollCallActions { setMode: (v: Mode) => void - setTotalTasks: (v: number | string) => void - setPickCount: (v: number | string) => void - setRounds: (v: number | string) => void + setTotalTasks: (v: number) => void + setPickCount: (v: number) => void + setRounds: (v: number) => void setAllowRepeat: (v: boolean) => void setSingleAllowRepeat: (v: boolean) => void handleRoll: () => void @@ -113,9 +114,9 @@ export const COLORS = [ 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 [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 }[]>([]) @@ -123,7 +124,6 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] { 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) @@ -131,39 +131,38 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] { // Common state const [isRolling, setIsRolling] = useState(false) const [errorMsg, setErrorMsg] = useState("") - const [placeholder, setPlaceholder] = useState() + + // 从 singleBatches 计算 selectedRecords,消除数据冗余 + const selectedRecords = useMemo(() => { + return singleBatches.flatMap(batch => + batch.numbers.map(n => ({ number: n, colorIndex: batch.colorIndex })) + ) + }, [singleBatches]) + + const resetAllState = useCallback(() => { + setHighlighted(new Set()) + setUsedAll(new Set()) + setGroups([]) + setHasResult(false) + setSingleBatches([]) + setSingleHighlighted([]) + setIsSingleComplete(false) + setErrorMsg("") + }, []) const handleModeChange = useCallback((newMode: Mode) => { setMode(newMode) - setHighlighted(new Set()) - setUsedAll(new Set()) - setGroups([]) - setHasResult(false) - setSelectedRecords([]) - setSingleBatches([]) - setSingleHighlighted([]) - setIsSingleComplete(false) - setErrorMsg("") - setPlaceholder(undefined) - }, []) + resetAllState() + }, [resetAllState]) - const handleTotalChange = useCallback((v: number | string) => { + const handleTotalChange = useCallback((v: number) => { setTotalTasks(v) - setHighlighted(new Set()) - setUsedAll(new Set()) - setGroups([]) - setHasResult(false) - setSelectedRecords([]) - setSingleBatches([]) - setSingleHighlighted([]) - setIsSingleComplete(false) - setErrorMsg("") - setPlaceholder(undefined) - }, []) + resetAllState() + }, [resetAllState]) - const handleRoundsChange = useCallback((v: number | string) => { + const handleRoundsChange = useCallback((v: number) => { setRounds(v) - if (typeof v === 'number' && v < 2) setAllowRepeat(false) + if (v < 2) setAllowRepeat(false) setHighlighted(new Set()) setGroups([]) setHasResult(false) @@ -173,21 +172,11 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] { const handleRoll = useCallback(async () => { setErrorMsg("") - // 检查空值 - if (totalTasks === "" || pickCount === "" || rounds === "") { - setErrorMsg("请填写所有参数") - return - } - - const total = totalTasks as number - const pick = pickCount as number - const roundCount = rounds as number - setIsRolling(true) await new Promise((r) => setTimeout(r, 240)) - const effectiveRepeat = roundCount < 2 ? false : allowRepeat - const result = generateGroups(total, pick, roundCount, effectiveRepeat) + const effectiveRepeat = rounds < 2 ? false : allowRepeat + const result = generateGroups(totalTasks, pickCount, rounds, effectiveRepeat) if (!result.success) { setErrorMsg(result.errorMsg) @@ -205,37 +194,25 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] { } setHasResult(true) setIsRolling(false) - setPlaceholder(undefined) }, [totalTasks, pickCount, rounds, allowRepeat]) const handleSingleRoll = useCallback(async () => { setErrorMsg("") - // 检查空值 - if (totalTasks === "" || pickCount === "") { - setErrorMsg("请填写所有参数") - return - } - - const total = totalTasks as number - const pick = pickCount as number - setIsRolling(true) setIsSingleComplete(false) await new Promise((r) => setTimeout(r, 240)) const usedNumbers = new Set(selectedRecords.map(r => r.number)) - // 如果不允许重复,检查剩余人数 - if (!singleAllowRepeat && pick > total - usedNumbers.size) { - setErrorMsg(`剩余可选人数不足 ${pick} 人`) + if (!singleAllowRepeat && pickCount > totalTasks - usedNumbers.size) { + setErrorMsg(`剩余可选人数不足 ${pickCount} 人`) setIsRolling(false) return } - // 允许重复时,不需要检查人数 const forbidSet = singleAllowRepeat ? new Set() : usedNumbers - const nums = getRandomDistinct(total, pick, forbidSet) + const nums = getRandomDistinct(totalTasks, pickCount, forbidSet) if (!nums) { setErrorMsg("抽取失败,人数不足") setIsRolling(false) @@ -244,10 +221,8 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] { 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) @@ -255,7 +230,6 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] { }, [totalTasks, pickCount, selectedRecords, singleBatches, singleAllowRepeat]) const resetSingle = useCallback(() => { - setSelectedRecords([]) setSingleBatches([]) setSingleHighlighted([]) setIsSingleComplete(false) @@ -267,7 +241,6 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] { setHighlighted(new Set()) setUsedAll(new Set()) setHasResult(false) - setPlaceholder(undefined) }, []) const getBallState = useCallback((n: number): "idle" | "highlighted" | "used" => { @@ -322,13 +295,12 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] { singleAllowRepeat, isRolling, errorMsg, - placeholder, } const actions: RollCallActions = { setMode: handleModeChange, setTotalTasks: handleTotalChange, - setPickCount: (v: number | string) => { setPickCount(v); setErrorMsg("") }, + setPickCount: (v: number) => { setPickCount(v); setErrorMsg("") }, setRounds: handleRoundsChange, setAllowRepeat, setSingleAllowRepeat, diff --git a/components/matrix.tsx b/components/matrix.tsx new file mode 100644 index 0000000..b7de3a7 --- /dev/null +++ b/components/matrix.tsx @@ -0,0 +1,34 @@ +"use client" + +import { NumberBall } from "./number-ball" + +interface MatrixProps { + total: number + getBallState: (n: number) => "idle" | "highlighted" | "used" + getBallColorIndex: (n: number) => number | undefined + getBallDimmed: (n: number) => boolean + size?: "normal" | "large" | "xlarge" +} + +export function Matrix({ + total, + getBallState, + getBallColorIndex, + getBallDimmed, + size = "normal" +}: MatrixProps) { + return ( +
+ {Array.from({ length: total }, (_, i) => i + 1).map((n) => ( + + ))} +
+ ) +} \ No newline at end of file diff --git a/components/settings.tsx b/components/settings.tsx index df979dd..1ea7974 100644 --- a/components/settings.tsx +++ b/components/settings.tsx @@ -5,16 +5,16 @@ import type { Mode } from "@/components/logic" interface SettingsPanelProps { mode: Mode - totalTasks: number | string - pickCount: number | string - rounds: number | string + totalTasks: number + pickCount: number + rounds: number allowRepeat: boolean singleAllowRepeat: boolean selectedRecords: { number: number; colorIndex: number }[] onModeChange: (v: Mode) => void - onTotalTasksChange: (v: number | string) => void - onPickCountChange: (v: number | string) => void - onRoundsChange: (v: number | string) => void + onTotalTasksChange: (v: number) => void + onPickCountChange: (v: number) => void + onRoundsChange: (v: number) => void onAllowRepeatChange: (v: boolean) => void onSingleAllowRepeatChange: (v: boolean) => void onRoll: () => void @@ -29,18 +29,13 @@ function FieldRow({ value, onChange, min = 1, - error, }: { label: string hint: string - value: number | string - onChange: (v: number | string) => void + value: number + onChange: (v: number) => void min?: number - error?: string }) { - const displayValue = value === "" ? "" : value - const hasError = error && value === "" - return (
@@ -49,13 +44,7 @@ function FieldRow({
{ - const inputValue = e.target.value - if (inputValue === "") { - onChange("") - } else { - const v = parseInt(inputValue, 10) - if (!isNaN(v)) onChange(v) - } + const v = parseInt(e.target.value, 10) + if (!isNaN(v)) onChange(v) }} className={cn( "w-12 h-7 text-center text-sm font-mono bg-[#f7f8fa]", "border-x border-[#e2e6ea]", "focus:outline-none focus:bg-[#ffffff]", - "[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none", - hasError && "text-[#d92d20] border-[#d92d20]" + "[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none" )} />