Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
89acd1f261
|
|||
|
b86716e952
|
|||
|
b926a6e30d
|
@@ -86,9 +86,15 @@
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground font-mono antialiased;
|
@apply bg-background text-foreground font-mono antialiased;
|
||||||
|
/* 禁用右键菜单 */
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
html {
|
html {
|
||||||
@apply bg-background;
|
@apply bg-background;
|
||||||
|
/* 禁用触摸移动 */
|
||||||
|
overscroll-behavior: none;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-27
@@ -1,16 +1,28 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { NumberBall } from "@/components/number-ball"
|
import { Matrix } from "@/components/matrix"
|
||||||
import { SettingsPanel } from "@/components/settings"
|
import { SettingsPanel } from "@/components/settings"
|
||||||
import { ResultPanel } from "@/components/result"
|
import { ResultPanel } from "@/components/group-result"
|
||||||
import { SingleModePanel } from "@/components/single-panel"
|
import { SingleModePanel } from "@/components/single-panel"
|
||||||
import { TitleBar } from "@/components/title-bar"
|
import { TitleBar } from "@/components/title-bar"
|
||||||
import { useRollCallLogic } from "@/components/logic"
|
import { useRollCallLogic } from "@/components/logic"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [state, actions] = useRollCallLogic()
|
const [state, actions] = useRollCallLogic()
|
||||||
const isSingleMode = state.mode === "single"
|
const isSingleMode = state.mode === "single"
|
||||||
|
|
||||||
|
// 禁用右键菜单
|
||||||
|
useEffect(() => {
|
||||||
|
const handleContextMenu = (e: MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
document.addEventListener('contextmenu', handleContextMenu)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('contextmenu', handleContextMenu)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full bg-[#0f141a] text-[#16191f] font-mono flex flex-col overflow-hidden">
|
<div className="h-screen w-full bg-[#0f141a] text-[#16191f] font-mono flex flex-col overflow-hidden">
|
||||||
{/* Custom title bar */}
|
{/* Custom title bar */}
|
||||||
@@ -61,19 +73,14 @@ export default function Page() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex items-center justify-center overflow-auto scrollbar-thin p-8">
|
<div className="flex-1 flex items-center justify-center overflow-auto scrollbar-thin p-3">
|
||||||
<div className="flex flex-wrap gap-2 justify-center max-w-2xl">
|
<Matrix
|
||||||
{Array.from({ length: state.totalTasks }, (_, i) => i + 1).map((n) => (
|
total={state.totalTasks}
|
||||||
<NumberBall
|
getBallState={actions.getBallState}
|
||||||
key={n}
|
getBallColorIndex={actions.getBallColorIndex}
|
||||||
number={n}
|
getBallDimmed={actions.getBallDimmed}
|
||||||
state={actions.getBallState(n)}
|
size="xlarge"
|
||||||
colorIndex={actions.getBallColorIndex(n)}
|
/>
|
||||||
dimmed={actions.getBallDimmed(n)}
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -95,22 +102,20 @@ export default function Page() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2 p-4 max-h-[40vh] overflow-y-auto scrollbar-thin">
|
<div className="p-3 max-h-[40vh] overflow-y-auto scrollbar-thin">
|
||||||
{Array.from({ length: state.totalTasks }, (_, i) => i + 1).map((n) => (
|
<Matrix
|
||||||
<NumberBall
|
total={state.totalTasks}
|
||||||
key={n}
|
getBallState={actions.getBallState}
|
||||||
number={n}
|
getBallColorIndex={actions.getBallColorIndex}
|
||||||
state={actions.getBallState(n)}
|
getBallDimmed={actions.getBallDimmed}
|
||||||
colorIndex={actions.getBallColorIndex(n)}
|
size="large"
|
||||||
size="large"
|
/>
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Group mode result panel */}
|
{/* Group mode result panel */}
|
||||||
<section className="flex flex-col flex-1 min-h-0">
|
<section className="flex flex-col flex-1 min-h-0">
|
||||||
<div className="flex items-center gap-3 px-5 py-2.5 border-b border-[#e2e6ea] flex-shrink-0">
|
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-[#e2e6ea] flex-shrink-0">
|
||||||
<span className="text-[10px] tracking-[0.2em] uppercase text-[#8a95a1]">OUTPUT</span>
|
<span className="text-[10px] tracking-[0.2em] uppercase text-[#8a95a1]">OUTPUT</span>
|
||||||
<span className="flex-1 h-px bg-[#e2e6ea]" />
|
<span className="flex-1 h-px bg-[#e2e6ea]" />
|
||||||
{state.hasResult && state.groups.length > 0 && (
|
{state.hasResult && state.groups.length > 0 && (
|
||||||
@@ -123,7 +128,7 @@ export default function Page() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto scrollbar-thin px-4 py-3">
|
<div className="flex-1 overflow-y-auto scrollbar-thin px-4 py-3">
|
||||||
<ResultPanel groups={state.groups} hasResult={state.hasResult} placeholder={state.placeholder} />
|
<ResultPanel groups={state.groups} hasResult={state.hasResult} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { COLORS } from "@/components/logic"
|
||||||
|
|
||||||
|
interface ResultPanelProps {
|
||||||
|
groups: { numbers: number[]; colorIndex: number }[]
|
||||||
|
hasResult: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResultPanel({ groups, hasResult }: ResultPanelProps) {
|
||||||
|
if (!hasResult || groups.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full min-h-40 gap-3">
|
||||||
|
<div className="text-[10px] tracking-[0.2em] text-[#cdd2d8] font-mono uppercase">
|
||||||
|
— AWAITING INPUT —
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将groups分成pairs,每组人数>=15时单独一行
|
||||||
|
const rows: { numbers: number[]; colorIndex: number }[][] = []
|
||||||
|
let currentRow: { numbers: number[]; colorIndex: number }[] = []
|
||||||
|
|
||||||
|
for (const group of groups) {
|
||||||
|
if (group.numbers.length >= 15) {
|
||||||
|
// 单独一行
|
||||||
|
if (currentRow.length > 0) {
|
||||||
|
rows.push(currentRow)
|
||||||
|
currentRow = []
|
||||||
|
}
|
||||||
|
rows.push([group])
|
||||||
|
} else {
|
||||||
|
// 尝试凑成一行两组
|
||||||
|
currentRow.push(group)
|
||||||
|
if (currentRow.length === 2) {
|
||||||
|
rows.push(currentRow)
|
||||||
|
currentRow = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理剩余的组
|
||||||
|
if (currentRow.length > 0) {
|
||||||
|
rows.push(currentRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预先建立索引映射
|
||||||
|
const groupIndexMap = new Map(groups.map((g, i) => [g, i]))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{rows.map((row, rowIdx) => (
|
||||||
|
<div
|
||||||
|
key={rowIdx}
|
||||||
|
className="flex gap-6 fui-slide-up"
|
||||||
|
style={{ animationDelay: `${rowIdx * 40}ms` }}
|
||||||
|
>
|
||||||
|
{row.map((group) => {
|
||||||
|
const sorted = [...group.numbers].sort((a, b) => a - b)
|
||||||
|
const color = COLORS[group.colorIndex]
|
||||||
|
const globalIdx = groupIndexMap.get(group)!
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={globalIdx}
|
||||||
|
className="flex-1 min-w-0"
|
||||||
|
>
|
||||||
|
{/* Group label with color */}
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-mono uppercase tracking-[0.15em]" style={{ color }}>
|
||||||
|
G{String(globalIdx + 1).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Numbers with color */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{sorted.map((num) => (
|
||||||
|
<span
|
||||||
|
key={num}
|
||||||
|
className="min-w-10 h-10 px-2 flex items-center justify-center text-base font-mono border fui-fadein"
|
||||||
|
style={{
|
||||||
|
borderColor: color + "40",
|
||||||
|
backgroundColor: color + "15",
|
||||||
|
color: color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{String(num).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+42
-50
@@ -1,23 +1,25 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useCallback } from "react"
|
import { useState, useCallback, useMemo } from "react"
|
||||||
|
|
||||||
export type Mode = "single" | "multi"
|
export type Mode = "single" | "multi"
|
||||||
|
|
||||||
function getRandomDistinct(total: number, count: number, forbidSet: Set<number> = new Set()): number[] | null {
|
function getRandomDistinct(total: number, count: number, forbidSet: Set<number> = new Set()): number[] | null {
|
||||||
if (count > total - forbidSet.size) return null
|
// 构建可用数字数组
|
||||||
const result: number[] = []
|
const available: number[] = []
|
||||||
const selected = new Set<number>()
|
for (let i = 1; i <= total; i++) {
|
||||||
let attempts = 0
|
if (!forbidSet.has(i)) available.push(i)
|
||||||
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
|
|
||||||
|
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(
|
function generateGroups(
|
||||||
@@ -72,7 +74,6 @@ export interface RollCallState {
|
|||||||
// Common
|
// Common
|
||||||
isRolling: boolean
|
isRolling: boolean
|
||||||
errorMsg: string
|
errorMsg: string
|
||||||
placeholder: string | undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RollCallActions {
|
export interface RollCallActions {
|
||||||
@@ -113,9 +114,9 @@ export const COLORS = [
|
|||||||
|
|
||||||
export function useRollCallLogic(): [RollCallState, RollCallActions] {
|
export function useRollCallLogic(): [RollCallState, RollCallActions] {
|
||||||
const [mode, setMode] = useState<Mode>("single")
|
const [mode, setMode] = useState<Mode>("single")
|
||||||
const [totalTasks, setTotalTasks] = useState(35)
|
const [totalTasks, setTotalTasks] = useState<number>(35)
|
||||||
const [pickCount, setPickCount] = useState(1)
|
const [pickCount, setPickCount] = useState<number>(1)
|
||||||
const [rounds, setRounds] = useState(1)
|
const [rounds, setRounds] = useState<number>(1)
|
||||||
const [allowRepeat, setAllowRepeat] = useState(false)
|
const [allowRepeat, setAllowRepeat] = useState(false)
|
||||||
// Multi mode state
|
// Multi mode state
|
||||||
const [groups, setGroups] = useState<{ numbers: number[]; colorIndex: number }[]>([])
|
const [groups, setGroups] = useState<{ numbers: number[]; colorIndex: number }[]>([])
|
||||||
@@ -123,7 +124,6 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] {
|
|||||||
const [usedAll, setUsedAll] = useState<Set<number>>(new Set())
|
const [usedAll, setUsedAll] = useState<Set<number>>(new Set())
|
||||||
const [hasResult, setHasResult] = useState(false)
|
const [hasResult, setHasResult] = useState(false)
|
||||||
// Single mode state
|
// Single mode state
|
||||||
const [selectedRecords, setSelectedRecords] = useState<{ number: number; colorIndex: number }[]>([])
|
|
||||||
const [singleBatches, setSingleBatches] = useState<{ numbers: number[]; colorIndex: number; batchNumber: number }[]>([])
|
const [singleBatches, setSingleBatches] = useState<{ numbers: number[]; colorIndex: number; batchNumber: number }[]>([])
|
||||||
const [singleHighlighted, setSingleHighlighted] = useState<number[]>([])
|
const [singleHighlighted, setSingleHighlighted] = useState<number[]>([])
|
||||||
const [isSingleComplete, setIsSingleComplete] = useState(false)
|
const [isSingleComplete, setIsSingleComplete] = useState(false)
|
||||||
@@ -131,35 +131,34 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] {
|
|||||||
// Common state
|
// Common state
|
||||||
const [isRolling, setIsRolling] = useState(false)
|
const [isRolling, setIsRolling] = useState(false)
|
||||||
const [errorMsg, setErrorMsg] = useState("")
|
const [errorMsg, setErrorMsg] = useState("")
|
||||||
const [placeholder, setPlaceholder] = useState<string | undefined>()
|
|
||||||
|
// 从 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) => {
|
const handleModeChange = useCallback((newMode: Mode) => {
|
||||||
setMode(newMode)
|
setMode(newMode)
|
||||||
setHighlighted(new Set())
|
resetAllState()
|
||||||
setUsedAll(new Set())
|
}, [resetAllState])
|
||||||
setGroups([])
|
|
||||||
setHasResult(false)
|
|
||||||
setSelectedRecords([])
|
|
||||||
setSingleBatches([])
|
|
||||||
setSingleHighlighted([])
|
|
||||||
setIsSingleComplete(false)
|
|
||||||
setErrorMsg("")
|
|
||||||
setPlaceholder(undefined)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleTotalChange = useCallback((v: number) => {
|
const handleTotalChange = useCallback((v: number) => {
|
||||||
setTotalTasks(v)
|
setTotalTasks(v)
|
||||||
setHighlighted(new Set())
|
resetAllState()
|
||||||
setUsedAll(new Set())
|
}, [resetAllState])
|
||||||
setGroups([])
|
|
||||||
setHasResult(false)
|
|
||||||
setSelectedRecords([])
|
|
||||||
setSingleBatches([])
|
|
||||||
setSingleHighlighted([])
|
|
||||||
setIsSingleComplete(false)
|
|
||||||
setErrorMsg("")
|
|
||||||
setPlaceholder(undefined)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleRoundsChange = useCallback((v: number) => {
|
const handleRoundsChange = useCallback((v: number) => {
|
||||||
setRounds(v)
|
setRounds(v)
|
||||||
@@ -168,11 +167,11 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] {
|
|||||||
setGroups([])
|
setGroups([])
|
||||||
setHasResult(false)
|
setHasResult(false)
|
||||||
setErrorMsg("")
|
setErrorMsg("")
|
||||||
setPlaceholder("参数已修改,请重新执行")
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleRoll = useCallback(async () => {
|
const handleRoll = useCallback(async () => {
|
||||||
setErrorMsg("")
|
setErrorMsg("")
|
||||||
|
|
||||||
setIsRolling(true)
|
setIsRolling(true)
|
||||||
await new Promise((r) => setTimeout(r, 240))
|
await new Promise((r) => setTimeout(r, 240))
|
||||||
|
|
||||||
@@ -195,25 +194,23 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] {
|
|||||||
}
|
}
|
||||||
setHasResult(true)
|
setHasResult(true)
|
||||||
setIsRolling(false)
|
setIsRolling(false)
|
||||||
setPlaceholder(undefined)
|
|
||||||
}, [totalTasks, pickCount, rounds, allowRepeat])
|
}, [totalTasks, pickCount, rounds, allowRepeat])
|
||||||
|
|
||||||
const handleSingleRoll = useCallback(async () => {
|
const handleSingleRoll = useCallback(async () => {
|
||||||
setErrorMsg("")
|
setErrorMsg("")
|
||||||
|
|
||||||
setIsRolling(true)
|
setIsRolling(true)
|
||||||
setIsSingleComplete(false)
|
setIsSingleComplete(false)
|
||||||
await new Promise((r) => setTimeout(r, 240))
|
await new Promise((r) => setTimeout(r, 240))
|
||||||
|
|
||||||
const usedNumbers = new Set(selectedRecords.map(r => r.number))
|
const usedNumbers = new Set(selectedRecords.map(r => r.number))
|
||||||
|
|
||||||
// 如果不允许重复,检查剩余人数
|
|
||||||
if (!singleAllowRepeat && pickCount > totalTasks - usedNumbers.size) {
|
if (!singleAllowRepeat && pickCount > totalTasks - usedNumbers.size) {
|
||||||
setErrorMsg(`剩余可选人数不足 ${pickCount} 人`)
|
setErrorMsg(`剩余可选人数不足 ${pickCount} 人`)
|
||||||
setIsRolling(false)
|
setIsRolling(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 允许重复时,不需要检查人数
|
|
||||||
const forbidSet = singleAllowRepeat ? new Set<number>() : usedNumbers
|
const forbidSet = singleAllowRepeat ? new Set<number>() : usedNumbers
|
||||||
const nums = getRandomDistinct(totalTasks, pickCount, forbidSet)
|
const nums = getRandomDistinct(totalTasks, pickCount, forbidSet)
|
||||||
if (!nums) {
|
if (!nums) {
|
||||||
@@ -224,10 +221,8 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] {
|
|||||||
|
|
||||||
const newColorIndex = singleBatches.length % COLORS.length
|
const newColorIndex = singleBatches.length % COLORS.length
|
||||||
const newBatchNumber = singleBatches.length + 1
|
const newBatchNumber = singleBatches.length + 1
|
||||||
const newRecords = [...selectedRecords, ...nums.map(n => ({ number: n, colorIndex: newColorIndex }))]
|
|
||||||
const newBatch = { numbers: nums, colorIndex: newColorIndex, batchNumber: newBatchNumber }
|
const newBatch = { numbers: nums, colorIndex: newColorIndex, batchNumber: newBatchNumber }
|
||||||
|
|
||||||
setSelectedRecords(newRecords)
|
|
||||||
setSingleBatches([...singleBatches, newBatch])
|
setSingleBatches([...singleBatches, newBatch])
|
||||||
setSingleHighlighted(nums)
|
setSingleHighlighted(nums)
|
||||||
setIsSingleComplete(true)
|
setIsSingleComplete(true)
|
||||||
@@ -235,7 +230,6 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] {
|
|||||||
}, [totalTasks, pickCount, selectedRecords, singleBatches, singleAllowRepeat])
|
}, [totalTasks, pickCount, selectedRecords, singleBatches, singleAllowRepeat])
|
||||||
|
|
||||||
const resetSingle = useCallback(() => {
|
const resetSingle = useCallback(() => {
|
||||||
setSelectedRecords([])
|
|
||||||
setSingleBatches([])
|
setSingleBatches([])
|
||||||
setSingleHighlighted([])
|
setSingleHighlighted([])
|
||||||
setIsSingleComplete(false)
|
setIsSingleComplete(false)
|
||||||
@@ -247,7 +241,6 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] {
|
|||||||
setHighlighted(new Set())
|
setHighlighted(new Set())
|
||||||
setUsedAll(new Set())
|
setUsedAll(new Set())
|
||||||
setHasResult(false)
|
setHasResult(false)
|
||||||
setPlaceholder(undefined)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const getBallState = useCallback((n: number): "idle" | "highlighted" | "used" => {
|
const getBallState = useCallback((n: number): "idle" | "highlighted" | "used" => {
|
||||||
@@ -302,7 +295,6 @@ export function useRollCallLogic(): [RollCallState, RollCallActions] {
|
|||||||
singleAllowRepeat,
|
singleAllowRepeat,
|
||||||
isRolling,
|
isRolling,
|
||||||
errorMsg,
|
errorMsg,
|
||||||
placeholder,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions: RollCallActions = {
|
const actions: RollCallActions = {
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="flex flex-wrap gap-2 justify-center">
|
||||||
|
{Array.from({ length: total }, (_, i) => i + 1).map((n) => (
|
||||||
|
<NumberBall
|
||||||
|
key={n}
|
||||||
|
number={n}
|
||||||
|
state={getBallState(n)}
|
||||||
|
colorIndex={getBallColorIndex(n)}
|
||||||
|
dimmed={getBallDimmed(n)}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ interface NumberBallProps {
|
|||||||
number: number
|
number: number
|
||||||
state: "idle" | "highlighted" | "used"
|
state: "idle" | "highlighted" | "used"
|
||||||
colorIndex?: number
|
colorIndex?: number
|
||||||
size?: "normal" | "large"
|
size?: "normal" | "large" | "xlarge"
|
||||||
dimmed?: boolean
|
dimmed?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ export function NumberBall({ number, state, colorIndex, size = "normal", dimmed
|
|||||||
"flex items-center justify-center font-mono select-none transition-all duration-150",
|
"flex items-center justify-center font-mono select-none transition-all duration-150",
|
||||||
size === "normal" && "w-8 h-8 text-sm",
|
size === "normal" && "w-8 h-8 text-sm",
|
||||||
size === "large" && "w-14 h-14 text-lg",
|
size === "large" && "w-14 h-14 text-lg",
|
||||||
|
size === "xlarge" && "w-16 h-16 text-xl",
|
||||||
dimmed && !isHighlighted && "opacity-30",
|
dimmed && !isHighlighted && "opacity-30",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { COLORS } from "./logic"
|
|
||||||
|
|
||||||
interface ResultPanelProps {
|
|
||||||
groups: { numbers: number[]; colorIndex: number }[]
|
|
||||||
hasResult: boolean
|
|
||||||
placeholder?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ResultPanel({ groups, hasResult, placeholder }: ResultPanelProps) {
|
|
||||||
if (!hasResult || groups.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center h-full min-h-40 gap-3">
|
|
||||||
<div className="text-[10px] tracking-[0.2em] text-[#cdd2d8] font-mono uppercase">
|
|
||||||
— AWAITING INPUT —
|
|
||||||
</div>
|
|
||||||
{placeholder && (
|
|
||||||
<div className="text-xs text-[#8a95a1] font-mono">{placeholder}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
{groups.map((group, idx) => {
|
|
||||||
const sorted = [...group.numbers].sort((a, b) => a - b)
|
|
||||||
const color = COLORS[group.colorIndex]
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="flex items-start gap-4 py-2.5 border-b border-[#e2e6ea] last:border-b-0 fui-slide-up"
|
|
||||||
style={{ animationDelay: `${idx * 40}ms` }}
|
|
||||||
>
|
|
||||||
{/* Group label with color */}
|
|
||||||
<div className="flex flex-col gap-0.5 w-14 flex-shrink-0 pt-0.5">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className="w-2 h-2 rounded-full"
|
|
||||||
style={{ backgroundColor: color }}
|
|
||||||
/>
|
|
||||||
<span className="text-xs font-mono uppercase tracking-[0.15em]" style={{ color }}>
|
|
||||||
G{String(idx + 1).padStart(2, "0")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] font-mono text-[#8a95a1]">
|
|
||||||
{String(group.numbers.length).padStart(2, "0")} P
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/* Numbers with color */}
|
|
||||||
<div className="flex flex-wrap gap-1.5 flex-1 min-w-0">
|
|
||||||
{sorted.map((num) => (
|
|
||||||
<span
|
|
||||||
key={num}
|
|
||||||
className="min-w-8 h-8 px-1.5 flex items-center justify-center text-sm font-mono border fui-fadein"
|
|
||||||
style={{
|
|
||||||
borderColor: color + "40",
|
|
||||||
backgroundColor: color + "15",
|
|
||||||
color: color,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{String(num).padStart(2, "0")}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -56,10 +56,10 @@ function FieldRow({
|
|||||||
min={min}
|
min={min}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const v = parseInt(e.target.value, 10)
|
const v = parseInt(e.target.value, 10)
|
||||||
if (!isNaN(v) && v >= min) onChange(v)
|
if (!isNaN(v)) onChange(v)
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-12 h-7 text-center text-sm font-mono text-[#16191f] bg-[#f7f8fa]",
|
"w-12 h-7 text-center text-sm font-mono bg-[#f7f8fa]",
|
||||||
"border-x border-[#e2e6ea]",
|
"border-x border-[#e2e6ea]",
|
||||||
"focus:outline-none focus:bg-[#ffffff]",
|
"focus:outline-none focus:bg-[#ffffff]",
|
||||||
"[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none"
|
"[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
@@ -96,7 +96,8 @@ export function SettingsPanel({
|
|||||||
isRolling,
|
isRolling,
|
||||||
errorMsg,
|
errorMsg,
|
||||||
}: SettingsPanelProps) {
|
}: SettingsPanelProps) {
|
||||||
const repeatDisabled = rounds < 2
|
// 只有当 rounds 是数字且小于 2 时才禁用重复选项
|
||||||
|
const repeatDisabled = typeof rounds === 'number' && rounds < 2
|
||||||
const isSingleMode = mode === "single"
|
const isSingleMode = mode === "single"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
Generated
+4
-4
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "my-project",
|
"name": "roll-call",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "my-project",
|
"name": "roll-call",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.5.0",
|
"@base-ui/react": "^1.5.0",
|
||||||
"@tauri-apps/api": "^2.11.0",
|
"@tauri-apps/api": "^2.11.0",
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-project",
|
"name": "roll-call",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"productName": "Roll Call",
|
"productName": "Roll Call",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"identifier": "dev.zhhe.rollcall",
|
"identifier": "dev.zhhe.rollcall",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../out",
|
"frontendDist": "../out",
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"resizable": true,
|
"resizable": true,
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
"decorations": false,
|
"decorations": false,
|
||||||
|
"shadow": true,
|
||||||
"transparent": true
|
"transparent": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user