diff --git a/src/App.css b/src/App.css index e84fda1..11da2e2 100644 --- a/src/App.css +++ b/src/App.css @@ -10,12 +10,28 @@ } .app-shell-scoreboard-fit { - min-height: 100dvh; + height: 100dvh; display: grid; grid-template-rows: auto minmax(0, 1fr); padding-bottom: max(12px, env(safe-area-inset-bottom)); } +/* 記分板為單屏鎖定(body overflow hidden),標題列必須跟著視窗高度縮放。 */ +.app-shell-scoreboard-fit .eyebrow { + margin-bottom: 6px; + padding: 5px 10px; + font-size: 0.68rem; +} + +.app-shell-scoreboard-fit .branding h1 { + margin: 4px 0 0; + font-size: clamp(1.1rem, 2.6dvh, 1.6rem); +} + +.app-shell-scoreboard-fit .nav-pill { + padding: clamp(6px, 1.2dvh, 11px) 14px; +} + .topbar { display: flex; justify-content: space-between; @@ -535,9 +551,12 @@ display: grid; grid-template-columns: minmax(0, 1fr) 160px; gap: 14px; - align-items: start; - min-height: 0; - height: 100%; + /* 記分板與右側欄一起撐滿可用高度,讓兩邊底部切齊; + 高度不足時允許整體長高改用頁面捲動,避免內容溢出背景。 + align-self: start 解除 shell 的 stretch 鎖定,讓內容能超過 1fr 列高。 */ + align-items: stretch; + align-self: start; + min-height: 100%; } .streak-banner { @@ -675,6 +694,8 @@ .scoreboard-court { display: grid; + /* 被撐高時,多的空間分給上下兩個隊伍區,中間橫幅維持自然高度。 */ + grid-template-rows: minmax(min-content, 1fr) auto minmax(min-content, 1fr); gap: 14px; min-height: 0; padding: 12px; @@ -690,6 +711,15 @@ gap: 8px; } +/* 隊伍區被撐高時,多的空間給分數面板(上方隊分數在第 3 列、下方隊在第 1 列)。 */ +.scoreboard-court > .scoreboard-team-section:first-child { + grid-template-rows: auto auto minmax(min-content, 1fr); +} + +.scoreboard-court > .scoreboard-team-section:last-child { + grid-template-rows: minmax(min-content, 1fr) auto auto; +} + .scoreboard-team-head { display: grid; grid-template-columns: minmax(0, 1fr) 62px; @@ -701,7 +731,7 @@ display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; - min-height: 64px; + min-height: clamp(48px, 6.5dvh, 64px); padding: 8px; border-radius: 4px; background: rgba(255, 248, 232, 0.92); @@ -790,7 +820,7 @@ grid-template-columns: 32px auto 1fr; align-items: center; gap: 8px; - min-height: 46px; + min-height: clamp(34px, 4.8dvh, 46px); width: min(100%, 440px); border: 0; border-radius: 4px; @@ -888,7 +918,7 @@ .score-panel-surface { display: grid; place-items: center; - min-height: 172px; + min-height: clamp(92px, 17dvh, 172px); border: 0; width: 100%; padding: 0; @@ -917,7 +947,7 @@ .score-panel-value { font-family: var(--mono); - font-size: clamp(4.4rem, 11vw, 7.2rem); + font-size: clamp(2.8rem, min(11vw, 12dvh), 7.2rem); line-height: 1; color: #0d544a; text-shadow: 0 2px 0 rgba(255, 255, 255, 0.42); @@ -931,7 +961,7 @@ } .scoreboard-center-banner p { - font-size: clamp(1.5rem, 3vw, 2.3rem); + font-size: clamp(1.15rem, min(3vw, 2.4dvh), 2.3rem); color: #fff7e9; text-align: center; } @@ -942,9 +972,101 @@ } .scoreboard-rail { - display: grid; + display: flex; + flex-direction: column; gap: 10px; - align-content: start; + align-self: stretch; + min-height: 0; +} + +.rail-history { + display: flex; + flex-direction: column; + flex: 1 1 0; + min-height: 0; + padding: 8px; + border-radius: 12px; + background: rgba(8, 47, 73, 0.72); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1); +} + +.rail-history-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 6px; + margin-bottom: 6px; + padding: 0 2px; + color: #f7fff8; +} + +.rail-history-head span { + font-size: 0.82rem; + letter-spacing: 0.06em; +} + +.rail-history-head small { + font-family: var(--mono); + font-size: 0.78rem; + color: rgba(247, 255, 248, 0.66); +} + +.rail-history-scroll { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1 1 0; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + touch-action: pan-y; + padding-right: 2px; +} + +.rail-history-row { + display: grid; + grid-template-columns: 26px minmax(0, 1fr) minmax(0, 1fr); + gap: 4px; + align-items: stretch; +} + +.rail-history-index { + display: grid; + place-items: center; + min-height: 26px; + border-radius: 6px; + font-family: var(--mono); + font-size: 0.74rem; + color: rgba(247, 255, 248, 0.62); + background: rgba(255, 255, 255, 0.06); +} + +.rail-history-cell { + display: grid; + place-items: center; + min-height: 26px; + border-radius: 6px; + font-family: var(--mono); + font-size: 0.94rem; + font-weight: 600; + color: #16342f; + background: rgba(255, 255, 255, 0.08); +} + +.rail-history-cell-win { + color: #24331a; + background: linear-gradient(180deg, #ebf8a4, #d6e164); + box-shadow: inset 0 0 0 1px rgba(111, 128, 27, 0.28); +} + +.rail-history-empty { + display: grid; + place-items: center; + flex: 1 1 0; + margin: 0; + font-size: 0.82rem; + color: rgba(247, 255, 248, 0.5); } .rail-icon-grid { @@ -954,7 +1076,7 @@ } .rail-square-button { - min-height: 52px; + min-height: clamp(40px, 5.5dvh, 52px); border: 0; border-radius: 10px; cursor: pointer; @@ -985,10 +1107,10 @@ .rail-clock { display: grid; place-items: center; - min-height: 68px; + min-height: clamp(44px, 6dvh, 68px); border-radius: 12px; font-family: var(--mono); - font-size: 1.7rem; + font-size: clamp(1.1rem, 2.4dvh, 1.7rem); color: #fff7e9; background: radial-gradient(circle at top right, rgba(255, 205, 96, 0.16), transparent 28%), @@ -1915,14 +2037,13 @@ margin-bottom: 6px; } + /* 手機單屏空間有限,裝飾用的英文膠囊直接省下高度。 */ .app-shell-scoreboard-fit .eyebrow { - margin-bottom: 6px; - padding: 6px 10px; - font-size: 0.66rem; + display: none; } .app-shell-scoreboard-fit .branding h1 { - margin: 8px 0 6px; + margin: 4px 0 2px; font-size: 1.12rem; } @@ -2046,16 +2167,48 @@ grid-template-columns: 1fr; } + /* 直向堆疊:court 保證至少內容高(不會被壓縮造成重疊), + rail 拿剩餘空間,歷史區在 rail 內自動填滿、吸收各機型高度差。 */ + .scoreboard-screen { + height: 100%; + grid-template-rows: minmax(min-content, auto) minmax(0, 1fr); + } + .scoreboard-rail { + display: grid; grid-template-columns: minmax(0, 1fr) 94px 106px; + grid-template-rows: auto auto minmax(0, 1fr); gap: 6px; align-items: stretch; + align-self: stretch; + min-height: 0; + } + + /* 說明文字改單行省略,避免窄螢幕折行把發球條撐高。 */ + .serve-lane { + grid-template-columns: 26px auto minmax(0, 1fr); + } + + .serve-lane small { + grid-column: auto; + justify-self: end; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .rail-pill-hold-wrap { grid-column: 1 / -1; } + .rail-history { + grid-column: 1 / -1; + height: auto; + min-height: 0; + max-height: 32dvh; + overflow: hidden; + } + .rail-square-button { min-height: 38px; font-size: 0.84rem; @@ -2270,7 +2423,7 @@ } } -@media (max-width: 720px) and (max-height: 840px) { +@media (max-width: 720px) and (max-height: 960px) { .app-shell-scoreboard-fit .scoreboard-screen { gap: 5px; } @@ -2285,16 +2438,114 @@ } .app-shell-scoreboard-fit .score-panel-surface { - min-height: clamp(82px, 14.2dvh, 96px); + min-height: clamp(70px, 12dvh, 88px); + } + + .app-shell-scoreboard-fit .rail-square-button { + min-height: 34px; + } + + .app-shell-scoreboard-fit .rail-icon-grid { + gap: 6px; } .app-shell-scoreboard-fit .score-panel-value { - font-size: clamp(2.7rem, 13.5vw, 4.1rem); + font-size: clamp(2.4rem, min(13.5vw, 10dvh), 4.1rem); } .app-shell-scoreboard-fit .scoreboard-center-banner p { font-size: 0.96rem; } + + .app-shell-scoreboard-fit .scoreboard-center-banner small { + display: none; + } + + .app-shell-scoreboard-fit .team-head-main { + min-height: 40px; + padding: 5px; + } + + .app-shell-scoreboard-fit .scoreboard-name-chip { + min-height: 32px; + } + + .app-shell-scoreboard-fit .serve-lane { + min-height: 30px; + padding: 5px 8px; + } + + .app-shell-scoreboard-fit .rail-history { + max-height: none; + } +} + +/* 更矮的手機(iPhone SE 等):全面壓縮,確保單屏塞得下。 */ +@media (max-width: 720px) and (max-height: 700px) { + .app-shell-scoreboard-fit .branding h1 { + margin: 2px 0 0; + font-size: 0.98rem; + } + + .app-shell-scoreboard-fit .nav-pill { + padding: 4px 8px; + font-size: 0.74rem; + white-space: nowrap; + } + + /* 極矮螢幕導覽列改單行橫向捲動,省下折行高度。 */ + .app-shell-scoreboard-fit .topnav { + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .app-shell-scoreboard-fit .topbar-compact { + margin-bottom: 4px; + } + + .app-shell-scoreboard-fit .score-panel-surface { + min-height: clamp(48px, 10dvh, 70px); + } + + .app-shell-scoreboard-fit .score-panel-value { + font-size: clamp(1.9rem, 8.5dvh, 2.7rem); + } + + .app-shell-scoreboard-fit .team-head-main { + min-height: 34px; + padding: 4px; + } + + .app-shell-scoreboard-fit .scoreboard-name-chip { + min-height: 28px; + } + + .app-shell-scoreboard-fit .serve-lane { + min-height: 26px; + padding: 4px 8px; + } + + .app-shell-scoreboard-fit .scoreboard-center-banner p { + font-size: 0.9rem; + } + + .app-shell-scoreboard-fit .rail-square-button, + .app-shell-scoreboard-fit .rail-clock { + min-height: 30px; + } + + .app-shell-scoreboard-fit .rail-pill { + padding: 5px 7px; + font-size: 0.78rem; + } +} + +/* 極矮螢幕(iPhone 5 世代)塞不下有意義的得分紀錄,直接隱藏。 */ +@media (max-width: 720px) and (max-height: 600px) { + .app-shell-scoreboard-fit .rail-history { + display: none; + } } .room-list-grid { diff --git a/src/App.tsx b/src/App.tsx index 44d0b26..2e3eddc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1115,6 +1115,7 @@ function App() { leftTeam={leftTeam} liveRoomId={liveRoomId} nextMatchSignal={nextMatchSignal} + pointLog={pointLog} rightTeam={rightTeam} scoreState={scoreState} playCounts={playCounts} diff --git a/src/pages/ScoreboardPage.tsx b/src/pages/ScoreboardPage.tsx index d36e714..1bc9148 100644 --- a/src/pages/ScoreboardPage.tsx +++ b/src/pages/ScoreboardPage.tsx @@ -13,6 +13,7 @@ import type { CourtSide, GroupTeam, PlayerSlot, + PointHistoryEntry, RoundGroup, ScoreSide, ScoreState, @@ -47,6 +48,7 @@ type ScoreboardPageProps = { liveRoomId: string | null nextMatchSignal: number playCounts: Record + pointLog: PointHistoryEntry[] rightTeam: GroupTeam | null scoreState: ScoreState selectedGroup: RoundGroup | null @@ -99,6 +101,7 @@ export function ScoreboardPage({ liveRoomId, nextMatchSignal, playCounts, + pointLog, rightTeam, scoreState, selectedGroup, @@ -216,6 +219,37 @@ export function ScoreboardPage({ [rightTeam, scoreState.rightRightCourtPlayer], ) + // 依 pointLog 累積雙方比分,最新一球排最上面。winner 0=上方(左)隊、1=下方(右)隊。 + const pointHistoryRows = useMemo(() => { + const rows: Array<{ + rally: number + winner: 0 | 1 + leftScore: number + rightScore: number + }> = [] + let left = 0 + let right = 0 + + for (const point of pointLog) { + if (point.winner === 0) { + left += 1 + } else { + right += 1 + } + + rows.push({ + rally: point.round + 1, + winner: point.winner, + leftScore: left, + rightScore: right, + }) + } + + rows.reverse() + + return rows + }, [pointLog]) + const currentServer = scoreState.serving === 'left' ? leftTeam @@ -619,6 +653,43 @@ export function ScoreboardPage({ ) : null} + +
+
+ 得分紀錄 + {pointHistoryRows.length} +
+ + {pointHistoryRows.length > 0 ? ( +
+ {pointHistoryRows.map((row) => ( +
+ {row.rally} + + {row.winner === 0 ? row.leftScore : ''} + + + {row.winner === 1 ? row.rightScore : ''} + +
+ ))} +
+ ) : ( +

尚無得分紀錄

+ )} +