From bbedb70e7ea7bf798ab21c518a8bc55cecbd682b Mon Sep 17 00:00:00 2001 From: JianMiau Date: Thu, 16 Apr 2026 10:26:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=AD=B7=E5=8F=B2=E6=88=B0?= =?UTF-8?q?=E7=B8=BE=E5=88=AA=E9=99=A4=E5=8A=9F=E8=83=BD=E4=B8=A6=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 +++- server/server.mjs | 47 +++++++++++++++++ src/App.css | 56 ++++++++++++++++++++ src/lib/api.ts | 15 ++++++ src/pages/HistoryPage.tsx | 105 ++++++++++++++++++++++++++------------ 5 files changed, 200 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 3237547..9628b91 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - `選隊伍`:可依指定日期從 DB 載入隊伍資料,若沒有資料可改用手動配對 - `記分板`:支援先攻設定、直接點分數記分、上一步回退、上下換隊、左右換位 -- `歷史戰績`:直接從 DB 的 `history` 表讀取戰績列表,點開可看得分紀錄 +- `歷史戰績`:直接從 DB 的 `history` 表讀取戰績列表,點開可看得分紀錄,也能直接刪除單筆紀錄 ## 目前記分板流程 @@ -27,6 +27,14 @@ - 設定隊伍彈窗會優先壓縮內容高度 - 若名單較多,改為彈窗內局部捲動,不影響整頁閱讀 +## 歷史戰績功能 + +- 歷史列表直接從 DB 的 `history` 表讀取 +- 點列表卡片可查看得分過程 +- 每筆列表右側都有 `刪除此筆` 按鈕 +- 刪除前會跳出確認視窗 +- 刪除成功後列表會即時更新 + ## Port 設定 ### 本機開發 diff --git a/server/server.mjs b/server/server.mjs index bfdd322..5d220d9 100644 --- a/server/server.mjs +++ b/server/server.mjs @@ -201,6 +201,53 @@ app.get('/api/history', async (_request, response) => { } }) +app.delete('/api/history/:id', async (request, response) => { + if (!pool) { + response.status(500).json({ + ok: false, + message: `DB 設定不完整,缺少:${missingEnv.join(', ')}`, + }) + return + } + + const id = Number(request.params.id) + + if (!Number.isInteger(id) || id <= 0) { + response.status(400).json({ + ok: false, + message: '戰績編號格式不正確。', + }) + return + } + + try { + await ensureHistoryTable(pool, historyTableName) + const [result] = await pool.execute( + `DELETE FROM \`${historyTableName}\` WHERE id = ? LIMIT 1`, + [id], + ) + + if (result.affectedRows === 0) { + response.status(404).json({ + ok: false, + message: '找不到要刪除的戰績。', + }) + return + } + + response.json({ + ok: true, + message: '戰績已刪除。', + }) + } catch (error) { + console.error('history delete error:', error) + response.status(500).json({ + ok: false, + message: error instanceof Error ? error.message : '刪除戰績失敗。', + }) + } +}) + if (distReady) { app.use(express.static(distDir)) diff --git a/src/App.css b/src/App.css index 47ce24c..e40eb7d 100644 --- a/src/App.css +++ b/src/App.css @@ -335,6 +335,53 @@ box-shadow: 0 12px 28px rgba(8, 47, 73, 0.08); } +.history-card-shell { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 14px; + align-items: center; +} + +.history-card-content { + padding: 0; + background: transparent; +} + +.history-delete-button { + border: 0; + border-radius: 999px; + padding: 12px 16px; + cursor: pointer; + font: inherit; + color: #fff; + background: linear-gradient(180deg, #e57a63, #c44c3d); + box-shadow: + inset 0 0 0 1px rgba(161, 54, 37, 0.22), + 0 10px 18px rgba(8, 47, 73, 0.12); + transition: + transform 0.16s ease, + box-shadow 0.16s ease, + filter 0.16s ease, + opacity 0.16s ease; +} + +.history-delete-button:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: + inset 0 0 0 1px rgba(161, 54, 37, 0.28), + 0 14px 22px rgba(8, 47, 73, 0.16); +} + +.history-delete-button:active:not(:disabled) { + transform: translateY(0); + filter: brightness(0.98); +} + +.history-delete-button:disabled { + cursor: default; + opacity: 0.62; +} + .scoreboard-screen { display: grid; grid-template-columns: minmax(0, 1fr) 160px; @@ -1413,6 +1460,15 @@ gap: 4px; } + .history-card-shell { + grid-template-columns: 1fr; + gap: 10px; + } + + .history-delete-button { + width: 100%; + } + .team-picker-ribbon { left: 18px; right: 90px; diff --git a/src/lib/api.ts b/src/lib/api.ts index 46cd5a1..4873996 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -62,6 +62,21 @@ export async function loadHistoryList() { return (payload.data ?? []).map(normalizeHistoryRecord) } +export async function deleteHistoryItem(id: number) { + const response = await fetch(`/api/history/${id}`, { + method: 'DELETE', + }) + + const payload = (await response.json()) as { + ok?: boolean + message?: string + } + + if (!response.ok || !payload.ok) { + throw new Error(payload.message ?? '刪除歷史戰績失敗。') + } +} + function normalizeHistoryRecord(record: HistoryRecord): HistoryListItem { const score = parseJson<[number, number]>(record.score, [0, 0]) const players = parseJson(record.players, []) diff --git a/src/pages/HistoryPage.tsx b/src/pages/HistoryPage.tsx index 55e92a0..89f78cb 100644 --- a/src/pages/HistoryPage.tsx +++ b/src/pages/HistoryPage.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { loadHistoryList } from '../lib/api' +import { deleteHistoryItem, loadHistoryList } from '../lib/api' import type { HistoryListItem } from '../types' export function HistoryPage() { @@ -7,6 +7,7 @@ export function HistoryPage() { const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [selectedItem, setSelectedItem] = useState(null) + const [deletingId, setDeletingId] = useState(null) useEffect(() => { let active = true @@ -28,7 +29,7 @@ export function HistoryPage() { return } - setError(fetchError instanceof Error ? fetchError.message : '無法讀取歷史戰績。') + setError(fetchError instanceof Error ? fetchError.message : '讀取歷史戰績失敗。') } finally { if (active) { setLoading(false) @@ -43,6 +44,29 @@ export function HistoryPage() { } }, []) + const handleDelete = async (item: HistoryListItem) => { + const confirmed = window.confirm( + `確定要刪除這筆戰績嗎?\n${item.leftTeamName} vs ${item.rightTeamName}`, + ) + + if (!confirmed) { + return + } + + setDeletingId(item.id) + setError('') + + try { + await deleteHistoryItem(item.id) + setHistory((current) => current.filter((entry) => entry.id !== item.id)) + setSelectedItem((current) => (current?.id === item.id ? null : current)) + } catch (deleteError) { + setError(deleteError instanceof Error ? deleteError.message : '刪除戰績失敗。') + } finally { + setDeletingId(null) + } + } + return ( <>
@@ -50,7 +74,7 @@ export function HistoryPage() {

History

歷史戰績

- 這裡直接顯示 DB 的 `history` 列表。點任一筆戰績,可快速查看該場的得分過程。 + 這裡會直接讀取資料庫 `history` 表中的比賽紀錄。點擊卡片可查看得分過程,右側按鈕可直接刪除此筆紀錄。

@@ -58,7 +82,7 @@ export function HistoryPage() { {loading ? (

正在讀取戰績

-

請稍候一下,正在從 DB 載入列表。

+

系統正在從資料庫載入歷史列表。

) : error ? (
@@ -68,35 +92,47 @@ export function HistoryPage() { ) : history.length === 0 ? (

目前沒有戰績

-

DB 的 `history` 資料表目前沒有可顯示的紀錄。

+

資料庫 `history` 表內還沒有可顯示的紀錄。

) : (
{history.map((item) => ( -
-
- 比分:{item.score[0]} - {item.score[1]} - 類型:{item.typeLabel} - 勝利分數:{item.winScore} -
- +
+ + 比分:{item.score[0]} - {item.score[1]} + + 模式:{item.typeLabel} + 目標分:{item.winScore} +
+ + + + ))}
)} @@ -118,8 +154,13 @@ type HistoryReplayModalProps = { function HistoryReplayModal({ item, onClose }: HistoryReplayModalProps) { return (
-
-

點任意位置關閉

+
event.stopPropagation()} + > +

得分紀錄

{item.leftTeamName} vs {item.rightTeamName}

@@ -139,12 +180,12 @@ function HistoryReplayModal({ item, onClose }: HistoryReplayModalProps) {
{item.playedAt} {item.typeLabel} - 勝方:{item.winnerTeamName} + 勝隊:{item.winnerTeamName}
{item.scoreList.length === 0 ? ( -

這筆資料沒有得分過程。

+

這筆資料沒有完整的得分過程。

) : ( item.scoreList.map(([round, starter, winCount, winner]) => (
@@ -162,5 +203,5 @@ function HistoryReplayModal({ item, onClose }: HistoryReplayModalProps) { } function getStarterName(item: HistoryListItem, starter: number) { - return item.players[starter] ?? '未知玩家' + return item.players[starter] ?? '未知球員' }