cb01c144216ffb04eee5ea900dd25a8b3df63219
摘要: - POST /api/history 寫入戰績後,檢查上場 4 人是否在當天 attendance 名單, 沒報名的自動補:有 TG 綁定用 tg_members 的真實 user_id 與名稱, 沒綁定用名字 md5 前 6 bytes 取負的固定 id(與歷史匯入規則一致,已驗證) - 名字比對沿用出席統計規則(別名表+大小寫不分),輪空等佔位字不補 - 前端上傳成功後浮出「已幫 ○○ 補報名」提示,6 秒自動消失 - 補報名失敗只記 log,不影響戰績上傳 根本原因: - 報名靠 TG bot,臨時上場的人常忘了報名,出席統計就少算; 記分板結算時已經知道誰上場,是補報名最準的時機 影響: - 跨裝置記分也適用,出席統計(attendance 表)不再漏掉臨時上場的人 - 同一場重複上傳不會重複補(已在名單就跳過) 修法: - server.mjs 新增 ensureAttendanceSignups / getTaipeiPollDate / syntheticUserId - types.ts 的 HistoryUploadResponse 增加 addedSignups - App.tsx 新增 signupNotice 狀態與浮動提示 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
羽毛球記分板
這是一個使用 Vite + React + TypeScript + Node.js 製作的羽毛球記分板專案,提供手機優先的記分介面、歷史戰績、房間觀戰、語音播報、PWA 安裝,以及 Docker / NAS 部署方式。
功能特色
- 選隊伍頁面
- 可依指定日期從資料庫讀取分組資料。
- match-hub V2 場地排程資料(
battlecombination以場地為 key、每場地 8 局 × 2 隊)會顯示成「2 號場地」「3 號場地」各 8 局賽程,挑一個場地帶進記分板;選隊面板的預設隊伍也會照局分組。 - 舊的 V1 三輪資料(personnel 含 B 區)仍照「第 N 組」顯示,兩種格式自動判別。
- 若當天沒有資料,可手動輸入「今日出席名單」排賽程:演算法與 match-hub V2 相同,依
tg_members.skill實力(GET /api/skills,表名可用DB_MEMBERS_TABLE覆寫)做強弱互補與兩隊平衡,搭檔在全部組合輪過一遍前不重複、每人局數落差最多 1 局;8 人以上排 2、3 號兩個場地,未滿 8 人只排 2 號場地。 - 點進記分板時會直接帶入該組對戰。
- 記分板
- 兩隊隊員可自由交換上下、左右位置。
- 畫面編號固定為左上
1、右上2、右下3、左下4。 - 先攻只能在開局設定一次,之後不會跟著發球權改變。
- 點擊分數直接加分,沒有加一減一按鈕。
- 第一分開始後,
設定隊伍會改成上一步。 比賽結算需要長按1秒才會觸發,避免誤觸。- 達標分數後有獲勝動畫與結算流程。
- 手機上會盡量壓縮成單頁滿版,避免上下滑動。
- 羽球規則
- 預設
21分制,可在設定隊伍時調整目標分數。 - 支援 Deuce:
20:20後需領先2分才獲勝。 29:29時第30分直接獲勝。- 發球方依羽球規則處理,
0分在右發球區。 - 畫面以下方隊伍為我方、上方隊伍為對方。
- 上方隊伍採鏡像顯示,所以我方
0:0在右邊發球時,對方會在左邊接發。
- 預設
- 語音播報
- 只在按下加分當下播報,不會因復原或其他操作重複報分。
- 可選擇是否播報得分與發球者。
- 同隊連續得分才會播報
換邊發球。 - 支援調整語速,最高可到
10x。 RURU會做大小寫無關判斷並以指定發音播報。
- 歷史戰績
- 可從資料庫讀取歷史列表。
- 點開單筆可查看得分過程。
- 每筆資料可刪除,刪除前會顯示確認提示。
- 今日場次
- 選隊伍面板每個人名字旁顯示
今日 N 場,每次結算幫上場的人各加一場,存在本機、跨日歸零。 - 結算上傳戰績時會檢查上場 4 人有沒有在當天
attendance報名名單,忘了報名會自動補:有 TG 綁定(tg_members)就用真實 user_id,沒有就用名字 md5 產生的固定負數 id(與歷史匯入規則一致);補了誰會在畫面上提示。輪空等佔位字不補。 - 面板的
同步今日場次按鈕會從history表撈今天(本機時區)的比賽,把每個人的今日場次覆蓋成 DB 的數字,跨裝置記分時用來補齊其他裝置上傳的場次。
- 選隊伍面板每個人名字旁顯示
- 出席率統計
- 歷史戰績頁的
出席率統計按鈕會彈窗顯示每個人的出席狀況。 - 資料取自
attendance表(TG 報名 bot 每天每人一列),不是history表,所以有報名到場但沒被記分的場次也算出席;當天名單還沒存進badminton表也已經算得到。 - 若 DB 沒有
attendance表,會退回用badminton表的每日分組名單統計。表名可用DB_ATTENDANCE_TABLE覆寫。 - 同時顯示兩種出席率:
全期:以資料庫全部場次為分母。加入後:從各自第一次出席那天算起,新加入的人不會被稀釋。
- 另外顯示最近
12場的到場次數,可看出誰還在打球。 - 英文名大小寫不同(
RURU/RuRu/Ruru)自動視為同一人;中文錯字用server/server.mjs的ATTENDANCE_ALIASES別名表對照,合併過的寫法會顯示在該列。 ATTENDANCE_IGNORED_NAMES列的佔位字(如輪空)不列入統計。
- 歷史戰績頁的
- 房間觀戰
- 記分板帶入隊伍後會自動建立房間。
- 房間列表可查看目前直播中的比賽。
- 觀戰者只能看,不能操作記分。
- 分數、房間狀態、比賽結算會即時同步給觀戰者。
- 房間失效、重整、重選隊伍後也會通知觀戰者。
- 房間列表有
重新取得列表,並帶有5秒冷卻。
- PWA
- 可安裝到 iPhone / iPad / Android 主畫面。
- 支援 Web App 模式啟動。
- 新版本部署後會提示重新整理或重新安裝。
本機開發
Port
- Client:
3501 - Server:
8788
安裝
npm install
啟動開發模式
npm run dev
啟動後:
- 前端:
http://localhost:3501 - API:
http://localhost:8788
檢查
npm run lint
npm run build
記分板滿版模式
- 記分板頁面會套用
100dvh高度。 - 手機進入記分板時會關閉頁面捲動與 overscroll。
viewport已加上viewport-fit=cover,較能貼合 iPhone / iPad 安全區。- 若手機高度較矮,會再縮小字級、按鈕與分數區,盡量維持整頁顯示。
環境變數
請先建立 .env:
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=your_password
DB_DATABASE=badminton
DB_TABLE=badminton
DB_HISTORY_TABLE=history
DB_ATTENDANCE_TABLE=attendance
PORT=8788
Docker / NAS 部署
對外服務配置:
- 容器內 Node / API:
8788 - 對外 HTTPS 網址:
3501
部署指令:
sudo docker compose up -d --build
部署完成後可用:
https://你的網域或 NAS IP:3501
每次執行 sudo docker compose up -d --build 都會重新建置前後端與 PWA 靜態資產。
SSL 憑證
Docker Compose 會掛載以下目錄:
/volume1/docker/certs/
需包含:
cert.pemchain.pemprivkey.pem
nginx 容器會監看這個目錄,更新憑證檔案後會自動重新載入,不需要重啟容器。
憑證到期偵測
- 後端會讀取
cert.pem的到期日,透過/api/version與/api/health回報給前端。 - 前端會把到期日存在 localStorage;當 API 出現
Failed to fetch時,若存下來的到期日已過,會明確顯示「SSL 憑證已於某日過期」而不是通用錯誤。 - 憑證到期前
7天,頁面會跳出提醒更新憑證的通知。
資料表格式
history
idtimedayOfWeekscorewinScoretype0: 雙打1: 單打
players- 依照
1 ~ 4固定編號順序儲存玩家名單。
- 依照
team1跟2一隊3跟4一隊
scoreList- 格式:
[round, 發球者編號, 連勝數, 得分隊伍(0 或 1)]
- 格式:
PWA Icon
目前使用:
public/favicon.pngpublic/apple-touch-icon.pngpublic/pwa-192.pngpublic/pwa-512.png
Git 中文顯示
若要讓 git log / commit 顯示中文,建議設定:
git config i18n.commitEncoding utf-8
git config i18n.logOutputEncoding utf-8
git config core.quotepath false
Languages
TypeScript
59.4%
CSS
24.3%
JavaScript
14.7%
Shell
1%
HTML
0.4%
Other
0.2%