gemification_admin bekötve
This commit is contained in:
545
frontend_admin/pages/gamification/config.vue
Normal file
545
frontend_admin/pages/gamification/config.vue
Normal file
@@ -0,0 +1,545 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-white">{{ t('gamification.config.title') }}</h1>
|
||||
<p class="text-slate-400 mt-1">{{ t('gamification.config.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="flex items-center justify-center py-20">
|
||||
<div class="text-slate-400 flex items-center gap-3">
|
||||
<svg class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span>{{ t('gamification.config.loading') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="bg-rose-500/10 border border-rose-500/30 rounded-xl p-6 mb-8">
|
||||
<p class="text-rose-400">{{ t('gamification.config.load_error') }}</p>
|
||||
<button @click="fetchConfig" class="mt-3 text-sm text-rose-300 hover:text-rose-200 underline">{{ t('gamification.config.retry') }}</button>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- Save Button Bar -->
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm text-slate-500">{{ t('gamification.config.last_saved') }} {{ lastSaved ? lastSaved : '—' }}</span>
|
||||
</div>
|
||||
<button @click="saveConfig" :disabled="saving" class="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 disabled:bg-emerald-600/50 text-white rounded-lg text-sm font-medium transition flex items-center gap-2">
|
||||
<svg v-if="saving" class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
{{ saving ? t('gamification.config.saving') : t('gamification.config.save_config') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Config Sections Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<!-- XP Logic -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
{{ t('gamification.config.xp_logic') }}
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('gamification.config.base_xp') }}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model.number="xpLogic.baseXp"
|
||||
type="range"
|
||||
min="0"
|
||||
max="10000"
|
||||
step="100"
|
||||
class="flex-1 accent-amber-500"
|
||||
/>
|
||||
<input
|
||||
v-model.number="xpLogic.baseXp"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10000"
|
||||
class="w-20 px-2 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm text-center focus:outline-none focus:ring-2 focus:ring-amber-500/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('gamification.config.exponent') }}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model.number="xpLogic.exponent"
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="5.0"
|
||||
step="0.1"
|
||||
class="flex-1 accent-amber-500"
|
||||
/>
|
||||
<input
|
||||
v-model.number="xpLogic.exponent"
|
||||
type="number"
|
||||
min="0.1"
|
||||
max="5.0"
|
||||
step="0.1"
|
||||
class="w-20 px-2 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm text-center focus:outline-none focus:ring-2 focus:ring-amber-500/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conversion Logic -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
</svg>
|
||||
{{ t('gamification.config.conversion_logic') }}
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('gamification.config.social_credit_rate') }}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model.number="conversionLogic.socialToCreditRate"
|
||||
type="range"
|
||||
min="0"
|
||||
max="10000"
|
||||
step="10"
|
||||
class="flex-1 accent-emerald-500"
|
||||
/>
|
||||
<input
|
||||
v-model.number="conversionLogic.socialToCreditRate"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10000"
|
||||
class="w-20 px-2 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm text-center focus:outline-none focus:ring-2 focus:ring-emerald-500/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Penalty Logic -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-rose-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
{{ t('gamification.config.penalty_logic') }}
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('gamification.config.recovery_rate') }}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model.number="penaltyLogic.recoveryRate"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
class="flex-1 accent-rose-500"
|
||||
/>
|
||||
<input
|
||||
v-model.number="penaltyLogic.recoveryRate"
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
class="w-20 px-2 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm text-center focus:outline-none focus:ring-2 focus:ring-rose-500/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-2">{{ t('gamification.config.thresholds') }}</label>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500 mb-1">{{ t('gamification.config.level_1') }}</label>
|
||||
<input v-model.number="penaltyLogic.thresholds.level1" type="number" min="0" class="w-full px-2 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm text-center focus:outline-none focus:ring-2 focus:ring-rose-500/50" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500 mb-1">{{ t('gamification.config.level_2') }}</label>
|
||||
<input v-model.number="penaltyLogic.thresholds.level2" type="number" min="0" class="w-full px-2 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm text-center focus:outline-none focus:ring-2 focus:ring-rose-500/50" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500 mb-1">{{ t('gamification.config.level_3') }}</label>
|
||||
<input v-model.number="penaltyLogic.thresholds.level3" type="number" min="0" class="w-full px-2 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm text-center focus:outline-none focus:ring-2 focus:ring-rose-500/50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-2">{{ t('gamification.config.multipliers') }}</label>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<div v-for="key in multiplierKeys" :key="key">
|
||||
<label class="block text-xs text-slate-500 mb-1">{{ key }}</label>
|
||||
<input v-model.number="penaltyLogic.multipliers[key]" type="number" min="0" max="1" step="0.1" class="w-full px-2 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm text-center focus:outline-none focus:ring-2 focus:ring-rose-500/50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Level Rewards -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-violet-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
||||
</svg>
|
||||
{{ t('gamification.config.level_rewards') }}
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('gamification.config.credits_per_10_levels') }}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model.number="levelRewards.creditsPer10Levels"
|
||||
type="range"
|
||||
min="0"
|
||||
max="10000"
|
||||
step="10"
|
||||
class="flex-1 accent-violet-500"
|
||||
/>
|
||||
<input
|
||||
v-model.number="levelRewards.creditsPer10Levels"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10000"
|
||||
class="w-20 px-2 py-1.5 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm text-center focus:outline-none focus:ring-2 focus:ring-violet-500/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Impact Calculator -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6 mb-8">
|
||||
<h2 class="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
{{ t('gamification.config.impact_calculator') }}
|
||||
</h2>
|
||||
<p class="text-sm text-slate-500 mb-6">{{ t('gamification.config.calculator_hint') }}</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Calculator Inputs -->
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-2">
|
||||
{{ t('gamification.config.user_level') }}: <span class="text-cyan-400 font-bold">{{ calculator.level }}</span>
|
||||
</label>
|
||||
<input v-model.number="calculator.level" type="range" min="1" max="100" class="w-full accent-cyan-500" />
|
||||
<div class="flex justify-between text-xs text-slate-600 mt-1">
|
||||
<span>1</span><span>100</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-2">
|
||||
{{ t('gamification.config.daily_activities') }}: <span class="text-cyan-400 font-bold">{{ calculator.activities }}</span>
|
||||
</label>
|
||||
<input v-model.number="calculator.activities" type="range" min="1" max="20" class="w-full accent-cyan-500" />
|
||||
<div class="flex justify-between text-xs text-slate-600 mt-1">
|
||||
<span>1</span><span>20</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-2">
|
||||
{{ t('gamification.config.social_points') }}: <span class="text-cyan-400 font-bold">{{ calculator.socialPoints }}</span>
|
||||
</label>
|
||||
<input v-model.number="calculator.socialPoints" type="range" min="0" max="500" step="10" class="w-full accent-cyan-500" />
|
||||
<div class="flex justify-between text-xs text-slate-600 mt-1">
|
||||
<span>0</span><span>500</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calculator Results -->
|
||||
<div class="bg-slate-700/50 rounded-xl p-6 space-y-4">
|
||||
<h3 class="text-sm font-medium text-slate-400 uppercase tracking-wider">{{ t('gamification.config.estimated_results') }}</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between py-2 border-b border-slate-600/50">
|
||||
<span class="text-sm text-slate-300">{{ t('gamification.config.daily_xp') }}</span>
|
||||
<span class="text-lg font-bold text-amber-400">{{ formatNumber(calculatedDailyXp) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-2 border-b border-slate-600/50">
|
||||
<span class="text-sm text-slate-300">{{ t('gamification.config.level_up_xp') }}</span>
|
||||
<span class="text-lg font-bold text-emerald-400">{{ formatNumber(calculatedLevelUpXp) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-2 border-b border-slate-600/50">
|
||||
<span class="text-sm text-slate-300">{{ t('gamification.config.social_credit') }}</span>
|
||||
<span class="text-lg font-bold text-violet-400">{{ formatNumber(calculatedSocialCredit) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<span class="text-sm text-slate-300">{{ t('gamification.config.days_to_level_up') }}</span>
|
||||
<span class="text-lg font-bold text-cyan-400">{{ calculatedDaysToLevelUp }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Raw JSON Editor (Collapsible) -->
|
||||
<div class="bg-slate-800 rounded-xl border border-slate-700 p-6">
|
||||
<button @click="showRawJson = !showRawJson" class="flex items-center justify-between w-full">
|
||||
<h2 class="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
{{ t('gamification.config.raw_json_editor') }}
|
||||
</h2>
|
||||
<svg class="w-5 h-5 text-slate-400 transition-transform" :class="showRawJson ? 'rotate-180' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showRawJson" class="mt-4">
|
||||
<textarea
|
||||
v-model="rawJson"
|
||||
rows="20"
|
||||
class="w-full px-3 py-2 bg-slate-900 border border-slate-600 rounded-lg text-green-400 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-amber-500/50 resize-none"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
<div v-if="rawJsonError" class="mt-2 bg-rose-500/10 border border-rose-500/30 rounded-lg p-3">
|
||||
<p class="text-sm text-rose-400 font-mono">{{ rawJsonError }}</p>
|
||||
</div>
|
||||
<button @click="applyRawJson" class="mt-3 px-4 py-2 bg-amber-600 hover:bg-amber-500 text-white rounded-lg text-sm font-medium transition">
|
||||
{{ t('gamification.config.apply_json') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Form Error -->
|
||||
<div v-if="formError" class="fixed bottom-6 right-6 z-50 bg-rose-600 text-white px-4 py-3 rounded-lg shadow-lg text-sm flex items-center gap-2">
|
||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{{ formError }}
|
||||
</div>
|
||||
|
||||
<!-- Success Toast -->
|
||||
<div v-if="toast" class="fixed bottom-6 right-6 z-50 bg-emerald-600 text-white px-4 py-3 rounded-lg shadow-lg text-sm flex items-center gap-2 animate-slide-up">
|
||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{{ toast }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface XpLogic {
|
||||
baseXp: number
|
||||
exponent: number
|
||||
}
|
||||
|
||||
interface PenaltyThresholds {
|
||||
level1: number
|
||||
level2: number
|
||||
level3: number
|
||||
}
|
||||
|
||||
interface PenaltyMultipliers {
|
||||
[key: string]: number
|
||||
}
|
||||
|
||||
interface PenaltyLogic {
|
||||
recoveryRate: number
|
||||
thresholds: PenaltyThresholds
|
||||
multipliers: PenaltyMultipliers
|
||||
}
|
||||
|
||||
interface ConversionLogic {
|
||||
socialToCreditRate: number
|
||||
}
|
||||
|
||||
interface LevelRewards {
|
||||
creditsPer10Levels: number
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const error = ref(false)
|
||||
const saving = ref(false)
|
||||
const formError = ref('')
|
||||
const toast = ref('')
|
||||
const lastSaved = ref('')
|
||||
const showRawJson = ref(false)
|
||||
const rawJson = ref('')
|
||||
const rawJsonError = ref('')
|
||||
|
||||
const multiplierKeys = ['L0', 'L1', 'L2', 'L3']
|
||||
|
||||
const xpLogic = reactive<XpLogic>({ baseXp: 500, exponent: 1.5 })
|
||||
const penaltyLogic = reactive<PenaltyLogic>({
|
||||
recoveryRate: 0.5,
|
||||
thresholds: { level1: 100, level2: 500, level3: 1000 },
|
||||
multipliers: { L0: 1.0, L1: 0.5, L2: 0.1, L3: 0.0 },
|
||||
})
|
||||
const conversionLogic = reactive<ConversionLogic>({ socialToCreditRate: 100 })
|
||||
const levelRewards = reactive<LevelRewards>({ creditsPer10Levels: 50 })
|
||||
|
||||
const calculator = reactive({ level: 5, activities: 3, socialPoints: 50 })
|
||||
|
||||
const calculatedDailyXp = computed(() => {
|
||||
return Math.round(xpLogic.baseXp * Math.pow(calculator.activities, xpLogic.exponent))
|
||||
})
|
||||
|
||||
const calculatedLevelUpXp = computed(() => {
|
||||
return calculator.level * 1000
|
||||
})
|
||||
|
||||
const calculatedSocialCredit = computed(() => {
|
||||
return conversionLogic.socialToCreditRate * calculator.socialPoints
|
||||
})
|
||||
|
||||
const calculatedDaysToLevelUp = computed(() => {
|
||||
const daily = calculatedDailyXp.value
|
||||
if (daily <= 0) return '∞'
|
||||
return Math.ceil(calculatedLevelUpXp.value / daily) + ' ' + t('gamification.config.days')
|
||||
})
|
||||
|
||||
function getHeaders(): Record<string, string> {
|
||||
const tokenCookie = useCookie('access_token')
|
||||
const token = tokenCookie.value
|
||||
return token ? { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' }
|
||||
}
|
||||
|
||||
function showToast(msg: string) {
|
||||
toast.value = msg
|
||||
setTimeout(() => { toast.value = '' }, 3000)
|
||||
}
|
||||
|
||||
function buildConfigObject(): Record<string, any> {
|
||||
return {
|
||||
xp_logic: {
|
||||
base_xp: xpLogic.baseXp,
|
||||
exponent: xpLogic.exponent,
|
||||
},
|
||||
penalty_logic: {
|
||||
recovery_rate: penaltyLogic.recoveryRate,
|
||||
thresholds: {
|
||||
level_1: penaltyLogic.thresholds.level1,
|
||||
level_2: penaltyLogic.thresholds.level2,
|
||||
level_3: penaltyLogic.thresholds.level3,
|
||||
},
|
||||
multipliers: { ...penaltyLogic.multipliers },
|
||||
},
|
||||
conversion_logic: {
|
||||
social_to_credit_rate: conversionLogic.socialToCreditRate,
|
||||
},
|
||||
level_rewards: {
|
||||
credits_per_10_levels: levelRewards.creditsPer10Levels,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function applyConfigToForm(cfg: Record<string, any>) {
|
||||
if (cfg.xp_logic) {
|
||||
xpLogic.baseXp = cfg.xp_logic.base_xp ?? 500
|
||||
xpLogic.exponent = cfg.xp_logic.exponent ?? 1.5
|
||||
}
|
||||
if (cfg.penalty_logic) {
|
||||
penaltyLogic.recoveryRate = cfg.penalty_logic.recovery_rate ?? 0.5
|
||||
if (cfg.penalty_logic.thresholds) {
|
||||
penaltyLogic.thresholds.level1 = cfg.penalty_logic.thresholds.level_1 ?? 100
|
||||
penaltyLogic.thresholds.level2 = cfg.penalty_logic.thresholds.level_2 ?? 500
|
||||
penaltyLogic.thresholds.level3 = cfg.penalty_logic.thresholds.level_3 ?? 1000
|
||||
}
|
||||
if (cfg.penalty_logic.multipliers) {
|
||||
for (const key of multiplierKeys) {
|
||||
penaltyLogic.multipliers[key] = cfg.penalty_logic.multipliers[key] ?? 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cfg.conversion_logic) {
|
||||
conversionLogic.socialToCreditRate = cfg.conversion_logic.social_to_credit_rate ?? 100
|
||||
}
|
||||
if (cfg.level_rewards) {
|
||||
levelRewards.creditsPer10Levels = cfg.level_rewards.credits_per_10_levels ?? 50
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchConfig() {
|
||||
loading.value = true
|
||||
error.value = false
|
||||
try {
|
||||
const data = await $fetch<{ key: string; value: Record<string, any> }>('/api/v1/admin/gamification/master-config', { headers: getHeaders() })
|
||||
if (data.value && Object.keys(data.value).length > 0) {
|
||||
applyConfigToForm(data.value)
|
||||
}
|
||||
rawJson.value = JSON.stringify(buildConfigObject(), null, 2)
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch master config:', e)
|
||||
error.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
saving.value = true
|
||||
formError.value = ''
|
||||
try {
|
||||
const config = buildConfigObject()
|
||||
await $fetch('/api/v1/admin/gamification/master-config', {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: { config },
|
||||
})
|
||||
lastSaved.value = new Date().toLocaleString('hu-HU')
|
||||
rawJson.value = JSON.stringify(config, null, 2)
|
||||
showToast(t('gamification.config.config_updated'))
|
||||
} catch (e: any) {
|
||||
formError.value = t('gamification.config.save_error')
|
||||
console.error('Failed to save master config:', e)
|
||||
setTimeout(() => { formError.value = '' }, 4000)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function applyRawJson() {
|
||||
rawJsonError.value = ''
|
||||
try {
|
||||
const parsed = JSON.parse(rawJson.value)
|
||||
applyConfigToForm(parsed)
|
||||
showRawJson.value = false
|
||||
showToast(t('gamification.config.json_applied'))
|
||||
} catch (e: any) {
|
||||
rawJsonError.value = t('gamification.config.invalid_json') + ' ' + (e.message || '')
|
||||
}
|
||||
}
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
return new Intl.NumberFormat('hu-HU').format(n)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(1rem);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user