Files
yourpart3/frontend/src/views/admin/MinigamesView.vue
Torsten Schulz (local) e168adeb51 feat(match3): Erweiterung der Match3-Admin-Funktionalitäten und -Modelle
- Implementierung neuer Endpunkte für die Verwaltung von Match3-Kampagnen, Levels, Objectives und Tile-Typen im Admin-Bereich.
- Anpassung der Admin-Services zur Unterstützung von Benutzerberechtigungen und Fehlerbehandlung.
- Einführung von neuen Modellen und Assoziationen für Match3-Levels und Tile-Typen in der Datenbank.
- Verbesserung der Internationalisierung für Match3-spezifische Texte in Deutsch und Englisch.
- Aktualisierung der Frontend-Routen und -Komponenten zur Verwaltung von Match3-Inhalten.
2025-08-23 06:00:29 +02:00

1518 lines
44 KiB
Vue

<template>
<div class="contenthidden">
<div class="contentscroll">
<div class="admin-header">
<h1>{{ $t('admin.match3.title') }}</h1>
<p>Verwalte Minigames, Level und Konfigurationen</p>
</div>
<SimpleTabs v-model="activeTab" :tabs="tabs" />
<!-- Match3 Levels Tab -->
<div v-if="activeTab === 'match3-levels'" class="match3-admin">
<div class="section-header">
<h2>{{ $t('admin.match3.title') }}</h2>
</div>
<div class="level-selection">
<div class="level-count">
<p>{{ $t('admin.match3.availableLevels', { count: levels.length }) }}</p>
</div>
<div class="level-dropdown">
<select v-model="selectedLevelId" @change="onLevelSelect" class="level-select">
<option value="new">{{ $t('admin.match3.newLevel') }}</option>
<option
v-for="level in levels"
:key="level.id"
:value="level.id"
class="level-option"
>
{{ $t('admin.match3.levelFormat', { number: level.order, name: level.name }) }}
</option>
</select>
</div>
</div>
<!-- Level Details -->
<div v-if="selectedLevelId !== 'new' && selectedLevel" class="level-details">
<div class="details-header">
<h3>{{ selectedLevel.name }}</h3>
</div>
<div class="details-content">
<div class="form-group">
<label for="levelName">{{ $t('admin.match3.levelName') }}:</label>
<input
id="levelName"
v-model="levelForm.name"
type="text"
required
:placeholder="$t('admin.match3.levelName')"
>
</div>
<div class="form-group">
<label for="levelDescription">{{ $t('admin.match3.levelDescription') }}:</label>
<textarea
id="levelDescription"
v-model="levelForm.description"
required
:placeholder="$t('admin.match3.levelDescription')"
rows="3"
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="boardWidth">{{ $t('admin.match3.boardWidth') }}:</label>
<input
id="boardWidth"
v-model.number="levelForm.boardWidth"
type="number"
min="3"
max="12"
required
@change="updateBoardMatrix"
>
</div>
<div class="form-group">
<label for="boardHeight">{{ $t('admin.match3.boardHeight') }}:</label>
<input
id="boardHeight"
v-model.number="levelForm.boardHeight"
type="number"
min="3"
max="12"
required
@change="updateBoardMatrix"
>
</div>
<div class="form-group">
<label for="moveLimit">{{ $t('admin.match3.moveLimit') }}:</label>
<input
id="moveLimit"
v-model.number="levelForm.moveLimit"
type="number"
min="5"
max="100"
required
>
</div>
<div class="form-group">
<label for="levelOrder">{{ $t('admin.match3.levelOrder') }}:</label>
<input
id="levelOrder"
v-model.number="levelForm.order"
type="number"
min="1"
required
>
</div>
</div>
<div class="form-group">
<label>{{ $t('admin.match3.boardLayout') }}:</label>
<div class="board-editor">
<div class="board-matrix" :style="boardMatrixStyle">
<div
v-for="(cell, index) in boardMatrix"
:key="index"
class="board-cell"
:class="{
'active': cell.active,
'inactive': !cell.active,
'random': cell.tileType === 'r',
'empty': cell.tileType === 'o',
'selected': selectedCellIndex === index
}"
@click="selectCell(index)"
>
<span v-if="cell.tileType === 'o'" class="cell-status"></span>
<span v-else-if="cell.tileType === 'r'" class="cell-status">🎲</span>
<span v-else class="cell-status">{{ getTileSymbol(cell.tileType) }}</span>
</div>
</div>
<!-- Minimale Tile-Auswahl -->
<div v-if="selectedCellIndex !== null" class="tile-selection-minimal">
<span class="selection-label">Position {{ selectedCellIndex }}:</span>
<div class="tile-options-minimal">
<span class="tile-option-mini" @click="setTileType(selectedCellIndex, 'o')" title="Leer"></span>
<span class="tile-option-mini" @click="setTileType(selectedCellIndex, 'r')" title="Zufällig">🎲</span>
<span
v-for="tileType in levelForm.tileTypes"
:key="tileType"
class="tile-option-mini"
@click="setTileType(selectedCellIndex, tileType)"
:title="tileType"
>{{ getTileSymbol(tileType) }}</span>
</div>
</div>
<div class="board-controls">
<button type="button" class="btn btn-secondary" @click="fillAllActive">
{{ $t('admin.match3.boardControls.fillAll') }}
</button>
<button type="button" class="btn btn-secondary" @click="clearAll">
{{ $t('admin.match3.boardControls.clearAll') }}
</button>
<button type="button" class="btn btn-secondary" @click="invertBoard">
{{ $t('admin.match3.boardControls.invert') }}
</button>
</div>
</div>
</div>
<div class="form-group">
<label>{{ $t('admin.match3.tileTypes') }}:</label>
<div class="tile-types-selection">
<label v-for="tileType in availableTileTypes" :key="tileType" class="tile-type-checkbox">
<input
type="checkbox"
:value="tileType"
v-model="levelForm.tileTypes"
>
<span class="tile-symbol">{{ getTileSymbol(tileType) }}</span>
<span class="tile-name">{{ tileType }}</span>
</label>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-danger" @click="deleteSelectedLevel">
{{ $t('admin.match3.delete') }}
</button>
<button type="button" class="btn btn-primary" @click="saveLevel">
{{ $t('admin.match3.update') }}
</button>
</div>
</div>
</div>
<!-- Level Objectives Section (immer verfügbar) -->
<div v-if="selectedLevelId !== 'new'" class="objectives-section-container">
<div class="form-header">
<h3>{{ $t('admin.match3.levelObjectives') }}</h3>
</div>
<div class="objectives-section">
<div class="objectives-header">
<h4>{{ $t('admin.match3.objectivesTitle') }}</h4>
<button type="button" class="btn btn-secondary btn-sm" @click="addObjective">
{{ $t('admin.match3.addObjective') }}
</button>
</div>
<div v-if="levelForm.objectives && levelForm.objectives.length > 0" class="objectives-list">
<div v-for="(objective, index) in levelForm.objectives" :key="index" class="objective-item">
<div class="objective-header">
<span class="objective-number">#{{ index + 1 }}</span>
<button type="button" class="btn btn-danger btn-sm" @click="removeObjective(index)">
{{ $t('admin.match3.removeObjective') }}
</button>
</div>
<div class="objective-form">
<div class="form-row">
<div class="form-group">
<label>{{ $t('admin.match3.objectiveType') }}:</label>
<select v-model="objective.type" class="form-control">
<option value="score">{{ $t('admin.match3.objectiveTypeScore') }}</option>
<option value="matches">{{ $t('admin.match3.objectiveTypeMatches') }}</option>
<option value="moves">{{ $t('admin.match3.objectiveTypeMoves') }}</option>
<option value="time">{{ $t('admin.match3.objectiveTypeTime') }}</option>
<option value="special">{{ $t('admin.match3.objectiveTypeSpecial') }}</option>
</select>
</div>
<div class="form-group">
<label>{{ $t('admin.match3.objectiveOperator') }}:</label>
<select v-model="objective.operator" class="form-control">
<option value=">=">{{ $t('admin.match3.operatorGreaterEqual') }}</option>
<option value="<=">{{ $t('admin.match3.operatorLessEqual') }}</option>
<option value="=">{{ $t('admin.match3.operatorEqual') }}</option>
<option value=">">{{ $t('admin.match3.operatorGreater') }}</option>
<option value="<">{{ $t('admin.match3.operatorLess') }}</option>
</select>
</div>
<div class="form-group">
<label>{{ $t('admin.match3.objectiveTarget') }}:</label>
<input
v-model.number="objective.target"
type="number"
min="1"
class="form-control"
:placeholder="$t('admin.match3.objectiveTargetPlaceholder')"
>
</div>
<div class="form-group">
<label>{{ $t('admin.match3.objectiveOrder') }}:</label>
<input
v-model.number="objective.order"
type="number"
min="1"
class="form-control"
:placeholder="$t('admin.match3.objectiveTargetPlaceholder')"
>
</div>
</div>
<div class="form-group">
<label>{{ $t('admin.match3.objectiveDescription') }}:</label>
<input
v-model="objective.description"
type="text"
class="form-control"
:placeholder="$t('admin.match3.objectiveDescriptionPlaceholder')"
>
</div>
<div class="form-group">
<label class="checkbox-label">
<input
type="checkbox"
v-model="objective.isRequired"
>
{{ $t('admin.match3.objectiveRequired') }}
</label>
</div>
</div>
</div>
</div>
<div v-else class="no-objectives">
<p>{{ $t('admin.match3.noObjectives') }}</p>
</div>
</div>
</div>
<!-- Level Form (nur für neue Level) -->
<div v-if="selectedLevelId === 'new'" class="level-form">
<div class="form-header">
<h3>{{ $t('admin.match3.newLevel') }}</h3>
</div>
<form @submit.prevent="saveLevel">
<div class="form-group">
<label for="levelName">{{ $t('admin.match3.levelName') }}:</label>
<input
id="levelName"
v-model="levelForm.name"
type="text"
required
:placeholder="$t('admin.match3.levelName')"
>
</div>
<div class="form-group">
<label for="levelDescription">{{ $t('admin.match3.levelDescription') }}:</label>
<textarea
id="levelDescription"
v-model="levelForm.description"
required
:placeholder="$t('admin.match3.levelDescription')"
rows="3"
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="boardWidth">{{ $t('admin.match3.boardWidth') }}:</label>
<input
id="boardWidth"
v-model.number="levelForm.boardWidth"
type="number"
min="3"
max="12"
required
@change="updateBoardMatrix"
>
</div>
<div class="form-group">
<label for="boardHeight">{{ $t('admin.match3.boardHeight') }}:</label>
<input
id="boardHeight"
v-model.number="levelForm.boardHeight"
type="number"
min="3"
max="12"
required
@change="updateBoardMatrix"
>
</div>
<div class="form-group">
<label for="moveLimit">{{ $t('admin.match3.moveLimit') }}:</label>
<input
id="moveLimit"
v-model.number="levelForm.moveLimit"
type="number"
min="5"
max="100"
required
>
</div>
<div class="form-group">
<label for="levelOrder">{{ $t('admin.match3.levelOrder') }}:</label>
<input
id="levelOrder"
v-model.number="levelForm.order"
type="number"
min="1"
required
>
</div>
</div>
<div class="form-group">
<label>{{ $t('admin.match3.boardLayout') }}:</label>
<div class="board-editor">
<div class="board-matrix" :style="boardMatrixStyle">
<div
v-for="(cell, index) in boardMatrix"
:key="index"
class="board-cell"
:class="{
'active': cell.active,
'inactive': !cell.active,
'random': cell.tileType === 'r',
'empty': cell.tileType === 'o',
'selected': selectedCellIndex === index
}"
@click="selectCell(index)"
>
<span v-if="cell.tileType === 'o'" class="cell-status"></span>
<span v-else-if="cell.tileType === 'r'" class="cell-status">🎲</span>
<span v-else class="cell-status">{{ getTileSymbol(cell.tileType) }}</span>
</div>
</div>
<!-- Minimale Tile-Auswahl -->
<div v-if="selectedCellIndex !== null" class="tile-selection-minimal">
<span class="selection-label">Position {{ selectedCellIndex }}:</span>
<div class="tile-options-minimal">
<span class="tile-option-mini" @click="setTileType(selectedCellIndex, 'o')" title="Leer"></span>
<span class="tile-option-mini" @click="setTileType(selectedCellIndex, 'r')" title="Zufällig">🎲</span>
<span
v-for="tileType in levelForm.tileTypes"
:key="tileType"
class="tile-option-mini"
@click="setTileType(selectedCellIndex, tileType)"
:title="tileType"
>{{ getTileSymbol(tileType) }}</span>
</div>
</div>
<div class="board-controls">
<button type="button" class="btn btn-secondary" @click="fillAllActive">
{{ $t('admin.match3.boardControls.fillAll') }}
</button>
<button type="button" class="btn btn-secondary" @click="clearAll">
{{ $t('admin.match3.boardControls.clearAll') }}
</button>
<button type="button" class="btn btn-secondary" @click="invertBoard">
{{ $t('admin.match3.boardControls.invert') }}
</button>
</div>
</div>
</div>
<div class="form-group">
<label>{{ $t('admin.match3.tileTypes') }}:</label>
<div class="tile-types-selection">
<label v-for="tileType in availableTileTypes" :key="tileType" class="tile-type-checkbox">
<input
type="checkbox"
:value="tileType"
v-model="levelForm.tileTypes"
>
<span class="tile-symbol">{{ getTileSymbol(tileType) }}</span>
<span class="tile-name">{{ tileType }}</span>
</label>
</div>
</div>
<!-- Level Objectives Section -->
<div class="form-group">
<label>{{ $t('admin.match3.levelObjectives') }}:</label>
<div class="objectives-section">
<div class="objectives-header">
<h4>{{ $t('admin.match3.objectivesTitle') }}</h4>
<button type="button" class="btn btn-secondary btn-sm" @click="addObjective">
{{ $t('admin.match3.addObjective') }}
</button>
</div>
<div v-if="levelForm.objectives && levelForm.objectives.length > 0" class="objectives-list">
<div v-for="(objective, index) in levelForm.objectives" :key="index" class="objective-item">
<div class="objective-header">
<span class="objective-number">#{{ index + 1 }}</span>
<button type="button" class="btn btn-danger btn-sm" @click="removeObjective(index)">
{{ $t('admin.match3.removeObjective') }}
</button>
</div>
<div class="objective-form">
<div class="form-row">
<div class="form-group">
<label>{{ $t('admin.match3.objectiveType') }}:</label>
<select v-model="objective.type" class="form-control">
<option value="score">{{ $t('admin.match3.objectiveTypeScore') }}</option>
<option value="matches">{{ $t('admin.match3.objectiveTypeMatches') }}</option>
<option value="moves">{{ $t('admin.match3.objectiveTypeMoves') }}</option>
<option value="time">{{ $t('admin.match3.objectiveTypeTime') }}</option>
<option value="special">{{ $t('admin.match3.objectiveTypeSpecial') }}</option>
</select>
</div>
<div class="form-group">
<label>{{ $t('admin.match3.objectiveOperator') }}:</label>
<select v-model="objective.operator" class="form-control">
<option value=">=">{{ $t('admin.match3.operatorGreaterEqual') }}</option>
<option value="<=">{{ $t('admin.match3.operatorLessEqual') }}</option>
<option value="=">{{ $t('admin.match3.operatorEqual') }}</option>
<option value=">">{{ $t('admin.match3.operatorGreater') }}</option>
<option value="<">{{ $t('admin.match3.operatorLess') }}</option>
</select>
</div>
<div class="form-group">
<label>{{ $t('admin.match3.objectiveTarget') }}:</label>
<input
v-model.number="objective.target"
type="number"
min="1"
class="form-control"
:placeholder="$t('admin.match3.objectiveTargetPlaceholder')"
>
</div>
<div class="form-group">
<label>{{ $t('admin.match3.objectiveOrder') }}:</label>
<input
v-model.number="objective.order"
type="number"
min="1"
class="form-control"
:placeholder="$t('admin.match3.objectiveOrderPlaceholder')"
>
</div>
</div>
<div class="form-group">
<label>{{ $t('admin.match3.objectiveDescription') }}:</label>
<input
v-model="objective.description"
type="text"
class="form-control"
:placeholder="$t('admin.match3.objectiveDescriptionPlaceholder')"
>
</div>
<div class="form-group">
<label class="checkbox-label">
<input
type="checkbox"
v-model="objective.isRequired"
>
{{ $t('admin.match3.objectiveRequired') }}
</label>
</div>
</div>
</div>
</div>
<div v-else class="no-objectives">
<p>{{ $t('admin.match3.noObjectives') }}</p>
</div>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" @click="cancelEdit">
{{ $t('admin.match3.cancel') }}
</button>
<button type="submit" class="btn btn-primary">
{{ $t('admin.match3.create') }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
import SimpleTabs from '../../components/SimpleTabs.vue';
import apiClient from '../../utils/axios.js';
export default {
name: 'AdminMinigamesView',
components: {
SimpleTabs
},
data() {
return {
activeTab: 'match3-levels',
tabs: [
{
value: 'match3-levels',
label: this.$t('admin.match3.title')
}
],
levels: [],
selectedLevelId: 'new',
editingLevel: null,
levelForm: {
name: '',
description: '',
boardWidth: 5,
boardHeight: 5,
moveLimit: 15,
order: 1,
tileTypes: ['gem', 'star', 'heart'],
objectives: []
},
boardMatrix: [],
availableTileTypes: ['gem', 'star', 'heart', 'diamond', 'circle', 'square', 'crown', 'rainbow'],
selectedCellIndex: null
};
},
computed: {
boardMatrixStyle() {
return {
gridTemplateColumns: `repeat(${this.levelForm.boardWidth}, 1fr)`,
gridTemplateRows: `repeat(${this.levelForm.boardHeight}, 1fr)`
};
},
selectedLevel() {
if (this.selectedLevelId === 'new') return null;
return this.levels.find(l => l.id === this.selectedLevelId);
}
},
watch: {
selectedLevelId: {
handler(newValue, oldValue) {
if (newValue !== oldValue) {
this.$nextTick(() => {
this.onLevelSelect();
});
}
},
immediate: true
}
},
mounted() {
// Explizite Initialisierung der Daten
this.levels = [];
this.editingLevel = null;
this.boardMatrix = [];
// Warte kurz, bis der Store geladen ist
this.$nextTick(() => {
this.loadLevels();
this.updateBoardMatrix();
});
},
methods: {
async loadLevels() {
try {
// Prüfe ob der Store geladen ist
const user = this.$store.getters.user;
if (!user || !user.authCode) {
setTimeout(() => this.loadLevels(), 100);
return;
}
const response = await apiClient.get('/api/admin/minigames/match3/levels');
this.levels = response.data;
} catch (error) {
console.error('Fehler beim Laden der Level:', error);
}
},
async onLevelSelect() {
if (this.selectedLevelId === 'new') {
this.createLevel();
} else {
const level = this.selectedLevel;
if (level) {
await this.editLevel(level);
} else {
console.warn('Kein Level gefunden für ID:', this.selectedLevelId);
}
}
},
createLevel() {
this.selectedLevelId = 'new';
this.editingLevel = null;
this.selectedCellIndex = null;
this.levelForm = {
name: '',
description: '',
boardWidth: 5,
boardHeight: 5,
moveLimit: 15,
order: 1,
tileTypes: ['gem', 'star', 'heart'],
objectives: []
};
this.updateBoardMatrix();
},
async editLevel(level) {
this.editingLevel = level;
this.selectedCellIndex = null;
this.levelForm = {
name: level.name,
description: level.description,
boardWidth: level.boardWidth,
boardHeight: level.boardHeight,
moveLimit: level.moveLimit,
order: level.order,
tileTypes: level.tileTypes || ['gem', 'star', 'heart'],
objectives: level.objectives || []
};
// Lade Objectives für dieses Level
try {
const objectives = await this.loadObjectivesForLevel(level.id);
this.levelForm.objectives = objectives;
} catch (error) {
console.error('Fehler beim Laden der Objectives:', error);
this.levelForm.objectives = [];
}
if (level.boardLayout) {
this.createBoardMatrixFromLayout(level.boardLayout);
} else {
this.updateBoardMatrix();
}
},
async deleteSelectedLevel() {
if (this.selectedLevelId && this.selectedLevelId !== 'new') {
await this.deleteLevel(this.selectedLevelId);
this.selectedLevelId = 'new';
}
},
getTileSymbol(type) {
const symbols = {
gem: '💎',
star: '⭐',
heart: '❤️',
diamond: '🔷',
circle: '⭕',
square: '🟦',
crown: '👑',
rainbow: '🌈'
};
return symbols[type] || '❓';
},
// Neue Methoden für das Tile-Auswahl-System
selectCell(index) {
this.selectedCellIndex = this.selectedCellIndex === index ? null : index;
},
setTileType(index, tileType) {
console.log('setTileType called with:', index, tileType);
if (tileType === 'o') {
// Leer
this.boardMatrix[index] = { active: false, tileType: 'o', index: index };
} else if (tileType === 'r') {
// Zufällig
this.boardMatrix[index] = { active: true, tileType: 'r', index: index };
console.log('Set random tile at index:', index, this.boardMatrix[index]);
} else {
// Spezifischer Tile-Typ
this.boardMatrix[index] = { active: true, tileType: tileType, index: index };
}
this.selectedCellIndex = null; // Auswahl aufheben
console.log('Board matrix after update:', this.boardMatrix);
},
// Mapping für Tile-Typen zu Zeichen
getTileTypeChar(tileType) {
const charMap = {
'gem': 'g',
'star': 's',
'heart': 'h',
'diamond': 'd',
'circle': 'c',
'square': 'q',
'crown': 'w',
'rainbow': 'b'
};
return charMap[tileType] || 'x';
},
// Konvertiert das Board für das Speichern
convertBoardForSave() {
return this.boardMatrix.map(cell => {
if (cell.tileType === 'o') return 'o'; // Leer
if (cell.tileType === 'r') return 'x'; // Zufällig wird als "x" gespeichert
return this.getTileTypeChar(cell.tileType); // Spezifischer Tile-Typ
});
},
cancelEdit() {
this.editingLevel = null;
this.selectedLevelId = 'new';
this.selectedCellIndex = null;
this.levelForm = {
name: '',
description: '',
boardWidth: 5,
boardHeight: 5,
moveLimit: 15,
order: 1,
tileTypes: ['gem', 'star', 'heart'],
objectives: []
};
this.updateBoardMatrix();
console.log('Bearbeitung abgebrochen, Objectives zurückgesetzt:', this.levelForm.objectives);
},
updateBoardMatrix() {
const totalCells = this.levelForm.boardWidth * this.levelForm.boardHeight;
this.boardMatrix = [];
for (let i = 0; i < totalCells; i++) {
this.boardMatrix.push({ active: false, tileType: 'o', index: i });
}
},
createBoardMatrixFromLayout(layout) {
const lines = layout.split('\n');
this.levelForm.boardHeight = lines.length;
this.levelForm.boardWidth = lines[0]?.length || 5;
this.boardMatrix = [];
let index = 0;
for (let row = 0; row < lines.length; row++) {
const line = lines[row];
for (let col = 0; col < line.length; col++) {
const char = line[col];
let tileType = 'o';
let active = false;
if (char === 'x') {
active = true;
tileType = 'r'; // x wird als zufällig interpretiert
} else if (char === 'r') {
active = true;
tileType = 'r';
} else if (char === 'g') {
active = true;
tileType = 'gem';
} else if (char === 's') {
active = true;
tileType = 'star';
} else if (char === 'h') {
active = true;
tileType = 'heart';
} else if (char === 'd') {
active = true;
tileType = 'diamond';
} else if (char === 'c') {
active = true;
tileType = 'circle';
} else if (char === 'q') {
active = true;
tileType = 'square';
} else if (char === 'w') {
active = true;
tileType = 'crown';
} else if (char === 'b') {
active = true;
tileType = 'rainbow';
}
this.boardMatrix.push({
active: active,
tileType: tileType,
index: index++
});
}
}
},
fillAllActive() {
this.boardMatrix.forEach(cell => {
cell.active = true;
cell.tileType = 'r'; // Alle auf zufällig setzen
});
},
clearAll() {
this.boardMatrix.forEach(cell => {
cell.active = false;
cell.tileType = 'o';
});
},
invertBoard() {
this.boardMatrix.forEach(cell => {
cell.active = !cell.active;
if (cell.active && cell.tileType === 'o') {
cell.tileType = 'r'; // Wenn aktiviert, auf zufällig setzen
} else if (!cell.active) {
cell.tileType = 'o';
}
});
},
generateBoardLayout() {
let layout = '';
for (let row = 0; row < this.levelForm.boardHeight; row++) {
for (let col = 0; col < this.levelForm.boardWidth; col++) {
const index = row * this.levelForm.boardWidth + col;
const cell = this.boardMatrix[index];
if (cell.tileType === 'o') {
layout += 'o'; // Leer
} else if (cell.tileType === 'r') {
layout += 'x'; // Zufällig wird als "x" gespeichert
} else {
layout += this.getTileTypeChar(cell.tileType); // Spezifischer Tile-Typ
}
}
if (row < this.levelForm.boardHeight - 1) {
layout += '\n';
}
}
return layout;
},
async saveLevel() {
try {
const levelData = {
...this.levelForm,
boardLayout: this.generateBoardLayout()
};
let savedLevel;
if (this.selectedLevelId !== 'new') {
// Level aktualisieren
const response = await apiClient.put(`/api/admin/minigames/match3/levels/${this.selectedLevelId}`, levelData);
savedLevel = response.data;
} else {
// Neues Level erstellen
const response = await apiClient.post('/api/admin/minigames/match3/levels', levelData);
savedLevel = response.data;
}
// Objectives speichern, falls vorhanden
if (this.levelForm.objectives && this.levelForm.objectives.length > 0) {
for (const objective of this.levelForm.objectives) {
const objectiveData = {
...objective,
levelId: savedLevel.id
};
if (objective.id) {
// Objective aktualisieren
await apiClient.put(`/api/admin/minigames/match3/objectives/${objective.id}`, objectiveData);
} else {
// Neues Objective erstellen
await apiClient.post('/api/admin/minigames/match3/objectives', objectiveData);
}
}
}
this.editingLevel = null;
this.selectedLevelId = 'new';
this.selectedCellIndex = null;
this.loadLevels();
} catch (error) {
console.error('Fehler beim Speichern des Levels:', error);
alert('Fehler beim Speichern des Levels');
}
},
async deleteLevel(levelId) {
if (confirm('Möchtest du dieses Level wirklich löschen?')) {
try {
await apiClient.delete(`/api/admin/minigames/match3/levels/${levelId}`);
this.loadLevels();
} catch (error) {
console.error('Fehler beim Löschen des Levels:', error);
}
}
},
// Objectives Management Methods
addObjective() {
if (!this.levelForm.objectives) {
this.levelForm.objectives = [];
}
const newObjective = {
type: 'score',
description: '',
target: 100,
operator: '>=',
order: this.levelForm.objectives.length + 1,
isRequired: true
};
this.levelForm.objectives.push(newObjective);
},
removeObjective(index) {
if (confirm('Möchtest du dieses Objective wirklich löschen?')) {
this.levelForm.objectives.splice(index, 1);
// Aktualisiere die Reihenfolge
this.levelForm.objectives.forEach((objective, idx) => {
objective.order = idx + 1;
});
}
},
async loadObjectivesForLevel(levelId) {
try {
// Objectives sind jetzt direkt in den Level-Daten enthalten
const level = this.levels.find(l => l.id == levelId);
if (level && level.objectives && Array.isArray(level.objectives)) {
return level.objectives;
}
return [];
} catch (error) {
console.error('Fehler beim Laden der Objectives:', error);
return [];
}
}
}
};
</script>
<style scoped>
.admin-header {
text-align: center;
margin-bottom: 30px;
}
.admin-header h1 {
color: #F9A22C;
margin-bottom: 10px;
}
.admin-header p {
color: #666;
font-size: 16px;
}
.match3-admin {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.section-header {
margin-bottom: 30px;
text-align: center;
}
.section-header h2 {
color: #F9A22C;
font-size: 24px;
margin-bottom: 10px;
}
/* Level Selection */
.level-selection {
margin-bottom: 30px;
text-align: center;
}
.level-count {
margin-bottom: 15px;
}
.level-count p {
font-size: 18px;
color: #333;
font-weight: 500;
}
.level-dropdown {
display: flex;
justify-content: center;
}
.level-select {
padding: 10px 15px;
font-size: 16px;
border: 2px solid #ddd;
border-radius: 8px;
background: white;
min-width: 300px;
cursor: pointer;
}
.level-select:focus {
outline: none;
border-color: #F9A22C;
}
/* Level Details & Form */
.level-details,
.level-form {
background: white;
border: 2px solid #ddd;
border-radius: 12px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.details-header,
.form-header {
margin-bottom: 25px;
text-align: center;
}
.details-header h3,
.form-header h3 {
color: #F9A22C;
font-size: 22px;
margin: 0;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
font-size: 16px;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s ease;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #F9A22C;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
/* Board Editor */
.board-editor {
margin-top: 10px;
}
.board-matrix {
display: grid;
gap: 2px;
margin-bottom: 15px;
border: 2px solid #333;
padding: 10px;
background: #f5f5f5;
}
.board-cell {
width: 40px;
height: 40px;
border: 1px solid #ccc;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: white;
transition: all 0.2s ease;
position: relative;
}
.board-cell:hover {
border-color: #F9A22C;
transform: scale(1.05);
}
.board-cell.selected {
border: 3px solid #ff0000;
box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
}
.board-cell.active {
background: #4CAF50;
color: white;
border-color: #45a049;
}
.board-cell.inactive {
background: #f44336;
color: white;
border-color: #da190b;
}
.board-cell.random {
background: #FF9800;
color: white;
border-color: #F57C00;
}
.board-cell.empty {
background: #9E9E9E;
color: white;
border-color: #757575;
}
.cell-status {
font-size: 16px;
font-weight: bold;
}
/* Minimale Tile Selection */
.tile-selection-minimal {
display: flex;
align-items: center;
gap: 15px;
margin: 10px 0;
padding: 10px 15px;
background: #f0f0f0;
border-radius: 6px;
border: 1px solid #ddd;
}
.selection-label {
font-size: 14px;
color: #666;
font-weight: 500;
white-space: nowrap;
}
.tile-options-minimal {
display: flex;
gap: 8px;
align-items: center;
}
.tile-option-mini {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
cursor: pointer;
border: 2px solid #ddd;
border-radius: 4px;
background: white;
transition: all 0.2s ease;
font-size: 18px;
}
.tile-option-mini:hover {
border-color: #F9A22C;
transform: scale(1.1);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.board-controls {
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
.board-controls .btn {
padding: 8px 16px;
font-size: 14px;
}
/* Tile Types Selector */
.tile-types-selection {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
}
.tile-type-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
background: #f9f9f9;
transition: all 0.2s ease;
}
.tile-type-checkbox:hover {
background: #f0f0f0;
border-color: #F9A22C;
}
.tile-type-checkbox input[type="checkbox"] {
width: auto;
margin: 0;
}
.tile-symbol {
font-size: 20px;
margin-right: 8px;
}
.tile-name {
font-size: 14px;
color: #333;
font-weight: 500;
}
/* Objectives Section Container */
.objectives-section-container {
background: white;
border: 2px solid #ddd;
border-radius: 12px;
padding: 25px;
margin-bottom: 30px;
}
.objectives-section-container .form-header {
margin-bottom: 20px;
text-align: center;
}
.objectives-section-container .form-header h3 {
color: #F9A22C;
font-size: 24px;
margin-bottom: 10px;
}
/* Objectives Section Styles */
.objectives-section {
margin-top: 20px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background: #f9f9f9;
}
.objectives-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #ddd;
}
.objectives-header h4 {
margin: 0;
color: #F9A22C;
font-size: 18px;
}
.btn-sm {
padding: 8px 16px;
font-size: 14px;
}
.objectives-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.objective-item {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.objective-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.objective-number {
font-weight: 600;
color: #F9A22C;
font-size: 16px;
}
.objective-form .form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.objective-form .form-group {
margin-bottom: 15px;
}
.objective-form label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.objective-form .form-control {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.objective-form .form-control:focus {
outline: none;
border-color: #F9A22C;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
width: auto;
margin: 0;
}
.no-objectives {
text-align: center;
padding: 30px;
color: #666;
font-style: italic;
}
/* Form Actions */
.form-actions {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.btn {
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-primary {
background: #F9A22C;
color: white;
}
.btn-primary:hover {
background: #e8941f;
transform: translateY(-2px);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-2px);
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
transform: translateY(-2px);
}
/* Responsive Design */
@media (max-width: 768px) {
.match3-admin {
padding: 15px;
}
.form-row {
grid-template-columns: 1fr;
}
.board-controls {
flex-direction: column;
align-items: center;
}
.form-actions {
flex-direction: column;
align-items: center;
}
.level-select {
min-width: 250px;
}
.tile-selection-minimal {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.selection-label {
font-size: 12px;
}
.tile-option-mini {
width: 28px;
height: 28px;
font-size: 16px;
}
}
</style>