From 8cc05183f766500ad21ff319f861a7b869a52dc2 Mon Sep 17 00:00:00 2001 From: JianMiau Date: Mon, 10 Aug 2026 15:22:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=A9=E5=8D=80=E4=BA=BA=E6=95=B8=E8=90=BD?= =?UTF-8?q?=E5=B7=AE=E6=99=82=E6=94=B9=E7=82=BA=E8=B7=A8=E5=8D=80=E5=B9=B3?= =?UTF-8?q?=E8=A1=A1=EF=BC=8C=E4=B8=A6=E8=AA=BF=E6=95=B4=E7=B5=90=E6=9E=9C?= =?UTF-8?q?=E7=89=88=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根本原因: 原本配對邏輯只以人數多的一區為基準,缺額全部補「那個」。例如 A 區 7 人、B 區 5 人時會補 2 個「那個」,但總人數 12 是偶數,其實可以讓 A 區 1 人跨區支援就湊滿 6 隊真人對打。 影響: - 兩區差距 2 以上時,每輪自動抽「差距÷2」人跨區支援,且每輪輪替不同人,跨區的人平均分攤 - 只有總人數為奇數時才會補 1 個「那個」 - 三輪之間維持無重複搭檔、同輪無人重複上場 - 「推送到 LINE」按鈕移到上方操作列,三組結果改為三欄並排顯示 - 摘要卡改顯示跨區支援與補位人數 修法: 新增 balanceZones / pickCrossMembers 依輪次輪替抽出跨區成員,createRoundTeams 改在每輪平衡後再建立隊伍;移除不再使用的 prepareParticipants 與 .share-row/.share-button 樣式,並同步更新 README 規則說明。 Co-Authored-By: Claude Fable 5 --- README.md | 3 +- src/App.css | 16 ++++++----- src/App.tsx | 80 +++++++++++++++++++++++++++++++++-------------------- 3 files changed, 61 insertions(+), 38 deletions(-) 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], })) }