546 lines
23 KiB
Vue
546 lines
23 KiB
Vue
<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>
|