功能:自動選隊排除「那個」、選隊面板顯示每人今日場次

摘要:
- 記分板「自動選擇」時排除名字為「那個」的球員,手動仍可勾選
- 結算完成(上傳或不上傳)時幫上場 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 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 11:18:44 +08:00
co-authored by Claude Fable 5
parent 1bc42f5bf6
commit 540050a991
3 changed files with 91 additions and 3 deletions
+18 -2
View File
@@ -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;
+61
View File
@@ -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<MatchHistoryItem[]>(() =>
loadStoredHistory(STORAGE_KEYS.history),
)
// 當天每位球員打過的場次(結算一次算一場),跨日自動歸零。
const [playCounts, setPlayCounts] = useState<Record<string, number>>(() =>
loadStoredPlayCounts(STORAGE_KEYS.playCounts),
)
const [settlement, setSettlement] = useState<SettlementState>({
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<string, number> {
const value = window.localStorage.getItem(storageKey)
if (!value) {
return {}
}
try {
const parsed = JSON.parse(value) as {
date?: string
counts?: Record<string, number>
}
if (parsed.date !== formatDateInputValue() || !parsed.counts) {
return {}
}
return parsed.counts
} catch {
return {}
}
}
function buildLiveRoomPayload({
groupId,
leftTeam,
+12 -1
View File
@@ -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<string, number>
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<string, number>
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 (
<button
@@ -909,6 +919,7 @@ function TeamPickerModal({
{selectedOrder ? `已選為第 ${selectedOrder}` : '尚未加入上場名單'}
</small>
</div>
<span className="team-picker-play-count"> {playedCount} </span>
</button>
)
})}