feat(CourtDrawing): enhance CourtDrawingDialog and CourtDrawingTool components
- Updated CourtDrawingDialog to improve layout and responsiveness, including a new drawing dialog body structure and styling adjustments. - Enhanced CourtDrawingTool with a comprehensive exercise selection workflow, allowing users to configure exercises with detailed options for start positions, strokes, and targets. - Introduced new UI elements for better user interaction and streamlined the process of adding additional strokes. - Improved overall styling for a more cohesive and user-friendly experience.
This commit is contained in:
@@ -4,40 +4,46 @@
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
:title="$t('courtDrawing.title')"
|
||||
size="large"
|
||||
width="92vw"
|
||||
:max-width="1500"
|
||||
:close-on-overlay="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<CourtDrawingTool
|
||||
ref="drawingTool"
|
||||
:drawing-data="initialDrawingData"
|
||||
:allow-image-upload="false"
|
||||
@update-drawing-data="handleDrawingDataUpdate"
|
||||
@update-fields="handleFieldsUpdate"
|
||||
/>
|
||||
|
||||
<div v-if="showDiaryFields" class="diary-fields">
|
||||
<label v-if="showDurationFields" class="diary-field">
|
||||
<span>{{ $t('courtDrawing.durationText') }}</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="diaryFields.durationText"
|
||||
:placeholder="$t('courtDrawing.durationTextPlaceholder')"
|
||||
@input="calculateDurationFromText"
|
||||
<div class="drawing-dialog-body">
|
||||
<div class="drawing-tool-shell">
|
||||
<CourtDrawingTool
|
||||
ref="drawingTool"
|
||||
:drawing-data="initialDrawingData"
|
||||
:allow-image-upload="false"
|
||||
@update-drawing-data="handleDrawingDataUpdate"
|
||||
@update-fields="handleFieldsUpdate"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="showDurationFields" class="diary-field">
|
||||
<span>{{ $t('courtDrawing.durationMinutes') }}</span>
|
||||
<input type="number" min="0" :value="diaryFields.duration" readonly />
|
||||
</label>
|
||||
<label v-if="showGroupSelect" class="diary-field">
|
||||
<span>{{ $t('courtDrawing.group') }}</span>
|
||||
<select v-model="diaryFields.groupId">
|
||||
<option value="">{{ $t('courtDrawing.selectGroup') }}</option>
|
||||
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
|
||||
{{ group.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="showDiaryFields" class="diary-fields">
|
||||
<label v-if="showDurationFields" class="diary-field">
|
||||
<span>{{ $t('courtDrawing.durationText') }}</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="diaryFields.durationText"
|
||||
:placeholder="$t('courtDrawing.durationTextPlaceholder')"
|
||||
@input="calculateDurationFromText"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="showDurationFields" class="diary-field">
|
||||
<span>{{ $t('courtDrawing.durationMinutes') }}</span>
|
||||
<input type="number" min="0" :value="diaryFields.duration" readonly />
|
||||
</label>
|
||||
<label v-if="showGroupSelect" class="diary-field">
|
||||
<span>{{ $t('courtDrawing.group') }}</span>
|
||||
<select v-model="diaryFields.groupId">
|
||||
<option value="">{{ $t('courtDrawing.selectGroup') }}</option>
|
||||
<option v-for="group in groups" :key="group.id" :value="String(group.id)">
|
||||
{{ group.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -266,6 +272,23 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.dialog-body) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.drawing-dialog-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
height: min(78vh, calc(100vh - 170px));
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.drawing-tool-shell {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
padding: 0.5rem 1.5rem;
|
||||
|
||||
@@ -1,12 +1,207 @@
|
||||
<template>
|
||||
<div class="court-drawing-tool">
|
||||
<div class="tool-header">
|
||||
<h4>{{ $t('courtDrawingTool.title') }}</h4>
|
||||
<div class="tool-layout">
|
||||
<div class="exercise-selection">
|
||||
<h5>{{ $t('courtDrawingTool.configureExercise') }}</h5>
|
||||
<div class="workflow-grid">
|
||||
<section class="workflow-card">
|
||||
<button type="button" class="workflow-toggle" @click="toggleStartSection">
|
||||
<div>
|
||||
<div class="workflow-step">1. Startposition</div>
|
||||
<p class="workflow-copy workflow-copy-compact">Aus welcher Aufschlagposition startet die Übung?</p>
|
||||
</div>
|
||||
<span class="workflow-toggle-icon">{{ startSectionExpanded ? '−' : '+' }}</span>
|
||||
</button>
|
||||
<div v-if="startSectionExpanded" class="workflow-section-body">
|
||||
<div class="choice-row">
|
||||
<button
|
||||
v-for="start in startPositionOptions"
|
||||
:key="start.value"
|
||||
type="button"
|
||||
:class="['choice-chip', { 'choice-chip-active': selectedStartPosition === start.value }]"
|
||||
@click="selectStartPosition(start.value)"
|
||||
>
|
||||
<strong>{{ start.short }}</strong>
|
||||
<span>{{ start.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="workflow-card">
|
||||
<button type="button" class="workflow-toggle" @click="toggleFirstStrokeSection">
|
||||
<div>
|
||||
<div class="workflow-step">2. 1. Schlag</div>
|
||||
<p class="workflow-copy workflow-copy-compact">Schlagseite und Rotation für den ersten Ball festlegen.</p>
|
||||
</div>
|
||||
<span class="workflow-toggle-icon">{{ firstStrokeSectionExpanded ? '−' : '+' }}</span>
|
||||
</button>
|
||||
<div v-if="firstStrokeSectionExpanded" class="workflow-section-body">
|
||||
<div class="workflow-block">
|
||||
<span class="group-label">{{ $t('courtDrawingTool.service') }}</span>
|
||||
<div class="choice-row">
|
||||
<button
|
||||
type="button"
|
||||
:class="['choice-chip', { 'choice-chip-active': strokeType === 'VH' }]"
|
||||
@click="strokeType = 'VH'"
|
||||
>
|
||||
<strong>VH</strong>
|
||||
<span>{{ $t('courtDrawingTool.forehand') }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['choice-chip', { 'choice-chip-active': strokeType === 'RH' }]"
|
||||
@click="strokeType = 'RH'"
|
||||
>
|
||||
<strong>RH</strong>
|
||||
<span>{{ $t('courtDrawingTool.backhand') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-block">
|
||||
<span class="group-label">{{ $t('courtDrawingTool.spin') }}</span>
|
||||
<div class="choice-row choice-row-dense">
|
||||
<button
|
||||
v-for="option in mainSpinOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
:class="['choice-chip', 'choice-chip-compact', { 'choice-chip-active': spinType === option.value }]"
|
||||
@click="spinType = option.value"
|
||||
>
|
||||
<strong>{{ option.short }}</strong>
|
||||
<span>{{ option.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="workflow-card">
|
||||
<button type="button" class="workflow-toggle" @click="toggleTargetSection">
|
||||
<div>
|
||||
<div class="workflow-step">3. Ziel</div>
|
||||
<p class="workflow-copy workflow-copy-compact">Zielzone für den ersten Schlag auswählen.</p>
|
||||
</div>
|
||||
<span class="workflow-toggle-icon">{{ targetSectionExpanded ? '−' : '+' }}</span>
|
||||
</button>
|
||||
<div v-if="targetSectionExpanded" class="workflow-section-body">
|
||||
<div class="workflow-block">
|
||||
<span class="group-label">{{ $t('courtDrawingTool.targetPosition') }}</span>
|
||||
<div class="target-grid target-grid-large">
|
||||
<button
|
||||
v-for="n in mainTargetPositions"
|
||||
:key="`main-target-${n}`"
|
||||
type="button"
|
||||
:class="['grid-btn', { 'is-active': targetPosition === String(n) }]"
|
||||
@click="selectMainTargetPosition(n)"
|
||||
:disabled="!spinType"
|
||||
>
|
||||
{{ n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="workflow-card workflow-card-wide" v-if="strokeType && spinType && targetPosition">
|
||||
<div class="workflow-step">4. Folgeschläge</div>
|
||||
<p class="workflow-copy">Optional weitere Bälle als Liste aufbauen.</p>
|
||||
|
||||
<div v-if="additionalStrokes.length" class="stroke-sequence-list">
|
||||
<div
|
||||
v-for="(stroke, index) in additionalStrokes"
|
||||
:key="`${stroke.counter}-${index}`"
|
||||
class="stroke-sequence-item"
|
||||
>
|
||||
<div class="stroke-sequence-index">{{ index + 2 }}</div>
|
||||
<div class="stroke-sequence-copy">{{ formatAdditionalStroke(stroke) }}</div>
|
||||
<button type="button" class="stroke-sequence-remove" @click="removeAdditionalStroke(index)">Entfernen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="stroke-sequence-empty">Noch keine Folgeschläge angelegt.</div>
|
||||
|
||||
<div class="next-stroke-selection">
|
||||
<div class="workflow-block">
|
||||
<span class="group-label">Seite</span>
|
||||
<div class="choice-row">
|
||||
<button
|
||||
type="button"
|
||||
:class="['choice-chip', { 'choice-chip-active': nextStrokeSide === 'VH' }]"
|
||||
@click="nextStrokeSide = 'VH'"
|
||||
>
|
||||
<strong>VH</strong>
|
||||
<span>{{ $t('courtDrawingTool.forehand') }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['choice-chip', { 'choice-chip-active': nextStrokeSide === 'RH' }]"
|
||||
@click="nextStrokeSide = 'RH'"
|
||||
>
|
||||
<strong>RH</strong>
|
||||
<span>{{ $t('courtDrawingTool.backhand') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="workflow-block">
|
||||
<span class="group-label">Schlagart</span>
|
||||
<div class="choice-row choice-row-dense">
|
||||
<button
|
||||
v-for="option in additionalStrokeTypeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
:class="['choice-chip', 'choice-chip-compact', { 'choice-chip-active': nextStrokeType === option.value }]"
|
||||
@click="nextStrokeType = option.value"
|
||||
:title="option.label"
|
||||
>
|
||||
<strong>{{ option.short }}</strong>
|
||||
<span>{{ option.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="workflow-block">
|
||||
<span class="group-label">Zielposition</span>
|
||||
<div class="target-grid target-grid-large">
|
||||
<button
|
||||
v-for="n in additionalTargetPositions"
|
||||
:key="`next-target-${n}`"
|
||||
type="button"
|
||||
:class="['grid-btn', { 'is-active': nextStrokeTargetPosition === String(n) }]"
|
||||
@click="selectAdditionalTargetPosition(n)"
|
||||
:disabled="additionalStrokes.length >= 4"
|
||||
>
|
||||
{{ n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary add-stroke-button"
|
||||
@click="addNextStroke"
|
||||
:disabled="additionalStrokes.length >= 4 || !nextStrokeTargetPosition"
|
||||
>
|
||||
Schlag hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="exercise-info" v-if="strokeType && spinType">
|
||||
<p><strong>Kürzel:</strong> {{ getFullCode() }}</p>
|
||||
<p><strong>Titel:</strong> {{ getFullTitle() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="canvas-container">
|
||||
<canvas
|
||||
ref="drawingCanvas"
|
||||
|
||||
<aside class="preview-panel">
|
||||
<div class="preview-panel-header">
|
||||
<div class="workflow-step">Vorschau</div>
|
||||
<p class="workflow-copy">Die Grafik erscheint, sobald der erste Schlag vollständig gesetzt ist.</p>
|
||||
</div>
|
||||
<div v-if="isPrimaryStrokeReady" class="canvas-container">
|
||||
<canvas
|
||||
ref="drawingCanvas"
|
||||
@mousedown="startDrawing"
|
||||
@mousemove="draw"
|
||||
@mouseup="stopDrawing"
|
||||
@@ -14,224 +209,13 @@
|
||||
:width="config.canvas.width"
|
||||
:height="config.canvas.height"
|
||||
></canvas>
|
||||
</div>
|
||||
<div v-else class="canvas-placeholder">
|
||||
<strong>1. Schlag vervollständigen</strong>
|
||||
<span>Wähle Startposition, Schlagseite, Rotation und Ziel. Danach erscheint die grafische Darstellung.</span>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- Startposition und Schlagart Auswahl -->
|
||||
<div class="exercise-selection" v-if="selectedStartPosition">
|
||||
<h5>{{ $t('courtDrawingTool.configureExercise') }}</h5>
|
||||
<div class="selection-group">
|
||||
<!-- Schlagart Auswahl -->
|
||||
<div class="stroke-selection">
|
||||
<div>
|
||||
<span class="group-label">{{ $t('courtDrawingTool.service') }}</span>
|
||||
<div class="stroke-buttons">
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke', { 'btn-primary': strokeType === 'VH', 'btn-secondary': strokeType !== 'VH' }]"
|
||||
@click="strokeType = 'VH'"
|
||||
:title="$t('courtDrawingTool.forehand')"
|
||||
>
|
||||
VH
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke', { 'btn-primary': strokeType === 'RH', 'btn-secondary': strokeType !== 'RH' }]"
|
||||
@click="strokeType = 'RH'"
|
||||
:title="$t('courtDrawingTool.backhand')"
|
||||
>
|
||||
RH
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schnittoption Auswahl -->
|
||||
<div class="spin-selection">
|
||||
<span class="group-label">{{ $t('courtDrawingTool.spin') }}</span>
|
||||
<div class="spin-buttons">
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Unterschnitt', 'btn-secondary': spinType !== 'Unterschnitt' }]"
|
||||
@click="spinType = 'Unterschnitt'"
|
||||
:title="$t('courtDrawingTool.underspin')"
|
||||
>
|
||||
US
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Überschnitt', 'btn-secondary': spinType !== 'Überschnitt' }]"
|
||||
@click="spinType = 'Überschnitt'"
|
||||
:title="$t('courtDrawingTool.topspin')"
|
||||
>
|
||||
OS
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Seitschnitt', 'btn-secondary': spinType !== 'Seitschnitt' }]"
|
||||
@click="spinType = 'Seitschnitt'"
|
||||
:title="$t('courtDrawingTool.sidespin')"
|
||||
>
|
||||
SR
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Seitunterschnitt', 'btn-secondary': spinType !== 'Seitunterschnitt' }]"
|
||||
@click="spinType = 'Seitunterschnitt'"
|
||||
:title="$t('courtDrawingTool.sideUnderspin')"
|
||||
>
|
||||
SU
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': spinType === 'Gegenläufer', 'btn-secondary': spinType !== 'Gegenläufer' }]"
|
||||
@click="spinType = 'Gegenläufer'"
|
||||
:title="$t('courtDrawingTool.counterSpin')"
|
||||
>
|
||||
GL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zielposition für Hauptschlag: explizite Auswahl 1–9 als Buttons -->
|
||||
<div class="target-selection" v-if="spinType">
|
||||
<span class="group-label">{{ $t('courtDrawingTool.targetPosition') }}</span>
|
||||
<div class="target-grid">
|
||||
<button
|
||||
v-for="n in mainTargetPositions"
|
||||
:key="`main-target-${n}`"
|
||||
type="button"
|
||||
:class="['grid-btn', { 'is-active': targetPosition === String(n) }]"
|
||||
@click="targetPosition = String(n)"
|
||||
>
|
||||
{{ n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Zusätzliche Schläge hinzufügen -->
|
||||
<div class="additional-strokes" v-if="strokeType && spinType && targetPosition">
|
||||
<div class="next-stroke-selection">
|
||||
<!-- Schlagart für zusätzlichen Schlag -->
|
||||
<div class="next-stroke-type">
|
||||
<span class="group-label">Seite:</span>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke', { 'btn-primary': nextStrokeSide === 'VH', 'btn-secondary': nextStrokeSide !== 'VH' }]"
|
||||
@click="nextStrokeSide = 'VH'"
|
||||
:title="$t('courtDrawingTool.forehand')"
|
||||
>
|
||||
VH
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke', { 'btn-primary': nextStrokeSide === 'RH', 'btn-secondary': nextStrokeSide !== 'RH' }]"
|
||||
@click="nextStrokeSide = 'RH'"
|
||||
:title="$t('courtDrawingTool.backhand')"
|
||||
>
|
||||
RH
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="next-stroke-buttons">
|
||||
<span class="group-label">Schlagart:</span>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'US', 'btn-secondary': nextStrokeType !== 'US' }]"
|
||||
@click="nextStrokeType = 'US'"
|
||||
title="Schupf"
|
||||
>
|
||||
US
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'OS', 'btn-secondary': nextStrokeType !== 'OS' }]"
|
||||
@click="nextStrokeType = 'OS'"
|
||||
title="Konter"
|
||||
>
|
||||
OS
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'TS', 'btn-secondary': nextStrokeType !== 'TS' }]"
|
||||
@click="nextStrokeType = 'TS'"
|
||||
title="Topspin"
|
||||
>
|
||||
TS
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'F', 'btn-secondary': nextStrokeType !== 'F' }]"
|
||||
@click="nextStrokeType = 'F'"
|
||||
title="Flip"
|
||||
>
|
||||
F
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'B', 'btn-secondary': nextStrokeType !== 'B' }]"
|
||||
@click="nextStrokeType = 'B'"
|
||||
title="Block"
|
||||
>
|
||||
B
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'SCH', 'btn-secondary': nextStrokeType !== 'SCH' }]"
|
||||
@click="nextStrokeType = 'SCH'"
|
||||
title="Schuss"
|
||||
>
|
||||
SCH
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'SAB', 'btn-secondary': nextStrokeType !== 'SAB' }]"
|
||||
@click="nextStrokeType = 'SAB'"
|
||||
title="Schnittabwehr"
|
||||
>
|
||||
SAB
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn-small', 'btn-stroke-type', { 'btn-primary': nextStrokeType === 'BAB', 'btn-secondary': nextStrokeType !== 'BAB' }]"
|
||||
@click="nextStrokeType = 'BAB'"
|
||||
title="Ballonabwehr"
|
||||
>
|
||||
BAB
|
||||
</button>
|
||||
</div>
|
||||
<!-- Zielposition für Zusatzschlag: explizite Auswahl 1–9 als Buttons -->
|
||||
<div class="next-target-selection">
|
||||
<span>Zielposition:</span>
|
||||
<div class="target-grid">
|
||||
<button
|
||||
v-for="n in additionalTargetPositions"
|
||||
:key="`next-target-${n}`"
|
||||
type="button"
|
||||
:class="['grid-btn', { 'is-active': nextStrokeTargetPosition === String(n) }]"
|
||||
@click="nextStrokeTargetPosition = String(n)"
|
||||
>
|
||||
{{ n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary btn-small add-stroke-button"
|
||||
@click="addNextStroke"
|
||||
:disabled="additionalStrokes.length >= 4"
|
||||
>
|
||||
Schlag hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="exercise-info" v-if="strokeType && spinType">
|
||||
<p><strong>Kürzel:</strong> {{ getFullCode() }}</p>
|
||||
<p><strong>Titel:</strong> {{ getFullTitle() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons entfernt: automatische Speicherung aktiv -->
|
||||
</div>
|
||||
@@ -394,10 +378,44 @@ export default {
|
||||
additionalStrokes: [],
|
||||
nextStrokeType: 'US',
|
||||
nextStrokeSide: 'VH',
|
||||
nextStrokeTargetPosition: ''
|
||||
nextStrokeTargetPosition: '',
|
||||
startSectionExpanded: true,
|
||||
firstStrokeSectionExpanded: true,
|
||||
targetSectionExpanded: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isPrimaryStrokeReady() {
|
||||
return Boolean(this.selectedStartPosition && this.strokeType && this.spinType && this.targetPosition);
|
||||
},
|
||||
startPositionOptions() {
|
||||
return [
|
||||
{ value: 'AS1', short: 'AS1', label: 'links' },
|
||||
{ value: 'AS2', short: 'AS2', label: 'mitte' },
|
||||
{ value: 'AS3', short: 'AS3', label: 'rechts' }
|
||||
];
|
||||
},
|
||||
mainSpinOptions() {
|
||||
return [
|
||||
{ value: 'Unterschnitt', short: 'US', label: this.$t('courtDrawingTool.underspin') },
|
||||
{ value: 'Überschnitt', short: 'OS', label: this.$t('courtDrawingTool.topspin') },
|
||||
{ value: 'Seitschnitt', short: 'SR', label: this.$t('courtDrawingTool.sidespin') },
|
||||
{ value: 'Seitunterschnitt', short: 'SU', label: this.$t('courtDrawingTool.sideUnderspin') },
|
||||
{ value: 'Gegenläufer', short: 'GL', label: this.$t('courtDrawingTool.counterSpin') }
|
||||
];
|
||||
},
|
||||
additionalStrokeTypeOptions() {
|
||||
return [
|
||||
{ value: 'US', short: 'US', label: 'Schupf' },
|
||||
{ value: 'OS', short: 'OS', label: 'Konter' },
|
||||
{ value: 'TS', short: 'TS', label: 'Topspin' },
|
||||
{ value: 'F', short: 'F', label: 'Flip' },
|
||||
{ value: 'B', short: 'B', label: 'Block' },
|
||||
{ value: 'SCH', short: 'SCH', label: 'Schuss' },
|
||||
{ value: 'SAB', short: 'SAB', label: 'Schnittabwehr' },
|
||||
{ value: 'BAB', short: 'BAB', label: 'Ballonabwehr' }
|
||||
];
|
||||
},
|
||||
// Reihenfolge der Positionen für Hauptschlag basierend auf Richtung
|
||||
mainTargetPositions() {
|
||||
const isLeftToRight = this.isMainStrokeLeftToRight();
|
||||
@@ -433,6 +451,17 @@ export default {
|
||||
this.emitDrawingData();
|
||||
this.updateTextFields();
|
||||
},
|
||||
isPrimaryStrokeReady(newVal) {
|
||||
if (newVal) {
|
||||
this.startSectionExpanded = false;
|
||||
this.firstStrokeSectionExpanded = false;
|
||||
this.targetSectionExpanded = false;
|
||||
this.$nextTick(() => {
|
||||
this.initCanvas();
|
||||
this.drawCourt(true);
|
||||
});
|
||||
}
|
||||
},
|
||||
selectedCirclePosition() {
|
||||
// bei Wechsel der Kreisposition: Daten + Strings aktualisieren
|
||||
this.emitDrawingData();
|
||||
@@ -473,6 +502,68 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
toggleStartSection() {
|
||||
this.startSectionExpanded = !this.startSectionExpanded;
|
||||
},
|
||||
toggleFirstStrokeSection() {
|
||||
this.firstStrokeSectionExpanded = !this.firstStrokeSectionExpanded;
|
||||
},
|
||||
toggleTargetSection() {
|
||||
this.targetSectionExpanded = !this.targetSectionExpanded;
|
||||
},
|
||||
selectStartPosition(position) {
|
||||
this.selectedStartPosition = position;
|
||||
this.selectedCirclePosition = position === 'AS1' ? 'top' : position === 'AS3' ? 'bottom' : 'middle';
|
||||
this.drawCourt();
|
||||
this.emitDrawingData();
|
||||
this.updateTextFields();
|
||||
},
|
||||
selectMainTargetPosition(position) {
|
||||
this.targetPosition = String(position);
|
||||
if (this.isPrimaryStrokeReady) {
|
||||
this.$nextTick(() => {
|
||||
this.initCanvas();
|
||||
this.drawCourt(true);
|
||||
});
|
||||
}
|
||||
this.emitDrawingData();
|
||||
this.updateTextFields();
|
||||
},
|
||||
selectAdditionalTargetPosition(position) {
|
||||
this.nextStrokeTargetPosition = String(position);
|
||||
this.emitDrawingData();
|
||||
},
|
||||
formatStrokeSide(side) {
|
||||
return side === 'VH' ? this.$t('courtDrawingTool.forehand') : this.$t('courtDrawingTool.backhand');
|
||||
},
|
||||
formatAdditionalStrokeType(type) {
|
||||
const option = this.additionalStrokeTypeOptions.find(entry => entry.value === type);
|
||||
return option ? option.label : type;
|
||||
},
|
||||
formatTargetPosition(position) {
|
||||
const labels = {
|
||||
'1': 'Vorhand lang',
|
||||
'2': 'Mitte lang',
|
||||
'3': 'Rückhand lang',
|
||||
'4': 'Vorhand halblang',
|
||||
'5': 'Mitte halblang',
|
||||
'6': 'Rückhand halblang',
|
||||
'7': 'Vorhand kurz',
|
||||
'8': 'Mitte kurz',
|
||||
'9': 'Rückhand kurz'
|
||||
};
|
||||
return labels[String(position)] || String(position);
|
||||
},
|
||||
formatAdditionalStroke(stroke) {
|
||||
return `${this.formatStrokeSide(stroke.side)} ${this.formatAdditionalStrokeType(stroke.type)} nach ${this.formatTargetPosition(stroke.targetPosition)}`;
|
||||
},
|
||||
removeAdditionalStroke(index) {
|
||||
this.additionalStrokes.splice(index, 1);
|
||||
this.nextStrokeTargetPosition = '';
|
||||
this.updateTextFields();
|
||||
this.drawCourt();
|
||||
this.emitDrawingData();
|
||||
},
|
||||
initCanvas() {
|
||||
this.canvas = this.$refs.drawingCanvas;
|
||||
if (this.canvas) {
|
||||
@@ -1642,12 +1733,15 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.court-drawing-tool {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 14px;
|
||||
padding: 1rem;
|
||||
background: var(--surface-color, #ffffff);
|
||||
margin: 1rem 0;
|
||||
box-shadow: 0 10px 26px rgba(15, 23, 42, 0.06);
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tool-header {
|
||||
@@ -1662,21 +1756,75 @@ export default {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.tool-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(360px, 430px) minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tool-controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
min-height: 0;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.exercise-selection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
padding-right: 0.25rem;
|
||||
padding: 1rem;
|
||||
background: var(--background-soft);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.preview-panel-header {
|
||||
padding: 0.9rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--surface-color, #ffffff);
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--background-soft);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.canvas-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
padding: 1rem;
|
||||
border: 1px dashed var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--background-soft);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.canvas-placeholder strong {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
cursor: crosshair;
|
||||
@@ -1784,81 +1932,141 @@ input[type="range"] {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.exercise-selection {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--background-soft);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.exercise-selection h5 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.selection-group {
|
||||
.workflow-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.workflow-card {
|
||||
padding: 0.9rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--surface-color, #ffffff);
|
||||
}
|
||||
|
||||
.workflow-card-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.workflow-step {
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.workflow-toggle {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.workflow-toggle-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
background: rgba(29, 90, 61, 0.1);
|
||||
color: var(--primary-color);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.workflow-copy {
|
||||
margin: 0 0 0.85rem 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.workflow-copy-compact {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.workflow-section-body {
|
||||
margin-top: 0.85rem;
|
||||
}
|
||||
|
||||
.workflow-block + .workflow-block {
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
|
||||
.choice-row {
|
||||
display: flex;
|
||||
gap: 0.55rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.choice-row-dense {
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.choice-chip {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
min-width: 90px;
|
||||
gap: 0.15rem;
|
||||
padding: 0.55rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
background: var(--surface-color, #ffffff);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, transform 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.stroke-selection {
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
vertical-align: middle;
|
||||
.choice-chip:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: var(--primary-soft);
|
||||
}
|
||||
|
||||
.stroke-selection label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
.choice-chip strong {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.choice-chip span {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stroke-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: nowrap;
|
||||
.choice-chip-active {
|
||||
background: linear-gradient(135deg, rgba(29, 90, 61, 0.1), rgba(29, 90, 61, 0.16));
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.spin-selection {
|
||||
margin-top: 0;
|
||||
.choice-chip-active span {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.spin-selection label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.spin-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.additional-strokes {
|
||||
margin-top: 1rem;
|
||||
.choice-chip-compact {
|
||||
min-width: 84px;
|
||||
}
|
||||
|
||||
.next-stroke-selection {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.next-stroke-type {
|
||||
gap: 0.5rem;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.next-stroke-buttons {
|
||||
gap: 0.5rem;
|
||||
flex-wrap: nowrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 0.9rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.exercise-info {
|
||||
@@ -1866,6 +2074,7 @@ input[type="range"] {
|
||||
padding: 0.75rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.exercise-info p {
|
||||
@@ -1881,6 +2090,12 @@ input[type="range"] {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.target-grid-large {
|
||||
grid-template-columns: repeat(3, 44px);
|
||||
grid-auto-rows: 44px;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.grid-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1900,12 +2115,70 @@ input[type="range"] {
|
||||
border: 1px solid rgba(29, 90, 61, 0.14);
|
||||
}
|
||||
|
||||
.target-grid-large .grid-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.grid-btn.is-active {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: var(--text-on-primary);
|
||||
font-weight: 700; /* fett */
|
||||
}
|
||||
|
||||
.stroke-sequence-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.55rem;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.stroke-sequence-item {
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr auto;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
padding: 0.6rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
background: var(--background-soft);
|
||||
}
|
||||
|
||||
.stroke-sequence-index {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 999px;
|
||||
background: rgba(29, 90, 61, 0.1);
|
||||
color: var(--primary-color);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stroke-sequence-copy {
|
||||
color: var(--text-color);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.stroke-sequence-remove {
|
||||
border: 1px solid rgba(200, 74, 56, 0.22);
|
||||
background: rgba(200, 74, 56, 0.08);
|
||||
color: #8b3327;
|
||||
border-radius: 8px;
|
||||
padding: 0.45rem 0.65rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stroke-sequence-empty {
|
||||
margin-bottom: 0.9rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 10px;
|
||||
background: rgba(29, 90, 61, 0.06);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.btn-stroke.btn-primary {
|
||||
color: COURT_TOOL_PALETTE.accentText !important;
|
||||
font-weight: 700;
|
||||
@@ -1917,6 +2190,23 @@ input[type="range"] {
|
||||
}
|
||||
|
||||
.add-stroke-button {
|
||||
align-self: flex-end;
|
||||
align-self: start;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.tool-layout,
|
||||
.workflow-grid,
|
||||
.next-stroke-selection {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.choice-chip {
|
||||
min-width: calc(50% - 0.4rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user