From 540050a991662768697c2b73637deec9439d502b Mon Sep 17 00:00:00 2001 From: JianMiau Date: Tue, 14 Jul 2026 11:18:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E8=87=AA=E5=8B=95?= =?UTF-8?q?=E9=81=B8=E9=9A=8A=E6=8E=92=E9=99=A4=E3=80=8C=E9=82=A3=E5=80=8B?= =?UTF-8?q?=E3=80=8D=E3=80=81=E9=81=B8=E9=9A=8A=E9=9D=A2=E6=9D=BF=E9=A1=AF?= =?UTF-8?q?=E7=A4=BA=E6=AF=8F=E4=BA=BA=E4=BB=8A=E6=97=A5=E5=A0=B4=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 摘要: - 記分板「自動選擇」時排除名字為「那個」的球員,手動仍可勾選 - 結算完成(上傳或不上傳)時幫上場 4 人各記一場,輪空不計 - 選球員面板每位球員名字旁顯示「今日 N 場」徽章 根本原因: - 名單裡的「那個」是佔位用名字,自動選擇會把它當一般球員排進場 - 現場排下一場時沒有依據看誰打得多、誰打得少,容易分配不均 影響: - src/App.tsx:新增 playCounts state 與 localStorage 持久化(跨日自動歸零)、recordPlayedMatch() 於 skipUpload 與 uploadSettledMatch 成功時計數、傳 playCounts 給記分板 - src/pages/ScoreboardPage.tsx:新增 AUTO_PICK_EXCLUDED_NAMES 常數過濾自動選擇、選隊 modal 每列加「今日 N 場」徽章 - src/App.css:徽章樣式與選項改三欄 grid(含手機版縮小字級) 修法: - 自動選擇先以排除名單過濾 selectablePlayers 再洗牌取 4 位 - 場次以 {date, counts} 存 localStorage,載入時日期非當天即歸零 - 計數以 activeMatchup 兩隊為準,isPlaceholder 的輪空欄位跳過 Co-Authored-By: Claude Fable 5 --- src/App.css | 20 ++++++++++-- src/App.tsx | 61 ++++++++++++++++++++++++++++++++++++ src/pages/ScoreboardPage.tsx | 13 +++++++- 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/App.css b/src/App.css index bdccf10..e84fda1 100644 --- a/src/App.css +++ b/src/App.css @@ -1324,7 +1324,7 @@ .team-picker-option { display: grid; - grid-template-columns: 34px minmax(0, 1fr); + grid-template-columns: 34px minmax(0, 1fr) auto; gap: 10px; align-items: center; padding: 12px 12px; @@ -1368,6 +1368,17 @@ background: linear-gradient(180deg, #ffbf3b, #f0a21a); } +.team-picker-play-count { + justify-self: end; + padding: 3px 9px; + border: 1px solid rgba(124, 98, 61, 0.2); + border-radius: 999px; + color: #7b6148; + background: rgba(255, 255, 255, 0.7); + font-size: 0.76rem; + white-space: nowrap; +} + .team-picker-option strong, .preset-team-card strong { display: block; @@ -2200,12 +2211,17 @@ } .team-picker-option { - grid-template-columns: 24px minmax(0, 1fr); + grid-template-columns: 24px minmax(0, 1fr) auto; gap: 8px; padding: 9px 8px; border-radius: 12px; } + .team-picker-play-count { + padding: 2px 7px; + font-size: 0.68rem; + } + .team-picker-checkbox { width: 24px; height: 24px; diff --git a/src/App.tsx b/src/App.tsx index 02afa91..44d0b26 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,6 +48,7 @@ const STORAGE_KEYS = { areaA: 'badminton-scoreboard::area-a', areaB: 'badminton-scoreboard::area-b', history: 'badminton-scoreboard::history', + playCounts: 'badminton-scoreboard::play-counts', } as const const defaultAreaA = ['柏威', '建喵', 'Yuki', '阿釧'] @@ -137,6 +138,10 @@ function App() { const [history, setHistory] = useState(() => loadStoredHistory(STORAGE_KEYS.history), ) + // 當天每位球員打過的場次(結算一次算一場),跨日自動歸零。 + const [playCounts, setPlayCounts] = useState>(() => + loadStoredPlayCounts(STORAGE_KEYS.playCounts), + ) const [settlement, setSettlement] = useState({ error: '', open: false, @@ -176,6 +181,13 @@ function App() { window.localStorage.setItem(STORAGE_KEYS.history, JSON.stringify(history)) }, [history]) + useEffect(() => { + window.localStorage.setItem( + STORAGE_KEYS.playCounts, + JSON.stringify({ date: formatDateInputValue(), counts: playCounts }), + ) + }, [playCounts]) + useEffect(() => { if (loadStatus !== 'loaded' || !loadMessage) { return @@ -895,8 +907,31 @@ function App() { })) } + // 結算完成時幫這場上場的球員各記一場(輪空不計)。 + const recordPlayedMatch = () => { + if (!leftTeam || !rightTeam) { + return + } + + const playedNames = [leftTeam, rightTeam].flatMap((team) => [ + team.isPlaceholderA ? null : team.playerA, + team.isPlaceholderB ? null : team.playerB, + ]) + + setPlayCounts((current) => { + const next = { ...current } + playedNames.forEach((name) => { + if (name) { + next[name] = (next[name] ?? 0) + 1 + } + }) + return next + }) + } + const skipUpload = () => { void finalizeLiveRoom().finally(() => { + recordPlayedMatch() setSettlement({ error: '', open: false, @@ -946,6 +981,7 @@ function App() { } setHistory((current) => [historyItem, ...current]) + recordPlayedMatch() await finalizeLiveRoom() setSettlement({ error: '', @@ -1081,6 +1117,7 @@ function App() { nextMatchSignal={nextMatchSignal} rightTeam={rightTeam} scoreState={scoreState} + playCounts={playCounts} selectedGroup={selectedGroup} streakAnnouncement={streakAnnouncement} victoryAnnouncement={victoryAnnouncement} @@ -1264,6 +1301,30 @@ function loadStoredHistory(storageKey: string) { } } +// 讀取當天的球員場次紀錄,日期不是今天就重新歸零。 +function loadStoredPlayCounts(storageKey: string): Record { + const value = window.localStorage.getItem(storageKey) + + if (!value) { + return {} + } + + try { + const parsed = JSON.parse(value) as { + date?: string + counts?: Record + } + + if (parsed.date !== formatDateInputValue() || !parsed.counts) { + return {} + } + + return parsed.counts + } catch { + return {} + } +} + function buildLiveRoomPayload({ groupId, leftTeam, diff --git a/src/pages/ScoreboardPage.tsx b/src/pages/ScoreboardPage.tsx index 73cb014..d36e714 100644 --- a/src/pages/ScoreboardPage.tsx +++ b/src/pages/ScoreboardPage.tsx @@ -24,6 +24,8 @@ type VoiceSettings = { rate: number } +// 自動選擇時要排除的名字(仍可手動勾選)。 +const AUTO_PICK_EXCLUDED_NAMES = new Set(['那個']) const VOICE_SETTINGS_STORAGE_KEY = 'badminton-scoreboard::voice-settings' const defaultVoiceSettings: VoiceSettings = { announceScore: true, @@ -44,6 +46,7 @@ type ScoreboardPageProps = { leftTeam: GroupTeam | null liveRoomId: string | null nextMatchSignal: number + playCounts: Record rightTeam: GroupTeam | null scoreState: ScoreState selectedGroup: RoundGroup | null @@ -95,6 +98,7 @@ export function ScoreboardPage({ leftTeam, liveRoomId, nextMatchSignal, + playCounts, rightTeam, scoreState, selectedGroup, @@ -462,7 +466,9 @@ export function ScoreboardPage({ } const autoPickDraftPlayers = () => { - const shuffled = [...selectablePlayers] + const shuffled = selectablePlayers.filter( + (player) => !AUTO_PICK_EXCLUDED_NAMES.has(player), + ) for (let index = shuffled.length - 1; index > 0; index -= 1) { const swapIndex = Math.floor(Math.random() * (index + 1)) @@ -621,6 +627,7 @@ export function ScoreboardPage({ draftPlayers={draftPlayers} draftTargetScore={draftTargetScore} group={selectedGroup} + playCounts={playCounts} presetTeams={presetTeams} selectablePlayers={selectablePlayers} selectionCount={draftPlayers.length} @@ -808,6 +815,7 @@ function ScoreboardTeamPanel({ type TeamPickerModalProps = { draftPlayers: string[] + playCounts: Record draftTargetScore: string group: RoundGroup presetTeams: GroupTeam[] @@ -826,6 +834,7 @@ type TeamPickerModalProps = { function TeamPickerModal({ draftPlayers, + playCounts, draftTargetScore, group, presetTeams, @@ -888,6 +897,7 @@ function TeamPickerModal({ {selectablePlayers.map((playerName) => { const checked = draftPlayers.includes(playerName) const selectedOrder = checked ? draftPlayers.indexOf(playerName) + 1 : null + const playedCount = playCounts[playerName] ?? 0 return ( ) })}