feat: Add teleport config frontend — Pinia store, views, 2 components, router + nav
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
All checks were successful
Test Asgard Runner / test (push) Successful in 2s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
195
frontend/src/components/teleport/PermissionGroupEditor.vue
Normal file
195
frontend/src/components/teleport/PermissionGroupEditor.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
configData: Record<string, any>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:configData': [configData: Record<string, any>]
|
||||
}>()
|
||||
|
||||
const newGroupName = ref('')
|
||||
|
||||
// Merge all VIP maps by key name to compute the unified group list
|
||||
const groups = computed(() => {
|
||||
const homesLimits: Record<string, number> = props.configData?.Home?.VIPHomesLimits || {}
|
||||
const cooldowns: Record<string, number> = props.configData?.TPR?.VIPCooldowns || {}
|
||||
const countdowns: Record<string, number> = props.configData?.TPR?.VIPCountdowns || {}
|
||||
const dailyLimits: Record<string, number> = props.configData?.TPR?.VIPDailyLimits || {}
|
||||
|
||||
const allKeys = new Set([
|
||||
...Object.keys(homesLimits),
|
||||
...Object.keys(cooldowns),
|
||||
...Object.keys(countdowns),
|
||||
...Object.keys(dailyLimits),
|
||||
])
|
||||
|
||||
return Array.from(allKeys).map(name => ({
|
||||
name,
|
||||
homesLimit: homesLimits[name] ?? 5,
|
||||
cooldown: cooldowns[name] ?? 300,
|
||||
countdown: countdowns[name] ?? 5,
|
||||
dailyLimit: dailyLimits[name] ?? 10,
|
||||
}))
|
||||
})
|
||||
|
||||
function ensurePaths(data: Record<string, any>) {
|
||||
if (!data.Home) data.Home = {}
|
||||
if (!data.Home.VIPHomesLimits) data.Home.VIPHomesLimits = {}
|
||||
if (!data.TPR) data.TPR = {}
|
||||
if (!data.TPR.VIPCooldowns) data.TPR.VIPCooldowns = {}
|
||||
if (!data.TPR.VIPCountdowns) data.TPR.VIPCountdowns = {}
|
||||
if (!data.TPR.VIPDailyLimits) data.TPR.VIPDailyLimits = {}
|
||||
}
|
||||
|
||||
function addGroup() {
|
||||
const name = newGroupName.value.trim()
|
||||
if (!name) return
|
||||
// Check if already exists
|
||||
if (groups.value.some(g => g.name === name)) return
|
||||
|
||||
const updated = { ...props.configData }
|
||||
ensurePaths(updated)
|
||||
updated.Home.VIPHomesLimits[name] = 5
|
||||
updated.TPR.VIPCooldowns[name] = 300
|
||||
updated.TPR.VIPCountdowns[name] = 5
|
||||
updated.TPR.VIPDailyLimits[name] = 10
|
||||
emit('update:configData', updated)
|
||||
newGroupName.value = ''
|
||||
}
|
||||
|
||||
function removeGroup(name: string) {
|
||||
if (!confirm(`Remove VIP group "${name}"?`)) return
|
||||
const updated = { ...props.configData }
|
||||
ensurePaths(updated)
|
||||
delete updated.Home.VIPHomesLimits[name]
|
||||
delete updated.TPR.VIPCooldowns[name]
|
||||
delete updated.TPR.VIPCountdowns[name]
|
||||
delete updated.TPR.VIPDailyLimits[name]
|
||||
emit('update:configData', updated)
|
||||
}
|
||||
|
||||
function updateField(groupName: string, field: string, value: number) {
|
||||
const updated = { ...props.configData }
|
||||
ensurePaths(updated)
|
||||
|
||||
switch (field) {
|
||||
case 'homesLimit':
|
||||
updated.Home.VIPHomesLimits[groupName] = value
|
||||
break
|
||||
case 'cooldown':
|
||||
updated.TPR.VIPCooldowns[groupName] = value
|
||||
break
|
||||
case 'countdown':
|
||||
updated.TPR.VIPCountdowns[groupName] = value
|
||||
break
|
||||
case 'dailyLimit':
|
||||
updated.TPR.VIPDailyLimits[groupName] = value
|
||||
break
|
||||
}
|
||||
|
||||
emit('update:configData', updated)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-neutral-300 uppercase tracking-wider">VIP Permission Groups</h3>
|
||||
</div>
|
||||
|
||||
<!-- Add Group -->
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="newGroupName"
|
||||
placeholder="New group name (e.g. vip, vip+, mvp)..."
|
||||
class="flex-1 bg-neutral-800 border border-neutral-700 rounded-lg px-3 py-2 text-sm text-neutral-200"
|
||||
@keydown.enter="addGroup"
|
||||
/>
|
||||
<button
|
||||
@click="addGroup"
|
||||
:disabled="!newGroupName.trim()"
|
||||
class="flex items-center gap-1 px-3 py-2 bg-oxide-500 text-white rounded-lg hover:bg-oxide-600 disabled:opacity-50 text-sm"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
Add Group
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="groups.length === 0" class="bg-neutral-900 border border-neutral-800 rounded-xl p-8 text-center text-neutral-500 text-sm">
|
||||
No VIP groups defined. Add groups to configure per-permission teleport limits, cooldowns, and countdowns.
|
||||
</div>
|
||||
|
||||
<!-- Groups Table -->
|
||||
<div v-else class="bg-neutral-900 border border-neutral-800 rounded-xl overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-neutral-800">
|
||||
<th class="text-left py-3 px-4 text-neutral-500 font-medium">Group Name</th>
|
||||
<th class="text-center py-3 px-4 text-neutral-500 font-medium w-28">Homes Limit</th>
|
||||
<th class="text-center py-3 px-4 text-neutral-500 font-medium w-28">Cooldown (s)</th>
|
||||
<th class="text-center py-3 px-4 text-neutral-500 font-medium w-28">Countdown (s)</th>
|
||||
<th class="text-center py-3 px-4 text-neutral-500 font-medium w-28">Daily Limit</th>
|
||||
<th class="w-12"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="group in groups"
|
||||
:key="group.name"
|
||||
class="border-b border-neutral-800/50"
|
||||
>
|
||||
<td class="py-3 px-4 text-neutral-200 font-medium">{{ group.name }}</td>
|
||||
<td class="py-3 px-4">
|
||||
<input
|
||||
type="number"
|
||||
:value="group.homesLimit"
|
||||
@input="updateField(group.name, 'homesLimit', Number(($event.target as HTMLInputElement).value))"
|
||||
class="w-full bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-center text-neutral-200"
|
||||
min="0"
|
||||
/>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<input
|
||||
type="number"
|
||||
:value="group.cooldown"
|
||||
@input="updateField(group.name, 'cooldown', Number(($event.target as HTMLInputElement).value))"
|
||||
class="w-full bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-center text-neutral-200"
|
||||
min="0"
|
||||
/>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<input
|
||||
type="number"
|
||||
:value="group.countdown"
|
||||
@input="updateField(group.name, 'countdown', Number(($event.target as HTMLInputElement).value))"
|
||||
class="w-full bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-center text-neutral-200"
|
||||
min="0"
|
||||
/>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<input
|
||||
type="number"
|
||||
:value="group.dailyLimit"
|
||||
@input="updateField(group.name, 'dailyLimit', Number(($event.target as HTMLInputElement).value))"
|
||||
class="w-full bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-center text-neutral-200"
|
||||
min="0"
|
||||
/>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<button
|
||||
@click="removeGroup(group.name)"
|
||||
class="text-neutral-600 hover:text-red-400 transition-colors p-1"
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
76
frontend/src/components/teleport/WarpEditor.vue
Normal file
76
frontend/src/components/teleport/WarpEditor.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
warps: Record<string, { x: number; y: number; z: number }>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:warps': [warps: Record<string, { x: number; y: number; z: number }>]
|
||||
}>()
|
||||
|
||||
const newWarpName = ref('')
|
||||
|
||||
function addWarp() {
|
||||
const name = newWarpName.value.trim()
|
||||
if (!name || props.warps[name]) return
|
||||
const updated = { ...props.warps, [name]: { x: 0, y: 0, z: 0 } }
|
||||
emit('update:warps', updated)
|
||||
newWarpName.value = ''
|
||||
}
|
||||
|
||||
function removeWarp(name: string) {
|
||||
const updated = { ...props.warps }
|
||||
delete updated[name]
|
||||
emit('update:warps', updated)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold text-neutral-300 uppercase tracking-wider">Warps</h3>
|
||||
|
||||
<!-- Add Warp -->
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="newWarpName"
|
||||
placeholder="Warp name..."
|
||||
class="flex-1 bg-neutral-800 border border-neutral-700 rounded-lg px-3 py-2 text-sm text-neutral-200"
|
||||
@keydown.enter="addWarp"
|
||||
/>
|
||||
<button
|
||||
@click="addWarp"
|
||||
:disabled="!newWarpName.trim()"
|
||||
class="flex items-center gap-1 px-3 py-2 bg-oxide-500 text-white rounded-lg hover:bg-oxide-600 disabled:opacity-50 text-sm"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Warp List -->
|
||||
<div v-if="Object.keys(warps).length === 0" class="text-neutral-500 text-sm text-center py-4">
|
||||
No warps defined. Add warps here and set coordinates in-game.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(coords, name) in warps"
|
||||
:key="name"
|
||||
class="flex items-center justify-between bg-neutral-800/50 border border-neutral-700/50 rounded-lg px-4 py-3"
|
||||
>
|
||||
<div>
|
||||
<span class="text-neutral-200 font-medium">{{ name }}</span>
|
||||
<span class="text-neutral-500 text-xs ml-3">
|
||||
{{ coords.x.toFixed(1) }}, {{ coords.y.toFixed(1) }}, {{ coords.z.toFixed(1) }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
@click="removeWarp(name as string)"
|
||||
class="text-neutral-600 hover:text-red-400 transition-colors p-1"
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user