功能:自動選隊排除「那個」、選隊面板顯示每人今日場次
摘要:
- 記分板「自動選擇」時排除名字為「那個」的球員,手動仍可勾選
- 結算完成(上傳或不上傳)時幫上場 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:
+18
-2
@@ -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
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user