feat: enhance task persistence and implement centralized kanban board

This commit is contained in:
2026-04-28 21:55:37 +05:00
parent 7da3fc8ff4
commit 1f87f0d6e9
9 changed files with 425 additions and 45 deletions
+170
View File
@@ -0,0 +1,170 @@
<template>
<div class="min-h-screen bg-slate-900 text-slate-200">
<div class="max-w-7xl mx-auto px-4 py-8">
<div class="flex items-center justify-between mb-8">
<h1 class="text-3xl font-bold text-white flex items-center gap-3">
<span class="text-blue-500">📋</span> Мои Задачи
</h1>
</div>
<div v-if="loading" class="flex items-center justify-center h-64">
<div class="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
</div>
<div v-else-if="tasks.length === 0" class="flex flex-col items-center justify-center h-64 bg-slate-800 rounded-xl border border-slate-700">
<span class="text-4xl mb-4">📭</span>
<h3 class="text-xl font-medium text-white mb-2">У вас пока нет задач</h3>
<p class="text-slate-400">Вам не назначено ни одной задачи в проектах.</p>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Kanban Columns -->
<div v-for="col in columns" :key="col.id" class="bg-slate-800 rounded-xl border border-slate-700 flex flex-col max-h-[80vh]">
<div class="p-4 border-b border-slate-700 flex items-center justify-between sticky top-0 bg-slate-800 rounded-t-xl z-10">
<h3 class="font-bold text-white flex items-center gap-2">
<span :class="col.color">{{ col.icon }}</span> {{ col.title }}
</h3>
<span class="bg-slate-700 text-slate-300 text-xs px-2 py-1 rounded-full font-medium">
{{ getTasksForColumn(col.id).length }}
</span>
</div>
<div class="p-4 flex-1 overflow-y-auto space-y-4 custom-scrollbar">
<div
v-for="task in getTasksForColumn(col.id)"
:key="task.id"
class="bg-slate-900 border border-slate-600 rounded-lg p-4 shadow-sm hover:border-blue-500 transition-colors cursor-pointer group"
@click="openTask(task)"
>
<div class="flex items-start justify-between mb-2">
<div class="text-xs font-medium px-2 py-0.5 rounded" :class="getPriorityClass(task)">
{{ getPriorityLabel(task) }}
</div>
<div class="text-[10px] text-slate-500 font-mono">{{ task.id.split('-')[1] || task.id }}</div>
</div>
<h4 class="font-medium text-white mb-2 line-clamp-2">{{ getTaskTitle(task) }}</h4>
<div class="flex items-center justify-between mt-4">
<div class="text-xs text-slate-400 flex items-center gap-1">
<span>🗺️</span>
<span class="truncate max-w-[120px]">{{ task.canvasName }}</span>
</div>
<div v-if="task.alarm || task.data?.alarm" class="text-xs text-orange-400 flex items-center gap-1" :title="new Date(task.alarm || task.data?.alarm).toLocaleString('ru-RU')">
<span>⏰</span>
<span>{{ formatShortDate(task.alarm || task.data?.alarm) }}</span>
</div>
</div>
</div>
<div v-if="getTasksForColumn(col.id).length === 0" class="text-center py-6 border-2 border-dashed border-slate-700 rounded-lg">
<span class="text-slate-500 text-sm">Нет задач</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
const auth = useAuth()
const loading = ref(true)
const tasks = ref([])
const columns = [
{ id: 'todo', title: 'К выполнению', icon: '📋', color: 'text-slate-400' },
{ id: 'in_progress', title: 'В работе', icon: '⏳', color: 'text-blue-400' },
{ id: 'review', title: 'На проверке', icon: '👀', color: 'text-purple-400' },
{ id: 'blocked', title: 'Заблокировано', icon: '⛔', color: 'text-red-400' },
{ id: 'completed', title: 'Завершено', icon: '✅', color: 'text-emerald-400' }
]
// Fetch all tasks for current user
const fetchTasks = async () => {
try {
const res = await fetch('/api/tasks', {
headers: { Authorization: `Bearer ${auth.token.value}` }
})
if (res.ok) {
const data = await res.json()
tasks.value = data.tasks || []
}
} catch (e) {
console.error('Failed to load tasks', e)
} finally {
loading.value = false
}
}
const getTasksForColumn = (state) => {
return tasks.value.filter(t => {
const taskState = t.state || t.data?.state || 'todo'
return taskState === state
})
}
const getTaskTitle = (task) => {
if (task.label) return task.label
const text = task.text || task.data?.text
if (text) {
const lines = text.split('\n')
const firstLine = lines.find(l => l.trim().startsWith('# ')) || lines.find(l => l.trim())
return firstLine ? firstLine.replace('# ', '').trim().slice(0, 50) : 'Без названия'
}
return 'Задача'
}
const getPriorityClass = (task) => {
const priority = task.priority || task.data?.priority
if (priority === 'high') return 'bg-red-900/50 text-red-300'
if (priority === 'low') return 'bg-slate-700 text-slate-300'
return 'bg-blue-900/50 text-blue-300' // medium
}
const getPriorityLabel = (task) => {
const priority = task.priority || task.data?.priority
if (priority === 'high') return 'Высокий'
if (priority === 'low') return 'Низкий'
return 'Средний'
}
const formatShortDate = (dateStr) => {
try {
const d = new Date(dateStr)
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' })
} catch {
return dateStr
}
}
const openTask = (task) => {
navigateTo(`/canvas/${task.canvasId}`)
}
onMounted(async () => {
if (!auth.user.value) {
await auth.init()
}
await fetchTasks()
})
</script>
<style scoped>
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #475569 transparent;
}
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #475569;
border-radius: 4px;
}
</style>