功能:自動選隊排除「那個」、選隊面板顯示每人今日場次
摘要:
- 記分板「自動選擇」時排除名字為「那個」的球員,手動仍可勾選
- 結算完成(上傳或不上傳)時幫上場 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 {
|
.team-picker-option {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 34px minmax(0, 1fr);
|
grid-template-columns: 34px minmax(0, 1fr) auto;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 12px;
|
padding: 12px 12px;
|
||||||
@@ -1368,6 +1368,17 @@
|
|||||||
background: linear-gradient(180deg, #ffbf3b, #f0a21a);
|
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,
|
.team-picker-option strong,
|
||||||
.preset-team-card strong {
|
.preset-team-card strong {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -2200,12 +2211,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.team-picker-option {
|
.team-picker-option {
|
||||||
grid-template-columns: 24px minmax(0, 1fr);
|
grid-template-columns: 24px minmax(0, 1fr) auto;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 9px 8px;
|
padding: 9px 8px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.team-picker-play-count {
|
||||||
|
padding: 2px 7px;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
}
|
||||||
|
|
||||||
.team-picker-checkbox {
|
.team-picker-checkbox {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
|||||||
+61
@@ -48,6 +48,7 @@ const STORAGE_KEYS = {
|
|||||||
areaA: 'badminton-scoreboard::area-a',
|
areaA: 'badminton-scoreboard::area-a',
|
||||||
areaB: 'badminton-scoreboard::area-b',
|
areaB: 'badminton-scoreboard::area-b',
|
||||||
history: 'badminton-scoreboard::history',
|
history: 'badminton-scoreboard::history',
|
||||||
|
playCounts: 'badminton-scoreboard::play-counts',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const defaultAreaA = ['柏威', '建喵', 'Yuki', '阿釧']
|
const defaultAreaA = ['柏威', '建喵', 'Yuki', '阿釧']
|
||||||
@@ -137,6 +138,10 @@ function App() {
|
|||||||
const [history, setHistory] = useState<MatchHistoryItem[]>(() =>
|
const [history, setHistory] = useState<MatchHistoryItem[]>(() =>
|
||||||
loadStoredHistory(STORAGE_KEYS.history),
|
loadStoredHistory(STORAGE_KEYS.history),
|
||||||
)
|
)
|
||||||
|
// 當天每位球員打過的場次(結算一次算一場),跨日自動歸零。
|
||||||
|
const [playCounts, setPlayCounts] = useState<Record<string, number>>(() =>
|
||||||
|
loadStoredPlayCounts(STORAGE_KEYS.playCounts),
|
||||||
|
)
|
||||||
const [settlement, setSettlement] = useState<SettlementState>({
|
const [settlement, setSettlement] = useState<SettlementState>({
|
||||||
error: '',
|
error: '',
|
||||||
open: false,
|
open: false,
|
||||||
@@ -176,6 +181,13 @@ function App() {
|
|||||||
window.localStorage.setItem(STORAGE_KEYS.history, JSON.stringify(history))
|
window.localStorage.setItem(STORAGE_KEYS.history, JSON.stringify(history))
|
||||||
}, [history])
|
}, [history])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
STORAGE_KEYS.playCounts,
|
||||||
|
JSON.stringify({ date: formatDateInputValue(), counts: playCounts }),
|
||||||
|
)
|
||||||
|
}, [playCounts])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadStatus !== 'loaded' || !loadMessage) {
|
if (loadStatus !== 'loaded' || !loadMessage) {
|
||||||
return
|
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 = () => {
|
const skipUpload = () => {
|
||||||
void finalizeLiveRoom().finally(() => {
|
void finalizeLiveRoom().finally(() => {
|
||||||
|
recordPlayedMatch()
|
||||||
setSettlement({
|
setSettlement({
|
||||||
error: '',
|
error: '',
|
||||||
open: false,
|
open: false,
|
||||||
@@ -946,6 +981,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setHistory((current) => [historyItem, ...current])
|
setHistory((current) => [historyItem, ...current])
|
||||||
|
recordPlayedMatch()
|
||||||
await finalizeLiveRoom()
|
await finalizeLiveRoom()
|
||||||
setSettlement({
|
setSettlement({
|
||||||
error: '',
|
error: '',
|
||||||
@@ -1081,6 +1117,7 @@ function App() {
|
|||||||
nextMatchSignal={nextMatchSignal}
|
nextMatchSignal={nextMatchSignal}
|
||||||
rightTeam={rightTeam}
|
rightTeam={rightTeam}
|
||||||
scoreState={scoreState}
|
scoreState={scoreState}
|
||||||
|
playCounts={playCounts}
|
||||||
selectedGroup={selectedGroup}
|
selectedGroup={selectedGroup}
|
||||||
streakAnnouncement={streakAnnouncement}
|
streakAnnouncement={streakAnnouncement}
|
||||||
victoryAnnouncement={victoryAnnouncement}
|
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({
|
function buildLiveRoomPayload({
|
||||||
groupId,
|
groupId,
|
||||||
leftTeam,
|
leftTeam,
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ type VoiceSettings = {
|
|||||||
rate: number
|
rate: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自動選擇時要排除的名字(仍可手動勾選)。
|
||||||
|
const AUTO_PICK_EXCLUDED_NAMES = new Set(['那個'])
|
||||||
const VOICE_SETTINGS_STORAGE_KEY = 'badminton-scoreboard::voice-settings'
|
const VOICE_SETTINGS_STORAGE_KEY = 'badminton-scoreboard::voice-settings'
|
||||||
const defaultVoiceSettings: VoiceSettings = {
|
const defaultVoiceSettings: VoiceSettings = {
|
||||||
announceScore: true,
|
announceScore: true,
|
||||||
@@ -44,6 +46,7 @@ type ScoreboardPageProps = {
|
|||||||
leftTeam: GroupTeam | null
|
leftTeam: GroupTeam | null
|
||||||
liveRoomId: string | null
|
liveRoomId: string | null
|
||||||
nextMatchSignal: number
|
nextMatchSignal: number
|
||||||
|
playCounts: Record<string, number>
|
||||||
rightTeam: GroupTeam | null
|
rightTeam: GroupTeam | null
|
||||||
scoreState: ScoreState
|
scoreState: ScoreState
|
||||||
selectedGroup: RoundGroup | null
|
selectedGroup: RoundGroup | null
|
||||||
@@ -95,6 +98,7 @@ export function ScoreboardPage({
|
|||||||
leftTeam,
|
leftTeam,
|
||||||
liveRoomId,
|
liveRoomId,
|
||||||
nextMatchSignal,
|
nextMatchSignal,
|
||||||
|
playCounts,
|
||||||
rightTeam,
|
rightTeam,
|
||||||
scoreState,
|
scoreState,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
@@ -462,7 +466,9 @@ export function ScoreboardPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const autoPickDraftPlayers = () => {
|
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) {
|
for (let index = shuffled.length - 1; index > 0; index -= 1) {
|
||||||
const swapIndex = Math.floor(Math.random() * (index + 1))
|
const swapIndex = Math.floor(Math.random() * (index + 1))
|
||||||
@@ -621,6 +627,7 @@ export function ScoreboardPage({
|
|||||||
draftPlayers={draftPlayers}
|
draftPlayers={draftPlayers}
|
||||||
draftTargetScore={draftTargetScore}
|
draftTargetScore={draftTargetScore}
|
||||||
group={selectedGroup}
|
group={selectedGroup}
|
||||||
|
playCounts={playCounts}
|
||||||
presetTeams={presetTeams}
|
presetTeams={presetTeams}
|
||||||
selectablePlayers={selectablePlayers}
|
selectablePlayers={selectablePlayers}
|
||||||
selectionCount={draftPlayers.length}
|
selectionCount={draftPlayers.length}
|
||||||
@@ -808,6 +815,7 @@ function ScoreboardTeamPanel({
|
|||||||
|
|
||||||
type TeamPickerModalProps = {
|
type TeamPickerModalProps = {
|
||||||
draftPlayers: string[]
|
draftPlayers: string[]
|
||||||
|
playCounts: Record<string, number>
|
||||||
draftTargetScore: string
|
draftTargetScore: string
|
||||||
group: RoundGroup
|
group: RoundGroup
|
||||||
presetTeams: GroupTeam[]
|
presetTeams: GroupTeam[]
|
||||||
@@ -826,6 +834,7 @@ type TeamPickerModalProps = {
|
|||||||
|
|
||||||
function TeamPickerModal({
|
function TeamPickerModal({
|
||||||
draftPlayers,
|
draftPlayers,
|
||||||
|
playCounts,
|
||||||
draftTargetScore,
|
draftTargetScore,
|
||||||
group,
|
group,
|
||||||
presetTeams,
|
presetTeams,
|
||||||
@@ -888,6 +897,7 @@ function TeamPickerModal({
|
|||||||
{selectablePlayers.map((playerName) => {
|
{selectablePlayers.map((playerName) => {
|
||||||
const checked = draftPlayers.includes(playerName)
|
const checked = draftPlayers.includes(playerName)
|
||||||
const selectedOrder = checked ? draftPlayers.indexOf(playerName) + 1 : null
|
const selectedOrder = checked ? draftPlayers.indexOf(playerName) + 1 : null
|
||||||
|
const playedCount = playCounts[playerName] ?? 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -909,6 +919,7 @@ function TeamPickerModal({
|
|||||||
{selectedOrder ? `已選為第 ${selectedOrder} 位` : '尚未加入上場名單'}
|
{selectedOrder ? `已選為第 ${selectedOrder} 位` : '尚未加入上場名單'}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="team-picker-play-count">今日 {playedCount} 場</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user