feat: enhance task persistence and implement centralized kanban board
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user