新增歷史戰績刪除功能並更新 README

This commit is contained in:
2026-04-16 10:26:58 +08:00
parent 31168e830b
commit bbedb70e7e
5 changed files with 200 additions and 33 deletions

View File

@@ -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;

View File

@@ -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<string[]>(record.players, [])

View File

@@ -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<HistoryListItem | null>(null)
const [deletingId, setDeletingId] = useState<number | null>(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 (
<>
<section className="page-grid">
@@ -50,7 +74,7 @@ export function HistoryPage() {
<p className="panel-kicker">History</p>
<h2></h2>
<p className="panel-copy">
DB `history`
`history`
</p>
</article>
@@ -58,7 +82,7 @@ export function HistoryPage() {
{loading ? (
<div className="empty-state">
<h3></h3>
<p> DB </p>
<p></p>
</div>
) : error ? (
<div className="empty-state">
@@ -68,35 +92,47 @@ export function HistoryPage() {
) : history.length === 0 ? (
<div className="empty-state">
<h3></h3>
<p>DB `history` </p>
<p> `history` </p>
</div>
) : (
<div className="history-list">
{history.map((item) => (
<button
className="history-card history-card-button"
key={item.id}
type="button"
onClick={() => setSelectedItem(item)}
>
<div className="history-head">
<div>
<p className="panel-kicker">
{item.playedAt} / {item.dayLabel}
</p>
<h3>
{item.leftTeamName} vs {item.rightTeamName}
</h3>
<article className="history-card history-card-shell" key={item.id}>
<button
className="history-card-button history-card-content"
type="button"
onClick={() => setSelectedItem(item)}
>
<div className="history-head">
<div>
<p className="panel-kicker">
{item.playedAt} / {item.dayLabel}
</p>
<h3>
{item.leftTeamName} vs {item.rightTeamName}
</h3>
</div>
<span className="winner-badge">{item.winnerTeamName}</span>
</div>
<span className="winner-badge">{item.winnerTeamName}</span>
</div>
<div className="history-meta">
<span>{item.score[0]} - {item.score[1]}</span>
<span>{item.typeLabel}</span>
<span>{item.winScore}</span>
</div>
</button>
<div className="history-meta">
<span>
{item.score[0]} - {item.score[1]}
</span>
<span>{item.typeLabel}</span>
<span>{item.winScore}</span>
</div>
</button>
<button
className="history-delete-button"
disabled={deletingId === item.id}
type="button"
onClick={() => void handleDelete(item)}
>
{deletingId === item.id ? '刪除中...' : '刪除此筆'}
</button>
</article>
))}
</div>
)}
@@ -118,8 +154,13 @@ type HistoryReplayModalProps = {
function HistoryReplayModal({ item, onClose }: HistoryReplayModalProps) {
return (
<div className="history-modal-overlay" role="presentation" onClick={onClose}>
<div aria-modal="true" className="history-modal" role="dialog">
<p className="panel-kicker"></p>
<div
aria-modal="true"
className="history-modal"
role="dialog"
onClick={(event) => event.stopPropagation()}
>
<p className="panel-kicker"></p>
<h3>
{item.leftTeamName} vs {item.rightTeamName}
</h3>
@@ -139,12 +180,12 @@ function HistoryReplayModal({ item, onClose }: HistoryReplayModalProps) {
<div className="history-modal-summary">
<span>{item.playedAt}</span>
<span>{item.typeLabel}</span>
<span>{item.winnerTeamName}</span>
<span>{item.winnerTeamName}</span>
</div>
<div className="history-replay-list">
{item.scoreList.length === 0 ? (
<p className="history-replay-empty"></p>
<p className="history-replay-empty"></p>
) : (
item.scoreList.map(([round, starter, winCount, winner]) => (
<div className="history-replay-row" key={`${item.id}-${round}`}>
@@ -162,5 +203,5 @@ function HistoryReplayModal({ item, onClose }: HistoryReplayModalProps) {
}
function getStarterName(item: HistoryListItem, starter: number) {
return item.players[starter] ?? '未知玩家'
return item.players[starter] ?? '未知球員'
}