功能:首頁日期預設當天、記分板語音改為報比分與發球區
摘要: - 首頁「指定日期」每次進入都預設為當天 - 記分板語音得分播報改為「比分(發球方先)+ 發球者左右發球區」,賽末點獲勝時整段改播「贏得比賽」 根本原因: - 日期原本從 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:
+24
-14
@@ -45,7 +45,6 @@ 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',
|
||||||
targetDate: 'badminton-scoreboard::target-date',
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const defaultAreaA = ['柏威', '建喵', 'Yuki', '阿釧']
|
const defaultAreaA = ['柏威', '建喵', 'Yuki', '阿釧']
|
||||||
@@ -86,9 +85,11 @@ type VictoryAnnouncement = {
|
|||||||
|
|
||||||
type VoiceAnnouncement = {
|
type VoiceAnnouncement = {
|
||||||
key: number
|
key: number
|
||||||
scorerName: string
|
servingScore: number
|
||||||
serverChanged: boolean
|
opponentScore: number
|
||||||
serverName: string
|
serverName: string
|
||||||
|
serverCourt: 'left' | 'right'
|
||||||
|
winnerTeamName: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const STREAK_TITLES: Record<number, string> = {
|
const STREAK_TITLES: Record<number, string> = {
|
||||||
@@ -108,9 +109,7 @@ function App() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const isScoreboardRoute = location.pathname === '/scoreboard'
|
const isScoreboardRoute = location.pathname === '/scoreboard'
|
||||||
|
|
||||||
const [targetDate, setTargetDate] = useState(() =>
|
const [targetDate, setTargetDate] = useState(() => formatDateInputValue())
|
||||||
loadStoredText(STORAGE_KEYS.targetDate, formatDateInputValue()),
|
|
||||||
)
|
|
||||||
const [areaAInput, setAreaAInput] = useState(() =>
|
const [areaAInput, setAreaAInput] = useState(() =>
|
||||||
loadStoredText(STORAGE_KEYS.areaA, defaultAreaA.join('\n')),
|
loadStoredText(STORAGE_KEYS.areaA, defaultAreaA.join('\n')),
|
||||||
)
|
)
|
||||||
@@ -155,10 +154,6 @@ function App() {
|
|||||||
const liveRoomId = liveRoomSession?.roomId ?? null
|
const liveRoomId = liveRoomSession?.roomId ?? null
|
||||||
const isNavigationLocked = Boolean(leftTeam && rightTeam && scoreState.serving !== null)
|
const isNavigationLocked = Boolean(leftTeam && rightTeam && scoreState.serving !== null)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.localStorage.setItem(STORAGE_KEYS.targetDate, targetDate)
|
|
||||||
}, [targetDate])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.localStorage.setItem(STORAGE_KEYS.areaA, areaAInput)
|
window.localStorage.setItem(STORAGE_KEYS.areaA, areaAInput)
|
||||||
}, [areaAInput])
|
}, [areaAInput])
|
||||||
@@ -771,14 +766,31 @@ function App() {
|
|||||||
: scoreState.rightRightCourtPlayer,
|
: 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 }])
|
setScoreHistory((current) => [...current, { pointLog, scoreState }])
|
||||||
setPointLog(nextPointLog)
|
setPointLog(nextPointLog)
|
||||||
setScoreState(nextScoreState)
|
setScoreState(nextScoreState)
|
||||||
setVoiceAnnouncement({
|
setVoiceAnnouncement({
|
||||||
key: Date.now(),
|
key: Date.now(),
|
||||||
scorerName: side === 'left' ? leftTeam.playerA : rightTeam.playerA,
|
servingScore,
|
||||||
serverChanged: side === scoreState.serving,
|
opponentScore,
|
||||||
serverName: getNextServerName(nextScoreState, leftTeam, rightTeam, side),
|
serverName: getNextServerName(nextScoreState, leftTeam, rightTeam, side),
|
||||||
|
serverCourt,
|
||||||
|
winnerTeamName,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (streakTitle) {
|
if (streakTitle) {
|
||||||
@@ -790,8 +802,6 @@ function App() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const reachedTarget = hasWonGame(nextScoreState)
|
|
||||||
|
|
||||||
if (reachedTarget) {
|
if (reachedTarget) {
|
||||||
setVictoryAnnouncement({
|
setVictoryAnnouncement({
|
||||||
key: Date.now() + 1,
|
key: Date.now() + 1,
|
||||||
|
|||||||
@@ -60,9 +60,11 @@ type ScoreboardPageProps = {
|
|||||||
} | null
|
} | null
|
||||||
voiceAnnouncement: {
|
voiceAnnouncement: {
|
||||||
key: number
|
key: number
|
||||||
scorerName: string
|
servingScore: number
|
||||||
serverChanged: boolean
|
opponentScore: number
|
||||||
serverName: string
|
serverName: string
|
||||||
|
serverCourt: 'left' | 'right'
|
||||||
|
winnerTeamName: string | null
|
||||||
} | null
|
} | null
|
||||||
targetDate: string
|
targetDate: string
|
||||||
onApplyMatchup: (
|
onApplyMatchup: (
|
||||||
@@ -245,17 +247,27 @@ export function ScoreboardPage({
|
|||||||
return
|
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[] = []
|
const parts: string[] = []
|
||||||
|
|
||||||
if (voiceSettings.announceScore) {
|
if (voiceSettings.announceScore) {
|
||||||
parts.push(`${getSpeechName(voiceAnnouncement.scorerName)}得分`)
|
parts.push(`${voiceAnnouncement.servingScore}比${voiceAnnouncement.opponentScore}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (voiceSettings.announceServer && voiceAnnouncement.serverName) {
|
if (voiceSettings.announceServer && voiceAnnouncement.serverName) {
|
||||||
parts.push(
|
parts.push(
|
||||||
`${getSpeechName(voiceAnnouncement.serverName)}${
|
`${getSpeechName(voiceAnnouncement.serverName)}${
|
||||||
voiceAnnouncement.serverChanged ? '換邊發球' : '發球'
|
voiceAnnouncement.serverCourt === 'left' ? '左邊' : '右邊'
|
||||||
}`,
|
}發球`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -983,7 +995,7 @@ function VoiceSettingsModal({
|
|||||||
<h3>播報內容</h3>
|
<h3>播報內容</h3>
|
||||||
|
|
||||||
<label className="voice-setting-row">
|
<label className="voice-setting-row">
|
||||||
<span>播報誰得分</span>
|
<span>播報比分</span>
|
||||||
<input
|
<input
|
||||||
checked={settings.announceScore}
|
checked={settings.announceScore}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
Reference in New Issue
Block a user