feat: restructure into XFlow monorepo with Nuxt 4, Modular Onion Monolith, Android client, and Desktop CLI
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-slate-950 p-6">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-slate-100">XFlow Admin Panel</h1>
|
||||
<div class="flex items-center gap-2 text-sm text-slate-400">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||
Online
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-xl p-4">
|
||||
<div class="text-3xl font-bold text-blue-400">{{ stats.users }}</div>
|
||||
<div class="text-sm text-slate-400 mt-1">Registered Users</div>
|
||||
</div>
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-xl p-4">
|
||||
<div class="text-3xl font-bold text-emerald-400">{{ stats.canvases }}</div>
|
||||
<div class="text-sm text-slate-400 mt-1">Canvases</div>
|
||||
</div>
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-xl p-4">
|
||||
<div class="text-3xl font-bold text-yellow-400">{{ stats.nodes }}</div>
|
||||
<div class="text-sm text-slate-400 mt-1">Total Nodes</div>
|
||||
</div>
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-xl p-4">
|
||||
<div class="text-3xl font-bold text-purple-400">{{ stats.pushes }}</div>
|
||||
<div class="text-sm text-slate-400 mt-1">Push Subscribers</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="flex gap-4 mb-6">
|
||||
<NuxtLink
|
||||
to="/admin/groups"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition flex items-center gap-2"
|
||||
>
|
||||
👥 Manage Groups & Teams
|
||||
</NuxtLink>
|
||||
<button
|
||||
@click="refreshData"
|
||||
class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg font-medium transition"
|
||||
>
|
||||
🔄 Refresh Data
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Users -->
|
||||
<div class="lg:col-span-2 bg-slate-900 border border-slate-700 rounded-xl overflow-hidden">
|
||||
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-slate-200">Users</h2>
|
||||
<button @click="refreshData" class="text-sm text-blue-400 hover:text-blue-300">Refresh</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-slate-800 text-slate-400">
|
||||
<tr>
|
||||
<th class="text-left p-3">Email</th>
|
||||
<th class="text-left p-3">Username</th>
|
||||
<th class="text-left p-3">Role</th>
|
||||
<th class="text-left p-3">ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-slate-300">
|
||||
<tr v-for="user in users" :key="user.id" class="border-t border-slate-800">
|
||||
<td class="p-3">{{ user.email }}</td>
|
||||
<td class="p-3">{{ user.username }}</td>
|
||||
<td class="p-3">
|
||||
<span :class="roleBadge(user.role)">{{ user.role }}</span>
|
||||
</td>
|
||||
<td class="p-3 text-xs text-slate-500 font-mono">{{ user.id }}</td>
|
||||
</tr>
|
||||
<tr v-if="users.length === 0">
|
||||
<td colspan="4" class="p-6 text-center text-slate-500">No users found</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Canvases -->
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-xl overflow-hidden">
|
||||
<div class="p-4 border-b border-slate-700">
|
||||
<h2 class="text-lg font-semibold text-slate-200">Canvases</h2>
|
||||
</div>
|
||||
<div class="divide-y divide-slate-800">
|
||||
<div v-for="canvas in canvases" :key="canvas.id" class="p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<NuxtLink :to="`/canvas/${canvas.id}`" class="text-blue-400 hover:text-blue-300 font-medium">
|
||||
{{ canvas.name }}
|
||||
</NuxtLink>
|
||||
<span class="text-xs text-slate-500">{{ canvas.nodeCount }} nodes</span>
|
||||
</div>
|
||||
<div class="text-xs text-slate-500 mt-1">{{ formatDate(canvas.updatedAt) }}</div>
|
||||
</div>
|
||||
<div v-if="canvases.length === 0" class="p-6 text-center text-slate-500">
|
||||
No canvases yet
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
middleware: ['auth', 'admin']
|
||||
})
|
||||
|
||||
const users = ref([])
|
||||
const canvases = ref([])
|
||||
const stats = reactive({ users: 0, canvases: 0, nodes: 0, pushes: 0 })
|
||||
|
||||
const auth = useAuth()
|
||||
|
||||
const refreshData = async () => {
|
||||
if (!auth.token.value) return
|
||||
|
||||
try {
|
||||
const data = await $fetch('/api/admin/stats', {
|
||||
headers: { Authorization: `Bearer ${auth.token.value}` }
|
||||
})
|
||||
users.value = data.users || []
|
||||
canvases.value = data.canvases || []
|
||||
stats.users = data.stats?.users || 0
|
||||
stats.canvases = data.stats?.canvases || 0
|
||||
stats.nodes = data.stats?.nodes || 0
|
||||
stats.pushes = data.stats?.pushes || 0
|
||||
} catch (e) {
|
||||
console.error('Failed to load admin data', e)
|
||||
}
|
||||
}
|
||||
|
||||
const roleBadge = (role) => {
|
||||
return role === 'admin'
|
||||
? 'px-2 py-0.5 bg-red-900/50 text-red-300 rounded text-xs'
|
||||
: 'px-2 py-0.5 bg-slate-700 text-slate-300 rounded text-xs'
|
||||
}
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return 'N/A'
|
||||
return new Date(date).toLocaleDateString('ru-RU')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Ждем инициализации пользователя если нужно
|
||||
if (!auth.user.value) {
|
||||
await auth.initAuth()
|
||||
}
|
||||
refreshData()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user