功能:首頁日期預設當天、記分板語音改為報比分與發球區

摘要:
- 首頁「指定日期」每次進入都預設為當天
- 記分板語音得分播報改為「比分(發球方先)+ 發球者左右發球區」,賽末點獲勝時整段改播「贏得比賽」

根本原因:
- 日期原本從 localStorage 讀上次選的值,重開會停在舊日期
- 語音原本只唸「誰得分、誰發球」,現場較不直覺,球敘想聽到即時比分與發球位置

影響:
- src/App.tsx:targetDate 改為直接用當天、移除日期的 localStorage 記憶;recordPoint 算出發球方比分與發球區,重整 voiceAnnouncement 欄位
- src/pages/ScoreboardPage.tsx:語音組字改為「X比Y」「OO左/右邊發球」,獲勝時改播「贏得比賽」;語音設定「播報誰得分」更名「播報比分」

修法:
- 發球方分數先報;上方(左側)隊伍發球區做左右鏡像以對齊畫面,下方隊伍不鏡像

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 15:33:22 +08:00
parent 3677162747
commit 091e654bdb
2 changed files with 473 additions and 451 deletions
+24 -14
View File
@@ -45,7 +45,6 @@ const STORAGE_KEYS = {
areaA: 'badminton-scoreboard::area-a',
areaB: 'badminton-scoreboard::area-b',
history: 'badminton-scoreboard::history',
targetDate: 'badminton-scoreboard::target-date',
} as const
const defaultAreaA = ['柏威', '建喵', 'Yuki', '阿釧']
@@ -86,9 +85,11 @@ type VictoryAnnouncement = {
type VoiceAnnouncement = {
key: number
scorerName: string
serverChanged: boolean
servingScore: number
opponentScore: number
serverName: string
serverCourt: 'left' | 'right'
winnerTeamName: string | null
}
const STREAK_TITLES: Record<number, string> = {
@@ -108,9 +109,7 @@ function App() {
const navigate = useNavigate()
const isScoreboardRoute = location.pathname === '/scoreboard'
const [targetDate, setTargetDate] = useState(() =>
loadStoredText(STORAGE_KEYS.targetDate, formatDateInputValue()),
)
const [targetDate, setTargetDate] = useState(() => formatDateInputValue())
const [areaAInput, setAreaAInput] = useState(() =>
loadStoredText(STORAGE_KEYS.areaA, defaultAreaA.join('\n')),
)
@@ -155,10 +154,6 @@ function App() {
const liveRoomId = liveRoomSession?.roomId ?? null
const isNavigationLocked = Boolean(leftTeam && rightTeam && scoreState.serving !== null)
useEffect(() => {
window.localStorage.setItem(STORAGE_KEYS.targetDate, targetDate)
}, [targetDate])
useEffect(() => {
window.localStorage.setItem(STORAGE_KEYS.areaA, areaAInput)
}, [areaAInput])
@@ -771,14 +766,31 @@ function App() {
: scoreState.rightRightCourtPlayer,
}
const reachedTarget = hasWonGame(nextScoreState)
const winnerTeamName = reachedTarget
? side === 'left'
? getTeamDisplayName(leftTeam)
: getTeamDisplayName(rightTeam)
: null
// 得分方接著發球,報分以發球方分數為先;左側隊伍的發球區需鏡像對應畫面。
const servingScore = side === 'left' ? nextScoreState.scoreLeft : nextScoreState.scoreRight
const opponentScore = side === 'left' ? nextScoreState.scoreRight : nextScoreState.scoreLeft
const serverCourt =
side === 'left'
? getMirroredCourt(getServiceCourt(servingScore))
: getServiceCourt(servingScore)
setScoreHistory((current) => [...current, { pointLog, scoreState }])
setPointLog(nextPointLog)
setScoreState(nextScoreState)
setVoiceAnnouncement({
key: Date.now(),
scorerName: side === 'left' ? leftTeam.playerA : rightTeam.playerA,
serverChanged: side === scoreState.serving,
servingScore,
opponentScore,
serverName: getNextServerName(nextScoreState, leftTeam, rightTeam, side),
serverCourt,
winnerTeamName,
})
if (streakTitle) {
@@ -790,8 +802,6 @@ function App() {
})
}
const reachedTarget = hasWonGame(nextScoreState)
if (reachedTarget) {
setVictoryAnnouncement({
key: Date.now() + 1,
+18 -6
View File
@@ -60,9 +60,11 @@ type ScoreboardPageProps = {
} | null
voiceAnnouncement: {
key: number
scorerName: string
serverChanged: boolean
servingScore: number
opponentScore: number
serverName: string
serverCourt: 'left' | 'right'
winnerTeamName: string | null
} | null
targetDate: string
onApplyMatchup: (
@@ -245,17 +247,27 @@ export function ScoreboardPage({
return
}
if (voiceAnnouncement.winnerTeamName) {
const winnerSpeech = voiceAnnouncement.winnerTeamName
.split('/')
.map((name) => getSpeechName(name.trim()))
.filter(Boolean)
.join('、')
speakAnnouncement(`${winnerSpeech}贏得比賽`, voiceSettings.rate)
return
}
const parts: string[] = []
if (voiceSettings.announceScore) {
parts.push(`${getSpeechName(voiceAnnouncement.scorerName)}得分`)
parts.push(`${voiceAnnouncement.servingScore}${voiceAnnouncement.opponentScore}`)
}
if (voiceSettings.announceServer && voiceAnnouncement.serverName) {
parts.push(
`${getSpeechName(voiceAnnouncement.serverName)}${
voiceAnnouncement.serverChanged ? '換邊發球' : '發球'
}`,
voiceAnnouncement.serverCourt === 'left' ? '左邊' : '右邊'
}發球`,
)
}
@@ -983,7 +995,7 @@ function VoiceSettingsModal({
<h3></h3>
<label className="voice-setting-row">
<span></span>
<span></span>
<input
checked={settings.announceScore}
type="checkbox"