Spell Learn Dialog Anzeige funktioniert

This commit is contained in:
2025-08-06 23:05:59 +02:00
parent 8f465f0095
commit 60b95a9a7a
6 changed files with 1105 additions and 8 deletions
+2 -2
View File
@@ -2079,7 +2079,7 @@ func GetAvailableSkillsNewSystem(c *gin.Context) {
// GetAvailableSpellsNewSystem gibt alle verfügbaren Zauber mit Lernkosten zurück (POST mit LernCostRequest)
func GetAvailableSpellsNewSystem(c *gin.Context) {
characterID := c.Param("id")
//characterID := c.Param("id")
// Parse LernCostRequest aus POST body
var baseRequest gsmaster.LernCostRequest
@@ -2089,7 +2089,7 @@ func GetAvailableSpellsNewSystem(c *gin.Context) {
}
var character models.Char
if err := database.DB.Preload("Zauber").Preload("Erfahrungsschatz").Preload("Vermoegen").First(&character, characterID).Error; err != nil {
if err := database.DB.Preload("Zauber").Preload("Erfahrungsschatz").Preload("Vermoegen").First(&character, baseRequest.CharId).Error; err != nil {
respondWithError(c, http.StatusNotFound, "Character not found")
return
}
+278
View File
@@ -0,0 +1,278 @@
package character
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"bamort/database"
"bamort/gsmaster"
"bamort/models"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestGetAvailableSpellsNewSystem(t *testing.T) {
// Setup test database with real data
database.SetupTestDB(true, true)
defer database.ResetTestDB()
// Setup Gin in test mode
gin.SetMode(gin.TestMode)
t.Run("Get available spells for character ID 20", func(t *testing.T) {
// Test Request wie im Frontend
request := gsmaster.LernCostRequest{
CharId: 18,
Type: "spell",
Action: "learn",
UsePP: 0,
UseGold: 0,
Reward: stringPtr("default"),
}
// Convert request to JSON
requestJSON, err := json.Marshal(request)
assert.NoError(t, err, "Should marshal request")
// Create HTTP request
req, _ := http.NewRequest("POST", "/api/characters/available-spells-new", bytes.NewBuffer(requestJSON))
req.Header.Set("Content-Type", "application/json")
// Create response recorder
w := httptest.NewRecorder()
// Create Gin context
c, _ := gin.CreateTestContext(w)
c.Request = req
fmt.Printf("Test: Get available spells for character ID 20\n")
fmt.Printf("Request: %s\n", string(requestJSON))
// Call the handler function
GetAvailableSpellsNewSystem(c)
// Print the response for debugging
fmt.Printf("Response Status: %d\n", w.Code)
fmt.Printf("Response Body: %s\n", w.Body.String())
// Assert response status
assert.Equal(t, http.StatusOK, w.Code, "Status code should be 200 OK")
// Parse response
var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err, "Response should be valid JSON")
// Check response structure
assert.Contains(t, response, "spells_by_school", "Response should contain spells_by_school field")
// Check if spells_by_school is an object
spellsBySchool, ok := response["spells_by_school"].(map[string]interface{})
assert.True(t, ok, "spells_by_school should be an object")
fmt.Printf("Found %d spell schools\n", len(spellsBySchool))
// Check each school
totalSpells := 0
for schoolName, schoolSpells := range spellsBySchool {
fmt.Printf("School: %s\n", schoolName)
spells, ok := schoolSpells.([]interface{})
assert.True(t, ok, fmt.Sprintf("School %s should contain an array of spells", schoolName))
totalSpells += len(spells)
fmt.Printf(" Spells in school: %d\n", len(spells))
// Check structure of first spell in school if exists
if len(spells) > 0 {
firstSpell, ok := spells[0].(map[string]interface{})
assert.True(t, ok, "First spell should be an object")
// Check required fields
assert.Contains(t, firstSpell, "name", "Spell should have name field")
assert.Contains(t, firstSpell, "level", "Spell should have level field")
assert.Contains(t, firstSpell, "epCost", "Spell should have epCost field")
assert.Contains(t, firstSpell, "goldCost", "Spell should have goldCost field")
spellName, ok := firstSpell["name"].(string)
assert.True(t, ok, "Spell name should be a string")
assert.NotEmpty(t, spellName, "Spell name should not be empty")
level, ok := firstSpell["level"].(float64)
assert.True(t, ok, "Spell level should be a number")
assert.GreaterOrEqual(t, level, float64(0), "Spell level should be at least 0")
epCost, ok := firstSpell["epCost"].(float64)
assert.True(t, ok, "EP cost should be a number")
assert.GreaterOrEqual(t, epCost, float64(0), "EP cost should be non-negative")
goldCost, ok := firstSpell["goldCost"].(float64)
assert.True(t, ok, "Gold cost should be a number")
assert.GreaterOrEqual(t, goldCost, float64(0), "Gold cost should be non-negative")
fmt.Printf(" Example spell: %s (Level %v, EP: %v, Gold: %v)\n",
spellName, level, epCost, goldCost)
}
}
fmt.Printf("Total spells found: %d\n", totalSpells)
assert.Greater(t, totalSpells, 0, "Should have at least some spells available")
})
t.Run("Test with different reward types", func(t *testing.T) {
// Verwende existierenden Charakter
rewardTypes := []string{"default", "noGold", "halveep", "halveepnoGold"}
for _, rewardType := range rewardTypes {
t.Run(fmt.Sprintf("RewardType_%s", rewardType), func(t *testing.T) {
request := gsmaster.LernCostRequest{
CharId: 18,
Type: "spell",
Action: "learn",
UsePP: 0,
UseGold: 0,
Reward: stringPtr(rewardType),
}
requestJSON, err := json.Marshal(request)
assert.NoError(t, err)
req, _ := http.NewRequest("POST", "/api/characters/available-spells-new", bytes.NewBuffer(requestJSON))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
GetAvailableSpellsNewSystem(c)
fmt.Printf("Testing reward type: %s - Status: %d\n", rewardType, w.Code)
assert.Equal(t, http.StatusOK, w.Code, fmt.Sprintf("Should work with reward type %s", rewardType))
var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err, "Response should be valid JSON")
assert.Contains(t, response, "spells_by_school", "Should contain spells_by_school")
})
}
})
t.Run("Test with invalid character ID", func(t *testing.T) {
request := gsmaster.LernCostRequest{
CharId: 99999, // Non-existent character
Type: "spell",
Action: "learn",
UsePP: 0,
UseGold: 0,
Reward: stringPtr("default"),
}
requestJSON, err := json.Marshal(request)
assert.NoError(t, err)
req, _ := http.NewRequest("POST", "/api/characters/available-spells-new", bytes.NewBuffer(requestJSON))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
GetAvailableSpellsNewSystem(c)
fmt.Printf("Testing invalid character ID - Status: %d\n", w.Code)
// Der Handler verwendet nicht den "id" Parameter aus der URL, sondern CharId aus dem Request Body
// Daher wird kein 404 zurückgegeben, sondern ein leeres Ergebnis oder Fehler
assert.True(t, w.Code == http.StatusNotFound || w.Code == http.StatusOK,
"Should return 404 or 200 for non-existent character")
})
t.Run("Test with invalid request format", func(t *testing.T) {
invalidJSON := []byte(`{"invalid": "request"}`)
req, _ := http.NewRequest("POST", "/api/characters/available-spells-new", bytes.NewBuffer(invalidJSON))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
GetAvailableSpellsNewSystem(c)
fmt.Printf("Testing invalid request format - Status: %d\n", w.Code)
assert.Equal(t, http.StatusBadRequest, w.Code, "Should return 400 for invalid request")
})
t.Run("Test excluding already learned spells", func(t *testing.T) {
// Füge dem Charakter einen Zauber hinzu
learnedSpell := models.SkZauber{
BamortCharTrait: models.BamortCharTrait{
BamortBase: models.BamortBase{
Name: "Feuerball",
},
CharacterID: 20,
},
Beschreibung: "Ein mächtiger Feuerball",
Bonus: 5,
Quelle: "Test",
}
err := database.DB.Create(&learnedSpell).Error
assert.NoError(t, err, "Should create learned spell")
request := gsmaster.LernCostRequest{
CharId: 18,
Type: "spell",
Action: "learn",
UsePP: 0,
UseGold: 0,
Reward: stringPtr("default"),
}
requestJSON, err := json.Marshal(request)
assert.NoError(t, err)
req, _ := http.NewRequest("POST", "/api/characters/available-spells-new", bytes.NewBuffer(requestJSON))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
GetAvailableSpellsNewSystem(c)
assert.Equal(t, http.StatusOK, w.Code, "Should return 200 OK")
var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
spellsBySchool := response["spells_by_school"].(map[string]interface{})
// Prüfe, dass "Feuerball" nicht in der Liste ist
foundFeuerball := false
for _, schoolSpells := range spellsBySchool {
spells := schoolSpells.([]interface{})
for _, spell := range spells {
spellObj := spell.(map[string]interface{})
if spellObj["name"].(string) == "Feuerball" {
foundFeuerball = true
break
}
}
}
assert.False(t, foundFeuerball, "Already learned spell 'Feuerball' should not be in available spells")
fmt.Printf("Correctly excluded already learned spell 'Feuerball'\n")
})
}
// Helper function to create string pointer
func stringPtr(s string) *string {
return &s
}
+4
View File
@@ -37,6 +37,10 @@ var (
)
func ConnectDatabase() *gorm.DB {
SetupTestDB()
return DB
}
func ConnectDatabaseOrig() *gorm.DB {
dsn := "bamort:bG4)efozrc@tcp(192.168.0.5:3306)/bamort?charset=utf8mb4&parseTime=True&loc=Local"
database, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
@@ -0,0 +1,691 @@
<template>
<div v-if="show" class="modal-overlay" @click.self="closeDialog">
<div class="modal-content modal-wide">
<h3>{{ $t('spells.learn.title') }}</h3>
<!-- Aktuelle Ressourcen -->
<div class="current-resources">
<div class="resource-display-card">
<span class="resource-icon"></span>
<div class="resource-info">
<div class="resource-label">Erfahrungspunkte</div>
<div class="resource-amount">{{ character.erfahrungsschatz?.ep || 0 }} EP</div>
<div class="resource-remaining">
<small :class="{ 'text-warning': remainingEP < 50, 'text-danger': remainingEP <= 0 }">
Verbleibend: {{ remainingEP }} EP
</small>
</div>
</div>
</div>
<div class="resource-display-card">
<span class="resource-icon">💰</span>
<div class="resource-info">
<div class="resource-label">Gold</div>
<div class="resource-amount">{{ character.vermoegen?.goldstücke || 0 }} GS</div>
<div class="resource-remaining">
<small :class="{ 'text-warning': remainingGold < 20, 'text-danger': remainingGold <= 0 }">
Nach Lernen: {{ remainingGold }} GS
</small>
</div>
</div>
</div>
</div>
<!-- Belohnungsart, Suche und Sortierung -->
<div class="form-group form-row">
<div class="form-col form-col-main">
<label>Lernen als Belohnung:</label>
<select v-model="selectedRewardType" :disabled="isLoadingRewardTypes">
<option value="" disabled>
{{ isLoadingRewardTypes ? 'Lade Belohnungsarten...' : 'Belohnungsart wählen' }}
</option>
<option
v-for="rewardType in availableRewardTypes"
:key="rewardType.value"
:value="rewardType.value"
>
{{ rewardType.label }}
</option>
</select>
</div>
<div class="form-col form-col-input">
<label>{{ $t('spells.learn.search.label') }}</label>
<input
v-model="searchTerm"
type="text"
:placeholder="$t('spells.learn.search.placeholder')"
/>
</div>
<div class="form-col form-col-input">
<label>Sortierung:</label>
<select v-model="sortBy">
<option value="name">Name</option>
<option value="epCost">EP Kosten</option>
<option value="goldCost">Gold Kosten</option>
</select>
</div>
</div>
<!-- Schule Buttons -->
<div class="form-group">
<label>{{ $t('spells.learn.school.label') }}</label>
<div class="school-buttons">
<button
class="school-btn"
:class="{ 'active': selectedSchool === '' }"
@click="selectedSchool = ''"
>
{{ $t('spells.learn.school.all') }}
</button>
<button
v-for="school in availableSchools"
:key="school"
class="school-btn"
:class="{ 'active': selectedSchool === school }"
@click="selectedSchool = school"
>
{{ school }}
</button>
</div>
</div>
<!-- Verfügbare Zauber -->
<div class="form-group">
<label>{{ $t('spells.learn.available') }}</label>
<div v-if="filteredSpells.length > 0" class="learning-levels">
<div
v-for="spell in filteredSpells"
:key="spell.name"
class="level-option"
:class="{ 'selected': selectedSpell?.name === spell.name }"
@click="selectSpell(spell)"
>
<div class="level-header">
<span class="level-target">{{ spell.name }}</span>
<span class="level-cost">
<span v-if="spell.epCost">{{ spell.epCost }} EP</span>
<span v-if="spell.epCost && spell.goldCost"> + </span>
<span v-if="spell.goldCost">{{ spell.goldCost }} GS</span>
</span>
</div>
</div>
</div>
<div v-else-if="!isLoading" class="no-spells">
Keine Zauber verfügbar
</div>
</div>
<!-- Ausgewählter Zauber Details -->
<div v-if="selectedSpell" class="form-group">
<div class="selection-summary">
<strong>Ausgewählt:</strong> {{ selectedSpell.name }}
<br>
<span class="cost-summary">
Lernkosten:
<span v-if="selectedSpell.epCost">{{ selectedSpell.epCost }} EP</span>
<span v-if="selectedSpell.epCost && selectedSpell.goldCost"> + </span>
<span v-if="selectedSpell.goldCost">{{ selectedSpell.goldCost }} GS</span>
</span>
</div>
</div>
<div v-if="isLoading" class="loading-message">
{{ $t('common.loading') }}
</div>
<div class="modal-actions">
<button
@click="learnSpell"
class="btn-confirm"
:disabled="!selectedSpell || isLoading"
>
{{ isLoading ? 'Wird gelernt...' : $t('spells.learn.action') }}
</button>
<button @click="closeDialog" class="btn-cancel" :disabled="isLoading">
{{ $t('common.cancel') }}
</button>
</div>
</div>
</div>
</template>
<script>
import API from '@/utils/api'
export default {
name: "SpellLearnDialog",
props: {
show: {
type: Boolean,
required: true
},
character: {
type: Object,
required: true
}
},
emits: ['close', 'spell-learned'],
data() {
return {
searchTerm: '',
selectedSchool: '',
selectedRewardType: '',
sortBy: 'name',
spellsBySchool: {},
selectedSpell: null,
isLoading: false,
availableRewardTypes: [],
isLoadingRewardTypes: false
};
},
computed: {
remainingEP() {
const currentEP = this.character.erfahrungsschatz?.ep || 0;
const spellEPCost = this.selectedSpell?.epCost || 0;
return Math.max(0, currentEP - spellEPCost);
},
remainingGold() {
const currentGold = this.character.vermoegen?.goldstücke || 0;
const spellGoldCost = this.selectedSpell?.goldCost || 0;
return Math.max(0, currentGold - spellGoldCost);
},
totalCosts() {
if (!this.selectedSpell) return 0;
return (this.selectedSpell.epCost || 0) + (this.selectedSpell.goldCost || 0);
},
filteredSpells() {
let allSpells = [];
// Sammle alle Zauber aus allen Schulen
Object.keys(this.spellsBySchool).forEach(school => {
if (!this.selectedSchool || school === this.selectedSchool) {
allSpells = allSpells.concat(this.spellsBySchool[school]);
}
});
// Filter nach Suchterm
let filtered = allSpells.filter(spell => {
const matchesSearch = !this.searchTerm ||
spell.name.toLowerCase().includes(this.searchTerm.toLowerCase());
return matchesSearch;
});
// Sortierung
return filtered.sort((a, b) => {
switch (this.sortBy) {
case 'epCost':
return (a.epCost || 0) - (b.epCost || 0);
case 'goldCost':
return (a.goldCost || 0) - (b.goldCost || 0);
case 'name':
default:
return a.name.localeCompare(b.name);
}
});
},
availableSchools() {
return Object.keys(this.spellsBySchool).sort();
}
},
watch: {
show(newVal) {
if (newVal) {
this.resetForm();
this.loadRewardTypes();
}
},
selectedRewardType() {
if (this.selectedRewardType) {
this.loadAvailableSpells();
}
}
},
created() {
this.$api = API;
},
methods: {
closeDialog() {
this.$emit('close');
},
resetForm() {
this.searchTerm = '';
this.selectedSchool = '';
this.selectedRewardType = '';
this.sortBy = 'name';
this.spellsBySchool = {};
this.selectedSpell = null;
this.availableRewardTypes = [];
},
async loadRewardTypes() {
const token = localStorage.getItem('token');
if (!token) {
console.error('No authentication token available');
this.availableRewardTypes = [{
value: 'error',
label: 'Anmeldung erforderlich'
}];
return;
}
this.isLoadingRewardTypes = true;
try {
const response = await this.$api.get(`/api/characters/${this.character.id}/reward-types`, {
params: {
learning_type: 'spell',
skill_name: 'spell',
current_level: 0,
skill_type: 'spell'
}
});
this.availableRewardTypes = response.data.reward_types || [];
// Setze Default-Belohnungsart wenn verfügbar
if (this.availableRewardTypes.length > 0 && !this.selectedRewardType) {
const defaultReward = this.availableRewardTypes.find(r => r.value === 'default');
this.selectedRewardType = defaultReward ? defaultReward.value : this.availableRewardTypes[0].value;
}
} catch (error) {
console.error('Fehler beim Laden der Belohnungsarten:', error);
this.availableRewardTypes = [{
value: 'default',
label: 'Standard'
}];
this.selectedRewardType = 'default';
} finally {
this.isLoadingRewardTypes = false;
}
},
performInitialSearch() {
// Beim Öffnen alle Zauber laden
this.loadAvailableSpells();
},
async loadAvailableSpells() {
if (!this.selectedRewardType) return;
try {
this.isLoading = true;
// Erstelle LernCostRequest wie vom Backend erwartet
const request = {
char_id: this.character.id,
type: 'spell',
action: 'learn',
use_pp: 0,
use_gold: 0,
reward: this.selectedRewardType
};
const response = await this.$api.post('/api/characters/available-spells-new', request);
this.spellsBySchool = response.data.spells_by_school || {};
} catch (error) {
console.error('Fehler beim Laden der verfügbaren Zauber:', error);
this.spellsBySchool = {};
} finally {
this.isLoading = false;
}
},
selectSpell(spell) {
this.selectedSpell = this.selectedSpell?.name === spell.name ? null : spell;
},
async learnSpell() {
if (!this.selectedSpell || this.isLoading) return;
try {
this.isLoading = true;
const response = await this.$api.post(`/characters/${this.character.id}/learn-spell-new`, {
char_id: this.character.id,
name: this.selectedSpell.name,
type: 'spell',
action: 'learn',
use_pp: 0,
use_gold: 0,
reward: this.selectedRewardType
});
this.$emit('spell-learned', {
spell: this.selectedSpell,
response: response.data
});
} catch (error) {
console.error('Fehler beim Lernen des Zaubers:', error);
alert('Fehler beim Lernen des Zaubers: ' + (error.response?.data?.message || error.message));
} finally {
this.isLoading = false;
}
}
}
};
</script>
<style scoped>
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: flex-start;
align-items: flex-start;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
padding: 24px;
width: 100vw;
height: 100vh;
max-width: 100vw;
max-height: 100vh;
overflow-y: auto;
animation: modalSlideIn 0.3s ease;
box-sizing: border-box;
}
.modal-wide {
max-width: 100vw;
}
/* Ressourcen-Anzeige im Dialog */
.current-resources {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.resource-display-card {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
flex: 1;
min-width: 160px;
}
.resource-display-card .resource-icon {
font-size: 20px;
}
.resource-info {
flex: 1;
}
.resource-label {
font-size: 12px;
color: #6c757d;
font-weight: 500;
}
.resource-amount {
font-size: 16px;
font-weight: bold;
color: #495057;
}
.resource-remaining {
margin-top: 4px;
}
.resource-remaining small {
color: #6c757d;
font-weight: normal;
}
.text-warning {
color: #f0ad4e !important;
}
.text-danger {
color: #d9534f !important;
}
/* Filter und Sortierung */
.school-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 5px;
}
.school-btn {
padding: 6px 12px;
border: 1px solid #dee2e6;
border-radius: 4px;
background: white;
color: #495057;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
}
.school-btn:hover {
background: #f8f9fa;
border-color: #007bff;
}
.school-btn.active {
background: #007bff;
color: white;
border-color: #007bff;
}
/* Zauber-Auswahl */
.selection-summary {
background: #e7f3ff;
padding: 12px;
border-radius: 6px;
margin-bottom: 10px;
border-left: 4px solid #007bff;
}
.cost-summary {
color: #28a745;
font-weight: bold;
}
.learning-levels {
border: 1px solid #dee2e6;
border-radius: 6px;
max-height: 300px;
overflow-y: auto;
}
.level-option {
padding: 12px 16px;
border-bottom: 1px solid #f1f1f1;
cursor: pointer;
transition: all 0.2s ease;
}
.level-option:last-child {
border-bottom: none;
}
.level-option:hover:not(.disabled) {
background: #f8f9fa;
}
.level-option.selected {
background: #e7f3ff;
border-left: 4px solid #007bff;
}
.level-option.disabled {
background: #f8f9fa;
color: #6c757d;
cursor: not-allowed;
opacity: 0.6;
}
.level-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 500;
}
.level-target {
color: #495057;
}
.level-cost {
color: #28a745;
font-weight: bold;
}
.level-option.disabled .level-cost {
color: #dc3545;
}
.no-spells {
text-align: center;
padding: 20px;
color: #6c757d;
font-style: italic;
}
.loading-message {
text-align: center;
padding: 20px;
color: #6c757d;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modal-content h3 {
margin-top: 0;
margin-bottom: 20px;
color: #333;
border-bottom: 2px solid #1da766;
padding-bottom: 10px;
}
.form-group {
margin-bottom: 15px;
}
.form-row {
display: flex;
gap: 15px;
align-items: flex-start;
}
.form-col {
flex: 1;
min-width: 0;
}
.form-col-main {
flex: 2;
min-width: 200px;
}
.form-col-input {
flex: 1;
min-width: 140px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.form-group textarea {
height: 80px;
resize: vertical;
}
.help-text {
display: block;
margin-top: 5px;
font-size: 12px;
color: #6c757d;
font-style: italic;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #eee;
}
.btn-confirm {
padding: 8px 20px;
background: #1da766;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background 0.2s ease;
}
.btn-confirm:hover:not(:disabled) {
background: #16a085;
}
.btn-confirm:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn-cancel {
padding: 8px 20px;
background: #6c757d;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s ease;
}
.btn-cancel:hover:not(:disabled) {
background: #5a6268;
}
</style>
+99 -3
View File
@@ -1,10 +1,25 @@
<template>
<div class="cd-view">
<h2>{{ character.name }}'s Zauber</h2>
<!-- Header mit Lernmodus-Kontrollen -->
<div class="header-section">
<h2>{{ character.name }}'s Zauber</h2>
<div class="learning-mode-controls">
<!-- Lernmodus Toggle Button -->
<button
@click="showLearnNewDialog"
class="btn-learning-mode"
title="Neuen Zauber lernen"
>
<span class="icon">🎓</span>
</button>
</div>
</div>
<div class="cd-list">
<table class="cd-table">
<thead>
<tr>
<tr class="cd-table-header">
<th>{{ $t('spell.name') }}</th>
<th>{{ $t('spell.description') }}</th>
<th>{{ $t('spell.bonus') }}</th>
@@ -12,7 +27,7 @@
</tr>
</thead>
<tbody>
<template v-for="spell in character.zauber">
<template v-for="spell in character.zauber" :key="spell.id || spell.name">
<tr>
<td>{{ spell.name || '-' }}</td>
<td>{{ spell.beschreibung || '-' }}</td>
@@ -23,22 +38,103 @@
</tbody>
</table>
</div> <!--- end cd-list-->
<!-- Dialog für neue Zauber lernen -->
<SpellLearnDialog
:character="character"
:show="showLearnDialog"
@close="closeDialogs"
@spell-learned="handleSpellLearned"
/>
</div> <!--- end character -datasheet-->
</template>
<style>
.cd-table {
width: 100%;
}
.cd-table-header {
background-color: #1da766;
color: white;
font-weight: bold;
}
/* Header mit Lernmodus-Kontrollen */
.header-section {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
}
.learning-mode-controls {
display: flex;
align-items: center;
gap: 15px;
}
/* Lernmodus Toggle Button */
.btn-learning-mode {
padding: 8px 16px;
border: 2px solid #1da766;
background: white;
color: #1da766;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
display: flex;
align-items: center;
gap: 5px;
transition: all 0.3s ease;
position: relative;
}
.btn-learning-mode:hover {
background: #1da766;
color: white;
}
</style>
<script>
import API from '@/utils/api'
import SpellLearnDialog from './SpellLearnDialog.vue'
export default {
name: "SpellView",
components: {
SpellLearnDialog
},
props: {
character: {
type: Object,
required: true
}
},
data() {
return {
showLearnDialog: false,
isLoading: false
};
},
created() {
this.$api = API;
},
methods: {
showLearnNewDialog() {
this.showLearnDialog = true;
},
closeDialogs() {
this.showLearnDialog = false;
},
handleSpellLearned(eventData) {
this.$emit('character-updated');
this.closeDialogs();
}
}
};
</script>
+31 -3
View File
@@ -78,10 +78,34 @@ export default {
wirkungsbereich:'Wirkungsbereich',
wirkungsdauer:'Wirkungsdauer',
ursprung:'Ursprung',
quelle:'Quelle',
system:'System',
bonus:'Bonus',
quelle:'Quelle',
},
spells: {
learn: {
title: 'Neuen Zauber lernen',
search: {
label: 'Suche',
placeholder: 'Zauber suchen...'
},
school: {
label: 'Schule',
all: 'Alle Schulen'
},
available: 'Verfügbare Zauber',
selected: 'Ausgewählter Zauber',
costs: {
title: 'Lernkosten',
ep: 'Erfahrungspunkte',
gold: 'Gold',
total: 'Gesamt'
},
action: 'Zauber lernen'
},
costs: {
ep: 'EP',
gold: 'Gold'
}
},
Spell:'Zauber',
weapon:{
@@ -128,6 +152,10 @@ export default {
},
search:'Suche',
Skill:'Fertigkeit',
common: {
loading: 'Laden...',
cancel: 'Abbrechen'
},
experience: {
title: 'Erfahrung & Vermögen',
experience_points: 'Erfahrungspunkte',