diff --git a/README.md b/README.md index ff821f0..881eabd 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ - 每次固定產生 3 組完整配對結果 - 每一隊都由 `1 位 A 區 + 1 位 B 區` 組成 - 每一組都會把 A 區與 B 區名單完整分配完成 -- 若某一區人數不足,系統會自動補入「那個」 +- 兩區人數差 2 以上時,每輪會輪流讓人多的一區成員跨區支援 +- 跨區平衡後仍有缺口(總人數為奇數)時,才補入「那個」 - 產生配對後可用按鈕手動上傳資料到資料庫 - 若指定日期已經有資料,上傳前會先詢問是否覆蓋 - 可讀取指定日期的資料庫內容並回填到畫面 diff --git a/src/App.css b/src/App.css index 6b20956..b6663d8 100644 --- a/src/App.css +++ b/src/App.css @@ -287,18 +287,16 @@ color: #8d2d22; } -.round-list, .team-list { display: grid; gap: 14px; } -.share-row { - margin-bottom: 18px; -} - -.share-button { - min-width: 160px; +.round-list { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 18px; + align-items: start; } .round-card, @@ -368,6 +366,10 @@ grid-template-columns: 1fr; } + .round-list { + grid-template-columns: 1fr; + } + .hero-copy h1 { max-width: 10ch; } diff --git a/src/App.tsx b/src/App.tsx index 6a0dfec..7424037 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -72,9 +72,10 @@ function App() { const parsedAreaA = parseRoster(areaAInput) const parsedAreaB = parseRoster(areaBInput) - const teamCount = Math.max(parsedAreaA.length, parsedAreaB.length) - const placeholderCountA = Math.max(0, teamCount - parsedAreaA.length) - const placeholderCountB = Math.max(0, teamCount - parsedAreaB.length) + const zoneGap = Math.abs(parsedAreaA.length - parsedAreaB.length) + const crossCount = Math.floor(zoneGap / 2) + const placeholderCount = zoneGap % 2 + const teamCount = Math.ceil((parsedAreaA.length + parsedAreaB.length) / 2) function generateGroups() { if (parsedAreaA.length === 0 || parsedAreaB.length === 0) { @@ -85,10 +86,11 @@ function App() { return } - const prepared = prepareParticipants(parsedAreaA, parsedAreaB) + const shuffledA = shuffleList(parsedAreaA) + const shuffledB = shuffleList(parsedAreaB) const nextResults = Array.from({ length: TOTAL_ROUNDS }, (_, index) => ({ id: index + 1, - teams: createRoundTeams(prepared.areaA, prepared.areaB, index), + teams: createRoundTeams(shuffledA, shuffledB, index), })) setResults(nextResults) @@ -213,8 +215,8 @@ function App() {

羽毛球分組配對器

輸入 A 區與 B 區名單後,系統會產生 3 組完整配對。 - 每一隊都是 1 位 A 區成員搭配 1 位 B 區成員; - 若某一區人數不足,會自動補入「那個」。 + 兩區人數有落差時,每輪會輪流讓人多的一區成員跨區支援; + 補完後仍有缺口才會補入「那個」。

@@ -278,6 +280,16 @@ function App() { > 讀取指定日期 + {results.length > 0 ? ( + + ) : null}
@@ -312,19 +324,6 @@ function App() {

三組名單

- {results.length > 0 ? ( -
- -
- ) : null} - {results.length > 0 ? (
{results.map((round) => ( @@ -359,14 +358,30 @@ function App() { ) } -function prepareParticipants(areaA: string[], areaB: string[]) { - const targetCount = Math.max(areaA.length, areaB.length) - const shuffledA = shuffleList(areaA) - const shuffledB = shuffleList(areaB) +function balanceZones(areaA: string[], areaB: string[], roundIndex: number) { + const moveCount = Math.floor(Math.abs(areaA.length - areaB.length) / 2) + + if (moveCount === 0) { + return { areaA, areaB } + } + + if (areaA.length > areaB.length) { + const { moved, remaining } = pickCrossMembers(areaA, moveCount, roundIndex) + return { areaA: remaining, areaB: [...areaB, ...moved] } + } + + const { moved, remaining } = pickCrossMembers(areaB, moveCount, roundIndex) + return { areaA: [...areaA, ...moved], areaB: remaining } +} + +function pickCrossMembers(list: string[], moveCount: number, roundIndex: number) { + const movedIndexes = new Set( + Array.from({ length: moveCount }, (_, offset) => (roundIndex * moveCount + offset) % list.length), + ) return { - areaA: createParticipants(shuffledA, targetCount, 'A'), - areaB: createParticipants(shuffledB, targetCount, 'B'), + moved: list.filter((_, index) => movedIndexes.has(index)), + remaining: list.filter((_, index) => !movedIndexes.has(index)), } } @@ -388,11 +403,16 @@ function createParticipants(players: string[], targetCount: number, zone: 'A' | return participants } -function createRoundTeams(areaA: Participant[], areaB: Participant[], roundIndex: number) { - return areaA.map((playerA, index) => ({ +function createRoundTeams(areaA: string[], areaB: string[], roundIndex: number) { + const balanced = balanceZones(areaA, areaB, roundIndex) + const targetCount = Math.max(balanced.areaA.length, balanced.areaB.length) + const playersA = createParticipants(balanced.areaA, targetCount, 'A') + const playersB = createParticipants(balanced.areaB, targetCount, 'B') + + return playersA.map((playerA, index) => ({ id: index + 1, playerA, - playerB: areaB[(index + roundIndex) % areaB.length], + playerB: playersB[(index + roundIndex) % playersB.length], })) }