feat: stabilize admin dashboard, refactor group management, implement PWA, and fix routing

This commit is contained in:
2026-05-03 19:51:18 +05:00
parent 622ca04170
commit 0598e07823
36 changed files with 1387 additions and 467 deletions
+25 -2
View File
@@ -23,7 +23,19 @@
<div class="flex-1 flex flex-col relative min-w-0">
<!-- Toolbar -->
<div class="h-10 bg-slate-900 border-b border-slate-700 flex items-center px-3 gap-2 z-20">
<input v-model="canvasName" @change="saveCanvas" class="text-sm font-semibold text-slate-300 mr-2 bg-transparent border border-transparent hover:border-slate-600 focus:border-blue-500 focus:bg-slate-800 outline-none rounded px-2 py-1 w-64 transition" title="Переименовать карту" />
<input v-model="canvasName" @change="saveCanvas" class="text-sm font-semibold text-slate-300 mr-2 bg-transparent border border-transparent hover:border-slate-600 focus:border-blue-500 focus:bg-slate-800 outline-none rounded px-2 py-1 w-48 transition" title="Переименовать карту" />
<!-- Group Selection -->
<div class="flex items-center gap-1 bg-slate-800 border border-slate-700 rounded px-2 py-1">
<span class="text-[10px] text-slate-500 uppercase font-bold mr-1">Группа:</span>
<select v-model="selectedGroupId" @change="saveCanvas" class="bg-transparent text-xs text-slate-300 outline-none cursor-pointer hover:text-white transition">
<option :value="null">Личная (нет)</option>
<option v-for="g in groupsList" :key="g.id" :value="g.id">
👥 {{ g.name }}
</option>
</select>
</div>
<span v-if="isDirty" class="px-2 py-1 bg-yellow-600 text-yellow-100 rounded text-xs font-medium">
● Несохранено
</span>
@@ -204,6 +216,8 @@ const authHeaders = computed(() => ({ Authorization: `Bearer ${auth.token.value}
const elements = ref([])
const selectedNode = ref(null)
const canvasName = ref(props.canvasId)
const selectedGroupId = ref(null)
const groupsList = ref([])
const nodeTypes = [
{ type: 'text', name: 'Текст', icon: '📝', description: 'Markdown заметка' },
@@ -219,6 +233,7 @@ const loadData = async () => {
const data = await loadCanvas(props.canvasId)
elements.value = transformToFlowElements(data)
canvasName.value = data.name || props.canvasId
selectedGroupId.value = data.groupId || null
// Mark initial state as saved
markAsSaved(data)
}
@@ -639,7 +654,7 @@ const saveCanvas = async () => {
console.log('[DEBUG saveCanvas] Sending PUT request...')
const res = await $fetch(`/api/canvas/${props.canvasId}`, {
method: 'PUT',
body: { ...obsidianCanvas, name: canvasName.value },
body: { ...obsidianCanvas, name: canvasName.value, groupId: selectedGroupId.value },
headers: authHeaders.value
})
console.log('[DEBUG saveCanvas] PUT request successful!', res)
@@ -690,6 +705,14 @@ onMounted(async () => {
} catch (e) {
console.error('Failed to load canvases', e)
}
// Load groups for assignment
try {
const data = await $fetch('/api/groups')
groupsList.value = data.groups || []
} catch (e) {
console.error('Failed to load groups', e)
}
})
</script>
+51
View File
@@ -0,0 +1,51 @@
<template>
<div v-if="deferredPrompt && !isStandalone" class="fixed bottom-6 right-6 z-50">
<button
@click="installApp"
class="bg-blue-600 hover:bg-blue-500 text-white px-6 py-3 rounded-2xl font-bold shadow-2xl flex items-center gap-3 transition-all transform hover:scale-105 active:scale-95 group"
>
<span class="text-xl group-hover:rotate-12 transition-transform">📲</span>
<span>Установить XFlow</span>
</button>
</div>
</template>
<script setup>
const deferredPrompt = ref(null)
const isStandalone = ref(false)
onMounted(() => {
isStandalone.value = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
deferredPrompt.value = e
})
window.addEventListener('appinstalled', () => {
deferredPrompt.value = null
})
})
const installApp = async () => {
if (!deferredPrompt.value) {
alert('Браузер еще не подготовил установку. Пожалуйста, подождите 10-20 секунд или проверьте вкладку Application в консоли.')
return
}
deferredPrompt.value.prompt()
const { outcome } = await deferredPrompt.value.userChoice
console.log(`PWA: user choice outcome: ${outcome}`)
deferredPrompt.value = null
}
</script>
<style scoped>
@keyframes bounce-subtle {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
.animate-bounce-subtle {
animation: bounce-subtle 3s ease-in-out infinite;
}
</style>