From 37fa1446af2c70f4272a561bdc2d4430e40df4b0 Mon Sep 17 00:00:00 2001 From: JianMiau Date: Mon, 10 Aug 2026 15:22:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E9=A6=96=E9=A0=81?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=85=B6=E4=BB=96=E5=8D=80=E5=90=8D=E5=96=AE?= =?UTF-8?q?=EF=BC=8C=E5=8F=AF=E5=9C=A8=E8=A8=98=E5=88=86=E6=9D=BF=E8=87=A8?= =?UTF-8?q?=E6=99=82=E9=81=B8=E4=B8=8A=E5=A0=B4=E4=BD=86=E4=B8=8D=E5=8F=83?= =?UTF-8?q?=E8=88=87=E9=85=8D=E5=B0=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根本原因: 現場常有臨時來打球的人,不屬於 A、B 區的正規輪替名單;原本要讓他們上場只能把名字塞進 A 或 B 區,但這樣會被排進對戰組合,打亂正規配對。 影響: 首頁 A、B 區下方新增跨整行的「其他區名單」輸入框,內容存 localStorage 且不會被資料庫載入蓋掉;分組配對完全不使用其他區。記分板選隊伍時,其他區的人排在清單最後、名字旁掛「其他區」徽章,可手動勾選上場(含與正規名單混搭),今日場次照計;「自動選擇」不會選到他們,自動輪替仍以正規名單為準。 修法: App 新增 areaOtherInput 狀態與 localStorage 持久化,parseRoster 後以 extraPlayers 傳入 ScoreboardPage;selectablePlayers 併入其他區人選並用 extraPlayerSet 標記,autoPickDraftPlayers 過濾掉其他區,TeamPickerModal 依標記顯示徽章;TeamSelectionPage 新增第三個輸入欄位與說明文字。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/App.css | 21 +++++++++++++++ src/App.tsx | 15 +++++++++++ src/pages/ScoreboardPage.tsx | 45 ++++++++++++++++++++++++++++++--- src/pages/TeamSelectionPage.tsx | 13 ++++++++++ 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/App.css b/src/App.css index 9101bb0..97dca1c 100644 --- a/src/App.css +++ b/src/App.css @@ -319,6 +319,15 @@ grid-template-columns: repeat(2, minmax(0, 1fr)); } +/* 其他區名單跨滿整行,放在 A/B 區下方。 */ +.field-area-other { + grid-column: 1 / -1; +} + +.field-area-other textarea { + min-height: 72px; +} + .field { display: grid; gap: 8px; @@ -1501,6 +1510,18 @@ white-space: nowrap; } +.team-picker-extra-badge { + margin-left: 6px; + padding: 2px 8px; + border-radius: 999px; + font-size: 0.68rem; + font-style: normal; + vertical-align: 2px; + color: #8a4d12; + background: rgba(248, 168, 45, 0.18); + box-shadow: inset 0 0 0 1px rgba(199, 108, 44, 0.32); +} + .team-picker-option strong, .preset-team-card strong { display: block; diff --git a/src/App.tsx b/src/App.tsx index 2e3eddc..be7af27 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,6 +47,7 @@ import type { const STORAGE_KEYS = { areaA: 'badminton-scoreboard::area-a', areaB: 'badminton-scoreboard::area-b', + areaOther: 'badminton-scoreboard::area-other', history: 'badminton-scoreboard::history', playCounts: 'badminton-scoreboard::play-counts', } as const @@ -123,6 +124,10 @@ function App() { const [areaBInput, setAreaBInput] = useState(() => loadStoredText(STORAGE_KEYS.areaB, defaultAreaB.join('\n')), ) + // 其他區:不參與分組配對,但會帶進記分板讓臨時上場的人可以被選到。 + const [areaOtherInput, setAreaOtherInput] = useState(() => + loadStoredText(STORAGE_KEYS.areaOther, ''), + ) const [groups, setGroups] = useState([]) const [groupSource, setGroupSource] = useState<'idle' | 'db' | 'manual'>('idle') const [loadStatus, setLoadStatus] = useState('idle') @@ -163,6 +168,7 @@ function App() { const parsedAreaA = useMemo(() => parseRoster(areaAInput), [areaAInput]) const parsedAreaB = useMemo(() => parseRoster(areaBInput), [areaBInput]) + const parsedAreaOther = useMemo(() => parseRoster(areaOtherInput), [areaOtherInput]) const selectedGroup = groups.find((group) => group.id === selectedGroupId) ?? null const leftTeam = activeMatchup.leftTeam const rightTeam = activeMatchup.rightTeam @@ -177,6 +183,10 @@ function App() { window.localStorage.setItem(STORAGE_KEYS.areaB, areaBInput) }, [areaBInput]) + useEffect(() => { + window.localStorage.setItem(STORAGE_KEYS.areaOther, areaOtherInput) + }, [areaOtherInput]) + useEffect(() => { window.localStorage.setItem(STORAGE_KEYS.history, JSON.stringify(history)) }, [history]) @@ -1068,6 +1078,7 @@ function App() { void loadGroupsFromDb()} onTargetDateChange={setTargetDate} @@ -1088,6 +1100,7 @@ function App() { void loadGroupsFromDb()} onTargetDateChange={setTargetDate} @@ -1107,6 +1121,7 @@ function App() { element={ = { type ScoreboardPageProps = { currentSelectionOrder: string[] + extraPlayers: string[] finishDialogError: string finishDialogOpen: boolean finishDialogUploading: boolean @@ -92,6 +93,7 @@ type ScoreboardPageProps = { export function ScoreboardPage({ currentSelectionOrder, + extraPlayers, finishDialogError, finishDialogOpen, finishDialogUploading, @@ -189,8 +191,37 @@ export function ScoreboardPage({ } }) + // 其他區的人排在正規名單後面,一樣可以被選上場。 + extraPlayers.forEach((player) => { + if (!seen.has(player)) { + seen.add(player) + players.push(player) + } + }) + return players - }, [selectedGroup]) + }, [selectedGroup, extraPlayers]) + + // 標記哪些人來自其他區:不參與自動選擇,選人面板上也會標示。 + const extraPlayerSet = useMemo(() => { + if (!selectedGroup) { + return new Set() + } + + const groupPlayers = new Set() + + selectedGroup.teams.forEach((team) => { + if (!team.isPlaceholderA) { + groupPlayers.add(team.playerA) + } + + if (!team.isPlaceholderB) { + groupPlayers.add(team.playerB) + } + }) + + return new Set(extraPlayers.filter((player) => !groupPlayers.has(player))) + }, [selectedGroup, extraPlayers]) const presetTeams = useMemo( () => @@ -500,8 +531,9 @@ export function ScoreboardPage({ } const autoPickDraftPlayers = () => { + // 其他區的人只能手動勾選,自動選擇留給正規輪替的名單。 const shuffled = selectablePlayers.filter( - (player) => !AUTO_PICK_EXCLUDED_NAMES.has(player), + (player) => !AUTO_PICK_EXCLUDED_NAMES.has(player) && !extraPlayerSet.has(player), ) for (let index = shuffled.length - 1; index > 0; index -= 1) { @@ -697,6 +729,7 @@ export function ScoreboardPage({ playCounts: Record draftTargetScore: string group: RoundGroup @@ -905,6 +939,7 @@ type TeamPickerModalProps = { function TeamPickerModal({ draftPlayers, + extraPlayerSet, playCounts, draftTargetScore, group, @@ -969,6 +1004,7 @@ function TeamPickerModal({ const checked = draftPlayers.includes(playerName) const selectedOrder = checked ? draftPlayers.indexOf(playerName) + 1 : null const playedCount = playCounts[playerName] ?? 0 + const isExtra = extraPlayerSet.has(playerName) return (