整理設定隊伍介面並更新 README
This commit is contained in:
+227
-61
@@ -657,14 +657,15 @@
|
||||
z-index: 60;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 20px;
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.52);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.team-picker-shell {
|
||||
position: relative;
|
||||
width: min(1080px, 100%);
|
||||
width: min(980px, 100%);
|
||||
max-height: calc(100dvh - 24px);
|
||||
}
|
||||
|
||||
.team-picker-close {
|
||||
@@ -715,50 +716,58 @@
|
||||
|
||||
.team-picker-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
gap: 22px;
|
||||
padding: 34px 22px 22px;
|
||||
border-radius: 34px;
|
||||
grid-template-columns: minmax(0, 1fr) 260px;
|
||||
gap: 16px;
|
||||
padding: 22px 16px 16px;
|
||||
border-radius: 28px;
|
||||
background: rgba(20, 10, 6, 0.18);
|
||||
max-height: calc(100dvh - 52px);
|
||||
}
|
||||
|
||||
.team-picker-panel {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
padding: 18px;
|
||||
border-radius: 26px;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(180deg, #fff8e8, #ffe5ad);
|
||||
box-shadow:
|
||||
0 0 0 4px rgba(255, 255, 255, 0.18),
|
||||
inset 0 0 0 2px rgba(200, 140, 46, 0.45);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.team-picker-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.team-picker-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 86px;
|
||||
min-height: 54px;
|
||||
padding: 0 18px;
|
||||
border-radius: 18px;
|
||||
font-size: 1.5rem;
|
||||
min-width: 72px;
|
||||
min-height: 46px;
|
||||
padding: 0 14px;
|
||||
border-radius: 16px;
|
||||
font-size: 1.25rem;
|
||||
color: #4a2e1d;
|
||||
background: linear-gradient(180deg, #f5d89f, #ecc170);
|
||||
}
|
||||
|
||||
.team-picker-title p {
|
||||
margin-top: 4px;
|
||||
margin-top: 2px;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.team-picker-config-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.team-picker-config {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
color: #4a2e1d;
|
||||
}
|
||||
|
||||
@@ -766,6 +775,16 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.team-picker-config-compact {
|
||||
grid-template-columns: auto auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.team-picker-config-compact span {
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.team-picker-score-input {
|
||||
width: 100%;
|
||||
max-width: 140px;
|
||||
@@ -777,22 +796,29 @@
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.team-picker-score-input-compact {
|
||||
width: 72px;
|
||||
max-width: 72px;
|
||||
padding: 8px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.team-picker-list {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
max-height: min(62vh, 760px);
|
||||
gap: 10px;
|
||||
max-height: min(48dvh, 430px);
|
||||
overflow: auto;
|
||||
padding-right: 8px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.team-picker-option {
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 18px 16px;
|
||||
padding: 12px 12px;
|
||||
border: 1px solid rgba(124, 98, 61, 0.18);
|
||||
border-radius: 18px;
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
color: #2e231b;
|
||||
@@ -832,24 +858,25 @@
|
||||
}
|
||||
|
||||
.team-picker-option strong,
|
||||
.picked-team-card strong {
|
||||
.preset-team-card strong {
|
||||
display: block;
|
||||
font-size: 1.35rem;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.team-picker-option small,
|
||||
.picked-team-card small,
|
||||
.preset-team-card small,
|
||||
.picker-side-hint {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
margin-top: 4px;
|
||||
color: #7b6148;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.team-picker-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.team-picker-ghost,
|
||||
@@ -857,7 +884,7 @@
|
||||
.team-picker-clear {
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 16px 18px;
|
||||
padding: 12px 14px;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
box-shadow:
|
||||
@@ -900,44 +927,68 @@
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.picked-team-list {
|
||||
.preset-team-block {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
gap: 10px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.picked-team-card {
|
||||
.preset-team-head {
|
||||
display: grid;
|
||||
grid-template-columns: 72px minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
padding: 18px 16px;
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 249, 238, 0.92);
|
||||
border: 1px solid rgba(124, 98, 61, 0.16);
|
||||
}
|
||||
|
||||
.picked-team-index {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 999px;
|
||||
font-size: 2rem;
|
||||
color: #5b2f13;
|
||||
background: linear-gradient(180deg, #ffc84d, #f2a316);
|
||||
}
|
||||
|
||||
.picker-mode-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 2px;
|
||||
color: #4a2e1d;
|
||||
}
|
||||
|
||||
.picker-mode-toggle input {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
.preset-team-head small {
|
||||
color: #7b6148;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.preset-team-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-height: min(44dvh, 360px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.preset-team-card {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border: 1px solid rgba(124, 98, 61, 0.16);
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
background: rgba(255, 249, 238, 0.92);
|
||||
transition:
|
||||
transform 0.16s ease,
|
||||
box-shadow 0.16s ease,
|
||||
border-color 0.16s ease;
|
||||
}
|
||||
|
||||
.preset-team-card:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(199, 155, 83, 0.34);
|
||||
box-shadow: 0 12px 22px rgba(8, 47, 73, 0.08);
|
||||
}
|
||||
|
||||
.preset-team-card-active {
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
box-shadow: 0 10px 20px rgba(147, 104, 35, 0.12);
|
||||
}
|
||||
|
||||
.preset-team-index {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 999px;
|
||||
font-size: 1.1rem;
|
||||
color: #5b2f13;
|
||||
background: linear-gradient(180deg, #ffc84d, #f2a316);
|
||||
}
|
||||
|
||||
.team-picker-clear {
|
||||
@@ -1155,7 +1206,6 @@
|
||||
.summary-grid,
|
||||
.double-grid,
|
||||
.history-meta,
|
||||
.team-picker-layout,
|
||||
.selection-toolbar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -1187,6 +1237,11 @@
|
||||
.team-head-main {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.team-picker-layout {
|
||||
grid-template-columns: minmax(0, 1fr) 220px;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@@ -1361,5 +1416,116 @@
|
||||
.team-picker-ribbon {
|
||||
left: 18px;
|
||||
right: 90px;
|
||||
top: -18px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 18px;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.team-picker-shell {
|
||||
max-height: calc(100dvh - 12px);
|
||||
}
|
||||
|
||||
.team-picker-layout {
|
||||
grid-template-columns: minmax(0, 1fr) 156px;
|
||||
gap: 8px;
|
||||
padding: 18px 10px 10px;
|
||||
border-radius: 20px;
|
||||
max-height: calc(100dvh - 24px);
|
||||
}
|
||||
|
||||
.team-picker-panel {
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.team-picker-title {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.team-picker-count {
|
||||
min-width: 56px;
|
||||
min-height: 38px;
|
||||
padding: 0 10px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.team-picker-title strong {
|
||||
font-size: 0.96rem;
|
||||
}
|
||||
|
||||
.team-picker-title p {
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.team-picker-config-compact span {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.team-picker-score-input-compact {
|
||||
width: 58px;
|
||||
max-width: 58px;
|
||||
padding: 6px 8px;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.team-picker-list {
|
||||
gap: 6px;
|
||||
max-height: min(46dvh, 330px);
|
||||
}
|
||||
|
||||
.team-picker-option {
|
||||
grid-template-columns: 24px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
padding: 9px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.team-picker-checkbox {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.team-picker-option strong,
|
||||
.preset-team-card strong {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.team-picker-option small,
|
||||
.preset-team-card small,
|
||||
.picker-side-hint,
|
||||
.preset-team-head small {
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.team-picker-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.team-picker-ghost,
|
||||
.team-picker-confirm,
|
||||
.team-picker-clear {
|
||||
padding: 10px 10px;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.preset-team-list {
|
||||
gap: 6px;
|
||||
max-height: min(42dvh, 300px);
|
||||
}
|
||||
|
||||
.preset-team-card {
|
||||
grid-template-columns: 34px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.preset-team-index {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
}
|
||||
|
||||
+106
-45
@@ -103,6 +103,12 @@ export function ScoreboardPage({
|
||||
return players
|
||||
}, [selectedGroup])
|
||||
|
||||
const presetTeams = useMemo(
|
||||
() =>
|
||||
selectedGroup?.teams.filter((team) => !team.isPlaceholderA && !team.isPlaceholderB) ?? [],
|
||||
[selectedGroup],
|
||||
)
|
||||
|
||||
const canArrangeMatch = !hasRecordedPoint
|
||||
const canScore = scoreState.serving !== null
|
||||
|
||||
@@ -198,6 +204,26 @@ export function ScoreboardPage({
|
||||
})
|
||||
}
|
||||
|
||||
const togglePresetTeam = (team: GroupTeam) => {
|
||||
setDraftPlayers((current) => {
|
||||
const removed = removePresetTeamFromDraft(current, team)
|
||||
|
||||
if (removed.length !== current.length) {
|
||||
return removed
|
||||
}
|
||||
|
||||
if (current.length >= 4 || current.length % 2 !== 0) {
|
||||
return current
|
||||
}
|
||||
|
||||
if (current.includes(team.playerA) || current.includes(team.playerB)) {
|
||||
return current
|
||||
}
|
||||
|
||||
return [...current, team.playerA, team.playerB]
|
||||
})
|
||||
}
|
||||
|
||||
const confirmDraftTeams = () => {
|
||||
if (draftPlayers.length !== 4) {
|
||||
return
|
||||
@@ -305,10 +331,10 @@ export function ScoreboardPage({
|
||||
|
||||
{pickerOpen ? (
|
||||
<TeamPickerModal
|
||||
currentSelectionOrder={currentSelectionOrder}
|
||||
draftPlayers={draftPlayers}
|
||||
draftTargetScore={draftTargetScore}
|
||||
group={selectedGroup}
|
||||
presetTeams={presetTeams}
|
||||
selectablePlayers={selectablePlayers}
|
||||
selectionCount={draftPlayers.length}
|
||||
sourceLabel={groupSource === 'db' ? 'DB' : groupSource === 'manual' ? '手動' : '-'}
|
||||
@@ -319,6 +345,7 @@ export function ScoreboardPage({
|
||||
onConfirm={confirmDraftTeams}
|
||||
onDraftTargetScoreChange={setDraftTargetScore}
|
||||
onTogglePlayer={toggleDraftPlayer}
|
||||
onTogglePresetTeam={togglePresetTeam}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -473,10 +500,10 @@ function ScoreboardTeamPanel({
|
||||
}
|
||||
|
||||
type TeamPickerModalProps = {
|
||||
currentSelectionOrder: string[]
|
||||
draftPlayers: string[]
|
||||
draftTargetScore: string
|
||||
group: RoundGroup
|
||||
presetTeams: GroupTeam[]
|
||||
selectablePlayers: string[]
|
||||
selectionCount: number
|
||||
sourceLabel: string
|
||||
@@ -487,13 +514,14 @@ type TeamPickerModalProps = {
|
||||
onConfirm: () => void
|
||||
onDraftTargetScoreChange: (value: string) => void
|
||||
onTogglePlayer: (playerName: string) => void
|
||||
onTogglePresetTeam: (team: GroupTeam) => void
|
||||
}
|
||||
|
||||
function TeamPickerModal({
|
||||
currentSelectionOrder,
|
||||
draftPlayers,
|
||||
draftTargetScore,
|
||||
group,
|
||||
presetTeams,
|
||||
selectablePlayers,
|
||||
selectionCount,
|
||||
sourceLabel,
|
||||
@@ -504,20 +532,8 @@ function TeamPickerModal({
|
||||
onConfirm,
|
||||
onDraftTargetScoreChange,
|
||||
onTogglePlayer,
|
||||
onTogglePresetTeam,
|
||||
}: TeamPickerModalProps) {
|
||||
const draftTeams = [
|
||||
draftPlayers.length >= 2 ? `${draftPlayers[0]} / ${draftPlayers[1]}` : '尚未選滿 2 位',
|
||||
draftPlayers.length >= 4 ? `${draftPlayers[2]} / ${draftPlayers[3]}` : '尚未選滿 2 位',
|
||||
]
|
||||
const currentTeams = [
|
||||
currentSelectionOrder.length >= 2
|
||||
? `${currentSelectionOrder[0]} / ${currentSelectionOrder[1]}`
|
||||
: null,
|
||||
currentSelectionOrder.length >= 4
|
||||
? `${currentSelectionOrder[2]} / ${currentSelectionOrder[3]}`
|
||||
: null,
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="team-picker-overlay" role="presentation" onClick={onClose}>
|
||||
<div
|
||||
@@ -540,24 +556,26 @@ function TeamPickerModal({
|
||||
<div className="team-picker-title">
|
||||
<span className="team-picker-count">{selectionCount}/4</span>
|
||||
<div>
|
||||
<strong>依序選人後自動配隊</strong>
|
||||
<strong>左邊逐一選人</strong>
|
||||
<p>
|
||||
第 {group.id} 組 / {sourceLabel} / {targetDate || '-'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="team-picker-config">
|
||||
<span>幾分獲勝</span>
|
||||
<input
|
||||
className="team-picker-score-input"
|
||||
inputMode="numeric"
|
||||
maxLength={2}
|
||||
type="text"
|
||||
value={draftTargetScore}
|
||||
onChange={(event) => onDraftTargetScoreChange(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<div className="team-picker-config-row">
|
||||
<label className="team-picker-config team-picker-config-compact">
|
||||
<span>勝利分數</span>
|
||||
<input
|
||||
className="team-picker-score-input team-picker-score-input-compact"
|
||||
inputMode="numeric"
|
||||
maxLength={2}
|
||||
type="text"
|
||||
value={draftTargetScore}
|
||||
onChange={(event) => onDraftTargetScoreChange(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="team-picker-list">
|
||||
{selectablePlayers.map((playerName) => {
|
||||
@@ -605,24 +623,40 @@ function TeamPickerModal({
|
||||
</section>
|
||||
|
||||
<aside className="team-picker-panel team-picker-side-panel">
|
||||
<div className="picked-team-list">
|
||||
{[0, 1].map((slotIndex) => {
|
||||
const teamLabel = draftTeams[slotIndex]
|
||||
const isCurrent = currentTeams[slotIndex] === teamLabel
|
||||
<div className="preset-team-block">
|
||||
<div className="preset-team-head">
|
||||
<strong>右邊快速選預設隊伍</strong>
|
||||
<small>直接點兩組,或搭配左邊逐一選人</small>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div className="picked-team-card" key={`picked-${slotIndex}`}>
|
||||
<span className="picked-team-index">{slotIndex + 1}</span>
|
||||
<div>
|
||||
<strong>{teamLabel}</strong>
|
||||
<small>
|
||||
{slotIndex === 0 ? '第 1、2 位自動成隊' : '第 3、4 位自動成隊'}
|
||||
{isCurrent ? ' / 目前場上配置' : ''}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="preset-team-list">
|
||||
{presetTeams.map((team) => {
|
||||
const selectedSlot = getPresetTeamSelectionSlot(draftPlayers, team)
|
||||
|
||||
return (
|
||||
<button
|
||||
className={
|
||||
selectedSlot === null
|
||||
? 'preset-team-card'
|
||||
: 'preset-team-card preset-team-card-active'
|
||||
}
|
||||
key={`preset-team-${team.id}`}
|
||||
type="button"
|
||||
onClick={() => onTogglePresetTeam(team)}
|
||||
>
|
||||
<span className="preset-team-index">{team.id}</span>
|
||||
<div>
|
||||
<strong>{getTeamDisplayName(team)}</strong>
|
||||
<small>
|
||||
{selectedSlot === null
|
||||
? '點擊帶入這組預設隊伍'
|
||||
: `已帶入第 ${selectedSlot === 0 ? '1' : '2'} 隊`}
|
||||
</small>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="team-picker-clear" type="button" onClick={onClear}>
|
||||
@@ -737,6 +771,33 @@ function sanitizeTargetScore(value: string) {
|
||||
return Math.min(99, Math.max(1, numeric))
|
||||
}
|
||||
|
||||
function removePresetTeamFromDraft(players: string[], team: GroupTeam) {
|
||||
const firstPairSelected = players[0] === team.playerA && players[1] === team.playerB
|
||||
const secondPairSelected = players[2] === team.playerA && players[3] === team.playerB
|
||||
|
||||
if (firstPairSelected) {
|
||||
return players.slice(2)
|
||||
}
|
||||
|
||||
if (secondPairSelected) {
|
||||
return players.slice(0, 2)
|
||||
}
|
||||
|
||||
return players
|
||||
}
|
||||
|
||||
function getPresetTeamSelectionSlot(players: string[], team: GroupTeam) {
|
||||
if (players[0] === team.playerA && players[1] === team.playerB) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (players[2] === team.playerA && players[3] === team.playerB) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function formatClock() {
|
||||
return new Date().toLocaleTimeString('zh-TW', {
|
||||
hour: '2-digit',
|
||||
|
||||
Reference in New Issue
Block a user