功能:記分板側欄新增得分紀錄清單,並讓左右兩欄底部切齊

根本原因:
計分時只能看到當前比分,沒有逐球紀錄可回顧;且右側欄在「比賽結算」下方留有大片閒置空間。原本 .scoreboard-screen 用 align-items: start,記分板維持自然高度、側欄卻撐滿,導致兩欄底部不齊。

影響:
計分頁右側欄在比賽結算按鈕下方顯示逐球得分表:每列一球,得分隊亮綠標記並顯示當下比分,最新在最上、可上下捲動看舊資料;記分板與側欄底部切齊、不超出畫面,多出的高度分配給上下分數面板,中間橫幅維持原高度。

修法:
App 把既有 pointLog 傳入 ScoreboardPage,新增 pointHistoryRows 反序累計比分並渲染 rail-history 清單;CSS 將 .scoreboard-screen 改 align-items: stretch(配合 align-self: start + min-height 100% 讓內容過高時可自然長高不重疊),.scoreboard-court 以 minmax(min-content, 1fr) 把撐高空間讓給隊伍區與分數面板;行動版清單改跨滿整行並以 32dvh 限高。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 14:11:22 +08:00
co-authored by Claude Opus 4.8
parent 540050a991
commit 68618a8e04
3 changed files with 344 additions and 21 deletions
+272 -21
View File
@@ -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 {
+1
View File
@@ -1115,6 +1115,7 @@ function App() {
leftTeam={leftTeam}
liveRoomId={liveRoomId}
nextMatchSignal={nextMatchSignal}
pointLog={pointLog}
rightTeam={rightTeam}
scoreState={scoreState}
playCounts={playCounts}
+71
View File
@@ -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<string, number>
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({
</div>
) : null}
</div>
<div className="rail-history">
<div className="rail-history-head">
<span></span>
<small>{pointHistoryRows.length}</small>
</div>
{pointHistoryRows.length > 0 ? (
<div className="rail-history-scroll">
{pointHistoryRows.map((row) => (
<div className="rail-history-row" key={row.rally}>
<span className="rail-history-index">{row.rally}</span>
<span
className={
row.winner === 0
? 'rail-history-cell rail-history-cell-win'
: 'rail-history-cell'
}
>
{row.winner === 0 ? row.leftScore : ''}
</span>
<span
className={
row.winner === 1
? 'rail-history-cell rail-history-cell-win'
: 'rail-history-cell'
}
>
{row.winner === 1 ? row.rightScore : ''}
</span>
</div>
))}
</div>
) : (
<p className="rail-history-empty"></p>
)}
</div>
</aside>
</section>