1
0
Files
TaskBoard/front_vue/src/components/Board.vue
Falknat 301e179160 Инициализация проекта
Загрузка проекта на GIT
2026-01-11 15:01:35 +07:00

218 lines
6.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="board">
<div class="columns">
<Column
v-for="column in filteredColumns"
:key="column.id"
:column="column"
:departments="departments"
:labels="labels"
@drop-card="handleDropCard"
@open-task="(card) => emit('open-task', { card, columnId: column.id })"
@create-task="emit('create-task', column.id)"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUpdated, watch } from 'vue'
import Column from './Column.vue'
const props = defineProps({
activeDepartment: Number,
departments: {
type: Array,
default: () => []
},
labels: {
type: Array,
default: () => []
},
columns: {
type: Array,
default: () => []
},
cards: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['stats-updated', 'open-task', 'create-task'])
const refreshIcons = () => {
if (window.lucide) {
window.lucide.createIcons()
}
}
onMounted(refreshIcons)
onUpdated(refreshIcons)
// Локальная копия карточек для optimistic UI
const localCards = ref([])
// Синхронизируем с props при загрузке данных
watch(() => props.cards, (newCards) => {
if (newCards.length > 0 && localCards.value.length === 0) {
// Первая загрузка - копируем данные и добавляем order если нет
localCards.value = JSON.parse(JSON.stringify(newCards)).map((card, idx) => ({
...card,
order: card.order ?? idx
}))
}
}, { immediate: true })
// Собираем колонки с карточками (используем localCards, сортируем по order)
const columnsWithCards = computed(() => {
return props.columns.map(col => ({
id: col.id,
title: col.name,
color: col.color,
cards: localCards.value
.filter(card => card.column_id === col.id)
.sort((a, b) => a.order - b.order)
.map(card => ({
id: card.id_card,
title: card.title,
description: card.descript,
details: card.descript_full,
departmentId: card.id_department,
labelId: card.id_label,
assignee: card.avatar_img,
dueDate: card.date,
dateCreate: card.date_create,
files: card.files || (card.file_img || []).map(f => ({
name: f.name,
url: f.url,
size: f.size,
preview: f.url
}))
}))
}))
})
// Фильтруем колонки по активному отделу
const filteredColumns = computed(() => {
if (!props.activeDepartment) return columnsWithCards.value
return columnsWithCards.value.map(col => ({
...col,
cards: col.cards.filter(card => card.departmentId === props.activeDepartment)
}))
})
const filteredTotalTasks = computed(() => {
return filteredColumns.value.reduce((sum, col) => sum + col.cards.length, 0)
})
const inProgressTasks = computed(() => {
const col = filteredColumns.value.find(c => c.id === 3) // В работе
return col ? col.cards.length : 0
})
const completedTasks = computed(() => {
const col = filteredColumns.value.find(c => c.id === 5) // Готово
return col ? col.cards.length : 0
})
// Отправляем статистику в родителя
watch([filteredTotalTasks, inProgressTasks, completedTasks], () => {
emit('stats-updated', {
total: filteredTotalTasks.value,
inProgress: inProgressTasks.value,
done: completedTasks.value
})
}, { immediate: true })
const handleDropCard = ({ cardId, fromColumnId, toColumnId, toIndex }) => {
const card = localCards.value.find(c => c.id_card === cardId)
if (!card) return
// Меняем колонку
card.column_id = toColumnId
// Получаем карточки целевой колонки (без перемещаемой)
const columnCards = localCards.value
.filter(c => c.column_id === toColumnId && c.id_card !== cardId)
.sort((a, b) => a.order - b.order)
// Вставляем карточку в нужную позицию и пересчитываем order
columnCards.splice(toIndex, 0, card)
columnCards.forEach((c, idx) => {
c.order = idx
})
// TODO: отправить изменение на сервер
}
// Генератор id для новых карточек
let nextCardId = 100
// Методы для модалки
const saveTask = (taskData, columnId) => {
if (taskData.id) {
// Редактирование существующей карточки
const card = localCards.value.find(c => c.id_card === taskData.id)
if (card) {
card.title = taskData.title
card.descript = taskData.description
card.descript_full = taskData.details
card.id_department = taskData.departmentId
card.id_label = taskData.labelId
card.date = taskData.dueDate
card.avatar_img = taskData.assignee
card.files = taskData.files || []
}
} else {
// Создание новой карточки (в конец колонки)
const columnCards = localCards.value.filter(c => c.column_id === columnId)
const maxOrder = columnCards.length > 0
? Math.max(...columnCards.map(c => c.order)) + 1
: 0
localCards.value.push({
id_card: nextCardId++,
id_department: taskData.departmentId,
id_label: taskData.labelId,
title: taskData.title,
descript: taskData.description,
descript_full: taskData.details,
avatar_img: taskData.assignee,
column_id: columnId,
date: taskData.dueDate,
date_create: new Date().toISOString().split('T')[0],
order: maxOrder,
files: taskData.files || []
})
}
// TODO: отправить на сервер
}
const deleteTask = (cardId, columnId) => {
const index = localCards.value.findIndex(c => c.id_card === cardId)
if (index !== -1) {
localCards.value.splice(index, 1)
}
// TODO: отправить на сервер
}
defineExpose({ saveTask, deleteTask })
</script>
<style scoped>
.board {
min-width: max-content;
}
.columns {
display: flex;
gap: 20px;
align-items: flex-start;
}
</style>