Verbessert die Logik zur Erstellung von Aktivitäten im DiaryDateActivityService, um PredefinedActivities robuster zu finden. Fügt Unterstützung für die Suche nach Aktivitäten per ID, Name oder Code hinzu. Aktualisiert die Benutzeroberfläche in DiaryView.vue zur Anzeige von Zeichnungsdaten und integriert ein neues Rendering-Modal für Zeichnungen. Optimiert die Bildanzeige in CourtDrawingTool.vue und implementiert eine verbesserte Fehlerbehandlung beim Laden von Bildern.
This commit is contained in:
@@ -13,10 +13,34 @@ class DiaryDateActivityService {
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log('[DiaryDateActivityService::createActivity] - add: ', data);
|
||||
const { activity, ...restData } = data;
|
||||
let predefinedActivity = await PredefinedActivity.findOne({ where: { name: data.activity } });
|
||||
// Versuche, die PredefinedActivity robust zu finden:
|
||||
// 1) per übergebener ID
|
||||
// 2) per Name ODER Code (das Feld "activity" kann Kürzel oder Name sein)
|
||||
// 3) erst dann neu anlegen
|
||||
let predefinedActivity = null;
|
||||
|
||||
if (data.predefinedActivityId) {
|
||||
predefinedActivity = await PredefinedActivity.findByPk(data.predefinedActivityId);
|
||||
}
|
||||
|
||||
if (!predefinedActivity) {
|
||||
const normalized = (data.activity || '').trim();
|
||||
if (normalized) {
|
||||
predefinedActivity = await PredefinedActivity.findOne({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ name: normalized },
|
||||
{ code: normalized }
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!predefinedActivity) {
|
||||
predefinedActivity = await PredefinedActivity.create({
|
||||
name: data.activity,
|
||||
name: data.name || data.activity || '',
|
||||
code: data.code || (data.activity || ''),
|
||||
description: data.description || '',
|
||||
duration: data.duration && data.duration !== '' ? parseInt(data.duration) : null
|
||||
});
|
||||
@@ -140,9 +164,7 @@ class DiaryDateActivityService {
|
||||
}
|
||||
|
||||
async getActivities(userToken, clubId, diaryDateId) {
|
||||
console.log('[DiaryDateActivityService::getActivities] - check user access - SERVER RESTARTED');
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log(`[DiaryDateActivityService::getActivities] - fetch activities for diaryDateId: ${diaryDateId}`);
|
||||
const activities = await DiaryDateActivity.findAll({
|
||||
where: { diaryDateId },
|
||||
order: [['orderId', 'ASC']],
|
||||
@@ -173,32 +195,25 @@ class DiaryDateActivityService {
|
||||
}
|
||||
]
|
||||
});
|
||||
console.log(`[DiaryDateActivityService::getActivities] - found ${activities.length} activities`);
|
||||
|
||||
// Füge imageUrl zu jeder PredefinedActivity hinzu
|
||||
console.log('[DiaryDateActivityService::getActivities] - Adding imageUrl to activities');
|
||||
const activitiesWithImages = await Promise.all(activities.map(async activity => {
|
||||
// Konvertiere zu JSON und zurück, um alle Eigenschaften zu serialisieren
|
||||
const activityData = activity.toJSON();
|
||||
|
||||
if (activityData.predefinedActivity) {
|
||||
console.log(JSON.parse(JSON.stringify(activityData)));
|
||||
// Hole die erste verfügbare Image-ID direkt aus der Datenbank
|
||||
const allImages = await PredefinedActivityImage.findAll({
|
||||
where: { predefinedActivityId: activityData.predefinedActivity.id },
|
||||
order: [['createdAt', 'ASC']]
|
||||
});
|
||||
|
||||
console.log(`Activity ${activityData.predefinedActivity.id}: allImages =`, allImages.map(img => ({ id: img.id, activityId: img.predefinedActivityId, hasDrawingData: !!img.drawingData })));
|
||||
|
||||
const firstImage = allImages.length > 0 ? allImages[0] : null;
|
||||
console.log(`Activity ${activityData.predefinedActivity.id}: firstImage =`, firstImage?.id);
|
||||
|
||||
// Füge Zeichnungsdaten hinzu, falls vorhanden
|
||||
if (firstImage && firstImage.drawingData) {
|
||||
try {
|
||||
activityData.predefinedActivity.drawingData = JSON.parse(firstImage.drawingData);
|
||||
console.log(`Activity ${activityData.predefinedActivity.id}: drawingData loaded:`, activityData.predefinedActivity.drawingData);
|
||||
} catch (error) {
|
||||
console.error(`Activity ${activityData.predefinedActivity.id}: Error parsing drawingData:`, error);
|
||||
}
|
||||
|
||||
420
frontend/src/components/CourtDrawingRender.vue
Normal file
420
frontend/src/components/CourtDrawingRender.vue
Normal file
@@ -0,0 +1,420 @@
|
||||
<template>
|
||||
<div class="render-container">
|
||||
<canvas
|
||||
ref="canvas"
|
||||
:width="config.canvas.width"
|
||||
:height="config.canvas.height"
|
||||
style="margin:0 auto;"
|
||||
></canvas>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CourtDrawingRender',
|
||||
props: {
|
||||
drawingData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 600
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 400
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
canvas: null,
|
||||
ctx: null,
|
||||
config: {
|
||||
// Mehr Platz links für die drei Startkreise (Rand außerhalb des Tisches)
|
||||
canvas: { width: 640, height: 400 },
|
||||
table: {
|
||||
// Tabelle etwas schmaler und weiter rechts platzieren
|
||||
width: 500,
|
||||
height: 300,
|
||||
color: '#2d5a2d',
|
||||
borderWidth: 2,
|
||||
borderColor: '#000000',
|
||||
outerFrameWidth: 5,
|
||||
outerFrameColor: '#ffffff',
|
||||
outerFrameBorderWidth: 0.5,
|
||||
outerFrameBorderColor: '#000000'
|
||||
},
|
||||
horizontalLine: { color: '#cfd8dc', width: 2, gap: 10, edgeMargin: 10 },
|
||||
net: { color: '#ffffff', width: 4, overhang: 14, borderColor: '#000000', borderWidth: 0.5 },
|
||||
serviceLine: { color: 'rgba(255,255,255,0.0)', width: 0 },
|
||||
startCircles: {
|
||||
radius: 10,
|
||||
// Abstand vom linken Canvas-Rand (größer = weiter links, da wir von tableX abziehen)
|
||||
x: 20,
|
||||
topYOffset: 35,
|
||||
bottomYOffset: 35,
|
||||
selectedColor: '#ff4444',
|
||||
unselectedColor: '#9e9e9e',
|
||||
selectedBorderColor: '#ffffff',
|
||||
unselectedBorderColor: '#555555',
|
||||
selectedBorderWidth: 2,
|
||||
unselectedBorderWidth: 1
|
||||
},
|
||||
arrows: {
|
||||
primaryColor: '#d32f2f', // rechts -> target (rot)
|
||||
secondaryColor: '#1565c0', // zurück (blau)
|
||||
width: 6,
|
||||
headLength: 24
|
||||
},
|
||||
targetCircles: {
|
||||
radius: 13,
|
||||
topYOffset: 45,
|
||||
bottomYOffset: 45,
|
||||
transparency: 0.25
|
||||
},
|
||||
hitMarker: {
|
||||
radius: 10.5,
|
||||
fill: '#ffffff',
|
||||
stroke: '#000000',
|
||||
lineWidth: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
drawingData: {
|
||||
handler() {
|
||||
this.redraw();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// apply custom dimensions if provided
|
||||
this.config.canvas.width = this.width || 600;
|
||||
this.config.canvas.height = this.height || 400;
|
||||
this.$nextTick(() => {
|
||||
console.log('CourtDrawingRender: mounted with drawingData =', this.drawingData);
|
||||
this.init();
|
||||
this.redraw();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.canvas = this.$refs.canvas;
|
||||
if (this.canvas) {
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
console.log('CourtDrawingRender: canvas/context initialized');
|
||||
}
|
||||
},
|
||||
redraw() {
|
||||
console.log('CourtDrawingRender: redraw called with data =', this.drawingData);
|
||||
if (!this.ctx) return;
|
||||
const { width, height } = this.config.canvas;
|
||||
// clear and background
|
||||
this.ctx.clearRect(0, 0, width, height);
|
||||
this.ctx.fillStyle = '#f0f0f0';
|
||||
this.ctx.fillRect(0, 0, width, height);
|
||||
this.drawTable();
|
||||
this.drawStartCircles();
|
||||
this.drawArrowsAndLabels();
|
||||
},
|
||||
drawTable() {
|
||||
const ctx = this.ctx;
|
||||
const { table, horizontalLine, net, outerFrameWidth } = {
|
||||
...this.config,
|
||||
outerFrameWidth: this.config.table.outerFrameWidth
|
||||
};
|
||||
const tableWidth = this.config.table.width;
|
||||
const tableHeight = this.config.table.height;
|
||||
const tableX = (this.config.canvas.width - tableWidth) / 2;
|
||||
const tableY = (this.config.canvas.height - tableHeight) / 2;
|
||||
|
||||
// table fill
|
||||
ctx.fillStyle = table.color;
|
||||
ctx.fillRect(tableX, tableY, tableWidth, tableHeight);
|
||||
|
||||
// table stroke
|
||||
ctx.strokeStyle = table.borderColor;
|
||||
ctx.lineWidth = table.borderWidth;
|
||||
ctx.strokeRect(tableX, tableY, tableWidth, tableHeight);
|
||||
|
||||
// outer white frame
|
||||
ctx.strokeStyle = table.outerFrameColor;
|
||||
ctx.lineWidth = table.outerFrameWidth;
|
||||
ctx.strokeRect(
|
||||
tableX - table.outerFrameWidth,
|
||||
tableY - table.outerFrameWidth,
|
||||
tableWidth + table.outerFrameWidth * 2,
|
||||
tableHeight + table.outerFrameWidth * 2
|
||||
);
|
||||
|
||||
// black thin frame around white
|
||||
ctx.strokeStyle = table.outerFrameBorderColor;
|
||||
ctx.lineWidth = table.outerFrameBorderWidth;
|
||||
ctx.strokeRect(
|
||||
tableX - table.outerFrameWidth - table.outerFrameBorderWidth,
|
||||
tableY - table.outerFrameWidth - table.outerFrameBorderWidth,
|
||||
tableWidth + (table.outerFrameWidth + table.outerFrameBorderWidth) * 2,
|
||||
tableHeight + (table.outerFrameWidth + table.outerFrameBorderWidth) * 2
|
||||
);
|
||||
|
||||
// horizontal split line (two segments)
|
||||
ctx.strokeStyle = horizontalLine.color;
|
||||
ctx.lineWidth = horizontalLine.width;
|
||||
const centerY = tableY + tableHeight / 2;
|
||||
const centerX = tableX + tableWidth / 2;
|
||||
const gap = horizontalLine.gap;
|
||||
const edge = horizontalLine.edgeMargin;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(tableX + edge, centerY);
|
||||
ctx.lineTo(centerX - gap, centerY);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX + gap, centerY);
|
||||
ctx.lineTo(tableX + tableWidth - edge, centerY);
|
||||
ctx.stroke();
|
||||
|
||||
// net vertical
|
||||
ctx.strokeStyle = net.color;
|
||||
ctx.lineWidth = net.width;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX, tableY - net.overhang);
|
||||
ctx.lineTo(centerX, tableY + tableHeight + net.overhang);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.strokeStyle = net.borderColor;
|
||||
ctx.lineWidth = net.borderWidth;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX, tableY - net.overhang);
|
||||
ctx.lineTo(centerX, tableY + tableHeight + net.overhang);
|
||||
ctx.stroke();
|
||||
},
|
||||
drawStartCircles() {
|
||||
const ctx = this.ctx;
|
||||
const cfg = this.config.startCircles;
|
||||
const tableWidth = this.config.table.width;
|
||||
const tableHeight = this.config.table.height;
|
||||
const tableX = (this.config.canvas.width - tableWidth) / 2;
|
||||
const tableY = (this.config.canvas.height - tableHeight) / 2;
|
||||
|
||||
// Startkreis links VOR dem Tisch positionieren – nur den Kreis zeichnen,
|
||||
// von dem auch der rote Pfeil startet
|
||||
const circleX = tableX - cfg.x; // Links vom Tisch
|
||||
const topY = tableY + cfg.topYOffset;
|
||||
const midY = tableY + tableHeight / 2;
|
||||
const botY = tableY + tableHeight - cfg.bottomYOffset;
|
||||
|
||||
// Mapping und Fallback: bei "AS" auf 'middle'
|
||||
const map = { AS1: 'top', AS2: 'middle', AS3: 'bottom', AS: 'middle' };
|
||||
const selKey = this.drawingData?.selectedStartPosition || 'AS';
|
||||
const selectedPos = map[selKey] || 'middle';
|
||||
const y = selectedPos === 'top' ? topY : selectedPos === 'bottom' ? botY : midY;
|
||||
|
||||
ctx.fillStyle = cfg.selectedColor;
|
||||
ctx.beginPath();
|
||||
ctx.arc(circleX, y, cfg.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = cfg.selectedBorderColor;
|
||||
ctx.lineWidth = cfg.selectedBorderWidth;
|
||||
ctx.stroke();
|
||||
},
|
||||
computeRightTargetPosition(number) {
|
||||
// replicate the same geometry as in the drawing tool
|
||||
const tableWidth = this.config.table.width;
|
||||
const tableHeight = this.config.table.height;
|
||||
const tableX = (this.config.canvas.width - tableWidth) / 2;
|
||||
const tableY = (this.config.canvas.height - tableHeight) / 2;
|
||||
const centerX = tableX + tableWidth / 2;
|
||||
const cfg = this.config.targetCircles;
|
||||
|
||||
const x1 = tableX + tableWidth - 30; // rightmost column (1,2,3)
|
||||
const x3 = centerX + (x1 - centerX) * 0.35; // near net (7,8,9)
|
||||
const x2 = (x1 + x3) / 2; // middle column (4,5,6)
|
||||
|
||||
const positions = {
|
||||
1: { x: x1, y: tableY + cfg.topYOffset },
|
||||
2: { x: x1, y: tableY + tableHeight / 2 },
|
||||
3: { x: x1, y: tableY + tableHeight - cfg.bottomYOffset },
|
||||
4: { x: x2, y: tableY + cfg.topYOffset },
|
||||
5: { x: x2, y: tableY + tableHeight / 2 },
|
||||
6: { x: x2, y: tableY + tableHeight - cfg.bottomYOffset },
|
||||
7: { x: x3, y: tableY + cfg.topYOffset },
|
||||
8: { x: x3, y: tableY + tableHeight / 2 },
|
||||
9: { x: x3, y: tableY + tableHeight - cfg.bottomYOffset }
|
||||
};
|
||||
return positions[number] || null;
|
||||
},
|
||||
getStartPoint() {
|
||||
const cfg = this.config.startCircles;
|
||||
const tableWidth = this.config.table.width;
|
||||
const tableHeight = this.config.table.height;
|
||||
const tableX = (this.config.canvas.width - tableWidth) / 2;
|
||||
const tableY = (this.config.canvas.height - tableHeight) / 2;
|
||||
const map = {
|
||||
AS1: 'top', AS2: 'middle', AS3: 'bottom', AS: 'middle'
|
||||
};
|
||||
const selected = map[this.drawingData?.selectedStartPosition] || 'middle';
|
||||
const circleX = tableX - cfg.x; // Links vom Tisch
|
||||
const topY = tableY + cfg.topYOffset;
|
||||
const midY = tableY + tableHeight / 2;
|
||||
const botY = tableY + tableHeight - cfg.bottomYOffset;
|
||||
const y = selected === 'top' ? topY : selected === 'bottom' ? botY : midY;
|
||||
// arrow should start slightly to the right of the start circle
|
||||
return { x: circleX + cfg.radius + 6, y };
|
||||
},
|
||||
drawArrow(ctx, from, to, color, label) {
|
||||
const { width, headLength } = this.config.arrows;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.fillStyle = color;
|
||||
ctx.lineWidth = width;
|
||||
// line
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(from.x, from.y);
|
||||
ctx.lineTo(to.x, to.y);
|
||||
ctx.stroke();
|
||||
// arrow head
|
||||
const angle = Math.atan2(to.y - from.y, to.x - from.x);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(to.x, to.y);
|
||||
ctx.lineTo(to.x - headLength * Math.cos(angle - Math.PI / 6), to.y - headLength * Math.sin(angle - Math.PI / 6));
|
||||
ctx.lineTo(to.x - headLength * Math.cos(angle + Math.PI / 6), to.y - headLength * Math.sin(angle + Math.PI / 6));
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
},
|
||||
drawLabelBelow(ctx, text, anchor) {
|
||||
if (!text) return;
|
||||
ctx.font = 'bold 13px Helvetica';
|
||||
const padding = 6;
|
||||
const yOffset = 23; // 5px tiefer
|
||||
const metrics = ctx.measureText(text);
|
||||
const textWidth = metrics.width;
|
||||
let x = anchor.x - textWidth / 2; // zentriert unter dem Punkt
|
||||
let y = anchor.y + yOffset;
|
||||
// clamp innerhalb Canvas
|
||||
x = Math.max(4, Math.min(this.config.canvas.width - textWidth - 4, x));
|
||||
y = Math.max(14, Math.min(this.config.canvas.height - 4, y));
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.strokeStyle = '#000000';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeText(text, x, y);
|
||||
ctx.fillText(text, x, y);
|
||||
},
|
||||
drawArrowsAndLabels() {
|
||||
if (!this.drawingData) return;
|
||||
const ctx = this.ctx;
|
||||
const from = this.getStartPoint();
|
||||
|
||||
// First arrow: to right target
|
||||
const tp = Number(this.drawingData.targetPosition);
|
||||
if (tp) {
|
||||
const to = this.computeRightTargetPosition(tp);
|
||||
// Zielmarkierung (unter dem Pfeilkopf)
|
||||
this.drawHitMarker(ctx, to);
|
||||
const strokeSide = this.drawingData.strokeType || '';
|
||||
const spinAbbrev = this.abbrevSpin(this.drawingData.spinType);
|
||||
// Text gehört an die Quelle (ohne "target")
|
||||
const sourceLabel = `${strokeSide} ${spinAbbrev}`.trim();
|
||||
this.drawArrow(ctx, from, to, this.config.arrows.primaryColor);
|
||||
// Unter dem Startkreis beschriften
|
||||
const startCenter = this.getStartCircleCenter();
|
||||
this.drawLabelBelow(ctx, sourceLabel, startCenter);
|
||||
}
|
||||
|
||||
// Second arrow (optional): from right source to left target
|
||||
const leftTarget = this.drawingData.nextStrokeTargetPosition ? Number(this.drawingData.nextStrokeTargetPosition) : null;
|
||||
if (tp && leftTarget) {
|
||||
// source near previous right target
|
||||
const sourceRight = this.computeRightTargetPosition(tp);
|
||||
// left target mapping: mirror scheme to left half
|
||||
const toLeft = this.computeLeftTargetPosition(leftTarget);
|
||||
// Zielmarkierung links
|
||||
this.drawHitMarker(ctx, toLeft);
|
||||
const side = this.drawingData.nextStrokeSide || '';
|
||||
const type = this.drawingData.nextStrokeType || '';
|
||||
// Text gehört ans Ziel (ohne "extra target")
|
||||
const targetLabel = `${side} ${type}`.trim();
|
||||
this.drawArrow(ctx, sourceRight, toLeft, this.config.arrows.secondaryColor);
|
||||
// Unter dem rechten Ziel (target der ersten Linie) beschriften
|
||||
this.drawLabelBelow(ctx, targetLabel, sourceRight);
|
||||
}
|
||||
},
|
||||
getStartCircleCenter() {
|
||||
const cfg = this.config.startCircles;
|
||||
const tableWidth = this.config.table.width;
|
||||
const tableHeight = this.config.table.height;
|
||||
const tableX = (this.config.canvas.width - tableWidth) / 2;
|
||||
const tableY = (this.config.canvas.height - tableHeight) / 2;
|
||||
const circleX = tableX - cfg.x;
|
||||
const map = { AS1: 'top', AS2: 'middle', AS3: 'bottom', AS: 'middle' };
|
||||
const selKey = this.drawingData?.selectedStartPosition || 'AS';
|
||||
const selectedPos = map[selKey] || 'middle';
|
||||
const topY = tableY + cfg.topYOffset;
|
||||
const midY = tableY + tableHeight / 2;
|
||||
const botY = tableY + tableHeight - cfg.bottomYOffset;
|
||||
const y = selectedPos === 'top' ? topY : selectedPos === 'bottom' ? botY : midY;
|
||||
return { x: circleX, y };
|
||||
},
|
||||
drawHitMarker(ctx, pos) {
|
||||
if (!pos) return;
|
||||
const mk = this.config.hitMarker;
|
||||
ctx.beginPath();
|
||||
ctx.arc(pos.x, pos.y, mk.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = mk.fill;
|
||||
ctx.fill();
|
||||
ctx.lineWidth = mk.lineWidth;
|
||||
ctx.strokeStyle = mk.stroke;
|
||||
ctx.stroke();
|
||||
},
|
||||
computeLeftTargetPosition(number) {
|
||||
// mirror target grid to left side
|
||||
const tableWidth = this.config.table.width;
|
||||
const tableHeight = this.config.table.height;
|
||||
const tableX = (this.config.canvas.width - tableWidth) / 2;
|
||||
const tableY = (this.config.canvas.height - tableHeight) / 2;
|
||||
const centerX = tableX + tableWidth / 2;
|
||||
const cfg = this.config.targetCircles;
|
||||
|
||||
const x1 = tableX + 30; // leftmost column
|
||||
const x3 = centerX - (centerX - x1) * 0.35; // near net
|
||||
const x2 = (x1 + x3) / 2;
|
||||
|
||||
const positions = {
|
||||
1: { x: x1, y: tableY + cfg.topYOffset },
|
||||
2: { x: x1, y: tableY + tableHeight / 2 },
|
||||
3: { x: x1, y: tableY + tableHeight - cfg.bottomYOffset },
|
||||
4: { x: x2, y: tableY + cfg.topYOffset },
|
||||
5: { x: x2, y: tableY + tableHeight / 2 },
|
||||
6: { x: x2, y: tableY + tableHeight - cfg.bottomYOffset },
|
||||
7: { x: x3, y: tableY + cfg.topYOffset },
|
||||
8: { x: x3, y: tableY + tableHeight / 2 },
|
||||
9: { x: x3, y: tableY + tableHeight - cfg.bottomYOffset }
|
||||
};
|
||||
return positions[number] || null;
|
||||
},
|
||||
abbrevSpin(spin) {
|
||||
if (!spin) return '';
|
||||
const map = {
|
||||
Unterschnitt: 'US',
|
||||
Überschnitt: 'OS',
|
||||
Seitschnitt: 'SR',
|
||||
Seitunterschnitt: 'SU',
|
||||
Gegenläufer: 'GL'
|
||||
};
|
||||
return map[spin] || spin;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.render-container {
|
||||
width: 100%;
|
||||
}
|
||||
canvas { display: block; max-width: 100%; height: auto; }
|
||||
</style>
|
||||
|
||||
|
||||
@@ -588,8 +588,12 @@ export default {
|
||||
this.drawArrowToTarget(ctx, tableX, tableY, tableWidth, tableHeight);
|
||||
}
|
||||
|
||||
// Kreise für zusätzliche Schläge auf der linken Seite anzeigen (nur nach Schlag-Auswahl für rechte Seite)
|
||||
if (this.nextStrokeType && this.nextStrokeSide && this.targetPosition) {
|
||||
// Kreise für zusätzliche Schläge auf der linken Seite anzeigen
|
||||
// Sollen bereits erscheinen, sobald rechts alles gewählt ist (Aufschlag-Seite):
|
||||
// also wenn strokeType, spinType und targetPosition gesetzt sind.
|
||||
// Zusätzlich weiterhin, wenn bereits nextStrokeType/Side gewählt wurden.
|
||||
if ((this.strokeType && this.spinType && this.targetPosition) ||
|
||||
(this.nextStrokeType && this.nextStrokeSide && this.targetPosition)) {
|
||||
this.drawLeftSideTargetCircles(ctx, tableX, tableY, tableWidth, tableHeight);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,10 +126,11 @@
|
||||
</span>
|
||||
<span v-else @click="startActivityEdit(item)" class="clickable activity-label"
|
||||
:title="item.predefinedActivity && item.predefinedActivity.name ? item.predefinedActivity.name : ''">
|
||||
<span v-if="item.predefinedActivity && item.predefinedActivity.imageLink"
|
||||
@click.stop="showActivityImage(item.predefinedActivity.imageLink)"
|
||||
class="image-icon"
|
||||
title="Bild anzeigen">🖼️</span>
|
||||
<!-- Icon öffnet Rendering (falls vorhanden) oder Bild im Modal -->
|
||||
<span v-if="item.predefinedActivity"
|
||||
@click.stop="openActivityVisual(item.predefinedActivity)"
|
||||
class="image-icon"
|
||||
title="Bild/Zeichnung anzeigen">🖼️</span>
|
||||
{{ (item.predefinedActivity && item.predefinedActivity.code && item.predefinedActivity.code.trim() !== '')
|
||||
? item.predefinedActivity.code
|
||||
: (item.predefinedActivity ? item.predefinedActivity.name : item.activity) }}
|
||||
@@ -161,10 +162,12 @@
|
||||
<td></td>
|
||||
<td>
|
||||
<span class="activity-label" :title="(groupItem.groupPredefinedActivity && groupItem.groupPredefinedActivity.name) ? groupItem.groupPredefinedActivity.name : ''">
|
||||
<span v-if="groupItem.groupPredefinedActivity && groupItem.groupPredefinedActivity.imageLink"
|
||||
@click.stop="showActivityImage(groupItem.groupPredefinedActivity.imageLink)"
|
||||
class="image-icon"
|
||||
title="Bild anzeigen">🖼️</span>
|
||||
<!-- Icon öffnet Rendering (falls vorhanden) oder Bild im Modal -->
|
||||
<span v-if="groupItem.groupPredefinedActivity"
|
||||
@click.stop="openActivityVisual(groupItem.groupPredefinedActivity)"
|
||||
class="image-icon"
|
||||
title="Bild/Zeichnung anzeigen">🖼️</span>
|
||||
|
||||
{{ (groupItem.groupPredefinedActivity && groupItem.groupPredefinedActivity.code && groupItem.groupPredefinedActivity.code.trim() !== '')
|
||||
? groupItem.groupPredefinedActivity.code
|
||||
: groupItem.groupPredefinedActivity.name }}
|
||||
@@ -319,8 +322,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showImage" class="memberImage">
|
||||
<img :src="imageUrl" @click="closeImage" />
|
||||
<div v-if="showImage || showRenderModal" class="memberImage" @click="closeImage">
|
||||
<template v-if="showImage">
|
||||
<img :src="imageUrl" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<CourtDrawingRender :drawing-data="renderModalData" :width="560" :height="360" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="showAccidentForm" class="accidentForm">
|
||||
<form @submit.prevent="submitAccident">
|
||||
@@ -397,10 +405,11 @@ import apiClient from '../apiClient.js';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import Sortable from 'sortablejs';
|
||||
import PDFGenerator from '../components/PDFGenerator.js';
|
||||
import CourtDrawingRender from '../components/CourtDrawingRender.vue';
|
||||
|
||||
export default {
|
||||
name: 'DiaryView',
|
||||
components: { Multiselect },
|
||||
components: { Multiselect, CourtDrawingRender },
|
||||
data() {
|
||||
return {
|
||||
date: null,
|
||||
@@ -436,6 +445,8 @@ export default {
|
||||
showDropdown: false,
|
||||
showImage: false,
|
||||
imageUrl: '',
|
||||
showRenderModal: false,
|
||||
renderModalData: null,
|
||||
groups: [],
|
||||
currentTimeBlockId: null,
|
||||
newGroupName: '',
|
||||
@@ -526,6 +537,36 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
drawingDataFor(pa) {
|
||||
// Zeichnungsdaten können bereits als Objekt vorliegen oder als JSON-String
|
||||
try {
|
||||
if (!pa) return null;
|
||||
if (pa.drawingData && typeof pa.drawingData === 'object') {
|
||||
console.debug('DiaryView: drawingData (object) gefunden für', pa.id);
|
||||
return pa.drawingData;
|
||||
}
|
||||
if (pa.drawingData && typeof pa.drawingData === 'string') {
|
||||
const parsed = JSON.parse(pa.drawingData);
|
||||
console.debug('DiaryView: drawingData (string→parsed) für', pa.id, parsed);
|
||||
return parsed;
|
||||
}
|
||||
// Fallback: falls über images[0].drawingData geliefert wurde
|
||||
if (pa.images && pa.images.length) {
|
||||
// Nimm das erste Bild, das drawingData enthält
|
||||
const withData = pa.images.find(img => !!img.drawingData);
|
||||
if (withData) {
|
||||
const parsedImg = typeof withData.drawingData === 'string'
|
||||
? JSON.parse(withData.drawingData)
|
||||
: withData.drawingData;
|
||||
console.debug('DiaryView: drawingData aus images für', pa.id, 'imageId=', withData.id, parsedImg);
|
||||
return parsedImg;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('DiaryView: drawingData parse error:', e);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
async init() {
|
||||
if (this.isAuthenticated && this.currentClub) {
|
||||
const response = await apiClient.get(`/diary/${this.currentClub}`);
|
||||
@@ -1119,17 +1160,43 @@ export default {
|
||||
},
|
||||
closeImage() {
|
||||
this.showImage = false;
|
||||
this.showRenderModal = false;
|
||||
this.renderModalData = null;
|
||||
this.imageUrl = '';
|
||||
},
|
||||
|
||||
showActivityImage(imageLink) {
|
||||
// Erstelle vollständige URL mit korrektem Port
|
||||
if (imageLink && imageLink.startsWith('/api/')) {
|
||||
this.imageUrl = `http://localhost:3000${imageLink}`;
|
||||
} else {
|
||||
this.imageUrl = imageLink;
|
||||
async showActivityImage(imageLink) {
|
||||
try {
|
||||
// Vorherige Object-URL freigeben
|
||||
if (this.imageUrl && this.imageUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(this.imageUrl);
|
||||
}
|
||||
|
||||
if (imageLink && imageLink.startsWith('/api/')) {
|
||||
// Über API mit Auth und als Blob laden → vermeidet ORB
|
||||
const path = imageLink.replace(/^\/api\//, '');
|
||||
const resp = await apiClient.get(`/${path}`, { responseType: 'blob' });
|
||||
this.imageUrl = URL.createObjectURL(resp.data);
|
||||
} else {
|
||||
this.imageUrl = imageLink;
|
||||
}
|
||||
this.showImage = true;
|
||||
} catch (e) {
|
||||
console.error('Bild laden fehlgeschlagen:', e);
|
||||
alert('Bild konnte nicht geladen werden.');
|
||||
}
|
||||
},
|
||||
async openActivityVisual(pa) {
|
||||
// Bevorzugt Rendering, ansonsten Bild
|
||||
const data = this.drawingDataFor(pa);
|
||||
if (data) {
|
||||
// Erzeuge temporär ein PNG im Client: Canvas offscreen rendern über Renderer-Komponente ist aufwändig.
|
||||
// Einfacher: öffne ein kleines Modal mit der Renderer-Komponente statt imageUrl.
|
||||
this.renderModalData = data;
|
||||
this.showRenderModal = true;
|
||||
} else if (pa.imageLink) {
|
||||
await this.showActivityImage(pa.imageLink);
|
||||
}
|
||||
this.showImage = true;
|
||||
},
|
||||
async loadMemberImage(member) {
|
||||
try {
|
||||
@@ -1915,6 +1982,12 @@ img {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.memberImage > div, .memberImage canvas {
|
||||
/* falls Komponenten-Inhalt (Renderer) da ist, mittig zeigen */
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.groups {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
Reference in New Issue
Block a user